@buzzni/saycode-cli 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -3
- package/index.mjs +802 -96
- 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.0";
|
|
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}",
|
|
@@ -787,6 +824,9 @@ const personalChatsKo = {
|
|
|
787
824
|
"personalChats.send": "전송",
|
|
788
825
|
"personalChats.creating": "채팅을 시작하는 중…",
|
|
789
826
|
"personalChats.noOnlineMachine": "온라인 머신이 없습니다. 채팅을 시작하려면 머신을 시작해주세요.",
|
|
827
|
+
"personalChats.localMachinePreparing": "이 컴퓨터를 Saycode에 연결하는 중이에요. 잠시 후 바로 시작할 수 있어요.",
|
|
828
|
+
"personalChats.localMachineAsk": "이 컴퓨터를 바로 사용할 수 있어요. 클릭 한 번으로 연결을 마치세요.",
|
|
829
|
+
"personalChats.localMachineFailed": "이 컴퓨터 자동 연결에 실패했어요. 다른 방법으로 연결할 수 있어요.",
|
|
790
830
|
"personalChats.pickMachine": "이 채팅을 실행할 머신을 선택하세요",
|
|
791
831
|
"personalChats.back": "뒤로",
|
|
792
832
|
"personalChats.stop": "중지",
|
|
@@ -851,6 +891,44 @@ const agentLoopDetectionKo = {
|
|
|
851
891
|
"settings.agentLoopAutoStop.help": "오토파일럿이 관리하는 세션이 같은 툴 호출이나 텍스트를 반복하며 멈춘 것으로 보이면 자동으로 응답을 중지합니다. 기본은 꺼짐 — 오탐으로 정상 작업을 멈추는 것이 놓치는 것보다 나쁩니다.",
|
|
852
892
|
"settings.agentLoopAutoStop.saveFailed": "설정을 저장하지 못했습니다."
|
|
853
893
|
};
|
|
894
|
+
const autopilotSelfReviewRoundsKo = {
|
|
895
|
+
"settings.autopilotSelfReviewRounds.label": "오토파일럿 셀프 리뷰 반복 횟수",
|
|
896
|
+
"settings.autopilotSelfReviewRounds.help": '오토파일럿이 "현재 에이전트로 점검"을 몇 번까지 이어서 시킬지 정합니다. 점검 결과에 아직 해결하지 못한 문제가 남아 있으면 남은 횟수만큼 다시 점검하게 하고, 문제가 없어지면 남은 횟수와 상관없이 바로 Commit & PR로 넘어갑니다. 지정한 횟수를 다 쓰고도 문제가 남으면 지금처럼 멈추고 사람에게 넘깁니다. 기본은 1회(지금까지와 동일).',
|
|
897
|
+
"settings.autopilotSelfReviewRounds.once": "1회 (반복 없음)",
|
|
898
|
+
"settings.autopilotSelfReviewRounds.times": "{count}회",
|
|
899
|
+
"settings.autopilotSelfReviewRounds.saveFailed": "설정을 저장하지 못했습니다.",
|
|
900
|
+
"agentBoard.autopilot.reviewingSelfRound": "오토파일럿 · 셀프 리뷰 중 {round}/{total}"
|
|
901
|
+
};
|
|
902
|
+
const externalWorktreeInboxKo = {
|
|
903
|
+
"externalWorktreeInbox.title": "앱 밖에서 만든 worktree",
|
|
904
|
+
"externalWorktreeInbox.description": "이 저장소에 앱이 만들지 않은 worktree 가 있습니다. 채택하면 목록과 새 대화 시작에서 고를 수 있어요.",
|
|
905
|
+
"externalWorktreeInbox.adopt": "채택",
|
|
906
|
+
"externalWorktreeInbox.ignore": "무시",
|
|
907
|
+
"externalWorktreeInbox.adoptAria": "외부 worktree {name} 채택",
|
|
908
|
+
"externalWorktreeInbox.ignoreAria": "외부 worktree {name} 무시",
|
|
909
|
+
"externalWorktreeInbox.externalBadge": "외부",
|
|
910
|
+
"externalWorktreeInbox.deleteBlocked": "앱 밖에서 만든 worktree 라 앱에서 삭제하지 않습니다.",
|
|
911
|
+
"externalWorktreeInbox.adoptRejectedStaleScan": "worktree 목록이 최신이 아닙니다. 새로고침한 뒤 다시 채택하세요.",
|
|
912
|
+
"externalWorktreeInbox.adoptRejectedNotOwned": "이 프로젝트 저장소의 worktree 가 아니라서 채택하지 않았습니다."
|
|
913
|
+
};
|
|
914
|
+
const externalSessionVaultKo = {
|
|
915
|
+
"externalSessionVault.openCommand": "앱 밖에서 시작한 대화 이어가기",
|
|
916
|
+
"externalSessionVault.dialogAria": "{name}의 앱 밖에서 시작한 대화",
|
|
917
|
+
"externalSessionVault.title": "앱 밖에서 시작한 대화",
|
|
918
|
+
"externalSessionVault.description": "이 프로젝트 폴더에서 터미널로 돌린 Claude Code 세션입니다. 이어가면 그 대화를 그대로 잇는 앱 대화가 시작됩니다.",
|
|
919
|
+
"externalSessionVault.closeAria": "닫기",
|
|
920
|
+
"externalSessionVault.loading": "앱 밖에서 시작한 대화를 찾는 중입니다…",
|
|
921
|
+
"externalSessionVault.empty": "이 프로젝트에서 앱 밖 대화를 찾지 못했습니다.",
|
|
922
|
+
"externalSessionVault.failed": "앱 밖에서 시작한 대화를 읽지 못했습니다.",
|
|
923
|
+
"externalSessionVault.untitled": "제목 없는 대화",
|
|
924
|
+
"externalSessionVault.resume": "이어가기",
|
|
925
|
+
"externalSessionVault.ignore": "무시",
|
|
926
|
+
"externalSessionVault.resumeAria": "{title} 대화 이어가기",
|
|
927
|
+
"externalSessionVault.ignoreAria": "{title} 대화 무시",
|
|
928
|
+
"externalSessionVault.rejectedStaleScan": "목록이 최신이 아닙니다. 새로고침한 뒤 다시 시도하세요.",
|
|
929
|
+
"externalSessionVault.rejectedNotOwned": "이 프로젝트 폴더에서 돌린 대화가 아니라서 이어가지 않았습니다.",
|
|
930
|
+
"externalSessionVault.resumeFailed": "그 대화를 이어가지 못했습니다."
|
|
931
|
+
};
|
|
854
932
|
const operationModelRetryKo = {
|
|
855
933
|
"review.job.fallbackModelUsed": "폴백 모델 사용됨: {model}"
|
|
856
934
|
};
|
|
@@ -1184,9 +1262,84 @@ const sidebarUpdateKo = {
|
|
|
1184
1262
|
"sidebarUpdate.retry": "업데이트 확인 실패 — 다시 시도",
|
|
1185
1263
|
"sidebarUpdate.unsupported": "이 빌드에서는 자동 업데이트를 쓸 수 없습니다",
|
|
1186
1264
|
"sidebarUpdate.currentVersion": "현재 v{version}",
|
|
1187
|
-
"sidebarUpdate.newVersion": "v{version}"
|
|
1265
|
+
"sidebarUpdate.newVersion": "v{version}",
|
|
1266
|
+
"sidebarUpdate.installFailed": "업데이트가 적용되지 않았습니다 — v{expected} 로 바뀌었어야 하는데 여전히 v{actual} 입니다. 다시 확인"
|
|
1188
1267
|
};
|
|
1189
|
-
const
|
|
1268
|
+
const globalPauseKo = {
|
|
1269
|
+
"settings.globalPause.enable": "전역 일시정지 (새 자동 액션 차단)",
|
|
1270
|
+
"settings.globalPause.help": "켜면 예약 자동화 실행, GitHub 이벤트 트리거 발화, 보드 오토파일럿의 다음 단계가 시작되지 않습니다. 이미 진행 중인 대화는 중단되지 않으며, 끄면 보류된 작업이 다음 주기에 이어서 진행됩니다.",
|
|
1271
|
+
"settings.globalPause.scope": "이 스위치는 이 데스크탑 앱이 직접 실행하는 자동화에만 적용됩니다. 머신 데몬이 소유한 실행 경로는 막지 않습니다.",
|
|
1272
|
+
"settings.globalPause.saveFailed": "전역 일시정지를 전환하지 못했습니다."
|
|
1273
|
+
};
|
|
1274
|
+
const terminalFontKo = {
|
|
1275
|
+
"settings.terminalFont.cardTitle": "터미널 폰트",
|
|
1276
|
+
"settings.terminalFont.sizeLabel": "글자 크기",
|
|
1277
|
+
"settings.terminalFont.sizeHelp": "{min}~{max} 사이의 정수를 입력하세요.",
|
|
1278
|
+
"settings.terminalFont.sizeInvalid": "{min}~{max} 사이의 정수만 적용됩니다.",
|
|
1279
|
+
"settings.terminalFont.familyLabel": "폰트 이름",
|
|
1280
|
+
"settings.terminalFont.familyHelp": "CSS font-family 값입니다. 쉼표로 여러 폰트를 적으면 앞에서부터 사용할 수 있는 것을 씁니다. 비워 두면 기본 폰트로 돌아갑니다.",
|
|
1281
|
+
"settings.terminalFont.cardHelp": "이 기기의 원격 터미널에 적용됩니다. 열려 있는 터미널에도 재연결 없이 바로 반영됩니다.",
|
|
1282
|
+
"settings.terminalFont.reset": "기본값으로 되돌리기"
|
|
1283
|
+
};
|
|
1284
|
+
const subagentKo = {
|
|
1285
|
+
"subagent.title": "자식 에이전트 {count}개",
|
|
1286
|
+
"subagent.open": "자식 대화 열기",
|
|
1287
|
+
"subagent.status.responding": "응답 중",
|
|
1288
|
+
"subagent.status.waiting": "입력 대기",
|
|
1289
|
+
"subagent.status.completed": "완료",
|
|
1290
|
+
"subagent.status.stopped": "중단됨",
|
|
1291
|
+
"subagent.steer.placeholder": "현재 턴에 전달할 지시",
|
|
1292
|
+
"subagent.steer.action": "지시 전달",
|
|
1293
|
+
"subagent.stop.action": "중단",
|
|
1294
|
+
"subagent.steer.requested": "전달 요청됨 · 다음 tool 경계에서 적용 예정",
|
|
1295
|
+
"subagent.steer.applied": "지시가 자식 대화에 적용됨",
|
|
1296
|
+
"subagent.steer.missed": "지시가 적용되지 못한 채 턴이 끝남",
|
|
1297
|
+
"subagent.error.notOwned": "이 대화에서 제어할 수 없는 세션입니다.",
|
|
1298
|
+
"subagent.error.empty": "전달할 지시를 입력하세요.",
|
|
1299
|
+
"subagent.error.inFlight": "이전 제어 요청을 처리하는 중입니다.",
|
|
1300
|
+
"subagent.error.rejected": "요청이 거부됐습니다: {detail}"
|
|
1301
|
+
};
|
|
1302
|
+
const aiCommitPrKo = {
|
|
1303
|
+
"aiCommitPr.stagedCount": "스테이지된 파일 {count}개",
|
|
1304
|
+
"aiCommitPr.noStaged": "스테이지된 변경이 없습니다.",
|
|
1305
|
+
"aiCommitPr.stageAll": "모든 변경 스테이지",
|
|
1306
|
+
"aiCommitPr.staging": "스테이지하는 중…",
|
|
1307
|
+
"aiCommitPr.messagePlaceholder": "커밋 메시지",
|
|
1308
|
+
"aiCommitPr.messageAria": "커밋 메시지",
|
|
1309
|
+
"aiCommitPr.stagedListAria": "스테이지된 파일 목록",
|
|
1310
|
+
"aiCommitPr.commit": "커밋",
|
|
1311
|
+
"aiCommitPr.committing": "커밋하는 중…",
|
|
1312
|
+
"aiCommitPr.committed": "커밋했습니다 ({hash})",
|
|
1313
|
+
"aiCommitPr.generate": "AI 생성",
|
|
1314
|
+
"aiCommitPr.cancelGenerate": "생성 취소",
|
|
1315
|
+
"aiCommitPr.overwriteConfirm": "이미 적어둔 메시지를 AI가 만든 것으로 바꿀까요?",
|
|
1316
|
+
"aiCommitPr.error.nothingStaged": "스테이지된 변경이 없어 커밋할 수 없습니다.",
|
|
1317
|
+
"aiCommitPr.error.commitFailed": "커밋하지 못했습니다.",
|
|
1318
|
+
"aiCommitPr.error.claudeMissing": "이 컴퓨터에서 claude 명령을 찾지 못했습니다.",
|
|
1319
|
+
"aiCommitPr.error.generationFailed": "메시지를 만들지 못했습니다.",
|
|
1320
|
+
"aiCommitPr.error.timedOut": "생성이 너무 오래 걸려 중단했습니다.",
|
|
1321
|
+
"aiCommitPr.error.nothingToDescribe": "설명할 변경이 없습니다.",
|
|
1322
|
+
"aiCommitPr.error.emptyOutput": "생성 결과가 비어 있습니다.",
|
|
1323
|
+
"aiCommitPr.error.machineOffline": "이 컴퓨터가 오프라인입니다.",
|
|
1324
|
+
"aiCommitPr.error.noResponse": "응답을 받지 못했습니다.",
|
|
1325
|
+
"aiCommitPr.error.cancelled": "생성을 취소했습니다.",
|
|
1326
|
+
"aiCommitPr.error.daemonBusy": "머신이 다른 작업으로 바쁩니다. 잠시 후 다시 시도하세요.",
|
|
1327
|
+
"aiCommitPr.error.commitNoResponse": "응답을 받지 못했습니다. 커밋이 완료됐을 수도 있으니 패널을 다시 열어 확인하세요.",
|
|
1328
|
+
"aiCommitPr.error.gitFailed": "git 명령이 실패했습니다.",
|
|
1329
|
+
"aiCommitPr.error.stagedProbeFailed": "스테이지된 변경을 읽지 못했습니다.",
|
|
1330
|
+
"aiCommitPr.error.stageFailed": "변경을 스테이지하지 못했습니다.",
|
|
1331
|
+
"aiCommitPr.settings.enable": "커밋 메시지와 PR 설명을 AI로 생성",
|
|
1332
|
+
"aiCommitPr.settings.help": "켜면 git 패널과 PR 만들기 화면에 “AI 생성” 버튼이 나타납니다. 생성은 이 컴퓨터에서 한 번 실행되며, 결과는 입력란에 채워질 뿐 자동으로 커밋하거나 PR을 만들지 않습니다. 기본은 꺼짐.",
|
|
1333
|
+
"aiCommitPr.settings.saveFailed": "설정을 저장하지 못했습니다.",
|
|
1334
|
+
"aiCommitPr.pr.dialogTitle": "Pull Request 만들기",
|
|
1335
|
+
"aiCommitPr.pr.dialogAria": "{branch} 브랜치의 Pull Request 내용 작성",
|
|
1336
|
+
"aiCommitPr.pr.titleLabel": "제목",
|
|
1337
|
+
"aiCommitPr.pr.bodyLabel": "설명",
|
|
1338
|
+
"aiCommitPr.pr.base": "{base} 브랜치로 병합",
|
|
1339
|
+
"aiCommitPr.pr.create": "Pull Request 만들기",
|
|
1340
|
+
"aiCommitPr.pr.creating": "만드는 중…"
|
|
1341
|
+
};
|
|
1342
|
+
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
1343
|
const onboardingKo = {
|
|
1191
1344
|
"onboarding.dialogAria": "시작 설정",
|
|
1192
1345
|
"onboarding.eyebrow": "처음 시작하기",
|
|
@@ -1208,6 +1361,14 @@ const onboardingKo = {
|
|
|
1208
1361
|
"onboarding.oneCommand.copy": "명령어 복사",
|
|
1209
1362
|
"onboarding.oneCommand.copied": "복사됨",
|
|
1210
1363
|
"onboarding.oneCommand.advanced": "고급: 수동 설치",
|
|
1364
|
+
"onboarding.localMachine.registering": "이 컴퓨터를 등록하는 중…",
|
|
1365
|
+
"onboarding.localMachine.registered": "✓ 이 컴퓨터가 등록되었습니다. 바로 작업할 수 있어요.",
|
|
1366
|
+
"onboarding.localMachine.askTitle": "이 컴퓨터를 바로 사용",
|
|
1367
|
+
"onboarding.localMachine.askBody": "이 컴퓨터에 이미 Happy 설정이 있습니다. 지금 설정으로 바꾸기 전에 원래 파일을 백업해 두고, 실패하면 되돌립니다.",
|
|
1368
|
+
"onboarding.localMachine.askAction": "이 컴퓨터를 바로 사용",
|
|
1369
|
+
"onboarding.localMachine.failed": "자동 등록에 실패했습니다. 아래 명령으로 직접 설정할 수 있어요.",
|
|
1370
|
+
"onboarding.localMachine.retry": "다시 시도",
|
|
1371
|
+
"onboarding.localMachine.remoteHint": "다른 컴퓨터나 조직 공유 머신은 머신 설정에서 추가할 수 있어요.",
|
|
1211
1372
|
"onboarding.machineDetected": "✓ {name} 연결됨",
|
|
1212
1373
|
"onboarding.standalone.title": "내장 서버와 로컬 머신",
|
|
1213
1374
|
"onboarding.standalone.body": "앱이 서버와 로컬 실행 머신을 자동으로 준비합니다. 별도 서버 명령은 필요하지 않습니다.",
|
|
@@ -1270,10 +1431,10 @@ const previewTunnelKo = {
|
|
|
1270
1431
|
};
|
|
1271
1432
|
const ko = {
|
|
1272
1433
|
// Common actions
|
|
1273
|
-
"common.save": "
|
|
1274
|
-
"common.reset": "
|
|
1275
|
-
"common.cancel": "
|
|
1276
|
-
"common.close": "
|
|
1434
|
+
"common.save": "저장",
|
|
1435
|
+
"common.reset": "초기화",
|
|
1436
|
+
"common.cancel": "취소",
|
|
1437
|
+
"common.close": "닫기",
|
|
1277
1438
|
"happyCliUpdate.title": "Happy CLI 업데이트가 있습니다",
|
|
1278
1439
|
"happyCliUpdate.checkTitle": "Happy CLI 업데이트 확인",
|
|
1279
1440
|
"happyCliUpdate.managerTitle": "Happy CLI 업데이트 관리",
|
|
@@ -1307,30 +1468,42 @@ const ko = {
|
|
|
1307
1468
|
"language.select.subtitle": "앱에서 사용할 언어를 선택하세요. 나중에 설정에서 바꿀 수 있습니다.",
|
|
1308
1469
|
// Login screen
|
|
1309
1470
|
"login.lead": "모바일 앱의 프로젝트, 대화, 문서 흐름을 데스크톱 작업 화면으로 옮긴 Electron 클라이언트입니다.",
|
|
1310
|
-
"login.
|
|
1311
|
-
"login.
|
|
1471
|
+
"login.stepsTitle": "시작 방법",
|
|
1472
|
+
"login.step1Title": "시작 방식 선택",
|
|
1473
|
+
"login.step1Body": "계정 없이 이 컴퓨터에서, 또는 Saycode 계정으로 시작해요.",
|
|
1474
|
+
"login.step2Title": "이 컴퓨터 자동 연결",
|
|
1475
|
+
"login.step2Body": "터미널 없이 이 컴퓨터가 자동으로 준비돼요. 로그인하면 개인 머신으로 등록돼요.",
|
|
1476
|
+
"login.step3Title": "바로 대화 시작",
|
|
1477
|
+
"login.step3Body": "프로젝트를 열고 첫 메시지를 보내면 끝이에요.",
|
|
1478
|
+
"login.localCardTitle": "이 컴퓨터에서 바로 시작",
|
|
1479
|
+
"login.localCardBody": "계정 없이 로컬 전용(Standalone) 모드로 시작해요.",
|
|
1480
|
+
"login.localCardAction": "계정 없이 시작하기",
|
|
1481
|
+
"login.localDataNotice": "데이터는 이 컴퓨터에만 저장되며 로그인 계정과는 별개예요.",
|
|
1482
|
+
"login.localUnavailable": "이 빌드에는 로컬 전용 런타임이 포함되어 있지 않아요.",
|
|
1483
|
+
"login.cloudCardTitle": "Saycode 계정으로 로그인",
|
|
1484
|
+
"login.cloudCardBody": "팀 공유, 원격 머신, 커뮤니티까지 모든 기능을 사용해요.",
|
|
1485
|
+
"login.localDataStaysNotice": "로컬 전용 모드의 데이터는 이 컴퓨터에 남아 있어요. 로그인 계정과는 별개예요.",
|
|
1312
1486
|
"login.email": "이메일",
|
|
1313
1487
|
"login.password": "비밀번호",
|
|
1314
1488
|
"login.passwordPlaceholder": "비밀번호",
|
|
1315
|
-
"login.loggingIn": "
|
|
1489
|
+
"login.loggingIn": "로그인 중...",
|
|
1316
1490
|
"login.login": "로그인",
|
|
1317
1491
|
"login.demoWorkspace": "데모 워크스페이스",
|
|
1318
1492
|
"login.standaloneStarting": "로컬 서버 시작 중...",
|
|
1319
|
-
"login.standalone": "로컬 전용 모드 (Standalone)",
|
|
1320
1493
|
"login.signupPrompt": "처음이신가요?",
|
|
1321
1494
|
"login.signupLink": "saycode.ai에서 계정 만들기",
|
|
1322
1495
|
"login.orgJoinHint": "회사 조직에 참여하려면 계정을 만든 뒤 saycode.ai에서 초대 코드를 입력하거나 관리자가 보낸 초대를 수락하세요. 그다음 같은 계정으로 여기에 로그인합니다.",
|
|
1323
1496
|
// Settings — Language tab
|
|
1324
|
-
"settings.tab.language": "
|
|
1497
|
+
"settings.tab.language": "언어",
|
|
1325
1498
|
"settings.language.help": "앱의 모든 화면에 사용할 언어를 선택합니다.",
|
|
1326
1499
|
"settings.language.current": "현재 적용: {language}",
|
|
1327
1500
|
"settings.language.updated": "언어가 변경되었습니다.",
|
|
1328
1501
|
"settings.language.loadFailed": "언어 설정을 불러오지 못했습니다.",
|
|
1329
1502
|
"settings.language.saveFailed": "언어 설정을 저장하지 못했습니다.",
|
|
1330
1503
|
// Sidebar / workspace chrome
|
|
1331
|
-
"account.unknownUser": "
|
|
1332
|
-
"account.demoWorkspace": "
|
|
1333
|
-
"account.connectedWorkspace": "
|
|
1504
|
+
"account.unknownUser": "알 수 없음",
|
|
1505
|
+
"account.demoWorkspace": "데모 워크스페이스",
|
|
1506
|
+
"account.connectedWorkspace": "연결된 워크스페이스",
|
|
1334
1507
|
"account.syncing": "캐시된 워크스페이스를 최신 정보로 동기화하는 중입니다.",
|
|
1335
1508
|
"sidebar.newProject": "새 프로젝트",
|
|
1336
1509
|
"sidebar.modeChats": "대화",
|
|
@@ -1372,6 +1545,32 @@ const ko = {
|
|
|
1372
1545
|
"pendingConversation.failed": "이 대화를 시작하지 못했습니다.",
|
|
1373
1546
|
"pendingConversation.retry": "다시 시도",
|
|
1374
1547
|
"newProject.title": "새 프로젝트",
|
|
1548
|
+
"machineDisplayName.dialogAria": "머신 이름 변경",
|
|
1549
|
+
"machineDisplayName.title": "머신 이름",
|
|
1550
|
+
"machineDisplayName.closeAria": "머신 이름 편집 닫기",
|
|
1551
|
+
"machineDisplayName.label": "표시명",
|
|
1552
|
+
"machineDisplayName.placeholder": "예: 작업 서버",
|
|
1553
|
+
"machineDisplayName.hint": "비워두고 저장하면 원래 host 이름으로 표시합니다.",
|
|
1554
|
+
"machineDisplayName.clear": "이름 지우기",
|
|
1555
|
+
"machineDisplayName.saving": "저장 중...",
|
|
1556
|
+
"newProject.eyebrow": "시작",
|
|
1557
|
+
"newProject.contextOrganization": "조직 컨텍스트",
|
|
1558
|
+
"newProject.contextPersonal": "개인 컨텍스트",
|
|
1559
|
+
"newProject.closeAria": "새 프로젝트 닫기",
|
|
1560
|
+
"newProject.sourceAria": "프로젝트 소스",
|
|
1561
|
+
"newProject.sourceBlank": "빈 프로젝트",
|
|
1562
|
+
"newProject.sourceFolder": "폴더",
|
|
1563
|
+
"newProject.nameLabel": "프로젝트 이름",
|
|
1564
|
+
"newProject.existingFolderLabel": "기존 폴더",
|
|
1565
|
+
"newProject.choose": "선택",
|
|
1566
|
+
"newProject.summaryBlank": "빈 동기화 프로젝트를 만듭니다.",
|
|
1567
|
+
"newProject.summaryGit": "프로젝트를 만들고 선택한 머신에서 Git 가져오기를 시작합니다.",
|
|
1568
|
+
"newProject.summaryZip": "프로젝트를 만들고 선택한 머신에서 선택한 ZIP/파일을 가져옵니다.",
|
|
1569
|
+
"newProject.summaryExistingFolder": "기존 절대 경로 폴더를 동기화 프로젝트로 등록합니다.",
|
|
1570
|
+
"newProject.creating": "만드는 중...",
|
|
1571
|
+
"newProject.createAction": "프로젝트 만들기",
|
|
1572
|
+
"newProject.importAction": "프로젝트 가져오기",
|
|
1573
|
+
"newProject.gitUrlLabel": "Git 저장소 주소",
|
|
1375
1574
|
"newProject.importTab": "Zip/파일 가져오기",
|
|
1376
1575
|
"newProject.importSection": "Zip/파일 가져오기",
|
|
1377
1576
|
"newProject.importPickAria": "Zip/파일 선택",
|
|
@@ -1403,7 +1602,7 @@ const ko = {
|
|
|
1403
1602
|
"upload.failed": "파일을 업로드하지 못했습니다.",
|
|
1404
1603
|
"upload.machineOffline": "선택한 머신이 오프라인입니다.",
|
|
1405
1604
|
"workspace.label": "작업공간",
|
|
1406
|
-
"workspace.contextAria": "
|
|
1605
|
+
"workspace.contextAria": "워크스페이스 컨텍스트",
|
|
1407
1606
|
"workspace.personal": "개인 작업공간",
|
|
1408
1607
|
"workspace.personalProjects": "개인 프로젝트",
|
|
1409
1608
|
"workspace.orgHome": "조직 홈",
|
|
@@ -1664,6 +1863,11 @@ const ko = {
|
|
|
1664
1863
|
"orgAdmin.vault.machineNotConfigured": "인증이 적용되지 않음",
|
|
1665
1864
|
"orgAdmin.vault.captureSucceeded": "인증 세트를 저장했습니다.",
|
|
1666
1865
|
"orgAdmin.vault.deploySucceeded": "머신 {count}대에 적용했습니다.",
|
|
1866
|
+
"myAiCredentials.title": "저장된 AI 인증 세트 적용",
|
|
1867
|
+
"myAiCredentials.description": "조직에 저장된 AI 인증 세트를 내 개인 머신이나 내가 등록한 공용 머신에 적용합니다. 인증 세트를 새로 저장하거나 삭제하려면 조직 관리자에게 문의하세요.",
|
|
1868
|
+
"myAiCredentials.noMachines": "적용할 수 있는 머신이 없습니다. 개인 머신을 등록하거나, 공용 머신을 직접 등록한 경우에만 여기에 나타납니다.",
|
|
1869
|
+
"myAiCredentials.targetMachines": "적용할 내 머신",
|
|
1870
|
+
"myAiCredentials.deploySucceeded": "{machines}에 인증 세트를 적용했습니다.",
|
|
1667
1871
|
"orgAdmin.vault.deleteSucceeded": "인증 세트를 삭제했습니다.",
|
|
1668
1872
|
"orgAdmin.vault.rotationStarted": "자동 회전을 시작했습니다.",
|
|
1669
1873
|
"orgAdmin.vault.rotationStopped": "자동 회전을 중지했습니다.",
|
|
@@ -1685,20 +1889,20 @@ const ko = {
|
|
|
1685
1889
|
"session.newConversation": "새 대화",
|
|
1686
1890
|
"session.lastConversation": "최근 대화 {time}",
|
|
1687
1891
|
// Settings dialog chrome
|
|
1688
|
-
"settings.eyebrow": "
|
|
1689
|
-
"settings.title": "
|
|
1690
|
-
"settings.close": "
|
|
1892
|
+
"settings.eyebrow": "환경설정",
|
|
1893
|
+
"settings.title": "데스크톱 설정",
|
|
1894
|
+
"settings.close": "설정 닫기",
|
|
1691
1895
|
// Settings — Notifications tab
|
|
1692
|
-
"settings.notifications.loading": "
|
|
1693
|
-
"settings.notifications.mobilePush": "
|
|
1694
|
-
"settings.notifications.updated": "
|
|
1896
|
+
"settings.notifications.loading": "알림 설정을 불러오는 중...",
|
|
1897
|
+
"settings.notifications.mobilePush": "모바일 완료 푸시",
|
|
1898
|
+
"settings.notifications.updated": "알림 설정을 저장했습니다.",
|
|
1695
1899
|
"settings.notifications.loadFailed": "알림 설정을 불러오지 못했습니다.",
|
|
1696
1900
|
"settings.notifications.saveFailed": "알림 설정을 저장하지 못했습니다.",
|
|
1697
1901
|
// Settings — Visual effects tab
|
|
1698
1902
|
"settings.visualEffects.loadFailed": "시각 효과 설정을 불러오지 못했습니다.",
|
|
1699
1903
|
"settings.visualEffects.saveFailed": "시각 효과 설정을 저장하지 못했습니다.",
|
|
1700
|
-
"settings.visualEffects.updated": "
|
|
1701
|
-
"settings.visualEffects.loading": "
|
|
1904
|
+
"settings.visualEffects.updated": "시각 효과를 업데이트했습니다.",
|
|
1905
|
+
"settings.visualEffects.loading": "시각 효과를 불러오는 중...",
|
|
1702
1906
|
"settings.visualEffects.help": "저사양 환경에서는 흐림, 큰 그림자와 장식 애니메이션을 줄여 스크롤과 창 이동을 부드럽게 합니다.",
|
|
1703
1907
|
"settings.visualEffects.auto": "자동",
|
|
1704
1908
|
"settings.visualEffects.full": "전체 효과",
|
|
@@ -1706,7 +1910,7 @@ const ko = {
|
|
|
1706
1910
|
"settings.visualEffects.currentAuto": "현재 적용: 자동 ({result})",
|
|
1707
1911
|
"settings.visualEffects.current": "현재 적용: {result}",
|
|
1708
1912
|
// Project list / sidebar sections
|
|
1709
|
-
"project.ownershipAria": "
|
|
1913
|
+
"project.ownershipAria": "프로젝트 소유",
|
|
1710
1914
|
"project.mine": "내 프로젝트",
|
|
1711
1915
|
"project.shared": "공유받은 프로젝트",
|
|
1712
1916
|
"project.recent": "최근 프로젝트",
|
|
@@ -1715,7 +1919,7 @@ const ko = {
|
|
|
1715
1919
|
"project.backToList": "프로젝트 목록으로",
|
|
1716
1920
|
"project.allProjects": "모든 프로젝트",
|
|
1717
1921
|
"project.navigationAria": "프로젝트 위치",
|
|
1718
|
-
"project.listAria": "
|
|
1922
|
+
"project.listAria": "프로젝트",
|
|
1719
1923
|
"project.emptyMineTitle": "내 프로젝트가 없습니다",
|
|
1720
1924
|
"project.emptySharedTitle": "공유받은 프로젝트가 없습니다",
|
|
1721
1925
|
"project.emptySearchHint": "검색 조건에 맞는 프로젝트가 없습니다.",
|
|
@@ -1723,7 +1927,7 @@ const ko = {
|
|
|
1723
1927
|
"sidebar.clickTarget": "프로젝트 클릭 시: {target} 에 열림",
|
|
1724
1928
|
"sidebar.newWindow": "새 창",
|
|
1725
1929
|
"sidebar.settings": "환경설정",
|
|
1726
|
-
"sidebar.logout": "
|
|
1930
|
+
"sidebar.logout": "로그아웃",
|
|
1727
1931
|
// Content pane / unified workspace
|
|
1728
1932
|
"runningIndicator.title": "실행 중인 작업 열기",
|
|
1729
1933
|
"runningIndicator.label": "실행 중 {count}",
|
|
@@ -1746,13 +1950,13 @@ const ko = {
|
|
|
1746
1950
|
"panel.recovery.demoDesc": "Demo workspace에서는 네트워크 연결을 열지 않습니다.",
|
|
1747
1951
|
"panel.recovery.loginDesc": "로그인 후 터미널을 시작할 수 있습니다.",
|
|
1748
1952
|
// Chat composer
|
|
1749
|
-
"composer.messageAria": "
|
|
1953
|
+
"composer.messageAria": "메시지",
|
|
1750
1954
|
"composer.placeholder": "데스크톱에서 보낼 메시지를 입력",
|
|
1751
1955
|
"composer.promptSuggestionPaused": "{window} 사용량 {percent}% — 사용량 경고 중에는 Claude가 추천 질문을 만들지 않습니다",
|
|
1752
1956
|
"composer.promptSuggestionPausedReset": "{reset} 초기화",
|
|
1753
|
-
"composer.starting": "
|
|
1754
|
-
"composer.sending": "
|
|
1755
|
-
"composer.working": "
|
|
1957
|
+
"composer.starting": "시작 중...",
|
|
1958
|
+
"composer.sending": "전송 중...",
|
|
1959
|
+
"composer.working": "작업 중...",
|
|
1756
1960
|
"composer.queue.limitReached": "대기열이 가득 찼습니다(최대 3개)",
|
|
1757
1961
|
"composer.queue.title": "대기 중인 요청",
|
|
1758
1962
|
"composer.queue.edit": "편집",
|
|
@@ -1760,11 +1964,11 @@ const ko = {
|
|
|
1760
1964
|
"composer.queue.steerNow": "즉시 요청",
|
|
1761
1965
|
"composer.queue.add": "대기열에 추가",
|
|
1762
1966
|
"composer.queue.editBlocked": "입력창을 비운 뒤 편집할 수 있습니다",
|
|
1763
|
-
"composer.send": "
|
|
1967
|
+
"composer.send": "보내기",
|
|
1764
1968
|
"composer.stopping": "중단 중...",
|
|
1765
1969
|
"composer.stop": "중단",
|
|
1766
1970
|
// Common (shared)
|
|
1767
|
-
"common.refresh": "
|
|
1971
|
+
"common.refresh": "새로고침",
|
|
1768
1972
|
// Project worktree dialog
|
|
1769
1973
|
"worktreeDialog.listFailed": "worktree 목록을 불러오지 못했습니다.",
|
|
1770
1974
|
"worktreeDialog.startFailed": "새 대화를 시작하지 못했습니다.",
|
|
@@ -1783,9 +1987,9 @@ const ko = {
|
|
|
1783
1987
|
"worktreeDialog.delete": "삭제",
|
|
1784
1988
|
"worktreeDialog.forceDelete": "강제 삭제",
|
|
1785
1989
|
"worktreeDialog.cancel": "취소",
|
|
1786
|
-
"worktreeDialog.dialogAria": "{name}
|
|
1787
|
-
"worktreeDialog.closeAria": "
|
|
1788
|
-
"worktreeDialog.listAria": "{name}
|
|
1990
|
+
"worktreeDialog.dialogAria": "{name} 워크트리",
|
|
1991
|
+
"worktreeDialog.closeAria": "워크트리 다이얼로그 닫기",
|
|
1992
|
+
"worktreeDialog.listAria": "{name} 워크트리 목록",
|
|
1789
1993
|
"worktreeDialog.loading": "Worktree 목록을 불러오는 중입니다.",
|
|
1790
1994
|
"worktreeDialog.empty": "이 프로젝트에는 관리 중인 worktree가 없습니다.",
|
|
1791
1995
|
"worktreeDialog.rowAria": "worktree {name}",
|
|
@@ -1793,6 +1997,9 @@ const ko = {
|
|
|
1793
1997
|
"worktreeDialog.startSessionTitle": "이 worktree에서 새 대화를 시작합니다 (새 worktree를 만들지 않습니다)",
|
|
1794
1998
|
"worktreeDialog.startSession": "이 worktree에서 새 대화",
|
|
1795
1999
|
"worktreeDialog.removeAria": "{name} 삭제",
|
|
2000
|
+
"worktreePathCopy.copy": "worktree 경로 복사",
|
|
2001
|
+
"worktreePathCopy.copied": "worktree 경로 복사됨",
|
|
2002
|
+
"worktreePathCopy.failed": "worktree 경로 복사 실패",
|
|
1796
2003
|
// Project git controls
|
|
1797
2004
|
"gitControls.branchLoading": "Git branch 확인 중",
|
|
1798
2005
|
"gitControls.noRemote": "origin remote 또는 branch를 찾을 수 없습니다.",
|
|
@@ -1807,6 +2014,7 @@ const ko = {
|
|
|
1807
2014
|
"gitControls.pushTitle": "{branch} push",
|
|
1808
2015
|
"gitControls.manageWorktreeAria": "{name} worktree 관리",
|
|
1809
2016
|
"gitControls.manageWorktreeTitle": "Worktree 관리",
|
|
2017
|
+
"gitControls.operationResultDialogTitle": "{label} 결과",
|
|
1810
2018
|
// Fan-out group section
|
|
1811
2019
|
"fanoutSection.groupsAria": "병렬 실행 그룹",
|
|
1812
2020
|
"fanoutSection.openGroupAria": "병렬 그룹 열기: {title} · 실행 {run}",
|
|
@@ -2340,6 +2548,11 @@ const ko = {
|
|
|
2340
2548
|
"git.branchNameRequired": "생성할 Git branch 이름을 입력해주세요.",
|
|
2341
2549
|
"git.noBranchCreateResponse": "머신이 Git branch 생성 응답을 반환하지 않았습니다.",
|
|
2342
2550
|
"git.branchCreateFailed": "Git branch 생성에 실패했습니다.",
|
|
2551
|
+
"git.branchNameUnchanged": "바꿀 Git branch 이름이 지금과 같습니다.",
|
|
2552
|
+
"git.noBranchRenameResponse": "머신이 Git branch 이름 변경 응답을 반환하지 않았습니다.",
|
|
2553
|
+
"git.branchRenameFailed": "Git branch 이름 변경에 실패했습니다.",
|
|
2554
|
+
"git.noBranchDeleteResponse": "머신이 Git branch 삭제 응답을 반환하지 않았습니다.",
|
|
2555
|
+
"git.branchDeleteFailed": "Git branch 삭제에 실패했습니다.",
|
|
2343
2556
|
"git.repoRootFailed": "Git 저장소 루트를 확인하지 못했습니다.",
|
|
2344
2557
|
"git.worktreeCreateFailed": "Git worktree 생성에 실패했습니다.",
|
|
2345
2558
|
"git.diffPathNotAllowed": "worktree diff 는 {segment} 하위 경로만 허용됩니다: {path}",
|
|
@@ -2507,6 +2720,23 @@ const ko = {
|
|
|
2507
2720
|
"standalone.startFailed": "로컬 standalone 서버를 시작하지 못했습니다.",
|
|
2508
2721
|
"workspace.loadFailed": "워크스페이스를 불러오지 못했습니다.",
|
|
2509
2722
|
"standalone.modeStartFailed": "로컬 standalone 모드를 시작하지 못했습니다.",
|
|
2723
|
+
"modeSwitch.toLocal": "로컬 전용 모드로 전환",
|
|
2724
|
+
"modeSwitch.toCloud": "계정으로 돌아가기",
|
|
2725
|
+
"standaloneImport.title": "로컬 전용 데이터 가져오기",
|
|
2726
|
+
"standaloneImport.introBody": "이 컴퓨터의 로컬 전용(Standalone) 모드 데이터(프로젝트·대화 기록)를 지금 로그인한 계정으로 복사해요. 원본은 그대로 남아요.",
|
|
2727
|
+
"standaloneImport.listOrderNotice": "가져온 대화 내용의 시각은 유지되지만, 세션 목록에서는 가져온 시점에 모여 보여요.",
|
|
2728
|
+
"standaloneImport.start": "가져오기 시작",
|
|
2729
|
+
"standaloneImport.later": "나중에",
|
|
2730
|
+
"standaloneImport.close": "닫기",
|
|
2731
|
+
"standaloneImport.running": "가져오는 중... ({completed}/{total} 세션)",
|
|
2732
|
+
"standaloneImport.machineNotReady": "이 컴퓨터의 개인 머신 등록이 끝난 뒤 다시 시도해주세요.",
|
|
2733
|
+
"standaloneImport.doneSummary": "프로젝트 {projects}개, 세션 {sessions}개를 가져왔어요.",
|
|
2734
|
+
"standaloneImport.doneSkipped": "이미 가져온 세션 {count}개는 건너뛰었어요.",
|
|
2735
|
+
"standaloneImport.undecryptable": "복호화할 수 없어 건너뛴 메시지가 {count}개 있어요.",
|
|
2736
|
+
"standaloneImport.failedSessions": "세션 {count}개를 가져오지 못했어요. 다시 실행하면 이어서 시도해요.",
|
|
2737
|
+
"standaloneImport.fatalError": "가져오기를 시작하지 못했어요: {error}",
|
|
2738
|
+
"standaloneImport.settingsBody": "이 컴퓨터의 로컬 전용 모드 데이터를 현재 계정으로 복사해요.",
|
|
2739
|
+
"standaloneImport.settingsOpen": "열기",
|
|
2510
2740
|
// Workspace / project / file flow toasts + errors
|
|
2511
2741
|
"workspace.contextSwitchFailed": "워크스페이스 컨텍스트를 전환하지 못했습니다.",
|
|
2512
2742
|
"error.loginRequired": "로그인이 필요합니다.",
|
|
@@ -2659,6 +2889,8 @@ const ko = {
|
|
|
2659
2889
|
"gitOp.failed": "{label} 실패",
|
|
2660
2890
|
"gitOp.branchChanged": "Branch 변경 완료",
|
|
2661
2891
|
"gitOp.branchCreated": "Branch 생성 완료",
|
|
2892
|
+
"gitOp.branchRenamed": "Branch 이름 변경 완료",
|
|
2893
|
+
"gitOp.branchDeleted": "Branch 삭제 완료",
|
|
2662
2894
|
"worktreeOp.noGithub": "GitHub 저장소가 연결돼 있지 않습니다.",
|
|
2663
2895
|
"worktreeOp.pushingBranch": "브랜치 푸시 중...",
|
|
2664
2896
|
"worktreeOp.creatingPr": "Pull request 생성 중...",
|
|
@@ -2788,6 +3020,20 @@ const ko = {
|
|
|
2788
3020
|
"preview.tokenUrlInvalid": "Preview 토큰 URL이 올바르지 않습니다.",
|
|
2789
3021
|
"branchDialog.loading": "Branch 목록을 불러오는 중입니다.",
|
|
2790
3022
|
"branchDialog.empty": "표시할 branch가 없습니다.",
|
|
3023
|
+
"branchDialog.actionsAria": "{name} 브랜치 작업",
|
|
3024
|
+
"branchDialog.rowAria": "{name} {meta}",
|
|
3025
|
+
"branchDialog.switch": "브랜치 이동",
|
|
3026
|
+
"branchDialog.rename": "브랜치 이름 변경",
|
|
3027
|
+
"branchDialog.delete": "브랜치 삭제",
|
|
3028
|
+
"branchDialog.switchConfirm": "'{name}' 브랜치로 이동할까요? 커밋되지 않은 변경이 있으면 Git이 거부할 수 있습니다.",
|
|
3029
|
+
"branchDialog.switchConfirmAction": "이동",
|
|
3030
|
+
"branchDialog.renameConfirm": "'{from}' 브랜치의 이름을 바꿀까요?",
|
|
3031
|
+
"branchDialog.renamePlaceholder": "새 브랜치 이름",
|
|
3032
|
+
"branchDialog.renameInputAria": "{from} 새 이름",
|
|
3033
|
+
"branchDialog.renameConfirmAction": "이름 변경",
|
|
3034
|
+
"branchDialog.deleteConfirm": "'{name}' 브랜치를 삭제할까요? 머지되지 않은 커밋이 있으면 Git이 거부합니다.",
|
|
3035
|
+
"branchDialog.deleteConfirmAction": "삭제",
|
|
3036
|
+
"branchDialog.cancel": "취소",
|
|
2791
3037
|
"message.deleteConfirm": "이 메시지를 삭제할까요? 이 작업은 새로고침 후에도 유지됩니다.",
|
|
2792
3038
|
"message.deleteFailed": "메시지를 삭제할 수 없습니다.",
|
|
2793
3039
|
"message.sendFailed": "메시지를 보낼 수 없습니다.",
|
|
@@ -2805,7 +3051,7 @@ const ko = {
|
|
|
2805
3051
|
"projectDelete.cancel": "취소",
|
|
2806
3052
|
"projectDelete.confirm": "삭제",
|
|
2807
3053
|
"projectDelete.deleting": "삭제 중…",
|
|
2808
|
-
"projectCopy.eyebrow": "
|
|
3054
|
+
"projectCopy.eyebrow": "조직 프로젝트",
|
|
2809
3055
|
"projectCopy.title": "프로젝트 복사",
|
|
2810
3056
|
"projectCopy.description": "'{name}' 프로젝트를 내 프로젝트로 복사합니다.",
|
|
2811
3057
|
"projectCopy.close": "프로젝트 복사 닫기",
|
|
@@ -2949,6 +3195,11 @@ const ko = {
|
|
|
2949
3195
|
"askUser.customPlaceholder": "답을 입력하세요",
|
|
2950
3196
|
"askUser.submitting": "제출 중...",
|
|
2951
3197
|
"askUser.submit": "제출",
|
|
3198
|
+
"conversationFilter.hideAutomated": "자동 실행 대화 숨기기",
|
|
3199
|
+
"conversationFilter.showAutomated": "자동 실행 대화 보기",
|
|
3200
|
+
"conversationFilter.hideAutomatedHint": "자동 리뷰·예약 자동화·GitHub 트리거가 실행한 대화를 목록에서 뺍니다.",
|
|
3201
|
+
"conversationFilter.hiddenCount": "{count}개 숨김",
|
|
3202
|
+
"conversationFilter.allHidden": "자동 실행 대화 {count}개만 있습니다. 보려면 필터를 끄세요.",
|
|
2952
3203
|
"sessionsTab.hideArchived": "완료된 대화 숨기기",
|
|
2953
3204
|
"sessionsTab.showArchived": "완료된 대화 보기",
|
|
2954
3205
|
"sessionsTab.emptyBody": "모바일 앱처럼 프로젝트에 연결된 대화가 여기에 표시됩니다.",
|
|
@@ -3013,6 +3264,27 @@ const ko = {
|
|
|
3013
3264
|
"cloudMobileLogin.refresh": "새 QR 만들기",
|
|
3014
3265
|
"systemPrompt.loadFailed": "설정을 불러오지 못했습니다.",
|
|
3015
3266
|
"systemPrompt.saveFailed": "시스템 프롬프트를 저장하지 못했습니다.",
|
|
3267
|
+
"saycodePromptPreference.dialogAria": "Saycode 지시 사용 설정",
|
|
3268
|
+
"saycodePromptPreference.eyebrow": "에이전트 지시 선택",
|
|
3269
|
+
"saycodePromptPreference.title": "Saycode 기본 지시를 사용할까요?",
|
|
3270
|
+
"saycodePromptPreference.body": "내가 만든 Claude/Codex 스킬과 하네스를 그대로 쓰려면 꺼진 상태를 유지하세요. 나중에 환경설정에서 언제든 바꿀 수 있습니다.",
|
|
3271
|
+
"saycodePromptPreference.keepOff": "꺼진 상태 유지",
|
|
3272
|
+
"saycodePromptPreference.enable": "Saycode 지시 켜기",
|
|
3273
|
+
"saycodePromptPreference.saving": "저장 중…",
|
|
3274
|
+
"saycodePromptPreference.retryHint": "선택을 다시 눌러 저장을 재시도하세요.",
|
|
3275
|
+
"saycodePromptPreference.settingsTitle": "Saycode 기본 지시",
|
|
3276
|
+
"saycodePromptPreference.settingsEnable": "Saycode가 제공하는 기본 행동 지시 사용",
|
|
3277
|
+
"saycodePromptPreference.settingsHelp": "사용자 시스템 프롬프트, 프로젝트 지침, 설치한 스킬과 하네스는 이 설정과 관계없이 유지됩니다.",
|
|
3278
|
+
"saycodePromptPreference.blocksTitle": "개별 지시 선택",
|
|
3279
|
+
"saycodePromptPreference.blocksHelp": "Saycode 지시를 항목별로 켜고 끕니다. 선택하지 않은 항목은 위의 전체 설정을 따릅니다.",
|
|
3280
|
+
"saycodePromptPreference.alwaysOnNotice": "채팅 제목 생성은 항상 켜져 있으며 끌 수 없습니다. 채팅 목록에서 대화를 다시 찾으려면 필요합니다.",
|
|
3281
|
+
"saycodePromptPreference.block.workerDelegation.label": "작업 위임",
|
|
3282
|
+
"saycodePromptPreference.block.workerDelegation.description": "기계적인 작업을 저렴한 worker 에이전트에 넘기도록 지시합니다.",
|
|
3283
|
+
"saycodePromptPreference.block.axBase.label": "기획부터 시작 워크플로우",
|
|
3284
|
+
"saycodePromptPreference.block.axBase.description": "AX 워크플로우의 기본 지시를 사용합니다.",
|
|
3285
|
+
"saycodePromptPreference.block.coAuthoredCredit.label": "커밋 크레딧",
|
|
3286
|
+
"saycodePromptPreference.block.coAuthoredCredit.description": "커밋 메시지에 Claude와 Happy 공동 작성자 표시를 추가합니다.",
|
|
3287
|
+
"saycodePromptPreference.loading": "계정 설정을 불러오는 중…",
|
|
3016
3288
|
"standalone.zrokStatus": "zrok 상태",
|
|
3017
3289
|
"standalone.zrokNeeded": "zrok2 설정이 필요합니다",
|
|
3018
3290
|
"standalone.zrokNeededBody": "다른 네트워크의 머신을 등록하려면 접속 가능한 서버 URL이 필요합니다.",
|
|
@@ -3240,6 +3512,8 @@ const ko = {
|
|
|
3240
3512
|
"sidebarFiles.invalidName": '이름에 "/", "\\"를 쓰거나 "." 또는 ".."로 지을 수 없습니다.',
|
|
3241
3513
|
"sidebarFiles.daemonTooOld": "이 머신의 Happy CLI가 오래되어 이름변경·복사본·삭제를 할 수 없습니다. 1.1.10-aplus.74 이상으로 업데이트하세요.",
|
|
3242
3514
|
// ProjectDocumentViewer
|
|
3515
|
+
"inlinePreview.openInViewer": "파일 뷰어로 열기",
|
|
3516
|
+
"inlinePreview.loading": "미리보기를 불러오는 중",
|
|
3243
3517
|
"documentViewer.docxConvertFailed": "DOCX 문서를 변환하지 못했습니다.",
|
|
3244
3518
|
"documentViewer.pdfViewer": "{name} PDF 뷰어",
|
|
3245
3519
|
"documentViewer.pdfPreparing": "PDF를 준비하는 중...",
|
|
@@ -3305,6 +3579,14 @@ const ko = {
|
|
|
3305
3579
|
"fileDownload.tooLarge": "다운로드 파일은 100 MiB 이하만 지원합니다.",
|
|
3306
3580
|
"codex.needsLatestCli": "GPT-5.6을 사용하려면 최신 Codex CLI가 필요합니다. `npm install -g @openai/codex@latest`로 업데이트한 뒤 새 대화를 시작해 주세요.",
|
|
3307
3581
|
"codex.error": "Codex 오류: {detail}",
|
|
3582
|
+
// specs/desktop-error-surface — 실패 알림의 계층 배지와 복구 액션
|
|
3583
|
+
"errorSurface.layer.provider": "Provider 오류",
|
|
3584
|
+
"errorSurface.layer.runtime": "런타임 오류",
|
|
3585
|
+
"errorSurface.layer.auth": "인증 오류",
|
|
3586
|
+
"errorSurface.retry": "다시 시도",
|
|
3587
|
+
"errorSurface.retrying": "보내는 중…",
|
|
3588
|
+
"errorSurface.retryFailed": "다시 보내지 못했습니다.",
|
|
3589
|
+
"errorSurface.openCredentials": "내 AI 자격증명 열기",
|
|
3308
3590
|
"fanout.slotCountRange": "병렬 실행 후보 수는 2~4개여야 합니다 (요청: {count}개).",
|
|
3309
3591
|
"deployPlan.contractNotFound": "배포 실행 계약을 찾지 못했습니다",
|
|
3310
3592
|
// Voice input
|
|
@@ -3366,6 +3648,8 @@ const ko = {
|
|
|
3366
3648
|
"projectGitView.noRemote": "remote 없음",
|
|
3367
3649
|
"projectGitView.branchSwitch": "Branch 변경",
|
|
3368
3650
|
"projectGitView.branchCreate": "Branch 생성",
|
|
3651
|
+
"projectGitView.branchRename": "Branch 이름 변경",
|
|
3652
|
+
"projectGitView.branchDelete": "Branch 삭제",
|
|
3369
3653
|
// Remote terminal + API client (non-hook)
|
|
3370
3654
|
"terminal.connectTimeout": "터미널 연결 시간이 초과되었습니다.",
|
|
3371
3655
|
"terminal.connectFailed": "터미널 연결에 실패했습니다.",
|
|
@@ -3376,7 +3660,7 @@ const ko = {
|
|
|
3376
3660
|
// Merge with main: git-controls dropdown + worktree section
|
|
3377
3661
|
"gitControls.branchMenu": "Branch 메뉴",
|
|
3378
3662
|
"gitControls.branchMenuWithDesc": "Branch 메뉴 · {description}",
|
|
3379
|
-
"gitControls.gitActionsAria": "{branch} git
|
|
3663
|
+
"gitControls.gitActionsAria": "{branch} git 작업",
|
|
3380
3664
|
"gitControls.pullLabel": "가져오기 (pull)",
|
|
3381
3665
|
"gitControls.pushLabel": "보내기 (push)",
|
|
3382
3666
|
"gitControls.switchAria": "{branch} branch 전환",
|
|
@@ -3424,6 +3708,7 @@ const ko = {
|
|
|
3424
3708
|
"settingsTab.security": "보안",
|
|
3425
3709
|
"settingsTab.memory": "Memory",
|
|
3426
3710
|
"settingsTab.connectors": "커넥터",
|
|
3711
|
+
"settingsTab.myAiCredentials": "내 AI 계정",
|
|
3427
3712
|
"settingsTab.extensions": "확장",
|
|
3428
3713
|
"settingsTab.about": "정보",
|
|
3429
3714
|
"settingsTab.shortcuts": "단축키",
|
|
@@ -3444,6 +3729,13 @@ const ko = {
|
|
|
3444
3729
|
"extensions.confirmEnableAction": "권한 승인 및 활성화",
|
|
3445
3730
|
"extensions.confirmUninstall": "이 확장과 설치된 파일을 제거할까요?",
|
|
3446
3731
|
"extensions.shellUnavailable": "확장 관리는 최신 Desktop 앱에서 사용할 수 있습니다.",
|
|
3732
|
+
"extensions.developerMode": "개발자 모드",
|
|
3733
|
+
"extensions.developerModeHint": "압축을 풀어둔 확장 폴더를 불러옵니다. 무결성 검사만 면제되고 권한·격리는 그대로 적용됩니다.",
|
|
3734
|
+
"extensions.loadUnpacked": "폴더 불러오기",
|
|
3735
|
+
"extensions.unloadDevelopment": "내리기",
|
|
3736
|
+
"extensions.developmentSource": "개발 빌드: {directory}",
|
|
3737
|
+
"extensions.apiDeprecated": "업데이트 필요",
|
|
3738
|
+
"extensions.apiDeprecatedHint": "이전 확장 API({apiVersion})로 동작 중이며, 다음 업데이트에서 지원이 끝납니다.",
|
|
3447
3739
|
"extensions.runtime.inactive": "비활성",
|
|
3448
3740
|
"extensions.runtime.active": "실행 중",
|
|
3449
3741
|
"extensions.runtime.quarantined": "격리됨",
|
|
@@ -3537,6 +3829,7 @@ const ko = {
|
|
|
3537
3829
|
"review.self.disabledWaiting": "먼저 대기 중인 질문이나 권한 요청에 응답해주세요.",
|
|
3538
3830
|
"review.self.rounds": "반복 횟수",
|
|
3539
3831
|
"review.self.roundsHint": "medium 이상 지적이 나오면 남은 횟수만큼 리뷰를 자동으로 이어갑니다. low·nit만 남으면 바로 끝냅니다.",
|
|
3832
|
+
"review.self.appOpenHint": "앱이 실행 중일 때만 이어집니다. 앱을 완전히 종료하면 멈추고, 다시 열면 남은 횟수부터 이어갑니다.",
|
|
3540
3833
|
"review.self.loopActive": "반복 리뷰 {round}/{total} 진행 중",
|
|
3541
3834
|
"review.self.loopStop": "반복 중지",
|
|
3542
3835
|
"review.self.loopDismiss": "기록 지우기",
|
|
@@ -3648,7 +3941,7 @@ function isAskUserQuestionToolName(value2) {
|
|
|
3648
3941
|
const normalized = value2.replace(/^functions\./i, "").replace(/^mcp__.+?__/i, "").replace(/\s+call$/i, "").replace(/[._\-\s]+/g, "").trim().toLowerCase();
|
|
3649
3942
|
return normalized === "askuserquestion" || normalized === "requestuserinput" || normalized === "사용자에게질문";
|
|
3650
3943
|
}
|
|
3651
|
-
function isRecord$
|
|
3944
|
+
function isRecord$5(value2) {
|
|
3652
3945
|
return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
|
|
3653
3946
|
}
|
|
3654
3947
|
function firstString(...values) {
|
|
@@ -3658,11 +3951,11 @@ function firstString(...values) {
|
|
|
3658
3951
|
return void 0;
|
|
3659
3952
|
}
|
|
3660
3953
|
function recordValue(value2) {
|
|
3661
|
-
if (isRecord$
|
|
3954
|
+
if (isRecord$5(value2)) return value2;
|
|
3662
3955
|
if (typeof value2 !== "string" || !value2.trim()) return void 0;
|
|
3663
3956
|
try {
|
|
3664
3957
|
const parsed = JSON.parse(value2);
|
|
3665
|
-
return isRecord$
|
|
3958
|
+
return isRecord$5(parsed) ? parsed : void 0;
|
|
3666
3959
|
} catch {
|
|
3667
3960
|
return void 0;
|
|
3668
3961
|
}
|
|
@@ -3681,9 +3974,9 @@ function timestampValue(value2) {
|
|
|
3681
3974
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
3682
3975
|
}
|
|
3683
3976
|
function normalizePendingPermissionEnvelope(requestId, value2) {
|
|
3684
|
-
if (!isRecord$
|
|
3685
|
-
const nestedTool = isRecord$
|
|
3686
|
-
const nestedFunction = nestedTool && isRecord$
|
|
3977
|
+
if (!isRecord$5(value2)) return null;
|
|
3978
|
+
const nestedTool = isRecord$5(value2.tool) ? value2.tool : null;
|
|
3979
|
+
const nestedFunction = nestedTool && isRecord$5(nestedTool.function) ? nestedTool.function : null;
|
|
3687
3980
|
const toolName = firstString(
|
|
3688
3981
|
value2.name,
|
|
3689
3982
|
value2.toolName,
|
|
@@ -3733,7 +4026,7 @@ function permissionKind(toolName) {
|
|
|
3733
4026
|
return "generic";
|
|
3734
4027
|
}
|
|
3735
4028
|
function pendingPermissionRequestsFromState(state) {
|
|
3736
|
-
if (!isRecord$
|
|
4029
|
+
if (!isRecord$5(state) || !isRecord$5(state.requests)) return [];
|
|
3737
4030
|
return Object.entries(state.requests).map(([requestId, value2]) => {
|
|
3738
4031
|
const envelope = normalizePendingPermissionEnvelope(requestId, value2);
|
|
3739
4032
|
const toolName = envelope?.toolName ?? "unknown";
|
|
@@ -3748,7 +4041,7 @@ function pendingPermissionRequestsFromState(state) {
|
|
|
3748
4041
|
});
|
|
3749
4042
|
}
|
|
3750
4043
|
function agentControlledByUserFromState(state) {
|
|
3751
|
-
return isRecord$
|
|
4044
|
+
return isRecord$5(state) && state.controlledByUser === true;
|
|
3752
4045
|
}
|
|
3753
4046
|
function createChatState() {
|
|
3754
4047
|
return { phase: "idle", pendingPermission: null };
|
|
@@ -4099,7 +4392,6 @@ const HELP_TEXT = `saycode — drive Saycode sessions across your machines from
|
|
|
4099
4392
|
|
|
4100
4393
|
Usage
|
|
4101
4394
|
saycode <command> [options]
|
|
4102
|
-
|
|
4103
4395
|
Commands
|
|
4104
4396
|
(no arguments) Interactive browser: workspace -> machine ->
|
|
4105
4397
|
project -> conversation, then open it
|
|
@@ -4117,7 +4409,6 @@ Commands
|
|
|
4117
4409
|
login / logout Manage stored credentials
|
|
4118
4410
|
agent <verb> For AI agents inside a Saycode session, not
|
|
4119
4411
|
for people — see \`saycode agent whoami\`
|
|
4120
|
-
|
|
4121
4412
|
Options
|
|
4122
4413
|
-w, --workspace <id|name> Workspace to act in (see \`saycode workspaces\`)
|
|
4123
4414
|
-m, --machine <id|host> Machine id prefix or hostname substring
|
|
@@ -4145,7 +4436,10 @@ Agent commands (machine-readable JSON on stdout, exit 3 on failure)
|
|
|
4145
4436
|
agent read <session> [--last n] Recent messages + completion signal
|
|
4146
4437
|
agent wait <session> --until <state> turn-end | waiting-input | completed
|
|
4147
4438
|
agent prompt <session> <text> [--wait] Send a prompt, optionally wait for it
|
|
4439
|
+
agent steer <session> <text> Redirect an owned child's active response
|
|
4440
|
+
agent stop <session> Stop an owned child's active response
|
|
4148
4441
|
agent spawn --prompt <text> [--agent n] Start a sibling in the same directory
|
|
4442
|
+
agent spawn --prompt <text> --file <path> ...with context files it reads first
|
|
4149
4443
|
Only works inside a session Saycode started (SAYCODE_AGENT_ENV).
|
|
4150
4444
|
|
|
4151
4445
|
Workspaces
|
|
@@ -6528,6 +6822,21 @@ function decryptJsonWithCandidates(blob, keys) {
|
|
|
6528
6822
|
decryptCacheSet(blob, keys, null);
|
|
6529
6823
|
return null;
|
|
6530
6824
|
}
|
|
6825
|
+
function buildUserTextMessageContent(input) {
|
|
6826
|
+
return {
|
|
6827
|
+
role: "user",
|
|
6828
|
+
content: { type: "text", text: input.text },
|
|
6829
|
+
meta: {
|
|
6830
|
+
permissionMode: "bypassPermissions",
|
|
6831
|
+
saycodeSystemPromptEnabled: input.saycodeSystemPromptEnabled ?? true,
|
|
6832
|
+
...input.saycodePromptBlocks && Object.keys(input.saycodePromptBlocks).length > 0 ? { saycodePromptBlocks: input.saycodePromptBlocks } : {},
|
|
6833
|
+
...input.model && input.model !== "default" ? { model: input.model } : {},
|
|
6834
|
+
...input.effort ? { effort: input.effort } : {},
|
|
6835
|
+
...input.autoRoute ? { autoRoute: input.autoRoute } : {},
|
|
6836
|
+
...input.appendSystemPrompt !== void 0 ? { appendSystemPrompt: input.appendSystemPrompt } : {}
|
|
6837
|
+
}
|
|
6838
|
+
};
|
|
6839
|
+
}
|
|
6531
6840
|
const CONNECTOR_PROVIDER_IDS = [
|
|
6532
6841
|
"notion",
|
|
6533
6842
|
"slack",
|
|
@@ -6576,6 +6885,9 @@ function aiCredentialsPath(companyId, bundleId, action) {
|
|
|
6576
6885
|
function personalMachinesPath(companyId) {
|
|
6577
6886
|
return `/api/admin/companies/${encodeURIComponent(companyId)}/personal-machines`;
|
|
6578
6887
|
}
|
|
6888
|
+
function myMachinesPath(companyId) {
|
|
6889
|
+
return `/api/admin/companies/${encodeURIComponent(companyId)}/my-machines`;
|
|
6890
|
+
}
|
|
6579
6891
|
function record$1(value2) {
|
|
6580
6892
|
return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : {};
|
|
6581
6893
|
}
|
|
@@ -6692,6 +7004,20 @@ function parseAiCredentialBatchResult(value2) {
|
|
|
6692
7004
|
warnings: array(body.warnings).filter((warning) => typeof warning === "string")
|
|
6693
7005
|
};
|
|
6694
7006
|
}
|
|
7007
|
+
function parseSelfServiceMachines(value2) {
|
|
7008
|
+
return array(record$1(value2).machines).map((entry) => {
|
|
7009
|
+
const row2 = record$1(entry);
|
|
7010
|
+
return {
|
|
7011
|
+
machineId: str(row2.machineId),
|
|
7012
|
+
type: row2.type === "shared" ? "shared" : "personal",
|
|
7013
|
+
displayName: typeof row2.displayName === "string" ? row2.displayName : null,
|
|
7014
|
+
host: typeof row2.host === "string" ? row2.host : null,
|
|
7015
|
+
workspaceRoot: typeof row2.workspaceRoot === "string" ? row2.workspaceRoot : null,
|
|
7016
|
+
registeredAt: num(row2.registeredAt),
|
|
7017
|
+
online: typeof row2.online === "boolean" ? row2.online : null
|
|
7018
|
+
};
|
|
7019
|
+
}).filter((machine) => machine.machineId !== "");
|
|
7020
|
+
}
|
|
6695
7021
|
function parsePersonalMachines(value2) {
|
|
6696
7022
|
return array(record$1(value2).machines).map((entry) => {
|
|
6697
7023
|
const row2 = record$1(entry);
|
|
@@ -6764,7 +7090,7 @@ function buildOpenConversationsPath(input) {
|
|
|
6764
7090
|
if (input.cursor) params.set("cursor", input.cursor);
|
|
6765
7091
|
return `/api/me/open-conversations?${params.toString()}`;
|
|
6766
7092
|
}
|
|
6767
|
-
function isRecord$
|
|
7093
|
+
function isRecord$4(value2) {
|
|
6768
7094
|
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
6769
7095
|
}
|
|
6770
7096
|
function normalizedOptionalText(value2, maxLength) {
|
|
@@ -6772,7 +7098,7 @@ function normalizedOptionalText(value2, maxLength) {
|
|
|
6772
7098
|
return value2.trim().slice(0, maxLength) || null;
|
|
6773
7099
|
}
|
|
6774
7100
|
function parseCard(value2) {
|
|
6775
|
-
if (!isRecord$
|
|
7101
|
+
if (!isRecord$4(value2)) return null;
|
|
6776
7102
|
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
7103
|
const sessionId = value2.sessionId.trim();
|
|
6778
7104
|
const projectId = value2.projectId.trim();
|
|
@@ -6801,7 +7127,7 @@ function parseRows(value2, limit) {
|
|
|
6801
7127
|
return rows;
|
|
6802
7128
|
}
|
|
6803
7129
|
function parseOpenConversationsPayload(value2) {
|
|
6804
|
-
if (!isRecord$
|
|
7130
|
+
if (!isRecord$4(value2)) throw new Error("Invalid open conversations response");
|
|
6805
7131
|
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
7132
|
const rawActive = value2.activeCandidates;
|
|
6807
7133
|
const rawRecent = value2.recent;
|
|
@@ -6876,7 +7202,7 @@ function buildProjectConversationsPath(projectId) {
|
|
|
6876
7202
|
if (!trimmed) throw new Error("Invalid project id");
|
|
6877
7203
|
return `/api/v2/projects/${encodeURIComponent(trimmed)}/conversations`;
|
|
6878
7204
|
}
|
|
6879
|
-
function isRecord$
|
|
7205
|
+
function isRecord$3(value2) {
|
|
6880
7206
|
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
6881
7207
|
}
|
|
6882
7208
|
function nullableString$2(value2) {
|
|
@@ -6889,7 +7215,7 @@ function stringArray(value2) {
|
|
|
6889
7215
|
return Array.isArray(value2) ? value2.filter((entry) => typeof entry === "string") : [];
|
|
6890
7216
|
}
|
|
6891
7217
|
function promptSuggestion(value2) {
|
|
6892
|
-
if (!isRecord$
|
|
7218
|
+
if (!isRecord$3(value2)) return null;
|
|
6893
7219
|
const text2 = value2.text;
|
|
6894
7220
|
const provider2 = value2.provider;
|
|
6895
7221
|
const updatedAt = value2.updatedAt;
|
|
@@ -6897,7 +7223,7 @@ function promptSuggestion(value2) {
|
|
|
6897
7223
|
return { text: text2, provider: provider2, updatedAt };
|
|
6898
7224
|
}
|
|
6899
7225
|
function parseRow(value2) {
|
|
6900
|
-
if (!isRecord$
|
|
7226
|
+
if (!isRecord$3(value2)) return null;
|
|
6901
7227
|
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
7228
|
return {
|
|
6903
7229
|
id: value2.id,
|
|
@@ -6925,7 +7251,7 @@ function parseRow(value2) {
|
|
|
6925
7251
|
};
|
|
6926
7252
|
}
|
|
6927
7253
|
function parseProjectConversationsPayload(value2) {
|
|
6928
|
-
if (!isRecord$
|
|
7254
|
+
if (!isRecord$3(value2)) throw new Error("Invalid project conversations response");
|
|
6929
7255
|
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
7256
|
const conversations = [];
|
|
6931
7257
|
for (const raw of value2.conversations.slice(0, MAX_MAPPING_COUNT)) {
|
|
@@ -6944,7 +7270,7 @@ function parseProjectConversationsPayload(value2) {
|
|
|
6944
7270
|
const COMPANY_MEMBER_DIRECTORY_PAGE_LIMIT = 100;
|
|
6945
7271
|
const MAX_PROJECT_MEMBER_COUNT = 1e3;
|
|
6946
7272
|
const MAX_CURSOR_LENGTH = 2048;
|
|
6947
|
-
function isRecord$
|
|
7273
|
+
function isRecord$2(value2) {
|
|
6948
7274
|
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
6949
7275
|
}
|
|
6950
7276
|
function nullableString$1(value2) {
|
|
@@ -6952,11 +7278,11 @@ function nullableString$1(value2) {
|
|
|
6952
7278
|
}
|
|
6953
7279
|
function parseAvatar(value2) {
|
|
6954
7280
|
if (value2 === null) return null;
|
|
6955
|
-
if (!isRecord$
|
|
7281
|
+
if (!isRecord$2(value2) || typeof value2.path !== "string" || typeof value2.url !== "string") return void 0;
|
|
6956
7282
|
return { path: value2.path, url: value2.url };
|
|
6957
7283
|
}
|
|
6958
7284
|
function parseProjectMember(value2) {
|
|
6959
|
-
if (!isRecord$
|
|
7285
|
+
if (!isRecord$2(value2)) return null;
|
|
6960
7286
|
const username = nullableString$1(value2.username);
|
|
6961
7287
|
const firstName = nullableString$1(value2.firstName);
|
|
6962
7288
|
const lastName = nullableString$1(value2.lastName);
|
|
@@ -6976,7 +7302,7 @@ function parseProjectMember(value2) {
|
|
|
6976
7302
|
};
|
|
6977
7303
|
}
|
|
6978
7304
|
function parseProjectMembersPayload(value2) {
|
|
6979
|
-
if (!isRecord$
|
|
7305
|
+
if (!isRecord$2(value2) || !Array.isArray(value2.members)) {
|
|
6980
7306
|
throw new Error("Invalid project members response");
|
|
6981
7307
|
}
|
|
6982
7308
|
if (value2.members.length > MAX_PROJECT_MEMBER_COUNT) {
|
|
@@ -6989,7 +7315,7 @@ function parseProjectMembersPayload(value2) {
|
|
|
6989
7315
|
});
|
|
6990
7316
|
}
|
|
6991
7317
|
function parseDirectoryEntry(value2) {
|
|
6992
|
-
if (!isRecord$
|
|
7318
|
+
if (!isRecord$2(value2)) return null;
|
|
6993
7319
|
const email = nullableString$1(value2.email);
|
|
6994
7320
|
const displayName = nullableString$1(value2.displayName);
|
|
6995
7321
|
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 +7336,8 @@ function buildCompanyMemberDirectoryPath(companyId, cursor) {
|
|
|
7010
7336
|
return `/api/companies/${encodeURIComponent(trimmedCompanyId)}/member-directory?${params.toString()}`;
|
|
7011
7337
|
}
|
|
7012
7338
|
function parseCompanyMemberDirectoryPage(value2) {
|
|
7013
|
-
const rawItems = Array.isArray(value2) ? value2 : isRecord$
|
|
7014
|
-
const rawCursor = Array.isArray(value2) ? null : isRecord$
|
|
7339
|
+
const rawItems = Array.isArray(value2) ? value2 : isRecord$2(value2) && Array.isArray(value2.items) ? value2.items : null;
|
|
7340
|
+
const rawCursor = Array.isArray(value2) ? null : isRecord$2(value2) ? value2.nextCursor : void 0;
|
|
7015
7341
|
if (!rawItems || rawItems.length > COMPANY_MEMBER_DIRECTORY_PAGE_LIMIT || !(rawCursor === null || typeof rawCursor === "string" && rawCursor.length > 0 && rawCursor.length <= MAX_CURSOR_LENGTH)) {
|
|
7016
7342
|
if (rawItems && rawItems.length > COMPANY_MEMBER_DIRECTORY_PAGE_LIMIT) {
|
|
7017
7343
|
throw new Error("Company member directory response is too large");
|
|
@@ -15444,7 +15770,7 @@ function branchSlugFromMetadata(metadata) {
|
|
|
15444
15770
|
function stringField(value2) {
|
|
15445
15771
|
return typeof value2 === "string" && value2.trim() ? value2.trim() : void 0;
|
|
15446
15772
|
}
|
|
15447
|
-
function isRecord(value2) {
|
|
15773
|
+
function isRecord$1(value2) {
|
|
15448
15774
|
return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
|
|
15449
15775
|
}
|
|
15450
15776
|
function numberField(value2) {
|
|
@@ -15471,7 +15797,7 @@ function firstTokenCount(...values) {
|
|
|
15471
15797
|
return void 0;
|
|
15472
15798
|
}
|
|
15473
15799
|
function coerceTokenUsageRecord(value2, allowGenericKeys = true) {
|
|
15474
|
-
if (!isRecord(value2)) return void 0;
|
|
15800
|
+
if (!isRecord$1(value2)) return void 0;
|
|
15475
15801
|
const inputTokens = firstTokenCount(
|
|
15476
15802
|
value2.inputTokens,
|
|
15477
15803
|
value2.input_tokens,
|
|
@@ -15534,7 +15860,7 @@ function coerceSessionTokenUsage(...sources) {
|
|
|
15534
15860
|
for (const source of sources) {
|
|
15535
15861
|
const direct = coerceTokenUsageRecord(source, false);
|
|
15536
15862
|
if (direct) return direct;
|
|
15537
|
-
if (!isRecord(source)) continue;
|
|
15863
|
+
if (!isRecord$1(source)) continue;
|
|
15538
15864
|
for (const key of usageKeys) {
|
|
15539
15865
|
const nested = coerceTokenUsageRecord(source[key]);
|
|
15540
15866
|
if (nested) return nested;
|
|
@@ -15546,7 +15872,7 @@ function pullRequestStateField(value2) {
|
|
|
15546
15872
|
return value2 === "open" || value2 === "closed" || value2 === "merged" ? value2 : void 0;
|
|
15547
15873
|
}
|
|
15548
15874
|
function coercePendingAskUserQuestionRequest(requestId, value2) {
|
|
15549
|
-
if (!isRecord(value2)) return null;
|
|
15875
|
+
if (!isRecord$1(value2)) return null;
|
|
15550
15876
|
const envelope = normalizePendingPermissionEnvelope(requestId, value2);
|
|
15551
15877
|
if (!envelope || !isAskUserQuestionToolName(envelope.toolName)) return null;
|
|
15552
15878
|
const name = envelope.toolName;
|
|
@@ -15563,7 +15889,7 @@ function apiSessionAgentStatePendingAskUserQuestion(agentState, candidateKeys) {
|
|
|
15563
15889
|
const state = decryptJsonWithCandidates(agentState, candidateKeys);
|
|
15564
15890
|
if (!state) return null;
|
|
15565
15891
|
const requests = state.requests;
|
|
15566
|
-
if (!isRecord(requests)) return null;
|
|
15892
|
+
if (!isRecord$1(requests)) return null;
|
|
15567
15893
|
for (const [requestId, request] of Object.entries(requests)) {
|
|
15568
15894
|
const pending = coercePendingAskUserQuestionRequest(requestId, request);
|
|
15569
15895
|
if (pending) return pending;
|
|
@@ -15587,7 +15913,7 @@ function coercePendingExitPlanModeRequest(requestId, value2) {
|
|
|
15587
15913
|
function apiSessionAgentStatePendingExitPlanMode(agentState, candidateKeys) {
|
|
15588
15914
|
if (!agentState) return null;
|
|
15589
15915
|
const state = decryptJsonWithCandidates(agentState, candidateKeys);
|
|
15590
|
-
if (!state || !isRecord(state.requests)) return null;
|
|
15916
|
+
if (!state || !isRecord$1(state.requests)) return null;
|
|
15591
15917
|
for (const [requestId, request] of Object.entries(state.requests)) {
|
|
15592
15918
|
const pending = coercePendingExitPlanModeRequest(requestId, request);
|
|
15593
15919
|
if (pending) return pending;
|
|
@@ -15633,7 +15959,7 @@ function optionalTimestamp(value2) {
|
|
|
15633
15959
|
return parsed > 0 ? parsed : void 0;
|
|
15634
15960
|
}
|
|
15635
15961
|
function coercePromptSuggestion(value2) {
|
|
15636
|
-
if (!isRecord(value2)) return void 0;
|
|
15962
|
+
if (!isRecord$1(value2)) return void 0;
|
|
15637
15963
|
const text2 = nonEmptyString(value2.text);
|
|
15638
15964
|
const provider2 = nonEmptyString(value2.provider);
|
|
15639
15965
|
const updatedAt = optionalTimestamp(value2.updatedAt);
|
|
@@ -15641,7 +15967,7 @@ function coercePromptSuggestion(value2) {
|
|
|
15641
15967
|
return { text: text2, provider: provider2, updatedAt };
|
|
15642
15968
|
}
|
|
15643
15969
|
function coerceCreatedBy(value2) {
|
|
15644
|
-
if (!isRecord(value2)) return void 0;
|
|
15970
|
+
if (!isRecord$1(value2)) return void 0;
|
|
15645
15971
|
const accountId = nonEmptyString(value2.accountId);
|
|
15646
15972
|
if (!accountId) return void 0;
|
|
15647
15973
|
const displayName = nonEmptyString(value2.displayName);
|
|
@@ -15672,6 +15998,7 @@ function apiSessionToSession(api, candidateKeys, existing) {
|
|
|
15672
15998
|
const archiveReason = nonEmptyString(metadata?.archiveReason) ?? existing?.archiveReason;
|
|
15673
15999
|
const createdBy = coerceCreatedBy(metadata?.createdBy) ?? existing?.createdBy;
|
|
15674
16000
|
const branchSlug = branchSlugFromMetadata(metadata) ?? existing?.branchSlug;
|
|
16001
|
+
const path = nonEmptyString(metadata?.path) ?? existing?.path;
|
|
15675
16002
|
const promptSuggestion2 = metadata && Object.prototype.hasOwnProperty.call(metadata, "promptSuggestion") ? coercePromptSuggestion(metadata.promptSuggestion) : existing?.promptSuggestion;
|
|
15676
16003
|
return {
|
|
15677
16004
|
...existing,
|
|
@@ -15707,6 +16034,7 @@ function apiSessionToSession(api, candidateKeys, existing) {
|
|
|
15707
16034
|
...archiveReason ? { archiveReason } : {},
|
|
15708
16035
|
...createdBy ? { createdBy } : {},
|
|
15709
16036
|
...branchSlug ? { branchSlug } : {},
|
|
16037
|
+
...path ? { path } : {},
|
|
15710
16038
|
...sessionFilesystemScopePatch(metadata, existing),
|
|
15711
16039
|
...remoteThinking === null ? {} : { agentStateBusy: remoteThinking }
|
|
15712
16040
|
};
|
|
@@ -16770,9 +17098,25 @@ class ApiClient {
|
|
|
16770
17098
|
throw error;
|
|
16771
17099
|
}
|
|
16772
17100
|
}
|
|
16773
|
-
|
|
16774
|
-
|
|
16775
|
-
|
|
17101
|
+
/**
|
|
17102
|
+
* 자동화·GitHub 트리거가 요구하는 두 전제를 멱등하게 맞춘다: happy 미러
|
|
17103
|
+
* 행이 존재할 것, 그리고 그 행의 `config.machineId` 가 대상 머신일 것.
|
|
17104
|
+
*
|
|
17105
|
+
* A+ 는 프로젝트를 자기 DB 에 만들고 happy 미러는 일부 경로에서만 만든다.
|
|
17106
|
+
* 미러가 없으면 아래 GET 이 404 `project-not-found` 로 죽어 데스크톱의
|
|
17107
|
+
* 자동화 화면이 통째로 막혔다 — 그 404 를 여기서 생성으로 흡수한다.
|
|
17108
|
+
* 정상 경로(미러 존재)는 기존과 같이 GET 한 번으로 끝난다.
|
|
17109
|
+
*/
|
|
17110
|
+
async ensureHappyProjectMachineBinding(input) {
|
|
17111
|
+
const { project: source, machineId } = input;
|
|
17112
|
+
const path = `/v1/projects/${encodeURIComponent(source.id)}`;
|
|
17113
|
+
let data;
|
|
17114
|
+
try {
|
|
17115
|
+
data = await this.request(path, { boundary: "sync" });
|
|
17116
|
+
} catch (error) {
|
|
17117
|
+
if (!(error instanceof ApiError) || error.status !== 404) throw error;
|
|
17118
|
+
data = await this.createHappyProjectMirror(source, machineId);
|
|
17119
|
+
}
|
|
16776
17120
|
if (!data.project || typeof data.project !== "object" || Array.isArray(data.project)) {
|
|
16777
17121
|
throw new Error("happy-project-response-invalid");
|
|
16778
17122
|
}
|
|
@@ -16788,6 +17132,34 @@ class ApiClient {
|
|
|
16788
17132
|
body: JSON.stringify({ config: { ...config, machineId } })
|
|
16789
17133
|
});
|
|
16790
17134
|
}
|
|
17135
|
+
/**
|
|
17136
|
+
* happy 의 projectCreate 는 `config` 를 그대로 받아 응답에 되돌려주므로,
|
|
17137
|
+
* 신규 생성이면 호출부의 바인딩 조정이 즉시 끝나 왕복이 하나로 마무리된다.
|
|
17138
|
+
* 응답을 그대로 돌려주는 이유는 호출부 주석 참고 — 이 연산은 멱등이라
|
|
17139
|
+
* 남이 만든 미러를 돌려줄 수도 있어서 응답을 믿고 건너뛰면 안 된다.
|
|
17140
|
+
*
|
|
17141
|
+
* 409 는 같은 id 를 다른 happy 계정이 쥐고 있다는 뜻이라 이 클라이언트에서
|
|
17142
|
+
* 고칠 수 없다 — 조용히 넘기면 자동화 화면이 원인 없는 404 를 계속
|
|
17143
|
+
* 보여주므로 그대로 올린다.
|
|
17144
|
+
*/
|
|
17145
|
+
async createHappyProjectMirror(source, machineId) {
|
|
17146
|
+
const body = { id: source.id, name: source.name };
|
|
17147
|
+
if (typeof source.description === "string") body.description = source.description;
|
|
17148
|
+
if (typeof source.color === "string") body.color = source.color;
|
|
17149
|
+
body.config = { machineId };
|
|
17150
|
+
try {
|
|
17151
|
+
return await this.request("/v1/projects", {
|
|
17152
|
+
method: "POST",
|
|
17153
|
+
boundary: "sync",
|
|
17154
|
+
body: JSON.stringify(body)
|
|
17155
|
+
});
|
|
17156
|
+
} catch (error) {
|
|
17157
|
+
if (error instanceof ApiError && error.status === 409) {
|
|
17158
|
+
throw new Error("happy-project-id-taken");
|
|
17159
|
+
}
|
|
17160
|
+
throw error;
|
|
17161
|
+
}
|
|
17162
|
+
}
|
|
16791
17163
|
async getProjectMembers(projectId) {
|
|
16792
17164
|
const data = await this.request(
|
|
16793
17165
|
`/v1/projects/${encodeURIComponent(projectId)}/members`,
|
|
@@ -17304,6 +17676,44 @@ class ApiClient {
|
|
|
17304
17676
|
tokenOverride
|
|
17305
17677
|
});
|
|
17306
17678
|
}
|
|
17679
|
+
/**
|
|
17680
|
+
* 복호화 없이 원본 행을 전량 — import 파이프가 metadata 블롭을 재암호화할 때 쓴다.
|
|
17681
|
+
* /v1/sessions 는 150건에서 잘리므로 /v2 커서 페이지네이션으로 끝까지 걷는다
|
|
17682
|
+
* (조용한 절단은 "일부만 이관됐는데 성공으로 보이는" 최악의 실패를 만든다).
|
|
17683
|
+
*/
|
|
17684
|
+
async fetchApiSessions(tokenOverride) {
|
|
17685
|
+
const all = [];
|
|
17686
|
+
let cursor = null;
|
|
17687
|
+
for (; ; ) {
|
|
17688
|
+
const params = new URLSearchParams({ limit: "200" });
|
|
17689
|
+
if (cursor) params.set("cursor", cursor);
|
|
17690
|
+
const data = await this.request(`/v2/sessions?${params.toString()}`, { boundary: "sync", tokenOverride });
|
|
17691
|
+
all.push(...data.sessions ?? []);
|
|
17692
|
+
if (data.hasNext !== true || !data.nextCursor || data.nextCursor === cursor) break;
|
|
17693
|
+
cursor = data.nextCursor;
|
|
17694
|
+
}
|
|
17695
|
+
return all;
|
|
17696
|
+
}
|
|
17697
|
+
/**
|
|
17698
|
+
* 라이브 CLI 없이 세션 행만 만든다 (specs/desktop-standalone-cloud-import).
|
|
17699
|
+
* 서버는 (accountId, tag) 기준 get-or-create라 같은 tag 재호출은 기존 세션을 돌려준다.
|
|
17700
|
+
* dataEncryptionKey 를 null 로 둬 legacy secretbox(계정 시드) 경로로 통일한다 —
|
|
17701
|
+
* 데스크탑의 encode/decodeMessageContent 가 읽고 쓸 수 있는 유일한 변형이다.
|
|
17702
|
+
*/
|
|
17703
|
+
async createRawSession(input, tokenOverride) {
|
|
17704
|
+
const data = await this.request("/v1/sessions", {
|
|
17705
|
+
method: "POST",
|
|
17706
|
+
boundary: "sync",
|
|
17707
|
+
tokenOverride,
|
|
17708
|
+
body: JSON.stringify({
|
|
17709
|
+
tag: input.tag,
|
|
17710
|
+
metadata: input.metadata,
|
|
17711
|
+
agentState: input.agentState ?? null,
|
|
17712
|
+
dataEncryptionKey: null
|
|
17713
|
+
})
|
|
17714
|
+
});
|
|
17715
|
+
return data.session;
|
|
17716
|
+
}
|
|
17307
17717
|
async getGithubPatStatus(tokenOverride) {
|
|
17308
17718
|
const raw = await this.request(
|
|
17309
17719
|
"/v1/connect/github-pat/status",
|
|
@@ -17436,17 +17846,7 @@ class ApiClient {
|
|
|
17436
17846
|
async sendUserTextMessage(input) {
|
|
17437
17847
|
const localId = input.localId || `desktop-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
17438
17848
|
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
|
-
};
|
|
17849
|
+
const content = buildUserTextMessageContent(input);
|
|
17450
17850
|
const encoded = encodeMessageContent(content, key);
|
|
17451
17851
|
const result = await this.sendMessages(input.sessionId, [{ localId, content: encoded }], input.tokenOverride);
|
|
17452
17852
|
return { localId, sentSeq: result.messages?.[0]?.seq };
|
|
@@ -17951,6 +18351,17 @@ class ApiClient {
|
|
|
17951
18351
|
{ boundary: "api" }
|
|
17952
18352
|
));
|
|
17953
18353
|
}
|
|
18354
|
+
/**
|
|
18355
|
+
* personal-credential-set-apply — owner/admin 이 아닌 일반 멤버도 호출 가능.
|
|
18356
|
+
* 서버가 이미 배포 자격(개인 머신 + 본인이 등록한 이 회사의 공용 머신)
|
|
18357
|
+
* 으로 필터링해 돌려준다.
|
|
18358
|
+
*/
|
|
18359
|
+
async getMyMachines(companyId) {
|
|
18360
|
+
return parseSelfServiceMachines(await this.request(
|
|
18361
|
+
myMachinesPath(companyId),
|
|
18362
|
+
{ boundary: "api" }
|
|
18363
|
+
));
|
|
18364
|
+
}
|
|
17954
18365
|
async getCompanyAiCredentials(companyId) {
|
|
17955
18366
|
return parseAiCredentialVaultSnapshot(await this.request(
|
|
17956
18367
|
aiCredentialsPath(companyId),
|
|
@@ -18063,11 +18474,20 @@ function credentialSearchPaths(input) {
|
|
|
18063
18474
|
}
|
|
18064
18475
|
return [
|
|
18065
18476
|
{ source: "saycode", path: joinPath(input.homeDir, ".saycode", "credentials.v1.json") },
|
|
18066
|
-
|
|
18067
|
-
|
|
18068
|
-
|
|
18477
|
+
...happyHomeOrder(input).map((source) => ({
|
|
18478
|
+
source,
|
|
18479
|
+
path: joinPath(input.homeDir, `.${source}`, "access.key")
|
|
18480
|
+
}))
|
|
18069
18481
|
];
|
|
18070
18482
|
}
|
|
18483
|
+
function happyHomeOrder(input) {
|
|
18484
|
+
const homes = CREDENTIAL_SOURCE_ORDER.filter((source) => source !== "saycode");
|
|
18485
|
+
const runtimeHome = input.env.HAPPY_HOME_DIR?.trim();
|
|
18486
|
+
if (!runtimeHome) return [...homes];
|
|
18487
|
+
const runtime = homes.find((source) => joinPath(input.homeDir, `.${source}`) === joinPath(runtimeHome));
|
|
18488
|
+
if (!runtime) return [...homes];
|
|
18489
|
+
return [runtime, ...homes.filter((source) => source !== runtime)];
|
|
18490
|
+
}
|
|
18071
18491
|
function selectFirstValidCredential(candidates, readFile2) {
|
|
18072
18492
|
for (const candidate of candidates) {
|
|
18073
18493
|
const raw = readFile2(candidate.path);
|
|
@@ -18397,6 +18817,12 @@ class CliRest {
|
|
|
18397
18817
|
if (!apiToken) throw new ApiUnavailableError();
|
|
18398
18818
|
return this.request(path, { method: "GET" }, apiToken);
|
|
18399
18819
|
}
|
|
18820
|
+
async listProjectWorktrees(projectId) {
|
|
18821
|
+
const data = await this.api(
|
|
18822
|
+
`/api/projects/${encodeURIComponent(projectId)}/worktrees`
|
|
18823
|
+
);
|
|
18824
|
+
return data.worktrees ?? [];
|
|
18825
|
+
}
|
|
18400
18826
|
async createProjectWorktree(projectId, input) {
|
|
18401
18827
|
const data = await this.apiMutation(
|
|
18402
18828
|
`/api/projects/${encodeURIComponent(projectId)}/worktrees`,
|
|
@@ -20429,6 +20855,7 @@ const USAGE_SETTINGS_NAVIGATION_DESTINATIONS = [
|
|
|
20429
20855
|
"settings_memory",
|
|
20430
20856
|
"settings_conversation_search",
|
|
20431
20857
|
"settings_connectors",
|
|
20858
|
+
"settings_my_ai_credentials",
|
|
20432
20859
|
"settings_extensions",
|
|
20433
20860
|
"settings_about"
|
|
20434
20861
|
];
|
|
@@ -22742,6 +23169,50 @@ function readMachineId(input, readFile2) {
|
|
|
22742
23169
|
function spawnBudgetKey(env) {
|
|
22743
23170
|
return env.agentId ?? env.root;
|
|
22744
23171
|
}
|
|
23172
|
+
function normalizeAbsolutePath(raw) {
|
|
23173
|
+
const trimmed = raw.trim();
|
|
23174
|
+
if (!trimmed.startsWith("/")) return "";
|
|
23175
|
+
const resolved = [];
|
|
23176
|
+
for (const segment of trimmed.split("/")) {
|
|
23177
|
+
if (!segment || segment === ".") continue;
|
|
23178
|
+
if (segment === "..") {
|
|
23179
|
+
resolved.pop();
|
|
23180
|
+
continue;
|
|
23181
|
+
}
|
|
23182
|
+
resolved.push(segment);
|
|
23183
|
+
}
|
|
23184
|
+
return `/${resolved.join("/")}`;
|
|
23185
|
+
}
|
|
23186
|
+
function isInsideTree(path, root) {
|
|
23187
|
+
const normalizedRoot = normalizeAbsolutePath(root);
|
|
23188
|
+
if (!normalizedRoot || !path) return false;
|
|
23189
|
+
if (path === normalizedRoot) return true;
|
|
23190
|
+
return path.startsWith(normalizedRoot === "/" ? "/" : `${normalizedRoot}/`);
|
|
23191
|
+
}
|
|
23192
|
+
function resolveSpawnContextFiles(input) {
|
|
23193
|
+
const resolved = [];
|
|
23194
|
+
for (const original of input.paths) {
|
|
23195
|
+
const path = normalizeAbsolutePath(original);
|
|
23196
|
+
if (!path || !isInsideTree(path, input.root)) {
|
|
23197
|
+
return { ok: false, reason: "out_of_scope", path: original };
|
|
23198
|
+
}
|
|
23199
|
+
if (!input.isReadableFile(path)) {
|
|
23200
|
+
return { ok: false, reason: "file_unreadable", path: original };
|
|
23201
|
+
}
|
|
23202
|
+
resolved.push(path);
|
|
23203
|
+
}
|
|
23204
|
+
return { ok: true, paths: resolved };
|
|
23205
|
+
}
|
|
23206
|
+
function composeSpawnPrompt(text2, contextPaths) {
|
|
23207
|
+
if (contextPaths.length === 0) return text2;
|
|
23208
|
+
const files = contextPaths.map((path) => `- ${path}`).join("\n");
|
|
23209
|
+
return [
|
|
23210
|
+
"Read these files first — they are the context for this task:",
|
|
23211
|
+
files,
|
|
23212
|
+
"",
|
|
23213
|
+
text2
|
|
23214
|
+
].join("\n");
|
|
23215
|
+
}
|
|
22745
23216
|
const HANDOFF_INLINE_LIMIT = 12e3;
|
|
22746
23217
|
const TRUNCATION_NOTICE = "(대화가 길어 앞부분이 잘렸습니다. 아래는 가장 최근 대화입니다.)";
|
|
22747
23218
|
const HEADER_TITLE_LIMIT = 200;
|
|
@@ -22808,6 +23279,13 @@ function buildAgentHandoffPrompt(source, context) {
|
|
|
22808
23279
|
...FIRST_ACTIONS.map((action) => `- ${action}`)
|
|
22809
23280
|
].join("\n");
|
|
22810
23281
|
}
|
|
23282
|
+
async function fileSpawnedSessionUnderProject(runtime, sessionId, directory) {
|
|
23283
|
+
if (!runtime.registerSpawnedSession) return;
|
|
23284
|
+
try {
|
|
23285
|
+
await runtime.registerSpawnedSession({ sessionId, directory });
|
|
23286
|
+
} catch {
|
|
23287
|
+
}
|
|
23288
|
+
}
|
|
22811
23289
|
function quote(text2) {
|
|
22812
23290
|
return text2.split("\n").map((line2) => line2 ? `> ${line2}` : ">").join("\n");
|
|
22813
23291
|
}
|
|
@@ -22881,6 +23359,7 @@ async function handoffToSibling(runtime, scope, env, handoff, agent) {
|
|
|
22881
23359
|
root: env.root,
|
|
22882
23360
|
depth: env.depth,
|
|
22883
23361
|
childSessionId: sessionId,
|
|
23362
|
+
...env.sessionId ? { parentSessionId: env.sessionId } : {},
|
|
22884
23363
|
agent,
|
|
22885
23364
|
promptPreview: `handoff from ${from}`,
|
|
22886
23365
|
outcome: sessionId ? "spawned" : "failed"
|
|
@@ -22892,6 +23371,7 @@ async function handoffToSibling(runtime, scope, env, handoff, agent) {
|
|
|
22892
23371
|
if (!sessionId) {
|
|
22893
23372
|
return agentFail("spawn_failed", `the machine daemon did not confirm a session: ${String(spawnError)}`);
|
|
22894
23373
|
}
|
|
23374
|
+
await fileSpawnedSessionUnderProject(runtime, sessionId, env.root);
|
|
22895
23375
|
return agentOk({
|
|
22896
23376
|
sessionId,
|
|
22897
23377
|
agent,
|
|
@@ -22929,6 +23409,107 @@ ${clamped.text}` : clamped.text
|
|
|
22929
23409
|
}
|
|
22930
23410
|
);
|
|
22931
23411
|
}
|
|
23412
|
+
function isRecord(value2, allowCli) {
|
|
23413
|
+
if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return false;
|
|
23414
|
+
const record2 = value2;
|
|
23415
|
+
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");
|
|
23416
|
+
}
|
|
23417
|
+
function validatedRecords(value2, allowCli = true) {
|
|
23418
|
+
if (!Array.isArray(value2) || !value2.every((record2) => isRecord(record2, allowCli))) return null;
|
|
23419
|
+
const parentByChild = /* @__PURE__ */ new Map();
|
|
23420
|
+
for (const record2 of value2) {
|
|
23421
|
+
if (parentByChild.has(record2.childSessionId)) return null;
|
|
23422
|
+
parentByChild.set(record2.childSessionId, record2.parentSessionId);
|
|
23423
|
+
}
|
|
23424
|
+
for (const child of parentByChild.keys()) {
|
|
23425
|
+
const visited = /* @__PURE__ */ new Set([child]);
|
|
23426
|
+
let current = child;
|
|
23427
|
+
while (parentByChild.has(current)) {
|
|
23428
|
+
current = parentByChild.get(current);
|
|
23429
|
+
if (visited.has(current)) return null;
|
|
23430
|
+
visited.add(current);
|
|
23431
|
+
}
|
|
23432
|
+
}
|
|
23433
|
+
return value2.map((record2) => ({ ...record2 }));
|
|
23434
|
+
}
|
|
23435
|
+
function isDescendant(records, ancestorSessionId, targetSessionId, maxDepth = 2) {
|
|
23436
|
+
if (!ancestorSessionId || !targetSessionId || ancestorSessionId === targetSessionId || maxDepth < 1) return false;
|
|
23437
|
+
const valid = validatedRecords(records);
|
|
23438
|
+
if (!valid) return false;
|
|
23439
|
+
const parentByChild = new Map(valid.map((record2) => [record2.childSessionId, record2.parentSessionId]));
|
|
23440
|
+
let current = targetSessionId;
|
|
23441
|
+
for (let depth = 1; depth <= maxDepth; depth += 1) {
|
|
23442
|
+
const parent = parentByChild.get(current);
|
|
23443
|
+
if (!parent) return false;
|
|
23444
|
+
if (parent === ancestorSessionId) return true;
|
|
23445
|
+
current = parent;
|
|
23446
|
+
}
|
|
23447
|
+
return false;
|
|
23448
|
+
}
|
|
23449
|
+
function resolveSubagentControlPermission(records, parentSessionId, childSessionId) {
|
|
23450
|
+
const valid = validatedRecords(records);
|
|
23451
|
+
return valid ? isDescendant(valid, parentSessionId, childSessionId, 2) : false;
|
|
23452
|
+
}
|
|
23453
|
+
function canControlSpawnedDescendant(records, parentSessionId, childSessionId) {
|
|
23454
|
+
const lineage = records.flatMap((record2) => record2.outcome === "spawned" && record2.parentSessionId && record2.childSessionId ? [{
|
|
23455
|
+
parentSessionId: record2.parentSessionId,
|
|
23456
|
+
childSessionId: record2.childSessionId,
|
|
23457
|
+
spawnedAt: record2.ts,
|
|
23458
|
+
source: "handoff"
|
|
23459
|
+
}] : []);
|
|
23460
|
+
return resolveSubagentControlPermission(lineage, parentSessionId, childSessionId);
|
|
23461
|
+
}
|
|
23462
|
+
function ownershipFailure(runtime, env, target) {
|
|
23463
|
+
if (!env.sessionId) return agentFail("session_unknown", "This session did not receive APLUS_SESSION_ID.");
|
|
23464
|
+
if (!runtime.readLineage) return agentFail("not_owned", "spawn lineage is unavailable");
|
|
23465
|
+
try {
|
|
23466
|
+
return canControlSpawnedDescendant(runtime.readLineage(), env.sessionId, target.session.id) ? null : agentFail("not_owned", `${target.session.id} is not an owned descendant`);
|
|
23467
|
+
} catch (error) {
|
|
23468
|
+
return agentFail("not_owned", `spawn ledger is unreadable: ${String(error)}`);
|
|
23469
|
+
}
|
|
23470
|
+
}
|
|
23471
|
+
async function steerOwnedTarget(runtime, env, target, text2) {
|
|
23472
|
+
if (!text2.trim()) return agentFail("steer_rejected", "steer text is empty");
|
|
23473
|
+
const refused = ownershipFailure(runtime, env, target);
|
|
23474
|
+
if (refused) return refused;
|
|
23475
|
+
if (!runtime.steerSession) return agentFail("steer_rejected", "steer transport is unavailable");
|
|
23476
|
+
try {
|
|
23477
|
+
const result = await runtime.steerSession(target.session.id, text2);
|
|
23478
|
+
if (result.accepted) {
|
|
23479
|
+
return agentOk({
|
|
23480
|
+
sessionId: target.session.id,
|
|
23481
|
+
accepted: true,
|
|
23482
|
+
// RPC acknowledgement proves acceptance, not transcript delivery.
|
|
23483
|
+
delivered: false
|
|
23484
|
+
});
|
|
23485
|
+
}
|
|
23486
|
+
return result.reason === "in-flight" ? agentFail("steer_in_flight", result.error) : agentFail("steer_rejected", result.error);
|
|
23487
|
+
} catch (error) {
|
|
23488
|
+
return agentFail("steer_rejected", error instanceof Error ? error.message : String(error));
|
|
23489
|
+
}
|
|
23490
|
+
}
|
|
23491
|
+
async function stopOwnedTarget(runtime, env, target) {
|
|
23492
|
+
const refused = ownershipFailure(runtime, env, target);
|
|
23493
|
+
if (refused) return refused;
|
|
23494
|
+
if (!runtime.stopSession) return agentFail("stop_rejected", "stop transport is unavailable");
|
|
23495
|
+
let stream2 = null;
|
|
23496
|
+
try {
|
|
23497
|
+
stream2 = await runtime.openStream(target.session.id);
|
|
23498
|
+
if (stream2.current().thinking !== true) {
|
|
23499
|
+
return agentFail("stop_not_responding", `${target.session.id} has no active response`);
|
|
23500
|
+
}
|
|
23501
|
+
} catch (error) {
|
|
23502
|
+
return agentFail("stop_rejected", error instanceof Error ? error.message : String(error));
|
|
23503
|
+
} finally {
|
|
23504
|
+
stream2?.close();
|
|
23505
|
+
}
|
|
23506
|
+
try {
|
|
23507
|
+
const result = await runtime.stopSession(target.session.machineId, target.session.id);
|
|
23508
|
+
return result.stopped ? agentOk({ sessionId: target.session.id, stopped: true }) : agentFail("stop_rejected", result.error);
|
|
23509
|
+
} catch (error) {
|
|
23510
|
+
return agentFail("stop_rejected", error instanceof Error ? error.message : String(error));
|
|
23511
|
+
}
|
|
23512
|
+
}
|
|
22932
23513
|
const DEFAULT_READ_LIMIT = 20;
|
|
22933
23514
|
const STATE_WINDOW_MESSAGES = 50;
|
|
22934
23515
|
function scopeOf(runtime) {
|
|
@@ -22982,10 +23563,18 @@ async function readSession(runtime, target, last) {
|
|
|
22982
23563
|
});
|
|
22983
23564
|
}
|
|
22984
23565
|
const DEFAULT_SPAWN_AGENT = "claude";
|
|
22985
|
-
async function spawnSibling(runtime, env, text2, agent) {
|
|
23566
|
+
async function spawnSibling(runtime, env, text2, agent, contextFiles = []) {
|
|
22986
23567
|
if (env.depth >= AGENT_MAX_DEPTH) {
|
|
22987
23568
|
return agentFail("spawn_depth_exceeded", `depth ${env.depth} is already at the ceiling of ${AGENT_MAX_DEPTH}`);
|
|
22988
23569
|
}
|
|
23570
|
+
const context = resolveSpawnContextFiles({
|
|
23571
|
+
paths: contextFiles,
|
|
23572
|
+
root: env.root,
|
|
23573
|
+
isReadableFile: runtime.isReadableFile
|
|
23574
|
+
});
|
|
23575
|
+
if (!context.ok) {
|
|
23576
|
+
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`);
|
|
23577
|
+
}
|
|
22989
23578
|
const key = spawnBudgetKey(env);
|
|
22990
23579
|
let spawned;
|
|
22991
23580
|
try {
|
|
@@ -23002,7 +23591,7 @@ async function spawnSibling(runtime, env, text2, agent) {
|
|
|
23002
23591
|
sessionId = (await runtime.spawnSession({
|
|
23003
23592
|
directory: env.root,
|
|
23004
23593
|
agent,
|
|
23005
|
-
initialPrompt: text2,
|
|
23594
|
+
initialPrompt: composeSpawnPrompt(text2, context.paths),
|
|
23006
23595
|
environmentVariables: childAgentEnv(env, childId)
|
|
23007
23596
|
})).sessionId;
|
|
23008
23597
|
} catch (error) {
|
|
@@ -23015,6 +23604,7 @@ async function spawnSibling(runtime, env, text2, agent) {
|
|
|
23015
23604
|
root: env.root,
|
|
23016
23605
|
depth: env.depth,
|
|
23017
23606
|
childSessionId: sessionId,
|
|
23607
|
+
...env.sessionId ? { parentSessionId: env.sessionId } : {},
|
|
23018
23608
|
agent,
|
|
23019
23609
|
promptPreview: text2,
|
|
23020
23610
|
outcome: sessionId ? "spawned" : "failed"
|
|
@@ -23026,6 +23616,7 @@ async function spawnSibling(runtime, env, text2, agent) {
|
|
|
23026
23616
|
if (!sessionId) {
|
|
23027
23617
|
return agentFail("spawn_failed", `the machine daemon did not confirm a session: ${String(spawnError)}`);
|
|
23028
23618
|
}
|
|
23619
|
+
await fileSpawnedSessionUnderProject(runtime, sessionId, env.root);
|
|
23029
23620
|
return agentOk({ sessionId, agent, directory: env.root, depth: env.depth + 1, spawned: spawned + 1, maxSpawn: env.maxSpawn });
|
|
23030
23621
|
}
|
|
23031
23622
|
async function runAgentCommand(runtime, args) {
|
|
@@ -23060,7 +23651,7 @@ async function runAgentCommand(runtime, args) {
|
|
|
23060
23651
|
if (args.handoff) {
|
|
23061
23652
|
return handoffToSibling(runtime, scope, env, args.handoff, args.agent ?? DEFAULT_SPAWN_AGENT);
|
|
23062
23653
|
}
|
|
23063
|
-
return spawnSibling(runtime, env, args.text ?? "", args.agent ?? DEFAULT_SPAWN_AGENT);
|
|
23654
|
+
return spawnSibling(runtime, env, args.text ?? "", args.agent ?? DEFAULT_SPAWN_AGENT, args.files ?? []);
|
|
23064
23655
|
}
|
|
23065
23656
|
if (!args.sessionId) return agentFail("session_not_found", "no session id given");
|
|
23066
23657
|
const target = await resolveTarget(runtime, scope, args.sessionId);
|
|
@@ -23077,10 +23668,19 @@ async function runAgentCommand(runtime, args) {
|
|
|
23077
23668
|
args.text ?? "",
|
|
23078
23669
|
args.wait ? { until: args.until ?? "turn-end", timeoutMs: clampWaitTimeout(args.timeoutMs) } : null
|
|
23079
23670
|
);
|
|
23671
|
+
case "steer":
|
|
23672
|
+
return steerOwnedTarget(runtime, env, target, args.text ?? "");
|
|
23673
|
+
case "stop":
|
|
23674
|
+
return stopOwnedTarget(runtime, env, target);
|
|
23080
23675
|
default:
|
|
23081
23676
|
return agentFail("session_not_found", `unsupported verb "${args.verb}"`);
|
|
23082
23677
|
}
|
|
23083
23678
|
}
|
|
23679
|
+
function parseSpawnRecord(value2) {
|
|
23680
|
+
if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return null;
|
|
23681
|
+
const record2 = value2;
|
|
23682
|
+
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;
|
|
23683
|
+
}
|
|
23084
23684
|
const LEDGER_FILE_MODE = 384;
|
|
23085
23685
|
const PREVIEW_MAX_LENGTH = 200;
|
|
23086
23686
|
function agentLineagePath(homeDir) {
|
|
@@ -23099,7 +23699,8 @@ function readSpawnRecords(homeDir) {
|
|
|
23099
23699
|
if (!line2.trim()) continue;
|
|
23100
23700
|
try {
|
|
23101
23701
|
const parsed = JSON.parse(line2);
|
|
23102
|
-
|
|
23702
|
+
const record2 = parseSpawnRecord(parsed);
|
|
23703
|
+
if (record2) records.push(record2);
|
|
23103
23704
|
} catch {
|
|
23104
23705
|
continue;
|
|
23105
23706
|
}
|
|
@@ -23116,8 +23717,106 @@ function appendSpawnRecord(homeDir, record2) {
|
|
|
23116
23717
|
appendFileSync(path, `${JSON.stringify(entry)}
|
|
23117
23718
|
`, { mode: LEDGER_FILE_MODE });
|
|
23118
23719
|
}
|
|
23720
|
+
function normalize(raw) {
|
|
23721
|
+
const trimmed = raw.trim();
|
|
23722
|
+
if (!trimmed) return "";
|
|
23723
|
+
const stripped = trimmed.replace(/\/+$/, "");
|
|
23724
|
+
return stripped || "/";
|
|
23725
|
+
}
|
|
23726
|
+
function onlyOne(matches) {
|
|
23727
|
+
return matches.length === 1 ? matches[0] : null;
|
|
23728
|
+
}
|
|
23729
|
+
function matchSpawnProject(input) {
|
|
23730
|
+
const machineId = input.machineId.trim();
|
|
23731
|
+
const directory = normalize(input.directory);
|
|
23732
|
+
if (!machineId || !directory) return null;
|
|
23733
|
+
const local = input.projects.filter((project) => project.machineId && project.machineId === machineId);
|
|
23734
|
+
const byWorkspace = local.filter((project) => normalize(project.workspaceDir) === directory);
|
|
23735
|
+
const workspaceHit = onlyOne(byWorkspace);
|
|
23736
|
+
if (workspaceHit) return { projectId: workspaceHit.id };
|
|
23737
|
+
if (byWorkspace.length > 1) return null;
|
|
23738
|
+
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 })));
|
|
23739
|
+
return onlyOne(byWorktree);
|
|
23740
|
+
}
|
|
23741
|
+
function readFileOrNull$1(path) {
|
|
23742
|
+
try {
|
|
23743
|
+
return readFileSync(path, "utf8");
|
|
23744
|
+
} catch {
|
|
23745
|
+
return null;
|
|
23746
|
+
}
|
|
23747
|
+
}
|
|
23748
|
+
async function fileSessionUnderProject(context, env, input) {
|
|
23749
|
+
const machineId = readMachineId(
|
|
23750
|
+
{
|
|
23751
|
+
homeDir: context.homeDir,
|
|
23752
|
+
env,
|
|
23753
|
+
credentialHome: context.credential.source === "saycode" ? null : dirname(context.credential.path)
|
|
23754
|
+
},
|
|
23755
|
+
readFileOrNull$1
|
|
23756
|
+
);
|
|
23757
|
+
if (!machineId) return;
|
|
23758
|
+
const projects = await loadWorkspaceProjects(context);
|
|
23759
|
+
const direct = matchSpawnProject({ machineId, directory: input.directory, projects, worktreesByProjectId: {} });
|
|
23760
|
+
if (direct) {
|
|
23761
|
+
await context.rest.assignSessionToProject(direct.projectId, input.sessionId);
|
|
23762
|
+
return;
|
|
23763
|
+
}
|
|
23764
|
+
const local = projects.filter((project) => project.machineId === machineId);
|
|
23765
|
+
const worktreesByProjectId = {};
|
|
23766
|
+
for (const project of local) {
|
|
23767
|
+
worktreesByProjectId[project.id] = await context.rest.listProjectWorktrees(project.id);
|
|
23768
|
+
}
|
|
23769
|
+
const viaWorktree = matchSpawnProject({ machineId, directory: input.directory, projects, worktreesByProjectId });
|
|
23770
|
+
if (!viaWorktree) return;
|
|
23771
|
+
await context.rest.assignSessionToProject(viaWorktree.projectId, input.sessionId, viaWorktree.worktreeId);
|
|
23772
|
+
}
|
|
23773
|
+
function rejectedSteer(error) {
|
|
23774
|
+
return {
|
|
23775
|
+
accepted: false,
|
|
23776
|
+
reason: /\bin[- ]flight\b/i.test(error) ? "in-flight" : "rejected",
|
|
23777
|
+
error
|
|
23778
|
+
};
|
|
23779
|
+
}
|
|
23780
|
+
async function steerAgentSession(transport, sessionId, text2) {
|
|
23781
|
+
try {
|
|
23782
|
+
const result = await transport.rpc(sessionId, "steer", { text: text2 });
|
|
23783
|
+
if (result?.success === true) return { accepted: true };
|
|
23784
|
+
const error = typeof result?.error === "string" ? result.error : "Session returned an invalid steer acknowledgement.";
|
|
23785
|
+
return rejectedSteer(error);
|
|
23786
|
+
} catch (error) {
|
|
23787
|
+
return rejectedSteer(error instanceof Error ? error.message : String(error));
|
|
23788
|
+
}
|
|
23789
|
+
}
|
|
23790
|
+
async function stopAgentSession(transport, machineId, sessionId) {
|
|
23791
|
+
try {
|
|
23792
|
+
const result = await transport.rpc(machineId, "stop-session", {
|
|
23793
|
+
sessionId,
|
|
23794
|
+
source: "saycode-agent-stop",
|
|
23795
|
+
reason: "user-requested",
|
|
23796
|
+
mode: "force"
|
|
23797
|
+
});
|
|
23798
|
+
if (result?.stopped === true) return { stopped: true };
|
|
23799
|
+
return {
|
|
23800
|
+
stopped: false,
|
|
23801
|
+
error: typeof result?.reason === "string" ? result.reason : "Machine returned an invalid stop acknowledgement."
|
|
23802
|
+
};
|
|
23803
|
+
} catch (error) {
|
|
23804
|
+
return {
|
|
23805
|
+
stopped: false,
|
|
23806
|
+
error: error instanceof Error ? error.message : String(error)
|
|
23807
|
+
};
|
|
23808
|
+
}
|
|
23809
|
+
}
|
|
23119
23810
|
const STREAM_HISTORY_LIMIT = 50;
|
|
23120
23811
|
const SPAWN_RPC_TIMEOUT_MS = 6e4;
|
|
23812
|
+
function isReadableFileSync(path) {
|
|
23813
|
+
try {
|
|
23814
|
+
accessSync(path, constants$1.R_OK);
|
|
23815
|
+
return statSync(path).isFile();
|
|
23816
|
+
} catch {
|
|
23817
|
+
return false;
|
|
23818
|
+
}
|
|
23819
|
+
}
|
|
23121
23820
|
function readFileOrNull(path) {
|
|
23122
23821
|
try {
|
|
23123
23822
|
return readFileSync(path, "utf8");
|
|
@@ -23197,6 +23896,7 @@ function createAgentRuntime(context, transport, env = process.env) {
|
|
|
23197
23896
|
credentialHome,
|
|
23198
23897
|
now: () => Date.now(),
|
|
23199
23898
|
readFile: readFileOrNull,
|
|
23899
|
+
isReadableFile: isReadableFileSync,
|
|
23200
23900
|
listSessions: async () => normalizeSessionRows(await context.rest.listSessions(), keys),
|
|
23201
23901
|
readMessages: (sessionId, limit) => recentContents(context, transport, sessionId, limit),
|
|
23202
23902
|
openStream: (sessionId) => openStream(context, transport, sessionId),
|
|
@@ -23228,7 +23928,13 @@ function createAgentRuntime(context, transport, env = process.env) {
|
|
|
23228
23928
|
},
|
|
23229
23929
|
newAgentId: () => randomBytes(8).toString("hex"),
|
|
23230
23930
|
countSpawns: (agentKey) => countSpawnsFor(homeDir, agentKey),
|
|
23231
|
-
recordSpawn: (record2) => appendSpawnRecord(homeDir, record2)
|
|
23931
|
+
recordSpawn: (record2) => appendSpawnRecord(homeDir, record2),
|
|
23932
|
+
readLineage: () => readSpawnRecords(homeDir),
|
|
23933
|
+
steerSession: (sessionId, text2) => steerAgentSession(transport, sessionId, text2),
|
|
23934
|
+
stopSession: (machineId, sessionId) => stopAgentSession(transport, machineId, sessionId),
|
|
23935
|
+
// Omitted entirely without API access: a Happy `access.key` is sync-only, so
|
|
23936
|
+
// there is no project list to match against and nothing to register with.
|
|
23937
|
+
...context.hasApiAccess ? { registerSpawnedSession: (input) => fileSessionUnderProject(context, env, input) } : {}
|
|
23232
23938
|
};
|
|
23233
23939
|
}
|
|
23234
23940
|
async function contextFor(workspace) {
|