agentlas 1.0.29 → 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 (50) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/bin/agentlas.cjs +15 -0
  3. package/engine/agentlas-cloud-runtime.cjs +54 -4
  4. package/engine/agentlas-input.cjs +10 -1
  5. package/engine/agentlas-judgment.cjs +0 -0
  6. package/engine/agentlas-native-host.cjs +69 -4
  7. package/engine/agentlas-ui.cjs +2 -0
  8. package/engine/agentlas-workforce.cjs +28 -5
  9. package/engine/agentlas.cjs +64 -2
  10. package/engine/agents/builder.cjs +84 -0
  11. package/engine/agents/router.cjs +8 -2
  12. package/engine/automation/launchd.cjs +131 -0
  13. package/engine/bootstrap-schema.sql +6 -44
  14. package/engine/browser/cdp.cjs +188 -0
  15. package/engine/browser/vault.cjs +124 -0
  16. package/engine/cli-output.cjs +262 -0
  17. package/engine/cloud-assets/package.cjs +5 -1
  18. package/engine/commands/automation.cjs +37 -1
  19. package/engine/commands/browser.cjs +166 -8
  20. package/engine/commands/build.cjs +101 -20
  21. package/engine/commands/connect.cjs +162 -9
  22. package/engine/commands/creds.cjs +16 -2
  23. package/engine/commands/doctor.cjs +20 -1
  24. package/engine/commands/document.cjs +79 -0
  25. package/engine/commands/graph.cjs +50 -10
  26. package/engine/commands/help.cjs +8 -6
  27. package/engine/commands/index.cjs +1 -0
  28. package/engine/commands/list.cjs +10 -1
  29. package/engine/commands/project.cjs +79 -16
  30. package/engine/commands/roles.cjs +10 -2
  31. package/engine/commands/telegram.cjs +23 -16
  32. package/engine/core/desktop-core-fetch.cjs +98 -0
  33. package/engine/core/desktop-core.cjs +170 -0
  34. package/engine/graph/ask-model.cjs +29 -1
  35. package/engine/graph/interview.cjs +105 -20
  36. package/engine/graph/layout.cjs +36 -34
  37. package/engine/graph/vocabulary.generated.cjs +1 -1
  38. package/engine/hephaestus/runtime.cjs +10 -2
  39. package/engine/project/controller.cjs +8 -8
  40. package/engine/project/team.cjs +99 -0
  41. package/engine/runtime-refusal.cjs +71 -0
  42. package/engine/runtimes/detect.cjs +3 -0
  43. package/engine/runtimes/resolve.cjs +1 -1
  44. package/engine/sessions/session.cjs +15 -2
  45. package/engine/telegram/connect.cjs +202 -0
  46. package/engine/ui/palette.cjs +1 -0
  47. package/engine/ui/repl.cjs +112 -1
  48. package/engine/vendor/desktop-core.manifest.json +7 -0
  49. package/engine/workforce/capture.cjs +51 -1
  50. package/package.json +3 -2
