shennian 0.3.48 → 0.3.49

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.
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "jsMinified": true,
4
4
  "minifier": "esbuild",
5
- "fileCount": 124,
5
+ "fileCount": 125,
6
6
  "files": [
7
7
  {
8
8
  "file": "bin/shennian.js",
@@ -156,8 +156,8 @@
156
156
  },
157
157
  {
158
158
  "file": "src/commands/daemon.js",
159
- "beforeBytes": 39808,
160
- "afterBytes": 19439
159
+ "beforeBytes": 39845,
160
+ "afterBytes": 19471
161
161
  },
162
162
  {
163
163
  "file": "src/commands/integration.js",
@@ -176,8 +176,8 @@
176
176
  },
177
177
  {
178
178
  "file": "src/commands/pair-browser.js",
179
- "beforeBytes": 6544,
180
- "afterBytes": 3636
179
+ "beforeBytes": 6655,
180
+ "afterBytes": 3699
181
181
  },
182
182
  {
183
183
  "file": "src/commands/pair-qr.js",
@@ -186,8 +186,8 @@
186
186
  },
187
187
  {
188
188
  "file": "src/commands/pair.js",
189
- "beforeBytes": 15652,
190
- "afterBytes": 8781
189
+ "beforeBytes": 15955,
190
+ "afterBytes": 8988
191
191
  },
192
192
  {
193
193
  "file": "src/commands/room.js",
@@ -276,8 +276,8 @@
276
276
  },
277
277
  {
278
278
  "file": "src/index.js",
279
- "beforeBytes": 22456,
280
- "afterBytes": 11565
279
+ "beforeBytes": 22986,
280
+ "afterBytes": 11906
281
281
  },
282
282
  {
283
283
  "file": "src/log-reporter.js",
@@ -364,15 +364,20 @@
364
364
  "beforeBytes": 54,
365
365
  "afterBytes": 60
366
366
  },
367
+ {
368
+ "file": "src/personal-sync/account-namespace.js",
369
+ "beforeBytes": 1412,
370
+ "afterBytes": 879
371
+ },
367
372
  {
368
373
  "file": "src/personal-sync/identity.js",
369
- "beforeBytes": 1844,
370
- "afterBytes": 1020
374
+ "beforeBytes": 1928,
375
+ "afterBytes": 1024
371
376
  },
372
377
  {
373
378
  "file": "src/personal-sync/paths.js",
374
- "beforeBytes": 364,
375
- "afterBytes": 223
379
+ "beforeBytes": 1345,
380
+ "afterBytes": 830
376
381
  },
377
382
  {
378
383
  "file": "src/region.js",
@@ -496,8 +501,8 @@
496
501
  },
497
502
  {
498
503
  "file": "src/session/canonical-store.js",
499
- "beforeBytes": 29119,
500
- "afterBytes": 14276
504
+ "beforeBytes": 29295,
505
+ "afterBytes": 14384
501
506
  },
502
507
  {
503
508
  "file": "src/session/deletion-sync-state.js",
@@ -586,8 +591,8 @@
586
591
  },
587
592
  {
588
593
  "file": "src/session/queue.js",
589
- "beforeBytes": 17539,
590
- "afterBytes": 8270
594
+ "beforeBytes": 17581,
595
+ "afterBytes": 8290
591
596
  },
592
597
  {
593
598
  "file": "src/session/store.js",
@@ -1,5 +1,5 @@
1
- import c from"chalk";import i from"node:fs";import s from"node:path";import u from"node:os";import{execSync as a,spawn as de}from"node:child_process";import{randomUUID as le}from"node:crypto";import{fileURLToPath as pe}from"node:url";import{getShennianDir as me,loadConfig as j,resolveShennianPath as h,saveConfig as fe}from"../config/index.js";import{buildAugmentedPath as Se}from"../env-path.js";import{buildWindowsLauncherCommand as he,buildWindowsScheduledTaskCommands as ge,buildWindowsScheduledTaskXml as we,buildWindowsStartupVbs as ye,escapeXml as T,isWindowsCmdScript as ve,quoteCmdArg as W,quoteSystemdArg as Ee,suspendWindowsScheduledTask as Pe}from"./daemon-windows.js";import{waitForDaemonReadyStatus as L}from"./daemon-start-wait.js";import{findRunningWindowsDaemonProcessIds as Ae,isShennianRunServiceCommand as Ie}from"./daemon-process.js";import{getCurrentVersion as F}from"../upgrade/engine.js";import{buildWindowsLauncherCommand as An,buildWindowsScheduledTaskCommands as In,buildWindowsScheduledTaskSuspendCommands as kn,buildWindowsScheduledTaskXml as Dn,buildWindowsStartupVbs as Nn,suspendWindowsScheduledTask as bn}from"./daemon-windows.js";import{isShennianRunServiceCommand as Tn}from"./daemon-process.js";const ke=s.dirname(pe(import.meta.url)),S=me(),g=h("daemon.pid"),d=h("daemon.log"),x=h("remote-access.disabled"),D=h("daemon-launcher.json"),B=s.resolve(ke,"../../bin/shennian.js"),V=process.execPath,J=new Set(["PATH","HOME","USERPROFILE","HOMEDRIVE","HOMEPATH","USERNAME","USERDOMAIN","COMPUTERNAME","USER","LOGNAME","SHELL","TMPDIR","LANG","LC_ALL","LC_CTYPE","SSH_AUTH_SOCK","XDG_CONFIG_HOME","XDG_DATA_HOME","TEMP","TMP","APPDATA","LOCALAPPDATA","SHENNIAN_DESKTOP_SERVER_URL","SHENNIAN_WECHAT_CHANNEL_HELPER_DIR","SHENNIAN_HOME","SHENNIAN_NATIVE_FUSION_DISABLED","SHENNIAN_WECHAT_DELIVERABILITY_TRACE"]);function De(e){return J.has(e)}function l(){const e=u.platform();return e==="darwin"||e==="linux"||e==="win32"?e:"linux"}function O(e){const n=e.replace(/\\/g,"/").toLowerCase(),t=u.tmpdir().replace(/\\/g,"/").toLowerCase();return n.includes("/_npx/")||n.includes("/npm-cache/_npx/")||n.includes("/pnpm/dlx/")||n.startsWith(t.endsWith("/")?t:`${t}/`)}function Ne(e){return i.existsSync(e.scriptPath)&&!O(e.scriptPath)?{command:e.nodeExec,args:[e.scriptPath,"run-service"],mode:"direct"}:e.shennianCommandPath&&!O(e.shennianCommandPath)?{command:e.shennianCommandPath,args:["run-service"],mode:"global-shim"}:e.npxPath?{command:e.npxPath,args:["--yes","shennian","run-service"],mode:"npx"}:{command:e.nodeExec,args:[e.scriptPath,"run-service"],mode:"direct"}}function be(e){const n=e.trim();if(!n)return null;if(n.startsWith("{"))try{const r=JSON.parse(n),o=Number(r.pid);return!Number.isInteger(o)||o<=0?null:{pid:o,...typeof r.instanceId=="string"?{instanceId:r.instanceId}:{},...typeof r.version=="string"?{version:r.version}:{},...r.launcher==="desktop-managed"||r.launcher==="global-cli"||r.launcher==="unknown"?{launcher:r.launcher}:{},...typeof r.startedAt=="string"?{startedAt:r.startedAt}:{},...r.adopted===!0?{adopted:!0}:{}}}catch{return null}const t=parseInt(n,10);return isNaN(t)?null:{pid:t}}function P(){try{return be(i.readFileSync(g,"utf-8"))}catch{return null}}function E(e){try{return process.kill(e,0),!0}catch{return!1}}function G(e,n=3e3){const t=Date.now();for(;Date.now()-t<n;){if(!E(e))return!0;Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,100)}return!E(e)}function Re(){return i.existsSync(x)}function Te(){try{i.unlinkSync(x)}catch{}}function K(){return"global-cli"}function Le(){return`${process.pid}-${Date.now()}-${le()}`}function xe(e,n,t={}){i.mkdirSync(S,{recursive:!0}),i.writeFileSync(g,JSON.stringify({pid:e,instanceId:n,...t.version?{version:t.version}:{},launcher:t.launcher??K(),...t.adopted?{adopted:!0}:{},startedAt:new Date().toISOString()},null,2))}function yn(e,n){const t=P();if(!(t?.pid!==e||t.instanceId!==n))try{i.unlinkSync(g)}catch{}}function X(e,n=K(),t){i.mkdirSync(S,{recursive:!0}),i.writeFileSync(D,JSON.stringify({pid:e,launcher:n,...t?{instanceId:t}:{},updatedAt:new Date().toISOString()},null,2))}function Oe(e){if(e)try{if(JSON.parse(i.readFileSync(D,"utf-8")).instanceId!==e)return}catch{return}try{i.unlinkSync(D)}catch{}}function He(e){e&&X(e)}function _e(e,n){if(!e||!n)return"unknown";try{const t=JSON.parse(i.readFileSync(D,"utf-8"));if(t.pid!==e)return"unknown";if(t.launcher==="desktop-managed"||t.launcher==="global-cli")return t.launcher}catch{}return H(e)}function H(e){if(l()==="win32")return"unknown";try{const n=a(`ps -p ${e} -o command=`,{encoding:"utf-8",stdio:["ignore","pipe","ignore"],timeout:1e3,windowsHide:!0}).replace(/\\/g,"/");if(n.includes("/node_modules/shennian/")||n.includes("/bin/shennian")||n.includes(" shennian run-service")||n.includes(" shennian.js run-service"))return"global-cli"}catch{}return"unknown"}function q(e=process.pid){if(l()==="win32")return Ae(e);try{return a("ps -axo pid=,command=",{encoding:"utf-8",stdio:["ignore","pipe","ignore"],timeout:1e3,windowsHide:!0}).split(/\r?\n/).map(t=>{const r=t.trim().match(/^(\d+)\s+(.+)$/);if(!r)return null;const o=Number(r[1]),f=r[2];return!Number.isInteger(o)||o===e?null:Ie(f)?o:null}).filter(t=>typeof t=="number")}catch{return[]}}function $e(e=process.env,n=S,t=u.tmpdir(),r=s.join(u.homedir(),".shennian")){if(e.SHENNIAN_E2E_ISOLATED_DAEMON!=="1"||!e.SHENNIAN_HOME?.trim()||s.resolve(n)===s.resolve(r))return!1;const o=s.relative(s.resolve(t),s.resolve(n));return o!==""&&o!==".."&&!o.startsWith(`..${s.sep}`)&&!s.isAbsolute(o)||s.resolve(n)===s.resolve(e.SHENNIAN_HOME)}function Ce(e=process.env,n=S){return!$e(e,n)}function m(e={}){let n=P(),t=n?.pid??null,r=t!==null&&E(t),o=t!==null&&!r,f=n?.adopted===!0;const v=j();if(o&&e.cleanupStale){try{i.unlinkSync(g)}catch{}n=null,t=null,r=!1,o=!1}if(!r&&Ce()){const w=q()[0];if(w&&(t=w,r=!0,o=!1,f=!0,e.cleanupStale)){const U=Le();xe(w,U,{launcher:H(w),adopted:!0}),X(w,H(w)),n={pid:w,instanceId:U,adopted:!0}}}return{running:r,pid:t,stale:o,remoteAccessDisabled:Re(),launcher:_e(t,r),platform:l(),shennianDir:S,pidFile:g,logFile:d,...n?.instanceId?{instanceId:n.instanceId}:{},...f?{adopted:!0}:{},...v.machineId?{machineId:v.machineId}:{},paired:!!(v.machineToken&&v.machineId),...v.serverUrl?{serverUrl:v.serverUrl}:{}}}function y(e){process.stdout.write(`${JSON.stringify(e,null,2)}
2
- `)}function Y(e){const n=e?.trim();if(!n)return;const t=j();t.serverUrl=n,fe(t)}function z(e){return e?.trim()||process.env.SHENNIAN_DESKTOP_SERVER_URL?.trim()||void 0}const _="com.shennian.agent",p=s.join(u.homedir(),"Library/LaunchAgents",`${_}.plist`),Me=s.join(u.homedir(),"AppData","Roaming","Microsoft","Windows","Start Menu","Programs","Startup"),N=h("autostart.cmd"),b=h("autostart.vbs"),R=h("autostart.xml"),Ue=s.join(Me,"ShennianAgent.vbs");function Q(e){const n=l()==="win32"?`where ${e}`:`command -v ${e}`;try{return a(n,{stdio:["ignore","pipe","pipe"],encoding:"utf-8",windowsHide:!0}).split(/\r?\n/).map(o=>o.trim()).find(Boolean)??null}catch{return null}}function je(){const e=l()==="win32"?"npx.cmd":"npx",n=s.join(s.dirname(V),e);return i.existsSync(n)?n:Q("npx")}function A(){return Ne({nodeExec:V,scriptPath:B,shennianCommandPath:Q("shennian"),npxPath:je()})}function Z(){try{a(`schtasks /delete /tn "${k}" /f`,{stdio:"pipe",windowsHide:!0})}catch{}}function ee(){try{i.unlinkSync(Ue)}catch{}}function We(){const e=process.env.USERDOMAIN?.trim(),n=process.env.COMPUTERNAME?.trim(),t=process.env.USERNAME?.trim()||u.userInfo().username;return e&&e.toUpperCase()!=="WORKGROUP"?`${e}\\${t}`:n?`${n}\\${t}`:t}function Fe(e){if(l()==="win32"&&ve(e.command)){const n=[e.command,...e.args].map(W).join(" ");return{command:process.env.ComSpec||"cmd.exe",args:["/d","/s","/c",`"${n}"`],windowsVerbatimArguments:!0}}return{command:e.command,args:e.args}}function Be(e,n,t=process.env){return{detached:!0,stdio:["ignore",n,n],env:t,windowsHide:!0,...e.windowsVerbatimArguments?{windowsVerbatimArguments:!0}:{}}}function Ve(){const e=A();i.writeFileSync(N,he(e,d)),i.writeFileSync(b,ye(N)),ee(),Z();const n=we({userId:We(),command:s.join(process.env.SystemRoot||"C:\\Windows","System32","wscript.exe"),arguments:[W(b)]});i.writeFileSync(R,n,"utf8");const t=ge({taskName:k,xmlPath:R});try{return a(t.create,{stdio:"pipe",windowsHide:!0}),a(t.run,{stdio:"pipe",windowsHide:!0}),!0}catch{return!1}}function Je(){Pe(k,e=>{a(e,{stdio:"pipe",windowsHide:!0})})}function ne(){const e={};for(const n of J)process.env[n]&&(e[n]=process.env[n]);if(e.HOME??=u.homedir(),e.PATH=Se({pathValue:e.PATH,env:e}),e.USER??=u.userInfo().username,l()==="win32"){const n=process.env.TEMP||process.env.TMP||u.tmpdir(),t=u.homedir();e.USERPROFILE??=t;const r=s.win32.parse(t);e.HOMEDRIVE??=r.root.replace(/[\\/]$/,"")||process.env.HOMEDRIVE||"",e.HOMEPATH??=t.slice((e.HOMEDRIVE||"").length)||process.env.HOMEPATH||"",e.USERNAME??=process.env.USERNAME||u.userInfo().username,e.COMPUTERNAME??=process.env.COMPUTERNAME||"",e.TMPDIR??=n,e.TEMP??=n,e.TMP??=n}else e.TMPDIR??="/tmp";return e}function Ge(){const e=ne(),n=A(),t=Object.entries(e).map(([o,f])=>` <key>${T(o)}</key>
1
+ import c from"chalk";import i from"node:fs";import s from"node:path";import u from"node:os";import{execSync as a,spawn as de}from"node:child_process";import{randomUUID as le}from"node:crypto";import{fileURLToPath as pe}from"node:url";import{getShennianDir as me,loadConfig as j,resolveShennianPath as h,saveConfig as fe}from"../config/index.js";import{buildAugmentedPath as Se}from"../env-path.js";import{buildWindowsLauncherCommand as he,buildWindowsScheduledTaskCommands as ge,buildWindowsScheduledTaskXml as we,buildWindowsStartupVbs as ye,escapeXml as T,isWindowsCmdScript as ve,quoteCmdArg as W,quoteSystemdArg as Ee,suspendWindowsScheduledTask as Ae}from"./daemon-windows.js";import{waitForDaemonReadyStatus as L}from"./daemon-start-wait.js";import{findRunningWindowsDaemonProcessIds as Pe,isShennianRunServiceCommand as Ie}from"./daemon-process.js";import{getCurrentVersion as F}from"../upgrade/engine.js";import{buildWindowsLauncherCommand as Pn,buildWindowsScheduledTaskCommands as In,buildWindowsScheduledTaskSuspendCommands as kn,buildWindowsScheduledTaskXml as Nn,buildWindowsStartupVbs as Dn,suspendWindowsScheduledTask as bn}from"./daemon-windows.js";import{isShennianRunServiceCommand as Tn}from"./daemon-process.js";const ke=s.dirname(pe(import.meta.url)),S=me(),g=h("daemon.pid"),d=h("daemon.log"),x=h("remote-access.disabled"),N=h("daemon-launcher.json"),B=s.resolve(ke,"../../bin/shennian.js"),V=process.execPath,J=new Set(["PATH","HOME","USERPROFILE","HOMEDRIVE","HOMEPATH","USERNAME","USERDOMAIN","COMPUTERNAME","USER","LOGNAME","SHELL","TMPDIR","LANG","LC_ALL","LC_CTYPE","SSH_AUTH_SOCK","XDG_CONFIG_HOME","XDG_DATA_HOME","TEMP","TMP","APPDATA","LOCALAPPDATA","SHENNIAN_DESKTOP_SERVER_URL","SHENNIAN_WECHAT_CHANNEL_HELPER_DIR","SHENNIAN_HOME","SHENNIAN_ACCOUNT_NAMESPACE_ID","SHENNIAN_NATIVE_FUSION_DISABLED","SHENNIAN_WECHAT_DELIVERABILITY_TRACE"]);function Ne(e){return J.has(e)}function l(){const e=u.platform();return e==="darwin"||e==="linux"||e==="win32"?e:"linux"}function O(e){const n=e.replace(/\\/g,"/").toLowerCase(),t=u.tmpdir().replace(/\\/g,"/").toLowerCase();return n.includes("/_npx/")||n.includes("/npm-cache/_npx/")||n.includes("/pnpm/dlx/")||n.startsWith(t.endsWith("/")?t:`${t}/`)}function De(e){return i.existsSync(e.scriptPath)&&!O(e.scriptPath)?{command:e.nodeExec,args:[e.scriptPath,"run-service"],mode:"direct"}:e.shennianCommandPath&&!O(e.shennianCommandPath)?{command:e.shennianCommandPath,args:["run-service"],mode:"global-shim"}:e.npxPath?{command:e.npxPath,args:["--yes","shennian","run-service"],mode:"npx"}:{command:e.nodeExec,args:[e.scriptPath,"run-service"],mode:"direct"}}function be(e){const n=e.trim();if(!n)return null;if(n.startsWith("{"))try{const r=JSON.parse(n),o=Number(r.pid);return!Number.isInteger(o)||o<=0?null:{pid:o,...typeof r.instanceId=="string"?{instanceId:r.instanceId}:{},...typeof r.version=="string"?{version:r.version}:{},...r.launcher==="desktop-managed"||r.launcher==="global-cli"||r.launcher==="unknown"?{launcher:r.launcher}:{},...typeof r.startedAt=="string"?{startedAt:r.startedAt}:{},...r.adopted===!0?{adopted:!0}:{}}}catch{return null}const t=parseInt(n,10);return isNaN(t)?null:{pid:t}}function A(){try{return be(i.readFileSync(g,"utf-8"))}catch{return null}}function E(e){try{return process.kill(e,0),!0}catch{return!1}}function G(e,n=3e3){const t=Date.now();for(;Date.now()-t<n;){if(!E(e))return!0;Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,100)}return!E(e)}function Re(){return i.existsSync(x)}function Te(){try{i.unlinkSync(x)}catch{}}function K(){return"global-cli"}function Le(){return`${process.pid}-${Date.now()}-${le()}`}function xe(e,n,t={}){i.mkdirSync(S,{recursive:!0}),i.writeFileSync(g,JSON.stringify({pid:e,instanceId:n,...t.version?{version:t.version}:{},launcher:t.launcher??K(),...t.adopted?{adopted:!0}:{},startedAt:new Date().toISOString()},null,2))}function yn(e,n){const t=A();if(!(t?.pid!==e||t.instanceId!==n))try{i.unlinkSync(g)}catch{}}function X(e,n=K(),t){i.mkdirSync(S,{recursive:!0}),i.writeFileSync(N,JSON.stringify({pid:e,launcher:n,...t?{instanceId:t}:{},updatedAt:new Date().toISOString()},null,2))}function Oe(e){if(e)try{if(JSON.parse(i.readFileSync(N,"utf-8")).instanceId!==e)return}catch{return}try{i.unlinkSync(N)}catch{}}function He(e){e&&X(e)}function _e(e,n){if(!e||!n)return"unknown";try{const t=JSON.parse(i.readFileSync(N,"utf-8"));if(t.pid!==e)return"unknown";if(t.launcher==="desktop-managed"||t.launcher==="global-cli")return t.launcher}catch{}return H(e)}function H(e){if(l()==="win32")return"unknown";try{const n=a(`ps -p ${e} -o command=`,{encoding:"utf-8",stdio:["ignore","pipe","ignore"],timeout:1e3,windowsHide:!0}).replace(/\\/g,"/");if(n.includes("/node_modules/shennian/")||n.includes("/bin/shennian")||n.includes(" shennian run-service")||n.includes(" shennian.js run-service"))return"global-cli"}catch{}return"unknown"}function q(e=process.pid){if(l()==="win32")return Pe(e);try{return a("ps -axo pid=,command=",{encoding:"utf-8",stdio:["ignore","pipe","ignore"],timeout:1e3,windowsHide:!0}).split(/\r?\n/).map(t=>{const r=t.trim().match(/^(\d+)\s+(.+)$/);if(!r)return null;const o=Number(r[1]),f=r[2];return!Number.isInteger(o)||o===e?null:Ie(f)?o:null}).filter(t=>typeof t=="number")}catch{return[]}}function $e(e=process.env,n=S,t=u.tmpdir(),r=s.join(u.homedir(),".shennian")){if(e.SHENNIAN_E2E_ISOLATED_DAEMON!=="1"||!e.SHENNIAN_HOME?.trim()||s.resolve(n)===s.resolve(r))return!1;const o=s.relative(s.resolve(t),s.resolve(n));return o!==""&&o!==".."&&!o.startsWith(`..${s.sep}`)&&!s.isAbsolute(o)||s.resolve(n)===s.resolve(e.SHENNIAN_HOME)}function Ce(e=process.env,n=S){return!$e(e,n)}function m(e={}){let n=A(),t=n?.pid??null,r=t!==null&&E(t),o=t!==null&&!r,f=n?.adopted===!0;const v=j();if(o&&e.cleanupStale){try{i.unlinkSync(g)}catch{}n=null,t=null,r=!1,o=!1}if(!r&&Ce()){const w=q()[0];if(w&&(t=w,r=!0,o=!1,f=!0,e.cleanupStale)){const U=Le();xe(w,U,{launcher:H(w),adopted:!0}),X(w,H(w)),n={pid:w,instanceId:U,adopted:!0}}}return{running:r,pid:t,stale:o,remoteAccessDisabled:Re(),launcher:_e(t,r),platform:l(),shennianDir:S,pidFile:g,logFile:d,...n?.instanceId?{instanceId:n.instanceId}:{},...f?{adopted:!0}:{},...v.machineId?{machineId:v.machineId}:{},paired:!!(v.machineToken&&v.machineId),...v.serverUrl?{serverUrl:v.serverUrl}:{}}}function y(e){process.stdout.write(`${JSON.stringify(e,null,2)}
2
+ `)}function Y(e){const n=e?.trim();if(!n)return;const t=j();t.serverUrl=n,fe(t)}function z(e){return e?.trim()||process.env.SHENNIAN_DESKTOP_SERVER_URL?.trim()||void 0}const _="com.shennian.agent",p=s.join(u.homedir(),"Library/LaunchAgents",`${_}.plist`),Me=s.join(u.homedir(),"AppData","Roaming","Microsoft","Windows","Start Menu","Programs","Startup"),D=h("autostart.cmd"),b=h("autostart.vbs"),R=h("autostart.xml"),Ue=s.join(Me,"ShennianAgent.vbs");function Q(e){const n=l()==="win32"?`where ${e}`:`command -v ${e}`;try{return a(n,{stdio:["ignore","pipe","pipe"],encoding:"utf-8",windowsHide:!0}).split(/\r?\n/).map(o=>o.trim()).find(Boolean)??null}catch{return null}}function je(){const e=l()==="win32"?"npx.cmd":"npx",n=s.join(s.dirname(V),e);return i.existsSync(n)?n:Q("npx")}function P(){return De({nodeExec:V,scriptPath:B,shennianCommandPath:Q("shennian"),npxPath:je()})}function Z(){try{a(`schtasks /delete /tn "${k}" /f`,{stdio:"pipe",windowsHide:!0})}catch{}}function ee(){try{i.unlinkSync(Ue)}catch{}}function We(){const e=process.env.USERDOMAIN?.trim(),n=process.env.COMPUTERNAME?.trim(),t=process.env.USERNAME?.trim()||u.userInfo().username;return e&&e.toUpperCase()!=="WORKGROUP"?`${e}\\${t}`:n?`${n}\\${t}`:t}function Fe(e){if(l()==="win32"&&ve(e.command)){const n=[e.command,...e.args].map(W).join(" ");return{command:process.env.ComSpec||"cmd.exe",args:["/d","/s","/c",`"${n}"`],windowsVerbatimArguments:!0}}return{command:e.command,args:e.args}}function Be(e,n,t=process.env){return{detached:!0,stdio:["ignore",n,n],env:t,windowsHide:!0,...e.windowsVerbatimArguments?{windowsVerbatimArguments:!0}:{}}}function Ve(){const e=P();i.writeFileSync(D,he(e,d)),i.writeFileSync(b,ye(D)),ee(),Z();const n=we({userId:We(),command:s.join(process.env.SystemRoot||"C:\\Windows","System32","wscript.exe"),arguments:[W(b)]});i.writeFileSync(R,n,"utf8");const t=ge({taskName:k,xmlPath:R});try{return a(t.create,{stdio:"pipe",windowsHide:!0}),a(t.run,{stdio:"pipe",windowsHide:!0}),!0}catch{return!1}}function Je(){Ae(k,e=>{a(e,{stdio:"pipe",windowsHide:!0})})}function ne(){const e={};for(const n of J)process.env[n]&&(e[n]=process.env[n]);if(e.HOME??=u.homedir(),e.PATH=Se({pathValue:e.PATH,env:e}),e.USER??=u.userInfo().username,l()==="win32"){const n=process.env.TEMP||process.env.TMP||u.tmpdir(),t=u.homedir();e.USERPROFILE??=t;const r=s.win32.parse(t);e.HOMEDRIVE??=r.root.replace(/[\\/]$/,"")||process.env.HOMEDRIVE||"",e.HOMEPATH??=t.slice((e.HOMEDRIVE||"").length)||process.env.HOMEPATH||"",e.USERNAME??=process.env.USERNAME||u.userInfo().username,e.COMPUTERNAME??=process.env.COMPUTERNAME||"",e.TMPDIR??=n,e.TEMP??=n,e.TMP??=n}else e.TMPDIR??="/tmp";return e}function Ge(){const e=ne(),n=P(),t=Object.entries(e).map(([o,f])=>` <key>${T(o)}</key>
3
3
  <string>${T(f)}</string>`).join(`
4
4
  `),r=[n.command,...n.args].map(o=>` <string>${T(o)}</string>`).join(`
5
5
  `);return`<?xml version="1.0" encoding="UTF-8"?>
@@ -27,7 +27,7 @@ ${t}
27
27
  <key>StandardErrorPath</key>
28
28
  <string>${d}</string>
29
29
  </dict>
30
- </plist>`}const I=s.join(u.homedir(),".config/systemd/user/shennian.service");function Ke(){const e=ne(),n=A(),t=Object.entries(e).map(([o,f])=>`Environment=${o}=${f}`).join(`
30
+ </plist>`}const I=s.join(u.homedir(),".config/systemd/user/shennian.service");function Ke(){const e=ne(),n=P(),t=Object.entries(e).map(([o,f])=>`Environment=${o}=${f}`).join(`
31
31
  `),r=[n.command,...n.args].map(Ee).join(" ");return`[Unit]
32
32
  Description=Shennian Agent Daemon
33
33
  After=network.target
@@ -41,6 +41,6 @@ StandardOutput=append:${d}
41
41
  StandardError=append:${d}
42
42
 
43
43
  [Install]
44
- WantedBy=default.target`}const k="ShennianAgent";function Xe(e){a(e,{stdio:"pipe",windowsHide:!0})}function qe(){const e=process.getuid?.();if(typeof e=="number")return e;const n=u.userInfo();return typeof n.uid=="number"?n.uid:n.username}function te(e=qe()){return`gui/${e}/${_}`}function Ye(e=Xe){try{return e(`launchctl unload "${p}" 2>/dev/null; launchctl load -w "${p}"`),e(`launchctl kickstart -k "${te()}"`),!0}catch{try{return e(`launchctl load -w "${p}"`),e(`launchctl kickstart -k "${te()}"`),!0}catch{return!1}}}function ze(){i.mkdirSync(S,{recursive:!0});const e={};for(const[n,t]of Object.entries(process.env))t!==void 0&&De(n)&&(e[n]=t);i.writeFileSync(h("env.json"),JSON.stringify(e,null,2))}function re(e={}){i.mkdirSync(S,{recursive:!0});const n=m({cleanupStale:!0});if(n.pid!==null&&n.running&&!$(n)){e.quiet||console.log(c.green(`\u2713 Background service already running (PID ${n.pid})`));return}ie(n);const t=i.openSync(d,"a"),r=Fe(A()),o=de(r.command,r.args,Be(r,t));o.unref(),i.closeSync(t),He(o.pid),e.quiet||(console.log(c.green(`\u2713 Background service started (PID ${o.pid})`)),console.log(c.gray(` Logs: ${d}`)))}function $(e,n=P(),t=F()){return e.pid===null||!e.running?!1:e.adopted===!0||n?.version!==t}function Qe(e=l()){switch(e){case"darwin":return i.existsSync(p);case"linux":return i.existsSync(I);case"win32":return i.existsSync(N)&&i.existsSync(b)&&i.existsSync(R)}}function Ze(e,n,t,r=P(),o=F()){return!n.api?.trim()&&e.pid!==null&&e.running&&!e.stale&&!e.remoteAccessDisabled&&t&&!$(e,r,o)}function ie(e=m({cleanupStale:!0})){if(!(!$(e)||e.pid===null))try{process.kill(e.pid,"SIGTERM"),G(e.pid,5e3)||(process.kill(e.pid,"SIGKILL"),G(e.pid,2e3));try{P()?.pid===e.pid&&i.unlinkSync(g)}catch{}}catch{}}function oe(){i.mkdirSync(S,{recursive:!0});const e=l();switch(A().mode==="direct"&&O(B)&&console.warn(c.yellow("\u26A0 Warning: Current CLI path is temporary (npx). Auto-start may not work after reboot.\n Run `npm install -g shennian@latest` for reliable auto-start.")),e){case"darwin":{const t=s.dirname(p);return i.mkdirSync(t,{recursive:!0}),i.writeFileSync(p,Ge()),Ye()}case"linux":{const t=s.dirname(I);i.mkdirSync(t,{recursive:!0}),i.writeFileSync(I,Ke());try{a("systemctl --user daemon-reload && systemctl --user enable shennian",{stdio:"pipe",windowsHide:!0})}catch{return!1}try{a(`loginctl enable-linger ${u.userInfo().username}`,{stdio:"pipe",windowsHide:!0})}catch{}try{return a("systemctl --user restart shennian",{stdio:"pipe",windowsHide:!0}),!0}catch{return!1}}case"win32":return Ve()}}function en(){const n=m({cleanupStale:!0}).pid;if(n===null)return{};if(!E(n)){try{i.unlinkSync(g)}catch{}return{stalePid:n}}try{return process.kill(n,"SIGTERM"),{stoppedPid:n}}catch(t){return{error:t instanceof Error?t.message:String(t)}}}async function se(e=5e3){const n=en();if(!n.stoppedPid)return n;if(!await M(n.stoppedPid,e))try{process.kill(n.stoppedPid,"SIGKILL"),await M(n.stoppedPid,2e3)}catch(r){return{...n,error:r instanceof Error?r.message:String(r)}}try{i.unlinkSync(g)}catch{}Oe();for(const r of q())if(r!==n.stoppedPid)try{process.kill(r,"SIGTERM"),await M(r,2e3)}catch{}return n}function nn(e={}){Y(z(e.api)),Te(),ie(),oe()||re({quiet:e.json}),e.json?y(L(()=>m({cleanupStale:!0}))):console.log(c.green("\u2713 Background service started"))}async function tn(e={}){switch(i.mkdirSync(S,{recursive:!0}),i.writeFileSync(x,new Date().toISOString()),l()){case"darwin":{if(i.existsSync(p))try{a(`launchctl unload -w "${p}"`,{stdio:"pipe",windowsHide:!0})}catch{}break}case"linux":{try{a("systemctl --user disable --now shennian",{stdio:"pipe",windowsHide:!0})}catch{}break}case"win32":{try{a(`schtasks /change /tn "${k}" /disable`,{stdio:"pipe",windowsHide:!0})}catch{}try{a(`schtasks /end /tn "${k}"`,{stdio:"pipe",windowsHide:!0})}catch{}break}}const t=await se(),r=m({cleanupStale:!0});if(e.json){y(t.error?{...r,error:t.error}:r);return}t.error?console.error(c.red(`\u2717 Failed to stop: ${t.error}`)):console.log(c.green("\u2713 Background service stopped"))}function ce(e){const n=m({cleanupStale:!0});if(Ze(n,e,Qe())){e.json?y(n):console.log(c.green(`\u2713 Background service already running (PID ${n.pid})`));return}ze(),nn(e)}async function C(e={}){await tn(e)}async function M(e,n=5e3){const t=Date.now();for(;Date.now()-t<n;){if(!E(e))return!0;await new Promise(r=>setTimeout(r,100))}return!E(e)}async function rn(e={}){Y(z(e.api));const n=l();n==="win32"&&Je();const t=await se();if(t.error){e.json?y({...m(),error:t.error}):console.error(c.red(`\u2717 Failed to stop: ${t.error}`));return}if(t.stoppedPid!==void 0?e.json||console.log(c.green(`\u2713 Background service stopped (PID ${t.stoppedPid})`)):t.stalePid!==void 0?e.json||console.log(c.yellow(`\u26A0 Service process no longer exists, cleaned up stale PID ${t.stalePid}`)):e.json||console.log(c.gray("\u25CF Background service not running, starting it now")),(n==="linux"||n==="win32")&&oe()){e.json&&y(L(()=>m({cleanupStale:!0})));return}re({quiet:e.json}),e.json&&y(L(()=>m({cleanupStale:!0})))}function ae(e={}){const n=m({cleanupStale:!0});if(e.json){y(n);return}if(n.pid===null){console.log(c.gray("\u25CF Background service not running"));return}n.running?(console.log(c.green(`\u25CF Background service running (PID ${n.pid})`)),console.log(c.gray(` Logs: ${d}`))):console.log(c.yellow(`\u25CF Background service stopped (PID ${n.pid} is stale)`))}function ue(e){if(!i.existsSync(d)){console.log(c.gray("No logs yet"));return}try{const n=a(u.platform()==="win32"?`powershell Get-Content -Tail ${e.lines} "${d}"`:`tail -n ${e.lines} "${d}"`,{encoding:"utf-8",windowsHide:!0});process.stdout.write(n)}catch{const n=i.readFileSync(d,"utf-8").split(`
44
+ WantedBy=default.target`}const k="ShennianAgent";function Xe(e){a(e,{stdio:"pipe",windowsHide:!0})}function qe(){const e=process.getuid?.();if(typeof e=="number")return e;const n=u.userInfo();return typeof n.uid=="number"?n.uid:n.username}function te(e=qe()){return`gui/${e}/${_}`}function Ye(e=Xe){try{return e(`launchctl unload "${p}" 2>/dev/null; launchctl load -w "${p}"`),e(`launchctl kickstart -k "${te()}"`),!0}catch{try{return e(`launchctl load -w "${p}"`),e(`launchctl kickstart -k "${te()}"`),!0}catch{return!1}}}function ze(){i.mkdirSync(S,{recursive:!0});const e={};for(const[n,t]of Object.entries(process.env))t!==void 0&&Ne(n)&&(e[n]=t);i.writeFileSync(h("env.json"),JSON.stringify(e,null,2))}function re(e={}){i.mkdirSync(S,{recursive:!0});const n=m({cleanupStale:!0});if(n.pid!==null&&n.running&&!$(n)){e.quiet||console.log(c.green(`\u2713 Background service already running (PID ${n.pid})`));return}ie(n);const t=i.openSync(d,"a"),r=Fe(P()),o=de(r.command,r.args,Be(r,t));o.unref(),i.closeSync(t),He(o.pid),e.quiet||(console.log(c.green(`\u2713 Background service started (PID ${o.pid})`)),console.log(c.gray(` Logs: ${d}`)))}function $(e,n=A(),t=F()){return e.pid===null||!e.running?!1:e.adopted===!0||n?.version!==t}function Qe(e=l()){switch(e){case"darwin":return i.existsSync(p);case"linux":return i.existsSync(I);case"win32":return i.existsSync(D)&&i.existsSync(b)&&i.existsSync(R)}}function Ze(e,n,t,r=A(),o=F()){return!n.api?.trim()&&e.pid!==null&&e.running&&!e.stale&&!e.remoteAccessDisabled&&t&&!$(e,r,o)}function ie(e=m({cleanupStale:!0})){if(!(!$(e)||e.pid===null))try{process.kill(e.pid,"SIGTERM"),G(e.pid,5e3)||(process.kill(e.pid,"SIGKILL"),G(e.pid,2e3));try{A()?.pid===e.pid&&i.unlinkSync(g)}catch{}}catch{}}function oe(){i.mkdirSync(S,{recursive:!0});const e=l();switch(P().mode==="direct"&&O(B)&&console.warn(c.yellow("\u26A0 Warning: Current CLI path is temporary (npx). Auto-start may not work after reboot.\n Run `npm install -g shennian@latest` for reliable auto-start.")),e){case"darwin":{const t=s.dirname(p);return i.mkdirSync(t,{recursive:!0}),i.writeFileSync(p,Ge()),Ye()}case"linux":{const t=s.dirname(I);i.mkdirSync(t,{recursive:!0}),i.writeFileSync(I,Ke());try{a("systemctl --user daemon-reload && systemctl --user enable shennian",{stdio:"pipe",windowsHide:!0})}catch{return!1}try{a(`loginctl enable-linger ${u.userInfo().username}`,{stdio:"pipe",windowsHide:!0})}catch{}try{return a("systemctl --user restart shennian",{stdio:"pipe",windowsHide:!0}),!0}catch{return!1}}case"win32":return Ve()}}function en(){const n=m({cleanupStale:!0}).pid;if(n===null)return{};if(!E(n)){try{i.unlinkSync(g)}catch{}return{stalePid:n}}try{return process.kill(n,"SIGTERM"),{stoppedPid:n}}catch(t){return{error:t instanceof Error?t.message:String(t)}}}async function se(e=5e3){const n=en();if(!n.stoppedPid)return n;if(!await M(n.stoppedPid,e))try{process.kill(n.stoppedPid,"SIGKILL"),await M(n.stoppedPid,2e3)}catch(r){return{...n,error:r instanceof Error?r.message:String(r)}}try{i.unlinkSync(g)}catch{}Oe();for(const r of q())if(r!==n.stoppedPid)try{process.kill(r,"SIGTERM"),await M(r,2e3)}catch{}return n}function nn(e={}){Y(z(e.api)),Te(),ie(),oe()||re({quiet:e.json}),e.json?y(L(()=>m({cleanupStale:!0}))):console.log(c.green("\u2713 Background service started"))}async function tn(e={}){switch(i.mkdirSync(S,{recursive:!0}),i.writeFileSync(x,new Date().toISOString()),l()){case"darwin":{if(i.existsSync(p))try{a(`launchctl unload -w "${p}"`,{stdio:"pipe",windowsHide:!0})}catch{}break}case"linux":{try{a("systemctl --user disable --now shennian",{stdio:"pipe",windowsHide:!0})}catch{}break}case"win32":{try{a(`schtasks /change /tn "${k}" /disable`,{stdio:"pipe",windowsHide:!0})}catch{}try{a(`schtasks /end /tn "${k}"`,{stdio:"pipe",windowsHide:!0})}catch{}break}}const t=await se(),r=m({cleanupStale:!0});if(e.json){y(t.error?{...r,error:t.error}:r);return}t.error?console.error(c.red(`\u2717 Failed to stop: ${t.error}`)):console.log(c.green("\u2713 Background service stopped"))}function ce(e){const n=m({cleanupStale:!0});if(Ze(n,e,Qe())){e.json?y(n):console.log(c.green(`\u2713 Background service already running (PID ${n.pid})`));return}ze(),nn(e)}async function C(e={}){await tn(e)}async function M(e,n=5e3){const t=Date.now();for(;Date.now()-t<n;){if(!E(e))return!0;await new Promise(r=>setTimeout(r,100))}return!E(e)}async function rn(e={}){Y(z(e.api));const n=l();n==="win32"&&Je();const t=await se();if(t.error){e.json?y({...m(),error:t.error}):console.error(c.red(`\u2717 Failed to stop: ${t.error}`));return}if(t.stoppedPid!==void 0?e.json||console.log(c.green(`\u2713 Background service stopped (PID ${t.stoppedPid})`)):t.stalePid!==void 0?e.json||console.log(c.yellow(`\u26A0 Service process no longer exists, cleaned up stale PID ${t.stalePid}`)):e.json||console.log(c.gray("\u25CF Background service not running, starting it now")),(n==="linux"||n==="win32")&&oe()){e.json&&y(L(()=>m({cleanupStale:!0})));return}re({quiet:e.json}),e.json&&y(L(()=>m({cleanupStale:!0})))}function ae(e={}){const n=m({cleanupStale:!0});if(e.json){y(n);return}if(n.pid===null){console.log(c.gray("\u25CF Background service not running"));return}n.running?(console.log(c.green(`\u25CF Background service running (PID ${n.pid})`)),console.log(c.gray(` Logs: ${d}`))):console.log(c.yellow(`\u25CF Background service stopped (PID ${n.pid} is stale)`))}function ue(e){if(!i.existsSync(d)){console.log(c.gray("No logs yet"));return}try{const n=a(u.platform()==="win32"?`powershell Get-Content -Tail ${e.lines} "${d}"`:`tail -n ${e.lines} "${d}"`,{encoding:"utf-8",windowsHide:!0});process.stdout.write(n)}catch{const n=i.readFileSync(d,"utf-8").split(`
45
45
  `).slice(-e.lines);console.log(n.join(`
46
- `))}}async function on(){switch(await C(),l()){case"darwin":{if(i.existsSync(p)){try{a(`launchctl unload -w "${p}"`,{stdio:"pipe",windowsHide:!0})}catch{}i.unlinkSync(p),console.log(c.green("\u2713 Auto-start uninstalled (launchd)"))}break}case"linux":{try{a("systemctl --user disable --now shennian",{stdio:"pipe",windowsHide:!0})}catch{}if(i.existsSync(I)){i.unlinkSync(I);try{a("systemctl --user daemon-reload",{stdio:"pipe",windowsHide:!0})}catch{}}console.log(c.green("\u2713 Auto-start uninstalled (systemd)"));break}case"win32":{Z();try{i.unlinkSync(N)}catch{}try{i.unlinkSync(b)}catch{}try{i.unlinkSync(R)}catch{}ee(),console.log(c.green("\u2713 Auto-start uninstalled (Task Scheduler)"));break}}}function vn(e){e.command("start").description("Start the background service").option("--json","Print machine-readable status JSON").option("--api <url>","Server URL override").action(r=>ce(r)),e.command("stop").description("Stop the background service").option("--json","Print machine-readable status JSON").action(r=>C(r)),e.command("status").description("Show background service status").option("--json","Print machine-readable status JSON").action(r=>ae(r)),e.command("logs").description("Show recent logs").option("-n, --lines <n>","Number of lines","50").action(r=>ue({lines:parseInt(r.lines,10)}));const n=e.command("daemon",{hidden:!0}).description("Deprecated: use top-level start/stop/status/logs"),t=(r,o)=>{o||console.error(c.yellow(`\u26A0 Deprecated command. Use \`shennian ${r}\` instead.`))};n.command("start").description("Start the background service").option("--json","Print machine-readable status JSON").option("--api <url>","Server URL override").action(r=>{t("start",r.json),ce(r)}),n.command("stop").description("Stop the background service").option("--json","Print machine-readable status JSON").action(async r=>{t("stop",r.json),await C(r)}),n.command("restart").description("Restart the background service").option("--json","Print machine-readable status JSON").option("--api <url>","Server URL override").action(r=>{t("stop && shennian start",r.json),rn(r)}),n.command("status").description("Show background service status").option("--json","Print machine-readable status JSON").action(r=>{t("status",r.json),ae(r)}),n.command("logs").description("Show recent logs").option("-n, --lines <n>","Number of lines","50").action(r=>{t("logs"),ue({lines:parseInt(r.lines,10)})}),n.command("uninstall").description("Uninstall auto-start service").action(on)}export{Be as buildDaemonSpawnOptions,An as buildWindowsLauncherCommand,In as buildWindowsScheduledTaskCommands,kn as buildWindowsScheduledTaskSuspendCommands,Dn as buildWindowsScheduledTaskXml,Nn as buildWindowsStartupVbs,Ze as canReuseRunningDaemonForStart,ne as captureEnvForService,Oe as clearDaemonLauncher,yn as clearDaemonPidIfOwner,Te as clearRemoteAccessDisabled,Le as createDaemonInstanceId,q as findRunningDaemonProcessIds,K as getCurrentProcessDaemonLauncher,m as getDaemonStatus,te as getLaunchdServiceTarget,oe as installService,Qe as isAutoStartServicePresent,O as isEphemeralCliPath,$e as isIsolatedDaemonE2eInstance,Re as isRemoteAccessDisabled,De as isSafeSnapshotEnvKey,Tn as isShennianRunServiceCommand,He as recordStartedDaemon,vn as registerDaemonCommand,Ye as reloadLaunchdAgent,Ne as resolveServiceLaunchSpec,ze as saveEnvSnapshot,Ce as shouldDiscoverGlobalDaemonProcesses,$ as shouldReplaceRunningDaemon,re as startDaemonProcess,bn as suspendWindowsScheduledTask,X as writeDaemonLauncher,xe as writeDaemonPid};
46
+ `))}}async function on(){switch(await C(),l()){case"darwin":{if(i.existsSync(p)){try{a(`launchctl unload -w "${p}"`,{stdio:"pipe",windowsHide:!0})}catch{}i.unlinkSync(p),console.log(c.green("\u2713 Auto-start uninstalled (launchd)"))}break}case"linux":{try{a("systemctl --user disable --now shennian",{stdio:"pipe",windowsHide:!0})}catch{}if(i.existsSync(I)){i.unlinkSync(I);try{a("systemctl --user daemon-reload",{stdio:"pipe",windowsHide:!0})}catch{}}console.log(c.green("\u2713 Auto-start uninstalled (systemd)"));break}case"win32":{Z();try{i.unlinkSync(D)}catch{}try{i.unlinkSync(b)}catch{}try{i.unlinkSync(R)}catch{}ee(),console.log(c.green("\u2713 Auto-start uninstalled (Task Scheduler)"));break}}}function vn(e){e.command("start").description("Start the background service").option("--json","Print machine-readable status JSON").option("--api <url>","Server URL override").action(r=>ce(r)),e.command("stop").description("Stop the background service").option("--json","Print machine-readable status JSON").action(r=>C(r)),e.command("status").description("Show background service status").option("--json","Print machine-readable status JSON").action(r=>ae(r)),e.command("logs").description("Show recent logs").option("-n, --lines <n>","Number of lines","50").action(r=>ue({lines:parseInt(r.lines,10)}));const n=e.command("daemon",{hidden:!0}).description("Deprecated: use top-level start/stop/status/logs"),t=(r,o)=>{o||console.error(c.yellow(`\u26A0 Deprecated command. Use \`shennian ${r}\` instead.`))};n.command("start").description("Start the background service").option("--json","Print machine-readable status JSON").option("--api <url>","Server URL override").action(r=>{t("start",r.json),ce(r)}),n.command("stop").description("Stop the background service").option("--json","Print machine-readable status JSON").action(async r=>{t("stop",r.json),await C(r)}),n.command("restart").description("Restart the background service").option("--json","Print machine-readable status JSON").option("--api <url>","Server URL override").action(r=>{t("stop && shennian start",r.json),rn(r)}),n.command("status").description("Show background service status").option("--json","Print machine-readable status JSON").action(r=>{t("status",r.json),ae(r)}),n.command("logs").description("Show recent logs").option("-n, --lines <n>","Number of lines","50").action(r=>{t("logs"),ue({lines:parseInt(r.lines,10)})}),n.command("uninstall").description("Uninstall auto-start service").action(on)}export{Be as buildDaemonSpawnOptions,Pn as buildWindowsLauncherCommand,In as buildWindowsScheduledTaskCommands,kn as buildWindowsScheduledTaskSuspendCommands,Nn as buildWindowsScheduledTaskXml,Dn as buildWindowsStartupVbs,Ze as canReuseRunningDaemonForStart,ne as captureEnvForService,Oe as clearDaemonLauncher,yn as clearDaemonPidIfOwner,Te as clearRemoteAccessDisabled,Le as createDaemonInstanceId,q as findRunningDaemonProcessIds,K as getCurrentProcessDaemonLauncher,m as getDaemonStatus,te as getLaunchdServiceTarget,oe as installService,Qe as isAutoStartServicePresent,O as isEphemeralCliPath,$e as isIsolatedDaemonE2eInstance,Re as isRemoteAccessDisabled,Ne as isSafeSnapshotEnvKey,Tn as isShennianRunServiceCommand,He as recordStartedDaemon,vn as registerDaemonCommand,Ye as reloadLaunchdAgent,De as resolveServiceLaunchSpec,ze as saveEnvSnapshot,Ce as shouldDiscoverGlobalDaemonProcesses,$ as shouldReplaceRunningDaemon,re as startDaemonProcess,bn as suspendWindowsScheduledTask,X as writeDaemonLauncher,xe as writeDaemonPid};
@@ -24,6 +24,7 @@ export declare function runBrowserPairFlow(input: {
24
24
  }): Promise<{
25
25
  machineId: string;
26
26
  machineToken: string;
27
+ accountNamespaceId: string;
27
28
  machineName: string;
28
29
  publicLoginUrl: string;
29
30
  }>;
