myshell-tools 2.8.0 → 2.13.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.
@@ -13,9 +13,11 @@
13
13
  * - 'code': a no-localhost flow that works anywhere.
14
14
  * · claude → `claude setup-token`: prints a link; the user signs in at
15
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.
16
+ * We TEE the output so we can scan it for the minted token
17
+ * (sk-ant-oat…) automatically. If auto-capture finds the token, it is
18
+ * persisted immediately with no user action required. Otherwise we fall
19
+ * back to prompting the user to paste it (up to 3 retries, with helpful
20
+ * warnings for blank or wrong-type inputs).
19
21
  * · codex → `codex login --device-auth`: prints a URL + one-time code;
20
22
  * the user authorizes their ChatGPT account on any device.
21
23
  *
@@ -23,14 +25,15 @@
23
25
  * to 'code' (so the localhost trap is avoided), everything else to 'browser'.
24
26
  *
25
27
  * 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).
28
+ * OAuth token (sk-ant-oat…) is either auto-captured from the tee'd output or
29
+ * captured after the user explicitly pastes it, and is stored in
30
+ * ~/.myshell-tools/credentials.json (mode 0o600).
28
31
  */
29
32
  import readline from 'node:readline';
30
33
  import { execa } from 'execa';
31
34
  import { detectProvider, getInstallCommand } from '../providers/detect.js';
32
- import { bold, dim, green, red } from '../ui/theme.js';
33
- import { extractClaudeToken, saveClaudeToken } from '../infra/credentials.js';
35
+ import { bold, dim, green, red, yellow } from '../ui/theme.js';
36
+ import { classifyPastedSecret, extractClaudeToken, saveClaudeToken, stripPastedSecretWrapper, } from '../infra/credentials.js';
34
37
  /** Each provider's default (browser/localhost) sign-in command. */
