surgent 0.7.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +407 -0
  3. package/bin/surgent.js +211 -0
  4. package/dist/optimizers/LICENSE +21 -0
  5. package/dist/optimizers/index.js +1984 -0
  6. package/dist/optimizers/index.js.map +7 -0
  7. package/dist/optimizers/package.json +31 -0
  8. package/package.json +45 -0
  9. package/src/agent/built-in/documenter.md +58 -0
  10. package/src/agent/built-in/general.md +107 -0
  11. package/src/agent/built-in/planner.md +73 -0
  12. package/src/agent/built-in/scout.md +97 -0
  13. package/src/agent/command.ts +140 -0
  14. package/src/agent/helpers.ts +95 -0
  15. package/src/agent/index.ts +9 -0
  16. package/src/agent/storage.ts +287 -0
  17. package/src/agent/types.ts +28 -0
  18. package/src/checkpoint/git.ts +173 -0
  19. package/src/checkpoint/index.ts +117 -0
  20. package/src/checkpoint/snapshot.ts +28 -0
  21. package/src/checkpoint/stage.ts +59 -0
  22. package/src/checkpoint/store.ts +108 -0
  23. package/src/cleanup/checkpoint.ts +31 -0
  24. package/src/cleanup/helpers.ts +24 -0
  25. package/src/cleanup/index.ts +21 -0
  26. package/src/cleanup/permission.ts +74 -0
  27. package/src/cleanup/subsession.ts +46 -0
  28. package/src/commands/helpers.ts +217 -0
  29. package/src/commands/index.ts +79 -0
  30. package/src/commands/render.ts +95 -0
  31. package/src/commands/types.ts +11 -0
  32. package/src/mcp-client/call-tool.ts +143 -0
  33. package/src/mcp-client/client.ts +90 -0
  34. package/src/mcp-client/command.ts +257 -0
  35. package/src/mcp-client/helpers.ts +153 -0
  36. package/src/mcp-client/index.ts +21 -0
  37. package/src/mcp-client/list-tools.ts +84 -0
  38. package/src/mcp-client/storage.ts +190 -0
  39. package/src/mcp-client/types.ts +34 -0
  40. package/src/mcp-client/validation.ts +115 -0
  41. package/src/optimizers/compactor/bash.ts +159 -0
  42. package/src/optimizers/compactor/grep.ts +141 -0
  43. package/src/optimizers/compactor/index.ts +132 -0
  44. package/src/optimizers/deduplicator/helpers.ts +75 -0
  45. package/src/optimizers/deduplicator/index.ts +23 -0
  46. package/src/optimizers/deduplicator/resources.ts +77 -0
  47. package/src/optimizers/deduplicator/state.ts +119 -0
  48. package/src/optimizers/deduplicator/types.ts +14 -0
  49. package/src/optimizers/entries.ts +104 -0
  50. package/src/optimizers/index.ts +17 -0
  51. package/src/optimizers/inspector/helpers.ts +60 -0
  52. package/src/optimizers/inspector/index.ts +89 -0
  53. package/src/optimizers/inspector/inspect.ts +88 -0
  54. package/src/optimizers/inspector/types.ts +7 -0
  55. package/src/optimizers/languages/go.ts +79 -0
  56. package/src/optimizers/languages/grammar.ts +200 -0
  57. package/src/optimizers/languages/index.ts +75 -0
  58. package/src/optimizers/languages/java.ts +64 -0
  59. package/src/optimizers/languages/python.ts +63 -0
  60. package/src/optimizers/languages/rust.ts +71 -0
  61. package/src/optimizers/languages/symbols.ts +95 -0
  62. package/src/optimizers/languages/tree-sitter-languages.d.ts +23 -0
  63. package/src/optimizers/languages/types.ts +134 -0
  64. package/src/optimizers/languages/typescript.ts +116 -0
  65. package/src/optimizers/mapper/files.ts +94 -0
  66. package/src/optimizers/mapper/index.ts +133 -0
  67. package/src/optimizers/mapper/types.ts +6 -0
  68. package/src/optimizers/pruner/cleanup.ts +121 -0
  69. package/src/optimizers/pruner/context.ts +46 -0
  70. package/src/optimizers/pruner/index.ts +45 -0
  71. package/src/optimizers/pruner/session.ts +34 -0
  72. package/src/optimizers/pruner/types.ts +18 -0
  73. package/src/permission/bash.ts +124 -0
  74. package/src/permission/command.ts +111 -0
  75. package/src/permission/components/prompt.ts +255 -0
  76. package/src/permission/components/rules-list.ts +342 -0
  77. package/src/permission/constants.ts +48 -0
  78. package/src/permission/helpers.ts +156 -0
  79. package/src/permission/index.ts +134 -0
  80. package/src/permission/pattern.ts +51 -0
  81. package/src/permission/piignore.ts +148 -0
  82. package/src/permission/precedence.ts +54 -0
  83. package/src/permission/resolution.ts +116 -0
  84. package/src/permission/storage.ts +142 -0
  85. package/src/permission/types.ts +57 -0
  86. package/src/questionnaire/component.ts +357 -0
  87. package/src/questionnaire/helpers.ts +220 -0
  88. package/src/questionnaire/index.ts +67 -0
  89. package/src/questionnaire/schemas.ts +50 -0
  90. package/src/questionnaire/types.ts +47 -0
  91. package/src/redactor/index.ts +34 -0
  92. package/src/redactor/patterns.ts +234 -0
  93. package/src/redactor/secrets.ts +113 -0
  94. package/src/subagent/helpers.ts +93 -0
  95. package/src/subagent/index.ts +81 -0
  96. package/src/subagent/storage.ts +100 -0
  97. package/src/subagent/subsession.ts +266 -0
  98. package/src/subagent/types.ts +83 -0
  99. package/src/subagent/validation.ts +100 -0
  100. package/src/ui/components/action-select-list.ts +165 -0
  101. package/src/ui/components/bash-mode.ts +281 -0
  102. package/src/ui/components/extended-select-list.ts +166 -0
  103. package/src/ui/components/form-field.ts +184 -0
  104. package/src/ui/components/form.ts +179 -0
  105. package/src/ui/components/frame.ts +60 -0
  106. package/src/ui/components/input-mode-indicator.ts +64 -0
  107. package/src/ui/components/keybound.ts +150 -0
  108. package/src/ui/components/lines.ts +27 -0
  109. package/src/ui/components/placeholder-input.ts +59 -0
  110. package/src/ui/components/scoped-input.ts +78 -0
  111. package/src/ui/components/scrollable-view.ts +155 -0
  112. package/src/ui/index.ts +40 -0
  113. package/src/utils.ts +206 -0
  114. package/src/web-tools/index.ts +15 -0
  115. package/src/web-tools/providers/brave.ts +55 -0
  116. package/src/web-tools/providers/firecrawl.ts +66 -0
  117. package/src/web-tools/providers/index.ts +50 -0
  118. package/src/web-tools/providers/jina.ts +48 -0
  119. package/src/web-tools/providers/native.ts +57 -0
  120. package/src/web-tools/providers/tavily.ts +56 -0
  121. package/src/web-tools/settings.ts +15 -0
  122. package/src/web-tools/web-fetch/helpers.ts +66 -0
  123. package/src/web-tools/web-fetch/index.ts +91 -0
  124. package/src/web-tools/web-fetch/parser.ts +51 -0
  125. package/src/web-tools/web-fetch/storage.ts +65 -0
  126. package/src/web-tools/web-fetch/types.ts +8 -0
  127. package/src/web-tools/web-login/helpers.ts +79 -0
  128. package/src/web-tools/web-login/index.ts +100 -0
  129. package/src/web-tools/web-login/types.ts +4 -0
  130. package/src/web-tools/web-search/helpers.ts +36 -0
  131. package/src/web-tools/web-search/index.ts +98 -0
  132. package/src/web-tools/web-search/types.ts +15 -0
