agentlas 1.0.28 → 1.0.35

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 (70) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +5 -2
  3. package/bin/agentlas.cjs +15 -0
  4. package/engine/agentlas-cloud-runtime.cjs +54 -4
  5. package/engine/agentlas-i18n.cjs +4 -4
  6. package/engine/agentlas-input.cjs +10 -3
  7. package/engine/agentlas-judgment.cjs +0 -0
  8. package/engine/agentlas-native-host.cjs +69 -4
  9. package/engine/agentlas-onboard.cjs +20 -0
  10. package/engine/agentlas-ui.cjs +2 -0
  11. package/engine/agentlas-workforce.cjs +69 -13
  12. package/engine/agentlas.cjs +71 -3
  13. package/engine/agents/builder.cjs +84 -0
  14. package/engine/agents/router.cjs +8 -2
  15. package/engine/automation/launchd.cjs +131 -0
  16. package/engine/bootstrap-schema.sql +6 -44
  17. package/engine/browser/cdp.cjs +188 -0
  18. package/engine/browser/vault.cjs +124 -0
  19. package/engine/cli-output.cjs +262 -0
  20. package/engine/cloud-assets/package.cjs +5 -1
  21. package/engine/commands/automation.cjs +37 -1
  22. package/engine/commands/billing.cjs +3 -0
  23. package/engine/commands/browser.cjs +166 -8
  24. package/engine/commands/build.cjs +101 -20
  25. package/engine/commands/connect.cjs +162 -9
  26. package/engine/commands/creds.cjs +65 -3
  27. package/engine/commands/doctor.cjs +66 -4
  28. package/engine/commands/document.cjs +79 -0
  29. package/engine/commands/graph.cjs +1190 -0
  30. package/engine/commands/help.cjs +87 -9
  31. package/engine/commands/hep-cloud.cjs +9 -23
  32. package/engine/commands/hep-hub.cjs +9 -22
  33. package/engine/commands/hep-local.cjs +9 -24
  34. package/engine/commands/hep-network.cjs +9 -35
  35. package/engine/commands/index.cjs +50 -25
  36. package/engine/commands/list.cjs +10 -1
  37. package/engine/commands/mcp.cjs +6 -2
  38. package/engine/commands/native.cjs +18 -2
  39. package/engine/commands/plugin.cjs +22 -0
  40. package/engine/commands/project.cjs +79 -16
  41. package/engine/commands/roles.cjs +210 -0
  42. package/engine/commands/telegram.cjs +23 -16
  43. package/engine/commands/workforce.cjs +63 -12
  44. package/engine/core/desktop-core-fetch.cjs +98 -0
  45. package/engine/core/desktop-core.cjs +170 -0
  46. package/engine/graph/ask-model.cjs +159 -0
  47. package/engine/graph/interview.cjs +960 -0
  48. package/engine/graph/layout.cjs +139 -0
  49. package/engine/graph/package.cjs +223 -0
  50. package/engine/graph/vocabulary.generated.cjs +30 -0
  51. package/engine/hephaestus/local-core.cjs +159 -0
  52. package/engine/hephaestus/runtime.cjs +14 -10
  53. package/engine/project/controller.cjs +8 -8
  54. package/engine/project/team.cjs +99 -0
  55. package/engine/runtime-refusal.cjs +71 -0
  56. package/engine/runtimes/auth-evidence.cjs +78 -0
  57. package/engine/runtimes/detect.cjs +3 -0
  58. package/engine/runtimes/resolve.cjs +1 -1
  59. package/engine/sessions/prompt.cjs +16 -0
  60. package/engine/sessions/session.cjs +24 -2
  61. package/engine/telegram/connect.cjs +202 -0
  62. package/engine/tools/access-notice.cjs +86 -0
  63. package/engine/ui/palette.cjs +7 -3
  64. package/engine/ui/repl.cjs +120 -3
  65. package/engine/vendor/desktop-core.manifest.json +7 -0
  66. package/engine/workforce/capture.cjs +51 -1
  67. package/engine/workforce/deps.cjs +13 -0
  68. package/engine/workforce/local-core-transport.cjs +298 -0
  69. package/package.json +4 -2
  70. package/engine/commands/legacy-network.cjs +0 -29
@@ -1,19 +1,177 @@
1
1
  "use strict";
2
2
  /*
3
- * browser — 실제 브라우저 실행 하드포인트 (hep-browser 라우트).
3
+ * browser — 실제 브라우저 실행 하드포인트 + 사이트 로그인 볼트·조종 (2026-08-06 확장).
4
4
  *
5
- * v1 디스패처 매핑 그대로:
6
- * `agentlas browser <url-or-query|sub…>` cmdHep(["hep-browser", ...rest])
7
- * v1은 browser에 무인자 가드가 없었다(missingArgumentUsage 미포함)
8
- * 무인자도 hep-browser 패스스루로 넘어가 네이티브 usage가 나온다. 보존.
5
+ * 오너 원칙("조종을 다른 흐름으로 확장"): telegram 뿐 아니라 데스크탑의 브라우저-볼트
6
+ * 흐름(사이트별 전용-프로필 로그인/세션)도 터미널에서 조종·공유되어야 한다. 저장 테이블은
7
+ * 데스크탑과 공유(browser_sites/…)하고, 조종은 CDP(engine/browser/cdp.cjs) 한다.
8
+ *
9
+ * 하위 명령(볼트·조종):
10
+ * browser status 브라우저(Chrome/CDP) 준비 상태 + 사이트 세션 요약
11
+ * browser sites 저장된 사이트와 로그인 세션 상태(valid/expired/none)
12
+ * browser add <site> [--label L] [--user U] 사이트 카드 추가(비밀번호 없음)
13
+ * browser login <site> 그 사이트를 Agentlas 브라우저로 연다 — 로그인은 사용자가 직접
14
+ * browser mark <site> <valid|expired|none> 로그인 뒤 세션 상태를 기록
15
+ * browser go <url> 이미 열린 Agentlas 브라우저를 그 URL 로 몬다(조종)
16
+ * browser rm <site> 사이트 카드 삭제
17
+ *
18
+ * 그 외(무인자 포함, URL·검색어)는 v1 그대로 hep-browser 하드포인트로 넘겨 Chrome 을 띄운다.
19
+ *
20
+ * 보안(데스크탑과 동일): 비밀번호를 받지도, 자동 입력하지도 않는다. 로그인은 제공자 페이지에서
21
+ * 사용자가 직접 한다 — 터미널은 페이지를 열어 주고 세션 상태만 기록한다.
9
22
  */
