toc-ocra 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,6 +73,14 @@ node dist/src/index.js run
73
73
  - **服务端对账兜底**:心跳会上报本机正在运行的任务,服务端据此释放本机已经不存在的运行;Agent 崩溃重启时,会清理上次遗留的 CLI 进程。
74
74
  - **加一个 CLI**:在 `src/providers/cli/` 新增一个文件,实现 `HeadlessProvider`(启动参数 + 解析每一行输出),再在 `src/providers/registry.ts` 加一行。
75
75
 
76
+ ### 看板附件
77
+
78
+ 任务链附件在 CLI 启动前由本机代理下载到本次实际工作目录的 `.echo/attachments/message-{message_id}/`,完成大小与 SHA-256 校验后,提示词中的附件引用会替换为本机绝对路径。下载目录必须属于已允许的 CLI 工作目录,不能通过符号链接跳出;未配置设备默认目录或工位工作目录时会拒绝附件任务。每条消息最多 16 个附件,准备过程可取消、可复用已校验文件,最长等待 15 分钟。
79
+
80
+ 窗口工位使用同机代理的 `run.files.prepare` 先下载文件,然后由服务端转发路径;同机设备代理必须在线。更新后的代理通过心跳声明 `task_attachments.version=1`,服务端会拒绝把附件任务发给不支持此协议的旧代理。请更新并重启设备代理与对应后端后使用。
81
+
82
+ 下载文件暂时保留,可手工清理;`.echo` 不进入设备目录扫描,并带有仅作用于该内部目录的 Git 忽略规则。视频也可下载为文件,实际解析取决于 CLI 的能力。
83
+
76
84
  详见 [docs/architecture.md](docs/architecture.md)。
77
85
 
78
86
  ## 开发
@@ -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 join9 } from "node:path";
10
+ import { delimiter as delimiter5, join as join10 } from "node:path";
11
11
 
12
12
  // src/config.ts
13
13
  import { randomUUID } from "node:crypto";
