shennian 0.2.89 → 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 (118) hide show
  1. package/dist/assets/wechat-channel/macos/manifest.json +13 -4
  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.js +1 -19
  8. package/dist/src/agents/claude.js +8 -305
  9. package/dist/src/agents/codex-control.js +2 -188
  10. package/dist/src/agents/codex-utils.js +7 -200
  11. package/dist/src/agents/codex.js +15 -916
  12. package/dist/src/agents/command-spec.js +2 -413
  13. package/dist/src/agents/config-status.js +1 -226
  14. package/dist/src/agents/cursor.js +1 -249
  15. package/dist/src/agents/custom.js +4 -271
  16. package/dist/src/agents/detect.js +1 -56
  17. package/dist/src/agents/external-channel-instructions.js +10 -94
  18. package/dist/src/agents/gemini.js +1 -173
  19. package/dist/src/agents/manager.js +13 -157
  20. package/dist/src/agents/model-registry/cache.js +1 -37
  21. package/dist/src/agents/model-registry/discovery.js +2 -187
  22. package/dist/src/agents/model-registry/parsers.js +4 -447
  23. package/dist/src/agents/model-registry/runner.js +1 -30
  24. package/dist/src/agents/model-registry/service.js +1 -78
  25. package/dist/src/agents/model-registry/types.js +1 -8
  26. package/dist/src/agents/model-registry.js +1 -18
  27. package/dist/src/agents/openclaw.js +2 -275
  28. package/dist/src/agents/opencode.js +1 -231
  29. package/dist/src/agents/pi-context.js +12 -217
  30. package/dist/src/agents/pi.js +14 -723
  31. package/dist/src/agents/platform-instructions.js +9 -54
  32. package/dist/src/channels/base.js +1 -3
  33. package/dist/src/channels/registry.js +1 -30
  34. package/dist/src/channels/reply-split.js +10 -89
  35. package/dist/src/channels/runtime.js +5 -564
  36. package/dist/src/channels/secret-registry.js +1 -46
  37. package/dist/src/channels/websocket.js +8 -378
  38. package/dist/src/channels/wechat-channel/anchor.js +1 -65
  39. package/dist/src/channels/wechat-channel/client.js +1 -96
  40. package/dist/src/channels/wechat-channel/cooldown.js +1 -38
  41. package/dist/src/channels/wechat-channel/fingerprint.js +1 -71
  42. package/dist/src/channels/wechat-channel/helper-assets.d.ts +10 -1
  43. package/dist/src/channels/wechat-channel/helper-assets.js +1 -68
  44. package/dist/src/channels/wechat-channel/helper-client.js +3 -149
  45. package/dist/src/channels/wechat-channel/helper-protocol.d.ts +1 -1
  46. package/dist/src/channels/wechat-channel/helper-protocol.js +1 -115
  47. package/dist/src/channels/wechat-channel/index.d.ts +1 -0
  48. package/dist/src/channels/wechat-channel/index.js +1 -19
  49. package/dist/src/channels/wechat-channel/ledger.js +1 -54
  50. package/dist/src/channels/wechat-channel/media-resolver.js +1 -181
  51. package/dist/src/channels/wechat-channel/message-key.js +1 -105
  52. package/dist/src/channels/wechat-channel/observer.js +1 -118
  53. package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +3 -0
  54. package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -112
  55. package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
  56. package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
  57. package/dist/src/channels/wechat-channel/preflight.js +1 -48
  58. package/dist/src/channels/wechat-channel/runner.js +1 -84
  59. package/dist/src/channels/wechat-channel/runtime.js +1 -66
  60. package/dist/src/channels/wechat-channel/scheduler.d.ts +5 -0
  61. package/dist/src/channels/wechat-channel/scheduler.js +1 -152
  62. package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
  63. package/dist/src/channels/wechat-rpa/macos.js +6 -48
  64. package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
  65. package/dist/src/channels/wechat-rpa.js +6 -1028
  66. package/dist/src/channels/wecom.js +4 -357
  67. package/dist/src/commands/agent.js +6 -131
  68. package/dist/src/commands/daemon-windows.js +8 -48
  69. package/dist/src/commands/daemon.js +19 -1013
  70. package/dist/src/commands/external-attachments.js +1 -51
  71. package/dist/src/commands/external.js +1 -137
  72. package/dist/src/commands/manager.js +2 -391
  73. package/dist/src/commands/pair-qr.js +1 -6
  74. package/dist/src/commands/pair.js +9 -287
  75. package/dist/src/commands/tools.js +1 -34
  76. package/dist/src/commands/upgrade.js +1 -198
  77. package/dist/src/config/index.js +1 -35
  78. package/dist/src/daemon-log.js +6 -58
  79. package/dist/src/env-path.js +1 -64
  80. package/dist/src/fs/boundary.js +1 -126
  81. package/dist/src/fs/handler.js +1 -130
  82. package/dist/src/fs/security.js +1 -32
  83. package/dist/src/fs/text-decoder.js +1 -110
  84. package/dist/src/index.js +2 -404
  85. package/dist/src/log-reporter.js +1 -16
  86. package/dist/src/manager/prompt.js +29 -34
  87. package/dist/src/manager/registry.js +2 -269
  88. package/dist/src/manager/runtime.js +19 -1007
  89. package/dist/src/native-fusion/config.js +1 -5
  90. package/dist/src/native-fusion/opencode-parser.js +3 -123
  91. package/dist/src/native-fusion/parser-common.js +8 -264
  92. package/dist/src/native-fusion/parsers.js +8 -729
  93. package/dist/src/native-fusion/service.js +2 -225
  94. package/dist/src/native-fusion/state.js +1 -22
  95. package/dist/src/native-fusion/types.js +1 -1
  96. package/dist/src/region.js +1 -88
  97. package/dist/src/relay/client.js +1 -343
  98. package/dist/src/session/archive-zip.js +1 -220
  99. package/dist/src/session/handlers/agent-config.js +1 -150
  100. package/dist/src/session/handlers/agents.js +1 -55
  101. package/dist/src/session/handlers/chat.js +2 -751
  102. package/dist/src/session/handlers/control.js +1 -55
  103. package/dist/src/session/handlers/fs.js +1 -783
  104. package/dist/src/session/handlers/session-refresh.js +1 -47
  105. package/dist/src/session/handlers/skills.js +1 -121
  106. package/dist/src/session/handlers/title.js +1 -60
  107. package/dist/src/session/handlers/tool-detail.js +1 -218
  108. package/dist/src/session/manager.js +1 -319
  109. package/dist/src/session/projection.js +1 -54
  110. package/dist/src/session/queue.js +4 -317
  111. package/dist/src/session/remote-attachments.js +1 -72
  112. package/dist/src/session/store.js +3 -109
  113. package/dist/src/session/types.js +1 -4
  114. package/dist/src/skills/registry.js +15 -148
  115. package/dist/src/skills/setup.js +1 -101
  116. package/dist/src/tools/markdown-to-pdf.js +10 -346
  117. package/dist/src/upgrade/engine.js +3 -347
  118. package/package.json +3 -2
