shennian 0.3.85 → 0.3.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/bin/shennian.js +1 -1
  2. package/dist/publish-build-manifest.json +28 -28
  3. package/dist/src/agents/adapter.js +19 -1
  4. package/dist/src/agents/claude-managed-permissions.js +248 -1
  5. package/dist/src/agents/claude.js +393 -8
  6. package/dist/src/agents/codex-managed-permissions.js +294 -1
  7. package/dist/src/agents/codex-managed-room-tools.js +249 -1
  8. package/dist/src/agents/codex-utils.js +200 -7
  9. package/dist/src/agents/codex.js +1352 -17
  10. package/dist/src/agents/config-status.js +226 -1
  11. package/dist/src/agents/cursor-managed-permissions.js +299 -2
  12. package/dist/src/agents/cursor.js +314 -1
  13. package/dist/src/agents/custom.js +418 -4
  14. package/dist/src/agents/external-channel-instructions.js +94 -11
  15. package/dist/src/agents/gemini.js +180 -1
  16. package/dist/src/agents/manager.d.ts +2 -0
  17. package/dist/src/agents/manager.js +250 -18
  18. package/dist/src/agents/model-registry/cache.js +37 -1
  19. package/dist/src/agents/model-registry/discovery.js +188 -2
  20. package/dist/src/agents/model-registry/parsers.js +521 -4
  21. package/dist/src/agents/model-registry/runner.js +30 -1
  22. package/dist/src/agents/model-registry/service.js +115 -1
  23. package/dist/src/agents/model-registry/types.js +8 -1
  24. package/dist/src/agents/model-registry.js +19 -1
  25. package/dist/src/agents/native-capabilities.js +237 -1
  26. package/dist/src/agents/openclaw.js +293 -2
  27. package/dist/src/agents/opencode-managed-permissions.js +187 -2
  28. package/dist/src/agents/opencode.js +298 -1
  29. package/dist/src/agents/platform-instructions.js +46 -17
  30. package/dist/src/agents/runtime-compatibility.js +44 -1
  31. package/dist/src/agents/workbuddy-managed-permissions.js +329 -2
  32. package/dist/src/agents/workbuddy.js +444 -3
  33. package/dist/src/commands/room.js +2 -2
  34. package/dist/src/fs/boundary.js +185 -1
  35. package/dist/src/fs/handler.js +130 -1
  36. package/dist/src/fs/security.js +32 -1
  37. package/dist/src/fs/text-decoder.js +110 -1
  38. package/dist/src/index.js +587 -3
  39. package/dist/src/manager/prompt.js +34 -29
  40. package/dist/src/manager/registry.js +317 -1
  41. package/dist/src/manager/retired-external-channel-compat.js +40 -1
  42. package/dist/src/manager/runtime.d.ts +4 -0
  43. package/dist/src/manager/runtime.js +1514 -23
  44. package/dist/src/native-fusion/codex-parser.js +660 -4
  45. package/dist/src/native-fusion/codex-user-input.js +102 -5
  46. package/dist/src/native-fusion/config.js +5 -1
  47. package/dist/src/native-fusion/file-watcher.js +191 -1
  48. package/dist/src/native-fusion/local-repository.js +569 -2
  49. package/dist/src/native-fusion/opencode-parser.js +130 -3
  50. package/dist/src/native-fusion/parsers.js +140 -3
  51. package/dist/src/native-fusion/service.js +739 -2
  52. package/dist/src/native-fusion/session-header.js +101 -2
  53. package/dist/src/native-fusion/state.js +104 -2
  54. package/dist/src/native-fusion/types.js +1 -1
  55. package/dist/src/personal-sync/machine-authority.js +34 -1
  56. package/dist/src/room/activation-audit.js +45 -2
  57. package/dist/src/room/activation-policy.js +30 -1
  58. package/dist/src/room/activation-service.d.ts +1 -0
  59. package/dist/src/room/activation-service.js +408 -8
  60. package/dist/src/room/invite-bootstrap.js +156 -1
  61. package/dist/src/room/managed-context.d.ts +1 -1
  62. package/dist/src/room/managed-context.js +1 -1
  63. package/dist/src/room/managed-prompt-composer.d.ts +1 -1
  64. package/dist/src/room/managed-prompt-composer.js +6 -5
  65. package/dist/src/room/managed-room-binding.js +167 -1
  66. package/dist/src/room/managed-room-ipc.d.ts +2 -2
  67. package/dist/src/room/managed-room-ipc.js +2 -2
  68. package/dist/src/room/mirror-sync-service.js +183 -1
  69. package/dist/src/room/room-action-client.d.ts +11 -0
  70. package/dist/src/room/room-action-client.js +1 -1
  71. package/dist/src/room/room-binding-client.d.ts +1 -0
  72. package/dist/src/room/room-binding-client.js +151 -1
  73. package/dist/src/security/managed-agent-policy.d.ts +3 -2
  74. package/dist/src/security/managed-agent-policy.js +1 -1
  75. package/dist/src/session/archive-zip.js +220 -1
  76. package/dist/src/session/deletion-sync-state.js +136 -2
  77. package/dist/src/session/handlers/agent-capabilities.js +14 -1
  78. package/dist/src/session/handlers/agent-config.js +150 -1
  79. package/dist/src/session/handlers/agents.js +55 -1
  80. package/dist/src/session/handlers/chat.d.ts +1 -1
  81. package/dist/src/session/handlers/chat.js +1030 -2
  82. package/dist/src/session/handlers/control.js +61 -1
  83. package/dist/src/session/handlers/fs.js +594 -1
  84. package/dist/src/session/handlers/message-status.js +15 -1
  85. package/dist/src/session/handlers/remote-access.js +43 -1
  86. package/dist/src/session/handlers/room-managed.js +101 -1
  87. package/dist/src/session/handlers/session-refresh.js +35 -1
  88. package/dist/src/session/handlers/title.js +79 -1
  89. package/dist/src/session/handlers/tool-detail.js +149 -1
  90. package/dist/src/session/handlers/voice-window.js +62 -1
  91. package/dist/src/session/history-subscriptions.js +470 -1
  92. package/dist/src/session/manager.js +658 -1
  93. package/dist/src/session/native-cleanup-outbox.js +287 -2
  94. package/dist/src/session/native-session-lifecycle.js +327 -6
  95. package/dist/src/session/queue.d.ts +5 -0
  96. package/dist/src/session/queue.js +8 -8
  97. package/dist/src/upgrade/scheduler.js +126 -1
  98. package/node_modules/@shennian/wire/dist/agent-workspace.js +1 -1
  99. package/package.json +1 -1
