honeydo 0.1.1 → 0.1.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honeydo",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "One CLI for every AI capability — chat, vision, image, video, audio. Built for AI agents.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -47,7 +47,28 @@ export declare const LAST_PROVIDER_PATH: string;
47
47
  * best-effort — the quota subtitle is an optimization, never an error.
48
48
  */
49
49
  export declare const QUOTA_CACHE_PATH: string;
50
- export type Subcommand = "agy" | "claude" | "api";
50
+ /** hermes 主配置文件(model + providers 段是本子命令的编辑目标)。 */
51
+ export declare const HERMES_CONFIG_PATH: string;
52
+ /** hermes env 文件(provider API key 的落点;全程保持 0o600)。 */
53
+ export declare const HERMES_ENV_PATH: string;
54
+ /** hermes cron 任务定义(重 pin 只读它;改动经 `hermes cron edit` 下发)。 */
55
+ export declare const HERMES_CRON_JOBS_PATH: string;
56
+ /** hermes 会话库(session_model_usage 表在此 —— 实测在顶层 state.db)。 */
57
+ export declare const HERMES_STATE_DB_PATH: string;
58
+ /** cc-switch name → hermes provider 字段映射(keyEnv/modelOverride)。 */
59
+ export declare const HERMES_PROVIDERS_REGISTRY_PATH: string;
60
+ /** 上一次切换的一跳记录(0o600,含旧 token 的 prevValue)。 */
61
+ export declare const HERMES_STATE_PATH: string;
62
+ /** hermes providers 条目的 transport 常量(cc-switch 只提供 anthropic 兼容端点)。 */
63
+ export declare const HERMES_TRANSPORT = "anthropic_messages";
64
+ /** 切换后验证 ping 的硬超时。 */
65
+ export declare const HERMES_VERIFY_TIMEOUT_MS = 60000;
66
+ /**
67
+ * 内置 seed(防推导造出与现有配置平行的条目):键是 cc-switch provider 名,
68
+ * 值是 hermes 侧已手工建立好的 id/keyEnv。
69
+ */
70
+ export declare const HERMES_PROVIDER_SEEDS: HermesProviderRegistry;
71
+ export type Subcommand = "agy" | "claude" | "api" | "hermes";
51
72
  export type SubcommandResult = {
52
73
  subcommand: Subcommand | undefined;
53
74
  rest: string[];
@@ -95,6 +116,66 @@ export type RawProvider = {
95
116
  name: string;
96
117
  settingsConfig: unknown;
97
118
  };
119
+ /** registry 单条:cc-switch name → hermes providers 条目字段。 */
120
+ export type HermesProviderRegistryEntry = {
121
+ id: string;
122
+ keyEnv: string;
123
+ modelOverride?: string;
124
+ };
125
+ /** `~/.config/gcli/hermes-providers.json` 的形状(损坏/缺失视为空)。 */
126
+ export type HermesProviderRegistry = Record<string, HermesProviderRegistryEntry>;
127
+ /** 一跳切换的一端(from/to 同构)。 */
128
+ export type HermesModelPoint = {
129
+ id: string;
130
+ model: string;
131
+ base_url: string;
132
+ };
133
+ /** 一条被重 pin 的 cron job 的旧 pin(rollback 回放用)。 */
134
+ export type CronRepinTarget = {
135
+ jobId: string;
136
+ prevProvider: string;
137
+ prevModel: string | null;
138
+ };
139
+ /** `~/.config/gcli/hermes-state.json`(0o600,env.prevValue 含旧 token)。 */
140
+ export type HermesStateFile = {
141
+ lastSwitch: {
142
+ ts: number;
143
+ ccName: string;
144
+ to: HermesModelPoint;
145
+ from: HermesModelPoint;
146
+ configBackup: string;
147
+ env: {
148
+ key: string;
149
+ prevValue: string | null;
150
+ };
151
+ cronRepinned: CronRepinTarget[];
152
+ };
153
+ };
154
+ /** editHermesConfig 的编辑指令。 */
155
+ export type HermesConfigEdit = {
156
+ model: {
157
+ default: string;
158
+ provider: string;
159
+ base_url: string;
160
+ };
161
+ provider: {
162
+ id: string;
163
+ name: string;
164
+ base_url: string;
165
+ transport: string;
166
+ key_env: string;
167
+ default_model: string;
168
+ };
169
+ };
170
+ /** parseHermesConfig 的读取结果(status / 冲突检测共用)。 */
171
+ export type HermesConfigInfo = {
172
+ model: {
173
+ default?: string;
174
+ provider?: string;
175
+ base_url?: string;
176
+ };
177
+ providerIds: string[];
178
+ };
98
179
  export type ProviderLookup = {
99
180
  ok: true;
100
181
  providers: RawProvider[];
@@ -166,6 +247,29 @@ export type RunDeps = {
166
247
  * after an arrow-key picker confirm, before any backend spawn.
167
248
  */
168
249
  writeLastProvider: (name: string) => Promise<void>;
250
+ /** Spawn `hermes <args>`(cron edit / gateway / -z ping),照 runClaude 的 timeout/SIGTERM 契约。 */
251
+ runHermes: (args: string[], timeoutMs?: number) => Promise<SpawnResult>;
252
+ /** 读 UTF-8 文本文件;缺失/不可读 → undefined(不 throw)。 */
253
+ readTextFile: (path: string) => Promise<string | undefined>;
254
+ /** 原子写(tmp + rename);mode 缺省时沿用目标文件现有权限(不存在则 0o600)。 */
255
+ writeTextFileAtomic: (path: string, text: string, mode?: number) => Promise<{
256
+ ok: true;
257
+ } | {
258
+ ok: false;
259
+ error: string;
260
+ }>;
261
+ /** 整文件复制(备份/整文件恢复)。 */
262
+ copyFile: (src: string, dest: string) => Promise<{
263
+ ok: true;
264
+ } | {
265
+ ok: false;
266
+ error: string;
267
+ }>;
268
+ /** 查 state.db session_model_usage 最新行(best-effort 验证);任何失败 → undefined。 */
269
+ queryLastSessionModel: () => Promise<{
270
+ model: string;
271
+ provider: string;
272
+ } | undefined>;
169
273
  };
170
274
  export type RunOutcome = {
171
275
  exitCode: number;
@@ -331,6 +435,17 @@ export interface ApiRequest {
331
435
  prompt: string;
332
436
  stream: boolean;
333
437
  timeoutMs: number;
438
+ /**
439
+ * Explicit thinking control: "off" sends {type:"disabled"}, "on" sends
440
+ * {type:"enabled", budget_tokens: floor(maxTokens/2)}. Undefined (= CLI
441
+ * `--thinking auto`) omits the field entirely and keeps the endpoint's
442
+ * default — which on bigmodel GLM endpoints means thinking ON, where
443
+ * thinking shares the max_tokens budget with text (small budgets can end
444
+ * up thinking-only, no text block).
445
+ */
446
+ thinking?: "off" | "on";
447
+ /** Transient-failure retries (network error / 408/429/5xx / malformed JSON / empty body). 0 = single attempt. */
448
+ retries?: number;
334
449
  }
335
450
  /**
336
451
  * Build the HTTP body for an anthropic-compatible /v1/messages request.
@@ -340,9 +455,12 @@ export interface ApiRequest {
340
455
  * high-quality output in ~45s vs claude-agent's 53min). We rely on a sufficient
341
456
  * --max-tokens budget (default 80000) to cover both thinking and text, not on
342
457
  * disabling thinking. Disabling it would discard the very capability we chose
343
- * k3 for.
458
+ * k3 for. (`--thinking auto`, the CLI default, preserves this omitted-field
459
+ * behaviour. bigmodel GLM endpoints default thinking ON and share max_tokens
460
+ * between thinking and text — small explicit budgets can end up thinking-only;
461
+ * --thinking off/on send explicit disabled/enabled for that case.)
344
462
  */
345
- export declare function buildApiBody(req: Pick<ApiRequest, "model" | "maxTokens" | "prompt" | "stream">): Record<string, unknown>;
463
+ export declare function buildApiBody(req: Pick<ApiRequest, "model" | "maxTokens" | "prompt" | "stream" | "thinking">): Record<string, unknown>;
346
464
  /**
347
465
  * Build the full URL for the messages endpoint. The cc-switch base_url is
348
466
  * stored with a trailing slash (e.g. `https://api.kimi.com/coding/`); we
@@ -368,6 +486,131 @@ export declare function extractTextDelta(line: string): string | null;
368
486
  * we concatenate every text block in order.
369
487
  */
370
488
  export declare function extractNonStreamText(body: unknown): string;
489
+ /** What a single SSE `data:` line contributes to the stream aggregation. */
490
+ export interface SseLineMeta {
491
+ /** Text fragment for content_block_delta/text_delta events, else null. */
492
+ text: string | null;
493
+ /** Characters contributed by a thinking_delta (thinking models spend the
494
+ * shared max_tokens budget here before any text is emitted). */
495
+ thinkingChars: number;
496
+ /** stop_reason carried by a message_delta event (e.g. "max_tokens"). */
497
+ stopReason?: string;
498
+ /** True when the line was a `data:` payload that JSON-parsed (any event type). */
499
+ parsed: boolean;
500
+ }
501
+ /**
502
+ * SSE line → structured meta (text delta + thinking volume + stop_reason).
503
+ *
504
+ * One JSON.parse per line feeding both the text aggregation and the "why did a
505
+ * stream produce no text" diagnosis (thinking-only streams that end in
506
+ * stop_reason=max_tokens are an endpoint budget issue, not a transport one).
507
+ * Non-`data:` lines / malformed JSON → empty meta.
508
+ */
509
+ export declare function extractSseLineMeta(line: string): SseLineMeta;
510
+ /** Non-streaming body → why extractNonStreamText found no text. */
511
+ export interface NoTextBodyInfo {
512
+ /** content block type names in order, comma-joined ("" = no content at all). */
513
+ blocks: string;
514
+ sawThinking: boolean;
515
+ stopReason?: string;
516
+ }
517
+ /**
518
+ * Inspect a 200 non-streaming messages body that yielded no text, so the error
519
+ * can say WHY (thinking-only under a starved max_tokens budget vs an empty
520
+ * content array from a flaky gateway — the former is deterministic, the latter
521
+ * is worth retrying).
522
+ */
523
+ export declare function describeNoTextBody(body: unknown): NoTextBodyInfo;
524
+ /** HTTP statuses worth an automatic retry (transient server/gateway trouble). */
525
+ export declare const API_RETRYABLE_STATUS: Set<number>;
526
+ /** Parsed options for the hermes subcommand (strict: unknown flags error). */
527
+ export interface ParsedHermesArgs {
528
+ /** Positional: provider name, or a reserved word (status/rollback). */
529
+ provider?: string;
530
+ model?: string;
531
+ dryRun: boolean;
532
+ verify: boolean;
533
+ keepOnFail: boolean;
534
+ help: boolean;
535
+ }
536
+ export type ParseHermesResult = ParsedHermesArgs | {
537
+ error: string;
538
+ };
539
+ /**
540
+ * Strip a trailing context-window marker like `[1M]` (a claude-agent
541
+ * convention cc-switch stores in model names; raw APIs reject it).
542
+ * Only a suffix anchored at the very end is removed.
543
+ */
544
+ export declare function stripContextSuffix(model: string): string;
545
+ /**
546
+ * Parse argv for `gcli hermes ...`. Strict like parseApiArgs (unknown flags
547
+ * are exit-2 errors — there is no backend to forward to). `--no-verify` is
548
+ * accepted via allowNegative. At most one positional.
549
+ */
550
+ export declare function parseHermesArgs(argv: string[]): ParseHermesResult;
551
+ /**
552
+ * Derive a hermes provider id from a cc-switch display name: lowercase,
553
+ * non-[a-z0-9] runs folded to `-`, leading/trailing dashes trimmed.
554
+ * May return "" for names with no alphanumerics (caller treats as error).
555
+ */
556
+ export declare function deriveHermesId(ccName: string): string;
557
+ /** Derive the .env key name from a hermes provider id: UPPER + `_API_KEY`. */
558
+ export declare function deriveKeyEnv(id: string): string;
559
+ /** Quote a scalar for our line-level YAML writer ('' escapes a single quote). */
560
+ export declare function quoteYamlScalar(value: string): string;
561
+ /**
562
+ * Targeted line-level YAML editor for hermes' config.yaml (zero-dep: no yaml
563
+ * lib guaranteed). Only the top-level `model:` (3 keys) and `providers:`
564
+ * (one entry) sections are touched; everything else is byte-preserved.
565
+ * Missing sections / unexpected nesting → {error}, never a guess (callers
566
+ * must not write on error). Idempotent by construction.
567
+ */
568
+ export declare function editHermesConfig(text: string, edit: HermesConfigEdit): {
569
+ text: string;
570
+ } | {
571
+ error: string;
572
+ };
573
+ /**
574
+ * Read (not edit) the parts of config.yaml the hermes backend needs: the
575
+ * current model section values and the list of providers entry ids.
576
+ * Missing model: section or unparseable model lines → {error}.
577
+ */
578
+ export declare function parseHermesConfig(text: string): {
579
+ info: HermesConfigInfo;
580
+ } | {
581
+ error: string;
582
+ };
583
+ /**
584
+ * Upsert one `KEY=value` line in a .env text (pure). Only active lines
585
+ * (`^KEY=`) match — commented-out occurrences are left alone; comments and
586
+ * blank lines are byte-preserved. value=null deletes the line(s); a missing
587
+ * key with a non-null value is appended at the end.
588
+ */
589
+ export declare function upsertEnvLines(text: string, key: string, value: string | null): string;
590
+ /**
591
+ * Plan cron re-pins: jobs that are enabled AND pinned to `oldProvider`
592
+ * (jobs.json shape `{jobs: [...]}`, a bare array tolerated). Malformed input
593
+ * yields an empty plan, never a throw.
594
+ */
595
+ export declare function buildCronRepinPlan(jobsJson: unknown, oldProvider: string): CronRepinTarget[];
596
+ /**
597
+ * Parse the provider registry (`hermes-providers.json`). Corrupt/missing
598
+ * content is best-effort → empty registry; entries lacking id/keyEnv are
599
+ * dropped.
600
+ */
601
+ export declare function parseHermesRegistry(text: string): HermesProviderRegistry;
602
+ export declare function serializeHermesRegistry(reg: HermesProviderRegistry): string;
603
+ /**
604
+ * Parse `hermes-state.json` STRICTLY (畸形 → {error},不猜): it carries the
605
+ * previous token and the rollback plan, so a half-broken state must not be
606
+ * acted on.
607
+ */
608
+ export declare function parseHermesStateFile(text: string): {
609
+ state: HermesStateFile;
610
+ } | {
611
+ error: string;
612
+ };
613
+ export declare function serializeHermesStateFile(s: HermesStateFile): string;
371
614
  export interface ParsedArgs {
372
615
  prompt?: string;
373
616
  model?: string;
@@ -399,6 +642,10 @@ export interface ParsedApiArgs {
399
642
  maxTokens: number;
400
643
  timeoutMs: number;
401
644
  stream: boolean;
645
+ /** `auto` (default) omits the thinking field; `off`/`on` send it explicitly. */
646
+ thinking: "auto" | "off" | "on";
647
+ /** Transient-failure retries (default 1; 0 disables). */
648
+ retries: number;
402
649
  version: boolean;
403
650
  help: boolean;
404
651
  /** --cwd was supplied (warned + ignored by the api backend). */
@@ -431,12 +678,21 @@ export declare function runClaudeInteractive(args: string[], cwd?: string): Prom
431
678
  *
432
679
  * - stream=true: reads the SSE body chunk-by-chunk, decodes UTF-8, splits on
433
680
  * newlines, and aggregates `text_delta` payloads into stdout. Two clocks
434
- * guard against hangs: an idle timer (reset on every text-bearing chunk)
435
- * and an absolute timer (timeoutMs). Either firing aborts the fetch via
436
- * AbortController → exit 1 with a timeout message.
681
+ * guard against hangs: an idle timer (reset on EVERY received chunk
682
+ * thinking models can emit long non-text stretches) and an absolute timer
683
+ * (timeoutMs). Either firing aborts the fetch via AbortController → exit 1
684
+ * with a timeout message.
437
685
  * - stream=false: awaits the full JSON body and extracts `content[].text`,
438
686
  * then applies the 50k-char truncation.
439
687
  *
688
+ * TRANSIENT failures are retried automatically (default 1 retry, `--retry N`):
689
+ * network-level fetch errors, HTTP 408/429/5xx, malformed JSON, empty-content
690
+ * 200 bodies, and streams that die or deliver no SSE events at all. Each retry
691
+ * note lands on stderr (stdout stays pipe-clean), so a recovered attempt still
692
+ * leaves a trace. Deterministic failures are NOT retried — timeouts, 4xx, and
693
+ * thinking-only responses (retrying cannot fix a max_tokens budget starved by
694
+ * endpoint-default thinking); those errors carry a diagnosis instead.
695
+ *
440
696
  * HTTP errors (non-2xx, network failure, abort) → exit 1 with diagnostics on
441
697
  * stderr; stdout stays empty so the caller's empty-output guard still works.
442
698
  */