10
23
  const { create, usageFor, isHelpToken } = require("../hephaestus/runtime.cjs");
24
+ const vault = require("../browser/vault.cjs");
25
+ const cdp = require("../browser/cdp.cjs");
11
26
 
12
- async function run(ctx, args) {
13
- if (args.some(isHelpToken)) {
14
- ctx.out(usageFor("browser", ctx.lang));
27
+ const SUBS = new Set(["status", "sites", "add", "login", "mark", "go", "rm", "remove"]);
28
+
29
+ function flag(rest, name) {
30
+ const i = rest.findIndex((a) => a === `--${name}`);
31
+ return i >= 0 && rest[i + 1] ? rest[i + 1] : null;
32
+ }
33
+ function positional(rest) { return rest.find((a) => a && !String(a).startsWith("-")) || null; }
34
+
35
+ function siteUrl(site) { return /^[a-z][a-z0-9+.-]*:\/\//i.test(site) ? site : `https://${site}`; }
36
+
37
+ function statusDot(ui, status) {
38
+ if (status === "valid") return ui.green("●");
39
+ if (status === "expired") return ui.yellow ? ui.yellow("●") : ui.dim("●");
40
+ return ui.dim("○");
41
+ }
42
+
43
+ async function browserStatus(ctx) {
44
+ const ko = ctx.lang === "ko";
45
+ const ready = await cdp.cdpReady();
46
+ ctx.out(`${ready ? ctx.ui.green("✓") : ctx.ui.dim("○")} ${ko ? "Agentlas 브라우저(CDP)" : "Agentlas browser (CDP)"}: ${ready ? (ko ? "실행 중 — 조종 가능" : "running — pilotable") : (ko ? "꺼짐" : "not running")} (port ${cdp.DEFAULT_PORT})`);
47
+ const sites = vault.listBrowserSites(ctx.db());
48
+ if (!sites.length) {
49
+ ctx.out(ctx.ui.dim(ko ? "저장된 사이트 없음. 추가: agentlas browser add <site>" : "No saved sites. Add one: agentlas browser add <site>"));
50
+ return 0;
51
+ }
52
+ const valid = sites.filter((s) => s.session.status === "valid").length;
53
+ ctx.out(ctx.ui.dim(ko ? `사이트 ${sites.length}개 (로그인 유효 ${valid}개) — 목록: agentlas browser sites` : `${sites.length} sites (${valid} logged in) — list: agentlas browser sites`));
54
+ return 0;
55
+ }
56
+
57
+ function browserSites(ctx) {
58
+ const ko = ctx.lang === "ko";
59
+ const sites = vault.listBrowserSites(ctx.db());
60
+ if (!sites.length) {
61
+ ctx.out(ctx.ui.dim(ko ? "저장된 사이트가 없습니다. 추가: agentlas browser add <site>" : "No saved sites yet. Add one: agentlas browser add <site>"));
15
62
  return 0;
16
63
  }
64
+ for (const s of sites) {
65
+ const when = s.session.capturedAt ? new Date(s.session.capturedAt).toLocaleDateString() : "";
66
+ const label = s.label ? ` ${ctx.ui.dim(s.label)}` : "";
67
+ const user = s.username ? ctx.ui.dim(` (${s.username})`) : "";
68
+ ctx.out(`${statusDot(ctx.ui, s.session.status)} ${s.site}${label}${user} ${ctx.ui.dim(`${s.session.status}${when ? " · " + when : ""}`)}`);
69
+ }
70
+ return 0;
71
+ }
72
+
73
+ function browserAdd(ctx, rest) {
74
+ const ko = ctx.lang === "ko";
75
+ const site = positional(rest);
76
+ if (!site) { ctx.err(ko ? "사용법: agentlas browser add <site> [--label 이름] [--user 아이디]" : "Usage: agentlas browser add <site> [--label name] [--user handle]"); return 1; }
77
+ try {
78
+ const row = vault.upsertBrowserSite(ctx.db(), { site, label: flag(rest, "label"), username: flag(rest, "user") });
79
+ vault.logBrowserAction(ctx.db(), { site: row.site, action: "vault.save", result: "ok" });
80
+ ctx.out(`${ctx.ui.green("✓")} ${ko ? "사이트를 저장했습니다" : "site saved"}: ${row.site}`);
81
+ ctx.out(ctx.ui.dim(ko ? `로그인: agentlas browser login ${row.site}` : `Log in: agentlas browser login ${row.site}`));
82
+ return 0;
83
+ } catch (e) { ctx.err(`${ctx.ui.red("✖")} ${String((e && e.message) || e)}`); return 1; }
84
+ }
85
+
86
+ async function browserLogin(ctx, rest) {
87
+ const ko = ctx.lang === "ko";
88
+ const site = positional(rest);
89
+ if (!site) { ctx.err(ko ? "사용법: agentlas browser login <site>" : "Usage: agentlas browser login <site>"); return 1; }
90
+ const db = ctx.db();
91
+ let row;
92
+ try { row = vault.upsertBrowserSite(db, { site }); } catch (e) { ctx.err(`${ctx.ui.red("✖")} ${String((e && e.message) || e)}`); return 1; }
93
+ const url = siteUrl(row.site);
94
+
95
+ if (await cdp.cdpReady()) {
96
+ // 이미 열린 Agentlas 브라우저를 로그인 페이지로 몬다(조종). 로그인은 사용자가 직접.
97
+ try {
98
+ const page = await cdp.attachPage();
99
+ try { await page.navigate(url, { waitMs: 1500 }); } finally { page.close(); }
100
+ vault.logBrowserAction(db, { site: row.site, action: "login.open", target: url, result: "navigated" });
101
+ ctx.out(`${ctx.ui.green("✓")} ${ko ? "브라우저를 로그인 페이지로 옮겼습니다" : "moved the browser to the login page"}: ${url}`);
102
+ } catch (e) {
103
+ ctx.err(`${ctx.ui.red("✖")} ${String((e && e.message) || e)}`);
104
+ return 1;
105
+ }
106
+ } else {
107
+ // CDP 미기동 — 조종할 대상이 없다. 먼저 브라우저를 띄우도록 정직하게 안내(자동 실행 안 함).
108
+ ctx.out(ko
109
+ ? `Agentlas 브라우저가 아직 실행 중이 아닙니다. 먼저 열어 주세요:\n agentlas browser ${url}`
110
+ : `The Agentlas browser is not running yet. Open it first:\n agentlas browser ${url}`);
111
+ }
112
+ ctx.out(ctx.ui.dim(ko
113
+ ? `그 창에서 직접 로그인한 뒤, 세션을 기록하세요:\n agentlas browser mark ${row.site} valid`
114
+ : `Log in yourself in that window, then record the session:\n agentlas browser mark ${row.site} valid`));
115
+ ctx.out(ctx.ui.dim(ko ? "비밀번호는 받지도, 자동 입력하지도 않습니다 — 로그인은 사용자만." : "No password is ever taken or auto-typed — you log in, not the tool."));
116
+ return 0;
117
+ }
118
+
119
+ function browserMark(ctx, rest) {
120
+ const ko = ctx.lang === "ko";
121
+ const site = positional(rest);
122
+ const status = rest.filter((a) => a && !String(a).startsWith("-") && a !== site)[0];
123
+ if (!site || !status) { ctx.err(ko ? "사용법: agentlas browser mark <site> <valid|expired|none>" : "Usage: agentlas browser mark <site> <valid|expired|none>"); return 1; }
124
+ try {
125
+ const row = vault.setBrowserSession(ctx.db(), site, status);
126
+ vault.logBrowserAction(ctx.db(), { site: row.site, action: "session.mark", result: status });
127
+ ctx.out(`${ctx.ui.green("✓")} ${row.site}: ${ko ? "세션 상태" : "session"} → ${status}`);
128
+ return 0;
129
+ } catch (e) { ctx.err(`${ctx.ui.red("✖")} ${String((e && e.message) || e)}`); return 1; }
130
+ }
131
+
132
+ async function browserGo(ctx, rest) {
133
+ const ko = ctx.lang === "ko";
134
+ const url = positional(rest);
135
+ if (!url) { ctx.err(ko ? "사용법: agentlas browser go <url>" : "Usage: agentlas browser go <url>"); return 1; }
136
+ if (!(await cdp.cdpReady())) {
137
+ ctx.err(ko
138
+ ? `Agentlas 브라우저가 실행 중이 아닙니다. 먼저 열어 주세요: agentlas browser ${siteUrl(url)}`
139
+ : `The Agentlas browser is not running. Open it first: agentlas browser ${siteUrl(url)}`);
140
+ return 1;
141
+ }
142
+ try {
143
+ const page = await cdp.attachPage();
144
+ let title;
145
+ try { await page.navigate(siteUrl(url), { waitMs: 1500 }); title = await page.evalExpr("document.title"); } finally { page.close(); }
146
+ ctx.out(`${ctx.ui.green("✓")} ${ko ? "이동했습니다" : "navigated"}: ${siteUrl(url)}${title ? ctx.ui.dim(` — ${title}`) : ""}`);
147
+ return 0;
148
+ } catch (e) { ctx.err(`${ctx.ui.red("✖")} ${String((e && e.message) || e)}`); return 1; }
149
+ }
150
+
151
+ function browserRemove(ctx, rest) {
152
+ const ko = ctx.lang === "ko";
153
+ const site = positional(rest);
154
+ if (!site) { ctx.err(ko ? "사용법: agentlas browser rm <site>" : "Usage: agentlas browser rm <site>"); return 1; }
155
+ vault.deleteBrowserSite(ctx.db(), site);
156
+ vault.logBrowserAction(ctx.db(), { site: vault.normalizeSite(site), action: "vault.delete", result: "ok" });
157
+ ctx.out(`${ctx.ui.green("✓")} ${ko ? "사이트를 삭제했습니다" : "site removed"}: ${vault.normalizeSite(site) || site}`);
158
+ return 0;
159
+ }
160
+
161
+ async function run(ctx, args) {
162
+ if (args.some(isHelpToken)) { ctx.out(usageFor("browser", ctx.lang)); return 0; }
163
+ const sub = String(args[0] || "").toLowerCase();
164
+ const rest = args.slice(1);
165
+ if (SUBS.has(sub)) {
166
+ if (sub === "status") return browserStatus(ctx);
167
+ if (sub === "sites") return browserSites(ctx);
168
+ if (sub === "add") return browserAdd(ctx, rest);
169
+ if (sub === "login") return browserLogin(ctx, rest);
170
+ if (sub === "mark") return browserMark(ctx, rest);
171
+ if (sub === "go") return browserGo(ctx, rest);
172
+ if (sub === "rm" || sub === "remove") return browserRemove(ctx, rest);
173
+ }
174
+ // 그 외(무인자·URL·검색어)는 v1 그대로 hep-browser 하드포인트로 — Chrome 을 실제로 띄운다.
17
175
  return create(ctx).cmdHep(["hep-browser", ...args]);
18
176
  }
19
177
 
@@ -1,33 +1,114 @@
1
1
  "use strict";
2
2
  /*
3
- * build — 에이전트/팀 빌드·수리·패키징 (hep-build 라우트).
3
+ * build — 에이전트를 로컬에서 빌드·설치한다 (독립, 2026-08-06 재작성).
4
4
  *
5
- * v1 디스패처 검증 메모 (legacy-v1-engine-snapshot engine/agentlas.cjs ~13097):
6
- * v1의 `agentlas build`는 hep-build 단순 패스스루가 아니라 터미널 소유
7
- * 빌더(terminalAssets.cmdBuild: 시스템 MCP 메타데이터 프리플라이트 1회
8
- * 동의 → Meta-Agent 실행)로 갔다. 그 프리플라이트/빌더 체인은 engine/mcp/*
9
- * 와 빌더 서브시스템 소유라 이 클러스터의 범위 밖이다.
10
- * v2의 이 파일은 v1 도움말이 계약으로 명시한 표면 —
11
- * `build "<request>" … (hep-build)` — 즉 Agentlas OS hep-build 라우트를
12
- * 그대로 노출한다. 터미널 소유 MCP-동의 빌더가 v2에 재구축되면 그 모듈이
13
- * 이 명령을 대체(또는 선행)해야 한다.
5
+ * 이전: `build "<req>"`는 Hephaestus 네이티브 hep-build 패스스루였고, 그 네이티브는
6
+ * "Open Claude Code or Codex with the plugin, then /hep-build"라는 스텁만 냈다
7
+ * 플러그인을 강제해 오너 원칙(데스크탑/플러그인은 공유하되 전제 아님)을 어겼다.
14
8
  *
15
- * v1 인자 가드 그대로:
16
- * - help 토큰 usage 출력, exit 0
17
- * - 무인자 → usage 실패, exit 1 (요청 문자열이 라우터로 새는 방지)
9
+ * 지금: 터미널 자체 런타임으로 로컬 빌더(agents/builder.cjs)를 `run`과 같은
10
+ * 실행 인프라(Orchestrator 세션)로 돌려 import 가능한 폴더를 만들고, 성공하면
11
+ * 자동 설치한다. 폴더는 남으므로 자동 설치가 실패해도 `agentlas import`로 복구 가능.
18
12
  */
19
- const { create, usageFor, isHelpToken } = require("../hephaestus/runtime.cjs");
13
+ const { Orchestrator } = require("../sessions/orchestrator.cjs");
14
+ const { Renderer } = require("../ui/renderer.cjs");
15
+ const { resolveRuntimeForAgent } = require("../runtimes/overrides.cjs");
16
+ const permissions = require("../agentlas-permissions.cjs");
17
+ const { projectCwd } = require("../project/paths.cjs");
18
+ const { ensureTerminalProjectForExecutionCli } = require("../project/state.cjs");
19
+ const { ensureBuilderAgent, parseBuiltFolder } = require("../agents/builder.cjs");
20
+ const { importLocalFolder } = require("../agents/import-local.cjs");
21
+ const path = require("node:path");
22
+ const fs = require("node:fs");
23
+
24
+ function usage(ko) {
25
+ return ko
26
+ ? "사용법: agentlas build \"<만들고 싶은 에이전트>\" [--runtime <kind>] [--print]"
27
+ : "Usage: agentlas build \"<the agent you want>\" [--runtime <kind>] [--print]";
28
+ }
20
29
 
21
30
  async function run(ctx, args) {
22
- if (args.some(isHelpToken)) {
23
- ctx.out(usageFor("build", ctx.lang));
24
- return 0;
31
+ const ko = ctx.lang === "ko";
32
+ if (args.some((a) => a === "--help" || a === "-h" || a === "help")) { ctx.out(usage(ko)); return 0; }
33
+
34
+ // --runtime/--print 만 벗겨내고 나머지는 요청 문장.
35
+ const flags = {};
36
+ const rest = [];
37
+ for (let i = 0; i < args.length; i += 1) {
38
+ if (args[i] === "--runtime" && args[i + 1]) { flags.runtime = args[++i]; continue; }
39
+ if (args[i] === "--print" || args[i] === "-p") { flags.print = true; continue; }
40
+ rest.push(args[i]);
41
+ }
42
+ const request = rest.join(" ").trim();
43
+ if (!request) { ctx.err("✖ " + usage(ko)); return 1; }
44
+
45
+ const db = ctx.db();
46
+ const cwd = projectCwd();
47
+ const agent = ensureBuilderAgent(db);
48
+
49
+ let runtime;
50
+ try {
51
+ runtime = resolveRuntimeForAgent({
52
+ db, prefs: ctx.prefs, explicit: flags.runtime, role: "orchestrator", agentId: agent.id,
53
+ });
54
+ } catch (e) {
55
+ // no_runtime 등 정직 정지 그대로.
56
+ ctx.err(String((e && e.message) || e));
57
+ return 1;
58
+ }
59
+
60
+ // 빌더는 파일을 써야 한다 — write 권한으로 실행한다(현재 폴더에 패키지 생성).
61
+ const permission = permissions.normalize("write");
62
+ try { ensureTerminalProjectForExecutionCli(db, cwd, permission, "terminal-build"); } catch { /* 프로젝트 없어도 빌드는 됨 */ }
63
+
64
+ const orch = new Orchestrator({ db, lang: ctx.lang });
65
+ const session = orch.spawn({ agent, runtime, permission, cwd, title: `build: ${request.slice(0, 48)}` });
66
+
67
+ let renderer = null;
68
+ if (!flags.print) {
69
+ renderer = new Renderer(ctx.uiInstance);
70
+ renderer.attach(session, { replay: false });
71
+ ctx.err(ctx.uiInstance.c.dim(`${agent.slug} · ${runtime.kind}${runtime.model ? ` · ${runtime.model}` : ""}`));
25
72
  }
26
- if (!args.length) {
27
- ctx.err("✖ " + usageFor("build", ctx.lang));
73
+
74
+ const res = await session.send(request);
75
+ if (renderer) renderer.detach();
76
+ const finalText = (res && (res.finalText || res.text)) || "";
77
+ if (flags.print && finalText) process.stdout.write(finalText.trimEnd() + "\n");
78
+
79
+ if (session.status === "failed") {
80
+ if (session.lastError) ctx.err(session.lastError);
28
81
  return 1;
29
82
  }
30
- return create(ctx).cmdHep(["hep-build", ...args]);
83
+
84
+ // 빌더가 `BUILT: <folder>`를 남겼으면 자동 설치한다.
85
+ const built = parseBuiltFolder(finalText);
86
+ if (!built) {
87
+ ctx.out(ctx.uiInstance.c.dim(ko
88
+ ? "빌드 산출물 위치를 확정하지 못했습니다. 만들어진 폴더를 확인해 `agentlas import <폴더>`로 설치하세요."
89
+ : "Could not confirm the built folder. Check the created folder and install it with `agentlas import <folder>`."));
90
+ return 0;
91
+ }
92
+ const builtPath = path.isAbsolute(built) ? built : path.join(cwd, built);
93
+ if (!fs.existsSync(builtPath)) {
94
+ ctx.out(ctx.uiInstance.c.dim(ko
95
+ ? `빌더가 알린 폴더가 없습니다: ${built}. 만들어진 폴더를 확인해 \`agentlas import\`로 설치하세요.`
96
+ : `The reported folder does not exist: ${built}. Check the created folder and install it with \`agentlas import\`.`));
97
+ return 0;
98
+ }
99
+ try {
100
+ const imported = importLocalFolder(db, builtPath);
101
+ ctx.out(`${ctx.uiInstance.c.green("✓")} ${ko ? "설치됨" : "installed"}: ${imported.slug} — ${imported.name}`);
102
+ ctx.out(ctx.uiInstance.c.dim(ko
103
+ ? `실행: agentlas run ${imported.slug} "<할 일>"`
104
+ : `Run it: agentlas run ${imported.slug} "<task>"`));
105
+ return 0;
106
+ } catch (e) {
107
+ ctx.out(ctx.uiInstance.c.dim(ko
108
+ ? `자동 설치 실패(${String((e && e.message) || e)}). 폴더는 남아 있습니다: ${builtPath}. \`agentlas import ${builtPath}\`로 설치하세요.`
109
+ : `Auto-install failed (${String((e && e.message) || e)}). The folder remains at ${builtPath}. Install with \`agentlas import ${builtPath}\`.`));
110
+ return 0;
111
+ }
31
112
  }
32
113
 
33
114
  module.exports = { run };
@@ -1,20 +1,173 @@
1
1
  "use strict";
2
2
  /*
3
- * connect — Telegram 플랫폼 연결 (hep-connect 라우트).
3
+ * connect — Telegram 연결·조종 (2026-08-06 독립화).
4
4
  *
5
- * v1 디스패처 매핑 그대로:
6
- * `agentlas connect <sub…>` cmdHep(["hep-connect", ...rest])
7
- * 무인자 connect는 v1에서 fail아니라 usage 안내 exit 0이었다
8
- * (topLevelCommandUsage("connect") 특례) 그대로 보존.
5
+ * 오너 원칙: 데스크탑에서 쉽게 되는 telegram 연결이 터미널에서도 돼야 한다.
6
+ * 데스크탑 electron/telegram/connect.ts 실제 연결 코어는 순수 HTTPS
7
+ * (api.telegram.org)이고 Electron필요 없다 브라우저 자동 조종은 봇 자동
8
+ * 생성 편의일 뿐. 그 코어를 engine/telegram/connect.cjs 이식했다. 여기서는
9
+ * CLI 표면만 배선한다.
10
+ *
11
+ * 이전: connect 는 hep-connect 플러그인 패스스루라 라우터 원시 JSON 을 덤프했다.
12
+ *
13
+ * 하위 명령:
14
+ * connect 상태 표
15
+ * connect telegram <agent|firm> 이 대상에 봇을 연결(토큰 stdin) + 방 페어링
16
+ * connect test <id> 연결된 방에 확인 메시지
17
+ * connect remove <id> 연결 제거(토큰 파일도 삭제)
9
18
  */
10
- const { create, usageFor, isHelpToken } = require("../hephaestus/runtime.cjs");
19
+ const { renderTelegram } = require("./telegram.cjs");
20
+ const tg = require("../telegram/connect.cjs");
21
+ const { findAgent } = require("../agents/registry.cjs");
22
+ const readline = require("node:readline");
23
+
24
+ function usage(ko) {
25
+ 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> ]";
28
+ }
29
+
30
+ function resolveTarget(db, token) {
31
+ const agent = findAgent(db, token);
32
+ if (agent) return { kind: "agent", id: agent.id, name: agent.name || agent.slug };
33
+ try {
34
+ const q = String(token).trim().toLowerCase();
35
+ const firm = db.prepare("SELECT id, slug, name FROM firms WHERE lower(id)=? OR lower(slug)=? OR lower(name)=?").get(q, q, q)
36
+ || db.prepare("SELECT id, slug, name FROM firms WHERE lower(slug) LIKE ? ORDER BY slug LIMIT 1").get(`%${q}%`);
37
+ if (firm) return { kind: "firm", id: firm.id, name: firm.name || firm.slug };
38
+ } catch { /* no firms table */ }
39
+ return null;
40
+ }
41
+
42
+ /** 봇 토큰을 stdin 에서 읽는다(비밀은 argv 금지). 파이프면 첫 줄, TTY면 숨김 안내. */
43
+ async function readTokenFromStdin(ctx, ko) {
44
+ if (!process.stdin.isTTY) {
45
+ return new Promise((resolve) => {
46
+ let buf = "";
47
+ process.stdin.on("data", (d) => { buf += d; });
48
+ process.stdin.on("end", () => resolve(buf.split(/\r?\n/)[0].trim()));
49
+ });
50
+ }
51
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
52
+ ctx.out(ko
53
+ ? "@BotFather 에서 봇을 만들고 받은 토큰을 붙여넣으세요 (입력은 화면에 남습니다 — 끝나면 창을 지우세요):"
54
+ : "Create a bot with @BotFather and paste the token (it echoes — clear your screen after):");
55
+ return new Promise((resolve) => {
56
+ rl.question("token> ", (answer) => { rl.close(); resolve(String(answer).trim()); });
57
+ });
58
+ }
59
+
60
+ async function connectTelegram(ctx, targetToken, { auto = false } = {}) {
61
+ const ko = ctx.lang === "ko";
62
+ const db = ctx.db();
63
+ const target = resolveTarget(db, targetToken);
64
+ if (!target) {
65
+ ctx.err((ko ? "대상을 찾을 수 없습니다: " : "target not found: ") + targetToken);
66
+ ctx.err(ctx.ui.dim(ko ? "에이전트·회사 목록: agentlas list" : "list agents and companies: agentlas list"));
67
+ return 1;
68
+ }
69
+
70
+ let token;
71
+ if (auto) {
72
+ // --auto: 터미널이 Agentlas 브라우저(CDP)로 BotFather 를 열고 토큰을 직접 읽는다.
73
+ ctx.out(ko
74
+ ? "Agentlas 브라우저에서 BotFather 를 엽니다. 필요하면 로그인(QR)한 뒤, `/newbot` 으로 봇을 만드세요 — 토큰은 제가 화면에서 읽어옵니다."
75
+ : "Opening BotFather in the Agentlas browser. Log in (QR) if needed, then create a bot with `/newbot` — I'll read the token off the page.");
76
+ try {
77
+ if (typeof ctx.ui.startSpinner === "function") ctx.ui.startSpinner(ko ? "BotFather 토큰 대기 중…" : "Waiting for a BotFather token…");
78
+ token = await require("../telegram/connect.cjs").captureBotTokenViaBrowser({ timeoutMs: 180_000 });
79
+ } catch (e) {
80
+ if (typeof ctx.ui.stopSpinner === "function") ctx.ui.stopSpinner();
81
+ if (e && e.code === "cdp_unavailable") {
82
+ ctx.err(ko
83
+ ? "Agentlas 브라우저가 실행 중이 아닙니다. 먼저 `agentlas browser https://web.telegram.org` 로 브라우저를 띄운 뒤 다시 --auto 하거나, --auto 없이 토큰을 직접 붙여넣으세요."
84
+ : "The Agentlas browser is not running. Start it with `agentlas browser https://web.telegram.org`, then retry --auto, or paste the token without --auto.");
85
+ return 1;
86
+ }
87
+ ctx.err(`${ctx.ui.red("✖")} ${String((e && e.message) || e)}`);
88
+ return 1;
89
+ }
90
+ if (typeof ctx.ui.stopSpinner === "function") ctx.ui.stopSpinner();
91
+ if (!token) {
92
+ ctx.err(ko ? "시간 내 토큰을 못 읽었습니다(3분). 다시 시도하거나 --auto 없이 붙여넣으세요." : "No token captured in time (3 min). Retry, or paste it without --auto.");
93
+ return 1;
94
+ }
95
+ ctx.out(ctx.ui.dim(ko ? "토큰을 읽었습니다." : "Captured the token."));
96
+ } else {
97
+ token = await readTokenFromStdin(ctx, ko);
98
+ if (!token) { ctx.err(ko ? "토큰이 필요합니다." : "a bot token is required."); return 1; }
99
+ }
100
+
101
+ let started;
102
+ try {
103
+ if (typeof ctx.ui.startSpinner === "function") ctx.ui.startSpinner(ko ? "봇 토큰 확인 중…" : "Verifying bot token…");
104
+ started = await tg.startConnection(db, target.kind, target.id, token);
105
+ } catch (e) {
106
+ if (typeof ctx.ui.stopSpinner === "function") ctx.ui.stopSpinner();
107
+ ctx.err(`${ctx.ui.red("✖")} ${String((e && e.message) || e)}`);
108
+ return 1;
109
+ }
110
+ if (typeof ctx.ui.stopSpinner === "function") ctx.ui.stopSpinner();
111
+ const botAt = started.botUsername ? "@" + started.botUsername : "(bot)";
112
+ ctx.out(`${ctx.ui.green("✓")} ${ko ? "봇 확인됨" : "bot verified"}: ${botAt} → ${target.name}`);
113
+ ctx.out(ko
114
+ ? `이제 텔레그램에서 ${botAt} 에게 아무 메시지나 보내세요 (예: /start). 방을 기다립니다…`
115
+ : `Now message ${botAt} on Telegram (e.g. /start). Waiting for the chat…`);
116
+
117
+ let paired;
118
+ try {
119
+ if (typeof ctx.ui.startSpinner === "function") ctx.ui.startSpinner(ko ? "방 연결 대기 중…" : "Waiting for the chat…");
120
+ paired = await tg.pairByPolling(db, started.id, { timeoutMs: 120_000 });
121
+ } catch (e) {
122
+ if (typeof ctx.ui.stopSpinner === "function") ctx.ui.stopSpinner();
123
+ ctx.err(`${ctx.ui.red("✖")} ${String((e && e.message) || e)}`);
124
+ return 1;
125
+ }
126
+ if (typeof ctx.ui.stopSpinner === "function") ctx.ui.stopSpinner();
127
+
128
+ if (!paired) {
129
+ ctx.out(ctx.ui.dim(ko
130
+ ? `방을 못 받았습니다(2분 초과). ${botAt} 에게 메시지를 보낸 뒤 다시: agentlas connect test ${started.id}`
131
+ : `No chat received (2 min). Message ${botAt}, then retry: agentlas connect test ${started.id}`));
132
+ return 0;
133
+ }
134
+ ctx.out(`${ctx.ui.green("✓")} ${ko ? "방 연결됨" : "chat paired"}: ${paired.telegram_chat_title || paired.telegram_chat_id}`);
135
+ try {
136
+ await tg.sendTest(db, started.id, ko ? `Agentlas 연결 완료 — ${target.name}` : `Agentlas connected — ${target.name}`);
137
+ ctx.out(ctx.ui.dim(ko ? "확인 메시지를 보냈습니다." : "Sent a confirmation message."));
138
+ } catch { /* 페어링은 됐으니 테스트 실패는 치명적 아님 */ }
139
+ return 0;
140
+ }
11
141
 
12
142
  async function run(ctx, args) {
13
- if (args.some(isHelpToken) || !args.length) {
14
- ctx.out(usageFor("connect", ctx.lang));
143
+ const ko = ctx.lang === "ko";
144
+ const sub = String(args[0] || "").toLowerCase();
145
+
146
+ if (!sub || sub === "help" || sub === "--help" || sub === "-h") { ctx.out(usage(ko)); return 0; }
147
+ if (sub === "status") return renderTelegram(ctx);
148
+ if (sub === "telegram") {
149
+ const rest = args.slice(1);
150
+ const auto = rest.includes("--auto");
151
+ const targetToken = rest.find((a) => a && !String(a).startsWith("-"));
152
+ if (!targetToken) { ctx.err(usage(ko)); return 1; }
153
+ return connectTelegram(ctx, targetToken, { auto });
154
+ }
155
+ if (sub === "test") {
156
+ if (!args[1]) { ctx.err(ko ? "사용법: agentlas connect test <id>" : "Usage: agentlas connect test <id>"); return 1; }
157
+ try {
158
+ await tg.sendTest(ctx.db(), args[1], ko ? "Agentlas 테스트 메시지" : "Agentlas test message");
159
+ ctx.out(`${ctx.ui.green("✓")} ${ko ? "메시지를 보냈습니다." : "message sent."}`);
160
+ return 0;
161
+ } catch (e) { ctx.err(`${ctx.ui.red("✖")} ${String((e && e.message) || e)}`); return 1; }
162
+ }
163
+ if (sub === "remove") {
164
+ if (!args[1]) { ctx.err(ko ? "사용법: agentlas connect remove <id>" : "Usage: agentlas connect remove <id>"); return 1; }
165
+ tg.removeBinding(ctx.db(), args[1]);
166
+ ctx.out(`${ctx.ui.green("✓")} ${ko ? "연결을 제거했습니다." : "connection removed."}`);
15
167
  return 0;
16
168
  }
17
- return create(ctx).cmdHep(["hep-connect", ...args]);
169
+ ctx.err(usage(ko));
170
+ return 1;
18
171
  }
19
172
 
20
173
  module.exports = { run };
@@ -134,17 +134,79 @@ async function cmdCredsFile(ctx, args) {
134
134
  return 0;
135
135
  }
136
136
 
137
+ /** KEY=value 파일에서 KEY 이름만 뽑는다 — 값은 절대 읽어 돌려주지 않는다. */
138
+ function envKeyNames(filePath) {
139
+ try {
140
+ return fs.readFileSync(filePath, "utf8")
141
+ .split(/\r?\n/)
142
+ .map((line) => line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/))
143
+ .filter(Boolean)
144
+ .map((m) => m[1]);
145
+ } catch {
146
+ return null; // 파일 없음/읽기 불가 — 목록에서 침묵이 아니라 부재로 표시
147
+ }
148
+ }
149
+
150
+ /*
151
+ * creds list — 어떤 자격증명 "이름"이 어디에 저장돼 있는지.
152
+ * 배경(2026-08-05 감사, 결함 H): save/file만 있어 저장한 것을 확인할 방법이
153
+ * 없었다. 계약은 상단과 동일 — 값은 어떤 경로로도 출력하지 않는다. 키 이름과
154
+ * 저장 위치까지만. (키체인 vault는 standalone에서 keytar를 부르면 무한 대기하는
155
+ * v1 실사고가 있어 조회조차 하지 않는다 — 부재가 아니라 "미조회"로 표시.)
156
+ */
157
+ function cmdCredsList(ctx) {
158
+ const ko = ctx.lang !== "en";
159
+ const project = activeProjectPath(ctx.db());
160
+ const stores = [];
161
+ if (project) stores.push({ label: (ko ? "프로젝트 .env — " : "project .env — ") + path.join(project, ".env"), keys: envKeyNames(path.join(project, ".env")) });
162
+ stores.push({ label: (ko ? "전역 credentials.env — " : "global credentials.env — ") + path.join(userDataDir(), "credentials.env"), keys: envKeyNames(path.join(userDataDir(), "credentials.env")) });
163
+ stores.push({ label: "~/.agentlas/credentials.env", keys: envKeyNames(path.join(os.homedir(), ".agentlas", "credentials.env")) });
164
+
165
+ let any = false;
166
+ for (const store of stores) {
167
+ if (store.keys === null) continue; // 파일 자체가 없으면 줄을 만들지 않는다
168
+ any = true;
169
+ ctx.out(ctx.ui.bold(store.label));
170
+ if (!store.keys.length) ctx.out(ctx.ui.dim(ko ? " (비어 있음)" : " (empty)"));
171
+ for (const key of store.keys) ctx.out(` ${key}${ctx.ui.dim("=•••")}`);
172
+ }
173
+ if (isElectronRuntime()) {
174
+ ctx.out(ctx.ui.dim(ko ? "키체인 vault: 데스크탑에서 확인" : "keychain vault: inspect in Desktop"));
175
+ } else {
176
+ ctx.out(ctx.ui.dim(ko
177
+ ? "키체인 vault는 standalone 터미널에서 조회하지 않습니다(서명 없는 keytar 호출은 키체인에 막혀 멈춥니다)."
178
+ : "The keychain vault is not queried from the standalone terminal (unsigned keytar calls hang on the OS keychain)."));
179
+ }
180
+ if (!any) ctx.out(ko ? "저장된 자격증명 파일이 없습니다. agentlas creds save 로 추가하세요." : "No credential files yet. Add one with: agentlas creds save");
181
+ return 0;
182
+ }
183
+
137
184
  async function run(ctx, args) {
138
185
  const fail = (msg) => { ctx.err(msg); return 1; };
139
186
  const sub = args[0];
140
187
  if (sub === "file") return cmdCredsFile(ctx, args.slice(1));
188
+ if (sub === "list" || sub === "ls") return cmdCredsList(ctx);
141
189
  if (sub !== "save") {
142
- return fail('usage: agentlas creds save --provider <name> --key <ENV_NAME> --value <value> [--project <path>] OR agentlas creds file --source <path> [--env <ENV_NAME>]');
190
+ return fail('usage: agentlas creds save --provider <name> --key <ENV_NAME> --value <value> [--project <path>] OR agentlas creds file --source <path> [--env <ENV_NAME>] OR agentlas creds list');
143
191
  }
144
192
  const f = parseCredFlags(args.slice(1));
145
193
  const key = typeof f.key === "string" ? f.key.trim() : "";
146
- const value = f.value === undefined || f.value === true ? "" : String(f.value);
147
- if (!key || !value) return fail("creds save requires --key and --value");
194
+ let value = f.value === undefined || f.value === true ? "" : String(f.value);
195
+ /*
196
+ * 비밀은 argv에 실리면 안 된다(clig.dev: never read secrets from flags) —
197
+ * 셸 히스토리와 `ps` 출력에 평문으로 남는다. `--value -` 는 stdin에서 읽는다:
198
+ * printf '%s' "$SECRET" | agentlas creds save --key X --value -
199
+ * argv로 직접 준 경우는 기존 사용을 깨지 않되, 노출 사실을 그 자리에서 알린다.
200
+ */
201
+ if (value === "-") {
202
+ value = fs.readFileSync(0, "utf8").replace(/\r?\n$/, "");
203
+ if (!value) return fail("creds save --value - expected the secret on stdin, but stdin was empty");
204
+ } else if (value) {
205
+ ctx.err(ctx.lang === "ko"
206
+ ? "참고: --value 로 준 비밀은 셸 히스토리·ps 에 남습니다. 다음부터는 stdin 을 쓰세요: printf '%s' \"$SECRET\" | agentlas creds save --key X --value -"
207
+ : "Note: a secret passed via --value lands in shell history and `ps`. Prefer stdin next time: printf '%s' \"$SECRET\" | agentlas creds save --key X --value -");
208
+ }
209
+ if (!key || !value) return fail("creds save requires --key and --value (use `--value -` to read the secret from stdin)");
148
210
  const provider = typeof f.provider === "string" && f.provider ? f.provider : key;
149
211
  const project = typeof f.project === "string" && f.project ? f.project : activeProjectPath(ctx.db());
150
212
  const targets = [];