oh-my-claudecode 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +50 -2
  2. package/commands/necronomicon-bind.md +69 -0
  3. package/dist/cli/bind-cron.d.ts +86 -0
  4. package/dist/cli/bind-cron.d.ts.map +1 -0
  5. package/dist/cli/bind-cron.js +161 -0
  6. package/dist/cli/bind-cron.js.map +1 -0
  7. package/dist/cli/bind.d.ts +102 -0
  8. package/dist/cli/bind.d.ts.map +1 -0
  9. package/dist/cli/bind.js +466 -0
  10. package/dist/cli/bind.js.map +1 -0
  11. package/dist/cli/index.js +102 -1
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/project-scan.d.ts +94 -0
  14. package/dist/cli/project-scan.d.ts.map +1 -0
  15. package/dist/cli/project-scan.js +387 -0
  16. package/dist/cli/project-scan.js.map +1 -0
  17. package/dist/cli/sisyphus-migrate.d.ts +50 -0
  18. package/dist/cli/sisyphus-migrate.d.ts.map +1 -0
  19. package/dist/cli/sisyphus-migrate.js +226 -0
  20. package/dist/cli/sisyphus-migrate.js.map +1 -0
  21. package/dist/cli/tui.d.ts +94 -0
  22. package/dist/cli/tui.d.ts.map +1 -0
  23. package/dist/cli/tui.js +180 -0
  24. package/dist/cli/tui.js.map +1 -0
  25. package/dist/cli/version.d.ts +2 -0
  26. package/dist/cli/version.d.ts.map +1 -0
  27. package/dist/cli/version.js +28 -0
  28. package/dist/cli/version.js.map +1 -0
  29. package/dist/config/schema.d.ts +4 -4
  30. package/dist/config/schema.d.ts.map +1 -1
  31. package/dist/config/schema.js +1 -0
  32. package/dist/config/schema.js.map +1 -1
  33. package/dist/features/yith-archive/functions/backfill.d.ts +43 -0
  34. package/dist/features/yith-archive/functions/backfill.d.ts.map +1 -1
  35. package/dist/features/yith-archive/functions/backfill.js +303 -145
  36. package/dist/features/yith-archive/functions/backfill.js.map +1 -1
  37. package/dist/features/yith-archive/functions/compress-batch.d.ts +4 -0
  38. package/dist/features/yith-archive/functions/compress-batch.d.ts.map +1 -0
  39. package/dist/features/yith-archive/functions/compress-batch.js +290 -0
  40. package/dist/features/yith-archive/functions/compress-batch.js.map +1 -0
  41. package/dist/features/yith-archive/functions/compress.d.ts +2 -1
  42. package/dist/features/yith-archive/functions/compress.d.ts.map +1 -1
  43. package/dist/features/yith-archive/functions/compress.js +1 -1
  44. package/dist/features/yith-archive/functions/compress.js.map +1 -1
  45. package/dist/features/yith-archive/functions/opencode-import.d.ts +67 -0
  46. package/dist/features/yith-archive/functions/opencode-import.d.ts.map +1 -0
  47. package/dist/features/yith-archive/functions/opencode-import.js +272 -0
  48. package/dist/features/yith-archive/functions/opencode-import.js.map +1 -0
  49. package/dist/features/yith-archive/index.d.ts.map +1 -1
  50. package/dist/features/yith-archive/index.js +4 -0
  51. package/dist/features/yith-archive/index.js.map +1 -1
  52. package/dist/features/yith-archive/providers/embedding/local.d.ts +22 -0
  53. package/dist/features/yith-archive/providers/embedding/local.d.ts.map +1 -1
  54. package/dist/features/yith-archive/providers/embedding/local.js +56 -2
  55. package/dist/features/yith-archive/providers/embedding/local.js.map +1 -1
  56. package/dist/features/yith-archive/state/bind-state.d.ts +84 -0
  57. package/dist/features/yith-archive/state/bind-state.d.ts.map +1 -0
  58. package/dist/features/yith-archive/state/bind-state.js +120 -0
  59. package/dist/features/yith-archive/state/bind-state.js.map +1 -0
  60. package/dist/features/yith-archive/state/schema.d.ts +14 -0
  61. package/dist/features/yith-archive/state/schema.d.ts.map +1 -1
  62. package/dist/features/yith-archive/state/schema.js +14 -0
  63. package/dist/features/yith-archive/state/schema.js.map +1 -1
  64. package/dist/hooks/cthulhu-auto.d.ts +1 -1
  65. package/dist/hooks/cthulhu-auto.d.ts.map +1 -1
  66. package/dist/hooks/cthulhu-auto.js +75 -11
  67. package/dist/hooks/cthulhu-auto.js.map +1 -1
  68. package/dist/hooks/cthulhu-preflight.d.ts +45 -0
  69. package/dist/hooks/cthulhu-preflight.d.ts.map +1 -0
  70. package/dist/hooks/cthulhu-preflight.js +91 -0
  71. package/dist/hooks/cthulhu-preflight.js.map +1 -0
  72. package/dist/hooks/index.d.ts +1 -0
  73. package/dist/hooks/index.d.ts.map +1 -1
  74. package/dist/hooks/index.js +9 -0
  75. package/dist/hooks/index.js.map +1 -1
  76. package/dist/hooks/yith-capture.d.ts +35 -0
  77. package/dist/hooks/yith-capture.d.ts.map +1 -0
  78. package/dist/hooks/yith-capture.js +113 -0
  79. package/dist/hooks/yith-capture.js.map +1 -0
  80. package/package.json +5 -2
  81. package/commands/bind-necronomicon.md +0 -106
