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.
- package/CHANGELOG.md +69 -0
- package/bin/agentlas.cjs +15 -0
- package/engine/agentlas-cloud-runtime.cjs +54 -4
- package/engine/agentlas-input.cjs +10 -1
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-native-host.cjs +69 -4
- package/engine/agentlas-ui.cjs +2 -0
- package/engine/agentlas-workforce.cjs +28 -5
- package/engine/agentlas.cjs +64 -2
- package/engine/agents/builder.cjs +84 -0
- package/engine/agents/router.cjs +8 -2
- package/engine/automation/launchd.cjs +131 -0
- package/engine/bootstrap-schema.sql +6 -44
- package/engine/browser/cdp.cjs +188 -0
- package/engine/browser/vault.cjs +124 -0
- package/engine/cli-output.cjs +262 -0
- package/engine/cloud-assets/package.cjs +5 -1
- package/engine/commands/automation.cjs +37 -1
- package/engine/commands/browser.cjs +166 -8
- package/engine/commands/build.cjs +101 -20
- package/engine/commands/connect.cjs +162 -9
- package/engine/commands/creds.cjs +16 -2
- package/engine/commands/doctor.cjs +20 -1
- package/engine/commands/document.cjs +79 -0
- package/engine/commands/graph.cjs +50 -10
- package/engine/commands/help.cjs +8 -6
- package/engine/commands/index.cjs +1 -0
- package/engine/commands/list.cjs +10 -1
- package/engine/commands/project.cjs +79 -16
- package/engine/commands/roles.cjs +10 -2
- package/engine/commands/telegram.cjs +23 -16
- package/engine/core/desktop-core-fetch.cjs +98 -0
- package/engine/core/desktop-core.cjs +170 -0
- package/engine/graph/ask-model.cjs +29 -1
- package/engine/graph/interview.cjs +105 -20
- package/engine/graph/layout.cjs +36 -34
- package/engine/graph/vocabulary.generated.cjs +1 -1
- package/engine/hephaestus/runtime.cjs +10 -2
- package/engine/project/controller.cjs +8 -8
- package/engine/project/team.cjs +99 -0
- package/engine/runtime-refusal.cjs +71 -0
- package/engine/runtimes/detect.cjs +3 -0
- package/engine/runtimes/resolve.cjs +1 -1
- package/engine/sessions/session.cjs +15 -2
- package/engine/telegram/connect.cjs +202 -0
- package/engine/ui/palette.cjs +1 -0
- package/engine/ui/repl.cjs +112 -1
- package/engine/vendor/desktop-core.manifest.json +7 -0
- package/engine/workforce/capture.cjs +51 -1
- package/package.json +3 -2
|
@@ -1,20 +1,173 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
|
-
* connect — Telegram
|
|
3
|
+
* connect — Telegram 연결·조종 (2026-08-06 독립화).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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 {
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
169
|
+
ctx.err(usage(ko));
|
|
170
|
+
return 1;
|
|
18
171
|
}
|
|
19
172
|
|
|
20
173
|
module.exports = { run };
|
|
@@ -191,8 +191,22 @@ async function run(ctx, args) {
|
|
|
191
191
|
}
|
|
192
192
|
const f = parseCredFlags(args.slice(1));
|
|
193
193
|
const key = typeof f.key === "string" ? f.key.trim() : "";
|
|
194
|
-
|
|
195
|
-
|
|
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)");
|
|
196
210
|
const provider = typeof f.provider === "string" && f.provider ? f.provider : key;
|
|
197
211
|
const project = typeof f.project === "string" && f.project ? f.project : activeProjectPath(ctx.db());
|
|
198
212
|
const targets = [];
|
|
@@ -24,8 +24,27 @@ function roleDetail(selection, role, en) {
|
|
|
24
24
|
].filter(Boolean).join(" · ");
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function run(ctx) {
|
|
27
|
+
function run(ctx, args = []) {
|
|
28
28
|
const en = ctx.lang === "en";
|
|
29
|
+
// clig.dev: 스크립트 소비자를 위한 기계 계약. 사람용 줄과 같은 사실만 담는다.
|
|
30
|
+
if (args.includes("--json")) {
|
|
31
|
+
const db = ctx.db();
|
|
32
|
+
const clis = listAvailableCliRuntimes().map((c) => ({ kind: c.kind, path: c.path, authEvidence: runtimeAuthEvidence(c.kind).status }));
|
|
33
|
+
const active = activeRuntimeRow(db);
|
|
34
|
+
return (() => {
|
|
35
|
+
ctx.out(JSON.stringify({
|
|
36
|
+
database: { path: dbPath(), exists: fs.existsSync(dbPath()) },
|
|
37
|
+
runtimes: clis,
|
|
38
|
+
activeRuntime: active ? { ...active, authEvidence: runtimeAuthEvidence(active.kind).status } : null,
|
|
39
|
+
modelRoles: {
|
|
40
|
+
orchestrator: resolvedModelRole(db, "orchestrator"),
|
|
41
|
+
worker: resolvedModelRole(db, "worker"),
|
|
42
|
+
},
|
|
43
|
+
cloudSession: Boolean(process.env.AGENTLAS_SESSION) || fs.existsSync(path.join(userDataDir(), "auth", "cli-session.v1.json")),
|
|
44
|
+
}, null, 2));
|
|
45
|
+
return 0;
|
|
46
|
+
})();
|
|
47
|
+
}
|
|
29
48
|
let failures = 0;
|
|
30
49
|
let warnings = 0;
|
|
31
50
|
const ok = (label, detail) => ctx.out(` ${ctx.ui.green("✓")} ${label}${detail ? ctx.ui.dim(" — " + detail) : ""}`);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* document — 문서 산출(2026-08-06). 지금은 PDF 내보내기.
|
|
4
|
+
*
|
|
5
|
+
* 오너 원칙("터미널인데 모든 기능이 다 돼야"): 데스크탑 document/export-pdf.ts 는 offscreen
|
|
6
|
+
* BrowserWindow.printToPDF 로 HTML/URL 을 PDF 로 굽는다. 터미널은 같은 메커니즘을 CDP
|
|
7
|
+
* Page.printToPDF(engine/browser/cdp.cjs)로 한다 — GUI 창 없이 Agentlas 전용 Chrome 에서.
|
|
8
|
+
*
|
|
9
|
+
* document pdf <html-file|url> [-o out.pdf] [--landscape]
|
|
10
|
+
*
|
|
11
|
+
* 로컬 HTML 파일은 file:// 로, http(s) 는 그대로 연다. 브라우저(CDP)가 없으면 정직하게
|
|
12
|
+
* 멈추고 먼저 띄우도록 안내한다(조용히 안 되는 척하지 않는다).
|
|
13
|
+
*/
|
|
14
|
+
const path = require("node:path");
|
|
15
|
+
const fs = require("node:fs");
|
|
16
|
+
const cdp = require("../browser/cdp.cjs");
|
|
17
|
+
|
|
18
|
+
function usage(ko) {
|
|
19
|
+
return ko
|
|
20
|
+
? "사용법: agentlas document pdf <html파일|url> [-o 출력.pdf] [--landscape]"
|
|
21
|
+
: "Usage: agentlas document pdf <html-file|url> [-o out.pdf] [--landscape]";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toUrl(src) {
|
|
25
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(src)) return src; // http(s):// · file:// · data:
|
|
26
|
+
return "file://" + path.resolve(src); // 로컬 파일
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function toPdf(ctx, args) {
|
|
30
|
+
const ko = ctx.lang === "ko";
|
|
31
|
+
const flags = { landscape: args.includes("--landscape") };
|
|
32
|
+
const oi = args.indexOf("-o");
|
|
33
|
+
const out = oi >= 0 && args[oi + 1] ? args[oi + 1] : null;
|
|
34
|
+
const src = args.find((a, i) => a && !String(a).startsWith("-") && (oi < 0 || i !== oi + 1));
|
|
35
|
+
if (!src) { ctx.err(usage(ko)); return 1; }
|
|
36
|
+
|
|
37
|
+
// 로컬 파일이면 존재 확인(없는 걸 조용히 빈 PDF 로 굽지 않는다).
|
|
38
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(src) && !fs.existsSync(path.resolve(src))) {
|
|
39
|
+
ctx.err((ko ? "파일이 없습니다: " : "no such file: ") + src);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
const url = toUrl(src);
|
|
43
|
+
const outPath = path.resolve(out || (src.replace(/^https?:\/\//, "").replace(/[^\w.-]+/g, "_").replace(/\.(html?|md)$/i, "") || "document") + ".pdf");
|
|
44
|
+
|
|
45
|
+
if (!(await cdp.cdpReady())) {
|
|
46
|
+
ctx.err(ko
|
|
47
|
+
? `Agentlas 브라우저가 실행 중이 아닙니다. 먼저 띄우세요: agentlas browser ${url}`
|
|
48
|
+
: `The Agentlas browser is not running. Start it first: agentlas browser ${url}`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
let page;
|
|
52
|
+
try {
|
|
53
|
+
if (typeof ctx.ui.startSpinner === "function") ctx.ui.startSpinner(ko ? "PDF 굽는 중…" : "Rendering PDF…");
|
|
54
|
+
page = await cdp.attachPage();
|
|
55
|
+
await page.navigate(url, { waitMs: 1800 });
|
|
56
|
+
await page.waitFor("document.readyState === 'complete'", { timeoutMs: 8000 });
|
|
57
|
+
const r = await page.printPdf(outPath, { landscape: flags.landscape });
|
|
58
|
+
if (typeof ctx.ui.stopSpinner === "function") ctx.ui.stopSpinner();
|
|
59
|
+
ctx.out(`${ctx.ui.green("✓")} ${ko ? "PDF 를 만들었습니다" : "wrote PDF"}: ${r.path} ${ctx.ui.dim(`(${Math.round(r.bytes / 1024)} KB)`)}`);
|
|
60
|
+
return 0;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
if (typeof ctx.ui.stopSpinner === "function") ctx.ui.stopSpinner();
|
|
63
|
+
ctx.err(`${ctx.ui.red("✖")} ${String((e && e.message) || e)}`);
|
|
64
|
+
return 1;
|
|
65
|
+
} finally {
|
|
66
|
+
if (page) page.close();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function run(ctx, args) {
|
|
71
|
+
const ko = ctx.lang === "ko";
|
|
72
|
+
const sub = String(args[0] || "").toLowerCase();
|
|
73
|
+
if (!sub || sub === "help" || sub === "-h" || sub === "--help") { ctx.out(usage(ko)); return 0; }
|
|
74
|
+
if (sub === "pdf") return toPdf(ctx, args.slice(1));
|
|
75
|
+
ctx.err(usage(ko));
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { run };
|
|
@@ -588,7 +588,12 @@ async function newGraph(ctx, request, flags) {
|
|
|
588
588
|
|
|
589
589
|
const interview = require("../graph/interview.cjs");
|
|
590
590
|
const { askModel } = require("../graph/ask-model.cjs");
|
|
591
|
-
|
|
591
|
+
// 이미 저장된 자동화들을 인터뷰에 싣는다 — runGraph 단계가 이 중에서만 고르게 하려는 것.
|
|
592
|
+
// 목록이 없으면 모델이 id를 지어내고, 그 그래프는 실행 때 죽는다(데스크탑과 같은 규칙).
|
|
593
|
+
const knownGraphs = (() => {
|
|
594
|
+
try { return graphRows(ctx, db).map((r) => ({ id: r.id, name: r.name })); } catch { return []; }
|
|
595
|
+
})();
|
|
596
|
+
let state = interview.startInterview(request, { knownGraphs });
|
|
592
597
|
|
|
593
598
|
// 파이프로 들어온 답은 **미리 전부 읽어 둔다.** readline은 입력 스트림이 끝나면 닫히므로,
|
|
594
599
|
// 질문마다 물으면 두 번째 질문에서 "readline was closed"로 죽는다(실측).
|
|
@@ -624,9 +629,28 @@ async function newGraph(ctx, request, flags) {
|
|
|
624
629
|
}
|
|
625
630
|
const parsed = interview.parseInterviewTurn(answer.text, state);
|
|
626
631
|
if (!parsed.ok) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
632
|
+
/*
|
|
633
|
+
* ★모델이 형식을 틀린 것과 사람이 답을 안 준 것은 다르다.
|
|
634
|
+
*
|
|
635
|
+
* 출력이 JSON으로 안 읽히면 지금까지는 그 자리에서 인터뷰가 죽고 **사람이 준
|
|
636
|
+
* 답까지 전부 사라졌다.** 화면에는 "자동으로 돌릴 일을 한 문장으로 다시 적어
|
|
637
|
+
* 주세요"가 떴다 — 사람 문장이 틀린 것처럼(실측 2026-08-06, 연속 3회 재현).
|
|
638
|
+
* 형식 문제는 이미 스스로 고치게 하는 장치가 있다. 같은 예산 안에서 그쪽으로 보낸다.
|
|
639
|
+
*/
|
|
640
|
+
const correctable = parsed.code === "INTERVIEW_OUTPUT_UNREADABLE"
|
|
641
|
+
&& (state.attempts || []).length < interview.MAX_SELF_CORRECTIONS;
|
|
642
|
+
if (!correctable) {
|
|
643
|
+
ctx.err(parsed.reason);
|
|
644
|
+
ctx.err(ctx.ui.dim(parsed.nextAction));
|
|
645
|
+
return 1;
|
|
646
|
+
}
|
|
647
|
+
state.attempts = [...(state.attempts || []), {
|
|
648
|
+
round,
|
|
649
|
+
problems: [en
|
|
650
|
+
? "The last answer did not read as a single JSON object. Return one JSON object and nothing else."
|
|
651
|
+
: "지난 답이 JSON 하나로 읽히지 않았습니다. 설명 없이 JSON 객체 하나만 내보내세요."],
|
|
652
|
+
}];
|
|
653
|
+
continue;
|
|
630
654
|
}
|
|
631
655
|
// ★모델이 형식을 틀렸다 — 사람이 답을 안 준 게 아니다. 무엇이 틀렸는지 돌려주고
|
|
632
656
|
// 정해진 횟수만큼 스스로 고치게 한다. "구체적으로 적어 주세요"로 떠넘기면
|
|
@@ -650,7 +674,7 @@ async function newGraph(ctx, request, flags) {
|
|
|
650
674
|
continue;
|
|
651
675
|
}
|
|
652
676
|
if (parsed.turn.kind === "blueprint") {
|
|
653
|
-
built = interview.buildGraphFromBlueprint(parsed.turn.blueprint, en ? "en" : "ko");
|
|
677
|
+
built = interview.buildGraphFromBlueprint(parsed.turn.blueprint, en ? "en" : "ko", { knownGraphs });
|
|
654
678
|
if (!built.ok) {
|
|
655
679
|
// 청사진 검증은 통과했는데 짓는 데서 걸렸다 — 이것도 형식 문제라 같은 규율.
|
|
656
680
|
state.attempts = [...(state.attempts || []), { round, problems: built.problems.map((p) => p.reason) }];
|
|
@@ -753,11 +777,27 @@ async function newGraph(ctx, request, flags) {
|
|
|
753
777
|
const target = pickDefaultAgent(ctx, db);
|
|
754
778
|
const id = `graph-${crypto.randomUUID()}`;
|
|
755
779
|
try {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
780
|
+
/*
|
|
781
|
+
* ★목적 문장(goal)을 함께 싣는다 — 빼면 "이 그래프가 무엇인지"를 아는 유일한 문장이
|
|
782
|
+
* 저장 순간 사라진다. 실측(2026-08-06): 터미널로 만든 그래프의 goal이 null이라
|
|
783
|
+
* Hub 발행 tagline이 한국어 하드코딩 폴백("N단계 자동화 그래프")으로 떨어졌고,
|
|
784
|
+
* 그 문장이 영문 칸에 실려 발행이 거절됐다. goal 칸은 데스크탑 스키마 백스톱이
|
|
785
|
+
* 보장하지만, 옛 DB 방어로 columnExists 가드를 둔다.
|
|
786
|
+
*/
|
|
787
|
+
const hasGoal = ctx.columnExists(db, "automations", "goal");
|
|
788
|
+
if (hasGoal) {
|
|
789
|
+
db.prepare(
|
|
790
|
+
`INSERT INTO automations
|
|
791
|
+
(id, name, schedule, target_type, target_id, prompt_template, enabled, created_by, created_at, next_run_at, graph_json, goal)
|
|
792
|
+
VALUES (?, ?, ?, 'agent', ?, ?, 0, 'user', ?, NULL, ?, ?)`,
|
|
793
|
+
).run(id, name, built.scheduleHuman, target, name, new Date().toISOString(), JSON.stringify(built.graph), (bp.goal || "").trim() || null);
|
|
794
|
+
} else {
|
|
795
|
+
db.prepare(
|
|
796
|
+
`INSERT INTO automations
|
|
797
|
+
(id, name, schedule, target_type, target_id, prompt_template, enabled, created_by, created_at, next_run_at, graph_json)
|
|
798
|
+
VALUES (?, ?, ?, 'agent', ?, ?, 0, 'user', ?, NULL, ?)`,
|
|
799
|
+
).run(id, name, built.scheduleHuman, target, name, new Date().toISOString(), JSON.stringify(built.graph));
|
|
800
|
+
}
|
|
761
801
|
} catch (err) {
|
|
762
802
|
ctx.err(en ? `Could not save: ${err.message}` : `저장하지 못했습니다: ${err.message}`);
|
|
763
803
|
return 1;
|
package/engine/commands/help.cjs
CHANGED
|
@@ -15,7 +15,7 @@ AGENTS & HUB
|
|
|
15
15
|
search "<what you need>" discover agents in the Hub
|
|
16
16
|
install <slug> install an agent from the Hub
|
|
17
17
|
plugin add <slug> · plugin list Hub plugins (MCP servers)
|
|
18
|
-
build "<request>" build
|
|
18
|
+
build "<request>" build an installable agent locally (auto-installs)
|
|
19
19
|
upload <path> save owner-private in Agent Cloud (--visibility marketplace to publish)
|
|
20
20
|
import <path> · cd · native prepare local folder agents
|
|
21
21
|
list installed agents/companies + orchestrator/worker runtimes
|
|
@@ -29,13 +29,14 @@ EXECUTE
|
|
|
29
29
|
workforce | network <request> Agent Workforce Ontology route (public Hub menu)
|
|
30
30
|
hep-network "<request>" staff across Local + owner Cloud + public Hub (local Core federation)
|
|
31
31
|
hep-local | hep-cloud | hep-hub "<request>" same, restricted to one source scope
|
|
32
|
-
call "a,b" "<ctx>" · browser · route "<req>" [--json] · research <sub>
|
|
32
|
+
call "a,b" "<ctx>" · browser <url|status|sites|login> · route "<req>" [--json] · research <sub>
|
|
33
33
|
|
|
34
34
|
KNOWLEDGE
|
|
35
35
|
memory import · evolve memory & prompt-evolution proposals
|
|
36
36
|
ontology · career-graph project knowledge & source routing
|
|
37
37
|
journal <sub> Stormbreaker run journal
|
|
38
|
-
project
|
|
38
|
+
project <sub> connect this folder + set an ordered team, standalone
|
|
39
|
+
(status · init · use <agent> · team <agent>…)
|
|
39
40
|
context <sub> dependency map: refresh|locate|refs|slice|impact|verify
|
|
40
41
|
|
|
41
42
|
ACCOUNT & OPS
|
|
@@ -82,7 +83,7 @@ AGENTS & HUB
|
|
|
82
83
|
search "<필요한 것>" Hub에서 에이전트 찾기
|
|
83
84
|
install <slug> Hub 에이전트 설치
|
|
84
85
|
plugin add <slug> · plugin list Hub 플러그인 (MCP 서버)
|
|
85
|
-
build "<요청>"
|
|
86
|
+
build "<요청>" 에이전트를 로컬에서 만들고 바로 설치
|
|
86
87
|
upload <path> Agent Cloud에 소유자 비공개 저장 (--visibility marketplace 로 발행)
|
|
87
88
|
import <path> · cd · native prepare 로컬 폴더 에이전트
|
|
88
89
|
list 설치 에이전트/회사 + 오케스트레이터·워커 런타임
|
|
@@ -96,13 +97,14 @@ EXECUTE
|
|
|
96
97
|
workforce | network <request> Agent Workforce Ontology 편성 (공개 Hub 메뉴)
|
|
97
98
|
hep-network "<request>" 로컬+오너 클라우드+공개 Hub 연합 편성 (로컬 Core 연합)
|
|
98
99
|
hep-local | hep-cloud | hep-hub "<request>" 같은 편성, 한 소스 스코프로 제한
|
|
99
|
-
call "a,b" "<ctx>" · browser · route "<req>" [--json] · research <sub>
|
|
100
|
+
call "a,b" "<ctx>" · browser <url|status|sites|login> · route "<req>" [--json] · research <sub>
|
|
100
101
|
|
|
101
102
|
KNOWLEDGE
|
|
102
103
|
memory import · evolve 메모리·프롬프트 진화 제안
|
|
103
104
|
ontology · career-graph 프로젝트 지식·소스 라우팅
|
|
104
105
|
journal <sub> Stormbreaker 런 저널
|
|
105
|
-
project
|
|
106
|
+
project <sub> 이 폴더를 프로젝트로 연결 + 순서 팀 편성 (독립)
|
|
107
|
+
(status · init · use <에이전트> · team <에이전트>…)
|
|
106
108
|
context <sub> 의존성 지도: refresh|locate|refs|slice|impact|verify
|
|
107
109
|
|
|
108
110
|
ACCOUNT & OPS
|
|
@@ -35,6 +35,7 @@ const COMMANDS = {
|
|
|
35
35
|
automation: () => require("./automation.cjs"),
|
|
36
36
|
native: () => require("./native.cjs"),
|
|
37
37
|
multimodal: () => require("./multimodal.cjs"),
|
|
38
|
+
document: () => require("./document.cjs"),
|
|
38
39
|
workforce: () => require("./workforce.cjs"),
|
|
39
40
|
network: () => require("./workforce.cjs"),
|
|
40
41
|
taskforce: () => require("./workforce.cjs"),
|
package/engine/commands/list.cjs
CHANGED
|
@@ -24,7 +24,7 @@ function roleRuntimeLabel(selection, role, en) {
|
|
|
24
24
|
return bits.join(" ");
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function run(ctx) {
|
|
27
|
+
function run(ctx, args = []) {
|
|
28
28
|
const db = ctx.db();
|
|
29
29
|
// 프라이버시 정책(웹 전용/백그라운드 제외)은 registry가 소유한다 — 직접 SQL 금지.
|
|
30
30
|
const agents = listAgents(db).map((a) => ({
|
|
@@ -34,6 +34,15 @@ function run(ctx) {
|
|
|
34
34
|
? db.prepare("SELECT id, slug, name FROM firms ORDER BY name").all()
|
|
35
35
|
: [];
|
|
36
36
|
|
|
37
|
+
// clig.dev: 스크립트 소비자는 사람용 표를 파싱하게 두지 말 것 — --json 은
|
|
38
|
+
// 사람용 출력과 같은 사실을 기계 계약으로 준다.
|
|
39
|
+
if (args.includes("--json")) {
|
|
40
|
+
const orchestrator = resolvedModelRole(db, "orchestrator");
|
|
41
|
+
const worker = resolvedModelRole(db, "worker");
|
|
42
|
+
ctx.out(JSON.stringify({ agents, firms, modelRoles: { orchestrator, worker } }, null, 2));
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
const en = ctx.lang === "en";
|
|
38
47
|
ctx.out(ctx.ui.bold(en ? "Installed agents" : "설치된 에이전트"));
|
|
39
48
|
if (!agents.length) {
|
|
@@ -1,31 +1,64 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
|
-
* project —
|
|
3
|
+
* project — 프로젝트 상태·초기화, 그리고 순서 팀 편성 (독립).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* `project
|
|
5
|
+
* 2026-08-06(오너 원칙): 데스크탑/플러그인은 산출물·설정을 공유할 뿐 선행 전제가
|
|
6
|
+
* 아니다. 그래서 `project team`·`project use`로 터미널에서 직접 폴더를 프로젝트로
|
|
7
|
+
* 연결하고 컨트롤러 팀을 정한다 — 이후 `run "<task>"`와 REPL 평문 입력이 데스크탑
|
|
8
|
+
* 없이 동작한다. 프로젝트 쓰기는 engine/project/team.cjs가 소유(스키마 공유).
|
|
7
9
|
*/
|
|
8
10
|
const { projectCwd } = require("../project/paths.cjs");
|
|
9
11
|
const {
|
|
10
12
|
ensureTerminalProjectForExecutionCli,
|
|
11
13
|
initializeTerminalProjectCli,
|
|
12
14
|
} = require("../project/state.cjs");
|
|
15
|
+
const { connectProjectTeam } = require("../project/team.cjs");
|
|
16
|
+
const { resolveProjectController } = require("../project/controller.cjs");
|
|
17
|
+
|
|
18
|
+
function showStatus(ctx, cwd, ko) {
|
|
19
|
+
const active = ensureTerminalProjectForExecutionCli(ctx.db(), cwd, "read", "terminal-project-status");
|
|
20
|
+
ctx.out(`${ko ? "프로젝트 폴더" : "project"}: ${cwd}`);
|
|
21
|
+
ctx.out(`${ko ? ".agentlas 상태" : "Agentlas state"}: ${active
|
|
22
|
+
? (ko ? "초기화됨" : "initialized")
|
|
23
|
+
: (ko ? "초기화되지 않음" : "not initialized")}`);
|
|
24
|
+
// 연결된 프로젝트 + 순서 팀(컨트롤러)을 보여준다 — 데스크탑 없이도 무엇이 실행될지.
|
|
25
|
+
try {
|
|
26
|
+
const resolved = resolveProjectController(ctx.db(), cwd);
|
|
27
|
+
const pool = resolved.project?.agentPool || [];
|
|
28
|
+
ctx.out(`${ko ? "연결된 프로젝트" : "connected project"}: ${resolved.project?.name || "—"}`);
|
|
29
|
+
if (pool.length) {
|
|
30
|
+
ctx.out(ko ? "순서 팀 (0번=컨트롤러):" : "ordered team (index 0 = controller):");
|
|
31
|
+
pool.forEach((m, i) => ctx.out(` ${i}. ${m.nameSnapshot}`));
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
if (e && e.code === "project_not_connected") {
|
|
35
|
+
ctx.out(ctx.ui.dim(ko
|
|
36
|
+
? "이 폴더는 아직 프로젝트에 연결되지 않았습니다. `agentlas project use <에이전트>`로 연결하세요."
|
|
37
|
+
: "This folder is not connected to a project yet. Connect it with `agentlas project use <agent>`."));
|
|
38
|
+
} else if (e && e.honestStop) {
|
|
39
|
+
ctx.out(ctx.ui.dim(String(e.message)));
|
|
40
|
+
}
|
|
41
|
+
// 그 외 오류는 status를 막지 않는다.
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function reportTeam(ctx, result, ko) {
|
|
47
|
+
ctx.out(`${ctx.ui.green("✓")} ${ko ? "프로젝트" : "project"} ${result.created ? (ko ? "생성" : "created") : (ko ? "갱신" : "updated")}: ${result.name}`);
|
|
48
|
+
ctx.out(ko ? "순서 팀 (0번=컨트롤러):" : "ordered team (index 0 = controller):");
|
|
49
|
+
result.team.forEach((m, i) => ctx.out(` ${i}. ${m.nameSnapshot}`));
|
|
50
|
+
ctx.out(ctx.ui.dim(ko
|
|
51
|
+
? "이제 이 폴더에서 바로 실행: agentlas run \"<할 일>\" 또는 REPL에서 그냥 문장을 치세요."
|
|
52
|
+
: "Now run here directly: agentlas run \"<task>\" or just type a sentence in the REPL."));
|
|
53
|
+
}
|
|
13
54
|
|
|
14
55
|
function run(ctx, args) {
|
|
15
56
|
const action = String(args[0] || "status").toLowerCase();
|
|
16
57
|
const cwd = projectCwd();
|
|
17
58
|
const ko = ctx.lang === "ko";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
ctx.out(`${ko ? "Agentlas 상태" : "Agentlas state"}: ${active
|
|
22
|
-
? (ko ? "초기화됨" : "initialized")
|
|
23
|
-
: (ko ? "초기화되지 않음" : "not initialized")}`);
|
|
24
|
-
if (!active) ctx.out(ko
|
|
25
|
-
? "비공개 .agentlas 상태와 로컬 제외 파일을 만들려면 `agentlas project init`을 실행하세요."
|
|
26
|
-
: "Run `agentlas project init` to create private .agentlas state and update local ignore files.");
|
|
27
|
-
return 0;
|
|
28
|
-
}
|
|
59
|
+
|
|
60
|
+
if (action === "status") return showStatus(ctx, cwd, ko);
|
|
61
|
+
|
|
29
62
|
if (action === "init") {
|
|
30
63
|
ctx.out(ko
|
|
31
64
|
? "비공개 Agentlas 프로젝트 상태를 초기화합니다. .agentlas/ 생성, .gitignore 갱신, 로컬 자격증명·서명 템플릿 추가가 포함될 수 있습니다."
|
|
@@ -33,14 +66,44 @@ function run(ctx, args) {
|
|
|
33
66
|
try {
|
|
34
67
|
initializeTerminalProjectCli(ctx.db(), cwd);
|
|
35
68
|
} catch (e) {
|
|
36
|
-
// fail-closed: 프라이버시 경계(.gitignore/심볼릭 링크)를 보증 못 하면 생성 금지.
|
|
37
69
|
ctx.err(String((e && e.message) || e));
|
|
38
70
|
return 1;
|
|
39
71
|
}
|
|
40
72
|
ctx.out(`${ko ? "초기화됨" : "Initialized"}: ${cwd}`);
|
|
41
73
|
return 0;
|
|
42
74
|
}
|
|
43
|
-
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* project use <에이전트> — 이 폴더를 프로젝트로 연결 + 단일 컨트롤러 팀
|
|
78
|
+
* project team <에이전트> [<에이전트>...] — 순서 팀 편성 (0번=컨트롤러)
|
|
79
|
+
* 둘 다 데스크탑 없이 동작한다.
|
|
80
|
+
*/
|
|
81
|
+
if (action === "use" || action === "team") {
|
|
82
|
+
const tokens = args.slice(1).filter((t) => t && !String(t).startsWith("-"));
|
|
83
|
+
if (!tokens.length) {
|
|
84
|
+
ctx.err(ko
|
|
85
|
+
? `사용법: agentlas project ${action} <에이전트> ${action === "team" ? "[<에이전트>...]" : ""} · 에이전트 목록: agentlas list`
|
|
86
|
+
: `Usage: agentlas project ${action} <agent> ${action === "team" ? "[<agent>...]" : ""} · list agents: agentlas list`);
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const result = connectProjectTeam(ctx.db(), cwd, tokens, {});
|
|
91
|
+
reportTeam(ctx, result, ko);
|
|
92
|
+
return 0;
|
|
93
|
+
} catch (e) {
|
|
94
|
+
if (e && e.code === "team_agent_not_found") {
|
|
95
|
+
ctx.err((ko ? "에이전트를 찾을 수 없습니다: " : "agent not found: ") + e.token);
|
|
96
|
+
ctx.err(ctx.ui.dim(ko ? "설치된 에이전트: agentlas list" : "installed agents: agentlas list"));
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
ctx.err(String((e && e.message) || e));
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
ctx.err(ko
|
|
105
|
+
? "사용법: agentlas project [status | init | use <에이전트> | team <에이전트>...]"
|
|
106
|
+
: "Usage: agentlas project [status | init | use <agent> | team <agent>...]");
|
|
44
107
|
return 1;
|
|
45
108
|
}
|
|
46
109
|
|
|
@@ -35,9 +35,17 @@ function fmt(selection, en) {
|
|
|
35
35
|
return bits.filter(Boolean).join(" ");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function show(ctx) {
|
|
38
|
+
function show(ctx, args = []) {
|
|
39
39
|
const en = ctx.lang === "en";
|
|
40
40
|
const db = ctx.db();
|
|
41
|
+
if (args.includes("--json")) {
|
|
42
|
+
ctx.out(JSON.stringify({
|
|
43
|
+
orchestrator: resolvedModelRole(db, "orchestrator"),
|
|
44
|
+
worker: resolvedModelRole(db, "worker"),
|
|
45
|
+
pools: { orchestrator: roleMembers(db, "orchestrator"), worker: roleMembers(db, "worker") },
|
|
46
|
+
}, null, 2));
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
41
49
|
ctx.out(ctx.ui.bold(en ? "Model roles" : "모델 역할"));
|
|
42
50
|
for (const role of ["orchestrator", "worker"]) {
|
|
43
51
|
const resolved = resolvedModelRole(db, role);
|
|
@@ -170,7 +178,7 @@ function set(ctx, args) {
|
|
|
170
178
|
function run(ctx, args = []) {
|
|
171
179
|
const en = ctx.lang === "en";
|
|
172
180
|
const [sub, ...rest] = args;
|
|
173
|
-
if (!sub || sub === "show" || sub === "list") return show(ctx);
|
|
181
|
+
if (!sub || sub === "show" || sub === "list" || sub === "--json") return show(ctx, sub === "--json" ? ["--json"] : rest);
|
|
174
182
|
if (sub === "set") return set(ctx, rest);
|
|
175
183
|
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
176
184
|
// SELF_HELP_COMMANDS 계약: --help 는 스텁이 아니라 실제 안내여야 한다.
|