@buzzni/saycode-cli 0.2.2 → 0.3.1

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 +22 -3
  2. package/index.mjs +923 -130
  3. package/package.json +1 -1
package/index.mjs CHANGED
@@ -16,7 +16,7 @@ import require$$3$1 from "net";
16
16
  import require$$4$1 from "tls";
17
17
  import require$$0$5 from "stream";
18
18
  import require$$0$3 from "zlib";
19
- import { readFileSync, mkdirSync, writeFileSync, rmSync, chmodSync, constants as constants$1, appendFileSync } from "node:fs";
19
+ import { readFileSync, mkdirSync, writeFileSync, rmSync, chmodSync, constants as constants$1, appendFileSync, accessSync, statSync } from "node:fs";
20
20
  import { dirname, join, basename, extname, delimiter } from "node:path";
21
21
  import { readFile, stat, mkdtemp, access, rm, mkdir, appendFile, writeFile, chmod } from "node:fs/promises";
22
22
  import { execFile, spawn } from "node:child_process";
@@ -40,11 +40,24 @@ function _mergeNamespaces(n, m) {
40
40
  }
41
41
  return Object.freeze(Object.defineProperty(n, Symbol.toStringTag, { value: "Module" }));
42
42
  }
43
- const version = "0.2.2";
43
+ const version = "0.3.1";
44
44
  const manifest = {
45
45
  version
46
46
  };
47
- const AGENT_VERBS = ["whoami", "ls", "read", "wait", "prompt", "spawn"];
47
+ const AGENT_CONTROL_VERBS = ["steer", "stop"];
48
+ function parseAgentControlArgs(verb, target, rest) {
49
+ if (verb === "steer") {
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();
52
+ return text2 ? { command: "agent", verb: "steer", sessionId: target, text: text2 } : { command: "error", message: "agent steer needs the text to send" };
53
+ }
54
+ if (verb === "stop") {
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" };
57
+ }
58
+ return null;
59
+ }
60
+ const AGENT_VERBS = ["whoami", "ls", "read", "wait", "prompt", "spawn", ...AGENT_CONTROL_VERBS];
48
61
  function fail(message) {
49
62
  return { command: "error", message };
50
63
  }
@@ -55,6 +68,8 @@ function parseAgentArgs(flags, positionals) {
55
68
  }
56
69
  const timeout = flags.timeoutMs !== void 0 ? { timeoutMs: flags.timeoutMs } : {};
57
70
  const target = rest[0] ?? flags.sessionId;
71
+ const control = parseAgentControlArgs(verb, target, rest);
72
+ if (control) return control;
58
73
  switch (verb) {
59
74
  case "whoami":
60
75
  return { command: "agent", verb: "whoami" };
@@ -85,10 +100,14 @@ function parseAgentArgs(flags, positionals) {
85
100
  };
86
101
  }
87
102
  case "spawn": {
103
+ const files = flags.files ?? [];
88
104
  if (flags.handoff) {
89
105
  if (flags.prompt !== void 0) {
90
106
  return fail("agent spawn --handoff sends the conversation, so it cannot also take --prompt");
91
107
  }
108
+ if (files.length > 0) {
109
+ return fail("agent spawn --handoff sends the conversation, so it cannot also take --file");
110
+ }
92
111
  const mode = flags.mode?.trim() || "summary";
93
112
  if (mode === "full") {
94
113
  return fail("agent spawn --handoff does not support --mode full yet; only summary is available");
@@ -104,8 +123,16 @@ function parseAgentArgs(flags, positionals) {
104
123
  }
105
124
  const text2 = flags.prompt?.trim();
106
125
  if (!text2) return fail("agent spawn needs --prompt <text>");
107
- return { command: "agent", verb: "spawn", text: text2, ...flags.agent ? { agent: flags.agent } : {} };
126
+ return {
127
+ command: "agent",
128
+ verb: "spawn",
129
+ text: text2,
130
+ ...flags.agent ? { agent: flags.agent } : {},
131
+ ...files.length > 0 ? { files } : {}
132
+ };
108
133
  }
134
+ default:
135
+ return fail(`unsupported agent verb: ${verb}`);
109
136
  }
110
137
  }
