agentlas 1.0.61 → 1.0.63
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 +24 -1
- package/engine/acp/server.cjs +47 -17
- package/engine/agentlas-evolution.cjs +85 -36
- package/engine/agentlas-experience-intake.cjs +21 -8
- package/engine/agentlas.cjs +6 -3
- package/engine/automation/daemon.cjs +36 -3
- package/engine/automation/store.cjs +13 -11
- package/engine/bootstrap-schema.sql +30 -2
- package/engine/cloud/auth.cjs +34 -8
- package/engine/commands/connect.cjs +34 -6
- package/engine/commands/oberon.cjs +1 -1
- package/engine/commands/update.cjs +28 -7
- package/engine/core/desktop-core-fetch.cjs +94 -21
- package/engine/hephaestus/local-core.cjs +44 -9
- package/engine/memory-cli/curate.cjs +5 -2
- package/engine/oberon/outputs.cjs +10 -3
- package/engine/project/career-graph.cjs +4 -2
- package/engine/project/memory-context.cjs +9 -6
- package/engine/project/ontology.cjs +17 -7
- package/engine/runtimes/acp-driver.cjs +42 -3
- package/engine/sessions/session.cjs +18 -0
- package/engine/telegram/connect.cjs +31 -10
- package/engine/ui/screens.cjs +30 -6
- package/engine/vendor/desktop-core.manifest.json +5 -5
- package/package.json +3 -1
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* 하위 명령:
|
|
14
14
|
* connect 상태 표
|
|
15
15
|
* connect telegram <agent|firm> 이 대상에 봇을 연결(토큰 stdin) + 방 페어링
|
|
16
|
+
* connect pair <id> 타임아웃된 방 페어링을 다시 기다림
|
|
16
17
|
* connect test <id> 연결된 방에 확인 메시지
|
|
17
18
|
* connect remove <id> 연결 제거(토큰 파일도 삭제)
|
|
18
19
|
*/
|
|
@@ -23,8 +24,8 @@ const readline = require("node:readline");
|
|
|
23
24
|
|
|
24
25
|
function usage(ko) {
|
|
25
26
|
return ko
|
|
26
|
-
? "사용법: agentlas connect [ status | telegram <agent|firm> [--auto] | test <id> | remove <id> ]"
|
|
27
|
-
: "Usage: agentlas connect [ status | telegram <agent|firm> [--auto] | test <id> | remove <id> ]";
|
|
27
|
+
? "사용법: agentlas connect [ status | telegram <agent|firm> [--auto] | pair <id> | test <id> | remove <id> ]"
|
|
28
|
+
: "Usage: agentlas connect [ status | telegram <agent|firm> [--auto] | pair <id> | test <id> | remove <id> ]";
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
function resolveTarget(db, token) {
|
|
@@ -111,8 +112,8 @@ async function connectTelegram(ctx, targetToken, { auto = false } = {}) {
|
|
|
111
112
|
const botAt = started.botUsername ? "@" + started.botUsername : "(bot)";
|
|
112
113
|
ctx.out(`${ctx.ui.green("✓")} ${ko ? "봇 확인됨" : "bot verified"}: ${botAt} → ${target.name}`);
|
|
113
114
|
ctx.out(ko
|
|
114
|
-
? `이제 텔레그램에서 ${botAt} 에게
|
|
115
|
-
: `Now
|
|
115
|
+
? `이제 텔레그램에서 ${botAt} 에게 정확히 \`/start ${started.id}\` 를 보내세요. 방을 기다립니다…`
|
|
116
|
+
: `Now send exactly \`/start ${started.id}\` to ${botAt} on Telegram. Waiting for the chat…`);
|
|
116
117
|
|
|
117
118
|
let paired;
|
|
118
119
|
try {
|
|
@@ -127,8 +128,8 @@ async function connectTelegram(ctx, targetToken, { auto = false } = {}) {
|
|
|
127
128
|
|
|
128
129
|
if (!paired) {
|
|
129
130
|
ctx.out(ctx.ui.dim(ko
|
|
130
|
-
? `방을 못 받았습니다(2분 초과). ${botAt} 에게
|
|
131
|
-
: `No chat received (2 min).
|
|
131
|
+
? `방을 못 받았습니다(2분 초과). ${botAt} 에게 \`/start ${started.id}\` 를 보낸 뒤 다시: agentlas connect pair ${started.id}`
|
|
132
|
+
: `No chat received (2 min). Send \`/start ${started.id}\` to ${botAt}, then retry: agentlas connect pair ${started.id}`));
|
|
132
133
|
return 0;
|
|
133
134
|
}
|
|
134
135
|
ctx.out(`${ctx.ui.green("✓")} ${ko ? "방 연결됨" : "chat paired"}: ${paired.telegram_chat_title || paired.telegram_chat_id}`);
|
|
@@ -152,6 +153,33 @@ async function run(ctx, args) {
|
|
|
152
153
|
if (!targetToken) { ctx.err(usage(ko)); return 1; }
|
|
153
154
|
return connectTelegram(ctx, targetToken, { auto });
|
|
154
155
|
}
|
|
156
|
+
if (sub === "pair") {
|
|
157
|
+
if (!args[1]) { ctx.err(ko ? "사용법: agentlas connect pair <id>" : "Usage: agentlas connect pair <id>"); return 1; }
|
|
158
|
+
const row = tg.getBinding(ctx.db(), args[1]);
|
|
159
|
+
if (!row) { ctx.err(ko ? "연결을 찾을 수 없습니다." : "connection not found."); return 1; }
|
|
160
|
+
if (row.telegram_chat_id) {
|
|
161
|
+
ctx.out(`${ctx.ui.green("✓")} ${ko ? "이미 방이 연결돼 있습니다" : "chat already paired"}: ${row.telegram_chat_title || row.telegram_chat_id}`);
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
ctx.out(ko
|
|
165
|
+
? `텔레그램에서 @${row.bot_username || "bot"} 에게 정확히 \`/start ${row.id}\` 를 보내세요.`
|
|
166
|
+
: `Send exactly \`/start ${row.id}\` to @${row.bot_username || "bot"} on Telegram.`);
|
|
167
|
+
try {
|
|
168
|
+
if (typeof ctx.ui.startSpinner === "function") ctx.ui.startSpinner(ko ? "방 연결 대기 중…" : "Waiting for the chat…");
|
|
169
|
+
const paired = await tg.pairByPolling(ctx.db(), row.id, { timeoutMs: 120_000 });
|
|
170
|
+
if (typeof ctx.ui.stopSpinner === "function") ctx.ui.stopSpinner();
|
|
171
|
+
if (!paired) {
|
|
172
|
+
ctx.err(ko ? "방을 못 받았습니다(2분 초과). 같은 명령으로 다시 기다릴 수 있습니다." : "No chat received (2 min). Run the same command to wait again.");
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
ctx.out(`${ctx.ui.green("✓")} ${ko ? "방 연결됨" : "chat paired"}: ${paired.telegram_chat_title || paired.telegram_chat_id}`);
|
|
176
|
+
return 0;
|
|
177
|
+
} catch (e) {
|
|
178
|
+
if (typeof ctx.ui.stopSpinner === "function") ctx.ui.stopSpinner();
|
|
179
|
+
ctx.err(`${ctx.ui.red("✖")} ${String((e && e.message) || e)}`);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
155
183
|
if (sub === "test") {
|
|
156
184
|
if (!args[1]) { ctx.err(ko ? "사용법: agentlas connect test <id>" : "Usage: agentlas connect test <id>"); return 1; }
|
|
157
185
|
try {
|
|
@@ -10,14 +10,35 @@
|
|
|
10
10
|
const { compareSemVer } = require("../semver.cjs");
|
|
11
11
|
const { readVersion } = require("../agentlas-banner.cjs");
|
|
12
12
|
|
|
13
|
-
async function checkNpmLatest({ fetch: fetchImpl } = {}) {
|
|
13
|
+
async function checkNpmLatest({ fetch: fetchImpl, timeoutMs = 8_000 } = {}) {
|
|
14
14
|
const f = fetchImpl || globalThis.fetch;
|
|
15
15
|
const currentVersion = readVersion();
|
|
16
16
|
let latestVersion = null;
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const boundedTimeoutMs = Math.min(60_000, Math.max(10, Number(timeoutMs) || 8_000));
|
|
19
|
+
let timeout;
|
|
17
20
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
// Bound both the connection and body parse. Some injected/custom fetch
|
|
22
|
+
// implementations ignore AbortSignal, so the explicit race is required to
|
|
23
|
+
// keep `agentlas update` from hanging the entire CLI indefinitely.
|
|
24
|
+
const request = Promise.resolve().then(async () => {
|
|
25
|
+
const resp = await f("https://registry.npmjs.org/agentlas/latest", {
|
|
26
|
+
headers: { accept: "application/json" },
|
|
27
|
+
signal: controller.signal,
|
|
28
|
+
});
|
|
29
|
+
return resp.ok ? String((await resp.json()).version || "") : null;
|
|
30
|
+
});
|
|
31
|
+
const expired = new Promise((_, reject) => {
|
|
32
|
+
timeout = setTimeout(() => {
|
|
33
|
+
const error = new Error(`npm update check timed out after ${boundedTimeoutMs}ms`);
|
|
34
|
+
error.code = "AGENTLAS_UPDATE_TIMEOUT";
|
|
35
|
+
try { controller.abort(error); } catch { controller.abort(); }
|
|
36
|
+
reject(error);
|
|
37
|
+
}, boundedTimeoutMs);
|
|
38
|
+
});
|
|
39
|
+
latestVersion = await Promise.race([request, expired]);
|
|
40
|
+
} catch { /* offline/timeout 등 — 호출부에서 안내 */ }
|
|
41
|
+
finally { if (timeout) clearTimeout(timeout); }
|
|
21
42
|
const comparison = latestVersion ? compareSemVer(currentVersion, latestVersion) : null;
|
|
22
43
|
return {
|
|
23
44
|
currentVersion,
|
|
@@ -27,9 +48,9 @@ async function checkNpmLatest({ fetch: fetchImpl } = {}) {
|
|
|
27
48
|
};
|
|
28
49
|
}
|
|
29
50
|
|
|
30
|
-
async function run(ctx, args) {
|
|
31
|
-
const json = args.includes("--json");
|
|
32
|
-
const status = await checkNpmLatest();
|
|
51
|
+
async function run(ctx, args, deps = {}) {
|
|
52
|
+
const json = ctx.output?.format === "json" || args.includes("--json");
|
|
53
|
+
const status = await checkNpmLatest(deps);
|
|
33
54
|
if (json) {
|
|
34
55
|
ctx.out(JSON.stringify(status, null, 2));
|
|
35
56
|
return 0;
|
|
@@ -27,7 +27,17 @@ function readManifest() {
|
|
|
27
27
|
try { return JSON.parse(fs.readFileSync(manifestPath(), "utf8")); } catch { return null; }
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function
|
|
30
|
+
function cacheRoot() { return path.join(userDataDir(), "desktop-core-cache"); }
|
|
31
|
+
function normalizedCacheVersion(version) {
|
|
32
|
+
const value = String(version ?? "").trim();
|
|
33
|
+
if (!value || value === "." || value === "..") return null;
|
|
34
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value) ? value : null;
|
|
35
|
+
}
|
|
36
|
+
function cacheDir(version) {
|
|
37
|
+
const normalized = normalizedCacheVersion(version);
|
|
38
|
+
if (!normalized) throw new TypeError("Desktop core manifest has an unsafe cache version");
|
|
39
|
+
return path.join(cacheRoot(), normalized);
|
|
40
|
+
}
|
|
31
41
|
function cacheDistDir(version) { return path.join(cacheDir(version), "dist"); }
|
|
32
42
|
|
|
33
43
|
function sha256File(file) {
|
|
@@ -38,13 +48,48 @@ function sha256File(file) {
|
|
|
38
48
|
|
|
39
49
|
/** 이미 캐시에 온전히 풀려 있으면 그 dist 경로, 아니면 null. */
|
|
40
50
|
function cachedCoreRoot(manifest = readManifest()) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
51
|
+
const version = normalizedCacheVersion(manifest?.version);
|
|
52
|
+
if (!version || !manifest?.sha256) return null;
|
|
53
|
+
const dist = cacheDistDir(version);
|
|
54
|
+
const marker = path.join(cacheDir(version), ".complete");
|
|
55
|
+
if (!fs.existsSync(path.join(dist, "electron", "workflow", "run-graph.js"))) return null;
|
|
56
|
+
try {
|
|
57
|
+
const completed = JSON.parse(fs.readFileSync(marker, "utf8"));
|
|
58
|
+
if (
|
|
59
|
+
completed?.schemaVersion === 2
|
|
60
|
+
&& String(completed.version) === version
|
|
61
|
+
&& completed.sha256 === manifest.sha256
|
|
62
|
+
) return dist;
|
|
63
|
+
} catch {
|
|
64
|
+
// Timestamp-only and malformed markers predate the content-bound cache
|
|
65
|
+
// contract. They must be refreshed instead of trusted as executable code.
|
|
66
|
+
}
|
|
45
67
|
return null;
|
|
46
68
|
}
|
|
47
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Remove only cache entries that are recognizably old engine downloads.
|
|
72
|
+
* Unknown files under the dedicated root are preserved: cleanup must never
|
|
73
|
+
* widen from a versioned engine cache into arbitrary user data.
|
|
74
|
+
*/
|
|
75
|
+
function pruneStaleCaches(keepVersion) {
|
|
76
|
+
const root = cacheRoot();
|
|
77
|
+
let removed = 0;
|
|
78
|
+
let entries = [];
|
|
79
|
+
try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return removed; }
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (!entry.isDirectory() || entry.name === String(keepVersion) || entry.name.includes(".partial-")) continue;
|
|
82
|
+
const dir = path.join(root, entry.name);
|
|
83
|
+
const recognizable = fs.existsSync(path.join(dir, ".complete"))
|
|
84
|
+
|| fs.existsSync(path.join(dir, "desktop-core.tar.gz"))
|
|
85
|
+
|| fs.existsSync(path.join(dir, "dist", "electron", "workflow", "run-graph.js"));
|
|
86
|
+
if (!recognizable) continue;
|
|
87
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
88
|
+
removed += 1;
|
|
89
|
+
}
|
|
90
|
+
return removed;
|
|
91
|
+
}
|
|
92
|
+
|
|
48
93
|
/**
|
|
49
94
|
* 매니페스트가 가리키는 데스크탑 코어를 내려받아 캐시에 푼다.
|
|
50
95
|
* onNotice(text): 사용자에게 보여줄 안내(무엇을·왜 받는지) — 조용히 받지 않는다.
|
|
@@ -52,47 +97,75 @@ function cachedCoreRoot(manifest = readManifest()) {
|
|
|
52
97
|
*/
|
|
53
98
|
async function fetchDesktopCore({ onNotice } = {}) {
|
|
54
99
|
const manifest = readManifest();
|
|
55
|
-
|
|
100
|
+
const version = normalizedCacheVersion(manifest?.version);
|
|
101
|
+
if (!manifest || !version || !manifest.url || !manifest.sha256) return null;
|
|
56
102
|
|
|
57
|
-
const existing = cachedCoreRoot(manifest);
|
|
103
|
+
const existing = cachedCoreRoot({ ...manifest, version });
|
|
58
104
|
if (existing) return existing;
|
|
59
105
|
|
|
60
106
|
const say = (t) => { if (typeof onNotice === "function") onNotice(t); };
|
|
61
107
|
say(`Downloading the graph-execution engine (${manifest.sizeBytes ? Math.round(manifest.sizeBytes / 1024 / 1024) + " MB" : "one-time"}) from ${manifest.url} …`);
|
|
62
108
|
|
|
63
|
-
const dir = cacheDir(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
109
|
+
const dir = cacheDir(version);
|
|
110
|
+
const partialDir = path.join(
|
|
111
|
+
cacheRoot(),
|
|
112
|
+
`${version}.partial-${process.pid}-${crypto.randomBytes(6).toString("hex")}`,
|
|
113
|
+
);
|
|
114
|
+
fs.mkdirSync(partialDir, { recursive: true });
|
|
115
|
+
const tarPath = path.join(partialDir, "desktop-core.tar.gz");
|
|
67
116
|
|
|
68
|
-
|
|
69
|
-
|
|
117
|
+
let res;
|
|
118
|
+
try {
|
|
119
|
+
res = await fetch(manifest.url);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
say(`Download failed: ${error?.message || error}`);
|
|
122
|
+
fs.rmSync(partialDir, { recursive: true, force: true });
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
say(`Download failed: HTTP ${res.status}`);
|
|
127
|
+
fs.rmSync(partialDir, { recursive: true, force: true });
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
70
130
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
71
131
|
fs.writeFileSync(tarPath, buf);
|
|
72
132
|
|
|
73
133
|
const digest = sha256File(tarPath);
|
|
74
134
|
if (digest !== manifest.sha256) {
|
|
75
135
|
say(`Checksum mismatch (expected ${manifest.sha256.slice(0, 12)}…, got ${digest.slice(0, 12)}…) — refusing to use it.`);
|
|
76
|
-
fs.rmSync(
|
|
136
|
+
fs.rmSync(partialDir, { recursive: true, force: true });
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
if (Number.isFinite(Number(manifest.sizeBytes)) && Number(manifest.sizeBytes) !== buf.length) {
|
|
140
|
+
say(`Size mismatch (expected ${manifest.sizeBytes} bytes, got ${buf.length}) — refusing to use it.`);
|
|
141
|
+
fs.rmSync(partialDir, { recursive: true, force: true });
|
|
77
142
|
return null;
|
|
78
143
|
}
|
|
79
144
|
|
|
80
|
-
const extract = spawnSync("tar", ["-xzf", tarPath, "-C",
|
|
145
|
+
const extract = spawnSync("tar", ["-xzf", tarPath, "-C", partialDir], { encoding: "utf8" });
|
|
81
146
|
if (extract.status !== 0) {
|
|
82
147
|
say(`Extraction failed: ${extract.stderr || extract.error || "unknown error"}`);
|
|
83
|
-
fs.rmSync(
|
|
148
|
+
fs.rmSync(partialDir, { recursive: true, force: true });
|
|
84
149
|
return null;
|
|
85
150
|
}
|
|
86
151
|
fs.rmSync(tarPath);
|
|
87
152
|
|
|
88
|
-
if (!fs.existsSync(path.join(
|
|
153
|
+
if (!fs.existsSync(path.join(partialDir, "dist", "electron", "workflow", "run-graph.js"))) {
|
|
89
154
|
say("Downloaded archive did not contain the expected engine files.");
|
|
90
|
-
fs.rmSync(
|
|
155
|
+
fs.rmSync(partialDir, { recursive: true, force: true });
|
|
91
156
|
return null;
|
|
92
157
|
}
|
|
93
|
-
fs.writeFileSync(path.join(
|
|
158
|
+
fs.writeFileSync(path.join(partialDir, ".complete"), JSON.stringify({
|
|
159
|
+
schemaVersion: 2,
|
|
160
|
+
version,
|
|
161
|
+
sha256: manifest.sha256,
|
|
162
|
+
completedAt: new Date().toISOString(),
|
|
163
|
+
}) + "\n");
|
|
164
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
165
|
+
fs.renameSync(partialDir, dir);
|
|
166
|
+
pruneStaleCaches(version);
|
|
94
167
|
say("Engine ready.");
|
|
95
|
-
return cacheDistDir(
|
|
168
|
+
return cacheDistDir(version);
|
|
96
169
|
}
|
|
97
170
|
|
|
98
|
-
module.exports = { readManifest, cachedCoreRoot, fetchDesktopCore, cacheDistDir };
|
|
171
|
+
module.exports = { readManifest, cachedCoreRoot, fetchDesktopCore, cacheDistDir, pruneStaleCaches };
|
|
@@ -30,7 +30,10 @@ function localCoreBin() {
|
|
|
30
30
|
if (process.platform === "win32") return null;
|
|
31
31
|
for (const candidate of candidates) {
|
|
32
32
|
try {
|
|
33
|
-
if (candidate
|
|
33
|
+
if (!candidate) continue;
|
|
34
|
+
const stat = fs.statSync(candidate);
|
|
35
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
36
|
+
if (stat.isFile()) return candidate;
|
|
34
37
|
} catch { /* keep looking */ }
|
|
35
38
|
}
|
|
36
39
|
return null;
|
|
@@ -69,12 +72,25 @@ function createLocalCoreClient({ cwd, timeoutMs = 60_000 } = {}) {
|
|
|
69
72
|
const pending = new Map();
|
|
70
73
|
let exited = false;
|
|
71
74
|
|
|
72
|
-
|
|
75
|
+
const failPending = (code, message) => {
|
|
76
|
+
if (exited) return;
|
|
73
77
|
exited = true;
|
|
74
78
|
for (const [, entry] of pending) {
|
|
75
|
-
entry.reject(coreError(
|
|
79
|
+
entry.reject(coreError(code, message));
|
|
76
80
|
}
|
|
77
81
|
pending.clear();
|
|
82
|
+
};
|
|
83
|
+
child.once("error", (error) => {
|
|
84
|
+
failPending("local_core_spawn_failed", `the local core process could not start: ${(error && error.message) || error}`);
|
|
85
|
+
});
|
|
86
|
+
child.on("exit", () => {
|
|
87
|
+
failPending("local_core_exited", "the local core process exited before responding");
|
|
88
|
+
});
|
|
89
|
+
// Always drain stderr. Leaving the pipe unread lets a verbose Core fill the
|
|
90
|
+
// kernel buffer and deadlock an otherwise healthy tools/call.
|
|
91
|
+
child.stderr.on("data", () => {});
|
|
92
|
+
child.stdin.on("error", (error) => {
|
|
93
|
+
failPending("local_core_transport_error", `the local core input stream failed: ${(error && error.message) || error}`);
|
|
78
94
|
});
|
|
79
95
|
child.stdout.on("data", (chunk) => {
|
|
80
96
|
buffer += chunk;
|
|
@@ -104,16 +120,35 @@ function createLocalCoreClient({ cwd, timeoutMs = 60_000 } = {}) {
|
|
|
104
120
|
resolve: (message) => { clearTimeout(timer); resolve(message); },
|
|
105
121
|
reject: (error) => { clearTimeout(timer); reject(error); },
|
|
106
122
|
});
|
|
107
|
-
|
|
123
|
+
try {
|
|
124
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`, (error) => {
|
|
125
|
+
if (!error) return;
|
|
126
|
+
const entry = pending.get(id);
|
|
127
|
+
if (!entry) return;
|
|
128
|
+
pending.delete(id);
|
|
129
|
+
entry.reject(coreError("local_core_transport_error", `${method} could not be written to the local core: ${error.message}`));
|
|
130
|
+
});
|
|
131
|
+
} catch (error) {
|
|
132
|
+
const entry = pending.get(id);
|
|
133
|
+
pending.delete(id);
|
|
134
|
+
if (entry) entry.reject(coreError("local_core_transport_error", `${method} could not be written to the local core: ${error.message}`));
|
|
135
|
+
}
|
|
108
136
|
});
|
|
109
137
|
|
|
110
138
|
async function ensureInitialized() {
|
|
111
139
|
if (!initialized) {
|
|
112
|
-
initialized =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
140
|
+
initialized = (async () => {
|
|
141
|
+
const response = await rpc("initialize", {
|
|
142
|
+
protocolVersion: "2024-11-05",
|
|
143
|
+
capabilities: {},
|
|
144
|
+
clientInfo: { name: "agentlas-terminal", version: "2" },
|
|
145
|
+
});
|
|
146
|
+
if (response.error) {
|
|
147
|
+
throw coreError("local_core_initialize_failed", `initialize: ${response.error.message || JSON.stringify(response.error)}`);
|
|
148
|
+
}
|
|
149
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", params: {} })}\n`);
|
|
150
|
+
return response;
|
|
151
|
+
})();
|
|
117
152
|
}
|
|
118
153
|
return initialized;
|
|
119
154
|
}
|
|
@@ -141,9 +141,12 @@ function curateCliReply(db, text, ctx) {
|
|
|
141
141
|
if (scope === "discard" || scope === "session") { logCli(ctx.projectPath, { action: scope, kind, content, at: now }); continue; }
|
|
142
142
|
if (scope === "project" && !ctx.projectPath) scope = "team_memory";
|
|
143
143
|
const ppath = scope === "project" ? ctx.projectPath : null;
|
|
144
|
+
// Same ownership rule as agentlas-memory-governance: team is shared
|
|
145
|
+
// (NULL owner), while an agent_repo memory belongs to the exact agent.
|
|
146
|
+
const scopedAgentId = scope === "agent_repo" ? (ctx.agentId || null) : null;
|
|
144
147
|
const requestContext = normalizeRequestContext(ev, ctx, ppath);
|
|
145
148
|
try {
|
|
146
|
-
const dup = db.prepare("SELECT id,scope,kind,content,confidence,sensitivity,context_json FROM memory_entries WHERE scope=? AND kind=? AND lower(trim(content))=? AND superseded_at IS NULL AND (project_path IS ? OR project_path=?) LIMIT 1").get(scope, kind, content.toLowerCase(), ppath, ppath);
|
|
149
|
+
const dup = db.prepare("SELECT id,scope,kind,content,confidence,sensitivity,context_json FROM memory_entries WHERE scope=? AND kind=? AND lower(trim(content))=? AND superseded_at IS NULL AND (project_path IS ? OR project_path=?) AND (agent_id IS ? OR agent_id=?) LIMIT 1").get(scope, kind, content.toLowerCase(), ppath, ppath, scopedAgentId, scopedAgentId);
|
|
147
150
|
if (dup) {
|
|
148
151
|
rememberCurated({ ...dup, requestContext });
|
|
149
152
|
continue;
|
|
@@ -151,7 +154,7 @@ function curateCliReply(db, text, ctx) {
|
|
|
151
154
|
const memoryId = randomUUID();
|
|
152
155
|
const confidence = ev.confidence || "medium";
|
|
153
156
|
const sensitivity = ev.sensitivity || "internal";
|
|
154
|
-
db.prepare("INSERT INTO memory_entries (id,scope,kind,content,project_id,project_path,agent_id,chat_id,confidence,sensitivity,evidence_json,context_json,superseded_at,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,NULL,?)").run(memoryId, scope, kind, content, ctx.projectId || null, ppath,
|
|
157
|
+
db.prepare("INSERT INTO memory_entries (id,scope,kind,content,project_id,project_path,agent_id,chat_id,confidence,sensitivity,evidence_json,context_json,superseded_at,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,NULL,?)").run(memoryId, scope, kind, content, ctx.projectId || null, ppath, scopedAgentId, null, confidence, sensitivity, JSON.stringify(Array.isArray(ev.evidence_refs) ? ev.evidence_refs : []), JSON.stringify(requestContext), now);
|
|
155
158
|
rememberCurated({ id: memoryId, scope, kind, content, confidence, sensitivity, requestContext });
|
|
156
159
|
logCli(ctx.projectPath, { action: "written", scope, kind, content, request_context: requestContext, at: now });
|
|
157
160
|
} catch { /* ignore */ }
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
const fs = require("node:fs");
|
|
8
8
|
const path = require("node:path");
|
|
9
|
-
const {
|
|
9
|
+
const { spawnSync } = require("node:child_process");
|
|
10
10
|
const { userDataDir } = require("../core/paths.cjs");
|
|
11
11
|
const { fail } = require("./common.cjs");
|
|
12
12
|
|
|
@@ -58,11 +58,18 @@ function list(io) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// `oberon open [path]` — 산출물 폴더를 OS 파일 매니저로 연다.
|
|
61
|
-
function open(io, args) {
|
|
61
|
+
function open(io, args, options = {}) {
|
|
62
62
|
const target = args[0] ? path.resolve(args[0]) : oberonHome();
|
|
63
63
|
if (!fs.existsSync(target)) fail(`Path not found: ${target}`);
|
|
64
64
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
65
|
-
spawn(
|
|
65
|
+
// `spawn(...).unref()` reported success before the OS opener had even spawned;
|
|
66
|
+
// a missing xdg-open/explorer then emitted an unhandled error and crashed the CLI
|
|
67
|
+
// after printing "Opening". The opener command is short-lived, so wait for its
|
|
68
|
+
// launch result and only claim success on exit 0.
|
|
69
|
+
const launch = options.spawnSyncImpl || spawnSync;
|
|
70
|
+
const result = launch(opener, [target], { stdio: "ignore", windowsHide: true });
|
|
71
|
+
if (result && result.error) fail(`Could not open ${target}: ${result.error.message}`);
|
|
72
|
+
if (!result || result.status !== 0) fail(`Could not open ${target} (opener exit ${result && result.status != null ? result.status : "unknown"})`);
|
|
66
73
|
io.out(`Opening folder: ${target}`);
|
|
67
74
|
return 0;
|
|
68
75
|
}
|
|
@@ -209,8 +209,10 @@ async function runCareerGraphCli(args, opts) {
|
|
|
209
209
|
}
|
|
210
210
|
const paths = ensureCareerGraphCli(projectPath, opts.lang);
|
|
211
211
|
if (sub === "open") {
|
|
212
|
-
|
|
213
|
-
return [`${
|
|
212
|
+
const opened = opts.noOpen ? true : openLocalPathCli(paths.inboxPath, opts.notify);
|
|
213
|
+
return [`${opened
|
|
214
|
+
? (ko ? "커리어 그래프 수신함을 열었습니다" : "Opened Career Graph inbox")
|
|
215
|
+
: (ko ? "커리어 그래프 수신함을 자동으로 열지 못했습니다. 직접 여세요" : "Could not open Career Graph inbox automatically; open it manually")}: ${paths.inboxPath}`];
|
|
214
216
|
}
|
|
215
217
|
if (sub === "add") {
|
|
216
218
|
const flags = parseFlagsCli(normalizedArgs.slice(1));
|
|
@@ -185,19 +185,21 @@ function cliMemoryContext(db, projectPath, agentId = null, task = "") {
|
|
|
185
185
|
WHERE superseded_at IS NULL AND (
|
|
186
186
|
(scope='user_identity' AND project_path IS NULL)
|
|
187
187
|
OR (scope='project' AND project_path=?)
|
|
188
|
-
OR (scope IN ('team_memory','agent_team'
|
|
188
|
+
OR (scope IN ('team_memory','agent_team') AND (agent_id IS NULL OR agent_id=?) AND (project_path IS NULL OR project_path=?))
|
|
189
|
+
OR (scope='agent_repo' AND agent_id=? AND (project_path IS NULL OR project_path=?))
|
|
189
190
|
)
|
|
190
191
|
ORDER BY created_at DESC LIMIT 16
|
|
191
|
-
`).all(projectPath, agentId, projectPath)
|
|
192
|
+
`).all(projectPath, agentId, projectPath, agentId, projectPath)
|
|
192
193
|
: db.prepare(`
|
|
193
194
|
SELECT id,kind,content,confidence,context_json,created_at
|
|
194
195
|
FROM memory_entries
|
|
195
196
|
WHERE superseded_at IS NULL AND (
|
|
196
197
|
(scope='user_identity' AND project_path IS NULL)
|
|
197
|
-
OR (scope IN ('team_memory','agent_team'
|
|
198
|
+
OR (scope IN ('team_memory','agent_team') AND (agent_id IS NULL OR agent_id=?) AND project_path IS NULL)
|
|
199
|
+
OR (scope='agent_repo' AND agent_id=? AND project_path IS NULL)
|
|
198
200
|
)
|
|
199
201
|
ORDER BY created_at DESC LIMIT 16
|
|
200
|
-
`).all(agentId);
|
|
202
|
+
`).all(agentId, agentId);
|
|
201
203
|
// R21 W2d — confidence was stored (governance normalizeConfidence) but
|
|
202
204
|
// never reached retrieval: no ranking function existed and the render
|
|
203
205
|
// dropped the column, so a one-off guess and a high-confidence procedure
|
|
@@ -268,9 +270,10 @@ function curateCliReply(db, text, ctx) {
|
|
|
268
270
|
if (scope === "discard" || scope === "session") { logCli(ctx.projectPath, { action: scope, kind, content, at: now }); continue; }
|
|
269
271
|
if (scope === "project" && !ctx.projectPath) scope = "team_memory";
|
|
270
272
|
const ppath = scope === "project" ? ctx.projectPath : null;
|
|
273
|
+
const scopedAgentId = scope === "agent_repo" ? (ctx.agentId || null) : null;
|
|
271
274
|
const requestContext = normalizeRequestContext(ev, ctx, ppath);
|
|
272
275
|
try {
|
|
273
|
-
const dup = db.prepare("SELECT id,scope,kind,content,confidence,sensitivity,context_json FROM memory_entries WHERE scope=? AND kind=? AND lower(trim(content))=? AND superseded_at IS NULL AND (project_path IS ? OR project_path=?) LIMIT 1").get(scope, kind, content.toLowerCase(), ppath, ppath);
|
|
276
|
+
const dup = db.prepare("SELECT id,scope,kind,content,confidence,sensitivity,context_json FROM memory_entries WHERE scope=? AND kind=? AND lower(trim(content))=? AND superseded_at IS NULL AND (project_path IS ? OR project_path=?) AND (agent_id IS ? OR agent_id=?) LIMIT 1").get(scope, kind, content.toLowerCase(), ppath, ppath, scopedAgentId, scopedAgentId);
|
|
274
277
|
if (dup) {
|
|
275
278
|
rememberCurated({ ...dup, requestContext });
|
|
276
279
|
continue;
|
|
@@ -278,7 +281,7 @@ function curateCliReply(db, text, ctx) {
|
|
|
278
281
|
const memoryId = randomUUID();
|
|
279
282
|
const confidence = ev.confidence || "medium";
|
|
280
283
|
const sensitivity = ev.sensitivity || "internal";
|
|
281
|
-
db.prepare("INSERT INTO memory_entries (id,scope,kind,content,project_id,project_path,agent_id,chat_id,confidence,sensitivity,evidence_json,context_json,superseded_at,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,NULL,?)").run(memoryId, scope, kind, content, ctx.projectId || null, ppath,
|
|
284
|
+
db.prepare("INSERT INTO memory_entries (id,scope,kind,content,project_id,project_path,agent_id,chat_id,confidence,sensitivity,evidence_json,context_json,superseded_at,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,NULL,?)").run(memoryId, scope, kind, content, ctx.projectId || null, ppath, scopedAgentId, null, confidence, sensitivity, JSON.stringify(Array.isArray(ev.evidence_refs) ? ev.evidence_refs : []), JSON.stringify(requestContext), now);
|
|
282
285
|
rememberCurated({ id: memoryId, scope, kind, content, confidence, sensitivity, requestContext });
|
|
283
286
|
logCli(ctx.projectPath, { action: "written", scope, kind, content, request_context: requestContext, at: now });
|
|
284
287
|
} catch { /* ignore */ }
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
* manifest/inbox를 만든다 — 초기화되지 않았으면 던진다.
|
|
12
12
|
*/
|
|
13
13
|
const fs = require("node:fs");
|
|
14
|
+
const crypto = require("node:crypto");
|
|
14
15
|
const os = require("node:os");
|
|
15
16
|
const path = require("node:path");
|
|
16
|
-
const {
|
|
17
|
+
const { spawnSync } = require("node:child_process");
|
|
17
18
|
const { loadArch } = require("../core/db.cjs");
|
|
18
19
|
const { initializedAgentlasProjectPathCli } = require("./state.cjs");
|
|
19
20
|
|
|
@@ -64,7 +65,7 @@ function readJsonSafeCli(filePath, fallback) {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function writeJsonSafeCli(filePath, value) {
|
|
67
|
-
|
|
68
|
+
return writeJsonPrivateAtomicCli(filePath, value);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
// 원자적(temp+rename) + 소유자 전용(0600) JSON 쓰기. 세션 ID/경로 등 민감 상태 파일용:
|
|
@@ -72,8 +73,12 @@ function writeJsonSafeCli(filePath, value) {
|
|
|
72
73
|
// (2) 기본 umask(0644)로 cli-sessions.json/agent-routes.json이 world-readable이던 정보 노출을 함께 막는다.
|
|
73
74
|
function writeJsonPrivateAtomicCli(filePath, value) {
|
|
74
75
|
const dir = path.dirname(filePath);
|
|
75
|
-
const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.tmp`);
|
|
76
|
-
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", {
|
|
76
|
+
const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
77
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", {
|
|
78
|
+
encoding: "utf8",
|
|
79
|
+
mode: 0o600,
|
|
80
|
+
flag: "wx",
|
|
81
|
+
});
|
|
77
82
|
try {
|
|
78
83
|
fs.renameSync(tmp, filePath);
|
|
79
84
|
} catch (e) {
|
|
@@ -415,9 +420,12 @@ function registerOntologySourceCli(paths, source, kind, scope, cwd, lang) {
|
|
|
415
420
|
function openLocalPathCli(targetPath, notify) {
|
|
416
421
|
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
417
422
|
try {
|
|
418
|
-
|
|
423
|
+
const result = spawnSync(command, [targetPath], { stdio: "ignore", windowsHide: true });
|
|
424
|
+
if (result.error || result.status !== 0) throw result.error || new Error(`${command} exited ${result.status}`);
|
|
425
|
+
return true;
|
|
419
426
|
} catch {
|
|
420
427
|
if (typeof notify === "function") notify(`Open manually: ${targetPath}`);
|
|
428
|
+
return false;
|
|
421
429
|
}
|
|
422
430
|
}
|
|
423
431
|
|
|
@@ -450,8 +458,10 @@ async function runOntologyCli(args, opts) {
|
|
|
450
458
|
}
|
|
451
459
|
const paths = ensureOntologyCli(projectPath, opts.lang);
|
|
452
460
|
if (sub === "open") {
|
|
453
|
-
|
|
454
|
-
return [`${
|
|
461
|
+
const opened = opts.noOpen ? true : openLocalPathCli(paths.inboxPath, opts.notify);
|
|
462
|
+
return [`${opened
|
|
463
|
+
? (ko ? "온톨로지 수신함을 열었습니다" : "Opened ontology inbox")
|
|
464
|
+
: (ko ? "온톨로지 수신함을 자동으로 열지 못했습니다. 직접 여세요" : "Could not open ontology inbox automatically; open it manually")}: ${paths.inboxPath}`];
|
|
455
465
|
}
|
|
456
466
|
if (sub === "add") {
|
|
457
467
|
const flags = parseFlagsCli(normalizedArgs.slice(1));
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* 조용히 다른 런타임으로 넘어가지 않는다.
|
|
11
11
|
*/
|
|
12
12
|
const { loadCoreAcpRuntime } = require("../core/desktop-core.cjs");
|
|
13
|
+
const permissions = require("../agentlas-permissions.cjs");
|
|
13
14
|
|
|
14
15
|
// 정본(runtimes/kinds.cjs)의 ACP 3종에서 파생 — resolve.cjs의 ACP_CLI_KINDS와 같은 원소.
|
|
15
16
|
const ACP_KINDS = new Set(require("./kinds.cjs").ACP_CLI_KINDS);
|
|
@@ -47,6 +48,28 @@ async function runAcpTurn(req) {
|
|
|
47
48
|
return { text: "", session: req.session || {}, error: `runtime '${kind}' is not an ACP agent in this core`, errorKind: "unsupported", errorSource: "marker" };
|
|
48
49
|
}
|
|
49
50
|
const runner = mod.createAcpRunner(spec);
|
|
51
|
+
let mcpConfigPath;
|
|
52
|
+
try {
|
|
53
|
+
// ACP runner consumes the same Claude-compatible MCP config shape as Desktop.
|
|
54
|
+
// Reuse native-host's content-addressed, credential-isolating materializer so
|
|
55
|
+
// Cursor/Grok/Kimi receive exactly the already-consented Terminal allowlist.
|
|
56
|
+
const nativeHost = require("../agentlas-native-host.cjs");
|
|
57
|
+
const allowedMcpServers = permissions.normalize(req.permission) === "full"
|
|
58
|
+
? (req.mcpServers || [])
|
|
59
|
+
: [];
|
|
60
|
+
mcpConfigPath = nativeHost.cliMcpConfigPath(allowedMcpServers, {
|
|
61
|
+
exactAllowlist: req.mcpAllowlistMode === "exact",
|
|
62
|
+
env: req.env || process.env,
|
|
63
|
+
}).file;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return {
|
|
66
|
+
text: "",
|
|
67
|
+
session: req.session || {},
|
|
68
|
+
error: `ACP MCP configuration failed: ${error && error.message ? error.message : error}`,
|
|
69
|
+
errorKind: "configuration",
|
|
70
|
+
errorSource: "marker",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
50
73
|
const locale = req.locale === "ko" ? "ko" : "en";
|
|
51
74
|
let streaming = false;
|
|
52
75
|
let lastText = "";
|
|
@@ -69,7 +92,12 @@ async function runAcpTurn(req) {
|
|
|
69
92
|
try {
|
|
70
93
|
const result = await runner({
|
|
71
94
|
systemPrompt: req.systemPrompt || "",
|
|
72
|
-
history:
|
|
95
|
+
history: Array.isArray(req.history)
|
|
96
|
+
? req.history.map((entry) => ({
|
|
97
|
+
role: entry && entry.role === "assistant" ? "assistant" : "user",
|
|
98
|
+
text: String((entry && (entry.text ?? entry.content)) || ""),
|
|
99
|
+
}))
|
|
100
|
+
: [],
|
|
73
101
|
userPrompt: req.prompt || "",
|
|
74
102
|
backendLabel: spec.label,
|
|
75
103
|
locale,
|
|
@@ -78,10 +106,20 @@ async function runAcpTurn(req) {
|
|
|
78
106
|
cwd: req.cwd,
|
|
79
107
|
env: req.env || process.env,
|
|
80
108
|
signal: req.signal,
|
|
109
|
+
mcpConfigPath,
|
|
110
|
+
runtimeSessionId: (req.session && (req.session.id || req.session.acpSessionId)) || undefined,
|
|
111
|
+
chatId: req.chatId,
|
|
112
|
+
approvalChatId: req.chatId,
|
|
113
|
+
agentId: req.agentId,
|
|
114
|
+
sessionFingerprintSeed: req.sessionFingerprintSeed,
|
|
115
|
+
unattended: req.unattended === true,
|
|
81
116
|
...(req.model ? { model: req.model } : {}),
|
|
82
117
|
}, events);
|
|
83
118
|
if (streaming) ui.streamEnd();
|
|
84
|
-
const session = {
|
|
119
|
+
const session = {
|
|
120
|
+
...(req.session || {}),
|
|
121
|
+
...(result.sessionId ? { id: result.sessionId, acpSessionId: result.sessionId } : {}),
|
|
122
|
+
};
|
|
85
123
|
if (result.failure) {
|
|
86
124
|
return { text: result.text || "", session, usage: null, error: result.failure.message, errorKind: result.failure.kind, errorSource: result.failure.source };
|
|
87
125
|
}
|
|
@@ -89,7 +127,8 @@ async function runAcpTurn(req) {
|
|
|
89
127
|
} catch (e) {
|
|
90
128
|
if (streaming) ui.streamEnd();
|
|
91
129
|
const message = e && e.message ? e.message : String(e);
|
|
92
|
-
|
|
130
|
+
const detail = process.env.AGENTLAS_DEBUG && e && e.stack ? e.stack : message;
|
|
131
|
+
return { text: "", session: req.session || {}, usage: null, error: detail, errorKind: /abort/i.test(message) ? "cancelled" : "exit", errorSource: "marker" };
|
|
93
132
|
}
|
|
94
133
|
}
|
|
95
134
|
|