shennian 0.2.88 → 0.2.90

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 (143) hide show
  1. package/dist/assets/wechat-channel/macos/manifest.json +22 -0
  2. package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
  3. package/dist/bin/shennian.js +1 -1
  4. package/dist/publish-build-manifest.json +548 -0
  5. package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
  6. package/dist/src/agent-env.js +4 -105
  7. package/dist/src/agents/adapter.d.ts +6 -0
  8. package/dist/src/agents/adapter.js +1 -19
  9. package/dist/src/agents/claude.js +8 -305
  10. package/dist/src/agents/codex-control.d.ts +35 -0
  11. package/dist/src/agents/codex-control.js +2 -0
  12. package/dist/src/agents/codex-utils.js +7 -200
  13. package/dist/src/agents/codex.d.ts +8 -0
  14. package/dist/src/agents/codex.js +15 -863
  15. package/dist/src/agents/command-spec.js +2 -413
  16. package/dist/src/agents/config-status.js +1 -226
  17. package/dist/src/agents/cursor.js +1 -249
  18. package/dist/src/agents/custom.js +4 -271
  19. package/dist/src/agents/detect.js +1 -56
  20. package/dist/src/agents/external-channel-instructions.js +10 -94
  21. package/dist/src/agents/gemini.js +1 -173
  22. package/dist/src/agents/manager.js +13 -157
  23. package/dist/src/agents/model-registry/cache.js +1 -37
  24. package/dist/src/agents/model-registry/discovery.js +2 -187
  25. package/dist/src/agents/model-registry/parsers.js +4 -447
  26. package/dist/src/agents/model-registry/runner.js +1 -30
  27. package/dist/src/agents/model-registry/service.js +1 -78
  28. package/dist/src/agents/model-registry/types.js +1 -8
  29. package/dist/src/agents/model-registry.js +1 -18
  30. package/dist/src/agents/openclaw.js +2 -275
  31. package/dist/src/agents/opencode.js +1 -231
  32. package/dist/src/agents/pi-context.js +12 -217
  33. package/dist/src/agents/pi.js +14 -723
  34. package/dist/src/agents/platform-instructions.js +9 -54
  35. package/dist/src/channels/base.d.ts +4 -1
  36. package/dist/src/channels/base.js +1 -3
  37. package/dist/src/channels/registry.js +1 -30
  38. package/dist/src/channels/reply-split.js +10 -89
  39. package/dist/src/channels/runtime.d.ts +1 -0
  40. package/dist/src/channels/runtime.js +5 -533
  41. package/dist/src/channels/secret-registry.d.ts +1 -0
  42. package/dist/src/channels/secret-registry.js +1 -46
  43. package/dist/src/channels/websocket.js +8 -378
  44. package/dist/src/channels/wechat-channel/anchor.d.ts +10 -0
  45. package/dist/src/channels/wechat-channel/anchor.js +1 -0
  46. package/dist/src/channels/wechat-channel/client.d.ts +74 -0
  47. package/dist/src/channels/wechat-channel/client.js +1 -0
  48. package/dist/src/channels/wechat-channel/cooldown.d.ts +15 -0
  49. package/dist/src/channels/wechat-channel/cooldown.js +1 -0
  50. package/dist/src/channels/wechat-channel/fingerprint.d.ts +28 -0
  51. package/dist/src/channels/wechat-channel/fingerprint.js +1 -0
  52. package/dist/src/channels/wechat-channel/helper-assets.d.ts +37 -0
  53. package/dist/src/channels/wechat-channel/helper-assets.js +1 -0
  54. package/dist/src/channels/wechat-channel/helper-client.d.ts +25 -0
  55. package/dist/src/channels/wechat-channel/helper-client.js +3 -0
  56. package/dist/src/channels/wechat-channel/helper-protocol.d.ts +84 -0
  57. package/dist/src/channels/wechat-channel/helper-protocol.js +1 -0
  58. package/dist/src/channels/wechat-channel/index.d.ts +17 -0
  59. package/dist/src/channels/wechat-channel/index.js +1 -0
  60. package/dist/src/channels/wechat-channel/ledger.d.ts +33 -0
  61. package/dist/src/channels/wechat-channel/ledger.js +1 -0
  62. package/dist/src/channels/wechat-channel/media-resolver.d.ts +32 -0
  63. package/dist/src/channels/wechat-channel/media-resolver.js +1 -0
  64. package/dist/src/channels/wechat-channel/message-key.d.ts +19 -0
  65. package/dist/src/channels/wechat-channel/message-key.js +1 -0
  66. package/dist/src/channels/wechat-channel/observer.d.ts +64 -0
  67. package/dist/src/channels/wechat-channel/observer.js +1 -0
  68. package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +69 -0
  69. package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -0
  70. package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
  71. package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
  72. package/dist/src/channels/wechat-channel/preflight.d.ts +37 -0
  73. package/dist/src/channels/wechat-channel/preflight.js +1 -0
  74. package/dist/src/channels/wechat-channel/runner.d.ts +34 -0
  75. package/dist/src/channels/wechat-channel/runner.js +1 -0
  76. package/dist/src/channels/wechat-channel/runtime.d.ts +45 -0
  77. package/dist/src/channels/wechat-channel/runtime.js +1 -0
  78. package/dist/src/channels/wechat-channel/scheduler.d.ts +35 -0
  79. package/dist/src/channels/wechat-channel/scheduler.js +1 -0
  80. package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
  81. package/dist/src/channels/wechat-rpa/macos.js +6 -48
  82. package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
  83. package/dist/src/channels/wechat-rpa.d.ts +21 -0
  84. package/dist/src/channels/wechat-rpa.js +6 -1022
  85. package/dist/src/channels/wecom.js +4 -357
  86. package/dist/src/commands/agent.js +6 -131
  87. package/dist/src/commands/daemon-windows.js +8 -48
  88. package/dist/src/commands/daemon.js +19 -1013
  89. package/dist/src/commands/external-attachments.js +1 -51
  90. package/dist/src/commands/external.js +1 -137
  91. package/dist/src/commands/manager.js +2 -389
  92. package/dist/src/commands/pair-qr.js +1 -6
  93. package/dist/src/commands/pair.js +9 -287
  94. package/dist/src/commands/tools.js +1 -34
  95. package/dist/src/commands/upgrade.js +1 -198
  96. package/dist/src/config/index.js +1 -35
  97. package/dist/src/daemon-log.js +6 -58
  98. package/dist/src/env-path.js +1 -64
  99. package/dist/src/fs/boundary.js +1 -126
  100. package/dist/src/fs/handler.js +1 -130
  101. package/dist/src/fs/security.js +1 -32
  102. package/dist/src/fs/text-decoder.d.ts +10 -0
  103. package/dist/src/fs/text-decoder.js +1 -0
  104. package/dist/src/index.js +2 -404
  105. package/dist/src/log-reporter.js +1 -16
  106. package/dist/src/manager/prompt.js +29 -34
  107. package/dist/src/manager/registry.js +2 -269
  108. package/dist/src/manager/runtime.js +19 -1003
  109. package/dist/src/native-fusion/config.js +1 -5
  110. package/dist/src/native-fusion/opencode-parser.js +3 -123
  111. package/dist/src/native-fusion/parser-common.js +8 -264
  112. package/dist/src/native-fusion/parsers.js +8 -729
  113. package/dist/src/native-fusion/service.d.ts +10 -0
  114. package/dist/src/native-fusion/service.js +2 -198
  115. package/dist/src/native-fusion/state.js +1 -22
  116. package/dist/src/native-fusion/types.js +1 -1
  117. package/dist/src/region.js +1 -88
  118. package/dist/src/relay/client.js +1 -343
  119. package/dist/src/session/archive-zip.js +1 -220
  120. package/dist/src/session/handlers/agent-config.js +1 -150
  121. package/dist/src/session/handlers/agents.js +1 -55
  122. package/dist/src/session/handlers/chat.js +2 -733
  123. package/dist/src/session/handlers/control.js +1 -55
  124. package/dist/src/session/handlers/fs.js +1 -747
  125. package/dist/src/session/handlers/session-refresh.js +1 -35
  126. package/dist/src/session/handlers/skills.js +1 -121
  127. package/dist/src/session/handlers/title.js +1 -60
  128. package/dist/src/session/handlers/tool-detail.d.ts +3 -0
  129. package/dist/src/session/handlers/tool-detail.js +1 -0
  130. package/dist/src/session/manager.d.ts +3 -0
  131. package/dist/src/session/manager.js +1 -261
  132. package/dist/src/session/projection.js +1 -54
  133. package/dist/src/session/queue.js +4 -317
  134. package/dist/src/session/remote-attachments.js +1 -72
  135. package/dist/src/session/store.js +3 -109
  136. package/dist/src/session/types.d.ts +4 -0
  137. package/dist/src/session/types.js +1 -4
  138. package/dist/src/skills/registry.js +15 -148
  139. package/dist/src/skills/setup.js +1 -101
  140. package/dist/src/tools/markdown-to-pdf.js +10 -346
  141. package/dist/src/upgrade/engine.js +3 -347
  142. package/package.json +3 -2
  143. package/dist/scripts/wechat-rpa-download-candidates.mjs +0 -105
