shennian 0.2.5 → 0.2.6

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/package.json +1 -1
  2. package/dist/bin/shennian.d.ts +0 -2
  3. package/dist/bin/shennian.js +0 -2
  4. package/dist/src/agents/adapter.d.ts +0 -35
  5. package/dist/src/agents/adapter.js +0 -19
  6. package/dist/src/agents/claude.d.ts +0 -25
  7. package/dist/src/agents/claude.js +0 -234
  8. package/dist/src/agents/codex.d.ts +0 -49
  9. package/dist/src/agents/codex.js +0 -794
  10. package/dist/src/agents/command-spec.d.ts +0 -36
  11. package/dist/src/agents/command-spec.js +0 -303
  12. package/dist/src/agents/cursor.d.ts +0 -22
  13. package/dist/src/agents/cursor.js +0 -248
  14. package/dist/src/agents/custom.d.ts +0 -30
  15. package/dist/src/agents/custom.js +0 -210
  16. package/dist/src/agents/detect.d.ts +0 -9
  17. package/dist/src/agents/detect.js +0 -46
  18. package/dist/src/agents/gemini.d.ts +0 -17
  19. package/dist/src/agents/gemini.js +0 -172
  20. package/dist/src/agents/model-registry/cache.d.ts +0 -4
  21. package/dist/src/agents/model-registry/cache.js +0 -37
  22. package/dist/src/agents/model-registry/discovery.d.ts +0 -4
  23. package/dist/src/agents/model-registry/discovery.js +0 -161
  24. package/dist/src/agents/model-registry/parsers.d.ts +0 -11
  25. package/dist/src/agents/model-registry/parsers.js +0 -349
  26. package/dist/src/agents/model-registry/runner.d.ts +0 -6
  27. package/dist/src/agents/model-registry/runner.js +0 -29
  28. package/dist/src/agents/model-registry/service.d.ts +0 -6
  29. package/dist/src/agents/model-registry/service.js +0 -71
  30. package/dist/src/agents/model-registry/types.d.ts +0 -30
  31. package/dist/src/agents/model-registry/types.js +0 -8
  32. package/dist/src/agents/model-registry.d.ts +0 -13
  33. package/dist/src/agents/model-registry.js +0 -15
  34. package/dist/src/agents/openclaw.d.ts +0 -20
  35. package/dist/src/agents/openclaw.js +0 -273
  36. package/dist/src/agents/pi.d.ts +0 -51
  37. package/dist/src/agents/pi.js +0 -793
  38. package/dist/src/commands/agent.d.ts +0 -2
  39. package/dist/src/commands/agent.js +0 -131
  40. package/dist/src/commands/daemon.d.ts +0 -35
  41. package/dist/src/commands/daemon.js +0 -612
  42. package/dist/src/commands/pair-qr.d.ts +0 -4
  43. package/dist/src/commands/pair-qr.js +0 -6
  44. package/dist/src/commands/pair.d.ts +0 -8
  45. package/dist/src/commands/pair.js +0 -162
  46. package/dist/src/commands/upgrade.d.ts +0 -5
  47. package/dist/src/commands/upgrade.js +0 -145
  48. package/dist/src/config/index.d.ts +0 -32
  49. package/dist/src/config/index.js +0 -31
  50. package/dist/src/fs/handler.d.ts +0 -26
  51. package/dist/src/fs/handler.js +0 -130
  52. package/dist/src/fs/security.d.ts +0 -2
  53. package/dist/src/fs/security.js +0 -32
  54. package/dist/src/index.d.ts +0 -1
  55. package/dist/src/index.js +0 -277
  56. package/dist/src/log-reporter.d.ts +0 -18
  57. package/dist/src/log-reporter.js +0 -16
  58. package/dist/src/native-fusion/parsers.d.ts +0 -12
  59. package/dist/src/native-fusion/parsers.js +0 -687
  60. package/dist/src/native-fusion/service.d.ts +0 -25
  61. package/dist/src/native-fusion/service.js +0 -176
  62. package/dist/src/native-fusion/state.d.ts +0 -3
  63. package/dist/src/native-fusion/state.js +0 -22
  64. package/dist/src/native-fusion/types.d.ts +0 -24
  65. package/dist/src/native-fusion/types.js +0 -1
  66. package/dist/src/region.d.ts +0 -15
  67. package/dist/src/region.js +0 -99
  68. package/dist/src/relay/client.d.ts +0 -62
  69. package/dist/src/relay/client.js +0 -311
  70. package/dist/src/session/handlers/agents.d.ts +0 -4
  71. package/dist/src/session/handlers/agents.js +0 -55
  72. package/dist/src/session/handlers/chat.d.ts +0 -4
  73. package/dist/src/session/handlers/chat.js +0 -326
  74. package/dist/src/session/handlers/control.d.ts +0 -5
  75. package/dist/src/session/handlers/control.js +0 -57
  76. package/dist/src/session/handlers/fs.d.ts +0 -10
  77. package/dist/src/session/handlers/fs.js +0 -256
  78. package/dist/src/session/manager.d.ts +0 -28
  79. package/dist/src/session/manager.js +0 -174
  80. package/dist/src/session/store.d.ts +0 -7
  81. package/dist/src/session/store.js +0 -46
  82. package/dist/src/session/types.d.ts +0 -35
  83. package/dist/src/session/types.js +0 -4
  84. package/dist/src/upgrade/engine.d.ts +0 -64
  85. package/dist/src/upgrade/engine.js +0 -238
