shennian 0.2.29 → 0.2.32
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.
- package/dist/bin/shennian.js +0 -0
- package/dist/src/agent-env.d.ts +7 -0
- package/dist/src/agent-env.js +82 -0
- package/dist/src/agents/claude.js +2 -1
- package/dist/src/agents/codex.js +3 -2
- package/dist/src/agents/cursor.js +2 -1
- package/dist/src/agents/custom.js +3 -2
- package/dist/src/agents/gemini.js +2 -1
- package/dist/src/agents/model-registry/discovery.js +2 -1
- package/dist/src/agents/model-registry/runner.js +2 -1
- package/dist/src/agents/openclaw.js +3 -2
- package/dist/src/agents/opencode.js +2 -1
- package/dist/src/commands/daemon.d.ts +2 -0
- package/dist/src/commands/daemon.js +37 -32
- package/dist/src/commands/pair.d.ts +1 -0
- package/dist/src/commands/pair.js +92 -25
- package/dist/src/commands/upgrade.d.ts +6 -2
- package/dist/src/commands/upgrade.js +6 -6
- package/dist/src/daemon-log.d.ts +9 -0
- package/dist/src/daemon-log.js +58 -0
- package/dist/src/env-path.d.ts +14 -0
- package/dist/src/env-path.js +64 -0
- package/dist/src/index.js +18 -11
- package/dist/src/native-fusion/parsers.js +77 -55
- package/dist/src/native-fusion/service.js +3 -0
- package/dist/src/session/manager.d.ts +2 -1
- package/dist/src/session/manager.js +7 -3
- package/dist/src/upgrade/engine.d.ts +4 -2
- package/dist/src/upgrade/engine.js +29 -4
- package/package.json +10 -9
package/dist/bin/shennian.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function readLatestUserEnv(): NodeJS.ProcessEnv;
|
|
2
|
+
export declare function mergeAgentProcessEnv(input: {
|
|
3
|
+
daemonEnv: NodeJS.ProcessEnv;
|
|
4
|
+
userEnv: NodeJS.ProcessEnv;
|
|
5
|
+
extra?: NodeJS.ProcessEnv;
|
|
6
|
+
}): NodeJS.ProcessEnv;
|
|
7
|
+
export declare function buildAgentProcessEnv(extra?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// @arch docs/architecture/cli/agent-adapters.md
|
|
2
|
+
// @test src/__tests__/agent-env.test.ts
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
const SHELL_ENV_START = '__SHENNIAN_AGENT_ENV_START__';
|
|
6
|
+
const SHELL_ENV_END = '__SHENNIAN_AGENT_ENV_END__';
|
|
7
|
+
function quotePosix(value) {
|
|
8
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
9
|
+
}
|
|
10
|
+
function parseEnvJson(stdout) {
|
|
11
|
+
const start = stdout.indexOf(SHELL_ENV_START);
|
|
12
|
+
const end = stdout.indexOf(SHELL_ENV_END, start + SHELL_ENV_START.length);
|
|
13
|
+
if (start === -1 || end === -1)
|
|
14
|
+
return null;
|
|
15
|
+
const json = stdout.slice(start + SHELL_ENV_START.length, end);
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(json);
|
|
18
|
+
const env = {};
|
|
19
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
20
|
+
if (typeof value === 'string')
|
|
21
|
+
env[key] = value;
|
|
22
|
+
}
|
|
23
|
+
return env;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function readPosixShellEnv() {
|
|
30
|
+
const shell = process.env.SHELL?.trim() || '/bin/sh';
|
|
31
|
+
const script = [
|
|
32
|
+
`printf ${quotePosix(SHELL_ENV_START)}`,
|
|
33
|
+
`${quotePosix(process.execPath)} -e ${quotePosix('process.stdout.write(JSON.stringify(process.env))')}`,
|
|
34
|
+
`printf ${quotePosix(SHELL_ENV_END)}`,
|
|
35
|
+
].join('; ');
|
|
36
|
+
for (const args of [['-ilc', script], ['-lc', script]]) {
|
|
37
|
+
const result = spawnSync(shell, args, {
|
|
38
|
+
env: process.env,
|
|
39
|
+
encoding: 'utf-8',
|
|
40
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
41
|
+
timeout: 1500,
|
|
42
|
+
});
|
|
43
|
+
const parsed = typeof result.stdout === 'string' ? parseEnvJson(result.stdout) : null;
|
|
44
|
+
if (parsed)
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function readWindowsGlobalEnv() {
|
|
50
|
+
const script = `
|
|
51
|
+
$envs = @{}
|
|
52
|
+
[Environment]::GetEnvironmentVariables('Machine').GetEnumerator() | ForEach-Object { $envs[$_.Key] = [string]$_.Value }
|
|
53
|
+
[Environment]::GetEnvironmentVariables('User').GetEnumerator() | ForEach-Object { $envs[$_.Key] = [string]$_.Value }
|
|
54
|
+
Write-Output '${SHELL_ENV_START}'
|
|
55
|
+
$envs | ConvertTo-Json -Compress
|
|
56
|
+
Write-Output '${SHELL_ENV_END}'
|
|
57
|
+
`;
|
|
58
|
+
const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', script], {
|
|
59
|
+
encoding: 'utf-8',
|
|
60
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
61
|
+
timeout: 1500,
|
|
62
|
+
});
|
|
63
|
+
return typeof result.stdout === 'string' ? parseEnvJson(result.stdout) : null;
|
|
64
|
+
}
|
|
65
|
+
export function readLatestUserEnv() {
|
|
66
|
+
const env = os.platform() === 'win32' ? readWindowsGlobalEnv() : readPosixShellEnv();
|
|
67
|
+
return env ?? {};
|
|
68
|
+
}
|
|
69
|
+
export function mergeAgentProcessEnv(input) {
|
|
70
|
+
return {
|
|
71
|
+
...input.daemonEnv,
|
|
72
|
+
...input.userEnv,
|
|
73
|
+
...input.extra,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function buildAgentProcessEnv(extra = {}) {
|
|
77
|
+
return mergeAgentProcessEnv({
|
|
78
|
+
daemonEnv: process.env,
|
|
79
|
+
userEnv: readLatestUserEnv(),
|
|
80
|
+
extra,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -2,6 +2,7 @@ import { createInterface } from 'node:readline';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { AgentAdapter, registerAgent } from './adapter.js';
|
|
4
4
|
import { resolveBuiltinCommand, spawnResolvedCommand } from './command-spec.js';
|
|
5
|
+
import { buildAgentProcessEnv } from '../agent-env.js';
|
|
5
6
|
export function normalizeClaudeModelId(modelId) {
|
|
6
7
|
const trimmed = modelId?.trim();
|
|
7
8
|
return trimmed || 'default';
|
|
@@ -60,7 +61,7 @@ export class ClaudeAdapter extends AgentAdapter {
|
|
|
60
61
|
const proc = spawnResolvedCommand(spec, args, {
|
|
61
62
|
cwd: this.workDir ?? undefined,
|
|
62
63
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
63
|
-
env:
|
|
64
|
+
env: buildAgentProcessEnv(),
|
|
64
65
|
});
|
|
65
66
|
this.process = proc;
|
|
66
67
|
const rl = createInterface({ input: proc.stdout });
|
package/dist/src/agents/codex.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createInterface } from 'node:readline';
|
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { AgentAdapter, registerAgent } from './adapter.js';
|
|
6
6
|
import { resolveBuiltinCommand, spawnResolvedCommand } from './command-spec.js';
|
|
7
|
+
import { buildAgentProcessEnv } from '../agent-env.js';
|
|
7
8
|
export class CodexAdapter extends AgentAdapter {
|
|
8
9
|
type = 'codex';
|
|
9
10
|
process = null;
|
|
@@ -78,7 +79,7 @@ export class CodexAdapter extends AgentAdapter {
|
|
|
78
79
|
const proc = spawnResolvedCommand(spec, args, {
|
|
79
80
|
cwd: this.workDir ?? undefined,
|
|
80
81
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
81
|
-
env:
|
|
82
|
+
env: buildAgentProcessEnv(),
|
|
82
83
|
});
|
|
83
84
|
this.process = proc;
|
|
84
85
|
const rl = createInterface({ input: proc.stdout });
|
|
@@ -125,7 +126,7 @@ export class CodexAdapter extends AgentAdapter {
|
|
|
125
126
|
const proc = spawnResolvedCommand(spec, ['app-server', '--listen', 'stdio://'], {
|
|
126
127
|
cwd: this.workDir ?? undefined,
|
|
127
128
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
128
|
-
env: {
|
|
129
|
+
env: buildAgentProcessEnv({ NO_COLOR: '1' }),
|
|
129
130
|
});
|
|
130
131
|
this.process = proc;
|
|
131
132
|
this.stderrBuf = '';
|
|
@@ -4,6 +4,7 @@ import { createInterface } from 'node:readline';
|
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { AgentAdapter, registerAgent } from './adapter.js';
|
|
6
6
|
import { resolveBuiltinCommand, spawnResolvedCommand } from './command-spec.js';
|
|
7
|
+
import { buildAgentProcessEnv } from '../agent-env.js';
|
|
7
8
|
export class CursorAdapter extends AgentAdapter {
|
|
8
9
|
type = 'cursor';
|
|
9
10
|
process = null;
|
|
@@ -60,7 +61,7 @@ export class CursorAdapter extends AgentAdapter {
|
|
|
60
61
|
const proc = spawnResolvedCommand(spec, args, {
|
|
61
62
|
cwd: this.workDir ?? undefined,
|
|
62
63
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
63
|
-
env:
|
|
64
|
+
env: buildAgentProcessEnv(),
|
|
64
65
|
});
|
|
65
66
|
this.process = proc;
|
|
66
67
|
const rl = createInterface({ input: proc.stdout });
|
|
@@ -3,6 +3,7 @@ import { createInterface } from 'node:readline';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { AgentAdapter, registerAgent } from './adapter.js';
|
|
5
5
|
import { spawnCommandString } from './command-spec.js';
|
|
6
|
+
import { buildAgentProcessEnv } from '../agent-env.js';
|
|
6
7
|
export class CustomAgentAdapter extends AgentAdapter {
|
|
7
8
|
type;
|
|
8
9
|
command;
|
|
@@ -75,7 +76,7 @@ export class CustomAgentAdapter extends AgentAdapter {
|
|
|
75
76
|
const proc = spawnCommandString(this.command, args, {
|
|
76
77
|
cwd: this.workDir ?? undefined,
|
|
77
78
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
78
|
-
env:
|
|
79
|
+
env: buildAgentProcessEnv(),
|
|
79
80
|
});
|
|
80
81
|
this.spawnProcess = proc;
|
|
81
82
|
this.attachOutputHandlers(proc, () => {
|
|
@@ -101,7 +102,7 @@ export class CustomAgentAdapter extends AgentAdapter {
|
|
|
101
102
|
const proc = spawnCommandString(this.command, ['/start', '--workdir', this.workDir ?? process.cwd()], {
|
|
102
103
|
cwd: this.workDir ?? undefined,
|
|
103
104
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
104
|
-
env:
|
|
105
|
+
env: buildAgentProcessEnv(),
|
|
105
106
|
});
|
|
106
107
|
this.stdioProcess = proc;
|
|
107
108
|
this.attachOutputHandlers(proc, () => {
|
|
@@ -2,6 +2,7 @@ import { createInterface } from 'node:readline';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { AgentAdapter, registerAgent } from './adapter.js';
|
|
4
4
|
import { resolveBuiltinCommand, spawnResolvedCommand } from './command-spec.js';
|
|
5
|
+
import { buildAgentProcessEnv } from '../agent-env.js';
|
|
5
6
|
function num(v) {
|
|
6
7
|
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
7
8
|
}
|
|
@@ -72,7 +73,7 @@ export class GeminiAdapter extends AgentAdapter {
|
|
|
72
73
|
const proc = spawnResolvedCommand(spec, args, {
|
|
73
74
|
cwd: this.workDir ?? undefined,
|
|
74
75
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
75
|
-
env:
|
|
76
|
+
env: buildAgentProcessEnv(),
|
|
76
77
|
});
|
|
77
78
|
this.process = proc;
|
|
78
79
|
const rl = createInterface({ input: proc.stdout });
|
|
@@ -5,6 +5,7 @@ import { resolveBuiltinCommand, spawnResolvedCommand } from '../command-spec.js'
|
|
|
5
5
|
import { fallbackClaudeAliasModels, fallbackGeminiModels, parseCodexAppServerModels, parseCursorModels, parseOpenCodeModels, } from './parsers.js';
|
|
6
6
|
import { runResolvedCommand } from './runner.js';
|
|
7
7
|
import { DISCOVERY_WORKDIR } from './types.js';
|
|
8
|
+
import { buildAgentProcessEnv } from '../../agent-env.js';
|
|
8
9
|
function sendAppServerRpc(proc, pending, id, method, params, timeoutMs) {
|
|
9
10
|
if (!proc.stdin)
|
|
10
11
|
return Promise.reject(new Error('codex app-server stdin unavailable'));
|
|
@@ -27,7 +28,7 @@ async function discoverCodexModelsViaAppServer(spec) {
|
|
|
27
28
|
const proc = spawnResolvedCommand(spec, ['app-server', '--listen', 'stdio://'], {
|
|
28
29
|
cwd: DISCOVERY_WORKDIR,
|
|
29
30
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
30
|
-
env: {
|
|
31
|
+
env: buildAgentProcessEnv({ NO_COLOR: '1' }),
|
|
31
32
|
});
|
|
32
33
|
const pending = new Map();
|
|
33
34
|
let seq = 1;
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// @test src/__tests__/model-switching.test.ts
|
|
3
3
|
import { spawnResolvedCommand } from '../command-spec.js';
|
|
4
4
|
import { DISCOVERY_WORKDIR } from './types.js';
|
|
5
|
+
import { buildAgentProcessEnv } from '../../agent-env.js';
|
|
5
6
|
export function runResolvedCommand(spec, args, timeoutMs = 15_000) {
|
|
6
7
|
return new Promise((resolve, reject) => {
|
|
7
8
|
const proc = spawnResolvedCommand(spec, args, {
|
|
8
9
|
cwd: DISCOVERY_WORKDIR,
|
|
9
10
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
10
|
-
env: {
|
|
11
|
+
env: buildAgentProcessEnv({ NO_COLOR: '1' }),
|
|
11
12
|
});
|
|
12
13
|
let stdout = '';
|
|
13
14
|
let stderr = '';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { AgentAdapter } from './adapter.js';
|
|
3
3
|
import { resolveBuiltinCommand, spawnResolvedCommand } from './command-spec.js';
|
|
4
|
+
import { buildAgentProcessEnv } from '../agent-env.js';
|
|
4
5
|
/** Best-effort parse of `openclaw agent --json` stdout; shape may vary by version. */
|
|
5
6
|
function extractResponseText(parsed) {
|
|
6
7
|
if (parsed == null)
|
|
@@ -125,7 +126,7 @@ export class OpenClawAdapter extends AgentAdapter {
|
|
|
125
126
|
const proc = spawnResolvedCommand(spec, args, {
|
|
126
127
|
cwd: this.workDir ?? undefined,
|
|
127
128
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
128
|
-
env:
|
|
129
|
+
env: buildAgentProcessEnv(),
|
|
129
130
|
});
|
|
130
131
|
this.process = proc;
|
|
131
132
|
let stdoutBuf = '';
|
|
@@ -204,7 +205,7 @@ export class OpenClawAdapter extends AgentAdapter {
|
|
|
204
205
|
const proc = spawnResolvedCommand(spec, args, {
|
|
205
206
|
cwd: this.workDir ?? undefined,
|
|
206
207
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
207
|
-
env:
|
|
208
|
+
env: buildAgentProcessEnv(),
|
|
208
209
|
});
|
|
209
210
|
let stdoutBuf = '';
|
|
210
211
|
let stderrBuf = '';
|
|
@@ -4,6 +4,7 @@ import { createInterface } from 'node:readline';
|
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { AgentAdapter, registerAgent } from './adapter.js';
|
|
6
6
|
import { resolveBuiltinCommand, spawnResolvedCommand } from './command-spec.js';
|
|
7
|
+
import { buildAgentProcessEnv } from '../agent-env.js';
|
|
7
8
|
function usageFromTokens(tokens) {
|
|
8
9
|
if (!tokens)
|
|
9
10
|
return undefined;
|
|
@@ -103,7 +104,7 @@ export class OpenCodeAdapter extends AgentAdapter {
|
|
|
103
104
|
const proc = spawnResolvedCommand(spec, args, {
|
|
104
105
|
cwd: this.workDir ?? undefined,
|
|
105
106
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
106
|
-
env: {
|
|
107
|
+
env: buildAgentProcessEnv({ NO_COLOR: '1' }),
|
|
107
108
|
});
|
|
108
109
|
this.process = proc;
|
|
109
110
|
const rl = createInterface({ input: proc.stdout });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
export declare function isSafeSnapshotEnvKey(key: string): boolean;
|
|
2
3
|
type Platform = 'darwin' | 'linux' | 'win32';
|
|
3
4
|
type ServiceLaunchMode = 'direct' | 'global-shim' | 'npx';
|
|
4
5
|
type DaemonLauncher = 'desktop-managed' | 'global-cli' | 'unknown';
|
|
@@ -13,6 +14,7 @@ export type DaemonStatus = {
|
|
|
13
14
|
pidFile: string;
|
|
14
15
|
logFile: string;
|
|
15
16
|
machineId?: string;
|
|
17
|
+
paired?: boolean;
|
|
16
18
|
serverUrl?: string;
|
|
17
19
|
};
|
|
18
20
|
export type ServiceLaunchSpec = {
|
|
@@ -7,6 +7,7 @@ import os from 'node:os';
|
|
|
7
7
|
import { execSync, spawn } from 'node:child_process';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { getShennianDir, loadConfig, resolveShennianPath, saveConfig } from '../config/index.js';
|
|
10
|
+
import { buildAugmentedPath } from '../env-path.js';
|
|
10
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const SHENNIAN_DIR = getShennianDir();
|
|
12
13
|
const PID_FILE = resolveShennianPath('daemon.pid');
|
|
@@ -15,6 +16,32 @@ const REMOTE_ACCESS_DISABLED_FILE = resolveShennianPath('remote-access.disabled'
|
|
|
15
16
|
const LAUNCHER_FILE = resolveShennianPath('daemon-launcher.json');
|
|
16
17
|
const SHENNIAN_SCRIPT = path.resolve(__dirname, '../../bin/shennian.js');
|
|
17
18
|
const NODE_EXEC = process.execPath;
|
|
19
|
+
const SAFE_SNAPSHOT_ENV_KEYS = new Set([
|
|
20
|
+
'PATH',
|
|
21
|
+
'HOME',
|
|
22
|
+
'USER',
|
|
23
|
+
'LOGNAME',
|
|
24
|
+
'SHELL',
|
|
25
|
+
'TMPDIR',
|
|
26
|
+
'LANG',
|
|
27
|
+
'LC_ALL',
|
|
28
|
+
'LC_CTYPE',
|
|
29
|
+
'SSH_AUTH_SOCK',
|
|
30
|
+
'XDG_CONFIG_HOME',
|
|
31
|
+
'XDG_DATA_HOME',
|
|
32
|
+
'TEMP',
|
|
33
|
+
'TMP',
|
|
34
|
+
'APPDATA',
|
|
35
|
+
'LOCALAPPDATA',
|
|
36
|
+
'ELECTRON_RUN_AS_NODE',
|
|
37
|
+
'SHENNIAN_DESKTOP_CLI_SCRIPT',
|
|
38
|
+
'SHENNIAN_DESKTOP_CLI_BRIDGE',
|
|
39
|
+
'SHENNIAN_DESKTOP_SERVER_URL',
|
|
40
|
+
'SHENNIAN_HOME',
|
|
41
|
+
]);
|
|
42
|
+
export function isSafeSnapshotEnvKey(key) {
|
|
43
|
+
return SAFE_SNAPSHOT_ENV_KEYS.has(key);
|
|
44
|
+
}
|
|
18
45
|
function getPlatform() {
|
|
19
46
|
const p = os.platform();
|
|
20
47
|
if (p === 'darwin' || p === 'linux' || p === 'win32')
|
|
@@ -176,6 +203,7 @@ export function getDaemonStatus(opts = {}) {
|
|
|
176
203
|
pidFile: PID_FILE,
|
|
177
204
|
logFile: LOG_FILE,
|
|
178
205
|
...(config.machineId ? { machineId: config.machineId } : {}),
|
|
206
|
+
paired: Boolean(config.machineToken && config.machineId),
|
|
179
207
|
...(config.serverUrl ? { serverUrl: config.serverUrl } : {}),
|
|
180
208
|
};
|
|
181
209
|
}
|
|
@@ -190,6 +218,9 @@ function persistServerUrlOverride(serverUrl) {
|
|
|
190
218
|
config.serverUrl = normalized;
|
|
191
219
|
saveConfig(config);
|
|
192
220
|
}
|
|
221
|
+
function resolveServerUrlOverride(api) {
|
|
222
|
+
return api?.trim() || process.env.SHENNIAN_DESKTOP_SERVER_URL?.trim() || undefined;
|
|
223
|
+
}
|
|
193
224
|
// ─── Service definitions ─────────────────────────────────────────────────────
|
|
194
225
|
const LAUNCHD_LABEL = 'com.shennian.agent';
|
|
195
226
|
const LAUNCHD_PLIST = path.join(os.homedir(), 'Library/LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
@@ -391,40 +422,13 @@ function installWindowsScheduledTask() {
|
|
|
391
422
|
}
|
|
392
423
|
}
|
|
393
424
|
export function captureEnvForService() {
|
|
394
|
-
const keep = [
|
|
395
|
-
'PATH',
|
|
396
|
-
'HOME',
|
|
397
|
-
'USER',
|
|
398
|
-
'LOGNAME',
|
|
399
|
-
'SHELL',
|
|
400
|
-
'TMPDIR',
|
|
401
|
-
'LANG',
|
|
402
|
-
'LC_ALL',
|
|
403
|
-
'LC_CTYPE',
|
|
404
|
-
'SSH_AUTH_SOCK',
|
|
405
|
-
'XDG_CONFIG_HOME',
|
|
406
|
-
'XDG_DATA_HOME',
|
|
407
|
-
'TEMP',
|
|
408
|
-
'TMP',
|
|
409
|
-
'ANTHROPIC_API_KEY',
|
|
410
|
-
'ANTHROPIC_AUTH_TOKEN',
|
|
411
|
-
'ANTHROPIC_BASE_URL',
|
|
412
|
-
'OPENAI_API_KEY',
|
|
413
|
-
'OPENAI_BASE_URL',
|
|
414
|
-
'GEMINI_API_KEY',
|
|
415
|
-
'GOOGLE_API_KEY',
|
|
416
|
-
'ELECTRON_RUN_AS_NODE',
|
|
417
|
-
'SHENNIAN_DESKTOP_CLI_SCRIPT',
|
|
418
|
-
'SHENNIAN_DESKTOP_CLI_BRIDGE',
|
|
419
|
-
'SHENNIAN_HOME',
|
|
420
|
-
];
|
|
421
425
|
const env = {};
|
|
422
|
-
for (const k of
|
|
426
|
+
for (const k of SAFE_SNAPSHOT_ENV_KEYS) {
|
|
423
427
|
if (process.env[k])
|
|
424
428
|
env[k] = process.env[k];
|
|
425
429
|
}
|
|
426
430
|
env.HOME ??= os.homedir();
|
|
427
|
-
env.PATH
|
|
431
|
+
env.PATH = buildAugmentedPath({ pathValue: env.PATH, env });
|
|
428
432
|
env.USER ??= os.userInfo().username;
|
|
429
433
|
if (getPlatform() === 'win32') {
|
|
430
434
|
const tempDir = process.env.TEMP || process.env.TMP || os.tmpdir();
|
|
@@ -505,7 +509,7 @@ export function saveEnvSnapshot() {
|
|
|
505
509
|
fs.mkdirSync(SHENNIAN_DIR, { recursive: true });
|
|
506
510
|
const snapshot = {};
|
|
507
511
|
for (const [k, v] of Object.entries(process.env)) {
|
|
508
|
-
if (v !== undefined)
|
|
512
|
+
if (v !== undefined && isSafeSnapshotEnvKey(k))
|
|
509
513
|
snapshot[k] = v;
|
|
510
514
|
}
|
|
511
515
|
fs.writeFileSync(resolveShennianPath('env.json'), JSON.stringify(snapshot, null, 2));
|
|
@@ -650,7 +654,7 @@ async function stopDaemonProcessAndWait(timeoutMs = 5000) {
|
|
|
650
654
|
return result;
|
|
651
655
|
}
|
|
652
656
|
function enableRemoteAccess(opts = {}) {
|
|
653
|
-
persistServerUrlOverride(opts.api);
|
|
657
|
+
persistServerUrlOverride(resolveServerUrlOverride(opts.api));
|
|
654
658
|
try {
|
|
655
659
|
fs.unlinkSync(REMOTE_ACCESS_DISABLED_FILE);
|
|
656
660
|
}
|
|
@@ -723,6 +727,7 @@ async function disableRemoteAccess(opts = {}) {
|
|
|
723
727
|
}
|
|
724
728
|
}
|
|
725
729
|
function daemonStart(opts) {
|
|
730
|
+
saveEnvSnapshot();
|
|
726
731
|
enableRemoteAccess(opts);
|
|
727
732
|
}
|
|
728
733
|
async function daemonStop(opts = {}) {
|
|
@@ -738,7 +743,7 @@ async function waitForPidExit(pid, timeoutMs = 5000) {
|
|
|
738
743
|
return !isRunning(pid);
|
|
739
744
|
}
|
|
740
745
|
async function daemonRestart(opts = {}) {
|
|
741
|
-
persistServerUrlOverride(opts.api);
|
|
746
|
+
persistServerUrlOverride(resolveServerUrlOverride(opts.api));
|
|
742
747
|
const pid = readPid();
|
|
743
748
|
if (pid !== null && isRunning(pid)) {
|
|
744
749
|
try {
|
|
@@ -8,6 +8,13 @@ import { detectAndChooseServer } from '../region.js';
|
|
|
8
8
|
import { buildPairQrPayload, PAIR_QR_RENDER_OPTIONS } from './pair-qr.js';
|
|
9
9
|
const POLL_INTERVAL_MS = 3000;
|
|
10
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
|
+
}
|
|
11
18
|
function renderTokenBox(token) {
|
|
12
19
|
const line = '─'.repeat(token.length + 4);
|
|
13
20
|
console.log(chalk.cyan(`┌${line}┐`));
|
|
@@ -23,8 +30,10 @@ async function renderQR(url) {
|
|
|
23
30
|
});
|
|
24
31
|
});
|
|
25
32
|
}
|
|
26
|
-
async function pollStatus(serverUrl, pairToken) {
|
|
33
|
+
async function pollStatus(serverUrl, pairToken, opts = {}) {
|
|
27
34
|
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
35
|
+
if (opts.json)
|
|
36
|
+
emitPairJson({ type: 'pair.waiting' });
|
|
28
37
|
while (Date.now() < deadline) {
|
|
29
38
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
30
39
|
let res;
|
|
@@ -35,13 +44,19 @@ async function pollStatus(serverUrl, pairToken) {
|
|
|
35
44
|
continue;
|
|
36
45
|
}
|
|
37
46
|
if (res.status === 410) {
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
}
|
|
40
53
|
}
|
|
41
54
|
if (!res.ok)
|
|
42
55
|
continue;
|
|
43
56
|
const data = (await res.json());
|
|
44
57
|
if (data.status === 'claimed' && data.machineToken && data.machineId) {
|
|
58
|
+
if (opts.json)
|
|
59
|
+
emitPairJson({ type: 'pair.claimed' });
|
|
45
60
|
return {
|
|
46
61
|
machineToken: data.machineToken,
|
|
47
62
|
machineId: data.machineId,
|
|
@@ -61,11 +76,15 @@ function askConfirm(question) {
|
|
|
61
76
|
});
|
|
62
77
|
}
|
|
63
78
|
export async function runPairFlow(opts) {
|
|
64
|
-
const { serverUrl, machineName, force } = opts;
|
|
79
|
+
const { serverUrl, machineName, force, json } = opts;
|
|
65
80
|
const config = loadConfig();
|
|
66
81
|
if (config.machineToken && config.machineId && !force) {
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
}
|
|
69
88
|
return;
|
|
70
89
|
}
|
|
71
90
|
if (config.machineToken && !force)
|
|
@@ -74,9 +93,11 @@ export async function runPairFlow(opts) {
|
|
|
74
93
|
const agentList = detectedAgents.map((a) => a.type);
|
|
75
94
|
const platform = os.platform();
|
|
76
95
|
const osName = platform === 'win32' ? 'windows' : platform === 'darwin' ? 'macos' : 'linux';
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|
|
80
101
|
let res;
|
|
81
102
|
try {
|
|
82
103
|
res = await fetch(`${serverUrl}/api/machines/pair/create`, {
|
|
@@ -86,12 +107,18 @@ export async function runPairFlow(opts) {
|
|
|
86
107
|
});
|
|
87
108
|
}
|
|
88
109
|
catch {
|
|
89
|
-
|
|
110
|
+
const message = `Network error: cannot connect to ${serverUrl}`;
|
|
111
|
+
if (json)
|
|
112
|
+
failPairJson('NETWORK_ERROR', message);
|
|
113
|
+
console.error(chalk.red(`✗ ${message}`));
|
|
90
114
|
process.exit(1);
|
|
91
115
|
}
|
|
92
116
|
if (!res.ok) {
|
|
93
117
|
const body = await res.json().catch(() => ({}));
|
|
94
|
-
|
|
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}`));
|
|
95
122
|
process.exit(1);
|
|
96
123
|
}
|
|
97
124
|
const data = (await res.json());
|
|
@@ -99,21 +126,37 @@ export async function runPairFlow(opts) {
|
|
|
99
126
|
config.serverUrl = serverUrl;
|
|
100
127
|
saveConfig(config);
|
|
101
128
|
const pairUrl = buildPairQrPayload(data.pairToken);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 });
|
|
109
146
|
if (!result) {
|
|
147
|
+
if (json)
|
|
148
|
+
failPairJson('PAIR_TIMEOUT', 'Timed out waiting for pairing');
|
|
110
149
|
console.error(chalk.red('\n✗ Timed out. Pairing code expired, please try again.'));
|
|
111
150
|
process.exit(1);
|
|
112
151
|
}
|
|
113
152
|
config.machineToken = result.machineToken;
|
|
114
153
|
config.machineId = result.machineId;
|
|
115
154
|
saveConfig(config);
|
|
116
|
-
|
|
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
|
+
}
|
|
117
160
|
}
|
|
118
161
|
export async function registerDesktopMachine(opts) {
|
|
119
162
|
const config = loadConfig();
|
|
@@ -149,6 +192,18 @@ export async function registerDesktopMachine(opts) {
|
|
|
149
192
|
saveConfig(config);
|
|
150
193
|
return { machineId: data.machine.id, machineName: data.machine.name };
|
|
151
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
|
+
}
|
|
152
207
|
export async function runSmartStart(serverUrl, machineName) {
|
|
153
208
|
const config = loadConfig();
|
|
154
209
|
if (!config.machineToken) {
|
|
@@ -169,15 +224,16 @@ export function registerPairCommand(program) {
|
|
|
169
224
|
program
|
|
170
225
|
.command('desktop-register', { hidden: true })
|
|
171
226
|
.description('Register this desktop-managed machine to the authenticated account')
|
|
172
|
-
.
|
|
227
|
+
.option('--api <url>', 'Server URL')
|
|
173
228
|
.option('--name <name>', 'Machine name', os.hostname())
|
|
174
229
|
.action(async (opts) => {
|
|
175
230
|
try {
|
|
176
231
|
const token = process.env.SHENNIAN_DESKTOP_AUTH_TOKEN;
|
|
177
232
|
if (!token)
|
|
178
233
|
throw new Error('Missing desktop auth token');
|
|
234
|
+
const serverUrl = await resolveDesktopRegisterServerUrl(opts.api);
|
|
179
235
|
const result = await registerDesktopMachine({
|
|
180
|
-
serverUrl
|
|
236
|
+
serverUrl,
|
|
181
237
|
authToken: token,
|
|
182
238
|
machineName: opts.name,
|
|
183
239
|
});
|
|
@@ -192,10 +248,16 @@ export function registerPairCommand(program) {
|
|
|
192
248
|
.command('pair')
|
|
193
249
|
.description('Pair this machine to a Shennian account (re-pair if already paired)')
|
|
194
250
|
.option('--api <url>', 'Server URL override')
|
|
251
|
+
.option('--server <url>', 'Server URL override')
|
|
195
252
|
.option('--name <name>', 'Machine name', os.hostname())
|
|
253
|
+
.option('--json', 'Emit machine-readable JSONL events')
|
|
196
254
|
.action(async (opts) => {
|
|
197
255
|
const config = loadConfig();
|
|
198
256
|
if (config.machineToken) {
|
|
257
|
+
if (opts.json) {
|
|
258
|
+
emitPairJson({ type: 'done', status: 'already-paired', machineId: config.machineId });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
199
261
|
console.log(chalk.yellow(`Currently paired (machine ID: ${config.machineId})`));
|
|
200
262
|
const yes = await askConfirm('Re-pairing will unlink the current account. Continue? (y/N) ');
|
|
201
263
|
if (!yes) {
|
|
@@ -206,13 +268,18 @@ export function registerPairCommand(program) {
|
|
|
206
268
|
delete config.machineId;
|
|
207
269
|
saveConfig(config);
|
|
208
270
|
}
|
|
209
|
-
const serverUrl = opts.api ?? (await detectAndChooseServer());
|
|
210
|
-
await runPairFlow({ serverUrl, machineName: opts.name, force: true });
|
|
271
|
+
const serverUrl = opts.server ?? opts.api ?? (await detectAndChooseServer());
|
|
272
|
+
await runPairFlow({ serverUrl, machineName: opts.name, force: true, json: Boolean(opts.json) });
|
|
211
273
|
saveEnvSnapshot();
|
|
212
|
-
|
|
274
|
+
if (opts.json)
|
|
275
|
+
emitPairJson({ type: 'daemon.starting' });
|
|
276
|
+
else
|
|
277
|
+
console.log(chalk.gray('\nStarting background service...'));
|
|
213
278
|
const startedByService = installService();
|
|
214
279
|
if (!startedByService) {
|
|
215
|
-
startDaemonProcess();
|
|
280
|
+
startDaemonProcess({ quiet: Boolean(opts.json) });
|
|
216
281
|
}
|
|
282
|
+
if (opts.json)
|
|
283
|
+
emitPairJson({ type: 'done', status: 'paired', machineId: loadConfig().machineId });
|
|
217
284
|
});
|
|
218
285
|
}
|