@@ -0,0 +1,262 @@
1
+ "use strict";
2
+ /*
3
+ * CLI 출력 계약 — 명령은 **문자열이 아니라 {데이터 + 스키마}** 를 돌려준다.
4
+ *
5
+ * 왜(2026-08-08, Paseo CLI 대조 실측):
6
+ * 우리 명령들은 각자 `ctx.out("...")` 로 직접 찍었다. 그래서
7
+ * · `--json` 이 어떤 명령엔 있고 어떤 명령엔 없다,
8
+ * · 에러 형식이 명령마다 다르다,
9
+ * · 스크립트가 쓰려면 사람용 문장을 파싱해야 한다.
10
+ *
11
+ * Paseo CLI(`packages/cli/src/output/`)는 명령이 `{type,data,schema}` 를 반환하고
12
+ * 렌더러 하나가 `--json/--yaml/--quiet/--no-color/--no-headers` 를 해석한다.
13
+ * 그 계약을 그대로 가져온다. 규칙 셋:
14
+ *
15
+ * 1. **quiet 이 모든 것을 이긴다.** `--quiet` 는 id 만 한 줄씩 — `xargs` 로 바로 흐른다.
16
+ * 2. **에러도 같은 형식 규율을 따른다.** `--json` 이면 `{"error":{"code","message"}}`.
17
+ * 사람용이면 빨간 `Error: `.
18
+ * 3. **색은 옵션이 정한다.** 명령이 직접 ANSI 를 박지 않는다(파이프 오염 금지).
19
+ *
20
+ * 이 파일은 의존성이 없다(테스트가 순수 함수로 검증한다).
21
+ */
22
+
23
+ /** @typedef {"table"|"json"|"yaml"} OutputFormat */
24
+
25
+ const DEFAULT_OPTIONS = Object.freeze({
26
+ format: "table",
27
+ quiet: false,
28
+ noHeaders: false,
29
+ noColor: false,
30
+ });
31
+
32
+ /** 명령이 돌려주는 단일 항목 결과. */
33
+ function single(data, schema) {
34
+ return { type: "single", data, schema };
35
+ }
36
+
37
+ /** 명령이 돌려주는 목록 결과. */
38
+ function list(data, schema) {
39
+ return { type: "list", data: Array.isArray(data) ? data : [], schema };
40
+ }
41
+
42
+ function isResult(value) {
43
+ return Boolean(
44
+ value &&
45
+ typeof value === "object" &&
46
+ (value.type === "single" || value.type === "list") &&
47
+ value.schema &&
48
+ Array.isArray(value.schema.columns),
49
+ );
50
+ }
51
+
52
+ function rowsOf(result) {
53
+ return result.type === "list" ? result.data : [result.data];
54
+ }
55
+
56
+ function readField(item, field) {
57
+ if (typeof field === "function") return field(item);
58
+ if (item && typeof item === "object") return item[field];
59
+ return undefined;
60
+ }
61
+
62
+ function textOf(value) {
63
+ if (value === null || value === undefined) return "";
64
+ if (typeof value === "string") return value;
65
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
66
+ if (value instanceof Date) return value.toISOString();
67
+ return JSON.stringify(value);
68
+ }
69
+
70
+ /**
71
+ * 표시 폭 — ANSI 를 벗기고, CJK 전각을 2칸으로 센다.
72
+ * 한글 표가 어긋나던 이유가 이것이다(폭을 문자 수로 세면 열이 밀린다).
73
+ */
74
+ function displayWidth(value) {
75
+ const plain = String(value).replace(/\[[0-9;]*m/g, "");
76
+ let width = 0;
77
+ for (const char of plain) {
78
+ const code = char.codePointAt(0) ?? 0;
79
+ const wide =
80
+ (code >= 0x1100 && code <= 0x115f) ||
81
+ (code >= 0x2e80 && code <= 0xa4cf) ||
82
+ (code >= 0xac00 && code <= 0xd7a3) ||
83
+ (code >= 0xf900 && code <= 0xfaff) ||
84
+ (code >= 0xfe30 && code <= 0xfe6f) ||
85
+ (code >= 0xff00 && code <= 0xff60) ||
86
+ (code >= 0xffe0 && code <= 0xffe6);
87
+ width += wide ? 2 : 1;
88
+ }
89
+ return width;
90
+ }
91
+
92
+ function padTo(value, width, align) {
93
+ const pad = Math.max(0, width - displayWidth(value));
94
+ if (align === "right") return " ".repeat(pad) + value;
95
+ if (align === "center") {
96
+ const left = Math.floor(pad / 2);
97
+ return " ".repeat(left) + value + " ".repeat(pad - left);
98
+ }
99
+ return value + " ".repeat(pad);
100
+ }
101
+
102
+ function renderQuiet(result) {
103
+ const idField = result.schema.idField;
104
+ return rowsOf(result)
105
+ .map((item) => textOf(readField(item, idField)))
106
+ .filter((line) => line.length > 0)
107
+ .join("\n");
108
+ }
109
+
110
+ function renderJson(result) {
111
+ const serialize = result.schema.serialize;
112
+ const project = (item) => (serialize ? serialize(item) : item);
113
+ const payload = result.type === "list" ? result.data.map(project) : project(result.data);
114
+ return JSON.stringify(payload, null, 2);
115
+ }
116
+
117
+ /** 의존성 없이 쓰는 최소 YAML(스칼라·객체·배열 1단계). */
118
+ function renderYaml(result) {
119
+ const serialize = result.schema.serialize;
120
+ const project = (item) => (serialize ? serialize(item) : item);
121
+ const scalar = (value) => {
122
+ if (value === null || value === undefined) return "null";
123
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
124
+ const text = textOf(value);
125
+ return /^[\w./:@-]+$/.test(text) ? text : JSON.stringify(text);
126
+ };
127
+ const objectLines = (object, indent) => {
128
+ const pad = " ".repeat(indent);
129
+ return Object.entries(object).map(([key, value]) => {
130
+ if (value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)) {
131
+ return `${pad}${key}:\n${objectLines(value, indent + 2).join("\n")}`;
132
+ }
133
+ if (Array.isArray(value)) {
134
+ if (value.length === 0) return `${pad}${key}: []`;
135
+ return `${pad}${key}:\n${value.map((entry) => `${pad} - ${scalar(entry)}`).join("\n")}`;
136
+ }
137
+ return `${pad}${key}: ${scalar(value)}`;
138
+ });
139
+ };
140
+ if (result.type === "list") {
141
+ if (result.data.length === 0) return "[]";
142
+ return result.data
143
+ .map((item) => {
144
+ const lines = objectLines(project(item), 2);
145
+ return `- ${lines.join("\n").trimStart()}`;
146
+ })
147
+ .join("\n");
148
+ }
149
+ return objectLines(project(result.data), 0).join("\n");
150
+ }
151
+
152
+ function renderTable(result, options) {
153
+ const columns = result.schema.columns;
154
+ const rows = rowsOf(result);
155
+ if (rows.length === 0) return "";
156
+ const cells = rows.map((item) =>
157
+ columns.map((column) => textOf(readField(item, column.field))),
158
+ );
159
+ const widths = columns.map((column, index) =>
160
+ Math.max(
161
+ options.noHeaders ? 0 : displayWidth(column.header),
162
+ ...cells.map((row) => displayWidth(row[index] ?? "")),
163
+ ),
164
+ );
165
+ const lines = [];
166
+ if (!options.noHeaders) {
167
+ lines.push(
168
+ columns
169
+ .map((column, index) => padTo(column.header.toUpperCase(), widths[index], column.align))
170
+ .join(" ")
171
+ .trimEnd(),
172
+ );
173
+ }
174
+ for (const row of cells) {
175
+ lines.push(
176
+ columns
177
+ .map((column, index) => padTo(row[index] ?? "", widths[index], column.align))
178
+ .join(" ")
179
+ .trimEnd(),
180
+ );
181
+ }
182
+ return lines.join("\n");
183
+ }
184
+
185
+ /** 형식 선택은 여기 한 곳. quiet 이 언제나 이긴다. */
186
+ function render(result, options = {}) {
187
+ if (!isResult(result)) {
188
+ throw new TypeError("render() expects a {type,data,schema} command result");
189
+ }
190
+ const opts = { ...DEFAULT_OPTIONS, ...options };
191
+ if (opts.quiet) return renderQuiet(result);
192
+ if (opts.format === "json") return renderJson(result);
193
+ if (opts.format === "yaml") return renderYaml(result);
194
+ if (typeof result.schema.renderHuman === "function") return result.schema.renderHuman(result, opts);
195
+ return renderTable(result, opts);
196
+ }
197
+
198
+ /** 무엇이든 구조화 에러로. code 는 기계가 분기할 값이다. */
199
+ function toCommandError(error) {
200
+ if (error && typeof error === "object" && typeof error.code === "string" && typeof error.message === "string") {
201
+ return { code: error.code, message: error.message, ...(error.details ? { details: error.details } : {}) };
202
+ }
203
+ if (error instanceof Error) {
204
+ return { code: "unknown_error", message: error.message, ...(error.stack ? { details: error.stack } : {}) };
205
+ }
206
+ return { code: "unknown_error", message: String(error) };
207
+ }
208
+
209
+ /** 에러도 같은 형식 규율을 따른다 — 사람용만 색을 쓴다. */
210
+ function renderError(error, options = {}) {
211
+ const opts = { ...DEFAULT_OPTIONS, ...options };
212
+ const commandError = toCommandError(error);
213
+ if (opts.format === "json") return JSON.stringify({ error: commandError }, null, 2);
214
+ if (opts.format === "yaml") {
215
+ return ["error:", ` code: ${commandError.code}`, ` message: ${JSON.stringify(commandError.message)}`].join("\n");
216
+ }
217
+ const prefix = opts.noColor ? "Error: " : "Error: ";
218
+ return commandError.details && typeof commandError.details === "string" && opts.format === "table"
219
+ ? `${prefix}${commandError.message}`
220
+ : `${prefix}${commandError.message}`;
221
+ }
222
+
223
+ /**
224
+ * argv 에서 전역 출력 플래그를 뜯어낸다. **모든 명령이 같은 이름·같은 의미**를 갖도록
225
+ * 파서가 한 곳이다(명령마다 --json 유무가 갈리던 것을 막는다).
226
+ *
227
+ * 색 규칙은 clig.dev 를 따른다: `NO_COLOR` 환경변수, `--no-color`, 그리고 TTY 가
228
+ * 아니면 자동으로 끈다(파이프에 ANSI 를 흘리지 않는다).
229
+ */
230
+ function parseOutputFlags(argv, env = process.env, isTty = Boolean(process.stdout.isTTY)) {
231
+ const rest = [];
232
+ const options = { ...DEFAULT_OPTIONS };
233
+ for (const token of argv) {
234
+ if (token === "--json") options.format = "json";
235
+ else if (token === "--yaml") options.format = "yaml";
236
+ else if (token === "--quiet" || token === "-q") options.quiet = true;
237
+ else if (token === "--no-headers") options.noHeaders = true;
238
+ else if (token === "--no-color") options.noColor = true;
239
+ else rest.push(token);
240
+ }
241
+ if (env && (env.NO_COLOR || env.AGENTLAS_NO_COLOR)) options.noColor = true;
242
+ if (!isTty) options.noColor = true;
243
+ return { options, rest };
244
+ }
245
+
246
+ /** 사람용 표현이 스피너·배너를 써도 되는가(비대화형이면 평문으로 떨어진다). */
247
+ function isRichUi(options, isTty = Boolean(process.stdout.isTTY)) {
248
+ return Boolean(isTty) && options.format === "table" && !options.quiet;
249
+ }
250
+
251
+ module.exports = {
252
+ DEFAULT_OPTIONS,
253
+ single,
254
+ list,
255
+ isResult,
256
+ render,
257
+ renderError,
258
+ toCommandError,
259
+ parseOutputFlags,
260
+ isRichUi,
261
+ displayWidth,
262
+ };
@@ -591,10 +591,14 @@ function cloudRoutingCardProblem(card) {
591
591
  if (!workforce || typeof workforce !== "object" || Array.isArray(workforce)) {
592
592
  return "workforce must be a complete semantic resume";
593
593
  }
594
+ // `skills` has a floor of 0, not 1. Skills are modules and live outside the
595
+ // core, so a fully modular agent legitimately declares none. Requiring one
596
+ // here would block publishing every modular package. Kept identical in
597
+ // agentlas_desktop/electron/cloud-agents/package.ts — the two must not drift.
594
598
  const semanticLists = [
595
599
  ["communities", /^community:[a-z0-9][a-z0-9-]*$/, 1, 5],
596
600
  ["roles", /^role:[a-z0-9][a-z0-9-]*$/, 0, 4],
597
- ["skills", /^skill:[a-z0-9][a-z0-9-]*$/, 1, 12],
601
+ ["skills", /^skill:[a-z0-9][a-z0-9-]*$/, 0, 12],
598
602
  ["knowledge", /^knowledge:[a-z0-9][a-z0-9-]*$/, 0, 256],
599
603
  ];
600
604
  for (const [field, pattern, minimum, maximum] of semanticLists) {
@@ -2,6 +2,9 @@
2
2
  /*
3
3
  * automation — 자동화 등록/목록/토글/삭제/실행 (v1 cmdAutomation 의 v2 포팅).
4
4
  * list (기본) | add | on <id> | off <id> | remove <id> | run <id> | runs | daemon
5
+ * tick 1회 due 스윕 후 종료(launchd/cron 이 poke 하는 진입점)
6
+ * install macOS launchd 상주 켜기 — 앱/창이 꺼져 있어도 발동(opt-in)
7
+ * uninstall 상주 끄기 · status 상주 상태
5
8
  *
6
9
  * 스케줄 계산은 automation/schedule, DB는 automation/store, 실행은
7
10
  * automation/daemon(세션 계층)만 쓴다. run <id> 는 스케줄을 건드리지 않는다
@@ -196,7 +199,40 @@ async function run(ctx, args) {
196
199
  return daemon.automationDaemon(ctx, db, { intervalSec: interval, runtimeOverride });
197
200
  }
198
201
 
199
- ctx.err(usage("list|add|on <id>|off <id>|remove <id>|run <id>|runs|daemon"));
202
+ // 1회 due 스윕 후 종료 — launchd/cron 이 poke 하는 진입점(상주 루프 아님).
203
+ if (sub === "tick") {
204
+ await daemon.daemonTick(ctx, db, {});
205
+ return 0;
206
+ }
207
+
208
+ // 앱/창이 꺼져 있어도 자동화가 발동하도록 macOS launchd 로 상주시킨다(opt-in).
209
+ if (sub === "install" || sub === "uninstall" || sub === "status") {
210
+ const launchd = require("../automation/launchd.cjs");
211
+ if (sub === "status") {
212
+ const st = launchd.launchdStatus();
213
+ if (!st.supported) { ctx.out(ko ? "launchd 상주는 macOS 전용입니다. 다른 OS 는 `agentlas automation daemon` 을 켜 두세요." : "launchd persistence is macOS-only. On other systems keep `agentlas automation daemon` running."); return 0; }
214
+ ctx.out(`${st.loaded ? ctx.ui.green("✓") : ctx.ui.dim("○")} ${ko ? "상주(launchd)" : "persistence (launchd)"}: ${st.loaded ? (ko ? "실행 중" : "loaded") : st.installed ? (ko ? "설치됨(미로드)" : "installed (not loaded)") : (ko ? "미설치" : "not installed")}`);
215
+ ctx.out(ctx.ui.dim(`plist: ${st.plistPath}`));
216
+ if (!st.loaded) ctx.out(ctx.ui.dim(ko ? "켜기: agentlas automation install" : "Enable: agentlas automation install"));
217
+ return 0;
218
+ }
219
+ if (sub === "install") {
220
+ let interval = 300;
221
+ for (let i = 1; i < args.length; i++) if (args[i] === "--interval") interval = Math.max(30, Number(args[++i]) || 300);
222
+ const st = launchd.enableLaunchd({ intervalSec: interval });
223
+ if (st.error) { ctx.err(`${ctx.ui.red("✖")} ${st.error}`); return 1; }
224
+ ctx.out(`${ctx.ui.green("✓")} ${ko ? "상주를 켰습니다 — 앱/창이 꺼져 있어도 자동화가 발동합니다" : "persistence on — automations fire even with the app/window closed"} (${interval}s)`);
225
+ ctx.out(ctx.ui.dim(ko ? `${Math.max(30, interval)}초마다 due 를 확인합니다. 끄기: agentlas automation uninstall` : `checks due automations every ${Math.max(30, interval)}s. Disable: agentlas automation uninstall`));
226
+ return 0;
227
+ }
228
+ // uninstall
229
+ const st = launchd.disableLaunchd();
230
+ if (st.error) { ctx.err(`${ctx.ui.red("✖")} ${st.error}`); return 1; }
231
+ ctx.out(`${ctx.ui.green("✓")} ${ko ? "상주를 껐습니다. 자동화는 포그라운드 daemon 을 켜 둘 때만 발동합니다." : "persistence off. Automations fire only while a foreground daemon runs."}`);
232
+ return 0;
233
+ }
234
+
235
+ ctx.err(usage("list|add|on <id>|off <id>|remove <id>|run <id>|runs|daemon|tick|install|uninstall|status"));
200
236
  return 1;
201
237
  }
202
238
 
@@ -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 };