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.
Files changed (60) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +26 -10
  3. package/dist/cli.js +43 -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/install.d.ts +66 -0
  10. package/dist/commands/install.js +174 -0
  11. package/dist/commands/install.js.map +1 -0
  12. package/dist/commands/login.d.ts +41 -2
  13. package/dist/commands/login.js +116 -11
  14. package/dist/commands/login.js.map +1 -1
  15. package/dist/core/assess.js +2 -62
  16. package/dist/core/assess.js.map +1 -1
  17. package/dist/core/budget.d.ts +26 -0
  18. package/dist/core/budget.js +37 -0
  19. package/dist/core/budget.js.map +1 -0
  20. package/dist/core/history.d.ts +35 -0
  21. package/dist/core/history.js +116 -0
  22. package/dist/core/history.js.map +1 -0
  23. package/dist/core/json-envelope.d.ts +49 -0
  24. package/dist/core/json-envelope.js +117 -0
  25. package/dist/core/json-envelope.js.map +1 -0
  26. package/dist/core/orchestrate.js +107 -8
  27. package/dist/core/orchestrate.js.map +1 -1
  28. package/dist/core/policy.js +17 -9
  29. package/dist/core/policy.js.map +1 -1
  30. package/dist/core/prompt.d.ts +9 -4
  31. package/dist/core/prompt.js +14 -5
  32. package/dist/core/prompt.js.map +1 -1
  33. package/dist/core/review.js +2 -49
  34. package/dist/core/review.js.map +1 -1
  35. package/dist/core/route.d.ts +13 -5
  36. package/dist/core/route.js +20 -6
  37. package/dist/core/route.js.map +1 -1
  38. package/dist/core/types.d.ts +37 -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 +12 -0
  43. package/dist/interface/menu.js +110 -25
  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,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 interactive sign-in command. */
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 codex.\n`, out.color));
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
- 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 });
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;;;;;;;;;;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":"AAAA;;;;;;;;;;;;;GAaG;AAeH,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E;;;;;;;;;;GAUG;AACH,SAAS,eAAe,CAAC,IAAY;IACnC,uEAAuE;IACvE,yEAAyE;IACzE,sEAAsE;IACtE,mDAAmD;IACnD,MAAM,UAAU,GAAkB,EAAE,CAAC;IAErC,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACnC,IAAI,KAAK,KAAK,CAAC,CAAC;YAAE,MAAM;QAExB,6DAA6D;QAC7D,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,GAAG,KAAK,CAAC;QACd,IAAI,UAAU,GAAG,KAAK,CAAC;QACvB,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YACvB,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBACpB,KAAK,EAAE,CAAC;YACV,CAAC;iBAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBAC3B,KAAK,EAAE,CAAC;gBACR,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;oBAChB,UAAU,GAAG,IAAI,CAAC;oBAClB,MAAM;gBACR,CAAC;YACH,CAAC;YACD,CAAC,EAAE,CAAC;QACN,CAAC;QAED,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YAC3C,IAAI,CAAC;gBACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;gBAC9C,IACE,MAAM,KAAK,IAAI;oBACf,OAAO,MAAM,KAAK,QAAQ;oBAC1B,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;oBACtB,YAAY,IAAK,MAAiB,EAClC,CAAC;oBACD,UAAU,CAAC,IAAI,CAAC,MAAqB,CAAC,CAAC;gBACzC,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,wBAAwB;YAC1B,CAAC;QACH,CAAC;QAED,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;IAChB,CAAC;IAED,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,2EAA2E;IAC3E,OAAO,UAAU,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC;AACnD,CAAC;AAED;;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,eAAe,CAAC,MAAM,CAAC,CAAC;IACrC,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"}
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;