package/README.md CHANGED
@@ -60,7 +60,7 @@ These aren't three plugins you pick and choose. They're one integrated system th
60
60
  | **Yith Archive** | Persistent cross-session memory with retrieval-based injection. Dozens of memory primitives: remember, search, consolidate, evict, crystallize, reflect, temporal graph, pattern extraction, and more. Exposed to Claude Code as a stdio MCP server with 7 tools. |
61
61
  | **Work-packet protocol** | LLM-requiring memory ops (consolidate, summarize, reflect, etc.) run in sessions with no API key — each function has a state-machine variant that emits prompts for the parent agent to execute with its own subscription auth. |
62
62
  | **Block Summarizer** | In-session delegation summarization with on-disk block archive |
63
- | **8 lifecycle hooks** | Auto-activation, memory redirect, todo enforcement, completion loops, code-quality checks, rule injection, write guards |
63
+ | **8 lifecycle hooks** | Auto-activation, memory redirect, continuous Yith capture, todo enforcement, completion loops, code-quality checks, rule injection, write guards |
64
64
  | **10 slash commands** | Direct-invoke any mode or flow from the Claude Code chat bar |
65
65
  | **Intent gate** | Every user message is classified and routed before Cthulhu acts |
66
66
  | **Work plan system** | Multi-step planning flow with interview → scope → plan → review before execution |
@@ -144,6 +144,53 @@ Named for the Great Race of Yith from *The Shadow Out of Time* — mind-transfer
144
144
  - **Crash-safe work-packet flows** — pending continuations for LLM-requiring operations persist to the same store and survive server restarts; resuming with the same continuation token picks up where the flow left off.
145
145
  - **Replaces Claude Code's built-in auto-memory** via the `memory-override` SessionStart hook, which tells the session not to write to the built-in memory files. Disable the override with `disabled_hooks: ["memory-override"]` if you prefer to keep the built-in system active.
146
146
 
147
+ ### The binding ritual (`oh-my-claudecode bind`)
148
+
149
+ Fresh installs start with an empty `necronomicon.json`. To populate it
150
+ with history, run one command in your terminal:
151
+
152
+ ```bash
153
+ oh-my-claudecode bind
154
+ ```
155
+
156
+ This kicks off a six-phase ritual with a real ANSI TUI (progress bars,
157
+ section headers, per-phase status):
158
+
159
+ 1. **Embedding sigil** — downloads the local nomic embedding model
160
+ (~137 MB) with a live byte-counter progress bar.
161
+ 2. **Claude Code transcripts** — scans every subdirectory under
162
+ `~/.claude/projects/` (every project you've ever opened a session
163
+ in), parses the `.jsonl` transcripts, and writes one raw
164
+ observation per user prompt / assistant text / tool call.
165
+ 3. **Opencode grimoire** — if you're migrating from oh-my-opencode, the
166
+ ritual auto-detects `~/.local/share/opencode/opencode.db` and
167
+ imports every project / session / message / part it finds.
168
+ 4. **Sisyphus migration** — walks your home looking for legacy
169
+ `.sisyphus/` directories (the oh-my-opencode equivalent of
170
+ `.elder-gods/`) and copies plans, handoffs, and evidence into the
171
+ new layout without touching the source.
172
+ 5. **Project code scan** — for each project the CLI has seen, walks
173
+ the code tree (gitignore-aware) and seeds preliminary memories:
174
+ language stats, package metadata, README sections, directory tree.
175
+ 6. **Sealing** — reports how many raw observations are queued for
176
+ compression and points you at the next step.
177
+
178
+ The ritual is **resumable**: if any phase errors or you interrupt it,
179
+ re-running `oh-my-claudecode bind` picks up from the failed phase via
180
+ the `KV.bindState` cursor — no manual intervention required.
181
+
182
+ Phase 2 (LLM-dependent compression of raw observations into
183
+ searchable memories) runs **inside a Claude Code session** via the
184
+ work-packet loop. Either:
185
+
186
+ - Open Claude Code and run `/necronomicon-bind` — uses your
187
+ subscription auth via the MCP work-packet protocol.
188
+ - Or install a cron entry that spawns `claude -p` on an interval:
189
+ ```bash
190
+ oh-my-claudecode bind --install-cron --interval 1h
191
+ ```
192
+ The cron tick drives compression unattended. No API key needed.
193
+
147
194
  ### Work-packet protocol — LLM ops without an API key
