myshell-tools 2.4.0 → 2.7.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 +24 -0
- package/README.md +28 -12
- package/dist/cli.js +38 -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/login.d.ts +51 -5
- package/dist/commands/login.js +207 -14
- 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/credentials.d.ts +58 -0
- package/dist/infra/credentials.js +172 -0
- package/dist/infra/credentials.js.map +1 -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 +26 -0
- package/dist/interface/menu.js +131 -26
- 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,60 +1,253 @@
|
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
+
* After the command exits, we prompt the user to paste the minted token
|
|
17
|
+
* (sk-ant-oat…), persist it, and inject it into process.env so that
|
|
18
|
+
* subsequent `claude auth status` and `claude -p …` calls see it.
|
|
19
|
+
* · codex → `codex login --device-auth`: prints a URL + one-time code;
|
|
20
|
+
* the user authorizes their ChatGPT account on any device.
|
|
21
|
+
*
|
|
22
|
+
* When no method is forced, we auto-detect: headless/remote environments default
|
|
23
|
+
* to 'code' (so the localhost trap is avoided), everything else to 'browser'.
|
|
24
|
+
*
|
|
25
|
+
* Security: myshell-tools never stores raw API keys or passwords. The Claude
|
|
26
|
+
* OAuth token (sk-ant-oat…) is captured only after the user explicitly pastes
|
|
27
|
+
* it and is stored in ~/.myshell-tools/credentials.json (mode 0o600).
|
|
11
28
|
*/
|
|
29
|
+
import readline from 'node:readline';
|
|
12
30
|
import { execa } from 'execa';
|
|
13
31
|
import { detectProvider, getInstallCommand } from '../providers/detect.js';
|
|
14
32
|
import { bold, dim, green, red } from '../ui/theme.js';
|
|
15
|
-
|
|
33
|
+
import { extractClaudeToken, saveClaudeToken } from '../infra/credentials.js';
|
|
34
|
+
/** Each provider's default (browser/localhost) sign-in command. */
|
|
16
35
|
const LOGIN_COMMAND = {
|
|
17
36
|
claude: { bin: 'claude', args: ['auth', 'login'] },
|
|
18
37
|
codex: { bin: 'codex', args: ['login'] },
|
|
38
|
+
// opencode ships free models — no credentials required. `opencode auth login` adds
|
|
39
|
+
// a premium provider/subscription (e.g. anthropic, openai, or opencode-zen).
|
|
40
|
+
opencode: { bin: 'opencode', args: ['auth', 'login'] },
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Each provider's no-localhost ("code") sign-in command, plus the human steps
|
|
44
|
+
* we print before handing over the terminal so the user knows what to expect.
|
|
45
|
+
*/
|
|
46
|
+
const LOGIN_CODE_COMMAND = {
|
|
47
|
+
claude: {
|
|
48
|
+
bin: 'claude',
|
|
49
|
+
args: ['setup-token'],
|
|
50
|
+
guidance: 'A sign-in link will appear below.\n' +
|
|
51
|
+
' 1. Open it in any browser and sign in at claude.ai.\n' +
|
|
52
|
+
' 2. Copy the authorization code it shows you.\n' +
|
|
53
|
+
' 3. Paste the code back here at the prompt and press Enter.',
|
|
54
|
+
},
|
|
55
|
+
codex: {
|
|
56
|
+
bin: 'codex',
|
|
57
|
+
args: ['login', '--device-auth'],
|
|
58
|
+
guidance: 'A URL and a one-time code will appear below.\n' +
|
|
59
|
+
' 1. On any device, open the URL.\n' +
|
|
60
|
+
' 2. Enter the code shown and authorize your ChatGPT account.\n' +
|
|
61
|
+
' 3. Sign-in completes here automatically once authorized.',
|
|
62
|
+
},
|
|
63
|
+
opencode: {
|
|
64
|
+
bin: 'opencode',
|
|
65
|
+
args: ['auth', 'login'],
|
|
66
|
+
guidance: 'Free models need no login.\n' +
|
|
67
|
+
' This starts `opencode auth login` to add a premium provider or subscription\n' +
|
|
68
|
+
' (e.g. anthropic, openai, or opencode-zen).\n' +
|
|
69
|
+
' myshell-tools never sees the credentials — opencode manages them.',
|
|
70
|
+
},
|
|
19
71
|
};
|
|
20
72
|
export function isProviderId(value) {
|
|
21
|
-
return value === 'claude' || value === 'codex';
|
|
73
|
+
return value === 'claude' || value === 'codex' || value === 'opencode';
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Decide whether the current environment can actually reach a localhost OAuth
|
|
77
|
+
* callback / open a browser. Pure (env + platform in, boolean out) so it is
|
|
78
|
+
* hermetically testable.
|
|
79
|
+
*
|
|
80
|
+
* Returns true for environments where the browser/localhost flow typically
|
|
81
|
+
* fails and the code method should be preferred:
|
|
82
|
+
* - Known cloud IDEs / containers (Replit, Codespaces, Gitpod).
|
|
83
|
+
* - SSH sessions (no local browser).
|
|
84
|
+
* - Linux with no X11/Wayland display (headless box — nothing to open).
|
|
85
|
+
*/
|
|
86
|
+
export function isHeadlessEnv(env, platform) {
|
|
87
|
+
if (env['REPL_ID'] !== undefined ||
|
|
88
|
+
env['REPLIT_DEV_DOMAIN'] !== undefined ||
|
|
89
|
+
env['CODESPACES'] !== undefined ||
|
|
90
|
+
env['GITPOD_WORKSPACE_ID'] !== undefined) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if (env['SSH_CONNECTION'] !== undefined || env['SSH_TTY'] !== undefined) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (platform === 'linux' &&
|
|
97
|
+
(env['DISPLAY'] === undefined || env['DISPLAY'] === '') &&
|
|
98
|
+
(env['WAYLAND_DISPLAY'] === undefined || env['WAYLAND_DISPLAY'] === '')) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the sign-in method to use. An explicit choice always wins; otherwise
|
|
105
|
+
* fall back to environment auto-detection (headless → 'code', else 'browser').
|
|
106
|
+
* Pure / testable.
|
|
107
|
+
*/
|
|
108
|
+
export function resolveLoginMethod(explicit, env, platform) {
|
|
109
|
+
if (explicit !== undefined)
|
|
110
|
+
return explicit;
|
|
111
|
+
return isHeadlessEnv(env, platform) ? 'code' : 'browser';
|
|
22
112
|
}
|
|
23
113
|
/**
|
|
24
114
|
* Run the interactive sign-in flow for one provider (or all installed providers
|
|
25
115
|
* when no argument is given). Returns 0 on success, 1 only for an invalid
|
|
26
116
|
* argument — individual sign-in failures are reported but do not fail the command.
|
|
117
|
+
*
|
|
118
|
+
* @param opts.method - Force 'browser' or 'code'. When omitted, the method is
|
|
119
|
+
* auto-detected from the environment via {@link resolveLoginMethod}.
|
|
120
|
+
* @param opts.readLine - Injected line-reader for the token-paste prompt (used
|
|
121
|
+
* by the menu so it shares the single readline interface). When absent, a
|
|
122
|
+
* temporary readline interface is created and immediately closed after one line.
|
|
27
123
|
*/
|
|
28
|
-
export async function runLogin(out, providerArg) {
|
|
124
|
+
export async function runLogin(out, providerArg, opts) {
|
|
29
125
|
let targets;
|
|
30
126
|
if (providerArg !== undefined) {
|
|
31
127
|
if (!isProviderId(providerArg)) {
|
|
32
|
-
out.write(red(`Unknown provider "${providerArg}". Use: claude or
|
|
128
|
+
out.write(red(`Unknown provider "${providerArg}". Use: claude, codex, or opencode.\n`, out.color));
|
|
33
129
|
return 1;
|
|
34
130
|
}
|
|
35
131
|
targets = [providerArg];
|
|
36
132
|
}
|
|
37
133
|
else {
|
|
38
|
-
targets = ['claude', 'codex'];
|
|
134
|
+
targets = ['claude', 'codex', 'opencode'];
|
|
39
135
|
}
|
|
136
|
+
const method = resolveLoginMethod(opts?.method, process.env, process.platform);
|
|
40
137
|
for (const id of targets) {
|
|
41
138
|
const status = await detectProvider(id);
|
|
42
139
|
if (!status.installed) {
|
|
43
140
|
out.write(dim(`${id}: not installed — skipping. Install with: ${getInstallCommand(id)}\n`, out.color));
|
|
44
141
|
continue;
|
|
45
142
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
143
|
+
// stdio:'inherit' hands the terminal to the provider CLI so its OAuth /
|
|
144
|
+
// device / paste flow runs in place. reject:false so we report rather than throw.
|
|
145
|
+
let result;
|
|
146
|
+
if (method === 'code') {
|
|
147
|
+
const { bin, args, guidance } = LOGIN_CODE_COMMAND[id];
|
|
148
|
+
out.write(bold(`\nSigning in to ${id} — code method (no localhost needed).\n`, out.color));
|
|
149
|
+
out.write(dim(guidance + '\n', out.color));
|
|
150
|
+
result = await execa(bin, [...args], { stdio: 'inherit', reject: false });
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
const { bin, args } = LOGIN_COMMAND[id];
|
|
154
|
+
out.write(bold(`\nSigning in to ${id} — a browser window may open…\n`, out.color));
|
|
155
|
+
result = await execa(bin, [...args], { stdio: 'inherit', reject: false });
|
|
156
|
+
}
|
|
51
157
|
if (result.exitCode === 0) {
|
|
52
158
|
out.write(green(`✓ ${id} sign-in complete.\n`, out.color));
|
|
159
|
+
// --- Claude code-method token capture -----------------------------------
|
|
160
|
+
// `claude setup-token` mints a long-lived token (sk-ant-oat01-…) and PRINTS
|
|
161
|
+
// it to the terminal but does NOT persist it. After the command exits we
|
|
162
|
+
// prompt the user to paste the token so we can store it and inject it into
|
|
163
|
+
// process.env — making `claude auth status` report loggedIn:true and making
|
|
164
|
+
// every subsequent `claude -p …` spawn work without manual env-var setup.
|
|
165
|
+
if (id === 'claude' && method === 'code') {
|
|
166
|
+
await captureClaudeToken(out, opts?.readLine);
|
|
167
|
+
}
|
|
53
168
|
}
|
|
54
169
|
else {
|
|
55
170
|
out.write(red(`✗ ${id} sign-in did not complete (exit ${result.exitCode ?? 'unknown'}).\n`, out.color));
|
|
171
|
+
// The classic container failure mode is a dead localhost callback. Point
|
|
172
|
+
// the user at the code method, which sidesteps localhost entirely.
|
|
173
|
+
if (method === 'browser') {
|
|
174
|
+
out.write(dim(`If the browser/localhost step failed, try the code method instead:\n` +
|
|
175
|
+
` myshell-tools login ${id} --code\n`, out.color));
|
|
176
|
+
}
|
|
56
177
|
}
|
|
57
178
|
}
|
|
58
179
|
return 0;
|
|
59
180
|
}
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Token capture helper (internal — exported for tests via credentials.ts)
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
/**
|
|
185
|
+
* Prompt the user to paste the token shown by `claude setup-token`, extract
|
|
186
|
+
* it, persist it, and inject it into `process.env.CLAUDE_CODE_OAUTH_TOKEN`.
|
|
187
|
+
*
|
|
188
|
+
* Uses the injected `readLine` when provided (menu shares its single readline
|
|
189
|
+
* interface). Otherwise creates a temporary readline interface, reads ONE line,
|
|
190
|
+
* and immediately closes it (so stdin is not held open).
|
|
191
|
+
*
|
|
192
|
+
* Never throws — a blank or invalid paste is reported as a dim advisory note.
|
|
193
|
+
*/
|
|
194
|
+
async function captureClaudeToken(out, readLine) {
|
|
195
|
+
out.write('\nPaste the token shown above (starts with sk-ant-oat) and press Enter' +
|
|
196
|
+
' — or leave blank to skip:\n> ');
|
|
197
|
+
let pasted;
|
|
198
|
+
if (readLine !== undefined) {
|
|
199
|
+
// Menu injected its own reader — use it directly, do NOT create a second
|
|
200
|
+
// readline interface (that would double-consume stdin).
|
|
201
|
+
pasted = await readLine();
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// CLI direct path — create a temporary readline, read one line, close.
|
|
205
|
+
pasted = await readOneLineFromStdin();
|
|
206
|
+
}
|
|
207
|
+
const token = extractClaudeToken(pasted ?? '');
|
|
208
|
+
if (token !== null) {
|
|
209
|
+
try {
|
|
210
|
+
await saveClaudeToken(token);
|
|
211
|
+
process.env['CLAUDE_CODE_OAUTH_TOKEN'] = token;
|
|
212
|
+
out.write(green('✓ Claude token saved — claude is now ready.\n', out.color));
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
out.write(dim('Could not save token to disk — you can re-run `myshell-tools login claude --code` later.\n', out.color));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
out.write(dim('Token not captured. Re-run `myshell-tools login claude --code` and paste the\n' +
|
|
220
|
+
'sk-ant-oat… value (NOT an Anthropic API key starting with sk-ant-api).\n', out.color));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Create a temporary readline interface on process.stdin, read exactly one
|
|
225
|
+
* line, and close the interface. Returns the trimmed line or null on EOF.
|
|
226
|
+
*
|
|
227
|
+
* This is used only from the `myshell-tools login` direct CLI path where no
|
|
228
|
+
* shared readline interface exists.
|
|
229
|
+
*/
|
|
230
|
+
function readOneLineFromStdin() {
|
|
231
|
+
return new Promise((resolve) => {
|
|
232
|
+
const rl = readline.createInterface({
|
|
233
|
+
input: process.stdin,
|
|
234
|
+
output: process.stdout,
|
|
235
|
+
terminal: false,
|
|
236
|
+
});
|
|
237
|
+
let resolved = false;
|
|
238
|
+
rl.once('line', (raw) => {
|
|
239
|
+
resolved = true;
|
|
240
|
+
rl.close();
|
|
241
|
+
resolve(raw.trim());
|
|
242
|
+
});
|
|
243
|
+
rl.once('close', () => {
|
|
244
|
+
if (!resolved) {
|
|
245
|
+
resolved = true;
|
|
246
|
+
resolve(null);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
// Re-export extractClaudeToken so test/unit/credentials.test.ts can import it
|
|
252
|
+
// directly from credentials.ts (where it is defined). No re-export needed here.
|
|
60
253
|
//# sourceMappingURL=login.js.map
|
|
@@ -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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"}
|