agentlas 0.9.10 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/README.md +319 -341
  3. package/bin/agentlas.cjs +32 -9
  4. package/engine/agentlas-banner.cjs +24 -3
  5. package/engine/agentlas-composer.cjs +239 -31
  6. package/engine/agentlas-config.cjs +103 -7
  7. package/engine/agentlas-core-harness.cjs +48 -4
  8. package/engine/agentlas-evolution.cjs +3 -4
  9. package/engine/agentlas-i18n.cjs +88 -32
  10. package/engine/agentlas-input.cjs +234 -18
  11. package/engine/agentlas-memory-governance.cjs +3 -5
  12. package/engine/agentlas-memory-import.cjs +3 -3
  13. package/engine/agentlas-native-host.cjs +200 -9
  14. package/engine/agentlas-onboard.cjs +112 -20
  15. package/engine/agentlas-permissions.cjs +14 -7
  16. package/engine/agentlas-sqlite-policy.cjs +34 -0
  17. package/engine/agentlas-ui.cjs +93 -42
  18. package/engine/agentlas-workforce.cjs +472 -58
  19. package/engine/agentlas-workload-routing.cjs +8 -3
  20. package/engine/agentlas.cjs +140 -12367
  21. package/engine/agents/files.cjs +61 -0
  22. package/engine/agents/import-local.cjs +237 -0
  23. package/engine/agents/registry.cjs +158 -0
  24. package/engine/agents/router.cjs +565 -0
  25. package/engine/agents/routes.cjs +43 -0
  26. package/engine/architecture.data.json +2 -2
  27. package/engine/automation/daemon.cjs +335 -0
  28. package/engine/automation/schedule.cjs +181 -0
  29. package/engine/automation/store.cjs +209 -0
  30. package/engine/cloud/auth.cjs +279 -0
  31. package/engine/cloud/hub-client.cjs +239 -0
  32. package/engine/cloud-assets/cargo.cjs +49 -0
  33. package/engine/cloud-assets/cas.cjs +235 -0
  34. package/engine/cloud-assets/commands.cjs +273 -0
  35. package/engine/cloud-assets/package.cjs +936 -0
  36. package/engine/cloud-assets/restore.cjs +172 -0
  37. package/engine/cloud-assets/state.cjs +268 -0
  38. package/engine/commands/automation.cjs +195 -0
  39. package/engine/commands/billing.cjs +96 -0
  40. package/engine/commands/browser.cjs +20 -0
  41. package/engine/commands/build.cjs +33 -0
  42. package/engine/commands/call.cjs +24 -0
  43. package/engine/commands/career-graph.cjs +51 -0
  44. package/engine/commands/cd.cjs +22 -0
  45. package/engine/commands/chat.cjs +12 -0
  46. package/engine/commands/chats.cjs +28 -0
  47. package/engine/commands/cloud.cjs +17 -0
  48. package/engine/commands/connect.cjs +20 -0
  49. package/engine/commands/context.cjs +66 -0
  50. package/engine/commands/creds.cjs +203 -0
  51. package/engine/commands/doctor.cjs +68 -0
  52. package/engine/commands/env.cjs +33 -0
  53. package/engine/commands/evolve.cjs +23 -0
  54. package/engine/commands/experience.cjs +34 -0
  55. package/engine/commands/film.cjs +8 -0
  56. package/engine/commands/firm.cjs +115 -0
  57. package/engine/commands/help.cjs +65 -0
  58. package/engine/commands/hep.cjs +23 -0
  59. package/engine/commands/import.cjs +35 -0
  60. package/engine/commands/index.cjs +136 -0
  61. package/engine/commands/install.cjs +27 -0
  62. package/engine/commands/journal.cjs +31 -0
  63. package/engine/commands/legacy-network.cjs +29 -0
  64. package/engine/commands/list.cjs +49 -0
  65. package/engine/commands/login.cjs +68 -0
  66. package/engine/commands/logout.cjs +28 -0
  67. package/engine/commands/mcp.cjs +91 -0
  68. package/engine/commands/memory.cjs +21 -0
  69. package/engine/commands/multimodal.cjs +91 -0
  70. package/engine/commands/native.cjs +34 -0
  71. package/engine/commands/netadmin.cjs +31 -0
  72. package/engine/commands/oberon.cjs +70 -0
  73. package/engine/commands/ontology.cjs +24 -0
  74. package/engine/commands/open.cjs +48 -0
  75. package/engine/commands/plugin.cjs +101 -0
  76. package/engine/commands/project.cjs +47 -0
  77. package/engine/commands/research.cjs +36 -0
  78. package/engine/commands/route.cjs +37 -0
  79. package/engine/commands/run.cjs +149 -0
  80. package/engine/commands/search.cjs +55 -0
  81. package/engine/commands/setup.cjs +45 -0
  82. package/engine/commands/storm.cjs +75 -0
  83. package/engine/commands/swarm.cjs +75 -0
  84. package/engine/commands/telegram.cjs +32 -0
  85. package/engine/commands/uninstall.cjs +68 -0
  86. package/engine/commands/update.cjs +54 -0
  87. package/engine/commands/upload.cjs +18 -0
  88. package/engine/commands/usage.cjs +35 -0
  89. package/engine/commands/variant.cjs +25 -0
  90. package/engine/commands/version.cjs +9 -0
  91. package/engine/commands/whoami.cjs +38 -0
  92. package/engine/commands/workforce.cjs +103 -0
  93. package/engine/core/db.cjs +160 -0
  94. package/engine/core/paths.cjs +35 -0
  95. package/engine/experience/build.cjs +181 -0
  96. package/engine/experience/intents.cjs +492 -0
  97. package/engine/experience/runtime.cjs +242 -0
  98. package/engine/experience/variant.cjs +196 -0
  99. package/engine/firms/orchestrate.cjs +333 -0
  100. package/engine/hephaestus/runtime.cjs +697 -0
  101. package/engine/hub/install.cjs +872 -0
  102. package/engine/hub/plugins.cjs +213 -0
  103. package/engine/mcp/consent.cjs +289 -0
  104. package/engine/mcp/contract.cjs +202 -0
  105. package/engine/mcp/index.cjs +43 -0
  106. package/engine/mcp/inventory.cjs +322 -0
  107. package/engine/mcp/plan.cjs +286 -0
  108. package/engine/mcp/probe.cjs +151 -0
  109. package/engine/memory-cli/curate.cjs +163 -0
  110. package/engine/oberon/common.cjs +69 -0
  111. package/engine/oberon/manifest.cjs +164 -0
  112. package/engine/oberon/outputs.cjs +70 -0
  113. package/engine/oberon/render.cjs +164 -0
  114. package/engine/project/career-graph.cjs +249 -0
  115. package/engine/project/credentials.cjs +262 -0
  116. package/engine/project/env-file.cjs +46 -0
  117. package/engine/project/index.cjs +27 -0
  118. package/engine/project/memory-context.cjs +453 -0
  119. package/engine/project/ontology.cjs +467 -0
  120. package/engine/project/paths.cjs +39 -0
  121. package/engine/project/seed.cjs +200 -0
  122. package/engine/project/state.cjs +403 -0
  123. package/engine/project/super-ontology-seed.json +3288 -0
  124. package/engine/runtimes/detect.cjs +54 -0
  125. package/engine/runtimes/overrides.cjs +139 -0
  126. package/engine/runtimes/resolve.cjs +64 -0
  127. package/engine/sessions/apply-fences.cjs +188 -0
  128. package/engine/sessions/fences.cjs +362 -0
  129. package/engine/sessions/orchestrator.cjs +170 -0
  130. package/engine/sessions/prompt.cjs +212 -0
  131. package/engine/sessions/session.cjs +245 -0
  132. package/engine/sessions/sink.cjs +54 -0
  133. package/engine/sessions/store.cjs +79 -0
  134. package/engine/storm/deps.cjs +88 -0
  135. package/engine/storm/storm.cjs +218 -0
  136. package/engine/storm/swarm.cjs +422 -0
  137. package/engine/ui/palette.cjs +105 -0
  138. package/engine/ui/renderer.cjs +85 -0
  139. package/engine/ui/repl.cjs +444 -0
  140. package/engine/workforce/capture.cjs +701 -0
  141. package/engine/workforce/deps.cjs +472 -0
  142. package/package.json +2 -6
  143. package/engine/agentlas-experience-mcp.cjs +0 -1709
  144. package/engine/agentlas-parity.cjs +0 -1499
  145. package/engine/agentlas-repl.cjs +0 -1780
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ /*
3
+ * agents/files — 에이전트의 로컬 폴더 해석 + 네이티브 CLI 문맥 파일 생성.
4
+ * 폴더 우선순위: 로컬 임포트 라우트 → 클라우드 설치본(cloud-agent-installs, 복원 마커
5
+ * 존재 시) → userData/agents/<slug>. v1 agentFolder와 동일 규칙 — cd/native가
6
+ * 관찰상 읽기 전용으로 유지돼야 소스 패키지를 재분류하지 않는다.
7
+ */
8
+ const fs = require("node:fs");
9
+ const path = require("node:path");
10
+ const { userDataDir } = require("../core/paths.cjs");
11
+ const { routesMap } = require("./routes.cjs");
12
+
13
+ const CLOUD_RESTORE_MARKER_PATH = ".agentlas-cloud-package.json";
14
+
15
+ function cloudSlug(value) {
16
+ return (String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "agentlas-cloud-agent");
17
+ }
18
+
19
+ function exists(p) { try { fs.accessSync(p); return true; } catch { return false; } }
20
+
21
+ function agentSystemPrompt(agent) {
22
+ return agent && agent.systemPrompt ? agent.systemPrompt : `You are ${(agent && agent.name) || "an Agentlas agent"}.`;
23
+ }
24
+
25
+ function agentFolder(agent) {
26
+ const routes = routesMap();
27
+ const r = routes[agent.id];
28
+ if (r && r.path) return r.path; // 로컬 임포트는 원본 폴더
29
+ const cloudRoot = path.join(userDataDir(), "cloud-agent-installs", cloudSlug(agent.slug));
30
+ if (exists(path.join(cloudRoot, CLOUD_RESTORE_MARKER_PATH))) return cloudRoot;
31
+ return path.join(userDataDir(), "agents", agent.slug);
32
+ }
33
+
34
+ function writeIfMissing(file, content) {
35
+ try { fs.lstatSync(file); return false; } catch (error) {
36
+ if (!error || error.code !== "ENOENT") throw error;
37
+ }
38
+ const fd = fs.openSync(file, "wx", 0o600);
39
+ try {
40
+ fs.writeFileSync(fd, content.endsWith("\n") ? content : content + "\n", "utf8");
41
+ fs.fsyncSync(fd);
42
+ } finally {
43
+ fs.closeSync(fd);
44
+ }
45
+ return true;
46
+ }
47
+
48
+ function ensureNativeFiles(agent, folder) {
49
+ fs.mkdirSync(folder, { recursive: true });
50
+ const sys = agentSystemPrompt(agent);
51
+ const created = [];
52
+ if (writeIfMissing(path.join(folder, "system-prompt.md"), sys)) created.push("system-prompt.md");
53
+ const header = `# ${agent.name}\n\n${agent.tagline || ""}\n\n${sys}\n`;
54
+ // 네이티브 CLI가 프로젝트 지시로 자동 인식하는 파일들
55
+ if (writeIfMissing(path.join(folder, "CLAUDE.md"), header)) created.push("CLAUDE.md");
56
+ if (writeIfMissing(path.join(folder, "AGENTS.md"), header)) created.push("AGENTS.md");
57
+ if (writeIfMissing(path.join(folder, "GEMINI.md"), header)) created.push("GEMINI.md");
58
+ return created;
59
+ }
60
+
61
+ module.exports = { agentFolder, ensureNativeFiles, agentSystemPrompt, cloudSlug, CLOUD_RESTORE_MARKER_PATH };
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ /*
3
+ * agents/import-local — 로컬 폴더 임포트 (데스크탑 electron/agents/import-local.ts 와 동일 규칙).
4
+ * 터미널에서 "폴더 드래그" = `agentlas import <path>`. 앱과 같은 DB/라우트를 공유한다.
5
+ * v1 모놀리스 890–1112 구간의 충실 이식 — 규칙 변경 금지(데스크탑과 판정 동형이어야 함).
6
+ */
7
+ const fs = require("node:fs");
8
+ const path = require("node:path");
9
+ const crypto = require("node:crypto");
10
+ const { tableExists, columnExists } = require("../core/db.cjs");
11
+ const { routesMap, saveRoutes } = require("./routes.cjs");
12
+
13
+ function exists(p) { try { fs.accessSync(p); return true; } catch { return false; } }
14
+ function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
15
+ function readFileSafe(p, maxChars) {
16
+ try { const s = fs.readFileSync(p, "utf8"); return maxChars ? s.slice(0, maxChars) : s; } catch { return ""; }
17
+ }
18
+ function readFirst(dir, names, maxChars) {
19
+ for (const n of names) {
20
+ const p = path.join(dir, n);
21
+ if (exists(p) && !isDir(p)) { const s = readFileSafe(p, maxChars || 8000); if (s) return s; }
22
+ }
23
+ return "";
24
+ }
25
+
26
+ function detectRuntimeLabels(dir) {
27
+ const labels = [];
28
+ if (exists(path.join(dir, "CLAUDE.md")) || isDir(path.join(dir, ".claude"))) labels.push("claude-code");
29
+ if (exists(path.join(dir, "AGENTS.md"))) labels.push("codex");
30
+ if (exists(path.join(dir, "GEMINI.md"))) labels.push("gemini");
31
+ if (isDir(path.join(dir, ".cursor")) || exists(path.join(dir, ".cursorrules"))) labels.push("cursor");
32
+ if (!labels.length) labels.push("generic");
33
+ return labels;
34
+ }
35
+
36
+ // 팀 감지 — 루트뿐 아니라 .claude/ 중첩 구조도 인식한다 (appbridge 처럼).
37
+ function detectKind(dir) {
38
+ const rootMarkers = ["TEAM.md", "ceo", "hr-departments", "projects"];
39
+ for (const m of rootMarkers) if (exists(path.join(dir, m))) return "team";
40
+ const nestedMarkers = [".claude/ceo", ".claude/hr-departments", ".claude/agents", ".claude/orgspec.yaml"];
41
+ for (const m of nestedMarkers) if (exists(path.join(dir, m))) return "team";
42
+ return "agent";
43
+ }
44
+
45
+ function readImportName(dir) {
46
+ const text = readFirst(dir, ["manifest.md", "AGENT.md", "CLAUDE.md", "README.md"], 2000);
47
+ const m = text.match(/^#\s+(.+)$/m);
48
+ if (m) { const n = m[1].replace(/\(.*?\)/g, "").trim().slice(0, 60); if (n) return n; }
49
+ return path.basename(dir);
50
+ }
51
+
52
+ function readImportTagline(dir) {
53
+ const text = readFirst(dir, ["README.md", "soul.md", "AGENT.md"], 2000);
54
+ for (const line of text.split("\n")) {
55
+ const t = line.trim();
56
+ if (t && !t.startsWith("#") && !t.startsWith(">")) return t.slice(0, 140);
57
+ }
58
+ // 팀 orgspec mission 첫 줄 fallback
59
+ const org = readFileSafe(path.join(dir, ".claude", "orgspec.yaml"), 4000);
60
+ const mm = org.match(/mission:\s*\|?\s*\n?\s*(.+)/);
61
+ if (mm) return mm[1].trim().slice(0, 140);
62
+ return "";
63
+ }
64
+
65
+ const IMPORT_ENV_RE = /\b[A-Z][A-Z0-9_]{2,}(?:API_KEY|TOKEN|SECRET|PASSWORD|CLIENT_ID|CLIENT_SECRET|ACCESS_TOKEN|REFRESH_TOKEN|PRIVATE_KEY|SERVICE_ACCOUNT|WEBHOOK_SECRET|CREDENTIALS|KEY)\b/g;
66
+ const IMPORT_PROCESS_ENV_RE = /process\.env\.([A-Z][A-Z0-9_]{2,})/g;
67
+ const IMPORT_DOTENV_LINE_RE = /^(?:export\s+)?([A-Z][A-Z0-9_]{2,})\s*=/gm;
68
+ const IMPORT_ENV_IGNORES = new Set(["CI", "HOME", "LANG", "NODE_ENV", "PATH", "PORT", "PWD", "SHELL", "TERM", "TMPDIR", "USER"]);
69
+
70
+ function detectImportEnvRequirements(dir, extraText) {
71
+ const files = [".env", ".env.local", ".env.example", ".env.sample", ".env.template", "env.example", "README.md", "AGENT.md", "AGENTS.md", "CLAUDE.md", "GEMINI.md", "manifest.md", "package.json", ".mcp.json"];
72
+ const found = new Map();
73
+ const add = (key, source, required) => {
74
+ if (!key || IMPORT_ENV_IGNORES.has(key) || key.length < 4 || key.length > 96 || !/^[A-Z][A-Z0-9_]+$/.test(key)) return;
75
+ const entry = found.get(key) || { sources: new Set(), required: false };
76
+ entry.sources.add(source);
77
+ entry.required = entry.required || required;
78
+ found.set(key, entry);
79
+ };
80
+ const collect = (text, source) => {
81
+ if (!text) return;
82
+ for (const m of text.matchAll(IMPORT_DOTENV_LINE_RE)) add(m[1], source, true);
83
+ for (const m of text.matchAll(IMPORT_PROCESS_ENV_RE)) add(m[1], source, true);
84
+ for (const m of text.matchAll(IMPORT_ENV_RE)) add(m[0], source, source.includes(".env"));
85
+ };
86
+ for (const name of files) collect(readFileSafe(path.join(dir, name), 256 * 1024), name);
87
+ collect(extraText || "", "system prompt");
88
+ return [...found.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, info]) => ({
89
+ key,
90
+ label: key.replace(/_/g, " ").toLowerCase().replace(/\b\w/g, (ch) => ch.toUpperCase()),
91
+ labelEn: key.replace(/_/g, " ").toLowerCase().replace(/\b\w/g, (ch) => ch.toUpperCase()),
92
+ required: info.required,
93
+ hint: "Detected in " + [...info.sources].slice(0, 3).join(", "),
94
+ hintEn: "Detected in " + [...info.sources].slice(0, 3).join(", "),
95
+ }));
96
+ }
97
+
98
+ // 팀이면 CEO 두뇌를 시스템 프롬프트로 잡고, 임의 cwd에서도 동작하도록 절대경로 헤더를 붙인다.
99
+ function buildImportSystemPrompt(dir, name, kind) {
100
+ if (kind === "team") {
101
+ const ceoBrain = readFileSafe(path.join(dir, ".claude", "ceo", "AGENT.md"));
102
+ const rootAgents = readFileSafe(path.join(dir, "AGENTS.md"));
103
+ const rootClaude = readFileSafe(path.join(dir, "CLAUDE.md"));
104
+ const nestedClaude = readFileSafe(path.join(dir, ".claude", "CLAUDE.md"));
105
+ const brain = ceoBrain || rootAgents || rootClaude || nestedClaude;
106
+ const claudeRoot = path.join(dir, ".claude");
107
+ const header =
108
+ `You are the CEO / orchestrator of the "${name}" agent team, now launched through Agentlas.\n\n` +
109
+ `TEAM ROOT: ${dir}\n` +
110
+ `Team definition (org spec, playbooks, department & role agents) lives under: ${claudeRoot}\n` +
111
+ `When the instructions below reference team files with relative paths (e.g. ./playbook.md, ../orgspec.yaml, .claude/...), resolve them as ABSOLUTE paths under that team root and read them as needed.\n\n` +
112
+ `TARGET PROJECT: your current working directory is the user's target project. Do ALL building, file creation, and delivery in the current working directory — never inside the team root. Route work to the right department/specialist, sequence multi-step work, keep a brief CEO-style status in Korean, and apply read-only-first safety gates for high-risk actions (billing/auth/security/deploy).\n\n` +
113
+ `--- TEAM BRAIN ---\n`;
114
+ return (header + (brain || `Act as the orchestrating CEO of ${name}.`)).slice(0, 16000);
115
+ }
116
+ const sys = readFirst(dir, ["system-prompt.md", "soul.md", "AGENT.md", "CLAUDE.md", "AGENTS.md", "GEMINI.md"]);
117
+ return sys || `You are ${name}, a locally imported agent.`;
118
+ }
119
+
120
+ function readTeamDepartments(dir) {
121
+ for (const root of [path.join(dir, "hr-departments"), path.join(dir, ".claude", "hr-departments")]) {
122
+ try {
123
+ if (isDir(root)) {
124
+ return fs.readdirSync(root, { withFileTypes: true })
125
+ .filter((e) => e.isDirectory() && !e.name.startsWith("."))
126
+ .map((e) => e.name).sort();
127
+ }
128
+ } catch { /* continue */ }
129
+ }
130
+ return [];
131
+ }
132
+
133
+ function deptLabel(name) {
134
+ return name.replace(/[-_]+/g, " ").split(" ").filter(Boolean)
135
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
136
+ }
137
+
138
+ // 팀 폴더 → 회사(firm) upsert (앱의 upsertLocalTeamFirm 과 동일). slug 기준 멱등.
139
+ function upsertLocalTeamFirm(db, dir, ceoAgentId, agentSlug, name, tagline) {
140
+ if (!tableExists(db, "firms")) return null;
141
+ const depts = readTeamDepartments(dir);
142
+ const orgChart = [
143
+ { agentSlug, agentId: ceoAgentId, role: "CEO", reportsTo: null },
144
+ ...depts.map((d) => ({ agentSlug: `${agentSlug}-${d}`, agentId: "", role: deptLabel(d), reportsTo: agentSlug })),
145
+ ];
146
+ const firmSlug = `firm-${agentSlug}`;
147
+ const chartJson = JSON.stringify(orgChart);
148
+ const existing = db.prepare("SELECT id FROM firms WHERE slug=?").get(firmSlug);
149
+ if (existing) {
150
+ db.prepare("UPDATE firms SET name=?, name_en=?, tagline=?, tagline_en=?, persona=?, ceo_agent_id=?, org_chart_json=? WHERE id=?")
151
+ .run(name, name, tagline, tagline, "", ceoAgentId, chartJson, existing.id);
152
+ return { id: existing.id, slug: firmSlug };
153
+ }
154
+ const id = crypto.randomUUID();
155
+ db.prepare(
156
+ "INSERT INTO firms (id, slug, name, name_en, tagline, tagline_en, persona, ceo_agent_id, org_chart_json, installed_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
157
+ ).run(id, firmSlug, name, name, tagline, tagline, "", ceoAgentId, chartJson, new Date().toISOString());
158
+ return { id, slug: firmSlug };
159
+ }
160
+
161
+ function importLocalFolder(db, absPath) {
162
+ const dir = path.resolve(absPath);
163
+ if (!isDir(dir)) throw new Error(`Not a directory: ${absPath}`);
164
+ const labels = detectRuntimeLabels(dir);
165
+ const runtime = labels[0];
166
+ const kind = detectKind(dir);
167
+ const name = readImportName(dir);
168
+ const tagline = readImportTagline(dir) || (kind === "team" ? "Imported local team" : "Imported local agent");
169
+ const systemPrompt = buildImportSystemPrompt(dir, name, kind);
170
+ const envRequirements = detectImportEnvRequirements(dir, systemPrompt);
171
+ const envReqsJson = JSON.stringify(envRequirements);
172
+
173
+ // 같은 경로가 이미 임포트돼 있으면 그 에이전트를 갱신(멱등).
174
+ const routes = routesMap();
175
+ let existingId = null;
176
+ for (const [aid, r] of Object.entries(routes)) {
177
+ if (r && path.resolve(r.path || "") === dir) { existingId = aid; break; }
178
+ }
179
+ const now = new Date().toISOString();
180
+ const TONES = ["blue", "green", "purple", "amber", "peach"];
181
+ let id, slug;
182
+ if (existingId) {
183
+ id = existingId;
184
+ const row = db.prepare("SELECT slug FROM installed_agents WHERE id=?").get(id);
185
+ slug = row ? row.slug : null;
186
+ if (slug) {
187
+ if (columnExists(db, "installed_agents", "visibility")) {
188
+ db.prepare("UPDATE installed_agents SET name=?, name_en=?, tagline=?, tagline_en=?, system_prompt=?, env_requirements_json=?, visibility='visible' WHERE id=?")
189
+ .run(name, name, tagline, tagline, systemPrompt, envReqsJson, id);
190
+ } else {
191
+ db.prepare("UPDATE installed_agents SET name=?, name_en=?, tagline=?, tagline_en=?, system_prompt=?, env_requirements_json=? WHERE id=?")
192
+ .run(name, name, tagline, tagline, systemPrompt, envReqsJson, id);
193
+ }
194
+ } else { existingId = null; }
195
+ }
196
+ if (!existingId) {
197
+ const base = "local-" + (path.basename(dir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "agent");
198
+ slug = base; let n = 1;
199
+ while (db.prepare("SELECT 1 FROM installed_agents WHERE slug=?").get(slug)) slug = `${base}-${++n}`;
200
+ id = crypto.randomUUID();
201
+ let h = 0; for (let i = 0; i < slug.length; i++) h = (h << 5) - h + slug.charCodeAt(i);
202
+ const tone = TONES[Math.abs(h) % TONES.length];
203
+ if (columnExists(db, "installed_agents", "visibility")) {
204
+ db.prepare(
205
+ "INSERT INTO installed_agents (id, slug, name, name_en, tagline, tagline_en, system_prompt, mcp_servers_json, env_requirements_json, preferred_backend, trust_grade, installed_at, tone, builtin, visibility) VALUES (?,?,?,?,?,?,?,'[]',?,NULL,'A',?,?,0,'visible')",
206
+ ).run(id, slug, name, name, tagline, tagline, systemPrompt, envReqsJson, now, tone);
207
+ } else {
208
+ db.prepare(
209
+ "INSERT INTO installed_agents (id, slug, name, name_en, tagline, tagline_en, system_prompt, mcp_servers_json, env_requirements_json, preferred_backend, trust_grade, installed_at, tone, builtin) VALUES (?,?,?,?,?,?,?,'[]',?,NULL,'A',?,?,0)",
210
+ ).run(id, slug, name, name, tagline, tagline, systemPrompt, envReqsJson, now, tone);
211
+ }
212
+ }
213
+ // detectKind 결과를 DB에도 기록 — needsImage의 팀 body-veto 등 능력 판정이
214
+ // 데스크탑이 써준 entity_kind에 무임승차하지 않고 터미널 단독 임포트에서도 성립한다.
215
+ if (columnExists(db, "installed_agents", "entity_kind")) {
216
+ db.prepare("UPDATE installed_agents SET entity_kind=? WHERE id=?").run(kind, id);
217
+ }
218
+ // 라우트 저장
219
+ routes[id] = { agentId: id, path: dir, runtime, labels, kind, importedAt: now };
220
+ saveRoutes(routes);
221
+
222
+ // 팀이면 회사(firm)로도 등록 → 앱 FIRMS 목록 + `agentlas firm <slug>` 사용 가능. slug 기준 멱등.
223
+ let firm = null;
224
+ if (kind === "team") {
225
+ try { firm = upsertLocalTeamFirm(db, dir, id, slug, name, tagline); } catch { /* best-effort */ }
226
+ }
227
+ return { id, slug, name, tagline, runtime, labels, kind, path: dir, updated: !!existingId, firmSlug: firm ? firm.slug : null };
228
+ }
229
+
230
+ module.exports = {
231
+ importLocalFolder,
232
+ detectKind,
233
+ detectRuntimeLabels,
234
+ detectImportEnvRequirements,
235
+ buildImportSystemPrompt,
236
+ upsertLocalTeamFirm,
237
+ };
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ /*
3
+ * agents/registry — 설치 에이전트 조회 (공유 DB 읽기 전용 표면).
4
+ * 라우팅/판정은 여기 없다 — 이 모듈은 결정론적 조회만 한다.
5
+ *
6
+ * 프라이버시 정책(v1/데스크탑 동형): 일부 아키텍처 에이전트는 웹 전용(private) 또는
7
+ * 백그라운드 인프라라서 목록/해석에서 제외된다. visibility 열 + 역할 + 정규화 지문
8
+ * 셋으로 판정한다 — 지문 셋은 이름이 리브랜딩돼도 구본을 계속 가리기 위한 것.
9
+ */
10
+ const crypto = require("node:crypto");
11
+
12
+ const PRIVATE_WEB_AGENT_FINGERPRINTS = new Set([
13
+ "880db20e11cd945e5777b5aaf73c10f24de3e2e190d13631b5f3ed0e4796821c",
14
+ "a0dba10416f15dac84202902284780ee23f31eda9dc068ccf6a28276b585ea36",
15
+ "479d879189166bf9bde1b0cd939db746bf8c1b94f2aad553d08cf7b4a2204f9e",
16
+ "79c16e0347312aceb57c0ec7ee6bb6ebd0118984cc716f9cd56db63d18679183",
17
+ "56ff55fcc909461b5fc449fdb3d685c6cceeb10d59836d9a91faf3ceb41896a4",
18
+ "978dd8a262d86397bbdaca13bbec5be313a68fb2d5c609330888818641af8079",
19
+ ]);
20
+ const BACKGROUND_AGENT_FINGERPRINTS = new Set([
21
+ "9011fb75e638676e23a36f86ea689b6e4de17cb5b5954b36810b5239ab077f0b",
22
+ "0331d654916d648797d31598e3e18eb7fd49166e91783ab9d731648b6e855b90",
23
+ ]);
24
+ const BACKGROUND_ROLES = new Set(["orchestrator", "pm", "curator", "governance"]);
25
+ // 데스크탑 electron/agents/policy.ts:35 REMOVED_MARKETPLACE_SEED_SLUGS 동형 —
26
+ // 마켓에서 회수된 시드 에이전트는 publicAgentVisibility가 무조건 'private'으로
27
+ // 강등한다(electron/agents/policy.ts:96). 데스크탑은 db.ts:1499에서 이 값을
28
+ // visibility 열에 백필하지만, 데스크탑 마이그레이션이 안 돈 DB에서 터미널이
29
+ // 이 목록 없이 읽으면 회수된 시드가 목록/해석에 다시 새어 나온다.
30
+ const REMOVED_MARKETPLACE_SEED_SLUGS = new Set([
31
+ "shop-product-writer",
32
+ "shop-cs-responder",
33
+ "shop-review-monitor",
34
+ "shop-pricing-scout",
35
+ "shop-keyword-finder",
36
+ "marketer-content-writer",
37
+ "marketer-seo-researcher",
38
+ "marketer-schedule-secretary",
39
+ "marketer-ad-copywriter",
40
+ "marketer-analytics-reader",
41
+ "firm-ceo-shop",
42
+ "firm-ceo-marketer",
43
+ ]);
44
+
45
+ function policyNormalize(value) {
46
+ return String(value || "").toLowerCase().replace(/\s+/g, " ").trim();
47
+ }
48
+ function policyFingerprint(value) {
49
+ const normalized = policyNormalize(value);
50
+ return normalized ? crypto.createHash("sha256").update(normalized).digest("hex") : null;
51
+ }
52
+ function agentRowFingerprints(row) {
53
+ return [row.slug, row.name, row.name_en, row.tagline, row.tagline_en]
54
+ .map(policyFingerprint)
55
+ .filter(Boolean);
56
+ }
57
+ function isPrivateWebOnlyAgentRow(row) {
58
+ if (policyNormalize(row.visibility) === "private") return true;
59
+ if (policyNormalize(row.role) === "meta") return true;
60
+ return agentRowFingerprints(row).some((value) => PRIVATE_WEB_AGENT_FINGERPRINTS.has(value));
61
+ }
62
+ function isBackgroundAgentRow(row) {
63
+ if (isPrivateWebOnlyAgentRow(row)) return false;
64
+ if (policyNormalize(row.visibility) === "background") return true;
65
+ if (row.builtin && BACKGROUND_ROLES.has(policyNormalize(row.role))) return true;
66
+ return agentRowFingerprints(row).some((value) => BACKGROUND_AGENT_FINGERPRINTS.has(value));
67
+ }
68
+
69
+ /**
70
+ * 데스크탑 publicAgentVisibility 동형 (electron/agents/policy.ts:95-100):
71
+ * 회수된 마켓 시드 슬러그 → private (지문/역할 검사보다 먼저),
72
+ * 웹 전용 → private, 백그라운드 → background, 그 외 → 열 값(없으면 visible).
73
+ */
74
+ function publicAgentVisibilityRow(row) {
75
+ if (REMOVED_MARKETPLACE_SEED_SLUGS.has(policyNormalize(row.slug))) return "private";
76
+ if (isPrivateWebOnlyAgentRow(row)) return "private";
77
+ if (isBackgroundAgentRow(row)) return "background";
78
+ const declared = policyNormalize(row.visibility);
79
+ return declared === "visible" || declared === "background" || declared === "private" ? declared : "visible";
80
+ }
81
+
82
+ function rowToAgent(row) {
83
+ if (!row) return null;
84
+ return {
85
+ id: row.id,
86
+ slug: row.slug,
87
+ name: row.name,
88
+ nameEn: row.name_en || "",
89
+ tagline: row.tagline || "",
90
+ taglineEn: row.tagline_en || "",
91
+ systemPrompt: row.system_prompt || "",
92
+ preferredBackend: row.preferred_backend || null,
93
+ builtin: !!row.builtin,
94
+ visibility: row.visibility || "visible",
95
+ role: row.role || null,
96
+ mcpServersJson: row.mcp_servers_json || "[]",
97
+ };
98
+ }
99
+
100
+ /** 웹 전용(private)·회수 시드 제외 전체 목록 — 백그라운드 인프라는 visibility='background'로 정규화. */
101
+ function listPublicAgents(db) {
102
+ return db.prepare("SELECT * FROM installed_agents ORDER BY installed_at DESC").all()
103
+ .filter((row) => publicAgentVisibilityRow(row) !== "private")
104
+ .map((row) => rowToAgent({ ...row, visibility: isBackgroundAgentRow(row) ? "background" : "visible" }));
105
+ }
106
+
107
+ function listAgents(db) {
108
+ return listPublicAgents(db).filter((agent) => agent.visibility !== "background");
109
+ }
110
+
111
+ /** 라우팅 후보(백그라운드 포함, 웹 전용 제외). */
112
+ function listRoutableAgents(db) {
113
+ return listPublicAgents(db);
114
+ }
115
+
116
+ /**
117
+ * slug/이름 해석 — 라우팅 가능 집합(listRoutableAgents) 안에서만 찾는다:
118
+ * 웹 전용(private) 에이전트는 이름을 알아도 터미널에서 실행되면 안 된다.
119
+ * 정확 일치 → 유일 부분 일치. 부분 일치가 여럿이면 모호 — 조용히 고르지 않는다.
120
+ */
121
+ function findAgent(db, token) {
122
+ const q = String(token || "").trim().toLowerCase();
123
+ if (!q) return null;
124
+ const agents = listRoutableAgents(db);
125
+ const exact = agents.find((a) => a.slug.toLowerCase() === q || a.id === token)
126
+ || agents.find((a) => (a.name || "").toLowerCase() === q || (a.nameEn || "").toLowerCase() === q);
127
+ if (exact) return exact;
128
+ const partial = agents.filter((a) =>
129
+ a.slug.toLowerCase().includes(q) || (a.name || "").toLowerCase().includes(q) || (a.nameEn || "").toLowerCase().includes(q));
130
+ if (partial.length === 1) return partial[0];
131
+ return null;
132
+ }
133
+
134
+ function agentMcpServerIds(db, agentId) {
135
+ try {
136
+ return db.prepare("SELECT server_id FROM agent_mcp_servers WHERE agent_id=?").all(agentId).map((r) => r.server_id);
137
+ } catch {
138
+ return [];
139
+ }
140
+ }
141
+
142
+ /** invoke 시스템 프롬프트: 저장 프롬프트가 없으면 이름 기반 최소 프롬프트 (v1 agentSystemPromptCli 동형). */
143
+ function agentSystemPrompt(row) {
144
+ return row && row.system_prompt ? row.system_prompt : `You are ${row?.name || "an Agentlas agent"}.`;
145
+ }
146
+
147
+ module.exports = {
148
+ findAgent,
149
+ listAgents,
150
+ listPublicAgents,
151
+ listRoutableAgents,
152
+ rowToAgent,
153
+ agentMcpServerIds,
154
+ agentSystemPrompt,
155
+ isPrivateWebOnlyAgentRow,
156
+ isBackgroundAgentRow,
157
+ publicAgentVisibilityRow,
158
+ };