@apholdings/jensen-code 0.0.3 → 0.0.5

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 (166) hide show
  1. package/dist/cli/args.d.ts.map +1 -1
  2. package/dist/cli/args.js +6 -6
  3. package/dist/cli/args.js.map +1 -1
  4. package/dist/config.d.ts +6 -5
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +32 -25
  7. package/dist/config.js.map +1 -1
  8. package/dist/core/agent-session.d.ts +1 -0
  9. package/dist/core/agent-session.d.ts.map +1 -1
  10. package/dist/core/agent-session.js +25 -0
  11. package/dist/core/agent-session.js.map +1 -1
  12. package/dist/core/extensions/loader.d.ts.map +1 -1
  13. package/dist/core/extensions/loader.js +1 -1
  14. package/dist/core/extensions/loader.js.map +1 -1
  15. package/dist/core/footer-data-provider.d.ts +4 -1
  16. package/dist/core/footer-data-provider.d.ts.map +1 -1
  17. package/dist/core/footer-data-provider.js +25 -11
  18. package/dist/core/footer-data-provider.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/modes/interactive/components/custom-editor.d.ts +1 -0
  24. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  25. package/dist/modes/interactive/components/custom-editor.js +5 -0
  26. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  27. package/dist/modes/interactive/components/footer.d.ts +0 -2
  28. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  29. package/dist/modes/interactive/components/footer.js +8 -146
  30. package/dist/modes/interactive/components/footer.js.map +1 -1
  31. package/dist/modes/interactive/components/header.d.ts +9 -3
  32. package/dist/modes/interactive/components/header.d.ts.map +1 -1
  33. package/dist/modes/interactive/components/header.js +125 -196
  34. package/dist/modes/interactive/components/header.js.map +1 -1
  35. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  36. package/dist/modes/interactive/components/tool-execution.js +1 -2
  37. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  38. package/dist/modes/interactive/interactive-mode.d.ts +23 -4
  39. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  40. package/dist/modes/interactive/interactive-mode.js +657 -243
  41. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  42. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  43. package/dist/modes/interactive/theme/theme.js +2 -0
  44. package/dist/modes/interactive/theme/theme.js.map +1 -1
  45. package/dist/utils/frontmatter.d.ts.map +1 -1
  46. package/dist/utils/frontmatter.js +8 -4
  47. package/dist/utils/frontmatter.js.map +1 -1
  48. package/dist/utils/tools-manager.d.ts.map +1 -1
  49. package/dist/utils/tools-manager.js +2 -2
  50. package/dist/utils/tools-manager.js.map +1 -1
  51. package/examples/extensions/osgrep.ts +643 -0
  52. package/examples/extensions/subagent/agents.ts +150 -38
  53. package/examples/extensions/subagent/index.ts +634 -514
  54. package/package.json +4 -3
  55. package/examples/README.md +0 -25
  56. package/examples/extensions/README.md +0 -206
  57. package/examples/extensions/antigravity-image-gen.ts +0 -416
  58. package/examples/extensions/auto-commit-on-exit.ts +0 -50
  59. package/examples/extensions/bash-spawn-hook.ts +0 -31
  60. package/examples/extensions/bookmark.ts +0 -51
  61. package/examples/extensions/built-in-tool-renderer.ts +0 -247
  62. package/examples/extensions/claude-rules.ts +0 -87
  63. package/examples/extensions/commands.ts +0 -73
  64. package/examples/extensions/confirm-destructive.ts +0 -60
  65. package/examples/extensions/custom-compaction.ts +0 -115
  66. package/examples/extensions/custom-footer.ts +0 -65
  67. package/examples/extensions/custom-header.ts +0 -74
  68. package/examples/extensions/custom-provider-anthropic/index.ts +0 -605
  69. package/examples/extensions/custom-provider-anthropic/package-lock.json +0 -24
  70. package/examples/extensions/custom-provider-anthropic/package.json +0 -19
  71. package/examples/extensions/custom-provider-gitlab-duo/index.ts +0 -350
  72. package/examples/extensions/custom-provider-gitlab-duo/package.json +0 -16
  73. package/examples/extensions/custom-provider-gitlab-duo/test.ts +0 -82
  74. package/examples/extensions/custom-provider-qwen-cli/index.ts +0 -346
  75. package/examples/extensions/custom-provider-qwen-cli/package.json +0 -16
  76. package/examples/extensions/dirty-repo-guard.ts +0 -57
  77. package/examples/extensions/doom-overlay/README.md +0 -46
  78. package/examples/extensions/doom-overlay/doom/build/doom.js +0 -21
  79. package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
  80. package/examples/extensions/doom-overlay/doom/build.sh +0 -152
  81. package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +0 -72
  82. package/examples/extensions/doom-overlay/doom-component.ts +0 -132
  83. package/examples/extensions/doom-overlay/doom-engine.ts +0 -173
  84. package/examples/extensions/doom-overlay/doom-keys.ts +0 -104
  85. package/examples/extensions/doom-overlay/index.ts +0 -75
  86. package/examples/extensions/doom-overlay/wad-finder.ts +0 -51
  87. package/examples/extensions/dynamic-resources/SKILL.md +0 -8
  88. package/examples/extensions/dynamic-resources/dynamic.json +0 -79
  89. package/examples/extensions/dynamic-resources/dynamic.md +0 -5
  90. package/examples/extensions/dynamic-resources/index.ts +0 -16
  91. package/examples/extensions/dynamic-tools.ts +0 -75
  92. package/examples/extensions/event-bus.ts +0 -44
  93. package/examples/extensions/file-trigger.ts +0 -42
  94. package/examples/extensions/git-checkpoint.ts +0 -54
  95. package/examples/extensions/handoff.ts +0 -151
  96. package/examples/extensions/hello.ts +0 -26
  97. package/examples/extensions/inline-bash.ts +0 -95
  98. package/examples/extensions/input-transform.ts +0 -44
  99. package/examples/extensions/interactive-shell.ts +0 -197
  100. package/examples/extensions/mac-system-theme.ts +0 -48
  101. package/examples/extensions/message-renderer.ts +0 -60
  102. package/examples/extensions/minimal-mode.ts +0 -427
  103. package/examples/extensions/modal-editor.ts +0 -86
  104. package/examples/extensions/model-status.ts +0 -32
  105. package/examples/extensions/notify.ts +0 -56
  106. package/examples/extensions/overlay-qa-tests.ts +0 -1349
  107. package/examples/extensions/overlay-test.ts +0 -151
  108. package/examples/extensions/permission-gate.ts +0 -35
  109. package/examples/extensions/pirate.ts +0 -48
  110. package/examples/extensions/plan-mode/README.md +0 -65
  111. package/examples/extensions/plan-mode/index.ts +0 -341
  112. package/examples/extensions/plan-mode/utils.ts +0 -168
  113. package/examples/extensions/preset.ts +0 -399
  114. package/examples/extensions/protected-paths.ts +0 -31
  115. package/examples/extensions/provider-payload.ts +0 -15
  116. package/examples/extensions/qna.ts +0 -120
  117. package/examples/extensions/question.ts +0 -265
  118. package/examples/extensions/questionnaire.ts +0 -428
  119. package/examples/extensions/rainbow-editor.ts +0 -89
  120. package/examples/extensions/reload-runtime.ts +0 -38
  121. package/examples/extensions/rpc-demo.ts +0 -125
  122. package/examples/extensions/sandbox/index.ts +0 -319
  123. package/examples/extensions/sandbox/package-lock.json +0 -92
  124. package/examples/extensions/sandbox/package.json +0 -19
  125. package/examples/extensions/send-user-message.ts +0 -98
  126. package/examples/extensions/session-name.ts +0 -28
  127. package/examples/extensions/shutdown-command.ts +0 -64
  128. package/examples/extensions/snake.ts +0 -344
  129. package/examples/extensions/space-invaders.ts +0 -561
  130. package/examples/extensions/ssh.ts +0 -221
  131. package/examples/extensions/status-line.ts +0 -41
  132. package/examples/extensions/subagent/README.md +0 -172
  133. package/examples/extensions/subagent/agents/planner.md +0 -37
  134. package/examples/extensions/subagent/agents/reviewer.md +0 -35
  135. package/examples/extensions/subagent/agents/scout.md +0 -50
  136. package/examples/extensions/subagent/agents/worker.md +0 -24
  137. package/examples/extensions/subagent/prompts/implement-and-review.md +0 -10
  138. package/examples/extensions/subagent/prompts/implement.md +0 -10
  139. package/examples/extensions/subagent/prompts/scout-and-plan.md +0 -9
  140. package/examples/extensions/summarize.ts +0 -196
  141. package/examples/extensions/system-prompt-header.ts +0 -18
  142. package/examples/extensions/timed-confirm.ts +0 -71
  143. package/examples/extensions/titlebar-spinner.ts +0 -59
  144. package/examples/extensions/todo.ts +0 -300
  145. package/examples/extensions/tool-override.ts +0 -144
  146. package/examples/extensions/tools.ts +0 -147
  147. package/examples/extensions/trigger-compact.ts +0 -41
  148. package/examples/extensions/truncated-tool.ts +0 -193
  149. package/examples/extensions/widget-placement.ts +0 -18
  150. package/examples/extensions/with-deps/index.ts +0 -33
  151. package/examples/extensions/with-deps/package-lock.json +0 -31
  152. package/examples/extensions/with-deps/package.json +0 -22
  153. package/examples/rpc-extension-ui.ts +0 -632
  154. package/examples/sdk/01-minimal.ts +0 -23
  155. package/examples/sdk/02-custom-model.ts +0 -50
  156. package/examples/sdk/03-custom-prompt.ts +0 -56
  157. package/examples/sdk/04-skills.ts +0 -47
  158. package/examples/sdk/05-tools.ts +0 -57
  159. package/examples/sdk/06-extensions.ts +0 -89
  160. package/examples/sdk/07-context-files.ts +0 -41
  161. package/examples/sdk/08-prompt-templates.ts +0 -48
  162. package/examples/sdk/09-api-keys-and-oauth.ts +0 -49
  163. package/examples/sdk/10-settings.ts +0 -52
  164. package/examples/sdk/11-sessions.ts +0 -49
  165. package/examples/sdk/12-full-control.ts +0 -83
  166. package/examples/sdk/README.md +0 -145
