@buzzni/saycode-cli 0.9.10 → 0.9.12

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 CHANGED
@@ -77,6 +77,11 @@ saycode logout
77
77
  | `SAYCODE_SERVER_URL` | 다른 서버 지정 (기본 `https://saycode.ai`) |
78
78
  | `NO_COLOR` / `FORCE_COLOR` | 색 출력 강제 해제/설정 |
79
79
 
80
+ 조직이 Claude 계정을 배정한 PC에서는 새 Claude 대화(`chat`, `agent spawn`)마다 조직 승인이 필요합니다.
81
+ CLI는 `saycode login` 계정으로 그 승인을 자동으로 요청하므로, 로그인한 뒤 조직 워크스페이스를 선택해 두세요
82
+ (`saycode workspaces use <조직>`). Happy `access.key`만으로는 승인을 요청할 수 없어 그 PC가
83
+ `CLAUDE_SETUP_TOKEN_SELECTION_REQUIRED`로 거절하며, CLI가 이 안내를 함께 보여 줍니다.
84
+
80
85
  ## 실시간 연결 문제
81
86
 
82
87
  대화를 열 때 CLI는 `https://saycode.ai/v1/updates`의 WebSocket 연결을 사용합니다. 첫 handshake가
@@ -224,7 +229,7 @@ saycode를 따로 설치하지 않은 작업PC는 `npx @buzzni/saycode-cli searc
224
229
  자기 자신에게 입력할 수 없고, `steer`와 `stop`은 depth와 관계없이 원장에 기록된 자기 후손에게만 보낼 수
225
230
  있습니다. 예외로 사람이 시작한 Claude·Codex 허브(broker 경로, depth 0)는 소유가 검증된 형제 세션(`ls`의
226
231
  `ownership.relation:"own-sibling"`)에도 보낼 수 있으며, 이때 `stop`은 프로세스를 끄지 않고 지금 응답만 끊습니다
227
- (`mode:"turn-abort"`, 터미널에서 쓰는 Claude는 stop을 `controlled-by-user`, steer를 `local-mode`로 거부). 형제 결과에는 대상 `title`과 `ownership.relation`이 붙습니다. 형제에게 보내기 전에 대상 제목과 전체 ID를 확인합니다. 후손이 아닌 세션에 `prompt`하려면 소유가 검증돼야 하며, `not_owned`를 받으면 `read`로만 보고
232
+ (`mode:"turn-abort"`, 터미널에서 쓰는 Claude는 stop을 `controlled-by-user`, steer를 `local-mode`로 거부). 회사 계정 PC에서는 owner relay가 확인한 형제(`ownership.proof:"owner-relay"`)에게 같은 규칙으로 relay를 통해서만 보냅니다(steer=`steer-own-session`, stop=`abort-own-turn`, 기본 꺼짐). 응답 중인지는 상대 Desktop이 보내기 직전에 확인하며, 명령이 읽은 runtime이 바뀌었으면 보내지 않습니다(`stop_rejected`, `runtime-changed`). Claude 대상이 터미널 사용 중인지 확인할 수 없으면 보내지 않습니다(`local-mode-unknown`). relay가 확인하지 못한 결과는 `owner-contact-unavailable`이며 다시 보내거나 credential 경로로 바꾸지 않습니다. 형제 결과에는 대상 `title`과 `ownership.relation`이 붙습니다. 형제에게 보내기 전에 대상 제목과 전체 ID를 확인합니다. 후손이 아닌 세션에 `prompt`하려면 소유가 검증돼야 하며, `not_owned`를 받으면 `read`로만 보고
228
233
  다시 시도하지 않습니다(ADR-078). `stop`은 보관·삭제를 하지 않습니다. spawn 한도(`whoami`의 `maxSpawn`)는 세션이
229
234
  도는 머신의 코어·메모리에서 산정하므로 머신마다 다르고, 최근 6시간 안의 spawn만 세는 롤링 윈도우라
230
235
  소진돼도 시간이 지나면 회복됩니다(`budgetResetsAt`, ADR-063). 데몬이 확인하지 못한 시도도 같은
@@ -526,3 +531,66 @@ T2 응답은 소비한 native turn과 runtime에 결속합니다. `prompt --wait
526
531
  송신 당시 검증된 runtime/epoch는 암호화 요청에 보존하므로 host 재시작 뒤에도 `read --since`로 과거 소비 사실을 조회할 수 있습니다. 이 기록은 새 runtime에 송신 권한을 주지 않습니다. 검증 기록이 없는 구 요청은 현재 runtime이 같고 ON이어도 unknown으로 남습니다. 기록을 소급 생성하거나 과거 소비를 확인됐다고 표시하지 않습니다.
