agentlas 0.4.0 → 0.5.5

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.
Files changed (41) hide show
  1. package/README.md +112 -23
  2. package/bin/agentlas.cjs +55 -8
  3. package/engine/agentlas-api-agent.cjs +1 -1
  4. package/engine/agentlas-banner.cjs +66 -51
  5. package/engine/agentlas-capabilities.cjs +3 -0
  6. package/engine/agentlas-cloud-runtime.cjs +65 -11
  7. package/engine/agentlas-composer.cjs +109 -44
  8. package/engine/agentlas-doctor.cjs +65 -14
  9. package/engine/agentlas-i18n.cjs +132 -12
  10. package/engine/agentlas-input.cjs +123 -19
  11. package/engine/agentlas-native-host.cjs +381 -83
  12. package/engine/agentlas-parity.cjs +373 -53
  13. package/engine/agentlas-permissions.cjs +90 -0
  14. package/engine/agentlas-repl.cjs +149 -47
  15. package/engine/agentlas-tasks.cjs +111 -0
  16. package/engine/agentlas-tools.cjs +174 -12
  17. package/engine/agentlas-ui.cjs +349 -24
  18. package/engine/agentlas.cjs +3074 -379
  19. package/engine/architecture.data.json +5 -1
  20. package/engine/semver.cjs +64 -0
  21. package/package.json +1 -1
  22. package/test/bootstrap-race.cjs +47 -0
  23. package/test/capture-runtime-guard.cjs +122 -0
  24. package/test/cloud-asset-restore.cjs +423 -0
  25. package/test/cloud-cas-client.cjs +333 -0
  26. package/test/cloud-owner-restore.cjs +183 -0
  27. package/test/cloud-runtime-paths.cjs +40 -0
  28. package/test/cloud-save-publish.cjs +453 -0
  29. package/test/credential-env-regression.cjs +52 -0
  30. package/test/login-loopback-security.cjs +115 -0
  31. package/test/mcp-config-isolation.cjs +36 -0
  32. package/test/permission-mapping.cjs +180 -0
  33. package/test/run-api-regression.cjs +322 -0
  34. package/test/runtime-env-protection.cjs +45 -0
  35. package/test/semver-precedence.cjs +39 -0
  36. package/test/smoke.sh +33 -0
  37. package/test/sqlite-driver-probe.cjs +22 -0
  38. package/test/terminal-ui-regression.cjs +454 -0
  39. package/test/timeout-regression.cjs +218 -0
  40. package/test/tool-workspace-boundary.cjs +165 -0
  41. package/test/update-safety.cjs +376 -0
@@ -16,6 +16,7 @@ const caps = require("./agentlas-capabilities.cjs");
16
16
  const input = require("./agentlas-input.cjs");
17
17
  const i18n = require("./agentlas-i18n.cjs");
18
18
  const style = require("./agentlas-style.cjs");
19
+ const permissions = require("./agentlas-permissions.cjs");
19
20
 
