shennian 0.3.85 → 0.3.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/bin/shennian.js +1 -1
  2. package/dist/publish-build-manifest.json +28 -28
  3. package/dist/src/agents/adapter.js +19 -1
  4. package/dist/src/agents/claude-managed-permissions.js +248 -1
  5. package/dist/src/agents/claude.js +393 -8
  6. package/dist/src/agents/codex-managed-permissions.js +294 -1
  7. package/dist/src/agents/codex-managed-room-tools.js +249 -1
  8. package/dist/src/agents/codex-utils.js +200 -7
  9. package/dist/src/agents/codex.js +1352 -17
  10. package/dist/src/agents/config-status.js +226 -1
  11. package/dist/src/agents/cursor-managed-permissions.js +299 -2
  12. package/dist/src/agents/cursor.js +314 -1
  13. package/dist/src/agents/custom.js +418 -4
  14. package/dist/src/agents/external-channel-instructions.js +94 -11
  15. package/dist/src/agents/gemini.js +180 -1
  16. package/dist/src/agents/manager.d.ts +2 -0
  17. package/dist/src/agents/manager.js +250 -18
  18. package/dist/src/agents/model-registry/cache.js +37 -1
  19. package/dist/src/agents/model-registry/discovery.js +188 -2
  20. package/dist/src/agents/model-registry/parsers.js +521 -4
  21. package/dist/src/agents/model-registry/runner.js +30 -1
  22. package/dist/src/agents/model-registry/service.js +115 -1
  23. package/dist/src/agents/model-registry/types.js +8 -1
  24. package/dist/src/agents/model-registry.js +19 -1
  25. package/dist/src/agents/native-capabilities.js +237 -1
  26. package/dist/src/agents/openclaw.js +293 -2
  27. package/dist/src/agents/opencode-managed-permissions.js +187 -2
  28. package/dist/src/agents/opencode.js +298 -1
  29. package/dist/src/agents/platform-instructions.js +46 -17
  30. package/dist/src/agents/runtime-compatibility.js +44 -1
  31. package/dist/src/agents/workbuddy-managed-permissions.js +329 -2
  32. package/dist/src/agents/workbuddy.js +444 -3
  33. package/dist/src/commands/room.js +2 -2
  34. package/dist/src/fs/boundary.js +185 -1
  35. package/dist/src/fs/handler.js +130 -1
  36. package/dist/src/fs/security.js +32 -1
  37. package/dist/src/fs/text-decoder.js +110 -1
  38. package/dist/src/index.js +587 -3
  39. package/dist/src/manager/prompt.js +34 -29
  40. package/dist/src/manager/registry.js +317 -1
  41. package/dist/src/manager/retired-external-channel-compat.js +40 -1
  42. package/dist/src/manager/runtime.d.ts +4 -0
  43. package/dist/src/manager/runtime.js +1514 -23
  44. package/dist/src/native-fusion/codex-parser.js +660 -4
  45. package/dist/src/native-fusion/codex-user-input.js +102 -5
  46. package/dist/src/native-fusion/config.js +5 -1
  47. package/dist/src/native-fusion/file-watcher.js +191 -1
  48. package/dist/src/native-fusion/local-repository.js +569 -2
  49. package/dist/src/native-fusion/opencode-parser.js +130 -3
  50. package/dist/src/native-fusion/parsers.js +140 -3
  51. package/dist/src/native-fusion/service.js +739 -2
  52. package/dist/src/native-fusion/session-header.js +101 -2
  53. package/dist/src/native-fusion/state.js +104 -2
  54. package/dist/src/native-fusion/types.js +1 -1
  55. package/dist/src/personal-sync/machine-authority.js +34 -1
  56. package/dist/src/room/activation-audit.js +45 -2
  57. package/dist/src/room/activation-policy.js +30 -1
  58. package/dist/src/room/activation-service.d.ts +1 -0
  59. package/dist/src/room/activation-service.js +408 -8
  60. package/dist/src/room/invite-bootstrap.js +156 -1
  61. package/dist/src/room/managed-context.d.ts +1 -1
  62. package/dist/src/room/managed-context.js +1 -1
  63. package/dist/src/room/managed-prompt-composer.d.ts +1 -1
  64. package/dist/src/room/managed-prompt-composer.js +6 -5
  65. package/dist/src/room/managed-room-binding.js +167 -1
  66. package/dist/src/room/managed-room-ipc.d.ts +2 -2
  67. package/dist/src/room/managed-room-ipc.js +2 -2
  68. package/dist/src/room/mirror-sync-service.js +183 -1
  69. package/dist/src/room/room-action-client.d.ts +11 -0
  70. package/dist/src/room/room-action-client.js +1 -1
  71. package/dist/src/room/room-binding-client.d.ts +1 -0
  72. package/dist/src/room/room-binding-client.js +151 -1
  73. package/dist/src/security/managed-agent-policy.d.ts +3 -2
  74. package/dist/src/security/managed-agent-policy.js +1 -1
  75. package/dist/src/session/archive-zip.js +220 -1
  76. package/dist/src/session/deletion-sync-state.js +136 -2
  77. package/dist/src/session/handlers/agent-capabilities.js +14 -1
  78. package/dist/src/session/handlers/agent-config.js +150 -1
  79. package/dist/src/session/handlers/agents.js +55 -1
  80. package/dist/src/session/handlers/chat.d.ts +1 -1
  81. package/dist/src/session/handlers/chat.js +1030 -2
  82. package/dist/src/session/handlers/control.js +61 -1
  83. package/dist/src/session/handlers/fs.js +594 -1
  84. package/dist/src/session/handlers/message-status.js +15 -1
  85. package/dist/src/session/handlers/remote-access.js +43 -1
  86. package/dist/src/session/handlers/room-managed.js +101 -1
  87. package/dist/src/session/handlers/session-refresh.js +35 -1
  88. package/dist/src/session/handlers/title.js +79 -1
  89. package/dist/src/session/handlers/tool-detail.js +149 -1
  90. package/dist/src/session/handlers/voice-window.js +62 -1
  91. package/dist/src/session/history-subscriptions.js +470 -1
  92. package/dist/src/session/manager.js +658 -1
  93. package/dist/src/session/native-cleanup-outbox.js +287 -2
  94. package/dist/src/session/native-session-lifecycle.js +327 -6
  95. package/dist/src/session/queue.d.ts +5 -0
  96. package/dist/src/session/queue.js +8 -8
  97. package/dist/src/upgrade/scheduler.js +126 -1
  98. package/node_modules/@shennian/wire/dist/agent-workspace.js +1 -1
  99. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -1,3 +1,587 @@