35
38
  const LOGIN_COMMAND = {
36
39
  claude: { bin: 'claude', args: ['auth', 'login'] },
@@ -50,7 +53,8 @@ const LOGIN_CODE_COMMAND = {
50
53
  guidance: 'A sign-in link will appear below.\n' +
51
54
  ' 1. Open it in any browser and sign in at claude.ai.\n' +
52
55
  ' 2. Copy the authorization code it shows you.\n' +
53
- ' 3. Paste the code back here at the prompt and press Enter.',
56
+ ' 3. Paste the code back here at the prompt and press Enter.\n' +
57
+ ' We will capture the token automatically when possible.',
54
58
  },
55
59
  codex: {
56
60
  bin: 'codex',
@@ -142,28 +146,60 @@ export async function runLogin(out, providerArg, opts) {
142
146
  }
143
147
  // stdio:'inherit' hands the terminal to the provider CLI so its OAuth /
144
148
  // device / paste flow runs in place. reject:false so we report rather than throw.
149
+ //
150
+ // For the claude code method we use a special tee-capture approach: stdout
151
+ // and stderr are piped so we can scan for the token, but each line is also
152
+ // written verbatim to the real terminal so the user sees the flow live.
153
+ // stdin is still inherited so the user can interact (paste the auth code).
145
154
  let result;
146
- if (method === 'code') {
155
+ let capturedOutput = '';
156
+ if (method === 'code' && id === 'claude') {
147
157
  const { bin, args, guidance } = LOGIN_CODE_COMMAND[id];
148
158
  out.write(bold(`\nSigning in to ${id} — code method (no localhost needed).\n`, out.color));
149
159
  out.write(dim(guidance + '\n', out.color));
150
- result = await execa(bin, [...args], { stdio: 'inherit', reject: false });
160
+ capturedOutput = await runWithTeeCapture(bin, [...args]);
161
+ // We use a synthetic exit-code-0 result — tee-capture never throws, and
162
+ // we treat the flow as complete regardless of the subprocess exit code
163
+ // (the token presence is the real signal).
164
+ result = { exitCode: 0 };
165
+ }
166
+ else if (method === 'code') {
167
+ const { bin, args, guidance } = LOGIN_CODE_COMMAND[id];
168
+ out.write(bold(`\nSigning in to ${id} — code method (no localhost needed).\n`, out.color));
169
+ out.write(dim(guidance + '\n', out.color));
170
+ result = await execa(bin, [...args], { stdin: 'inherit', stdout: 'inherit', stderr: 'inherit', reject: false });
151
171
  }
152
172
  else {
153
173
  const { bin, args } = LOGIN_COMMAND[id];
154
174
  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 });
175
+ result = await execa(bin, [...args], { stdin: 'inherit', stdout: 'inherit', stderr: 'inherit', reject: false });
156
176
  }
157
177
  if (result.exitCode === 0) {
158
- out.write(green(`✓ ${id} sign-in complete.\n`, out.color));
159
178
  // --- Claude code-method token capture -----------------------------------
160
179
  // `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.
180
+ // it to the terminal. We tee'd the output above and scan it first.
181
+ // If auto-capture finds the token, we're done. Otherwise we fall back to
182
+ // the paste prompt (up to 3 retries) so the user can paste it manually.
165
183
  if (id === 'claude' && method === 'code') {
166
- await captureClaudeToken(out, opts?.readLine);
184
+ const autoToken = extractClaudeToken(capturedOutput);
185
+ if (autoToken !== null) {
186
+ try {
187
+ await saveClaudeToken(autoToken);
188
+ process.env['CLAUDE_CODE_OAUTH_TOKEN'] = autoToken;
189
+ out.write(green('✓ Claude token captured and saved — claude is now ready.\n', out.color));
190
+ }
191
+ catch {
192
+ out.write(dim('Could not save token to disk — re-run `myshell-tools login claude --code` later.\n', out.color));
193
+ }
194
+ }
195
+ else {
196
+ // Auto-capture found nothing — fall back to paste prompt.
197
+ out.write(green(`✓ ${id} sign-in complete.\n`, out.color));
198
+ await captureClaudeTokenWithPaste(out, opts?.readLine);
199
+ }
200
+ }
201
+ else {
202
+ out.write(green(`✓ ${id} sign-in complete.\n`, out.color));
167
203
  }
168
204
  }
169
205
  else {
@@ -179,45 +215,108 @@ export async function runLogin(out, providerArg, opts) {
179
215
  return 0;
180
216
  }
181
217
  // ---------------------------------------------------------------------------
182
- // Token capture helper (internal — exported for tests via credentials.ts)
218
+ // Tee-capture helper (internal)
219
+ // ---------------------------------------------------------------------------
220
+ /**
221
+ * Run a command with stdout and stderr piped (tee'd to the real terminal) and
222
+ * stdin inherited. Returns the accumulated stdout+stderr text, or an empty
223
+ * string on spawn failure. Never throws.
224
+ *
225
+ * Uses execa v9's `all: true` option to merge stdout+stderr into one stream,
226
+ * then iterates it via `subprocess.iterable({ from: 'all', preserveNewlines: true })`
227
+ * while mirroring each chunk to `process.stdout` so the user sees the output
228
+ * live. stdin is inherited so the user can interact (e.g. paste an auth code).
229
+ */
230
+ async function runWithTeeCapture(bin, args) {
231
+ try {
232
+ const subprocess = execa(bin, [...args], {
233
+ stdin: 'inherit',
234
+ stdout: 'pipe',
235
+ stderr: 'pipe',
236
+ all: true,
237
+ reject: false,
238
+ timeout: 300_000,
239
+ });
240
+ const chunks = [];
241
+ for await (const line of subprocess.iterable({ from: 'all', preserveNewlines: true })) {
242
+ const text = typeof line === 'string' ? line : String(line);
243
+ process.stdout.write(text);
244
+ chunks.push(text);
245
+ }
246
+ await subprocess; // wait for exit (already resolved by the iteration above)
247
+ return chunks.join('');
248
+ }
249
+ catch {
250
+ // Spawn error or unexpected failure — return empty so the caller falls back
251
+ // to the paste prompt.
252
+ return '';
253
+ }
254
+ }
255
+ // ---------------------------------------------------------------------------
256
+ // Paste-fallback token capture helper (internal)
183
257
  // ---------------------------------------------------------------------------
184
258
  /**
185
259
  * Prompt the user to paste the token shown by `claude setup-token`, extract
186
260
  * it, persist it, and inject it into `process.env.CLAUDE_CODE_OAUTH_TOKEN`.
187
261
  *
262
+ * Retries up to 3 times:
263
+ * - Blank input → skip silently with a note.
264
+ * - API key → print a specific warning (that's sk-ant-api, not sk-ant-oat).
265
+ * - Invalid → warn and re-prompt.
266
+ * - Valid token → save and set env.
267
+ *
188
268
  * Uses the injected `readLine` when provided (menu shares its single readline
189
269
  * interface). Otherwise creates a temporary readline interface, reads ONE line,
190
270
  * and immediately closes it (so stdin is not held open).
191
271
  *
192
- * Never throws — a blank or invalid paste is reported as a dim advisory note.
272
+ * Never throws.
193
273
  */
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));
274
+ async function captureClaudeTokenWithPaste(out, readLine) {
275
+ const MAX_RETRIES = 3;
276
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
277
+ out.write(`\nPaste the token shown above (starts with sk-ant-oat) and press Enter` +
278
+ ` — or leave blank to skip (attempt ${attempt}/${MAX_RETRIES}):\n> `);
279
+ let raw;
280
+ if (readLine !== undefined) {
281
+ // Menu injected its own reader — use it directly, do NOT create a second
282
+ // readline interface (that would double-consume stdin).
283
+ raw = await readLine();
284
+ }
285
+ else {
286
+ // CLI direct path — create a temporary readline, read one line, close.
287
+ raw = await readOneLineFromStdin();
213
288
  }
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));
289
+ const normalised = stripPastedSecretWrapper(raw ?? '');
290
+ if (normalised === '') {
291
+ out.write(dim('Skipped — no token entered.\n', out.color));
292
+ return;
293
+ }
294
+ const kind = classifyPastedSecret(normalised);
295
+ if (kind === 'api-key') {
296
+ out.write(yellow('That looks like an Anthropic API key (sk-ant-api…), not the setup-token\n' +
297
+ 'OAuth token. Please paste the sk-ant-oat… value instead.\n', out.color));
298
+ continue;
299
+ }
300
+ const token = extractClaudeToken(normalised);
301
+ if (token !== null) {
302
+ try {
303
+ await saveClaudeToken(token);
304
+ process.env['CLAUDE_CODE_OAUTH_TOKEN'] = token;
305
+ out.write(green('✓ Claude token saved — claude is now ready.\n', out.color));
306
+ }
307
+ catch {
308
+ out.write(dim('Could not save token to disk — you can re-run `myshell-tools login claude --code` later.\n', out.color));
309
+ }
310
+ return;
311
+ }
312
+ // Not a recognised token format.
313
+ if (attempt < MAX_RETRIES) {
314
+ out.write(dim('Token not recognised — expected sk-ant-oat… format. Please try again.\n', out.color));
315
+ }
316
+ else {
317
+ out.write(dim('Token not captured after 3 attempts. Re-run `myshell-tools login claude --code`\n' +
318
+ 'and paste the sk-ant-oat… value (NOT an Anthropic API key starting with sk-ant-api).\n', out.color));
216
319
  }
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
320
  }
