@justin06lee/yagami 0.4.1

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.
@@ -0,0 +1,317 @@
1
+ import {
2
+ SessionCache,
3
+ VERSION,
4
+ YagamiEngine,
5
+ toApiError
6
+ } from "./chunk-ASS6MJ7C.js";
7
+
8
+ // src/server.ts
9
+ import { serve } from "@hono/node-server";
10
+
11
+ // src/server/app.ts
12
+ import { createHash, randomUUID, timingSafeEqual } from "crypto";
13
+ import { Hono } from "hono";
14
+ import { cors } from "hono/cors";
15
+ import { streamSSE } from "hono/streaming";
16
+ var FALLBACK_MODELS = [
17
+ "claude-fable-5",
18
+ "claude-opus-5",
19
+ "claude-sonnet-5",
20
+ "claude-haiku-4-5-20251001"
21
+ ].map((id) => ({ id, display_name: id }));
22
+ function safeEqual(a, b) {
23
+ const ha = createHash("sha256").update(a).digest();
24
+ const hb = createHash("sha256").update(b).digest();
25
+ return timingSafeEqual(ha, hb);
26
+ }
27
+ function errorBody(type, message) {
28
+ return { type: "error", error: { type, message } };
29
+ }
30
+ function requestLine(status, model, startedAt, extra = {}) {
31
+ const parts = [
32
+ (/* @__PURE__ */ new Date()).toISOString(),
33
+ `POST /v1/messages ${status}`,
34
+ `model=${model}`,
35
+ `${((Date.now() - startedAt) / 1e3).toFixed(1)}s`
36
+ ];
37
+ if (extra.stream) parts.push("stream");
38
+ if (extra.cost !== void 0) parts.push(`cost=$${extra.cost.toFixed(6)}`);
39
+ if (extra.session) parts.push(`session=${extra.session}`);
40
+ if (extra.error) parts.push(`error=${extra.error}`);
41
+ return parts.join(" ");
42
+ }
43
+ function createApp(options) {
44
+ const { engine, apiKeys, log } = options;
45
+ const app = new Hono();
46
+ const stats = { startedAt: Date.now(), requests: 0, totalCostUsd: 0 };
47
+ if (options.cors) app.use("*", cors());
48
+ app.use("*", async (c, next) => {
49
+ c.header("request-id", `req_${randomUUID().replace(/-/g, "")}`);
50
+ await next();
51
+ });
52
+ app.get(
53
+ "/healthz",
54
+ (c) => c.json({
55
+ ok: true,
56
+ service: "yagami",
57
+ version: options.version,
58
+ provider: engine.defaultProviderId,
59
+ providers: engine.providerIds,
60
+ executable: engine.executable,
61
+ uptime_s: Math.round((Date.now() - stats.startedAt) / 1e3),
62
+ requests: stats.requests,
63
+ total_cost_usd: stats.totalCostUsd
64
+ })
65
+ );
66
+ app.use("/v1/*", async (c, next) => {
67
+ const header = c.req.header("x-api-key") ?? c.req.header("authorization")?.replace(/^Bearer\s+/i, "");
68
+ const ok = header != null && apiKeys.some((key) => safeEqual(key, header));
69
+ if (!ok) {
70
+ return c.json(errorBody("authentication_error", "invalid x-api-key"), 401);
71
+ }
72
+ await next();
73
+ });
74
+ app.get("/v1/models", async (c) => {
75
+ let models = FALLBACK_MODELS;
76
+ let source = "fallback";
77
+ try {
78
+ const probed = await engine.listModels();
79
+ if (probed.length > 0) {
80
+ models = probed;
81
+ source = "engine";
82
+ }
83
+ } catch {
84
+ }
85
+ 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
+ });
92
+ });
93
+ app.post("/v1/messages", async (c) => {
94
+ let body;
95
+ try {
96
+ body = await c.req.json();
97
+ } catch {
98
+ return c.json(errorBody("invalid_request_error", "request body must be valid JSON"), 400);
99
+ }
100
+ const req = body;
101
+ const startedAt = Date.now();
102
+ const model = typeof req.model === "string" ? req.model : "(default)";
103
+ stats.requests += 1;
104
+ try {
105
+ if (req.stream === true) {
106
+ const abortController = new AbortController();
107
+ const { ignored, provider, events } = engine.stream(req, {
108
+ signal: abortController.signal,
109
+ onResult: (info) => {
110
+ if (info.costUsd !== void 0) stats.totalCostUsd += info.costUsd;
111
+ log?.(
112
+ requestLine(200, model, startedAt, {
113
+ stream: true,
114
+ ...info.costUsd !== void 0 ? { cost: info.costUsd } : {},
115
+ ...info.sessionId ? { session: info.sessionId } : {}
116
+ })
117
+ );
118
+ }
119
+ });
120
+ c.header("x-yagami-provider", provider);
121
+ if (ignored.length > 0) c.header("x-yagami-ignored", ignored.join(","));
122
+ return streamSSE(c, async (stream) => {
123
+ stream.onAbort(() => abortController.abort());
124
+ for await (const ev of events) {
125
+ await stream.writeSSE({ event: ev.event, data: JSON.stringify(ev.data) });
126
+ }
127
+ });
128
+ }
129
+ const result = await engine.complete(req);
130
+ if (result.costUsd !== void 0) stats.totalCostUsd += result.costUsd;
131
+ log?.(
132
+ requestLine(200, model, startedAt, {
133
+ ...result.costUsd !== void 0 ? { cost: result.costUsd } : {},
134
+ ...result.sessionId ? { session: result.sessionId } : {}
135
+ })
136
+ );
137
+ c.header("x-yagami-provider", result.provider);
138
+ if (result.ignored.length > 0) c.header("x-yagami-ignored", result.ignored.join(","));
139
+ if (result.costUsd !== void 0) c.header("x-yagami-cost-usd", result.costUsd.toFixed(6));
140
+ if (result.sessionId) c.header("x-yagami-session", result.sessionId);
141
+ return c.json(result.response);
142
+ } catch (err) {
143
+ const apiErr = toApiError(err);
144
+ log?.(requestLine(apiErr.status, model, startedAt, { error: apiErr.type }));
145
+ return c.json(apiErr.toBody(), apiErr.status);
146
+ }
147
+ });
148
+ app.notFound(
149
+ (c) => c.json(errorBody("not_found_error", `no route for ${c.req.method} ${c.req.path}`), 404)
150
+ );
151
+ app.onError((err, c) => {
152
+ const apiErr = toApiError(err);
153
+ return c.json(apiErr.toBody(), apiErr.status);
154
+ });
155
+ return app;
156
+ }
157
+
158
+ // src/server/config.ts
159
+ import { randomBytes } from "crypto";
160
+ import * as fs from "fs";
161
+ import * as os from "os";
162
+ import * as path from "path";
163
+ var DEFAULT_CONFIG = {
164
+ host: "127.0.0.1",
165
+ port: 8787,
166
+ apiKeys: []
167
+ };
168
+ function yagamiConfigDir() {
169
+ return process.env["YAGAMI_CONFIG_DIR"] ?? path.join(os.homedir(), ".config", "yagami");
170
+ }
171
+ function configFilePath() {
172
+ return path.join(yagamiConfigDir(), "config.json");
173
+ }
174
+ function sessionCachePath() {
175
+ return path.join(yagamiConfigDir(), "sessions.json");
176
+ }
177
+ function serverStatePath() {
178
+ return path.join(yagamiConfigDir(), "server.json");
179
+ }
180
+ function logFilePath() {
181
+ return path.join(yagamiConfigDir(), "yagami.log");
182
+ }
183
+ function readServerState() {
184
+ try {
185
+ const state = JSON.parse(fs.readFileSync(serverStatePath(), "utf8"));
186
+ return typeof state?.pid === "number" ? state : void 0;
187
+ } catch {
188
+ return void 0;
189
+ }
190
+ }
191
+ function writeServerState(state) {
192
+ fs.mkdirSync(yagamiConfigDir(), { recursive: true, mode: 448 });
193
+ fs.writeFileSync(serverStatePath(), `${JSON.stringify(state, null, 2)}
194
+ `, { mode: 384 });
195
+ }
196
+ function clearServerState(pid) {
197
+ try {
198
+ if (pid !== void 0 && readServerState()?.pid !== pid) return;
199
+ fs.unlinkSync(serverStatePath());
200
+ } catch {
201
+ }
202
+ }
203
+ function isProcessAlive(pid) {
204
+ try {
205
+ process.kill(pid, 0);
206
+ return true;
207
+ } catch {
208
+ return false;
209
+ }
210
+ }
211
+ function loadFileConfig() {
212
+ let fromFile = {};
213
+ try {
214
+ fromFile = JSON.parse(fs.readFileSync(configFilePath(), "utf8"));
215
+ } catch {
216
+ }
217
+ return {
218
+ ...DEFAULT_CONFIG,
219
+ ...fromFile,
220
+ apiKeys: Array.isArray(fromFile.apiKeys) ? fromFile.apiKeys.filter((k) => typeof k === "string") : []
221
+ };
222
+ }
223
+ function loadConfig() {
224
+ const cfg = loadFileConfig();
225
+ const env = process.env;
226
+ if (env["YAGAMI_HOST"]) cfg.host = env["YAGAMI_HOST"];
227
+ if (env["YAGAMI_PORT"] && Number.isFinite(Number(env["YAGAMI_PORT"]))) {
228
+ cfg.port = Number(env["YAGAMI_PORT"]);
229
+ }
230
+ if (env["YAGAMI_API_KEY"] && !cfg.apiKeys.includes(env["YAGAMI_API_KEY"])) {
231
+ cfg.apiKeys.push(env["YAGAMI_API_KEY"]);
232
+ }
233
+ if (env["YAGAMI_CLAUDE_PATH"]) cfg.claudePath = env["YAGAMI_CLAUDE_PATH"];
234
+ if (env["YAGAMI_DEFAULT_MODEL"]) cfg.defaultModel = env["YAGAMI_DEFAULT_MODEL"];
235
+ if (env["YAGAMI_PROVIDER"]) cfg.defaultProvider = env["YAGAMI_PROVIDER"];
236
+ return cfg;
237
+ }
238
+ function saveConfig(cfg) {
239
+ const dir = yagamiConfigDir();
240
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
241
+ const file = configFilePath();
242
+ fs.writeFileSync(file, `${JSON.stringify(cfg, null, 2)}
243
+ `, { mode: 384 });
244
+ return file;
245
+ }
246
+ function generateApiKey() {
247
+ return `ygm_${randomBytes(24).toString("hex")}`;
248
+ }
249
+ function maskKey(key) {
250
+ return key.length <= 12 ? key : `${key.slice(0, 12)}\u2026`;
251
+ }
252
+
253
+ // src/server.ts
254
+ async function startYagami(overrides = {}) {
255
+ const { log, ...configOverrides } = overrides;
256
+ const config = { ...loadConfig(), ...definedProps(configOverrides) };
257
+ if (config.apiKeys.length === 0) {
258
+ throw new Error(
259
+ "no API keys configured \u2014 run `yagami keygen` (or set YAGAMI_API_KEY) so the endpoint isn't unauthenticated"
260
+ );
261
+ }
262
+ const sessionCache = new SessionCache({ persistPath: sessionCachePath() });
263
+ const engine = new YagamiEngine({
264
+ ...config.providers ? { providerConfig: config.providers } : {},
265
+ ...config.defaultProvider ? { defaultProvider: config.defaultProvider } : {},
266
+ ...config.claudePath ? { claudePath: config.claudePath } : {},
267
+ ...config.claudeConfigDir ? { claudeConfigDir: config.claudeConfigDir } : {},
268
+ ...config.defaultModel ? { defaultModel: config.defaultModel } : {},
269
+ sessionCache,
270
+ appName: "yagami"
271
+ });
272
+ const app = createApp({
273
+ engine,
274
+ apiKeys: config.apiKeys,
275
+ cors: config.cors,
276
+ version: VERSION,
277
+ ...log === null ? {} : { log: log ?? ((line) => console.log(line)) }
278
+ });
279
+ const server = await new Promise((resolve) => {
280
+ const s = serve({ fetch: app.fetch, hostname: config.host, port: config.port }, () => resolve(s));
281
+ });
282
+ const address = server.address();
283
+ const port = typeof address === "object" && address ? address.port : config.port;
284
+ return {
285
+ server,
286
+ engine,
287
+ sessionCache,
288
+ config: { ...config, port },
289
+ url: `http://${config.host}:${port}`,
290
+ close: () => new Promise((resolve, reject) => {
291
+ server.close((err) => err ? reject(err) : resolve());
292
+ })
293
+ };
294
+ }
295
+ function definedProps(obj) {
296
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
297
+ }
298
+
299
+ export {
300
+ createApp,
301
+ yagamiConfigDir,
302
+ configFilePath,
303
+ sessionCachePath,
304
+ serverStatePath,
305
+ logFilePath,
306
+ readServerState,
307
+ writeServerState,
308
+ clearServerState,
309
+ isProcessAlive,
310
+ loadFileConfig,
311
+ loadConfig,
312
+ saveConfig,
313
+ generateApiKey,
314
+ maskKey,
315
+ startYagami
316
+ };
317
+ //# sourceMappingURL=chunk-M5UHR273.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\";\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 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 /v1/messages ${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 return c.json(errorBody(\"authentication_error\", \"invalid x-api-key\"), 401);\n }\n await next();\n });\n\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({\n data: models.map((m) => ({ type: \"model\", ...m })),\n has_more: false,\n first_id: models[0]?.id,\n last_id: models[models.length - 1]?.id,\n });\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(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(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(apiErr.status, model, startedAt, { error: apiErr.type }));\n return c.json(apiErr.toBody(), 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 os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { ProviderConfigEntry } from \"../core/providers/registry.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 yagamiConfigDir(): string {\n return process.env[\"YAGAMI_CONFIG_DIR\"] ?? path.join(os.homedir(), \".config\", \"yagami\");\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":[]}
package/dist/cli.js ADDED
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ clearServerState,
4
+ configFilePath,
5
+ generateApiKey,
6
+ isProcessAlive,
7
+ loadConfig,
8
+ loadFileConfig,
9
+ logFilePath,
10
+ maskKey,
11
+ readServerState,
12
+ saveConfig,
13
+ sessionCachePath,
14
+ startYagami,
15
+ writeServerState
16
+ } from "./chunk-M5UHR273.js";
17
+ import {
18
+ VERSION,
19
+ YagamiEngine,
20
+ createProvider,
21
+ detectProviders
22
+ } from "./chunk-ASS6MJ7C.js";
23
+
24
+ // src/cli.ts
25
+ import { spawn } from "child_process";
26
+ import * as fs from "fs";
27
+ import * as path from "path";
28
+ import { Command } from "commander";
29
+ var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
30
+ var program = new Command();
31
+ program.name("yagami").description("Anthropic-compatible API served by your signed-in Claude Code CLI").version(VERSION);
32
+ program.command("start", { isDefault: true }).description("start the yagami server").option("-p, --port <port>", "port to listen on").option("-H, --host <host>", "host to bind (default 127.0.0.1)").option("--claude <path>", "path to the claude executable").option("--provider <id>", "default provider for bare model ids (claude, codex, opencode, gemini, \u2026)").option("--cors", "enable permissive CORS (for browser clients)").option("--daemon", "run in the background (managed with `yagami stop`/`yagami status`)").option("--log <file>", "log file for --daemon mode (default ~/.config/yagami/yagami.log)").action(async (opts) => {
33
+ const fileConfig = loadFileConfig();
34
+ let freshKey;
35
+ if (fileConfig.apiKeys.length === 0 && !process.env["YAGAMI_API_KEY"]) {
36
+ freshKey = generateApiKey();
37
+ fileConfig.apiKeys.push(freshKey);
38
+ saveConfig(fileConfig);
39
+ }
40
+ if (opts.daemon) {
41
+ await startDaemon(opts, freshKey);
42
+ return;
43
+ }
44
+ try {
45
+ const running = await startYagami({
46
+ port: opts.port !== void 0 ? Number(opts.port) : void 0,
47
+ host: opts.host,
48
+ claudePath: opts.claude,
49
+ defaultProvider: opts.provider,
50
+ cors: opts.cors
51
+ });
52
+ writeServerState({
53
+ pid: process.pid,
54
+ host: running.config.host,
55
+ port: running.config.port,
56
+ url: running.url,
57
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
58
+ version: VERSION,
59
+ ...process.env["YAGAMI_LOG_FILE"] ? { log: process.env["YAGAMI_LOG_FILE"] } : {}
60
+ });
61
+ const shutdown = () => {
62
+ running.sessionCache.persistNow();
63
+ clearServerState(process.pid);
64
+ void running.close().finally(() => process.exit(0));
65
+ setTimeout(() => process.exit(0), 3e3).unref?.();
66
+ };
67
+ process.on("SIGINT", shutdown);
68
+ process.on("SIGTERM", shutdown);
69
+ const engine = running.engine;
70
+ const version = await engine.defaultProvider.version();
71
+ const others = engine.providerIds.filter((id) => id !== engine.defaultProviderId);
72
+ console.log(`yagami v${VERSION}`);
73
+ console.log(` listening ${running.url}`);
74
+ console.log(` provider ${engine.defaultProviderId} \u2014 ${engine.executable}${version ? ` (${version})` : ""}`);
75
+ console.log(
76
+ ` also ${others.length > 0 ? `${others.join(", ")} (use model "<provider>:<model>")` : "no other harness CLIs found \u2014 see `yagami doctor`"}`
77
+ );
78
+ console.log(` config ${configFilePath()}`);
79
+ if (freshKey) {
80
+ console.log(` api key ${freshKey}`);
81
+ console.log(" (newly generated and saved \u2014 copy it now, it is shown in full only once)");
82
+ } else {
83
+ console.log(` api keys ${running.config.apiKeys.map(maskKey).join(", ")}`);
84
+ }
85
+ if (!["127.0.0.1", "localhost", "::1"].includes(running.config.host)) {
86
+ console.log(
87
+ ` \u26A0 bound to ${running.config.host} \u2014 reachable beyond this machine. Only do this on a network you trust.`
88
+ );
89
+ }
90
+ console.log("\nPoint any Anthropic SDK at it:");
91
+ console.log(` baseURL: "${running.url}" apiKey: <your yagami key>`);
92
+ } catch (err) {
93
+ console.error(`yagami: ${err instanceof Error ? err.message : String(err)}`);
94
+ process.exitCode = 1;
95
+ }
96
+ });
97
+ async function startDaemon(opts, freshKey) {
98
+ const existing = readServerState();
99
+ if (existing && isProcessAlive(existing.pid)) {
100
+ console.error(`yagami is already running (pid ${existing.pid}, ${existing.url}) \u2014 \`yagami stop\` first`);
101
+ process.exitCode = 1;
102
+ return;
103
+ }
104
+ clearServerState();
105
+ const logPath = opts.log ? path.resolve(opts.log) : logFilePath();
106
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
107
+ const fd = fs.openSync(logPath, "a");
108
+ const args = [process.argv[1], "start"];
109
+ if (opts.port !== void 0) args.push("-p", opts.port);
110
+ if (opts.host !== void 0) args.push("-H", opts.host);
111
+ if (opts.claude !== void 0) args.push("--claude", opts.claude);
112
+ if (opts.provider !== void 0) args.push("--provider", opts.provider);
113
+ if (opts.cors) args.push("--cors");
114
+ const child = spawn(process.execPath, args, {
115
+ detached: true,
116
+ stdio: ["ignore", fd, fd],
117
+ env: { ...process.env, YAGAMI_LOG_FILE: logPath }
118
+ });
119
+ fs.closeSync(fd);
120
+ let exitCode;
121
+ child.on("exit", (code) => {
122
+ exitCode = code;
123
+ });
124
+ child.unref();
125
+ const deadline = Date.now() + 15e3;
126
+ while (Date.now() < deadline && exitCode === void 0) {
127
+ const state = readServerState();
128
+ if (state && state.pid === child.pid) {
129
+ console.log(`yagami v${VERSION} running in the background`);
130
+ console.log(` pid ${child.pid}`);
131
+ console.log(` url ${state.url}`);
132
+ console.log(` log ${logPath}`);
133
+ if (freshKey) {
134
+ console.log(` key ${freshKey}`);
135
+ console.log(" (newly generated and saved \u2014 copy it now, it is shown in full only once)");
136
+ }
137
+ return;
138
+ }
139
+ await sleep(200);
140
+ }
141
+ console.error(
142
+ exitCode !== void 0 ? `yagami exited immediately (code ${exitCode}) \u2014 see ${logPath}` : `yagami did not report ready within 15s \u2014 see ${logPath}`
143
+ );
144
+ process.exitCode = 1;
145
+ }
146
+ program.command("stop").description("stop a running yagami server").action(async () => {
147
+ const state = readServerState();
148
+ if (!state || !isProcessAlive(state.pid)) {
149
+ if (state) clearServerState();
150
+ console.log("yagami is not running");
151
+ return;
152
+ }
153
+ process.kill(state.pid, "SIGTERM");
154
+ const deadline = Date.now() + 5e3;
155
+ while (Date.now() < deadline) {
156
+ if (!isProcessAlive(state.pid)) {
157
+ clearServerState();
158
+ console.log(`stopped yagami (pid ${state.pid})`);
159
+ return;
160
+ }
161
+ await sleep(100);
162
+ }
163
+ console.error(`yagami (pid ${state.pid}) did not exit within 5s`);
164
+ process.exitCode = 1;
165
+ });
166
+ program.command("status").description("show whether yagami is running, plus request/cost totals").action(async () => {
167
+ const state = readServerState();
168
+ if (!state || !isProcessAlive(state.pid)) {
169
+ if (state) clearServerState();
170
+ console.log("yagami is not running");
171
+ process.exitCode = 1;
172
+ return;
173
+ }
174
+ console.log(`yagami running (pid ${state.pid})`);
175
+ console.log(` url ${state.url}`);
176
+ console.log(` since ${state.startedAt}`);
177
+ if (state.log) console.log(` log ${state.log}`);
178
+ try {
179
+ const res = await fetch(`${state.url}/healthz`, { signal: AbortSignal.timeout(3e3) });
180
+ const body = await res.json();
181
+ console.log(` version ${body.version ?? "?"}`);
182
+ console.log(` claude ${body.claude ?? "?"}`);
183
+ console.log(` requests ${body.requests ?? 0}`);
184
+ console.log(` cost $${(body.total_cost_usd ?? 0).toFixed(4)} (would-be API cost since start)`);
185
+ } catch {
186
+ console.log(` healthz unreachable \u2014 process is alive but ${state.url} is not answering`);
187
+ }
188
+ });
189
+ program.command("keygen").description("generate an API key and add it to the config").action(() => {
190
+ const cfg = loadFileConfig();
191
+ const key = generateApiKey();
192
+ cfg.apiKeys.push(key);
193
+ const file = saveConfig(cfg);
194
+ console.log(key);
195
+ console.error(`saved to ${file} (${cfg.apiKeys.length} key${cfg.apiKeys.length === 1 ? "" : "s"} total)`);
196
+ });
197
+ program.command("doctor").description("check which coding-agent CLIs yagami can drive and whether they work").option("--live", "send one real (tiny) completion through the default provider").option("--provider <id>", "provider to use for --live (default: config/claude)").action(async (opts) => {
198
+ let failed = false;
199
+ const cfg = loadConfig();
200
+ const providerConfig = { ...cfg.providers };
201
+ if (cfg.claudePath || cfg.claudeConfigDir) {
202
+ providerConfig["claude"] = {
203
+ ...providerConfig["claude"],
204
+ ...cfg.claudePath ? { path: cfg.claudePath } : {},
205
+ ...cfg.claudeConfigDir ? { configDir: cfg.claudeConfigDir } : {}
206
+ };
207
+ }
208
+ const defaultProvider = opts.provider ?? cfg.defaultProvider ?? "claude";
209
+ console.log(`node ${process.version}`);
210
+ console.log(`config ${configFilePath()}${fs.existsSync(configFilePath()) ? "" : " (not created yet)"}`);
211
+ console.log(`api keys ${cfg.apiKeys.length === 0 ? "none \u2014 run `yagami keygen`" : cfg.apiKeys.map(maskKey).join(", ")}`);
212
+ console.log(`bind ${cfg.host}:${cfg.port}`);
213
+ console.log(`sessions ${sessionCachePath()}${fs.existsSync(sessionCachePath()) ? "" : " (empty)"}`);
214
+ const state = readServerState();
215
+ console.log(
216
+ `server ${state && isProcessAlive(state.pid) ? `running (pid ${state.pid}, ${state.url})` : "not running"}`
217
+ );
218
+ console.log('\nproviders (model ids route as "<provider>:<model>"; bare ids go to the default)');
219
+ const detected = detectProviders(providerConfig);
220
+ const installed = detected.filter((d) => d.installed);
221
+ for (const d of detected) {
222
+ const marker = d.id === defaultProvider ? "*" : " ";
223
+ if (!d.installed) {
224
+ if (presetIsNiche(d.id)) continue;
225
+ console.log(` ${marker} ${d.id.padEnd(11)} not installed \u2014 ${d.installHint}`);
226
+ continue;
227
+ }
228
+ let version;
229
+ try {
230
+ version = await createProvider(d.id, providerConfig[d.id] ?? {}, {}).version();
231
+ } catch (err) {
232
+ version = `\u2717 ${err instanceof Error ? err.message : String(err)}`;
233
+ }
234
+ console.log(` ${marker} ${d.id.padEnd(11)} ${d.path}${version ? ` (${version})` : ""}`);
235
+ }
236
+ const hidden = detected.filter((d) => !d.installed && presetIsNiche(d.id)).length;
237
+ if (hidden > 0) console.log(` \u2026 ${hidden} more ACP presets not installed (see README for the full list)`);
238
+ if (!installed.some((d) => d.id === defaultProvider)) {
239
+ failed = true;
240
+ console.log(` \u2717 default provider "${defaultProvider}" is not installed`);
241
+ }
242
+ if (installed.some((d) => d.id === "claude")) {
243
+ try {
244
+ const claude = createProvider("claude", providerConfig["claude"] ?? {}, {});
245
+ const skew = await claude.versionSkew();
246
+ if (skew) {
247
+ console.log(`
248
+ agent sdk ${skew.sdkVersion} \u2194 claude ${skew.binaryVersion} \u2014 ${skew.inSync ? "in sync" : `\u26A0 ${skew.note}`}`);
249
+ }
250
+ } catch {
251
+ }
252
+ }
253
+ if (opts.live && !failed) {
254
+ console.log(`
255
+ live check: sending one tiny completion through ${defaultProvider}\u2026`);
256
+ try {
257
+ const engine = new YagamiEngine({ providerConfig, defaultProvider, ...cfg.defaultModel ? { defaultModel: cfg.defaultModel } : {} });
258
+ const started = Date.now();
259
+ const result = await engine.complete({
260
+ messages: [{ role: "user", content: "Reply with exactly: pong" }],
261
+ max_tokens: 32
262
+ });
263
+ const text = result.response.content.filter((b) => b.type === "text").map((b) => b["text"]).join("");
264
+ console.log(` reply ${JSON.stringify(text)}`);
265
+ console.log(` model ${result.response.model}`);
266
+ console.log(` latency ${((Date.now() - started) / 1e3).toFixed(1)}s`);
267
+ if (result.costUsd !== void 0) console.log(` cost $${result.costUsd.toFixed(6)}`);
268
+ } catch (err) {
269
+ failed = true;
270
+ console.log(` \u2717 ${err instanceof Error ? err.message : String(err)}`);
271
+ }
272
+ }
273
+ if (failed) process.exitCode = 1;
274
+ });
275
+ function presetIsNiche(id) {
276
+ return !["claude", "codex", "opencode", "gemini", "copilot", "cursor", "qwen", "goose", "kimi"].includes(id);
277
+ }
278
+ program.command("models").description("list models across every installed provider (ids are ready to paste into requests)").option("--provider <id>", "only this provider").action(async (opts) => {
279
+ const cfg = loadConfig();
280
+ try {
281
+ const engine = new YagamiEngine({
282
+ ...cfg.providers ? { providerConfig: cfg.providers } : {},
283
+ ...cfg.defaultProvider ? { defaultProvider: cfg.defaultProvider } : {}
284
+ });
285
+ const models = await engine.listModels();
286
+ const byProvider = /* @__PURE__ */ new Map();
287
+ for (const m of models) {
288
+ if (opts.provider && m.provider !== opts.provider) continue;
289
+ if (!m.id.includes(":")) continue;
290
+ const list = byProvider.get(m.provider ?? "?") ?? [];
291
+ list.push(m);
292
+ byProvider.set(m.provider ?? "?", list);
293
+ }
294
+ for (const [provider, list] of byProvider) {
295
+ console.log(`${provider}${provider === engine.defaultProviderId ? " (default \u2014 bare ids work too)" : ""}`);
296
+ for (const m of list) {
297
+ console.log(` ${m.id.padEnd(40)} ${m.display_name}${m.resolved_model ? ` \u2192 ${m.resolved_model}` : ""}`);
298
+ }
299
+ }
300
+ if (byProvider.size === 0) console.log("no models reported \u2014 run `yagami doctor`");
301
+ } catch (err) {
302
+ console.error(`yagami: ${err instanceof Error ? err.message : String(err)}`);
303
+ process.exitCode = 1;
304
+ }
305
+ });
306
+ await program.parseAsync(process.argv);
307
+ //# sourceMappingURL=cli.js.map