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 +48 -8
- package/dist/cli.cjs +364 -17
- package/dist/cli.js +364 -17
- package/dist/index.cjs +296 -4
- package/dist/index.d.cts +157 -1
- package/dist/index.d.ts +157 -1
- package/dist/index.js +287 -4
- package/package.json +1 -1
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.
|
|
132
|
-
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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)
|
|
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
|
|
902
|
-
tokensOut:
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
--
|
|
1157
|
-
--tools
|
|
1158
|
-
--
|
|
1159
|
-
--
|
|
1160
|
-
--
|
|
1161
|
-
--
|
|
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 "
|
|
1165
|
-
oh-my-fable run "outline a talk on durable agents" --
|
|
1166
|
-
oh-my-fable
|
|
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:
|