shennian 0.2.6 → 0.2.8

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 (85) hide show
  1. package/dist/bin/shennian.d.ts +2 -0
  2. package/dist/bin/shennian.js +2 -0
  3. package/dist/src/agents/adapter.d.ts +35 -0
  4. package/dist/src/agents/adapter.js +19 -0
  5. package/dist/src/agents/claude.d.ts +25 -0
  6. package/dist/src/agents/claude.js +234 -0
  7. package/dist/src/agents/codex.d.ts +52 -0
  8. package/dist/src/agents/codex.js +829 -0
  9. package/dist/src/agents/command-spec.d.ts +36 -0
  10. package/dist/src/agents/command-spec.js +303 -0
  11. package/dist/src/agents/cursor.d.ts +22 -0
  12. package/dist/src/agents/cursor.js +248 -0
  13. package/dist/src/agents/custom.d.ts +30 -0
  14. package/dist/src/agents/custom.js +210 -0
  15. package/dist/src/agents/detect.d.ts +9 -0
  16. package/dist/src/agents/detect.js +46 -0
  17. package/dist/src/agents/gemini.d.ts +17 -0
  18. package/dist/src/agents/gemini.js +172 -0
  19. package/dist/src/agents/model-registry/cache.d.ts +4 -0
  20. package/dist/src/agents/model-registry/cache.js +37 -0
  21. package/dist/src/agents/model-registry/discovery.d.ts +4 -0
  22. package/dist/src/agents/model-registry/discovery.js +166 -0
  23. package/dist/src/agents/model-registry/parsers.d.ts +11 -0
  24. package/dist/src/agents/model-registry/parsers.js +349 -0
  25. package/dist/src/agents/model-registry/runner.d.ts +6 -0
  26. package/dist/src/agents/model-registry/runner.js +29 -0
  27. package/dist/src/agents/model-registry/service.d.ts +6 -0
  28. package/dist/src/agents/model-registry/service.js +71 -0
  29. package/dist/src/agents/model-registry/types.d.ts +30 -0
  30. package/dist/src/agents/model-registry/types.js +8 -0
  31. package/dist/src/agents/model-registry.d.ts +13 -0
  32. package/dist/src/agents/model-registry.js +15 -0
  33. package/dist/src/agents/openclaw.d.ts +20 -0
  34. package/dist/src/agents/openclaw.js +273 -0
  35. package/dist/src/agents/pi.d.ts +51 -0
  36. package/dist/src/agents/pi.js +793 -0
  37. package/dist/src/commands/agent.d.ts +2 -0
  38. package/dist/src/commands/agent.js +131 -0
  39. package/dist/src/commands/daemon.d.ts +35 -0
  40. package/dist/src/commands/daemon.js +612 -0
  41. package/dist/src/commands/pair-qr.d.ts +4 -0
  42. package/dist/src/commands/pair-qr.js +6 -0
  43. package/dist/src/commands/pair.d.ts +8 -0
  44. package/dist/src/commands/pair.js +162 -0
  45. package/dist/src/commands/upgrade.d.ts +5 -0
  46. package/dist/src/commands/upgrade.js +145 -0
  47. package/dist/src/config/index.d.ts +32 -0
  48. package/dist/src/config/index.js +31 -0
  49. package/dist/src/fs/handler.d.ts +26 -0
  50. package/dist/src/fs/handler.js +130 -0
  51. package/dist/src/fs/security.d.ts +2 -0
  52. package/dist/src/fs/security.js +32 -0
  53. package/dist/src/index.d.ts +1 -0
  54. package/dist/src/index.js +277 -0
  55. package/dist/src/log-reporter.d.ts +18 -0
  56. package/dist/src/log-reporter.js +16 -0
  57. package/dist/src/native-fusion/parsers.d.ts +12 -0
  58. package/dist/src/native-fusion/parsers.js +687 -0
  59. package/dist/src/native-fusion/service.d.ts +25 -0
  60. package/dist/src/native-fusion/service.js +176 -0
  61. package/dist/src/native-fusion/state.d.ts +3 -0
  62. package/dist/src/native-fusion/state.js +22 -0
  63. package/dist/src/native-fusion/types.d.ts +24 -0
  64. package/dist/src/native-fusion/types.js +1 -0
  65. package/dist/src/region.d.ts +15 -0
  66. package/dist/src/region.js +99 -0
  67. package/dist/src/relay/client.d.ts +62 -0
  68. package/dist/src/relay/client.js +311 -0
  69. package/dist/src/session/handlers/agents.d.ts +4 -0
  70. package/dist/src/session/handlers/agents.js +55 -0
  71. package/dist/src/session/handlers/chat.d.ts +4 -0
  72. package/dist/src/session/handlers/chat.js +326 -0
  73. package/dist/src/session/handlers/control.d.ts +5 -0
  74. package/dist/src/session/handlers/control.js +57 -0
  75. package/dist/src/session/handlers/fs.d.ts +10 -0
  76. package/dist/src/session/handlers/fs.js +256 -0
  77. package/dist/src/session/manager.d.ts +28 -0
  78. package/dist/src/session/manager.js +174 -0
  79. package/dist/src/session/store.d.ts +7 -0
  80. package/dist/src/session/store.js +46 -0
  81. package/dist/src/session/types.d.ts +35 -0
  82. package/dist/src/session/types.js +4 -0
  83. package/dist/src/upgrade/engine.d.ts +65 -0
  84. package/dist/src/upgrade/engine.js +239 -0
  85. package/package.json +2 -2
