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,25 @@
1
+ import type { CliRelayClient } from '../relay/client.js';
2
+ export declare class NativeSessionFusionService {
3
+ private client;
4
+ private timer;
5
+ private scanPromise;
6
+ private pendingClaims;
7
+ private suppressionWindows;
8
+ constructor(client: CliRelayClient);
9
+ start(): void;
10
+ stop(): void;
11
+ handleConnected(): void;
12
+ registerManagedSend(params: {
13
+ sessionId: string;
14
+ agentType: string;
15
+ canonicalMessageId?: string | null;
16
+ sourceSessionKey?: string | null;
17
+ text: string;
18
+ }): void;
19
+ noteManagedSourceSession(sessionId: string, agentType: string, sourceSessionKey: string | null): void;
20
+ private pruneState;
21
+ scanNow(): Promise<void>;
22
+ private runScan;
23
+ private tryClaimManagedEcho;
24
+ private isSuppressed;
25
+ }
@@ -0,0 +1,176 @@
1
+ // @arch docs/architecture/cli/native-session-fusion.md
2
+ // @test src/__tests__/native-fusion-parsers.test.ts
3
+ import { randomUUID } from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import { listClaudeTranscriptFiles, listCodexRolloutFiles, parseClaudeTranscriptChunk, parseCodexRolloutChunk, } from './parsers.js';
6
+ import { loadNativeScannerState, saveNativeScannerState } from './state.js';
7
+ const SCAN_INTERVAL_MS = 60_000;
8
+ const CLAIM_TTL_MS = SCAN_INTERVAL_MS * 2;
9
+ const MANAGED_RUN_SUPPRESSION_TTL_MS = 30 * 60_000;
10
+ const MAX_BATCH_SIZE = 20;
11
+ function normalizeText(text) {
12
+ return text.replace(/\r\n/g, '\n').trim();
13
+ }
14
+ export class NativeSessionFusionService {
15
+ client;
16
+ timer = null;
17
+ scanPromise = null;
18
+ pendingClaims = new Map();
19
+ suppressionWindows = new Map();
20
+ constructor(client) {
21
+ this.client = client;
22
+ }
23
+ start() {
24
+ if (this.timer)
25
+ return;
26
+ this.timer = setInterval(() => {
27
+ void this.scanNow().catch((error) => {
28
+ console.error('[native-fusion] periodic scan failed', error);
29
+ });
30
+ }, SCAN_INTERVAL_MS);
31
+ }
32
+ stop() {
33
+ if (this.timer)
34
+ clearInterval(this.timer);
35
+ this.timer = null;
36
+ }
37
+ handleConnected() {
38
+ this.start();
39
+ }
40
+ registerManagedSend(params) {
41
+ if (!params.canonicalMessageId)
42
+ return;
43
+ if (params.agentType !== 'codex' && params.agentType !== 'claude')
44
+ return;
45
+ this.pendingClaims.set(params.sessionId, {
46
+ sessionId: params.sessionId,
47
+ agentType: params.agentType,
48
+ canonicalMessageId: params.canonicalMessageId,
49
+ text: normalizeText(params.text),
50
+ createdAt: Date.now(),
51
+ expiresAt: Date.now() + CLAIM_TTL_MS,
52
+ sourceSessionKey: params.sourceSessionKey ?? null,
53
+ });
54
+ }
55
+ noteManagedSourceSession(sessionId, agentType, sourceSessionKey) {
56
+ if (!sourceSessionKey)
57
+ return;
58
+ const claim = this.pendingClaims.get(sessionId);
59
+ if (claim && claim.agentType === agentType) {
60
+ claim.sourceSessionKey = sourceSessionKey;
61
+ }
62
+ }
63
+ pruneState() {
64
+ const now = Date.now();
65
+ for (const [sessionId, claim] of this.pendingClaims.entries()) {
66
+ if (claim.expiresAt <= now)
67
+ this.pendingClaims.delete(sessionId);
68
+ }
69
+ for (const [key, window] of this.suppressionWindows.entries()) {
70
+ if (window.endsAt <= now)
71
+ this.suppressionWindows.delete(key);
72
+ }
73
+ }
74
+ async scanNow() {
75
+ if (this.client.getState() !== 'connected')
76
+ return;
77
+ if (this.scanPromise)
78
+ return this.scanPromise;
79
+ this.scanPromise = this.runScan().finally(() => {
80
+ this.scanPromise = null;
81
+ });
82
+ return this.scanPromise;
83
+ }
84
+ async runScan() {
85
+ this.pruneState();
86
+ const state = loadNativeScannerState();
87
+ const nextFilesState = { ...state.files };
88
+ const batches = [];
89
+ const files = [...listCodexRolloutFiles(), ...listClaudeTranscriptFiles()];
90
+ for (const filePath of files) {
91
+ const stat = fs.statSync(filePath);
92
+ const current = state.files[filePath];
93
+ const startOffset = current?.offset ?? 0;
94
+ const parsed = filePath.includes('/.codex/sessions/')
95
+ ? parseCodexRolloutChunk(filePath, startOffset)
96
+ : parseClaudeTranscriptChunk(filePath, startOffset);
97
+ nextFilesState[filePath] = {
98
+ offset: parsed.nextOffset,
99
+ mtimeMs: stat.mtimeMs,
100
+ };
101
+ for (const event of parsed.events) {
102
+ const claimed = this.tryClaimManagedEcho(event);
103
+ if (claimed) {
104
+ batches.push(claimed);
105
+ continue;
106
+ }
107
+ if (this.isSuppressed(event))
108
+ continue;
109
+ batches.push(event);
110
+ }
111
+ }
112
+ for (let index = 0; index < batches.length; index += MAX_BATCH_SIZE) {
113
+ const chunk = batches.slice(index, index + MAX_BATCH_SIZE);
114
+ if (chunk.length === 0)
115
+ continue;
116
+ await this.client.sendBufferedEvent({
117
+ type: 'event',
118
+ event: 'native.session.events',
119
+ id: `native-fusion-${randomUUID()}`,
120
+ payload: { events: chunk },
121
+ }, 60_000);
122
+ }
123
+ state.files = nextFilesState;
124
+ saveNativeScannerState(state);
125
+ }
126
+ tryClaimManagedEcho(event) {
127
+ const now = Date.now();
128
+ for (const [sessionId, claim] of this.pendingClaims.entries()) {
129
+ if (claim.agentType !== event.agentType)
130
+ continue;
131
+ if (claim.expiresAt <= now)
132
+ continue;
133
+ if (event.role !== 'user')
134
+ continue;
135
+ if (!claim.sourceSessionKey)
136
+ continue;
137
+ if (claim.sourceSessionKey !== event.sourceSessionKey)
138
+ continue;
139
+ if (Math.abs(event.ts - claim.createdAt) > CLAIM_TTL_MS)
140
+ continue;
141
+ if (normalizeText(event.payload) !== claim.text)
142
+ continue;
143
+ this.pendingClaims.delete(sessionId);
144
+ this.suppressionWindows.set(`${event.agentType}:${event.sourceSessionKey}`, {
145
+ sessionId,
146
+ agentType: event.agentType,
147
+ sourceSessionKey: event.sourceSessionKey,
148
+ startedAt: event.ts,
149
+ endsAt: Math.max(now, event.ts) + MANAGED_RUN_SUPPRESSION_TTL_MS,
150
+ });
151
+ return {
152
+ ...event,
153
+ sourceMode: 'managed_echo_claim',
154
+ claimCanonicalMessageId: claim.canonicalMessageId,
155
+ };
156
+ }
157
+ return null;
158
+ }
159
+ isSuppressed(event) {
160
+ const key = `${event.agentType}:${event.sourceSessionKey}`;
161
+ const window = this.suppressionWindows.get(key);
162
+ if (!window)
163
+ return false;
164
+ if (event.ts < window.startedAt)
165
+ return false;
166
+ if (event.ts > window.endsAt) {
167
+ this.suppressionWindows.delete(key);
168
+ return false;
169
+ }
170
+ if (event.role === 'user') {
171
+ this.suppressionWindows.delete(key);
172
+ return false;
173
+ }
174
+ return true;
175
+ }
176
+ }
@@ -0,0 +1,3 @@
1
+ import type { NativeScannerState } from './types.js';
2
+ export declare function loadNativeScannerState(): NativeScannerState;
3
+ export declare function saveNativeScannerState(state: NativeScannerState): void;
@@ -0,0 +1,22 @@
1
+ // @arch docs/architecture/cli/native-session-fusion.md
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { resolveShennianPath } from '../config/index.js';
5
+ const STATE_FILE = resolveShennianPath('native-fusion-state.json');
6
+ export function loadNativeScannerState() {
7
+ try {
8
+ const raw = fs.readFileSync(STATE_FILE, 'utf-8');
9
+ const parsed = JSON.parse(raw);
10
+ if (parsed && typeof parsed === 'object' && parsed.files && typeof parsed.files === 'object') {
11
+ return parsed;
12
+ }
13
+ }
14
+ catch {
15
+ // ignore missing or malformed state
16
+ }
17
+ return { files: {} };
18
+ }
19
+ export function saveNativeScannerState(state) {
20
+ fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
21
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
22
+ }
@@ -0,0 +1,24 @@
1
+ import type { NativeSessionEventPayload } from '@shennian/wire';
2
+ export type NativeScannerState = {
3
+ files: Record<string, {
4
+ offset: number;
5
+ mtimeMs: number;
6
+ }>;
7
+ };
8
+ export type ParsedNativeEvent = NativeSessionEventPayload;
9
+ export type PendingManagedClaim = {
10
+ sessionId: string;
11
+ agentType: 'codex' | 'claude';
12
+ canonicalMessageId: string;
13
+ text: string;
14
+ createdAt: number;
15
+ expiresAt: number;
16
+ sourceSessionKey?: string | null;
17
+ };
18
+ export type ManagedSuppressionWindow = {
19
+ sessionId: string;
20
+ agentType: 'codex' | 'claude';
21
+ sourceSessionKey: string;
22
+ startedAt: number;
23
+ endsAt: number;
24
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ export type Region = 'cn' | 'global';
2
+ export declare const SERVERS: Record<Region, {
3
+ url: string;
4
+ label: string;
5
+ }>;
6
+ /**
7
+ * Detect the best server region and return its URL.
8
+ *
9
+ * Strategy: timezone as primary signal, IP geolocation as supplement.
10
+ * - Chinese timezone → cn immediately (covers servers with UTC too via IP fallback).
11
+ * - Non-Chinese timezone → try IP to detect if physically in China.
12
+ */
13
+ export declare function detectAndChooseServer(): Promise<string>;
14
+ export declare function urlToRegion(url: string): Region;
15
+ export declare function regionToUrl(region: Region): string;
@@ -0,0 +1,99 @@
1
+ import chalk from 'chalk';
2
+ export const SERVERS = {
3
+ cn: { url: 'https://shennian.net', label: 'China (shennian.net)' },
4
+ global: { url: 'https://shennian.ai', label: 'Global (shennian.ai)' },
5
+ };
6
+ const CN_TIMEZONES = [
7
+ 'Asia/Shanghai', 'Asia/Chongqing', 'Asia/Chungking',
8
+ 'Asia/Harbin', 'Asia/Urumqi', 'PRC',
9
+ ];
10
+ function inferRegionByTimezone() {
11
+ try {
12
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
13
+ if (CN_TIMEZONES.includes(tz))
14
+ return 'cn';
15
+ }
16
+ catch { /* Intl unavailable */ }
17
+ return 'global';
18
+ }
19
+ async function detectRegionByIP(timeoutMs = 1500) {
20
+ const controller = new AbortController();
21
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
22
+ try {
23
+ const res = await Promise.any([
24
+ fetch(`${SERVERS.cn.url}/geo`, { signal: controller.signal }),
25
+ fetch(`${SERVERS.global.url}/geo`, { signal: controller.signal }),
26
+ ]);
27
+ const data = await res.json();
28
+ return data.country === 'CN' ? 'cn' : 'global';
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ finally {
34
+ clearTimeout(timer);
35
+ }
36
+ }
37
+ async function verifyReachable(url, timeoutMs = 3000) {
38
+ const controller = new AbortController();
39
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
40
+ try {
41
+ await fetch(`${url}/health`, { signal: controller.signal });
42
+ return true;
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ finally {
48
+ clearTimeout(timer);
49
+ }
50
+ }
51
+ /**
52
+ * Detect the best server region and return its URL.
53
+ *
54
+ * Strategy: timezone as primary signal, IP geolocation as supplement.
55
+ * - Chinese timezone → cn immediately (covers servers with UTC too via IP fallback).
56
+ * - Non-Chinese timezone → try IP to detect if physically in China.
57
+ */
58
+ export async function detectAndChooseServer() {
59
+ console.log(chalk.gray(' Detecting best server...'));
60
+ const tzRegion = inferRegionByTimezone();
61
+ if (tzRegion === 'cn') {
62
+ const reachable = await verifyReachable(SERVERS.cn.url);
63
+ if (reachable) {
64
+ console.log(chalk.gray(` Using ${SERVERS.cn.label} (timezone: China)`));
65
+ return SERVERS.cn.url;
66
+ }
67
+ const fallback = await verifyReachable(SERVERS.global.url);
68
+ if (fallback) {
69
+ console.log(chalk.gray(` Using ${SERVERS.global.label} (cn unreachable, fallback)`));
70
+ return SERVERS.global.url;
71
+ }
72
+ console.error(chalk.red(' Error: cannot reach any server.'));
73
+ process.exit(1);
74
+ }
75
+ // Non-CN timezone: check IP to catch servers in China with UTC timezone
76
+ const ipRegion = await detectRegionByIP();
77
+ const region = ipRegion ?? tzRegion;
78
+ const reachable = await verifyReachable(SERVERS[region].url);
79
+ if (reachable) {
80
+ const source = ipRegion ? 'IP geolocation' : 'timezone';
81
+ console.log(chalk.gray(` Using ${SERVERS[region].label} (${source})`));
82
+ return SERVERS[region].url;
83
+ }
84
+ // Fallback to the other server
85
+ const other = region === 'cn' ? 'global' : 'cn';
86
+ const fallback = await verifyReachable(SERVERS[other].url);
87
+ if (fallback) {
88
+ console.log(chalk.gray(` Using ${SERVERS[other].label} (fallback)`));
89
+ return SERVERS[other].url;
90
+ }
91
+ console.error(chalk.red(' Error: cannot reach any server.'));
92
+ process.exit(1);
93
+ }
94
+ export function urlToRegion(url) {
95
+ return url.includes('shennian.net') ? 'cn' : 'global';
96
+ }
97
+ export function regionToUrl(region) {
98
+ return SERVERS[region].url;
99
+ }
@@ -0,0 +1,62 @@
1
+ import type { ReqFrame, ResFrame, EventFrame } from '@shennian/wire';
2
+ export type CliRelayOptions = {
3
+ serverUrl: string;
4
+ machineToken: string;
5
+ cliVersion?: string;
6
+ agentList?: string[];
7
+ onReq?: (req: ReqFrame) => void;
8
+ onConnected?: () => void;
9
+ onDisconnected?: (info: CliRelayDisconnectInfo) => void;
10
+ };
11
+ export type CliRelayState = 'connecting' | 'connected' | 'disconnected';
12
+ export type CliRelayDisconnectInfo = {
13
+ code?: number;
14
+ reason?: string;
15
+ error?: string;
16
+ phase: 'connecting' | 'connected' | 'disconnected';
17
+ trigger?: 'connect-timeout' | 'pong-timeout' | 'heartbeat-timeout' | 'socket-error' | 'socket-close';
18
+ reconnectAttempt: number;
19
+ };
20
+ export declare class CliRelayClient {
21
+ private options;
22
+ private ws;
23
+ private state;
24
+ private reconnectAttempts;
25
+ private heartbeatTimer;
26
+ private reconnectTimer;
27
+ private connectTimer;
28
+ private pingTimer;
29
+ private pongTimer;
30
+ private disposed;
31
+ private closeTrigger;
32
+ private lastSocketError;
33
+ /** Buffered agent events awaiting server ack, keyed by event id */
34
+ private sendBuffer;
35
+ private pendingAcks;
36
+ constructor(options: CliRelayOptions);
37
+ connect(): void;
38
+ disconnect(): void;
39
+ sendRes(res: ResFrame): void;
40
+ sendEvent(event: EventFrame): void;
41
+ sendBufferedEvent(event: EventFrame, timeoutMs?: number): Promise<void>;
42
+ /**
43
+ * Send an agent event with at-least-once delivery guarantee.
44
+ * The event is buffered until the server acknowledges it.
45
+ * On reconnect, all unacked events are resent in order.
46
+ */
47
+ sendAgentEvent(event: EventFrame): void;
48
+ getState(): CliRelayState;
49
+ private handleFrame;
50
+ /** Resend all buffered agent events after reconnect, in insertion order */
51
+ private flushSendBuffer;
52
+ private bufferEvent;
53
+ private clearConnectTimer;
54
+ private startPing;
55
+ private clearPing;
56
+ private clearPongTimer;
57
+ private startHeartbeat;
58
+ private resetHeartbeat;
59
+ private clearHeartbeat;
60
+ private scheduleReconnect;
61
+ private cleanup;
62
+ }