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.
- package/CHANGELOG.md +106 -0
- package/README.md +319 -341
- package/bin/agentlas.cjs +32 -9
- package/engine/agentlas-banner.cjs +24 -3
- package/engine/agentlas-composer.cjs +239 -31
- package/engine/agentlas-config.cjs +103 -7
- package/engine/agentlas-core-harness.cjs +48 -4
- package/engine/agentlas-evolution.cjs +3 -4
- package/engine/agentlas-i18n.cjs +88 -32
- package/engine/agentlas-input.cjs +234 -18
- package/engine/agentlas-memory-governance.cjs +3 -5
- package/engine/agentlas-memory-import.cjs +3 -3
- package/engine/agentlas-native-host.cjs +200 -9
- package/engine/agentlas-onboard.cjs +112 -20
- package/engine/agentlas-permissions.cjs +14 -7
- package/engine/agentlas-sqlite-policy.cjs +34 -0
- package/engine/agentlas-ui.cjs +93 -42
- package/engine/agentlas-workforce.cjs +472 -58
- package/engine/agentlas-workload-routing.cjs +8 -3
- package/engine/agentlas.cjs +140 -12367
- package/engine/agents/files.cjs +61 -0
- package/engine/agents/import-local.cjs +237 -0
- package/engine/agents/registry.cjs +158 -0
- package/engine/agents/router.cjs +565 -0
- package/engine/agents/routes.cjs +43 -0
- package/engine/architecture.data.json +2 -2
- package/engine/automation/daemon.cjs +335 -0
- package/engine/automation/schedule.cjs +181 -0
- package/engine/automation/store.cjs +209 -0
- package/engine/cloud/auth.cjs +279 -0
- package/engine/cloud/hub-client.cjs +239 -0
- package/engine/cloud-assets/cargo.cjs +49 -0
- package/engine/cloud-assets/cas.cjs +235 -0
- package/engine/cloud-assets/commands.cjs +273 -0
- package/engine/cloud-assets/package.cjs +936 -0
- package/engine/cloud-assets/restore.cjs +172 -0
- package/engine/cloud-assets/state.cjs +268 -0
- package/engine/commands/automation.cjs +195 -0
- package/engine/commands/billing.cjs +96 -0
- package/engine/commands/browser.cjs +20 -0
- package/engine/commands/build.cjs +33 -0
- package/engine/commands/call.cjs +24 -0
- package/engine/commands/career-graph.cjs +51 -0
- package/engine/commands/cd.cjs +22 -0
- package/engine/commands/chat.cjs +12 -0
- package/engine/commands/chats.cjs +28 -0
- package/engine/commands/cloud.cjs +17 -0
- package/engine/commands/connect.cjs +20 -0
- package/engine/commands/context.cjs +66 -0
- package/engine/commands/creds.cjs +203 -0
- package/engine/commands/doctor.cjs +68 -0
- package/engine/commands/env.cjs +33 -0
- package/engine/commands/evolve.cjs +23 -0
- package/engine/commands/experience.cjs +34 -0
- package/engine/commands/film.cjs +8 -0
- package/engine/commands/firm.cjs +115 -0
- package/engine/commands/help.cjs +65 -0
- package/engine/commands/hep.cjs +23 -0
- package/engine/commands/import.cjs +35 -0
- package/engine/commands/index.cjs +136 -0
- package/engine/commands/install.cjs +27 -0
- package/engine/commands/journal.cjs +31 -0
- package/engine/commands/legacy-network.cjs +29 -0
- package/engine/commands/list.cjs +49 -0
- package/engine/commands/login.cjs +68 -0
- package/engine/commands/logout.cjs +28 -0
- package/engine/commands/mcp.cjs +91 -0
- package/engine/commands/memory.cjs +21 -0
- package/engine/commands/multimodal.cjs +91 -0
- package/engine/commands/native.cjs +34 -0
- package/engine/commands/netadmin.cjs +31 -0
- package/engine/commands/oberon.cjs +70 -0
- package/engine/commands/ontology.cjs +24 -0
- package/engine/commands/open.cjs +48 -0
- package/engine/commands/plugin.cjs +101 -0
- package/engine/commands/project.cjs +47 -0
- package/engine/commands/research.cjs +36 -0
- package/engine/commands/route.cjs +37 -0
- package/engine/commands/run.cjs +149 -0
- package/engine/commands/search.cjs +55 -0
- package/engine/commands/setup.cjs +45 -0
- package/engine/commands/storm.cjs +75 -0
- package/engine/commands/swarm.cjs +75 -0
- package/engine/commands/telegram.cjs +32 -0
- package/engine/commands/uninstall.cjs +68 -0
- package/engine/commands/update.cjs +54 -0
- package/engine/commands/upload.cjs +18 -0
- package/engine/commands/usage.cjs +35 -0
- package/engine/commands/variant.cjs +25 -0
- package/engine/commands/version.cjs +9 -0
- package/engine/commands/whoami.cjs +38 -0
- package/engine/commands/workforce.cjs +103 -0
- package/engine/core/db.cjs +160 -0
- package/engine/core/paths.cjs +35 -0
- package/engine/experience/build.cjs +181 -0
- package/engine/experience/intents.cjs +492 -0
- package/engine/experience/runtime.cjs +242 -0
- package/engine/experience/variant.cjs +196 -0
- package/engine/firms/orchestrate.cjs +333 -0
- package/engine/hephaestus/runtime.cjs +697 -0
- package/engine/hub/install.cjs +872 -0
- package/engine/hub/plugins.cjs +213 -0
- package/engine/mcp/consent.cjs +289 -0
- package/engine/mcp/contract.cjs +202 -0
- package/engine/mcp/index.cjs +43 -0
- package/engine/mcp/inventory.cjs +322 -0
- package/engine/mcp/plan.cjs +286 -0
- package/engine/mcp/probe.cjs +151 -0
- package/engine/memory-cli/curate.cjs +163 -0
- package/engine/oberon/common.cjs +69 -0
- package/engine/oberon/manifest.cjs +164 -0
- package/engine/oberon/outputs.cjs +70 -0
- package/engine/oberon/render.cjs +164 -0
- package/engine/project/career-graph.cjs +249 -0
- package/engine/project/credentials.cjs +262 -0
- package/engine/project/env-file.cjs +46 -0
- package/engine/project/index.cjs +27 -0
- package/engine/project/memory-context.cjs +453 -0
- package/engine/project/ontology.cjs +467 -0
- package/engine/project/paths.cjs +39 -0
- package/engine/project/seed.cjs +200 -0
- package/engine/project/state.cjs +403 -0
- package/engine/project/super-ontology-seed.json +3288 -0
- package/engine/runtimes/detect.cjs +54 -0
- package/engine/runtimes/overrides.cjs +139 -0
- package/engine/runtimes/resolve.cjs +64 -0
- package/engine/sessions/apply-fences.cjs +188 -0
- package/engine/sessions/fences.cjs +362 -0
- package/engine/sessions/orchestrator.cjs +170 -0
- package/engine/sessions/prompt.cjs +212 -0
- package/engine/sessions/session.cjs +245 -0
- package/engine/sessions/sink.cjs +54 -0
- package/engine/sessions/store.cjs +79 -0
- package/engine/storm/deps.cjs +88 -0
- package/engine/storm/storm.cjs +218 -0
- package/engine/storm/swarm.cjs +422 -0
- package/engine/ui/palette.cjs +105 -0
- package/engine/ui/renderer.cjs +85 -0
- package/engine/ui/repl.cjs +444 -0
- package/engine/workforce/capture.cjs +701 -0
- package/engine/workforce/deps.cjs +472 -0
- package/package.json +2 -6
- package/engine/agentlas-experience-mcp.cjs +0 -1709
- package/engine/agentlas-parity.cjs +0 -1499
- 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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 = [
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
55
|
-
const cells =
|
|
99
|
+
for (const entry of GRAPHEME_SEGMENTER.segment(text)) {
|
|
100
|
+
const cells = graphemeWidth(entry.segment);
|
|
56
101
|
if (width + cells > room) break;
|
|
57
|
-
out +=
|
|
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)
|
|
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 (
|
|
91
|
-
const
|
|
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 +=
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
237
|
-
if (
|
|
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 <
|
|
240
|
-
|
|
241
|
-
|
|
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)
|
|
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 = {
|
|
523
|
+
module.exports = {
|
|
524
|
+
createComposer,
|
|
525
|
+
visWidth,
|
|
526
|
+
buildComposerFrame,
|
|
527
|
+
truncateWidth,
|
|
528
|
+
splitWidth,
|
|
529
|
+
wrapWidth,
|
|
530
|
+
previousGraphemeIndex,
|
|
531
|
+
nextGraphemeIndex,
|
|
532
|
+
};
|