@@ -0,0 +1,277 @@
1
+ // @arch docs/architecture/cli/daemon.md
2
+ // @test src/__tests__/agents-e2e.ts
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import os from 'node:os';
6
+ import fs from 'node:fs';
7
+ import { loadConfig, saveConfig, configPath, getShennianDir, resolveShennianPath } from './config/index.js';
8
+ import { CliRelayClient } from './relay/client.js';
9
+ import { registerPairCommand, runSmartStart } from './commands/pair.js';
10
+ import { registerDaemonCommand } from './commands/daemon.js';
11
+ import { registerAgentCommand } from './commands/agent.js';
12
+ import { registerUpgradeCommand } from './commands/upgrade.js';
13
+ import { SessionManager } from './session/manager.js';
14
+ import { SERVERS, regionToUrl, urlToRegion } from './region.js';
15
+ import { getCurrentVersion, handleStartupCrashCheck, checkForUpdate } from './upgrade/engine.js';
16
+ import { detectAgents } from './agents/detect.js';
17
+ const cliVersion = getCurrentVersion();
18
+ import { getCachedAgentInfos, resolveAgentInfos } from './agents/model-registry.js';
19
+ import { initCliLogReporter, reportLog } from './log-reporter.js';
20
+ import { NativeSessionFusionService } from './native-fusion/service.js';
21
+ const SHENNIAN_DIR = getShennianDir();
22
+ const PID_FILE = resolveShennianPath('daemon.pid');
23
+ function httpToWs(url) {
24
+ return url.replace(/^https:\/\//, 'wss://').replace(/^http:\/\//, 'ws://');
25
+ }
26
+ function formatDisconnectInfo(info) {
27
+ const parts = [
28
+ `trigger=${info.trigger ?? 'unknown'}`,
29
+ `phase=${info.phase}`,
30
+ `code=${info.code ?? 'unknown'}`,
31
+ `attempt=${info.reconnectAttempt}`,
32
+ ];
33
+ if (info.reason)
34
+ parts.push(`reason=${info.reason}`);
35
+ if (info.error)
36
+ parts.push(`error=${info.error}`);
37
+ return parts.join(' ');
38
+ }
39
+ const program = new Command();
40
+ program
41
+ .name('shennian')
42
+ .description('Shennian — AI Agent Mobile Console')
43
+ .version(cliVersion);
44
+ program
45
+ .option('--api <url>', 'Server URL override')
46
+ .option('--name <name>', 'Machine name', os.hostname())
47
+ .action(async (opts) => {
48
+ const config = loadConfig();
49
+ const serverUrl = opts.api ?? config.serverUrl ?? undefined;
50
+ await runSmartStart(serverUrl, opts.name);
51
+ });
52
+ // start: internal command used by the daemon process
53
+ program
54
+ .command('start', { hidden: true })
55
+ .description('(internal) Connect to relay server, called by daemon')
56
+ .option('--api <url>', 'Server URL override')
57
+ .action(async (opts) => {
58
+ const envFile = resolveShennianPath('env.json');
59
+ try {
60
+ const saved = JSON.parse(fs.readFileSync(envFile, 'utf-8'));
61
+ for (const [k, v] of Object.entries(saved)) {
62
+ if (!process.env[k])
63
+ process.env[k] = v;
64
+ }
65
+ }
66
+ catch {
67
+ // env.json may not exist yet
68
+ }
69
+ // Single-instance guard: kill any existing daemon before starting
70
+ const pidFile = resolveShennianPath('daemon.pid');
71
+ try {
72
+ const oldPid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
73
+ if (oldPid && oldPid !== process.pid) {
74
+ try {
75
+ process.kill(oldPid, 0);
76
+ console.log(`[${new Date().toISOString()}] daemon already running (PID ${oldPid}), skipping duplicate start`);
77
+ process.exit(0);
78
+ }
79
+ catch {
80
+ // Stale pid; continue booting and overwrite it below.
81
+ }
82
+ }
83
+ }
84
+ catch { /* noop */ }
85
+ fs.writeFileSync(pidFile, String(process.pid));
86
+ process.on('exit', () => { try {
87
+ fs.unlinkSync(pidFile);
88
+ }
89
+ catch { /* noop */ } });
90
+ // Crash detection: if we're recovering from a failed upgrade, rollback and exit
91
+ const didRollback = await handleStartupCrashCheck();
92
+ if (didRollback) {
93
+ console.log(`[${new Date().toISOString()}] Rolled back to previous version, restarting...`);
94
+ process.exit(0);
95
+ }
96
+ const config = loadConfig();
97
+ if (!config.machineToken) {
98
+ console.error(chalk.red('✗ Not paired yet. Run: shennian'));
99
+ process.exit(1);
100
+ }
101
+ const serverUrl = opts.api ?? config.serverUrl ?? SERVERS.global.url;
102
+ const wsBase = httpToWs(serverUrl);
103
+ const wsUrl = `${wsBase}/relay/machine`;
104
+ const cliVersion = getCurrentVersion();
105
+ const detectedAgents = detectAgents();
106
+ const agentList = detectedAgents.map((a) => a.type);
107
+ const cachedAgentInfos = getCachedAgentInfos(detectedAgents);
108
+ if (config.machineId) {
109
+ initCliLogReporter(serverUrl, config.machineId);
110
+ }
111
+ console.log(`[${new Date().toISOString()}] Connecting to ${wsUrl}... (v${cliVersion}) agents: ${agentList.join(',')}`);
112
+ reportLog({ level: 'info', wsEvent: 'daemon.start', metadata: { version: cliVersion, agents: agentList } });
113
+ let nativeFusion = null;
114
+ const client = new CliRelayClient({
115
+ serverUrl: wsUrl,
116
+ machineToken: config.machineToken,
117
+ cliVersion,
118
+ agentList,
119
+ onConnected: () => {
120
+ console.log(`[${new Date().toISOString()}] ✓ Connected`);
121
+ reportLog({ level: 'info', wsEvent: 'daemon.connected' });
122
+ if (cachedAgentInfos.some((agent) => agent.models.length > 0)) {
123
+ client.sendEvent({
124
+ type: 'event',
125
+ event: 'machine.agents',
126
+ payload: {
127
+ agentList,
128
+ agents: cachedAgentInfos,
129
+ },
130
+ });
131
+ }
132
+ void resolveAgentInfos(detectedAgents, {
133
+ serverUrl: config.serverUrl ?? serverUrl,
134
+ authToken: config.machineToken ?? config.accessToken,
135
+ }).then((agents) => {
136
+ if (JSON.stringify(agents) === JSON.stringify(cachedAgentInfos))
137
+ return;
138
+ client.sendEvent({
139
+ type: 'event',
140
+ event: 'machine.agents',
141
+ payload: {
142
+ agentList,
143
+ agents,
144
+ },
145
+ });
146
+ }).catch(() => { });
147
+ import('./upgrade/engine.js').then(({ readUpgradeAttempt, clearUpgradeAttempt }) => {
148
+ const attempt = readUpgradeAttempt();
149
+ clearUpgradeAttempt();
150
+ if (attempt) {
151
+ console.log(`[${new Date().toISOString()}] [upgrade] Reporting success: ${attempt.from} → ${attempt.to}`);
152
+ client.sendEvent({
153
+ type: 'event',
154
+ event: 'upgrade',
155
+ payload: { state: 'success', machineId: 'self', from: attempt.from, to: attempt.to },
156
+ });
157
+ }
158
+ }).catch(() => { });
159
+ void scheduleAutoUpgrade(client, config.autoUpgrade ?? 'patch');
160
+ nativeFusion?.handleConnected();
161
+ },
162
+ onDisconnected: (info) => {
163
+ console.log(`[${new Date().toISOString()}] ⚠ Disconnected, reconnecting... ${formatDisconnectInfo(info)}`);
164
+ reportLog({
165
+ level: 'warn',
166
+ wsEvent: 'daemon.disconnected',
167
+ metadata: {
168
+ code: info.code,
169
+ reason: info.reason,
170
+ error: info.error,
171
+ phase: info.phase,
172
+ trigger: info.trigger,
173
+ reconnectAttempt: info.reconnectAttempt,
174
+ },
175
+ });
176
+ },
177
+ onReq: (req) => {
178
+ console.log(`[${new Date().toISOString()}] [req] ${req.method}`);
179
+ reportLog({ level: 'info', type: 'ws', wsEvent: req.method, wsDirection: 'in', traceId: req.traceId });
180
+ void sessionManager.handleReq(req);
181
+ },
182
+ });
183
+ nativeFusion = process.env.SHENNIAN_NATIVE_FUSION_DISABLED === '1'
184
+ ? null
185
+ : new NativeSessionFusionService(client);
186
+ const sessionManager = new SessionManager(client, nativeFusion);
187
+ fs.mkdirSync(SHENNIAN_DIR, { recursive: true });
188
+ fs.writeFileSync(PID_FILE, String(process.pid));
189
+ client.connect();
190
+ process.stdin.resume();
191
+ const shutdown = () => {
192
+ console.log(chalk.gray('\nDisconnecting...'));
193
+ reportLog({ level: 'info', wsEvent: 'daemon.stop' });
194
+ sessionManager.cleanup();
195
+ nativeFusion?.stop();
196
+ client.disconnect();
197
+ try {
198
+ fs.unlinkSync(PID_FILE);
199
+ }
200
+ catch { /* noop */ }
201
+ process.exit(0);
202
+ };
203
+ process.on('SIGINT', shutdown);
204
+ process.on('SIGTERM', shutdown);
205
+ });
206
+ // config: view and modify settings
207
+ const configCmd = program
208
+ .command('config')
209
+ .description('View or modify configuration');
210
+ configCmd
211
+ .command('show', { isDefault: true })
212
+ .description('Show current configuration')
213
+ .action(() => {
214
+ console.log(`Config file: ${configPath}`);
215
+ const config = loadConfig();
216
+ const display = { ...config };
217
+ if (display.machineToken)
218
+ display.machineToken = display.machineToken.slice(0, 12) + '...';
219
+ if (display.serverUrl) {
220
+ const region = urlToRegion(display.serverUrl);
221
+ console.log(`Region: ${region} (${SERVERS[region].label})`);
222
+ }
223
+ console.log(JSON.stringify(display, null, 2));
224
+ });
225
+ configCmd
226
+ .command('set')
227
+ .description('Update a config value')
228
+ .argument('<key>', 'Config key (e.g. "server")')
229
+ .argument('<value>', 'Config value (e.g. "cn" or "global")')
230
+ .action((key, value) => {
231
+ const config = loadConfig();
232
+ if (key === 'server') {
233
+ if (value !== 'cn' && value !== 'global') {
234
+ console.error(chalk.red('✗ Value must be "cn" or "global"'));
235
+ process.exit(1);
236
+ }
237
+ const region = value;
238
+ config.serverUrl = regionToUrl(region);
239
+ saveConfig(config);
240
+ console.log(chalk.green(`✓ Server set to ${SERVERS[region].label}`));
241
+ console.log(chalk.yellow(' Restart the daemon for this to take effect: shennian daemon stop && shennian'));
242
+ return;
243
+ }
244
+ console.error(chalk.red(`✗ Unknown config key: ${key}. Supported: server`));
245
+ process.exit(1);
246
+ });
247
+ registerPairCommand(program);
248
+ registerDaemonCommand(program);
249
+ registerAgentCommand(program);
250
+ registerUpgradeCommand(program);
251
+ program.parse();
252
+ // ─── Auto-upgrade helper ──────────────────────────────────────────────────────
253
+ async function scheduleAutoUpgrade(client, policy) {
254
+ if (policy === 'none')
255
+ return;
256
+ // Delay slightly so we don't hit npm on every cold start during development
257
+ await new Promise((r) => setTimeout(r, 30_000));
258
+ let result;
259
+ try {
260
+ result = await checkForUpdate();
261
+ }
262
+ catch {
263
+ return; // silently skip on network error
264
+ }
265
+ if (!result.hasUpdate)
266
+ return;
267
+ const { changeType, current, latest } = result;
268
+ // Respect the policy
269
+ if (policy === 'patch' && changeType !== 'patch')
270
+ return;
271
+ if (policy === 'minor' && changeType === 'major')
272
+ return;
273
+ console.log(`[${new Date().toISOString()}] [upgrade] ${current} → ${latest} (${changeType}), auto-upgrading...`);
274
+ const { handleUpgradeStart } = await import('./commands/upgrade.js');
275
+ // Use a dummy reqId since this is self-triggered
276
+ await handleUpgradeStart(client, 'auto-upgrade', latest);
277
+ }
@@ -0,0 +1,18 @@
1
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+ type LogType = 'http' | 'ws' | 'internal';
3
+ type LogEntry = {
4
+ traceId?: string;
5
+ sessionId?: string;
6
+ type?: LogType;
7
+ level: LogLevel;
8
+ wsEvent?: string;
9
+ wsDirection?: 'in' | 'out';
10
+ errorMessage?: string;
11
+ errorStack?: string;
12
+ durationMs?: number;
13
+ metadata?: unknown;
14
+ };
15
+ export declare function initCliLogReporter(serverUrl: string, machineId: string): void;
16
+ export declare function generateTraceId(): string;
17
+ export declare function reportLog(entry: LogEntry): void;
18
+ export {};
@@ -0,0 +1,16 @@
1
+ import crypto from 'node:crypto';
2
+ let _serverUrl;
3
+ let _machineId;
4
+ export function initCliLogReporter(serverUrl, machineId) {
5
+ _serverUrl = serverUrl.replace(/\/$/, '');
6
+ _machineId = machineId;
7
+ }
8
+ export function generateTraceId() {
9
+ return crypto.randomBytes(8).toString('hex');
10
+ }
11
+ export function reportLog(entry) {
12
+ if (!_serverUrl || !_machineId)
13
+ return;
14
+ // Remote DB log reporting is intentionally disabled; keep the API for callers.
15
+ void entry;
16
+ }
@@ -0,0 +1,12 @@
1
+ import type { ParsedNativeEvent } from './types.js';
2
+ export declare function listCodexRolloutFiles(): string[];
3
+ export declare function listClaudeTranscriptFiles(): string[];
4
+ export declare function lookupClaudeTranscriptCwd(sourceSessionKey: string): string | null;
5
+ export declare function parseCodexRolloutChunk(filePath: string, startOffset: number): {
6
+ nextOffset: number;
7
+ events: ParsedNativeEvent[];
8
+ };
9
+ export declare function parseClaudeTranscriptChunk(filePath: string, startOffset: number): {
10
+ nextOffset: number;
11
+ events: ParsedNativeEvent[];
12
+ };