@oh-my-pi/pi-coding-agent 16.3.2 → 16.3.4
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 +44 -0
- package/dist/cli.js +3553 -3540
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/cli/update-cli.test.d.ts +1 -0
- package/dist/types/commands/update.d.ts +5 -0
- package/dist/types/config/config-file.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/dap/client.d.ts +9 -1
- package/dist/types/edit/file-snapshot-store.d.ts +9 -1
- package/dist/types/edit/hashline/execute.d.ts +1 -1
- package/dist/types/edit/hashline/params.d.ts +3 -6
- package/dist/types/edit/renderer.d.ts +1 -0
- package/dist/types/extensibility/plugins/parser.d.ts +2 -1
- package/dist/types/hindsight/client.d.ts +15 -11
- package/dist/types/hindsight/client.test.d.ts +1 -0
- package/dist/types/mcp/smithery-registry.d.ts +1 -0
- package/dist/types/mcp/smithery-registry.test.d.ts +1 -0
- package/dist/types/modes/components/advisor-message.d.ts +4 -2
- package/dist/types/modes/components/tool-execution.d.ts +9 -6
- package/dist/types/modes/components/transcript-container.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/session/indexed-session-storage.d.ts +2 -2
- package/dist/types/session/session-storage.d.ts +31 -3
- package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +1 -0
- package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +1 -0
- package/dist/types/ssh/connection-manager.d.ts +19 -0
- package/dist/types/ssh/sshfs-mount.d.ts +10 -1
- package/dist/types/subprocess/worker-client.d.ts +20 -6
- package/dist/types/tools/bash.d.ts +27 -0
- package/dist/types/tools/renderers.d.ts +13 -0
- package/dist/types/tools/ssh.d.ts +2 -0
- package/dist/types/utils/fetch-timeout.d.ts +4 -0
- package/dist/types/web/search/providers/base.d.ts +1 -0
- package/dist/types/web/search/providers/gemini.d.ts +1 -0
- package/package.json +12 -12
- package/scripts/generate-legacy-pi-bundled-registry.ts +8 -2
- package/src/cli/models-cli.ts +19 -0
- package/src/cli/update-cli.test.ts +28 -0
- package/src/cli/update-cli.ts +35 -8
- package/src/cli/usage-cli.ts +34 -5
- package/src/commands/update.ts +8 -2
- package/src/config/config-file.ts +6 -6
- package/src/config/settings-schema.ts +10 -0
- package/src/dap/client.ts +134 -36
- package/src/edit/file-snapshot-store.ts +12 -1
- package/src/edit/hashline/diff.ts +4 -13
- package/src/edit/hashline/execute.ts +1 -1
- package/src/edit/hashline/params.ts +5 -12
- package/src/edit/renderer.ts +4 -2
- package/src/edit/streaming.ts +15 -5
- package/src/export/html/tool-views.generated.js +2 -2
- package/src/extensibility/plugins/installer.ts +12 -3
- package/src/extensibility/plugins/manager.ts +32 -8
- package/src/extensibility/plugins/parser.ts +7 -5
- package/src/extensibility/tool-event-input.ts +1 -1
- package/src/hindsight/client.test.ts +33 -0
- package/src/hindsight/client.ts +42 -22
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/main.ts +7 -1
- package/src/mcp/oauth-flow.ts +93 -4
- package/src/mcp/smithery-auth.ts +3 -0
- package/src/mcp/smithery-connect.ts +9 -0
- package/src/mcp/smithery-registry.test.ts +51 -0
- package/src/mcp/smithery-registry.ts +27 -4
- package/src/modes/components/advisor-message.ts +13 -10
- package/src/modes/components/status-line/component.ts +11 -4
- package/src/modes/components/tool-execution.ts +74 -8
- package/src/modes/components/transcript-container.ts +26 -0
- package/src/modes/controllers/event-controller.ts +7 -3
- package/src/modes/controllers/tool-args-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +1 -0
- package/src/modes/rpc/rpc-client.ts +29 -16
- package/src/modes/theme/shimmer.ts +49 -15
- package/src/modes/theme/theme.ts +7 -0
- package/src/session/agent-session.ts +166 -30
- package/src/session/indexed-session-storage.ts +40 -3
- package/src/session/session-manager.ts +83 -14
- package/src/session/session-storage.ts +112 -26
- package/src/slash-commands/helpers/usage-report.ts +6 -1
- package/src/ssh/__tests__/connection-manager-timeout.test.ts +61 -0
- package/src/ssh/__tests__/sshfs-mount.test.ts +13 -0
- package/src/ssh/connection-manager.ts +44 -11
- package/src/ssh/sshfs-mount.ts +27 -4
- package/src/subprocess/worker-client.ts +161 -10
- package/src/tools/bash.ts +30 -1
- package/src/tools/grep.ts +21 -1
- package/src/tools/read.ts +14 -4
- package/src/tools/renderers.ts +13 -0
- package/src/tools/ssh.ts +8 -0
- package/src/utils/clipboard.ts +49 -12
- package/src/utils/fetch-timeout.ts +10 -0
- package/src/web/search/index.ts +8 -0
- package/src/web/search/providers/base.ts +1 -0
- package/src/web/search/providers/gemini.ts +23 -6
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "16.3.
|
|
4
|
+
"version": "16.3.4",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -56,17 +56,17 @@
|
|
|
56
56
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
57
57
|
"@babel/parser": "^7.29.7",
|
|
58
58
|
"@mozilla/readability": "^0.6.0",
|
|
59
|
-
"@oh-my-pi/hashline": "16.3.
|
|
60
|
-
"@oh-my-pi/omp-stats": "16.3.
|
|
61
|
-
"@oh-my-pi/pi-agent-core": "16.3.
|
|
62
|
-
"@oh-my-pi/pi-ai": "16.3.
|
|
63
|
-
"@oh-my-pi/pi-catalog": "16.3.
|
|
64
|
-
"@oh-my-pi/pi-mnemopi": "16.3.
|
|
65
|
-
"@oh-my-pi/pi-natives": "16.3.
|
|
66
|
-
"@oh-my-pi/pi-tui": "16.3.
|
|
67
|
-
"@oh-my-pi/pi-utils": "16.3.
|
|
68
|
-
"@oh-my-pi/pi-wire": "16.3.
|
|
69
|
-
"@oh-my-pi/snapcompact": "16.3.
|
|
59
|
+
"@oh-my-pi/hashline": "16.3.4",
|
|
60
|
+
"@oh-my-pi/omp-stats": "16.3.4",
|
|
61
|
+
"@oh-my-pi/pi-agent-core": "16.3.4",
|
|
62
|
+
"@oh-my-pi/pi-ai": "16.3.4",
|
|
63
|
+
"@oh-my-pi/pi-catalog": "16.3.4",
|
|
64
|
+
"@oh-my-pi/pi-mnemopi": "16.3.4",
|
|
65
|
+
"@oh-my-pi/pi-natives": "16.3.4",
|
|
66
|
+
"@oh-my-pi/pi-tui": "16.3.4",
|
|
67
|
+
"@oh-my-pi/pi-utils": "16.3.4",
|
|
68
|
+
"@oh-my-pi/pi-wire": "16.3.4",
|
|
69
|
+
"@oh-my-pi/snapcompact": "16.3.4",
|
|
70
70
|
"@opentelemetry/api": "^1.9.1",
|
|
71
71
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
72
72
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
|
@@ -332,9 +332,15 @@ async function formatInPlace(targets: readonly string[]): Promise<void> {
|
|
|
332
332
|
stdout: "pipe",
|
|
333
333
|
stderr: "pipe",
|
|
334
334
|
});
|
|
335
|
-
|
|
335
|
+
// Drain both pipes concurrently with proc.exited to avoid a pipe-buffer
|
|
336
|
+
// deadlock — biome check can emit thousands of lines when it rewrites the
|
|
337
|
+
// generated registry, easily exceeding the ~64 KiB OS pipe buffer.
|
|
338
|
+
const [exit, , stderr] = await Promise.all([
|
|
339
|
+
proc.exited,
|
|
340
|
+
new Response(proc.stdout).text(),
|
|
341
|
+
new Response(proc.stderr).text(),
|
|
342
|
+
]);
|
|
336
343
|
if (exit !== 0) {
|
|
337
|
-
const stderr = await new Response(proc.stderr).text();
|
|
338
344
|
throw new Error(`biome check --write failed (exit ${exit}): ${stderr}`);
|
|
339
345
|
}
|
|
340
346
|
}
|
package/src/cli/models-cli.ts
CHANGED
|
@@ -84,6 +84,14 @@ function writeLine(line = ""): void {
|
|
|
84
84
|
process.stdout.write(`${line}\n`);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function writeModelsConfigError(error: Error): void {
|
|
88
|
+
writeLine(chalk.yellow("Warning: models.yml validation failed — custom providers disabled"));
|
|
89
|
+
for (const line of error.message.split("\n")) {
|
|
90
|
+
writeLine(` ${line}`);
|
|
91
|
+
}
|
|
92
|
+
writeLine();
|
|
93
|
+
}
|
|
94
|
+
|
|
87
95
|
function formatLimit(n: number | null): string {
|
|
88
96
|
return n === null ? "-" : formatNumber(n);
|
|
89
97
|
}
|
|
@@ -187,12 +195,23 @@ function renderProviderModels(
|
|
|
187
195
|
}
|
|
188
196
|
}
|
|
189
197
|
|
|
198
|
+
const configError = modelRegistry.getError();
|
|
199
|
+
|
|
190
200
|
if (json) {
|
|
201
|
+
if (configError) {
|
|
202
|
+
process.stderr.write(
|
|
203
|
+
`Warning: models.yml validation failed — custom providers disabled\n${configError.message}\n`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
191
206
|
const output: ModelsJson = { models: filtered.slice().sort(byProviderThenId).map(toModelJson) };
|
|
192
207
|
writeLine(JSON.stringify(output));
|
|
193
208
|
return;
|
|
194
209
|
}
|
|
195
210
|
|
|
211
|
+
if (configError) {
|
|
212
|
+
writeModelsConfigError(configError);
|
|
213
|
+
}
|
|
214
|
+
|
|
196
215
|
if (available.length === 0) {
|
|
197
216
|
writeLine("No models available. Set API keys in environment variables.");
|
|
198
217
|
return;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
+
import { runUpdateCommand } from "./update-cli";
|
|
3
|
+
|
|
4
|
+
type FetchInput = string | URL | Request;
|
|
5
|
+
type FetchInit = RequestInit | BunFetchRequestInit;
|
|
6
|
+
|
|
7
|
+
describe("runUpdateCommand fetch cancellation", () => {
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("checks release metadata with a timeout signal", async () => {
|
|
13
|
+
let requestSignal: AbortSignal | undefined;
|
|
14
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
15
|
+
const fetchStub = Object.assign(
|
|
16
|
+
async (_input: FetchInput, init?: FetchInit) => {
|
|
17
|
+
requestSignal = init?.signal ?? undefined;
|
|
18
|
+
return Response.json({ version: "999.0.0" });
|
|
19
|
+
},
|
|
20
|
+
{ preconnect: globalThis.fetch.preconnect },
|
|
21
|
+
);
|
|
22
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(fetchStub);
|
|
23
|
+
|
|
24
|
+
await runUpdateCommand({ force: false, check: true });
|
|
25
|
+
|
|
26
|
+
expect(requestSignal).toBeInstanceOf(AbortSignal);
|
|
27
|
+
});
|
|
28
|
+
});
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { $which, APP_NAME, isEnoent, VERSION } from "@oh-my-pi/pi-utils";
|
|
|
12
12
|
import { $ } from "bun";
|
|
13
13
|
import chalk from "chalk";
|
|
14
14
|
import { theme } from "../modes/theme/theme";
|
|
15
|
+
import { isTimeoutError, withTimeoutSignal } from "../utils/fetch-timeout";
|
|
15
16
|
|
|
16
17
|
const REPO = "can1357/oh-my-pi";
|
|
17
18
|
const PACKAGE = "@oh-my-pi/pi-coding-agent";
|
|
@@ -29,6 +30,8 @@ const MISE_TOOL = "github:can1357/oh-my-pi";
|
|
|
29
30
|
* See #1686.
|
|
30
31
|
*/
|
|
31
32
|
const NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
33
|
+
const RELEASE_METADATA_TIMEOUT_MS = 30_000;
|
|
34
|
+
const BINARY_DOWNLOAD_TIMEOUT_MS = 15 * 60_000;
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
37
|
* Core native addon package. Bumped in lock-step with {@link PACKAGE} so the
|
|
@@ -82,7 +85,7 @@ export interface BinaryReplacementOptions {
|
|
|
82
85
|
* Parse update subcommand arguments.
|
|
83
86
|
* Returns undefined if not an update command.
|
|
84
87
|
*/
|
|
85
|
-
export function parseUpdateArgs(args: string[]): { force: boolean; check: boolean } | undefined {
|
|
88
|
+
export function parseUpdateArgs(args: string[]): { force: boolean; check: boolean; plugins: boolean } | undefined {
|
|
86
89
|
if (args.length === 0 || args[0] !== "update") {
|
|
87
90
|
return undefined;
|
|
88
91
|
}
|
|
@@ -90,6 +93,7 @@ export function parseUpdateArgs(args: string[]): { force: boolean; check: boolea
|
|
|
90
93
|
return {
|
|
91
94
|
force: args.includes("--force") || args.includes("-f"),
|
|
92
95
|
check: args.includes("--check") || args.includes("-c"),
|
|
96
|
+
plugins: args.includes("--plugins") || args.includes("-l"),
|
|
93
97
|
};
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -239,7 +243,17 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
|
239
243
|
* Uses npm instead of GitHub API to avoid unauthenticated rate limiting.
|
|
240
244
|
*/
|
|
241
245
|
async function getLatestRelease(): Promise<ReleaseInfo> {
|
|
242
|
-
|
|
246
|
+
let response: Response;
|
|
247
|
+
try {
|
|
248
|
+
response = await fetch(`${NPM_REGISTRY}${PACKAGE}/latest`, {
|
|
249
|
+
signal: withTimeoutSignal(RELEASE_METADATA_TIMEOUT_MS),
|
|
250
|
+
});
|
|
251
|
+
} catch (err) {
|
|
252
|
+
if (isTimeoutError(err)) {
|
|
253
|
+
throw new Error("Timed out fetching release info after 30s", { cause: err });
|
|
254
|
+
}
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
243
257
|
if (!response.ok) {
|
|
244
258
|
throw new Error(`Failed to fetch release info: ${response.statusText}`);
|
|
245
259
|
}
|
|
@@ -832,7 +846,18 @@ async function updateViaBinaryAt(targetPath: string, expectedVersion: string): P
|
|
|
832
846
|
const backupPath = `${targetPath}.${Date.now()}.${process.pid}.bak`;
|
|
833
847
|
console.log(chalk.dim(`Downloading ${binaryName}…`));
|
|
834
848
|
|
|
835
|
-
|
|
849
|
+
let response: Response;
|
|
850
|
+
try {
|
|
851
|
+
response = await fetch(url, {
|
|
852
|
+
redirect: "follow",
|
|
853
|
+
signal: withTimeoutSignal(BINARY_DOWNLOAD_TIMEOUT_MS),
|
|
854
|
+
});
|
|
855
|
+
} catch (err) {
|
|
856
|
+
if (isTimeoutError(err)) {
|
|
857
|
+
throw new Error("Timed out downloading release binary after 15 minutes", { cause: err });
|
|
858
|
+
}
|
|
859
|
+
throw err;
|
|
860
|
+
}
|
|
836
861
|
if (!response.ok || !response.body) {
|
|
837
862
|
throw new Error(`Download failed: ${response.statusText}`);
|
|
838
863
|
}
|
|
@@ -914,12 +939,14 @@ ${chalk.bold("Usage:")}
|
|
|
914
939
|
${APP_NAME} update [options]
|
|
915
940
|
|
|
916
941
|
${chalk.bold("Options:")}
|
|
917
|
-
-c, --check
|
|
918
|
-
-f, --force
|
|
942
|
+
-c, --check Check for updates without installing
|
|
943
|
+
-f, --force Force reinstall even if up to date
|
|
944
|
+
-l, --plugins Update installed plugins
|
|
919
945
|
|
|
920
946
|
${chalk.bold("Examples:")}
|
|
921
|
-
${APP_NAME} update
|
|
922
|
-
${APP_NAME} update --check
|
|
923
|
-
${APP_NAME} update --force
|
|
947
|
+
${APP_NAME} update Update to latest version
|
|
948
|
+
${APP_NAME} update --check Check if updates are available
|
|
949
|
+
${APP_NAME} update --force Force reinstall
|
|
950
|
+
${APP_NAME} update -l Update installed plugins
|
|
924
951
|
`);
|
|
925
952
|
}
|
package/src/cli/usage-cli.ts
CHANGED
|
@@ -370,6 +370,29 @@ function formatLimitLine(limit: UsageLimit, labelWidth: number, nowMs: number):
|
|
|
370
370
|
return lines;
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
interface ProviderLimitTemplate {
|
|
374
|
+
id: string;
|
|
375
|
+
title: string;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function collectProviderLimitTemplates(reports: UsageReport[]): ProviderLimitTemplate[] {
|
|
379
|
+
const seen = new Set<string>();
|
|
380
|
+
const templates: ProviderLimitTemplate[] = [];
|
|
381
|
+
for (const report of reports) {
|
|
382
|
+
for (const limit of report.limits) {
|
|
383
|
+
if (seen.has(limit.id)) continue;
|
|
384
|
+
seen.add(limit.id);
|
|
385
|
+
templates.push({ id: limit.id, title: limitTitle(limit) });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return templates;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function formatMissingLimitLine(template: ProviderLimitTemplate, labelWidth: number): string {
|
|
392
|
+
const padded = template.title.padEnd(labelWidth);
|
|
393
|
+
return ` ${chalk.dim("○")} ${padded} ${chalk.dim("·".repeat(BAR_WIDTH))} ${chalk.dim("not reported")}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
373
396
|
/** Per-window capacity stat: how much account quota is burned and left. */
|
|
374
397
|
export interface ProviderWindowStat {
|
|
375
398
|
/** Compact window label, e.g. "5h", "7d". */
|
|
@@ -474,9 +497,8 @@ export function formatUsageBreakdown(
|
|
|
474
497
|
for (const note of providerNotes)
|
|
475
498
|
lines.push(` ${chalk.dim(sanitizeText(note.replace(/[\r\n]+/g, " ").replace(/\t/g, " ")))}`);
|
|
476
499
|
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
.reduce((max, limit) => Math.max(max, limitTitle(limit).length), 0);
|
|
500
|
+
const providerLimitTemplates = collectProviderLimitTemplates(providerReports);
|
|
501
|
+
const labelWidth = providerLimitTemplates.reduce((max, template) => Math.max(max, template.title.length), 0);
|
|
480
502
|
|
|
481
503
|
providerReports.forEach((report, index) => {
|
|
482
504
|
lines.push(` ${formatAccountHeader(report, index, nowMs, redaction)}`);
|
|
@@ -484,8 +506,15 @@ export function formatUsageBreakdown(
|
|
|
484
506
|
lines.push(` ${chalk.dim("no limits reported")}`);
|
|
485
507
|
return;
|
|
486
508
|
}
|
|
487
|
-
|
|
488
|
-
|
|
509
|
+
const limitsById = new Map<string, UsageLimit>();
|
|
510
|
+
for (const limit of report.limits) limitsById.set(limit.id, limit);
|
|
511
|
+
for (const template of providerLimitTemplates) {
|
|
512
|
+
const limit = limitsById.get(template.id);
|
|
513
|
+
if (limit) {
|
|
514
|
+
lines.push(...formatLimitLine(limit, labelWidth, nowMs));
|
|
515
|
+
} else {
|
|
516
|
+
lines.push(formatMissingLimitLine(template, labelWidth));
|
|
517
|
+
}
|
|
489
518
|
}
|
|
490
519
|
});
|
|
491
520
|
|
package/src/commands/update.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Check for and install updates.
|
|
3
3
|
*/
|
|
4
4
|
import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
|
5
|
-
import
|
|
5
|
+
import * as pluginCli from "../cli/plugin-cli";
|
|
6
|
+
import * as updateCli from "../cli/update-cli";
|
|
6
7
|
import { initTheme } from "../modes/theme/theme";
|
|
7
8
|
|
|
8
9
|
export default class Update extends Command {
|
|
@@ -11,11 +12,16 @@ export default class Update extends Command {
|
|
|
11
12
|
static flags = {
|
|
12
13
|
force: Flags.boolean({ char: "f", description: "Force update", default: false }),
|
|
13
14
|
check: Flags.boolean({ char: "c", description: "Check for updates without installing", default: false }),
|
|
15
|
+
plugins: Flags.boolean({ char: "l", description: "Update installed plugins", default: false }),
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
async run(): Promise<void> {
|
|
17
19
|
const { flags } = await this.parse(Update);
|
|
18
20
|
await initTheme();
|
|
19
|
-
|
|
21
|
+
if (flags.plugins) {
|
|
22
|
+
await pluginCli.runPluginCommand({ action: "upgrade", args: [], flags: {} });
|
|
23
|
+
} else {
|
|
24
|
+
await updateCli.runUpdateCommand({ force: flags.force, check: flags.check });
|
|
25
|
+
}
|
|
20
26
|
}
|
|
21
27
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
4
|
-
import type
|
|
4
|
+
import { ArkErrors, type Type } from "arktype";
|
|
5
5
|
import { JSONC, YAML } from "bun";
|
|
6
6
|
|
|
7
7
|
/** Minimal subset of the AJV ConfigSchemaError shape this module actually relies on. */
|
|
@@ -220,11 +220,11 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
const checked = this.schema(parsed);
|
|
223
|
-
if (checked instanceof
|
|
224
|
-
const schemaErrors: ConfigSchemaError[] =
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
223
|
+
if (checked instanceof ArkErrors) {
|
|
224
|
+
const schemaErrors: ConfigSchemaError[] = checked.map(error => ({
|
|
225
|
+
instancePath: error.path.length === 0 ? "root" : error.path.join("."),
|
|
226
|
+
message: error.problem,
|
|
227
|
+
}));
|
|
228
228
|
const error = new ConfigError(this.id, schemaErrors);
|
|
229
229
|
logger.warn("Failed to parse config file", { path: this.path(), error });
|
|
230
230
|
return this.#storeCache({ error, status: "error" });
|
|
@@ -4337,6 +4337,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
4337
4337
|
description: "Providers that web_search should never use, even as fallbacks",
|
|
4338
4338
|
},
|
|
4339
4339
|
},
|
|
4340
|
+
"providers.webSearchGeminiModel": {
|
|
4341
|
+
type: "string",
|
|
4342
|
+
default: undefined,
|
|
4343
|
+
ui: {
|
|
4344
|
+
tab: "providers",
|
|
4345
|
+
group: "Services",
|
|
4346
|
+
label: "Gemini web_search model",
|
|
4347
|
+
description: "Model ID for Gemini Google Search grounding. Defaults to gemini-2.5-flash.",
|
|
4348
|
+
},
|
|
4349
|
+
},
|
|
4340
4350
|
"providers.antigravityEndpoint": {
|
|
4341
4351
|
type: "enum",
|
|
4342
4352
|
values: ["auto", "production", "sandbox"] as const,
|
package/src/dap/client.ts
CHANGED
|
@@ -17,6 +17,14 @@ import type {
|
|
|
17
17
|
interface DapSpawnOptions {
|
|
18
18
|
adapter: DapResolvedAdapter;
|
|
19
19
|
cwd: string;
|
|
20
|
+
/**
|
|
21
|
+
* Cap on how long the socket-mode helpers wait for the adapter to open its
|
|
22
|
+
* socket (unix) or dial back into our listener (TCP). Exposed for tests;
|
|
23
|
+
* production callers rely on the default.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
socketReadyTimeoutMs?: number;
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
/** Minimal write interface shared by Bun.FileSink and Bun TCP sockets. */
|
|
@@ -29,13 +37,14 @@ type DapEventHandler = (body: unknown, event: DapEventMessage) => void | Promise
|
|
|
29
37
|
type DapReverseRequestHandler = (args: unknown) => unknown | Promise<unknown>;
|
|
30
38
|
|
|
31
39
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Hard cap on a single message write. A wedged adapter stdin used to hang the
|
|
42
|
+
* whole client forever; on hitting this cap the client disposes itself so the
|
|
43
|
+
* next request fails fast instead of piling more work onto a broken adapter.
|
|
44
|
+
*/
|
|
45
|
+
const WRITE_MESSAGE_TIMEOUT_MS = 30_000;
|
|
46
|
+
/** Default wait for socket-mode adapters to become reachable. */
|
|
47
|
+
const SOCKET_READY_TIMEOUT_MS = 10_000;
|
|
39
48
|
|
|
40
49
|
function toErrorMessage(value: unknown): string {
|
|
41
50
|
if (value instanceof Error) return value.message;
|
|
@@ -62,6 +71,8 @@ export class DapClient {
|
|
|
62
71
|
#eventHandlers = new Map<string, Set<DapEventHandler>>();
|
|
63
72
|
#anyEventHandlers = new Set<DapEventHandler>();
|
|
64
73
|
#reverseRequestHandlers = new Map<string, DapReverseRequestHandler>();
|
|
74
|
+
#adapterExited = false;
|
|
75
|
+
#pendingWriteExitRejectors = new Set<() => void>();
|
|
65
76
|
|
|
66
77
|
constructor(
|
|
67
78
|
adapter: DapResolvedAdapter,
|
|
@@ -75,11 +86,15 @@ export class DapClient {
|
|
|
75
86
|
this.#readable = options?.readable ?? (proc.stdout as ReadableStream<Uint8Array>);
|
|
76
87
|
this.#writeSink = options?.writeSink ?? proc.stdin;
|
|
77
88
|
this.#socket = options?.socket;
|
|
89
|
+
this.proc.exited.then(
|
|
90
|
+
() => this.#rejectPendingWritesForExit(),
|
|
91
|
+
() => this.#rejectPendingWritesForExit(),
|
|
92
|
+
);
|
|
78
93
|
}
|
|
79
94
|
|
|
80
|
-
static async spawn({ adapter, cwd }: DapSpawnOptions): Promise<DapClient> {
|
|
95
|
+
static async spawn({ adapter, cwd, socketReadyTimeoutMs }: DapSpawnOptions): Promise<DapClient> {
|
|
81
96
|
if (adapter.connectMode === "socket") {
|
|
82
|
-
return DapClient.#spawnSocket({ adapter, cwd });
|
|
97
|
+
return DapClient.#spawnSocket({ adapter, cwd, socketReadyTimeoutMs });
|
|
83
98
|
}
|
|
84
99
|
// Merge non-interactive env and start in a new session (detached → setsid)
|
|
85
100
|
// so the adapter process tree has no controlling terminal. Without this,
|
|
@@ -108,17 +123,18 @@ export class DapClient {
|
|
|
108
123
|
* Linux: connect to a unix domain socket via --listen=unix:<path>
|
|
109
124
|
* macOS/other: the adapter dials into our TCP listener via --client-addr
|
|
110
125
|
*/
|
|
111
|
-
static async #spawnSocket({ adapter, cwd }: DapSpawnOptions): Promise<DapClient> {
|
|
126
|
+
static async #spawnSocket({ adapter, cwd, socketReadyTimeoutMs }: DapSpawnOptions): Promise<DapClient> {
|
|
112
127
|
const env = {
|
|
113
128
|
...Bun.env,
|
|
114
129
|
...NON_INTERACTIVE_ENV,
|
|
115
130
|
};
|
|
131
|
+
const timeoutMs = socketReadyTimeoutMs ?? SOCKET_READY_TIMEOUT_MS;
|
|
116
132
|
const isLinux = process.platform === "linux";
|
|
117
133
|
|
|
118
134
|
if (isLinux) {
|
|
119
|
-
return DapClient.#spawnSocketUnix({ adapter, cwd, env });
|
|
135
|
+
return DapClient.#spawnSocketUnix({ adapter, cwd, env, timeoutMs });
|
|
120
136
|
}
|
|
121
|
-
return DapClient.#spawnSocketClientAddr({ adapter, cwd, env });
|
|
137
|
+
return DapClient.#spawnSocketClientAddr({ adapter, cwd, env, timeoutMs });
|
|
122
138
|
}
|
|
123
139
|
|
|
124
140
|
/** Linux: spawn adapter with --listen=unix:<path>, then connect to the socket. */
|
|
@@ -126,10 +142,12 @@ export class DapClient {
|
|
|
126
142
|
adapter,
|
|
127
143
|
cwd,
|
|
128
144
|
env,
|
|
145
|
+
timeoutMs,
|
|
129
146
|
}: {
|
|
130
147
|
adapter: DapResolvedAdapter;
|
|
131
148
|
cwd: string;
|
|
132
149
|
env: Record<string, string | undefined>;
|
|
150
|
+
timeoutMs: number;
|
|
133
151
|
}): Promise<DapClient> {
|
|
134
152
|
const socketPath = `/tmp/dap-${adapter.name}-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`;
|
|
135
153
|
const proc = ptree.spawn([adapter.resolvedCommand, ...adapter.args, `--listen=unix:${socketPath}`], {
|
|
@@ -139,13 +157,23 @@ export class DapClient {
|
|
|
139
157
|
detached: true,
|
|
140
158
|
});
|
|
141
159
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
160
|
+
// If waitForCondition throws (timeout, or adapter exited early) or the
|
|
161
|
+
// socket connect fails, we must not leak the detached adapter process.
|
|
162
|
+
try {
|
|
163
|
+
await waitForCondition(() => isUnixSocketReady(socketPath), timeoutMs, proc);
|
|
164
|
+
const { readable, writeSink, socket } = await connectSocket({ unix: socketPath });
|
|
165
|
+
const client = new DapClient(adapter, cwd, proc, { readable, writeSink, socket });
|
|
166
|
+
proc.exited.then(() => client.#handleProcessExit());
|
|
167
|
+
void client.#startMessageReader();
|
|
168
|
+
return client;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
try {
|
|
171
|
+
proc.kill();
|
|
172
|
+
} catch {
|
|
173
|
+
/* proc may already be dead */
|
|
174
|
+
}
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
149
177
|
}
|
|
150
178
|
|
|
151
179
|
/** macOS/other: listen on a random TCP port, spawn adapter with --client-addr, accept connection. */
|
|
@@ -153,10 +181,12 @@ export class DapClient {
|
|
|
153
181
|
adapter,
|
|
154
182
|
cwd,
|
|
155
183
|
env,
|
|
184
|
+
timeoutMs,
|
|
156
185
|
}: {
|
|
157
186
|
adapter: DapResolvedAdapter;
|
|
158
187
|
cwd: string;
|
|
159
188
|
env: Record<string, string | undefined>;
|
|
189
|
+
timeoutMs: number;
|
|
160
190
|
}): Promise<DapClient> {
|
|
161
191
|
const { promise: connPromise, resolve: resolveConn } = Promise.withResolvers<Bun.Socket<undefined>>();
|
|
162
192
|
|
|
@@ -182,25 +212,32 @@ export class DapClient {
|
|
|
182
212
|
detached: true,
|
|
183
213
|
});
|
|
184
214
|
|
|
185
|
-
// Wait for
|
|
186
|
-
|
|
215
|
+
// Wait for the adapter to dial back. On timeout (or any other failure
|
|
216
|
+
// before we've wired up the client) kill `proc` — otherwise the detached
|
|
217
|
+
// adapter process is orphaned.
|
|
187
218
|
const { promise: timeoutPromise, reject: rejectTimeout } = Promise.withResolvers<never>();
|
|
188
219
|
const connectTimeout = setTimeout(
|
|
189
|
-
() => rejectTimeout(new Error(`${adapter.name} did not connect within
|
|
190
|
-
|
|
220
|
+
() => rejectTimeout(new Error(`${adapter.name} did not connect within ${timeoutMs}ms`)),
|
|
221
|
+
timeoutMs,
|
|
191
222
|
);
|
|
192
223
|
try {
|
|
193
|
-
rawSocket = await Promise.race([connPromise, timeoutPromise]);
|
|
224
|
+
const rawSocket = await Promise.race([connPromise, timeoutPromise]);
|
|
225
|
+
const { readable, writeSink, socket } = wrapBunSocket(rawSocket);
|
|
226
|
+
const client = new DapClient(adapter, cwd, proc, { readable, writeSink, socket });
|
|
227
|
+
proc.exited.then(() => client.#handleProcessExit());
|
|
228
|
+
void client.#startMessageReader();
|
|
229
|
+
return client;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
try {
|
|
232
|
+
proc.kill();
|
|
233
|
+
} catch {
|
|
234
|
+
/* proc may already be dead */
|
|
235
|
+
}
|
|
236
|
+
throw error;
|
|
194
237
|
} finally {
|
|
195
238
|
clearTimeout(connectTimeout);
|
|
196
239
|
server.stop();
|
|
197
240
|
}
|
|
198
|
-
|
|
199
|
-
const { readable, writeSink, socket } = wrapBunSocket(rawSocket);
|
|
200
|
-
const client = new DapClient(adapter, cwd, proc, { readable, writeSink, socket });
|
|
201
|
-
proc.exited.then(() => client.#handleProcessExit());
|
|
202
|
-
void client.#startMessageReader();
|
|
203
|
-
return client;
|
|
204
241
|
}
|
|
205
242
|
|
|
206
243
|
get capabilities(): DapCapabilities | undefined {
|
|
@@ -309,6 +346,12 @@ export class DapClient {
|
|
|
309
346
|
arguments: args,
|
|
310
347
|
};
|
|
311
348
|
const { promise, resolve, reject } = Promise.withResolvers<TBody>();
|
|
349
|
+
// Suppress "unhandled rejection" if the request timer or abort fires
|
|
350
|
+
// before the caller's `await` subscribes — e.g. while #writeMessage is
|
|
351
|
+
// still racing a wedged stdin flush. The caller's own `await` still
|
|
352
|
+
// receives the rejection normally; this handler is a passive guard.
|
|
353
|
+
promise.catch(() => {});
|
|
354
|
+
|
|
312
355
|
let timeout: NodeJS.Timeout | undefined;
|
|
313
356
|
const cleanup = () => {
|
|
314
357
|
if (timeout) clearTimeout(timeout);
|
|
@@ -342,13 +385,15 @@ export class DapClient {
|
|
|
342
385
|
},
|
|
343
386
|
});
|
|
344
387
|
this.#lastActivity = Date.now();
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
388
|
+
// Fire the write in the background. Awaiting it here would let a wedged
|
|
389
|
+
// stdin flush block the caller's `timeoutMs`; if it fails, propagate the
|
|
390
|
+
// failure into `promise` — the timer or abort may still win the race.
|
|
391
|
+
void this.#writeMessage(request).catch(error => {
|
|
392
|
+
if (!this.#pendingRequests.has(requestSeq)) return;
|
|
348
393
|
this.#pendingRequests.delete(requestSeq);
|
|
349
394
|
cleanup();
|
|
350
|
-
|
|
351
|
-
}
|
|
395
|
+
reject(error);
|
|
396
|
+
});
|
|
352
397
|
return promise;
|
|
353
398
|
}
|
|
354
399
|
|
|
@@ -362,7 +407,60 @@ export class DapClient {
|
|
|
362
407
|
...(message ? { message } : {}),
|
|
363
408
|
...(body !== undefined ? { body } : {}),
|
|
364
409
|
};
|
|
365
|
-
await writeMessage(
|
|
410
|
+
await this.#writeMessage(response);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Framed write to the adapter, bounded by {@link WRITE_MESSAGE_TIMEOUT_MS}
|
|
415
|
+
* and by adapter exit. Without this bound a wedged adapter stdin used to
|
|
416
|
+
* hang the whole client forever. On timeout or exit-before-flush the client
|
|
417
|
+
* disposes itself and rethrows.
|
|
418
|
+
*/
|
|
419
|
+
async #writeMessage(message: DapRequestMessage | DapResponseMessage): Promise<void> {
|
|
420
|
+
const content = JSON.stringify(message);
|
|
421
|
+
this.#writeSink.write(`Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n`);
|
|
422
|
+
this.#writeSink.write(content);
|
|
423
|
+
const flushResult = this.#writeSink.flush();
|
|
424
|
+
if (!(flushResult instanceof Promise)) return;
|
|
425
|
+
|
|
426
|
+
if (this.#adapterExited) {
|
|
427
|
+
throw new Error(`DAP adapter ${this.adapter.name} exited before write completed`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const { promise: guardPromise, reject: guardReject, resolve: guardResolve } = Promise.withResolvers<void>();
|
|
431
|
+
const timer = setTimeout(
|
|
432
|
+
() =>
|
|
433
|
+
guardReject(
|
|
434
|
+
new Error(`DAP adapter ${this.adapter.name} write timed out after ${WRITE_MESSAGE_TIMEOUT_MS}ms`),
|
|
435
|
+
),
|
|
436
|
+
WRITE_MESSAGE_TIMEOUT_MS,
|
|
437
|
+
);
|
|
438
|
+
const rejectOnExit = () => {
|
|
439
|
+
guardReject(new Error(`DAP adapter ${this.adapter.name} exited before write completed`));
|
|
440
|
+
};
|
|
441
|
+
this.#pendingWriteExitRejectors.add(rejectOnExit);
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
await Promise.race([flushResult, guardPromise]);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
// The client is now known-broken. Kick off dispose in the background;
|
|
447
|
+
// callers will see subsequent sendRequest calls fail fast.
|
|
448
|
+
void this.dispose();
|
|
449
|
+
throw error;
|
|
450
|
+
} finally {
|
|
451
|
+
clearTimeout(timer);
|
|
452
|
+
this.#pendingWriteExitRejectors.delete(rejectOnExit);
|
|
453
|
+
// Release the guard so any late timeout callback becomes a no-op.
|
|
454
|
+
guardResolve();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
#rejectPendingWritesForExit(): void {
|
|
459
|
+
this.#adapterExited = true;
|
|
460
|
+
for (const reject of this.#pendingWriteExitRejectors) {
|
|
461
|
+
reject();
|
|
462
|
+
}
|
|
463
|
+
this.#pendingWriteExitRejectors.clear();
|
|
366
464
|
}
|
|
367
465
|
|
|
368
466
|
async dispose(): Promise<void> {
|
|
@@ -132,12 +132,23 @@ export function recordSeenLines(
|
|
|
132
132
|
* can reject edits anchored on lines the model never saw. Best-effort: a no-op
|
|
133
133
|
* when the body has no numbered rows or the snapshot already aged out. `tag`
|
|
134
134
|
* must be the tag returned when this exact content was recorded.
|
|
135
|
+
*
|
|
136
|
+
* `excludedLines` prunes 1-indexed line numbers whose displayed text was
|
|
137
|
+
* column-truncated (or otherwise not shown in full). A column-clipped row
|
|
138
|
+
* still carries a `NN:` prefix — the parser sees the number and would
|
|
139
|
+
* otherwise mark the line "seen" even though only its prefix ever reached
|
|
140
|
+
* the model. Producers that apply per-line column truncation MUST supply
|
|
141
|
+
* the clipped line set so the patcher's seen-line guard keeps rejecting
|
|
142
|
+
* edits against those lines until a full-width read of them occurs.
|
|
135
143
|
*/
|
|
136
144
|
export function recordSeenLinesFromBody(
|
|
137
145
|
session: FileSnapshotStoreOwner,
|
|
138
146
|
absolutePath: string,
|
|
139
147
|
tag: string,
|
|
140
148
|
body: string,
|
|
149
|
+
excludedLines?: ReadonlySet<number>,
|
|
141
150
|
): void {
|
|
142
|
-
|
|
151
|
+
const parsed = parseSeenLinesFromHashlineBody(body);
|
|
152
|
+
const filtered = excludedLines && excludedLines.size > 0 ? parsed.filter(line => !excludedLines.has(line)) : parsed;
|
|
153
|
+
recordSeenLines(session, absolutePath, tag, filtered);
|
|
143
154
|
}
|