@buzzni/saycode-cli 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/index.mjs +896 -124
  3. package/package.json +1 -1
package/index.mjs CHANGED
@@ -40,35 +40,36 @@ 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.1";
43
+ const version = "0.3.3";
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
  }
60
60
  const AGENT_VERBS = ["whoami", "ls", "read", "wait", "prompt", "spawn", ...AGENT_CONTROL_VERBS];
61
- function fail(message) {
61
+ function fail$1(message) {
62
62
  return { command: "error", message };
63
63
  }
64
64
  function parseAgentArgs(flags, positionals) {
65
65
  const [verb, ...rest] = positionals;
66
66
  if (!verb || !AGENT_VERBS.includes(verb)) {
67
- return fail(`agent needs one of: ${AGENT_VERBS.join(", ")}`);
67
+ return fail$1(`agent needs one of: ${AGENT_VERBS.join(", ")}`);
68
68
  }
69
69
  const timeout = flags.timeoutMs !== void 0 ? { timeoutMs: flags.timeoutMs } : {};
70
- const target = rest[0] ?? flags.sessionId;
71
- const control = parseAgentControlArgs(verb, target, rest);
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":
@@ -76,18 +77,18 @@ function parseAgentArgs(flags, positionals) {
76
77
  case "ls":
77
78
  return { command: "agent", verb: "ls" };
78
79
  case "read": {
79
- if (!target) return fail("agent read needs a session id (see `saycode agent ls`)");
80
+ if (!target) return fail$1("agent read needs a session id (see `saycode agent ls`)");
80
81
  return { command: "agent", verb: "read", sessionId: target, ...flags.last !== void 0 ? { last: flags.last } : {} };
81
82
  }
82
83
  case "wait": {
83
- if (!target) return fail("agent wait needs a session id (see `saycode agent ls`)");
84
- if (!flags.until) return fail("agent wait needs --until turn-end|waiting-input|completed");
84
+ if (!target) return fail$1("agent wait needs a session id (see `saycode agent ls`)");
85
+ if (!flags.until) return fail$1("agent wait needs --until turn-end|waiting-input|completed");
85
86
  return { command: "agent", verb: "wait", sessionId: target, until: flags.until, ...timeout };
86
87
  }
87
88
  case "prompt": {
88
- if (!target) return fail("agent prompt needs a session id (see `saycode agent ls`)");
89
- const text2 = rest.slice(1).join(" ").trim();
90
- if (!text2) return fail("agent prompt needs the text to send");
89
+ if (!target) return fail$1("agent prompt needs a session id (see `saycode agent ls`)");
90
+ const text2 = rest.slice(hasPositionalTarget ? 1 : 0).join(" ").trim();
91
+ if (!text2) return fail$1("agent prompt needs the text to send");
91
92
  return {
92
93
  command: "agent",
93
94
  verb: "prompt",
@@ -103,36 +104,40 @@ function parseAgentArgs(flags, positionals) {
103
104
  const files = flags.files ?? [];
104
105
  if (flags.handoff) {
105
106
  if (flags.prompt !== void 0) {
106
- return fail("agent spawn --handoff sends the conversation, so it cannot also take --prompt");
107
+ return fail$1("agent spawn --handoff sends the conversation, so it cannot also take --prompt");
107
108
  }
108
109
  if (files.length > 0) {
109
- return fail("agent spawn --handoff sends the conversation, so it cannot also take --file");
110
+ return fail$1("agent spawn --handoff sends the conversation, so it cannot also take --file");
110
111
  }
111
112
  const mode = flags.mode?.trim() || "summary";
112
113
  if (mode === "full") {
113
- return fail("agent spawn --handoff does not support --mode full yet; only summary is available");
114
+ return fail$1("agent spawn --handoff does not support --mode full yet; only summary is available");
114
115
  }
115
- if (mode !== "summary") return fail(`agent spawn --handoff got an unknown --mode: ${mode}`);
116
+ if (mode !== "summary") return fail$1(`agent spawn --handoff got an unknown --mode: ${mode}`);
116
117
  const from = flags.from?.trim();
117
118
  return {
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();
125
- if (!text2) return fail("agent spawn needs --prompt <text>");
128
+ if (!text2) return fail$1("agent spawn needs --prompt <text>");
126
129
  return {
127
130
  command: "agent",
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
  }
134
139
  default:
135
- return fail(`unsupported agent verb: ${verb}`);
140
+ return fail$1(`unsupported agent verb: ${verb}`);
136
141
  }
137
142
  }
138
143
  function fencedMarkdownRanges(content) {
@@ -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": "응답을 준비하는 중…",
@@ -810,12 +816,21 @@ const extensionPanelKo = {
810
816
  };
811
817
  const extensionInstallKo = {
812
818
  "extensions.installOfficial": "확장 설치",
819
+ "extensions.installArtifactPublisher": "공개 링크 확장 설치",
813
820
  "extensions.installDescription": "계속하려면 공식 확장을 설치하세요.",
814
821
  "extensions.machineExecutePermission": "이 확장은 미리 정의된 동작에만 machine.execute 권한을 요청합니다.",
815
822
  "extensions.notInstalled": "요청한 확장이 설치되지 않았습니다.",
816
823
  "extensions.disabled": "요청한 확장이 비활성화되어 있습니다.",
817
824
  "extensions.quarantined": "요청한 확장이 반복된 오류로 격리되었습니다.",
818
- "extensions.unavailable": "요청한 확장이 설치되어 있지만 현재 사용할 수 없습니다."
825
+ "extensions.unavailable": "요청한 확장이 설치되어 있지만 현재 사용할 수 없습니다.",
826
+ "extensions.permission.artifactPublishPublic": "선택한 결과물의 공개 링크를 만들 수 있습니다. 파일 내용과 링크 처리는 Saycode Core가 담당합니다.",
827
+ "artifactPublication.dialogTitle": "공개 링크",
828
+ "artifactPublication.copy": "링크 복사",
829
+ "artifactPublication.copied": "복사됨",
830
+ "artifactPublication.open": "링크 열기",
831
+ "artifactPublication.revoke": "공개 중지",
832
+ "artifactPublication.revoking": "중지 중…",
833
+ "artifactPublication.failed": "공개 링크 작업에 실패했습니다."
819
834
  };
820
835
  const personalChatsLandingKo = {
821
836
  "personalChats.heroTitle": "무엇을 도와드릴까요?",
@@ -1126,7 +1141,9 @@ const machineSettingsConsoleKo = {
1126
1141
  "machineConsole.updateTimeout": "업데이트가 시작됐지만 완료를 확인하지 못했습니다. 잠시 후 다시 진단해 주세요.",
1127
1142
  "machineConsole.updateFailed": "CLI 업데이트에 실패했습니다.",
1128
1143
  "machineConsole.aiCliStarted": "Codex와 Claude Code 설치를 시작했습니다.",
1129
- "machineConsole.aiCliFailed": "AI CLI 설치를 시작하지 못했습니다."
1144
+ "machineConsole.aiCliFailed": "AI CLI 설치를 시작하지 못했습니다.",
1145
+ "machineConsole.machineActionFailed": "머신 작업에 실패했습니다.",
1146
+ "machineConsole.extensionCommandsUnavailable": "Extension 명령을 사용할 수 없습니다."
1130
1147
  };
1131
1148
  const sessionTokenUsageKo = {
1132
1149
  "sessionTokenUsage.total": "누적 {count} 토큰",
@@ -1254,7 +1271,12 @@ const additionalDirectoriesKo = {
1254
1271
  "additionalDirectories.limitReached": "추가 디렉터리는 최대 8개까지 등록할 수 있습니다.",
1255
1272
  "additionalDirectories.machineMismatch": "현재 머신과 다른 위치에서 선택한 디렉터리입니다.",
1256
1273
  "additionalDirectories.saveFailed": "디렉터리를 등록하지 못했습니다.",
1257
- "additionalDirectories.folderLoadFailed": "디렉터리를 불러오지 못했습니다."
1274
+ "additionalDirectories.folderLoadFailed": "디렉터리를 불러오지 못했습니다.",
1275
+ "additionalDirectories.relatedMountMissing": "관련 프로젝트 디렉터리 {count}개를 자동 마운트에서 제외하고 세션을 시작했습니다.",
1276
+ "additionalDirectories.relatedMountLinkLookupFailed": "관련 프로젝트 목록을 불러오지 못해 자동 마운트 없이 세션을 시작했습니다.",
1277
+ "additionalDirectories.relatedMountLookupFailed": "관련 프로젝트 {count}개를 불러오지 못해 제외하고 세션을 시작했습니다.",
1278
+ "additionalDirectories.relatedMountRuntimeUnsupported": "이 머신의 Happy CLI가 관련 프로젝트 자동 마운트를 지원하지 않아 기본 세션으로 시작했습니다.",
1279
+ "additionalDirectories.relatedMountLimit": "관련 프로젝트 디렉터리 상한을 넘어 {count}개를 제외하고 세션을 시작했습니다."
1258
1280
  };
1259
1281
  const agentHandoffKo = {
1260
1282
  "agentHandoff.open": "다른 에이전트로 이어가기",
@@ -1323,6 +1345,18 @@ const subagentKo = {
1323
1345
  "subagent.error.inFlight": "이전 제어 요청을 처리하는 중입니다.",
1324
1346
  "subagent.error.rejected": "요청이 거부됐습니다: {detail}"
1325
1347
  };
1348
+ const reviewNavigationKo = {
1349
+ "review.action.openSourceSession": "원본 대화로 이동: {title}"
1350
+ };
1351
+ const chatFolderUploadKo = {
1352
+ "upload.kindFolder": "폴더",
1353
+ "upload.folderReference": "{path} ({count}개 파일)",
1354
+ "upload.folderChip": "{name} · {count}개 파일",
1355
+ "upload.folderEmpty": "폴더에 업로드할 파일이 없습니다: {name}",
1356
+ "upload.progress": "파일 업로드 중 {done}/{total}",
1357
+ "upload.partialIssue": "업로드 실패 {failed}개 · 건너뜀 {skipped}개",
1358
+ "upload.invalidPath": "안전하지 않은 첨부 경로입니다."
1359
+ };
1326
1360
  const aiCommitPrKo = {
1327
1361
  "aiCommitPr.stagedCount": "스테이지된 파일 {count}개",
1328
1362
  "aiCommitPr.noStaged": "스테이지된 변경이 없습니다.",
@@ -1363,7 +1397,46 @@ const aiCommitPrKo = {
1363
1397
  "aiCommitPr.pr.create": "Pull Request 만들기",
1364
1398
  "aiCommitPr.pr.creating": "만드는 중…"
1365
1399
  };
1366
- 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 };
1400
+ const projectHandoffKo = {
1401
+ "projectHandoff.loading": "핸드오프를 확인하는 중…",
1402
+ "projectHandoff.notFound": "핸드오프를 찾을 수 없습니다.",
1403
+ "projectHandoff.autoApproving": "자동 승인 중…",
1404
+ "projectHandoff.autoApproveFailed": "자동 승인에 실패했습니다.",
1405
+ "projectHandoff.status.queued": "대기 중",
1406
+ "projectHandoff.status.running": "진행 중",
1407
+ "projectHandoff.status.completed": "완료됨",
1408
+ "projectHandoff.status.failed": "실패함",
1409
+ "projectHandoff.status.cancelled": "거절됨",
1410
+ "projectHandoff.status.blocked": "막힘",
1411
+ "projectHandoff.indicatorLabel": "넘긴 작업 {count}건 진행 중"
1412
+ };
1413
+ const projectLinksKo = {
1414
+ "projectLinks.empty": "등록된 관련 프로젝트가 없습니다.",
1415
+ "projectLinks.loading": "불러오는 중…",
1416
+ "projectLinks.loadFailed": "관련 프로젝트를 불러오지 못했습니다.",
1417
+ "projectLinks.unknownProject": "알 수 없는 프로젝트",
1418
+ "projectLinks.removeAria": "{name} 관련 등록 해제",
1419
+ "projectLinks.removeFailed": "관련 등록을 해제하지 못했습니다.",
1420
+ "projectLinks.selectProject": "프로젝트 선택",
1421
+ "projectLinks.selectProjectAria": "관련 프로젝트로 등록할 프로젝트",
1422
+ "projectLinks.rolePlaceholder": "역할 (예: 웹, 백엔드, 앱)",
1423
+ "projectLinks.add": "등록",
1424
+ "projectLinks.adding": "등록 중…",
1425
+ "projectLinks.addFailed": "관련 프로젝트를 등록하지 못했습니다.",
1426
+ "projectLinks.noCandidates": "등록할 수 있는 다른 프로젝트가 없습니다.",
1427
+ "projectLinks.unavailableInDemo": "데모 모드에서는 관련 프로젝트를 관리할 수 없습니다.",
1428
+ "projectLinks.mcpToggleLabel": "AI가 이 프로젝트에서 작업을 넘길 수 있게 허용",
1429
+ "projectLinks.mcpToggleHint": "켜면 AI가 관련 프로젝트로 작업을 제안했을 때 확인 없이 바로 진행됩니다.",
1430
+ "projectLinks.mcpToggleFailed": "설정을 바꾸지 못했습니다."
1431
+ };
1432
+ const connectorReadinessKo = {
1433
+ "connectorReadiness.badge": "커넥터 {count}건 확인 필요",
1434
+ "connectorReadiness.state.needsAuth": "다시 연결 필요",
1435
+ "connectorReadiness.state.runtimeFailed": "연결 실패",
1436
+ "connectorReadiness.state.configMissing": "설정 없음",
1437
+ "connectorReadiness.state.unknown": "상태 미확인"
1438
+ };
1439
+ 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, ...chatFolderUploadKo, ...connectorReadinessKo };
1367
1440
  const onboardingKo = {
1368
1441
  "onboarding.dialogAria": "시작 설정",
1369
1442
  "onboarding.eyebrow": "처음 시작하기",
@@ -1459,6 +1532,44 @@ const ko = {
1459
1532
  "common.reset": "초기화",
1460
1533
  "common.cancel": "취소",
1461
1534
  "common.close": "닫기",
1535
+ "networkDataSaver.transferConfirm": "{operation} 작업은 데이터를 많이 사용할 수 있습니다. 예상 크기: {size}. 계속할까요?",
1536
+ "networkDataSaver.sizeUnknown": "알 수 없음",
1537
+ "networkDataSaver.operation.update-download": "업데이트 다운로드",
1538
+ "networkDataSaver.operation.file-upload": "파일 업로드",
1539
+ "networkDataSaver.operation.folder-upload": "폴더 업로드",
1540
+ "networkDataSaver.operation.file-download": "파일 다운로드",
1541
+ "networkDataSaver.operation.project-deploy": "프로젝트 배포",
1542
+ "networkDataSaver.operation.project-import": "프로젝트 가져오기",
1543
+ "networkDataSaver.title": "네트워크 데이터 절약",
1544
+ "networkDataSaver.description": "macOS가 비용이 들거나 저데이터 모드인 경로로 판단하면 백그라운드 데이터 사용을 자동으로 줄입니다.",
1545
+ "networkDataSaver.loading": "네트워크 상태를 확인하는 중…",
1546
+ "networkDataSaver.unsupported": "이 앱 버전에서는 네트워크 비용 상태를 확인할 수 없습니다.",
1547
+ "networkDataSaver.status.active": "데이터 절약 중",
1548
+ "networkDataSaver.status.inactive": "데이터 절약 꺼짐",
1549
+ "networkDataSaver.modeLabel": "절약 방식",
1550
+ "networkDataSaver.mode.automatic": "자동",
1551
+ "networkDataSaver.mode.always-save": "항상 절약",
1552
+ "networkDataSaver.mode.never-save": "절약 안 함",
1553
+ "networkDataSaver.interfaceLabel": "현재 연결",
1554
+ "networkDataSaver.interface.wifi": "Wi-Fi",
1555
+ "networkDataSaver.interface.wired-ethernet": "유선 이더넷",
1556
+ "networkDataSaver.interface.cellular": "셀룰러",
1557
+ "networkDataSaver.interface.loopback": "로컬 연결",
1558
+ "networkDataSaver.interface.other": "기타 연결",
1559
+ "networkDataSaver.interface.unknown": "알 수 없음",
1560
+ "networkDataSaver.reasonLabel": "판단 이유",
1561
+ "networkDataSaver.reason.expensive": "비용이 드는 네트워크로 감지됨",
1562
+ "networkDataSaver.reason.constrained": "저데이터 모드로 감지됨",
1563
+ "networkDataSaver.reason.offline": "오프라인",
1564
+ "networkDataSaver.reason.unrestricted": "일반 네트워크로 감지됨",
1565
+ "networkDataSaver.reason.unknown": "네트워크 비용 상태를 알 수 없음",
1566
+ "networkDataSaver.reason.user-always": "사용자가 항상 절약을 선택함",
1567
+ "networkDataSaver.reason.user-never": "사용자가 절약 안 함을 선택함",
1568
+ "networkDataSaver.deferred.title": "절약 중 지연되는 작업",
1569
+ "networkDataSaver.activeHelp": "수동 새로고침은 계속 사용할 수 있고, 대용량 작업은 확인 후 한 번 허용할 수 있습니다.",
1570
+ "networkDataSaver.deferred.model": "난이도 모델 최초 다운로드",
1571
+ "networkDataSaver.deferred.memory": "Memory Layer 대화 백필",
1572
+ "networkDataSaver.deferred.gitPr": "백그라운드 Git 및 PR 새로고침",
1462
1573
  "happyCliUpdate.title": "Happy CLI 업데이트가 있습니다",
1463
1574
  "happyCliUpdate.checkTitle": "Happy CLI 업데이트 확인",
1464
1575
  "happyCliUpdate.managerTitle": "Happy CLI 업데이트 관리",
@@ -2306,6 +2417,7 @@ const ko = {
2306
2417
  "projectSettings.tabRepository": "코드 저장소",
2307
2418
  "projectSettings.tabDeploy": "배포 저장소",
2308
2419
  "projectSettings.tabCollaboration": "협업",
2420
+ "projectSettings.tabLinks": "관련 프로젝트",
2309
2421
  "projectSettings.tabMachine": "머신 및 경로",
2310
2422
  "projectSettings.tabRun": "실행",
2311
2423
  "projectSettings.tabMcp": "MCP 서버/스킬",
@@ -2935,6 +3047,7 @@ const ko = {
2935
3047
  "worktreeOp.checkingStatus": "worktree 상태 확인 중...",
2936
3048
  "worktreeOp.forceDeleteUnpushed": "worktree에 푸시되지 않은 커밋 {count}개가 있습니다. 강제로 삭제할까요? 커밋이 사라집니다.",
2937
3049
  "worktreeOp.forceDeleteDirty": "worktree에 정리되지 않은 변경이 있습니다. 강제로 삭제할까요? 변경 사항이 사라집니다.",
3050
+ "worktreeOp.trackedChangesBlocked": "이 worktree에 커밋되지 않은 tracked 변경이 있습니다. 변경을 커밋하거나 stash하거나 명시적으로 버린 뒤 다시 시도하세요.",
2938
3051
  "worktreeOp.removeCancelled": "정리되지 않은 변경/커밋이 있어 삭제를 취소했습니다.",
2939
3052
  "worktreeOp.removed": "worktree를 삭제했습니다.",
2940
3053
  "project.sharedBy": " · 공유: {owner}",
@@ -3300,6 +3413,7 @@ const ko = {
3300
3413
  "saycodePromptPreference.eyebrow": "에이전트 지시 선택",
3301
3414
  "saycodePromptPreference.title": "Saycode 기본 지시를 사용할까요?",
3302
3415
  "saycodePromptPreference.body": "내가 만든 Claude/Codex 스킬과 하네스를 그대로 쓰려면 꺼진 상태를 유지하세요. 나중에 환경설정에서 언제든 바꿀 수 있습니다.",
3416
+ "saycodePromptPreference.defaultOnNotice": "자식 Agent 호출, 내부 작업 위임, 결과물 인라인 미리보기는 기본으로 계속 켜집니다. 이 기능도 끄려면 환경설정 → 시스템 프롬프트에서 각각 해제하세요.",
3303
3417
  "saycodePromptPreference.keepOff": "꺼진 상태 유지",
3304
3418
  "saycodePromptPreference.enable": "Saycode 지시 켜기",
3305
3419
  "saycodePromptPreference.saving": "저장 중…",
@@ -3308,9 +3422,11 @@ const ko = {
3308
3422
  "saycodePromptPreference.settingsEnable": "Saycode가 제공하는 기본 행동 지시 사용",
3309
3423
  "saycodePromptPreference.settingsHelp": "사용자 시스템 프롬프트, 프로젝트 지침, 설치한 스킬과 하네스는 이 설정과 관계없이 유지됩니다.",
3310
3424
  "saycodePromptPreference.blocksTitle": "개별 지시 선택",
3311
- "saycodePromptPreference.blocksHelp": "Saycode 지시를 항목별로 켜고 끕니다. 선택하지 않은 항목은 위의 전체 설정을 따릅니다.",
3425
+ "saycodePromptPreference.blocksHelp": "자식 Agent 호출, 내부 작업 위임, 결과물 인라인 미리보기는 기본으로 켜지며, 나머지 항목은 위의 전체 설정을 따릅니다.",
3312
3426
  "saycodePromptPreference.alwaysOnNotice": "채팅 제목 생성은 항상 켜져 있으며 끌 수 없습니다. 채팅 목록에서 대화를 다시 찾으려면 필요합니다.",
3313
- "saycodePromptPreference.block.workerDelegation.label": "작업 위임",
3427
+ "saycodePromptPreference.block.agentOrchestration.label": "자식 Agent 호출",
3428
+ "saycodePromptPreference.block.agentOrchestration.description": "자연어로 요청한 작업을 독립된 자식 Agent 세션에 맡기고 결과를 회수하도록 지시합니다.",
3429
+ "saycodePromptPreference.block.workerDelegation.label": "내부 작업 위임",
3314
3430
  "saycodePromptPreference.block.workerDelegation.description": "기계적인 작업을 저렴한 worker 에이전트에 넘기도록 지시합니다.",
3315
3431
  "saycodePromptPreference.block.axBase.label": "기획부터 시작 워크플로우",
3316
3432
  "saycodePromptPreference.block.axBase.description": "AX 워크플로우의 기본 지시를 사용합니다.",
@@ -3351,6 +3467,7 @@ const ko = {
3351
3467
  "file.readFailed": "파일을 읽지 못했습니다.",
3352
3468
  "session.loadMessagesFailed": "대화 메시지를 불러올 수 없습니다.",
3353
3469
  "session.sendToSessionFailed": "대화에 메시지를 보낼 수 없습니다.",
3470
+ "session.sendBlockedAccountKeyLocked": "이 대화는 계정 키로 잠겨 있습니다. 잠금을 해제한 뒤 다시 시도하세요.",
3354
3471
  "message.noEncryptKey": "메시지 삭제 이벤트를 암호화할 세션 키를 찾을 수 없습니다.",
3355
3472
  "machineSel.boundMissing": "프로젝트에 연결된 머신 정보를 찾을 수 없습니다. 새로고침 후 다시 시도해주세요.",
3356
3473
  "machineSel.connectedMachine": "연결된 머신",
@@ -3738,6 +3855,7 @@ const ko = {
3738
3855
  "settingsTab.language": "언어",
3739
3856
  "settingsTab.machines": "머신",
3740
3857
  "settingsTab.mobile": "모바일 연결",
3858
+ "settingsTab.network": "네트워크 데이터 절약",
3741
3859
  "settingsTab.env": "환경 변수",
3742
3860
  "settingsTab.security": "보안",
3743
3861
  "settingsTab.memory": "Memory",
@@ -3746,12 +3864,15 @@ const ko = {
3746
3864
  "settingsTab.extensions": "확장",
3747
3865
  "settingsTab.about": "정보",
3748
3866
  "settingsTab.shortcuts": "단축키",
3749
- "extensions.help": "필요한 기능만 로컬 확장 패키지로 설치하고 관리합니다.",
3867
+ "extensions.help": "필요한 기능만 공식 또는 로컬 확장 패키지로 설치하고 관리합니다.",
3750
3868
  "extensions.installLocal": "로컬 파일 설치",
3751
3869
  "extensions.loading": "설치된 확장을 불러오는 중입니다.",
3752
3870
  "extensions.empty": "설치된 확장이 없습니다.",
3753
3871
  "extensions.permissions": "권한",
3754
3872
  "extensions.noPermissions": "요청 권한 없음",
3873
+ "extensions.permission.browserViewerOpen": "접근 가능한 온라인 머신에서 원격 Chrome 화면을 준비해 기본 브라우저로 엽니다. 소프트웨어 설치는 포함하지 않습니다.",
3874
+ "extensions.permission.browserViewerInstall": "선택한 머신에 원격 화면용 시스템 패키지(Xvfb/noVNC)를 설치합니다. Chrome 설치는 포함하지 않습니다.",
3875
+ "extensions.permission.browserViewerInstallChrome": "선택한 머신에 Google Chrome을 설치합니다. 머신의 시스템 소프트웨어가 변경됩니다.",
3755
3876
  "extensions.approvePermission": "'{permission}' 승인",
3756
3877
  "extensions.revokePermission": "'{permission}' 철회",
3757
3878
  "extensions.enable": "활성화",
@@ -4450,7 +4571,7 @@ Options
4450
4571
  -p, --project <id|name> Project id prefix or name
4451
4572
  -d, --directory <path> Working directory on that machine
4452
4573
  -s, --session <id> Session id (see \`saycode ls\`)
4453
- --agent <name> claude (default), codex, gemini, openclaw, opencode
4574
+ --agent <name> claude (default), codex, gemini, grok, openclaw, opencode
4454
4575
  --model <name> Model for new-session messages (Default if omitted)
4455
4576
  --effort <level> Model effort (validated against agent + model)
4456
4577
  --worktree Create an isolated worktree for a project chat
@@ -4473,7 +4594,8 @@ Agent commands (machine-readable JSON on stdout, exit 3 on failure)
4473
4594
  agent prompt <session> <text> [--wait] Send a prompt, optionally wait for it
4474
4595
  agent steer <session> <text> Redirect an owned child's active response
4475
4596
  agent stop <session> Stop an owned child's active response
4476
- agent spawn --prompt <text> [--agent n] Start a sibling in the same directory
4597
+ agent spawn --prompt <text> [--agent n] [--model n] [--effort n]
4598
+ Start a sibling with an explicit execution choice
4477
4599
  agent spawn --prompt <text> --file <path> ...with context files it reads first
4478
4600
  Only works inside a session Saycode started (SAYCODE_AGENT_ENV).
4479
4601
 
@@ -6857,6 +6979,147 @@ function decryptJsonWithCandidates(blob, keys) {
6857
6979
  decryptCacheSet(blob, keys, null);
6858
6980
  return null;
6859
6981
  }
6982
+ function bounded(value2, maxLength) {
6983
+ if (typeof value2 !== "string" || value2.length === 0 || value2.length > maxLength) return void 0;
6984
+ return value2;
6985
+ }
6986
+ function parseCloudLoginResult(raw) {
6987
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
6988
+ throw new Error("CLOUD_LOGIN_RESPONSE_INVALID");
6989
+ }
6990
+ const value2 = raw;
6991
+ const required = (field, maxLength) => {
6992
+ const entry = bounded(value2[field], maxLength);
6993
+ if (entry === void 0) throw new Error("CLOUD_LOGIN_RESPONSE_INVALID");
6994
+ return entry;
6995
+ };
6996
+ const accountPublicKey = bounded(value2.accountPublicKey, 128);
6997
+ const accountKeySalt = bounded(value2.accountKeySalt, 128);
6998
+ return {
6999
+ userId: required("userId", 256),
7000
+ username: required("username", 256),
7001
+ token: required("token", 8192),
7002
+ secret: required("secret", 8192),
7003
+ ...accountPublicKey ? { accountPublicKey } : {},
7004
+ ...accountKeySalt ? { accountKeySalt } : {}
7005
+ };
7006
+ }
7007
+ const ENVELOPE_VERSION = 0;
7008
+ const EPHEMERAL_PUBLIC_KEY_LENGTH = 32;
7009
+ const ENVELOPE_NONCE_LENGTH = 24;
7010
+ const ENVELOPE_HEADER_LENGTH = 1 + EPHEMERAL_PUBLIC_KEY_LENGTH + ENVELOPE_NONCE_LENGTH;
7011
+ const GCM_VERSION = 0;
7012
+ const GCM_NONCE_LENGTH = 12;
7013
+ const GCM_TAG_LENGTH = 16;
7014
+ function unwrapDataEncryptionKey(envelopeBase64, recipientPrivateKey) {
7015
+ let envelope;
7016
+ try {
7017
+ envelope = decodeBase64(envelopeBase64);
7018
+ } catch {
7019
+ return null;
7020
+ }
7021
+ if (envelope.length <= ENVELOPE_HEADER_LENGTH) return null;
7022
+ if (envelope[0] !== ENVELOPE_VERSION) return null;
7023
+ const ephemeralPublicKey = envelope.subarray(1, 1 + EPHEMERAL_PUBLIC_KEY_LENGTH);
7024
+ const nonce = envelope.subarray(1 + EPHEMERAL_PUBLIC_KEY_LENGTH, ENVELOPE_HEADER_LENGTH);
7025
+ const ciphertext = envelope.subarray(ENVELOPE_HEADER_LENGTH);
7026
+ const opened = nacl.box.open(ciphertext, nonce, ephemeralPublicKey, recipientPrivateKey);
7027
+ return opened ? new Uint8Array(opened) : null;
7028
+ }
7029
+ async function importGcmKey(dataEncryptionKey) {
7030
+ return crypto.subtle.importKey("raw", new Uint8Array(dataEncryptionKey), "AES-GCM", false, ["encrypt", "decrypt"]);
7031
+ }
7032
+ async function decryptDataKeyToString(dataEncryptionKey, bundleBase64) {
7033
+ let bundle;
7034
+ try {
7035
+ bundle = decodeBase64(bundleBase64);
7036
+ } catch {
7037
+ return null;
7038
+ }
7039
+ if (bundle.length < 1 + GCM_NONCE_LENGTH + GCM_TAG_LENGTH) return null;
7040
+ if (bundle[0] !== GCM_VERSION) return null;
7041
+ const nonce = bundle.subarray(1, 1 + GCM_NONCE_LENGTH);
7042
+ const ciphertextWithTag = bundle.subarray(1 + GCM_NONCE_LENGTH);
7043
+ try {
7044
+ const key = await importGcmKey(dataEncryptionKey);
7045
+ const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv: new Uint8Array(nonce) }, key, new Uint8Array(ciphertextWithTag));
7046
+ return new TextDecoder().decode(plaintext);
7047
+ } catch {
7048
+ return null;
7049
+ }
7050
+ }
7051
+ async function encryptDataKeyContent(dataEncryptionKey, value2) {
7052
+ const nonce = nacl.randomBytes(GCM_NONCE_LENGTH);
7053
+ const key = await importGcmKey(dataEncryptionKey);
7054
+ const plaintext = new TextEncoder().encode(JSON.stringify(value2));
7055
+ const ciphertextWithTag = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(nonce) }, key, plaintext));
7056
+ const bundle = new Uint8Array(1 + GCM_NONCE_LENGTH + ciphertextWithTag.length);
7057
+ bundle[0] = GCM_VERSION;
7058
+ bundle.set(nonce, 1);
7059
+ bundle.set(ciphertextWithTag, 1 + GCM_NONCE_LENGTH);
7060
+ return encodeBase64(bundle);
7061
+ }
7062
+ const dekBySessionId = /* @__PURE__ */ new Map();
7063
+ const lockedSessionIds = /* @__PURE__ */ new Set();
7064
+ const fingerprints = /* @__PURE__ */ new Map();
7065
+ function registerSessionDek(session, accountPrivateKey) {
7066
+ const envelope = session.dataEncryptionKey ?? null;
7067
+ if (!envelope) {
7068
+ dekBySessionId.delete(session.id);
7069
+ lockedSessionIds.delete(session.id);
7070
+ fingerprints.delete(session.id);
7071
+ return;
7072
+ }
7073
+ const fingerprint = `${envelope}|${accountPrivateKey ? "1" : "0"}`;
7074
+ if (fingerprints.get(session.id) === fingerprint) return;
7075
+ fingerprints.set(session.id, fingerprint);
7076
+ const dek = accountPrivateKey ? unwrapDataEncryptionKey(envelope, accountPrivateKey) : null;
7077
+ if (dek) {
7078
+ dekBySessionId.set(session.id, dek);
7079
+ lockedSessionIds.delete(session.id);
7080
+ } else {
7081
+ dekBySessionId.delete(session.id);
7082
+ lockedSessionIds.add(session.id);
7083
+ }
7084
+ }
7085
+ function getSessionDek(sessionId) {
7086
+ return dekBySessionId.get(sessionId) ?? null;
7087
+ }
7088
+ const MAX_ENTRIES = 3e3;
7089
+ const headerCache = /* @__PURE__ */ new Map();
7090
+ function cacheGet(store, sessionId, ciphertext) {
7091
+ return store.get(sessionId)?.get(ciphertext) ?? null;
7092
+ }
7093
+ function cacheSet(store, sessionId, ciphertext, plaintext) {
7094
+ let perSession = store.get(sessionId);
7095
+ if (!perSession) {
7096
+ perSession = /* @__PURE__ */ new Map();
7097
+ store.set(sessionId, perSession);
7098
+ }
7099
+ perSession.set(ciphertext, plaintext);
7100
+ if (perSession.size > MAX_ENTRIES) {
7101
+ const oldest = perSession.keys().next().value;
7102
+ if (oldest !== void 0) perSession.delete(oldest);
7103
+ }
7104
+ }
7105
+ function getCachedDataKeyMetadataJson(sessionId, ciphertext) {
7106
+ return cacheGet(headerCache, sessionId, ciphertext);
7107
+ }
7108
+ async function preDecryptSessionHeaders(sessions, accountPrivateKey) {
7109
+ await Promise.all(sessions.map(async (session) => {
7110
+ registerSessionDek(session, accountPrivateKey);
7111
+ const dek = getSessionDek(session.id);
7112
+ if (!dek) return;
7113
+ if (session.metadata && getCachedDataKeyMetadataJson(session.id, session.metadata) === null) {
7114
+ const json = await decryptDataKeyToString(dek, session.metadata);
7115
+ if (json !== null) cacheSet(headerCache, session.id, session.metadata, json);
7116
+ }
7117
+ if (session.agentState && getCachedDataKeyMetadataJson(session.id, session.agentState) === null) {
7118
+ const json = await decryptDataKeyToString(dek, session.agentState);
7119
+ if (json !== null) cacheSet(headerCache, session.id, session.agentState, json);
7120
+ }
7121
+ }));
7122
+ }
6860
7123
  function buildUserTextMessageContent(input) {
6861
7124
  return {
6862
7125
  role: "user",
@@ -6872,6 +7135,58 @@ function buildUserTextMessageContent(input) {
6872
7135
  }
6873
7136
  };
6874
7137
  }
7138
+ function sanitizedString(value2) {
7139
+ return typeof value2 === "string" && value2.trim() ? value2.trim() : null;
7140
+ }
7141
+ function sanitizedCompanyId(value2) {
7142
+ if (typeof value2 === "string") return value2.trim() ? value2.trim() : null;
7143
+ if (value2 === null) return null;
7144
+ return void 0;
7145
+ }
7146
+ function buildStoredNotice(sessionId, value2, receivedAt) {
7147
+ const companyId = sanitizedCompanyId(value2.companyId);
7148
+ return {
7149
+ sessionId,
7150
+ projectId: sanitizedString(value2.projectId),
7151
+ ...sanitizedString(value2.personalChatId) ? { personalChatId: sanitizedString(value2.personalChatId) } : {},
7152
+ projectName: sanitizedString(value2.projectName),
7153
+ sessionTitle: sanitizedString(value2.sessionTitle),
7154
+ status: value2.status === "completed" || value2.status === "failed" || value2.status === "cancelled" ? value2.status : "completed",
7155
+ receivedAt,
7156
+ ...companyId !== void 0 ? { companyId } : {},
7157
+ // Only non-chat kinds are stored; absence means chat so legacy snapshots round-trip.
7158
+ ...value2.kind === "deploy" || value2.kind === "automation" || value2.kind === "platform_notice" ? { kind: value2.kind } : {},
7159
+ // title/body are platform_notice-only (server-authoritative admin message). Other
7160
+ // kinds never legitimately carry them, so they are dropped rather than stored blank.
7161
+ ...value2.kind === "platform_notice" ? { title: sanitizedString(value2.title), body: sanitizedString(value2.body) } : {}
7162
+ };
7163
+ }
7164
+ function parseServerCompletionNotifications(payload) {
7165
+ if (!payload || typeof payload !== "object") return [];
7166
+ const { notifications } = payload;
7167
+ if (!Array.isArray(notifications)) return [];
7168
+ const result = [];
7169
+ for (const raw of notifications) {
7170
+ if (!raw || typeof raw !== "object") continue;
7171
+ const value2 = raw;
7172
+ if (value2.kind !== "platform_notice") continue;
7173
+ const sessionId = sanitizedString(value2.sessionId);
7174
+ if (!sessionId) continue;
7175
+ result.push({
7176
+ ...buildStoredNotice(
7177
+ sessionId,
7178
+ value2,
7179
+ typeof value2.receivedAt === "number" && Number.isFinite(value2.receivedAt) ? value2.receivedAt : 0
7180
+ ),
7181
+ // 서버 목록은 읽은 행도 함께 돌려주고(`listCompletionNotifications` 는 unread 로
7182
+ // 필터하지 않는다) 읽음 여부를 `unread` 에 담는다. `buildStoredNotice` 는 이 필드를
7183
+ // 모르므로 여기서 직접 얹지 않으면, 조회할 때마다 `recordCompletionNotice` 의
7184
+ // 기본값(미확인)으로 되살아난다.
7185
+ ...typeof value2.unread === "boolean" ? { unread: value2.unread } : {}
7186
+ });
7187
+ }
7188
+ return result;
7189
+ }
6875
7190
  const CONNECTOR_PROVIDER_IDS = [
6876
7191
  "notion",
6877
7192
  "slack",
@@ -6923,7 +7238,7 @@ function personalMachinesPath(companyId) {
6923
7238
  function myMachinesPath(companyId) {
6924
7239
  return `/api/admin/companies/${encodeURIComponent(companyId)}/my-machines`;
6925
7240
  }
6926
- function record$1(value2) {
7241
+ function record$2(value2) {
6927
7242
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : {};
6928
7243
  }
6929
7244
  function array(value2) {
@@ -6942,7 +7257,7 @@ function provider(value2) {
6942
7257
  return PROVIDERS.includes(str(value2)) ? str(value2) : null;
6943
7258
  }
6944
7259
  function parseBundle(value2) {
6945
- const row2 = record$1(value2);
7260
+ const row2 = record$2(value2);
6946
7261
  const id = str(row2.id);
6947
7262
  const bundleProvider = provider(row2.provider);
6948
7263
  if (!id || !bundleProvider) return null;
@@ -6961,7 +7276,7 @@ function parseBundle(value2) {
6961
7276
  };
6962
7277
  }
6963
7278
  function parseDeployment(value2) {
6964
- const row2 = record$1(value2);
7279
+ const row2 = record$2(value2);
6965
7280
  const machineId = str(row2.machineId);
6966
7281
  const bundleId = str(row2.bundleId);
6967
7282
  if (!machineId || !bundleId) return null;
@@ -6981,10 +7296,10 @@ function parseDeployment(value2) {
6981
7296
  };
6982
7297
  }
6983
7298
  function parseStatus(value2) {
6984
- const row2 = record$1(value2);
7299
+ const row2 = record$2(value2);
6985
7300
  const statusProvider = provider(row2.provider);
6986
7301
  if (!statusProvider) return void 0;
6987
- const rotationRow = record$1(row2.rotation);
7302
+ const rotationRow = record$2(row2.rotation);
6988
7303
  const state = str(rotationRow.state);
6989
7304
  const rotation = ROTATION_STATES.includes(state) ? {
6990
7305
  state,
@@ -7005,7 +7320,7 @@ function parseStatus(value2) {
7005
7320
  };
7006
7321
  }
7007
7322
  function parseMachineResult(value2) {
7008
- const row2 = record$1(value2);
7323
+ const row2 = record$2(value2);
7009
7324
  const machineId = str(row2.machineId);
7010
7325
  if (!machineId) return null;
7011
7326
  if (row2.ok !== true) {
@@ -7020,28 +7335,28 @@ function parseMachineResult(value2) {
7020
7335
  };
7021
7336
  }
7022
7337
  function parseAiCredentialVaultSnapshot(value2) {
7023
- const body = record$1(value2);
7338
+ const body = record$2(value2);
7024
7339
  return {
7025
7340
  bundles: array(body.bundles).map(parseBundle).filter((bundle) => bundle !== null),
7026
7341
  deployments: array(body.deployments).map(parseDeployment).filter((deployment) => deployment !== null)
7027
7342
  };
7028
7343
  }
7029
7344
  function parseAiCredentialCaptureResponse(value2) {
7030
- const body = record$1(value2);
7345
+ const body = record$2(value2);
7031
7346
  const bundle = parseBundle(body.bundle);
7032
7347
  if (!bundle) throw new Error("AI credential capture response did not contain a bundle");
7033
7348
  return { bundle, warnings: array(body.warnings).filter((warning) => typeof warning === "string") };
7034
7349
  }
7035
7350
  function parseAiCredentialBatchResult(value2) {
7036
- const body = record$1(value2);
7351
+ const body = record$2(value2);
7037
7352
  return {
7038
7353
  results: array(body.results).map(parseMachineResult).filter((result) => result !== null),
7039
7354
  warnings: array(body.warnings).filter((warning) => typeof warning === "string")
7040
7355
  };
7041
7356
  }
7042
7357
  function parseSelfServiceMachines(value2) {
7043
- return array(record$1(value2).machines).map((entry) => {
7044
- const row2 = record$1(entry);
7358
+ return array(record$2(value2).machines).map((entry) => {
7359
+ const row2 = record$2(entry);
7045
7360
  return {
7046
7361
  machineId: str(row2.machineId),
7047
7362
  type: row2.type === "shared" ? "shared" : "personal",
@@ -7054,8 +7369,8 @@ function parseSelfServiceMachines(value2) {
7054
7369
  }).filter((machine) => machine.machineId !== "");
7055
7370
  }
7056
7371
  function parsePersonalMachines(value2) {
7057
- return array(record$1(value2).machines).map((entry) => {
7058
- const row2 = record$1(entry);
7372
+ return array(record$2(value2).machines).map((entry) => {
7373
+ const row2 = record$2(entry);
7059
7374
  const type = row2.type === "shared" ? "shared" : "personal";
7060
7375
  return {
7061
7376
  machineId: str(row2.machineId),
@@ -7425,6 +7740,123 @@ function parseProjectListV2Page(body) {
7425
7740
  total: page.total
7426
7741
  };
7427
7742
  }
7743
+ const PUBLIC_ID_PATTERN = /^[A-Za-z0-9_-]{32}$/;
7744
+ const CHECKSUM_PATTERN = /^[a-f\d]{64}$/i;
7745
+ const MAX_ID_LENGTH = 256;
7746
+ const MAX_TITLE_LENGTH = 255;
7747
+ const MAX_WARNINGS = 32;
7748
+ const MAX_WARNING_LENGTH = 500;
7749
+ const PUBLICATION_KEYS = /* @__PURE__ */ new Set([
7750
+ "id",
7751
+ "publicId",
7752
+ "sourceType",
7753
+ "sourceIdentity",
7754
+ "sourceChecksumSha256",
7755
+ "title",
7756
+ "contentType",
7757
+ "sizeBytes",
7758
+ "checksumSha256",
7759
+ "status",
7760
+ "createdAt",
7761
+ "revokedAt",
7762
+ "publicUrl"
7763
+ ]);
7764
+ function fail(detail) {
7765
+ throw new Error(`artifact publication response is invalid: ${detail}`);
7766
+ }
7767
+ function record$1(value2, label) {
7768
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) fail(`${label} must be an object`);
7769
+ return value2;
7770
+ }
7771
+ function exactKeys(value2, keys) {
7772
+ const expected = new Set(keys);
7773
+ if (Object.keys(value2).some((key) => !expected.has(key)) || keys.some((key) => !(key in value2))) fail("response fields do not match the contract");
7774
+ }
7775
+ function nonEmptyString$1(value2, label, maxLength = MAX_ID_LENGTH) {
7776
+ if (typeof value2 !== "string" || !value2 || value2.trim() !== value2 || value2.length > maxLength) fail(`${label} is invalid`);
7777
+ return value2;
7778
+ }
7779
+ function checksum(value2, label) {
7780
+ if (typeof value2 !== "string" || !CHECKSUM_PATTERN.test(value2)) fail(`${label} is invalid`);
7781
+ }
7782
+ function safeInteger(value2, label) {
7783
+ if (typeof value2 !== "number" || !Number.isSafeInteger(value2) || value2 < 0) fail(`${label} is invalid`);
7784
+ }
7785
+ function warnings(value2) {
7786
+ if (!Array.isArray(value2) || value2.length > MAX_WARNINGS || value2.some((warning) => typeof warning !== "string" || warning.length > MAX_WARNING_LENGTH)) fail("warnings are invalid");
7787
+ return [...value2];
7788
+ }
7789
+ function isLoopbackHostname(hostname) {
7790
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
7791
+ }
7792
+ function parseTrustedPublicUrl(value2, apiBaseUrl, publicId) {
7793
+ if (typeof value2 !== "string") fail("publicUrl is invalid");
7794
+ let publicUrl;
7795
+ let apiUrl;
7796
+ try {
7797
+ publicUrl = new URL(value2);
7798
+ apiUrl = new URL(apiBaseUrl);
7799
+ } catch {
7800
+ fail("publicUrl is invalid");
7801
+ }
7802
+ if (publicUrl.protocol !== "https:" && !(publicUrl.protocol === "http:" && isLoopbackHostname(publicUrl.hostname)) || publicUrl.origin !== apiUrl.origin || publicUrl.protocol !== apiUrl.protocol || publicUrl.username || publicUrl.password || publicUrl.search || publicUrl.hash || publicUrl.pathname !== `/artifacts/${publicId}`) fail("publicUrl is not trusted");
7803
+ return publicUrl.toString();
7804
+ }
7805
+ function sourceIdentity(source) {
7806
+ return source.type === "project-file" ? JSON.stringify([
7807
+ source.projectId,
7808
+ ...source.worktreeId ? [source.worktreeId] : [],
7809
+ source.path
7810
+ ]) : JSON.stringify([source.chatId, source.path]);
7811
+ }
7812
+ function validatePublication(value2, apiBaseUrl, operation, expectedSource) {
7813
+ const publication = record$1(value2, "publication");
7814
+ if (Object.keys(publication).some((key) => !PUBLICATION_KEYS.has(key))) {
7815
+ fail("publication contains an unexpected field");
7816
+ }
7817
+ if ([...PUBLICATION_KEYS].some((key) => !(key in publication))) {
7818
+ fail("publication is missing a field");
7819
+ }
7820
+ const publicationId = nonEmptyString$1(publication.id, "publication id");
7821
+ const publicId = nonEmptyString$1(publication.publicId, "public id");
7822
+ if (!PUBLIC_ID_PATTERN.test(publicId)) fail("public id is invalid");
7823
+ if (!["project-file", "personal-chat-file", "personal-chat-artifact"].includes(String(publication.sourceType))) {
7824
+ fail("source type is invalid");
7825
+ }
7826
+ const parsedSourceIdentity = nonEmptyString$1(publication.sourceIdentity, "source identity", 4096);
7827
+ if (expectedSource && (publication.sourceType !== expectedSource.type || parsedSourceIdentity !== sourceIdentity(expectedSource))) fail("source does not match the request");
7828
+ checksum(publication.sourceChecksumSha256, "source checksum");
7829
+ const title = nonEmptyString$1(publication.title, "title", MAX_TITLE_LENGTH);
7830
+ nonEmptyString$1(publication.contentType, "content type");
7831
+ safeInteger(publication.sizeBytes, "size");
7832
+ checksum(publication.checksumSha256, "checksum");
7833
+ safeInteger(publication.createdAt, "created time");
7834
+ const expectedStatus = operation === "publish" ? "active" : "revoked";
7835
+ if (publication.status !== expectedStatus) fail("status does not match the operation");
7836
+ if (publication.revokedAt !== null && typeof publication.revokedAt !== "number" || typeof publication.revokedAt === "number" && (!Number.isSafeInteger(publication.revokedAt) || publication.revokedAt < 0) || expectedStatus === "active" && publication.revokedAt !== null || expectedStatus === "revoked" && publication.revokedAt === null) fail("revoked time is invalid");
7837
+ return {
7838
+ publicationId,
7839
+ publicId,
7840
+ publicUrl: parseTrustedPublicUrl(publication.publicUrl, apiBaseUrl, publicId),
7841
+ title,
7842
+ status: expectedStatus
7843
+ };
7844
+ }
7845
+ function parseStaticArtifactPublicationResult(value2, options) {
7846
+ const response = record$1(value2, "response");
7847
+ if (options.operation === "publish") {
7848
+ exactKeys(response, ["publication", "publicUrl", "reused"]);
7849
+ if (typeof response.reused !== "boolean") fail("reused is invalid");
7850
+ const publication2 = validatePublication(response.publication, options.apiBaseUrl, options.operation, options.source);
7851
+ const publicUrl = parseTrustedPublicUrl(response.publicUrl, options.apiBaseUrl, publication2.publicId);
7852
+ if (publication2.publicUrl !== publicUrl) fail("public URLs do not match");
7853
+ return { version: 1, ...publication2, reused: response.reused, warnings: [] };
7854
+ }
7855
+ exactKeys(response, ["publication", "warnings"]);
7856
+ const parsedWarnings = warnings(response.warnings);
7857
+ const publication = validatePublication(response.publication, options.apiBaseUrl, options.operation);
7858
+ return { version: 1, ...publication, reused: false, warnings: parsedWarnings };
7859
+ }
7428
7860
  function asRecord(value2) {
7429
7861
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
7430
7862
  }
@@ -16001,6 +16433,31 @@ function coercePromptSuggestion(value2) {
16001
16433
  if (!text2 || !provider2 || updatedAt === void 0) return void 0;
16002
16434
  return { text: text2, provider: provider2, updatedAt };
16003
16435
  }
16436
+ const MCP_RUNTIME_STATUSES = /* @__PURE__ */ new Set([
16437
+ "connected",
16438
+ "reconnecting",
16439
+ "failed",
16440
+ "needs-auth",
16441
+ "config-fetch-failed",
16442
+ "mcp-config-missing",
16443
+ "connector-config-missing",
16444
+ "connector-runtime-failed",
16445
+ "connector-needs-auth"
16446
+ ]);
16447
+ function coerceMcpServerStatuses(value2) {
16448
+ if (!Array.isArray(value2)) return void 0;
16449
+ const entries = [];
16450
+ for (const raw of value2) {
16451
+ if (!isRecord$1(raw)) continue;
16452
+ const name = nonEmptyString(raw.name);
16453
+ const status = typeof raw.status === "string" && MCP_RUNTIME_STATUSES.has(raw.status) ? raw.status : void 0;
16454
+ const checkedAt = optionalTimestamp(raw.checkedAt);
16455
+ if (!name || !status || checkedAt === void 0) continue;
16456
+ const error = nonEmptyString(raw.error);
16457
+ entries.push({ name, status, checkedAt, ...error ? { error } : {} });
16458
+ }
16459
+ return entries.length > 0 ? entries : void 0;
16460
+ }
16004
16461
  function coerceCreatedBy(value2) {
16005
16462
  if (!isRecord$1(value2)) return void 0;
16006
16463
  const accountId = nonEmptyString(value2.accountId);
@@ -16008,6 +16465,18 @@ function coerceCreatedBy(value2) {
16008
16465
  const displayName = nonEmptyString(value2.displayName);
16009
16466
  return displayName ? { accountId, displayName } : { accountId };
16010
16467
  }
16468
+ function decryptSessionJsonSync(sessionId, blob, candidateKeys) {
16469
+ if (!blob) return null;
16470
+ const cached = getCachedDataKeyMetadataJson(sessionId, blob);
16471
+ if (cached !== null) {
16472
+ try {
16473
+ return JSON.parse(cached);
16474
+ } catch {
16475
+ return null;
16476
+ }
16477
+ }
16478
+ return decryptJsonWithCandidates(blob, candidateKeys);
16479
+ }
16011
16480
  function apiSessionToSession(api, candidateKeys, existing) {
16012
16481
  const updatedAt = mergeUpdatedAt(api.updatedAt);
16013
16482
  const summaryText = decryptSummaryText(api.metadata, candidateKeys) ?? existing?.summaryText;
@@ -16017,8 +16486,8 @@ function apiSessionToSession(api, candidateKeys, existing) {
16017
16486
  const pendingExitPlanMode = apiSessionAgentStatePendingExitPlanMode(api.agentState, candidateKeys);
16018
16487
  const pendingPermissionRequests = apiSessionAgentStatePendingPermissions(api.agentState, candidateKeys);
16019
16488
  const agentControlledByUser = apiSessionAgentStateControlledByUser(api.agentState, candidateKeys);
16020
- const agentState = api.agentState ? decryptJsonWithCandidates(api.agentState, candidateKeys) : null;
16021
- const metadata = decryptJsonWithCandidates(api.metadata, candidateKeys);
16489
+ const agentState = api.agentState ? decryptSessionJsonSync(api.id, api.agentState, candidateKeys) : null;
16490
+ const metadata = decryptSessionJsonSync(api.id, api.metadata, candidateKeys);
16022
16491
  const agent = agentFromFlavor(typeof metadata?.flavor === "string" ? metadata.flavor : void 0) ?? existing?.agent;
16023
16492
  const models = coerceSessionModels(metadata?.models) ?? existing?.models;
16024
16493
  const worktree = coerceSessionWorktree(metadata?.worktree) ?? coerceSessionWorktree(metadata) ?? existing?.worktree;
@@ -16035,6 +16504,7 @@ function apiSessionToSession(api, candidateKeys, existing) {
16035
16504
  const branchSlug = branchSlugFromMetadata(metadata) ?? existing?.branchSlug;
16036
16505
  const path = nonEmptyString(metadata?.path) ?? existing?.path;
16037
16506
  const promptSuggestion2 = metadata && Object.prototype.hasOwnProperty.call(metadata, "promptSuggestion") ? coercePromptSuggestion(metadata.promptSuggestion) : existing?.promptSuggestion;
16507
+ const mcpServers = coerceMcpServerStatuses(metadata?.mcpServers) ?? existing?.mcpServers;
16038
16508
  return {
16039
16509
  ...existing,
16040
16510
  id: api.id,
@@ -16061,6 +16531,7 @@ function apiSessionToSession(api, candidateKeys, existing) {
16061
16531
  ...currentModelCode ? { currentModelCode } : {},
16062
16532
  ...tokenUsage ? { tokenUsage } : {},
16063
16533
  ...slashCommands ? { slashCommands } : {},
16534
+ ...mcpServers ? { mcpServers } : {},
16064
16535
  ...claudeSessionId ? { claudeSessionId } : {},
16065
16536
  ...codexThreadId ? { codexThreadId } : {},
16066
16537
  ...lifecycleState ? { lifecycleState } : {},
@@ -16103,9 +16574,10 @@ function readMcpCallerGrantPublicKey(daemonState) {
16103
16574
  const value2 = daemonState.mcpCallerGrantPublicKey;
16104
16575
  return typeof value2 === "string" && value2 ? value2 : void 0;
16105
16576
  }
16577
+ const CLEARABLE_STRING_KEYS = /* @__PURE__ */ new Set(["branch", "baseBranch"]);
16106
16578
  function buildWorktreeMutationBody(input, options = {}) {
16107
16579
  return JSON.stringify(Object.fromEntries(
16108
- Object.entries(input).filter(([key, value2]) => (key !== "machinePlatform" || options.includeMachinePlatform) && (typeof value2 === "string" && (value2.length > 0 || key === "branch") || key === "branch" && value2 === null))
16580
+ 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)))
16109
16581
  ));
16110
16582
  }
16111
16583
  const STANDALONE_USER_ID = "standalone-local";
@@ -16359,18 +16831,18 @@ function readCloudMobileLoginPairingErrorCode(body) {
16359
16831
  const PAIRING_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16360
16832
  const CODE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
16361
16833
  const STATUSES = /* @__PURE__ */ new Set(["pending", "consumed", "revoked", "expired"]);
16362
- function invalidResponse() {
16834
+ function invalidResponse$1() {
16363
16835
  throw new Error("PAIRING_RESPONSE_INVALID");
16364
16836
  }
16365
16837
  function parseCloudMobileLoginPairing(raw, options = {}) {
16366
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) invalidResponse();
16838
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) invalidResponse$1();
16367
16839
  const value2 = raw;
16368
- if (typeof value2.pairingId !== "string" || !PAIRING_ID_PATTERN.test(value2.pairingId)) invalidResponse();
16369
- if (!Number.isSafeInteger(value2.expiresAt) || value2.expiresAt <= 0) invalidResponse();
16370
- if (!Number.isSafeInteger(value2.serverTime) || value2.serverTime <= 0) invalidResponse();
16371
- if (typeof value2.status !== "string" || !STATUSES.has(value2.status)) invalidResponse();
16372
- if (options.requireCode && (typeof value2.code !== "string" || !CODE_PATTERN.test(value2.code))) invalidResponse();
16373
- if (value2.code !== void 0 && (typeof value2.code !== "string" || !CODE_PATTERN.test(value2.code))) invalidResponse();
16840
+ if (typeof value2.pairingId !== "string" || !PAIRING_ID_PATTERN.test(value2.pairingId)) invalidResponse$1();
16841
+ if (!Number.isSafeInteger(value2.expiresAt) || value2.expiresAt <= 0) invalidResponse$1();
16842
+ if (!Number.isSafeInteger(value2.serverTime) || value2.serverTime <= 0) invalidResponse$1();
16843
+ if (typeof value2.status !== "string" || !STATUSES.has(value2.status)) invalidResponse$1();
16844
+ if (options.requireCode && (typeof value2.code !== "string" || !CODE_PATTERN.test(value2.code))) invalidResponse$1();
16845
+ if (value2.code !== void 0 && (typeof value2.code !== "string" || !CODE_PATTERN.test(value2.code))) invalidResponse$1();
16374
16846
  return {
16375
16847
  pairingId: value2.pairingId,
16376
16848
  expiresAt: value2.expiresAt,
@@ -16379,6 +16851,43 @@ function parseCloudMobileLoginPairing(raw, options = {}) {
16379
16851
  ...typeof value2.code === "string" ? { code: value2.code } : {}
16380
16852
  };
16381
16853
  }
16854
+ const BROWSER_VIEWER_OPEN_TIMEOUT_MS = 13e4;
16855
+ const BROWSER_VIEWER_INSTALL_TIMEOUT_MS = 31e4;
16856
+ function invalidResponse() {
16857
+ throw new Error("BROWSER_VIEWER_RESPONSE_INVALID");
16858
+ }
16859
+ function responseRecord(value2) {
16860
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) invalidResponse();
16861
+ return value2;
16862
+ }
16863
+ function parseBrowserViewerOpenResponse(value2) {
16864
+ const response = responseRecord(value2);
16865
+ if (typeof response.token !== "string" || response.token.length === 0 || typeof response.relayUrl !== "string" && response.relayUrl !== null || response.routingVersion !== 1 || typeof response.previewDomain !== "string" && response.previewDomain !== null || !Number.isInteger(response.webPort) || response.webPort < 1 || response.webPort > 65535 || typeof response.bridgeReady !== "boolean") invalidResponse();
16866
+ return {
16867
+ token: response.token,
16868
+ relayUrl: response.relayUrl,
16869
+ routingVersion: 1,
16870
+ previewDomain: response.previewDomain,
16871
+ webPort: response.webPort,
16872
+ bridgeReady: response.bridgeReady
16873
+ };
16874
+ }
16875
+ function parseBrowserViewerInstallResponse(value2) {
16876
+ const response = responseRecord(value2);
16877
+ if (response.action === "already-installed") return { kind: "already-installed" };
16878
+ if (response.action === "run") {
16879
+ if (response.ok !== true) invalidResponse();
16880
+ return { kind: "installed" };
16881
+ }
16882
+ if (response.action === "manual") {
16883
+ if (typeof response.command !== "string" || typeof response.reason !== "string") invalidResponse();
16884
+ return {
16885
+ kind: "manual-required",
16886
+ reason: response.command.length === 0 ? "unsupported-platform" : "admin-required"
16887
+ };
16888
+ }
16889
+ return invalidResponse();
16890
+ }
16382
16891
  const FIXED_API_BASE_URL = "https://saycode.ai";
16383
16892
  const FIXED_SYNC_BASE_URL = "https://saycode.ai";
16384
16893
  const machineRegistryCache = /* @__PURE__ */ new Map();
@@ -16559,6 +17068,7 @@ function normalizeMachine(row2, candidateKeys) {
16559
17068
  mcpCallerGrantPublicKey: readMcpCallerGrantPublicKey(daemonState),
16560
17069
  automationSupport,
16561
17070
  filesystemScope: decrypted.filesystemScope,
17071
+ additionalDirectories: decrypted.additionalDirectories,
16562
17072
  registry
16563
17073
  };
16564
17074
  }
@@ -16586,7 +17096,7 @@ class ApiClient {
16586
17096
  headers: { "Content-Type": "application/json" },
16587
17097
  body: JSON.stringify(input)
16588
17098
  }, DEFAULT_REQUEST_TIMEOUT_MS);
16589
- return this.handleResponse(response);
17099
+ return parseCloudLoginResult(await this.handleResponse(response));
16590
17100
  }
16591
17101
  async getMe() {
16592
17102
  return this.request("/api/auth/me", { boundary: "api" });
@@ -16642,6 +17152,117 @@ class ApiClient {
16642
17152
  });
16643
17153
  return data.items ?? [];
16644
17154
  }
17155
+ /**
17156
+ * 관리자가 보낸 플랫폼 공지(`kind: 'platform_notice'`)만 골라 돌려준다. 다른 kind
17157
+ * (`chat_completion`)는 데스크탑이 이미 스스로 만들고 있어 동기화 대상이 아니다.
17158
+ * specs/desktop-platform-notice-inbox
17159
+ */
17160
+ async listServerNotifications() {
17161
+ const data = await this.request("/api/me/completion-notifications", { boundary: "api" });
17162
+ return parseServerCompletionNotifications(data);
17163
+ }
17164
+ /**
17165
+ * 서버 알림함에 읽음을 반영한다. `kind` 를 반드시 넘긴다 — 서버 유니크 키가
17166
+ * (userId, kind, sessionId) 라 kind 없이는 같은 세션의 어느 kind를 읽음 처리할지
17167
+ * 서버가 구분하지 못한다. 실패해도 로컬 상태는 이미 반영됐으므로 호출부가 삼킨다.
17168
+ * specs/desktop-platform-notice-inbox R4
17169
+ */
17170
+ async markServerNotificationRead(sessionId, kind) {
17171
+ await this.request(`/api/me/completion-notifications/${encodeURIComponent(sessionId)}/read?kind=${encodeURIComponent(kind)}`, {
17172
+ method: "PATCH",
17173
+ boundary: "api"
17174
+ });
17175
+ }
17176
+ /**
17177
+ * 관련 프로젝트 등록 목록. 서버가 양방향으로 조회해 준다(대상으로 등록한 쪽도
17178
+ * 나온다). specs/desktop-related-project-handoff
17179
+ */
17180
+ async listProjectLinks(projectId) {
17181
+ const data = await this.request(
17182
+ `/api/projects/${encodeURIComponent(projectId)}/links`,
17183
+ { boundary: "api" }
17184
+ );
17185
+ return data.links ?? [];
17186
+ }
17187
+ async addProjectLink(projectId, targetProjectId, role) {
17188
+ const data = await this.request(
17189
+ `/api/projects/${encodeURIComponent(projectId)}/links`,
17190
+ { method: "POST", boundary: "api", body: JSON.stringify({ targetProjectId, role }) }
17191
+ );
17192
+ return data.link;
17193
+ }
17194
+ async removeProjectLink(projectId, targetProjectId) {
17195
+ await this.request(`/api/projects/${encodeURIComponent(projectId)}/links`, {
17196
+ method: "DELETE",
17197
+ boundary: "api",
17198
+ body: JSON.stringify({ targetProjectId })
17199
+ });
17200
+ }
17201
+ /** 진행 중(queued/running/blocked) 핸드오프 전체 — 전역 인디케이터용. */
17202
+ async listActiveProjectHandoffs() {
17203
+ const data = await this.request("/api/handoffs", { boundary: "api" });
17204
+ return data.handoffs ?? [];
17205
+ }
17206
+ /** 이 프로젝트에서 내보낸 핸드오프 전체 — 프로젝트 설정 화면용. */
17207
+ async listProjectHandoffsForProject(projectId) {
17208
+ const data = await this.request(
17209
+ `/api/projects/${encodeURIComponent(projectId)}/handoffs`,
17210
+ { boundary: "api" }
17211
+ );
17212
+ return data.handoffs ?? [];
17213
+ }
17214
+ /**
17215
+ * 승인. `sourceSessionId` 는 승인 카드가 렌더된 대화 — 서버가 딥링크 조립에 쓴다.
17216
+ * 이미 승인된(queued 이상) 핸드오프를 다시 승인하면 서버가 409 를 던진다(멱등은
17217
+ * 호출부가 그 에러를 흡수하는 방식으로 구현한다). specs/desktop-related-project-handoff R7
17218
+ */
17219
+ async approveProjectHandoff(handoffId, sourceSessionId) {
17220
+ const data = await this.request(
17221
+ `/api/handoffs/${encodeURIComponent(handoffId)}/approve`,
17222
+ { method: "POST", boundary: "api", body: JSON.stringify({ sourceSessionId }) }
17223
+ );
17224
+ return data.handoff;
17225
+ }
17226
+ async rejectProjectHandoff(handoffId) {
17227
+ const data = await this.request(
17228
+ `/api/handoffs/${encodeURIComponent(handoffId)}/reject`,
17229
+ { method: "POST", boundary: "api" }
17230
+ );
17231
+ return data.handoff;
17232
+ }
17233
+ /** 대상 프로젝트에 실제 세션을 만든 뒤 보고한다. 서버가 targetSessionId 기준으로
17234
+ * 멱등을 보장한다 — 이미 있으면 새 매핑을 만들지 않고 그대로 돌려준다. */
17235
+ async reportProjectHandoffStarted(handoffId, targetSessionId) {
17236
+ const data = await this.request(
17237
+ `/api/handoffs/${encodeURIComponent(handoffId)}/started`,
17238
+ { method: "POST", boundary: "api", body: JSON.stringify({ targetSessionId }) }
17239
+ );
17240
+ return data.handoff;
17241
+ }
17242
+ /** 대상 프로젝트에 온라인 머신이 없는 등, 세션 생성 자체를 시작하지 못했을 때. */
17243
+ async reportProjectHandoffBlocked(handoffId, reason) {
17244
+ const data = await this.request(
17245
+ `/api/handoffs/${encodeURIComponent(handoffId)}/blocked`,
17246
+ { method: "POST", boundary: "api", body: JSON.stringify({ reason }) }
17247
+ );
17248
+ return data.handoff;
17249
+ }
17250
+ /** 세션 생성 시도가 던졌을 때. */
17251
+ async reportProjectHandoffFailed(handoffId, reason) {
17252
+ const data = await this.request(
17253
+ `/api/handoffs/${encodeURIComponent(handoffId)}/failed`,
17254
+ { method: "POST", boundary: "api", body: JSON.stringify({ reason }) }
17255
+ );
17256
+ return data.handoff;
17257
+ }
17258
+ /** 기본 꺼짐 — request_project_handoff MCP 도구 노출 토글(spec R9). */
17259
+ async setProjectHandoffMcpEnabled(projectId, enabled) {
17260
+ return this.request(`/api/projects/${encodeURIComponent(projectId)}/handoff-mcp-enabled`, {
17261
+ method: "PATCH",
17262
+ boundary: "api",
17263
+ body: JSON.stringify({ enabled })
17264
+ });
17265
+ }
16645
17266
  async addProjectFavorite(projectId) {
16646
17267
  return this.request(`/api/projects/${encodeURIComponent(projectId)}/favorite`, {
16647
17268
  method: "PUT",
@@ -16657,6 +17278,7 @@ class ApiClient {
16657
17278
  // --- 개인 Chat (specs/personal-chats) ---
16658
17279
  async listPersonalChats(input = {}) {
16659
17280
  const params = new URLSearchParams();
17281
+ params.set("context", input.companyId ? `company:${input.companyId}` : "personal");
16660
17282
  if (input.limit !== void 0) params.set("limit", String(input.limit));
16661
17283
  if (input.offset !== void 0) params.set("offset", String(input.offset));
16662
17284
  if (input.query) params.set("query", input.query);
@@ -16672,12 +17294,22 @@ class ApiClient {
16672
17294
  const data = await this.request("/api/personal-chats", {
16673
17295
  method: "POST",
16674
17296
  boundary: "api",
16675
- body: JSON.stringify(
16676
- input.title === void 0 ? { machineId: input.machineId } : { machineId: input.machineId, title: input.title }
16677
- )
17297
+ body: JSON.stringify({
17298
+ machineId: input.machineId,
17299
+ ...input.title === void 0 ? {} : { title: input.title },
17300
+ ..."companyId" in input ? { companyId: input.companyId } : {}
17301
+ })
16678
17302
  });
16679
17303
  return data.chat;
16680
17304
  }
17305
+ async getPersonalChatRuntimeConfig(chatId, machineId) {
17306
+ const params = new URLSearchParams({ machineId });
17307
+ const data = await this.request(
17308
+ `/api/personal-chats/${encodeURIComponent(chatId)}/runtime-config?${params.toString()}`,
17309
+ { boundary: "api", headers: { "Cache-Control": "no-cache" } }
17310
+ );
17311
+ return data.environmentVariables ?? {};
17312
+ }
16681
17313
  async getPersonalChat(chatId) {
16682
17314
  const data = await this.request(
16683
17315
  `/api/personal-chats/${encodeURIComponent(chatId)}`,
@@ -17003,19 +17635,23 @@ class ApiClient {
17003
17635
  }
17004
17636
  /**
17005
17637
  * Mint the signed caller grant that proves *this* user is the connector
17006
- * subject of a session spawned on a shared machine. The server binds it to
17007
- * userId/machineId/projectId/expiry and rejects a caller who cannot access
17008
- * the machine or project (web-ui server/mcpCallerGrantRoute.ts). `projectId`
17009
- * is required by the route — send an explicit null, not an omitted field,
17010
- * because the grant's project scope must match the spawn's.
17638
+ * subject of a session spawned on a grant-capable machine. The server binds it to
17639
+ * userId/machineId/projectId-or-personalChatId/expiry and rejects a caller
17640
+ * who cannot access that runtime scope (web-ui server/mcpCallerGrantRoute.ts).
17641
+ * `projectId` is required by the route — Personal Chat grants send an explicit
17642
+ * null plus `personalChatId` so the signed scope matches the spawn.
17011
17643
  */
17012
- async mintMcpCallerGrant(machineId, projectId) {
17644
+ async mintMcpCallerGrant(machineId, projectId, personalChatId) {
17013
17645
  const data = await this.request(
17014
17646
  "/api/me/mcp-caller-grant",
17015
17647
  {
17016
17648
  method: "POST",
17017
17649
  boundary: "api",
17018
- body: JSON.stringify({ machineId, projectId })
17650
+ body: JSON.stringify({
17651
+ machineId,
17652
+ projectId,
17653
+ ...personalChatId ? { personalChatId } : {}
17654
+ })
17019
17655
  }
17020
17656
  );
17021
17657
  if (typeof data.grant !== "string" || !data.grant) throw new Error("mcp caller grant is missing");
@@ -17699,23 +18335,26 @@ class ApiClient {
17699
18335
  boundary: "api"
17700
18336
  });
17701
18337
  }
17702
- async fetchSessions(candidateKeys = [], tokenOverride) {
18338
+ async fetchSessions(candidateKeys = [], tokenOverride, accountPrivateKey = null) {
17703
18339
  const data = await this.request("/v1/sessions", {
17704
18340
  boundary: "sync",
17705
18341
  tokenOverride
17706
18342
  });
17707
18343
  const rows = Array.isArray(data) ? data : data.sessions ?? [];
18344
+ await preDecryptSessionHeaders(rows, accountPrivateKey);
17708
18345
  return rows.map((row2) => apiSessionToSession(row2, candidateKeys));
17709
18346
  }
17710
- async fetchSessionsPage(candidateKeys = [], cursor, limit = 200, tokenOverride) {
18347
+ async fetchSessionsPage(candidateKeys = [], cursor, limit = 200, tokenOverride, accountPrivateKey = null) {
17711
18348
  const params = new URLSearchParams({ limit: String(limit) });
17712
18349
  if (cursor) params.set("cursor", cursor);
17713
18350
  const data = await this.request(
17714
18351
  `/v2/sessions?${params.toString()}`,
17715
18352
  { boundary: "sync", tokenOverride }
17716
18353
  );
18354
+ const rows = data.sessions ?? [];
18355
+ await preDecryptSessionHeaders(rows, accountPrivateKey);
17717
18356
  return {
17718
- sessions: (data.sessions ?? []).map((row2) => apiSessionToSession(row2, candidateKeys)),
18357
+ sessions: rows.map((row2) => apiSessionToSession(row2, candidateKeys)),
17719
18358
  nextCursor: data.nextCursor ?? null,
17720
18359
  hasNext: data.hasNext === true
17721
18360
  };
@@ -17730,8 +18369,10 @@ class ApiClient {
17730
18369
  });
17731
18370
  return data.sessions ?? [];
17732
18371
  }
17733
- async lookupSessions(candidateKeys, ids, tokenOverride) {
17734
- return (await this.lookupApiSessions(ids, tokenOverride)).map((row2) => apiSessionToSession(row2, candidateKeys));
18372
+ async lookupSessions(candidateKeys, ids, tokenOverride, accountPrivateKey = null) {
18373
+ const rows = await this.lookupApiSessions(ids, tokenOverride);
18374
+ await preDecryptSessionHeaders(rows, accountPrivateKey);
18375
+ return rows.map((row2) => apiSessionToSession(row2, candidateKeys));
17735
18376
  }
17736
18377
  async deleteSession(sessionId, tokenOverride) {
17737
18378
  await this.request(`/v1/sessions/${encodeURIComponent(sessionId)}`, {
@@ -17916,9 +18557,8 @@ class ApiClient {
17916
18557
  }
17917
18558
  async sendUserTextMessage(input) {
17918
18559
  const localId = input.localId || `desktop-${Date.now()}-${Math.random().toString(36).slice(2)}`;
17919
- const key = input.secretBase64 ? decodeBase64(input.secretBase64) : null;
17920
18560
  const content = buildUserTextMessageContent(input);
17921
- const encoded = encodeMessageContent(content, key);
18561
+ const encoded = input.dataEncryptionKey ? await encryptDataKeyContent(input.dataEncryptionKey, content) : encodeMessageContent(content, input.secretBase64 ? decodeBase64(input.secretBase64) : null);
17922
18562
  const result = await this.sendMessages(input.sessionId, [{ localId, content: encoded }], input.tokenOverride);
17923
18563
  return { localId, sentSeq: result.messages?.[0]?.seq };
17924
18564
  }
@@ -17990,6 +18630,34 @@ class ApiClient {
17990
18630
  }
17991
18631
  return resolvedProjectPreviewRelayUrl({ ...input, response: { ...response, token: response.token }, syncBaseUrl: this.config.syncBaseUrl });
17992
18632
  }
18633
+ async openBrowserViewer(machineId, signal) {
18634
+ return parseBrowserViewerOpenResponse(await this.request(
18635
+ `/api/machines/${encodeURIComponent(machineId)}/browser-viewer/open`,
18636
+ {
18637
+ method: "POST",
18638
+ boundary: "api",
18639
+ signal,
18640
+ timeoutMs: BROWSER_VIEWER_OPEN_TIMEOUT_MS
18641
+ }
18642
+ ));
18643
+ }
18644
+ async installBrowserViewer(machineId, signal) {
18645
+ return this.installBrowserViewerComponent(machineId, "install", signal);
18646
+ }
18647
+ async installBrowserViewerChrome(machineId, signal) {
18648
+ return this.installBrowserViewerComponent(machineId, "install-chrome", signal);
18649
+ }
18650
+ async installBrowserViewerComponent(machineId, action, signal) {
18651
+ return parseBrowserViewerInstallResponse(await this.request(
18652
+ `/api/machines/${encodeURIComponent(machineId)}/browser-viewer/${action}`,
18653
+ {
18654
+ method: "POST",
18655
+ boundary: "api",
18656
+ signal,
18657
+ timeoutMs: BROWSER_VIEWER_INSTALL_TIMEOUT_MS
18658
+ }
18659
+ ));
18660
+ }
17993
18661
  // --- Project deploy & share (specs/desktop-project-deploy-share) ---
17994
18662
  async moveProject(projectId, input) {
17995
18663
  return this.request(`/api/projects/${encodeURIComponent(projectId)}/move`, {
@@ -18111,6 +18779,30 @@ class ApiClient {
18111
18779
  timeoutMs: LONG_REQUEST_TIMEOUT_MS
18112
18780
  });
18113
18781
  }
18782
+ async publishStaticArtifact(source, signal) {
18783
+ const { version: _version, ...serverSource } = source;
18784
+ const response = await this.request("/api/artifact-publications", {
18785
+ method: "POST",
18786
+ boundary: "api",
18787
+ body: JSON.stringify({ source: serverSource }),
18788
+ signal
18789
+ });
18790
+ return parseStaticArtifactPublicationResult(response, {
18791
+ apiBaseUrl: this.config.apiBaseUrl,
18792
+ operation: "publish",
18793
+ source
18794
+ });
18795
+ }
18796
+ async revokeStaticArtifactPublication(publicationId, signal) {
18797
+ const response = await this.request(
18798
+ `/api/artifact-publications/${encodeURIComponent(publicationId)}`,
18799
+ { method: "DELETE", boundary: "api", signal }
18800
+ );
18801
+ return parseStaticArtifactPublicationResult(response, {
18802
+ apiBaseUrl: this.config.apiBaseUrl,
18803
+ operation: "revoke"
18804
+ });
18805
+ }
18114
18806
  async getDeployGitHubFiles(projectId, branch = "main") {
18115
18807
  const data = await this.request("/deploy/github-files", {
18116
18808
  method: "POST",
@@ -19531,11 +20223,11 @@ function reduceSessionReadState(state, change, merge, fingerprintLimit = DEFAULT
19531
20223
  const { item } = change;
19532
20224
  if (state.fingerprints.includes(item.fingerprint)) return state;
19533
20225
  if (change.kind === "event" && item.cursor !== null && state.cursor !== null && item.cursor < state.cursor) return state;
19534
- const fingerprints = [...state.fingerprints, item.fingerprint].slice(-Math.max(1, fingerprintLimit));
20226
+ const fingerprints2 = [...state.fingerprints, item.fingerprint].slice(-Math.max(1, fingerprintLimit));
19535
20227
  return {
19536
20228
  value: merge(state.value, item.value),
19537
20229
  cursor: item.cursor === null ? state.cursor : Math.max(state.cursor ?? item.cursor, item.cursor),
19538
- fingerprints
20230
+ fingerprints: fingerprints2
19539
20231
  };
19540
20232
  }
19541
20233
  function record(value2) {
@@ -20488,7 +21180,7 @@ function chatUploadRelPath(file, id) {
20488
21180
  if (file.type.startsWith("image/")) return `.aplus/uploads/${id}.${extensionFor(file)}`;
20489
21181
  return `.aplus/uploads/${id}-${safeFileName(file)}`;
20490
21182
  }
20491
- async function uploadChatFileToMachineCore(file, workspaceRoot, deps) {
21183
+ async function uploadChatFileToMachineCore(file, workspaceRoot, deps, relativePath) {
20492
21184
  const root = workspaceRoot.trim().replace(/\/+$/, "");
20493
21185
  if (!root) throw new Error(translateCurrent("upload.noWorkspaceRoot"));
20494
21186
  const id = (deps.now?.() ?? Date.now()).toString(36) + (deps.random?.() ?? Math.random()).toString(36).slice(2, 8);
@@ -20937,6 +21629,7 @@ const USAGE_SETTINGS_NAVIGATION_DESTINATIONS = [
20937
21629
  "settings_shortcuts",
20938
21630
  "settings_machines",
20939
21631
  "settings_mobile",
21632
+ "settings_network",
20940
21633
  "settings_env",
20941
21634
  "settings_security",
20942
21635
  "settings_memory",
@@ -23394,7 +24087,7 @@ ${text2}` : text2);
23394
24087
  return sections.join("\n\n");
23395
24088
  }
23396
24089
  const HANDOFF_READ_MESSAGES = 400;
23397
- async function handoffToSibling(runtime, scope, env, handoff, agent) {
24090
+ async function handoffToSibling(runtime, scope, env, handoff, agent, modelOptions = {}) {
23398
24091
  const from = handoff.from ?? env.sessionId;
23399
24092
  if (!from) {
23400
24093
  return agentFail(
@@ -23433,6 +24126,7 @@ async function handoffToSibling(runtime, scope, env, handoff, agent) {
23433
24126
  sessionId = (await runtime.spawnSession({
23434
24127
  directory: env.root,
23435
24128
  agent,
24129
+ ...modelOptions,
23436
24130
  initialPrompt: prompt,
23437
24131
  environmentVariables: childAgentEnv(env, childId)
23438
24132
  })).sessionId;
@@ -23538,7 +24232,7 @@ function resolveSubagentControlPermission(records, parentSessionId, childSession
23538
24232
  return valid ? isDescendant(valid, parentSessionId, childSessionId, 2) : false;
23539
24233
  }
23540
24234
  function canControlSpawnedDescendant(records, parentSessionId, childSessionId) {
23541
- const lineage = records.flatMap((record2) => record2.outcome === "spawned" && record2.parentSessionId && record2.childSessionId ? [{
24235
+ const lineage = records.flatMap((record2) => record2.outcome === "spawned" && record2.parentSessionId && record2.childSessionId && record2.parentSessionId !== record2.childSessionId && Number.isFinite(record2.ts) && record2.ts >= 0 ? [{
23542
24236
  parentSessionId: record2.parentSessionId,
23543
24237
  childSessionId: record2.childSessionId,
23544
24238
  spawnedAt: record2.ts,
@@ -23579,24 +24273,27 @@ async function stopOwnedTarget(runtime, env, target) {
23579
24273
  const refused = ownershipFailure(runtime, env, target);
23580
24274
  if (refused) return refused;
23581
24275
  if (!runtime.stopSession) return agentFail("stop_rejected", "stop transport is unavailable");
23582
- let stream2 = null;
23583
- try {
23584
- stream2 = await runtime.openStream(target.session.id);
23585
- if (stream2.current().thinking !== true) {
23586
- return agentFail("stop_not_responding", `${target.session.id} has no active response`);
23587
- }
23588
- } catch (error) {
23589
- return agentFail("stop_rejected", error instanceof Error ? error.message : String(error));
23590
- } finally {
23591
- stream2?.close();
23592
- }
23593
24276
  try {
23594
24277
  const result = await runtime.stopSession(target.session.machineId, target.session.id);
23595
- return result.stopped ? agentOk({ sessionId: target.session.id, stopped: true }) : agentFail("stop_rejected", result.error);
24278
+ if (result.stopped) return agentOk({ sessionId: target.session.id, stopped: true });
24279
+ return result.reason === "not-responding" ? agentFail("stop_not_responding", result.error) : agentFail("stop_rejected", result.error);
23596
24280
  } catch (error) {
23597
24281
  return agentFail("stop_rejected", error instanceof Error ? error.message : String(error));
23598
24282
  }
23599
24283
  }
24284
+ const CLI_CONFIGURABLE_AGENTS = [.../* @__PURE__ */ new Set([
24285
+ ...newSessionAgents.map(({ id }) => id),
24286
+ "gemini"
24287
+ ])];
24288
+ function cliSpawnModelOptions() {
24289
+ return CLI_CONFIGURABLE_AGENTS.map((agent) => ({
24290
+ agent,
24291
+ models: modelChoices(agent).map(({ key: id }) => ({
24292
+ id,
24293
+ efforts: effortChoices(agent, id).map(({ key }) => key)
24294
+ }))
24295
+ }));
24296
+ }
23600
24297
  const DEFAULT_READ_LIMIT = 20;
23601
24298
  const STATE_WINDOW_MESSAGES = 50;
23602
24299
  function scopeOf(runtime) {
@@ -23650,7 +24347,7 @@ async function readSession(runtime, target, last) {
23650
24347
  });
23651
24348
  }
23652
24349
  const DEFAULT_SPAWN_AGENT = "claude";
23653
- async function spawnSibling(runtime, env, text2, agent, contextFiles = []) {
24350
+ async function spawnSibling(runtime, env, text2, agent, contextFiles = [], modelOptions = {}) {
23654
24351
  if (env.depth >= AGENT_MAX_DEPTH) {
23655
24352
  return agentFail("spawn_depth_exceeded", `depth ${env.depth} is already at the ceiling of ${AGENT_MAX_DEPTH}`);
23656
24353
  }
@@ -23678,6 +24375,7 @@ async function spawnSibling(runtime, env, text2, agent, contextFiles = []) {
23678
24375
  sessionId = (await runtime.spawnSession({
23679
24376
  directory: env.root,
23680
24377
  agent,
24378
+ ...modelOptions,
23681
24379
  initialPrompt: composeSpawnPrompt(text2, context.paths),
23682
24380
  environmentVariables: childAgentEnv(env, childId)
23683
24381
  })).sessionId;
@@ -23727,7 +24425,8 @@ async function runAgentCommand(runtime, args) {
23727
24425
  maxSpawn: env.maxSpawn,
23728
24426
  spawned,
23729
24427
  spawnsRemaining: spawned === null ? null : Math.max(0, env.maxSpawn - spawned),
23730
- canSpawn: env.depth < AGENT_MAX_DEPTH && (spawned === null || spawned < env.maxSpawn)
24428
+ canSpawn: env.depth < AGENT_MAX_DEPTH && spawned !== null && spawned < env.maxSpawn,
24429
+ spawnModelOptions: cliSpawnModelOptions()
23731
24430
  });
23732
24431
  }
23733
24432
  if (args.verb === "ls") {
@@ -23735,10 +24434,13 @@ async function runAgentCommand(runtime, args) {
23735
24434
  return agentOk({ sessions: sessions.filter((session) => isInScope(session, scope)).map(sessionSummary) });
23736
24435
  }
23737
24436
  if (args.verb === "spawn") {
24437
+ const agent = args.agent ?? DEFAULT_SPAWN_AGENT;
24438
+ const modelOptions = args.model !== void 0 || args.effort !== void 0 ? await validateSpawnModelOptions(agent, args.model, args.effort) : {};
24439
+ if (isFailure(modelOptions)) return modelOptions;
23738
24440
  if (args.handoff) {
23739
- return handoffToSibling(runtime, scope, env, args.handoff, args.agent ?? DEFAULT_SPAWN_AGENT);
24441
+ return handoffToSibling(runtime, scope, env, args.handoff, agent, modelOptions);
23740
24442
  }
23741
- return spawnSibling(runtime, env, args.text ?? "", args.agent ?? DEFAULT_SPAWN_AGENT, args.files ?? []);
24443
+ return spawnSibling(runtime, env, args.text ?? "", agent, args.files ?? [], modelOptions);
23742
24444
  }
23743
24445
  if (!args.sessionId) return agentFail("session_not_found", "no session id given");
23744
24446
  const target = await resolveTarget(runtime, scope, args.sessionId);
@@ -23763,6 +24465,17 @@ async function runAgentCommand(runtime, args) {
23763
24465
  return agentFail("session_not_found", `unsupported verb "${args.verb}"`);
23764
24466
  }
23765
24467
  }
24468
+ async function validateSpawnModelOptions(agent, model, effort) {
24469
+ try {
24470
+ await resolveCliNewConversationOptions({ agent, model, effort });
24471
+ return {
24472
+ ...model !== void 0 && model !== "default" ? { model } : {},
24473
+ ...effort !== void 0 ? { effort } : {}
24474
+ };
24475
+ } catch (error) {
24476
+ return agentFail("invalid_spawn_options", error instanceof Error ? error.message : String(error));
24477
+ }
24478
+ }
23766
24479
  function parseSpawnRecord(value2) {
23767
24480
  if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return null;
23768
24481
  const record2 = value2;
@@ -23770,13 +24483,16 @@ function parseSpawnRecord(value2) {
23770
24483
  }
23771
24484
  const LEDGER_FILE_MODE = 384;
23772
24485
  const PREVIEW_MAX_LENGTH = 200;
23773
- function agentLineagePath(homeDir) {
24486
+ function agentLineagePath(rootDir) {
24487
+ return join(rootDir, ".aplus", "agent-lineage.jsonl");
24488
+ }
24489
+ function legacyAgentLineagePath(homeDir) {
23774
24490
  return join(homeDir, ".saycode", "agent-lineage.jsonl");
23775
24491
  }
23776
- function readSpawnRecords(homeDir) {
24492
+ function readLedger(path) {
23777
24493
  let raw;
23778
24494
  try {
23779
- raw = readFileSync(agentLineagePath(homeDir), "utf8");
24495
+ raw = readFileSync(path, "utf8");
23780
24496
  } catch (error) {
23781
24497
  if (error.code === "ENOENT") return [];
23782
24498
  throw error;
@@ -23794,12 +24510,38 @@ function readSpawnRecords(homeDir) {
23794
24510
  }
23795
24511
  return records;
23796
24512
  }
23797
- function countSpawnsFor(homeDir, agentKey) {
23798
- return readSpawnRecords(homeDir).filter((record2) => record2.agentKey === agentKey).length;
24513
+ function recordIdentity(record2) {
24514
+ return JSON.stringify([
24515
+ record2.ts,
24516
+ record2.agentKey,
24517
+ record2.root,
24518
+ record2.depth,
24519
+ record2.childSessionId,
24520
+ record2.parentSessionId ?? null,
24521
+ record2.agent,
24522
+ record2.promptPreview,
24523
+ record2.outcome
24524
+ ]);
23799
24525
  }
23800
- function appendSpawnRecord(homeDir, record2) {
23801
- const path = agentLineagePath(homeDir);
23802
- mkdirSync(join(homeDir, ".saycode"), { recursive: true, mode: 448 });
24526
+ function readSpawnRecords(homeDir, rootDir) {
24527
+ const records = [
24528
+ ...readLedger(legacyAgentLineagePath(homeDir)),
24529
+ ...readLedger(agentLineagePath(rootDir))
24530
+ ];
24531
+ const seen = /* @__PURE__ */ new Set();
24532
+ return records.filter((record2) => {
24533
+ const identity = recordIdentity(record2);
24534
+ if (seen.has(identity)) return false;
24535
+ seen.add(identity);
24536
+ return true;
24537
+ });
24538
+ }
24539
+ function countSpawnsFor(homeDir, rootDir, agentKey) {
24540
+ return readSpawnRecords(homeDir, rootDir).filter((record2) => record2.agentKey === agentKey).length;
24541
+ }
24542
+ function appendSpawnRecord(rootDir, record2) {
24543
+ const path = agentLineagePath(rootDir);
24544
+ mkdirSync(join(rootDir, ".aplus"), { recursive: true, mode: 448 });
23803
24545
  const entry = { ...record2, promptPreview: record2.promptPreview.slice(0, PREVIEW_MAX_LENGTH) };
23804
24546
  appendFileSync(path, `${JSON.stringify(entry)}
23805
24547
  `, { mode: LEDGER_FILE_MODE });
@@ -23857,25 +24599,33 @@ async function fileSessionUnderProject(context, env, input) {
23857
24599
  if (!viaWorktree) return;
23858
24600
  await context.rest.assignSessionToProject(viaWorktree.projectId, input.sessionId, viaWorktree.worktreeId);
23859
24601
  }
23860
- function rejectedSteer(error) {
23861
- return {
23862
- accepted: false,
23863
- reason: /\bin[- ]flight\b/i.test(error) ? "in-flight" : "rejected",
23864
- error
23865
- };
23866
- }
23867
- async function steerAgentSession(transport, sessionId, text2) {
23868
- try {
23869
- const result = await transport.rpc(sessionId, "steer", { text: text2 });
23870
- if (result?.success === true) return { accepted: true };
23871
- const error = typeof result?.error === "string" ? result.error : "Session returned an invalid steer acknowledgement.";
23872
- return rejectedSteer(error);
23873
- } catch (error) {
23874
- return rejectedSteer(error instanceof Error ? error.message : String(error));
23875
- }
24602
+ const ACTIVITY_WAIT_TIMEOUT_MS = 5e3;
24603
+ function waitForResponseActivity(transport, sessionId) {
24604
+ return new Promise((resolve) => {
24605
+ let unsubscribe = () => {
24606
+ };
24607
+ const finish = (thinking) => {
24608
+ clearTimeout(timer);
24609
+ unsubscribe();
24610
+ resolve(thinking);
24611
+ };
24612
+ const timer = setTimeout(() => finish(null), ACTIVITY_WAIT_TIMEOUT_MS);
24613
+ unsubscribe = transport.onEphemeral((event) => {
24614
+ if (!event || typeof event !== "object") return;
24615
+ const activity = event;
24616
+ if (activity.type !== "activity" || activity.id !== sessionId || typeof activity.thinking !== "boolean") return;
24617
+ finish(activity.thinking);
24618
+ });
24619
+ });
23876
24620
  }
23877
24621
  async function stopAgentSession(transport, machineId, sessionId) {
23878
24622
  try {
24623
+ const thinking = await waitForResponseActivity(transport, sessionId);
24624
+ if (thinking !== true) return {
24625
+ stopped: false,
24626
+ reason: thinking === false ? "not-responding" : "rejected",
24627
+ error: thinking === false ? "Session has no active response." : "Could not confirm the session response state."
24628
+ };
23879
24629
  const result = await transport.rpc(machineId, "stop-session", {
23880
24630
  sessionId,
23881
24631
  source: "saycode-agent-stop",
@@ -23883,17 +24633,37 @@ async function stopAgentSession(transport, machineId, sessionId) {
23883
24633
  mode: "force"
23884
24634
  });
23885
24635
  if (result?.stopped === true) return { stopped: true };
23886
- return {
24636
+ if (result?.stopped === false) return {
23887
24637
  stopped: false,
23888
- error: typeof result?.reason === "string" ? result.reason : "Machine returned an invalid stop acknowledgement."
24638
+ reason: "not-responding",
24639
+ error: typeof result.reason === "string" ? result.reason : "Session has no active response."
23889
24640
  };
24641
+ return { stopped: false, reason: "rejected", error: "Machine returned an invalid stop acknowledgement." };
23890
24642
  } catch (error) {
23891
24643
  return {
23892
24644
  stopped: false,
24645
+ reason: "rejected",
23893
24646
  error: error instanceof Error ? error.message : String(error)
23894
24647
  };
23895
24648
  }
23896
24649
  }
24650
+ function rejectedSteer(error) {
24651
+ return {
24652
+ accepted: false,
24653
+ reason: /\bin[- ]flight\b/i.test(error) ? "in-flight" : "rejected",
24654
+ error
24655
+ };
24656
+ }
24657
+ async function steerAgentSession(transport, sessionId, text2) {
24658
+ try {
24659
+ const result = await transport.rpc(sessionId, "steer", { text: text2 });
24660
+ if (result?.success === true) return { accepted: true };
24661
+ const error = typeof result?.error === "string" ? result.error : "Session returned an invalid steer acknowledgement.";
24662
+ return rejectedSteer(error);
24663
+ } catch (error) {
24664
+ return rejectedSteer(error instanceof Error ? error.message : String(error));
24665
+ }
24666
+ }
23897
24667
  const STREAM_HISTORY_LIMIT = 50;
23898
24668
  const SPAWN_RPC_TIMEOUT_MS = 6e4;
23899
24669
  function isReadableFileSync(path) {
@@ -23975,7 +24745,7 @@ async function openStream(context, transport, sessionId) {
23975
24745
  }
23976
24746
  function createAgentRuntime(context, transport, env = process.env) {
23977
24747
  const keys = [decodeBase64(context.syncCredential.secret)];
23978
- const homeDir = context.homeDir;
24748
+ const homeDir = context.homeDir, lineageRoot = readAgentEnv(env)?.root ?? context.homeDir;
23979
24749
  const credentialHome = context.credential.source === "saycode" ? null : dirname(context.credential.path);
23980
24750
  return {
23981
24751
  env,
@@ -24005,6 +24775,8 @@ function createAgentRuntime(context, transport, env = process.env) {
24005
24775
  {
24006
24776
  directory: input.directory,
24007
24777
  agent: input.agent,
24778
+ ...input.model ? { model: input.model } : {},
24779
+ ...input.effort ? { effort: input.effort } : {},
24008
24780
  initialPrompt: input.initialPrompt,
24009
24781
  environmentVariables: input.environmentVariables
24010
24782
  },
@@ -24014,9 +24786,9 @@ function createAgentRuntime(context, transport, env = process.env) {
24014
24786
  return { sessionId: result.sessionId };
24015
24787
  },
24016
24788
  newAgentId: () => randomBytes(8).toString("hex"),
24017
- countSpawns: (agentKey) => countSpawnsFor(homeDir, agentKey),
24018
- recordSpawn: (record2) => appendSpawnRecord(homeDir, record2),
24019
- readLineage: () => readSpawnRecords(homeDir),
24789
+ countSpawns: (agentKey) => countSpawnsFor(homeDir, lineageRoot, agentKey),
24790
+ recordSpawn: (record2) => appendSpawnRecord(lineageRoot, record2),
24791
+ readLineage: () => readSpawnRecords(homeDir, lineageRoot),
24020
24792
  steerSession: (sessionId, text2) => steerAgentSession(transport, sessionId, text2),
24021
24793
  stopSession: (machineId, sessionId) => stopAgentSession(transport, machineId, sessionId),
24022
24794
  // Omitted entirely without API access: a Happy `access.key` is sync-only, so