honeydo 0.1.1 → 0.1.3

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.3",
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": [
@@ -6,8 +6,9 @@
6
6
  * `gcli agy ...` routes to the agy CLI (explicit subcommand required); the
7
7
  * claude backend can switch cc-switch providers inline via
8
8
  * `claude -p ... --settings`. Adds value over calling the backends directly:
9
- * prompt via argv or stdin (`-p -`), 50k-char output truncation, a hard
10
- * timeout (spawn SIGTERM), explicit exit codes, and empty-output detection.
9
+ * prompt via argv or stdin (`-p -`), a hard timeout (spawn SIGTERM), explicit
10
+ * exit codes, and empty-output detection. Output is passed through unmodified
11
+ * — size limits are the endpoint's business, not ours.
11
12
  *
12
13
  * The claude backend resolves `--provider <name>` from the cc-switch SQLite
13
14
  * DB (read-only) and never rewrites ~/.claude/settings.json — the provider
@@ -25,7 +26,6 @@
25
26
  * backend's own timeout flag.
26
27
  */
27
28
  export declare const DEFAULT_TIMEOUT_MS = 300000;
28
- export declare const CHARACTER_LIMIT = 50000;
29
29
  export declare const API_DEFAULT_MAX_TOKENS = 80000;
30
30
  /**
31
31
  * Path to the cc-switch SQLite database. cc-switch ships provider configs
@@ -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;
@@ -185,7 +289,6 @@ export type RunOutcome = {
185
289
  * prompt="claude" — the subcommand must literally lead.
186
290
  */
187
291
  export declare function parseSubcommand(argv: string[]): SubcommandResult;
