@apholdings/jensen-code 0.0.4 → 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 (157) 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.map +1 -1
  9. package/dist/core/agent-session.js +10 -0
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/modes/interactive/components/assistant-message.d.ts +1 -6
  16. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  17. package/dist/modes/interactive/components/assistant-message.js +10 -40
  18. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  19. package/dist/modes/interactive/components/custom-editor.d.ts +1 -0
  20. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  21. package/dist/modes/interactive/components/custom-editor.js +5 -0
  22. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  23. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  24. package/dist/modes/interactive/components/tool-execution.js +1 -2
  25. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  26. package/dist/modes/interactive/components/top-bar.d.ts.map +1 -1
  27. package/dist/modes/interactive/components/top-bar.js +1 -1
  28. package/dist/modes/interactive/components/top-bar.js.map +1 -1
  29. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  30. package/dist/modes/interactive/components/user-message.js +1 -1
  31. package/dist/modes/interactive/components/user-message.js.map +1 -1
  32. package/dist/modes/interactive/interactive-mode.d.ts +6 -3
  33. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  34. package/dist/modes/interactive/interactive-mode.js +204 -86
  35. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  36. package/dist/utils/frontmatter.d.ts.map +1 -1
  37. package/dist/utils/frontmatter.js +8 -4
  38. package/dist/utils/frontmatter.js.map +1 -1
  39. package/dist/utils/tools-manager.d.ts.map +1 -1
  40. package/dist/utils/tools-manager.js +2 -2
  41. package/dist/utils/tools-manager.js.map +1 -1
  42. package/examples/extensions/osgrep.ts +643 -0
  43. package/examples/extensions/subagent/agents.ts +150 -37
  44. package/examples/extensions/subagent/index.ts +634 -513
  45. package/package.json +2 -2
  46. package/examples/README.md +0 -25
  47. package/examples/extensions/README.md +0 -206
  48. package/examples/extensions/antigravity-image-gen.ts +0 -415
  49. package/examples/extensions/auto-commit-on-exit.ts +0 -49
  50. package/examples/extensions/bash-spawn-hook.ts +0 -30
  51. package/examples/extensions/bookmark.ts +0 -50
  52. package/examples/extensions/built-in-tool-renderer.ts +0 -246
  53. package/examples/extensions/claude-rules.ts +0 -86
  54. package/examples/extensions/commands.ts +0 -72
  55. package/examples/extensions/confirm-destructive.ts +0 -59
  56. package/examples/extensions/custom-compaction.ts +0 -114
  57. package/examples/extensions/custom-footer.ts +0 -64
  58. package/examples/extensions/custom-header.ts +0 -73
  59. package/examples/extensions/custom-provider-anthropic/index.ts +0 -604
  60. package/examples/extensions/custom-provider-anthropic/package-lock.json +0 -24
  61. package/examples/extensions/custom-provider-anthropic/package.json +0 -19
  62. package/examples/extensions/custom-provider-gitlab-duo/index.ts +0 -349
  63. package/examples/extensions/custom-provider-gitlab-duo/package.json +0 -16
  64. package/examples/extensions/custom-provider-gitlab-duo/test.ts +0 -82
  65. package/examples/extensions/custom-provider-qwen-cli/index.ts +0 -345
  66. package/examples/extensions/custom-provider-qwen-cli/package.json +0 -16
  67. package/examples/extensions/dirty-repo-guard.ts +0 -56
  68. package/examples/extensions/doom-overlay/README.md +0 -46
  69. package/examples/extensions/doom-overlay/doom/build/doom.js +0 -21
  70. package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
  71. package/examples/extensions/doom-overlay/doom/build.sh +0 -152
  72. package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +0 -72
  73. package/examples/extensions/doom-overlay/doom-component.ts +0 -132
  74. package/examples/extensions/doom-overlay/doom-engine.ts +0 -173
  75. package/examples/extensions/doom-overlay/doom-keys.ts +0 -104
  76. package/examples/extensions/doom-overlay/index.ts +0 -74
  77. package/examples/extensions/doom-overlay/wad-finder.ts +0 -51
  78. package/examples/extensions/dynamic-resources/SKILL.md +0 -8
  79. package/examples/extensions/dynamic-resources/dynamic.json +0 -79
  80. package/examples/extensions/dynamic-resources/dynamic.md +0 -5
  81. package/examples/extensions/dynamic-resources/index.ts +0 -15
  82. package/examples/extensions/dynamic-tools.ts +0 -74
  83. package/examples/extensions/event-bus.ts +0 -43
  84. package/examples/extensions/file-trigger.ts +0 -41
  85. package/examples/extensions/git-checkpoint.ts +0 -53
  86. package/examples/extensions/handoff.ts +0 -150
  87. package/examples/extensions/hello.ts +0 -25
  88. package/examples/extensions/inline-bash.ts +0 -94
  89. package/examples/extensions/input-transform.ts +0 -43
  90. package/examples/extensions/interactive-shell.ts +0 -196
  91. package/examples/extensions/mac-system-theme.ts +0 -47
  92. package/examples/extensions/message-renderer.ts +0 -59
  93. package/examples/extensions/minimal-mode.ts +0 -426
  94. package/examples/extensions/modal-editor.ts +0 -85
  95. package/examples/extensions/model-status.ts +0 -31
  96. package/examples/extensions/notify.ts +0 -55
  97. package/examples/extensions/overlay-qa-tests.ts +0 -1348
  98. package/examples/extensions/overlay-test.ts +0 -150
  99. package/examples/extensions/permission-gate.ts +0 -34
  100. package/examples/extensions/pirate.ts +0 -47
  101. package/examples/extensions/plan-mode/README.md +0 -65
  102. package/examples/extensions/plan-mode/index.ts +0 -340
  103. package/examples/extensions/plan-mode/utils.ts +0 -168
  104. package/examples/extensions/preset.ts +0 -398
  105. package/examples/extensions/protected-paths.ts +0 -30
  106. package/examples/extensions/provider-payload.ts +0 -14
  107. package/examples/extensions/qna.ts +0 -119
  108. package/examples/extensions/question.ts +0 -264
  109. package/examples/extensions/questionnaire.ts +0 -427
  110. package/examples/extensions/rainbow-editor.ts +0 -88
  111. package/examples/extensions/reload-runtime.ts +0 -37
  112. package/examples/extensions/rpc-demo.ts +0 -124
  113. package/examples/extensions/sandbox/index.ts +0 -318
  114. package/examples/extensions/sandbox/package-lock.json +0 -92
  115. package/examples/extensions/sandbox/package.json +0 -19
  116. package/examples/extensions/send-user-message.ts +0 -97
  117. package/examples/extensions/session-name.ts +0 -27
  118. package/examples/extensions/shutdown-command.ts +0 -63
  119. package/examples/extensions/snake.ts +0 -343
  120. package/examples/extensions/space-invaders.ts +0 -560
  121. package/examples/extensions/ssh.ts +0 -220
  122. package/examples/extensions/status-line.ts +0 -40
  123. package/examples/extensions/subagent/README.md +0 -172
  124. package/examples/extensions/subagent/agents/planner.md +0 -37
  125. package/examples/extensions/subagent/agents/reviewer.md +0 -35
  126. package/examples/extensions/subagent/agents/scout.md +0 -50
  127. package/examples/extensions/subagent/agents/worker.md +0 -24
  128. package/examples/extensions/subagent/prompts/implement-and-review.md +0 -10
  129. package/examples/extensions/subagent/prompts/implement.md +0 -10
  130. package/examples/extensions/subagent/prompts/scout-and-plan.md +0 -9
  131. package/examples/extensions/summarize.ts +0 -195
  132. package/examples/extensions/system-prompt-header.ts +0 -17
  133. package/examples/extensions/timed-confirm.ts +0 -70
  134. package/examples/extensions/titlebar-spinner.ts +0 -58
  135. package/examples/extensions/todo.ts +0 -299
  136. package/examples/extensions/tool-override.ts +0 -143
  137. package/examples/extensions/tools.ts +0 -146
  138. package/examples/extensions/trigger-compact.ts +0 -40
  139. package/examples/extensions/truncated-tool.ts +0 -192
  140. package/examples/extensions/widget-placement.ts +0 -17
  141. package/examples/extensions/with-deps/index.ts +0 -32
  142. package/examples/extensions/with-deps/package-lock.json +0 -31
  143. package/examples/extensions/with-deps/package.json +0 -22
  144. package/examples/rpc-extension-ui.ts +0 -632
  145. package/examples/sdk/01-minimal.ts +0 -22
  146. package/examples/sdk/02-custom-model.ts +0 -49
  147. package/examples/sdk/03-custom-prompt.ts +0 -55
  148. package/examples/sdk/04-skills.ts +0 -46
  149. package/examples/sdk/05-tools.ts +0 -56
  150. package/examples/sdk/06-extensions.ts +0 -88
  151. package/examples/sdk/07-context-files.ts +0 -40
  152. package/examples/sdk/08-prompt-templates.ts +0 -47
  153. package/examples/sdk/09-api-keys-and-oauth.ts +0 -48
  154. package/examples/sdk/10-settings.ts +0 -51
  155. package/examples/sdk/11-sessions.ts +0 -48
  156. package/examples/sdk/12-full-control.ts +0 -82
  157. 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
+ }