toc-ocra 0.2.5 → 0.2.6

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.
@@ -7,7 +7,7 @@ var __export = (target, all) => {
7
7
 
8
8
  // src/plugin-service.ts
9
9
  import { existsSync as existsSync6 } from "node:fs";
10
- import { delimiter as delimiter5, join as join8 } from "node:path";
10
+ import { delimiter as delimiter5, join as join9 } from "node:path";
11
11
 
12
12
  // src/config.ts
13
13
  import { randomUUID } from "node:crypto";
@@ -15731,7 +15731,8 @@ var CliDetectionService = class {
15731
15731
  var cliDetection = new CliDetectionService();
15732
15732
 
15733
15733
  // src/runtime/agent-runtime.ts
15734
- import { join as join7 } from "node:path";
15734
+ import { randomUUID as randomUUID2 } from "node:crypto";
15735
+ import { join as join8 } from "node:path";
15735
15736
 
15736
15737
  // src/exec/script-runner.ts
15737
15738
  import { spawn as spawn3 } from "node:child_process";
@@ -16454,10 +16455,158 @@ function commandOf(pid) {
16454
16455
 
16455
16456
  // src/runs/run-supervisor.ts
16456
16457
  import { spawn as spawn7 } from "node:child_process";
16457
- import { realpath } from "node:fs/promises";
16458
- import { isAbsolute as isAbsolute2, relative, resolve as resolve3 } from "node:path";
16458
+ import { mkdir as mkdir4, realpath, stat as stat2, writeFile as writeFile4 } from "node:fs/promises";
16459
+ import { basename as basename2, isAbsolute as isAbsolute2, join as join6, relative, resolve as resolve3 } from "node:path";
16459
16460
  import { createInterface as createInterface2 } from "node:readline";
16460
16461
 
16462
+ // src/providers/chat/codex-app-server.ts
16463
+ function codexChatProvider(request, attachments) {
16464
+ let write;
16465
+ let threadId = request.sessionId;
16466
+ let hasStartedTurn = false;
16467
+ const send = (message) => write?.(`${JSON.stringify(message)}
16468
+ `);
16469
+ const options = {
16470
+ cwd: request.workdir,
16471
+ approvalPolicy: "never",
16472
+ sandbox: "danger-full-access",
16473
+ ...request.model ? { model: request.model } : {}
16474
+ };
16475
+ const threadRequest = () => send({
16476
+ id: 2,
16477
+ method: threadId ? "thread/resume" : "thread/start",
16478
+ params: { ...options, ...threadId ? { threadId } : { ephemeral: false } }
16479
+ });
16480
+ const startTurn = () => {
16481
+ if (hasStartedTurn) return;
16482
+ hasStartedTurn = true;
16483
+ const input = [];
16484
+ if (request.prompt) input.push({ type: "text", text: request.prompt });
16485
+ for (const attachment of attachments) {
16486
+ if (attachment.kind === "image") input.push({ type: "localImage", path: attachment.path });
16487
+ else input.push({ type: "text", text: `\u9644\u4EF6\u6587\u4EF6\uFF1A${attachment.name}
16488
+ \u672C\u673A\u8DEF\u5F84\uFF1A${attachment.path}` });
16489
+ }
16490
+ send({ id: 3, method: "turn/start", params: {
16491
+ threadId,
16492
+ input,
16493
+ cwd: request.workdir,
16494
+ approvalPolicy: "never",
16495
+ sandboxPolicy: { type: "dangerFullAccess" },
16496
+ ...request.model ? { model: request.model } : {},
16497
+ ...request.reasoningEffort ? { effort: request.reasoningEffort } : {}
16498
+ } });
16499
+ };
16500
+ const empty = () => ({ display: [] });
16501
+ return {
16502
+ id: "codex",
16503
+ buildLaunch() {
16504
+ return resolveExecutable("codex", ["app-server", "--listen", "stdio://"], providerEnvironment(), process.env.TOC_OCRA_CODEX_PATH);
16505
+ },
16506
+ startInput(writer) {
16507
+ write = writer;
16508
+ send({ id: 1, method: "initialize", params: {
16509
+ clientInfo: { name: "echo_cli_chat", title: "Echo CLI Chat", version: "1.0" },
16510
+ capabilities: { experimentalApi: true }
16511
+ } });
16512
+ },
16513
+ parseLine(line) {
16514
+ const message = tryJson(line);
16515
+ if (!message) return empty();
16516
+ if (message.id !== void 0 && !message.method) {
16517
+ const error51 = message.error;
16518
+ if (error51) return { display: [], turnError: str(error51.message) ?? "Codex \u4F1A\u8BDD\u8BF7\u6C42\u5931\u8D25", endProcess: true };
16519
+ if (message.id === 1) {
16520
+ send({ method: "initialized" });
16521
+ threadRequest();
16522
+ return empty();
16523
+ }
16524
+ if (message.id === 2) {
16525
+ const result = message.result;
16526
+ const thread = result?.thread;
16527
+ const id = str(thread?.id);
16528
+ if (!id) return { display: [], turnError: "Codex \u672A\u8FD4\u56DE\u539F\u751F\u4F1A\u8BDD ID", endProcess: true };
16529
+ threadId = id;
16530
+ startTurn();
16531
+ return { display: [`\u25CF \u4F1A\u8BDD ${id}`], sessionId: id };
16532
+ }
16533
+ return empty();
16534
+ }
16535
+ const params = message.params ?? {};
16536
+ if (params.threadId && params.threadId !== threadId) return empty();
16537
+ if (message.id !== void 0) {
16538
+ send({ id: message.id, error: { code: -32601, message: "\u5F53\u524D\u804A\u5929\u5C1A\u672A\u63A5\u5165\u4EA4\u4E92\u5F0F\u6743\u9650\u6216\u95EE\u7B54" } });
16539
+ return { display: [], turnError: "CLI \u6B63\u5728\u8BF7\u6C42\u4EA4\u4E92\u786E\u8BA4\uFF0C\u5F53\u524D\u804A\u5929\u5C1A\u672A\u63A5\u5165\u6B64\u64CD\u4F5C\u3002", endProcess: true };
16540
+ }
16541
+ const itemId = str(params.itemId) ?? "";
16542
+ switch (message.method) {
16543
+ case "item/agentMessage/delta":
16544
+ case "item/reasoning/textDelta":
16545
+ case "item/reasoning/summaryTextDelta": {
16546
+ const text = typeof params.delta === "string" ? params.delta : "";
16547
+ return { display: [], chatEvents: itemId && text ? [{ event: message.method === "item/agentMessage/delta" ? "text_delta" : "thought_delta", item_id: itemId, text }] : [] };
16548
+ }
16549
+ case "turn/plan/updated": {
16550
+ const plan = Array.isArray(params.plan) ? params.plan : [];
16551
+ return { display: [], chatEvents: [{ event: "item", item: {
16552
+ itemId: "plan",
16553
+ type: "plan",
16554
+ entries: plan.map((entry) => ({ content: String(entry.step ?? ""), status: String(entry.status ?? "pending") }))
16555
+ } }] };
16556
+ }
16557
+ case "item/started":
16558
+ case "item/completed": {
16559
+ const item = params.item ?? {};
16560
+ const id = str(item.id);
16561
+ if (!id || item.type === "userMessage") return empty();
16562
+ const completed = message.method === "item/completed";
16563
+ const status = completed ? item.status === "failed" ? "failed" : "completed" : "in_progress";
16564
+ let normalized;
16565
+ switch (item.type) {
16566
+ case "agentMessage":
16567
+ normalized = { itemId: id, type: "text", text: String(item.text ?? ""), status };
16568
+ break;
16569
+ case "reasoning": {
16570
+ const summary = Array.isArray(item.summary) ? item.summary.map(String).join("\n\n") : "";
16571
+ normalized = { itemId: id, type: "thought", text: summary, status };
16572
+ break;
16573
+ }
16574
+ case "plan":
16575
+ normalized = { itemId: id, type: "text", text: String(item.text ?? ""), status };
16576
+ break;
16577
+ case "commandExecution":
16578
+ normalized = { itemId: id, type: "tool_call", name: "shell", kind: "execute", title: String(item.command ?? "\u6267\u884C\u547D\u4EE4"), text: clip(String(item.aggregatedOutput ?? ""), 64e3), status };
16579
+ break;
16580
+ case "fileChange":
16581
+ normalized = { itemId: id, type: "tool_call", name: "file_change", kind: "edit", title: "\u4FEE\u6539\u6587\u4EF6", text: clip(JSON.stringify(item.changes ?? []), 64e3), status };
16582
+ break;
16583
+ case "mcpToolCall":
16584
+ normalized = { itemId: id, type: "tool_call", name: String(item.tool ?? "MCP"), kind: "call", title: String(item.tool ?? "\u5DE5\u5177\u8C03\u7528"), text: clip(JSON.stringify(item.result ?? item.arguments ?? {}), 64e3), status };
16585
+ break;
16586
+ default:
16587
+ normalized = { itemId: id, type: "tool_call", name: String(item.type ?? "tool"), title: String(item.type ?? "\u6267\u884C\u8FC7\u7A0B"), status };
16588
+ break;
16589
+ }
16590
+ if (completed && normalized.type === "thought" && !normalized.text) return empty();
16591
+ return {
16592
+ display: [],
16593
+ chatEvents: [{ event: "item", item: normalized }],
16594
+ ...completed && item.type === "agentMessage" ? { message: String(item.text ?? "") } : {}
16595
+ };
16596
+ }
16597
+ case "turn/completed": {
16598
+ const turn = params.turn ?? {};
16599
+ const error51 = turn.error ?? {};
16600
+ if (turn.status !== "completed") return { display: [], turnError: str(error51.message) ?? (turn.status === "interrupted" ? "\u6267\u884C\u5DF2\u4E2D\u65AD" : "Codex \u672C\u8F6E\u6267\u884C\u5931\u8D25"), endProcess: true };
16601
+ return { display: ["\u25CF \u672C\u8F6E\u5B8C\u6210"], turnCompleted: true, endProcess: true };
16602
+ }
16603
+ default:
16604
+ return empty();
16605
+ }
16606
+ }
16607
+ };
16608
+ }
16609
+
16461
16610
  // src/session/terminal-line-sanitizer.ts
16462
16611
  var TERMINAL_ESCAPE = /\u001b(?:\][^\u0007]*?(?:\u0007|\u001b\\)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])/g;
16463
16612
  var CONTROL_CHARACTER = /[\u0000-\u0008\u000b-\u001f\u007f]/g;
@@ -16594,6 +16743,7 @@ var RunSupervisor = class {
16594
16743
  recent = /* @__PURE__ */ new Map();
16595
16744
  /** Run ids whose finish report has not been acknowledged yet. */
16596
16745
  unreported = /* @__PURE__ */ new Set();
16746
+ chatRunIds = /* @__PURE__ */ new Set();
16597
16747
  get activeCount() {
16598
16748
  return this.active.size;
16599
16749
  }
@@ -16601,8 +16751,15 @@ var RunSupervisor = class {
16601
16751
  activeRunIds() {
16602
16752
  return [.../* @__PURE__ */ new Set([...this.active.keys(), ...this.unreported])];
16603
16753
  }
16754
+ activeChatRunIds() {
16755
+ return this.activeRunIds().filter((id) => this.chatRunIds.has(id));
16756
+ }
16757
+ activeTaskRunIds() {
16758
+ return this.activeRunIds().filter((id) => !this.chatRunIds.has(id));
16759
+ }
16604
16760
  markReported(runId) {
16605
16761
  this.unreported.delete(runId);
16762
+ this.chatRunIds.delete(runId);
16606
16763
  }
16607
16764
  recentRuns() {
16608
16765
  const running = [...this.active.values()].map((run) => ({ runId: run.runId, workstationId: run.workstationId, status: "running", at: run.startedAt }));
@@ -16618,9 +16775,10 @@ var RunSupervisor = class {
16618
16775
  this.logger.info("run-duplicate-ignored", { runId: payload.run_id });
16619
16776
  return;
16620
16777
  }
16778
+ if (payload.chat_session_id) this.chatRunIds.add(payload.run_id);
16621
16779
  const refuse = async (error51) => {
16622
16780
  this.logger.warn("run-refused", { runId: payload.run_id, error: error51 });
16623
- this.remember({ runId: payload.run_id, workstationId: payload.workstation_id, tail: new TerminalTailBuffer(), finishedAt: Date.now(), status: "failed" });
16781
+ this.remember({ runId: payload.run_id, workstationId: payload.workstation_id, tail: new TerminalTailBuffer(), finishedAt: Date.now(), status: "failed", chat: !!payload.chat_session_id });
16624
16782
  await this.report(payload.run_id, { status: "failed", error: error51, duration_ms: 0 });
16625
16783
  };
16626
16784
  if ([...this.active.values()].some((run2) => run2.workstationId === payload.workstation_id)) {
@@ -16633,10 +16791,30 @@ var RunSupervisor = class {
16633
16791
  let workdir;
16634
16792
  try {
16635
16793
  provider = resolveHeadlessProvider(payload.agent_type);
16636
- workdir = await this.validateWorkdir(payload.workdir || this.config.allowedRoots[0]);
16794
+ workdir = payload.chat_session_id && !payload.workdir ? await this.defaultChatWorkdir() : await this.validateWorkdir(payload.workdir || this.config.allowedRoots[0]);
16637
16795
  } catch (error51) {
16638
16796
  return refuse(errorMessage(error51));
16639
16797
  }
16798
+ const request = {
16799
+ prompt: payload.prompt,
16800
+ sessionId: payload.session_id,
16801
+ model: payload.model,
16802
+ reasoningEffort: payload.reasoning_effort,
16803
+ workdir
16804
+ };
16805
+ if (payload.chat_session_id) {
16806
+ try {
16807
+ if (payload.agent_type !== "codex") throw new Error("\u6B64 CLI \u7684\u5B9E\u65F6\u804A\u5929\u9A71\u52A8\u5C1A\u672A\u542F\u7528");
16808
+ if (!this.reporter.chatEvents) throw new Error("\u5B9E\u65F6\u804A\u5929\u8F6C\u53D1\u672A\u914D\u7F6E");
16809
+ const attachments = await this.prepareChatAttachments(payload);
16810
+ provider = codexChatProvider(request, attachments);
16811
+ } catch (error51) {
16812
+ return refuse(errorMessage(error51));
16813
+ }
16814
+ }
16815
+ if (!payload.prompt && !payload.attachments?.length) return refuse("\u6D88\u606F\u5185\u5BB9\u4E3A\u7A7A");
16816
+ if ([...this.active.values()].some((run2) => run2.workstationId === payload.workstation_id)) return refuse("\u672C\u673A\u8BE5\u5DE5\u4F4D\u5DF2\u6709\u4EFB\u52A1\u5728\u8FD0\u884C");
16817
+ if (this.active.size >= this.config.maxConcurrentRuns) return refuse("\u672C\u673A\u540C\u65F6\u8FD0\u884C\u7684\u4EFB\u52A1\u5DF2\u8FBE\u4E0A\u9650");
16640
16818
  const launch = provider.buildLaunch({
16641
16819
  prompt: payload.prompt,
16642
16820
  sessionId: payload.session_id,
@@ -16652,12 +16830,20 @@ var RunSupervisor = class {
16652
16830
  detached: process.platform !== "win32",
16653
16831
  shell: false,
16654
16832
  windowsHide: true,
16655
- stdio: ["ignore", "pipe", "pipe"]
16833
+ stdio: [provider.startInput ? "pipe" : "ignore", "pipe", "pipe"]
16656
16834
  });
16657
16835
  } catch (error51) {
16658
16836
  return refuse(`\u65E0\u6CD5\u542F\u52A8 ${provider.id}\uFF1A${errorMessage(error51)}`);
16659
16837
  }
16660
16838
  const run = {
16839
+ chat: Boolean(payload.chat_session_id),
16840
+ chatReady: false,
16841
+ eventSequence: 0,
16842
+ pendingEvents: [],
16843
+ eventTail: Promise.resolve(),
16844
+ pendingBytes: 0,
16845
+ queuedBytes: 0,
16846
+ forwardingFailed: false,
16661
16847
  runId: payload.run_id,
16662
16848
  workstationId: payload.workstation_id,
16663
16849
  agentType: provider.id,
@@ -16683,6 +16869,7 @@ var RunSupervisor = class {
16683
16869
  `);
16684
16870
  this.logger.info("run-started", { runId: run.runId, agentType: provider.id, pid: run.pid, resume: Boolean(payload.session_id) });
16685
16871
  let spawnError = "";
16872
+ child.stdin?.on("error", () => void 0);
16686
16873
  child.once("error", (error51) => {
16687
16874
  spawnError = error51.message;
16688
16875
  });
@@ -16696,6 +16883,24 @@ var RunSupervisor = class {
16696
16883
  `);
16697
16884
  });
16698
16885
  child.once("close", (code, signal2) => void this.onExit(run, code, signal2, spawnError));
16886
+ if (provider.startInput) {
16887
+ child.once("spawn", () => {
16888
+ try {
16889
+ provider.startInput?.((message) => {
16890
+ child.stdin?.write(message);
16891
+ });
16892
+ } catch (error51) {
16893
+ run.turnError = errorMessage(error51);
16894
+ void killProcessTree(run.pid);
16895
+ }
16896
+ });
16897
+ run.timers.push(setTimeout(() => {
16898
+ if (!run.chatReady && this.active.has(run.runId)) {
16899
+ run.turnError = "CLI \u5B9E\u65F6\u4F1A\u8BDD\u521D\u59CB\u5316\u8D85\u65F6";
16900
+ void killProcessTree(run.pid);
16901
+ }
16902
+ }, 6e4));
16903
+ }
16699
16904
  const maxMs = this.config.maxRunSeconds * 1e3;
16700
16905
  run.timers.push(setTimeout(() => {
16701
16906
  run.timedOut = true;
@@ -16710,6 +16915,10 @@ var RunSupervisor = class {
16710
16915
  onStdout(run, rawLine) {
16711
16916
  const line = sanitizeTerminalLine(rawLine);
16712
16917
  const parsed = run.provider.parseLine(line);
16918
+ if (run.chat && !run.forwardingFailed) {
16919
+ for (const event of parsed.chatEvents ?? []) this.queueChatEvent(run, event);
16920
+ if (parsed.sessionId) run.chatReady = true;
16921
+ }
16713
16922
  for (const row of parsed.display) run.tail.append(`${row}
16714
16923
  `);
16715
16924
  if (parsed.message !== void 0) run.finalMessage = parsed.message;
@@ -16718,6 +16927,14 @@ var RunSupervisor = class {
16718
16927
  run.sessionId = parsed.sessionId;
16719
16928
  void this.reporter.started(run.runId, { session_id: parsed.sessionId }).catch(() => void 0);
16720
16929
  }
16930
+ if (parsed.endProcess) {
16931
+ run.child.stdin?.end();
16932
+ run.timers.push(setTimeout(() => {
16933
+ if (!this.active.has(run.runId)) return;
16934
+ run.killedAfterCompletion = Boolean(parsed.turnCompleted);
16935
+ void killProcessTree(run.pid);
16936
+ }, 1e3));
16937
+ }
16721
16938
  if ((parsed.turnCompleted || parsed.turnError) && !run.turnCompleted) {
16722
16939
  run.turnCompleted = true;
16723
16940
  run.timers.push(setTimeout(() => {
@@ -16732,6 +16949,9 @@ var RunSupervisor = class {
16732
16949
  for (const timer of run.timers) clearTimeout(timer);
16733
16950
  this.active.delete(run.runId);
16734
16951
  this.orphans?.remove(run.runId);
16952
+ if (run.eventTimer) clearTimeout(run.eventTimer);
16953
+ this.flushChatEvents(run);
16954
+ await run.eventTail;
16735
16955
  const durationMs = Date.now() - run.startedAt;
16736
16956
  let report;
16737
16957
  if (run.cancelRequested) {
@@ -16753,10 +16973,66 @@ var RunSupervisor = class {
16753
16973
  if (run.finalMessage) report.final_message = run.finalMessage;
16754
16974
  run.tail.append(`\u25CF \u8FDB\u7A0B\u5DF2\u9000\u51FA\uFF1A${report.status}${code !== null ? `\uFF08exit ${code}\uFF09` : ""}
16755
16975
  `);
16756
- this.remember({ runId: run.runId, workstationId: run.workstationId, tail: run.tail, finishedAt: Date.now(), status: report.status });
16976
+ this.remember({ runId: run.runId, workstationId: run.workstationId, tail: run.tail, finishedAt: Date.now(), status: report.status, chat: !!run.chat });
16757
16977
  this.logger.info("run-finished", { runId: run.runId, status: report.status, code, durationMs });
16758
16978
  await this.report(run.runId, report);
16759
16979
  }
16980
+ queueChatEvent(run, event) {
16981
+ const bytes = Buffer.byteLength(JSON.stringify(event));
16982
+ if (run.queuedBytes + bytes > 8 * 1024 * 1024) {
16983
+ run.forwardingFailed = true;
16984
+ run.turnError = "\u5B9E\u65F6\u8F93\u51FA\u8F6C\u53D1\u79EF\u538B\uFF0C\u8BF7\u68C0\u67E5\u8BBE\u5907\u7F51\u7EDC";
16985
+ void killProcessTree(run.pid);
16986
+ return;
16987
+ }
16988
+ run.pendingEvents.push({ ...event, sequence: ++run.eventSequence });
16989
+ run.pendingBytes += bytes;
16990
+ run.queuedBytes += bytes;
16991
+ if (run.pendingBytes > 512 * 1024 || run.pendingEvents.length >= 64) this.flushChatEvents(run);
16992
+ else if (!run.eventTimer) run.eventTimer = setTimeout(() => {
16993
+ delete run.eventTimer;
16994
+ this.flushChatEvents(run);
16995
+ }, 60);
16996
+ }
16997
+ flushChatEvents(run) {
16998
+ if (!run.pendingEvents.length) return;
16999
+ if (run.eventTimer) {
17000
+ clearTimeout(run.eventTimer);
17001
+ delete run.eventTimer;
17002
+ }
17003
+ const events = run.pendingEvents.splice(0);
17004
+ const bytes = run.pendingBytes;
17005
+ run.pendingBytes = 0;
17006
+ run.eventTail = run.eventTail.then(async () => {
17007
+ if (run.forwardingFailed) return;
17008
+ await this.reporter.chatEvents?.(run.runId, events);
17009
+ }).catch((error51) => {
17010
+ run.forwardingFailed = true;
17011
+ run.turnError = `\u5B9E\u65F6\u8F93\u51FA\u8F6C\u53D1\u5931\u8D25\uFF1A${errorMessage(error51)}`;
17012
+ this.logger.warn("chat-events-failed", { runId: run.runId, error: errorMessage(error51) });
17013
+ if (this.active.has(run.runId)) void killProcessTree(run.pid);
17014
+ }).finally(() => {
17015
+ run.queuedBytes -= bytes;
17016
+ });
17017
+ }
17018
+ async prepareChatAttachments(payload) {
17019
+ if (!payload.attachments?.length) return [];
17020
+ if (!this.reporter.chatMedia) throw new Error("\u9644\u4EF6\u8F6C\u53D1\u672A\u914D\u7F6E");
17021
+ const directory = join6(this.config.dataDir, "chat-media", encodeURIComponent(payload.chat_session_id ?? payload.run_id));
17022
+ await mkdir4(directory, { recursive: true, mode: 448 });
17023
+ const files = [];
17024
+ for (const attachment of payload.attachments) {
17025
+ const name = basename2(attachment.name).replace(/[^\p{L}\p{N}._-]/gu, "_").slice(-120) || "attachment";
17026
+ const path = join6(directory, `${encodeURIComponent(attachment.media_id)}-${name}`);
17027
+ const existing = await stat2(path).catch(() => void 0);
17028
+ if (!existing?.isFile()) {
17029
+ const data = await this.reporter.chatMedia(payload.run_id, attachment.media_id);
17030
+ await writeFile4(path, data, { mode: 384 });
17031
+ }
17032
+ files.push({ path, name: attachment.name, kind: attachment.kind });
17033
+ }
17034
+ return files;
17035
+ }
16760
17036
  async report(runId, report) {
16761
17037
  this.unreported.add(runId);
16762
17038
  await this.reporter.finished(runId, report);
@@ -16799,6 +17075,15 @@ var RunSupervisor = class {
16799
17075
  if (item.finishedAt < cutoff || this.recent.size > MAX_RECENT_RUNS) this.recent.delete(id);
16800
17076
  }
16801
17077
  }
17078
+ async defaultChatWorkdir() {
17079
+ for (const root of this.config.allowedRoots) {
17080
+ try {
17081
+ return await this.validateWorkdir(root);
17082
+ } catch {
17083
+ }
17084
+ }
17085
+ throw new Error("\u6CA1\u6709\u53EF\u7528\u7684\u804A\u5929\u5DE5\u4F5C\u76EE\u5F55\uFF0C\u8BF7\u914D\u7F6E\u4ECD\u5B58\u5728\u7684\u5141\u8BB8\u76EE\u5F55");
17086
+ }
16802
17087
  async validateWorkdir(input) {
16803
17088
  if (!input) throw new Error("\u6CA1\u6709\u914D\u7F6E\u5141\u8BB8\u7684\u5DE5\u4F5C\u76EE\u5F55");
16804
17089
  let candidate;
@@ -16873,9 +17158,31 @@ var GatewayClient = class {
16873
17158
  const data = await this.request("GET", `/commands?wait=${waitSeconds}`, void 0, (waitSeconds + 15) * 1e3, signal2);
16874
17159
  return data.items ?? [];
16875
17160
  }
17161
+ chatHeartbeat(runnerId, providers, activeRuns) {
17162
+ return this.request("POST", "/chat-heartbeat", { runner_id: runnerId, providers, active_runs: activeRuns }, 15e3);
17163
+ }
17164
+ async pollChat(waitSeconds, runnerId, signal2) {
17165
+ const data = await this.request("GET", `/chat-commands?wait=${waitSeconds}&runner_id=${encodeURIComponent(runnerId)}`, void 0, (waitSeconds + 15) * 1e3, signal2);
17166
+ return data.items ?? [];
17167
+ }
16876
17168
  commandResult(commandId, body) {
16877
17169
  return this.request("POST", `/commands/${encodeURIComponent(commandId)}/result`, body, 15e3);
16878
17170
  }
17171
+ chatEvents(runId, events) {
17172
+ return this.request("POST", `/runs/${encodeURIComponent(runId)}/chat-events`, { events }, 15e3);
17173
+ }
17174
+ async chatMedia(runId, mediaId) {
17175
+ const response = await fetch(`${this.config.serverUrl}/api/agent/runs/${encodeURIComponent(runId)}/chat-media/${encodeURIComponent(mediaId)}`, {
17176
+ headers: { authorization: `Bearer ${this.config.token}` },
17177
+ signal: AbortSignal.timeout(3e4)
17178
+ });
17179
+ if (!response.ok) throw new GatewayError("\u65E0\u6CD5\u8BFB\u53D6\u804A\u5929\u9644\u4EF6", response.status);
17180
+ const declared = Number(response.headers.get("content-length") ?? 0);
17181
+ if (declared > 10 * 1024 * 1024) throw new GatewayError("\u804A\u5929\u9644\u4EF6\u8D85\u8FC7 10 MB", 413);
17182
+ const bytes = new Uint8Array(await response.arrayBuffer());
17183
+ if (bytes.length > 10 * 1024 * 1024) throw new GatewayError("\u804A\u5929\u9644\u4EF6\u8D85\u8FC7 10 MB", 413);
17184
+ return bytes;
17185
+ }
16879
17186
  runStarted(runId, body) {
16880
17187
  return this.request("POST", `/runs/${encodeURIComponent(runId)}/started`, body, 15e3);
16881
17188
  }
@@ -16885,7 +17192,7 @@ var GatewayClient = class {
16885
17192
  };
16886
17193
 
16887
17194
  // src/workspace/directory-browser.ts
16888
- import { readdir, realpath as realpath2, stat as stat2 } from "node:fs/promises";
17195
+ import { readdir, realpath as realpath2, stat as stat3 } from "node:fs/promises";
16889
17196
  import { homedir as homedir5 } from "node:os";
16890
17197
  import { dirname as dirname6, parse as parse3, resolve as resolve4 } from "node:path";
16891
17198
  var MAX_ENTRIES = 1e3;
@@ -16894,7 +17201,7 @@ async function listDirectory(requested, allowedRoots) {
16894
17201
  const directory = await realpath2(resolve4(raw)).catch(() => {
16895
17202
  throw new Error(`\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${requested}`);
16896
17203
  });
16897
- if (!(await stat2(directory)).isDirectory()) throw new Error("\u8FD9\u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55");
17204
+ if (!(await stat3(directory)).isDirectory()) throw new Error("\u8FD9\u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55");
16898
17205
  const entries = await readdir(directory, { withFileTypes: true }).catch((error51) => {
16899
17206
  throw new Error(error51.code === "EACCES" || error51.code === "EPERM" ? "\u6CA1\u6709\u6743\u9650\u8BFB\u53D6\u8FD9\u4E2A\u76EE\u5F55" : error51.message);
16900
17207
  });
@@ -16923,9 +17230,9 @@ async function listDirectory(requested, allowedRoots) {
16923
17230
 
16924
17231
  // src/workspace/workspace-manager.ts
16925
17232
  import { spawn as spawn8 } from "node:child_process";
16926
- import { chmod as chmod3, mkdir as mkdir4, mkdtemp, opendir, realpath as realpath3, rm as rm2, stat as stat3, writeFile as writeFile4 } from "node:fs/promises";
17233
+ import { chmod as chmod3, mkdir as mkdir5, mkdtemp, opendir, realpath as realpath3, rm as rm2, stat as stat4, writeFile as writeFile5 } from "node:fs/promises";
16927
17234
  import { homedir as homedir6, tmpdir } from "node:os";
16928
- import { isAbsolute as isAbsolute3, join as join6, relative as relative2, resolve as resolve5 } from "node:path";
17235
+ import { isAbsolute as isAbsolute3, join as join7, relative as relative2, resolve as resolve5 } from "node:path";
16929
17236
  var MAX_NODES = 2e3;
16930
17237
  var MAX_DIRECTORY_ENTRIES = 12e3;
16931
17238
  var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
@@ -16979,7 +17286,7 @@ var WorkspaceManager = class {
16979
17286
  return root;
16980
17287
  }
16981
17288
  async syncTree(rootPath, request) {
16982
- await mkdir4(rootPath, { recursive: true });
17289
+ await mkdir5(rootPath, { recursive: true });
16983
17290
  await addAllowedRoot(this.config, rootPath);
16984
17291
  const results = [];
16985
17292
  const requestedPaths = /* @__PURE__ */ new Set();
@@ -16989,7 +17296,7 @@ var WorkspaceManager = class {
16989
17296
  requestedPaths.add(relativePath);
16990
17297
  const target = this.resolveWithinRoot(rootPath, relativePath);
16991
17298
  try {
16992
- const metadata = await stat3(target).catch(() => void 0);
17299
+ const metadata = await stat4(target).catch(() => void 0);
16993
17300
  if (metadata) {
16994
17301
  if (!metadata.isDirectory()) {
16995
17302
  throw new Error("The existing workspace path is not a directory");
@@ -17003,7 +17310,7 @@ var WorkspaceManager = class {
17003
17310
  });
17004
17311
  continue;
17005
17312
  }
17006
- await mkdir4(target, { recursive: true });
17313
+ await mkdir5(target, { recursive: true });
17007
17314
  results.push({
17008
17315
  relative_path: relativePath,
17009
17316
  status: "created",
@@ -17047,12 +17354,12 @@ var WorkspaceManager = class {
17047
17354
  let created = false;
17048
17355
  let metadata;
17049
17356
  try {
17050
- metadata = await stat3(target);
17357
+ metadata = await stat4(target);
17051
17358
  } catch {
17052
17359
  }
17053
17360
  if (!metadata) {
17054
17361
  if (!gitUrl) throw new Error("A missing repository needs a Git remote URL");
17055
- await mkdir4(resolve5(target, ".."), { recursive: true });
17362
+ await mkdir5(resolve5(target, ".."), { recursive: true });
17056
17363
  const cloneArgs = branch ? ["clone", "--branch", branch, "--single-branch", gitUrl, target] : ["clone", gitUrl, target];
17057
17364
  await this.runGit(cloneArgs, rootPath, timeoutSeconds, false, gitEnvironment);
17058
17365
  created = true;
@@ -17147,7 +17454,7 @@ var WorkspaceManager = class {
17147
17454
  }
17148
17455
  async scanTree(rootPath, maxDepth, scanRoots) {
17149
17456
  const resolvedRoot = await realpath3(rootPath);
17150
- const rootMetadata = await stat3(resolvedRoot);
17457
+ const rootMetadata = await stat4(resolvedRoot);
17151
17458
  if (!rootMetadata.isDirectory()) throw new Error("The workspace root is not a directory");
17152
17459
  const nodes = [];
17153
17460
  let entriesRead = 0;
@@ -17202,7 +17509,7 @@ var WorkspaceManager = class {
17202
17509
  const target = this.resolveWithinRoot(resolvedRoot, relativePath);
17203
17510
  let metadata;
17204
17511
  try {
17205
- metadata = await stat3(target);
17512
+ metadata = await stat4(target);
17206
17513
  } catch (error51) {
17207
17514
  this.logger.warn("workspace-scan-root-missing", {
17208
17515
  rootPath: target,
@@ -17299,13 +17606,13 @@ var WorkspaceManager = class {
17299
17606
  }
17300
17607
  async createGitAuthEnvironment(auth) {
17301
17608
  if (!auth) return void 0;
17302
- const directory = await mkdtemp(join6(tmpdir(), "toc-ocra-git-auth-"));
17609
+ const directory = await mkdtemp(join7(tmpdir(), "toc-ocra-git-auth-"));
17303
17610
  const isWindows = process.platform === "win32";
17304
- const scriptPath = join6(directory, isWindows ? "askpass.cmd" : "askpass.sh");
17611
+ const scriptPath = join7(directory, isWindows ? "askpass.cmd" : "askpass.sh");
17305
17612
  let script;
17306
17613
  if (isWindows) {
17307
- const helperPath = join6(directory, "askpass.cjs");
17308
- await writeFile4(
17614
+ const helperPath = join7(directory, "askpass.cjs");
17615
+ await writeFile5(
17309
17616
  helperPath,
17310
17617
  'const prompt = process.argv.slice(2).join(" ");\nprocess.stdout.write(/username/i.test(prompt) ? process.env.ECHO_GIT_USERNAME ?? "" : process.env.ECHO_GIT_PASSWORD ?? "");\n',
17311
17618
  { mode: 448 }
@@ -17314,7 +17621,7 @@ var WorkspaceManager = class {
17314
17621
  } else {
17315
17622
  script = '#!/bin/sh\ncase "$1" in *Username*|*username*) printf "%s\\n" "$ECHO_GIT_USERNAME" ;; *) printf "%s\\n" "$ECHO_GIT_PASSWORD" ;; esac\n';
17316
17623
  }
17317
- await writeFile4(scriptPath, script, { mode: 448 });
17624
+ await writeFile5(scriptPath, script, { mode: 448 });
17318
17625
  if (!isWindows) await chmod3(scriptPath, 448);
17319
17626
  return {
17320
17627
  env: {
@@ -17347,7 +17654,9 @@ var RunStartPayloadSchema = external_exports.object({
17347
17654
  agent_type: external_exports.string().min(1).max(40),
17348
17655
  task_chain_id: external_exports.string().optional().default(""),
17349
17656
  message_id: external_exports.string().optional().default(""),
17350
- prompt: external_exports.string().min(1).max(512 * 1024),
17657
+ prompt: external_exports.string().max(512 * 1024),
17658
+ chat_session_id: external_exports.string().max(64).optional(),
17659
+ attachments: external_exports.array(external_exports.object({ media_id: external_exports.string().min(1).max(64), name: external_exports.string().max(200), kind: external_exports.enum(["image", "file"]) })).max(16).optional(),
17351
17660
  session_id: external_exports.string().max(200).optional().default(""),
17352
17661
  model: external_exports.string().max(100).optional().default(""),
17353
17662
  reasoning_effort: external_exports.string().max(40).optional().default(""),
@@ -17402,9 +17711,10 @@ var sleep2 = (ms, signal2) => new Promise((resolve7) => {
17402
17711
  }, { once: true });
17403
17712
  });
17404
17713
  var AgentRuntime = class {
17405
- constructor(config2, version2, logger) {
17714
+ constructor(config2, version2, logger, chatOnly = false) {
17406
17715
  this.config = config2;
17407
17716
  this.version = version2;
17717
+ this.chatOnly = chatOnly;
17408
17718
  this.ownsLogger = !logger;
17409
17719
  this.logger = logger ?? createDiagnosticLogger(config2.logFile, { level: config2.logLevel, maxAgeMs: 5 * 60 * 1e3 });
17410
17720
  this.client = new GatewayClient(config2);
@@ -17413,17 +17723,22 @@ var AgentRuntime = class {
17413
17723
  started: async (runId, report) => {
17414
17724
  await this.retry(() => this.client.runStarted(runId, report), 3);
17415
17725
  },
17726
+ chatEvents: async (runId, events) => {
17727
+ await this.retry(() => this.client.chatEvents(runId, events), 5);
17728
+ },
17729
+ chatMedia: (runId, mediaId) => this.client.chatMedia(runId, mediaId),
17416
17730
  finished: async (runId, report) => {
17417
17731
  this.pendingReports.set(runId, report);
17418
17732
  await this.flushReport(runId);
17419
17733
  }
17420
17734
  };
17421
17735
  this.workspaces = new WorkspaceManager(config2, this.logger);
17422
- this.orphans = new OrphanRegistry(join7(config2.dataDir, "active-runs.json"));
17736
+ this.orphans = new OrphanRegistry(join8(config2.dataDir, chatOnly ? "chat-active-runs.json" : "active-runs.json"));
17423
17737
  this.runs = new RunSupervisor(config2, this.logger, reporter, this.orphans);
17424
17738
  }
17425
17739
  config;
17426
17740
  version;
17741
+ chatOnly;
17427
17742
  logger;
17428
17743
  client;
17429
17744
  runs;
@@ -17431,6 +17746,10 @@ var AgentRuntime = class {
17431
17746
  workspaces;
17432
17747
  stopController = new AbortController();
17433
17748
  startedAt = (/* @__PURE__ */ new Date()).toISOString();
17749
+ chatRunnerId = randomUUID2();
17750
+ chatReady = false;
17751
+ chatLeaseInitialized = false;
17752
+ chatError;
17434
17753
  detection;
17435
17754
  policy = DEFAULT_POLICY;
17436
17755
  heartbeatInterval = 30;
@@ -17450,13 +17769,14 @@ var AgentRuntime = class {
17450
17769
  /** Hash of the catalogs the server has stored (from its heartbeat reply). */
17451
17770
  serverCatalogHash;
17452
17771
  async start() {
17453
- const reaped = await this.orphans.reapPrevious();
17772
+ const reaped = this.chatOnly ? [] : await this.orphans.reapPrevious();
17454
17773
  if (reaped.length > 0) this.logger.warn("orphan-runs-stopped", { runIds: reaped.map((r) => r.runId) });
17455
17774
  this.detection = await cliDetection.detect();
17456
17775
  await this.refreshCatalogs();
17457
17776
  await this.heartbeat();
17458
17777
  void this.heartbeatLoop();
17459
- void this.pollLoop();
17778
+ if (!this.chatOnly) void this.pollLoop();
17779
+ void this.pollLoop(true);
17460
17780
  this.logger.info("agent-started", { serverUrl: this.config.serverUrl, deviceId: this.config.deviceId });
17461
17781
  process.stdout.write(`toc-ocra ${this.version} \u5DF2\u542F\u52A8\uFF1A${this.config.serverUrl}\uFF08\u8BBE\u5907 ${this.deviceName}\uFF09
17462
17782
  `);
@@ -17503,12 +17823,12 @@ var AgentRuntime = class {
17503
17823
  for (const runId of [...this.pendingReports.keys()]) await this.flushReport(runId, 1);
17504
17824
  const report = this.catalogReport;
17505
17825
  const sendCatalogs = report !== void 0 && report.hash !== this.serverCatalogHash;
17506
- try {
17826
+ if (!this.chatOnly) try {
17507
17827
  const response = await this.client.heartbeat({
17508
17828
  agent_version: this.version,
17509
17829
  hostname: this.config.hostname,
17510
17830
  capabilities: this.capabilitySummary(),
17511
- active_runs: this.runs.activeRunIds(),
17831
+ active_runs: this.runs.activeTaskRunIds(),
17512
17832
  ...sendCatalogs ? { model_catalogs: report } : {}
17513
17833
  });
17514
17834
  this.serverCatalogHash = response.model_catalog_hash ?? (sendCatalogs ? report.hash : this.serverCatalogHash);
@@ -17522,6 +17842,27 @@ var AgentRuntime = class {
17522
17842
  } catch (error51) {
17523
17843
  this.noteFailure("heartbeat", error51);
17524
17844
  }
17845
+ try {
17846
+ await this.client.chatHeartbeat(this.chatRunnerId, this.detection?.detectedIds.includes("codex") ? ["codex"] : [], this.runs.activeChatRunIds());
17847
+ if (!this.chatLeaseInitialized && this.chatOnly) {
17848
+ const reaped = await this.orphans.reapPrevious();
17849
+ if (reaped.length) this.logger.warn("chat-orphans-stopped", { runIds: reaped.map((run) => run.runId) });
17850
+ }
17851
+ this.chatLeaseInitialized = true;
17852
+ if (!this.chatReady) this.logger.info("chat-channel-ready");
17853
+ this.chatReady = true;
17854
+ this.chatError = void 0;
17855
+ if (this.chatOnly) {
17856
+ this.lastHeartbeatOk = true;
17857
+ this.heartbeatInterval = 20;
17858
+ }
17859
+ } catch (error51) {
17860
+ this.chatReady = false;
17861
+ const reason = error51 instanceof Error ? error51.message : String(error51);
17862
+ if (this.chatError !== reason) this.logger.warn("chat-channel-unavailable", { error: reason });
17863
+ this.chatError = reason;
17864
+ if (this.chatOnly) this.noteFailure("chat-heartbeat", error51);
17865
+ }
17525
17866
  void this.refreshCatalogs();
17526
17867
  }
17527
17868
  /** Re-reads catalogs whose sources changed; concurrent calls share one run. */
@@ -17545,6 +17886,7 @@ var AgentRuntime = class {
17545
17886
  if (!snapshot) return { providers: [] };
17546
17887
  return {
17547
17888
  providers: snapshot.providers.map((p) => ({ id: p.id, installed: p.installed, runnable: p.available, version: p.version ?? "", path: p.resolvedPath ?? "" })),
17889
+ cli_chat: { version: 1, providers: ["codex"] },
17548
17890
  detected_ids: snapshot.detectedIds,
17549
17891
  refreshed_at: snapshot.refreshedAt
17550
17892
  };
@@ -17561,16 +17903,23 @@ var AgentRuntime = class {
17561
17903
  this.lastError = { reason, at: (/* @__PURE__ */ new Date()).toISOString() };
17562
17904
  }
17563
17905
  // ---- Commands ----
17564
- async pollLoop() {
17906
+ async pollLoop(chat = false) {
17565
17907
  let backoff = 1e3;
17566
17908
  while (!this.stopController.signal.aborted) {
17567
17909
  try {
17568
- const commands = await this.client.poll(this.pollWait, this.stopController.signal);
17910
+ if (chat && !this.chatReady) {
17911
+ await sleep2(1e3, this.stopController.signal);
17912
+ continue;
17913
+ }
17914
+ const commands = chat ? await this.client.pollChat(this.pollWait, this.chatRunnerId, this.stopController.signal) : await this.client.poll(this.pollWait, this.stopController.signal);
17569
17915
  backoff = 1e3;
17570
17916
  for (const command of commands) void this.handle(command);
17571
17917
  } catch (error51) {
17572
17918
  if (this.stopController.signal.aborted) return;
17573
- this.noteFailure("poll", error51);
17919
+ if (chat) {
17920
+ this.chatReady = false;
17921
+ this.logger.warn("chat-poll-failed", { error: error51 instanceof Error ? error51.message : String(error51) });
17922
+ } else this.noteFailure("poll", error51);
17574
17923
  const unauthorized = error51 instanceof GatewayError && error51.unauthorized;
17575
17924
  await sleep2(unauthorized ? 6e4 : backoff, this.stopController.signal);
17576
17925
  backoff = Math.min(backoff * 2, 3e4);
@@ -17591,11 +17940,13 @@ var AgentRuntime = class {
17591
17940
  }
17592
17941
  async execute(command) {
17593
17942
  switch (command.type) {
17943
+ case "chat.run.start":
17594
17944
  case "run.start": {
17595
17945
  const payload = RunStartPayloadSchema.parse(command.payload);
17596
17946
  await this.runs.start(payload);
17597
17947
  return { success: true, result: { accepted: true } };
17598
17948
  }
17949
+ case "chat.run.cancel":
17599
17950
  case "run.cancel": {
17600
17951
  const payload = RunCancelPayloadSchema.parse(command.payload);
17601
17952
  const cancelled = await this.runs.cancel(payload.run_id);
@@ -17923,7 +18274,7 @@ function preferSystemNode() {
17923
18274
  const names = process.platform === "win32" ? ["node.exe"] : ["node"];
17924
18275
  for (const directory of (process.env.PATH ?? "").split(delimiter5)) {
17925
18276
  for (const name of names) {
17926
- const candidate = directory && join8(directory, name);
18277
+ const candidate = directory && join9(directory, name);
17927
18278
  if (candidate && existsSync6(candidate)) {
17928
18279
  process.env.TOC_OCRA_NODE_PATH = candidate;
17929
18280
  return;