myshell-tools 2.3.0 → 2.6.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/CHANGELOG.md +20 -0
- package/README.md +26 -10
- package/dist/cli.js +43 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/cost.js +4 -1
- package/dist/commands/cost.js.map +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/install.d.ts +66 -0
- package/dist/commands/install.js +174 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/login.d.ts +41 -2
- package/dist/commands/login.js +116 -11
- package/dist/commands/login.js.map +1 -1
- package/dist/core/assess.js +2 -62
- package/dist/core/assess.js.map +1 -1
- package/dist/core/budget.d.ts +26 -0
- package/dist/core/budget.js +37 -0
- package/dist/core/budget.js.map +1 -0
- package/dist/core/history.d.ts +35 -0
- package/dist/core/history.js +116 -0
- package/dist/core/history.js.map +1 -0
- package/dist/core/json-envelope.d.ts +49 -0
- package/dist/core/json-envelope.js +117 -0
- package/dist/core/json-envelope.js.map +1 -0
- package/dist/core/orchestrate.js +107 -8
- package/dist/core/orchestrate.js.map +1 -1
- package/dist/core/policy.js +17 -9
- package/dist/core/policy.js.map +1 -1
- package/dist/core/prompt.d.ts +9 -4
- package/dist/core/prompt.js +14 -5
- package/dist/core/prompt.js.map +1 -1
- package/dist/core/review.js +2 -49
- package/dist/core/review.js.map +1 -1
- package/dist/core/route.d.ts +13 -5
- package/dist/core/route.js +20 -6
- package/dist/core/route.js.map +1 -1
- package/dist/core/types.d.ts +37 -0
- package/dist/infra/pricing.d.ts +17 -4
- package/dist/infra/pricing.js +73 -3
- package/dist/infra/pricing.js.map +1 -1
- package/dist/interface/menu.d.ts +12 -0
- package/dist/interface/menu.js +110 -25
- package/dist/interface/menu.js.map +1 -1
- package/dist/providers/detect.d.ts +17 -5
- package/dist/providers/detect.js +56 -4
- package/dist/providers/detect.js.map +1 -1
- package/dist/providers/install.js +1 -0
- package/dist/providers/install.js.map +1 -1
- package/dist/providers/opencode-parse.d.ts +49 -0
- package/dist/providers/opencode-parse.js +181 -0
- package/dist/providers/opencode-parse.js.map +1 -0
- package/dist/providers/opencode.d.ts +43 -0
- package/dist/providers/opencode.js +121 -0
- package/dist/providers/opencode.js.map +1 -0
- package/dist/providers/port.d.ts +1 -1
- package/dist/providers/registry.d.ts +2 -2
- package/dist/providers/registry.js +6 -2
- package/dist/providers/registry.js.map +1 -1
- package/package.json +2 -2
package/dist/commands/login.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* src/commands/login.ts — `myshell-tools login [claude|codex]`.
|
|
2
|
+
* src/commands/login.ts — `myshell-tools login [claude|codex] [--code|--browser]`.
|
|
3
3
|
*
|
|
4
4
|
* Frictionless authentication: rather than make the user remember each vendor's
|
|
5
5
|
* CLI auth command, we delegate to the provider's OWN OAuth flow and inherit the
|
|
6
6
|
* terminal so the browser/device sign-in works in place.
|
|
7
7
|
*
|
|
8
|
+
* Two sign-in methods:
|
|
9
|
+
* - 'browser': the provider's default flow, which spins up a localhost
|
|
10
|
+
* callback server and opens a browser. Great on a laptop; FAILS inside
|
|
11
|
+
* containers / over SSH (Replit, Codespaces, etc.) where localhost can't be
|
|
12
|
+
* reached from the user's browser.
|
|
13
|
+
* - 'code': a no-localhost flow that works anywhere.
|
|
14
|
+
* · claude → `claude setup-token`: prints a link; the user signs in at
|
|
15
|
+
* claude.ai, copies the authorization code, and pastes it back here.
|
|
16
|
+
* · codex → `codex login --device-auth`: prints a URL + one-time code;
|
|
17
|
+
* the user authorizes their ChatGPT account on any device.
|
|
18
|
+
*
|
|
19
|
+
* When no method is forced, we auto-detect: headless/remote environments default
|
|
20
|
+
* to 'code' (so the localhost trap is avoided), everything else to 'browser'.
|
|
21
|
+
*
|
|
8
22
|
* Security: myshell-tools never sees, handles, or stores raw credentials. Each
|
|
9
23
|
* CLI manages its own tokens; we only trigger its login. (This is what keeps the
|
|
10
24
|
* "use your subscription, no API keys" model honest.)
|
|
@@ -12,47 +26,138 @@
|
|
|
12
26
|
import { execa } from 'execa';
|
|
13
27
|
import { detectProvider, getInstallCommand } from '../providers/detect.js';
|
|
14
28
|
import { bold, dim, green, red } from '../ui/theme.js';
|
|
15
|
-
/** Each provider's
|
|
29
|
+
/** Each provider's default (browser/localhost) sign-in command. */
|
|
16
30
|
const LOGIN_COMMAND = {
|
|
17
31
|
claude: { bin: 'claude', args: ['auth', 'login'] },
|
|
18
32
|
codex: { bin: 'codex', args: ['login'] },
|
|
33
|
+
// opencode ships free models — no credentials required. `opencode auth login -p <provider>`
|
|
34
|
+
// is only needed for premium providers. We default to no-op by pointing at `auth list`
|
|
35
|
+
// so the user sees configured credentials without being forced through a login flow.
|
|
36
|
+
opencode: { bin: 'opencode', args: ['auth', 'list'] },
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Each provider's no-localhost ("code") sign-in command, plus the human steps
|
|
40
|
+
* we print before handing over the terminal so the user knows what to expect.
|
|
41
|
+
*/
|
|
42
|
+
const LOGIN_CODE_COMMAND = {
|
|
43
|
+
claude: {
|
|
44
|
+
bin: 'claude',
|
|
45
|
+
args: ['setup-token'],
|
|
46
|
+
guidance: 'A sign-in link will appear below.\n' +
|
|
47
|
+
' 1. Open it in any browser and sign in at claude.ai.\n' +
|
|
48
|
+
' 2. Copy the authorization code it shows you.\n' +
|
|
49
|
+
' 3. Paste the code back here at the prompt and press Enter.',
|
|
50
|
+
},
|
|
51
|
+
codex: {
|
|
52
|
+
bin: 'codex',
|
|
53
|
+
args: ['login', '--device-auth'],
|
|
54
|
+
guidance: 'A URL and a one-time code will appear below.\n' +
|
|
55
|
+
' 1. On any device, open the URL.\n' +
|
|
56
|
+
' 2. Enter the code shown and authorize your ChatGPT account.\n' +
|
|
57
|
+
' 3. Sign-in completes here automatically once authorized.',
|
|
58
|
+
},
|
|
59
|
+
opencode: {
|
|
60
|
+
bin: 'opencode',
|
|
61
|
+
args: ['auth', 'list'],
|
|
62
|
+
guidance: 'opencode ships free models with no credentials required.\n' +
|
|
63
|
+
' To add a premium provider, run:\n' +
|
|
64
|
+
' opencode auth login -p <provider> -m <method>\n' +
|
|
65
|
+
' e.g. opencode auth login -p anthropic -m apikey',
|
|
66
|
+
},
|
|
19
67
|
};
|
|
20
68
|
export function isProviderId(value) {
|
|
21
|
-
return value === 'claude' || value === 'codex';
|
|
69
|
+
return value === 'claude' || value === 'codex' || value === 'opencode';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Decide whether the current environment can actually reach a localhost OAuth
|
|
73
|
+
* callback / open a browser. Pure (env + platform in, boolean out) so it is
|
|
74
|
+
* hermetically testable.
|
|
75
|
+
*
|
|
76
|
+
* Returns true for environments where the browser/localhost flow typically
|
|
77
|
+
* fails and the code method should be preferred:
|
|
78
|
+
* - Known cloud IDEs / containers (Replit, Codespaces, Gitpod).
|
|
79
|
+
* - SSH sessions (no local browser).
|
|
80
|
+
* - Linux with no X11/Wayland display (headless box — nothing to open).
|
|
81
|
+
*/
|
|
82
|
+
export function isHeadlessEnv(env, platform) {
|
|
83
|
+
if (env['REPL_ID'] !== undefined ||
|
|
84
|
+
env['REPLIT_DEV_DOMAIN'] !== undefined ||
|
|
85
|
+
env['CODESPACES'] !== undefined ||
|
|
86
|
+
env['GITPOD_WORKSPACE_ID'] !== undefined) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (env['SSH_CONNECTION'] !== undefined || env['SSH_TTY'] !== undefined) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (platform === 'linux' &&
|
|
93
|
+
(env['DISPLAY'] === undefined || env['DISPLAY'] === '') &&
|
|
94
|
+
(env['WAYLAND_DISPLAY'] === undefined || env['WAYLAND_DISPLAY'] === '')) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Resolve the sign-in method to use. An explicit choice always wins; otherwise
|
|
101
|
+
* fall back to environment auto-detection (headless → 'code', else 'browser').
|
|
102
|
+
* Pure / testable.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveLoginMethod(explicit, env, platform) {
|
|
105
|
+
if (explicit !== undefined)
|
|
106
|
+
return explicit;
|
|
107
|
+
return isHeadlessEnv(env, platform) ? 'code' : 'browser';
|
|
22
108
|
}
|
|
23
109
|
/**
|
|
24
110
|
* Run the interactive sign-in flow for one provider (or all installed providers
|
|
25
111
|
* when no argument is given). Returns 0 on success, 1 only for an invalid
|
|
26
112
|
* argument — individual sign-in failures are reported but do not fail the command.
|
|
113
|
+
*
|
|
114
|
+
* @param opts.method - Force 'browser' or 'code'. When omitted, the method is
|
|
115
|
+
* auto-detected from the environment via {@link resolveLoginMethod}.
|
|
27
116
|
*/
|
|
28
|
-
export async function runLogin(out, providerArg) {
|
|
117
|
+
export async function runLogin(out, providerArg, opts) {
|
|
29
118
|
let targets;
|
|
30
119
|
if (providerArg !== undefined) {
|
|
31
120
|
if (!isProviderId(providerArg)) {
|
|
32
|
-
out.write(red(`Unknown provider "${providerArg}". Use: claude or
|
|
121
|
+
out.write(red(`Unknown provider "${providerArg}". Use: claude, codex, or opencode.\n`, out.color));
|
|
33
122
|
return 1;
|
|
34
123
|
}
|
|
35
124
|
targets = [providerArg];
|
|
36
125
|
}
|
|
37
126
|
else {
|
|
38
|
-
targets = ['claude', 'codex'];
|
|
127
|
+
targets = ['claude', 'codex', 'opencode'];
|
|
39
128
|
}
|
|
129
|
+
const method = resolveLoginMethod(opts?.method, process.env, process.platform);
|
|
40
130
|
for (const id of targets) {
|
|
41
131
|
const status = await detectProvider(id);
|
|
42
132
|
if (!status.installed) {
|
|
43
133
|
out.write(dim(`${id}: not installed — skipping. Install with: ${getInstallCommand(id)}\n`, out.color));
|
|
44
134
|
continue;
|
|
45
135
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
136
|
+
// stdio:'inherit' hands the terminal to the provider CLI so its OAuth /
|
|
137
|
+
// device / paste flow runs in place. reject:false so we report rather than throw.
|
|
138
|
+
let result;
|
|
139
|
+
if (method === 'code') {
|
|
140
|
+
const { bin, args, guidance } = LOGIN_CODE_COMMAND[id];
|
|
141
|
+
out.write(bold(`\nSigning in to ${id} — code method (no localhost needed).\n`, out.color));
|
|
142
|
+
out.write(dim(guidance + '\n', out.color));
|
|
143
|
+
result = await execa(bin, [...args], { stdio: 'inherit', reject: false });
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
const { bin, args } = LOGIN_COMMAND[id];
|
|
147
|
+
out.write(bold(`\nSigning in to ${id} — a browser window may open…\n`, out.color));
|
|
148
|
+
result = await execa(bin, [...args], { stdio: 'inherit', reject: false });
|
|
149
|
+
}
|
|
51
150
|
if (result.exitCode === 0) {
|
|
52
151
|
out.write(green(`✓ ${id} sign-in complete.\n`, out.color));
|
|
53
152
|
}
|
|
54
153
|
else {
|
|
55
154
|
out.write(red(`✗ ${id} sign-in did not complete (exit ${result.exitCode ?? 'unknown'}).\n`, out.color));
|
|
155
|
+
// The classic container failure mode is a dead localhost callback. Point
|
|
156
|
+
// the user at the code method, which sidesteps localhost entirely.
|
|
157
|
+
if (method === 'browser') {
|
|
158
|
+
out.write(dim(`If the browser/localhost step failed, try the code method instead:\n` +
|
|
159
|
+
` myshell-tools login ${id} --code\n`, out.color));
|
|
160
|
+
}
|
|
56
161
|
}
|
|
57
162
|
}
|
|
58
163
|
return 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"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"}
|
package/dist/core/assess.js
CHANGED
|
@@ -12,70 +12,10 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Pure module: no I/O, no time, no randomness.
|
|
14
14
|
*/
|
|
15
|
+
import { lastJsonObjectWithKey } from './json-envelope.js';
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
17
|
// Helpers
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
|
-
/**
|
|
19
|
-
* Attempt to extract the last JSON object from `text` that looks like a
|
|
20
|
-
* confidence envelope. Returns the raw parsed object or null.
|
|
21
|
-
*
|
|
22
|
-
* Strategy: scan backwards for the last `{...}` block that contains at least
|
|
23
|
-
* the `confidence` key, then try to parse it. This is robust to:
|
|
24
|
-
* - trailing whitespace / newlines
|
|
25
|
-
* - the model emitting extra text after the envelope (we pick the LAST match)
|
|
26
|
-
* - duplicate envelopes (last one wins — the model may have regenerated it)
|
|
27
|
-
* - garbage / truncated JSON elsewhere in the output
|
|
28
|
-
*/
|
|
29
|
-
function extractEnvelope(text) {
|
|
30
|
-
// Find all candidates: substrings that start with '{' and end with '}'
|
|
31
|
-
// We do this by iterating over all '{' positions and trying to match the
|
|
32
|
-
// closing '}', handling depth. We collect all valid JSON objects and
|
|
33
|
-
// return the last one that has a 'confidence' key.
|
|
34
|
-
const candidates = [];
|
|
35
|
-
let i = 0;
|
|
36
|
-
while (i < text.length) {
|
|
37
|
-
const start = text.indexOf('{', i);
|
|
38
|
-
if (start === -1)
|
|
39
|
-
break;
|
|
40
|
-
// Walk forward tracking brace depth to find the matching '}'
|
|
41
|
-
let depth = 0;
|
|
42
|
-
let j = start;
|
|
43
|
-
let foundClose = false;
|
|
44
|
-
while (j < text.length) {
|
|
45
|
-
if (text[j] === '{') {
|
|
46
|
-
depth++;
|
|
47
|
-
}
|
|
48
|
-
else if (text[j] === '}') {
|
|
49
|
-
depth--;
|
|
50
|
-
if (depth === 0) {
|
|
51
|
-
foundClose = true;
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
j++;
|
|
56
|
-
}
|
|
57
|
-
if (foundClose) {
|
|
58
|
-
const candidate = text.slice(start, j + 1);
|
|
59
|
-
try {
|
|
60
|
-
const parsed = JSON.parse(candidate);
|
|
61
|
-
if (parsed !== null &&
|
|
62
|
-
typeof parsed === 'object' &&
|
|
63
|
-
!Array.isArray(parsed) &&
|
|
64
|
-
'confidence' in parsed) {
|
|
65
|
-
candidates.push(parsed);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
// Not valid JSON — skip
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
i = start + 1;
|
|
73
|
-
}
|
|
74
|
-
if (candidates.length === 0)
|
|
75
|
-
return null;
|
|
76
|
-
// Return the last valid envelope (handles duplicate/regenerated envelopes)
|
|
77
|
-
return candidates[candidates.length - 1] ?? null;
|
|
78
|
-
}
|
|
79
19
|
/**
|
|
80
20
|
* Clamp a number to [0, 1].
|
|
81
21
|
*/
|
|
@@ -119,7 +59,7 @@ export function assess(output) {
|
|
|
119
59
|
}
|
|
120
60
|
let envelope;
|
|
121
61
|
try {
|
|
122
|
-
envelope =
|
|
62
|
+
envelope = lastJsonObjectWithKey(output, 'confidence');
|
|
123
63
|
}
|
|
124
64
|
catch {
|
|
125
65
|
return NULL_RESULT;
|
package/dist/core/assess.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assess.js","sourceRoot":"","sources":["../../src/core/assess.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;
|
|
1
|
+
{"version":3,"file":"assess.js","sourceRoot":"","sources":["../../src/core/assess.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAa3D,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E;;GAEG;AACH,SAAS,OAAO,CAAC,CAAS;IACxB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC;AAED;;GAEG;AACH,SAAS,UAAU,CAAC,CAAU;IAC5B,IAAI,OAAO,CAAC,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC;IACrC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,GAAG,CAAC;IAC5D,OAAO,KAAK,CAAC;AACf,CAAC;AAED,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;;;;;;GAQG;AACH,MAAM,UAAU,MAAM,CAAC,MAAc;IACnC,MAAM,WAAW,GAAe;QAC9B,UAAU,EAAE,IAAI;QAChB,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,wBAAwB;QAChC,WAAW,EAAE,KAAK;KACnB,CAAC;IAEF,kCAAkC;IAClC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtD,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,IAAI,QAA4B,CAAC;IACjC,IAAI,CAAC;QACH,QAAQ,GAAG,qBAAqB,CAAC,MAAM,EAAE,YAAY,CAAuB,CAAC;IAC/E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,+CAA+C;IAC/C,IAAI,OAAO,QAAQ,CAAC,UAAU,KAAK,QAAQ,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9E,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC/C,MAAM,WAAW,GAAG,UAAU,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IACtD,MAAM,MAAM,GACV,OAAO,QAAQ,CAAC,MAAM,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QACtE,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE;QACxB,CAAC,CAAC,0BAA0B,CAAC;IAEjC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/budget.ts — pure per-task cost budget helpers.
|
|
3
|
+
*
|
|
4
|
+
* Purity rules (enforced by test/arch/guards.test.ts):
|
|
5
|
+
* - No imports of fs / path / child_process
|
|
6
|
+
* - No console.* calls
|
|
7
|
+
* - No Date.now() / Math.random() / new Date()
|
|
8
|
+
* - No process.exit()
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Returns `true` when `spentUsd` has reached or exceeded the budget cap, so
|
|
12
|
+
* that orchestrate() must stop spending.
|
|
13
|
+
*
|
|
14
|
+
* Returns `false` (no cap) when:
|
|
15
|
+
* - `maxCostUsd` is `null` or `undefined`
|
|
16
|
+
* - `maxCostUsd` is ≤ 0 (non-positive cap is treated as "uncapped")
|
|
17
|
+
*/
|
|
18
|
+
export declare function budgetExceeded(spentUsd: number, maxCostUsd: number | null | undefined): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Returns how many USD remain in the budget, or `null` when uncapped.
|
|
21
|
+
*
|
|
22
|
+
* The returned value may be zero or negative if the cap has already been
|
|
23
|
+
* reached; callers should use {@link budgetExceeded} to make gate decisions
|
|
24
|
+
* rather than checking the sign here.
|
|
25
|
+
*/
|
|
26
|
+
export declare function remainingBudget(spentUsd: number, maxCostUsd: number | null | undefined): number | null;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/budget.ts — pure per-task cost budget helpers.
|
|
3
|
+
*
|
|
4
|
+
* Purity rules (enforced by test/arch/guards.test.ts):
|
|
5
|
+
* - No imports of fs / path / child_process
|
|
6
|
+
* - No console.* calls
|
|
7
|
+
* - No Date.now() / Math.random() / new Date()
|
|
8
|
+
* - No process.exit()
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Returns `true` when `spentUsd` has reached or exceeded the budget cap, so
|
|
12
|
+
* that orchestrate() must stop spending.
|
|
13
|
+
*
|
|
14
|
+
* Returns `false` (no cap) when:
|
|
15
|
+
* - `maxCostUsd` is `null` or `undefined`
|
|
16
|
+
* - `maxCostUsd` is ≤ 0 (non-positive cap is treated as "uncapped")
|
|
17
|
+
*/
|
|
18
|
+
export function budgetExceeded(spentUsd, maxCostUsd) {
|
|
19
|
+
if (maxCostUsd === null || maxCostUsd === undefined || maxCostUsd <= 0) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return spentUsd >= maxCostUsd;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Returns how many USD remain in the budget, or `null` when uncapped.
|
|
26
|
+
*
|
|
27
|
+
* The returned value may be zero or negative if the cap has already been
|
|
28
|
+
* reached; callers should use {@link budgetExceeded} to make gate decisions
|
|
29
|
+
* rather than checking the sign here.
|
|
30
|
+
*/
|
|
31
|
+
export function remainingBudget(spentUsd, maxCostUsd) {
|
|
32
|
+
if (maxCostUsd === null || maxCostUsd === undefined || maxCostUsd <= 0) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return maxCostUsd - spentUsd;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.js","sourceRoot":"","sources":["../../src/core/budget.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,QAAgB,EAChB,UAAqC;IAErC,IAAI,UAAU,KAAK,IAAI,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,IAAI,CAAC,EAAE,CAAC;QACvE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,QAAQ,IAAI,UAAU,CAAC;AAChC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,QAAgB,EAChB,UAAqC;IAErC,IAAI,UAAU,KAAK,IAAI,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,IAAI,CAAC,EAAE,CAAC;QACvE,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,UAAU,GAAG,QAAQ,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/history.ts — compact prior conversation history for context injection.
|
|
3
|
+
*
|
|
4
|
+
* Produces a bounded, human-readable summary of prior SessionEntry turns to be
|
|
5
|
+
* injected into the next provider prompt, giving stateless one-shot providers
|
|
6
|
+
* (claude -p / codex exec) awareness of earlier conversation context.
|
|
7
|
+
*
|
|
8
|
+
* Purity rules (enforced by test/arch/guards.test.ts):
|
|
9
|
+
* - No imports of fs / path / child_process
|
|
10
|
+
* - No console.* calls
|
|
11
|
+
* - No Date.now() / Math.random() / new Date()
|
|
12
|
+
* - No process.exit()
|
|
13
|
+
*/
|
|
14
|
+
import type { SessionEntry } from './types.js';
|
|
15
|
+
export interface CompactHistoryOptions {
|
|
16
|
+
/** Maximum total characters in the returned string. Default: 6000. */
|
|
17
|
+
readonly maxChars?: number;
|
|
18
|
+
/** Maximum number of conversation turns to include. Default: 12. */
|
|
19
|
+
readonly maxTurns?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Compact prior conversation history into a bounded string for context injection.
|
|
23
|
+
*
|
|
24
|
+
* - Takes the MOST RECENT up-to-`maxTurns` entries (preserves chronological order).
|
|
25
|
+
* - Strips confidence-envelope JSON from assistant turns before including them.
|
|
26
|
+
* - Enforces `maxChars` by dropping the OLDEST included turns first until under budget.
|
|
27
|
+
* - If a single turn's content alone exceeds `maxChars`, truncates it with a marker.
|
|
28
|
+
* - Returns '' for empty / undefined input.
|
|
29
|
+
*
|
|
30
|
+
* Pure: no I/O, no Date, no Math.random. Never throws.
|
|
31
|
+
*
|
|
32
|
+
* @param entries - The prior conversation entries (oldest first).
|
|
33
|
+
* @param opts - Optional bounds overrides.
|
|
34
|
+
*/
|
|
35
|
+
export declare function compactHistory(entries: readonly SessionEntry[], opts?: CompactHistoryOptions): string;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/history.ts — compact prior conversation history for context injection.
|
|
3
|
+
*
|
|
4
|
+
* Produces a bounded, human-readable summary of prior SessionEntry turns to be
|
|
5
|
+
* injected into the next provider prompt, giving stateless one-shot providers
|
|
6
|
+
* (claude -p / codex exec) awareness of earlier conversation context.
|
|
7
|
+
*
|
|
8
|
+
* Purity rules (enforced by test/arch/guards.test.ts):
|
|
9
|
+
* - No imports of fs / path / child_process
|
|
10
|
+
* - No console.* calls
|
|
11
|
+
* - No Date.now() / Math.random() / new Date()
|
|
12
|
+
* - No process.exit()
|
|
13
|
+
*/
|
|
14
|
+
import { lastJsonObjectBoundsWithKey } from './json-envelope.js';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Constants
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const DEFAULT_MAX_CHARS = 6000;
|
|
19
|
+
const DEFAULT_MAX_TURNS = 12;
|
|
20
|
+
const TRUNCATION_MARKER = ' …[truncated]';
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Internal helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* Strip any trailing confidence-envelope JSON block from an assistant's content.
|
|
26
|
+
*
|
|
27
|
+
* The envelope is a trailing `{ ... }` object that contains `"confidence"`.
|
|
28
|
+
* Uses {@link lastJsonObjectBoundsWithKey} to locate the last such block, then
|
|
29
|
+
* removes it (and surrounding whitespace) from the content.
|
|
30
|
+
*
|
|
31
|
+
* Never throws — returns the original content on any parse failure.
|
|
32
|
+
*/
|
|
33
|
+
function stripEnvelope(content) {
|
|
34
|
+
try {
|
|
35
|
+
const match = lastJsonObjectBoundsWithKey(content, 'confidence');
|
|
36
|
+
if (match === null) {
|
|
37
|
+
return content;
|
|
38
|
+
}
|
|
39
|
+
// Remove the envelope and any leading whitespace/newline before it
|
|
40
|
+
const before = content.slice(0, match.start).replace(/\s+$/, '');
|
|
41
|
+
const after = content.slice(match.end).replace(/^\s+/, '');
|
|
42
|
+
return after.length > 0 ? `${before}\n${after}` : before;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return content;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Map a SessionEntry role to the display label used in the history block.
|
|
50
|
+
*/
|
|
51
|
+
function roleLabel(role) {
|
|
52
|
+
if (role === 'user')
|
|
53
|
+
return 'User';
|
|
54
|
+
if (role === 'assistant')
|
|
55
|
+
return 'Assistant';
|
|
56
|
+
return 'System';
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Compact prior conversation history into a bounded string for context injection.
|
|
60
|
+
*
|
|
61
|
+
* - Takes the MOST RECENT up-to-`maxTurns` entries (preserves chronological order).
|
|
62
|
+
* - Strips confidence-envelope JSON from assistant turns before including them.
|
|
63
|
+
* - Enforces `maxChars` by dropping the OLDEST included turns first until under budget.
|
|
64
|
+
* - If a single turn's content alone exceeds `maxChars`, truncates it with a marker.
|
|
65
|
+
* - Returns '' for empty / undefined input.
|
|
66
|
+
*
|
|
67
|
+
* Pure: no I/O, no Date, no Math.random. Never throws.
|
|
68
|
+
*
|
|
69
|
+
* @param entries - The prior conversation entries (oldest first).
|
|
70
|
+
* @param opts - Optional bounds overrides.
|
|
71
|
+
*/
|
|
72
|
+
export function compactHistory(entries, opts) {
|
|
73
|
+
try {
|
|
74
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
77
|
+
const maxChars = opts?.maxChars ?? DEFAULT_MAX_CHARS;
|
|
78
|
+
const maxTurns = opts?.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
79
|
+
// Take the most recent up-to-maxTurns entries, then keep chronological order
|
|
80
|
+
const window = entries.slice(-maxTurns);
|
|
81
|
+
// Format each entry into a display line
|
|
82
|
+
const formatted = window.map((entry) => {
|
|
83
|
+
const label = roleLabel(entry.role);
|
|
84
|
+
const rawContent = entry.role === 'assistant' ? stripEnvelope(entry.content) : entry.content;
|
|
85
|
+
const content = rawContent.trim();
|
|
86
|
+
return `${label}: ${content}`;
|
|
87
|
+
});
|
|
88
|
+
// Enforce maxChars by dropping oldest turns first
|
|
89
|
+
// Each formatted turn is separated by '\n\n'
|
|
90
|
+
let kept = formatted.slice(); // copy so we can mutate
|
|
91
|
+
while (kept.length > 0) {
|
|
92
|
+
const joined = kept.join('\n\n');
|
|
93
|
+
if (joined.length <= maxChars) {
|
|
94
|
+
return joined;
|
|
95
|
+
}
|
|
96
|
+
// Drop the oldest turn
|
|
97
|
+
kept = kept.slice(1);
|
|
98
|
+
}
|
|
99
|
+
// If we get here, even a single turn is too long — truncate it
|
|
100
|
+
if (formatted.length > 0) {
|
|
101
|
+
const last = formatted[formatted.length - 1];
|
|
102
|
+
if (last !== undefined && last.length > maxChars) {
|
|
103
|
+
const truncated = last.slice(0, maxChars - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
|
|
104
|
+
return truncated;
|
|
105
|
+
}
|
|
106
|
+
if (last !== undefined) {
|
|
107
|
+
return last;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.js","sourceRoot":"","sources":["../../src/core/history.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/json-envelope.ts — shared brace-depth JSON-envelope scanner.
|
|
3
|
+
*
|
|
4
|
+
* Provides reusable helpers that scan text for the LAST balanced `{...}` JSON
|
|
5
|
+
* object containing a given key. Used by assess.ts (confidence envelope),
|
|
6
|
+
* review.ts (verdict envelope), and history.ts (envelope stripping) to avoid
|
|
7
|
+
* duplicated scanning logic.
|
|
8
|
+
*
|
|
9
|
+
* Honesty Contract: these functions NEVER throw on any input. On parse failure
|
|
10
|
+
* or absent key they return null. They never fabricate data.
|
|
11
|
+
*
|
|
12
|
+
* Pure module: no I/O, no time, no randomness.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Scan `text` and return the LAST balanced `{...}` block that:
|
|
16
|
+
* 1. Parses as valid JSON,
|
|
17
|
+
* 2. Is a plain object (not null, not an array), and
|
|
18
|
+
* 3. Contains the given `key` as a direct property.
|
|
19
|
+
*
|
|
20
|
+
* Returns `null` when no matching block is found. Never throws.
|
|
21
|
+
*
|
|
22
|
+
* Scanning semantics:
|
|
23
|
+
* - All `{` positions are tried left-to-right.
|
|
24
|
+
* - For each `{`, the matching `}` is located by tracking brace depth.
|
|
25
|
+
* - All candidates that parse and contain `key` are collected; the LAST one
|
|
26
|
+
* wins. This handles duplicate/regenerated envelopes in model output.
|
|
27
|
+
*
|
|
28
|
+
* @param text - The text to scan (any string).
|
|
29
|
+
* @param key - The property key that must be present in the JSON object.
|
|
30
|
+
*/
|
|
31
|
+
export declare function lastJsonObjectWithKey(text: string, key: string): Record<string, unknown> | null;
|
|
32
|
+
/**
|
|
33
|
+
* Like {@link lastJsonObjectWithKey}, but also returns the character offsets of
|
|
34
|
+
* the matched block within `text` so callers can excise it.
|
|
35
|
+
*
|
|
36
|
+
* Returns `null` when no matching block is found. Never throws.
|
|
37
|
+
*
|
|
38
|
+
* The returned `start` and `end` follow the same convention as
|
|
39
|
+
* `String.prototype.slice`: `text.slice(start, end)` reproduces the matched
|
|
40
|
+
* `{...}` block exactly.
|
|
41
|
+
*
|
|
42
|
+
* @param text - The text to scan (any string).
|
|
43
|
+
* @param key - The property key that must be present in the JSON object.
|
|
44
|
+
*/
|
|
45
|
+
export declare function lastJsonObjectBoundsWithKey(text: string, key: string): {
|
|
46
|
+
readonly start: number;
|
|
47
|
+
readonly end: number;
|
|
48
|
+
readonly value: Record<string, unknown>;
|
|
49
|
+
} | null;
|