shennian 0.3.86 → 0.4.0

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