188
- export declare function truncate(text: string): string;
189
292
  export interface GcliOptions {
190
293
  /** Omit for interactive mode (no -p emitted). */
191
294
  prompt?: string;
@@ -331,6 +434,17 @@ export interface ApiRequest {
331
434
  prompt: string;
332
435
  stream: boolean;
333
436
  timeoutMs: number;
437
+ /**
438
+ * Explicit thinking control: "off" sends {type:"disabled"}, "on" sends
439
+ * {type:"enabled", budget_tokens: floor(maxTokens/2)}. Undefined (= CLI
440
+ * `--thinking auto`) omits the field entirely and keeps the endpoint's
441
+ * default — which on bigmodel GLM endpoints means thinking ON, where
442
+ * thinking shares the max_tokens budget with text (small budgets can end
443
+ * up thinking-only, no text block).
444
+ */
445
+ thinking?: "off" | "on";
446
+ /** Transient-failure retries (network error / 408/429/5xx / malformed JSON / empty body). 0 = single attempt. */
447
+ retries?: number;
334
448
  }
335
449
  /**
336
450
  * Build the HTTP body for an anthropic-compatible /v1/messages request.
@@ -340,9 +454,12 @@ export interface ApiRequest {
340
454
  * high-quality output in ~45s vs claude-agent's 53min). We rely on a sufficient
341
455
  * --max-tokens budget (default 80000) to cover both thinking and text, not on
342
456
  * disabling thinking. Disabling it would discard the very capability we chose
343
- * k3 for.
457
+ * k3 for. (`--thinking auto`, the CLI default, preserves this omitted-field
458
+ * behaviour. bigmodel GLM endpoints default thinking ON and share max_tokens
459
+ * between thinking and text — small explicit budgets can end up thinking-only;
460
+ * --thinking off/on send explicit disabled/enabled for that case.)
344
461
  */
345
- export declare function buildApiBody(req: Pick<ApiRequest, "model" | "maxTokens" | "prompt" | "stream">): Record<string, unknown>;
462
+ export declare function buildApiBody(req: Pick<ApiRequest, "model" | "maxTokens" | "prompt" | "stream" | "thinking">): Record<string, unknown>;
346
463
  /**
347
464
  * Build the full URL for the messages endpoint. The cc-switch base_url is
348
465
  * stored with a trailing slash (e.g. `https://api.kimi.com/coding/`); we
@@ -368,6 +485,131 @@ export declare function extractTextDelta(line: string): string | null;
368
485
  * we concatenate every text block in order.
369
486
  */
370
487
  export declare function extractNonStreamText(body: unknown): string;
488
+ /** What a single SSE `data:` line contributes to the stream aggregation. */
489
+ export interface SseLineMeta {
490
+ /** Text fragment for content_block_delta/text_delta events, else null. */
491
+ text: string | null;
492
+ /** Characters contributed by a thinking_delta (thinking models spend the
493
+ * shared max_tokens budget here before any text is emitted). */
494
+ thinkingChars: number;
495
+ /** stop_reason carried by a message_delta event (e.g. "max_tokens"). */
496
+ stopReason?: string;
497
+ /** True when the line was a `data:` payload that JSON-parsed (any event type). */
498
+ parsed: boolean;
499
+ }
500
+ /**
501
+ * SSE line → structured meta (text delta + thinking volume + stop_reason).
502
+ *
503
+ * One JSON.parse per line feeding both the text aggregation and the "why did a
504
+ * stream produce no text" diagnosis (thinking-only streams that end in
505
+ * stop_reason=max_tokens are an endpoint budget issue, not a transport one).
506
+ * Non-`data:` lines / malformed JSON → empty meta.
507
+ */
508
+ export declare function extractSseLineMeta(line: string): SseLineMeta;
509
+ /** Non-streaming body → why extractNonStreamText found no text. */
510
+ export interface NoTextBodyInfo {
511
+ /** content block type names in order, comma-joined ("" = no content at all). */
512
+ blocks: string;
513
+ sawThinking: boolean;
514
+ stopReason?: string;
515
+ }
516
+ /**
517
+ * Inspect a 200 non-streaming messages body that yielded no text, so the error
518
+ * can say WHY (thinking-only under a starved max_tokens budget vs an empty
519
+ * content array from a flaky gateway — the former is deterministic, the latter
520
+ * is worth retrying).
521
+ */
522
+ export declare function describeNoTextBody(body: unknown): NoTextBodyInfo;
523
+ /** HTTP statuses worth an automatic retry (transient server/gateway trouble). */
524
+ export declare const API_RETRYABLE_STATUS: Set<number>;
525
+ /** Parsed options for the hermes subcommand (strict: unknown flags error). */
526
+ export interface ParsedHermesArgs {
527
+ /** Positional: provider name, or a reserved word (status/rollback). */
528
+ provider?: string;
529
+ model?: string;
530
+ dryRun: boolean;
531
+ verify: boolean;
532
+ keepOnFail: boolean;
533
+ help: boolean;
534
+ }
535
+ export type ParseHermesResult = ParsedHermesArgs | {
536
+ error: string;
537
+ };
538
+ /**
539
+ * Strip a trailing context-window marker like `[1M]` (a claude-agent
540
+ * convention cc-switch stores in model names; raw APIs reject it).
541
+ * Only a suffix anchored at the very end is removed.
542
+ */
543
+ export declare function stripContextSuffix(model: string): string;
544
+ /**
545
+ * Parse argv for `gcli hermes ...`. Strict like parseApiArgs (unknown flags
546
+ * are exit-2 errors — there is no backend to forward to). `--no-verify` is
547
+ * accepted via allowNegative. At most one positional.
548
+ */
549
+ export declare function parseHermesArgs(argv: string[]): ParseHermesResult;
550
+ /**
551
+ * Derive a hermes provider id from a cc-switch display name: lowercase,
552
+ * non-[a-z0-9] runs folded to `-`, leading/trailing dashes trimmed.
553
+ * May return "" for names with no alphanumerics (caller treats as error).
554
+ */
555
+ export declare function deriveHermesId(ccName: string): string;
556
+ /** Derive the .env key name from a hermes provider id: UPPER + `_API_KEY`. */
557
+ export declare function deriveKeyEnv(id: string): string;
558
+ /** Quote a scalar for our line-level YAML writer ('' escapes a single quote). */
559
+ export declare function quoteYamlScalar(value: string): string;
560
+ /**
561
+ * Targeted line-level YAML editor for hermes' config.yaml (zero-dep: no yaml
562
+ * lib guaranteed). Only the top-level `model:` (3 keys) and `providers:`
563
+ * (one entry) sections are touched; everything else is byte-preserved.
564
+ * Missing sections / unexpected nesting → {error}, never a guess (callers
565
+ * must not write on error). Idempotent by construction.
566
+ */
567
+ export declare function editHermesConfig(text: string, edit: HermesConfigEdit): {
568
+ text: string;
569
+ } | {
570
+ error: string;
571
+ };
572
+ /**
573
+ * Read (not edit) the parts of config.yaml the hermes backend needs: the
574
+ * current model section values and the list of providers entry ids.
575
+ * Missing model: section or unparseable model lines → {error}.
576
+ */
577
+ export declare function parseHermesConfig(text: string): {
578
+ info: HermesConfigInfo;
579
+ } | {
580
+ error: string;
581
+ };
582
+ /**
583
+ * Upsert one `KEY=value` line in a .env text (pure). Only active lines
584
+ * (`^KEY=`) match — commented-out occurrences are left alone; comments and
585
+ * blank lines are byte-preserved. value=null deletes the line(s); a missing
586
+ * key with a non-null value is appended at the end.
587
+ */
588
+ export declare function upsertEnvLines(text: string, key: string, value: string | null): string;
589
+ /**
590
+ * Plan cron re-pins: jobs that are enabled AND pinned to `oldProvider`
591
+ * (jobs.json shape `{jobs: [...]}`, a bare array tolerated). Malformed input
592
+ * yields an empty plan, never a throw.
593
+ */
594
+ export declare function buildCronRepinPlan(jobsJson: unknown, oldProvider: string): CronRepinTarget[];
595
+ /**
596
+ * Parse the provider registry (`hermes-providers.json`). Corrupt/missing
597
+ * content is best-effort → empty registry; entries lacking id/keyEnv are
598
+ * dropped.
599
+ */
600
+ export declare function parseHermesRegistry(text: string): HermesProviderRegistry;
601
+ export declare function serializeHermesRegistry(reg: HermesProviderRegistry): string;
602
+ /**
603
+ * Parse `hermes-state.json` STRICTLY (畸形 → {error},不猜): it carries the
604
+ * previous token and the rollback plan, so a half-broken state must not be
605
+ * acted on.
606
+ */
607
+ export declare function parseHermesStateFile(text: string): {
608
+ state: HermesStateFile;
609
+ } | {
610
+ error: string;
611
+ };
612
+ export declare function serializeHermesStateFile(s: HermesStateFile): string;
371
613
  export interface ParsedArgs {
372
614
  prompt?: string;
373
615
  model?: string;
@@ -399,6 +641,10 @@ export interface ParsedApiArgs {
399
641
  maxTokens: number;
400
642
  timeoutMs: number;
401
643
  stream: boolean;
644
+ /** `auto` (default) omits the thinking field; `off`/`on` send it explicitly. */
645
+ thinking: "auto" | "off" | "on";
646
+ /** Transient-failure retries (default 1; 0 disables). */
647
+ retries: number;
402
648
  version: boolean;
403
649
  help: boolean;
404
650
  /** --cwd was supplied (warned + ignored by the api backend). */
@@ -431,11 +677,20 @@ export declare function runClaudeInteractive(args: string[], cwd?: string): Prom
431
677
  *
432
678
  * - stream=true: reads the SSE body chunk-by-chunk, decodes UTF-8, splits on
433
679
  * 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.
680
+ * guard against hangs: an idle timer (reset on EVERY received chunk
681
+ * thinking models can emit long non-text stretches) and an absolute timer
682
+ * (timeoutMs). Either firing aborts the fetch via AbortController → exit 1
683
+ * with a timeout message.
437
684
  * - stream=false: awaits the full JSON body and extracts `content[].text`,
438
- * then applies the 50k-char truncation.
685
+ * returned as-is (no size cap — endpoint limits are the endpoint's call).
686
+ *
687
+ * TRANSIENT failures are retried automatically (default 1 retry, `--retry N`):
688
+ * network-level fetch errors, HTTP 408/429/5xx, malformed JSON, empty-content
689
+ * 200 bodies, and streams that die or deliver no SSE events at all. Each retry
690
+ * note lands on stderr (stdout stays pipe-clean), so a recovered attempt still
691
+ * leaves a trace. Deterministic failures are NOT retried — timeouts, 4xx, and
692
+ * thinking-only responses (retrying cannot fix a max_tokens budget starved by
693
+ * endpoint-default thinking); those errors carry a diagnosis instead.
439
694
  *
440
695
  * HTTP errors (non-2xx, network failure, abort) → exit 1 with diagnostics on
441
696
  * stderr; stdout stays empty so the caller's empty-output guard still works.