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,3 +1,444 @@
1
- import{spawnSync as y}from"node:child_process";import{createInterface as w}from"node:readline";import{randomUUID as k}from"node:crypto";import E from"node:path";import{AgentAdapter as S,registerAgent as P}from"./adapter.js";import{resolveBuiltinCommand as I,spawnAgentCommand as x}from"./command-spec.js";import{buildAgentProcessEnv as _,buildManagedAgentProcessEnv as B}from"../agent-env.js";import{buildPlatformInstructions as C,ensurePlatformInstructionsFile as A}from"./platform-instructions.js";import{resolveWindowsSystemRoot as v}from"../windows-shell.js";import{resolveManagedAgentAdapterEnforcement as W}from"../security/managed-agent-policy.js";import{registerManagedAgentRuntimeSecurity as D}from"../security/managed-agent-runtime.js";import{resolveManagedRoomIpcPaths as T}from"../daemon-ipc/paths.js";import{compileWorkBuddyManagedPermissions as M,createWorkBuddyManagedConfigFiles as O,disposeWorkBuddyManagedConfigFiles as R,WORKBUDDY_MANAGED_RUNTIME_SECURITY as N}from"./workbuddy-managed-permissions.js";D("workbuddy",N);const b=new Set(["minimal","low","medium","high","xhigh","max"]);function q(n){const e=n?.trim();return e&&e!=="auto"?e:void 0}function F(n){const e=n?.trim();if(e){if(b.has(e))return e;throw new Error(`Unsupported WorkBuddy reasoning effort "${e}". Supported values: minimal, low, medium, high, xhigh, max.`)}}function U(n,e,t){if(!n)return[];if(e==="win32"){if(!t)throw new Error("WorkBuddy system prompt file is required on Windows.");return["--system-prompt-file",t]}return["--append-system-prompt",n]}function $(n){const e=n.split(/\n\n(?=<[^>\n]+>\n)/);return(e[e.length-1]??n).replace(/^\[群消息\][ \t]+<[^>\n]+>\n/,"").replace(/^\[群消息\]\n?/,"").replace(/^<[^>\n]+>\n/,"").trim()}function c(n,e=process.platform,t=z){e==="win32"&&n.pid&&t(n.pid)||n.kill("SIGTERM")}function z(n){const e=E.win32.join(v(),"System32","taskkill.exe");return y(e,["/pid",String(n),"/T","/F"],{stdio:"ignore",windowsHide:!0,timeout:5e3}).status===0}class j extends S{type="workbuddy";process=null;agentSessionId=null;workDir=null;seq=0;runId="";terminalState="open";hasEmittedText=!1;runPhase=null;persistNativeSession=!0;externalChannel=null;shennianSessionId=null;extraEnv={};managedAgentPolicy=null;managedConfigDir=null;managedProcessEnv=null;configure(e){this.shennianSessionId=e.sessionId??null,this.externalChannel=e.externalChannel??null,this.extraEnv=e.env??{},"managedAgentPolicy"in e&&(this.managedAgentPolicy=e.managedAgentPolicy??null),this.managedAgentPolicy&&(this.workDir=this.managedAgentPolicy.adapterPolicy.workDir)}async start(e,t,s,r){this.workDir=this.managedAgentPolicy?.adapterPolicy.workDir??t,this.seq=0,this.persistNativeSession=r?.persistNativeSession!==!1,s&&(this.agentSessionId=s)}async send(e,t,s,r,o,i){if((i?.source==="room_activation"||this.shennianSessionId?.startsWith("room-managed-")===!0)&&!this.managedAgentPolicy)throw new Error("managed_agent_policy_missing");await this.killProcess(),this.runId=k(),this.resetRunState();const d=i?.source==="room_activation",a=["-p",d?$(e):e,"--output-format","stream-json","--verbose"];this.persistNativeSession||a.push("--no-session-persistence");const h=q(t);h&&a.push("--model",h),d&&a.push("--max-turns","8");const m=this.shennianSessionId?.startsWith("room-managed-")&&!this.agentSessionId&&!d?null:C(this.workDir??process.cwd(),this.externalChannel,this.shennianSessionId??void 0,o);if(m){const f=process.platform==="win32"?A(this.workDir??process.cwd(),this.externalChannel,this.shennianSessionId??void 0,o):void 0;a.push(...U(m,process.platform,f))}const p=F(s);p&&a.push("--effort",p),this.agentSessionId&&a.push("--resume",this.agentSessionId),a.push(...await this.prepareManagedLaunch()),this.spawnAndParse(a)}async resume(e){await this.killProcess(),this.agentSessionId=e,this.resetRunState()}async stop(){await this.killProcess()}async getStatus(){return{active:this.process!=null&&this.terminalState==="open",runId:this.runId||null,runPhase:this.runPhase,canStop:this.process!=null&&this.terminalState==="open"}}spawnAndParse(e){const t=I("workbuddy");if(!t){this.disposeManagedConfig(),this.emit("error",new Error("WorkBuddy was not found. Install WorkBuddy Desktop or the CodeBuddy CLI, then retry."));return}let s;try{s=x(t,e,{cwd:this.workDir??void 0,stdio:["ignore","pipe","pipe"],env:this.buildProcessEnv()})}catch(i){throw this.disposeManagedConfig(),i}this.process=s,w({input:s.stdout}).on("line",i=>{if(i.trim())try{this.handleStreamEvent(JSON.parse(i))}catch{}});let o="";s.stderr?.on("data",i=>{o=`${o}${i.toString()}`.slice(-8e3)}),s.on("close",i=>{this.process===s&&(this.process=null,this.runPhase=null,this.disposeManagedConfig(),this.handleProcessClose(i,o))}),s.on("error",i=>{if(this.process!==s)return;this.process=null,this.runPhase=null,this.disposeManagedConfig();const l=i.code==="ENOENT";this.emitErrorIfOpen({state:"error",message:l?"WorkBuddy was not found. Install WorkBuddy Desktop or the CodeBuddy CLI, then retry.":u(i.message)})})}handleStreamEvent(e){if(e.type==="system"&&e.subtype==="init"&&e.session_id){this.agentSessionId=e.session_id,this.runPhase="thinking",this.emitEvent({state:"start",agentSessionId:e.session_id});return}if(e.type==="control_request"&&e.request?.subtype==="can_use_tool"){this.runPhase="waiting_approval",this.emitEvent({state:"approval-pending",name:e.request.tool_name,args:e.request.input,approval:{title:`WorkBuddy \u8BF7\u6C42\u4F7F\u7528 ${e.request.tool_name||"\u5DE5\u5177"}`,source:"workbuddy",actionHint:"\u8BF7\u5728 WorkBuddy \u6216\u795E\u5FF5\u4E2D\u786E\u8BA4\u540E\u7EE7\u7EED\u3002"}});return}if(e.type==="assistant"){const s=e.message?.content;if(s?.length)for(const r of s)this.handleContentBlock(r);else e.text&&this.emitText(e.text,!!e.thinking);return}if(e.type!=="result")return;if(e.subtype==="tool_result"){this.runPhase="tool_running",this.emitEvent({state:"tool-result",name:e.name,result:g(e.content)});return}if(e.subtype==="error"||e.error){this.emitErrorIfOpen({state:"error",message:u(e.error||e.result||"WorkBuddy failed.")});return}!this.hasEmittedText&&e.result&&this.emitText(e.result,!1),e.session_id&&(this.agentSessionId=e.session_id);const t=e.usage??e.message?.usage;this.emitFinalIfOpen({state:"final",agentSessionId:this.agentSessionId??void 0,usage:t?{inputTokens:t.input_tokens??0,outputTokens:t.output_tokens??0}:void 0})}handleContentBlock(e){e.type==="text"&&e.text?this.emitText(e.text,!1):e.type==="thinking"&&e.thinking?this.emitText(e.thinking,!0):e.type==="tool_use"?(this.runPhase="tool_running",this.emitEvent({state:"tool-call",name:e.name,args:e.input})):e.type==="tool_result"&&this.emitEvent({state:"tool-result",name:e.name,result:g(e.content)})}emitText(e,t){this.runPhase=t?"thinking":"streaming_text";const s=!t&&this.hasEmittedText?`
2
-
3
- `:"";this.emitEvent({state:"delta",text:s+e,thinking:t||void 0}),t||(this.hasEmittedText=!0)}handleProcessClose(e,t){if(e!==0&&e!==null){this.emitErrorIfOpen({state:"error",message:u(t)||`WorkBuddy exited with code ${e}.`});return}this.emitFinalIfOpen({state:"final",agentSessionId:this.agentSessionId??void 0})}emitFinalIfOpen(e){this.terminalState==="open"&&(this.terminalState="final",this.runPhase=null,this.emitEvent(e))}emitErrorIfOpen(e){this.terminalState==="open"&&(this.terminalState="error",this.runPhase=null,this.emitEvent(e))}emitEvent(e){this.emit("agentEvent",{...e,runId:this.runId,seq:this.seq++})}resetRunState(){this.seq=0,this.terminalState="open",this.hasEmittedText=!1,this.runPhase=null}async killProcess(){const e=this.process;if(!e){await this.disposeManagedConfig();return}this.process=null,this.runPhase=null,c(e),await new Promise(t=>{let s=!1;const r=()=>{s||(s=!0,t())};e.once("close",r),setTimeout(()=>{c(e),r()},3e3).unref?.()}),await this.disposeManagedConfig()}async prepareManagedLaunch(){const e=this.managedAgentPolicy;if(!e)return this.managedProcessEnv=null,["--permission-mode","bypassPermissions","--setting-sources","user,project,local","--tools","default"];const t=T(),s=M(W(e),e.adapterPolicy.workDir,{internalUnixSockets:t.transport==="unix_socket"?[t.endpoint]:[]}),r=await O(s);this.managedConfigDir=r.directory;const o=B({agentType:"workbuddy",providerEnv:this.extraEnv,sessionEnv:this.extraEnv,runtimeEnv:{NO_COLOR:"1"}});return Object.assign(o,s.runtimeEnv),delete o.CODEBUDDY_IS_SANDBOX,this.managedProcessEnv=o,["--permission-mode","dontAsk","--subagent-permission-mode","dontAsk","--settings",r.settingsPath,"--setting-sources","none","--strict-mcp-config","--mcp-config","{}","--tools",s.toolNames.join(",")]}buildProcessEnv(){return this.managedProcessEnv??_(this.extraEnv)}async disposeManagedConfig(){const e=this.managedConfigDir;this.managedConfigDir=null,this.managedProcessEnv=null,e&&await R(e).catch(()=>{})}}function g(n){if(typeof n=="string")return n;try{return JSON.stringify(n)}catch{return"[unserializable WorkBuddy tool result]"}}function u(n){return n.replace(/(Authorization\s*:\s*)Bearer\s+[A-Za-z0-9._~+/-]+/gi,"$1Bearer [redacted]").replace(/(api[-_ ]?key|access[-_ ]?token|refresh[-_ ]?token|connector[-_ ]?token)(\s*[:=]\s*)[^\s,;]+/gi,"$1$2[redacted]").trim().slice(0,2e3)}P("workbuddy",()=>new j);export{j as WorkBuddyAdapter,q as normalizeWorkBuddyModelId,F as normalizeWorkBuddyReasoningEffort,u as sanitizeWorkBuddyError,c as terminateWorkBuddyProcess,$ as workBuddyRoomActivationText,U as workBuddySystemPromptArgs};
1
+ // @arch docs/architecture/cli/agent-adapters.md
2
+ // @test src/__tests__/workbuddy-adapter.test.ts
3
+ import { spawnSync } from 'node:child_process';
4
+ import { createInterface } from 'node:readline';
5
+ import { randomUUID } from 'node:crypto';
6
+ import path from 'node:path';
7
+ import { AgentAdapter, registerAgent, } from './adapter.js';
8
+ import { resolveBuiltinCommand, spawnAgentCommand } from './command-spec.js';
9
+ import { buildAgentProcessEnv, buildManagedAgentProcessEnv } from '../agent-env.js';
10
+ import { buildPlatformInstructions, ensurePlatformInstructionsFile, } from './platform-instructions.js';
11
+ import { resolveWindowsSystemRoot } from '../windows-shell.js';
12
+ import { resolveManagedAgentAdapterEnforcement, } from '../security/managed-agent-policy.js';
13
+ import { registerManagedAgentRuntimeSecurity } from '../security/managed-agent-runtime.js';
14
+ import { resolveManagedRoomIpcPaths } from '../daemon-ipc/paths.js';
15
+ import { compileWorkBuddyManagedPermissions, createWorkBuddyManagedConfigFiles, disposeWorkBuddyManagedConfigFiles, WORKBUDDY_MANAGED_RUNTIME_SECURITY, } from './workbuddy-managed-permissions.js';
16
+ registerManagedAgentRuntimeSecurity('workbuddy', WORKBUDDY_MANAGED_RUNTIME_SECURITY);
17
+ const WORKBUDDY_EFFORTS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
18
+ export function normalizeWorkBuddyModelId(modelId) {
19
+ const trimmed = modelId?.trim();
20
+ return trimmed && trimmed !== 'auto' ? trimmed : undefined;
21
+ }
22
+ export function normalizeWorkBuddyReasoningEffort(value) {
23
+ const trimmed = value?.trim();
24
+ if (!trimmed)
25
+ return undefined;
26
+ if (WORKBUDDY_EFFORTS.has(trimmed))
27
+ return trimmed;
28
+ throw new Error(`Unsupported WorkBuddy reasoning effort "${trimmed}". Supported values: minimal, low, medium, high, xhigh, max.`);
29
+ }
30
+ export function workBuddySystemPromptArgs(systemPrompt, platform, promptFile) {
31
+ if (!systemPrompt)
32
+ return [];
33
+ if (platform === 'win32') {
34
+ if (!promptFile)
35
+ throw new Error('WorkBuddy system prompt file is required on Windows.');
36
+ return ['--system-prompt-file', promptFile];
37
+ }
38
+ return ['--append-system-prompt', systemPrompt];
39
+ }
40
+ export function workBuddyRoomActivationText(text) {
41
+ const blocks = text.split(/\n\n(?=<[^>\n]+>\n)/);
42
+ const current = blocks[blocks.length - 1] ?? text;
43
+ return current
44
+ .replace(/^\[群消息\][ \t]+<[^>\n]+>\n/, '')
45
+ .replace(/^\[群消息\]\n?/, '')
46
+ .replace(/^<[^>\n]+>\n/, '')
47
+ .trim();
48
+ }
49
+ export function terminateWorkBuddyProcess(proc, platform = process.platform, terminateWindowsTree = terminateWindowsProcessTree) {
50
+ if (platform === 'win32' && proc.pid && terminateWindowsTree(proc.pid))
51
+ return;
52
+ proc.kill('SIGTERM');
53
+ }
54
+ function terminateWindowsProcessTree(pid) {
55
+ const taskkill = path.win32.join(resolveWindowsSystemRoot(), 'System32', 'taskkill.exe');
56
+ const result = spawnSync(taskkill, ['/pid', String(pid), '/T', '/F'], {
57
+ stdio: 'ignore',
58
+ windowsHide: true,
59
+ timeout: 5_000,
60
+ });
61
+ return result.status === 0;
62
+ }
63
+ export class WorkBuddyAdapter extends AgentAdapter {
64
+ type = 'workbuddy';
65
+ process = null;
66
+ agentSessionId = null;
67
+ workDir = null;
68
+ seq = 0;
69
+ runId = '';
70
+ terminalState = 'open';
71
+ hasEmittedText = false;
72
+ runPhase = null;
73
+ persistNativeSession = true;
74
+ externalChannel = null;
75
+ shennianSessionId = null;
76
+ extraEnv = {};
77
+ managedAgentPolicy = null;
78
+ managedConfigDir = null;
79
+ managedProcessEnv = null;
80
+ configure(options) {
81
+ this.shennianSessionId = options.sessionId ?? null;
82
+ this.externalChannel = options.externalChannel ?? null;
83
+ this.extraEnv = options.env ?? {};
84
+ if ('managedAgentPolicy' in options) {
85
+ this.managedAgentPolicy = options.managedAgentPolicy ?? null;
86
+ }
87
+ if (this.managedAgentPolicy)
88
+ this.workDir = this.managedAgentPolicy.adapterPolicy.workDir;
89
+ }
90
+ async start(_sessionId, workDir, agentSessionId, options) {
91
+ this.workDir = this.managedAgentPolicy?.adapterPolicy.workDir ?? workDir;
92
+ this.seq = 0;
93
+ this.persistNativeSession = options?.persistNativeSession !== false;
94
+ if (agentSessionId)
95
+ this.agentSessionId = agentSessionId;
96
+ }
97
+ async send(text, modelId, reasoningEffort, _attachments, agentSystemPrompt, context) {
98
+ const managedInvocation = context?.source === 'room_activation' ||
99
+ this.shennianSessionId?.startsWith('room-managed-') === true;
100
+ if (managedInvocation && !this.managedAgentPolicy) {
101
+ throw new Error('managed_agent_policy_missing');
102
+ }
103
+ await this.killProcess();
104
+ this.runId = randomUUID();
105
+ this.resetRunState();
106
+ const managedRoomActivation = context?.source === 'room_activation';
107
+ // CodeBuddy's resumed native Session treats the Room's `<speaker>` block
108
+ // envelope as already-consumed input and exits without calling the tool.
109
+ // Give it the current addressed message only. Additional history remains
110
+ // available through the scoped Shennian CLI read command.
111
+ const promptText = managedRoomActivation ? workBuddyRoomActivationText(text) : text;
112
+ const args = ['-p', promptText, '--output-format', 'stream-json', '--verbose'];
113
+ if (!this.persistNativeSession)
114
+ args.push('--no-session-persistence');
115
+ const resolvedModelId = normalizeWorkBuddyModelId(modelId);
116
+ if (resolvedModelId)
117
+ args.push('--model', resolvedModelId);
118
+ if (managedRoomActivation)
119
+ args.push('--max-turns', '8');
120
+ // Windows launches the npm WorkBuddy shim through cmd.exe, whose command
121
+ // line is capped at 8191 characters. The managed bootstrap only needs a
122
+ // native Session id, so avoid attaching the large platform prompt until
123
+ // that Session exists. Room activations still receive the full prompt.
124
+ const isManagedBootstrap = this.shennianSessionId?.startsWith('room-managed-') &&
125
+ !this.agentSessionId &&
126
+ !managedRoomActivation;
127
+ const systemPrompt = isManagedBootstrap
128
+ ? null
129
+ : buildPlatformInstructions(this.workDir ?? process.cwd(), this.externalChannel, this.shennianSessionId ?? undefined, agentSystemPrompt);
130
+ if (systemPrompt) {
131
+ // The npm shim is launched through cmd.exe on Windows. Room context can
132
+ // exceed its 8191-character command-line limit, so pass the same private
133
+ // generated instructions by file instead of truncating the activation.
134
+ const promptFile = process.platform === 'win32'
135
+ ? ensurePlatformInstructionsFile(this.workDir ?? process.cwd(), this.externalChannel, this.shennianSessionId ?? undefined, agentSystemPrompt)
136
+ : undefined;
137
+ args.push(...workBuddySystemPromptArgs(systemPrompt, process.platform, promptFile));
138
+ }
139
+ const effort = normalizeWorkBuddyReasoningEffort(reasoningEffort);
140
+ if (effort)
141
+ args.push('--effort', effort);
142
+ if (this.agentSessionId)
143
+ args.push('--resume', this.agentSessionId);
144
+ args.push(...(await this.prepareManagedLaunch()));
145
+ this.spawnAndParse(args);
146
+ }
147
+ async resume(agentSessionId) {
148
+ await this.killProcess();
149
+ this.agentSessionId = agentSessionId;
150
+ this.resetRunState();
151
+ }
152
+ async stop() {
153
+ await this.killProcess();
154
+ }
155
+ async getStatus() {
156
+ return {
157
+ active: this.process != null && this.terminalState === 'open',
158
+ runId: this.runId || null,
159
+ runPhase: this.runPhase,
160
+ canStop: this.process != null && this.terminalState === 'open',
161
+ };
162
+ }
163
+ spawnAndParse(args) {
164
+ const spec = resolveBuiltinCommand('workbuddy');
165
+ if (!spec) {
166
+ void this.disposeManagedConfig();
167
+ this.emit('error', new Error('WorkBuddy was not found. Install WorkBuddy Desktop or the CodeBuddy CLI, then retry.'));
168
+ return;
169
+ }
170
+ let proc;
171
+ try {
172
+ proc = spawnAgentCommand(spec, args, {
173
+ cwd: this.workDir ?? undefined,
174
+ stdio: ['ignore', 'pipe', 'pipe'],
175
+ env: this.buildProcessEnv(),
176
+ });
177
+ }
178
+ catch (error) {
179
+ void this.disposeManagedConfig();
180
+ throw error;
181
+ }
182
+ this.process = proc;
183
+ const lines = createInterface({ input: proc.stdout });
184
+ lines.on('line', (line) => {
185
+ if (!line.trim())
186
+ return;
187
+ try {
188
+ this.handleStreamEvent(JSON.parse(line));
189
+ }
190
+ catch {
191
+ // WorkBuddy may write non-protocol notices to stdout. Never surface
192
+ // unstructured output because it may contain local connector details.
193
+ }
194
+ });
195
+ let stderr = '';
196
+ proc.stderr?.on('data', (chunk) => {
197
+ stderr = `${stderr}${chunk.toString()}`.slice(-8_000);
198
+ });
199
+ proc.on('close', (code) => {
200
+ if (this.process !== proc)
201
+ return;
202
+ this.process = null;
203
+ this.runPhase = null;
204
+ void this.disposeManagedConfig();
205
+ this.handleProcessClose(code, stderr);
206
+ });
207
+ proc.on('error', (error) => {
208
+ if (this.process !== proc)
209
+ return;
210
+ this.process = null;
211
+ this.runPhase = null;
212
+ void this.disposeManagedConfig();
213
+ const missing = error.code === 'ENOENT';
214
+ this.emitErrorIfOpen({
215
+ state: 'error',
216
+ message: missing
217
+ ? 'WorkBuddy was not found. Install WorkBuddy Desktop or the CodeBuddy CLI, then retry.'
218
+ : sanitizeWorkBuddyError(error.message),
219
+ });
220
+ });
221
+ }
222
+ handleStreamEvent(event) {
223
+ if (event.type === 'system' && event.subtype === 'init' && event.session_id) {
224
+ this.agentSessionId = event.session_id;
225
+ this.runPhase = 'thinking';
226
+ this.emitEvent({ state: 'start', agentSessionId: event.session_id });
227
+ return;
228
+ }
229
+ if (event.type === 'control_request' && event.request?.subtype === 'can_use_tool') {
230
+ this.runPhase = 'waiting_approval';
231
+ this.emitEvent({
232
+ state: 'approval-pending',
233
+ name: event.request.tool_name,
234
+ args: event.request.input,
235
+ approval: {
236
+ title: `WorkBuddy 请求使用 ${event.request.tool_name || '工具'}`,
237
+ source: 'workbuddy',
238
+ actionHint: '请在 WorkBuddy 或神念中确认后继续。',
239
+ },
240
+ });
241
+ return;
242
+ }
243
+ if (event.type === 'assistant') {
244
+ const blocks = event.message?.content;
245
+ if (blocks?.length) {
246
+ for (const block of blocks)
247
+ this.handleContentBlock(block);
248
+ }
249
+ else if (event.text) {
250
+ this.emitText(event.text, Boolean(event.thinking));
251
+ }
252
+ return;
253
+ }
254
+ if (event.type !== 'result')
255
+ return;
256
+ if (event.subtype === 'tool_result') {
257
+ this.runPhase = 'tool_running';
258
+ this.emitEvent({
259
+ state: 'tool-result',
260
+ name: event.name,
261
+ result: stringifyToolResult(event.content),
262
+ });
263
+ return;
264
+ }
265
+ if (event.subtype === 'error' || event.error) {
266
+ this.emitErrorIfOpen({
267
+ state: 'error',
268
+ message: sanitizeWorkBuddyError(event.error || event.result || 'WorkBuddy failed.'),
269
+ });
270
+ return;
271
+ }
272
+ if (!this.hasEmittedText && event.result)
273
+ this.emitText(event.result, false);
274
+ if (event.session_id)
275
+ this.agentSessionId = event.session_id;
276
+ const usage = event.usage ?? event.message?.usage;
277
+ this.emitFinalIfOpen({
278
+ state: 'final',
279
+ agentSessionId: this.agentSessionId ?? undefined,
280
+ usage: usage
281
+ ? { inputTokens: usage.input_tokens ?? 0, outputTokens: usage.output_tokens ?? 0 }
282
+ : undefined,
283
+ });
284
+ }
285
+ handleContentBlock(block) {
286
+ if (block.type === 'text' && block.text) {
287
+ this.emitText(block.text, false);
288
+ }
289
+ else if (block.type === 'thinking' && block.thinking) {
290
+ this.emitText(block.thinking, true);
291
+ }
292
+ else if (block.type === 'tool_use') {
293
+ this.runPhase = 'tool_running';
294
+ this.emitEvent({ state: 'tool-call', name: block.name, args: block.input });
295
+ }
296
+ else if (block.type === 'tool_result') {
297
+ this.emitEvent({
298
+ state: 'tool-result',
299
+ name: block.name,
300
+ result: stringifyToolResult(block.content),
301
+ });
302
+ }
303
+ }
304
+ emitText(text, thinking) {
305
+ this.runPhase = thinking ? 'thinking' : 'streaming_text';
306
+ const prefix = !thinking && this.hasEmittedText ? '\n\n' : '';
307
+ this.emitEvent({ state: 'delta', text: prefix + text, thinking: thinking || undefined });
308
+ if (!thinking)
309
+ this.hasEmittedText = true;
310
+ }
311
+ handleProcessClose(code, stderr) {
312
+ if (code !== 0 && code !== null) {
313
+ this.emitErrorIfOpen({
314
+ state: 'error',
315
+ message: sanitizeWorkBuddyError(stderr) || `WorkBuddy exited with code ${code}.`,
316
+ });
317
+ return;
318
+ }
319
+ this.emitFinalIfOpen({ state: 'final', agentSessionId: this.agentSessionId ?? undefined });
320
+ }
321
+ emitFinalIfOpen(partial) {
322
+ if (this.terminalState !== 'open')
323
+ return;
324
+ this.terminalState = 'final';
325
+ this.runPhase = null;
326
+ this.emitEvent(partial);
327
+ }
328
+ emitErrorIfOpen(partial) {
329
+ if (this.terminalState !== 'open')
330
+ return;
331
+ this.terminalState = 'error';
332
+ this.runPhase = null;
333
+ this.emitEvent(partial);
334
+ }
335
+ emitEvent(partial) {
336
+ this.emit('agentEvent', { ...partial, runId: this.runId, seq: this.seq++ });
337
+ }
338
+ resetRunState() {
339
+ this.seq = 0;
340
+ this.terminalState = 'open';
341
+ this.hasEmittedText = false;
342
+ this.runPhase = null;
343
+ }
344
+ async killProcess() {
345
+ const proc = this.process;
346
+ if (!proc) {
347
+ await this.disposeManagedConfig();
348
+ return;
349
+ }
350
+ this.process = null;
351
+ this.runPhase = null;
352
+ terminateWorkBuddyProcess(proc);
353
+ await new Promise((resolve) => {
354
+ let settled = false;
355
+ const finish = () => {
356
+ if (settled)
357
+ return;
358
+ settled = true;
359
+ resolve();
360
+ };
361
+ proc.once('close', finish);
362
+ setTimeout(() => {
363
+ terminateWorkBuddyProcess(proc);
364
+ finish();
365
+ }, 3_000).unref?.();
366
+ });
367
+ await this.disposeManagedConfig();
368
+ }
369
+ async prepareManagedLaunch() {
370
+ const compiled = this.managedAgentPolicy;
371
+ if (!compiled) {
372
+ this.managedProcessEnv = null;
373
+ return [
374
+ '--permission-mode',
375
+ 'bypassPermissions',
376
+ '--setting-sources',
377
+ 'user,project,local',
378
+ '--tools',
379
+ 'default',
380
+ ];
381
+ }
382
+ const daemonIpc = resolveManagedRoomIpcPaths();
383
+ const nativeConfig = compileWorkBuddyManagedPermissions(resolveManagedAgentAdapterEnforcement(compiled), compiled.adapterPolicy.workDir, {
384
+ internalUnixSockets: daemonIpc.transport === 'unix_socket' ? [daemonIpc.endpoint] : [],
385
+ });
386
+ const files = await createWorkBuddyManagedConfigFiles(nativeConfig);
387
+ this.managedConfigDir = files.directory;
388
+ const env = buildManagedAgentProcessEnv({
389
+ agentType: 'workbuddy',
390
+ providerEnv: this.extraEnv,
391
+ sessionEnv: this.extraEnv,
392
+ runtimeEnv: { NO_COLOR: '1' },
393
+ });
394
+ // These values are generated from the verified policy rather than inherited
395
+ // from the daemon. They contain no credentials and cannot enable bypass mode.
396
+ Object.assign(env, nativeConfig.runtimeEnv);
397
+ delete env.CODEBUDDY_IS_SANDBOX;
398
+ this.managedProcessEnv = env;
399
+ return [
400
+ '--permission-mode',
401
+ 'dontAsk',
402
+ '--subagent-permission-mode',
403
+ 'dontAsk',
404
+ '--settings',
405
+ files.settingsPath,
406
+ '--setting-sources',
407
+ 'none',
408
+ '--strict-mcp-config',
409
+ '--mcp-config',
410
+ '{}',
411
+ '--tools',
412
+ nativeConfig.toolNames.join(','),
413
+ ];
414
+ }
415
+ buildProcessEnv() {
416
+ return this.managedProcessEnv ?? buildAgentProcessEnv(this.extraEnv);
417
+ }
418
+ async disposeManagedConfig() {
419
+ const directory = this.managedConfigDir;
420
+ this.managedConfigDir = null;
421
+ this.managedProcessEnv = null;
422
+ if (!directory)
423
+ return;
424
+ await disposeWorkBuddyManagedConfigFiles(directory).catch(() => { });
425
+ }
426
+ }
427
+ function stringifyToolResult(value) {
428
+ if (typeof value === 'string')
429
+ return value;
430
+ try {
431
+ return JSON.stringify(value);
432
+ }
433
+ catch {
434
+ return '[unserializable WorkBuddy tool result]';
435
+ }
436
+ }
437
+ export function sanitizeWorkBuddyError(value) {
438
+ return value
439
+ .replace(/(Authorization\s*:\s*)Bearer\s+[A-Za-z0-9._~+/-]+/gi, '$1Bearer [redacted]')
440
+ .replace(/(api[-_ ]?key|access[-_ ]?token|refresh[-_ ]?token|connector[-_ ]?token)(\s*[:=]\s*)[^\s,;]+/gi, '$1$2[redacted]')
441
+ .trim()
442
+ .slice(0, 2_000);
443
+ }
444
+ registerAgent('workbuddy', () => new WorkBuddyAdapter());
@@ -1,3 +1,3 @@
1
- import y from"node:crypto";import U from"node:fs";import b from"node:path";import{PublicAttachmentCache as j}from"../room/public-attachment-cache.js";import{PublicRoomClient as z,PublicRoomClientError as A}from"../room/public-room-client.js";import{callDaemonIpc as O}from"../daemon-ipc/client.js";import{DaemonIpcError as S}from"../daemon-ipc/protocol.js";import{MANAGED_ROOM_IPC_ROOT_ENV as k,resolveManagedRoomIpcPaths as D}from"../daemon-ipc/paths.js";import{MANAGED_ROOM_CONTEXT_ENV as T}from"../room/managed-context.js";import{createRoomInviteQr as $}from"../room/invite-qr.js";const C="https://shennian.net/install.md",N={write:e=>{process.stdout.write(e)},fail:()=>{process.exitCode=1}};function te(e,t={}){const n=t.client??new z,a=t.cache??new j({client:n}),s=t.io??N,p=t.createInviteQr??$,d=e.command("room").description("Use one explicitly referenced Shennian Room");d.command("create").description("Create a Room as the paired user").requiredOption("--name <name>","Room name").action(async m=>f("room.create",s,async()=>{R("create");const o=m.name.trim();(!o||o.length>80)&&i("invalid_room_name","Room name must contain 1 to 80 characters.");const r=await n.create(o),c=h(r.room,"invalid_room_response");typeof c.inviteUrl!="string"&&i("invalid_room_response","Shennian returned an invalid Room invitation.");const u=await p(c.inviteUrl).catch(()=>({imagePath:null,mimeType:"image/png",payload:c.inviteUrl}));return{...r,sharing:{inviteUrl:c.inviteUrl,inviteQr:u,installGuideUrl:C}}})),d.command("join <invite-url>").description("Join or request access through an official Room invitation").option("--message <message>","Introduction and reason required when owner approval is enabled").action(async(m,o)=>f("room.join",s,async()=>{R("join");const r=I(m,"invite"),c=o.message?.trim();return c&&c.length>500&&i("join_message_too_long","Join request message must be at most 500 characters."),n.join(r.roomId,r.inviteToken,c)})),d.command("open <room-url>").description("Open one Room where the paired user is already a member").action(async m=>f("room.open",s,async()=>{R("open");const o=I(m,"stable");return n.open(o.roomId)})),d.command("status [room-ref]").description("Show the managed Room, or one explicitly referenced Room").action(async m=>f("room.status",s,()=>{const o=g();return o?(m&&i("managed_room_ref_forbidden","Do not pass a Room reference in managed Room context."),w(o,"status",{})):n.status(x(m))})),d.command("read [room-ref]").description("Read recent, older, or newer messages from one Room").option("--before <sequence>","Read messages before this sequence").option("--after <sequence>","Read messages after this sequence").option("--limit <count>","Maximum messages, 1 to 50","50").action(async(m,o)=>{await f("room.read",s,async()=>{o.before&&o.after&&i("ambiguous_sequence_cursor","--before and --after are mutually exclusive."),o.before&&!q(o.before)&&i("invalid_sequence","--before sequence is invalid."),o.after&&!q(o.after)&&i("invalid_sequence","--after sequence is invalid.");const r=Number(o.limit);(!Number.isInteger(r)||r<1||r>50)&&i("invalid_limit","--limit must be between 1 and 50.");const c=g();if(c)return m&&i("managed_room_ref_forbidden","Do not pass a Room reference in managed Room context."),w(c,"read",{...o.before?{before:o.before}:{},...o.after?{after:o.after}:{},limit:r});const u=x(m),_=await n.read(u,{before:o.before,after:o.after,limit:r});return G(_,u,a)})}),d.command("send [room-ref]").description("Send text and up to 10 files as the paired user").option("--text <text>","Message text").option("--file <path>","Local file path; repeat for multiple files",P,[]).action(async(m,o)=>{await f("room.send",s,async()=>{const r=o.text?.trim()??"";!r&&o.file.length===0&&i("room_message_empty","Provide --text or at least one --file."),o.file.length>10&&i("room_attachments_too_many","A message can contain at most 10 files.");const c=g();if(c)return m&&i("managed_room_ref_forbidden","Do not pass a Room reference in managed Room context."),w(c,"send",{clientMessageId:`managed:${y.randomUUID()}`,text:r,files:o.file});const u=x(m),_=[];for(const L of o.file)_.push(await E(n,u,L));return n.send(u,{clientMessageId:`cli:${y.randomUUID()}`,...r?{text:r}:{},..._.length>0?{attachmentIds:_}:{}})})}),d.command("attachment").description("Download Room attachments through Shennian").command("get [room-ref] [attachment-id]").description("Download and verify one Room attachment").action(async(m,o)=>{await f("room.attachment.get",s,async()=>{const r=g(),c=r?m:o;return(!c||!/^ratt_[A-Za-z0-9_-]{1,191}$/.test(c))&&i("invalid_attachment_ref","attachment-id is invalid."),r?w(r,"attachment.get",{attachmentId:c}):a.materialize(x(m),c)})}),d.command("leave <room-ref>").description("Leave one explicitly referenced Room").action(async m=>f("room.leave",s,()=>(R("leave"),n.leave(M(m))))),d.command("list",{hidden:!0}).action(()=>v(s,"command_removed","Room listing is not available to Agents.",null)),d.command("search",{hidden:!0}).allowUnknownOption(!0).action(()=>v(s,"command_removed","Room search is not available to Agents.",null)),d.command("agent",{hidden:!0}).allowUnknownOption(!0).action(()=>v(s,"command_removed","Managed Agents are created in the Shennian mobile App.",null))}async function f(e,t,n){try{const a=await n();t.write(`${JSON.stringify({ok:!0,command:e,result:a})}
2
- `)}catch(a){if(a instanceof A){v(t,a.code,a.message,a.nextAction);return}if(a instanceof S){v(t,a.code,a.message,null);return}v(t,"room_command_failed","Shennian Room command failed.",null)}}function g(){return process.env[T]?.trim()||null}async function w(e,t,n){const a=process.env[k]?.trim();if(!a||!b.isAbsolute(a))throw new S("managed_room_context_unavailable","Managed Room IPC context is unavailable.");return O({method:"managed-room.execute",params:{context:e,action:t,...n},paths:D(a),timeoutMs:t==="send"||t==="attachment.get"?18e4:15e3})}function R(e){g()&&i("managed_room_action_forbidden",`${e} is not available in managed Room context.`)}function x(e){return e||i("room_ref_required","Use an official Room URL returned by Shennian."),M(e)}function M(e){return I(e,"stable").roomId}function I(e,t){let n;const a=t==="invite"?"invalid_invite_url":"invalid_room_ref",s=t==="invite"?"Use an official Shennian Room invitation URL.":"Use an official Room URL returned by Shennian.";try{n=new URL(e)}catch{i(a,s)}const p=n.pathname.match(/^\/spaces\/([A-Za-z0-9_-]{1,191})$/),d=n.searchParams.get("invite");return(n.protocol!=="https:"||n.hostname!=="app.shennian.net"||n.port||n.username||n.password||n.hash||!p||n.searchParams.has("access")||[...n.searchParams.keys()].some(l=>l!=="invite")||t==="stable"&&d!=null||t==="invite"&&(d==null||!/^[A-Za-z0-9_-]{32,256}$/.test(d)))&&i(a,s),{roomId:p[1],...d?{inviteToken:d}:{}}}function q(e){return/^(0|[1-9]\d{0,19})$/.test(e)}function P(e,t){return[...t,e]}async function E(e,t,n){const a=b.resolve(n);let s;try{s=await U.promises.lstat(a)}catch{i("file_not_found",`File does not exist: ${n}`)}(!s.isFile()||s.isSymbolicLink())&&i("invalid_file",`Not a regular file: ${n}`),(s.size<1||s.size>100*1024*1024)&&i("invalid_file_size","Each file must be between 1 byte and 100 MB.");const p=await U.promises.readFile(a),d=y.createHash("sha256").update(p).digest("hex"),l=F(a),m=await e.initializeAttachment(t,{clientUploadId:`cli:${y.randomUUID()}`,name:b.basename(a),mediaKind:l.startsWith("image/")?"image":l.startsWith("audio/")?"audio":"file",mimeType:l,byteSize:s.size,sha256:d}),o=h(m.attachment,"invalid_attachment_response"),r=h(m.upload,"invalid_attachment_response");(typeof o.id!="string"||typeof r.url!="string"||r.method!=="PUT")&&i("invalid_attachment_response","Shennian returned an invalid attachment response.");const c=h(r.headers,"invalid_attachment_response");return Object.values(c).some(u=>typeof u!="string")&&i("invalid_attachment_response","Shennian returned invalid upload headers."),J(r.url),await e.upload(r.url,c,p),await e.completeAttachment(t,o.id),o.id}function F(e){const t=b.extname(e).toLowerCase();return{".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".webp":"image/webp",".gif":"image/gif",".heic":"image/heic",".mp3":"audio/mpeg",".wav":"audio/wav",".m4a":"audio/mp4",".mp4":"video/mp4",".mov":"video/quicktime",".pdf":"application/pdf",".txt":"text/plain",".md":"text/markdown",".json":"application/json",".csv":"text/csv",".zip":"application/zip",".docx":"application/vnd.openxmlformats-officedocument.wordprocessingml.document",".xlsx":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}[t]??"application/octet-stream"}function G(e,t,n){return Array.isArray(e.messages)||i("invalid_server_response","Shennian returned invalid messages."),{messages:e.messages.map(a=>{const s=h(a,"invalid_server_response"),p=Array.isArray(s.attachments)?s.attachments:[];return{...s,attachments:p.map(d=>{const l=h(d,"invalid_server_response");return(typeof l.attachmentId!="string"||typeof l.name!="string"||typeof l.mimeType!="string"||typeof l.byteSize!="string"||typeof l.sha256!="string")&&i("invalid_server_response","Shennian returned invalid attachment metadata."),{...l,...n.lookup(t,l)}})}})}}function h(e,t){return(!e||typeof e!="object"||Array.isArray(e))&&i(t,"Invalid structured response."),e}function J(e){let t;try{t=new URL(e)}catch{i("invalid_attachment_response","Invalid upload URL.")}const n=t.protocol==="http:"&&(t.hostname==="localhost"||t.hostname==="127.0.0.1");(t.protocol!=="https:"&&!n||t.username||t.password||t.hash)&&i("invalid_attachment_response","Invalid upload URL.")}function i(e,t){throw new A(e,t)}function v(e,t,n,a){e.write(`${JSON.stringify({ok:!1,error:t,message:n,nextAction:a})}
1
+ import b from"node:crypto";import U from"node:fs";import R from"node:path";import{PublicAttachmentCache as O}from"../room/public-attachment-cache.js";import{PublicRoomClient as j,PublicRoomClientError as A}from"../room/public-room-client.js";import{callDaemonIpc as z}from"../daemon-ipc/client.js";import{DaemonIpcError as S}from"../daemon-ipc/protocol.js";import{MANAGED_ROOM_IPC_ROOT_ENV as k,resolveManagedRoomIpcPaths as D}from"../daemon-ipc/paths.js";import{MANAGED_ROOM_CONTEXT_ENV as P}from"../room/managed-context.js";import{createRoomInviteQr as T}from"../room/invite-qr.js";const $="https://shennian.net/install.md",C={write:e=>{process.stdout.write(e)},fail:()=>{process.exitCode=1}};function te(e,t={}){const o=t.client??new j,a=t.cache??new O({client:o}),r=t.io??C,p=t.createInviteQr??T,c=e.command("room").description("Use one explicitly referenced Shennian Room");c.command("create").description("Create a Room as the paired user").requiredOption("--name <name>","Room name").action(async m=>f("room.create",r,async()=>{w("create");const n=m.name.trim();(!n||n.length>80)&&i("invalid_room_name","Room name must contain 1 to 80 characters.");const s=await o.create(n),d=v(s.room,"invalid_room_response");typeof d.inviteUrl!="string"&&i("invalid_room_response","Shennian returned an invalid Room invitation.");const u=await p(d.inviteUrl).catch(()=>({imagePath:null,mimeType:"image/png",payload:d.inviteUrl}));return{...s,sharing:{inviteUrl:d.inviteUrl,inviteQr:u,installGuideUrl:$}}})),c.command("join <invite-url>").description("Join or request access through an official Room invitation").option("--message <message>","Introduction and reason required when owner approval is enabled").action(async(m,n)=>f("room.join",r,async()=>{w("join");const s=I(m,"invite"),d=n.message?.trim();return d&&d.length>500&&i("join_message_too_long","Join request message must be at most 500 characters."),o.join(s.roomId,s.inviteToken,d)})),c.command("open <room-url>").description("Open one Room where the paired user is already a member").action(async m=>f("room.open",r,async()=>{w("open");const n=I(m,"stable");return o.open(n.roomId)})),c.command("status [room-ref]").description("Show the managed Room, or one explicitly referenced Room").action(async m=>f("room.status",r,()=>{const n=h();return n?(m&&i("managed_room_ref_forbidden","Do not pass a Room reference in managed Room context."),y(n,"status",{})):o.status(x(m))})),c.command("read [room-ref]").description("Read recent, older, or newer messages from one Room").option("--before <sequence>","Read messages before this sequence").option("--after <sequence>","Read messages after this sequence").option("--limit <count>","Maximum messages, 1 to 50","50").action(async(m,n)=>{await f("room.read",r,async()=>{n.before&&n.after&&i("ambiguous_sequence_cursor","--before and --after are mutually exclusive."),n.before&&!M(n.before)&&i("invalid_sequence","--before sequence is invalid."),n.after&&!M(n.after)&&i("invalid_sequence","--after sequence is invalid.");const s=Number(n.limit);(!Number.isInteger(s)||s<1||s>50)&&i("invalid_limit","--limit must be between 1 and 50.");const d=h();if(d)return m&&i("managed_room_ref_forbidden","Do not pass a Room reference in managed Room context."),y(d,"read",{...n.before?{before:n.before}:{},...n.after?{after:n.after}:{},limit:s});const u=x(m),g=await o.read(u,{before:n.before,after:n.after,limit:s});return G(g,u,a)})}),c.command("send [room-ref]").description("Send text and up to 10 files as the paired user").option("--text <text>","Message text").option("--file <path>","Local file path; repeat for multiple files",N,[]).action(async(m,n)=>{await f("room.send",r,async()=>{const s=n.text?.trim()??"";!s&&n.file.length===0&&i("room_message_empty","Provide --text or at least one --file."),n.file.length>10&&i("room_attachments_too_many","A message can contain at most 10 files.");const d=h();if(d)return m&&i("managed_room_ref_forbidden","Do not pass a Room reference in managed Room context."),y(d,"send",{clientMessageId:`managed:${b.randomUUID()}`,text:s,files:n.file});const u=x(m),g=[];for(const L of n.file)g.push(await E(o,u,L));return o.send(u,{clientMessageId:`cli:${b.randomUUID()}`,...s?{text:s}:{},...g.length>0?{attachmentIds:g}:{}})})}),c.command("pause").description("Pause the current managed Agent in its bound Room").requiredOption("--reason <reason>","Short reason shown to all Room members").action(async m=>f("room.pause",r,()=>{const n=h();return n||i("managed_room_context_required","Pause is only available in managed Room context."),y(n,"pause",{reason:m.reason})})),c.command("attachment").description("Download Room attachments through Shennian").command("get [room-ref] [attachment-id]").description("Download and verify one Room attachment").action(async(m,n)=>{await f("room.attachment.get",r,async()=>{const s=h(),d=s?m:n;return(!d||!/^ratt_[A-Za-z0-9_-]{1,191}$/.test(d))&&i("invalid_attachment_ref","attachment-id is invalid."),s?y(s,"attachment.get",{attachmentId:d}):a.materialize(x(m),d)})}),c.command("leave <room-ref>").description("Leave one explicitly referenced Room").action(async m=>f("room.leave",r,()=>(w("leave"),o.leave(q(m))))),c.command("list",{hidden:!0}).action(()=>_(r,"command_removed","Room listing is not available to Agents.",null)),c.command("search",{hidden:!0}).allowUnknownOption(!0).action(()=>_(r,"command_removed","Room search is not available to Agents.",null)),c.command("agent",{hidden:!0}).allowUnknownOption(!0).action(()=>_(r,"command_removed","Managed Agents are created in the Shennian mobile App.",null))}async function f(e,t,o){try{const a=await o();t.write(`${JSON.stringify({ok:!0,command:e,result:a})}
2
+ `)}catch(a){if(a instanceof A){_(t,a.code,a.message,a.nextAction);return}if(a instanceof S){_(t,a.code,a.message,null);return}_(t,"room_command_failed","Shennian Room command failed.",null)}}function h(){return process.env[P]?.trim()||null}async function y(e,t,o){const a=process.env[k]?.trim();if(!a||!R.isAbsolute(a))throw new S("managed_room_context_unavailable","Managed Room IPC context is unavailable.");return z({method:"managed-room.execute",params:{context:e,action:t,...o},paths:D(a),timeoutMs:t==="send"||t==="attachment.get"?18e4:15e3})}function w(e){h()&&i("managed_room_action_forbidden",`${e} is not available in managed Room context.`)}function x(e){return e||i("room_ref_required","Use an official Room URL returned by Shennian."),q(e)}function q(e){return I(e,"stable").roomId}function I(e,t){let o;const a=t==="invite"?"invalid_invite_url":"invalid_room_ref",r=t==="invite"?"Use an official Shennian Room invitation URL.":"Use an official Room URL returned by Shennian.";try{o=new URL(e)}catch{i(a,r)}const p=o.pathname.match(/^\/spaces\/([A-Za-z0-9_-]{1,191})$/),c=o.searchParams.get("invite");return(o.protocol!=="https:"||o.hostname!=="app.shennian.net"||o.port||o.username||o.password||o.hash||!p||o.searchParams.has("access")||[...o.searchParams.keys()].some(l=>l!=="invite")||t==="stable"&&c!=null||t==="invite"&&(c==null||!/^[A-Za-z0-9_-]{32,256}$/.test(c)))&&i(a,r),{roomId:p[1],...c?{inviteToken:c}:{}}}function M(e){return/^(0|[1-9]\d{0,19})$/.test(e)}function N(e,t){return[...t,e]}async function E(e,t,o){const a=R.resolve(o);let r;try{r=await U.promises.lstat(a)}catch{i("file_not_found",`File does not exist: ${o}`)}(!r.isFile()||r.isSymbolicLink())&&i("invalid_file",`Not a regular file: ${o}`),(r.size<1||r.size>100*1024*1024)&&i("invalid_file_size","Each file must be between 1 byte and 100 MB.");const p=await U.promises.readFile(a),c=b.createHash("sha256").update(p).digest("hex"),l=F(a),m=await e.initializeAttachment(t,{clientUploadId:`cli:${b.randomUUID()}`,name:R.basename(a),mediaKind:l.startsWith("image/")?"image":l.startsWith("audio/")?"audio":"file",mimeType:l,byteSize:r.size,sha256:c}),n=v(m.attachment,"invalid_attachment_response"),s=v(m.upload,"invalid_attachment_response");(typeof n.id!="string"||typeof s.url!="string"||s.method!=="PUT")&&i("invalid_attachment_response","Shennian returned an invalid attachment response.");const d=v(s.headers,"invalid_attachment_response");return Object.values(d).some(u=>typeof u!="string")&&i("invalid_attachment_response","Shennian returned invalid upload headers."),J(s.url),await e.upload(s.url,d,p),await e.completeAttachment(t,n.id),n.id}function F(e){const t=R.extname(e).toLowerCase();return{".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".webp":"image/webp",".gif":"image/gif",".heic":"image/heic",".mp3":"audio/mpeg",".wav":"audio/wav",".m4a":"audio/mp4",".mp4":"video/mp4",".mov":"video/quicktime",".pdf":"application/pdf",".txt":"text/plain",".md":"text/markdown",".json":"application/json",".csv":"text/csv",".zip":"application/zip",".docx":"application/vnd.openxmlformats-officedocument.wordprocessingml.document",".xlsx":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}[t]??"application/octet-stream"}function G(e,t,o){return Array.isArray(e.messages)||i("invalid_server_response","Shennian returned invalid messages."),{messages:e.messages.map(a=>{const r=v(a,"invalid_server_response"),p=Array.isArray(r.attachments)?r.attachments:[];return{...r,attachments:p.map(c=>{const l=v(c,"invalid_server_response");return(typeof l.attachmentId!="string"||typeof l.name!="string"||typeof l.mimeType!="string"||typeof l.byteSize!="string"||typeof l.sha256!="string")&&i("invalid_server_response","Shennian returned invalid attachment metadata."),{...l,...o.lookup(t,l)}})}})}}function v(e,t){return(!e||typeof e!="object"||Array.isArray(e))&&i(t,"Invalid structured response."),e}function J(e){let t;try{t=new URL(e)}catch{i("invalid_attachment_response","Invalid upload URL.")}const o=t.protocol==="http:"&&(t.hostname==="localhost"||t.hostname==="127.0.0.1");(t.protocol!=="https:"&&!o||t.username||t.password||t.hash)&&i("invalid_attachment_response","Invalid upload URL.")}function i(e,t){throw new A(e,t)}function _(e,t,o,a){e.write(`${JSON.stringify({ok:!1,error:t,message:o,nextAction:a})}
3
3
  `),e.fail()}export{te as registerRoomCommand};