@@ -1,287 +1,9 @@
1
- import chalk from 'chalk';
2
- import os from 'node:os';
3
- import readline from 'node:readline';
4
- import { loadConfig, saveConfig } from '../config/index.js';
5
- import { detectAgents } from '../agents/detect.js';
6
- import { clearRemoteAccessDisabled, startDaemonProcess, installService, saveEnvSnapshot, } from './daemon.js';
7
- import { detectAndChooseServer } from '../region.js';
8
- import { buildPairQrPayload, PAIR_QR_RENDER_OPTIONS } from './pair-qr.js';
9
- const POLL_INTERVAL_MS = 3000;
10
- const POLL_TIMEOUT_MS = 10 * 60 * 1000;
11
- function emitPairJson(event) {
12
- process.stdout.write(`${JSON.stringify(event)}\n`);
13
- }
14
- function failPairJson(code, message) {
15
- emitPairJson({ type: 'error', code, message });
16
- process.exit(1);
17
- }
18
- function renderTokenBox(token) {
19
- const line = '─'.repeat(token.length + 4);
20
- console.log(chalk.cyan(`┌${line}┐`));
21
- console.log(chalk.cyan('│ ') + chalk.bold.yellow(token) + chalk.cyan(' │'));
22
- console.log(chalk.cyan(`└${line}┘`));
23
- }
24
- async function renderQR(url) {
25
- const qrcode = (await import('qrcode-terminal')).default;
26
- return new Promise((resolve) => {
27
- qrcode.generate(url, PAIR_QR_RENDER_OPTIONS, (qr) => {
28
- console.log(qr);
29
- resolve();
30
- });
31
- });
32
- }
33
- async function pollStatus(serverUrl, pairToken, opts = {}) {
34
- const deadline = Date.now() + POLL_TIMEOUT_MS;
35
- if (opts.json)
36
- emitPairJson({ type: 'pair.waiting' });
37
- while (Date.now() < deadline) {
38
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
39
- let res;
40
- try {
41
- res = await fetch(`${serverUrl}/api/machines/pair/status?token=${encodeURIComponent(pairToken)}`);
42
- }
43
- catch {
44
- continue;
45
- }
46
- if (res.status === 410) {
47
- if (opts.json)
48
- failPairJson('PAIR_EXPIRED', 'Pairing code expired');
49
- else {
50
- console.error(chalk.red('\n✗ Pairing code expired. Please re-run: shennian pair'));
51
- process.exit(1);
52
- }
53
- }
54
- if (!res.ok)
55
- continue;
56
- const data = (await res.json());
57
- if (data.status === 'claimed' && data.machineToken && data.machineId) {
58
- if (opts.json)
59
- emitPairJson({ type: 'pair.claimed' });
60
- return {
61
- machineToken: data.machineToken,
62
- machineId: data.machineId,
63
- machineName: data.machineName ?? '',
64
- };
65
- }
66
- }
67
- return null;
68
- }
69
- function askConfirm(question) {
70
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
71
- return new Promise((resolve) => {
72
- rl.question(question, (ans) => {
73
- rl.close();
74
- resolve(ans.trim().toLowerCase() === 'y');
75
- });
76
- });
77
- }
78
- export async function runPairFlow(opts) {
79
- const { serverUrl, machineName, force, json } = opts;
80
- const config = loadConfig();
81
- if (config.machineToken && config.machineId && !force) {
82
- if (json)
83
- emitPairJson({ type: 'done', status: 'already-paired', machineId: config.machineId });
84
- else {
85
- console.log(chalk.green(`✓ Already paired (machine ID: ${config.machineId})`));
86
- console.log(chalk.gray(' Run "shennian pair" to re-pair to another account'));
87
- }
88
- return;
89
- }
90
- if (config.machineToken && !force)
91
- return;
92
- const detectedAgents = detectAgents();
93
- const agentList = detectedAgents.map((a) => a.type);
94
- const platform = os.platform();
95
- const osName = platform === 'win32' ? 'windows' : platform === 'darwin' ? 'macos' : 'linux';
96
- if (!json) {
97
- console.log(chalk.bold('\nShennian CLI — Pairing\n'));
98
- console.log(chalk.gray(` Machine: ${machineName} | OS: ${osName} | Agents: ${agentList.join(', ') || 'none'}`));
99
- console.log();
100
- }
101
- let res;
102
- try {
103
- res = await fetch(`${serverUrl}/api/machines/pair/create`, {
104
- method: 'POST',
105
- headers: { 'Content-Type': 'application/json' },
106
- body: JSON.stringify({ name: machineName, os: osName, agentList }),
107
- });
108
- }
109
- catch {
110
- const message = `Network error: cannot connect to ${serverUrl}`;
111
- if (json)
112
- failPairJson('NETWORK_ERROR', message);
113
- console.error(chalk.red(`✗ ${message}`));
114
- process.exit(1);
115
- }
116
- if (!res.ok) {
117
- const body = await res.json().catch(() => ({}));
118
- const message = `Failed to create machine: ${body.error ?? res.statusText}`;
119
- if (json)
120
- failPairJson('PAIR_CREATE_FAILED', message);
121
- console.error(chalk.red(`✗ ${message}`));
122
- process.exit(1);
123
- }
124
- const data = (await res.json());
125
- config.machineId = data.machineId;
126
- config.serverUrl = serverUrl;
127
- saveConfig(config);
128
- const pairUrl = buildPairQrPayload(data.pairToken);
129
- if (json) {
130
- emitPairJson({
131
- type: 'pair.created',
132
- code: data.pairToken,
133
- machineId: data.machineId,
134
- expiresAt: new Date(Date.now() + POLL_TIMEOUT_MS).toISOString(),
135
- });
136
- }
137
- else {
138
- console.log(chalk.white('Scan QR code or enter the pairing code in the Shennian app:'));
139
- console.log();
140
- await renderQR(pairUrl);
141
- renderTokenBox(data.pairToken);
142
- console.log();
143
- console.log(chalk.gray('Waiting for pairing... (Ctrl+C to cancel)'));
144
- }
145
- const result = await pollStatus(serverUrl, data.pairToken, { json });
146
- if (!result) {
147
- if (json)
148
- failPairJson('PAIR_TIMEOUT', 'Timed out waiting for pairing');
149
- console.error(chalk.red('\n✗ Timed out. Pairing code expired, please try again.'));
150
- process.exit(1);
151
- }
152
- config.machineToken = result.machineToken;
153
- config.machineId = result.machineId;
154
- saveConfig(config);
155
- if (json)
156
- emitPairJson({ type: 'pair.saved', machineId: result.machineId });
157
- if (!json) {
158
- console.log(chalk.green(`\n✓ Paired successfully! Machine "${result.machineName}" is now linked.`));
159
- }
160
- }
161
- export async function registerDesktopMachine(opts) {
162
- const config = loadConfig();
163
- const detectedAgents = detectAgents();
164
- const agentList = detectedAgents.map((a) => a.type);
165
- const platform = os.platform();
166
- const osName = platform === 'win32' ? 'windows' : platform === 'darwin' ? 'macos' : 'linux';
167
- const res = await fetch(`${opts.serverUrl}/api/machines/desktop/register`, {
168
- method: 'POST',
169
- headers: {
170
- 'Content-Type': 'application/json',
171
- Authorization: `Bearer ${opts.authToken}`,
172
- },
173
- body: JSON.stringify({
174
- machineId: config.machineId,
175
- name: opts.machineName,
176
- os: osName,
177
- agentList,
178
- }),
179
- });
180
- if (!res.ok) {
181
- const body = await res.json().catch(() => ({}));
182
- const message = body.error ?? res.statusText;
183
- throw new Error(`Failed to register desktop machine: ${message}`);
184
- }
185
- const data = (await res.json());
186
- if (!data.machine?.id || !data.machineToken) {
187
- throw new Error('Failed to register desktop machine: missing machine token');
188
- }
189
- config.machineId = data.machine.id;
190
- config.machineToken = data.machineToken;
191
- config.serverUrl = opts.serverUrl;
192
- saveConfig(config);
193
- return { machineId: data.machine.id, machineName: data.machine.name };
194
- }
195
- async function resolveDesktopRegisterServerUrl(api) {
196
- const explicit = api?.trim();
197
- if (explicit)
198
- return explicit;
199
- const envUrl = process.env.SHENNIAN_DESKTOP_SERVER_URL?.trim();
200
- if (envUrl)
201
- return envUrl;
202
- const configUrl = loadConfig().serverUrl?.trim();
203
- if (configUrl)
204
- return configUrl;
205
- return detectAndChooseServer();
206
- }
207
- export async function runSmartStart(serverUrl, machineName) {
208
- const config = loadConfig();
209
- if (!config.machineToken) {
210
- const resolvedUrl = serverUrl ?? (await detectAndChooseServer());
211
- await runPairFlow({ serverUrl: resolvedUrl, machineName, force: false });
212
- }
213
- else {
214
- console.log(chalk.green(`✓ Already paired (machine ID: ${config.machineId})`));
215
- }
216
- saveEnvSnapshot();
217
- clearRemoteAccessDisabled();
218
- console.log(chalk.gray('\nStarting background service...'));
219
- const startedByService = installService();
220
- if (!startedByService) {
221
- startDaemonProcess();
222
- }
223
- }
224
- export function registerPairCommand(program) {
225
- program
226
- .command('desktop-register', { hidden: true })
227
- .description('Register this desktop-managed machine to the authenticated account')
228
- .option('--api <url>', 'Server URL')
229
- .option('--name <name>', 'Machine name', os.hostname())
230
- .action(async (opts) => {
231
- try {
232
- const token = process.env.SHENNIAN_DESKTOP_AUTH_TOKEN;
233
- if (!token)
234
- throw new Error('Missing desktop auth token');
235
- const serverUrl = await resolveDesktopRegisterServerUrl(opts.api);
236
- const result = await registerDesktopMachine({
237
- serverUrl,
238
- authToken: token,
239
- machineName: opts.name,
240
- });
241
- console.log(JSON.stringify({ ok: true, ...result }));
242
- }
243
- catch (error) {
244
- console.error(error instanceof Error ? error.message : String(error));
245
- process.exit(1);
246
- }
247
- });
248
- program
249
- .command('pair')
250
- .description('Pair this machine to a Shennian account (re-pair if already paired)')
251
- .option('--api <url>', 'Server URL override')
252
- .option('--server <url>', 'Server URL override')
253
- .option('--name <name>', 'Machine name', os.hostname())
254
- .option('--json', 'Emit machine-readable JSONL events')
255
- .action(async (opts) => {
256
- const config = loadConfig();
257
- if (config.machineToken) {
258
- if (opts.json) {
259
- emitPairJson({ type: 'done', status: 'already-paired', machineId: config.machineId });
260
- return;
261
- }
262
- console.log(chalk.yellow(`Currently paired (machine ID: ${config.machineId})`));
263
- const yes = await askConfirm('Re-pairing will unlink the current account. Continue? (y/N) ');
264
- if (!yes) {
265
- console.log(chalk.gray('Cancelled'));
266
- return;
267
- }
268
- delete config.machineToken;
269
- delete config.machineId;
270
- saveConfig(config);
271
- }
272
- const serverUrl = opts.server ?? opts.api ?? (await detectAndChooseServer());
273
- await runPairFlow({ serverUrl, machineName: opts.name, force: true, json: Boolean(opts.json) });
274
- saveEnvSnapshot();
275
- clearRemoteAccessDisabled();
276
- if (opts.json)
277
- emitPairJson({ type: 'daemon.starting' });
278
- else
279
- console.log(chalk.gray('\nStarting background service...'));
280
- const startedByService = installService();
281
- if (!startedByService) {
282
- startDaemonProcess({ quiet: Boolean(opts.json) });
283
- }
284
- if (opts.json)
285
- emitPairJson({ type: 'done', status: 'paired', machineId: loadConfig().machineId });
286
- });
287
- }
1
+ import r from"chalk";import y from"node:os";import x from"node:readline";import{loadConfig as l,saveConfig as w}from"../config/index.js";import{detectAgents as S}from"../agents/detect.js";import{clearRemoteAccessDisabled as v,startDaemonProcess as P,installService as N,saveEnvSnapshot as R}from"./daemon.js";import{detectAndChooseServer as k}from"../region.js";import{buildPairQrPayload as O,PAIR_QR_RENDER_OPTIONS as $}from"./pair-qr.js";const j=3e3,E=600*1e3;function s(o){process.stdout.write(`${JSON.stringify(o)}
2
+ `)}function I(o,e){s({type:"error",code:o,message:e}),process.exit(1)}function _(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 C(o){const e=(await import("qrcode-terminal")).default;return new Promise(n=>{e.generate(o,$,t=>{console.log(t),n()})})}async function D(o,e,n={}){const t=Date.now()+E;for(n.json&&s({type:"pair.waiting"});Date.now()<t;){await new Promise(m=>setTimeout(m,j));let i;try{i=await fetch(`${o}/api/machines/pair/status?token=${encodeURIComponent(e)}`)}catch{continue}if(i.status===410&&(n.json?I("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&&s({type:"pair.claimed"}),{machineToken:a.machineToken,machineId:a.machineId,machineName:a.machineName??""}}return null}function L(o){const e=x.createInterface({input:process.stdin,output:process.stdout});return new Promise(n=>{e.question(o,t=>{e.close(),n(t.trim().toLowerCase()==="y")})})}async function A(o){const{serverUrl:e,machineName:n,force:t,json:i}=o,a=l();if(a.machineToken&&a.machineId&&!t){i?s({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}if(a.machineToken&&!t)return;const c=S().map(f=>f.type),h=y.platform(),u=h==="win32"?"windows":h==="darwin"?"macos":"linux";i||(console.log(r.bold(`
4
+ Shennian CLI \u2014 Pairing
5
+ `)),console.log(r.gray(` Machine: ${n} | OS: ${u} | Agents: ${c.join(", ")||"none"}`)),console.log());let p;try{p=await fetch(`${e}/api/machines/pair/create`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,os:u,agentList:c})})}catch{const f=`Network error: cannot connect to ${e}`;i&&I("NETWORK_ERROR",f),console.error(r.red(`\u2717 ${f}`)),process.exit(1)}if(!p.ok){const T=`Failed to create machine: ${(await p.json().catch(()=>({}))).error??p.statusText}`;i&&I("PAIR_CREATE_FAILED",T),console.error(r.red(`\u2717 ${T}`)),process.exit(1)}const d=await p.json();a.machineId=d.machineId,a.serverUrl=e,w(a);const U=O(d.pairToken);i?s({type:"pair.created",code:d.pairToken,machineId:d.machineId,expiresAt:new Date(Date.now()+E).toISOString()}):(console.log(r.white("Scan QR code or enter the pairing code in the Shennian app:")),console.log(),await C(U),_(d.pairToken),console.log(),console.log(r.gray("Waiting for pairing... (Ctrl+C to cancel)")));const g=await D(e,d.pairToken,{json:i});g||(i&&I("PAIR_TIMEOUT","Timed out waiting for pairing"),console.error(r.red(`
6
+ \u2717 Timed out. Pairing code expired, please try again.`)),process.exit(1)),a.machineToken=g.machineToken,a.machineId=g.machineId,w(a),i&&s({type:"pair.saved",machineId:g.machineId}),i||console.log(r.green(`
7
+ \u2713 Paired successfully! Machine "${g.machineName}" is now linked.`))}async function b(o){const e=l(),t=S().map(h=>h.type),i=y.platform(),a=i==="win32"?"windows":i==="darwin"?"macos":"linux",m=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(!m.ok){const u=(await m.json().catch(()=>({}))).error??m.statusText;throw new Error(`Failed to register desktop machine: ${u}`)}const c=await m.json();if(!c.machine?.id||!c.machineToken)throw new Error("Failed to register desktop machine: missing machine token");return e.machineId=c.machine.id,e.machineToken=c.machineToken,e.serverUrl=o.serverUrl,w(e),{machineId:c.machine.id,machineName:c.machine.name}}async function M(o){const e=o?.trim();if(e)return e;const n=process.env.SHENNIAN_DESKTOP_SERVER_URL?.trim();if(n)return n;const t=l().serverUrl?.trim();return t||k()}async function W(o,e){const n=l();if(n.machineToken)console.log(r.green(`\u2713 Already paired (machine ID: ${n.machineId})`));else{const i=o??await k();await A({serverUrl:i,machineName:e,force:!1})}R(),v(),console.log(r.gray(`
8
+ Starting background service...`)),N()||P()}function z(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",y.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 M(e.api),i=await b({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",y.hostname()).option("--json","Emit machine-readable JSONL events").action(async e=>{const n=l();if(n.machineToken){if(e.json){s({type:"done",status:"already-paired",machineId:n.machineId});return}if(console.log(r.yellow(`Currently paired (machine ID: ${n.machineId})`)),!await L("Re-pairing will unlink the current account. Continue? (y/N) ")){console.log(r.gray("Cancelled"));return}delete n.machineToken,delete n.machineId,w(n)}const t=e.server??e.api??await k();await A({serverUrl:t,machineName:e.name,force:!0,json:!!e.json}),R(),v(),e.json?s({type:"daemon.starting"}):console.log(r.gray(`
9
+ Starting background service...`)),N()||P({quiet:!!e.json}),e.json&&s({type:"done",status:"paired",machineId:l().machineId})})}export{b as registerDesktopMachine,z as registerPairCommand,A as runPairFlow,W as runSmartStart};
@@ -1,34 +1 @@
1
- // @arch docs/features/markdown-pdf-export.md
2
- // @test src/__tests__/markdown-to-pdf.test.ts
3
- import chalk from 'chalk';
4
- import { convertMarkdownToPdf, defaultPdfOutputPath } from '../tools/markdown-to-pdf.js';
5
- export function registerToolsCommand(program) {
6
- const tools = program.command('tools').description('Local deterministic Shennian tools');
7
- tools
8
- .command('markdown-to-pdf')
9
- .alias('md-to-pdf')
10
- .description('Convert a local Markdown file to PDF on this machine')
11
- .argument('<input>', 'Markdown file path (.md or .mdx)')
12
- .option('-o, --output <path>', 'PDF output path; defaults to the same folder and basename')
13
- .option('--title <title>', 'PDF document title')
14
- .option('--keep-html', 'Keep the intermediate HTML file next to the PDF')
15
- .option('--chrome <path>', 'Chrome/Edge/Chromium executable path')
16
- .action(async (input, opts) => {
17
- try {
18
- const outputPath = opts.output || defaultPdfOutputPath(input);
19
- const result = await convertMarkdownToPdf(input, {
20
- outputPath,
21
- title: opts.title,
22
- keepHtml: opts.keepHtml,
23
- chromePath: opts.chrome,
24
- });
25
- console.log(chalk.green('✓ PDF exported'));
26
- console.log(result.outputPath);
27
- }
28
- catch (error) {
29
- console.error(chalk.red('✗ Markdown to PDF failed'));
30
- console.error(error instanceof Error ? error.message : String(error));
31
- process.exit(1);
32
- }
33
- });
34
- }
1
+ import r from"chalk";import{convertMarkdownToPdf as i,defaultPdfOutputPath as l}from"../tools/markdown-to-pdf.js";function p(n){n.command("tools").description("Local deterministic Shennian tools").command("markdown-to-pdf").alias("md-to-pdf").description("Convert a local Markdown file to PDF on this machine").argument("<input>","Markdown file path (.md or .mdx)").option("-o, --output <path>","PDF output path; defaults to the same folder and basename").option("--title <title>","PDF document title").option("--keep-html","Keep the intermediate HTML file next to the PDF").option("--chrome <path>","Chrome/Edge/Chromium executable path").action(async(e,o)=>{try{const t=o.output||l(e),a=await i(e,{outputPath:t,title:o.title,keepHtml:o.keepHtml,chromePath:o.chrome});console.log(r.green("\u2713 PDF exported")),console.log(a.outputPath)}catch(t){console.error(r.red("\u2717 Markdown to PDF failed")),console.error(t instanceof Error?t.message:String(t)),process.exit(1)}})}export{p as registerToolsCommand};
@@ -1,198 +1 @@
1
- // @arch docs/architecture/cli/upgrade.md
2
- // @test packages/cli/src/__tests__/upgrade-command.test.ts
3
- import chalk from 'chalk';
4
- import { execSync, spawn } from 'node:child_process';
5
- import os from 'node:os';
6
- import { getCurrentVersion, fetchLatestVersion, compareVersions, performUpgrade, checkForUpdate, recordUpgradeFailure, } from '../upgrade/engine.js';
7
- const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
8
- export function restartCurrentDaemonAfterUpgrade(deps = {}) {
9
- const platform = deps.platform ?? os.platform();
10
- const kill = deps.kill ?? process.kill;
11
- const currentPid = deps.pid ?? process.pid;
12
- const env = deps.env ?? process.env;
13
- const spawnFn = deps.spawn ?? spawn;
14
- const execSyncFn = deps.execSync ?? execSync;
15
- if (platform === 'win32') {
16
- const command = env.ComSpec || 'cmd.exe';
17
- try {
18
- const child = spawnFn(command, [
19
- '/d',
20
- '/s',
21
- '/c',
22
- 'timeout /t 2 /nobreak >nul & schtasks /Run /TN ShennianAgent >nul',
23
- ], {
24
- detached: true,
25
- stdio: 'ignore',
26
- windowsHide: true,
27
- });
28
- child.unref();
29
- kill(currentPid, 'SIGTERM');
30
- return;
31
- }
32
- catch {
33
- try {
34
- execSyncFn('schtasks /run /tn "ShennianAgent"', { stdio: 'pipe', windowsHide: true });
35
- }
36
- catch {
37
- // Fall through to process exit; manual start/status still works.
38
- }
39
- }
40
- }
41
- else {
42
- try {
43
- const child = spawnFn('sh', ['-c', 'sleep 2; shennian start >/dev/null 2>&1'], {
44
- detached: true,
45
- stdio: 'ignore',
46
- env,
47
- });
48
- child.unref();
49
- }
50
- catch {
51
- // launchd/systemd may still restart this process; otherwise manual start/status still works.
52
- }
53
- }
54
- kill(currentPid, 'SIGTERM');
55
- }
56
- // ─── Interactive CLI upgrade command ─────────────────────────────────────────
57
- export function registerUpgradeCommand(program) {
58
- program
59
- .command('upgrade')
60
- .description('Upgrade shennian CLI to the latest version')
61
- .option('--check', 'Only check for updates, do not install')
62
- .option('--to <version>', 'Upgrade to a specific version')
63
- .action(async (opts) => {
64
- const current = getCurrentVersion();
65
- console.log(chalk.gray(`Current version: ${current}`));
66
- console.log();
67
- let spinIdx = 0;
68
- const spinTimer = setInterval(() => {
69
- process.stdout.write(`\r ${SPINNER[spinIdx % SPINNER.length]} Checking for updates...`);
70
- spinIdx++;
71
- }, 80);
72
- let latest;
73
- try {
74
- latest = opts.to ?? await fetchLatestVersion();
75
- clearInterval(spinTimer);
76
- process.stdout.write('\r' + ' '.repeat(40) + '\r');
77
- }
78
- catch (err) {
79
- clearInterval(spinTimer);
80
- process.stdout.write('\r' + ' '.repeat(40) + '\r');
81
- console.error(chalk.red(`✗ Failed to check for updates: ${err instanceof Error ? err.message : String(err)}`));
82
- process.exit(1);
83
- }
84
- const changeType = compareVersions(current, latest);
85
- if (changeType === 'none' && !opts.to) {
86
- console.log(chalk.green(`✓ Already up to date (${current})`));
87
- return;
88
- }
89
- console.log(` Available: ${chalk.green(latest)} ${changeType === 'major' ? chalk.red('(major)') : changeType === 'minor' ? chalk.yellow('(minor)') : chalk.green('(patch)')}`);
90
- console.log();
91
- if (opts.check)
92
- return;
93
- if (changeType === 'major') {
94
- console.log(chalk.yellow(' ⚠ This is a major version update and may contain breaking changes.'));
95
- console.log(chalk.gray(' Please review the changelog before upgrading.'));
96
- console.log();
97
- }
98
- const result = await performUpgrade(latest, (progress) => {
99
- switch (progress.step) {
100
- case 'checking':
101
- console.log(chalk.gray(' → Checking npm...'));
102
- break;
103
- case 'backing-up':
104
- console.log(chalk.gray(' → Backing up current version...'));
105
- break;
106
- case 'installing':
107
- console.log(chalk.gray(` → Installing ${progress.version}...`));
108
- break;
109
- case 'verifying':
110
- console.log(chalk.gray(` → Verifying ${progress.version}...`));
111
- break;
112
- case 'restarting':
113
- console.log(chalk.gray(` → Restarting daemon...`));
114
- break;
115
- }
116
- });
117
- if (result.ok) {
118
- console.log();
119
- console.log(chalk.green(`✓ Upgraded ${result.from} → ${result.to}`));
120
- console.log(chalk.gray(' Daemon is restarting...'));
121
- restartCurrentDaemonAfterUpgrade();
122
- }
123
- else {
124
- recordUpgradeFailure(latest, result.error);
125
- console.log();
126
- if (result.rolledBack) {
127
- console.error(chalk.red(`✗ Upgrade failed (rolled back to ${current})`));
128
- }
129
- else {
130
- console.error(chalk.red(`✗ Upgrade failed: ${result.error}`));
131
- }
132
- process.exit(1);
133
- }
134
- });
135
- }
136
- // ─── Relay-triggered upgrade (called from session manager) ───────────────────
137
- export async function handleUpgradeStart(client, reqId, targetVersion, opts = {}) {
138
- const sendEvent = (payload) => {
139
- client.sendEvent({ type: 'event', event: 'upgrade', payload });
140
- };
141
- const current = opts.currentVersion ?? getCurrentVersion();
142
- // Resolve target version
143
- let latest;
144
- try {
145
- sendEvent({ state: 'checking', machineId: 'self' });
146
- latest = targetVersion ?? await fetchLatestVersion();
147
- }
148
- catch (err) {
149
- client.sendRes({ type: 'res', id: reqId, ok: false, error: `Version check failed: ${err instanceof Error ? err.message : String(err)}` });
150
- return;
151
- }
152
- const changeType = compareVersions(current, latest);
153
- if (changeType === 'none') {
154
- client.sendRes({ type: 'res', id: reqId, ok: true, payload: { alreadyLatest: true, version: current } });
155
- return;
156
- }
157
- // ACK immediately — upgrade is async
158
- client.sendRes({ type: 'res', id: reqId, ok: true, payload: { from: current, to: latest } });
159
- const result = await performUpgrade(latest, (progress) => {
160
- switch (progress.step) {
161
- case 'backing-up':
162
- sendEvent({ state: 'downloading', machineId: 'self', version: latest });
163
- break;
164
- case 'installing':
165
- sendEvent({ state: 'installing', machineId: 'self', version: latest });
166
- break;
167
- case 'restarting':
168
- sendEvent({ state: 'restarting', machineId: 'self', from: current, to: latest });
169
- break;
170
- }
171
- }, { currentVersion: current });
172
- if (result.ok) {
173
- sendEvent({ state: 'restarting', machineId: 'self', from: result.from, to: result.to });
174
- // Brief delay so the event is flushed before we exit
175
- await new Promise((r) => setTimeout(r, 500));
176
- restartCurrentDaemonAfterUpgrade();
177
- }
178
- else {
179
- const failure = recordUpgradeFailure(latest, result.error);
180
- console.log(`[${new Date().toISOString()}] [upgrade] ${latest} failed, retry after ${new Date(failure.nextRetryAt).toISOString()}: ${result.error}`);
181
- sendEvent({
182
- state: result.rolledBack ? 'rolledback' : 'failed',
183
- machineId: 'self',
184
- ...(result.rolledBack ? { to: current, reason: result.error } : { error: result.error }),
185
- });
186
- }
187
- }
188
- export async function handleUpgradeStatus(client, reqId, opts = {}) {
189
- const result = await checkForUpdate(opts.currentVersion);
190
- client.sendRes({
191
- type: 'res',
192
- id: reqId,
193
- ok: true,
194
- payload: result.hasUpdate
195
- ? { hasUpdate: true, current: result.current, latest: result.latest, changeType: result.changeType }
196
- : { hasUpdate: false, current: opts.currentVersion ?? getCurrentVersion() },
197
- });
198
- }
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 h,checkForUpdate as S,recordUpgradeFailure as f}from"../upgrade/engine.js";const m=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"];function y(n={}){const i=n.platform??v.platform(),a=n.kill??process.kill,s=n.pid??process.pid,c=n.env??process.env,o=n.spawn??w,t=n.execSync??k;if(i==="win32"){const d=c.ComSpec||"cmd.exe";try{o(d,["/d","/s","/c","timeout /t 2 /nobreak >nul & schtasks /Run /TN ShennianAgent >nul"],{detached:!0,stdio:"ignore",windowsHide:!0}).unref(),a(s,"SIGTERM");return}catch{try{t('schtasks /run /tn "ShennianAgent"',{stdio:"pipe",windowsHide:!0})}catch{}}}else try{o("sh",["-c","sleep 2; shennian start >/dev/null 2>&1"],{detached:!0,stdio:"ignore",env:c}).unref()}catch{}a(s,"SIGTERM")}function x(n){n.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").action(async i=>{const a=g();console.log(r.gray(`Current version: ${a}`)),console.log();let s=0;const c=setInterval(()=>{process.stdout.write(`\r ${m[s%m.length]} Checking for updates...`),s++},80);let o;try{o=i.to??await p(),clearInterval(c),process.stdout.write("\r"+" ".repeat(40)+"\r")}catch(e){clearInterval(c),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 t=u(a,o);if(t==="none"&&!i.to){console.log(r.green(`\u2713 Already up to date (${a})`));return}if(console.log(` Available: ${r.green(o)} ${t==="major"?r.red("(major)"):t==="minor"?r.yellow("(minor)"):r.green("(patch)")}`),console.log(),i.check)return;t==="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());const d=await h(o,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()):(f(o,d.error),console.log(),d.rolledBack?console.error(r.red(`\u2717 Upgrade failed (rolled back to ${a})`)):console.error(r.red(`\u2717 Upgrade failed: ${d.error}`)),process.exit(1))})}async function T(n,i,a,s={}){const c=l=>{n.sendEvent({type:"event",event:"upgrade",payload:l})},o=s.currentVersion??g();let t;try{c({state:"checking",machineId:"self"}),t=a??await p()}catch(l){n.sendRes({type:"res",id:i,ok:!1,error:`Version check failed: ${l instanceof Error?l.message:String(l)}`});return}if(u(o,t)==="none"){n.sendRes({type:"res",id:i,ok:!0,payload:{alreadyLatest:!0,version:o}});return}n.sendRes({type:"res",id:i,ok:!0,payload:{from:o,to:t}});const e=await h(t,l=>{switch(l.step){case"backing-up":c({state:"downloading",machineId:"self",version:t});break;case"installing":c({state:"installing",machineId:"self",version:t});break;case"restarting":c({state:"restarting",machineId:"self",from:o,to:t});break}},{currentVersion:o});if(e.ok)c({state:"restarting",machineId:"self",from:e.from,to:e.to}),await new Promise(l=>setTimeout(l,500)),y();else{const l=f(t,e.error);console.log(`[${new Date().toISOString()}] [upgrade] ${t} failed, retry after ${new Date(l.nextRetryAt).toISOString()}: ${e.error}`),c({state:e.rolledBack?"rolledback":"failed",machineId:"self",...e.rolledBack?{to:o,reason:e.error}:{error:e.error}})}}async function R(n,i,a={}){const s=await S(a.currentVersion);n.sendRes({type:"res",id:i,ok:!0,payload:s.hasUpdate?{hasUpdate:!0,current:s.current,latest:s.latest,changeType:s.changeType}:{hasUpdate:!1,current:a.currentVersion??g()}})}export{T as handleUpgradeStart,R as handleUpgradeStatus,x as registerUpgradeCommand,y as restartCurrentDaemonAfterUpgrade};
@@ -1,35 +1 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import os from 'node:os';
4
- export function getShennianDir() {
5
- const customHome = process.env.SHENNIAN_HOME?.trim();
6
- if (customHome)
7
- return path.resolve(customHome);
8
- return path.join(os.homedir(), '.shennian');
9
- }
10
- export function resolveShennianPath(...parts) {
11
- return path.join(getShennianDir(), ...parts);
12
- }
13
- export const configPath = resolveShennianPath('config.json');
14
- export function loadConfig() {
15
- try {
16
- if (!fs.existsSync(configPath))
17
- return {};
18
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
19
- if (config.serverUrl?.includes('shennian.ai')) {
20
- config.serverUrl = 'https://shennian.net';
21
- }
22
- return config;
23
- }
24
- catch {
25
- return {};
26
- }
27
- }
28
- export function saveConfig(config) {
29
- fs.mkdirSync(getShennianDir(), { recursive: true });
30
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
31
- }
32
- export function ensureDir() {
33
- fs.mkdirSync(getShennianDir(), { recursive: true });
34
- fs.mkdirSync(resolveShennianPath('sessions'), { recursive: true });
35
- }
1
+ import r from"node:fs";import e from"node:path";import s from"node:os";function i(){const n=process.env.SHENNIAN_HOME?.trim();return n?e.resolve(n):e.join(s.homedir(),".shennian")}function o(...n){return e.join(i(),...n)}const t=o("config.json");function a(){try{if(!r.existsSync(t))return{};const n=JSON.parse(r.readFileSync(t,"utf-8"));return n.serverUrl?.includes("shennian.ai")&&(n.serverUrl="https://shennian.net"),n}catch{return{}}}function m(n){r.mkdirSync(i(),{recursive:!0}),r.writeFileSync(t,JSON.stringify(n,null,2))}function p(){r.mkdirSync(i(),{recursive:!0}),r.mkdirSync(o("sessions"),{recursive:!0})}export{t as configPath,p as ensureDir,i as getShennianDir,a as loadConfig,o as resolveShennianPath,m as saveConfig};
@@ -1,58 +1,6 @@
1
- import fs from 'node:fs';
2
- import { resolveShennianPath } from './config/index.js';
3
- const DEFAULT_MAX_LINES = 5_000;
4
- const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
5
- const TRIM_INTERVAL_MS = 5 * 60_000;
6
- function readPositiveInt(value, fallback) {
7
- const parsed = Number(value);
8
- return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
9
- }
10
- export function getDaemonLogRetention() {
11
- return {
12
- maxLines: readPositiveInt(process.env.SHENNIAN_DAEMON_LOG_MAX_LINES, DEFAULT_MAX_LINES),
13
- maxBytes: readPositiveInt(process.env.SHENNIAN_DAEMON_LOG_MAX_BYTES, DEFAULT_MAX_BYTES),
14
- };
15
- }
16
- export function trimDaemonLogFile(filePath = resolveShennianPath('daemon.log'), retention = getDaemonLogRetention()) {
17
- try {
18
- const stat = fs.statSync(filePath);
19
- if (stat.size === 0)
20
- return false;
21
- const bytesToRead = Math.min(stat.size, retention.maxBytes);
22
- const fd = fs.openSync(filePath, 'r');
23
- let text = '';
24
- try {
25
- const buffer = Buffer.allocUnsafe(bytesToRead);
26
- fs.readSync(fd, buffer, 0, bytesToRead, stat.size - bytesToRead);
27
- text = buffer.toString('utf8');
28
- }
29
- finally {
30
- fs.closeSync(fd);
31
- }
32
- if (bytesToRead < stat.size) {
33
- const firstNewline = text.indexOf('\n');
34
- text = firstNewline >= 0 ? text.slice(firstNewline + 1) : '';
35
- }
36
- const hadTrailingNewline = text.endsWith('\n');
37
- let lines = text.split('\n');
38
- if (hadTrailingNewline)
39
- lines = lines.slice(0, -1);
40
- if (lines.length > retention.maxLines) {
41
- lines = lines.slice(-retention.maxLines);
42
- }
43
- const next = `${lines.join('\n')}${lines.length > 0 ? '\n' : ''}`;
44
- if (next.length === stat.size && bytesToRead === stat.size)
45
- return false;
46
- fs.writeFileSync(filePath, next, 'utf8');
47
- return true;
48
- }
49
- catch {
50
- return false;
51
- }
52
- }
53
- export function startDaemonLogRetention() {
54
- trimDaemonLogFile();
55
- return setInterval(() => {
56
- trimDaemonLogFile();
57
- }, TRIM_INTERVAL_MS);
58
- }
1
+ import c from"node:fs";import{resolveShennianPath as _}from"./config/index.js";const N=5e3,S=5*1024*1024,L=5*6e4;function l(s,i){const e=Number(s);return Number.isFinite(e)&&e>0?Math.floor(e):i}function x(){return{maxLines:l(process.env.SHENNIAN_DAEMON_LOG_MAX_LINES,N),maxBytes:l(process.env.SHENNIAN_DAEMON_LOG_MAX_BYTES,S)}}function u(s=_("daemon.log"),i=x()){try{const e=c.statSync(s);if(e.size===0)return!1;const o=Math.min(e.size,i.maxBytes),a=c.openSync(s,"r");let t="";try{const r=Buffer.allocUnsafe(o);c.readSync(a,r,0,o,e.size-o),t=r.toString("utf8")}finally{c.closeSync(a)}if(o<e.size){const r=t.indexOf(`
2
+ `);t=r>=0?t.slice(r+1):""}const m=t.endsWith(`
3
+ `);let n=t.split(`
4
+ `);m&&(n=n.slice(0,-1)),n.length>i.maxLines&&(n=n.slice(-i.maxLines));const f=`${n.join(`
5
+ `)}${n.length>0?`
6
+ `:""}`;return f.length===e.size&&o===e.size?!1:(c.writeFileSync(s,f,"utf8"),!0)}catch{return!1}}function E(){return u(),setInterval(()=>{u()},L)}export{x as getDaemonLogRetention,E as startDaemonLogRetention,u as trimDaemonLogFile};