@justin06lee/yagami 0.4.1 → 0.6.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 CHANGED
@@ -4,54 +4,62 @@
4
4
 
5
5
  # yagami
6
6
 
7
- **Your signed-in coding-agent CLIs as one self-hosted Anthropic-compatible API.**<br>
8
- *Claude Code, Codex, OpenCode, Gemini CLI and any ACP agent — point any Anthropic client at your own subscriptions, or embed the engine as a library.*
7
+ **Your signed-in coding-agent CLIs as one self-hosted Anthropic- and OpenAI-compatible API.**<br>
8
+ *Claude Code, Codex, OpenCode, Gemini CLI and any ACP agent — point any Anthropic or OpenAI client at your own subscriptions, or embed the engine as a library with zero config.*
9
9
 
10
10
  </div>
11
11
 
12
12
  ---
13
13
 
14
- yagami does the T3-Code trick, generalized: it drives the coding-agent CLIs you already installed and logged into — Claude Code through the Agent SDK (`pathToClaudeCodeExecutable`), Codex through `codex exec`, and OpenCode, Gemini CLI, Copilot, Cursor, Qwen Code, Kimi, Goose and the rest of the [ACP registry](https://agentclientprotocol.com) through the Agent Client Protocol. No API keys from any vendor, no separate auth — each spawned engine uses the same login your terminal sessions do. On top it serves `POST /v1/messages` with Anthropic request/response shapes and SSE streaming, so anything that accepts an Anthropic `baseURL` + `apiKey` can use it as a drop-in, and pick a harness per request with `model: "<provider>:<model>"`.
14
+ yagami does the T3-Code trick, generalized: it drives the coding-agent CLIs you already installed and logged into — Claude Code through the Agent SDK (`pathToClaudeCodeExecutable`), Codex through `codex exec`, and OpenCode, Gemini CLI, Copilot, Cursor, Qwen Code, Kimi, Goose and the rest of the [ACP registry](https://agentclientprotocol.com) through the Agent Client Protocol. No API keys from any vendor, no separate auth — each spawned engine uses the same login your terminal sessions do.
15
+
16
+ There are two doors in, and they're deliberately different:
17
+
18
+ | | Who it's for | What you get |
19
+ |---|---|---|
20
+ | **Server** — the `yagami` binary | A machine that should hand out an API key (a home server, a box your other apps talk to) | `yagami start` prints a **URL + API key** that works with any app that accepts an Anthropic *or* OpenAI base URL + key |
21
+ | **Library** — `@justin06lee/yagami` | An app running *on* the machine with the signed-in CLIs | `new Yagami()` — **no URL, no API key, nothing to configure**. It finds the CLIs itself and syncs with the binary's config |
15
22
 
16
23
  > **Personal use only.** This exists so *you* can point *your own tools* at *your own subscriptions*. Offering subscription-backed access to other people is against every one of these vendors' terms. Keep the endpoint private and don't share keys.
17
24
 
18
- ## Install
25
+ ## Server mode
19
26
 
20
27
  ```sh
21
28
  make # bun install + build + install `yagami` onto your PATH (~/.local/bin)
22
29
  yagami start # first run generates + saves an API key and prints it
23
30
  ```
24
31
 
25
- `make update` stops any running yagami server, rebuilds, reinstalls, and restarts it.
26
-
27
- Or install the published package from npm:
28
-
29
- ```sh
30
- bun add -g @justin06lee/yagami # global `yagami` CLI
31
- bun add @justin06lee/yagami # or as a library (see Library mode)
32
- ```
32
+ `make update` stops any running yagami server, rebuilds, reinstalls, and restarts it. Or install from npm: `bun add -g @justin06lee/yagami`.
33
33
 
34
34
  ```
35
- yagami v0.4.1
35
+ yagami v0.5.0
36
36
  listening http://127.0.0.1:8787
37
37
  provider claude — /Users/you/.local/bin/claude (2.1.238 (Claude Code))
38
38
  also codex, opencode (use model "<provider>:<model>")
39
39
  api key ygm_…
40
+
41
+ Connect apps — either dialect, same key (`yagami key` prints ready-to-paste env exports):
42
+ Anthropic apps baseURL http://127.0.0.1:8787 (ANTHROPIC_BASE_URL / ANTHROPIC_API_KEY)
43
+ OpenAI apps baseURL http://127.0.0.1:8787/v1 (OPENAI_BASE_URL / OPENAI_API_KEY)
44
+ ```
45
+
46
+ Apps can't be reached by a key alone — they need somewhere to route — so the pair is always **URL + key**. Most apps take them as a "base URL" field or the standard env vars; `yagami key` prints both ready to paste:
47
+
48
+ ```sh
49
+ export ANTHROPIC_BASE_URL=http://127.0.0.1:8787 ANTHROPIC_API_KEY=ygm_...
50
+ export OPENAI_BASE_URL=http://127.0.0.1:8787/v1 OPENAI_API_KEY=ygm_...
40
51
  ```
41
52
 
42
- Then from any Anthropic client:
53
+ Anthropic-dialect apps speak to `POST /v1/messages`, OpenAI-dialect apps to `POST /v1/chat/completions` — same engine, same key, streaming included:
43
54
 
44
55
  ```ts
45
56
  import Anthropic from "@anthropic-ai/sdk";
57
+ const anthropic = new Anthropic({ baseURL: "http://127.0.0.1:8787", apiKey: process.env.YAGAMI_KEY });
58
+ await anthropic.messages.create({ model: "sonnet", max_tokens: 1024, messages: [{ role: "user", content: "hello" }] });
46
59
 
47
- const client = new Anthropic({
48
- baseURL: "http://127.0.0.1:8787",
49
- apiKey: process.env.YAGAMI_KEY, // your ygm_ key
50
- });
51
-
52
- await client.messages.create({ model: "sonnet", max_tokens: 1024, messages: [{ role: "user", content: "hello" }] });
53
- await client.messages.create({ model: "codex:gpt-5.6-sol", max_tokens: 1024, messages: [{ role: "user", content: "hello" }] });
54
- await client.messages.create({ model: "opencode:anthropic/claude-sonnet-4", max_tokens: 1024, messages: [{ role: "user", content: "hello" }] });
60
+ import OpenAI from "openai";
61
+ const openai = new OpenAI({ baseURL: "http://127.0.0.1:8787/v1", apiKey: process.env.YAGAMI_KEY });
62
+ await openai.chat.completions.create({ model: "codex:gpt-5.6-sol", messages: [{ role: "user", content: "hello" }] });
55
63
  ```
56
64
 
57
65
  Or raw curl:
@@ -60,8 +68,111 @@ Or raw curl:
60
68
  curl http://127.0.0.1:8787/v1/messages \
61
69
  -H "x-api-key: ygm_..." -H "content-type: application/json" \
62
70
  -d '{"model":"codex","max_tokens":64,"messages":[{"role":"user","content":"ping"}]}'
71
+
72
+ curl http://127.0.0.1:8787/v1/chat/completions \
73
+ -H "authorization: Bearer ygm_..." -H "content-type: application/json" \
74
+ -d '{"model":"codex","messages":[{"role":"user","content":"ping"}]}'
75
+ ```
76
+
77
+ One caveat for OpenAI-dialect apps: the `model` field still routes through yagami's providers — set it to a model your CLIs actually serve (`sonnet`, `codex:gpt-5.6-sol`, `opencode:…`), not whatever `gpt-*` id the app defaults to.
78
+
79
+ ## Library mode
80
+
81
+ For apps that run on the machine with the signed-in CLIs — a desktop app, a script, anything embedding the engine in-process. **No server, no URL, no API key**: `new Yagami()` auto-detects the CLIs and reads `~/.config/yagami/config.json` if the binary has one, so library and server stay in sync automatically.
82
+
83
+ ```ts
84
+ import { Yagami } from "@justin06lee/yagami";
85
+
86
+ const yagami = new Yagami(); // that's it — hooks straight into the host's CLIs
87
+
88
+ // Anthropic SDK shape:
89
+ const msg = await yagami.messages.create({ messages: [{ role: "user", content: "hello" }] });
90
+ for await (const ev of yagami.messages.create({ messages: [...], stream: true })) { /* Anthropic stream events */ }
91
+
92
+ // OpenAI SDK shape, same engine:
93
+ const completion = await yagami.chat.completions.create({ messages: [{ role: "user", content: "hello" }] });
94
+ for await (const chunk of yagami.chat.completions.create({ messages: [...], stream: true })) { /* chat chunks */ }
95
+
96
+ // Models across every installed harness:
97
+ const { data } = await yagami.models.list();
98
+ ```
99
+
100
+ Options are for overrides only (`new Yagami({ defaultModel: "sonnet" })`, `{ defaultProvider: "codex" }`, `{ syncHostConfig: false }`, …); the zero-argument form is the intended use. For lower-level control the engine underneath is `yagami.engine` (a `YagamiEngine` — `complete()` returns cost/session/provider metadata, `stream()` returns raw SSE events), and you can hand-pick providers instead of auto-detecting:
101
+
102
+ ```ts
103
+ import { YagamiEngine, ClaudeProvider, AcpProvider } from "@justin06lee/yagami";
104
+
105
+ const engine = new YagamiEngine({
106
+ providers: [new ClaudeProvider(), new AcpProvider({ id: "gemini", label: "Gemini", command: "gemini", args: ["--acp"] })],
107
+ });
108
+ const { response, costUsd } = await engine.complete({ messages: [{ role: "user", content: "hello" }] });
63
109
  ```
64
110
 
111
+ Every provider implements one small `Provider` contract (`run(turn)` → normalized `session`/`text`/`thinking`/`done` events, plus `listModels()` and `version()`), so adding a harness that isn't ACP-capable is one file. Failures are typed: `AuthRequiredError` (carries the login command), `ProviderNotInstalledError` (carries the install hint), `ProviderError`.
112
+
113
+ ### Building a UI on Claude Code
114
+
115
+ `Yagami`/`YagamiEngine` are completions-only by design. To build an actual coding UI — tools, permissions, plan mode, a warm session across turns — use `AgentSession`, which wraps the full Claude Code agent with the lifecycle the interactive terminal gives you for free:
116
+
117
+ ```ts
118
+ import { AgentSession } from "@justin06lee/yagami";
119
+
120
+ const session = new AgentSession({
121
+ cwd: "/path/to/project",
122
+ parity: "terminal", // load your CLAUDE.md, skills, hooks, .mcp.json — like the CLI
123
+ appName: "my-app", // reported to Claude as the client
124
+ onPermission: async (req) => {
125
+ // Your approve/deny UI. Policy stays here; yagami owns the state machine.
126
+ const ok = await showDialog(req.toolName, req.input);
127
+ return ok ? { behavior: "allow" } : { behavior: "deny", message: "user declined" };
128
+ },
129
+ });
130
+
131
+ session.send("fix the failing test"); // process starts here and stays warm
132
+ for await (const msg of session) { // raw SDKMessages — render however you like
133
+ render(msg);
134
+ if (msg.type === "result") break;
135
+ }
136
+ session.send("now add a test for the edge case"); // next turn resumes the same session
137
+ await session.interrupt(); // the CLI's Esc
138
+ await session.setModel("opus"); // the CLI's /model
139
+ await session.setPermissionMode("plan"); // shift+tab
140
+ session.close();
141
+ ```
142
+
143
+ This resolves the parts of embedding Claude Code that every host would otherwise reimplement identically — process lifecycle, session resume, interrupt, settings parity, and the permission state machine. What stays yours are the genuinely app-specific choices: rendering the `SDKMessage` stream, deciding what to auto-approve, and picking the working directory. `parity` is `"terminal"` (load user+project+local settings, matching your CLI), `"project"` (project+local only), or `"isolated"` (load nothing — reproducible, no personal config). The permission `fallback` defaults to `"deny"`, so a session is safe before the UI is wired up; `autoAllow`/`autoDeny` skip the handler for named tools.
144
+
145
+ For a lower-level handle, `claudeCodeSession(prompt, { options })` returns the raw Agent SDK `Query`.
146
+
147
+ ### Building a UI on any other harness
148
+
149
+ The same idea works for the non-Claude harnesses — verbatim. Codex and every ACP agent implement `SessionProvider.openSession()`: a live, warm session on the harness's own engine (`codex app-server` — what the Codex TUI runs on; a persistent ACP connection for OpenCode, Gemini, and friends), with the harness's own config, sandbox, and approval flow. Nothing is overridden unless you pass `native` overrides; approval requests are forwarded to your handler exactly as the harness's own UI would prompt.
150
+
151
+ ```ts
152
+ import { createProvider, isSessionProvider } from "@justin06lee/yagami";
153
+
154
+ const codex = createProvider("codex", {}, { appName: "my-app" });
155
+ if (isSessionProvider(codex)) {
156
+ const session = codex.openSession({
157
+ cwd: "/path/to/project",
158
+ permissions: {
159
+ decide: async (req) => (await showDialog(req.tool, req.input)) ? "allow" : "deny",
160
+ }, // "allow_always" answers like the TUI's "don't ask again"
161
+ });
162
+ for await (const ev of session.send("fix the failing test")) {
163
+ // normalized AgentEvents: text / thinking / tool_call (started→completed,
164
+ // with inputs and outputs) / permission / done (usage, stop reason)
165
+ }
166
+ session.send("now add a test"); // same warm thread, context carries
167
+ await session.interrupt();
168
+ await session.close(); // session.id resumes it later via { resume }
169
+ }
170
+ ```
171
+
172
+ `ProviderSessionOptions` takes `cwd`, `model`, `resume`, `effort`, `systemPrompt` (extra developer instructions where the harness supports them), and a `native` escape hatch (Codex: `{ sandbox, approvalPolicy, config }`; ACP: `{ mode }`). The completion-turn `run()` path stays for API-style callers; sessions are for hosts that want the real interactive agent.
173
+
174
+ The server is also embeddable: `import { startYagami } from "@justin06lee/yagami/server"`.
175
+
65
176
  ## Providers
66
177
 
67
178
  A bare model id goes to the **default provider** (Claude Code unless you change it). `"<provider>:<model>"` routes to another harness; a bare provider id (`"codex"`) means that harness's own default model. `GET /v1/models` and `yagami models` list everything that's actually installed, with ids ready to paste.
@@ -97,6 +208,7 @@ Any other ACP agent works too — add it to config with its launch command:
97
208
  | `yagami start` | Start the server (`-p` port, `-H` host, `--provider <id>` default provider, `--claude <path>`, `--cors`). Add `--daemon` to run it in the background (`--log <file>` overrides the default log at `~/.config/yagami/yagami.log`) |
98
209
  | `yagami stop` | Stop the running server |
99
210
  | `yagami status` | Show whether it's running, plus uptime, request count, and cumulative would-be API cost |
211
+ | `yagami key` | Print the URL + API key, plus ready-to-paste `ANTHROPIC_*`/`OPENAI_*` env exports for client apps |
100
212
  | `yagami models` | List models across every installed provider (`--provider <id>` to filter) |
101
213
  | `yagami keygen` | Generate another API key and save it to the config |
102
214
  | `yagami doctor` | Check every harness CLI; `--live` sends one tiny real completion (`--provider <id>` to pick which) |
@@ -119,80 +231,14 @@ Every request is logged as one line (time, status, model, duration, cost, sessio
119
231
  }
120
232
  ```
121
233
 
122
- Env overrides: `YAGAMI_HOST`, `YAGAMI_PORT`, `YAGAMI_API_KEY`, `YAGAMI_PROVIDER`, `YAGAMI_DEFAULT_MODEL`, `YAGAMI_CLAUDE_PATH`, `YAGAMI_CODEX_PATH`. The older `claudePath` / `claudeConfigDir` keys still work as shorthands for `providers.claude`.
123
-
124
- ## Library mode
125
-
126
- For apps that want the engine in-process with no HTTP hop (e.g. a desktop app):
127
-
128
- ```ts
129
- import { YagamiEngine, claudeCodeSession, ClaudeProvider, CodexProvider, AcpProvider } from "@justin06lee/yagami";
130
-
131
- // 1. Anthropic-shaped completions across every installed harness:
132
- const engine = new YagamiEngine({ defaultModel: "sonnet" });
133
- const { response } = await engine.complete({ messages: [{ role: "user", content: "hello" }] });
134
- const codex = await engine.complete({ model: "codex", messages: [{ role: "user", content: "hello" }] });
135
-
136
- // streaming (Anthropic SSE event objects):
137
- const { events } = engine.stream({ model: "opencode", messages: [...], stream: true });
138
- for await (const ev of events) { /* ev.event, ev.data */ }
139
-
140
- // hand-pick providers instead of auto-detecting:
141
- const custom = new YagamiEngine({
142
- providers: [new ClaudeProvider(), new AcpProvider({ id: "gemini", label: "Gemini", command: "gemini", args: ["--acp"] })],
143
- });
144
-
145
- // 2. Full agentic Claude Code sessions — tools, permissions, the works.
146
- for await (const msg of claudeCodeSession("fix the failing test", {
147
- options: { cwd: "/path/to/project", permissionMode: "acceptEdits" },
148
- })) {
149
- // render SDK messages however you like
150
- }
151
- ```
152
-
153
- Every provider implements one small `Provider` contract (`run(turn)` → normalized `session`/`text`/`thinking`/`done` events, plus `listModels()` and `version()`), so adding a harness that isn't ACP-capable is one file. Failures are typed: `AuthRequiredError` (carries the login command), `ProviderNotInstalledError` (carries the install hint), `ProviderError`.
154
-
155
- ### Building a UI on Claude Code
156
-
157
- `YagamiEngine` is completions-only by design. To build an actual coding UI — tools, permissions, plan mode, a warm session across turns — use `AgentSession`, which wraps the full Claude Code agent with the lifecycle the interactive terminal gives you for free:
158
-
159
- ```ts
160
- import { AgentSession } from "@justin06lee/yagami";
161
-
162
- const session = new AgentSession({
163
- cwd: "/path/to/project",
164
- parity: "terminal", // load your CLAUDE.md, skills, hooks, .mcp.json — like the CLI
165
- appName: "my-app", // reported to Claude as the client
166
- onPermission: async (req) => {
167
- // Your approve/deny UI. Policy stays here; yagami owns the state machine.
168
- const ok = await showDialog(req.toolName, req.input);
169
- return ok ? { behavior: "allow" } : { behavior: "deny", message: "user declined" };
170
- },
171
- });
172
-
173
- session.send("fix the failing test"); // process starts here and stays warm
174
- for await (const msg of session) { // raw SDKMessages — render however you like
175
- render(msg);
176
- if (msg.type === "result") break;
177
- }
178
- session.send("now add a test for the edge case"); // next turn resumes the same session
179
- await session.interrupt(); // the CLI's Esc
180
- await session.setModel("opus"); // the CLI's /model
181
- await session.setPermissionMode("plan"); // shift+tab
182
- session.close();
183
- ```
184
-
185
- This resolves the parts of embedding Claude Code that every host would otherwise reimplement identically — process lifecycle, session resume, interrupt, settings parity, and the permission state machine. What stays yours are the genuinely app-specific choices: rendering the `SDKMessage` stream, deciding what to auto-approve, and picking the working directory. `parity` is `"terminal"` (load user+project+local settings, matching your CLI), `"project"` (project+local only), or `"isolated"` (load nothing — reproducible, no personal config). The permission `fallback` defaults to `"deny"`, so a session is safe before the UI is wired up; `autoAllow`/`autoDeny` skip the handler for named tools.
186
-
187
- For a lower-level handle, `claudeCodeSession(prompt, { options })` returns the raw Agent SDK `Query`.
188
-
189
- The server is also embeddable: `import { startYagami } from "@justin06lee/yagami/server"`.
234
+ Env overrides: `YAGAMI_HOST`, `YAGAMI_PORT`, `YAGAMI_API_KEY`, `YAGAMI_PROVIDER`, `YAGAMI_DEFAULT_MODEL`, `YAGAMI_CLAUDE_PATH`, `YAGAMI_CODEX_PATH`. The older `claudePath` / `claudeConfigDir` keys still work as shorthands for `providers.claude`. Library mode reads the same file (minus the server-only fields — host, port, keys), which is what keeps an embedded `Yagami` and the binary in agreement.
190
235
 
191
236
  ## How it works
192
237
 
193
- - **Engine**: each `/v1/messages` request becomes one sandboxed turn on the chosen harness. Claude runs with `tools: []`, `settingSources: []` (your CLAUDE.md/skills never leak into API completions), `maxTurns: 1` and a deny-all permission callback; Codex runs in its read-only sandbox with no approvals; ACP agents are moved to a plan/read-only mode when they offer one and every permission request is refused. All of them work in a throwaway directory. The API is text-in/text-out; a leaked key can burn tokens but never edit anything on the host — though note that agents other than Claude keep their own read-only tools, so they can still *look* at that empty directory.
238
+ - **Engine**: each request becomes one sandboxed turn on the chosen harness. Claude runs with `tools: []`, `settingSources: []` (your CLAUDE.md/skills never leak into API completions), `maxTurns: 1` and a deny-all permission callback; Codex runs in its read-only sandbox with no approvals; ACP agents are moved to a plan/read-only mode when they offer one and every permission request is refused. All of them work in a throwaway directory. The API is text-in/text-out; a leaked key can burn tokens but never edit anything on the host — though note that agents other than Claude keep their own read-only tools, so they can still *look* at that empty directory.
239
+ - **Dialects**: `POST /v1/messages` is native. `POST /v1/chat/completions` translates OpenAI shapes at the edge — system/developer messages fold into `system`, `image_url` parts become image blocks, streams are re-emitted as `chat.completion.chunk` events ending in `[DONE]`, and thinking output rides along as `reasoning_content`. Errors on that path come back OpenAI-shaped too. `GET /v1/models` serves one merged shape both SDKs parse.
194
240
  - **Multi-turn**: the Messages API is stateless but harness sessions aren't. yagami hashes each conversation prefix (per provider) and remembers which session produced it; a follow-up request resumes that session and sends only the new user message. Unmatched histories fall back to replaying the transcript in a single prompt, and if a cached session turns out to be gone, the stale mapping is dropped and the request transparently retries via replay. The cache persists across restarts at `~/.config/yagami/sessions.json`.
195
- - **Streaming**: every harness's output is normalized into deltas and re-emitted as a proper Anthropic SSE sequence — `message_start` → thinking/text content blocks → `message_delta` → `message_stop`. Claude and ACP agents stream tokens; Codex streams per message part.
241
+ - **Streaming**: every harness's output is normalized into deltas and re-emitted as a proper Anthropic SSE sequence — `message_start` → thinking/text content blocks → `message_delta` → `message_stop` (or the OpenAI chunk sequence on the chat-completions path). Claude and ACP agents stream tokens; Codex streams per message part.
196
242
  - **Models**: `GET /v1/models` asks each installed CLI what it supports (Claude via the SDK, Codex via its app-server protocol, ACP agents via their session config) — probed once per process, then cached. Failed probes are skipped and retried next time; a static fallback list is served only if nothing answers (`x-yagami-models-source` says which).
197
243
  - **Auth**: `x-api-key` or `Authorization: Bearer`, compared in constant time. Binds to `127.0.0.1` by default and warns loudly on anything else.
198
244
 
@@ -200,11 +246,11 @@ Extra response headers: `x-yagami-provider`, `x-yagami-cost-usd` (what the turn
200
246
 
201
247
  ## Limitations
202
248
 
203
- - No `tools` / `tool_choice` (rejected with 400 — by design, see above). `tool_use`/`tool_result` content blocks are rejected too.
249
+ - No `tools` / `tool_choice` / function calling (rejected with 400 — by design, see above). `tool_use`/`tool_result` content blocks and OpenAI `tool`/`function` messages are rejected too.
204
250
  - User messages may contain `text`, `image`, and `document` blocks (documents: Claude only; images: base64 sources only outside Claude); `system` and assistant messages are text-only. Thinking blocks echoed back in assistant history are dropped, not rejected. A conversation whose *history* contains images/documents can only be continued while the server that produced it still has that session cached.
205
251
  - Assistant prefill (a trailing `assistant` message) is emulated: the engine is instructed to continue from the prefill text, and the response carries only the continuation, like the real API. An accidentally repeated prefill is stripped from the reply, including mid-stream.
206
- - `max_tokens`, `temperature`, `top_p`, `top_k`, `stop_sequences` are accepted but ignored (reported via `x-yagami-ignored`) — none of the CLI engines expose them.
207
- - `thinking` and a yagami-extension `effort` ("low"…"max") are passed through where the harness supports them (see the provider table) and reported as ignored elsewhere.
252
+ - `max_tokens`, `temperature`, `top_p`, `top_k`, `stop_sequences` (and their OpenAI counterparts, plus `presence_penalty`, `seed`, `response_format`, …) are accepted but ignored (reported via `x-yagami-ignored`) — none of the CLI engines expose them. OpenAI `n` must be 1.
253
+ - `thinking` and a yagami-extension `effort` ("low"…"max") are passed through where the harness supports them (see the provider table) and reported as ignored elsewhere. OpenAI `reasoning_effort` maps onto `effort`.
208
254
  - Cost is reported only by harnesses that price their own turns (Claude, OpenCode); Codex reports token usage without cost.
209
255
 
210
256
  ## Development
@@ -1,9 +1,16 @@
1
1
  import {
2
+ ApiError,
3
+ ChatChunkTranslator,
2
4
  SessionCache,
3
5
  VERSION,
4
6
  YagamiEngine,
5
- toApiError
6
- } from "./chunk-ASS6MJ7C.js";
7
+ chatToMessagesRequest,
8
+ modelListBody,
9
+ openAiErrorBody,
10
+ toApiError,
11
+ toChatCompletion,
12
+ yagamiConfigDir
13
+ } from "./chunk-ZYHC7PXX.js";
7
14
 
8
15
  // src/server.ts
9
16
  import { serve } from "@hono/node-server";
@@ -27,10 +34,10 @@ function safeEqual(a, b) {
27
34
  function errorBody(type, message) {
28
35
  return { type: "error", error: { type, message } };
29
36
  }
30
- function requestLine(status, model, startedAt, extra = {}) {
37
+ function requestLine(path2, status, model, startedAt, extra = {}) {
31
38
  const parts = [
32
39
  (/* @__PURE__ */ new Date()).toISOString(),
33
- `POST /v1/messages ${status}`,
40
+ `POST ${path2} ${status}`,
34
41
  `model=${model}`,
35
42
  `${((Date.now() - startedAt) / 1e3).toFixed(1)}s`
36
43
  ];
@@ -67,7 +74,8 @@ function createApp(options) {
67
74
  const header = c.req.header("x-api-key") ?? c.req.header("authorization")?.replace(/^Bearer\s+/i, "");
68
75
  const ok = header != null && apiKeys.some((key) => safeEqual(key, header));
69
76
  if (!ok) {
70
- return c.json(errorBody("authentication_error", "invalid x-api-key"), 401);
77
+ const err = new ApiError(401, "authentication_error", "invalid API key (x-api-key or Authorization: Bearer)");
78
+ return c.req.path.startsWith("/v1/chat") ? c.json(openAiErrorBody(err), 401) : c.json(err.toBody(), 401);
71
79
  }
72
80
  await next();
73
81
  });
@@ -83,12 +91,7 @@ function createApp(options) {
83
91
  } catch {
84
92
  }
85
93
  c.header("x-yagami-models-source", source);
86
- return c.json({
87
- data: models.map((m) => ({ type: "model", ...m })),
88
- has_more: false,
89
- first_id: models[0]?.id,
90
- last_id: models[models.length - 1]?.id
91
- });
94
+ return c.json(modelListBody(models));
92
95
  });
93
96
  app.post("/v1/messages", async (c) => {
94
97
  let body;
@@ -109,7 +112,7 @@ function createApp(options) {
109
112
  onResult: (info) => {
110
113
  if (info.costUsd !== void 0) stats.totalCostUsd += info.costUsd;
111
114
  log?.(
112
- requestLine(200, model, startedAt, {
115
+ requestLine("/v1/messages", 200, model, startedAt, {
113
116
  stream: true,
114
117
  ...info.costUsd !== void 0 ? { cost: info.costUsd } : {},
115
118
  ...info.sessionId ? { session: info.sessionId } : {}
@@ -129,7 +132,7 @@ function createApp(options) {
129
132
  const result = await engine.complete(req);
130
133
  if (result.costUsd !== void 0) stats.totalCostUsd += result.costUsd;
131
134
  log?.(
132
- requestLine(200, model, startedAt, {
135
+ requestLine("/v1/messages", 200, model, startedAt, {
133
136
  ...result.costUsd !== void 0 ? { cost: result.costUsd } : {},
134
137
  ...result.sessionId ? { session: result.sessionId } : {}
135
138
  })
@@ -141,10 +144,72 @@ function createApp(options) {
141
144
  return c.json(result.response);
142
145
  } catch (err) {
143
146
  const apiErr = toApiError(err);
144
- log?.(requestLine(apiErr.status, model, startedAt, { error: apiErr.type }));
147
+ log?.(requestLine("/v1/messages", apiErr.status, model, startedAt, { error: apiErr.type }));
145
148
  return c.json(apiErr.toBody(), apiErr.status);
146
149
  }
147
150
  });
151
+ app.post("/v1/chat/completions", async (c) => {
152
+ let body;
153
+ try {
154
+ body = await c.req.json();
155
+ } catch {
156
+ return c.json(openAiErrorBody(new ApiError(400, "invalid_request_error", "request body must be valid JSON")), 400);
157
+ }
158
+ const startedAt = Date.now();
159
+ const chatReq = body;
160
+ const model = typeof chatReq?.model === "string" ? chatReq.model : "(default)";
161
+ stats.requests += 1;
162
+ try {
163
+ const { req, extraIgnored, includeUsage } = chatToMessagesRequest(chatReq);
164
+ if (req.stream === true) {
165
+ const abortController = new AbortController();
166
+ const { ignored, provider, events } = engine.stream(req, {
167
+ signal: abortController.signal,
168
+ onResult: (info) => {
169
+ if (info.costUsd !== void 0) stats.totalCostUsd += info.costUsd;
170
+ log?.(
171
+ requestLine("/v1/chat/completions", 200, model, startedAt, {
172
+ stream: true,
173
+ ...info.costUsd !== void 0 ? { cost: info.costUsd } : {},
174
+ ...info.sessionId ? { session: info.sessionId } : {}
175
+ })
176
+ );
177
+ }
178
+ });
179
+ c.header("x-yagami-provider", provider);
180
+ const allIgnored2 = [...ignored, ...extraIgnored];
181
+ if (allIgnored2.length > 0) c.header("x-yagami-ignored", allIgnored2.join(","));
182
+ const translator = new ChatChunkTranslator(includeUsage);
183
+ return streamSSE(c, async (stream) => {
184
+ stream.onAbort(() => abortController.abort());
185
+ for await (const ev of events) {
186
+ for (const chunk of translator.push(ev)) {
187
+ await stream.writeSSE({ data: JSON.stringify(chunk) });
188
+ }
189
+ }
190
+ if (!translator.errored) await stream.writeSSE({ data: "[DONE]" });
191
+ });
192
+ }
193
+ const result = await engine.complete(req);
194
+ if (result.costUsd !== void 0) stats.totalCostUsd += result.costUsd;
195
+ log?.(
196
+ requestLine("/v1/chat/completions", 200, model, startedAt, {
197
+ ...result.costUsd !== void 0 ? { cost: result.costUsd } : {},
198
+ ...result.sessionId ? { session: result.sessionId } : {}
199
+ })
200
+ );
201
+ c.header("x-yagami-provider", result.provider);
202
+ const allIgnored = [...result.ignored, ...extraIgnored];
203
+ if (allIgnored.length > 0) c.header("x-yagami-ignored", allIgnored.join(","));
204
+ if (result.costUsd !== void 0) c.header("x-yagami-cost-usd", result.costUsd.toFixed(6));
205
+ if (result.sessionId) c.header("x-yagami-session", result.sessionId);
206
+ return c.json(toChatCompletion(result.response));
207
+ } catch (err) {
208
+ const apiErr = toApiError(err);
209
+ log?.(requestLine("/v1/chat/completions", apiErr.status, model, startedAt, { error: apiErr.type }));
210
+ return c.json(openAiErrorBody(apiErr), apiErr.status);
211
+ }
212
+ });
148
213
  app.notFound(
149
214
  (c) => c.json(errorBody("not_found_error", `no route for ${c.req.method} ${c.req.path}`), 404)
150
215
  );
@@ -158,16 +223,12 @@ function createApp(options) {
158
223
  // src/server/config.ts
159
224
  import { randomBytes } from "crypto";
160
225
  import * as fs from "fs";
161
- import * as os from "os";
162
226
  import * as path from "path";
163
227
  var DEFAULT_CONFIG = {
164
228
  host: "127.0.0.1",
165
229
  port: 8787,
166
230
  apiKeys: []
167
231
  };
168
- function yagamiConfigDir() {
169
- return process.env["YAGAMI_CONFIG_DIR"] ?? path.join(os.homedir(), ".config", "yagami");
170
- }
171
232
  function configFilePath() {
172
233
  return path.join(yagamiConfigDir(), "config.json");
173
234
  }
@@ -298,7 +359,6 @@ function definedProps(obj) {
298
359
 
299
360
  export {
300
361
  createApp,
301
- yagamiConfigDir,
302
362
  configFilePath,
303
363
  sessionCachePath,
304
364
  serverStatePath,
@@ -314,4 +374,4 @@ export {
314
374
  maskKey,
315
375
  startYagami
316
376
  };
317
- //# sourceMappingURL=chunk-M5UHR273.js.map
377
+ //# sourceMappingURL=chunk-D2PNH6GV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts","../src/server/app.ts","../src/server/config.ts"],"sourcesContent":["import { serve, type ServerType } from \"@hono/node-server\";\nimport { YagamiEngine } from \"./core/engine.js\";\nimport { SessionCache } from \"./core/sessionCache.js\";\nimport { createApp } from \"./server/app.js\";\nimport {\n loadConfig,\n sessionCachePath,\n type YagamiConfig,\n} from \"./server/config.js\";\nimport { VERSION } from \"./version.js\";\n\nexport interface RunningServer {\n server: ServerType;\n engine: YagamiEngine;\n sessionCache: SessionCache;\n config: YagamiConfig;\n url: string;\n close(): Promise<void>;\n}\n\nexport interface StartOptions extends Partial<YagamiConfig> {\n /** Sink for one-line request logs (default: console.log). Pass null to disable. */\n log?: ((line: string) => void) | null;\n}\n\n/**\n * Start a yagami server. Overrides are merged over the loaded config\n * (~/.config/yagami/config.json plus YAGAMI_* env vars).\n */\nexport async function startYagami(overrides: StartOptions = {}): Promise<RunningServer> {\n const { log, ...configOverrides } = overrides;\n const config: YagamiConfig = { ...loadConfig(), ...definedProps(configOverrides) };\n if (config.apiKeys.length === 0) {\n throw new Error(\n \"no API keys configured — run `yagami keygen` (or set YAGAMI_API_KEY) so the endpoint isn't unauthenticated\",\n );\n }\n\n const sessionCache = new SessionCache({ persistPath: sessionCachePath() });\n const engine = new YagamiEngine({\n ...(config.providers ? { providerConfig: config.providers } : {}),\n ...(config.defaultProvider ? { defaultProvider: config.defaultProvider } : {}),\n ...(config.claudePath ? { claudePath: config.claudePath } : {}),\n ...(config.claudeConfigDir ? { claudeConfigDir: config.claudeConfigDir } : {}),\n ...(config.defaultModel ? { defaultModel: config.defaultModel } : {}),\n sessionCache,\n appName: \"yagami\",\n });\n\n const app = createApp({\n engine,\n apiKeys: config.apiKeys,\n cors: config.cors,\n version: VERSION,\n ...(log === null ? {} : { log: log ?? ((line: string) => console.log(line)) }),\n });\n\n const server = await new Promise<ServerType>((resolve) => {\n const s = serve({ fetch: app.fetch, hostname: config.host, port: config.port }, () => resolve(s));\n });\n\n const address = server.address();\n const port = typeof address === \"object\" && address ? address.port : config.port;\n\n return {\n server,\n engine,\n sessionCache,\n config: { ...config, port },\n url: `http://${config.host}:${port}`,\n close: () =>\n new Promise<void>((resolve, reject) => {\n server.close((err) => (err ? reject(err) : resolve()));\n }),\n };\n}\n\nfunction definedProps<T extends object>(obj: T): Partial<T> {\n return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)) as Partial<T>;\n}\n\nexport { createApp } from \"./server/app.js\";\nexport type { AppOptions, EngineLike } from \"./server/app.js\";\nexport {\n loadConfig,\n loadFileConfig,\n saveConfig,\n generateApiKey,\n configFilePath,\n sessionCachePath,\n serverStatePath,\n logFilePath,\n readServerState,\n writeServerState,\n clearServerState,\n isProcessAlive,\n yagamiConfigDir,\n type YagamiConfig,\n type ServerState,\n} from \"./server/config.js\";\n","import { createHash, randomUUID, timingSafeEqual } from \"node:crypto\";\nimport { Hono } from \"hono\";\nimport { cors } from \"hono/cors\";\nimport { streamSSE } from \"hono/streaming\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport { ApiError, type MessagesRequest } from \"../core/types.js\";\nimport { toApiError } from \"../core/errors.js\";\nimport type { CompleteResult, EngineModel, StreamOptions, StreamStart } from \"../core/engine.js\";\nimport {\n ChatChunkTranslator,\n chatToMessagesRequest,\n modelListBody,\n openAiErrorBody,\n toChatCompletion,\n type ChatCompletionsRequest,\n} from \"../core/openai.js\";\n\n/** What the app needs from an engine — lets tests inject a fake. */\nexport interface EngineLike {\n /** Executable of the default provider. */\n executable: string;\n defaultProviderId: string;\n providerIds: string[];\n complete(req: MessagesRequest): Promise<CompleteResult>;\n stream(req: MessagesRequest, opts?: StreamOptions): StreamStart;\n listModels(): Promise<EngineModel[]>;\n}\n\nexport interface AppOptions {\n engine: EngineLike;\n apiKeys: string[];\n cors?: boolean;\n version?: string;\n /** Sink for one-line request logs; omit to disable request logging. */\n log?: (line: string) => void;\n}\n\n/** Served by GET /v1/models only when probing the CLI fails. */\nconst FALLBACK_MODELS: EngineModel[] = [\n \"claude-fable-5\",\n \"claude-opus-5\",\n \"claude-sonnet-5\",\n \"claude-haiku-4-5-20251001\",\n].map((id) => ({ id, display_name: id }));\n\nfunction safeEqual(a: string, b: string): boolean {\n const ha = createHash(\"sha256\").update(a).digest();\n const hb = createHash(\"sha256\").update(b).digest();\n return timingSafeEqual(ha, hb);\n}\n\nfunction errorBody(type: ApiError[\"type\"], message: string) {\n return { type: \"error\" as const, error: { type, message } };\n}\n\nfunction requestLine(\n path: string,\n status: number,\n model: string,\n startedAt: number,\n extra: { cost?: number; session?: string; stream?: boolean; error?: string } = {},\n): string {\n const parts = [\n new Date().toISOString(),\n `POST ${path} ${status}`,\n `model=${model}`,\n `${((Date.now() - startedAt) / 1000).toFixed(1)}s`,\n ];\n if (extra.stream) parts.push(\"stream\");\n if (extra.cost !== undefined) parts.push(`cost=$${extra.cost.toFixed(6)}`);\n if (extra.session) parts.push(`session=${extra.session}`);\n if (extra.error) parts.push(`error=${extra.error}`);\n return parts.join(\" \");\n}\n\nexport function createApp(options: AppOptions): Hono {\n const { engine, apiKeys, log } = options;\n const app = new Hono();\n const stats = { startedAt: Date.now(), requests: 0, totalCostUsd: 0 };\n\n if (options.cors) app.use(\"*\", cors());\n\n app.use(\"*\", async (c, next) => {\n c.header(\"request-id\", `req_${randomUUID().replace(/-/g, \"\")}`);\n await next();\n });\n\n app.get(\"/healthz\", (c) =>\n c.json({\n ok: true,\n service: \"yagami\",\n version: options.version,\n provider: engine.defaultProviderId,\n providers: engine.providerIds,\n executable: engine.executable,\n uptime_s: Math.round((Date.now() - stats.startedAt) / 1000),\n requests: stats.requests,\n total_cost_usd: stats.totalCostUsd,\n }),\n );\n\n app.use(\"/v1/*\", async (c, next) => {\n const header = c.req.header(\"x-api-key\") ?? c.req.header(\"authorization\")?.replace(/^Bearer\\s+/i, \"\");\n const ok = header != null && apiKeys.some((key) => safeEqual(key, header));\n if (!ok) {\n const err = new ApiError(401, \"authentication_error\", \"invalid API key (x-api-key or Authorization: Bearer)\");\n // The chat-completions path answers in the OpenAI error shape.\n return c.req.path.startsWith(\"/v1/chat\")\n ? c.json(openAiErrorBody(err), 401)\n : c.json(err.toBody(), 401);\n }\n await next();\n });\n\n // Served in a merged shape: Anthropic fields + OpenAI fields per model, so\n // both SDKs' models.list() parse it.\n app.get(\"/v1/models\", async (c) => {\n let models = FALLBACK_MODELS;\n let source = \"fallback\";\n try {\n const probed = await engine.listModels();\n if (probed.length > 0) {\n models = probed;\n source = \"engine\";\n }\n } catch {\n // engine unavailable or slow — the static list keeps clients working\n }\n c.header(\"x-yagami-models-source\", source);\n return c.json(modelListBody(models));\n });\n\n app.post(\"/v1/messages\", async (c) => {\n let body: unknown;\n try {\n body = await c.req.json();\n } catch {\n return c.json(errorBody(\"invalid_request_error\", \"request body must be valid JSON\"), 400);\n }\n const req = body as MessagesRequest;\n const startedAt = Date.now();\n const model = typeof req.model === \"string\" ? req.model : \"(default)\";\n stats.requests += 1;\n\n try {\n if (req.stream === true) {\n const abortController = new AbortController();\n const { ignored, provider, events } = engine.stream(req, {\n signal: abortController.signal,\n onResult: (info) => {\n if (info.costUsd !== undefined) stats.totalCostUsd += info.costUsd;\n log?.(\n requestLine(\"/v1/messages\", 200, model, startedAt, {\n stream: true,\n ...(info.costUsd !== undefined ? { cost: info.costUsd } : {}),\n ...(info.sessionId ? { session: info.sessionId } : {}),\n }),\n );\n },\n });\n c.header(\"x-yagami-provider\", provider);\n if (ignored.length > 0) c.header(\"x-yagami-ignored\", ignored.join(\",\"));\n return streamSSE(c, async (stream) => {\n stream.onAbort(() => abortController.abort());\n for await (const ev of events) {\n await stream.writeSSE({ event: ev.event, data: JSON.stringify(ev.data) });\n }\n });\n }\n\n const result = await engine.complete(req);\n if (result.costUsd !== undefined) stats.totalCostUsd += result.costUsd;\n log?.(\n requestLine(\"/v1/messages\", 200, model, startedAt, {\n ...(result.costUsd !== undefined ? { cost: result.costUsd } : {}),\n ...(result.sessionId ? { session: result.sessionId } : {}),\n }),\n );\n c.header(\"x-yagami-provider\", result.provider);\n if (result.ignored.length > 0) c.header(\"x-yagami-ignored\", result.ignored.join(\",\"));\n if (result.costUsd !== undefined) c.header(\"x-yagami-cost-usd\", result.costUsd.toFixed(6));\n if (result.sessionId) c.header(\"x-yagami-session\", result.sessionId);\n return c.json(result.response);\n } catch (err) {\n const apiErr = toApiError(err);\n log?.(requestLine(\"/v1/messages\", apiErr.status, model, startedAt, { error: apiErr.type }));\n return c.json(apiErr.toBody(), apiErr.status as ContentfulStatusCode);\n }\n });\n\n // OpenAI dialect: the same engine behind Chat Completions shapes, so apps\n // that expect an OpenAI base URL + API key work against the same key.\n app.post(\"/v1/chat/completions\", async (c) => {\n let body: unknown;\n try {\n body = await c.req.json();\n } catch {\n return c.json(openAiErrorBody(new ApiError(400, \"invalid_request_error\", \"request body must be valid JSON\")), 400);\n }\n const startedAt = Date.now();\n const chatReq = body as ChatCompletionsRequest;\n const model = typeof chatReq?.model === \"string\" ? chatReq.model : \"(default)\";\n stats.requests += 1;\n\n try {\n const { req, extraIgnored, includeUsage } = chatToMessagesRequest(chatReq);\n\n if (req.stream === true) {\n const abortController = new AbortController();\n const { ignored, provider, events } = engine.stream(req, {\n signal: abortController.signal,\n onResult: (info) => {\n if (info.costUsd !== undefined) stats.totalCostUsd += info.costUsd;\n log?.(\n requestLine(\"/v1/chat/completions\", 200, model, startedAt, {\n stream: true,\n ...(info.costUsd !== undefined ? { cost: info.costUsd } : {}),\n ...(info.sessionId ? { session: info.sessionId } : {}),\n }),\n );\n },\n });\n c.header(\"x-yagami-provider\", provider);\n const allIgnored = [...ignored, ...extraIgnored];\n if (allIgnored.length > 0) c.header(\"x-yagami-ignored\", allIgnored.join(\",\"));\n const translator = new ChatChunkTranslator(includeUsage);\n return streamSSE(c, async (stream) => {\n stream.onAbort(() => abortController.abort());\n for await (const ev of events) {\n for (const chunk of translator.push(ev)) {\n await stream.writeSSE({ data: JSON.stringify(chunk) });\n }\n }\n if (!translator.errored) await stream.writeSSE({ data: \"[DONE]\" });\n });\n }\n\n const result = await engine.complete(req);\n if (result.costUsd !== undefined) stats.totalCostUsd += result.costUsd;\n log?.(\n requestLine(\"/v1/chat/completions\", 200, model, startedAt, {\n ...(result.costUsd !== undefined ? { cost: result.costUsd } : {}),\n ...(result.sessionId ? { session: result.sessionId } : {}),\n }),\n );\n c.header(\"x-yagami-provider\", result.provider);\n const allIgnored = [...result.ignored, ...extraIgnored];\n if (allIgnored.length > 0) c.header(\"x-yagami-ignored\", allIgnored.join(\",\"));\n if (result.costUsd !== undefined) c.header(\"x-yagami-cost-usd\", result.costUsd.toFixed(6));\n if (result.sessionId) c.header(\"x-yagami-session\", result.sessionId);\n return c.json(toChatCompletion(result.response));\n } catch (err) {\n const apiErr = toApiError(err);\n log?.(requestLine(\"/v1/chat/completions\", apiErr.status, model, startedAt, { error: apiErr.type }));\n return c.json(openAiErrorBody(apiErr), apiErr.status as ContentfulStatusCode);\n }\n });\n\n app.notFound((c) =>\n c.json(errorBody(\"not_found_error\", `no route for ${c.req.method} ${c.req.path}`), 404),\n );\n\n app.onError((err, c) => {\n const apiErr = toApiError(err);\n return c.json(apiErr.toBody(), apiErr.status as ContentfulStatusCode);\n });\n\n return app;\n}\n","import { randomBytes } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { yagamiConfigDir } from \"../core/hostConfig.js\";\nimport type { ProviderConfigEntry } from \"../core/providers/registry.js\";\n\nexport { yagamiConfigDir } from \"../core/hostConfig.js\";\n\nexport interface YagamiConfig {\n host: string;\n port: number;\n apiKeys: string[];\n /** @deprecated Use providers.claude.path. */\n claudePath?: string;\n /** @deprecated Use providers.claude.configDir. */\n claudeConfigDir?: string;\n defaultModel?: string;\n cors?: boolean;\n /** Provider used for bare model ids (default: claude). */\n defaultProvider?: string;\n /** Per-provider settings, keyed by provider id. */\n providers?: Record<string, ProviderConfigEntry>;\n}\n\nexport const DEFAULT_CONFIG: YagamiConfig = {\n host: \"127.0.0.1\",\n port: 8787,\n apiKeys: [],\n};\n\nexport function configFilePath(): string {\n return path.join(yagamiConfigDir(), \"config.json\");\n}\n\nexport function sessionCachePath(): string {\n return path.join(yagamiConfigDir(), \"sessions.json\");\n}\n\nexport function serverStatePath(): string {\n return path.join(yagamiConfigDir(), \"server.json\");\n}\n\nexport function logFilePath(): string {\n return path.join(yagamiConfigDir(), \"yagami.log\");\n}\n\n/** What a running server records about itself for `stop`/`status`. */\nexport interface ServerState {\n pid: number;\n host: string;\n port: number;\n url: string;\n startedAt: string;\n version: string;\n log?: string;\n}\n\nexport function readServerState(): ServerState | undefined {\n try {\n const state = JSON.parse(fs.readFileSync(serverStatePath(), \"utf8\")) as ServerState;\n return typeof state?.pid === \"number\" ? state : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport function writeServerState(state: ServerState): void {\n fs.mkdirSync(yagamiConfigDir(), { recursive: true, mode: 0o700 });\n fs.writeFileSync(serverStatePath(), `${JSON.stringify(state, null, 2)}\\n`, { mode: 0o600 });\n}\n\n/** Remove the state file; with `pid`, only if it still belongs to that pid. */\nexport function clearServerState(pid?: number): void {\n try {\n if (pid !== undefined && readServerState()?.pid !== pid) return;\n fs.unlinkSync(serverStatePath());\n } catch {\n // already gone\n }\n}\n\nexport function isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Config as stored on disk, without env overrides (safe to save back). */\nexport function loadFileConfig(): YagamiConfig {\n let fromFile: Partial<YagamiConfig> = {};\n try {\n fromFile = JSON.parse(fs.readFileSync(configFilePath(), \"utf8\")) as Partial<YagamiConfig>;\n } catch {\n // no config file yet\n }\n return {\n ...DEFAULT_CONFIG,\n ...fromFile,\n apiKeys: Array.isArray(fromFile.apiKeys) ? fromFile.apiKeys.filter((k) => typeof k === \"string\") : [],\n };\n}\n\n/** File config plus environment overrides. */\nexport function loadConfig(): YagamiConfig {\n const cfg = loadFileConfig();\n const env = process.env;\n if (env[\"YAGAMI_HOST\"]) cfg.host = env[\"YAGAMI_HOST\"];\n if (env[\"YAGAMI_PORT\"] && Number.isFinite(Number(env[\"YAGAMI_PORT\"]))) {\n cfg.port = Number(env[\"YAGAMI_PORT\"]);\n }\n if (env[\"YAGAMI_API_KEY\"] && !cfg.apiKeys.includes(env[\"YAGAMI_API_KEY\"])) {\n cfg.apiKeys.push(env[\"YAGAMI_API_KEY\"]);\n }\n if (env[\"YAGAMI_CLAUDE_PATH\"]) cfg.claudePath = env[\"YAGAMI_CLAUDE_PATH\"];\n if (env[\"YAGAMI_DEFAULT_MODEL\"]) cfg.defaultModel = env[\"YAGAMI_DEFAULT_MODEL\"];\n if (env[\"YAGAMI_PROVIDER\"]) cfg.defaultProvider = env[\"YAGAMI_PROVIDER\"];\n return cfg;\n}\n\nexport function saveConfig(cfg: YagamiConfig): string {\n const dir = yagamiConfigDir();\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n const file = configFilePath();\n fs.writeFileSync(file, `${JSON.stringify(cfg, null, 2)}\\n`, { mode: 0o600 });\n return file;\n}\n\nexport function generateApiKey(): string {\n return `ygm_${randomBytes(24).toString(\"hex\")}`;\n}\n\nexport function maskKey(key: string): string {\n return key.length <= 12 ? key : `${key.slice(0, 12)}…`;\n}\n"],"mappings":"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","names":["path","allIgnored"]}