527
532
 
528
533
  Codex 명령/패치 승인은 tool 시작 전에 대기할 수 있지만 .359의 REST pending 상태에는 approval↔turn 결속이 없습니다. `prompt --wait --until waiting-input`은 이 승인을 현재 턴으로 추측하지 않습니다. 해당 대기가 확인되지 않은 채 timeout되면 `outcome:unconfirmed`, `waitingInput:{state:"unsupported",reason:"pending-approval-turn-binding-unavailable"}`를 반환합니다. 이는 pending 승인이 요청 B에 속한다는 뜻이 아닙니다. Claude 질문·Codex 질문은 실제 tool-call ID/턴 결속이 있을 때 판정하며, 채널 routing requestId로 대체하지 않습니다. 미확인 결과를 새 메시지로 자동 재시도하지 마세요.
534
+
535
+ ### Owner relay 개발 경로 (T1c-b2 D2, 기본 off)
536
+
537
+ 호환 host가 제공하는 typed broker IPC 또는 `SAYCODE_OWNER_CONTACT_SOCKET`이 있을 때 새 own lookup/read/prompt를 main으로 보냅니다.
538
+ 기존 후손 권한은 별도 경로로 유지하며 포트가 없으면 기존 동작을 유지합니다. broker agent의 형제 확장은 depth 0만 가능하고,
539
+ 직접 경로와 chat은 host/main의 별도 권한 검사를 받습니다. 소켓 주소는 source/owner 권한 증거가 아닙니다.
540
+
541
+ `agent read <id>`의 새 own 경로는 cursor·snapshot·truncated를 반환합니다. `agent prompt <id> <text> --local-id <id>`는
542
+ read의 live guard 관측을 사용하고 main이 저장 직전에 다시 검증합니다. guard가 없으면 저장하지 않고 기존 경로와 같이 `prompt_unsupported`(`reason:guard-unavailable`, `accepted:false`)입니다.
543
+ `storage-verification-unknown`은 저장이 시도됐을 수 있을 때만 씁니다. relay read 실패나 저장 전 거부는 `owner-contact-unavailable`이며 재조정할 저장이 없고, 직접 주지 않은 localId는 결과에 넣지 않습니다.
544
+ `localid_conflict`·`storage-verification-unknown`이면 같은 localId로 원래 저장을 확인하며 새 ID 자동 재송신을 하지 않습니다.
545
+ 저장 receipt는 runtime 소비 증거가 아닙니다.
546
+
547
+ agent 환경의 `chat -s <id>`는 host socket이 있으면 credential을 읽기 전에 typed adapter로 진입합니다.
548
+ `/detach`·`/exit`로 나갈 수 있고, 첨부·voice는 아직 unavailable입니다. resume/permission/abort는 아래 D3 절을 따릅니다.
549
+ broker 환경에서도 chat은 host의 chat 전용 socket을 사용합니다. depth≥1 broker의 chat lookup 거부는
550
+ `owner-contact-unavailable`로 표시하며 credential 경로로 재시도하지 않습니다. 명시적 localId 재시도에서는 원래 guard를 유지합니다.
551
+ H1/H2 보완 소비는 연결했으며 운영 활성화·공식 artifact 및 실제 host/CLI 소켓 왕복 수락은 별도입니다.
552
+
553
+
554
+ D2 relay의 prompt 거부는 기존 CLI와 같이 `prompt_rejected`, 소비 후 대기 만료는 `wait_timeout`(exit 3)입니다.
555
+ 관측 공백이 있는 응답은 소비 사실을 보존하되 단독 답변·완료를 확정하지 않습니다. 단순 페이지 연속 조회는 공백으로 표시하지 않습니다.
556
+ main은 같은 localId의 저장 행을 먼저 확인합니다. 같은 본문·guard이면 기존 receipt를 돌려주고, 다르면 `localid_conflict`입니다.
557
+ CLI가 read로 기존 행을 확인했을 때만 `storage.existing`을 반환하며, 본문이 같을 때만 `next`로 원래 저장의 read를 안내합니다.
558
+ 조회 불가·행 미발견이면 existing을 추정하지 않습니다. ACK 유실은 우선 저장 조회로 복구하고, 저장이 없다고 확인한 같은 intent는 최대 1회 재시도합니다.
559
+ 대상이 비활성이 되어 live guard가 없어도 같은 `--local-id` 재실행은 저장 행의 원래 guard로 기존 receipt를 확인합니다. 새 ID 송신은 여전히 live guard가 필요하고, 저장 여부를 증명할 수 없으면 `storage-verification-unknown`입니다.
560
+ 저장 뒤 관측(read)이 끊기면 저장 불명확으로 바꾸지 않고 확인된 receipt·소비·`next`를 유지한 채 `observation.state:failed`로 귀속·완료만 낮춥니다. relay read도 기존 read처럼 `structuredOutput`을 실패 없이 보여 줍니다.
561
+ relay의 `read --since <seq>`도 기존 read와 같은 `prompt` 정산 결과를 돌려줍니다. 관측 공백은 귀속을 낮추지만 명시적 거부(`prompt_rejected`)는 공백이 있어도 유지됩니다.
562
+ relay의 `read --last/--around`도 이력 끝까지 확인합니다. 60초 안에 끝을 확인하지 못하면 `read-window-incomplete`로 실패하므로 오래된 구간을 최신으로 쓰지 마세요.
563
+ 구 host의 일반 broker IPC 연결만으로 owner contact를 시도하지 않습니다. socket env가 없는 broker 자식은 broker가 `SAYCODE_OWNER_CONTACT_BROKER_IPC=1`로 지원을 표시했을 때만 broker IPC로 owner contact를 보냅니다. 직접 workspace 원장 후손의 chat은 기존 경로를 먼저 선택합니다.
564
+ owner relay의 형제·부모·chat 입력에는 후손 전용 `delegatedAttendance`를 전달하지 않습니다.
565
+
566
+ ### Owner relay resume·permission·abort (T1c-b2 D3, 기본 off)
567
+
568
+ relay가 자기 외 own으로 확인한 대상만 main의 `resume-own-session`·`reply-own-permission`·`abort-own-turn`으로 보냅니다.
569
+ main은 source/target 소유·세대를 다시 조회하고 RPC 직전에 같은 소유자인지 재확인합니다. self·타인·미확인·scope 밖은 RPC 없이 거부합니다.
570
+ - resume: 관측한 machine/runtime이 그대로인 정지 세션만 기존 machine `resume-happy-session`으로 재개하고, 새 runtime과 같은 소유자를 관측한 뒤에만 accepted입니다.
571
+ socket chat은 그 새 runtime의 live guard를 확인하기 전 입력을 보내지 않습니다(`guard-unavailable`, 미송신).
572
+ - permission: 실제 pending request의 permissionId와 현재 runtime에 결속하며 기존 `permission` RPC 인자(`id/approved/decision/updatedInput/mode`)를 그대로 씁니다.
573
+ 가짜·만료·종결 request는 RPC 없이 거부합니다. socket chat에서는 공지된 request에 `/allow`·`/allow-always`·`/deny [id]`로 답합니다.
574
+ - abort: 세션 `abort` RPC만 씁니다. 머신 `stop-session`으로 바꾸거나 승격하지 않습니다(`agent stop`의 후손 stop과 별개).
575
+ relay abort는 socket chat의 명시 `/stop`만 씁니다. Claude local 대상에도 보내며 remote 전환 효과를 표시합니다. 일반 chat의 공유 abort 포트(중단·즉시 보내기)는 main이 명시 중단과 즉시 보내기를 구분할 수 없어 relay를 쓰지 않으며, own 대상은 기존처럼 `not_owned`입니다(local 여부와 무관).
576
+ - 제어 RPC는 키 조회·암호화 뒤 실제 전송 직전에 요청 수명과 source/target 권위·대상 상태를 다시 확인하고, 실패하면 보내지 않고 `unavailable`입니다.
577
+ - ACK 유실은 `unknown`이며 자동 재송신하지 않습니다. 같은 permission 재응답도 보내지 않으며, 미확정 응답은 request 종결을 관측할 때까지 보존하고 보존 한도가 차면 새 응답을 `answer-capacity`로 거부합니다. resume 복구는 같은 owner authority를 다시 관측할 때만 성공이며 main의 `owner-unconfirmed`는 복구하지 않습니다.
578
+ - 일반 chat의 permission 응답도 relay를 쓸 때는 공지 시점에 같은 request가 대기 중인 runtime에 묶입니다. 묶지 못하면 보내지 않으며 같은 질문을 반복하지 않습니다: 요청이 끝났으면 질문을 멈추고, 바뀌었으면 현재 요청으로 다시 묻고, 입력이 끝났으면 chat을 정상 종료하며, 그 밖에는 한 번 안내한 뒤 다음 세션 갱신 때 다시 묻습니다.
579
+ - permission 공지는 runtime과 요청 내용(tool·arguments·생성 시각)에 묶입니다. 같은 runtime에서 같은 ID의 내용이 바뀌면 이전 공지를 철회하고 다시 공지하며, 응답 직전에 현재 요청이 공지한 내용과 다르면 보내지 않습니다.
580
+ - socket chat과 일반 chat 모두, main이 실행 전에 거부한 응답(`control-in-flight` 등)은 보내지 않은 것으로 보고 다시 답할 수 있습니다(socket chat은 공지 복구, 일반 chat은 같은 질문 재질문). 전달 불명 응답만 다시 보내지 않습니다.
581
+ - socket chat의 permission 공지는 공지 당시 runtime에 묶이고 그 runtime으로만 답합니다. runtime이 바뀌면 공지를 철회하고 새 요청을 다시 공지합니다. 전달 확인(`delivered:true`)일 때만 `Answered.`를 표시합니다.
582
+ owner port가 없거나 relay가 own을 증명하지 못하면 기존 credential 경로와 verb gate를 그대로 씁니다(새 IPC 0회). relay 결과로 credential RPC를 허가하지 않습니다.
583
+
584
+ ### Owner relay owned child (T1c-b2 D4, 기본 off)
585
+
586
+ owner contact가 있고 자기 세션의 lookup 행이 `self` + `confirmed-owner`일 때만 `agent spawn`·`spawn --handoff`가
587
+ main의 `spawn-owned-child`로 자식을 만듭니다. 포트가 없거나 개인 계정·미확인 source, `--output-schema`, claude/codex 밖의
588
+ agent, 부모 scope와 다른 directory(`--worktree`·worktree 안의 root)는 이 판정 단계에서 기존 daemon spawn을 그대로 씁니다. broker 하위(depth≥1) 자식은 host가 자기 세션 하나만의 lookup을 허용할 때(형제·부모 ID가 섞인 lookup은 계속 host가 거부) 같은 규칙으로 owned 손자를 만들고, 그 lookup을 거부하는 구 broker host에서는 기존 daemon spawn입니다. Desktop의 owner relay 전환이 꺼져 있으면 host contact가 있어도 self lookup을 하지 않습니다. owned 경로를 시작한 뒤에는 어떤 실패도 무서명 spawn으로 바꾸지 않습니다.
589
+ 예산 예약(기존 6시간 차감, 환급 없음)과 `owned-spawns/<operationId>.json` 기록·재읽기가 먼저 성공해야 main을 부릅니다.
590
+ ACK 유실·pending은 같은 operation의 `owned-child-status`만 2~10초 간격 최대 120회 조회합니다. 620초 deadline은 명령 시작부터 세며 각 contact 호출도 남은 시간 안에 끊습니다.
591
+ 서명된 started receipt(session·binding·provider runtime)를 같은 예약에 대조한 뒤에만 lineage를 기록하고 결과에 `ownership`을 붙입니다.
592
+ 끝까지 확인되지 않으면 `spawn_pending`(state·stage·observedSessionId·`next`)입니다. `spawn_failed`는 보고된 실패이거나, main이 생성 단계 전 거부임을 표시한(`reason`이 `before-creation:`으로 시작) 경우뿐입니다.
593
+ `agent spawn --resume-op <operationId>`(`next.command`)는 CLI가 끝난 뒤에도 같은 operation의 status만 다시 조회해 정산합니다.
594
+ 기록(`owned-spawns/<operationId>.json`)이 이 세션·scope·예산 key의 것이고 원장에 그 예약이 있을 때만 진행하며, 새 spawn·새 예약·환급은 없습니다.
595
+ started면 원래 spawnId로 lineage를 기록해 `resumedOperation`과 함께 반환하고, 아직이거나 status를 조회할 수 없으면 다시 `spawn_pending`입니다(보고된 실패만 `spawn_failed`). 기록이 없거나 다른 source·scope면 `not_owned`,
596
+ owner contact가 없으면 `owner-contact-unavailable`, 재개 명령이 초기화(인증·연결)조차 못 하면 같은 operation의 `spawn_pending`(`next` 유지)입니다. 다른 spawn 옵션과 함께 쓸 수 없습니다.
@@ -262,7 +262,7 @@ function path(value) {
262
262
  }
