otterly 0.3.3 → 0.3.4
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 +95 -91
- package/dist/server/index.js +1 -1
- package/dist/server/playground.js +1 -1
- 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,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
import { Worker } from "worker_threads";
|
|
3
3
|
import { normalizeEvents, createEventContext } from "./events.js";
|
|
4
4
|
import { classifyError, AgentError } from "./errors.js";
|
|
5
5
|
import { wrapPermissionHandler } from "./permissions.js";
|
|
@@ -7,15 +7,12 @@ import { Session } from "./session.js";
|
|
|
7
7
|
let cachedQueryFn = null;
|
|
8
8
|
let resolvedMode = null;
|
|
9
9
|
/**
|
|
10
|
-
* Find the `claude` CLI binary. Returns the
|
|
10
|
+
* Find the `claude` CLI binary. Returns the name or null.
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
const { execFileSync } = await import("child_process");
|
|
14
|
-
// Check common names: claude, claude-code
|
|
12
|
+
function findClaudeCLI() {
|
|
15
13
|
for (const bin of ["claude", "claude-code"]) {
|
|
16
14
|
try {
|
|
17
15
|
execFileSync("which", [bin], { stdio: "pipe" });
|
|
18
|
-
// Verify it actually runs
|
|
19
16
|
execFileSync(bin, ["--version"], { stdio: "pipe", timeout: 5000 });
|
|
20
17
|
return bin;
|
|
21
18
|
}
|
|
@@ -26,102 +23,109 @@ async function findClaudeCLI() {
|
|
|
26
23
|
return null;
|
|
27
24
|
}
|
|
28
25
|
/**
|
|
29
|
-
* Build a
|
|
30
|
-
*
|
|
31
|
-
|
|
26
|
+
* Build a shell command string for `claude -p`.
|
|
27
|
+
* Escapes the prompt for safe shell embedding.
|
|
28
|
+
*/
|
|
29
|
+
function buildCLICommand(cliBin, prompt, opts) {
|
|
30
|
+
// Shell-escape single quotes in prompt
|
|
31
|
+
const safePrompt = prompt.replace(/'/g, "'\\''");
|
|
32
|
+
const parts = [cliBin, "-p", `'${safePrompt}'`, "--output-format", "stream-json", "--verbose"];
|
|
33
|
+
if (opts.cwd)
|
|
34
|
+
parts.push("--cwd", `'${String(opts.cwd).replace(/'/g, "'\\''")}'`);
|
|
35
|
+
if (opts.model)
|
|
36
|
+
parts.push("--model", String(opts.model));
|
|
37
|
+
if (opts.maxTurns)
|
|
38
|
+
parts.push("--max-turns", String(opts.maxTurns));
|
|
39
|
+
if (opts.systemPrompt) {
|
|
40
|
+
const safe = String(opts.systemPrompt).replace(/'/g, "'\\''");
|
|
41
|
+
parts.push("--system-prompt", `'${safe}'`);
|
|
42
|
+
}
|
|
43
|
+
if (opts.resume)
|
|
44
|
+
parts.push("--resume", String(opts.resume));
|
|
45
|
+
if (opts.permissionMode)
|
|
46
|
+
parts.push("--permission-mode", String(opts.permissionMode));
|
|
47
|
+
if (opts.allowedTools) {
|
|
48
|
+
for (const tool of opts.allowedTools)
|
|
49
|
+
parts.push("--allowedTools", tool);
|
|
50
|
+
}
|
|
51
|
+
if (opts.disallowedTools) {
|
|
52
|
+
for (const tool of opts.disallowedTools)
|
|
53
|
+
parts.push("--disallowedTools", tool);
|
|
54
|
+
}
|
|
55
|
+
return parts.join(" ");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build a QueryFn that runs `claude -p` via execSync in a worker thread.
|
|
59
|
+
*
|
|
60
|
+
* The Bun-compiled `claude` binary doesn't pipe stdout to Node.js child_process
|
|
61
|
+
* async APIs (spawn/exec), but execSync through a shell works reliably.
|
|
62
|
+
* We run it in a worker thread to avoid blocking the event loop.
|
|
32
63
|
*/
|
|
33
64
|
function createCLIQueryFn(cliBin) {
|
|
34
65
|
return function cliQuery(args) {
|
|
35
66
|
const opts = args.options || {};
|
|
36
67
|
const prompt = typeof args.prompt === "string" ? args.prompt : JSON.stringify(args.prompt);
|
|
37
|
-
const
|
|
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;
|
|
68
|
+
const cmd = buildCLICommand(cliBin, prompt, opts);
|
|
65
69
|
return {
|
|
66
|
-
[Symbol.asyncIterator]() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
async *[Symbol.asyncIterator]() {
|
|
71
|
+
// Run execSync in a worker thread to keep the event loop free
|
|
72
|
+
const stdout = await new Promise((resolve, reject) => {
|
|
73
|
+
const workerCode = `
|
|
74
|
+
const { parentPort, workerData } = require('worker_threads');
|
|
75
|
+
const { execSync } = require('child_process');
|
|
76
|
+
try {
|
|
77
|
+
const out = execSync(workerData.cmd, {
|
|
78
|
+
encoding: 'utf-8',
|
|
79
|
+
timeout: workerData.timeout,
|
|
80
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
81
|
+
env: process.env,
|
|
82
|
+
});
|
|
83
|
+
parentPort.postMessage({ ok: true, data: out });
|
|
84
|
+
} catch (err) {
|
|
85
|
+
parentPort.postMessage({ ok: false, error: err.message });
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
const timeout = opts.abortController
|
|
89
|
+
? 10 * 60 * 1000 // 10 min for streaming
|
|
90
|
+
: 5 * 60 * 1000; // 5 min for one-shot
|
|
91
|
+
const worker = new Worker(workerCode, {
|
|
92
|
+
eval: true,
|
|
93
|
+
workerData: { cmd, timeout },
|
|
79
94
|
});
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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;
|
|
95
|
+
worker.on("message", (msg) => {
|
|
96
|
+
if (msg.ok) {
|
|
97
|
+
resolve(msg.data || "");
|
|
91
98
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
else {
|
|
100
|
+
reject(new Error(msg.error || "CLI execution failed"));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
worker.on("error", reject);
|
|
104
|
+
worker.on("exit", (code) => {
|
|
105
|
+
if (code !== 0)
|
|
106
|
+
reject(new Error(`Worker exited with code ${code}`));
|
|
107
|
+
});
|
|
108
|
+
// Handle abort
|
|
109
|
+
const ac = opts.abortController;
|
|
110
|
+
if (ac) {
|
|
111
|
+
ac.signal.addEventListener("abort", () => {
|
|
112
|
+
worker.terminate();
|
|
113
|
+
reject(new Error("Aborted"));
|
|
114
|
+
});
|
|
95
115
|
}
|
|
96
116
|
});
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
// Parse the NDJSON output line by line and yield each event
|
|
118
|
+
const lines = stdout.trim().split("\n");
|
|
119
|
+
for (const line of lines) {
|
|
120
|
+
if (!line.trim())
|
|
121
|
+
continue;
|
|
122
|
+
try {
|
|
123
|
+
yield JSON.parse(line);
|
|
103
124
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
done = true;
|
|
107
|
-
if (resolveNext) {
|
|
108
|
-
resolveNext();
|
|
109
|
-
resolveNext = null;
|
|
125
|
+
catch {
|
|
126
|
+
// Skip non-JSON lines
|
|
110
127
|
}
|
|
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
|
-
};
|
|
128
|
+
}
|
|
125
129
|
},
|
|
126
130
|
};
|
|
127
131
|
};
|
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.4", playground: "/playground" });
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
120
|
// ── POST routes: auth → rate limit → circuit breaker → queue ──
|
|
@@ -1335,7 +1335,7 @@ function playgroundHTML() {
|
|
|
1335
1335
|
<div class="header-left">
|
|
1336
1336
|
<span class="logo">\u{1F9A6}</span>
|
|
1337
1337
|
<span class="logo-text">otterly</span>
|
|
1338
|
-
<span class="version-badge">v0.3.
|
|
1338
|
+
<span class="version-badge">v0.3.4</span>
|
|
1339
1339
|
</div>
|
|
1340
1340
|
<div class="header-center">
|
|
1341
1341
|
<nav class="nav">
|
|
@@ -5,7 +5,7 @@ import { AgentError } from "../errors.js";
|
|
|
5
5
|
import { apiSessions } from "./session-store.js";
|
|
6
6
|
import { errorToHttpStatus } from "./openai-compat.js";
|
|
7
7
|
import { logError } from "./logger.js";
|
|
8
|
-
const PKG_VERSION = "0.3.
|
|
8
|
+
const PKG_VERSION = "0.3.4";
|
|
9
9
|
/**
|
|
10
10
|
* GET /api/status — health check with queue and circuit breaker stats
|
|
11
11
|
*/
|
package/dist/server/swagger.js
CHANGED
|
@@ -3,7 +3,7 @@ export const openApiSpec = {
|
|
|
3
3
|
openapi: "3.0.3",
|
|
4
4
|
info: {
|
|
5
5
|
title: "Otterly API",
|
|
6
|
-
version: "0.3.
|
|
6
|
+
version: "0.3.4",
|
|
7
7
|
description: "Local inference server with OpenAI-compatible and native endpoints. " +
|
|
8
8
|
"WebSocket available at ws://localhost:{port}/ws for interactive sessions.",
|
|
9
9
|
},
|
|
@@ -21,7 +21,7 @@ export const openApiSpec = {
|
|
|
21
21
|
type: "object",
|
|
22
22
|
properties: {
|
|
23
23
|
status: { type: "string", example: "ok" },
|
|
24
|
-
version: { type: "string", example: "0.3.
|
|
24
|
+
version: { type: "string", example: "0.3.4" },
|
|
25
25
|
activeSessions: { type: "integer" },
|
|
26
26
|
queue: {
|
|
27
27
|
type: "object",
|
|
@@ -67,7 +67,7 @@ export const openApiSpec = {
|
|
|
67
67
|
type: "object",
|
|
68
68
|
properties: {
|
|
69
69
|
name: { type: "string", example: "otterly" },
|
|
70
|
-
version: { type: "string", example: "0.3.
|
|
70
|
+
version: { type: "string", example: "0.3.4" },
|
|
71
71
|
playground: { type: "string", example: "/playground" },
|
|
72
72
|
},
|
|
73
73
|
required: ["name", "version", "playground"],
|