agentlas 1.0.28 → 1.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +5 -2
  3. package/bin/agentlas.cjs +15 -0
  4. package/engine/agentlas-cloud-runtime.cjs +54 -4
  5. package/engine/agentlas-i18n.cjs +4 -4
  6. package/engine/agentlas-input.cjs +10 -3
  7. package/engine/agentlas-judgment.cjs +0 -0
  8. package/engine/agentlas-native-host.cjs +69 -4
  9. package/engine/agentlas-onboard.cjs +20 -0
  10. package/engine/agentlas-ui.cjs +2 -0
  11. package/engine/agentlas-workforce.cjs +69 -13
  12. package/engine/agentlas.cjs +71 -3
  13. package/engine/agents/builder.cjs +84 -0
  14. package/engine/agents/router.cjs +8 -2
  15. package/engine/automation/launchd.cjs +131 -0
  16. package/engine/bootstrap-schema.sql +6 -44
  17. package/engine/browser/cdp.cjs +188 -0
  18. package/engine/browser/vault.cjs +124 -0
  19. package/engine/cli-output.cjs +262 -0
  20. package/engine/cloud-assets/package.cjs +5 -1
  21. package/engine/commands/automation.cjs +37 -1
  22. package/engine/commands/billing.cjs +3 -0
  23. package/engine/commands/browser.cjs +166 -8
  24. package/engine/commands/build.cjs +101 -20
  25. package/engine/commands/connect.cjs +162 -9
  26. package/engine/commands/creds.cjs +65 -3
  27. package/engine/commands/doctor.cjs +66 -4
  28. package/engine/commands/document.cjs +79 -0
  29. package/engine/commands/graph.cjs +1190 -0
  30. package/engine/commands/help.cjs +87 -9
  31. package/engine/commands/hep-cloud.cjs +9 -23
  32. package/engine/commands/hep-hub.cjs +9 -22
  33. package/engine/commands/hep-local.cjs +9 -24
  34. package/engine/commands/hep-network.cjs +9 -35
  35. package/engine/commands/index.cjs +50 -25
  36. package/engine/commands/list.cjs +10 -1
  37. package/engine/commands/mcp.cjs +6 -2
  38. package/engine/commands/native.cjs +18 -2
  39. package/engine/commands/plugin.cjs +22 -0
  40. package/engine/commands/project.cjs +79 -16
  41. package/engine/commands/roles.cjs +210 -0
  42. package/engine/commands/telegram.cjs +23 -16
  43. package/engine/commands/workforce.cjs +63 -12
  44. package/engine/core/desktop-core-fetch.cjs +98 -0
  45. package/engine/core/desktop-core.cjs +170 -0
  46. package/engine/graph/ask-model.cjs +159 -0
  47. package/engine/graph/interview.cjs +960 -0
  48. package/engine/graph/layout.cjs +139 -0
  49. package/engine/graph/package.cjs +223 -0
  50. package/engine/graph/vocabulary.generated.cjs +30 -0
  51. package/engine/hephaestus/local-core.cjs +159 -0
  52. package/engine/hephaestus/runtime.cjs +14 -10
  53. package/engine/project/controller.cjs +8 -8
  54. package/engine/project/team.cjs +99 -0
  55. package/engine/runtime-refusal.cjs +71 -0
  56. package/engine/runtimes/auth-evidence.cjs +78 -0
  57. package/engine/runtimes/detect.cjs +3 -0
  58. package/engine/runtimes/resolve.cjs +1 -1
  59. package/engine/sessions/prompt.cjs +16 -0
  60. package/engine/sessions/session.cjs +24 -2
  61. package/engine/telegram/connect.cjs +202 -0
  62. package/engine/tools/access-notice.cjs +86 -0
  63. package/engine/ui/palette.cjs +7 -3
  64. package/engine/ui/repl.cjs +120 -3
  65. package/engine/vendor/desktop-core.manifest.json +7 -0
  66. package/engine/workforce/capture.cjs +51 -1
  67. package/engine/workforce/deps.cjs +13 -0
  68. package/engine/workforce/local-core-transport.cjs +298 -0
  69. package/package.json +4 -2
  70. package/engine/commands/legacy-network.cjs +0 -29
