agentlas 0.5.5 → 0.7.0
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/README.md +73 -0
- package/engine/agentlas-capabilities.cjs +34 -3
- package/engine/agentlas-composer.cjs +3 -0
- package/engine/agentlas-experience-exchange.cjs +1401 -0
- package/engine/agentlas-experience-mcp.cjs +1147 -0
- package/engine/agentlas-i18n.cjs +16 -0
- package/engine/agentlas-input.cjs +2 -0
- package/engine/agentlas-repl.cjs +161 -38
- package/engine/agentlas-ui.cjs +5 -1
- package/engine/agentlas.cjs +348 -53
- package/package.json +1 -1
- package/test/cloud-save-publish.cjs +34 -0
- package/test/engine-hardening-regression.cjs +74 -0
- package/test/experience-exchange-contract.cjs +569 -0
- package/test/experience-mcp-contract.cjs +391 -0
- package/test/fixtures/portable-experience-bundle-v1-golden.json +124 -0
- package/test/route-regression.cjs +357 -0
- package/test/runtime-env-protection.cjs +45 -1
- package/test/smoke.sh +4 -0
- package/test/terminal-ui-regression.cjs +26 -3
package/engine/agentlas.cjs
CHANGED
|
@@ -29,6 +29,8 @@ const fs = require("node:fs");
|
|
|
29
29
|
const { spawn } = require("node:child_process");
|
|
30
30
|
const crypto = require("node:crypto");
|
|
31
31
|
const { compareSemVer, normalizeSemVer, parseSemVer } = require("./semver.cjs");
|
|
32
|
+
const terminalAssets = require("./agentlas-experience-mcp.cjs");
|
|
33
|
+
const terminalExperienceExchange = require("./agentlas-experience-exchange.cjs");
|
|
32
34
|
|
|
33
35
|
// ── 앱과 동일한 userData 경로 (electron app.getPath('userData')와 일치) ──
|
|
34
36
|
function userDataDir() {
|
|
@@ -93,6 +95,19 @@ function openNodeSqliteDb(p) {
|
|
|
93
95
|
run: (...args) => stmt.run(...args),
|
|
94
96
|
};
|
|
95
97
|
},
|
|
98
|
+
// better-sqlite3 API 패리티 — 폴백 경로에서도 db.exec/db.pragma가 있어야 한다.
|
|
99
|
+
// 누락 시 ensureMemoryContextColumn 등 ALTER TABLE(exec)이 TypeError로 조용히 죽어
|
|
100
|
+
// (try/catch 삼킴) context_json 컬럼 마이그레이션이 되지 않고 memory 조회가 깨진다.
|
|
101
|
+
exec: (sql) => db.exec(sql),
|
|
102
|
+
pragma: (source) => {
|
|
103
|
+
const rows = db.prepare(`PRAGMA ${source}`).all();
|
|
104
|
+
// better-sqlite3 pragma()의 단일값 반환 관례를 근사(단일 컬럼·단일 행 → 스칼라).
|
|
105
|
+
if (rows.length === 1) {
|
|
106
|
+
const keys = Object.keys(rows[0]);
|
|
107
|
+
if (keys.length === 1) return rows[0][keys[0]];
|
|
108
|
+
}
|
|
109
|
+
return rows;
|
|
110
|
+
},
|
|
96
111
|
transaction(fn) {
|
|
97
112
|
return (...args) => {
|
|
98
113
|
db.exec("BEGIN");
|
|
@@ -280,7 +295,17 @@ const AGENT_BUILD_TERMS = [
|
|
|
280
295
|
const BUILD_ENTITY_RE = /(에이전트|agent|팀|team|회사|company)/i;
|
|
281
296
|
const BUILD_VERB_RE = /(만들|만든|생성|구축|구성해|꾸려|세팅|패키징|scaffold|build|create|\bmake\b|set\s?up|spin\s?up)/i;
|
|
282
297
|
function isAgentBuildIntent(prompt) {
|
|
283
|
-
|
|
298
|
+
// 경로/파일 참조는 빌드 의도의 증거가 아니다 — "/Users/x/agent-tools/notes.md 요약본
|
|
299
|
+
// 만들어줘"의 디렉터리명이나 "agent-notes.md" 같은 파일명이 BUILD_ENTITY_RE를 때려
|
|
300
|
+
// 메타빌더(score 1000)로 직행하던 우회로 차단. 빌드 의도는 산문에서만 읽는다.
|
|
301
|
+
// ⚠️ 남은 슬래시는 통째로 지우지 않고 공백으로만 벌린다 — "에이전트/팀 만들어줘"의
|
|
302
|
+
// 슬래시-엔티티("에이전트/팀")를 삭제하면 BUILD_ENTITY_RE가 못 맞아 빌드 의도를 놓친다.
|
|
303
|
+
// 진짜 경로는 이미 routeStripPaths(마지막 세그먼트만)+확장자 제거가 처리했다.
|
|
304
|
+
const p = routeNormalize(
|
|
305
|
+
routeStripPaths(prompt)
|
|
306
|
+
.replace(/\S+\.[A-Za-z0-9]{1,6}(?=\s|$)/g, " ")
|
|
307
|
+
.replace(/[\\/]+/g, " "),
|
|
308
|
+
);
|
|
284
309
|
if (!p.trim() || isTrivialRoutePrompt(p)) return false;
|
|
285
310
|
if (AGENT_BUILD_TERMS.some((term) => p.includes(routeNormalize(term)))) return true;
|
|
286
311
|
// 예: "단일 에이전트 하나만 만들어줘", "팀 좀 꾸려줘", "make me an agent"
|
|
@@ -300,7 +325,11 @@ function resolveMetaBuilder(db) {
|
|
|
300
325
|
}
|
|
301
326
|
return null;
|
|
302
327
|
}
|
|
303
|
-
|
|
328
|
+
// "ai"/"llm" 같은 초범용 토큰은 모든 에이전트 프롬프트에 나오므로 판별력이 0이다 —
|
|
329
|
+
// 이런 단어 하나로 전문 에이전트가 선택되던 오라우팅(예: 일반 맥 질문 → Pitch Deck Architect)을 막는다.
|
|
330
|
+
// "local"/"imported"/"team"은 임포터 보일러플레이트("Imported local team")와 slug 접두/접미에
|
|
331
|
+
// 편재해 판별력이 없다 — 'team' 한 단어가 아무 임포트 팀의 slug 부분문자열(+6 strong)을 때리던 구멍.
|
|
332
|
+
const ROUTE_STOP_WORDS = new Set(["the", "and", "for", "with", "this", "that", "from", "into", "make", "build", "create", "agent", "agents", "team", "please", "ai", "llm", "local", "imported", "인공지능", "에이아이", "좀", "해주세요", "해줘", "만들어", "붙여", "연결", "작업", "요청"]);
|
|
304
333
|
const ROUTE_HINTS = [
|
|
305
334
|
{
|
|
306
335
|
slug: "agentlas-app-builder",
|
|
@@ -358,20 +387,46 @@ const ROUTE_HINTS = [
|
|
|
358
387
|
function routeNormalize(value) {
|
|
359
388
|
return String(value || "").toLowerCase().replace(/[_/]+/g, "-");
|
|
360
389
|
}
|
|
390
|
+
// 경로 디렉터리 성분은 라우팅 의도가 아니다 — 마지막 세그먼트(파일/폴더명)만 남긴다.
|
|
391
|
+
// 사고(2026-07-12): "/Users/mason/Documents/…/Appbridge_Template.이 …" 프롬프트의 경로 토큰
|
|
392
|
+
// ("users","mason","documents","users-mason-documents-")이 임포트 에이전트 system_prompt 속
|
|
393
|
+
// 절대경로와 맞아떨어져 +2씩 쌓이고 라우팅 근거에까지 노출됐다. 프롬프트/헤이스택 양쪽에
|
|
394
|
+
// 대칭 적용해 경로↔경로 우연 일치를 차단한다. 파일/폴더명은 실제 의도라서 보존한다.
|
|
395
|
+
// 규칙: 공백/인용부호/괄호 뒤(또는 문자열 시작)에서 시작하고, "세그먼트+구분자"가 2회 이상
|
|
396
|
+
// 이어지는 절대·홈·드라이브·UNC 경로만 경로로 본다 — "and/or", "서울/부산", 날짜(2026/07/12),
|
|
397
|
+
// "https://…"(콜론 뒤 //는 시작 조건 불충족)는 건드리지 않는다. 세그먼트 안의 단일 공백은
|
|
398
|
+
// 뒤가 대문자로 시작할 때만 허용해 "Mobile Documents"/"Application Support"는 접되,
|
|
399
|
+
// "/tmp/out 기획/디자인 …" 같은 한글 프로즈를 경로로 삼켜버리지 않는다. 상대경로는
|
|
400
|
+
// 확장자 있는 파일 참조("docs/plan/roadmap.md")만 접는다 — 디렉터리명("plan")이 힌트/이름
|
|
401
|
+
// strong 채널을 때리는 것을 막으면서 "서울/부산/대구" 같은 나열은 보존한다.
|
|
402
|
+
const ROUTE_PATH_RE = /(^|[\s"'`(<\[{])((?:~|[A-Za-z]:)?[\\/]{1,2}(?:[^\s\\/]+(?: [A-Z][^\s\\/]*)?[\\/]+){2,}[^\s\\/]*|(?:[^\s\\/]+[\\/]+){2,}[^\s\\/]+\.[A-Za-z0-9]{1,6})/g;
|
|
403
|
+
function routeStripPaths(value) {
|
|
404
|
+
return String(value || "").replace(ROUTE_PATH_RE, (whole, pre, p) => {
|
|
405
|
+
const segs = p.split(/[\\/]+/).filter(Boolean);
|
|
406
|
+
return pre + (segs.length ? segs[segs.length - 1] : "");
|
|
407
|
+
});
|
|
408
|
+
}
|
|
361
409
|
function routeTokenize(value) {
|
|
362
|
-
|
|
410
|
+
// 매치가 영숫자로 끝나도록 강제해 "users-mason-documents-" 같은 후행 하이픈 토큰을 원천 차단.
|
|
411
|
+
const matches = routeNormalize(routeStripPaths(value)).match(/[a-z0-9][a-z0-9-]*[a-z0-9]|[가-힣]{2,}/g) || [];
|
|
363
412
|
const expanded = matches.flatMap((term) => term.split("-").filter(Boolean).concat(term));
|
|
364
413
|
return [...new Set(expanded.filter((term) => term.length >= 2 && !ROUTE_STOP_WORDS.has(term)))];
|
|
365
414
|
}
|
|
415
|
+
// 정체성 존(slug/이름/태그라인) — 여기 적중은 강한 라우팅 신호. system_prompt 본문 적중은 약한 신호.
|
|
416
|
+
// 임포터 보일러플레이트 태그라인("Imported local team/agent")의 세 단어는 전부 스톱워드라
|
|
417
|
+
// 프롬프트 토큰이 될 수 없다 — 별도 필터 불필요.
|
|
418
|
+
function routeIdentityHaystack(agent) {
|
|
419
|
+
return routeNormalize(routeStripPaths([agent.slug, agent.name, agent.name_en, agent.tagline, agent.tagline_en].join("\n")));
|
|
420
|
+
}
|
|
366
421
|
function routeHaystack(agent) {
|
|
367
|
-
return routeNormalize([
|
|
422
|
+
return routeNormalize(routeStripPaths([
|
|
368
423
|
agent.slug,
|
|
369
424
|
agent.name,
|
|
370
425
|
agent.name_en,
|
|
371
426
|
agent.tagline,
|
|
372
427
|
agent.tagline_en,
|
|
373
428
|
String(agent.system_prompt || "").slice(0, 3500),
|
|
374
|
-
].join("\n"));
|
|
429
|
+
].join("\n")));
|
|
375
430
|
}
|
|
376
431
|
const APP_BUILDER_EXPLICIT_TERMS = [
|
|
377
432
|
"apps generate", "app builder", "make an app", "build an app", "create an app",
|
|
@@ -414,7 +469,7 @@ function isTrivialRoutePrompt(promptText) {
|
|
|
414
469
|
return words.length <= 3 && TRIVIAL_ROUTE_PROMPTS.has(stripped);
|
|
415
470
|
}
|
|
416
471
|
function isAppBuilderWorthyRoutePrompt(prompt) {
|
|
417
|
-
const promptText = routeNormalize(prompt);
|
|
472
|
+
const promptText = routeNormalize(routeStripPaths(prompt));
|
|
418
473
|
if (!promptText.trim() || isTrivialRoutePrompt(promptText)) return false;
|
|
419
474
|
const explicit = routeMatchedTerms(promptText, APP_BUILDER_EXPLICIT_TERMS);
|
|
420
475
|
if (explicit.length) return true;
|
|
@@ -436,8 +491,10 @@ function routeHint(promptText, agent, lang) {
|
|
|
436
491
|
if (!terms.length) return { score: 0, terms: [], reason: "" };
|
|
437
492
|
return { score: 12 + terms.length * 3, terms, reason: lang === "ko" ? hint.reasonKo : hint.reasonEn };
|
|
438
493
|
}
|
|
439
|
-
function scoreRouteAgent(prompt, promptTerms, agent, lang) {
|
|
440
|
-
|
|
494
|
+
function scoreRouteAgent(prompt, promptTerms, agent, lang, pre) {
|
|
495
|
+
// 대칭 스트리핑 필수: promptText는 이름(+20)·힌트(+12↑) strong 채널의 입력이라, 여기서
|
|
496
|
+
// 경로를 안 벗기면 "/Users/x/project-plan/…"의 디렉터리명이 strong 게이트를 그대로 뚫는다.
|
|
497
|
+
const promptText = routeNormalize(routeStripPaths(prompt));
|
|
441
498
|
if (agent.slug === "agentlas-app-builder" && !isAppBuilderWorthyRoutePrompt(promptText)) {
|
|
442
499
|
return {
|
|
443
500
|
agent,
|
|
@@ -446,27 +503,41 @@ function scoreRouteAgent(prompt, promptTerms, agent, lang) {
|
|
|
446
503
|
? "전용 App을 만들 만큼 반복·상태·편집·자동화가 뚜렷하지 않아 App Builder 라우트를 보류했습니다"
|
|
447
504
|
: "the request does not clearly need a dedicated App with durable workflow, state, editing, or automation",
|
|
448
505
|
terms: [],
|
|
506
|
+
strong: false,
|
|
449
507
|
};
|
|
450
508
|
}
|
|
451
|
-
|
|
509
|
+
// 헤이스택은 설치/임포트 시에만 변하므로 autoRouteAgent가 미리 계산해 넘긴다(중복 계산 제거).
|
|
510
|
+
const identityHay = (pre && pre.identityHay) || routeIdentityHaystack(agent);
|
|
511
|
+
const haystack = (pre && pre.haystack) || routeHaystack(agent);
|
|
452
512
|
let score = 0;
|
|
513
|
+
let strong = false; // 이름 언급/정체성 적중/큐레이션 힌트 — 데스크탑처럼 "이름/힌트급 증거"가 있어야 위임한다
|
|
453
514
|
const terms = [];
|
|
515
|
+
const seenNames = new Set();
|
|
454
516
|
for (const name of [agent.slug, agent.name, agent.name_en].filter(Boolean)) {
|
|
455
517
|
const n = routeNormalize(name);
|
|
456
518
|
// 4자 미만 일반 단어("team","agent" 등)가 프롬프트에 우연히 들어가 +20을 독식하지 않도록 가드.
|
|
457
|
-
|
|
519
|
+
// name === name_en 인 임포트 에이전트(appbridge 등)가 +20을 두 번 받지 않도록 정규화 기준 dedupe.
|
|
520
|
+
if (!n || n.length < 4 || seenNames.has(n)) continue;
|
|
521
|
+
seenNames.add(n);
|
|
522
|
+
if (promptText.includes(n)) {
|
|
458
523
|
score += 20;
|
|
459
524
|
terms.push(name);
|
|
525
|
+
strong = true;
|
|
460
526
|
}
|
|
461
527
|
}
|
|
462
528
|
for (const term of promptTerms) {
|
|
463
|
-
if (
|
|
529
|
+
if (identityHay.includes(term)) {
|
|
530
|
+
score += 6; // 이름/태그라인 적중 = 그 에이전트의 정체성 자체를 부른 것
|
|
531
|
+
terms.push(term);
|
|
532
|
+
strong = true;
|
|
533
|
+
} else if (haystack.includes(term)) {
|
|
464
534
|
score += term.length >= 5 ? 3 : 2;
|
|
465
535
|
terms.push(term);
|
|
466
536
|
}
|
|
467
537
|
}
|
|
468
538
|
const hint = routeHint(promptText, agent, lang);
|
|
469
539
|
score += hint.score;
|
|
540
|
+
if (hint.score) strong = true;
|
|
470
541
|
terms.push(...hint.terms);
|
|
471
542
|
const unique = [...new Set(terms)].slice(0, 6);
|
|
472
543
|
const reason = hint.reason || (lang === "ko"
|
|
@@ -476,7 +547,33 @@ function scoreRouteAgent(prompt, promptTerms, agent, lang) {
|
|
|
476
547
|
: unique.length
|
|
477
548
|
? `request terms ${unique.map((term) => `"${term}"`).join(", ")} best match this agent's role/triggers`
|
|
478
549
|
: "no specialist matched clearly, so the default project coordinator is safest");
|
|
479
|
-
return { agent, score, reason, terms: unique };
|
|
550
|
+
return { agent, score, reason, terms: unique, strong };
|
|
551
|
+
}
|
|
552
|
+
// 라우팅 확신 임계값 — 데스크탑 auto-router의 MIN_SPECIALIST_SCORE(10)와 동일 기준.
|
|
553
|
+
// 위임에는 점수뿐 아니라 strong 신호(이름 포함 +20 / 정체성 적중 +6 / 큐레이션 힌트 +12↑)가
|
|
554
|
+
// 반드시 있어야 한다. system_prompt 본문의 약한 단어 적중(+2~3)이 몇 개 쌓여도, strong 신호가
|
|
555
|
+
// 없으면 절대 위임하지 않는다. 미달이면 "직답"(에이전트·능력 라우팅 없음) — 일반 질문이
|
|
556
|
+
// Pitch Deck Architect 같은 무관 페르소나 + gemini 이미지 런타임으로 끌려가던 사고의 근본 수리.
|
|
557
|
+
const MIN_ROUTE_SCORE = 10;
|
|
558
|
+
function directRouteChoice(lang) {
|
|
559
|
+
const resolvedLang = lang || prefsLang();
|
|
560
|
+
return {
|
|
561
|
+
direct: true,
|
|
562
|
+
agent: null,
|
|
563
|
+
score: 0,
|
|
564
|
+
terms: [],
|
|
565
|
+
strong: false,
|
|
566
|
+
reason: resolvedLang === "ko"
|
|
567
|
+
? "특정 전문 에이전트가 필요 없는 일반 요청입니다"
|
|
568
|
+
: "this is a general request that needs no specialist agent",
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
// 직답 모드 시스템 프롬프트 — 페르소나·라우팅 오염 없이 현재 런타임 그대로 답한다.
|
|
572
|
+
function directSystemPrompt(lang) {
|
|
573
|
+
const resolvedLang = lang || prefsLang();
|
|
574
|
+
return resolvedLang === "ko"
|
|
575
|
+
? "당신은 Agentlas 터미널의 기본 어시스턴트입니다. 특별한 페르소나 없이 사용자의 요청에 정확하고 간결하게 바로 답하세요. 에이전트 라우팅이나 이미지 생성 능력을 스스로 언급하지 마세요."
|
|
576
|
+
: "You are the Agentlas terminal's default assistant. Answer the user's request directly and concisely, with no special persona. Do not bring up agent routing or image-generation capabilities on your own.";
|
|
480
577
|
}
|
|
481
578
|
function autoRouteAgent(db, prompt, lang) {
|
|
482
579
|
const resolvedLang = lang || prefsLang();
|
|
@@ -487,6 +584,7 @@ function autoRouteAgent(db, prompt, lang) {
|
|
|
487
584
|
return {
|
|
488
585
|
agent: meta,
|
|
489
586
|
score: 1000,
|
|
587
|
+
strong: true,
|
|
490
588
|
reason:
|
|
491
589
|
resolvedLang === "ko"
|
|
492
590
|
? "새 에이전트/팀/회사를 만드는 요청이라 메타에이전트(빌더)로 라우팅했습니다"
|
|
@@ -496,28 +594,46 @@ function autoRouteAgent(db, prompt, lang) {
|
|
|
496
594
|
}
|
|
497
595
|
}
|
|
498
596
|
const agents = listRoutableAgents(db).filter((agent) => !NON_GENERIC_ROUTE_SLUGS.has(agent.slug));
|
|
499
|
-
if (!agents.length) return
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
597
|
+
if (!agents.length) return directRouteChoice(resolvedLang);
|
|
598
|
+
let terms = routeTokenize(prompt);
|
|
599
|
+
// 헤이스택은 한 번만 계산해 IDF와 스코어링 양쪽에서 재사용한다.
|
|
600
|
+
const hays = agents.map((agent) => ({ identityHay: routeIdentityHaystack(agent), haystack: routeHaystack(agent) }));
|
|
601
|
+
// IDF 근사 — 설치 에이전트 절반 이상의 haystack에 나오는 단어("ai","도구" 등)는 판별력이 없어 제외.
|
|
602
|
+
if (agents.length >= 3) {
|
|
603
|
+
terms = terms.filter((term) => hays.filter((h) => h.haystack.includes(term)).length * 2 <= agents.length);
|
|
604
|
+
}
|
|
605
|
+
const ranked = agents
|
|
606
|
+
.map((agent, i) => scoreRouteAgent(prompt, terms, agent, resolvedLang, hays[i]))
|
|
607
|
+
.sort((a, b) => b.score - a.score);
|
|
608
|
+
// 1위가 아니라 "임계값+strong을 모두 만족하는 최고 순위"를 뽑는다 — 장황한 프롬프트의
|
|
609
|
+
// 약한 단어 적중이 점수 1위를 먹어도, 자격 있는 전문 에이전트가 직답으로 밀려나지 않는다.
|
|
610
|
+
const pick = ranked.find((r) => r.score >= MIN_ROUTE_SCORE && r.strong);
|
|
611
|
+
if (pick) return pick;
|
|
612
|
+
return directRouteChoice(resolvedLang);
|
|
512
613
|
}
|
|
513
614
|
function autoRouteNote(choice, lang) {
|
|
514
|
-
const
|
|
515
|
-
|
|
615
|
+
const resolvedLang = lang || prefsLang();
|
|
616
|
+
if (choice.direct) {
|
|
617
|
+
return resolvedLang === "ko"
|
|
618
|
+
? `사용 에이전트: 없음 — 바로 답합니다. 이유: ${choice.reason}.`
|
|
619
|
+
: `Selected agent: none — answering directly. Reason: ${choice.reason}.`;
|
|
620
|
+
}
|
|
621
|
+
const name = resolvedLang === "ko" ? choice.agent.name : choice.agent.name_en || choice.agent.name;
|
|
622
|
+
return resolvedLang === "ko"
|
|
516
623
|
? `사용 에이전트: ${name}. 이유: ${choice.reason}.`
|
|
517
624
|
: `Selected agent: ${name}. Reason: ${choice.reason}.`;
|
|
518
625
|
}
|
|
519
626
|
function autoRoutePreamble(choice, lang) {
|
|
520
627
|
const resolvedLang = lang || prefsLang();
|
|
628
|
+
if (choice.direct) {
|
|
629
|
+
return [
|
|
630
|
+
"## Agentlas direct answer",
|
|
631
|
+
"",
|
|
632
|
+
resolvedLang === "ko"
|
|
633
|
+
? "이 요청은 전문 에이전트 라우팅 없이 처리합니다. 라우팅이나 에이전트를 언급하지 말고 사용자 요청에 바로 답하세요."
|
|
634
|
+
: "This request is handled without specialist routing. Answer the user directly, without mentioning routing or agents.",
|
|
635
|
+
].join("\n");
|
|
636
|
+
}
|
|
521
637
|
const appBuilderNeedsConsent = choice.agent && choice.agent.slug === "agentlas-app-builder";
|
|
522
638
|
const instruction = appBuilderNeedsConsent
|
|
523
639
|
? resolvedLang === "ko"
|
|
@@ -708,9 +824,14 @@ function importLocalFolderCli(db, absPath) {
|
|
|
708
824
|
).run(id, slug, name, name, tagline, tagline, systemPrompt, envReqsJson, now, tone);
|
|
709
825
|
}
|
|
710
826
|
}
|
|
827
|
+
// detectKind 결과를 DB에도 기록 — needsImage의 팀 body-veto 등 능력 판정이
|
|
828
|
+
// 데스크탑이 써준 entity_kind에 무임승차하지 않고 터미널 단독 임포트에서도 성립한다.
|
|
829
|
+
if (columnExists(db, "installed_agents", "entity_kind")) {
|
|
830
|
+
db.prepare("UPDATE installed_agents SET entity_kind=? WHERE id=?").run(kind, id);
|
|
831
|
+
}
|
|
711
832
|
// 라우트 저장
|
|
712
833
|
routes[id] = { agentId: id, path: dir, runtime, labels, kind, importedAt: now };
|
|
713
|
-
|
|
834
|
+
writeJsonPrivateAtomicCli(path.join(userDataDir(), "agent-routes.json"), routes);
|
|
714
835
|
|
|
715
836
|
// 팀이면 회사(firm)로도 등록 → 앱 FIRMS 목록 + `agentlas firm <slug>` 사용 가능. slug 기준 멱등.
|
|
716
837
|
let firm = null;
|
|
@@ -785,6 +906,7 @@ const CLOUD_AGENT_FILES = new Set(["AGENT.md", "AGENTS.md", "CLAUDE.md", "GEMINI
|
|
|
785
906
|
const CLOUD_SKIP_DIRS = new Set([".git", ".next", ".studio-runtime", ".turbo", "build", "coverage", "dist", "node_modules", "out", "release"]);
|
|
786
907
|
const CLOUD_BLOCKED_FILE_RE = [/^\.env(?:\..*)?$/i, /^id_rsa(?:\.pub)?$/i, /^credentials(?:\..*)?$/i, /^secrets?(?:\..*)?$/i, /^cloud-asset-state\.v1\.json$/i, /(?:^|[._-])service-account(?:[._-]|$)/i, /\.(?:key|pem|p12|pfx|mobileprovision)$/i];
|
|
787
908
|
const CLOUD_ROUTING_CARD_PATH = ".agentlas/routing-card.json";
|
|
909
|
+
const CLOUD_LOCAL_EXPERIENCE_LINEAGE_PATH = ".agentlas/experience-relations.jsonl";
|
|
788
910
|
const CLOUD_ROUTING_CARD_CAPABILITY_RE = /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/;
|
|
789
911
|
const CLOUD_ROUTING_CARD_STATUSES = new Set(["draft", "searchable", "candidate", "routing_ready", "trusted"]);
|
|
790
912
|
const CLOUD_SECRET_RE = [
|
|
@@ -1574,6 +1696,12 @@ function scanCloudFolderCli(rootPath) {
|
|
|
1574
1696
|
if (entry.name.startsWith("._")) continue;
|
|
1575
1697
|
const abs = path.join(dir, entry.name);
|
|
1576
1698
|
const rel = path.relative(rootPath, abs).split(path.sep).join("/");
|
|
1699
|
+
if (cloudIsLocalExperienceLineagePath(rel)) {
|
|
1700
|
+
let bytes = 0;
|
|
1701
|
+
try { bytes = Number(fs.lstatSync(abs).size) || 0; } catch { /* excluded local state */ }
|
|
1702
|
+
files.push({ path: rel, bytes, sha256: "", kind: "text", included: false, reason: "experience-lineage-separate-asset" });
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1577
1705
|
if (cloudPortablePathKey(rel) === cloudPortablePathKey(CLOUD_RESTORE_MARKER_PATH)) {
|
|
1578
1706
|
// Local restore/CAS metadata is runtime state, never portable asset
|
|
1579
1707
|
// data, but it must be captured with the same no-follow stability gate.
|
|
@@ -2318,6 +2446,17 @@ function persistCloudListingCli(db, listing) {
|
|
|
2318
2446
|
throw error;
|
|
2319
2447
|
}
|
|
2320
2448
|
const localPath = restore?.path || null;
|
|
2449
|
+
// entity_kind 기록 — needsImage의 팀 body-veto가 로컬 폴더 임포트(detectKind)뿐 아니라
|
|
2450
|
+
// 클라우드/Hub 소스 설치 팀에도 걸리게 한다. 안 하면 팀 CEO 두뇌의 부서 키워드
|
|
2451
|
+
// ("Design HQ" 등)로 needsImage가 참이 되어 세션 런타임이 통째로 gemini로 하이재킹된다.
|
|
2452
|
+
// Hub가 준 entityKind를 우선하고, 없으면 materialize된 팩 폴더 구조로 판정한다.
|
|
2453
|
+
if (columnExists(db, "installed_agents", "entity_kind")) {
|
|
2454
|
+
let kind = String(listing.entityKind || "").toLowerCase();
|
|
2455
|
+
if (kind !== "team" && kind !== "agent") {
|
|
2456
|
+
kind = localPath && fs.existsSync(localPath) ? detectKind(localPath) : "agent";
|
|
2457
|
+
}
|
|
2458
|
+
db.prepare("UPDATE installed_agents SET entity_kind=? WHERE id=?").run(kind, id);
|
|
2459
|
+
}
|
|
2321
2460
|
return existing
|
|
2322
2461
|
? { ...existing, slug, name: dbExpected.name, ...(localPath ? { localPath } : {}) }
|
|
2323
2462
|
: { id, slug, name: dbExpected.name, ...(localPath ? { localPath } : {}) };
|
|
@@ -2663,6 +2802,13 @@ function printCloudPackageResult(result) {
|
|
|
2663
2802
|
function cloudPackageSnapshot(files) {
|
|
2664
2803
|
return new Map(files.map((file) => [file.path, file]));
|
|
2665
2804
|
}
|
|
2805
|
+
function cloudIsLocalExperienceLineagePath(value) {
|
|
2806
|
+
const normalized = cloudPortablePathKey(String(value || "").replace(/\\/g, "/"));
|
|
2807
|
+
const canonical = cloudPortablePathKey(CLOUD_LOCAL_EXPERIENCE_LINEAGE_PATH);
|
|
2808
|
+
return normalized === canonical
|
|
2809
|
+
|| normalized.startsWith(`${canonical}.`)
|
|
2810
|
+
|| normalized.startsWith(cloudPortablePathKey(".agentlas/.experience-relations.jsonl."));
|
|
2811
|
+
}
|
|
2666
2812
|
function cloudReadPublicCareerCard(snapshot, findings) {
|
|
2667
2813
|
const relativePath = ".agentlas/public-career-card.json";
|
|
2668
2814
|
const file = snapshot.get(relativePath);
|
|
@@ -2855,7 +3001,7 @@ function cloudHashPackage(files, version = CLOUD_PACKAGE_HASH_V1) {
|
|
|
2855
3001
|
// 서버 package-contract.ts와 바이트 동일해야 한다: 경로 코드포인트 순 정렬.
|
|
2856
3002
|
// 정렬 없이 스캔 순서로 해시하면 대소문자 혼합 경로 패키지(AGENTS.md + agents/…)가
|
|
2857
3003
|
// 전부 package_hash_mismatch로 거절된다(2026-07-02 근본 수정).
|
|
2858
|
-
for (const file of [...files].sort(cloudCodePointPathOrder)) {
|
|
3004
|
+
for (const file of [...files].filter((file) => !cloudIsLocalExperienceLineagePath(file.path)).sort(cloudCodePointPathOrder)) {
|
|
2859
3005
|
h.update(file.path);
|
|
2860
3006
|
h.update("\0");
|
|
2861
3007
|
h.update(file.sha256);
|
|
@@ -6940,6 +7086,21 @@ function readJsonSafeCli(filePath, fallback) {
|
|
|
6940
7086
|
function writeJsonSafeCli(filePath, value) {
|
|
6941
7087
|
fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
6942
7088
|
}
|
|
7089
|
+
// 원자적(temp+rename) + 소유자 전용(0600) JSON 쓰기. 세션 ID/경로 등 민감 상태 파일용:
|
|
7090
|
+
// (1) 크래시 중간 쓰기로 JSON이 깨져 routesMap()이 {}를 돌려주며 임포트 매핑을 통째로 잃던 사고,
|
|
7091
|
+
// (2) 기본 umask(0644)로 cli-sessions.json/agent-routes.json이 world-readable이던 정보 노출을 함께 막는다.
|
|
7092
|
+
function writeJsonPrivateAtomicCli(filePath, value) {
|
|
7093
|
+
const dir = path.dirname(filePath);
|
|
7094
|
+
const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.tmp`);
|
|
7095
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
7096
|
+
try {
|
|
7097
|
+
fs.renameSync(tmp, filePath);
|
|
7098
|
+
} catch (e) {
|
|
7099
|
+
try { fs.unlinkSync(tmp); } catch { /* ignore */ }
|
|
7100
|
+
throw e;
|
|
7101
|
+
}
|
|
7102
|
+
try { fs.chmodSync(filePath, 0o600); } catch { /* 일부 FS는 chmod 미지원 — best-effort */ }
|
|
7103
|
+
}
|
|
6943
7104
|
|
|
6944
7105
|
function ontologySourceManifestSkeletonCli(root) {
|
|
6945
7106
|
return {
|
|
@@ -7802,10 +7963,30 @@ async function runApi(backend, model, system, prompt, options) {
|
|
|
7802
7963
|
async function executeOnce(db, system, prompt, override, ctx) {
|
|
7803
7964
|
ctx = ctx || { projectPath: null, agentId: null };
|
|
7804
7965
|
if (!ctx.cwdAtRequest) ctx.cwdAtRequest = projectCwd();
|
|
7966
|
+
let runtimeSystem = system;
|
|
7967
|
+
let localExperienceContext = null;
|
|
7968
|
+
if (ctx.runtimeExperience && ctx.runtimeExperience.disabled !== true) {
|
|
7969
|
+
const runtimeExperience = ctx.runtimeExperience;
|
|
7970
|
+
const augmented = terminalExperienceExchange.augmentRuntimeSystemWithLocalExperience(system, {
|
|
7971
|
+
userDataDir: userDataDir(),
|
|
7972
|
+
cwd: ctx.projectPath || ctx.cwdAtRequest,
|
|
7973
|
+
baseAgentReleaseId: runtimeExperience.baseAgentReleaseId,
|
|
7974
|
+
agentDefinitionId: runtimeExperience.agentDefinitionId,
|
|
7975
|
+
taskSignatures: runtimeExperience.taskSignatures || [],
|
|
7976
|
+
environmentTags: Array.isArray(runtimeExperience.environmentTags) && runtimeExperience.environmentTags.length
|
|
7977
|
+
? runtimeExperience.environmentTags
|
|
7978
|
+
: terminalExperienceExchange.defaultEnvironmentTags(),
|
|
7979
|
+
});
|
|
7980
|
+
runtimeSystem = augmented.systemPrompt;
|
|
7981
|
+
localExperienceContext = augmented.experienceContext;
|
|
7982
|
+
if (localExperienceContext.itemIds.length) {
|
|
7983
|
+
process.stderr.write(`▸ local Experience advisory · ${localExperienceContext.itemIds.length} item(s) · ~${localExperienceContext.estimatedTokens} tokens · no server rental receipt\n`);
|
|
7984
|
+
}
|
|
7985
|
+
}
|
|
7805
7986
|
const rt = resolveRuntime(db, override);
|
|
7806
7987
|
if (rt.mode === "cli") {
|
|
7807
7988
|
// 네이티브 CLI는 자체 세션을 가지므로 emitter는 넣지 않고(노이즈 방지) 메모리 컨텍스트만 주입.
|
|
7808
|
-
const sys = augmentSystem(db,
|
|
7989
|
+
const sys = augmentSystem(db, runtimeSystem, ctx, false);
|
|
7809
7990
|
const cwd = ctx.projectPath || projectCwd();
|
|
7810
7991
|
const permission = ctx.permission || "write";
|
|
7811
7992
|
const env = await buildChildEnvCli(db, { ...ctx, cwd });
|
|
@@ -7839,7 +8020,7 @@ async function executeOnce(db, system, prompt, override, ctx) {
|
|
|
7839
8020
|
return res.error ? 1 : 0;
|
|
7840
8021
|
}
|
|
7841
8022
|
// API 경로 — emitter 동봉 → 답변에서 메모리 이벤트를 파싱·큐레이션하고 블록은 제거.
|
|
7842
|
-
const sys = augmentSystem(db,
|
|
8023
|
+
const sys = augmentSystem(db, runtimeSystem, ctx, true);
|
|
7843
8024
|
const env = await buildChildEnvCli(db, { ...ctx, cwd: ctx.cwd || projectCwd() });
|
|
7844
8025
|
Object.assign(process.env, env);
|
|
7845
8026
|
process.stderr.write(`▸ ${rt.backend}${rt.model ? " · " + rt.model : ""}\n`);
|
|
@@ -8009,13 +8190,28 @@ const PROTECTED_CHILD_ENV_KEYS_CLI = new Set([
|
|
|
8009
8190
|
"AGENTLAS_NATIVE_IDLE_TIMEOUT_MS", "AGENTLAS_NATIVE_TOTAL_TIMEOUT_MS", "AGENTLAS_NATIVE_KILL_GRACE_MS",
|
|
8010
8191
|
"AGENTLAS_CAPTURE_MAX_OUTPUT_BYTES",
|
|
8011
8192
|
]);
|
|
8012
|
-
|
|
8013
|
-
|
|
8193
|
+
// 네트워크 무결성 키 — TLS 검증·프록시·CA·엔드포인트·세션. 프로젝트/에이전트 dotenv(신뢰 불가:
|
|
8194
|
+
// 클론한 레포에 딸려올 수 있음)로 주입되면 MITM/SSRF/세션 하이재킹이 된다. 단, 사용자 본인의
|
|
8195
|
+
// 전역 credentials.env와 호스트 셸 env는 신뢰하므로 그대로 허용한다. 사고 방지: 원샷 API 경로는
|
|
8196
|
+
// buildChildEnvCli 결과를 process.env에 병합(Object.assign)하므로, 프로젝트 .env가 부모 프로세스의
|
|
8197
|
+
// 클라우드 호출(세션 쿠키 동반)까지 오염시킬 수 있었다.
|
|
8198
|
+
const UNTRUSTED_PROTECTED_ENV_KEYS_CLI = new Set([
|
|
8199
|
+
"NODE_TLS_REJECT_UNAUTHORIZED", "NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "SSL_CERT_DIR",
|
|
8200
|
+
"OPENSSL_CONF", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE",
|
|
8201
|
+
"HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY", "GRPC_PROXY", "NPM_CONFIG_PROXY",
|
|
8202
|
+
"AGENTLAS_SESSION", "AGENTLAS_MCP_BASE_URL", "AGENTLAS_WEB_BASE_URL", "AGENTLAS_API_BASE_URL",
|
|
8203
|
+
"AGENTLAS_HUB_BASE_URL", "AGENTLAS_CLOUD_BASE_URL", "OLLAMA_HOST",
|
|
8204
|
+
]);
|
|
8205
|
+
function isProtectedChildEnvKeyCli(key, trusted) {
|
|
8206
|
+
const k = String(key || "").trim().toUpperCase();
|
|
8207
|
+
if (PROTECTED_CHILD_ENV_KEYS_CLI.has(k)) return true; // 호스트 신원/플러그인 루트 — 모든 출처 차단
|
|
8208
|
+
if (!trusted && UNTRUSTED_PROTECTED_ENV_KEYS_CLI.has(k)) return true; // 네트워크 무결성 — 비신뢰 출처만 차단
|
|
8209
|
+
return false;
|
|
8014
8210
|
}
|
|
8015
|
-
function mergeChildEnvValuesCli(target, values, overwrite) {
|
|
8211
|
+
function mergeChildEnvValuesCli(target, values, overwrite, trusted) {
|
|
8016
8212
|
const injected = [];
|
|
8017
8213
|
for (const [key, value] of Object.entries(values || {})) {
|
|
8018
|
-
if (!value || isProtectedChildEnvKeyCli(key)) continue;
|
|
8214
|
+
if (!value || isProtectedChildEnvKeyCli(key, trusted)) continue;
|
|
8019
8215
|
if (!overwrite && target[key]) continue;
|
|
8020
8216
|
target[key] = value;
|
|
8021
8217
|
injected.push(key);
|
|
@@ -8024,19 +8220,20 @@ function mergeChildEnvValuesCli(target, values, overwrite) {
|
|
|
8024
8220
|
}
|
|
8025
8221
|
async function buildChildEnvCli(db, ctx) {
|
|
8026
8222
|
const env = { ...process.env };
|
|
8027
|
-
|
|
8028
|
-
|
|
8223
|
+
// trusted=true: 사용자 본인의 전역 자격/볼트. trusted=false: 프로젝트·에이전트 폴더 dotenv.
|
|
8224
|
+
const apply = (values, overwrite, trusted) => {
|
|
8225
|
+
mergeChildEnvValuesCli(env, values, overwrite, trusted);
|
|
8029
8226
|
};
|
|
8030
8227
|
const globalCredentials = {
|
|
8031
8228
|
...readDotEnvFileCli(path.join(userDataDir(), "credentials.env")),
|
|
8032
8229
|
...readDotEnvFileCli(path.join(os.homedir(), ".agentlas", "credentials.env")),
|
|
8033
8230
|
};
|
|
8034
|
-
apply(globalCredentials, false);
|
|
8035
|
-
if (ctx && ctx.projectPath) apply(projectScopedEnvValuesCli(globalCredentials, ctx.projectPath), true);
|
|
8036
|
-
if (ctx && ctx.cwd) apply(readDotEnvDirCli(ctx.cwd), true);
|
|
8037
|
-
if (ctx && ctx.projectPath) apply(readDotEnvDirCli(ctx.projectPath), true);
|
|
8231
|
+
apply(globalCredentials, false, true);
|
|
8232
|
+
if (ctx && ctx.projectPath) apply(projectScopedEnvValuesCli(globalCredentials, ctx.projectPath), true, true);
|
|
8233
|
+
if (ctx && ctx.cwd) apply(readDotEnvDirCli(ctx.cwd), true, false);
|
|
8234
|
+
if (ctx && ctx.projectPath) apply(readDotEnvDirCli(ctx.projectPath), true, false);
|
|
8038
8235
|
const agentDir = agentEnvDirCli(ctx && ctx.agentId);
|
|
8039
|
-
if (agentDir) apply(readDotEnvDirCli(agentDir), true);
|
|
8236
|
+
if (agentDir) apply(readDotEnvDirCli(agentDir), true, false);
|
|
8040
8237
|
|
|
8041
8238
|
const mm = loadMultimodalCatalog();
|
|
8042
8239
|
const settings = getMultimodalSettingsCli(db);
|
|
@@ -8045,7 +8242,7 @@ async function buildChildEnvCli(db, ctx) {
|
|
|
8045
8242
|
if (req && req.key) keys.add(req.key);
|
|
8046
8243
|
}
|
|
8047
8244
|
const vaultValues = await readVaultEnvValuesCli([...keys].filter((key) => !env[key]), ctx && ctx.projectPath);
|
|
8048
|
-
apply(vaultValues, false);
|
|
8245
|
+
apply(vaultValues, false, true); // 볼트는 사용자 본인 저장소 — 신뢰
|
|
8049
8246
|
env.AGENTLAS_MULTIMODAL_IMAGE_PROVIDER = settings.imageProvider;
|
|
8050
8247
|
env.AGENTLAS_MULTIMODAL_VIDEO_PROVIDER = settings.videoProvider;
|
|
8051
8248
|
env.AGENTLAS_MULTIMODAL_AUDIO_PROVIDER = settings.audioProvider;
|
|
@@ -8117,6 +8314,7 @@ function buildHelpers(db) {
|
|
|
8117
8314
|
autoRouteAgent: (db_, prompt, lang) => autoRouteAgent(db_, prompt, lang),
|
|
8118
8315
|
autoRouteNote: (choice, lang) => autoRouteNote(choice, lang),
|
|
8119
8316
|
autoRoutePreamble: (choice, lang) => autoRoutePreamble(choice, lang),
|
|
8317
|
+
directSystemPrompt: (lang) => directSystemPrompt(lang),
|
|
8120
8318
|
cliMemoryContext: (db_, pp) => cliMemoryContext(db_, pp),
|
|
8121
8319
|
importLocal: (db_, p) => importLocalFolderCli(db_, p),
|
|
8122
8320
|
// REPL-safe public Hub install: fail()(process.exit) 대신 Error를 throw 해 REPL이 직접 렌더하게 한다.
|
|
@@ -8164,11 +8362,21 @@ function buildHelpers(db) {
|
|
|
8164
8362
|
try { return JSON.parse(fs.readFileSync(path.join(userDataDir(), "cli-sessions.json"), "utf8")) || []; } catch { return []; }
|
|
8165
8363
|
},
|
|
8166
8364
|
sessionsSave: (list) => {
|
|
8167
|
-
try {
|
|
8365
|
+
try { writeJsonPrivateAtomicCli(path.join(userDataDir(), "cli-sessions.json"), (list || []).slice(0, 30)); } catch { /* ignore */ }
|
|
8168
8366
|
},
|
|
8169
8367
|
// 패리티: REPL의 /storm·/swarm·/build·/route·/research 가 그대로 호출한다.
|
|
8170
8368
|
stormRun: (db_, goal, ctx) => parity().stormRun(db_, goal, ctx),
|
|
8171
8369
|
swarmRun: (db_, goal, ctx) => parity().swarmRun(db_, goal, ctx),
|
|
8370
|
+
terminalBuild: (db_, args, ctx = {}) => terminalAssets.cmdBuild({
|
|
8371
|
+
db: db_,
|
|
8372
|
+
args: Array.isArray(args) ? args : terminalAssets.tokenizeBuildCommandLine(String(args || "")),
|
|
8373
|
+
userDataDir: userDataDir(),
|
|
8374
|
+
cwd: ctx.cwd || projectCwd(),
|
|
8375
|
+
input: ctx.input || process.stdin,
|
|
8376
|
+
promptOutput: ctx.promptOutput || process.stderr,
|
|
8377
|
+
out: ctx.out || out,
|
|
8378
|
+
invokeBuild: (request) => parity().cmdHep(db_, request ? ["hep-build", request] : ["hep-build"]),
|
|
8379
|
+
}),
|
|
8172
8380
|
hepRun: (args, opts) => parity().runHephaestusInteractive(args, opts),
|
|
8173
8381
|
cloudSearch: (db_, args) => parity().cloudSearch(db_, args),
|
|
8174
8382
|
careerGraphCommand: (text, ctx) => runCareerGraphNaturalCli(text, {
|
|
@@ -8517,25 +8725,68 @@ function cmdCd(db, query) {
|
|
|
8517
8725
|
process.stdout.write(folder + "\n");
|
|
8518
8726
|
}
|
|
8519
8727
|
|
|
8520
|
-
|
|
8728
|
+
function parseRunExperienceArgs(args) {
|
|
8729
|
+
const prompt = [];
|
|
8730
|
+
const experience = { taskSignatures: [], environmentTags: [] };
|
|
8731
|
+
let passthrough = false;
|
|
8732
|
+
const addList = (target, value) => {
|
|
8733
|
+
for (const item of String(value || "").split(",").map((entry) => entry.trim()).filter(Boolean)) {
|
|
8734
|
+
if (!target.includes(item)) target.push(item);
|
|
8735
|
+
}
|
|
8736
|
+
};
|
|
8737
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
8738
|
+
const token = String(args[index]);
|
|
8739
|
+
if (passthrough) { prompt.push(token); continue; }
|
|
8740
|
+
if (token === "--") { passthrough = true; continue; }
|
|
8741
|
+
const take = () => index + 1 < args.length ? String(args[++index]) : "";
|
|
8742
|
+
if (token === "--experience-base-release") experience.baseAgentReleaseId = take();
|
|
8743
|
+
else if (token.startsWith("--experience-base-release=")) experience.baseAgentReleaseId = token.slice(26);
|
|
8744
|
+
else if (token === "--experience-agent-definition") experience.agentDefinitionId = take();
|
|
8745
|
+
else if (token.startsWith("--experience-agent-definition=")) experience.agentDefinitionId = token.slice(30);
|
|
8746
|
+
else if (token === "--experience-task-signature") addList(experience.taskSignatures, take());
|
|
8747
|
+
else if (token.startsWith("--experience-task-signature=")) addList(experience.taskSignatures, token.slice(28));
|
|
8748
|
+
else if (token === "--experience-environment") addList(experience.environmentTags, take());
|
|
8749
|
+
else if (token.startsWith("--experience-environment=")) addList(experience.environmentTags, token.slice(25));
|
|
8750
|
+
else if (token === "--no-experience") experience.disabled = true;
|
|
8751
|
+
else prompt.push(token);
|
|
8752
|
+
}
|
|
8753
|
+
return { prompt: prompt.join(" "), experience };
|
|
8754
|
+
}
|
|
8755
|
+
|
|
8756
|
+
async function cmdRun(db, query, prompt, runtimeOverride, runtimeExperience = null) {
|
|
8521
8757
|
const agent = resolveAgent(db, query);
|
|
8522
8758
|
if (!agent) {
|
|
8523
8759
|
const routedPrompt = [query, prompt].filter(Boolean).join(" ").trim() || (await readStdin());
|
|
8524
8760
|
if (!routedPrompt || !routedPrompt.trim()) fail("프롬프트가 비어 있습니다. agentlas run <agent> \"...\" 또는 agentlas run \"...\" 형식으로 입력하세요.");
|
|
8525
|
-
return cmdAutoRun(db, routedPrompt.trim(), runtimeOverride);
|
|
8761
|
+
return cmdAutoRun(db, routedPrompt.trim(), runtimeOverride, runtimeExperience);
|
|
8526
8762
|
}
|
|
8527
8763
|
let userPrompt = prompt;
|
|
8528
8764
|
if (!userPrompt) userPrompt = await readStdin();
|
|
8529
8765
|
if (!userPrompt || !userPrompt.trim()) fail("프롬프트가 비어 있습니다. agentlas run <agent> \"...\" 또는 stdin으로 전달하세요.");
|
|
8530
8766
|
process.stderr.write(`▸ ${agent.name}\n`);
|
|
8531
|
-
const code = await executeOnce(db, agentSystemPromptCli(agent), userPrompt.trim(), runtimeOverride, {
|
|
8767
|
+
const code = await executeOnce(db, agentSystemPromptCli(agent), userPrompt.trim(), runtimeOverride, {
|
|
8768
|
+
projectPath: activeProjectPath(db), agentId: agent.id, permission: PERMISSION, runtimeExperience,
|
|
8769
|
+
});
|
|
8532
8770
|
process.exit(code);
|
|
8533
8771
|
}
|
|
8534
8772
|
|
|
8535
|
-
async function cmdAutoRun(db, prompt, runtimeOverride) {
|
|
8773
|
+
async function cmdAutoRun(db, prompt, runtimeOverride, runtimeExperience = null) {
|
|
8536
8774
|
const lang = prefsLang();
|
|
8537
8775
|
const choice = autoRouteAgent(db, prompt, lang);
|
|
8538
8776
|
if (!choice) fail("자동 라우팅할 에이전트가 없습니다. agentlas list로 설치 상태를 확인하세요.");
|
|
8777
|
+
if (choice.direct) {
|
|
8778
|
+
// 전문 에이전트 확신 없음 → 페르소나/능력 라우팅 없이 현재 런타임으로 직답.
|
|
8779
|
+
process.stderr.write(`▸ direct (no agent)\n`);
|
|
8780
|
+
process.stderr.write(` ${autoRouteNote(choice, lang)}\n`);
|
|
8781
|
+
const sys = `${autoRoutePreamble(choice, lang)}\n\n${directSystemPrompt(lang)}`;
|
|
8782
|
+
const code = await executeOnce(db, sys, prompt.trim(), runtimeOverride, {
|
|
8783
|
+
projectPath: activeProjectPath(db),
|
|
8784
|
+
agentId: null,
|
|
8785
|
+
permission: PERMISSION,
|
|
8786
|
+
runtimeExperience,
|
|
8787
|
+
});
|
|
8788
|
+
process.exit(code);
|
|
8789
|
+
}
|
|
8539
8790
|
process.stderr.write(`▸ ${choice.agent.name} (auto)\n`);
|
|
8540
8791
|
process.stderr.write(` ${autoRouteNote(choice, lang)}\n`);
|
|
8541
8792
|
const sys = `${autoRoutePreamble(choice, lang)}\n\n${agentSystemPromptCli(choice.agent)}`;
|
|
@@ -8543,6 +8794,7 @@ async function cmdAutoRun(db, prompt, runtimeOverride) {
|
|
|
8543
8794
|
projectPath: activeProjectPath(db),
|
|
8544
8795
|
agentId: choice.agent.id,
|
|
8545
8796
|
permission: PERMISSION,
|
|
8797
|
+
runtimeExperience,
|
|
8546
8798
|
});
|
|
8547
8799
|
process.exit(code);
|
|
8548
8800
|
}
|
|
@@ -8603,7 +8855,7 @@ async function cmdFirm(db, query, prompt, runtimeOverride) {
|
|
|
8603
8855
|
slug: firm.slug,
|
|
8604
8856
|
label: firm.name + " CEO",
|
|
8605
8857
|
system: sys,
|
|
8606
|
-
capAgent: { name: firm.name, name_en: firm.name_en || firm.name, tagline: firm.tagline, system_prompt: sys },
|
|
8858
|
+
capAgent: { name: firm.name, name_en: firm.name_en || firm.name, tagline: firm.tagline, tagline_en: firm.tagline_en, entity_kind: "team", system_prompt: sys },
|
|
8607
8859
|
};
|
|
8608
8860
|
return launchTui(db, subject, runtimeOverride);
|
|
8609
8861
|
}
|
|
@@ -9936,6 +10188,9 @@ function cmdHelp() {
|
|
|
9936
10188
|
" connect [<sub>] wire Telegram / platforms to an agent team (hep-connect)",
|
|
9937
10189
|
" import <path> import a local agent/team folder",
|
|
9938
10190
|
" list installed agents/companies + active runtime",
|
|
10191
|
+
" experience <sub> portable Experience: validate|save|publish|status|export|withdraw",
|
|
10192
|
+
" legacy local intents: list|inspect|unpublish|legacy-publish",
|
|
10193
|
+
" variant resolve local variant selection: selected|fallback|base-only|error",
|
|
9939
10194
|
"",
|
|
9940
10195
|
hdr("EXECUTE"),
|
|
9941
10196
|
" storm <goal> force-robust pipeline: route → verify → execute (Stormbreaker) [--research]",
|
|
@@ -10038,8 +10293,10 @@ async function main() {
|
|
|
10038
10293
|
return cmdImport(db, rest[1]);
|
|
10039
10294
|
case "cd":
|
|
10040
10295
|
return cmdCd(db, rest[1]);
|
|
10041
|
-
case "run":
|
|
10042
|
-
|
|
10296
|
+
case "run": {
|
|
10297
|
+
const runInput = parseRunExperienceArgs(rest.slice(2));
|
|
10298
|
+
return cmdRun(db, rest[1], runInput.prompt, runtimeOverride, runInput.experience);
|
|
10299
|
+
}
|
|
10043
10300
|
case "chat":
|
|
10044
10301
|
case "open":
|
|
10045
10302
|
return cmdOpen(db, rest[1], runtimeOverride);
|
|
@@ -10074,8 +10331,37 @@ async function main() {
|
|
|
10074
10331
|
return parity().cmdHep(db, rest.slice(1));
|
|
10075
10332
|
// ── Agentlas OS 정식 표면 (hep-*) 1급 노출 ──
|
|
10076
10333
|
case "build":
|
|
10077
|
-
//
|
|
10078
|
-
|
|
10334
|
+
// Terminal-owned preflight: trusted system-global MCP metadata first, one consent,
|
|
10335
|
+
// then pass only approved catalog IDs/value-free shortages to the existing builder.
|
|
10336
|
+
return terminalAssets.cmdBuild({
|
|
10337
|
+
db,
|
|
10338
|
+
args: rest.slice(1),
|
|
10339
|
+
userDataDir: userDataDir(),
|
|
10340
|
+
cwd: projectCwd(),
|
|
10341
|
+
input: process.stdin,
|
|
10342
|
+
promptOutput: process.stderr,
|
|
10343
|
+
out,
|
|
10344
|
+
invokeBuild: (request) => parity().cmdHep(db, request ? ["hep-build", request] : ["hep-build"]),
|
|
10345
|
+
});
|
|
10346
|
+
case "experience":
|
|
10347
|
+
return terminalExperienceExchange.cmdExperienceExchange({
|
|
10348
|
+
args: rest.slice(1),
|
|
10349
|
+
userDataDir: userDataDir(),
|
|
10350
|
+
cwd: projectCwd(),
|
|
10351
|
+
out,
|
|
10352
|
+
env: process.env,
|
|
10353
|
+
getSessionCookie: cloudSessionCookieCli,
|
|
10354
|
+
fetchHub: (url, init) => fetchHubCli(url, init),
|
|
10355
|
+
legacyCommand: (legacyOptions) => terminalAssets.cmdExperience(legacyOptions),
|
|
10356
|
+
});
|
|
10357
|
+
case "variant":
|
|
10358
|
+
return terminalAssets.cmdVariant({
|
|
10359
|
+
db,
|
|
10360
|
+
args: rest.slice(1),
|
|
10361
|
+
userDataDir: userDataDir(),
|
|
10362
|
+
cwd: projectCwd(),
|
|
10363
|
+
out,
|
|
10364
|
+
});
|
|
10079
10365
|
case "search": // hep-search — 에이전트 디렉터리 발견 (Hub + 로컬)
|
|
10080
10366
|
if (!rest[1]) return fail('usage: agentlas search "<찾는 일>" [--limit 10]');
|
|
10081
10367
|
return parity().cloudSearch(db, rest.slice(1));
|
|
@@ -10160,6 +10446,9 @@ module.exports = {
|
|
|
10160
10446
|
parseDotEnvCli,
|
|
10161
10447
|
isProtectedChildEnvKeyCli,
|
|
10162
10448
|
mergeChildEnvValuesCli,
|
|
10449
|
+
openNodeSqliteDb,
|
|
10450
|
+
ensureMemoryContextColumn,
|
|
10451
|
+
writeJsonPrivateAtomicCli,
|
|
10163
10452
|
resolveCredentialSourcePath,
|
|
10164
10453
|
upsertEnvLine,
|
|
10165
10454
|
fetchHubCli,
|
|
@@ -10193,6 +10482,12 @@ module.exports = {
|
|
|
10193
10482
|
cloudPackageHashVersion,
|
|
10194
10483
|
cloudPortablePathConflict,
|
|
10195
10484
|
cloudPortableExecutableForFile,
|
|
10485
|
+
parseRunExperienceArgs,
|
|
10196
10486
|
DEFAULT_API_MODEL,
|
|
10197
10487
|
ANTHROPIC_COMPAT_API,
|
|
10488
|
+
// 자동 라우팅 회귀 테스트 표면 — 약한 매치 직답/오라우팅 방지 규칙 검증용.
|
|
10489
|
+
autoRouteAgent,
|
|
10490
|
+
autoRouteNote,
|
|
10491
|
+
autoRoutePreamble,
|
|
10492
|
+
directSystemPrompt,
|
|
10198
10493
|
};
|