oh-my-fable 0.1.1 → 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/README.md CHANGED
@@ -128,8 +128,18 @@ console.log(result.ctx.plan.steps);
128
128
  npm i oh-my-fable # zero runtime dependencies
129
129
  ```
130
130
 
131
- Node ≥ 18. The `AnthropicProvider` talks to the API over `fetch` no SDK. Bring
132
- any model by implementing the `Provider` interface (three methods).
131
+ Node ≥ 18. Ships with `AnthropicProvider` and `OpenAICompatProvider` (works with
132
+ OpenAI, Ollama, LM Studio, OpenRouter, Groq… — `ollama("llama3.1")` for a local
133
+ model with no key), both over `fetch`, no SDK. Or bring any model by implementing
134
+ the `Provider` interface (three methods).
135
+
136
+ `AnthropicProvider` works with the current flagship models (`claude-opus-4-8`,
137
+ `claude-fable-5`) out of the box — it drops the `temperature` parameter they
138
+ reject — and **prompt-caches the system+tools prefix by default**, so a long
139
+ durable run pays ~10× less on the context it replays every step. Opt into
140
+ `{ thinking: "adaptive", effort: "high" }` for harder planning. The `claude`
141
+ provider can return real `--output-format json` cost/usage and run Claude's own
142
+ tools (`{ tools: true, permissionMode: "acceptEdits" }`).
133
143
 
134
144
  ## Or use it from the terminal
135
145
 
@@ -138,18 +148,48 @@ Don't want to write code? It ships a CLI (zero extra deps):
138
148
  ```bash
139
149
  npx oh-my-fable demo # watch crash → resume, no API key
140
150
 
151
+ # ⭐ already pay for Claude Code? drive it as a DURABLE, TOOL-USING agent — your
152
+ # login, no separate API key, $0 per token. Claude edits files & runs commands:
153
+ npx oh-my-fable run "refactor utils.ts and run the tests" --provider claude --cli-tools
154
+
155
+ # pure-reasoning over the same login (no tools):
156
+ npx oh-my-fable run "outline a talk on durable agents" --provider claude
157
+
158
+ # or a LOCAL model (Ollama / LM Studio), also no key:
159
+ npx oh-my-fable run "outline a talk on durable agents" --provider ollama --model llama3.1
160
+
161
+ # or any hosted model:
141
162
  export ANTHROPIC_API_KEY=sk-...
142
163
  npx oh-my-fable run "summarize README.md into SUMMARY.md" --tools fs
143
- npx oh-my-fable run "outline a talk on durable agents" --success "5 sections"
144
164
 
145
165
  npx oh-my-fable list # your saved runs
166
+ npx oh-my-fable show run_abc123 # the run's plan, steps & budget as a timeline
146
167
  npx oh-my-fable resume run_abc123 # continue one from its checkpoint