263
263
  function parseManagedLocalTool(value, extensionId2) {
264
264
  const root = row(value);
265
- keys(root, ["version", "id", "toolVersion", "platforms", "operations"]);
265
+ keys(root, ["version", "id", "toolVersion", "platforms", "operations", "background"]);
266
266
  if (root.version !== 1 || !ID.test(text$2(root.id)) || !String(root.id).startsWith(extensionId2 + ".") || !/^\d+\.\d+\.\d+$/.test(text$2(root.toolVersion))) fail$4();
267
267
  const platforms = {};
268
268
  for (const [platform, raw] of Object.entries(row(root.platforms))) {
@@ -319,7 +319,23 @@ function parseManagedLocalTool(value, extensionId2) {
319
319
  operations[name] = operation;
320
320
  }
321
321
  if (!Object.keys(platforms).length || !Object.keys(operations).length || Object.keys(operations).length > 32) fail$4();
322
- return { version: 1, id: String(root.id), toolVersion: String(root.toolVersion), platforms, operations };
322
+ const background = root.background === void 0 ? void 0 : parseBackground(root.background, platforms, operations);
323
+ return { version: 1, id: String(root.id), toolVersion: String(root.toolVersion), platforms, operations, ...background ? { background } : {} };
324
+ }
325
+ function parseBackground(value, platforms, operations) {
326
+ const source = row(value);
327
+ keys(source, ["argv", "stopOperation"]);
328
+ if (!Object.keys(platforms).some((platform) => platform.startsWith("win32-") || platform.startsWith("darwin-"))) fail$4();
329
+ if (!Array.isArray(source.argv) || !source.argv.length || source.argv.length > 8) fail$4();
330
+ const argv = source.argv.map((arg) => {
331
+ const value2 = text$2(arg);
332
+ if (value2.includes("{{")) fail$4();
333
+ return value2;
334
+ });
335
+ if (source.stopOperation === void 0) return { argv };
336
+ const stop = operations[text$2(source.stopOperation)];
337
+ if (!stop || Object.keys(stop.inputs ?? {}).length || stop.payload !== void 0) fail$4();
338
+ return { argv, stopOperation: String(source.stopOperation) };
323
339
  }
324
340
  function managedToolArguments(operation, parameters, session) {
325
341
  const inputs = operation.inputs ?? {};
@@ -647,7 +663,7 @@ const CONVERSATION_MODS_INTERNAL = typeof __SAYCODE_MODS_INTERNAL__ !== "undefin
647
663
  const SAYCODE_EXTENSION_API_VERSION = CONVERSATION_MODS_INTERNAL ? 5 : 4;
648
664
  const SAYCODE_EXTENSION_MINIMUM_API_VERSION = CONVERSATION_MODS_INTERNAL ? 4 : 2;
649
665
  const MAX_EXTENSION_API_WINDOW_WIDTH = 2;
650
- const SAYCODE_EXTENSION_ENGINE_VERSION = "1.2.0";
666
+ const SAYCODE_EXTENSION_ENGINE_VERSION = "1.3.0";
651
667
  const SAYCODE_CHANNEL_API_VERSION = CHANNEL_API_VERSION;
652
668
  const SAYCODE_CHANNEL_MINIMUM_API_VERSION = CHANNEL_MINIMUM_API_VERSION;
653
669
  class UnsupportedChannelApiVersionError extends Error {