@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.
@@ -108,6 +108,9 @@ function parseModelRef(model, providerIds) {
108
108
  function qualifiedModel(providerId, model) {
109
109
  return `${providerId}:${model}`;
110
110
  }
111
+ function isSessionProvider(p) {
112
+ return typeof p.openSession === "function";
113
+ }
111
114
 
112
115
  // src/core/executable.ts
113
116
  import * as fs from "fs";
@@ -180,7 +183,7 @@ function resolveClaudeExecutable(explicit) {
180
183
  }
181
184
 
182
185
  // src/version.ts
183
- var VERSION = "0.4.1";
186
+ var VERSION = "0.5.0";
184
187
 
185
188
  // src/core/providers/acp.ts
186
189
  import { spawn, spawnSync } from "child_process";
@@ -439,6 +442,22 @@ var AcpProvider = class {
439
442
  conn.close();
440
443
  }
441
444
  }
445
+ /**
446
+ * A live, interactive ACP session: the agent runs warm in the project
447
+ * directory with ITS OWN defaults — no plan-mode hardening, no forced
448
+ * config. Tool calls stream as normalized events and permission requests
449
+ * go to the host's handler, exactly as the agent's own client would ask.
450
+ */
451
+ openSession(options) {
452
+ return new AcpAgentSession({
453
+ id: this.id,
454
+ modelConfigId: this.modelConfigId,
455
+ connect: (cwd) => this.connectImpl(cwd),
456
+ classify: (err, ctx) => this.classify(err, ctx),
457
+ selectModel: (conn, sessionId, configOptions, model) => this.selectModel(conn, sessionId, configOptions, model),
458
+ options
459
+ });
460
+ }
442
461
  async selectModel(conn, sessionId, configOptions, model) {
443
462
  const option = configOptions?.find((o) => o.id === this.modelConfigId) ?? configOptions?.find((o) => o.category === "model");
444
463
  if (!option || option.type !== "select") {
@@ -495,6 +514,175 @@ var AcpProvider = class {
495
514
  return `${plain ?? "unknown version"} \u26A0 no ACP handshake (${handshakeError ?? "unknown error"}) \u2014 too old, or a different program with the same name?`;
496
515
  }
497
516
  };
517
+ var AcpAgentSession = class {
518
+ constructor(cfg) {
519
+ this.cfg = cfg;
520
+ this.provider = cfg.id;
521
+ }
522
+ cfg;
523
+ provider;
524
+ conn;
525
+ sessionId;
526
+ queue = null;
527
+ opening;
528
+ closed = false;
529
+ costUsd;
530
+ /** Updates often omit title/kind — remember them from the start event. */
531
+ toolMeta = /* @__PURE__ */ new Map();
532
+ get id() {
533
+ return this.sessionId;
534
+ }
535
+ ensureOpen() {
536
+ this.opening ??= this.open();
537
+ return this.opening;
538
+ }
539
+ async open() {
540
+ const { options } = this.cfg;
541
+ const conn = await this.cfg.connect(options.cwd);
542
+ this.conn = conn;
543
+ let configOptions;
544
+ if (options.resume && supportsResume(conn.init)) {
545
+ const resumed = await conn.agent.resumeSession({ sessionId: options.resume, cwd: options.cwd }).catch((err) => {
546
+ throw this.cfg.classify(err);
547
+ });
548
+ this.sessionId = options.resume;
549
+ configOptions = resumed.configOptions;
550
+ } else {
551
+ const created = await conn.agent.newSession({ cwd: options.cwd, mcpServers: [] }).catch((err) => {
552
+ throw this.cfg.classify(err);
553
+ });
554
+ this.sessionId = created.sessionId;
555
+ configOptions = created.configOptions;
556
+ }
557
+ const sessionId = this.sessionId;
558
+ const mode = options.native?.mode;
559
+ if (mode) await conn.agent.setSessionMode({ sessionId, modeId: mode }).catch(() => {
560
+ });
561
+ if (options.model) await this.cfg.selectModel(conn, sessionId, configOptions, options.model);
562
+ conn.setHandlers({
563
+ onPermission: (p) => this.onPermission(p),
564
+ onUpdate: (n) => this.onUpdate(n)
565
+ });
566
+ }
567
+ async onPermission(p) {
568
+ const toolCall = p.toolCall;
569
+ const request = {
570
+ provider: this.provider,
571
+ ...this.sessionId ? { sessionId: this.sessionId } : {},
572
+ tool: toolCall?.title ?? "tool",
573
+ ...toolCall?.kind ? { kind: toolCall.kind } : {},
574
+ ...toolCall?.title ? { title: toolCall.title } : {},
575
+ input: toolCall?.rawInput,
576
+ raw: p
577
+ };
578
+ let decision = "deny";
579
+ try {
580
+ decision = await this.cfg.options.permissions.decide(request);
581
+ } catch {
582
+ }
583
+ this.queue?.push({ type: "permission", request, decision });
584
+ const preferred = {
585
+ allow: ["allow_once", "allow_always"],
586
+ allow_always: ["allow_always", "allow_once"],
587
+ deny: ["reject_once", "reject_always"],
588
+ deny_always: ["reject_always", "reject_once"]
589
+ };
590
+ for (const kind of preferred[decision]) {
591
+ const option = p.options.find((o) => o.kind === kind);
592
+ if (option) return { outcome: { outcome: "selected", optionId: option.optionId } };
593
+ }
594
+ return rejectOption(p);
595
+ }
596
+ onUpdate(n) {
597
+ if (n.sessionId !== this.sessionId) return;
598
+ const u = n.update;
599
+ switch (u.sessionUpdate) {
600
+ case "agent_message_chunk":
601
+ if (u.content.type === "text") this.queue?.push({ type: "text", text: u.content.text });
602
+ break;
603
+ case "agent_thought_chunk":
604
+ if (u.content.type === "text") this.queue?.push({ type: "thinking", text: u.content.text });
605
+ break;
606
+ case "tool_call": {
607
+ const t = u;
608
+ const meta = { name: t.kind ?? "tool", ...t.title ? { title: t.title } : {}, ...t.kind ? { kind: t.kind } : {} };
609
+ this.toolMeta.set(t.toolCallId, meta);
610
+ this.queue?.push({
611
+ type: "tool_call",
612
+ id: t.toolCallId,
613
+ status: "started",
614
+ ...meta,
615
+ ...t.rawInput !== void 0 ? { input: t.rawInput } : {}
616
+ });
617
+ break;
618
+ }
619
+ case "tool_call_update": {
620
+ const t = u;
621
+ const known = this.toolMeta.get(t.toolCallId);
622
+ const meta = {
623
+ name: t.kind ?? known?.name ?? "tool",
624
+ ...t.title ?? known?.title ? { title: t.title ?? known?.title } : {},
625
+ ...t.kind ?? known?.kind ? { kind: t.kind ?? known?.kind } : {}
626
+ };
627
+ this.queue?.push({
628
+ type: "tool_call",
629
+ id: t.toolCallId,
630
+ status: t.status === "completed" ? "completed" : t.status === "failed" ? "failed" : "updated",
631
+ ...meta,
632
+ ...t.rawOutput !== void 0 ? { output: t.rawOutput } : {}
633
+ });
634
+ break;
635
+ }
636
+ case "usage_update": {
637
+ const cost = u.cost;
638
+ if (cost && typeof cost.amount === "number" && (cost.currency ?? "USD") === "USD") this.costUsd = cost.amount;
639
+ break;
640
+ }
641
+ default:
642
+ this.queue?.push({ type: "raw", provider: this.provider, payload: u });
643
+ break;
644
+ }
645
+ }
646
+ async *send(input) {
647
+ if (this.closed) throw new ProviderError(this.provider, "session is closed");
648
+ if (this.queue) throw new ProviderError(this.provider, "a turn is already running");
649
+ await this.ensureOpen();
650
+ const sessionId = this.sessionId;
651
+ const text = typeof input === "string" ? input : input.filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n");
652
+ const media = typeof input === "string" ? [] : input.filter((b) => b.type === "image");
653
+ const queue = new AsyncQueue();
654
+ this.queue = queue;
655
+ this.costUsd = void 0;
656
+ this.conn.agent.prompt({ sessionId, prompt: toAcpBlocks(text, media, this.provider) }).then((res) => {
657
+ queue.push({
658
+ type: "done",
659
+ usage: mapAcpUsage(res.usage ?? void 0),
660
+ ...this.costUsd !== void 0 ? { costUsd: this.costUsd } : {},
661
+ stopReason: mapStopReason(res.stopReason)
662
+ });
663
+ queue.end();
664
+ }).catch((err) => queue.fail(this.cfg.classify(err)));
665
+ try {
666
+ yield { type: "session", sessionId };
667
+ for await (const event of queue) yield event;
668
+ } finally {
669
+ if (this.queue === queue) this.queue = null;
670
+ }
671
+ }
672
+ async interrupt() {
673
+ if (!this.sessionId || !this.conn) return;
674
+ await this.conn.agent.cancel({ sessionId: this.sessionId }).catch(() => {
675
+ });
676
+ }
677
+ async close() {
678
+ if (this.closed) return;
679
+ this.closed = true;
680
+ this.queue?.fail(new ProviderError(this.provider, "session closed"));
681
+ this.queue = null;
682
+ this.conn?.close();
683
+ this.conn = void 0;
684
+ }
685
+ };
498
686
  function supportsResume(init) {
499
687
  const caps = init.agentCapabilities;
500
688
  return caps?.sessionCapabilities?.resume !== void 0;
@@ -787,15 +975,419 @@ function mediaPrompt(text, media) {
787
975
  }
788
976
 
789
977
  // src/core/providers/codex.ts
790
- import { spawn as spawn3, spawnSync as spawnSync3 } from "child_process";
978
+ import { spawn as spawn4, spawnSync as spawnSync3 } from "child_process";
791
979
  import * as fs4 from "fs";
792
980
  import * as os4 from "os";
793
981
  import * as path4 from "path";
794
- import * as readline3 from "readline";
982
+ import * as readline4 from "readline";
795
983
 
796
- // src/core/providers/jsonl.ts
984
+ // src/core/providers/codexSession.ts
797
985
  import { spawn as spawn2 } from "child_process";
798
986
  import * as readline2 from "readline";
987
+ var CodexAgentSession = class {
988
+ constructor(config) {
989
+ this.config = config;
990
+ }
991
+ config;
992
+ provider = "codex";
993
+ child;
994
+ nextId = 1;
995
+ pending = /* @__PURE__ */ new Map();
996
+ threadId;
997
+ currentTurnId;
998
+ queue = null;
999
+ lastUsage;
1000
+ opening;
1001
+ closed = false;
1002
+ /** Item text already emitted as deltas, so item/completed only fills gaps. */
1003
+ emitted = /* @__PURE__ */ new Map();
1004
+ get id() {
1005
+ return this.threadId;
1006
+ }
1007
+ fail(err) {
1008
+ for (const [, p] of this.pending) p.reject(err);
1009
+ this.pending.clear();
1010
+ this.queue?.fail(err);
1011
+ this.queue = null;
1012
+ }
1013
+ classify(err) {
1014
+ const message = err instanceof Error ? err.message : String(err);
1015
+ if (looksLikeAuthFailure(message)) {
1016
+ return new AuthRequiredError("codex", this.config.loginCommand, message.slice(0, 200));
1017
+ }
1018
+ return classifyProviderFailure("codex", this.config.loginCommand, err);
1019
+ }
1020
+ request(method, params) {
1021
+ const child = this.child;
1022
+ if (!child?.stdin?.writable) return Promise.reject(new ProviderError("codex", "app-server is not running"));
1023
+ const id = this.nextId++;
1024
+ const promise = new Promise((resolve, reject) => {
1025
+ this.pending.set(id, { resolve, reject });
1026
+ });
1027
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
1028
+ `);
1029
+ return promise;
1030
+ }
1031
+ respond(id, result) {
1032
+ this.child?.stdin?.write(`${JSON.stringify({ jsonrpc: "2.0", id, result })}
1033
+ `);
1034
+ }
1035
+ notify(method, params) {
1036
+ this.child?.stdin?.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
1037
+ `);
1038
+ }
1039
+ ensureOpen() {
1040
+ this.opening ??= this.open();
1041
+ return this.opening;
1042
+ }
1043
+ async open() {
1044
+ const { executable, env, options } = this.config;
1045
+ const child = spawn2(executable, ["app-server"], {
1046
+ cwd: options.cwd,
1047
+ env,
1048
+ stdio: ["pipe", "pipe", "pipe"]
1049
+ });
1050
+ this.child = child;
1051
+ let stderr = "";
1052
+ child.stderr?.on("data", (d) => {
1053
+ stderr += d.toString();
1054
+ if (stderr.length > 16e3) stderr = stderr.slice(-8e3);
1055
+ });
1056
+ child.on("error", (err) => this.fail(this.classify(err)));
1057
+ child.on("exit", (code) => {
1058
+ if (this.closed) return;
1059
+ this.fail(this.classify(new Error(`codex app-server exited with code ${code}${stderr ? `: ${stderr.trim().slice(-400)}` : ""}`)));
1060
+ });
1061
+ const rl = readline2.createInterface({ input: child.stdout });
1062
+ rl.on("line", (line) => {
1063
+ let msg;
1064
+ try {
1065
+ msg = JSON.parse(line);
1066
+ } catch {
1067
+ return;
1068
+ }
1069
+ this.dispatch(msg);
1070
+ });
1071
+ await this.request("initialize", {
1072
+ clientInfo: { name: this.config.options.appName ?? "yagami", title: this.config.options.appName ?? "yagami", version: VERSION },
1073
+ capabilities: { experimentalApi: true, requestAttestation: false }
1074
+ });
1075
+ this.notify("initialized", {});
1076
+ const native = options.native ?? {};
1077
+ const overrides = {
1078
+ cwd: options.cwd,
1079
+ ...options.model ? { model: options.model } : {},
1080
+ ...native.approvalPolicy ? { approvalPolicy: native.approvalPolicy } : {},
1081
+ ...native.sandbox ? { sandbox: native.sandbox } : {},
1082
+ ...native.config ? { config: native.config } : {},
1083
+ ...options.systemPrompt ? { developerInstructions: options.systemPrompt } : {}
1084
+ };
1085
+ if (options.resume) {
1086
+ try {
1087
+ const resumed = await this.request("thread/resume", { threadId: options.resume, ...overrides });
1088
+ this.threadId = resumed["thread"]?.id ?? options.resume;
1089
+ return;
1090
+ } catch {
1091
+ }
1092
+ }
1093
+ const started = await this.request("thread/start", overrides);
1094
+ this.threadId = started["thread"]?.id;
1095
+ if (!this.threadId) throw new ProviderError("codex", "thread/start returned no thread id");
1096
+ }
1097
+ // ── inbound traffic ────────────────────────────────────────────────
1098
+ dispatch(msg) {
1099
+ if (msg.method === void 0 && msg.id !== void 0) {
1100
+ const pending = this.pending.get(msg.id);
1101
+ if (!pending) return;
1102
+ this.pending.delete(msg.id);
1103
+ if (msg.error) pending.reject(this.classify(new Error(msg.error.message ?? "codex error")));
1104
+ else pending.resolve(msg.result ?? {});
1105
+ return;
1106
+ }
1107
+ if (!msg.method) return;
1108
+ if (msg.id !== void 0) {
1109
+ void this.handleServerRequest(msg.method, msg.id, msg.params ?? {});
1110
+ return;
1111
+ }
1112
+ this.handleNotification(msg.method, msg.params ?? {});
1113
+ }
1114
+ push(event) {
1115
+ this.queue?.push(event);
1116
+ }
1117
+ handleNotification(method, params) {
1118
+ if (params["threadId"] !== void 0 && params["threadId"] !== this.threadId) return;
1119
+ switch (method) {
1120
+ case "item/agentMessage/delta": {
1121
+ const itemId = params["itemId"];
1122
+ const delta = params["delta"];
1123
+ this.emitted.set(itemId, (this.emitted.get(itemId) ?? 0) + delta.length);
1124
+ this.push({ type: "text", text: delta });
1125
+ break;
1126
+ }
1127
+ case "item/started":
1128
+ case "item/completed": {
1129
+ this.handleItem(params["item"], method === "item/completed");
1130
+ break;
1131
+ }
1132
+ case "thread/tokenUsage/updated": {
1133
+ const usage = params["tokenUsage"];
1134
+ const last = usage?.last;
1135
+ if (last) {
1136
+ this.lastUsage = {
1137
+ input_tokens: last["inputTokens"] ?? 0,
1138
+ output_tokens: last["outputTokens"] ?? 0,
1139
+ cache_read_input_tokens: last["cachedInputTokens"] ?? 0,
1140
+ cache_creation_input_tokens: last["cacheWriteInputTokens"] ?? 0
1141
+ };
1142
+ }
1143
+ break;
1144
+ }
1145
+ case "turn/completed": {
1146
+ const turn = params["turn"];
1147
+ const queue = this.queue;
1148
+ this.queue = null;
1149
+ this.currentTurnId = void 0;
1150
+ if (!queue) break;
1151
+ if (turn.status === "failed") {
1152
+ queue.fail(this.classify(new Error(turn.error?.message ?? "turn failed")));
1153
+ } else {
1154
+ queue.push({
1155
+ type: "done",
1156
+ ...this.lastUsage ? { usage: this.lastUsage } : {},
1157
+ stopReason: turn.status === "interrupted" ? "interrupted" : "end_turn"
1158
+ });
1159
+ queue.end();
1160
+ }
1161
+ break;
1162
+ }
1163
+ case "error": {
1164
+ this.push({ type: "raw", provider: "codex", payload: { method, params } });
1165
+ break;
1166
+ }
1167
+ default:
1168
+ break;
1169
+ }
1170
+ }
1171
+ /** Normalize thread items into tool_call / thinking events. */
1172
+ handleItem(item, completed) {
1173
+ if (!item) return;
1174
+ const id = String(item["id"] ?? "");
1175
+ switch (item["type"]) {
1176
+ case "agentMessage": {
1177
+ if (!completed) break;
1178
+ const text = String(item["text"] ?? "");
1179
+ const seen = this.emitted.get(id) ?? 0;
1180
+ if (text.length > seen) this.push({ type: "text", text: text.slice(seen) });
1181
+ this.emitted.delete(id);
1182
+ break;
1183
+ }
1184
+ case "reasoning": {
1185
+ if (!completed) break;
1186
+ const summary = item["summary"]?.join("\n") ?? "";
1187
+ if (summary) this.push({ type: "thinking", text: summary });
1188
+ break;
1189
+ }
1190
+ case "commandExecution": {
1191
+ const failed = item["status"] === "failed" || item["status"] === "declined";
1192
+ this.push({
1193
+ type: "tool_call",
1194
+ id,
1195
+ name: "shell",
1196
+ status: completed ? failed ? "failed" : "completed" : "started",
1197
+ title: String(item["command"] ?? "command"),
1198
+ kind: "execute",
1199
+ input: { command: item["command"], cwd: item["cwd"] },
1200
+ ...completed ? { output: { output: item["aggregatedOutput"], exitCode: item["exitCode"] } } : {}
1201
+ });
1202
+ break;
1203
+ }
1204
+ case "fileChange": {
1205
+ const changes = item["changes"] ?? [];
1206
+ const failed = item["status"] === "failed" || item["status"] === "declined";
1207
+ this.push({
1208
+ type: "tool_call",
1209
+ id,
1210
+ name: "apply_patch",
1211
+ status: completed ? failed ? "failed" : "completed" : "started",
1212
+ title: changes.map((c) => c.path).filter(Boolean).join(", ") || "file changes",
1213
+ kind: "edit",
1214
+ input: { changes }
1215
+ });
1216
+ break;
1217
+ }
1218
+ case "mcpToolCall": {
1219
+ this.push({
1220
+ type: "tool_call",
1221
+ id,
1222
+ name: `${String(item["server"] ?? "mcp")}.${String(item["tool"] ?? "tool")}`,
1223
+ status: completed ? item["status"] === "failed" ? "failed" : "completed" : "started",
1224
+ kind: "other",
1225
+ input: item["arguments"]
1226
+ });
1227
+ break;
1228
+ }
1229
+ case "webSearch": {
1230
+ this.push({
1231
+ type: "tool_call",
1232
+ id,
1233
+ name: "web_search",
1234
+ status: completed ? "completed" : "started",
1235
+ kind: "fetch",
1236
+ input: { query: item["query"] }
1237
+ });
1238
+ break;
1239
+ }
1240
+ case "userMessage":
1241
+ case "plan":
1242
+ break;
1243
+ default:
1244
+ this.push({ type: "raw", provider: "codex", payload: item });
1245
+ break;
1246
+ }
1247
+ }
1248
+ // ── approvals: forwarded to the host, answered like the TUI would ──
1249
+ async decide(request) {
1250
+ try {
1251
+ const decision = await this.config.options.permissions.decide(request);
1252
+ this.push({ type: "permission", request, decision });
1253
+ return decision;
1254
+ } catch {
1255
+ return "deny";
1256
+ }
1257
+ }
1258
+ async handleServerRequest(method, id, params) {
1259
+ switch (method) {
1260
+ case "item/commandExecution/requestApproval": {
1261
+ const decision = await this.decide({
1262
+ provider: "codex",
1263
+ ...this.threadId ? { sessionId: this.threadId } : {},
1264
+ tool: "shell",
1265
+ kind: "execute",
1266
+ title: String(params["command"] ?? "command"),
1267
+ input: { command: params["command"], cwd: params["cwd"], reason: params["reason"] },
1268
+ raw: params
1269
+ });
1270
+ this.respond(id, {
1271
+ decision: decision === "allow" ? "accept" : decision === "allow_always" ? "acceptForSession" : "decline"
1272
+ });
1273
+ break;
1274
+ }
1275
+ case "item/fileChange/requestApproval": {
1276
+ const decision = await this.decide({
1277
+ provider: "codex",
1278
+ ...this.threadId ? { sessionId: this.threadId } : {},
1279
+ tool: "apply_patch",
1280
+ kind: "edit",
1281
+ title: String(params["reason"] ?? "apply file changes"),
1282
+ input: { reason: params["reason"], grantRoot: params["grantRoot"] },
1283
+ raw: params
1284
+ });
1285
+ this.respond(id, {
1286
+ decision: decision === "allow" ? "accept" : decision === "allow_always" ? "acceptForSession" : "decline"
1287
+ });
1288
+ break;
1289
+ }
1290
+ case "item/permissions/requestApproval": {
1291
+ const requested = params["permissions"];
1292
+ const decision = await this.decide({
1293
+ provider: "codex",
1294
+ ...this.threadId ? { sessionId: this.threadId } : {},
1295
+ tool: "permissions",
1296
+ kind: "other",
1297
+ title: String(params["reason"] ?? "extra permissions"),
1298
+ input: requested,
1299
+ raw: params
1300
+ });
1301
+ const granted = decision === "allow" || decision === "allow_always";
1302
+ this.respond(id, {
1303
+ permissions: granted ? { network: requested?.["network"] ?? void 0, fileSystem: requested?.["fileSystem"] ?? void 0 } : {},
1304
+ scope: decision === "allow_always" ? "session" : "turn"
1305
+ });
1306
+ break;
1307
+ }
1308
+ // legacy approval shapes, still sent by some codepaths
1309
+ case "execCommandApproval":
1310
+ case "applyPatchApproval": {
1311
+ const decision = await this.decide({
1312
+ provider: "codex",
1313
+ ...this.threadId ? { sessionId: this.threadId } : {},
1314
+ tool: method === "execCommandApproval" ? "shell" : "apply_patch",
1315
+ kind: method === "execCommandApproval" ? "execute" : "edit",
1316
+ title: String(params["command"] ?? params["reason"] ?? "approval"),
1317
+ input: params,
1318
+ raw: params
1319
+ });
1320
+ this.respond(id, {
1321
+ decision: decision === "allow" ? "approved" : decision === "allow_always" ? "approved_for_session" : { denied: { rejection: "denied by the user" } }
1322
+ });
1323
+ break;
1324
+ }
1325
+ case "item/tool/requestUserInput": {
1326
+ this.respond(id, { answers: {} });
1327
+ break;
1328
+ }
1329
+ case "mcpServer/elicitation/request": {
1330
+ this.respond(id, { action: "decline", content: null, _meta: null });
1331
+ break;
1332
+ }
1333
+ default: {
1334
+ this.child?.stdin?.write(
1335
+ `${JSON.stringify({ jsonrpc: "2.0", id, error: { code: -32601, message: `unsupported request: ${method}` } })}
1336
+ `
1337
+ );
1338
+ break;
1339
+ }
1340
+ }
1341
+ }
1342
+ // ── the ProviderSession surface ────────────────────────────────────
1343
+ async *send(input) {
1344
+ if (this.closed) throw new ProviderError("codex", "session is closed");
1345
+ if (this.queue) throw new ProviderError("codex", "a turn is already running");
1346
+ await this.ensureOpen();
1347
+ const blocks = typeof input === "string" ? [{ type: "text", text: input }] : input;
1348
+ const { paths, cleanup } = writeTempImages(blocks);
1349
+ const items = [];
1350
+ for (const block of blocks) {
1351
+ if (block.type === "text" && typeof block.text === "string") {
1352
+ items.push({ type: "text", text: block.text, text_elements: [] });
1353
+ }
1354
+ }
1355
+ for (const p of paths) items.push({ type: "localImage", path: p });
1356
+ if (items.length === 0) items.push({ type: "text", text: "", text_elements: [] });
1357
+ const queue = new AsyncQueue();
1358
+ this.queue = queue;
1359
+ this.lastUsage = void 0;
1360
+ try {
1361
+ const result = await this.request("turn/start", {
1362
+ threadId: this.threadId,
1363
+ input: items,
1364
+ ...this.config.options.effort ? { effort: this.config.options.effort } : {}
1365
+ });
1366
+ this.currentTurnId = result["turn"]?.id;
1367
+ yield { type: "session", sessionId: this.threadId };
1368
+ for await (const event of queue) yield event;
1369
+ } finally {
1370
+ cleanup();
1371
+ if (this.queue === queue) this.queue = null;
1372
+ }
1373
+ }
1374
+ async interrupt() {
1375
+ if (!this.threadId || !this.currentTurnId) return;
1376
+ await this.request("turn/interrupt", { threadId: this.threadId, turnId: this.currentTurnId }).catch(() => {
1377
+ });
1378
+ }
1379
+ async close() {
1380
+ if (this.closed) return;
1381
+ this.closed = true;
1382
+ this.fail(new ProviderError("codex", "session closed"));
1383
+ this.child?.kill("SIGTERM");
1384
+ this.child = void 0;
1385
+ }
1386
+ };
1387
+
1388
+ // src/core/providers/jsonl.ts
1389
+ import { spawn as spawn3 } from "child_process";
1390
+ import * as readline3 from "readline";
799
1391
  var ProcessExitError = class extends Error {
800
1392
  constructor(exitCode, stderr) {
801
1393
  super(`process exited with code ${exitCode}${stderr ? `: ${stderr.trim().split("\n").slice(-3).join(" | ")}` : ""}`);
@@ -808,7 +1400,7 @@ var ProcessExitError = class extends Error {
808
1400
  };
809
1401
  function spawnJsonl(options) {
810
1402
  const queue = new AsyncQueue();
811
- const child = spawn2(options.command, options.args, {
1403
+ const child = spawn3(options.command, options.args, {
812
1404
  cwd: options.cwd,
813
1405
  env: options.env,
814
1406
  stdio: [options.stdin !== void 0 ? "pipe" : "ignore", "pipe", "pipe"]
@@ -818,7 +1410,7 @@ function spawnJsonl(options) {
818
1410
  stderr += chunk.toString();
819
1411
  if (stderr.length > 16e3) stderr = stderr.slice(-8e3);
820
1412
  });
821
- const rl = readline2.createInterface({ input: child.stdout, crlfDelay: Infinity });
1413
+ const rl = readline3.createInterface({ input: child.stdout, crlfDelay: Infinity });
822
1414
  rl.on("line", (line) => {
823
1415
  const trimmed = line.trim();
824
1416
  if (!trimmed.startsWith("{")) return;
@@ -933,10 +1525,24 @@ var CodexProvider = class {
933
1525
  if (req.signal?.aborted) return;
934
1526
  if (!done) throw new ProviderError(this.id, "codex exited without completing the turn");
935
1527
  }
1528
+ /**
1529
+ * A live, interactive Codex session (`codex app-server` — the TUI's own
1530
+ * engine): streamed tool events, the user's config.toml verbatim, and
1531
+ * approval requests forwarded to the host's permission handler. The
1532
+ * completion-turn `run()` path above stays for API-style callers.
1533
+ */
1534
+ openSession(options) {
1535
+ return new CodexAgentSession({
1536
+ executable: this.executable,
1537
+ env: this.env,
1538
+ loginCommand: this.loginCommand,
1539
+ options
1540
+ });
1541
+ }
936
1542
  /** Ask the app-server protocol for the model catalog (no tokens spent). */
937
1543
  listModels() {
938
1544
  return new Promise((resolve, reject) => {
939
- const child = spawn3(this.executable, ["app-server"], { env: this.env, stdio: ["pipe", "pipe", "pipe"] });
1545
+ const child = spawn4(this.executable, ["app-server"], { env: this.env, stdio: ["pipe", "pipe", "pipe"] });
940
1546
  let stderr = "";
941
1547
  let settled = false;
942
1548
  const finish = (fn) => {
@@ -954,7 +1560,7 @@ var CodexProvider = class {
954
1560
  "close",
955
1561
  () => finish(() => reject(classifyProviderFailure(this.id, this.loginCommand, new Error(stderr.trim() || "app-server exited"))))
956
1562
  );
957
- const rl = readline3.createInterface({ input: child.stdout });
1563
+ const rl = readline4.createInterface({ input: child.stdout });
958
1564
  rl.on("line", (line) => {
959
1565
  let msg;
960
1566
  try {
@@ -1115,13 +1721,13 @@ function detectProviders(config = {}) {
1115
1721
  const preset = presetFor(id);
1116
1722
  const entry = config[id] ?? {};
1117
1723
  const command = entry.command ?? preset?.command ?? id;
1118
- const path7 = findExecutable(command, entry.path ? { explicit: entry.path } : {});
1724
+ const path8 = findExecutable(command, entry.path ? { explicit: entry.path } : {});
1119
1725
  return {
1120
1726
  id,
1121
1727
  label: entry.label ?? preset?.label ?? id,
1122
1728
  kind: preset?.kind ?? "acp",
1123
- installed: path7 !== void 0 && entry.enabled !== false,
1124
- ...path7 ? { path: path7 } : {},
1729
+ installed: path8 !== void 0 && entry.enabled !== false,
1730
+ ...path8 ? { path: path8 } : {},
1125
1731
  loginCommand: entry.loginCommand ?? preset?.loginCommand ?? `${command} (sign in per its docs)`,
1126
1732
  installHint: preset?.installHint ?? `Install \`${command}\`.`
1127
1733
  };
@@ -1792,6 +2398,266 @@ ${promptText}`;
1792
2398
  }
1793
2399
  };
1794
2400
 
2401
+ // src/core/hostConfig.ts
2402
+ import * as fs7 from "fs";
2403
+ import * as os6 from "os";
2404
+ import * as path7 from "path";
2405
+ function yagamiConfigDir() {
2406
+ return process.env["YAGAMI_CONFIG_DIR"] ?? path7.join(os6.homedir(), ".config", "yagami");
2407
+ }
2408
+ function loadHostEngineConfig() {
2409
+ let file = {};
2410
+ try {
2411
+ file = JSON.parse(fs7.readFileSync(path7.join(yagamiConfigDir(), "config.json"), "utf8"));
2412
+ } catch {
2413
+ }
2414
+ const env = process.env;
2415
+ const defaultProvider = env["YAGAMI_PROVIDER"] ?? (typeof file["defaultProvider"] === "string" ? file["defaultProvider"] : void 0);
2416
+ const defaultModel = env["YAGAMI_DEFAULT_MODEL"] ?? (typeof file["defaultModel"] === "string" ? file["defaultModel"] : void 0);
2417
+ const claudePath = env["YAGAMI_CLAUDE_PATH"] ?? (typeof file["claudePath"] === "string" ? file["claudePath"] : void 0);
2418
+ const claudeConfigDir = typeof file["claudeConfigDir"] === "string" ? file["claudeConfigDir"] : void 0;
2419
+ const providers = file["providers"];
2420
+ return {
2421
+ ...defaultProvider ? { defaultProvider } : {},
2422
+ ...defaultModel ? { defaultModel } : {},
2423
+ ...providers != null && typeof providers === "object" && !Array.isArray(providers) ? { providerConfig: providers } : {},
2424
+ ...claudePath ? { claudePath } : {},
2425
+ ...claudeConfigDir ? { claudeConfigDir } : {}
2426
+ };
2427
+ }
2428
+
2429
+ // src/core/openai.ts
2430
+ var IGNORED_OPENAI_PARAMS = [
2431
+ "presence_penalty",
2432
+ "frequency_penalty",
2433
+ "logit_bias",
2434
+ "logprobs",
2435
+ "top_logprobs",
2436
+ "seed",
2437
+ "user",
2438
+ "response_format",
2439
+ "prediction",
2440
+ "modalities",
2441
+ "audio",
2442
+ "store",
2443
+ "parallel_tool_calls",
2444
+ "web_search_options"
2445
+ ];
2446
+ var EFFORT_MAP = {
2447
+ minimal: "low",
2448
+ low: "low",
2449
+ medium: "medium",
2450
+ high: "high",
2451
+ xhigh: "xhigh",
2452
+ max: "max"
2453
+ };
2454
+ function imagePartToBlock(part) {
2455
+ const url = part["image_url"]?.["url"];
2456
+ if (typeof url !== "string" || url.length === 0) {
2457
+ throw new ApiError(400, "invalid_request_error", "`image_url` parts must carry an `image_url.url` string");
2458
+ }
2459
+ const dataUrl = /^data:([^;,]+);base64,(.+)$/s.exec(url);
2460
+ if (dataUrl) {
2461
+ return { type: "image", source: { type: "base64", media_type: dataUrl[1], data: dataUrl[2] } };
2462
+ }
2463
+ if (/^https?:\/\//.test(url)) {
2464
+ return { type: "image", source: { type: "url", url } };
2465
+ }
2466
+ throw new ApiError(400, "invalid_request_error", "`image_url.url` must be a data: URL or an http(s) URL");
2467
+ }
2468
+ function partsToContent(parts, role) {
2469
+ const blocks = [];
2470
+ for (const part of parts) {
2471
+ if (part?.type === "text" && typeof part["text"] === "string") {
2472
+ blocks.push({ type: "text", text: part["text"] });
2473
+ } else if (part?.type === "image_url" && role === "user") {
2474
+ blocks.push(imagePartToBlock(part));
2475
+ } else {
2476
+ throw new ApiError(
2477
+ 400,
2478
+ "invalid_request_error",
2479
+ `unsupported content part type "${String(part?.type)}" for role "${role}" (yagami supports "text", plus "image_url" in user messages)`
2480
+ );
2481
+ }
2482
+ }
2483
+ return blocks.every((b) => b.type === "text") ? blocks.map((b) => b["text"]).join("\n") : blocks;
2484
+ }
2485
+ function messageText(content, role) {
2486
+ if (content == null) return "";
2487
+ if (typeof content === "string") return content;
2488
+ const flattened = partsToContent(content, role);
2489
+ if (typeof flattened !== "string") {
2490
+ throw new ApiError(400, "invalid_request_error", `"${role}" messages may only contain text parts`);
2491
+ }
2492
+ return flattened;
2493
+ }
2494
+ function chatToMessagesRequest(body) {
2495
+ if (body == null || typeof body !== "object") {
2496
+ throw new ApiError(400, "invalid_request_error", "request body must be a JSON object");
2497
+ }
2498
+ if (body.tools != null || body.tool_choice != null || body.functions != null || body.function_call != null) {
2499
+ throw new ApiError(
2500
+ 400,
2501
+ "invalid_request_error",
2502
+ "yagami does not support `tools`/function calling: the backing engine runs as a pure completions endpoint and never executes or emits tool calls."
2503
+ );
2504
+ }
2505
+ if (body.n != null && body.n !== 1) {
2506
+ throw new ApiError(400, "invalid_request_error", "`n` must be 1 (yagami produces a single completion)");
2507
+ }
2508
+ if (!Array.isArray(body.messages) || body.messages.length === 0) {
2509
+ throw new ApiError(400, "invalid_request_error", "`messages` must be a non-empty array");
2510
+ }
2511
+ const systemParts = [];
2512
+ const messages = [];
2513
+ for (const [i, m] of body.messages.entries()) {
2514
+ const role = m?.role;
2515
+ if (role === "system" || role === "developer") {
2516
+ systemParts.push(messageText(m.content, role));
2517
+ } else if (role === "user") {
2518
+ messages.push({ role: "user", content: Array.isArray(m.content) ? partsToContent(m.content, "user") : m.content ?? "" });
2519
+ } else if (role === "assistant") {
2520
+ messages.push({ role: "assistant", content: messageText(m.content, "assistant") });
2521
+ } else if (role === "tool" || role === "function") {
2522
+ throw new ApiError(400, "invalid_request_error", "yagami does not support tool/function messages (tool calling is disabled by design)");
2523
+ } else {
2524
+ throw new ApiError(400, "invalid_request_error", `messages[${i}].role must be "system", "developer", "user", or "assistant"`);
2525
+ }
2526
+ }
2527
+ const extraIgnored = IGNORED_OPENAI_PARAMS.filter((p) => body[p] != null).map(String);
2528
+ let effort;
2529
+ if (body.reasoning_effort != null) {
2530
+ effort = EFFORT_MAP[String(body.reasoning_effort)];
2531
+ if (!effort) extraIgnored.push("reasoning_effort");
2532
+ }
2533
+ const maxTokens = body.max_completion_tokens ?? body.max_tokens;
2534
+ const system = systemParts.filter((s) => s.length > 0).join("\n\n");
2535
+ const req = {
2536
+ ...body.model !== void 0 ? { model: body.model } : {},
2537
+ messages,
2538
+ ...system.length > 0 ? { system } : {},
2539
+ ...maxTokens !== void 0 ? { max_tokens: maxTokens } : {},
2540
+ ...body.temperature !== void 0 ? { temperature: body.temperature } : {},
2541
+ ...body.top_p !== void 0 ? { top_p: body.top_p } : {},
2542
+ ...body.stop != null ? { stop_sequences: Array.isArray(body.stop) ? body.stop : [body.stop] } : {},
2543
+ ...body.metadata != null ? { metadata: body.metadata } : {},
2544
+ ...body.service_tier != null ? { service_tier: String(body.service_tier) } : {},
2545
+ ...effort ? { effort } : {},
2546
+ ...body.stream === true ? { stream: true } : {}
2547
+ };
2548
+ return { req, extraIgnored, includeUsage: body.stream_options?.include_usage === true };
2549
+ }
2550
+ function finishReason(stopReason) {
2551
+ return stopReason === "max_tokens" ? "length" : "stop";
2552
+ }
2553
+ function toOpenAiUsage(usage) {
2554
+ return {
2555
+ prompt_tokens: usage.input_tokens,
2556
+ completion_tokens: usage.output_tokens,
2557
+ total_tokens: usage.input_tokens + usage.output_tokens
2558
+ };
2559
+ }
2560
+ function toChatCompletion(resp) {
2561
+ const text = resp.content.filter((b) => b.type === "text").map((b) => String(b["text"] ?? "")).join("");
2562
+ const thinking = resp.content.filter((b) => b.type === "thinking").map((b) => String(b["thinking"] ?? "")).join("");
2563
+ return {
2564
+ id: resp.id.replace(/^msg_/, "chatcmpl_"),
2565
+ object: "chat.completion",
2566
+ created: Math.floor(Date.now() / 1e3),
2567
+ model: resp.model,
2568
+ choices: [
2569
+ {
2570
+ index: 0,
2571
+ message: { role: "assistant", content: text, ...thinking ? { reasoning_content: thinking } : {}, refusal: null },
2572
+ finish_reason: finishReason(resp.stop_reason),
2573
+ logprobs: null
2574
+ }
2575
+ ],
2576
+ usage: toOpenAiUsage(resp.usage)
2577
+ };
2578
+ }
2579
+ function openAiErrorBody(err) {
2580
+ return { error: { message: err.message, type: err.type, param: null, code: null } };
2581
+ }
2582
+ var ChatChunkTranslator = class {
2583
+ constructor(includeUsage) {
2584
+ this.includeUsage = includeUsage;
2585
+ }
2586
+ includeUsage;
2587
+ id = "chatcmpl_stream";
2588
+ model = "";
2589
+ created = Math.floor(Date.now() / 1e3);
2590
+ stopReason = null;
2591
+ usage = { input_tokens: 0, output_tokens: 0 };
2592
+ /** Set when the engine reported an error mid-stream (no [DONE] after). */
2593
+ errored = false;
2594
+ chunk(delta, finish = null) {
2595
+ return {
2596
+ id: this.id,
2597
+ object: "chat.completion.chunk",
2598
+ created: this.created,
2599
+ model: this.model,
2600
+ choices: [{ index: 0, delta, finish_reason: finish }]
2601
+ };
2602
+ }
2603
+ /** Translate one engine SSE event into zero or more OpenAI chunk payloads. */
2604
+ push(ev) {
2605
+ const data = ev.data;
2606
+ switch (ev.event) {
2607
+ case "message_start": {
2608
+ const message = data["message"];
2609
+ if (message?.id) this.id = message.id.replace(/^msg_/, "chatcmpl_");
2610
+ if (message?.model) this.model = message.model;
2611
+ return [this.chunk({ role: "assistant", content: "" })];
2612
+ }
2613
+ case "content_block_delta": {
2614
+ const delta = data["delta"];
2615
+ if (delta?.type === "text_delta" && delta.text) return [this.chunk({ content: delta.text })];
2616
+ if (delta?.type === "thinking_delta" && delta.thinking) return [this.chunk({ reasoning_content: delta.thinking })];
2617
+ return [];
2618
+ }
2619
+ case "message_delta": {
2620
+ const delta = data["delta"];
2621
+ if (delta?.stop_reason) this.stopReason = delta.stop_reason;
2622
+ const usage = data["usage"];
2623
+ if (usage) this.usage = usage;
2624
+ return [];
2625
+ }
2626
+ case "message_stop": {
2627
+ const out = [this.chunk({}, finishReason(this.stopReason))];
2628
+ if (this.includeUsage) {
2629
+ out.push({
2630
+ id: this.id,
2631
+ object: "chat.completion.chunk",
2632
+ created: this.created,
2633
+ model: this.model,
2634
+ choices: [],
2635
+ usage: toOpenAiUsage(this.usage)
2636
+ });
2637
+ }
2638
+ return out;
2639
+ }
2640
+ case "error": {
2641
+ this.errored = true;
2642
+ const error = data["error"];
2643
+ return [{ error: { message: error?.message ?? "stream error", type: error?.type ?? "api_error", param: null, code: null } }];
2644
+ }
2645
+ default:
2646
+ return [];
2647
+ }
2648
+ }
2649
+ };
2650
+ function modelListBody(models) {
2651
+ const created = Math.floor(Date.now() / 1e3);
2652
+ return {
2653
+ object: "list",
2654
+ data: models.map((m) => ({ type: "model", object: "model", created, owned_by: "yagami", ...m })),
2655
+ has_more: false,
2656
+ ...models[0] ? { first_id: models[0].id } : {},
2657
+ ...models.length > 0 ? { last_id: models[models.length - 1].id } : {}
2658
+ };
2659
+ }
2660
+
1795
2661
  export {
1796
2662
  ApiError,
1797
2663
  YagamiError,
@@ -1802,6 +2668,7 @@ export {
1802
2668
  toApiError,
1803
2669
  parseModelRef,
1804
2670
  qualifiedModel,
2671
+ isSessionProvider,
1805
2672
  findExecutable,
1806
2673
  resolveExecutable,
1807
2674
  resolveClaudeExecutable,
@@ -1816,6 +2683,13 @@ export {
1816
2683
  loadProviders,
1817
2684
  detectProviders,
1818
2685
  SessionCache,
1819
- YagamiEngine
2686
+ YagamiEngine,
2687
+ yagamiConfigDir,
2688
+ loadHostEngineConfig,
2689
+ chatToMessagesRequest,
2690
+ toChatCompletion,
2691
+ openAiErrorBody,
2692
+ ChatChunkTranslator,
2693
+ modelListBody
1820
2694
  };
1821
- //# sourceMappingURL=chunk-ASS6MJ7C.js.map
2695
+ //# sourceMappingURL=chunk-ZYHC7PXX.js.map