oh-my-fable 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -3
- package/dist/cli.cjs +187 -6
- package/dist/cli.js +187 -6
- package/dist/index.cjs +168 -0
- package/dist/index.d.cts +70 -1
- package/dist/index.d.ts +70 -1
- package/dist/index.js +163 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,8 +128,10 @@ 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).
|
|
133
135
|
|
|
134
136
|
## Or use it from the terminal
|
|
135
137
|
|
|
@@ -138,14 +140,33 @@ Don't want to write code? It ships a CLI (zero extra deps):
|
|
|
138
140
|
```bash
|
|
139
141
|
npx oh-my-fable demo # watch crash → resume, no API key
|
|
140
142
|
|
|
143
|
+
# already use Claude Code or Codex? drive it — uses that login, no separate key:
|
|
144
|
+
npx oh-my-fable run "outline a talk on durable agents" --provider claude
|
|
145
|
+
|
|
146
|
+
# or a LOCAL model (Ollama / LM Studio), also no key:
|
|
147
|
+
npx oh-my-fable run "outline a talk on durable agents" --provider ollama --model llama3.1
|
|
148
|
+
|
|
149
|
+
# or any hosted model:
|
|
141
150
|
export ANTHROPIC_API_KEY=sk-...
|
|
142
151
|
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
152
|
|
|
145
153
|
npx oh-my-fable list # your saved runs
|
|
146
154
|
npx oh-my-fable resume run_abc123 # continue one from its checkpoint
|
|
147
155
|
```
|
|
148
156
|
|
|
157
|
+
**You don't need an Anthropic API key.** Pick how it talks to a model:
|
|
158
|
+
|
|
159
|
+
| `--provider` | uses | key? |
|
|
160
|
+
| --- | --- | --- |
|
|
161
|
+
| `claude` / `codex` | your Claude Code / Codex CLI login | **none** — rides the CLI's auth |
|
|
162
|
+
| `ollama` | a local Ollama model | **none** |
|
|
163
|
+
| `--base-url <url>` | LM Studio / OpenRouter / Groq / any OpenAI-compatible | per that server |
|
|
164
|
+
| `openai` | OpenAI | `OPENAI_API_KEY` |
|
|
165
|
+
| *(default)* | Anthropic | `ANTHROPIC_API_KEY` |
|
|
166
|
+
|
|
167
|
+
(The CLI-driven providers are text-only — great for planning/reasoning runs; they
|
|
168
|
+
don't expose `--tools`.)
|
|
169
|
+
|
|
149
170
|
You watch the plan form and each step get reflected on, live. `--tools fs` gives
|
|
150
171
|
the agent a sandboxed `read_file`/`write_file`/`list_dir` (confined to the working
|
|
151
172
|
directory) so a terminal run can actually produce files; without it, runs are
|
package/dist/cli.cjs
CHANGED
|
@@ -905,6 +905,164 @@ var AnthropicProvider = class {
|
|
|
905
905
|
}
|
|
906
906
|
};
|
|
907
907
|
|
|
908
|
+
// src/providers/openai.ts
|
|
909
|
+
var FINISH = { stop: "end", tool_calls: "tool_use", function_call: "tool_use", length: "max_tokens" };
|
|
910
|
+
var OpenAICompatProvider = class {
|
|
911
|
+
name;
|
|
912
|
+
baseUrl;
|
|
913
|
+
apiKey;
|
|
914
|
+
model;
|
|
915
|
+
maxRetries;
|
|
916
|
+
defaultMaxTokens;
|
|
917
|
+
constructor(opts) {
|
|
918
|
+
if (!opts.baseUrl) throw new Error("OpenAICompatProvider needs a baseUrl.");
|
|
919
|
+
if (!opts.model) throw new Error("OpenAICompatProvider needs a model.");
|
|
920
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
921
|
+
this.apiKey = opts.apiKey;
|
|
922
|
+
this.model = opts.model;
|
|
923
|
+
this.name = opts.label ?? "openai-compatible";
|
|
924
|
+
this.maxRetries = opts.maxRetries ?? 4;
|
|
925
|
+
this.defaultMaxTokens = opts.defaultMaxTokens ?? 4096;
|
|
926
|
+
}
|
|
927
|
+
estimateTokens(messages) {
|
|
928
|
+
return estimateTokens(messages);
|
|
929
|
+
}
|
|
930
|
+
async complete(req) {
|
|
931
|
+
const body = {
|
|
932
|
+
model: this.model,
|
|
933
|
+
messages: req.messages.map((m) => ({ role: m.role, content: m.content })),
|
|
934
|
+
max_tokens: req.maxTokens ?? this.defaultMaxTokens,
|
|
935
|
+
temperature: req.temperature ?? 1
|
|
936
|
+
};
|
|
937
|
+
if (req.tools?.length) {
|
|
938
|
+
body["tools"] = req.tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters } }));
|
|
939
|
+
}
|
|
940
|
+
const data = await withRetry(
|
|
941
|
+
async () => {
|
|
942
|
+
const headers = { "content-type": "application/json" };
|
|
943
|
+
if (this.apiKey) headers["authorization"] = `Bearer ${this.apiKey}`;
|
|
944
|
+
const res = await fetch(`${this.baseUrl}/chat/completions`, { method: "POST", headers, body: JSON.stringify(body) });
|
|
945
|
+
if (!res.ok) {
|
|
946
|
+
const text = await res.text().catch(() => "");
|
|
947
|
+
const err = new Error(`${this.name} ${res.status}: ${text.slice(0, 300)}`);
|
|
948
|
+
err.status = res.status;
|
|
949
|
+
throw err;
|
|
950
|
+
}
|
|
951
|
+
return await res.json();
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
retries: this.maxRetries,
|
|
955
|
+
isRetryable: (e) => {
|
|
956
|
+
const s = e.status;
|
|
957
|
+
return s === void 0 || s === 429 || s >= 500 && s < 600;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
);
|
|
961
|
+
const choice = data.choices?.[0];
|
|
962
|
+
const content = typeof choice?.message?.content === "string" ? choice.message.content : "";
|
|
963
|
+
const toolCalls = [];
|
|
964
|
+
for (const tc of choice?.message?.tool_calls ?? []) {
|
|
965
|
+
if (!tc.function?.name) continue;
|
|
966
|
+
let input = {};
|
|
967
|
+
try {
|
|
968
|
+
input = tc.function.arguments ? JSON.parse(tc.function.arguments) : {};
|
|
969
|
+
} catch {
|
|
970
|
+
input = tc.function.arguments ?? {};
|
|
971
|
+
}
|
|
972
|
+
toolCalls.push({ id: tc.id ?? tc.function.name, name: tc.function.name, input });
|
|
973
|
+
}
|
|
974
|
+
return {
|
|
975
|
+
content,
|
|
976
|
+
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
977
|
+
tokensIn: data.usage?.prompt_tokens ?? 0,
|
|
978
|
+
tokensOut: data.usage?.completion_tokens ?? 0,
|
|
979
|
+
stopReason: FINISH[choice?.finish_reason ?? ""] ?? "end"
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
function ollama(model, opts = {}) {
|
|
984
|
+
return new OpenAICompatProvider({ baseUrl: "http://localhost:11434/v1", model, label: "ollama", ...opts });
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/providers/cli.ts
|
|
988
|
+
var import_node_child_process = require("child_process");
|
|
989
|
+
function flatten(messages) {
|
|
990
|
+
const parts = [];
|
|
991
|
+
for (const m of messages) {
|
|
992
|
+
if (m.role === "assistant") parts.push(`Assistant: ${m.content}`);
|
|
993
|
+
else parts.push(m.content);
|
|
994
|
+
}
|
|
995
|
+
return parts.join("\n\n");
|
|
996
|
+
}
|
|
997
|
+
function runCli(command, args, input, timeoutMs, env) {
|
|
998
|
+
return new Promise((resolve2, reject) => {
|
|
999
|
+
let child;
|
|
1000
|
+
try {
|
|
1001
|
+
child = (0, import_node_child_process.spawn)(command, args, { env: { ...process.env, ...env } });
|
|
1002
|
+
} catch (e) {
|
|
1003
|
+
return reject(e);
|
|
1004
|
+
}
|
|
1005
|
+
let out = "";
|
|
1006
|
+
let err = "";
|
|
1007
|
+
const timer = setTimeout(() => {
|
|
1008
|
+
child.kill("SIGKILL");
|
|
1009
|
+
reject(new Error(`${command} timed out after ${timeoutMs} ms`));
|
|
1010
|
+
}, timeoutMs);
|
|
1011
|
+
child.stdout.on("data", (d) => out += d);
|
|
1012
|
+
child.stderr.on("data", (d) => err += d);
|
|
1013
|
+
child.on("error", (e) => {
|
|
1014
|
+
clearTimeout(timer);
|
|
1015
|
+
reject(e.code === "ENOENT" ? new Error(`"${command}" is not installed or not on your PATH.`) : e);
|
|
1016
|
+
});
|
|
1017
|
+
child.on("close", (code) => {
|
|
1018
|
+
clearTimeout(timer);
|
|
1019
|
+
if (code === 0) resolve2(out);
|
|
1020
|
+
else reject(new Error(`${command} exited ${code}: ${err.trim().slice(0, 300)}`));
|
|
1021
|
+
});
|
|
1022
|
+
if (input !== null) child.stdin.write(input);
|
|
1023
|
+
child.stdin.end();
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
var CliProvider = class {
|
|
1027
|
+
name;
|
|
1028
|
+
command;
|
|
1029
|
+
args;
|
|
1030
|
+
promptVia;
|
|
1031
|
+
parse;
|
|
1032
|
+
env;
|
|
1033
|
+
timeoutMs;
|
|
1034
|
+
constructor(opts) {
|
|
1035
|
+
this.command = opts.command;
|
|
1036
|
+
this.args = opts.args ?? [];
|
|
1037
|
+
this.promptVia = opts.promptVia ?? "arg";
|
|
1038
|
+
this.parse = opts.parse ?? ((s) => s.trim());
|
|
1039
|
+
this.env = opts.env;
|
|
1040
|
+
this.timeoutMs = opts.timeoutMs ?? 12e4;
|
|
1041
|
+
this.name = opts.label ?? `cli:${opts.command}`;
|
|
1042
|
+
}
|
|
1043
|
+
estimateTokens(messages) {
|
|
1044
|
+
return estimateTokens(messages);
|
|
1045
|
+
}
|
|
1046
|
+
async complete(req) {
|
|
1047
|
+
const prompt = flatten(req.messages);
|
|
1048
|
+
const args = this.promptVia === "arg" ? [...this.args, prompt] : this.args;
|
|
1049
|
+
const stdout = await runCli(this.command, args, this.promptVia === "stdin" ? prompt : null, this.timeoutMs, this.env);
|
|
1050
|
+
const content = this.parse(stdout);
|
|
1051
|
+
return {
|
|
1052
|
+
content,
|
|
1053
|
+
tokensIn: estimateTokens(req.messages),
|
|
1054
|
+
tokensOut: Math.ceil(content.length / 4),
|
|
1055
|
+
stopReason: "end"
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
function claudeCode(opts = {}) {
|
|
1060
|
+
return new CliProvider({ command: "claude", args: ["-p"], promptVia: "arg", label: "claude-code", ...opts });
|
|
1061
|
+
}
|
|
1062
|
+
function codexCli(opts = {}) {
|
|
1063
|
+
return new CliProvider({ command: "codex", args: ["exec"], promptVia: "arg", label: "codex", ...opts });
|
|
1064
|
+
}
|
|
1065
|
+
|
|
908
1066
|
// src/index.ts
|
|
909
1067
|
function buildDeps(config, serializable) {
|
|
910
1068
|
const provider = config.provider;
|
|
@@ -1006,10 +1164,27 @@ function renderer() {
|
|
|
1006
1164
|
};
|
|
1007
1165
|
}
|
|
1008
1166
|
function makeProvider(flags) {
|
|
1167
|
+
const str = (k) => typeof flags[k] === "string" ? flags[k] : void 0;
|
|
1168
|
+
const model = str("model");
|
|
1169
|
+
const provider = str("provider");
|
|
1170
|
+
const baseUrl = str("base-url");
|
|
1171
|
+
const apiKey = str("api-key");
|
|
1009
1172
|
try {
|
|
1010
|
-
|
|
1173
|
+
if (provider === "claude" || provider === "claude-code") return claudeCode();
|
|
1174
|
+
if (provider === "codex") return codexCli();
|
|
1175
|
+
if (provider === "ollama") return ollama(model ?? "llama3.1", baseUrl ? { baseUrl } : {});
|
|
1176
|
+
if (provider === "openai") {
|
|
1177
|
+
return new OpenAICompatProvider({ baseUrl: baseUrl ?? "https://api.openai.com/v1", apiKey: apiKey ?? process.env["OPENAI_API_KEY"], model: model ?? "gpt-4o-mini", label: "openai" });
|
|
1178
|
+
}
|
|
1179
|
+
if (baseUrl) {
|
|
1180
|
+
if (!model) fail("--base-url needs --model too.");
|
|
1181
|
+
return new OpenAICompatProvider({ baseUrl, apiKey: apiKey ?? process.env["OPENAI_API_KEY"], model });
|
|
1182
|
+
}
|
|
1183
|
+
return new AnthropicProvider({ model });
|
|
1011
1184
|
} catch (err) {
|
|
1012
|
-
fail(
|
|
1185
|
+
fail(
|
|
1186
|
+
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"
|
|
1187
|
+
);
|
|
1013
1188
|
}
|
|
1014
1189
|
}
|
|
1015
1190
|
function commonConfig(flags, provider) {
|
|
@@ -1152,18 +1327,24 @@ ${bold("Usage")}
|
|
|
1152
1327
|
oh-my-fable list list saved runs
|
|
1153
1328
|
oh-my-fable demo watch crash \u2192 resume, scripted (no API key)
|
|
1154
1329
|
|
|
1330
|
+
${bold("Model")} ${dim("(default: Anthropic, needs ANTHROPIC_API_KEY)")}
|
|
1331
|
+
--provider claude drive your Claude Code CLI \u2014 uses its login, NO separate key
|
|
1332
|
+
--provider codex drive your Codex CLI \u2014 same idea
|
|
1333
|
+
--provider ollama --model llama3.1 a LOCAL model \u2014 no API key, no cost
|
|
1334
|
+
--provider openai --model gpt-4o-mini OpenAI (OPENAI_API_KEY)
|
|
1335
|
+
--base-url <url> --model <id> [--api-key] any OpenAI-compatible server (LM Studio, OpenRouter, Groq, \u2026)
|
|
1336
|
+
|
|
1155
1337
|
${bold("Options for run")}
|
|
1156
1338
|
--success "a; b" success criteria (semicolon-separated)
|
|
1157
1339
|
--tools fs allow sandboxed read_file/write_file/list_dir (default: none)
|
|
1158
|
-
--model <id> model for the Anthropic provider
|
|
1159
1340
|
--max-steps <n> step budget --max-tokens <n> token budget
|
|
1160
1341
|
--runs-dir <dir> where checkpoints live (default: runs/)
|
|
1161
1342
|
--quiet no live event stream
|
|
1162
1343
|
|
|
1163
1344
|
${bold("Examples")}
|
|
1164
|
-
oh-my-fable run "
|
|
1165
|
-
oh-my-fable run "
|
|
1166
|
-
oh-my-fable demo
|
|
1345
|
+
oh-my-fable run "outline a talk on durable agents" --provider ollama --model llama3.1
|
|
1346
|
+
oh-my-fable run "summarize README.md into SUMMARY.md" --tools fs ${dim("# uses Anthropic")}
|
|
1347
|
+
oh-my-fable demo ${dim("# no key at all")}
|
|
1167
1348
|
|
|
1168
1349
|
${dim(`It's also a library: import { run, AnthropicProvider } from "oh-my-fable".`)}
|
|
1169
1350
|
`);
|
package/dist/cli.js
CHANGED
|
@@ -904,6 +904,164 @@ var AnthropicProvider = class {
|
|
|
904
904
|
}
|
|
905
905
|
};
|
|
906
906
|
|
|
907
|
+
// src/providers/openai.ts
|
|
908
|
+
var FINISH = { stop: "end", tool_calls: "tool_use", function_call: "tool_use", length: "max_tokens" };
|
|
909
|
+
var OpenAICompatProvider = class {
|
|
910
|
+
name;
|
|
911
|
+
baseUrl;
|
|
912
|
+
apiKey;
|
|
913
|
+
model;
|
|
914
|
+
maxRetries;
|
|
915
|
+
defaultMaxTokens;
|
|
916
|
+
constructor(opts) {
|
|
917
|
+
if (!opts.baseUrl) throw new Error("OpenAICompatProvider needs a baseUrl.");
|
|
918
|
+
if (!opts.model) throw new Error("OpenAICompatProvider needs a model.");
|
|
919
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
920
|
+
this.apiKey = opts.apiKey;
|
|
921
|
+
this.model = opts.model;
|
|
922
|
+
this.name = opts.label ?? "openai-compatible";
|
|
923
|
+
this.maxRetries = opts.maxRetries ?? 4;
|
|
924
|
+
this.defaultMaxTokens = opts.defaultMaxTokens ?? 4096;
|
|
925
|
+
}
|
|
926
|
+
estimateTokens(messages) {
|
|
927
|
+
return estimateTokens(messages);
|
|
928
|
+
}
|
|
929
|
+
async complete(req) {
|
|
930
|
+
const body = {
|
|
931
|
+
model: this.model,
|
|
932
|
+
messages: req.messages.map((m) => ({ role: m.role, content: m.content })),
|
|
933
|
+
max_tokens: req.maxTokens ?? this.defaultMaxTokens,
|
|
934
|
+
temperature: req.temperature ?? 1
|
|
935
|
+
};
|
|
936
|
+
if (req.tools?.length) {
|
|
937
|
+
body["tools"] = req.tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters } }));
|
|
938
|
+
}
|
|
939
|
+
const data = await withRetry(
|
|
940
|
+
async () => {
|
|
941
|
+
const headers = { "content-type": "application/json" };
|
|
942
|
+
if (this.apiKey) headers["authorization"] = `Bearer ${this.apiKey}`;
|
|
943
|
+
const res = await fetch(`${this.baseUrl}/chat/completions`, { method: "POST", headers, body: JSON.stringify(body) });
|
|
944
|
+
if (!res.ok) {
|
|
945
|
+
const text = await res.text().catch(() => "");
|
|
946
|
+
const err = new Error(`${this.name} ${res.status}: ${text.slice(0, 300)}`);
|
|
947
|
+
err.status = res.status;
|
|
948
|
+
throw err;
|
|
949
|
+
}
|
|
950
|
+
return await res.json();
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
retries: this.maxRetries,
|
|
954
|
+
isRetryable: (e) => {
|
|
955
|
+
const s = e.status;
|
|
956
|
+
return s === void 0 || s === 429 || s >= 500 && s < 600;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
);
|
|
960
|
+
const choice = data.choices?.[0];
|
|
961
|
+
const content = typeof choice?.message?.content === "string" ? choice.message.content : "";
|
|
962
|
+
const toolCalls = [];
|
|
963
|
+
for (const tc of choice?.message?.tool_calls ?? []) {
|
|
964
|
+
if (!tc.function?.name) continue;
|
|
965
|
+
let input = {};
|
|
966
|
+
try {
|
|
967
|
+
input = tc.function.arguments ? JSON.parse(tc.function.arguments) : {};
|
|
968
|
+
} catch {
|
|
969
|
+
input = tc.function.arguments ?? {};
|
|
970
|
+
}
|
|
971
|
+
toolCalls.push({ id: tc.id ?? tc.function.name, name: tc.function.name, input });
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
content,
|
|
975
|
+
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
976
|
+
tokensIn: data.usage?.prompt_tokens ?? 0,
|
|
977
|
+
tokensOut: data.usage?.completion_tokens ?? 0,
|
|
978
|
+
stopReason: FINISH[choice?.finish_reason ?? ""] ?? "end"
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
function ollama(model, opts = {}) {
|
|
983
|
+
return new OpenAICompatProvider({ baseUrl: "http://localhost:11434/v1", model, label: "ollama", ...opts });
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/providers/cli.ts
|
|
987
|
+
import { spawn } from "child_process";
|
|
988
|
+
function flatten(messages) {
|
|
989
|
+
const parts = [];
|
|
990
|
+
for (const m of messages) {
|
|
991
|
+
if (m.role === "assistant") parts.push(`Assistant: ${m.content}`);
|
|
992
|
+
else parts.push(m.content);
|
|
993
|
+
}
|
|
994
|
+
return parts.join("\n\n");
|
|
995
|
+
}
|
|
996
|
+
function runCli(command, args, input, timeoutMs, env) {
|
|
997
|
+
return new Promise((resolve2, reject) => {
|
|
998
|
+
let child;
|
|
999
|
+
try {
|
|
1000
|
+
child = spawn(command, args, { env: { ...process.env, ...env } });
|
|
1001
|
+
} catch (e) {
|
|
1002
|
+
return reject(e);
|
|
1003
|
+
}
|
|
1004
|
+
let out = "";
|
|
1005
|
+
let err = "";
|
|
1006
|
+
const timer = setTimeout(() => {
|
|
1007
|
+
child.kill("SIGKILL");
|
|
1008
|
+
reject(new Error(`${command} timed out after ${timeoutMs} ms`));
|
|
1009
|
+
}, timeoutMs);
|
|
1010
|
+
child.stdout.on("data", (d) => out += d);
|
|
1011
|
+
child.stderr.on("data", (d) => err += d);
|
|
1012
|
+
child.on("error", (e) => {
|
|
1013
|
+
clearTimeout(timer);
|
|
1014
|
+
reject(e.code === "ENOENT" ? new Error(`"${command}" is not installed or not on your PATH.`) : e);
|
|
1015
|
+
});
|
|
1016
|
+
child.on("close", (code) => {
|
|
1017
|
+
clearTimeout(timer);
|
|
1018
|
+
if (code === 0) resolve2(out);
|
|
1019
|
+
else reject(new Error(`${command} exited ${code}: ${err.trim().slice(0, 300)}`));
|
|
1020
|
+
});
|
|
1021
|
+
if (input !== null) child.stdin.write(input);
|
|
1022
|
+
child.stdin.end();
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
var CliProvider = class {
|
|
1026
|
+
name;
|
|
1027
|
+
command;
|
|
1028
|
+
args;
|
|
1029
|
+
promptVia;
|
|
1030
|
+
parse;
|
|
1031
|
+
env;
|
|
1032
|
+
timeoutMs;
|
|
1033
|
+
constructor(opts) {
|
|
1034
|
+
this.command = opts.command;
|
|
1035
|
+
this.args = opts.args ?? [];
|
|
1036
|
+
this.promptVia = opts.promptVia ?? "arg";
|
|
1037
|
+
this.parse = opts.parse ?? ((s) => s.trim());
|
|
1038
|
+
this.env = opts.env;
|
|
1039
|
+
this.timeoutMs = opts.timeoutMs ?? 12e4;
|
|
1040
|
+
this.name = opts.label ?? `cli:${opts.command}`;
|
|
1041
|
+
}
|
|
1042
|
+
estimateTokens(messages) {
|
|
1043
|
+
return estimateTokens(messages);
|
|
1044
|
+
}
|
|
1045
|
+
async complete(req) {
|
|
1046
|
+
const prompt = flatten(req.messages);
|
|
1047
|
+
const args = this.promptVia === "arg" ? [...this.args, prompt] : this.args;
|
|
1048
|
+
const stdout = await runCli(this.command, args, this.promptVia === "stdin" ? prompt : null, this.timeoutMs, this.env);
|
|
1049
|
+
const content = this.parse(stdout);
|
|
1050
|
+
return {
|
|
1051
|
+
content,
|
|
1052
|
+
tokensIn: estimateTokens(req.messages),
|
|
1053
|
+
tokensOut: Math.ceil(content.length / 4),
|
|
1054
|
+
stopReason: "end"
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
function claudeCode(opts = {}) {
|
|
1059
|
+
return new CliProvider({ command: "claude", args: ["-p"], promptVia: "arg", label: "claude-code", ...opts });
|
|
1060
|
+
}
|
|
1061
|
+
function codexCli(opts = {}) {
|
|
1062
|
+
return new CliProvider({ command: "codex", args: ["exec"], promptVia: "arg", label: "codex", ...opts });
|
|
1063
|
+
}
|
|
1064
|
+
|
|
907
1065
|
// src/index.ts
|
|
908
1066
|
function buildDeps(config, serializable) {
|
|
909
1067
|
const provider = config.provider;
|
|
@@ -1005,10 +1163,27 @@ function renderer() {
|
|
|
1005
1163
|
};
|
|
1006
1164
|
}
|
|
1007
1165
|
function makeProvider(flags) {
|
|
1166
|
+
const str = (k) => typeof flags[k] === "string" ? flags[k] : void 0;
|
|
1167
|
+
const model = str("model");
|
|
1168
|
+
const provider = str("provider");
|
|
1169
|
+
const baseUrl = str("base-url");
|
|
1170
|
+
const apiKey = str("api-key");
|
|
1008
1171
|
try {
|
|
1009
|
-
|
|
1172
|
+
if (provider === "claude" || provider === "claude-code") return claudeCode();
|
|
1173
|
+
if (provider === "codex") return codexCli();
|
|
1174
|
+
if (provider === "ollama") return ollama(model ?? "llama3.1", baseUrl ? { baseUrl } : {});
|
|
1175
|
+
if (provider === "openai") {
|
|
1176
|
+
return new OpenAICompatProvider({ baseUrl: baseUrl ?? "https://api.openai.com/v1", apiKey: apiKey ?? process.env["OPENAI_API_KEY"], model: model ?? "gpt-4o-mini", label: "openai" });
|
|
1177
|
+
}
|
|
1178
|
+
if (baseUrl) {
|
|
1179
|
+
if (!model) fail("--base-url needs --model too.");
|
|
1180
|
+
return new OpenAICompatProvider({ baseUrl, apiKey: apiKey ?? process.env["OPENAI_API_KEY"], model });
|
|
1181
|
+
}
|
|
1182
|
+
return new AnthropicProvider({ model });
|
|
1010
1183
|
} catch (err) {
|
|
1011
|
-
fail(
|
|
1184
|
+
fail(
|
|
1185
|
+
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"
|
|
1186
|
+
);
|
|
1012
1187
|
}
|
|
1013
1188
|
}
|
|
1014
1189
|
function commonConfig(flags, provider) {
|
|
@@ -1151,18 +1326,24 @@ ${bold("Usage")}
|
|
|
1151
1326
|
oh-my-fable list list saved runs
|
|
1152
1327
|
oh-my-fable demo watch crash \u2192 resume, scripted (no API key)
|
|
1153
1328
|
|
|
1329
|
+
${bold("Model")} ${dim("(default: Anthropic, needs ANTHROPIC_API_KEY)")}
|
|
1330
|
+
--provider claude drive your Claude Code CLI \u2014 uses its login, NO separate key
|
|
1331
|
+
--provider codex drive your Codex CLI \u2014 same idea
|
|
1332
|
+
--provider ollama --model llama3.1 a LOCAL model \u2014 no API key, no cost
|
|
1333
|
+
--provider openai --model gpt-4o-mini OpenAI (OPENAI_API_KEY)
|
|
1334
|
+
--base-url <url> --model <id> [--api-key] any OpenAI-compatible server (LM Studio, OpenRouter, Groq, \u2026)
|
|
1335
|
+
|
|
1154
1336
|
${bold("Options for run")}
|
|
1155
1337
|
--success "a; b" success criteria (semicolon-separated)
|
|
1156
1338
|
--tools fs allow sandboxed read_file/write_file/list_dir (default: none)
|
|
1157
|
-
--model <id> model for the Anthropic provider
|
|
1158
1339
|
--max-steps <n> step budget --max-tokens <n> token budget
|
|
1159
1340
|
--runs-dir <dir> where checkpoints live (default: runs/)
|
|
1160
1341
|
--quiet no live event stream
|
|
1161
1342
|
|
|
1162
1343
|
${bold("Examples")}
|
|
1163
|
-
oh-my-fable run "
|
|
1164
|
-
oh-my-fable run "
|
|
1165
|
-
oh-my-fable demo
|
|
1344
|
+
oh-my-fable run "outline a talk on durable agents" --provider ollama --model llama3.1
|
|
1345
|
+
oh-my-fable run "summarize README.md into SUMMARY.md" --tools fs ${dim("# uses Anthropic")}
|
|
1346
|
+
oh-my-fable demo ${dim("# no key at all")}
|
|
1166
1347
|
|
|
1167
1348
|
${dim(`It's also a library: import { run, AnthropicProvider } from "oh-my-fable".`)}
|
|
1168
1349
|
`);
|
package/dist/index.cjs
CHANGED
|
@@ -21,22 +21,27 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
AnthropicProvider: () => AnthropicProvider,
|
|
24
|
+
CliProvider: () => CliProvider,
|
|
24
25
|
ContextManager: () => ContextManager,
|
|
25
26
|
DEFAULT_CONFIG: () => DEFAULT_CONFIG,
|
|
26
27
|
Executor: () => Executor,
|
|
27
28
|
FileStore: () => FileStore,
|
|
28
29
|
MemoryStore: () => MemoryStore,
|
|
30
|
+
OpenAICompatProvider: () => OpenAICompatProvider,
|
|
29
31
|
Planner: () => Planner,
|
|
30
32
|
Reflector: () => Reflector,
|
|
31
33
|
ScriptedProvider: () => ScriptedProvider,
|
|
32
34
|
ToolRegistry: () => ToolRegistry,
|
|
33
35
|
checkBudget: () => checkBudget,
|
|
36
|
+
claudeCode: () => claudeCode,
|
|
37
|
+
codexCli: () => codexCli,
|
|
34
38
|
createContext: () => createContext,
|
|
35
39
|
defineTool: () => defineTool,
|
|
36
40
|
estimateTokens: () => estimateTokens,
|
|
37
41
|
fsTools: () => fsTools,
|
|
38
42
|
genId: () => genId,
|
|
39
43
|
nextPendingStep: () => nextPendingStep,
|
|
44
|
+
ollama: () => ollama,
|
|
40
45
|
reply: () => reply,
|
|
41
46
|
resolveSerializable: () => resolveSerializable,
|
|
42
47
|
resume: () => resume,
|
|
@@ -925,6 +930,164 @@ var AnthropicProvider = class {
|
|
|
925
930
|
}
|
|
926
931
|
};
|
|
927
932
|
|
|
933
|
+
// src/providers/openai.ts
|
|
934
|
+
var FINISH = { stop: "end", tool_calls: "tool_use", function_call: "tool_use", length: "max_tokens" };
|
|
935
|
+
var OpenAICompatProvider = class {
|
|
936
|
+
name;
|
|
937
|
+
baseUrl;
|
|
938
|
+
apiKey;
|
|
939
|
+
model;
|
|
940
|
+
maxRetries;
|
|
941
|
+
defaultMaxTokens;
|
|
942
|
+
constructor(opts) {
|
|
943
|
+
if (!opts.baseUrl) throw new Error("OpenAICompatProvider needs a baseUrl.");
|
|
944
|
+
if (!opts.model) throw new Error("OpenAICompatProvider needs a model.");
|
|
945
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
946
|
+
this.apiKey = opts.apiKey;
|
|
947
|
+
this.model = opts.model;
|
|
948
|
+
this.name = opts.label ?? "openai-compatible";
|
|
949
|
+
this.maxRetries = opts.maxRetries ?? 4;
|
|
950
|
+
this.defaultMaxTokens = opts.defaultMaxTokens ?? 4096;
|
|
951
|
+
}
|
|
952
|
+
estimateTokens(messages) {
|
|
953
|
+
return estimateTokens(messages);
|
|
954
|
+
}
|
|
955
|
+
async complete(req) {
|
|
956
|
+
const body = {
|
|
957
|
+
model: this.model,
|
|
958
|
+
messages: req.messages.map((m) => ({ role: m.role, content: m.content })),
|
|
959
|
+
max_tokens: req.maxTokens ?? this.defaultMaxTokens,
|
|
960
|
+
temperature: req.temperature ?? 1
|
|
961
|
+
};
|
|
962
|
+
if (req.tools?.length) {
|
|
963
|
+
body["tools"] = req.tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters } }));
|
|
964
|
+
}
|
|
965
|
+
const data = await withRetry(
|
|
966
|
+
async () => {
|
|
967
|
+
const headers = { "content-type": "application/json" };
|
|
968
|
+
if (this.apiKey) headers["authorization"] = `Bearer ${this.apiKey}`;
|
|
969
|
+
const res = await fetch(`${this.baseUrl}/chat/completions`, { method: "POST", headers, body: JSON.stringify(body) });
|
|
970
|
+
if (!res.ok) {
|
|
971
|
+
const text = await res.text().catch(() => "");
|
|
972
|
+
const err = new Error(`${this.name} ${res.status}: ${text.slice(0, 300)}`);
|
|
973
|
+
err.status = res.status;
|
|
974
|
+
throw err;
|
|
975
|
+
}
|
|
976
|
+
return await res.json();
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
retries: this.maxRetries,
|
|
980
|
+
isRetryable: (e) => {
|
|
981
|
+
const s = e.status;
|
|
982
|
+
return s === void 0 || s === 429 || s >= 500 && s < 600;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
);
|
|
986
|
+
const choice = data.choices?.[0];
|
|
987
|
+
const content = typeof choice?.message?.content === "string" ? choice.message.content : "";
|
|
988
|
+
const toolCalls = [];
|
|
989
|
+
for (const tc of choice?.message?.tool_calls ?? []) {
|
|
990
|
+
if (!tc.function?.name) continue;
|
|
991
|
+
let input = {};
|
|
992
|
+
try {
|
|
993
|
+
input = tc.function.arguments ? JSON.parse(tc.function.arguments) : {};
|
|
994
|
+
} catch {
|
|
995
|
+
input = tc.function.arguments ?? {};
|
|
996
|
+
}
|
|
997
|
+
toolCalls.push({ id: tc.id ?? tc.function.name, name: tc.function.name, input });
|
|
998
|
+
}
|
|
999
|
+
return {
|
|
1000
|
+
content,
|
|
1001
|
+
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
1002
|
+
tokensIn: data.usage?.prompt_tokens ?? 0,
|
|
1003
|
+
tokensOut: data.usage?.completion_tokens ?? 0,
|
|
1004
|
+
stopReason: FINISH[choice?.finish_reason ?? ""] ?? "end"
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
function ollama(model, opts = {}) {
|
|
1009
|
+
return new OpenAICompatProvider({ baseUrl: "http://localhost:11434/v1", model, label: "ollama", ...opts });
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// src/providers/cli.ts
|
|
1013
|
+
var import_node_child_process = require("child_process");
|
|
1014
|
+
function flatten(messages) {
|
|
1015
|
+
const parts = [];
|
|
1016
|
+
for (const m of messages) {
|
|
1017
|
+
if (m.role === "assistant") parts.push(`Assistant: ${m.content}`);
|
|
1018
|
+
else parts.push(m.content);
|
|
1019
|
+
}
|
|
1020
|
+
return parts.join("\n\n");
|
|
1021
|
+
}
|
|
1022
|
+
function runCli(command, args, input, timeoutMs, env) {
|
|
1023
|
+
return new Promise((resolve2, reject) => {
|
|
1024
|
+
let child;
|
|
1025
|
+
try {
|
|
1026
|
+
child = (0, import_node_child_process.spawn)(command, args, { env: { ...process.env, ...env } });
|
|
1027
|
+
} catch (e) {
|
|
1028
|
+
return reject(e);
|
|
1029
|
+
}
|
|
1030
|
+
let out = "";
|
|
1031
|
+
let err = "";
|
|
1032
|
+
const timer = setTimeout(() => {
|
|
1033
|
+
child.kill("SIGKILL");
|
|
1034
|
+
reject(new Error(`${command} timed out after ${timeoutMs} ms`));
|
|
1035
|
+
}, timeoutMs);
|
|
1036
|
+
child.stdout.on("data", (d) => out += d);
|
|
1037
|
+
child.stderr.on("data", (d) => err += d);
|
|
1038
|
+
child.on("error", (e) => {
|
|
1039
|
+
clearTimeout(timer);
|
|
1040
|
+
reject(e.code === "ENOENT" ? new Error(`"${command}" is not installed or not on your PATH.`) : e);
|
|
1041
|
+
});
|
|
1042
|
+
child.on("close", (code) => {
|
|
1043
|
+
clearTimeout(timer);
|
|
1044
|
+
if (code === 0) resolve2(out);
|
|
1045
|
+
else reject(new Error(`${command} exited ${code}: ${err.trim().slice(0, 300)}`));
|
|
1046
|
+
});
|
|
1047
|
+
if (input !== null) child.stdin.write(input);
|
|
1048
|
+
child.stdin.end();
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
var CliProvider = class {
|
|
1052
|
+
name;
|
|
1053
|
+
command;
|
|
1054
|
+
args;
|
|
1055
|
+
promptVia;
|
|
1056
|
+
parse;
|
|
1057
|
+
env;
|
|
1058
|
+
timeoutMs;
|
|
1059
|
+
constructor(opts) {
|
|
1060
|
+
this.command = opts.command;
|
|
1061
|
+
this.args = opts.args ?? [];
|
|
1062
|
+
this.promptVia = opts.promptVia ?? "arg";
|
|
1063
|
+
this.parse = opts.parse ?? ((s) => s.trim());
|
|
1064
|
+
this.env = opts.env;
|
|
1065
|
+
this.timeoutMs = opts.timeoutMs ?? 12e4;
|
|
1066
|
+
this.name = opts.label ?? `cli:${opts.command}`;
|
|
1067
|
+
}
|
|
1068
|
+
estimateTokens(messages) {
|
|
1069
|
+
return estimateTokens(messages);
|
|
1070
|
+
}
|
|
1071
|
+
async complete(req) {
|
|
1072
|
+
const prompt = flatten(req.messages);
|
|
1073
|
+
const args = this.promptVia === "arg" ? [...this.args, prompt] : this.args;
|
|
1074
|
+
const stdout = await runCli(this.command, args, this.promptVia === "stdin" ? prompt : null, this.timeoutMs, this.env);
|
|
1075
|
+
const content = this.parse(stdout);
|
|
1076
|
+
return {
|
|
1077
|
+
content,
|
|
1078
|
+
tokensIn: estimateTokens(req.messages),
|
|
1079
|
+
tokensOut: Math.ceil(content.length / 4),
|
|
1080
|
+
stopReason: "end"
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
function claudeCode(opts = {}) {
|
|
1085
|
+
return new CliProvider({ command: "claude", args: ["-p"], promptVia: "arg", label: "claude-code", ...opts });
|
|
1086
|
+
}
|
|
1087
|
+
function codexCli(opts = {}) {
|
|
1088
|
+
return new CliProvider({ command: "codex", args: ["exec"], promptVia: "arg", label: "codex", ...opts });
|
|
1089
|
+
}
|
|
1090
|
+
|
|
928
1091
|
// src/index.ts
|
|
929
1092
|
function buildDeps(config, serializable) {
|
|
930
1093
|
const provider = config.provider;
|
|
@@ -958,22 +1121,27 @@ async function runWith(ctx, config) {
|
|
|
958
1121
|
// Annotate the CommonJS export names for ESM import in node:
|
|
959
1122
|
0 && (module.exports = {
|
|
960
1123
|
AnthropicProvider,
|
|
1124
|
+
CliProvider,
|
|
961
1125
|
ContextManager,
|
|
962
1126
|
DEFAULT_CONFIG,
|
|
963
1127
|
Executor,
|
|
964
1128
|
FileStore,
|
|
965
1129
|
MemoryStore,
|
|
1130
|
+
OpenAICompatProvider,
|
|
966
1131
|
Planner,
|
|
967
1132
|
Reflector,
|
|
968
1133
|
ScriptedProvider,
|
|
969
1134
|
ToolRegistry,
|
|
970
1135
|
checkBudget,
|
|
1136
|
+
claudeCode,
|
|
1137
|
+
codexCli,
|
|
971
1138
|
createContext,
|
|
972
1139
|
defineTool,
|
|
973
1140
|
estimateTokens,
|
|
974
1141
|
fsTools,
|
|
975
1142
|
genId,
|
|
976
1143
|
nextPendingStep,
|
|
1144
|
+
ollama,
|
|
977
1145
|
reply,
|
|
978
1146
|
resolveSerializable,
|
|
979
1147
|
resume,
|
package/dist/index.d.cts
CHANGED
|
@@ -377,6 +377,75 @@ declare class AnthropicProvider implements Provider {
|
|
|
377
377
|
complete(req: CompletionRequest): Promise<CompletionResult>;
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
interface OpenAICompatOptions {
|
|
381
|
+
/** e.g. http://localhost:11434/v1 (Ollama), http://localhost:1234/v1 (LM Studio), https://api.openai.com/v1 */
|
|
382
|
+
baseUrl: string;
|
|
383
|
+
/** Optional — local servers don't need one. */
|
|
384
|
+
apiKey?: string;
|
|
385
|
+
model: string;
|
|
386
|
+
label?: string;
|
|
387
|
+
maxRetries?: number;
|
|
388
|
+
defaultMaxTokens?: number;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Talks the OpenAI Chat Completions format — which means it works with almost
|
|
392
|
+
* everything: **Ollama and LM Studio (local, no API key)**, llama.cpp's server,
|
|
393
|
+
* OpenRouter, Groq, Together, OpenAI itself… Point `baseUrl` at any of them.
|
|
394
|
+
* No SDK, no dependencies.
|
|
395
|
+
*/
|
|
396
|
+
declare class OpenAICompatProvider implements Provider {
|
|
397
|
+
readonly name: string;
|
|
398
|
+
private readonly baseUrl;
|
|
399
|
+
private readonly apiKey?;
|
|
400
|
+
private readonly model;
|
|
401
|
+
private readonly maxRetries;
|
|
402
|
+
private readonly defaultMaxTokens;
|
|
403
|
+
constructor(opts: OpenAICompatOptions);
|
|
404
|
+
estimateTokens(messages: Message[]): number;
|
|
405
|
+
complete(req: CompletionRequest): Promise<CompletionResult>;
|
|
406
|
+
}
|
|
407
|
+
/** Convenience: a local Ollama model, no API key. `ollama("llama3.1")`. */
|
|
408
|
+
declare function ollama(model: string, opts?: Partial<OpenAICompatOptions>): OpenAICompatProvider;
|
|
409
|
+
|
|
410
|
+
interface CliProviderOptions {
|
|
411
|
+
/** The executable, e.g. "claude" or "codex". */
|
|
412
|
+
command: string;
|
|
413
|
+
/** Fixed args before the prompt, e.g. ["-p"] for Claude Code print mode. */
|
|
414
|
+
args?: string[];
|
|
415
|
+
/** Pass the prompt as the final arg ("arg", default) or on stdin ("stdin"). */
|
|
416
|
+
promptVia?: "arg" | "stdin";
|
|
417
|
+
/** Extract the assistant text from the command's stdout. Default: trim. */
|
|
418
|
+
parse?: (stdout: string) => string;
|
|
419
|
+
env?: Record<string, string>;
|
|
420
|
+
timeoutMs?: number;
|
|
421
|
+
label?: string;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Drives an agentic CLI (Claude Code, Codex, …) in non-interactive mode: every
|
|
425
|
+
* model call shells out to the command and captures its stdout. This means
|
|
426
|
+
* people who use those CLIs via a **subscription login use oh-my-fable with no
|
|
427
|
+
* separate API key** — it rides whatever auth the CLI already has.
|
|
428
|
+
*
|
|
429
|
+
* Note: it returns text only (no native tool-calling), so the agent runs in
|
|
430
|
+
* pure-reasoning mode; `--tools fs` is unavailable through a CLI provider.
|
|
431
|
+
*/
|
|
432
|
+
declare class CliProvider implements Provider {
|
|
433
|
+
readonly name: string;
|
|
434
|
+
private readonly command;
|
|
435
|
+
private readonly args;
|
|
436
|
+
private readonly promptVia;
|
|
437
|
+
private readonly parse;
|
|
438
|
+
private readonly env?;
|
|
439
|
+
private readonly timeoutMs;
|
|
440
|
+
constructor(opts: CliProviderOptions);
|
|
441
|
+
estimateTokens(messages: Message[]): number;
|
|
442
|
+
complete(req: CompletionRequest): Promise<CompletionResult>;
|
|
443
|
+
}
|
|
444
|
+
/** Claude Code in print mode — uses your existing `claude` auth (subscription or key). */
|
|
445
|
+
declare function claudeCode(opts?: Partial<CliProviderOptions>): CliProvider;
|
|
446
|
+
/** OpenAI Codex CLI in non-interactive exec mode — uses your existing `codex` auth. */
|
|
447
|
+
declare function codexCli(opts?: Partial<CliProviderOptions>): CliProvider;
|
|
448
|
+
|
|
380
449
|
/** Run an agent to completion (or to a budget halt). The whole run is checkpointed every step. */
|
|
381
450
|
declare function run(goal: Goal | string, config: RunConfig): Promise<RunResult>;
|
|
382
451
|
/** Resume a run from its last checkpoint — same plan, same progress, continues where it died. */
|
|
@@ -384,4 +453,4 @@ declare function resume(runId: string, config: RunConfig): Promise<RunResult>;
|
|
|
384
453
|
/** Continue a RunContext you already hold in memory (advanced; same as resume without the store load). */
|
|
385
454
|
declare function runWith(ctx: RunContext, config: RunConfig): Promise<RunResult>;
|
|
386
455
|
|
|
387
|
-
export { type AnthropicOptions, AnthropicProvider, type BudgetState, type CompletionRequest, type CompletionResult, ContextManager, DEFAULT_CONFIG, type Digest, Executor, FileStore, type Goal, type LoopDeps, MemoryStore, type Message, type Observation, type Plan, type PlanStatus, Planner, type Progress, type Provider, type Reflection, Reflector, type Role, type RunConfig, type RunContext, type RunEvent, type RunResult, type RunStatus, type RunSummary, ScriptedProvider, type ScriptedResponse, type SerializableConfig, type Step, type StepStatus, type StopReason, type Store, type Tool, type ToolCall, ToolRegistry, type ToolResult, type ToolSchema, checkBudget, createContext, defineTool, estimateTokens, fsTools, genId, nextPendingStep, reply, resolveSerializable, resume, run, runLoop, runWith, withRetry };
|
|
456
|
+
export { type AnthropicOptions, AnthropicProvider, type BudgetState, CliProvider, type CliProviderOptions, type CompletionRequest, type CompletionResult, ContextManager, DEFAULT_CONFIG, type Digest, Executor, FileStore, type Goal, type LoopDeps, MemoryStore, type Message, type Observation, type OpenAICompatOptions, OpenAICompatProvider, type Plan, type PlanStatus, Planner, type Progress, type Provider, type Reflection, Reflector, type Role, type RunConfig, type RunContext, type RunEvent, type RunResult, type RunStatus, type RunSummary, ScriptedProvider, type ScriptedResponse, type SerializableConfig, type Step, type StepStatus, type StopReason, type Store, type Tool, type ToolCall, ToolRegistry, type ToolResult, type ToolSchema, checkBudget, claudeCode, codexCli, createContext, defineTool, estimateTokens, fsTools, genId, nextPendingStep, ollama, reply, resolveSerializable, resume, run, runLoop, runWith, withRetry };
|
package/dist/index.d.ts
CHANGED
|
@@ -377,6 +377,75 @@ declare class AnthropicProvider implements Provider {
|
|
|
377
377
|
complete(req: CompletionRequest): Promise<CompletionResult>;
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
interface OpenAICompatOptions {
|
|
381
|
+
/** e.g. http://localhost:11434/v1 (Ollama), http://localhost:1234/v1 (LM Studio), https://api.openai.com/v1 */
|
|
382
|
+
baseUrl: string;
|
|
383
|
+
/** Optional — local servers don't need one. */
|
|
384
|
+
apiKey?: string;
|
|
385
|
+
model: string;
|
|
386
|
+
label?: string;
|
|
387
|
+
maxRetries?: number;
|
|
388
|
+
defaultMaxTokens?: number;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Talks the OpenAI Chat Completions format — which means it works with almost
|
|
392
|
+
* everything: **Ollama and LM Studio (local, no API key)**, llama.cpp's server,
|
|
393
|
+
* OpenRouter, Groq, Together, OpenAI itself… Point `baseUrl` at any of them.
|
|
394
|
+
* No SDK, no dependencies.
|
|
395
|
+
*/
|
|
396
|
+
declare class OpenAICompatProvider implements Provider {
|
|
397
|
+
readonly name: string;
|
|
398
|
+
private readonly baseUrl;
|
|
399
|
+
private readonly apiKey?;
|
|
400
|
+
private readonly model;
|
|
401
|
+
private readonly maxRetries;
|
|
402
|
+
private readonly defaultMaxTokens;
|
|
403
|
+
constructor(opts: OpenAICompatOptions);
|
|
404
|
+
estimateTokens(messages: Message[]): number;
|
|
405
|
+
complete(req: CompletionRequest): Promise<CompletionResult>;
|
|
406
|
+
}
|
|
407
|
+
/** Convenience: a local Ollama model, no API key. `ollama("llama3.1")`. */
|
|
408
|
+
declare function ollama(model: string, opts?: Partial<OpenAICompatOptions>): OpenAICompatProvider;
|
|
409
|
+
|
|
410
|
+
interface CliProviderOptions {
|
|
411
|
+
/** The executable, e.g. "claude" or "codex". */
|
|
412
|
+
command: string;
|
|
413
|
+
/** Fixed args before the prompt, e.g. ["-p"] for Claude Code print mode. */
|
|
414
|
+
args?: string[];
|
|
415
|
+
/** Pass the prompt as the final arg ("arg", default) or on stdin ("stdin"). */
|
|
416
|
+
promptVia?: "arg" | "stdin";
|
|
417
|
+
/** Extract the assistant text from the command's stdout. Default: trim. */
|
|
418
|
+
parse?: (stdout: string) => string;
|
|
419
|
+
env?: Record<string, string>;
|
|
420
|
+
timeoutMs?: number;
|
|
421
|
+
label?: string;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Drives an agentic CLI (Claude Code, Codex, …) in non-interactive mode: every
|
|
425
|
+
* model call shells out to the command and captures its stdout. This means
|
|
426
|
+
* people who use those CLIs via a **subscription login use oh-my-fable with no
|
|
427
|
+
* separate API key** — it rides whatever auth the CLI already has.
|
|
428
|
+
*
|
|
429
|
+
* Note: it returns text only (no native tool-calling), so the agent runs in
|
|
430
|
+
* pure-reasoning mode; `--tools fs` is unavailable through a CLI provider.
|
|
431
|
+
*/
|
|
432
|
+
declare class CliProvider implements Provider {
|
|
433
|
+
readonly name: string;
|
|
434
|
+
private readonly command;
|
|
435
|
+
private readonly args;
|
|
436
|
+
private readonly promptVia;
|
|
437
|
+
private readonly parse;
|
|
438
|
+
private readonly env?;
|
|
439
|
+
private readonly timeoutMs;
|
|
440
|
+
constructor(opts: CliProviderOptions);
|
|
441
|
+
estimateTokens(messages: Message[]): number;
|
|
442
|
+
complete(req: CompletionRequest): Promise<CompletionResult>;
|
|
443
|
+
}
|
|
444
|
+
/** Claude Code in print mode — uses your existing `claude` auth (subscription or key). */
|
|
445
|
+
declare function claudeCode(opts?: Partial<CliProviderOptions>): CliProvider;
|
|
446
|
+
/** OpenAI Codex CLI in non-interactive exec mode — uses your existing `codex` auth. */
|
|
447
|
+
declare function codexCli(opts?: Partial<CliProviderOptions>): CliProvider;
|
|
448
|
+
|
|
380
449
|
/** Run an agent to completion (or to a budget halt). The whole run is checkpointed every step. */
|
|
381
450
|
declare function run(goal: Goal | string, config: RunConfig): Promise<RunResult>;
|
|
382
451
|
/** Resume a run from its last checkpoint — same plan, same progress, continues where it died. */
|
|
@@ -384,4 +453,4 @@ declare function resume(runId: string, config: RunConfig): Promise<RunResult>;
|
|
|
384
453
|
/** Continue a RunContext you already hold in memory (advanced; same as resume without the store load). */
|
|
385
454
|
declare function runWith(ctx: RunContext, config: RunConfig): Promise<RunResult>;
|
|
386
455
|
|
|
387
|
-
export { type AnthropicOptions, AnthropicProvider, type BudgetState, type CompletionRequest, type CompletionResult, ContextManager, DEFAULT_CONFIG, type Digest, Executor, FileStore, type Goal, type LoopDeps, MemoryStore, type Message, type Observation, type Plan, type PlanStatus, Planner, type Progress, type Provider, type Reflection, Reflector, type Role, type RunConfig, type RunContext, type RunEvent, type RunResult, type RunStatus, type RunSummary, ScriptedProvider, type ScriptedResponse, type SerializableConfig, type Step, type StepStatus, type StopReason, type Store, type Tool, type ToolCall, ToolRegistry, type ToolResult, type ToolSchema, checkBudget, createContext, defineTool, estimateTokens, fsTools, genId, nextPendingStep, reply, resolveSerializable, resume, run, runLoop, runWith, withRetry };
|
|
456
|
+
export { type AnthropicOptions, AnthropicProvider, type BudgetState, CliProvider, type CliProviderOptions, type CompletionRequest, type CompletionResult, ContextManager, DEFAULT_CONFIG, type Digest, Executor, FileStore, type Goal, type LoopDeps, MemoryStore, type Message, type Observation, type OpenAICompatOptions, OpenAICompatProvider, type Plan, type PlanStatus, Planner, type Progress, type Provider, type Reflection, Reflector, type Role, type RunConfig, type RunContext, type RunEvent, type RunResult, type RunStatus, type RunSummary, ScriptedProvider, type ScriptedResponse, type SerializableConfig, type Step, type StepStatus, type StopReason, type Store, type Tool, type ToolCall, ToolRegistry, type ToolResult, type ToolSchema, checkBudget, claudeCode, codexCli, createContext, defineTool, estimateTokens, fsTools, genId, nextPendingStep, ollama, reply, resolveSerializable, resume, run, runLoop, runWith, withRetry };
|
package/dist/index.js
CHANGED
|
@@ -876,6 +876,164 @@ var AnthropicProvider = class {
|
|
|
876
876
|
}
|
|
877
877
|
};
|
|
878
878
|
|
|
879
|
+
// src/providers/openai.ts
|
|
880
|
+
var FINISH = { stop: "end", tool_calls: "tool_use", function_call: "tool_use", length: "max_tokens" };
|
|
881
|
+
var OpenAICompatProvider = class {
|
|
882
|
+
name;
|
|
883
|
+
baseUrl;
|
|
884
|
+
apiKey;
|
|
885
|
+
model;
|
|
886
|
+
maxRetries;
|
|
887
|
+
defaultMaxTokens;
|
|
888
|
+
constructor(opts) {
|
|
889
|
+
if (!opts.baseUrl) throw new Error("OpenAICompatProvider needs a baseUrl.");
|
|
890
|
+
if (!opts.model) throw new Error("OpenAICompatProvider needs a model.");
|
|
891
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
892
|
+
this.apiKey = opts.apiKey;
|
|
893
|
+
this.model = opts.model;
|
|
894
|
+
this.name = opts.label ?? "openai-compatible";
|
|
895
|
+
this.maxRetries = opts.maxRetries ?? 4;
|
|
896
|
+
this.defaultMaxTokens = opts.defaultMaxTokens ?? 4096;
|
|
897
|
+
}
|
|
898
|
+
estimateTokens(messages) {
|
|
899
|
+
return estimateTokens(messages);
|
|
900
|
+
}
|
|
901
|
+
async complete(req) {
|
|
902
|
+
const body = {
|
|
903
|
+
model: this.model,
|
|
904
|
+
messages: req.messages.map((m) => ({ role: m.role, content: m.content })),
|
|
905
|
+
max_tokens: req.maxTokens ?? this.defaultMaxTokens,
|
|
906
|
+
temperature: req.temperature ?? 1
|
|
907
|
+
};
|
|
908
|
+
if (req.tools?.length) {
|
|
909
|
+
body["tools"] = req.tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters } }));
|
|
910
|
+
}
|
|
911
|
+
const data = await withRetry(
|
|
912
|
+
async () => {
|
|
913
|
+
const headers = { "content-type": "application/json" };
|
|
914
|
+
if (this.apiKey) headers["authorization"] = `Bearer ${this.apiKey}`;
|
|
915
|
+
const res = await fetch(`${this.baseUrl}/chat/completions`, { method: "POST", headers, body: JSON.stringify(body) });
|
|
916
|
+
if (!res.ok) {
|
|
917
|
+
const text = await res.text().catch(() => "");
|
|
918
|
+
const err = new Error(`${this.name} ${res.status}: ${text.slice(0, 300)}`);
|
|
919
|
+
err.status = res.status;
|
|
920
|
+
throw err;
|
|
921
|
+
}
|
|
922
|
+
return await res.json();
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
retries: this.maxRetries,
|
|
926
|
+
isRetryable: (e) => {
|
|
927
|
+
const s = e.status;
|
|
928
|
+
return s === void 0 || s === 429 || s >= 500 && s < 600;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
);
|
|
932
|
+
const choice = data.choices?.[0];
|
|
933
|
+
const content = typeof choice?.message?.content === "string" ? choice.message.content : "";
|
|
934
|
+
const toolCalls = [];
|
|
935
|
+
for (const tc of choice?.message?.tool_calls ?? []) {
|
|
936
|
+
if (!tc.function?.name) continue;
|
|
937
|
+
let input = {};
|
|
938
|
+
try {
|
|
939
|
+
input = tc.function.arguments ? JSON.parse(tc.function.arguments) : {};
|
|
940
|
+
} catch {
|
|
941
|
+
input = tc.function.arguments ?? {};
|
|
942
|
+
}
|
|
943
|
+
toolCalls.push({ id: tc.id ?? tc.function.name, name: tc.function.name, input });
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
content,
|
|
947
|
+
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
948
|
+
tokensIn: data.usage?.prompt_tokens ?? 0,
|
|
949
|
+
tokensOut: data.usage?.completion_tokens ?? 0,
|
|
950
|
+
stopReason: FINISH[choice?.finish_reason ?? ""] ?? "end"
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
function ollama(model, opts = {}) {
|
|
955
|
+
return new OpenAICompatProvider({ baseUrl: "http://localhost:11434/v1", model, label: "ollama", ...opts });
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/providers/cli.ts
|
|
959
|
+
import { spawn } from "child_process";
|
|
960
|
+
function flatten(messages) {
|
|
961
|
+
const parts = [];
|
|
962
|
+
for (const m of messages) {
|
|
963
|
+
if (m.role === "assistant") parts.push(`Assistant: ${m.content}`);
|
|
964
|
+
else parts.push(m.content);
|
|
965
|
+
}
|
|
966
|
+
return parts.join("\n\n");
|
|
967
|
+
}
|
|
968
|
+
function runCli(command, args, input, timeoutMs, env) {
|
|
969
|
+
return new Promise((resolve2, reject) => {
|
|
970
|
+
let child;
|
|
971
|
+
try {
|
|
972
|
+
child = spawn(command, args, { env: { ...process.env, ...env } });
|
|
973
|
+
} catch (e) {
|
|
974
|
+
return reject(e);
|
|
975
|
+
}
|
|
976
|
+
let out = "";
|
|
977
|
+
let err = "";
|
|
978
|
+
const timer = setTimeout(() => {
|
|
979
|
+
child.kill("SIGKILL");
|
|
980
|
+
reject(new Error(`${command} timed out after ${timeoutMs} ms`));
|
|
981
|
+
}, timeoutMs);
|
|
982
|
+
child.stdout.on("data", (d) => out += d);
|
|
983
|
+
child.stderr.on("data", (d) => err += d);
|
|
984
|
+
child.on("error", (e) => {
|
|
985
|
+
clearTimeout(timer);
|
|
986
|
+
reject(e.code === "ENOENT" ? new Error(`"${command}" is not installed or not on your PATH.`) : e);
|
|
987
|
+
});
|
|
988
|
+
child.on("close", (code) => {
|
|
989
|
+
clearTimeout(timer);
|
|
990
|
+
if (code === 0) resolve2(out);
|
|
991
|
+
else reject(new Error(`${command} exited ${code}: ${err.trim().slice(0, 300)}`));
|
|
992
|
+
});
|
|
993
|
+
if (input !== null) child.stdin.write(input);
|
|
994
|
+
child.stdin.end();
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
var CliProvider = class {
|
|
998
|
+
name;
|
|
999
|
+
command;
|
|
1000
|
+
args;
|
|
1001
|
+
promptVia;
|
|
1002
|
+
parse;
|
|
1003
|
+
env;
|
|
1004
|
+
timeoutMs;
|
|
1005
|
+
constructor(opts) {
|
|
1006
|
+
this.command = opts.command;
|
|
1007
|
+
this.args = opts.args ?? [];
|
|
1008
|
+
this.promptVia = opts.promptVia ?? "arg";
|
|
1009
|
+
this.parse = opts.parse ?? ((s) => s.trim());
|
|
1010
|
+
this.env = opts.env;
|
|
1011
|
+
this.timeoutMs = opts.timeoutMs ?? 12e4;
|
|
1012
|
+
this.name = opts.label ?? `cli:${opts.command}`;
|
|
1013
|
+
}
|
|
1014
|
+
estimateTokens(messages) {
|
|
1015
|
+
return estimateTokens(messages);
|
|
1016
|
+
}
|
|
1017
|
+
async complete(req) {
|
|
1018
|
+
const prompt = flatten(req.messages);
|
|
1019
|
+
const args = this.promptVia === "arg" ? [...this.args, prompt] : this.args;
|
|
1020
|
+
const stdout = await runCli(this.command, args, this.promptVia === "stdin" ? prompt : null, this.timeoutMs, this.env);
|
|
1021
|
+
const content = this.parse(stdout);
|
|
1022
|
+
return {
|
|
1023
|
+
content,
|
|
1024
|
+
tokensIn: estimateTokens(req.messages),
|
|
1025
|
+
tokensOut: Math.ceil(content.length / 4),
|
|
1026
|
+
stopReason: "end"
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
function claudeCode(opts = {}) {
|
|
1031
|
+
return new CliProvider({ command: "claude", args: ["-p"], promptVia: "arg", label: "claude-code", ...opts });
|
|
1032
|
+
}
|
|
1033
|
+
function codexCli(opts = {}) {
|
|
1034
|
+
return new CliProvider({ command: "codex", args: ["exec"], promptVia: "arg", label: "codex", ...opts });
|
|
1035
|
+
}
|
|
1036
|
+
|
|
879
1037
|
// src/index.ts
|
|
880
1038
|
function buildDeps(config, serializable) {
|
|
881
1039
|
const provider = config.provider;
|
|
@@ -908,22 +1066,27 @@ async function runWith(ctx, config) {
|
|
|
908
1066
|
}
|
|
909
1067
|
export {
|
|
910
1068
|
AnthropicProvider,
|
|
1069
|
+
CliProvider,
|
|
911
1070
|
ContextManager,
|
|
912
1071
|
DEFAULT_CONFIG,
|
|
913
1072
|
Executor,
|
|
914
1073
|
FileStore,
|
|
915
1074
|
MemoryStore,
|
|
1075
|
+
OpenAICompatProvider,
|
|
916
1076
|
Planner,
|
|
917
1077
|
Reflector,
|
|
918
1078
|
ScriptedProvider,
|
|
919
1079
|
ToolRegistry,
|
|
920
1080
|
checkBudget,
|
|
1081
|
+
claudeCode,
|
|
1082
|
+
codexCli,
|
|
921
1083
|
createContext,
|
|
922
1084
|
defineTool,
|
|
923
1085
|
estimateTokens,
|
|
924
1086
|
fsTools,
|
|
925
1087
|
genId,
|
|
926
1088
|
nextPendingStep,
|
|
1089
|
+
ollama,
|
|
927
1090
|
reply,
|
|
928
1091
|
resolveSerializable,
|
|
929
1092
|
resume,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-my-fable",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "The autonomous-agent harness that actually finishes long tasks — because it plans first, self-corrects every step, and survives crashes. The whole run lives in one serializable RunContext, checkpointed after every step, so it resumes exactly where it died. Model-agnostic, zero dependencies, deterministically testable.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|