agentlas 0.9.10 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +106 -0
- package/README.md +319 -341
- package/bin/agentlas.cjs +32 -9
- package/engine/agentlas-banner.cjs +24 -3
- package/engine/agentlas-composer.cjs +239 -31
- package/engine/agentlas-config.cjs +103 -7
- package/engine/agentlas-core-harness.cjs +48 -4
- package/engine/agentlas-evolution.cjs +3 -4
- package/engine/agentlas-i18n.cjs +88 -32
- package/engine/agentlas-input.cjs +234 -18
- package/engine/agentlas-memory-governance.cjs +3 -5
- package/engine/agentlas-memory-import.cjs +3 -3
- package/engine/agentlas-native-host.cjs +200 -9
- package/engine/agentlas-onboard.cjs +112 -20
- package/engine/agentlas-permissions.cjs +14 -7
- package/engine/agentlas-sqlite-policy.cjs +34 -0
- package/engine/agentlas-ui.cjs +93 -42
- package/engine/agentlas-workforce.cjs +472 -58
- package/engine/agentlas-workload-routing.cjs +8 -3
- package/engine/agentlas.cjs +140 -12367
- package/engine/agents/files.cjs +61 -0
- package/engine/agents/import-local.cjs +237 -0
- package/engine/agents/registry.cjs +158 -0
- package/engine/agents/router.cjs +565 -0
- package/engine/agents/routes.cjs +43 -0
- package/engine/architecture.data.json +2 -2
- package/engine/automation/daemon.cjs +335 -0
- package/engine/automation/schedule.cjs +181 -0
- package/engine/automation/store.cjs +209 -0
- package/engine/cloud/auth.cjs +279 -0
- package/engine/cloud/hub-client.cjs +239 -0
- package/engine/cloud-assets/cargo.cjs +49 -0
- package/engine/cloud-assets/cas.cjs +235 -0
- package/engine/cloud-assets/commands.cjs +273 -0
- package/engine/cloud-assets/package.cjs +936 -0
- package/engine/cloud-assets/restore.cjs +172 -0
- package/engine/cloud-assets/state.cjs +268 -0
- package/engine/commands/automation.cjs +195 -0
- package/engine/commands/billing.cjs +96 -0
- package/engine/commands/browser.cjs +20 -0
- package/engine/commands/build.cjs +33 -0
- package/engine/commands/call.cjs +24 -0
- package/engine/commands/career-graph.cjs +51 -0
- package/engine/commands/cd.cjs +22 -0
- package/engine/commands/chat.cjs +12 -0
- package/engine/commands/chats.cjs +28 -0
- package/engine/commands/cloud.cjs +17 -0
- package/engine/commands/connect.cjs +20 -0
- package/engine/commands/context.cjs +66 -0
- package/engine/commands/creds.cjs +203 -0
- package/engine/commands/doctor.cjs +68 -0
- package/engine/commands/env.cjs +33 -0
- package/engine/commands/evolve.cjs +23 -0
- package/engine/commands/experience.cjs +34 -0
- package/engine/commands/film.cjs +8 -0
- package/engine/commands/firm.cjs +115 -0
- package/engine/commands/help.cjs +65 -0
- package/engine/commands/hep.cjs +23 -0
- package/engine/commands/import.cjs +35 -0
- package/engine/commands/index.cjs +136 -0
- package/engine/commands/install.cjs +27 -0
- package/engine/commands/journal.cjs +31 -0
- package/engine/commands/legacy-network.cjs +29 -0
- package/engine/commands/list.cjs +49 -0
- package/engine/commands/login.cjs +68 -0
- package/engine/commands/logout.cjs +28 -0
- package/engine/commands/mcp.cjs +91 -0
- package/engine/commands/memory.cjs +21 -0
- package/engine/commands/multimodal.cjs +91 -0
- package/engine/commands/native.cjs +34 -0
- package/engine/commands/netadmin.cjs +31 -0
- package/engine/commands/oberon.cjs +70 -0
- package/engine/commands/ontology.cjs +24 -0
- package/engine/commands/open.cjs +48 -0
- package/engine/commands/plugin.cjs +101 -0
- package/engine/commands/project.cjs +47 -0
- package/engine/commands/research.cjs +36 -0
- package/engine/commands/route.cjs +37 -0
- package/engine/commands/run.cjs +149 -0
- package/engine/commands/search.cjs +55 -0
- package/engine/commands/setup.cjs +45 -0
- package/engine/commands/storm.cjs +75 -0
- package/engine/commands/swarm.cjs +75 -0
- package/engine/commands/telegram.cjs +32 -0
- package/engine/commands/uninstall.cjs +68 -0
- package/engine/commands/update.cjs +54 -0
- package/engine/commands/upload.cjs +18 -0
- package/engine/commands/usage.cjs +35 -0
- package/engine/commands/variant.cjs +25 -0
- package/engine/commands/version.cjs +9 -0
- package/engine/commands/whoami.cjs +38 -0
- package/engine/commands/workforce.cjs +103 -0
- package/engine/core/db.cjs +160 -0
- package/engine/core/paths.cjs +35 -0
- package/engine/experience/build.cjs +181 -0
- package/engine/experience/intents.cjs +492 -0
- package/engine/experience/runtime.cjs +242 -0
- package/engine/experience/variant.cjs +196 -0
- package/engine/firms/orchestrate.cjs +333 -0
- package/engine/hephaestus/runtime.cjs +697 -0
- package/engine/hub/install.cjs +872 -0
- package/engine/hub/plugins.cjs +213 -0
- package/engine/mcp/consent.cjs +289 -0
- package/engine/mcp/contract.cjs +202 -0
- package/engine/mcp/index.cjs +43 -0
- package/engine/mcp/inventory.cjs +322 -0
- package/engine/mcp/plan.cjs +286 -0
- package/engine/mcp/probe.cjs +151 -0
- package/engine/memory-cli/curate.cjs +163 -0
- package/engine/oberon/common.cjs +69 -0
- package/engine/oberon/manifest.cjs +164 -0
- package/engine/oberon/outputs.cjs +70 -0
- package/engine/oberon/render.cjs +164 -0
- package/engine/project/career-graph.cjs +249 -0
- package/engine/project/credentials.cjs +262 -0
- package/engine/project/env-file.cjs +46 -0
- package/engine/project/index.cjs +27 -0
- package/engine/project/memory-context.cjs +453 -0
- package/engine/project/ontology.cjs +467 -0
- package/engine/project/paths.cjs +39 -0
- package/engine/project/seed.cjs +200 -0
- package/engine/project/state.cjs +403 -0
- package/engine/project/super-ontology-seed.json +3288 -0
- package/engine/runtimes/detect.cjs +54 -0
- package/engine/runtimes/overrides.cjs +139 -0
- package/engine/runtimes/resolve.cjs +64 -0
- package/engine/sessions/apply-fences.cjs +188 -0
- package/engine/sessions/fences.cjs +362 -0
- package/engine/sessions/orchestrator.cjs +170 -0
- package/engine/sessions/prompt.cjs +212 -0
- package/engine/sessions/session.cjs +245 -0
- package/engine/sessions/sink.cjs +54 -0
- package/engine/sessions/store.cjs +79 -0
- package/engine/storm/deps.cjs +88 -0
- package/engine/storm/storm.cjs +218 -0
- package/engine/storm/swarm.cjs +422 -0
- package/engine/ui/palette.cjs +105 -0
- package/engine/ui/renderer.cjs +85 -0
- package/engine/ui/repl.cjs +444 -0
- package/engine/workforce/capture.cjs +701 -0
- package/engine/workforce/deps.cjs +472 -0
- package/package.json +2 -6
- package/engine/agentlas-experience-mcp.cjs +0 -1709
- package/engine/agentlas-parity.cjs +0 -1499
- package/engine/agentlas-repl.cjs +0 -1780
|
@@ -1,1499 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/*
|
|
3
|
-
* agentlas-parity: 데스크탑 앱 전용이던 기능의 터미널 패리티 구현.
|
|
4
|
-
*
|
|
5
|
-
* storm — Agentlas-owned Goal/UltraCode harness (Hephaestus route evidence + local parallel workers)
|
|
6
|
-
* swarm — emergent 에이전트 스웜 (블랙보드 + `## Spawn` 그래프 성장 + 종합)
|
|
7
|
-
* automation — 앱 스케줄러가 실행하는 자동화의 등록/목록/토글 (같은 SQLite)
|
|
8
|
-
* usage — 로컬 실행/자동화/세션 집계
|
|
9
|
-
* telegram — 텔레그램 바인딩 현황 (읽기 전용; 페어링은 앱 Connect)
|
|
10
|
-
* cloud search — 마켓플레이스 검색 (MCP marketplace.search_agents)
|
|
11
|
-
*
|
|
12
|
-
* agentlas.cjs 가 create(deps)로 주입한 헬퍼(captureRuntime/runApi/resolveRuntime 등)만
|
|
13
|
-
* 사용한다 — 이 파일은 DB 스키마와 프로세스 스폰 외에 자체 상태를 갖지 않는다.
|
|
14
|
-
*/
|
|
15
|
-
const os = require("node:os");
|
|
16
|
-
const path = require("node:path");
|
|
17
|
-
const fs = require("node:fs");
|
|
18
|
-
const crypto = require("node:crypto");
|
|
19
|
-
const { spawn } = require("node:child_process");
|
|
20
|
-
const { Ui } = require("./agentlas-ui.cjs");
|
|
21
|
-
const workloadRouting = require("./agentlas-workload-routing.cjs");
|
|
22
|
-
const {
|
|
23
|
-
loadCoreStormbreakerHarness,
|
|
24
|
-
resolveCoreRuntimeRoot,
|
|
25
|
-
spawnCoreModule,
|
|
26
|
-
} = require("./agentlas-core-harness.cjs");
|
|
27
|
-
|
|
28
|
-
// ── 스웜 상수 (앱 mcp/swarm-run.ts 와 동일한 안전 상한) ──
|
|
29
|
-
const SWARM_MAX_TASKS = 24;
|
|
30
|
-
const SWARM_SPAWN_PER_TURN = 12;
|
|
31
|
-
|
|
32
|
-
// Agentlas-OS/Hephaestus 내부 시스템 에이전트(마켓 제품 아님) — 검색/목록에서 숨긴다.
|
|
33
|
-
// 데스크탑 electron/agents/hired-agents.ts isInternalAgentSlug 와 규칙 동일.
|
|
34
|
-
function isInternalAgentSlug(slug) {
|
|
35
|
-
const s = String(slug || "").toLowerCase();
|
|
36
|
-
return /^researcher-\d+/.test(s) || s === "research-intelligence-desk" || s.startsWith("hephaestus-");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const LOGIN_CALLBACK_PATH = "/callback";
|
|
40
|
-
const LOGIN_TIMEOUT_MS = 180_000;
|
|
41
|
-
const MAX_LOGIN_SESSION_BYTES = 16 * 1024;
|
|
42
|
-
|
|
43
|
-
function createLoginState(randomBytes = crypto.randomBytes) {
|
|
44
|
-
const bytes = Buffer.from(randomBytes(32));
|
|
45
|
-
if (bytes.length !== 32) throw new Error("Could not create the login state.");
|
|
46
|
-
return bytes.toString("base64url");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function loginStatesMatch(actual, expected) {
|
|
50
|
-
const left = Buffer.from(String(actual || ""), "utf8");
|
|
51
|
-
const right = Buffer.from(String(expected || ""), "utf8");
|
|
52
|
-
return left.length === right.length && left.length > 0 && crypto.timingSafeEqual(left, right);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* OAuth loopback callback의 1회용 state guard. `/callback` GET이 도착하면 성공/실패와
|
|
57
|
-
* 무관하게 transaction을 소비한다. 따라서 잘못된 state 뒤에 공격자 세션을 재주입하거나,
|
|
58
|
-
* 성공 URL을 재생해 다른 세션으로 덮어쓸 수 없다.
|
|
59
|
-
*/
|
|
60
|
-
function createLoginCallbackGuard(expectedState) {
|
|
61
|
-
let consumed = false;
|
|
62
|
-
return {
|
|
63
|
-
consume(rawUrl, method = "GET") {
|
|
64
|
-
let url;
|
|
65
|
-
try {
|
|
66
|
-
url = new URL(String(rawUrl || "/"), "http://127.0.0.1");
|
|
67
|
-
} catch {
|
|
68
|
-
return { handled: true, final: false, ok: false, statusCode: 400, message: "Invalid login callback." };
|
|
69
|
-
}
|
|
70
|
-
if (url.pathname !== LOGIN_CALLBACK_PATH) {
|
|
71
|
-
return { handled: false, final: false, ok: false, statusCode: 404, message: "not found" };
|
|
72
|
-
}
|
|
73
|
-
if (method !== "GET") {
|
|
74
|
-
return { handled: true, final: false, ok: false, statusCode: 405, message: "method not allowed" };
|
|
75
|
-
}
|
|
76
|
-
if (consumed) {
|
|
77
|
-
return { handled: true, final: false, ok: false, statusCode: 410, message: "Login callback has already been used." };
|
|
78
|
-
}
|
|
79
|
-
consumed = true;
|
|
80
|
-
|
|
81
|
-
if (!loginStatesMatch(url.searchParams.get("state"), expectedState)) {
|
|
82
|
-
return {
|
|
83
|
-
handled: true,
|
|
84
|
-
final: true,
|
|
85
|
-
ok: false,
|
|
86
|
-
statusCode: 400,
|
|
87
|
-
message: "Login callback state validation failed. Run agentlas login again.",
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
const oauthError = url.searchParams.get("error");
|
|
91
|
-
if (oauthError) {
|
|
92
|
-
const safeCode = /^[A-Za-z0-9_.-]{1,80}$/.test(oauthError) ? oauthError : "oauth_error";
|
|
93
|
-
return {
|
|
94
|
-
handled: true,
|
|
95
|
-
final: true,
|
|
96
|
-
ok: false,
|
|
97
|
-
statusCode: 400,
|
|
98
|
-
message: `Agentlas login denied: ${safeCode}`,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
const value = url.searchParams.get("session") || url.searchParams.get("token") || "";
|
|
102
|
-
if (!value) {
|
|
103
|
-
return {
|
|
104
|
-
handled: true,
|
|
105
|
-
final: true,
|
|
106
|
-
ok: false,
|
|
107
|
-
statusCode: 400,
|
|
108
|
-
message: "The callback did not include a session value.",
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
if (Buffer.byteLength(value, "utf8") > MAX_LOGIN_SESSION_BYTES) {
|
|
112
|
-
return {
|
|
113
|
-
handled: true,
|
|
114
|
-
final: true,
|
|
115
|
-
ok: false,
|
|
116
|
-
statusCode: 400,
|
|
117
|
-
message: "The login session value is too large.",
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
return { handled: true, final: true, ok: true, statusCode: 200, value, message: "Agentlas login complete" };
|
|
121
|
-
},
|
|
122
|
-
isConsumed() { return consumed; },
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function create(deps) {
|
|
127
|
-
const D = deps;
|
|
128
|
-
|
|
129
|
-
function newUi(lang) {
|
|
130
|
-
return new Ui({ lang: lang || D.prefsLang() });
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ── Hephaestus 런타임 해석 (설치 런처 우선, 앱 번들 폴백) ──
|
|
134
|
-
function hephaestusBin() {
|
|
135
|
-
const candidates = [
|
|
136
|
-
process.env.HEPHAESTUS_BIN,
|
|
137
|
-
path.join(os.homedir(), ".agentlas", "runtime", "current", "bin", "hephaestus"),
|
|
138
|
-
];
|
|
139
|
-
for (const c of process.platform === "win32" ? [] : candidates) {
|
|
140
|
-
try {
|
|
141
|
-
if (c && fs.existsSync(c)) {
|
|
142
|
-
fs.accessSync(c, fs.constants.X_OK);
|
|
143
|
-
return { kind: "bin", exec: c };
|
|
144
|
-
}
|
|
145
|
-
} catch { /* 다음 후보 */ }
|
|
146
|
-
}
|
|
147
|
-
const root = resolveCoreRuntimeRoot();
|
|
148
|
-
if (root) return { kind: "python", root };
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function careerGraphRuntime() {
|
|
153
|
-
const binCandidates = [
|
|
154
|
-
process.env.HEPHAESTUS_CAREER_GRAPH_BIN,
|
|
155
|
-
process.env.HEPHAESTUS_BIN ? path.join(path.dirname(process.env.HEPHAESTUS_BIN), "career-graph") : null,
|
|
156
|
-
path.join(os.homedir(), ".agentlas", "runtime", "current", "bin", "career-graph"),
|
|
157
|
-
];
|
|
158
|
-
for (const c of binCandidates) {
|
|
159
|
-
try {
|
|
160
|
-
if (c && fs.existsSync(c)) {
|
|
161
|
-
fs.accessSync(c, fs.constants.X_OK);
|
|
162
|
-
return { kind: "bin", exec: c };
|
|
163
|
-
}
|
|
164
|
-
} catch { /* 다음 후보 */ }
|
|
165
|
-
}
|
|
166
|
-
const roots = [
|
|
167
|
-
process.env.HEPHAESTUS_RUNTIME_ROOT,
|
|
168
|
-
path.join(os.homedir(), ".agentlas", "runtime", "current"),
|
|
169
|
-
];
|
|
170
|
-
if (process.resourcesPath) roots.push(path.join(process.resourcesPath, "Hephaestus"));
|
|
171
|
-
if (process.platform === "darwin") roots.push("/Applications/Agentlas.app/Contents/Resources/Hephaestus");
|
|
172
|
-
roots.push(path.resolve(__dirname, "..", "..", "agentlas_desktop", "Hephaestus"));
|
|
173
|
-
for (const root of roots) {
|
|
174
|
-
try {
|
|
175
|
-
if (root && fs.existsSync(path.join(root, "career_graph", "__main__.py"))) return { kind: "python", root };
|
|
176
|
-
} catch { /* 다음 후보 */ }
|
|
177
|
-
}
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function spawnHephaestus(args, opts) {
|
|
182
|
-
const found = hephaestusBin();
|
|
183
|
-
if (!found) return null;
|
|
184
|
-
if (found.kind === "bin") return spawn(found.exec, args, opts);
|
|
185
|
-
return spawnCoreModule("agentlas_cloud", args, opts, found.root);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ── storm — Agentlas 자체 Goal/UltraCode 하네스 ──
|
|
189
|
-
// Core owns the exact Goal/UltraCode prompt. Terminal supplies only host
|
|
190
|
-
// runtime inventory, worker context, and execution; no local prompt fallback.
|
|
191
|
-
// ctx: { ui?, cwd?, research?, background?, runtimeOverride? }
|
|
192
|
-
async function stormRun(db, goal, ctx = {}) {
|
|
193
|
-
const ui = ctx.ui || newUi();
|
|
194
|
-
goal = String(goal || "").trim();
|
|
195
|
-
if (!goal) {
|
|
196
|
-
ui.warn("usage: storm <goal> [--research]");
|
|
197
|
-
return { ok: false };
|
|
198
|
-
}
|
|
199
|
-
if (goal.startsWith("-")) {
|
|
200
|
-
ui.error("goal cannot start with '-'.");
|
|
201
|
-
return { ok: false };
|
|
202
|
-
}
|
|
203
|
-
const cwd = ctx.cwd || (typeof D.projectCwd === "function" ? D.projectCwd() : D.runCwd());
|
|
204
|
-
let executionHarness;
|
|
205
|
-
try {
|
|
206
|
-
executionHarness = await loadCoreStormbreakerHarness(cwd);
|
|
207
|
-
} catch (error) {
|
|
208
|
-
ui.error(`Stormbreaker Core harness unavailable: ${String((error && error.message) || error).slice(0, 400)}`);
|
|
209
|
-
return { ok: false, error: "stormbreaker-core-harness-unavailable" };
|
|
210
|
-
}
|
|
211
|
-
const args = ["route", goal, "--project", cwd, "--runtime", "terminal"];
|
|
212
|
-
if (ctx.research) args.push("--research-evidence");
|
|
213
|
-
if (ctx.background) {
|
|
214
|
-
ui.warn(ui.lang === "ko"
|
|
215
|
-
? "Agentlas 자체 Stormbreaker 하네스는 현재 포그라운드에서 실행합니다. 세션이 끝나도 영수증으로 재개 지점을 보존합니다."
|
|
216
|
-
: "The Agentlas-owned Stormbreaker harness currently runs in the foreground and preserves resume receipts.");
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
let result = { code: 0, stdout: "", stderr: "" };
|
|
220
|
-
if (hephaestusBin()) {
|
|
221
|
-
ui.beginTurn();
|
|
222
|
-
ui.startSpinner(ui.lang === "ko" ? "Stormbreaker 라우팅 근거 수집 중…" : "Stormbreaker gathering route evidence…");
|
|
223
|
-
result = await new Promise((resolve) => {
|
|
224
|
-
const child = spawnHephaestus(args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
225
|
-
let stdout = "";
|
|
226
|
-
let stderrTail = [];
|
|
227
|
-
child.stdout.on("data", (c) => { stdout += c.toString(); });
|
|
228
|
-
child.stderr.on("data", (c) => {
|
|
229
|
-
for (const ln of c.toString().split("\n")) {
|
|
230
|
-
const line = ln.trim();
|
|
231
|
-
if (!line) continue;
|
|
232
|
-
stderrTail.push(line);
|
|
233
|
-
if (stderrTail.length > 30) stderrTail.shift();
|
|
234
|
-
ui.updateSpinner(line.slice(0, 100));
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
child.on("error", (err) => resolve({ code: 1, stdout, stderr: String(err.message) }));
|
|
238
|
-
child.on("close", (code) => resolve({ code: code ?? 0, stdout, stderr: stderrTail.join("\n") }));
|
|
239
|
-
});
|
|
240
|
-
ui.stopSpinner();
|
|
241
|
-
ui.endTurn();
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
let json = null;
|
|
245
|
-
try {
|
|
246
|
-
const s = result.stdout.indexOf("{");
|
|
247
|
-
const e = result.stdout.lastIndexOf("}");
|
|
248
|
-
if (s >= 0 && e > s) json = JSON.parse(result.stdout.slice(s, e + 1));
|
|
249
|
-
} catch { /* 비JSON 출력 */ }
|
|
250
|
-
|
|
251
|
-
let routeContext = "";
|
|
252
|
-
if (json) {
|
|
253
|
-
const action = json.action || json.route_action || (json.route_decision && json.route_decision.action) || json.status || "?";
|
|
254
|
-
ui.line("");
|
|
255
|
-
ui.ok((ui.lang === "ko" ? "storm 결과: " : "storm result: ") + action);
|
|
256
|
-
const fields = {
|
|
257
|
-
receipt_id: json.receipt_id || (json.route_decision && json.route_decision.receipt_id),
|
|
258
|
-
pipeline_id: json.pipeline_id,
|
|
259
|
-
journal: json.journal,
|
|
260
|
-
status: json.status,
|
|
261
|
-
can_report_success: json.final_gate && json.final_gate.can_report_success,
|
|
262
|
-
};
|
|
263
|
-
if (json.auto_run) {
|
|
264
|
-
fields.auto_run = String(json.auto_run.status || "") + (json.auto_run.reason ? ` — ${json.auto_run.reason}` : "");
|
|
265
|
-
}
|
|
266
|
-
const sel = json.selected;
|
|
267
|
-
if (sel) fields.selected = typeof sel === "string" ? sel : sel.id || sel.slug || sel.name;
|
|
268
|
-
for (const [k, v] of Object.entries(fields)) {
|
|
269
|
-
if (v !== undefined && v !== null && v !== "") ui.info(`${k}: ${v}`);
|
|
270
|
-
}
|
|
271
|
-
if (json.clarify_question) ui.warn(String(json.clarify_question));
|
|
272
|
-
if (json.reason) ui.info(String(json.reason));
|
|
273
|
-
// 파이프라인 패킷 요약
|
|
274
|
-
const packets = json.execution_fabric && json.execution_fabric.packets;
|
|
275
|
-
if (Array.isArray(packets)) {
|
|
276
|
-
routeContext = packets.slice(0, 24).map((p) => {
|
|
277
|
-
const title = String(p.title || p.id || "packet").replace(/\s+/g, " ").slice(0, 160);
|
|
278
|
-
const card = p.card ? ` [agent:${String(p.card).slice(0, 100)}]` : "";
|
|
279
|
-
return `- ${title}${card}`;
|
|
280
|
-
}).join("\n");
|
|
281
|
-
for (const p of packets.slice(0, 12)) {
|
|
282
|
-
ui.line(" " + ui.c.emerald("▸ ") + ui.c.text(String(p.title || p.id || "packet")) + (p.card ? ui.c.dim(" " + p.card) : ""));
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
// 파이프라인이 아니면 추천 에이전트라도 보여준다 (hub_candidates 등)
|
|
286
|
-
const exec = json.execution || {};
|
|
287
|
-
const recos = []
|
|
288
|
-
.concat(exec.recommended_agents || [], exec.alternatives || [])
|
|
289
|
-
.map((a) => (typeof a === "string" ? a : a && (a.id || a.slug || a.name)))
|
|
290
|
-
.filter(Boolean);
|
|
291
|
-
if (recos.length) {
|
|
292
|
-
ui.line("");
|
|
293
|
-
ui.info(ui.lang === "ko" ? "추천 에이전트:" : "recommended agents:");
|
|
294
|
-
for (const r of recos.slice(0, 8)) ui.line(" " + ui.c.emerald("▸ ") + ui.c.text(r));
|
|
295
|
-
ui.info(ui.lang === "ko" ? '빌려 실행: agentlas cloud install <slug> 또는 "/storm"을 더 구체적 목표로.' : "borrow: agentlas cloud install <slug>, or re-run /storm with a more specific goal.");
|
|
296
|
-
}
|
|
297
|
-
} else {
|
|
298
|
-
const raw = (result.stdout || result.stderr || "").trim();
|
|
299
|
-
if (raw) ui.markdown(raw.slice(0, 4000));
|
|
300
|
-
}
|
|
301
|
-
if (result.code !== 0 && !json) {
|
|
302
|
-
ui.warn(`Hephaestus route evidence unavailable (${result.code}); Agentlas parent planner will continue from the original goal.`);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const harnessResult = await swarmRun(db, goal, {
|
|
306
|
-
...ctx,
|
|
307
|
-
ui,
|
|
308
|
-
cwd,
|
|
309
|
-
runtimeOverride: ctx.runtimeOverride,
|
|
310
|
-
stormbreaker: true,
|
|
311
|
-
executionHarness,
|
|
312
|
-
routeContext,
|
|
313
|
-
});
|
|
314
|
-
return { ...harnessResult, routeDecision: json };
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async function cmdStorm(db, args, runtimeOverride, executionContext = {}) {
|
|
318
|
-
const rest = [];
|
|
319
|
-
const ctx = {
|
|
320
|
-
...executionContext,
|
|
321
|
-
cwd: executionContext.cwd || (typeof D.projectCwd === "function" ? D.projectCwd() : D.runCwd()),
|
|
322
|
-
runtimeOverride,
|
|
323
|
-
};
|
|
324
|
-
for (let i = 0; i < args.length; i++) {
|
|
325
|
-
if (args[i] === "--research" || args[i] === "--research-evidence") ctx.research = true;
|
|
326
|
-
else if (args[i] === "--background") ctx.background = true;
|
|
327
|
-
else rest.push(args[i]);
|
|
328
|
-
}
|
|
329
|
-
const r = await stormRun(db, rest.join(" "), ctx);
|
|
330
|
-
if (!r.ok) process.exitCode = 1;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// ── Hephaestus 네이티브 패스스루 — 엔진 전 기능을 터미널 1급으로 노출 ──
|
|
334
|
-
// stdio inherit 로 돌려 색/프롬프트/스트리밍이 네이티브 그대로 나온다.
|
|
335
|
-
function runHephaestusInteractive(args, opts = {}) {
|
|
336
|
-
const isCareerGraph = args[0] === "career-graph" || args[0] === "career_graph";
|
|
337
|
-
const found = isCareerGraph ? careerGraphRuntime() : hephaestusBin();
|
|
338
|
-
if (!found) {
|
|
339
|
-
process.stderr.write(
|
|
340
|
-
isCareerGraph
|
|
341
|
-
? "Career Graph 런타임이 없습니다 — 최신 Agentlas OS / Hephaestus 설치 후 다시 시도하세요.\n"
|
|
342
|
-
: "Hephaestus 런타임이 없습니다 — 데스크탑 앱 설치 또는 Hephaestus 인스톨러 실행 후 다시 시도하세요.\n",
|
|
343
|
-
);
|
|
344
|
-
process.stderr.write(
|
|
345
|
-
isCareerGraph
|
|
346
|
-
? "설치 또는 지정: HEPHAESTUS_CAREER_GRAPH_BIN=<경로> 또는 HEPHAESTUS_RUNTIME_ROOT=<경로>\n"
|
|
347
|
-
: "설치: https://agentlas.cloud · 또는 HEPHAESTUS_BIN=<경로> 지정\n",
|
|
348
|
-
);
|
|
349
|
-
return Promise.resolve(1);
|
|
350
|
-
}
|
|
351
|
-
const cwd = opts.cwd || D.runCwd();
|
|
352
|
-
const moduleName = found.kind === "python" && isCareerGraph ? "career_graph" : "agentlas_cloud";
|
|
353
|
-
const moduleArgs = moduleName === "career_graph" ? args.slice(1) : args;
|
|
354
|
-
const child =
|
|
355
|
-
found.kind === "bin"
|
|
356
|
-
? spawn(found.exec, isCareerGraph ? args.slice(1) : args, { cwd, stdio: "inherit" })
|
|
357
|
-
: spawnCoreModule(moduleName, moduleArgs, { cwd, stdio: "inherit" }, found.root);
|
|
358
|
-
if (!child) {
|
|
359
|
-
process.stderr.write("Hephaestus failed: Python 3.9+ was not found.\n");
|
|
360
|
-
return Promise.resolve(1);
|
|
361
|
-
}
|
|
362
|
-
return new Promise((resolve) => {
|
|
363
|
-
child.on("error", (e) => {
|
|
364
|
-
process.stderr.write(`Hephaestus failed: ${e.message}\n`);
|
|
365
|
-
resolve(1);
|
|
366
|
-
});
|
|
367
|
-
child.on("close", (code) => resolve(code == null ? 0 : code));
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const HEP_USAGE = [
|
|
372
|
-
"agentlas hep <hephaestus 서브커맨드…> — 엔진 전 기능 네이티브 패스스루",
|
|
373
|
-
"",
|
|
374
|
-
" 1급 별칭:",
|
|
375
|
-
" agentlas build \"<요청>\" 에이전트/팀 빌드·수리·패키징 (hep-build)",
|
|
376
|
-
" agentlas route \"<요청>\" 라우팅 미리보기 — 어떤 에이전트가 잡히는지",
|
|
377
|
-
" agentlas research <sub…> Research Engine (status|gather|search|read|plan…)",
|
|
378
|
-
" agentlas network <sub…> 로컬 에이전트 네트워크 (init|status|reindex|add-source…)",
|
|
379
|
-
" agentlas journal <sub…> Stormbreaker 런 저널 (status|verify|repair|gate)",
|
|
380
|
-
" agentlas call \"a,b\" \"<컨텍스트>\" 지정한 Hub/Cloud 에이전트 준비 (hep-call)",
|
|
381
|
-
"",
|
|
382
|
-
" 전체 서브커맨드: agentlas hep --help (wizard·security·package·publish·cards·ao·plugins·meta-agent…)",
|
|
383
|
-
].join("\n");
|
|
384
|
-
|
|
385
|
-
async function cmdHep(db, args) {
|
|
386
|
-
if (!args.length || args[0] === "help") {
|
|
387
|
-
D.out(HEP_USAGE);
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
const code = await runHephaestusInteractive(args);
|
|
391
|
-
if (code !== 0) process.exitCode = code;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// ── swarm — 앱 swarm-run.ts 프로토콜의 CLI 포트 ──
|
|
395
|
-
function swarmProtocol(goal, tasks, task, liveRuntimeInventory) {
|
|
396
|
-
const doneList = tasks
|
|
397
|
-
.filter((t) => t.status === "done")
|
|
398
|
-
.slice(-8)
|
|
399
|
-
.map((t) => `- ${t.title}`)
|
|
400
|
-
.join("\n");
|
|
401
|
-
const assignedList = tasks
|
|
402
|
-
.filter((t) => t.id !== task.id && t.status !== "failed")
|
|
403
|
-
.slice(0, 24)
|
|
404
|
-
.map((t) => `- [${t.status}] ${t.title}${t.brief ? ` — ${t.brief}` : ""}`)
|
|
405
|
-
.join("\n");
|
|
406
|
-
return [
|
|
407
|
-
"You are one worker in an EMERGENT AGENT SWARM collaborating on a shared goal.",
|
|
408
|
-
`SHARED GOAL: ${goal}`,
|
|
409
|
-
"",
|
|
410
|
-
"YOUR TASK RIGHT NOW:",
|
|
411
|
-
`- ${task.title}${task.role ? ` (role: ${task.role})` : ""}`,
|
|
412
|
-
task.brief ? `- Details: ${task.brief}` : "",
|
|
413
|
-
"",
|
|
414
|
-
doneList ? `Already completed by peers (recent):\n${doneList}` : "No peer results yet — you may be first.",
|
|
415
|
-
assignedList ? `WORK ALREADY ASSIGNED TO PEERS (never duplicate these packets):\n${assignedList}` : "",
|
|
416
|
-
"",
|
|
417
|
-
"RULES:",
|
|
418
|
-
"1. Do your task concretely with available tools/files in the current working folder.",
|
|
419
|
-
`LIVE_RUNTIME_INVENTORY=${JSON.stringify(liveRuntimeInventory || [])}`,
|
|
420
|
-
"2. If the goal needs MORE work beyond your task — split into concrete next steps — end your",
|
|
421
|
-
" message with a `## Spawn` block. Every child MUST be one JSON object with a higher-level AI allocation. Choose runtimeId and exactModelId only from LIVE_RUNTIME_INVENTORY:",
|
|
422
|
-
" ## Spawn",
|
|
423
|
-
' - {"role":"webmaster","brief":"build the landing page structure","allocation":{"schema":"agentlas.workload-allocation.v1","runtimeId":"runtime-1","exactModelId":"model-from-inventory","tier":"balanced","effort":"high","phase":"delegate","reasonCodes":["complex-reasoning"],"rationale":"requires coordinated implementation","requiredCapabilities":["code","tools"]}}',
|
|
424
|
-
' - {"brief":"run focused tests","allocation":{"schema":"agentlas.workload-allocation.v1","runtimeId":"runtime-2","exactModelId":"model-from-inventory","tier":"economy","effort":"low","phase":"delegate","reasonCodes":["bounded-scope"],"rationale":"bounded verification","requiredCapabilities":["code","tools"]}}',
|
|
425
|
-
" Choose each exact runtime/model and effort from the actual child difficulty; do not copy one allocation to every child.",
|
|
426
|
-
" (role is optional. Do NOT spawn if the goal is already met or another pending/running/done packet already owns that work.)",
|
|
427
|
-
"3. Do NOT restate the whole goal. Do NOT invent work that isn't needed — over-spawning wastes the user's money.",
|
|
428
|
-
"4. Everything above the `## Spawn` block is your result and is shared with peers on the blackboard.",
|
|
429
|
-
]
|
|
430
|
-
.filter(Boolean)
|
|
431
|
-
.join("\n");
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function parseSwarmOutput(text) {
|
|
435
|
-
const m = String(text).match(/^[ \t]*##[ \t]*Spawn[ \t]*$/im);
|
|
436
|
-
if (!m || m.index === undefined) return { result: String(text).trim(), spawn: [] };
|
|
437
|
-
const result = String(text).slice(0, m.index).trim();
|
|
438
|
-
const block = String(text).slice(m.index + m[0].length).split("\n");
|
|
439
|
-
const spawn = [];
|
|
440
|
-
for (const raw of block) {
|
|
441
|
-
const line = raw.trim();
|
|
442
|
-
if (!line.startsWith("-")) {
|
|
443
|
-
if (line.startsWith("#")) break;
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
const body = line.replace(/^-\s*/, "");
|
|
447
|
-
if (body.startsWith("{")) {
|
|
448
|
-
const item = workloadRouting.extractJsonObject(body);
|
|
449
|
-
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
450
|
-
const brief = String(item.brief || "").trim();
|
|
451
|
-
const allocation = workloadRouting.normalizeAllocation(item.allocation || item, "delegate");
|
|
452
|
-
if (brief) {
|
|
453
|
-
spawn.push({
|
|
454
|
-
title: String(item.title || brief).trim().slice(0, 80),
|
|
455
|
-
brief: brief.slice(0, 8_000),
|
|
456
|
-
role: item.role ? String(item.role).trim().slice(0, 80) : undefined,
|
|
457
|
-
allocation,
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
if (spawn.length >= SWARM_SPAWN_PER_TURN) break;
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
const parts = body.split("|");
|
|
465
|
-
let role;
|
|
466
|
-
let brief;
|
|
467
|
-
if (parts.length >= 2) {
|
|
468
|
-
role = parts[0].trim() || undefined;
|
|
469
|
-
brief = parts.slice(1).join("|").trim();
|
|
470
|
-
} else {
|
|
471
|
-
brief = body.trim();
|
|
472
|
-
}
|
|
473
|
-
// Legacy text remains parseable, but it has no AI-authored allocation and
|
|
474
|
-
// therefore runs on the current model with an observable fallback receipt.
|
|
475
|
-
if (brief) spawn.push({ title: brief.slice(0, 80), brief, role, allocation: null });
|
|
476
|
-
if (spawn.length >= SWARM_SPAWN_PER_TURN) break;
|
|
477
|
-
}
|
|
478
|
-
return { result, spawn };
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// ctx: { ui?, cwd?, permission?, runtime?, runtimeOverride?, concurrency?, agent?, projectPath? }
|
|
482
|
-
async function swarmRun(db, goal, ctx = {}) {
|
|
483
|
-
const ui = ctx.ui || newUi();
|
|
484
|
-
goal = String(goal || "").trim();
|
|
485
|
-
if (!goal) {
|
|
486
|
-
ui.warn("usage: swarm <goal> [--parallel N]");
|
|
487
|
-
return { ok: false };
|
|
488
|
-
}
|
|
489
|
-
const runtime = ctx.runtime || D.resolveRuntime(db, ctx.runtimeOverride);
|
|
490
|
-
const stormbreaker = ctx.stormbreaker === true;
|
|
491
|
-
const executionHarness = stormbreaker ? ctx.executionHarness : null;
|
|
492
|
-
if (stormbreaker && (!executionHarness || typeof executionHarness.system_prompt !== "string")) {
|
|
493
|
-
ui.error("Stormbreaker requires the canonical Goal + UltraCode harness from Agentlas Core.");
|
|
494
|
-
return { ok: false, error: "stormbreaker-core-harness-unavailable" };
|
|
495
|
-
}
|
|
496
|
-
const coreHarnessPrompt = executionHarness && executionHarness.system_prompt;
|
|
497
|
-
const permission = ctx.permission || "write";
|
|
498
|
-
const discoveredRuntimes = ctx.runtimes && ctx.runtimes.length
|
|
499
|
-
? ctx.runtimes
|
|
500
|
-
: typeof D.listAvailableRuntimes === "function"
|
|
501
|
-
? D.listAvailableRuntimes(db, runtime)
|
|
502
|
-
: [runtime];
|
|
503
|
-
const runtimes = discoveredRuntimes
|
|
504
|
-
.map((candidate, index) => ({ ...candidate, runtimeId: candidate.runtimeId || `runtime-${index + 1}` }));
|
|
505
|
-
const liveRuntimeInventory = workloadRouting.runtimeInventory(runtimes);
|
|
506
|
-
const cwd = ctx.cwd || (typeof D.projectCwd === "function" ? D.projectCwd() : D.runCwd());
|
|
507
|
-
const concurrency = Math.max(1, Math.min(8, Number(ctx.concurrency) || 3));
|
|
508
|
-
const env = await D.buildChildEnvCli(db, {
|
|
509
|
-
projectPath: ctx.projectPath || null,
|
|
510
|
-
agentId: ctx.agent && ctx.agent.id,
|
|
511
|
-
permission,
|
|
512
|
-
cwd,
|
|
513
|
-
lang: ui.lang,
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
async function runBaseWorker(system, prompt) {
|
|
517
|
-
if (runtime.mode === "cli") {
|
|
518
|
-
return await D.captureRuntime(runtime.kind, system, prompt, {
|
|
519
|
-
cwd,
|
|
520
|
-
env,
|
|
521
|
-
permission,
|
|
522
|
-
model: ctx.modelPin || runtime.model || null,
|
|
523
|
-
effort: ctx.effortPin === undefined ? null : ctx.effortPin,
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
const text = await D.runApi(runtime.backend, ctx.modelPin || runtime.model, system, prompt);
|
|
527
|
-
return typeof text === "string" ? text : (text && text.text) || "";
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function recordAllocation(task, stage, decision, resolution, parentTaskId = null) {
|
|
531
|
-
const receipt = workloadRouting.createDecisionReceipt({
|
|
532
|
-
taskId: `${stage}-${task.id || "synthesis"}`,
|
|
533
|
-
parentTaskId,
|
|
534
|
-
taskText: task.brief || task.title || goal,
|
|
535
|
-
stage,
|
|
536
|
-
decision,
|
|
537
|
-
resolution,
|
|
538
|
-
});
|
|
539
|
-
try {
|
|
540
|
-
workloadRouting.appendDecisionReceipt(
|
|
541
|
-
receipt,
|
|
542
|
-
ctx.receiptFile || (D.modelRoutingReceiptPath && D.modelRoutingReceiptPath()),
|
|
543
|
-
);
|
|
544
|
-
} catch (error) {
|
|
545
|
-
ui.warn(`model routing receipt failed: ${String((error && error.message) || error).slice(0, 120)}`);
|
|
546
|
-
}
|
|
547
|
-
return receipt;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
async function runAllocatedWorker(system, prompt, task, stage, parentTaskId = null) {
|
|
551
|
-
const resolution = workloadRouting.resolveAllocationAcrossRuntimes({
|
|
552
|
-
runtimes,
|
|
553
|
-
fallbackRuntime: runtime,
|
|
554
|
-
decision: task.allocation,
|
|
555
|
-
modelPin: ctx.modelPin,
|
|
556
|
-
effortPin: ctx.effortPin,
|
|
557
|
-
availableModels: ctx.availableModels,
|
|
558
|
-
maxTier: ctx.maxTier || process.env.AGENTLAS_MODEL_MAX_TIER,
|
|
559
|
-
});
|
|
560
|
-
recordAllocation(task, stage, task.allocation, resolution, parentTaskId);
|
|
561
|
-
if (!resolution.ok) {
|
|
562
|
-
throw new Error(`model allocation failed closed: ${resolution.fallbackReason || "no compliant live model"}`);
|
|
563
|
-
}
|
|
564
|
-
if (resolution.fallbackReason) {
|
|
565
|
-
ui.info(`model route: ${resolution.source} · ${resolution.runtimeId || "current"} · ${resolution.model || runtime.kind || runtime.backend} · ${resolution.fallbackReason}`);
|
|
566
|
-
}
|
|
567
|
-
const selectedRuntime = resolution.runtime || runtime;
|
|
568
|
-
task.resolvedAllocation = {
|
|
569
|
-
runtimeId: resolution.runtimeId || selectedRuntime.runtimeId || null,
|
|
570
|
-
runtimeKind: selectedRuntime.kind || selectedRuntime.backend || null,
|
|
571
|
-
model: resolution.model || selectedRuntime.model || null,
|
|
572
|
-
effort: resolution.effort ?? null,
|
|
573
|
-
source: resolution.source,
|
|
574
|
-
fallbackReason: resolution.fallbackReason || null,
|
|
575
|
-
};
|
|
576
|
-
if (selectedRuntime.mode === "cli") {
|
|
577
|
-
return await D.captureRuntime(selectedRuntime.kind, system, prompt, {
|
|
578
|
-
cwd,
|
|
579
|
-
env,
|
|
580
|
-
permission,
|
|
581
|
-
model: resolution.model,
|
|
582
|
-
effort: resolution.effort,
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
const text = await D.runApi(selectedRuntime.backend, resolution.model || selectedRuntime.model, system, prompt);
|
|
586
|
-
return typeof text === "string" ? text : (text && text.text) || "";
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
const label = runtime.mode === "cli" ? runtime.kind : runtime.backend;
|
|
590
|
-
ui.line("");
|
|
591
|
-
ui.line(ui.c.paw("◤ ") + ui.c.bold(ui.c.text(stormbreaker ? "stormbreaker" : "swarm")) + ui.c.dim(` Agentlas harness · ${label} · x${concurrency} · max ${SWARM_MAX_TASKS} tasks`));
|
|
592
|
-
ui.info(goal.slice(0, 120));
|
|
593
|
-
|
|
594
|
-
ui.startSpinner(ui.lang === "ko" ? "상위 AI가 작업별 모델 비용을 배정 중…" : "Higher-level AI is allocating task models…");
|
|
595
|
-
let planned = null;
|
|
596
|
-
try {
|
|
597
|
-
const plannerText = await runBaseWorker(
|
|
598
|
-
[
|
|
599
|
-
coreHarnessPrompt,
|
|
600
|
-
workloadRouting.plannerSystemPrompt({
|
|
601
|
-
language: ui.lang === "ko" ? "Korean" : "English",
|
|
602
|
-
maxTasks: Math.min(SWARM_SPAWN_PER_TURN, SWARM_MAX_TASKS),
|
|
603
|
-
mode: stormbreaker ? "stormbreaker-goal-ultracode" : "swarm",
|
|
604
|
-
liveRuntimeInventory,
|
|
605
|
-
}),
|
|
606
|
-
].filter(Boolean).join("\n\n"),
|
|
607
|
-
ctx.routeContext
|
|
608
|
-
? `${goal}\n\nHEPHAESTUS ROUTE EVIDENCE (advisory; the Agentlas parent owns the final plan):\n${ctx.routeContext}`
|
|
609
|
-
: goal,
|
|
610
|
-
);
|
|
611
|
-
planned = workloadRouting.normalizePlan(plannerText, { maxTasks: SWARM_SPAWN_PER_TURN });
|
|
612
|
-
} catch (error) {
|
|
613
|
-
ui.warn(`workload planner failed: ${String((error && error.message) || error).slice(0, 160)}`);
|
|
614
|
-
}
|
|
615
|
-
ui.stopSpinner();
|
|
616
|
-
if (!planned) ui.warn(ui.lang === "ko" ? "모델 배정 JSON이 유효하지 않아 현재 모델로 투명하게 폴백합니다." : "Invalid allocation JSON; transparently falling back to the current model.");
|
|
617
|
-
if (planned) {
|
|
618
|
-
ui.line("");
|
|
619
|
-
ui.info(stormbreaker
|
|
620
|
-
? (ui.lang === "ko" ? "Stormbreaker Goal/UltraCode 실행 계획:" : "Stormbreaker Goal/UltraCode execution plan:")
|
|
621
|
-
: (ui.lang === "ko" ? "스웜 실행 계획:" : "Swarm execution plan:"));
|
|
622
|
-
for (const task of planned.tasks) {
|
|
623
|
-
const allocation = task.allocation;
|
|
624
|
-
ui.line(` ${ui.c.emerald("▸ ")}${ui.c.text(task.title)}${ui.c.dim(` ${allocation.runtimeId || "current"} · ${allocation.exactModelId || allocation.tier} · ${allocation.effort}`)}`);
|
|
625
|
-
}
|
|
626
|
-
ui.line(` ${ui.c.emerald("◆ ")}${ui.c.text("synthesis")}${ui.c.dim(` ${planned.synthesis.runtimeId || "current"} · ${planned.synthesis.exactModelId || planned.synthesis.tier} · ${planned.synthesis.effort}`)}`);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
let seq = 0;
|
|
630
|
-
const initialTasks = planned
|
|
631
|
-
? planned.tasks
|
|
632
|
-
: [{ title: goal.slice(0, 80), brief: goal, role: undefined, allocation: null }];
|
|
633
|
-
const tasks = initialTasks.map((task) => ({ id: ++seq, ...task, status: "pending", result: "", parentTaskId: null }));
|
|
634
|
-
const seen = new Set(tasks.map((task) => task.title.toLowerCase()));
|
|
635
|
-
let active = 0;
|
|
636
|
-
let failed = 0;
|
|
637
|
-
|
|
638
|
-
await new Promise((resolveAll) => {
|
|
639
|
-
const pump = () => {
|
|
640
|
-
const pending = tasks.filter((t) => t.status === "pending");
|
|
641
|
-
if (!pending.length && active === 0) return resolveAll();
|
|
642
|
-
for (const task of pending) {
|
|
643
|
-
if (active >= concurrency) break;
|
|
644
|
-
task.status = "running";
|
|
645
|
-
active++;
|
|
646
|
-
ui.tool(`⚑ ${task.title}` + (task.role ? ` (${task.role})` : ""));
|
|
647
|
-
runAllocatedWorker(
|
|
648
|
-
[coreHarnessPrompt, swarmProtocol(goal, tasks, task, liveRuntimeInventory)].filter(Boolean).join("\n\n"),
|
|
649
|
-
task.brief || task.title,
|
|
650
|
-
task,
|
|
651
|
-
"worker",
|
|
652
|
-
task.parentTaskId,
|
|
653
|
-
)
|
|
654
|
-
.then((text) => {
|
|
655
|
-
const parsed = parseSwarmOutput(text);
|
|
656
|
-
task.status = "done";
|
|
657
|
-
task.result = parsed.result;
|
|
658
|
-
ui.toolResult(parsed.result.split("\n").slice(0, 3).join("\n") || "(empty result)", true);
|
|
659
|
-
for (const s of parsed.spawn) {
|
|
660
|
-
const key = s.title.toLowerCase();
|
|
661
|
-
if (tasks.length >= SWARM_MAX_TASKS || seen.has(key)) continue;
|
|
662
|
-
seen.add(key);
|
|
663
|
-
tasks.push({ id: ++seq, title: s.title, brief: s.brief, role: s.role, allocation: s.allocation, status: "pending", result: "", parentTaskId: `worker-${task.id}` });
|
|
664
|
-
ui.info(`+ spawn: ${s.title}`);
|
|
665
|
-
}
|
|
666
|
-
})
|
|
667
|
-
.catch((e) => {
|
|
668
|
-
task.status = "failed";
|
|
669
|
-
failed++;
|
|
670
|
-
ui.toolResult(String((e && e.message) || e).slice(0, 200), false);
|
|
671
|
-
})
|
|
672
|
-
.finally(() => {
|
|
673
|
-
active--;
|
|
674
|
-
setImmediate(pump);
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
};
|
|
678
|
-
pump();
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
const done = tasks.filter((t) => t.status === "done" && t.result);
|
|
682
|
-
ui.line("");
|
|
683
|
-
ui.info(`tasks: ${tasks.length} · done: ${done.length} · failed: ${failed}`);
|
|
684
|
-
if (!done.length) {
|
|
685
|
-
ui.error(ui.lang === "ko" ? "스웜이 완료한 작업이 없습니다." : "The swarm completed no work.");
|
|
686
|
-
return { ok: false };
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
ui.startSpinner(ui.lang === "ko" ? "스웜 결과 종합 중…" : "Synthesizing swarm results…");
|
|
690
|
-
const pieces = done.map((t, i) => [
|
|
691
|
-
`### ${i + 1}. ${t.title}`,
|
|
692
|
-
`HOST-VERIFIED ALLOCATION: ${JSON.stringify(t.resolvedAllocation || null)}`,
|
|
693
|
-
t.result,
|
|
694
|
-
].join("\n")).join("\n\n");
|
|
695
|
-
let finalText;
|
|
696
|
-
try {
|
|
697
|
-
const synthesisTask = {
|
|
698
|
-
id: "final",
|
|
699
|
-
title: "swarm synthesis",
|
|
700
|
-
brief: goal,
|
|
701
|
-
allocation: planned && planned.synthesis,
|
|
702
|
-
};
|
|
703
|
-
finalText = await runAllocatedWorker(
|
|
704
|
-
[
|
|
705
|
-
coreHarnessPrompt,
|
|
706
|
-
"You are the synthesizer of an agent swarm. Below are the results your peers produced for the shared goal.",
|
|
707
|
-
"Integrate them into ONE coherent final answer for the user. Reconcile overlaps, note anything incomplete.",
|
|
708
|
-
"Do not just concatenate. Do not include a `## Spawn` block.",
|
|
709
|
-
`SHARED GOAL: ${goal}`,
|
|
710
|
-
`Answer in the user's language (${ui.lang === "ko" ? "Korean" : "English"}).`,
|
|
711
|
-
].join("\n"),
|
|
712
|
-
pieces,
|
|
713
|
-
synthesisTask,
|
|
714
|
-
"synthesis",
|
|
715
|
-
);
|
|
716
|
-
} catch (e) {
|
|
717
|
-
ui.stopSpinner();
|
|
718
|
-
ui.error("Synthesis failed: " + String((e && e.message) || e).slice(0, 200));
|
|
719
|
-
finalText = pieces;
|
|
720
|
-
}
|
|
721
|
-
ui.stopSpinner();
|
|
722
|
-
ui.line("");
|
|
723
|
-
ui.markdown(String(finalText).trim());
|
|
724
|
-
return { ok: true, finalText, taskCount: tasks.length, doneCount: done.length };
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
async function cmdSwarm(db, args, runtimeOverride, executionContext = {}) {
|
|
728
|
-
const rest = [];
|
|
729
|
-
let concurrency;
|
|
730
|
-
for (let i = 0; i < args.length; i++) {
|
|
731
|
-
if (args[i] === "--parallel" || args[i] === "-n") concurrency = Number(args[++i]);
|
|
732
|
-
else rest.push(args[i]);
|
|
733
|
-
}
|
|
734
|
-
const r = await swarmRun(db, rest.join(" "), { ...executionContext, concurrency, runtimeOverride });
|
|
735
|
-
if (!r.ok) process.exitCode = 1;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// ── 미니 cron (5필드: 분 시 일 월 요일) — next_run_at 계산용 ──
|
|
739
|
-
// 앱 스케줄러(croner)는 next_run_at IS NULL 을 "시계 없음"으로 취급하므로 CLI가 직접 채워야 한다.
|
|
740
|
-
function cronField(expr, min, max) {
|
|
741
|
-
const set = new Set();
|
|
742
|
-
for (const part of String(expr).split(",")) {
|
|
743
|
-
const m = part.match(/^(\*|\d+(?:-\d+)?)(?:\/(\d+))?$/);
|
|
744
|
-
if (!m) return null;
|
|
745
|
-
const step = m[2] ? Number(m[2]) : 1;
|
|
746
|
-
let lo = min;
|
|
747
|
-
let hi = max;
|
|
748
|
-
if (m[1] !== "*") {
|
|
749
|
-
const range = m[1].split("-").map(Number);
|
|
750
|
-
lo = range[0];
|
|
751
|
-
hi = range.length > 1 ? range[1] : m[2] ? max : range[0];
|
|
752
|
-
}
|
|
753
|
-
if (lo < min || hi > max || lo > hi || step < 1) return null;
|
|
754
|
-
for (let v = lo; v <= hi; v += step) set.add(v);
|
|
755
|
-
}
|
|
756
|
-
return set;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
const WEEKDAY_INDEX = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
|
|
760
|
-
const zonedFormatterCache = new Map();
|
|
761
|
-
|
|
762
|
-
function zonedDateParts(date, timezone) {
|
|
763
|
-
if (!timezone) {
|
|
764
|
-
return {
|
|
765
|
-
minute: date.getMinutes(),
|
|
766
|
-
hour: date.getHours(),
|
|
767
|
-
day: date.getDate(),
|
|
768
|
-
month: date.getMonth() + 1,
|
|
769
|
-
weekday: date.getDay(),
|
|
770
|
-
};
|
|
771
|
-
}
|
|
772
|
-
let formatter = zonedFormatterCache.get(timezone);
|
|
773
|
-
if (!formatter) {
|
|
774
|
-
formatter = new Intl.DateTimeFormat("en-US", {
|
|
775
|
-
timeZone: timezone,
|
|
776
|
-
hourCycle: "h23",
|
|
777
|
-
minute: "2-digit",
|
|
778
|
-
hour: "2-digit",
|
|
779
|
-
day: "2-digit",
|
|
780
|
-
month: "2-digit",
|
|
781
|
-
weekday: "short",
|
|
782
|
-
});
|
|
783
|
-
zonedFormatterCache.set(timezone, formatter);
|
|
784
|
-
}
|
|
785
|
-
const parts = Object.fromEntries(
|
|
786
|
-
formatter.formatToParts(date).filter((part) => part.type !== "literal").map((part) => [part.type, part.value]),
|
|
787
|
-
);
|
|
788
|
-
return {
|
|
789
|
-
minute: Number(parts.minute),
|
|
790
|
-
hour: Number(parts.hour),
|
|
791
|
-
day: Number(parts.day),
|
|
792
|
-
month: Number(parts.month),
|
|
793
|
-
weekday: WEEKDAY_INDEX[parts.weekday],
|
|
794
|
-
};
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
function nextCronRun(cron, from = new Date(), timezone = null) {
|
|
798
|
-
const parts = String(cron).trim().split(/\s+/);
|
|
799
|
-
if (parts.length !== 5) return null;
|
|
800
|
-
const [minS, hourS, domS, monS, dowS] = parts;
|
|
801
|
-
const mins = cronField(minS, 0, 59);
|
|
802
|
-
const hours = cronField(hourS, 0, 23);
|
|
803
|
-
const doms = cronField(domS, 1, 31);
|
|
804
|
-
const mons = cronField(monS, 1, 12);
|
|
805
|
-
const dows = cronField(dowS, 0, 7);
|
|
806
|
-
if (!mins || !hours || !doms || !mons || !dows) return null;
|
|
807
|
-
if (dows.has(7)) dows.add(0);
|
|
808
|
-
try {
|
|
809
|
-
if (timezone) zonedDateParts(from, timezone);
|
|
810
|
-
} catch {
|
|
811
|
-
return null;
|
|
812
|
-
}
|
|
813
|
-
const t = new Date(from.getTime());
|
|
814
|
-
t.setSeconds(0, 0);
|
|
815
|
-
t.setMinutes(t.getMinutes() + 1);
|
|
816
|
-
for (let i = 0; i < 366 * 24 * 60; i++) {
|
|
817
|
-
const local = zonedDateParts(t, timezone);
|
|
818
|
-
const domOk = doms.has(local.day);
|
|
819
|
-
const dowOk = dows.has(local.weekday);
|
|
820
|
-
// 표준 cron: dom/dow 둘 다 제한이면 OR, 아니면 AND
|
|
821
|
-
const domRestricted = domS !== "*";
|
|
822
|
-
const dowRestricted = dowS !== "*";
|
|
823
|
-
const dayOk = domRestricted && dowRestricted ? domOk || dowOk : domOk && dowOk;
|
|
824
|
-
if (mons.has(local.month) && dayOk && hours.has(local.hour) && mins.has(local.minute)) return t;
|
|
825
|
-
t.setMinutes(t.getMinutes() + 1);
|
|
826
|
-
}
|
|
827
|
-
return null;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function localTimezone() {
|
|
831
|
-
try { return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; } catch { return "UTC"; }
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
function legacyScheduleSpec(raw, timezone) {
|
|
835
|
-
const value = String(raw || "").trim();
|
|
836
|
-
if (!value) return null;
|
|
837
|
-
if (value.startsWith("cron:")) {
|
|
838
|
-
const expr = value.slice(5).trim();
|
|
839
|
-
return expr ? { kind: "cron", expr, tz: timezone } : null;
|
|
840
|
-
}
|
|
841
|
-
if (value.split(/\s+/).length === 5) return { kind: "cron", expr: value, tz: timezone };
|
|
842
|
-
if (value === "hourly") return { kind: "interval", everyMs: 60 * 60 * 1000, anchor: "lastRun" };
|
|
843
|
-
const every = value.match(/^every-(\d+)(m|h)$/);
|
|
844
|
-
if (every) {
|
|
845
|
-
const amount = Number(every[1]);
|
|
846
|
-
if (amount > 0) return { kind: "interval", everyMs: amount * (every[2] === "h" ? 3600000 : 60000), anchor: "lastRun" };
|
|
847
|
-
}
|
|
848
|
-
let match = value.match(/^daily-(\d{1,2}):(\d{2})$/);
|
|
849
|
-
if (match) return { kind: "cron", expr: `${Number(match[2])} ${Number(match[1])} * * *`, tz: timezone };
|
|
850
|
-
match = value.match(/^weekday-(\d{1,2}):(\d{2})$/);
|
|
851
|
-
if (match) return { kind: "cron", expr: `${Number(match[2])} ${Number(match[1])} * * 1-5`, tz: timezone };
|
|
852
|
-
match = value.match(/^weekly-(sun|mon|tue|wed|thu|fri|sat)-(\d{1,2}):(\d{2})$/i);
|
|
853
|
-
if (match) {
|
|
854
|
-
const dow = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 }[match[1].toLowerCase()];
|
|
855
|
-
return { kind: "cron", expr: `${Number(match[3])} ${Number(match[2])} * * ${dow}`, tz: timezone };
|
|
856
|
-
}
|
|
857
|
-
match = value.match(/^monthly-(\d{1,2})-(\d{1,2}):(\d{2})$/);
|
|
858
|
-
if (match && Number(match[1]) >= 1 && Number(match[1]) <= 31) {
|
|
859
|
-
return { kind: "cron", expr: `${Number(match[3])} ${Number(match[2])} ${Number(match[1])} * *`, tz: timezone };
|
|
860
|
-
}
|
|
861
|
-
return null;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
/** Desktop schedule_json + legacy mirror token parity, including IANA timezone. */
|
|
865
|
-
function nextAutomationRun(row, from = new Date()) {
|
|
866
|
-
const timezone = row.timezone || localTimezone();
|
|
867
|
-
let spec = null;
|
|
868
|
-
if (row.schedule_json && String(row.schedule_json).trim()) {
|
|
869
|
-
try {
|
|
870
|
-
const parsed = JSON.parse(row.schedule_json);
|
|
871
|
-
if (parsed && typeof parsed.kind === "string") spec = parsed;
|
|
872
|
-
} catch { /* fall through to legacy schedule */ }
|
|
873
|
-
}
|
|
874
|
-
if (!spec) spec = legacyScheduleSpec(row.schedule, timezone);
|
|
875
|
-
if (!spec) {
|
|
876
|
-
// Desktop computeNextRun preserves unknown legacy schedules with a 24h
|
|
877
|
-
// fallback. More importantly, never leave a due row at the same instant.
|
|
878
|
-
return row.schedule ? new Date(from.getTime() + 24 * 3600 * 1000) : null;
|
|
879
|
-
}
|
|
880
|
-
if (spec.kind === "cron") return nextCronRun(spec.expr, from, spec.tz || timezone);
|
|
881
|
-
if (spec.kind === "interval") {
|
|
882
|
-
const every = Number(spec.everyMs);
|
|
883
|
-
if (!Number.isFinite(every) || every <= 0) return null;
|
|
884
|
-
return spec.anchor === "wallclock"
|
|
885
|
-
? new Date(Math.ceil((from.getTime() + 1) / every) * every)
|
|
886
|
-
: new Date(from.getTime() + every);
|
|
887
|
-
}
|
|
888
|
-
if (spec.kind === "once") {
|
|
889
|
-
const at = new Date(spec.atIso);
|
|
890
|
-
return at.getTime() > from.getTime() ? at : null;
|
|
891
|
-
}
|
|
892
|
-
return null;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// ── automation — 등록/목록/토글/실행 (run·daemon은 로컬 실행기) ──
|
|
896
|
-
async function cmdAutomation(db, args, runtimeOverride) {
|
|
897
|
-
const sub = args[0] || "list";
|
|
898
|
-
const now = new Date().toISOString();
|
|
899
|
-
|
|
900
|
-
if (sub === "list") {
|
|
901
|
-
const rows = db.prepare(
|
|
902
|
-
"SELECT id, name, schedule, target_type, target_id, enabled, next_run_at, last_run_at, run_count, trigger_type FROM automations ORDER BY created_at DESC",
|
|
903
|
-
).all();
|
|
904
|
-
if (!rows.length) return D.out("No automations. Run agentlas automation add --help.");
|
|
905
|
-
for (const r of rows) {
|
|
906
|
-
const target = r.target_type + ":" + String(r.target_id).slice(0, 24);
|
|
907
|
-
D.out(
|
|
908
|
-
`${r.enabled ? "●" : "○"} ${String(r.id).slice(0, 8)} ${String(r.name).padEnd(28).slice(0, 28)} ` +
|
|
909
|
-
`${String(r.schedule || r.trigger_type).padEnd(14)} ${target.padEnd(32)} ` +
|
|
910
|
-
`next=${r.next_run_at ? r.next_run_at.slice(0, 16) : "-"} runs=${r.run_count}`,
|
|
911
|
-
);
|
|
912
|
-
}
|
|
913
|
-
D.out("");
|
|
914
|
-
D.out("Run now: agentlas automation run <id> · daemon: agentlas automation daemon");
|
|
915
|
-
D.out("The Desktop scheduler also runs when the app is open; leases prevent duplicate runs.");
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
if (sub === "add") {
|
|
920
|
-
// agentlas automation add --name "..." --agent <slug>|--firm <slug> --cron "0 9 * * *" --prompt "..."
|
|
921
|
-
const flags = {};
|
|
922
|
-
for (let i = 1; i < args.length; i++) {
|
|
923
|
-
const a = args[i];
|
|
924
|
-
if (a === "--name") flags.name = args[++i];
|
|
925
|
-
else if (a === "--agent") flags.agent = args[++i];
|
|
926
|
-
else if (a === "--firm") flags.firm = args[++i];
|
|
927
|
-
else if (a === "--cron") flags.cron = args[++i];
|
|
928
|
-
else if (a === "--prompt") flags.prompt = args[++i];
|
|
929
|
-
else if (a === "--tz") flags.tz = args[++i];
|
|
930
|
-
else if (a === "--disabled") flags.disabled = true;
|
|
931
|
-
}
|
|
932
|
-
if (!flags.cron || !flags.prompt || (!flags.agent && !flags.firm)) {
|
|
933
|
-
D.out('usage: agentlas automation add --name "name" --agent <slug>|--firm <slug> --cron "0 9 * * *" --prompt "instructions" [--tz Asia/Seoul] [--disabled]');
|
|
934
|
-
process.exitCode = 1;
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
let targetType;
|
|
938
|
-
let targetId;
|
|
939
|
-
let targetLabel;
|
|
940
|
-
if (flags.agent) {
|
|
941
|
-
const a = D.resolveAgent(db, flags.agent);
|
|
942
|
-
if (!a) return D.fail(`Agent not found: ${flags.agent}`);
|
|
943
|
-
targetType = "agent";
|
|
944
|
-
targetId = a.id;
|
|
945
|
-
targetLabel = a.name;
|
|
946
|
-
} else {
|
|
947
|
-
const f = D.resolveFirm(db, flags.firm);
|
|
948
|
-
if (!f) return D.fail(`Company not found: ${flags.firm}`);
|
|
949
|
-
targetType = "firm";
|
|
950
|
-
targetId = f.id;
|
|
951
|
-
targetLabel = f.name;
|
|
952
|
-
}
|
|
953
|
-
const next = nextCronRun(flags.cron, new Date(), flags.tz || null);
|
|
954
|
-
if (!next) return D.fail(`Could not parse cron expression: "${flags.cron}" (5 fields: minute hour day month weekday)`);
|
|
955
|
-
const id = crypto.randomUUID();
|
|
956
|
-
db.prepare(
|
|
957
|
-
`INSERT INTO automations (id, name, schedule, target_type, target_id, prompt_template, enabled, created_by,
|
|
958
|
-
next_run_at, created_at, timezone, trigger_type, tool_mode, hub_mode, run_count)
|
|
959
|
-
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,0)`,
|
|
960
|
-
).run(
|
|
961
|
-
id,
|
|
962
|
-
flags.name || `${targetLabel} automation`,
|
|
963
|
-
flags.cron,
|
|
964
|
-
targetType,
|
|
965
|
-
targetId,
|
|
966
|
-
flags.prompt,
|
|
967
|
-
flags.disabled ? 0 : 1,
|
|
968
|
-
"cli",
|
|
969
|
-
next.toISOString(),
|
|
970
|
-
now,
|
|
971
|
-
flags.tz || null,
|
|
972
|
-
"schedule",
|
|
973
|
-
"auto",
|
|
974
|
-
"hub-allowed",
|
|
975
|
-
);
|
|
976
|
-
D.out(`Created: ${id.slice(0, 8)} ${flags.name || targetLabel} next=${next.toISOString().slice(0, 16)}`);
|
|
977
|
-
D.out(`Run now: agentlas automation run ${id.slice(0, 8)} · scheduled: agentlas automation daemon (or Desktop)`);
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
if (sub === "on" || sub === "off") {
|
|
982
|
-
const idPrefix = args[1];
|
|
983
|
-
if (!idPrefix) return D.fail(`usage: agentlas automation ${sub} <id>`);
|
|
984
|
-
const row = db.prepare("SELECT id, name, schedule, schedule_json, timezone FROM automations WHERE id LIKE ?").get(idPrefix + "%");
|
|
985
|
-
if (!row) return D.fail(`Automation not found: ${idPrefix}`);
|
|
986
|
-
if (sub === "on") {
|
|
987
|
-
const next = nextAutomationRun(row) || null;
|
|
988
|
-
db.prepare("UPDATE automations SET enabled=1, next_run_at=? WHERE id=?").run(next ? next.toISOString() : null, row.id);
|
|
989
|
-
} else {
|
|
990
|
-
db.prepare("UPDATE automations SET enabled=0 WHERE id=?").run(row.id);
|
|
991
|
-
}
|
|
992
|
-
D.out(`${sub === "on" ? "Enabled" : "Disabled"}: ${row.id.slice(0, 8)} ${row.name}`);
|
|
993
|
-
return;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
if (sub === "remove" || sub === "rm") {
|
|
997
|
-
const idPrefix = args[1];
|
|
998
|
-
if (!idPrefix) return D.fail("usage: agentlas automation remove <id>");
|
|
999
|
-
const row = db.prepare("SELECT id, name FROM automations WHERE id LIKE ?").get(idPrefix + "%");
|
|
1000
|
-
if (!row) return D.fail(`Automation not found: ${idPrefix}`);
|
|
1001
|
-
db.prepare("DELETE FROM automations WHERE id=?").run(row.id);
|
|
1002
|
-
D.out(`Deleted: ${row.id.slice(0, 8)} ${row.name}`);
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
if (sub === "runs") {
|
|
1007
|
-
const rows = db.prepare(
|
|
1008
|
-
`SELECT h.ran_at, h.status, h.error, a.name FROM run_history h
|
|
1009
|
-
LEFT JOIN automations a ON a.id = h.automation_id
|
|
1010
|
-
ORDER BY h.ran_at DESC LIMIT 15`,
|
|
1011
|
-
).all();
|
|
1012
|
-
if (!rows.length) return D.out("No run history.");
|
|
1013
|
-
for (const r of rows) {
|
|
1014
|
-
D.out(
|
|
1015
|
-
`${(r.ran_at || "").slice(0, 16).padEnd(17)} ${(r.status || "?").padEnd(9)} ${(r.name || "(deleted)").slice(0, 30).padEnd(31)} ${r.error ? String(r.error).slice(0, 40) : ""}`,
|
|
1016
|
-
);
|
|
1017
|
-
}
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
if (sub === "run") {
|
|
1022
|
-
const idPrefix = args[1];
|
|
1023
|
-
if (!idPrefix) return D.fail("usage: agentlas automation run <id>");
|
|
1024
|
-
const row = db.prepare("SELECT * FROM automations WHERE id LIKE ?").get(idPrefix + "%");
|
|
1025
|
-
if (!row) return D.fail(`Automation not found: ${idPrefix}`);
|
|
1026
|
-
const ui = newUi();
|
|
1027
|
-
// run-now는 스케줄을 건드리지 않는다 (앱의 advanceSchedule=false와 동일).
|
|
1028
|
-
const r = await runAutomationOnce(db, row, { ui, advanceSchedule: false, runtimeOverride });
|
|
1029
|
-
if (!r.ok) process.exitCode = 1;
|
|
1030
|
-
return;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
if (sub === "daemon") {
|
|
1034
|
-
let interval = 30;
|
|
1035
|
-
for (let i = 1; i < args.length; i++) {
|
|
1036
|
-
if (args[i] === "--interval") interval = Math.max(10, Number(args[++i]) || 30);
|
|
1037
|
-
}
|
|
1038
|
-
return automationDaemon(db, { intervalSec: interval });
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
D.out("usage: agentlas automation list|add|on <id>|off <id>|remove <id>|run <id>|runs|daemon");
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// ── automation 실행기 — 앱 스케줄러와 같은 SQLite 리스(claimed_at TTL 15분)로 중복 실행 방지 ──
|
|
1045
|
-
const AUTOMATION_LEASE_TTL_MS = 15 * 60 * 1000; // 앱 store/automations.ts LEASE_TTL_MS와 동일
|
|
1046
|
-
const AUTOMATION_LEASE_OWNER = `cli:${os.hostname()}:${process.pid}`;
|
|
1047
|
-
|
|
1048
|
-
function claimAutomation(db, id, now = new Date()) {
|
|
1049
|
-
const cutoff = new Date(now.getTime() - AUTOMATION_LEASE_TTL_MS).toISOString();
|
|
1050
|
-
const result = db
|
|
1051
|
-
.prepare(
|
|
1052
|
-
"UPDATE automations SET claimed_at = ?, lease_owner = ? WHERE id = ? AND enabled = 1 AND (claimed_at IS NULL OR claimed_at < ?)",
|
|
1053
|
-
)
|
|
1054
|
-
.run(now.toISOString(), AUTOMATION_LEASE_OWNER, id, cutoff);
|
|
1055
|
-
return (result.changes ?? result.rowsAffected ?? 0) > 0;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
function releaseAutomation(db, id) {
|
|
1059
|
-
try { db.prepare("UPDATE automations SET claimed_at = NULL, lease_owner = NULL WHERE id = ?").run(id); } catch { /* best-effort */ }
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
function recordAutomationRun(db, automationId, status, error, scheduledFor) {
|
|
1063
|
-
try {
|
|
1064
|
-
db.prepare(
|
|
1065
|
-
"INSERT INTO run_history (id, automation_id, scheduled_for, ran_at, status, skipped_count, error) VALUES (?,?,?,?,?,0,?)",
|
|
1066
|
-
).run(crypto.randomUUID(), automationId, scheduledFor || null, new Date().toISOString(), status, error || null);
|
|
1067
|
-
} catch { /* best-effort */ }
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
// 자동화 1건 실행: 타깃(agent/firm)의 시스템 프롬프트로 prompt_template을 활성 런타임에 태운다.
|
|
1071
|
-
// ctx: { ui, advanceSchedule?, runtimeOverride?, scheduledFor? }
|
|
1072
|
-
async function runAutomationOnce(db, row, ctx = {}) {
|
|
1073
|
-
const ui = ctx.ui || newUi();
|
|
1074
|
-
// run-now도 리스를 잡는다 — 앱 스케줄러가 같은 행을 동시에 돌리는 것을 방지.
|
|
1075
|
-
if (!claimAutomation(db, row.id)) {
|
|
1076
|
-
ui.warn(`Another runner holds this automation (lease TTL 15 minutes): ${row.name}`);
|
|
1077
|
-
return { ok: false, skipped: true };
|
|
1078
|
-
}
|
|
1079
|
-
ui.line("");
|
|
1080
|
-
ui.line(ui.c.paw("◤ ") + ui.c.bold(ui.c.text("automation")) + ui.c.dim(` ${row.name} (${String(row.id).slice(0, 8)})`));
|
|
1081
|
-
|
|
1082
|
-
try {
|
|
1083
|
-
// 타깃 해석 (agent/firm) — background/비공개 포함 id 직접 조회.
|
|
1084
|
-
let system;
|
|
1085
|
-
let agentId = null;
|
|
1086
|
-
if (row.target_type === "firm") {
|
|
1087
|
-
const firm = db.prepare("SELECT * FROM firms WHERE id = ?").get(row.target_id);
|
|
1088
|
-
if (!firm) throw new Error(`Company not found: ${row.target_id}`);
|
|
1089
|
-
system = D.firmSystemPrompt(db, firm);
|
|
1090
|
-
} else {
|
|
1091
|
-
const agent = db.prepare("SELECT * FROM installed_agents WHERE id = ?").get(row.target_id);
|
|
1092
|
-
if (!agent) throw new Error(`Agent not found: ${row.target_id}`);
|
|
1093
|
-
system = agent.system_prompt || `You are ${agent.name}.`;
|
|
1094
|
-
agentId = agent.id;
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
const cwd = D.runCwd();
|
|
1098
|
-
const env = await D.buildChildEnvCli(db, { projectPath: null, agentId, permission: "write", cwd, lang: ui.lang });
|
|
1099
|
-
const runtime = D.resolveRuntime(db, ctx.runtimeOverride);
|
|
1100
|
-
ui.info(`${runtime.mode === "cli" ? runtime.kind : runtime.backend} · write · ${cwd}`);
|
|
1101
|
-
ui.startSpinner(ui.lang === "ko" ? "자동화 실행 중…" : "running automation…");
|
|
1102
|
-
|
|
1103
|
-
let text;
|
|
1104
|
-
if (runtime.mode === "cli") {
|
|
1105
|
-
text = await D.captureRuntime(runtime.kind, system, row.prompt_template, { cwd, env, permission: "write" });
|
|
1106
|
-
} else {
|
|
1107
|
-
const r = await D.runApi(runtime.backend, runtime.model, system, row.prompt_template);
|
|
1108
|
-
text = typeof r === "string" ? r : (r && r.text) || "";
|
|
1109
|
-
}
|
|
1110
|
-
ui.stopSpinner();
|
|
1111
|
-
ui.markdown(String(text).trim().slice(0, 4000));
|
|
1112
|
-
|
|
1113
|
-
recordAutomationRun(db, row.id, "ok", null, ctx.scheduledFor);
|
|
1114
|
-
const ranAt = new Date();
|
|
1115
|
-
const shouldAdvance = !!ctx.advanceSchedule && (row.trigger_type || "schedule") === "schedule";
|
|
1116
|
-
const advance = shouldAdvance ? nextAutomationRun(row, ranAt) : null;
|
|
1117
|
-
if (shouldAdvance) {
|
|
1118
|
-
db.prepare(
|
|
1119
|
-
"UPDATE automations SET last_run_at = ?, run_count = run_count + 1, next_run_at = ?, enabled = ? WHERE id = ?",
|
|
1120
|
-
).run(ranAt.toISOString(), advance ? advance.toISOString() : null, advance ? row.enabled : 0, row.id);
|
|
1121
|
-
} else {
|
|
1122
|
-
db.prepare("UPDATE automations SET last_run_at = ?, run_count = run_count + 1 WHERE id = ?")
|
|
1123
|
-
.run(ranAt.toISOString(), row.id);
|
|
1124
|
-
}
|
|
1125
|
-
// max_runs 도달 시 비활성화 (앱과 동일한 종료 조건).
|
|
1126
|
-
if (row.max_runs && row.run_count + 1 >= row.max_runs) {
|
|
1127
|
-
db.prepare("UPDATE automations SET enabled = 0 WHERE id = ?").run(row.id);
|
|
1128
|
-
ui.info(ui.lang === "ko" ? "max_runs 도달 — 자동화를 비활성화했습니다." : "max_runs reached — automation disabled.");
|
|
1129
|
-
}
|
|
1130
|
-
return { ok: true };
|
|
1131
|
-
} catch (e) {
|
|
1132
|
-
ui.stopSpinner();
|
|
1133
|
-
const msg = String((e && e.message) || e).slice(0, 500);
|
|
1134
|
-
ui.error(msg);
|
|
1135
|
-
recordAutomationRun(db, row.id, "error", msg, ctx.scheduledFor);
|
|
1136
|
-
const ranAt = new Date();
|
|
1137
|
-
const shouldAdvance = !!ctx.advanceSchedule && (row.trigger_type || "schedule") === "schedule";
|
|
1138
|
-
const advance = shouldAdvance ? nextAutomationRun(row, ranAt) : null;
|
|
1139
|
-
if (shouldAdvance) {
|
|
1140
|
-
db.prepare("UPDATE automations SET last_run_at = ?, next_run_at = ?, enabled = ? WHERE id = ?")
|
|
1141
|
-
.run(ranAt.toISOString(), advance ? advance.toISOString() : null, advance ? row.enabled : 0, row.id);
|
|
1142
|
-
} else {
|
|
1143
|
-
db.prepare("UPDATE automations SET last_run_at = ? WHERE id = ?").run(ranAt.toISOString(), row.id);
|
|
1144
|
-
}
|
|
1145
|
-
return { ok: false };
|
|
1146
|
-
} finally {
|
|
1147
|
-
releaseAutomation(db, row.id);
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
// 상주 실행기 — 앱 없이도 자동화가 돌게 하는 포그라운드 데몬 (Ctrl-C로 종료).
|
|
1152
|
-
async function automationDaemon(db, opts = {}) {
|
|
1153
|
-
const ui = newUi();
|
|
1154
|
-
const intervalSec = Math.max(10, opts.intervalSec || 30);
|
|
1155
|
-
let stopping = false;
|
|
1156
|
-
process.on("SIGINT", () => { stopping = true; ui.line(""); ui.info(ui.lang === "ko" ? "종료 중…" : "stopping…"); });
|
|
1157
|
-
process.on("SIGTERM", () => { stopping = true; });
|
|
1158
|
-
|
|
1159
|
-
ui.line("");
|
|
1160
|
-
ui.ok(`automation daemon — polling every ${intervalSec}s · owner ${AUTOMATION_LEASE_OWNER}`);
|
|
1161
|
-
ui.info(ui.lang === "ko" ? "Ctrl-C로 종료. (데스크탑 앱 스케줄러와 리스를 공유해 중복 실행되지 않습니다.)" : "Ctrl-C to stop.");
|
|
1162
|
-
|
|
1163
|
-
while (!stopping) {
|
|
1164
|
-
const nowIso = new Date().toISOString();
|
|
1165
|
-
let due = [];
|
|
1166
|
-
try {
|
|
1167
|
-
due = db.prepare(
|
|
1168
|
-
"SELECT * FROM automations WHERE enabled = 1 AND trigger_type = 'schedule' AND next_run_at IS NOT NULL AND next_run_at <= ? ORDER BY next_run_at ASC LIMIT 5",
|
|
1169
|
-
).all(nowIso);
|
|
1170
|
-
} catch (e) {
|
|
1171
|
-
ui.error("Failed to query due automations: " + String((e && e.message) || e));
|
|
1172
|
-
}
|
|
1173
|
-
for (const row of due) {
|
|
1174
|
-
if (stopping) break;
|
|
1175
|
-
await runAutomationOnce(db, row, { ui, advanceSchedule: true, scheduledFor: row.next_run_at });
|
|
1176
|
-
// 스케줄이 없는(1회성) 행이 남으면 재발화 방지.
|
|
1177
|
-
if (!row.schedule || !nextAutomationRun(row)) {
|
|
1178
|
-
db.prepare("UPDATE automations SET enabled = 0 WHERE id = ? AND (schedule IS NULL OR schedule = '')").run(row.id);
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
// interval 대기 (1초 단위로 stop 체크)
|
|
1182
|
-
for (let i = 0; i < intervalSec && !stopping; i++) {
|
|
1183
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
ui.info(ui.lang === "ko" ? "데몬 종료." : "daemon stopped.");
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
// ── mcp / chats — 데스크탑 데이터 열람 ──
|
|
1190
|
-
function cmdMcp(db) {
|
|
1191
|
-
let rows = [];
|
|
1192
|
-
try {
|
|
1193
|
-
rows = db.prepare("SELECT id, name, name_en, transport, enabled FROM mcp_servers ORDER BY installed_at ASC").all();
|
|
1194
|
-
} catch { /* 테이블 없음 */ }
|
|
1195
|
-
if (!rows.length) return D.out("No MCP servers are installed. Configure them in Desktop or an agent package.");
|
|
1196
|
-
for (const r of rows) {
|
|
1197
|
-
D.out(`${r.enabled ? "●" : "○"} ${String(r.name || r.name_en || r.id).padEnd(28).slice(0, 28)} ${String(r.transport || "stdio").padEnd(8)} ${String(r.id).slice(0, 12)}`);
|
|
1198
|
-
}
|
|
1199
|
-
D.out("");
|
|
1200
|
-
D.out("Only full-access turns wire active (●) stdio servers into the runtime. In the REPL, use /mcp.");
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
function cmdChats(db, args) {
|
|
1204
|
-
const limit = Math.max(1, Math.min(50, Number(args[0]) || 15));
|
|
1205
|
-
let rows = [];
|
|
1206
|
-
try {
|
|
1207
|
-
rows = db.prepare(
|
|
1208
|
-
`SELECT c.id, c.title, c.updated_at, a.name AS agent_name, a.name_en AS agent_name_en
|
|
1209
|
-
FROM chats c LEFT JOIN installed_agents a ON a.id = c.agent_id
|
|
1210
|
-
WHERE c.archived_at IS NULL AND c.kind = 'user'
|
|
1211
|
-
ORDER BY c.updated_at DESC LIMIT ?`,
|
|
1212
|
-
).all(limit);
|
|
1213
|
-
} catch { /* 스키마 차이 */ }
|
|
1214
|
-
if (!rows.length) return D.out("No chats.");
|
|
1215
|
-
for (const r of rows) {
|
|
1216
|
-
D.out(`${String(r.updated_at || "").slice(0, 16).padEnd(17)} ${String(r.agent_name_en || r.agent_name || "-").slice(0, 18).padEnd(19)} ${String(r.title || "(untitled)").slice(0, 60)}`);
|
|
1217
|
-
}
|
|
1218
|
-
D.out("");
|
|
1219
|
-
D.out("These chats are shared with Desktop. Resume terminal sessions with /resume in the REPL.");
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
// ── usage — 로컬 집계 ──
|
|
1223
|
-
async function cmdUsage(db) {
|
|
1224
|
-
const day = new Date(Date.now() - 86400000).toISOString();
|
|
1225
|
-
const week = new Date(Date.now() - 7 * 86400000).toISOString();
|
|
1226
|
-
const q = (sql, ...p) => {
|
|
1227
|
-
try { return db.prepare(sql).get(...p) || {}; } catch { return {}; }
|
|
1228
|
-
};
|
|
1229
|
-
const ar = q("SELECT kind FROM active_runtime WHERE id=1");
|
|
1230
|
-
const agents = q("SELECT COUNT(*) AS n FROM installed_agents");
|
|
1231
|
-
const chats = q("SELECT COUNT(*) AS n FROM chats WHERE archived_at IS NULL");
|
|
1232
|
-
const msg24 = q("SELECT COUNT(*) AS n FROM chat_messages WHERE created_at > ?", day);
|
|
1233
|
-
const msg7 = q("SELECT COUNT(*) AS n FROM chat_messages WHERE created_at > ?", week);
|
|
1234
|
-
const auto = q("SELECT COUNT(*) AS n FROM automations WHERE enabled=1");
|
|
1235
|
-
const runs7 = q("SELECT COUNT(*) AS n, SUM(CASE WHEN status='error' OR error IS NOT NULL THEN 1 ELSE 0 END) AS err FROM run_history WHERE ran_at > ?", week);
|
|
1236
|
-
D.out(`Active runtime ${ar.kind || "(none)"}`);
|
|
1237
|
-
D.out(`Installed agents ${agents.n ?? "?"}`);
|
|
1238
|
-
D.out(`Active chats ${chats.n ?? "?"}`);
|
|
1239
|
-
D.out(`Messages 24h ${msg24.n ?? 0} · 7d ${msg7.n ?? 0}`);
|
|
1240
|
-
D.out(`Automations ${auto.n ?? 0}`);
|
|
1241
|
-
D.out(`Runs (7d) ${runs7.n ?? 0}${runs7.err ? ` (${runs7.err} failed)` : ""}`);
|
|
1242
|
-
D.out("");
|
|
1243
|
-
D.out("Session tokens/cost: /cost in chat. Provider quota dashboards are in Desktop.");
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
// ── telegram — 바인딩 현황 (읽기 전용) ──
|
|
1247
|
-
function cmdTelegram(db, args) {
|
|
1248
|
-
let rows;
|
|
1249
|
-
try {
|
|
1250
|
-
rows = db.prepare("SELECT * FROM telegram_bindings ORDER BY rowid DESC").all();
|
|
1251
|
-
} catch {
|
|
1252
|
-
rows = [];
|
|
1253
|
-
}
|
|
1254
|
-
if (!rows.length) {
|
|
1255
|
-
D.out("No Telegram bindings. Pair devices from Desktop Connect.");
|
|
1256
|
-
return;
|
|
1257
|
-
}
|
|
1258
|
-
for (const r of rows) {
|
|
1259
|
-
const bot = r.bot_username ? "@" + r.bot_username : "(bot not set)";
|
|
1260
|
-
const chat = r.telegram_chat_title || r.telegram_chat_id || "(chat not connected)";
|
|
1261
|
-
const status = r.status || (r.telegram_chat_id ? "paired" : "pending");
|
|
1262
|
-
D.out(`${String(r.id).slice(0, 8)} ${r.target_kind}:${String(r.target_id).slice(0, 20).padEnd(21)} ${String(bot).padEnd(24)} ${String(chat).slice(0, 28).padEnd(29)} ${status}`);
|
|
1263
|
-
}
|
|
1264
|
-
D.out("");
|
|
1265
|
-
D.out("Pairing and bot issuance happen in Desktop Connect; this command only shows status.");
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
// ── login / logout / whoami — Agentlas Cloud 세션 (데스크탑과 동일한 loopback 브라우저 플로우) ──
|
|
1269
|
-
function webBaseUrl() {
|
|
1270
|
-
return (process.env.AGENTLAS_WEB_BASE_URL || "https://agentlas.cloud").replace(/\/$/, "");
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
function openInBrowser(url) {
|
|
1274
|
-
const argv =
|
|
1275
|
-
process.platform === "darwin" ? ["open", url]
|
|
1276
|
-
: process.platform === "win32" ? ["cmd", "/c", "start", "", url]
|
|
1277
|
-
: ["xdg-open", url];
|
|
1278
|
-
try {
|
|
1279
|
-
const child = spawn(argv[0], argv.slice(1), { stdio: "ignore", detached: true });
|
|
1280
|
-
child.unref();
|
|
1281
|
-
} catch { /* URL은 이미 출력됨 — 수동으로 열면 된다 */ }
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
async function fetchSessionMeta(cookie) {
|
|
1285
|
-
const resp = await fetch(`${webBaseUrl()}/api/auth/session`, { headers: { cookie }, signal: AbortSignal.timeout(8000) });
|
|
1286
|
-
if (!resp.ok) throw new Error(`Session check returned ${resp.status}`);
|
|
1287
|
-
return resp.json();
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
function loginCallbackHtml(ok) {
|
|
1291
|
-
const title = ok ? "Agentlas login complete" : "Agentlas login failed";
|
|
1292
|
-
const body = ok
|
|
1293
|
-
? "Return to the terminal. You can close this window."
|
|
1294
|
-
: "Return to the terminal and run agentlas login again.";
|
|
1295
|
-
return `<!doctype html><html><head><meta charset="utf-8"><title>Agentlas</title></head><body style="font-family:-apple-system,system-ui,sans-serif;padding:40px"><h3>${title}</h3><p>${body}</p></body></html>`;
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
function waitForLoopbackSession(options = {}) {
|
|
1299
|
-
const http = options.http || require("node:http");
|
|
1300
|
-
const timeoutCandidate = Number(options.timeoutMs);
|
|
1301
|
-
const timeoutMs = Number.isFinite(timeoutCandidate) && timeoutCandidate > 0 ? timeoutCandidate : LOGIN_TIMEOUT_MS;
|
|
1302
|
-
const state = createLoginState(options.randomBytes || crypto.randomBytes);
|
|
1303
|
-
const guard = createLoginCallbackGuard(state);
|
|
1304
|
-
const onLoginUrl = options.onLoginUrl || ((url) => {
|
|
1305
|
-
D.out("Sign in to Agentlas in the browser (opening automatically):");
|
|
1306
|
-
D.out(" " + url);
|
|
1307
|
-
openInBrowser(url);
|
|
1308
|
-
});
|
|
1309
|
-
|
|
1310
|
-
return new Promise((resolve, reject) => {
|
|
1311
|
-
let settled = false;
|
|
1312
|
-
let timer = null;
|
|
1313
|
-
let server;
|
|
1314
|
-
const finish = (error, value) => {
|
|
1315
|
-
if (settled) return;
|
|
1316
|
-
settled = true;
|
|
1317
|
-
if (timer) clearTimeout(timer);
|
|
1318
|
-
try { if (server) server.close(); } catch { /* ignore */ }
|
|
1319
|
-
if (error) reject(error);
|
|
1320
|
-
else resolve(value);
|
|
1321
|
-
};
|
|
1322
|
-
|
|
1323
|
-
server = http.createServer((req, res) => {
|
|
1324
|
-
const result = guard.consume(req.url, req.method || "GET");
|
|
1325
|
-
const headers = {
|
|
1326
|
-
"content-type": result.handled ? "text/html; charset=utf-8" : "text/plain; charset=utf-8",
|
|
1327
|
-
"cache-control": "no-store",
|
|
1328
|
-
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'",
|
|
1329
|
-
"x-content-type-options": "nosniff",
|
|
1330
|
-
connection: "close",
|
|
1331
|
-
};
|
|
1332
|
-
if (result.statusCode === 405) headers.allow = "GET";
|
|
1333
|
-
res.writeHead(result.statusCode, headers);
|
|
1334
|
-
res.end(result.handled ? loginCallbackHtml(result.ok) : result.message);
|
|
1335
|
-
if (!result.final) return;
|
|
1336
|
-
if (result.ok) finish(null, result.value);
|
|
1337
|
-
else finish(new Error(result.message));
|
|
1338
|
-
});
|
|
1339
|
-
server.on("error", (error) => finish(error));
|
|
1340
|
-
server.listen(0, "127.0.0.1", () => {
|
|
1341
|
-
const address = server.address();
|
|
1342
|
-
const port = address && typeof address === "object" ? address.port : 0;
|
|
1343
|
-
if (!port) {
|
|
1344
|
-
finish(new Error("Could not open the login loopback port."));
|
|
1345
|
-
return;
|
|
1346
|
-
}
|
|
1347
|
-
const callback = new URL(`http://127.0.0.1:${port}${LOGIN_CALLBACK_PATH}`);
|
|
1348
|
-
callback.searchParams.set("state", state);
|
|
1349
|
-
let loginUrl;
|
|
1350
|
-
try {
|
|
1351
|
-
loginUrl = new URL("/account", `${options.baseUrl || webBaseUrl()}/`);
|
|
1352
|
-
} catch {
|
|
1353
|
-
finish(new Error("The Agentlas login URL is invalid."));
|
|
1354
|
-
return;
|
|
1355
|
-
}
|
|
1356
|
-
loginUrl.searchParams.set("desktop", "1");
|
|
1357
|
-
loginUrl.searchParams.set("callback", callback.toString());
|
|
1358
|
-
timer = setTimeout(
|
|
1359
|
-
() => finish(new Error(`Login timed out after ${Math.ceil(timeoutMs / 1000)} seconds. Try: agentlas login`)),
|
|
1360
|
-
timeoutMs,
|
|
1361
|
-
);
|
|
1362
|
-
if (timer.unref) timer.unref();
|
|
1363
|
-
try {
|
|
1364
|
-
const notified = onLoginUrl(loginUrl.toString());
|
|
1365
|
-
if (notified && typeof notified.then === "function") {
|
|
1366
|
-
void notified.catch((error) => finish(error));
|
|
1367
|
-
}
|
|
1368
|
-
} catch (error) {
|
|
1369
|
-
finish(error);
|
|
1370
|
-
}
|
|
1371
|
-
});
|
|
1372
|
-
});
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
async function cmdWhoami() {
|
|
1376
|
-
const cookie = await D.cloudSessionCookieCli();
|
|
1377
|
-
if (!cookie) {
|
|
1378
|
-
D.out("You are signed out. Sign in with agentlas login.");
|
|
1379
|
-
process.exitCode = 1;
|
|
1380
|
-
return;
|
|
1381
|
-
}
|
|
1382
|
-
try {
|
|
1383
|
-
const j = await fetchSessionMeta(cookie);
|
|
1384
|
-
if (j && j.authenticated) {
|
|
1385
|
-
const email = (j.user && j.user.email) || "?";
|
|
1386
|
-
const ws = j.workspace || {};
|
|
1387
|
-
D.out(`Signed in: ${email} · workspace: ${ws.name || "?"} (${ws.plan || "free"})`);
|
|
1388
|
-
} else {
|
|
1389
|
-
D.out("The session is expired or invalid. Sign in again with agentlas login.");
|
|
1390
|
-
process.exitCode = 1;
|
|
1391
|
-
}
|
|
1392
|
-
} catch (e) {
|
|
1393
|
-
D.fail("Session check failed: " + String((e && e.message) || e));
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
// 웹 /account?desktop=1&callback=<loopback+state> 이 유효 세션이면 callback의 state를
|
|
1398
|
-
// 보존한 채 session을 추가해 302한다. Terminal은 state를 1회 검증한 뒤에만 저장한다.
|
|
1399
|
-
async function cmdLogin(args = []) {
|
|
1400
|
-
const force = args.includes("--force");
|
|
1401
|
-
if (!force) {
|
|
1402
|
-
const existing = await D.cloudSessionCookieCli();
|
|
1403
|
-
if (existing) {
|
|
1404
|
-
try {
|
|
1405
|
-
const j = await fetchSessionMeta(existing);
|
|
1406
|
-
if (j && j.authenticated) {
|
|
1407
|
-
D.out(`Already signed in (${(j.user && j.user.email) || "?"}). Re-authenticate with agentlas login --force`);
|
|
1408
|
-
return;
|
|
1409
|
-
}
|
|
1410
|
-
} catch { /* 확인 실패 — 새로 로그인 진행 */ }
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
let value;
|
|
1415
|
-
try {
|
|
1416
|
-
value = await waitForLoopbackSession();
|
|
1417
|
-
} catch (e) {
|
|
1418
|
-
return D.fail(String((e && e.message) || e));
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
const p = D.cliSessionPath();
|
|
1422
|
-
fs.mkdirSync(path.dirname(p), { recursive: true, mode: 0o700 });
|
|
1423
|
-
fs.writeFileSync(p, JSON.stringify({ version: 1, value, updatedAt: new Date().toISOString() }, null, 2) + "\n", { mode: 0o600 });
|
|
1424
|
-
try { fs.chmodSync(p, 0o600); } catch { /* win32 */ }
|
|
1425
|
-
D.out(`Session saved: ${p}`);
|
|
1426
|
-
await cmdWhoami();
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
function cmdLogout() {
|
|
1430
|
-
const p = D.cliSessionPath();
|
|
1431
|
-
if (fs.existsSync(p)) {
|
|
1432
|
-
try { fs.rmSync(p); D.out("Signed out (CLI session deleted)."); } catch (e) { return D.fail("Could not delete the session file: " + e.message); }
|
|
1433
|
-
} else {
|
|
1434
|
-
D.out("No saved CLI session.");
|
|
1435
|
-
}
|
|
1436
|
-
if (process.env.AGENTLAS_SESSION) D.out("Warning: AGENTLAS_SESSION is still set, so the CLI may appear signed in.");
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
// ── cloud search — 마켓플레이스 검색 ──
|
|
1440
|
-
async function cloudSearch(db, args) {
|
|
1441
|
-
let limit = 10;
|
|
1442
|
-
const rest = [];
|
|
1443
|
-
for (let i = 0; i < args.length; i++) {
|
|
1444
|
-
if (args[i] === "--limit") limit = Math.max(1, Math.min(30, Number(args[++i]) || 10));
|
|
1445
|
-
else rest.push(args[i]);
|
|
1446
|
-
}
|
|
1447
|
-
const query = rest.join(" ").trim();
|
|
1448
|
-
if (!query) return D.fail('usage: agentlas cloud search "<task>" [--limit 10]');
|
|
1449
|
-
if (typeof fetch !== "function") return D.fail("fetch is not available in this runtime.");
|
|
1450
|
-
const base = process.env.AGENTLAS_MCP_BASE_URL || "https://agentlas.cloud/api/mcp/v1";
|
|
1451
|
-
const headers = { "content-type": "application/json" };
|
|
1452
|
-
try {
|
|
1453
|
-
const cookie = await D.cloudSessionCookieCli();
|
|
1454
|
-
if (cookie) headers.cookie = cookie;
|
|
1455
|
-
} catch { /* 로그인 없어도 검색은 가능 */ }
|
|
1456
|
-
let resp;
|
|
1457
|
-
try {
|
|
1458
|
-
resp = await fetch(`${base.replace(/\/$/, "")}/tools/call`, {
|
|
1459
|
-
method: "POST",
|
|
1460
|
-
headers,
|
|
1461
|
-
body: JSON.stringify({
|
|
1462
|
-
method: "marketplace.search_agents",
|
|
1463
|
-
// Hub는 파라미터 이름이 `q` — `query`만 보내면 무시하고 기본 목록을 준다(데스크탑 mcp-source.ts와 동일하게 둘 다 전송).
|
|
1464
|
-
params: { name: "marketplace.search_agents", arguments: { q: query, query, limit } },
|
|
1465
|
-
}),
|
|
1466
|
-
});
|
|
1467
|
-
} catch (e) {
|
|
1468
|
-
return D.fail(`Marketplace connection failed: ${(e && e.message) || e}`);
|
|
1469
|
-
}
|
|
1470
|
-
if (!resp.ok) return D.fail(`Marketplace returned ${resp.status}`);
|
|
1471
|
-
const json = await resp.json();
|
|
1472
|
-
if (json.error) return D.fail(json.error.message || "marketplace error");
|
|
1473
|
-
const result = json.result || {};
|
|
1474
|
-
const rawItems = result.results || result.agents || result.items || (Array.isArray(result) ? result : null);
|
|
1475
|
-
// 엔진 내부 에이전트(researcher-<n>, research-intelligence-desk, hephaestus-*)는 제품이 아니므로 숨긴다.
|
|
1476
|
-
const items = Array.isArray(rawItems) ? rawItems.filter((it) => !isInternalAgentSlug(it && (it.slug || it.id))) : rawItems;
|
|
1477
|
-
if (!Array.isArray(items) || !items.length) {
|
|
1478
|
-
D.out(`No results for "${query}"`);
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
for (const it of items.slice(0, limit)) {
|
|
1482
|
-
const slug = it.slug || it.id || "?";
|
|
1483
|
-
const name = it.name || it.title || slug;
|
|
1484
|
-
const kind = it.kind || it.entity_kind || "";
|
|
1485
|
-
const tagline = it.tagline || it.description || "";
|
|
1486
|
-
D.out(`${String(slug).padEnd(34).slice(0, 34)} ${String(name).slice(0, 26).padEnd(27)} ${String(kind).padEnd(14)} ${String(tagline).slice(0, 60)}`);
|
|
1487
|
-
}
|
|
1488
|
-
D.out("");
|
|
1489
|
-
D.out("Install: agentlas install <slug>");
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
return {
|
|
1493
|
-
cmdStorm, stormRun, cmdSwarm, swarmRun, cmdAutomation, cmdUsage, cmdTelegram, cloudSearch,
|
|
1494
|
-
cmdLogin, cmdLogout, cmdWhoami, cmdHep, runHephaestusInteractive, cmdMcp, cmdChats,
|
|
1495
|
-
nextCronRun, nextAutomationRun, runAutomationOnce, parseSwarmOutput, waitForLoopbackSession,
|
|
1496
|
-
};
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
module.exports = { create, _test: { createLoginState, createLoginCallbackGuard } };
|