@@ -1 +1,61 @@
1
- import{SERVERS as r}from"../../region.js";import{loadConfig as i,saveConfig as p}from"../../config/index.js";async function f(o,t){const n=await Promise.all(Object.keys(r).map(async a=>{const{url:s,label:c}=r[a],d=performance.now();try{const l=new AbortController,u=setTimeout(()=>l.abort(),5e3);return await fetch(`${s}/health`,{signal:l.signal}),clearTimeout(u),{region:a,url:s,label:c,latencyMs:Math.round(performance.now()-d)}}catch{return{region:a,url:s,label:c,latencyMs:null}}}));o.client.sendRes({type:"res",id:t.id,ok:!0,payload:{current:"cn",servers:n}})}async function m(o,t){const e=t.params.region;if(e!=="cn"&&e!=="global"){o.client.sendRes({type:"res",id:t.id,ok:!1,error:'region must be "cn" or "global"'});return}const n=i();n.serverUrl=r[e].url,p(n),o.client.sendRes({type:"res",id:t.id,ok:!0,payload:{region:e,url:r[e].url}}),setTimeout(()=>process.exit(0),500)}async function h(o,t){const e=t.params.policy;if(e!=="none"&&e!=="patch"){o.client.sendRes({type:"res",id:t.id,ok:!1,error:'policy must be "none" or "patch"'});return}const n=i();n.autoUpgrade=e,p(n),o.upgradePolicyController?.setPolicy(e),o.client.sendEvent({type:"event",event:"machine.upgrade-policy",payload:{policy:e}}),o.client.sendRes({type:"res",id:t.id,ok:!0,payload:{autoUpgrade:e}})}export{f as handleRegionProbe,m as handleRegionSwitch,h as handleUpgradeSetPolicy};
1
+ // @arch docs/architecture/cli/daemon.md#会话管理
2
+ // @test src/__tests__/model-switching.test.ts
3
+ import { SERVERS } from '../../region.js';
4
+ import { loadConfig, saveConfig } from '../../config/index.js';
5
+ export async function handleRegionProbe(runtime, req) {
6
+ const currentRegion = 'cn';
7
+ const results = await Promise.all(Object.keys(SERVERS).map(async (region) => {
8
+ const { url, label } = SERVERS[region];
9
+ const start = performance.now();
10
+ try {
11
+ const controller = new AbortController();
12
+ const timer = setTimeout(() => controller.abort(), 5000);
13
+ await fetch(`${url}/health`, { signal: controller.signal });
14
+ clearTimeout(timer);
15
+ return { region, url, label, latencyMs: Math.round(performance.now() - start) };
16
+ }
17
+ catch {
18
+ return { region, url, label, latencyMs: null };
19
+ }
20
+ }));
21
+ runtime.client.sendRes({
22
+ type: 'res',
23
+ id: req.id,
24
+ ok: true,
25
+ payload: { current: currentRegion, servers: results },
26
+ });
27
+ }
28
+ export async function handleRegionSwitch(runtime, req) {
29
+ const targetRegion = req.params.region;
30
+ if (targetRegion !== 'cn' && targetRegion !== 'global') {
31
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'region must be "cn" or "global"' });
32
+ return;
33
+ }
34
+ const config = loadConfig();
35
+ config.serverUrl = SERVERS[targetRegion].url;
36
+ saveConfig(config);
37
+ runtime.client.sendRes({
38
+ type: 'res',
39
+ id: req.id,
40
+ ok: true,
41
+ payload: { region: targetRegion, url: SERVERS[targetRegion].url },
42
+ });
43
+ setTimeout(() => process.exit(0), 500);
44
+ }
45
+ export async function handleUpgradeSetPolicy(runtime, req) {
46
+ const policy = req.params.policy;
47
+ if (policy !== 'none' && policy !== 'patch') {
48
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'policy must be "none" or "patch"' });
49
+ return;
50
+ }
51
+ const config = loadConfig();
52
+ config.autoUpgrade = policy;
53
+ saveConfig(config);
54
+ runtime.upgradePolicyController?.setPolicy(policy);
55
+ runtime.client.sendEvent({
56
+ type: 'event',
57
+ event: 'machine.upgrade-policy',
58
+ payload: { policy },
59
+ });
60
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: true, payload: { autoUpgrade: policy } });
61
+ }
@@ -1 +1,594 @@
1
- import l from"node:fs";import R from"node:os";import h from"node:path";import{convertMarkdownToPdf as F,defaultPdfOutputPath as E,findChromiumExecutable as T,installManagedChromium as b,managedChromiumBrowsersPath as I,MarkdownPdfBrowserMissingError as C}from"../../tools/markdown-to-pdf.js";import{createZipArchive as D}from"../archive-zip.js";import{BinaryTextPreviewError as M,decodeTextBufferAuto as N,isAutoTextEncoding as $}from"../../fs/text-decoder.js";import{abortAttachmentTransfer as _,appendAttachmentTransferChunk as O,finishAttachmentTransfer as j,startAttachmentTransfer as B}from"../attachment-transfer-store.js";import{assertPersonalSyncIdentity as k,getPersonalSyncIdentity as u}from"../../personal-sync/epoch.js";import{getDaemonInstallationId as P}from"../../personal-sync/identity.js";const x="__roots__",L=5e3,Z=1024*1024*1024;function v(e){return/^[A-Za-z]:([\\/]|$)/.test(e)||/^\\\\[^\\]+\\[^\\]+/.test(e)}function m(e){return v(e)?h.win32:h.posix}function g(e){const t=l.statSync(e);return{name:m(e).basename(e),path:e,isDir:t.isDirectory(),size:t.isFile()?t.size:void 0,modifiedAt:t.mtimeMs}}function H(){if(R.platform()==="win32"){const e=[];for(let t=65;t<=90;t+=1){const r=`${String.fromCharCode(t)}:\\`;l.existsSync(r)&&e.push({name:r,path:r,isDir:!0})}return e}return[{name:"/",path:"/",isDir:!0}]}function U(e,t){const r=typeof e=="object"&&e&&"code"in e?String(e.code):"";return r==="ENOENT"?`Directory not found: ${t}`:r==="EACCES"||r==="EPERM"?`Permission denied: ${t}`:e instanceof Error?e.message:String(e)}function W(e){const t=e.trim();if(!t||t!==e||e==="."||e===".."||X(e)||/[\\/:]/.test(e)||/[<>|"?*]/.test(e)||/[. ]$/.test(e))return!1;const r=e.split(".")[0]?.toUpperCase();return!(r&&/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/.test(r))}function X(e){return Array.from(e).some(t=>{const r=t.charCodeAt(0);return r>=0&&r<=31})}async function se(e,t){const r=t.params.path||R.homedir(),s=t.params.rootPath||r;if(r===x){e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:x,entries:H()}});return}const o=await e.resolveAuthorizedPathAsync(r,s);if(!o.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:o.error});return}const a=o.path;try{const n=await l.promises.readdir(a,{withFileTypes:!0}),c=v(a)?h.win32.join:h.join,d=n.map(p=>({name:p.name,path:c(a,p.name),isDir:p.isDirectory()})).sort((p,i)=>p.isDir!==i.isDir?p.isDir?-1:1:p.name.localeCompare(i.name));e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:a,entries:d}})}catch(n){e.client.sendRes({type:"res",id:t.id,ok:!1,error:U(n,a)})}}async function ae(e,t){const r=t.params.path,s=t.params.rootPath||r,o=e.resolveAuthorizedPath(r,s);if(!o.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:o.error});return}const a=o.path,n=t.params.encoding,c=typeof n=="string"?n:"utf8",d=t.params.offset,p=t.params.length;try{const i=l.statSync(a);if(!i.isFile()){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Not a file"});return}if(d!=null&&p!=null){const f=Math.min(p,Math.max(0,i.size-d));if(f<=0){e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{data:"",offset:d,length:0,totalSize:i.size,path:a}});return}const y=l.openSync(a,"r");try{const A=Buffer.alloc(f),z=l.readSync(y,A,0,f,d);e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{data:A.subarray(0,z).toString("base64"),offset:d,length:z,totalSize:i.size,path:a}})}finally{l.closeSync(y)}return}const S=t.params.maxSize||(c==="base64"?5*1024*1024:512*1024);if(i.size>S){e.client.sendRes({type:"res",id:t.id,ok:!1,error:`File too large: ${i.size} bytes (max ${S})`,payload:{size:i.size,path:a}});return}if(c==="base64"){const f=l.readFileSync(a);e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{data:f.toString("base64"),path:a,size:i.size}});return}if($(c)){const f=l.readFileSync(a),y=N(f);e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{content:y.content,path:a,size:i.size,encoding:y.encoding,encodingFallback:y.fallback}});return}const w=l.readFileSync(a,"utf-8");e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{content:w,path:a,size:i.size}})}catch(i){e.client.sendRes({type:"res",id:t.id,ok:!1,error:i instanceof M?i.message:String(i)})}}async function oe(e,t){const r=t.params.path,s=t.params.content,o=t.params.rootPath||r;if(!r||typeof s!="string"){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"path and content are required"});return}const a=e.resolveAuthorizedPath(r,o);if(!a.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:a.error});return}try{const n=l.existsSync(a.path)?l.statSync(a.path):null;if(n&&!n.isFile()){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Not a file"});return}l.writeFileSync(a.path,s,"utf-8");const c=l.statSync(a.path);e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:a.path,size:c.size,modifiedAt:c.mtimeMs}})}catch(n){e.client.sendRes({type:"res",id:t.id,ok:!1,error:String(n)})}}async function ne(e,t){const r=t.params.path,s=t.params.newName;if(!r||!s){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"path and newName are required"});return}if(!W(s)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Invalid newName"});return}const o=m(r),a=t.params.rootPath||o.dirname(r),n=e.resolveAuthorizedPath(r,a);if(!n.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:n.error});return}try{const c=m(n.path),d=c.join(c.dirname(n.path),s),p=e.resolveAuthorizedPath(d,a);if(!p.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:p.error});return}if(l.existsSync(p.path)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Target already exists"});return}l.renameSync(n.path,p.path),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{oldPath:n.path,newPath:p.path,entry:g(p.path)}})}catch(c){e.client.sendRes({type:"res",id:t.id,ok:!1,error:String(c)})}}async function ie(e,t){const r=t.params.path,s=t.params.rootPath||h.dirname(r||"."),o=typeof t.params.title=="string"?t.params.title:void 0;if(!r){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"path is required"});return}const a=e.resolveAuthorizedPath(r,s);if(!a.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:a.error});return}if(!/\.mdx?$/i.test(a.path)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Only Markdown files can be exported to PDF"});return}const n=E(a.path),c=e.resolveAuthorizedPath(n,s);if(!c.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:c.error});return}try{const d=await F(a.path,{outputPath:c.path,title:o});e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{sourcePath:a.path,outputPath:d.outputPath,entry:g(d.outputPath)}})}catch(d){if(d instanceof C){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"This machine needs the PDF export component before Markdown files can be exported to PDF.",payload:d.setup});return}e.client.sendRes({type:"res",id:t.id,ok:!1,error:d instanceof Error?d.message:String(d)})}}async function de(e,t){try{await b();const r=await T();e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{ready:!0,browserPath:r,managedPath:I()}})}catch(r){e.client.sendRes({type:"res",id:t.id,ok:!1,error:r instanceof Error?r.message:String(r)})}}function V(e){let t="";for(const s of e.trim())s.charCodeAt(0)<=31||'<>:"/\\|?*'.includes(s)?t.endsWith("-")||(t+="-"):t+=s;return t.replace(/[. ]+$/g,"")||"folder"}async function ce(e,t){const r=t.params.path,s=t.params.rootPath||r;if(!r){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"path is required"});return}const o=e.resolveAuthorizedPath(r,s);if(!o.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:o.error});return}try{if(!l.statSync(o.path).isDirectory()){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Not a directory"});return}const n=m(o.path),c=V(n.basename(o.path)),d=h.join(R.tmpdir(),"shennian-archives"),p=h.join(d,`${c}-${Date.now()}-${Math.random().toString(36).slice(2,8)}.zip`),i=D(o.path,p,{maxFiles:L,maxTotalSize:Z});e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{sourcePath:o.path,outputPath:i.outputPath,entry:g(i.outputPath),fileCount:i.fileCount,totalSize:i.totalSize}})}catch(a){e.client.sendRes({type:"res",id:t.id,ok:!1,error:a instanceof Error?a.message:String(a)})}}async function pe(e,t){const r=t.params;try{k(r);const s=e.resolveAuthorizedPath(r.rootPath,r.rootPath);if(!s.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s.error});return}const o=B({...r,rootPath:s.path});o.reused||e.pendingTransfers.set(r.transferId,{transferId:r.transferId,rootPath:s.path,lastActivityAt:Date.now()}),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{...u(P()),...o,...o.attachment?{attachment:{...u(P()),...o.attachment}}:{}}})}catch(s){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s instanceof Error?s.message:String(s)})}}async function le(e,t){const r=t.params;try{k(r);const s=e.resolveAuthorizedPath(r.rootPath,r.rootPath);if(!s.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s.error});return}const o=Buffer.from(r.data,"base64"),a=O({rootPath:s.path,transferId:r.transferId,relativePath:r.relativePath,offset:r.offset,data:o});e.pendingTransfers.set(r.transferId,{transferId:r.transferId,rootPath:s.path,lastActivityAt:Date.now()}),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{...u(P()),transferId:r.transferId,...a}})}catch(s){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s instanceof Error?s.message:String(s)})}}async function fe(e,t){const r=t.params;try{k(r);const s=e.resolveAuthorizedPath(r.rootPath,r.rootPath);if(!s.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s.error});return}const o=j(s.path,r.transferId);e.pendingTransfers.delete(r.transferId),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{...u(P()),...o}})}catch(s){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s instanceof Error?s.message:String(s)})}}async function he(e,t){const r=t.params;try{k(r);const s=e.resolveAuthorizedPath(r.rootPath,r.rootPath);if(!s.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s.error});return}_(s.path,r.transferId),e.pendingTransfers.delete(r.transferId),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{...u(P()),transferId:r.transferId,aborted:!0}})}catch(s){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s instanceof Error?s.message:String(s)})}}function ye(e){e.pendingTransfers.clear()}export{ye as cleanupPendingTransfers,ce as handleFsArchiveZip,ie as handleFsExportMarkdownPdf,de as handleFsExportMarkdownPdfSetup,se as handleFsLs,ae as handleFsRead,ne as handleFsRename,he as handleFsTransferAbort,le as handleFsTransferChunk,fe as handleFsTransferFinish,pe as handleFsTransferStart,oe as handleFsWrite};
1
+ // @arch docs/architecture/cli/daemon.md#会话管理
2
+ // @test src/__tests__/session-manager.test.ts
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { convertMarkdownToPdf, defaultPdfOutputPath, findChromiumExecutable, installManagedChromium, managedChromiumBrowsersPath, MarkdownPdfBrowserMissingError, } from '../../tools/markdown-to-pdf.js';
7
+ import { createZipArchive } from '../archive-zip.js';
8
+ import { BinaryTextPreviewError, decodeTextBufferAuto, isAutoTextEncoding, } from '../../fs/text-decoder.js';
9
+ import { abortAttachmentTransfer, appendAttachmentTransferChunk, finishAttachmentTransfer, startAttachmentTransfer, } from '../attachment-transfer-store.js';
10
+ import { assertPersonalSyncIdentity, getPersonalSyncIdentity } from '../../personal-sync/epoch.js';
11
+ import { getDaemonInstallationId } from '../../personal-sync/identity.js';
12
+ const FILE_SYSTEM_ROOTS_PATH = '__roots__';
13
+ const MAX_ARCHIVE_FILES = 5000;
14
+ const MAX_ARCHIVE_TOTAL_SIZE = 1024 * 1024 * 1024;
15
+ function isWindowsAbsolutePath(pathValue) {
16
+ return /^[A-Za-z]:([\\/]|$)/.test(pathValue) || /^\\\\[^\\]+\\[^\\]+/.test(pathValue);
17
+ }
18
+ function pathApiForPath(pathValue) {
19
+ return isWindowsAbsolutePath(pathValue) ? path.win32 : path.posix;
20
+ }
21
+ function makeFsEntry(entryPath) {
22
+ const stat = fs.statSync(entryPath);
23
+ const api = pathApiForPath(entryPath);
24
+ return {
25
+ name: api.basename(entryPath),
26
+ path: entryPath,
27
+ isDir: stat.isDirectory(),
28
+ size: stat.isFile() ? stat.size : undefined,
29
+ modifiedAt: stat.mtimeMs,
30
+ };
31
+ }
32
+ function listFileSystemRoots() {
33
+ if (os.platform() === 'win32') {
34
+ const entries = [];
35
+ for (let code = 65; code <= 90; code += 1) {
36
+ const drive = `${String.fromCharCode(code)}:\\`;
37
+ if (fs.existsSync(drive))
38
+ entries.push({ name: drive, path: drive, isDir: true });
39
+ }
40
+ return entries;
41
+ }
42
+ return [{ name: '/', path: '/', isDir: true }];
43
+ }
44
+ function fsErrorMessage(err, fallbackPath) {
45
+ const code = typeof err === 'object' && err && 'code' in err
46
+ ? String(err.code)
47
+ : '';
48
+ if (code === 'ENOENT')
49
+ return `Directory not found: ${fallbackPath}`;
50
+ if (code === 'EACCES' || code === 'EPERM')
51
+ return `Permission denied: ${fallbackPath}`;
52
+ return err instanceof Error ? err.message : String(err);
53
+ }
54
+ function isSafeRenameName(name) {
55
+ const trimmed = name.trim();
56
+ if (!trimmed || trimmed !== name)
57
+ return false;
58
+ if (name === '.' || name === '..')
59
+ return false;
60
+ if (hasControlChar(name) || /[\\/:]/.test(name))
61
+ return false;
62
+ if (/[<>|"?*]/.test(name))
63
+ return false;
64
+ if (/[. ]$/.test(name))
65
+ return false;
66
+ const upper = name.split('.')[0]?.toUpperCase();
67
+ if (upper && /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/.test(upper))
68
+ return false;
69
+ return true;
70
+ }
71
+ function hasControlChar(value) {
72
+ return Array.from(value).some((char) => {
73
+ const code = char.charCodeAt(0);
74
+ return code >= 0x00 && code <= 0x1f;
75
+ });
76
+ }
77
+ export async function handleFsLs(runtime, req) {
78
+ const requestedPath = req.params.path || os.homedir();
79
+ const rootPath = req.params.rootPath || requestedPath;
80
+ if (requestedPath === FILE_SYSTEM_ROOTS_PATH) {
81
+ runtime.client.sendRes({
82
+ type: 'res',
83
+ id: req.id,
84
+ ok: true,
85
+ payload: { path: FILE_SYSTEM_ROOTS_PATH, entries: listFileSystemRoots() },
86
+ });
87
+ return;
88
+ }
89
+ const resolved = await runtime.resolveAuthorizedPathAsync(requestedPath, rootPath);
90
+ if (!resolved.ok) {
91
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
92
+ return;
93
+ }
94
+ const dirPath = resolved.path;
95
+ try {
96
+ const raw = await fs.promises.readdir(dirPath, { withFileTypes: true });
97
+ const joinPath = isWindowsAbsolutePath(dirPath) ? path.win32.join : path.join;
98
+ const entries = raw
99
+ .map((entry) => ({
100
+ name: entry.name,
101
+ path: joinPath(dirPath, entry.name),
102
+ isDir: entry.isDirectory(),
103
+ }))
104
+ .sort((left, right) => {
105
+ if (left.isDir !== right.isDir)
106
+ return left.isDir ? -1 : 1;
107
+ return left.name.localeCompare(right.name);
108
+ });
109
+ runtime.client.sendRes({
110
+ type: 'res',
111
+ id: req.id,
112
+ ok: true,
113
+ payload: { path: dirPath, entries },
114
+ });
115
+ }
116
+ catch (err) {
117
+ runtime.client.sendRes({
118
+ type: 'res',
119
+ id: req.id,
120
+ ok: false,
121
+ error: fsErrorMessage(err, dirPath),
122
+ });
123
+ }
124
+ }
125
+ export async function handleFsRead(runtime, req) {
126
+ const requestedPath = req.params.path;
127
+ const rootPath = req.params.rootPath || requestedPath;
128
+ const resolved = runtime.resolveAuthorizedPath(requestedPath, rootPath);
129
+ if (!resolved.ok) {
130
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
131
+ return;
132
+ }
133
+ const filePath = resolved.path;
134
+ const requestedEncoding = req.params.encoding;
135
+ const encoding = typeof requestedEncoding === 'string' ? requestedEncoding : 'utf8';
136
+ const offset = req.params.offset;
137
+ const length = req.params.length;
138
+ try {
139
+ const stat = fs.statSync(filePath);
140
+ if (!stat.isFile()) {
141
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Not a file' });
142
+ return;
143
+ }
144
+ if (offset != null && length != null) {
145
+ const readLen = Math.min(length, Math.max(0, stat.size - offset));
146
+ if (readLen <= 0) {
147
+ runtime.client.sendRes({
148
+ type: 'res',
149
+ id: req.id,
150
+ ok: true,
151
+ payload: { data: '', offset, length: 0, totalSize: stat.size, path: filePath },
152
+ });
153
+ return;
154
+ }
155
+ const fd = fs.openSync(filePath, 'r');
156
+ try {
157
+ const buffer = Buffer.alloc(readLen);
158
+ const bytesRead = fs.readSync(fd, buffer, 0, readLen, offset);
159
+ runtime.client.sendRes({
160
+ type: 'res',
161
+ id: req.id,
162
+ ok: true,
163
+ payload: {
164
+ data: buffer.subarray(0, bytesRead).toString('base64'),
165
+ offset,
166
+ length: bytesRead,
167
+ totalSize: stat.size,
168
+ path: filePath,
169
+ },
170
+ });
171
+ }
172
+ finally {
173
+ fs.closeSync(fd);
174
+ }
175
+ return;
176
+ }
177
+ const maxSize = req.params.maxSize || (encoding === 'base64' ? 5 * 1024 * 1024 : 512 * 1024);
178
+ if (stat.size > maxSize) {
179
+ runtime.client.sendRes({
180
+ type: 'res',
181
+ id: req.id,
182
+ ok: false,
183
+ error: `File too large: ${stat.size} bytes (max ${maxSize})`,
184
+ payload: { size: stat.size, path: filePath },
185
+ });
186
+ return;
187
+ }
188
+ if (encoding === 'base64') {
189
+ const buffer = fs.readFileSync(filePath);
190
+ runtime.client.sendRes({
191
+ type: 'res',
192
+ id: req.id,
193
+ ok: true,
194
+ payload: { data: buffer.toString('base64'), path: filePath, size: stat.size },
195
+ });
196
+ return;
197
+ }
198
+ if (isAutoTextEncoding(encoding)) {
199
+ const buffer = fs.readFileSync(filePath);
200
+ const decoded = decodeTextBufferAuto(buffer);
201
+ runtime.client.sendRes({
202
+ type: 'res',
203
+ id: req.id,
204
+ ok: true,
205
+ payload: {
206
+ content: decoded.content,
207
+ path: filePath,
208
+ size: stat.size,
209
+ encoding: decoded.encoding,
210
+ encodingFallback: decoded.fallback,
211
+ },
212
+ });
213
+ return;
214
+ }
215
+ const content = fs.readFileSync(filePath, 'utf-8');
216
+ runtime.client.sendRes({
217
+ type: 'res',
218
+ id: req.id,
219
+ ok: true,
220
+ payload: { content, path: filePath, size: stat.size },
221
+ });
222
+ }
223
+ catch (err) {
224
+ runtime.client.sendRes({
225
+ type: 'res',
226
+ id: req.id,
227
+ ok: false,
228
+ error: err instanceof BinaryTextPreviewError ? err.message : String(err),
229
+ });
230
+ }
231
+ }
232
+ export async function handleFsWrite(runtime, req) {
233
+ const requestedPath = req.params.path;
234
+ const content = req.params.content;
235
+ const rootPath = req.params.rootPath || requestedPath;
236
+ if (!requestedPath || typeof content !== 'string') {
237
+ runtime.client.sendRes({
238
+ type: 'res',
239
+ id: req.id,
240
+ ok: false,
241
+ error: 'path and content are required',
242
+ });
243
+ return;
244
+ }
245
+ const resolved = runtime.resolveAuthorizedPath(requestedPath, rootPath);
246
+ if (!resolved.ok) {
247
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
248
+ return;
249
+ }
250
+ try {
251
+ const existing = fs.existsSync(resolved.path) ? fs.statSync(resolved.path) : null;
252
+ if (existing && !existing.isFile()) {
253
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Not a file' });
254
+ return;
255
+ }
256
+ fs.writeFileSync(resolved.path, content, 'utf-8');
257
+ const stat = fs.statSync(resolved.path);
258
+ runtime.client.sendRes({
259
+ type: 'res',
260
+ id: req.id,
261
+ ok: true,
262
+ payload: { path: resolved.path, size: stat.size, modifiedAt: stat.mtimeMs },
263
+ });
264
+ }
265
+ catch (err) {
266
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: String(err) });
267
+ }
268
+ }
269
+ export async function handleFsRename(runtime, req) {
270
+ const requestedPath = req.params.path;
271
+ const newName = req.params.newName;
272
+ if (!requestedPath || !newName) {
273
+ runtime.client.sendRes({
274
+ type: 'res',
275
+ id: req.id,
276
+ ok: false,
277
+ error: 'path and newName are required',
278
+ });
279
+ return;
280
+ }
281
+ if (!isSafeRenameName(newName)) {
282
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Invalid newName' });
283
+ return;
284
+ }
285
+ const api = pathApiForPath(requestedPath);
286
+ const rootPath = req.params.rootPath || api.dirname(requestedPath);
287
+ const resolved = runtime.resolveAuthorizedPath(requestedPath, rootPath);
288
+ if (!resolved.ok) {
289
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
290
+ return;
291
+ }
292
+ try {
293
+ const api = pathApiForPath(resolved.path);
294
+ const newPath = api.join(api.dirname(resolved.path), newName);
295
+ const checkedNewPath = runtime.resolveAuthorizedPath(newPath, rootPath);
296
+ if (!checkedNewPath.ok) {
297
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: checkedNewPath.error });
298
+ return;
299
+ }
300
+ if (fs.existsSync(checkedNewPath.path)) {
301
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Target already exists' });
302
+ return;
303
+ }
304
+ fs.renameSync(resolved.path, checkedNewPath.path);
305
+ runtime.client.sendRes({
306
+ type: 'res',
307
+ id: req.id,
308
+ ok: true,
309
+ payload: {
310
+ oldPath: resolved.path,
311
+ newPath: checkedNewPath.path,
312
+ entry: makeFsEntry(checkedNewPath.path),
313
+ },
314
+ });
315
+ }
316
+ catch (err) {
317
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: String(err) });
318
+ }
319
+ }
320
+ export async function handleFsExportMarkdownPdf(runtime, req) {
321
+ const requestedPath = req.params.path;
322
+ const rootPath = req.params.rootPath || path.dirname(requestedPath || '.');
323
+ const title = typeof req.params.title === 'string' ? req.params.title : undefined;
324
+ if (!requestedPath) {
325
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'path is required' });
326
+ return;
327
+ }
328
+ const resolved = runtime.resolveAuthorizedPath(requestedPath, rootPath);
329
+ if (!resolved.ok) {
330
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
331
+ return;
332
+ }
333
+ if (!/\.mdx?$/i.test(resolved.path)) {
334
+ runtime.client.sendRes({
335
+ type: 'res',
336
+ id: req.id,
337
+ ok: false,
338
+ error: 'Only Markdown files can be exported to PDF',
339
+ });
340
+ return;
341
+ }
342
+ const outputPath = defaultPdfOutputPath(resolved.path);
343
+ const checkedOutput = runtime.resolveAuthorizedPath(outputPath, rootPath);
344
+ if (!checkedOutput.ok) {
345
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: checkedOutput.error });
346
+ return;
347
+ }
348
+ try {
349
+ const result = await convertMarkdownToPdf(resolved.path, {
350
+ outputPath: checkedOutput.path,
351
+ title,
352
+ });
353
+ runtime.client.sendRes({
354
+ type: 'res',
355
+ id: req.id,
356
+ ok: true,
357
+ payload: {
358
+ sourcePath: resolved.path,
359
+ outputPath: result.outputPath,
360
+ entry: makeFsEntry(result.outputPath),
361
+ },
362
+ });
363
+ }
364
+ catch (err) {
365
+ if (err instanceof MarkdownPdfBrowserMissingError) {
366
+ runtime.client.sendRes({
367
+ type: 'res',
368
+ id: req.id,
369
+ ok: false,
370
+ error: 'This machine needs the PDF export component before Markdown files can be exported to PDF.',
371
+ payload: err.setup,
372
+ });
373
+ return;
374
+ }
375
+ runtime.client.sendRes({
376
+ type: 'res',
377
+ id: req.id,
378
+ ok: false,
379
+ error: err instanceof Error ? err.message : String(err),
380
+ });
381
+ }
382
+ }
383
+ export async function handleFsExportMarkdownPdfSetup(runtime, req) {
384
+ try {
385
+ await installManagedChromium();
386
+ const browserPath = await findChromiumExecutable();
387
+ runtime.client.sendRes({
388
+ type: 'res',
389
+ id: req.id,
390
+ ok: true,
391
+ payload: { ready: true, browserPath, managedPath: managedChromiumBrowsersPath() },
392
+ });
393
+ }
394
+ catch (error) {
395
+ runtime.client.sendRes({
396
+ type: 'res',
397
+ id: req.id,
398
+ ok: false,
399
+ error: error instanceof Error ? error.message : String(error),
400
+ });
401
+ }
402
+ }
403
+ function safeArchiveBaseName(name) {
404
+ let safeName = '';
405
+ for (const char of name.trim()) {
406
+ const code = char.charCodeAt(0);
407
+ const unsafe = code <= 0x1f || '<>:"/\\|?*'.includes(char);
408
+ if (unsafe) {
409
+ if (!safeName.endsWith('-'))
410
+ safeName += '-';
411
+ }
412
+ else {
413
+ safeName += char;
414
+ }
415
+ }
416
+ const trimmed = safeName.replace(/[. ]+$/g, '');
417
+ return trimmed || 'folder';
418
+ }
419
+ export async function handleFsArchiveZip(runtime, req) {
420
+ const requestedPath = req.params.path;
421
+ const rootPath = req.params.rootPath || requestedPath;
422
+ if (!requestedPath) {
423
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'path is required' });
424
+ return;
425
+ }
426
+ const resolved = runtime.resolveAuthorizedPath(requestedPath, rootPath);
427
+ if (!resolved.ok) {
428
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
429
+ return;
430
+ }
431
+ try {
432
+ const stat = fs.statSync(resolved.path);
433
+ if (!stat.isDirectory()) {
434
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Not a directory' });
435
+ return;
436
+ }
437
+ const api = pathApiForPath(resolved.path);
438
+ const baseName = safeArchiveBaseName(api.basename(resolved.path));
439
+ const archiveDir = path.join(os.tmpdir(), 'shennian-archives');
440
+ const outputPath = path.join(archiveDir, `${baseName}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.zip`);
441
+ const result = createZipArchive(resolved.path, outputPath, {
442
+ maxFiles: MAX_ARCHIVE_FILES,
443
+ maxTotalSize: MAX_ARCHIVE_TOTAL_SIZE,
444
+ });
445
+ runtime.client.sendRes({
446
+ type: 'res',
447
+ id: req.id,
448
+ ok: true,
449
+ payload: {
450
+ sourcePath: resolved.path,
451
+ outputPath: result.outputPath,
452
+ entry: makeFsEntry(result.outputPath),
453
+ fileCount: result.fileCount,
454
+ totalSize: result.totalSize,
455
+ },
456
+ });
457
+ }
458
+ catch (err) {
459
+ runtime.client.sendRes({
460
+ type: 'res',
461
+ id: req.id,
462
+ ok: false,
463
+ error: err instanceof Error ? err.message : String(err),
464
+ });
465
+ }
466
+ }
467
+ export async function handleFsTransferStart(runtime, req) {
468
+ const params = req.params;
469
+ try {
470
+ assertPersonalSyncIdentity(params);
471
+ const resolved = runtime.resolveAuthorizedPath(params.rootPath, params.rootPath);
472
+ if (!resolved.ok) {
473
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
474
+ return;
475
+ }
476
+ const result = startAttachmentTransfer({ ...params, rootPath: resolved.path });
477
+ if (!result.reused) {
478
+ runtime.pendingTransfers.set(params.transferId, {
479
+ transferId: params.transferId,
480
+ rootPath: resolved.path,
481
+ lastActivityAt: Date.now(),
482
+ });
483
+ }
484
+ runtime.client.sendRes({
485
+ type: 'res', id: req.id, ok: true,
486
+ payload: {
487
+ ...getPersonalSyncIdentity(getDaemonInstallationId()),
488
+ ...result,
489
+ ...(result.attachment
490
+ ? { attachment: { ...getPersonalSyncIdentity(getDaemonInstallationId()), ...result.attachment } }
491
+ : {}),
492
+ },
493
+ });
494
+ }
495
+ catch (err) {
496
+ runtime.client.sendRes({
497
+ type: 'res', id: req.id, ok: false,
498
+ error: err instanceof Error ? err.message : String(err),
499
+ });
500
+ }
501
+ }
502
+ export async function handleFsTransferChunk(runtime, req) {
503
+ const params = req.params;
504
+ try {
505
+ assertPersonalSyncIdentity(params);
506
+ const resolved = runtime.resolveAuthorizedPath(params.rootPath, params.rootPath);
507
+ if (!resolved.ok) {
508
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
509
+ return;
510
+ }
511
+ const buffer = Buffer.from(params.data, 'base64');
512
+ const result = appendAttachmentTransferChunk({
513
+ rootPath: resolved.path,
514
+ transferId: params.transferId,
515
+ relativePath: params.relativePath,
516
+ offset: params.offset,
517
+ data: buffer,
518
+ });
519
+ runtime.pendingTransfers.set(params.transferId, {
520
+ transferId: params.transferId,
521
+ rootPath: resolved.path,
522
+ lastActivityAt: Date.now(),
523
+ });
524
+ runtime.client.sendRes({
525
+ type: 'res', id: req.id, ok: true,
526
+ payload: {
527
+ ...getPersonalSyncIdentity(getDaemonInstallationId()),
528
+ transferId: params.transferId,
529
+ ...result,
530
+ },
531
+ });
532
+ }
533
+ catch (err) {
534
+ runtime.client.sendRes({
535
+ type: 'res', id: req.id, ok: false,
536
+ error: err instanceof Error ? err.message : String(err),
537
+ });
538
+ }
539
+ }
540
+ export async function handleFsTransferFinish(runtime, req) {
541
+ const params = req.params;
542
+ try {
543
+ assertPersonalSyncIdentity(params);
544
+ const resolved = runtime.resolveAuthorizedPath(params.rootPath, params.rootPath);
545
+ if (!resolved.ok) {
546
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
547
+ return;
548
+ }
549
+ const result = finishAttachmentTransfer(resolved.path, params.transferId);
550
+ runtime.pendingTransfers.delete(params.transferId);
551
+ runtime.client.sendRes({
552
+ type: 'res', id: req.id, ok: true,
553
+ payload: { ...getPersonalSyncIdentity(getDaemonInstallationId()), ...result },
554
+ });
555
+ }
556
+ catch (err) {
557
+ runtime.client.sendRes({
558
+ type: 'res', id: req.id, ok: false,
559
+ error: err instanceof Error ? err.message : String(err),
560
+ });
561
+ }
562
+ }
563
+ export async function handleFsTransferAbort(runtime, req) {
564
+ const params = req.params;
565
+ try {
566
+ assertPersonalSyncIdentity(params);
567
+ const resolved = runtime.resolveAuthorizedPath(params.rootPath, params.rootPath);
568
+ if (!resolved.ok) {
569
+ runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: resolved.error });
570
+ return;
571
+ }
572
+ abortAttachmentTransfer(resolved.path, params.transferId);
573
+ runtime.pendingTransfers.delete(params.transferId);
574
+ runtime.client.sendRes({
575
+ type: 'res', id: req.id, ok: true,
576
+ payload: {
577
+ ...getPersonalSyncIdentity(getDaemonInstallationId()),
578
+ transferId: params.transferId,
579
+ aborted: true,
580
+ },
581
+ });
582
+ }
583
+ catch (err) {
584
+ runtime.client.sendRes({
585
+ type: 'res', id: req.id, ok: false,
586
+ error: err instanceof Error ? err.message : String(err),
587
+ });
588
+ }
589
+ }
590
+ export function cleanupPendingTransfers(runtime) {
591
+ // Staging is deliberately durable across daemon restarts. The 10-minute
592
+ // lease sweeper owns deletion; process shutdown must not destroy resumable data.
593
+ runtime.pendingTransfers.clear();
594
+ }