honeydo 0.1.3 → 0.2.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/package.json +1 -1
- package/packages/gcli/dist/cli.d.ts +140 -10
- package/packages/gcli/dist/cli.js +515 -63
- package/packages/gcli/dist/cli.js.map +1 -1
- package/packages/lmedia/dist/index.js +15 -1
|
@@ -227,7 +227,7 @@ export function buildSettingsEnv(providerEnv, model) {
|
|
|
227
227
|
* - "return" / "enter" → confirm the current row (the physical Enter key
|
|
228
228
|
* emits "return" in raw mode; "enter" is the LF byte, which a tty may
|
|
229
229
|
* substitute for CR in input buffered before raw mode was enabled)
|
|
230
|
-
* - "escape" → skip (
|
|
230
|
+
* - "escape" → skip (退出,不启动后端)
|
|
231
231
|
* - Emacs (revise-2): ctrl+"n" ≡ down, ctrl+"p" ≡ up (same wrap); ctrl+"g"
|
|
232
232
|
* ≡ escape → skip; meta+"<" → first row (absolute), meta+">" → last row
|
|
233
233
|
* (absolute). Horizontal Emacs keys (C-f/C-b/C-a/C-e) and paging (C-v/M-v)
|
|
@@ -236,7 +236,7 @@ export function buildSettingsEnv(providerEnv, model) {
|
|
|
236
236
|
* exit 130)
|
|
237
237
|
*
|
|
238
238
|
* `index` is the highlighted row, `count` the total rendered rows INCLUDING
|
|
239
|
-
* the
|
|
239
|
+
* the footer row. Movement wraps with `(index±1+count)%count`; with
|
|
240
240
|
* count <= 0 moves are a noop (nothing is rendered).
|
|
241
241
|
*/
|
|
242
242
|
export function applyPickerKey(k, index, count) {
|
|
@@ -396,7 +396,9 @@ export function parseKimiUsages(body) {
|
|
|
396
396
|
}
|
|
397
397
|
/**
|
|
398
398
|
* Parse GLM `/api/monitor/usage/quota/limit` (C-Q2): data.limits[] entries
|
|
399
|
-
* with type
|
|
399
|
+
* with type "TOKENS_LIMIT" (coding-plan 订阅) or "CREDIT_LIMIT" (credit 资源
|
|
400
|
+
* 包) — billing type is per-account, fields are identical, same rendering
|
|
401
|
+
* (parity with statusline-sage); sorted by nextResetTime ascending — first is
|
|
400
402
|
* the short (5h) window, last the weekly one.
|
|
401
403
|
*/
|
|
402
404
|
export function parseGlmQuota(body) {
|
|
@@ -413,7 +415,7 @@ export function parseGlmQuota(body) {
|
|
|
413
415
|
if (typeof item !== "object" || item === null)
|
|
414
416
|
continue;
|
|
415
417
|
const r = item;
|
|
416
|
-
if (r.type !== "TOKENS_LIMIT")
|
|
418
|
+
if (r.type !== "TOKENS_LIMIT" && r.type !== "CREDIT_LIMIT")
|
|
417
419
|
continue;
|
|
418
420
|
const pct = toNum(r.percentage);
|
|
419
421
|
if (pct === undefined)
|
|
@@ -451,20 +453,90 @@ function formatReset(iso, nowMs) {
|
|
|
451
453
|
return rh > 0 ? `${d}d${rh}h` : `${d}d`;
|
|
452
454
|
}
|
|
453
455
|
/**
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
456
|
+
* Structured render input shared by the plain and the colored quota
|
|
457
|
+
* renderers: one percentage + one relative reset per window (each window's
|
|
458
|
+
* reset computed independently; missing/expired → omitted for that window).
|
|
459
|
+
* No windows at all → empty parts (the plain join then yields "").
|
|
460
|
+
*/
|
|
461
|
+
function quotaParts(q, nowMs) {
|
|
462
|
+
const shortPct = q.short?.pct;
|
|
463
|
+
const weeklyPct = q.weekly?.pct;
|
|
464
|
+
if (shortPct === undefined && weeklyPct === undefined)
|
|
465
|
+
return {};
|
|
466
|
+
return {
|
|
467
|
+
shortPct,
|
|
468
|
+
weeklyPct,
|
|
469
|
+
shortResetRel: q.short === undefined ? undefined : formatReset(q.short.resetIso, nowMs),
|
|
470
|
+
weeklyResetRel: q.weekly === undefined
|
|
471
|
+
? undefined
|
|
472
|
+
: formatReset(q.weekly.resetIso, nowMs),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Format quota windows as the menu subtitle (C-Q3, 2026-09 双窗重置):
|
|
477
|
+
* `5h:P% ↻<rel> wk:P% ↻<rel>` — each window carries its own reset time.
|
|
478
|
+
* Missing windows degrade; a missing/expired reset omits that window's ↻;
|
|
479
|
+
* no windows → "". Byte-exact contract with downstream consumers — the
|
|
480
|
+
* plain-text output is locked by unit tests; colors live in `colorQuota`,
|
|
481
|
+
* never here.
|
|
457
482
|
*/
|
|
458
483
|
export function formatQuota(q, nowMs) {
|
|
484
|
+
const p = quotaParts(q, nowMs);
|
|
459
485
|
const parts = [];
|
|
460
|
-
if (
|
|
461
|
-
parts.push(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
486
|
+
if (p.shortPct !== undefined) {
|
|
487
|
+
parts.push(p.shortResetRel === undefined
|
|
488
|
+
? `5h:${p.shortPct}%`
|
|
489
|
+
: `5h:${p.shortPct}% ↻${p.shortResetRel}`);
|
|
490
|
+
}
|
|
491
|
+
if (p.weeklyPct !== undefined) {
|
|
492
|
+
parts.push(p.weeklyResetRel === undefined
|
|
493
|
+
? `wk:${p.weeklyPct}%`
|
|
494
|
+
: `wk:${p.weeklyPct}% ↻${p.weeklyResetRel}`);
|
|
495
|
+
}
|
|
496
|
+
return parts.join(" ");
|
|
497
|
+
}
|
|
498
|
+
// Limit 染色阈值(对齐 statusline-sage 的 GLM_HIGH/GLM_MID)。
|
|
499
|
+
export const QUOTA_HIGH = 85;
|
|
500
|
+
export const QUOTA_MID = 60;
|
|
501
|
+
/** truecolor Sage 同款三色:#3A7D68 苔绿 / #D4920A 琥珀 / #D94F3D 朱红。 */
|
|
502
|
+
const COLOR_SAGE = "\x1b[38;2;58;125;104m";
|
|
503
|
+
const COLOR_AMBER = "\x1b[38;2;212;146;10m";
|
|
504
|
+
const COLOR_VERMILION = "\x1b[38;2;217;79;61m";
|
|
505
|
+
/** 关闭前景色回到终端默认,但不复位背景——选中行的整行高亮依赖这一点。 */
|
|
506
|
+
const FG_OFF = "\x1b[39m";
|
|
507
|
+
/**
|
|
508
|
+
* Limit 用量 → 段颜色:≥85 朱红、≥60 琥珀、其余(含非有限数)回退苔绿
|
|
509
|
+
* (阈值语义照搬 statusline-sage `_level_color`)。
|
|
510
|
+
*/
|
|
511
|
+
export function levelColor(pct) {
|
|
512
|
+
if (!Number.isFinite(pct))
|
|
513
|
+
return COLOR_SAGE;
|
|
514
|
+
if (pct >= QUOTA_HIGH)
|
|
515
|
+
return COLOR_VERMILION;
|
|
516
|
+
if (pct >= QUOTA_MID)
|
|
517
|
+
return COLOR_AMBER;
|
|
518
|
+
return COLOR_SAGE;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* `formatQuota` 的染色版(picker 用):`5h:P% ↻rel`/`wk:P% ↻rel` 每个窗口段
|
|
522
|
+
* 按各自 pct 独立选色,各自的重置时间恒 dim,不参与染色。结构与降级规则和
|
|
523
|
+
* `formatQuota` 一致(无窗口 → "")。行内使用 `\x1b[39m`/`\x1b[22m` 收尾而
|
|
524
|
+
* 非全复位,以免抹掉选中行背景。
|
|
525
|
+
*/
|
|
526
|
+
export function colorQuota(q, nowMs) {
|
|
527
|
+
const p = quotaParts(q, nowMs);
|
|
528
|
+
const parts = [];
|
|
529
|
+
if (p.shortPct !== undefined) {
|
|
530
|
+
parts.push(p.shortResetRel === undefined
|
|
531
|
+
? `${levelColor(p.shortPct)}5h:${p.shortPct}%${FG_OFF}`
|
|
532
|
+
: `${levelColor(p.shortPct)}5h:${p.shortPct}%${FG_OFF} \x1b[2m↻${p.shortResetRel}\x1b[22m`);
|
|
533
|
+
}
|
|
534
|
+
if (p.weeklyPct !== undefined) {
|
|
535
|
+
parts.push(p.weeklyResetRel === undefined
|
|
536
|
+
? `${levelColor(p.weeklyPct)}wk:${p.weeklyPct}%${FG_OFF}`
|
|
537
|
+
: `${levelColor(p.weeklyPct)}wk:${p.weeklyPct}%${FG_OFF} \x1b[2m↻${p.weeklyResetRel}\x1b[22m`);
|
|
538
|
+
}
|
|
539
|
+
return parts.join(" ");
|
|
468
540
|
}
|
|
469
541
|
/**
|
|
470
542
|
* Build claude argv (C8). `--model` is NOT passed here — for the claude
|
|
@@ -1405,12 +1477,20 @@ export function runClaude(args, timeoutMs, cwd) {
|
|
|
1405
1477
|
* Unlike runAgy/runClaude: no timeout, no stdout/stderr capture —
|
|
1406
1478
|
* the child owns the terminal. The child's exit code is passed
|
|
1407
1479
|
* through unchanged (SIGINT→130, SIGTERM→143 per shell convention).
|
|
1480
|
+
*
|
|
1481
|
+
* An inherited CLAUDE_CODE_CHILD_SESSION=1 is stripped: claude reads it as
|
|
1482
|
+
* "spawned sub-worker" and silently turns transcript saving off, so the
|
|
1483
|
+
* session never shows up in `claude --resume`. An attended TUI is a
|
|
1484
|
+
* top-level session, never a sub-worker. agy never reads the var, so the
|
|
1485
|
+
* strip is a no-op there; print mode keeps the env verbatim.
|
|
1408
1486
|
*/
|
|
1409
|
-
function spawnInteractive(bin, args, cwd) {
|
|
1487
|
+
export function spawnInteractive(bin, args, cwd) {
|
|
1488
|
+
const env = { ...process.env };
|
|
1489
|
+
delete env.CLAUDE_CODE_CHILD_SESSION;
|
|
1410
1490
|
return new Promise((resolveFn) => {
|
|
1411
1491
|
const child = spawn(bin, args, {
|
|
1412
1492
|
cwd,
|
|
1413
|
-
env
|
|
1493
|
+
env,
|
|
1414
1494
|
stdio: "inherit",
|
|
1415
1495
|
});
|
|
1416
1496
|
child.on("error", (err) => {
|
|
@@ -1870,17 +1950,122 @@ function readStdin() {
|
|
|
1870
1950
|
// ---------------------------------------------------------------------------
|
|
1871
1951
|
// Provider picker (TTY only) — arrow keys, zero deps
|
|
1872
1952
|
// ---------------------------------------------------------------------------
|
|
1953
|
+
// Picker frame colors: 16-color structure + truecolor Sage accents (B2).
|
|
1954
|
+
const DIM_ON = "\x1b[2m";
|
|
1955
|
+
const INTENSITY_OFF = "\x1b[22m";
|
|
1956
|
+
const CYAN = "\x1b[36m";
|
|
1957
|
+
const BOLD_ON = "\x1b[1m";
|
|
1958
|
+
const RESET_ALL = "\x1b[0m";
|
|
1959
|
+
/** 选中行背景 #292e42(truecolor)。 */
|
|
1960
|
+
const SELECTED_BG = "\x1b[48;2;41;46;66m";
|
|
1961
|
+
const PICKER_TITLE_MAIN = "◆ gcli";
|
|
1962
|
+
const PICKER_TITLE_SUB = " · 选择 provider";
|
|
1963
|
+
const PICKER_HINT_KEYS = "↑↓/j/k 移动 · Enter 确认 · Esc 不切换";
|
|
1964
|
+
const PICKER_HINT_SKIP = "Esc 不切换";
|
|
1965
|
+
const PICKER_SEPARATOR = "─".repeat(50);
|
|
1966
|
+
const NO_QUOTA_MARK = "—";
|
|
1967
|
+
/**
|
|
1968
|
+
* 给 picker 实际持有的 quota 文本(`formatQuota` 输出格式,byte-locked)上
|
|
1969
|
+
* 色:`5h:P%`/`wk:P%` token 按各自 pct 染色,`↻rel` token 恒 dim。与
|
|
1970
|
+
* `colorQuota` 同一视觉,只是入参是已格式化文本(DI 契约里 entries.quota
|
|
1971
|
+
* 为 string,渲染层拿不到结构化窗口)。前提:pct 为整数——生产 parsers
|
|
1972
|
+
* 均 Math.floor 保证;异形 token 优雅退化为不染色。
|
|
1973
|
+
*/
|
|
1974
|
+
function colorQuotaText(text) {
|
|
1975
|
+
return text
|
|
1976
|
+
.split(" ")
|
|
1977
|
+
.map((token) => {
|
|
1978
|
+
const win = /^(5h|wk):(\d+)%$/.exec(token);
|
|
1979
|
+
if (win !== null) {
|
|
1980
|
+
return `${levelColor(Number(win[2]))}${token}${FG_OFF}`;
|
|
1981
|
+
}
|
|
1982
|
+
if (token.startsWith("↻"))
|
|
1983
|
+
return `${DIM_ON}${token}${INTENSITY_OFF}`;
|
|
1984
|
+
return token;
|
|
1985
|
+
})
|
|
1986
|
+
.join(" ");
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* One entry row: `❯ `/two-space prefix | name column (max name width + 2,
|
|
1990
|
+
* left-aligned) | colored quota (or dim — placeholder) padded to the widest
|
|
1991
|
+
* quota across entries | dim ` <n>会话` (only when entry.sessions is set) |
|
|
1992
|
+
* cyan `●<tag>`. The quota pad (by JS `.length`) keeps the session column
|
|
1993
|
+
* start aligned across rows; padding sits outside the colored segments so
|
|
1994
|
+
* the dim placeholder stays contiguous. Selected rows add the truecolor
|
|
1995
|
+
* background + a bold name and end with a full reset; unselected rows keep
|
|
1996
|
+
* the same structure without background.
|
|
1997
|
+
*/
|
|
1998
|
+
function pickerEntryRow(entry, selected, nameWidth, quotaWidth, noColor) {
|
|
1999
|
+
const name = entry.name.padEnd(nameWidth);
|
|
2000
|
+
const quotaText = entry.quota !== undefined && entry.quota !== "" ? entry.quota : undefined;
|
|
2001
|
+
const pad = " ".repeat(Math.max(0, quotaWidth - (quotaText ?? NO_QUOTA_MARK).length));
|
|
2002
|
+
const quota = quotaText === undefined
|
|
2003
|
+
? noColor
|
|
2004
|
+
? NO_QUOTA_MARK
|
|
2005
|
+
: `${DIM_ON}${NO_QUOTA_MARK}${INTENSITY_OFF}`
|
|
2006
|
+
: noColor
|
|
2007
|
+
? quotaText
|
|
2008
|
+
: colorQuotaText(quotaText);
|
|
2009
|
+
const quotaColumn = `${quota}${pad}`;
|
|
2010
|
+
const sessions = entry.sessions === undefined
|
|
2011
|
+
? ""
|
|
2012
|
+
: noColor
|
|
2013
|
+
? ` ${entry.sessions}会话`
|
|
2014
|
+
: `${DIM_ON} ${entry.sessions}会话${INTENSITY_OFF}`;
|
|
2015
|
+
const tag = entry.tag !== undefined && entry.tag !== ""
|
|
2016
|
+
? noColor
|
|
2017
|
+
? ` ●${entry.tag}`
|
|
2018
|
+
: ` ${CYAN}●${entry.tag}${FG_OFF}`
|
|
2019
|
+
: "";
|
|
2020
|
+
if (noColor) {
|
|
2021
|
+
return `${selected ? "❯ " : " "}${name}${quotaColumn}${sessions}${tag}`;
|
|
2022
|
+
}
|
|
2023
|
+
return selected
|
|
2024
|
+
? `${SELECTED_BG}${COLOR_SAGE}❯${FG_OFF} ${BOLD_ON}${name}${INTENSITY_OFF}${quotaColumn}${sessions}${tag}${RESET_ALL}`
|
|
2025
|
+
: ` ${name}${quotaColumn}${sessions}${tag}`;
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* The full picker frame as plain lines: 标题 2 行 + dim 分隔线 + 条目 N 行 +
|
|
2029
|
+
* dim 末行提示 (non-selectable — skip 只经 Esc/C-g). With `unattributedCount >
|
|
2030
|
+
* 0` the footer reads `另有 N 个裸 claude 会话未归属 · Esc 不切换` (bare claude
|
|
2031
|
+
* processes carry no --settings and cannot be attributed to a provider);
|
|
2032
|
+
* otherwise it is plain `Esc 不切换`. Pure: no I/O; with `noColor` (NO_COLOR
|
|
2033
|
+
* downgrade) zero ANSI escapes — 仅纯文本排版,❯ 缩进 + 列对齐保留。Redraw
|
|
2034
|
+
* cursor-up height = `entries.length + 4` regardless (footer merges into the
|
|
2035
|
+
* one hint row).
|
|
2036
|
+
*/
|
|
2037
|
+
export function renderPickerRows(entries, selectedIndex, noColor, unattributedCount = 0) {
|
|
2038
|
+
const nameWidth = entries.reduce((w, e) => Math.max(w, e.name.length), 0) + 2;
|
|
2039
|
+
const quotaWidth = entries.reduce((w, e) => Math.max(w, (e.quota !== undefined && e.quota !== "" ? e.quota : NO_QUOTA_MARK)
|
|
2040
|
+
.length), 0);
|
|
2041
|
+
const rows = [
|
|
2042
|
+
noColor
|
|
2043
|
+
? `${PICKER_TITLE_MAIN}${PICKER_TITLE_SUB}`
|
|
2044
|
+
: `${CYAN}${BOLD_ON}${PICKER_TITLE_MAIN}${RESET_ALL}${PICKER_TITLE_SUB}`,
|
|
2045
|
+
noColor ? PICKER_HINT_KEYS : `${DIM_ON}${PICKER_HINT_KEYS}${INTENSITY_OFF}`,
|
|
2046
|
+
noColor ? PICKER_SEPARATOR : `${DIM_ON}${PICKER_SEPARATOR}${INTENSITY_OFF}`,
|
|
2047
|
+
];
|
|
2048
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2049
|
+
rows.push(pickerEntryRow(entries[i], i === selectedIndex, nameWidth, quotaWidth, noColor));
|
|
2050
|
+
}
|
|
2051
|
+
const footerText = unattributedCount > 0
|
|
2052
|
+
? `另有 ${unattributedCount} 个裸 claude 会话未归属 · ${PICKER_HINT_SKIP}`
|
|
2053
|
+
: PICKER_HINT_SKIP;
|
|
2054
|
+
rows.push(noColor ? footerText : `${DIM_ON}${footerText}${INTENSITY_OFF}`);
|
|
2055
|
+
return rows;
|
|
2056
|
+
}
|
|
1873
2057
|
/**
|
|
1874
2058
|
* Production deps.pickProvider: arrow-key menu rendered entirely on stderr
|
|
1875
2059
|
* (stdout stays pipe-clean). ↑↓/j/k move with wrap, Enter confirms, Esc
|
|
1876
2060
|
* skips, ctrl-c restores the terminal then exits 130; any other key is a
|
|
1877
|
-
* noop (C-P1).
|
|
2061
|
+
* noop (C-P1). Frame = 标题 2 行 + 分隔线 + 条目 N 行 + dim `Esc 不切换`
|
|
2062
|
+
* 末行提示——旧的可选中「不切换」行已退化为提示(skip 走 Esc/C-g),移动
|
|
2063
|
+
* 只覆盖条目行。
|
|
1878
2064
|
*
|
|
1879
2065
|
* Zero deps: `readline.emitKeypressEvents` + raw-mode stdin + hand-written
|
|
1880
|
-
* ANSI
|
|
1881
|
-
*
|
|
1882
|
-
*
|
|
1883
|
-
* pressing Esc) resolves {kind:"skip"}.
|
|
2066
|
+
* ANSI (16-color structure + truecolor Sage Limit 染色; `NO_COLOR` 非空 →
|
|
2067
|
+
* 全无色纯文本). Rows are redrawn in place (cursor-up + `\r` + clear-to-EOL
|
|
2068
|
+
* per line) so navigation leaves no ghosting (C-P3).
|
|
1884
2069
|
*
|
|
1885
2070
|
* Raw-mode lifecycle: `process.stdin.isRaw` is saved before
|
|
1886
2071
|
* `setRawMode(true)` and restored on EVERY exit path (confirm / Esc / ctrl-c
|
|
@@ -1888,51 +2073,74 @@ function readStdin() {
|
|
|
1888
2073
|
* its lazily-attached internal `data`) listeners are removed — so the
|
|
1889
2074
|
* spawned claude TUI takes stdin over cleanly. The picker always completes
|
|
1890
2075
|
* before any backend spawn.
|
|
2076
|
+
*
|
|
2077
|
+
* `unattributedCount` (optional) drives the bare-claude footer hint; 0 or
|
|
2078
|
+
* omitted renders the plain `Esc 不切换` footer. When the positional arg is
|
|
2079
|
+
* absent, the count is recovered from the entries themselves
|
|
2080
|
+
* (`PickerEntry.unattributed`, attached by the claude menu path) so two-arg
|
|
2081
|
+
* pickProvider wrappers still render the hint.
|
|
1891
2082
|
*/
|
|
1892
|
-
function pickProviderInteractive(entries, initialIndex) {
|
|
2083
|
+
export function pickProviderInteractive(entries, initialIndex, unattributedCount) {
|
|
1893
2084
|
return new Promise((resolvePromise) => {
|
|
1894
2085
|
const stderr = process.stderr;
|
|
1895
2086
|
const stdin = process.stdin;
|
|
1896
|
-
const
|
|
1897
|
-
|
|
2087
|
+
const noColor = (process.env.NO_COLOR ?? "") !== "";
|
|
2088
|
+
const frameUnattributed = unattributedCount ??
|
|
2089
|
+
entries.find((e) => e.unattributed !== undefined)?.unattributed ??
|
|
2090
|
+
0;
|
|
2091
|
+
const frameRows = entries.length + 4; // 标题2 + 分隔线1 + 条目N + 末行1
|
|
2092
|
+
let index = Math.min(Math.max(Math.trunc(initialIndex), 0), Math.max(entries.length - 1, 0)); // clamp (D4)
|
|
1898
2093
|
let settled = false;
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
const
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
const entry = entries[row];
|
|
1905
|
-
return entry.quota ? `${entry.name} ${entry.quota}` : entry.name;
|
|
1906
|
-
};
|
|
1907
|
-
// Selected row: ❯ + full-line inverse video; unselected: two-space indent
|
|
1908
|
-
// (C-P7). \x1b[K clears to EOL so a shorter previous render leaves no
|
|
1909
|
-
// ghosting (残影).
|
|
1910
|
-
const rowText = (row) => {
|
|
1911
|
-
const label = labelOf(row);
|
|
1912
|
-
return row === index
|
|
1913
|
-
? `\x1b[7m❯ ${label}\x1b[27m\x1b[K`
|
|
1914
|
-
: ` ${label}\x1b[K`;
|
|
1915
|
-
};
|
|
1916
|
-
const drawRows = () => {
|
|
1917
|
-
for (let row = 0; row < rowCount; row++) {
|
|
1918
|
-
stderr.write(`${rowText(row)}\n`);
|
|
2094
|
+
// Full repaint: pure row construction + per-line clear-to-EOL so a
|
|
2095
|
+
// shorter previous render leaves no ghosting (残影).
|
|
2096
|
+
const drawFrame = () => {
|
|
2097
|
+
for (const line of renderPickerRows(entries, index, noColor, frameUnattributed)) {
|
|
2098
|
+
stderr.write(`${line}\x1b[K\n`);
|
|
1919
2099
|
}
|
|
1920
2100
|
};
|
|
1921
2101
|
// In-place redraw: the cursor sits just below the last row after each
|
|
1922
|
-
// draw, so move it back up over every
|
|
2102
|
+
// draw, so move it back up over every frame line before repainting.
|
|
1923
2103
|
const redraw = () => {
|
|
1924
|
-
stderr.write(`\x1b[${
|
|
1925
|
-
|
|
2104
|
+
stderr.write(`\x1b[${frameRows}A\r`);
|
|
2105
|
+
drawFrame();
|
|
1926
2106
|
};
|
|
1927
2107
|
// Raw-mode lifecycle (C-P3): save → raw → ... EVERY exit path restores.
|
|
1928
2108
|
const wasRaw = stdin.isRaw === true;
|
|
1929
2109
|
const dataListenersBefore = stdin.listeners("data");
|
|
1930
2110
|
let onKeypress;
|
|
1931
2111
|
let onGone = () => { };
|
|
2112
|
+
// 孤立 ESC 快速路径:emitKeypressEvents 不带 interface 参数时没有 escape
|
|
2113
|
+
// 消歧超时,真实终端按 Esc 发出的孤立 \x1b 会被状态机无限挂起(keypress
|
|
2114
|
+
// 永不发出)——实测 PTY 下 30s 无事件。因此监听 data 层:单字节 \x1b 且
|
|
2115
|
+
// 50ms 内无后续字节 → 视为 Esc 键 skip;有后续(箭头序列拆包等)→ 取消
|
|
2116
|
+
// 计时器交还 readline 正常解析。readline 的 name==="escape" 路径保留,
|
|
2117
|
+
// settled 守卫防双触发。
|
|
2118
|
+
let escTimer;
|
|
2119
|
+
const onDataFastEsc = (chunk) => {
|
|
2120
|
+
const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
2121
|
+
if (s !== "\x1b") {
|
|
2122
|
+
if (escTimer !== undefined) {
|
|
2123
|
+
clearTimeout(escTimer);
|
|
2124
|
+
escTimer = undefined;
|
|
2125
|
+
}
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
if (escTimer !== undefined)
|
|
2129
|
+
clearTimeout(escTimer);
|
|
2130
|
+
escTimer = setTimeout(() => {
|
|
2131
|
+
escTimer = undefined;
|
|
2132
|
+
finish({ kind: "skip" });
|
|
2133
|
+
}, 50);
|
|
2134
|
+
};
|
|
1932
2135
|
const cleanup = () => {
|
|
1933
2136
|
if (onKeypress !== undefined) {
|
|
1934
2137
|
stdin.removeListener("keypress", onKeypress);
|
|
1935
2138
|
}
|
|
2139
|
+
if (escTimer !== undefined) {
|
|
2140
|
+
clearTimeout(escTimer);
|
|
2141
|
+
escTimer = undefined;
|
|
2142
|
+
}
|
|
2143
|
+
stdin.removeListener("data", onDataFastEsc);
|
|
1936
2144
|
stdin.removeListener("close", onGone);
|
|
1937
2145
|
stdin.removeListener("error", onGone);
|
|
1938
2146
|
// emitKeypressEvents lazily attaches an internal 'data' listener (via
|
|
@@ -1955,9 +2163,8 @@ function pickProviderInteractive(entries, initialIndex) {
|
|
|
1955
2163
|
cleanup();
|
|
1956
2164
|
resolvePromise(outcome);
|
|
1957
2165
|
};
|
|
1958
|
-
// Title + hint drawn once; entry rows below it (C-P7).
|
|
1959
|
-
|
|
1960
|
-
drawRows();
|
|
2166
|
+
// Title + hint drawn once; entry rows below it get repainted (C-P7).
|
|
2167
|
+
drawFrame();
|
|
1961
2168
|
emitKeypressEvents(stdin);
|
|
1962
2169
|
onKeypress = (_str, key) => {
|
|
1963
2170
|
try {
|
|
@@ -1968,13 +2175,15 @@ function pickProviderInteractive(entries, initialIndex) {
|
|
|
1968
2175
|
}
|
|
1969
2176
|
if (key === undefined)
|
|
1970
2177
|
return;
|
|
1971
|
-
|
|
2178
|
+
// Movement covers entry rows only (the 末行提示 is not selectable).
|
|
2179
|
+
const action = applyPickerKey(key, index, entries.length);
|
|
1972
2180
|
if (action.type === "move") {
|
|
1973
2181
|
index = action.index;
|
|
1974
2182
|
redraw();
|
|
1975
2183
|
}
|
|
1976
2184
|
else if (action.type === "confirm") {
|
|
1977
|
-
//
|
|
2185
|
+
// index is always an entry row; the guard only matters for the
|
|
2186
|
+
// empty-list edge (nothing selectable → skip).
|
|
1978
2187
|
finish(index < entries.length
|
|
1979
2188
|
? { kind: "select", entry: entries[index] }
|
|
1980
2189
|
: { kind: "skip" });
|
|
@@ -1994,12 +2203,213 @@ function pickProviderInteractive(entries, initialIndex) {
|
|
|
1994
2203
|
// skip rather than hang.
|
|
1995
2204
|
onGone = () => finish({ kind: "skip" });
|
|
1996
2205
|
stdin.on("keypress", onKeypress);
|
|
2206
|
+
stdin.on("data", onDataFastEsc);
|
|
1997
2207
|
stdin.once("close", onGone);
|
|
1998
2208
|
stdin.once("error", onGone);
|
|
1999
2209
|
if (stdin.isTTY)
|
|
2000
2210
|
stdin.setRawMode(true);
|
|
2001
2211
|
});
|
|
2002
2212
|
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Extract the `--settings <json>` value from one joined ps argv line: scan
|
|
2215
|
+
* from the first `{` after `--settings` with string-aware bracket balancing
|
|
2216
|
+
* (ps concatenates argv with spaces, so the JSON may contain any whitespace,
|
|
2217
|
+
* and the payload itself nests objects). Returns undefined when the line has
|
|
2218
|
+
* no `--settings`, no `{` after it, or never closes — the caller treats that
|
|
2219
|
+
* as unattributable (honest: `--settings <file>` cannot be attributed either).
|
|
2220
|
+
*/
|
|
2221
|
+
function extractSettingsJson(line) {
|
|
2222
|
+
const marker = line.indexOf("--settings");
|
|
2223
|
+
if (marker === -1)
|
|
2224
|
+
return undefined;
|
|
2225
|
+
const start = line.indexOf("{", marker);
|
|
2226
|
+
if (start === -1)
|
|
2227
|
+
return undefined;
|
|
2228
|
+
let depth = 0;
|
|
2229
|
+
let inString = false;
|
|
2230
|
+
let escaped = false;
|
|
2231
|
+
for (let i = start; i < line.length; i++) {
|
|
2232
|
+
const ch = line[i];
|
|
2233
|
+
if (inString) {
|
|
2234
|
+
if (escaped)
|
|
2235
|
+
escaped = false;
|
|
2236
|
+
else if (ch === "\\")
|
|
2237
|
+
escaped = true;
|
|
2238
|
+
else if (ch === '"')
|
|
2239
|
+
inString = false;
|
|
2240
|
+
continue;
|
|
2241
|
+
}
|
|
2242
|
+
if (ch === '"')
|
|
2243
|
+
inString = true;
|
|
2244
|
+
else if (ch === "{")
|
|
2245
|
+
depth++;
|
|
2246
|
+
else if (ch === "}") {
|
|
2247
|
+
depth--;
|
|
2248
|
+
if (depth === 0)
|
|
2249
|
+
return line.slice(start, i + 1);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
return undefined;
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Whether one process's settings env matches a provider: baseUrl strict-equal
|
|
2256
|
+
* AND (authToken strict-equal OR apiKey strict-equal) — empty/missing tokens
|
|
2257
|
+
* never participate, so a bare URL can never match anything (same-domain
|
|
2258
|
+
* dual-plan providers are told apart by token).
|
|
2259
|
+
*/
|
|
2260
|
+
function sessionProviderMatches(provider, baseUrl, authToken, apiKey) {
|
|
2261
|
+
if (baseUrl === undefined || provider.baseUrl !== baseUrl)
|
|
2262
|
+
return false;
|
|
2263
|
+
const tokenHit = authToken !== undefined &&
|
|
2264
|
+
authToken !== "" &&
|
|
2265
|
+
provider.authToken === authToken;
|
|
2266
|
+
const keyHit = apiKey !== undefined && apiKey !== "" && provider.apiKey === apiKey;
|
|
2267
|
+
return tokenHit || keyHit;
|
|
2268
|
+
}
|
|
2269
|
+
/**
|
|
2270
|
+
* Attribute joined ps argv lines to providers (pure, total — never throws).
|
|
2271
|
+
* Lines with a parseable `--settings` env matching a provider increment that
|
|
2272
|
+
* provider's count; providers with identical URL+token each count the same
|
|
2273
|
+
* line (accepted edge). Bare claude lines, unparseable JSON, and settings
|
|
2274
|
+
* matching no provider all count as `unattributed`.
|
|
2275
|
+
*/
|
|
2276
|
+
export function parseSessionAttribution(lines, providers) {
|
|
2277
|
+
const counts = {};
|
|
2278
|
+
let unattributed = 0;
|
|
2279
|
+
for (const line of lines) {
|
|
2280
|
+
const json = extractSettingsJson(line);
|
|
2281
|
+
if (json === undefined) {
|
|
2282
|
+
unattributed++;
|
|
2283
|
+
continue;
|
|
2284
|
+
}
|
|
2285
|
+
let parsed;
|
|
2286
|
+
try {
|
|
2287
|
+
parsed = JSON.parse(json);
|
|
2288
|
+
}
|
|
2289
|
+
catch {
|
|
2290
|
+
unattributed++;
|
|
2291
|
+
continue;
|
|
2292
|
+
}
|
|
2293
|
+
const env = typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
2294
|
+
? parsed.env
|
|
2295
|
+
: undefined;
|
|
2296
|
+
if (typeof env !== "object" || env === null || Array.isArray(env)) {
|
|
2297
|
+
unattributed++;
|
|
2298
|
+
continue;
|
|
2299
|
+
}
|
|
2300
|
+
const rec = env;
|
|
2301
|
+
const baseUrl = typeof rec.ANTHROPIC_BASE_URL === "string"
|
|
2302
|
+
? rec.ANTHROPIC_BASE_URL
|
|
2303
|
+
: undefined;
|
|
2304
|
+
const authToken = typeof rec.ANTHROPIC_AUTH_TOKEN === "string"
|
|
2305
|
+
? rec.ANTHROPIC_AUTH_TOKEN
|
|
2306
|
+
: undefined;
|
|
2307
|
+
const apiKey = typeof rec.ANTHROPIC_API_KEY === "string"
|
|
2308
|
+
? rec.ANTHROPIC_API_KEY
|
|
2309
|
+
: undefined;
|
|
2310
|
+
const hits = providers.filter((p) => sessionProviderMatches(p, baseUrl, authToken, apiKey));
|
|
2311
|
+
if (hits.length === 0) {
|
|
2312
|
+
unattributed++;
|
|
2313
|
+
continue;
|
|
2314
|
+
}
|
|
2315
|
+
for (const p of hits) {
|
|
2316
|
+
counts[p.name] = (counts[p.name] ?? 0) + 1;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
return { counts, unattributed };
|
|
2320
|
+
}
|
|
2321
|
+
/**
|
|
2322
|
+
* Glue for the claude TTY menu path: scan live processes once and attribute
|
|
2323
|
+
* them. Any failure (deps capability missing, spawn error, non-zero pgrep/ps)
|
|
2324
|
+
* resolves to undefined — the caller degrades to no session column and no
|
|
2325
|
+
* bare-session footer (cannot distinguish "none" from "scan failed"; prefer
|
|
2326
|
+
* silence over a wrong number). Never throws.
|
|
2327
|
+
*/
|
|
2328
|
+
async function countProviderSessions(deps, providers) {
|
|
2329
|
+
if (deps.listClaudeProcessArgs === undefined)
|
|
2330
|
+
return undefined;
|
|
2331
|
+
try {
|
|
2332
|
+
const lines = await deps.listClaudeProcessArgs();
|
|
2333
|
+
return parseSessionAttribution(lines, providers);
|
|
2334
|
+
}
|
|
2335
|
+
catch {
|
|
2336
|
+
return undefined;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Production deps.listClaudeProcessArgs: `pgrep -x claude` for pids, then
|
|
2341
|
+
* `ps -ww -o command= -p <pids>` (BSD flags; `-ww` defeats column truncation
|
|
2342
|
+
* so long argv carrying `--settings` JSON survives). Read-only, no secrets in
|
|
2343
|
+
* the commands themselves; ps output (which contains tokens) stays in memory.
|
|
2344
|
+
* Exit semantics: pgrep 1 = no match → []; pgrep >1 or any ps failure →
|
|
2345
|
+
* rejects (error text carries only exit codes / ps stderr — never ps stdout).
|
|
2346
|
+
*/
|
|
2347
|
+
function listClaudeProcessArgsOnDisk() {
|
|
2348
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
2349
|
+
const pgrep = spawn("pgrep", ["-x", CLAUDE_BIN], {
|
|
2350
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2351
|
+
});
|
|
2352
|
+
let pidsOut = "";
|
|
2353
|
+
let pgrepErr = "";
|
|
2354
|
+
pgrep.stdout.on("data", (chunk) => {
|
|
2355
|
+
pidsOut += chunk.toString();
|
|
2356
|
+
});
|
|
2357
|
+
pgrep.stderr.on("data", (chunk) => {
|
|
2358
|
+
pgrepErr += chunk.toString();
|
|
2359
|
+
});
|
|
2360
|
+
pgrep.on("error", (err) => {
|
|
2361
|
+
rejectPromise(new Error(`pgrep failed to spawn: ${err.message}`));
|
|
2362
|
+
});
|
|
2363
|
+
pgrep.on("close", (code) => {
|
|
2364
|
+
if (code === 1) {
|
|
2365
|
+
resolvePromise([]); // no processes matched — not an error
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
if (code !== 0) {
|
|
2369
|
+
const msg = pgrepErr.trim();
|
|
2370
|
+
rejectPromise(new Error(msg
|
|
2371
|
+
? `pgrep exited with code ${code}: ${msg}`
|
|
2372
|
+
: `pgrep exited with code ${code}`));
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
const pids = pidsOut
|
|
2376
|
+
.split("\n")
|
|
2377
|
+
.map((s) => s.trim())
|
|
2378
|
+
.filter((s) => s !== "");
|
|
2379
|
+
if (pids.length === 0) {
|
|
2380
|
+
resolvePromise([]);
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
const ps = spawn("ps", ["-ww", "-o", "command=", "-p", pids.join(",")], {
|
|
2384
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2385
|
+
});
|
|
2386
|
+
let psOut = "";
|
|
2387
|
+
let psErr = "";
|
|
2388
|
+
ps.stdout.on("data", (chunk) => {
|
|
2389
|
+
psOut += chunk.toString();
|
|
2390
|
+
});
|
|
2391
|
+
ps.stderr.on("data", (chunk) => {
|
|
2392
|
+
psErr += chunk.toString();
|
|
2393
|
+
});
|
|
2394
|
+
ps.on("error", (err) => {
|
|
2395
|
+
rejectPromise(new Error(`ps failed to spawn: ${err.message}`));
|
|
2396
|
+
});
|
|
2397
|
+
ps.on("close", (psCode) => {
|
|
2398
|
+
if (psCode !== 0) {
|
|
2399
|
+
const msg = psErr.trim();
|
|
2400
|
+
rejectPromise(new Error(msg
|
|
2401
|
+
? `ps exited with code ${psCode}: ${msg}`
|
|
2402
|
+
: `ps exited with code ${psCode}`));
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
resolvePromise(psOut
|
|
2406
|
+
.split("\n")
|
|
2407
|
+
.map((s) => s.trimEnd())
|
|
2408
|
+
.filter((s) => s !== ""));
|
|
2409
|
+
});
|
|
2410
|
+
});
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2003
2413
|
// ---------------------------------------------------------------------------
|
|
2004
2414
|
// Last-provider memory (D2) — production deps.readLastProvider/writeLastProvider
|
|
2005
2415
|
// ---------------------------------------------------------------------------
|
|
@@ -2315,8 +2725,8 @@ claude backend (default; bare \`gcli ...\` === \`gcli claude ...\`):
|
|
|
2315
2725
|
does NOT rewrite ~/.claude/settings.json.
|
|
2316
2726
|
- No --provider in a TTY: gcli lists all cc-switch providers (in
|
|
2317
2727
|
cc-switch's own DB order) in an arrow-key picker (↑↓/j/k and Emacs
|
|
2318
|
-
C-n/C-p move · Enter 确认 · Esc/C-g =
|
|
2319
|
-
|
|
2728
|
+
C-n/C-p move · Enter 确认 · Esc/C-g = 退出(不启动 claude);
|
|
2729
|
+
M-</M-> jump to first/last; ctrl-c exits 130). In print mode
|
|
2320
2730
|
(-p) the last confirmed provider is reused silently (one stderr hint
|
|
2321
2731
|
line); the menu only pops when nothing valid is remembered, or when
|
|
2322
2732
|
--pick is given. Without a TTY the picker never triggers — no prompt,
|
|
@@ -2566,6 +2976,7 @@ async function runClaudeBackend(parsed, deps) {
|
|
|
2566
2976
|
}
|
|
2567
2977
|
}
|
|
2568
2978
|
let settingsEnv;
|
|
2979
|
+
let pickerSkipped = false;
|
|
2569
2980
|
let pickerWarning;
|
|
2570
2981
|
if (parsed.provider !== undefined) {
|
|
2571
2982
|
const validated = validateProviderName(parsed.provider);
|
|
@@ -2666,17 +3077,45 @@ async function runClaudeBackend(parsed, deps) {
|
|
|
2666
3077
|
quotaItems.push({ name: p.name, env: envResult.env });
|
|
2667
3078
|
}
|
|
2668
3079
|
}
|
|
2669
|
-
|
|
3080
|
+
// Session attribution runs once, in parallel with the quota fetch,
|
|
3081
|
+
// on this menu path only. Any failure resolves to undefined and the
|
|
3082
|
+
// picker degrades to no session column (same soft-degrade as quota).
|
|
3083
|
+
const sessionProviders = quotaItems.map((q) => ({
|
|
3084
|
+
name: q.name,
|
|
3085
|
+
baseUrl: q.env.ANTHROPIC_BASE_URL,
|
|
3086
|
+
authToken: q.env.ANTHROPIC_AUTH_TOKEN,
|
|
3087
|
+
apiKey: q.env.ANTHROPIC_API_KEY,
|
|
3088
|
+
}));
|
|
3089
|
+
const [quotaMap, attribution] = await Promise.all([
|
|
3090
|
+
deps.fetchProviderQuotas(quotaItems),
|
|
3091
|
+
countProviderSessions(deps, sessionProviders),
|
|
3092
|
+
]);
|
|
2670
3093
|
// D5/D6 (revise-3): rows are name + quota subtitle (host is gone);
|
|
2671
|
-
// the remembered row
|
|
3094
|
+
// the remembered row carries its ●上次 marker as a structured tag
|
|
3095
|
+
// (rendering decides placement/color; quota text stays byte-clean).
|
|
3096
|
+
// Live-session counts ride the same rows (0 included); undefined
|
|
3097
|
+
// when the scan was unavailable so the column degrades per entry.
|
|
3098
|
+
// The frame-level bare-claude count rides every entry too (same
|
|
3099
|
+
// value) so the footer hint survives pickProvider wrappers that
|
|
3100
|
+
// forward only (entries, initialIndex); undefined → degrade to 0.
|
|
2672
3101
|
const entries = ordered.map((p) => {
|
|
2673
|
-
|
|
3102
|
+
const entry = {
|
|
3103
|
+
name: p.name,
|
|
3104
|
+
quota: quotaMap.get(p.name),
|
|
3105
|
+
sessions: attribution === undefined
|
|
3106
|
+
? undefined
|
|
3107
|
+
: (attribution.counts[p.name] ?? 0),
|
|
3108
|
+
unattributed: attribution?.unattributed,
|
|
3109
|
+
};
|
|
2674
3110
|
if (remembered !== undefined && p.name === remembered) {
|
|
2675
|
-
|
|
3111
|
+
entry.tag = "上次";
|
|
2676
3112
|
}
|
|
2677
|
-
return
|
|
3113
|
+
return entry;
|
|
2678
3114
|
});
|
|
2679
|
-
const picked = await deps.pickProvider(entries, memoryValid ? memoryIndex : 0
|
|
3115
|
+
const picked = await deps.pickProvider(entries, memoryValid ? memoryIndex : 0,
|
|
3116
|
+
// Degraded scan → 0: cannot distinguish "no bare sessions" from
|
|
3117
|
+
// "scan failed", so the footer hint stays silent either way.
|
|
3118
|
+
attribution?.unattributed ?? 0);
|
|
2680
3119
|
if (picked.kind === "select") {
|
|
2681
3120
|
// Exact-name lookup (no matchProviderName): the picker returns
|
|
2682
3121
|
// an entry straight from this list.
|
|
@@ -2694,7 +3133,10 @@ async function runClaudeBackend(parsed, deps) {
|
|
|
2694
3133
|
await deps.writeLastProvider(picked.entry.name);
|
|
2695
3134
|
}
|
|
2696
3135
|
}
|
|
2697
|
-
|
|
3136
|
+
else {
|
|
3137
|
+
pickerSkipped = true; // Esc/C-g → 退出,不启动 claude
|
|
3138
|
+
}
|
|
3139
|
+
// skip 语义(2026-09 用户裁定):退出。memory untouched.
|
|
2698
3140
|
}
|
|
2699
3141
|
}
|
|
2700
3142
|
}
|
|
@@ -2713,6 +3155,15 @@ async function runClaudeBackend(parsed, deps) {
|
|
|
2713
3155
|
...o,
|
|
2714
3156
|
stderr: o.stderr ? `${pickerWarning}\n${o.stderr}` : pickerWarning,
|
|
2715
3157
|
};
|
|
3158
|
+
// Esc/C-g skip = 退出(2026-09 用户裁定,替代旧「不切换照常启动」语义):
|
|
3159
|
+
// 不 spawn 任何后端,干净退出 exit 0。stderr 提示确认动作已被感知。
|
|
3160
|
+
if (pickerSkipped) {
|
|
3161
|
+
return withPickerWarning({
|
|
3162
|
+
exitCode: 0,
|
|
3163
|
+
stdout: "",
|
|
3164
|
+
stderr: "gcli: 已退出(未启动 claude)",
|
|
3165
|
+
});
|
|
3166
|
+
}
|
|
2716
3167
|
const cwdAbs = parsed.cwd ? resolve(parsed.cwd) : undefined;
|
|
2717
3168
|
// Interactive mode (no -p in a TTY): inherit stdio, pass exit code through.
|
|
2718
3169
|
if (prompt === undefined) {
|
|
@@ -3411,8 +3862,9 @@ async function main() {
|
|
|
3411
3862
|
runClaudeInteractive: (args, cwd) => runClaudeInteractive(args, cwd),
|
|
3412
3863
|
runAgyInteractive: (args, cwd) => runAgyInteractive(args, cwd),
|
|
3413
3864
|
isInteractive: () => process.stdin.isTTY === true,
|
|
3414
|
-
pickProvider: (entries, initialIndex) => pickProviderInteractive(entries, initialIndex),
|
|
3865
|
+
pickProvider: (entries, initialIndex, unattributedCount) => pickProviderInteractive(entries, initialIndex, unattributedCount),
|
|
3415
3866
|
fetchProviderQuotas: (items) => fetchProviderQuotasHttp(items),
|
|
3867
|
+
listClaudeProcessArgs: () => listClaudeProcessArgsOnDisk(),
|
|
3416
3868
|
readLastProvider: () => readLastProviderFromDisk(),
|
|
3417
3869
|
writeLastProvider: (name) => writeLastProviderToDisk(name),
|
|
3418
3870
|
runHermes: (args, timeoutMs) => runHermesProcess(args, timeoutMs ?? DEFAULT_TIMEOUT_MS),
|