@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.
- package/README.md +22 -3
- package/index.mjs +923 -130
- 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.
|
|
43
|
+
const version = "0.3.1";
|
|
44
44
|
const manifest = {
|
|
45
45
|
version
|
|
46
46
|
};
|
|
47
|
-
const
|
|
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 {
|
|
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.
|
|
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
|
|
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": "
|
|
1274
|
-
"common.reset": "
|
|
1275
|
-
"common.cancel": "
|
|
1276
|
-
"common.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.
|
|
1311
|
-
"login.
|
|
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": "
|
|
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": "
|
|
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": "
|
|
1332
|
-
"account.demoWorkspace": "
|
|
1333
|
-
"account.connectedWorkspace": "
|
|
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": "
|
|
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": "
|
|
1689
|
-
"settings.title": "
|
|
1690
|
-
"settings.close": "
|
|
1916
|
+
"settings.eyebrow": "환경설정",
|
|
1917
|
+
"settings.title": "데스크톱 설정",
|
|
1918
|
+
"settings.close": "설정 닫기",
|
|
1691
1919
|
// Settings — Notifications tab
|
|
1692
|
-
"settings.notifications.loading": "
|
|
1693
|
-
"settings.notifications.mobilePush": "
|
|
1694
|
-
"settings.notifications.updated": "
|
|
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": "
|
|
1701
|
-
"settings.visualEffects.loading": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
1977
|
+
"composer.messageAria": "메시지",
|
|
1750
1978
|
"composer.placeholder": "데스크톱에서 보낼 메시지를 입력",
|
|
1751
1979
|
"composer.promptSuggestionPaused": "{window} 사용량 {percent}% — 사용량 경고 중에는 Claude가 추천 질문을 만들지 않습니다",
|
|
1752
1980
|
"composer.promptSuggestionPausedReset": "{reset} 초기화",
|
|
1753
|
-
"composer.starting": "
|
|
1754
|
-
"composer.sending": "
|
|
1755
|
-
"composer.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": "
|
|
1991
|
+
"composer.send": "보내기",
|
|
1764
1992
|
"composer.stopping": "중단 중...",
|
|
1765
1993
|
"composer.stop": "중단",
|
|
1766
1994
|
// Common (shared)
|
|
1767
|
-
"common.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}
|
|
1787
|
-
"worktreeDialog.closeAria": "
|
|
1788
|
-
"worktreeDialog.listAria": "{name}
|
|
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}
|
|
2039
|
+
"gitControls.pullAria": "{branch} 브랜치 pull",
|
|
1805
2040
|
"gitControls.pullTitle": "{branch} pull",
|
|
1806
|
-
"gitControls.pushAria": "{branch}
|
|
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": "
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
3685
|
-
const nestedTool = isRecord$
|
|
3686
|
-
const nestedFunction = nestedTool && isRecord$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
7014
|
-
const rawCursor = Array.isArray(value2) ? null : isRecord$
|
|
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
|
|
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:
|
|
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
|
-
|
|
16774
|
-
|
|
16775
|
-
|
|
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
|
-
|
|
18067
|
-
|
|
18068
|
-
|
|
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
|
|
19852
|
-
const path = worktreePath
|
|
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
|
-
|
|
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) {
|