agentlas 0.9.10 → 1.0.2

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 (145) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/README.md +319 -341
  3. package/bin/agentlas.cjs +32 -9
  4. package/engine/agentlas-banner.cjs +24 -3
  5. package/engine/agentlas-composer.cjs +239 -31
  6. package/engine/agentlas-config.cjs +103 -7
  7. package/engine/agentlas-core-harness.cjs +48 -4
  8. package/engine/agentlas-evolution.cjs +3 -4
  9. package/engine/agentlas-i18n.cjs +88 -32
  10. package/engine/agentlas-input.cjs +234 -18
  11. package/engine/agentlas-memory-governance.cjs +3 -5
  12. package/engine/agentlas-memory-import.cjs +3 -3
  13. package/engine/agentlas-native-host.cjs +200 -9
  14. package/engine/agentlas-onboard.cjs +112 -20
  15. package/engine/agentlas-permissions.cjs +14 -7
  16. package/engine/agentlas-sqlite-policy.cjs +34 -0
  17. package/engine/agentlas-ui.cjs +93 -42
  18. package/engine/agentlas-workforce.cjs +472 -58
  19. package/engine/agentlas-workload-routing.cjs +8 -3
  20. package/engine/agentlas.cjs +140 -12367
  21. package/engine/agents/files.cjs +61 -0
  22. package/engine/agents/import-local.cjs +237 -0
  23. package/engine/agents/registry.cjs +158 -0
  24. package/engine/agents/router.cjs +565 -0
  25. package/engine/agents/routes.cjs +43 -0
  26. package/engine/architecture.data.json +2 -2
  27. package/engine/automation/daemon.cjs +335 -0
  28. package/engine/automation/schedule.cjs +181 -0
  29. package/engine/automation/store.cjs +209 -0
  30. package/engine/cloud/auth.cjs +279 -0
  31. package/engine/cloud/hub-client.cjs +239 -0
  32. package/engine/cloud-assets/cargo.cjs +49 -0
  33. package/engine/cloud-assets/cas.cjs +235 -0
  34. package/engine/cloud-assets/commands.cjs +273 -0
  35. package/engine/cloud-assets/package.cjs +936 -0
  36. package/engine/cloud-assets/restore.cjs +172 -0
  37. package/engine/cloud-assets/state.cjs +268 -0
  38. package/engine/commands/automation.cjs +195 -0
  39. package/engine/commands/billing.cjs +96 -0
  40. package/engine/commands/browser.cjs +20 -0
  41. package/engine/commands/build.cjs +33 -0
  42. package/engine/commands/call.cjs +24 -0
  43. package/engine/commands/career-graph.cjs +51 -0
  44. package/engine/commands/cd.cjs +22 -0
  45. package/engine/commands/chat.cjs +12 -0
  46. package/engine/commands/chats.cjs +28 -0
  47. package/engine/commands/cloud.cjs +17 -0
  48. package/engine/commands/connect.cjs +20 -0
  49. package/engine/commands/context.cjs +66 -0
  50. package/engine/commands/creds.cjs +203 -0
  51. package/engine/commands/doctor.cjs +68 -0
  52. package/engine/commands/env.cjs +33 -0
  53. package/engine/commands/evolve.cjs +23 -0
  54. package/engine/commands/experience.cjs +34 -0
  55. package/engine/commands/film.cjs +8 -0
  56. package/engine/commands/firm.cjs +115 -0
  57. package/engine/commands/help.cjs +65 -0
  58. package/engine/commands/hep.cjs +23 -0
  59. package/engine/commands/import.cjs +35 -0
  60. package/engine/commands/index.cjs +136 -0
  61. package/engine/commands/install.cjs +27 -0
  62. package/engine/commands/journal.cjs +31 -0
  63. package/engine/commands/legacy-network.cjs +29 -0
  64. package/engine/commands/list.cjs +49 -0
  65. package/engine/commands/login.cjs +68 -0
  66. package/engine/commands/logout.cjs +28 -0
  67. package/engine/commands/mcp.cjs +91 -0
  68. package/engine/commands/memory.cjs +21 -0
  69. package/engine/commands/multimodal.cjs +91 -0
  70. package/engine/commands/native.cjs +34 -0
  71. package/engine/commands/netadmin.cjs +31 -0
  72. package/engine/commands/oberon.cjs +70 -0
  73. package/engine/commands/ontology.cjs +24 -0
  74. package/engine/commands/open.cjs +48 -0
  75. package/engine/commands/plugin.cjs +101 -0
  76. package/engine/commands/project.cjs +47 -0
  77. package/engine/commands/research.cjs +36 -0
  78. package/engine/commands/route.cjs +37 -0
  79. package/engine/commands/run.cjs +149 -0
  80. package/engine/commands/search.cjs +55 -0
  81. package/engine/commands/setup.cjs +45 -0
  82. package/engine/commands/storm.cjs +75 -0
  83. package/engine/commands/swarm.cjs +75 -0
  84. package/engine/commands/telegram.cjs +32 -0
  85. package/engine/commands/uninstall.cjs +68 -0
  86. package/engine/commands/update.cjs +54 -0
  87. package/engine/commands/upload.cjs +18 -0
  88. package/engine/commands/usage.cjs +35 -0
  89. package/engine/commands/variant.cjs +25 -0
  90. package/engine/commands/version.cjs +9 -0
  91. package/engine/commands/whoami.cjs +38 -0
  92. package/engine/commands/workforce.cjs +103 -0
  93. package/engine/core/db.cjs +160 -0
  94. package/engine/core/paths.cjs +35 -0
  95. package/engine/experience/build.cjs +181 -0
  96. package/engine/experience/intents.cjs +492 -0
  97. package/engine/experience/runtime.cjs +242 -0
  98. package/engine/experience/variant.cjs +196 -0
  99. package/engine/firms/orchestrate.cjs +333 -0
  100. package/engine/hephaestus/runtime.cjs +697 -0
  101. package/engine/hub/install.cjs +872 -0
  102. package/engine/hub/plugins.cjs +213 -0
  103. package/engine/mcp/consent.cjs +289 -0
  104. package/engine/mcp/contract.cjs +202 -0
  105. package/engine/mcp/index.cjs +43 -0
  106. package/engine/mcp/inventory.cjs +322 -0
  107. package/engine/mcp/plan.cjs +286 -0
  108. package/engine/mcp/probe.cjs +151 -0
  109. package/engine/memory-cli/curate.cjs +163 -0
  110. package/engine/oberon/common.cjs +69 -0
  111. package/engine/oberon/manifest.cjs +164 -0
  112. package/engine/oberon/outputs.cjs +70 -0
  113. package/engine/oberon/render.cjs +164 -0
  114. package/engine/project/career-graph.cjs +249 -0
  115. package/engine/project/credentials.cjs +262 -0
  116. package/engine/project/env-file.cjs +46 -0
  117. package/engine/project/index.cjs +27 -0
  118. package/engine/project/memory-context.cjs +453 -0
  119. package/engine/project/ontology.cjs +467 -0
  120. package/engine/project/paths.cjs +39 -0
  121. package/engine/project/seed.cjs +200 -0
  122. package/engine/project/state.cjs +403 -0
  123. package/engine/project/super-ontology-seed.json +3288 -0
  124. package/engine/runtimes/detect.cjs +54 -0
  125. package/engine/runtimes/overrides.cjs +139 -0
  126. package/engine/runtimes/resolve.cjs +64 -0
  127. package/engine/sessions/apply-fences.cjs +188 -0
  128. package/engine/sessions/fences.cjs +362 -0
  129. package/engine/sessions/orchestrator.cjs +170 -0
  130. package/engine/sessions/prompt.cjs +212 -0
  131. package/engine/sessions/session.cjs +245 -0
  132. package/engine/sessions/sink.cjs +54 -0
  133. package/engine/sessions/store.cjs +79 -0
  134. package/engine/storm/deps.cjs +88 -0
  135. package/engine/storm/storm.cjs +218 -0
  136. package/engine/storm/swarm.cjs +422 -0
  137. package/engine/ui/palette.cjs +105 -0
  138. package/engine/ui/renderer.cjs +85 -0
  139. package/engine/ui/repl.cjs +444 -0
  140. package/engine/workforce/capture.cjs +701 -0
  141. package/engine/workforce/deps.cjs +472 -0
  142. package/package.json +2 -6
  143. package/engine/agentlas-experience-mcp.cjs +0 -1709
  144. package/engine/agentlas-parity.cjs +0 -1499
  145. package/engine/agentlas-repl.cjs +0 -1780
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * 스키마는 실측으로 확인됨 (cli/agentlas.cjs 상단 주석 참고).
14
14
  */