147
168
  ```
148
169
 
149
- You watch the plan form and each step get reflected on, live. `--tools fs` gives
150
- the agent a sandboxed `read_file`/`write_file`/`list_dir` (confined to the working
151
- directory) so a terminal run can actually produce files; without it, runs are
152
- pure-reasoning. Every run is checkpointed, so `resume <runId>` always works.
170
+ **You don't need an Anthropic API key.** Pick how it talks to a model:
171
+
172
+ | `--provider` | uses | key? | tools? |
173
+ | --- | --- | --- | --- |
174
+ | `claude` | your Claude Code login | **none** | `--cli-tools` → Claude runs Read/Write/Edit/Bash itself |
175
+ | `codex` | your Codex CLI login | **none** | `--cli-tools` → workspace-write |
176
+ | `ollama` | a local Ollama model | **none** | `--tools fs` (harness-run) |
177
+ | `--base-url <url>` | LM Studio / OpenRouter / Groq / any OpenAI-compatible | per that server | `--tools fs` |
178
+ | `openai` | OpenAI | `OPENAI_API_KEY` | `--tools fs` |
179
+ | *(default)* | Anthropic | `ANTHROPIC_API_KEY` | `--tools fs` |
180
+
181
+ **Two ways to give an agent hands:**
182
+
183
+ - `--cli-tools` (claude/codex) — the CLI runs its **own** tools (file edits, shell)
184
+ on your subscription. oh-my-fable stays the durable planner/reflector around it:
185
+ it plans, checkpoints every step, and reflects — Claude does the work. Tune with
186
+ `--permission-mode acceptEdits|dontAsk|plan` and `--allow "Read,Edit,Bash(npm test)"`.
187
+ - `--tools fs` (API providers) — the harness gives the agent a sandboxed
188
+ `read_file`/`write_file`/`list_dir`, confined to the working directory.
189
+
190
+ You watch the plan form and each step get reflected on, live. Every run is
191
+ checkpointed, so `resume <runId>` always works — and `show <runId>` prints the
192
+ whole run (plan, steps, budget) from its serialized `RunContext`.
153
193
 
154
194
  ## Tools
155
195
 
@@ -217,7 +257,7 @@ architecture writeup is in [`ARCHITECTURE.md`](./ARCHITECTURE.md).
217
257
 
218
258
  ## Roadmap
219
259
 
220
- - A web dashboard that tails a run's events and lets you resume from any checkpoint.
260
+ - A web dashboard that tails a run's events and lets you resume from any checkpoint (`show <runId>` is the CLI version of this today).
221
261
  - More providers in-repo (OpenAI-compatible, local) — though it's a 3-method interface.
222
262
  - Parallel step execution for independent branches of the plan DAG.
223
263
  - Human-in-the-loop: pause for approval as a first-class step status.
package/dist/cli.cjs CHANGED
@@ -812,6 +812,10 @@ var reply = {
812
812
  };
813
813
 
814
814
  // src/providers/anthropic.ts
815
+ function modelRejectsSampling(model) {
816
+ const m = model.toLowerCase();
817
+ return m.includes("opus-4-7") || m.includes("opus-4-8") || m.includes("fable") || m.includes("mythos");
818
+ }
815
819
  function coalesce(messages) {
816
820
  const out = [];
817
821
  for (const m of messages) {
@@ -830,6 +834,9 @@ var AnthropicProvider = class {
830
834
  version;
831
835
  maxRetries;
832
836
  defaultMaxTokens;
837
+ cache;
838
+ thinking;
839
+ effort;
833
840
  constructor(opts = {}) {
834
841
  this.apiKey = opts.apiKey ?? process.env["ANTHROPIC_API_KEY"] ?? "";
835
842
  this.model = opts.model ?? "claude-sonnet-4-6";
@@ -837,6 +844,9 @@ var AnthropicProvider = class {
837
844
  this.version = opts.version ?? "2023-06-01";
838
845
  this.maxRetries = opts.maxRetries ?? 4;
839
846
  this.defaultMaxTokens = opts.defaultMaxTokens ?? 4096;
847
+ this.cache = opts.cache ?? true;
848
+ this.thinking = opts.thinking;
849
+ this.effort = opts.effort;
840
850
  if (!this.apiKey) {
841
851
  throw new Error("AnthropicProvider needs an API key (pass { apiKey } or set ANTHROPIC_API_KEY).");
842
852
  }
@@ -852,12 +862,18 @@ var AnthropicProvider = class {
852
862
  const body = {
853
863
  model: this.model,
854
864
  max_tokens: req.maxTokens ?? this.defaultMaxTokens,
855
- temperature: req.temperature ?? 1,
856
865
  messages: convo
857
866
  };
867
+ if (typeof req.temperature === "number" && !this.thinking && !modelRejectsSampling(this.model)) {
868
+ body["temperature"] = req.temperature;
869
+ }
870
+ if (this.thinking === "adaptive") body["thinking"] = { type: "adaptive" };
871
+ if (this.effort) body["output_config"] = { effort: this.effort };
858
872
  let sys = system;
859
873
  if (req.responseFormat === "json") sys = (sys ? sys + "\n\n" : "") + "Output ONLY valid JSON. No prose, no code fences.";
860
- if (sys) body["system"] = sys;
874
+ if (sys) {
875
+ body["system"] = this.cache ? [{ type: "text", text: sys, cache_control: { type: "ephemeral" } }] : sys;
876
+ }
861
877
  if (req.tools?.length) {
862
878
  body["tools"] = req.tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.parameters }));
863
879
  }
@@ -894,17 +910,275 @@ var AnthropicProvider = class {
894
910
  if (block.type === "text" && block.text) content += block.text;
895
911
  else if (block.type === "tool_use" && block.name) toolCalls.push({ id: block.id ?? block.name, name: block.name, input: block.input });
896
912
  }
913
+ const u = data.usage ?? {};
914
+ const tokensIn = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
897
915
  const map = { end_turn: "end", tool_use: "tool_use", max_tokens: "max_tokens", stop_sequence: "end" };
898
916
  return {
899
917
  content,
900
918
  toolCalls: toolCalls.length ? toolCalls : void 0,
901
- tokensIn: data.usage?.input_tokens ?? 0,
902
- tokensOut: data.usage?.output_tokens ?? 0,
919
+ tokensIn,
920
+ tokensOut: u.output_tokens ?? 0,
903
921
  stopReason: map[data.stop_reason ?? ""] ?? "end"
904
922
  };
905
923
  }
906
924
  };
907
925
 
926
+ // src/providers/openai.ts
927
+ var FINISH = { stop: "end", tool_calls: "tool_use", function_call: "tool_use", length: "max_tokens" };
928
+ var OpenAICompatProvider = class {
929
+ name;
930
+ baseUrl;
931
+ apiKey;
932
+ model;
933
+ maxRetries;
934
+ defaultMaxTokens;
935
+ constructor(opts) {
936
+ if (!opts.baseUrl) throw new Error("OpenAICompatProvider needs a baseUrl.");
937
+ if (!opts.model) throw new Error("OpenAICompatProvider needs a model.");
938
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
939
+ this.apiKey = opts.apiKey;
940
+ this.model = opts.model;
941
+ this.name = opts.label ?? "openai-compatible";
942
+ this.maxRetries = opts.maxRetries ?? 4;
943
+ this.defaultMaxTokens = opts.defaultMaxTokens ?? 4096;
944
+ }
945
+ estimateTokens(messages) {
946
+ return estimateTokens(messages);
947
+ }
948
+ async complete(req) {
949
+ const body = {
950
+ model: this.model,
951
+ messages: req.messages.map((m) => ({ role: m.role, content: m.content })),
952
+ max_tokens: req.maxTokens ?? this.defaultMaxTokens,
953
+ temperature: req.temperature ?? 1
954
+ };
955
+ if (req.tools?.length) {
956
+ body["tools"] = req.tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters } }));
957
+ }
958
+ const data = await withRetry(
959
+ async () => {
960
+ const headers = { "content-type": "application/json" };
961
+ if (this.apiKey) headers["authorization"] = `Bearer ${this.apiKey}`;
962
+ const res = await fetch(`${this.baseUrl}/chat/completions`, { method: "POST", headers, body: JSON.stringify(body) });
963
+ if (!res.ok) {
964
+ const text = await res.text().catch(() => "");
965
+ const err = new Error(`${this.name} ${res.status}: ${text.slice(0, 300)}`);
966
+ err.status = res.status;
967
+ throw err;
968
+ }
969
+ return await res.json();
970
+ },
971
+ {
972
+ retries: this.maxRetries,
973
+ isRetryable: (e) => {
974
+ const s = e.status;
975
+ return s === void 0 || s === 429 || s >= 500 && s < 600;
976
+ }
977
+ }
978
+ );
979
+ const choice = data.choices?.[0];
980
+ const content = typeof choice?.message?.content === "string" ? choice.message.content : "";
981
+ const toolCalls = [];
982
+ for (const tc of choice?.message?.tool_calls ?? []) {
983
+ if (!tc.function?.name) continue;
984
+ let input = {};
985
+ try {
986
+ input = tc.function.arguments ? JSON.parse(tc.function.arguments) : {};
987
+ } catch {
988
+ input = tc.function.arguments ?? {};
989
+ }
990
+ toolCalls.push({ id: tc.id ?? tc.function.name, name: tc.function.name, input });
991
+ }
992
+ return {
993
+ content,
994
+ toolCalls: toolCalls.length ? toolCalls : void 0,
995
+ tokensIn: data.usage?.prompt_tokens ?? 0,
996
+ tokensOut: data.usage?.completion_tokens ?? 0,
997
+ stopReason: FINISH[choice?.finish_reason ?? ""] ?? "end"
998
+ };
999
+ }
1000
+ };
1001
+ function ollama(model, opts = {}) {
1002
+ return new OpenAICompatProvider({ baseUrl: "http://localhost:11434/v1", model, label: "ollama", ...opts });
1003
+ }
1004
+
1005
+ // src/providers/cli.ts
1006
+ var import_node_child_process = require("child_process");
1007
+ function flatten(messages, includeSystem) {
1008
+ const parts = [];
1009
+ for (const m of messages) {
1010
+ if (m.role === "system" && !includeSystem) continue;
1011
+ if (m.role === "assistant") parts.push(`Assistant: ${m.content}`);
1012
+ else parts.push(m.content);
1013
+ }
1014
+ return parts.join("\n\n");
1015
+ }
1016
+ function runCli(command, args, input, timeoutMs, env) {
1017
+ return new Promise((resolve2, reject) => {
1018
+ let child;
1019
+ try {
1020
+ child = (0, import_node_child_process.spawn)(command, args, { env: { ...process.env, ...env } });
1021
+ } catch (e) {
1022
+ return reject(e);
1023
+ }
1024
+ let out = "";
1025
+ let err = "";
1026
+ const timer = setTimeout(() => {
1027
+ child.kill("SIGKILL");
1028
+ reject(new Error(`${command} timed out after ${timeoutMs} ms`));
1029
+ }, timeoutMs);
1030
+ child.stdout.on("data", (d) => out += d);
1031
+ child.stderr.on("data", (d) => err += d);
1032
+ child.on("error", (e) => {
1033
+ clearTimeout(timer);
1034
+ reject(e.code === "ENOENT" ? new Error(`"${command}" is not installed or not on your PATH.`) : e);
1035
+ });
1036
+ child.on("close", (code) => {
1037
+ clearTimeout(timer);
1038
+ if (code === 0) resolve2(out);
1039
+ else reject(new Error(`${command} exited ${code}: ${err.trim().slice(0, 300)}`));
1040
+ });
1041
+ if (input !== null) child.stdin.write(input);
1042
+ child.stdin.end();
1043
+ });
1044
+ }
1045
+ var CliProvider = class {
1046
+ name;
1047
+ command;
1048
+ args;
1049
+ promptVia;
1050
+ parse;
1051
+ env;
1052
+ timeoutMs;
1053
+ extraArgs;
1054
+ requestArgs;
1055
+ parseResult;
1056
+ systemInPrompt;
1057
+ constructor(opts) {
1058
+ this.command = opts.command;
1059
+ this.args = opts.args ?? [];
1060
+ this.promptVia = opts.promptVia ?? "arg";
1061
+ this.parse = opts.parse ?? ((s) => s.trim());
1062
+ this.env = opts.env;
1063
+ this.timeoutMs = opts.timeoutMs ?? 12e4;
1064
+ this.name = opts.label ?? `cli:${opts.command}`;
1065
+ this.extraArgs = opts.extraArgs ?? [];
1066
+ this.requestArgs = opts.requestArgs;
1067
+ this.parseResult = opts.parseResult;
1068
+ this.systemInPrompt = opts.systemInPrompt ?? true;
1069
+ }
1070
+ estimateTokens(messages) {
1071
+ return estimateTokens(messages);
1072
+ }
1073
+ async complete(req) {
1074
+ const prompt = flatten(req.messages, this.systemInPrompt);
1075
+ const reqArgs = this.requestArgs ? this.requestArgs(req) : [];
1076
+ const argv = [...this.args, ...this.extraArgs, ...reqArgs];
1077
+ const finalArgs = this.promptVia === "arg" ? [...argv, prompt] : argv;
1078
+ const stdout = await runCli(this.command, finalArgs, this.promptVia === "stdin" ? prompt : null, this.timeoutMs, this.env);
1079
+ if (this.parseResult) {
1080
+ const r = this.parseResult(stdout);
1081
+ const content2 = r.content ?? "";
1082
+ return {
1083
+ content: content2,
1084
+ toolCalls: r.toolCalls,
1085
+ tokensIn: r.tokensIn ?? estimateTokens(req.messages),
1086
+ tokensOut: r.tokensOut ?? Math.ceil(content2.length / 4),
1087
+ stopReason: r.stopReason ?? "end",
1088
+ sessionId: r.sessionId,
1089
+ costUsd: r.costUsd
1090
+ };
1091
+ }
1092
+ const content = this.parse(stdout);
1093
+ return {
1094
+ content,
1095
+ tokensIn: estimateTokens(req.messages),
1096
+ tokensOut: Math.ceil(content.length / 4),
1097
+ stopReason: "end"
1098
+ };
1099
+ }
1100
+ };
1101
+ var num = (x) => typeof x === "number" && Number.isFinite(x) ? x : 0;
1102
+ function parseClaudeJson(stdout) {
1103
+ let data;
1104
+ try {
1105
+ data = JSON.parse(stdout);
1106
+ } catch {
1107
+ return { content: stdout.trim() };
1108
+ }
1109
+ let content;
1110
+ if (data["structured_output"] !== void 0) {
1111
+ const so = data["structured_output"];
1112
+ content = typeof so === "string" ? so : JSON.stringify(so);
1113
+ } else {
1114
+ content = typeof data["result"] === "string" ? data["result"] : stdout.trim();
1115
+ }
1116
+ const usage = data["usage"] ?? {};
1117
+ const tokensIn = num(usage["input_tokens"]) + num(usage["cache_read_input_tokens"]) + num(usage["cache_creation_input_tokens"]);
1118
+ const tokensOut = num(usage["output_tokens"]);
1119
+ const out = { content };
1120
+ if (tokensIn) out.tokensIn = tokensIn;
1121
+ if (tokensOut) out.tokensOut = tokensOut;
1122
+ if (typeof data["session_id"] === "string") out.sessionId = data["session_id"];
1123
+ if (typeof data["total_cost_usd"] === "number") out.costUsd = data["total_cost_usd"];
1124
+ return out;
1125
+ }
1126
+ function claudeRequestArgs(req, opts) {
1127
+ const args = [];
1128
+ if (opts.json) args.push("--output-format", "json");
1129
+ if (opts.appendSystem) {
1130
+ let sys = req.messages.filter((m) => m.role === "system").map((m) => m.content).join("\n\n");
1131
+ if (req.responseFormat === "json") sys = (sys ? sys + "\n\n" : "") + "Output ONLY valid JSON. No prose, no code fences.";
1132
+ if (sys) args.push("--append-system-prompt", sys);
1133
+ }
1134
+ if (opts.jsonSchema && req.responseFormat === "json") args.push("--json-schema", JSON.stringify(opts.jsonSchema));
1135
+ return args;
1136
+ }
1137
+ var DEFAULT_CLAUDE_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep"];
1138
+ function claudeCode(opts = {}) {
1139
+ const json = opts.json ?? true;
1140
+ const appendSystem = opts.appendSystem ?? true;
1141
+ const extra = [];
1142
+ if (opts.model) extra.push("--model", opts.model);
1143
+ if (opts.tools) {
1144
+ const allow = Array.isArray(opts.tools) ? opts.tools : DEFAULT_CLAUDE_TOOLS;
1145
+ extra.push("--allowedTools", allow.join(","), "--permission-mode", opts.permissionMode ?? "acceptEdits");
1146
+ } else if (opts.permissionMode) {
1147
+ extra.push("--permission-mode", opts.permissionMode);
1148
+ }
1149
+ for (const d of opts.addDirs ?? []) extra.push("--add-dir", d);
1150
+ if (opts.resumeSessionId) extra.push("--resume", opts.resumeSessionId);
1151
+ return new CliProvider({
1152
+ command: "claude",
1153
+ args: ["-p"],
1154
+ promptVia: "arg",
1155
+ label: "claude-code",
1156
+ timeoutMs: opts.timeoutMs,
1157
+ env: opts.env,
1158
+ extraArgs: extra,
1159
+ requestArgs: (req) => claudeRequestArgs(req, { json, appendSystem, jsonSchema: opts.jsonSchema }),
1160
+ parseResult: json ? parseClaudeJson : void 0,
1161
+ systemInPrompt: !appendSystem
1162
+ });
1163
+ }
1164
+ function codexCli(opts = {}) {
1165
+ const extra = [];
1166
+ if (opts.model) extra.push("--model", opts.model);
1167
+ const sandbox = opts.sandbox ?? (opts.tools ? "workspace-write" : void 0);
1168
+ if (sandbox) extra.push("--sandbox", sandbox);
1169
+ const approval = opts.approval ?? (opts.tools ? "never" : void 0);
1170
+ if (approval) extra.push("--ask-for-approval", approval);
1171
+ return new CliProvider({
1172
+ command: "codex",
1173
+ args: ["exec"],
1174
+ promptVia: "arg",
1175
+ label: "codex",
1176
+ timeoutMs: opts.timeoutMs,
1177
+ env: opts.env,
1178
+ extraArgs: extra
1179
+ });
1180
+ }
1181
+
908
1182
  // src/index.ts
909
1183
  function buildDeps(config, serializable) {
910
1184
  const provider = config.provider;
@@ -932,7 +1206,7 @@ async function runWith(ctx, config) {
932
1206
 
933
1207
  // src/cli.ts
934
1208
  init_store();
935
- var VERSION = "0.1.0";
1209
+ var VERSION = "0.2.0";
936
1210
  var useColor = process.stdout.isTTY && !process.env["NO_COLOR"];
937
1211
  var c = (code) => (s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
938
1212
  var cyan = c("36");
@@ -1006,10 +1280,31 @@ function renderer() {
1006
1280
  };
1007
1281
  }
1008
1282
  function makeProvider(flags) {
1283
+ const str = (k) => typeof flags[k] === "string" ? flags[k] : void 0;
1284
+ const model = str("model");
1285
+ const provider = str("provider");
1286
+ const baseUrl = str("base-url");
1287
+ const apiKey = str("api-key");
1288
+ const permissionMode = str("permission-mode");
1289
+ const allowList = typeof flags["allow"] === "string" ? flags["allow"].split(",").map((s) => s.trim()).filter(Boolean) : void 0;
1290
+ const cliTools = flags["cli-tools"] === true;
1291
+ const toolsOpt = allowList ?? (cliTools ? true : void 0);
1009
1292
  try {
1010
- return new AnthropicProvider({ model: typeof flags["model"] === "string" ? flags["model"] : void 0 });
1293
+ if (provider === "claude" || provider === "claude-code") return claudeCode({ model, tools: toolsOpt, permissionMode });
1294
+ if (provider === "codex") return codexCli({ model, tools: cliTools || !!allowList });
1295
+ if (provider === "ollama") return ollama(model ?? "llama3.1", baseUrl ? { baseUrl } : {});
1296
+ if (provider === "openai") {
1297
+ return new OpenAICompatProvider({ baseUrl: baseUrl ?? "https://api.openai.com/v1", apiKey: apiKey ?? process.env["OPENAI_API_KEY"], model: model ?? "gpt-4o-mini", label: "openai" });
1298
+ }
1299
+ if (baseUrl) {
1300
+ if (!model) fail("--base-url needs --model too.");
1301
+ return new OpenAICompatProvider({ baseUrl, apiKey: apiKey ?? process.env["OPENAI_API_KEY"], model });
1302
+ }
1303
+ return new AnthropicProvider({ model });
1011
1304
  } catch (err) {
1012
- fail(err.message + "\nSet ANTHROPIC_API_KEY, or run `oh-my-fable demo` (no key needed).");
1305
+ fail(
1306
+ err.message + "\n\nNo API key needed \u2014 pick one:\n --provider claude (your Claude Code login)\n --provider ollama --model llama3.1 (a local model)\nOr just watch the mechanics: oh-my-fable demo"
1307
+ );
1013
1308
  }
1014
1309
  }
1015
1310
  function commonConfig(flags, provider) {
@@ -1083,6 +1378,42 @@ async function cmdList(args) {
1083
1378
  }
1084
1379
  process.stdout.write("\n");
1085
1380
  }
1381
+ async function cmdShow(args) {
1382
+ const runId = args._[0];
1383
+ if (!runId) fail("Give a run id: oh-my-fable show <runId> (see `oh-my-fable list`)");
1384
+ const store = new FileStore(typeof args.flags["runs-dir"] === "string" ? args.flags["runs-dir"] : "runs");
1385
+ const ctx = await store.load(runId);
1386
+ if (!ctx) fail(`no run ${runId} \u2014 try \`oh-my-fable list\``);
1387
+ const icon = { done: green("\u2714"), failed: red("\u2717"), running: yellow("\u25B6"), skipped: dim("\u2013"), pending: dim("\xB7") };
1388
+ const p = ctx.plan;
1389
+ const planColor = p.status === "done" ? green : p.status === "failed" ? red : yellow;
1390
+ process.stdout.write(`
1391
+ ${dim("\u2500\u2500 run")} ${mag(ctx.runId)} ${dim("\u2500\u2500")}
1392
+ `);
1393
+ process.stdout.write(` ${bold("goal")} ${ctx.goal.description}
1394
+ `);
1395
+ if (ctx.goal.successCriteria?.length) process.stdout.write(` ${dim("done when")} ${dim(ctx.goal.successCriteria.join("; "))}
1396
+ `);
1397
+ process.stdout.write(` ${bold("plan")} ${planColor(p.status)} ${dim(`\xB7 rev ${p.revision} \xB7 ${p.steps.length} steps`)}
1398
+
1399
+ `);
1400
+ for (const s of p.steps) {
1401
+ process.stdout.write(` ${icon[s.status] ?? "\xB7"} ${s.intent}${s.attempts > 1 ? dim(` \xD7${s.attempts}`) : ""}
1402
+ `);
1403
+ if (s.result) process.stdout.write(` ${dim("\u21B3 " + s.result.replace(/\s+/g, " ").slice(0, 120))}
1404
+ `);
1405
+ }
1406
+ const b = ctx.budget;
1407
+ const mins = Math.max(0, Math.round((Date.parse(ctx.updatedAt) - Date.parse(ctx.createdAt)) / 6e4));
1408
+ process.stdout.write(`
1409
+ ${dim(`budget: ${b.steps} steps \xB7 ${b.tokens.toLocaleString()} tokens \xB7 ${b.replans} replans \xB7 ~${mins}m`)}
1410
+ `);
1411
+ if (ctx.digests.length) process.stdout.write(` ${dim(`compacted: ${ctx.digests.length} digest${ctx.digests.length === 1 ? "" : "s"}`)}
1412
+ `);
1413
+ if (p.status !== "done") process.stdout.write(` ${dim("resume:")} ${cyan(`oh-my-fable resume ${ctx.runId}`)}
1414
+ `);
1415
+ process.stdout.write("\n");
1416
+ }
1086
1417
  async function cmdDemo() {
1087
1418
  const { MemoryStore: MemoryStore2 } = await Promise.resolve().then(() => (init_store(), store_exports));
1088
1419
  const store = new MemoryStore2();
@@ -1147,23 +1478,37 @@ function help() {
1147
1478
  ${bold("oh-my-fable")} ${dim("v" + VERSION)} \u2014 give an agent a goal; it plans, self-corrects, and survives crashes.
1148
1479
 
1149
1480
  ${bold("Usage")}
1150
- oh-my-fable run "<goal>" run an agent on a goal (needs ANTHROPIC_API_KEY)
1481
+ oh-my-fable run "<goal>" run an agent on a goal
1151
1482
  oh-my-fable resume <runId> continue a crashed/halted run from its checkpoint
1483
+ oh-my-fable show <runId> print a run's plan, steps, and budget as a timeline
1152
1484
  oh-my-fable list list saved runs
1153
1485
  oh-my-fable demo watch crash \u2192 resume, scripted (no API key)
1154
1486
 
1487
+ ${bold("Model")} ${dim("\u2014 no API key needed; ride a CLI login you already have")}
1488
+ --provider claude drive your Claude Code login \u2014 NO separate API key
1489
+ --provider claude --cli-tools \u2026and let Claude edit files / run tools itself (durable agent on your sub)
1490
+ --provider codex --cli-tools drive your Codex login, workspace-write
1491
+ --provider ollama --model llama3.1 a LOCAL model \u2014 no API key, no cost
1492
+ --provider openai --model gpt-4o-mini OpenAI (OPENAI_API_KEY)
1493
+ --base-url <url> --model <id> [--api-key] any OpenAI-compatible server (LM Studio, OpenRouter, Groq, \u2026)
1494
+ ${dim("(default: Anthropic API, needs ANTHROPIC_API_KEY)")}
1495
+
1155
1496
  ${bold("Options for run")}
1156
- --success "a; b" success criteria (semicolon-separated)
1157
- --tools fs allow sandboxed read_file/write_file/list_dir (default: none)
1158
- --model <id> model for the Anthropic provider
1159
- --max-steps <n> step budget --max-tokens <n> token budget
1160
- --runs-dir <dir> where checkpoints live (default: runs/)
1161
- --quiet no live event stream
1497
+ --model <id> model/alias for the chosen provider (e.g. opus, sonnet, llama3.1)
1498
+ --cli-tools let a CLI provider run its own tools (claude/codex); pairs with --permission-mode
1499
+ --permission-mode <m> claude: acceptEdits (default) | dontAsk | plan
1500
+ --allow "Read,Edit" claude: exact tool allowlist instead of the file-only default
1501
+ --success "a; b" success criteria (semicolon-separated)
1502
+ --tools fs give the harness sandboxed read_file/write_file/list_dir (API providers)
1503
+ --max-steps <n> step budget --max-tokens <n> token budget
1504
+ --runs-dir <dir> where checkpoints live (default: runs/)
1505
+ --quiet no live event stream
1162
1506
 
1163
1507
  ${bold("Examples")}
1164
- oh-my-fable run "summarize README.md into SUMMARY.md" --tools fs
1165
- oh-my-fable run "outline a talk on durable agents" --success "an outline with 5 sections"
1166
- oh-my-fable demo
1508
+ oh-my-fable run "refactor utils.ts and run the tests" --provider claude --cli-tools ${dim("# no API key")}
1509
+ oh-my-fable run "outline a talk on durable agents" --provider ollama --model llama3.1
1510
+ oh-my-fable show run_abc123 ${dim("# inspect any saved run")}
1511
+ oh-my-fable demo ${dim("# no key at all")}
1167
1512
 
1168
1513
  ${dim(`It's also a library: import { run, AnthropicProvider } from "oh-my-fable".`)}
1169
1514
  `);
@@ -1180,6 +1525,8 @@ async function main() {
1180
1525
  return cmdResume(args);
1181
1526
  case "list":
1182
1527
  return cmdList(args);
1528
+ case "show":
1529
+ return cmdShow(args);
1183
1530
  case "demo":
1184
1531
  return cmdDemo();
1185
1532
  default: