otterly 0.3.2 → 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 CHANGED
@@ -17,7 +17,7 @@ const { values, positionals } = parseArgs({
17
17
  });
18
18
  const command = positionals[0] || "serve";
19
19
  if (values.version) {
20
- console.log("0.3.2");
20
+ console.log("0.3.3");
21
21
  process.exit(0);
22
22
  }
23
23
  if (values.help || command === "help") {
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 primary SDK first, then the alternative package name
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
- throw new AgentError("SDK_NOT_FOUND", "Could not find Claude Code SDK. Install it:\n npm install @anthropic-ai/claude-code");
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;
@@ -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.2", playground: "/playground" });
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 ──
@@ -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.2";
8
+ const PKG_VERSION = "0.3.3";
9
9
  /**
10
10
  * GET /api/status — health check with queue and circuit breaker stats
11
11
  */
@@ -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.2",
6
+ version: "0.3.3",
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.2" },
24
+ version: { type: "string", example: "0.3.3" },
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.2" },
70
+ version: { type: "string", example: "0.3.3" },
71
71
  playground: { type: "string", example: "/playground" },
72
72
  },
73
73
  required: ["name", "version", "playground"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "otterly",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Local AI inference for your apps. Use Claude Code instead of paying for API tokens.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",