@buzzni/saycode-cli 0.3.0 → 0.3.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/README.md +2 -1
- package/index.mjs +484 -94
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,7 +124,8 @@ happy agent stop <id> # 사용자가 명시한 경우에만 현재 응
|
|
|
124
124
|
|
|
125
125
|
`steer` 성공의 `accepted:true`는 세션 RPC가 요청을 받았다는 뜻이며 실제 transcript 전달 완료를
|
|
126
126
|
뜻하지 않습니다(`delivered:false`). 끝난 턴의 후속 작업에는 `prompt`를 사용합니다. `steer`와
|
|
127
|
-
`stop`은
|
|
127
|
+
`stop`은 원장에 기록된 최대 2단계 자식만 허용하며, `stop`은 보관·삭제를 하지 않습니다. 이 원장 검사는
|
|
128
|
+
같은 OS 사용자 아래의 오조작을 줄이는 advisory 가드레일이며 보안 경계나 서버 측 권한 증명은 아닙니다.
|
|
128
129
|
같은 worktree에는 동시에 둘 이상의 쓰기 자식을 만들지 말고 격리가 필요하면 Desktop의 관리형
|
|
129
130
|
worktree 병렬 실행을 사용하세요.
|
|
130
131
|
|
package/index.mjs
CHANGED
|
@@ -40,20 +40,20 @@ function _mergeNamespaces(n, m) {
|
|
|
40
40
|
}
|
|
41
41
|
return Object.freeze(Object.defineProperty(n, Symbol.toStringTag, { value: "Module" }));
|
|
42
42
|
}
|
|
43
|
-
const version = "0.3.
|
|
43
|
+
const version = "0.3.2";
|
|
44
44
|
const manifest = {
|
|
45
45
|
version
|
|
46
46
|
};
|
|
47
47
|
const AGENT_CONTROL_VERBS = ["steer", "stop"];
|
|
48
|
-
function parseAgentControlArgs(verb, target, rest) {
|
|
48
|
+
function parseAgentControlArgs(verb, target, rest, hasPositionalTarget) {
|
|
49
49
|
if (verb === "steer") {
|
|
50
50
|
if (!target) return { command: "error", message: "agent steer needs a session id (see `saycode agent ls`)" };
|
|
51
|
-
const text2 = rest.slice(1).join(" ").trim();
|
|
51
|
+
const text2 = rest.slice(hasPositionalTarget ? 1 : 0).join(" ").trim();
|
|
52
52
|
return text2 ? { command: "agent", verb: "steer", sessionId: target, text: text2 } : { command: "error", message: "agent steer needs the text to send" };
|
|
53
53
|
}
|
|
54
54
|
if (verb === "stop") {
|
|
55
55
|
if (!target) return { command: "error", message: "agent stop needs a session id (see `saycode agent ls`)" };
|
|
56
|
-
return rest.length === 1 ? { command: "agent", verb: "stop", sessionId: target } : { command: "error", message: "agent stop takes exactly one session id" };
|
|
56
|
+
return rest.length === (hasPositionalTarget ? 1 : 0) ? { command: "agent", verb: "stop", sessionId: target } : { command: "error", message: "agent stop takes exactly one session id" };
|
|
57
57
|
}
|
|
58
58
|
return null;
|
|
59
59
|
}
|
|
@@ -67,8 +67,9 @@ function parseAgentArgs(flags, positionals) {
|
|
|
67
67
|
return fail(`agent needs one of: ${AGENT_VERBS.join(", ")}`);
|
|
68
68
|
}
|
|
69
69
|
const timeout = flags.timeoutMs !== void 0 ? { timeoutMs: flags.timeoutMs } : {};
|
|
70
|
-
const
|
|
71
|
-
const
|
|
70
|
+
const hasPositionalTarget = flags.sessionId === void 0 && rest[0] !== void 0;
|
|
71
|
+
const target = flags.sessionId ?? rest[0];
|
|
72
|
+
const control = parseAgentControlArgs(verb, target, rest, hasPositionalTarget);
|
|
72
73
|
if (control) return control;
|
|
73
74
|
switch (verb) {
|
|
74
75
|
case "whoami":
|
|
@@ -86,7 +87,7 @@ function parseAgentArgs(flags, positionals) {
|
|
|
86
87
|
}
|
|
87
88
|
case "prompt": {
|
|
88
89
|
if (!target) return fail("agent prompt needs a session id (see `saycode agent ls`)");
|
|
89
|
-
const text2 = rest.slice(1).join(" ").trim();
|
|
90
|
+
const text2 = rest.slice(hasPositionalTarget ? 1 : 0).join(" ").trim();
|
|
90
91
|
if (!text2) return fail("agent prompt needs the text to send");
|
|
91
92
|
return {
|
|
92
93
|
command: "agent",
|
|
@@ -118,7 +119,9 @@ function parseAgentArgs(flags, positionals) {
|
|
|
118
119
|
command: "agent",
|
|
119
120
|
verb: "spawn",
|
|
120
121
|
handoff: { mode: "summary", ...from ? { from } : {} },
|
|
121
|
-
...flags.agent ? { agent: flags.agent } : {}
|
|
122
|
+
...flags.agent ? { agent: flags.agent } : {},
|
|
123
|
+
...flags.model !== void 0 ? { model: flags.model } : {},
|
|
124
|
+
...flags.effort !== void 0 ? { effort: flags.effort } : {}
|
|
122
125
|
};
|
|
123
126
|
}
|
|
124
127
|
const text2 = flags.prompt?.trim();
|
|
@@ -128,6 +131,8 @@ function parseAgentArgs(flags, positionals) {
|
|
|
128
131
|
verb: "spawn",
|
|
129
132
|
text: text2,
|
|
130
133
|
...flags.agent ? { agent: flags.agent } : {},
|
|
134
|
+
...flags.model !== void 0 ? { model: flags.model } : {},
|
|
135
|
+
...flags.effort !== void 0 ? { effort: flags.effort } : {},
|
|
131
136
|
...files.length > 0 ? { files } : {}
|
|
132
137
|
};
|
|
133
138
|
}
|
|
@@ -296,7 +301,8 @@ const completionInboxKo = {
|
|
|
296
301
|
"completionInbox.emptyReadDescription": "확인한 알림이 여기에 표시됩니다.",
|
|
297
302
|
"completionInbox.unreadList": "미확인 알림 목록",
|
|
298
303
|
"completionInbox.readList": "확인한 알림 목록",
|
|
299
|
-
"completionInbox.projectFallback": "프로젝트"
|
|
304
|
+
"completionInbox.projectFallback": "프로젝트",
|
|
305
|
+
"completionInbox.platformNoticeFallbackTitle": "알림"
|
|
300
306
|
};
|
|
301
307
|
const agentBoardKo = {
|
|
302
308
|
"agentBoard.preparingResponse": "응답을 준비하는 중…",
|
|
@@ -622,8 +628,30 @@ const scheduledAutomationsKo = {
|
|
|
622
628
|
"scheduledAutomations.scheduleDaily": "매일 지정 시각",
|
|
623
629
|
"scheduledAutomations.intervalMinutesLabel": "실행 주기(분, 최소 {min}분)",
|
|
624
630
|
"scheduledAutomations.dailyTimeLabel": "실행 시각",
|
|
625
|
-
"scheduledAutomations.
|
|
631
|
+
"scheduledAutomations.monitorLabel": "변화 감지 커맨드 (선택)",
|
|
632
|
+
"scheduledAutomations.executionConditionLabel": "실행 조건",
|
|
633
|
+
"scheduledAutomations.executionEverySchedule": "예약마다 실행",
|
|
634
|
+
"scheduledAutomations.executionOnChange": "감시 결과가 바뀔 때만 실행",
|
|
635
|
+
"scheduledAutomations.advancedOptions": "고급 옵션 보기",
|
|
636
|
+
"scheduledAutomations.changeDetectionBadge": "변화 감지",
|
|
637
|
+
"scheduledAutomations.executionDetails": "실행 상세",
|
|
638
|
+
"scheduledAutomations.lastChange": "마지막 변화: {time}",
|
|
639
|
+
"scheduledAutomations.monitorOutputTitle": "최근 감시 출력",
|
|
640
|
+
"scheduledAutomations.monitorRequired": "변화 감지 명령을 입력하세요.",
|
|
641
|
+
"scheduledAutomations.templateLabel": "시작 템플릿",
|
|
642
|
+
"scheduledAutomations.templateNone": "직접 설정",
|
|
643
|
+
"scheduledAutomations.templateService": "서비스 상태 변화",
|
|
644
|
+
"scheduledAutomations.templateCommand": "명령 출력 변화",
|
|
645
|
+
"scheduledAutomations.templateHint": "템플릿은 폼만 채웁니다. 명령을 입력하고 확인한 뒤 직접 저장하세요.",
|
|
646
|
+
"scheduledAutomations.templateServiceName": "서비스 상태 감시",
|
|
647
|
+
"scheduledAutomations.templateCommandName": "명령 출력 감시",
|
|
648
|
+
"scheduledAutomations.templateServicePrompt": "서비스 상태가 어떻게 바뀌었는지 분석하고 필요한 조치를 제안해줘.",
|
|
649
|
+
"scheduledAutomations.templateCommandPrompt": "명령 출력의 변화를 분석하고 중요한 차이와 필요한 조치를 알려줘.",
|
|
650
|
+
"scheduledAutomations.monitorHint": "출력이 이전 성공 실행과 정확히 같으면 gate와 세션 실행을 생략합니다.",
|
|
651
|
+
"scheduledAutomations.notepadTitle": "자동화 메모장",
|
|
652
|
+
"scheduledAutomations.scriptLabel": "실행 gate 커맨드 (선택)",
|
|
626
653
|
"scheduledAutomations.scriptHint": '마지막 줄이 {"wakeAgent": false} JSON이면 세션을 깨우지 않습니다.',
|
|
654
|
+
"scheduledAutomations.scriptHintFallback": '마지막 줄 JSON의 {"wakeAgent": false}는 세션을 깨우지 않고, {"notepad": {"key": "value"}}는 읽기 전용 메모장을 갱신합니다.',
|
|
627
655
|
"scheduledAutomations.scriptWarning": "이 커맨드는 프로젝트 머신에서 사용자 권한으로 실행됩니다.",
|
|
628
656
|
"scheduledAutomations.suppressSilentLabel": "응답이 [SILENT]면 알림 배지 생략",
|
|
629
657
|
"scheduledAutomations.save": "저장",
|
|
@@ -637,6 +665,7 @@ const scheduledAutomationsKo = {
|
|
|
637
665
|
"scheduledAutomations.nextRun": "다음 실행: {time}",
|
|
638
666
|
"scheduledAutomations.historyTitle": "최근 실행",
|
|
639
667
|
"scheduledAutomations.outcomeWoke": "세션 실행",
|
|
668
|
+
"scheduledAutomations.outcomeNoChange": "모니터 변화 없음(스킵)",
|
|
640
669
|
"scheduledAutomations.outcomeSkippedGate": "변화 없음(스킵)",
|
|
641
670
|
"scheduledAutomations.outcomeSkippedOverlap": "이전 실행 진행 중(스킵)",
|
|
642
671
|
"scheduledAutomations.outcomeSilent": "이상 없음([SILENT])",
|
|
@@ -656,6 +685,7 @@ const scheduledAutomationsKo = {
|
|
|
656
685
|
"scheduledAutomations.errorViewerKeyInUse": "기존 DB 자동화가 다른 암호화 키를 사용하고 있어 자동으로 전환할 수 없습니다. 기존 DB 자동화를 삭제한 뒤 다시 시도하세요.",
|
|
657
686
|
"scheduledAutomations.errorWorkspaceMissing": "프로젝트 작업 폴더를 찾을 수 없습니다.",
|
|
658
687
|
"scheduledAutomations.errorMachineOffline": "프로젝트 머신이 오프라인입니다.",
|
|
688
|
+
"scheduledAutomations.errorMonitorUnsupported": "대상 daemon이 변화 감지 자동화를 지원하지 않습니다. 연결 상태를 확인한 뒤 다시 시도하세요.",
|
|
659
689
|
"scheduledAutomations.errorUnavailable": "자동화 서비스를 현재 사용할 수 없습니다.",
|
|
660
690
|
"scheduledAutomations.runNow": "바로 실행",
|
|
661
691
|
"scheduledAutomations.runRequestedNotice": "자동화 실행을 요청했습니다."
|
|
@@ -1299,6 +1329,9 @@ const subagentKo = {
|
|
|
1299
1329
|
"subagent.error.inFlight": "이전 제어 요청을 처리하는 중입니다.",
|
|
1300
1330
|
"subagent.error.rejected": "요청이 거부됐습니다: {detail}"
|
|
1301
1331
|
};
|
|
1332
|
+
const reviewNavigationKo = {
|
|
1333
|
+
"review.action.openSourceSession": "원본 대화로 이동: {title}"
|
|
1334
|
+
};
|
|
1302
1335
|
const aiCommitPrKo = {
|
|
1303
1336
|
"aiCommitPr.stagedCount": "스테이지된 파일 {count}개",
|
|
1304
1337
|
"aiCommitPr.noStaged": "스테이지된 변경이 없습니다.",
|
|
@@ -1339,7 +1372,39 @@ const aiCommitPrKo = {
|
|
|
1339
1372
|
"aiCommitPr.pr.create": "Pull Request 만들기",
|
|
1340
1373
|
"aiCommitPr.pr.creating": "만드는 중…"
|
|
1341
1374
|
};
|
|
1342
|
-
const
|
|
1375
|
+
const projectHandoffKo = {
|
|
1376
|
+
"projectHandoff.loading": "핸드오프를 확인하는 중…",
|
|
1377
|
+
"projectHandoff.notFound": "핸드오프를 찾을 수 없습니다.",
|
|
1378
|
+
"projectHandoff.autoApproving": "자동 승인 중…",
|
|
1379
|
+
"projectHandoff.autoApproveFailed": "자동 승인에 실패했습니다.",
|
|
1380
|
+
"projectHandoff.status.queued": "대기 중",
|
|
1381
|
+
"projectHandoff.status.running": "진행 중",
|
|
1382
|
+
"projectHandoff.status.completed": "완료됨",
|
|
1383
|
+
"projectHandoff.status.failed": "실패함",
|
|
1384
|
+
"projectHandoff.status.cancelled": "거절됨",
|
|
1385
|
+
"projectHandoff.status.blocked": "막힘",
|
|
1386
|
+
"projectHandoff.indicatorLabel": "넘긴 작업 {count}건 진행 중"
|
|
1387
|
+
};
|
|
1388
|
+
const projectLinksKo = {
|
|
1389
|
+
"projectLinks.empty": "등록된 관련 프로젝트가 없습니다.",
|
|
1390
|
+
"projectLinks.loading": "불러오는 중…",
|
|
1391
|
+
"projectLinks.loadFailed": "관련 프로젝트를 불러오지 못했습니다.",
|
|
1392
|
+
"projectLinks.unknownProject": "알 수 없는 프로젝트",
|
|
1393
|
+
"projectLinks.removeAria": "{name} 관련 등록 해제",
|
|
1394
|
+
"projectLinks.removeFailed": "관련 등록을 해제하지 못했습니다.",
|
|
1395
|
+
"projectLinks.selectProject": "프로젝트 선택",
|
|
1396
|
+
"projectLinks.selectProjectAria": "관련 프로젝트로 등록할 프로젝트",
|
|
1397
|
+
"projectLinks.rolePlaceholder": "역할 (예: 웹, 백엔드, 앱)",
|
|
1398
|
+
"projectLinks.add": "등록",
|
|
1399
|
+
"projectLinks.adding": "등록 중…",
|
|
1400
|
+
"projectLinks.addFailed": "관련 프로젝트를 등록하지 못했습니다.",
|
|
1401
|
+
"projectLinks.noCandidates": "등록할 수 있는 다른 프로젝트가 없습니다.",
|
|
1402
|
+
"projectLinks.unavailableInDemo": "데모 모드에서는 관련 프로젝트를 관리할 수 없습니다.",
|
|
1403
|
+
"projectLinks.mcpToggleLabel": "AI가 이 프로젝트에서 작업을 넘길 수 있게 허용",
|
|
1404
|
+
"projectLinks.mcpToggleHint": "켜면 AI가 관련 프로젝트로 작업을 제안했을 때 확인 없이 바로 진행됩니다.",
|
|
1405
|
+
"projectLinks.mcpToggleFailed": "설정을 바꾸지 못했습니다."
|
|
1406
|
+
};
|
|
1407
|
+
const catalogExtensionsKo = { ...taskNotificationKo, ...machineUsageKo, ...completionInboxKo, ...agentBoardKo, ...fanoutLifecycleKo, ...conversationSearchKo, ...browserCookieImportKo, ...orcaPortKo, ...usageAnalyticsKo, ...outboundWebhookKo, ...scheduledAutomationsKo, ...githubTriggersKo, ...memoryHubKo, ...extensionPanelKo, ...extensionInstallKo, ...personalChatsKo, ...newSessionStarterKo, ...browserConsoleCaptureKo, ...completionSignalKo, ...agentLoopDetectionKo, ...operationModelRetryKo, ...skillCatalogKo, ...ciAutoFixKo, ...collaborationKo, ...backgroundTaskKo, ...sessionTitleKo, ...accountSettingsKo, ...projectScopeKo, ...machineSettingsConsoleKo, ...sessionTokenUsageKo, ...worktreeSetupKo, ...connectionIndicatorKo, ...voiceDictationKo, ...newSessionProfileKo, ...agentBoardBroadcastKo, ...judgementEvidenceKo, ...releaseNoteKo, ...notificationSoundKo, ...officeViewerKo, ...additionalDirectoriesKo, ...agentHandoffKo, ...sidebarUpdateKo, ...autopilotSelfReviewRoundsKo, ...aiCommitPrKo, ...externalWorktreeInboxKo, ...externalSessionVaultKo, ...terminalFontKo, ...globalPauseKo, ...subagentKo, ...reviewNavigationKo, ...projectLinksKo, ...projectHandoffKo };
|
|
1343
1408
|
const onboardingKo = {
|
|
1344
1409
|
"onboarding.dialogAria": "시작 설정",
|
|
1345
1410
|
"onboarding.eyebrow": "처음 시작하기",
|
|
@@ -2000,6 +2065,10 @@ const ko = {
|
|
|
2000
2065
|
"worktreePathCopy.copy": "worktree 경로 복사",
|
|
2001
2066
|
"worktreePathCopy.copied": "worktree 경로 복사됨",
|
|
2002
2067
|
"worktreePathCopy.failed": "worktree 경로 복사 실패",
|
|
2068
|
+
"sessionWorktree.worktree": "워크트리: {name}",
|
|
2069
|
+
"sessionWorktree.branch": "브랜치: {branch}",
|
|
2070
|
+
"sessionWorktree.path": "경로: {path}",
|
|
2071
|
+
"sessionWorktree.merged": "병합됨",
|
|
2003
2072
|
// Project git controls
|
|
2004
2073
|
"gitControls.branchLoading": "Git branch 확인 중",
|
|
2005
2074
|
"gitControls.noRemote": "origin remote 또는 branch를 찾을 수 없습니다.",
|
|
@@ -2008,9 +2077,9 @@ const ko = {
|
|
|
2008
2077
|
"gitControls.manageBranchAria": "{branch} branch 관리",
|
|
2009
2078
|
"gitControls.manageBranchTitle": "Branch 변경 및 생성",
|
|
2010
2079
|
"gitControls.manageBranchTitleWithDesc": "Branch 변경 및 생성 · {description}",
|
|
2011
|
-
"gitControls.pullAria": "{branch}
|
|
2080
|
+
"gitControls.pullAria": "{branch} 브랜치 pull",
|
|
2012
2081
|
"gitControls.pullTitle": "{branch} pull",
|
|
2013
|
-
"gitControls.pushAria": "{branch}
|
|
2082
|
+
"gitControls.pushAria": "{branch} 브랜치 push",
|
|
2014
2083
|
"gitControls.pushTitle": "{branch} push",
|
|
2015
2084
|
"gitControls.manageWorktreeAria": "{name} worktree 관리",
|
|
2016
2085
|
"gitControls.manageWorktreeTitle": "Worktree 관리",
|
|
@@ -2278,6 +2347,7 @@ const ko = {
|
|
|
2278
2347
|
"projectSettings.tabRepository": "코드 저장소",
|
|
2279
2348
|
"projectSettings.tabDeploy": "배포 저장소",
|
|
2280
2349
|
"projectSettings.tabCollaboration": "협업",
|
|
2350
|
+
"projectSettings.tabLinks": "관련 프로젝트",
|
|
2281
2351
|
"projectSettings.tabMachine": "머신 및 경로",
|
|
2282
2352
|
"projectSettings.tabRun": "실행",
|
|
2283
2353
|
"projectSettings.tabMcp": "MCP 서버/스킬",
|
|
@@ -2733,6 +2803,8 @@ const ko = {
|
|
|
2733
2803
|
"standaloneImport.doneSummary": "프로젝트 {projects}개, 세션 {sessions}개를 가져왔어요.",
|
|
2734
2804
|
"standaloneImport.doneSkipped": "이미 가져온 세션 {count}개는 건너뛰었어요.",
|
|
2735
2805
|
"standaloneImport.undecryptable": "복호화할 수 없어 건너뛴 메시지가 {count}개 있어요.",
|
|
2806
|
+
"standaloneImport.undecryptableMetadata": "세션 {count}개의 메타데이터 또는 에이전트 상태를 복호화하지 못해 일부 상태 없이 가져왔어요.",
|
|
2807
|
+
"standaloneImport.failedProjects": "프로젝트 {count}개를 가져오지 못했어요. 가능한 세션은 프로젝트 연결 없이 가져왔어요.",
|
|
2736
2808
|
"standaloneImport.failedSessions": "세션 {count}개를 가져오지 못했어요. 다시 실행하면 이어서 시도해요.",
|
|
2737
2809
|
"standaloneImport.fatalError": "가져오기를 시작하지 못했어요: {error}",
|
|
2738
2810
|
"standaloneImport.settingsBody": "이 컴퓨터의 로컬 전용 모드 데이터를 현재 계정으로 복사해요.",
|
|
@@ -2905,6 +2977,7 @@ const ko = {
|
|
|
2905
2977
|
"worktreeOp.checkingStatus": "worktree 상태 확인 중...",
|
|
2906
2978
|
"worktreeOp.forceDeleteUnpushed": "worktree에 푸시되지 않은 커밋 {count}개가 있습니다. 강제로 삭제할까요? 커밋이 사라집니다.",
|
|
2907
2979
|
"worktreeOp.forceDeleteDirty": "worktree에 정리되지 않은 변경이 있습니다. 강제로 삭제할까요? 변경 사항이 사라집니다.",
|
|
2980
|
+
"worktreeOp.trackedChangesBlocked": "이 worktree에 커밋되지 않은 tracked 변경이 있습니다. 변경을 커밋하거나 stash하거나 명시적으로 버린 뒤 다시 시도하세요.",
|
|
2908
2981
|
"worktreeOp.removeCancelled": "정리되지 않은 변경/커밋이 있어 삭제를 취소했습니다.",
|
|
2909
2982
|
"worktreeOp.removed": "worktree를 삭제했습니다.",
|
|
2910
2983
|
"project.sharedBy": " · 공유: {owner}",
|
|
@@ -3211,6 +3284,8 @@ const ko = {
|
|
|
3211
3284
|
"sessionsTab.searchFailed": "사용자 프롬프트를 검색하지 못했습니다. 제목 검색 결과만 표시합니다.",
|
|
3212
3285
|
"sessionsTab.noSearchResults": "“{query}”와 일치하는 대화가 없습니다.",
|
|
3213
3286
|
"sessionsTab.clearSearch": "검색 지우기",
|
|
3287
|
+
"sessionsTab.noVisibleMessage": "표시할 메시지가 아직 없습니다",
|
|
3288
|
+
"sessionsTab.updated": "업데이트",
|
|
3214
3289
|
// Planning / preview sidecar / preview tab
|
|
3215
3290
|
"previewSidecar.closeAria": "Preview 닫기",
|
|
3216
3291
|
"preview.serverStarting": "서버 시작 중...",
|
|
@@ -3268,6 +3343,7 @@ const ko = {
|
|
|
3268
3343
|
"saycodePromptPreference.eyebrow": "에이전트 지시 선택",
|
|
3269
3344
|
"saycodePromptPreference.title": "Saycode 기본 지시를 사용할까요?",
|
|
3270
3345
|
"saycodePromptPreference.body": "내가 만든 Claude/Codex 스킬과 하네스를 그대로 쓰려면 꺼진 상태를 유지하세요. 나중에 환경설정에서 언제든 바꿀 수 있습니다.",
|
|
3346
|
+
"saycodePromptPreference.defaultOnNotice": "자식 Agent 호출, 내부 작업 위임, 결과물 인라인 미리보기는 기본으로 계속 켜집니다. 이 기능도 끄려면 환경설정 → 시스템 프롬프트에서 각각 해제하세요.",
|
|
3271
3347
|
"saycodePromptPreference.keepOff": "꺼진 상태 유지",
|
|
3272
3348
|
"saycodePromptPreference.enable": "Saycode 지시 켜기",
|
|
3273
3349
|
"saycodePromptPreference.saving": "저장 중…",
|
|
@@ -3276,14 +3352,18 @@ const ko = {
|
|
|
3276
3352
|
"saycodePromptPreference.settingsEnable": "Saycode가 제공하는 기본 행동 지시 사용",
|
|
3277
3353
|
"saycodePromptPreference.settingsHelp": "사용자 시스템 프롬프트, 프로젝트 지침, 설치한 스킬과 하네스는 이 설정과 관계없이 유지됩니다.",
|
|
3278
3354
|
"saycodePromptPreference.blocksTitle": "개별 지시 선택",
|
|
3279
|
-
"saycodePromptPreference.blocksHelp": "
|
|
3355
|
+
"saycodePromptPreference.blocksHelp": "자식 Agent 호출, 내부 작업 위임, 결과물 인라인 미리보기는 기본으로 켜지며, 나머지 항목은 위의 전체 설정을 따릅니다.",
|
|
3280
3356
|
"saycodePromptPreference.alwaysOnNotice": "채팅 제목 생성은 항상 켜져 있으며 끌 수 없습니다. 채팅 목록에서 대화를 다시 찾으려면 필요합니다.",
|
|
3281
|
-
"saycodePromptPreference.block.
|
|
3357
|
+
"saycodePromptPreference.block.agentOrchestration.label": "자식 Agent 호출",
|
|
3358
|
+
"saycodePromptPreference.block.agentOrchestration.description": "자연어로 요청한 작업을 독립된 자식 Agent 세션에 맡기고 결과를 회수하도록 지시합니다.",
|
|
3359
|
+
"saycodePromptPreference.block.workerDelegation.label": "내부 작업 위임",
|
|
3282
3360
|
"saycodePromptPreference.block.workerDelegation.description": "기계적인 작업을 저렴한 worker 에이전트에 넘기도록 지시합니다.",
|
|
3283
3361
|
"saycodePromptPreference.block.axBase.label": "기획부터 시작 워크플로우",
|
|
3284
3362
|
"saycodePromptPreference.block.axBase.description": "AX 워크플로우의 기본 지시를 사용합니다.",
|
|
3285
3363
|
"saycodePromptPreference.block.coAuthoredCredit.label": "커밋 크레딧",
|
|
3286
3364
|
"saycodePromptPreference.block.coAuthoredCredit.description": "커밋 메시지에 Claude와 Happy 공동 작성자 표시를 추가합니다.",
|
|
3365
|
+
"saycodePromptPreference.block.inlinePreview.label": "결과물 인라인 미리보기",
|
|
3366
|
+
"saycodePromptPreference.block.inlinePreview.description": "HTML 결과물을 만들면 답변 안에 미리보기를 띄우도록 지시합니다.",
|
|
3287
3367
|
"saycodePromptPreference.loading": "계정 설정을 불러오는 중…",
|
|
3288
3368
|
"standalone.zrokStatus": "zrok 상태",
|
|
3289
3369
|
"standalone.zrokNeeded": "zrok2 설정이 필요합니다",
|
|
@@ -3834,6 +3914,7 @@ const ko = {
|
|
|
3834
3914
|
"review.self.loopStop": "반복 중지",
|
|
3835
3915
|
"review.self.loopDismiss": "기록 지우기",
|
|
3836
3916
|
"review.self.loopDone": "반복 리뷰 종료 ({rounds}회) — {reason}",
|
|
3917
|
+
"review.self.startCancelled": "다른 창의 작업으로 반복 리뷰 시작이 취소됐어요.",
|
|
3837
3918
|
"review.self.loopReason.clean": "지적 없음",
|
|
3838
3919
|
"review.self.loopReason.belowThreshold": "low·nit만 남아 조기 종료",
|
|
3839
3920
|
"review.self.loopReason.roundsExhausted": "지정한 횟수를 모두 사용",
|
|
@@ -4415,7 +4496,7 @@ Options
|
|
|
4415
4496
|
-p, --project <id|name> Project id prefix or name
|
|
4416
4497
|
-d, --directory <path> Working directory on that machine
|
|
4417
4498
|
-s, --session <id> Session id (see \`saycode ls\`)
|
|
4418
|
-
--agent <name> claude (default), codex, gemini, openclaw, opencode
|
|
4499
|
+
--agent <name> claude (default), codex, gemini, grok, openclaw, opencode
|
|
4419
4500
|
--model <name> Model for new-session messages (Default if omitted)
|
|
4420
4501
|
--effort <level> Model effort (validated against agent + model)
|
|
4421
4502
|
--worktree Create an isolated worktree for a project chat
|
|
@@ -4438,7 +4519,8 @@ Agent commands (machine-readable JSON on stdout, exit 3 on failure)
|
|
|
4438
4519
|
agent prompt <session> <text> [--wait] Send a prompt, optionally wait for it
|
|
4439
4520
|
agent steer <session> <text> Redirect an owned child's active response
|
|
4440
4521
|
agent stop <session> Stop an owned child's active response
|
|
4441
|
-
agent spawn --prompt <text> [--agent n]
|
|
4522
|
+
agent spawn --prompt <text> [--agent n] [--model n] [--effort n]
|
|
4523
|
+
Start a sibling with an explicit execution choice
|
|
4442
4524
|
agent spawn --prompt <text> --file <path> ...with context files it reads first
|
|
4443
4525
|
Only works inside a session Saycode started (SAYCODE_AGENT_ENV).
|
|
4444
4526
|
|
|
@@ -6837,6 +6919,58 @@ function buildUserTextMessageContent(input) {
|
|
|
6837
6919
|
}
|
|
6838
6920
|
};
|
|
6839
6921
|
}
|
|
6922
|
+
function sanitizedString(value2) {
|
|
6923
|
+
return typeof value2 === "string" && value2.trim() ? value2.trim() : null;
|
|
6924
|
+
}
|
|
6925
|
+
function sanitizedCompanyId(value2) {
|
|
6926
|
+
if (typeof value2 === "string") return value2.trim() ? value2.trim() : null;
|
|
6927
|
+
if (value2 === null) return null;
|
|
6928
|
+
return void 0;
|
|
6929
|
+
}
|
|
6930
|
+
function buildStoredNotice(sessionId, value2, receivedAt) {
|
|
6931
|
+
const companyId = sanitizedCompanyId(value2.companyId);
|
|
6932
|
+
return {
|
|
6933
|
+
sessionId,
|
|
6934
|
+
projectId: sanitizedString(value2.projectId),
|
|
6935
|
+
...sanitizedString(value2.personalChatId) ? { personalChatId: sanitizedString(value2.personalChatId) } : {},
|
|
6936
|
+
projectName: sanitizedString(value2.projectName),
|
|
6937
|
+
sessionTitle: sanitizedString(value2.sessionTitle),
|
|
6938
|
+
status: value2.status === "completed" || value2.status === "failed" || value2.status === "cancelled" ? value2.status : "completed",
|
|
6939
|
+
receivedAt,
|
|
6940
|
+
...companyId !== void 0 ? { companyId } : {},
|
|
6941
|
+
// Only non-chat kinds are stored; absence means chat so legacy snapshots round-trip.
|
|
6942
|
+
...value2.kind === "deploy" || value2.kind === "automation" || value2.kind === "platform_notice" ? { kind: value2.kind } : {},
|
|
6943
|
+
// title/body are platform_notice-only (server-authoritative admin message). Other
|
|
6944
|
+
// kinds never legitimately carry them, so they are dropped rather than stored blank.
|
|
6945
|
+
...value2.kind === "platform_notice" ? { title: sanitizedString(value2.title), body: sanitizedString(value2.body) } : {}
|
|
6946
|
+
};
|
|
6947
|
+
}
|
|
6948
|
+
function parseServerCompletionNotifications(payload) {
|
|
6949
|
+
if (!payload || typeof payload !== "object") return [];
|
|
6950
|
+
const { notifications } = payload;
|
|
6951
|
+
if (!Array.isArray(notifications)) return [];
|
|
6952
|
+
const result = [];
|
|
6953
|
+
for (const raw of notifications) {
|
|
6954
|
+
if (!raw || typeof raw !== "object") continue;
|
|
6955
|
+
const value2 = raw;
|
|
6956
|
+
if (value2.kind !== "platform_notice") continue;
|
|
6957
|
+
const sessionId = sanitizedString(value2.sessionId);
|
|
6958
|
+
if (!sessionId) continue;
|
|
6959
|
+
result.push({
|
|
6960
|
+
...buildStoredNotice(
|
|
6961
|
+
sessionId,
|
|
6962
|
+
value2,
|
|
6963
|
+
typeof value2.receivedAt === "number" && Number.isFinite(value2.receivedAt) ? value2.receivedAt : 0
|
|
6964
|
+
),
|
|
6965
|
+
// 서버 목록은 읽은 행도 함께 돌려주고(`listCompletionNotifications` 는 unread 로
|
|
6966
|
+
// 필터하지 않는다) 읽음 여부를 `unread` 에 담는다. `buildStoredNotice` 는 이 필드를
|
|
6967
|
+
// 모르므로 여기서 직접 얹지 않으면, 조회할 때마다 `recordCompletionNotice` 의
|
|
6968
|
+
// 기본값(미확인)으로 되살아난다.
|
|
6969
|
+
...typeof value2.unread === "boolean" ? { unread: value2.unread } : {}
|
|
6970
|
+
});
|
|
6971
|
+
}
|
|
6972
|
+
return result;
|
|
6973
|
+
}
|
|
6840
6974
|
const CONNECTOR_PROVIDER_IDS = [
|
|
6841
6975
|
"notion",
|
|
6842
6976
|
"slack",
|
|
@@ -15966,6 +16100,31 @@ function coercePromptSuggestion(value2) {
|
|
|
15966
16100
|
if (!text2 || !provider2 || updatedAt === void 0) return void 0;
|
|
15967
16101
|
return { text: text2, provider: provider2, updatedAt };
|
|
15968
16102
|
}
|
|
16103
|
+
const MCP_RUNTIME_STATUSES = /* @__PURE__ */ new Set([
|
|
16104
|
+
"connected",
|
|
16105
|
+
"reconnecting",
|
|
16106
|
+
"failed",
|
|
16107
|
+
"needs-auth",
|
|
16108
|
+
"config-fetch-failed",
|
|
16109
|
+
"mcp-config-missing",
|
|
16110
|
+
"connector-config-missing",
|
|
16111
|
+
"connector-runtime-failed",
|
|
16112
|
+
"connector-needs-auth"
|
|
16113
|
+
]);
|
|
16114
|
+
function coerceMcpServerStatuses(value2) {
|
|
16115
|
+
if (!Array.isArray(value2)) return void 0;
|
|
16116
|
+
const entries = [];
|
|
16117
|
+
for (const raw of value2) {
|
|
16118
|
+
if (!isRecord$1(raw)) continue;
|
|
16119
|
+
const name = nonEmptyString(raw.name);
|
|
16120
|
+
const status = typeof raw.status === "string" && MCP_RUNTIME_STATUSES.has(raw.status) ? raw.status : void 0;
|
|
16121
|
+
const checkedAt = optionalTimestamp(raw.checkedAt);
|
|
16122
|
+
if (!name || !status || checkedAt === void 0) continue;
|
|
16123
|
+
const error = nonEmptyString(raw.error);
|
|
16124
|
+
entries.push({ name, status, checkedAt, ...error ? { error } : {} });
|
|
16125
|
+
}
|
|
16126
|
+
return entries.length > 0 ? entries : void 0;
|
|
16127
|
+
}
|
|
15969
16128
|
function coerceCreatedBy(value2) {
|
|
15970
16129
|
if (!isRecord$1(value2)) return void 0;
|
|
15971
16130
|
const accountId = nonEmptyString(value2.accountId);
|
|
@@ -16000,6 +16159,7 @@ function apiSessionToSession(api, candidateKeys, existing) {
|
|
|
16000
16159
|
const branchSlug = branchSlugFromMetadata(metadata) ?? existing?.branchSlug;
|
|
16001
16160
|
const path = nonEmptyString(metadata?.path) ?? existing?.path;
|
|
16002
16161
|
const promptSuggestion2 = metadata && Object.prototype.hasOwnProperty.call(metadata, "promptSuggestion") ? coercePromptSuggestion(metadata.promptSuggestion) : existing?.promptSuggestion;
|
|
16162
|
+
const mcpServers = coerceMcpServerStatuses(metadata?.mcpServers) ?? existing?.mcpServers;
|
|
16003
16163
|
return {
|
|
16004
16164
|
...existing,
|
|
16005
16165
|
id: api.id,
|
|
@@ -16026,6 +16186,7 @@ function apiSessionToSession(api, candidateKeys, existing) {
|
|
|
16026
16186
|
...currentModelCode ? { currentModelCode } : {},
|
|
16027
16187
|
...tokenUsage ? { tokenUsage } : {},
|
|
16028
16188
|
...slashCommands ? { slashCommands } : {},
|
|
16189
|
+
...mcpServers ? { mcpServers } : {},
|
|
16029
16190
|
...claudeSessionId ? { claudeSessionId } : {},
|
|
16030
16191
|
...codexThreadId ? { codexThreadId } : {},
|
|
16031
16192
|
...lifecycleState ? { lifecycleState } : {},
|
|
@@ -16068,11 +16229,16 @@ function readMcpCallerGrantPublicKey(daemonState) {
|
|
|
16068
16229
|
const value2 = daemonState.mcpCallerGrantPublicKey;
|
|
16069
16230
|
return typeof value2 === "string" && value2 ? value2 : void 0;
|
|
16070
16231
|
}
|
|
16071
|
-
|
|
16232
|
+
const CLEARABLE_STRING_KEYS = /* @__PURE__ */ new Set(["branch", "baseBranch"]);
|
|
16233
|
+
function buildWorktreeMutationBody(input, options = {}) {
|
|
16072
16234
|
return JSON.stringify(Object.fromEntries(
|
|
16073
|
-
Object.entries(input).filter(([, value2]) => typeof value2 === "string" && value2.length > 0)
|
|
16235
|
+
Object.entries(input).filter(([key, value2]) => (key !== "machinePlatform" || options.includeMachinePlatform) && (typeof value2 === "string" && (value2.length > 0 || CLEARABLE_STRING_KEYS.has(key)) || value2 === null && CLEARABLE_STRING_KEYS.has(key)))
|
|
16074
16236
|
));
|
|
16075
16237
|
}
|
|
16238
|
+
const STANDALONE_USER_ID = "standalone-local";
|
|
16239
|
+
function isStandaloneAuthSession(session) {
|
|
16240
|
+
return session?.userId === STANDALONE_USER_ID;
|
|
16241
|
+
}
|
|
16076
16242
|
function indexMachineRegistry(entries) {
|
|
16077
16243
|
const byId = {};
|
|
16078
16244
|
for (const entry of entries) {
|
|
@@ -16137,6 +16303,17 @@ function resolvedProjectPreviewRelayUrl(input) {
|
|
|
16137
16303
|
}
|
|
16138
16304
|
return `${input.syncBaseUrl}/v1/preview/${encodeURIComponent(input.machineId)}/${input.port}/?ptoken=${encodeURIComponent(response.token)}`;
|
|
16139
16305
|
}
|
|
16306
|
+
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
16307
|
+
function versionedAutomationBase64(value2, maxBytes, exactBytes) {
|
|
16308
|
+
if (typeof value2 !== "string" || !value2 || value2.length > Math.ceil(maxBytes / 3) * 4 || !BASE64_PATTERN.test(value2)) {
|
|
16309
|
+
throw new Error("automation-response-invalid");
|
|
16310
|
+
}
|
|
16311
|
+
const decoded = Buffer$1.from(value2, "base64");
|
|
16312
|
+
if (decoded.length < 41 || decoded.length > maxBytes || exactBytes !== void 0 && decoded.length !== exactBytes || decoded[0] !== 1) {
|
|
16313
|
+
throw new Error("automation-response-invalid");
|
|
16314
|
+
}
|
|
16315
|
+
return value2;
|
|
16316
|
+
}
|
|
16140
16317
|
function row(value2) {
|
|
16141
16318
|
if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) throw new Error("automation-response-invalid");
|
|
16142
16319
|
return value2;
|
|
@@ -16158,10 +16335,23 @@ function nullableInteger(value2) {
|
|
|
16158
16335
|
function optionalNullableInteger(value2) {
|
|
16159
16336
|
return value2 === void 0 ? null : nullableInteger(value2);
|
|
16160
16337
|
}
|
|
16161
|
-
function
|
|
16338
|
+
function parseRuntimeState(value2) {
|
|
16162
16339
|
const item = row(value2);
|
|
16163
|
-
if (item.payloadVersion !== 1 || typeof item.paused !== "boolean") throw new Error("automation-response-invalid");
|
|
16164
16340
|
return {
|
|
16341
|
+
automationId: stringValue(item.automationId),
|
|
16342
|
+
generation: integer(item.generation, 1),
|
|
16343
|
+
stateVersion: integer(item.stateVersion, 1),
|
|
16344
|
+
stateCiphertext: versionedAutomationBase64(item.stateCiphertext, 96 * 1024),
|
|
16345
|
+
viewerKeyVersion: integer(item.viewerKeyVersion, 1),
|
|
16346
|
+
viewerKeyEnvelope: versionedAutomationBase64(item.viewerKeyEnvelope, 105, 105),
|
|
16347
|
+
machineKeyVersion: integer(item.machineKeyVersion, 1),
|
|
16348
|
+
updatedAt: integer(item.updatedAt)
|
|
16349
|
+
};
|
|
16350
|
+
}
|
|
16351
|
+
function parseServerAutomationPublic(value2) {
|
|
16352
|
+
const item = row(value2);
|
|
16353
|
+
if (item.payloadVersion !== 1 && item.payloadVersion !== 2 || typeof item.paused !== "boolean") throw new Error("automation-response-invalid");
|
|
16354
|
+
const parsed = {
|
|
16165
16355
|
id: stringValue(item.id),
|
|
16166
16356
|
projectId: stringValue(item.projectId),
|
|
16167
16357
|
ownerAccountId: stringValue(item.ownerAccountId),
|
|
@@ -16169,7 +16359,7 @@ function parseServerAutomationPublic(value2) {
|
|
|
16169
16359
|
machineId: nullableString(item.machineId),
|
|
16170
16360
|
revision: integer(item.revision, 1),
|
|
16171
16361
|
generation: integer(item.generation, 1),
|
|
16172
|
-
payloadVersion:
|
|
16362
|
+
payloadVersion: item.payloadVersion,
|
|
16173
16363
|
payloadCiphertext: stringValue(item.payloadCiphertext),
|
|
16174
16364
|
viewerKeyId: stringValue(item.viewerKeyId),
|
|
16175
16365
|
viewerKeyVersion: integer(item.viewerKeyVersion, 1),
|
|
@@ -16181,8 +16371,13 @@ function parseServerAutomationPublic(value2) {
|
|
|
16181
16371
|
appliedRevision: integer(item.appliedRevision),
|
|
16182
16372
|
appliedAt: nullableInteger(item.appliedAt),
|
|
16183
16373
|
createdAt: integer(item.createdAt),
|
|
16184
|
-
updatedAt: integer(item.updatedAt)
|
|
16374
|
+
updatedAt: integer(item.updatedAt),
|
|
16375
|
+
runtimeState: item.runtimeState === void 0 || item.runtimeState === null ? null : parseRuntimeState(item.runtimeState)
|
|
16185
16376
|
};
|
|
16377
|
+
if (parsed.payloadVersion === 2 !== (parsed.runtimeState !== null) || parsed.runtimeState && (parsed.runtimeState.automationId !== parsed.id || parsed.runtimeState.generation !== parsed.generation || parsed.runtimeState.viewerKeyVersion !== parsed.viewerKeyVersion || parsed.runtimeState.machineKeyVersion !== parsed.machineKeyVersion)) {
|
|
16378
|
+
throw new Error("automation-response-invalid");
|
|
16379
|
+
}
|
|
16380
|
+
return parsed;
|
|
16186
16381
|
}
|
|
16187
16382
|
function parseServerAutomationAdoption(value2) {
|
|
16188
16383
|
const item = row(value2);
|
|
@@ -16203,11 +16398,12 @@ function parseServerAutomationTarget(value2) {
|
|
|
16203
16398
|
machinePublicKey: stringValue(item.machinePublicKey),
|
|
16204
16399
|
machineKeyVersion: integer(item.machineKeyVersion, 1),
|
|
16205
16400
|
viewerPublicKey: nullableString(item.viewerPublicKey),
|
|
16206
|
-
viewerKeyVersion: integer(item.viewerKeyVersion)
|
|
16401
|
+
viewerKeyVersion: integer(item.viewerKeyVersion),
|
|
16402
|
+
automationProtocolVersion: item.automationProtocolVersion === void 0 ? 1 : integer(item.automationProtocolVersion, 1)
|
|
16207
16403
|
};
|
|
16208
16404
|
}
|
|
16209
16405
|
const RUN_STATUSES = /* @__PURE__ */ new Set(["CLAIMED", "RUNNING", "COMPLETED", "FAILED", "CANCELLED", "EXPIRED", "ABANDONED"]);
|
|
16210
|
-
const RUN_OUTCOMES = /* @__PURE__ */ new Set(["WOKE", "SILENT", "SKIPPED_GATE", "ERROR"]);
|
|
16406
|
+
const RUN_OUTCOMES = /* @__PURE__ */ new Set(["WOKE", "SILENT", "SKIPPED_GATE", "NO_CHANGE", "ERROR"]);
|
|
16211
16407
|
function parseServerAutomationRun(value2) {
|
|
16212
16408
|
const item = row(value2);
|
|
16213
16409
|
if (!RUN_STATUSES.has(item.status) || item.outcome !== null && !RUN_OUTCOMES.has(item.outcome) || typeof item.lateReport !== "boolean") throw new Error("automation-response-invalid");
|
|
@@ -16463,6 +16659,7 @@ function normalizeAutomationSupport(value2) {
|
|
|
16463
16659
|
return {
|
|
16464
16660
|
rpcAvailable: capability.rpcAvailable,
|
|
16465
16661
|
serverBacked: capability.serverBacked === true,
|
|
16662
|
+
...typeof capability.monitorMode === "boolean" ? { monitorMode: capability.monitorMode } : {},
|
|
16466
16663
|
...Number.isSafeInteger(capability.keyVersion) && capability.keyVersion > 0 ? { keyVersion: capability.keyVersion } : {}
|
|
16467
16664
|
};
|
|
16468
16665
|
}
|
|
@@ -16506,7 +16703,8 @@ class ApiClient {
|
|
|
16506
16703
|
apiBaseUrl: session.apiBaseUrl,
|
|
16507
16704
|
syncBaseUrl: session.happyBaseUrl,
|
|
16508
16705
|
apiToken: session.token,
|
|
16509
|
-
syncToken: session.syncToken || session.token
|
|
16706
|
+
syncToken: session.syncToken || session.token,
|
|
16707
|
+
standalone: isStandaloneAuthSession(session)
|
|
16510
16708
|
});
|
|
16511
16709
|
}
|
|
16512
16710
|
async login(input) {
|
|
@@ -16571,6 +16769,117 @@ class ApiClient {
|
|
|
16571
16769
|
});
|
|
16572
16770
|
return data.items ?? [];
|
|
16573
16771
|
}
|
|
16772
|
+
/**
|
|
16773
|
+
* 관리자가 보낸 플랫폼 공지(`kind: 'platform_notice'`)만 골라 돌려준다. 다른 kind
|
|
16774
|
+
* (`chat_completion`)는 데스크탑이 이미 스스로 만들고 있어 동기화 대상이 아니다.
|
|
16775
|
+
* specs/desktop-platform-notice-inbox
|
|
16776
|
+
*/
|
|
16777
|
+
async listServerNotifications() {
|
|
16778
|
+
const data = await this.request("/api/me/completion-notifications", { boundary: "api" });
|
|
16779
|
+
return parseServerCompletionNotifications(data);
|
|
16780
|
+
}
|
|
16781
|
+
/**
|
|
16782
|
+
* 서버 알림함에 읽음을 반영한다. `kind` 를 반드시 넘긴다 — 서버 유니크 키가
|
|
16783
|
+
* (userId, kind, sessionId) 라 kind 없이는 같은 세션의 어느 kind를 읽음 처리할지
|
|
16784
|
+
* 서버가 구분하지 못한다. 실패해도 로컬 상태는 이미 반영됐으므로 호출부가 삼킨다.
|
|
16785
|
+
* specs/desktop-platform-notice-inbox R4
|
|
16786
|
+
*/
|
|
16787
|
+
async markServerNotificationRead(sessionId, kind) {
|
|
16788
|
+
await this.request(`/api/me/completion-notifications/${encodeURIComponent(sessionId)}/read?kind=${encodeURIComponent(kind)}`, {
|
|
16789
|
+
method: "PATCH",
|
|
16790
|
+
boundary: "api"
|
|
16791
|
+
});
|
|
16792
|
+
}
|
|
16793
|
+
/**
|
|
16794
|
+
* 관련 프로젝트 등록 목록. 서버가 양방향으로 조회해 준다(대상으로 등록한 쪽도
|
|
16795
|
+
* 나온다). specs/desktop-related-project-handoff
|
|
16796
|
+
*/
|
|
16797
|
+
async listProjectLinks(projectId) {
|
|
16798
|
+
const data = await this.request(
|
|
16799
|
+
`/api/projects/${encodeURIComponent(projectId)}/links`,
|
|
16800
|
+
{ boundary: "api" }
|
|
16801
|
+
);
|
|
16802
|
+
return data.links ?? [];
|
|
16803
|
+
}
|
|
16804
|
+
async addProjectLink(projectId, targetProjectId, role) {
|
|
16805
|
+
const data = await this.request(
|
|
16806
|
+
`/api/projects/${encodeURIComponent(projectId)}/links`,
|
|
16807
|
+
{ method: "POST", boundary: "api", body: JSON.stringify({ targetProjectId, role }) }
|
|
16808
|
+
);
|
|
16809
|
+
return data.link;
|
|
16810
|
+
}
|
|
16811
|
+
async removeProjectLink(projectId, targetProjectId) {
|
|
16812
|
+
await this.request(`/api/projects/${encodeURIComponent(projectId)}/links`, {
|
|
16813
|
+
method: "DELETE",
|
|
16814
|
+
boundary: "api",
|
|
16815
|
+
body: JSON.stringify({ targetProjectId })
|
|
16816
|
+
});
|
|
16817
|
+
}
|
|
16818
|
+
/** 진행 중(queued/running/blocked) 핸드오프 전체 — 전역 인디케이터용. */
|
|
16819
|
+
async listActiveProjectHandoffs() {
|
|
16820
|
+
const data = await this.request("/api/handoffs", { boundary: "api" });
|
|
16821
|
+
return data.handoffs ?? [];
|
|
16822
|
+
}
|
|
16823
|
+
/** 이 프로젝트에서 내보낸 핸드오프 전체 — 프로젝트 설정 화면용. */
|
|
16824
|
+
async listProjectHandoffsForProject(projectId) {
|
|
16825
|
+
const data = await this.request(
|
|
16826
|
+
`/api/projects/${encodeURIComponent(projectId)}/handoffs`,
|
|
16827
|
+
{ boundary: "api" }
|
|
16828
|
+
);
|
|
16829
|
+
return data.handoffs ?? [];
|
|
16830
|
+
}
|
|
16831
|
+
/**
|
|
16832
|
+
* 승인. `sourceSessionId` 는 승인 카드가 렌더된 대화 — 서버가 딥링크 조립에 쓴다.
|
|
16833
|
+
* 이미 승인된(queued 이상) 핸드오프를 다시 승인하면 서버가 409 를 던진다(멱등은
|
|
16834
|
+
* 호출부가 그 에러를 흡수하는 방식으로 구현한다). specs/desktop-related-project-handoff R7
|
|
16835
|
+
*/
|
|
16836
|
+
async approveProjectHandoff(handoffId, sourceSessionId) {
|
|
16837
|
+
const data = await this.request(
|
|
16838
|
+
`/api/handoffs/${encodeURIComponent(handoffId)}/approve`,
|
|
16839
|
+
{ method: "POST", boundary: "api", body: JSON.stringify({ sourceSessionId }) }
|
|
16840
|
+
);
|
|
16841
|
+
return data.handoff;
|
|
16842
|
+
}
|
|
16843
|
+
async rejectProjectHandoff(handoffId) {
|
|
16844
|
+
const data = await this.request(
|
|
16845
|
+
`/api/handoffs/${encodeURIComponent(handoffId)}/reject`,
|
|
16846
|
+
{ method: "POST", boundary: "api" }
|
|
16847
|
+
);
|
|
16848
|
+
return data.handoff;
|
|
16849
|
+
}
|
|
16850
|
+
/** 대상 프로젝트에 실제 세션을 만든 뒤 보고한다. 서버가 targetSessionId 기준으로
|
|
16851
|
+
* 멱등을 보장한다 — 이미 있으면 새 매핑을 만들지 않고 그대로 돌려준다. */
|
|
16852
|
+
async reportProjectHandoffStarted(handoffId, targetSessionId) {
|
|
16853
|
+
const data = await this.request(
|
|
16854
|
+
`/api/handoffs/${encodeURIComponent(handoffId)}/started`,
|
|
16855
|
+
{ method: "POST", boundary: "api", body: JSON.stringify({ targetSessionId }) }
|
|
16856
|
+
);
|
|
16857
|
+
return data.handoff;
|
|
16858
|
+
}
|
|
16859
|
+
/** 대상 프로젝트에 온라인 머신이 없는 등, 세션 생성 자체를 시작하지 못했을 때. */
|
|
16860
|
+
async reportProjectHandoffBlocked(handoffId, reason) {
|
|
16861
|
+
const data = await this.request(
|
|
16862
|
+
`/api/handoffs/${encodeURIComponent(handoffId)}/blocked`,
|
|
16863
|
+
{ method: "POST", boundary: "api", body: JSON.stringify({ reason }) }
|
|
16864
|
+
);
|
|
16865
|
+
return data.handoff;
|
|
16866
|
+
}
|
|
16867
|
+
/** 세션 생성 시도가 던졌을 때. */
|
|
16868
|
+
async reportProjectHandoffFailed(handoffId, reason) {
|
|
16869
|
+
const data = await this.request(
|
|
16870
|
+
`/api/handoffs/${encodeURIComponent(handoffId)}/failed`,
|
|
16871
|
+
{ method: "POST", boundary: "api", body: JSON.stringify({ reason }) }
|
|
16872
|
+
);
|
|
16873
|
+
return data.handoff;
|
|
16874
|
+
}
|
|
16875
|
+
/** 기본 꺼짐 — request_project_handoff MCP 도구 노출 토글(spec R9). */
|
|
16876
|
+
async setProjectHandoffMcpEnabled(projectId, enabled) {
|
|
16877
|
+
return this.request(`/api/projects/${encodeURIComponent(projectId)}/handoff-mcp-enabled`, {
|
|
16878
|
+
method: "PATCH",
|
|
16879
|
+
boundary: "api",
|
|
16880
|
+
body: JSON.stringify({ enabled })
|
|
16881
|
+
});
|
|
16882
|
+
}
|
|
16574
16883
|
async addProjectFavorite(projectId) {
|
|
16575
16884
|
return this.request(`/api/projects/${encodeURIComponent(projectId)}/favorite`, {
|
|
16576
16885
|
method: "PUT",
|
|
@@ -16586,6 +16895,7 @@ class ApiClient {
|
|
|
16586
16895
|
// --- 개인 Chat (specs/personal-chats) ---
|
|
16587
16896
|
async listPersonalChats(input = {}) {
|
|
16588
16897
|
const params = new URLSearchParams();
|
|
16898
|
+
params.set("context", input.companyId ? `company:${input.companyId}` : "personal");
|
|
16589
16899
|
if (input.limit !== void 0) params.set("limit", String(input.limit));
|
|
16590
16900
|
if (input.offset !== void 0) params.set("offset", String(input.offset));
|
|
16591
16901
|
if (input.query) params.set("query", input.query);
|
|
@@ -16601,12 +16911,22 @@ class ApiClient {
|
|
|
16601
16911
|
const data = await this.request("/api/personal-chats", {
|
|
16602
16912
|
method: "POST",
|
|
16603
16913
|
boundary: "api",
|
|
16604
|
-
body: JSON.stringify(
|
|
16605
|
-
|
|
16606
|
-
|
|
16914
|
+
body: JSON.stringify({
|
|
16915
|
+
machineId: input.machineId,
|
|
16916
|
+
...input.title === void 0 ? {} : { title: input.title },
|
|
16917
|
+
..."companyId" in input ? { companyId: input.companyId } : {}
|
|
16918
|
+
})
|
|
16607
16919
|
});
|
|
16608
16920
|
return data.chat;
|
|
16609
16921
|
}
|
|
16922
|
+
async getPersonalChatRuntimeConfig(chatId, machineId) {
|
|
16923
|
+
const params = new URLSearchParams({ machineId });
|
|
16924
|
+
const data = await this.request(
|
|
16925
|
+
`/api/personal-chats/${encodeURIComponent(chatId)}/runtime-config?${params.toString()}`,
|
|
16926
|
+
{ boundary: "api", headers: { "Cache-Control": "no-cache" } }
|
|
16927
|
+
);
|
|
16928
|
+
return data.environmentVariables ?? {};
|
|
16929
|
+
}
|
|
16610
16930
|
async getPersonalChat(chatId) {
|
|
16611
16931
|
const data = await this.request(
|
|
16612
16932
|
`/api/personal-chats/${encodeURIComponent(chatId)}`,
|
|
@@ -16932,19 +17252,23 @@ class ApiClient {
|
|
|
16932
17252
|
}
|
|
16933
17253
|
/**
|
|
16934
17254
|
* Mint the signed caller grant that proves *this* user is the connector
|
|
16935
|
-
* subject of a session spawned on a
|
|
16936
|
-
* userId/machineId/projectId/expiry and rejects a caller
|
|
16937
|
-
*
|
|
16938
|
-
* is required by the route —
|
|
16939
|
-
*
|
|
17255
|
+
* subject of a session spawned on a grant-capable machine. The server binds it to
|
|
17256
|
+
* userId/machineId/projectId-or-personalChatId/expiry and rejects a caller
|
|
17257
|
+
* who cannot access that runtime scope (web-ui server/mcpCallerGrantRoute.ts).
|
|
17258
|
+
* `projectId` is required by the route — Personal Chat grants send an explicit
|
|
17259
|
+
* null plus `personalChatId` so the signed scope matches the spawn.
|
|
16940
17260
|
*/
|
|
16941
|
-
async mintMcpCallerGrant(machineId, projectId) {
|
|
17261
|
+
async mintMcpCallerGrant(machineId, projectId, personalChatId) {
|
|
16942
17262
|
const data = await this.request(
|
|
16943
17263
|
"/api/me/mcp-caller-grant",
|
|
16944
17264
|
{
|
|
16945
17265
|
method: "POST",
|
|
16946
17266
|
boundary: "api",
|
|
16947
|
-
body: JSON.stringify({
|
|
17267
|
+
body: JSON.stringify({
|
|
17268
|
+
machineId,
|
|
17269
|
+
projectId,
|
|
17270
|
+
...personalChatId ? { personalChatId } : {}
|
|
17271
|
+
})
|
|
16948
17272
|
}
|
|
16949
17273
|
);
|
|
16950
17274
|
if (typeof data.grant !== "string" || !data.grant) throw new Error("mcp caller grant is missing");
|
|
@@ -17585,7 +17909,7 @@ class ApiClient {
|
|
|
17585
17909
|
{
|
|
17586
17910
|
method: "POST",
|
|
17587
17911
|
boundary: "api",
|
|
17588
|
-
body: buildWorktreeMutationBody(input)
|
|
17912
|
+
body: buildWorktreeMutationBody(input, { includeMachinePlatform: this.config.standalone })
|
|
17589
17913
|
}
|
|
17590
17914
|
);
|
|
17591
17915
|
return data.worktree;
|
|
@@ -20216,6 +20540,22 @@ class ChatInputController {
|
|
|
20216
20540
|
this.readline.off("close", this.handleClose);
|
|
20217
20541
|
}
|
|
20218
20542
|
}
|
|
20543
|
+
const MACOS_FIRMLINK_ALIASES = [
|
|
20544
|
+
["/tmp", "/private/tmp"],
|
|
20545
|
+
["/var", "/private/var"],
|
|
20546
|
+
["/etc", "/private/etc"]
|
|
20547
|
+
];
|
|
20548
|
+
function canonicalizeMacOsFirmlinkAlias(path) {
|
|
20549
|
+
for (const [logical, physical] of MACOS_FIRMLINK_ALIASES) {
|
|
20550
|
+
if (path === logical) return physical;
|
|
20551
|
+
if (path.startsWith(`${logical}/`)) return physical + path.slice(logical.length);
|
|
20552
|
+
}
|
|
20553
|
+
return path;
|
|
20554
|
+
}
|
|
20555
|
+
function canonicalizeRemotePathForComparison(value2, platform2) {
|
|
20556
|
+
const normalized = value2.trim().replace(/\/+$/, "");
|
|
20557
|
+
return platform2 === "darwin" ? canonicalizeMacOsFirmlinkAlias(normalized) : normalized;
|
|
20558
|
+
}
|
|
20219
20559
|
function worktreeTargetMismatch(record2, expected) {
|
|
20220
20560
|
if (record2.projectId !== expected.projectId) return "project";
|
|
20221
20561
|
if (record2.machineId !== expected.machineId) return "machine";
|
|
@@ -20273,32 +20613,15 @@ function resolveWorktreeName(input) {
|
|
|
20273
20613
|
}
|
|
20274
20614
|
const MANAGED_WORKTREE_SEGMENT = "/.aplus/worktrees/";
|
|
20275
20615
|
const MAX_MANAGED_WORKTREE_DEPTH = 2;
|
|
20276
|
-
function isWorktreePathWithinProjectRoot(projectRoot, worktreePath) {
|
|
20277
|
-
const root = projectRoot
|
|
20278
|
-
const path = worktreePath
|
|
20616
|
+
function isWorktreePathWithinProjectRoot(projectRoot, worktreePath, platform2) {
|
|
20617
|
+
const root = canonicalizeRemotePathForComparison(projectRoot, platform2);
|
|
20618
|
+
const path = canonicalizeRemotePathForComparison(worktreePath, platform2);
|
|
20279
20619
|
if (!root || !path) return false;
|
|
20280
20620
|
if (!path.startsWith(`${root}${MANAGED_WORKTREE_SEGMENT}`)) return false;
|
|
20281
20621
|
const segments = path.slice(root.length + MANAGED_WORKTREE_SEGMENT.length).split("/");
|
|
20282
20622
|
if (segments.length < 1 || segments.length > MAX_MANAGED_WORKTREE_DEPTH) return false;
|
|
20283
20623
|
return segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
20284
20624
|
}
|
|
20285
|
-
async function createWorktreeOnMachine(input) {
|
|
20286
|
-
const record2 = await input.client.createProjectWorktree(input.projectId, {
|
|
20287
|
-
name: input.name,
|
|
20288
|
-
baseRef: input.baseRef,
|
|
20289
|
-
machineId: input.machineId,
|
|
20290
|
-
...input.confirmRepoRoot ? { confirmRepoRoot: input.confirmRepoRoot } : {}
|
|
20291
|
-
});
|
|
20292
|
-
const targetMismatch = worktreeTargetMismatch(record2, {
|
|
20293
|
-
projectId: input.projectId,
|
|
20294
|
-
machineId: input.machineId
|
|
20295
|
-
});
|
|
20296
|
-
const allowedRoot = input.confirmRepoRoot ?? input.repoCwd;
|
|
20297
|
-
const pathMatches = isWorktreePathWithinProjectRoot(allowedRoot, record2.path);
|
|
20298
|
-
if (!targetMismatch && pathMatches) return record2;
|
|
20299
|
-
await input.client.deleteProjectWorktree(input.projectId, record2.id, true).catch(() => void 0);
|
|
20300
|
-
throw new Error(!pathMatches && input.pathMismatchMessage ? input.pathMismatchMessage(record2.path) : input.targetMismatchMessage);
|
|
20301
|
-
}
|
|
20302
20625
|
const WORKTREE_SUBDIR_CONFIRM_CODE = "worktree-subdir-confirm-required";
|
|
20303
20626
|
function trimmedString(value2) {
|
|
20304
20627
|
if (typeof value2 !== "string") return null;
|
|
@@ -20338,6 +20661,23 @@ function parseWorktreeSubdirConfirmRequest(status, body, projectRoot) {
|
|
|
20338
20661
|
if (!isSafeRelativeDir(repoRelativeDir)) return null;
|
|
20339
20662
|
return { repoRoot, workspaceDir, repoRelativeDir };
|
|
20340
20663
|
}
|
|
20664
|
+
async function createWorktreeOnMachine(input) {
|
|
20665
|
+
const record2 = await input.client.createProjectWorktree(input.projectId, {
|
|
20666
|
+
name: input.name,
|
|
20667
|
+
baseRef: input.baseRef,
|
|
20668
|
+
machineId: input.machineId,
|
|
20669
|
+
...input.confirmRepoRoot ? { confirmRepoRoot: input.confirmRepoRoot } : {}
|
|
20670
|
+
});
|
|
20671
|
+
const targetMismatch = worktreeTargetMismatch(record2, {
|
|
20672
|
+
projectId: input.projectId,
|
|
20673
|
+
machineId: input.machineId
|
|
20674
|
+
});
|
|
20675
|
+
const allowedRoot = input.confirmRepoRoot ?? input.repoCwd;
|
|
20676
|
+
const pathMatches = isWorktreePathWithinProjectRoot(allowedRoot, record2.path, input.machinePlatform);
|
|
20677
|
+
if (!targetMismatch && pathMatches) return record2;
|
|
20678
|
+
await input.client.deleteProjectWorktree(input.projectId, record2.id, true).catch(() => void 0);
|
|
20679
|
+
throw new Error(!pathMatches && input.pathMismatchMessage ? input.pathMismatchMessage(record2.path) : input.targetMismatchMessage);
|
|
20680
|
+
}
|
|
20341
20681
|
async function createCliProjectWorktree(input, deps) {
|
|
20342
20682
|
const createInput = {
|
|
20343
20683
|
client: {
|
|
@@ -23307,7 +23647,7 @@ ${text2}` : text2);
|
|
|
23307
23647
|
return sections.join("\n\n");
|
|
23308
23648
|
}
|
|
23309
23649
|
const HANDOFF_READ_MESSAGES = 400;
|
|
23310
|
-
async function handoffToSibling(runtime, scope, env, handoff, agent) {
|
|
23650
|
+
async function handoffToSibling(runtime, scope, env, handoff, agent, modelOptions = {}) {
|
|
23311
23651
|
const from = handoff.from ?? env.sessionId;
|
|
23312
23652
|
if (!from) {
|
|
23313
23653
|
return agentFail(
|
|
@@ -23346,6 +23686,7 @@ async function handoffToSibling(runtime, scope, env, handoff, agent) {
|
|
|
23346
23686
|
sessionId = (await runtime.spawnSession({
|
|
23347
23687
|
directory: env.root,
|
|
23348
23688
|
agent,
|
|
23689
|
+
...modelOptions,
|
|
23349
23690
|
initialPrompt: prompt,
|
|
23350
23691
|
environmentVariables: childAgentEnv(env, childId)
|
|
23351
23692
|
})).sessionId;
|
|
@@ -23451,7 +23792,7 @@ function resolveSubagentControlPermission(records, parentSessionId, childSession
|
|
|
23451
23792
|
return valid ? isDescendant(valid, parentSessionId, childSessionId, 2) : false;
|
|
23452
23793
|
}
|
|
23453
23794
|
function canControlSpawnedDescendant(records, parentSessionId, childSessionId) {
|
|
23454
|
-
const lineage = records.flatMap((record2) => record2.outcome === "spawned" && record2.parentSessionId && record2.childSessionId ? [{
|
|
23795
|
+
const lineage = records.flatMap((record2) => record2.outcome === "spawned" && record2.parentSessionId && record2.childSessionId && record2.parentSessionId !== record2.childSessionId && Number.isFinite(record2.ts) && record2.ts >= 0 ? [{
|
|
23455
23796
|
parentSessionId: record2.parentSessionId,
|
|
23456
23797
|
childSessionId: record2.childSessionId,
|
|
23457
23798
|
spawnedAt: record2.ts,
|
|
@@ -23492,24 +23833,27 @@ async function stopOwnedTarget(runtime, env, target) {
|
|
|
23492
23833
|
const refused = ownershipFailure(runtime, env, target);
|
|
23493
23834
|
if (refused) return refused;
|
|
23494
23835
|
if (!runtime.stopSession) return agentFail("stop_rejected", "stop transport is unavailable");
|
|
23495
|
-
let stream2 = null;
|
|
23496
|
-
try {
|
|
23497
|
-
stream2 = await runtime.openStream(target.session.id);
|
|
23498
|
-
if (stream2.current().thinking !== true) {
|
|
23499
|
-
return agentFail("stop_not_responding", `${target.session.id} has no active response`);
|
|
23500
|
-
}
|
|
23501
|
-
} catch (error) {
|
|
23502
|
-
return agentFail("stop_rejected", error instanceof Error ? error.message : String(error));
|
|
23503
|
-
} finally {
|
|
23504
|
-
stream2?.close();
|
|
23505
|
-
}
|
|
23506
23836
|
try {
|
|
23507
23837
|
const result = await runtime.stopSession(target.session.machineId, target.session.id);
|
|
23508
|
-
|
|
23838
|
+
if (result.stopped) return agentOk({ sessionId: target.session.id, stopped: true });
|
|
23839
|
+
return result.reason === "not-responding" ? agentFail("stop_not_responding", result.error) : agentFail("stop_rejected", result.error);
|
|
23509
23840
|
} catch (error) {
|
|
23510
23841
|
return agentFail("stop_rejected", error instanceof Error ? error.message : String(error));
|
|
23511
23842
|
}
|
|
23512
23843
|
}
|
|
23844
|
+
const CLI_CONFIGURABLE_AGENTS = [.../* @__PURE__ */ new Set([
|
|
23845
|
+
...newSessionAgents.map(({ id }) => id),
|
|
23846
|
+
"gemini"
|
|
23847
|
+
])];
|
|
23848
|
+
function cliSpawnModelOptions() {
|
|
23849
|
+
return CLI_CONFIGURABLE_AGENTS.map((agent) => ({
|
|
23850
|
+
agent,
|
|
23851
|
+
models: modelChoices(agent).map(({ key: id }) => ({
|
|
23852
|
+
id,
|
|
23853
|
+
efforts: effortChoices(agent, id).map(({ key }) => key)
|
|
23854
|
+
}))
|
|
23855
|
+
}));
|
|
23856
|
+
}
|
|
23513
23857
|
const DEFAULT_READ_LIMIT = 20;
|
|
23514
23858
|
const STATE_WINDOW_MESSAGES = 50;
|
|
23515
23859
|
function scopeOf(runtime) {
|
|
@@ -23563,7 +23907,7 @@ async function readSession(runtime, target, last) {
|
|
|
23563
23907
|
});
|
|
23564
23908
|
}
|
|
23565
23909
|
const DEFAULT_SPAWN_AGENT = "claude";
|
|
23566
|
-
async function spawnSibling(runtime, env, text2, agent, contextFiles = []) {
|
|
23910
|
+
async function spawnSibling(runtime, env, text2, agent, contextFiles = [], modelOptions = {}) {
|
|
23567
23911
|
if (env.depth >= AGENT_MAX_DEPTH) {
|
|
23568
23912
|
return agentFail("spawn_depth_exceeded", `depth ${env.depth} is already at the ceiling of ${AGENT_MAX_DEPTH}`);
|
|
23569
23913
|
}
|
|
@@ -23591,6 +23935,7 @@ async function spawnSibling(runtime, env, text2, agent, contextFiles = []) {
|
|
|
23591
23935
|
sessionId = (await runtime.spawnSession({
|
|
23592
23936
|
directory: env.root,
|
|
23593
23937
|
agent,
|
|
23938
|
+
...modelOptions,
|
|
23594
23939
|
initialPrompt: composeSpawnPrompt(text2, context.paths),
|
|
23595
23940
|
environmentVariables: childAgentEnv(env, childId)
|
|
23596
23941
|
})).sessionId;
|
|
@@ -23640,7 +23985,8 @@ async function runAgentCommand(runtime, args) {
|
|
|
23640
23985
|
maxSpawn: env.maxSpawn,
|
|
23641
23986
|
spawned,
|
|
23642
23987
|
spawnsRemaining: spawned === null ? null : Math.max(0, env.maxSpawn - spawned),
|
|
23643
|
-
canSpawn: env.depth < AGENT_MAX_DEPTH &&
|
|
23988
|
+
canSpawn: env.depth < AGENT_MAX_DEPTH && spawned !== null && spawned < env.maxSpawn,
|
|
23989
|
+
spawnModelOptions: cliSpawnModelOptions()
|
|
23644
23990
|
});
|
|
23645
23991
|
}
|
|
23646
23992
|
if (args.verb === "ls") {
|
|
@@ -23648,10 +23994,13 @@ async function runAgentCommand(runtime, args) {
|
|
|
23648
23994
|
return agentOk({ sessions: sessions.filter((session) => isInScope(session, scope)).map(sessionSummary) });
|
|
23649
23995
|
}
|
|
23650
23996
|
if (args.verb === "spawn") {
|
|
23997
|
+
const agent = args.agent ?? DEFAULT_SPAWN_AGENT;
|
|
23998
|
+
const modelOptions = args.model !== void 0 || args.effort !== void 0 ? await validateSpawnModelOptions(agent, args.model, args.effort) : {};
|
|
23999
|
+
if (isFailure(modelOptions)) return modelOptions;
|
|
23651
24000
|
if (args.handoff) {
|
|
23652
|
-
return handoffToSibling(runtime, scope, env, args.handoff,
|
|
24001
|
+
return handoffToSibling(runtime, scope, env, args.handoff, agent, modelOptions);
|
|
23653
24002
|
}
|
|
23654
|
-
return spawnSibling(runtime, env, args.text ?? "",
|
|
24003
|
+
return spawnSibling(runtime, env, args.text ?? "", agent, args.files ?? [], modelOptions);
|
|
23655
24004
|
}
|
|
23656
24005
|
if (!args.sessionId) return agentFail("session_not_found", "no session id given");
|
|
23657
24006
|
const target = await resolveTarget(runtime, scope, args.sessionId);
|
|
@@ -23676,6 +24025,17 @@ async function runAgentCommand(runtime, args) {
|
|
|
23676
24025
|
return agentFail("session_not_found", `unsupported verb "${args.verb}"`);
|
|
23677
24026
|
}
|
|
23678
24027
|
}
|
|
24028
|
+
async function validateSpawnModelOptions(agent, model, effort) {
|
|
24029
|
+
try {
|
|
24030
|
+
await resolveCliNewConversationOptions({ agent, model, effort });
|
|
24031
|
+
return {
|
|
24032
|
+
...model !== void 0 && model !== "default" ? { model } : {},
|
|
24033
|
+
...effort !== void 0 ? { effort } : {}
|
|
24034
|
+
};
|
|
24035
|
+
} catch (error) {
|
|
24036
|
+
return agentFail("invalid_spawn_options", error instanceof Error ? error.message : String(error));
|
|
24037
|
+
}
|
|
24038
|
+
}
|
|
23679
24039
|
function parseSpawnRecord(value2) {
|
|
23680
24040
|
if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return null;
|
|
23681
24041
|
const record2 = value2;
|
|
@@ -23770,25 +24130,33 @@ async function fileSessionUnderProject(context, env, input) {
|
|
|
23770
24130
|
if (!viaWorktree) return;
|
|
23771
24131
|
await context.rest.assignSessionToProject(viaWorktree.projectId, input.sessionId, viaWorktree.worktreeId);
|
|
23772
24132
|
}
|
|
23773
|
-
|
|
23774
|
-
|
|
23775
|
-
|
|
23776
|
-
|
|
23777
|
-
|
|
23778
|
-
|
|
23779
|
-
|
|
23780
|
-
|
|
23781
|
-
|
|
23782
|
-
|
|
23783
|
-
|
|
23784
|
-
|
|
23785
|
-
|
|
23786
|
-
|
|
23787
|
-
|
|
23788
|
-
|
|
24133
|
+
const ACTIVITY_WAIT_TIMEOUT_MS = 5e3;
|
|
24134
|
+
function waitForResponseActivity(transport, sessionId) {
|
|
24135
|
+
return new Promise((resolve) => {
|
|
24136
|
+
let unsubscribe = () => {
|
|
24137
|
+
};
|
|
24138
|
+
const finish = (thinking) => {
|
|
24139
|
+
clearTimeout(timer);
|
|
24140
|
+
unsubscribe();
|
|
24141
|
+
resolve(thinking);
|
|
24142
|
+
};
|
|
24143
|
+
const timer = setTimeout(() => finish(null), ACTIVITY_WAIT_TIMEOUT_MS);
|
|
24144
|
+
unsubscribe = transport.onEphemeral((event) => {
|
|
24145
|
+
if (!event || typeof event !== "object") return;
|
|
24146
|
+
const activity = event;
|
|
24147
|
+
if (activity.type !== "activity" || activity.id !== sessionId || typeof activity.thinking !== "boolean") return;
|
|
24148
|
+
finish(activity.thinking);
|
|
24149
|
+
});
|
|
24150
|
+
});
|
|
23789
24151
|
}
|
|
23790
24152
|
async function stopAgentSession(transport, machineId, sessionId) {
|
|
23791
24153
|
try {
|
|
24154
|
+
const thinking = await waitForResponseActivity(transport, sessionId);
|
|
24155
|
+
if (thinking !== true) return {
|
|
24156
|
+
stopped: false,
|
|
24157
|
+
reason: thinking === false ? "not-responding" : "rejected",
|
|
24158
|
+
error: thinking === false ? "Session has no active response." : "Could not confirm the session response state."
|
|
24159
|
+
};
|
|
23792
24160
|
const result = await transport.rpc(machineId, "stop-session", {
|
|
23793
24161
|
sessionId,
|
|
23794
24162
|
source: "saycode-agent-stop",
|
|
@@ -23796,17 +24164,37 @@ async function stopAgentSession(transport, machineId, sessionId) {
|
|
|
23796
24164
|
mode: "force"
|
|
23797
24165
|
});
|
|
23798
24166
|
if (result?.stopped === true) return { stopped: true };
|
|
23799
|
-
return {
|
|
24167
|
+
if (result?.stopped === false) return {
|
|
23800
24168
|
stopped: false,
|
|
23801
|
-
|
|
24169
|
+
reason: "not-responding",
|
|
24170
|
+
error: typeof result.reason === "string" ? result.reason : "Session has no active response."
|
|
23802
24171
|
};
|
|
24172
|
+
return { stopped: false, reason: "rejected", error: "Machine returned an invalid stop acknowledgement." };
|
|
23803
24173
|
} catch (error) {
|
|
23804
24174
|
return {
|
|
23805
24175
|
stopped: false,
|
|
24176
|
+
reason: "rejected",
|
|
23806
24177
|
error: error instanceof Error ? error.message : String(error)
|
|
23807
24178
|
};
|
|
23808
24179
|
}
|
|
23809
24180
|
}
|
|
24181
|
+
function rejectedSteer(error) {
|
|
24182
|
+
return {
|
|
24183
|
+
accepted: false,
|
|
24184
|
+
reason: /\bin[- ]flight\b/i.test(error) ? "in-flight" : "rejected",
|
|
24185
|
+
error
|
|
24186
|
+
};
|
|
24187
|
+
}
|
|
24188
|
+
async function steerAgentSession(transport, sessionId, text2) {
|
|
24189
|
+
try {
|
|
24190
|
+
const result = await transport.rpc(sessionId, "steer", { text: text2 });
|
|
24191
|
+
if (result?.success === true) return { accepted: true };
|
|
24192
|
+
const error = typeof result?.error === "string" ? result.error : "Session returned an invalid steer acknowledgement.";
|
|
24193
|
+
return rejectedSteer(error);
|
|
24194
|
+
} catch (error) {
|
|
24195
|
+
return rejectedSteer(error instanceof Error ? error.message : String(error));
|
|
24196
|
+
}
|
|
24197
|
+
}
|
|
23810
24198
|
const STREAM_HISTORY_LIMIT = 50;
|
|
23811
24199
|
const SPAWN_RPC_TIMEOUT_MS = 6e4;
|
|
23812
24200
|
function isReadableFileSync(path) {
|
|
@@ -23827,7 +24215,7 @@ function readFileOrNull(path) {
|
|
|
23827
24215
|
async function recentContents(context, transport, sessionId, limit) {
|
|
23828
24216
|
const messages = await context.rest.recentMessages(sessionId, limit);
|
|
23829
24217
|
const contents = [];
|
|
23830
|
-
for (const message of messages) {
|
|
24218
|
+
for (const message of [...messages].sort((left, right) => left.seq - right.seq)) {
|
|
23831
24219
|
const ciphertext = unwrapEncryptedField(message.content);
|
|
23832
24220
|
if (!ciphertext) continue;
|
|
23833
24221
|
const content = decodeContent(transport, ciphertext);
|
|
@@ -23918,6 +24306,8 @@ function createAgentRuntime(context, transport, env = process.env) {
|
|
|
23918
24306
|
{
|
|
23919
24307
|
directory: input.directory,
|
|
23920
24308
|
agent: input.agent,
|
|
24309
|
+
...input.model ? { model: input.model } : {},
|
|
24310
|
+
...input.effort ? { effort: input.effort } : {},
|
|
23921
24311
|
initialPrompt: input.initialPrompt,
|
|
23922
24312
|
environmentVariables: input.environmentVariables
|
|
23923
24313
|
},
|