@@ -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 };
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ /*
3
+ * 인터뷰 한 턴을 모델에게 묻는 최소 경로.
4
+ *
5
+ * 세션·영속·메모리 티켓 없이 **한 번 묻고 최종 텍스트만** 받는다. 인터뷰는 사람이 화면 앞에
6
+ * 앉아 기다리는 대화라, 채팅 세션의 부수효과(대화 저장·메모리 방출·펜스 처리)가 끼어들면
7
+ * 사용자가 요청하지 않은 기록이 남는다.
8
+ *
9
+ * 실패를 삼키지 않는다. 모델이 안 돌면 그 사실과 다음 행동을 그대로 돌려준다 —
10
+ * 조용히 빈 문자열을 주면 파서가 "읽지 못했습니다"로 바꿔 원인이 사라진다.
11
+ */
12
+ const nativeHost = require("../agentlas-native-host.cjs");
13
+ // ★resolveRuntime(resolve.cjs)이 아니라 resolveRuntimeForAgent를 쓴다.
14
+ // resolveRuntime은 `role`을 **아예 읽지 않아서**, 사용자가 모델 역할에 gemini를 지정해 둬도
15
+ // active_runtime(codex)으로 흘렀다(실측: 인터뷰가 사용자가 고르지 않은 런타임에서 돌았고,
16
+ // 그쪽 사용 한도가 소진돼 있었다). 사용자가 정한 역할 사다리를 따르는 쪽이 정본이다.
17
+ const { resolveRuntimeForAgent } = require("../runtimes/overrides.cjs");
18
+ const { listAvailableCliRuntimes } = require("../runtimes/detect.cjs");
19
+
20
+ /**
21
+ * native-host가 부르는 이벤트 싱크 — 인터뷰 중에는 화면에 아무것도 흘리지 않는다.
22
+ *
23
+ * ★메서드를 손으로 골라 적으면 안 된다. native-host가 부르는 이름이 하나라도 빠지면
24
+ * 턴 중간에 TypeError로 죽는다(실측: streamStart 누락으로 프로세스가 통째로 죽었다).
25
+ * 실제 EventSink의 표면을 그대로 덮어써서, 저쪽이 늘어나도 여기가 따라간다.
26
+ */
27
+ function quietSink() {
28
+ const { EventSink } = require("../sessions/sink.cjs");
29
+ const sink = new EventSink(() => {}, () => {});
30
+ const quiet = Object.create(Object.getPrototypeOf(sink));
31
+ for (const name of Object.getOwnPropertyNames(Object.getPrototypeOf(sink))) {
32
+ if (name === "constructor") continue;
33
+ quiet[name] = () => {};
34
+ }
35
+ // native-host가 색/번역 헬퍼도 만진다.
36
+ quiet.c = new Proxy({}, { get: () => (text) => String(text ?? "") });
37
+ quiet.t = (_key, fallback) => String(fallback ?? "");
38
+ quiet.replaceTasks = () => {};
39
+ return quiet;
40
+ }
41
+
42
+ /**
43
+ * 인터뷰 한 턴을 묻는다.
44
+ *
45
+ * 사용자가 정한 역할(orchestrator)을 **먼저** 쓴다. 그게 실패하면 조용히 포기하지 않고
46
+ * 이 컴퓨터에 있는 다른 런타임으로 이어서 물어보되, **어느 것이 답했는지 말한다** —
47
+ * 조용히 다른 모델로 바꾸면 사용자는 자기가 고른 모델이 돈 줄 안다.
48
+ * 전부 실패하면 각각의 사유를 그대로 돌려준다(하나로 뭉뚱그리면 원인이 사라진다).
49
+ *
50
+ * @returns {Promise<{ok:true,text:string,runtime:string,fellBackFrom?:string}
51
+ * |{ok:false,reason:string,nextAction:string}>}
52
+ */
53
+ async function askModel(ctx, prompt, opts = {}) {
54
+ const db = ctx.db();
55
+ let primary = null;
56
+ try {
57
+ primary = resolveRuntimeForAgent({
58
+ db,
59
+ prefs: ctx.prefs,
60
+ role: "orchestrator",
61
+ ...(opts.runtime ? { explicit: opts.runtime } : {}),
62
+ ...(opts.model ? { model: opts.model } : {}),
63
+ });
64
+ } catch (err) {
65
+ primary = null;
66
+ var primaryError = (err && err.message) || String(err);
67
+ }
68
+
69
+ // 시도 순서: 사용자가 정한 것 먼저, 그다음 이 컴퓨터에 있는 나머지.
70
+ const candidates = [];
71
+ if (primary && primary.kind && primary.bin) candidates.push(primary);
72
+ if (!opts.runtime) {
73
+ // listAvailableCliRuntimes()는 문자열이 아니라 {kind, bin, path} 객체를 돌려준다.
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");
79
+ for (const found of listAvailableCliRuntimes()) {
80
+ if (!found || !found.kind || !found.path) continue;
81
+ if (!CLI_EXECUTABLE_KINDS.has(found.kind)) continue;
82
+ if (candidates.some((c) => c.kind === found.kind)) continue;
83
+ candidates.push({ kind: found.kind, bin: found.path });
84
+ }
85
+ }
86
+ if (!candidates.length) {
87
+ return {
88
+ ok: false,
89
+ reason: primaryError
90
+ ? `실행할 AI 런타임을 찾지 못했습니다: ${primaryError}`
91
+ : "이 컴퓨터에서 쓸 수 있는 AI 런타임이 없습니다.",
92
+ nextAction: "`agentlas doctor`로 런타임 상태를 확인한 뒤 다시 시도해 주세요.",
93
+ };
94
+ }
95
+
96
+ const failures = [];
97
+ for (const runtime of candidates) {
98
+ let res;
99
+ try {
100
+ res = await nativeHost.runNativeTurn({
101
+ kind: runtime.kind,
102
+ bin: runtime.bin,
103
+ ui: quietSink(),
104
+ cwd: opts.cwd || process.cwd(),
105
+ prompt,
106
+ // 읽기 권한 — 인터뷰는 사람에게 묻고 형식을 만드는 일이라 파일을 바꿀 이유가 없다.
107
+ permission: "read",
108
+ session: {},
109
+ model: runtime.model,
110
+ effort: runtime.effort,
111
+ mcpServers: [],
112
+ mcpAllowlistMode: "exact",
113
+ });
114
+ } catch (err) {
115
+ failures.push(`${runtime.kind}: ${(err && err.message) || err}`);
116
+ continue;
117
+ }
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
+ }
141
+ if (text.trim()) {
142
+ const out = { ok: true, text, runtime: runtime.kind };
143
+ if (candidates[0] !== runtime) out.fellBackFrom = candidates[0].kind;
144
+ return out;
145
+ }
146
+ failures.push(res && res.error
147
+ ? `${runtime.kind}: ${String(res.error).replace(/\s+/g, " ").slice(0, 200)}`
148
+ : `${runtime.kind}: 빈 답`);
149
+ }
150
+
151
+ return {
152
+ ok: false,
153
+ reason: `AI가 답하지 못했습니다.\n ${failures.join("\n ")}`,
154
+ nextAction: "`agentlas doctor`로 런타임 상태를 확인하거나, 로그인이 필요한 런타임에 다시 로그인해 주세요.",
155
+ };
156
+ }
157
+
158
+
159
+ module.exports = { askModel };