@@ -0,0 +1,643 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtempSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ type AgentToolResult,
7
+ DEFAULT_MAX_BYTES,
8
+ DEFAULT_MAX_LINES,
9
+ type ExtensionAPI,
10
+ formatSize,
11
+ type ToolRenderResultOptions,
12
+ type TruncationResult,
13
+ truncateHead,
14
+ } from "@apholdings/jensen-code";
15
+ import { Text } from "@apholdings/jensen-tui";
16
+ import { Type } from "@sinclair/typebox";
17
+
18
+ const OSGREP_BIN = process.platform === "win32" ? "osgrep.cmd" : "osgrep";
19
+
20
+ const OsgrepSearchParams = Type.Object({
21
+ query: Type.String({
22
+ description: "Semantic search query, e.g. 'where do we validate JWT tokens?'",
23
+ }),
24
+ maxResults: Type.Optional(
25
+ Type.Integer({
26
+ minimum: 1,
27
+ maximum: 200,
28
+ description: "Maximum total results to return (-m). Default: 12",
29
+ }),
30
+ ),
31
+ perFile: Type.Optional(
32
+ Type.Integer({
33
+ minimum: 1,
34
+ maximum: 50,
35
+ description: "Maximum matches per file (--per-file). Default: 2",
36
+ }),
37
+ ),
38
+ content: Type.Optional(
39
+ Type.Boolean({
40
+ description: "Show full chunk content instead of snippets (--content)",
41
+ default: false,
42
+ }),
43
+ ),
44
+ scores: Type.Optional(
45
+ Type.Boolean({
46
+ description: "Show relevance scores (--scores)",
47
+ default: false,
48
+ }),
49
+ ),
50
+ minScore: Type.Optional(
51
+ Type.Number({
52
+ minimum: 0,
53
+ maximum: 1,
54
+ description: "Minimum score threshold (--min-score)",
55
+ }),
56
+ ),
57
+ compact: Type.Optional(
58
+ Type.Boolean({
59
+ description: "Show file paths only (--compact)",
60
+ default: false,
61
+ }),
62
+ ),
63
+ sync: Type.Optional(
64
+ Type.Boolean({
65
+ description: "Force re-index changed files before searching (--sync)",
66
+ default: false,
67
+ }),
68
+ ),
69
+ reset: Type.Optional(
70
+ Type.Boolean({
71
+ description: "Reset the index and re-index from scratch before searching (--reset)",
72
+ default: false,
73
+ }),
74
+ ),
75
+ });
76
+
77
+ const OsgrepTraceParams = Type.Object({
78
+ symbol: Type.String({
79
+ description: "Function, method, or symbol to trace, e.g. 'registerTool'",
80
+ }),
81
+ });
82
+
83
+ interface BaseDetails {
84
+ tool: "osgrep_search" | "osgrep_trace";
85
+ cwd: string;
86
+ args: string[];
87
+ outputLines: number;
88
+ exitCode?: number | null;
89
+ durationMs?: number;
90
+ truncation?: TruncationResult;
91
+ fullOutputPath?: string;
92
+ error?: string;
93
+ }
94
+
95
+ interface OsgrepSearchDetails extends BaseDetails {
96
+ tool: "osgrep_search";
97
+ query: string;
98
+ }
99
+
100
+ interface OsgrepTraceDetails extends BaseDetails {
101
+ tool: "osgrep_trace";
102
+ symbol: string;
103
+ }
104
+
105
+ type OsgrepDetails = OsgrepSearchDetails | OsgrepTraceDetails;
106
+
107
+ function getTextContent(result: { content?: Array<{ type: string; text?: string }> }): string {
108
+ const block = result.content?.find((c) => c.type === "text");
109
+ return block?.text ?? "";
110
+ }
111
+
112
+ function countOutputLines(text: string): number {
113
+ return text
114
+ .split("\n")
115
+ .map((line) => line.trim())
116
+ .filter(Boolean).length;
117
+ }
118
+
119
+ function summarizeError(err: unknown): string {
120
+ if (typeof err === "string") return err;
121
+ if (err && typeof err === "object" && "message" in err) {
122
+ const message = (err as { message?: unknown }).message;
123
+ if (typeof message === "string") return message;
124
+ }
125
+ return "unknown error";
126
+ }
127
+
128
+ async function runOsgrep(
129
+ args: string[],
130
+ cwd: string,
131
+ signal?: AbortSignal,
132
+ ): Promise<{
133
+ stdout: string;
134
+ stderr: string;
135
+ code: number | null;
136
+ durationMs: number;
137
+ }> {
138
+ const started = Date.now();
139
+
140
+ return await new Promise((resolve, reject) => {
141
+ const child = spawn(OSGREP_BIN, args, {
142
+ cwd,
143
+ env: {
144
+ ...process.env,
145
+ NO_COLOR: "1",
146
+ },
147
+ stdio: ["ignore", "pipe", "pipe"],
148
+ windowsHide: true,
149
+ });
150
+
151
+ let stdout = "";
152
+ let stderr = "";
153
+ let settled = false;
154
+
155
+ const finish = (fn: () => void) => {
156
+ if (settled) return;
157
+ settled = true;
158
+ signal?.removeEventListener("abort", onAbort);
159
+ fn();
160
+ };
161
+
162
+ const onAbort = () => {
163
+ try {
164
+ child.kill("SIGTERM");
165
+ } catch {
166
+ // ignore
167
+ }
168
+
169
+ setTimeout(() => {
170
+ try {
171
+ child.kill("SIGKILL");
172
+ } catch {
173
+ // ignore
174
+ }
175
+ }, 250).unref?.();
176
+ };
177
+
178
+ signal?.addEventListener("abort", onAbort, { once: true });
179
+
180
+ child.stdout.on("data", (chunk) => {
181
+ stdout += chunk.toString();
182
+ });
183
+
184
+ child.stderr.on("data", (chunk) => {
185
+ stderr += chunk.toString();
186
+ });
187
+
188
+ child.on("error", (err) => {
189
+ finish(() => reject(err));
190
+ });
191
+
192
+ child.on("close", (code) => {
193
+ finish(() =>
194
+ resolve({
195
+ stdout: stdout.trim(),
196
+ stderr: stderr.trim(),
197
+ code,
198
+ durationMs: Date.now() - started,
199
+ }),
200
+ );
201
+ });
202
+ });
203
+ }
204
+
205
+ function withTruncation(text: string, details: OsgrepDetails): string {
206
+ const truncation = truncateHead(text, {
207
+ maxLines: DEFAULT_MAX_LINES,
208
+ maxBytes: DEFAULT_MAX_BYTES,
209
+ });
210
+
211
+ let resultText = truncation.content;
212
+
213
+ if (truncation.truncated) {
214
+ const tempDir = mkdtempSync(join(tmpdir(), "pi-osgrep-"));
215
+ const tempFile = join(tempDir, "output.txt");
216
+ writeFileSync(tempFile, text, { mode: 0o600 });
217
+
218
+ details.truncation = truncation;
219
+ details.fullOutputPath = tempFile;
220
+
221
+ resultText += `\n\n[Output truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`;
222
+ resultText += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
223
+ resultText += ` Full output saved to ${tempFile}]`;
224
+ }
225
+
226
+ return resultText;
227
+ }
228
+
229
+ function renderSearchCall(
230
+ args: {
231
+ query: string;
232
+ maxResults?: number;
233
+ perFile?: number;
234
+ content?: boolean;
235
+ scores?: boolean;
236
+ minScore?: number;
237
+ compact?: boolean;
238
+ sync?: boolean;
239
+ reset?: boolean;
240
+ },
241
+ theme: any,
242
+ ) {
243
+ const flags: string[] = [];
244
+ if (args.maxResults !== undefined) flags.push(`m=${args.maxResults}`);
245
+ if (args.perFile !== undefined) flags.push(`per-file=${args.perFile}`);
246
+ if (args.content) flags.push("content");
247
+ if (args.scores) flags.push("scores");
248
+ if (args.compact) flags.push("compact");
249
+ if (args.sync) flags.push("sync");
250
+ if (args.reset) flags.push("reset");
251
+ if (typeof args.minScore === "number") flags.push(`min-score=${args.minScore}`);
252
+
253
+ let text = theme.fg("toolTitle", theme.bold("osgrep_search ")) + theme.fg("accent", JSON.stringify(args.query));
254
+
255
+ if (flags.length > 0) {
256
+ text += theme.fg("muted", ` [${flags.join(" ")}]`);
257
+ }
258
+
259
+ return new Text(text, 0, 0);
260
+ }
261
+
262
+ function renderTraceCall(args: { symbol: string }, theme: any) {
263
+ const text = theme.fg("toolTitle", theme.bold("osgrep_trace ")) + theme.fg("accent", JSON.stringify(args.symbol));
264
+
265
+ return new Text(text, 0, 0);
266
+ }
267
+
268
+ function renderToolResult(
269
+ result: AgentToolResult<OsgrepDetails> & { isError?: boolean },
270
+ expanded: boolean,
271
+ theme: any,
272
+ ) {
273
+ const details = result.details;
274
+ const content = getTextContent(result);
275
+
276
+ let header = theme.fg("toolTitle", theme.bold(details?.tool ?? "osgrep"));
277
+ if (details) header += theme.fg("muted", ` · ${details.outputLines} lines`);
278
+ if (details?.durationMs !== undefined) header += theme.fg("muted", ` · ${details.durationMs}ms`);
279
+ if (details?.truncation?.truncated) header += theme.fg("warning", " · truncated");
280
+ if (details?.error || result.isError) header += theme.fg("error", " · error");
281
+
282
+ if (expanded) {
283
+ return new Text(`${header}\n${content || "(no output)"}`, 0, 0);
284
+ }
285
+
286
+ const lines = (content || "(no output)").split("\n");
287
+ const preview = lines.slice(0, 12).join("\n");
288
+ const needsMore = lines.length > 12;
289
+
290
+ let text = `${header}\n${preview}`;
291
+ if (needsMore) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
292
+
293
+ return new Text(text, 0, 0);
294
+ }
295
+
296
+ export default function (pi: ExtensionAPI) {
297
+ pi.registerTool<typeof OsgrepSearchParams, OsgrepSearchDetails>({
298
+ name: "osgrep_search",
299
+ label: "osgrep search",
300
+ description: `Semantic code search using local osgrep in the current working directory. Best for concept-based repository reconnaissance. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)}; if truncated, full output is saved to a temp file.`,
301
+ promptSnippet: "Semantic repository search with osgrep for concept-level code discovery.",
302
+ promptGuidelines: [
303
+ "Use osgrep_search when the task is about behavior, responsibility, or architecture rather than exact text.",
304
+ "Use grep for exact strings/symbols and osgrep_search for semantic reconnaissance.",
305
+ "Use compact=true for broad discovery, then rerun a targeted query for detailed content.",
306
+ "Keep queries concrete and repository-specific.",
307
+ ],
308
+ parameters: OsgrepSearchParams,
309
+
310
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
311
+ if (params.content && params.compact) {
312
+ return {
313
+ content: [
314
+ {
315
+ type: "text",
316
+ text: "Invalid parameters: content and compact cannot both be true.",
317
+ },
318
+ ],
319
+ details: {
320
+ tool: "osgrep_search",
321
+ query: params.query,
322
+ cwd: ctx.cwd,
323
+ args: [],
324
+ outputLines: 0,
325
+ error: "invalid_parameters",
326
+ } as OsgrepSearchDetails,
327
+ isError: true,
328
+ };
329
+ }
330
+
331
+ const args: string[] = [];
332
+ const maxResults = params.maxResults ?? 12;
333
+ const perFile = params.perFile ?? 2;
334
+
335
+ args.push("-m", String(maxResults));
336
+ args.push("--per-file", String(perFile));
337
+
338
+ if (params.content) args.push("--content");
339
+ if (params.scores) args.push("--scores");
340
+ if (typeof params.minScore === "number") args.push("--min-score", String(params.minScore));
341
+ if (params.compact) args.push("--compact");
342
+ if (params.sync) args.push("--sync");
343
+ if (params.reset) args.push("--reset");
344
+
345
+ args.push(params.query);
346
+
347
+ try {
348
+ const execResult = await runOsgrep(args, ctx.cwd, signal);
349
+ const combined = [execResult.stderr, execResult.stdout].filter(Boolean).join("\n").trim();
350
+
351
+ if (signal?.aborted) {
352
+ return {
353
+ content: [{ type: "text", text: "osgrep search was aborted." }],
354
+ details: {
355
+ tool: "osgrep_search",
356
+ query: params.query,
357
+ cwd: ctx.cwd,
358
+ args,
359
+ outputLines: 0,
360
+ exitCode: execResult.code,
361
+ durationMs: execResult.durationMs,
362
+ error: "aborted",
363
+ } as OsgrepSearchDetails,
364
+ isError: true,
365
+ };
366
+ }
367
+
368
+ if (execResult.code && execResult.code !== 0) {
369
+ if (execResult.code === 1 || /no matches/i.test(combined)) {
370
+ return {
371
+ content: [{ type: "text", text: "No matches found" }],
372
+ details: {
373
+ tool: "osgrep_search",
374
+ query: params.query,
375
+ cwd: ctx.cwd,
376
+ args,
377
+ outputLines: 0,
378
+ exitCode: execResult.code,
379
+ durationMs: execResult.durationMs,
380
+ } as OsgrepSearchDetails,
381
+ };
382
+ }
383
+
384
+ return {
385
+ content: [
386
+ {
387
+ type: "text",
388
+ text: `osgrep failed: ${combined || `exit code ${String(execResult.code)}`}`,
389
+ },
390
+ ],
391
+ details: {
392
+ tool: "osgrep_search",
393
+ query: params.query,
394
+ cwd: ctx.cwd,
395
+ args,
396
+ outputLines: 0,
397
+ exitCode: execResult.code,
398
+ durationMs: execResult.durationMs,
399
+ error: combined || `exit code ${String(execResult.code)}`,
400
+ } as OsgrepSearchDetails,
401
+ isError: true,
402
+ };
403
+ }
404
+
405
+ if (!execResult.stdout.trim()) {
406
+ return {
407
+ content: [{ type: "text", text: "No matches found" }],
408
+ details: {
409
+ tool: "osgrep_search",
410
+ query: params.query,
411
+ cwd: ctx.cwd,
412
+ args,
413
+ outputLines: 0,
414
+ exitCode: execResult.code,
415
+ durationMs: execResult.durationMs,
416
+ } as OsgrepSearchDetails,
417
+ };
418
+ }
419
+
420
+ const details: OsgrepSearchDetails = {
421
+ tool: "osgrep_search",
422
+ query: params.query,
423
+ cwd: ctx.cwd,
424
+ args,
425
+ outputLines: countOutputLines(execResult.stdout),
426
+ exitCode: execResult.code,
427
+ durationMs: execResult.durationMs,
428
+ };
429
+
430
+ const resultText = withTruncation(execResult.stdout, details);
431
+
432
+ return {
433
+ content: [{ type: "text", text: resultText }],
434
+ details,
435
+ };
436
+ } catch (err) {
437
+ const message = summarizeError(err);
438
+
439
+ if (/ENOENT/i.test(message) || /not found/i.test(message)) {
440
+ return {
441
+ content: [
442
+ {
443
+ type: "text",
444
+ text: "osgrep executable not found in PATH. Install osgrep and ensure Jensen can see it.",
445
+ },
446
+ ],
447
+ details: {
448
+ tool: "osgrep_search",
449
+ query: params.query,
450
+ cwd: ctx.cwd,
451
+ args,
452
+ outputLines: 0,
453
+ error: "ENOENT",
454
+ } as OsgrepSearchDetails,
455
+ isError: true,
456
+ };
457
+ }
458
+
459
+ return {
460
+ content: [
461
+ {
462
+ type: "text",
463
+ text: `osgrep failed: ${message}`,
464
+ },
465
+ ],
466
+ details: {
467
+ tool: "osgrep_search",
468
+ query: params.query,
469
+ cwd: ctx.cwd,
470
+ args,
471
+ outputLines: 0,
472
+ error: message,
473
+ } as OsgrepSearchDetails,
474
+ isError: true,
475
+ };
476
+ }
477
+ },
478
+
479
+ renderCall(args, theme) {
480
+ return renderSearchCall(args, theme);
481
+ },
482
+
483
+ renderResult(result: AgentToolResult<OsgrepSearchDetails>, { expanded }: ToolRenderResultOptions, theme) {
484
+ return renderToolResult(result, expanded, theme);
485
+ },
486
+ });
487
+
488
+ pi.registerTool<typeof OsgrepTraceParams, OsgrepTraceDetails>({
489
+ name: "osgrep_trace",
490
+ label: "osgrep trace",
491
+ description: `Call-graph tracing using local osgrep in the current working directory. Use it for impact analysis: who calls a symbol and what it calls. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)}; if truncated, full output is saved to a temp file.`,
492
+ promptSnippet: "Call-graph and impact tracing with osgrep for upstream/downstream code understanding.",
493
+ promptGuidelines: [
494
+ "Use osgrep_trace when you need blast-radius analysis for a function, method, or symbol.",
495
+ "Use osgrep_trace before refactors that may affect callers or callees.",
496
+ "Use osgrep_search to find concepts; use osgrep_trace to understand dependencies around a specific symbol.",
497
+ ],
498
+ parameters: OsgrepTraceParams,
499
+
500
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
501
+ const args = ["trace", params.symbol];
502
+
503
+ try {
504
+ const execResult = await runOsgrep(args, ctx.cwd, signal);
505
+ const combined = [execResult.stderr, execResult.stdout].filter(Boolean).join("\n").trim();
506
+
507
+ if (signal?.aborted) {
508
+ return {
509
+ content: [{ type: "text", text: "osgrep trace was aborted." }],
510
+ details: {
511
+ tool: "osgrep_trace",
512
+ symbol: params.symbol,
513
+ cwd: ctx.cwd,
514
+ args,
515
+ outputLines: 0,
516
+ exitCode: execResult.code,
517
+ durationMs: execResult.durationMs,
518
+ error: "aborted",
519
+ } as OsgrepTraceDetails,
520
+ isError: true,
521
+ };
522
+ }
523
+
524
+ if (execResult.code && execResult.code !== 0) {
525
+ if (execResult.code === 1 || /no matches|not found|no trace/i.test(combined)) {
526
+ return {
527
+ content: [{ type: "text", text: "No trace data found" }],
528
+ details: {
529
+ tool: "osgrep_trace",
530
+ symbol: params.symbol,
531
+ cwd: ctx.cwd,
532
+ args,
533
+ outputLines: 0,
534
+ exitCode: execResult.code,
535
+ durationMs: execResult.durationMs,
536
+ } as OsgrepTraceDetails,
537
+ };
538
+ }
539
+
540
+ return {
541
+ content: [
542
+ {
543
+ type: "text",
544
+ text: `osgrep trace failed: ${combined || `exit code ${String(execResult.code)}`}`,
545
+ },
546
+ ],
547
+ details: {
548
+ tool: "osgrep_trace",
549
+ symbol: params.symbol,
550
+ cwd: ctx.cwd,
551
+ args,
552
+ outputLines: 0,
553
+ exitCode: execResult.code,
554
+ durationMs: execResult.durationMs,
555
+ error: combined || `exit code ${String(execResult.code)}`,
556
+ } as OsgrepTraceDetails,
557
+ isError: true,
558
+ };
559
+ }
560
+
561
+ if (!execResult.stdout.trim()) {
562
+ return {
563
+ content: [{ type: "text", text: "No trace data found" }],
564
+ details: {
565
+ tool: "osgrep_trace",
566
+ symbol: params.symbol,
567
+ cwd: ctx.cwd,
568
+ args,
569
+ outputLines: 0,
570
+ exitCode: execResult.code,
571
+ durationMs: execResult.durationMs,
572
+ } as OsgrepTraceDetails,
573
+ };
574
+ }
575
+
576
+ const details: OsgrepTraceDetails = {
577
+ tool: "osgrep_trace",
578
+ symbol: params.symbol,
579
+ cwd: ctx.cwd,
580
+ args,
581
+ outputLines: countOutputLines(execResult.stdout),
582
+ exitCode: execResult.code,
583
+ durationMs: execResult.durationMs,
584
+ };
585
+
586
+ const resultText = withTruncation(execResult.stdout, details);
587
+
588
+ return {
589
+ content: [{ type: "text", text: resultText }],
590
+ details,
591
+ };
592
+ } catch (err) {
593
+ const message = summarizeError(err);
594
+
595
+ if (/ENOENT/i.test(message) || /not found/i.test(message)) {
596
+ return {
597
+ content: [
598
+ {
599
+ type: "text",
600
+ text: "osgrep executable not found in PATH. Install osgrep and ensure Jensen can see it.",
601
+ },
602
+ ],
603
+ details: {
604
+ tool: "osgrep_trace",
605
+ symbol: params.symbol,
606
+ cwd: ctx.cwd,
607
+ args,
608
+ outputLines: 0,
609
+ error: "ENOENT",
610
+ } as OsgrepTraceDetails,
611
+ isError: true,
612
+ };
613
+ }
614
+
615
+ return {
616
+ content: [
617
+ {
618
+ type: "text",
619
+ text: `osgrep trace failed: ${message}`,
620
+ },
621
+ ],
622
+ details: {
623
+ tool: "osgrep_trace",
624
+ symbol: params.symbol,
625
+ cwd: ctx.cwd,
626
+ args,
627
+ outputLines: 0,
628
+ error: message,
629
+ } as OsgrepTraceDetails,
630
+ isError: true,
631
+ };
632
+ }
633
+ },
634
+
635
+ renderCall(args, theme) {
636
+ return renderTraceCall(args, theme);
637
+ },
638
+
639
+ renderResult(result: AgentToolResult<OsgrepTraceDetails>, { expanded }: ToolRenderResultOptions, theme) {
640
+ return renderToolResult(result, expanded, theme);
641
+ },
642
+ });
643
+ }