15
- const { spawn } = require("node:child_process");
15
+ const { execFileSync, spawn } = require("node:child_process");
16
16
  const crypto = require("node:crypto");
17
17
  const fs = require("node:fs");
18
18
  const os = require("node:os");
@@ -821,6 +821,7 @@ function runNativeTurn(req) {
821
821
  let child;
822
822
  try {
823
823
  const spawnImpl = req.spawn || spawn;
824
+ const groupedChild = process.platform !== "win32" && spawnImpl === spawn;
824
825
  const childEnv = launchReq.prepareRuntimeEnv === false
825
826
  ? (launchReq.env || process.env)
826
827
  : runtimeEnvForKind(kind, launchReq.env || process.env, launchReq);
@@ -828,7 +829,16 @@ function runNativeTurn(req) {
828
829
  cwd,
829
830
  stdio: ["ignore", "pipe", "pipe"],
830
831
  env: childEnv,
832
+ detached: groupedChild,
833
+ windowsHide: true,
831
834
  });
835
+ child.__agentlasNativeChild = spawnImpl === spawn;
836
+ child.__agentlasGroupedChild = groupedChild;
837
+ // 세션 계층(오르카)이 실행 중 중단(kill)을 하려면 child 핸들이 필요하다.
838
+ // 커스텀 spawn 주입 방식은 위 트리킬 플래그를 깨므로 훅으로 노출한다.
839
+ if (typeof req.onSpawn === "function") {
840
+ try { req.onSpawn(child); } catch { /* observer 실패가 턴을 죽이면 안 됨 */ }
841
+ }
832
842
  } catch (e) {
833
843
  ui.error(uiText(ui, "runtime.failed", kind, e.message));
834
844
  return resolve({ text: "", session: st.session, error: e.message });
@@ -840,6 +850,7 @@ function runNativeTurn(req) {
840
850
  let totalTimer = null;
841
851
  let killTimer = null;
842
852
  let forceTimer = null;
853
+ let verifyTimer = null;
843
854
  let removeLineReader = () => {};
844
855
  let stderrBuf = "";
845
856
  const clearWatchdogs = () => {
@@ -847,7 +858,8 @@ function runNativeTurn(req) {
847
858
  if (totalTimer) clearTimeout(totalTimer);
848
859
  if (killTimer) clearTimeout(killTimer);
849
860
  if (forceTimer) clearTimeout(forceTimer);
850
- idleTimer = totalTimer = killTimer = forceTimer = null;
861
+ if (verifyTimer) clearTimeout(verifyTimer);
862
+ idleTimer = totalTimer = killTimer = forceTimer = verifyTimer = null;
851
863
  };
852
864
  const cleanup = () => {
853
865
  clearWatchdogs();
@@ -866,7 +878,29 @@ function runNativeTurn(req) {
866
878
  ui.stopSpinner();
867
879
  const text = (st.finalText || st.text || "").trim();
868
880
  if (st.usage) ui.cost(st.usage);
869
- finish({ text, session: st.session, usage: st.usage, error: termination ? termination.message : "native runtime stopped" });
881
+ finish({
882
+ text,
883
+ session: st.session,
884
+ usage: st.usage,
885
+ error: termination ? termination.message : "native runtime stopped",
886
+ terminationVerified: Boolean(termination?.verified),
887
+ });
888
+ };
889
+ const verifyTermination = () => {
890
+ if (settled || !termination) return;
891
+ if (verifyTimer) return;
892
+ if (child.__agentlasNativeChild === true && !(termination.pids || []).length) return;
893
+ const alive = activeNativeProcessIds(termination.pids || []);
894
+ if (Array.isArray(alive) && !alive.length) {
895
+ termination.verified = true;
896
+ terminationResult();
897
+ return;
898
+ }
899
+ for (const pid of alive || termination.pids || []) signalNativePid(pid, "SIGKILL");
900
+ verifyTimer = setTimeout(() => {
901
+ verifyTimer = null;
902
+ verifyTermination();
903
+ }, 25);
870
904
  };
871
905
  const requestStop = (reason) => {
872
906
  if (termination || settled) return;
@@ -878,11 +912,17 @@ function runNativeTurn(req) {
878
912
  if (idleTimer) clearTimeout(idleTimer);
879
913
  if (totalTimer) clearTimeout(totalTimer);
880
914
  idleTimer = totalTimer = null;
881
- try { child.kill("SIGTERM"); } catch { /* ignore */ }
915
+ const forcedTree = forceStopNativeProcessTree(child);
916
+ if (forcedTree.length) {
917
+ termination.pids = forcedTree;
918
+ verifyTermination();
919
+ return;
920
+ }
921
+ terminateNativeChild(child, "SIGTERM");
882
922
  if (settled) return;
883
923
  killTimer = setTimeout(() => {
884
924
  if (settled) return;
885
- try { child.kill("SIGKILL"); } catch { /* ignore */ }
925
+ terminateNativeChild(child, "SIGKILL");
886
926
  if (settled) return;
887
927
  forceTimer = setTimeout(terminationResult, Math.max(250, Math.min(1_000, timeout.killGraceMs)));
888
928
  }, timeout.killGraceMs);
@@ -920,7 +960,7 @@ function runNativeTurn(req) {
920
960
  child.on("error", (err) => {
921
961
  if (settled) return;
922
962
  if (termination) {
923
- terminationResult();
963
+ verifyTermination();
924
964
  return;
925
965
  }
926
966
  ui.stopSpinner();
@@ -930,7 +970,7 @@ function runNativeTurn(req) {
930
970
  child.on("close", (code) => {
931
971
  if (settled) return;
932
972
  if (termination) {
933
- terminationResult();
973
+ verifyTermination();
934
974
  return;
935
975
  }
936
976
  ui.streamEnd();
@@ -940,7 +980,7 @@ function runNativeTurn(req) {
940
980
  if (st.error && !st.errorShown) {
941
981
  // claude `result` is_error 등 — 이전에 표시되지 않은 에러를 노출
942
982
  ui.error(String(st.error));
943
- } else if (code !== 0 && !text) {
983
+ } else if (code !== 0) {
944
984
  // Runtime Doctor — 아는 시스템 원인(미인증 OAuth MCP 플러그인 등)이면 즉시 수리하고
945
985
  // 1회 자동 재시도한다(2026-07-08 notion@openai-curated가 codex 전멸시킨 사고).
946
986
  if (!req._doctorRetried) {
@@ -966,7 +1006,10 @@ function runNativeTurn(req) {
966
1006
  ui.warn(uiText(ui, "runtime.noOutput", kind) + (errTail ? ` (${errTail.slice(-200)})` : ""));
967
1007
  }
968
1008
  if (st.usage) ui.cost(st.usage);
969
- finish({ text, session: st.session, usage: st.usage, error: st.error });
1009
+ const exitError = code === 0
1010
+ ? st.error
1011
+ : st.error || `${kind} exited with code ${code == null ? "unknown" : code}`;
1012
+ finish({ text, session: st.session, usage: st.usage, error: exitError });
970
1013
  });
971
1014
 
972
1015
  armIdle();
@@ -978,6 +1021,150 @@ function runNativeTurn(req) {
978
1021
  });
979
1022
  }
980
1023
 
1024
+ function nativeProcessRows() {
1025
+ if (process.platform === "win32") return [];
1026
+ try {
1027
+ const output = execFileSync("/bin/ps", ["-axo", "pid=,ppid=,stat="], {
1028
+ encoding: "utf8",
1029
+ maxBuffer: 4 * 1024 * 1024,
1030
+ timeout: 1_000,
1031
+ });
1032
+ return String(output || "").split(/\r?\n/).map((line) => {
1033
+ const match = line.trim().match(/^(\d+)\s+(\d+)\s+(\S+)/);
1034
+ return match
1035
+ ? { pid: Number(match[1]), ppid: Number(match[2]), stat: match[3] }
1036
+ : null;
1037
+ }).filter(Boolean);
1038
+ } catch {
1039
+ return null;
1040
+ }
1041
+ }
1042
+
1043
+ function nativeProcessTreeIds(rootPid, rows = nativeProcessRows()) {
1044
+ if (!Number.isInteger(rootPid) || rootPid <= 1 || !Array.isArray(rows)) return [];
1045
+ const children = new Map();
1046
+ for (const row of rows) {
1047
+ if (!children.has(row.ppid)) children.set(row.ppid, []);
1048
+ children.get(row.ppid).push(row.pid);
1049
+ }
1050
+ const ordered = [];
1051
+ const seen = new Set();
1052
+ const visit = (pid) => {
1053
+ if (seen.has(pid)) return;
1054
+ seen.add(pid);
1055
+ for (const childPid of children.get(pid) || []) visit(childPid);
1056
+ ordered.push(pid);
1057
+ };
1058
+ visit(rootPid);
1059
+ return ordered;
1060
+ }
1061
+
1062
+ function activeNativeProcessIds(pids, rows = nativeProcessRows()) {
1063
+ if (!Array.isArray(pids) || !pids.length) return [];
1064
+ if (!Array.isArray(rows)) return null;
1065
+ const wanted = new Set(pids);
1066
+ return rows
1067
+ .filter((row) => wanted.has(row.pid) && !String(row.stat || "").startsWith("Z"))
1068
+ .map((row) => row.pid);
1069
+ }
1070
+
1071
+ function signalNativePid(pid, signal) {
1072
+ if (!Number.isInteger(pid) || pid <= 1) return false;
1073
+ try {
1074
+ process.kill(pid, signal);
1075
+ return true;
1076
+ } catch {
1077
+ return false;
1078
+ }
1079
+ }
1080
+
1081
+ function forceStopNativeProcessTree(child) {
1082
+ if (
1083
+ process.platform === "win32" &&
1084
+ child?.__agentlasNativeChild === true &&
1085
+ Number.isInteger(child.pid) &&
1086
+ child.pid > 1
1087
+ ) {
1088
+ try {
1089
+ execFileSync("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
1090
+ stdio: "ignore",
1091
+ timeout: 5_000,
1092
+ windowsHide: true,
1093
+ });
1094
+ return [child.pid];
1095
+ } catch {
1096
+ return [];
1097
+ }
1098
+ }
1099
+ if (
1100
+ !child ||
1101
+ child.__agentlasGroupedChild !== true ||
1102
+ !Number.isInteger(child.pid) ||
1103
+ child.pid <= 1
1104
+ ) return [];
1105
+
1106
+ // Native model CLIs may place tool shells in their own process groups. A
1107
+ // group-only kill can therefore close the model process while a Bash tool
1108
+ // keeps writing in the background. Freeze the whole observed ancestry first
1109
+ // so no descendant can advance, take one more snapshot for races, then kill
1110
+ // every owned process. The caller does not report cancellation until all
1111
+ // captured non-zombie PIDs are gone.
1112
+ const firstRows = nativeProcessRows();
1113
+ if (!Array.isArray(firstRows)) return [];
1114
+ const first = nativeProcessTreeIds(child.pid, firstRows);
1115
+ for (const pid of first) signalNativePid(pid, "SIGSTOP");
1116
+ const secondRows = nativeProcessRows();
1117
+ const second = Array.isArray(secondRows) ? nativeProcessTreeIds(child.pid, secondRows) : [];
1118
+ const all = [...new Set([...first, ...second])];
1119
+ for (const pid of second) signalNativePid(pid, "SIGSTOP");
1120
+ for (const pid of all) signalNativePid(pid, "SIGKILL");
1121
+ return all;
1122
+ }
1123
+
1124
+ function terminateNativeChild(child, signal = "SIGTERM") {
1125
+ if (!child) return false;
1126
+ let signalled = false;
1127
+ if (
1128
+ process.platform !== "win32" &&
1129
+ child.__agentlasGroupedChild === true &&
1130
+ Number.isInteger(child.pid) &&
1131
+ child.pid > 1
1132
+ ) {
1133
+ try {
1134
+ process.kill(-child.pid, signal);
1135
+ signalled = true;
1136
+ } catch {
1137
+ // The group may already be gone while the direct child is still
1138
+ // observable. Fall through to the direct-child signal.
1139
+ }
1140
+ }
1141
+ if (!signalled) {
1142
+ try {
1143
+ signalled = child.kill(signal);
1144
+ } catch {
1145
+ signalled = false;
1146
+ }
1147
+ }
1148
+ if (
1149
+ process.platform === "win32" &&
1150
+ signal === "SIGKILL" &&
1151
+ Number.isInteger(child.pid) &&
1152
+ child.pid > 1
1153
+ ) {
1154
+ try {
1155
+ const killer = spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
1156
+ stdio: "ignore",
1157
+ windowsHide: true,
1158
+ });
1159
+ killer.unref();
1160
+ signalled = true;
1161
+ } catch {
1162
+ // Direct child.kill above remains the best available fallback.
1163
+ }
1164
+ }
1165
+ return signalled;
1166
+ }
1167
+
981
1168
  module.exports = {
982
1169
  runNativeTurn,
983
1170
  summarizeToolInput,
@@ -997,4 +1184,8 @@ module.exports = {
997
1184
  codexMcpArgs,
998
1185
  nativeTimeoutConfig,
999
1186
  directNativeTimeoutConfig,
1187
+ nativeProcessTreeIds,
1188
+ activeNativeProcessIds,
1189
+ forceStopNativeProcessTree,
1190
+ terminateNativeChild,
1000
1191
  };
@@ -1,71 +1,163 @@
1
1
  "use strict";
2
2
  /*
3
- * First-run onboarding wizard (openclaw-style): language → default runtime → default permission.
3
+ * First-run onboarding wizard (openclaw-style): language → default runtime → safe default permission.
4
4
  * Runs once; result is saved to cli-prefs.json. Re-run anytime with `agentlas setup`.
5
5
  */
6
6
  const i18n = require("./agentlas-i18n.cjs");
7
7
  const banner = require("./agentlas-banner.cjs");
8
+ const { visWidth, wrapWidth } = require("./agentlas-composer.cjs");
8
9
 
9
10
  // req = { ui, rl, helpers } → Promise<{ onboarded, lang, runtime, permission }>
10
11
  async function runOnboard({ ui, rl, helpers }) {
11
12
  const H = helpers;
12
13
  const c = ui.c;
13
- const ask = (q) => new Promise((res) => rl.question(q, (a) => res((a || "").trim())));
14
+ const contentWidth = () => Math.max(20, (ui.out.columns || 80) - 5);
15
+ const printIndented = (value, paint = (text) => text, indent = " ") => {
16
+ const room = Math.max(2, (ui.out.columns || 80) - visWidth(indent));
17
+ for (const line of wrapWidth(value, room)) ui.line(indent + paint(line));
18
+ };
19
+ const printSaved = (value) => {
20
+ const lines = wrapWidth(value, Math.max(2, (ui.out.columns || 80) - 6));
21
+ ui.ok(lines[0] || "");
22
+ for (const line of lines.slice(1)) ui.line(" " + c.text(line));
23
+ };
24
+ const optionLine = (index, label, isDefault = false) => {
25
+ const suffix = isDefault ? ` ${ui.t("wiz.default")}` : "";
26
+ const prefix = `${index} `;
27
+ const continuation = " ".repeat(visWidth(prefix));
28
+ const lines = wrapWidth(`${label}${suffix}`, Math.max(2, contentWidth() - visWidth(prefix)));
29
+ lines.forEach((line, lineIndex) => {
30
+ ui.line(" " + c.text((lineIndex === 0 ? prefix : continuation) + line));
31
+ });
32
+ };
33
+ const pendingLines = [];
34
+ let lineWaiter = null;
35
+ const onLine = (line) => {
36
+ if (lineWaiter) {
37
+ const resolve = lineWaiter;
38
+ lineWaiter = null;
39
+ resolve((line || "").trim());
40
+ } else {
41
+ // A pasted multi-line setup response can arrive in one terminal data
42
+ // burst before the next prompt is installed. Preserve those later lines.
43
+ pendingLines.push((line || "").trim());
44
+ }
45
+ };
46
+ rl.on("line", onLine);
47
+ const ask = (q) => {
48
+ if (pendingLines.length) return Promise.resolve(pendingLines.shift());
49
+ return new Promise((resolve) => {
50
+ lineWaiter = resolve;
51
+ rl.setPrompt(q);
52
+ rl.prompt();
53
+ });
54
+ };
55
+ let localSigintArmedUntil = 0;
56
+ const ownsSigint = rl.listenerCount("SIGINT") === 0;
57
+ const onSigint = () => {
58
+ const now = Date.now();
59
+ if (now < localSigintArmedUntil) {
60
+ ui.line(ui.c.dim(ui.t("bye")));
61
+ process.exit(0);
62
+ }
63
+ localSigintArmedUntil = now + 3_000;
64
+ ui.warn(ui.t("ctrlcAgain"));
65
+ };
66
+ if (ownsSigint) rl.on("SIGINT", onSigint);
14
67
  const pickNum = async (n) => {
15
- // loop until a valid 1..n number; empty 1 (first option as default)
68
+ // Empty keeps the explicitly labelled first option. In a TTY, Up/Down
69
+ // places a real numeric choice into the readline field so arrows and
70
+ // number entry share the same validation path.
71
+ let selected = 1;
72
+ printIndented(ui.t("wiz.nav"), c.dim);
16
73
  for (;;) {
74
+ let settled = false;
75
+ let arrowWriteScheduled = false;
76
+ const flushArrowWrite = () => {
77
+ arrowWriteScheduled = false;
78
+ if (settled || rl.closed) return;
79
+ rl.write(null, { ctrl: true, name: "u" });
80
+ rl.write(String(selected));
81
+ };
82
+ const onKeypress = (_str, key = {}) => {
83
+ if (key.name === "return" || key.name === "enter") {
84
+ // readline may emit `line` before the coalesced setImmediate write.
85
+ // Commit the final arrow target synchronously before Enter reaches
86
+ // readline's own key handler.
87
+ if (arrowWriteScheduled) flushArrowWrite();
88
+ return;
89
+ }
90
+ if (key.name !== "up" && key.name !== "down") return;
91
+ selected = key.name === "up"
92
+ ? (selected - 2 + n) % n + 1
93
+ : selected % n + 1;
94
+ if (arrowWriteScheduled) return;
95
+ arrowWriteScheduled = true;
96
+ setImmediate(() => {
97
+ if (arrowWriteScheduled) flushArrowWrite();
98
+ });
99
+ };
100
+ if (rl.terminal && rl.input) rl.input.prependListener("keypress", onKeypress);
17
101
  const a = await ask(" " + c.emerald(ui.t("wiz.pick")));
18
- if (a === "") return 1;
102
+ settled = true;
103
+ if (rl.input) rl.input.removeListener("keypress", onKeypress);
104
+ if (a === "") return selected;
19
105
  if (/^\d+$/.test(a)) {
20
106
  const i = parseInt(a, 10);
21
107
  if (i >= 1 && i <= n) return i;
22
108
  }
109
+ ui.warn(ui.t("wiz.invalid", String(n)));
23
110
  }
24
111
  };
25
112
 
26
- // small mascot + header
27
- ui.line("");
28
- banner.renderMascot(ui);
29
- ui.line(" " + c.bold(c.emerald("Agentlas")) + c.dim(" · setup"));
30
- ui.line(" " + c.dim(ui.t("wiz.welcome")));
113
+ try {
114
+ // small mascot + header
115
+ ui.line("");
116
+ banner.renderMascot(ui);
117
+ ui.line(" " + c.bold(c.emerald("Agentlas")) + c.dim(" · setup"));
118
+ printIndented(ui.t("wiz.welcome"), c.dim);
31
119
 
32
120
  // Step 1 — language
33
121
  ui.line("");
34
- ui.line(" " + c.bold(ui.t("wiz.langQ")));
35
- i18n.LANGS.forEach((l, i) => ui.line(" " + c.faint(String(i + 1)) + " " + c.text(l.label)));
122
+ printIndented(ui.t("wiz.langQ"), c.bold);
123
+ i18n.LANGS.forEach((l, i) => optionLine(i + 1, l.label, i === 0));
36
124
  const li = await pickNum(i18n.LANGS.length);
37
125
  const lang = i18n.LANGS[li - 1].code;
38
126
  ui.lang = lang; // localize the rest of the wizard
39
127
 
40
128
  // Step 2 — default runtime
41
129
  ui.line("");
42
- ui.line(" " + c.bold(ui.t("wiz.runtimeQ")));
130
+ printIndented(ui.t("wiz.runtimeQ"), c.bold);
43
131
  const cliKinds = ["claude-code", "codex", "gemini"];
44
132
  const rtOpts = [{ value: "auto", label: ui.t("wiz.runtimeAuto") }];
45
133
  for (const k of cliKinds) {
46
134
  const has = !!H.which(H.RUNTIME_BIN[k]);
47
135
  rtOpts.push({ value: k, label: `${k} (${has ? ui.t("wiz.runtimeInstalled") : ui.t("wiz.runtimeMissing")})` });
48
136
  }
49
- rtOpts.forEach((o, i) => ui.line(" " + c.faint(String(i + 1)) + " " + c.text(o.label)));
137
+ rtOpts.forEach((o, i) => optionLine(i + 1, o.label, i === 0));
50
138
  const ri = await pickNum(rtOpts.length);
51
139
  const runtime = rtOpts[ri - 1].value;
52
140
 
53
141
  // Step 3 — default permission
54
142
  ui.line("");
55
- ui.line(" " + c.bold(ui.t("wiz.permQ")));
143
+ printIndented(ui.t("wiz.permQ"), c.bold);
56
144
  const permOpts = [
57
145
  { v: "read", l: ui.t("wiz.permRead") },
58
146
  { v: "write", l: ui.t("wiz.permWrite") },
59
- { v: "full", l: ui.t("wiz.permFull") },
60
147
  ];
61
- permOpts.forEach((o, i) => ui.line(" " + c.faint(String(i + 1)) + " " + c.text(o.l)));
148
+ permOpts.forEach((o, i) => optionLine(i + 1, o.l, i === 0));
62
149
  const pi = await pickNum(permOpts.length);
63
150
  const permission = permOpts[pi - 1].v;
64
151
 
65
- ui.line("");
66
- ui.ok(ui.t("wiz.saved"));
67
- ui.line(" " + c.faint(ui.t("wiz.changeLang")));
68
- return { onboarded: true, lang, runtime, permission };
152
+ ui.line("");
153
+ printSaved(ui.t("wiz.saved"));
154
+ printIndented(ui.t("wiz.changeLang"), c.faint);
155
+ return { onboarded: true, lang, runtime, permission };
156
+ } finally {
157
+ rl.removeListener("line", onLine);
158
+ if (ownsSigint) rl.removeListener("SIGINT", onSigint);
159
+ lineWaiter = null;
160
+ }
69
161
  }
70
162
 
71
163
  module.exports = { runOnboard };
@@ -14,34 +14,34 @@ const COPY = {
14
14
  read: {
15
15
  label: "read only",
16
16
  short: "inspect only",
17
- description: "inspect files and reason; runtime tools cannot change the workspace",
17
+ description: "inspect only; workspace writes blocked",
18
18
  },
19
19
  write: {
20
20
  label: "workspace write",
21
21
  short: "edit workspace",
22
- description: "read and edit the current workspace inside the runtime sandbox",
22
+ description: "workspace + runtime temp writes; external MCP off",
23
23
  },
24
24
  full: {
25
25
  label: "unrestricted",
26
26
  short: "unrestricted",
27
- description: "bypass runtime approvals and sandboxing; use only in a trusted environment",
27
+ description: "bypass approvals and sandbox; trusted use only",
28
28
  },
29
29
  },
30
30
  ko: {
31
31
  read: {
32
32
  label: "읽기 전용",
33
33
  short: "조회만",
34
- description: "파일을 읽고 판단하지만 런타임 도구가 작업 공간을 변경할 수 없음",
34
+ description: "조회만; 작업 공간 쓰기 차단",
35
35
  },
36
36
  write: {
37
37
  label: "작업 공간 쓰기",
38
38
  short: "작업 공간 편집",
39
- description: "런타임 샌드박스 안에서 현재 작업 공간을 읽고 편집",
39
+ description: "작업 공간·런타임 임시 경로 쓰기; 외부 MCP 꺼짐",
40
40
  },
41
41
  full: {
42
42
  label: "무제한 권한",
43
43
  short: "무제한",
44
- description: "런타임 승인과 샌드박스를 우회함; 신뢰할 있는 환경에서만 사용",
44
+ description: "승인과 샌드박스를 우회; 신뢰 환경 전용",
45
45
  },
46
46
  },
47
47
  };
@@ -52,6 +52,13 @@ function normalize(value, fallback = "read") {
52
52
  return LEVELS.includes(fallback) ? fallback : "read";
53
53
  }
54
54
 
55
+ function persistent(value, fallback = "write") {
56
+ const level = normalize(value, fallback);
57
+ if (level !== "full") return level;
58
+ const safeFallback = normalize(fallback, "write");
59
+ return safeFallback === "full" ? "write" : safeFallback;
60
+ }
61
+
55
62
  function next(value) {
56
63
  const current = normalize(value);
57
64
  return LEVELS[(LEVELS.indexOf(current) + 1) % LEVELS.length];
@@ -87,4 +94,4 @@ function createCycleController(options = {}) {
87
94
  };
88
95
  }
89
96
 
90
- module.exports = { LEVELS, normalize, next, copy, createCycleController };
97
+ module.exports = { LEVELS, normalize, persistent, next, copy, createCycleController };
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+
3
+ // Terminal processes may share the Desktop application's SQLite file. Keep
4
+ // lock waits bounded, and acquire writer authority before a transaction reads
5
+ // state that it may subsequently update. A deferred read-to-write upgrade can
6
+ // fail immediately with SQLITE_BUSY even when busy_timeout is configured.
7
+ const SQLITE_BUSY_TIMEOUT_MS = 15_000;
8
+
9
+ function configureSqliteConnection(db) {
10
+ if (!db) throw new TypeError("SQLite connection is required");
11
+ if (typeof db.pragma === "function") {
12
+ db.pragma(`busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);
13
+ } else if (typeof db.exec === "function") {
14
+ db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);
15
+ } else {
16
+ throw new TypeError("SQLite connection must expose pragma() or exec()");
17
+ }
18
+ return db;
19
+ }
20
+
21
+ function runWriteTransaction(db, fn, ...args) {
22
+ if (!db || typeof db.transaction !== "function") return fn(...args);
23
+ const transaction = db.transaction(fn);
24
+ if (typeof transaction.immediate === "function") {
25
+ return transaction.immediate(...args);
26
+ }
27
+ return transaction(...args);
28
+ }
29
+
30
+ module.exports = {
31
+ SQLITE_BUSY_TIMEOUT_MS,
32
+ configureSqliteConnection,
33
+ runWriteTransaction,
34
+ };