222
321
  }
223
322
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"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"}
@@ -2,17 +2,33 @@
2
2
  * src/core/classify.ts — pure task classification.
3
3
  *
4
4
  * Determines the orchestration tier and security risk level from a free-text
5
- * task description using keyword/regex signals. No I/O, no time, no randomness.
5
+ * task description using a multi-signal scoring approach. No I/O, no time,
6
+ * no randomness — pure function.
6
7
  *
7
8
  * Honesty Contract: no fabricated confidence numbers are produced here.
8
- * The rationale field names the matched signal so callers can audit decisions.
9
+ * The rationale field names the matched signals so callers can audit decisions.
10
+ *
11
+ * # Tier scoring model
12
+ *
13
+ * Each tier has a list of keyword patterns. The text is matched against all
14
+ * three lists and a score (count of matches) is accumulated per tier.
15
+ * Tie-break is deterministic: manager > ic > worker.
16
+ * Default when nothing matches: ic.
17
+ *
18
+ * # Risk model
19
+ *
20
+ * Priority cascade: critical > high > medium > low.
21
+ * Word boundaries prevent false positives (e.g. "keyboard" does not match
22
+ * the "key" critical signal).
9
23
  */
10
24
  import type { Classification } from './types.js';
11
25
  /**
12
26
  * Classify a free-text `task` string into a {@link Classification}.
13
27
  *
14
- * Tier priority: manager > worker > ic (ic is the default).
15
- * Risk priority: critical > high > medium > low (low is the default).
28
+ * Tier: multi-signal scoring; tie-break manager > ic > worker; default ic.
29
+ * Risk: priority cascade critical > high > medium > low; default low.
30
+ *
31
+ * Never throws — empty or whitespace input returns a safe default.
16
32
  *
17
33
  * @param task - The raw task description from the user.
18
34
  */
