{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../../src/core/lsp/tools.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAgC,MAAM,+BAA+B,CAAC;AAK7F,OAAO,EAAsC,aAAa,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAkC/C,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AA2CD,iBAAS,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAKlE;AAqBD,oDAAoD;AACpD,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CA0QpE;AA4CD,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAC","sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport type { AgentTool, AgentToolResult, ToolEffects } from \"@apholdings/jensen-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport { uriToPath } from \"./client.js\";\nimport { getDiagnosticRows } from \"./diagnostics.js\";\nimport { reportAllServers } from \"./discovery.js\";\nimport { applyTextEdits, extractEditsByPath, sha256Content } from \"./rename.js\";\nimport type { LspRuntime } from \"./runtime.js\";\nimport type { LspLocationResult } from \"./types.js\";\n\nconst readEffects: ToolEffects = {\n\treadsWorkspace: true,\n\twritesWorkspace: false,\n\tcreatesFiles: false,\n\tdeletesFiles: false,\n\texecutesProcesses: false,\n\tstartsPersistentProcesses: false,\n\taccessesNetwork: false,\n\tmutatesGit: false,\n\tmutatesExternalState: false,\n\thandlesSecrets: false,\n\tpotentiallyDestructive: false,\n\trequiresExclusiveWorkspaceLease: false,\n\tparallelSafe: true,\n\tscopes: [{ kind: \"workspace\" }],\n};\n\nconst renamePreviewEffects: ToolEffects = { ...readEffects, parallelSafe: true };\n\nconst fileParams = Type.Object(\n\t{\n\t\tfile: Type.String({ description: \"Workspace-relative file path\" }),\n\t\tline: Type.Optional(Type.Integer({ default: 0, description: \"0-based line\" })),\n\t\tcharacter: Type.Optional(Type.Integer({ default: 0, description: \"0-based UTF-16 character\" })),\n\t\tsymbol: Type.Optional(Type.String({ description: \"Symbol query (for workspace symbols)\" })),\n\t\tlanguage: Type.Optional(Type.String({ description: \"Language override\" })),\n\t\tlimit: Type.Optional(Type.Integer({ default: 50, description: \"Max results (bounded)\" })),\n\t},\n\t{ additionalProperties: false },\n);\n\n/** Permissive tool input (superset of every LSP tool's schema fields). */\nexport interface LspToolInput {\n\tfile: string;\n\tline?: number;\n\tcharacter?: number;\n\tsymbol?: string;\n\tlanguage?: string;\n\tlimit?: number;\n\tnewName?: string;\n}\n\nfunction capLimit(limit: number | undefined): number {\n\tconst n = limit ?? 50;\n\treturn Math.max(1, Math.min(n, 500));\n}\n\nfunction locationResults(\n\tlocations: Array<{\n\t\turi: string;\n\t\tstartLine: number;\n\t\tstartCharacter: number;\n\t\tendLine: number;\n\t\tendCharacter: number;\n\t\tsymbol?: string;\n\t}>,\n\tserverId: string,\n\tlanguageId: string,\n\tworkspaceRoot: string,\n\tlimit: number,\n): { results: LspLocationResult[]; truncated: boolean } {\n\tconst seen = new Set<string>();\n\tconst results: LspLocationResult[] = [];\n\tfor (const loc of locations) {\n\t\tconst rel = toWorkspaceRel(loc.uri, workspaceRoot);\n\t\tconst key = `${rel}:${loc.startLine}:${loc.startCharacter}`;\n\t\tif (seen.has(key)) continue;\n\t\tseen.add(key);\n\t\tresults.push({\n\t\t\tworkspaceRelativePath: rel,\n\t\t\tstartLine: loc.startLine,\n\t\t\tstartCharacter: loc.startCharacter,\n\t\t\tendLine: loc.endLine,\n\t\t\tendCharacter: loc.endCharacter,\n\t\t\tsymbol: loc.symbol,\n\t\t\tlanguageId,\n\t\t\tserverId,\n\t\t});\n\t\tif (results.length >= limit) break;\n\t}\n\treturn { results, truncated: results.length >= limit };\n}\n\nfunction toWorkspaceRel(uri: string, workspaceRoot: string): string {\n\tconst raw = uriToPath(uri);\n\tif (!raw) return uri;\n\tconst rel = raw.replace(workspaceRoot.replace(/[\\\\/]+/g, \"/\"), \"\").replace(/^[/\\\\]/, \"\");\n\treturn rel || raw;\n}\n\nfunction toLoc(l: {\n\turi: string;\n\trange: { start: { line: number; character: number }; end: { line: number; character: number } };\n}): {\n\turi: string;\n\tstartLine: number;\n\tstartCharacter: number;\n\tendLine: number;\n\tendCharacter: number;\n} {\n\treturn {\n\t\turi: l.uri,\n\t\tstartLine: l.range.start.line,\n\t\tstartCharacter: l.range.start.character,\n\t\tendLine: l.range.end.line,\n\t\tendCharacter: l.range.end.character,\n\t};\n}\n\n/** Create the nine LSP tools bound to a runtime. */\nexport function createLspTools(runtime: LspRuntime): AgentTool<any>[] {\n\tconst mk = <TSchema>(\n\t\tname: string,\n\t\tdescription: string,\n\t\tparameters: TSchema,\n\t\teffects: ToolEffects,\n\t\texecute: (c: string, p: LspToolInput, signal?: AbortSignal) => Promise<AgentToolResult<any> | string>,\n\t): AgentTool<any> => ({\n\t\tname,\n\t\tlabel: name,\n\t\tdescription,\n\t\tparameters,\n\t\teffects,\n\t\texecute: async (toolCallId, params, signal) => {\n\t\t\tif (typeof execute === \"function\") {\n\t\t\t\tconst r = await execute(toolCallId, params as never as LspToolInput, signal);\n\t\t\t\tif (typeof r === \"string\") return { content: [{ type: \"text\", text: r }], details: {} };\n\t\t\t\treturn r;\n\t\t\t}\n\t\t\treturn { content: [{ type: \"text\", text: \"noop\" }], details: {} };\n\t\t},\n\t});\n\n\tconst unavailable = (lang: string | undefined, reason: string): string =>\n\t\t`LSP server unavailable for ${lang ?? \"file\"}: ${reason}`;\n\n\tconst definition = mk(\n\t\t\"lsp_definition\",\n\t\t\"Resolve the definition locations for a symbol at a file position. Read-only.\",\n\t\tfileParams,\n\t\treadEffects,\n\t\tasync (_c, p) => {\n\t\t\tconst limit = capLimit(p.limit);\n\t\t\tconst sd = await runtime.serverForFile(p.file, p.language ?? null);\n\t\t\tif (!sd.client || !sd.uri) return unavailable(p.language, sd.unavailableReason ?? \"no server\");\n\t\t\tconst locations = await sd.client.definition({ uri: sd.uri, line: p.line ?? 0, character: p.character ?? 0 });\n\t\t\tconst { results, truncated } = locationResults(\n\t\t\t\tlocations.map((l) => toLoc(l)),\n\t\t\t\tsd.serverId ?? \"?\",\n\t\t\t\tsd.languageId ?? \"?\",\n\t\t\t\truntime.workspaceRoot,\n\t\t\t\tlimit,\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: formatLocations(results, truncated) }],\n\t\t\t\tdetails: { locations: results, truncated },\n\t\t\t};\n\t\t},\n\t);\n\n\tconst references = mk(\n\t\t\"lsp_references\",\n\t\t\"Resolve reference locations for a symbol at a file position. Read-only.\",\n\t\tfileParams,\n\t\treadEffects,\n\t\tasync (_c, p) => {\n\t\t\tconst limit = capLimit(p.limit);\n\t\t\tconst sd = await runtime.serverForFile(p.file, p.language ?? null);\n\t\t\tif (!sd.client || !sd.uri) return unavailable(p.language, sd.unavailableReason ?? \"no server\");\n\t\t\tconst locations = await sd.client.references({ uri: sd.uri, line: p.line ?? 0, character: p.character ?? 0 });\n\t\t\tconst { results, truncated } = locationResults(\n\t\t\t\tlocations.map((l) => toLoc(l)),\n\t\t\t\tsd.serverId ?? \"?\",\n\t\t\t\tsd.languageId ?? \"?\",\n\t\t\t\truntime.workspaceRoot,\n\t\t\t\tlimit,\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: formatLocations(results, truncated) }],\n\t\t\t\tdetails: { locations: results, truncated },\n\t\t\t};\n\t\t},\n\t);\n\n\tconst implementations = mk(\n\t\t\"lsp_implementations\",\n\t\t\"Resolve implementation locations for a symbol at a file position. Read-only.\",\n\t\tfileParams,\n\t\treadEffects,\n\t\tasync (_c, p) => {\n\t\t\tconst limit = capLimit(p.limit);\n\t\t\tconst sd = await runtime.serverForFile(p.file, p.language ?? null);\n\t\t\tif (!sd.client || !sd.uri) return unavailable(p.language, sd.unavailableReason ?? \"no server\");\n\t\t\tconst locations = await sd.client.implementations({\n\t\t\t\turi: sd.uri,\n\t\t\t\tline: p.line ?? 0,\n\t\t\t\tcharacter: p.character ?? 0,\n\t\t\t});\n\t\t\tconst { results, truncated } = locationResults(\n\t\t\t\tlocations.map((l) => toLoc(l)),\n\t\t\t\tsd.serverId ?? \"?\",\n\t\t\t\tsd.languageId ?? \"?\",\n\t\t\t\truntime.workspaceRoot,\n\t\t\t\tlimit,\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: formatLocations(results, truncated) }],\n\t\t\t\tdetails: { locations: results, truncated },\n\t\t\t};\n\t\t},\n\t);\n\n\tconst hover = mk(\n\t\t\"lsp_hover\",\n\t\t\"Return hover documentation for a symbol at a file position. Read-only.\",\n\t\tfileParams,\n\t\treadEffects,\n\t\tasync (_c, p) => {\n\t\t\tconst sd = await runtime.serverForFile(p.file, p.language ?? null);\n\t\t\tif (!sd.client || !sd.uri) return unavailable(p.language, sd.unavailableReason ?? \"no server\");\n\t\t\tconst h = await sd.client.hover({ uri: sd.uri, line: p.line ?? 0, character: p.character ?? 0 });\n\t\t\tif (!h) return { content: [{ type: \"text\", text: \"No hover content.\" }], details: { hover: null } };\n\t\t\treturn { content: [{ type: \"text\", text: hoverText(h.contents) }], details: { hover: h } };\n\t\t},\n\t);\n\n\tconst diagnosticsTool = mk(\n\t\t\"lsp_diagnostics\",\n\t\t\"Return current LSP diagnostics for a file. Read-only; never authortive for execution.\",\n\t\tfileParams,\n\t\treadEffects,\n\t\tasync (_c, p) => {\n\t\t\tconst limit = capLimit(p.limit);\n\t\t\tconst sd = await runtime.serverForFile(p.file, p.language ?? null);\n\t\t\tif (!sd.client || !sd.uri) return unavailable(p.language, sd.unavailableReason ?? \"no server\");\n\t\t\tconst paramsList = runtime.manager.diagnosticsForUri(sd.uri);\n\t\t\tconst rows = paramsList\n\t\t\t\t.flatMap((params) => getDiagnosticRows(params, runtime.workspaceRoot, sd.serverId ?? \"?\", limit))\n\t\t\t\t.slice(0, limit);\n\t\t\tconst text =\n\t\t\t\trows.length === 0\n\t\t\t\t\t? \"No diagnostics.\"\n\t\t\t\t\t: rows\n\t\t\t\t\t\t\t.map(\n\t\t\t\t\t\t\t\t(d) =>\n\t\t\t\t\t\t\t\t\t`${d.workspaceRelativePath}:${d.range.start.line}:${d.range.start.character} [${severityName(d.severity)}] ${d.message}`,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.join(\"\\n\");\n\t\t\treturn { content: [{ type: \"text\", text }], details: { diagnostics: rows, truncated: rows.length >= limit } };\n\t\t},\n\t);\n\n\tconst documentSymbols = mk(\n\t\t\"lsp_document_symbols\",\n\t\t\"List symbols declared in a document. Read-only.\",\n\t\tType.Object({ file: Type.String(), limit: Type.Optional(Type.Integer({ default: 200 })) }),\n\t\treadEffects,\n\t\tasync (_c, p) => {\n\t\t\tconst limit = capLimit((p as unknown as { limit?: number }).limit);\n\t\t\tconst sd = await runtime.serverForFile(p.file, p.language ?? null);\n\t\t\tif (!sd.client || !sd.uri) return unavailable(p.language, sd.unavailableReason ?? \"no server\");\n\t\t\tconst symbols = await sd.client.documentSymbols(sd.uri);\n\t\t\tconst names = symbolNames(symbols, limit);\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: names.join(\"\\n\") || \"No symbols.\" }],\n\t\t\t\tdetails: { symbols: symbols.slice(0, limit) },\n\t\t\t};\n\t\t},\n\t);\n\n\tconst workspaceSymbols = mk(\n\t\t\"lsp_workspace_symbols\",\n\t\t\"Query workspace symbols by name. Read-only.\",\n\t\tType.Object({ symbol: Type.String(), limit: Type.Optional(Type.Integer({ default: 100 })) }),\n\t\treadEffects,\n\t\tasync (_c, p) => {\n\t\t\tconst limit = capLimit((p as unknown as { limit?: number }).limit);\n\t\t\tconst sd = await runtime.serverForFile(p.file ?? \"\", p.language ?? null);\n\t\t\tconst query = (p as unknown as { symbol?: string }).symbol ?? \"\";\n\t\t\tif (!sd.client) return unavailable(undefined, sd.unavailableReason ?? \"no server\");\n\t\t\tconst symbols = await sd.client.workspaceSymbols(query);\n\t\t\tconst names = symbols\n\t\t\t\t.slice(0, limit)\n\t\t\t\t.map((s) => `${s.name} @ ${toWorkspaceRel(s.location.uri, runtime.workspaceRoot)}`);\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: names.join(\"\\n\") || \"No matching symbols.\" }],\n\t\t\t\tdetails: { symbols: symbols.slice(0, limit) },\n\t\t\t};\n\t\t},\n\t);\n\n\tconst renamePreview = mk(\n\t\t\"lsp_rename_preview\",\n\t\t\"Produce a read-only preview of a rename. Zero physical mutation; applying uses a transaction.\",\n\t\tType.Object({\n\t\t\tfile: Type.String(),\n\t\t\tline: Type.Optional(Type.Integer({ default: 0 })),\n\t\t\tcharacter: Type.Optional(Type.Integer({ default: 0 })),\n\t\t\tnewName: Type.String(),\n\t\t}),\n\t\trenamePreviewEffects,\n\t\tasync (_c, p) => {\n\t\t\tconst sd = await runtime.serverForFile(p.file, p.language ?? null);\n\t\t\tif (!sd.client || !sd.uri) return unavailable(p.language, sd.unavailableReason ?? \"no server\");\n\t\t\tconst prepare = await sd.client.prepareRename({ uri: sd.uri, line: p.line ?? 0, character: p.character ?? 0 });\n\t\t\tif (!prepare) return \"Rename not applicable at this position.\";\n\t\t\tconst newName = p.newName ?? \"\";\n\t\t\tif (!newName) return \"newName is required.\";\n\t\t\tconst edit = await sd.client.rename({ uri: sd.uri, line: p.line ?? 0, character: p.character ?? 0, newName });\n\t\t\tif (!edit) return \"Rename produced no edits.\";\n\t\t\tconst { byPath, conflicts, unsupported } = extractEditsByPath(edit, runtime.workspaceRoot);\n\t\t\tif (conflicts.length) return `Rename preview rejected: ${conflicts.join(\"; \")}`;\n\t\t\tconst affected: Array<{ file: string; edits: number; preview: string }> = [];\n\t\t\tfor (const [abs, edits] of byPath) {\n\t\t\t\tconst content = await readFile(abs, \"utf-8\");\n\t\t\t\tconst applied = applyTextEdits(content, edits);\n\t\t\t\taffected.push({\n\t\t\t\t\tfile: abs.replace(runtime.workspaceRoot, \"\").replace(/^[/\\\\]/, \"\"),\n\t\t\t\t\tedits: edits.length,\n\t\t\t\t\tpreview: applied.newContent.slice(0, 400),\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: JSON.stringify(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tsymbol: p.newName,\n\t\t\t\t\t\t\t\taffectedFiles: affected,\n\t\t\t\t\t\t\t\ttotalEditCount: affected.reduce((n, f) => n + f.edits, 0),\n\t\t\t\t\t\t\t\tconflicts,\n\t\t\t\t\t\t\t\tunsupportedResourceOperations: unsupported,\n\t\t\t\t\t\t\t\tzeroMutation: true,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tnull,\n\t\t\t\t\t\t\t2,\n\t\t\t\t\t\t),\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdetails: { affected, conflicts, unsupported, zeroMutation: true },\n\t\t\t};\n\t\t},\n\t);\n\n\tconst status = mk(\n\t\t\"lsp_status\",\n\t\t\"Report LSP server discovery and health status. Read-only.\",\n\t\tType.Object({ filter: Type.Optional(Type.String()) }),\n\t\treadEffects,\n\t\tasync () => {\n\t\t\tconst rows: string[] = [];\n\t\t\trows.push(\"language server discovery:\");\n\t\t\tfor (const r of await reportAllServers()) {\n\t\t\t\trows.push(`  ${r.languageId}: ${r.candidate} ${r.available ? \"available\" : \"unavailable\"}`);\n\t\t\t}\n\t\t\trows.push(\"active LSP servers:\");\n\t\t\tfor (const s of runtime.manager.listServers()) {\n\t\t\t\tconst h = runtime.manager.health(s.languageId);\n\t\t\t\trows.push(`  ${s.serverId}: ${s.state} (${h?.lastError ?? \"ok\"})`);\n\t\t\t}\n\t\t\treturn rows.join(\"\\n\");\n\t\t},\n\t);\n\n\treturn [\n\t\tdefinition,\n\t\treferences,\n\t\timplementations,\n\t\thover,\n\t\tdiagnosticsTool,\n\t\tdocumentSymbols,\n\t\tworkspaceSymbols,\n\t\trenamePreview,\n\t\tstatus,\n\t];\n}\n\nfunction formatLocations(results: LspLocationResult[], truncated: boolean): string {\n\tconst lines = results.map(\n\t\t(r) =>\n\t\t\t`${r.workspaceRelativePath}:${r.startLine}:${r.startCharacter}-${r.endLine}:${r.endCharacter}${r.symbol ? ` (${r.symbol})` : \"\"}`,\n\t);\n\tif (truncated) lines.push(\"...(truncated)\");\n\treturn lines.join(\"\\n\") || \"No results.\";\n}\n\nfunction severityName(s: number | undefined): string {\n\treturn s === 1 ? \"ERROR\" : s === 2 ? \"WARNING\" : s === 3 ? \"INFO\" : s === 4 ? \"HINT\" : \"?\";\n}\n\nfunction hoverText(contents: unknown): string {\n\tif (typeof contents === \"string\") return contents;\n\tif (Array.isArray(contents)) return contents.map((c) => (typeof c === \"string\" ? c : (c.value ?? \"\"))).join(\"\\n\\n\");\n\tif (contents && typeof contents === \"object\") {\n\t\tconst v = (contents as { value?: string }).value ?? \"\";\n\t\treturn v;\n\t}\n\treturn \"\";\n}\n\nfunction symbolNames(symbols: unknown, limit: number): string[] {\n\tconst out: string[] = [];\n\tconst walk = (s: unknown) => {\n\t\tif (out.length >= limit) return;\n\t\tif (Array.isArray(s)) {\n\t\t\tfor (const item of s) walk(item);\n\t\t\treturn;\n\t\t}\n\t\tif (s && typeof s === \"object\") {\n\t\t\tconst name = (s as { name?: string }).name;\n\t\t\tif (name) out.push(name);\n\t\t\tconst children = (s as { children?: unknown }).children;\n\t\t\tif (children) walk(children);\n\t\t}\n\t};\n\twalk(symbols);\n\treturn out;\n}\n\nexport { toWorkspaceRel, sha256Content };\n"]}