diffsplain 0.9.0 → 0.11.0
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/README.md +21 -4
- package/dist/assets/{angular-html-m_m91ZYB.js → angular-html-CjYwnGyV.js} +1 -1
- package/dist/assets/{angular-ts-CO5Z--Rb.js → angular-ts-DSPf2G4O.js} +1 -1
- package/dist/assets/{apl-ogYYgLUu.js → apl-B9ytghJx.js} +1 -1
- package/dist/assets/{astro-DAfY3cNR.js → astro-BsGV0yBF.js} +1 -1
- package/dist/assets/{blade-E6RzsLUV.js → blade-Cc2eCYlt.js} +1 -1
- package/dist/assets/{c-kKdxhtHF.js → c-CWKyCnUx.js} +1 -1
- package/dist/assets/{cobol-3oDDyFNf.js → cobol-DkamOI1E.js} +1 -1
- package/dist/assets/{coffee-dsiILuWe.js → coffee-DyaEXePe.js} +1 -1
- package/dist/assets/{cpp-DYyNFq__.js → cpp-16-d2Eyo.js} +1 -1
- package/dist/assets/{crystal-B7dVp9TF.js → crystal-CavPPRhQ.js} +1 -1
- package/dist/assets/{css-DlxJ0aex.js → css-BME_wHJq.js} +1 -1
- package/dist/assets/{edge-BjEVphkC.js → edge-DeVi2hvK.js} +1 -1
- package/dist/assets/{elixir-0xTOtMcM.js → elixir-CCWgH_vc.js} +1 -1
- package/dist/assets/{elm-iQk_Wrxq.js → elm-CZ570J7x.js} +1 -1
- package/dist/assets/{erb-BO74ztiE.js → erb-CDPT5bea.js} +1 -1
- package/dist/assets/{git-rebase-BN667QRY.js → git-rebase-Dq5Z37DJ.js} +1 -1
- package/dist/assets/{glimmer-js-DTGUIHeS.js → glimmer-js-BoPVZK8p.js} +1 -1
- package/dist/assets/{glimmer-ts-BU3oQn2d.js → glimmer-ts-DazEWfg1.js} +1 -1
- package/dist/assets/{glsl-KLiqLJi2.js → glsl-DCUjYCZY.js} +1 -1
- package/dist/assets/{graphql-D_idcsoJ.js → graphql-DkCeSxy7.js} +1 -1
- package/dist/assets/{hack-BXlsgPIr.js → hack-tiY3l-W-.js} +1 -1
- package/dist/assets/{haml-fP0aUg4-.js → haml-DyQZrJg_.js} +1 -1
- package/dist/assets/{handlebars-C1ZEaot-.js → handlebars-Bvu5Rr0t.js} +1 -1
- package/dist/assets/{html-D04qIuup.js → html-C60VFgKW.js} +1 -1
- package/dist/assets/{html-derivative-C6ai-9QL.js → html-derivative-DB69gpnL.js} +1 -1
- package/dist/assets/{http-C7bz3ZvQ.js → http-DmS8hddd.js} +1 -1
- package/dist/assets/{hurl-jL6B6Ahr.js → hurl-Cdp6lmUx.js} +1 -1
- package/dist/assets/{index-BPaJGi-m.js → index-CUiLG0r9.js} +4 -4
- package/dist/assets/{index-VAyBpb3u.css → index-YnLIxcTm.css} +1 -1
- package/dist/assets/{java-BVzvLCON.js → java-CkOnwoVQ.js} +1 -1
- package/dist/assets/{javascript-UDbZHxCI.js → javascript-D2tF_Ylj.js} +1 -1
- package/dist/assets/{jinja-DB-QKo2A.js → jinja-CdWpLcff.js} +1 -1
- package/dist/assets/{jison-B1ZbR2FF.js → jison-BII58uuR.js} +1 -1
- package/dist/assets/{json-DV4WwJzT.js → json-BKlPRLrU.js} +1 -1
- package/dist/assets/{jsx-DS6_VTvW.js → jsx-Ba5UouxN.js} +1 -1
- package/dist/assets/{julia-C4tevET0.js → julia-CwdQF8Ff.js} +1 -1
- package/dist/assets/{just-CWMjefzB.js → just-Be3sr1-h.js} +1 -1
- package/dist/assets/{latex-tyEFf_Af.js → latex-CDF6t93N.js} +1 -1
- package/dist/assets/{liquid-C3d1X1rL.js → liquid-BE3LVHkw.js} +1 -1
- package/dist/assets/{lua-sXvrzLQY.js → lua-D_W8Ux9S.js} +1 -1
- package/dist/assets/{marko-CQweucif.js → marko-BeUfMD-U.js} +1 -1
- package/dist/assets/{mdc-TrxLWF7s.js → mdc-C8tRJlVU.js} +1 -1
- package/dist/assets/{nginx-ChE2nSNJ.js → nginx-CLPimMM6.js} +1 -1
- package/dist/assets/{nim-BY1CTRfx.js → nim-ikQhXKQf.js} +1 -1
- package/dist/assets/{perl-Cvj2PQQH.js → perl-C27cToFa.js} +1 -1
- package/dist/assets/{php-CGmHp8HW.js → php-Btdg-RP3.js} +1 -1
- package/dist/assets/{pug-DM4S0bRr.js → pug-Bk73ERXL.js} +1 -1
- package/dist/assets/{qml-DI89Al9K.js → qml-DhY7FDIl.js} +1 -1
- package/dist/assets/{r-Bs5_4yYh.js → r-CEwua7nj.js} +1 -1
- package/dist/assets/{razor-DimzayhZ.js → razor-sPO7Sh9d.js} +1 -1
- package/dist/assets/{regexp-BLACz0cx.js → regexp-D9DLFGXE.js} +1 -1
- package/dist/assets/{rst-DcWzxQh2.js → rst-YhauCnId.js} +1 -1
- package/dist/assets/{ruby-Dlat5E8d.js → ruby-DpMkRaoz.js} +1 -1
- package/dist/assets/{sas-CCDmMaBY.js → sas-0KdmKGRO.js} +1 -1
- package/dist/assets/{scss-BPiU2nu6.js → scss-CohvHWvq.js} +1 -1
- package/dist/assets/{shellscript-CKinVNrN.js → shellscript-C93Q8fLt.js} +1 -1
- package/dist/assets/{shellsession-Cea8Pu5d.js → shellsession-YCh2AQXu.js} +1 -1
- package/dist/assets/{soy--BRAPY4-.js → soy-CWVYdXUH.js} +1 -1
- package/dist/assets/{sql-DjKiCyWK.js → sql-CJNtax5I.js} +1 -1
- package/dist/assets/{stata-CDvo3J6y.js → stata-C7v41XAM.js} +1 -1
- package/dist/assets/{surrealql-C7b7E5sh.js → surrealql-CRUSOxDf.js} +1 -1
- package/dist/assets/{svelte-DO0tKtiK.js → svelte-Z0harRr1.js} +1 -1
- package/dist/assets/{templ-DhhmR_rv.js → templ-BplDouJX.js} +1 -1
- package/dist/assets/{tex-Cn_V-Gg4.js → tex-MsB6StN5.js} +1 -1
- package/dist/assets/{ts-tags-B6c-FTmA.js → ts-tags-BWx8aq5t.js} +1 -1
- package/dist/assets/{tsx-Cvtt2sJ-.js → tsx-DBT1gmNl.js} +1 -1
- package/dist/assets/{twig-B7ZbCUmB.js → twig-jz67rR3C.js} +1 -1
- package/dist/assets/{typescript-i3C6um2B.js → typescript-CzwGN36B.js} +1 -1
- package/dist/assets/{vue-html-D362KpK3.js → vue-html-Dhrmu7sO.js} +1 -1
- package/dist/assets/{vue-vine-D34cBbwO.js → vue-vine-BfQGXZs6.js} +1 -1
- package/dist/assets/{vue-DyPX0Vum.js → vue-xsrSiVMC.js} +1 -1
- package/dist/assets/{xml-DJEk7u4n.js → xml-W7cP4qeN.js} +1 -1
- package/dist/assets/{xsl-CoTtf90f.js → xsl-BlXf8qm_.js} +1 -1
- package/dist/assets/{yaml-Cv5epQ4-.js → yaml-BtNwxUT9.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +4 -2
- package/scripts/agent-config.mjs +184 -0
- package/scripts/agent-usage.mjs +141 -0
- package/scripts/build-diff-data.mjs +32 -12
- package/scripts/cli-args.mjs +36 -2
- package/scripts/coding-agents.mjs +265 -13
- package/scripts/generate-summaries.mjs +72 -13
- package/scripts/present.mjs +156 -58
- package/scripts/review-chat-context.mjs +6 -4
- package/scripts/review-chat-controller.mjs +142 -17
- package/scripts/review-chat-provider.mjs +40 -7
- package/scripts/serve-built.mjs +139 -7
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const tokenFields = [
|
|
2
|
+
'inputTokens',
|
|
3
|
+
'outputTokens',
|
|
4
|
+
'cacheReadTokens',
|
|
5
|
+
'cacheWriteTokens',
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
function tokenCount(value) {
|
|
9
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function usageObject(value) {
|
|
13
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
14
|
+
? value
|
|
15
|
+
: undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function usageEntries(value) {
|
|
19
|
+
return tokenFields.reduce((entries, field) => {
|
|
20
|
+
const count = tokenCount(value[field]);
|
|
21
|
+
return count === undefined ? entries : [...entries, [field, count]];
|
|
22
|
+
}, []);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function providerUsage(value) {
|
|
26
|
+
const usage = usageObject(value);
|
|
27
|
+
if (!usage) return undefined;
|
|
28
|
+
const tokens = Object.fromEntries(usageEntries(usage));
|
|
29
|
+
return Object.keys(tokens).length > 0 ? tokens : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function countOrZero(value) {
|
|
33
|
+
return tokenCount(value) || 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function summaryTokens(value) {
|
|
37
|
+
return providerUsage(value) || {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function emptyUsageAccumulator() {
|
|
41
|
+
return { calls: 0, reportedCalls: 0, tokens: {} };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function mergedTokens(left, right) {
|
|
45
|
+
const tokens = { ...left };
|
|
46
|
+
for (const [field, count] of Object.entries(right || {})) {
|
|
47
|
+
tokens[field] = (tokens[field] || 0) + count;
|
|
48
|
+
}
|
|
49
|
+
return tokens;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function recordUsage(accumulator, usage) {
|
|
53
|
+
const reported = providerUsage(usage);
|
|
54
|
+
return {
|
|
55
|
+
calls: accumulator.calls + 1,
|
|
56
|
+
reportedCalls: accumulator.reportedCalls + Number(Boolean(reported)),
|
|
57
|
+
tokens: mergedTokens(accumulator.tokens, reported),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function usageSummary(accumulator) {
|
|
62
|
+
if (accumulator.calls === 0) {
|
|
63
|
+
return {
|
|
64
|
+
status: 'complete',
|
|
65
|
+
calls: 0,
|
|
66
|
+
reportedCalls: 0,
|
|
67
|
+
tokens: { inputTokens: 0, outputTokens: 0 },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (accumulator.reportedCalls === 0) {
|
|
71
|
+
return {
|
|
72
|
+
status: 'unavailable',
|
|
73
|
+
calls: accumulator.calls,
|
|
74
|
+
reportedCalls: 0,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
status: accumulator.reportedCalls === accumulator.calls
|
|
79
|
+
? 'complete'
|
|
80
|
+
: 'partial',
|
|
81
|
+
calls: accumulator.calls,
|
|
82
|
+
reportedCalls: accumulator.reportedCalls,
|
|
83
|
+
tokens: { ...accumulator.tokens },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function accumulatorFromSummary(summary) {
|
|
88
|
+
const usage = usageObject(summary);
|
|
89
|
+
if (!usage) return emptyUsageAccumulator();
|
|
90
|
+
return {
|
|
91
|
+
calls: countOrZero(usage.calls),
|
|
92
|
+
reportedCalls: countOrZero(usage.reportedCalls),
|
|
93
|
+
tokens: summaryTokens(usage.tokens),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function combineUsage(left, right) {
|
|
98
|
+
const first = accumulatorFromSummary(left);
|
|
99
|
+
const second = accumulatorFromSummary(right);
|
|
100
|
+
return usageSummary({
|
|
101
|
+
calls: first.calls + second.calls,
|
|
102
|
+
reportedCalls: first.reportedCalls + second.reportedCalls,
|
|
103
|
+
tokens: mergedTokens(first.tokens, second.tokens),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function reviewUsage(agentNotes, reviewChat) {
|
|
108
|
+
return {
|
|
109
|
+
agentNotes,
|
|
110
|
+
reviewChat,
|
|
111
|
+
combined: combineUsage(agentNotes, reviewChat),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const terminalLabels = {
|
|
116
|
+
inputTokens: 'input',
|
|
117
|
+
outputTokens: 'output',
|
|
118
|
+
cacheReadTokens: 'cache read',
|
|
119
|
+
cacheWriteTokens: 'cache write',
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
function formatUsageTokens(tokens) {
|
|
123
|
+
return Object.entries(tokens || {})
|
|
124
|
+
.map(([field, count]) => `${terminalLabels[field]} ${count.toLocaleString('en-US')}`)
|
|
125
|
+
.join(', ');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function formatUsage(label, summary) {
|
|
129
|
+
if (summary.status === 'unavailable') return `${label}: Unavailable`;
|
|
130
|
+
const totals = formatUsageTokens(summary.tokens);
|
|
131
|
+
const prefix = summary.status === 'partial' ? 'Partial, ' : '';
|
|
132
|
+
return `${label}: ${prefix}${totals || '0 tokens'}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function formatReviewUsage(usage) {
|
|
136
|
+
return [
|
|
137
|
+
formatUsage('Agent note usage', usage.agentNotes),
|
|
138
|
+
formatUsage('Review chat usage', usage.reviewChat),
|
|
139
|
+
formatUsage('Combined agent usage', usage.combined),
|
|
140
|
+
].join('\n');
|
|
141
|
+
}
|
|
@@ -32,6 +32,12 @@ import {
|
|
|
32
32
|
createAgentReviewFingerprint,
|
|
33
33
|
} from './agent-review.mjs';
|
|
34
34
|
import { summaryPath } from './summary-path.mjs';
|
|
35
|
+
import {
|
|
36
|
+
combineUsage,
|
|
37
|
+
emptyUsageAccumulator,
|
|
38
|
+
reviewUsage,
|
|
39
|
+
usageSummary,
|
|
40
|
+
} from './agent-usage.mjs';
|
|
35
41
|
|
|
36
42
|
const args = process.argv.slice(2);
|
|
37
43
|
const fail = (message) => {
|
|
@@ -1338,10 +1344,18 @@ function build() {
|
|
|
1338
1344
|
...(typeof summaryDoc.meta?.reasoning === 'string'
|
|
1339
1345
|
? { reasoning: summaryDoc.meta.reasoning }
|
|
1340
1346
|
: {}),
|
|
1347
|
+
fast: summaryDoc.meta?.fast === true,
|
|
1341
1348
|
...(typeof summaryDoc.meta?.accessMode === 'string'
|
|
1342
1349
|
? { accessMode: summaryDoc.meta.accessMode }
|
|
1343
1350
|
: {}),
|
|
1344
1351
|
},
|
|
1352
|
+
usage: reviewUsage(
|
|
1353
|
+
combineUsage(
|
|
1354
|
+
sourceSummaries.meta?.usage,
|
|
1355
|
+
usageSummary(emptyUsageAccumulator()),
|
|
1356
|
+
),
|
|
1357
|
+
usageSummary(emptyUsageAccumulator()),
|
|
1358
|
+
),
|
|
1345
1359
|
};
|
|
1346
1360
|
const version = createHash('sha256')
|
|
1347
1361
|
.update(JSON.stringify(content))
|
|
@@ -1465,23 +1479,29 @@ const refresh = () => {
|
|
|
1465
1479
|
}
|
|
1466
1480
|
};
|
|
1467
1481
|
|
|
1468
|
-
|
|
1482
|
+
let watcher;
|
|
1483
|
+
const stopWatching = (error) => {
|
|
1484
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1485
|
+
process.exitCode = 1;
|
|
1486
|
+
if (watcher) clearInterval(watcher);
|
|
1487
|
+
};
|
|
1488
|
+
let initialFingerprint;
|
|
1489
|
+
let startupFailed = false;
|
|
1490
|
+
if (watching) {
|
|
1491
|
+
try {
|
|
1492
|
+
initialFingerprint = fingerprint();
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
startupFailed = true;
|
|
1495
|
+
stopWatching(error);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
const started = !startupFailed && refresh();
|
|
1469
1499
|
if (watching && started) {
|
|
1470
|
-
let last;
|
|
1500
|
+
let last = initialFingerprint;
|
|
1471
1501
|
let remoteWait = 0;
|
|
1472
|
-
let watcher;
|
|
1473
|
-
const stopWatching = (error) => {
|
|
1474
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
1475
|
-
process.exitCode = 1;
|
|
1476
|
-
if (watcher) clearInterval(watcher);
|
|
1477
|
-
};
|
|
1478
1502
|
const poll = () => {
|
|
1479
1503
|
try {
|
|
1480
1504
|
const next = fingerprint();
|
|
1481
|
-
if (last === undefined) {
|
|
1482
|
-
last = next;
|
|
1483
|
-
return true;
|
|
1484
|
-
}
|
|
1485
1505
|
remoteWait += watchInterval;
|
|
1486
1506
|
const remoteDue = remoteMode && remoteWait >= remoteRefreshInterval;
|
|
1487
1507
|
if (next !== last || remoteDue || watchContent) {
|
package/scripts/cli-args.mjs
CHANGED
|
@@ -42,6 +42,7 @@ export const cliOptions = defineCliOptions({
|
|
|
42
42
|
'--no-agent': { kind: 'no-agent' },
|
|
43
43
|
'--no-checkout-access': { kind: 'flag' },
|
|
44
44
|
'--force': { kind: 'flag' },
|
|
45
|
+
'--fast': { kind: 'flag' },
|
|
45
46
|
'--worktree': { kind: 'flag' },
|
|
46
47
|
'--no-browser': { kind: 'flag' },
|
|
47
48
|
'--support-record': { kind: 'flag' },
|
|
@@ -82,6 +83,8 @@ Commands:
|
|
|
82
83
|
doctor [--json] [--deep]
|
|
83
84
|
Check review, agent, and pull request capabilities
|
|
84
85
|
cache Show or prune saved agent notes
|
|
86
|
+
config agent [NAME|--unset]
|
|
87
|
+
Show, set, or unset the default coding agent
|
|
85
88
|
|
|
86
89
|
Targets:
|
|
87
90
|
--branch NAME Show a remote branch against its default branch
|
|
@@ -100,6 +103,7 @@ Options:
|
|
|
100
103
|
--exclude PATTERN Omit matching files from automatic agent context
|
|
101
104
|
--model NAME Model for agent notes
|
|
102
105
|
--reasoning LEVEL Agent reasoning effort when supported
|
|
106
|
+
--fast Enable provider Fast mode for agent notes and chat
|
|
103
107
|
--batch-size COUNT Maximum files per agent pass (default: ${batchSizeOption.default})
|
|
104
108
|
--jobs COUNT Agent passes to run at once (default: ${jobsOption.default})
|
|
105
109
|
--force Regenerate all agent notes
|
|
@@ -118,8 +122,8 @@ Options:
|
|
|
118
122
|
-v, --version Show the installed version
|
|
119
123
|
|
|
120
124
|
Agent choice:
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
Choice order is --no-agent, --agent, then the configured default.
|
|
126
|
+
Without any choice, an interactive terminal lists usable agents.
|
|
123
127
|
|
|
124
128
|
Cursor:
|
|
125
129
|
Requires Cursor Agent 2026.08.11 or newer. Uses the signed-in CLI in the
|
|
@@ -195,6 +199,31 @@ export function parseCliArgs(
|
|
|
195
199
|
pathExists = existsSync,
|
|
196
200
|
} = {},
|
|
197
201
|
) {
|
|
202
|
+
if (rawArgs[0] === 'config') {
|
|
203
|
+
if (rawArgs[1] !== 'agent') {
|
|
204
|
+
fail('Use: diffsplain config agent [NAME|--unset]');
|
|
205
|
+
}
|
|
206
|
+
if (rawArgs.length === 2) {
|
|
207
|
+
return { config: { kind: 'show' } };
|
|
208
|
+
}
|
|
209
|
+
if (rawArgs.length !== 3) {
|
|
210
|
+
fail('Use: diffsplain config agent [NAME|--unset]');
|
|
211
|
+
}
|
|
212
|
+
const value = rawArgs[2];
|
|
213
|
+
if (value === '--unset') {
|
|
214
|
+
return { config: { kind: 'unset' } };
|
|
215
|
+
}
|
|
216
|
+
if (value.startsWith('--')) {
|
|
217
|
+
fail('Use: diffsplain config agent [NAME|--unset]');
|
|
218
|
+
}
|
|
219
|
+
if (!codingAgents.includes(value)) {
|
|
220
|
+
fail(
|
|
221
|
+
`Unsupported agent "${value}". Choose ${enabledCodingAgents.join(', ')}.`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return { config: { kind: 'set', agent: value } };
|
|
225
|
+
}
|
|
226
|
+
|
|
198
227
|
if (rawArgs[0] === 'doctor') {
|
|
199
228
|
const options = new Set(rawArgs.slice(1));
|
|
200
229
|
for (const option of options) {
|
|
@@ -284,6 +313,9 @@ export function parseCliArgs(
|
|
|
284
313
|
fail('Pass the repo once, either as REPO or with --repo');
|
|
285
314
|
}
|
|
286
315
|
if (noAgent && agentSet) fail('--agent and --no-agent cannot be used together');
|
|
316
|
+
if (noAgent && options.has('--fast')) {
|
|
317
|
+
fail('--fast and --no-agent cannot be used together');
|
|
318
|
+
}
|
|
287
319
|
if (noAgent && options.has('--summaries')) {
|
|
288
320
|
fail('--no-agent cannot be used with --summaries');
|
|
289
321
|
}
|
|
@@ -390,6 +422,7 @@ export function parseCliArgs(
|
|
|
390
422
|
agentArgs.push(`--exclude=${pattern}`);
|
|
391
423
|
}
|
|
392
424
|
if (options.has('--force')) agentArgs.push('--force');
|
|
425
|
+
if (options.has('--fast')) agentArgs.push('--fast');
|
|
393
426
|
for (const name of [
|
|
394
427
|
'--codex-bin',
|
|
395
428
|
'--model',
|
|
@@ -499,5 +532,6 @@ export function parseCliArgs(
|
|
|
499
532
|
host,
|
|
500
533
|
browserEnabled: !options.has('--no-browser'),
|
|
501
534
|
forceSummaryRegeneration: options.has('--force'),
|
|
535
|
+
fast: options.has('--fast'),
|
|
502
536
|
};
|
|
503
537
|
}
|
|
@@ -8,13 +8,29 @@ import {
|
|
|
8
8
|
isAbsolute,
|
|
9
9
|
join,
|
|
10
10
|
} from 'node:path';
|
|
11
|
+
import { readConfiguredAgent } from './agent-config.mjs';
|
|
11
12
|
|
|
12
13
|
export const codingAgentCapabilities = {
|
|
13
|
-
codex: {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
codex: {
|
|
15
|
+
binary: 'codex',
|
|
16
|
+
model: true,
|
|
17
|
+
reasoning: true,
|
|
18
|
+
fast: { label: 'Codex CLI', minimumVersion: '0.108.0' },
|
|
19
|
+
},
|
|
20
|
+
claude: {
|
|
21
|
+
binary: 'claude',
|
|
22
|
+
model: true,
|
|
23
|
+
reasoning: false,
|
|
24
|
+
fast: { label: 'Claude Code', minimumVersion: '2.1.36' },
|
|
25
|
+
},
|
|
26
|
+
copilot: { binary: 'copilot', model: true, reasoning: false, fast: false },
|
|
27
|
+
cursor: {
|
|
28
|
+
binary: 'cursor-agent',
|
|
29
|
+
model: true,
|
|
30
|
+
reasoning: false,
|
|
31
|
+
fast: false,
|
|
32
|
+
},
|
|
33
|
+
opencode: { binary: 'opencode', model: true, reasoning: true, fast: false },
|
|
18
34
|
};
|
|
19
35
|
|
|
20
36
|
export const codingAgents = Object.keys(codingAgentCapabilities);
|
|
@@ -23,6 +39,10 @@ export function agentSupportsReasoning(agent) {
|
|
|
23
39
|
return codingAgentCapabilities[agent]?.reasoning === true;
|
|
24
40
|
}
|
|
25
41
|
|
|
42
|
+
export function agentSupportsFast(agent) {
|
|
43
|
+
return Boolean(codingAgentCapabilities[agent]?.fast);
|
|
44
|
+
}
|
|
45
|
+
|
|
26
46
|
export function assertReasoningSupported(agent, reasoning) {
|
|
27
47
|
if (reasoning && !agentSupportsReasoning(agent)) {
|
|
28
48
|
throw new Error(
|
|
@@ -88,6 +108,67 @@ function versionAtLeast(current, minimum) {
|
|
|
88
108
|
return true;
|
|
89
109
|
}
|
|
90
110
|
|
|
111
|
+
function semanticVersion(value) {
|
|
112
|
+
const match = value?.match(/(?:^|\D)(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?/);
|
|
113
|
+
if (!match) return undefined;
|
|
114
|
+
return {
|
|
115
|
+
parts: match.slice(1, 4).map(Number),
|
|
116
|
+
prerelease: Boolean(match[4]),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function inspectFastCompatibility(
|
|
121
|
+
agent,
|
|
122
|
+
command,
|
|
123
|
+
{
|
|
124
|
+
env = process.env,
|
|
125
|
+
timeout = 5_000,
|
|
126
|
+
run = spawnSync,
|
|
127
|
+
} = {},
|
|
128
|
+
) {
|
|
129
|
+
const capability = codingAgentCapabilities[agent]?.fast;
|
|
130
|
+
if (!capability) {
|
|
131
|
+
return {
|
|
132
|
+
compatible: false,
|
|
133
|
+
reason: `--fast is supported only by codex and claude; ${agent} does not support it.`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const result = run(command, ['--version'], {
|
|
137
|
+
encoding: 'utf8',
|
|
138
|
+
env,
|
|
139
|
+
timeout,
|
|
140
|
+
windowsHide: true,
|
|
141
|
+
});
|
|
142
|
+
const version = firstLine(`${result.stdout || ''}\n${result.stderr || ''}`);
|
|
143
|
+
const current = semanticVersion(version);
|
|
144
|
+
const minimum = semanticVersion(capability.minimumVersion);
|
|
145
|
+
const compatible = current && minimum &&
|
|
146
|
+
(versionAtLeast(current.parts, minimum.parts) &&
|
|
147
|
+
!(current.prerelease &&
|
|
148
|
+
current.parts.every((part, index) => part === minimum.parts[index])));
|
|
149
|
+
if (result.error || result.status !== 0 || !version || !current) {
|
|
150
|
+
return {
|
|
151
|
+
compatible: false,
|
|
152
|
+
version,
|
|
153
|
+
reason: `${capability.label} ${capability.minimumVersion} or newer is required for --fast, but its version could not be checked. Upgrade ${capability.label}.`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (!compatible) {
|
|
157
|
+
return {
|
|
158
|
+
compatible: false,
|
|
159
|
+
version,
|
|
160
|
+
reason: `${capability.label} ${capability.minimumVersion} or newer is required for --fast; found ${version}. Upgrade ${capability.label}.`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return { compatible: true, version };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function assertFastCompatible(agent, command, fast, options) {
|
|
167
|
+
if (!fast) return;
|
|
168
|
+
const inspection = inspectFastCompatibility(agent, command, options);
|
|
169
|
+
if (!inspection.compatible) throw new Error(inspection.reason);
|
|
170
|
+
}
|
|
171
|
+
|
|
91
172
|
function cursorCompatibilityError(detail) {
|
|
92
173
|
return `Cursor Agent is incompatible: ${detail} ${cursorRequirementSummary} Upgrade Cursor Agent.`;
|
|
93
174
|
}
|
|
@@ -263,15 +344,57 @@ function nonInteractiveSelectionError() {
|
|
|
263
344
|
);
|
|
264
345
|
}
|
|
265
346
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
347
|
+
class CodingAgentSelectionError extends Error {
|
|
348
|
+
constructor(agent, message) {
|
|
349
|
+
super(message);
|
|
350
|
+
this.agent = agent;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function codingAgentFromSelectionError(error) {
|
|
355
|
+
return error instanceof CodingAgentSelectionError
|
|
356
|
+
? error.agent
|
|
357
|
+
: undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function unsupportedSelectionError(requested, configured) {
|
|
361
|
+
if (configured) {
|
|
362
|
+
return new CodingAgentSelectionError(
|
|
363
|
+
requested,
|
|
364
|
+
`Configured coding agent "${requested}" is unsupported. Choose ${enabledCodingAgents.join(', ')} with diffsplain config agent NAME, or use --agent NAME for this run.`,
|
|
270
365
|
);
|
|
271
366
|
}
|
|
367
|
+
return new CodingAgentSelectionError(
|
|
368
|
+
requested,
|
|
369
|
+
`Unsupported agent "${requested}". Choose ${enabledCodingAgents.join(', ')}.`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function unavailableSelectionError(requested, result, configured) {
|
|
374
|
+
if (!configured) {
|
|
375
|
+
return new CodingAgentSelectionError(
|
|
376
|
+
requested,
|
|
377
|
+
unavailableAgentError(requested, result).message,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const reason = typeof result === 'object' ? result.reason : undefined;
|
|
381
|
+
return new CodingAgentSelectionError(
|
|
382
|
+
requested,
|
|
383
|
+
`Configured coding agent "${requested}" is not available.${reason ? ` ${reason}` : ''} Change it with diffsplain config agent NAME, or use --agent NAME for this run.`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function selectRequestedCodingAgent(
|
|
388
|
+
requested,
|
|
389
|
+
available,
|
|
390
|
+
{ configured = false } = {},
|
|
391
|
+
) {
|
|
392
|
+
if (!codingAgents.includes(requested)) {
|
|
393
|
+
throw unsupportedSelectionError(requested, configured);
|
|
394
|
+
}
|
|
272
395
|
const result = await available(requested);
|
|
273
396
|
if (!availableResult(result)) {
|
|
274
|
-
throw
|
|
397
|
+
throw unavailableSelectionError(requested, result, configured);
|
|
275
398
|
}
|
|
276
399
|
return requested;
|
|
277
400
|
}
|
|
@@ -340,15 +463,23 @@ export async function selectCodingAgent(
|
|
|
340
463
|
requested,
|
|
341
464
|
{
|
|
342
465
|
available = commandAvailable,
|
|
466
|
+
configuredAgent = readConfiguredAgent,
|
|
343
467
|
input = process.stdin,
|
|
344
468
|
output = process.stderr,
|
|
345
469
|
signal,
|
|
346
470
|
} = {},
|
|
347
471
|
) {
|
|
348
|
-
if (requested) {
|
|
472
|
+
if (requested !== undefined) {
|
|
349
473
|
return selectRequestedCodingAgent(requested, available);
|
|
350
474
|
}
|
|
351
475
|
|
|
476
|
+
const configured = await configuredAgent();
|
|
477
|
+
if (configured !== undefined) {
|
|
478
|
+
return selectRequestedCodingAgent(configured, available, {
|
|
479
|
+
configured: true,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
352
483
|
if (!input?.isTTY || !output?.isTTY) {
|
|
353
484
|
throw nonInteractiveSelectionError();
|
|
354
485
|
}
|
|
@@ -412,6 +543,27 @@ function parseClaudeResponse(stdout) {
|
|
|
412
543
|
return envelope;
|
|
413
544
|
}
|
|
414
545
|
|
|
546
|
+
function jsonLines(stdout) {
|
|
547
|
+
const lines = stdout.trim().split('\n').filter(Boolean);
|
|
548
|
+
if (!lines.length) return undefined;
|
|
549
|
+
const events = lines.map(parseEvent);
|
|
550
|
+
return events.every(Boolean) ? events : undefined;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function parseCodexResponse(stdout) {
|
|
554
|
+
const events = jsonLines(stdout);
|
|
555
|
+
const message = events && [...events]
|
|
556
|
+
.reverse()
|
|
557
|
+
.find((event) =>
|
|
558
|
+
event.type === 'item.completed' &&
|
|
559
|
+
event.item?.type === 'agent_message' &&
|
|
560
|
+
typeof event.item.text === 'string',
|
|
561
|
+
);
|
|
562
|
+
return message
|
|
563
|
+
? parseJsonText(message.item.text, 'Codex')
|
|
564
|
+
: parseJsonText(stdout, 'Codex');
|
|
565
|
+
}
|
|
566
|
+
|
|
415
567
|
function parseEvent(line) {
|
|
416
568
|
try {
|
|
417
569
|
return JSON.parse(line);
|
|
@@ -470,8 +622,94 @@ export function parseAgentResponse(agent, stdout) {
|
|
|
470
622
|
if (agent === 'claude') return parseClaudeResponse(stdout);
|
|
471
623
|
if (agent === 'opencode') return parseOpenCodeResponse(stdout);
|
|
472
624
|
if (agent === 'cursor') return parseCursorResponse(stdout);
|
|
473
|
-
|
|
474
|
-
return parseJsonText(stdout,
|
|
625
|
+
if (agent === 'codex') return parseCodexResponse(stdout);
|
|
626
|
+
return parseJsonText(stdout, 'Copilot');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function addUsageField(totals, [field, count]) {
|
|
630
|
+
if (!Number.isSafeInteger(count) || count < 0) return;
|
|
631
|
+
totals[field] = (totals[field] || 0) + count;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function addProviderUsage(totals, usage) {
|
|
635
|
+
Object.entries(usage).forEach((entry) => addUsageField(totals, entry));
|
|
636
|
+
return totals;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function sumProviderUsage(usages) {
|
|
640
|
+
const totals = usages.reduce(addProviderUsage, {});
|
|
641
|
+
return Object.keys(totals).length ? totals : undefined;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function providerUsage(inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens) {
|
|
645
|
+
return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function codexEventUsage(event) {
|
|
649
|
+
if (event.type !== 'turn.completed' || !event.usage) return undefined;
|
|
650
|
+
const usage = event.usage;
|
|
651
|
+
return providerUsage(
|
|
652
|
+
usage.input_tokens,
|
|
653
|
+
usage.output_tokens,
|
|
654
|
+
usage.cached_input_tokens,
|
|
655
|
+
usage.cache_write_input_tokens,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function eventPartTokens(event, type) {
|
|
660
|
+
if (event.type !== type) return undefined;
|
|
661
|
+
return event.part?.tokens;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function openCodeEventUsage(event) {
|
|
665
|
+
const tokens = eventPartTokens(event, 'step_finish');
|
|
666
|
+
if (!tokens) return undefined;
|
|
667
|
+
const cache = tokens.cache || {};
|
|
668
|
+
return providerUsage(
|
|
669
|
+
tokens.input,
|
|
670
|
+
tokens.output,
|
|
671
|
+
cache.read,
|
|
672
|
+
cache.write,
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function eventUsage(stdout, extractUsage) {
|
|
677
|
+
const events = jsonLines(stdout) || [];
|
|
678
|
+
return sumProviderUsage(events.map(extractUsage).filter(Boolean));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function codexUsage(stdout) {
|
|
682
|
+
return eventUsage(stdout, codexEventUsage);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function safeProviderEnvelope(stdout, label) {
|
|
686
|
+
try {
|
|
687
|
+
return parseJsonText(stdout, label);
|
|
688
|
+
} catch {
|
|
689
|
+
return undefined;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function claudeUsage(stdout) {
|
|
694
|
+
const usage = safeProviderEnvelope(stdout, 'Claude')?.usage;
|
|
695
|
+
if (!usage || typeof usage !== 'object') return undefined;
|
|
696
|
+
return sumProviderUsage([providerUsage(
|
|
697
|
+
usage.input_tokens,
|
|
698
|
+
usage.output_tokens,
|
|
699
|
+
usage.cache_read_input_tokens,
|
|
700
|
+
usage.cache_creation_input_tokens,
|
|
701
|
+
)]);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function openCodeUsage(stdout) {
|
|
705
|
+
return eventUsage(stdout, openCodeEventUsage);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export function parseAgentUsage(agent, stdout) {
|
|
709
|
+
if (agent === 'codex') return codexUsage(stdout);
|
|
710
|
+
if (agent === 'claude') return claudeUsage(stdout);
|
|
711
|
+
if (agent === 'opencode') return openCodeUsage(stdout);
|
|
712
|
+
return undefined;
|
|
475
713
|
}
|
|
476
714
|
|
|
477
715
|
function codexIsolationArgs(snapshotOnly) {
|
|
@@ -503,6 +741,7 @@ function codexRepoArgs(snapshotOnly) {
|
|
|
503
741
|
|
|
504
742
|
function codexCommand({
|
|
505
743
|
binary,
|
|
744
|
+
fast,
|
|
506
745
|
model,
|
|
507
746
|
reasoning,
|
|
508
747
|
prompt,
|
|
@@ -514,6 +753,7 @@ function codexCommand({
|
|
|
514
753
|
const args = [
|
|
515
754
|
'exec',
|
|
516
755
|
'--ephemeral',
|
|
756
|
+
'--json',
|
|
517
757
|
'--sandbox',
|
|
518
758
|
'read-only',
|
|
519
759
|
...codexIsolationArgs(snapshotOnly),
|
|
@@ -526,6 +766,14 @@ function codexCommand({
|
|
|
526
766
|
schemaPath,
|
|
527
767
|
...codexConfigArgs(snapshotOnly),
|
|
528
768
|
];
|
|
769
|
+
if (fast) {
|
|
770
|
+
args.push(
|
|
771
|
+
'--config',
|
|
772
|
+
'service_tier="fast"',
|
|
773
|
+
'--config',
|
|
774
|
+
'features.fast_mode=true',
|
|
775
|
+
);
|
|
776
|
+
}
|
|
529
777
|
if (model) args.push('--model', model);
|
|
530
778
|
if (reasoning) {
|
|
531
779
|
args.push(
|
|
@@ -545,6 +793,7 @@ function codexCommand({
|
|
|
545
793
|
|
|
546
794
|
function claudeCommand({
|
|
547
795
|
binary,
|
|
796
|
+
fast,
|
|
548
797
|
model,
|
|
549
798
|
prompt,
|
|
550
799
|
schema,
|
|
@@ -561,6 +810,7 @@ function claudeCommand({
|
|
|
561
810
|
...(snapshotOnly ? ['--tools', ''] : ['--permission-mode', 'plan']),
|
|
562
811
|
'--no-session-persistence',
|
|
563
812
|
];
|
|
813
|
+
if (fast) args.push('--settings', JSON.stringify({ fastMode: true }));
|
|
564
814
|
if (model) args.push('--model', model);
|
|
565
815
|
args.push(prompt);
|
|
566
816
|
return {
|
|
@@ -713,6 +963,7 @@ export function agentCommand({
|
|
|
713
963
|
binary = agent,
|
|
714
964
|
model,
|
|
715
965
|
reasoning,
|
|
966
|
+
fast = false,
|
|
716
967
|
prompt,
|
|
717
968
|
schema,
|
|
718
969
|
schemaPath,
|
|
@@ -724,6 +975,7 @@ export function agentCommand({
|
|
|
724
975
|
const checkoutAccess = accessMode?.mode === 'checkout-read-only';
|
|
725
976
|
const options = {
|
|
726
977
|
binary,
|
|
978
|
+
fast,
|
|
727
979
|
inputPath,
|
|
728
980
|
model,
|
|
729
981
|
prompt,
|