agentlas 0.9.4 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,299 @@
1
+ "use strict";
2
+
3
+ // `agentlas evolve [list|apply <id>|revert <id>]` — Phase 2 / 2+ (terminal 표면).
4
+ //
5
+ // 데스크탑 트리거가 만든 "성장 제안"을 공유 agentlas.sqlite에서 읽어 검토·적용·되돌린다.
6
+ // hep/터미널 세션은 UI가 없으므로 이 명령이 4표면 발화 UX의 터미널 창구다.
7
+ // list — 대기 중(고위험 candidate) + 자동적용(저위험) 제안을 사람이 읽는 3줄로.
8
+ // apply <id> — 고위험 candidate를 명시 승인해 프롬프트에 적용(런타임 authority=system_prompt + 파일).
9
+ // revert <id> — 적용된 제안을 되돌린다.
10
+ //
11
+ // 적용/되돌리기 게이트는 도구 무관한 "타깃 파일 내용 해시(before_hash/after_hash)"로만 판정한다
12
+ // (데스크탑이 생성 시 저장한 sha256). 그래서 데스크탑이 자동적용한 저위험 제안도 터미널에서
13
+ // 안전하게 되돌릴 수 있다. 어떤 실패도 폴백 없이 상태로 노출한다.
14
+
15
+ const fs = require("node:fs");
16
+ const path = require("node:path");
17
+ const { createHash, randomUUID } = require("node:crypto");
18
+
19
+ function sha256(content) {
20
+ return createHash("sha256").update(String(content), "utf8").digest("hex");
21
+ }
22
+
23
+ const ABSENT_TARGET_HASH = sha256("agentlas:absent-agent-asset:v1");
24
+ const RULE_TARGETS = new Set([
25
+ "system-prompt.md",
26
+ "soul.md",
27
+ "agent.md",
28
+ "claude.md",
29
+ "agents.md",
30
+ "gemini.md",
31
+ "persona.md",
32
+ "prompt.md",
33
+ ]);
34
+
35
+ function parseSource(json) {
36
+ try {
37
+ const parsed = JSON.parse(json || "{}");
38
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
39
+ } catch {
40
+ return {};
41
+ }
42
+ }
43
+
44
+ /** 대기 중(사람 결정 필요) 고위험 성장 제안 개수 — 터미널 홈 배너용. */
45
+ function countPendingGrowthProposals(db) {
46
+ try {
47
+ const row = db
48
+ .prepare(
49
+ `SELECT COUNT(*) AS n FROM agent_evolution_proposals
50
+ WHERE json_extract(source_json, '$._growth') = 1 AND status = 'candidate'`,
51
+ )
52
+ .get();
53
+ return Number(row && row.n ? row.n : 0);
54
+ } catch {
55
+ return 0;
56
+ }
57
+ }
58
+
59
+ function listGrowthProposals(db, limit = 50) {
60
+ let rows = [];
61
+ try {
62
+ rows = db
63
+ .prepare(
64
+ `SELECT * FROM agent_evolution_proposals
65
+ WHERE json_extract(source_json, '$._growth') = 1
66
+ AND status IN ('candidate','applied','measured')
67
+ ORDER BY datetime(updated_at) DESC, datetime(created_at) DESC
68
+ LIMIT ?`,
69
+ )
70
+ .all(limit);
71
+ } catch {
72
+ rows = [];
73
+ }
74
+ const pending = [];
75
+ const autoApplied = [];
76
+ for (const row of rows) {
77
+ const source = parseSource(row.source_json);
78
+ const entry = { row, source };
79
+ if (row.status === "candidate") pending.push(entry);
80
+ else if (source._autoApplied === true) autoApplied.push(entry);
81
+ }
82
+ return { pending, autoApplied };
83
+ }
84
+
85
+ function agentById(db, id) {
86
+ try {
87
+ return db.prepare("SELECT * FROM installed_agents WHERE id = ?").get(id) || null;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ function targetFilePath(agentFolder, agent, targetPath) {
94
+ const dir = agentFolder(agent);
95
+ const safe = path.resolve(dir, targetPath);
96
+ if (safe !== path.join(dir, targetPath) && !safe.startsWith(path.resolve(dir) + path.sep)) {
97
+ throw new Error("Evolution target escapes the agent folder");
98
+ }
99
+ return safe;
100
+ }
101
+
102
+ function currentTargetHash(file) {
103
+ try {
104
+ const content = fs.readFileSync(file, "utf8");
105
+ return { exists: true, content, hash: sha256(content) };
106
+ } catch (error) {
107
+ if (error && error.code === "ENOENT") return { exists: false, content: "", hash: ABSENT_TARGET_HASH };
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ function printCard(out, entry, index) {
113
+ const { row, source } = entry;
114
+ const card = source.humanCard && typeof source.humanCard === "object" ? source.humanCard : null;
115
+ const tier = source.riskTier === "high" ? "high" : "low";
116
+ out(` [${index}] ${row.id} (${tier} · ${row.status} · ${row.agent_id})`);
117
+ if (card) {
118
+ out(` 배운 것 : ${card.learned}`);
119
+ out(` 바뀌는 것: ${card.change}`);
120
+ out(` 되돌리기 : ${card.reversible}`);
121
+ } else {
122
+ out(` ${row.summary}`);
123
+ }
124
+ }
125
+
126
+ function cmdList(db, out) {
127
+ const { pending, autoApplied } = listGrowthProposals(db);
128
+ out("== agent growth proposals ==");
129
+ out("");
130
+ out(`대기(승인 필요) ${pending.length}건:`);
131
+ if (!pending.length) out(" (없음)");
132
+ pending.forEach((entry, i) => printCard(out, entry, i + 1));
133
+ out("");
134
+ out(`자동 적용됨(저위험) ${autoApplied.length}건:`);
135
+ if (!autoApplied.length) out(" (없음)");
136
+ autoApplied.forEach((entry, i) => printCard(out, entry, i + 1));
137
+ out("");
138
+ out("적용: agentlas evolve apply <id> · 되돌리기: agentlas evolve revert <id>");
139
+ }
140
+
141
+ function loadProposal(db, id, fail) {
142
+ const row = db.prepare("SELECT * FROM agent_evolution_proposals WHERE id = ?").get(id);
143
+ if (!row) return fail(`Proposal not found: ${id}`);
144
+ if (row.proposal_type !== "rule") {
145
+ return fail(`Terminal evolve applies rule (prompt) proposals only; ${id} is '${row.proposal_type}'. Use the desktop app.`);
146
+ }
147
+ if (!RULE_TARGETS.has(String(row.target_path).toLowerCase())) {
148
+ return fail(`Unsupported evolution target: ${row.target_path}`);
149
+ }
150
+ return row;
151
+ }
152
+
153
+ // 터미널이 부트스트랩한 DB는 v45라 진화 영수증 테이블이 없을 수 있다(데스크탑 v51+ 마이그레이션이
154
+ // 추가). 앱과 동일한 스키마로 idempotent 생성 — 앱이 이미 만들었으면 no-op.
155
+ function ensureReceiptTable(db) {
156
+ db.exec(`
157
+ CREATE TABLE IF NOT EXISTS agent_evolution_receipts (
158
+ id TEXT PRIMARY KEY,
159
+ proposal_id TEXT NOT NULL,
160
+ agent_id TEXT NOT NULL,
161
+ action TEXT NOT NULL,
162
+ target_path TEXT NOT NULL,
163
+ version_before INTEGER NOT NULL,
164
+ version_after INTEGER NOT NULL,
165
+ target_hash_before TEXT NOT NULL,
166
+ target_hash_after TEXT NOT NULL,
167
+ package_hash_before TEXT NOT NULL,
168
+ package_hash_after TEXT NOT NULL,
169
+ created_at TEXT NOT NULL,
170
+ UNIQUE(proposal_id, action)
171
+ );
172
+ `);
173
+ }
174
+
175
+ function insertReceipt(db, row, action, hashBefore, hashAfter, now) {
176
+ ensureReceiptTable(db);
177
+ db.prepare(
178
+ `INSERT INTO agent_evolution_receipts (
179
+ id, proposal_id, agent_id, action, target_path,
180
+ version_before, version_after, target_hash_before, target_hash_after,
181
+ package_hash_before, package_hash_after, created_at
182
+ ) VALUES (?, ?, ?, ?, ?, 1, 2, ?, ?, ?, ?, ?)
183
+ ON CONFLICT(proposal_id, action) DO NOTHING`,
184
+ ).run(
185
+ `evo_receipt_${randomUUID()}`,
186
+ row.id,
187
+ row.agent_id,
188
+ action,
189
+ row.target_path,
190
+ hashBefore,
191
+ hashAfter,
192
+ hashBefore,
193
+ hashAfter,
194
+ now,
195
+ );
196
+ }
197
+
198
+ function cmdApply(db, id, out, fail, agentFolder) {
199
+ const row = loadProposal(db, id, fail);
200
+ if (!row) return;
201
+ if (row.status !== "candidate") {
202
+ return fail(`Only a pending candidate can be applied; ${id} is '${row.status}'.`);
203
+ }
204
+ const agent = agentById(db, row.agent_id);
205
+ if (!agent) return fail(`Agent not found for proposal: ${row.agent_id}`);
206
+ const file = targetFilePath(agentFolder, agent, row.target_path);
207
+ const current = currentTargetHash(file);
208
+ if (current.hash !== row.before_hash) {
209
+ return fail("Agent prompt changed after this proposal was created; review it in the desktop app and re-propose.");
210
+ }
211
+ const now = new Date().toISOString();
212
+ fs.mkdirSync(path.dirname(file), { recursive: true });
213
+ fs.writeFileSync(file, row.after_content, "utf8");
214
+ const verify = sha256(fs.readFileSync(file, "utf8"));
215
+ if (verify !== row.after_hash) {
216
+ // 원상복구 후 실패 노출(폴백 금지).
217
+ fs.writeFileSync(file, row.before_content, "utf8");
218
+ return fail("Applied content did not match the approved hash; restored the original.");
219
+ }
220
+ const tx = db.transaction(() => {
221
+ db.prepare("UPDATE installed_agents SET system_prompt = ? WHERE id = ?").run(row.after_content, row.agent_id);
222
+ insertReceipt(db, row, "apply", row.before_hash, row.after_hash, now);
223
+ db.prepare(
224
+ `UPDATE agent_evolution_proposals
225
+ SET status = 'applied', applied_at = COALESCE(applied_at, ?),
226
+ last_error = NULL, updated_at = ?
227
+ WHERE id = ? AND status = 'candidate'`,
228
+ ).run(now, now, row.id);
229
+ });
230
+ tx();
231
+ out(`applied ${id} → ${row.target_path} (agent ${row.agent_id}). Revert with: agentlas evolve revert ${id}`);
232
+ }
233
+
234
+ function cmdRevert(db, id, out, fail, agentFolder) {
235
+ const row = loadProposal(db, id, fail);
236
+ if (!row) return;
237
+ if (row.status !== "applied" && row.status !== "measured") {
238
+ return fail(`Only an applied proposal can be reverted; ${id} is '${row.status}'.`);
239
+ }
240
+ const applyReceipt = db
241
+ .prepare("SELECT 1 FROM agent_evolution_receipts WHERE proposal_id = ? AND action = 'apply' LIMIT 1")
242
+ .get(row.id);
243
+ if (!applyReceipt) return fail("Revert requires the verified apply receipt.");
244
+ const agent = agentById(db, row.agent_id);
245
+ if (!agent) return fail(`Agent not found for proposal: ${row.agent_id}`);
246
+ const file = targetFilePath(agentFolder, agent, row.target_path);
247
+ const current = currentTargetHash(file);
248
+ if (current.hash !== row.after_hash) {
249
+ return fail("Agent prompt changed after this proposal was applied; revert blocked to avoid clobbering newer edits.");
250
+ }
251
+ const now = new Date().toISOString();
252
+ fs.writeFileSync(file, row.before_content, "utf8");
253
+ const verify = sha256(fs.readFileSync(file, "utf8"));
254
+ if (verify !== row.before_hash) {
255
+ fs.writeFileSync(file, row.after_content, "utf8");
256
+ return fail("Reverted content did not match the original hash; restored the applied version.");
257
+ }
258
+ const tx = db.transaction(() => {
259
+ db.prepare("UPDATE installed_agents SET system_prompt = ? WHERE id = ?").run(row.before_content, row.agent_id);
260
+ insertReceipt(db, row, "rollback", row.after_hash, row.before_hash, now);
261
+ db.prepare(
262
+ `UPDATE agent_evolution_proposals
263
+ SET status = 'rolled_back', rolled_back_at = COALESCE(rolled_back_at, ?),
264
+ last_error = NULL, updated_at = ?
265
+ WHERE id = ? AND status IN ('applied','measured')`,
266
+ ).run(now, now, row.id);
267
+ });
268
+ tx();
269
+ out(`reverted ${id} → restored ${row.target_path} (agent ${row.agent_id}).`);
270
+ }
271
+
272
+ /**
273
+ * cmdEvolve — `agentlas evolve <sub> ...`.
274
+ * @param {{db:any,args:string[],out:(s:string)=>void,fail:(s:string)=>void,agentFolder:(a:any)=>string}} ctx
275
+ */
276
+ function cmdEvolve(ctx) {
277
+ const { db, out, fail, agentFolder } = ctx;
278
+ const args = Array.isArray(ctx.args) ? ctx.args : [];
279
+ const sub = args[0] || "list";
280
+ if (sub === "help" || sub === "--help" || sub === "-h") {
281
+ out("usage: agentlas evolve [list | apply <id> | revert <id>]");
282
+ out(" list — review pending (approval-needed) and auto-applied agent growth proposals");
283
+ out(" apply <id> — approve and apply a pending proposal to the agent prompt");
284
+ out(" revert <id> — roll back an applied proposal");
285
+ return;
286
+ }
287
+ if (sub === "list" || sub === "ls") return cmdList(db, out);
288
+ if (sub === "apply") {
289
+ if (!args[1]) return fail("usage: agentlas evolve apply <id>");
290
+ return cmdApply(db, args[1], out, fail, agentFolder);
291
+ }
292
+ if (sub === "revert" || sub === "rollback") {
293
+ if (!args[1]) return fail("usage: agentlas evolve revert <id>");
294
+ return cmdRevert(db, args[1], out, fail, agentFolder);
295
+ }
296
+ return fail(`Unknown evolve subcommand: ${sub} (list|apply|revert)`);
297
+ }
298
+
299
+ module.exports = { cmdEvolve, countPendingGrowthProposals, listGrowthProposals };
@@ -0,0 +1,315 @@
1
+ "use strict";
2
+
3
+ // `agentlas memory import <path> [--apply]` — Phase 1b.
4
+ //
5
+ // Promote legacy markdown memory into the shared agentlas.sqlite the desktop
6
+ // uses (same userData/agentlas.sqlite). Maps each substantive markdown section
7
+ // to a durable memory_entries row owned by the right layer of a team (member
8
+ // cell / orchestrator / shared team_memory) or a single agent. Mirrors the app's
9
+ // electron/memory/import.ts mapping so app and terminal agree. Dry-run by
10
+ // default (prints the preview table); --apply writes. Idempotent via a stable
11
+ // per-section source-hash sentinel embedded in evidence. Secrets are dropped.
12
+
13
+ const fs = require("node:fs");
14
+ const path = require("node:path");
15
+ const { createHash, randomUUID } = require("node:crypto");
16
+
17
+ const SOURCE_TOKEN_PREFIX = "mem-import:v1";
18
+ const MAX_FILES = 400;
19
+ const MAX_CONTENT = 4000;
20
+
21
+ // ── Section extraction (parity with electron/memory/import.ts) ───────────────
22
+ const TEMPLATE_LINE =
23
+ /^\s*[-*]?\s*(Add\b|Fill in\b|Example:|Record\b|Link\b|Prefer\b|Note\b|Which\b|Date:\s*$|Topic:\s*$|Decision:\s*$|Why:\s*$|Risk accepted:\s*$)/i;
24
+ const META_HEADING =
25
+ /^(How To Use|사용 규칙|사용법|구조|형식|사전 참조|누가 업데이트|Entries|Read First|Memory Rules|Recent Activity|Recently Touched|CROSS-REFERENCES|LEARNINGS LOG|ANTIPATTERNS|GOTCHAS|Repeated Failures|성공 패턴|발견 사항|안티패턴|에이전트 구성)/i;
26
+ const SHARED_HINT =
27
+ /(team[-_ ]?memory|team_memory|glossary|handoff|scope[-_ ]?ownership|common[-_ ]?safety|safety|tone|language|dossier|operating[-_ ]?architecture|memory[-_ ]?architecture|용어|공통|안전|인계|톤)/i;
28
+ const RISK_HINT = /(security|attack|vuln|bug|gotcha|incident|보안|취약|버그|사고)/i;
29
+ const ALWAYS_KEEP_HINT = /(team[-_ ]?memory|glossary|dossier|handoff|safety|scope[-_ ]?ownership|tone)/i;
30
+
31
+ // Minimal secret guard (a subset of the shared secret-patterns chokepoint) so a
32
+ // stray key in legacy notes never becomes a durable memory row.
33
+ const SECRET_RE =
34
+ /(sk-[A-Za-z0-9]{20,}|sk_live_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|gh[opsu]_[A-Za-z0-9]{20,}|AIza[0-9A-Za-z_-]{20,}|glpat-[A-Za-z0-9_-]{16,}|xox[baprs]-[A-Za-z0-9-]{10,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|AKIA[0-9A-Z]{16})/;
35
+
36
+ function looksSecret(content) {
37
+ return SECRET_RE.test(String(content || ""));
38
+ }
39
+
40
+ function substantiveBody(body) {
41
+ const lines = body.split("\n").map((l) => l.trim()).filter(Boolean);
42
+ const real = lines.filter((l) => !TEMPLATE_LINE.test(l) && l.replace(/^#+\s*/, "").length >= 12);
43
+ return real.join("\n");
44
+ }
45
+
46
+ function kindForHeading(heading, fallback) {
47
+ const m = /\[([A-Z_]+)\]/.exec(heading);
48
+ const tag = m ? m[1] : "";
49
+ if (["SUCCESS", "DISCOVERY"].includes(tag)) return "procedure";
50
+ if (["ANTIPATTERN", "GOTCHA", "SECURITY", "CONFIRMED", "REGRESSION", "FALSE_POSITIVE", "BLOCKED_BY_GUARD", "FAILURE"].includes(tag)) {
51
+ return "risk";
52
+ }
53
+ return fallback;
54
+ }
55
+
56
+ function keepSection(heading, body, alwaysKeep) {
57
+ if (META_HEADING.test(heading.replace(/\[[A-Z_]+\]\s*/, "").trim())) return false;
58
+ const hasDate = /\(20\d\d-\d\d-\d\d\)|Date:\s*20\d\d-\d\d-\d\d/.test(heading + "\n" + body);
59
+ const hasTag = /\[[A-Z_]+\]/.test(heading);
60
+ const real = substantiveBody(body);
61
+ if (alwaysKeep) return real.length >= 60;
62
+ if (hasDate || hasTag) return real.length >= 40;
63
+ return real.length >= 160;
64
+ }
65
+
66
+ function splitSections(md) {
67
+ const lines = md.split("\n");
68
+ const sections = [];
69
+ let cur = null;
70
+ for (const line of lines) {
71
+ if (/^#{2,3}\s+\S/.test(line)) {
72
+ if (cur) sections.push(cur);
73
+ cur = { heading: line.replace(/^#{2,3}\s+/, "").trim(), body: "" };
74
+ } else if (cur) {
75
+ cur.body += line + "\n";
76
+ }
77
+ }
78
+ if (cur) sections.push(cur);
79
+ return sections;
80
+ }
81
+
82
+ function splitDatedBullets(md) {
83
+ const idx = md.indexOf("- Date:");
84
+ if (idx === -1) return [];
85
+ return md
86
+ .slice(idx)
87
+ .split(/\n(?=- Date:)/)
88
+ .map((p) => p.trim())
89
+ .filter((p) => /Date:\s*20\d\d-\d\d-\d\d/.test(p))
90
+ .map((p) => {
91
+ const topic = /Topic:\s*(.+)/.exec(p);
92
+ const date = /Date:\s*(20\d\d-\d\d-\d\d)/.exec(p);
93
+ return { heading: `Decision ${date ? date[1] : ""}: ${topic ? topic[1].trim() : ""}`.trim(), body: p };
94
+ });
95
+ }
96
+
97
+ function normalizeToken(value) {
98
+ return String(value || "").toLowerCase().replace(/[^a-z0-9가-힣]+/g, " ").trim();
99
+ }
100
+
101
+ function resolveTarget(db, agentId) {
102
+ let firms = [];
103
+ try {
104
+ firms = db.prepare("SELECT id, ceo_agent_id, org_chart_json FROM firms").all();
105
+ } catch {
106
+ firms = [];
107
+ }
108
+ const firm =
109
+ firms.find((f) => f.id === agentId) ||
110
+ firms.find((f) => f.ceo_agent_id === agentId) ||
111
+ null;
112
+ if (!firm) return { agentId, kind: "agent", members: [] };
113
+ let chart = [];
114
+ try {
115
+ const parsed = JSON.parse(firm.org_chart_json);
116
+ if (Array.isArray(parsed)) chart = parsed;
117
+ } catch {
118
+ chart = [];
119
+ }
120
+ const members = chart
121
+ .filter((node) => node && node.agentId && node.agentId !== firm.ceo_agent_id)
122
+ .map((node) => ({ agentId: node.agentId, role: node.role || node.agentSlug, slug: node.agentSlug }));
123
+ return { agentId: firm.ceo_agent_id, kind: "team", members };
124
+ }
125
+
126
+ function matchMember(fileTokens, target) {
127
+ if (!target.members.length) return null;
128
+ let best = null;
129
+ for (const member of target.members) {
130
+ const roleTokens = normalizeToken(member.role).split(" ").filter((t) => t.length >= 3);
131
+ const slugTokens = normalizeToken(member.slug).split(" ").filter((t) => t.length >= 3);
132
+ const tokens = [...new Set([...roleTokens, ...slugTokens])];
133
+ let score = 0;
134
+ for (const token of tokens) if (fileTokens.includes(token)) score += token.length;
135
+ if (score > 0 && (!best || score > best.score)) best = { agentId: member.agentId, role: member.role, score };
136
+ }
137
+ return best ? { agentId: best.agentId, role: best.role } : null;
138
+ }
139
+
140
+ function decideOwner(relFile, target) {
141
+ const lower = relFile.toLowerCase();
142
+ const fileTokens = normalizeToken(relFile);
143
+ const alwaysKeep = ALWAYS_KEEP_HINT.test(lower);
144
+ if (SHARED_HINT.test(lower)) {
145
+ return {
146
+ scope: "team_memory",
147
+ ownerAgentId: null,
148
+ ownerLabel: "team_memory",
149
+ fallbackKind: /glossary|dossier|용어/i.test(lower) ? "fact" : "procedure",
150
+ alwaysKeep,
151
+ };
152
+ }
153
+ if (target.kind === "team") {
154
+ const member = matchMember(fileTokens, target);
155
+ if (member) {
156
+ return { scope: "agent_repo", ownerAgentId: member.agentId, ownerLabel: member.role, fallbackKind: RISK_HINT.test(lower) ? "risk" : "procedure", alwaysKeep };
157
+ }
158
+ return { scope: "agent_repo", ownerAgentId: target.agentId, ownerLabel: "orchestrator", fallbackKind: RISK_HINT.test(lower) ? "risk" : "decision", alwaysKeep };
159
+ }
160
+ return { scope: "agent_repo", ownerAgentId: target.agentId, ownerLabel: "agent", fallbackKind: RISK_HINT.test(lower) ? "risk" : "procedure", alwaysKeep };
161
+ }
162
+
163
+ function collectMarkdown(root) {
164
+ const stat = fs.statSync(root);
165
+ if (stat.isFile()) {
166
+ return /\.(md|markdown|mdx|txt)$/i.test(root) ? [{ abs: root, rel: path.basename(root) }] : [];
167
+ }
168
+ const out = [];
169
+ const walk = (dir) => {
170
+ if (out.length >= MAX_FILES) return;
171
+ let entries;
172
+ try {
173
+ entries = fs.readdirSync(dir, { withFileTypes: true });
174
+ } catch {
175
+ return;
176
+ }
177
+ for (const entry of entries) {
178
+ if (out.length >= MAX_FILES) return;
179
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
180
+ const abs = path.join(dir, entry.name);
181
+ if (entry.isDirectory()) walk(abs);
182
+ else if (entry.isFile() && /\.(md|markdown|mdx|txt)$/i.test(entry.name)) out.push({ abs, rel: path.relative(root, abs) });
183
+ }
184
+ };
185
+ walk(root);
186
+ return out.sort((a, b) => a.rel.localeCompare(b.rel));
187
+ }
188
+
189
+ function stableToken(relFile, heading) {
190
+ const hash = createHash("sha256").update(`${SOURCE_TOKEN_PREFIX}|${relFile}|${heading}`).digest("hex").slice(0, 16);
191
+ return `${SOURCE_TOKEN_PREFIX}:${hash}`;
192
+ }
193
+
194
+ function buildEntries(sourcePath, target) {
195
+ const built = [];
196
+ for (const { abs, rel } of collectMarkdown(sourcePath)) {
197
+ let md;
198
+ try {
199
+ md = fs.readFileSync(abs, "utf8");
200
+ } catch {
201
+ continue;
202
+ }
203
+ const owner = decideOwner(rel, target);
204
+ const sections = /decisions\.md$/i.test(rel) ? splitDatedBullets(md) : splitSections(md);
205
+ for (const sec of sections) {
206
+ if (!keepSection(sec.heading, sec.body, owner.alwaysKeep)) continue;
207
+ const bodyText = sec.body.replace(/\n{3,}/g, "\n\n").trim();
208
+ const content = `${sec.heading}\n${bodyText}`.trim().slice(0, MAX_CONTENT);
209
+ if (content.length < 40) continue;
210
+ built.push({
211
+ token: stableToken(rel, sec.heading),
212
+ relFile: rel,
213
+ heading: sec.heading,
214
+ content,
215
+ scope: owner.scope,
216
+ kind: kindForHeading(sec.heading, owner.fallbackKind),
217
+ ownerAgentId: owner.ownerAgentId,
218
+ ownerLabel: owner.ownerLabel,
219
+ redacted: looksSecret(content),
220
+ });
221
+ }
222
+ }
223
+ return built;
224
+ }
225
+
226
+ function existsByToken(db, token) {
227
+ try {
228
+ return Boolean(db.prepare("SELECT 1 FROM memory_entries WHERE evidence_json LIKE ? LIMIT 1").get(`%${token}%`));
229
+ } catch {
230
+ return false;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * cmdMemory — `agentlas memory <sub> ...`. Currently: import.
236
+ * @param {{db:any,args:string[],out:(s:string)=>void,fail:(s:string)=>void}} ctx
237
+ */
238
+ function cmdMemory(ctx) {
239
+ const { db, out, fail } = ctx;
240
+ const args = Array.isArray(ctx.args) ? ctx.args : [];
241
+ const sub = args[0] || "help";
242
+ if (sub === "help" || sub === "--help" || sub === "-h") {
243
+ out("usage: agentlas memory import <folder-or-file> --agent <agentId> [--apply]");
244
+ out(" dry-run by default (prints the preview table); --apply writes to the shared DB.");
245
+ return;
246
+ }
247
+ if (sub !== "import") return fail(`Unknown memory subcommand: ${sub} (import)`);
248
+
249
+ const apply = args.includes("--apply");
250
+ const agentIdx = args.indexOf("--agent");
251
+ const agentId = agentIdx >= 0 ? String(args[agentIdx + 1] || "").trim() : "";
252
+ const positional = args.slice(1).filter((a, i, arr) => a !== "--apply" && a !== "--agent" && arr[i - 1] !== "--agent");
253
+ const rawPath = positional[0];
254
+ if (!rawPath) return fail('usage: agentlas memory import <folder-or-file> --agent <agentId> [--apply]');
255
+ if (!agentId) return fail("memory import requires --agent <agentId> (the single agent or team to import into).");
256
+ const sourcePath = path.resolve(rawPath);
257
+ if (!fs.existsSync(sourcePath)) return fail(`Import source not found: ${sourcePath}`);
258
+
259
+ const target = resolveTarget(db, agentId);
260
+ const entries = buildEntries(sourcePath, target);
261
+
262
+ out(`== memory import (${apply ? "APPLY" : "DRY-RUN"}) ==`);
263
+ out(`source: ${sourcePath}`);
264
+ out(`target: ${agentId} (${target.kind})`);
265
+ out("");
266
+ out(pad("OWNER", 26) + pad("KIND", 10) + pad("STATUS", 8) + "SECTION");
267
+ const byOwner = {};
268
+ let newCount = 0;
269
+ let dupCount = 0;
270
+ let redacted = 0;
271
+ for (const e of entries) {
272
+ const status = e.redacted ? "skip" : existsByToken(db, e.token) ? "dup" : "new";
273
+ if (status === "new") {
274
+ newCount += 1;
275
+ byOwner[e.ownerLabel] = (byOwner[e.ownerLabel] || 0) + 1;
276
+ } else if (status === "dup") dupCount += 1;
277
+ else redacted += 1;
278
+ out(pad(e.ownerLabel, 26) + pad(e.kind, 10) + pad(status, 8) + e.heading.slice(0, 70));
279
+ }
280
+ out("");
281
+ out(`total ${entries.length} · new ${newCount} · duplicate ${dupCount} · redacted ${redacted}`);
282
+
283
+ if (!apply) {
284
+ out("");
285
+ out("dry-run — nothing written. Re-run with --apply to write to the shared agentlas.sqlite.");
286
+ return;
287
+ }
288
+
289
+ const now = new Date().toISOString();
290
+ let imported = 0;
291
+ const insert = db.prepare(
292
+ "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,?)",
293
+ );
294
+ const write = db.transaction((list) => {
295
+ for (const e of list) {
296
+ if (e.redacted || existsByToken(db, e.token)) continue;
297
+ const confidence = /\(20\d\d-\d\d-\d\d\)|Date:\s*20\d\d/.test(e.content) ? "high" : "medium";
298
+ const context = JSON.stringify({ userIntent: `Imported memory: ${e.heading}`.slice(0, 200), outcome: "imported-from-existing-memory" });
299
+ const evidence = JSON.stringify([e.token, `source:memory-import/${e.relFile}`]);
300
+ insert.run(randomUUID(), e.scope, e.kind, e.content, null, null, e.ownerAgentId, null, confidence, "internal", evidence, context, now);
301
+ imported += 1;
302
+ }
303
+ });
304
+ write(entries);
305
+
306
+ out("");
307
+ out(`imported ${imported} memory entries into the shared DB. (Embedding runs in the desktop app on next open.)`);
308
+ }
309
+
310
+ function pad(value, width) {
311
+ const s = String(value == null ? "" : value);
312
+ return s.length >= width ? s.slice(0, width - 1) + " " : s + " ".repeat(width - s.length);
313
+ }
314
+
315
+ module.exports = { cmdMemory, buildEntries, resolveTarget, decideOwner };
@@ -10022,6 +10022,19 @@ function cmdList(db) {
10022
10022
  : "\n(Built-in orchestration agents run in the background. Find agents with `agentlas cloud search \"what you need\"`, or just open `agentlas` and type a task.)",
10023
10023
  );
10024
10024
  }
10025
+ // Phase 2+: 검토 대기 중인 에이전트 성장 제안이 있으면 홈에 한 줄로 노출.
10026
+ try {
10027
+ const pendingGrowth = require("./agentlas-evolution.cjs").countPendingGrowthProposals(db);
10028
+ if (pendingGrowth > 0) {
10029
+ out(
10030
+ lang === "ko"
10031
+ ? `\n🧬 에이전트 성장 제안 ${pendingGrowth}건 · \`agentlas evolve\`로 검토`
10032
+ : `\n🧬 ${pendingGrowth} agent growth proposal(s) · review with \`agentlas evolve\``,
10033
+ );
10034
+ }
10035
+ } catch {
10036
+ /* 홈 배너 실패는 무해 */
10037
+ }
10025
10038
  out("\nRun: agentlas <agent> · agentlas firm <firm> · agentlas run <agent> \"...\"");
10026
10039
  }
10027
10040
 
@@ -11737,6 +11750,12 @@ async function main() {
11737
11750
  return cmdFirm(db, rest[1], rest.slice(2).join(" "), runtimeOverride);
11738
11751
  case "env":
11739
11752
  return cmdEnv(db);
11753
+ case "memory":
11754
+ // Phase 1b: 기존 마크다운 메모리 → 공유 agentlas.sqlite 이관(dry-run 기본, --apply).
11755
+ return require("./agentlas-memory-import.cjs").cmdMemory({ db, args: rest.slice(1), out, fail });
11756
+ case "evolve":
11757
+ // Phase 2/2+: 데스크탑 트리거가 만든 성장 제안 검토·적용·되돌리기(공유 DB).
11758
+ return require("./agentlas-evolution.cjs").cmdEvolve({ db, args: rest.slice(1), out, fail, agentFolder });
11740
11759
  case "multimodal":
11741
11760
  return cmdMultimodal(db, rest.slice(1));
11742
11761
  case "oberon":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlas",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "Agentlas agent terminal — chat with your installed AI agents and teams from the terminal, Claude Code style. Standalone: no desktop app required.",
5
5
  "bin": {
6
6
  "agentlas": "bin/agentlas.cjs"