@@ -2,79 +2,277 @@
2
2
  * src/core/classify.ts — pure task classification.
3
3
  *
4
4
  * Determines the orchestration tier and security risk level from a free-text
5
- * task description using keyword/regex signals. No I/O, no time, no randomness.
5
+ * task description using a multi-signal scoring approach. No I/O, no time,
6
+ * no randomness — pure function.
6
7
  *
7
8
  * Honesty Contract: no fabricated confidence numbers are produced here.
8
- * The rationale field names the matched signal so callers can audit decisions.
9
+ * The rationale field names the matched signals so callers can audit decisions.
10
+ *
11
+ * # Tier scoring model
12
+ *
13
+ * Each tier has a list of keyword patterns. The text is matched against all
14
+ * three lists and a score (count of matches) is accumulated per tier.
15
+ * Tie-break is deterministic: manager > ic > worker.
16
+ * Default when nothing matches: ic.
17
+ *
18
+ * # Risk model
19
+ *
20
+ * Priority cascade: critical > high > medium > low.
21
+ * Word boundaries prevent false positives (e.g. "keyboard" does not match
22
+ * the "key" critical signal).
9
23
  */
10
24
  // ---------------------------------------------------------------------------
11
25
  // Tier signal tables
12
26
  // ---------------------------------------------------------------------------
