agentlas 0.7.0 → 0.9.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 +199 -0
- package/README.md +161 -18
- package/bin/agentlas.cjs +8 -8
- package/engine/agentlas-core-harness.cjs +212 -0
- package/engine/agentlas-desktop-loadout.cjs +527 -0
- package/engine/agentlas-doctor.cjs +1 -1
- package/engine/agentlas-experience-exchange.cjs +835 -85
- package/engine/agentlas-experience-intake.cjs +444 -0
- package/engine/agentlas-experience-mcp.cjs +580 -18
- package/engine/agentlas-i18n.cjs +10 -10
- package/engine/agentlas-input.cjs +5 -4
- package/engine/agentlas-mcp-env.cjs +219 -0
- package/engine/agentlas-mcp-wrapper.cjs +51 -0
- package/engine/agentlas-memory-governance.cjs +1029 -0
- package/engine/agentlas-native-host.cjs +129 -39
- package/engine/agentlas-parity.cjs +339 -154
- package/engine/agentlas-repl.cjs +306 -31
- package/engine/agentlas-workforce.cjs +2991 -0
- package/engine/agentlas-workload-routing.cjs +523 -0
- package/engine/agentlas.cjs +1619 -234
- package/engine/bootstrap-schema.sql +1 -1
- package/engine/experience-taxonomy-v1.json +49 -0
- package/package.json +8 -4
- package/scripts/gen-bootstrap-schema.sh +0 -23
- package/test/bootstrap-race.cjs +0 -47
- package/test/capture-runtime-guard.cjs +0 -122
- package/test/cloud-asset-restore.cjs +0 -423
- package/test/cloud-cas-client.cjs +0 -333
- package/test/cloud-owner-restore.cjs +0 -183
- package/test/cloud-runtime-paths.cjs +0 -40
- package/test/cloud-save-publish.cjs +0 -487
- package/test/credential-env-regression.cjs +0 -52
- package/test/engine-hardening-regression.cjs +0 -74
- package/test/experience-exchange-contract.cjs +0 -569
- package/test/experience-mcp-contract.cjs +0 -391
- package/test/fixtures/portable-experience-bundle-v1-golden.json +0 -124
- package/test/login-loopback-security.cjs +0 -115
- package/test/mcp-config-isolation.cjs +0 -36
- package/test/permission-mapping.cjs +0 -180
- package/test/route-regression.cjs +0 -357
- package/test/run-api-regression.cjs +0 -322
- package/test/runtime-env-protection.cjs +0 -89
- package/test/semver-precedence.cjs +0 -39
- package/test/smoke.sh +0 -93
- package/test/sqlite-driver-probe.cjs +0 -22
- package/test/terminal-ui-regression.cjs +0 -477
- package/test/timeout-regression.cjs +0 -218
- package/test/tool-workspace-boundary.cjs +0 -165
- package/test/update-safety.cjs +0 -376
package/engine/agentlas.cjs
CHANGED
|
@@ -31,6 +31,11 @@ const crypto = require("node:crypto");
|
|
|
31
31
|
const { compareSemVer, normalizeSemVer, parseSemVer } = require("./semver.cjs");
|
|
32
32
|
const terminalAssets = require("./agentlas-experience-mcp.cjs");
|
|
33
33
|
const terminalExperienceExchange = require("./agentlas-experience-exchange.cjs");
|
|
34
|
+
const desktopOntologyLoadout = require("./agentlas-desktop-loadout.cjs");
|
|
35
|
+
const workloadRouting = require("./agentlas-workload-routing.cjs");
|
|
36
|
+
const terminalExperienceIntake = require("./agentlas-experience-intake.cjs");
|
|
37
|
+
const terminalMemoryGovernance = require("./agentlas-memory-governance.cjs");
|
|
38
|
+
const { captureCoreJsonSync, resolveCoreRuntimeRoot } = require("./agentlas-core-harness.cjs");
|
|
34
39
|
|
|
35
40
|
// ── 앱과 동일한 userData 경로 (electron app.getPath('userData')와 일치) ──
|
|
36
41
|
function userDataDir() {
|
|
@@ -256,7 +261,7 @@ function saveMultimodalSettingsCli(db, patch) {
|
|
|
256
261
|
db.prepare("INSERT INTO meta(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value")
|
|
257
262
|
.run(MULTIMODAL_META_KEY, JSON.stringify(next));
|
|
258
263
|
} catch (e) {
|
|
259
|
-
fail("multimodal settings
|
|
264
|
+
fail("Failed to save multimodal settings: " + e.message);
|
|
260
265
|
}
|
|
261
266
|
return next;
|
|
262
267
|
}
|
|
@@ -388,8 +393,8 @@ function routeNormalize(value) {
|
|
|
388
393
|
return String(value || "").toLowerCase().replace(/[_/]+/g, "-");
|
|
389
394
|
}
|
|
390
395
|
// 경로 디렉터리 성분은 라우팅 의도가 아니다 — 마지막 세그먼트(파일/폴더명)만 남긴다.
|
|
391
|
-
// 사고(2026-07-12): "/Users/
|
|
392
|
-
// ("users","
|
|
396
|
+
// 사고(2026-07-12): "/Users/example/Projects/…/Appbridge_Template.이 …" 프롬프트의 경로 토큰
|
|
397
|
+
// ("users","example","projects","users-example-projects-")이 임포트 에이전트 system_prompt 속
|
|
393
398
|
// 절대경로와 맞아떨어져 +2씩 쌓이고 라우팅 근거에까지 노출됐다. 프롬프트/헤이스택 양쪽에
|
|
394
399
|
// 대칭 적용해 경로↔경로 우연 일치를 차단한다. 파일/폴더명은 실제 의도라서 보존한다.
|
|
395
400
|
// 규칙: 공백/인용부호/괄호 뒤(또는 문자열 시작)에서 시작하고, "세그먼트+구분자"가 2회 이상
|
|
@@ -667,6 +672,40 @@ function agentFolder(agent) {
|
|
|
667
672
|
if (exists(path.join(cloudRoot, CLOUD_RESTORE_MARKER_PATH))) return cloudRoot;
|
|
668
673
|
return path.join(userDataDir(), "agents", agent.slug);
|
|
669
674
|
}
|
|
675
|
+
function exactAgentBaseForExecution(db, agent, runtimeExperience = null) {
|
|
676
|
+
if (!agent || agent.builtin) return null;
|
|
677
|
+
const portableId = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{2,255}$/;
|
|
678
|
+
let binding = null;
|
|
679
|
+
try {
|
|
680
|
+
if (tableExists(db, "installed_agent_hub_bindings")) {
|
|
681
|
+
binding = db.prepare(
|
|
682
|
+
"SELECT agent_definition_id,agent_release_id FROM installed_agent_hub_bindings WHERE installed_agent_id=?",
|
|
683
|
+
).get(agent.id) || null;
|
|
684
|
+
}
|
|
685
|
+
} catch { binding = null; }
|
|
686
|
+
const route = routesMap()[agent.id] || {};
|
|
687
|
+
const markerResult = terminalExperienceExchange.readExactLocalBaseMarker(agentFolder(agent), agent.slug);
|
|
688
|
+
const marker = markerResult.marker;
|
|
689
|
+
const rawHash = String(marker?.packageHash || route.packageHash || route.definitionHash || "").replace(/^sha256:/i, "").toLowerCase();
|
|
690
|
+
const packageHash = /^[a-f0-9]{64}$/.test(rawHash) ? `sha256:${rawHash}` : null;
|
|
691
|
+
const explicitDefinition = String(runtimeExperience?.agentDefinitionId || "");
|
|
692
|
+
const explicitRelease = String(runtimeExperience?.baseAgentReleaseId || "");
|
|
693
|
+
if (portableId.test(explicitDefinition) && portableId.test(explicitRelease)) {
|
|
694
|
+
return { agentDefinitionId: explicitDefinition, agentReleaseId: explicitRelease, packageHash, authority: "explicit-runtime-binding" };
|
|
695
|
+
}
|
|
696
|
+
if (binding && portableId.test(String(binding.agent_definition_id)) && portableId.test(String(binding.agent_release_id))) {
|
|
697
|
+
return { agentDefinitionId: binding.agent_definition_id, agentReleaseId: binding.agent_release_id, packageHash, authority: "installed-hub-binding" };
|
|
698
|
+
}
|
|
699
|
+
if (!packageHash) return null;
|
|
700
|
+
const definitionDigest = sha(`terminal-local-definition\0${agent.id}\0${agent.slug}`);
|
|
701
|
+
const releaseDigest = sha(`terminal-local-release\0${definitionDigest}\0${packageHash}`);
|
|
702
|
+
return {
|
|
703
|
+
agentDefinitionId: `local-agent-definition:${definitionDigest.slice(0, 32)}`,
|
|
704
|
+
agentReleaseId: `local-agent-release:${releaseDigest.slice(0, 32)}`,
|
|
705
|
+
packageHash,
|
|
706
|
+
authority: "exact-local-package-hash",
|
|
707
|
+
};
|
|
708
|
+
}
|
|
670
709
|
function agentSystemPromptCli(agent) {
|
|
671
710
|
return agent && agent.system_prompt ? agent.system_prompt : `You are ${agent?.name || "an Agentlas agent"}.`;
|
|
672
711
|
}
|
|
@@ -774,7 +813,7 @@ function buildImportSystemPrompt(dir, name, kind) {
|
|
|
774
813
|
}
|
|
775
814
|
function importLocalFolderCli(db, absPath) {
|
|
776
815
|
const dir = path.resolve(absPath);
|
|
777
|
-
if (!isDir(dir)) fail(
|
|
816
|
+
if (!isDir(dir)) fail(`Not a directory: ${absPath}`);
|
|
778
817
|
const labels = detectRuntimeLabels(dir);
|
|
779
818
|
const runtime = labels[0];
|
|
780
819
|
const kind = detectKind(dir);
|
|
@@ -879,15 +918,15 @@ function upsertLocalTeamFirmCli(db, dir, ceoAgentId, agentSlug, name, tagline) {
|
|
|
879
918
|
return { id, slug: firmSlug };
|
|
880
919
|
}
|
|
881
920
|
function cmdImport(db, absPath) {
|
|
882
|
-
if (!absPath) fail("
|
|
921
|
+
if (!absPath) fail("Usage: agentlas import <folder-path>");
|
|
883
922
|
const r = importLocalFolderCli(db, absPath);
|
|
884
|
-
out(`${r.updated ? "
|
|
923
|
+
out(`${r.updated ? "Updated" : "Imported"}: ${r.name} (${r.kind})`);
|
|
885
924
|
out(` slug: ${r.slug}`);
|
|
886
925
|
out(` runtime: ${r.runtime} [${r.labels.join(", ")}]`);
|
|
887
926
|
out(` path: ${r.path}`);
|
|
888
|
-
if (r.firmSlug) out(` firm: ${r.firmSlug} (
|
|
927
|
+
if (r.firmSlug) out(` firm: ${r.firmSlug} (registered in Firms — Desktop sidebar + 'agentlas firm ${r.firmSlug}')`);
|
|
889
928
|
out("");
|
|
890
|
-
out(
|
|
929
|
+
out(`Run: agentlas ${r.slug} "..." · agentlas run ${r.slug} "..." (run from the target project folder)`);
|
|
891
930
|
}
|
|
892
931
|
|
|
893
932
|
// ── Agentlas Cloud packaging / marketplace ────────────────────────────────
|
|
@@ -963,7 +1002,7 @@ function hubTimeoutError(kind, ms) {
|
|
|
963
1002
|
/** Hub/Cloud fetch + body reader. Headers 전 connect, chunk 사이 idle, 전 구간 total timeout. */
|
|
964
1003
|
async function fetchHubCli(url, init = {}, options = {}) {
|
|
965
1004
|
const fetchImpl = options.fetch || globalThis.fetch;
|
|
966
|
-
if (typeof fetchImpl !== "function") throw new Error("
|
|
1005
|
+
if (typeof fetchImpl !== "function") throw new Error("fetch is unavailable in this runtime.");
|
|
967
1006
|
const timeout = options.timeoutConfig ? directHubTimeoutConfig(options.timeoutConfig) : hubTimeoutConfig(options.env || process.env);
|
|
968
1007
|
const controller = new AbortController();
|
|
969
1008
|
const upstreamSignal = init.signal;
|
|
@@ -1027,7 +1066,7 @@ async function fetchHubCli(url, init = {}, options = {}) {
|
|
|
1027
1066
|
} else {
|
|
1028
1067
|
const raw = Buffer.from(await Promise.race([response.arrayBuffer(), terminal]));
|
|
1029
1068
|
bytes = raw.length;
|
|
1030
|
-
if (bytes > HUB_RESPONSE_MAX_BYTES) throw new Error(`Hub
|
|
1069
|
+
if (bytes > HUB_RESPONSE_MAX_BYTES) throw new Error(`Hub response exceeds the allowed size (${HUB_RESPONSE_MAX_BYTES} bytes).`);
|
|
1031
1070
|
chunks.push(raw);
|
|
1032
1071
|
}
|
|
1033
1072
|
if (idleTimer) clearTimeout(idleTimer);
|
|
@@ -1052,7 +1091,7 @@ function parseHubJsonCli(response, label) {
|
|
|
1052
1091
|
try {
|
|
1053
1092
|
return JSON.parse(response.text || "null");
|
|
1054
1093
|
} catch {
|
|
1055
|
-
throw new Error(`${label}
|
|
1094
|
+
throw new Error(`${label} returned invalid JSON.`);
|
|
1056
1095
|
}
|
|
1057
1096
|
}
|
|
1058
1097
|
|
|
@@ -1126,6 +1165,7 @@ async function cmdCloud(db, args, runtimeOverride) {
|
|
|
1126
1165
|
" list [--json] list packages in your private Agent Cloud",
|
|
1127
1166
|
" restore <slug> [--json] restore an owned Cloud package on this machine",
|
|
1128
1167
|
" install <slug> compatibility alias: install from the public Hub",
|
|
1168
|
+
" plugin add <slug> install a Hub plugin (MCP servers)",
|
|
1129
1169
|
" delete <slug> [--scope owner-private|hub-public] [--json]",
|
|
1130
1170
|
" conditionally delete one exact observed Cloud revision",
|
|
1131
1171
|
" search \"<what you need>\" [--limit 10]",
|
|
@@ -1144,7 +1184,7 @@ async function cmdCloud(db, args, runtimeOverride) {
|
|
|
1144
1184
|
const result = await listOwnedCloudAgentsCli(Number(flags.limit || 100));
|
|
1145
1185
|
if (flags.json) return out(JSON.stringify(result, null, 2));
|
|
1146
1186
|
const agents = Array.isArray(result.results) ? result.results : [];
|
|
1147
|
-
if (!agents.length) return out("Private Agent Cloud
|
|
1187
|
+
if (!agents.length) return out("No agents are stored in Private Agent Cloud.");
|
|
1148
1188
|
for (const agent of agents) out(`${agent.slug}\t${agent.name || agent.nameEn || agent.slug}\t${agent.entityKind || "agent"}`);
|
|
1149
1189
|
return;
|
|
1150
1190
|
}
|
|
@@ -1240,8 +1280,8 @@ async function cmdCloud(db, args, runtimeOverride) {
|
|
|
1240
1280
|
async function packageCloudAgentCli(db, root, opts) {
|
|
1241
1281
|
const requestedRoot = path.resolve(root);
|
|
1242
1282
|
let st;
|
|
1243
|
-
try { st = fs.lstatSync(requestedRoot); } catch { throw new Error(
|
|
1244
|
-
if (!st.isDirectory() || st.isSymbolicLink()) throw new Error(
|
|
1283
|
+
try { st = fs.lstatSync(requestedRoot); } catch { throw new Error(`Folder not found: ${root}`); }
|
|
1284
|
+
if (!st.isDirectory() || st.isSymbolicLink()) throw new Error(`Not a real directory: ${root}`);
|
|
1245
1285
|
const rootPath = fs.realpathSync.native(requestedRoot);
|
|
1246
1286
|
const visibility = opts.visibility || "private-link";
|
|
1247
1287
|
const isPublicHubPublish = visibility === "marketplace";
|
|
@@ -1940,19 +1980,19 @@ function cloudCasResponseErrorCli(response, label) {
|
|
|
1940
1980
|
let body = null;
|
|
1941
1981
|
try { body = JSON.parse(response.text || "null"); } catch { /* generic below */ }
|
|
1942
1982
|
const code = body && typeof body.code === "string" ? body.code : "cloud_request_failed";
|
|
1943
|
-
let message = `${label}
|
|
1983
|
+
let message = `${label} failed with HTTP ${response.status}`;
|
|
1944
1984
|
if (response.status === 412 && code === "cloud_agent_revision_conflict") {
|
|
1945
1985
|
const current = body && body.current ? body.current : body && body.conflict && body.conflict.current;
|
|
1946
1986
|
message = current
|
|
1947
1987
|
? `다른 PC에서 이 Agent Cloud 자산이 변경되었습니다. 자동 덮어쓰기는 중단했습니다. \`agentlas cloud list\`로 최신 revision을 확인하고 \`agentlas cloud restore ${current.slug || "<slug>"}\`로 복원한 뒤 변경 사항을 병합하세요.`
|
|
1948
1988
|
: "이 Agent Cloud 자산은 다른 PC에서 삭제되었거나 다른 식별자로 다시 생성되었습니다. 자동 재생성은 중단했습니다. `agentlas cloud list`로 현재 상태를 확인하세요.";
|
|
1949
1989
|
} else if (response.status === 428 && code === "client_upgrade_required") {
|
|
1950
|
-
message = "
|
|
1990
|
+
message = "No base revision is available to safely update the existing Cloud asset. The server revision will not be copied automatically. Check `agentlas cloud list`, restore with `agentlas cloud restore <slug>`, then save again.";
|
|
1951
1991
|
} else if (response.status === 503 && code === "cloud_mutations_maintenance") {
|
|
1952
1992
|
const retryAfter = response.headers && typeof response.headers.get === "function" ? response.headers.get("retry-after") : null;
|
|
1953
|
-
message = `Agent Cloud
|
|
1993
|
+
message = `Agent Cloud save/delete is temporarily under maintenance${retryAfter ? ` (retry in about ${retryAfter} seconds)` : ""}. Read, list, and restore remain available.`;
|
|
1954
1994
|
} else if (body && typeof body.error === "string") {
|
|
1955
|
-
message = `${label}
|
|
1995
|
+
message = `${label} failed with HTTP ${response.status}: ${body.error.slice(0, 300)}`;
|
|
1956
1996
|
}
|
|
1957
1997
|
const error = new Error(message);
|
|
1958
1998
|
error.code = code;
|
|
@@ -1964,8 +2004,8 @@ function cloudCasResponseErrorCli(response, label) {
|
|
|
1964
2004
|
|
|
1965
2005
|
async function registerCloudAgentCli(manifest, bundlePath, review, visibility, options = {}) {
|
|
1966
2006
|
const cookie = await cloudSessionCookieCli();
|
|
1967
|
-
if (!cookie) fail("
|
|
1968
|
-
if (typeof fetch !== "function") fail("
|
|
2007
|
+
if (!cookie) fail("Agent Cloud sign-in is required. Sign in through Desktop or set AGENTLAS_SESSION.");
|
|
2008
|
+
if (typeof fetch !== "function") fail("fetch is unavailable in this runtime (run through the app runtime).");
|
|
1969
2009
|
const base = (process.env.AGENTLAS_WEB_BASE_URL || "https://agentlas.cloud").replace(/\/$/, "");
|
|
1970
2010
|
const bundle = JSON.parse(fs.readFileSync(bundlePath, "utf8"));
|
|
1971
2011
|
const expectedScope = cloudScopeForVisibility(visibility);
|
|
@@ -2038,8 +2078,8 @@ async function deleteCloudAgentCli(slug, options = {}) {
|
|
|
2038
2078
|
const safeSlug = String(slug || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
2039
2079
|
if (!safeSlug) fail("usage: agentlas cloud delete <slug> [--json]");
|
|
2040
2080
|
const cookie = await cloudSessionCookieCli();
|
|
2041
|
-
if (!cookie) fail("
|
|
2042
|
-
if (typeof fetch !== "function") fail("
|
|
2081
|
+
if (!cookie) fail("Agent Cloud sign-in is required. Sign in through Desktop or set AGENTLAS_SESSION.");
|
|
2082
|
+
if (typeof fetch !== "function") fail("fetch is unavailable in this runtime (run through the app runtime).");
|
|
2043
2083
|
const scope = options.scope == null ? null : normalizeCloudScopeFlagCli(options.scope);
|
|
2044
2084
|
if (options.scope != null && !scope) throw new Error("--scope must be owner-private or hub-public");
|
|
2045
2085
|
const localEntry = findCloudAssetDescriptorCli(safeSlug, scope);
|
|
@@ -2142,28 +2182,121 @@ async function cloudSessionCookieCli() {
|
|
|
2142
2182
|
async function cmdCloudInstall(db, slug) {
|
|
2143
2183
|
if (!slug) fail("usage: agentlas cloud install <slug>");
|
|
2144
2184
|
const listing = await fetchCloudManifestCli(slug);
|
|
2145
|
-
if (!listing) fail(`Hub agent
|
|
2185
|
+
if (!listing) fail(`Hub agent not found: ${slug}`);
|
|
2146
2186
|
if (listing.delivery && listing.delivery.mode === "call_only") {
|
|
2147
|
-
fail(
|
|
2187
|
+
fail(`This Hub agent is call-only and cannot be installed from source. Run: agentlas call ${slug}`);
|
|
2148
2188
|
}
|
|
2149
2189
|
const agent = persistCloudListingCli(db, listing);
|
|
2150
2190
|
out(`✓ Hub installed ${agent.slug} — ${agent.name}`);
|
|
2151
2191
|
if (agent.localPath) out(` files: ${agent.localPath}`);
|
|
2152
2192
|
}
|
|
2153
2193
|
|
|
2194
|
+
// ── Hub 플러그인 설치 ────────────────────────────────────────────────────────
|
|
2195
|
+
// 서버는 처음부터 준비돼 있었다: /api/plugins/<slug>가 agentlas.plugin/v1 매니페스트를 주고,
|
|
2196
|
+
// 그 라우트 주석이 이 CLI(`agentlas plugin add <slug>`)를 소비자로 지목한다. 그런데 이 명령이
|
|
2197
|
+
// 구현된 적이 없어서, 카탈로그 146개가 전부 "존재하지 않는 설치 명령"을 광고하고 있었다.
|
|
2198
|
+
// (`agentlas install`은 marketplace.get_manifest{kind:"agent"} 고정이라 플러그인엔 안 먹는다.)
|
|
2199
|
+
async function fetchPluginManifestCli(slug) {
|
|
2200
|
+
const base = (process.env.AGENTLAS_WEB_BASE_URL || "https://agentlas.cloud").replace(/\/$/, "");
|
|
2201
|
+
const resp = await fetchHubCli(`${base}/api/plugins/${encodeURIComponent(slug)}`, {
|
|
2202
|
+
headers: { accept: "application/json" },
|
|
2203
|
+
});
|
|
2204
|
+
if (resp.status === 404) return null;
|
|
2205
|
+
if (!resp.ok) fail(`plugin lookup failed with HTTP ${resp.status}`);
|
|
2206
|
+
const manifest = parseHubJsonCli(resp, "plugin manifest");
|
|
2207
|
+
if (!manifest || manifest.schema !== "agentlas.plugin/v1") {
|
|
2208
|
+
fail(`Unexpected plugin manifest schema for ${slug}.`);
|
|
2209
|
+
}
|
|
2210
|
+
return manifest;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
/** 매니페스트의 mcp[] 항목을 mcp_servers 행으로 정규화. stdio(command)와 remote(url)를 구분한다. */
|
|
2214
|
+
function pluginMcpRowCli(slug, entry, index) {
|
|
2215
|
+
const source = typeof entry?.source === "string" ? entry.source.trim() : "";
|
|
2216
|
+
const name = (typeof entry?.name === "string" && entry.name.trim()) || `${slug}-${index + 1}`;
|
|
2217
|
+
const remote = /^https?:\/\//i.test(source);
|
|
2218
|
+
// 원격은 URL, stdio는 실행 커맨드다. 둘을 섞으면 codex config.toml 스키마 위반으로
|
|
2219
|
+
// 런타임이 통째로 죽는다(Runtime Doctor가 반복해서 잡던 사고 계열).
|
|
2220
|
+
if (!remote && !source) return null;
|
|
2221
|
+
const argv = remote ? [] : source.split(/\s+/).filter(Boolean);
|
|
2222
|
+
return {
|
|
2223
|
+
id: require("node:crypto").randomUUID(),
|
|
2224
|
+
catalogId: `hub:${slug}:${name}`,
|
|
2225
|
+
name,
|
|
2226
|
+
transport: remote ? "http" : "stdio",
|
|
2227
|
+
command: remote ? null : (argv[0] ?? null),
|
|
2228
|
+
argsJson: JSON.stringify(remote ? [] : argv.slice(1)),
|
|
2229
|
+
url: remote ? source : null,
|
|
2230
|
+
envKeysJson: JSON.stringify(
|
|
2231
|
+
Array.isArray(entry?.envKeys) ? entry.envKeys.filter((key) => typeof key === "string") : [],
|
|
2232
|
+
),
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
async function cmdPluginAdd(db, slug) {
|
|
2237
|
+
if (!slug) fail('usage: agentlas plugin add <slug> (run agentlas plugin list first)');
|
|
2238
|
+
const manifest = await fetchPluginManifestCli(slug);
|
|
2239
|
+
if (!manifest) fail(`Hub plugin not found: ${slug}`);
|
|
2240
|
+
const entries = Array.isArray(manifest.mcp) ? manifest.mcp : [];
|
|
2241
|
+
const rows = entries.map((entry, index) => pluginMcpRowCli(slug, entry, index)).filter(Boolean);
|
|
2242
|
+
if (!rows.length) {
|
|
2243
|
+
// 설치할 MCP 서버가 없으면 조용히 성공했다고 하지 않는다 — 사용자는 이 플러그인이
|
|
2244
|
+
// 붙었다고 믿고 도구를 기대하게 된다.
|
|
2245
|
+
fail(
|
|
2246
|
+
`${slug} ships no MCP server to install (skills-only or source-link plugin). ` +
|
|
2247
|
+
`Nothing was registered. See: ${manifest.source?.repo || manifest.source?.homepage || "the plugin page"}`,
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
let installed = 0;
|
|
2251
|
+
let reused = 0;
|
|
2252
|
+
for (const row of rows) {
|
|
2253
|
+
const existing = db.prepare("SELECT id FROM mcp_servers WHERE catalog_id = ? LIMIT 1").get(row.catalogId);
|
|
2254
|
+
if (existing) { reused += 1; continue; } // 멱등: 재설치가 중복 행을 만들지 않는다
|
|
2255
|
+
db.prepare(
|
|
2256
|
+
`INSERT INTO mcp_servers (id, catalog_id, name, name_en, transport, command, args_json, url, env_keys_json, enabled, installed_at)
|
|
2257
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`,
|
|
2258
|
+
).run(
|
|
2259
|
+
row.id, row.catalogId, row.name, row.name, row.transport,
|
|
2260
|
+
row.command, row.argsJson, row.url, row.envKeysJson, new Date().toISOString(),
|
|
2261
|
+
);
|
|
2262
|
+
installed += 1;
|
|
2263
|
+
}
|
|
2264
|
+
out(`✓ Plugin installed ${manifest.slug} — ${manifest.name}`);
|
|
2265
|
+
out(` MCP servers: ${installed} added${reused ? `, ${reused} already present` : ""}`);
|
|
2266
|
+
const authKind = manifest.auth?.kind;
|
|
2267
|
+
if (authKind && authKind !== "none") {
|
|
2268
|
+
out(` ⚠ Requires ${authKind} — set credentials before use (agentlas creds).`);
|
|
2269
|
+
}
|
|
2270
|
+
if (Array.isArray(manifest.skills) && manifest.skills.length) {
|
|
2271
|
+
out(` skills declared: ${manifest.skills.map((skill) => skill.name).filter(Boolean).join(", ")}`);
|
|
2272
|
+
}
|
|
2273
|
+
out(" Only full-access turns wire active stdio servers into the runtime (agentlas mcp).");
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
async function cmdPluginList() {
|
|
2277
|
+
const result = await callAgentlasMcpToolCli("marketplace.list_plugins", {});
|
|
2278
|
+
const plugins = (result && (result.plugins || result.results)) || [];
|
|
2279
|
+
if (!plugins.length) return out("No Hub plugins are available.");
|
|
2280
|
+
for (const plugin of plugins.slice(0, 60)) {
|
|
2281
|
+
out(`${String(plugin.slug || "").padEnd(32).slice(0, 32)} ${String(plugin.name || "").slice(0, 44)}`);
|
|
2282
|
+
}
|
|
2283
|
+
out("");
|
|
2284
|
+
out("Install: agentlas plugin add <slug>");
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2154
2287
|
async function callAgentlasMcpToolCli(name, args, { requireSession = false } = {}) {
|
|
2155
|
-
if (typeof fetch !== "function") fail("
|
|
2288
|
+
if (typeof fetch !== "function") fail("fetch is unavailable in this runtime (run through the app runtime).");
|
|
2156
2289
|
const base = process.env.AGENTLAS_MCP_BASE_URL || "https://agentlas.cloud/api/mcp/v1";
|
|
2157
2290
|
const headers = { "content-type": "application/json" };
|
|
2158
2291
|
const cookie = await cloudSessionCookieCli();
|
|
2159
|
-
if (requireSession && !cookie) fail("Agent Cloud
|
|
2292
|
+
if (requireSession && !cookie) fail("Agent Cloud sign-in is required. Run `agentlas login` first.");
|
|
2160
2293
|
if (cookie) headers.cookie = cookie;
|
|
2161
2294
|
const resp = await fetchHubCli(`${base.replace(/\/$/, "")}/tools/call`, {
|
|
2162
2295
|
method: "POST",
|
|
2163
2296
|
headers,
|
|
2164
2297
|
body: JSON.stringify({ method: name, params: { name, arguments: args || {} } }),
|
|
2165
2298
|
});
|
|
2166
|
-
if (!resp.ok) fail(`${name}
|
|
2299
|
+
if (!resp.ok) fail(`${name} failed with HTTP ${resp.status}`);
|
|
2167
2300
|
const json = parseHubJsonCli(resp, name);
|
|
2168
2301
|
if (json.error) fail(`${name}: ${json.error.message || "unknown error"}`);
|
|
2169
2302
|
return json.result || null;
|
|
@@ -7643,38 +7776,320 @@ function contextLine(json) {
|
|
|
7643
7776
|
return "";
|
|
7644
7777
|
}
|
|
7645
7778
|
}
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7779
|
+
const AGENTLAS_PROJECT_STATE_IGNORE_START = "# >>> agentlas local project state >>>";
|
|
7780
|
+
const AGENTLAS_PROJECT_STATE_IGNORE_END = "# <<< agentlas local project state <<<";
|
|
7781
|
+
const AGENTLAS_GITIGNORE_MAX_BYTES = 1024 * 1024;
|
|
7782
|
+
const projectBootstrapStates = new Map();
|
|
7783
|
+
|
|
7784
|
+
function terminalProjectCandidateCli(projectPath) {
|
|
7650
7785
|
try {
|
|
7651
|
-
const
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7786
|
+
const root = path.resolve(projectPath || process.cwd());
|
|
7787
|
+
const unsafe = new Set([
|
|
7788
|
+
path.resolve(os.homedir()),
|
|
7789
|
+
path.parse(root).root,
|
|
7790
|
+
path.resolve(userDataDir()),
|
|
7791
|
+
path.resolve(runCwd()),
|
|
7792
|
+
]);
|
|
7793
|
+
if (unsafe.has(root)) return null;
|
|
7794
|
+
const stat = fs.statSync(root);
|
|
7795
|
+
if (!stat.isDirectory()) return null;
|
|
7796
|
+
return root;
|
|
7797
|
+
} catch {
|
|
7798
|
+
return null;
|
|
7799
|
+
}
|
|
7800
|
+
}
|
|
7801
|
+
|
|
7802
|
+
function assertNoSymlinkInAgentlasStateCli(stateDir) {
|
|
7803
|
+
const pending = [stateDir];
|
|
7804
|
+
let visited = 0;
|
|
7805
|
+
while (pending.length && visited < 4096) {
|
|
7806
|
+
const current = pending.pop();
|
|
7807
|
+
visited += 1;
|
|
7808
|
+
let stat;
|
|
7809
|
+
try { stat = fs.lstatSync(current); } catch (error) {
|
|
7810
|
+
if (error && error.code === "ENOENT") continue;
|
|
7811
|
+
throw error;
|
|
7659
7812
|
}
|
|
7660
|
-
if (
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
activatedAt = now;
|
|
7813
|
+
if (stat.isSymbolicLink()) throw new Error(".agentlas local state must not contain symbolic links");
|
|
7814
|
+
if (stat.isDirectory()) {
|
|
7815
|
+
for (const entry of fs.readdirSync(current)) pending.push(path.join(current, entry));
|
|
7664
7816
|
}
|
|
7665
|
-
|
|
7666
|
-
|
|
7817
|
+
}
|
|
7818
|
+
if (pending.length) throw new Error(".agentlas local state exceeds the safe bootstrap inspection limit");
|
|
7667
7819
|
}
|
|
7668
|
-
|
|
7669
|
-
function
|
|
7820
|
+
|
|
7821
|
+
function readRegularUtf8FileNoFollowCli(filePath, maxBytes = AGENTLAS_GITIGNORE_MAX_BYTES) {
|
|
7822
|
+
let before;
|
|
7823
|
+
try { before = fs.lstatSync(filePath); } catch (error) {
|
|
7824
|
+
if (error && error.code === "ENOENT") return { exists: false, content: "", mode: 0o644, stat: null };
|
|
7825
|
+
throw error;
|
|
7826
|
+
}
|
|
7827
|
+
if (before.isSymbolicLink() || !before.isFile()) throw new Error(".gitignore must be a regular non-symbolic-link file");
|
|
7828
|
+
if (before.size > maxBytes) throw new Error(`.gitignore exceeds the ${maxBytes}-byte safe bootstrap limit`);
|
|
7829
|
+
|
|
7830
|
+
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
7831
|
+
let fd;
|
|
7670
7832
|
try {
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
}
|
|
7833
|
+
fd = fs.openSync(filePath, fs.constants.O_RDONLY | noFollow);
|
|
7834
|
+
} catch (error) {
|
|
7835
|
+
if (process.platform !== "win32" || !noFollow || !["EINVAL", "ENOTSUP"].includes(error && error.code)) throw error;
|
|
7836
|
+
fd = fs.openSync(filePath, fs.constants.O_RDONLY);
|
|
7837
|
+
}
|
|
7838
|
+
try {
|
|
7839
|
+
const opened = fs.fstatSync(fd);
|
|
7840
|
+
if (!opened.isFile()) throw new Error(".gitignore changed type during bootstrap");
|
|
7841
|
+
if (opened.size > maxBytes) throw new Error(`.gitignore exceeds the ${maxBytes}-byte safe bootstrap limit`);
|
|
7842
|
+
if (
|
|
7843
|
+
Number.isFinite(before.dev) && Number.isFinite(before.ino) &&
|
|
7844
|
+
(before.dev !== opened.dev || before.ino !== opened.ino)
|
|
7845
|
+
) {
|
|
7846
|
+
throw new Error(".gitignore changed during bootstrap");
|
|
7847
|
+
}
|
|
7848
|
+
const chunks = [];
|
|
7849
|
+
let total = 0;
|
|
7850
|
+
while (total <= maxBytes) {
|
|
7851
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - total));
|
|
7852
|
+
const count = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
7853
|
+
if (!count) break;
|
|
7854
|
+
chunks.push(buffer.subarray(0, count));
|
|
7855
|
+
total += count;
|
|
7856
|
+
}
|
|
7857
|
+
if (total > maxBytes) throw new Error(`.gitignore exceeds the ${maxBytes}-byte safe bootstrap limit`);
|
|
7858
|
+
const after = fs.fstatSync(fd);
|
|
7859
|
+
if (after.size !== opened.size || after.mtimeMs !== opened.mtimeMs) throw new Error(".gitignore changed while it was being read");
|
|
7860
|
+
let content;
|
|
7861
|
+
try { content = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, total)); } catch {
|
|
7862
|
+
throw new Error(".gitignore must contain valid UTF-8 text");
|
|
7863
|
+
}
|
|
7864
|
+
return { exists: true, content, mode: before.mode & 0o777, stat: before };
|
|
7865
|
+
} finally {
|
|
7866
|
+
fs.closeSync(fd);
|
|
7867
|
+
}
|
|
7868
|
+
}
|
|
7869
|
+
|
|
7870
|
+
function assertFileSnapshotUnchangedCli(filePath, snapshot) {
|
|
7871
|
+
if (!snapshot.exists) {
|
|
7872
|
+
try {
|
|
7873
|
+
fs.lstatSync(filePath);
|
|
7874
|
+
throw new Error(".gitignore appeared during bootstrap");
|
|
7875
|
+
} catch (error) {
|
|
7876
|
+
if (error && error.code === "ENOENT") return;
|
|
7877
|
+
throw error;
|
|
7878
|
+
}
|
|
7879
|
+
}
|
|
7880
|
+
const current = fs.lstatSync(filePath);
|
|
7881
|
+
if (current.isSymbolicLink() || !current.isFile()) throw new Error(".gitignore changed type during bootstrap");
|
|
7882
|
+
const original = snapshot.stat;
|
|
7883
|
+
if (
|
|
7884
|
+
!original || current.dev !== original.dev || current.ino !== original.ino ||
|
|
7885
|
+
current.size !== original.size || current.mtimeMs !== original.mtimeMs
|
|
7886
|
+
) {
|
|
7887
|
+
throw new Error(".gitignore changed during bootstrap");
|
|
7888
|
+
}
|
|
7889
|
+
}
|
|
7890
|
+
|
|
7891
|
+
function replaceRegularFileCli(tempPath, destinationPath, snapshot) {
|
|
7892
|
+
try {
|
|
7893
|
+
fs.renameSync(tempPath, destinationPath);
|
|
7894
|
+
return;
|
|
7895
|
+
} catch (error) {
|
|
7896
|
+
if (process.platform !== "win32" || !snapshot.exists || !["EEXIST", "EPERM", "EACCES"].includes(error && error.code)) {
|
|
7897
|
+
throw error;
|
|
7898
|
+
}
|
|
7899
|
+
}
|
|
7900
|
+
|
|
7901
|
+
// Windows can reject replacement of an existing file. Keep a same-directory
|
|
7902
|
+
// rollback copy so an interrupted replacement never silently loses user rules.
|
|
7903
|
+
assertFileSnapshotUnchangedCli(destinationPath, snapshot);
|
|
7904
|
+
const backup = `${destinationPath}.agentlas-${process.pid}-${crypto.randomUUID()}.bak`;
|
|
7905
|
+
fs.renameSync(destinationPath, backup);
|
|
7906
|
+
try {
|
|
7907
|
+
fs.renameSync(tempPath, destinationPath);
|
|
7908
|
+
} catch (error) {
|
|
7909
|
+
try {
|
|
7910
|
+
if (!fs.existsSync(destinationPath)) fs.renameSync(backup, destinationPath);
|
|
7911
|
+
} catch { /* preserve the original error and leave the backup recoverable */ }
|
|
7912
|
+
throw error;
|
|
7913
|
+
}
|
|
7914
|
+
try { fs.unlinkSync(backup); } catch { /* a harmless rollback copy may remain on locked Windows hosts */ }
|
|
7915
|
+
}
|
|
7916
|
+
|
|
7917
|
+
function ensureAgentlasProjectStateIgnoreCli(projectPath) {
|
|
7918
|
+
const root = terminalProjectCandidateCli(projectPath);
|
|
7919
|
+
if (!root) throw new Error("refusing to initialize an unsafe Agentlas project root");
|
|
7920
|
+
const stateDir = path.join(root, ".agentlas");
|
|
7921
|
+
let stateExists = false;
|
|
7922
|
+
try {
|
|
7923
|
+
const state = fs.lstatSync(stateDir);
|
|
7924
|
+
stateExists = true;
|
|
7925
|
+
if (state.isSymbolicLink() || !state.isDirectory()) throw new Error(".agentlas must be a real directory");
|
|
7926
|
+
assertNoSymlinkInAgentlasStateCli(stateDir);
|
|
7927
|
+
} catch (error) {
|
|
7928
|
+
if (error && error.code !== "ENOENT") throw error;
|
|
7929
|
+
}
|
|
7930
|
+
|
|
7931
|
+
const gitignorePath = path.join(root, ".gitignore");
|
|
7932
|
+
const snapshot = readRegularUtf8FileNoFollowCli(gitignorePath);
|
|
7933
|
+
const existing = snapshot.content;
|
|
7934
|
+
const mode = snapshot.mode || 0o644;
|
|
7935
|
+
|
|
7936
|
+
let next = existing;
|
|
7937
|
+
const start = existing.indexOf(AGENTLAS_PROJECT_STATE_IGNORE_START);
|
|
7938
|
+
const end = start >= 0 ? existing.indexOf(AGENTLAS_PROJECT_STATE_IGNORE_END, start) : -1;
|
|
7939
|
+
if (start >= 0 && end >= 0) {
|
|
7940
|
+
const blockEnd = end + AGENTLAS_PROJECT_STATE_IGNORE_END.length;
|
|
7941
|
+
const block = existing.slice(start, blockEnd);
|
|
7942
|
+
if (!/^\.agentlas\/$/m.test(block)) {
|
|
7943
|
+
next = `${existing.slice(0, start)}${block.replace(AGENTLAS_PROJECT_STATE_IGNORE_START, `${AGENTLAS_PROJECT_STATE_IGNORE_START}\n.agentlas/`)}${existing.slice(blockEnd)}`;
|
|
7944
|
+
}
|
|
7945
|
+
} else {
|
|
7946
|
+
const block = `${AGENTLAS_PROJECT_STATE_IGNORE_START}\n.agentlas/\n${AGENTLAS_PROJECT_STATE_IGNORE_END}\n`;
|
|
7947
|
+
next = existing.trimEnd() ? `${existing.trimEnd()}\n\n${block}` : block;
|
|
7948
|
+
}
|
|
7949
|
+
if (next !== existing) {
|
|
7950
|
+
const temp = path.join(root, `.gitignore.agentlas-${process.pid}-${crypto.randomUUID()}.tmp`);
|
|
7951
|
+
fs.writeFileSync(temp, next.endsWith("\n") ? next : `${next}\n`, { encoding: "utf8", mode, flag: "wx" });
|
|
7952
|
+
try {
|
|
7953
|
+
assertFileSnapshotUnchangedCli(gitignorePath, snapshot);
|
|
7954
|
+
replaceRegularFileCli(temp, gitignorePath, snapshot);
|
|
7955
|
+
} catch (error) {
|
|
7956
|
+
try { fs.unlinkSync(temp); } catch { /* ignore */ }
|
|
7957
|
+
throw error;
|
|
7958
|
+
}
|
|
7959
|
+
}
|
|
7960
|
+
if (!stateExists) fs.mkdirSync(stateDir, { recursive: false, mode: 0o700 });
|
|
7961
|
+
assertNoSymlinkInAgentlasStateCli(stateDir);
|
|
7962
|
+
try { fs.chmodSync(stateDir, 0o700); } catch { /* Windows/best effort */ }
|
|
7963
|
+
}
|
|
7964
|
+
|
|
7965
|
+
function hardenAgentlasProjectStateCli(projectPath) {
|
|
7966
|
+
const root = terminalProjectCandidateCli(projectPath);
|
|
7967
|
+
if (!root) return;
|
|
7968
|
+
const stateDir = path.join(root, ".agentlas");
|
|
7969
|
+
const pending = [stateDir];
|
|
7970
|
+
let visited = 0;
|
|
7971
|
+
while (pending.length && visited < 4096) {
|
|
7972
|
+
const current = pending.pop();
|
|
7973
|
+
visited += 1;
|
|
7974
|
+
try {
|
|
7975
|
+
const stat = fs.lstatSync(current);
|
|
7976
|
+
if (stat.isSymbolicLink()) continue;
|
|
7977
|
+
if (stat.isDirectory()) {
|
|
7978
|
+
try { fs.chmodSync(current, 0o700); } catch { /* Windows/best effort */ }
|
|
7979
|
+
for (const entry of fs.readdirSync(current)) pending.push(path.join(current, entry));
|
|
7980
|
+
} else if (stat.isFile()) {
|
|
7981
|
+
try { fs.chmodSync(current, 0o600); } catch { /* Windows/best effort */ }
|
|
7982
|
+
}
|
|
7983
|
+
} catch { /* disappearing files and ACL-only hosts are best effort */ }
|
|
7984
|
+
}
|
|
7985
|
+
}
|
|
7986
|
+
|
|
7987
|
+
function ensureCoreProjectCli(projectPath, options = {}) {
|
|
7988
|
+
const root = terminalProjectCandidateCli(projectPath);
|
|
7989
|
+
if (!root) throw new Error("Agentlas project bootstrap requires a real project directory");
|
|
7990
|
+
fs.accessSync(root, fs.constants.R_OK | fs.constants.W_OK);
|
|
7991
|
+
ensureAgentlasProjectStateIgnoreCli(root);
|
|
7992
|
+
const cached = projectBootstrapStates.get(root);
|
|
7993
|
+
if (cached && fs.existsSync(path.join(root, ".agentlas", "project-soul-memory.md"))) {
|
|
7994
|
+
return cached === "core";
|
|
7995
|
+
}
|
|
7996
|
+
projectBootstrapStates.delete(root);
|
|
7997
|
+
const coreRoot = resolveCoreRuntimeRoot(options.coreRoot);
|
|
7998
|
+
const hasCanonicalBootstrap = Boolean(
|
|
7999
|
+
coreRoot && fs.existsSync(path.join(coreRoot, "agentlas_cloud", "project_bootstrap.py")),
|
|
8000
|
+
);
|
|
8001
|
+
if (hasCanonicalBootstrap) {
|
|
8002
|
+
const result = captureCoreJsonSync(
|
|
8003
|
+
"agentlas_cloud",
|
|
8004
|
+
["project", "ensure", "--project", root, "--reason", options.reason || "terminal-first-contact"],
|
|
8005
|
+
{ cwd: root },
|
|
8006
|
+
coreRoot,
|
|
8007
|
+
);
|
|
8008
|
+
const canonical = Boolean(
|
|
8009
|
+
result
|
|
8010
|
+
&& result.schemaVersion === "agentlas.project-bootstrap.v1"
|
|
8011
|
+
&& ["active", "privacy_warning"].includes(result.status)
|
|
8012
|
+
&& result.mergeOnly === true
|
|
8013
|
+
&& result.privacyBlockInstalled === true
|
|
8014
|
+
&& result.privateModeCompliant === true
|
|
8015
|
+
&& Array.isArray(result.missing)
|
|
8016
|
+
&& result.missing.length === 0
|
|
8017
|
+
&& Array.isArray(result.overwritten)
|
|
8018
|
+
&& result.overwritten.length === 0
|
|
8019
|
+
&& Array.isArray(result.permissionIssues)
|
|
8020
|
+
&& result.permissionIssues.length === 0
|
|
8021
|
+
);
|
|
8022
|
+
if (canonical) {
|
|
8023
|
+
// Core owns the canonical seed. Terminal adds one intentionally broader
|
|
8024
|
+
// guard so future local memory files are private without a release update.
|
|
8025
|
+
ensureAgentlasProjectStateIgnoreCli(root);
|
|
8026
|
+
hardenAgentlasProjectStateCli(root);
|
|
8027
|
+
projectBootstrapStates.set(root, "core");
|
|
8028
|
+
return true;
|
|
8029
|
+
}
|
|
8030
|
+
throw new Error("Agentlas Core returned an incomplete project bootstrap contract");
|
|
8031
|
+
}
|
|
8032
|
+
// A just-updated Terminal can briefly see the previous Core. The legacy
|
|
8033
|
+
// merge-only seed remains local-only and Core is retried next process.
|
|
8034
|
+
ensureProjectMemoryCli(root);
|
|
8035
|
+
if (!fs.existsSync(path.join(root, ".agentlas"))) {
|
|
8036
|
+
throw new Error("Agentlas project bootstrap could not create private local state");
|
|
8037
|
+
}
|
|
8038
|
+
ensureAgentlasProjectStateIgnoreCli(root);
|
|
8039
|
+
hardenAgentlasProjectStateCli(root);
|
|
8040
|
+
projectBootstrapStates.set(root, "fallback");
|
|
8041
|
+
return false;
|
|
8042
|
+
}
|
|
8043
|
+
|
|
8044
|
+
// Passive checks never increment visits or touch the project. Activation is
|
|
8045
|
+
// reserved for an actual write/full Terminal execution or an explicit ensure.
|
|
8046
|
+
function recordCliFolderVisit(db, projectPath, options = {}) {
|
|
8047
|
+
const root = terminalProjectCandidateCli(projectPath);
|
|
8048
|
+
if (!root) return { activated: false };
|
|
8049
|
+
const activate = options.activate === true;
|
|
8050
|
+
try {
|
|
8051
|
+
if (!activate) {
|
|
8052
|
+
const row = tableExists(db, "folder_activity")
|
|
8053
|
+
? db.prepare("SELECT activated_at FROM folder_activity WHERE path=?").get(root)
|
|
8054
|
+
: null;
|
|
8055
|
+
return { activated: Boolean(row && row.activated_at) || fs.existsSync(path.join(root, ".agentlas")) };
|
|
8056
|
+
}
|
|
8057
|
+
|
|
8058
|
+
ensureCoreProjectCli(root, { reason: options.reason || "terminal-first-contact", coreRoot: options.coreRoot });
|
|
8059
|
+
if (!tableExists(db, "folder_activity")) return { activated: true };
|
|
8060
|
+
const now = new Date().toISOString();
|
|
8061
|
+
const row = db.prepare("SELECT visits FROM folder_activity WHERE path=?").get(root);
|
|
8062
|
+
if (row) {
|
|
8063
|
+
db.prepare("UPDATE folder_activity SET visits=?, activated_at=COALESCE(activated_at,?), last_seen=? WHERE path=?")
|
|
8064
|
+
.run(Number(row.visits || 0) + 1, now, now, root);
|
|
8065
|
+
} else {
|
|
8066
|
+
db.prepare("INSERT INTO folder_activity (path, visits, activated_at, first_seen, last_seen) VALUES (?,?,?,?,?)")
|
|
8067
|
+
.run(root, 1, now, now, now);
|
|
8068
|
+
}
|
|
8069
|
+
return { activated: true };
|
|
8070
|
+
} catch (error) {
|
|
8071
|
+
// An activation failure can mean that the project-local privacy boundary
|
|
8072
|
+
// could not be established (for example, a symlinked or oversized
|
|
8073
|
+
// .gitignore). Never continue a write/full execution in that state.
|
|
8074
|
+
if (activate) throw error;
|
|
8075
|
+
return { activated: false };
|
|
8076
|
+
}
|
|
7676
8077
|
}
|
|
7677
|
-
|
|
8078
|
+
|
|
8079
|
+
function activeProjectPath(db, options = {}) {
|
|
8080
|
+
const root = terminalProjectCandidateCli(options.projectPath || process.cwd());
|
|
8081
|
+
if (!root) return null;
|
|
8082
|
+
const result = recordCliFolderVisit(db, root, options);
|
|
8083
|
+
return result.activated ? root : null;
|
|
8084
|
+
}
|
|
8085
|
+
|
|
8086
|
+
function ensureTerminalProjectForExecutionCli(db, projectPath, permission = PERMISSION, reason = "terminal-first-contact") {
|
|
8087
|
+
const root = terminalProjectCandidateCli(projectPath);
|
|
8088
|
+
if (!root) return null;
|
|
8089
|
+
if (permission === "read") return activeProjectPath(db, { projectPath: root });
|
|
8090
|
+
return activeProjectPath(db, { projectPath: root, activate: true, reason });
|
|
8091
|
+
}
|
|
8092
|
+
function cliMemoryContext(db, projectPath, agentId = null) {
|
|
7678
8093
|
const sections = [];
|
|
7679
8094
|
const arch = loadArch();
|
|
7680
8095
|
ensureMemoryContextColumn(db);
|
|
@@ -7690,14 +8105,49 @@ function cliMemoryContext(db, projectPath) {
|
|
|
7690
8105
|
}
|
|
7691
8106
|
if (tableExists(db, "memory_entries")) {
|
|
7692
8107
|
try {
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
8108
|
+
// New writes are read through a scoped global<->project timeline. The
|
|
8109
|
+
// project key is a digest, and team/agent lanes additionally require the
|
|
8110
|
+
// current owner id, so project B cannot recall project A's local memory.
|
|
8111
|
+
const governed = terminalMemoryGovernance.listScopedTimeline(db, {
|
|
8112
|
+
projectPath,
|
|
8113
|
+
agentId,
|
|
8114
|
+
limit: 16,
|
|
8115
|
+
});
|
|
8116
|
+
const seen = new Set(governed.map((row) => row.id));
|
|
8117
|
+
// Legacy rows predate the timeline. Keep only intentional user-global
|
|
8118
|
+
// rows, this exact project, and this exact agent/team owner. In
|
|
8119
|
+
// particular, do not revive the old global team-memory leakage query.
|
|
8120
|
+
const legacy = projectPath
|
|
8121
|
+
? db.prepare(`
|
|
8122
|
+
SELECT id,kind,content,context_json,created_at
|
|
8123
|
+
FROM memory_entries
|
|
8124
|
+
WHERE superseded_at IS NULL AND (
|
|
8125
|
+
(scope='user_identity' AND project_path IS NULL)
|
|
8126
|
+
OR (scope='project' AND project_path=?)
|
|
8127
|
+
OR (scope IN ('team_memory','agent_team','agent_repo') AND agent_id=? AND (project_path IS NULL OR project_path=?))
|
|
8128
|
+
)
|
|
8129
|
+
ORDER BY created_at DESC LIMIT 16
|
|
8130
|
+
`).all(projectPath, agentId, projectPath)
|
|
8131
|
+
: db.prepare(`
|
|
8132
|
+
SELECT id,kind,content,context_json,created_at
|
|
8133
|
+
FROM memory_entries
|
|
8134
|
+
WHERE superseded_at IS NULL AND (
|
|
8135
|
+
(scope='user_identity' AND project_path IS NULL)
|
|
8136
|
+
OR (scope IN ('team_memory','agent_team','agent_repo') AND agent_id=? AND project_path IS NULL)
|
|
8137
|
+
)
|
|
8138
|
+
ORDER BY created_at DESC LIMIT 16
|
|
8139
|
+
`).all(agentId);
|
|
8140
|
+
const rows = [...governed, ...legacy.filter((row) => !seen.has(row.id))].slice(0, 16);
|
|
8141
|
+
if (rows.length) {
|
|
8142
|
+
sections.push(
|
|
8143
|
+
(projectPath ? "### Scoped global + current-project memory timeline\n" : "### Curated user-global memory\n") +
|
|
8144
|
+
rows.map((r) => `- [${r.kind}] ${r.content}${contextLine(r.context_json)}`).join("\n"),
|
|
8145
|
+
);
|
|
8146
|
+
}
|
|
7697
8147
|
} catch { /* ignore */ }
|
|
7698
8148
|
}
|
|
7699
8149
|
if (!sections.length) return "";
|
|
7700
|
-
return "## Agentlas memory (read before answering;
|
|
8150
|
+
return "## Agentlas memory (read before answering; governed scope recall)\n\n" + sections.join("\n\n");
|
|
7701
8151
|
}
|
|
7702
8152
|
function parseMemoryEventsCli(text) {
|
|
7703
8153
|
const heading = loadArch().eventsHeading;
|
|
@@ -7715,11 +8165,16 @@ function parseMemoryEventsCli(text) {
|
|
|
7715
8165
|
function curateCliReply(db, text, ctx) {
|
|
7716
8166
|
const { events, cleaned } = parseMemoryEventsCli(text);
|
|
7717
8167
|
const style = require("./agentlas-style.cjs");
|
|
8168
|
+
if (ctx && ctx.permission === "read") return style.sanitizeAssistantText(cleaned);
|
|
7718
8169
|
if (!events.length || !tableExists(db, "memory_entries")) return style.sanitizeAssistantText(cleaned);
|
|
7719
8170
|
ensureMemoryContextColumn(db);
|
|
7720
8171
|
const arch = loadArch();
|
|
7721
8172
|
const { randomUUID } = require("node:crypto");
|
|
7722
8173
|
const now = new Date().toISOString();
|
|
8174
|
+
const rememberCurated = (memory) => {
|
|
8175
|
+
if (!ctx || !Array.isArray(ctx.curatedMemories) || !memory) return;
|
|
8176
|
+
if (!ctx.curatedMemories.some((item) => item.id === memory.id)) ctx.curatedMemories.push(memory);
|
|
8177
|
+
};
|
|
7723
8178
|
for (const ev of events) {
|
|
7724
8179
|
const content = ev && typeof ev.content === "string" ? ev.content.trim() : "";
|
|
7725
8180
|
if (!content) continue;
|
|
@@ -7735,9 +8190,16 @@ function curateCliReply(db, text, ctx) {
|
|
|
7735
8190
|
const ppath = scope === "project" ? ctx.projectPath : null;
|
|
7736
8191
|
const requestContext = normalizeRequestContext(ev, ctx, ppath);
|
|
7737
8192
|
try {
|
|
7738
|
-
const dup = db.prepare("SELECT
|
|
7739
|
-
if (dup)
|
|
7740
|
-
|
|
8193
|
+
const dup = db.prepare("SELECT id,scope,kind,content,confidence,sensitivity,context_json FROM memory_entries WHERE scope=? AND kind=? AND lower(trim(content))=? AND superseded_at IS NULL AND (project_path IS ? OR project_path=?) LIMIT 1").get(scope, kind, content.toLowerCase(), ppath, ppath);
|
|
8194
|
+
if (dup) {
|
|
8195
|
+
rememberCurated({ ...dup, requestContext });
|
|
8196
|
+
continue;
|
|
8197
|
+
}
|
|
8198
|
+
const memoryId = randomUUID();
|
|
8199
|
+
const confidence = ev.confidence || "medium";
|
|
8200
|
+
const sensitivity = ev.sensitivity || "internal";
|
|
8201
|
+
db.prepare("INSERT INTO memory_entries (id,scope,kind,content,project_id,project_path,agent_id,chat_id,confidence,sensitivity,evidence_json,context_json,superseded_at,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,NULL,?)").run(memoryId, scope, kind, content, ctx.projectId || null, ppath, ctx.agentId || null, null, confidence, sensitivity, JSON.stringify(Array.isArray(ev.evidence_refs) ? ev.evidence_refs : []), JSON.stringify(requestContext), now);
|
|
8202
|
+
rememberCurated({ id: memoryId, scope, kind, content, confidence, sensitivity, requestContext });
|
|
7741
8203
|
logCli(ctx.projectPath, { action: "written", scope, kind, content, request_context: requestContext, at: now });
|
|
7742
8204
|
} catch { /* ignore */ }
|
|
7743
8205
|
}
|
|
@@ -7756,7 +8218,50 @@ function prefsLang() {
|
|
|
7756
8218
|
}
|
|
7757
8219
|
}
|
|
7758
8220
|
|
|
7759
|
-
|
|
8221
|
+
const TERMINAL_MEMORY_CORE_MAX_TOKENS = 150;
|
|
8222
|
+
const TERMINAL_MEMORY_CORE = [
|
|
8223
|
+
"## Memory governance",
|
|
8224
|
+
"End every completed reply with hidden `## Memory Events` plus fenced JSON:",
|
|
8225
|
+
'{"turn_id":"<stable-id>","observation":{"outcome":"completed","summary":"safe short outcome"},"candidates":[]}',
|
|
8226
|
+
"Candidates 0..N: memory_kind,content,suggested_scope,confidence.",
|
|
8227
|
+
"Scopes: user_global|team|agent|project|session|discard.",
|
|
8228
|
+
"No raw prompts/transcripts, secrets, logs, or absolute paths. Curator suggests; deterministic gates decide writes.",
|
|
8229
|
+
].join("\n");
|
|
8230
|
+
const MEMORY_DETAIL_RE = /\b(?:remember|memory|save this|record this|memory event)\b|기억|메모리|저장해|기록해|남겨/i;
|
|
8231
|
+
const CREDENTIAL_INDEX_RE = /\b(?:deploy|release|billing|auth|oauth|credential|api key|secret key|cloud)\b|배포|릴리스|출시|결제|인증|자격 증명|API\s*키|시크릿|클라우드/i;
|
|
8232
|
+
|
|
8233
|
+
function approximatePromptTokens(text) {
|
|
8234
|
+
return Math.ceil(Buffer.byteLength(String(text || ""), "utf8") / 3);
|
|
8235
|
+
}
|
|
8236
|
+
if (approximatePromptTokens(TERMINAL_MEMORY_CORE) > TERMINAL_MEMORY_CORE_MAX_TOKENS) {
|
|
8237
|
+
throw new Error("Terminal always-on memory core exceeds 150 tokens");
|
|
8238
|
+
}
|
|
8239
|
+
|
|
8240
|
+
function memoryEmitterPromptFor(request, arch = loadArch(), turnId = null, permission = "write") {
|
|
8241
|
+
const stableId = String(turnId || "").replace(/[^A-Za-z0-9:._-]/g, "").slice(0, 160);
|
|
8242
|
+
let prompt = TERMINAL_MEMORY_CORE;
|
|
8243
|
+
if (stableId) prompt += `\nUse turn_id=${stableId}. permission=${permission === "read" ? "receipt-only" : "curated-write"}.`;
|
|
8244
|
+
if (!MEMORY_DETAIL_RE.test(String(request || ""))) return prompt;
|
|
8245
|
+
const kinds = Array.isArray(arch?.kinds) && arch.kinds.length ? arch.kinds.join("|") : "fact|decision|preference|risk|procedure";
|
|
8246
|
+
prompt += [
|
|
8247
|
+
"",
|
|
8248
|
+
`Allowed memory_kind: ${kinds}.`,
|
|
8249
|
+
"Global requires explicit owner authorization; suggest only, never promote.",
|
|
8250
|
+
"Do not emit request_context; put only a safe, short outcome in observation.",
|
|
8251
|
+
].join("\n");
|
|
8252
|
+
return prompt;
|
|
8253
|
+
}
|
|
8254
|
+
|
|
8255
|
+
function credentialIndexReminderFor(request) {
|
|
8256
|
+
if (!CREDENTIAL_INDEX_RE.test(String(request || ""))) return "";
|
|
8257
|
+
return [
|
|
8258
|
+
"## Local credential lookup (triggered)",
|
|
8259
|
+
"Before saying a deploy, release, billing, auth, API, or cloud credential is missing, read `.agentlas/local-credentials.map.json` and the Local Credential Index in `.agentlas/project-soul-memory.md`.",
|
|
8260
|
+
"Use only env names and local relative references; never copy credential values into memory or output.",
|
|
8261
|
+
].join("\n");
|
|
8262
|
+
}
|
|
8263
|
+
|
|
8264
|
+
function augmentSystem(db, baseSystem, ctx, withEmitter, request = "") {
|
|
7760
8265
|
const arch = loadArch();
|
|
7761
8266
|
let sys = baseSystem || "";
|
|
7762
8267
|
// 언어/말투 지시를 맨 앞에 둔다. imported/cloud/company agents도 같은 전역 계약을 따른다.
|
|
@@ -7764,12 +8269,133 @@ function augmentSystem(db, baseSystem, ctx, withEmitter) {
|
|
|
7764
8269
|
sys = langDirective(lang) + (sys ? "\n\n" + sys : "");
|
|
7765
8270
|
const connectionSkill = loadGlobalConnectionSkill();
|
|
7766
8271
|
if (connectionSkill) sys += "\n\n" + connectionSkill;
|
|
7767
|
-
const mem = cliMemoryContext(db, ctx && ctx.projectPath);
|
|
8272
|
+
const mem = cliMemoryContext(db, ctx && ctx.projectPath, ctx && ctx.agentId);
|
|
7768
8273
|
if (mem) sys += "\n\n" + mem;
|
|
7769
|
-
if (withEmitter
|
|
8274
|
+
if (withEmitter) {
|
|
8275
|
+
sys += "\n\n" + memoryEmitterPromptFor(request, arch, ctx && ctx.turnId, ctx && ctx.permission);
|
|
8276
|
+
const credentialReminder = credentialIndexReminderFor(request);
|
|
8277
|
+
if (credentialReminder) sys += "\n\n" + credentialReminder;
|
|
8278
|
+
}
|
|
7770
8279
|
return sys;
|
|
7771
8280
|
}
|
|
7772
8281
|
|
|
8282
|
+
function curatorRuntimeDirCli() {
|
|
8283
|
+
const root = path.join(userDataDir(), "memory-governance", "curator-runtime");
|
|
8284
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
8285
|
+
try { fs.chmodSync(path.dirname(root), 0o700); } catch { /* Windows/ACL-only host */ }
|
|
8286
|
+
try { fs.chmodSync(root, 0o700); } catch { /* Windows/ACL-only host */ }
|
|
8287
|
+
return root;
|
|
8288
|
+
}
|
|
8289
|
+
|
|
8290
|
+
function curatorRuntimeEnvCli(source = process.env) {
|
|
8291
|
+
// The semantic Curator has no tools and receives only pre-gated candidates.
|
|
8292
|
+
// Keep its process environment equally narrow: subscription CLIs can locate
|
|
8293
|
+
// their normal file-backed auth, but project/provider secret env is absent.
|
|
8294
|
+
const allowed = new Set([
|
|
8295
|
+
"PATH", "HOME", "USER", "LOGNAME", "SHELL", "TMPDIR", "TMP", "TEMP",
|
|
8296
|
+
"LANG", "LC_ALL", "LC_CTYPE", "TERM", "COLORTERM", "NO_COLOR",
|
|
8297
|
+
"CODEX_HOME", "CLAUDE_CONFIG_DIR", "XDG_CONFIG_HOME", "USERPROFILE",
|
|
8298
|
+
"APPDATA", "LOCALAPPDATA", "SYSTEMROOT", "SystemRoot", "COMSPEC", "ComSpec", "PATHEXT",
|
|
8299
|
+
]);
|
|
8300
|
+
const env = {};
|
|
8301
|
+
for (const [key, value] of Object.entries(source || {})) {
|
|
8302
|
+
if (allowed.has(key) || key.startsWith("LC_")) env[key] = value;
|
|
8303
|
+
}
|
|
8304
|
+
env.AGENTLAS_MEMORY_CURATOR = "1";
|
|
8305
|
+
return env;
|
|
8306
|
+
}
|
|
8307
|
+
|
|
8308
|
+
function ensureGeminiNoToolsPolicyCli() {
|
|
8309
|
+
const dir = curatorRuntimeDirCli();
|
|
8310
|
+
const file = path.join(dir, "gemini-no-tools-policy.toml");
|
|
8311
|
+
const content = [
|
|
8312
|
+
"# Managed by Agentlas Terminal for the semantic Memory Curator.",
|
|
8313
|
+
"[[rule]]",
|
|
8314
|
+
'toolName = "*"',
|
|
8315
|
+
'decision = "deny"',
|
|
8316
|
+
"priority = 999",
|
|
8317
|
+
"",
|
|
8318
|
+
].join("\n");
|
|
8319
|
+
let current = null;
|
|
8320
|
+
try { current = fs.readFileSync(file, "utf8"); } catch { /* first write */ }
|
|
8321
|
+
if (current !== content) {
|
|
8322
|
+
const temp = path.join(dir, `.gemini-no-tools-policy.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
8323
|
+
fs.writeFileSync(temp, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
8324
|
+
fs.renameSync(temp, file);
|
|
8325
|
+
}
|
|
8326
|
+
try { fs.chmodSync(file, 0o600); } catch { /* Windows/ACL-only host */ }
|
|
8327
|
+
return file;
|
|
8328
|
+
}
|
|
8329
|
+
|
|
8330
|
+
async function invokeMemoryCuratorCli(db, runtime, model, payload, systemPrompt) {
|
|
8331
|
+
const serialized = JSON.stringify(payload);
|
|
8332
|
+
if (
|
|
8333
|
+
terminalMemoryGovernance.hasSecret(serialized) ||
|
|
8334
|
+
terminalMemoryGovernance.hasAbsolutePath(serialized) ||
|
|
8335
|
+
terminalMemoryGovernance.hasTranscriptBody(serialized)
|
|
8336
|
+
) {
|
|
8337
|
+
throw new Error("Memory Curator payload failed the pre-invocation privacy gate");
|
|
8338
|
+
}
|
|
8339
|
+
if (runtime.mode === "cli") {
|
|
8340
|
+
return captureRuntime(runtime.kind, systemPrompt, serialized, {
|
|
8341
|
+
cwd: curatorRuntimeDirCli(),
|
|
8342
|
+
env: curatorRuntimeEnvCli(),
|
|
8343
|
+
permission: "read",
|
|
8344
|
+
model: model || runtime.model || null,
|
|
8345
|
+
effort: "low",
|
|
8346
|
+
authorityMode: "no-authority",
|
|
8347
|
+
noToolsPolicyPath: runtime.kind === "gemini" ? ensureGeminiNoToolsPolicyCli() : null,
|
|
8348
|
+
outputLimitBytes: 64 * 1024,
|
|
8349
|
+
timeoutConfig: { idleMs: 60_000, totalMs: 120_000, killGraceMs: 2_000 },
|
|
8350
|
+
});
|
|
8351
|
+
}
|
|
8352
|
+
return runApi(runtime.backend, model || runtime.model, systemPrompt, serialized);
|
|
8353
|
+
}
|
|
8354
|
+
|
|
8355
|
+
function beginMemoryTurnCli(db, prompt, ctx = {}) {
|
|
8356
|
+
return terminalMemoryGovernance.beginTurn(db, {
|
|
8357
|
+
prompt,
|
|
8358
|
+
projectPath: ctx.projectPath,
|
|
8359
|
+
agentId: ctx.agentId,
|
|
8360
|
+
permission: ctx.permission,
|
|
8361
|
+
surface: ctx.surface || "terminal-normal-turn",
|
|
8362
|
+
conversationRef: ctx.conversationRef,
|
|
8363
|
+
priorContextDigest: ctx.priorContextDigest,
|
|
8364
|
+
stableTurnId: ctx.stableTurnId || process.env.AGENTLAS_TURN_ID,
|
|
8365
|
+
});
|
|
8366
|
+
}
|
|
8367
|
+
|
|
8368
|
+
async function completeMemoryTurnCli(db, text, ctx, runtime, options = {}) {
|
|
8369
|
+
const turnId = ctx?.memoryTurn?.turnId || ctx?.turnId;
|
|
8370
|
+
const arch = loadArch();
|
|
8371
|
+
const runtimeInfo = runtime || {};
|
|
8372
|
+
const completion = {
|
|
8373
|
+
turnId,
|
|
8374
|
+
mainOutput: text,
|
|
8375
|
+
requestText: options.requestText,
|
|
8376
|
+
permission: ctx && ctx.permission,
|
|
8377
|
+
projectPath: ctx && ctx.projectPath,
|
|
8378
|
+
agentId: ctx && ctx.agentId,
|
|
8379
|
+
eventsHeading: arch.eventsHeading,
|
|
8380
|
+
outcome: options.outcome || "completed",
|
|
8381
|
+
coreFiles: {
|
|
8382
|
+
memoryDir: arch.memoryDir || ".agentlas",
|
|
8383
|
+
ticketFile: arch.memoryTicketsFile || "memory-tickets.jsonl",
|
|
8384
|
+
decisionFile: arch.curatorDecisionsFile || "curator-decisions.jsonl",
|
|
8385
|
+
},
|
|
8386
|
+
};
|
|
8387
|
+
if (options.invokeCurator !== false) {
|
|
8388
|
+
completion.invokeCurator = (payload, systemPrompt) => invokeMemoryCuratorCli(
|
|
8389
|
+
db,
|
|
8390
|
+
runtimeInfo,
|
|
8391
|
+
options.model || runtimeInfo.model || null,
|
|
8392
|
+
payload,
|
|
8393
|
+
systemPrompt,
|
|
8394
|
+
);
|
|
8395
|
+
}
|
|
8396
|
+
return terminalMemoryGovernance.completeTurn(db, completion);
|
|
8397
|
+
}
|
|
8398
|
+
|
|
7773
8399
|
function loadGlobalConnectionSkill() {
|
|
7774
8400
|
try {
|
|
7775
8401
|
return require("../dist/electron/runtime/global-skill.js").GLOBAL_CONNECTION_SKILL || "";
|
|
@@ -7791,19 +8417,78 @@ const RUNTIME_BIN = {
|
|
|
7791
8417
|
|
|
7792
8418
|
// 활성 런타임 → 실행 방식 결정. CLI(claude/codex/gemini) 또는 API(BYOK/Ollama).
|
|
7793
8419
|
function resolveRuntime(db, override) {
|
|
8420
|
+
const ar = activeRuntime(db);
|
|
8421
|
+
const activeCli = ar && RUNTIME_BIN[ar.kind]
|
|
8422
|
+
? {
|
|
8423
|
+
mode: "cli",
|
|
8424
|
+
kind: ar.kind,
|
|
8425
|
+
model: ar.model || null,
|
|
8426
|
+
capabilities: ["code", "tools", ...(ar.long_context ? ["long-context"] : [])],
|
|
8427
|
+
efforts: [],
|
|
8428
|
+
}
|
|
8429
|
+
: null;
|
|
7794
8430
|
if (override) {
|
|
7795
|
-
if (!RUNTIME_BIN[override]) fail(
|
|
7796
|
-
return { mode: "cli", kind: override };
|
|
8431
|
+
if (!RUNTIME_BIN[override]) fail(`Unknown runtime: ${override} (claude-code|codex|gemini)`);
|
|
8432
|
+
return activeCli && activeCli.kind === override ? activeCli : { mode: "cli", kind: override };
|
|
7797
8433
|
}
|
|
7798
|
-
|
|
7799
|
-
if (ar && RUNTIME_BIN[ar.kind]) return { mode: "cli", kind: ar.kind };
|
|
8434
|
+
if (activeCli) return activeCli;
|
|
7800
8435
|
if (ar && ar.kind === "byok" && ar.backend) return { mode: "api", backend: ar.backend, model: ar.model };
|
|
7801
8436
|
if (ar && ar.kind === "ollama") return { mode: "api", backend: "ollama", model: ar.model };
|
|
7802
8437
|
// 폴백: 설치된 CLI 탐지
|
|
7803
8438
|
for (const kind of Object.keys(RUNTIME_BIN)) {
|
|
7804
8439
|
if (which(RUNTIME_BIN[kind])) return { mode: "cli", kind };
|
|
7805
8440
|
}
|
|
7806
|
-
fail("
|
|
8441
|
+
fail("No runtime is available. Install a CLI (claude/codex/gemini) or configure an API key/Ollama in the app.");
|
|
8442
|
+
}
|
|
8443
|
+
|
|
8444
|
+
// Build the executable runtime inventory for the parent allocator. It is
|
|
8445
|
+
// intentionally local to this host: a Terminal/Codex/Claude plugin never
|
|
8446
|
+
// pretends it can schedule a runtime that is not installed and connected here.
|
|
8447
|
+
function listAvailableRuntimes(db, fallbackRuntime = null) {
|
|
8448
|
+
const routing = require("./agentlas-workload-routing.cjs");
|
|
8449
|
+
const active = fallbackRuntime || resolveRuntime(db);
|
|
8450
|
+
const candidates = [];
|
|
8451
|
+
const add = (runtime) => {
|
|
8452
|
+
if (!runtime) return;
|
|
8453
|
+
const key = runtime.mode === "cli" ? `cli:${runtime.kind}` : `api:${runtime.backend}:${runtime.model || ""}`;
|
|
8454
|
+
if (candidates.some((item) => item.key === key)) return;
|
|
8455
|
+
const discovered = routing.defaultAvailableModels(runtime);
|
|
8456
|
+
const availableModels = [...discovered];
|
|
8457
|
+
if (runtime.model && !availableModels.some((model) => model.id === runtime.model)) {
|
|
8458
|
+
availableModels.push({
|
|
8459
|
+
id: runtime.model,
|
|
8460
|
+
tier: runtime.modelTier || runtime.tier || null,
|
|
8461
|
+
capabilities: runtime.capabilities || [],
|
|
8462
|
+
contextWindow: runtime.contextWindow || null,
|
|
8463
|
+
efforts: runtime.efforts || [],
|
|
8464
|
+
description: runtime.modelDescription || "host-selected current model",
|
|
8465
|
+
});
|
|
8466
|
+
}
|
|
8467
|
+
candidates.push({ ...runtime, key, availableModels });
|
|
8468
|
+
};
|
|
8469
|
+
add(active);
|
|
8470
|
+
for (const kind of Object.keys(RUNTIME_BIN)) {
|
|
8471
|
+
if (!which(RUNTIME_BIN[kind])) continue;
|
|
8472
|
+
add({ mode: "cli", kind });
|
|
8473
|
+
}
|
|
8474
|
+
return candidates
|
|
8475
|
+
.filter((runtime) => runtime.availableModels.length)
|
|
8476
|
+
.map(({ key, ...runtime }, index) => ({ ...runtime, runtimeId: `runtime-${index + 1}` }));
|
|
8477
|
+
}
|
|
8478
|
+
|
|
8479
|
+
function currentRuntimeInventoryCli(db, runtime) {
|
|
8480
|
+
const candidates = listAvailableRuntimes(db, runtime);
|
|
8481
|
+
const current = candidates.find((candidate) =>
|
|
8482
|
+
candidate.mode === runtime.mode &&
|
|
8483
|
+
(runtime.mode === "cli"
|
|
8484
|
+
? candidate.kind === runtime.kind
|
|
8485
|
+
: candidate.backend === runtime.backend && candidate.model === runtime.model));
|
|
8486
|
+
if (current) return current;
|
|
8487
|
+
return {
|
|
8488
|
+
...runtime,
|
|
8489
|
+
runtimeId: "runtime-current",
|
|
8490
|
+
availableModels: workloadRouting.defaultAvailableModels(runtime),
|
|
8491
|
+
};
|
|
7807
8492
|
}
|
|
7808
8493
|
|
|
7809
8494
|
// ── API 러너 (BYOK / Ollama) — 비스트리밍, 최종 텍스트 반환 ──
|
|
@@ -7843,14 +8528,14 @@ function normalizeCustomApiBaseUrl(raw) {
|
|
|
7843
8528
|
try {
|
|
7844
8529
|
parsed = new URL(value);
|
|
7845
8530
|
} catch {
|
|
7846
|
-
throw new Error("Custom API base URL
|
|
8531
|
+
throw new Error("Custom API base URL is invalid.");
|
|
7847
8532
|
}
|
|
7848
8533
|
const host = parsed.hostname.toLowerCase();
|
|
7849
8534
|
const isLoopback = host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
7850
8535
|
const isPrivateLan =
|
|
7851
8536
|
/^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
|
|
7852
8537
|
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && (isLoopback || isPrivateLan))) {
|
|
7853
|
-
throw new Error("Custom API base URL
|
|
8538
|
+
throw new Error("Custom API base URL must use HTTPS or HTTP on localhost/LAN.");
|
|
7854
8539
|
}
|
|
7855
8540
|
return value.replace(/\/+$/, "");
|
|
7856
8541
|
}
|
|
@@ -7876,7 +8561,7 @@ function readCustomApiBaseUrl() {
|
|
|
7876
8561
|
raw = "";
|
|
7877
8562
|
}
|
|
7878
8563
|
} catch (e) {
|
|
7879
|
-
throw new Error(`Custom API base URL
|
|
8564
|
+
throw new Error(`Could not read the Custom API base URL from the shared database: ${(e && e.message) || e}`);
|
|
7880
8565
|
} finally {
|
|
7881
8566
|
try { if (db && typeof db.close === "function") db.close(); } catch { /* ignore close failure */ }
|
|
7882
8567
|
}
|
|
@@ -7892,22 +8577,22 @@ async function runApi(backend, model, system, prompt, options) {
|
|
|
7892
8577
|
options = options || {};
|
|
7893
8578
|
model = model || DEFAULT_API_MODEL[backend];
|
|
7894
8579
|
const fetchImpl = options.fetch || globalThis.fetch;
|
|
7895
|
-
if (typeof fetchImpl !== "function") throw new Error("
|
|
8580
|
+
if (typeof fetchImpl !== "function") throw new Error("fetch is unavailable in this runtime (run through the app runtime).");
|
|
7896
8581
|
if (backend === "ollama") {
|
|
7897
8582
|
const resp = await fetchImpl("http://127.0.0.1:11434/api/chat", {
|
|
7898
8583
|
method: "POST",
|
|
7899
8584
|
headers: { "content-type": "application/json" },
|
|
7900
8585
|
body: JSON.stringify({ model, stream: false, messages: [{ role: "system", content: system }, { role: "user", content: prompt }] }),
|
|
7901
8586
|
});
|
|
7902
|
-
if (!resp.ok) throw new Error(`Ollama ${resp.status} — 'ollama serve'
|
|
8587
|
+
if (!resp.ok) throw new Error(`Ollama ${resp.status} — run 'ollama serve' and check the model`);
|
|
7903
8588
|
const j = await resp.json();
|
|
7904
8589
|
return (j.message && j.message.content) || "";
|
|
7905
8590
|
}
|
|
7906
8591
|
const supported = backend === "anthropic" || backend === "openai" || backend === "google" ||
|
|
7907
8592
|
backend === "upstage" || backend === "custom" || !!ANTHROPIC_COMPAT_API[backend];
|
|
7908
|
-
if (!supported) throw new Error("
|
|
8593
|
+
if (!supported) throw new Error("Unsupported backend: " + backend);
|
|
7909
8594
|
const key = Object.prototype.hasOwnProperty.call(options, "apiKey") ? options.apiKey : await apiKey(backend);
|
|
7910
|
-
if (!key) throw new Error(`${backend} API
|
|
8595
|
+
if (!key) throw new Error(`${backend} API key is missing. Register it in App settings → BYOK.`);
|
|
7911
8596
|
|
|
7912
8597
|
const anthropicCompat = ANTHROPIC_COMPAT_API[backend];
|
|
7913
8598
|
if (backend === "anthropic" || anthropicCompat) {
|
|
@@ -7955,38 +8640,129 @@ async function runApi(backend, model, system, prompt, options) {
|
|
|
7955
8640
|
const c = j.candidates && j.candidates[0];
|
|
7956
8641
|
return (c && c.content && c.content.parts && c.content.parts[0] && c.content.parts[0].text) || "";
|
|
7957
8642
|
}
|
|
7958
|
-
throw new Error("
|
|
8643
|
+
throw new Error("Unsupported backend: " + backend);
|
|
7959
8644
|
}
|
|
7960
8645
|
|
|
7961
8646
|
// 1회 실행 — CLI면 spawn(스트리밍 stdout), API면 호출 후 텍스트 출력. 종료코드 반환.
|
|
7962
8647
|
// ctx = { projectPath, agentId } — 메모리 주입/큐레이션에 사용.
|
|
8648
|
+
function finalizeExperienceExecutionCli(db, input) {
|
|
8649
|
+
if (input.permission === "read") return null;
|
|
8650
|
+
if (!input.agentId) return null;
|
|
8651
|
+
let agent;
|
|
8652
|
+
try { agent = db.prepare("SELECT * FROM installed_agents WHERE id=?").get(input.agentId); }
|
|
8653
|
+
catch { return null; }
|
|
8654
|
+
if (!agent) return null;
|
|
8655
|
+
const exactBase = exactAgentBaseForExecution(db, agent, input.runtimeExperience);
|
|
8656
|
+
if (!exactBase) return null;
|
|
8657
|
+
const runtime = input.runtime || {};
|
|
8658
|
+
const provider = runtime.mode === "cli" ? runtime.kind : runtime.backend;
|
|
8659
|
+
const modelId = input.model || runtime.model || provider;
|
|
8660
|
+
const usage = input.usage || {};
|
|
8661
|
+
try {
|
|
8662
|
+
return terminalExperienceIntake.finalizeAgentExecution({
|
|
8663
|
+
db,
|
|
8664
|
+
userDataDir: userDataDir(),
|
|
8665
|
+
cwd: input.cwd || input.projectPath || projectCwd(),
|
|
8666
|
+
agent,
|
|
8667
|
+
exactBase,
|
|
8668
|
+
environment: { runtime: provider || "terminal", os: process.platform, arch: process.arch },
|
|
8669
|
+
model: { provider: provider || "terminal-runtime", modelId: modelId || "terminal-runtime" },
|
|
8670
|
+
mcp: (input.mcpServers || []).flatMap((server) => {
|
|
8671
|
+
const catalogId = server.catalog_id || server.catalogId;
|
|
8672
|
+
// A reviewed runtime allowlist proves approval, not that this turn's
|
|
8673
|
+
// child completed an MCP initialize/tool call. Do not inflate it into
|
|
8674
|
+
// connected evidence without an exact runtime signal.
|
|
8675
|
+
return catalogId ? [{ catalogId, status: "approved" }] : [];
|
|
8676
|
+
}),
|
|
8677
|
+
outcome: input.outcome,
|
|
8678
|
+
metrics: {
|
|
8679
|
+
promptTokens: usage.input_tokens || usage.prompt_tokens || 0,
|
|
8680
|
+
completionTokens: usage.output_tokens || usage.completion_tokens || 0,
|
|
8681
|
+
totalTokens: usage.total_tokens || 0,
|
|
8682
|
+
durationMs: input.durationMs || usage.duration_ms || 0,
|
|
8683
|
+
retryCount: 0,
|
|
8684
|
+
},
|
|
8685
|
+
curatedMemories: input.curatedMemories || [],
|
|
8686
|
+
taskHint: input.taskHint,
|
|
8687
|
+
taskSignatures: input.runtimeExperience?.taskSignatures || [],
|
|
8688
|
+
experiencePackReleaseId: input.runtimeExperience?.experiencePackReleaseIds?.[0] || null,
|
|
8689
|
+
locale: input.lang || prefsLang(),
|
|
8690
|
+
runId: input.runId,
|
|
8691
|
+
createdAt: input.createdAt,
|
|
8692
|
+
});
|
|
8693
|
+
} catch (error) {
|
|
8694
|
+
process.stderr.write(`▸ local Experience intake skipped · ${String((error && error.message) || error).slice(0, 180)}\n`);
|
|
8695
|
+
return null;
|
|
8696
|
+
}
|
|
8697
|
+
}
|
|
8698
|
+
|
|
7963
8699
|
async function executeOnce(db, system, prompt, override, ctx) {
|
|
7964
8700
|
ctx = ctx || { projectPath: null, agentId: null };
|
|
8701
|
+
const runStartedAt = Date.now();
|
|
8702
|
+
const memoryTurn = beginMemoryTurnCli(db, prompt, {
|
|
8703
|
+
...ctx,
|
|
8704
|
+
surface: ctx.surface || "terminal-one-shot",
|
|
8705
|
+
stableTurnId: ctx.turnId,
|
|
8706
|
+
});
|
|
8707
|
+
ctx.memoryTurn = memoryTurn;
|
|
8708
|
+
ctx.turnId = memoryTurn.turnId;
|
|
8709
|
+
const experienceRunId = `terminal-run:${memoryTurn.turnId}`;
|
|
8710
|
+
const curatedMemories = [];
|
|
8711
|
+
ctx.curatedMemories = curatedMemories;
|
|
7965
8712
|
if (!ctx.cwdAtRequest) ctx.cwdAtRequest = projectCwd();
|
|
8713
|
+
let memoryRuntime = null;
|
|
8714
|
+
let memorySettled = false;
|
|
8715
|
+
try {
|
|
7966
8716
|
let runtimeSystem = system;
|
|
7967
8717
|
let localExperienceContext = null;
|
|
7968
|
-
if (ctx.runtimeExperience && ctx.runtimeExperience.
|
|
8718
|
+
if (ctx.runtimeExperience?.disabled === true && ctx.runtimeExperience.observableReason) {
|
|
8719
|
+
process.stderr.write(`▸ local Experience skipped · ${ctx.runtimeExperience.observableReason}\n`);
|
|
8720
|
+
} else if (ctx.runtimeExperience && ctx.runtimeExperience.disabled !== true) {
|
|
7969
8721
|
const runtimeExperience = ctx.runtimeExperience;
|
|
7970
8722
|
const augmented = terminalExperienceExchange.augmentRuntimeSystemWithLocalExperience(system, {
|
|
7971
8723
|
userDataDir: userDataDir(),
|
|
7972
8724
|
cwd: ctx.projectPath || ctx.cwdAtRequest,
|
|
7973
8725
|
baseAgentReleaseId: runtimeExperience.baseAgentReleaseId,
|
|
7974
8726
|
agentDefinitionId: runtimeExperience.agentDefinitionId,
|
|
8727
|
+
experiencePackReleaseIds: runtimeExperience.experiencePackReleaseIds || [],
|
|
7975
8728
|
taskSignatures: runtimeExperience.taskSignatures || [],
|
|
7976
8729
|
environmentTags: Array.isArray(runtimeExperience.environmentTags) && runtimeExperience.environmentTags.length
|
|
7977
8730
|
? runtimeExperience.environmentTags
|
|
7978
8731
|
: terminalExperienceExchange.defaultEnvironmentTags(),
|
|
8732
|
+
reservedTokens: ctx.runtimeExperience?.tasteRuntimeOverlay?.estimatedTokens ?? 0,
|
|
7979
8733
|
});
|
|
7980
8734
|
runtimeSystem = augmented.systemPrompt;
|
|
7981
8735
|
localExperienceContext = augmented.experienceContext;
|
|
7982
8736
|
if (localExperienceContext.itemIds.length) {
|
|
7983
|
-
|
|
8737
|
+
const source = runtimeExperience.loadoutAuthority === "desktop-terminal-exact-loadout"
|
|
8738
|
+
? "Desktop-approved exact Experience"
|
|
8739
|
+
: "local Experience advisory";
|
|
8740
|
+
process.stderr.write(`▸ ${source} · ${localExperienceContext.itemIds.length} item(s) · ~${localExperienceContext.estimatedTokens} tokens · no server rental receipt\n`);
|
|
7984
8741
|
}
|
|
7985
8742
|
}
|
|
8743
|
+
const tasteTaskResolution = terminalExperienceExchange.deriveCanonicalTaskClasses(prompt);
|
|
8744
|
+
if (
|
|
8745
|
+
ctx.runtimeExperience?.tasteRuntimeOverlay &&
|
|
8746
|
+
desktopOntologyLoadout.tasteRuntimeOverlayMatchesTask(
|
|
8747
|
+
ctx.runtimeExperience.tasteRuntimeOverlay,
|
|
8748
|
+
tasteTaskResolution.taskIds,
|
|
8749
|
+
prompt,
|
|
8750
|
+
)
|
|
8751
|
+
) {
|
|
8752
|
+
const tasteDirective = desktopOntologyLoadout.renderTasteRuntimeDirective(
|
|
8753
|
+
ctx.runtimeExperience.tasteRuntimeOverlay,
|
|
8754
|
+
);
|
|
8755
|
+
runtimeSystem = `${runtimeSystem}\n\n${tasteDirective}`;
|
|
8756
|
+
process.stderr.write(
|
|
8757
|
+
`▸ Desktop-approved exact Taste · ${ctx.runtimeExperience.tasteRuntimeOverlay.releaseId} · ~${ctx.runtimeExperience.tasteRuntimeOverlay.estimatedTokens} tokens · session snapshot\n`,
|
|
8758
|
+
);
|
|
8759
|
+
}
|
|
7986
8760
|
const rt = resolveRuntime(db, override);
|
|
8761
|
+
memoryRuntime = rt;
|
|
7987
8762
|
if (rt.mode === "cli") {
|
|
7988
|
-
// 네이티브 CLI
|
|
7989
|
-
|
|
8763
|
+
// 네이티브 CLI에도 같은 Memory emitter를 주입하되 guard가 화면의 JSON 블록을 숨긴다.
|
|
8764
|
+
// 큐레이터가 만든 구조화 Memory만 성공 RunReceipt 이후 Experience intake로 전달된다.
|
|
8765
|
+
const sys = augmentSystem(db, runtimeSystem, ctx, true, prompt);
|
|
7990
8766
|
const cwd = ctx.projectPath || projectCwd();
|
|
7991
8767
|
const permission = ctx.permission || "write";
|
|
7992
8768
|
const env = await buildChildEnvCli(db, { ...ctx, cwd });
|
|
@@ -7994,40 +8770,300 @@ async function executeOnce(db, system, prompt, override, ctx) {
|
|
|
7994
8770
|
// one-shot(`agentlas "작업"`)도 REPL과 동일한 리치 렌더(⏺ 툴 / └ 결과 / 토큰)로 출력한다.
|
|
7995
8771
|
const { runNativeTurn } = require("./agentlas-native-host.cjs");
|
|
7996
8772
|
const { Ui } = require("./agentlas-ui.cjs");
|
|
8773
|
+
const { makeMemoryGuard } = require("./agentlas-repl.cjs");
|
|
7997
8774
|
const ui = new Ui({ lang: prefsLang() });
|
|
7998
8775
|
let mcpServers = [];
|
|
7999
8776
|
if (permission === "full") {
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8777
|
+
if (Array.isArray(ctx.mcpServers)) {
|
|
8778
|
+
// Build's reviewed host allowlist is authoritative, including the valid
|
|
8779
|
+
// empty list. Never fall back to every enabled registry row.
|
|
8780
|
+
mcpServers = ctx.mcpServers;
|
|
8781
|
+
} else {
|
|
8782
|
+
try {
|
|
8783
|
+
mcpServers = terminalAssets.readConsentedSystemMcpServers(db, { userDataDir: userDataDir() });
|
|
8784
|
+
} catch { /* ignore */ }
|
|
8785
|
+
}
|
|
8003
8786
|
}
|
|
8004
8787
|
ui.beginTurn();
|
|
8005
|
-
const
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8788
|
+
const memoryGuard = makeMemoryGuard(ui, loadArch().eventsHeading);
|
|
8789
|
+
let res;
|
|
8790
|
+
try {
|
|
8791
|
+
res = await runNativeTurn({
|
|
8792
|
+
kind: rt.kind,
|
|
8793
|
+
bin: which(RUNTIME_BIN[rt.kind]) || RUNTIME_BIN[rt.kind],
|
|
8794
|
+
prompt,
|
|
8795
|
+
systemPrompt: sys,
|
|
8796
|
+
cwd,
|
|
8797
|
+
permission,
|
|
8798
|
+
session: {},
|
|
8799
|
+
model: ctx.model || null,
|
|
8800
|
+
effort: ctx.effort || null,
|
|
8801
|
+
mcpServers,
|
|
8802
|
+
mcpAllowlistMode: ctx.mcpAllowlistMode,
|
|
8803
|
+
env,
|
|
8804
|
+
ui: memoryGuard,
|
|
8805
|
+
});
|
|
8806
|
+
} finally {
|
|
8807
|
+
ui.endTurn();
|
|
8808
|
+
}
|
|
8809
|
+
const nativeText = String(res.text || "");
|
|
8810
|
+
const memoryResult = await completeMemoryTurnCli(db, nativeText, ctx, rt, {
|
|
8811
|
+
model: ctx.model || rt.model,
|
|
8812
|
+
outcome: res.error ? "failed" : "succeeded",
|
|
8813
|
+
requestText: prompt,
|
|
8814
|
+
// Every failed runtime needs a deterministic receipt, but must not
|
|
8815
|
+
// recursively call the runtime that just failed.
|
|
8816
|
+
invokeCurator: !res.error,
|
|
8817
|
+
});
|
|
8818
|
+
memorySettled = true;
|
|
8819
|
+
for (const memory of memoryResult.curatedMemories || []) {
|
|
8820
|
+
if (memory && !curatedMemories.some((item) => item.id === memory.id)) curatedMemories.push(memory);
|
|
8821
|
+
}
|
|
8822
|
+
finalizeExperienceExecutionCli(db, {
|
|
8823
|
+
agentId: ctx.agentId,
|
|
8824
|
+
projectPath: ctx.projectPath,
|
|
8010
8825
|
cwd,
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
model:
|
|
8014
|
-
|
|
8826
|
+
runtime: rt,
|
|
8827
|
+
permission: ctx.permission,
|
|
8828
|
+
model: ctx.model || rt.model,
|
|
8829
|
+
runtimeExperience: ctx.runtimeExperience,
|
|
8015
8830
|
mcpServers,
|
|
8016
|
-
|
|
8017
|
-
|
|
8831
|
+
curatedMemories,
|
|
8832
|
+
taskHint: prompt,
|
|
8833
|
+
outcome: { status: res.error ? "failed" : "succeeded", failureCode: res.error ? "runtime-error" : null },
|
|
8834
|
+
usage: res.usage,
|
|
8835
|
+
durationMs: Date.now() - runStartedAt,
|
|
8836
|
+
runId: experienceRunId,
|
|
8837
|
+
lang: ctx.lang,
|
|
8018
8838
|
});
|
|
8019
|
-
ui.endTurn();
|
|
8020
8839
|
return res.error ? 1 : 0;
|
|
8021
8840
|
}
|
|
8022
8841
|
// API 경로 — emitter 동봉 → 답변에서 메모리 이벤트를 파싱·큐레이션하고 블록은 제거.
|
|
8023
|
-
const sys = augmentSystem(db, runtimeSystem, ctx, true);
|
|
8842
|
+
const sys = augmentSystem(db, runtimeSystem, ctx, true, prompt);
|
|
8024
8843
|
const env = await buildChildEnvCli(db, { ...ctx, cwd: ctx.cwd || projectCwd() });
|
|
8025
8844
|
Object.assign(process.env, env);
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8845
|
+
const selectedModel = ctx.model || rt.model;
|
|
8846
|
+
process.stderr.write(`▸ ${rt.backend}${selectedModel ? " · " + selectedModel : ""}\n`);
|
|
8847
|
+
let text;
|
|
8848
|
+
try {
|
|
8849
|
+
text = await runApi(rt.backend, selectedModel, sys, prompt);
|
|
8850
|
+
} catch (error) {
|
|
8851
|
+
finalizeExperienceExecutionCli(db, {
|
|
8852
|
+
agentId: ctx.agentId,
|
|
8853
|
+
projectPath: ctx.projectPath,
|
|
8854
|
+
cwd: ctx.cwd || projectCwd(),
|
|
8855
|
+
runtime: rt,
|
|
8856
|
+
permission: ctx.permission,
|
|
8857
|
+
model: selectedModel,
|
|
8858
|
+
runtimeExperience: ctx.runtimeExperience,
|
|
8859
|
+
curatedMemories,
|
|
8860
|
+
taskHint: prompt,
|
|
8861
|
+
outcome: { status: "failed", failureCode: "runtime-error" },
|
|
8862
|
+
durationMs: Date.now() - runStartedAt,
|
|
8863
|
+
runId: experienceRunId,
|
|
8864
|
+
lang: ctx.lang,
|
|
8865
|
+
});
|
|
8866
|
+
throw error;
|
|
8867
|
+
}
|
|
8868
|
+
const memoryResult = await completeMemoryTurnCli(db, text || "", ctx, rt, {
|
|
8869
|
+
model: selectedModel,
|
|
8870
|
+
outcome: "succeeded",
|
|
8871
|
+
requestText: prompt,
|
|
8872
|
+
});
|
|
8873
|
+
memorySettled = true;
|
|
8874
|
+
for (const memory of memoryResult.curatedMemories || []) {
|
|
8875
|
+
if (memory && !curatedMemories.some((item) => item.id === memory.id)) curatedMemories.push(memory);
|
|
8876
|
+
}
|
|
8877
|
+
const cleaned = require("./agentlas-style.cjs").sanitizeAssistantText(memoryResult.cleaned || "");
|
|
8878
|
+
finalizeExperienceExecutionCli(db, {
|
|
8879
|
+
agentId: ctx.agentId,
|
|
8880
|
+
projectPath: ctx.projectPath,
|
|
8881
|
+
cwd: ctx.cwd || projectCwd(),
|
|
8882
|
+
runtime: rt,
|
|
8883
|
+
permission: ctx.permission,
|
|
8884
|
+
model: selectedModel,
|
|
8885
|
+
runtimeExperience: ctx.runtimeExperience,
|
|
8886
|
+
curatedMemories,
|
|
8887
|
+
taskHint: prompt,
|
|
8888
|
+
outcome: { status: "succeeded", failureCode: null },
|
|
8889
|
+
durationMs: Date.now() - runStartedAt,
|
|
8890
|
+
runId: experienceRunId,
|
|
8891
|
+
lang: ctx.lang,
|
|
8892
|
+
});
|
|
8029
8893
|
process.stdout.write((cleaned || "").trim() + "\n");
|
|
8030
8894
|
return 0;
|
|
8895
|
+
} catch (error) {
|
|
8896
|
+
if (!memorySettled) {
|
|
8897
|
+
try {
|
|
8898
|
+
await completeMemoryTurnCli(db, "", ctx, memoryRuntime, {
|
|
8899
|
+
model: ctx.model || memoryRuntime?.model || null,
|
|
8900
|
+
outcome: "failed",
|
|
8901
|
+
requestText: prompt,
|
|
8902
|
+
invokeCurator: false,
|
|
8903
|
+
});
|
|
8904
|
+
memorySettled = true;
|
|
8905
|
+
} catch {
|
|
8906
|
+
// A missing/locked DB is the only remaining case where a receipt may
|
|
8907
|
+
// be impossible. Preserve the original runtime error for the caller.
|
|
8908
|
+
}
|
|
8909
|
+
}
|
|
8910
|
+
throw error;
|
|
8911
|
+
}
|
|
8912
|
+
}
|
|
8913
|
+
|
|
8914
|
+
async function runTerminalBuilder(db, request, metadata = {}, runtimeOverride = null, cwd = projectCwd()) {
|
|
8915
|
+
const builder = resolveMetaBuilder(db);
|
|
8916
|
+
if (!builder) throw new Error("Agentlas Core Engine Meta-Agent is unavailable; Build did not start.");
|
|
8917
|
+
const runtime = resolveRuntime(db, runtimeOverride);
|
|
8918
|
+
const routingOptions = metadata.workloadRouting && typeof metadata.workloadRouting === "object"
|
|
8919
|
+
? metadata.workloadRouting
|
|
8920
|
+
: {};
|
|
8921
|
+
let allocation = null;
|
|
8922
|
+
try {
|
|
8923
|
+
const currentInventory = currentRuntimeInventoryCli(db, runtime);
|
|
8924
|
+
const plannerSystem = workloadRouting.plannerSystemPrompt({
|
|
8925
|
+
language: prefsLang() === "ko" ? "Korean" : "English",
|
|
8926
|
+
maxTasks: 1,
|
|
8927
|
+
mode: "builder",
|
|
8928
|
+
liveRuntimeInventory: workloadRouting.runtimeInventory([currentInventory]),
|
|
8929
|
+
});
|
|
8930
|
+
let plannerText;
|
|
8931
|
+
if (runtime.mode === "cli") {
|
|
8932
|
+
const env = await buildChildEnvCli(db, { projectPath: cwd, agentId: builder.id, permission: "read", cwd });
|
|
8933
|
+
plannerText = await captureRuntime(runtime.kind, plannerSystem, request, {
|
|
8934
|
+
cwd,
|
|
8935
|
+
env,
|
|
8936
|
+
permission: "read",
|
|
8937
|
+
model: routingOptions.modelPin || runtime.model || null,
|
|
8938
|
+
effort: routingOptions.effortPin === undefined ? null : routingOptions.effortPin,
|
|
8939
|
+
});
|
|
8940
|
+
} else {
|
|
8941
|
+
plannerText = await runApi(runtime.backend, routingOptions.modelPin || runtime.model, plannerSystem, request);
|
|
8942
|
+
}
|
|
8943
|
+
const plan = workloadRouting.normalizePlan(plannerText, { maxTasks: 1 });
|
|
8944
|
+
allocation = plan && plan.tasks[0] && plan.tasks[0].allocation;
|
|
8945
|
+
} catch (error) {
|
|
8946
|
+
process.stderr.write(`▸ builder model planner fallback · ${String((error && error.message) || error).slice(0, 160)}\n`);
|
|
8947
|
+
}
|
|
8948
|
+
const currentInventory = currentRuntimeInventoryCli(db, runtime);
|
|
8949
|
+
const resolution = workloadRouting.resolveAllocation({
|
|
8950
|
+
runtime: currentInventory,
|
|
8951
|
+
decision: allocation,
|
|
8952
|
+
modelPin: routingOptions.modelPin,
|
|
8953
|
+
effortPin: routingOptions.effortPin,
|
|
8954
|
+
availableModels: currentInventory.availableModels,
|
|
8955
|
+
maxTier: routingOptions.maxTier || process.env.AGENTLAS_MODEL_MAX_TIER,
|
|
8956
|
+
});
|
|
8957
|
+
const receipt = workloadRouting.createDecisionReceipt({
|
|
8958
|
+
taskId: "builder-execution",
|
|
8959
|
+
stage: "builder",
|
|
8960
|
+
decision: allocation,
|
|
8961
|
+
resolution,
|
|
8962
|
+
});
|
|
8963
|
+
try {
|
|
8964
|
+
workloadRouting.appendDecisionReceipt(receipt, path.join(userDataDir(), "model-routing-receipts.jsonl"));
|
|
8965
|
+
} catch (error) {
|
|
8966
|
+
process.stderr.write(`▸ builder model routing receipt failed · ${String((error && error.message) || error).slice(0, 120)}\n`);
|
|
8967
|
+
}
|
|
8968
|
+
if (!resolution.ok) {
|
|
8969
|
+
throw new Error(`Agentlas builder model allocation failed closed: ${resolution.fallbackReason || "no compliant live model"}`);
|
|
8970
|
+
}
|
|
8971
|
+
process.stderr.write(
|
|
8972
|
+
`▸ builder model route · ${resolution.source} · ${resolution.model || runtime.kind || runtime.backend}` +
|
|
8973
|
+
`${resolution.effort ? ` · ${resolution.effort}` : ""}` +
|
|
8974
|
+
`${resolution.fallbackReason ? ` · ${resolution.fallbackReason}` : ""}\n`,
|
|
8975
|
+
);
|
|
8976
|
+
const code = await executeOnce(db, agentSystemPromptCli(builder), request, runtimeOverride, {
|
|
8977
|
+
projectPath: cwd,
|
|
8978
|
+
agentId: builder.id,
|
|
8979
|
+
permission: "full",
|
|
8980
|
+
// This is an exact private host object created after consent. An empty array
|
|
8981
|
+
// deliberately overrides all global/project/default MCP configuration.
|
|
8982
|
+
mcpServers: Array.isArray(metadata.mcpServers) ? metadata.mcpServers : [],
|
|
8983
|
+
mcpAllowlistMode: "exact",
|
|
8984
|
+
model: resolution.model,
|
|
8985
|
+
effort: resolution.effort,
|
|
8986
|
+
});
|
|
8987
|
+
if (code !== 0) throw new Error(`Agentlas builder runtime exited ${code}`);
|
|
8988
|
+
return code;
|
|
8989
|
+
}
|
|
8990
|
+
|
|
8991
|
+
async function allocateSingleWorkloadCli(db, request, options = {}) {
|
|
8992
|
+
const runtime = options.runtime || resolveRuntime(db, options.runtimeOverride);
|
|
8993
|
+
const cwd = options.cwd || projectCwd();
|
|
8994
|
+
let allocation = null;
|
|
8995
|
+
try {
|
|
8996
|
+
const currentInventory = currentRuntimeInventoryCli(db, runtime);
|
|
8997
|
+
const plannerSystem = workloadRouting.plannerSystemPrompt({
|
|
8998
|
+
language: options.lang === "ko" ? "Korean" : "English",
|
|
8999
|
+
maxTasks: 1,
|
|
9000
|
+
mode: options.mode || "team",
|
|
9001
|
+
liveRuntimeInventory: workloadRouting.runtimeInventory([currentInventory]),
|
|
9002
|
+
});
|
|
9003
|
+
let plannerText;
|
|
9004
|
+
if (runtime.mode === "cli") {
|
|
9005
|
+
const env = await buildChildEnvCli(db, {
|
|
9006
|
+
projectPath: options.projectPath || null,
|
|
9007
|
+
agentId: options.agentId || null,
|
|
9008
|
+
permission: "read",
|
|
9009
|
+
cwd,
|
|
9010
|
+
});
|
|
9011
|
+
plannerText = await captureRuntime(runtime.kind, plannerSystem, request, {
|
|
9012
|
+
cwd,
|
|
9013
|
+
env,
|
|
9014
|
+
permission: "read",
|
|
9015
|
+
model: options.modelPin || runtime.model || null,
|
|
9016
|
+
effort: options.effortPin === undefined ? null : options.effortPin,
|
|
9017
|
+
});
|
|
9018
|
+
} else {
|
|
9019
|
+
plannerText = await runApi(runtime.backend, options.modelPin || runtime.model, plannerSystem, request);
|
|
9020
|
+
}
|
|
9021
|
+
const plan = workloadRouting.normalizePlan(plannerText, { maxTasks: 1 });
|
|
9022
|
+
allocation = plan && plan.tasks[0] && plan.tasks[0].allocation;
|
|
9023
|
+
} catch (error) {
|
|
9024
|
+
if (options.onWarning) options.onWarning(`model planner fallback: ${String((error && error.message) || error).slice(0, 160)}`);
|
|
9025
|
+
}
|
|
9026
|
+
const currentInventory = currentRuntimeInventoryCli(db, runtime);
|
|
9027
|
+
const resolution = workloadRouting.resolveAllocation({
|
|
9028
|
+
runtime: currentInventory,
|
|
9029
|
+
decision: allocation,
|
|
9030
|
+
modelPin: options.modelPin,
|
|
9031
|
+
effortPin: options.effortPin,
|
|
9032
|
+
availableModels: options.availableModels || currentInventory.availableModels,
|
|
9033
|
+
maxTier: options.maxTier || process.env.AGENTLAS_MODEL_MAX_TIER,
|
|
9034
|
+
});
|
|
9035
|
+
const receipt = workloadRouting.createDecisionReceipt({
|
|
9036
|
+
taskId: options.taskId || `${options.mode || "team"}-execution`,
|
|
9037
|
+
stage: options.mode || "team",
|
|
9038
|
+
decision: allocation,
|
|
9039
|
+
resolution,
|
|
9040
|
+
});
|
|
9041
|
+
try {
|
|
9042
|
+
workloadRouting.appendDecisionReceipt(receipt, options.receiptFile || path.join(userDataDir(), "model-routing-receipts.jsonl"));
|
|
9043
|
+
} catch (error) {
|
|
9044
|
+
if (options.onWarning) options.onWarning(`model routing receipt failed: ${String((error && error.message) || error).slice(0, 120)}`);
|
|
9045
|
+
}
|
|
9046
|
+
if (!resolution.ok) {
|
|
9047
|
+
throw new Error(`Agentlas model allocation failed closed: ${resolution.fallbackReason || "no compliant live model"}`);
|
|
9048
|
+
}
|
|
9049
|
+
return { allocation, resolution, receipt };
|
|
9050
|
+
}
|
|
9051
|
+
|
|
9052
|
+
async function probeApprovedTerminalMcp(db, server, runtimeOverride, cwd, probeOptions = {}) {
|
|
9053
|
+
const runtime = resolveRuntime(db, runtimeOverride);
|
|
9054
|
+
if (runtime.mode !== "cli") return { connected: false, reason: "runtime_incompatible" };
|
|
9055
|
+
const env = await buildChildEnvCli(db, { cwd, permission: "full" });
|
|
9056
|
+
if (runtime.kind === "gemini") {
|
|
9057
|
+
const readiness = require("./agentlas-native-host.cjs").geminiMcpIsolationReadiness(env);
|
|
9058
|
+
if (!readiness.ready) return { connected: false, reason: "runtime_isolation_unavailable" };
|
|
9059
|
+
}
|
|
9060
|
+
return terminalAssets.probeSystemMcpServerConnection(server, {
|
|
9061
|
+
cwd,
|
|
9062
|
+
env,
|
|
9063
|
+
userDataDir: userDataDir(),
|
|
9064
|
+
timeoutMs: probeOptions.timeoutMs,
|
|
9065
|
+
signal: probeOptions.signal,
|
|
9066
|
+
});
|
|
8031
9067
|
}
|
|
8032
9068
|
|
|
8033
9069
|
// API 백엔드용 간이 대화형 REPL (네이티브 인터랙티브가 없는 BYOK/Ollama).
|
|
@@ -8036,7 +9072,7 @@ function apiRepl(db, backend, model, system, label, ctx) {
|
|
|
8036
9072
|
ctx = ctx || { projectPath: null, agentId: null };
|
|
8037
9073
|
if (!ctx.cwdAtRequest) ctx.cwdAtRequest = ctx.cwd || projectCwd();
|
|
8038
9074
|
const readline = require("node:readline");
|
|
8039
|
-
process.stderr.write(`▸ ${label} (${backend}${model ? " · " + model : ""}) —
|
|
9075
|
+
process.stderr.write(`▸ ${label} (${backend}${model ? " · " + model : ""}) — type /exit to stop\n`);
|
|
8040
9076
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
8041
9077
|
const ask = () =>
|
|
8042
9078
|
rl.question("\nyou › ", async (line) => {
|
|
@@ -8044,7 +9080,7 @@ function apiRepl(db, backend, model, system, label, ctx) {
|
|
|
8044
9080
|
if (tt === "/exit" || tt === "/quit") return rl.close();
|
|
8045
9081
|
if (!tt) return ask();
|
|
8046
9082
|
try {
|
|
8047
|
-
const sys = augmentSystem(db, system, ctx, true);
|
|
9083
|
+
const sys = augmentSystem(db, system, ctx, true, tt);
|
|
8048
9084
|
const text = await runApi(backend, model, sys, tt);
|
|
8049
9085
|
const cleaned = curateCliReply(db, text || "", ctx);
|
|
8050
9086
|
process.stdout.write("\n" + (cleaned || "").trim() + "\n");
|
|
@@ -8090,12 +9126,6 @@ function runCwd() {
|
|
|
8090
9126
|
}
|
|
8091
9127
|
}
|
|
8092
9128
|
|
|
8093
|
-
function cliMcpConfigPath() {
|
|
8094
|
-
return require("./agentlas-native-host.cjs").cliMcpConfigPath([]).file;
|
|
8095
|
-
}
|
|
8096
|
-
|
|
8097
|
-
const CODEX_PLAYWRIGHT_MCP_ARGS = require("./agentlas-native-host.cjs").codexMcpArgs([]);
|
|
8098
|
-
|
|
8099
9129
|
// 에이전트가 실제로 실행될 작업 폴더 = 사용자가 명령을 친 현재 디렉터리(= 대상 프로젝트).
|
|
8100
9130
|
// 단, home/userData/agent-cwd 같은 "프로젝트 아님" 위치면 안전한 전용 폴더로 폴백한다.
|
|
8101
9131
|
function projectCwd() {
|
|
@@ -8251,29 +9281,89 @@ async function buildChildEnvCli(db, ctx) {
|
|
|
8251
9281
|
|
|
8252
9282
|
// One-shot/background capture uses the same permission truth as the interactive host.
|
|
8253
9283
|
// Keep its plain-output argument shape, but never duplicate the security mapping here.
|
|
8254
|
-
function buildArgs(kind, systemPrompt, prompt, permission) {
|
|
9284
|
+
function buildArgs(kind, systemPrompt, prompt, permission, runtimeOptions = {}) {
|
|
8255
9285
|
const native = require("./agentlas-native-host.cjs");
|
|
8256
9286
|
const level = require("./agentlas-permissions.cjs").normalize(permission);
|
|
9287
|
+
const model = runtimeOptions.model ? String(runtimeOptions.model) : null;
|
|
9288
|
+
const effort = runtimeOptions.effort ? String(runtimeOptions.effort) : null;
|
|
9289
|
+
const noAuthority = runtimeOptions.authorityMode === "no-authority";
|
|
8257
9290
|
if (kind === "claude-code") {
|
|
8258
|
-
const perm = native.claudePermissionArgs(level);
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
9291
|
+
const perm = native.claudePermissionArgs(noAuthority ? "read" : level);
|
|
9292
|
+
// Background/capture has no one-pass reviewed server list. Full permission
|
|
9293
|
+
// changes tool authority, not MCP consent, so this path remains exact-empty.
|
|
9294
|
+
const mcp = native.claudeMcpIsolationArgs();
|
|
9295
|
+
const thinking = effort === "max" || effort === "xhigh" ? "Ultrathink. " : effort === "high" ? "Think hard. " : effort === "medium" ? "Think. " : "";
|
|
9296
|
+
const claudeEffort = effort === "minimal" ? "low" : effort === "xhigh" ? "max" : effort;
|
|
9297
|
+
const effortArgs = claudeEffort && claudeEffort !== "none" ? ["--effort", claudeEffort] : [];
|
|
9298
|
+
return [
|
|
9299
|
+
"-p", thinking + prompt,
|
|
9300
|
+
"--append-system-prompt", systemPrompt,
|
|
9301
|
+
...(model ? ["--model", model] : []),
|
|
9302
|
+
...effortArgs,
|
|
9303
|
+
...perm,
|
|
9304
|
+
...(noAuthority ? ["--tools", ""] : []),
|
|
9305
|
+
...mcp,
|
|
9306
|
+
];
|
|
8263
9307
|
}
|
|
8264
9308
|
if (kind === "codex") {
|
|
8265
|
-
const perm = native.codexPermissionArgs(level);
|
|
8266
|
-
const mcp =
|
|
8267
|
-
|
|
9309
|
+
const perm = native.codexPermissionArgs(noAuthority ? "read" : level);
|
|
9310
|
+
const mcp = [];
|
|
9311
|
+
const modelArgs = model ? ["-m", model] : [];
|
|
9312
|
+
const effortArgs = effort ? ["-c", `model_reasoning_effort="${effort}"`] : [];
|
|
9313
|
+
const noAuthorityArgs = noAuthority ? [
|
|
9314
|
+
"--ephemeral",
|
|
9315
|
+
"--ignore-user-config",
|
|
9316
|
+
"--ignore-rules",
|
|
9317
|
+
"--disable", "shell_tool",
|
|
9318
|
+
"--disable", "unified_exec",
|
|
9319
|
+
"--disable", "apps",
|
|
9320
|
+
"--disable", "browser_use",
|
|
9321
|
+
"--disable", "computer_use",
|
|
9322
|
+
"--disable", "image_generation",
|
|
9323
|
+
"--disable", "workspace_dependencies",
|
|
9324
|
+
"--disable", "goals",
|
|
9325
|
+
"--disable", "memories",
|
|
9326
|
+
"--disable", "plugins",
|
|
9327
|
+
"--disable", "hooks",
|
|
9328
|
+
"--disable", "multi_agent",
|
|
9329
|
+
"--disable", "tool_suggest",
|
|
9330
|
+
"--json",
|
|
9331
|
+
] : [];
|
|
9332
|
+
return ["exec", "--skip-git-repo-check", ...noAuthorityArgs, ...modelArgs, ...effortArgs, ...perm, ...mcp, `[SYSTEM]\n${systemPrompt}\n\n${prompt}`];
|
|
8268
9333
|
}
|
|
8269
9334
|
if (kind === "gemini") {
|
|
8270
|
-
const perm = native.geminiPermissionArgs(level);
|
|
8271
|
-
|
|
8272
|
-
|
|
9335
|
+
const perm = native.geminiPermissionArgs(noAuthority ? "read" : level);
|
|
9336
|
+
if (noAuthority && !runtimeOptions.noToolsPolicyPath) {
|
|
9337
|
+
throw new Error("Gemini no-authority capture requires an explicit deny-all policy");
|
|
9338
|
+
}
|
|
9339
|
+
const noAuthorityArgs = noAuthority
|
|
9340
|
+
? ["--admin-policy", String(runtimeOptions.noToolsPolicyPath)]
|
|
9341
|
+
: [];
|
|
9342
|
+
// Legacy/background capture has no structured reviewed server list. Even
|
|
9343
|
+
// at full permission it must stay exact-empty instead of inheriting the
|
|
9344
|
+
// user's global Gemini MCP definitions with the provider credential env.
|
|
9345
|
+
const mcp = native.geminiMcpIsolationArgs();
|
|
9346
|
+
return ["--prompt", `[SYSTEM]\n${systemPrompt}\n\n${prompt}`, ...(model ? ["-m", model] : []), ...perm, ...noAuthorityArgs, ...mcp];
|
|
8273
9347
|
}
|
|
8274
9348
|
return [prompt];
|
|
8275
9349
|
}
|
|
8276
9350
|
|
|
9351
|
+
function codexCaptureAgentText(jsonl) {
|
|
9352
|
+
const completed = [];
|
|
9353
|
+
const latest = new Map();
|
|
9354
|
+
for (const line of String(jsonl || "").split(/\r?\n/)) {
|
|
9355
|
+
if (!line.trim()) continue;
|
|
9356
|
+
let event;
|
|
9357
|
+
try { event = JSON.parse(line); } catch { continue; }
|
|
9358
|
+
const item = event?.item;
|
|
9359
|
+
if (!item || item.type !== "agent_message" || typeof item.text !== "string") continue;
|
|
9360
|
+
if (event.type === "item.completed") completed.push(item.text);
|
|
9361
|
+
else if (event.type === "item.started" || event.type === "item.updated") latest.set(String(item.id || latest.size), item.text);
|
|
9362
|
+
}
|
|
9363
|
+
if (completed.length) return completed.join("");
|
|
9364
|
+
return [...latest.values()].join("");
|
|
9365
|
+
}
|
|
9366
|
+
|
|
8277
9367
|
// `claude` 치면 바로 대화형 세션 뜨듯이 — 에이전트 폴더(CLAUDE.md/AGENTS.md/GEMINI.md 보유)에서
|
|
8278
9368
|
// 네이티브 CLI를 인자 없이(대화형) 실행. 에이전트 페르소나는 그 폴더의 프로젝트 지시로 자동 로드. (A+B 결합)
|
|
8279
9369
|
// 보스턴테리어 터미널(대화형 TUI)로 진입. agentlas 가 항상 "호스트"다 —
|
|
@@ -8296,14 +9386,23 @@ function buildHelpers(db) {
|
|
|
8296
9386
|
return {
|
|
8297
9387
|
which,
|
|
8298
9388
|
RUNTIME_BIN,
|
|
8299
|
-
augmentSystem: (db_, base, ctx, emit) => augmentSystem(db_, base, ctx, emit),
|
|
8300
|
-
|
|
9389
|
+
augmentSystem: (db_, base, ctx, emit, request) => augmentSystem(db_, base, ctx, emit, request),
|
|
9390
|
+
memoryEmitterPrompt: (request, ctx) => memoryEmitterPromptFor(
|
|
9391
|
+
request,
|
|
9392
|
+
loadArch(),
|
|
9393
|
+
ctx && ctx.turnId,
|
|
9394
|
+
ctx && ctx.permission,
|
|
9395
|
+
),
|
|
9396
|
+
beginMemoryTurn: (db_, prompt, ctx) => beginMemoryTurnCli(db_, prompt, ctx),
|
|
9397
|
+
completeMemoryTurn: (db_, text, ctx, runtime, options) => completeMemoryTurnCli(db_, text, ctx, runtime, options),
|
|
8301
9398
|
detectResponseLanguage: (prompt, fallback) => require("./agentlas-style.cjs").detectResponseLanguage(prompt, fallback),
|
|
8302
9399
|
sanitizeAssistantText: (text) => require("./agentlas-style.cjs").sanitizeAssistantText(text),
|
|
8303
9400
|
apiKey: (backend) => apiKey(backend),
|
|
8304
9401
|
eventsHeading: () => loadArch().eventsHeading,
|
|
8305
9402
|
defaultApiModel: (backend) => DEFAULT_API_MODEL[backend],
|
|
8306
9403
|
buildChildEnv: (db_, ctx) => buildChildEnvCli(db_, ctx),
|
|
9404
|
+
allocateWorkload: (db_, request, ctx) => allocateSingleWorkloadCli(db_, request, ctx),
|
|
9405
|
+
finalizeExperienceRun: (db_, input) => finalizeExperienceExecutionCli(db_, input),
|
|
8307
9406
|
multimodalStatus: (db_) => multimodalStatusCli(db_),
|
|
8308
9407
|
setMultimodal: (db_, modality, providerId) => setMultimodalCli(db_, modality, providerId),
|
|
8309
9408
|
resolveAgent,
|
|
@@ -8315,11 +9414,11 @@ function buildHelpers(db) {
|
|
|
8315
9414
|
autoRouteNote: (choice, lang) => autoRouteNote(choice, lang),
|
|
8316
9415
|
autoRoutePreamble: (choice, lang) => autoRoutePreamble(choice, lang),
|
|
8317
9416
|
directSystemPrompt: (lang) => directSystemPrompt(lang),
|
|
8318
|
-
cliMemoryContext: (db_, pp) => cliMemoryContext(db_, pp),
|
|
9417
|
+
cliMemoryContext: (db_, pp, agentId) => cliMemoryContext(db_, pp, agentId),
|
|
8319
9418
|
importLocal: (db_, p) => importLocalFolderCli(db_, p),
|
|
8320
9419
|
// REPL-safe public Hub install: fail()(process.exit) 대신 Error를 throw 해 REPL이 직접 렌더하게 한다.
|
|
8321
9420
|
cloudInstall: async (db_, slug) => {
|
|
8322
|
-
if (typeof fetch !== "function") throw new Error("
|
|
9421
|
+
if (typeof fetch !== "function") throw new Error("fetch is unavailable in this runtime (app runtime required).");
|
|
8323
9422
|
const base = process.env.AGENTLAS_MCP_BASE_URL || "https://agentlas.cloud/api/mcp/v1";
|
|
8324
9423
|
const headers = { "content-type": "application/json" };
|
|
8325
9424
|
const cookie = await cloudSessionCookieCli();
|
|
@@ -8332,18 +9431,18 @@ function buildHelpers(db) {
|
|
|
8332
9431
|
body: JSON.stringify({ method: "marketplace.get_manifest", params: { name: "marketplace.get_manifest", arguments: { kind: "agent", slug } } }),
|
|
8333
9432
|
});
|
|
8334
9433
|
} catch (e) {
|
|
8335
|
-
throw new Error(`Hub
|
|
9434
|
+
throw new Error(`Hub connection failed: ${(e && e.message) || e}`);
|
|
8336
9435
|
}
|
|
8337
9436
|
if (!resp.ok) {
|
|
8338
9437
|
const authHint = resp.status === 401 || resp.status === 403 ? " — 로그인이 필요합니다 (앱에서 로그인 또는 AGENTLAS_SESSION 설정)" : "";
|
|
8339
|
-
throw new Error(`Hub
|
|
9438
|
+
throw new Error(`Hub returned HTTP ${resp.status}${authHint}`);
|
|
8340
9439
|
}
|
|
8341
9440
|
const json = parseHubJsonCli(resp, "marketplace.get_manifest");
|
|
8342
9441
|
if (json.error) throw new Error(json.error.message || "Hub error");
|
|
8343
9442
|
const listing = json.result;
|
|
8344
|
-
if (!listing) throw new Error(`
|
|
9443
|
+
if (!listing) throw new Error(`Not found in Hub: ${slug}`);
|
|
8345
9444
|
if (listing.delivery && listing.delivery.mode === "call_only") {
|
|
8346
|
-
throw new Error(
|
|
9445
|
+
throw new Error(`This Hub agent is call-only. Run: agentlas call ${slug}`);
|
|
8347
9446
|
}
|
|
8348
9447
|
return persistCloudListingCli(db_, listing);
|
|
8349
9448
|
},
|
|
@@ -8352,7 +9451,22 @@ function buildHelpers(db) {
|
|
|
8352
9451
|
},
|
|
8353
9452
|
mcpServers: (db_) => {
|
|
8354
9453
|
try {
|
|
8355
|
-
|
|
9454
|
+
const consentedIds = new Set(
|
|
9455
|
+
terminalAssets.readConsentedSystemMcpServers(db_, { userDataDir: userDataDir(), createRuntimeHome: false })
|
|
9456
|
+
.map((server) => server.id),
|
|
9457
|
+
);
|
|
9458
|
+
return db_.prepare("SELECT id, catalog_id, name, name_en, transport, command, args_json, url, env_keys_json, enabled FROM mcp_servers ORDER BY installed_at ASC")
|
|
9459
|
+
.all()
|
|
9460
|
+
.map((row) => {
|
|
9461
|
+
const runtime = terminalAssets.materializeTrustedSystemMcpServer(row, { userDataDir: userDataDir(), createRuntimeHome: false });
|
|
9462
|
+
Object.defineProperty(row, "runtimeEligible", { value: Boolean(runtime), enumerable: false });
|
|
9463
|
+
Object.defineProperty(row, "runtimeConsented", { value: Boolean(runtime && consentedIds.has(String(row.id))), enumerable: false });
|
|
9464
|
+
if (runtime) {
|
|
9465
|
+
Object.defineProperty(row, "credentialKeyNames", { value: runtime.credentialKeyNames, enumerable: false });
|
|
9466
|
+
if (runtime.mcpRuntimeHome) Object.defineProperty(row, "mcpRuntimeHome", { value: runtime.mcpRuntimeHome, enumerable: false });
|
|
9467
|
+
}
|
|
9468
|
+
return row;
|
|
9469
|
+
});
|
|
8356
9470
|
} catch {
|
|
8357
9471
|
return [];
|
|
8358
9472
|
}
|
|
@@ -8367,6 +9481,7 @@ function buildHelpers(db) {
|
|
|
8367
9481
|
// 패리티: REPL의 /storm·/swarm·/build·/route·/research 가 그대로 호출한다.
|
|
8368
9482
|
stormRun: (db_, goal, ctx) => parity().stormRun(db_, goal, ctx),
|
|
8369
9483
|
swarmRun: (db_, goal, ctx) => parity().swarmRun(db_, goal, ctx),
|
|
9484
|
+
workforceRun: (db_, goal, ctx) => workforce().workforceRun(db_, goal, ctx),
|
|
8370
9485
|
terminalBuild: (db_, args, ctx = {}) => terminalAssets.cmdBuild({
|
|
8371
9486
|
db: db_,
|
|
8372
9487
|
args: Array.isArray(args) ? args : terminalAssets.tokenizeBuildCommandLine(String(args || "")),
|
|
@@ -8375,7 +9490,15 @@ function buildHelpers(db) {
|
|
|
8375
9490
|
input: ctx.input || process.stdin,
|
|
8376
9491
|
promptOutput: ctx.promptOutput || process.stderr,
|
|
8377
9492
|
out: ctx.out || out,
|
|
8378
|
-
|
|
9493
|
+
probeMcpServer: (server, probeOptions) => probeApprovedTerminalMcp(db_, server, null, ctx.cwd || projectCwd(), probeOptions),
|
|
9494
|
+
invokeBuild: (request, metadata) => runTerminalBuilder(db_, request, {
|
|
9495
|
+
...metadata,
|
|
9496
|
+
workloadRouting: {
|
|
9497
|
+
modelPin: ctx.modelPin || null,
|
|
9498
|
+
effortPin: ctx.effortPin,
|
|
9499
|
+
maxTier: ctx.maxTier,
|
|
9500
|
+
},
|
|
9501
|
+
}, null, ctx.cwd || projectCwd()),
|
|
8379
9502
|
}),
|
|
8380
9503
|
hepRun: (args, opts) => parity().runHephaestusInteractive(args, opts),
|
|
8381
9504
|
cloudSearch: (db_, args) => parity().cloudSearch(db_, args),
|
|
@@ -8397,12 +9520,14 @@ function buildHelpers(db) {
|
|
|
8397
9520
|
return null;
|
|
8398
9521
|
}
|
|
8399
9522
|
},
|
|
9523
|
+
ensureProjectForExecution: (db_, dir, permission, reason) =>
|
|
9524
|
+
ensureTerminalProjectForExecutionCli(db_, dir, permission, reason || "terminal-interactive-turn"),
|
|
8400
9525
|
doctor: async (db_, ui) => {
|
|
8401
9526
|
ui.line("");
|
|
8402
9527
|
ui.info("userData: " + userDataDir());
|
|
8403
|
-
ui.info("db: " + (fs.existsSync(dbPath()) ? "OK" : "
|
|
9528
|
+
ui.info("db: " + (fs.existsSync(dbPath()) ? "OK" : "missing"));
|
|
8404
9529
|
const ar = activeRuntime(db_);
|
|
8405
|
-
ui.info("
|
|
9530
|
+
ui.info("Active runtime: " + (ar ? ar.kind : "(none)"));
|
|
8406
9531
|
// CLI 런타임: 설치 + 로그인(인증 파일) 휴리스틱
|
|
8407
9532
|
const home = os.homedir();
|
|
8408
9533
|
const authFiles = {
|
|
@@ -8414,17 +9539,17 @@ function buildHelpers(db) {
|
|
|
8414
9539
|
for (const [kind, bin] of Object.entries(RUNTIME_BIN)) {
|
|
8415
9540
|
const installed = !!which(bin);
|
|
8416
9541
|
const authed = (authFiles[kind] || []).some(has);
|
|
8417
|
-
ui.info(` ${kind.padEnd(12)} ${!installed ? "
|
|
9542
|
+
ui.info(` ${kind.padEnd(12)} ${!installed ? "not installed" : authed ? "installed · signed in" : "installed · sign-in unverified"}`);
|
|
8418
9543
|
}
|
|
8419
9544
|
// BYOK 키 (keytar) + 클라우드 세션
|
|
8420
9545
|
const byok = [];
|
|
8421
9546
|
for (const b of ["anthropic", "openai", "google", "upstage"]) {
|
|
8422
9547
|
try { if (await apiKey(b)) byok.push(b); } catch { /* keytar 미사용 */ }
|
|
8423
9548
|
}
|
|
8424
|
-
ui.info("BYOK
|
|
9549
|
+
ui.info("BYOK keys: " + (byok.length ? byok.join(", ") : "(none — App settings → BYOK)"));
|
|
8425
9550
|
let cloud = false;
|
|
8426
9551
|
try { cloud = !!(await cloudSessionCookieCli()); } catch { /* ignore */ }
|
|
8427
|
-
ui.info("
|
|
9552
|
+
ui.info("Cloud session: " + (cloud ? "signed in" : "signed out"));
|
|
8428
9553
|
},
|
|
8429
9554
|
};
|
|
8430
9555
|
}
|
|
@@ -8465,14 +9590,18 @@ function spawnRuntime(kind, systemPrompt, prompt, opts) {
|
|
|
8465
9590
|
const cwd = opts.cwd || runCwd();
|
|
8466
9591
|
return new Promise((resolve) => {
|
|
8467
9592
|
const bin = which(RUNTIME_BIN[kind]) || RUNTIME_BIN[kind];
|
|
8468
|
-
const env = require("./agentlas-native-host.cjs").runtimeEnvForKind(kind, opts.env || process.env
|
|
9593
|
+
const env = require("./agentlas-native-host.cjs").runtimeEnvForKind(kind, opts.env || process.env, {
|
|
9594
|
+
permission: opts.permission,
|
|
9595
|
+
mcpServers: [],
|
|
9596
|
+
mcpAllowlistMode: kind === "gemini" ? "exact" : undefined,
|
|
9597
|
+
});
|
|
8469
9598
|
const child = spawn(bin, buildArgs(kind, systemPrompt, prompt, opts.permission), {
|
|
8470
9599
|
cwd,
|
|
8471
9600
|
stdio: ["ignore", "inherit", "inherit"],
|
|
8472
9601
|
env,
|
|
8473
9602
|
});
|
|
8474
9603
|
child.on("error", (err) => {
|
|
8475
|
-
process.stderr.write(`\
|
|
9604
|
+
process.stderr.write(`\nExecution failed (${kind}): ${err.message}\n`);
|
|
8476
9605
|
resolve(1);
|
|
8477
9606
|
});
|
|
8478
9607
|
child.on("close", (code) => resolve(code ?? 0));
|
|
@@ -8502,8 +9631,17 @@ function captureRuntime(kind, systemPrompt, prompt, opts) {
|
|
|
8502
9631
|
let child;
|
|
8503
9632
|
try {
|
|
8504
9633
|
const spawnImpl = opts.spawn || spawn;
|
|
8505
|
-
const env = require("./agentlas-native-host.cjs").runtimeEnvForKind(kind, opts.env || process.env
|
|
8506
|
-
|
|
9634
|
+
const env = require("./agentlas-native-host.cjs").runtimeEnvForKind(kind, opts.env || process.env, {
|
|
9635
|
+
permission: opts.permission,
|
|
9636
|
+
mcpServers: [],
|
|
9637
|
+
mcpAllowlistMode: kind === "gemini" ? "exact" : undefined,
|
|
9638
|
+
});
|
|
9639
|
+
child = spawnImpl(bin, buildArgs(kind, systemPrompt, prompt, opts.permission, {
|
|
9640
|
+
model: opts.model,
|
|
9641
|
+
effort: opts.effort,
|
|
9642
|
+
authorityMode: opts.authorityMode,
|
|
9643
|
+
noToolsPolicyPath: opts.noToolsPolicyPath,
|
|
9644
|
+
}), {
|
|
8507
9645
|
cwd,
|
|
8508
9646
|
stdio: ["ignore", "pipe", "pipe"],
|
|
8509
9647
|
env,
|
|
@@ -8614,7 +9752,11 @@ function captureRuntime(kind, systemPrompt, prompt, opts) {
|
|
|
8614
9752
|
finishReject(new Error(`${kind} exited ${code}: ${stderr.slice(-500)}`));
|
|
8615
9753
|
return;
|
|
8616
9754
|
}
|
|
8617
|
-
|
|
9755
|
+
const raw = stdout.trim() || stderr.trim();
|
|
9756
|
+
const captured = kind === "codex" && opts.authorityMode === "no-authority"
|
|
9757
|
+
? codexCaptureAgentText(raw)
|
|
9758
|
+
: "";
|
|
9759
|
+
finishResolve(captured || raw);
|
|
8618
9760
|
};
|
|
8619
9761
|
onAbort = () => {
|
|
8620
9762
|
const reason = opts.signal && opts.signal.reason;
|
|
@@ -8644,10 +9786,12 @@ function parity() {
|
|
|
8644
9786
|
captureRuntime,
|
|
8645
9787
|
runApi,
|
|
8646
9788
|
resolveRuntime,
|
|
9789
|
+
listAvailableRuntimes,
|
|
8647
9790
|
buildChildEnvCli,
|
|
8648
9791
|
projectCwd,
|
|
8649
9792
|
runCwd,
|
|
8650
9793
|
userDataDir,
|
|
9794
|
+
modelRoutingReceiptPath: () => path.join(userDataDir(), "model-routing-receipts.jsonl"),
|
|
8651
9795
|
resolveAgent,
|
|
8652
9796
|
resolveFirm,
|
|
8653
9797
|
listAgents,
|
|
@@ -8666,6 +9810,99 @@ function parity() {
|
|
|
8666
9810
|
return parity._i;
|
|
8667
9811
|
}
|
|
8668
9812
|
|
|
9813
|
+
// Agent Workforce Ontology is a separate, fail-closed route. Unlike the
|
|
9814
|
+
// compatibility router it gives final staffing authority to the active host
|
|
9815
|
+
// LLM and uses Hub MCP only for retrieval, validation, and pinned preparation.
|
|
9816
|
+
async function listWorkforceToolsCli({ db, roster, runtimeId, cwd, env, timeoutMs, signal }) {
|
|
9817
|
+
const mcp = require("./agentlas-experience-mcp.cjs");
|
|
9818
|
+
const servers = mcp.readConsentedSystemMcpServers(db, {
|
|
9819
|
+
userDataDir: userDataDir(),
|
|
9820
|
+
createRuntimeHome: false,
|
|
9821
|
+
}).slice(0, 8);
|
|
9822
|
+
if (!servers.length) return [];
|
|
9823
|
+
const deadline = Date.now() + Math.max(50, Math.min(12_000, Number(timeoutMs) || 12_000));
|
|
9824
|
+
const outcomes = new Array(servers.length);
|
|
9825
|
+
let cursor = 0;
|
|
9826
|
+
const worker = async () => {
|
|
9827
|
+
while (true) {
|
|
9828
|
+
const index = cursor++;
|
|
9829
|
+
if (index >= servers.length) return;
|
|
9830
|
+
const remaining = deadline - Date.now();
|
|
9831
|
+
if (remaining <= 0 || signal?.aborted) return;
|
|
9832
|
+
outcomes[index] = await mcp.probeSystemMcpServerConnection(servers[index], {
|
|
9833
|
+
cwd,
|
|
9834
|
+
env,
|
|
9835
|
+
userDataDir: userDataDir(),
|
|
9836
|
+
timeoutMs: Math.min(4_000, remaining),
|
|
9837
|
+
signal,
|
|
9838
|
+
});
|
|
9839
|
+
}
|
|
9840
|
+
};
|
|
9841
|
+
await Promise.all(Array.from({ length: Math.min(3, servers.length) }, () => worker()));
|
|
9842
|
+
|
|
9843
|
+
const safeId = /^[A-Za-z0-9][A-Za-z0-9_.$:/@+~-]{0,127}$/;
|
|
9844
|
+
const rows = [];
|
|
9845
|
+
for (let index = 0; index < servers.length; index += 1) {
|
|
9846
|
+
const server = servers[index];
|
|
9847
|
+
const listed = outcomes[index];
|
|
9848
|
+
if (!listed?.connected || !Array.isArray(listed.tools)) continue;
|
|
9849
|
+
for (const tool of listed.tools.slice(0, 256)) {
|
|
9850
|
+
if (!tool || typeof tool !== "object" || Array.isArray(tool)) continue;
|
|
9851
|
+
const declared = tool._meta?.agentlas || {};
|
|
9852
|
+
const toolId = typeof declared.toolId === "string" ? declared.toolId : String(tool.name || "");
|
|
9853
|
+
const capabilityIds = Array.isArray(declared.capabilityIds)
|
|
9854
|
+
? [...new Set(declared.capabilityIds.filter((id) => /^[A-Za-z0-9][A-Za-z0-9._:/@-]{1,255}$/.test(String(id))))]
|
|
9855
|
+
: [];
|
|
9856
|
+
if (!safeId.test(toolId) || !capabilityIds.length) continue;
|
|
9857
|
+
const schemaJson = JSON.stringify(tool.inputSchema || {}, Object.keys(tool.inputSchema || {}).sort());
|
|
9858
|
+
for (const pinned of roster || []) {
|
|
9859
|
+
if (pinned?.permissionPolicy?.mcp?.mode !== "allowlist" || !pinned.permissionPolicy.mcp.allowedTools.includes(toolId)) continue;
|
|
9860
|
+
rows.push({
|
|
9861
|
+
slotId: pinned.slotId,
|
|
9862
|
+
agentReleaseId: pinned.agentReleaseId,
|
|
9863
|
+
permissionPolicyDigest: pinned.permissionPolicyDigest,
|
|
9864
|
+
provider: "mcp",
|
|
9865
|
+
toolId,
|
|
9866
|
+
serverId: server.id,
|
|
9867
|
+
description: "Ready consented host MCP tool",
|
|
9868
|
+
inputSchemaDigest: `sha256:${crypto.createHash("sha256").update(schemaJson).digest("hex")}`,
|
|
9869
|
+
// Terminal's one-shot native/API runners do not yet expose a proven
|
|
9870
|
+
// exact per-tool attachment boundary. Preserve the real tools/list
|
|
9871
|
+
// observation, but advertise no executable runtime instead of
|
|
9872
|
+
// manufacturing authority. collectToolInventory filters this row and
|
|
9873
|
+
// fails closed before the planner for a required capability.
|
|
9874
|
+
runtimeIds: [],
|
|
9875
|
+
selectiveEnforcement: "unavailable",
|
|
9876
|
+
capabilityIds,
|
|
9877
|
+
status: "observed-not-executable",
|
|
9878
|
+
});
|
|
9879
|
+
}
|
|
9880
|
+
}
|
|
9881
|
+
}
|
|
9882
|
+
return rows;
|
|
9883
|
+
}
|
|
9884
|
+
|
|
9885
|
+
function workforce() {
|
|
9886
|
+
if (!workforce._i) {
|
|
9887
|
+
workforce._i = require("./agentlas-workforce.cjs").create({
|
|
9888
|
+
captureRuntime,
|
|
9889
|
+
runApi,
|
|
9890
|
+
resolveRuntime,
|
|
9891
|
+
buildChildEnv: buildChildEnvCli,
|
|
9892
|
+
projectCwd,
|
|
9893
|
+
userDataDir,
|
|
9894
|
+
receiptFile: () => path.join(userDataDir(), "workforce-execution-receipts.jsonl"),
|
|
9895
|
+
cloudSessionCookie: cloudSessionCookieCli,
|
|
9896
|
+
fetchHub: (url, init) => fetchHubCli(url, init),
|
|
9897
|
+
listWorkforceTools: listWorkforceToolsCli,
|
|
9898
|
+
supportsWorkforceToolAuthority: async () => false,
|
|
9899
|
+
prefsLang,
|
|
9900
|
+
out,
|
|
9901
|
+
});
|
|
9902
|
+
}
|
|
9903
|
+
return workforce._i;
|
|
9904
|
+
}
|
|
9905
|
+
|
|
8669
9906
|
// ── 명령 구현 ──────────────────────────────────────────────
|
|
8670
9907
|
function cmdList(db) {
|
|
8671
9908
|
const agents = listAgents(db);
|
|
@@ -8717,17 +9954,17 @@ function writeIfMissing(file, content) {
|
|
|
8717
9954
|
|
|
8718
9955
|
function cmdCd(db, query) {
|
|
8719
9956
|
const agent = resolveAgent(db, query);
|
|
8720
|
-
if (!agent) fail(
|
|
9957
|
+
if (!agent) fail(`Agent not found: ${query}`);
|
|
8721
9958
|
const folder = agentFolder(agent);
|
|
8722
9959
|
ensureNativeFiles(agent, folder);
|
|
8723
9960
|
// 경로만 stdout으로 (cd "$(agentlas cd seo)") — 안내는 stderr로.
|
|
8724
|
-
process.stderr.write(`# ${agent.name} —
|
|
9961
|
+
process.stderr.write(`# ${agent.name} — native CLI context ready (CLAUDE.md/AGENTS.md/GEMINI.md)\n`);
|
|
8725
9962
|
process.stdout.write(folder + "\n");
|
|
8726
9963
|
}
|
|
8727
9964
|
|
|
8728
9965
|
function parseRunExperienceArgs(args) {
|
|
8729
9966
|
const prompt = [];
|
|
8730
|
-
const experience = { taskSignatures: [], environmentTags: [] };
|
|
9967
|
+
const experience = { taskSignatures: [], declaredTaskClasses: [], environmentTags: [], experiencePackReleaseIds: [] };
|
|
8731
9968
|
let passthrough = false;
|
|
8732
9969
|
const addList = (target, value) => {
|
|
8733
9970
|
for (const item of String(value || "").split(",").map((entry) => entry.trim()).filter(Boolean)) {
|
|
@@ -8741,31 +9978,103 @@ function parseRunExperienceArgs(args) {
|
|
|
8741
9978
|
const take = () => index + 1 < args.length ? String(args[++index]) : "";
|
|
8742
9979
|
if (token === "--experience-base-release") experience.baseAgentReleaseId = take();
|
|
8743
9980
|
else if (token.startsWith("--experience-base-release=")) experience.baseAgentReleaseId = token.slice(26);
|
|
9981
|
+
else if (token === "--experience-pack-release") addList(experience.experiencePackReleaseIds, take());
|
|
9982
|
+
else if (token.startsWith("--experience-pack-release=")) addList(experience.experiencePackReleaseIds, token.slice(26));
|
|
8744
9983
|
else if (token === "--experience-agent-definition") experience.agentDefinitionId = take();
|
|
8745
9984
|
else if (token.startsWith("--experience-agent-definition=")) experience.agentDefinitionId = token.slice(30);
|
|
8746
9985
|
else if (token === "--experience-task-signature") addList(experience.taskSignatures, take());
|
|
8747
9986
|
else if (token.startsWith("--experience-task-signature=")) addList(experience.taskSignatures, token.slice(28));
|
|
9987
|
+
else if (token === "--experience-task-class") addList(experience.declaredTaskClasses, take());
|
|
9988
|
+
else if (token.startsWith("--experience-task-class=")) addList(experience.declaredTaskClasses, token.slice(24));
|
|
8748
9989
|
else if (token === "--experience-environment") addList(experience.environmentTags, take());
|
|
8749
9990
|
else if (token.startsWith("--experience-environment=")) addList(experience.environmentTags, token.slice(25));
|
|
9991
|
+
else if (token === "--experience-desktop-loadout") experience.desktopLoadout = true;
|
|
9992
|
+
else if (
|
|
9993
|
+
token === "--experience-loadout" || token === "--experience-loadout-file" ||
|
|
9994
|
+
token.startsWith("--experience-loadout=") || token.startsWith("--experience-loadout-file=")
|
|
9995
|
+
) {
|
|
9996
|
+
throw new Error("Custom Experience loadout paths are no longer supported; use --experience-desktop-loadout.");
|
|
9997
|
+
}
|
|
8750
9998
|
else if (token === "--no-experience") experience.disabled = true;
|
|
8751
9999
|
else prompt.push(token);
|
|
8752
10000
|
}
|
|
8753
10001
|
return { prompt: prompt.join(" "), experience };
|
|
8754
10002
|
}
|
|
8755
10003
|
|
|
10004
|
+
function resolveRuntimeExperienceCli(agent, prompt, requested, cwd, overrides = {}) {
|
|
10005
|
+
const prepared = desktopOntologyLoadout.prepareDesktopLoadoutRequest({
|
|
10006
|
+
db: overrides.db,
|
|
10007
|
+
agent,
|
|
10008
|
+
userDataDir: overrides.userDataDir || userDataDir(),
|
|
10009
|
+
requested: requested || {},
|
|
10010
|
+
now: overrides.now,
|
|
10011
|
+
});
|
|
10012
|
+
if (prepared.mode === "skip") {
|
|
10013
|
+
return { disabled: true, observableReason: prepared.reason, resolution: "skipped" };
|
|
10014
|
+
}
|
|
10015
|
+
const resolved = terminalExperienceExchange.resolveRuntimeExperienceForAgent({
|
|
10016
|
+
userDataDir: overrides.userDataDir || userDataDir(),
|
|
10017
|
+
cwd,
|
|
10018
|
+
prompt,
|
|
10019
|
+
requested: prepared.requested || requested || {},
|
|
10020
|
+
agent,
|
|
10021
|
+
agentRoot: agent ? (overrides.agentRoot || agentFolder(agent)) : null,
|
|
10022
|
+
...(overrides.platform ? { platform: overrides.platform } : {}),
|
|
10023
|
+
...(overrides.arch ? { arch: overrides.arch } : {}),
|
|
10024
|
+
...(overrides.runtime ? { runtime: overrides.runtime } : {}),
|
|
10025
|
+
});
|
|
10026
|
+
if (prepared.mode !== "resolved") return resolved;
|
|
10027
|
+
const authority = prepared.authority;
|
|
10028
|
+
const tasteRuntime = {
|
|
10029
|
+
tasteRuntimeOverlay: authority.tasteRuntimeOverlay || null,
|
|
10030
|
+
loadoutAuthority: "desktop-terminal-exact-loadout",
|
|
10031
|
+
projectionRevision: authority.projectionRevision,
|
|
10032
|
+
loadoutRevision: authority.loadoutRevision,
|
|
10033
|
+
};
|
|
10034
|
+
if (!authority.experiencePackReleaseId) {
|
|
10035
|
+
return {
|
|
10036
|
+
disabled: true,
|
|
10037
|
+
resolution: "desktop-loadout-taste-only",
|
|
10038
|
+
...tasteRuntime,
|
|
10039
|
+
};
|
|
10040
|
+
}
|
|
10041
|
+
if (resolved.disabled === true) return { ...resolved, ...tasteRuntime };
|
|
10042
|
+
if (
|
|
10043
|
+
resolved.agentDefinitionId !== authority.agentDefinitionId ||
|
|
10044
|
+
resolved.baseAgentReleaseId !== authority.baseAgentReleaseId ||
|
|
10045
|
+
!Array.isArray(resolved.experiencePackReleaseIds) ||
|
|
10046
|
+
resolved.experiencePackReleaseIds.length !== 1 ||
|
|
10047
|
+
resolved.experiencePackReleaseIds[0] !== authority.experiencePackReleaseId
|
|
10048
|
+
) {
|
|
10049
|
+
return {
|
|
10050
|
+
disabled: true,
|
|
10051
|
+
observableReason: "desktop-loadout-runtime-resolution-mismatch",
|
|
10052
|
+
resolution: "skipped",
|
|
10053
|
+
...tasteRuntime,
|
|
10054
|
+
};
|
|
10055
|
+
}
|
|
10056
|
+
return {
|
|
10057
|
+
...resolved,
|
|
10058
|
+
...tasteRuntime,
|
|
10059
|
+
};
|
|
10060
|
+
}
|
|
10061
|
+
|
|
8756
10062
|
async function cmdRun(db, query, prompt, runtimeOverride, runtimeExperience = null) {
|
|
8757
10063
|
const agent = resolveAgent(db, query);
|
|
8758
10064
|
if (!agent) {
|
|
8759
10065
|
const routedPrompt = [query, prompt].filter(Boolean).join(" ").trim() || (await readStdin());
|
|
8760
|
-
if (!routedPrompt || !routedPrompt.trim()) fail("
|
|
10066
|
+
if (!routedPrompt || !routedPrompt.trim()) fail("Prompt is empty. Use agentlas run <agent> \"...\" or agentlas run \"...\".");
|
|
8761
10067
|
return cmdAutoRun(db, routedPrompt.trim(), runtimeOverride, runtimeExperience);
|
|
8762
10068
|
}
|
|
8763
10069
|
let userPrompt = prompt;
|
|
8764
10070
|
if (!userPrompt) userPrompt = await readStdin();
|
|
8765
|
-
if (!userPrompt || !userPrompt.trim()) fail("
|
|
10071
|
+
if (!userPrompt || !userPrompt.trim()) fail("Prompt is empty. Pass agentlas run <agent> \"...\" or provide it through stdin.");
|
|
8766
10072
|
process.stderr.write(`▸ ${agent.name}\n`);
|
|
10073
|
+
const projectPath = ensureTerminalProjectForExecutionCli(db, projectCwd(), PERMISSION, "terminal-run");
|
|
10074
|
+
const cwd = projectPath || projectCwd();
|
|
10075
|
+
const resolvedExperience = resolveRuntimeExperienceCli(agent, userPrompt.trim(), runtimeExperience, cwd, { db });
|
|
8767
10076
|
const code = await executeOnce(db, agentSystemPromptCli(agent), userPrompt.trim(), runtimeOverride, {
|
|
8768
|
-
projectPath
|
|
10077
|
+
projectPath, agentId: agent.id, permission: PERMISSION, runtimeExperience: resolvedExperience,
|
|
8769
10078
|
});
|
|
8770
10079
|
process.exit(code);
|
|
8771
10080
|
}
|
|
@@ -8773,28 +10082,34 @@ async function cmdRun(db, query, prompt, runtimeOverride, runtimeExperience = nu
|
|
|
8773
10082
|
async function cmdAutoRun(db, prompt, runtimeOverride, runtimeExperience = null) {
|
|
8774
10083
|
const lang = prefsLang();
|
|
8775
10084
|
const choice = autoRouteAgent(db, prompt, lang);
|
|
8776
|
-
if (!choice) fail("
|
|
10085
|
+
if (!choice) fail("No agent is available for automatic routing. Check installation with agentlas list.");
|
|
8777
10086
|
if (choice.direct) {
|
|
8778
10087
|
// 전문 에이전트 확신 없음 → 페르소나/능력 라우팅 없이 현재 런타임으로 직답.
|
|
8779
10088
|
process.stderr.write(`▸ direct (no agent)\n`);
|
|
8780
10089
|
process.stderr.write(` ${autoRouteNote(choice, lang)}\n`);
|
|
8781
10090
|
const sys = `${autoRoutePreamble(choice, lang)}\n\n${directSystemPrompt(lang)}`;
|
|
10091
|
+
const projectPath = ensureTerminalProjectForExecutionCli(db, projectCwd(), PERMISSION, "terminal-auto-run");
|
|
10092
|
+
const cwd = projectPath || projectCwd();
|
|
10093
|
+
const resolvedExperience = resolveRuntimeExperienceCli(null, prompt.trim(), runtimeExperience, cwd, { db });
|
|
8782
10094
|
const code = await executeOnce(db, sys, prompt.trim(), runtimeOverride, {
|
|
8783
|
-
projectPath
|
|
10095
|
+
projectPath,
|
|
8784
10096
|
agentId: null,
|
|
8785
10097
|
permission: PERMISSION,
|
|
8786
|
-
runtimeExperience,
|
|
10098
|
+
runtimeExperience: resolvedExperience,
|
|
8787
10099
|
});
|
|
8788
10100
|
process.exit(code);
|
|
8789
10101
|
}
|
|
8790
10102
|
process.stderr.write(`▸ ${choice.agent.name} (auto)\n`);
|
|
8791
10103
|
process.stderr.write(` ${autoRouteNote(choice, lang)}\n`);
|
|
8792
10104
|
const sys = `${autoRoutePreamble(choice, lang)}\n\n${agentSystemPromptCli(choice.agent)}`;
|
|
10105
|
+
const projectPath = ensureTerminalProjectForExecutionCli(db, projectCwd(), PERMISSION, "terminal-auto-run");
|
|
10106
|
+
const cwd = projectPath || projectCwd();
|
|
10107
|
+
const resolvedExperience = resolveRuntimeExperienceCli(choice.agent, prompt.trim(), runtimeExperience, cwd, { db });
|
|
8793
10108
|
const code = await executeOnce(db, sys, prompt.trim(), runtimeOverride, {
|
|
8794
|
-
projectPath
|
|
10109
|
+
projectPath,
|
|
8795
10110
|
agentId: choice.agent.id,
|
|
8796
10111
|
permission: PERMISSION,
|
|
8797
|
-
runtimeExperience,
|
|
10112
|
+
runtimeExperience: resolvedExperience,
|
|
8798
10113
|
});
|
|
8799
10114
|
process.exit(code);
|
|
8800
10115
|
}
|
|
@@ -8802,7 +10117,7 @@ async function cmdAutoRun(db, prompt, runtimeOverride, runtimeExperience = null)
|
|
|
8802
10117
|
// chat / open / 에이전트명 단독 → 네이티브 CLI 대화형 세션 (claude처럼 바로 접속)
|
|
8803
10118
|
function cmdOpen(db, query, runtimeOverride) {
|
|
8804
10119
|
const agent = resolveAgent(db, query);
|
|
8805
|
-
if (!agent) fail(
|
|
10120
|
+
if (!agent) fail(`Agent not found: ${query}`);
|
|
8806
10121
|
launchInteractive(db, agent, runtimeOverride);
|
|
8807
10122
|
}
|
|
8808
10123
|
|
|
@@ -8837,15 +10152,37 @@ function firmSystemPrompt(db, firm) {
|
|
|
8837
10152
|
/* ignore */
|
|
8838
10153
|
}
|
|
8839
10154
|
const base = (ceo && ceo.system_prompt) || `You are the CEO of ${firm.name}.`;
|
|
8840
|
-
return `${base}\n\n[FIRM]
|
|
10155
|
+
return `${base}\n\n[FIRM] You are the CEO of '${firm.name}'. Delegate user requests to the appropriate departments.\nOrganization:\n${roster}`;
|
|
8841
10156
|
}
|
|
8842
10157
|
async function cmdFirm(db, query, prompt, runtimeOverride) {
|
|
8843
10158
|
const firm = resolveFirm(db, query);
|
|
8844
|
-
if (!firm) fail(
|
|
10159
|
+
if (!firm) fail(`Company not found: ${query}`);
|
|
8845
10160
|
const sys = firmSystemPrompt(db, firm);
|
|
8846
10161
|
if (prompt && prompt.trim()) {
|
|
8847
10162
|
process.stderr.write(`▸ ${firm.name} CEO\n`);
|
|
8848
|
-
const
|
|
10163
|
+
const runtime = resolveRuntime(db, runtimeOverride);
|
|
10164
|
+
const projectPath = ensureTerminalProjectForExecutionCli(db, projectCwd(), PERMISSION, "terminal-firm-run");
|
|
10165
|
+
const allocated = await allocateSingleWorkloadCli(db, prompt.trim(), {
|
|
10166
|
+
runtime,
|
|
10167
|
+
cwd: projectCwd(),
|
|
10168
|
+
projectPath,
|
|
10169
|
+
agentId: firm.ceo_agent_id,
|
|
10170
|
+
lang: prefsLang(),
|
|
10171
|
+
mode: "team",
|
|
10172
|
+
onWarning: (message) => process.stderr.write(`▸ ${message}\n`),
|
|
10173
|
+
});
|
|
10174
|
+
process.stderr.write(
|
|
10175
|
+
`▸ team model route · ${allocated.resolution.source} · ${allocated.resolution.model || runtime.kind || runtime.backend}` +
|
|
10176
|
+
`${allocated.resolution.effort ? ` · ${allocated.resolution.effort}` : ""}` +
|
|
10177
|
+
`${allocated.resolution.fallbackReason ? ` · ${allocated.resolution.fallbackReason}` : ""}\n`,
|
|
10178
|
+
);
|
|
10179
|
+
const code = await executeOnce(db, sys, prompt.trim(), runtimeOverride, {
|
|
10180
|
+
projectPath,
|
|
10181
|
+
agentId: firm.ceo_agent_id,
|
|
10182
|
+
permission: PERMISSION,
|
|
10183
|
+
model: allocated.resolution.model,
|
|
10184
|
+
effort: allocated.resolution.effort,
|
|
10185
|
+
});
|
|
8849
10186
|
process.exit(code);
|
|
8850
10187
|
}
|
|
8851
10188
|
// 대화형 — agentlas TUI. CEO 페르소나를 system으로, 작업은 현재 폴더에서.
|
|
@@ -9037,23 +10374,23 @@ async function cmdEnv(db) {
|
|
|
9037
10374
|
...readDotEnvFileCli(path.join(os.homedir(), ".agentlas", "credentials.env")),
|
|
9038
10375
|
};
|
|
9039
10376
|
const keys = Object.keys(fromFiles).sort();
|
|
9040
|
-
out(
|
|
10377
|
+
out(`Shared env keys: ${keys.length} (values hidden; from credentials.env):`);
|
|
9041
10378
|
for (const k of keys) out(` ${k}`);
|
|
9042
10379
|
out("");
|
|
9043
|
-
out("
|
|
10380
|
+
out("Keychain entries are available in Desktop settings → Credentials.");
|
|
9044
10381
|
return;
|
|
9045
10382
|
}
|
|
9046
10383
|
const keytar = readKeytar();
|
|
9047
|
-
if (!keytar) fail("keytar
|
|
10384
|
+
if (!keytar) fail("The keytar module is unavailable (run through the app runtime).");
|
|
9048
10385
|
let creds;
|
|
9049
10386
|
try {
|
|
9050
10387
|
creds = await keytar.findCredentials(SERVICE);
|
|
9051
10388
|
} catch (e) {
|
|
9052
|
-
fail("
|
|
10389
|
+
fail("Failed to read environment settings: " + ((e && e.message) || e));
|
|
9053
10390
|
return;
|
|
9054
10391
|
}
|
|
9055
10392
|
const keys = creds.map((c) => c.account).filter((a) => a.startsWith(ENV_PREFIX)).map((a) => a.slice(ENV_PREFIX.length));
|
|
9056
|
-
out(
|
|
10393
|
+
out(`Shared env keys: ${keys.length} (values hidden):`);
|
|
9057
10394
|
for (const k of keys.sort()) out(` ${k}`);
|
|
9058
10395
|
}
|
|
9059
10396
|
|
|
@@ -9082,7 +10419,7 @@ function setMultimodalCli(db, modality, providerId) {
|
|
|
9082
10419
|
const mm = loadMultimodalCatalog();
|
|
9083
10420
|
if (!["image", "video", "audio"].includes(modality)) fail("usage: agentlas multimodal set <image|video|audio> <provider-id>");
|
|
9084
10421
|
const provider = mm.MULTIMODAL_PROVIDERS.find((p) => p.id === providerId && p.modality === modality);
|
|
9085
|
-
if (!provider) fail(`
|
|
10422
|
+
if (!provider) fail(`Provider not found: ${providerId} (${modality})`);
|
|
9086
10423
|
const key = modality === "image" ? "imageProvider" : modality === "video" ? "videoProvider" : "audioProvider";
|
|
9087
10424
|
return saveMultimodalSettingsCli(db, { [key]: providerId });
|
|
9088
10425
|
}
|
|
@@ -9145,7 +10482,7 @@ function parseUpdateFlags(args) {
|
|
|
9145
10482
|
else if (arg === "--no-launch") flags.launch = false;
|
|
9146
10483
|
else if (arg === "--url") flags.url = args[++i] || flags.url;
|
|
9147
10484
|
else if (arg === "--help" || arg === "-h" || arg === "help") flags.help = true;
|
|
9148
|
-
else fail(
|
|
10485
|
+
else fail(`Unknown update option: ${arg}`);
|
|
9149
10486
|
}
|
|
9150
10487
|
return flags;
|
|
9151
10488
|
}
|
|
@@ -9220,19 +10557,19 @@ function updateTimeoutError(kind, ms) {
|
|
|
9220
10557
|
return updateTransferError(`AGENTLAS_UPDATE_${kind.toUpperCase()}_TIMEOUT`, message);
|
|
9221
10558
|
}
|
|
9222
10559
|
|
|
9223
|
-
function parseSafeUpdateUrl(value, label = "
|
|
10560
|
+
function parseSafeUpdateUrl(value, label = "update URL") {
|
|
9224
10561
|
let parsed;
|
|
9225
10562
|
try {
|
|
9226
10563
|
parsed = new URL(String(value || ""));
|
|
9227
10564
|
} catch (error) {
|
|
9228
|
-
throw updateTransferError("AGENTLAS_UPDATE_INVALID_URL", `${label}
|
|
10565
|
+
throw updateTransferError("AGENTLAS_UPDATE_INVALID_URL", `${label} has an invalid format.`, error);
|
|
9229
10566
|
}
|
|
9230
10567
|
const loopback = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]" || parsed.hostname === "::1";
|
|
9231
10568
|
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && loopback)) {
|
|
9232
|
-
throw updateTransferError("AGENTLAS_UPDATE_INSECURE_URL", `${label}
|
|
10569
|
+
throw updateTransferError("AGENTLAS_UPDATE_INSECURE_URL", `${label} must use HTTPS (except local loopback).`);
|
|
9233
10570
|
}
|
|
9234
10571
|
if (parsed.username || parsed.password) {
|
|
9235
|
-
throw updateTransferError("AGENTLAS_UPDATE_INVALID_URL", `${label}
|
|
10572
|
+
throw updateTransferError("AGENTLAS_UPDATE_INVALID_URL", `${label} cannot contain user information.`);
|
|
9236
10573
|
}
|
|
9237
10574
|
return parsed.toString();
|
|
9238
10575
|
}
|
|
@@ -9379,7 +10716,7 @@ async function fetchDesktopRelease(url) {
|
|
|
9379
10716
|
return await fetchUpdateMetadata(url, { signal: controller.signal });
|
|
9380
10717
|
} catch (error) {
|
|
9381
10718
|
const message = String((error && error.message) || error);
|
|
9382
|
-
fail(
|
|
10719
|
+
fail(`Update check failed: ${message}`);
|
|
9383
10720
|
}
|
|
9384
10721
|
}
|
|
9385
10722
|
|
|
@@ -9418,19 +10755,19 @@ async function cmdUpdateStandalone(flags) {
|
|
|
9418
10755
|
if (flags.json) {
|
|
9419
10756
|
return out(JSON.stringify({ currentVersion, latestVersion, updateAvailable: comparison == null ? null : comparison < 0, channel: "npm" }, null, 2));
|
|
9420
10757
|
}
|
|
9421
|
-
out(
|
|
10758
|
+
out(`Current version: ${currentVersion}`);
|
|
9422
10759
|
if (!latestVersion) {
|
|
9423
|
-
out("
|
|
9424
|
-
out("
|
|
10760
|
+
out("Could not check the latest version on the npm registry (offline or not published yet).");
|
|
10761
|
+
out("Manual update: npm i -g agentlas@latest");
|
|
9425
10762
|
return;
|
|
9426
10763
|
}
|
|
9427
|
-
out(
|
|
10764
|
+
out(`Latest version: ${latestVersion}`);
|
|
9428
10765
|
if (comparison == null) {
|
|
9429
|
-
out("
|
|
10766
|
+
out("Could not compare version formats. Manual update: npm i -g agentlas@latest");
|
|
9430
10767
|
} else if (comparison < 0) {
|
|
9431
|
-
out("
|
|
10768
|
+
out("Update: npm i -g agentlas@latest");
|
|
9432
10769
|
} else {
|
|
9433
|
-
out("
|
|
10770
|
+
out("Already on the latest version.");
|
|
9434
10771
|
}
|
|
9435
10772
|
}
|
|
9436
10773
|
|
|
@@ -9445,7 +10782,7 @@ async function cmdUpdate(args) {
|
|
|
9445
10782
|
const latestVersion = String(release.version || "");
|
|
9446
10783
|
const artifact = findCurrentArtifact(release);
|
|
9447
10784
|
const comparison = compareSemVer(currentVersion, latestVersion);
|
|
9448
|
-
if (comparison == null) fail(
|
|
10785
|
+
if (comparison == null) fail(`Current/latest version is not valid SemVer: current=${currentVersion} latest=${latestVersion}`);
|
|
9449
10786
|
const updateAvailable = comparison < 0;
|
|
9450
10787
|
const status = {
|
|
9451
10788
|
currentVersion,
|
|
@@ -9463,15 +10800,15 @@ async function cmdUpdate(args) {
|
|
|
9463
10800
|
if (flags.json) return out(JSON.stringify(status, null, 2));
|
|
9464
10801
|
out(formatUpdateSummary(status));
|
|
9465
10802
|
if (flags.check) return;
|
|
9466
|
-
if (release.ready !== true) fail("
|
|
9467
|
-
if (!updateAvailable && !flags.force) return out("
|
|
9468
|
-
if (process.platform !== "darwin") return out("
|
|
9469
|
-
if (!artifact || !artifact.url) fail("
|
|
10803
|
+
if (release.ready !== true) fail("The latest release is not ready for public installation.");
|
|
10804
|
+
if (!updateAvailable && !flags.force) return out("Already on the latest version.");
|
|
10805
|
+
if (process.platform !== "darwin") return out("Automatic installation is not supported on this OS yet. Use the release/download link above.");
|
|
10806
|
+
if (!artifact || !artifact.url) fail("Could not find a DMG for this Mac.");
|
|
9470
10807
|
await installMacDesktopUpdate(release, artifact, flags);
|
|
9471
10808
|
}
|
|
9472
10809
|
|
|
9473
10810
|
function requirePath(commandPath, label) {
|
|
9474
|
-
if (!fs.existsSync(commandPath)) fail(
|
|
10811
|
+
if (!fs.existsSync(commandPath)) fail(`Required update tool not found: ${label}`);
|
|
9475
10812
|
return commandPath;
|
|
9476
10813
|
}
|
|
9477
10814
|
|
|
@@ -9807,9 +11144,9 @@ async function installMacDesktopUpdate(release, artifact, flags) {
|
|
|
9807
11144
|
const stagingPath = path.join(targetDir, `.${targetName}.installing.${transactionId}.app`);
|
|
9808
11145
|
|
|
9809
11146
|
try {
|
|
9810
|
-
out(
|
|
11147
|
+
out(`Download: ${fileName}`);
|
|
9811
11148
|
await downloadUpdateFile(validatedArtifact.url, dmgPath, validatedArtifact);
|
|
9812
|
-
out("
|
|
11149
|
+
out("Verify: DMG, notarization, Gatekeeper");
|
|
9813
11150
|
await runCommand(hdiutil, ["verify", dmgPath]);
|
|
9814
11151
|
await runCommand(xcrun, ["stapler", "validate", dmgPath]);
|
|
9815
11152
|
await runCommand(spctl, ["-a", "-t", "open", "--context", "context:primary-signature", "-vv", dmgPath]);
|
|
@@ -9818,16 +11155,16 @@ async function installMacDesktopUpdate(release, artifact, flags) {
|
|
|
9818
11155
|
mountPoint = parseHdiutilMountPoint(mount.stdout);
|
|
9819
11156
|
const sourceApp = mountPoint ? path.join(mountPoint, "Agentlas.app") : "";
|
|
9820
11157
|
if (!sourceApp || !fs.existsSync(sourceApp)) {
|
|
9821
|
-
throw updateTransferError("AGENTLAS_UPDATE_APP_MISSING", "
|
|
11158
|
+
throw updateTransferError("AGENTLAS_UPDATE_APP_MISSING", "Agentlas.app was not found in the DMG.");
|
|
9822
11159
|
}
|
|
9823
11160
|
|
|
9824
11161
|
const installedVersion = await runCommand(plistBuddy, ["-c", "Print :CFBundleShortVersionString", path.join(sourceApp, "Contents", "Info.plist")], { capture: true });
|
|
9825
11162
|
const appVersion = installedVersion.stdout.trim();
|
|
9826
11163
|
if (appVersion !== String(release.version)) {
|
|
9827
|
-
throw updateTransferError("AGENTLAS_UPDATE_VERSION_MISMATCH",
|
|
11164
|
+
throw updateTransferError("AGENTLAS_UPDATE_VERSION_MISMATCH", `App version does not match the release: release=${release.version} app=${appVersion}`);
|
|
9828
11165
|
}
|
|
9829
11166
|
|
|
9830
|
-
out("
|
|
11167
|
+
out("Install: quit the existing Agentlas app and replace it");
|
|
9831
11168
|
await runCommand(osascript, ["-e", 'tell application "Agentlas" to quit'], { capture: true, allowFailure: true });
|
|
9832
11169
|
await sleep(2_000);
|
|
9833
11170
|
const replacement = await replaceMacAppBundle({
|
|
@@ -9838,10 +11175,10 @@ async function installMacDesktopUpdate(release, artifact, flags) {
|
|
|
9838
11175
|
runCommand,
|
|
9839
11176
|
commands: { codesign, spctl, ditto, mv, rm },
|
|
9840
11177
|
});
|
|
9841
|
-
if (replacement.backupRetained) out(
|
|
11178
|
+
if (replacement.backupRetained) out(`Warning: the verified app was installed, but the previous app backup could not be removed: ${replacement.backupPath}`);
|
|
9842
11179
|
if (fs.existsSync(lsregister)) await runCommand(lsregister, ["-f", targetApp], { allowFailure: true });
|
|
9843
11180
|
if (flags.launch) await runCommand(open, ["-a", "Agentlas"], { allowFailure: true });
|
|
9844
|
-
out(`Agentlas ${release.version}
|
|
11181
|
+
out(`Agentlas ${release.version} installed.`);
|
|
9845
11182
|
} finally {
|
|
9846
11183
|
if (mountPoint) await runCommand(hdiutil, ["detach", mountPoint], { allowFailure: true });
|
|
9847
11184
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
@@ -9933,32 +11270,32 @@ function oberonScaffold(args) {
|
|
|
9933
11270
|
};
|
|
9934
11271
|
if (flags.titles) manifest.titles = oberonSampleTitles(title);
|
|
9935
11272
|
fs.writeFileSync(outPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
9936
|
-
out(`✓
|
|
9937
|
-
out(` ·
|
|
9938
|
-
out(` ·
|
|
9939
|
-
out(` ·
|
|
9940
|
-
if (!flags.titles) out(` ·
|
|
11273
|
+
out(`✓ Manifest created: ${outPath}`);
|
|
11274
|
+
out(` · ${shotCount} shots · ${aspect} · ${manifest.provider}`);
|
|
11275
|
+
out(` · fill prompts, then run: agentlas oberon render ${path.basename(outPath)}`);
|
|
11276
|
+
out(` · or use an agent: agentlas run oberon-film-studio "30-second fragrance ad trailer"`);
|
|
11277
|
+
if (!flags.titles) out(` · use --titles to include title/subtitle burn-in samples`);
|
|
9941
11278
|
}
|
|
9942
11279
|
|
|
9943
11280
|
function oberonRender(args) {
|
|
9944
11281
|
const { flags, rest } = oberonParseFlags(args);
|
|
9945
|
-
if (!rest[0]) fail("
|
|
11282
|
+
if (!rest[0]) fail("A manifest path is required: agentlas oberon render <manifest.json>");
|
|
9946
11283
|
const manifestPath = path.resolve(rest[0]);
|
|
9947
|
-
if (!fs.existsSync(manifestPath)) fail(
|
|
11284
|
+
if (!fs.existsSync(manifestPath)) fail(`Manifest not found: ${manifestPath}`);
|
|
9948
11285
|
|
|
9949
11286
|
let manifest;
|
|
9950
11287
|
try {
|
|
9951
11288
|
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
9952
11289
|
} catch (e) {
|
|
9953
|
-
return fail(
|
|
11290
|
+
return fail(`Failed to parse manifest JSON: ${e.message}`);
|
|
9954
11291
|
}
|
|
9955
|
-
if (!Array.isArray(manifest.shots) || !manifest.shots.length) fail("
|
|
11292
|
+
if (!Array.isArray(manifest.shots) || !manifest.shots.length) fail("The manifest has no shots[].");
|
|
9956
11293
|
|
|
9957
11294
|
const root = oberonRepoRoot();
|
|
9958
11295
|
const script = path.join(root, "scripts", "render-oberon-live-request.cjs");
|
|
9959
11296
|
const builtRender = path.join(root, "dist", "electron", "oberon", "render.js");
|
|
9960
|
-
if (!fs.existsSync(script)) fail(
|
|
9961
|
-
if (!fs.existsSync(builtRender)) fail(`Electron
|
|
11297
|
+
if (!fs.existsSync(script)) fail(`Headless render script not found (not included in the packaged app): ${script}`);
|
|
11298
|
+
if (!fs.existsSync(builtRender)) fail(`An Electron build is required. Run npm run build:electron first (missing: ${builtRender})`);
|
|
9962
11299
|
|
|
9963
11300
|
// --max-shots 등 오버라이드가 있으면 사용자 매니페스트는 그대로 두고 임시 패치본을 만든다.
|
|
9964
11301
|
let reqPath = manifestPath;
|
|
@@ -9983,18 +11320,18 @@ function oberonRender(args) {
|
|
|
9983
11320
|
if (flags["poll-ms"]) childEnv.OBERON_LIVE_POLL_MS = String(flags["poll-ms"]);
|
|
9984
11321
|
if (flags.open) childEnv.OBERON_LIVE_OPEN_DELIVERY = "1";
|
|
9985
11322
|
|
|
9986
|
-
out(`▶ Oberon
|
|
9987
|
-
out(`
|
|
9988
|
-
out(`
|
|
9989
|
-
if (manifest.titles) out(`
|
|
11323
|
+
out(`▶ Oberon render: "${manifest.title}" (${manifest.shots.length} shots, max ${overrides.maxShots ?? manifest.maxShots ?? 3})`);
|
|
11324
|
+
out(` Manifest: ${manifestPath}`);
|
|
11325
|
+
out(` Delivery folder: ${deliveryDir}`);
|
|
11326
|
+
if (manifest.titles) out(` title/subtitle burn-in: enabled → generating additional *_titled.mp4`);
|
|
9990
11327
|
|
|
9991
11328
|
if (flags["dry-run"]) {
|
|
9992
|
-
out("\n[dry-run]
|
|
11329
|
+
out("\n[dry-run] Command to run:");
|
|
9993
11330
|
out(` ${process.execPath} ${script}`);
|
|
9994
11331
|
out(" env: OBERON_LIVE_VEO=1");
|
|
9995
11332
|
out(` OBERON_LIVE_REQUEST_FILE=${reqPath}`);
|
|
9996
11333
|
out(` OBERON_LIVE_DELIVERY_DIR=${deliveryDir}`);
|
|
9997
|
-
|
|
11334
|
+
out(" (full Electron · GEMINI_API_KEY/GOOGLE_CLOUD_PROJECT vault required)");
|
|
9998
11335
|
return;
|
|
9999
11336
|
}
|
|
10000
11337
|
|
|
@@ -10015,11 +11352,11 @@ function oberonRender(args) {
|
|
|
10015
11352
|
child.stderr.on("data", (c) => process.stderr.write(c));
|
|
10016
11353
|
child.on("close", (code) => {
|
|
10017
11354
|
if (code === 0) {
|
|
10018
|
-
out(`\n✓
|
|
11355
|
+
out(`\n✓ Render complete — delivery folder: ${deliveryDir}`);
|
|
10019
11356
|
const titled = files.filter((f) => f.kind && f.kind.startsWith("titled"));
|
|
10020
|
-
if (titled.length) out(`
|
|
11357
|
+
if (titled.length) out(` title/subtitle burn-in files: ${titled.map((f) => f.name).join(", ")}`);
|
|
10021
11358
|
} else {
|
|
10022
|
-
process.stderr.write(`\n✖
|
|
11359
|
+
process.stderr.write(`\n✖ Render failed (exit ${code})\n`);
|
|
10023
11360
|
process.exitCode = code || 1;
|
|
10024
11361
|
}
|
|
10025
11362
|
resolve();
|
|
@@ -10077,7 +11414,7 @@ function slugifyOberon(value) {
|
|
|
10077
11414
|
function oberonList() {
|
|
10078
11415
|
const dir = path.join(userDataDir(), "oberon");
|
|
10079
11416
|
if (!fs.existsSync(dir)) {
|
|
10080
|
-
out("
|
|
11417
|
+
out("No render outputs yet. Start with agentlas oberon scaffold my.json.");
|
|
10081
11418
|
return;
|
|
10082
11419
|
}
|
|
10083
11420
|
const entries = fs
|
|
@@ -10101,40 +11438,40 @@ function oberonList() {
|
|
|
10101
11438
|
.sort((a, b) => b.mtime - a.mtime)
|
|
10102
11439
|
.slice(0, 15);
|
|
10103
11440
|
if (!entries.length) {
|
|
10104
|
-
out("
|
|
11441
|
+
out("No render outputs yet.");
|
|
10105
11442
|
return;
|
|
10106
11443
|
}
|
|
10107
|
-
out(
|
|
11444
|
+
out(`Recent Oberon renders (${dir}):\n`);
|
|
10108
11445
|
for (const e of entries) {
|
|
10109
11446
|
const masters = e.files.filter((f) => /master|titled/.test(f) && /\.(mp4|mov)$/.test(f));
|
|
10110
11447
|
const when = e.mtime ? new Date(e.mtime).toISOString().slice(0, 16).replace("T", " ") : "";
|
|
10111
11448
|
out(` ${when} ${e.name}`);
|
|
10112
11449
|
if (masters.length) out(` ${masters.join(", ")}`);
|
|
10113
11450
|
}
|
|
10114
|
-
out(`\
|
|
11451
|
+
out(`\nOpen the folder with: agentlas oberon open`);
|
|
10115
11452
|
}
|
|
10116
11453
|
|
|
10117
11454
|
function oberonOpen(args) {
|
|
10118
11455
|
const target = args[0] ? path.resolve(args[0]) : path.join(userDataDir(), "oberon");
|
|
10119
|
-
if (!fs.existsSync(target)) fail(
|
|
11456
|
+
if (!fs.existsSync(target)) fail(`Path not found: ${target}`);
|
|
10120
11457
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
10121
11458
|
spawn(opener, [target], { detached: true, stdio: "ignore" }).unref();
|
|
10122
|
-
out(
|
|
11459
|
+
out(`Opening folder: ${target}`);
|
|
10123
11460
|
}
|
|
10124
11461
|
|
|
10125
11462
|
function oberonHelp() {
|
|
10126
11463
|
out(
|
|
10127
11464
|
[
|
|
10128
|
-
"agentlas oberon —
|
|
11465
|
+
"agentlas oberon — AI film rendering from the terminal",
|
|
10129
11466
|
"",
|
|
10130
11467
|
" oberon scaffold [out.json] [--title T] [--aspect 16:9] [--shots N] [--titles]",
|
|
10131
|
-
"
|
|
11468
|
+
" create an editable render manifest (--titles: include title/subtitle burn-in samples)",
|
|
10132
11469
|
" oberon render <manifest.json> [--delivery DIR] [--max-shots N] [--open] [--dry-run]",
|
|
10133
|
-
" full Electron
|
|
11470
|
+
" spawn full Electron render + stream progress (GEMINI_API_KEY vault required)",
|
|
10134
11471
|
" oberon list 최근 렌더 산출물",
|
|
10135
11472
|
" oberon open [path] 산출물 폴더 열기",
|
|
10136
11473
|
"",
|
|
10137
|
-
"
|
|
11474
|
+
"Fill prompts directly, or ask an agent: agentlas run oberon-film-studio \"30-second fragrance ad\"",
|
|
10138
11475
|
].join("\n"),
|
|
10139
11476
|
);
|
|
10140
11477
|
}
|
|
@@ -10158,7 +11495,7 @@ async function cmdOberon(args) {
|
|
|
10158
11495
|
case "-h":
|
|
10159
11496
|
return oberonHelp();
|
|
10160
11497
|
default:
|
|
10161
|
-
fail(
|
|
11498
|
+
fail(`Unknown oberon subcommand: ${sub} (scaffold|render|list|open|help)`);
|
|
10162
11499
|
}
|
|
10163
11500
|
}
|
|
10164
11501
|
|
|
@@ -10176,26 +11513,31 @@ function cmdHelp() {
|
|
|
10176
11513
|
hdr("TALK & RUN"),
|
|
10177
11514
|
" <agent> jump into a chat with one agent (e.g. agentlas seo)",
|
|
10178
11515
|
" run [agent] [prompt] one-shot — omit agent to auto-route (reads stdin if no prompt)",
|
|
11516
|
+
" --experience-desktop-loadout use Desktop's fresh exact Operational/Taste loadout receipt",
|
|
11517
|
+
" --no-experience highest precedence; do not read or inject a loadout",
|
|
10179
11518
|
" firm <firm> [cmd] delegate to a company's CEO (interactive if no cmd)",
|
|
10180
11519
|
" chats [n] recent conversations · chat resume in REPL: /resume",
|
|
10181
11520
|
"",
|
|
10182
11521
|
hdr("AGENTS & HUB (Agentlas OS surface)"),
|
|
10183
11522
|
" search \"<what you need>\" discover agents in the Hub + local (hep-search)",
|
|
10184
11523
|
" install <slug> install an agent from the Hub (hep-cloud)",
|
|
11524
|
+
" plugin add <slug> install a Hub plugin (MCP servers) into this machine",
|
|
10185
11525
|
" build \"<request>\" build/repair/package an agent or team (hep-build)",
|
|
10186
11526
|
" upload <path> save owner-private in Agent Cloud (default) (hep-upload)",
|
|
10187
11527
|
" --visibility marketplace explicit compatibility flag: publish to Hub",
|
|
10188
11528
|
" connect [<sub>] wire Telegram / platforms to an agent team (hep-connect)",
|
|
10189
11529
|
" import <path> import a local agent/team folder",
|
|
10190
11530
|
" list installed agents/companies + active runtime",
|
|
10191
|
-
" experience <sub> portable Experience: validate|save|publish|status|export|
|
|
10192
|
-
" legacy local intents
|
|
11531
|
+
" experience <sub> portable Experience: list|inspect|validate|save|publish|status|export|unpublish",
|
|
11532
|
+
" legacy local intents require explicit legacy-* commands",
|
|
10193
11533
|
" variant resolve local variant selection: selected|fallback|base-only|error",
|
|
10194
11534
|
"",
|
|
10195
11535
|
hdr("EXECUTE"),
|
|
10196
|
-
" storm <goal>
|
|
11536
|
+
" storm <goal> Agentlas Goal+UltraCode harness: plan → allocate → execute → verify [--research]",
|
|
10197
11537
|
" swarm <goal> emergent agent swarm — parallel workers + synthesizer [--parallel N]",
|
|
10198
|
-
" network <request>
|
|
11538
|
+
" network <request> host-LLM workforce ontology → exact TF → execute [--benchmark]",
|
|
11539
|
+
" workforce <request> same Agent Workforce Ontology route (explicit name)",
|
|
11540
|
+
" legacy-network <request> compatibility-only Hephaestus network route",
|
|
10199
11541
|
" call \"a,b\" \"<ctx>\" invoke named Hub/Cloud agents (hep-call)",
|
|
10200
11542
|
" browser [<sub>] real browser execution hardpoint (hep-browser)",
|
|
10201
11543
|
" route \"<request>\" routing preview — which agent/pipeline would take this",
|
|
@@ -10258,7 +11600,7 @@ async function main() {
|
|
|
10258
11600
|
runtimeOverride = argv[++i];
|
|
10259
11601
|
} else if (argv[i] === "--permission" || argv[i] === "-P") {
|
|
10260
11602
|
const p = (argv[++i] || "").toLowerCase();
|
|
10261
|
-
if (!["read", "write", "full"].includes(p)) fail(
|
|
11603
|
+
if (!["read", "write", "full"].includes(p)) fail(`Unknown permission: ${p} (read|write|full)`);
|
|
10262
11604
|
PERMISSION = p;
|
|
10263
11605
|
PERMISSION_EXPLICIT = true;
|
|
10264
11606
|
} else {
|
|
@@ -10319,10 +11661,16 @@ async function main() {
|
|
|
10319
11661
|
return cmdCloud(db, rest.slice(1), runtimeOverride);
|
|
10320
11662
|
case "creds":
|
|
10321
11663
|
return cmdCreds(db, rest.slice(1));
|
|
10322
|
-
case "storm":
|
|
10323
|
-
|
|
10324
|
-
|
|
10325
|
-
return parity().
|
|
11664
|
+
case "storm": {
|
|
11665
|
+
const cwd = projectCwd();
|
|
11666
|
+
const projectPath = ensureTerminalProjectForExecutionCli(db, cwd, PERMISSION, "terminal-storm");
|
|
11667
|
+
return parity().cmdStorm(db, rest.slice(1), runtimeOverride, { cwd, projectPath, permission: PERMISSION });
|
|
11668
|
+
}
|
|
11669
|
+
case "swarm": {
|
|
11670
|
+
const cwd = projectCwd();
|
|
11671
|
+
const projectPath = ensureTerminalProjectForExecutionCli(db, cwd, PERMISSION, "terminal-swarm");
|
|
11672
|
+
return parity().cmdSwarm(db, rest.slice(1), runtimeOverride, { cwd, projectPath, permission: PERMISSION });
|
|
11673
|
+
}
|
|
10326
11674
|
case "automation":
|
|
10327
11675
|
case "automations":
|
|
10328
11676
|
return parity().cmdAutomation(db, rest.slice(1), runtimeOverride);
|
|
@@ -10333,6 +11681,7 @@ async function main() {
|
|
|
10333
11681
|
case "build":
|
|
10334
11682
|
// Terminal-owned preflight: trusted system-global MCP metadata first, one consent,
|
|
10335
11683
|
// then pass only approved catalog IDs/value-free shortages to the existing builder.
|
|
11684
|
+
ensureTerminalProjectForExecutionCli(db, projectCwd(), PERMISSION, "terminal-build");
|
|
10336
11685
|
return terminalAssets.cmdBuild({
|
|
10337
11686
|
db,
|
|
10338
11687
|
args: rest.slice(1),
|
|
@@ -10341,7 +11690,8 @@ async function main() {
|
|
|
10341
11690
|
input: process.stdin,
|
|
10342
11691
|
promptOutput: process.stderr,
|
|
10343
11692
|
out,
|
|
10344
|
-
|
|
11693
|
+
probeMcpServer: (server, probeOptions) => probeApprovedTerminalMcp(db, server, runtimeOverride, projectCwd(), probeOptions),
|
|
11694
|
+
invokeBuild: (request, metadata) => runTerminalBuilder(db, request, metadata, runtimeOverride, projectCwd()),
|
|
10345
11695
|
});
|
|
10346
11696
|
case "experience":
|
|
10347
11697
|
return terminalExperienceExchange.cmdExperienceExchange({
|
|
@@ -10363,13 +11713,20 @@ async function main() {
|
|
|
10363
11713
|
out,
|
|
10364
11714
|
});
|
|
10365
11715
|
case "search": // hep-search — 에이전트 디렉터리 발견 (Hub + 로컬)
|
|
10366
|
-
if (!rest[1]) return fail('usage: agentlas search "
|
|
11716
|
+
if (!rest[1]) return fail('usage: agentlas search "<what you need>" [--limit 10]');
|
|
10367
11717
|
return parity().cloudSearch(db, rest.slice(1));
|
|
10368
11718
|
case "install": // public Hub package install — slug로 에이전트 설치
|
|
10369
|
-
if (!rest[1]) return fail('usage: agentlas install <slug> (
|
|
11719
|
+
if (!rest[1]) return fail('usage: agentlas install <slug> (run agentlas search "what you need" first)');
|
|
10370
11720
|
return cmdCloudInstall(db, rest[1]);
|
|
11721
|
+
case "plugin": // Hub 플러그인(MCP 서버 번들) — 에이전트 설치(install)와 다른 카탈로그다
|
|
11722
|
+
case "plugins": {
|
|
11723
|
+
const action = rest[1];
|
|
11724
|
+
if (action === "add") return cmdPluginAdd(db, rest[2]);
|
|
11725
|
+
if (action === "list" || !action) return cmdPluginList();
|
|
11726
|
+
return fail("usage: agentlas plugin add <slug> | agentlas plugin list");
|
|
11727
|
+
}
|
|
10371
11728
|
case "upload": { // 기본은 owner-private Agent Cloud, public Hub는 명시 flag로만.
|
|
10372
|
-
if (!rest[1]) return fail("usage: agentlas upload
|
|
11729
|
+
if (!rest[1]) return fail("usage: agentlas upload <agent-folder-path> [--visibility marketplace]");
|
|
10373
11730
|
const uploadArgs = rest.slice(1);
|
|
10374
11731
|
return cmdCloud(db, [cloudActionForTopLevelUpload(uploadArgs), ...uploadArgs], runtimeOverride);
|
|
10375
11732
|
}
|
|
@@ -10379,8 +11736,14 @@ async function main() {
|
|
|
10379
11736
|
return parity().cmdHep(db, ["hep-browser", ...rest.slice(1)]);
|
|
10380
11737
|
case "call": // hep-call — 지정 에이전트 호출/준비
|
|
10381
11738
|
return parity().cmdHep(db, ["hep-call", ...rest.slice(1)]);
|
|
10382
|
-
case "
|
|
10383
|
-
case "
|
|
11739
|
+
case "workforce":
|
|
11740
|
+
case "network":
|
|
11741
|
+
case "taskforce": {
|
|
11742
|
+
const cwd = projectCwd();
|
|
11743
|
+
const projectPath = ensureTerminalProjectForExecutionCli(db, cwd, PERMISSION, "terminal-workforce");
|
|
11744
|
+
return workforce().cmdWorkforce(db, rest.slice(1), runtimeOverride, { cwd, projectPath, permission: PERMISSION });
|
|
11745
|
+
}
|
|
11746
|
+
case "legacy-network": // explicit compatibility escape hatch only
|
|
10384
11747
|
return parity().cmdHep(db, ["hep-network", ...rest.slice(1)]);
|
|
10385
11748
|
case "route": // 라우팅 미리보기 (실행 없음)
|
|
10386
11749
|
return parity().cmdHep(
|
|
@@ -10429,7 +11792,7 @@ async function main() {
|
|
|
10429
11792
|
if (firm) return cmdFirm(db, cmd, "", runtimeOverride);
|
|
10430
11793
|
const prompt = rest.join(" ").trim();
|
|
10431
11794
|
if (prompt) return cmdAutoRun(db, prompt, runtimeOverride);
|
|
10432
|
-
fail(
|
|
11795
|
+
fail(`Agent/company not found: ${cmd} (check with agentlas list)`);
|
|
10433
11796
|
}
|
|
10434
11797
|
}
|
|
10435
11798
|
}
|
|
@@ -10463,6 +11826,7 @@ module.exports = {
|
|
|
10463
11826
|
replaceMacAppBundle,
|
|
10464
11827
|
captureRuntime,
|
|
10465
11828
|
buildArgs,
|
|
11829
|
+
codexCaptureAgentText,
|
|
10466
11830
|
captureOutputLimit,
|
|
10467
11831
|
materializeCloudListingCli,
|
|
10468
11832
|
recoverCloudInstallJournalCli,
|
|
@@ -10483,6 +11847,27 @@ module.exports = {
|
|
|
10483
11847
|
cloudPortablePathConflict,
|
|
10484
11848
|
cloudPortableExecutableForFile,
|
|
10485
11849
|
parseRunExperienceArgs,
|
|
11850
|
+
resolveRuntimeExperienceCli,
|
|
11851
|
+
runTerminalBuilder,
|
|
11852
|
+
resolveRuntime,
|
|
11853
|
+
listAvailableRuntimes,
|
|
11854
|
+
probeApprovedTerminalMcp,
|
|
11855
|
+
finalizeExperienceExecutionCli,
|
|
11856
|
+
buildChildEnvCli,
|
|
11857
|
+
augmentSystem,
|
|
11858
|
+
beginMemoryTurnCli,
|
|
11859
|
+
completeMemoryTurnCli,
|
|
11860
|
+
curatorRuntimeEnvCli,
|
|
11861
|
+
ensureGeminiNoToolsPolicyCli,
|
|
11862
|
+
curateCliReply,
|
|
11863
|
+
TERMINAL_MEMORY_CORE,
|
|
11864
|
+
TERMINAL_MEMORY_CORE_MAX_TOKENS,
|
|
11865
|
+
approximatePromptTokens,
|
|
11866
|
+
memoryEmitterPromptFor,
|
|
11867
|
+
credentialIndexReminderFor,
|
|
11868
|
+
ensureCoreProjectCli,
|
|
11869
|
+
ensureTerminalProjectForExecutionCli,
|
|
11870
|
+
ensureAgentlasProjectStateIgnoreCli,
|
|
10486
11871
|
DEFAULT_API_MODEL,
|
|
10487
11872
|
ANTHROPIC_COMPAT_API,
|
|
10488
11873
|
// 자동 라우팅 회귀 테스트 표면 — 약한 매치 직답/오라우팅 방지 규칙 검증용.
|