otterly 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/engine.js +133 -2
- package/dist/server/index.js +1 -1
- package/dist/server/playground.js +1075 -676
- package/dist/server/routes-native.js +1 -1
- package/dist/server/swagger.js +3 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
package/dist/engine.js
CHANGED
|
@@ -1,18 +1,142 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { createInterface } from "readline";
|
|
1
3
|
import { normalizeEvents, createEventContext } from "./events.js";
|
|
2
4
|
import { classifyError, AgentError } from "./errors.js";
|
|
3
5
|
import { wrapPermissionHandler } from "./permissions.js";
|
|
4
6
|
import { Session } from "./session.js";
|
|
5
7
|
let cachedQueryFn = null;
|
|
8
|
+
let resolvedMode = null;
|
|
9
|
+
/**
|
|
10
|
+
* Find the `claude` CLI binary. Returns the path or null.
|
|
11
|
+
*/
|
|
12
|
+
async function findClaudeCLI() {
|
|
13
|
+
const { execFileSync } = await import("child_process");
|
|
14
|
+
// Check common names: claude, claude-code
|
|
15
|
+
for (const bin of ["claude", "claude-code"]) {
|
|
16
|
+
try {
|
|
17
|
+
execFileSync("which", [bin], { stdio: "pipe" });
|
|
18
|
+
// Verify it actually runs
|
|
19
|
+
execFileSync(bin, ["--version"], { stdio: "pipe", timeout: 5000 });
|
|
20
|
+
return bin;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Try next
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build a QueryFn that spawns `claude -p` with stream-json output.
|
|
30
|
+
* The CLI emits the same JSON event types the SDK does, so normalizeEvents
|
|
31
|
+
* works unchanged.
|
|
32
|
+
*/
|
|
33
|
+
function createCLIQueryFn(cliBin) {
|
|
34
|
+
return function cliQuery(args) {
|
|
35
|
+
const opts = args.options || {};
|
|
36
|
+
const prompt = typeof args.prompt === "string" ? args.prompt : JSON.stringify(args.prompt);
|
|
37
|
+
const cliArgs = [
|
|
38
|
+
"-p", prompt,
|
|
39
|
+
"--output-format", "stream-json",
|
|
40
|
+
"--verbose",
|
|
41
|
+
];
|
|
42
|
+
if (opts.cwd)
|
|
43
|
+
cliArgs.push("--cwd", String(opts.cwd));
|
|
44
|
+
if (opts.model)
|
|
45
|
+
cliArgs.push("--model", String(opts.model));
|
|
46
|
+
if (opts.maxTurns)
|
|
47
|
+
cliArgs.push("--max-turns", String(opts.maxTurns));
|
|
48
|
+
if (opts.systemPrompt)
|
|
49
|
+
cliArgs.push("--system-prompt", String(opts.systemPrompt));
|
|
50
|
+
if (opts.resume)
|
|
51
|
+
cliArgs.push("--resume", String(opts.resume));
|
|
52
|
+
if (opts.permissionMode)
|
|
53
|
+
cliArgs.push("--permission-mode", String(opts.permissionMode));
|
|
54
|
+
if (opts.allowedTools) {
|
|
55
|
+
for (const tool of opts.allowedTools) {
|
|
56
|
+
cliArgs.push("--allowedTools", tool);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (opts.disallowedTools) {
|
|
60
|
+
for (const tool of opts.disallowedTools) {
|
|
61
|
+
cliArgs.push("--disallowedTools", tool);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const abortController = opts.abortController;
|
|
65
|
+
return {
|
|
66
|
+
[Symbol.asyncIterator]() {
|
|
67
|
+
let lineBuffer = [];
|
|
68
|
+
let done = false;
|
|
69
|
+
let error = null;
|
|
70
|
+
let resolveNext = null;
|
|
71
|
+
const child = spawn(cliBin, cliArgs, {
|
|
72
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
73
|
+
env: { ...process.env },
|
|
74
|
+
});
|
|
75
|
+
// Handle abort
|
|
76
|
+
if (abortController) {
|
|
77
|
+
abortController.signal.addEventListener("abort", () => {
|
|
78
|
+
child.kill("SIGTERM");
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const rl = createInterface({ input: child.stdout });
|
|
82
|
+
rl.on("line", (line) => {
|
|
83
|
+
if (!line.trim())
|
|
84
|
+
return;
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(line);
|
|
87
|
+
lineBuffer.push(parsed);
|
|
88
|
+
if (resolveNext) {
|
|
89
|
+
resolveNext();
|
|
90
|
+
resolveNext = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Skip non-JSON lines (stderr leakage, etc.)
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
child.on("error", (err) => {
|
|
98
|
+
error = err;
|
|
99
|
+
done = true;
|
|
100
|
+
if (resolveNext) {
|
|
101
|
+
resolveNext();
|
|
102
|
+
resolveNext = null;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
child.on("close", () => {
|
|
106
|
+
done = true;
|
|
107
|
+
if (resolveNext) {
|
|
108
|
+
resolveNext();
|
|
109
|
+
resolveNext = null;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return {
|
|
113
|
+
async next() {
|
|
114
|
+
while (lineBuffer.length === 0 && !done) {
|
|
115
|
+
await new Promise((resolve) => { resolveNext = resolve; });
|
|
116
|
+
}
|
|
117
|
+
if (error)
|
|
118
|
+
throw error;
|
|
119
|
+
if (lineBuffer.length > 0) {
|
|
120
|
+
return { value: lineBuffer.shift(), done: false };
|
|
121
|
+
}
|
|
122
|
+
return { value: undefined, done: true };
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
}
|
|
6
129
|
async function resolveSDK() {
|
|
7
130
|
if (cachedQueryFn)
|
|
8
131
|
return cachedQueryFn;
|
|
9
|
-
// Try the
|
|
132
|
+
// 1. Try the npm SDK packages
|
|
10
133
|
for (const pkg of ["@anthropic-ai/claude-code", "@anthropic-ai/claude-agent-sdk"]) {
|
|
11
134
|
try {
|
|
12
135
|
const mod = await import(pkg);
|
|
13
136
|
const fn = mod.query || mod.default?.query;
|
|
14
137
|
if (typeof fn === "function") {
|
|
15
138
|
cachedQueryFn = fn;
|
|
139
|
+
resolvedMode = "sdk";
|
|
16
140
|
return cachedQueryFn;
|
|
17
141
|
}
|
|
18
142
|
}
|
|
@@ -20,7 +144,14 @@ async function resolveSDK() {
|
|
|
20
144
|
// Try next
|
|
21
145
|
}
|
|
22
146
|
}
|
|
23
|
-
|
|
147
|
+
// 2. Fall back to the CLI binary (works regardless of install method)
|
|
148
|
+
const cliBin = await findClaudeCLI();
|
|
149
|
+
if (cliBin) {
|
|
150
|
+
cachedQueryFn = createCLIQueryFn(cliBin);
|
|
151
|
+
resolvedMode = "cli";
|
|
152
|
+
return cachedQueryFn;
|
|
153
|
+
}
|
|
154
|
+
throw new AgentError("SDK_NOT_FOUND", "Could not find Claude Code. Install it from https://docs.anthropic.com/en/docs/claude-code or:\n npm install -g @anthropic-ai/claude-code");
|
|
24
155
|
}
|
|
25
156
|
export class ClaudeEngine {
|
|
26
157
|
defaults;
|
package/dist/server/index.js
CHANGED
|
@@ -114,7 +114,7 @@ export async function startApiServer(opts = {}) {
|
|
|
114
114
|
}
|
|
115
115
|
// GET / — server info
|
|
116
116
|
if (req.method === "GET" && path === "/") {
|
|
117
|
-
jsonResponse(res, 200, { name: "otterly", version: "0.3.
|
|
117
|
+
jsonResponse(res, 200, { name: "otterly", version: "0.3.3", playground: "/playground" });
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
120
|
// ── POST routes: auth → rate limit → circuit breaker → queue ──
|