148
195
 
149
196
  13 of Yith's memory operations need an LLM to do their work (`crystallize`, `consolidate`, `consolidate-pipeline`, `compress`, `summarize`, `flow-compress`, `graph-extract`, `temporal-graph-extract`, `expand-query`, `skill-extract`, `reflect`, `enrich-window`, `enrich-session`). If Yith has its own `ANTHROPIC_API_KEY` in `~/.oh-my-claudecode/yith/.env`, these run directly in-process.
@@ -233,7 +280,7 @@ After installation these are available in Claude Code sessions:
233
280
  | Command | Description |
234
281
  |---------|-------------|
235
282
  | `/cthulhu` | Activate Cthulhu orchestrator mode (also creates `.elder-gods/` on first use) |
236
- | `/bind-necronomicon` | First-time Yith Archive setup ritual — tome check, embedding warmup, search verify, optional session backfill |
283
+ | `/necronomicon-bind` | Necronomicon binding ritual — shells out to `oh-my-claudecode bind` (real TUI, all-projects ingestion, opencode import, sisyphus migration, preliminary code scan) then drains pending compression via the work-packet loop using this session's LLM |
237
284
  | `/shoggoth` | Fast parallel codebase search |
238
285
  | `/yog-sothoth` | Consult the architecture/debug advisor |
239
286
  | `/elder-loop` | Start the self-referential completion loop |
@@ -251,6 +298,7 @@ After installation these are available in Claude Code sessions:
251
298
  |------|-------|-------------|
252
299
  | `cthulhu-auto` | SessionStart | Auto-activate Cthulhu orchestrator mode when `.elder-gods/` is present in the project |
253
300
  | `memory-override` | SessionStart | Redirect persistent memory writes from Claude Code's built-in auto-memory to Yith Archive |
301
+ | `yith-capture` | Stop | Continuous Yith ingestion — after every assistant turn, spawns a background `bind --resume --claude-only` to pull new transcript lines into the archive, and occasionally spawns `claude -p` to drain pending compression when the queue grows past threshold. Debounced; non-blocking; fail-safe. |
254
302
  | `todo-continuation` | Stop | Inject a reminder to continue if incomplete todos exist when stopping |
255
303
  | `elder-loop` | Stop | Self-referential completion loop — keeps running until the promise is met |