@@ -1,413 +1,2 @@
1
- // @arch docs/architecture/cli/windows-agent-launch.md
2
- // @test src/__tests__/command-spec.test.ts
3
- import { spawn, spawnSync } from 'node:child_process';
4
- import fs from 'node:fs';
5
- import os from 'node:os';
6
- import path from 'node:path';
7
- const BUILTIN_COMMANDS = {
8
- claude: [
9
- { command: 'claude', args: [], display: 'claude' },
10
- ],
11
- codex: [
12
- { command: 'codex', args: [], display: 'codex' },
13
- ],
14
- gemini: [
15
- { command: 'gemini', args: [], display: 'gemini' },
16
- ],
17
- cursor: [
18
- { command: 'agent', args: [], display: 'agent' },
19
- { command: 'cursor', args: ['agent'], display: 'cursor agent' },
20
- ],
21
- // OpenClaw support is intentionally disabled.
22
- openclaw: [],
23
- opencode: [
24
- { command: 'opencode', args: [], display: 'opencode' },
25
- ],
26
- pi: [
27
- { command: 'shennian', args: [], display: 'shennian' },
28
- ],
29
- manager: [
30
- { command: 'shennian', args: ['manager'], display: 'shennian manager' },
31
- ],
32
- };
33
- const availabilityCache = new Map();
34
- const resolvedCache = new Map();
35
- const pathLookupCache = new Map();
36
- const wslAvailabilityCache = new Map();
37
- export function resetCommandSpecCache() {
38
- availabilityCache.clear();
39
- resolvedCache.clear();
40
- pathLookupCache.clear();
41
- wslAvailabilityCache.clear();
42
- }
43
- export function getProcessPlatform() {
44
- return process.platform;
45
- }
46
- function splitLines(text) {
47
- return text
48
- .split(/\r?\n/)
49
- .map((line) => line.trim())
50
- .filter(Boolean);
51
- }
52
- function getFallbackCommandCandidates(command) {
53
- if (path.basename(command) !== command)
54
- return [command];
55
- const home = os.homedir();
56
- if (getProcessPlatform() === 'win32') {
57
- const winPath = path.win32;
58
- const names = path.extname(command)
59
- ? [command]
60
- : [command, `${command}.cmd`, `${command}.exe`, `${command}.bat`];
61
- const appData = process.env.APPDATA || winPath.join(home, 'AppData', 'Roaming');
62
- const localAppData = process.env.LOCALAPPDATA || winPath.join(home, 'AppData', 'Local');
63
- const dirs = [
64
- winPath.join(home, '.shennian', 'node'),
65
- winPath.join('C:\\', 'nvm4w', 'nodejs'),
66
- winPath.join(localAppData, 'npm-global'),
67
- winPath.join(appData, 'npm'),
68
- winPath.join(localAppData, 'pnpm'),
69
- winPath.join(home, 'scoop', 'shims'),
70
- winPath.join('C:\\', 'Program Files', 'nodejs'),
71
- ];
72
- return dirs.flatMap((dir) => names.map((name) => winPath.join(dir, name)));
73
- }
74
- const dirs = [
75
- path.join(home, '.npm-global', 'bin'),
76
- path.join(home, '.local', 'bin'),
77
- path.join(home, '.bun', 'bin'),
78
- '/opt/homebrew/bin',
79
- '/usr/local/bin',
80
- '/usr/bin',
81
- '/bin',
82
- ];
83
- return dirs.map((dir) => path.join(dir, command));
84
- }
85
- function buildFallbackPathEnv(currentPath, command) {
86
- const parts = currentPath?.split(path.delimiter).filter(Boolean) ?? [];
87
- if (getProcessPlatform() === 'win32') {
88
- if (command && path.basename(command) !== command) {
89
- const commandDir = path.win32.dirname(command);
90
- if (!parts.includes(commandDir))
91
- parts.unshift(commandDir);
92
- }
93
- for (const candidate of getFallbackCommandCandidates('shennian')) {
94
- const dir = path.win32.dirname(candidate);
95
- if (!parts.includes(dir))
96
- parts.push(dir);
97
- }
98
- return parts.join(path.delimiter);
99
- }
100
- if (command && path.basename(command) !== command) {
101
- const commandDir = path.dirname(command);
102
- if (!parts.includes(commandDir))
103
- parts.unshift(commandDir);
104
- }
105
- for (const candidate of getFallbackCommandCandidates('shennian')) {
106
- const dir = path.dirname(candidate);
107
- if (!parts.includes(dir))
108
- parts.push(dir);
109
- }
110
- return parts.join(path.delimiter);
111
- }
112
- function lookupCommandPaths(command) {
113
- if (pathLookupCache.has(command)) {
114
- return pathLookupCache.get(command) ?? [];
115
- }
116
- const isWindows = getProcessPlatform() === 'win32';
117
- const lookup = isWindows ? 'where' : 'which';
118
- const result = spawnSync(lookup, [command], {
119
- encoding: 'utf-8',
120
- stdio: ['ignore', 'pipe', 'pipe'],
121
- timeout: 3000,
122
- windowsHide: true,
123
- env: {
124
- ...process.env,
125
- PATH: isWindows ? process.env.PATH : buildFallbackPathEnv(process.env.PATH, command),
126
- },
127
- });
128
- const paths = result.status === 0 ? splitLines(result.stdout ?? '') : [];
129
- const withFallbacks = [...paths];
130
- for (const candidate of getFallbackCommandCandidates(command)) {
131
- if (fs.existsSync(candidate) && !withFallbacks.includes(candidate))
132
- withFallbacks.push(candidate);
133
- }
134
- pathLookupCache.set(command, withFallbacks);
135
- return withFallbacks;
136
- }
137
- function getWindowsShellPath() {
138
- return process.env.ComSpec || 'cmd.exe';
139
- }
140
- function isWindowsNativeBinary(filePath) {
141
- const ext = path.extname(filePath).toLowerCase();
142
- return ext === '.exe' || ext === '.com';
143
- }
144
- function isWindowsCmdShim(filePath) {
145
- const ext = path.extname(filePath).toLowerCase();
146
- return ext === '.cmd' || ext === '.bat';
147
- }
148
- function isWindowsNodeScript(filePath) {
149
- const ext = path.extname(filePath).toLowerCase();
150
- return ext === '.js' || ext === '.mjs' || ext === '.cjs';
151
- }
152
- function resolveWindowsNodeCommand() {
153
- return lookupCommandPaths('node.exe')[0] ?? lookupCommandPaths('node')[0] ?? 'node';
154
- }
155
- function quoteCmdArg(text) {
156
- return `"${text.replace(/"/g, '""')}"`;
157
- }
158
- function quotePosixArg(text) {
159
- return `'${text.replace(/'/g, `'\\''`)}'`;
160
- }
161
- export function windowsPathToWsl(input) {
162
- if (!input)
163
- return undefined;
164
- const normalized = input.replace(/\//g, '\\');
165
- const driveMatch = normalized.match(/^([A-Za-z]):\\(.*)$/);
166
- if (driveMatch) {
167
- const [, drive, rest] = driveMatch;
168
- const suffix = rest.replace(/\\/g, '/');
169
- return `/mnt/${drive.toLowerCase()}/${suffix}`;
170
- }
171
- if (normalized.startsWith('\\\\wsl$\\')) {
172
- const remainder = normalized.replace(/^\\\\wsl\$\\[^\\]+\\?/, '');
173
- return `/${remainder.replace(/\\/g, '/')}`;
174
- }
175
- if (input.startsWith('/'))
176
- return input;
177
- return undefined;
178
- }
179
- function resolveArbitraryWindowsCommand(command) {
180
- const normalized = command.trim();
181
- if (!normalized) {
182
- throw new Error('Command string is empty');
183
- }
184
- const hasExplicitPath = /[\\/]/.test(normalized) || /^[A-Za-z]:/.test(normalized);
185
- if (hasExplicitPath) {
186
- if (isWindowsCmdShim(normalized))
187
- return { kind: 'cmd-shim', path: normalized };
188
- return { kind: 'direct', path: normalized };
189
- }
190
- const paths = lookupCommandPaths(normalized);
191
- const native = paths.find(isWindowsNativeBinary);
192
- if (native)
193
- return { kind: 'direct', path: native };
194
- const shim = paths.find(isWindowsCmdShim);
195
- if (shim)
196
- return { kind: 'cmd-shim', path: shim };
197
- return { kind: 'direct', path: normalized };
198
- }
199
- function canUseWslBridge(command) {
200
- if (wslAvailabilityCache.has(command)) {
201
- return wslAvailabilityCache.get(command) ?? false;
202
- }
203
- const wslPath = lookupCommandPaths('wsl.exe')[0] ?? lookupCommandPaths('wsl')[0];
204
- if (!wslPath) {
205
- wslAvailabilityCache.set(command, false);
206
- return false;
207
- }
208
- const result = spawnSync(wslPath, ['-e', 'sh', '-lc', `command -v ${quotePosixArg(command)} >/dev/null 2>&1`], {
209
- stdio: 'ignore',
210
- timeout: 5000,
211
- windowsHide: true,
212
- });
213
- const ok = result.status === 0;
214
- wslAvailabilityCache.set(command, ok);
215
- return ok;
216
- }
217
- export function commandExists(command) {
218
- if (availabilityCache.has(command)) {
219
- return availabilityCache.get(command) ?? false;
220
- }
221
- const exists = lookupCommandPaths(command).length > 0 || (getProcessPlatform() === 'win32' && canUseWslBridge(command));
222
- availabilityCache.set(command, exists);
223
- return exists;
224
- }
225
- function resolveWindowsBuiltinCommand(candidate) {
226
- const paths = lookupCommandPaths(candidate.command);
227
- const native = paths.find(isWindowsNativeBinary);
228
- if (native) {
229
- return { ...candidate, kind: 'native', path: native };
230
- }
231
- const shim = paths.find(isWindowsCmdShim);
232
- if (shim) {
233
- return { ...candidate, kind: 'cmd-shim', path: shim };
234
- }
235
- if (canUseWslBridge(candidate.command)) {
236
- const wslPath = lookupCommandPaths('wsl.exe')[0] ?? lookupCommandPaths('wsl')[0];
237
- if (wslPath) {
238
- return { ...candidate, kind: 'wsl-bridge', path: wslPath };
239
- }
240
- }
241
- return null;
242
- }
243
- export function resolveBuiltinCommand(type) {
244
- if (resolvedCache.has(type)) {
245
- return resolvedCache.get(type) ?? null;
246
- }
247
- const resolved = BUILTIN_COMMANDS[type]
248
- .map((candidate) => {
249
- if (getProcessPlatform() === 'win32') {
250
- return resolveWindowsBuiltinCommand(candidate);
251
- }
252
- const commandPath = lookupCommandPaths(candidate.command)[0];
253
- return commandPath ? { ...candidate, kind: 'native', path: commandPath } : null;
254
- })
255
- .find(Boolean) ?? null;
256
- resolvedCache.set(type, resolved);
257
- return resolved;
258
- }
259
- export function buildLaunchSpec(spec, runtimeArgs, cwd) {
260
- if (spec.kind === 'native') {
261
- if (getProcessPlatform() === 'win32' && isWindowsNodeScript(spec.path)) {
262
- return {
263
- command: resolveWindowsNodeCommand(),
264
- args: [spec.path, ...spec.args, ...runtimeArgs],
265
- cwd,
266
- };
267
- }
268
- return {
269
- command: spec.path,
270
- args: [...spec.args, ...runtimeArgs],
271
- cwd,
272
- };
273
- }
274
- if (spec.kind === 'cmd-shim') {
275
- const commandLine = [spec.path, ...spec.args, ...runtimeArgs].map(quoteCmdArg).join(' ');
276
- return {
277
- command: getWindowsShellPath(),
278
- args: ['/d', '/s', '/c', `"${commandLine}"`],
279
- cwd,
280
- };
281
- }
282
- const wslCwd = cwd ? windowsPathToWsl(cwd) : undefined;
283
- if (cwd && !wslCwd) {
284
- throw new Error(`Cannot map Windows path to WSL workDir: ${cwd}`);
285
- }
286
- const shellCommand = [
287
- wslCwd ? `cd ${quotePosixArg(wslCwd)}` : '',
288
- `exec ${[spec.command, ...spec.args, ...runtimeArgs].map(quotePosixArg).join(' ')}`,
289
- ].filter(Boolean).join(' && ');
290
- return {
291
- command: spec.path,
292
- args: ['-e', 'sh', '-lc', shellCommand],
293
- cwd: undefined,
294
- };
295
- }
296
- export function spawnResolvedCommand(spec, runtimeArgs, options = {}) {
297
- const launch = buildLaunchSpec(spec, runtimeArgs, options.cwd);
298
- return spawn(launch.command, launch.args, {
299
- ...options,
300
- cwd: launch.cwd,
301
- windowsHide: options.windowsHide ?? true,
302
- ...(getProcessPlatform() === 'win32' && spec.kind === 'cmd-shim'
303
- ? { windowsVerbatimArguments: true }
304
- : {}),
305
- });
306
- }
307
- export function spawnAgentCommand(spec, runtimeArgs, options = {}) {
308
- return spawnResolvedCommand(spec, runtimeArgs, options);
309
- }
310
- export function spawnResolvedCommandSync(spec, runtimeArgs, options = {}) {
311
- const launch = buildLaunchSpec(spec, runtimeArgs, options.cwd);
312
- return spawnSync(launch.command, launch.args, {
313
- ...options,
314
- cwd: launch.cwd,
315
- windowsHide: options.windowsHide ?? true,
316
- ...(getProcessPlatform() === 'win32' && spec.kind === 'cmd-shim'
317
- ? { windowsVerbatimArguments: true }
318
- : {}),
319
- });
320
- }
321
- export function parseCommandString(command) {
322
- const args = [];
323
- let current = '';
324
- let inSingle = false;
325
- let inDouble = false;
326
- for (const ch of command) {
327
- if (ch === "'" && !inDouble) {
328
- inSingle = !inSingle;
329
- continue;
330
- }
331
- if (ch === '"' && !inSingle) {
332
- inDouble = !inDouble;
333
- continue;
334
- }
335
- if (ch === ' ' && !inSingle && !inDouble) {
336
- if (current) {
337
- args.push(current);
338
- current = '';
339
- }
340
- continue;
341
- }
342
- current += ch;
343
- }
344
- if (current)
345
- args.push(current);
346
- return args;
347
- }
348
- export function buildCommandStringLaunchSpec(commandString, runtimeArgs, cwd) {
349
- const parts = parseCommandString(commandString);
350
- const bin = parts[0];
351
- if (!bin) {
352
- throw new Error('Command string is empty');
353
- }
354
- const baseArgs = parts.slice(1);
355
- if (getProcessPlatform() !== 'win32') {
356
- return {
357
- command: bin,
358
- args: [...baseArgs, ...runtimeArgs],
359
- cwd,
360
- };
361
- }
362
- const invocation = resolveArbitraryWindowsCommand(bin);
363
- if (invocation.kind === 'cmd-shim') {
364
- return {
365
- command: getWindowsShellPath(),
366
- args: ['/d', '/s', '/c', `"${[invocation.path, ...baseArgs, ...runtimeArgs].map(quoteCmdArg).join(' ')}"`],
367
- cwd,
368
- };
369
- }
370
- if (isWindowsNodeScript(invocation.path)) {
371
- return {
372
- command: resolveWindowsNodeCommand(),
373
- args: [invocation.path, ...baseArgs, ...runtimeArgs],
374
- cwd,
375
- };
376
- }
377
- return {
378
- command: invocation.path,
379
- args: [...baseArgs, ...runtimeArgs],
380
- cwd,
381
- };
382
- }
383
- export function spawnCommandString(commandString, runtimeArgs, options = {}) {
384
- const launch = buildCommandStringLaunchSpec(commandString, runtimeArgs, options.cwd);
385
- const parts = parseCommandString(commandString);
386
- const invocation = getProcessPlatform() === 'win32' && parts[0]
387
- ? resolveArbitraryWindowsCommand(parts[0])
388
- : null;
389
- return spawn(launch.command, launch.args, {
390
- ...options,
391
- cwd: launch.cwd,
392
- windowsHide: options.windowsHide ?? true,
393
- env: {
394
- ...options.env,
395
- PATH: buildFallbackPathEnv(options.env?.PATH ?? process.env.PATH, parts[0]),
396
- },
397
- ...(getProcessPlatform() === 'win32' && invocation?.kind === 'cmd-shim'
398
- ? { windowsVerbatimArguments: true }
399
- : {}),
400
- });
401
- }
402
- export function getCommandVersion(spec, flag = '--version') {
403
- const result = spawnResolvedCommandSync(spec, [flag], {
404
- encoding: 'utf-8',
405
- stdio: ['ignore', 'pipe', 'pipe'],
406
- timeout: 5000,
407
- });
408
- if (result.status !== 0)
409
- return undefined;
410
- const text = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
411
- const match = text.match(/\d+\.\d+[\w.-]*/);
412
- return match?.[0];
413
- }
1
+ import{spawn as x,spawnSync as p}from"node:child_process";import W from"node:fs";import D from"node:os";import s from"node:path";const T={claude:[{command:"claude",args:[],display:"claude"}],codex:[{command:"codex",args:[],display:"codex"}],gemini:[{command:"gemini",args:[],display:"gemini"}],cursor:[{command:"agent",args:[],display:"agent"},{command:"cursor",args:["agent"],display:"cursor agent"}],openclaw:[],opencode:[{command:"opencode",args:[],display:"opencode"}],pi:[{command:"shennian",args:[],display:"shennian"}],manager:[{command:"shennian",args:["manager"],display:"shennian manager"}]},l=new Map,m=new Map,f=new Map,u=new Map;function U(){l.clear(),m.clear(),f.clear(),u.clear()}function c(){return process.platform}function B(n){return n.split(/\r?\n/).map(e=>e.trim()).filter(Boolean)}function h(n){if(s.basename(n)!==n)return[n];const e=D.homedir();if(c()==="win32"){const t=s.win32,r=s.extname(n)?[n]:[n,`${n}.cmd`,`${n}.exe`,`${n}.bat`],o=process.env.APPDATA||t.join(e,"AppData","Roaming"),a=process.env.LOCALAPPDATA||t.join(e,"AppData","Local");return[t.join(e,".shennian","node"),t.join("C:\\","nvm4w","nodejs"),t.join(a,"npm-global"),t.join(o,"npm"),t.join(a,"pnpm"),t.join(e,"scoop","shims"),t.join("C:\\","Program Files","nodejs")].flatMap(H=>r.map(L=>t.join(H,L)))}return[s.join(e,".npm-global","bin"),s.join(e,".local","bin"),s.join(e,".bun","bin"),"/opt/homebrew/bin","/usr/local/bin","/usr/bin","/bin"].map(t=>s.join(t,n))}function v(n,e){const i=n?.split(s.delimiter).filter(Boolean)??[];if(c()==="win32"){if(e&&s.basename(e)!==e){const t=s.win32.dirname(e);i.includes(t)||i.unshift(t)}for(const t of h("shennian")){const r=s.win32.dirname(t);i.includes(r)||i.push(r)}return i.join(s.delimiter)}if(e&&s.basename(e)!==e){const t=s.dirname(e);i.includes(t)||i.unshift(t)}for(const t of h("shennian")){const r=s.dirname(t);i.includes(r)||i.push(r)}return i.join(s.delimiter)}function d(n){if(f.has(n))return f.get(n)??[];const e=c()==="win32",t=p(e?"where":"which",[n],{encoding:"utf-8",stdio:["ignore","pipe","pipe"],timeout:3e3,windowsHide:!0,env:{...process.env,PATH:e?process.env.PATH:v(process.env.PATH,n)}}),o=[...t.status===0?B(t.stdout??""):[]];for(const a of h(n))W.existsSync(a)&&!o.includes(a)&&o.push(a);return f.set(n,o),o}function C(){return process.env.ComSpec||"cmd.exe"}function b(n){const e=s.extname(n).toLowerCase();return e===".exe"||e===".com"}function w(n){const e=s.extname(n).toLowerCase();return e===".cmd"||e===".bat"}function j(n){const e=s.extname(n).toLowerCase();return e===".js"||e===".mjs"||e===".cjs"}function k(){return d("node.exe")[0]??d("node")[0]??"node"}function P(n){return`"${n.replace(/"/g,'""')}"`}function g(n){return`'${n.replace(/'/g,"'\\''")}'`}function M(n){if(!n)return;const e=n.replace(/\//g,"\\"),i=e.match(/^([A-Za-z]):\\(.*)$/);if(i){const[,t,r]=i,o=r.replace(/\\/g,"/");return`/mnt/${t.toLowerCase()}/${o}`}if(e.startsWith("\\\\wsl$\\"))return`/${e.replace(/^\\\\wsl\$\\[^\\]+\\?/,"").replace(/\\/g,"/")}`;if(n.startsWith("/"))return n}function A(n){const e=n.trim();if(!e)throw new Error("Command string is empty");if(/[\\/]/.test(e)||/^[A-Za-z]:/.test(e))return w(e)?{kind:"cmd-shim",path:e}:{kind:"direct",path:e};const t=d(e),r=t.find(b);if(r)return{kind:"direct",path:r};const o=t.find(w);return o?{kind:"cmd-shim",path:o}:{kind:"direct",path:e}}function $(n){if(u.has(n))return u.get(n)??!1;const e=d("wsl.exe")[0]??d("wsl")[0];if(!e)return u.set(n,!1),!1;const t=p(e,["-e","sh","-lc",`command -v ${g(n)} >/dev/null 2>&1`],{stdio:"ignore",timeout:5e3,windowsHide:!0}).status===0;return u.set(n,t),t}function Z(n){if(l.has(n))return l.get(n)??!1;const e=d(n).length>0||c()==="win32"&&$(n);return l.set(n,e),e}function E(n){const e=d(n.command),i=e.find(b);if(i)return{...n,kind:"native",path:i};const t=e.find(w);if(t)return{...n,kind:"cmd-shim",path:t};if($(n.command)){const r=d("wsl.exe")[0]??d("wsl")[0];if(r)return{...n,kind:"wsl-bridge",path:r}}return null}function _(n){if(m.has(n))return m.get(n)??null;const e=T[n].map(i=>{if(c()==="win32")return E(i);const t=d(i.command)[0];return t?{...i,kind:"native",path:t}:null}).find(Boolean)??null;return m.set(n,e),e}function S(n,e,i){if(n.kind==="native")return c()==="win32"&&j(n.path)?{command:k(),args:[n.path,...n.args,...e],cwd:i}:{command:n.path,args:[...n.args,...e],cwd:i};if(n.kind==="cmd-shim"){const o=[n.path,...n.args,...e].map(P).join(" ");return{command:C(),args:["/d","/s","/c",`"${o}"`],cwd:i}}const t=i?M(i):void 0;if(i&&!t)throw new Error(`Cannot map Windows path to WSL workDir: ${i}`);const r=[t?`cd ${g(t)}`:"",`exec ${[n.command,...n.args,...e].map(g).join(" ")}`].filter(Boolean).join(" && ");return{command:n.path,args:["-e","sh","-lc",r],cwd:void 0}}function N(n,e,i={}){const t=S(n,e,i.cwd);return x(t.command,t.args,{...i,cwd:t.cwd,windowsHide:i.windowsHide??!0,...c()==="win32"&&n.kind==="cmd-shim"?{windowsVerbatimArguments:!0}:{}})}function G(n,e,i={}){return N(n,e,i)}function z(n,e,i={}){const t=S(n,e,i.cwd);return p(t.command,t.args,{...i,cwd:t.cwd,windowsHide:i.windowsHide??!0,...c()==="win32"&&n.kind==="cmd-shim"?{windowsVerbatimArguments:!0}:{}})}function y(n){const e=[];let i="",t=!1,r=!1;for(const o of n){if(o==="'"&&!r){t=!t;continue}if(o==='"'&&!t){r=!r;continue}if(o===" "&&!t&&!r){i&&(e.push(i),i="");continue}i+=o}return i&&e.push(i),e}function F(n,e,i){const t=y(n),r=t[0];if(!r)throw new Error("Command string is empty");const o=t.slice(1);if(c()!=="win32")return{command:r,args:[...o,...e],cwd:i};const a=A(r);return a.kind==="cmd-shim"?{command:C(),args:["/d","/s","/c",`"${[a.path,...o,...e].map(P).join(" ")}"`],cwd:i}:j(a.path)?{command:k(),args:[a.path,...o,...e],cwd:i}:{command:a.path,args:[...o,...e],cwd:i}}function J(n,e,i={}){const t=F(n,e,i.cwd),r=y(n),o=c()==="win32"&&r[0]?A(r[0]):null;return x(t.command,t.args,{...i,cwd:t.cwd,windowsHide:i.windowsHide??!0,env:{...i.env,PATH:v(i.env?.PATH??process.env.PATH,r[0])},...c()==="win32"&&o?.kind==="cmd-shim"?{windowsVerbatimArguments:!0}:{}})}function K(n,e="--version"){const i=z(n,[e],{encoding:"utf-8",stdio:["ignore","pipe","pipe"],timeout:5e3});return i.status!==0?void 0:`${i.stdout??""}
2
+ ${i.stderr??""}`.trim().match(/\d+\.\d+[\w.-]*/)?.[0]}export{F as buildCommandStringLaunchSpec,S as buildLaunchSpec,Z as commandExists,K as getCommandVersion,c as getProcessPlatform,y as parseCommandString,U as resetCommandSpecCache,_ as resolveBuiltinCommand,G as spawnAgentCommand,J as spawnCommandString,N as spawnResolvedCommand,z as spawnResolvedCommandSync,M as windowsPathToWsl};
@@ -1,226 +1 @@
1
- // @arch docs/features/agent-provider-config.md
2
- // @test src/__tests__/agent-config-status.test.ts
3
- import fs from 'node:fs';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
- import { readLatestUserEnv } from '../agent-env.js';
7
- import { loadConfig, resolveShennianPath } from '../config/index.js';
8
- const MANAGED_CONFIG_PATH = resolveShennianPath('agent-provider-config.json');
9
- const TOKEN_SUFFIX_LENGTH = 4;
10
- function isConfigurableAgent(agent) {
11
- return agent === 'codex' || agent === 'claude' || agent === 'pi';
12
- }
13
- const AGENT_ENV_KEYS = {
14
- codex: {
15
- baseUrl: ['OPENAI_BASE_URL', 'OPENAI_API_BASE', 'OPENAI_API_URL'],
16
- token: ['OPENAI_API_KEY', 'OPENAI_TOKEN'],
17
- },
18
- claude: {
19
- baseUrl: ['ANTHROPIC_BASE_URL', 'ANTHROPIC_API_URL'],
20
- token: ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'],
21
- },
22
- pi: {
23
- baseUrl: ['DASHSCOPE_BASE_URL', 'DASHSCOPE_API_BASE', 'DASHSCOPE_API_URL'],
24
- token: ['DASHSCOPE_API_KEY', 'DASHSCOPE_TOKEN'],
25
- },
26
- };
27
- export function getManagedAgentProviderConfig(agent) {
28
- return loadManagedConfig().configs[agent];
29
- }
30
- export function upsertManagedAgentProviderConfig(input) {
31
- const file = loadManagedConfig();
32
- const existing = file.configs[input.agent];
33
- const record = {
34
- agent: input.agent,
35
- baseUrl: input.baseUrl?.trim() || existing?.baseUrl,
36
- token: input.token?.trim() || existing?.token,
37
- updatedAt: new Date().toISOString(),
38
- };
39
- file.configs[input.agent] = record;
40
- saveManagedConfig(file);
41
- return record;
42
- }
43
- export function deleteManagedAgentProviderConfig(agent) {
44
- const file = loadManagedConfig();
45
- delete file.configs[agent];
46
- saveManagedConfig(file);
47
- }
48
- export function buildManagedAgentEnv(agent) {
49
- if (!isConfigurableAgent(agent))
50
- return {};
51
- const config = getManagedAgentProviderConfig(agent);
52
- if (!config)
53
- return {};
54
- if (agent === 'codex') {
55
- return compactEnv({
56
- OPENAI_BASE_URL: config.baseUrl,
57
- OPENAI_API_KEY: config.token,
58
- });
59
- }
60
- if (agent === 'claude') {
61
- return compactEnv({
62
- ANTHROPIC_BASE_URL: config.baseUrl,
63
- ANTHROPIC_API_KEY: config.token,
64
- });
65
- }
66
- return compactEnv({
67
- DASHSCOPE_BASE_URL: config.baseUrl,
68
- DASHSCOPE_API_KEY: config.token,
69
- });
70
- }
71
- export function getAgentConfigSummary(agent, env = readLatestUserEnv()) {
72
- if (!isConfigurableAgent(agent))
73
- return undefined;
74
- const managed = getManagedAgentProviderConfig(agent);
75
- if (managed?.token || managed?.baseUrl) {
76
- return toSummary({
77
- agent,
78
- source: 'shennian',
79
- status: managed.token ? 'managed' : 'missing',
80
- baseUrl: managed.baseUrl,
81
- token: managed.token,
82
- tokenPresent: !!managed.token,
83
- });
84
- }
85
- const envConfig = detectFromEnv(agent, env);
86
- if (envConfig.tokenPresent || envConfig.baseUrl) {
87
- return toSummary({
88
- agent,
89
- source: 'env',
90
- status: envConfig.tokenPresent ? 'detected' : 'missing',
91
- baseUrl: envConfig.baseUrl,
92
- token: envConfig.token,
93
- tokenPresent: envConfig.tokenPresent,
94
- });
95
- }
96
- const fileConfig = detectFromKnownConfigFiles(agent);
97
- if (fileConfig.tokenPresent || fileConfig.baseUrl) {
98
- return toSummary({
99
- agent,
100
- source: 'config-file',
101
- status: fileConfig.tokenPresent ? 'detected' : 'missing',
102
- baseUrl: fileConfig.baseUrl,
103
- token: fileConfig.token,
104
- tokenPresent: fileConfig.tokenPresent,
105
- });
106
- }
107
- return toSummary({
108
- agent,
109
- source: 'unknown',
110
- status: 'missing',
111
- tokenPresent: false,
112
- });
113
- }
114
- function loadManagedConfig() {
115
- try {
116
- const parsed = JSON.parse(fs.readFileSync(MANAGED_CONFIG_PATH, 'utf-8'));
117
- return { configs: parsed.configs ?? {} };
118
- }
119
- catch {
120
- return { configs: {} };
121
- }
122
- }
123
- function saveManagedConfig(file) {
124
- fs.mkdirSync(path.dirname(MANAGED_CONFIG_PATH), { recursive: true });
125
- fs.writeFileSync(MANAGED_CONFIG_PATH, JSON.stringify(file, null, 2), { mode: 0o600 });
126
- try {
127
- fs.chmodSync(MANAGED_CONFIG_PATH, 0o600);
128
- }
129
- catch {
130
- // Best effort on filesystems without POSIX modes.
131
- }
132
- }
133
- function compactEnv(env) {
134
- const compacted = {};
135
- for (const [key, value] of Object.entries(env)) {
136
- if (value)
137
- compacted[key] = value;
138
- }
139
- return compacted;
140
- }
141
- function detectFromEnv(agent, env) {
142
- const keys = AGENT_ENV_KEYS[agent];
143
- const baseUrl = firstEnv(env, keys.baseUrl);
144
- const token = firstEnv(env, keys.token);
145
- return { baseUrl, token, tokenPresent: !!token };
146
- }
147
- function firstEnv(env, keys) {
148
- for (const key of keys) {
149
- const value = env[key]?.trim();
150
- if (value)
151
- return value;
152
- }
153
- return undefined;
154
- }
155
- function detectFromKnownConfigFiles(agent) {
156
- const home = os.homedir();
157
- if (agent === 'pi') {
158
- const config = loadConfig();
159
- const token = config.apiKeys?.dashscope?.trim();
160
- if (token)
161
- return { token, tokenPresent: true };
162
- }
163
- const candidates = agent === 'codex'
164
- ? [path.join(home, '.codex', 'config.toml'), path.join(home, '.codex', 'auth.json')]
165
- : agent === 'claude'
166
- ? [path.join(home, '.claude', 'settings.json'), path.join(home, '.claude.json')]
167
- : [path.join(home, '.dashscope', 'config.json')];
168
- for (const file of candidates) {
169
- const text = readSmallTextFile(file);
170
- if (!text)
171
- continue;
172
- const token = extractTokenLikeValue(text, agent);
173
- const baseUrl = extractBaseUrl(text);
174
- if (token || baseUrl)
175
- return { token, baseUrl, tokenPresent: !!token };
176
- }
177
- return { tokenPresent: false };
178
- }
179
- function readSmallTextFile(file) {
180
- try {
181
- const stat = fs.statSync(file);
182
- if (!stat.isFile() || stat.size > 1024 * 1024)
183
- return null;
184
- return fs.readFileSync(file, 'utf-8');
185
- }
186
- catch {
187
- return null;
188
- }
189
- }
190
- function extractBaseUrl(text) {
191
- const match = text.match(/(?:base_url|baseURL|api_url|apiUrl|ANTHROPIC_BASE_URL|OPENAI_BASE_URL|DASHSCOPE_BASE_URL)["'\s:=]+([^"'\s,}]+)/i);
192
- return match?.[1]?.trim();
193
- }
194
- function extractTokenLikeValue(text, agent) {
195
- const prefix = agent === 'claude' ? 'sk-ant-' : agent === 'pi' ? 'sk-' : 'sk-';
196
- const direct = text.match(new RegExp(`${prefix.replace(/-/g, '\\-')}[A-Za-z0-9_\\-]{8,}`));
197
- if (direct?.[0])
198
- return direct[0];
199
- const keyMatch = text.match(/(?:api[_-]?key|auth[_-]?token|token|dashscope)["'\s:=]+([A-Za-z0-9_\-.]{12,})/i);
200
- return keyMatch?.[1]?.trim();
201
- }
202
- function toSummary(input) {
203
- return {
204
- status: input.status,
205
- source: input.source,
206
- tokenPresent: input.tokenPresent,
207
- ...(input.baseUrl ? { baseUrlHost: formatBaseUrlHost(input.baseUrl) } : {}),
208
- ...(input.token ? { tokenHint: maskToken(input.token) } : {}),
209
- updatedAt: new Date().toISOString(),
210
- };
211
- }
212
- export function maskToken(token) {
213
- const trimmed = token.trim();
214
- if (!trimmed)
215
- return '';
216
- const suffix = trimmed.slice(-TOKEN_SUFFIX_LENGTH);
217
- return `••••${suffix}`;
218
- }
219
- function formatBaseUrlHost(baseUrl) {
220
- try {
221
- return new URL(baseUrl).host || baseUrl;
222
- }
223
- catch {
224
- return baseUrl.replace(/^https?:\/\//i, '').replace(/\/.*$/, '') || baseUrl;
225
- }
226
- }
1
+ import s from"node:fs";import m from"node:os";import i from"node:path";import{readLatestUserEnv as E}from"../agent-env.js";import{loadConfig as S,resolveShennianPath as g}from"../config/index.js";const c=g("agent-provider-config.json"),U=4;function A(e){return e==="codex"||e==="claude"||e==="pi"}const O={codex:{baseUrl:["OPENAI_BASE_URL","OPENAI_API_BASE","OPENAI_API_URL"],token:["OPENAI_API_KEY","OPENAI_TOKEN"]},claude:{baseUrl:["ANTHROPIC_BASE_URL","ANTHROPIC_API_URL"],token:["ANTHROPIC_API_KEY","ANTHROPIC_AUTH_TOKEN"]},pi:{baseUrl:["DASHSCOPE_BASE_URL","DASHSCOPE_API_BASE","DASHSCOPE_API_URL"],token:["DASHSCOPE_API_KEY","DASHSCOPE_TOKEN"]}};function k(e){return u().configs[e]}function v(e){const n=u(),o=n.configs[e.agent],t={agent:e.agent,baseUrl:e.baseUrl?.trim()||o?.baseUrl,token:e.token?.trim()||o?.token,updatedAt:new Date().toISOString()};return n.configs[e.agent]=t,_(n),t}function B(e){const n=u();delete n.configs[e],_(n)}function K(e){if(!A(e))return{};const n=k(e);return n?l(e==="codex"?{OPENAI_BASE_URL:n.baseUrl,OPENAI_API_KEY:n.token}:e==="claude"?{ANTHROPIC_BASE_URL:n.baseUrl,ANTHROPIC_API_KEY:n.token}:{DASHSCOPE_BASE_URL:n.baseUrl,DASHSCOPE_API_KEY:n.token}):{}}function j(e,n=E()){if(!A(e))return;const o=k(e);if(o?.token||o?.baseUrl)return a({agent:e,source:"shennian",status:o.token?"managed":"missing",baseUrl:o.baseUrl,token:o.token,tokenPresent:!!o.token});const t=I(e,n);if(t.tokenPresent||t.baseUrl)return a({agent:e,source:"env",status:t.tokenPresent?"detected":"missing",baseUrl:t.baseUrl,token:t.token,tokenPresent:t.tokenPresent});const r=p(e);return r.tokenPresent||r.baseUrl?a({agent:e,source:"config-file",status:r.tokenPresent?"detected":"missing",baseUrl:r.baseUrl,token:r.token,tokenPresent:r.tokenPresent}):a({agent:e,source:"unknown",status:"missing",tokenPresent:!1})}function u(){try{return{configs:JSON.parse(s.readFileSync(c,"utf-8")).configs??{}}}catch{return{configs:{}}}}function _(e){s.mkdirSync(i.dirname(c),{recursive:!0}),s.writeFileSync(c,JSON.stringify(e,null,2),{mode:384});try{s.chmodSync(c,384)}catch{}}function l(e){const n={};for(const[o,t]of Object.entries(e))t&&(n[o]=t);return n}function I(e,n){const o=O[e],t=P(n,o.baseUrl),r=P(n,o.token);return{baseUrl:t,token:r,tokenPresent:!!r}}function P(e,n){for(const o of n){const t=e[o]?.trim();if(t)return t}}function p(e){const n=m.homedir();if(e==="pi"){const r=S().apiKeys?.dashscope?.trim();if(r)return{token:r,tokenPresent:!0}}const o=e==="codex"?[i.join(n,".codex","config.toml"),i.join(n,".codex","auth.json")]:e==="claude"?[i.join(n,".claude","settings.json"),i.join(n,".claude.json")]:[i.join(n,".dashscope","config.json")];for(const t of o){const r=C(t);if(!r)continue;const f=N(r,e),d=b(r);if(f||d)return{token:f,baseUrl:d,tokenPresent:!!f}}return{tokenPresent:!1}}function C(e){try{const n=s.statSync(e);return!n.isFile()||n.size>1024*1024?null:s.readFileSync(e,"utf-8")}catch{return null}}function b(e){return e.match(/(?:base_url|baseURL|api_url|apiUrl|ANTHROPIC_BASE_URL|OPENAI_BASE_URL|DASHSCOPE_BASE_URL)["'\s:=]+([^"'\s,}]+)/i)?.[1]?.trim()}function N(e,n){const o=n==="claude"?"sk-ant-":"sk-",t=e.match(new RegExp(`${o.replace(/-/g,"\\-")}[A-Za-z0-9_\\-]{8,}`));return t?.[0]?t[0]:e.match(/(?:api[_-]?key|auth[_-]?token|token|dashscope)["'\s:=]+([A-Za-z0-9_\-.]{12,})/i)?.[1]?.trim()}function a(e){return{status:e.status,source:e.source,tokenPresent:e.tokenPresent,...e.baseUrl?{baseUrlHost:R(e.baseUrl)}:{},...e.token?{tokenHint:h(e.token)}:{},updatedAt:new Date().toISOString()}}function h(e){const n=e.trim();return n?`\u2022\u2022\u2022\u2022${n.slice(-U)}`:""}function R(e){try{return new URL(e).host||e}catch{return e.replace(/^https?:\/\//i,"").replace(/\/.*$/,"")||e}}export{K as buildManagedAgentEnv,B as deleteManagedAgentProviderConfig,j as getAgentConfigSummary,k as getManagedAgentProviderConfig,h as maskToken,v as upsertManagedAgentProviderConfig};