13
- /** Words that signal a manager-tier task (review, planning, architecture, security). */
14
- const MANAGER_WORDS = /\b(review|plan|design|architect|audit|security|threat|evaluate|assess|complex)\b/i;
15
- /** Words that signal a worker-tier task (read-only lookups, searches). */
16
- const WORKER_WORDS = /\b(find|search|grep|locate|list|read[-\s]only|lookup|scan|what\s+is)\b/i;
27
+ /**
28
+ * Manager-tier signals: review/planning/architecture/audit tasks that require
29
+ * high-level judgment across the codebase or system.
30
+ */
31
+ const MANAGER_SIGNALS = [
32
+ /\breview\b/i,
33
+ /\bplan\b/i,
34
+ /\bdesign\b/i,
35
+ /\barchitect(?:ure)?\b/i,
36
+ /\baudit\b/i,
37
+ /\bsecurity\b/i,
38
+ /\bthreat\s+model\b/i,
39
+ /\bevaluate\b/i,
40
+ /\bassess\b/i,
41
+ /\bcompare\s+approaches?\b/i,
42
+ /\btrade[-\s]?offs?\b/i,
43
+ /\bwhich\s+approach\b/i,
44
+ /\bshould\s+we\b/i,
45
+ /\bstrategy\b/i,
46
+ /\bhigh[-\s]level\b/i,
47
+ /\bacross\s+the\s+(?:codebase|system)\b/i,
48
+ /\bend[-\s]to[-\s]end\b/i,
49
+ /\blarge\s+migration\b/i,
50
+ /\bcomplex(?:ity)?\b/i,
51
+ /\bcomplicated\b/i,
52
+ ];
53
+ /**
54
+ * Worker-tier signals: pure read-only lookups, searches, and explanations —
55
+ * no file mutation implied.
56
+ */
57
+ const WORKER_SIGNALS = [
58
+ /\bfind\b/i,
59
+ /\bsearch\b/i,
60
+ /\bgrep\b/i,
61
+ /\blocate\b/i,
62
+ /\blist\b/i,
63
+ /\bread[-\s]?only\b/i,
64
+ /\blookup\b/i,
65
+ /\bscan\b/i,
66
+ /\bshow\b/i,
67
+ /\bdisplay\b/i,
68
+ /\bprint\b/i,
69
+ /\bcount\b/i,
70
+ /\bwhat\s+(?:is|are)\b/i,
71
+ /\bwhere\s+(?:is|are)\b/i,
72
+ /\bwhich\s+(?:is|are)\b/i,
73
+ /\bwho\s+(?:is|are)\b/i,
74
+ /\bhow\s+does\b/i,
75
+ /\bexplain\b/i,
76
+ /\bdescribe\b/i,
77
+ /\bsummariz(?:e|ise)\b/i,
78
+ /\bwhat\s+does\b/i,
79
+ ];
80
+ /**
81
+ * IC-tier signals: implementation and hands-on editing tasks.
82
+ * (Used for tie-breaking and direct scoring; ic is the default tier.)
83
+ */
84
+ const IC_SIGNALS = [
85
+ /\bimplement\b/i,
86
+ /\bwrite\b/i,
87
+ /\badd\b/i,
88
+ /\bcreate\b/i,
89
+ /\bbuild\b/i,
90
+ /\bfix\b/i,
91
+ /\bdebug\b/i,
92
+ /\brefactor\b/i,
93
+ /\bupdate\b/i,
94
+ /\bmodify\b/i,
95
+ /\bchange\b/i,
96
+ /\brename\b/i,
97
+ /\bremove\b/i,
98
+ /\bdelete\b/i,
99
+ /\bmove\b/i,
100
+ /\brewrite\b/i,
101
+ /\boptimize\b/i,
102
+ /\bformat\b/i,
103
+ /\bwire\s+up\b/i,
104
+ /\bhook\s+up\b/i,
105
+ /\badd\s+tests?\b/i,
106
+ ];
107
+ // ---------------------------------------------------------------------------
108
+ // Risk signal tables — highest priority wins (critical > high > medium > low)
109
+ // ---------------------------------------------------------------------------
110
+ /**
111
+ * Critical: auth / secrets / credentials / encryption artefacts.
112
+ * Conservative: a security subject immediately elevates to critical.
113
+ */
114
+ const CRITICAL_SIGNALS = [
115
+ /\bauth(?:entication|orization)?\b/i,
116
+ /\bcredential\b/i,
117
+ /\bsecret\b/i,
118
+ /\bpassword\b/i,
119
+ /\btoken\b/i,
120
+ /\bapi[-\s]?key\b/i,
121
+ /\bprivate[-\s]?key\b/i,
122
+ /\bencrypt(?:ion|ed)?\b/i,
123
+ /\bcertificate\b/i,
124
+ /\boauth\b/i,
125
+ /\bvault\b/i,
126
+ /\bsession\b/i,
127
+ /\bcookie\b/i,
128
+ /\bjwt\b/i,
129
+ /\.env\b/i,
130
+ ];
131
+ /**
132
+ * High: payments, deploys, migrations, CI/CD, permissions, schema changes,
133
+ * production infrastructure.
134
+ */
135
+ const HIGH_SIGNALS = [
136
+ /\blogin\b/i,
137
+ /\bpayment\b/i,
138
+ /\bbilling\b/i,
139
+ /\bdeploy(?:ment)?\b/i,
140
+ /\bmigration\b/i,
141
+ /\bci\/cd\b/i,
142
+ /\bpermission\b/i,
143
+ /\bschema\b/i,
144
+ /\bproduction\b/i,
145
+ /\bprod\b/i,
146
+ /\brelease\b/i,
147
+ /\brollback\b/i,
148
+ /\binfra(?:structure)?\b/i,
149
+ /\bterraform\b/i,
150
+ /\bkubernetes\b/i,
151
+ /\bk8s\b/i,
152
+ /\bdocker\b/i,
153
+ /\bdb\s+migration\b/i,
154
+ /\bdatabase\s+migration\b/i,
155
+ ];
156
+ /**
157
+ * Medium: tests, configs, shared utilities, integrations, build/CI signals.
158
+ */
159
+ const MEDIUM_SIGNALS = [
160
+ /\btests?\b/i,
161
+ /\bspecs?\b/i,
162
+ /\bconfig(?:uration)?\b/i,
163
+ /\bintegration\b/i,
164
+ /\bshared\b/i,
165
+ /\butil(?:ity|ities)?\b/i,
166
+ /\blib\b/i,
167
+ /\blint\b/i,
168
+ /\bci\b/i,
169
+ /\bbuild\b/i,
170
+ /\bdependenc(?:y|ies)\b/i,
171
+ ];
17
172
  // ---------------------------------------------------------------------------