@@ -1,162 +0,0 @@
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 { 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 renderTokenBox(token) {
12
- const line = '─'.repeat(token.length + 4);
13
- console.log(chalk.cyan(`┌${line}┐`));
14
- console.log(chalk.cyan('│ ') + chalk.bold.yellow(token) + chalk.cyan(' │'));
15
- console.log(chalk.cyan(`└${line}┘`));
16
- }
17
- async function renderQR(url) {
18
- const qrcode = (await import('qrcode-terminal')).default;
19
- return new Promise((resolve) => {
20
- qrcode.generate(url, PAIR_QR_RENDER_OPTIONS, (qr) => {
21
- console.log(qr);
22
- resolve();
23
- });
24
- });
25
- }
26
- async function pollStatus(serverUrl, pairToken) {
27
- const deadline = Date.now() + POLL_TIMEOUT_MS;
28
- while (Date.now() < deadline) {
29
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
30
- let res;
31
- try {
32
- res = await fetch(`${serverUrl}/api/machines/pair/status?token=${encodeURIComponent(pairToken)}`);
33
- }
34
- catch {
35
- continue;
36
- }
37
- if (res.status === 410) {
38
- console.error(chalk.red('\n✗ Pairing code expired. Please re-run: shennian pair'));
39
- process.exit(1);
40
- }
41
- if (!res.ok)
42
- continue;
43
- const data = (await res.json());
44
- if (data.status === 'claimed' && data.machineToken && data.machineId) {
45
- return {
46
- machineToken: data.machineToken,
47
- machineId: data.machineId,
48
- machineName: data.machineName ?? '',
49
- };
50
- }
51
- }
52
- return null;
53
- }
54
- function askConfirm(question) {
55
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
56
- return new Promise((resolve) => {
57
- rl.question(question, (ans) => {
58
- rl.close();
59
- resolve(ans.trim().toLowerCase() === 'y');
60
- });
61
- });
62
- }
63
- export async function runPairFlow(opts) {
64
- const { serverUrl, machineName, force } = opts;
65
- const config = loadConfig();
66
- if (config.machineToken && config.machineId && !force) {
67
- console.log(chalk.green(`✓ Already paired (machine ID: ${config.machineId})`));
68
- console.log(chalk.gray(' Run "shennian pair" to re-pair to another account'));
69
- return;
70
- }
71
- if (config.machineToken && !force)
72
- return;
73
- const detectedAgents = detectAgents();
74
- const agentList = detectedAgents.map((a) => a.type);
75
- const platform = os.platform();
76
- const osName = platform === 'win32' ? 'windows' : platform === 'darwin' ? 'macos' : 'linux';
77
- console.log(chalk.bold('\nShennian CLI — Pairing\n'));
78
- console.log(chalk.gray(` Machine: ${machineName} | OS: ${osName} | Agents: ${agentList.join(', ') || 'none'}`));
79
- console.log();
80
- let res;
81
- try {
82
- res = await fetch(`${serverUrl}/api/machines/pair/create`, {
83
- method: 'POST',
84
- headers: { 'Content-Type': 'application/json' },
85
- body: JSON.stringify({ name: machineName, os: osName, agentList }),
86
- });
87
- }
88
- catch {
89
- console.error(chalk.red(`✗ Network error: cannot connect to ${serverUrl}`));
90
- process.exit(1);
91
- }
92
- if (!res.ok) {
93
- const body = await res.json().catch(() => ({}));
94
- console.error(chalk.red(`✗ Failed to create machine: ${body.error ?? res.statusText}`));
95
- process.exit(1);
96
- }
97
- const data = (await res.json());
98
- config.machineId = data.machineId;
99
- config.serverUrl = serverUrl;
100
- saveConfig(config);
101
- const pairUrl = buildPairQrPayload(data.pairToken);
102
- console.log(chalk.white('Scan QR code or enter the pairing code in the Shennian app:'));
103
- console.log();
104
- await renderQR(pairUrl);
105
- renderTokenBox(data.pairToken);
106
- console.log();
107
- console.log(chalk.gray('Waiting for pairing... (Ctrl+C to cancel)'));
108
- const result = await pollStatus(serverUrl, data.pairToken);
109
- if (!result) {
110
- console.error(chalk.red('\n✗ Timed out. Pairing code expired, please try again.'));
111
- process.exit(1);
112
- }
113
- config.machineToken = result.machineToken;
114
- config.machineId = result.machineId;
115
- saveConfig(config);
116
- console.log(chalk.green(`\n✓ Paired successfully! Machine "${result.machineName}" is now linked.`));
117
- }
118
- export async function runSmartStart(serverUrl, machineName) {
119
- const config = loadConfig();
120
- if (!config.machineToken) {
121
- const resolvedUrl = serverUrl ?? (await detectAndChooseServer());
122
- await runPairFlow({ serverUrl: resolvedUrl, machineName, force: false });
123
- }
124
- else {
125
- console.log(chalk.green(`✓ Already paired (machine ID: ${config.machineId})`));
126
- }
127
- saveEnvSnapshot();
128
- console.log(chalk.gray('\nStarting background service...'));
129
- const startedByService = installService();
130
- if (!startedByService) {
131
- startDaemonProcess();
132
- }
133
- }
134
- export function registerPairCommand(program) {
135
- program
136
- .command('pair')
137
- .description('Pair this machine to a Shennian account (re-pair if already paired)')
138
- .option('--api <url>', 'Server URL override')
139
- .option('--name <name>', 'Machine name', os.hostname())
140
- .action(async (opts) => {
141
- const config = loadConfig();
142
- if (config.machineToken) {
143
- console.log(chalk.yellow(`Currently paired (machine ID: ${config.machineId})`));
144
- const yes = await askConfirm('Re-pairing will unlink the current account. Continue? (y/N) ');
145
- if (!yes) {
146
- console.log(chalk.gray('Cancelled'));
147
- return;
148
- }
149
- delete config.machineToken;
150
- delete config.machineId;
151
- saveConfig(config);
152
- }
153
- const serverUrl = opts.api ?? (await detectAndChooseServer());
154
- await runPairFlow({ serverUrl, machineName: opts.name, force: true });
155
- saveEnvSnapshot();
156
- console.log(chalk.gray('\nStarting background service...'));
157
- const startedByService = installService();
158
- if (!startedByService) {
159
- startDaemonProcess();
160
- }
161
- });
162
- }
@@ -1,5 +0,0 @@
1
- import type { Command } from 'commander';
2
- import type { CliRelayClient } from '../relay/client.js';
3
- export declare function registerUpgradeCommand(program: Command): void;
4
- export declare function handleUpgradeStart(client: CliRelayClient, reqId: string, targetVersion?: string): Promise<void>;
5
- export declare function handleUpgradeStatus(client: CliRelayClient, reqId: string): Promise<void>;
@@ -1,145 +0,0 @@
1
- // @arch docs/architecture/cli/upgrade.md
2
- import chalk from 'chalk';
3
- import { getCurrentVersion, fetchLatestVersion, compareVersions, performUpgrade, checkForUpdate, } from '../upgrade/engine.js';
4
- const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
5
- // ─── Interactive CLI upgrade command ─────────────────────────────────────────
6
- export function registerUpgradeCommand(program) {
7
- program
8
- .command('upgrade')
9
- .description('Upgrade shennian CLI to the latest version')
10
- .option('--check', 'Only check for updates, do not install')
11
- .option('--to <version>', 'Upgrade to a specific version')
12
- .action(async (opts) => {
13
- const current = getCurrentVersion();
14
- console.log(chalk.gray(`Current version: ${current}`));
15
- console.log();
16
- let spinIdx = 0;
17
- const spinTimer = setInterval(() => {
18
- process.stdout.write(`\r ${SPINNER[spinIdx % SPINNER.length]} Checking for updates...`);
19
- spinIdx++;
20
- }, 80);
21
- let latest;
22
- try {
23
- latest = opts.to ?? await fetchLatestVersion();
24
- clearInterval(spinTimer);
25
- process.stdout.write('\r' + ' '.repeat(40) + '\r');
26
- }
27
- catch (err) {
28
- clearInterval(spinTimer);
29
- process.stdout.write('\r' + ' '.repeat(40) + '\r');
30
- console.error(chalk.red(`✗ Failed to check for updates: ${err instanceof Error ? err.message : String(err)}`));
31
- process.exit(1);
32
- }
33
- const changeType = compareVersions(current, latest);
34
- if (changeType === 'none' && !opts.to) {
35
- console.log(chalk.green(`✓ Already up to date (${current})`));
36
- return;
37
- }
38
- console.log(` Available: ${chalk.green(latest)} ${changeType === 'major' ? chalk.red('(major)') : changeType === 'minor' ? chalk.yellow('(minor)') : chalk.green('(patch)')}`);
39
- console.log();
40
- if (opts.check)
41
- return;
42
- if (changeType === 'major') {
43
- console.log(chalk.yellow(' ⚠ This is a major version update and may contain breaking changes.'));
44
- console.log(chalk.gray(' Please review the changelog before upgrading.'));
45
- console.log();
46
- }
47
- const result = await performUpgrade(latest, (progress) => {
48
- switch (progress.step) {
49
- case 'checking':
50
- console.log(chalk.gray(' → Checking npm...'));
51
- break;
52
- case 'backing-up':
53
- console.log(chalk.gray(' → Backing up current version...'));
54
- break;
55
- case 'installing':
56
- console.log(chalk.gray(` → Installing ${progress.version}...`));
57
- break;
58
- case 'verifying':
59
- console.log(chalk.gray(` → Verifying ${progress.version}...`));
60
- break;
61
- case 'restarting':
62
- console.log(chalk.gray(` → Restarting daemon...`));
63
- break;
64
- }
65
- });
66
- if (result.ok) {
67
- console.log();
68
- console.log(chalk.green(`✓ Upgraded ${result.from} → ${result.to}`));
69
- console.log(chalk.gray(' Daemon is restarting...'));
70
- // Kill self — systemd/launchd will restart with new version
71
- process.kill(process.pid, 'SIGTERM');
72
- }
73
- else {
74
- console.log();
75
- if (result.rolledBack) {
76
- console.error(chalk.red(`✗ Upgrade failed (rolled back to ${current})`));
77
- }
78
- else {
79
- console.error(chalk.red(`✗ Upgrade failed: ${result.error}`));
80
- }
81
- process.exit(1);
82
- }
83
- });
84
- }
85
- // ─── Relay-triggered upgrade (called from session manager) ───────────────────
86
- export async function handleUpgradeStart(client, reqId, targetVersion) {
87
- const sendEvent = (payload) => {
88
- client.sendEvent({ type: 'event', event: 'upgrade', payload });
89
- };
90
- const current = getCurrentVersion();
91
- // Resolve target version
92
- let latest;
93
- try {
94
- sendEvent({ state: 'checking', machineId: 'self' });
95
- latest = targetVersion ?? await fetchLatestVersion();
96
- }
97
- catch (err) {
98
- client.sendRes({ type: 'res', id: reqId, ok: false, error: `Version check failed: ${err instanceof Error ? err.message : String(err)}` });
99
- return;
100
- }
101
- const changeType = compareVersions(current, latest);
102
- if (changeType === 'none') {
103
- client.sendRes({ type: 'res', id: reqId, ok: true, payload: { alreadyLatest: true, version: current } });
104
- return;
105
- }
106
- // ACK immediately — upgrade is async
107
- client.sendRes({ type: 'res', id: reqId, ok: true, payload: { from: current, to: latest } });
108
- const result = await performUpgrade(latest, (progress) => {
109
- switch (progress.step) {
110
- case 'backing-up':
111
- sendEvent({ state: 'downloading', machineId: 'self', version: latest });
112
- break;
113
- case 'installing':
114
- sendEvent({ state: 'installing', machineId: 'self', version: latest });
115
- break;
116
- case 'restarting':
117
- sendEvent({ state: 'restarting', machineId: 'self', from: current, to: latest });
118
- break;
119
- }
120
- });
121
- if (result.ok) {
122
- sendEvent({ state: 'restarting', machineId: 'self', from: result.from, to: result.to });
123
- // Brief delay so the event is flushed before we exit
124
- await new Promise((r) => setTimeout(r, 500));
125
- process.kill(process.pid, 'SIGTERM');
126
- }
127
- else {
128
- sendEvent({
129
- state: result.rolledBack ? 'rolledback' : 'failed',
130
- machineId: 'self',
131
- ...(result.rolledBack ? { to: current, reason: result.error } : { error: result.error }),
132
- });
133
- }
134
- }
135
- export async function handleUpgradeStatus(client, reqId) {
136
- const result = await checkForUpdate();
137
- client.sendRes({
138
- type: 'res',
139
- id: reqId,
140
- ok: true,
141
- payload: result.hasUpdate
142
- ? { hasUpdate: true, current: result.current, latest: result.latest, changeType: result.changeType }
143
- : { hasUpdate: false, current: getCurrentVersion() },
144
- });
145
- }
@@ -1,32 +0,0 @@
1
- export declare function getShennianDir(): string;
2
- export declare function resolveShennianPath(...parts: string[]): string;
3
- export declare const configPath: string;
4
- export type CustomAgentEntry = {
5
- command: string;
6
- caps: {
7
- name: string;
8
- model?: string;
9
- models?: string[];
10
- defaultModel?: string;
11
- mode: 'spawn' | 'stdio';
12
- proactive?: boolean;
13
- resume?: boolean;
14
- version?: string;
15
- };
16
- };
17
- export type ShennianConfig = {
18
- defaultProvider?: string;
19
- defaultModel?: string;
20
- apiKeys?: Record<string, string>;
21
- machineId?: string;
22
- machineToken?: string;
23
- accessToken?: string;
24
- refreshToken?: string;
25
- serverUrl?: string;
26
- customAgents?: Record<string, CustomAgentEntry>;
27
- /** Auto-upgrade policy: 'none' | 'patch' | 'minor' (default: 'patch') */
28
- autoUpgrade?: 'none' | 'patch' | 'minor';
29
- };
30
- export declare function loadConfig(): ShennianConfig;
31
- export declare function saveConfig(config: ShennianConfig): void;
32
- export declare function ensureDir(): void;
@@ -1,31 +0,0 @@
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
- return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
19
- }
20
- catch {
21
- return {};
22
- }
23
- }
24
- export function saveConfig(config) {
25
- fs.mkdirSync(getShennianDir(), { recursive: true });
26
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
27
- }
28
- export function ensureDir() {
29
- fs.mkdirSync(getShennianDir(), { recursive: true });
30
- fs.mkdirSync(resolveShennianPath('sessions'), { recursive: true });
31
- }
@@ -1,26 +0,0 @@
1
- import type { FsEntry, FsTransferRequest, FsChunk } from '@shennian/wire';
2
- export type TransferProgress = {
3
- transferId: string;
4
- seq: number;
5
- total: number;
6
- bytesSent: number;
7
- bytesTotal: number;
8
- };
9
- export declare function handleFsLs(reqPath: string): {
10
- entries: FsEntry[];
11
- } | {
12
- error: string;
13
- };
14
- export declare function handleFsRead(reqPath: string): {
15
- path: string;
16
- content: string;
17
- } | {
18
- error: string;
19
- };
20
- export declare function handleFsDownload(req: FsTransferRequest, onProgress?: (progress: TransferProgress) => void): AsyncGenerator<FsChunk>;
21
- export declare function handleFsUploadInit(req: FsTransferRequest): {
22
- targetPath: string;
23
- } | {
24
- error: string;
25
- };
26
- export declare function handleFsUploadChunk(targetPath: string, chunk: FsChunk): void;
@@ -1,130 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { normalizePath, isPathSafe } from './security.js';
5
- const FILE_SYSTEM_ROOTS_PATH = '__roots__';
6
- const MAX_READ_SIZE = 100 * 1024;
7
- const MAX_TRANSFER_SIZE = 20 * 1024 * 1024;
8
- const CHUNK_SIZE = 64 * 1024;
9
- const RATE_LIMIT_BYTES_PER_SEC = 5 * 1024 * 1024;
10
- function sleep(ms) {
11
- return new Promise((r) => setTimeout(r, ms));
12
- }
13
- function listFileSystemRoots() {
14
- if (os.platform() === 'win32') {
15
- const entries = [];
16
- for (let code = 65; code <= 90; code += 1) {
17
- const drive = `${String.fromCharCode(code)}:\\`;
18
- if (!fs.existsSync(drive))
19
- continue;
20
- entries.push({ name: drive, path: drive, isDir: true });
21
- }
22
- return { entries };
23
- }
24
- return { entries: [{ name: '/', path: '/', isDir: true }] };
25
- }
26
- export function handleFsLs(reqPath) {
27
- if (reqPath === FILE_SYSTEM_ROOTS_PATH) {
28
- return listFileSystemRoots();
29
- }
30
- const normalized = normalizePath(reqPath);
31
- if (!isPathSafe(normalized)) {
32
- return { error: `Access denied: ${reqPath}` };
33
- }
34
- try {
35
- const dirents = fs.readdirSync(normalized, { withFileTypes: true });
36
- const entries = dirents.map((d) => {
37
- const fullPath = path.join(normalized, d.name);
38
- const isDir = d.isDirectory();
39
- const entry = { name: d.name, path: fullPath, isDir };
40
- try {
41
- const stat = fs.statSync(fullPath);
42
- entry.size = isDir ? undefined : stat.size;
43
- entry.modifiedAt = stat.mtimeMs;
44
- }
45
- catch {
46
- // stat failure is non-fatal
47
- }
48
- return entry;
49
- });
50
- return { entries };
51
- }
52
- catch (err) {
53
- return { error: `Failed to list ${reqPath}: ${err.message}` };
54
- }
55
- }
56
- export function handleFsRead(reqPath) {
57
- const normalized = normalizePath(reqPath);
58
- if (!isPathSafe(normalized)) {
59
- return { error: `Access denied: ${reqPath}` };
60
- }
61
- try {
62
- const stat = fs.statSync(normalized);
63
- if (stat.isDirectory()) {
64
- return { error: `Not a file: ${reqPath}` };
65
- }
66
- if (stat.size > MAX_READ_SIZE) {
67
- return { error: `File too large (${stat.size} bytes, max ${MAX_READ_SIZE})` };
68
- }
69
- const content = fs.readFileSync(normalized, 'utf-8');
70
- return { path: normalized, content };
71
- }
72
- catch (err) {
73
- return { error: `Failed to read ${reqPath}: ${err.message}` };
74
- }
75
- }
76
- export async function* handleFsDownload(req, onProgress) {
77
- const filePath = req.path;
78
- if (!filePath) {
79
- throw new Error('Missing path for download');
80
- }
81
- const normalized = normalizePath(filePath);
82
- if (!isPathSafe(normalized)) {
83
- throw new Error(`Access denied: ${filePath}`);
84
- }
85
- const stat = fs.statSync(normalized);
86
- if (stat.size > MAX_TRANSFER_SIZE) {
87
- throw new Error(`File too large (${stat.size} bytes, max ${MAX_TRANSFER_SIZE})`);
88
- }
89
- const buf = fs.readFileSync(normalized);
90
- const total = Math.ceil(buf.length / CHUNK_SIZE);
91
- const chunksPerSec = Math.floor(RATE_LIMIT_BYTES_PER_SEC / CHUNK_SIZE);
92
- const delayMs = chunksPerSec > 0 ? Math.ceil(1000 / chunksPerSec) : 0;
93
- for (let seq = 0; seq < total; seq++) {
94
- const start = seq * CHUNK_SIZE;
95
- const end = Math.min(start + CHUNK_SIZE, buf.length);
96
- const data = buf.subarray(start, end).toString('base64');
97
- yield { transferId: req.transferId, seq, total, data };
98
- onProgress?.({
99
- transferId: req.transferId,
100
- seq,
101
- total,
102
- bytesSent: end,
103
- bytesTotal: buf.length,
104
- });
105
- if (delayMs > 0 && seq < total - 1) {
106
- await sleep(delayMs);
107
- }
108
- }
109
- }
110
- export function handleFsUploadInit(req) {
111
- const targetPath = req.targetPath ?? req.path;
112
- if (!targetPath) {
113
- return { error: 'Missing targetPath for upload' };
114
- }
115
- const normalized = normalizePath(targetPath);
116
- if (!isPathSafe(normalized)) {
117
- return { error: `Access denied: ${targetPath}` };
118
- }
119
- if (req.size != null && req.size > MAX_TRANSFER_SIZE) {
120
- return { error: `File too large (${req.size} bytes, max ${MAX_TRANSFER_SIZE})` };
121
- }
122
- const dir = path.dirname(normalized);
123
- fs.mkdirSync(dir, { recursive: true });
124
- return { targetPath: normalized };
125
- }
126
- export function handleFsUploadChunk(targetPath, chunk) {
127
- const buf = Buffer.from(chunk.data, 'base64');
128
- const flag = chunk.seq === 0 ? 'w' : 'a';
129
- fs.writeFileSync(targetPath, buf, { flag });
130
- }
@@ -1,2 +0,0 @@
1
- export declare function normalizePath(inputPath: string): string;
2
- export declare function isPathSafe(normalizedPath: string): boolean;
@@ -1,32 +0,0 @@
1
- import path from 'node:path';
2
- import os from 'node:os';
3
- const FILE_SYSTEM_ROOTS_PATH = '__roots__';
4
- const ALLOWED_ROOTS = [os.homedir()];
5
- const BLOCKED_PATTERNS = [
6
- /\.\./, // directory traversal
7
- /\/\.ssh\//,
8
- /\/\.gnupg\//,
9
- /\/\.aws\/credentials/,
10
- /\/\.env$/,
11
- /\/\.env\./,
12
- ];
13
- export function normalizePath(inputPath) {
14
- if (inputPath === FILE_SYSTEM_ROOTS_PATH)
15
- return inputPath;
16
- const expanded = inputPath.startsWith('~')
17
- ? path.join(os.homedir(), inputPath.slice(1))
18
- : inputPath;
19
- return path.resolve(expanded);
20
- }
21
- export function isPathSafe(normalizedPath) {
22
- if (normalizedPath === FILE_SYSTEM_ROOTS_PATH)
23
- return true;
24
- const withinAllowed = ALLOWED_ROOTS.some((root) => normalizedPath === root || normalizedPath.startsWith(root + path.sep));
25
- if (!withinAllowed)
26
- return false;
27
- for (const pattern of BLOCKED_PATTERNS) {
28
- if (pattern.test(normalizedPath))
29
- return false;
30
- }
31
- return true;
32
- }
@@ -1 +0,0 @@
1
- export {};