@@ -15731,8 +15731,8 @@ var CliDetectionService = class {
15731
15731
  var cliDetection = new CliDetectionService();
15732
15732
 
15733
15733
  // src/runtime/agent-runtime.ts
15734
- import { randomUUID as randomUUID2 } from "node:crypto";
15735
- import { join as join8 } from "node:path";
15734
+ import { randomUUID as randomUUID3 } from "node:crypto";
15735
+ import { join as join9 } from "node:path";
15736
15736
 
15737
15737
  // src/exec/script-runner.ts
15738
15738
  import { spawn as spawn3 } from "node:child_process";
@@ -16717,8 +16717,8 @@ function commandOf(pid) {
16717
16717
 
16718
16718
  // src/runs/run-supervisor.ts
16719
16719
  import { spawn as spawn7 } from "node:child_process";
16720
- import { mkdir as mkdir4, realpath, stat as stat2, writeFile as writeFile4 } from "node:fs/promises";
16721
- import { basename as basename2, isAbsolute as isAbsolute2, join as join6, relative, resolve as resolve3 } from "node:path";
16720
+ import { mkdir as mkdir5, realpath as realpath2, stat as stat2, writeFile as writeFile5 } from "node:fs/promises";
16721
+ import { basename as basename3, isAbsolute as isAbsolute3, join as join7, relative as relative2, resolve as resolve3 } from "node:path";
16722
16722
  import { createInterface as createInterface2 } from "node:readline";
16723
16723
 
16724
16724
  // src/session/terminal-line-sanitizer.ts
@@ -16837,6 +16837,88 @@ function clipToCharacterBudget(rows, budget) {
16837
16837
  return { lines: rows, limited: false };
16838
16838
  }
16839
16839
 
16840
+ // src/runs/task-attachments.ts
16841
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
16842
+ import { constants as constants2 } from "node:fs";
16843
+ import { lstat, mkdir as mkdir4, open, realpath, rename as rename4, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
16844
+ import { basename as basename2, isAbsolute as isAbsolute2, join as join6, relative } from "node:path";
16845
+ function within(root, path) {
16846
+ const segment = relative(root, path);
16847
+ return segment === "" || !segment.startsWith("..") && !isAbsolute2(segment);
16848
+ }
16849
+ async function attachmentDirectory(workdir, messageId) {
16850
+ let directory = workdir;
16851
+ for (const segment of [".echo", "attachments", `message-${encodeURIComponent(messageId).slice(0, 180)}`]) {
16852
+ directory = join6(directory, segment);
16853
+ await mkdir4(directory, { mode: 448 }).catch((error51) => {
16854
+ if (error51.code !== "EEXIST") throw error51;
16855
+ });
16856
+ const metadata = await lstat(directory);
16857
+ if (metadata.isSymbolicLink() || !metadata.isDirectory() || !within(workdir, await realpath(directory))) {
16858
+ throw new Error("\u9644\u4EF6\u76EE\u5F55\u5FC5\u987B\u4F4D\u4E8E CLI \u5DE5\u4F5C\u76EE\u5F55\u5185\uFF0C\u4E14\u4E0D\u80FD\u662F\u7B26\u53F7\u94FE\u63A5");
16859
+ }
16860
+ if (segment === ".echo") await writeFile4(join6(directory, ".gitignore"), "*\n", { flag: "wx", mode: 384 }).catch((error51) => {
16861
+ if (error51.code !== "EEXIST") throw error51;
16862
+ });
16863
+ }
16864
+ return directory;
16865
+ }
16866
+ async function matches(path, size, sha256, signal2) {
16867
+ const metadata = await lstat(path).catch(() => void 0);
16868
+ if (!metadata?.isFile() || metadata.isSymbolicLink() || metadata.size !== size) return false;
16869
+ const file2 = await open(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
16870
+ try {
16871
+ const hash2 = createHash2("sha256");
16872
+ let read = 0;
16873
+ for await (const chunk of file2.createReadStream({ autoClose: false })) {
16874
+ signal2.throwIfAborted();
16875
+ read += chunk.length;
16876
+ if (read > size) return false;
16877
+ hash2.update(chunk);
16878
+ }
16879
+ return hash2.digest("hex") === sha256;
16880
+ } finally {
16881
+ await file2.close();
16882
+ }
16883
+ }
16884
+ async function prepareTaskAttachments(payload, workdir, download, signal2) {
16885
+ const files = payload.task_attachments ?? [];
16886
+ if (!files.length) return payload.prompt;
16887
+ const directory = await attachmentDirectory(workdir, payload.message_id || payload.run_id);
16888
+ let prompt = payload.prompt;
16889
+ const paths = [];
16890
+ for (const file2 of files) {
16891
+ signal2.throwIfAborted();
16892
+ const token = `{{echo-file:${file2.file_id}}}`;
16893
+ if (file2.prompt_token !== token) throw new Error("\u9644\u4EF6\u8DEF\u5F84\u5360\u4F4D\u7B26\u65E0\u6548\uFF0C\u8BF7\u66F4\u65B0\u670D\u52A1\u5668");
16894
+ const characters = Array.from(basename2(file2.name.replace(/\\/g, "/")).replace(/[^\p{L}\p{N}._-]/gu, "_").replace(/\.+$/u, ""));
16895
+ while (Buffer.byteLength(characters.join("")) > 160) characters.shift();
16896
+ const name = characters.join("") || "attachment";
16897
+ const path = join6(directory, `${file2.file_id}-${name}`);
16898
+ if (!await matches(path, file2.size, file2.sha256, signal2)) {
16899
+ const temporary = join6(directory, `.download-${file2.file_id}-${randomUUID2()}.part`);
16900
+ try {
16901
+ await download(payload.run_id, file2.file_id, temporary, file2.size, file2.sha256, signal2);
16902
+ signal2.throwIfAborted();
16903
+ if (!within(workdir, await realpath(directory))) throw new Error("\u9644\u4EF6\u76EE\u5F55\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u8BD5");
16904
+ await rename4(temporary, path);
16905
+ } finally {
16906
+ await rm2(temporary, { force: true });
16907
+ }
16908
+ }
16909
+ if (!within(workdir, await realpath(path))) throw new Error("\u9644\u4EF6\u8DEF\u5F84\u4E0D\u5728 CLI \u5DE5\u4F5C\u76EE\u5F55\u5185");
16910
+ prompt = prompt.split(token).join(path);
16911
+ paths.push(`- ${JSON.stringify(path)}\uFF08${file2.mime}\uFF0C${file2.size} \u5B57\u8282\uFF09`);
16912
+ }
16913
+ if (prompt.includes("{{echo-file:")) throw new Error("\u9644\u4EF6\u8DEF\u5F84\u672A\u5B8C\u6574\u89E3\u6790");
16914
+ prompt += `
16915
+
16916
+ \u9644\u4EF6\u5DF2\u7531\u8BBE\u5907\u4EE3\u7406\u4E0B\u8F7D\u5230\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u3002\u8BF7\u76F4\u63A5\u8BFB\u53D6\u4EE5\u4E0B\u672C\u673A\u6587\u4EF6\uFF0C\u65E0\u9700\u4E0B\u8F7D\uFF1A
16917
+ ${paths.join("\n")}`;
16918
+ if (Buffer.byteLength(prompt) > 512 * 1024) throw new Error("\u5305\u542B\u9644\u4EF6\u8DEF\u5F84\u7684\u6D88\u606F\u8FC7\u957F");
16919
+ return prompt;
16920
+ }
16921
+
16840
16922
  // src/runs/run-supervisor.ts
16841
16923
  var COMPLETION_EXIT_GRACE_MS = 15e3;
16842
16924
  var RECENT_RUN_TTL_MS = 10 * 6e4;
@@ -16858,12 +16940,13 @@ var RunSupervisor = class {
16858
16940
  /** Run ids whose finish report has not been acknowledged yet. */
16859
16941
  unreported = /* @__PURE__ */ new Set();
16860
16942
  chatRunIds = /* @__PURE__ */ new Set();
16943
+ preparing = /* @__PURE__ */ new Map();
16861
16944
  get activeCount() {
16862
- return this.active.size;
16945
+ return this.active.size + this.preparing.size;
16863
16946
  }
16864
16947
  /** Runs to list in the heartbeat: still running, or finished but not yet reported. */
16865
16948
  activeRunIds() {
16866
- return [.../* @__PURE__ */ new Set([...this.active.keys(), ...this.unreported])];
16949
+ return [.../* @__PURE__ */ new Set([...this.active.keys(), ...this.preparing.keys(), ...this.unreported])];
16867
16950
  }
16868
16951
  activeChatRunIds() {
16869
16952
  return this.activeRunIds().filter((id) => this.chatRunIds.has(id));
@@ -16885,6 +16968,16 @@ var RunSupervisor = class {
16885
16968
  * directory, unknown CLI) are reported as a failed run, never thrown.
16886
16969
  */
16887
16970
  async start(payload) {
16971
+ if (this.preparing.has(payload.run_id)) return;
16972
+ const controller = new AbortController();
16973
+ this.preparing.set(payload.run_id, { workstationId: payload.workstation_id, controller });
16974
+ try {
16975
+ await this.startPrepared(payload, controller.signal);
16976
+ } finally {
16977
+ this.preparing.delete(payload.run_id);
16978
+ }
16979
+ }
16980
+ async startPrepared(payload, signal2) {
16888
16981
  if (this.active.has(payload.run_id) || this.recent.has(payload.run_id)) {
16889
16982
  this.logger.info("run-duplicate-ignored", { runId: payload.run_id });
16890
16983
  return;
@@ -16895,20 +16988,32 @@ var RunSupervisor = class {
16895
16988
  this.remember({ runId: payload.run_id, workstationId: payload.workstation_id, tail: new TerminalTailBuffer(), finishedAt: Date.now(), status: "failed", chat: !!payload.chat_session_id });
16896
16989
  await this.report(payload.run_id, { status: "failed", error: error51, duration_ms: 0 });
16897
16990
  };
16898
- if ([...this.active.values()].some((run2) => run2.workstationId === payload.workstation_id)) {
16991
+ if ([...this.active.values()].some((run2) => run2.workstationId === payload.workstation_id) || [...this.preparing].some(([id, run2]) => id !== payload.run_id && run2.workstationId === payload.workstation_id)) {
16899
16992
  return refuse("\u672C\u673A\u8BE5\u5DE5\u4F4D\u5DF2\u6709\u4EFB\u52A1\u5728\u8FD0\u884C");
16900
16993
  }
16901
- if (this.active.size >= this.config.maxConcurrentRuns) {
16994
+ if (this.active.size + this.preparing.size > this.config.maxConcurrentRuns) {
16902
16995
  return refuse(`\u672C\u673A\u540C\u65F6\u8FD0\u884C\u7684\u4EFB\u52A1\u5DF2\u8FBE\u4E0A\u9650\uFF08${this.config.maxConcurrentRuns}\uFF09`);
16903
16996
  }
16904
16997
  let provider;
16905
16998
  let workdir;
16906
16999
  try {
16907
17000
  provider = resolveHeadlessProvider(payload.agent_type);
17001
+ if (payload.task_attachments?.length && !payload.workdir) throw new Error("\u9644\u4EF6\u9700\u8981\u914D\u7F6E\u8BBE\u5907\u9ED8\u8BA4\u76EE\u5F55\u6216\u5DE5\u4F4D\u5DE5\u4F5C\u76EE\u5F55");
16908
17002
  workdir = payload.chat_session_id && !payload.workdir ? await this.defaultChatWorkdir() : await this.validateWorkdir(payload.workdir || this.config.allowedRoots[0]);
16909
17003
  } catch (error51) {
16910
17004
  return refuse(errorMessage(error51));
16911
17005
  }
17006
+ try {
17007
+ if (payload.task_attachments?.length) payload = { ...payload, prompt: await this.prepareTaskFiles(payload, signal2, workdir) };
17008
+ signal2.throwIfAborted();
17009
+ } catch (error51) {
17010
+ if (signal2.aborted) {
17011
+ this.remember({ runId: payload.run_id, workstationId: payload.workstation_id, tail: new TerminalTailBuffer(), finishedAt: Date.now(), status: "cancelled", chat: !!payload.chat_session_id });
17012
+ await this.report(payload.run_id, { status: "cancelled", error: "\u9644\u4EF6\u51C6\u5907\u5DF2\u53D6\u6D88", duration_ms: 0 });
17013
+ return;
17014
+ }
17015
+ return refuse(`\u9644\u4EF6\u4E0B\u8F7D\u5931\u8D25\uFF1A${errorMessage(error51)}`);
17016
+ }
16912
17017
  const request = {
16913
17018
  prompt: payload.prompt,
16914
17019
  sessionId: payload.session_id,
@@ -16927,7 +17032,7 @@ var RunSupervisor = class {
16927
17032
  }
16928
17033
  if (!payload.prompt && !payload.attachments?.length) return refuse("\u6D88\u606F\u5185\u5BB9\u4E3A\u7A7A");
16929
17034
  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");
16930
- if (this.active.size >= this.config.maxConcurrentRuns) return refuse("\u672C\u673A\u540C\u65F6\u8FD0\u884C\u7684\u4EFB\u52A1\u5DF2\u8FBE\u4E0A\u9650");
17035
+ if (this.active.size + this.preparing.size > this.config.maxConcurrentRuns) return refuse("\u672C\u673A\u540C\u65F6\u8FD0\u884C\u7684\u4EFB\u52A1\u5DF2\u8FBE\u4E0A\u9650");
16931
17036
  const launch = provider.buildLaunch({
16932
17037
  prompt: payload.prompt,
16933
17038
  sessionId: payload.session_id,
@@ -16976,6 +17081,7 @@ var RunSupervisor = class {
16976
17081
  timers: []
16977
17082
  };
16978
17083
  this.active.set(run.runId, run);
17084
+ this.preparing.delete(run.runId);
16979
17085
  this.unreported.add(run.runId);
16980
17086
  if (run.pid > 1) this.orphans?.add({ runId: run.runId, pid: run.pid, executable: launch.executable, startedAt: run.startedAt });
16981
17087
  run.tail.append(`\u25CF ${provider.id} \u542F\u52A8\u4E8E ${workdir}${payload.session_id ? `\uFF08\u7EE7\u7EED\u4F1A\u8BDD ${payload.session_id}\uFF09` : ""}
@@ -16995,7 +17101,7 @@ var RunSupervisor = class {
16995
17101
  run.tail.append(`${clean}
16996
17102
  `);
16997
17103
  });
16998
- child.once("close", (code, signal2) => void this.onExit(run, code, signal2, spawnError));
17104
+ child.once("close", (code, signal3) => void this.onExit(run, code, signal3, spawnError));
16999
17105
  if (provider.startInput) {
17000
17106
  child.once("spawn", () => {
17001
17107
  try {
@@ -17026,16 +17132,15 @@ var RunSupervisor = class {
17026
17132
  });
17027
17133
  }
17028
17134
  onStdout(run, rawLine) {
17029
- const line = sanitizeTerminalLine(rawLine);
17030
- const parsed = run.provider.parseLine(line);
17135
+ const parsed = run.provider.parseLine(rawLine);
17031
17136
  if (run.chat && !run.forwardingFailed) {
17032
- for (const event of parsed.chatEvents ?? []) this.queueChatEvent(run, event);
17137
+ for (const event of parsed.chatEvents ?? []) this.queueChatEvent(run, sanitizeChatEvent(event));
17033
17138
  if (parsed.sessionId) run.chatReady = true;
17034
17139
  }
17035
- for (const row of parsed.display) run.tail.append(`${row}
17140
+ for (const row of parsed.display) run.tail.append(`${sanitizeTerminalLine(row)}
17036
17141
  `);
17037
- if (parsed.message !== void 0) run.finalMessage = parsed.message;
17038
- if (parsed.turnError) run.turnError = parsed.turnError;
17142
+ if (parsed.message !== void 0) run.finalMessage = sanitizeTerminalLine(parsed.message);
17143
+ if (parsed.turnError) run.turnError = sanitizeTerminalLine(parsed.turnError);
17039
17144
  if (parsed.sessionId && parsed.sessionId !== run.sessionId) {
17040
17145
  run.sessionId = parsed.sessionId;
17041
17146
  void this.reporter.started(run.runId, { session_id: parsed.sessionId }).catch(() => void 0);
@@ -17131,16 +17236,16 @@ var RunSupervisor = class {
17131
17236
  async prepareChatAttachments(payload) {
17132
17237
  if (!payload.attachments?.length) return [];
17133
17238
  if (!this.reporter.chatMedia) throw new Error("\u9644\u4EF6\u8F6C\u53D1\u672A\u914D\u7F6E");
17134
- const directory = join6(this.config.dataDir, "chat-media", encodeURIComponent(payload.chat_session_id ?? payload.run_id));
17135
- await mkdir4(directory, { recursive: true, mode: 448 });
17239
+ const directory = join7(this.config.dataDir, "chat-media", encodeURIComponent(payload.chat_session_id ?? payload.run_id));
17240
+ await mkdir5(directory, { recursive: true, mode: 448 });
17136
17241
  const files = [];
17137
17242
  for (const attachment of payload.attachments) {
17138
- const name = basename2(attachment.name).replace(/[^\p{L}\p{N}._-]/gu, "_").slice(-120) || "attachment";
17139
- const path = join6(directory, `${encodeURIComponent(attachment.media_id)}-${name}`);
17243
+ const name = basename3(attachment.name).replace(/[^\p{L}\p{N}._-]/gu, "_").slice(-120) || "attachment";
17244
+ const path = join7(directory, `${encodeURIComponent(attachment.media_id)}-${name}`);
17140
17245
  const existing = await stat2(path).catch(() => void 0);
17141
17246
  if (!existing?.isFile()) {
17142
17247
  const data = await this.reporter.chatMedia(payload.run_id, attachment.media_id);
17143
- await writeFile4(path, data, { mode: 384 });
17248
+ await writeFile5(path, data, { mode: 384 });
17144
17249
  }
17145
17250
  files.push({ path, name: attachment.name, kind: attachment.kind });
17146
17251
  }
@@ -17153,7 +17258,14 @@ var RunSupervisor = class {
17153
17258
  /** Stops a run: the whole process tree, then a `cancelled` report. */
17154
17259
  async cancel(runId) {
17155
17260
  const run = this.active.get(runId);
17156
- if (!run) return false;
17261
+ if (!run) {
17262
+ const preparing = this.preparing.get(runId);
17263
+ if (preparing) {
17264
+ preparing.controller.abort();
17265
+ return true;
17266
+ }
17267
+ return false;
17268
+ }
17157
17269
  run.cancelRequested = true;
17158
17270
  run.tail.append("\u25CF \u6536\u5230\u505C\u6B62\u6307\u4EE4\uFF0C\u6B63\u5728\u7ED3\u675F\u8FDB\u7A0B\u6811\n");
17159
17271
  this.logger.info("run-cancel", { runId, pid: run.pid });
@@ -17176,6 +17288,7 @@ var RunSupervisor = class {
17176
17288
  return { ...read, status: this.active.has(target.runId) ? "running" : "finished", run_id: target.runId };
17177
17289
  }
17178
17290
  async disposeAll() {
17291
+ for (const entry of this.preparing.values()) entry.controller.abort();
17179
17292
  await Promise.all([...this.active.values()].map((run) => {
17180
17293
  run.cancelRequested = true;
17181
17294
  return killProcessTree(run.pid, 1e3);
@@ -17201,23 +17314,47 @@ var RunSupervisor = class {
17201
17314
  if (!input) throw new Error("\u6CA1\u6709\u914D\u7F6E\u5141\u8BB8\u7684\u5DE5\u4F5C\u76EE\u5F55");
17202
17315
  let candidate;
17203
17316
  try {
17204
- candidate = await realpath(isAbsolute2(input) ? input : resolve3(input));
17317
+ candidate = await realpath2(isAbsolute3(input) ? input : resolve3(input));
17205
17318
  } catch {
17206
17319
  throw new Error(`\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${input}`);
17207
17320
  }
17208
17321
  for (const root of this.config.allowedRoots) {
17209
- const allowed = await realpath(root).catch(() => resolve3(root));
17210
- const segment = relative(allowed, candidate);
17211
- if (segment === "" || !segment.startsWith("..") && !isAbsolute2(segment)) return candidate;
17322
+ const allowed = await realpath2(root).catch(() => resolve3(root));
17323
+ const segment = relative2(allowed, candidate);
17324
+ if (segment === "" || !segment.startsWith("..") && !isAbsolute3(segment)) return candidate;
17212
17325
  }
17213
17326
  throw new Error(`\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5728\u5141\u8BB8\u8303\u56F4\u5185\uFF1A${input}`);
17214
17327
  }
17328
+ async prepareTaskFiles(payload, signal2 = AbortSignal.timeout(15 * 6e4), workdir) {
17329
+ if (!payload.workdir) throw new Error("\u8BF7\u5148\u914D\u7F6E\u8BBE\u5907\u9ED8\u8BA4\u76EE\u5F55\u6216\u5DE5\u4F4D\u5DE5\u4F5C\u76EE\u5F55");
17330
+ if (!this.reporter.taskFile) throw new Error("\u4EFB\u52A1\u9644\u4EF6\u4E0B\u8F7D\u672A\u914D\u7F6E");
17331
+ return prepareTaskAttachments(payload, workdir ?? await this.validateWorkdir(payload.workdir), this.reporter.taskFile, AbortSignal.any([signal2, AbortSignal.timeout(15 * 6e4)]));
17332
+ }
17215
17333
  };
17216
17334
  function errorMessage(error51) {
17217
17335
  return error51 instanceof Error ? error51.message : String(error51);
17218
17336
  }
17337
+ function sanitizeChatEvent(event) {
17338
+ const sanitize = (value) => {
17339
+ if (typeof value === "string") return sanitizeTerminalLine(value);
17340
+ if (Array.isArray(value)) return value.map(sanitize);
17341
+ if (value && typeof value === "object") {
17342
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitize(item)]));
17343
+ }
17344
+ return value;
17345
+ };
17346
+ return {
17347
+ ...event,
17348
+ ...event.text !== void 0 ? { text: sanitizeTerminalLine(event.text) } : {},
17349
+ ...event.item ? { item: sanitize(event.item) } : {}
17350
+ };
17351
+ }
17219
17352
 
17220
17353
  // src/transport/gateway-client.ts
17354
+ import { createHash as createHash3 } from "node:crypto";
17355
+ import { createWriteStream } from "node:fs";
17356
+ import { Readable, Transform } from "node:stream";
17357
+ import { pipeline } from "node:stream/promises";
17221
17358
  var GatewayError = class extends Error {
17222
17359
  constructor(message, status) {
17223
17360
  super(message);
@@ -17299,19 +17436,46 @@ var GatewayClient = class {
17299
17436
  runStarted(runId, body) {
17300
17437
  return this.request("POST", `/runs/${encodeURIComponent(runId)}/started`, body, 15e3);
17301
17438
  }
17439
+ async taskFile(runId, fileId, destination, size, sha256, signal2) {
17440
+ const response = await fetch(`${this.config.serverUrl}/api/agent/runs/${encodeURIComponent(runId)}/files/${encodeURIComponent(fileId)}`, {
17441
+ headers: { authorization: `Bearer ${this.config.token}` },
17442
+ redirect: "error",
17443
+ signal: AbortSignal.any([signal2, AbortSignal.timeout(15 * 6e4)])
17444
+ });
17445
+ if (!response.ok || !response.body) throw new GatewayError("\u65E0\u6CD5\u8BFB\u53D6\u4EFB\u52A1\u9644\u4EF6\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u662F\u5426\u5DF2\u5220\u9664\u53CA\u8BBE\u5907\u51ED\u636E", response.status);
17446
+ const declared = response.headers.get("content-length");
17447
+ if (declared !== null && Number(declared) !== size) {
17448
+ await response.body.cancel();
17449
+ throw new Error("\u9644\u4EF6\u5927\u5C0F\u4E0E\u4FDD\u5B58\u7684\u8BB0\u5F55\u4E0D\u4E00\u81F4");
17450
+ }
17451
+ const hash2 = createHash3("sha256");
17452
+ let received = 0;
17453
+ const verifier = new Transform({ transform(chunk, _encoding, callback) {
17454
+ received += chunk.length;
17455
+ if (received > size) {
17456
+ callback(new Error("\u9644\u4EF6\u4E0B\u8F7D\u8D85\u51FA\u8BB0\u5F55\u5927\u5C0F"));
17457
+ return;
17458
+ }
17459
+ hash2.update(chunk);
17460
+ callback(null, chunk);
17461
+ } });
17462
+ const target = createWriteStream(destination, { flags: "wx", mode: 384 });
17463
+ await pipeline(Readable.fromWeb(response.body), verifier, target, { signal: signal2 });
17464
+ if (received !== size || hash2.digest("hex") !== sha256) throw new Error("\u9644\u4EF6\u4E0B\u8F7D\u4E0D\u5B8C\u6574\u6216\u6821\u9A8C\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
17465
+ }
17302
17466
  runFinished(runId, body) {
17303
17467
  return this.request("POST", `/runs/${encodeURIComponent(runId)}/finished`, body, 15e3);
17304
17468
  }
17305
17469
  };
17306
17470
 
17307
17471
  // src/workspace/directory-browser.ts
17308
- import { readdir, realpath as realpath2, stat as stat3 } from "node:fs/promises";
17472
+ import { readdir, realpath as realpath3, stat as stat3 } from "node:fs/promises";
17309
17473
  import { homedir as homedir5 } from "node:os";
17310
17474
  import { dirname as dirname6, parse as parse3, resolve as resolve4 } from "node:path";
17311
17475
  var MAX_ENTRIES = 1e3;
17312
17476
  async function listDirectory(requested, allowedRoots) {
17313
17477
  const raw = requested.trim().replace(/^~(?=$|[\\/])/, homedir5()) || homedir5();
17314
- const directory = await realpath2(resolve4(raw)).catch(() => {
17478
+ const directory = await realpath3(resolve4(raw)).catch(() => {
17315
17479
  throw new Error(`\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${requested}`);
17316
17480
  });
17317
17481
  if (!(await stat3(directory)).isDirectory()) throw new Error("\u8FD9\u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55");
@@ -17343,12 +17507,13 @@ async function listDirectory(requested, allowedRoots) {
17343
17507
 
17344
17508
  // src/workspace/workspace-manager.ts
17345
17509
  import { spawn as spawn8 } from "node:child_process";
17346
- 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";
17510
+ import { chmod as chmod3, mkdir as mkdir6, mkdtemp, opendir, realpath as realpath4, rm as rm3, stat as stat4, writeFile as writeFile6 } from "node:fs/promises";
17347
17511
  import { homedir as homedir6, tmpdir } from "node:os";
17348
- import { isAbsolute as isAbsolute3, join as join7, relative as relative2, resolve as resolve5 } from "node:path";
17512
+ import { isAbsolute as isAbsolute4, join as join8, relative as relative3, resolve as resolve5 } from "node:path";
17349
17513
  var MAX_NODES = 2e3;
17350
17514
  var MAX_DIRECTORY_ENTRIES = 12e3;
17351
17515
  var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
17516
+ ".echo",
17352
17517
  ".git",
17353
17518
  "node_modules",
17354
17519
  ".next",
@@ -17394,12 +17559,12 @@ var WorkspaceManager = class {
17394
17559
  validateRootPath(value) {
17395
17560
  const raw = value.trim().replace(/^~(?=$|[\\/])/, homedir6());
17396
17561
  const root = resolve5(raw);
17397
- if (!isAbsolute3(raw)) throw new Error("\u6839\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
17398
- if (relative2(root, resolve5(root, "..")) === "") throw new Error("\u4E0D\u80FD\u628A\u6587\u4EF6\u7CFB\u7EDF\u6839\u76EE\u5F55\u4F5C\u4E3A\u76EE\u5F55\u6839");
17562
+ if (!isAbsolute4(raw)) throw new Error("\u6839\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
17563
+ if (relative3(root, resolve5(root, "..")) === "") throw new Error("\u4E0D\u80FD\u628A\u6587\u4EF6\u7CFB\u7EDF\u6839\u76EE\u5F55\u4F5C\u4E3A\u76EE\u5F55\u6839");
17399
17564
  return root;
17400
17565
  }
17401
17566
  async syncTree(rootPath, request) {
17402
- await mkdir5(rootPath, { recursive: true });
17567
+ await mkdir6(rootPath, { recursive: true });
17403
17568
  await addAllowedRoot(this.config, rootPath);
17404
17569
  const results = [];
17405
17570
  const requestedPaths = /* @__PURE__ */ new Set();
@@ -17423,7 +17588,7 @@ var WorkspaceManager = class {
17423
17588
  });
17424
17589
  continue;
17425
17590
  }
17426
- await mkdir5(target, { recursive: true });
17591
+ await mkdir6(target, { recursive: true });
17427
17592
  results.push({
17428
17593
  relative_path: relativePath,
17429
17594
  status: "created",
@@ -17472,7 +17637,7 @@ var WorkspaceManager = class {
17472
17637
  }
17473
17638
  if (!metadata) {
17474
17639
  if (!gitUrl) throw new Error("A missing repository needs a Git remote URL");
17475
- await mkdir5(resolve5(target, ".."), { recursive: true });
17640
+ await mkdir6(resolve5(target, ".."), { recursive: true });
17476
17641
  const cloneArgs = branch ? ["clone", "--branch", branch, "--single-branch", gitUrl, target] : ["clone", gitUrl, target];
17477
17642
  await this.runGit(cloneArgs, rootPath, timeoutSeconds, false, gitEnvironment);
17478
17643
  created = true;
@@ -17566,13 +17731,13 @@ var WorkspaceManager = class {
17566
17731
  };
17567
17732
  }
17568
17733
  async scanTree(rootPath, maxDepth, scanRoots) {
17569
- const resolvedRoot = await realpath3(rootPath);
17734
+ const resolvedRoot = await realpath4(rootPath);
17570
17735
  const rootMetadata = await stat4(resolvedRoot);
17571
17736
  if (!rootMetadata.isDirectory()) throw new Error("The workspace root is not a directory");
17572
17737
  const nodes = [];
17573
17738
  let entriesRead = 0;
17574
17739
  const appendNode = async (target) => {
17575
- const relativePath = relative2(resolvedRoot, target).replaceAll("\\", "/");
17740
+ const relativePath = relative3(resolvedRoot, target).replaceAll("\\", "/");
17576
17741
  if (!relativePath || !this.isSafeRelativePath(relativePath)) return void 0;
17577
17742
  const git = await this.readGitMetadata(target);
17578
17743
  if (git) {
@@ -17656,8 +17821,8 @@ var WorkspaceManager = class {
17656
17821
  if (result.exitCode !== 0 || !topLevel) return false;
17657
17822
  try {
17658
17823
  const [resolvedDirectory, resolvedTopLevel] = await Promise.all([
17659
- realpath3(directory),
17660
- realpath3(topLevel)
17824
+ realpath4(directory),
17825
+ realpath4(topLevel)
17661
17826
  ]);
17662
17827
  return resolvedDirectory === resolvedTopLevel;
17663
17828
  } catch {
@@ -17677,8 +17842,8 @@ var WorkspaceManager = class {
17677
17842
  }
17678
17843
  resolveWithinRoot(rootPath, relativePath) {
17679
17844
  const target = resolve5(rootPath, relativePath);
17680
- const relativeToRoot = relative2(rootPath, target);
17681
- if (relativeToRoot.startsWith("..") || isAbsolute3(relativeToRoot)) {
17845
+ const relativeToRoot = relative3(rootPath, target);
17846
+ if (relativeToRoot.startsWith("..") || isAbsolute4(relativeToRoot)) {
17682
17847
  throw new Error("A workspace path must remain under the configured workspace root");
17683
17848
  }
17684
17849
  return target;
@@ -17719,13 +17884,13 @@ var WorkspaceManager = class {
17719
17884
  }
17720
17885
  async createGitAuthEnvironment(auth) {
17721
17886
  if (!auth) return void 0;
17722
- const directory = await mkdtemp(join7(tmpdir(), "toc-ocra-git-auth-"));
17887
+ const directory = await mkdtemp(join8(tmpdir(), "toc-ocra-git-auth-"));
17723
17888
  const isWindows = process.platform === "win32";
17724
- const scriptPath = join7(directory, isWindows ? "askpass.cmd" : "askpass.sh");
17889
+ const scriptPath = join8(directory, isWindows ? "askpass.cmd" : "askpass.sh");
17725
17890
  let script;
17726
17891
  if (isWindows) {
17727
- const helperPath = join7(directory, "askpass.cjs");
17728
- await writeFile5(
17892
+ const helperPath = join8(directory, "askpass.cjs");
17893
+ await writeFile6(
17729
17894
  helperPath,
17730
17895
  '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',
17731
17896
  { mode: 448 }
@@ -17734,7 +17899,7 @@ var WorkspaceManager = class {
17734
17899
  } else {
17735
17900
  script = '#!/bin/sh\ncase "$1" in *Username*|*username*) printf "%s\\n" "$ECHO_GIT_USERNAME" ;; *) printf "%s\\n" "$ECHO_GIT_PASSWORD" ;; esac\n';
17736
17901
  }
17737
- await writeFile5(scriptPath, script, { mode: 448 });
17902
+ await writeFile6(scriptPath, script, { mode: 448 });
17738
17903
  if (!isWindows) await chmod3(scriptPath, 448);
17739
17904
  return {
17740
17905
  env: {
@@ -17745,7 +17910,7 @@ var WorkspaceManager = class {
17745
17910
  ECHO_GIT_USERNAME: auth.username,
17746
17911
  ECHO_GIT_PASSWORD: auth.password
17747
17912
  },
17748
- cleanup: () => rm2(directory, { recursive: true, force: true })
17913
+ cleanup: () => rm3(directory, { recursive: true, force: true })
17749
17914
  };
17750
17915
  }
17751
17916
  message(error51) {
@@ -17770,6 +17935,7 @@ var RunStartPayloadSchema = external_exports.object({
17770
17935
  prompt: external_exports.string().max(512 * 1024),
17771
17936
  chat_session_id: external_exports.string().max(64).optional(),
17772
17937
  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(),
17938
+ task_attachments: external_exports.array(external_exports.object({ file_id: external_exports.uuid(), name: external_exports.string().min(1).max(255), mime: external_exports.string().max(255), size: external_exports.number().int().positive(), sha256: external_exports.string().regex(/^[a-f0-9]{64}$/), prompt_token: external_exports.string().max(100) })).max(16).nullish(),
17773
17939
  session_id: external_exports.string().max(200).optional().default(""),
17774
17940
  model: external_exports.string().max(100).optional().default(""),
17775
17941
  reasoning_effort: external_exports.string().max(40).optional().default(""),
@@ -17840,13 +18006,14 @@ var AgentRuntime = class {
17840
18006
  await this.retry(() => this.client.chatEvents(runId, events), 5);
17841
18007
  },
17842
18008
  chatMedia: (runId, mediaId) => this.client.chatMedia(runId, mediaId),
18009
+ taskFile: (runId, fileId, destination, size, sha256, signal2) => this.client.taskFile(runId, fileId, destination, size, sha256, signal2),
17843
18010
  finished: async (runId, report) => {
17844
18011
  this.pendingReports.set(runId, report);
17845
18012
  await this.flushReport(runId);
17846
18013
  }
17847
18014
  };
17848
18015
  this.workspaces = new WorkspaceManager(config2, this.logger);
17849
- this.orphans = new OrphanRegistry(join8(config2.dataDir, chatOnly ? "chat-active-runs.json" : "active-runs.json"));
18016
+ this.orphans = new OrphanRegistry(join9(config2.dataDir, chatOnly ? "chat-active-runs.json" : "active-runs.json"));
17850
18017
  this.runs = new RunSupervisor(config2, this.logger, reporter, this.orphans);
17851
18018
  }
17852
18019
  config;
@@ -17859,7 +18026,7 @@ var AgentRuntime = class {
17859
18026
  workspaces;
17860
18027
  stopController = new AbortController();
17861
18028
  startedAt = (/* @__PURE__ */ new Date()).toISOString();
17862
- chatRunnerId = randomUUID2();
18029
+ chatRunnerId = randomUUID3();
17863
18030
  chatReady = false;
17864
18031
  chatLeaseInitialized = false;
17865
18032
  chatError;
@@ -18000,6 +18167,7 @@ var AgentRuntime = class {
18000
18167
  return {
18001
18168
  providers: snapshot.providers.map((p) => ({ id: p.id, installed: p.installed, runnable: p.available, version: p.version ?? "", path: p.resolvedPath ?? "" })),
18002
18169
  cli_chat: { version: 1, providers: CHAT_PROVIDERS.filter((id) => snapshot.detectedIds.includes(id)) },
18170
+ task_attachments: { version: 1 },
18003
18171
  detected_ids: snapshot.detectedIds,
18004
18172
  refreshed_at: snapshot.refreshedAt
18005
18173
  };
@@ -18053,6 +18221,11 @@ var AgentRuntime = class {
18053
18221
  }
18054
18222
  async execute(command) {
18055
18223
  switch (command.type) {
18224
+ case "run.files.prepare": {
18225
+ const payload = RunStartPayloadSchema.parse(command.payload);
18226
+ const prompt = await this.runs.prepareTaskFiles(payload);
18227
+ return { success: true, result: { prompt } };
18228
+ }
18056
18229
  case "chat.run.start":
18057
18230
  case "run.start": {
18058
18231
  const payload = RunStartPayloadSchema.parse(command.payload);
@@ -18387,7 +18560,7 @@ function preferSystemNode() {
18387
18560
  const names = process.platform === "win32" ? ["node.exe"] : ["node"];
18388
18561
  for (const directory of (process.env.PATH ?? "").split(delimiter5)) {
18389
18562
  for (const name of names) {
18390
- const candidate = directory && join9(directory, name);
18563
+ const candidate = directory && join10(directory, name);
18391
18564
  if (candidate && existsSync6(candidate)) {
18392
18565
  process.env.TOC_OCRA_NODE_PATH = candidate;
18393
18566
  return;
@@ -4,6 +4,7 @@ import type { ChatContentEvent } from '../providers/provider.js';
4
4
  import { type TerminalTailRead } from '../session/terminal-tail-buffer.js';
5
5
  import type { RunFinishedReport, RunStartPayload, RunStartedReport } from '../types/protocol.js';
6
6
  import { OrphanRegistry } from './orphan-registry.js';
7
+ import { type TaskFileDownload } from './task-attachments.js';
7
8
  /**
8
9
  * Supervises headless CLI turns. One process per turn:
9
10
  *
@@ -17,6 +18,7 @@ import { OrphanRegistry } from './orphan-registry.js';
17
18
  */
18
19
  export declare const COMPLETION_EXIT_GRACE_MS = 15000;
19
20
  export type RunReporter = {
21
+ taskFile?: TaskFileDownload;
20
22
  chatEvents?(runId: string, events: Array<ChatContentEvent & {
21
23
  sequence: number;
22
24
  }>): Promise<void>;
@@ -38,6 +40,7 @@ export declare class RunSupervisor {
38
40
  /** Run ids whose finish report has not been acknowledged yet. */
39
41
  private readonly unreported;
40
42
  private readonly chatRunIds;
43
+ private readonly preparing;
41
44
  constructor(config: AgentConfig, logger: DiagnosticLogger, reporter: RunReporter, orphans?: OrphanRegistry | undefined);
42
45
  get activeCount(): number;
43
46
  /** Runs to list in the heartbeat: still running, or finished but not yet reported. */
@@ -56,6 +59,7 @@ export declare class RunSupervisor {
56
59
  * directory, unknown CLI) are reported as a failed run, never thrown.
57
60
  */
58
61
  start(payload: RunStartPayload): Promise<void>;
62
+ private startPrepared;
59
63
  private onStdout;
60
64
  private onExit;
61
65
  private queueChatEvent;
@@ -72,4 +76,5 @@ export declare class RunSupervisor {
72
76
  private remember;
73
77
  private defaultChatWorkdir;
74
78
  validateWorkdir(input: string | undefined): Promise<string>;
79
+ prepareTaskFiles(payload: RunStartPayload, signal?: AbortSignal, workdir?: string): Promise<string>;
75
80
  }