18
- // Risk signal tables — highest match wins
173
+ // Scoring helpers
19
174
  // ---------------------------------------------------------------------------
20
- /** Critical: auth / secrets / credentials / encryption artefacts. */
21
- const CRITICAL_RE = /\b(auth|credential|secret|password|token|key|encrypt|certificate)\b|\.env\b/i;
22
- /** High: payments, deploys, migrations, CI/CD, permissions, schema changes. */
23
- const HIGH_RE = /\b(login|payment|billing|deploy|migration|ci\/cd|permission|schema)\b/i;
24
- /** Medium: tests, configs, shared utilities, integrations. */
25
- const MEDIUM_RE = /\b(test|spec|config|integration|shared|util|lib)\b/i;
175
+ /**
176
+ * Count how many patterns in `signals` match `text` (each pattern counts once).
177
+ * Returns matched signal labels (the matching portion of the first match) for
178
+ * rationale generation.
179
+ */
180
+ function scoreSignals(text, signals) {
181
+ const matched = [];
182
+ for (const re of signals) {
183
+ const m = re.exec(text);
184
+ if (m !== null) {
185
+ matched.push(m[0].toLowerCase());
186
+ }
187
+ }
188
+ return matched;
189
+ }
26
190
  // ---------------------------------------------------------------------------
27
191
  // Public API
28
192
  // ---------------------------------------------------------------------------
29
193
  /**
30
194
  * Classify a free-text `task` string into a {@link Classification}.
31
195
  *
32
- * Tier priority: manager > worker > ic (ic is the default).
33
- * Risk priority: critical > high > medium > low (low is the default).
196
+ * Tier: multi-signal scoring; tie-break manager > ic > worker; default ic.
197
+ * Risk: priority cascade critical > high > medium > low; default low.
198
+ *
199
+ * Never throws — empty or whitespace input returns a safe default.
34
200
  *
35
201
  * @param task - The raw task description from the user.
36
202
  */