@@ -1 +1 @@
1
- import I from"node:crypto";import{spawn as u}from"node:child_process";import{HttpDeviceAuthorizationClient as f}from"../room/device-authorization-client.js";import{hashActionPayload as A,hashPendingDeviceSecret as g}from"./pair-authorization.js";const p=3e3;class n extends Error{code;constructor(r,t){super(t),this.code=r,this.name="BrowserPairError"}}async function E(e){const r=new f(e.serverUrl),t=I.randomBytes(32).toString("base64url"),a={action:"pair_machine",agentList:[...e.agentList].sort(),machineName:e.machineName,machineOs:e.machineOs},o=A(a),i=await r.create({deviceSecretHash:g(t),machineName:e.machineName,machineOs:e.machineOs,agentList:e.agentList,actionType:"machine.pair",actionSummary:"Pair this machine with your Shennian account",actionPayloadHash:o,returnTo:"/spaces"}),d=_(i.publicLoginUrl,e.serverUrl);e.emit?.({type:"pair.browser.created",intentId:i.loginIntentId,url:d,expiresAt:i.expiresAt}),await(e.openUrl??P)(d).catch(()=>!1);const l=new Date(i.expiresAt).getTime(),h=e.pollIntervalMs??p;let m=!1;for(;Date.now()<l;){if(e.signal?.aborted)throw await r.cancel({loginIntentId:i.loginIntentId,deviceSecret:t}).catch(()=>{}),new n("PAIR_CANCELLED","Pairing was cancelled");let s;try{s=await r.getStatus({loginIntentId:i.loginIntentId,deviceSecret:t})}catch{await w(h,e.signal);continue}if(s.actionPayloadHash!==o)throw new n("PAIR_PAYLOAD_MISMATCH","Authorization request changed unexpectedly");if(s.status==="denied")throw new n("PAIR_DENIED","Pairing request was denied");if(s.status==="expired")throw new n("PAIR_EXPIRED","Pairing request expired");if(s.status==="claimed")throw new n("PAIR_ALREADY_CLAIMED","Pairing credentials were already claimed");if(s.status==="approved"){const c=await r.claim({loginIntentId:i.loginIntentId,deviceSecret:t});if(c.actionPayloadHash!==o||!c.machineId||!c.machineToken)throw new n("PAIR_INVALID_CLAIM","Authorization did not return machine credentials");return e.emit?.({type:"pair.claimed"}),{machineId:c.machineId,machineToken:c.machineToken,machineName:e.machineName,publicLoginUrl:d}}m||(m=!0,e.emit?.({type:"pair.waiting"})),await w(h,e.signal)}throw new n("PAIR_TIMEOUT","Timed out waiting for browser authorization")}function N(e){if(e.browser&&e.token)throw new n("PAIR_MODE_CONFLICT","Choose either --browser or --token, not both");return e.browser?"browser":e.token||e.json||!e.stdinIsTty||!e.stdoutIsTty?"token":"prompt"}function O(e){return e.trim()==="2"?"token":"browser"}async function P(e){if(process.env.SSH_CONNECTION||process.env.SSH_TTY)return!1;const{command:r,args:t}=L(process.platform,e);return new Promise(a=>{const o=u(r,t,{detached:!0,stdio:"ignore",windowsHide:!0});o.once("error",()=>a(!1)),o.once("spawn",()=>{o.unref(),a(!0)})})}function L(e,r){return e==="darwin"?{command:"open",args:[r]}:e==="win32"?{command:"explorer.exe",args:[r]}:{command:"xdg-open",args:[r]}}function _(e,r){let t,a;try{t=new URL(e),a=new URL(r)}catch{throw new n("PAIR_INVALID_AUTHORIZATION_URL","Server returned an invalid authorization URL")}if(!["http:","https:"].includes(t.protocol)||t.hostname!==a.hostname||t.username||t.password||t.search||t.hash||!/^\/authorize\/[A-Za-z0-9_-]{8,128}$/.test(t.pathname))throw new n("PAIR_INVALID_AUTHORIZATION_URL","Server returned an unsafe authorization URL");return t.toString()}function w(e,r){return new Promise(t=>{if(r?.aborted)return t();const a=setTimeout(t,e);r?.addEventListener("abort",()=>{clearTimeout(a),t()},{once:!0})})}export{n as BrowserPairError,L as browserOpenCommand,_ as normalizePairAuthorizationUrl,P as openExternalUrl,O as parsePairModeChoice,N as resolvePairMode,E as runBrowserPairFlow};
1
+ import I from"node:crypto";import{spawn as u}from"node:child_process";import{HttpDeviceAuthorizationClient as f}from"../room/device-authorization-client.js";import{hashActionPayload as p,hashPendingDeviceSecret as A}from"./pair-authorization.js";const g=3e3;class n extends Error{code;constructor(r,t){super(t),this.code=r,this.name="BrowserPairError"}}async function x(e){const r=new f(e.serverUrl),t=I.randomBytes(32).toString("base64url"),a={action:"pair_machine",agentList:[...e.agentList].sort(),machineName:e.machineName,machineOs:e.machineOs},o=p(a),i=await r.create({deviceSecretHash:A(t),machineName:e.machineName,machineOs:e.machineOs,agentList:e.agentList,actionType:"machine.pair",actionSummary:"Pair this machine with your Shennian account",actionPayloadHash:o,returnTo:"/spaces"}),d=_(i.publicLoginUrl,e.serverUrl);e.emit?.({type:"pair.browser.created",intentId:i.loginIntentId,url:d,expiresAt:i.expiresAt}),await(e.openUrl??P)(d).catch(()=>!1);const l=new Date(i.expiresAt).getTime(),m=e.pollIntervalMs??g;let h=!1;for(;Date.now()<l;){if(e.signal?.aborted)throw await r.cancel({loginIntentId:i.loginIntentId,deviceSecret:t}).catch(()=>{}),new n("PAIR_CANCELLED","Pairing was cancelled");let c;try{c=await r.getStatus({loginIntentId:i.loginIntentId,deviceSecret:t})}catch{await w(m,e.signal);continue}if(c.actionPayloadHash!==o)throw new n("PAIR_PAYLOAD_MISMATCH","Authorization request changed unexpectedly");if(c.status==="denied")throw new n("PAIR_DENIED","Pairing request was denied");if(c.status==="expired")throw new n("PAIR_EXPIRED","Pairing request expired");if(c.status==="claimed")throw new n("PAIR_ALREADY_CLAIMED","Pairing credentials were already claimed");if(c.status==="approved"){const s=await r.claim({loginIntentId:i.loginIntentId,deviceSecret:t});if(s.actionPayloadHash!==o||!s.machineId||!s.machineToken||!s.accountNamespaceId)throw new n("PAIR_INVALID_CLAIM","Authorization did not return machine credentials");return e.emit?.({type:"pair.claimed"}),{machineId:s.machineId,machineToken:s.machineToken,accountNamespaceId:s.accountNamespaceId,machineName:e.machineName,publicLoginUrl:d}}h||(h=!0,e.emit?.({type:"pair.waiting"})),await w(m,e.signal)}throw new n("PAIR_TIMEOUT","Timed out waiting for browser authorization")}function E(e){if(e.browser&&e.token)throw new n("PAIR_MODE_CONFLICT","Choose either --browser or --token, not both");return e.browser?"browser":e.token||e.json||!e.stdinIsTty||!e.stdoutIsTty?"token":"prompt"}function O(e){return e.trim()==="2"?"token":"browser"}async function P(e){if(process.env.SSH_CONNECTION||process.env.SSH_TTY)return!1;const{command:r,args:t}=L(process.platform,e);return new Promise(a=>{const o=u(r,t,{detached:!0,stdio:"ignore",windowsHide:!0});o.once("error",()=>a(!1)),o.once("spawn",()=>{o.unref(),a(!0)})})}function L(e,r){return e==="darwin"?{command:"open",args:[r]}:e==="win32"?{command:"explorer.exe",args:[r]}:{command:"xdg-open",args:[r]}}function _(e,r){let t,a;try{t=new URL(e),a=new URL(r)}catch{throw new n("PAIR_INVALID_AUTHORIZATION_URL","Server returned an invalid authorization URL")}if(!["http:","https:"].includes(t.protocol)||t.hostname!==a.hostname||t.username||t.password||t.search||t.hash||!/^\/authorize\/[A-Za-z0-9_-]{8,128}$/.test(t.pathname))throw new n("PAIR_INVALID_AUTHORIZATION_URL","Server returned an unsafe authorization URL");return t.toString()}function w(e,r){return new Promise(t=>{if(r?.aborted)return t();const a=setTimeout(t,e);r?.addEventListener("abort",()=>{clearTimeout(a),t()},{once:!0})})}export{n as BrowserPairError,L as browserOpenCommand,_ as normalizePairAuthorizationUrl,P as openExternalUrl,O as parsePairModeChoice,E as resolvePairMode,x as runBrowserPairFlow};
@@ -1,12 +1,12 @@
1
- import r from"chalk";import{getDaemonInstallationId as D}from"../personal-sync/identity.js";import k from"node:os";import N from"node:readline";import{loadConfig as p,saveConfig as v}from"../config/index.js";import{detectAgents as E}from"../agents/detect.js";import{clearRemoteAccessDisabled as A,getDaemonStatus as L,startDaemonProcess as b,installService as O,saveEnvSnapshot as x}from"./daemon.js";import{waitForDaemonReadyStatus as B}from"./daemon-start-wait.js";import{detectAndChooseServer as P}from"../region.js";import{buildPairQrPayload as M,PAIR_QR_RENDER_OPTIONS as F}from"./pair-qr.js";import{BrowserPairError as J,parsePairModeChoice as Q,resolvePairMode as C,runBrowserPairFlow as q}from"./pair-browser.js";const z=3e3,U=600*1e3;function l(o){process.stdout.write(`${JSON.stringify(o)}
2
- `)}function g(o,e){l({type:"error",code:o,message:e}),process.exit(1)}function K(o){const e="\u2500".repeat(o.length+4);console.log(r.cyan(`\u250C${e}\u2510`)),console.log(r.cyan("\u2502 ")+r.bold.yellow(o)+r.cyan(" \u2502")),console.log(r.cyan(`\u2514${e}\u2518`))}async function W(o){const e=(await import("qrcode-terminal")).default;return new Promise(n=>{e.generate(o,F,t=>{console.log(t),n()})})}async function Y(o,e,n={}){const t=Date.now()+U;for(n.json&&l({type:"pair.waiting"});Date.now()<t;){await new Promise(d=>setTimeout(d,z));let i;try{i=await fetch(`${o}/api/machines/pair/status?token=${encodeURIComponent(e)}`)}catch{continue}if(i.status===410&&(n.json?g("PAIR_EXPIRED","Pairing code expired"):(console.error(r.red(`
3
- \u2717 Pairing code expired. Please re-run: shennian pair`)),process.exit(1))),!i.ok)continue;const a=await i.json();if(a.status==="claimed"&&a.machineToken&&a.machineId)return n.json&&l({type:"pair.claimed"}),{machineToken:a.machineToken,machineId:a.machineId,machineName:a.machineName??""}}return null}function H(o){const e=N.createInterface({input:process.stdin,output:process.stdout});return new Promise(n=>{e.question(o,t=>{e.close(),n(t.trim().toLowerCase()==="y")})})}function j(){const o=N.createInterface({input:process.stdin,output:process.stdout});return console.log(r.white("Choose how to pair this machine:")),console.log(r.cyan(" 1. Browser authorization (recommended)")),console.log(r.gray(" 2. QR code / pairing token (servers and remote terminals)")),new Promise(e=>{o.question("Selection (1/2) [1]: ",n=>{o.close(),e(Q(n))})})}async function $(o){const{serverUrl:e,machineName:n,force:t,json:i}=o,a=p();if(a.machineToken&&a.machineId&&!t){i?l({type:"done",status:"already-paired",machineId:a.machineId}):(console.log(r.green(`\u2713 Already paired (machine ID: ${a.machineId})`)),console.log(r.gray(' Run "shennian pair" to re-pair to another account')));return}const s=E().map(h=>h.type),m=k.platform(),f=m==="win32"?"windows":m==="darwin"?"macos":"linux";i||(console.log(r.bold(`
1
+ import r from"chalk";import{getDaemonInstallationId as D}from"../personal-sync/identity.js";import k from"node:os";import R from"node:readline";import{loadConfig as u,saveConfig as N}from"../config/index.js";import{detectAgents as E}from"../agents/detect.js";import{clearRemoteAccessDisabled as A,getDaemonStatus as L,startDaemonProcess as b,installService as O,saveEnvSnapshot as x}from"./daemon.js";import{waitForDaemonReadyStatus as B}from"./daemon-start-wait.js";import{detectAndChooseServer as v}from"../region.js";import{buildPairQrPayload as M,PAIR_QR_RENDER_OPTIONS as F}from"./pair-qr.js";import{BrowserPairError as J,parsePairModeChoice as Q,resolvePairMode as C,runBrowserPairFlow as q}from"./pair-browser.js";const z=3e3,U=600*1e3;function d(o){process.stdout.write(`${JSON.stringify(o)}
2
+ `)}function g(o,e){d({type:"error",code:o,message:e}),process.exit(1)}function K(o){const e="\u2500".repeat(o.length+4);console.log(r.cyan(`\u250C${e}\u2510`)),console.log(r.cyan("\u2502 ")+r.bold.yellow(o)+r.cyan(" \u2502")),console.log(r.cyan(`\u2514${e}\u2518`))}async function W(o){const e=(await import("qrcode-terminal")).default;return new Promise(n=>{e.generate(o,F,i=>{console.log(i),n()})})}async function Y(o,e,n={}){const i=Date.now()+U;for(n.json&&d({type:"pair.waiting"});Date.now()<i;){await new Promise(l=>setTimeout(l,z));let a;try{a=await fetch(`${o}/api/machines/pair/status?token=${encodeURIComponent(e)}`)}catch{continue}if(a.status===410&&(n.json?g("PAIR_EXPIRED","Pairing code expired"):(console.error(r.red(`
3
+ \u2717 Pairing code expired. Please re-run: shennian pair`)),process.exit(1))),!a.ok)continue;const t=await a.json();if(t.status==="claimed"&&t.machineToken&&t.machineId&&t.accountNamespaceId)return n.json&&d({type:"pair.claimed"}),{machineToken:t.machineToken,machineId:t.machineId,machineName:t.machineName??"",accountNamespaceId:t.accountNamespaceId}}return null}function H(o){const e=R.createInterface({input:process.stdin,output:process.stdout});return new Promise(n=>{e.question(o,i=>{e.close(),n(i.trim().toLowerCase()==="y")})})}function j(){const o=R.createInterface({input:process.stdin,output:process.stdout});return console.log(r.white("Choose how to pair this machine:")),console.log(r.cyan(" 1. Browser authorization (recommended)")),console.log(r.gray(" 2. QR code / pairing token (servers and remote terminals)")),new Promise(e=>{o.question("Selection (1/2) [1]: ",n=>{o.close(),e(Q(n))})})}async function $(o){const{serverUrl:e,machineName:n,force:i,json:a}=o,t=u();if(t.machineToken&&t.machineId&&!i){a?d({type:"done",status:"already-paired",machineId:t.machineId}):(console.log(r.green(`\u2713 Already paired (machine ID: ${t.machineId})`)),console.log(r.gray(' Run "shennian pair" to re-pair to another account')));return}const c=E().map(p=>p.type),m=k.platform(),f=m==="win32"?"windows":m==="darwin"?"macos":"linux";a||(console.log(r.bold(`
4
4
  Shennian CLI \u2014 Pairing
5
- `)),console.log(r.gray(` Machine: ${n} | OS: ${f} | Agents: ${s.join(", ")||"none"}`)),console.log());let S=o.mode??C({json:i,stdinIsTty:!!process.stdin.isTTY,stdoutIsTty:!!process.stdout.isTTY});if(S==="prompt"&&(S=await j()),S==="browser"){const h=new AbortController,I=()=>h.abort();process.once("SIGINT",I);try{const c=await q({serverUrl:e,machineName:n,machineOs:f,agentList:s,signal:h.signal,openUrl:o.openUrl,emit:T=>{i?l(T):T.type==="pair.browser.created"&&(console.log(r.white("Approve this machine in your browser:")),console.log(r.cyan.underline(T.url)),console.log(r.gray(`
6
- If the browser did not open, copy this URL to any signed-in device.`)),console.log(r.gray("Waiting for authorization... (Ctrl+C to cancel)")))}});v({...a,machineToken:c.machineToken,machineId:c.machineId,serverUrl:e}),i?l({type:"pair.saved",machineId:c.machineId}):console.log(r.green(`
7
- \u2713 Paired successfully! Machine "${c.machineName}" is now linked.`));return}catch(c){const T=c instanceof J||typeof c=="object"&&c&&"code"in c&&typeof c.code=="string"?c.code:"PAIR_BROWSER_FAILED",R=c instanceof Error?c.message:"Browser pairing failed";i&&g(T,R),console.error(r.red(`
8
- \u2717 ${R}`)),process.exit(1)}finally{process.removeListener("SIGINT",I)}}let u;try{u=await fetch(`${e}/api/machines/pair/create`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,os:f,agentList:s,daemonInstallationId:D()})})}catch{const h=`Network error: cannot connect to ${e}`;i&&g("NETWORK_ERROR",h),console.error(r.red(`\u2717 ${h}`)),process.exit(1)}if(!u.ok){const I=`Failed to create machine: ${(await u.json().catch(()=>({}))).error??u.statusText}`;i&&g("PAIR_CREATE_FAILED",I),console.error(r.red(`\u2717 ${I}`)),process.exit(1)}const y=await u.json(),_=M(y.pairToken);i?l({type:"pair.created",code:y.pairToken,machineId:y.machineId,expiresAt:new Date(Date.now()+U).toISOString()}):(console.log(r.white("Scan QR code or enter the pairing code in the Shennian app:")),console.log(),await W(_),K(y.pairToken),console.log(),console.log(r.gray("Waiting for pairing... (Ctrl+C to cancel)")));const w=await Y(e,y.pairToken,{json:i});w||(i&&g("PAIR_TIMEOUT","Timed out waiting for pairing"),console.error(r.red(`
9
- \u2717 Timed out. Pairing code expired, please try again.`)),process.exit(1)),v({...a,machineToken:w.machineToken,machineId:w.machineId,serverUrl:e}),i&&l({type:"pair.saved",machineId:w.machineId}),i||console.log(r.green(`
10
- \u2713 Paired successfully! Machine "${w.machineName}" is now linked.`))}async function G(o){const e=p(),t=E().map(m=>m.type),i=k.platform(),a=i==="win32"?"windows":i==="darwin"?"macos":"linux",d=await fetch(`${o.serverUrl}/api/machines/desktop/register`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o.authToken}`},body:JSON.stringify({machineId:e.machineId,name:o.machineName,os:a,agentList:t})});if(!d.ok){const f=(await d.json().catch(()=>({}))).error??d.statusText;throw new Error(`Failed to register desktop machine: ${f}`)}const s=await d.json();if(!s.machine?.id||!s.machineToken)throw new Error("Failed to register desktop machine: missing machine token");return e.machineId=s.machine.id,e.machineToken=s.machineToken,e.serverUrl=o.serverUrl,v(e),{machineId:s.machine.id,machineName:s.machine.name}}async function V(o){const e=o?.trim();if(e)return e;const n=process.env.SHENNIAN_DESKTOP_SERVER_URL?.trim();if(n)return n;const t=p().serverUrl?.trim();return t||P()}async function le(o,e){const n=p();if(!n.machineToken||!n.machineId){const i=o??await P();await $({serverUrl:i,machineName:e,force:!1})}else console.log(r.green(`\u2713 Already paired (machine ID: ${n.machineId})`));x(),A(),console.log(r.gray(`
11
- Starting background service...`)),O()||b()}function de(o){o.command("desktop-register",{hidden:!0}).description("Register this desktop-managed machine to the authenticated account").option("--api <url>","Server URL").option("--name <name>","Machine name",k.hostname()).action(async e=>{try{const n=process.env.SHENNIAN_DESKTOP_AUTH_TOKEN;if(!n)throw new Error("Missing desktop auth token");const t=await V(e.api),i=await G({serverUrl:t,authToken:n,machineName:e.name});console.log(JSON.stringify({ok:!0,...i}))}catch(n){console.error(n instanceof Error?n.message:String(n)),process.exit(1)}}),o.command("pair").description("Pair this machine to a Shennian account (re-pair if already paired)").option("--api <url>","Server URL override").option("--server <url>","Server URL override").option("--name <name>","Machine name",k.hostname()).option("--json","Emit machine-readable JSONL events").option("--browser","Pair by approving a browser authorization request").option("--token","Pair with the existing QR code / token flow").option("--force","Force re-pair even when a machine token exists").action(async e=>{const n=p();let t;try{t=C({browser:e.browser,token:e.token,json:e.json,stdinIsTty:!!process.stdin.isTTY,stdoutIsTty:!!process.stdout.isTTY})}catch(s){const m=s instanceof Error?s.message:"Invalid pairing mode";e.json&&g("PAIR_MODE_CONFLICT",m),console.error(r.red(`\u2717 ${m}`)),process.exit(1)}if(t==="prompt"&&(t=await j()),n.machineToken&&n.machineId&&!e.force){if(e.json){l({type:"done",status:"already-paired",machineId:n.machineId});return}if(console.log(r.yellow(`Currently paired (machine ID: ${n.machineId})`)),!await H("Re-pairing will unlink the current account. Continue? (y/N) ")){console.log(r.gray("Cancelled"));return}}const i=e.server??e.api??await P(),a=!!(e.force||n.machineToken&&n.machineId);await $({serverUrl:i,machineName:e.name,force:a,json:!!e.json,mode:t}),x(),A(),e.json?l({type:"daemon.starting"}):console.log(r.gray(`
12
- Starting background service...`)),O()||b({quiet:!!e.json}),e.json&&(B(()=>L({cleanupStale:!0})),l({type:"done",status:"paired",machineId:p().machineId}))})}export{G as registerDesktopMachine,de as registerPairCommand,$ as runPairFlow,le as runSmartStart};
5
+ `)),console.log(r.gray(` Machine: ${n} | OS: ${f} | Agents: ${c.join(", ")||"none"}`)),console.log());let S=o.mode??C({json:a,stdinIsTty:!!process.stdin.isTTY,stdoutIsTty:!!process.stdout.isTTY});if(S==="prompt"&&(S=await j()),S==="browser"){const p=new AbortController,w=()=>p.abort();process.once("SIGINT",w);try{const s=await q({serverUrl:e,machineName:n,machineOs:f,agentList:c,signal:p.signal,openUrl:o.openUrl,emit:T=>{a?d(T):T.type==="pair.browser.created"&&(console.log(r.white("Approve this machine in your browser:")),console.log(r.cyan.underline(T.url)),console.log(r.gray(`
6
+ If the browser did not open, copy this URL to any signed-in device.`)),console.log(r.gray("Waiting for authorization... (Ctrl+C to cancel)")))}});N({...t,machineToken:s.machineToken,machineId:s.machineId,accountNamespaceId:s.accountNamespaceId,serverUrl:e}),a?d({type:"pair.saved",machineId:s.machineId}):console.log(r.green(`
7
+ \u2713 Paired successfully! Machine "${s.machineName}" is now linked.`));return}catch(s){const T=s instanceof J||typeof s=="object"&&s&&"code"in s&&typeof s.code=="string"?s.code:"PAIR_BROWSER_FAILED",P=s instanceof Error?s.message:"Browser pairing failed";a&&g(T,P),console.error(r.red(`
8
+ \u2717 ${P}`)),process.exit(1)}finally{process.removeListener("SIGINT",w)}}let y;try{y=await fetch(`${e}/api/machines/pair/create`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,os:f,agentList:c,daemonInstallationId:D()})})}catch{const p=`Network error: cannot connect to ${e}`;a&&g("NETWORK_ERROR",p),console.error(r.red(`\u2717 ${p}`)),process.exit(1)}if(!y.ok){const w=`Failed to create machine: ${(await y.json().catch(()=>({}))).error??y.statusText}`;a&&g("PAIR_CREATE_FAILED",w),console.error(r.red(`\u2717 ${w}`)),process.exit(1)}const I=await y.json(),_=M(I.pairToken);a?d({type:"pair.created",code:I.pairToken,machineId:I.machineId,expiresAt:new Date(Date.now()+U).toISOString()}):(console.log(r.white("Scan QR code or enter the pairing code in the Shennian app:")),console.log(),await W(_),K(I.pairToken),console.log(),console.log(r.gray("Waiting for pairing... (Ctrl+C to cancel)")));const h=await Y(e,I.pairToken,{json:a});h||(a&&g("PAIR_TIMEOUT","Timed out waiting for pairing"),console.error(r.red(`
9
+ \u2717 Timed out. Pairing code expired, please try again.`)),process.exit(1)),N({...t,machineToken:h.machineToken,machineId:h.machineId,accountNamespaceId:h.accountNamespaceId,serverUrl:e}),a&&d({type:"pair.saved",machineId:h.machineId}),a||console.log(r.green(`
10
+ \u2713 Paired successfully! Machine "${h.machineName}" is now linked.`))}async function G(o){const e=u(),i=E().map(m=>m.type),a=k.platform(),t=a==="win32"?"windows":a==="darwin"?"macos":"linux",l=await fetch(`${o.serverUrl}/api/machines/desktop/register`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o.authToken}`},body:JSON.stringify({machineId:e.machineId,name:o.machineName,os:t,agentList:i})});if(!l.ok){const f=(await l.json().catch(()=>({}))).error??l.statusText;throw new Error(`Failed to register desktop machine: ${f}`)}const c=await l.json();if(!c.machine?.id||!c.machineToken||!c.accountNamespaceId)throw new Error("Failed to register desktop machine: missing machine token");return e.machineId=c.machine.id,e.machineToken=c.machineToken,e.accountNamespaceId=c.accountNamespaceId,e.serverUrl=o.serverUrl,N(e),{machineId:c.machine.id,machineName:c.machine.name}}async function V(o){const e=o?.trim();if(e)return e;const n=process.env.SHENNIAN_DESKTOP_SERVER_URL?.trim();if(n)return n;const i=u().serverUrl?.trim();return i||v()}async function de(o,e){const n=u();if(!n.machineToken||!n.machineId){const a=o??await v();await $({serverUrl:a,machineName:e,force:!1})}else console.log(r.green(`\u2713 Already paired (machine ID: ${n.machineId})`));x(),A(),console.log(r.gray(`
11
+ Starting background service...`)),O()||b()}function le(o){o.command("desktop-register",{hidden:!0}).description("Register this desktop-managed machine to the authenticated account").option("--api <url>","Server URL").option("--name <name>","Machine name",k.hostname()).action(async e=>{try{const n=process.env.SHENNIAN_DESKTOP_AUTH_TOKEN;if(!n)throw new Error("Missing desktop auth token");const i=await V(e.api),a=await G({serverUrl:i,authToken:n,machineName:e.name});console.log(JSON.stringify({ok:!0,...a}))}catch(n){console.error(n instanceof Error?n.message:String(n)),process.exit(1)}}),o.command("pair").description("Pair this machine to a Shennian account (re-pair if already paired)").option("--api <url>","Server URL override").option("--server <url>","Server URL override").option("--name <name>","Machine name",k.hostname()).option("--json","Emit machine-readable JSONL events").option("--browser","Pair by approving a browser authorization request").option("--token","Pair with the existing QR code / token flow").option("--force","Force re-pair even when a machine token exists").action(async e=>{const n=u();let i;try{i=C({browser:e.browser,token:e.token,json:e.json,stdinIsTty:!!process.stdin.isTTY,stdoutIsTty:!!process.stdout.isTTY})}catch(c){const m=c instanceof Error?c.message:"Invalid pairing mode";e.json&&g("PAIR_MODE_CONFLICT",m),console.error(r.red(`\u2717 ${m}`)),process.exit(1)}if(i==="prompt"&&(i=await j()),n.machineToken&&n.machineId&&!e.force){if(e.json){d({type:"done",status:"already-paired",machineId:n.machineId});return}if(console.log(r.yellow(`Currently paired (machine ID: ${n.machineId})`)),!await H("Re-pairing will unlink the current account. Continue? (y/N) ")){console.log(r.gray("Cancelled"));return}}const a=e.server??e.api??await v(),t=!!(e.force||n.machineToken&&n.machineId);await $({serverUrl:a,machineName:e.name,force:t,json:!!e.json,mode:i}),x(),A(),e.json?d({type:"daemon.starting"}):console.log(r.gray(`
12
+ Starting background service...`)),O()||b({quiet:!!e.json}),e.json&&(B(()=>L({cleanupStale:!0})),d({type:"done",status:"paired",machineId:u().machineId}))})}export{G as registerDesktopMachine,le as registerPairCommand,$ as runPairFlow,de as runSmartStart};
@@ -21,6 +21,7 @@ export type ShennianConfig = {
21
21
  apiKeys?: Record<string, string>;
22
22
  machineId?: string;
23
23
  machineToken?: string;
24
+ accountNamespaceId?: string;
24
25
  accessToken?: string;
25
26
  refreshToken?: string;
26
27
  serverUrl?: string;
package/dist/src/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import{Command as te}from"commander";import S from"chalk";import re from"node:os";import N from"node:fs";import{loadConfig as d,saveConfig as ie,getConfigPath as se,getShennianDir as ae,resolveShennianPath as J,normalizeAutoUpgradePolicy as ce}from"./config/index.js";import{CliRelayClient as me}from"./relay/client.js";import{registerPairCommand as le,runSmartStart as de}from"./commands/pair.js";import{clearDaemonPidIfOwner as j,createDaemonInstanceId as pe,clearDaemonLauncher as z,findRunningDaemonProcessIds as ge,isRemoteAccessDisabled as ue,isIsolatedDaemonE2eInstance as he,registerDaemonCommand as fe,writeDaemonPid as K,writeDaemonLauncher as W}from"./commands/daemon.js";import{registerRuntimeCommand as ve}from"./commands/runtime.js";import{registerManagerCommand as Se}from"./commands/manager.js";import{registerUpgradeCommand as we}from"./commands/upgrade.js";import{registerToolsCommand as Ie}from"./commands/tools.js";import{registerIntegrationCommand as ye}from"./commands/integration.js";import{registerRoomCommand as Re}from"./commands/room.js";import{SessionManager as Ce}from"./session/manager.js";import{SERVERS as b,regionToUrl as ke,urlToRegion as De}from"./region.js";import{getCurrentVersion as Y,handleStartupCrashCheck as Ee}from"./upgrade/engine.js";import{AutoUpgradeScheduler as Te}from"./upgrade/scheduler.js";import{detectAgents as Ue}from"./agents/detect.js";import{augmentProcessPath as Q}from"./env-path.js";Q();const q=Y();import{getCachedAgentInfos as Oe,resolveAgentInfos as Ae}from"./agents/model-registry.js";import{initCliLogReporter as $e,reportLog as w}from"./log-reporter.js";import{isNativeFusionEnabled as Ne}from"./native-fusion/config.js";import{NativeSessionFusionService as be}from"./native-fusion/service.js";import{startDaemonLogRetention as Pe}from"./daemon-log.js";import{startDaemonIpcServer as _e}from"./daemon-ipc/server.js";import{RoomSessionRegistry as Me}from"./room/session-registry.js";import{HttpRoomBindingClient as Le}from"./room/room-binding-client.js";import{TrustedManagedRoomBinder as xe}from"./room/managed-room-binding.js";import{RoomHostSessionObservationStore as Ve}from"./room/host-session-observations.js";import{RoomMirrorSyncService as Be}from"./room/mirror-sync-service.js";import{RoomActivationService as Ge}from"./room/activation-service.js";import{RoomActivationAudit as He}from"./room/activation-audit.js";import{RoomAttachmentCache as Fe}from"./room/attachment-cache.js";import{RoomAttachmentUploader as Je}from"./room/attachment-upload.js";import{ManagedRoomContextAuthority as je}from"./room/managed-context.js";import{createManagedRoomIpcHandlers as ze}from"./room/managed-room-ipc.js";import{SESSION_SYNC_PROTOCOL_VERSION as Ke,SESSION_SYNC_REQUIRED_CAPABILITIES as We}from"@shennian/wire";import{getDaemonInstallationId as Ye}from"./session/store.js";const X=ae(),Qe=J("daemon.pid");function qe(){try{const o=N.readFileSync(Qe,"utf-8").trim();if(o.startsWith("{")){const r=JSON.parse(o),a=Number(r.pid);return Number.isInteger(a)&&a>0?a:null}const n=parseInt(o,10);return Number.isInteger(n)&&n>0?n:null}catch{return null}}function Xe(o){return o.replace(/^https:\/\//,"wss://").replace(/^http:\/\//,"ws://")}function P(){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 Ze(o){const n=[`trigger=${o.trigger??"unknown"}`,`phase=${o.phase}`,`code=${o.code??"unknown"}`,`attempt=${o.reconnectAttempt}`];return o.reason&&n.push(`reason=${o.reason}`),o.error&&n.push(`error=${o.error}`),n.join(" ")}async function Z(o,n=5e3){const r=Date.now();for(;Date.now()-r<n;){try{process.kill(o,0)}catch{return!0}await new Promise(a=>setTimeout(a,100))}try{return process.kill(o,0),!1}catch{return!0}}const t=new te;t.name("shennian").description("Shennian \u2014 AI Agent Control Plane").version(q),t.enablePositionalOptions(),ye(t),Re(t),t.option("--api <url>","Server URL override").option("--name <name>","Machine name",re.hostname()).action(async o=>{const n=d(),r=o.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??n.serverUrl??void 0;await de(r,o.name)}),t.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 n=J("env.json");try{const e=JSON.parse(N.readFileSync(n,"utf-8"));for(const[i,s]of Object.entries(e))process.env[i]||(process.env[i]=s);Q()}catch{}const r=Pe();ue()&&(console.log(`[${new Date().toISOString()}] remote access disabled, service start skipped`),clearInterval(r),process.exit(0));const a=!!(process.env.INVOCATION_ID||process.env.JOURNAL_STREAM||process.env.SHENNIAN_DESKTOP_SERVER_URL),_=async(e,i)=>{console.log(`[${new Date().toISOString()}] ${i} (PID ${e})`),process.kill(e,"SIGTERM"),await Z(e)||(process.kill(e,"SIGKILL"),await Z(e,2e3))};try{const e=qe();if(e&&e!==process.pid)try{process.kill(e,0),a?await _(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 T=he(process.env,X)?[]:ge(process.pid);if(T.length>0)if(a)for(const e of T)try{await _(e,"managed start taking over from orphan daemon")}catch{}else console.log(`[${new Date().toISOString()}] daemon already running (PID ${T[0]}), skipping duplicate start`),process.exit(0);const l=pe();K(process.pid,l,{version:q}),W(process.pid,void 0,l),process.on("exit",()=>{j(process.pid,l),z(l)}),await Ee()&&(console.log(`[${new Date().toISOString()}] Rolled back to previous version, restarting...`),process.exit(0));const c=d(),u=o.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??c.serverUrl??b.cn.url,p=Y(),U=Ue(),h=U.map(e=>e.type),O=Oe(U),I=new Me,y=new Ve,M=new je(I),f=new Le(u),oe=new Fe({actionClient:f}),ne=new Je({actionClient:f});let L=null;const A=await _e({handlers:{...ze({authority:M,actionClient:f,attachmentCache:oe,attachmentUploader:ne,observations:y,getMachineCredentials:()=>({machineToken:d().machineToken})})}});let x=!1;const R=()=>{x||(x=!0,A.close().finally(()=>process.exit(0)))};if(process.once("SIGINT",R),process.once("SIGTERM",R),!c.machineToken){console.error('Shennian CLI is not paired. Run "shennian pair --browser" or "shennian pair --token" first.'),await A.close(),process.exitCode=1;return}process.removeListener("SIGINT",R),process.removeListener("SIGTERM",R);const V=`${Xe(u)}/relay/machine`,B=c.machineToken;if(!B)return;c.machineId&&$e(u,c.machineId),console.log(`[${new Date().toISOString()}] Connecting to ${V}... (v${p}) agents: ${h.join(",")}`),w({level:"info",wsEvent:"daemon.start",metadata:{version:p,agents:h}});let C=null,k=null,D=0;const G=ce(c.autoUpgrade),m=new me({serverUrl:V,machineToken:B,cliVersion:p,agentList:h,autoUpgradePolicy:G,sessionSyncProtocolVersion:Ke,sessionSyncCapabilities:We,daemonInstallationId:Ye(),onConnected:()=>{console.log(`[${new Date().toISOString()}] \u2713 Connected`),w({level:"info",wsEvent:"daemon.connected"}),O.some(s=>s.models.length>0)&&m.sendEvent({type:"event",event:"machine.agents",payload:{agentList:h,agents:O}}),Ae(U,{serverUrl:c.serverUrl??u,authToken:c.machineToken??c.accessToken}).then(s=>{JSON.stringify(s)!==JSON.stringify(O)&&m.sendEvent({type:"event",event:"machine.agents",payload:{agentList:h,agents:s}})}).catch(()=>{}),import("./upgrade/engine.js").then(({readUpgradeAttempt:s,clearUpgradeAttempt:$})=>{const v=s();$(),v&&(console.log(`[${new Date().toISOString()}] [upgrade] Reporting success: ${v.from} \u2192 ${v.to}`),m.sendEvent({type:"event",event:"upgrade",payload:{state:"success",machineId:"self",from:v.from,to:v.to}}))}).catch(()=>{}),E.start(),k?.start();const e=++D,i=async()=>{try{await g.synchronizeSessionDeletions(),e===D&&m.getState()==="connected"&&C?.handleConnected()}catch(s){console.error("[session.deletions] startup catch-up failed",s),e===D&&m.getState()==="connected"&&setTimeout(()=>{i()},5e3).unref?.()}};i()},onDisconnected:e=>{D+=1,k?.stop(),console.log(`[${new Date().toISOString()}] \u26A0 Disconnected, reconnecting... ${Ze(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}),g.handleReq(e)}});C=Ne()?new be(m):null,k=new Be({registry:I,observations:y,actionClient:f,getMachineCredentials:()=>({machineToken:d().machineToken}),onMessages:async(e,i,s,$)=>{await L?.processMessages(e,i,s,$)}});const E=new Te({currentVersion:p,machineId:c.machineId??"unknown-machine",serverUrl:c.serverUrl??u,getIdleState:()=>g.getUpgradeIdleState(),startUpgrade:async e=>{const{handleUpgradeStart:i}=await import("./commands/upgrade.js");await i(m,"auto-upgrade",e,{currentVersion:p})}});E.setPolicy(G);const g=new Ce(m,C,p,E,new xe({registry:I,observations:y,bindingClient:f,getMachineCredentials:()=>{const e=d();return{machineId:e.machineId,machineToken:e.machineToken}},hostProcessIdentity:()=>`daemon:${l}`}));L=new Ge({registry:I,observations:y,activate:e=>g.activateRoomSession(e),issueManagedRoomContext:e=>{const i=d().machineToken;if(!i)throw new Error("machine_not_authorized");return M.issue({registryKey:e,machineCredential:i})},abort:e=>g.abortRoomSession(e),audit:new He}),N.mkdirSync(X,{recursive:!0}),K(process.pid,l,{version:p}),W(process.pid,void 0,l),m.connect(),process.stdin.resume();let H=!1;const F=async()=>{if(!H){H=!0,console.log(S.gray(`
3
- Disconnecting...`)),w({level:"info",wsEvent:"daemon.stop"}),clearInterval(r);try{await g.cleanup(),E.stop(),C?.stop(),k?.stop(),m.disconnect(),await A.close(),j(process.pid,l),z(l)}finally{process.exit(0)}}};process.on("SIGINT",()=>{F()}),process.on("SIGTERM",()=>{F()})}),t.command("workbuddy-session-hook",{hidden:!0}).description("(retired) WorkBuddy Room Hook").allowUnknownOption(!0).action(()=>P()),t.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(()=>P()),t.command("room-tool-server",{hidden:!0}).description("(internal) Run the official Shennian Room MCP tool").allowUnknownOption(!0).action(()=>P());const ee=t.command("config").description("View or modify configuration");ee.command("show",{isDefault:!0}).description("Show current configuration").action(()=>{console.log(`Config file: ${se()}`);const n={...d()};if(n.machineToken&&(n.machineToken=n.machineToken.slice(0,12)+"..."),n.serverUrl){const r=De(n.serverUrl);console.log(`Region: ${r} (${b[r].label})`)}console.log(JSON.stringify(n,null,2))}),ee.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,n)=>{const r=d();if(o==="server"){n!=="cn"&&n!=="global"&&(console.error(S.red('\u2717 Value must be "cn" or "global"')),process.exit(1));const a=n;r.serverUrl=ke(a),ie(r),console.log(S.green(`\u2713 Server set to ${b[a].label}`)),console.log(S.yellow(" Restart the background service for this to take effect: shennian stop && shennian start"));return}console.error(S.red(`\u2717 Unknown config key: ${o}. Supported: server`)),process.exit(1)}),le(t),fe(t),ve(t),Se(t),we(t),Ie(t),t.parse();
1
+ import{Command as te}from"commander";import S from"chalk";import re from"node:os";import b from"node:fs";import{loadConfig as d,saveConfig as ie,getConfigPath as se,getShennianDir as ae,resolveShennianPath as J,normalizeAutoUpgradePolicy as ce}from"./config/index.js";import{CliRelayClient as me}from"./relay/client.js";import{registerPairCommand as le,runSmartStart as de}from"./commands/pair.js";import{clearDaemonPidIfOwner as z,createDaemonInstanceId as pe,clearDaemonLauncher as j,findRunningDaemonProcessIds as ge,isRemoteAccessDisabled as ue,isIsolatedDaemonE2eInstance as he,registerDaemonCommand as fe,writeDaemonPid as K,writeDaemonLauncher as W}from"./commands/daemon.js";import{registerRuntimeCommand as ve}from"./commands/runtime.js";import{registerManagerCommand as Se}from"./commands/manager.js";import{registerUpgradeCommand as we}from"./commands/upgrade.js";import{registerToolsCommand as Ie}from"./commands/tools.js";import{registerIntegrationCommand as ye}from"./commands/integration.js";import{registerRoomCommand as Re}from"./commands/room.js";import{SessionManager as Ce}from"./session/manager.js";import{SERVERS as P,regionToUrl as ke,urlToRegion as De}from"./region.js";import{getCurrentVersion as Y,handleStartupCrashCheck as Ee}from"./upgrade/engine.js";import{AutoUpgradeScheduler as Te}from"./upgrade/scheduler.js";import{detectAgents as Ue}from"./agents/detect.js";import{augmentProcessPath as Q}from"./env-path.js";Q();const q=Y();import{getCachedAgentInfos as Ae,resolveAgentInfos as Ne}from"./agents/model-registry.js";import{initCliLogReporter as Oe,reportLog as w}from"./log-reporter.js";import{isNativeFusionEnabled as $e}from"./native-fusion/config.js";import{NativeSessionFusionService as be}from"./native-fusion/service.js";import{startDaemonLogRetention as Pe}from"./daemon-log.js";import{startDaemonIpcServer as _e}from"./daemon-ipc/server.js";import{RoomSessionRegistry as Me}from"./room/session-registry.js";import{HttpRoomBindingClient as Le}from"./room/room-binding-client.js";import{TrustedManagedRoomBinder as xe}from"./room/managed-room-binding.js";import{RoomHostSessionObservationStore as Ve}from"./room/host-session-observations.js";import{RoomMirrorSyncService as Be}from"./room/mirror-sync-service.js";import{RoomActivationService as Ge}from"./room/activation-service.js";import{RoomActivationAudit as He}from"./room/activation-audit.js";import{RoomAttachmentCache as Fe}from"./room/attachment-cache.js";import{RoomAttachmentUploader as Je}from"./room/attachment-upload.js";import{ManagedRoomContextAuthority as ze}from"./room/managed-context.js";import{createManagedRoomIpcHandlers as je}from"./room/managed-room-ipc.js";import{SESSION_SYNC_PROTOCOL_VERSION as Ke,SESSION_SYNC_REQUIRED_CAPABILITIES as We}from"@shennian/wire";import{getDaemonInstallationId as Ye}from"./session/store.js";import{synchronizeActiveAccountNamespace as Qe}from"./personal-sync/account-namespace.js";const X=ae(),qe=J("daemon.pid");function Xe(){try{const o=b.readFileSync(qe,"utf-8").trim();if(o.startsWith("{")){const r=JSON.parse(o),c=Number(r.pid);return Number.isInteger(c)&&c>0?c:null}const n=parseInt(o,10);return Number.isInteger(n)&&n>0?n:null}catch{return null}}function Ze(o){return o.replace(/^https:\/\//,"wss://").replace(/^http:\/\//,"ws://")}function _(){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 eo(o){const n=[`trigger=${o.trigger??"unknown"}`,`phase=${o.phase}`,`code=${o.code??"unknown"}`,`attempt=${o.reconnectAttempt}`];return o.reason&&n.push(`reason=${o.reason}`),o.error&&n.push(`error=${o.error}`),n.join(" ")}async function Z(o,n=5e3){const r=Date.now();for(;Date.now()-r<n;){try{process.kill(o,0)}catch{return!0}await new Promise(c=>setTimeout(c,100))}try{return process.kill(o,0),!1}catch{return!0}}const t=new te;t.name("shennian").description("Shennian \u2014 AI Agent Control Plane").version(q),t.enablePositionalOptions(),ye(t),Re(t),t.option("--api <url>","Server URL override").option("--name <name>","Machine name",re.hostname()).action(async o=>{const n=d(),r=o.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??n.serverUrl??void 0;await de(r,o.name)}),t.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 n=J("env.json");try{const e=JSON.parse(b.readFileSync(n,"utf-8"));for(const[i,a]of Object.entries(e))process.env[i]||(process.env[i]=a);Q()}catch{}const r=Pe();ue()&&(console.log(`[${new Date().toISOString()}] remote access disabled, service start skipped`),clearInterval(r),process.exit(0));const c=!!(process.env.INVOCATION_ID||process.env.JOURNAL_STREAM||process.env.SHENNIAN_DESKTOP_SERVER_URL),M=async(e,i)=>{console.log(`[${new Date().toISOString()}] ${i} (PID ${e})`),process.kill(e,"SIGTERM"),await Z(e)||(process.kill(e,"SIGKILL"),await Z(e,2e3))};try{const e=Xe();if(e&&e!==process.pid)try{process.kill(e,0),c?await M(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 U=he(process.env,X)?[]:ge(process.pid);if(U.length>0)if(c)for(const e of U)try{await M(e,"managed start taking over from orphan daemon")}catch{}else console.log(`[${new Date().toISOString()}] daemon already running (PID ${U[0]}), skipping duplicate start`),process.exit(0);const l=pe();K(process.pid,l,{version:q}),W(process.pid,void 0,l),process.on("exit",()=>{z(process.pid,l),j(l)}),await Ee()&&(console.log(`[${new Date().toISOString()}] Rolled back to previous version, restarting...`),process.exit(0));const s=d(),g=o.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??s.serverUrl??P.cn.url,p=Y(),A=Ue(),h=A.map(e=>e.type),N=Ae(A),I=new Me,y=new Ve,L=new ze(I),f=new Le(g),oe=new Fe({actionClient:f}),ne=new Je({actionClient:f});let x=null;const R=await _e({handlers:{...je({authority:L,actionClient:f,attachmentCache:oe,attachmentUploader:ne,observations:y,getMachineCredentials:()=>({machineToken:d().machineToken})})}});let V=!1;const C=()=>{V||(V=!0,R.close().finally(()=>process.exit(0)))};if(process.once("SIGINT",C),process.once("SIGTERM",C),!s.machineToken){console.error('Shennian CLI is not paired. Run "shennian pair --browser" or "shennian pair --token" first.'),await R.close(),process.exitCode=1;return}process.removeListener("SIGINT",C),process.removeListener("SIGTERM",C);const B=`${Ze(g)}/relay/machine`,O=s.machineToken;if(!O)return;try{s.accountNamespaceId=await Qe({serverUrl:g,machineToken:O,machineId:s.machineId})}catch(e){console.error(`[personal-sync] account namespace verification failed: ${e instanceof Error?e.message:String(e)}`),await R.close(),process.exitCode=1;return}s.machineId&&Oe(g,s.machineId),console.log(`[${new Date().toISOString()}] Connecting to ${B}... (v${p}) agents: ${h.join(",")}`),w({level:"info",wsEvent:"daemon.start",metadata:{version:p,agents:h}});let k=null,D=null,E=0;const G=ce(s.autoUpgrade),m=new me({serverUrl:B,machineToken:O,cliVersion:p,agentList:h,autoUpgradePolicy:G,sessionSyncProtocolVersion:Ke,sessionSyncCapabilities:We,daemonInstallationId:Ye(),onConnected:()=>{console.log(`[${new Date().toISOString()}] \u2713 Connected`),w({level:"info",wsEvent:"daemon.connected"}),N.some(a=>a.models.length>0)&&m.sendEvent({type:"event",event:"machine.agents",payload:{agentList:h,agents:N}}),Ne(A,{serverUrl:s.serverUrl??g,authToken:s.machineToken??s.accessToken}).then(a=>{JSON.stringify(a)!==JSON.stringify(N)&&m.sendEvent({type:"event",event:"machine.agents",payload:{agentList:h,agents:a}})}).catch(()=>{}),import("./upgrade/engine.js").then(({readUpgradeAttempt:a,clearUpgradeAttempt:$})=>{const v=a();$(),v&&(console.log(`[${new Date().toISOString()}] [upgrade] Reporting success: ${v.from} \u2192 ${v.to}`),m.sendEvent({type:"event",event:"upgrade",payload:{state:"success",machineId:"self",from:v.from,to:v.to}}))}).catch(()=>{}),T.start(),D?.start();const e=++E,i=async()=>{try{await u.synchronizeSessionDeletions(),e===E&&m.getState()==="connected"&&k?.handleConnected()}catch(a){console.error("[session.deletions] startup catch-up failed",a),e===E&&m.getState()==="connected"&&setTimeout(()=>{i()},5e3).unref?.()}};i()},onDisconnected:e=>{E+=1,D?.stop(),console.log(`[${new Date().toISOString()}] \u26A0 Disconnected, reconnecting... ${eo(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}),u.handleReq(e)}});k=$e()?new be(m):null,D=new Be({registry:I,observations:y,actionClient:f,getMachineCredentials:()=>({machineToken:d().machineToken}),onMessages:async(e,i,a,$)=>{await x?.processMessages(e,i,a,$)}});const T=new Te({currentVersion:p,machineId:s.machineId??"unknown-machine",serverUrl:s.serverUrl??g,getIdleState:()=>u.getUpgradeIdleState(),startUpgrade:async e=>{const{handleUpgradeStart:i}=await import("./commands/upgrade.js");await i(m,"auto-upgrade",e,{currentVersion:p})}});T.setPolicy(G);const u=new Ce(m,k,p,T,new xe({registry:I,observations:y,bindingClient:f,getMachineCredentials:()=>{const e=d();return{machineId:e.machineId,machineToken:e.machineToken}},hostProcessIdentity:()=>`daemon:${l}`}));x=new Ge({registry:I,observations:y,activate:e=>u.activateRoomSession(e),issueManagedRoomContext:e=>{const i=d().machineToken;if(!i)throw new Error("machine_not_authorized");return L.issue({registryKey:e,machineCredential:i})},abort:e=>u.abortRoomSession(e),audit:new He}),b.mkdirSync(X,{recursive:!0}),K(process.pid,l,{version:p}),W(process.pid,void 0,l),m.connect(),process.stdin.resume();let H=!1;const F=async()=>{if(!H){H=!0,console.log(S.gray(`
3
+ Disconnecting...`)),w({level:"info",wsEvent:"daemon.stop"}),clearInterval(r);try{await u.cleanup(),T.stop(),k?.stop(),D?.stop(),m.disconnect(),await R.close(),z(process.pid,l),j(l)}finally{process.exit(0)}}};process.on("SIGINT",()=>{F()}),process.on("SIGTERM",()=>{F()})}),t.command("workbuddy-session-hook",{hidden:!0}).description("(retired) WorkBuddy Room Hook").allowUnknownOption(!0).action(()=>_()),t.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(()=>_()),t.command("room-tool-server",{hidden:!0}).description("(internal) Run the official Shennian Room MCP tool").allowUnknownOption(!0).action(()=>_());const ee=t.command("config").description("View or modify configuration");ee.command("show",{isDefault:!0}).description("Show current configuration").action(()=>{console.log(`Config file: ${se()}`);const n={...d()};if(n.machineToken&&(n.machineToken=n.machineToken.slice(0,12)+"..."),n.serverUrl){const r=De(n.serverUrl);console.log(`Region: ${r} (${P[r].label})`)}console.log(JSON.stringify(n,null,2))}),ee.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,n)=>{const r=d();if(o==="server"){n!=="cn"&&n!=="global"&&(console.error(S.red('\u2717 Value must be "cn" or "global"')),process.exit(1));const c=n;r.serverUrl=ke(c),ie(r),console.log(S.green(`\u2713 Server set to ${P[c].label}`)),console.log(S.yellow(" Restart the background service for this to take effect: shennian stop && shennian start"));return}console.error(S.red(`\u2717 Unknown config key: ${o}. Supported: server`)),process.exit(1)}),le(t),fe(t),ve(t),Se(t),we(t),Ie(t),t.parse();
@@ -0,0 +1,6 @@
1
+ export declare function synchronizeActiveAccountNamespace(input: {
2
+ serverUrl: string;
3
+ machineToken: string;
4
+ machineId?: string | null;
5
+ fetchImpl?: typeof fetch;
6
+ }): Promise<string>;
@@ -0,0 +1 @@
1
+ import{loadConfig as o,saveConfig as t}from"../config/index.js";const i=/^[a-f0-9]{64}$/;async function r(e){const c=await(e.fetchImpl??fetch)(`${e.serverUrl.replace(/\/+$/,"")}/api/machines/identity`,{headers:{Authorization:`Bearer ${e.machineToken}`},signal:AbortSignal.timeout(1e4)});if(!c.ok)throw new Error(`account_namespace_unavailable:${c.status}`);const a=await c.json();if(typeof a.machineId!="string"||!a.machineId||typeof a.accountNamespaceId!="string"||!i.test(a.accountNamespaceId))throw new Error("account_namespace_invalid_response");if(e.machineId&&a.machineId!==e.machineId)throw new Error("account_namespace_machine_mismatch");const n=o();return(n.accountNamespaceId!==a.accountNamespaceId||n.machineId!==a.machineId)&&t({...n,machineId:a.machineId,accountNamespaceId:a.accountNamespaceId}),a.accountNamespaceId}export{r as synchronizeActiveAccountNamespace};
@@ -1,2 +1,2 @@
1
- import c from"node:crypto";import n from"node:fs";import s from"node:path";import{resolvePersonalSyncV1Path as d}from"./paths.js";function p(){const t=s.join(d(),"installation.json");if(n.existsSync(t)){const e=JSON.parse(n.readFileSync(t,"utf8"));if(e.schemaVersion!==1||!e.daemonInstallationId)throw new Error(`invalid daemon installation identity: ${t}`);return e.daemonInstallationId}const i={schemaVersion:1,daemonInstallationId:c.randomUUID(),createdAt:new Date().toISOString()};return l(t,i),i.daemonInstallationId}function l(t,i){const e=s.dirname(t);n.mkdirSync(e,{recursive:!0,mode:448});const r=`${t}.tmp-${process.pid}-${c.randomBytes(6).toString("hex")}`;let o;try{if(o=n.openSync(r,"wx",384),n.writeFileSync(o,`${JSON.stringify(i,null,2)}
1
+ import c from"node:crypto";import n from"node:fs";import s from"node:path";import{resolvePersonalSyncV1RootPath as d}from"./paths.js";function p(){const t=s.join(d(),"installation.json");if(n.existsSync(t)){const e=JSON.parse(n.readFileSync(t,"utf8"));if(e.schemaVersion!==1||!e.daemonInstallationId)throw new Error(`invalid daemon installation identity: ${t}`);return e.daemonInstallationId}const i={schemaVersion:1,daemonInstallationId:c.randomUUID(),createdAt:new Date().toISOString()};return l(t,i),i.daemonInstallationId}function l(t,i){const e=s.dirname(t);n.mkdirSync(e,{recursive:!0,mode:448});const r=`${t}.tmp-${process.pid}-${c.randomBytes(6).toString("hex")}`;let o;try{if(o=n.openSync(r,"wx",384),n.writeFileSync(o,`${JSON.stringify(i,null,2)}
2
2
  `,"utf8"),n.fsyncSync(o),n.closeSync(o),o=void 0,n.renameSync(r,t),process.platform!=="win32"){const a=n.openSync(e,"r");try{n.fsyncSync(a)}finally{n.closeSync(a)}}}finally{o!==void 0&&n.closeSync(o);try{n.unlinkSync(r)}catch{}}}export{p as getDaemonInstallationId};
@@ -1,2 +1,8 @@
1
1
  export declare const PERSONAL_SYNC_V1_ROOT_NAME = "personal-sync-v1";
2
+ export declare class PersonalSyncAccountNamespaceError extends Error {
3
+ readonly code = "account_namespace_required";
4
+ constructor();
5
+ }
6
+ export declare function resolvePersonalSyncV1RootPath(...parts: string[]): string;
7
+ export declare function getActivePersonalSyncAccountNamespaceId(): string;
2
8
  export declare function resolvePersonalSyncV1Path(...parts: string[]): string;
@@ -1 +1 @@
1
- import n from"node:path";import{resolveShennianPath as r}from"../config/index.js";const e="personal-sync-v1";function p(...o){return n.join(r(e),...o)}export{e as PERSONAL_SYNC_V1_ROOT_NAME,p as resolvePersonalSyncV1Path};
1
+ import o from"node:path";import{loadConfig as r,resolveShennianPath as t}from"../config/index.js";const a="personal-sync-v1",c=/^[a-f0-9]{64}$/;class s extends Error{code="account_namespace_required";constructor(){super("personal sync account namespace is unavailable; pair this daemon again"),this.name="PersonalSyncAccountNamespaceError"}}function p(...e){return o.join(t(a),...e)}function i(){const n=(process.env.VITEST==="true"||process.env.VITEST_WORKER_ID?process.env.SHENNIAN_ACCOUNT_NAMESPACE_ID?.trim():void 0)||r().accountNamespaceId?.trim();if(!n||!c.test(n))throw new s;return n}function N(...e){return p("accounts",i(),...e)}export{a as PERSONAL_SYNC_V1_ROOT_NAME,s as PersonalSyncAccountNamespaceError,i as getActivePersonalSyncAccountNamespaceId,N as resolvePersonalSyncV1Path,p as resolvePersonalSyncV1RootPath};
@@ -29,6 +29,7 @@ export interface DeviceAuthorizationClient {
29
29
  actionPayloadHash: string;
30
30
  machineId?: string;
31
31
  machineToken?: string;
32
+ accountNamespaceId?: string;
32
33
  }>;
33
34
  cancel(input: {
34
35
  loginIntentId: string;
@@ -75,6 +76,7 @@ export declare class HttpDeviceAuthorizationClient implements DeviceAuthorizatio
75
76
  actionPayloadHash: string;
76
77
  machineId?: string;
77
78
  machineToken?: string;
79
+ accountNamespaceId?: string;
78
80
  }>;
79
81
  cancel(input: {
80
82
  loginIntentId: string;
@@ -1,5 +1,5 @@
1
- import M from"node:crypto";import o from"node:fs";import r from"node:path";const v=1,H=64,b="mutations-000001.jsonl";class q extends Error{constructor(e,s){super(e,s),this.name="CanonicalStoreError"}}class w extends q{constructor(e,s){super(e,s),this.name="CanonicalStoreCorruptionError"}}class p extends q{constructor(e){super(e),this.name="CanonicalStoreConflictError"}}class K{rootDir;daemonInstallationId;mutationListeners=new Set;constructor(e,s){if(!s)throw new q("daemonInstallationId is required");this.rootDir=r.resolve(e),this.daemonInstallationId=s}commit(e){k(e);const s=this.sessionDir(e.sessionId);this.assertHealthy(s),o.mkdirSync(this.messageStateDir(s),{recursive:!0,mode:448});let t=this.loadManifestOrRebuild(e.sessionId);const n=this.readMessageState(s,e.messageId),a=z(e);if(n&&n.contentHash===a&&n.tombstoned===(e.mutation==="tombstone"))return{bodyRevision:t.bodyRevision,sequence:n.sequence,mutation:this.findMutationByRevision(e.sessionId,n.lastRevision),duplicate:!0};if(e.mutation==="append"&&n)throw new p(`message ${e.messageId} already exists in session ${e.sessionId}; use upsert`);const m=n?.sequence??t.nextSequence,h=t.bodyRevision+1,S=e.committedAt??new Date().toISOString(),y={schemaVersion:v,mutationId:T(e.sessionId,e.messageId,e.mutation,n?.contentHash??"none",a),sessionId:e.sessionId,revision:h,mutation:e.mutation,messageId:e.messageId,sequence:m,...e.message?{message:e.message}:{},...e.deliveryState?{deliveryState:e.deliveryState}:{},committedAt:S,contentHash:a},d=r.join(s,b),u=_(d,`${JSON.stringify(y)}
2
- `),c={schemaVersion:v,sessionId:e.sessionId,messageId:e.messageId,sequence:m,lastRevision:h,contentHash:a,tombstoned:e.mutation==="tombstone",...e.message?{message:e.message}:{},...e.deliveryState?{deliveryState:e.deliveryState}:{}};I(this.messageStateFile(s,m,e.messageId),c);const f=this.readSparseIndex(s,e.sessionId);(h===1||(h-1)%H===0)&&(f.entries.push({revision:h,offset:u}),I(r.join(s,"sparse-index.json"),f)),t={...t,bodyRevision:h,nextSequence:n?t.nextSequence:m+1,firstAvailableSeq:t.firstAvailableSeq??m,lastAvailableSeq:Math.max(t.lastAvailableSeq??m,m),mutationCount:t.mutationCount+1,visibleMessageCount:t.visibleMessageCount+(!n&&e.mutation!=="tombstone"?1:0)-(n&&!n.tombstoned&&e.mutation==="tombstone"?1:0),lastVisibleMessageId:e.mutation==="tombstone"?t.lastVisibleMessageId===e.messageId?this.findLastVisibleMessageId(s,e.messageId):t.lastVisibleMessageId:e.messageId,lastBodyCommittedAt:S,logByteLength:o.statSync(d).size,updatedAt:S},I(r.join(s,"manifest.json"),t);for(const x of this.mutationListeners)try{x(y)}catch(j){console.error("[canonical-store] mutation listener failed",j)}return{bodyRevision:h,sequence:m,mutation:y,duplicate:!1}}subscribe(e){return this.mutationListeners.add(e),()=>this.mutationListeners.delete(e)}append(e,s,t){return this.commit({sessionId:e,mutation:"append",messageId:s.id,message:s,deliveryState:t})}upsert(e,s,t){return this.commit({sessionId:e,mutation:"upsert",messageId:s.id,message:s,deliveryState:t})}tombstone(e,s,t){return this.commit({sessionId:e,mutation:"tombstone",messageId:s,deliveryState:t})}readRecent(e,s){return this.readPage(e,{limit:s})}readPage(e,s){const t=O(s.limit),n=this.sessionDir(e);if(!o.existsSync(n))return B();const a=this.loadManifestOrRebuild(e),m=this.listMessageStateFiles(n).filter(d=>s.beforeSequence===void 0||d.sequence<s.beforeSequence).sort((d,u)=>u.sequence-d.sequence),h=[];let S=0;for(const d of m){S+=1;const u=D(r.join(this.messageStateDir(n),d.name));if(!u.tombstoned&&u.message&&h.push(u),h.length>=t)break}h.sort((d,u)=>d.sequence-u.sequence);const y=h.length>0&&m.slice(S).some(d=>{const u=D(r.join(this.messageStateDir(n),d.name));return!u.tombstoned&&!!u.message});return{bodyRevision:a.bodyRevision,firstAvailableSeq:a.firstAvailableSeq,lastAvailableSeq:a.lastAvailableSeq,hasOlder:y,messages:h.map(d=>d.message),entries:h.map(d=>({sequence:d.sequence,revision:d.lastRevision,message:d.message}))}}readPageAtRevision(e,s){const t=O(s.limit),n=this.sessionDir(e);if(!o.existsSync(n))return B();const a=this.loadManifestOrRebuild(e);if(!Number.isSafeInteger(s.revision)||s.revision<0||s.revision>a.bodyRevision)throw new q("snapshot revision is unavailable");const m=new Map,h=L(r.join(n,b),0);for(const c of h.split(`
3
- `)){if(!c)continue;const f=A(c,e);if(f.revision>s.revision)break;m.set(f.messageId,{schemaVersion:v,sessionId:e,messageId:f.messageId,sequence:f.sequence,lastRevision:f.revision,contentHash:f.contentHash,tombstoned:f.mutation==="tombstone",...f.message?{message:f.message}:{},...f.deliveryState?{deliveryState:f.deliveryState}:{}})}const S=[...m.values()].filter(c=>!c.tombstoned&&c.message&&(s.beforeSequence===void 0||c.sequence<s.beforeSequence)).sort((c,f)=>f.sequence-c.sequence),y=S.slice(0,t).sort((c,f)=>c.sequence-f.sequence),u=[...m.values()].filter(c=>!c.tombstoned&&c.message).map(c=>c.sequence);return{bodyRevision:s.revision,firstAvailableSeq:u.length?Math.min(...u):null,lastAvailableSeq:u.length?Math.max(...u):null,hasOlder:S.length>y.length,messages:y.map(c=>c.message),entries:y.map(c=>({sequence:c.sequence,revision:c.lastRevision,message:c.message}))}}readDeltas(e,s,t){if(!Number.isSafeInteger(s)||s<0)throw new q("afterRevision must be a non-negative safe integer");const n=O(t),a=this.sessionDir(e);if(!o.existsSync(a))return[];const m=this.loadManifestOrRebuild(e);if(s>=m.bodyRevision)return[];const h=this.readSparseIndex(a,e),S=s+1,y=[...h.entries].reverse().find(c=>c.revision<=S),d=L(r.join(a,b),y?.offset??0),u=[];for(const c of d.split(`
4
- `)){if(!c)continue;const f=A(c,e);if(f.revision>s&&u.push(f),u.length>=n)break}return u}getManifest(e){const s=this.sessionDir(e);return o.existsSync(s)?this.loadManifestOrRebuild(e):null}getMessageState(e,s){const t=this.sessionDir(e);if(!o.existsSync(t))return null;this.loadManifestOrRebuild(e);const n=this.readMessageState(t,s);return n?{sequence:n.sequence,lastRevision:n.lastRevision,tombstoned:n.tombstoned,message:n.message,deliveryState:n.deliveryState}:null}listSessionIds(){const e=r.join(this.rootDir,"sessions");if(!o.existsSync(e))return[];const s=[];for(const t of o.readdirSync(e,{withFileTypes:!0})){if(!t.isDirectory())continue;const n=r.join(e,t.name,"manifest.json");o.existsSync(n)&&s.push(D(n).sessionId)}return s.sort()}purgeSession(e){if(!e)throw new q("sessionId is required");const s=r.resolve(this.rootDir,"sessions"),t=r.resolve(s,F(e));if(r.dirname(t)!==s)throw new q("invalid session path");o.rmSync(t,{recursive:!0,force:!0})}findLastVisibleMessageId(e,s){for(const t of this.listMessageStateFiles(e).sort((n,a)=>a.sequence-n.sequence)){const n=D(r.join(this.messageStateDir(e),t.name));if(n.messageId!==s&&!n.tombstoned&&n.message)return n.messageId}return null}rebuild(e){const s=this.sessionDir(e);this.assertHealthy(s),o.mkdirSync(s,{recursive:!0,mode:448});const t=r.join(s,"manifest.json");if(o.existsSync(t))try{const l=JSON.parse(o.readFileSync(t,"utf8"));if(l.daemonInstallationId&&l.daemonInstallationId!==this.daemonInstallationId)throw new p(`canonical session ${e} belongs to a different daemon installation`)}catch(l){if(l instanceof p)throw l}const n=r.join(s,b);if(!o.existsSync(n)){const l=P(e,this.daemonInstallationId);return I(r.join(s,"manifest.json"),l),I(r.join(s,"sparse-index.json"),C(e)),l}const a=o.readFileSync(n);let m=a.length;if(a.length>0&&a[a.length-1]!==10){const l=a.lastIndexOf(10);m=l<0?0:l+1,o.truncateSync(n,m)}const h=a.subarray(0,m).toString("utf8"),S=new Map,y=C(e);let d=1,u=0,c=new Date(0).toISOString();try{for(const l of h.match(/.*\n/g)??[]){const R=l.slice(0,-1);if(!R){u+=Buffer.byteLength(l);continue}const g=A(R,e);if(g.revision!==d)throw new w(`non-contiguous revision in ${e}: expected ${d}, got ${g.revision}`);(g.revision===1||(g.revision-1)%H===0)&&y.entries.push({revision:g.revision,offset:u});const V=S.get(g.messageId);if(V&&V.sequence!==g.sequence)throw new w(`sequence changed for message ${g.messageId}`);S.set(g.messageId,{schemaVersion:v,sessionId:e,messageId:g.messageId,sequence:g.sequence,lastRevision:g.revision,contentHash:g.contentHash,tombstoned:g.mutation==="tombstone",...g.message?{message:g.message}:{},...g.deliveryState?{deliveryState:g.deliveryState}:{}}),d+=1,u+=Buffer.byteLength(l),c=g.committedAt}}catch(l){this.quarantineCorruptSegment(e,n,l)}const f=this.messageStateDir(s);o.rmSync(f,{recursive:!0,force:!0}),o.mkdirSync(f,{recursive:!0,mode:448});for(const l of S.values())I(this.messageStateFile(s,l.sequence,l.messageId),l);I(r.join(s,"sparse-index.json"),y);const x=[...S.values()].map(l=>l.sequence),j=[...S.values()].filter(l=>!l.tombstoned&&!!l.message).sort((l,R)=>l.sequence-R.sequence),$=d-1,N={schemaVersion:v,daemonInstallationId:this.daemonInstallationId,sessionId:e,bodyRevision:$,nextSequence:x.length===0?1:Math.max(...x)+1,firstAvailableSeq:x.length===0?null:Math.min(...x),lastAvailableSeq:x.length===0?null:Math.max(...x),mutationCount:$,visibleMessageCount:j.length,lastVisibleMessageId:j.at(-1)?.messageId??null,lastBodyCommittedAt:$>0?c:null,segment:b,logByteLength:m,updatedAt:c};return I(r.join(s,"manifest.json"),N),N}loadManifestOrRebuild(e){const s=this.sessionDir(e);this.assertHealthy(s);const t=r.join(s,"manifest.json"),n=r.join(s,b);if(!o.existsSync(t))return this.rebuild(e);try{const a=D(t);J(a,e,this.daemonInstallationId);const m=o.existsSync(n)?o.statSync(n).size:0;return a.logByteLength===m?a:this.rebuild(e)}catch(a){if(a instanceof p)throw a;return this.rebuild(e)}}findMutationByRevision(e,s){const t=this.readDeltas(e,s-1,1)[0];if(!t||t.revision!==s)throw new w(`missing mutation revision ${s} in ${e}`);return t}readSparseIndex(e,s){const t=r.join(e,"sparse-index.json");if(!o.existsSync(t))return C(s);const n=D(t);if(n.schemaVersion!==v||n.sessionId!==s||n.segment!==b)throw new w(`invalid sparse index for session ${s}`);return n}readMessageState(e,s){const t=`.${E(s)}.json`,n=this.listMessageStateFiles(e).find(m=>m.name.endsWith(t));if(!n)return null;const a=D(r.join(this.messageStateDir(e),n.name));if(a.messageId!==s)throw new w(`message state hash collision for ${s}`);return a}listMessageStateFiles(e){const s=this.messageStateDir(e);return o.existsSync(s)?o.readdirSync(s).map(t=>({name:t,match:/^(\d{16})\.[a-f0-9]{32}\.json$/.exec(t)})).filter(t=>!!t.match).map(t=>({name:t.name,sequence:Number(t.match[1])})):[]}quarantineCorruptSegment(e,s,t){const n=r.join(this.rootDir,"quarantine");o.mkdirSync(n,{recursive:!0,mode:448});const a=r.join(n,`${F(e)}-${Date.now()}-${b}`);throw o.renameSync(s,a),I(r.join(this.sessionDir(e),"CORRUPT.json"),{schemaVersion:v,sessionId:e,quarantined:a,detectedAt:new Date().toISOString(),reason:t instanceof Error?t.message:String(t)}),new w(`canonical segment for ${e} was quarantined at ${a}`,{cause:t})}assertHealthy(e){const s=r.join(e,"CORRUPT.json");if(o.existsSync(s))throw new w(`canonical session is quarantined: ${s}`)}sessionDir(e){return r.join(this.rootDir,"sessions",F(e))}messageStateDir(e){return r.join(e,"messages")}messageStateFile(e,s,t){return r.join(this.messageStateDir(e),`${String(s).padStart(16,"0")}.${E(t)}.json`)}}function k(i){if(!i.sessionId||!i.messageId)throw new q("sessionId and messageId are required");if(i.message&&(i.message.id!==i.messageId||i.message.sessionId!==i.sessionId))throw new q("message identity does not match commit identity");if(i.mutation!=="tombstone"&&!i.message)throw new q(`${i.mutation} mutation requires a message`)}function J(i,e,s){if(i.daemonInstallationId!==s)throw new p(`canonical session ${e} belongs to a different daemon installation`);if(i.schemaVersion!==v||i.sessionId!==e||i.segment!==b||!Number.isSafeInteger(i.bodyRevision)||i.bodyRevision<0)throw new Error(`invalid canonical manifest for ${e}`)}function A(i,e){let s;try{s=JSON.parse(i)}catch(t){throw new w(`malformed canonical mutation in ${e}`,{cause:t})}if(s.schemaVersion!==v||s.sessionId!==e||!Number.isSafeInteger(s.revision)||s.revision<1||!Number.isSafeInteger(s.sequence)||s.sequence<1||!["append","upsert","tombstone"].includes(s.mutation))throw new w(`invalid canonical mutation in ${e}`);return s}function P(i,e){return{schemaVersion:v,daemonInstallationId:e,sessionId:i,bodyRevision:0,nextSequence:1,firstAvailableSeq:null,lastAvailableSeq:null,mutationCount:0,visibleMessageCount:0,lastVisibleMessageId:null,lastBodyCommittedAt:null,segment:b,logByteLength:0,updatedAt:new Date(0).toISOString()}}function C(i){return{schemaVersion:v,sessionId:i,segment:b,entries:[]}}function B(){return{bodyRevision:0,firstAvailableSeq:null,lastAvailableSeq:null,hasOlder:!1,messages:[],entries:[]}}function O(i){if(!Number.isSafeInteger(i)||i<1||i>500)throw new q("limit must be an integer between 1 and 500");return i}function F(i){return M.createHash("sha256").update(i).digest("hex")}function E(i){return M.createHash("sha256").update(i).digest("hex").slice(0,32)}function z(i){return M.createHash("sha256").update(JSON.stringify({mutation:i.mutation,message:i.message??null,deliveryState:i.deliveryState??null})).digest("hex")}function T(i,e,s,t,n){return M.createHash("sha256").update(`${i}\0${e}\0${s}\0${t}\0${n}`).digest("hex")}function _(i,e){o.mkdirSync(r.dirname(i),{recursive:!0,mode:448});const s=o.openSync(i,"a",384);try{const t=o.fstatSync(s).size;return o.writeFileSync(s,e,"utf8"),o.fsyncSync(s),t}finally{o.closeSync(s)}}function I(i,e){const s=r.dirname(i);o.mkdirSync(s,{recursive:!0,mode:448});const t=`${i}.tmp-${process.pid}-${M.randomBytes(6).toString("hex")}`;let n;try{n=o.openSync(t,"wx",384),o.writeFileSync(n,`${JSON.stringify(e,null,2)}
1
+ import M from"node:crypto";import o from"node:fs";import c from"node:path";const v=1,H=64,b="mutations-000001.jsonl";class q extends Error{constructor(e,s){super(e,s),this.name="CanonicalStoreError"}}class w extends q{constructor(e,s){super(e,s),this.name="CanonicalStoreCorruptionError"}}class p extends q{constructor(e){super(e),this.name="CanonicalStoreConflictError"}}class K{rootDir;daemonInstallationId;mutationListeners=new Set;constructor(e,s){if(!s)throw new q("daemonInstallationId is required");this.rootDir=c.resolve(e),this.daemonInstallationId=s}commit(e){k(e);const s=this.sessionDir(e.sessionId);this.assertHealthy(s),o.mkdirSync(this.messageStateDir(s),{recursive:!0,mode:448});let t=this.loadManifestOrRebuild(e.sessionId);const n=this.readMessageState(s,e.messageId),a=z(e);if(n&&n.contentHash===a&&n.tombstoned===(e.mutation==="tombstone"))return{bodyRevision:t.bodyRevision,sequence:n.sequence,mutation:this.findMutationByRevision(e.sessionId,n.lastRevision),duplicate:!0};if(e.mutation==="append"&&n)throw new p(`message ${e.messageId} already exists in session ${e.sessionId}; use upsert`);const d=n?.sequence??t.nextSequence,h=t.bodyRevision+1,S=e.committedAt??new Date().toISOString(),y={schemaVersion:v,mutationId:T(e.sessionId,e.messageId,e.mutation,n?.contentHash??"none",a),sessionId:e.sessionId,revision:h,mutation:e.mutation,messageId:e.messageId,sequence:d,...e.message?{message:e.message}:{},...e.deliveryState?{deliveryState:e.deliveryState}:{},committedAt:S,contentHash:a},l=c.join(s,b),u=_(l,`${JSON.stringify(y)}
2
+ `),r={schemaVersion:v,sessionId:e.sessionId,messageId:e.messageId,sequence:d,lastRevision:h,contentHash:a,tombstoned:e.mutation==="tombstone",...e.message?{message:e.message}:{},...e.deliveryState?{deliveryState:e.deliveryState}:{}};I(this.messageStateFile(s,d,e.messageId),r);const f=this.readSparseIndex(s,e.sessionId);(h===1||(h-1)%H===0)&&(f.entries.push({revision:h,offset:u}),I(c.join(s,"sparse-index.json"),f)),t={...t,bodyRevision:h,nextSequence:n?t.nextSequence:d+1,firstAvailableSeq:t.firstAvailableSeq??d,lastAvailableSeq:Math.max(t.lastAvailableSeq??d,d),mutationCount:t.mutationCount+1,visibleMessageCount:t.visibleMessageCount+(!n&&e.mutation!=="tombstone"?1:0)-(n&&!n.tombstoned&&e.mutation==="tombstone"?1:0),lastVisibleMessageId:e.mutation==="tombstone"?t.lastVisibleMessageId===e.messageId?this.findLastVisibleMessageId(s,e.messageId):t.lastVisibleMessageId:e.messageId,lastBodyCommittedAt:S,logByteLength:o.statSync(l).size,updatedAt:S},I(c.join(s,"manifest.json"),t);for(const x of this.mutationListeners)try{x(y)}catch(j){console.error("[canonical-store] mutation listener failed",j)}return{bodyRevision:h,sequence:d,mutation:y,duplicate:!1}}subscribe(e){return this.mutationListeners.add(e),()=>this.mutationListeners.delete(e)}append(e,s,t){return this.commit({sessionId:e,mutation:"append",messageId:s.id,message:s,deliveryState:t})}upsert(e,s,t){return this.commit({sessionId:e,mutation:"upsert",messageId:s.id,message:s,deliveryState:t})}tombstone(e,s,t){return this.commit({sessionId:e,mutation:"tombstone",messageId:s,deliveryState:t})}readRecent(e,s){return this.readPage(e,{limit:s})}readPage(e,s){const t=O(s.limit),n=this.sessionDir(e);if(!o.existsSync(n))return B();const a=this.loadManifestOrRebuild(e),d=this.listMessageStateFiles(n).filter(l=>s.beforeSequence===void 0||l.sequence<s.beforeSequence).sort((l,u)=>u.sequence-l.sequence),h=[];let S=0;for(const l of d){S+=1;const u=D(c.join(this.messageStateDir(n),l.name));if(!u.tombstoned&&u.message&&h.push(u),h.length>=t)break}h.sort((l,u)=>l.sequence-u.sequence);const y=h.length>0&&d.slice(S).some(l=>{const u=D(c.join(this.messageStateDir(n),l.name));return!u.tombstoned&&!!u.message});return{bodyRevision:a.bodyRevision,firstAvailableSeq:a.firstAvailableSeq,lastAvailableSeq:a.lastAvailableSeq,hasOlder:y,messages:h.map(l=>l.message),entries:h.map(l=>({sequence:l.sequence,revision:l.lastRevision,message:l.message,...l.deliveryState?{deliveryState:l.deliveryState}:{}}))}}readPageAtRevision(e,s){const t=O(s.limit),n=this.sessionDir(e);if(!o.existsSync(n))return B();const a=this.loadManifestOrRebuild(e);if(!Number.isSafeInteger(s.revision)||s.revision<0||s.revision>a.bodyRevision)throw new q("snapshot revision is unavailable");const d=new Map,h=L(c.join(n,b),0);for(const r of h.split(`
3
+ `)){if(!r)continue;const f=A(r,e);if(f.revision>s.revision)break;d.set(f.messageId,{schemaVersion:v,sessionId:e,messageId:f.messageId,sequence:f.sequence,lastRevision:f.revision,contentHash:f.contentHash,tombstoned:f.mutation==="tombstone",...f.message?{message:f.message}:{},...f.deliveryState?{deliveryState:f.deliveryState}:{}})}const S=[...d.values()].filter(r=>!r.tombstoned&&r.message&&(s.beforeSequence===void 0||r.sequence<s.beforeSequence)).sort((r,f)=>f.sequence-r.sequence),y=S.slice(0,t).sort((r,f)=>r.sequence-f.sequence),u=[...d.values()].filter(r=>!r.tombstoned&&r.message).map(r=>r.sequence);return{bodyRevision:s.revision,firstAvailableSeq:u.length?Math.min(...u):null,lastAvailableSeq:u.length?Math.max(...u):null,hasOlder:S.length>y.length,messages:y.map(r=>r.message),entries:y.map(r=>({sequence:r.sequence,revision:r.lastRevision,message:r.message,...r.deliveryState?{deliveryState:r.deliveryState}:{}}))}}readDeltas(e,s,t){if(!Number.isSafeInteger(s)||s<0)throw new q("afterRevision must be a non-negative safe integer");const n=O(t),a=this.sessionDir(e);if(!o.existsSync(a))return[];const d=this.loadManifestOrRebuild(e);if(s>=d.bodyRevision)return[];const h=this.readSparseIndex(a,e),S=s+1,y=[...h.entries].reverse().find(r=>r.revision<=S),l=L(c.join(a,b),y?.offset??0),u=[];for(const r of l.split(`
4
+ `)){if(!r)continue;const f=A(r,e);if(f.revision>s&&u.push(f),u.length>=n)break}return u}getManifest(e){const s=this.sessionDir(e);return o.existsSync(s)?this.loadManifestOrRebuild(e):null}getMessageState(e,s){const t=this.sessionDir(e);if(!o.existsSync(t))return null;this.loadManifestOrRebuild(e);const n=this.readMessageState(t,s);return n?{sequence:n.sequence,lastRevision:n.lastRevision,tombstoned:n.tombstoned,message:n.message,deliveryState:n.deliveryState}:null}listSessionIds(){const e=c.join(this.rootDir,"sessions");if(!o.existsSync(e))return[];const s=[];for(const t of o.readdirSync(e,{withFileTypes:!0})){if(!t.isDirectory())continue;const n=c.join(e,t.name,"manifest.json");o.existsSync(n)&&s.push(D(n).sessionId)}return s.sort()}purgeSession(e){if(!e)throw new q("sessionId is required");const s=c.resolve(this.rootDir,"sessions"),t=c.resolve(s,F(e));if(c.dirname(t)!==s)throw new q("invalid session path");o.rmSync(t,{recursive:!0,force:!0})}findLastVisibleMessageId(e,s){for(const t of this.listMessageStateFiles(e).sort((n,a)=>a.sequence-n.sequence)){const n=D(c.join(this.messageStateDir(e),t.name));if(n.messageId!==s&&!n.tombstoned&&n.message)return n.messageId}return null}rebuild(e){const s=this.sessionDir(e);this.assertHealthy(s),o.mkdirSync(s,{recursive:!0,mode:448});const t=c.join(s,"manifest.json");if(o.existsSync(t))try{const m=JSON.parse(o.readFileSync(t,"utf8"));if(m.daemonInstallationId&&m.daemonInstallationId!==this.daemonInstallationId)throw new p(`canonical session ${e} belongs to a different daemon installation`)}catch(m){if(m instanceof p)throw m}const n=c.join(s,b);if(!o.existsSync(n)){const m=P(e,this.daemonInstallationId);return I(c.join(s,"manifest.json"),m),I(c.join(s,"sparse-index.json"),C(e)),m}const a=o.readFileSync(n);let d=a.length;if(a.length>0&&a[a.length-1]!==10){const m=a.lastIndexOf(10);d=m<0?0:m+1,o.truncateSync(n,d)}const h=a.subarray(0,d).toString("utf8"),S=new Map,y=C(e);let l=1,u=0,r=new Date(0).toISOString();try{for(const m of h.match(/.*\n/g)??[]){const R=m.slice(0,-1);if(!R){u+=Buffer.byteLength(m);continue}const g=A(R,e);if(g.revision!==l)throw new w(`non-contiguous revision in ${e}: expected ${l}, got ${g.revision}`);(g.revision===1||(g.revision-1)%H===0)&&y.entries.push({revision:g.revision,offset:u});const V=S.get(g.messageId);if(V&&V.sequence!==g.sequence)throw new w(`sequence changed for message ${g.messageId}`);S.set(g.messageId,{schemaVersion:v,sessionId:e,messageId:g.messageId,sequence:g.sequence,lastRevision:g.revision,contentHash:g.contentHash,tombstoned:g.mutation==="tombstone",...g.message?{message:g.message}:{},...g.deliveryState?{deliveryState:g.deliveryState}:{}}),l+=1,u+=Buffer.byteLength(m),r=g.committedAt}}catch(m){this.quarantineCorruptSegment(e,n,m)}const f=this.messageStateDir(s);o.rmSync(f,{recursive:!0,force:!0}),o.mkdirSync(f,{recursive:!0,mode:448});for(const m of S.values())I(this.messageStateFile(s,m.sequence,m.messageId),m);I(c.join(s,"sparse-index.json"),y);const x=[...S.values()].map(m=>m.sequence),j=[...S.values()].filter(m=>!m.tombstoned&&!!m.message).sort((m,R)=>m.sequence-R.sequence),$=l-1,N={schemaVersion:v,daemonInstallationId:this.daemonInstallationId,sessionId:e,bodyRevision:$,nextSequence:x.length===0?1:Math.max(...x)+1,firstAvailableSeq:x.length===0?null:Math.min(...x),lastAvailableSeq:x.length===0?null:Math.max(...x),mutationCount:$,visibleMessageCount:j.length,lastVisibleMessageId:j.at(-1)?.messageId??null,lastBodyCommittedAt:$>0?r:null,segment:b,logByteLength:d,updatedAt:r};return I(c.join(s,"manifest.json"),N),N}loadManifestOrRebuild(e){const s=this.sessionDir(e);this.assertHealthy(s);const t=c.join(s,"manifest.json"),n=c.join(s,b);if(!o.existsSync(t))return this.rebuild(e);try{const a=D(t);J(a,e,this.daemonInstallationId);const d=o.existsSync(n)?o.statSync(n).size:0;return a.logByteLength===d?a:this.rebuild(e)}catch(a){if(a instanceof p)throw a;return this.rebuild(e)}}findMutationByRevision(e,s){const t=this.readDeltas(e,s-1,1)[0];if(!t||t.revision!==s)throw new w(`missing mutation revision ${s} in ${e}`);return t}readSparseIndex(e,s){const t=c.join(e,"sparse-index.json");if(!o.existsSync(t))return C(s);const n=D(t);if(n.schemaVersion!==v||n.sessionId!==s||n.segment!==b)throw new w(`invalid sparse index for session ${s}`);return n}readMessageState(e,s){const t=`.${E(s)}.json`,n=this.listMessageStateFiles(e).find(d=>d.name.endsWith(t));if(!n)return null;const a=D(c.join(this.messageStateDir(e),n.name));if(a.messageId!==s)throw new w(`message state hash collision for ${s}`);return a}listMessageStateFiles(e){const s=this.messageStateDir(e);return o.existsSync(s)?o.readdirSync(s).map(t=>({name:t,match:/^(\d{16})\.[a-f0-9]{32}\.json$/.exec(t)})).filter(t=>!!t.match).map(t=>({name:t.name,sequence:Number(t.match[1])})):[]}quarantineCorruptSegment(e,s,t){const n=c.join(this.rootDir,"quarantine");o.mkdirSync(n,{recursive:!0,mode:448});const a=c.join(n,`${F(e)}-${Date.now()}-${b}`);throw o.renameSync(s,a),I(c.join(this.sessionDir(e),"CORRUPT.json"),{schemaVersion:v,sessionId:e,quarantined:a,detectedAt:new Date().toISOString(),reason:t instanceof Error?t.message:String(t)}),new w(`canonical segment for ${e} was quarantined at ${a}`,{cause:t})}assertHealthy(e){const s=c.join(e,"CORRUPT.json");if(o.existsSync(s))throw new w(`canonical session is quarantined: ${s}`)}sessionDir(e){return c.join(this.rootDir,"sessions",F(e))}messageStateDir(e){return c.join(e,"messages")}messageStateFile(e,s,t){return c.join(this.messageStateDir(e),`${String(s).padStart(16,"0")}.${E(t)}.json`)}}function k(i){if(!i.sessionId||!i.messageId)throw new q("sessionId and messageId are required");if(i.message&&(i.message.id!==i.messageId||i.message.sessionId!==i.sessionId))throw new q("message identity does not match commit identity");if(i.mutation!=="tombstone"&&!i.message)throw new q(`${i.mutation} mutation requires a message`)}function J(i,e,s){if(i.daemonInstallationId!==s)throw new p(`canonical session ${e} belongs to a different daemon installation`);if(i.schemaVersion!==v||i.sessionId!==e||i.segment!==b||!Number.isSafeInteger(i.bodyRevision)||i.bodyRevision<0)throw new Error(`invalid canonical manifest for ${e}`)}function A(i,e){let s;try{s=JSON.parse(i)}catch(t){throw new w(`malformed canonical mutation in ${e}`,{cause:t})}if(s.schemaVersion!==v||s.sessionId!==e||!Number.isSafeInteger(s.revision)||s.revision<1||!Number.isSafeInteger(s.sequence)||s.sequence<1||!["append","upsert","tombstone"].includes(s.mutation))throw new w(`invalid canonical mutation in ${e}`);return s}function P(i,e){return{schemaVersion:v,daemonInstallationId:e,sessionId:i,bodyRevision:0,nextSequence:1,firstAvailableSeq:null,lastAvailableSeq:null,mutationCount:0,visibleMessageCount:0,lastVisibleMessageId:null,lastBodyCommittedAt:null,segment:b,logByteLength:0,updatedAt:new Date(0).toISOString()}}function C(i){return{schemaVersion:v,sessionId:i,segment:b,entries:[]}}function B(){return{bodyRevision:0,firstAvailableSeq:null,lastAvailableSeq:null,hasOlder:!1,messages:[],entries:[]}}function O(i){if(!Number.isSafeInteger(i)||i<1||i>500)throw new q("limit must be an integer between 1 and 500");return i}function F(i){return M.createHash("sha256").update(i).digest("hex")}function E(i){return M.createHash("sha256").update(i).digest("hex").slice(0,32)}function z(i){return M.createHash("sha256").update(JSON.stringify({mutation:i.mutation,message:i.message??null,deliveryState:i.deliveryState??null})).digest("hex")}function T(i,e,s,t,n){return M.createHash("sha256").update(`${i}\0${e}\0${s}\0${t}\0${n}`).digest("hex")}function _(i,e){o.mkdirSync(c.dirname(i),{recursive:!0,mode:448});const s=o.openSync(i,"a",384);try{const t=o.fstatSync(s).size;return o.writeFileSync(s,e,"utf8"),o.fsyncSync(s),t}finally{o.closeSync(s)}}function I(i,e){const s=c.dirname(i);o.mkdirSync(s,{recursive:!0,mode:448});const t=`${i}.tmp-${process.pid}-${M.randomBytes(6).toString("hex")}`;let n;try{n=o.openSync(t,"wx",384),o.writeFileSync(n,`${JSON.stringify(e,null,2)}
5
5
  `,"utf8"),o.fsyncSync(n),o.closeSync(n),n=void 0,o.renameSync(t,i),U(s)}finally{n!==void 0&&o.closeSync(n);try{o.unlinkSync(t)}catch{}}}function U(i){if(process.platform==="win32")return;const e=o.openSync(i,"r");try{o.fsyncSync(e)}finally{o.closeSync(e)}}function D(i){try{return JSON.parse(o.readFileSync(i,"utf8"))}catch(e){throw new w(`failed to read canonical JSON file ${i}`,{cause:e})}}function L(i,e){const s=o.openSync(i,"r");try{const t=o.fstatSync(s).size;if(e>=t)return"";const n=Buffer.allocUnsafe(t-e);return o.readSync(s,n,0,n.length,e),n.toString("utf8")}finally{o.closeSync(s)}}export{K as CanonicalSessionStore,p as CanonicalStoreConflictError,w as CanonicalStoreCorruptionError,q as CanonicalStoreError};
@@ -1,5 +1,5 @@
1
- import c from"node:fs";import{randomUUID as p}from"node:crypto";import{buildUserMessagePayload as R}from"@shennian/wire";import{mergeProjectedSessions as T}from"./projection.js";import{parsePersonalAttachmentReferences as m}from"./attachment-reference.js";import{assertFinishedAttachmentReferences as k,markAttachmentReferences as q}from"./attachment-transfer-store.js";import{resolvePersonalSyncV1Path as w}from"../personal-sync/paths.js";import{appendMessage as b,getMessageDeliveryStatus as E,recordSession as v,tombstoneMessage as P,upsertMessage as D}from"./store.js";import{requestSessionIndexPublish as y}from"./index-publisher.js";const I=w("outbound-queue.json");function A(){return{sessions:{}}}function M(){return new Date().toISOString()}function Q(n){return`enqueue-send-${n}-${p()}`}function l(){try{const n=JSON.parse(c.readFileSync(I,"utf-8"));return{sessions:n.sessions&&typeof n.sessions=="object"?n.sessions:{}}}catch(n){if(n.code==="ENOENT"||n instanceof Error&&n.message.startsWith("ENOENT:"))return A();throw n}}function f(n){const e=w();c.mkdirSync(e,{recursive:!0,mode:448});const s=`${I}.tmp-${process.pid}-${p()}`;try{c.writeFileSync(s,`${JSON.stringify(n,null,2)}
2
- `,{encoding:"utf8",mode:384,flag:"wx"});const t=c.openSync(s,"r");if(typeof t=="number")try{c.fsyncSync(t)}finally{c.closeSync(t)}c.renameSync(s,I)}finally{try{c.unlinkSync(s)}catch{}}}function $(n){const e=M();return{id:n.queueMessageId||n.clientMessageId||`queue-${p()}`,sessionId:n.sessionId,text:n.text,agentType:n.agentType,sessionMode:n.sessionMode,managerConfig:n.managerConfig,workDir:n.workDir,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,systemPrompt:n.systemPrompt??null,managerDefaultWorkerAgentType:n.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:n.managerDefaultWorkerModelId??null,reasoningEffort:n.reasoningEffort??null,clientMessageId:n.clientMessageId??null,attachments:m(n.attachments),externalChannel:n.externalChannel??null,replyTarget:n.replyTarget??null,origin:n.origin,createdAt:e,updatedAt:e}}function x(n){return{id:n.clientMessageId??n.id,sessionId:n.sessionId,role:"user",ts:Date.parse(n.createdAt),payload:R(n.text,n.attachments)}}class _{opts;draining=new Set;constructor(e){this.opts=e,setTimeout(()=>this.drainIdleQueues(),0).unref?.()}getSnapshot(e){return{sessionId:e,busy:!!this.opts.getRuntime().sessions.get(e)?.currentRunId,pending:l().sessions[e]??[]}}async handleEnqueue(e){const s=this.opts.getRuntime(),t=e.params;if(T(t.sessionListProjection),!t.sessionId||!t.text||!t.agentType||!t.workDir){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId, text, agentType and workDir are required"});return}const i=s.resolvePath(t.workDir),o=m(t.attachments);o?.length&&k(i,o);const r={text:t.text,attachments:o,localized:!1},d=await this.isSessionBusy(t.sessionId),u=l(),h=u.sessions[t.sessionId]??[];if(!d&&!h.length){await this.opts.dispatchReq({...e,id:Q(e.id),method:"chat.send",params:{...t,text:r.text,responseId:e.id,clientMessageId:t.clientMessageId??t.queueMessageId,waitForDispatch:!0,attachments:r.attachments}});return}const a=$({...t,text:r.text,attachments:r.attachments}),g=t.origin==="external"?C(h,a):null;g||(u.sessions[t.sessionId]=[...h,a],f(u),v({sessionId:a.sessionId,agentType:a.agentType,sessionMode:a.sessionMode,managerConfig:a.managerConfig,workDir:a.workDir,agentSessionId:a.agentSessionId,modelId:a.modelId}),b(a.sessionId,x(a),"queued"),q(i,a.attachments??[]),y(s.client)),this.broadcast(t.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queued:!0,queueMessageId:g?.id??a.id,...E(t.sessionId,g?.clientMessageId??g?.id??a.clientMessageId??a.id),queue:this.getSnapshot(t.sessionId),...r.localized?{localizedAttachments:!0}:{}}}),d||this.drainNext(t.sessionId)}async handleGet(e){const s=this.opts.getRuntime(),t=e.params;if(!t.sessionId){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId is required"});return}s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}}),this.getSnapshot(t.sessionId).busy||this.drainNext(t.sessionId)}async handleEdit(e){const s=this.opts.getRuntime(),t=e.params;if(!t.sessionId||!t.queueMessageId||!t.text){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId, queueMessageId and text are required"});return}const i=l(),o=i.sessions[t.sessionId]??[],r=o.findIndex(h=>h.id===t.queueMessageId);if(r<0){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"Queued message not found"});return}const d=m(t.attachments),u=s.resolvePath(o[r].workDir);d?.length&&k(u,d),o[r]={...o[r],text:t.text,attachments:d,updatedAt:M()},i.sessions[t.sessionId]=o,f(i),D(t.sessionId,x(o[r]),"queued"),o[r].attachments?.length&&q(u,o[r].attachments??[]),y(s.client),this.broadcast(t.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}})}async handleDelete(e){const s=this.opts.getRuntime(),t=e.params;if(!t.sessionId||!t.queueMessageId){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId and queueMessageId are required"});return}const i=l(),o=i.sessions[t.sessionId]??[],r=o.find(u=>u.id===t.queueMessageId),d=o.filter(u=>u.id!==t.queueMessageId);if(d.length===o.length){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"Queued message not found"});return}d.length?i.sessions[t.sessionId]=d:delete i.sessions[t.sessionId],f(i),P(t.sessionId,r?.clientMessageId??r?.id??t.queueMessageId),y(s.client),this.broadcast(t.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}})}noteTerminal(e){this.drainNext(e)}purgeSession(e){const s=l();s.sessions[e]&&(delete s.sessions[e],f(s),this.draining.delete(e))}drainIdleQueues(){const e=l();for(const s of Object.keys(e.sessions))this.drainNext(s)}async drainNext(e){if(this.draining.has(e)||await this.isSessionBusy(e))return;const i=(l().sessions[e]??[])[0];if(!i){this.broadcast(e);return}const o=i;this.draining.add(e);try{D(e,x(o),"queued"),await this.dispatchQueuedMessage(o);const r=l(),d=(r.sessions[e]??[]).filter(u=>u.id!==i.id);d.length?r.sessions[e]=d:delete r.sessions[e],f(r),this.broadcast(e)}finally{this.draining.delete(e)}}mergeExternalMessages(e,s){const t=[e];for(;s[0]?.origin==="external";)t.push(s.shift());const i=W(t);return i.length===1?i[0]:{...e,id:`external-batch-${e.id}`,text:i.map(o=>o.text).filter(Boolean).join(`
1
+ import c from"node:fs";import{randomUUID as p}from"node:crypto";import{buildUserMessagePayload as R}from"@shennian/wire";import{mergeProjectedSessions as T}from"./projection.js";import{parsePersonalAttachmentReferences as m}from"./attachment-reference.js";import{assertFinishedAttachmentReferences as S,markAttachmentReferences as k}from"./attachment-transfer-store.js";import{resolvePersonalSyncV1Path as q}from"../personal-sync/paths.js";import{appendMessage as b,getMessageDeliveryStatus as v,recordSession as E,tombstoneMessage as P,upsertMessage as w}from"./store.js";import{requestSessionIndexPublish as y}from"./index-publisher.js";function A(){return{sessions:{}}}function I(){return new Date().toISOString()}function $(s){return`enqueue-send-${s}-${p()}`}function l(){try{const s=JSON.parse(c.readFileSync(D(),"utf-8"));return{sessions:s.sessions&&typeof s.sessions=="object"?s.sessions:{}}}catch(s){if(s.code==="ENOENT"||s instanceof Error&&s.message.startsWith("ENOENT:"))return A();throw s}}function h(s){const e=q();c.mkdirSync(e,{recursive:!0,mode:448});const n=D(),t=`${n}.tmp-${process.pid}-${p()}`;try{c.writeFileSync(t,`${JSON.stringify(s,null,2)}
2
+ `,{encoding:"utf8",mode:384,flag:"wx"});const i=c.openSync(t,"r");if(typeof i=="number")try{c.fsyncSync(i)}finally{c.closeSync(i)}c.renameSync(t,n)}finally{try{c.unlinkSync(t)}catch{}}}function D(){return q("outbound-queue.json")}function Q(s){const e=I();return{id:s.queueMessageId||s.clientMessageId||`queue-${p()}`,sessionId:s.sessionId,text:s.text,agentType:s.agentType,sessionMode:s.sessionMode,managerConfig:s.managerConfig,workDir:s.workDir,agentSessionId:s.agentSessionId??null,modelId:s.modelId??null,systemPrompt:s.systemPrompt??null,managerDefaultWorkerAgentType:s.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:s.managerDefaultWorkerModelId??null,reasoningEffort:s.reasoningEffort??null,clientMessageId:s.clientMessageId??null,attachments:m(s.attachments),externalChannel:s.externalChannel??null,replyTarget:s.replyTarget??null,origin:s.origin,createdAt:e,updatedAt:e}}function M(s){return{id:s.clientMessageId??s.id,sessionId:s.sessionId,role:"user",ts:Date.parse(s.createdAt),payload:R(s.text,s.attachments)}}class H{opts;draining=new Set;constructor(e){this.opts=e,setTimeout(()=>this.drainIdleQueues(),0).unref?.()}getSnapshot(e){return{sessionId:e,busy:!!this.opts.getRuntime().sessions.get(e)?.currentRunId,pending:l().sessions[e]??[]}}async handleEnqueue(e){const n=this.opts.getRuntime(),t=e.params;if(T(t.sessionListProjection),!t.sessionId||!t.text||!t.agentType||!t.workDir){n.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId, text, agentType and workDir are required"});return}const i=n.resolvePath(t.workDir),o=m(t.attachments);o?.length&&S(i,o);const r={text:t.text,attachments:o,localized:!1},d=await this.isSessionBusy(t.sessionId),u=l(),f=u.sessions[t.sessionId]??[];if(!d&&!f.length){await this.opts.dispatchReq({...e,id:$(e.id),method:"chat.send",params:{...t,text:r.text,responseId:e.id,clientMessageId:t.clientMessageId??t.queueMessageId,waitForDispatch:!0,attachments:r.attachments}});return}const a=Q({...t,text:r.text,attachments:r.attachments}),g=t.origin==="external"?C(f,a):null;g||(u.sessions[t.sessionId]=[...f,a],h(u),E({sessionId:a.sessionId,agentType:a.agentType,sessionMode:a.sessionMode,managerConfig:a.managerConfig,workDir:a.workDir,agentSessionId:a.agentSessionId,modelId:a.modelId}),b(a.sessionId,M(a),"queued"),k(i,a.attachments??[]),y(n.client)),this.broadcast(t.sessionId),n.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queued:!0,queueMessageId:g?.id??a.id,...v(t.sessionId,g?.clientMessageId??g?.id??a.clientMessageId??a.id),queue:this.getSnapshot(t.sessionId),...r.localized?{localizedAttachments:!0}:{}}}),d||this.drainNext(t.sessionId)}async handleGet(e){const n=this.opts.getRuntime(),t=e.params;if(!t.sessionId){n.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId is required"});return}n.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}}),this.getSnapshot(t.sessionId).busy||this.drainNext(t.sessionId)}async handleEdit(e){const n=this.opts.getRuntime(),t=e.params;if(!t.sessionId||!t.queueMessageId||!t.text){n.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId, queueMessageId and text are required"});return}const i=l(),o=i.sessions[t.sessionId]??[],r=o.findIndex(f=>f.id===t.queueMessageId);if(r<0){n.client.sendRes({type:"res",id:e.id,ok:!1,error:"Queued message not found"});return}const d=m(t.attachments),u=n.resolvePath(o[r].workDir);d?.length&&S(u,d),o[r]={...o[r],text:t.text,attachments:d,updatedAt:I()},i.sessions[t.sessionId]=o,h(i),w(t.sessionId,M(o[r]),"queued"),o[r].attachments?.length&&k(u,o[r].attachments??[]),y(n.client),this.broadcast(t.sessionId),n.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}})}async handleDelete(e){const n=this.opts.getRuntime(),t=e.params;if(!t.sessionId||!t.queueMessageId){n.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId and queueMessageId are required"});return}const i=l(),o=i.sessions[t.sessionId]??[],r=o.find(u=>u.id===t.queueMessageId),d=o.filter(u=>u.id!==t.queueMessageId);if(d.length===o.length){n.client.sendRes({type:"res",id:e.id,ok:!1,error:"Queued message not found"});return}d.length?i.sessions[t.sessionId]=d:delete i.sessions[t.sessionId],h(i),P(t.sessionId,r?.clientMessageId??r?.id??t.queueMessageId),y(n.client),this.broadcast(t.sessionId),n.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}})}noteTerminal(e){this.drainNext(e)}purgeSession(e){const n=l();n.sessions[e]&&(delete n.sessions[e],h(n),this.draining.delete(e))}drainIdleQueues(){const e=l();for(const n of Object.keys(e.sessions))this.drainNext(n)}async drainNext(e){if(this.draining.has(e)||await this.isSessionBusy(e))return;const i=(l().sessions[e]??[])[0];if(!i){this.broadcast(e);return}const o=i;this.draining.add(e);try{w(e,M(o),"queued"),await this.dispatchQueuedMessage(o);const r=l(),d=(r.sessions[e]??[]).filter(u=>u.id!==i.id);d.length?r.sessions[e]=d:delete r.sessions[e],h(r),this.broadcast(e)}finally{this.draining.delete(e)}}mergeExternalMessages(e,n){const t=[e];for(;n[0]?.origin==="external";)t.push(n.shift());const i=W(t);return i.length===1?i[0]:{...e,id:`external-batch-${e.id}`,text:i.map(o=>o.text).filter(Boolean).join(`
3
3
 
4
- `),attachments:B(i.flatMap(o=>o.attachments??[])),updatedAt:M()}}async dispatchQueuedMessage(e){await this.opts.dispatchReq({type:"req",id:`queue-send-${e.id}-${Date.now()}`,method:"chat.send",params:{sessionId:e.sessionId,text:e.text,agentType:e.agentType,sessionMode:e.sessionMode,managerConfig:e.managerConfig,workDir:e.workDir,agentSessionId:e.agentSessionId??null,modelId:e.modelId??void 0,systemPrompt:e.systemPrompt??null,managerDefaultWorkerAgentType:e.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:e.managerDefaultWorkerModelId,reasoningEffort:e.reasoningEffort??void 0,clientMessageId:e.clientMessageId??e.id,attachments:e.attachments,externalChannel:e.externalChannel,replyTarget:e.replyTarget,waitForDispatch:!0}})}async isSessionBusy(e){const s=this.opts.getRuntime().sessions.get(e);if(!s)return!1;if(s.currentRunId)return!0;if(!s.adapter.getStatus)return!1;const t=await s.adapter.getStatus().catch(()=>null);return t?.active?(t.runId&&(s.currentRunId=t.runId,s.currentRunPhase=t.runPhase??s.currentRunPhase),!0):!1}broadcast(e){this.opts.getRuntime().client.sendEvent({type:"event",event:"session.queue.update",payload:{queue:this.getSnapshot(e)}})}}function W(n){const e=new Set,s=[];for(const t of n){const i=S(t);e.has(i)||(e.add(i),s.push(t))}return s}function C(n,e){const s=S(e);return n.find(t=>S(t)===s)??null}function S(n){const e=N(n);return e?`id:${e}`:["body",j(n.text),...(n.attachments??[]).map(s=>s.path||`${s.name}:${s.mimeType}`)].join(`
5
- `)}function N(n){const e=typeof n.clientMessageId=="string"?n.clientMessageId.trim():"";if(e)return e;const s=typeof n.id=="string"?n.id.trim():"";return s&&!s.startsWith("queue-")?s:""}function B(n){const e=new Set,s=[];for(const t of n){const i=t.path||`${t.name}:${t.mimeType}`;e.has(i)||(e.add(i),s.push(t))}return s.length?s:void 0}function j(n){return String(n||"").replace(/\s+/g," ").trim()}export{_ as ChatQueueManager};
4
+ `),attachments:B(i.flatMap(o=>o.attachments??[])),updatedAt:I()}}async dispatchQueuedMessage(e){await this.opts.dispatchReq({type:"req",id:`queue-send-${e.id}-${Date.now()}`,method:"chat.send",params:{sessionId:e.sessionId,text:e.text,agentType:e.agentType,sessionMode:e.sessionMode,managerConfig:e.managerConfig,workDir:e.workDir,agentSessionId:e.agentSessionId??null,modelId:e.modelId??void 0,systemPrompt:e.systemPrompt??null,managerDefaultWorkerAgentType:e.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:e.managerDefaultWorkerModelId,reasoningEffort:e.reasoningEffort??void 0,clientMessageId:e.clientMessageId??e.id,attachments:e.attachments,externalChannel:e.externalChannel,replyTarget:e.replyTarget,waitForDispatch:!0}})}async isSessionBusy(e){const n=this.opts.getRuntime().sessions.get(e);if(!n)return!1;if(n.currentRunId)return!0;if(!n.adapter.getStatus)return!1;const t=await n.adapter.getStatus().catch(()=>null);return t?.active?(t.runId&&(n.currentRunId=t.runId,n.currentRunPhase=t.runPhase??n.currentRunPhase),!0):!1}broadcast(e){this.opts.getRuntime().client.sendEvent({type:"event",event:"session.queue.update",payload:{queue:this.getSnapshot(e)}})}}function W(s){const e=new Set,n=[];for(const t of s){const i=x(t);e.has(i)||(e.add(i),n.push(t))}return n}function C(s,e){const n=x(e);return s.find(t=>x(t)===n)??null}function x(s){const e=N(s);return e?`id:${e}`:["body",j(s.text),...(s.attachments??[]).map(n=>n.path||`${n.name}:${n.mimeType}`)].join(`
5
+ `)}function N(s){const e=typeof s.clientMessageId=="string"?s.clientMessageId.trim():"";if(e)return e;const n=typeof s.id=="string"?s.id.trim():"";return n&&!n.startsWith("queue-")?n:""}function B(s){const e=new Set,n=[];for(const t of s){const i=t.path||`${t.name}:${t.mimeType}`;e.has(i)||(e.add(i),n.push(t))}return n.length?n:void 0}function j(s){return String(s||"").replace(/\s+/g," ").trim()}export{H as ChatQueueManager};
@@ -90,6 +90,7 @@ export type SessionHistoryMessage = {
90
90
  sequence: number;
91
91
  revision: number;
92
92
  message: Envelope;
93
+ deliveryState?: SessionDeliveryState;
93
94
  };
94
95
  export type SessionHistorySnapshot = {
95
96
  sessionId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shennian",
3
- "version": "0.3.48",
3
+ "version": "0.3.49",
4
4
  "description": "Shennian — AI Agent Control Plane CLI",
5
5
  "type": "module",
6
6
  "bin": {