111
138
  function fencedMarkdownRanges(content) {
@@ -389,7 +416,17 @@ const agentBoardKo = {
389
416
  "agentBoard.autopilot.loopStopFailed": "반복을 감지했지만 세션 중지에 실패했습니다: {detail}",
390
417
  "agentBoard.autopilot.forceProceed": "그래도 진행",
391
418
  "agentBoard.autopilot.forceProceedTitle": "리뷰어가 완료를 명시적으로 선언하지 않았습니다. 그래도 Commit & PR을 진행합니다.",
392
- "agentBoard.autopilot.dismiss": "오토파일럿 기록 지우기"
419
+ "agentBoard.autopilot.dismiss": "오토파일럿 기록 지우기",
420
+ "agentBoard.verify.run": "변경 검증",
421
+ "agentBoard.verify.title": "프로젝트의 빌드·테스트·구동 레시피를 실행합니다.",
422
+ "agentBoard.verify.running": "검증 중 · {phase}",
423
+ "agentBoard.verify.passed": "검증 통과",
424
+ "agentBoard.verify.failed": "검증 실패",
425
+ "agentBoard.verify.phase.detect": "레시피 확인",
426
+ "agentBoard.verify.phase.bootstrap": "준비",
427
+ "agentBoard.verify.phase.build": "빌드",
428
+ "agentBoard.verify.phase.test": "테스트",
429
+ "agentBoard.verify.phase.start": "구동 확인"
393
430
  };
394
431
  const fanoutLifecycleKo = {
395
432
  "fanoutSection.restoreGroupAria": "병렬 그룹 복원: {title} · 실행 {run}",
@@ -585,8 +622,30 @@ const scheduledAutomationsKo = {
585
622
  "scheduledAutomations.scheduleDaily": "매일 지정 시각",
586
623
  "scheduledAutomations.intervalMinutesLabel": "실행 주기(분, 최소 {min}분)",
587
624
  "scheduledAutomations.dailyTimeLabel": "실행 시각",
588
- "scheduledAutomations.scriptLabel": "감시 스크립트 커맨드 (선택)",
625
+ "scheduledAutomations.monitorLabel": "변화 감지 커맨드 (선택)",
626
+ "scheduledAutomations.executionConditionLabel": "실행 조건",
627
+ "scheduledAutomations.executionEverySchedule": "예약마다 실행",
628
+ "scheduledAutomations.executionOnChange": "감시 결과가 바뀔 때만 실행",
629
+ "scheduledAutomations.advancedOptions": "고급 옵션 보기",
630
+ "scheduledAutomations.changeDetectionBadge": "변화 감지",
631
+ "scheduledAutomations.executionDetails": "실행 상세",
632
+ "scheduledAutomations.lastChange": "마지막 변화: {time}",
633
+ "scheduledAutomations.monitorOutputTitle": "최근 감시 출력",
634
+ "scheduledAutomations.monitorRequired": "변화 감지 명령을 입력하세요.",
635
+ "scheduledAutomations.templateLabel": "시작 템플릿",
636
+ "scheduledAutomations.templateNone": "직접 설정",
637
+ "scheduledAutomations.templateService": "서비스 상태 변화",
638
+ "scheduledAutomations.templateCommand": "명령 출력 변화",
639
+ "scheduledAutomations.templateHint": "템플릿은 폼만 채웁니다. 명령을 입력하고 확인한 뒤 직접 저장하세요.",
640
+ "scheduledAutomations.templateServiceName": "서비스 상태 감시",
641
+ "scheduledAutomations.templateCommandName": "명령 출력 감시",
642
+ "scheduledAutomations.templateServicePrompt": "서비스 상태가 어떻게 바뀌었는지 분석하고 필요한 조치를 제안해줘.",
643
+ "scheduledAutomations.templateCommandPrompt": "명령 출력의 변화를 분석하고 중요한 차이와 필요한 조치를 알려줘.",
644
+ "scheduledAutomations.monitorHint": "출력이 이전 성공 실행과 정확히 같으면 gate와 세션 실행을 생략합니다.",
645
+ "scheduledAutomations.notepadTitle": "자동화 메모장",
646
+ "scheduledAutomations.scriptLabel": "실행 gate 커맨드 (선택)",
589
647
  "scheduledAutomations.scriptHint": '마지막 줄이 {"wakeAgent": false} JSON이면 세션을 깨우지 않습니다.',
648
+ "scheduledAutomations.scriptHintFallback": '마지막 줄 JSON의 {"wakeAgent": false}는 세션을 깨우지 않고, {"notepad": {"key": "value"}}는 읽기 전용 메모장을 갱신합니다.',
590
649
  "scheduledAutomations.scriptWarning": "이 커맨드는 프로젝트 머신에서 사용자 권한으로 실행됩니다.",
591
650
  "scheduledAutomations.suppressSilentLabel": "응답이 [SILENT]면 알림 배지 생략",
592
651
  "scheduledAutomations.save": "저장",
@@ -600,6 +659,7 @@ const scheduledAutomationsKo = {
600
659
  "scheduledAutomations.nextRun": "다음 실행: {time}",
601
660
  "scheduledAutomations.historyTitle": "최근 실행",
602
661
  "scheduledAutomations.outcomeWoke": "세션 실행",
662
+ "scheduledAutomations.outcomeNoChange": "모니터 변화 없음(스킵)",
603
663
  "scheduledAutomations.outcomeSkippedGate": "변화 없음(스킵)",
604
664
  "scheduledAutomations.outcomeSkippedOverlap": "이전 실행 진행 중(스킵)",
605
665
  "scheduledAutomations.outcomeSilent": "이상 없음([SILENT])",
@@ -619,6 +679,7 @@ const scheduledAutomationsKo = {
619
679
  "scheduledAutomations.errorViewerKeyInUse": "기존 DB 자동화가 다른 암호화 키를 사용하고 있어 자동으로 전환할 수 없습니다. 기존 DB 자동화를 삭제한 뒤 다시 시도하세요.",
620
680
  "scheduledAutomations.errorWorkspaceMissing": "프로젝트 작업 폴더를 찾을 수 없습니다.",
621
681
  "scheduledAutomations.errorMachineOffline": "프로젝트 머신이 오프라인입니다.",
682
+ "scheduledAutomations.errorMonitorUnsupported": "대상 daemon이 변화 감지 자동화를 지원하지 않습니다. 연결 상태를 확인한 뒤 다시 시도하세요.",
622
683
  "scheduledAutomations.errorUnavailable": "자동화 서비스를 현재 사용할 수 없습니다.",
623
684
  "scheduledAutomations.runNow": "바로 실행",
624
685
  "scheduledAutomations.runRequestedNotice": "자동화 실행을 요청했습니다."
@@ -787,6 +848,9 @@ const personalChatsKo = {
787
848
  "personalChats.send": "전송",
788
849
  "personalChats.creating": "채팅을 시작하는 중…",
789
850
  "personalChats.noOnlineMachine": "온라인 머신이 없습니다. 채팅을 시작하려면 머신을 시작해주세요.",
851
+ "personalChats.localMachinePreparing": "이 컴퓨터를 Saycode에 연결하는 중이에요. 잠시 후 바로 시작할 수 있어요.",
852
+ "personalChats.localMachineAsk": "이 컴퓨터를 바로 사용할 수 있어요. 클릭 한 번으로 연결을 마치세요.",
853
+ "personalChats.localMachineFailed": "이 컴퓨터 자동 연결에 실패했어요. 다른 방법으로 연결할 수 있어요.",
790
854
  "personalChats.pickMachine": "이 채팅을 실행할 머신을 선택하세요",
791
855
  "personalChats.back": "뒤로",
792
856
  "personalChats.stop": "중지",
@@ -851,6 +915,44 @@ const agentLoopDetectionKo = {
851
915
  "settings.agentLoopAutoStop.help": "오토파일럿이 관리하는 세션이 같은 툴 호출이나 텍스트를 반복하며 멈춘 것으로 보이면 자동으로 응답을 중지합니다. 기본은 꺼짐 — 오탐으로 정상 작업을 멈추는 것이 놓치는 것보다 나쁩니다.",
852
916
  "settings.agentLoopAutoStop.saveFailed": "설정을 저장하지 못했습니다."
853
917
  };
918
+ const autopilotSelfReviewRoundsKo = {
919
+ "settings.autopilotSelfReviewRounds.label": "오토파일럿 셀프 리뷰 반복 횟수",
920
+ "settings.autopilotSelfReviewRounds.help": '오토파일럿이 "현재 에이전트로 점검"을 몇 번까지 이어서 시킬지 정합니다. 점검 결과에 아직 해결하지 못한 문제가 남아 있으면 남은 횟수만큼 다시 점검하게 하고, 문제가 없어지면 남은 횟수와 상관없이 바로 Commit & PR로 넘어갑니다. 지정한 횟수를 다 쓰고도 문제가 남으면 지금처럼 멈추고 사람에게 넘깁니다. 기본은 1회(지금까지와 동일).',
921
+ "settings.autopilotSelfReviewRounds.once": "1회 (반복 없음)",
922
+ "settings.autopilotSelfReviewRounds.times": "{count}회",
923
+ "settings.autopilotSelfReviewRounds.saveFailed": "설정을 저장하지 못했습니다.",
924
+ "agentBoard.autopilot.reviewingSelfRound": "오토파일럿 · 셀프 리뷰 중 {round}/{total}"
925
+ };
926
+ const externalWorktreeInboxKo = {
927
+ "externalWorktreeInbox.title": "앱 밖에서 만든 worktree",
928
+ "externalWorktreeInbox.description": "이 저장소에 앱이 만들지 않은 worktree 가 있습니다. 채택하면 목록과 새 대화 시작에서 고를 수 있어요.",
929
+ "externalWorktreeInbox.adopt": "채택",
930
+ "externalWorktreeInbox.ignore": "무시",
931
+ "externalWorktreeInbox.adoptAria": "외부 worktree {name} 채택",
932
+ "externalWorktreeInbox.ignoreAria": "외부 worktree {name} 무시",
933
+ "externalWorktreeInbox.externalBadge": "외부",
934
+ "externalWorktreeInbox.deleteBlocked": "앱 밖에서 만든 worktree 라 앱에서 삭제하지 않습니다.",
935
+ "externalWorktreeInbox.adoptRejectedStaleScan": "worktree 목록이 최신이 아닙니다. 새로고침한 뒤 다시 채택하세요.",
936
+ "externalWorktreeInbox.adoptRejectedNotOwned": "이 프로젝트 저장소의 worktree 가 아니라서 채택하지 않았습니다."
937
+ };
938
+ const externalSessionVaultKo = {
939
+ "externalSessionVault.openCommand": "앱 밖에서 시작한 대화 이어가기",
940
+ "externalSessionVault.dialogAria": "{name}의 앱 밖에서 시작한 대화",
941
+ "externalSessionVault.title": "앱 밖에서 시작한 대화",
942
+ "externalSessionVault.description": "이 프로젝트 폴더에서 터미널로 돌린 Claude Code 세션입니다. 이어가면 그 대화를 그대로 잇는 앱 대화가 시작됩니다.",
943
+ "externalSessionVault.closeAria": "닫기",
944
+ "externalSessionVault.loading": "앱 밖에서 시작한 대화를 찾는 중입니다…",
945
+ "externalSessionVault.empty": "이 프로젝트에서 앱 밖 대화를 찾지 못했습니다.",
946
+ "externalSessionVault.failed": "앱 밖에서 시작한 대화를 읽지 못했습니다.",
947
+ "externalSessionVault.untitled": "제목 없는 대화",
948
+ "externalSessionVault.resume": "이어가기",
949
+ "externalSessionVault.ignore": "무시",
950
+ "externalSessionVault.resumeAria": "{title} 대화 이어가기",
951
+ "externalSessionVault.ignoreAria": "{title} 대화 무시",
952
+ "externalSessionVault.rejectedStaleScan": "목록이 최신이 아닙니다. 새로고침한 뒤 다시 시도하세요.",
953
+ "externalSessionVault.rejectedNotOwned": "이 프로젝트 폴더에서 돌린 대화가 아니라서 이어가지 않았습니다.",
954
+ "externalSessionVault.resumeFailed": "그 대화를 이어가지 못했습니다."
955
+ };
854
956
  const operationModelRetryKo = {
855
957
  "review.job.fallbackModelUsed": "폴백 모델 사용됨: {model}"
856
958
  };
@@ -1184,9 +1286,84 @@ const sidebarUpdateKo = {
1184
1286
  "sidebarUpdate.retry": "업데이트 확인 실패 — 다시 시도",
1185
1287
  "sidebarUpdate.unsupported": "이 빌드에서는 자동 업데이트를 쓸 수 없습니다",
1186
1288
  "sidebarUpdate.currentVersion": "현재 v{version}",
1187
- "sidebarUpdate.newVersion": "v{version}"
1289
+ "sidebarUpdate.newVersion": "v{version}",
1290
+ "sidebarUpdate.installFailed": "업데이트가 적용되지 않았습니다 — v{expected} 로 바뀌었어야 하는데 여전히 v{actual} 입니다. 다시 확인"
1291
+ };
1292
+ const globalPauseKo = {
1293
+ "settings.globalPause.enable": "전역 일시정지 (새 자동 액션 차단)",
1294
+ "settings.globalPause.help": "켜면 예약 자동화 실행, GitHub 이벤트 트리거 발화, 보드 오토파일럿의 다음 단계가 시작되지 않습니다. 이미 진행 중인 대화는 중단되지 않으며, 끄면 보류된 작업이 다음 주기에 이어서 진행됩니다.",
1295
+ "settings.globalPause.scope": "이 스위치는 이 데스크탑 앱이 직접 실행하는 자동화에만 적용됩니다. 머신 데몬이 소유한 실행 경로는 막지 않습니다.",
1296
+ "settings.globalPause.saveFailed": "전역 일시정지를 전환하지 못했습니다."
1297
+ };
1298
+ const terminalFontKo = {
1299
+ "settings.terminalFont.cardTitle": "터미널 폰트",
1300
+ "settings.terminalFont.sizeLabel": "글자 크기",
1301
+ "settings.terminalFont.sizeHelp": "{min}~{max} 사이의 정수를 입력하세요.",
1302
+ "settings.terminalFont.sizeInvalid": "{min}~{max} 사이의 정수만 적용됩니다.",
1303
+ "settings.terminalFont.familyLabel": "폰트 이름",
1304
+ "settings.terminalFont.familyHelp": "CSS font-family 값입니다. 쉼표로 여러 폰트를 적으면 앞에서부터 사용할 수 있는 것을 씁니다. 비워 두면 기본 폰트로 돌아갑니다.",
1305
+ "settings.terminalFont.cardHelp": "이 기기의 원격 터미널에 적용됩니다. 열려 있는 터미널에도 재연결 없이 바로 반영됩니다.",
1306
+ "settings.terminalFont.reset": "기본값으로 되돌리기"
1307
+ };
1308
+ const subagentKo = {
1309
+ "subagent.title": "자식 에이전트 {count}개",
1310
+ "subagent.open": "자식 대화 열기",
1311
+ "subagent.status.responding": "응답 중",
1312
+ "subagent.status.waiting": "입력 대기",
1313
+ "subagent.status.completed": "완료",
1314
+ "subagent.status.stopped": "중단됨",
1315
+ "subagent.steer.placeholder": "현재 턴에 전달할 지시",
1316
+ "subagent.steer.action": "지시 전달",
1317
+ "subagent.stop.action": "중단",
1318
+ "subagent.steer.requested": "전달 요청됨 · 다음 tool 경계에서 적용 예정",
1319
+ "subagent.steer.applied": "지시가 자식 대화에 적용됨",
1320
+ "subagent.steer.missed": "지시가 적용되지 못한 채 턴이 끝남",
1321
+ "subagent.error.notOwned": "이 대화에서 제어할 수 없는 세션입니다.",
1322
+ "subagent.error.empty": "전달할 지시를 입력하세요.",
1323
+ "subagent.error.inFlight": "이전 제어 요청을 처리하는 중입니다.",
1324
+ "subagent.error.rejected": "요청이 거부됐습니다: {detail}"
1188
1325
  };
1189
- 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 };
1326
+ const aiCommitPrKo = {
1327
+ "aiCommitPr.stagedCount": "스테이지된 파일 {count}개",
1328
+ "aiCommitPr.noStaged": "스테이지된 변경이 없습니다.",
1329
+ "aiCommitPr.stageAll": "모든 변경 스테이지",
1330
+ "aiCommitPr.staging": "스테이지하는 중…",
1331
+ "aiCommitPr.messagePlaceholder": "커밋 메시지",
1332
+ "aiCommitPr.messageAria": "커밋 메시지",
1333
+ "aiCommitPr.stagedListAria": "스테이지된 파일 목록",
1334
+ "aiCommitPr.commit": "커밋",
1335
+ "aiCommitPr.committing": "커밋하는 중…",
1336
+ "aiCommitPr.committed": "커밋했습니다 ({hash})",
1337
+ "aiCommitPr.generate": "AI 생성",
1338
+ "aiCommitPr.cancelGenerate": "생성 취소",
1339
+ "aiCommitPr.overwriteConfirm": "이미 적어둔 메시지를 AI가 만든 것으로 바꿀까요?",
1340
+ "aiCommitPr.error.nothingStaged": "스테이지된 변경이 없어 커밋할 수 없습니다.",
1341
+ "aiCommitPr.error.commitFailed": "커밋하지 못했습니다.",
1342
+ "aiCommitPr.error.claudeMissing": "이 컴퓨터에서 claude 명령을 찾지 못했습니다.",
1343
+ "aiCommitPr.error.generationFailed": "메시지를 만들지 못했습니다.",
1344
+ "aiCommitPr.error.timedOut": "생성이 너무 오래 걸려 중단했습니다.",
1345
+ "aiCommitPr.error.nothingToDescribe": "설명할 변경이 없습니다.",
1346
+ "aiCommitPr.error.emptyOutput": "생성 결과가 비어 있습니다.",
1347
+ "aiCommitPr.error.machineOffline": "이 컴퓨터가 오프라인입니다.",
1348
+ "aiCommitPr.error.noResponse": "응답을 받지 못했습니다.",
1349
+ "aiCommitPr.error.cancelled": "생성을 취소했습니다.",
1350
+ "aiCommitPr.error.daemonBusy": "머신이 다른 작업으로 바쁩니다. 잠시 후 다시 시도하세요.",
1351
+ "aiCommitPr.error.commitNoResponse": "응답을 받지 못했습니다. 커밋이 완료됐을 수도 있으니 패널을 다시 열어 확인하세요.",
1352
+ "aiCommitPr.error.gitFailed": "git 명령이 실패했습니다.",
1353
+ "aiCommitPr.error.stagedProbeFailed": "스테이지된 변경을 읽지 못했습니다.",
1354
+ "aiCommitPr.error.stageFailed": "변경을 스테이지하지 못했습니다.",
1355
+ "aiCommitPr.settings.enable": "커밋 메시지와 PR 설명을 AI로 생성",
1356
+ "aiCommitPr.settings.help": "켜면 git 패널과 PR 만들기 화면에 “AI 생성” 버튼이 나타납니다. 생성은 이 컴퓨터에서 한 번 실행되며, 결과는 입력란에 채워질 뿐 자동으로 커밋하거나 PR을 만들지 않습니다. 기본은 꺼짐.",
1357
+ "aiCommitPr.settings.saveFailed": "설정을 저장하지 못했습니다.",
1358
+ "aiCommitPr.pr.dialogTitle": "Pull Request 만들기",
1359
+ "aiCommitPr.pr.dialogAria": "{branch} 브랜치의 Pull Request 내용 작성",
1360
+ "aiCommitPr.pr.titleLabel": "제목",
1361
+ "aiCommitPr.pr.bodyLabel": "설명",
1362
+ "aiCommitPr.pr.base": "{base} 브랜치로 병합",
1363
+ "aiCommitPr.pr.create": "Pull Request 만들기",
1364
+ "aiCommitPr.pr.creating": "만드는 중…"
1365
+ };
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 };
1190
1367
  const onboardingKo = {
1191
1368
  "onboarding.dialogAria": "시작 설정",
1192
1369
  "onboarding.eyebrow": "처음 시작하기",
@@ -1208,6 +1385,14 @@ const onboardingKo = {
1208
1385
  "onboarding.oneCommand.copy": "명령어 복사",
1209
1386
  "onboarding.oneCommand.copied": "복사됨",
1210
1387
  "onboarding.oneCommand.advanced": "고급: 수동 설치",
1388
+ "onboarding.localMachine.registering": "이 컴퓨터를 등록하는 중…",
1389
+ "onboarding.localMachine.registered": "✓ 이 컴퓨터가 등록되었습니다. 바로 작업할 수 있어요.",
1390
+ "onboarding.localMachine.askTitle": "이 컴퓨터를 바로 사용",
1391
+ "onboarding.localMachine.askBody": "이 컴퓨터에 이미 Happy 설정이 있습니다. 지금 설정으로 바꾸기 전에 원래 파일을 백업해 두고, 실패하면 되돌립니다.",
1392
+ "onboarding.localMachine.askAction": "이 컴퓨터를 바로 사용",
1393
+ "onboarding.localMachine.failed": "자동 등록에 실패했습니다. 아래 명령으로 직접 설정할 수 있어요.",
1394
+ "onboarding.localMachine.retry": "다시 시도",
1395
+ "onboarding.localMachine.remoteHint": "다른 컴퓨터나 조직 공유 머신은 머신 설정에서 추가할 수 있어요.",
1211
1396
  "onboarding.machineDetected": "✓ {name} 연결됨",
1212
1397
  "onboarding.standalone.title": "내장 서버와 로컬 머신",
1213
1398
  "onboarding.standalone.body": "앱이 서버와 로컬 실행 머신을 자동으로 준비합니다. 별도 서버 명령은 필요하지 않습니다.",
@@ -1270,10 +1455,10 @@ const previewTunnelKo = {
1270
1455
  };
1271
1456
  const ko = {
1272
1457
  // Common actions
1273
- "common.save": "Save",
1274
- "common.reset": "Reset",
1275
- "common.cancel": "Cancel",
1276
- "common.close": "Close",
1458
+ "common.save": "저장",
1459
+ "common.reset": "초기화",
1460
+ "common.cancel": "취소",
1461
+ "common.close": "닫기",
1277
1462
  "happyCliUpdate.title": "Happy CLI 업데이트가 있습니다",
1278
1463
  "happyCliUpdate.checkTitle": "Happy CLI 업데이트 확인",
1279
1464
  "happyCliUpdate.managerTitle": "Happy CLI 업데이트 관리",
@@ -1307,30 +1492,42 @@ const ko = {
1307
1492
  "language.select.subtitle": "앱에서 사용할 언어를 선택하세요. 나중에 설정에서 바꿀 수 있습니다.",
1308
1493
  // Login screen
1309
1494
  "login.lead": "모바일 앱의 프로젝트, 대화, 문서 흐름을 데스크톱 작업 화면으로 옮긴 Electron 클라이언트입니다.",
1310
- "login.secureWorkspace": "Secure Workspace",
1311
- "login.description": "saycode.ai 계정으로 로그인하거나 데모 워크스페이스를 열어 로컬에서 검토하세요.",
1495
+ "login.stepsTitle": "시작 방법",
1496
+ "login.step1Title": "시작 방식 선택",
1497
+ "login.step1Body": "계정 없이 이 컴퓨터에서, 또는 Saycode 계정으로 시작해요.",
1498
+ "login.step2Title": "이 컴퓨터 자동 연결",
1499
+ "login.step2Body": "터미널 없이 이 컴퓨터가 자동으로 준비돼요. 로그인하면 개인 머신으로 등록돼요.",
1500
+ "login.step3Title": "바로 대화 시작",
1501
+ "login.step3Body": "프로젝트를 열고 첫 메시지를 보내면 끝이에요.",
1502
+ "login.localCardTitle": "이 컴퓨터에서 바로 시작",
1503
+ "login.localCardBody": "계정 없이 로컬 전용(Standalone) 모드로 시작해요.",
1504
+ "login.localCardAction": "계정 없이 시작하기",
1505
+ "login.localDataNotice": "데이터는 이 컴퓨터에만 저장되며 로그인 계정과는 별개예요.",
1506
+ "login.localUnavailable": "이 빌드에는 로컬 전용 런타임이 포함되어 있지 않아요.",
1507
+ "login.cloudCardTitle": "Saycode 계정으로 로그인",
1508
+ "login.cloudCardBody": "팀 공유, 원격 머신, 커뮤니티까지 모든 기능을 사용해요.",
1509
+ "login.localDataStaysNotice": "로컬 전용 모드의 데이터는 이 컴퓨터에 남아 있어요. 로그인 계정과는 별개예요.",
1312
1510
  "login.email": "이메일",
1313
1511
  "login.password": "비밀번호",
1314
1512
  "login.passwordPlaceholder": "비밀번호",
1315
- "login.loggingIn": "Logging in...",
1513
+ "login.loggingIn": "로그인 중...",
1316
1514
  "login.login": "로그인",
1317
1515
  "login.demoWorkspace": "데모 워크스페이스",
1318
1516
  "login.standaloneStarting": "로컬 서버 시작 중...",
1319
- "login.standalone": "로컬 전용 모드 (Standalone)",
1320
1517
  "login.signupPrompt": "처음이신가요?",
1321
1518
  "login.signupLink": "saycode.ai에서 계정 만들기",
1322
1519
  "login.orgJoinHint": "회사 조직에 참여하려면 계정을 만든 뒤 saycode.ai에서 초대 코드를 입력하거나 관리자가 보낸 초대를 수락하세요. 그다음 같은 계정으로 여기에 로그인합니다.",
1323
1520
  // Settings — Language tab
1324
- "settings.tab.language": "Language",
1521
+ "settings.tab.language": "언어",
1325
1522
  "settings.language.help": "앱의 모든 화면에 사용할 언어를 선택합니다.",
1326
1523
  "settings.language.current": "현재 적용: {language}",
1327
1524
  "settings.language.updated": "언어가 변경되었습니다.",
1328
1525
  "settings.language.loadFailed": "언어 설정을 불러오지 못했습니다.",
1329
1526
  "settings.language.saveFailed": "언어 설정을 저장하지 못했습니다.",
1330
1527
  // Sidebar / workspace chrome
1331
- "account.unknownUser": "Unknown",
1332
- "account.demoWorkspace": "Demo workspace",
1333
- "account.connectedWorkspace": "Connected workspace",
1528
+ "account.unknownUser": "알 수 없음",
1529
+ "account.demoWorkspace": "데모 워크스페이스",
1530
+ "account.connectedWorkspace": "연결된 워크스페이스",
1334
1531
  "account.syncing": "캐시된 워크스페이스를 최신 정보로 동기화하는 중입니다.",
1335
1532
  "sidebar.newProject": "새 프로젝트",
1336
1533
  "sidebar.modeChats": "대화",
@@ -1372,6 +1569,32 @@ const ko = {
1372
1569
  "pendingConversation.failed": "이 대화를 시작하지 못했습니다.",
1373
1570
  "pendingConversation.retry": "다시 시도",
1374
1571
  "newProject.title": "새 프로젝트",
1572
+ "machineDisplayName.dialogAria": "머신 이름 변경",
1573
+ "machineDisplayName.title": "머신 이름",
1574
+ "machineDisplayName.closeAria": "머신 이름 편집 닫기",
1575
+ "machineDisplayName.label": "표시명",
1576
+ "machineDisplayName.placeholder": "예: 작업 서버",
1577
+ "machineDisplayName.hint": "비워두고 저장하면 원래 host 이름으로 표시합니다.",
1578
+ "machineDisplayName.clear": "이름 지우기",
1579
+ "machineDisplayName.saving": "저장 중...",
1580
+ "newProject.eyebrow": "시작",
1581
+ "newProject.contextOrganization": "조직 컨텍스트",
1582
+ "newProject.contextPersonal": "개인 컨텍스트",
1583
+ "newProject.closeAria": "새 프로젝트 닫기",
1584
+ "newProject.sourceAria": "프로젝트 소스",
1585
+ "newProject.sourceBlank": "빈 프로젝트",
1586
+ "newProject.sourceFolder": "폴더",
1587
+ "newProject.nameLabel": "프로젝트 이름",
1588
+ "newProject.existingFolderLabel": "기존 폴더",
1589
+ "newProject.choose": "선택",
1590
+ "newProject.summaryBlank": "빈 동기화 프로젝트를 만듭니다.",
1591
+ "newProject.summaryGit": "프로젝트를 만들고 선택한 머신에서 Git 가져오기를 시작합니다.",
1592
+ "newProject.summaryZip": "프로젝트를 만들고 선택한 머신에서 선택한 ZIP/파일을 가져옵니다.",
1593
+ "newProject.summaryExistingFolder": "기존 절대 경로 폴더를 동기화 프로젝트로 등록합니다.",
1594
+ "newProject.creating": "만드는 중...",
1595
+ "newProject.createAction": "프로젝트 만들기",
1596
+ "newProject.importAction": "프로젝트 가져오기",
1597
+ "newProject.gitUrlLabel": "Git 저장소 주소",
1375
1598
  "newProject.importTab": "Zip/파일 가져오기",
1376
1599
  "newProject.importSection": "Zip/파일 가져오기",
1377
1600
  "newProject.importPickAria": "Zip/파일 선택",
@@ -1403,7 +1626,7 @@ const ko = {
1403
1626
  "upload.failed": "파일을 업로드하지 못했습니다.",
1404
1627
  "upload.machineOffline": "선택한 머신이 오프라인입니다.",
1405
1628
  "workspace.label": "작업공간",
1406
- "workspace.contextAria": "Workspace context",
1629
+ "workspace.contextAria": "워크스페이스 컨텍스트",
1407
1630
  "workspace.personal": "개인 작업공간",
1408
1631
  "workspace.personalProjects": "개인 프로젝트",
1409
1632
  "workspace.orgHome": "조직 홈",
@@ -1664,6 +1887,11 @@ const ko = {
1664
1887
  "orgAdmin.vault.machineNotConfigured": "인증이 적용되지 않음",
1665
1888
  "orgAdmin.vault.captureSucceeded": "인증 세트를 저장했습니다.",
1666
1889
  "orgAdmin.vault.deploySucceeded": "머신 {count}대에 적용했습니다.",
1890
+ "myAiCredentials.title": "저장된 AI 인증 세트 적용",
1891
+ "myAiCredentials.description": "조직에 저장된 AI 인증 세트를 내 개인 머신이나 내가 등록한 공용 머신에 적용합니다. 인증 세트를 새로 저장하거나 삭제하려면 조직 관리자에게 문의하세요.",
1892
+ "myAiCredentials.noMachines": "적용할 수 있는 머신이 없습니다. 개인 머신을 등록하거나, 공용 머신을 직접 등록한 경우에만 여기에 나타납니다.",
1893
+ "myAiCredentials.targetMachines": "적용할 내 머신",
1894
+ "myAiCredentials.deploySucceeded": "{machines}에 인증 세트를 적용했습니다.",
1667
1895
  "orgAdmin.vault.deleteSucceeded": "인증 세트를 삭제했습니다.",
1668
1896
  "orgAdmin.vault.rotationStarted": "자동 회전을 시작했습니다.",
1669
1897
  "orgAdmin.vault.rotationStopped": "자동 회전을 중지했습니다.",
@@ -1685,20 +1913,20 @@ const ko = {
1685
1913
  "session.newConversation": "새 대화",
1686
1914
  "session.lastConversation": "최근 대화 {time}",
1687
1915
  // Settings dialog chrome
1688
- "settings.eyebrow": "Preferences",
1689
- "settings.title": "Desktop settings",
1690
- "settings.close": "Close settings",
1916
+ "settings.eyebrow": "환경설정",
1917
+ "settings.title": "데스크톱 설정",
1918
+ "settings.close": "설정 닫기",
1691
1919
  // Settings — Notifications tab
1692
- "settings.notifications.loading": "Loading notification settings...",
1693
- "settings.notifications.mobilePush": "Mobile completion push",
1694
- "settings.notifications.updated": "Notification settings saved.",
1920
+ "settings.notifications.loading": "알림 설정을 불러오는 중...",
1921
+ "settings.notifications.mobilePush": "모바일 완료 푸시",
1922
+ "settings.notifications.updated": "알림 설정을 저장했습니다.",
1695
1923
  "settings.notifications.loadFailed": "알림 설정을 불러오지 못했습니다.",
1696
1924
  "settings.notifications.saveFailed": "알림 설정을 저장하지 못했습니다.",
1697
1925
  // Settings — Visual effects tab
1698
1926
  "settings.visualEffects.loadFailed": "시각 효과 설정을 불러오지 못했습니다.",
1699
1927
  "settings.visualEffects.saveFailed": "시각 효과 설정을 저장하지 못했습니다.",
1700
- "settings.visualEffects.updated": "Visual effects updated.",
1701
- "settings.visualEffects.loading": "Loading visual effects...",
1928
+ "settings.visualEffects.updated": "시각 효과를 업데이트했습니다.",
1929
+ "settings.visualEffects.loading": "시각 효과를 불러오는 중...",
1702
1930
  "settings.visualEffects.help": "저사양 환경에서는 흐림, 큰 그림자와 장식 애니메이션을 줄여 스크롤과 창 이동을 부드럽게 합니다.",
1703
1931
  "settings.visualEffects.auto": "자동",
1704
1932
  "settings.visualEffects.full": "전체 효과",
@@ -1706,7 +1934,7 @@ const ko = {
1706
1934
  "settings.visualEffects.currentAuto": "현재 적용: 자동 ({result})",
1707
1935
  "settings.visualEffects.current": "현재 적용: {result}",
1708
1936
  // Project list / sidebar sections
1709
- "project.ownershipAria": "Project ownership",
1937
+ "project.ownershipAria": "프로젝트 소유",
1710
1938
  "project.mine": "내 프로젝트",
1711
1939
  "project.shared": "공유받은 프로젝트",
1712
1940
  "project.recent": "최근 프로젝트",
@@ -1715,7 +1943,7 @@ const ko = {
1715
1943
  "project.backToList": "프로젝트 목록으로",
1716
1944
  "project.allProjects": "모든 프로젝트",
1717
1945
  "project.navigationAria": "프로젝트 위치",
1718
- "project.listAria": "Projects",
1946
+ "project.listAria": "프로젝트",
1719
1947
  "project.emptyMineTitle": "내 프로젝트가 없습니다",
1720
1948
  "project.emptySharedTitle": "공유받은 프로젝트가 없습니다",
1721
1949
  "project.emptySearchHint": "검색 조건에 맞는 프로젝트가 없습니다.",
@@ -1723,7 +1951,7 @@ const ko = {
1723
1951
  "sidebar.clickTarget": "프로젝트 클릭 시: {target} 에 열림",
1724
1952
  "sidebar.newWindow": "새 창",
1725
1953
  "sidebar.settings": "환경설정",
1726
- "sidebar.logout": "Logout",
1954
+ "sidebar.logout": "로그아웃",
1727
1955
  // Content pane / unified workspace
1728
1956
  "runningIndicator.title": "실행 중인 작업 열기",
1729
1957
  "runningIndicator.label": "실행 중 {count}",
@@ -1746,13 +1974,13 @@ const ko = {
1746
1974
  "panel.recovery.demoDesc": "Demo workspace에서는 네트워크 연결을 열지 않습니다.",
1747
1975
  "panel.recovery.loginDesc": "로그인 후 터미널을 시작할 수 있습니다.",
1748
1976
  // Chat composer
1749
- "composer.messageAria": "Message",
1977
+ "composer.messageAria": "메시지",
1750
1978
  "composer.placeholder": "데스크톱에서 보낼 메시지를 입력",
1751
1979
  "composer.promptSuggestionPaused": "{window} 사용량 {percent}% — 사용량 경고 중에는 Claude가 추천 질문을 만들지 않습니다",
1752
1980
  "composer.promptSuggestionPausedReset": "{reset} 초기화",
1753
- "composer.starting": "Starting...",
1754
- "composer.sending": "Sending...",
1755
- "composer.working": "Working...",
1981
+ "composer.starting": "시작 중...",
1982
+ "composer.sending": "전송 중...",
1983
+ "composer.working": "작업 중...",
1756
1984
  "composer.queue.limitReached": "대기열이 가득 찼습니다(최대 3개)",
1757
1985
  "composer.queue.title": "대기 중인 요청",
1758
1986
  "composer.queue.edit": "편집",
@@ -1760,11 +1988,11 @@ const ko = {
1760
1988
  "composer.queue.steerNow": "즉시 요청",
1761
1989
  "composer.queue.add": "대기열에 추가",
1762
1990
  "composer.queue.editBlocked": "입력창을 비운 뒤 편집할 수 있습니다",
1763
- "composer.send": "Send",
1991
+ "composer.send": "보내기",
1764
1992
  "composer.stopping": "중단 중...",
1765
1993
  "composer.stop": "중단",
1766
1994
  // Common (shared)
1767
- "common.refresh": "Refresh",
1995
+ "common.refresh": "새로고침",
1768
1996
  // Project worktree dialog
1769
1997
  "worktreeDialog.listFailed": "worktree 목록을 불러오지 못했습니다.",
1770
1998
  "worktreeDialog.startFailed": "새 대화를 시작하지 못했습니다.",
@@ -1783,9 +2011,9 @@ const ko = {
1783
2011
  "worktreeDialog.delete": "삭제",
1784
2012
  "worktreeDialog.forceDelete": "강제 삭제",
1785
2013
  "worktreeDialog.cancel": "취소",
1786
- "worktreeDialog.dialogAria": "{name} worktrees",
1787
- "worktreeDialog.closeAria": "Close worktree dialog",
1788
- "worktreeDialog.listAria": "{name} worktree list",
2014
+ "worktreeDialog.dialogAria": "{name} 워크트리",
2015
+ "worktreeDialog.closeAria": "워크트리 다이얼로그 닫기",
2016
+ "worktreeDialog.listAria": "{name} 워크트리 목록",
1789
2017
  "worktreeDialog.loading": "Worktree 목록을 불러오는 중입니다.",
1790
2018
  "worktreeDialog.empty": "이 프로젝트에는 관리 중인 worktree가 없습니다.",
1791
2019
  "worktreeDialog.rowAria": "worktree {name}",
@@ -1793,6 +2021,13 @@ const ko = {
1793
2021
  "worktreeDialog.startSessionTitle": "이 worktree에서 새 대화를 시작합니다 (새 worktree를 만들지 않습니다)",
1794
2022
  "worktreeDialog.startSession": "이 worktree에서 새 대화",
1795
2023
  "worktreeDialog.removeAria": "{name} 삭제",
2024
+ "worktreePathCopy.copy": "worktree 경로 복사",
2025
+ "worktreePathCopy.copied": "worktree 경로 복사됨",
2026
+ "worktreePathCopy.failed": "worktree 경로 복사 실패",
2027
+ "sessionWorktree.worktree": "워크트리: {name}",
2028
+ "sessionWorktree.branch": "브랜치: {branch}",
2029
+ "sessionWorktree.path": "경로: {path}",
2030
+ "sessionWorktree.merged": "병합됨",
1796
2031
  // Project git controls
1797
2032
  "gitControls.branchLoading": "Git branch 확인 중",
1798
2033
  "gitControls.noRemote": "origin remote 또는 branch를 찾을 수 없습니다.",
@@ -1801,12 +2036,13 @@ const ko = {
1801
2036
  "gitControls.manageBranchAria": "{branch} branch 관리",
1802
2037
  "gitControls.manageBranchTitle": "Branch 변경 및 생성",
1803
2038
  "gitControls.manageBranchTitleWithDesc": "Branch 변경 및 생성 · {description}",
1804
- "gitControls.pullAria": "{branch} branch pull",
2039
+ "gitControls.pullAria": "{branch} 브랜치 pull",
1805
2040
  "gitControls.pullTitle": "{branch} pull",
1806
- "gitControls.pushAria": "{branch} branch push",
2041
+ "gitControls.pushAria": "{branch} 브랜치 push",
1807
2042
  "gitControls.pushTitle": "{branch} push",
1808
2043
  "gitControls.manageWorktreeAria": "{name} worktree 관리",
1809
2044
  "gitControls.manageWorktreeTitle": "Worktree 관리",
2045
+ "gitControls.operationResultDialogTitle": "{label} 결과",
1810
2046
  // Fan-out group section
1811
2047
  "fanoutSection.groupsAria": "병렬 실행 그룹",
1812
2048
  "fanoutSection.openGroupAria": "병렬 그룹 열기: {title} · 실행 {run}",
@@ -2340,6 +2576,11 @@ const ko = {
2340
2576
  "git.branchNameRequired": "생성할 Git branch 이름을 입력해주세요.",
2341
2577
  "git.noBranchCreateResponse": "머신이 Git branch 생성 응답을 반환하지 않았습니다.",
2342
2578
  "git.branchCreateFailed": "Git branch 생성에 실패했습니다.",
2579
+ "git.branchNameUnchanged": "바꿀 Git branch 이름이 지금과 같습니다.",
2580
+ "git.noBranchRenameResponse": "머신이 Git branch 이름 변경 응답을 반환하지 않았습니다.",
2581
+ "git.branchRenameFailed": "Git branch 이름 변경에 실패했습니다.",
2582
+ "git.noBranchDeleteResponse": "머신이 Git branch 삭제 응답을 반환하지 않았습니다.",
2583
+ "git.branchDeleteFailed": "Git branch 삭제에 실패했습니다.",
2343
2584
  "git.repoRootFailed": "Git 저장소 루트를 확인하지 못했습니다.",
2344
2585
  "git.worktreeCreateFailed": "Git worktree 생성에 실패했습니다.",
2345
2586
  "git.diffPathNotAllowed": "worktree diff 는 {segment} 하위 경로만 허용됩니다: {path}",
@@ -2507,6 +2748,25 @@ const ko = {
2507
2748
  "standalone.startFailed": "로컬 standalone 서버를 시작하지 못했습니다.",
2508
2749
  "workspace.loadFailed": "워크스페이스를 불러오지 못했습니다.",
2509
2750
  "standalone.modeStartFailed": "로컬 standalone 모드를 시작하지 못했습니다.",
2751
+ "modeSwitch.toLocal": "로컬 전용 모드로 전환",
2752
+ "modeSwitch.toCloud": "계정으로 돌아가기",
2753
+ "standaloneImport.title": "로컬 전용 데이터 가져오기",
2754
+ "standaloneImport.introBody": "이 컴퓨터의 로컬 전용(Standalone) 모드 데이터(프로젝트·대화 기록)를 지금 로그인한 계정으로 복사해요. 원본은 그대로 남아요.",
2755
+ "standaloneImport.listOrderNotice": "가져온 대화 내용의 시각은 유지되지만, 세션 목록에서는 가져온 시점에 모여 보여요.",
2756
+ "standaloneImport.start": "가져오기 시작",
2757
+ "standaloneImport.later": "나중에",
2758
+ "standaloneImport.close": "닫기",
2759
+ "standaloneImport.running": "가져오는 중... ({completed}/{total} 세션)",
2760
+ "standaloneImport.machineNotReady": "이 컴퓨터의 개인 머신 등록이 끝난 뒤 다시 시도해주세요.",
2761
+ "standaloneImport.doneSummary": "프로젝트 {projects}개, 세션 {sessions}개를 가져왔어요.",
2762
+ "standaloneImport.doneSkipped": "이미 가져온 세션 {count}개는 건너뛰었어요.",
2763
+ "standaloneImport.undecryptable": "복호화할 수 없어 건너뛴 메시지가 {count}개 있어요.",
2764
+ "standaloneImport.undecryptableMetadata": "세션 {count}개의 메타데이터 또는 에이전트 상태를 복호화하지 못해 일부 상태 없이 가져왔어요.",
2765
+ "standaloneImport.failedProjects": "프로젝트 {count}개를 가져오지 못했어요. 가능한 세션은 프로젝트 연결 없이 가져왔어요.",
2766
+ "standaloneImport.failedSessions": "세션 {count}개를 가져오지 못했어요. 다시 실행하면 이어서 시도해요.",
2767
+ "standaloneImport.fatalError": "가져오기를 시작하지 못했어요: {error}",
2768
+ "standaloneImport.settingsBody": "이 컴퓨터의 로컬 전용 모드 데이터를 현재 계정으로 복사해요.",
2769
+ "standaloneImport.settingsOpen": "열기",
2510
2770
  // Workspace / project / file flow toasts + errors
2511
2771
  "workspace.contextSwitchFailed": "워크스페이스 컨텍스트를 전환하지 못했습니다.",
2512
2772
  "error.loginRequired": "로그인이 필요합니다.",
@@ -2659,6 +2919,8 @@ const ko = {
2659
2919
  "gitOp.failed": "{label} 실패",
2660
2920
  "gitOp.branchChanged": "Branch 변경 완료",
2661
2921
  "gitOp.branchCreated": "Branch 생성 완료",
2922
+ "gitOp.branchRenamed": "Branch 이름 변경 완료",
2923
+ "gitOp.branchDeleted": "Branch 삭제 완료",
2662
2924
  "worktreeOp.noGithub": "GitHub 저장소가 연결돼 있지 않습니다.",
2663
2925
  "worktreeOp.pushingBranch": "브랜치 푸시 중...",
2664
2926
  "worktreeOp.creatingPr": "Pull request 생성 중...",
@@ -2788,6 +3050,20 @@ const ko = {
2788
3050
  "preview.tokenUrlInvalid": "Preview 토큰 URL이 올바르지 않습니다.",
2789
3051
  "branchDialog.loading": "Branch 목록을 불러오는 중입니다.",
2790
3052
  "branchDialog.empty": "표시할 branch가 없습니다.",
3053
+ "branchDialog.actionsAria": "{name} 브랜치 작업",
3054
+ "branchDialog.rowAria": "{name} {meta}",
3055
+ "branchDialog.switch": "브랜치 이동",
3056
+ "branchDialog.rename": "브랜치 이름 변경",
3057
+ "branchDialog.delete": "브랜치 삭제",
3058
+ "branchDialog.switchConfirm": "'{name}' 브랜치로 이동할까요? 커밋되지 않은 변경이 있으면 Git이 거부할 수 있습니다.",
3059
+ "branchDialog.switchConfirmAction": "이동",
3060
+ "branchDialog.renameConfirm": "'{from}' 브랜치의 이름을 바꿀까요?",
3061
+ "branchDialog.renamePlaceholder": "새 브랜치 이름",
3062
+ "branchDialog.renameInputAria": "{from} 새 이름",
3063
+ "branchDialog.renameConfirmAction": "이름 변경",
3064
+ "branchDialog.deleteConfirm": "'{name}' 브랜치를 삭제할까요? 머지되지 않은 커밋이 있으면 Git이 거부합니다.",
3065
+ "branchDialog.deleteConfirmAction": "삭제",
3066
+ "branchDialog.cancel": "취소",
2791
3067
  "message.deleteConfirm": "이 메시지를 삭제할까요? 이 작업은 새로고침 후에도 유지됩니다.",
2792
3068
  "message.deleteFailed": "메시지를 삭제할 수 없습니다.",
2793
3069
  "message.sendFailed": "메시지를 보낼 수 없습니다.",
@@ -2805,7 +3081,7 @@ const ko = {
2805
3081
  "projectDelete.cancel": "취소",
2806
3082
  "projectDelete.confirm": "삭제",
2807
3083
  "projectDelete.deleting": "삭제 중…",
2808
- "projectCopy.eyebrow": "Organization project",
3084
+ "projectCopy.eyebrow": "조직 프로젝트",
2809
3085
  "projectCopy.title": "프로젝트 복사",
2810
3086
  "projectCopy.description": "'{name}' 프로젝트를 내 프로젝트로 복사합니다.",
2811
3087
  "projectCopy.close": "프로젝트 복사 닫기",
@@ -2949,6 +3225,11 @@ const ko = {
2949
3225
  "askUser.customPlaceholder": "답을 입력하세요",
2950
3226
  "askUser.submitting": "제출 중...",
2951
3227
  "askUser.submit": "제출",
3228
+ "conversationFilter.hideAutomated": "자동 실행 대화 숨기기",
3229
+ "conversationFilter.showAutomated": "자동 실행 대화 보기",
3230
+ "conversationFilter.hideAutomatedHint": "자동 리뷰·예약 자동화·GitHub 트리거가 실행한 대화를 목록에서 뺍니다.",
3231
+ "conversationFilter.hiddenCount": "{count}개 숨김",
3232
+ "conversationFilter.allHidden": "자동 실행 대화 {count}개만 있습니다. 보려면 필터를 끄세요.",
2952
3233
  "sessionsTab.hideArchived": "완료된 대화 숨기기",
2953
3234
  "sessionsTab.showArchived": "완료된 대화 보기",
2954
3235
  "sessionsTab.emptyBody": "모바일 앱처럼 프로젝트에 연결된 대화가 여기에 표시됩니다.",
@@ -2960,6 +3241,8 @@ const ko = {
2960
3241
  "sessionsTab.searchFailed": "사용자 프롬프트를 검색하지 못했습니다. 제목 검색 결과만 표시합니다.",
2961
3242
  "sessionsTab.noSearchResults": "“{query}”와 일치하는 대화가 없습니다.",
2962
3243
  "sessionsTab.clearSearch": "검색 지우기",
3244
+ "sessionsTab.noVisibleMessage": "표시할 메시지가 아직 없습니다",
3245
+ "sessionsTab.updated": "업데이트",
2963
3246
  // Planning / preview sidecar / preview tab
2964
3247
  "previewSidecar.closeAria": "Preview 닫기",
2965
3248
  "preview.serverStarting": "서버 시작 중...",
@@ -3013,6 +3296,29 @@ const ko = {
3013
3296
  "cloudMobileLogin.refresh": "새 QR 만들기",
3014
3297
  "systemPrompt.loadFailed": "설정을 불러오지 못했습니다.",
3015
3298
  "systemPrompt.saveFailed": "시스템 프롬프트를 저장하지 못했습니다.",
3299
+ "saycodePromptPreference.dialogAria": "Saycode 지시 사용 설정",
3300
+ "saycodePromptPreference.eyebrow": "에이전트 지시 선택",
3301
+ "saycodePromptPreference.title": "Saycode 기본 지시를 사용할까요?",
3302
+ "saycodePromptPreference.body": "내가 만든 Claude/Codex 스킬과 하네스를 그대로 쓰려면 꺼진 상태를 유지하세요. 나중에 환경설정에서 언제든 바꿀 수 있습니다.",
3303
+ "saycodePromptPreference.keepOff": "꺼진 상태 유지",
3304
+ "saycodePromptPreference.enable": "Saycode 지시 켜기",
3305
+ "saycodePromptPreference.saving": "저장 중…",
3306
+ "saycodePromptPreference.retryHint": "선택을 다시 눌러 저장을 재시도하세요.",
3307
+ "saycodePromptPreference.settingsTitle": "Saycode 기본 지시",
3308
+ "saycodePromptPreference.settingsEnable": "Saycode가 제공하는 기본 행동 지시 사용",
3309
+ "saycodePromptPreference.settingsHelp": "사용자 시스템 프롬프트, 프로젝트 지침, 설치한 스킬과 하네스는 이 설정과 관계없이 유지됩니다.",
3310
+ "saycodePromptPreference.blocksTitle": "개별 지시 선택",
3311
+ "saycodePromptPreference.blocksHelp": "Saycode 지시를 항목별로 켜고 끕니다. 선택하지 않은 항목은 위의 전체 설정을 따릅니다.",
3312
+ "saycodePromptPreference.alwaysOnNotice": "채팅 제목 생성은 항상 켜져 있으며 끌 수 없습니다. 채팅 목록에서 대화를 다시 찾으려면 필요합니다.",
3313
+ "saycodePromptPreference.block.workerDelegation.label": "작업 위임",
3314
+ "saycodePromptPreference.block.workerDelegation.description": "기계적인 작업을 저렴한 worker 에이전트에 넘기도록 지시합니다.",
3315
+ "saycodePromptPreference.block.axBase.label": "기획부터 시작 워크플로우",
3316
+ "saycodePromptPreference.block.axBase.description": "AX 워크플로우의 기본 지시를 사용합니다.",
3317
+ "saycodePromptPreference.block.coAuthoredCredit.label": "커밋 크레딧",
3318
+ "saycodePromptPreference.block.coAuthoredCredit.description": "커밋 메시지에 Claude와 Happy 공동 작성자 표시를 추가합니다.",
3319
+ "saycodePromptPreference.block.inlinePreview.label": "결과물 인라인 미리보기",
3320
+ "saycodePromptPreference.block.inlinePreview.description": "HTML 결과물을 만들면 답변 안에 미리보기를 띄우도록 지시합니다.",
3321
+ "saycodePromptPreference.loading": "계정 설정을 불러오는 중…",
3016
3322
  "standalone.zrokStatus": "zrok 상태",
3017
3323
  "standalone.zrokNeeded": "zrok2 설정이 필요합니다",
3018
3324
  "standalone.zrokNeededBody": "다른 네트워크의 머신을 등록하려면 접속 가능한 서버 URL이 필요합니다.",
@@ -3240,6 +3546,8 @@ const ko = {
3240
3546
  "sidebarFiles.invalidName": '이름에 "/", "\\"를 쓰거나 "." 또는 ".."로 지을 수 없습니다.',
3241
3547
  "sidebarFiles.daemonTooOld": "이 머신의 Happy CLI가 오래되어 이름변경·복사본·삭제를 할 수 없습니다. 1.1.10-aplus.74 이상으로 업데이트하세요.",
3242
3548
  // ProjectDocumentViewer
3549
+ "inlinePreview.openInViewer": "파일 뷰어로 열기",
3550
+ "inlinePreview.loading": "미리보기를 불러오는 중",
3243
3551
  "documentViewer.docxConvertFailed": "DOCX 문서를 변환하지 못했습니다.",
3244
3552
  "documentViewer.pdfViewer": "{name} PDF 뷰어",
3245
3553
  "documentViewer.pdfPreparing": "PDF를 준비하는 중...",
@@ -3305,6 +3613,14 @@ const ko = {
3305
3613
  "fileDownload.tooLarge": "다운로드 파일은 100 MiB 이하만 지원합니다.",
3306
3614
  "codex.needsLatestCli": "GPT-5.6을 사용하려면 최신 Codex CLI가 필요합니다. `npm install -g @openai/codex@latest`로 업데이트한 뒤 새 대화를 시작해 주세요.",
3307
3615
  "codex.error": "Codex 오류: {detail}",
3616
+ // specs/desktop-error-surface — 실패 알림의 계층 배지와 복구 액션
3617
+ "errorSurface.layer.provider": "Provider 오류",
3618
+ "errorSurface.layer.runtime": "런타임 오류",
3619
+ "errorSurface.layer.auth": "인증 오류",
3620
+ "errorSurface.retry": "다시 시도",
3621
+ "errorSurface.retrying": "보내는 중…",
3622
+ "errorSurface.retryFailed": "다시 보내지 못했습니다.",
3623
+ "errorSurface.openCredentials": "내 AI 자격증명 열기",
3308
3624
  "fanout.slotCountRange": "병렬 실행 후보 수는 2~4개여야 합니다 (요청: {count}개).",
3309
3625
  "deployPlan.contractNotFound": "배포 실행 계약을 찾지 못했습니다",
3310
3626
  // Voice input
@@ -3366,6 +3682,8 @@ const ko = {
3366
3682
  "projectGitView.noRemote": "remote 없음",
3367
3683
  "projectGitView.branchSwitch": "Branch 변경",
3368
3684
  "projectGitView.branchCreate": "Branch 생성",
3685
+ "projectGitView.branchRename": "Branch 이름 변경",
3686
+ "projectGitView.branchDelete": "Branch 삭제",
3369
3687
  // Remote terminal + API client (non-hook)
3370
3688
  "terminal.connectTimeout": "터미널 연결 시간이 초과되었습니다.",
3371
3689
  "terminal.connectFailed": "터미널 연결에 실패했습니다.",
@@ -3376,7 +3694,7 @@ const ko = {
3376
3694
  // Merge with main: git-controls dropdown + worktree section
3377
3695
  "gitControls.branchMenu": "Branch 메뉴",
3378
3696
  "gitControls.branchMenuWithDesc": "Branch 메뉴 · {description}",
3379
- "gitControls.gitActionsAria": "{branch} git actions",
3697
+ "gitControls.gitActionsAria": "{branch} git 작업",
3380
3698
  "gitControls.pullLabel": "가져오기 (pull)",
3381
3699
  "gitControls.pushLabel": "보내기 (push)",
3382
3700
  "gitControls.switchAria": "{branch} branch 전환",
@@ -3424,6 +3742,7 @@ const ko = {
3424
3742
  "settingsTab.security": "보안",
3425
3743
  "settingsTab.memory": "Memory",
3426
3744
  "settingsTab.connectors": "커넥터",
3745
+ "settingsTab.myAiCredentials": "내 AI 계정",
3427
3746
  "settingsTab.extensions": "확장",
3428
3747
  "settingsTab.about": "정보",
3429
3748
  "settingsTab.shortcuts": "단축키",
@@ -3444,6 +3763,13 @@ const ko = {
3444
3763
  "extensions.confirmEnableAction": "권한 승인 및 활성화",
3445
3764
  "extensions.confirmUninstall": "이 확장과 설치된 파일을 제거할까요?",
3446
3765
  "extensions.shellUnavailable": "확장 관리는 최신 Desktop 앱에서 사용할 수 있습니다.",
3766
+ "extensions.developerMode": "개발자 모드",
3767
+ "extensions.developerModeHint": "압축을 풀어둔 확장 폴더를 불러옵니다. 무결성 검사만 면제되고 권한·격리는 그대로 적용됩니다.",
3768
+ "extensions.loadUnpacked": "폴더 불러오기",
3769
+ "extensions.unloadDevelopment": "내리기",
3770
+ "extensions.developmentSource": "개발 빌드: {directory}",
3771
+ "extensions.apiDeprecated": "업데이트 필요",
3772
+ "extensions.apiDeprecatedHint": "이전 확장 API({apiVersion})로 동작 중이며, 다음 업데이트에서 지원이 끝납니다.",
3447
3773
  "extensions.runtime.inactive": "비활성",
3448
3774
  "extensions.runtime.active": "실행 중",
3449
3775
  "extensions.runtime.quarantined": "격리됨",
@@ -3537,10 +3863,12 @@ const ko = {
3537
3863
  "review.self.disabledWaiting": "먼저 대기 중인 질문이나 권한 요청에 응답해주세요.",
3538
3864
  "review.self.rounds": "반복 횟수",
3539
3865
  "review.self.roundsHint": "medium 이상 지적이 나오면 남은 횟수만큼 리뷰를 자동으로 이어갑니다. low·nit만 남으면 바로 끝냅니다.",
3866
+ "review.self.appOpenHint": "앱이 실행 중일 때만 이어집니다. 앱을 완전히 종료하면 멈추고, 다시 열면 남은 횟수부터 이어갑니다.",
3540
3867
  "review.self.loopActive": "반복 리뷰 {round}/{total} 진행 중",
3541
3868
  "review.self.loopStop": "반복 중지",
3542
3869
  "review.self.loopDismiss": "기록 지우기",
3543
3870
  "review.self.loopDone": "반복 리뷰 종료 ({rounds}회) — {reason}",
3871
+ "review.self.startCancelled": "다른 창의 작업으로 반복 리뷰 시작이 취소됐어요.",
3544
3872
  "review.self.loopReason.clean": "지적 없음",
3545
3873
  "review.self.loopReason.belowThreshold": "low·nit만 남아 조기 종료",
3546
3874
  "review.self.loopReason.roundsExhausted": "지정한 횟수를 모두 사용",
@@ -3648,7 +3976,7 @@ function isAskUserQuestionToolName(value2) {
3648
3976
  const normalized = value2.replace(/^functions\./i, "").replace(/^mcp__.+?__/i, "").replace(/\s+call$/i, "").replace(/[._\-\s]+/g, "").trim().toLowerCase();
3649
3977
  return normalized === "askuserquestion" || normalized === "requestuserinput" || normalized === "사용자에게질문";
3650
3978
  }
3651
- function isRecord$4(value2) {
3979
+ function isRecord$5(value2) {
3652
3980
  return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
3653
3981
  }
3654
3982
  function firstString(...values) {
@@ -3658,11 +3986,11 @@ function firstString(...values) {
3658
3986
  return void 0;
3659
3987
  }
3660
3988
  function recordValue(value2) {
3661
- if (isRecord$4(value2)) return value2;
3989
+ if (isRecord$5(value2)) return value2;
3662
3990
  if (typeof value2 !== "string" || !value2.trim()) return void 0;
3663
3991
  try {
3664
3992
  const parsed = JSON.parse(value2);
3665
- return isRecord$4(parsed) ? parsed : void 0;
3993
+ return isRecord$5(parsed) ? parsed : void 0;
3666
3994
  } catch {
3667
3995
  return void 0;
3668
3996
  }
@@ -3681,9 +4009,9 @@ function timestampValue(value2) {
3681
4009
  return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
3682
4010
  }
3683
4011
  function normalizePendingPermissionEnvelope(requestId, value2) {
3684
- if (!isRecord$4(value2)) return null;
3685
- const nestedTool = isRecord$4(value2.tool) ? value2.tool : null;
3686
- const nestedFunction = nestedTool && isRecord$4(nestedTool.function) ? nestedTool.function : null;
4012
+ if (!isRecord$5(value2)) return null;
4013
+ const nestedTool = isRecord$5(value2.tool) ? value2.tool : null;
4014
+ const nestedFunction = nestedTool && isRecord$5(nestedTool.function) ? nestedTool.function : null;
3687
4015
  const toolName = firstString(
3688
4016
  value2.name,
3689
4017
  value2.toolName,
@@ -3733,7 +4061,7 @@ function permissionKind(toolName) {
3733
4061
  return "generic";
3734
4062
  }
3735
4063
  function pendingPermissionRequestsFromState(state) {
3736
- if (!isRecord$4(state) || !isRecord$4(state.requests)) return [];
4064
+ if (!isRecord$5(state) || !isRecord$5(state.requests)) return [];
3737
4065
  return Object.entries(state.requests).map(([requestId, value2]) => {
3738
4066
  const envelope = normalizePendingPermissionEnvelope(requestId, value2);
3739
4067
  const toolName = envelope?.toolName ?? "unknown";
@@ -3748,7 +4076,7 @@ function pendingPermissionRequestsFromState(state) {
3748
4076
  });
3749
4077
  }
3750
4078
  function agentControlledByUserFromState(state) {
3751
- return isRecord$4(state) && state.controlledByUser === true;
4079
+ return isRecord$5(state) && state.controlledByUser === true;
3752
4080
  }
3753
4081
  function createChatState() {
3754
4082
  return { phase: "idle", pendingPermission: null };
@@ -4099,7 +4427,6 @@ const HELP_TEXT = `saycode — drive Saycode sessions across your machines from
4099
4427
 
4100
4428
  Usage
4101
4429
  saycode <command> [options]
4102
-
4103
4430
  Commands
4104
4431
  (no arguments) Interactive browser: workspace -> machine ->
4105
4432
  project -> conversation, then open it
@@ -4117,7 +4444,6 @@ Commands
4117
4444
  login / logout Manage stored credentials
4118
4445
  agent <verb> For AI agents inside a Saycode session, not
4119
4446
  for people — see \`saycode agent whoami\`
4120
-
4121
4447
  Options
4122
4448
  -w, --workspace <id|name> Workspace to act in (see \`saycode workspaces\`)
4123
4449
  -m, --machine <id|host> Machine id prefix or hostname substring
@@ -4145,7 +4471,10 @@ Agent commands (machine-readable JSON on stdout, exit 3 on failure)
4145
4471
  agent read <session> [--last n] Recent messages + completion signal
4146
4472
  agent wait <session> --until <state> turn-end | waiting-input | completed
4147
4473
  agent prompt <session> <text> [--wait] Send a prompt, optionally wait for it
4474
+ agent steer <session> <text> Redirect an owned child's active response
4475
+ agent stop <session> Stop an owned child's active response
4148
4476
  agent spawn --prompt <text> [--agent n] Start a sibling in the same directory
4477
+ agent spawn --prompt <text> --file <path> ...with context files it reads first
4149
4478
  Only works inside a session Saycode started (SAYCODE_AGENT_ENV).
4150
4479
 
4151
4480
  Workspaces
@@ -6528,6 +6857,21 @@ function decryptJsonWithCandidates(blob, keys) {
6528
6857
  decryptCacheSet(blob, keys, null);
6529
6858
  return null;
6530
6859
  }
6860
+ function buildUserTextMessageContent(input) {
6861
+ return {
6862
+ role: "user",
6863
+ content: { type: "text", text: input.text },
6864
+ meta: {
6865
+ permissionMode: "bypassPermissions",
6866
+ saycodeSystemPromptEnabled: input.saycodeSystemPromptEnabled ?? true,
6867
+ ...input.saycodePromptBlocks && Object.keys(input.saycodePromptBlocks).length > 0 ? { saycodePromptBlocks: input.saycodePromptBlocks } : {},
6868
+ ...input.model && input.model !== "default" ? { model: input.model } : {},
6869
+ ...input.effort ? { effort: input.effort } : {},
6870
+ ...input.autoRoute ? { autoRoute: input.autoRoute } : {},
6871
+ ...input.appendSystemPrompt !== void 0 ? { appendSystemPrompt: input.appendSystemPrompt } : {}
6872
+ }
6873
+ };
6874
+ }
6531
6875
  const CONNECTOR_PROVIDER_IDS = [
6532
6876
  "notion",
6533
6877
  "slack",
@@ -6576,6 +6920,9 @@ function aiCredentialsPath(companyId, bundleId, action) {
6576
6920
  function personalMachinesPath(companyId) {
6577
6921
  return `/api/admin/companies/${encodeURIComponent(companyId)}/personal-machines`;
6578
6922
  }
6923
+ function myMachinesPath(companyId) {
6924
+ return `/api/admin/companies/${encodeURIComponent(companyId)}/my-machines`;
6925
+ }
6579
6926
  function record$1(value2) {
6580
6927
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : {};
6581
6928
  }
@@ -6692,6 +7039,20 @@ function parseAiCredentialBatchResult(value2) {
6692
7039
  warnings: array(body.warnings).filter((warning) => typeof warning === "string")
6693
7040
  };
6694
7041
  }
7042
+ function parseSelfServiceMachines(value2) {
7043
+ return array(record$1(value2).machines).map((entry) => {
7044
+ const row2 = record$1(entry);
7045
+ return {
7046
+ machineId: str(row2.machineId),
7047
+ type: row2.type === "shared" ? "shared" : "personal",
7048
+ displayName: typeof row2.displayName === "string" ? row2.displayName : null,
7049
+ host: typeof row2.host === "string" ? row2.host : null,
7050
+ workspaceRoot: typeof row2.workspaceRoot === "string" ? row2.workspaceRoot : null,
7051
+ registeredAt: num(row2.registeredAt),
7052
+ online: typeof row2.online === "boolean" ? row2.online : null
7053
+ };
7054
+ }).filter((machine) => machine.machineId !== "");
7055
+ }
6695
7056
  function parsePersonalMachines(value2) {
6696
7057
  return array(record$1(value2).machines).map((entry) => {
6697
7058
  const row2 = record$1(entry);
@@ -6764,7 +7125,7 @@ function buildOpenConversationsPath(input) {
6764
7125
  if (input.cursor) params.set("cursor", input.cursor);
6765
7126
  return `/api/me/open-conversations?${params.toString()}`;
6766
7127
  }
6767
- function isRecord$3(value2) {
7128
+ function isRecord$4(value2) {
6768
7129
  return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
6769
7130
  }
6770
7131
  function normalizedOptionalText(value2, maxLength) {
@@ -6772,7 +7133,7 @@ function normalizedOptionalText(value2, maxLength) {
6772
7133
  return value2.trim().slice(0, maxLength) || null;
6773
7134
  }
6774
7135
  function parseCard(value2) {
6775
- if (!isRecord$3(value2)) return null;
7136
+ if (!isRecord$4(value2)) return null;
6776
7137
  if (typeof value2.sessionId !== "string" || typeof value2.projectId !== "string" || typeof value2.title !== "string" || typeof value2.projectName !== "string" || value2.branch !== null && typeof value2.branch !== "string" || value2.agent !== null && typeof value2.agent !== "string" || typeof value2.updatedAt !== "number" || !Number.isFinite(value2.updatedAt) || typeof value2.serverActive !== "boolean" || typeof value2.workspaceCompleted !== "boolean") return null;
6777
7138
  const sessionId = value2.sessionId.trim();
6778
7139
  const projectId = value2.projectId.trim();
@@ -6801,7 +7162,7 @@ function parseRows(value2, limit) {
6801
7162
  return rows;
6802
7163
  }
6803
7164
  function parseOpenConversationsPayload(value2) {
6804
- if (!isRecord$3(value2)) throw new Error("Invalid open conversations response");
7165
+ if (!isRecord$4(value2)) throw new Error("Invalid open conversations response");
6805
7166
  if (!Array.isArray(value2.activeCandidates) || !Array.isArray(value2.recent) || typeof value2.truncated !== "boolean" || !(value2.nextCursor === null || typeof value2.nextCursor === "string" && value2.nextCursor.length > 0 && value2.nextCursor.length <= MAX_CURSOR_LENGTH$1)) throw new Error("Invalid open conversations response");
6806
7167
  const rawActive = value2.activeCandidates;
6807
7168
  const rawRecent = value2.recent;
@@ -6876,7 +7237,7 @@ function buildProjectConversationsPath(projectId) {
6876
7237
  if (!trimmed) throw new Error("Invalid project id");
6877
7238
  return `/api/v2/projects/${encodeURIComponent(trimmed)}/conversations`;
6878
7239
  }
6879
- function isRecord$2(value2) {
7240
+ function isRecord$3(value2) {
6880
7241
  return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
6881
7242
  }
6882
7243
  function nullableString$2(value2) {
@@ -6889,7 +7250,7 @@ function stringArray(value2) {
6889
7250
  return Array.isArray(value2) ? value2.filter((entry) => typeof entry === "string") : [];
6890
7251
  }
6891
7252
  function promptSuggestion(value2) {
6892
- if (!isRecord$2(value2)) return null;
7253
+ if (!isRecord$3(value2)) return null;
6893
7254
  const text2 = value2.text;
6894
7255
  const provider2 = value2.provider;
6895
7256
  const updatedAt = value2.updatedAt;
@@ -6897,7 +7258,7 @@ function promptSuggestion(value2) {
6897
7258
  return { text: text2, provider: provider2, updatedAt };
6898
7259
  }
6899
7260
  function parseRow(value2) {
6900
- if (!isRecord$2(value2)) return null;
7261
+ if (!isRecord$3(value2)) return null;
6901
7262
  if (typeof value2.id !== "string" || !value2.id.trim() || typeof value2.title !== "string" || typeof value2.active !== "boolean" || typeof value2.agentStateBusy !== "boolean" || typeof value2.workspaceCompleted !== "boolean" || typeof value2.hydrated !== "boolean") return null;
6902
7263
  return {
6903
7264
  id: value2.id,
@@ -6925,7 +7286,7 @@ function parseRow(value2) {
6925
7286
  };
6926
7287
  }
6927
7288
  function parseProjectConversationsPayload(value2) {
6928
- if (!isRecord$2(value2)) throw new Error("Invalid project conversations response");
7289
+ if (!isRecord$3(value2)) throw new Error("Invalid project conversations response");
6929
7290
  if (typeof value2.projectId !== "string" || !Array.isArray(value2.conversations) || typeof value2.partial !== "boolean" || typeof value2.truncated !== "boolean") throw new Error("Invalid project conversations response");
6930
7291
  const conversations = [];
6931
7292
  for (const raw of value2.conversations.slice(0, MAX_MAPPING_COUNT)) {
@@ -6944,7 +7305,7 @@ function parseProjectConversationsPayload(value2) {
6944
7305
  const COMPANY_MEMBER_DIRECTORY_PAGE_LIMIT = 100;
6945
7306
  const MAX_PROJECT_MEMBER_COUNT = 1e3;
6946
7307
  const MAX_CURSOR_LENGTH = 2048;
6947
- function isRecord$1(value2) {
7308
+ function isRecord$2(value2) {
6948
7309
  return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
6949
7310
  }
6950
7311
  function nullableString$1(value2) {
@@ -6952,11 +7313,11 @@ function nullableString$1(value2) {
6952
7313
  }
6953
7314
  function parseAvatar(value2) {
6954
7315
  if (value2 === null) return null;
6955
- if (!isRecord$1(value2) || typeof value2.path !== "string" || typeof value2.url !== "string") return void 0;
7316
+ if (!isRecord$2(value2) || typeof value2.path !== "string" || typeof value2.url !== "string") return void 0;
6956
7317
  return { path: value2.path, url: value2.url };
6957
7318
  }
6958
7319
  function parseProjectMember(value2) {
6959
- if (!isRecord$1(value2)) return null;
7320
+ if (!isRecord$2(value2)) return null;
6960
7321
  const username = nullableString$1(value2.username);
6961
7322
  const firstName = nullableString$1(value2.firstName);
6962
7323
  const lastName = nullableString$1(value2.lastName);
@@ -6976,7 +7337,7 @@ function parseProjectMember(value2) {
6976
7337
  };
6977
7338
  }
6978
7339
  function parseProjectMembersPayload(value2) {
6979
- if (!isRecord$1(value2) || !Array.isArray(value2.members)) {
7340
+ if (!isRecord$2(value2) || !Array.isArray(value2.members)) {
6980
7341
  throw new Error("Invalid project members response");
6981
7342
  }
6982
7343
  if (value2.members.length > MAX_PROJECT_MEMBER_COUNT) {
@@ -6989,7 +7350,7 @@ function parseProjectMembersPayload(value2) {
6989
7350
  });
6990
7351
  }
6991
7352
  function parseDirectoryEntry(value2) {
6992
- if (!isRecord$1(value2)) return null;
7353
+ if (!isRecord$2(value2)) return null;
6993
7354
  const email = nullableString$1(value2.email);
6994
7355
  const displayName = nullableString$1(value2.displayName);
6995
7356
  if (typeof value2.accountId !== "string" || !value2.accountId.trim() || typeof value2.username !== "string" || !value2.username.trim() || email === void 0 || displayName === void 0) return null;
@@ -7010,8 +7371,8 @@ function buildCompanyMemberDirectoryPath(companyId, cursor) {
7010
7371
  return `/api/companies/${encodeURIComponent(trimmedCompanyId)}/member-directory?${params.toString()}`;
7011
7372
  }
7012
7373
  function parseCompanyMemberDirectoryPage(value2) {
7013
- const rawItems = Array.isArray(value2) ? value2 : isRecord$1(value2) && Array.isArray(value2.items) ? value2.items : null;
7014
- const rawCursor = Array.isArray(value2) ? null : isRecord$1(value2) ? value2.nextCursor : void 0;
7374
+ const rawItems = Array.isArray(value2) ? value2 : isRecord$2(value2) && Array.isArray(value2.items) ? value2.items : null;
7375
+ const rawCursor = Array.isArray(value2) ? null : isRecord$2(value2) ? value2.nextCursor : void 0;
7015
7376
  if (!rawItems || rawItems.length > COMPANY_MEMBER_DIRECTORY_PAGE_LIMIT || !(rawCursor === null || typeof rawCursor === "string" && rawCursor.length > 0 && rawCursor.length <= MAX_CURSOR_LENGTH)) {
7016
7377
  if (rawItems && rawItems.length > COMPANY_MEMBER_DIRECTORY_PAGE_LIMIT) {
7017
7378
  throw new Error("Company member directory response is too large");
@@ -15444,7 +15805,7 @@ function branchSlugFromMetadata(metadata) {
15444
15805
  function stringField(value2) {
15445
15806
  return typeof value2 === "string" && value2.trim() ? value2.trim() : void 0;
15446
15807
  }
15447
- function isRecord(value2) {
15808
+ function isRecord$1(value2) {
15448
15809
  return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
15449
15810
  }
15450
15811
  function numberField(value2) {
@@ -15471,7 +15832,7 @@ function firstTokenCount(...values) {
15471
15832
  return void 0;
15472
15833
  }
15473
15834
  function coerceTokenUsageRecord(value2, allowGenericKeys = true) {
15474
- if (!isRecord(value2)) return void 0;
15835
+ if (!isRecord$1(value2)) return void 0;
15475
15836
  const inputTokens = firstTokenCount(
15476
15837
  value2.inputTokens,
15477
15838
  value2.input_tokens,
@@ -15534,7 +15895,7 @@ function coerceSessionTokenUsage(...sources) {
15534
15895
  for (const source of sources) {
15535
15896
  const direct = coerceTokenUsageRecord(source, false);
15536
15897
  if (direct) return direct;
15537
- if (!isRecord(source)) continue;
15898
+ if (!isRecord$1(source)) continue;
15538
15899
  for (const key of usageKeys) {
15539
15900
  const nested = coerceTokenUsageRecord(source[key]);
15540
15901
  if (nested) return nested;
@@ -15546,7 +15907,7 @@ function pullRequestStateField(value2) {
15546
15907
  return value2 === "open" || value2 === "closed" || value2 === "merged" ? value2 : void 0;
15547
15908
  }
15548
15909
  function coercePendingAskUserQuestionRequest(requestId, value2) {
15549
- if (!isRecord(value2)) return null;
15910
+ if (!isRecord$1(value2)) return null;
15550
15911
  const envelope = normalizePendingPermissionEnvelope(requestId, value2);
15551
15912
  if (!envelope || !isAskUserQuestionToolName(envelope.toolName)) return null;
15552
15913
  const name = envelope.toolName;
@@ -15563,7 +15924,7 @@ function apiSessionAgentStatePendingAskUserQuestion(agentState, candidateKeys) {
15563
15924
  const state = decryptJsonWithCandidates(agentState, candidateKeys);
15564
15925
  if (!state) return null;
15565
15926
  const requests = state.requests;
15566
- if (!isRecord(requests)) return null;
15927
+ if (!isRecord$1(requests)) return null;
15567
15928
  for (const [requestId, request] of Object.entries(requests)) {
15568
15929
  const pending = coercePendingAskUserQuestionRequest(requestId, request);
15569
15930
  if (pending) return pending;
@@ -15587,7 +15948,7 @@ function coercePendingExitPlanModeRequest(requestId, value2) {
15587
15948
  function apiSessionAgentStatePendingExitPlanMode(agentState, candidateKeys) {
15588
15949
  if (!agentState) return null;
15589
15950
  const state = decryptJsonWithCandidates(agentState, candidateKeys);
15590
- if (!state || !isRecord(state.requests)) return null;
15951
+ if (!state || !isRecord$1(state.requests)) return null;
15591
15952
  for (const [requestId, request] of Object.entries(state.requests)) {
15592
15953
  const pending = coercePendingExitPlanModeRequest(requestId, request);
15593
15954
  if (pending) return pending;
@@ -15633,7 +15994,7 @@ function optionalTimestamp(value2) {
15633
15994
  return parsed > 0 ? parsed : void 0;
15634
15995
  }
15635
15996
  function coercePromptSuggestion(value2) {
15636
- if (!isRecord(value2)) return void 0;
15997
+ if (!isRecord$1(value2)) return void 0;
15637
15998
  const text2 = nonEmptyString(value2.text);
15638
15999
  const provider2 = nonEmptyString(value2.provider);
15639
16000
  const updatedAt = optionalTimestamp(value2.updatedAt);
@@ -15641,7 +16002,7 @@ function coercePromptSuggestion(value2) {
15641
16002
  return { text: text2, provider: provider2, updatedAt };
15642
16003
  }
15643
16004
  function coerceCreatedBy(value2) {
15644
- if (!isRecord(value2)) return void 0;
16005
+ if (!isRecord$1(value2)) return void 0;
15645
16006
  const accountId = nonEmptyString(value2.accountId);
15646
16007
  if (!accountId) return void 0;
15647
16008
  const displayName = nonEmptyString(value2.displayName);
@@ -15672,6 +16033,7 @@ function apiSessionToSession(api, candidateKeys, existing) {
15672
16033
  const archiveReason = nonEmptyString(metadata?.archiveReason) ?? existing?.archiveReason;
15673
16034
  const createdBy = coerceCreatedBy(metadata?.createdBy) ?? existing?.createdBy;
15674
16035
  const branchSlug = branchSlugFromMetadata(metadata) ?? existing?.branchSlug;
16036
+ const path = nonEmptyString(metadata?.path) ?? existing?.path;
15675
16037
  const promptSuggestion2 = metadata && Object.prototype.hasOwnProperty.call(metadata, "promptSuggestion") ? coercePromptSuggestion(metadata.promptSuggestion) : existing?.promptSuggestion;
15676
16038
  return {
15677
16039
  ...existing,
@@ -15707,6 +16069,7 @@ function apiSessionToSession(api, candidateKeys, existing) {
15707
16069
  ...archiveReason ? { archiveReason } : {},
15708
16070
  ...createdBy ? { createdBy } : {},
15709
16071
  ...branchSlug ? { branchSlug } : {},
16072
+ ...path ? { path } : {},
15710
16073
  ...sessionFilesystemScopePatch(metadata, existing),
15711
16074
  ...remoteThinking === null ? {} : { agentStateBusy: remoteThinking }
15712
16075
  };
@@ -15740,11 +16103,15 @@ function readMcpCallerGrantPublicKey(daemonState) {
15740
16103
  const value2 = daemonState.mcpCallerGrantPublicKey;
15741
16104
  return typeof value2 === "string" && value2 ? value2 : void 0;
15742
16105
  }
15743
- function buildWorktreeMutationBody(input) {
16106
+ function buildWorktreeMutationBody(input, options = {}) {
15744
16107
  return JSON.stringify(Object.fromEntries(
15745
- Object.entries(input).filter(([, value2]) => typeof value2 === "string" && value2.length > 0)
16108
+ Object.entries(input).filter(([key, value2]) => (key !== "machinePlatform" || options.includeMachinePlatform) && (typeof value2 === "string" && (value2.length > 0 || key === "branch") || key === "branch" && value2 === null))
15746
16109
  ));
15747
16110
  }
16111
+ const STANDALONE_USER_ID = "standalone-local";
16112
+ function isStandaloneAuthSession(session) {
16113
+ return session?.userId === STANDALONE_USER_ID;
16114
+ }
15748
16115
  function indexMachineRegistry(entries) {
15749
16116
  const byId = {};
15750
16117
  for (const entry of entries) {
@@ -15809,6 +16176,17 @@ function resolvedProjectPreviewRelayUrl(input) {
15809
16176
  }
15810
16177
  return `${input.syncBaseUrl}/v1/preview/${encodeURIComponent(input.machineId)}/${input.port}/?ptoken=${encodeURIComponent(response.token)}`;
15811
16178
  }
16179
+ const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
16180
+ function versionedAutomationBase64(value2, maxBytes, exactBytes) {
16181
+ if (typeof value2 !== "string" || !value2 || value2.length > Math.ceil(maxBytes / 3) * 4 || !BASE64_PATTERN.test(value2)) {
16182
+ throw new Error("automation-response-invalid");
16183
+ }
16184
+ const decoded = Buffer$1.from(value2, "base64");
16185
+ if (decoded.length < 41 || decoded.length > maxBytes || exactBytes !== void 0 && decoded.length !== exactBytes || decoded[0] !== 1) {
16186
+ throw new Error("automation-response-invalid");
16187
+ }
16188
+ return value2;
16189
+ }
15812
16190
  function row(value2) {
15813
16191
  if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) throw new Error("automation-response-invalid");
15814
16192
  return value2;
@@ -15830,10 +16208,23 @@ function nullableInteger(value2) {
15830
16208
  function optionalNullableInteger(value2) {
15831
16209
  return value2 === void 0 ? null : nullableInteger(value2);
15832
16210
  }
15833
- function parseServerAutomationPublic(value2) {
16211
+ function parseRuntimeState(value2) {
15834
16212
  const item = row(value2);
15835
- if (item.payloadVersion !== 1 || typeof item.paused !== "boolean") throw new Error("automation-response-invalid");
15836
16213
  return {
16214
+ automationId: stringValue(item.automationId),
16215
+ generation: integer(item.generation, 1),
16216
+ stateVersion: integer(item.stateVersion, 1),
16217
+ stateCiphertext: versionedAutomationBase64(item.stateCiphertext, 96 * 1024),
16218
+ viewerKeyVersion: integer(item.viewerKeyVersion, 1),
16219
+ viewerKeyEnvelope: versionedAutomationBase64(item.viewerKeyEnvelope, 105, 105),
16220
+ machineKeyVersion: integer(item.machineKeyVersion, 1),
16221
+ updatedAt: integer(item.updatedAt)
16222
+ };
16223
+ }
16224
+ function parseServerAutomationPublic(value2) {
16225
+ const item = row(value2);
16226
+ if (item.payloadVersion !== 1 && item.payloadVersion !== 2 || typeof item.paused !== "boolean") throw new Error("automation-response-invalid");
16227
+ const parsed = {
15837
16228
  id: stringValue(item.id),
15838
16229
  projectId: stringValue(item.projectId),
15839
16230
  ownerAccountId: stringValue(item.ownerAccountId),
@@ -15841,7 +16232,7 @@ function parseServerAutomationPublic(value2) {
15841
16232
  machineId: nullableString(item.machineId),
15842
16233
  revision: integer(item.revision, 1),
15843
16234
  generation: integer(item.generation, 1),
15844
- payloadVersion: 1,
16235
+ payloadVersion: item.payloadVersion,
15845
16236
  payloadCiphertext: stringValue(item.payloadCiphertext),
15846
16237
  viewerKeyId: stringValue(item.viewerKeyId),
15847
16238
  viewerKeyVersion: integer(item.viewerKeyVersion, 1),
@@ -15853,8 +16244,13 @@ function parseServerAutomationPublic(value2) {
15853
16244
  appliedRevision: integer(item.appliedRevision),
15854
16245
  appliedAt: nullableInteger(item.appliedAt),
15855
16246
  createdAt: integer(item.createdAt),
15856
- updatedAt: integer(item.updatedAt)
16247
+ updatedAt: integer(item.updatedAt),
16248
+ runtimeState: item.runtimeState === void 0 || item.runtimeState === null ? null : parseRuntimeState(item.runtimeState)
15857
16249
  };
16250
+ 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)) {
16251
+ throw new Error("automation-response-invalid");
16252
+ }
16253
+ return parsed;
15858
16254
  }
15859
16255
  function parseServerAutomationAdoption(value2) {
15860
16256
  const item = row(value2);
@@ -15875,11 +16271,12 @@ function parseServerAutomationTarget(value2) {
15875
16271
  machinePublicKey: stringValue(item.machinePublicKey),
15876
16272
  machineKeyVersion: integer(item.machineKeyVersion, 1),
15877
16273
  viewerPublicKey: nullableString(item.viewerPublicKey),
15878
- viewerKeyVersion: integer(item.viewerKeyVersion)
16274
+ viewerKeyVersion: integer(item.viewerKeyVersion),
16275
+ automationProtocolVersion: item.automationProtocolVersion === void 0 ? 1 : integer(item.automationProtocolVersion, 1)
15879
16276
  };
15880
16277
  }
15881
16278
  const RUN_STATUSES = /* @__PURE__ */ new Set(["CLAIMED", "RUNNING", "COMPLETED", "FAILED", "CANCELLED", "EXPIRED", "ABANDONED"]);
15882
- const RUN_OUTCOMES = /* @__PURE__ */ new Set(["WOKE", "SILENT", "SKIPPED_GATE", "ERROR"]);
16279
+ const RUN_OUTCOMES = /* @__PURE__ */ new Set(["WOKE", "SILENT", "SKIPPED_GATE", "NO_CHANGE", "ERROR"]);
15883
16280
  function parseServerAutomationRun(value2) {
15884
16281
  const item = row(value2);
15885
16282
  if (!RUN_STATUSES.has(item.status) || item.outcome !== null && !RUN_OUTCOMES.has(item.outcome) || typeof item.lateReport !== "boolean") throw new Error("automation-response-invalid");
@@ -16135,6 +16532,7 @@ function normalizeAutomationSupport(value2) {
16135
16532
  return {
16136
16533
  rpcAvailable: capability.rpcAvailable,
16137
16534
  serverBacked: capability.serverBacked === true,
16535
+ ...typeof capability.monitorMode === "boolean" ? { monitorMode: capability.monitorMode } : {},
16138
16536
  ...Number.isSafeInteger(capability.keyVersion) && capability.keyVersion > 0 ? { keyVersion: capability.keyVersion } : {}
16139
16537
  };
16140
16538
  }
@@ -16178,7 +16576,8 @@ class ApiClient {
16178
16576
  apiBaseUrl: session.apiBaseUrl,
16179
16577
  syncBaseUrl: session.happyBaseUrl,
16180
16578
  apiToken: session.token,
16181
- syncToken: session.syncToken || session.token
16579
+ syncToken: session.syncToken || session.token,
16580
+ standalone: isStandaloneAuthSession(session)
16182
16581
  });
16183
16582
  }
16184
16583
  async login(input) {
@@ -16770,9 +17169,25 @@ class ApiClient {
16770
17169
  throw error;
16771
17170
  }
16772
17171
  }
16773
- async ensureHappyProjectMachineBinding(projectId, machineId) {
16774
- const path = `/v1/projects/${encodeURIComponent(projectId)}`;
16775
- const data = await this.request(path, { boundary: "sync" });
17172
+ /**
17173
+ * 자동화·GitHub 트리거가 요구하는 두 전제를 멱등하게 맞춘다: happy 미러
17174
+ * 행이 존재할 것, 그리고 그 행의 `config.machineId` 가 대상 머신일 것.
17175
+ *
17176
+ * A+ 는 프로젝트를 자기 DB 에 만들고 happy 미러는 일부 경로에서만 만든다.
17177
+ * 미러가 없으면 아래 GET 이 404 `project-not-found` 로 죽어 데스크톱의
17178
+ * 자동화 화면이 통째로 막혔다 — 그 404 를 여기서 생성으로 흡수한다.
17179
+ * 정상 경로(미러 존재)는 기존과 같이 GET 한 번으로 끝난다.
17180
+ */
17181
+ async ensureHappyProjectMachineBinding(input) {
17182
+ const { project: source, machineId } = input;
17183
+ const path = `/v1/projects/${encodeURIComponent(source.id)}`;
17184
+ let data;
17185
+ try {
17186
+ data = await this.request(path, { boundary: "sync" });
17187
+ } catch (error) {
17188
+ if (!(error instanceof ApiError) || error.status !== 404) throw error;
17189
+ data = await this.createHappyProjectMirror(source, machineId);
17190
+ }
16776
17191
  if (!data.project || typeof data.project !== "object" || Array.isArray(data.project)) {
16777
17192
  throw new Error("happy-project-response-invalid");
16778
17193
  }
@@ -16788,6 +17203,34 @@ class ApiClient {
16788
17203
  body: JSON.stringify({ config: { ...config, machineId } })
16789
17204
  });
16790
17205
  }
17206
+ /**
17207
+ * happy 의 projectCreate 는 `config` 를 그대로 받아 응답에 되돌려주므로,
17208
+ * 신규 생성이면 호출부의 바인딩 조정이 즉시 끝나 왕복이 하나로 마무리된다.
17209
+ * 응답을 그대로 돌려주는 이유는 호출부 주석 참고 — 이 연산은 멱등이라
17210
+ * 남이 만든 미러를 돌려줄 수도 있어서 응답을 믿고 건너뛰면 안 된다.
17211
+ *
17212
+ * 409 는 같은 id 를 다른 happy 계정이 쥐고 있다는 뜻이라 이 클라이언트에서
17213
+ * 고칠 수 없다 — 조용히 넘기면 자동화 화면이 원인 없는 404 를 계속
17214
+ * 보여주므로 그대로 올린다.
17215
+ */
17216
+ async createHappyProjectMirror(source, machineId) {
17217
+ const body = { id: source.id, name: source.name };
17218
+ if (typeof source.description === "string") body.description = source.description;
17219
+ if (typeof source.color === "string") body.color = source.color;
17220
+ body.config = { machineId };
17221
+ try {
17222
+ return await this.request("/v1/projects", {
17223
+ method: "POST",
17224
+ boundary: "sync",
17225
+ body: JSON.stringify(body)
17226
+ });
17227
+ } catch (error) {
17228
+ if (error instanceof ApiError && error.status === 409) {
17229
+ throw new Error("happy-project-id-taken");
17230
+ }
17231
+ throw error;
17232
+ }
17233
+ }
16791
17234
  async getProjectMembers(projectId) {
16792
17235
  const data = await this.request(
16793
17236
  `/v1/projects/${encodeURIComponent(projectId)}/members`,
@@ -17213,7 +17656,7 @@ class ApiClient {
17213
17656
  {
17214
17657
  method: "POST",
17215
17658
  boundary: "api",
17216
- body: buildWorktreeMutationBody(input)
17659
+ body: buildWorktreeMutationBody(input, { includeMachinePlatform: this.config.standalone })
17217
17660
  }
17218
17661
  );
17219
17662
  return data.worktree;
@@ -17304,6 +17747,44 @@ class ApiClient {
17304
17747
  tokenOverride
17305
17748
  });
17306
17749
  }
17750
+ /**
17751
+ * 복호화 없이 원본 행을 전량 — import 파이프가 metadata 블롭을 재암호화할 때 쓴다.
17752
+ * /v1/sessions 는 150건에서 잘리므로 /v2 커서 페이지네이션으로 끝까지 걷는다
17753
+ * (조용한 절단은 "일부만 이관됐는데 성공으로 보이는" 최악의 실패를 만든다).
17754
+ */
17755
+ async fetchApiSessions(tokenOverride) {
17756
+ const all = [];
17757
+ let cursor = null;
17758
+ for (; ; ) {
17759
+ const params = new URLSearchParams({ limit: "200" });
17760
+ if (cursor) params.set("cursor", cursor);
17761
+ const data = await this.request(`/v2/sessions?${params.toString()}`, { boundary: "sync", tokenOverride });
17762
+ all.push(...data.sessions ?? []);
17763
+ if (data.hasNext !== true || !data.nextCursor || data.nextCursor === cursor) break;
17764
+ cursor = data.nextCursor;
17765
+ }
17766
+ return all;
17767
+ }
17768
+ /**
17769
+ * 라이브 CLI 없이 세션 행만 만든다 (specs/desktop-standalone-cloud-import).
17770
+ * 서버는 (accountId, tag) 기준 get-or-create라 같은 tag 재호출은 기존 세션을 돌려준다.
17771
+ * dataEncryptionKey 를 null 로 둬 legacy secretbox(계정 시드) 경로로 통일한다 —
17772
+ * 데스크탑의 encode/decodeMessageContent 가 읽고 쓸 수 있는 유일한 변형이다.
17773
+ */
17774
+ async createRawSession(input, tokenOverride) {
17775
+ const data = await this.request("/v1/sessions", {
17776
+ method: "POST",
17777
+ boundary: "sync",
17778
+ tokenOverride,
17779
+ body: JSON.stringify({
17780
+ tag: input.tag,
17781
+ metadata: input.metadata,
17782
+ agentState: input.agentState ?? null,
17783
+ dataEncryptionKey: null
17784
+ })
17785
+ });
17786
+ return data.session;
17787
+ }
17307
17788
  async getGithubPatStatus(tokenOverride) {
17308
17789
  const raw = await this.request(
17309
17790
  "/v1/connect/github-pat/status",
@@ -17436,17 +17917,7 @@ class ApiClient {
17436
17917
  async sendUserTextMessage(input) {
17437
17918
  const localId = input.localId || `desktop-${Date.now()}-${Math.random().toString(36).slice(2)}`;
17438
17919
  const key = input.secretBase64 ? decodeBase64(input.secretBase64) : null;
17439
- const content = {
17440
- role: "user",
17441
- content: { type: "text", text: input.text },
17442
- meta: {
17443
- permissionMode: "bypassPermissions",
17444
- ...input.model && input.model !== "default" ? { model: input.model } : {},
17445
- ...input.effort ? { effort: input.effort } : {},
17446
- ...input.autoRoute ? { autoRoute: input.autoRoute } : {},
17447
- ...input.appendSystemPrompt ? { appendSystemPrompt: input.appendSystemPrompt } : {}
17448
- }
17449
- };
17920
+ const content = buildUserTextMessageContent(input);
17450
17921
  const encoded = encodeMessageContent(content, key);
17451
17922
  const result = await this.sendMessages(input.sessionId, [{ localId, content: encoded }], input.tokenOverride);
17452
17923
  return { localId, sentSeq: result.messages?.[0]?.seq };
@@ -17951,6 +18422,17 @@ class ApiClient {
17951
18422
  { boundary: "api" }
17952
18423
  ));
17953
18424
  }
18425
+ /**
18426
+ * personal-credential-set-apply — owner/admin 이 아닌 일반 멤버도 호출 가능.
18427
+ * 서버가 이미 배포 자격(개인 머신 + 본인이 등록한 이 회사의 공용 머신)
18428
+ * 으로 필터링해 돌려준다.
18429
+ */
18430
+ async getMyMachines(companyId) {
18431
+ return parseSelfServiceMachines(await this.request(
18432
+ myMachinesPath(companyId),
18433
+ { boundary: "api" }
18434
+ ));
18435
+ }
17954
18436
  async getCompanyAiCredentials(companyId) {
17955
18437
  return parseAiCredentialVaultSnapshot(await this.request(
17956
18438
  aiCredentialsPath(companyId),
@@ -18063,11 +18545,20 @@ function credentialSearchPaths(input) {
18063
18545
  }
18064
18546
  return [
18065
18547
  { source: "saycode", path: joinPath(input.homeDir, ".saycode", "credentials.v1.json") },
18066
- { source: "happy", path: joinPath(input.homeDir, ".happy", "access.key") },
18067
- { source: "happy_remote", path: joinPath(input.homeDir, ".happy_remote", "access.key") },
18068
- { source: "happy_local", path: joinPath(input.homeDir, ".happy_local", "access.key") }
18548
+ ...happyHomeOrder(input).map((source) => ({
18549
+ source,
18550
+ path: joinPath(input.homeDir, `.${source}`, "access.key")
18551
+ }))
18069
18552
  ];
18070
18553
  }
18554
+ function happyHomeOrder(input) {
18555
+ const homes = CREDENTIAL_SOURCE_ORDER.filter((source) => source !== "saycode");
18556
+ const runtimeHome = input.env.HAPPY_HOME_DIR?.trim();
18557
+ if (!runtimeHome) return [...homes];
18558
+ const runtime = homes.find((source) => joinPath(input.homeDir, `.${source}`) === joinPath(runtimeHome));
18559
+ if (!runtime) return [...homes];
18560
+ return [runtime, ...homes.filter((source) => source !== runtime)];
18561
+ }
18071
18562
  function selectFirstValidCredential(candidates, readFile2) {
18072
18563
  for (const candidate of candidates) {
18073
18564
  const raw = readFile2(candidate.path);
@@ -18397,6 +18888,12 @@ class CliRest {
18397
18888
  if (!apiToken) throw new ApiUnavailableError();
18398
18889
  return this.request(path, { method: "GET" }, apiToken);
18399
18890
  }
18891
+ async listProjectWorktrees(projectId) {
18892
+ const data = await this.api(
18893
+ `/api/projects/${encodeURIComponent(projectId)}/worktrees`
18894
+ );
18895
+ return data.worktrees ?? [];
18896
+ }
18400
18897
  async createProjectWorktree(projectId, input) {
18401
18898
  const data = await this.apiMutation(
18402
18899
  `/api/projects/${encodeURIComponent(projectId)}/worktrees`,
@@ -19790,6 +20287,22 @@ class ChatInputController {
19790
20287
  this.readline.off("close", this.handleClose);
19791
20288
  }
19792
20289
  }
20290
+ const MACOS_FIRMLINK_ALIASES = [
20291
+ ["/tmp", "/private/tmp"],
20292
+ ["/var", "/private/var"],
20293
+ ["/etc", "/private/etc"]
20294
+ ];
20295
+ function canonicalizeMacOsFirmlinkAlias(path) {
20296
+ for (const [logical, physical] of MACOS_FIRMLINK_ALIASES) {
20297
+ if (path === logical) return physical;
20298
+ if (path.startsWith(`${logical}/`)) return physical + path.slice(logical.length);
20299
+ }
20300
+ return path;
20301
+ }
20302
+ function canonicalizeRemotePathForComparison(value2, platform2) {
20303
+ const normalized = value2.trim().replace(/\/+$/, "");
20304
+ return platform2 === "darwin" ? canonicalizeMacOsFirmlinkAlias(normalized) : normalized;
20305
+ }
19793
20306
  function worktreeTargetMismatch(record2, expected) {
19794
20307
  if (record2.projectId !== expected.projectId) return "project";
19795
20308
  if (record2.machineId !== expected.machineId) return "machine";
@@ -19847,32 +20360,15 @@ function resolveWorktreeName(input) {
19847
20360
  }
19848
20361
  const MANAGED_WORKTREE_SEGMENT = "/.aplus/worktrees/";
19849
20362
  const MAX_MANAGED_WORKTREE_DEPTH = 2;
19850
- function isWorktreePathWithinProjectRoot(projectRoot, worktreePath) {
19851
- const root = projectRoot.trim().replace(/\/+$/, "");
19852
- const path = worktreePath.trim().replace(/\/+$/, "");
20363
+ function isWorktreePathWithinProjectRoot(projectRoot, worktreePath, platform2) {
20364
+ const root = canonicalizeRemotePathForComparison(projectRoot, platform2);
20365
+ const path = canonicalizeRemotePathForComparison(worktreePath, platform2);
19853
20366
  if (!root || !path) return false;
19854
20367
  if (!path.startsWith(`${root}${MANAGED_WORKTREE_SEGMENT}`)) return false;
19855
20368
  const segments = path.slice(root.length + MANAGED_WORKTREE_SEGMENT.length).split("/");
19856
20369
  if (segments.length < 1 || segments.length > MAX_MANAGED_WORKTREE_DEPTH) return false;
19857
20370
  return segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
19858
20371
  }
19859
- async function createWorktreeOnMachine(input) {
19860
- const record2 = await input.client.createProjectWorktree(input.projectId, {
19861
- name: input.name,
19862
- baseRef: input.baseRef,
19863
- machineId: input.machineId,
19864
- ...input.confirmRepoRoot ? { confirmRepoRoot: input.confirmRepoRoot } : {}
19865
- });
19866
- const targetMismatch = worktreeTargetMismatch(record2, {
19867
- projectId: input.projectId,
19868
- machineId: input.machineId
19869
- });
19870
- const allowedRoot = input.confirmRepoRoot ?? input.repoCwd;
19871
- const pathMatches = isWorktreePathWithinProjectRoot(allowedRoot, record2.path);
19872
- if (!targetMismatch && pathMatches) return record2;
19873
- await input.client.deleteProjectWorktree(input.projectId, record2.id, true).catch(() => void 0);
19874
- throw new Error(!pathMatches && input.pathMismatchMessage ? input.pathMismatchMessage(record2.path) : input.targetMismatchMessage);
19875
- }
19876
20372
  const WORKTREE_SUBDIR_CONFIRM_CODE = "worktree-subdir-confirm-required";
19877
20373
  function trimmedString(value2) {
19878
20374
  if (typeof value2 !== "string") return null;
@@ -19912,6 +20408,23 @@ function parseWorktreeSubdirConfirmRequest(status, body, projectRoot) {
19912
20408
  if (!isSafeRelativeDir(repoRelativeDir)) return null;
19913
20409
  return { repoRoot, workspaceDir, repoRelativeDir };
19914
20410
  }
20411
+ async function createWorktreeOnMachine(input) {
20412
+ const record2 = await input.client.createProjectWorktree(input.projectId, {
20413
+ name: input.name,
20414
+ baseRef: input.baseRef,
20415
+ machineId: input.machineId,
20416
+ ...input.confirmRepoRoot ? { confirmRepoRoot: input.confirmRepoRoot } : {}
20417
+ });
20418
+ const targetMismatch = worktreeTargetMismatch(record2, {
20419
+ projectId: input.projectId,
20420
+ machineId: input.machineId
20421
+ });
20422
+ const allowedRoot = input.confirmRepoRoot ?? input.repoCwd;
20423
+ const pathMatches = isWorktreePathWithinProjectRoot(allowedRoot, record2.path, input.machinePlatform);
20424
+ if (!targetMismatch && pathMatches) return record2;
20425
+ await input.client.deleteProjectWorktree(input.projectId, record2.id, true).catch(() => void 0);
20426
+ throw new Error(!pathMatches && input.pathMismatchMessage ? input.pathMismatchMessage(record2.path) : input.targetMismatchMessage);
20427
+ }
19915
20428
  async function createCliProjectWorktree(input, deps) {
19916
20429
  const createInput = {
19917
20430
  client: {
@@ -20429,6 +20942,7 @@ const USAGE_SETTINGS_NAVIGATION_DESTINATIONS = [
20429
20942
  "settings_memory",
20430
20943
  "settings_conversation_search",
20431
20944
  "settings_connectors",
20945
+ "settings_my_ai_credentials",
20432
20946
  "settings_extensions",
20433
20947
  "settings_about"
20434
20948
  ];
@@ -22742,6 +23256,50 @@ function readMachineId(input, readFile2) {
22742
23256
  function spawnBudgetKey(env) {
22743
23257
  return env.agentId ?? env.root;
22744
23258
  }
23259
+ function normalizeAbsolutePath(raw) {
23260
+ const trimmed = raw.trim();
23261
+ if (!trimmed.startsWith("/")) return "";
23262
+ const resolved = [];
23263
+ for (const segment of trimmed.split("/")) {
23264
+ if (!segment || segment === ".") continue;
23265
+ if (segment === "..") {
23266
+ resolved.pop();
23267
+ continue;
23268
+ }
23269
+ resolved.push(segment);
23270
+ }
23271
+ return `/${resolved.join("/")}`;
23272
+ }
23273
+ function isInsideTree(path, root) {
23274
+ const normalizedRoot = normalizeAbsolutePath(root);
23275
+ if (!normalizedRoot || !path) return false;
23276
+ if (path === normalizedRoot) return true;
23277
+ return path.startsWith(normalizedRoot === "/" ? "/" : `${normalizedRoot}/`);
23278
+ }
23279
+ function resolveSpawnContextFiles(input) {
23280
+ const resolved = [];
23281
+ for (const original of input.paths) {
23282
+ const path = normalizeAbsolutePath(original);
23283
+ if (!path || !isInsideTree(path, input.root)) {
23284
+ return { ok: false, reason: "out_of_scope", path: original };
23285
+ }
23286
+ if (!input.isReadableFile(path)) {
23287
+ return { ok: false, reason: "file_unreadable", path: original };
23288
+ }
23289
+ resolved.push(path);
23290
+ }
23291
+ return { ok: true, paths: resolved };
23292
+ }
23293
+ function composeSpawnPrompt(text2, contextPaths) {
23294
+ if (contextPaths.length === 0) return text2;
23295
+ const files = contextPaths.map((path) => `- ${path}`).join("\n");
23296
+ return [
23297
+ "Read these files first — they are the context for this task:",
23298
+ files,
23299
+ "",
23300
+ text2
23301
+ ].join("\n");
23302
+ }
22745
23303
  const HANDOFF_INLINE_LIMIT = 12e3;
22746
23304
  const TRUNCATION_NOTICE = "(대화가 길어 앞부분이 잘렸습니다. 아래는 가장 최근 대화입니다.)";
22747
23305
  const HEADER_TITLE_LIMIT = 200;
@@ -22808,6 +23366,13 @@ function buildAgentHandoffPrompt(source, context) {
22808
23366
  ...FIRST_ACTIONS.map((action) => `- ${action}`)
22809
23367
  ].join("\n");
22810
23368
  }
23369
+ async function fileSpawnedSessionUnderProject(runtime, sessionId, directory) {
23370
+ if (!runtime.registerSpawnedSession) return;
23371
+ try {
23372
+ await runtime.registerSpawnedSession({ sessionId, directory });
23373
+ } catch {
23374
+ }
23375
+ }
22811
23376
  function quote(text2) {
22812
23377
  return text2.split("\n").map((line2) => line2 ? `> ${line2}` : ">").join("\n");
22813
23378
  }
@@ -22881,6 +23446,7 @@ async function handoffToSibling(runtime, scope, env, handoff, agent) {
22881
23446
  root: env.root,
22882
23447
  depth: env.depth,
22883
23448
  childSessionId: sessionId,
23449
+ ...env.sessionId ? { parentSessionId: env.sessionId } : {},
22884
23450
  agent,
22885
23451
  promptPreview: `handoff from ${from}`,
22886
23452
  outcome: sessionId ? "spawned" : "failed"
@@ -22892,6 +23458,7 @@ async function handoffToSibling(runtime, scope, env, handoff, agent) {
22892
23458
  if (!sessionId) {
22893
23459
  return agentFail("spawn_failed", `the machine daemon did not confirm a session: ${String(spawnError)}`);
22894
23460
  }
23461
+ await fileSpawnedSessionUnderProject(runtime, sessionId, env.root);
22895
23462
  return agentOk({
22896
23463
  sessionId,
22897
23464
  agent,
@@ -22929,6 +23496,107 @@ ${clamped.text}` : clamped.text
22929
23496
  }
22930
23497
  );
22931
23498
  }
23499
+ function isRecord(value2, allowCli) {
23500
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return false;
23501
+ const record2 = value2;
23502
+ return Object.keys(record2).length === 4 && typeof record2.parentSessionId === "string" && record2.parentSessionId.length > 0 && typeof record2.childSessionId === "string" && record2.childSessionId.length > 0 && record2.parentSessionId !== record2.childSessionId && Number.isFinite(record2.spawnedAt) && Number(record2.spawnedAt) >= 0 && (record2.source === "handoff" || record2.source === "autopilot-fanout" || allowCli && record2.source === "cli");
23503
+ }
23504
+ function validatedRecords(value2, allowCli = true) {
23505
+ if (!Array.isArray(value2) || !value2.every((record2) => isRecord(record2, allowCli))) return null;
23506
+ const parentByChild = /* @__PURE__ */ new Map();
23507
+ for (const record2 of value2) {
23508
+ if (parentByChild.has(record2.childSessionId)) return null;
23509
+ parentByChild.set(record2.childSessionId, record2.parentSessionId);
23510
+ }
23511
+ for (const child of parentByChild.keys()) {
23512
+ const visited = /* @__PURE__ */ new Set([child]);
23513
+ let current = child;
23514
+ while (parentByChild.has(current)) {
23515
+ current = parentByChild.get(current);
23516
+ if (visited.has(current)) return null;
23517
+ visited.add(current);
23518
+ }
23519
+ }
23520
+ return value2.map((record2) => ({ ...record2 }));
23521
+ }
23522
+ function isDescendant(records, ancestorSessionId, targetSessionId, maxDepth = 2) {
23523
+ if (!ancestorSessionId || !targetSessionId || ancestorSessionId === targetSessionId || maxDepth < 1) return false;
23524
+ const valid = validatedRecords(records);
23525
+ if (!valid) return false;
23526
+ const parentByChild = new Map(valid.map((record2) => [record2.childSessionId, record2.parentSessionId]));
23527
+ let current = targetSessionId;
23528
+ for (let depth = 1; depth <= maxDepth; depth += 1) {
23529
+ const parent = parentByChild.get(current);
23530
+ if (!parent) return false;
23531
+ if (parent === ancestorSessionId) return true;
23532
+ current = parent;
23533
+ }
23534
+ return false;
23535
+ }
23536
+ function resolveSubagentControlPermission(records, parentSessionId, childSessionId) {
23537
+ const valid = validatedRecords(records);
23538
+ return valid ? isDescendant(valid, parentSessionId, childSessionId, 2) : false;
23539
+ }
23540
+ function canControlSpawnedDescendant(records, parentSessionId, childSessionId) {
23541
+ const lineage = records.flatMap((record2) => record2.outcome === "spawned" && record2.parentSessionId && record2.childSessionId ? [{
23542
+ parentSessionId: record2.parentSessionId,
23543
+ childSessionId: record2.childSessionId,
23544
+ spawnedAt: record2.ts,
23545
+ source: "handoff"
23546
+ }] : []);
23547
+ return resolveSubagentControlPermission(lineage, parentSessionId, childSessionId);
23548
+ }
23549
+ function ownershipFailure(runtime, env, target) {
23550
+ if (!env.sessionId) return agentFail("session_unknown", "This session did not receive APLUS_SESSION_ID.");
23551
+ if (!runtime.readLineage) return agentFail("not_owned", "spawn lineage is unavailable");
23552
+ try {
23553
+ return canControlSpawnedDescendant(runtime.readLineage(), env.sessionId, target.session.id) ? null : agentFail("not_owned", `${target.session.id} is not an owned descendant`);
23554
+ } catch (error) {
23555
+ return agentFail("not_owned", `spawn ledger is unreadable: ${String(error)}`);
23556
+ }
23557
+ }
23558
+ async function steerOwnedTarget(runtime, env, target, text2) {
23559
+ if (!text2.trim()) return agentFail("steer_rejected", "steer text is empty");
23560
+ const refused = ownershipFailure(runtime, env, target);
23561
+ if (refused) return refused;
23562
+ if (!runtime.steerSession) return agentFail("steer_rejected", "steer transport is unavailable");
23563
+ try {
23564
+ const result = await runtime.steerSession(target.session.id, text2);
23565
+ if (result.accepted) {
23566
+ return agentOk({
23567
+ sessionId: target.session.id,
23568
+ accepted: true,
23569
+ // RPC acknowledgement proves acceptance, not transcript delivery.
23570
+ delivered: false
23571
+ });
23572
+ }
23573
+ return result.reason === "in-flight" ? agentFail("steer_in_flight", result.error) : agentFail("steer_rejected", result.error);
23574
+ } catch (error) {
23575
+ return agentFail("steer_rejected", error instanceof Error ? error.message : String(error));
23576
+ }
23577
+ }
23578
+ async function stopOwnedTarget(runtime, env, target) {
23579
+ const refused = ownershipFailure(runtime, env, target);
23580
+ if (refused) return refused;
23581
+ 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
+ try {
23594
+ 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);
23596
+ } catch (error) {
23597
+ return agentFail("stop_rejected", error instanceof Error ? error.message : String(error));
23598
+ }
23599
+ }
22932
23600
  const DEFAULT_READ_LIMIT = 20;
22933
23601
  const STATE_WINDOW_MESSAGES = 50;
22934
23602
  function scopeOf(runtime) {
@@ -22982,10 +23650,18 @@ async function readSession(runtime, target, last) {
22982
23650
  });
22983
23651
  }
22984
23652
  const DEFAULT_SPAWN_AGENT = "claude";
22985
- async function spawnSibling(runtime, env, text2, agent) {
23653
+ async function spawnSibling(runtime, env, text2, agent, contextFiles = []) {
22986
23654
  if (env.depth >= AGENT_MAX_DEPTH) {
22987
23655
  return agentFail("spawn_depth_exceeded", `depth ${env.depth} is already at the ceiling of ${AGENT_MAX_DEPTH}`);
22988
23656
  }
23657
+ const context = resolveSpawnContextFiles({
23658
+ paths: contextFiles,
23659
+ root: env.root,
23660
+ isReadableFile: runtime.isReadableFile
23661
+ });
23662
+ if (!context.ok) {
23663
+ return context.reason === "out_of_scope" ? agentFail("out_of_scope", `${context.path} is outside ${env.root}`) : agentFail("file_unreadable", `${context.path} could not be read`);
23664
+ }
22989
23665
  const key = spawnBudgetKey(env);
22990
23666
  let spawned;
22991
23667
  try {
@@ -23002,7 +23678,7 @@ async function spawnSibling(runtime, env, text2, agent) {
23002
23678
  sessionId = (await runtime.spawnSession({
23003
23679
  directory: env.root,
23004
23680
  agent,
23005
- initialPrompt: text2,
23681
+ initialPrompt: composeSpawnPrompt(text2, context.paths),
23006
23682
  environmentVariables: childAgentEnv(env, childId)
23007
23683
  })).sessionId;
23008
23684
  } catch (error) {
@@ -23015,6 +23691,7 @@ async function spawnSibling(runtime, env, text2, agent) {
23015
23691
  root: env.root,
23016
23692
  depth: env.depth,
23017
23693
  childSessionId: sessionId,
23694
+ ...env.sessionId ? { parentSessionId: env.sessionId } : {},
23018
23695
  agent,
23019
23696
  promptPreview: text2,
23020
23697
  outcome: sessionId ? "spawned" : "failed"
@@ -23026,6 +23703,7 @@ async function spawnSibling(runtime, env, text2, agent) {
23026
23703
  if (!sessionId) {
23027
23704
  return agentFail("spawn_failed", `the machine daemon did not confirm a session: ${String(spawnError)}`);
23028
23705
  }
23706
+ await fileSpawnedSessionUnderProject(runtime, sessionId, env.root);
23029
23707
  return agentOk({ sessionId, agent, directory: env.root, depth: env.depth + 1, spawned: spawned + 1, maxSpawn: env.maxSpawn });
23030
23708
  }
23031
23709
  async function runAgentCommand(runtime, args) {
@@ -23060,7 +23738,7 @@ async function runAgentCommand(runtime, args) {
23060
23738
  if (args.handoff) {
23061
23739
  return handoffToSibling(runtime, scope, env, args.handoff, args.agent ?? DEFAULT_SPAWN_AGENT);
23062
23740
  }
23063
- return spawnSibling(runtime, env, args.text ?? "", args.agent ?? DEFAULT_SPAWN_AGENT);
23741
+ return spawnSibling(runtime, env, args.text ?? "", args.agent ?? DEFAULT_SPAWN_AGENT, args.files ?? []);
23064
23742
  }
23065
23743
  if (!args.sessionId) return agentFail("session_not_found", "no session id given");
23066
23744
  const target = await resolveTarget(runtime, scope, args.sessionId);
@@ -23077,10 +23755,19 @@ async function runAgentCommand(runtime, args) {
23077
23755
  args.text ?? "",
23078
23756
  args.wait ? { until: args.until ?? "turn-end", timeoutMs: clampWaitTimeout(args.timeoutMs) } : null
23079
23757
  );
23758
+ case "steer":
23759
+ return steerOwnedTarget(runtime, env, target, args.text ?? "");
23760
+ case "stop":
23761
+ return stopOwnedTarget(runtime, env, target);
23080
23762
  default:
23081
23763
  return agentFail("session_not_found", `unsupported verb "${args.verb}"`);
23082
23764
  }
23083
23765
  }
23766
+ function parseSpawnRecord(value2) {
23767
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return null;
23768
+ const record2 = value2;
23769
+ return typeof record2.ts === "number" && Number.isFinite(record2.ts) && typeof record2.agentKey === "string" && record2.agentKey.length > 0 && typeof record2.root === "string" && record2.root.length > 0 && typeof record2.depth === "number" && Number.isInteger(record2.depth) && record2.depth >= 0 && (record2.childSessionId === null || typeof record2.childSessionId === "string" && record2.childSessionId.length > 0) && (record2.parentSessionId === void 0 || typeof record2.parentSessionId === "string" && record2.parentSessionId.length > 0) && typeof record2.agent === "string" && record2.agent.length > 0 && typeof record2.promptPreview === "string" && (record2.outcome === "spawned" || record2.outcome === "failed") ? record2 : null;
23770
+ }
23084
23771
  const LEDGER_FILE_MODE = 384;
23085
23772
  const PREVIEW_MAX_LENGTH = 200;
23086
23773
  function agentLineagePath(homeDir) {
@@ -23099,7 +23786,8 @@ function readSpawnRecords(homeDir) {
23099
23786
  if (!line2.trim()) continue;
23100
23787
  try {
23101
23788
  const parsed = JSON.parse(line2);
23102
- if (parsed && typeof parsed.agentKey === "string") records.push(parsed);
23789
+ const record2 = parseSpawnRecord(parsed);
23790
+ if (record2) records.push(record2);
23103
23791
  } catch {
23104
23792
  continue;
23105
23793
  }
@@ -23116,8 +23804,106 @@ function appendSpawnRecord(homeDir, record2) {
23116
23804
  appendFileSync(path, `${JSON.stringify(entry)}
23117
23805
  `, { mode: LEDGER_FILE_MODE });
23118
23806
  }
23807
+ function normalize(raw) {
23808
+ const trimmed = raw.trim();
23809
+ if (!trimmed) return "";
23810
+ const stripped = trimmed.replace(/\/+$/, "");
23811
+ return stripped || "/";
23812
+ }
23813
+ function onlyOne(matches) {
23814
+ return matches.length === 1 ? matches[0] : null;
23815
+ }
23816
+ function matchSpawnProject(input) {
23817
+ const machineId = input.machineId.trim();
23818
+ const directory = normalize(input.directory);
23819
+ if (!machineId || !directory) return null;
23820
+ const local = input.projects.filter((project) => project.machineId && project.machineId === machineId);
23821
+ const byWorkspace = local.filter((project) => normalize(project.workspaceDir) === directory);
23822
+ const workspaceHit = onlyOne(byWorkspace);
23823
+ if (workspaceHit) return { projectId: workspaceHit.id };
23824
+ if (byWorkspace.length > 1) return null;
23825
+ const byWorktree = local.flatMap((project) => (input.worktreesByProjectId[project.id] ?? []).filter((tree) => !tree.removedAt && tree.machineId === machineId && normalize(tree.path) === directory).map((tree) => ({ projectId: project.id, worktreeId: tree.id })));
23826
+ return onlyOne(byWorktree);
23827
+ }
23828
+ function readFileOrNull$1(path) {
23829
+ try {
23830
+ return readFileSync(path, "utf8");
23831
+ } catch {
23832
+ return null;
23833
+ }
23834
+ }
23835
+ async function fileSessionUnderProject(context, env, input) {
23836
+ const machineId = readMachineId(
23837
+ {
23838
+ homeDir: context.homeDir,
23839
+ env,
23840
+ credentialHome: context.credential.source === "saycode" ? null : dirname(context.credential.path)
23841
+ },
23842
+ readFileOrNull$1
23843
+ );
23844
+ if (!machineId) return;
23845
+ const projects = await loadWorkspaceProjects(context);
23846
+ const direct = matchSpawnProject({ machineId, directory: input.directory, projects, worktreesByProjectId: {} });
23847
+ if (direct) {
23848
+ await context.rest.assignSessionToProject(direct.projectId, input.sessionId);
23849
+ return;
23850
+ }
23851
+ const local = projects.filter((project) => project.machineId === machineId);
23852
+ const worktreesByProjectId = {};
23853
+ for (const project of local) {
23854
+ worktreesByProjectId[project.id] = await context.rest.listProjectWorktrees(project.id);
23855
+ }
23856
+ const viaWorktree = matchSpawnProject({ machineId, directory: input.directory, projects, worktreesByProjectId });
23857
+ if (!viaWorktree) return;
23858
+ await context.rest.assignSessionToProject(viaWorktree.projectId, input.sessionId, viaWorktree.worktreeId);
23859
+ }
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
+ }
23876
+ }
23877
+ async function stopAgentSession(transport, machineId, sessionId) {
23878
+ try {
23879
+ const result = await transport.rpc(machineId, "stop-session", {
23880
+ sessionId,
23881
+ source: "saycode-agent-stop",
23882
+ reason: "user-requested",
23883
+ mode: "force"
23884
+ });
23885
+ if (result?.stopped === true) return { stopped: true };
23886
+ return {
23887
+ stopped: false,
23888
+ error: typeof result?.reason === "string" ? result.reason : "Machine returned an invalid stop acknowledgement."
23889
+ };
23890
+ } catch (error) {
23891
+ return {
23892
+ stopped: false,
23893
+ error: error instanceof Error ? error.message : String(error)
23894
+ };
23895
+ }
23896
+ }
23119
23897
  const STREAM_HISTORY_LIMIT = 50;
23120
23898
  const SPAWN_RPC_TIMEOUT_MS = 6e4;
23899
+ function isReadableFileSync(path) {
23900
+ try {
23901
+ accessSync(path, constants$1.R_OK);
23902
+ return statSync(path).isFile();
23903
+ } catch {
23904
+ return false;
23905
+ }
23906
+ }
23121
23907
  function readFileOrNull(path) {
23122
23908
  try {
23123
23909
  return readFileSync(path, "utf8");
@@ -23128,7 +23914,7 @@ function readFileOrNull(path) {
23128
23914
  async function recentContents(context, transport, sessionId, limit) {
23129
23915
  const messages = await context.rest.recentMessages(sessionId, limit);
23130
23916
  const contents = [];
23131
- for (const message of messages) {
23917
+ for (const message of [...messages].sort((left, right) => left.seq - right.seq)) {
23132
23918
  const ciphertext = unwrapEncryptedField(message.content);
23133
23919
  if (!ciphertext) continue;
23134
23920
  const content = decodeContent(transport, ciphertext);
@@ -23197,6 +23983,7 @@ function createAgentRuntime(context, transport, env = process.env) {
23197
23983
  credentialHome,
23198
23984
  now: () => Date.now(),
23199
23985
  readFile: readFileOrNull,
23986
+ isReadableFile: isReadableFileSync,
23200
23987
  listSessions: async () => normalizeSessionRows(await context.rest.listSessions(), keys),
23201
23988
  readMessages: (sessionId, limit) => recentContents(context, transport, sessionId, limit),
23202
23989
  openStream: (sessionId) => openStream(context, transport, sessionId),
@@ -23228,7 +24015,13 @@ function createAgentRuntime(context, transport, env = process.env) {
23228
24015
  },
23229
24016
  newAgentId: () => randomBytes(8).toString("hex"),
23230
24017
  countSpawns: (agentKey) => countSpawnsFor(homeDir, agentKey),
23231
- recordSpawn: (record2) => appendSpawnRecord(homeDir, record2)
24018
+ recordSpawn: (record2) => appendSpawnRecord(homeDir, record2),
24019
+ readLineage: () => readSpawnRecords(homeDir),
24020
+ steerSession: (sessionId, text2) => steerAgentSession(transport, sessionId, text2),
24021
+ stopSession: (machineId, sessionId) => stopAgentSession(transport, machineId, sessionId),
24022
+ // Omitted entirely without API access: a Happy `access.key` is sync-only, so
24023
+ // there is no project list to match against and nothing to register with.
24024
+ ...context.hasApiAccess ? { registerSpawnedSession: (input) => fileSessionUnderProject(context, env, input) } : {}
23232
24025
  };
23233
24026
  }
23234
24027
  async function contextFor(workspace) {