256
304
  | `comment-checker` | PostToolUse | Warn when AI-slop comments are introduced (comments that explain obvious code) |
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: necronomicon-bind
3
+ description: Start, continue, or resume the Necronomicon binding ritual. Thin wrapper that shells out to `oh-my-claudecode bind` (real progress bars, state-machine resumption, all-projects ingestion) and then offers to drain any pending compression via the work-packet loop using this session's own LLM.
4
+ ---
5
+
6
+ You are invoking the **Necronomicon Binding Ritual**. The ritual has two halves that must work together:
7
+
8
+ 1. **The fast half** (no LLM) — download the embedding model, scan every past Claude Code transcript across every project on this machine, import opencode history, migrate `.sisyphus/` dirs, seed preliminary memories from project code. This runs as a **real Node process** in the user's terminal with an ANSI TUI showing actual download progress bars and per-phase status. It must run OUTSIDE this Claude Code session.
9
+
10
+ 2. **The slow half** (LLM-dependent) — compress the thousands of raw observations the fast half ingested into searchable memories. This runs **inside this session** via the work-packet protocol, using the session's own subscription auth. Each round is one `yith_trigger → yith_commit_work` loop iteration.
11
+
12
+ ## Why not do everything from this slash command?
13
+
14
+ Previous versions of this ritual were prompt-only — they asked Claude to call `yith_remember` / `yith_trigger` inline from this slash command. That design was unreliable: Claude would skip steps, print cosmetic ✓ marks without actually invoking the tools, and the user would see "bound in 5 seconds" with an empty Necronomicon. **Do not try to recreate that approach.** The fast half MUST run as a real CLI, and this command's job is to drive it and then handle the compression half.
15
+
16
+ ## Phase 1 — Delegate to the CLI
17
+
18
+ Do this BEFORE anything else:
19
+
20
+ 1. Tell the user: "Running `oh-my-claudecode bind` in your terminal. This downloads the embedding model (~137 MB, one-time), scans every past Claude Code transcript, imports opencode data, migrates sisyphus directories, and seeds preliminary memories from project code. It's resumable — if it errors mid-run, re-running this command picks up where it stopped."
21
+
22
+ 2. Run via `Bash`:
23
+ ```bash
24
+ oh-my-claudecode bind
25
+ ```
26
+ Stream the output to the user as it arrives. The CLI already produces its own TUI (section headers, progress bars, status glyphs) so you should forward the output verbatim without adding your own commentary.
27
+
28
+ 3. When `oh-my-claudecode bind` exits, inspect its final lines. If the CLI reports "Ritual elapsed: Xs" and a green ✓, the fast half succeeded. If it reports a red ✗ with an error, surface that to the user and stop — re-running this command will retry the failed phase automatically from the state machine cursor.
29
+
30
+ ## Phase 2 — Drain pending compression via the work-packet loop
31
+
32
+ The CLI's last line tells you how many raw observations are pending compression. That's the input to this half.
33
+
34
+ 1. Call `yith_trigger({ name: "mem::compress-batch-step", args: { limit: 100 } })`. Expect a `needs_llm_work` envelope with one or more `workPackets` and a `continuation` token.
35
+
36
+ 2. For each packet in the envelope: read `systemPrompt` + `userPrompt`, reason about them inline (you ARE the LLM for Yith — just produce the compression XML the system prompt asks for), and collect the results.
37
+
38
+ 3. Call `yith_commit_work({ continuation, packetResults: [{id, completion}, ...] })`. The response is either terminal (`{status: "success"}`) or another `needs_llm_work` for the next batch.
39
+
40
+ 4. Between rounds, render a monospace ASCII progress bar so the user sees forward motion:
41
+ ```
42
+ [▓▓▓▓▓▓░░░░░░░░░░░░░░] 34% — 171/500 raw observations compressed
43
+ ```
44
+ Re-print the bar each round with updated numbers. (The Claude Code chat UI won't animate in place, but a re-printed bar per tool call shows clear progress.)
45
+
46
+ 5. Loop until terminal. Each round's `limit` can be 50-100 — adjust based on how large the prompts are. If a round takes more than ~2 minutes, drop the limit for the next one.
47
+
48
+ ## Phase 3 — Seal the ritual
49
+
50
+ Final output:
51
+
52
+ ```
53
+ ═══════════════════════════════════════════════════════
54
+ The Necronomicon is bound.
55
+ ═══════════════════════════════════════════════════════
56
+ Tome: ~/.oh-my-claudecode/yith/necronomicon.json
57
+ MCP server: yith-archive (via ~/.claude.json)
58
+ Embedding: local:nomic-embed-text-v1.5 (768 dims)
59
+ Observations: <total from mem::diagnose>
60
+ Compressed: <total - pending>
61
+ Pending: <remaining count>
62
+ ═══════════════════════════════════════════════════════
63
+ ```
64
+
65
+ If there are still pending observations (either because `limit` capped the batch or because the user interrupted), tell them exactly how many and that running `/necronomicon-bind` again resumes from where it stopped.
66
+
67
+ ## Unattended mode
68
+
69
+ Mention once at the end: if the user wants this to happen in the background without opening a session, they can run `oh-my-claudecode bind --install-cron [--interval 1h]` once, and a cron entry will run `oh-my-claudecode bind --resume` on the interval. That form uses `claude -p` (Claude Code's non-interactive mode) to drive the compression half, so nothing needs to be open.
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Cron installer + `claude -p` spawn-command assembly for unattended
3
+ * Necronomicon bind / compression.
4
+ *
5
+ * Everything here is a pure function over strings — no process
6
+ * spawning, no crontab mutation, no fs writes. The CLI entry point
7
+ * orchestrates the actual `crontab -l` / `crontab -` dance; this
8
+ * module only handles the string-level logic so tests can verify
9
+ * every edge case without side effects.
10
+ *
11
+ * The cron entry calls `oh-my-claudecode bind --resume`, which:
12
+ * 1. Runs any pending Phase 1 work (filesystem ingestion — no LLM).
13
+ * 2. For Phase 2 (compression), if an ANTHROPIC_API_KEY is
14
+ * configured, routes directly through the in-process LLM
15
+ * provider. Otherwise spawns `claude -p` with the command
16
+ * produced by `buildClaudePSpawnCommand` below, so Claude Code's
17
+ * own subscription auth drives the work-packet loop.
18
+ */
19
+ /** Suffix appended to every bind-installed crontab entry so the
20
+ * installer can find + replace its own line without clobbering
21
+ * unrelated entries. */
22
+ export declare const BIND_CRON_MARKER = "# oh-my-claudecode bind";
23
+ /**
24
+ * Convert a short interval spec like `"1h"`, `"30m"`, `"1d"` into a
25
+ * cron schedule string. Accepts:
26
+ *
27
+ * `Nm` — every N minutes (N > 0)
28
+ * `Nh` — every N hours (N > 0)
29
+ * `Nd` — every N days (currently supports 1d; multi-day uses
30
+ * "at midnight every day" since cron doesn't natively
31
+ * express "every 2 days" without a workaround)
32
+ * `N` — bare number, interpreted as minutes
33
+ *
34
+ * Throws on malformed input or zero/negative intervals so the CLI
35
+ * can surface a clear error.
36
+ */
37
+ export declare function parseIntervalSpec(spec: string): string;
38
+ export interface ClaudePSpawnOptions {
39
+ /** Observations per compress-batch-step call. Default 100. */
40
+ limit?: number;
41
+ /** Dollar cap on the claude -p invocation. Default 2.00. */
42
+ maxBudgetUsd?: number;
43
+ /** Model alias. Default "sonnet" — fast and cheap for compression. */
44
+ model?: string;
45
+ /** Where to write the claude -p log. Default /dev/null. */
46
+ logPath?: string;
47
+ }
48
+ /**
49
+ * Assemble the shell command that a cron tick runs to drive Phase 2
50
+ * compression via `claude -p`. The command is a single string suitable
51
+ * for a crontab line or a shell -c invocation.
52
+ *
53
+ * Security contract: --allowedTools restricts the spawn to the yith
54
+ * MCP tools only, so a runaway prompt can't shell out / edit files /
55
+ * read arbitrary code. --max-budget-usd caps the total API spend per
56
+ * tick. --permission-mode auto skips interactive prompts.
57
+ *
58
+ * The embedded prompt tells the spawned Claude to call
59
+ * mem::compress-batch-step in a loop via the work-packet protocol,
60
+ * terminating when the response is {status: "success"} or after 20
61
+ * rounds (safety cap).
62
+ */
63
+ export declare function buildClaudePSpawnCommand(opts?: ClaudePSpawnOptions): string;
64
+ export interface CrontabLineOptions {
65
+ /** 5-field cron schedule string, e.g. `0 * * * *`. */
66
+ schedule: string;
67
+ /** Command to run on each tick. */
68
+ command: string;
69
+ }
70
+ /**
71
+ * Build a full crontab line including the installer marker. The marker
72
+ * is a trailing comment so the line looks like a normal cron entry to
73
+ * operators who inspect their crontab.
74
+ */
75
+ export declare function buildCrontabLine(opts: CrontabLineOptions): string;
76
+ /**
77
+ * Merge `newLine` into an existing crontab file body, replacing any
78
+ * previous bind entry (identified by BIND_CRON_MARKER) in place. If
79
+ * no previous entry exists, appends. Preserves all other lines
80
+ * unchanged — including comments, whitespace, and unrelated jobs.
81
+ *
82
+ * Returns the updated crontab body as a string. Callers pipe this
83
+ * to `crontab -` to commit.
84
+ */
85
+ export declare function installCrontabEntry(existing: string, newLine: string): string;
86
+ //# sourceMappingURL=bind-cron.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind-cron.d.ts","sourceRoot":"","sources":["../../src/cli/bind-cron.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;yBAEyB;AACzB,eAAO,MAAM,gBAAgB,4BAA4B,CAAA;AAMzD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAiCtD;AAMD,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,GAAE,mBAAwB,GAC7B,MAAM,CA0BR;AAYD,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAA;IAChB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,CAEjE;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,MAAM,CA8BR"}
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Cron installer + `claude -p` spawn-command assembly for unattended
3
+ * Necronomicon bind / compression.
4
+ *
5
+ * Everything here is a pure function over strings — no process
6
+ * spawning, no crontab mutation, no fs writes. The CLI entry point
7
+ * orchestrates the actual `crontab -l` / `crontab -` dance; this
8
+ * module only handles the string-level logic so tests can verify
9
+ * every edge case without side effects.
10
+ *
11
+ * The cron entry calls `oh-my-claudecode bind --resume`, which:
12
+ * 1. Runs any pending Phase 1 work (filesystem ingestion — no LLM).
13
+ * 2. For Phase 2 (compression), if an ANTHROPIC_API_KEY is
14
+ * configured, routes directly through the in-process LLM
15
+ * provider. Otherwise spawns `claude -p` with the command
16
+ * produced by `buildClaudePSpawnCommand` below, so Claude Code's
17
+ * own subscription auth drives the work-packet loop.
18
+ */
19
+ /** Suffix appended to every bind-installed crontab entry so the
20
+ * installer can find + replace its own line without clobbering
21
+ * unrelated entries. */
22
+ export const BIND_CRON_MARKER = "# oh-my-claudecode bind";
23
+ // ============================================================================
24
+ // Interval spec parser
25
+ // ============================================================================
26
+ /**
27
+ * Convert a short interval spec like `"1h"`, `"30m"`, `"1d"` into a
28
+ * cron schedule string. Accepts:
29
+ *
30
+ * `Nm` — every N minutes (N > 0)
31
+ * `Nh` — every N hours (N > 0)
32
+ * `Nd` — every N days (currently supports 1d; multi-day uses
33
+ * "at midnight every day" since cron doesn't natively
34
+ * express "every 2 days" without a workaround)
35
+ * `N` — bare number, interpreted as minutes
36
+ *
37
+ * Throws on malformed input or zero/negative intervals so the CLI
38
+ * can surface a clear error.
39
+ */
40
+ export function parseIntervalSpec(spec) {
41
+ if (!spec)
42
+ throw new Error("parseIntervalSpec: empty interval");
43
+ const m = spec.match(/^(\d+)([mhd])?$/);
44
+ if (!m) {
45
+ throw new Error(`parseIntervalSpec: invalid interval "${spec}" — use Nm / Nh / Nd`);
46
+ }
47
+ const n = parseInt(m[1], 10);
48
+ const unit = m[2] ?? "m";
49
+ if (!Number.isFinite(n) || n <= 0) {
50
+ throw new Error(`parseIntervalSpec: interval must be > 0 (got ${n})`);
51
+ }
52
+ if (unit === "m") {
53
+ // Every N minutes. If N divides 60 evenly, use step syntax.
54
+ if (n >= 60 && n % 60 === 0) {
55
+ const hours = n / 60;
56
+ return hours === 1 ? "0 * * * *" : `0 */${hours} * * *`;
57
+ }
58
+ return `*/${n} * * * *`;
59
+ }
60
+ if (unit === "h") {
61
+ return n === 1 ? "0 * * * *" : `0 */${n} * * *`;
62
+ }
63
+ // Days: run at midnight every N days. Cron doesn't natively support
64
+ // "every 2 days" so we emit the 1d form only.
65
+ if (n === 1)
66
+ return "0 0 * * *";
67
+ // Fall back to "every N-th day of month" — not perfect but better
68
+ // than refusing the input.
69
+ return `0 0 */${n} * *`;
70
+ }
71
+ /**
72
+ * Assemble the shell command that a cron tick runs to drive Phase 2
73
+ * compression via `claude -p`. The command is a single string suitable
74
+ * for a crontab line or a shell -c invocation.
75
+ *
76
+ * Security contract: --allowedTools restricts the spawn to the yith
77
+ * MCP tools only, so a runaway prompt can't shell out / edit files /
78
+ * read arbitrary code. --max-budget-usd caps the total API spend per
79
+ * tick. --permission-mode auto skips interactive prompts.
80
+ *
81
+ * The embedded prompt tells the spawned Claude to call
82
+ * mem::compress-batch-step in a loop via the work-packet protocol,
83
+ * terminating when the response is {status: "success"} or after 20
84
+ * rounds (safety cap).
85
+ */
86
+ export function buildClaudePSpawnCommand(opts = {}) {
87
+ const limit = opts.limit ?? 100;
88
+ const maxBudgetUsd = opts.maxBudgetUsd ?? 2.0;
89
+ const model = opts.model ?? "sonnet";
90
+ const logPath = opts.logPath ?? "/dev/null";
91
+ const prompt = `Call yith_trigger with name "mem::compress-batch-step" and args {"limit": ${limit}}. ` +
92
+ `If the response has status "needs_llm_work", execute each packet's prompts ` +
93
+ `(read the systemPrompt + userPrompt, produce the compression XML the system ` +
94
+ `prompt asks for), then call yith_commit_work with the continuation token and ` +
95
+ `an array of {id, completion} results. Repeat this loop until the response is ` +
96
+ `{status: "success"} or you've processed 20 batches total. Output a single-line ` +
97
+ `JSON summary {"compressed": N, "failed": N, "errors": []} and exit.`;
98
+ return [
99
+ `claude`,
100
+ `--print`,
101
+ `--permission-mode auto`,
102
+ `--max-budget-usd ${maxBudgetUsd}`,
103
+ `--model ${model}`,
104
+ `--allowedTools "mcp__yith-archive__yith_trigger,mcp__yith-archive__yith_commit_work"`,
105
+ `--output-format json`,
106
+ `${shellEscapeSingle(prompt)}`,
107
+ `>> ${logPath} 2>&1`,
108
+ ].join(" ");
109
+ }
110
+ function shellEscapeSingle(s) {
111
+ // Wrap in single quotes and escape any literal single quote the
112
+ // shell-safe way: '...''...'...
113
+ return `'${s.replace(/'/g, `'\\''`)}'`;
114
+ }
115
+ /**
116
+ * Build a full crontab line including the installer marker. The marker
117
+ * is a trailing comment so the line looks like a normal cron entry to
118
+ * operators who inspect their crontab.
119
+ */
120
+ export function buildCrontabLine(opts) {
121
+ return `${opts.schedule} ${opts.command} ${BIND_CRON_MARKER}`;
122
+ }
123
+ /**
124
+ * Merge `newLine` into an existing crontab file body, replacing any
125
+ * previous bind entry (identified by BIND_CRON_MARKER) in place. If
126
+ * no previous entry exists, appends. Preserves all other lines
127
+ * unchanged — including comments, whitespace, and unrelated jobs.
128
+ *
129
+ * Returns the updated crontab body as a string. Callers pipe this
130
+ * to `crontab -` to commit.
131
+ */
132
+ export function installCrontabEntry(existing, newLine) {
133
+ const lines = existing.split("\n");
134
+ const out = [];
135
+ let replaced = false;
136
+ for (const line of lines) {
137
+ if (line.includes(BIND_CRON_MARKER)) {
138
+ if (!replaced) {
139
+ out.push(newLine);
140
+ replaced = true;
141
+ }
142
+ // Drop duplicate bind entries.
143
+ continue;
144
+ }
145
+ out.push(line);
146
+ }
147
+ if (!replaced) {
148
+ // Make sure we don't leave a trailing empty line + the new entry
149
+ // mashed together. Trim trailing blank lines before appending.
150
+ while (out.length > 0 && out[out.length - 1] === "") {
151
+ out.pop();
152
+ }
153
+ out.push(newLine);
154
+ }
155
+ // Ensure trailing newline — crontabs want it.
156
+ let body = out.join("\n");
157
+ if (!body.endsWith("\n"))
158
+ body += "\n";
159
+ return body;
160
+ }
161
+ //# sourceMappingURL=bind-cron.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind-cron.js","sourceRoot":"","sources":["../../src/cli/bind-cron.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * oh-my-claudecode bind — the CLI subcommand that drives the
3
+ * Necronomicon binding ritual.
4
+ *
5
+ * Architecture: a state-machine runner (`runBind`) reads `KV.bindState`,
6
+ * walks through each phase in `BIND_PHASE_ORDER`, invokes the matching
7
+ * `PhaseRunner`, and persists progress after every transition. Phases
8
+ * are injectable — tests pass fakes; production passes the real
9
+ * runners defined in `defaultPhaseRunners()`.
10
+ *
11
+ * Failure semantics: if any phase throws, `runBind` records the error
12
+ * into bindState, halts (does NOT run subsequent phases), and returns.
13
+ * The CLI entry point surfaces the error via the TUI and exits with
14
+ * a non-zero code. A re-run picks up from the failed phase and retries
15
+ * it — the state machine treats `failed` the same as `pending`.
16
+ *
17
+ * Cron-friendly: the same entry point is called from `bind --resume`
18
+ * which prints to stdout and exits, so a crontab can invoke it on
19
+ * an interval without an interactive session.
20
+ */
21
+ import type { YithArchiveHandle } from "../features/yith-archive/index.js";
22
+ import { type BindPhase, type BindState } from "../features/yith-archive/state/bind-state.js";
23
+ import { TuiWriter } from "./tui.js";
24
+ export interface BindContext {
25
+ archive: YithArchiveHandle;
26
+ tui: TuiWriter;
27
+ /**
28
+ * Absolute project cwd to scope work to, when the caller wants a
29
+ * narrow run. Set by the Stop-hook capture path so each assistant-
30
+ * response tick only scans the current session's project's
31
+ * transcripts rather than every project under `~/.claude/projects/`.
32
+ * Undefined means "all projects" (the full bind default).
33
+ */
34
+ projectCwd?: string;
35
+ }
36
+ export interface PhaseRunner {
37
+ name: BindPhase;
38
+ /**
39
+ * Execute the phase's work. Return a details object to merge into
40
+ * the phase's `details` field (e.g., per-project counts, cursor
41
+ * positions). Throw on failure — the runner catches and records
42
+ * the error into bindState for you.
43
+ */
44
+ run(ctx: BindContext): Promise<{
45
+ details?: Record<string, unknown>;
46
+ }>;
47
+ }
48
+ export interface RunBindOptions {
49
+ archive: YithArchiveHandle;
50
+ tui: TuiWriter;
51
+ /**
52
+ * Phase implementations. Defaults to `defaultPhaseRunners()` when
53
+ * omitted. Tests pass fakes to drive the state machine without
54
+ * hitting the real embedding / backfill / opencode paths.
55
+ */
56
+ phases?: PhaseRunner[];
57
+ /**
58
+ * When set, runs only the listed phases (even if they're already
59
+ * completed). Used by `bind --force <phase>` to re-run a specific
60
+ * phase without touching the others. Default: respect bindState.
61
+ */
62
+ force?: BindPhase[];
63
+ /**
64
+ * Narrow the runner to a specific subset of phases. Phases NOT in
65
+ * this list are neither run nor marked complete — their bindState
66
+ * stays untouched so the next full `bind` invocation still treats
67
+ * them as pending. Used by `--claude-only` / `--compress-only`
68
+ * flags and by the Stop hook to run a fast tick without re-doing
69
+ * unrelated work.
70
+ */
71
+ onlyPhases?: BindPhase[];
72
+ /**
73
+ * Absolute project cwd to pass through to phase runners via
74
+ * `BindContext.projectCwd`. The default `claude_transcripts`
75
+ * runner uses this to scope transcript ingestion to one project
76
+ * instead of scanning every `~/.claude/projects/` subdir.
77
+ */
78
+ projectCwd?: string;
79
+ }
80
+ /**
81
+ * State-machine driver. Reads bindState, runs pending phases in order,
82
+ * persists progress after each, halts on first failure.
83
+ *
84
+ * Returns the final BindState so callers (the CLI entry point) can
85
+ * summarize what happened and exit with the right code.
86
+ */
87
+ export declare function runBind(opts: RunBindOptions): Promise<BindState>;
88
+ /**
89
+ * Production phase runners. Tests inject fakes; the CLI entry point
90
+ * uses these defaults.
91
+ *
92
+ * Each runner is a thin wrapper that calls the underlying feature
93
+ * (provider warmup, backfill function, opencode importer, etc.) and
94
+ * reports progress via the context's TUI writer.
95
+ *
96
+ * Phases B, C, and D (opencode_import, sisyphus_migrate,
97
+ * preliminary_seed) currently register placeholders that log a
98
+ * "pending implementation" status and complete without work. They
99
+ * get real implementations in their respective phase files.
100
+ */
101
+ export declare function defaultPhaseRunners(): PhaseRunner[];
102
+ //# sourceMappingURL=bind.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind.d.ts","sourceRoot":"","sources":["../../src/cli/bind.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAA;AAO1E,OAAO,EAKL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,8CAA8C,CAAA;AACrD,OAAO,EACL,SAAS,EAMV,MAAM,UAAU,CAAA;AAMjB,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,iBAAiB,CAAA;IAC1B,GAAG,EAAE,SAAS,CAAA;IACd;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,SAAS,CAAA;IACf;;;;;OAKG;IACH,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAA;CACtE;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,iBAAiB,CAAA;IAC1B,GAAG,EAAE,SAAS,CAAA;IACd;;;;OAIG;IACH,MAAM,CAAC,EAAE,WAAW,EAAE,CAAA;IACtB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,EAAE,CAAA;IACnB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,SAAS,EAAE,CAAA;IACxB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAwBD;;;;;;GAMG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,SAAS,CAAC,CAiKtE;AA4DD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,IAAI,WAAW,EAAE,CA8VnD"}