@oh-my-pi/pi-coding-agent 15.7.6 → 15.8.2
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 +172 -198
- package/dist/types/async/job-manager.d.ts +3 -3
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/claude-trace-cli.d.ts +54 -0
- package/dist/types/cli/session-picker.d.ts +10 -3
- package/dist/types/cli/update-cli.d.ts +17 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +10 -1
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/edit/index.d.ts +6 -0
- package/dist/types/edit/streaming.d.ts +8 -0
- package/dist/types/eval/concurrency-bridge.d.ts +26 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- package/dist/types/export/ttsr.d.ts +9 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +11 -0
- package/dist/types/main.d.ts +5 -0
- package/dist/types/mcp/transports/stdio.d.ts +19 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/hook-selector.d.ts +3 -0
- package/dist/types/modes/components/session-selector.d.ts +32 -5
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -2
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +9 -2
- package/dist/types/modes/types.d.ts +4 -2
- package/dist/types/registry/agent-registry.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/history-storage.d.ts +16 -1
- package/dist/types/session/session-manager.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +6 -15
- package/dist/types/tools/find.d.ts +0 -9
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/sqlite-reader.d.ts +25 -8
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/clipboard.d.ts +4 -0
- package/dist/types/utils/jj.d.ts +49 -0
- package/dist/types/web/kagi.d.ts +76 -0
- package/dist/types/web/search/providers/exa.d.ts +7 -1
- package/dist/types/web/search/providers/kagi.d.ts +1 -0
- package/package.json +9 -9
- package/src/async/job-manager.ts +3 -3
- package/src/cli/args.ts +6 -2
- package/src/cli/claude-trace-cli.ts +783 -0
- package/src/cli/session-picker.ts +36 -10
- package/src/cli/update-cli.ts +35 -2
- package/src/commands/launch.ts +3 -0
- package/src/config/keybindings.ts +14 -1
- package/src/config/model-registry.ts +18 -7
- package/src/config/settings-schema.ts +2 -2
- package/src/config/settings.ts +23 -0
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +44 -0
- package/src/discovery/claude-plugins.ts +7 -9
- package/src/edit/index.ts +10 -0
- package/src/edit/streaming.ts +65 -0
- package/src/eval/__tests__/agent-bridge.test.ts +58 -4
- package/src/eval/concurrency-bridge.ts +34 -0
- package/src/eval/js/shared/prelude.txt +20 -17
- package/src/eval/js/tool-bridge.ts +5 -0
- package/src/eval/py/prelude.py +23 -15
- package/src/export/ttsr.ts +18 -1
- package/src/extensibility/custom-commands/bundled/review/index.ts +74 -45
- package/src/extensibility/plugins/legacy-pi-compat.ts +115 -131
- package/src/extensibility/skills.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +12 -11
- package/src/main.ts +92 -24
- package/src/mcp/transports/stdio.ts +55 -22
- package/src/modes/acp/acp-event-mapper.ts +54 -4
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/hook-selector.ts +89 -31
- package/src/modes/components/oauth-selector.ts +12 -6
- package/src/modes/components/session-selector.ts +179 -24
- package/src/modes/components/tool-execution.ts +16 -3
- package/src/modes/controllers/command-controller.ts +2 -11
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +19 -1
- package/src/modes/controllers/selector-controller.ts +61 -21
- package/src/modes/interactive-mode.ts +125 -15
- package/src/modes/types.ts +5 -2
- package/src/prompts/agents/reviewer.md +2 -2
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/empty-stop-retry.md +6 -0
- package/src/prompts/system/orchestrate-notice.md +5 -3
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +5 -5
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/irc.md +6 -6
- package/src/prompts/tools/search-tool-bm25.md +9 -2
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -1
- package/src/registry/agent-registry.ts +1 -1
- package/src/sdk.ts +85 -31
- package/src/session/agent-session.ts +209 -54
- package/src/session/history-storage.ts +56 -12
- package/src/session/session-manager.ts +34 -0
- package/src/task/output-manager.ts +40 -48
- package/src/task/render.ts +3 -8
- package/src/tools/browser/tab-worker.ts +8 -5
- package/src/tools/find.ts +5 -29
- package/src/tools/index.ts +1 -1
- package/src/tools/path-utils.ts +144 -1
- package/src/tools/read.ts +47 -0
- package/src/tools/search-tool-bm25.ts +7 -1
- package/src/tools/search.ts +2 -27
- package/src/tools/sqlite-reader.ts +92 -9
- package/src/tools/write.ts +6 -0
- package/src/utils/clipboard.ts +38 -1
- package/src/utils/git.ts +19 -23
- package/src/utils/jj.ts +225 -0
- package/src/utils/open.ts +37 -2
- package/src/web/kagi.ts +168 -49
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/exa.ts +20 -86
- package/src/web/search/providers/kagi.ts +4 -0
package/src/tools/read.ts
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
type LineRange,
|
|
70
70
|
parseLineRanges,
|
|
71
71
|
resolveReadPath,
|
|
72
|
+
splitDelimitedPathEntry,
|
|
72
73
|
splitInternalUrlSel,
|
|
73
74
|
splitPathAndSel,
|
|
74
75
|
} from "./path-utils";
|
|
@@ -693,6 +694,50 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
693
694
|
});
|
|
694
695
|
}
|
|
695
696
|
|
|
697
|
+
async #tryReadDelimitedPaths(
|
|
698
|
+
readPath: string,
|
|
699
|
+
signal?: AbortSignal,
|
|
700
|
+
): Promise<AgentToolResult<ReadToolDetails> | null> {
|
|
701
|
+
const parts = await splitDelimitedPathEntry(readPath, this.session.cwd);
|
|
702
|
+
if (!parts) return null;
|
|
703
|
+
|
|
704
|
+
const notice = `Note: interpreted as ${parts.length} paths: ${parts.join(", ")}`;
|
|
705
|
+
const notes = [notice];
|
|
706
|
+
const content: Array<TextContent | ImageContent> = [];
|
|
707
|
+
let pendingText = notice;
|
|
708
|
+
const flushText = () => {
|
|
709
|
+
if (pendingText.length === 0) return;
|
|
710
|
+
content.push({ type: "text", text: pendingText });
|
|
711
|
+
pendingText = "";
|
|
712
|
+
};
|
|
713
|
+
const appendText = (text: string) => {
|
|
714
|
+
pendingText = pendingText.length > 0 ? `${pendingText}\n\n${text}` : text;
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
for (const part of parts) {
|
|
718
|
+
try {
|
|
719
|
+
const result = await this.execute("read-delimited-part", { path: part }, signal);
|
|
720
|
+
for (const block of result.content) {
|
|
721
|
+
if (block.type === "text") {
|
|
722
|
+
appendText(block.text);
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
flushText();
|
|
726
|
+
content.push(block);
|
|
727
|
+
}
|
|
728
|
+
} catch (error) {
|
|
729
|
+
if (error instanceof ToolAbortError || signal?.aborted) throw error;
|
|
730
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
731
|
+
const errorNote = `Could not read ${part}: ${message}`;
|
|
732
|
+
notes.push(errorNote);
|
|
733
|
+
appendText(`[${errorNote}]`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
flushText();
|
|
737
|
+
|
|
738
|
+
return toolResult<ReadToolDetails>({ notes }).content(content).done();
|
|
739
|
+
}
|
|
740
|
+
|
|
696
741
|
async #resolveArchiveReadPath(readPath: string, signal?: AbortSignal): Promise<ResolvedArchiveReadPath | null> {
|
|
697
742
|
const candidates = parseArchivePathCandidates(readPath);
|
|
698
743
|
for (const candidate of candidates) {
|
|
@@ -1596,6 +1641,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1596
1641
|
}
|
|
1597
1642
|
|
|
1598
1643
|
if (!suffixResolution) {
|
|
1644
|
+
const delimitedResult = await this.#tryReadDelimitedPaths(readPath, signal);
|
|
1645
|
+
if (delimitedResult) return delimitedResult;
|
|
1599
1646
|
throw new ToolError(`Path '${localReadPath}' not found`);
|
|
1600
1647
|
}
|
|
1601
1648
|
} else {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
buildDiscoverableToolSearchIndex,
|
|
11
11
|
type DiscoverableTool,
|
|
12
12
|
type DiscoverableToolSearchIndex,
|
|
13
|
+
filterBySource,
|
|
13
14
|
formatDiscoverableToolServerSummary,
|
|
14
15
|
searchDiscoverableTools,
|
|
15
16
|
summarizeDiscoverableTools,
|
|
@@ -141,10 +142,15 @@ function isDiscoveryEnabled(session: ToolSession): boolean {
|
|
|
141
142
|
|
|
142
143
|
export function renderSearchToolBm25Description(discoverableTools: DiscoverableTool[] = []): string {
|
|
143
144
|
const summary = summarizeDiscoverableTools(discoverableTools);
|
|
145
|
+
const builtinToolNames = filterBySource(discoverableTools, "builtin")
|
|
146
|
+
.map(t => t.name)
|
|
147
|
+
.sort();
|
|
144
148
|
return prompt.render(searchToolBm25Description, {
|
|
145
|
-
|
|
149
|
+
discoverableToolCount: summary.toolCount,
|
|
146
150
|
discoverableMCPServerSummaries: summary.servers.map(formatDiscoverableToolServerSummary),
|
|
147
151
|
hasDiscoverableMCPServers: summary.servers.length > 0,
|
|
152
|
+
discoverableBuiltinToolNames: builtinToolNames,
|
|
153
|
+
hasDiscoverableBuiltinTools: builtinToolNames.length > 0,
|
|
148
154
|
});
|
|
149
155
|
}
|
|
150
156
|
|
package/src/tools/search.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { formatGroupedFiles } from "./grouped-file-output";
|
|
|
30
30
|
import { formatMatchLine } from "./match-line-format";
|
|
31
31
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
32
32
|
import {
|
|
33
|
+
expandDelimitedPathEntries,
|
|
33
34
|
hasGlobPathChars,
|
|
34
35
|
isLineInRanges,
|
|
35
36
|
type LineRange,
|
|
@@ -93,29 +94,6 @@ export const SINGLE_FILE_MATCHES = 200;
|
|
|
93
94
|
* pagination headroom so the caller can see total file count. */
|
|
94
95
|
const INTERNAL_TOTAL_CAP = 2000;
|
|
95
96
|
|
|
96
|
-
/**
|
|
97
|
-
* Detect a `,` that is not inside a `{…}` brace expansion. Used to catch
|
|
98
|
-
* `paths: ["a,b"]` mistakes where the caller flattened multiple entries
|
|
99
|
-
* into a single string instead of passing a JSON array of strings.
|
|
100
|
-
*/
|
|
101
|
-
function containsTopLevelComma(entry: string): boolean {
|
|
102
|
-
let depth = 0;
|
|
103
|
-
for (let i = 0; i < entry.length; i++) {
|
|
104
|
-
const ch = entry[i];
|
|
105
|
-
if (ch === "\\" && i + 1 < entry.length) {
|
|
106
|
-
i++;
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (ch === "{") depth++;
|
|
110
|
-
else if (ch === "}") {
|
|
111
|
-
if (depth > 0) depth--;
|
|
112
|
-
} else if (ch === "," && depth === 0) {
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
97
|
/**
|
|
120
98
|
* Parsed `paths` entry — a path (possibly archive-shaped) plus an optional
|
|
121
99
|
* line-range selector peeled off the trailing `:N-M` (or `:N+K`, `:N,M`, …)
|
|
@@ -146,9 +124,6 @@ function parsePathSpecs(rawEntries: readonly string[]): SearchPathSpec[] {
|
|
|
146
124
|
clean = split.path;
|
|
147
125
|
ranges = parsed;
|
|
148
126
|
}
|
|
149
|
-
if (containsTopLevelComma(clean)) {
|
|
150
|
-
throw new ToolError('paths is an array — pass ["a", "b"] not ["a,b"]');
|
|
151
|
-
}
|
|
152
127
|
specs.push({ original: entry, clean, ranges });
|
|
153
128
|
}
|
|
154
129
|
return specs;
|
|
@@ -663,7 +638,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
663
638
|
if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) {
|
|
664
639
|
throw new ToolError("Skip must be a non-negative number");
|
|
665
640
|
}
|
|
666
|
-
const rawEntries = toPathList(rawPaths);
|
|
641
|
+
const rawEntries = await expandDelimitedPathEntries(toPathList(rawPaths), this.session.cwd);
|
|
667
642
|
const pathSpecs = parsePathSpecs(rawEntries);
|
|
668
643
|
const paths = pathSpecs.map(spec => spec.clean);
|
|
669
644
|
const {
|
|
@@ -12,6 +12,15 @@ const MAX_QUERY_LIMIT = 500;
|
|
|
12
12
|
const MAX_RENDER_WIDTH = 120;
|
|
13
13
|
const MAX_COLUMN_WIDTH = 40;
|
|
14
14
|
const MIN_COLUMN_WIDTH = 1;
|
|
15
|
+
/**
|
|
16
|
+
* Upper bound on rows scanned when counting a table for the listing. SQLite has
|
|
17
|
+
* no stored row count, so `COUNT(*)` is a full b-tree scan — multi-second on a
|
|
18
|
+
* multi-GB database, and `bun:sqlite` runs it synchronously on the JS thread
|
|
19
|
+
* that also drives the TUI, freezing rendering and input. The listing instead
|
|
20
|
+
* trusts the planner's `sqlite_stat1` estimate for large tables and only counts
|
|
21
|
+
* exactly when a table is provably small, reading at most this many rows.
|
|
22
|
+
*/
|
|
23
|
+
const ROW_COUNT_PROBE_CAP = 50_000;
|
|
15
24
|
|
|
16
25
|
type SqliteBinding = Exclude<SQLQueryBindings, Record<string, unknown>>;
|
|
17
26
|
|
|
@@ -26,6 +35,11 @@ interface SqliteCountRow {
|
|
|
26
35
|
count: number;
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
interface SqliteStat1Row {
|
|
39
|
+
tbl: string;
|
|
40
|
+
stat: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
interface SqliteTableInfoRow {
|
|
30
44
|
cid: number;
|
|
31
45
|
name: string;
|
|
@@ -50,6 +64,23 @@ export type SqliteSelector =
|
|
|
50
64
|
|
|
51
65
|
export type SqliteRowLookup = { kind: "pk"; column: string; type: string } | { kind: "rowid" };
|
|
52
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Row count for a table in the listing.
|
|
69
|
+
* - `exact`: counted in full (the table is small enough to count cheaply).
|
|
70
|
+
* - `estimate`: the planner's `sqlite_stat1` figure; the table is too large to
|
|
71
|
+
* scan, so this may be stale.
|
|
72
|
+
* - `atLeast`: a lower bound; counting was capped before reaching the end.
|
|
73
|
+
*/
|
|
74
|
+
export type TableRowCount =
|
|
75
|
+
| { kind: "exact"; rows: number }
|
|
76
|
+
| { kind: "estimate"; rows: number }
|
|
77
|
+
| { kind: "atLeast"; rows: number };
|
|
78
|
+
|
|
79
|
+
export interface SqliteTableSummary {
|
|
80
|
+
name: string;
|
|
81
|
+
count: TableRowCount;
|
|
82
|
+
}
|
|
83
|
+
|
|
53
84
|
function splitSqliteRemainder(remainder: string): { subPath: string; queryString: string } {
|
|
54
85
|
const queryIndex = remainder.indexOf("?");
|
|
55
86
|
if (queryIndex === -1) {
|
|
@@ -495,20 +526,61 @@ export function parseSqliteSelector(subPath: string, queryString: string): Sqlit
|
|
|
495
526
|
return { kind: "schema", table, sampleLimit: DEFAULT_SCHEMA_SAMPLE_LIMIT };
|
|
496
527
|
}
|
|
497
528
|
|
|
498
|
-
|
|
529
|
+
/**
|
|
530
|
+
* Reads the planner's per-table row estimate from `sqlite_stat1` (populated by
|
|
531
|
+
* `ANALYZE`). The first integer of each `stat` string is the number of rows in
|
|
532
|
+
* that index; for a full (non-partial) index it equals the table's row count,
|
|
533
|
+
* so the max across a table's entries is the table estimate. Returns an empty
|
|
534
|
+
* map when the database was never analyzed. One small indexed read — no scan.
|
|
535
|
+
*/
|
|
536
|
+
function loadRowEstimates(db: Database): Map<string, number> {
|
|
537
|
+
const estimates = new Map<string, number>();
|
|
538
|
+
const hasStat1 = db
|
|
539
|
+
.prepare<Pick<SqliteMasterRow, "name">, []>(
|
|
540
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_stat1'",
|
|
541
|
+
)
|
|
542
|
+
.get();
|
|
543
|
+
if (!hasStat1) return estimates;
|
|
544
|
+
|
|
545
|
+
for (const { tbl, stat } of db.prepare<SqliteStat1Row, []>("SELECT tbl, stat FROM sqlite_stat1").all()) {
|
|
546
|
+
if (!stat) continue;
|
|
547
|
+
const rows = Number.parseInt(stat, 10);
|
|
548
|
+
if (!Number.isFinite(rows)) continue;
|
|
549
|
+
const prev = estimates.get(tbl);
|
|
550
|
+
if (prev === undefined || rows > prev) estimates.set(tbl, rows);
|
|
551
|
+
}
|
|
552
|
+
return estimates;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Counts a table while reading at most `cap + 1` rows. Returns an exact count
|
|
557
|
+
* when the table holds `cap` rows or fewer, otherwise a lower bound of `cap`.
|
|
558
|
+
* Bounds the worst-case scan so a stale or missing estimate can never trigger a
|
|
559
|
+
* full-table scan on the JS thread.
|
|
560
|
+
*/
|
|
561
|
+
function probeRowCount(db: Database, table: string, cap: number): TableRowCount {
|
|
562
|
+
const sql = `SELECT COUNT(*) AS count FROM (SELECT 1 FROM ${quoteSqliteIdentifier(table)} LIMIT ${cap + 1})`;
|
|
563
|
+
const counted = db.prepare<SqliteCountRow, []>(sql).get()?.count ?? 0;
|
|
564
|
+
return counted > cap ? { kind: "atLeast", rows: cap } : { kind: "exact", rows: counted };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function listTables(db: Database, options: { probeCap?: number } = {}): SqliteTableSummary[] {
|
|
568
|
+
const cap = options.probeCap ?? ROW_COUNT_PROBE_CAP;
|
|
499
569
|
const names = db
|
|
500
570
|
.prepare<Pick<SqliteMasterRow, "name">, []>(
|
|
501
571
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name COLLATE NOCASE",
|
|
502
572
|
)
|
|
503
573
|
.all();
|
|
574
|
+
const estimates = loadRowEstimates(db);
|
|
504
575
|
|
|
505
576
|
return names.map(({ name }) => {
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
577
|
+
const estimate = estimates.get(name);
|
|
578
|
+
// Trust the planner only when it says the table is too large to count
|
|
579
|
+
// cheaply; otherwise count exactly (bounded), which also corrects a
|
|
580
|
+
// stale-low estimate without ever scanning more than `cap` rows.
|
|
581
|
+
const count: TableRowCount =
|
|
582
|
+
estimate !== undefined && estimate > cap ? { kind: "estimate", rows: estimate } : probeRowCount(db, name, cap);
|
|
583
|
+
return { name, count };
|
|
512
584
|
});
|
|
513
585
|
}
|
|
514
586
|
|
|
@@ -679,13 +751,24 @@ export function deleteRowByRowId(db: Database, table: string, key: string): numb
|
|
|
679
751
|
return statement.run(binding).changes;
|
|
680
752
|
}
|
|
681
753
|
|
|
682
|
-
|
|
754
|
+
function formatRowCount(count: TableRowCount): string {
|
|
755
|
+
switch (count.kind) {
|
|
756
|
+
case "exact":
|
|
757
|
+
return `${count.rows} rows`;
|
|
758
|
+
case "estimate":
|
|
759
|
+
return `~${count.rows} rows`;
|
|
760
|
+
case "atLeast":
|
|
761
|
+
return `${count.rows}+ rows`;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export function renderTableList(tables: SqliteTableSummary[]): string {
|
|
683
766
|
if (tables.length === 0) {
|
|
684
767
|
return "(no tables)";
|
|
685
768
|
}
|
|
686
769
|
|
|
687
770
|
return tables
|
|
688
|
-
.map(table => truncateToWidth(replaceTabs(`${table.name} (${table.
|
|
771
|
+
.map(table => truncateToWidth(replaceTabs(`${table.name} (${formatRowCount(table.count)})`), MAX_RENDER_WIDTH))
|
|
689
772
|
.join("\n");
|
|
690
773
|
}
|
|
691
774
|
|
package/src/tools/write.ts
CHANGED
|
@@ -273,6 +273,12 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
273
273
|
readonly loadMode = "discoverable";
|
|
274
274
|
readonly summary = "Write content to a file (creates or overwrites)";
|
|
275
275
|
|
|
276
|
+
/** Stream matchers should see the real file content, not its JSON-escaped argument encoding. */
|
|
277
|
+
matcherDigest(args: unknown): string | undefined {
|
|
278
|
+
const content = (args as Partial<WriteParams>).content;
|
|
279
|
+
return typeof content === "string" ? content : undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
276
282
|
readonly #writethrough: WritethroughCallback;
|
|
277
283
|
|
|
278
284
|
constructor(private readonly session: ToolSession) {
|
package/src/utils/clipboard.ts
CHANGED
|
@@ -119,7 +119,7 @@ async function readImageViaPowerShell(): Promise<ClipboardImage | null> {
|
|
|
119
119
|
if (!b64) return null;
|
|
120
120
|
const bytes = Buffer.from(b64, "base64");
|
|
121
121
|
if (bytes.byteLength === 0) return null;
|
|
122
|
-
return { data:
|
|
122
|
+
return { data: bytes, mimeType: "image/png" };
|
|
123
123
|
} catch {
|
|
124
124
|
return null;
|
|
125
125
|
}
|
|
@@ -154,3 +154,40 @@ export async function readImageFromClipboard(): Promise<ClipboardImage | null> {
|
|
|
154
154
|
|
|
155
155
|
return (await native.readImageFromClipboard()) ?? null;
|
|
156
156
|
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Read plain text from the system clipboard.
|
|
160
|
+
*/
|
|
161
|
+
export async function readTextFromClipboard(): Promise<string> {
|
|
162
|
+
try {
|
|
163
|
+
const p = process.platform;
|
|
164
|
+
if (p === "darwin") {
|
|
165
|
+
return execSync("pbpaste", { encoding: "utf8", timeout: 2000 }).toString();
|
|
166
|
+
}
|
|
167
|
+
if (p === "win32") {
|
|
168
|
+
return execSync('powershell.exe -NoProfile -Command "Get-Clipboard"', {
|
|
169
|
+
encoding: "utf8",
|
|
170
|
+
timeout: 2000,
|
|
171
|
+
}).toString();
|
|
172
|
+
}
|
|
173
|
+
if (process.env.TERMUX_VERSION) {
|
|
174
|
+
return execSync("termux-clipboard-get", { encoding: "utf8", timeout: 2000 }).toString();
|
|
175
|
+
}
|
|
176
|
+
const hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);
|
|
177
|
+
const hasX11Display = Boolean(process.env.DISPLAY);
|
|
178
|
+
if (hasWaylandDisplay) {
|
|
179
|
+
try {
|
|
180
|
+
return execSync("wl-paste --type text/plain --no-newline", { encoding: "utf8", timeout: 2000 }).toString();
|
|
181
|
+
} catch {
|
|
182
|
+
if (hasX11Display) {
|
|
183
|
+
return execSync("xclip -selection clipboard -o", { encoding: "utf8", timeout: 2000 }).toString();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} else if (hasX11Display) {
|
|
187
|
+
return execSync("xclip -selection clipboard -o", { encoding: "utf8", timeout: 2000 }).toString();
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
logger.warn("clipboard: failed to read clipboard text", { error: String(error) });
|
|
191
|
+
}
|
|
192
|
+
return "";
|
|
193
|
+
}
|
package/src/utils/git.ts
CHANGED
|
@@ -189,11 +189,7 @@ function formatCommandFailure(
|
|
|
189
189
|
return `git ${args.join(" ")} failed with exit code ${result.exitCode}`;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
async function
|
|
193
|
-
cwd: string,
|
|
194
|
-
args: readonly string[],
|
|
195
|
-
options: CommandOptions = {},
|
|
196
|
-
): Promise<GitCommandResult> {
|
|
192
|
+
async function git(cwd: string, args: readonly string[], options: CommandOptions = {}): Promise<GitCommandResult> {
|
|
197
193
|
const commandArgs = withShortLivedGitConfig(options.readOnly ? withNoOptionalLocks(args) : [...args]);
|
|
198
194
|
const child = Bun.spawn(["git", ...commandArgs], {
|
|
199
195
|
cwd,
|
|
@@ -248,7 +244,7 @@ async function runChecked(
|
|
|
248
244
|
options: CommandOptions = {},
|
|
249
245
|
): Promise<GitCommandResult> {
|
|
250
246
|
ensureAvailable();
|
|
251
|
-
const result = await
|
|
247
|
+
const result = await git(cwd, args, options);
|
|
252
248
|
if (result.exitCode !== 0) {
|
|
253
249
|
throw new GitCommandError(args, result);
|
|
254
250
|
}
|
|
@@ -269,7 +265,7 @@ async function tryText(
|
|
|
269
265
|
options: CommandOptions = {},
|
|
270
266
|
): Promise<string | undefined> {
|
|
271
267
|
ensureAvailable();
|
|
272
|
-
const result = await
|
|
268
|
+
const result = await git(cwd, args, options);
|
|
273
269
|
if (result.exitCode !== 0) return undefined;
|
|
274
270
|
return result.stdout;
|
|
275
271
|
}
|
|
@@ -709,7 +705,7 @@ export const diff = Object.assign(
|
|
|
709
705
|
async function diff(cwd: string, options: DiffOptions = {}): Promise<string> {
|
|
710
706
|
const args = buildDiffArgs(options);
|
|
711
707
|
if (options.allowFailure) {
|
|
712
|
-
return (await
|
|
708
|
+
return (await git(cwd, args, { env: options.env, readOnly: true, signal: options.signal })).stdout;
|
|
713
709
|
}
|
|
714
710
|
return runText(cwd, args, { env: options.env, readOnly: true, signal: options.signal });
|
|
715
711
|
},
|
|
@@ -741,7 +737,7 @@ export const diff = Object.assign(
|
|
|
741
737
|
if (options.cached) args.push("--cached");
|
|
742
738
|
args.push("--quiet");
|
|
743
739
|
if (options.files?.length) args.push("--", ...options.files);
|
|
744
|
-
const result = await
|
|
740
|
+
const result = await git(cwd, args, { readOnly: true, signal: options.signal });
|
|
745
741
|
if (result.exitCode === 0) return false;
|
|
746
742
|
if (result.exitCode === 1) return true;
|
|
747
743
|
throw new GitCommandError(args, result);
|
|
@@ -757,7 +753,7 @@ export const diff = Object.assign(
|
|
|
757
753
|
if (options.binary) args.push("--binary");
|
|
758
754
|
args.push(base, headRef);
|
|
759
755
|
if (options.allowFailure) {
|
|
760
|
-
return (await
|
|
756
|
+
return (await git(cwd, args, { readOnly: true, signal: options.signal })).stdout;
|
|
761
757
|
}
|
|
762
758
|
return runText(cwd, args, { readOnly: true, signal: options.signal });
|
|
763
759
|
},
|
|
@@ -789,7 +785,7 @@ export const status = Object.assign(
|
|
|
789
785
|
{
|
|
790
786
|
/** Parsed status counts (staged, unstaged, untracked). */
|
|
791
787
|
async summary(cwd: string, signal?: AbortSignal): Promise<GitStatusSummary | null> {
|
|
792
|
-
const result = await
|
|
788
|
+
const result = await git(cwd, ["status", "--porcelain"], { readOnly: true, signal });
|
|
793
789
|
if (result.exitCode !== 0) return null;
|
|
794
790
|
return parseStatusPorcelain(result.stdout);
|
|
795
791
|
},
|
|
@@ -950,7 +946,7 @@ export const branch = {
|
|
|
950
946
|
async current(cwd: string, signal?: AbortSignal): Promise<string | null> {
|
|
951
947
|
const headState = await resolveHead(cwd);
|
|
952
948
|
if (headState?.kind === "ref") return headState.branchName ?? headState.ref;
|
|
953
|
-
const result = await
|
|
949
|
+
const result = await git(cwd, ["symbolic-ref", "--short", "HEAD"], { readOnly: true, signal });
|
|
954
950
|
if (result.exitCode !== 0) return null;
|
|
955
951
|
return result.stdout.trim() || null;
|
|
956
952
|
},
|
|
@@ -966,7 +962,7 @@ export const branch = {
|
|
|
966
962
|
}
|
|
967
963
|
}
|
|
968
964
|
for (const remoteRef of ["origin/HEAD", "upstream/HEAD"]) {
|
|
969
|
-
const result = await
|
|
965
|
+
const result = await git(cwd, ["rev-parse", "--abbrev-ref", remoteRef], { readOnly: true, signal });
|
|
970
966
|
if (result.exitCode !== 0) continue;
|
|
971
967
|
const branchName = stripRemotePrefix(result.stdout.trim());
|
|
972
968
|
if (branchName) return branchName;
|
|
@@ -995,7 +991,7 @@ export const branch = {
|
|
|
995
991
|
name: string,
|
|
996
992
|
options: { force?: boolean; signal?: AbortSignal } = {},
|
|
997
993
|
): Promise<boolean> {
|
|
998
|
-
const result = await
|
|
994
|
+
const result = await git(cwd, ["branch", options.force === false ? "-d" : "-D", name], {
|
|
999
995
|
signal: options.signal,
|
|
1000
996
|
});
|
|
1001
997
|
return result.exitCode === 0;
|
|
@@ -1038,7 +1034,7 @@ export const remote = {
|
|
|
1038
1034
|
* needs to resolve, not paper over.
|
|
1039
1035
|
*/
|
|
1040
1036
|
async add(cwd: string, name: string, url: string, signal?: AbortSignal): Promise<void> {
|
|
1041
|
-
const result = await
|
|
1037
|
+
const result = await git(cwd, ["remote", "add", name, url], { signal });
|
|
1042
1038
|
if (result.exitCode === 0) return;
|
|
1043
1039
|
if (REMOTE_ALREADY_EXISTS.test(result.stderr)) {
|
|
1044
1040
|
const existing = await remote.url(cwd, name, signal);
|
|
@@ -1059,7 +1055,7 @@ export const ref = {
|
|
|
1059
1055
|
if (refName === "HEAD") return (await head.sha(cwd, signal)) !== null;
|
|
1060
1056
|
const repository = await resolveRepository(cwd);
|
|
1061
1057
|
if (repository && refName.startsWith("refs/")) return (await readRef(repository, refName)) !== null;
|
|
1062
|
-
const result = await
|
|
1058
|
+
const result = await git(cwd, ["show-ref", "--verify", "--quiet", refName], { readOnly: true, signal });
|
|
1063
1059
|
return result.exitCode === 0;
|
|
1064
1060
|
},
|
|
1065
1061
|
|
|
@@ -1068,7 +1064,7 @@ export const ref = {
|
|
|
1068
1064
|
if (refName === "HEAD") return head.sha(cwd, signal);
|
|
1069
1065
|
const repository = await resolveRepository(cwd);
|
|
1070
1066
|
if (repository && refName.startsWith("refs/")) return readRef(repository, refName);
|
|
1071
|
-
const result = await
|
|
1067
|
+
const result = await git(cwd, ["rev-parse", refName], { readOnly: true, signal });
|
|
1072
1068
|
if (result.exitCode !== 0) return null;
|
|
1073
1069
|
return result.stdout.trim() || null;
|
|
1074
1070
|
},
|
|
@@ -1150,7 +1146,7 @@ export const worktree = {
|
|
|
1150
1146
|
const args = ["worktree", "remove"];
|
|
1151
1147
|
if (options.force ?? true) args.push("-f");
|
|
1152
1148
|
args.push(worktreePath);
|
|
1153
|
-
const result = await
|
|
1149
|
+
const result = await git(cwd, args, { signal: options.signal });
|
|
1154
1150
|
return result.exitCode === 0;
|
|
1155
1151
|
},
|
|
1156
1152
|
|
|
@@ -1186,7 +1182,7 @@ export const patch = {
|
|
|
1186
1182
|
|
|
1187
1183
|
/** Check if a patch file can be applied cleanly. */
|
|
1188
1184
|
async canApply(cwd: string, patchPath: string, options: Omit<PatchOptions, "check"> = {}): Promise<boolean> {
|
|
1189
|
-
const result = await
|
|
1185
|
+
const result = await git(cwd, buildApplyArgs(patchPath, { ...options, check: true }), {
|
|
1190
1186
|
env: options.env,
|
|
1191
1187
|
readOnly: true,
|
|
1192
1188
|
signal: options.signal,
|
|
@@ -1346,7 +1342,7 @@ export const ls = {
|
|
|
1346
1342
|
|
|
1347
1343
|
/** List submodule paths (recursive). */
|
|
1348
1344
|
async submodules(cwd: string, signal?: AbortSignal): Promise<string[]> {
|
|
1349
|
-
const output = await
|
|
1345
|
+
const output = await git(cwd, ["submodule", "--quiet", "foreach", "--recursive", "echo $sm_path"], {
|
|
1350
1346
|
readOnly: true,
|
|
1351
1347
|
signal,
|
|
1352
1348
|
});
|
|
@@ -1381,14 +1377,14 @@ export const head = {
|
|
|
1381
1377
|
async sha(cwd: string, signal?: AbortSignal): Promise<string | null> {
|
|
1382
1378
|
const headState = await head.resolve(cwd);
|
|
1383
1379
|
if (headState?.commit) return headState.commit;
|
|
1384
|
-
const result = await
|
|
1380
|
+
const result = await git(cwd, ["rev-parse", "HEAD"], { readOnly: true, signal });
|
|
1385
1381
|
if (result.exitCode !== 0) return null;
|
|
1386
1382
|
return result.stdout.trim() || null;
|
|
1387
1383
|
},
|
|
1388
1384
|
|
|
1389
1385
|
/** Abbreviated HEAD commit SHA. */
|
|
1390
1386
|
async short(cwd: string, length = 7, signal?: AbortSignal): Promise<string | null> {
|
|
1391
|
-
const result = await
|
|
1387
|
+
const result = await git(cwd, ["rev-parse", `--short=${length}`, "HEAD"], { readOnly: true, signal });
|
|
1392
1388
|
if (result.exitCode !== 0) return null;
|
|
1393
1389
|
return result.stdout.trim() || null;
|
|
1394
1390
|
},
|
|
@@ -1403,7 +1399,7 @@ export const repo = {
|
|
|
1403
1399
|
async root(cwd: string, signal?: AbortSignal): Promise<string | null> {
|
|
1404
1400
|
const repository = await resolveRepository(cwd);
|
|
1405
1401
|
if (repository) return repository.repoRoot;
|
|
1406
|
-
const result = await
|
|
1402
|
+
const result = await git(cwd, ["rev-parse", "--show-toplevel"], { readOnly: true, signal });
|
|
1407
1403
|
if (result.exitCode !== 0) return null;
|
|
1408
1404
|
return result.stdout.trim() || null;
|
|
1409
1405
|
},
|