20
21
  function runtimeLabel(rt) {
21
22
  if (!rt) return "(none)";
@@ -56,6 +57,8 @@ function makeMemoryGuard(ui, heading) {
56
57
  };
57
58
  return {
58
59
  c: ui.c,
60
+ lang: ui.lang,
61
+ t: (...a) => ui.t(...a),
59
62
  streamStart: () => ui.streamStart(),
60
63
  streamDelta: (t) => {
61
64
  if (cut) {
@@ -89,6 +92,9 @@ function makeMemoryGuard(ui, heading) {
89
92
  ok: (...a) => ui.ok(...a),
90
93
  cost: (...a) => ui.cost(...a),
91
94
  line: (...a) => ui.line(...a),
95
+ applyTaskTool: (...a) => ui.applyTaskTool(...a),
96
+ applyTaskResult: (...a) => ui.applyTaskResult(...a),
97
+ replaceTasks: (...a) => ui.replaceTasks(...a),
92
98
  };
93
99
  }
94
100
 
@@ -111,10 +117,13 @@ function makeStyleGuard(ui) {
111
117
  };
112
118
  return {
113
119
  c: ui.c,
120
+ lang: ui.lang,
121
+ t: (...a) => ui.t(...a),
114
122
  streamStart: () => {
115
123
  buf = "";
116
124
  inCode = false;
117
- ui.streamStart();
125
+ // This guard emits complete lines, so the persistent turn footer can safely stay visible.
126
+ ui.streamStart(true);
118
127
  },
119
128
  streamDelta: (text) => {
120
129
  if (!text) return;
@@ -143,6 +152,9 @@ function makeStyleGuard(ui) {
143
152
  cost: (...a) => ui.cost(...a),
144
153
  line: (...a) => ui.line(...a),
145
154
  stopSpinner: (...a) => ui.stopSpinner(...a),
155
+ applyTaskTool: (...a) => ui.applyTaskTool(...a),
156
+ applyTaskResult: (...a) => ui.applyTaskResult(...a),
157
+ replaceTasks: (...a) => ui.replaceTasks(...a),
146
158
  };
147
159
  }
148
160
 
@@ -157,7 +169,7 @@ function startRepl(opts) {
157
169
  const state = {
158
170
  subject: opts.subject || null,
159
171
  runtime: opts.runtime,
160
- permission: opts.permission || "write",
172
+ permission: opts.permission == null ? "write" : permissions.normalize(opts.permission),
161
173
  cwd: opts.cwd,
162
174
  history: [],
163
175
  native: {}, // kind → { id }
@@ -197,6 +209,7 @@ function startRepl(opts) {
197
209
  let closed = false;
198
210
  let currentAbort = null;
199
211
  let idleExitArmedUntil = 0;
212
+ const permissionCycle = permissions.createCycleController();
200
213
  rl.on("close", () => {
201
214
  if (handoff) return; // intentionally closed to hand stdin to the raw-mode composer
202
215
  closed = true;
@@ -235,6 +248,19 @@ function startRepl(opts) {
235
248
  return { projectPath: state.projectPath, agentId: state.subject && state.subject.id, permission: state.permission, cwd: state.cwd, lang: ui.lang };
236
249
  }
237
250
 
251
+ function setPermission(value, options = {}) {
252
+ const notify = options.notify !== false;
253
+ const persist = options.persist !== false;
254
+ const level = permissions.normalize(value);
255
+ state.permission = level;
256
+ if (persist) {
257
+ prefs.permission = level;
258
+ if (opts.savePrefs) opts.savePrefs(prefs);
259
+ }
260
+ if (notify) ui.ok(ui.t("permSet", level));
261
+ return level;
262
+ }
263
+
238
264
  // Session usage ledger — accumulate per runtime label (host advantage: no single-model CLI can show this).
239
265
  function recordCost(label, usage) {
240
266
  const e = state.cost[label] || (state.cost[label] = { turns: 0, in: 0, out: 0, cost: 0, ms: 0 });
@@ -270,22 +296,27 @@ function startRepl(opts) {
270
296
  // ── run one turn ──
271
297
  async function runTurn(prompt, runOptions = {}) {
272
298
  busy = true;
273
- ui.beginTurn(); // 라이브 경과시간 스피너의 턴 시작점
274
299
  currentAbort = new AbortController();
275
300
  const signal = currentAbort.signal;
276
- const recordHistoryEntry = !runOptions.side;
277
- const targetLang = H.detectResponseLanguage ? H.detectResponseLanguage(prompt, ui.lang) : ui.lang;
278
- const ctx = { ...ctxNow(), lang: targetLang, uiLang: ui.lang };
279
- const rt = state.runtime;
280
- const costLabel = runtimeLabel(rt);
281
- const runEnv = H.buildChildEnv ? await H.buildChildEnv(db, { ...ctx, cwd: state.cwd }) : process.env;
282
- Object.assign(process.env, runEnv);
283
- ui._lastUsage = null;
284
- const assistantUi = makeStyleGuard(ui);
285
- const thinkingText = i18n.t(targetLang, "thinkingWith", costLabel);
286
- ui.info(thinkingText);
287
- ui.status(thinkingText);
288
301
  try {
302
+ ui.beginTurn({
303
+ ...composerMeta(),
304
+ onInterrupt: () => {
305
+ if (!currentAbort || currentAbort.signal.aborted) return;
306
+ currentAbort.abort();
307
+ ui.warn(ui.t("interrupted"));
308
+ },
309
+ }); // 작업 중에도 composer/status bar와 실제 runtime task 목록을 화면 하단에 유지
310
+ const recordHistoryEntry = !runOptions.side;
311
+ const targetLang = H.detectResponseLanguage ? H.detectResponseLanguage(prompt, ui.lang) : ui.lang;
312
+ const ctx = { ...ctxNow(), lang: targetLang, uiLang: ui.lang };
313
+ const rt = state.runtime;
314
+ const costLabel = runtimeLabel(rt);
315
+ const runEnv = H.buildChildEnv ? await H.buildChildEnv(db, { ...ctx, cwd: state.cwd }) : process.env;
316
+ ui._lastUsage = null;
317
+ const assistantUi = makeStyleGuard(ui);
318
+ const thinkingText = i18n.t(targetLang, "thinkingWith", costLabel);
319
+ ui.status(thinkingText);
289
320
  if (rt.mode === "cli") {
290
321
  const bin = H.which(H.RUNTIME_BIN[rt.kind]) || H.RUNTIME_BIN[rt.kind];
291
322
  const session = state.native[rt.kind] || (state.native[rt.kind] = {});
@@ -305,7 +336,7 @@ function startRepl(opts) {
305
336
  model: rt.model || null, // /model (claude --model, codex -m, gemini -m)
306
337
  effort: state.effort || null, // /effort (codex reasoning effort, claude think-keyword)
307
338
  mcpServers:
308
- (state.permission === "write" || state.permission === "full") && H.mcpServers
339
+ state.permission === "full" && H.mcpServers
309
340
  ? H.mcpServers(db).filter((s) => s.enabled && s.transport === "stdio")
310
341
  : [],
311
342
  env: runEnv,
@@ -341,7 +372,7 @@ function startRepl(opts) {
341
372
  apiKey,
342
373
  system: sys,
343
374
  messages,
344
- ctx,
375
+ ctx: { ...ctx, env: runEnv },
345
376
  ui: guard,
346
377
  signal,
347
378
  });
@@ -537,7 +568,7 @@ function startRepl(opts) {
537
568
  const fmt = (e) => {
538
569
  const bits = [e.turns + (e.turns === 1 ? " turn" : " turns")];
539
570
  if (e.in || e.out) bits.push(e.in + "→" + e.out + " tok");
540
- if (e.cost) bits.push("$" + e.cost.toFixed(4));
571
+ // 달러 비용 미표시 — 토큰 수만.
541
572
  if (e.ms) bits.push((e.ms / 1000).toFixed(1) + "s");
542
573
  return bits.join(" · ");
543
574
  };
@@ -551,8 +582,8 @@ function startRepl(opts) {
551
582
 
552
583
  function printSlashSkills() {
553
584
  ui.line("");
554
- ui.rule("Skills");
555
- for (const entry of input.slashCommandEntries()) {
585
+ ui.rule(ui.t("skills.title"));
586
+ for (const entry of input.slashCommandEntries(ui.lang)) {
556
587
  const tag = entry.category ? ui.c.faint(entry.category.padEnd(10)) : "";
557
588
  ui.line(" " + ui.c.emerald(entry.command.padEnd(18)) + tag + ui.c.dim(entry.description));
558
589
  if (!entry.aliasOf && entry.usage) ui.line(" " + ui.c.faint(" ".repeat(18) + entry.usage));
@@ -561,14 +592,10 @@ function startRepl(opts) {
561
592
 
562
593
  function printPermissions() {
563
594
  ui.line("");
564
- ui.rule("Permissions");
565
- ui.line(" " + ui.c.faint("Current") + " " + ui.c.emerald(state.permission));
566
- const rows = [
567
- ["read", "inspect files and answer; no file writes or shell automation"],
568
- ["write", "read plus create/edit files in the current work area"],
569
- ["full", "write plus shell commands and external local automation"],
570
- ];
571
- for (const [level, description] of rows) {
595
+ ui.rule(ui.t("permissions.title"));
596
+ ui.line(" " + ui.c.faint(ui.t("permissions.current")) + " " + ui.c.emerald(state.permission));
597
+ for (const level of permissions.LEVELS) {
598
+ const description = permissions.copy(level, ui.lang).description;
572
599
  const mark = level === state.permission ? "› " : " ";
573
600
  ui.line(" " + ui.c.emerald((mark + level).padEnd(10)) + ui.c.dim(description));
574
601
  }
@@ -633,7 +660,8 @@ function startRepl(opts) {
633
660
  ui.tool("$ " + cmd);
634
661
  const r = spawnSync("bash", ["-lc", cmd], { cwd: state.cwd, encoding: "utf8", timeout: 120000, maxBuffer: 8 * 1024 * 1024 });
635
662
  const out = ((r.stdout || "") + (r.stderr || "")).trim();
636
- ui.toolResult(out || ("exit " + (r.status == null ? "?" : r.status)), r.status === 0 || r.status == null);
663
+ // `!command` is explicit user output, unlike autonomous runtime traces: keep it inspectable.
664
+ ui.toolResult(out || ("exit " + (r.status == null ? "?" : r.status)), r.status === 0 || r.status == null, { verbose: true });
637
665
  }
638
666
 
639
667
  // @path — inline the contents of mentioned files into the prompt as fenced context.
@@ -722,6 +750,37 @@ function startRepl(opts) {
722
750
  await H.hepRun(["research", ...arg.split(/\s+/)], { cwd: state.cwd });
723
751
  return true;
724
752
  }
753
+ case "search": {
754
+ if (!arg) return ui.warn("usage: /search <할 일>"), true;
755
+ if (H.cloudSearch) await H.cloudSearch(db, [arg]);
756
+ else await H.hepRun(["hep-search", arg], { cwd: state.cwd });
757
+ return true;
758
+ }
759
+ case "install": {
760
+ if (!arg) return ui.warn("usage: /install <slug> (/search 로 먼저 찾으세요)"), true;
761
+ if (H.cloudInstall) {
762
+ try { const r = await H.cloudInstall(db, arg.trim()); ui.ok((r && r.name) ? `설치됨: ${r.name}` : `설치됨: ${arg.trim()}`); }
763
+ catch (e) { ui.error((e && e.message) || String(e)); }
764
+ } else ui.warn("설치 기능을 사용할 수 없습니다.");
765
+ return true;
766
+ }
767
+ case "network":
768
+ case "taskforce": {
769
+ if (!arg) return ui.warn("usage: /network <요청> (A2A 태스크포스)"), true;
770
+ ui.line("");
771
+ await H.hepRun(["hep-network", arg, "--project", state.cwd, "--runtime", "terminal"], { cwd: state.cwd });
772
+ return true;
773
+ }
774
+ case "browser": {
775
+ ui.line("");
776
+ await H.hepRun(["hep-browser", ...(arg ? arg.split(/\s+/) : [])], { cwd: state.cwd });
777
+ return true;
778
+ }
779
+ case "connect": {
780
+ ui.line("");
781
+ await H.hepRun(["hep-connect", ...(arg ? arg.split(/\s+/) : [])], { cwd: state.cwd });
782
+ return true;
783
+ }
725
784
  case "swarm": {
726
785
  if (!arg) return ui.warn("usage: /swarm <goal> [--parallel N]"), true;
727
786
  let goal = arg;
@@ -770,16 +829,14 @@ function startRepl(opts) {
770
829
  return true;
771
830
  }
772
831
  if (!["read", "write", "full"].includes(p)) return ui.warn(ui.t("permUsage")), true;
773
- state.permission = p;
774
- ui.ok(ui.t("permSet", p));
832
+ setPermission(p);
775
833
  return true;
776
834
  }
777
835
  case "permissions":
778
836
  if (arg) {
779
837
  const p = (arg || "").toLowerCase();
780
838
  if (!["read", "write", "full"].includes(p)) return ui.warn(ui.t("permUsage")), true;
781
- state.permission = p;
782
- ui.ok(ui.t("permSet", p));
839
+ setPermission(p);
783
840
  } else {
784
841
  printPermissions();
785
842
  }
@@ -818,6 +875,18 @@ function startRepl(opts) {
818
875
  }
819
876
  return true;
820
877
  }
878
+ case "career-graph":
879
+ case "graph": {
880
+ if (!H.careerGraphCommand) return ui.warn("career graph command unavailable"), true;
881
+ try {
882
+ const lines = H.careerGraphCommand(arg, { cwd: state.cwd, projectPath: state.projectPath });
883
+ ui.line("");
884
+ for (const item of lines || []) ui.line(" " + ui.c.text(String(item)));
885
+ } catch (e) {
886
+ ui.error((e && e.message) || String(e));
887
+ }
888
+ return true;
889
+ }
821
890
  case "side":
822
891
  case "btw":
823
892
  if (!arg) return ui.warn(ui.t("sideUsage")), true;
@@ -870,10 +939,7 @@ function startRepl(opts) {
870
939
  const servers = H.mcpServers ? H.mcpServers(db) : [];
871
940
  ui.line("");
872
941
  ui.rule("MCP");
873
- if (!servers.length) {
874
- ui.info(ui.t("mcp.none"));
875
- return true;
876
- }
942
+ ui.line(" " + ui.c.emerald(ui.t("mcp.playwright").padEnd(22)) + ui.c.blue("stdio ") + ui.c.green("on ") + ui.c.dim(" " + ui.t("mcp.fullOnly")));
877
943
  for (const s of servers) {
878
944
  let envKeys = [];
879
945
  try { envKeys = JSON.parse(s.env_keys_json || "[]"); } catch { /* ignore */ }
@@ -882,7 +948,7 @@ function startRepl(opts) {
882
948
  const envStr = envKeys.length ? envKeys.join(", ") : "no key";
883
949
  ui.line(" " + ui.c.emerald(String(name).padEnd(22)) + ui.c.blue(String(s.transport || "").padEnd(7)) + on + ui.c.dim(" " + envStr));
884
950
  }
885
- const wired = servers.filter((s) => s.enabled && s.transport === "stdio").length + 1; // +1 = playwright(항상)
951
+ const wired = servers.filter((s) => s.enabled && s.transport === "stdio").length + 1; // +1 = full-only Playwright
886
952
  ui.line(" " + ui.c.faint(ui.t("mcp.wired", String(wired))));
887
953
  ui.line(" " + ui.c.faint(ui.t("mcp.usage")));
888
954
  return true;
@@ -1086,21 +1152,50 @@ function startRepl(opts) {
1086
1152
  }
1087
1153
 
1088
1154
  // ── composer (raw-mode bottom box) main loop ──
1089
- function composerStatus() {
1155
+ function composerMeta() {
1090
1156
  const rt = runtimeLabel(state.runtime);
1091
1157
  const subj = state.subject ? state.subject.label : ui.t("composer.autoroute");
1092
1158
  const eff = state.effort ? " · " + state.effort : "";
1093
- return `${rt} · ${state.permission}${eff} · ${subj} · ${ui.t("composer.hint")}`;
1159
+ const permissionLabel = permissions.copy(state.permission, ui.lang).label;
1160
+ return {
1161
+ lang: ui.lang,
1162
+ permission: state.permission,
1163
+ permissionLabel,
1164
+ status: `${rt}${eff} · ${subj} · ${ui.t("permCycleHint")} · ${ui.t("composer.hint")} · ↑↓ history`,
1165
+ onCyclePermission: () => {
1166
+ const cycle = permissionCycle.step(state.permission);
1167
+ if (cycle.armed) {
1168
+ return {
1169
+ ...composerMeta(),
1170
+ confirmation: ui.t("permFullArm"),
1171
+ confirmationTone: "danger",
1172
+ };
1173
+ }
1174
+ const level = setPermission(cycle.level, { notify: false, persist: false });
1175
+ return {
1176
+ ...composerMeta(),
1177
+ confirmation: cycle.enteredFull
1178
+ ? ui.t("permFullConfirm")
1179
+ : ui.t("permCycleConfirm", permissions.copy(level, ui.lang).label),
1180
+ confirmationTone: cycle.enteredFull ? "danger" : "normal",
1181
+ };
1182
+ },
1183
+ onPermissionCycleCancel: () => {
1184
+ permissionCycle.cancel();
1185
+ return { ...composerMeta(), confirmation: null, confirmationTone: null };
1186
+ },
1187
+ };
1094
1188
  }
1095
1189
  async function composerLoop() {
1096
1190
  let buffer = "";
1097
1191
  while (!closed) {
1098
1192
  let r;
1099
1193
  try {
1194
+ const meta = composerMeta();
1100
1195
  r = await composer.read({
1101
1196
  glyph: buffer ? "…" : "›",
1102
- status: composerStatus(),
1103
- suggest: (l) => input.slashCommandSuggestions(l),
1197
+ ...meta,
1198
+ suggest: (l) => input.slashCommandSuggestions(l, 12, ui.lang),
1104
1199
  complete: completer,
1105
1200
  });
1106
1201
  } catch (e) {
@@ -1193,10 +1288,10 @@ function startRepl(opts) {
1193
1288
  function printHelp(ui) {
1194
1289
  const c = ui.c;
1195
1290
  ui.line("");
1196
- ui.rule("Help");
1197
- ui.line(" " + c.bold(c.text("Agentlas runs local agents from this terminal, with runtime, permission, files, shell, and history controls.")));
1291
+ ui.rule(ui.t("help.title"));
1292
+ ui.line(" " + c.bold(c.text(ui.t("help.intro"))));
1198
1293
  ui.line("");
1199
- ui.line(" " + c.faint("Commands"));
1294
+ ui.line(" " + c.faint(ui.t("help.commands")));
1200
1295
  const rows = [
1201
1296
  [ui.t("help.talkKey"), ui.t("help.talk")],
1202
1297
  ["/skills", ui.t("help.skills")],
@@ -1212,6 +1307,7 @@ function printHelp(ui) {
1212
1307
  ["/setup", ui.t("help.setup")],
1213
1308
  ["/cwd [path]", ui.t("help.cwd")],
1214
1309
  ["/memory", ui.t("help.memory")],
1310
+ ["/career-graph [text]", ui.t("help.careerGraph")],
1215
1311
  ["/ontology [text]", ui.t("help.ontology")],
1216
1312
  ["/side <question>", ui.t("help.side")],
1217
1313
  ["/status", ui.t("help.status")],
@@ -1228,8 +1324,12 @@ function printHelp(ui) {
1228
1324
  ["/build <request>", ui.t("help.build")],
1229
1325
  ["/route <request>", ui.t("help.route")],
1230
1326
  ["/research <sub>", ui.t("help.research")],
1231
- ["/marketplace", ui.t("help.market")],
1327
+ ["/search <task>", ui.t("help.search")],
1232
1328
  ["/install <slug>", ui.t("help.install")],
1329
+ ["/network <req>", ui.t("help.network")],
1330
+ ["/browser", ui.t("help.browser")],
1331
+ ["/connect", ui.t("help.connect")],
1332
+ ["/marketplace", ui.t("help.market")],
1233
1333
  ["/clear", ui.t("help.clear")],
1234
1334
  ["/doctor", ui.t("help.doctor")],
1235
1335
  ["/keybindings", ui.t("help.keybindings")],
@@ -1249,10 +1349,12 @@ function printKeybindings(ui) {
1249
1349
  ["!cmd", ui.t("help.bang")],
1250
1350
  ["\\ + Enter", ui.t("help.multiline")],
1251
1351
  ["Tab", ui.t("help.tab")],
1352
+ ["Shift-Tab", ui.t("help.shiftTab")],
1353
+ ["Ctrl-T", ui.t("help.ctrlT")],
1252
1354
  ["Up / Down", ui.t("help.arrows")],
1253
1355
  ["Ctrl-C", ui.t("help.ctrlc")],
1254
1356
  ];
1255
1357
  for (const [k, v] of tips) ui.line(" " + c.emerald(k.padEnd(24)) + c.dim(v));
1256
1358
  }
1257
1359
 
1258
- module.exports = { startRepl, runtimeLabel };
1360
+ module.exports = { startRepl, runtimeLabel, makeMemoryGuard, makeStyleGuard };
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+
3
+ /* Normalize only task/todo events emitted by the native runtimes.
4
+ * Ordinary tool activity is intentionally excluded: a command is not a fabricated plan item.
5
+ */
6
+
7
+ function statusOf(value, completed) {
8
+ if (completed === true) return "completed";
9
+ const status = String(value || "pending").trim().toLowerCase().replace(/[ -]+/g, "_");
10
+ if (["completed", "complete", "done", "success", "succeeded"].includes(status)) return "completed";
11
+ if (["in_progress", "active", "running", "started"].includes(status)) return "in_progress";
12
+ if (["failed", "error", "blocked", "cancelled", "canceled"].includes(status)) return "failed";
13
+ return "pending";
14
+ }
15
+
16
+ function listFrom(payload) {
17
+ if (Array.isArray(payload)) return payload;
18
+ if (!payload || typeof payload !== "object") return [];
19
+ for (const key of ["todos", "items", "tasks"]) {
20
+ if (Array.isArray(payload[key])) return payload[key];
21
+ }
22
+ return [];
23
+ }
24
+
25
+ function hasExplicitList(payload) {
26
+ if (Array.isArray(payload)) return true;
27
+ if (!payload || typeof payload !== "object") return false;
28
+ return ["todos", "items", "tasks"].some((key) => Array.isArray(payload[key]));
29
+ }
30
+
31
+ function sanitizeLabel(value, max = 500) {
32
+ let text = String(value || "");
33
+ // Runtime task text is untrusted terminal content. Strip OSC/CSI/escape controls,
34
+ // flatten line breaks, then cap it before it reaches footer row accounting.
35
+ text = text
36
+ .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
37
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
38
+ .replace(/\x1b./g, "")
39
+ .replace(/[\r\n\t]+/g, " ")
40
+ .replace(/[\x00-\x1f\x7f-\x9f]/g, "")
41
+ .replace(/\s+/g, " ")
42
+ .trim();
43
+ return Array.from(text).slice(0, max).join("");
44
+ }
45
+
46
+ function normalizeTaskList(payload, source = "runtime") {
47
+ return listFrom(payload).flatMap((raw, index) => {
48
+ if (!raw || typeof raw !== "object") return [];
49
+ const label = sanitizeLabel(raw.content || raw.subject || raw.description || raw.text || raw.title || raw.activeForm);
50
+ if (!label) return [];
51
+ return [{
52
+ id: String(raw.id || raw.taskId || `${source}:${index}`),
53
+ label,
54
+ status: statusOf(raw.status, raw.completed),
55
+ source,
56
+ }];
57
+ });
58
+ }
59
+
60
+ function applyTaskTool(current, name, payload, toolId) {
61
+ const tool = String(name || "").toLowerCase();
62
+ if (tool === "todowrite" || tool === "write_todos") {
63
+ return normalizeTaskList(payload, tool);
64
+ }
65
+
66
+ if (tool !== "taskcreate" && tool !== "taskupdate") return current;
67
+
68
+ const next = Array.isArray(current) ? current.map((task) => ({ ...task })) : [];
69
+ if (tool === "taskcreate") {
70
+ const label = sanitizeLabel(payload?.subject || payload?.description || payload?.activeForm);
71
+ if (!label) return next;
72
+ next.push({ id: String(payload?.taskId || toolId || `taskcreate:${next.length}`), label, status: "pending", source: "taskcreate" });
73
+ return next;
74
+ }
75
+ if (tool === "taskupdate") {
76
+ const id = String(payload?.taskId || toolId || "");
77
+ if (!id) return next;
78
+ if (String(payload?.status || "").toLowerCase() === "deleted") return next.filter((task) => task.id !== id);
79
+ const index = next.findIndex((task) => task.id === id);
80
+ const label = sanitizeLabel(payload?.subject || payload?.description || payload?.activeForm || (index >= 0 && next[index].label) || id);
81
+ const task = { id, label, status: statusOf(payload?.status), source: "taskupdate" };
82
+ if (index >= 0) next[index] = { ...next[index], ...task };
83
+ else next.push(task);
84
+ }
85
+ return next;
86
+ }
87
+
88
+ function applyTaskResult(current, name, payload, toolId) {
89
+ const tool = String(name || "").toLowerCase();
90
+ if (!["taskcreate", "taskupdate", "tasklist"].includes(tool)) return current;
91
+ if (!payload || typeof payload !== "object") return current;
92
+ if (tool === "tasklist") {
93
+ return hasExplicitList(payload) ? normalizeTaskList(payload, "tasklist") : current;
94
+ }
95
+ const record = payload.task && typeof payload.task === "object" ? payload.task : payload;
96
+ const id = String(record.id || record.taskId || payload.taskId || "");
97
+ if (!id) return current;
98
+ const next = Array.isArray(current) ? current.map((task) => ({ ...task })) : [];
99
+ const provisional = next.findIndex((task) => task.id === String(toolId || ""));
100
+ const existing = next.findIndex((task) => task.id === id);
101
+ const index = existing >= 0 ? existing : provisional;
102
+ const prior = index >= 0 ? next[index] : null;
103
+ const label = sanitizeLabel(record.content || record.subject || record.description || record.text || record.title || prior?.label || id);
104
+ const task = { id, label, status: statusOf(record.status || prior?.status), source: tool || "task-result" };
105
+ if (index >= 0) next[index] = { ...prior, ...task };
106
+ else next.push(task);
107
+ if (existing >= 0 && provisional >= 0 && provisional !== existing) next.splice(provisional, 1);
108
+ return next;
109
+ }
110
+
111
+ module.exports = { statusOf, sanitizeLabel, normalizeTaskList, applyTaskTool, applyTaskResult };