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,134 @@
1
+ import type { Language, SyntaxNode } from "tree-sitter";
2
+
3
+ export type SymbolKind = "func" | "class" | "method" | "decl" | "deps" | "public";
4
+
5
+ export interface SymbolKindRule {
6
+ kind: SymbolKind;
7
+ parent?: string;
8
+ container?: string;
9
+ topLevelOnly?: boolean;
10
+ }
11
+
12
+ export interface LanguageSymbol {
13
+ name: string;
14
+ path: string;
15
+ kind: SymbolKind;
16
+ node: SyntaxNode;
17
+ range?: [number, number];
18
+ public?: boolean;
19
+ }
20
+
21
+ export interface LanguageProfile {
22
+ extensions: Set<string>;
23
+ loadLanguage(extension: string): Promise<Language>;
24
+ readNodeName(node: SyntaxNode): string | undefined;
25
+ readContainerName(node: SyntaxNode): string | undefined;
26
+ isPublicSymbol(node: SyntaxNode): boolean;
27
+ resolveSymbolKind(node: SyntaxNode): SymbolKind | undefined;
28
+ shouldSkipSymbol(node: SyntaxNode): boolean;
29
+ }
30
+
31
+ export interface SupportedLanguageBucket {
32
+ bucketName: string;
33
+ packageName: string;
34
+ version: string;
35
+ extensions: Set<string>;
36
+ }
37
+
38
+ export interface GrammarInstallSettings {
39
+ allowedRepos?: string[];
40
+ deniedRepos?: string[];
41
+ }
42
+
43
+ export abstract class RuleBasedLanguageProfile implements LanguageProfile {
44
+ constructor(
45
+ readonly extensions: Set<string>,
46
+ protected readonly nameFieldByType: { __default__: string } & Record<string, string>,
47
+ protected readonly topLevelRoots: Set<string>,
48
+ protected readonly topLevelParents: Set<string>,
49
+ protected readonly typeRule: Map<string, SymbolKindRule[]>,
50
+ ) {}
51
+
52
+ abstract loadLanguage(extension: string): Promise<Language>;
53
+
54
+ abstract readNodeName(node: SyntaxNode): string | undefined;
55
+
56
+ protected findContainerNode(node: SyntaxNode) {
57
+ let current = node.parent;
58
+ while (current) {
59
+ if (this.typeRule.has(current.type)) {
60
+ return current;
61
+ }
62
+ current = current.parent;
63
+ }
64
+ }
65
+
66
+ protected matchesTopLevelRule(node: SyntaxNode) {
67
+ let current = node.parent;
68
+
69
+ while (current) {
70
+ if (this.topLevelRoots.has(current.type)) {
71
+ return true;
72
+ }
73
+ if (this.topLevelParents.size > 0 && !this.topLevelParents.has(current.type)) {
74
+ return false;
75
+ }
76
+ current = current.parent;
77
+ }
78
+
79
+ return false;
80
+ }
81
+
82
+ protected readFieldText(node: SyntaxNode, fieldName: string) {
83
+ return this.readNodeText(node.childForFieldName(fieldName));
84
+ }
85
+
86
+ protected readNameField(node: SyntaxNode) {
87
+ const fieldName = this.nameFieldByType[node.type] ?? this.nameFieldByType.__default__;
88
+ return this.readFieldText(node, fieldName);
89
+ }
90
+
91
+ protected readNodeText(node: SyntaxNode | null | undefined) {
92
+ if (!node) return;
93
+
94
+ const nodeText = node.text.trim().replaceAll("\n", " ");
95
+ return nodeText.length > 0 ? nodeText : undefined;
96
+ }
97
+
98
+ readContainerName(node: SyntaxNode) {
99
+ const containerNode = this.findContainerNode(node);
100
+ return containerNode ? this.readNodeName(containerNode) : undefined;
101
+ }
102
+
103
+ isPublicSymbol(node: SyntaxNode) {
104
+ return this.findContainerNode(node)?.type === "export_statement";
105
+ }
106
+
107
+ resolveSymbolKind(node: SyntaxNode) {
108
+ const symbolKindRules = this.typeRule.get(node.type);
109
+ if (!symbolKindRules) return;
110
+
111
+ for (const symbolKindRule of symbolKindRules) {
112
+ if (symbolKindRule.parent && node.parent?.type !== symbolKindRule.parent) {
113
+ continue;
114
+ }
115
+
116
+ if (symbolKindRule.container) {
117
+ const containerNode = this.findContainerNode(node);
118
+ if (!containerNode || containerNode.type !== symbolKindRule.container) {
119
+ continue;
120
+ }
121
+ }
122
+
123
+ if (symbolKindRule.topLevelOnly && !this.matchesTopLevelRule(node)) {
124
+ continue;
125
+ }
126
+
127
+ return symbolKindRule.kind;
128
+ }
129
+ }
130
+
131
+ shouldSkipSymbol(_node: SyntaxNode) {
132
+ return false;
133
+ }
134
+ }
@@ -0,0 +1,116 @@
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 TypeScriptLanguageProfile extends RuleBasedLanguageProfile {
6
+ constructor() {
7
+ super(
8
+ new Set([".ts", ".tsx", ".js", ".mjs", ".cjs", ".jsx"]),
9
+ { __default__: "name" },
10
+ new Set(["program"]),
11
+ new Set([
12
+ "lexical_declaration",
13
+ "variable_declaration",
14
+ "variable_declarator",
15
+ "export_statement",
16
+ ]),
17
+ new Map<string, SymbolKindRule[]>([
18
+ ["function_declaration", [{ kind: "func", topLevelOnly: true }]],
19
+ ["arrow_function", [{ kind: "func", topLevelOnly: true }]],
20
+ ["function_expression", [{ kind: "func", topLevelOnly: true }]],
21
+ ["abstract_class_declaration", [{ kind: "class" }]],
22
+ ["class_declaration", [{ kind: "class" }]],
23
+ ["abstract_method_signature", [{ kind: "method", parent: "class_body" }]],
24
+ [
25
+ "method_definition",
26
+ [
27
+ { kind: "method", parent: "class_body" },
28
+ { kind: "method", parent: "object" },
29
+ ],
30
+ ],
31
+ ["variable_declarator", [{ kind: "decl", topLevelOnly: true }]],
32
+ ["import_specifier", [{ kind: "deps" }]],
33
+ ["namespace_import", [{ kind: "deps" }]],
34
+ ["identifier", [{ kind: "deps", parent: "import_clause" }]],
35
+ ["export_statement", [{ kind: "public", topLevelOnly: true }]],
36
+ ]),
37
+ );
38
+ }
39
+
40
+ async loadLanguage(extension: string) {
41
+ const languagePack = await loadGrammarModule("tree-sitter-typescript");
42
+ const languageExports =
43
+ typeof languagePack.default === "object" && languagePack.default !== null
44
+ ? languagePack.default
45
+ : languagePack;
46
+
47
+ const typeScriptLanguage = languageExports.typescript as Language | undefined;
48
+ if (!typeScriptLanguage) {
49
+ throw new Error("tree-sitter-typescript missing typescript export");
50
+ }
51
+
52
+ if (extension === ".tsx" || extension === ".jsx") {
53
+ return (languageExports.tsx as Language | undefined) ?? typeScriptLanguage;
54
+ }
55
+ return typeScriptLanguage;
56
+ }
57
+
58
+ readNodeName(node: SyntaxNode) {
59
+ if (node.type === "export_statement") {
60
+ return "exports";
61
+ }
62
+
63
+ if (node.type === "import_specifier") {
64
+ const aliasText = this.readFieldText(node, "alias");
65
+ if (aliasText) {
66
+ return aliasText;
67
+ }
68
+ }
69
+
70
+ const nodeName = this.readNameField(node);
71
+ if (nodeName) {
72
+ return nodeName;
73
+ }
74
+
75
+ if (node.type === "namespace_import") {
76
+ return this.readNodeText(node.namedChild(0));
77
+ }
78
+
79
+ if (node.type === "identifier") {
80
+ return this.readNodeText(node);
81
+ }
82
+
83
+ if (
84
+ node.type === "arrow_function" ||
85
+ node.type === "function_expression" ||
86
+ node.type === "function_declaration"
87
+ ) {
88
+ const declaratorNode = node.parent?.type === "variable_declarator" ? node.parent : undefined;
89
+ if (declaratorNode) {
90
+ const declaratorName = this.readFieldText(declaratorNode, "name");
91
+ if (declaratorName) {
92
+ return declaratorName;
93
+ }
94
+ }
95
+
96
+ return "anonymous";
97
+ }
98
+ }
99
+
100
+ shouldSkipSymbol(node: SyntaxNode) {
101
+ if (node.type === "export_statement") {
102
+ const inlineNode = node.namedChild(0);
103
+ return (
104
+ node.childForFieldName("declaration") !== null ||
105
+ inlineNode?.type === "arrow_function" ||
106
+ inlineNode?.type === "function_expression" ||
107
+ inlineNode?.type === "function_declaration" ||
108
+ (inlineNode?.type === "class_declaration" && inlineNode.childForFieldName("name") !== null)
109
+ );
110
+ }
111
+ if (node.type === "variable_declarator") {
112
+ return node.childForFieldName("value")?.type === "arrow_function";
113
+ }
114
+ return node.type === "function_expression" && node.childForFieldName("name") !== null;
115
+ }
116
+ }
@@ -0,0 +1,94 @@
1
+ import picomatch from "picomatch";
2
+ import { runCommand, unique } from "../../utils.js";
3
+
4
+ const SKIPPED_DIRECTORIES = new Set([".git", ".pi", "build", "coverage", "dist", "node_modules"]);
5
+
6
+ function normalizePath(pathValue: string) {
7
+ const normalizedPath = pathValue.replaceAll("\\", "/");
8
+ return normalizedPath.startsWith("./") ? normalizedPath.slice(2) : normalizedPath;
9
+ }
10
+
11
+ async function rgFiles(
12
+ projectPath: string,
13
+ globTargets: string[],
14
+ signal?: AbortSignal,
15
+ ) {
16
+ const args = ["--files", "--hidden"];
17
+
18
+ for (const skipped of SKIPPED_DIRECTORIES) {
19
+ args.push("--glob", `!**/${skipped}/**`);
20
+ }
21
+ for (const globTarget of globTargets) {
22
+ args.push("--glob", globTarget);
23
+ }
24
+
25
+ args.push(".");
26
+ const commandResult = await runCommand(projectPath, "rg", args, {
27
+ signal,
28
+ successExitCodes: [0, 1],
29
+ abortMessage: "mapper aborted",
30
+ });
31
+ return commandResult.stdout
32
+ .split("\n")
33
+ .map((line) => normalizePath(line.trim()))
34
+ .filter((line) => line.length > 0);
35
+ }
36
+
37
+ async function grepFiles(
38
+ projectPath: string,
39
+ globTargets: string[],
40
+ signal?: AbortSignal,
41
+ ) {
42
+ const args = ["-r", "-I", "-l"];
43
+
44
+ for (const skipped of SKIPPED_DIRECTORIES) {
45
+ args.push("--exclude-dir", skipped);
46
+ }
47
+
48
+ args.push("-e", "", ".");
49
+ const commandResult = await runCommand(projectPath, "grep", args, {
50
+ signal,
51
+ successExitCodes: [0, 1],
52
+ abortMessage: "mapper aborted",
53
+ });
54
+ const paths = commandResult.stdout
55
+ .split("\n")
56
+ .map((line) => normalizePath(line.trim()))
57
+ .filter((line) => line.length > 0);
58
+ const matchers = globTargets.map((globTarget) => picomatch(globTarget, { dot: true }));
59
+ return paths.filter((pathValue) => matchers.some((matcher) => matcher(pathValue)));
60
+ }
61
+
62
+ export async function resolveTargetPaths(
63
+ projectPath: string,
64
+ targets: string[],
65
+ signal?: AbortSignal,
66
+ ) {
67
+ const globTargets = targets.flatMap((target) => {
68
+ const normalized = normalizePath(target);
69
+ if (normalized.length === 0) return [];
70
+ if (normalized === ".") return ["**"];
71
+ if (/[*?[\]{}]/.test(normalized)) return [normalized];
72
+ return [normalized, `${normalized}/**`];
73
+ });
74
+
75
+ if (globTargets.length === 0) return [];
76
+
77
+ try {
78
+ const paths = await rgFiles(projectPath, globTargets, signal);
79
+ return unique(paths).sort();
80
+ } catch (rgError) {
81
+ const rgMessage = rgError instanceof Error ? rgError.message : String(rgError);
82
+ if (rgMessage === "mapper aborted") {
83
+ throw rgError;
84
+ }
85
+
86
+ try {
87
+ const paths = await grepFiles(projectPath, globTargets, signal);
88
+ return unique(paths).sort();
89
+ } catch (grepError) {
90
+ const grepMessage = grepError instanceof Error ? grepError.message : String(grepError);
91
+ throw new Error(`mapper file scan failed: rg=${rgMessage}; grep=${grepMessage}`);
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,133 @@
1
+ import { defineTool, truncateHead } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { resolveTargetPaths } from "./files.js";
4
+ import { collectSymbols, SYMBOL_KINDS } from "../languages/index.js";
5
+ import type { LanguageSymbol } from "../languages/index.js";
6
+ import type { MapperResult } from "./types.js";
7
+ import { renderCallText } from "../../utils.js";
8
+
9
+ function collapseGroupedSymbols(symbols: LanguageSymbol[]) {
10
+ let groupedSymbolKind: "deps" | "public" | undefined;
11
+ let importsGroupIndex = 0;
12
+ let exportsGroupIndex = 0;
13
+
14
+ return symbols.flatMap((symbol) => {
15
+ if (symbol.kind !== "deps" && symbol.kind !== "public") {
16
+ groupedSymbolKind = undefined;
17
+ return [symbol];
18
+ }
19
+ if (symbol.kind === groupedSymbolKind) return [];
20
+
21
+ groupedSymbolKind = symbol.kind;
22
+ if (symbol.kind === "deps") {
23
+ importsGroupIndex += 1;
24
+ return [{ ...symbol, name: `imports~${importsGroupIndex}`, range: undefined }];
25
+ }
26
+
27
+ exportsGroupIndex += 1;
28
+ return [{ ...symbol, name: `exports~${exportsGroupIndex}`, range: undefined }];
29
+ });
30
+ }
31
+
32
+ const codeMap = defineTool({
33
+ name: "code_map",
34
+ label: "Code map",
35
+ description:
36
+ "Fast symbol and code blocks offset/limit indexing. Best for: narrowing targets to inspect/read or code discovery.",
37
+ parameters: Type.Object({
38
+ targets: Type.Array(Type.String(), {
39
+ description: "Paths or globs to scan (relative to cwd). Keep scope narrow.",
40
+ }),
41
+ kinds: Type.Optional(
42
+ Type.Array(Type.Union(SYMBOL_KINDS.map((kind) => Type.Literal(kind))), {
43
+ description: "Abstraction kinds to include. Omit to include all supported.",
44
+ }),
45
+ ),
46
+ }),
47
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
48
+ const kinds = new Set(params.kinds ?? SYMBOL_KINDS);
49
+ const result: MapperResult = { symbols: [], failed: [] };
50
+ let paths: string[] = [];
51
+ try {
52
+ paths = await resolveTargetPaths(ctx.cwd, params.targets, signal);
53
+ } catch (error) {
54
+ const message = error instanceof Error ? error.message : String(error);
55
+ return {
56
+ isError: true,
57
+ details: `code_map target scan failed: ${message}`,
58
+ content: [{ type: "text", text: `code_map target scan failed: ${message}` }],
59
+ };
60
+ }
61
+
62
+ if (paths.length === 0) {
63
+ return {
64
+ isError: false,
65
+ details: "(no symbols found) targets matched no supported files or symbols",
66
+ content: [
67
+ {
68
+ type: "text",
69
+ text: "(no symbols found) targets matched no supported files or symbols",
70
+ },
71
+ ],
72
+ };
73
+ }
74
+
75
+ for (const path of paths) {
76
+ if (signal?.aborted) {
77
+ return {
78
+ isError: true,
79
+ details: "code_map aborted",
80
+ content: [{ type: "text", text: "code_map aborted" }],
81
+ };
82
+ }
83
+
84
+ try {
85
+ const symbols = collapseGroupedSymbols(await collectSymbols(ctx.cwd, path, kinds));
86
+ result.symbols.push(...symbols);
87
+ } catch (error) {
88
+ const message = error instanceof Error ? error.message : String(error);
89
+ result.failed.push(`${path}: ${message}`);
90
+ }
91
+ }
92
+
93
+ const outputLines: string[] = [];
94
+ let outputPath = "";
95
+
96
+ for (const symbol of result.symbols) {
97
+ if (symbol.path !== outputPath) {
98
+ outputPath = symbol.path;
99
+ outputLines.push(symbol.path);
100
+ }
101
+
102
+ let line = ` [${symbol.public ? "public " : ""}${symbol.kind}] ${symbol.name}`;
103
+ if (symbol.range) {
104
+ line += ` L${symbol.range[0]}-L${symbol.range[1]}`;
105
+ }
106
+ outputLines.push(line);
107
+ }
108
+
109
+ if (result.failed.length > 0) {
110
+ outputLines.push(...result.failed.map((failure) => `failed ${failure}`));
111
+ }
112
+ if (outputLines.length === 0) {
113
+ outputLines.push("(no symbols found) targets matched no supported files or symbols");
114
+ }
115
+
116
+ const truncation = truncateHead(outputLines.join("\n"));
117
+ const output = truncation.truncated
118
+ ? `${truncation.content}\n\n[Output truncated at 2000 lines or 50KB. Narrow targets or kinds.]`
119
+ : truncation.content;
120
+ return { isError: false, details: output, content: [{ type: "text", text: output }] };
121
+ },
122
+ renderCall(args, theme, { isPartial }) {
123
+ const targets = Array.isArray(args.targets) ? args.targets.join(", ") : "";
124
+ const kinds = Array.isArray(args.kinds) ? args.kinds.join(", ") : "default";
125
+
126
+ return renderCallText(
127
+ `${theme.fg("toolTitle", "code_map")} ${theme.underline(theme.fg("accent", targets))} ${theme.fg("dim", `[${kinds}]`)}`,
128
+ isPartial,
129
+ );
130
+ },
131
+ });
132
+
133
+ export default codeMap;
@@ -0,0 +1,6 @@
1
+ import type { LanguageSymbol } from "../languages/types.js";
2
+
3
+ export interface MapperResult {
4
+ symbols: LanguageSymbol[];
5
+ failed: string[];
6
+ }
@@ -0,0 +1,121 @@
1
+ import { getEntryId, getMessage, getParentId, getToolResultMessage } from "../entries.js";
2
+ import type { RemovedEntryRemoval } from "./types.js";
3
+ import { isRecord } from "../../utils.js";
4
+
5
+ function resolveRemovedId(
6
+ entryId: string | null,
7
+ replacementParents: Map<string, string | null>,
8
+ ): string | null {
9
+ const visitedIds = new Set<string>();
10
+ let resolvedId = entryId;
11
+ while (resolvedId && replacementParents.has(resolvedId) && !visitedIds.has(resolvedId)) {
12
+ visitedIds.add(resolvedId);
13
+ resolvedId = replacementParents.get(resolvedId) ?? null;
14
+ }
15
+ return resolvedId;
16
+ }
17
+
18
+ function hasEmptyResult(message: {
19
+ content?: unknown;
20
+ toolName?: unknown;
21
+ }): boolean {
22
+ if (message.toolName !== "ls" && message.toolName !== "find") {
23
+ return false;
24
+ }
25
+
26
+ if (!Array.isArray(message.content) || message.content.length !== 1) return false;
27
+ const content = message.content[0];
28
+ if (!isRecord(content) || content.type !== "text" || typeof content.text !== "string") return false;
29
+
30
+ return (
31
+ content.text === "(empty directory)" ||
32
+ content.text === "No files found matching pattern" ||
33
+ content.text === "No changes found." ||
34
+ content.text === "No changes in selected files."
35
+ );
36
+ }
37
+
38
+ export function getRemovedToolCallId(message: {
39
+ content?: unknown;
40
+ isError?: unknown;
41
+ toolCallId?: unknown;
42
+ toolName?: unknown;
43
+ }): string | undefined {
44
+ if (typeof message.toolCallId !== "string") return;
45
+ if (message.isError === true) return message.toolName === "bash" ? undefined : message.toolCallId;
46
+ return hasEmptyResult(message) ? message.toolCallId : undefined;
47
+ }
48
+
49
+ export function removeEntries(entries: Record<string, unknown>[]): RemovedEntryRemoval {
50
+ const removedToolCallIds = new Set<string>();
51
+ const replacementParents = new Map<string, string | null>();
52
+
53
+ for (const entry of entries) {
54
+ const message = getToolResultMessage(entry);
55
+ const entryId = getEntryId(entry);
56
+ const toolCallId = message ? getRemovedToolCallId(message) : undefined;
57
+ if (!entryId || !toolCallId) continue;
58
+
59
+ removedToolCallIds.add(toolCallId);
60
+ replacementParents.set(entryId, getParentId(entry));
61
+ }
62
+
63
+ if (removedToolCallIds.size === 0) {
64
+ return { changed: false, entries, replacementParents };
65
+ }
66
+
67
+ const retainedEntries: Record<string, unknown>[] = [];
68
+ for (const entry of entries) {
69
+ const entryId = getEntryId(entry);
70
+ if (entryId && replacementParents.has(entryId)) continue;
71
+
72
+ const message = getMessage(entry);
73
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) {
74
+ retainedEntries.push(entry);
75
+ continue;
76
+ }
77
+
78
+ const retainedContent = message.content.filter(
79
+ (block) =>
80
+ !isRecord(block) ||
81
+ block.type !== "toolCall" ||
82
+ typeof block.id !== "string" ||
83
+ !removedToolCallIds.has(block.id),
84
+ );
85
+ if (retainedContent.length === message.content.length) {
86
+ retainedEntries.push(entry);
87
+ continue;
88
+ }
89
+ if (entryId && retainedContent.every((block) => isRecord(block) && block.type === "thinking")) {
90
+ replacementParents.set(entryId, getParentId(entry));
91
+ continue;
92
+ }
93
+ retainedEntries.push({ ...entry, message: { ...message, content: retainedContent } });
94
+ }
95
+
96
+ const repairedEntries: Record<string, unknown>[] = [];
97
+ for (const entry of retainedEntries) {
98
+ const updatedEntry = { ...entry };
99
+ let changed = false;
100
+
101
+ const parentId = getParentId(entry);
102
+ const repairedParentId = resolveRemovedId(parentId, replacementParents);
103
+ if (parentId !== repairedParentId) {
104
+ updatedEntry.parentId = repairedParentId;
105
+ changed = true;
106
+ }
107
+
108
+ for (const referenceField of ["fromId", "targetId", "firstKeptEntryId"]) {
109
+ const referenceId = entry[referenceField];
110
+ if (typeof referenceId !== "string") continue;
111
+ const repairedReferenceId = resolveRemovedId(referenceId, replacementParents);
112
+ if (repairedReferenceId && repairedReferenceId !== referenceId) {
113
+ updatedEntry[referenceField] = repairedReferenceId;
114
+ changed = true;
115
+ }
116
+ }
117
+ repairedEntries.push(changed ? updatedEntry : entry);
118
+ }
119
+
120
+ return { changed: true, entries: repairedEntries, replacementParents };
121
+ }
@@ -0,0 +1,46 @@
1
+ import { getToolResultMessage } from "../entries.js";
2
+ import { getRemovedToolCallId } from "./cleanup.js";
3
+ import type { ContextPruneResult } from "./types.js";
4
+
5
+ export function buildPrunerState(entries: Record<string, unknown>[]): Set<string> {
6
+ const removedToolCallIds = new Set<string>();
7
+ for (const entry of entries) {
8
+ const message = getToolResultMessage(entry);
9
+ const toolCallId = message ? getRemovedToolCallId(message) : undefined;
10
+ if (toolCallId) removedToolCallIds.add(toolCallId);
11
+ }
12
+ return removedToolCallIds;
13
+ }
14
+
15
+ export function filterContextMessages(
16
+ messages: ContextPruneResult["messages"],
17
+ state: Set<string>,
18
+ ): ContextPruneResult {
19
+ if (state.size === 0) return { changed: false, messages };
20
+
21
+ const retainedMessages: ContextPruneResult["messages"] = [];
22
+ let changed = false;
23
+ for (const message of messages) {
24
+ if (message.role === "toolResult" && state.has(message.toolCallId)) {
25
+ changed = true;
26
+ continue;
27
+ }
28
+ if (message.role !== "assistant") {
29
+ retainedMessages.push(message);
30
+ continue;
31
+ }
32
+
33
+ const retainedContent = message.content.filter(
34
+ (block) => block.type !== "toolCall" || !state.has(block.id),
35
+ );
36
+ if (retainedContent.length === message.content.length) {
37
+ retainedMessages.push(message);
38
+ continue;
39
+ }
40
+ changed = true;
41
+ if (retainedContent.some((block) => block.type !== "thinking")) {
42
+ retainedMessages.push({ ...message, content: retainedContent });
43
+ }
44
+ }
45
+ return { changed, messages: retainedMessages };
46
+ }
@@ -0,0 +1,45 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { getRemovedToolCallId } from "./cleanup.js";
3
+ import { buildPrunerState, filterContextMessages } from "./context.js";
4
+ import { readSessionEntries } from "../entries.js";
5
+ import { rewritePrunedSessionFile } from "./session.js";
6
+
7
+ function loadPrunerState(sessionFile: string | undefined): Set<string> {
8
+ const entries = readSessionEntries(sessionFile);
9
+ return entries ? buildPrunerState(entries) : new Set<string>();
10
+ }
11
+
12
+ export default function (pi: ExtensionAPI) {
13
+ let state = new Set<string>();
14
+
15
+ pi.on("session_start", (_event, ctx) => {
16
+ state = loadPrunerState(ctx.sessionManager.getSessionFile());
17
+ });
18
+
19
+ pi.on("session_tree", (_event, ctx) => {
20
+ state = loadPrunerState(ctx.sessionManager.getSessionFile());
21
+ });
22
+
23
+ pi.on("agent_end", (event) => {
24
+ for (const message of event.messages) {
25
+ if (message.role !== "toolResult") continue;
26
+ const toolCallId = getRemovedToolCallId(message);
27
+ if (toolCallId) state.add(toolCallId);
28
+ }
29
+ });
30
+
31
+ pi.on("session_shutdown", (event, ctx) => {
32
+ const sessionFile = ctx.sessionManager.getSessionFile();
33
+ if (sessionFile) {
34
+ rewritePrunedSessionFile(sessionFile, ctx.sessionManager.getLeafId(), false);
35
+ }
36
+ if (event.targetSessionFile && event.targetSessionFile !== sessionFile) {
37
+ rewritePrunedSessionFile(event.targetSessionFile, null, true);
38
+ }
39
+ });
40
+
41
+ pi.on("context", (event) => {
42
+ const pruned = filterContextMessages(event.messages, state);
43
+ if (pruned.changed) return { messages: pruned.messages };
44
+ });
45
+ }