jeopi-cli 16.4.1 → 16.4.3
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.
- package/CHANGELOG.md +80 -0
- package/dist/cli.js +3179 -3140
- package/dist/types/advisor/config.d.ts +4 -3
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/commit/agentic/agent.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +7 -1
- package/dist/types/config/settings-schema.d.ts +3 -3
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -6
- package/dist/types/internal-urls/types.d.ts +7 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/lsp/deferred-diagnostics.d.ts +11 -0
- package/dist/types/modes/components/move-overlay.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/welcome.d.ts +4 -0
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/session/auth-broker-config.d.ts +2 -2
- package/dist/types/tools/bash-skill-urls.d.ts +3 -2
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -1
- package/dist/types/tools/browser/cmux/rpc.d.ts +20 -0
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +17 -2
- package/dist/types/tools/browser/run-output.d.ts +25 -0
- package/dist/types/tools/browser/tab-worker.d.ts +22 -0
- package/dist/types/utils/changelog.d.ts +35 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +13 -0
- package/src/advisor/__tests__/config.test.ts +36 -0
- package/src/advisor/config.ts +11 -8
- package/src/autolearn/controller.ts +14 -2
- package/src/cli/args.ts +12 -0
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +63 -13
- package/src/cli/usage-error.ts +7 -0
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commit/agentic/agent.ts +4 -0
- package/src/commit/agentic/index.ts +46 -21
- package/src/config/model-resolver.ts +24 -6
- package/src/config/settings-schema.ts +3 -3
- package/src/edit/index.ts +10 -76
- package/src/edit/renderer.ts +13 -10
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +60 -0
- package/src/eval/executor-base.ts +84 -7
- package/src/eval/js/shared/rewrite-imports.ts +31 -13
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/ttsr.ts +0 -3
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/plugins/manager.ts +21 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/memory-protocol.ts +13 -8
- package/src/internal-urls/skill-protocol.ts +1 -1
- package/src/internal-urls/types.ts +7 -0
- package/src/irc/bus.ts +48 -18
- package/src/lsp/client.ts +7 -1
- package/src/lsp/deferred-diagnostics.ts +66 -0
- package/src/main.ts +13 -13
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/mcp/transports/stdio.test.ts +45 -1
- package/src/mcp/transports/stdio.ts +6 -3
- package/src/modes/acp/acp-agent.ts +73 -2
- package/src/modes/acp/acp-event-mapper.ts +5 -0
- package/src/modes/acp/acp-mode.ts +11 -0
- package/src/modes/components/__tests__/move-overlay.test.ts +16 -1
- package/src/modes/components/advisor-config.ts +15 -7
- package/src/modes/components/move-overlay.ts +2 -3
- package/src/modes/components/plan-review-overlay.ts +7 -0
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/welcome.ts +13 -14
- package/src/modes/controllers/command-controller.ts +8 -10
- package/src/modes/controllers/event-controller.ts +17 -30
- package/src/modes/controllers/input-controller.ts +9 -6
- package/src/modes/controllers/selector-controller.ts +3 -2
- package/src/modes/controllers/tan-command-controller.ts +40 -1
- package/src/modes/interactive-mode.ts +13 -0
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/workflow.ts +5 -4
- package/src/prompts/system/eager-task.md +2 -2
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/tools/browser.md +5 -5
- package/src/sdk.ts +3 -0
- package/src/session/agent-session.ts +237 -50
- package/src/session/auth-broker-config.ts +4 -4
- package/src/session/snapcompact-inline.ts +3 -19
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/builtin-registry.ts +8 -8
- package/src/task/executor.ts +8 -0
- package/src/tools/bash-skill-urls.ts +7 -4
- package/src/tools/bash.ts +30 -22
- package/src/tools/browser/cmux/cmux-tab.ts +49 -50
- package/src/tools/browser/cmux/rpc.ts +50 -0
- package/src/tools/browser/launch.ts +8 -4
- package/src/tools/browser/run-cancellation.ts +44 -5
- package/src/tools/browser/run-output.ts +76 -0
- package/src/tools/browser/tab-worker.ts +280 -133
- package/src/tools/eval-render.ts +16 -14
- package/src/tools/glob.ts +20 -6
- package/src/tools/grep.ts +1 -0
- package/src/tools/image-gen.ts +3 -4
- package/src/tools/irc.ts +27 -13
- package/src/tools/read.ts +14 -7
- package/src/tools/write.ts +22 -5
- package/src/tools/yield.ts +29 -1
- package/src/utils/changelog.ts +106 -9
- package/src/utils/git.ts +0 -1
- package/src/utils/title-generator.ts +68 -7
- package/src/web/search/index.ts +14 -6
- package/src/web/search/providers/perplexity.ts +18 -2
package/src/tools/eval-render.ts
CHANGED
|
@@ -414,37 +414,39 @@ function formatStatusEventExpanded(event: EvalStatusEvent, theme: Theme): string
|
|
|
414
414
|
return lines;
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
-
/**
|
|
417
|
+
/**
|
|
418
|
+
* Render status events as tree lines. Shows a tail window (newest events are
|
|
419
|
+
* the live edge for `log()` progress loops) behind an "… N earlier" marker,
|
|
420
|
+
* matching the code/output tail-window convention. Collapsed keeps a small
|
|
421
|
+
* fixed window; expanded widens to the viewport-sized preview window.
|
|
422
|
+
*/
|
|
418
423
|
function renderStatusEvents(events: EvalStatusEvent[], theme: Theme, expanded: boolean): string[] {
|
|
419
424
|
if (events.length === 0) return [];
|
|
420
425
|
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
const
|
|
426
|
+
const max = expanded ? Math.max(10, previewWindowRows()) : 3;
|
|
427
|
+
const hidden = Math.max(0, events.length - max);
|
|
428
|
+
const visible = hidden > 0 ? events.slice(hidden) : events;
|
|
424
429
|
|
|
425
430
|
const lines: string[] = [];
|
|
426
|
-
|
|
427
|
-
|
|
431
|
+
if (hidden > 0) {
|
|
432
|
+
lines.push(`${theme.fg("dim", theme.tree.branch)} ${theme.fg("dim", `… ${hidden} earlier`)}`);
|
|
433
|
+
}
|
|
434
|
+
for (let i = 0; i < visible.length; i++) {
|
|
435
|
+
const isLast = i === visible.length - 1;
|
|
428
436
|
const branch = isLast ? theme.tree.last : theme.tree.branch;
|
|
429
437
|
|
|
430
438
|
if (expanded) {
|
|
431
|
-
const eventLines = formatStatusEventExpanded(
|
|
439
|
+
const eventLines = formatStatusEventExpanded(visible[i], theme);
|
|
432
440
|
lines.push(`${theme.fg("dim", branch)} ${eventLines[0]}`);
|
|
433
441
|
const continueBranch = isLast ? " " : `${theme.tree.vertical} `;
|
|
434
442
|
for (let j = 1; j < eventLines.length; j++) {
|
|
435
443
|
lines.push(`${theme.fg("dim", continueBranch)}${eventLines[j]}`);
|
|
436
444
|
}
|
|
437
445
|
} else {
|
|
438
|
-
lines.push(`${theme.fg("dim", branch)} ${formatStatusEvent(
|
|
446
|
+
lines.push(`${theme.fg("dim", branch)} ${formatStatusEvent(visible[i], theme)}`);
|
|
439
447
|
}
|
|
440
448
|
}
|
|
441
449
|
|
|
442
|
-
if (!expanded && events.length > maxCollapsed) {
|
|
443
|
-
lines.push(`${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", `… ${events.length - maxCollapsed} more`)}`);
|
|
444
|
-
} else if (expanded && events.length > maxExpanded) {
|
|
445
|
-
lines.push(`${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", `… ${events.length - maxExpanded} more`)}`);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
450
|
return lines;
|
|
449
451
|
}
|
|
450
452
|
|
package/src/tools/glob.ts
CHANGED
|
@@ -173,6 +173,7 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
|
|
|
173
173
|
signal,
|
|
174
174
|
localProtocolOptions: this.session.localProtocolOptions,
|
|
175
175
|
skills: this.session.skills,
|
|
176
|
+
pathOnly: true,
|
|
176
177
|
});
|
|
177
178
|
if (!resource.sourcePath) {
|
|
178
179
|
throw new ToolError(`Cannot find internal URL without a backing file: ${rawPattern}`);
|
|
@@ -247,7 +248,7 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
|
|
|
247
248
|
|
|
248
249
|
const buildResult = (
|
|
249
250
|
files: string[],
|
|
250
|
-
opts?: { notice?: string; forceTruncated?: boolean },
|
|
251
|
+
opts?: { notice?: string; forceTruncated?: boolean; timedOut?: boolean },
|
|
251
252
|
): AgentToolResult<GlobToolDetails> => {
|
|
252
253
|
const notice = opts?.notice;
|
|
253
254
|
const forceTruncated = opts?.forceTruncated ?? false;
|
|
@@ -260,7 +261,10 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
|
|
|
260
261
|
cwd: this.session.cwd,
|
|
261
262
|
missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
|
|
262
263
|
};
|
|
263
|
-
|
|
264
|
+
// A timed-out empty result is an incomplete scan, not a verified
|
|
265
|
+
// absence — never emit the definitive "No files found" claim next
|
|
266
|
+
// to a timeout notice (the two statements contradict each other).
|
|
267
|
+
const parts = opts?.timedOut ? [] : ["No files found matching pattern"];
|
|
264
268
|
if (notice) parts.push(notice);
|
|
265
269
|
if (missingPathsNote) parts.push(missingPathsNote);
|
|
266
270
|
// Zero results is useless regardless of notices: the follow-up
|
|
@@ -441,8 +445,15 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
|
|
|
441
445
|
partial.sort((a, b) => b.m - a.m);
|
|
442
446
|
const sortedPaths = partial.map(entry => entry.p);
|
|
443
447
|
const seconds = timeoutMs % 1000 === 0 ? `${timeoutMs / 1000}` : (timeoutMs / 1000).toFixed(1);
|
|
444
|
-
|
|
445
|
-
|
|
448
|
+
// Walk cost tracks directory-tree size, not pattern specificity: a
|
|
449
|
+
// mtime-ranked scan cannot early-exit, so a "narrow" pattern over a
|
|
450
|
+
// huge tree still times out. Say so instead of implying the pattern
|
|
451
|
+
// was too broad.
|
|
452
|
+
const notice =
|
|
453
|
+
sortedPaths.length > 0
|
|
454
|
+
? `glob timed out after ${seconds}s; returning ${sortedPaths.length} partial matches — results are incomplete, scope to a deeper directory instead of retrying blindly`
|
|
455
|
+
: `Glob timed out after ${seconds}s before finding any matches — the scan is incomplete, NOT proof of absence. The walk is bounded by directory size, not pattern width; scope the search to a deeper directory (e.g. \`sub/dir/*.ext\` instead of \`*.ext\` at a huge root).`;
|
|
456
|
+
return buildResult(sortedPaths, { notice, forceTruncated: true, timedOut: true });
|
|
446
457
|
}
|
|
447
458
|
|
|
448
459
|
// Merge per-target results: native glob already ranks each target's own
|
|
@@ -568,17 +579,20 @@ export const globToolRenderer = {
|
|
|
568
579
|
missingPaths.length > 0 ? uiTheme.fg("warning", `skipped missing: ${missingPaths.join(", ")}`) : undefined;
|
|
569
580
|
|
|
570
581
|
if (fileCount === 0) {
|
|
582
|
+
// `truncated` on an empty result means the scan timed out mid-walk —
|
|
583
|
+
// render "incomplete", not a definitive "No files found".
|
|
584
|
+
const emptyLabel = truncated ? "No matches before timeout (scan incomplete)" : "No files found";
|
|
571
585
|
const header = renderStatusLine(
|
|
572
586
|
{
|
|
573
587
|
icon: "warning",
|
|
574
588
|
title: "Glob",
|
|
575
589
|
titleColor: "toolTitle",
|
|
576
590
|
description: formatGlobRenderPaths(args?.paths),
|
|
577
|
-
meta: ["0 files"],
|
|
591
|
+
meta: truncated ? ["0 files", uiTheme.fg("warning", "timed out")] : ["0 files"],
|
|
578
592
|
},
|
|
579
593
|
uiTheme,
|
|
580
594
|
);
|
|
581
|
-
const lines = [header, formatEmptyMessage(
|
|
595
|
+
const lines = [header, formatEmptyMessage(emptyLabel, uiTheme)];
|
|
582
596
|
if (missingNote) lines.push(missingNote);
|
|
583
597
|
return new Text(lines.join("\n"), 1, 0);
|
|
584
598
|
}
|
package/src/tools/grep.ts
CHANGED
package/src/tools/image-gen.ts
CHANGED
|
@@ -822,11 +822,10 @@ function buildOpenAIImageHeaders(model: Model, apiKey: string, sessionId: string
|
|
|
822
822
|
|
|
823
823
|
if (model.api === "openai-codex-responses" || model.provider === "openai-codex") {
|
|
824
824
|
const accountId = getCodexAccountId(apiKey);
|
|
825
|
-
if (!accountId) {
|
|
826
|
-
throw new Error("Failed to extract accountId from OpenAI Codex token");
|
|
827
|
-
}
|
|
828
825
|
headers.delete("x-api-key");
|
|
829
|
-
|
|
826
|
+
if (accountId) {
|
|
827
|
+
headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
|
|
828
|
+
}
|
|
830
829
|
headers.set(OPENAI_HEADERS.BETA, OPENAI_HEADER_VALUES.BETA_RESPONSES);
|
|
831
830
|
headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
|
|
832
831
|
headers.set("User-Agent", `pi/${packageJson.version} (${os.platform()} ${os.release()}; ${os.arch()})`);
|
package/src/tools/irc.ts
CHANGED
|
@@ -171,7 +171,7 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
171
171
|
case "send":
|
|
172
172
|
return this.#executeSend(registry, senderId, params, signal);
|
|
173
173
|
case "wait":
|
|
174
|
-
return this.#executeWait(senderId, params, signal);
|
|
174
|
+
return this.#executeWait(registry, senderId, params, signal);
|
|
175
175
|
case "inbox":
|
|
176
176
|
return this.#executeInbox(registry, senderId, params);
|
|
177
177
|
default:
|
|
@@ -367,23 +367,37 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
async #executeWait(
|
|
370
|
+
async #executeWait(
|
|
371
|
+
registry: AgentRegistry,
|
|
372
|
+
senderId: string,
|
|
373
|
+
params: IrcParams,
|
|
374
|
+
signal?: AbortSignal,
|
|
375
|
+
): Promise<AgentToolResult<IrcDetails>> {
|
|
371
376
|
const from = params.from?.trim() || undefined;
|
|
372
377
|
const timeoutMs = this.#resolveTimeoutMs(params);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
378
|
+
try {
|
|
379
|
+
const waited = await IrcBus.global().wait(senderId, { from }, timeoutMs, signal, {
|
|
380
|
+
liveness: { registry, senderId },
|
|
381
|
+
});
|
|
382
|
+
if (!waited) {
|
|
383
|
+
const filterNote = from ? ` from ${from}` : "";
|
|
384
|
+
return {
|
|
385
|
+
content: [{ type: "text", text: `No message${filterNote} within ${formatDuration(timeoutMs)}.` }],
|
|
386
|
+
details: { op: "wait", from: senderId, waited: null },
|
|
387
|
+
// A clean wait timeout carries no information once consumed.
|
|
388
|
+
useless: true,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
376
391
|
return {
|
|
377
|
-
content: [{ type: "text", text:
|
|
378
|
-
details: { op: "wait", from: senderId, waited
|
|
379
|
-
// A clean wait timeout carries no information once consumed.
|
|
380
|
-
useless: true,
|
|
392
|
+
content: [{ type: "text", text: formatIncoming(waited) }],
|
|
393
|
+
details: { op: "wait", from: senderId, waited },
|
|
381
394
|
};
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (signal?.aborted) {
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
return errorResult(error instanceof Error ? error.message : String(error), { op: "wait", from: senderId });
|
|
382
400
|
}
|
|
383
|
-
return {
|
|
384
|
-
content: [{ type: "text", text: formatIncoming(waited) }],
|
|
385
|
-
details: { op: "wait", from: senderId, waited },
|
|
386
|
-
};
|
|
387
401
|
}
|
|
388
402
|
|
|
389
403
|
#executeInbox(registry: AgentRegistry, senderId: string, params: IrcParams): AgentToolResult<IrcDetails> {
|
package/src/tools/read.ts
CHANGED
|
@@ -1251,13 +1251,17 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1251
1251
|
const ignoreResultLimits = options.ignoreResultLimits ?? false;
|
|
1252
1252
|
const requestedEnd = limit !== undefined ? Math.min(requestedStart + limit, allLines.length) : allLines.length;
|
|
1253
1253
|
// Expand only on sides the user actually constrained: leading context
|
|
1254
|
-
// when offset>1, trailing context when a finite limit was set.
|
|
1254
|
+
// when offset>1, trailing context when a finite limit was set. Raw mode
|
|
1255
|
+
// never expands — without line numbers the padding is indistinguishable
|
|
1256
|
+
// from requested content, so `raw:31-31` must return line 31 and nothing
|
|
1257
|
+
// else (verbatim-extraction contract).
|
|
1258
|
+
const rawDisplay = options.raw === true;
|
|
1255
1259
|
const expanded = expandRangeWithContext(
|
|
1256
1260
|
requestedStart,
|
|
1257
1261
|
requestedEnd,
|
|
1258
1262
|
allLines.length,
|
|
1259
|
-
offset !== undefined && offset > 1,
|
|
1260
|
-
limit !== undefined,
|
|
1263
|
+
!rawDisplay && offset !== undefined && offset > 1,
|
|
1264
|
+
!rawDisplay && limit !== undefined,
|
|
1261
1265
|
);
|
|
1262
1266
|
const startLine = expanded.startLine;
|
|
1263
1267
|
const endLineExpanded = expanded.endLine;
|
|
@@ -2415,10 +2419,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2415
2419
|
}
|
|
2416
2420
|
|
|
2417
2421
|
// User-requested 0-indexed range start. Lines BEFORE this become
|
|
2418
|
-
// leading context (added below if offset is explicit).
|
|
2422
|
+
// leading context (added below if offset is explicit). Raw mode
|
|
2423
|
+
// never adds context: without line numbers the padding is
|
|
2424
|
+
// indistinguishable from requested content, so `raw:31-31` must
|
|
2425
|
+
// return line 31 and nothing else.
|
|
2426
|
+
const rawSelector = isRawSelector(parsed);
|
|
2419
2427
|
const requestedStart = offset ? Math.max(0, offset - 1) : 0;
|
|
2420
|
-
const expandStart = offset !== undefined && offset > 1;
|
|
2421
|
-
const expandEnd = limit !== undefined;
|
|
2428
|
+
const expandStart = !rawSelector && offset !== undefined && offset > 1;
|
|
2429
|
+
const expandEnd = !rawSelector && limit !== undefined;
|
|
2422
2430
|
const leadingContext = expandStart ? Math.min(requestedStart, RANGE_LEADING_CONTEXT_LINES) : 0;
|
|
2423
2431
|
const trailingContext = expandEnd ? RANGE_TRAILING_CONTEXT_LINES : 0;
|
|
2424
2432
|
const startLine = requestedStart - leadingContext;
|
|
@@ -2469,7 +2477,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2469
2477
|
// verbatim bytes for paste-back-into-tool workflows. Total byte/line
|
|
2470
2478
|
// counts in `truncation` keep reflecting the source, not the trimmed
|
|
2471
2479
|
// view — column truncation surfaces separately via `.limits()`.
|
|
2472
|
-
const rawSelector = isRawSelector(parsed);
|
|
2473
2480
|
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
2474
2481
|
// Column truncation is display-only. `collectedLines` MUST stay
|
|
2475
2482
|
// byte-for-byte with the on-disk content so the snapshot recorded
|
package/src/tools/write.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
|
13
13
|
import { InternalUrlRouter } from "../internal-urls";
|
|
14
14
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
15
15
|
import { createLspWritethrough, type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "../lsp";
|
|
16
|
+
import { DeferredDiagnostics } from "../lsp/deferred-diagnostics";
|
|
16
17
|
import { getDiagnosticsLedger } from "../lsp/diagnostics-ledger";
|
|
17
18
|
import { getLanguageFromPath, highlightCode, type Theme } from "../modes/theme/theme";
|
|
18
19
|
import writeDescription from "../prompts/tools/write.md" with { type: "text" };
|
|
@@ -317,12 +318,15 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
317
318
|
}
|
|
318
319
|
|
|
319
320
|
readonly #writethrough: WritethroughCallback;
|
|
321
|
+
readonly #deferredDiagnostics: DeferredDiagnostics | undefined;
|
|
320
322
|
|
|
321
323
|
constructor(private readonly session: ToolSession) {
|
|
322
324
|
const enableLsp = session.enableLsp ?? true;
|
|
323
325
|
const enableFormat = enableLsp && session.settings.get("lsp.formatOnWrite");
|
|
324
326
|
const enableDiagnostics = enableLsp && session.settings.get("lsp.diagnosticsOnWrite");
|
|
325
327
|
const dedup = enableDiagnostics && session.settings.get("lsp.diagnosticsDeduplicate");
|
|
328
|
+
this.#deferredDiagnostics =
|
|
329
|
+
enableDiagnostics && session.queueDeferredDiagnostics ? new DeferredDiagnostics(session, dedup) : undefined;
|
|
326
330
|
this.#writethrough = withPathWriteLock(
|
|
327
331
|
enableLsp
|
|
328
332
|
? createLspWritethrough(session.cwd, {
|
|
@@ -826,9 +830,13 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
826
830
|
}
|
|
827
831
|
return { content: [{ type: "text", text: resultText }], details: {} };
|
|
828
832
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
833
|
+
if (scheme !== "local") {
|
|
834
|
+
throw new ToolError(
|
|
835
|
+
`${scheme}:// URLs are read-only for write; use the protocol-specific tool for mutations.`,
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
// local:// is backed by the session-local artifact sandbox and is
|
|
839
|
+
// resolved by resolvePlanPath below so write/read share the same root.
|
|
832
840
|
}
|
|
833
841
|
|
|
834
842
|
const conflictUri = parseConflictUri(path);
|
|
@@ -927,9 +935,18 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
927
935
|
};
|
|
928
936
|
}
|
|
929
937
|
|
|
930
|
-
const diagnostics = await this.#writethrough(
|
|
938
|
+
const diagnostics = await this.#writethrough(
|
|
939
|
+
absolutePath,
|
|
940
|
+
cleanContent,
|
|
941
|
+
signal,
|
|
942
|
+
undefined,
|
|
943
|
+
batchRequest,
|
|
944
|
+
dst => this.#deferredDiagnostics?.begin(dst),
|
|
945
|
+
);
|
|
931
946
|
invalidateFsScanAfterWrite(absolutePath);
|
|
932
|
-
this
|
|
947
|
+
if (!this.#deferredDiagnostics || batchRequest?.flush === false) {
|
|
948
|
+
this.session.bumpFileMutationVersion?.(absolutePath);
|
|
949
|
+
}
|
|
933
950
|
const madeExecutable = await maybeMarkExecutableForShebang(absolutePath, cleanContent);
|
|
934
951
|
|
|
935
952
|
const header = maybeWriteSnapshotHeader(this.session, absolutePath, cleanContent);
|
package/src/tools/yield.ts
CHANGED
|
@@ -198,6 +198,14 @@ function wrapYieldParameters(dataSchema: Record<string, unknown>): Record<string
|
|
|
198
198
|
*/
|
|
199
199
|
const MAX_SCHEMA_RETRIES = 3;
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Max consecutive untyped empty-result submissions before the yield tool fails
|
|
203
|
+
* the child explicitly. Some weak tool callers can acknowledge the required
|
|
204
|
+
* wrapper in prose while repeatedly sending `{ result: {} }`; without a hard
|
|
205
|
+
* stop the parent waits forever.
|
|
206
|
+
*/
|
|
207
|
+
const MAX_EMPTY_RESULT_RETRIES = 3;
|
|
208
|
+
|
|
201
209
|
export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
202
210
|
readonly name = "yield";
|
|
203
211
|
readonly approval = "read" as const;
|
|
@@ -217,6 +225,7 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
217
225
|
#knownSectionLabels: readonly string[] = [];
|
|
218
226
|
#isKnownSection?: (label: string) => boolean;
|
|
219
227
|
#schemaValidationFailures = 0;
|
|
228
|
+
#emptyResultFailures = 0;
|
|
220
229
|
|
|
221
230
|
constructor(session: ToolSession) {
|
|
222
231
|
let validate: ((value: unknown) => JsonSchemaValidationResult) | undefined;
|
|
@@ -322,8 +331,26 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
322
331
|
throw new Error("result cannot contain both data and error");
|
|
323
332
|
}
|
|
324
333
|
if (errorMessage === undefined && data === undefined && yieldType === undefined) {
|
|
334
|
+
this.#emptyResultFailures++;
|
|
335
|
+
if (this.#emptyResultFailures > MAX_EMPTY_RESULT_RETRIES) {
|
|
336
|
+
const attemptCount = this.#emptyResultFailures;
|
|
337
|
+
this.#emptyResultFailures = 0;
|
|
338
|
+
const error =
|
|
339
|
+
`yield result stayed empty after ${attemptCount} consecutive attempt(s); aborting child instead of retrying forever. ` +
|
|
340
|
+
'Submit success as `{ "result": { "data": <your output> } }` or failure as `{ "result": { "error": "message" } }`.';
|
|
341
|
+
return {
|
|
342
|
+
content: [{ type: "text", text: `Task aborted: ${error}` }],
|
|
343
|
+
details: {
|
|
344
|
+
data: undefined,
|
|
345
|
+
status: "aborted",
|
|
346
|
+
error,
|
|
347
|
+
type: yieldType,
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const remaining = MAX_EMPTY_RESULT_RETRIES - this.#emptyResultFailures;
|
|
325
352
|
throw new Error(
|
|
326
|
-
|
|
353
|
+
`result must contain either \`data\` or \`error\`. Use \`{result: {data: <your output>}}\` for success or \`{result: {error: "message"}}\` for failure. Empty untyped result retries remaining before abort: ${remaining}.`,
|
|
327
354
|
);
|
|
328
355
|
}
|
|
329
356
|
|
|
@@ -370,6 +397,7 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
370
397
|
}
|
|
371
398
|
}
|
|
372
399
|
|
|
400
|
+
this.#emptyResultFailures = 0;
|
|
373
401
|
const responseText =
|
|
374
402
|
status === "aborted"
|
|
375
403
|
? `Task aborted: ${errorMessage}`
|
package/src/utils/changelog.ts
CHANGED
|
@@ -7,6 +7,27 @@ export interface ChangelogEntry {
|
|
|
7
7
|
content: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/** Number of changelog releases shown by automatic and default recent views. */
|
|
11
|
+
export const RECENT_CHANGELOG_ENTRY_LIMIT = 3;
|
|
12
|
+
/** Maximum Markdown source bytes allowed in automatic startup release notes. */
|
|
13
|
+
export const STARTUP_CHANGELOG_MAX_BYTES = 64 * 1024;
|
|
14
|
+
/** Hint appended when automatic startup release notes are truncated. */
|
|
15
|
+
export const STARTUP_CHANGELOG_FULL_HINT = "Use `/changelog full` to view the complete changelog.";
|
|
16
|
+
|
|
17
|
+
/** Markdown generated from selected changelog entries and whether it hit a size cap. */
|
|
18
|
+
export interface RenderedChangelog {
|
|
19
|
+
markdown: string;
|
|
20
|
+
truncated: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Automatic startup changelog decision, including whether the marker should advance. */
|
|
24
|
+
export interface StartupChangelogSelection {
|
|
25
|
+
markdown: string | undefined;
|
|
26
|
+
persistCurrentVersion: boolean;
|
|
27
|
+
truncated: boolean;
|
|
28
|
+
selectedEntries: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
10
31
|
/**
|
|
11
32
|
* Parse changelog entries from the file at `changelogPath`. Scans for `## [x.y.z]`
|
|
12
33
|
* headings and collects each block until the next heading or EOF.
|
|
@@ -87,19 +108,95 @@ export function compareVersions(v1: ChangelogEntry, v2: ChangelogEntry): number
|
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
/**
|
|
90
|
-
*
|
|
111
|
+
* Parse an omp changelog marker version into comparable parts.
|
|
91
112
|
*/
|
|
92
|
-
export function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
export function parseChangelogVersion(version: string | undefined): ChangelogEntry | undefined {
|
|
114
|
+
const match = version?.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
115
|
+
if (!match) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
major: Number.parseInt(match[1], 10),
|
|
121
|
+
minor: Number.parseInt(match[2], 10),
|
|
122
|
+
patch: Number.parseInt(match[3], 10),
|
|
99
123
|
content: "",
|
|
100
124
|
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get entries newer than lastVersion.
|
|
129
|
+
*/
|
|
130
|
+
export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): ChangelogEntry[] {
|
|
131
|
+
const parsedLastVersion = parseChangelogVersion(lastVersion);
|
|
132
|
+
if (!parsedLastVersion) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return entries.filter(entry => compareVersions(entry, parsedLastVersion) > 0);
|
|
137
|
+
}
|
|
101
138
|
|
|
102
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Render changelog entries oldest-first by default and optionally cap the Markdown source size.
|
|
141
|
+
*/
|
|
142
|
+
export function renderChangelogEntries(
|
|
143
|
+
entries: ChangelogEntry[],
|
|
144
|
+
options: { maxBytes?: number; truncationHint?: string; oldestFirst?: boolean } = {},
|
|
145
|
+
): RenderedChangelog {
|
|
146
|
+
const orderedEntries = options.oldestFirst === false ? entries : [...entries].reverse();
|
|
147
|
+
const markdown = orderedEntries.map(entry => entry.content).join("\n\n");
|
|
148
|
+
if (options.maxBytes === undefined || Buffer.byteLength(markdown) <= options.maxBytes) {
|
|
149
|
+
return { markdown, truncated: false };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const suffix = `\n\n…\n\n${options.truncationHint ?? STARTUP_CHANGELOG_FULL_HINT}`;
|
|
153
|
+
let low = 0;
|
|
154
|
+
let high = markdown.length;
|
|
155
|
+
while (low < high) {
|
|
156
|
+
const middle = Math.floor((low + high + 1) / 2);
|
|
157
|
+
if (Buffer.byteLength(markdown.slice(0, middle) + suffix) <= options.maxBytes) {
|
|
158
|
+
low = middle;
|
|
159
|
+
} else {
|
|
160
|
+
high = middle - 1;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { markdown: markdown.slice(0, low) + suffix, truncated: true };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Select bounded release notes for interactive startup.
|
|
169
|
+
*/
|
|
170
|
+
export function selectStartupChangelog(
|
|
171
|
+
entries: ChangelogEntry[],
|
|
172
|
+
lastVersion: string | undefined,
|
|
173
|
+
currentVersion: string,
|
|
174
|
+
): StartupChangelogSelection {
|
|
175
|
+
const parsedLastVersion = parseChangelogVersion(lastVersion);
|
|
176
|
+
if (!parsedLastVersion) {
|
|
177
|
+
return { markdown: undefined, persistCurrentVersion: true, truncated: false, selectedEntries: 0 };
|
|
178
|
+
}
|
|
179
|
+
const markerVersion = lastVersion ?? "";
|
|
180
|
+
if (markerVersion === currentVersion) {
|
|
181
|
+
return { markdown: undefined, persistCurrentVersion: false, truncated: false, selectedEntries: 0 };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const newEntries = getNewEntries(entries, markerVersion).slice(0, RECENT_CHANGELOG_ENTRY_LIMIT);
|
|
185
|
+
if (newEntries.length === 0) {
|
|
186
|
+
return { markdown: undefined, persistCurrentVersion: false, truncated: false, selectedEntries: 0 };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const rendered = renderChangelogEntries(newEntries, {
|
|
190
|
+
maxBytes: STARTUP_CHANGELOG_MAX_BYTES,
|
|
191
|
+
truncationHint: STARTUP_CHANGELOG_FULL_HINT,
|
|
192
|
+
oldestFirst: false,
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
markdown: rendered.markdown,
|
|
196
|
+
persistCurrentVersion: true,
|
|
197
|
+
truncated: rendered.truncated,
|
|
198
|
+
selectedEntries: newEntries.length,
|
|
199
|
+
};
|
|
103
200
|
}
|
|
104
201
|
|
|
105
202
|
// Re-export getChangelogPath from paths.ts for convenience
|
package/src/utils/git.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
|
|
6
6
|
import { type Api, type AssistantMessage, completeSimple, type Model, type Tool } from "jeopi-ai";
|
|
7
|
+
import { StreamMarkupHealing } from "jeopi-ai/utils/stream-markup-healing";
|
|
7
8
|
import { isTerminalHeadless, logger, prompt } from "jeopi-utils";
|
|
8
9
|
import type { ModelRegistry } from "../config/model-registry";
|
|
9
10
|
|
|
@@ -45,7 +46,12 @@ const setTitleTool: Tool = {
|
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
/** Matches the title a tool-choice-less model wraps in `<title>...</title>`. */
|
|
48
|
-
const
|
|
49
|
+
const TITLE_MARKER_GLOBAL_RE = /<title>([\s\S]*?)<\/title>/gi;
|
|
50
|
+
const TITLE_VISIBILITY_SENTINEL = "\uE000jeopi-title-visible\uE000";
|
|
51
|
+
const THINKING_TAG_ENVELOPE_RE = /<(think|thinking|reasoning)>\s*[\s\S]*?<\/\1>/gi;
|
|
52
|
+
const THINKING_FENCE_ENVELOPE_RE = /```(?:thinking|reasoning)\b[\s\S]*?```/gi;
|
|
53
|
+
const LEADING_THINKING_TAG_RE = /^\s*<(think|thinking|reasoning)>\s*[\s\S]*?<\/\1>\s*/i;
|
|
54
|
+
const LEADING_THINKING_FENCE_RE = /^\s*```(?:thinking|reasoning)\b[\s\S]*?```\s*/i;
|
|
49
55
|
|
|
50
56
|
/**
|
|
51
57
|
* Whether the model honors a forced `tool_choice` so the `set_title` tool can be
|
|
@@ -289,12 +295,67 @@ function extractGeneratedTitle(contentBlocks: AssistantMessage["content"]): stri
|
|
|
289
295
|
}
|
|
290
296
|
}
|
|
291
297
|
// Tool-choice-less models are asked to wrap the title in <title>...</title>,
|
|
292
|
-
// but stay lenient: prefer the
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
const
|
|
296
|
-
if (
|
|
297
|
-
return textTitle
|
|
298
|
+
// but stay lenient: prefer the first closed title marker in visible text,
|
|
299
|
+
// then fall back to a plain sentence after stripping only known leading
|
|
300
|
+
// leaked thinking envelopes plus any stray/unclosed title tag fragment.
|
|
301
|
+
const markedTitle = extractVisibleMarkedTitle(textTitle);
|
|
302
|
+
if (markedTitle !== undefined) return markedTitle;
|
|
303
|
+
return stripLeadingLeakedThinkingMarkup(textTitle)
|
|
304
|
+
.replace(/<\/?title>/gi, "")
|
|
305
|
+
.trim();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function extractVisibleMarkedTitle(text: string): string | undefined {
|
|
309
|
+
TITLE_MARKER_GLOBAL_RE.lastIndex = 0;
|
|
310
|
+
let marker: RegExpExecArray | null = TITLE_MARKER_GLOBAL_RE.exec(text);
|
|
311
|
+
while (marker !== null) {
|
|
312
|
+
const title = marker[1];
|
|
313
|
+
if (title !== undefined && isVisibleTitleMarker(text, marker.index)) return title.trim();
|
|
314
|
+
marker = TITLE_MARKER_GLOBAL_RE.exec(text);
|
|
315
|
+
}
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function isVisibleTitleMarker(text: string, markerIndex: number): boolean {
|
|
320
|
+
if (isInsideKnownThinkingEnvelope(text, markerIndex)) return false;
|
|
321
|
+
return stripLeakedThinkingMarkup(`${text.slice(0, markerIndex)}${TITLE_VISIBILITY_SENTINEL}`).endsWith(
|
|
322
|
+
TITLE_VISIBILITY_SENTINEL,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isInsideKnownThinkingEnvelope(text: string, index: number): boolean {
|
|
327
|
+
return (
|
|
328
|
+
isInsideEnvelopeMatchedBy(THINKING_TAG_ENVELOPE_RE, text, index) ||
|
|
329
|
+
isInsideEnvelopeMatchedBy(THINKING_FENCE_ENVELOPE_RE, text, index)
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function isInsideEnvelopeMatchedBy(pattern: RegExp, text: string, index: number): boolean {
|
|
334
|
+
pattern.lastIndex = 0;
|
|
335
|
+
let marker = pattern.exec(text);
|
|
336
|
+
while (marker !== null) {
|
|
337
|
+
const start = marker.index;
|
|
338
|
+
const end = start + marker[0].length;
|
|
339
|
+
if (index > start && index < end) return true;
|
|
340
|
+
if (start > index) return false;
|
|
341
|
+
marker = pattern.exec(text);
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function stripLeadingLeakedThinkingMarkup(text: string): string {
|
|
347
|
+
let current = text;
|
|
348
|
+
while (true) {
|
|
349
|
+
const withoutTag = current.replace(LEADING_THINKING_TAG_RE, "");
|
|
350
|
+
const withoutFence = withoutTag.replace(LEADING_THINKING_FENCE_RE, "");
|
|
351
|
+
if (withoutFence === current) return current;
|
|
352
|
+
current = withoutFence;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function stripLeakedThinkingMarkup(text: string): string {
|
|
357
|
+
const healer = new StreamMarkupHealing({ pattern: "thinking" });
|
|
358
|
+
return healer.feed(text) + healer.flushPending();
|
|
298
359
|
}
|
|
299
360
|
|
|
300
361
|
/**
|
package/src/web/search/index.ts
CHANGED
|
@@ -224,12 +224,20 @@ export async function runSearchQuery(
|
|
|
224
224
|
params: SearchQueryParams,
|
|
225
225
|
options: { authStorage?: AuthStorage; sessionId?: string; signal?: AbortSignal } = {},
|
|
226
226
|
): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
227
|
+
const createdAuthStorage = options.authStorage ? undefined : await discoverAuthStorage();
|
|
228
|
+
const authStorage = options.authStorage ?? createdAuthStorage;
|
|
229
|
+
if (!authStorage) {
|
|
230
|
+
throw new Error("Failed to initialize authentication storage");
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
return await executeSearch("cli-web-search", params, {
|
|
234
|
+
authStorage,
|
|
235
|
+
sessionId: options.sessionId,
|
|
236
|
+
signal: options.signal,
|
|
237
|
+
});
|
|
238
|
+
} finally {
|
|
239
|
+
createdAuthStorage?.close();
|
|
240
|
+
}
|
|
233
241
|
}
|
|
234
242
|
|
|
235
243
|
/**
|