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.
Files changed (60) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +28 -12
  3. package/dist/cli.js +38 -3
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/cost.js +4 -1
  6. package/dist/commands/cost.js.map +1 -1
  7. package/dist/commands/doctor.js +2 -2
  8. package/dist/commands/doctor.js.map +1 -1
  9. package/dist/commands/login.d.ts +51 -5
  10. package/dist/commands/login.js +207 -14
  11. package/dist/commands/login.js.map +1 -1
  12. package/dist/core/assess.js +2 -62
  13. package/dist/core/assess.js.map +1 -1
  14. package/dist/core/budget.d.ts +26 -0
  15. package/dist/core/budget.js +37 -0
  16. package/dist/core/budget.js.map +1 -0
  17. package/dist/core/history.d.ts +35 -0
  18. package/dist/core/history.js +116 -0
  19. package/dist/core/history.js.map +1 -0
  20. package/dist/core/json-envelope.d.ts +49 -0
  21. package/dist/core/json-envelope.js +117 -0
  22. package/dist/core/json-envelope.js.map +1 -0
  23. package/dist/core/orchestrate.js +107 -8
  24. package/dist/core/orchestrate.js.map +1 -1
  25. package/dist/core/policy.js +17 -9
  26. package/dist/core/policy.js.map +1 -1
  27. package/dist/core/prompt.d.ts +9 -4
  28. package/dist/core/prompt.js +14 -5
  29. package/dist/core/prompt.js.map +1 -1
  30. package/dist/core/review.js +2 -49
  31. package/dist/core/review.js.map +1 -1
  32. package/dist/core/route.d.ts +13 -5
  33. package/dist/core/route.js +20 -6
  34. package/dist/core/route.js.map +1 -1
  35. package/dist/core/types.d.ts +37 -0
  36. package/dist/infra/credentials.d.ts +58 -0
  37. package/dist/infra/credentials.js +172 -0
  38. package/dist/infra/credentials.js.map +1 -0
  39. package/dist/infra/pricing.d.ts +17 -4
  40. package/dist/infra/pricing.js +73 -3
  41. package/dist/infra/pricing.js.map +1 -1
  42. package/dist/interface/menu.d.ts +26 -0
  43. package/dist/interface/menu.js +131 -26
  44. package/dist/interface/menu.js.map +1 -1
  45. package/dist/providers/detect.d.ts +17 -5
  46. package/dist/providers/detect.js +56 -4
  47. package/dist/providers/detect.js.map +1 -1
  48. package/dist/providers/install.js +1 -0
  49. package/dist/providers/install.js.map +1 -1
  50. package/dist/providers/opencode-parse.d.ts +49 -0
  51. package/dist/providers/opencode-parse.js +181 -0
  52. package/dist/providers/opencode-parse.js.map +1 -0
  53. package/dist/providers/opencode.d.ts +43 -0
  54. package/dist/providers/opencode.js +121 -0
  55. package/dist/providers/opencode.js.map +1 -0
  56. package/dist/providers/port.d.ts +1 -1
  57. package/dist/providers/registry.d.ts +2 -2
  58. package/dist/providers/registry.js +6 -2
  59. package/dist/providers/registry.js.map +1 -1
  60. package/package.json +2 -2
@@ -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
- * Security: myshell-tools never sees, handles, or stores raw credentials. Each
9
- * CLI manages its own tokens; we only trigger its login. (This is what keeps the
10
- * "use your subscription, no API keys" model honest.)
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
- /** Each provider's interactive sign-in command. */
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 codex.\n`, out.color));
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
- out.write(bold(`\nSigning in to ${id} a browser window may open…\n`, out.color));
47
- const { bin, args } = LOGIN_COMMAND[id];
48
- // stdio:'inherit' hands the terminal to the provider CLI so its OAuth/device
49
- // flow runs in place. reject:false so we report rather than throw.
50
- const result = await execa(bin, [...args], { stdio: 'inherit', reject: false });
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;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAG9B,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3E,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAEvD,mDAAmD;AACnD,MAAM,aAAa,GAAmF;IACpG,MAAM,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE;IAClD,KAAK,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,OAAO,CAAC,EAAE;CACzC,CAAC;AAEF,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,OAAO,CAAC;AACjD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,GAAe,EAAE,WAAoB;IAClE,IAAI,OAAqB,CAAC;IAC1B,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,EAAE,CAAC;YAC/B,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,qBAAqB,WAAW,4BAA4B,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;YACxF,OAAO,CAAC,CAAC;QACX,CAAC;QACD,OAAO,GAAG,CAAC,WAAW,CAAC,CAAC;IAC1B,CAAC;SAAM,CAAC;QACN,OAAO,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAChC,CAAC;IAED,KAAK,MAAM,EAAE,IAAI,OAAO,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,EAAE,CAAC,CAAC;QACxC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACtB,GAAG,CAAC,KAAK,CACP,GAAG,CAAC,GAAG,EAAE,6CAA6C,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAC5F,CAAC;YACF,SAAS;QACX,CAAC;QAED,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,mBAAmB,EAAE,iCAAiC,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;QACnF,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,aAAa,CAAC,EAAE,CAAC,CAAC;QACxC,6EAA6E;QAC7E,mEAAmE;QACnE,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QAEhF,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;YAC1B,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,sBAAsB,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;QAC7D,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,KAAK,CACP,GAAG,CAAC,KAAK,EAAE,mCAAmC,MAAM,CAAC,QAAQ,IAAI,SAAS,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,CAC7F,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,CAAC,CAAC;AACX,CAAC"}
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"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"}
@@ -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 = extractEnvelope(output);
62
+ envelope = lastJsonObjectWithKey(output, 'confidence');
123
63
  }
124
64
  catch {
125
65
  return NULL_RESULT;
@@ -1 +1 @@
1
- {"version":3,"file":"assess.js","sourceRoot":"","sources":["../../src/core/assess.ts"],"names":[],"mappings":"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"}
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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"}