1
- import{Command as ie}from"commander";import I from"chalk";import ae from"node:os";import _ from"node:fs";import{loadConfig as d,saveConfig as ce,getConfigPath as me,getShennianDir as le,resolveShennianPath as J,normalizeAutoUpgradePolicy as de}from"./config/index.js";import{CliRelayClient as pe}from"./relay/client.js";import{collectKnownLogSecrets as K,installCliLogRedaction as ge,sanitizeCliLogValue as ue}from"./log-redaction.js";import{registerPairCommand as he,runSmartStart as fe}from"./commands/pair.js";import{clearDaemonPidIfOwner as j,createDaemonInstanceId as Se,clearDaemonLauncher as W,findRunningDaemonProcessIds as ve,isRemoteAccessDisabled as Y,prepareRemoteAccessPause as Ie,stopPreparedRemoteAccessService as we,isIsolatedDaemonE2eInstance as ye,registerDaemonCommand as Re,writeDaemonPid as q,writeDaemonLauncher as Q}from"./commands/daemon.js";import{registerRuntimeCommand as Ce}from"./commands/runtime.js";import{registerManagerCommand as ke}from"./commands/manager.js";import{registerUpgradeCommand as Ee}from"./commands/upgrade.js";import{registerToolsCommand as De}from"./commands/tools.js";import{registerIntegrationCommand as Te}from"./commands/integration.js";import{registerRoomCommand as Ae}from"./commands/room.js";import{SessionManager as be}from"./session/manager.js";import{SERVERS as M,regionToUrl as Oe,urlToRegion as Ue}from"./region.js";import{getCurrentVersion as X,handleStartupCrashCheck as Ne}from"./upgrade/engine.js";import{AutoUpgradeScheduler as Pe}from"./upgrade/scheduler.js";import{detectAgents as $e}from"./agents/detect.js";import{augmentProcessPath as Z}from"./env-path.js";Z();const ee=X();import{getCachedAgentInfos as _e,resolveAgentInfos as Me}from"./agents/model-registry.js";import{initCliLogReporter as xe,reportLog as w}from"./log-reporter.js";import{isNativeFusionEnabled as Le}from"./native-fusion/config.js";import{NativeSessionFusionService as Ve}from"./native-fusion/service.js";import{startDaemonLogRetention as Be}from"./daemon-log.js";import{startDaemonIpcServer as Ge}from"./daemon-ipc/server.js";import{resolveManagedRoomIpcPaths as He}from"./daemon-ipc/paths.js";import{RoomSessionRegistry as Fe}from"./room/session-registry.js";import{HttpRoomBindingClient as ze}from"./room/room-binding-client.js";import{TrustedManagedRoomBinder as Je}from"./room/managed-room-binding.js";import{RoomHostSessionObservationStore as Ke}from"./room/host-session-observations.js";import{RoomMirrorSyncService as je}from"./room/mirror-sync-service.js";import{RoomActivationService as We}from"./room/activation-service.js";import{RoomActivationAudit as Ye}from"./room/activation-audit.js";import{RoomAttachmentCache as qe}from"./room/attachment-cache.js";import{RoomAttachmentUploader as Qe}from"./room/attachment-upload.js";import{ManagedRoomContextAuthority as Xe}from"./room/managed-context.js";import{createManagedRoomExecutor as Ze}from"./room/managed-room-ipc.js";import{SESSION_SYNC_PROTOCOL_VERSION as eo,SESSION_SYNC_ADVERTISED_CAPABILITIES as oo}from"@shennian/wire";import{getDaemonInstallationId as no}from"./session/store.js";import{synchronizeMachinePersonalAuthority as to}from"./personal-sync/machine-authority.js";import{CLI_SESSION_SYNC_DATA_EPOCH as ro,ensurePersonalDataEpoch as so}from"./personal-sync/epoch.js";import{DeletionSyncRetryWaitError as io,DeletionSyncTerminalError as ao}from"./session/deletion-sync-state.js";const oe=le(),co=J("daemon.pid");function mo(){try{const o=_.readFileSync(co,"utf-8").trim();if(o.startsWith("{")){const a=JSON.parse(o),m=Number(a.pid);return Number.isInteger(m)&&m>0?m:null}const t=parseInt(o,10);return Number.isInteger(t)&&t>0?t:null}catch{return null}}function lo(o){return o.replace(/^https:\/\//,"wss://").replace(/^http:\/\//,"ws://")}function x(){process.stdout.write(`${JSON.stringify({ok:!1,error:"integration_command_retired",message:"Room MCP and Hooks were retired. Use the Shennian Skill and CLI.",nextAction:{type:"open_install_guide",url:"https://shennian.net/install.md"}})}
2
- `),process.exitCode=1}function po(o){const t=[`trigger=${o.trigger??"unknown"}`,`phase=${o.phase}`,`code=${o.code??"unknown"}`,`attempt=${o.reconnectAttempt}`];return o.reason&&t.push(`reason=${o.reason}`),o.error&&t.push(`error=${o.error}`),t.join(" ")}async function ne(o,t=5e3){const a=Date.now();for(;Date.now()-a<t;){try{process.kill(o,0)}catch{return!0}await new Promise(m=>setTimeout(m,100))}try{return process.kill(o,0),!1}catch{return!0}}const r=new ie;r.name("shennian").description("Shennian \u2014 AI Agent Control Plane").version(ee),r.enablePositionalOptions(),Te(r),Ae(r),r.option("--api <url>","Server URL override").option("--name <name>","Machine name",ae.hostname()).action(async o=>{const t=d(),a=o.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??t.serverUrl??void 0;await fe(a,o.name)}),r.command("run-service",{hidden:!0}).description("(internal) Connect to relay server, called by the background service").option("--api <url>","Server URL override").action(async o=>{const t=J("env.json");try{const e=JSON.parse(_.readFileSync(t,"utf-8"));for(const[s,n]of Object.entries(e))process.env[s]||(process.env[s]=n);Z()}catch{}ge(K(process.env,d()));const a=Be();Y()&&(console.log(`[${new Date().toISOString()}] remote access disabled, service start skipped`),clearInterval(a),process.exit(0));const m=!!(process.env.INVOCATION_ID||process.env.JOURNAL_STREAM||process.env.SHENNIAN_DESKTOP_SERVER_URL),L=async(e,s)=>{console.log(`[${new Date().toISOString()}] ${s} (PID ${e})`),process.kill(e,"SIGTERM"),await ne(e)||(process.kill(e,"SIGKILL"),await ne(e,2e3))};try{const e=mo();if(e&&e!==process.pid)try{process.kill(e,0),m?await L(e,"managed start taking over from existing daemon"):(console.log(`[${new Date().toISOString()}] daemon already running (PID ${e}), skipping duplicate start`),process.exit(0))}catch{}}catch{}const b=ye(process.env,oe)?[]:ve(process.pid);if(b.length>0)if(m)for(const e of b)try{await L(e,"managed start taking over from orphan daemon")}catch{}else console.log(`[${new Date().toISOString()}] daemon already running (PID ${b[0]}), skipping duplicate start`),process.exit(0);const p=Se();q(process.pid,p,{version:ee}),Q(process.pid,void 0,p),process.on("exit",()=>{j(process.pid,p),W(p)}),await Ne()&&(console.log(`[${new Date().toISOString()}] Rolled back to previous version, restarting...`),process.exit(0));try{so()}catch(e){console.error(`[personal-sync] data epoch initialization failed: ${e instanceof Error?e.message:String(e)}`),process.exitCode=1;return}const c=d(),u=o.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??c.serverUrl??M.cn.url,g=X(),O=$e(),S=O.map(e=>e.type),U=_e(O),y=new Fe,R=new Ke,N=new Xe(y),h=new ze(u),re=new qe({actionClient:h}),se=new Qe({actionClient:h});let P=null;const V=Ze({authority:N,actionClient:h,attachmentCache:re,attachmentUploader:se,observations:R,getMachineCredentials:()=>({machineToken:d().machineToken})}),C=await Ge({paths:He(),handlers:{"managed-room.execute":e=>V(e)}});let B=!1;const k=()=>{B||(B=!0,C.close().finally(()=>process.exit(0)))};if(process.once("SIGINT",k),process.once("SIGTERM",k),!c.machineToken){console.error('Shennian CLI is not paired. Run "shennian pair --browser" or "shennian pair --token" first.'),await C.close(),process.exitCode=1;return}process.removeListener("SIGINT",k),process.removeListener("SIGTERM",k);const G=`${lo(u)}/relay/machine`,$=c.machineToken;if(!$)return;try{await to({serverUrl:u,machineToken:$,machineId:c.machineId})}catch(e){console.error(`[personal-sync] machine authority verification failed: ${e instanceof Error?e.message:String(e)}`),await C.close(),process.exitCode=1;return}c.machineId&&xe(u,c.machineId),console.log(`[${new Date().toISOString()}] Connecting to ${G}... (v${g}) agents: ${S.join(",")}`),w({level:"info",wsEvent:"daemon.start",metadata:{version:g,agents:S}});let E=null,D=null,T=0;const H=de(c.autoUpgrade),l=new pe({serverUrl:G,machineToken:$,cliVersion:g,agentList:S,autoUpgradePolicy:H,sessionSyncProtocolVersion:eo,sessionSyncCapabilities:oo,daemonInstallationId:no(),dataEpoch:ro,onConnected:()=>{console.log(`[${new Date().toISOString()}] \u2713 Connected`),w({level:"info",wsEvent:"daemon.connected"}),U.some(n=>n.models.length>0)&&l.sendEvent({type:"event",event:"machine.agents",payload:{agentList:S,agents:U}}),Me(O,{serverUrl:c.serverUrl??u,authToken:c.machineToken??c.accessToken}).then(n=>{JSON.stringify(n)!==JSON.stringify(U)&&l.sendEvent({type:"event",event:"machine.agents",payload:{agentList:S,agents:n}})}).catch(()=>{}),import("./upgrade/engine.js").then(({readUpgradeAttempt:n,clearUpgradeAttempt:i})=>{const v=n();i(),v&&(console.log(`[${new Date().toISOString()}] [upgrade] Reporting success: ${v.from} \u2192 ${v.to}`),l.sendEvent({type:"event",event:"upgrade",payload:{state:"success",machineId:"self",from:v.from,to:v.to}}))}).catch(()=>{}),A.start(),D?.start();const e=++T,s=async()=>{try{await f.synchronizeSessionDeletions(),e===T&&l.getState()==="connected"&&E?.handleConnected()}catch(n){if(console.error("[session.deletions] startup catch-up failed",n),e===T&&l.getState()==="connected"){if(n instanceof ao)return;const i=n instanceof io?Math.max(1e3,n.retryAfterMs):5e3;setTimeout(()=>{s()},i).unref?.()}}};s()},onDisconnected:e=>{T+=1,D?.stop(),console.log(`[${new Date().toISOString()}] \u26A0 Disconnected, reconnecting... ${po(e)}`),w({level:"warn",wsEvent:"daemon.disconnected",metadata:{code:e.code,reason:e.reason,error:e.error,phase:e.phase,trigger:e.trigger,reconnectAttempt:e.reconnectAttempt}})},onReq:e=>{console.log(`[${new Date().toISOString()}] [req] ${e.method}`),w({level:"info",type:"ws",wsEvent:e.method,wsDirection:"in",traceId:e.traceId}),f.handleReq(e)}});E=Le()?new Ve(l):null,D=new je({registry:y,observations:R,actionClient:h,getMachineCredentials:()=>({machineToken:d().machineToken}),onMessages:async(e,s,n,i)=>{await P?.processMessages(e,s,n,i)}});const A=new Pe({currentVersion:g,machineId:c.machineId??"unknown-machine",serverUrl:c.serverUrl??u,getIdleState:()=>f.getUpgradeIdleState(),startUpgrade:async e=>{const{handleUpgradeStart:s}=await import("./commands/upgrade.js");await s(l,"auto-upgrade",e,{currentVersion:g})}});A.setPolicy(H);const f=new be(l,E,g,A,new Je({registry:y,observations:R,bindingClient:h,getMachineCredentials:()=>{const e=d();return{machineId:e.machineId,machineToken:e.machineToken}},hostProcessIdentity:()=>`daemon:${p}`}),{prepare:()=>Ie(),isDisabled:()=>Y(),shutdownAfterResponse:()=>{setTimeout(()=>{we(),process.kill(process.pid,"SIGTERM")},100).unref?.()}},e=>N.revokeToken(e),V);P=new We({registry:y,observations:R,enqueueConversationBatch:e=>f.enqueueManagedRoomBatch(e),loadContext:async e=>{const s=d().machineToken?.trim(),n=e.binding;if(!s||!n)throw new Error("managed_room_context_unavailable");const i=await h.status({machineToken:s,roomId:n.roomId,bindingId:n.bindingId});return{roomName:i.room.name,memberCount:i.room.memberCount,members:i.room.members,agentsByIdentityId:i.agent?{[i.agentIdentityId]:i.agent}:{},agentSystemPromptsByIdentityId:i.agent?{[i.agentIdentityId]:i.agent.systemPrompt??""}:{}}},issueManagedRoomContext:(e,s)=>{const n=d().machineToken;if(!n)throw new Error("machine_not_authorized");return N.issue({registryKey:e,machineCredential:n,activationCauseMessageId:s})},audit:new Ye}),f.setManagedRoomTurnPreparer(e=>P.prepareConversationTurn(e)),_.mkdirSync(oe,{recursive:!0}),q(process.pid,p,{version:g}),Q(process.pid,void 0,p),l.connect(),process.stdin.resume();let F=!1;const z=async()=>{if(!F){F=!0,console.log(I.gray(`
3
- Disconnecting...`)),w({level:"info",wsEvent:"daemon.stop"}),clearInterval(a);try{await f.cleanup(),A.stop(),E?.stop(),D?.stop(),l.disconnect(),await C.close(),j(process.pid,p),W(p)}finally{process.exit(0)}}};process.on("SIGINT",()=>{z()}),process.on("SIGTERM",()=>{z()})}),r.command("workbuddy-session-hook",{hidden:!0}).description("(retired) WorkBuddy Room Hook").allowUnknownOption(!0).action(()=>x()),r.command("room-session-hook",{hidden:!0}).description("(internal) Authorize the official Room tool for one trusted Agent Session").argument("<runtime>","Trusted runtime adapter").allowUnknownOption(!0).action(()=>x()),r.command("room-tool-server",{hidden:!0}).description("(internal) Run the official Shennian Room MCP tool").allowUnknownOption(!0).action(()=>x());const te=r.command("config").description("View or modify configuration");te.command("show",{isDefault:!0}).description("Show current configuration").action(()=>{console.log(`Config file: ${me()}`);const o=d(),t=ue(o,K(process.env,o));if(o.serverUrl){const a=Ue(o.serverUrl);console.log(`Region: ${a} (${M[a].label})`)}console.log(JSON.stringify(t,null,2))}),te.command("set").description("Update a config value").argument("<key>",'Config key (e.g. "server")').argument("<value>",'Config value (e.g. "cn" or "global")').action((o,t)=>{const a=d();if(o==="server"){t!=="cn"&&t!=="global"&&(console.error(I.red('\u2717 Value must be "cn" or "global"')),process.exit(1));const m=t;a.serverUrl=Oe(m),ce(a),console.log(I.green(`\u2713 Server set to ${M[m].label}`)),console.log(I.yellow(" Restart the background service for this to take effect: shennian stop && shennian start"));return}console.error(I.red(`\u2717 Unknown config key: ${o}. Supported: server`)),process.exit(1)}),he(r),Re(r),Ce(r),ke(r),Ee(r),De(r),r.parse();
1
+ // @arch docs/architecture/cli/daemon.md
2
+ // @test src/__tests__/agents-e2e.ts
3
+ // @test src/__tests__/daemon-autostart.test.ts
4
+ import { Command } from 'commander';
5
+ import chalk from 'chalk';
6
+ import os from 'node:os';
7
+ import fs from 'node:fs';
8
+ import { loadConfig, saveConfig, getConfigPath, getShennianDir, resolveShennianPath, normalizeAutoUpgradePolicy, } from './config/index.js';
9
+ import { CliRelayClient } from './relay/client.js';
10
+ import { collectKnownLogSecrets, installCliLogRedaction, sanitizeCliLogValue, } from './log-redaction.js';
11
+ import { registerPairCommand, runSmartStart } from './commands/pair.js';
12
+ import { clearDaemonPidIfOwner, createDaemonInstanceId, clearDaemonLauncher, findRunningDaemonProcessIds, isRemoteAccessDisabled, prepareRemoteAccessPause, stopPreparedRemoteAccessService, isIsolatedDaemonE2eInstance, registerDaemonCommand, writeDaemonPid, writeDaemonLauncher, } from './commands/daemon.js';
13
+ import { registerRuntimeCommand } from './commands/runtime.js';
14
+ import { registerManagerCommand } from './commands/manager.js';
15
+ import { registerUpgradeCommand } from './commands/upgrade.js';
16
+ import { registerToolsCommand } from './commands/tools.js';
17
+ import { registerIntegrationCommand } from './commands/integration.js';
18
+ import { registerRoomCommand } from './commands/room.js';
19
+ import { SessionManager } from './session/manager.js';
20
+ import { SERVERS, regionToUrl, urlToRegion } from './region.js';
21
+ import { getCurrentVersion, handleStartupCrashCheck } from './upgrade/engine.js';
22
+ import { AutoUpgradeScheduler } from './upgrade/scheduler.js';
23
+ import { detectAgents } from './agents/detect.js';
24
+ import { augmentProcessPath } from './env-path.js';
25
+ augmentProcessPath();
26
+ const cliVersion = getCurrentVersion();
27
+ import { getCachedAgentInfos, resolveAgentInfos } from './agents/model-registry.js';
28
+ import { initCliLogReporter, reportLog } from './log-reporter.js';
29
+ import { isNativeFusionEnabled } from './native-fusion/config.js';
30
+ import { NativeSessionFusionService } from './native-fusion/service.js';
31
+ import { startDaemonLogRetention } from './daemon-log.js';
32
+ import { startDaemonIpcServer } from './daemon-ipc/server.js';
33
+ import { resolveManagedRoomIpcPaths } from './daemon-ipc/paths.js';
34
+ import { RoomSessionRegistry } from './room/session-registry.js';
35
+ import { HttpRoomBindingClient } from './room/room-binding-client.js';
36
+ import { TrustedManagedRoomBinder } from './room/managed-room-binding.js';
37
+ import { RoomHostSessionObservationStore } from './room/host-session-observations.js';
38
+ import { RoomMirrorSyncService } from './room/mirror-sync-service.js';
39
+ import { RoomActivationService } from './room/activation-service.js';
40
+ import { RoomActivationAudit } from './room/activation-audit.js';
41
+ import { RoomAttachmentCache } from './room/attachment-cache.js';
42
+ import { RoomAttachmentUploader } from './room/attachment-upload.js';
43
+ import { ManagedRoomContextAuthority } from './room/managed-context.js';
44
+ import { createManagedRoomExecutor } from './room/managed-room-ipc.js';
45
+ import { SESSION_SYNC_PROTOCOL_VERSION, SESSION_SYNC_ADVERTISED_CAPABILITIES } from '@shennian/wire';
46
+ import { getDaemonInstallationId } from './session/store.js';
47
+ import { synchronizeMachinePersonalAuthority } from './personal-sync/machine-authority.js';
48
+ import { CLI_SESSION_SYNC_DATA_EPOCH, ensurePersonalDataEpoch } from './personal-sync/epoch.js';
49
+ import { DeletionSyncRetryWaitError, DeletionSyncTerminalError, } from './session/deletion-sync-state.js';
50
+ const SHENNIAN_DIR = getShennianDir();
51
+ const PID_FILE = resolveShennianPath('daemon.pid');
52
+ function readDaemonPidFile() {
53
+ try {
54
+ const raw = fs.readFileSync(PID_FILE, 'utf-8').trim();
55
+ if (raw.startsWith('{')) {
56
+ const parsed = JSON.parse(raw);
57
+ const pid = Number(parsed.pid);
58
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
59
+ }
60
+ const pid = parseInt(raw, 10);
61
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ function httpToWs(url) {
68
+ return url.replace(/^https:\/\//, 'wss://').replace(/^http:\/\//, 'ws://');
69
+ }
70
+ function writeRetiredIntegrationError() {
71
+ process.stdout.write(`${JSON.stringify({
72
+ ok: false,
73
+ error: 'integration_command_retired',
74
+ message: 'Room MCP and Hooks were retired. Use the Shennian Skill and CLI.',
75
+ nextAction: { type: 'open_install_guide', url: 'https://shennian.net/install.md' },
76
+ })}\n`);
77
+ process.exitCode = 1;
78
+ }
79
+ function formatDisconnectInfo(info) {
80
+ const parts = [
81
+ `trigger=${info.trigger ?? 'unknown'}`,
82
+ `phase=${info.phase}`,
83
+ `code=${info.code ?? 'unknown'}`,
84
+ `attempt=${info.reconnectAttempt}`,
85
+ ];
86
+ if (info.reason)
87
+ parts.push(`reason=${info.reason}`);
88
+ if (info.error)
89
+ parts.push(`error=${info.error}`);
90
+ return parts.join(' ');
91
+ }
92
+ async function waitForPidExit(pid, timeoutMs = 5000) {
93
+ const startedAt = Date.now();
94
+ while (Date.now() - startedAt < timeoutMs) {
95
+ try {
96
+ process.kill(pid, 0);
97
+ }
98
+ catch {
99
+ return true;
100
+ }
101
+ await new Promise((resolve) => setTimeout(resolve, 100));
102
+ }
103
+ try {
104
+ process.kill(pid, 0);
105
+ return false;
106
+ }
107
+ catch {
108
+ return true;
109
+ }
110
+ }
111
+ const program = new Command();
112
+ program.name('shennian').description('Shennian — AI Agent Control Plane').version(cliVersion);
113
+ // Keep options written after a subcommand scoped to that subcommand. In particular,
114
+ // `room create --name` must not be consumed by the legacy root machine-name option.
115
+ program.enablePositionalOptions();
116
+ registerIntegrationCommand(program);
117
+ registerRoomCommand(program);
118
+ program
119
+ .option('--api <url>', 'Server URL override')
120
+ .option('--name <name>', 'Machine name', os.hostname())
121
+ .action(async (opts) => {
122
+ const config = loadConfig();
123
+ const serverUrl = opts.api ?? process.env.SHENNIAN_DESKTOP_SERVER_URL ?? config.serverUrl ?? undefined;
124
+ await runSmartStart(serverUrl, opts.name);
125
+ });
126
+ // run-service: internal command used by the background service process
127
+ program
128
+ .command('run-service', { hidden: true })
129
+ .description('(internal) Connect to relay server, called by the background service')
130
+ .option('--api <url>', 'Server URL override')
131
+ .action(async (opts) => {
132
+ const envFile = resolveShennianPath('env.json');
133
+ try {
134
+ const saved = JSON.parse(fs.readFileSync(envFile, 'utf-8'));
135
+ for (const [k, v] of Object.entries(saved)) {
136
+ if (!process.env[k])
137
+ process.env[k] = v;
138
+ }
139
+ augmentProcessPath();
140
+ }
141
+ catch {
142
+ // env.json may not exist yet
143
+ }
144
+ installCliLogRedaction(collectKnownLogSecrets(process.env, loadConfig()));
145
+ const logRetentionTimer = startDaemonLogRetention();
146
+ if (isRemoteAccessDisabled()) {
147
+ console.log(`[${new Date().toISOString()}] remote access disabled, service start skipped`);
148
+ clearInterval(logRetentionTimer);
149
+ process.exit(0);
150
+ }
151
+ // Single-instance guard. Service-manager starts are authoritative and may
152
+ // need to take over from an older detached process after app/daemon upgrades.
153
+ const serviceManagedStart = Boolean(process.env.INVOCATION_ID ||
154
+ process.env.JOURNAL_STREAM ||
155
+ process.env.SHENNIAN_DESKTOP_SERVER_URL);
156
+ const stopExistingDaemon = async (oldPid, reason) => {
157
+ console.log(`[${new Date().toISOString()}] ${reason} (PID ${oldPid})`);
158
+ process.kill(oldPid, 'SIGTERM');
159
+ const stopped = await waitForPidExit(oldPid);
160
+ if (!stopped) {
161
+ process.kill(oldPid, 'SIGKILL');
162
+ await waitForPidExit(oldPid, 2000);
163
+ }
164
+ };
165
+ try {
166
+ const oldPid = readDaemonPidFile();
167
+ if (oldPid && oldPid !== process.pid) {
168
+ try {
169
+ process.kill(oldPid, 0);
170
+ if (serviceManagedStart) {
171
+ await stopExistingDaemon(oldPid, 'managed start taking over from existing daemon');
172
+ }
173
+ else {
174
+ console.log(`[${new Date().toISOString()}] daemon already running (PID ${oldPid}), skipping duplicate start`);
175
+ process.exit(0);
176
+ }
177
+ }
178
+ catch {
179
+ // Stale pid; continue booting and overwrite it below.
180
+ }
181
+ }
182
+ }
183
+ catch {
184
+ /* noop */
185
+ }
186
+ const orphanPids = isIsolatedDaemonE2eInstance(process.env, SHENNIAN_DIR)
187
+ ? []
188
+ : findRunningDaemonProcessIds(process.pid);
189
+ if (orphanPids.length > 0) {
190
+ if (serviceManagedStart) {
191
+ for (const orphanPid of orphanPids) {
192
+ try {
193
+ await stopExistingDaemon(orphanPid, 'managed start taking over from orphan daemon');
194
+ }
195
+ catch {
196
+ // Keep booting; the normal pid-file guard below still protects the managed daemon.
197
+ }
198
+ }
199
+ }
200
+ else {
201
+ console.log(`[${new Date().toISOString()}] daemon already running (PID ${orphanPids[0]}), skipping duplicate start`);
202
+ process.exit(0);
203
+ }
204
+ }
205
+ const daemonInstanceId = createDaemonInstanceId();
206
+ writeDaemonPid(process.pid, daemonInstanceId, { version: cliVersion });
207
+ writeDaemonLauncher(process.pid, undefined, daemonInstanceId);
208
+ process.on('exit', () => {
209
+ clearDaemonPidIfOwner(process.pid, daemonInstanceId);
210
+ clearDaemonLauncher(daemonInstanceId);
211
+ });
212
+ // Crash detection: if we're recovering from a failed upgrade, rollback and exit
213
+ const didRollback = await handleStartupCrashCheck();
214
+ if (didRollback) {
215
+ console.log(`[${new Date().toISOString()}] Rolled back to previous version, restarting...`);
216
+ process.exit(0);
217
+ }
218
+ // No personal producer may open its store until the machine generation is
219
+ // fully migrated and its crash-safe marker is committed.
220
+ try {
221
+ ensurePersonalDataEpoch();
222
+ }
223
+ catch (error) {
224
+ console.error(`[personal-sync] data epoch initialization failed: ${error instanceof Error ? error.message : String(error)}`);
225
+ process.exitCode = 1;
226
+ return;
227
+ }
228
+ const config = loadConfig();
229
+ const serverUrl = opts.api ?? process.env.SHENNIAN_DESKTOP_SERVER_URL ?? config.serverUrl ?? SERVERS.cn.url;
230
+ const currentCliVersion = getCurrentVersion();
231
+ const detectedAgents = detectAgents();
232
+ const agentList = detectedAgents.map((a) => a.type);
233
+ const cachedAgentInfos = getCachedAgentInfos(detectedAgents);
234
+ const roomSessionRegistry = new RoomSessionRegistry();
235
+ const roomHostSessionObservations = new RoomHostSessionObservationStore();
236
+ const managedRoomContexts = new ManagedRoomContextAuthority(roomSessionRegistry);
237
+ const roomBindingClient = new HttpRoomBindingClient(serverUrl);
238
+ const roomAttachmentCache = new RoomAttachmentCache({ actionClient: roomBindingClient });
239
+ const roomAttachmentUploader = new RoomAttachmentUploader({ actionClient: roomBindingClient });
240
+ let roomActivationService = null;
241
+ const managedRoomExecutor = createManagedRoomExecutor({
242
+ authority: managedRoomContexts,
243
+ actionClient: roomBindingClient,
244
+ attachmentCache: roomAttachmentCache,
245
+ attachmentUploader: roomAttachmentUploader,
246
+ observations: roomHostSessionObservations,
247
+ getMachineCredentials: () => ({ machineToken: loadConfig().machineToken }),
248
+ });
249
+ const daemonIpc = await startDaemonIpcServer({
250
+ paths: resolveManagedRoomIpcPaths(),
251
+ handlers: {
252
+ 'managed-room.execute': (params) => managedRoomExecutor(params),
253
+ },
254
+ });
255
+ let stoppingBeforeRelay = false;
256
+ const stopBeforeRelay = () => {
257
+ if (stoppingBeforeRelay)
258
+ return;
259
+ stoppingBeforeRelay = true;
260
+ void daemonIpc.close().finally(() => process.exit(0));
261
+ };
262
+ process.once('SIGINT', stopBeforeRelay);
263
+ process.once('SIGTERM', stopBeforeRelay);
264
+ if (!config.machineToken) {
265
+ console.error('Shennian CLI is not paired. Run "shennian pair --browser" or "shennian pair --token" first.');
266
+ await daemonIpc.close();
267
+ process.exitCode = 1;
268
+ return;
269
+ }
270
+ process.removeListener('SIGINT', stopBeforeRelay);
271
+ process.removeListener('SIGTERM', stopBeforeRelay);
272
+ const wsBase = httpToWs(serverUrl);
273
+ const wsUrl = `${wsBase}/relay/machine`;
274
+ const machineToken = config.machineToken;
275
+ if (!machineToken)
276
+ return;
277
+ try {
278
+ await synchronizeMachinePersonalAuthority({
279
+ serverUrl,
280
+ machineToken,
281
+ machineId: config.machineId,
282
+ });
283
+ }
284
+ catch (error) {
285
+ console.error(`[personal-sync] machine authority verification failed: ${error instanceof Error ? error.message : String(error)}`);
286
+ await daemonIpc.close();
287
+ process.exitCode = 1;
288
+ return;
289
+ }
290
+ if (config.machineId) {
291
+ initCliLogReporter(serverUrl, config.machineId);
292
+ }
293
+ console.log(`[${new Date().toISOString()}] Connecting to ${wsUrl}... (v${currentCliVersion}) agents: ${agentList.join(',')}`);
294
+ reportLog({
295
+ level: 'info',
296
+ wsEvent: 'daemon.start',
297
+ metadata: { version: currentCliVersion, agents: agentList },
298
+ });
299
+ let nativeFusion = null;
300
+ let roomMirrorSync = null;
301
+ let deletionCatchupGeneration = 0;
302
+ const autoUpgradePolicy = normalizeAutoUpgradePolicy(config.autoUpgrade);
303
+ const client = new CliRelayClient({
304
+ serverUrl: wsUrl,
305
+ machineToken,
306
+ cliVersion: currentCliVersion,
307
+ agentList,
308
+ autoUpgradePolicy,
309
+ sessionSyncProtocolVersion: SESSION_SYNC_PROTOCOL_VERSION,
310
+ sessionSyncCapabilities: SESSION_SYNC_ADVERTISED_CAPABILITIES,
311
+ daemonInstallationId: getDaemonInstallationId(),
312
+ dataEpoch: CLI_SESSION_SYNC_DATA_EPOCH,
313
+ onConnected: () => {
314
+ console.log(`[${new Date().toISOString()}] ✓ Connected`);
315
+ reportLog({ level: 'info', wsEvent: 'daemon.connected' });
316
+ if (cachedAgentInfos.some((agent) => agent.models.length > 0)) {
317
+ client.sendEvent({
318
+ type: 'event',
319
+ event: 'machine.agents',
320
+ payload: {
321
+ agentList,
322
+ agents: cachedAgentInfos,
323
+ },
324
+ });
325
+ }
326
+ void resolveAgentInfos(detectedAgents, {
327
+ serverUrl: config.serverUrl ?? serverUrl,
328
+ authToken: config.machineToken ?? config.accessToken,
329
+ })
330
+ .then((agents) => {
331
+ if (JSON.stringify(agents) === JSON.stringify(cachedAgentInfos))
332
+ return;
333
+ client.sendEvent({
334
+ type: 'event',
335
+ event: 'machine.agents',
336
+ payload: {
337
+ agentList,
338
+ agents,
339
+ },
340
+ });
341
+ })
342
+ .catch(() => { });
343
+ import('./upgrade/engine.js')
344
+ .then(({ readUpgradeAttempt, clearUpgradeAttempt }) => {
345
+ const attempt = readUpgradeAttempt();
346
+ clearUpgradeAttempt();
347
+ if (attempt) {
348
+ console.log(`[${new Date().toISOString()}] [upgrade] Reporting success: ${attempt.from} → ${attempt.to}`);
349
+ client.sendEvent({
350
+ type: 'event',
351
+ event: 'upgrade',
352
+ payload: {
353
+ state: 'success',
354
+ machineId: 'self',
355
+ from: attempt.from,
356
+ to: attempt.to,
357
+ },
358
+ });
359
+ }
360
+ })
361
+ .catch(() => { });
362
+ upgradeScheduler.start();
363
+ roomMirrorSync?.start();
364
+ const catchupGeneration = ++deletionCatchupGeneration;
365
+ const catchUpBeforeDiscovery = async () => {
366
+ try {
367
+ await sessionManager.synchronizeSessionDeletions();
368
+ if (catchupGeneration === deletionCatchupGeneration &&
369
+ client.getState() === 'connected') {
370
+ nativeFusion?.handleConnected();
371
+ }
372
+ }
373
+ catch (error) {
374
+ // Discovery and dirty-index publication deliberately stay stopped:
375
+ // scanning before deletion catch-up can resurrect local history.
376
+ console.error('[session.deletions] startup catch-up failed', error);
377
+ if (catchupGeneration === deletionCatchupGeneration &&
378
+ client.getState() === 'connected') {
379
+ if (error instanceof DeletionSyncTerminalError)
380
+ return;
381
+ const delay = error instanceof DeletionSyncRetryWaitError
382
+ ? Math.max(1_000, error.retryAfterMs)
383
+ : 5_000;
384
+ setTimeout(() => void catchUpBeforeDiscovery(), delay).unref?.();
385
+ }
386
+ }
387
+ };
388
+ void catchUpBeforeDiscovery();
389
+ },
390
+ onDisconnected: (info) => {
391
+ deletionCatchupGeneration += 1;
392
+ roomMirrorSync?.stop();
393
+ console.log(`[${new Date().toISOString()}] ⚠ Disconnected, reconnecting... ${formatDisconnectInfo(info)}`);
394
+ reportLog({
395
+ level: 'warn',
396
+ wsEvent: 'daemon.disconnected',
397
+ metadata: {
398
+ code: info.code,
399
+ reason: info.reason,
400
+ error: info.error,
401
+ phase: info.phase,
402
+ trigger: info.trigger,
403
+ reconnectAttempt: info.reconnectAttempt,
404
+ },
405
+ });
406
+ },
407
+ onReq: (req) => {
408
+ console.log(`[${new Date().toISOString()}] [req] ${req.method}`);
409
+ reportLog({
410
+ level: 'info',
411
+ type: 'ws',
412
+ wsEvent: req.method,
413
+ wsDirection: 'in',
414
+ traceId: req.traceId,
415
+ });
416
+ void sessionManager.handleReq(req);
417
+ },
418
+ });
419
+ nativeFusion = isNativeFusionEnabled() ? new NativeSessionFusionService(client) : null;
420
+ roomMirrorSync = new RoomMirrorSyncService({
421
+ registry: roomSessionRegistry,
422
+ observations: roomHostSessionObservations,
423
+ actionClient: roomBindingClient,
424
+ getMachineCredentials: () => ({ machineToken: loadConfig().machineToken }),
425
+ onMessages: async (roomId, messages, registrations, context) => {
426
+ await roomActivationService?.processMessages(roomId, messages, registrations, context);
427
+ },
428
+ });
429
+ const upgradeScheduler = new AutoUpgradeScheduler({
430
+ currentVersion: currentCliVersion,
431
+ machineId: config.machineId ?? 'unknown-machine',
432
+ serverUrl: config.serverUrl ?? serverUrl,
433
+ getIdleState: () => sessionManager.getUpgradeIdleState(),
434
+ startUpgrade: async (targetVersion) => {
435
+ const { handleUpgradeStart } = await import('./commands/upgrade.js');
436
+ await handleUpgradeStart(client, 'auto-upgrade', targetVersion, {
437
+ currentVersion: currentCliVersion,
438
+ });
439
+ },
440
+ });
441
+ upgradeScheduler.setPolicy(autoUpgradePolicy);
442
+ const sessionManager = new SessionManager(client, nativeFusion, currentCliVersion, upgradeScheduler, new TrustedManagedRoomBinder({
443
+ registry: roomSessionRegistry,
444
+ observations: roomHostSessionObservations,
445
+ bindingClient: roomBindingClient,
446
+ getMachineCredentials: () => {
447
+ const current = loadConfig();
448
+ return { machineId: current.machineId, machineToken: current.machineToken };
449
+ },
450
+ hostProcessIdentity: () => `daemon:${daemonInstanceId}`,
451
+ }), {
452
+ prepare: () => prepareRemoteAccessPause(),
453
+ isDisabled: () => isRemoteAccessDisabled(),
454
+ shutdownAfterResponse: () => {
455
+ setTimeout(() => {
456
+ stopPreparedRemoteAccessService();
457
+ process.kill(process.pid, 'SIGTERM');
458
+ }, 100).unref?.();
459
+ },
460
+ }, (token) => managedRoomContexts.revokeToken(token), managedRoomExecutor);
461
+ roomActivationService = new RoomActivationService({
462
+ registry: roomSessionRegistry,
463
+ observations: roomHostSessionObservations,
464
+ enqueueConversationBatch: (inputs) => sessionManager.enqueueManagedRoomBatch(inputs),
465
+ loadContext: async (registration) => {
466
+ const machineToken = loadConfig().machineToken?.trim();
467
+ const binding = registration.binding;
468
+ if (!machineToken || !binding)
469
+ throw new Error('managed_room_context_unavailable');
470
+ const remote = await roomBindingClient.status({
471
+ machineToken,
472
+ roomId: binding.roomId,
473
+ bindingId: binding.bindingId,
474
+ });
475
+ return {
476
+ roomName: remote.room.name,
477
+ memberCount: remote.room.memberCount,
478
+ members: remote.room.members,
479
+ agentsByIdentityId: remote.agent ? { [remote.agentIdentityId]: remote.agent } : {},
480
+ agentSystemPromptsByIdentityId: remote.agent
481
+ ? { [remote.agentIdentityId]: remote.agent.systemPrompt ?? '' }
482
+ : {},
483
+ };
484
+ },
485
+ issueManagedRoomContext: (registryKey, activationCauseMessageId) => {
486
+ const machineToken = loadConfig().machineToken;
487
+ if (!machineToken)
488
+ throw new Error('machine_not_authorized');
489
+ return managedRoomContexts.issue({
490
+ registryKey,
491
+ machineCredential: machineToken,
492
+ activationCauseMessageId,
493
+ });
494
+ },
495
+ audit: new RoomActivationAudit(),
496
+ });
497
+ sessionManager.setManagedRoomTurnPreparer((contexts) => roomActivationService.prepareConversationTurn(contexts));
498
+ fs.mkdirSync(SHENNIAN_DIR, { recursive: true });
499
+ writeDaemonPid(process.pid, daemonInstanceId, { version: currentCliVersion });
500
+ writeDaemonLauncher(process.pid, undefined, daemonInstanceId);
501
+ client.connect();
502
+ process.stdin.resume();
503
+ let shuttingDown = false;
504
+ const shutdown = async () => {
505
+ if (shuttingDown)
506
+ return;
507
+ shuttingDown = true;
508
+ console.log(chalk.gray('\nDisconnecting...'));
509
+ reportLog({ level: 'info', wsEvent: 'daemon.stop' });
510
+ clearInterval(logRetentionTimer);
511
+ try {
512
+ await sessionManager.cleanup();
513
+ upgradeScheduler.stop();
514
+ nativeFusion?.stop();
515
+ roomMirrorSync?.stop();
516
+ client.disconnect();
517
+ await daemonIpc.close();
518
+ clearDaemonPidIfOwner(process.pid, daemonInstanceId);
519
+ clearDaemonLauncher(daemonInstanceId);
520
+ }
521
+ finally {
522
+ process.exit(0);
523
+ }
524
+ };
525
+ process.on('SIGINT', () => void shutdown());
526
+ process.on('SIGTERM', () => void shutdown());
527
+ });
528
+ program
529
+ .command('workbuddy-session-hook', { hidden: true })
530
+ .description('(retired) WorkBuddy Room Hook')
531
+ .allowUnknownOption(true)
532
+ .action(() => writeRetiredIntegrationError());
533
+ program
534
+ .command('room-session-hook', { hidden: true })
535
+ .description('(internal) Authorize the official Room tool for one trusted Agent Session')
536
+ .argument('<runtime>', 'Trusted runtime adapter')
537
+ .allowUnknownOption(true)
538
+ .action(() => writeRetiredIntegrationError());
539
+ program
540
+ .command('room-tool-server', { hidden: true })
541
+ .description('(internal) Run the official Shennian Room MCP tool')
542
+ .allowUnknownOption(true)
543
+ .action(() => writeRetiredIntegrationError());
544
+ // config: view and modify settings
545
+ const configCmd = program.command('config').description('View or modify configuration');
546
+ configCmd
547
+ .command('show', { isDefault: true })
548
+ .description('Show current configuration')
549
+ .action(() => {
550
+ console.log(`Config file: ${getConfigPath()}`);
551
+ const config = loadConfig();
552
+ const display = sanitizeCliLogValue(config, collectKnownLogSecrets(process.env, config));
553
+ if (config.serverUrl) {
554
+ const region = urlToRegion(config.serverUrl);
555
+ console.log(`Region: ${region} (${SERVERS[region].label})`);
556
+ }
557
+ console.log(JSON.stringify(display, null, 2));
558
+ });
559
+ configCmd
560
+ .command('set')
561
+ .description('Update a config value')
562
+ .argument('<key>', 'Config key (e.g. "server")')
563
+ .argument('<value>', 'Config value (e.g. "cn" or "global")')
564
+ .action((key, value) => {
565
+ const config = loadConfig();
566
+ if (key === 'server') {
567
+ if (value !== 'cn' && value !== 'global') {
568
+ console.error(chalk.red('✗ Value must be "cn" or "global"'));
569
+ process.exit(1);
570
+ }
571
+ const region = value;
572
+ config.serverUrl = regionToUrl(region);
573
+ saveConfig(config);
574
+ console.log(chalk.green(`✓ Server set to ${SERVERS[region].label}`));
575
+ console.log(chalk.yellow(' Restart the background service for this to take effect: shennian stop && shennian start'));
576
+ return;
577
+ }
578
+ console.error(chalk.red(`✗ Unknown config key: ${key}. Supported: server`));
579
+ process.exit(1);
580
+ });
581
+ registerPairCommand(program);
582
+ registerDaemonCommand(program);
583
+ registerRuntimeCommand(program);
584
+ registerManagerCommand(program);
585
+ registerUpgradeCommand(program);
586
+ registerToolsCommand(program);
587
+ program.parse();