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.
- package/README.md +112 -23
- package/bin/agentlas.cjs +55 -8
- package/engine/agentlas-api-agent.cjs +1 -1
- package/engine/agentlas-banner.cjs +66 -51
- package/engine/agentlas-capabilities.cjs +3 -0
- package/engine/agentlas-cloud-runtime.cjs +65 -11
- package/engine/agentlas-composer.cjs +109 -44
- package/engine/agentlas-doctor.cjs +65 -14
- package/engine/agentlas-i18n.cjs +132 -12
- package/engine/agentlas-input.cjs +123 -19
- package/engine/agentlas-native-host.cjs +381 -83
- package/engine/agentlas-parity.cjs +373 -53
- package/engine/agentlas-permissions.cjs +90 -0
- package/engine/agentlas-repl.cjs +149 -47
- package/engine/agentlas-tasks.cjs +111 -0
- package/engine/agentlas-tools.cjs +174 -12
- package/engine/agentlas-ui.cjs +349 -24
- package/engine/agentlas.cjs +3074 -379
- package/engine/architecture.data.json +5 -1
- package/engine/semver.cjs +64 -0
- package/package.json +1 -1
- package/test/bootstrap-race.cjs +47 -0
- package/test/capture-runtime-guard.cjs +122 -0
- package/test/cloud-asset-restore.cjs +423 -0
- package/test/cloud-cas-client.cjs +333 -0
- package/test/cloud-owner-restore.cjs +183 -0
- package/test/cloud-runtime-paths.cjs +40 -0
- package/test/cloud-save-publish.cjs +453 -0
- package/test/credential-env-regression.cjs +52 -0
- package/test/login-loopback-security.cjs +115 -0
- package/test/mcp-config-isolation.cjs +36 -0
- package/test/permission-mapping.cjs +180 -0
- package/test/run-api-regression.cjs +322 -0
- package/test/runtime-env-protection.cjs +45 -0
- package/test/semver-precedence.cjs +39 -0
- package/test/smoke.sh +33 -0
- package/test/sqlite-driver-probe.cjs +22 -0
- package/test/terminal-ui-regression.cjs +454 -0
- package/test/timeout-regression.cjs +218 -0
- package/test/tool-workspace-boundary.cjs +165 -0
- package/test/update-safety.cjs +376 -0
package/engine/agentlas-repl.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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("
|
|
565
|
-
ui.line(" " + ui.c.faint("
|
|
566
|
-
const
|
|
567
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
1197
|
-
ui.line(" " + c.bold(c.text("
|
|
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("
|
|
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
|
-
["/
|
|
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 };
|