shennian 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/publish-build-manifest.json +291 -341
- package/dist/src/agents/codex.js +1 -1
- package/dist/src/agents/command-spec.js +2 -2
- package/dist/src/agents/detect.js +1 -1
- package/dist/src/agents/model-registry/discovery.js +2 -2
- package/dist/src/agents/model-registry/parsers.d.ts +1 -0
- package/dist/src/agents/model-registry/parsers.js +4 -4
- package/dist/src/agents/model-registry/service.js +1 -1
- package/dist/src/agents/native-capabilities.d.ts +10 -0
- package/dist/src/agents/native-capabilities.js +1 -0
- package/dist/src/agents/platform-instructions.js +5 -5
- package/dist/src/agents/runtime-compatibility.d.ts +11 -0
- package/dist/src/agents/runtime-compatibility.js +1 -0
- package/dist/src/agents/workbuddy.d.ts +44 -0
- package/dist/src/agents/workbuddy.js +3 -0
- package/dist/src/commands/daemon.d.ts +1 -0
- package/dist/src/commands/daemon.js +9 -9
- package/dist/src/commands/integration.d.ts +2 -0
- package/dist/src/commands/integration.js +1 -0
- package/dist/src/commands/manager.js +1 -2
- package/dist/src/commands/pair-browser.d.ts +43 -0
- package/dist/src/commands/pair-browser.js +1 -0
- package/dist/src/commands/pair.d.ts +2 -0
- package/dist/src/commands/pair.js +11 -8
- package/dist/src/commands/room.d.ts +13 -0
- package/dist/src/commands/room.js +4 -0
- package/dist/src/commands/upgrade.d.ts +3 -0
- package/dist/src/commands/upgrade.js +1 -1
- package/dist/src/config/index.d.ts +3 -1
- package/dist/src/config/index.js +2 -1
- package/dist/src/daemon-ipc/client.d.ts +8 -0
- package/dist/src/daemon-ipc/client.js +3 -0
- package/dist/src/daemon-ipc/paths.d.ts +7 -0
- package/dist/src/daemon-ipc/paths.js +1 -0
- package/dist/src/daemon-ipc/protocol.d.ts +30 -0
- package/dist/src/daemon-ipc/protocol.js +1 -0
- package/dist/src/daemon-ipc/secret.d.ts +3 -0
- package/dist/src/daemon-ipc/secret.js +3 -0
- package/dist/src/daemon-ipc/server.d.ts +13 -0
- package/dist/src/daemon-ipc/server.js +3 -0
- package/dist/src/index.js +2 -2
- package/dist/src/integrations/claude-room-plugin-lifecycle.d.ts +47 -0
- package/dist/src/integrations/claude-room-plugin-lifecycle.js +1 -0
- package/dist/src/integrations/codex-room-plugin-lifecycle.d.ts +49 -0
- package/dist/src/integrations/codex-room-plugin-lifecycle.js +1 -0
- package/dist/src/integrations/room-plugin-bundles.d.ts +23 -0
- package/dist/src/integrations/room-plugin-bundles.js +1 -0
- package/dist/src/integrations/room-plugin-lifecycle-common.d.ts +17 -0
- package/dist/src/integrations/room-plugin-lifecycle-common.js +1 -0
- package/dist/src/integrations/room-plugin-shared.d.ts +24 -0
- package/dist/src/integrations/room-plugin-shared.js +2 -0
- package/dist/src/integrations/room-session-hook.d.ts +30 -0
- package/dist/src/integrations/room-session-hook.js +2 -0
- package/dist/src/integrations/workbuddy-plugin-bundle.d.ts +15 -0
- package/dist/src/integrations/workbuddy-plugin-bundle.js +1 -0
- package/dist/src/integrations/workbuddy-plugin-lifecycle.d.ts +52 -0
- package/dist/src/integrations/workbuddy-plugin-lifecycle.js +2 -0
- package/dist/src/integrations/workbuddy-session-hook.d.ts +22 -0
- package/dist/src/integrations/workbuddy-session-hook.js +1 -0
- package/dist/src/manager/prompt.d.ts +1 -1
- package/dist/src/manager/prompt.js +1 -1
- package/dist/src/manager/retired-external-channel-compat.d.ts +78 -0
- package/dist/src/manager/retired-external-channel-compat.js +1 -0
- package/dist/src/manager/runtime.d.ts +1 -3
- package/dist/src/manager/runtime.js +7 -7
- package/dist/src/relay/client.d.ts +2 -0
- package/dist/src/relay/client.js +1 -1
- package/dist/src/room/activation-audit.d.ts +15 -0
- package/dist/src/room/activation-audit.js +2 -0
- package/dist/src/room/activation-policy.d.ts +8 -0
- package/dist/src/room/activation-policy.js +1 -0
- package/dist/src/room/activation-service.d.ts +25 -0
- package/dist/src/room/activation-service.js +2 -0
- package/dist/src/room/agent-tool.d.ts +62 -0
- package/dist/src/room/agent-tool.js +1 -0
- package/dist/src/room/attachment-cache.d.ts +40 -0
- package/dist/src/room/attachment-cache.js +2 -0
- package/dist/src/room/attachment-upload.d.ts +20 -0
- package/dist/src/room/attachment-upload.js +1 -0
- package/dist/src/room/authorized-action-coordinator.d.ts +40 -0
- package/dist/src/room/authorized-action-coordinator.js +1 -0
- package/dist/src/room/authorized-action-ipc.d.ts +11 -0
- package/dist/src/room/authorized-action-ipc.js +1 -0
- package/dist/src/room/device-authorization-client.d.ts +49 -0
- package/dist/src/room/device-authorization-client.js +1 -0
- package/dist/src/room/host-session-observation-ipc.d.ts +9 -0
- package/dist/src/room/host-session-observation-ipc.js +1 -0
- package/dist/src/room/host-session-observations.d.ts +54 -0
- package/dist/src/room/host-session-observations.js +2 -0
- package/dist/src/room/invite-bootstrap.d.ts +35 -0
- package/dist/src/room/invite-bootstrap.js +1 -0
- package/dist/src/room/invocation-token-ipc.d.ts +9 -0
- package/dist/src/room/invocation-token-ipc.js +1 -0
- package/dist/src/room/invocation-token.d.ts +53 -0
- package/dist/src/room/invocation-token.js +1 -0
- package/dist/src/room/jsonl-mirror.d.ts +73 -0
- package/dist/src/room/jsonl-mirror.js +6 -0
- package/dist/src/room/managed-room-binding.d.ts +42 -0
- package/dist/src/room/managed-room-binding.js +1 -0
- package/dist/src/room/managed-room-session.d.ts +143 -0
- package/dist/src/room/managed-room-session.js +3 -0
- package/dist/src/room/mirror-sync-service.d.ts +33 -0
- package/dist/src/room/mirror-sync-service.js +1 -0
- package/dist/src/room/operator-ipc.d.ts +11 -0
- package/dist/src/room/operator-ipc.js +1 -0
- package/dist/src/room/pending-authorized-actions.d.ts +69 -0
- package/dist/src/room/pending-authorized-actions.js +3 -0
- package/dist/src/room/room-action-client.d.ts +210 -0
- package/dist/src/room/room-action-client.js +1 -0
- package/dist/src/room/room-action-ipc.d.ts +33 -0
- package/dist/src/room/room-action-ipc.js +1 -0
- package/dist/src/room/room-action-schema.d.ts +3 -0
- package/dist/src/room/room-action-schema.js +1 -0
- package/dist/src/room/room-authorized-action-executor.d.ts +23 -0
- package/dist/src/room/room-authorized-action-executor.js +1 -0
- package/dist/src/room/room-binding-client.d.ts +51 -0
- package/dist/src/room/room-binding-client.js +1 -0
- package/dist/src/room/room-binding-ipc.d.ts +29 -0
- package/dist/src/room/room-binding-ipc.js +1 -0
- package/dist/src/room/session-registry-ipc.d.ts +14 -0
- package/dist/src/room/session-registry-ipc.js +1 -0
- package/dist/src/room/session-registry.d.ts +128 -0
- package/dist/src/room/session-registry.js +2 -0
- package/dist/src/room/tool-schema.d.ts +118 -0
- package/dist/src/room/tool-schema.js +1 -0
- package/dist/src/room/tool-server.d.ts +5 -0
- package/dist/src/room/tool-server.js +3 -0
- package/dist/src/session/handlers/agent-capabilities.d.ts +3 -0
- package/dist/src/session/handlers/agent-capabilities.js +1 -0
- package/dist/src/session/handlers/agent-config.js +1 -1
- package/dist/src/session/handlers/chat.js +2 -2
- package/dist/src/session/handlers/control.js +1 -1
- package/dist/src/session/handlers/fs.d.ts +1 -0
- package/dist/src/session/handlers/fs.js +1 -1
- package/dist/src/session/handlers/room-managed.d.ts +4 -0
- package/dist/src/session/handlers/room-managed.js +1 -0
- package/dist/src/session/manager.d.ts +18 -6
- package/dist/src/session/manager.js +1 -1
- package/dist/src/session/types.d.ts +5 -0
- package/dist/src/upgrade/engine.d.ts +14 -1
- package/dist/src/upgrade/engine.js +2 -2
- package/dist/src/upgrade/scheduler.d.ts +45 -0
- package/dist/src/upgrade/scheduler.js +1 -0
- package/node_modules/@shennian/wire/dist/agent-capabilities.d.ts +17 -0
- package/node_modules/@shennian/wire/dist/agent-capabilities.d.ts.map +1 -0
- package/node_modules/@shennian/wire/dist/agent-capabilities.js +2 -0
- package/node_modules/@shennian/wire/dist/agent-capabilities.js.map +1 -0
- package/node_modules/@shennian/wire/dist/agent-workspace.d.ts +41 -0
- package/node_modules/@shennian/wire/dist/agent-workspace.d.ts.map +1 -0
- package/node_modules/@shennian/wire/dist/agent-workspace.js +3 -0
- package/node_modules/@shennian/wire/dist/agent-workspace.js.map +1 -0
- package/node_modules/@shennian/wire/dist/index.d.ts +3 -1
- package/node_modules/@shennian/wire/dist/index.d.ts.map +1 -1
- package/node_modules/@shennian/wire/dist/index.js +3 -1
- package/node_modules/@shennian/wire/dist/index.js.map +1 -1
- package/node_modules/@shennian/wire/dist/machine.d.ts +1 -0
- package/node_modules/@shennian/wire/dist/machine.d.ts.map +1 -1
- package/node_modules/@shennian/wire/dist/methods.d.ts +1 -1
- package/node_modules/@shennian/wire/dist/methods.d.ts.map +1 -1
- package/node_modules/@shennian/wire/dist/room.d.ts +112 -0
- package/node_modules/@shennian/wire/dist/room.d.ts.map +1 -0
- package/node_modules/@shennian/wire/dist/room.js +4 -0
- package/node_modules/@shennian/wire/dist/room.js.map +1 -0
- package/node_modules/@shennian/wire/dist/session.d.ts +15 -2
- package/node_modules/@shennian/wire/dist/session.d.ts.map +1 -1
- package/node_modules/@shennian/wire/dist/session.js +1 -1
- package/node_modules/@shennian/wire/dist/session.js.map +1 -1
- package/package.json +3 -5
- package/dist/scripts/wechat-rpa-confirmation.mjs +0 -5
- package/dist/src/agents/pi-context.d.ts +0 -42
- package/dist/src/agents/pi-context.js +0 -12
- package/dist/src/agents/pi.d.ts +0 -70
- package/dist/src/agents/pi.js +0 -17
- package/dist/src/channels/base.d.ts +0 -144
- package/dist/src/channels/base.js +0 -1
- package/dist/src/channels/registry.d.ts +0 -8
- package/dist/src/channels/registry.js +0 -1
- package/dist/src/channels/reply-split.d.ts +0 -1
- package/dist/src/channels/reply-split.js +0 -10
- package/dist/src/channels/runtime.d.ts +0 -138
- package/dist/src/channels/runtime.js +0 -5
- package/dist/src/channels/secret-registry.d.ts +0 -35
- package/dist/src/channels/secret-registry.js +0 -1
- package/dist/src/channels/websocket.d.ts +0 -55
- package/dist/src/channels/websocket.js +0 -8
- package/dist/src/channels/wechat-channel/anchor.d.ts +0 -10
- package/dist/src/channels/wechat-channel/anchor.js +0 -1
- package/dist/src/channels/wechat-channel/automation-lane.d.ts +0 -35
- package/dist/src/channels/wechat-channel/automation-lane.js +0 -2
- package/dist/src/channels/wechat-channel/client.d.ts +0 -283
- package/dist/src/channels/wechat-channel/client.js +0 -1
- package/dist/src/channels/wechat-channel/cooldown.d.ts +0 -21
- package/dist/src/channels/wechat-channel/cooldown.js +0 -1
- package/dist/src/channels/wechat-channel/core/fixture-compare.d.ts +0 -52
- package/dist/src/channels/wechat-channel/core/fixture-compare.js +0 -1
- package/dist/src/channels/wechat-channel/core/media-action-plan.d.ts +0 -3
- package/dist/src/channels/wechat-channel/core/media-action-plan.js +0 -1
- package/dist/src/channels/wechat-channel/core/schema.d.ts +0 -67
- package/dist/src/channels/wechat-channel/core/schema.js +0 -1
- package/dist/src/channels/wechat-channel/core/trace-comparator.d.ts +0 -28
- package/dist/src/channels/wechat-channel/core/trace-comparator.js +0 -1
- package/dist/src/channels/wechat-channel/fingerprint.d.ts +0 -31
- package/dist/src/channels/wechat-channel/fingerprint.js +0 -1
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +0 -103
- package/dist/src/channels/wechat-channel/helper-assets.js +0 -1
- package/dist/src/channels/wechat-channel/helper-client.d.ts +0 -87
- package/dist/src/channels/wechat-channel/helper-client.js +0 -3
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +0 -122
- package/dist/src/channels/wechat-channel/helper-protocol.js +0 -1
- package/dist/src/channels/wechat-channel/human-coordination.d.ts +0 -25
- package/dist/src/channels/wechat-channel/human-coordination.js +0 -1
- package/dist/src/channels/wechat-channel/index.d.ts +0 -21
- package/dist/src/channels/wechat-channel/index.js +0 -1
- package/dist/src/channels/wechat-channel/ledger.d.ts +0 -48
- package/dist/src/channels/wechat-channel/ledger.js +0 -2
- package/dist/src/channels/wechat-channel/media-cache-resolver.d.ts +0 -42
- package/dist/src/channels/wechat-channel/media-cache-resolver.js +0 -1
- package/dist/src/channels/wechat-channel/media-resolver.d.ts +0 -186
- package/dist/src/channels/wechat-channel/media-resolver.js +0 -1
- package/dist/src/channels/wechat-channel/message-key.d.ts +0 -19
- package/dist/src/channels/wechat-channel/message-key.js +0 -1
- package/dist/src/channels/wechat-channel/observer.d.ts +0 -171
- package/dist/src/channels/wechat-channel/observer.js +0 -7
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +0 -96
- package/dist/src/channels/wechat-channel/outbound-ledger.js +0 -2
- package/dist/src/channels/wechat-channel/outbound-sender.d.ts +0 -74
- package/dist/src/channels/wechat-channel/outbound-sender.js +0 -1
- package/dist/src/channels/wechat-channel/pacing.d.ts +0 -3
- package/dist/src/channels/wechat-channel/pacing.js +0 -1
- package/dist/src/channels/wechat-channel/preflight.d.ts +0 -46
- package/dist/src/channels/wechat-channel/preflight.js +0 -1
- package/dist/src/channels/wechat-channel/runner.d.ts +0 -43
- package/dist/src/channels/wechat-channel/runner.js +0 -1
- package/dist/src/channels/wechat-channel/runtime.d.ts +0 -50
- package/dist/src/channels/wechat-channel/runtime.js +0 -1
- package/dist/src/channels/wechat-channel/scheduler.d.ts +0 -74
- package/dist/src/channels/wechat-channel/scheduler.js +0 -1
- package/dist/src/channels/wechat-channel/vector-store.d.ts +0 -44
- package/dist/src/channels/wechat-channel/vector-store.js +0 -1
- package/dist/src/channels/wechat-rpa/macos-flow.d.ts +0 -55
- package/dist/src/channels/wechat-rpa/macos-flow.js +0 -1
- package/dist/src/channels/wechat-rpa/macos.d.ts +0 -11
- package/dist/src/channels/wechat-rpa/macos.js +0 -21
- package/dist/src/channels/wechat-rpa/normalizer.d.ts +0 -49
- package/dist/src/channels/wechat-rpa/normalizer.js +0 -7
- package/dist/src/channels/wechat-rpa/product-channel.d.ts +0 -76
- package/dist/src/channels/wechat-rpa/product-channel.js +0 -16
- package/dist/src/channels/wechat-rpa-session-sync.d.ts +0 -26
- package/dist/src/channels/wechat-rpa-session-sync.js +0 -1
- package/dist/src/channels/wechat-rpa.d.ts +0 -104
- package/dist/src/channels/wechat-rpa.js +0 -6
- package/dist/src/channels/wecom.d.ts +0 -51
- package/dist/src/channels/wecom.js +0 -4
- package/dist/src/commands/external-attachments.d.ts +0 -9
- package/dist/src/commands/external-attachments.js +0 -1
- package/dist/src/commands/external.d.ts +0 -2
- package/dist/src/commands/external.js +0 -1
- package/dist/src/commands/helper-runtime-official-download.d.ts +0 -80
- package/dist/src/commands/helper-runtime-official-download.js +0 -1
- package/dist/src/commands/runtime.d.ts +0 -120
- package/dist/src/commands/runtime.js +0 -1
- package/dist/src/commands/wechat/command.d.ts +0 -3
- package/dist/src/commands/wechat/command.js +0 -1
- package/dist/src/commands/wechat/direct.d.ts +0 -14
- package/dist/src/commands/wechat/direct.js +0 -2
- package/dist/src/commands/wechat/doctor.d.ts +0 -2
- package/dist/src/commands/wechat/doctor.js +0 -1
- package/dist/src/commands/wechat/ipc.d.ts +0 -9
- package/dist/src/commands/wechat/ipc.js +0 -1
- package/dist/src/commands/wechat/operations.d.ts +0 -3
- package/dist/src/commands/wechat/operations.js +0 -4
- package/dist/src/commands/wechat/output.d.ts +0 -7
- package/dist/src/commands/wechat/output.js +0 -4
- package/dist/src/commands/wechat/types.d.ts +0 -142
- package/dist/src/commands/wechat/types.js +0 -1
- package/dist/src/commands/wechat/utils.d.ts +0 -16
- package/dist/src/commands/wechat/utils.js +0 -1
- package/dist/src/commands/wechat.d.ts +0 -6
- package/dist/src/commands/wechat.js +0 -1
- package/dist/src/devtools/wechat-channel-action-smoke.d.ts +0 -105
- package/dist/src/devtools/wechat-channel-action-smoke.js +0 -10
- package/dist/src/devtools/wechat-channel-download-report.d.ts +0 -111
- package/dist/src/devtools/wechat-channel-download-report.js +0 -2
- package/dist/src/devtools/wechat-channel-markdown-report.d.ts +0 -6
- package/dist/src/devtools/wechat-channel-markdown-report.js +0 -5
- package/dist/src/devtools/wechat-channel-product-steps.d.ts +0 -9
- package/dist/src/devtools/wechat-channel-product-steps.js +0 -40
- package/dist/src/session/handlers/skills.d.ts +0 -7
- package/dist/src/session/handlers/skills.js +0 -1
- package/dist/src/session/handlers/wechat-helper-runtime.d.ts +0 -33
- package/dist/src/session/handlers/wechat-helper-runtime.js +0 -1
- package/dist/src/skills/registry.d.ts +0 -16
- package/dist/src/skills/registry.js +0 -15
- package/dist/src/skills/setup.d.ts +0 -11
- package/dist/src/skills/setup.js +0 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type BrowserPairEvent = {
|
|
2
|
+
type: 'pair.browser.created';
|
|
3
|
+
intentId: string;
|
|
4
|
+
url: string;
|
|
5
|
+
expiresAt: string;
|
|
6
|
+
} | {
|
|
7
|
+
type: 'pair.waiting';
|
|
8
|
+
} | {
|
|
9
|
+
type: 'pair.claimed';
|
|
10
|
+
};
|
|
11
|
+
export declare class BrowserPairError extends Error {
|
|
12
|
+
readonly code: string;
|
|
13
|
+
constructor(code: string, message: string);
|
|
14
|
+
}
|
|
15
|
+
export declare function runBrowserPairFlow(input: {
|
|
16
|
+
serverUrl: string;
|
|
17
|
+
machineName: string;
|
|
18
|
+
machineOs: string;
|
|
19
|
+
agentList: string[];
|
|
20
|
+
emit?: (event: BrowserPairEvent) => void;
|
|
21
|
+
openUrl?: (url: string) => Promise<boolean>;
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
pollIntervalMs?: number;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
machineId: string;
|
|
26
|
+
machineToken: string;
|
|
27
|
+
machineName: string;
|
|
28
|
+
publicLoginUrl: string;
|
|
29
|
+
}>;
|
|
30
|
+
export declare function resolvePairMode(input: {
|
|
31
|
+
browser?: boolean;
|
|
32
|
+
token?: boolean;
|
|
33
|
+
json?: boolean;
|
|
34
|
+
stdinIsTty?: boolean;
|
|
35
|
+
stdoutIsTty?: boolean;
|
|
36
|
+
}): 'browser' | 'token' | 'prompt';
|
|
37
|
+
export declare function parsePairModeChoice(value: string): 'browser' | 'token';
|
|
38
|
+
export declare function openExternalUrl(url: string): Promise<boolean>;
|
|
39
|
+
export declare function browserOpenCommand(platform: NodeJS.Platform, url: string): {
|
|
40
|
+
command: string;
|
|
41
|
+
args: string[];
|
|
42
|
+
};
|
|
43
|
+
export declare function normalizePairAuthorizationUrl(value: string, serverUrl: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import I from"node:crypto";import{spawn as u}from"node:child_process";import{HttpDeviceAuthorizationClient as f}from"../room/device-authorization-client.js";import{hashActionPayload as A,hashPendingDeviceSecret as g}from"../room/pending-authorized-actions.js";const p=3e3;class n extends Error{code;constructor(r,t){super(t),this.code=r,this.name="BrowserPairError"}}async function E(e){const r=new f(e.serverUrl),t=I.randomBytes(32).toString("base64url"),a={action:"pair_machine",agentList:[...e.agentList].sort(),machineName:e.machineName,machineOs:e.machineOs},o=A(a),i=await r.create({deviceSecretHash:g(t),machineName:e.machineName,machineOs:e.machineOs,agentList:e.agentList,actionType:"machine.pair",actionSummary:"Pair this machine with your Shennian account",actionPayloadHash:o,returnTo:"/spaces"}),d=_(i.publicLoginUrl,e.serverUrl);e.emit?.({type:"pair.browser.created",intentId:i.loginIntentId,url:d,expiresAt:i.expiresAt}),await(e.openUrl??P)(d).catch(()=>!1);const l=new Date(i.expiresAt).getTime(),h=e.pollIntervalMs??p;let m=!1;for(;Date.now()<l;){if(e.signal?.aborted)throw await r.cancel({loginIntentId:i.loginIntentId,deviceSecret:t}).catch(()=>{}),new n("PAIR_CANCELLED","Pairing was cancelled");let s;try{s=await r.getStatus({loginIntentId:i.loginIntentId,deviceSecret:t})}catch{await w(h,e.signal);continue}if(s.actionPayloadHash!==o)throw new n("PAIR_PAYLOAD_MISMATCH","Authorization request changed unexpectedly");if(s.status==="denied")throw new n("PAIR_DENIED","Pairing request was denied");if(s.status==="expired")throw new n("PAIR_EXPIRED","Pairing request expired");if(s.status==="claimed")throw new n("PAIR_ALREADY_CLAIMED","Pairing credentials were already claimed");if(s.status==="approved"){const c=await r.claim({loginIntentId:i.loginIntentId,deviceSecret:t});if(c.actionPayloadHash!==o||!c.machineId||!c.machineToken)throw new n("PAIR_INVALID_CLAIM","Authorization did not return machine credentials");return e.emit?.({type:"pair.claimed"}),{machineId:c.machineId,machineToken:c.machineToken,machineName:e.machineName,publicLoginUrl:d}}m||(m=!0,e.emit?.({type:"pair.waiting"})),await w(h,e.signal)}throw new n("PAIR_TIMEOUT","Timed out waiting for browser authorization")}function N(e){if(e.browser&&e.token)throw new n("PAIR_MODE_CONFLICT","Choose either --browser or --token, not both");return e.browser?"browser":e.token||e.json||!e.stdinIsTty||!e.stdoutIsTty?"token":"prompt"}function O(e){return e.trim()==="2"?"token":"browser"}async function P(e){if(process.env.SSH_CONNECTION||process.env.SSH_TTY)return!1;const{command:r,args:t}=L(process.platform,e);return new Promise(a=>{const o=u(r,t,{detached:!0,stdio:"ignore",windowsHide:!0});o.once("error",()=>a(!1)),o.once("spawn",()=>{o.unref(),a(!0)})})}function L(e,r){return e==="darwin"?{command:"open",args:[r]}:e==="win32"?{command:"explorer.exe",args:[r]}:{command:"xdg-open",args:[r]}}function _(e,r){let t,a;try{t=new URL(e),a=new URL(r)}catch{throw new n("PAIR_INVALID_AUTHORIZATION_URL","Server returned an invalid authorization URL")}if(!["http:","https:"].includes(t.protocol)||t.hostname!==a.hostname||t.username||t.password||t.search||t.hash||!/^\/authorize\/[A-Za-z0-9_-]{8,128}$/.test(t.pathname))throw new n("PAIR_INVALID_AUTHORIZATION_URL","Server returned an unsafe authorization URL");return t.toString()}function w(e,r){return new Promise(t=>{if(r?.aborted)return t();const a=setTimeout(t,e);r?.addEventListener("abort",()=>{clearTimeout(a),t()},{once:!0})})}export{n as BrowserPairError,L as browserOpenCommand,_ as normalizePairAuthorizationUrl,P as openExternalUrl,O as parsePairModeChoice,N as resolvePairMode,E as runBrowserPairFlow};
|
|
@@ -4,6 +4,8 @@ export declare function runPairFlow(opts: {
|
|
|
4
4
|
machineName: string;
|
|
5
5
|
force?: boolean;
|
|
6
6
|
json?: boolean;
|
|
7
|
+
mode?: 'browser' | 'token';
|
|
8
|
+
openUrl?: (url: string) => Promise<boolean>;
|
|
7
9
|
}): Promise<void>;
|
|
8
10
|
export declare function registerDesktopMachine(opts: {
|
|
9
11
|
serverUrl: string;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
`)}function
|
|
3
|
-
\u2717 Pairing code expired. Please re-run: shennian pair`)),process.exit(1))),!
|
|
1
|
+
import r from"chalk";import k from"node:os";import N from"node:readline";import{loadConfig as p,saveConfig as v}from"../config/index.js";import{detectAgents as E}from"../agents/detect.js";import{clearRemoteAccessDisabled as A,getDaemonStatus as D,startDaemonProcess as b,installService as O,saveEnvSnapshot as x}from"./daemon.js";import{waitForDaemonReadyStatus as L}from"./daemon-start-wait.js";import{detectAndChooseServer as P}from"../region.js";import{buildPairQrPayload as B,PAIR_QR_RENDER_OPTIONS as M}from"./pair-qr.js";import{BrowserPairError as F,parsePairModeChoice as J,resolvePairMode as C,runBrowserPairFlow as Q}from"./pair-browser.js";const q=3e3,U=600*1e3;function l(o){process.stdout.write(`${JSON.stringify(o)}
|
|
2
|
+
`)}function g(o,e){l({type:"error",code:o,message:e}),process.exit(1)}function z(o){const e="\u2500".repeat(o.length+4);console.log(r.cyan(`\u250C${e}\u2510`)),console.log(r.cyan("\u2502 ")+r.bold.yellow(o)+r.cyan(" \u2502")),console.log(r.cyan(`\u2514${e}\u2518`))}async function K(o){const e=(await import("qrcode-terminal")).default;return new Promise(n=>{e.generate(o,M,t=>{console.log(t),n()})})}async function W(o,e,n={}){const t=Date.now()+U;for(n.json&&l({type:"pair.waiting"});Date.now()<t;){await new Promise(d=>setTimeout(d,q));let i;try{i=await fetch(`${o}/api/machines/pair/status?token=${encodeURIComponent(e)}`)}catch{continue}if(i.status===410&&(n.json?g("PAIR_EXPIRED","Pairing code expired"):(console.error(r.red(`
|
|
3
|
+
\u2717 Pairing code expired. Please re-run: shennian pair`)),process.exit(1))),!i.ok)continue;const a=await i.json();if(a.status==="claimed"&&a.machineToken&&a.machineId)return n.json&&l({type:"pair.claimed"}),{machineToken:a.machineToken,machineId:a.machineId,machineName:a.machineName??""}}return null}function Y(o){const e=N.createInterface({input:process.stdin,output:process.stdout});return new Promise(n=>{e.question(o,t=>{e.close(),n(t.trim().toLowerCase()==="y")})})}function j(){const o=N.createInterface({input:process.stdin,output:process.stdout});return console.log(r.white("Choose how to pair this machine:")),console.log(r.cyan(" 1. Browser authorization (recommended)")),console.log(r.gray(" 2. QR code / pairing token (servers and remote terminals)")),new Promise(e=>{o.question("Selection (1/2) [1]: ",n=>{o.close(),e(J(n))})})}async function $(o){const{serverUrl:e,machineName:n,force:t,json:i}=o,a=p();if(a.machineToken&&a.machineId&&!t){i?l({type:"done",status:"already-paired",machineId:a.machineId}):(console.log(r.green(`\u2713 Already paired (machine ID: ${a.machineId})`)),console.log(r.gray(' Run "shennian pair" to re-pair to another account')));return}const s=E().map(h=>h.type),m=k.platform(),f=m==="win32"?"windows":m==="darwin"?"macos":"linux";i||(console.log(r.bold(`
|
|
4
4
|
Shennian CLI \u2014 Pairing
|
|
5
|
-
`)),console.log(
|
|
6
|
-
|
|
7
|
-
\u2713 Paired successfully! Machine "${
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
`)),console.log(r.gray(` Machine: ${n} | OS: ${f} | Agents: ${s.join(", ")||"none"}`)),console.log());let S=o.mode??C({json:i,stdinIsTty:!!process.stdin.isTTY,stdoutIsTty:!!process.stdout.isTTY});if(S==="prompt"&&(S=await j()),S==="browser"){const h=new AbortController,I=()=>h.abort();process.once("SIGINT",I);try{const c=await Q({serverUrl:e,machineName:n,machineOs:f,agentList:s,signal:h.signal,openUrl:o.openUrl,emit:T=>{i?l(T):T.type==="pair.browser.created"&&(console.log(r.white("Approve this machine in your browser:")),console.log(r.cyan.underline(T.url)),console.log(r.gray(`
|
|
6
|
+
If the browser did not open, copy this URL to any signed-in device.`)),console.log(r.gray("Waiting for authorization... (Ctrl+C to cancel)")))}});v({...a,machineToken:c.machineToken,machineId:c.machineId,serverUrl:e}),i?l({type:"pair.saved",machineId:c.machineId}):console.log(r.green(`
|
|
7
|
+
\u2713 Paired successfully! Machine "${c.machineName}" is now linked.`));return}catch(c){const T=c instanceof F||typeof c=="object"&&c&&"code"in c&&typeof c.code=="string"?c.code:"PAIR_BROWSER_FAILED",R=c instanceof Error?c.message:"Browser pairing failed";i&&g(T,R),console.error(r.red(`
|
|
8
|
+
\u2717 ${R}`)),process.exit(1)}finally{process.removeListener("SIGINT",I)}}let u;try{u=await fetch(`${e}/api/machines/pair/create`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,os:f,agentList:s})})}catch{const h=`Network error: cannot connect to ${e}`;i&&g("NETWORK_ERROR",h),console.error(r.red(`\u2717 ${h}`)),process.exit(1)}if(!u.ok){const I=`Failed to create machine: ${(await u.json().catch(()=>({}))).error??u.statusText}`;i&&g("PAIR_CREATE_FAILED",I),console.error(r.red(`\u2717 ${I}`)),process.exit(1)}const y=await u.json(),_=B(y.pairToken);i?l({type:"pair.created",code:y.pairToken,machineId:y.machineId,expiresAt:new Date(Date.now()+U).toISOString()}):(console.log(r.white("Scan QR code or enter the pairing code in the Shennian app:")),console.log(),await K(_),z(y.pairToken),console.log(),console.log(r.gray("Waiting for pairing... (Ctrl+C to cancel)")));const w=await W(e,y.pairToken,{json:i});w||(i&&g("PAIR_TIMEOUT","Timed out waiting for pairing"),console.error(r.red(`
|
|
9
|
+
\u2717 Timed out. Pairing code expired, please try again.`)),process.exit(1)),v({...a,machineToken:w.machineToken,machineId:w.machineId,serverUrl:e}),i&&l({type:"pair.saved",machineId:w.machineId}),i||console.log(r.green(`
|
|
10
|
+
\u2713 Paired successfully! Machine "${w.machineName}" is now linked.`))}async function H(o){const e=p(),t=E().map(m=>m.type),i=k.platform(),a=i==="win32"?"windows":i==="darwin"?"macos":"linux",d=await fetch(`${o.serverUrl}/api/machines/desktop/register`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o.authToken}`},body:JSON.stringify({machineId:e.machineId,name:o.machineName,os:a,agentList:t})});if(!d.ok){const f=(await d.json().catch(()=>({}))).error??d.statusText;throw new Error(`Failed to register desktop machine: ${f}`)}const s=await d.json();if(!s.machine?.id||!s.machineToken)throw new Error("Failed to register desktop machine: missing machine token");return e.machineId=s.machine.id,e.machineToken=s.machineToken,e.serverUrl=o.serverUrl,v(e),{machineId:s.machine.id,machineName:s.machine.name}}async function G(o){const e=o?.trim();if(e)return e;const n=process.env.SHENNIAN_DESKTOP_SERVER_URL?.trim();if(n)return n;const t=p().serverUrl?.trim();return t||P()}async function se(o,e){const n=p();if(!n.machineToken||!n.machineId){const i=o??await P();await $({serverUrl:i,machineName:e,force:!1})}else console.log(r.green(`\u2713 Already paired (machine ID: ${n.machineId})`));x(),A(),console.log(r.gray(`
|
|
11
|
+
Starting background service...`)),O()||b()}function ce(o){o.command("desktop-register",{hidden:!0}).description("Register this desktop-managed machine to the authenticated account").option("--api <url>","Server URL").option("--name <name>","Machine name",k.hostname()).action(async e=>{try{const n=process.env.SHENNIAN_DESKTOP_AUTH_TOKEN;if(!n)throw new Error("Missing desktop auth token");const t=await G(e.api),i=await H({serverUrl:t,authToken:n,machineName:e.name});console.log(JSON.stringify({ok:!0,...i}))}catch(n){console.error(n instanceof Error?n.message:String(n)),process.exit(1)}}),o.command("pair").description("Pair this machine to a Shennian account (re-pair if already paired)").option("--api <url>","Server URL override").option("--server <url>","Server URL override").option("--name <name>","Machine name",k.hostname()).option("--json","Emit machine-readable JSONL events").option("--browser","Pair by approving a browser authorization request").option("--token","Pair with the existing QR code / token flow").option("--force","Force re-pair even when a machine token exists").action(async e=>{const n=p();let t;try{t=C({browser:e.browser,token:e.token,json:e.json,stdinIsTty:!!process.stdin.isTTY,stdoutIsTty:!!process.stdout.isTTY})}catch(s){const m=s instanceof Error?s.message:"Invalid pairing mode";e.json&&g("PAIR_MODE_CONFLICT",m),console.error(r.red(`\u2717 ${m}`)),process.exit(1)}if(t==="prompt"&&(t=await j()),n.machineToken&&n.machineId&&!e.force){if(e.json){l({type:"done",status:"already-paired",machineId:n.machineId});return}if(console.log(r.yellow(`Currently paired (machine ID: ${n.machineId})`)),!await Y("Re-pairing will unlink the current account. Continue? (y/N) ")){console.log(r.gray("Cancelled"));return}}const i=e.server??e.api??await P(),a=!!(e.force||n.machineToken&&n.machineId);await $({serverUrl:i,machineName:e.name,force:a,json:!!e.json,mode:t}),x(),A(),e.json?l({type:"daemon.starting"}):console.log(r.gray(`
|
|
12
|
+
Starting background service...`)),O()||b({quiet:!!e.json}),e.json&&(L(()=>D({cleanupStale:!0})),l({type:"done",status:"paired",machineId:p().machineId}))})}export{H as registerDesktopMachine,ce as registerPairCommand,$ as runPairFlow,se as runSmartStart};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { callDaemonIpc } from '../daemon-ipc/client.js';
|
|
3
|
+
type InvokeDaemon = typeof callDaemonIpc;
|
|
4
|
+
type RoomCommandIo = {
|
|
5
|
+
stdout: (text: string) => void;
|
|
6
|
+
stderr: (text: string) => void;
|
|
7
|
+
fail: () => void;
|
|
8
|
+
};
|
|
9
|
+
export declare function registerRoomCommand(program: Command, options?: {
|
|
10
|
+
invoke?: InvokeDaemon;
|
|
11
|
+
io?: RoomCommandIo;
|
|
12
|
+
}): void;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{callDaemonIpc as c}from"../daemon-ipc/client.js";import{DaemonIpcError as s}from"../daemon-ipc/protocol.js";const d={stdout:o=>process.stdout.write(o),stderr:o=>process.stderr.write(o),fail:()=>{process.exitCode=1}};function l(o,a={}){const t=a.invoke??c,e=a.io??d,n=o.command("room").description("Inspect locally observed Shennian Rooms and manage safe Room operations");n.command("status").description("Show local Room integration status").action(async()=>{await m("status",t,e)}),n.command("list").description("List Rooms observed by this daemon on this machine").action(async()=>{await m("list",t,e)}),n.command("create").description("Create a private Room owned by the signed-in Shennian account").requiredOption("--name <name>","Room name").action(async i=>{await p(i.name,t,e)})}async function m(o,a,t){try{const e=await a({method:"room-operator.inspect",params:{action:o}});t.stdout(`${JSON.stringify({ok:!0,command:`room.${o}`,result:e})}
|
|
2
|
+
`)}catch(e){r(e,t)}}async function p(o,a,t){const e=o.trim();if(!e||e.length>80){r(new s("invalid_params","Room name must contain 1 to 80 characters."),t);return}try{const n=await a({method:"room-operator.create",params:{name:e},timeoutMs:15e3});t.stdout(`${JSON.stringify({ok:!0,command:"room.create",result:n})}
|
|
3
|
+
`)}catch(n){r(n,t)}}function r(o,a){const t=o instanceof s?{code:o.code,message:o.message}:{code:"room_command_failed",message:"Shennian Room command failed."};a.stdout(`${JSON.stringify({ok:!1,error:t})}
|
|
4
|
+
`),a.fail()}export{l as registerRoomCommand};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execSync, spawn } from 'node:child_process';
|
|
2
2
|
import type { Command } from 'commander';
|
|
3
|
+
import { compareVersions } from '../upgrade/engine.js';
|
|
3
4
|
import type { CliRelayClient } from '../relay/client.js';
|
|
4
5
|
type RestartAfterUpgradeDeps = {
|
|
5
6
|
platform?: NodeJS.Platform;
|
|
@@ -9,10 +10,12 @@ type RestartAfterUpgradeDeps = {
|
|
|
9
10
|
execSync?: typeof execSync;
|
|
10
11
|
env?: NodeJS.ProcessEnv;
|
|
11
12
|
};
|
|
13
|
+
export declare function hasMajorUpgradeConfirmation(changeType: ReturnType<typeof compareVersions>, confirmed: boolean): boolean;
|
|
12
14
|
export declare function restartCurrentDaemonAfterUpgrade(deps?: RestartAfterUpgradeDeps): void;
|
|
13
15
|
export declare function registerUpgradeCommand(program: Command): void;
|
|
14
16
|
export declare function handleUpgradeStart(client: CliRelayClient, reqId: string, targetVersion?: string, opts?: {
|
|
15
17
|
currentVersion?: string;
|
|
18
|
+
confirmedMajor?: boolean;
|
|
16
19
|
}): Promise<void>;
|
|
17
20
|
export declare function handleUpgradeStatus(client: CliRelayClient, reqId: string, opts?: {
|
|
18
21
|
currentVersion?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import r from"chalk";import{execSync as k,spawn as w}from"node:child_process";import v from"node:os";import{getCurrentVersion as g,fetchLatestVersion as p,compareVersions as u,performUpgrade as
|
|
1
|
+
import r from"chalk";import{execSync as k,spawn as w}from"node:child_process";import v from"node:os";import{createInterface as $}from"node:readline/promises";import{getCurrentVersion as g,fetchLatestVersion as p,compareVersions as u,performUpgrade as f,checkForUpdate as S,recordUpgradeFailure as m}from"../upgrade/engine.js";const h=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function U(o,a){return o!=="major"||a}function y(o={}){const a=o.platform??v.platform(),c=o.kill??process.kill,s=o.pid??process.pid,l=o.env??process.env,t=o.spawn??w,n=o.execSync??k;if(a==="win32"){const d=l.ComSpec||"cmd.exe";try{t(d,["/d","/s","/c","timeout /t 2 /nobreak >nul & schtasks /Run /TN ShennianAgent >nul"],{detached:!0,stdio:"ignore",windowsHide:!0}).unref(),c(s,"SIGTERM");return}catch{try{n('schtasks /run /tn "ShennianAgent"',{stdio:"pipe",windowsHide:!0})}catch{}}}else try{t("sh",["-c","sleep 2; shennian start >/dev/null 2>&1"],{detached:!0,stdio:"ignore",env:l}).unref()}catch{}c(s,"SIGTERM")}function R(o){o.command("upgrade").description("Upgrade shennian CLI to the latest version").option("--check","Only check for updates, do not install").option("--to <version>","Upgrade to a specific version").option("--yes","Confirm a major version upgrade (required in non-interactive environments)").action(async a=>{const c=g();console.log(r.gray(`Current version: ${c}`)),console.log();let s=0;const l=setInterval(()=>{process.stdout.write(`\r ${h[s%h.length]} Checking for updates...`),s++},80);let t;try{t=a.to??await p(),clearInterval(l),process.stdout.write("\r"+" ".repeat(40)+"\r")}catch(e){clearInterval(l),process.stdout.write("\r"+" ".repeat(40)+"\r"),console.error(r.red(`\u2717 Failed to check for updates: ${e instanceof Error?e.message:String(e)}`)),process.exit(1)}const n=u(c,t);if(n==="none"&&!a.to){console.log(r.green(`\u2713 Already up to date (${c})`));return}if(console.log(` Available: ${r.green(t)} ${n==="major"?r.red("(major)"):n==="minor"?r.yellow("(minor)"):r.green("(patch)")}`),console.log(),a.check)return;if(n==="major"&&(console.log(r.yellow(" \u26A0 This is a major version update and may contain breaking changes.")),console.log(r.gray(" Please review the changelog before upgrading.")),console.log(),!a.yes)){(!process.stdin.isTTY||!process.stdout.isTTY)&&(console.error(r.red("\u2717 Major upgrades require explicit confirmation; rerun with --yes.")),process.exit(1));const e=$({input:process.stdin,output:process.stdout}),i=await e.question(` Type ${t} to confirm this major upgrade: `);if(e.close(),i.trim()!==t){console.log(r.gray(" Upgrade cancelled."));return}}const d=await f(t,e=>{switch(e.step){case"checking":console.log(r.gray(" \u2192 Checking npm..."));break;case"backing-up":console.log(r.gray(" \u2192 Backing up current version..."));break;case"installing":console.log(r.gray(` \u2192 Installing ${e.version}...`));break;case"verifying":console.log(r.gray(` \u2192 Verifying ${e.version}...`));break;case"restarting":console.log(r.gray(" \u2192 Restarting daemon..."));break}});d.ok?(console.log(),console.log(r.green(`\u2713 Upgraded ${d.from} \u2192 ${d.to}`)),console.log(r.gray(" Daemon is restarting...")),y()):(m(t,d.error),console.log(),d.rolledBack?console.error(r.red(`\u2717 Upgrade failed (rolled back to ${c})`)):console.error(r.red(`\u2717 Upgrade failed: ${d.error}`)),process.exit(1))})}async function C(o,a,c,s={}){const l=i=>{o.sendEvent({type:"event",event:"upgrade",payload:i})},t=s.currentVersion??g();let n;try{l({state:"checking",machineId:"self"}),n=c??await p()}catch(i){o.sendRes({type:"res",id:a,ok:!1,error:`Version check failed: ${i instanceof Error?i.message:String(i)}`});return}const d=u(t,n);if(d==="none"){o.sendRes({type:"res",id:a,ok:!0,payload:{alreadyLatest:!0,version:t}});return}if(!U(d,s.confirmedMajor===!0)){o.sendRes({type:"res",id:a,ok:!1,error:"Major upgrades require explicit second confirmation"});return}o.sendRes({type:"res",id:a,ok:!0,payload:{from:t,to:n}});const e=await f(n,i=>{switch(i.step){case"backing-up":l({state:"downloading",machineId:"self",version:n});break;case"installing":l({state:"installing",machineId:"self",version:n});break;case"restarting":l({state:"restarting",machineId:"self",from:t,to:n});break}},{currentVersion:t});if(e.ok)l({state:"restarting",machineId:"self",from:e.from,to:e.to}),await new Promise(i=>setTimeout(i,500)),y();else{const i=m(n,e.error);console.log(`[${new Date().toISOString()}] [upgrade] ${n} failed, retry after ${new Date(i.nextRetryAt).toISOString()}: ${e.error}`),l({state:e.rolledBack?"rolledback":"failed",machineId:"self",...e.rolledBack?{to:t,reason:e.error}:{error:e.error}})}}async function V(o,a,c={}){const s=await S(c.currentVersion);o.sendRes({type:"res",id:a,ok:!0,payload:s.hasUpdate?{hasUpdate:!0,current:s.current,latest:s.latest,changeType:s.changeType}:{hasUpdate:!1,current:c.currentVersion??g()}})}export{C as handleUpgradeStart,V as handleUpgradeStatus,U as hasMajorUpgradeConfirmation,R as registerUpgradeCommand,y as restartCurrentDaemonAfterUpgrade};
|
|
@@ -25,9 +25,11 @@ export type ShennianConfig = {
|
|
|
25
25
|
refreshToken?: string;
|
|
26
26
|
serverUrl?: string;
|
|
27
27
|
customAgents?: Record<string, CustomAgentEntry>;
|
|
28
|
-
/** Auto-upgrade policy
|
|
28
|
+
/** Auto-upgrade policy. Legacy "minor" values are normalized to "none" at runtime. */
|
|
29
29
|
autoUpgrade?: 'none' | 'patch' | 'minor';
|
|
30
30
|
};
|
|
31
|
+
export type RuntimeAutoUpgradePolicy = 'none' | 'patch';
|
|
32
|
+
export declare function normalizeAutoUpgradePolicy(value: unknown): RuntimeAutoUpgradePolicy;
|
|
31
33
|
export declare function loadConfig(): ShennianConfig;
|
|
32
34
|
export declare function saveConfig(config: ShennianConfig): void;
|
|
33
35
|
export declare function ensureDir(): void;
|
package/dist/src/config/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
import
|
|
1
|
+
import o from"node:fs";import i from"node:path";import m from"node:os";import p from"node:crypto";function c(){const n=process.env.SHENNIAN_HOME?.trim();return n?i.resolve(n):a()?i.join(m.tmpdir(),"shennian-vitest-runtime",String(process.pid)):i.join(m.homedir(),".shennian")}function s(...n){return i.join(c(),...n)}function f(){return s("config.json")}const g=f();function v(n){return n==="patch"?"patch":"none"}function x(){try{const n=f();if(!o.existsSync(n))return{};const e=JSON.parse(d(o.readFileSync(n,"utf-8")));return e.serverUrl?.includes("shennian.ai")&&(e.serverUrl="https://shennian.net"),e}catch{return{}}}function E(n){const e=c(),r=f(),u=`${JSON.stringify(n,null,2)}
|
|
2
|
+
`;if(o.mkdirSync(e,{recursive:!0,mode:448}),process.platform!=="win32"&&o.chmodSync(e,448),process.platform==="win32"){o.writeFileSync(r,u,{encoding:"utf8",mode:384});return}const t=`${r}.tmp-${process.pid}-${p.randomBytes(6).toString("hex")}`;try{o.writeFileSync(t,u,{encoding:"utf8",mode:384,flag:"wx"}),o.chmodSync(t,384),o.renameSync(t,r),o.chmodSync(r,384)}finally{try{o.unlinkSync(t)}catch{}}}function N(){o.mkdirSync(c(),{recursive:!0,mode:448}),o.mkdirSync(s("sessions"),{recursive:!0,mode:448}),process.platform!=="win32"&&(o.chmodSync(c(),448),o.chmodSync(s("sessions"),448))}function a(){return(process.env.VITEST==="true"||!!process.env.VITEST_WORKER_ID)&&!process.env.SHENNIAN_HOME?.trim()}function d(n){return n.charCodeAt(0)===65279?n.slice(1):n}export{g as configPath,N as ensureDir,f as getConfigPath,c as getShennianDir,x as loadConfig,v as normalizeAutoUpgradePolicy,s as resolveShennianPath,E as saveConfig};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { JsonValue } from '../room/pending-authorized-actions.js';
|
|
2
|
+
import { type DaemonIpcPaths } from './paths.js';
|
|
3
|
+
export declare function callDaemonIpc<T extends JsonValue = JsonValue>(input: {
|
|
4
|
+
method: string;
|
|
5
|
+
params?: JsonValue;
|
|
6
|
+
paths?: DaemonIpcPaths;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
}): Promise<T>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import w from"node:crypto";import I from"node:net";import{resolveDaemonIpcPaths as D}from"./paths.js";import{DAEMON_IPC_MAX_REQUEST_BYTES as f,DAEMON_IPC_VERSION as u,DaemonIpcError as o}from"./protocol.js";import{readDaemonIpcSecret as g}from"./secret.js";async function C(t){const m=t.paths??D(),h=g(m.secretFile),c=w.randomUUID(),d=`${JSON.stringify({version:u,requestId:c,auth:h,method:t.method,params:t.params})}
|
|
2
|
+
`;if(Buffer.byteLength(d,"utf8")>f)throw new o("request_too_large","Daemon IPC request is too large.");const n=await new Promise((l,r)=>{const e=I.createConnection(m.endpoint);let s="";const _=setTimeout(()=>{e.destroy(),r(new o("ipc_timeout","Shennian daemon did not respond."))},t.timeoutMs??5e3),a=i=>{clearTimeout(_),e.destroy(),i()};e.setEncoding("utf8"),e.once("connect",()=>e.write(d)),e.on("data",i=>{if(s+=i,Buffer.byteLength(s,"utf8")>f){a(()=>r(new o("response_too_large","Daemon IPC response is too large.")));return}const p=s.indexOf(`
|
|
3
|
+
`);p<0||a(()=>{try{l(JSON.parse(s.slice(0,p)))}catch{r(new o("invalid_response","Daemon IPC response is invalid."))}})}),e.once("error",()=>a(()=>r(new o("daemon_unavailable","Shennian daemon is not running."))))});if(n.requestId!==c||n.version!==u)throw new o("response_mismatch","Daemon IPC response did not match the request.");if(!n.ok)throw new o(n.error.code,n.error.message);return n.result}export{C as callDaemonIpc};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import s from"node:crypto";import n from"node:path";import{getShennianDir as p}from"../config/index.js";function d(o=p(),r=process.platform){const e=n.join(o,"runtime"),t=n.join(e,"daemon-ipc.secret");if(r==="win32"){const i=s.createHash("sha256").update(n.resolve(o).toLowerCase()).digest("hex").slice(0,20);return{runtimeDirectory:e,secretFile:t,endpoint:`\\\\.\\pipe\\shennian-daemon-${i}`,transport:"windows_named_pipe"}}return{runtimeDirectory:e,secretFile:t,endpoint:n.join(e,"daemon.sock"),transport:"unix_socket"}}export{d as resolveDaemonIpcPaths};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { JsonValue } from '../room/pending-authorized-actions.js';
|
|
2
|
+
export declare const DAEMON_IPC_VERSION: 1;
|
|
3
|
+
export declare const DAEMON_IPC_MAX_REQUEST_BYTES: number;
|
|
4
|
+
export type DaemonIpcRequest = {
|
|
5
|
+
version: typeof DAEMON_IPC_VERSION;
|
|
6
|
+
requestId: string;
|
|
7
|
+
auth: string;
|
|
8
|
+
method: string;
|
|
9
|
+
params?: JsonValue;
|
|
10
|
+
};
|
|
11
|
+
export type DaemonIpcSuccess = {
|
|
12
|
+
version: typeof DAEMON_IPC_VERSION;
|
|
13
|
+
requestId: string;
|
|
14
|
+
ok: true;
|
|
15
|
+
result: JsonValue;
|
|
16
|
+
};
|
|
17
|
+
export type DaemonIpcFailure = {
|
|
18
|
+
version: typeof DAEMON_IPC_VERSION;
|
|
19
|
+
requestId: string;
|
|
20
|
+
ok: false;
|
|
21
|
+
error: {
|
|
22
|
+
code: string;
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export type DaemonIpcResponse = DaemonIpcSuccess | DaemonIpcFailure;
|
|
27
|
+
export declare class DaemonIpcError extends Error {
|
|
28
|
+
readonly code: string;
|
|
29
|
+
constructor(code: string, message: string);
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const t=1,c=1048576;class s extends Error{code;constructor(r,o){super(o),this.code=r,this.name="DaemonIpcError"}}export{c as DAEMON_IPC_MAX_REQUEST_BYTES,t as DAEMON_IPC_VERSION,s as DaemonIpcError};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import a from"node:crypto";import o from"node:fs";import{DaemonIpcError as c}from"./protocol.js";function f(t,n){l(t);try{const e=o.lstatSync(n);if(!e.isFile()||e.isSymbolicLink())throw new c("unsafe_ipc_secret","Daemon IPC secret is not a regular file.");process.platform!=="win32"&&(e.mode&63)!==0&&o.chmodSync(n,384);const s=o.readFileSync(n,"utf8").trim();if(s.length<40||s.length>128)throw new c("invalid_ipc_secret","Daemon IPC secret is invalid.");return s}catch(e){if(!d(e))throw e}const i=a.randomBytes(32).toString("base64url");if(process.platform==="win32"){try{o.writeFileSync(n,`${i}
|
|
2
|
+
`,{encoding:"utf8",mode:384,flag:"wx"})}catch(e){if(!m(e))throw e}return f(t,n)}const r=`${n}.tmp-${process.pid}-${a.randomBytes(6).toString("hex")}`;try{o.writeFileSync(r,`${i}
|
|
3
|
+
`,{encoding:"utf8",mode:384,flag:"wx"}),o.chmodSync(r,384);try{o.linkSync(r,n)}catch(e){if(!m(e))throw e}}finally{try{o.unlinkSync(r)}catch{}}return f(t,n)}function h(t){const n=o.lstatSync(t);if(!n.isFile()||n.isSymbolicLink())throw new c("unsafe_ipc_secret","Daemon IPC secret is not a regular file.");const i=o.readFileSync(t,"utf8").trim();if(i.length<40||i.length>128)throw new c("invalid_ipc_secret","Daemon IPC secret is invalid.");return i}function l(t){o.mkdirSync(t,{recursive:!0,mode:448});const n=o.lstatSync(t);if(!n.isDirectory()||n.isSymbolicLink())throw new c("unsafe_runtime_directory","Daemon runtime path is not a directory.");process.platform!=="win32"&&o.chmodSync(t,448)}function d(t){return typeof t=="object"&&t!==null&&"code"in t&&t.code==="ENOENT"}function m(t){return typeof t=="object"&&t!==null&&"code"in t&&t.code==="EEXIST"}export{l as ensurePrivateRuntimeDirectory,f as loadOrCreateDaemonIpcSecret,h as readDaemonIpcSecret};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { JsonValue } from '../room/pending-authorized-actions.js';
|
|
2
|
+
import { type DaemonIpcPaths } from './paths.js';
|
|
3
|
+
export type DaemonIpcHandler = (params: JsonValue | undefined, context: {
|
|
4
|
+
requestId: string;
|
|
5
|
+
}) => Promise<JsonValue> | JsonValue;
|
|
6
|
+
export type DaemonIpcServer = {
|
|
7
|
+
paths: DaemonIpcPaths;
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
export declare function startDaemonIpcServer(options: {
|
|
11
|
+
handlers: Record<string, DaemonIpcHandler>;
|
|
12
|
+
paths?: DaemonIpcPaths;
|
|
13
|
+
}): Promise<DaemonIpcServer>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import _ from"node:crypto";import u from"node:fs";import h from"node:net";import{resolveDaemonIpcPaths as I}from"./paths.js";import{DAEMON_IPC_MAX_REQUEST_BYTES as S,DAEMON_IPC_VERSION as d,DaemonIpcError as l}from"./protocol.js";import{ensurePrivateRuntimeDirectory as v,loadOrCreateDaemonIpcSecret as w}from"./secret.js";async function b(n){const e=n.paths??I();v(e.runtimeDirectory);const i=w(e.runtimeDirectory,e.secretFile);e.transport==="unix_socket"&&await P(e.endpoint);const t=h.createServer(r=>{r.setEncoding("utf8");let s="",c=!1;r.on("data",f=>{if(c)return;if(s+=f,Buffer.byteLength(s,"utf8")>S){c=!0,m(r,a("unknown","request_too_large","Daemon IPC request is too large."));return}const p=s.indexOf(`
|
|
2
|
+
`);p<0||(c=!0,D(s.slice(0,p),i,n.handlers).then(y=>m(r,y)).catch(()=>m(r,a("unknown","internal_error","Daemon IPC request failed."))))}),r.on("error",()=>{})});await new Promise((r,s)=>{const c=f=>s(f);t.once("error",c),t.listen(e.endpoint,()=>{t.off("error",c),r()})});let o=null;if(e.transport==="unix_socket"){u.chmodSync(e.endpoint,384);const r=u.lstatSync(e.endpoint);o={dev:r.dev,ino:r.ino}}return{paths:e,close:async()=>{if(await new Promise(r=>t.close(()=>r())),e.transport==="unix_socket"&&o)try{const r=u.lstatSync(e.endpoint);r.isSocket()&&r.dev===o.dev&&r.ino===o.ino&&u.unlinkSync(e.endpoint)}catch{}}}}async function P(n){try{const e=u.lstatSync(n);if(!e.isSocket()||e.isSymbolicLink())throw new l("unsafe_ipc_endpoint","Refusing to replace a non-socket IPC endpoint.");if(await C(n))throw new l("daemon_ipc_in_use","Another Shennian daemon IPC server is already running.");u.unlinkSync(n)}catch(e){if(k(e))return;throw e}}function C(n){return new Promise(e=>{const i=h.createConnection(n),t=o=>{i.destroy(),e(o)};i.once("connect",()=>t(!0)),i.once("error",()=>t(!1)),i.setTimeout(300,()=>t(!1))})}async function D(n,e,i){let t;try{t=JSON.parse(n)}catch{return a("unknown","invalid_json","Daemon IPC request must be JSON.")}const o=typeof t.requestId=="string"?t.requestId.slice(0,128):"unknown";if(t.version!==d)return a(o,"unsupported_version","Unsupported daemon IPC version.");if(!g(t.auth,e))return a(o,"unauthorized","Daemon IPC authentication failed.");if(typeof t.method!="string"||!/^[a-z][a-z0-9_.-]{0,127}$/.test(t.method))return a(o,"invalid_method","Invalid daemon IPC method.");const r=i[t.method];if(!r)return a(o,"method_not_found","Daemon IPC method is not supported.");try{const s=await r(t.params,{requestId:o});return{version:d,requestId:o,ok:!0,result:s}}catch(s){return s instanceof l?a(o,s.code,s.message):a(o,"method_failed","Daemon IPC method failed.")}}function g(n,e){if(typeof n!="string")return!1;const i=Buffer.from(n),t=Buffer.from(e);return i.length===t.length&&_.timingSafeEqual(i,t)}function a(n,e,i){return{version:d,requestId:n,ok:!1,error:{code:e,message:i}}}function m(n,e){n.end(`${JSON.stringify(e)}
|
|
3
|
+
`)}function k(n){return typeof n=="object"&&n!==null&&"code"in n&&n.code==="ENOENT"}export{b as startDaemonIpcServer};
|
package/dist/src/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{Command as M}from"commander";import u from"chalk";import G from"node:os";import R from"node:fs";import{loadConfig as I,saveConfig as J,getConfigPath as j,getShennianDir as H,resolveShennianPath as N}from"./config/index.js";import{CliRelayClient as K}from"./relay/client.js";import{registerPairCommand as B,runSmartStart as W}from"./commands/pair.js";import{clearDaemonPidIfOwner as O,createDaemonInstanceId as Y,clearDaemonLauncher as T,findRunningDaemonProcessIds as q,isRemoteAccessDisabled as z,registerDaemonCommand as Q,writeDaemonPid as _,writeDaemonLauncher as P}from"./commands/daemon.js";import{registerAgentCommand as X}from"./commands/agent.js";import{registerManagerCommand as Z}from"./commands/manager.js";import{registerExternalCommand as ee}from"./commands/external.js";import{registerWeChatCommand as ne}from"./commands/wechat.js";import{registerRuntimeCommand as te}from"./commands/runtime.js";import{registerUpgradeCommand as oe}from"./commands/upgrade.js";import{registerToolsCommand as re}from"./commands/tools.js";import{SessionManager as se}from"./session/manager.js";import{SERVERS as k,regionToUrl as ae,urlToRegion as ie}from"./region.js";import{getCurrentVersion as L,handleStartupCrashCheck as ce,checkForUpdate as le,isUpgradeVersionInCooldown as me,recordUpgradeFailure as de}from"./upgrade/engine.js";import{detectAgents as pe}from"./agents/detect.js";import{augmentProcessPath as b}from"./env-path.js";b();const x=L(),ge=3e4,ue=5*6e4;import{getCachedAgentInfos as fe,resolveAgentInfos as he}from"./agents/model-registry.js";import{initCliLogReporter as ve,reportLog as v}from"./log-reporter.js";import{isNativeFusionEnabled as Se}from"./native-fusion/config.js";import{NativeSessionFusionService as we}from"./native-fusion/service.js";import{startDaemonLogRetention as Ie}from"./daemon-log.js";const ye=H(),De=N("daemon.pid");function $e(){try{const n=R.readFileSync(De,"utf-8").trim();if(n.startsWith("{")){const o=JSON.parse(n),r=Number(o.pid);return Number.isInteger(r)&&r>0?r:null}const t=parseInt(n,10);return Number.isInteger(t)&&t>0?t:null}catch{return null}}function Re(n){return n.replace(/^https:\/\//,"wss://").replace(/^http:\/\//,"ws://")}function ke(n){const t=[`trigger=${n.trigger??"unknown"}`,`phase=${n.phase}`,`code=${n.code??"unknown"}`,`attempt=${n.reconnectAttempt}`];return n.reason&&t.push(`reason=${n.reason}`),n.error&&t.push(`error=${n.error}`),t.join(" ")}async function F(n,t=5e3){const o=Date.now();for(;Date.now()-o<t;){try{process.kill(n,0)}catch{return!0}await new Promise(r=>setTimeout(r,100))}try{return process.kill(n,0),!1}catch{return!0}}const a=new M;a.name("shennian").description("Shennian \u2014 AI Agent Control Plane").version(x),a.option("--api <url>","Server URL override").option("--name <name>","Machine name",G.hostname()).action(async n=>{const t=I(),o=n.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??t.serverUrl??void 0;await W(o,n.name)}),a.command("run-service",{hidden:!0}).description("(internal) Connect to relay server, called by the background service").option("--api <url>","Server URL override").action(async n=>{const t=N("env.json");try{const e=JSON.parse(R.readFileSync(t,"utf-8"));for(const[g,m]of Object.entries(e))process.env[g]||(process.env[g]=m);b()}catch{}const o=Ie();z()&&(console.log(`[${new Date().toISOString()}] remote access disabled, service start skipped`),clearInterval(o),process.exit(0));const r=!!(process.env.INVOCATION_ID||process.env.JOURNAL_STREAM||process.env.SHENNIAN_DESKTOP_SERVER_URL),f=async(e,g)=>{console.log(`[${new Date().toISOString()}] ${g} (PID ${e})`),process.kill(e,"SIGTERM"),await F(e)||(process.kill(e,"SIGKILL"),await F(e,2e3))};try{const e=$e();if(e&&e!==process.pid)try{process.kill(e,0),r?await f(e,"managed start taking over from existing daemon"):(console.log(`[${new Date().toISOString()}] daemon already running (PID ${e}), skipping duplicate start`),process.exit(0))}catch{}}catch{}const d=q(process.pid);if(d.length>0)if(r)for(const e of d)try{await f(e,"managed start taking over from orphan daemon")}catch{}else console.log(`[${new Date().toISOString()}] daemon already running (PID ${d[0]}), skipping duplicate start`),process.exit(0);const i=Y();_(process.pid,i,{version:x}),P(process.pid,void 0,i),process.on("exit",()=>{O(process.pid,i),T(i)}),await ce()&&(console.log(`[${new Date().toISOString()}] Rolled back to previous version, restarting...`),process.exit(0));const s=I();s.machineToken||(console.error(u.red("\u2717 Not paired yet. Run: shennian")),process.exit(1));const c=n.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??s.serverUrl??k.cn.url,S=`${Re(c)}/relay/machine`,p=L(),D=pe(),h=D.map(e=>e.type),$=fe(D);s.machineId&&ve(c,s.machineId),console.log(`[${new Date().toISOString()}] Connecting to ${S}... (v${p}) agents: ${h.join(",")}`),v({level:"info",wsEvent:"daemon.start",metadata:{version:p,agents:h}});let w=null;const l=new K({serverUrl:S,machineToken:s.machineToken,cliVersion:p,agentList:h,onConnected:()=>{console.log(`[${new Date().toISOString()}] \u2713 Connected`),v({level:"info",wsEvent:"daemon.connected"}),$.some(e=>e.models.length>0)&&l.sendEvent({type:"event",event:"machine.agents",payload:{agentList:h,agents:$}}),he(D,{serverUrl:s.serverUrl??c,authToken:s.machineToken??s.accessToken}).then(e=>{JSON.stringify(e)!==JSON.stringify($)&&l.sendEvent({type:"event",event:"machine.agents",payload:{agentList:h,agents:e}})}).catch(()=>{}),import("./upgrade/engine.js").then(({readUpgradeAttempt:e,clearUpgradeAttempt:g})=>{const m=e();g(),m&&(console.log(`[${new Date().toISOString()}] [upgrade] Reporting success: ${m.from} \u2192 ${m.to}`),l.sendEvent({type:"event",event:"upgrade",payload:{state:"success",machineId:"self",from:m.from,to:m.to}}))}).catch(()=>{}),Ee(l,s.autoUpgrade??"none",p),w?.handleConnected()},onDisconnected:e=>{console.log(`[${new Date().toISOString()}] \u26A0 Disconnected, reconnecting... ${ke(e)}`),v({level:"warn",wsEvent:"daemon.disconnected",metadata:{code:e.code,reason:e.reason,error:e.error,phase:e.phase,trigger:e.trigger,reconnectAttempt:e.reconnectAttempt}})},onReq:e=>{console.log(`[${new Date().toISOString()}] [req] ${e.method}`),v({level:"info",type:"ws",wsEvent:e.method,wsDirection:"in",traceId:e.traceId}),C.handleReq(e)}});w=Se()?new we(l):null;const C=new se(l,w,p);R.mkdirSync(ye,{recursive:!0}),_(process.pid,i,{version:p}),P(process.pid,void 0,i),l.connect(),process.stdin.resume();let U=!1;const A=async()=>{if(!U){U=!0,console.log(u.gray(`
|
|
2
|
-
Disconnecting...`)),
|
|
1
|
+
import{Command as ae}from"commander";import I from"chalk";import U from"node:os";import b from"node:fs";import{loadConfig as a,saveConfig as J,getConfigPath as ce,getShennianDir as me,resolveShennianPath as W,normalizeAutoUpgradePolicy as le}from"./config/index.js";import{CliRelayClient as de}from"./relay/client.js";import{registerPairCommand as pe,runSmartStart as ge}from"./commands/pair.js";import{clearDaemonPidIfOwner as j,createDaemonInstanceId as he,clearDaemonLauncher as K,findRunningDaemonProcessIds as ue,isRemoteAccessDisabled as fe,isIsolatedDaemonE2eInstance as ve,registerDaemonCommand as Ie,writeDaemonPid as q,writeDaemonLauncher as Q}from"./commands/daemon.js";import{registerAgentCommand as Se}from"./commands/agent.js";import{registerManagerCommand as we}from"./commands/manager.js";import{registerUpgradeCommand as ye}from"./commands/upgrade.js";import{registerToolsCommand as Re}from"./commands/tools.js";import{registerIntegrationCommand as ke}from"./commands/integration.js";import{registerRoomCommand as Ce}from"./commands/room.js";import{SessionManager as Te}from"./session/manager.js";import{SERVERS as O,regionToUrl as Ae,urlToRegion as De}from"./region.js";import{getCurrentVersion as X,handleStartupCrashCheck as $e}from"./upgrade/engine.js";import{AutoUpgradeScheduler as Ee}from"./upgrade/scheduler.js";import{detectAgents as Ue}from"./agents/detect.js";import{augmentProcessPath as Y}from"./env-path.js";Y();const Z=X();import{getCachedAgentInfos as be,resolveAgentInfos as Oe}from"./agents/model-registry.js";import{initCliLogReporter as Ne,reportLog as S}from"./log-reporter.js";import{isNativeFusionEnabled as Pe}from"./native-fusion/config.js";import{NativeSessionFusionService as Me}from"./native-fusion/service.js";import{startDaemonLogRetention as Le}from"./daemon-log.js";import{startDaemonIpcServer as He}from"./daemon-ipc/server.js";import{createAuthorizedActionIpcHandlers as xe}from"./room/authorized-action-ipc.js";import{PendingAuthorizedActionStore as _e}from"./room/pending-authorized-actions.js";import{HttpDeviceAuthorizationClient as ze}from"./room/device-authorization-client.js";import{AuthorizedActionCoordinator as Ve}from"./room/authorized-action-coordinator.js";import{RoomSessionRegistry as Be}from"./room/session-registry.js";import{createRoomSessionRegistryIpcHandlers as Fe}from"./room/session-registry-ipc.js";import{RoomInvocationTokenAuthority as Ge}from"./room/invocation-token.js";import{createRoomInvocationTokenIpcHandlers as Je}from"./room/invocation-token-ipc.js";import{HttpRoomBindingClient as We}from"./room/room-binding-client.js";import{TrustedManagedRoomBinder as je}from"./room/managed-room-binding.js";import{createRoomBindingIpcHandlers as Ke}from"./room/room-binding-ipc.js";import{RoomHostSessionObservationStore as qe}from"./room/host-session-observations.js";import{createRoomHostSessionObservationIpcHandlers as Qe}from"./room/host-session-observation-ipc.js";import{runWorkBuddySessionHook as Xe}from"./integrations/workbuddy-session-hook.js";import{runRoomSessionHook as Ye}from"./integrations/room-session-hook.js";import{RoomJsonlMirror as ee}from"./room/jsonl-mirror.js";import{RoomMirrorSyncService as Ze}from"./room/mirror-sync-service.js";import{RoomActivationService as eo}from"./room/activation-service.js";import{RoomActivationAudit as oo}from"./room/activation-audit.js";import{RoomAttachmentCache as no}from"./room/attachment-cache.js";import{RoomAttachmentUploader as to}from"./room/attachment-upload.js";import{runRoomToolServer as ro}from"./room/tool-server.js";import{RoomAuthorizedActionExecutor as io}from"./room/room-authorized-action-executor.js";import{createRoomOperatorIpcHandlers as so}from"./room/operator-ipc.js";import{openExternalUrl as ao}from"./commands/pair-browser.js";const oe=me(),co=W("daemon.pid");function mo(){try{const o=b.readFileSync(co,"utf-8").trim();if(o.startsWith("{")){const r=JSON.parse(o),c=Number(r.pid);return Number.isInteger(c)&&c>0?c:null}const n=parseInt(o,10);return Number.isInteger(n)&&n>0?n:null}catch{return null}}function lo(o){return o.replace(/^https:\/\//,"wss://").replace(/^http:\/\//,"ws://")}function po(o){const n=[`trigger=${o.trigger??"unknown"}`,`phase=${o.phase}`,`code=${o.code??"unknown"}`,`attempt=${o.reconnectAttempt}`];return o.reason&&n.push(`reason=${o.reason}`),o.error&&n.push(`error=${o.error}`),n.join(" ")}async function ne(o,n=5e3){const r=Date.now();for(;Date.now()-r<n;){try{process.kill(o,0)}catch{return!0}await new Promise(c=>setTimeout(c,100))}try{return process.kill(o,0),!1}catch{return!0}}const t=new ae;t.name("shennian").description("Shennian \u2014 AI Agent Control Plane").version(Z),t.enablePositionalOptions(),ke(t),Ce(t),t.option("--api <url>","Server URL override").option("--name <name>","Machine name",U.hostname()).action(async o=>{const n=a(),r=o.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??n.serverUrl??void 0;await ge(r,o.name)}),t.command("run-service",{hidden:!0}).description("(internal) Connect to relay server, called by the background service").option("--api <url>","Server URL override").action(async o=>{const n=W("env.json");try{const e=JSON.parse(b.readFileSync(n,"utf-8"));for(const[s,m]of Object.entries(e))process.env[s]||(process.env[s]=m);Y()}catch{}const r=Le();fe()&&(console.log(`[${new Date().toISOString()}] remote access disabled, service start skipped`),clearInterval(r),process.exit(0));const c=!!(process.env.INVOCATION_ID||process.env.JOURNAL_STREAM||process.env.SHENNIAN_DESKTOP_SERVER_URL),N=async(e,s)=>{console.log(`[${new Date().toISOString()}] ${s} (PID ${e})`),process.kill(e,"SIGTERM"),await ne(e)||(process.kill(e,"SIGKILL"),await ne(e,2e3))};try{const e=mo();if(e&&e!==process.pid)try{process.kill(e,0),c?await N(e,"managed start taking over from existing daemon"):(console.log(`[${new Date().toISOString()}] daemon already running (PID ${e}), skipping duplicate start`),process.exit(0))}catch{}}catch{}const A=ve(process.env,oe)?[]:ue(process.pid);if(A.length>0)if(c)for(const e of A)try{await N(e,"managed start taking over from orphan daemon")}catch{}else console.log(`[${new Date().toISOString()}] daemon already running (PID ${A[0]}), skipping duplicate start`),process.exit(0);const l=he();q(process.pid,l,{version:Z}),Q(process.pid,void 0,l),process.on("exit",()=>{j(process.pid,l),K(l)}),await $e()&&(console.log(`[${new Date().toISOString()}] Rolled back to previous version, restarting...`),process.exit(0));let i=a();const h=o.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??i.serverUrl??O.cn.url,u=X(),D=Ue(),f=D.map(e=>e.type),$=be(D),P=new _e,d=new Be,v=new qe,M=new Ge(d),p=new We(h),re=new no({actionClient:p}),ie=new to({actionClient:p}),L=new ze(h);let E=null;const H=await He({handlers:{...xe({store:P,authorizationClient:L,machineName:U.hostname(),machineOs:`${process.platform} ${U.release()}`,agentList:f,openPublicLoginUrl:ao}),...Fe({registry:d,observations:v,sessionResolver:p,getMachineCredentials:()=>{const e=a();return{machineId:e.machineId,machineToken:e.machineToken}}}),...Qe({store:v,registry:d,getMachineId:()=>a().machineId,onRegistrationIdle:e=>{E?.drain(e).catch(s=>{console.error("[room.activation] idle drain failed",s)})}}),...Je({authority:M,getMachineCredentials:()=>{const e=a();return{machineId:e.machineId,machineToken:e.machineToken}}}),...Ke({authority:M,registry:d,bindingClient:p,getMachineCredentials:()=>{const e=a();return{machineId:e.machineId,machineToken:e.machineToken}},mirrorFactory:e=>new ee(e),attachmentCache:re,attachmentUploader:ie,observations:v}),...so({registry:d,actionClient:p,getMachineCredentials:()=>{const e=a();return{machineId:e.machineId,machineToken:e.machineToken}}})}}),se=new io({registry:d,observations:v,actionClient:p,getMachineCredentials:()=>{const e=a();return{machineId:e.machineId,machineToken:e.machineToken}}}),x=new Ve(P,L,se,({machineId:e,machineToken:s})=>{J({...a(),machineId:e,machineToken:s,serverUrl:h})});let w=!1;const y=()=>{w||(w=!0,H.close().finally(()=>process.exit(0)))};if(process.once("SIGINT",y),process.once("SIGTERM",y),!i.machineToken){for(console.log(`[${new Date().toISOString()}] Waiting for browser device authorization...`);!w&&!i.machineToken;)await x.runOnce(),i=a(),i.machineToken||await new Promise(e=>setTimeout(e,1e3));if(w)return}process.removeListener("SIGINT",y),process.removeListener("SIGTERM",y);const _=setInterval(()=>{x.runOnce().catch(e=>{console.error("[room.authorization] pending action retry failed",e)})},1e3);_.unref?.();const z=`${lo(h)}/relay/machine`,V=i.machineToken;if(!V)return;i.machineId&&Ne(h,i.machineId),console.log(`[${new Date().toISOString()}] Connecting to ${z}... (v${u}) agents: ${f.join(",")}`),S({level:"info",wsEvent:"daemon.start",metadata:{version:u,agents:f}});let R=null,k=null;const B=le(i.autoUpgrade),g=new de({serverUrl:z,machineToken:V,cliVersion:u,agentList:f,autoUpgradePolicy:B,onConnected:()=>{console.log(`[${new Date().toISOString()}] \u2713 Connected`),S({level:"info",wsEvent:"daemon.connected"}),$.some(e=>e.models.length>0)&&g.sendEvent({type:"event",event:"machine.agents",payload:{agentList:f,agents:$}}),Oe(D,{serverUrl:i.serverUrl??h,authToken:i.machineToken??i.accessToken}).then(e=>{JSON.stringify(e)!==JSON.stringify($)&&g.sendEvent({type:"event",event:"machine.agents",payload:{agentList:f,agents:e}})}).catch(()=>{}),import("./upgrade/engine.js").then(({readUpgradeAttempt:e,clearUpgradeAttempt:s})=>{const m=e();s(),m&&(console.log(`[${new Date().toISOString()}] [upgrade] Reporting success: ${m.from} \u2192 ${m.to}`),g.sendEvent({type:"event",event:"upgrade",payload:{state:"success",machineId:"self",from:m.from,to:m.to}}))}).catch(()=>{}),C.start(),R?.handleConnected(),k?.start()},onDisconnected:e=>{k?.stop(),console.log(`[${new Date().toISOString()}] \u26A0 Disconnected, reconnecting... ${po(e)}`),S({level:"warn",wsEvent:"daemon.disconnected",metadata:{code:e.code,reason:e.reason,error:e.error,phase:e.phase,trigger:e.trigger,reconnectAttempt:e.reconnectAttempt}})},onReq:e=>{console.log(`[${new Date().toISOString()}] [req] ${e.method}`),S({level:"info",type:"ws",wsEvent:e.method,wsDirection:"in",traceId:e.traceId}),T.handleReq(e)}});R=Pe()?new Me(g):null,k=new Ze({registry:d,actionClient:p,getMachineCredentials:()=>({machineToken:a().machineToken}),mirrorFactory:e=>new ee(e),onMessages:async(e,s,m)=>{await E?.processMessages(e,s,m)}});const C=new Ee({currentVersion:u,machineId:i.machineId??"unknown-machine",serverUrl:i.serverUrl??h,getIdleState:()=>T.getUpgradeIdleState(),startUpgrade:async e=>{const{handleUpgradeStart:s}=await import("./commands/upgrade.js");await s(g,"auto-upgrade",e,{currentVersion:u})}});C.setPolicy(B);const T=new Te(g,R,u,C,new je({registry:d,bindingClient:p,getMachineCredentials:()=>{const e=a();return{machineId:e.machineId,machineToken:e.machineToken}},hostProcessIdentity:()=>`daemon:${l}`}));E=new eo({registry:d,observations:v,activate:e=>T.activateRoomSession(e),audit:new oo}),b.mkdirSync(oe,{recursive:!0}),q(process.pid,l,{version:u}),Q(process.pid,void 0,l),g.connect(),process.stdin.resume();let F=!1;const G=async()=>{if(!F){F=!0,console.log(I.gray(`
|
|
2
|
+
Disconnecting...`)),S({level:"info",wsEvent:"daemon.stop"}),clearInterval(r),clearInterval(_);try{await T.cleanup(),C.stop(),R?.stop(),k?.stop(),g.disconnect(),await H.close(),j(process.pid,l),K(l)}finally{process.exit(0)}}};process.on("SIGINT",()=>{G()}),process.on("SIGTERM",()=>{G()})}),t.command("workbuddy-session-hook",{hidden:!0}).description("(internal) Receive a trusted WorkBuddy plugin lifecycle event").action(async()=>{await Xe()}),t.command("room-session-hook",{hidden:!0}).description("(internal) Authorize the official Room tool for one trusted Agent Session").argument("<runtime>","Trusted runtime adapter").action(async o=>{o!=="codex"&&o!=="claude_code"&&o!=="workbuddy"||await Ye(o)}),t.command("room-tool-server",{hidden:!0}).description("(internal) Run the official Shennian Room MCP tool").action(async()=>{await ro()});const te=t.command("config").description("View or modify configuration");te.command("show",{isDefault:!0}).description("Show current configuration").action(()=>{console.log(`Config file: ${ce()}`);const n={...a()};if(n.machineToken&&(n.machineToken=n.machineToken.slice(0,12)+"..."),n.serverUrl){const r=De(n.serverUrl);console.log(`Region: ${r} (${O[r].label})`)}console.log(JSON.stringify(n,null,2))}),te.command("set").description("Update a config value").argument("<key>",'Config key (e.g. "server")').argument("<value>",'Config value (e.g. "cn" or "global")').action((o,n)=>{const r=a();if(o==="server"){n!=="cn"&&n!=="global"&&(console.error(I.red('\u2717 Value must be "cn" or "global"')),process.exit(1));const c=n;r.serverUrl=Ae(c),J(r),console.log(I.green(`\u2713 Server set to ${O[c].label}`)),console.log(I.yellow(" Restart the background service for this to take effect: shennian stop && shennian start"));return}console.error(I.red(`\u2717 Unknown config key: ${o}. Supported: server`)),process.exit(1)}),pe(t),Ie(t),Se(t),we(t),ye(t),Re(t),t.parse();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ResolvedCommandSpec } from '../agents/command-spec.js';
|
|
2
|
+
import { type ClaudeRoomPluginBundle, type RoomPluginMarketplaceBundle } from './room-plugin-bundles.js';
|
|
3
|
+
import { type IntegrationCommandResult } from './room-plugin-lifecycle-common.js';
|
|
4
|
+
type Bundle = RoomPluginMarketplaceBundle<ClaudeRoomPluginBundle>;
|
|
5
|
+
type Action = 'install' | 'enable' | 'disable' | 'update' | 'uninstall';
|
|
6
|
+
export type ClaudeRoomPluginStatus = {
|
|
7
|
+
available: boolean;
|
|
8
|
+
state: 'unavailable' | 'not_installed' | 'enabled' | 'disabled' | 'conflict';
|
|
9
|
+
installedVersion: string | null;
|
|
10
|
+
availableVersion: string | null;
|
|
11
|
+
needsUpdate: boolean;
|
|
12
|
+
consentRequired: boolean;
|
|
13
|
+
restartRequired: boolean;
|
|
14
|
+
errorCode?: string;
|
|
15
|
+
};
|
|
16
|
+
export declare class ClaudeRoomPluginLifecycle {
|
|
17
|
+
private readonly receiptPath;
|
|
18
|
+
private readonly configRoot;
|
|
19
|
+
private readonly resolveCommand;
|
|
20
|
+
private readonly ensureBundle;
|
|
21
|
+
private readonly runCommand;
|
|
22
|
+
constructor(options?: {
|
|
23
|
+
receiptPath?: string;
|
|
24
|
+
configRoot?: string;
|
|
25
|
+
resolveCommand?: () => ResolvedCommandSpec | null;
|
|
26
|
+
ensureBundle?: () => Bundle;
|
|
27
|
+
runCommand?: (spec: ResolvedCommandSpec, args: string[]) => Promise<IntegrationCommandResult>;
|
|
28
|
+
});
|
|
29
|
+
status(options?: {
|
|
30
|
+
bundle?: Bundle;
|
|
31
|
+
restartRequired?: boolean;
|
|
32
|
+
}): Promise<ClaudeRoomPluginStatus>;
|
|
33
|
+
apply(action: Action): Promise<ClaudeRoomPluginStatus>;
|
|
34
|
+
private install;
|
|
35
|
+
private enable;
|
|
36
|
+
private disable;
|
|
37
|
+
private update;
|
|
38
|
+
private uninstall;
|
|
39
|
+
private ensureMarketplaceRegistered;
|
|
40
|
+
private listMarketplaces;
|
|
41
|
+
private listPlugins;
|
|
42
|
+
private run;
|
|
43
|
+
private readReceipt;
|
|
44
|
+
private writeReceipt;
|
|
45
|
+
private removeReceipt;
|
|
46
|
+
}
|
|
47
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import g from"node:os";import f from"node:path";import{resolveBuiltinCommand as R}from"../agents/command-spec.js";import{resolveShennianPath as b}from"../config/index.js";import{ensureClaudeRoomMarketplaceBundle as _}from"./room-plugin-bundles.js";import{ensurePrivateDirectory as v,writeJsonAtomic as y}from"./room-plugin-shared.js";import{hashDirectoryContent as m,hashPath as c,parseJson as w,readJsonFile as k,removePrivateFile as C,RoomPluginLifecycleError as n,runIntegrationCommand as V,sameRealPath as p}from"./room-plugin-lifecycle-common.js";const l="shennian",s=`shennian-room@${l}`;class j{receiptPath;configRoot;resolveCommand;ensureBundle;runCommand;constructor(a={}){this.receiptPath=a.receiptPath??b("integrations","claude-code","installation.json"),this.configRoot=a.configRoot??process.env.CLAUDE_CONFIG_DIR?.trim()??f.join(g.homedir(),".claude"),this.resolveCommand=a.resolveCommand??(()=>R("claude")),this.ensureBundle=a.ensureBundle??(()=>_()),this.runCommand=a.runCommand??((e,t)=>V(e,t))}async status(a={}){const e=this.resolveCommand();if(!e)return P();const t=a.bundle??this.ensureBundle(),i=this.readReceipt(t),r=(await this.listMarketplaces(e)).find(u=>u.name===l);if(r&&!p(r.path,t.marketplaceRoot))return A(t,"foreign_marketplace");const d=(await this.listPlugins(e)).find(u=>u.id===s);if(d){const u=d.version!==t.pluginVersion||i!=null&&(i.state==="installing"||i.bundleContentHash!==m(t.pluginRoot));return{available:!0,state:d.enabled?"enabled":"disabled",installedVersion:d.version,availableVersion:t.pluginVersion,needsUpdate:u,consentRequired:i==null,restartRequired:!!a.restartRequired}}return{available:!0,state:"not_installed",installedVersion:null,availableVersion:t.pluginVersion,needsUpdate:!1,consentRequired:i==null,restartRequired:!!a.restartRequired}}async apply(a){const e=this.resolveCommand();if(!e)throw new n("claude_unavailable");const t=this.ensureBundle(),i=await this.status({bundle:t});if(i.state==="conflict")throw new n(i.errorCode??"integration_conflict");if(a==="install")await this.install(e,t,i);else{if(!this.readReceipt(t))throw new n("user_consent_required");a==="enable"?await this.enable(e,t,i):a==="disable"?await this.disable(e,t,i):a==="update"?await this.update(e,t,i):await this.uninstall(e,t,i)}return this.status({bundle:t,restartRequired:!0})}async install(a,e,t){this.writeReceipt(e,"installing"),await this.ensureMarketplaceRegistered(a,e),t.state==="not_installed"?await this.run(a,["plugin","install",s,"--scope","user","--yes"]):t.needsUpdate&&await this.run(a,["plugin","update",s,"--scope","user","--yes"]),t.state==="disabled"&&await this.run(a,["plugin","enable",s,"--scope","user"]),this.writeReceipt(e,"installed")}async enable(a,e,t){if(t.state==="not_installed")throw new n("plugin_not_installed");t.state==="disabled"&&await this.run(a,["plugin","enable",s,"--scope","user"]),this.writeReceipt(e,"installed")}async disable(a,e,t){if(t.state==="not_installed")throw new n("plugin_not_installed");t.state==="enabled"&&await this.run(a,["plugin","disable",s,"--scope","user"]),this.writeReceipt(e,"disabled")}async update(a,e,t){if(t.state==="not_installed")throw new n("plugin_not_installed");await this.run(a,["plugin","marketplace","update",l]),t.installedVersion===t.availableVersion?(await this.run(a,["plugin","uninstall",s,"--scope","user","--yes"]),await this.run(a,["plugin","install",s,"--scope","user","--yes"]),t.state==="disabled"&&await this.run(a,["plugin","disable",s,"--scope","user"])):await this.run(a,["plugin","update",s,"--scope","user","--yes"]),this.writeReceipt(e,t.state==="disabled"?"disabled":"installed")}async uninstall(a,e,t){t.state!=="not_installed"&&await this.run(a,["plugin","uninstall",s,"--scope","user","--yes"]);const i=(await this.listMarketplaces(a)).find(r=>r.name===l);i&&p(i.path,e.marketplaceRoot)&&await this.run(a,["plugin","marketplace","remove",l,"--scope","user"]),this.removeReceipt()}async ensureMarketplaceRegistered(a,e){const t=(await this.listMarketplaces(a)).find(i=>i.name===l);if(!t){await this.run(a,["plugin","marketplace","add",e.marketplaceRoot,"--scope","user"]);return}if(!p(t.path,e.marketplaceRoot))throw new n("foreign_marketplace")}async listMarketplaces(a){const e=await this.run(a,["plugin","marketplace","list","--json"]),t=w(e.stdout,"invalid_marketplace_list");if(!Array.isArray(t))throw new n("invalid_marketplace_list");return t.map(i=>{if(!h(i)||typeof i.name!="string")throw new n("invalid_marketplace_list");const r=typeof i.path=="string"?i.path:typeof i.installLocation=="string"?i.installLocation:null;if(!r)throw new n("invalid_marketplace_list");return{name:i.name,path:r}})}async listPlugins(a){const e=await this.run(a,["plugin","list","--json"]),t=w(e.stdout,"invalid_plugin_list");if(!Array.isArray(t))throw new n("invalid_plugin_list");return t.map(i=>{if(!h(i)||typeof i.id!="string"||typeof i.version!="string"||typeof i.scope!="string"||typeof i.enabled!="boolean")throw new n("invalid_plugin_list");return{id:i.id,version:i.version,scope:i.scope,enabled:i.enabled}})}async run(a,e){const t=await this.runCommand(a,e);if(t.code!==0)throw new n("claude_command_failed");return t}readReceipt(a){const e=k(this.receiptPath);return e==null||!h(e)||e.schemaVersion!==1||e.runtime!=="claude_code"||e.pluginId!==s||e.marketplaceName!==l||e.consentVersion!==1||e.marketplaceRootHash!==c(a.marketplaceRoot)||e.claudeConfigHash!==c(this.configRoot)||typeof e.consentedAt!="string"||typeof e.lastActionAt!="string"||e.state!=="installing"&&e.state!=="installed"&&e.state!=="disabled"?null:e}writeReceipt(a,e){const t=this.readReceipt(a),i=new Date().toISOString();v(f.dirname(this.receiptPath)),y(this.receiptPath,{schemaVersion:1,runtime:"claude_code",pluginId:s,marketplaceName:l,marketplaceRootHash:c(a.marketplaceRoot),claudeConfigHash:c(this.configRoot),bundleContentHash:e==="installing"?t?.bundleContentHash:m(a.pluginRoot),consentVersion:1,state:e,consentedAt:t?.consentedAt??i,lastActionAt:i})}removeReceipt(){C(this.receiptPath)}}function h(o){return!!o&&typeof o=="object"&&!Array.isArray(o)}function P(){return{available:!1,state:"unavailable",installedVersion:null,availableVersion:null,needsUpdate:!1,consentRequired:!0,restartRequired:!1}}function A(o,a){return{available:!0,state:"conflict",installedVersion:null,availableVersion:o.pluginVersion,needsUpdate:!1,consentRequired:!0,restartRequired:!1,errorCode:a}}export{j as ClaudeRoomPluginLifecycle};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ResolvedCommandSpec } from '../agents/command-spec.js';
|
|
2
|
+
import { type RoomPluginMarketplaceBundle, type CodexRoomPluginBundle } from './room-plugin-bundles.js';
|
|
3
|
+
import { type IntegrationCommandResult } from './room-plugin-lifecycle-common.js';
|
|
4
|
+
type Bundle = RoomPluginMarketplaceBundle<CodexRoomPluginBundle>;
|
|
5
|
+
type Action = 'install' | 'enable' | 'disable' | 'update' | 'uninstall';
|
|
6
|
+
export type CodexRoomPluginStatus = {
|
|
7
|
+
available: boolean;
|
|
8
|
+
state: 'unavailable' | 'not_installed' | 'enabled' | 'disabled' | 'conflict';
|
|
9
|
+
installedVersion: string | null;
|
|
10
|
+
availableVersion: string | null;
|
|
11
|
+
needsUpdate: boolean;
|
|
12
|
+
consentRequired: boolean;
|
|
13
|
+
restartRequired: boolean;
|
|
14
|
+
errorCode?: string;
|
|
15
|
+
};
|
|
16
|
+
export declare class CodexRoomPluginLifecycle {
|
|
17
|
+
private readonly receiptPath;
|
|
18
|
+
private readonly hooksFile;
|
|
19
|
+
private readonly resolveCommand;
|
|
20
|
+
private readonly ensureBundle;
|
|
21
|
+
private readonly runCommand;
|
|
22
|
+
constructor(options?: {
|
|
23
|
+
receiptPath?: string;
|
|
24
|
+
hooksFile?: string;
|
|
25
|
+
resolveCommand?: () => ResolvedCommandSpec | null;
|
|
26
|
+
ensureBundle?: () => Bundle;
|
|
27
|
+
runCommand?: (spec: ResolvedCommandSpec, args: string[]) => Promise<IntegrationCommandResult>;
|
|
28
|
+
});
|
|
29
|
+
status(options?: {
|
|
30
|
+
bundle?: Bundle;
|
|
31
|
+
restartRequired?: boolean;
|
|
32
|
+
}): Promise<CodexRoomPluginStatus>;
|
|
33
|
+
apply(action: Action): Promise<CodexRoomPluginStatus>;
|
|
34
|
+
private install;
|
|
35
|
+
private enable;
|
|
36
|
+
private disable;
|
|
37
|
+
private update;
|
|
38
|
+
private uninstall;
|
|
39
|
+
private reinstallPlugin;
|
|
40
|
+
private ensureMarketplaceRegistered;
|
|
41
|
+
private removeHook;
|
|
42
|
+
private listMarketplaces;
|
|
43
|
+
private listPlugins;
|
|
44
|
+
private run;
|
|
45
|
+
private readReceipt;
|
|
46
|
+
private writeReceipt;
|
|
47
|
+
private removeReceipt;
|
|
48
|
+
}
|
|
49
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import b from"node:os";import m from"node:path";import{resolveBuiltinCommand as C}from"../agents/command-spec.js";import{resolveShennianPath as A}from"../config/index.js";import{ensureCodexRoomMarketplaceBundle as P}from"./room-plugin-bundles.js";import{ensurePrivateDirectory as j,writeJsonAtomic as g}from"./room-plugin-shared.js";import{hashDirectoryContent as w,hashPath as f,parseJson as v,readJsonFile as R,removePrivateFile as x,RoomPluginLifecycleError as a,runIntegrationCommand as H,sameRealPath as k}from"./room-plugin-lifecycle-common.js";const u="shennian",c=`shennian-room@${u}`;class L{receiptPath;hooksFile;resolveCommand;ensureBundle;runCommand;constructor(i={}){this.receiptPath=i.receiptPath??A("integrations","codex","installation.json");const e=process.env.CODEX_HOME?.trim()||m.join(b.homedir(),".codex");this.hooksFile=i.hooksFile??m.join(e,"hooks.json"),this.resolveCommand=i.resolveCommand??(()=>C("codex")),this.ensureBundle=i.ensureBundle??(()=>P()),this.runCommand=i.runCommand??((t,n)=>H(t,n))}async status(i={}){const e=this.resolveCommand();if(!e)return M();const t=i.bundle??this.ensureBundle(),n=this.readReceipt(t),r=(await this.listMarketplaces(e)).find(h=>h.name===u);if(r&&!k(r.root,t.marketplaceRoot))return _(t,"foreign_marketplace");const l=(await this.listPlugins(e)).find(h=>h.pluginId===c);if(l&&!k(l.sourcePath,t.pluginRoot))return _(t,"foreign_plugin_installation");if(l){const h=l.version!==t.pluginVersion||n!=null&&(n.state==="installing"||n.bundleContentHash!==w(t.pluginRoot));return{available:!0,state:l.enabled?"enabled":"disabled",installedVersion:l.version,availableVersion:t.pluginVersion,needsUpdate:h,consentRequired:n==null,restartRequired:!!i.restartRequired}}return{available:!0,state:n?.state==="disabled"?"disabled":"not_installed",installedVersion:null,availableVersion:t.pluginVersion,needsUpdate:!1,consentRequired:n==null,restartRequired:!!i.restartRequired}}async apply(i){const e=this.resolveCommand();if(!e)throw new a("codex_unavailable");const t=this.ensureBundle(),n=await this.status({bundle:t});if(n.state==="conflict")throw new a(n.errorCode??"integration_conflict");return i==="install"?await this.install(e,t,n):i==="enable"?await this.enable(e,t,n):i==="disable"?await this.disable(e,t,n):i==="update"?await this.update(e,t,n):await this.uninstall(e,t,n),i==="uninstall"?this.status({bundle:t,restartRequired:!0}):this.status({bundle:t,restartRequired:!0})}async install(i,e,t){const n=this.readReceipt(e)?.hookCommand;n&&this.removeHook(n),this.writeReceipt(e,"installing"),await this.ensureMarketplaceRegistered(i,e),t.state==="enabled"?t.needsUpdate&&await this.reinstallPlugin(i):await this.run(i,["plugin","add",c,"--json"]),this.writeReceipt(e,"installed")}async enable(i,e,t){if(!this.readReceipt(e))throw new a("user_consent_required");t.state!=="enabled"&&(await this.ensureMarketplaceRegistered(i,e),await this.run(i,["plugin","add",c,"--json"]),this.writeReceipt(e,"installed"))}async disable(i,e,t){const n=this.readReceipt(e);if(!n)throw new a("user_consent_required");t.state==="enabled"&&await this.run(i,["plugin","remove",c,"--json"]),this.removeHook(n.hookCommand),this.writeReceipt(e,"disabled")}async update(i,e,t){const n=this.readReceipt(e);if(!n)throw new a("user_consent_required");if(this.removeHook(n.hookCommand),t.state==="enabled"){await this.reinstallPlugin(i),this.writeReceipt(e,"installed");return}this.writeReceipt(e,"disabled")}async uninstall(i,e,t){const n=this.readReceipt(e);if(t.state==="enabled"&&await this.run(i,["plugin","remove",c,"--json"]),n){this.removeHook(n.hookCommand);const d=(await this.listMarketplaces(i)).find(r=>r.name===u);d&&k(d.root,e.marketplaceRoot)&&await this.run(i,["plugin","marketplace","remove",u,"--json"])}this.removeReceipt()}async reinstallPlugin(i){await this.run(i,["plugin","remove",c,"--json"]),await this.run(i,["plugin","add",c,"--json"])}async ensureMarketplaceRegistered(i,e){const t=(await this.listMarketplaces(i)).find(n=>n.name===u);if(!t){await this.run(i,["plugin","marketplace","add",e.marketplaceRoot,"--json"]);return}if(!k(t.root,e.marketplaceRoot))throw new a("foreign_marketplace")}removeHook(i){const e=y(this.hooksFile,!0);if(!e.exists)return;const t={};for(const[n,d]of Object.entries(e.hooks)){const r=d.flatMap(p=>q(p,i));r.length>0&&(t[n]=r)}g(this.hooksFile,{...e.root,hooks:t})}async listMarketplaces(i){const e=await this.run(i,["plugin","marketplace","list","--json"]),t=v(e.stdout,"invalid_marketplace_list");if(!s(t)||!Array.isArray(t.marketplaces))throw new a("invalid_marketplace_list");return t.marketplaces.map(n=>{if(!s(n)||typeof n.name!="string"||typeof n.root!="string")throw new a("invalid_marketplace_list");return{name:n.name,root:n.root}})}async listPlugins(i){const e=await this.run(i,["plugin","list","--json"]),t=v(e.stdout,"invalid_plugin_list");if(!s(t)||!Array.isArray(t.installed))throw new a("invalid_plugin_list");return t.installed.map(n=>{if(!s(n)||typeof n.pluginId!="string"||typeof n.version!="string"||typeof n.enabled!="boolean"||!s(n.source)||typeof n.source.path!="string")throw new a("invalid_plugin_list");return{pluginId:n.pluginId,version:n.version,enabled:n.enabled,sourcePath:n.source.path}})}async run(i,e){const t=await this.runCommand(i,e);if(t.code!==0)throw new a("codex_command_failed");return t}readReceipt(i){const e=R(this.receiptPath);return e==null||!s(e)||e.schemaVersion!==1||e.runtime!=="codex"||e.pluginId!==c||e.marketplaceName!==u||e.consentVersion!==1||e.marketplaceRootHash!==f(i.marketplaceRoot)||e.codexHomeHash!==f(m.dirname(this.hooksFile))||typeof e.hookCommand!="string"||typeof e.consentedAt!="string"||typeof e.lastActionAt!="string"||e.state!=="installing"&&e.state!=="installed"&&e.state!=="disabled"?null:e}writeReceipt(i,e){const t=this.readReceipt(i),n=new Date().toISOString();j(m.dirname(this.receiptPath)),g(this.receiptPath,{schemaVersion:1,runtime:"codex",pluginId:c,marketplaceName:u,marketplaceRootHash:f(i.marketplaceRoot),codexHomeHash:f(m.dirname(this.hooksFile)),hookCommand:V(i),bundleContentHash:e==="installing"?t?.bundleContentHash:w(i.pluginRoot),consentVersion:1,state:e,consentedAt:t?.consentedAt??n,lastActionAt:n})}removeReceipt(){x(this.receiptPath)}}function V(o){const i=y(o.hooksFile);for(const e of Object.values(i.hooks))for(const t of e)if(!(!s(t)||!Array.isArray(t.hooks))){for(const n of t.hooks)if(s(n)&&n.type==="command"&&typeof n.command=="string")return n.command}throw new a("invalid_generated_hook")}function y(o,i=!1){const e=R(o);if(e==null&&i)return{exists:!1,root:{},hooks:{}};if(!s(e))throw new a("invalid_hook_config");const t=e.hooks;if(t!=null&&!s(t))throw new a("invalid_hook_config");const n={};for(const[p,l]of Object.entries(t??{})){if(!Array.isArray(l))throw new a("invalid_hook_config");n[p]=l}const{hooks:d,...r}=e;return{exists:!0,root:r,hooks:n}}function q(o,i){if(!s(o)||!Array.isArray(o.hooks))return[o];const e=o.hooks.filter(t=>!s(t)||t.command!==i);return e.length>0?[{...o,hooks:e}]:[]}function s(o){return!!o&&typeof o=="object"&&!Array.isArray(o)}function M(){return{available:!1,state:"unavailable",installedVersion:null,availableVersion:null,needsUpdate:!1,consentRequired:!0,restartRequired:!1}}function _(o,i){return{available:!0,state:"conflict",installedVersion:null,availableVersion:o.pluginVersion,needsUpdate:!1,consentRequired:!0,restartRequired:!1,errorCode:i}}export{L as CodexRoomPluginLifecycle};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type RoomPluginFileOptions, type RoomPluginFiles } from './room-plugin-shared.js';
|
|
2
|
+
export type CodexRoomPluginBundle = RoomPluginFiles & {
|
|
3
|
+
manifestFile: string;
|
|
4
|
+
};
|
|
5
|
+
export type ClaudeRoomPluginBundle = RoomPluginFiles & {
|
|
6
|
+
manifestFile: string;
|
|
7
|
+
};
|
|
8
|
+
export type RoomPluginMarketplaceBundle<T extends RoomPluginFiles> = T & {
|
|
9
|
+
marketplaceRoot: string;
|
|
10
|
+
marketplaceManifest: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function ensureCodexRoomPluginBundle(options?: RoomPluginFileOptions & {
|
|
13
|
+
pluginRoot?: string;
|
|
14
|
+
}): CodexRoomPluginBundle;
|
|
15
|
+
export declare function ensureClaudeRoomPluginBundle(options?: RoomPluginFileOptions & {
|
|
16
|
+
pluginRoot?: string;
|
|
17
|
+
}): ClaudeRoomPluginBundle;
|
|
18
|
+
export declare function ensureCodexRoomMarketplaceBundle(options?: RoomPluginFileOptions & {
|
|
19
|
+
marketplaceRoot?: string;
|
|
20
|
+
}): RoomPluginMarketplaceBundle<CodexRoomPluginBundle>;
|
|
21
|
+
export declare function ensureClaudeRoomMarketplaceBundle(options?: RoomPluginFileOptions & {
|
|
22
|
+
marketplaceRoot?: string;
|
|
23
|
+
}): RoomPluginMarketplaceBundle<ClaudeRoomPluginBundle>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"node:path";import{resolveShennianPath as l}from"../config/index.js";import{ensurePrivateDirectory as a,ensureSharedRoomPluginFiles as p,ROOM_PLUGIN_NAME as s,writeJsonAtomic as c}from"./room-plugin-shared.js";function u(o={}){const n=o.pluginRoot??l("integrations","codex","plugin"),t=p("codex",n,o),i=e.join(n,".codex-plugin"),r=e.join(i,"plugin.json");return a(i),c(r,{name:s,version:t.pluginVersion,description:"Connect Codex to Shennian collaboration spaces and stable private Agents.",author:{name:"Shennian"},homepage:"https://shennian.net",license:"MIT",keywords:["shennian","room","collaboration"],skills:"./skills/",mcpServers:"./.mcp.json",interface:{displayName:"Shennian Room",shortDescription:"Collaborate with people and Agents in a Shennian space.",longDescription:"Create or join a Shennian collaboration space and securely exchange messages from the current Codex conversation.",developerName:"Shennian",category:"Productivity",capabilities:["Interactive","Read","Write"],websiteURL:"https://shennian.net",defaultPrompt:["Create a collaboration space for this conversation.","Join this Shennian invitation link.","Create a private Shennian Agent for this project."]}}),{...t,manifestFile:r}}function g(o={}){const n=o.pluginRoot??l("integrations","claude-code","plugin"),t=p("claude_code",n,o),i=e.join(n,".claude-plugin"),r=e.join(i,"plugin.json");return a(i),c(r,{name:s,version:t.pluginVersion,description:"Connect the current Claude Code conversation to a Shennian collaboration space.",author:{name:"Shennian"},homepage:"https://shennian.net",license:"MIT"}),{...t,manifestFile:r}}function f(o={}){const n=o.marketplaceRoot??l("integrations","codex","marketplace");a(n),a(e.join(n,".agents")),a(e.join(n,".agents","plugins")),a(e.join(n,"plugins"));const t=u({...o,pluginRoot:e.join(n,"plugins",s)}),i=e.join(n,".agents","plugins","marketplace.json");return c(i,{name:"shennian",interface:{displayName:"Shennian"},plugins:[{name:s,source:{source:"local",path:`./plugins/${s}`},policy:{installation:"AVAILABLE",authentication:"ON_INSTALL"},category:"Productivity"}]}),{...t,marketplaceRoot:n,marketplaceManifest:i}}function R(o={}){const n=o.marketplaceRoot??l("integrations","claude-code","marketplace");a(n),a(e.join(n,".claude-plugin")),a(e.join(n,"plugins"));const t=g({...o,pluginRoot:e.join(n,"plugins",s)}),i=e.join(n,".claude-plugin","marketplace.json");return c(i,{name:"shennian",owner:{name:"Shennian"},plugins:[{name:s,source:`./plugins/${s}`,description:"Connect Claude Code to a Shennian collaboration space.",version:t.pluginVersion}]}),{...t,marketplaceRoot:n,marketplaceManifest:i}}export{R as ensureClaudeRoomMarketplaceBundle,g as ensureClaudeRoomPluginBundle,f as ensureCodexRoomMarketplaceBundle,u as ensureCodexRoomPluginBundle};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ResolvedCommandSpec } from '../agents/command-spec.js';
|
|
2
|
+
export type IntegrationCommandResult = {
|
|
3
|
+
code: number | null;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function runIntegrationCommand(spec: ResolvedCommandSpec, args: string[], env?: NodeJS.ProcessEnv): Promise<IntegrationCommandResult>;
|
|
8
|
+
export declare function parseJson(raw: string, code: string): unknown;
|
|
9
|
+
export declare function hashPath(value: string): string;
|
|
10
|
+
export declare function hashDirectoryContent(root: string): string;
|
|
11
|
+
export declare function sameRealPath(first: string, second: string): boolean;
|
|
12
|
+
export declare function readJsonFile(filePath: string, maxBytes?: number): unknown | null;
|
|
13
|
+
export declare function removePrivateFile(filePath: string): void;
|
|
14
|
+
export declare class RoomPluginLifecycleError extends Error {
|
|
15
|
+
readonly code: string;
|
|
16
|
+
constructor(code: string);
|
|
17
|
+
}
|