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
package/bin/agentlas.cjs CHANGED
@@ -22,6 +22,7 @@ const fs = require("node:fs");
22
22
  const os = require("node:os");
23
23
  const path = require("node:path");
24
24
  const { spawn } = require("node:child_process");
25
+ const { configureSqliteConnection } = require("../engine/agentlas-sqlite-policy.cjs");
25
26
 
26
27
  const REAL_SELF = (() => {
27
28
  try { return fs.realpathSync(__filename); } catch { return __filename; }
@@ -47,6 +48,11 @@ function dbPath() {
47
48
  return path.join(userDataDir(), "agentlas.sqlite");
48
49
  }
49
50
 
51
+ function securePrivateMode(target, mode) {
52
+ if (process.platform === "win32") return;
53
+ fs.chmodSync(target, mode);
54
+ }
55
+
50
56
  // ── SQLite 로더 (부트스트랩용): better-sqlite3 → node:sqlite ──
51
57
  // require.resolve가 아니라 실제 로드로 판별한다 — ABI가 깨진 better-sqlite3나
52
58
  // node:sqlite가 아직 없는/플래그가 필요한 Node(≤22.4 등)를 정확히 걸러낸다.
@@ -90,6 +96,7 @@ function openSqlite(p) {
90
96
  try {
91
97
  const Database = require("better-sqlite3");
92
98
  const db = new Database(p);
99
+ configureSqliteConnection(db);
93
100
  return { exec: (sql) => db.exec(sql), close: () => db.close(), driver: "better-sqlite3" };
94
101
  } catch { /* optional dep 미설치/ABI 불일치 */ }
95
102
  const major = Number(process.versions.node.split(".")[0]);
@@ -98,6 +105,7 @@ function openSqlite(p) {
98
105
  }
99
106
  const { DatabaseSync } = loadNodeSqliteQuietly();
100
107
  const db = new DatabaseSync(p);
108
+ configureSqliteConnection(db);
101
109
  return { exec: (sql) => db.exec(sql), close: () => db.close(), driver: "node:sqlite" };
102
110
  }
103
111
 
@@ -106,12 +114,17 @@ function openSqlite(p) {
106
114
  // 스키마가 생기면 엔진의 seedBuiltins가 빌트인 에이전트를 채운다.)
107
115
  function bootstrapDbIfMissing() {
108
116
  const p = dbPath();
109
- if (exists(p)) return { created: false, path: p };
117
+ const dir = path.dirname(p);
118
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
119
+ securePrivateMode(dir, 0o700);
120
+ if (exists(p)) {
121
+ securePrivateMode(p, 0o600);
122
+ return { created: false, path: p };
123
+ }
110
124
  const schemaFile = path.join(PKG_ROOT, "engine", "bootstrap-schema.sql");
111
125
  if (!exists(schemaFile)) {
112
126
  throw new Error(`Bootstrap schema not found: ${schemaFile}`);
113
127
  }
114
- fs.mkdirSync(path.dirname(p), { recursive: true });
115
128
  const sql = fs.readFileSync(schemaFile, "utf8");
116
129
  // DB를 정식 경로에서 직접 만들면 두 첫 실행이 모두 exists=false를 본 뒤 한 프로세스의
117
130
  // 실패 cleanup이 다른 프로세스의 정상 DB까지 지울 수 있다. 각자 같은 볼륨의 임시 DB를
@@ -126,6 +139,9 @@ function bootstrapDbIfMissing() {
126
139
  throw new Error(`Database bootstrap failed: ${e.message}`);
127
140
  }
128
141
  db.close();
142
+ // chmod the inode before it can become visible at the final hard-link path.
143
+ // The winning and losing processes both harden the final path below.
144
+ securePrivateMode(temp, 0o600);
129
145
  let created = false;
130
146
  try {
131
147
  fs.linkSync(temp, p);
@@ -140,11 +156,16 @@ function bootstrapDbIfMissing() {
140
156
  try { fs.rmSync(temp + "-wal", { force: true }); } catch { /* noop */ }
141
157
  try { fs.rmSync(temp + "-shm", { force: true }); } catch { /* noop */ }
142
158
  }
159
+ securePrivateMode(p, 0o600);
143
160
  return { created, path: p };
144
161
  }
145
162
 
146
163
  function main() {
147
164
  const args = process.argv.slice(2);
165
+ const metadataOnly = args.some((arg) => arg === "help" || arg === "--help" || arg === "-h")
166
+ || args[0] === "version"
167
+ || args[0] === "--version"
168
+ || args[0] === "-V";
148
169
  const sqliteDriver = probeSqliteDriver();
149
170
  const engineFound = exists(ENGINE);
150
171
 
@@ -175,14 +196,16 @@ function main() {
175
196
  process.exit(1);
176
197
  }
177
198
 
178
- try {
179
- const boot = bootstrapDbIfMissing();
180
- if (boot.created) {
181
- process.stderr.write(`First run: initialized Agentlas data (${boot.path})\n`);
199
+ if (!metadataOnly) {
200
+ try {
201
+ const boot = bootstrapDbIfMissing();
202
+ if (boot.created) {
203
+ process.stderr.write("First run: Agentlas data initialized.\n");
204
+ }
205
+ } catch (e) {
206
+ process.stderr.write(`${e.message}\n`);
207
+ process.exit(1);
182
208
  }
183
- } catch (e) {
184
- process.stderr.write(`${e.message}\n`);
185
- process.exit(1);
186
209
  }
187
210
 
188
211
  const child = spawn(process.execPath, [ENGINE, ...args], { stdio: "inherit" });
@@ -77,6 +77,8 @@ function sessionValues(ctx) {
77
77
  subject: ctx.subjectLabel || ui.t("composer.autoroute"),
78
78
  permission: permissions.copy(ctx.permission || "write", ui.lang).label,
79
79
  runtime: ctx.runtimeLabel || "—",
80
+ model: ctx.modelLabel || "auto",
81
+ effort: ctx.effortLabel || "auto",
80
82
  cwd: ctx.cwd ? shorten(ctx.cwd) : process.cwd(),
81
83
  };
82
84
  }
@@ -86,7 +88,14 @@ function renderStatusCard(ctx, opts = {}) {
86
88
  const c = ui.c;
87
89
  const value = sessionValues(ctx);
88
90
  const columns = ui.out.columns || 80;
89
- const labels = [ui.t("status.runtime"), ui.t("status.agent"), ui.t("status.permission"), ui.t("status.directory")].map((label) => `${label}:`);
91
+ const labels = [
92
+ ui.t("status.runtime"),
93
+ ui.t("status.model"),
94
+ ui.t("status.effort"),
95
+ ui.t("status.agent"),
96
+ ui.t("status.permission"),
97
+ ui.t("status.directory"),
98
+ ].map((label) => `${label}:`);
90
99
  const labelCells = Math.min(Math.max(...labels.map((label) => visWidth(label))) + 2, Math.max(12, Math.floor(columns * 0.42)));
91
100
  const line = (label, text) => {
92
101
  const labelText = `${label}:`;
@@ -97,6 +106,8 @@ function renderStatusCard(ctx, opts = {}) {
97
106
  ui.line("");
98
107
  ui.rule(ui.t("status.title"));
99
108
  line(ui.t("status.runtime"), value.runtime);
109
+ line(ui.t("status.model"), value.model);
110
+ line(ui.t("status.effort"), value.effort);
100
111
  line(ui.t("status.agent"), value.subject);
101
112
  line(ui.t("status.permission"), value.permission);
102
113
  line(ui.t("status.directory"), value.cwd);
@@ -107,9 +118,19 @@ function renderBanner(ctx) {
107
118
  const ui = ctx.ui;
108
119
  const c = ui.c;
109
120
  const value = sessionValues(ctx);
110
- const room = Math.max(20, (ui.out.columns || 80) - 4);
121
+ const columns = ui.out.columns || 80;
122
+ const room = Math.max(20, columns - 4);
123
+ const headlineRoom = Math.max(12, columns - 2);
124
+ const version = value.version ? " v" + value.version : "";
125
+ const separator = " · ";
126
+ const headline = `${WORDMARK_COMPACT}${version}${separator}${ui.t("banner.product")}`;
111
127
  ui.line("");
112
- ui.line(" " + c.bold(c.emerald(WORDMARK_COMPACT)) + (value.version ? c.faint(" v" + value.version) : "") + c.dim(" · " + ui.t("banner.product")));
128
+ if (visWidth(headline) <= headlineRoom) {
129
+ ui.line(" " + c.bold(c.emerald(WORDMARK_COMPACT)) + c.faint(version) + c.dim(separator + ui.t("banner.product")));
130
+ } else {
131
+ ui.line(" " + c.bold(c.emerald(WORDMARK_COMPACT)) + c.faint(version));
132
+ ui.line(" " + c.dim(ui.t("banner.product")));
133
+ }
113
134
  ui.line(" " + c.text(truncateWidth(ui.t("banner.session", value.runtime, value.subject, value.permission), room)));
114
135
  ui.line(" " + c.faint(truncateWidth(ui.t("banner.location", value.cwd), room)));
115
136
  ui.line("");
@@ -15,6 +15,9 @@
15
15
  const readline = require("node:readline");
16
16
  const i18n = require("./agentlas-i18n.cjs");
17
17
  const permissions = require("./agentlas-permissions.cjs");
18
+ const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
19
+ const MARK_RE = /\p{Mark}/u;
20
+ const EXTENDED_PICTOGRAPHIC_RE = /\p{Extended_Pictographic}/u;
18
21
 
19
22
  // East-Asian width: CJK / Hangul / Kana / fullwidth glyphs occupy 2 terminal cells.
20
23
  function isWide(cp) {
@@ -36,12 +39,54 @@ function isWide(cp) {
36
39
  function charWidth(ch) {
37
40
  const cp = ch.codePointAt(0);
38
41
  if (cp < 0x20) return 0;
42
+ if (
43
+ cp === 0x200d || // zero-width joiner
44
+ (cp >= 0xfe00 && cp <= 0xfe0f) || // variation selectors
45
+ (cp >= 0xe0100 && cp <= 0xe01ef) ||
46
+ (cp >= 0x1f3fb && cp <= 0x1f3ff) || // emoji skin tones
47
+ MARK_RE.test(ch)
48
+ ) return 0;
39
49
  return isWide(cp) ? 2 : 1;
40
50
  }
51
+ function graphemeSegments(value) {
52
+ return [...GRAPHEME_SEGMENTER.segment(String(value || ""))];
53
+ }
54
+ function graphemeWidth(segment) {
55
+ const text = String(segment || "");
56
+ if (
57
+ EXTENDED_PICTOGRAPHIC_RE.test(text) ||
58
+ /[\u{1f1e6}-\u{1f1ff}]/u.test(text) ||
59
+ text.includes("\u20e3")
60
+ ) return 2;
61
+ let width = 0;
62
+ for (const ch of text) width += charWidth(ch);
63
+ return width;
64
+ }
65
+ function previousGraphemeIndex(value, index) {
66
+ const text = String(value || "");
67
+ const cursor = Math.max(0, Math.min(Number(index) || 0, text.length));
68
+ let previous = 0;
69
+ for (const entry of GRAPHEME_SEGMENTER.segment(text)) {
70
+ const end = entry.index + entry.segment.length;
71
+ if (cursor <= entry.index) return previous;
72
+ if (cursor <= end) return entry.index;
73
+ previous = entry.index;
74
+ }
75
+ return previous;
76
+ }
77
+ function nextGraphemeIndex(value, index) {
78
+ const text = String(value || "");
79
+ const cursor = Math.max(0, Math.min(Number(index) || 0, text.length));
80
+ for (const entry of GRAPHEME_SEGMENTER.segment(text)) {
81
+ const end = entry.index + entry.segment.length;
82
+ if (cursor < end) return end;
83
+ }
84
+ return text.length;
85
+ }
41
86
  function visWidth(s) {
42
87
  const clean = String(s).replace(/\x1b\[[0-9;]*m/g, "");
43
88
  let n = 0;
44
- for (const ch of clean) n += charWidth(ch);
89
+ for (const entry of GRAPHEME_SEGMENTER.segment(clean)) n += graphemeWidth(entry.segment);
45
90
  return n;
46
91
  }
47
92
 
@@ -51,15 +96,81 @@ function truncateWidth(value, max) {
51
96
  let out = "";
52
97
  let width = 0;
53
98
  const room = Math.max(0, max - 1);
54
- for (const ch of text) {
55
- const cells = charWidth(ch);
99
+ for (const entry of GRAPHEME_SEGMENTER.segment(text)) {
100
+ const cells = graphemeWidth(entry.segment);
56
101
  if (width + cells > room) break;
57
- out += ch;
102
+ out += entry.segment;
58
103
  width += cells;
59
104
  }
60
105
  return out + "…";
61
106
  }
62
107
 
108
+ function splitWidth(value, max) {
109
+ const text = String(value || "");
110
+ const limit = Math.max(1, Math.floor(Number(max) || 1));
111
+ const lines = [];
112
+ let line = "";
113
+ let width = 0;
114
+ for (const entry of GRAPHEME_SEGMENTER.segment(text)) {
115
+ const cells = graphemeWidth(entry.segment);
116
+ if (line && width + cells > limit) {
117
+ lines.push(line);
118
+ line = "";
119
+ width = 0;
120
+ }
121
+ line += entry.segment;
122
+ width += cells;
123
+ }
124
+ if (line || !lines.length) lines.push(line);
125
+ return lines;
126
+ }
127
+
128
+ function wrapWidth(value, max) {
129
+ const limit = Math.max(2, Math.floor(Number(max) || 2));
130
+ const lines = [];
131
+ const pushWord = (word, state) => {
132
+ if (!word) return;
133
+ const cells = visWidth(word);
134
+ if (state.line && state.width + 1 + cells <= limit) {
135
+ state.line += " " + word;
136
+ state.width += 1 + cells;
137
+ return;
138
+ }
139
+ if (state.line) {
140
+ lines.push(state.line);
141
+ state.line = "";
142
+ state.width = 0;
143
+ }
144
+ if (cells <= limit) {
145
+ state.line = word;
146
+ state.width = cells;
147
+ return;
148
+ }
149
+ let chunk = "";
150
+ let chunkWidth = 0;
151
+ for (const entry of GRAPHEME_SEGMENTER.segment(word)) {
152
+ const width = graphemeWidth(entry.segment);
153
+ if (chunk && chunkWidth + width > limit) {
154
+ lines.push(chunk);
155
+ chunk = "";
156
+ chunkWidth = 0;
157
+ }
158
+ chunk += entry.segment;
159
+ chunkWidth += width;
160
+ }
161
+ state.line = chunk;
162
+ state.width = chunkWidth;
163
+ };
164
+ const paragraphs = String(value || "").split(/\r?\n/);
165
+ paragraphs.forEach((paragraph) => {
166
+ const state = { line: "", width: 0 };
167
+ for (const word of paragraph.trim().split(/\s+/u).filter(Boolean)) pushWord(word, state);
168
+ if (state.line) lines.push(state.line);
169
+ else if (!paragraph.trim()) lines.push("");
170
+ });
171
+ return lines.length ? lines : [""];
172
+ }
173
+
63
174
  function identityPalette() {
64
175
  const id = (value) => String(value);
65
176
  return { faint: id, emerald: id, text: id, paw: id, amber: id, blue: id, dim: id, inverse: id };
@@ -82,18 +193,18 @@ function buildComposerFrame(state, ctx = {}, palette, width = 80) {
82
193
 
83
194
  // horizontal scroll by visual width — keep the cursor visible (CJK-safe)
84
195
  let start = Math.min(state.scroll, state.cur);
85
- while (start < state.cur && visWidth(state.buf.slice(start, state.cur)) > fieldW - 2) start++;
196
+ while (start < state.cur && visWidth(state.buf.slice(start, state.cur)) > fieldW - 2) {
197
+ start = nextGraphemeIndex(state.buf, start);
198
+ }
86
199
  state.scroll = start;
87
200
 
88
201
  let shown = "";
89
202
  let shownWidth = 0;
90
- for (let i = start; i < state.buf.length; ) {
91
- const ch = state.buf.codePointAt(i) > 0xffff ? state.buf.slice(i, i + 2) : state.buf[i];
92
- const cw = charWidth(ch);
203
+ for (const entry of GRAPHEME_SEGMENTER.segment(state.buf.slice(start))) {
204
+ const cw = graphemeWidth(entry.segment);
93
205
  if (shownWidth + cw > fieldW - 2) break;
94
- shown += ch;
206
+ shown += entry.segment;
95
207
  shownWidth += cw;
96
- i += ch.length;
97
208
  }
98
209
 
99
210
  const prefix = (ctx.glyph || "›") + " ";
@@ -113,6 +224,7 @@ function buildComposerFrame(state, ctx = {}, palette, width = 80) {
113
224
  // 연결 LLM 세션 사용량 상시 표시줄 — 입력박스 바로 아래에 항상 유지된다.
114
225
  const usageText = typeof ctx.usage === "function" ? ctx.usage() : ctx.usage;
115
226
  if (usageText) lines.push(c.faint(truncateWidth(String(usageText), w)));
227
+ if (state.notice) lines.push(c.faint(truncateWidth(String(state.notice), w)));
116
228
  if (ctx.confirmation) {
117
229
  const prefix = ctx.confirmationTone === "danger" ? "! " : "✓ ";
118
230
  const paint = ctx.confirmationTone === "danger" && c.paw ? c.paw : c.green || c.emerald || ((value) => value);
@@ -120,7 +232,15 @@ function buildComposerFrame(state, ctx = {}, palette, width = 80) {
120
232
  }
121
233
 
122
234
  const rows = state.suggest || [];
123
- rows.slice(0, 8).forEach((row, index) => {
235
+ const terminalRows = Math.max(5, Number(ctx.rows) || 24);
236
+ const maxSuggestions = Math.min(8, Math.max(0, terminalRows - lines.length - 1));
237
+ const selected = Math.max(0, Math.min(Number(state.suggestSel) || 0, Math.max(0, rows.length - 1)));
238
+ const startRow = Math.min(
239
+ Math.max(0, selected - maxSuggestions + 1),
240
+ Math.max(0, rows.length - maxSuggestions),
241
+ );
242
+ rows.slice(startRow, startRow + maxSuggestions).forEach((row, offset) => {
243
+ const index = startRow + offset;
124
244
  const cmd = String(row.command || "").padEnd(16);
125
245
  const desc = String(row.description || "");
126
246
  const descRoom = Math.max(0, w - visWidth(" " + cmd + " "));
@@ -128,7 +248,9 @@ function buildComposerFrame(state, ctx = {}, palette, width = 80) {
128
248
  const label = " " + cmd + " " + clippedDesc;
129
249
  lines.push(index === state.suggestSel ? c.inverse(label) : " " + c.blue(cmd) + " " + c.dim(clippedDesc));
130
250
  });
131
- if (rows.length) lines.push(c.faint(" " + i18n.t(ctx.lang || "en", "palette.controls")));
251
+ if (rows.length && lines.length < terminalRows) {
252
+ lines.push(c.faint(" " + i18n.t(ctx.lang || "en", "palette.controls")));
253
+ }
132
254
 
133
255
  return { lines, curCol: visWidth(prefix) + visWidth(state.buf.slice(start, state.cur)) };
134
256
  }
@@ -140,7 +262,10 @@ function createComposer(opts) {
140
262
  const c = ui.c;
141
263
  const loadHistory = opts.loadHistory || (() => []);
142
264
  const saveHistory = opts.saveHistory || (() => {});
265
+ const getHistoryScope = opts.getHistoryScope || (() => "");
266
+ let loadedHistoryScope = String(getHistoryScope() || "");
143
267
  let history = (loadHistory() || []).filter((x) => typeof x === "string"); // index 0 = most recent
268
+ let idleExitArmedUntil = 0;
144
269
 
145
270
  function cols() {
146
271
  return Math.max(30, out.columns || process.stdout.columns || 80);
@@ -151,7 +276,12 @@ function createComposer(opts) {
151
276
 
152
277
  // Build the rendered block (array of lines) + the cursor target column on the input line.
153
278
  function frame(state, ctx) {
154
- return buildComposerFrame(state, ctx, c, boxWidth());
279
+ return buildComposerFrame(
280
+ state,
281
+ { ...ctx, rows: out.rows || process.stdout.rows || 24 },
282
+ c,
283
+ boxWidth(),
284
+ );
155
285
  }
156
286
 
157
287
  function render(state, ctx) {
@@ -176,8 +306,14 @@ function createComposer(opts) {
176
306
 
177
307
  function read(ctx) {
178
308
  ctx = ctx || {};
309
+ const nextHistoryScope = String(getHistoryScope() || "");
310
+ if (nextHistoryScope !== loadedHistoryScope) {
311
+ history = (loadHistory() || []).filter((x) => typeof x === "string");
312
+ loadedHistoryScope = nextHistoryScope;
313
+ }
179
314
  return new Promise((resolve) => {
180
- const state = { buf: "", cur: 0, scroll: 0, drawn: 0, suggest: [], suggestSel: 0, hist: -1, stash: "", dismissed: null };
315
+ const state = { buf: "", cur: 0, scroll: 0, drawn: 0, suggest: [], suggestSel: 0, hist: -1, stash: "", dismissed: null, notice: null };
316
+ let scheduledDraw = null;
181
317
 
182
318
  function refreshSuggest() {
183
319
  if (ctx.suggest && state.buf !== state.dismissed) {
@@ -188,26 +324,43 @@ function createComposer(opts) {
188
324
  if (state.suggestSel >= state.suggest.length) state.suggestSel = 0;
189
325
  }
190
326
  function draw() {
327
+ if (scheduledDraw) {
328
+ clearImmediate(scheduledDraw);
329
+ scheduledDraw = null;
330
+ }
191
331
  refreshSuggest();
192
332
  render(state, ctx);
193
333
  }
334
+ function drawSoon() {
335
+ if (scheduledDraw) return;
336
+ scheduledDraw = setImmediate(() => {
337
+ scheduledDraw = null;
338
+ refreshSuggest();
339
+ render(state, ctx);
340
+ });
341
+ }
194
342
 
195
343
  const wasRaw = !!inp.isRaw;
196
344
  try { if (inp.setRawMode) inp.setRawMode(true); } catch { /* ignore */ }
197
345
  readline.emitKeypressEvents(inp);
198
346
  inp.resume();
199
347
 
200
- let ctrlc = 0;
201
348
  function done(result) {
349
+ if (scheduledDraw) {
350
+ clearImmediate(scheduledDraw);
351
+ scheduledDraw = null;
352
+ }
353
+ if (typeof out.removeListener === "function") out.removeListener("resize", drawSoon);
202
354
  inp.removeListener("keypress", onKey);
203
355
  try { if (inp.setRawMode) inp.setRawMode(wasRaw); } catch { /* ignore */ }
204
356
  resolve(result);
205
357
  }
206
- function setBuf(s, cur) {
358
+ function setBuf(s, cur, deferDraw = false) {
207
359
  state.buf = s;
208
360
  state.cur = cur == null ? s.length : Math.max(0, Math.min(cur, s.length));
209
361
  state.dismissed = null;
210
- draw();
362
+ if (deferDraw) drawSoon();
363
+ else draw();
211
364
  }
212
365
  function submit() {
213
366
  const value = state.buf;
@@ -225,6 +378,17 @@ function createComposer(opts) {
225
378
  key = key || {};
226
379
  const name = key.name;
227
380
  const shiftTab = name === "tab" && key.shift;
381
+ // Node's keypress decoder keeps a lone Escape open briefly in case it
382
+ // starts a longer sequence. If Ctrl-C arrives in that window, the
383
+ // event can be reported as meta-C with sequence ESC+ETX and
384
+ // `key.ctrl === false`. ETX still means cancel; never leave the prior
385
+ // buffer armed for the next submitted command.
386
+ const keySequence = String(key.sequence ?? str ?? "");
387
+ const ctrlC = (key.ctrl && name === "c") || keySequence.includes("\x03");
388
+ if (!ctrlC) {
389
+ state.notice = null;
390
+ idleExitArmedUntil = 0;
391
+ }
228
392
 
229
393
  if (!shiftTab && ctx.onPermissionCycleCancel) {
230
394
  const hadConfirmation = Boolean(ctx.confirmation);
@@ -233,12 +397,21 @@ function createComposer(opts) {
233
397
  if (hadConfirmation && !ctx.confirmation) draw();
234
398
  }
235
399
 
236
- if (key.ctrl && name === "c") {
237
- if (state.buf.length) { ctrlc = 0; return setBuf("", 0); }
400
+ if (ctrlC) {
401
+ if (ctx.continuation) {
402
+ clearBox(state);
403
+ return done({ cancel: true });
404
+ }
405
+ if (state.buf.length) { idleExitArmedUntil = 0; return setBuf("", 0); }
238
406
  const now = Date.now();
239
- if (now < ctrlc) { clearBox(state); return done({ exit: true }); }
240
- ctrlc = now + 1500;
241
- return;
407
+ if (now < idleExitArmedUntil) {
408
+ idleExitArmedUntil = 0;
409
+ clearBox(state);
410
+ return done({ exit: true });
411
+ }
412
+ idleExitArmedUntil = now + 3000;
413
+ state.notice = i18n.t(ctx.lang || "en", "ctrlcAgain");
414
+ return draw();
242
415
  }
243
416
  if (key.ctrl && name === "d") {
244
417
  if (!state.buf.length) { clearBox(state); return done({ eof: true }); }
@@ -250,19 +423,37 @@ function createComposer(opts) {
250
423
  return setBuf("", 0);
251
424
  }
252
425
  if (name === "backspace" || (key.ctrl && name === "h")) {
253
- if (state.cur > 0) setBuf(state.buf.slice(0, state.cur - 1) + state.buf.slice(state.cur), state.cur - 1);
426
+ if (state.cur > 0) {
427
+ const previous = previousGraphemeIndex(state.buf, state.cur);
428
+ setBuf(state.buf.slice(0, previous) + state.buf.slice(state.cur), previous, true);
429
+ }
430
+ return;
431
+ }
432
+ if (name === "delete") {
433
+ const next = nextGraphemeIndex(state.buf, state.cur);
434
+ return setBuf(state.buf.slice(0, state.cur) + state.buf.slice(next), state.cur, true);
435
+ }
436
+ if (name === "left") {
437
+ if (state.cur > 0) {
438
+ state.cur = previousGraphemeIndex(state.buf, state.cur);
439
+ draw();
440
+ }
441
+ return;
442
+ }
443
+ if (name === "right") {
444
+ if (state.cur < state.buf.length) {
445
+ state.cur = nextGraphemeIndex(state.buf, state.cur);
446
+ draw();
447
+ }
254
448
  return;
255
449
  }
256
- if (name === "delete") return setBuf(state.buf.slice(0, state.cur) + state.buf.slice(state.cur + 1), state.cur);
257
- if (name === "left") { if (state.cur > 0) { state.cur--; draw(); } return; }
258
- if (name === "right") { if (state.cur < state.buf.length) { state.cur++; draw(); } return; }
259
450
  if (name === "home" || (key.ctrl && name === "a")) { state.cur = 0; draw(); return; }
260
451
  if (name === "end" || (key.ctrl && name === "e")) { state.cur = state.buf.length; draw(); return; }
261
- if (key.ctrl && name === "u") return setBuf(state.buf.slice(state.cur), 0);
262
- if (key.ctrl && name === "k") return setBuf(state.buf.slice(0, state.cur), state.cur);
452
+ if (key.ctrl && name === "u") return setBuf(state.buf.slice(state.cur), 0, true);
453
+ if (key.ctrl && name === "k") return setBuf(state.buf.slice(0, state.cur), state.cur, true);
263
454
  if (key.ctrl && name === "w") {
264
455
  const left = state.buf.slice(0, state.cur).replace(/\s*\S+\s*$/, "");
265
- return setBuf(left + state.buf.slice(state.cur), left.length);
456
+ return setBuf(left + state.buf.slice(state.cur), left.length, true);
266
457
  }
267
458
  if (name === "up") {
268
459
  if (state.suggest.length) { state.suggestSel = (state.suggestSel - 1 + state.suggest.length) % state.suggest.length; state.buf = state.suggest[state.suggestSel].command; state.cur = state.buf.length; return render(state, ctx); }
@@ -296,7 +487,7 @@ function createComposer(opts) {
296
487
  // printable insert (single char or paste). Strip control/newlines → single-line field.
297
488
  if (str && !key.ctrl && !key.meta) {
298
489
  const text = String(str).replace(/[\r\n\t]+/g, " ").replace(/[\x00-\x1f]/g, "");
299
- if (text) return setBuf(state.buf.slice(0, state.cur) + text + state.buf.slice(state.cur), state.cur + text.length);
490
+ if (text) return setBuf(state.buf.slice(0, state.cur) + text + state.buf.slice(state.cur), state.cur + text.length, true);
300
491
  }
301
492
  }
302
493
 
@@ -310,10 +501,18 @@ function createComposer(opts) {
310
501
  const v = i === -1 ? state.stash : history[i];
311
502
  state.buf = v;
312
503
  state.cur = v.length;
504
+ // A recalled slash command is history, not a newly opened palette.
505
+ // Keep Up/Down on history until the user edits the recalled value.
506
+ state.dismissed = v;
313
507
  draw();
314
508
  }
315
509
 
316
510
  inp.on("keypress", onKey);
511
+ // stdout emits `resize` when the real terminal changes dimensions. The
512
+ // composer owns the visible frame while read() is pending, so repaint it
513
+ // immediately instead of leaving a stale soft-wrapped footer until the
514
+ // next keypress.
515
+ if (typeof out.on === "function") out.on("resize", drawSoon);
317
516
  draw();
318
517
  });
319
518
  }
@@ -321,4 +520,13 @@ function createComposer(opts) {
321
520
  return { read, setHistory: (h) => { history = (h || []).filter((x) => typeof x === "string"); } };
322
521
  }
323
522
 
324
- module.exports = { createComposer, visWidth, buildComposerFrame, truncateWidth };
523
+ module.exports = {
524
+ createComposer,
525
+ visWidth,
526
+ buildComposerFrame,
527
+ truncateWidth,
528
+ splitWidth,
529
+ wrapWidth,
530
+ previousGraphemeIndex,
531
+ nextGraphemeIndex,
532
+ };