@@ -0,0 +1,77 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { normalize, resolve } from "node:path";
3
+ import { isRecord } from "../../utils.js";
4
+ import { parseInspectToolDetails } from "../inspector/helpers.js";
5
+ import type { Range } from "../inspector/types.js";
6
+
7
+ export interface ResourceCoverage {
8
+ resource: string;
9
+ range: Range;
10
+ }
11
+
12
+ function getResultText(message: Record<string, unknown>): string | undefined {
13
+ if (!Array.isArray(message.content) || message.content.length !== 1) return;
14
+
15
+ const content = message.content[0];
16
+ if (!isRecord(content) || content.type !== "text" || typeof content.text !== "string") return;
17
+ return content.text;
18
+ }
19
+
20
+ function normalizeResourcePath(sourcePath: string, cwd: string): string {
21
+ const absolutePath = resolve(cwd, sourcePath);
22
+ try {
23
+ return normalize(realpathSync(absolutePath));
24
+ } catch {
25
+ return normalize(absolutePath);
26
+ }
27
+ }
28
+
29
+ export function getResourceCoverage(
30
+ toolName: string,
31
+ input: Record<string, unknown>,
32
+ result: unknown,
33
+ cwd: string,
34
+ ): ResourceCoverage | undefined {
35
+ if (!isRecord(result)) return;
36
+
37
+ if (toolName === "inspect") {
38
+ const inspected = parseInspectToolDetails(result.details);
39
+ if (!inspected) return;
40
+ return {
41
+ resource: normalizeResourcePath(inspected.path, cwd),
42
+ range: inspected.range,
43
+ };
44
+ }
45
+
46
+ if (toolName !== "read") return;
47
+ const sourcePath = input.path;
48
+ const offset = input.offset;
49
+ if (typeof sourcePath !== "string" || sourcePath.length === 0) return;
50
+ if (
51
+ offset !== undefined &&
52
+ (typeof offset !== "number" || !Number.isInteger(offset) || offset < 1)
53
+ ) {
54
+ return;
55
+ }
56
+
57
+ const details = isRecord(result.details) ? result.details : undefined;
58
+ const truncation = details?.truncation;
59
+ if (truncation !== undefined && !isRecord(truncation)) return;
60
+ if (isRecord(truncation) && truncation.firstLineExceedsLimit === true) return;
61
+
62
+ const resultText = getResultText(result);
63
+ if (resultText === undefined) return;
64
+ const continuation = resultText.match(/\n\n\[[^\n]*Use offset=\d+ to continue\.\]$/)?.[0];
65
+ const visibleText = continuation ? resultText.slice(0, -continuation.length) : resultText;
66
+ const outputLines =
67
+ isRecord(truncation) && truncation.truncated === true
68
+ ? truncation.outputLines
69
+ : visibleText.split("\n").length;
70
+ if (typeof outputLines !== "number" || !Number.isInteger(outputLines) || outputLines <= 0) return;
71
+
72
+ const start = typeof offset === "number" ? offset : 1;
73
+ return {
74
+ resource: normalizeResourcePath(sourcePath, cwd),
75
+ range: [start, start + outputLines - 1],
76
+ };
77
+ }
@@ -0,0 +1,119 @@
1
+ import {
2
+ getBranchEntries,
3
+ getEntryId,
4
+ getLastEntryId,
5
+ getMessage,
6
+ getToolResultMessage,
7
+ } from "../entries.js";
8
+ import { isRecord } from "../../utils.js";
9
+ import { hasFullCoverage } from "./helpers.js";
10
+ import { getResourceCoverage } from "./resources.js";
11
+ import type { DeduplicatorState, ResourceResult } from "./types.js";
12
+
13
+ function collectToolCallInputs(
14
+ activeEntries: Record<string, unknown>[],
15
+ ): Map<string, Record<string, unknown>> {
16
+ const inputsByCallId = new Map<string, Record<string, unknown>>();
17
+
18
+ for (const entry of activeEntries) {
19
+ const message = getMessage(entry);
20
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
21
+
22
+ for (const block of message.content) {
23
+ if (!isRecord(block) || block.type !== "toolCall") continue;
24
+ if (typeof block.id !== "string" || !isRecord(block.arguments)) continue;
25
+ inputsByCallId.set(block.id, block.arguments);
26
+ }
27
+ }
28
+
29
+ return inputsByCallId;
30
+ }
31
+
32
+ function collectResourceResults(
33
+ activeEntries: Record<string, unknown>[],
34
+ inputsByCallId: Map<string, Record<string, unknown>>,
35
+ cwd: string,
36
+ ): ResourceResult[] {
37
+ const results: ResourceResult[] = [];
38
+ for (const entry of activeEntries) {
39
+ const entryId = getEntryId(entry);
40
+ const message = getToolResultMessage(entry);
41
+ if (
42
+ !entryId ||
43
+ !message ||
44
+ message.isError === true ||
45
+ typeof message.toolCallId !== "string"
46
+ ) {
47
+ continue;
48
+ }
49
+
50
+ const input = inputsByCallId.get(message.toolCallId);
51
+ if (!input || (message.toolName !== "read" && message.toolName !== "inspect")) continue;
52
+
53
+ const coverage = getResourceCoverage(message.toolName, input, message, cwd);
54
+ if (!coverage) continue;
55
+ results.push({
56
+ entryId,
57
+ range: coverage.range,
58
+ resource: coverage.resource,
59
+ toolCallId: message.toolCallId,
60
+ prunable: true,
61
+ });
62
+ }
63
+ return results;
64
+ }
65
+
66
+ function collectReplacementsById(results: ResourceResult[]): Map<string, string[]> {
67
+ const retainedByResource = new Map<string, ResourceResult[]>();
68
+ const replacementsById = new Map<string, string[]>();
69
+
70
+ for (let index = results.length - 1; index >= 0; index -= 1) {
71
+ const result = results[index]!;
72
+ const retained = retainedByResource.get(result.resource) ?? [];
73
+ const covering = retained.filter(
74
+ (candidate) => candidate.range[0] <= result.range[1] && candidate.range[1] >= result.range[0],
75
+ );
76
+
77
+ if (
78
+ hasFullCoverage(
79
+ result.range,
80
+ covering.map(({ range }) => range),
81
+ )
82
+ ) {
83
+ if (result.prunable) {
84
+ replacementsById.set(result.entryId, [
85
+ ...new Set(covering.map((candidate) => candidate.entryId)),
86
+ ]);
87
+ }
88
+ continue;
89
+ }
90
+ retained.push(result);
91
+ retainedByResource.set(result.resource, retained);
92
+ }
93
+
94
+ return replacementsById;
95
+ }
96
+
97
+ export function buildDeduplicatorState(
98
+ entries: Record<string, unknown>[],
99
+ leafId: string | null,
100
+ cwd: string,
101
+ ): DeduplicatorState {
102
+ let activeEntries = getBranchEntries(entries, leafId);
103
+ if (activeEntries.length === 0 && leafId !== null) {
104
+ activeEntries = getBranchEntries(entries, getLastEntryId(entries));
105
+ }
106
+
107
+ const results = collectResourceResults(activeEntries, collectToolCallInputs(activeEntries), cwd);
108
+ const replacementsById = collectReplacementsById(results);
109
+ const replacements = new Map<string, string[]>();
110
+
111
+ for (const result of results) {
112
+ if (!result.prunable) continue;
113
+ const replacement = replacementsById.get(result.entryId);
114
+ if (!replacement) continue;
115
+ replacements.set(result.toolCallId, replacement);
116
+ }
117
+
118
+ return { replacements, resultEntryIds: new Set(results.map((result) => result.entryId)) };
119
+ }
@@ -0,0 +1,14 @@
1
+ import type { Range } from "../inspector/types.js";
2
+
3
+ export interface DeduplicatorState {
4
+ replacements: Map<string, string[]>;
5
+ resultEntryIds: Set<string>;
6
+ }
7
+
8
+ export interface ResourceResult {
9
+ entryId: string;
10
+ range: Range;
11
+ resource: string;
12
+ toolCallId: string;
13
+ prunable: boolean;
14
+ }
@@ -0,0 +1,104 @@
1
+ import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { isRecord } from "../utils.js";
3
+
4
+ export function readSessionEntries(
5
+ sessionFile: string | undefined,
6
+ ): Record<string, unknown>[] | undefined {
7
+ if (!sessionFile || !existsSync(sessionFile)) return;
8
+
9
+ try {
10
+ const sessionText = readFileSync(sessionFile, "utf8");
11
+ const entries: Record<string, unknown>[] = [];
12
+ for (const line of sessionText.split("\n")) {
13
+ if (line.trim().length === 0) continue;
14
+ const entry = JSON.parse(line);
15
+ if (!isRecord(entry)) return;
16
+ entries.push(entry);
17
+ }
18
+ return entries;
19
+ } catch {
20
+ return;
21
+ }
22
+ }
23
+
24
+ export function writeSessionEntries(sessionFile: string, entries: Record<string, unknown>[]) {
25
+ const temporaryFile = `${sessionFile}.${process.pid}.${Date.now()}.tmp`;
26
+ try {
27
+ writeFileSync(
28
+ temporaryFile,
29
+ `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`,
30
+ "utf8",
31
+ );
32
+ renameSync(temporaryFile, sessionFile);
33
+ } finally {
34
+ if (existsSync(temporaryFile)) unlinkSync(temporaryFile);
35
+ }
36
+ }
37
+
38
+ export function getEntryId(entry: Record<string, unknown>): string | undefined {
39
+ const entryId = entry.id;
40
+ if (typeof entryId !== "string" || entryId.length === 0) return;
41
+ return entryId;
42
+ }
43
+
44
+ export function getMessage(entry: Record<string, unknown>): Record<string, unknown> | undefined {
45
+ if (entry.type !== "message") return;
46
+ const message = entry.message;
47
+ if (!isRecord(message)) return;
48
+ return message;
49
+ }
50
+
51
+ export function getToolResultMessage(
52
+ entry: Record<string, unknown>,
53
+ ): Record<string, unknown> | undefined {
54
+ const message = getMessage(entry);
55
+ if (!message || message.role !== "toolResult") return;
56
+ if (typeof message.toolCallId !== "string" || message.toolCallId.length === 0) return;
57
+ if (typeof message.toolName !== "string" || message.toolName.length === 0) return;
58
+ return message;
59
+ }
60
+
61
+ export function getDetails(message: Record<string, unknown>): Record<string, unknown> | undefined {
62
+ const details = message.details;
63
+ if (!isRecord(details)) return;
64
+ return details;
65
+ }
66
+
67
+ export function getParentId(entry: Record<string, unknown>): string | null {
68
+ if (typeof entry.parentId === "string") return entry.parentId;
69
+ return null;
70
+ }
71
+
72
+ export function getBranchEntries(
73
+ entries: Record<string, unknown>[],
74
+ leafId: string | null,
75
+ ): Record<string, unknown>[] {
76
+ if (!leafId) return [];
77
+
78
+ const entriesById = new Map<string, Record<string, unknown>>();
79
+ for (const entry of entries) {
80
+ const entryId = getEntryId(entry);
81
+ if (entryId) entriesById.set(entryId, entry);
82
+ }
83
+ if (!entriesById.has(leafId)) return [];
84
+
85
+ const branch: Record<string, unknown>[] = [];
86
+ const visitedIds = new Set<string>();
87
+ let currentId: string | null = leafId;
88
+ while (currentId && !visitedIds.has(currentId)) {
89
+ const entry = entriesById.get(currentId);
90
+ if (!entry) break;
91
+ visitedIds.add(currentId);
92
+ branch.push(entry);
93
+ currentId = getParentId(entry);
94
+ }
95
+ return branch.reverse();
96
+ }
97
+
98
+ export function getLastEntryId(entries: Record<string, unknown>[]): string | null {
99
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
100
+ const entryId = getEntryId(entries[index]!);
101
+ if (entryId) return entryId;
102
+ }
103
+ return null;
104
+ }
@@ -0,0 +1,17 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import deduplicator from "./deduplicator/index.js";
3
+ import compactor from "./compactor/index.js";
4
+ import codeMap from "./mapper/index.js";
5
+ import inspect from "./inspector/index.js";
6
+ import languages from "./languages/index.js";
7
+ import pruner from "./pruner/index.js";
8
+
9
+ export default function (pi: ExtensionAPI) {
10
+ compactor(pi);
11
+ languages(pi);
12
+ deduplicator(pi);
13
+ pruner(pi);
14
+
15
+ pi.registerTool(codeMap);
16
+ pi.registerTool(inspect);
17
+ }
@@ -0,0 +1,60 @@
1
+ import type { InspectToolDetails } from "./types.js";
2
+
3
+ export function parseInspectToolDetails(inspected: unknown) {
4
+ if (!inspected || typeof inspected !== "object") return;
5
+
6
+ const path = (inspected as { path?: unknown }).path;
7
+ const symbol = (inspected as { symbol?: unknown }).symbol;
8
+ const range = (inspected as { range?: unknown }).range;
9
+
10
+ if (typeof path !== "string" || path.length === 0) return;
11
+ if (typeof symbol !== "string" || symbol.length === 0) return;
12
+ if (!Array.isArray(range) || range.length !== 2) return;
13
+
14
+ const start = range[0];
15
+ const end = range[1];
16
+ if (
17
+ typeof start !== "number" ||
18
+ typeof end !== "number" ||
19
+ !Number.isInteger(start) ||
20
+ !Number.isInteger(end) ||
21
+ start < 1 ||
22
+ end < start
23
+ ) {
24
+ return;
25
+ }
26
+
27
+ return { path, symbol, range: [start, end] } satisfies InspectToolDetails;
28
+ }
29
+
30
+ export function pruneInspectResults(messages: Array<{ role?: string }>) {
31
+ const seen = new Set<string>();
32
+ let changed = false;
33
+
34
+ for (let index = messages.length - 1; index >= 0; index--) {
35
+ const message = messages[index] as {
36
+ role: string;
37
+ toolName?: string;
38
+ details?: unknown;
39
+ isError?: boolean;
40
+ };
41
+
42
+ if (message.role !== "toolResult" || message.toolName !== "inspect" || message.isError) {
43
+ continue;
44
+ }
45
+
46
+ const details = parseInspectToolDetails(message.details);
47
+ if (!details) continue;
48
+
49
+ const symbolKey = `${details.path}#${details.symbol}`;
50
+ if (!seen.has(symbolKey)) {
51
+ seen.add(symbolKey);
52
+ continue;
53
+ }
54
+
55
+ changed = true;
56
+ messages.splice(index, 1);
57
+ }
58
+
59
+ return changed;
60
+ }
@@ -0,0 +1,89 @@
1
+ import { defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
+ import { Type } from "typebox";
4
+ import { inspectSymbol } from "./inspect.js";
5
+ import type { InspectToolDetails } from "./types.js";
6
+
7
+ const inspect = defineTool({
8
+ name: "inspect",
9
+ label: "Inspect",
10
+ description: "Fetch one symbol's full body from one file. Output is safe for targeted edits.",
11
+ parameters: Type.Object({
12
+ path: Type.String({
13
+ description: "Exact file path containing target symbol (relative to cwd or absolute)",
14
+ }),
15
+ symbol: Type.String({
16
+ description:
17
+ "Exact symbol string for one declaration in file: function name, class name, method name (MyClass.method), synthetic anonymous name (anonymous~1), or duplicate form name~2",
18
+ }),
19
+ }),
20
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
21
+ const path = params.path.trim().replaceAll("\\", "/");
22
+ const symbol = params.symbol.trim();
23
+
24
+ if (path.length === 0 || symbol.length === 0 || symbol.includes("#")) {
25
+ return {
26
+ isError: false,
27
+ details: null,
28
+ content: [
29
+ {
30
+ type: "text",
31
+ text: "inspect symbol format invalid. path/symbol must be non-empty; symbol cannot include '#'. use symbol name, e.g. name or name~2",
32
+ },
33
+ ],
34
+ };
35
+ }
36
+
37
+ try {
38
+ const inspected = await inspectSymbol(ctx.cwd, path, symbol, signal);
39
+ if (!inspected) {
40
+ return {
41
+ isError: false,
42
+ details: null,
43
+ content: [
44
+ {
45
+ type: "text",
46
+ text: "inspect symbol not found. verify path/symbol; for duplicates use ~n suffix (name~2). then try container symbol. if still missing, run code_map with broader kinds",
47
+ },
48
+ ],
49
+ };
50
+ }
51
+
52
+ const details = {
53
+ path: inspected.path,
54
+ symbol: inspected.symbol,
55
+ range: inspected.range,
56
+ } satisfies InspectToolDetails;
57
+
58
+ return { isError: false, details, content: [{ type: "text", text: inspected.text }] };
59
+ } catch (error) {
60
+ const message = error instanceof Error ? error.message : String(error);
61
+ return {
62
+ isError: true,
63
+ details: null,
64
+ content: [{ type: "text", text: `inspect failed: ${message}` }],
65
+ };
66
+ }
67
+ },
68
+ renderCall(args, theme) {
69
+ return new Text(
70
+ `${theme.fg("toolTitle", "inspect")} ${theme.underline(theme.fg("accent", args.path))} ${theme.fg("warning", args.symbol)}`,
71
+ 0,
72
+ 0,
73
+ );
74
+ },
75
+ renderResult(result, { expanded }, theme, context) {
76
+ if (!expanded && !context.isError) {
77
+ return new Text("", 0, 0);
78
+ }
79
+
80
+ const output = result.content[0];
81
+ return new Text(
82
+ `\n${output?.type === "text" ? theme.fg("toolOutput", output.text) : ""}`,
83
+ 0,
84
+ 0,
85
+ );
86
+ },
87
+ });
88
+
89
+ export default inspect;
@@ -0,0 +1,88 @@
1
+ import type { SyntaxNode } from "tree-sitter";
2
+ import type { LanguageSymbol } from "../languages/index.js";
3
+ import { collectSymbols, SYMBOL_KINDS } from "../languages/index.js";
4
+ import type { Range } from "./types.js";
5
+
6
+ function inspectGroupedSymbols(
7
+ path: string,
8
+ symbolName: string,
9
+ symbols: LanguageSymbol[],
10
+ ): { path: string; symbol: string; range: Range; text: string } | undefined {
11
+ const groupedSymbolsMatch = /^(imports|exports)~([1-9]\d*)$/.exec(symbolName);
12
+ if (!groupedSymbolsMatch) return;
13
+
14
+ const groupedSymbolKind = groupedSymbolsMatch[1] === "imports" ? "deps" : "public";
15
+ const requestedGroup = Number(groupedSymbolsMatch[2]);
16
+ const groupedNodes: SyntaxNode[] = [];
17
+ const groupedRanges = new Set<string>();
18
+ let symbolsCollapsed = false;
19
+ let symbolsGroupIndex = 0;
20
+
21
+ for (const symbol of symbols) {
22
+ if (symbol.kind !== groupedSymbolKind) {
23
+ symbolsCollapsed = false;
24
+ continue;
25
+ }
26
+ if (!symbolsCollapsed) {
27
+ symbolsCollapsed = true;
28
+ symbolsGroupIndex += 1;
29
+ }
30
+ if (symbolsGroupIndex !== requestedGroup) continue;
31
+
32
+ let groupedNode = symbol.node;
33
+ if (groupedSymbolKind === "deps") {
34
+ while (groupedNode.parent?.parent) {
35
+ groupedNode = groupedNode.parent;
36
+ }
37
+ }
38
+
39
+ const groupedRange = `${groupedNode.startIndex}-${groupedNode.endIndex}`;
40
+ if (groupedRanges.has(groupedRange)) continue;
41
+
42
+ groupedRanges.add(groupedRange);
43
+ groupedNodes.push(groupedNode);
44
+ }
45
+
46
+ const firstNode = groupedNodes[0];
47
+ const lastNode = groupedNodes[groupedNodes.length - 1];
48
+ if (!firstNode || !lastNode) return;
49
+ return {
50
+ path,
51
+ symbol: symbolName,
52
+ text: groupedNodes.map((groupedNode) => groupedNode.text).join("\n"),
53
+ range: [
54
+ firstNode.startPosition.row + 1,
55
+ lastNode.endPosition.row + (lastNode.endPosition.column > 0 ? 1 : 0),
56
+ ],
57
+ };
58
+ }
59
+
60
+ export async function inspectSymbol(
61
+ cwd: string,
62
+ path: string,
63
+ symbolName: string,
64
+ signal?: AbortSignal,
65
+ ): Promise<{ path: string; symbol: string; range: Range; text: string } | undefined> {
66
+ if (signal?.aborted) {
67
+ throw new Error("inspector aborted");
68
+ }
69
+ const kinds = new Set(SYMBOL_KINDS);
70
+ const symbols = await collectSymbols(cwd, path, kinds);
71
+
72
+ const inspectedGroup = inspectGroupedSymbols(path, symbolName, symbols);
73
+ if (inspectedGroup) return inspectedGroup;
74
+
75
+ for (const symbol of symbols) {
76
+ if (symbol.name !== symbolName) continue;
77
+
78
+ return {
79
+ path: symbol.path,
80
+ symbol: symbol.name,
81
+ range: [
82
+ symbol.node.startPosition.row + 1,
83
+ symbol.node.endPosition.row + (symbol.node.endPosition.column > 0 ? 1 : 0),
84
+ ],
85
+ text: symbol.node.text,
86
+ };
87
+ }
88
+ }
@@ -0,0 +1,7 @@
1
+ export type Range = [number, number];
2
+
3
+ export interface InspectToolDetails {
4
+ path: string;
5
+ symbol: string;
6
+ range: Range;
7
+ }
@@ -0,0 +1,79 @@
1
+ import type { Language, SyntaxNode } from "tree-sitter";
2
+ import { loadGrammarModule } from "./grammar.js";
3
+ import { RuleBasedLanguageProfile, type SymbolKindRule } from "./types.js";
4
+
5
+ export class GoLanguageProfile extends RuleBasedLanguageProfile {
6
+ constructor() {
7
+ super(
8
+ new Set([".go"]),
9
+ { __default__: "name" },
10
+ new Set(["source_file"]),
11
+ new Set([
12
+ "const_declaration",
13
+ "import_declaration",
14
+ "import_spec_list",
15
+ "type_declaration",
16
+ "var_declaration",
17
+ "var_spec_list",
18
+ ]),
19
+ new Map<string, SymbolKindRule[]>([
20
+ ["function_declaration", [{ kind: "func", topLevelOnly: true }]],
21
+ ["method_declaration", [{ kind: "method" }]],
22
+ ["method_elem", [{ kind: "method", parent: "interface_type", container: "type_spec" }]],
23
+ ["const_spec", [{ kind: "decl", topLevelOnly: true }]],
24
+ ["import_spec", [{ kind: "deps", topLevelOnly: true }]],
25
+ ["type_alias", [{ kind: "class", topLevelOnly: true }]],
26
+ ["type_spec", [{ kind: "class", topLevelOnly: true }]],
27
+ ["var_spec", [{ kind: "decl", topLevelOnly: true }]],
28
+ ]),
29
+ );
30
+ }
31
+
32
+ async loadLanguage(_extension: string) {
33
+ const languagePack = await loadGrammarModule("tree-sitter-go");
34
+ const languageExport = (languagePack.default ?? languagePack) as Language;
35
+ return languageExport;
36
+ }
37
+
38
+ readNodeName(node: SyntaxNode) {
39
+ if (node.type === "import_spec") {
40
+ const importName = this.readFieldText(node, "name");
41
+ if (importName) {
42
+ return importName;
43
+ }
44
+
45
+ const importPath = this.readFieldText(node, "path");
46
+ if (importPath) {
47
+ return importPath.replace(/^("|')(.*)\1$/, "$2");
48
+ }
49
+ }
50
+
51
+ const nodeName = this.readNameField(node);
52
+ if (nodeName) {
53
+ return nodeName;
54
+ }
55
+
56
+ if (
57
+ node.type === "field_identifier" ||
58
+ node.type === "identifier" ||
59
+ node.type === "package_identifier" ||
60
+ node.type === "type_identifier"
61
+ ) {
62
+ return this.readNodeText(node);
63
+ }
64
+ }
65
+
66
+ readContainerName(node: SyntaxNode) {
67
+ if (node.type === "method_declaration") {
68
+ const receiverNode = node.childForFieldName("receiver");
69
+ const receiverParameterNode = receiverNode?.namedChild(0);
70
+ const receiverTypeNode = receiverParameterNode?.childForFieldName("type");
71
+ const receiverType = this.readNodeText(receiverTypeNode);
72
+ if (receiverType) {
73
+ return receiverType.replace(/^\*+/, "");
74
+ }
75
+ }
76
+
77
+ return super.readContainerName(node);
78
+ }
79
+ }