37
203
  export function classify(task) {
38
- // --- Tier ---
204
+ // Treat empty / whitespace as ic + low with a clear rationale.
205
+ if (!task || task.trim().length === 0) {
206
+ return {
207
+ tier: 'ic',
208
+ risk: 'low',
209
+ rationale: 'tier: ic (empty input — defaulting to ic); risk: low (no risk signals — defaulting to low)',
210
+ };
211
+ }
212
+ // --- Tier scoring ---
213
+ const managerMatches = scoreSignals(task, MANAGER_SIGNALS);
214
+ const icMatches = scoreSignals(task, IC_SIGNALS);
215
+ const workerMatches = scoreSignals(task, WORKER_SIGNALS);
216
+ const managerScore = managerMatches.length;
217
+ const icScore = icMatches.length;
218
+ const workerScore = workerMatches.length;
39
219
  let tier;
40
- let tierSignal;
41
- if (MANAGER_WORDS.test(task)) {
220
+ let tierSignals;
221
+ if (managerScore > 0 && managerScore >= icScore && managerScore >= workerScore) {
222
+ // Manager wins (or ties with any other tier — manager has highest priority)
42
223
  tier = 'manager';
43
- const m = task.match(MANAGER_WORDS);
44
- tierSignal = m ? `manager keyword '${m[0].toLowerCase()}'` : 'manager keyword';
224
+ tierSignals = managerMatches;
45
225
  }
46
- else if (WORKER_WORDS.test(task)) {
226
+ else if (icScore > 0 && icScore >= workerScore) {
227
+ // IC wins over worker (ic has higher priority on tie)
228
+ tier = 'ic';
229
+ tierSignals = icMatches;
230
+ }
231
+ else if (workerScore > 0) {
47
232
  tier = 'worker';
48
- const m = task.match(WORKER_WORDS);
49
- tierSignal = m ? `worker keyword '${m[0].toLowerCase()}'` : 'worker keyword';
233
+ tierSignals = workerMatches;
50
234
  }
51
235
  else {
236
+ // Nothing matched — default to ic
52
237
  tier = 'ic';
53
- tierSignal = 'no tier keyword matched — defaulting to ic';
238
+ tierSignals = [];
54
239
  }
55
- // --- Risk ---
240
+ const tierRationale = tierSignals.length > 0
241
+ ? `tier: ${tier} (matched: ${tierSignals.join(', ')})`
242
+ : `tier: ${tier} (no tier keyword matched — defaulting to ic)`;
243
+ // --- Risk cascade (critical > high > medium > low) ---
56
244
  let risk;
57
- let riskSignal;
58
- if (CRITICAL_RE.test(task)) {
245
+ let riskSignals;
246
+ const criticalMatches = scoreSignals(task, CRITICAL_SIGNALS);
247
+ if (criticalMatches.length > 0) {
59
248
  risk = 'critical';
60
- const m = task.match(CRITICAL_RE);
61
- riskSignal = `critical keyword '${m ? m[0].toLowerCase() : 'unknown'}'`;
62
- }
63
- else if (HIGH_RE.test(task)) {
64
- risk = 'high';
65
- const m = task.match(HIGH_RE);
66
- riskSignal = `high keyword '${m ? m[0].toLowerCase() : 'unknown'}'`;
67
- }
68
- else if (MEDIUM_RE.test(task)) {
69
- risk = 'medium';
70
- const m = task.match(MEDIUM_RE);
71
- riskSignal = `medium keyword '${m ? m[0].toLowerCase() : 'unknown'}'`;
249
+ riskSignals = criticalMatches;
72
250
  }
73
251
  else {
74
- risk = 'low';
75
- riskSignal = 'no risk keyword matched — defaulting to low';
252
+ const highMatches = scoreSignals(task, HIGH_SIGNALS);
253
+ if (highMatches.length > 0) {
254
+ risk = 'high';
255
+ riskSignals = highMatches;
256
+ }
257
+ else {
258
+ const mediumMatches = scoreSignals(task, MEDIUM_SIGNALS);
259
+ if (mediumMatches.length > 0) {
260
+ risk = 'medium';
261
+ riskSignals = mediumMatches;
262
+ }
263
+ else {
264
+ risk = 'low';
265
+ riskSignals = [];
266
+ }
267
+ }
76
268
  }
77
- const rationale = `tier: ${tierSignal}; risk: ${riskSignal}`;
78
- return { tier, risk, rationale };
269
+ const riskRationale = riskSignals.length > 0
270
+ ? `risk: ${risk} (matched: ${riskSignals.join(', ')})`
271
+ : `risk: ${risk} (no risk keyword matched — defaulting to low)`;
272
+ return {
273
+ tier,
274
+ risk,
275
+ rationale: `${tierRationale}; ${riskRationale}`,
276
+ };
79
277
  }
80
278
  //# sourceMappingURL=classify.js.map