agentlas 1.0.29 → 1.0.36
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,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/* telegram — 바인딩 현황 (읽기 전용). 페어링·봇 발급은 Desktop Connect 소관. */
|
|
2
|
+
/* telegram — 바인딩 현황 (읽기 전용). 페어링·봇 발급은 아직 Desktop Connect 소관. */
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/*
|
|
5
|
+
* 바인딩 표를 그린다. connect.cjs 도 같은 표를 쓰도록 공용화(2026-08-06):
|
|
6
|
+
* 예전 `connect telegram` 은 Hephaestus 플러그인으로 패스스루해 원시 라우터
|
|
7
|
+
* JSON(hub_candidates)을 사용자에게 그대로 덤프했다 — build 스텁과 같은 계열.
|
|
8
|
+
*/
|
|
9
|
+
function renderTelegram(ctx) {
|
|
5
10
|
const ko = ctx.lang === "ko";
|
|
6
11
|
const db = ctx.db();
|
|
7
12
|
let rows;
|
|
@@ -11,22 +16,24 @@ function run(ctx) {
|
|
|
11
16
|
rows = [];
|
|
12
17
|
}
|
|
13
18
|
if (!rows.length) {
|
|
14
|
-
ctx.out(ko
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
ctx.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}`);
|
|
19
|
+
ctx.out(ko ? "Telegram 연결이 없습니다." : "No Telegram bindings.");
|
|
20
|
+
} else {
|
|
21
|
+
for (const r of rows) {
|
|
22
|
+
const bot = r.bot_username ? "@" + r.bot_username : "(bot not set)";
|
|
23
|
+
const chat = r.telegram_chat_title || r.telegram_chat_id || "(chat not connected)";
|
|
24
|
+
const status = r.status || (r.telegram_chat_id ? "paired" : "pending");
|
|
25
|
+
ctx.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}`);
|
|
26
|
+
}
|
|
27
|
+
ctx.out("");
|
|
24
28
|
}
|
|
25
|
-
ctx.out("");
|
|
26
29
|
ctx.out(ctx.ui.dim(ko
|
|
27
|
-
? "
|
|
28
|
-
: "
|
|
30
|
+
? "봇 발급·페어링은 현재 Agentlas Desktop Connect에서 수행합니다. 이 명령은 바인딩 상태를 보여줍니다."
|
|
31
|
+
: "Bot issuance and pairing currently happen in Agentlas Desktop Connect. This command shows binding status."));
|
|
29
32
|
return 0;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
function run(ctx) {
|
|
36
|
+
return renderTelegram(ctx);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { run, renderTelegram };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* core/desktop-core-fetch — 데스크탑 코어를 필요할 때만 내려받는다 (2026-08-06).
|
|
4
|
+
*
|
|
5
|
+
* 배경(오너: "코덱스는 어떻게 했냐"): 코덱스 CLI는 무거운 실행파일을 npm 패키지 안에 미리
|
|
6
|
+
* 담지 않는다 — 설치 시점(postinstall)에 그 사람 플랫폼용 파일만 GitHub Release 에서 내려받는다.
|
|
7
|
+
* 우리도 같은 패턴을 쓴다: 그래프 실행 커널(52MB, engine/vendor/desktop-core.cjs 가 재사용하는
|
|
8
|
+
* 데스크탑 코어)은 git 에 커밋하지 않고, `graph run` 을 실제로 쓸 때만 GitHub Release 자산을
|
|
9
|
+
* 내려받아 로컬 캐시(userDataDir()/desktop-core-cache/<version>/)에 푼다. 다음부턴 캐시를 쓴다.
|
|
10
|
+
*
|
|
11
|
+
* 안전: 무엇을 왜 받는지 사용자에게 **말하고** 받는다(조용히 안 받는다). sha256 체크섬으로
|
|
12
|
+
* 무결성을 확인한 뒤에만 푼다 — 받아온 걸 검증 없이 실행하지 않는다.
|
|
13
|
+
*
|
|
14
|
+
* 매니페스트(engine/vendor/desktop-core.manifest.json, git 커밋 — 이 파일만 작다)가
|
|
15
|
+
* {version, url, sha256, sizeBytes} 를 담는다. 실물(52MB)은 그 url 이 가리키는 곳에서 온다.
|
|
16
|
+
*/
|
|
17
|
+
const crypto = require("node:crypto");
|
|
18
|
+
const fs = require("node:fs");
|
|
19
|
+
const path = require("node:path");
|
|
20
|
+
const { spawnSync } = require("node:child_process");
|
|
21
|
+
const { userDataDir } = require("./paths.cjs");
|
|
22
|
+
|
|
23
|
+
// env 오버라이드는 테스트 전용(게이트가 로컬 서버를 가리키는 가짜 매니페스트로 전 경로를 잠근다).
|
|
24
|
+
function manifestPath() { return process.env.AGENTLAS_DESKTOP_CORE_MANIFEST || path.join(__dirname, "..", "vendor", "desktop-core.manifest.json"); }
|
|
25
|
+
|
|
26
|
+
function readManifest() {
|
|
27
|
+
try { return JSON.parse(fs.readFileSync(manifestPath(), "utf8")); } catch { return null; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cacheDir(version) { return path.join(userDataDir(), "desktop-core-cache", String(version)); }
|
|
31
|
+
function cacheDistDir(version) { return path.join(cacheDir(version), "dist"); }
|
|
32
|
+
|
|
33
|
+
function sha256File(file) {
|
|
34
|
+
const hash = crypto.createHash("sha256");
|
|
35
|
+
hash.update(fs.readFileSync(file));
|
|
36
|
+
return hash.digest("hex");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 이미 캐시에 온전히 풀려 있으면 그 dist 경로, 아니면 null. */
|
|
40
|
+
function cachedCoreRoot(manifest = readManifest()) {
|
|
41
|
+
if (!manifest) return null;
|
|
42
|
+
const dist = cacheDistDir(manifest.version);
|
|
43
|
+
const marker = path.join(cacheDir(manifest.version), ".complete");
|
|
44
|
+
if (fs.existsSync(marker) && fs.existsSync(path.join(dist, "electron", "workflow", "run-graph.js"))) return dist;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 매니페스트가 가리키는 데스크탑 코어를 내려받아 캐시에 푼다.
|
|
50
|
+
* onNotice(text): 사용자에게 보여줄 안내(무엇을·왜 받는지) — 조용히 받지 않는다.
|
|
51
|
+
* 반환: 성공 시 dist 경로, 실패 시 null(호출부가 정직하게 멈춘다).
|
|
52
|
+
*/
|
|
53
|
+
async function fetchDesktopCore({ onNotice } = {}) {
|
|
54
|
+
const manifest = readManifest();
|
|
55
|
+
if (!manifest || !manifest.url || !manifest.sha256) return null;
|
|
56
|
+
|
|
57
|
+
const existing = cachedCoreRoot(manifest);
|
|
58
|
+
if (existing) return existing;
|
|
59
|
+
|
|
60
|
+
const say = (t) => { if (typeof onNotice === "function") onNotice(t); };
|
|
61
|
+
say(`Downloading the graph-execution engine (${manifest.sizeBytes ? Math.round(manifest.sizeBytes / 1024 / 1024) + " MB" : "one-time"}) from ${manifest.url} …`);
|
|
62
|
+
|
|
63
|
+
const dir = cacheDir(manifest.version);
|
|
64
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
65
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
66
|
+
const tarPath = path.join(dir, "desktop-core.tar.gz");
|
|
67
|
+
|
|
68
|
+
const res = await fetch(manifest.url);
|
|
69
|
+
if (!res.ok) { say(`Download failed: HTTP ${res.status}`); return null; }
|
|
70
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
71
|
+
fs.writeFileSync(tarPath, buf);
|
|
72
|
+
|
|
73
|
+
const digest = sha256File(tarPath);
|
|
74
|
+
if (digest !== manifest.sha256) {
|
|
75
|
+
say(`Checksum mismatch (expected ${manifest.sha256.slice(0, 12)}…, got ${digest.slice(0, 12)}…) — refusing to use it.`);
|
|
76
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const extract = spawnSync("tar", ["-xzf", tarPath, "-C", dir], { encoding: "utf8" });
|
|
81
|
+
if (extract.status !== 0) {
|
|
82
|
+
say(`Extraction failed: ${extract.stderr || extract.error || "unknown error"}`);
|
|
83
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
fs.rmSync(tarPath);
|
|
87
|
+
|
|
88
|
+
if (!fs.existsSync(path.join(cacheDistDir(manifest.version), "electron", "workflow", "run-graph.js"))) {
|
|
89
|
+
say("Downloaded archive did not contain the expected engine files.");
|
|
90
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
fs.writeFileSync(path.join(dir, ".complete"), new Date().toISOString());
|
|
94
|
+
say("Engine ready.");
|
|
95
|
+
return cacheDistDir(manifest.version);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { readManifest, cachedCoreRoot, fetchDesktopCore, cacheDistDir };
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* core/desktop-core — 데스크탑의 이미-컴파일된 코어를 터미널이 **재사용**한다 (2026-08-06).
|
|
4
|
+
*
|
|
5
|
+
* 배경(오너: "더 어려운 Electron 을 만들었는데 왜 더 쉬운 터미널을 못 만드냐 — 그래프 하나
|
|
6
|
+
* 문제가 아니다"): 정상 패턴은 CLI 를 먼저 만들고 그 위에 GUI 를 얹는 것이다. Agentlas 는
|
|
7
|
+
* 거꾸로 갔고, 터미널이 데스크탑 엔진을 손으로 재구현한 별도 사본이라 늘 뒤처졌다. 근본
|
|
8
|
+
* 수리는 기능을 하나씩 베끼는 게 아니라 **데스크탑의 컴파일된 코어를 그대로 불러 쓰는 것**이다.
|
|
9
|
+
*
|
|
10
|
+
* 그 코어(electron/workflow/run-graph.js + electron/mcp/* + electron/store/*)는 Electron 의존이
|
|
11
|
+
* 0이라(실측: `from "electron"`·BrowserWindow·app 전무, store/db 의 유일한 app.getPath 는
|
|
12
|
+
* AGENTLAS_STORE_PATH 로 이미 오버라이드 가능) 순수 Node 에서 그대로 require 된다.
|
|
13
|
+
*
|
|
14
|
+
* 이 로더는 컴파일된 코어의 위치를 찾아, **공유 DB 경로를 코어가 읽기 전에** 세팅하고 로드한다.
|
|
15
|
+
* 찾지 못하면 null 을 준다 — 호출부가 정직하게 멈춘다(가짜 성공 금지).
|
|
16
|
+
*
|
|
17
|
+
* 해석 순서:
|
|
18
|
+
* 1. env AGENTLAS_DESKTOP_CORE — 코어 루트(…/dist/electron 를 담는 디렉터리, 즉 dist)
|
|
19
|
+
* 2. 벤더본: <pkg>/engine/vendor/desktop-core/dist (pack 시 복사되는 자립본)
|
|
20
|
+
* 3. 개발(동일 저장소 체크아웃): <pkg>/../agentlas_desktop/dist
|
|
21
|
+
*/
|
|
22
|
+
const fs = require("node:fs");
|
|
23
|
+
const os = require("node:os");
|
|
24
|
+
const path = require("node:path");
|
|
25
|
+
const { dbPath, packageRoot, userDataDir } = require("./paths.cjs");
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* 재사용 코어를 electron 없이 돌리는 셰임(2026-08-06).
|
|
29
|
+
* 재사용 클로저(200파일)의 electron 사용은 거의 전부 app.getPath(...) — 디렉터리 해석뿐이고,
|
|
30
|
+
* 나머지(BrowserWindow·dialog·session)는 헤드리스 실행 경로에서 쓰이지 않는다. 그래서 작은
|
|
31
|
+
* 가짜 electron 을 require 훅으로 돌려주면, electron 도 데스크탑 앱도 설치하지 않은 순수 Node
|
|
32
|
+
* 에서 데스크탑 코어가 그대로 돈다. GUI 전용 심볼은 실제로 불릴 때만 "CLI 에선 불가"로 던진다
|
|
33
|
+
* (조용히 no-op 하지 않는다 — 가짜 성공 금지).
|
|
34
|
+
*/
|
|
35
|
+
function makeElectronShim() {
|
|
36
|
+
const base = userDataDir();
|
|
37
|
+
const paths = {
|
|
38
|
+
userData: base,
|
|
39
|
+
appData: path.dirname(base),
|
|
40
|
+
home: os.homedir(),
|
|
41
|
+
temp: os.tmpdir(),
|
|
42
|
+
downloads: path.join(os.homedir(), "Downloads"),
|
|
43
|
+
documents: path.join(os.homedir(), "Documents"),
|
|
44
|
+
logs: path.join(base, "logs"),
|
|
45
|
+
sessionData: base,
|
|
46
|
+
exe: process.execPath,
|
|
47
|
+
module: process.execPath,
|
|
48
|
+
};
|
|
49
|
+
const app = {
|
|
50
|
+
getPath: (name) => paths[name] || base,
|
|
51
|
+
getName: () => "agentlas",
|
|
52
|
+
getVersion: () => { try { return require(path.join(packageRoot(), "package.json")).version; } catch { return "0.0.0"; } },
|
|
53
|
+
getLocale: () => "en-US",
|
|
54
|
+
isReady: () => true,
|
|
55
|
+
whenReady: () => Promise.resolve(),
|
|
56
|
+
isPackaged: true,
|
|
57
|
+
on() { return app; }, once() { return app; }, off() { return app; },
|
|
58
|
+
quit() {}, exit() {}, setPath() {},
|
|
59
|
+
};
|
|
60
|
+
const notAvailable = (what) => () => { throw new Error(`electron.${what} is not available in the Agentlas CLI (headless).`); };
|
|
61
|
+
const safeStorage = { isEncryptionAvailable: () => false, encryptString: notAvailable("safeStorage.encryptString"), decryptString: notAvailable("safeStorage.decryptString") };
|
|
62
|
+
class Notification { constructor() {} show() {} close() {} static isSupported() { return false; } }
|
|
63
|
+
const gui = new Proxy({}, { get: (_t, prop) => notAvailable(String(prop)) });
|
|
64
|
+
return { app, safeStorage, Notification, BrowserWindow: gui, dialog: gui, shell: gui, session: gui, ipcMain: gui, nativeTheme: {}, powerMonitor: gui };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function candidateRoots() {
|
|
68
|
+
const roots = [];
|
|
69
|
+
if (process.env.AGENTLAS_DESKTOP_CORE) roots.push(process.env.AGENTLAS_DESKTOP_CORE);
|
|
70
|
+
roots.push(path.join(packageRoot(), "engine", "vendor", "desktop-core", "dist"));
|
|
71
|
+
// 코덱스 CLI 패턴: 무거운 실행부는 git/npm 패키지에 미리 담지 않고, 처음 필요할 때만 내려받아
|
|
72
|
+
// 로컬에 캐시한다(userDataDir 아래) — 다음부턴 이 경로가 바로 잡힌다.
|
|
73
|
+
try { const cached = require("./desktop-core-fetch.cjs").cachedCoreRoot(); if (cached) roots.push(cached); } catch { /* 매니페스트 없음 */ }
|
|
74
|
+
roots.push(path.resolve(packageRoot(), "..", "agentlas_desktop", "dist"));
|
|
75
|
+
return roots;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 컴파일된 코어의 dist 루트를 찾는다(run-graph.js 존재로 판정). 없으면 null. */
|
|
79
|
+
function findCoreRoot() {
|
|
80
|
+
for (const root of candidateRoots()) {
|
|
81
|
+
try {
|
|
82
|
+
if (fs.existsSync(path.join(root, "electron", "workflow", "run-graph.js"))) return root;
|
|
83
|
+
} catch { /* 다음 후보 */ }
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* 재사용 코어가 네이티브 모듈(better-sqlite3·keytar)을 **터미널 것**으로 쓰게 한다.
|
|
90
|
+
* 데스크탑 node_modules 의 것은 Electron ABI 로 빌드돼 순수 Node 와 호환되지 않는다
|
|
91
|
+
* (NODE_MODULE_VERSION 불일치). require 를 한 번 가로채 터미널의 바인딩으로 돌린다 — 같은
|
|
92
|
+
* 패키지의 ABI 만 맞추는 것이라 안전. ★다운로드 캐시(userDataDir 아래)는 터미널 패키지 트리
|
|
93
|
+
* 밖이라 node_modules 디렉터리 walk-up으로 자연 해결되지 않는다 — 이 훅이 위치와 무관하게 잡는다.
|
|
94
|
+
*/
|
|
95
|
+
let _nativeHookInstalled = false;
|
|
96
|
+
function installNativeModuleHook() {
|
|
97
|
+
if (_nativeHookInstalled) return;
|
|
98
|
+
const Module = require("node:module");
|
|
99
|
+
const terminalBetterSqlite3 = require("better-sqlite3"); // 터미널 ABI 로 빌드된 것
|
|
100
|
+
let terminalKeytar = null;
|
|
101
|
+
try { terminalKeytar = require("keytar"); } catch { /* optionalDependency — 없을 수 있다 */ }
|
|
102
|
+
const electronShim = makeElectronShim(); // electron 없이 코어를 돌리는 셰임
|
|
103
|
+
const orig = Module._load;
|
|
104
|
+
Module._load = function (request, parent, isMain) {
|
|
105
|
+
if (request === "better-sqlite3") return terminalBetterSqlite3;
|
|
106
|
+
if (request === "keytar" && terminalKeytar) return terminalKeytar;
|
|
107
|
+
if (request === "electron") return electronShim;
|
|
108
|
+
return orig.apply(this, arguments);
|
|
109
|
+
};
|
|
110
|
+
_nativeHookInstalled = true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let _cache = undefined;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 재사용 코어를 로드한다. 반환: { root, require(rel), runGraph, ... } 또는 null.
|
|
117
|
+
* · require(rel): 코어 안의 임의 컴파일 모듈을 상대경로로 로드(예: "store/automations").
|
|
118
|
+
* · runGraph: electron/workflow/run-graph.js 의 runGraph(automation, graph, opts).
|
|
119
|
+
* 공유 DB(AGENTLAS_STORE_PATH)를 코어 로드 전에 반드시 세팅한다 — store/db 가 모듈 로드 시 읽는다.
|
|
120
|
+
*/
|
|
121
|
+
function loadDesktopCore() {
|
|
122
|
+
if (_cache !== undefined) return _cache;
|
|
123
|
+
const root = findCoreRoot();
|
|
124
|
+
if (!root) { _cache = null; return null; }
|
|
125
|
+
installNativeModuleHook();
|
|
126
|
+
// 코어의 store 가 이 값을 모듈 로드 시점에 읽는다 — require 이전에 세팅해야 한다.
|
|
127
|
+
if (!process.env.AGENTLAS_STORE_PATH) process.env.AGENTLAS_STORE_PATH = dbPath();
|
|
128
|
+
const req = (rel) => require(path.join(root, "electron", rel.replace(/\.js$/, "") + ".js"));
|
|
129
|
+
let kernel;
|
|
130
|
+
try {
|
|
131
|
+
// 데스크탑은 app.whenReady 에서 initStore() 를 부른다 — 터미널도 코어를 쓰기 전에 부른다.
|
|
132
|
+
// initStore 가 스키마 마이그레이션까지 소유하므로(데스크탑과 같은 정본 스키마) 공유 DB 는
|
|
133
|
+
// 언제나 코어가 기대하는 모양이 된다. 이것이 "재구현"이 아니라 "재사용"의 핵심.
|
|
134
|
+
req("store/db").initStore();
|
|
135
|
+
kernel = req("workflow/run-graph");
|
|
136
|
+
} catch (e) { _cache = { root, error: e }; return _cache; }
|
|
137
|
+
_cache = {
|
|
138
|
+
root,
|
|
139
|
+
require: req,
|
|
140
|
+
initStore: req("store/db").initStore,
|
|
141
|
+
getDb: req("store/db").getDb,
|
|
142
|
+
runGraph: kernel.runGraph,
|
|
143
|
+
graphFailureOf: kernel.graphFailureOf,
|
|
144
|
+
planGraphLoops: kernel.planGraphLoops,
|
|
145
|
+
};
|
|
146
|
+
return _cache;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 재사용 코어가 이 머신에서 가용한가(정직한 가부). */
|
|
150
|
+
function desktopCoreAvailable() {
|
|
151
|
+
const c = loadDesktopCore();
|
|
152
|
+
return !!(c && c.runGraph && !c.error);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 로컬에서 코어를 못 찾으면(개발용 형제 저장소도, 캐시도 없음) 필요한 순간에만 내려받는다
|
|
157
|
+
* (코덱스 CLI 패턴 — 조용히 안 받는다, onNotice 로 사용자에게 알린다). 매니페스트가 없으면
|
|
158
|
+
* (그래프 실행 기능이 이 배포에 아직 안 실렸으면) null 을 준다 — 정직한 실패.
|
|
159
|
+
*/
|
|
160
|
+
async function loadDesktopCoreAsync({ onNotice } = {}) {
|
|
161
|
+
const sync = loadDesktopCore();
|
|
162
|
+
if (sync && sync.runGraph && !sync.error) return sync;
|
|
163
|
+
const { fetchDesktopCore } = require("./desktop-core-fetch.cjs");
|
|
164
|
+
const dist = await fetchDesktopCore({ onNotice });
|
|
165
|
+
if (!dist) return null;
|
|
166
|
+
_cache = undefined; // 방금 캐시가 채워졌으니 후보 목록을 다시 계산해 태운다.
|
|
167
|
+
return loadDesktopCore();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { findCoreRoot, loadDesktopCore, loadDesktopCoreAsync, desktopCoreAvailable };
|
|
@@ -72,8 +72,13 @@ async function askModel(ctx, prompt, opts = {}) {
|
|
|
72
72
|
if (!opts.runtime) {
|
|
73
73
|
// listAvailableCliRuntimes()는 문자열이 아니라 {kind, bin, path} 객체를 돌려준다.
|
|
74
74
|
// 문자열로 읽으면 후보가 하나도 안 쌓여 폴백이 통째로 죽는다(실측).
|
|
75
|
+
// ★그리고 **실행 드라이버가 있는 종류만** 후보다 — detect는 grok/kimi/cursor도
|
|
76
|
+
// 광고하는데 native host에 드라이버가 없어, 후보 슬롯 하나가 "unknown runtime"으로
|
|
77
|
+
// 낭비됐다(실측 2026-08-06). 판별 기준은 목록 복제가 아니라 resolve의 정본을 쓴다.
|
|
78
|
+
const { CLI_EXECUTABLE_KINDS } = require("../runtimes/resolve.cjs");
|
|
75
79
|
for (const found of listAvailableCliRuntimes()) {
|
|
76
80
|
if (!found || !found.kind || !found.path) continue;
|
|
81
|
+
if (!CLI_EXECUTABLE_KINDS.has(found.kind)) continue;
|
|
77
82
|
if (candidates.some((c) => c.kind === found.kind)) continue;
|
|
78
83
|
candidates.push({ kind: found.kind, bin: found.path });
|
|
79
84
|
}
|
|
@@ -110,7 +115,29 @@ async function askModel(ctx, prompt, opts = {}) {
|
|
|
110
115
|
failures.push(`${runtime.kind}: ${(err && err.message) || err}`);
|
|
111
116
|
continue;
|
|
112
117
|
}
|
|
113
|
-
const text = String((res &&
|
|
118
|
+
const text = String((res && res.text) || "");
|
|
119
|
+
/*
|
|
120
|
+
* ★런타임이 실패를 **이미 표식으로 말하고 있었다** — 우리가 안 읽었을 뿐이다.
|
|
121
|
+
*
|
|
122
|
+
* 실측 2026-08-06: claude-code가 한도 소진 시 `--output-format stream-json`으로
|
|
123
|
+
* `{"type":"rate_limit_event",...,"status":"rejected"}` → `"error":"rate_limit"` →
|
|
124
|
+
* 최종 `{"is_error":true,"api_error_status":429}`를 보내고 종료코드 1로 끝난다.
|
|
125
|
+
* 네이티브 호스트는 그것을 읽어 `res.error`까지 정확히 채워 놓았다. 그런데 여기서
|
|
126
|
+
* **`text.trim()`을 먼저 보고 ok를 반환**하는 바람에 그 error는 한 번도 읽히지 않았고,
|
|
127
|
+
* `You've hit your weekly limit`이라는 사람용 문장이 산출물 행세를 했다. 살아 있는
|
|
128
|
+
* codex로 넘어가지도 않았다.
|
|
129
|
+
*
|
|
130
|
+
* 그래서 판별은 문구 추측이 아니라 **표식**으로 한다. 표식이 있으면 그 런타임은 실패다.
|
|
131
|
+
* (표식조차 없는 런타임을 위한 마지막 그물은 runtime-refusal.cjs 한 곳뿐이다.)
|
|
132
|
+
*/
|
|
133
|
+
const marked = res && res.error ? String(res.error).replace(/\s+/g, " ").slice(0, 240) : null;
|
|
134
|
+
const { detectRuntimeRefusal } = require("../runtime-refusal.cjs");
|
|
135
|
+
const heur = marked ? null : detectRuntimeRefusal(text);
|
|
136
|
+
const notice = marked || (heur && heur.message);
|
|
137
|
+
if (notice) {
|
|
138
|
+
failures.push(`${runtime.kind}: ${notice}`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
114
141
|
if (text.trim()) {
|
|
115
142
|
const out = { ok: true, text, runtime: runtime.kind };
|
|
116
143
|
if (candidates[0] !== runtime) out.fellBackFrom = candidates[0].kind;
|
|
@@ -128,4 +155,5 @@ async function askModel(ctx, prompt, opts = {}) {
|
|
|
128
155
|
};
|
|
129
156
|
}
|
|
130
157
|
|
|
158
|
+
|
|
131
159
|
module.exports = { askModel };
|
|
@@ -62,8 +62,10 @@ const OPS = new Set(CONDITION_OPS);
|
|
|
62
62
|
const VALUE_OPS = new Set(["contains", "eq", "ne", "gt", "lt"]);
|
|
63
63
|
const VAR_RE = /^[A-Za-z_][\w-]*$/;
|
|
64
64
|
|
|
65
|
-
function startInterview(request) {
|
|
66
|
-
|
|
65
|
+
function startInterview(request, { knownGraphs = [], selfId = null } = {}) {
|
|
66
|
+
// knownGraphs: 이 사람이 이미 가진 저장된 자동화들(id·name). runGraph 단계가 이 중에서만
|
|
67
|
+
// 고르게 하려고 인터뷰 프롬프트와 검증에 함께 흐른다(데스크탑과 같은 규칙).
|
|
68
|
+
return { request: String(request || "").trim(), answers: [], asked: [], round: 0, attempts: [], knownGraphs, selfId };
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
function recordAnswers(state, answers) {
|
|
@@ -133,6 +135,11 @@ const RULES = [
|
|
|
133
135
|
" If the script imports anything outside the Python standard library, declare the pip names in",
|
|
134
136
|
' packages:["yfinance"] on that step — the product installs them before the run. Prefer the',
|
|
135
137
|
" standard library when it can do the job; an undeclared import dies on the user's machine.",
|
|
138
|
+
" · kind:\"runGraph\" when the person wants an automation they ALREADY have to run as one",
|
|
139
|
+
" step of this one (\"then run my weekly report\"). Add graphRef:\"<id>\" chosen from the",
|
|
140
|
+
" list of saved automations at the end of this prompt — never invent an id, and never use",
|
|
141
|
+
" the name (names change, ids do not). If nothing in that list matches, do not guess:",
|
|
142
|
+
" write the work as ordinary steps instead.",
|
|
136
143
|
" · kind:\"agent\" (the default, omit it) for judgement, writing, summarizing, deciding — anything",
|
|
137
144
|
" where being approximately right is fine. Split a step: fetch+compute in a code step, then",
|
|
138
145
|
" judge/write in an agent step. Do not put exact math inside an agent instruction.",
|
|
@@ -203,6 +210,11 @@ function buildInterviewPrompt(state, locale = "ko") {
|
|
|
203
210
|
if (state.asked.length) {
|
|
204
211
|
lines.push("", `Question ids already asked (do not repeat): ${state.asked.join(", ")}`);
|
|
205
212
|
}
|
|
213
|
+
// ★부를 수 있는 자동화 목록을 그 순간 실물로 싣는다(데스크탑과 같은 규칙). 이것이 없으면
|
|
214
|
+
// 모델은 id를 지어내고, 그 그래프는 실행 때 죽는다.
|
|
215
|
+
if (state.knownGraphs && state.knownGraphs.length) {
|
|
216
|
+
lines.push("", "Saved automations you may call with kind:\"runGraph\" (use the id exactly):", ...state.knownGraphs.slice(0, 40).map((g) => ` ${g.id} — ${g.name}`));
|
|
217
|
+
}
|
|
206
218
|
// ★산출 언어는 입력 언어가 아니라 **제품 설정**이 정한다(데스크탑과 같은 규칙).
|
|
207
219
|
lines.push("", `PRODUCT LANGUAGE: ${locale === "ko" ? "Korean" : "English"}. Every user-facing string you emit is in this language.`);
|
|
208
220
|
// ★지난 시도가 왜 지어지지 못했는지를 모델 앞에 놓는다. 데스크탑과 같은 규율이다 —
|
|
@@ -239,7 +251,7 @@ function triggerQuestion() {
|
|
|
239
251
|
}
|
|
240
252
|
|
|
241
253
|
/** 청사진이 그래프로 지어질 수 있는지. 모자란 곳은 기본값이 아니라 질문으로 돌려준다. */
|
|
242
|
-
function validateBlueprint(bp) {
|
|
254
|
+
function validateBlueprint(bp, ctx = {}) {
|
|
243
255
|
const problems = [];
|
|
244
256
|
const push = (reason, ask = null) => problems.push({ reason, ask });
|
|
245
257
|
if (!bp || typeof bp !== "object") { push("만들 내용을 읽지 못했습니다."); return problems; }
|
|
@@ -305,6 +317,33 @@ function validateBlueprint(bp) {
|
|
|
305
317
|
choices: ["아니요, 만들기만 합니다", "네, 바깥으로 나갑니다"],
|
|
306
318
|
});
|
|
307
319
|
}
|
|
320
|
+
// ★부를 자동화는 실재해야 한다(데스크탑 shared/graph-blueprint.ts와 같은 규칙). 이름이
|
|
321
|
+
// 아니라 id로 가리키는 이유도 같다 — 이름은 바뀌고 지어낸 id는 실행에서 죽는다.
|
|
322
|
+
// 목록을 모르는 호출부에서는 형식만 본다(ctx.knownGraphs 없으면 건너뜀).
|
|
323
|
+
if (step.kind === "runGraph") {
|
|
324
|
+
const ref = String(step.graphRef || "").trim();
|
|
325
|
+
if (!ref) {
|
|
326
|
+
push(`${at}가 어느 자동화를 부를지 정하지 않았습니다.`);
|
|
327
|
+
} else if (ctx.selfId && ref === ctx.selfId) {
|
|
328
|
+
push(`${at}가 자기 자신을 부릅니다 — 끝나지 않습니다.`);
|
|
329
|
+
} else if (ctx.knownGraphs && ctx.knownGraphs.length && !ctx.knownGraphs.some((g) => g.id === ref)) {
|
|
330
|
+
push(`${at}가 부르려는 자동화("${ref}")가 없습니다. 저장된 자동화 중에서 골라야 합니다.`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// ★코드 스텝은 스크립트가 있어야 한다. 없으면 "코드로 하겠다"고 해 놓고 빈 채로 저장돼
|
|
334
|
+
// 실행에서 CODE_NODE_EMPTY로 죽는다(데스크탑과 같은 규칙, 저작 시점에 막는다).
|
|
335
|
+
if (step.kind === "code") {
|
|
336
|
+
if (!String(step.code || "").trim()) {
|
|
337
|
+
push(`${at}는 코드로 실행한다고 했는데 스크립트가 비어 있습니다.`, {
|
|
338
|
+
id: `step-${index}-code`,
|
|
339
|
+
question: `"${step.title || at}" 단계에서 무엇을 계산·가공하나요? (AI가 스크립트를 채웁니다)`,
|
|
340
|
+
why: "코드 단계는 스크립트가 없으면 실행되지 않습니다.",
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
if (step.codeLang && step.codeLang !== "python" && step.codeLang !== "js") {
|
|
344
|
+
push(`${at}의 코드 언어 "${step.codeLang}"을(를) 이 제품이 모릅니다(python 또는 js).`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
308
347
|
for (const name of step.consumes || []) {
|
|
309
348
|
if (!produced.has(name)) {
|
|
310
349
|
push(`${at}가 쓰는 "${name}" 값을 아무도 만들지 않습니다.`, {
|
|
@@ -484,7 +523,7 @@ function describeBranches(bp, locale) {
|
|
|
484
523
|
const lines = [];
|
|
485
524
|
const title = (index) => (typeof index === "number" && bp.steps[index] ? bp.steps[index].title : (ko ? "끝" : "the end"));
|
|
486
525
|
for (const branch of bp.branches || []) {
|
|
487
|
-
const rule = branchLabel(branch);
|
|
526
|
+
const rule = branchLabel(branch, locale);
|
|
488
527
|
const repeatText = branch.repeatStep !== undefined
|
|
489
528
|
? (ko ? `"${title(branch.repeatStep)}"부터 다시 (최대 ${branch.maxRepeats}번)` : `back to "${title(branch.repeatStep)}" (up to ${branch.maxRepeats}x)`)
|
|
490
529
|
: null;
|
|
@@ -502,8 +541,25 @@ function describeBranches(bp, locale) {
|
|
|
502
541
|
return lines;
|
|
503
542
|
}
|
|
504
543
|
|
|
505
|
-
|
|
544
|
+
/*
|
|
545
|
+
* ★언어를 받는다(데스크탑 shared/graph-blueprint.ts 미러). 예전에는 무조건 한국어라
|
|
546
|
+
* 영어로 만든 그래프에도 `verdict이(가) "fail"인가?`가 박혔고, 그 라벨이 공개
|
|
547
|
+
* 설명문에 실려 Hub 발행이 통째로 거절됐다("descriptionEn contains Hangul").
|
|
548
|
+
*/
|
|
549
|
+
function branchLabel(branch, locale = "ko") {
|
|
506
550
|
const shown = typeof branch.value === "string" ? `"${branch.value}"` : String(branch.value ?? "");
|
|
551
|
+
if (locale === "en") {
|
|
552
|
+
switch (branch.op) {
|
|
553
|
+
case "contains": return `Does ${branch.var} contain ${shown}?`;
|
|
554
|
+
case "truthy": return `Does ${branch.var} have a value?`;
|
|
555
|
+
case "falsy": return `Is ${branch.var} empty?`;
|
|
556
|
+
case "eq": return `Is ${branch.var} ${shown}?`;
|
|
557
|
+
case "ne": return `Is ${branch.var} not ${shown}?`;
|
|
558
|
+
case "gt": return `Is ${branch.var} greater than ${shown}?`;
|
|
559
|
+
case "lt": return `Is ${branch.var} less than ${shown}?`;
|
|
560
|
+
default: return `Check ${branch.var}`;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
507
563
|
switch (branch.op) {
|
|
508
564
|
case "contains": return `${branch.var}에 ${shown}이(가) 있나?`;
|
|
509
565
|
case "truthy": return `${branch.var}에 값이 있나?`;
|
|
@@ -576,8 +632,8 @@ function clipAtWord(text, max) {
|
|
|
576
632
|
return `${body.trimEnd()}…`;
|
|
577
633
|
}
|
|
578
634
|
|
|
579
|
-
function buildGraphFromBlueprint(bp, locale = "ko") {
|
|
580
|
-
const problems = validateBlueprint(bp);
|
|
635
|
+
function buildGraphFromBlueprint(bp, locale = "ko", ctx = {}) {
|
|
636
|
+
const problems = validateBlueprint(bp, ctx);
|
|
581
637
|
if (problems.length) return { ok: false, problems };
|
|
582
638
|
|
|
583
639
|
const nodes = [];
|
|
@@ -595,14 +651,19 @@ function buildGraphFromBlueprint(bp, locale = "ko") {
|
|
|
595
651
|
const stepId = (i) => `step${i + 1}`;
|
|
596
652
|
bp.steps.forEach((step, index) => {
|
|
597
653
|
const isCode = step.kind === "code";
|
|
654
|
+
// 다른 자동화를 한 단계로 부른다(데스크탑 커넥터 C46 subgraph). 캔버스엔 있는데
|
|
655
|
+
// 말로는 못 만들던 구멍 — 데스크탑 스케줄러가 이 노드를 재귀 실행한다.
|
|
656
|
+
const isSub = step.kind === "runGraph";
|
|
598
657
|
nodes.push({
|
|
599
658
|
id: stepId(index),
|
|
600
|
-
// 코드 스텝은 code 노드로(데스크탑 shared/graph-blueprint.ts와 같은 규칙).
|
|
601
|
-
type: isCode ? "code" : (step.effect === "mutation" ? "action" : "agent"),
|
|
659
|
+
// 코드 스텝은 code 노드로, subgraph 는 subgraph 노드로(데스크탑 shared/graph-blueprint.ts와 같은 규칙).
|
|
660
|
+
type: isSub ? "subgraph" : isCode ? "code" : (step.effect === "mutation" ? "action" : "agent"),
|
|
602
661
|
label: step.title,
|
|
603
662
|
position: { x: column(index + 1), y: 0 },
|
|
604
663
|
config: {
|
|
605
|
-
...(
|
|
664
|
+
...(isSub
|
|
665
|
+
? { graphRef: step.graphRef || "", note: step.instruction }
|
|
666
|
+
: isCode
|
|
606
667
|
? {
|
|
607
668
|
code: step.code || "", codeLang: step.codeLang === "js" ? "js" : "python", note: step.instruction,
|
|
608
669
|
...(Array.isArray(step.packages) && step.packages.length
|
|
@@ -695,7 +756,7 @@ function buildGraphFromBlueprint(bp, locale = "ko") {
|
|
|
695
756
|
}
|
|
696
757
|
const branchId = `check${index + 1}`;
|
|
697
758
|
nodes.push({
|
|
698
|
-
id: branchId, type: "condition", label: branchLabel(branch),
|
|
759
|
+
id: branchId, type: "condition", label: branchLabel(branch, locale),
|
|
699
760
|
position: { x: column(index + 1) + 140, y: 0 },
|
|
700
761
|
config: { var: branch.var, op: branch.op, ...(branch.value !== undefined ? { value: branch.value } : {}) },
|
|
701
762
|
});
|
|
@@ -762,11 +823,35 @@ function firstJsonObject(text) {
|
|
|
762
823
|
return null;
|
|
763
824
|
}
|
|
764
825
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
826
|
+
/**
|
|
827
|
+
* ★런타임이 JSON 대신 **사람에게 하는 말**을 돌려줬을 수 있다.
|
|
828
|
+
*
|
|
829
|
+
* 실측 2026-08-06: 모델이 `You've hit your weekly limit · resets Aug 8 at 6pm (Asia/Seoul)`을
|
|
830
|
+
* 돌려줬는데, 제품은 그 문장을 삼키고 "만들 내용을 읽지 못했습니다 / 한 문장으로 다시
|
|
831
|
+
* 적어 주세요"라고 말했다. 사람은 자기 문장이 틀린 줄 알고 몇 번이고 다시 쓴다 —
|
|
832
|
+
* 아무리 잘 써도 안 된다. 무엇이 막혔는지 아는 쪽은 제품인데, 그것을 지우고 있었다.
|
|
833
|
+
*
|
|
834
|
+
* 짧고 JSON이 아니면 잘린 청사진이 아니라 **고지문**이다. 그대로 보여준다.
|
|
835
|
+
*/
|
|
836
|
+
const RUNTIME_NOTICE_MAX = 240;
|
|
837
|
+
const unreadable = (rawText) => {
|
|
838
|
+
const text = typeof rawText === "string" ? rawText.trim() : "";
|
|
839
|
+
const looksLikeNotice = text.length > 0 && text.length <= RUNTIME_NOTICE_MAX && !text.includes("{");
|
|
840
|
+
if (looksLikeNotice) {
|
|
841
|
+
return {
|
|
842
|
+
ok: false, code: "INTERVIEW_MODEL_UNAVAILABLE",
|
|
843
|
+
reason: `AI가 만들지 못했습니다 — ${text}`,
|
|
844
|
+
nextAction: "다른 모델을 연결하거나, 안내에 적힌 시각 이후에 다시 시도해 주세요.",
|
|
845
|
+
// 재시도로 해결되지 않는다 — 형식 문제가 아니다.
|
|
846
|
+
terminal: true,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
ok: false, code: "INTERVIEW_OUTPUT_UNREADABLE",
|
|
851
|
+
reason: "만들 내용을 읽지 못했습니다.",
|
|
852
|
+
nextAction: "자동으로 돌릴 일을 한 문장으로 다시 적어 주세요.",
|
|
853
|
+
};
|
|
854
|
+
};
|
|
770
855
|
|
|
771
856
|
function normalizeQuestions(candidates, state) {
|
|
772
857
|
const seen = new Set(state.asked);
|
|
@@ -820,10 +905,10 @@ function weakenedAgainstLastAttempt(blueprint, state) {
|
|
|
820
905
|
|
|
821
906
|
function parseInterviewTurn(text, state) {
|
|
822
907
|
const raw = firstJsonObject(text);
|
|
823
|
-
if (!raw) return unreadable();
|
|
908
|
+
if (!raw) return unreadable(text);
|
|
824
909
|
let parsed;
|
|
825
|
-
try { parsed = JSON.parse(raw); } catch { return unreadable(); }
|
|
826
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return unreadable();
|
|
910
|
+
try { parsed = JSON.parse(raw); } catch { return unreadable(text); }
|
|
911
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return unreadable(text);
|
|
827
912
|
|
|
828
913
|
if (Array.isArray(parsed.ask) && parsed.ask.length > 0) {
|
|
829
914
|
const questions = normalizeQuestions(parsed.ask, state);
|
|
@@ -838,7 +923,7 @@ function parseInterviewTurn(text, state) {
|
|
|
838
923
|
}
|
|
839
924
|
|
|
840
925
|
const blueprint = parsed.blueprint;
|
|
841
|
-
if (!blueprint || typeof blueprint !== "object") return unreadable();
|
|
926
|
+
if (!blueprint || typeof blueprint !== "object") return unreadable(text);
|
|
842
927
|
const normalized = { ...blueprint, schema: BLUEPRINT_SCHEMA };
|
|
843
928
|
const problems = validateBlueprint(normalized);
|
|
844
929
|
if (problems.length === 0) {
|