shennian 0.2.6 → 0.2.7
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.d.ts +2 -0
- package/dist/bin/shennian.js +2 -0
- package/dist/src/agents/adapter.d.ts +35 -0
- package/dist/src/agents/adapter.js +19 -0
- package/dist/src/agents/claude.d.ts +25 -0
- package/dist/src/agents/claude.js +234 -0
- package/dist/src/agents/codex.d.ts +52 -0
- package/dist/src/agents/codex.js +829 -0
- package/dist/src/agents/command-spec.d.ts +36 -0
- package/dist/src/agents/command-spec.js +303 -0
- package/dist/src/agents/cursor.d.ts +22 -0
- package/dist/src/agents/cursor.js +248 -0
- package/dist/src/agents/custom.d.ts +30 -0
- package/dist/src/agents/custom.js +210 -0
- package/dist/src/agents/detect.d.ts +9 -0
- package/dist/src/agents/detect.js +46 -0
- package/dist/src/agents/gemini.d.ts +17 -0
- package/dist/src/agents/gemini.js +172 -0
- package/dist/src/agents/model-registry/cache.d.ts +4 -0
- package/dist/src/agents/model-registry/cache.js +37 -0
- package/dist/src/agents/model-registry/discovery.d.ts +4 -0
- package/dist/src/agents/model-registry/discovery.js +166 -0
- package/dist/src/agents/model-registry/parsers.d.ts +11 -0
- package/dist/src/agents/model-registry/parsers.js +349 -0
- package/dist/src/agents/model-registry/runner.d.ts +6 -0
- package/dist/src/agents/model-registry/runner.js +29 -0
- package/dist/src/agents/model-registry/service.d.ts +6 -0
- package/dist/src/agents/model-registry/service.js +71 -0
- package/dist/src/agents/model-registry/types.d.ts +30 -0
- package/dist/src/agents/model-registry/types.js +8 -0
- package/dist/src/agents/model-registry.d.ts +13 -0
- package/dist/src/agents/model-registry.js +15 -0
- package/dist/src/agents/openclaw.d.ts +20 -0
- package/dist/src/agents/openclaw.js +273 -0
- package/dist/src/agents/pi.d.ts +51 -0
- package/dist/src/agents/pi.js +793 -0
- package/dist/src/commands/agent.d.ts +2 -0
- package/dist/src/commands/agent.js +131 -0
- package/dist/src/commands/daemon.d.ts +35 -0
- package/dist/src/commands/daemon.js +612 -0
- package/dist/src/commands/pair-qr.d.ts +4 -0
- package/dist/src/commands/pair-qr.js +6 -0
- package/dist/src/commands/pair.d.ts +8 -0
- package/dist/src/commands/pair.js +162 -0
- package/dist/src/commands/upgrade.d.ts +5 -0
- package/dist/src/commands/upgrade.js +145 -0
- package/dist/src/config/index.d.ts +32 -0
- package/dist/src/config/index.js +31 -0
- package/dist/src/fs/handler.d.ts +26 -0
- package/dist/src/fs/handler.js +130 -0
- package/dist/src/fs/security.d.ts +2 -0
- package/dist/src/fs/security.js +32 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +277 -0
- package/dist/src/log-reporter.d.ts +18 -0
- package/dist/src/log-reporter.js +16 -0
- package/dist/src/native-fusion/parsers.d.ts +12 -0
- package/dist/src/native-fusion/parsers.js +687 -0
- package/dist/src/native-fusion/service.d.ts +25 -0
- package/dist/src/native-fusion/service.js +176 -0
- package/dist/src/native-fusion/state.d.ts +3 -0
- package/dist/src/native-fusion/state.js +22 -0
- package/dist/src/native-fusion/types.d.ts +24 -0
- package/dist/src/native-fusion/types.js +1 -0
- package/dist/src/region.d.ts +15 -0
- package/dist/src/region.js +99 -0
- package/dist/src/relay/client.d.ts +62 -0
- package/dist/src/relay/client.js +311 -0
- package/dist/src/session/handlers/agents.d.ts +4 -0
- package/dist/src/session/handlers/agents.js +55 -0
- package/dist/src/session/handlers/chat.d.ts +4 -0
- package/dist/src/session/handlers/chat.js +326 -0
- package/dist/src/session/handlers/control.d.ts +5 -0
- package/dist/src/session/handlers/control.js +57 -0
- package/dist/src/session/handlers/fs.d.ts +10 -0
- package/dist/src/session/handlers/fs.js +256 -0
- package/dist/src/session/manager.d.ts +28 -0
- package/dist/src/session/manager.js +174 -0
- package/dist/src/session/store.d.ts +7 -0
- package/dist/src/session/store.js +46 -0
- package/dist/src/session/types.d.ts +35 -0
- package/dist/src/session/types.js +4 -0
- package/dist/src/upgrade/engine.d.ts +64 -0
- package/dist/src/upgrade/engine.js +238 -0
- package/package.json +2 -2
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ChildProcess, type SpawnOptions, type SpawnSyncOptions } from 'node:child_process';
|
|
2
|
+
import type { BuiltinAgentType } from '@shennian/wire';
|
|
3
|
+
export type CommandSpec = {
|
|
4
|
+
command: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
display: string;
|
|
7
|
+
};
|
|
8
|
+
export type ResolvedCommandKind = 'native' | 'cmd-shim' | 'wsl-bridge';
|
|
9
|
+
export type ResolvedCommandSpec = CommandSpec & {
|
|
10
|
+
kind: ResolvedCommandKind;
|
|
11
|
+
path: string;
|
|
12
|
+
};
|
|
13
|
+
type LaunchSpec = {
|
|
14
|
+
command: string;
|
|
15
|
+
args: string[];
|
|
16
|
+
cwd?: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function resetCommandSpecCache(): void;
|
|
19
|
+
export declare function getProcessPlatform(): NodeJS.Platform;
|
|
20
|
+
export declare function windowsPathToWsl(input: string): string | undefined;
|
|
21
|
+
export declare function commandExists(command: string): boolean;
|
|
22
|
+
export declare function resolveBuiltinCommand(type: BuiltinAgentType): ResolvedCommandSpec | null;
|
|
23
|
+
export declare function buildLaunchSpec(spec: ResolvedCommandSpec, runtimeArgs: string[], cwd?: string): LaunchSpec;
|
|
24
|
+
export declare function spawnResolvedCommand(spec: ResolvedCommandSpec, runtimeArgs: string[], options?: Omit<SpawnOptions, 'cwd'> & {
|
|
25
|
+
cwd?: string;
|
|
26
|
+
}): ChildProcess;
|
|
27
|
+
export declare function spawnResolvedCommandSync(spec: ResolvedCommandSpec, runtimeArgs: string[], options?: Omit<SpawnSyncOptions, 'cwd'> & {
|
|
28
|
+
cwd?: string;
|
|
29
|
+
}): import("child_process").SpawnSyncReturns<string | NonSharedBuffer>;
|
|
30
|
+
export declare function parseCommandString(command: string): string[];
|
|
31
|
+
export declare function buildCommandStringLaunchSpec(commandString: string, runtimeArgs: string[], cwd?: string): LaunchSpec;
|
|
32
|
+
export declare function spawnCommandString(commandString: string, runtimeArgs: string[], options?: Omit<SpawnOptions, 'cwd'> & {
|
|
33
|
+
cwd?: string;
|
|
34
|
+
}): ChildProcess;
|
|
35
|
+
export declare function getCommandVersion(spec: ResolvedCommandSpec, flag?: string): string | undefined;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,303 @@
|
|
|
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 path from 'node:path';
|
|
5
|
+
const BUILTIN_COMMANDS = {
|
|
6
|
+
claude: [
|
|
7
|
+
{ command: 'claude', args: [], display: 'claude' },
|
|
8
|
+
],
|
|
9
|
+
codex: [
|
|
10
|
+
{ command: 'codex', args: [], display: 'codex' },
|
|
11
|
+
],
|
|
12
|
+
gemini: [
|
|
13
|
+
{ command: 'gemini', args: [], display: 'gemini' },
|
|
14
|
+
],
|
|
15
|
+
cursor: [
|
|
16
|
+
{ command: 'agent', args: [], display: 'agent' },
|
|
17
|
+
{ command: 'cursor', args: ['agent'], display: 'cursor agent' },
|
|
18
|
+
],
|
|
19
|
+
openclaw: [
|
|
20
|
+
{ command: 'openclaw', args: [], display: 'openclaw' },
|
|
21
|
+
],
|
|
22
|
+
pi: [
|
|
23
|
+
{ command: 'shennian', args: [], display: 'shennian' },
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
const availabilityCache = new Map();
|
|
27
|
+
const resolvedCache = new Map();
|
|
28
|
+
const pathLookupCache = new Map();
|
|
29
|
+
const wslAvailabilityCache = new Map();
|
|
30
|
+
export function resetCommandSpecCache() {
|
|
31
|
+
availabilityCache.clear();
|
|
32
|
+
resolvedCache.clear();
|
|
33
|
+
pathLookupCache.clear();
|
|
34
|
+
wslAvailabilityCache.clear();
|
|
35
|
+
}
|
|
36
|
+
export function getProcessPlatform() {
|
|
37
|
+
return process.platform;
|
|
38
|
+
}
|
|
39
|
+
function splitLines(text) {
|
|
40
|
+
return text
|
|
41
|
+
.split(/\r?\n/)
|
|
42
|
+
.map((line) => line.trim())
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
function lookupCommandPaths(command) {
|
|
46
|
+
if (pathLookupCache.has(command)) {
|
|
47
|
+
return pathLookupCache.get(command) ?? [];
|
|
48
|
+
}
|
|
49
|
+
const lookup = getProcessPlatform() === 'win32' ? 'where' : 'which';
|
|
50
|
+
const result = spawnSync(lookup, [command], {
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
53
|
+
timeout: 3000,
|
|
54
|
+
});
|
|
55
|
+
const paths = result.status === 0 ? splitLines(result.stdout ?? '') : [];
|
|
56
|
+
pathLookupCache.set(command, paths);
|
|
57
|
+
return paths;
|
|
58
|
+
}
|
|
59
|
+
function getWindowsShellPath() {
|
|
60
|
+
return process.env.ComSpec || 'cmd.exe';
|
|
61
|
+
}
|
|
62
|
+
function isWindowsNativeBinary(filePath) {
|
|
63
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
64
|
+
return ext === '.exe' || ext === '.com';
|
|
65
|
+
}
|
|
66
|
+
function isWindowsCmdShim(filePath) {
|
|
67
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
68
|
+
return ext === '.cmd' || ext === '.bat';
|
|
69
|
+
}
|
|
70
|
+
function quoteCmdArg(text) {
|
|
71
|
+
return `"${text.replace(/"/g, '""')}"`;
|
|
72
|
+
}
|
|
73
|
+
function quotePosixArg(text) {
|
|
74
|
+
return `'${text.replace(/'/g, `'\\''`)}'`;
|
|
75
|
+
}
|
|
76
|
+
export function windowsPathToWsl(input) {
|
|
77
|
+
if (!input)
|
|
78
|
+
return undefined;
|
|
79
|
+
const normalized = input.replace(/\//g, '\\');
|
|
80
|
+
const driveMatch = normalized.match(/^([A-Za-z]):\\(.*)$/);
|
|
81
|
+
if (driveMatch) {
|
|
82
|
+
const [, drive, rest] = driveMatch;
|
|
83
|
+
const suffix = rest.replace(/\\/g, '/');
|
|
84
|
+
return `/mnt/${drive.toLowerCase()}/${suffix}`;
|
|
85
|
+
}
|
|
86
|
+
if (normalized.startsWith('\\\\wsl$\\')) {
|
|
87
|
+
const remainder = normalized.replace(/^\\\\wsl\$\\[^\\]+\\?/, '');
|
|
88
|
+
return `/${remainder.replace(/\\/g, '/')}`;
|
|
89
|
+
}
|
|
90
|
+
if (input.startsWith('/'))
|
|
91
|
+
return input;
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
function resolveArbitraryWindowsCommand(command) {
|
|
95
|
+
const normalized = command.trim();
|
|
96
|
+
if (!normalized) {
|
|
97
|
+
throw new Error('Command string is empty');
|
|
98
|
+
}
|
|
99
|
+
const hasExplicitPath = /[\\/]/.test(normalized) || /^[A-Za-z]:/.test(normalized);
|
|
100
|
+
if (hasExplicitPath) {
|
|
101
|
+
if (isWindowsCmdShim(normalized))
|
|
102
|
+
return { kind: 'cmd-shim', path: normalized };
|
|
103
|
+
return { kind: 'direct', path: normalized };
|
|
104
|
+
}
|
|
105
|
+
const paths = lookupCommandPaths(normalized);
|
|
106
|
+
const native = paths.find(isWindowsNativeBinary);
|
|
107
|
+
if (native)
|
|
108
|
+
return { kind: 'direct', path: native };
|
|
109
|
+
const shim = paths.find(isWindowsCmdShim);
|
|
110
|
+
if (shim)
|
|
111
|
+
return { kind: 'cmd-shim', path: shim };
|
|
112
|
+
return { kind: 'direct', path: normalized };
|
|
113
|
+
}
|
|
114
|
+
function canUseWslBridge(command) {
|
|
115
|
+
if (wslAvailabilityCache.has(command)) {
|
|
116
|
+
return wslAvailabilityCache.get(command) ?? false;
|
|
117
|
+
}
|
|
118
|
+
const wslPath = lookupCommandPaths('wsl.exe')[0] ?? lookupCommandPaths('wsl')[0];
|
|
119
|
+
if (!wslPath) {
|
|
120
|
+
wslAvailabilityCache.set(command, false);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
const result = spawnSync(wslPath, ['-e', 'sh', '-lc', `command -v ${quotePosixArg(command)} >/dev/null 2>&1`], {
|
|
124
|
+
stdio: 'ignore',
|
|
125
|
+
timeout: 5000,
|
|
126
|
+
});
|
|
127
|
+
const ok = result.status === 0;
|
|
128
|
+
wslAvailabilityCache.set(command, ok);
|
|
129
|
+
return ok;
|
|
130
|
+
}
|
|
131
|
+
export function commandExists(command) {
|
|
132
|
+
if (availabilityCache.has(command)) {
|
|
133
|
+
return availabilityCache.get(command) ?? false;
|
|
134
|
+
}
|
|
135
|
+
const exists = lookupCommandPaths(command).length > 0 || (getProcessPlatform() === 'win32' && canUseWslBridge(command));
|
|
136
|
+
availabilityCache.set(command, exists);
|
|
137
|
+
return exists;
|
|
138
|
+
}
|
|
139
|
+
function resolveWindowsBuiltinCommand(candidate) {
|
|
140
|
+
const paths = lookupCommandPaths(candidate.command);
|
|
141
|
+
const native = paths.find(isWindowsNativeBinary);
|
|
142
|
+
if (native) {
|
|
143
|
+
return { ...candidate, kind: 'native', path: native };
|
|
144
|
+
}
|
|
145
|
+
const shim = paths.find(isWindowsCmdShim);
|
|
146
|
+
if (shim) {
|
|
147
|
+
return { ...candidate, kind: 'cmd-shim', path: shim };
|
|
148
|
+
}
|
|
149
|
+
if (canUseWslBridge(candidate.command)) {
|
|
150
|
+
const wslPath = lookupCommandPaths('wsl.exe')[0] ?? lookupCommandPaths('wsl')[0];
|
|
151
|
+
if (wslPath) {
|
|
152
|
+
return { ...candidate, kind: 'wsl-bridge', path: wslPath };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
export function resolveBuiltinCommand(type) {
|
|
158
|
+
if (resolvedCache.has(type)) {
|
|
159
|
+
return resolvedCache.get(type) ?? null;
|
|
160
|
+
}
|
|
161
|
+
const resolved = BUILTIN_COMMANDS[type]
|
|
162
|
+
.map((candidate) => {
|
|
163
|
+
if (getProcessPlatform() === 'win32') {
|
|
164
|
+
return resolveWindowsBuiltinCommand(candidate);
|
|
165
|
+
}
|
|
166
|
+
const commandPath = lookupCommandPaths(candidate.command)[0];
|
|
167
|
+
return commandPath ? { ...candidate, kind: 'native', path: commandPath } : null;
|
|
168
|
+
})
|
|
169
|
+
.find(Boolean) ?? null;
|
|
170
|
+
resolvedCache.set(type, resolved);
|
|
171
|
+
return resolved;
|
|
172
|
+
}
|
|
173
|
+
export function buildLaunchSpec(spec, runtimeArgs, cwd) {
|
|
174
|
+
if (spec.kind === 'native') {
|
|
175
|
+
return {
|
|
176
|
+
command: spec.path,
|
|
177
|
+
args: [...spec.args, ...runtimeArgs],
|
|
178
|
+
cwd,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (spec.kind === 'cmd-shim') {
|
|
182
|
+
const commandLine = [spec.path, ...spec.args, ...runtimeArgs].map(quoteCmdArg).join(' ');
|
|
183
|
+
return {
|
|
184
|
+
command: getWindowsShellPath(),
|
|
185
|
+
args: ['/d', '/s', '/c', `"${commandLine}"`],
|
|
186
|
+
cwd,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const wslCwd = cwd ? windowsPathToWsl(cwd) : undefined;
|
|
190
|
+
if (cwd && !wslCwd) {
|
|
191
|
+
throw new Error(`Cannot map Windows path to WSL workDir: ${cwd}`);
|
|
192
|
+
}
|
|
193
|
+
const shellCommand = [
|
|
194
|
+
wslCwd ? `cd ${quotePosixArg(wslCwd)}` : '',
|
|
195
|
+
`exec ${[spec.command, ...spec.args, ...runtimeArgs].map(quotePosixArg).join(' ')}`,
|
|
196
|
+
].filter(Boolean).join(' && ');
|
|
197
|
+
return {
|
|
198
|
+
command: spec.path,
|
|
199
|
+
args: ['-e', 'sh', '-lc', shellCommand],
|
|
200
|
+
cwd: undefined,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
export function spawnResolvedCommand(spec, runtimeArgs, options = {}) {
|
|
204
|
+
const launch = buildLaunchSpec(spec, runtimeArgs, options.cwd);
|
|
205
|
+
return spawn(launch.command, launch.args, {
|
|
206
|
+
...options,
|
|
207
|
+
cwd: launch.cwd,
|
|
208
|
+
...(getProcessPlatform() === 'win32' && spec.kind === 'cmd-shim'
|
|
209
|
+
? { windowsVerbatimArguments: true }
|
|
210
|
+
: {}),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
export function spawnResolvedCommandSync(spec, runtimeArgs, options = {}) {
|
|
214
|
+
const launch = buildLaunchSpec(spec, runtimeArgs, options.cwd);
|
|
215
|
+
return spawnSync(launch.command, launch.args, {
|
|
216
|
+
...options,
|
|
217
|
+
cwd: launch.cwd,
|
|
218
|
+
...(getProcessPlatform() === 'win32' && spec.kind === 'cmd-shim'
|
|
219
|
+
? { windowsVerbatimArguments: true }
|
|
220
|
+
: {}),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
export function parseCommandString(command) {
|
|
224
|
+
const args = [];
|
|
225
|
+
let current = '';
|
|
226
|
+
let inSingle = false;
|
|
227
|
+
let inDouble = false;
|
|
228
|
+
for (const ch of command) {
|
|
229
|
+
if (ch === "'" && !inDouble) {
|
|
230
|
+
inSingle = !inSingle;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (ch === '"' && !inSingle) {
|
|
234
|
+
inDouble = !inDouble;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (ch === ' ' && !inSingle && !inDouble) {
|
|
238
|
+
if (current) {
|
|
239
|
+
args.push(current);
|
|
240
|
+
current = '';
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
current += ch;
|
|
245
|
+
}
|
|
246
|
+
if (current)
|
|
247
|
+
args.push(current);
|
|
248
|
+
return args;
|
|
249
|
+
}
|
|
250
|
+
export function buildCommandStringLaunchSpec(commandString, runtimeArgs, cwd) {
|
|
251
|
+
const parts = parseCommandString(commandString);
|
|
252
|
+
const bin = parts[0];
|
|
253
|
+
if (!bin) {
|
|
254
|
+
throw new Error('Command string is empty');
|
|
255
|
+
}
|
|
256
|
+
const baseArgs = parts.slice(1);
|
|
257
|
+
if (getProcessPlatform() !== 'win32') {
|
|
258
|
+
return {
|
|
259
|
+
command: bin,
|
|
260
|
+
args: [...baseArgs, ...runtimeArgs],
|
|
261
|
+
cwd,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const invocation = resolveArbitraryWindowsCommand(bin);
|
|
265
|
+
if (invocation.kind === 'cmd-shim') {
|
|
266
|
+
return {
|
|
267
|
+
command: getWindowsShellPath(),
|
|
268
|
+
args: ['/d', '/s', '/c', `"${[invocation.path, ...baseArgs, ...runtimeArgs].map(quoteCmdArg).join(' ')}"`],
|
|
269
|
+
cwd,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
command: invocation.path,
|
|
274
|
+
args: [...baseArgs, ...runtimeArgs],
|
|
275
|
+
cwd,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
export function spawnCommandString(commandString, runtimeArgs, options = {}) {
|
|
279
|
+
const launch = buildCommandStringLaunchSpec(commandString, runtimeArgs, options.cwd);
|
|
280
|
+
const parts = parseCommandString(commandString);
|
|
281
|
+
const invocation = getProcessPlatform() === 'win32' && parts[0]
|
|
282
|
+
? resolveArbitraryWindowsCommand(parts[0])
|
|
283
|
+
: null;
|
|
284
|
+
return spawn(launch.command, launch.args, {
|
|
285
|
+
...options,
|
|
286
|
+
cwd: launch.cwd,
|
|
287
|
+
...(getProcessPlatform() === 'win32' && invocation?.kind === 'cmd-shim'
|
|
288
|
+
? { windowsVerbatimArguments: true }
|
|
289
|
+
: {}),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
export function getCommandVersion(spec, flag = '--version') {
|
|
293
|
+
const result = spawnResolvedCommandSync(spec, [flag], {
|
|
294
|
+
encoding: 'utf-8',
|
|
295
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
296
|
+
timeout: 5000,
|
|
297
|
+
});
|
|
298
|
+
if (result.status !== 0)
|
|
299
|
+
return undefined;
|
|
300
|
+
const text = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
|
|
301
|
+
const match = text.match(/\d+\.\d+[\w.-]*/);
|
|
302
|
+
return match?.[0];
|
|
303
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AgentAdapter } from './adapter.js';
|
|
2
|
+
export declare class CursorAdapter extends AgentAdapter {
|
|
3
|
+
readonly type: "cursor";
|
|
4
|
+
private process;
|
|
5
|
+
private chatId;
|
|
6
|
+
private workDir;
|
|
7
|
+
private seq;
|
|
8
|
+
private runId;
|
|
9
|
+
private emittedText;
|
|
10
|
+
start(_sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
|
|
11
|
+
send(text: string, modelId?: string): Promise<void>;
|
|
12
|
+
resume(agentSessionId: string): Promise<void>;
|
|
13
|
+
stop(): Promise<void>;
|
|
14
|
+
private spawnAndParse;
|
|
15
|
+
private handleStreamEvent;
|
|
16
|
+
private emitEvent;
|
|
17
|
+
private killProcess;
|
|
18
|
+
private resetRunState;
|
|
19
|
+
private isLiveAssistantDelta;
|
|
20
|
+
private emitTextDelta;
|
|
21
|
+
private emitFinalResultDelta;
|
|
22
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// @arch docs/architecture/cli/agent-adapters.md
|
|
2
|
+
// @test src/__tests__/cursor-adapter.test.ts
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { AgentAdapter, registerAgent } from './adapter.js';
|
|
6
|
+
import { resolveBuiltinCommand, spawnResolvedCommand } from './command-spec.js';
|
|
7
|
+
export class CursorAdapter extends AgentAdapter {
|
|
8
|
+
type = 'cursor';
|
|
9
|
+
process = null;
|
|
10
|
+
chatId = null;
|
|
11
|
+
workDir = null;
|
|
12
|
+
seq = 0;
|
|
13
|
+
runId = '';
|
|
14
|
+
emittedText = '';
|
|
15
|
+
async start(_sessionId, workDir, agentSessionId) {
|
|
16
|
+
this.workDir = workDir;
|
|
17
|
+
this.seq = 0;
|
|
18
|
+
if (agentSessionId)
|
|
19
|
+
this.chatId = agentSessionId;
|
|
20
|
+
}
|
|
21
|
+
async send(text, modelId) {
|
|
22
|
+
await this.killProcess();
|
|
23
|
+
this.runId = randomUUID();
|
|
24
|
+
this.resetRunState();
|
|
25
|
+
const args = [
|
|
26
|
+
'-p', text,
|
|
27
|
+
'--output-format', 'stream-json',
|
|
28
|
+
'--stream-partial-output',
|
|
29
|
+
'--trust',
|
|
30
|
+
];
|
|
31
|
+
if (modelId) {
|
|
32
|
+
args.push('--model', modelId);
|
|
33
|
+
}
|
|
34
|
+
if (this.chatId) {
|
|
35
|
+
args.push('--continue');
|
|
36
|
+
}
|
|
37
|
+
this.spawnAndParse(args);
|
|
38
|
+
}
|
|
39
|
+
async resume(agentSessionId) {
|
|
40
|
+
await this.killProcess();
|
|
41
|
+
this.chatId = agentSessionId;
|
|
42
|
+
this.runId = randomUUID();
|
|
43
|
+
this.resetRunState();
|
|
44
|
+
this.spawnAndParse([
|
|
45
|
+
'--resume', agentSessionId,
|
|
46
|
+
'--output-format', 'stream-json',
|
|
47
|
+
'--stream-partial-output',
|
|
48
|
+
'--trust',
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
async stop() {
|
|
52
|
+
await this.killProcess();
|
|
53
|
+
}
|
|
54
|
+
spawnAndParse(args) {
|
|
55
|
+
const spec = resolveBuiltinCommand('cursor');
|
|
56
|
+
if (!spec) {
|
|
57
|
+
this.emit('error', new Error('Cursor Agent CLI not found. Expected "agent" or "cursor agent".'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const proc = spawnResolvedCommand(spec, args, {
|
|
61
|
+
cwd: this.workDir ?? undefined,
|
|
62
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
63
|
+
env: { ...process.env },
|
|
64
|
+
});
|
|
65
|
+
this.process = proc;
|
|
66
|
+
const rl = createInterface({ input: proc.stdout });
|
|
67
|
+
rl.on('line', (line) => {
|
|
68
|
+
if (!line.trim())
|
|
69
|
+
return;
|
|
70
|
+
try {
|
|
71
|
+
const obj = JSON.parse(line);
|
|
72
|
+
this.handleStreamEvent(obj);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// 非 JSON 行当作纯文本 delta
|
|
76
|
+
this.emitEvent({ state: 'delta', text: line });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
let stderrBuf = '';
|
|
80
|
+
proc.stderr?.on('data', (chunk) => {
|
|
81
|
+
stderrBuf += chunk.toString();
|
|
82
|
+
});
|
|
83
|
+
proc.on('close', (code) => {
|
|
84
|
+
if (this.process !== proc)
|
|
85
|
+
return;
|
|
86
|
+
this.process = null;
|
|
87
|
+
if (code !== 0 && code !== null) {
|
|
88
|
+
const msg = stderrBuf.trim() || `cursor agent exited with code ${code}`;
|
|
89
|
+
this.emitEvent({ state: 'error', message: msg });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
proc.on('error', (err) => {
|
|
93
|
+
if (this.process !== proc)
|
|
94
|
+
return;
|
|
95
|
+
this.process = null;
|
|
96
|
+
if (err.code === 'ENOENT') {
|
|
97
|
+
this.emit('error', new Error('Command "cursor" not found. Is Cursor CLI installed?'));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
this.emit('error', err);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
handleStreamEvent(obj) {
|
|
105
|
+
if (obj.session_id && !this.chatId) {
|
|
106
|
+
this.chatId = obj.session_id;
|
|
107
|
+
}
|
|
108
|
+
if (obj.chat_id && !this.chatId) {
|
|
109
|
+
this.chatId = obj.chat_id;
|
|
110
|
+
}
|
|
111
|
+
const eventType = obj.type ?? obj.event;
|
|
112
|
+
switch (eventType) {
|
|
113
|
+
case 'system': {
|
|
114
|
+
// init event — session_id already captured above
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case 'assistant': {
|
|
118
|
+
// Cursor's --stream-partial-output currently emits duplicate buffered
|
|
119
|
+
// flushes. Only timestamped assistant events without model_call_id are
|
|
120
|
+
// safe to treat as live deltas; use result.result as the terminal
|
|
121
|
+
// canonical text.
|
|
122
|
+
if (obj.message?.content) {
|
|
123
|
+
if (!this.isLiveAssistantDelta(obj))
|
|
124
|
+
break;
|
|
125
|
+
for (const block of obj.message.content) {
|
|
126
|
+
if (block.type === 'text' && block.text) {
|
|
127
|
+
this.emitTextDelta(block.text);
|
|
128
|
+
}
|
|
129
|
+
else if (block.type === 'thinking' && block.thinking) {
|
|
130
|
+
this.emitEvent({ state: 'delta', text: block.thinking, thinking: true });
|
|
131
|
+
}
|
|
132
|
+
else if (block.type === 'tool_use') {
|
|
133
|
+
this.emitEvent({ state: 'tool-call', name: block.name, args: block.input });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
// Legacy flat
|
|
139
|
+
if (obj.delta ?? obj.text) {
|
|
140
|
+
this.emitEvent({ state: 'delta', text: (obj.delta ?? obj.text) });
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case 'user': break; // echo of user message
|
|
145
|
+
case 'result': {
|
|
146
|
+
if (obj.subtype === 'success' || !obj.is_error) {
|
|
147
|
+
this.emitFinalResultDelta(obj.result);
|
|
148
|
+
const u = obj.usage;
|
|
149
|
+
this.emitEvent({
|
|
150
|
+
state: 'final',
|
|
151
|
+
agentSessionId: this.chatId ?? undefined,
|
|
152
|
+
usage: u
|
|
153
|
+
? {
|
|
154
|
+
inputTokens: u.inputTokens ?? u.input_tokens ?? 0,
|
|
155
|
+
outputTokens: u.outputTokens ?? u.output_tokens ?? 0,
|
|
156
|
+
}
|
|
157
|
+
: undefined,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
this.emitEvent({
|
|
162
|
+
state: 'error',
|
|
163
|
+
message: obj.result ?? obj.error ?? 'unknown error',
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case 'error': {
|
|
169
|
+
this.emitEvent({
|
|
170
|
+
state: 'error',
|
|
171
|
+
message: obj.error ?? 'unknown error',
|
|
172
|
+
});
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
// Legacy event types
|
|
176
|
+
case 'text':
|
|
177
|
+
case 'delta':
|
|
178
|
+
case 'content': {
|
|
179
|
+
const text = obj.delta ?? obj.text;
|
|
180
|
+
if (text)
|
|
181
|
+
this.emitEvent({ state: 'delta', text });
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case 'done': {
|
|
185
|
+
this.emitEvent({ state: 'final', agentSessionId: this.chatId ?? undefined });
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
default: {
|
|
189
|
+
if (obj.done) {
|
|
190
|
+
this.emitEvent({ state: 'final', agentSessionId: this.chatId ?? undefined });
|
|
191
|
+
}
|
|
192
|
+
else if (obj.delta ?? obj.text) {
|
|
193
|
+
this.emitEvent({ state: 'delta', text: (obj.delta ?? obj.text) });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
emitEvent(partial) {
|
|
199
|
+
const event = {
|
|
200
|
+
...partial,
|
|
201
|
+
runId: this.runId,
|
|
202
|
+
seq: this.seq++,
|
|
203
|
+
};
|
|
204
|
+
this.emit('agentEvent', event);
|
|
205
|
+
}
|
|
206
|
+
async killProcess() {
|
|
207
|
+
const proc = this.process;
|
|
208
|
+
if (!proc)
|
|
209
|
+
return;
|
|
210
|
+
this.process = null;
|
|
211
|
+
proc.kill('SIGTERM');
|
|
212
|
+
await new Promise((resolve) => {
|
|
213
|
+
proc.on('close', resolve);
|
|
214
|
+
setTimeout(() => {
|
|
215
|
+
proc.kill('SIGKILL');
|
|
216
|
+
resolve();
|
|
217
|
+
}, 3000);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
resetRunState() {
|
|
221
|
+
this.seq = 0;
|
|
222
|
+
this.emittedText = '';
|
|
223
|
+
}
|
|
224
|
+
isLiveAssistantDelta(obj) {
|
|
225
|
+
return obj.timestamp_ms != null && !obj.model_call_id;
|
|
226
|
+
}
|
|
227
|
+
emitTextDelta(text) {
|
|
228
|
+
if (!text)
|
|
229
|
+
return;
|
|
230
|
+
this.emittedText += text;
|
|
231
|
+
this.emitEvent({ state: 'delta', text });
|
|
232
|
+
}
|
|
233
|
+
emitFinalResultDelta(result) {
|
|
234
|
+
if (!result)
|
|
235
|
+
return;
|
|
236
|
+
if (!this.emittedText) {
|
|
237
|
+
this.emitTextDelta(result);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (!result.startsWith(this.emittedText))
|
|
241
|
+
return;
|
|
242
|
+
const tail = result.slice(this.emittedText.length);
|
|
243
|
+
if (tail) {
|
|
244
|
+
this.emitTextDelta(tail);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
registerAgent('cursor', () => new CursorAdapter());
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AgentAdapter } from './adapter.js';
|
|
2
|
+
import type { CustomAgentType } from '@shennian/wire';
|
|
3
|
+
import type { CustomAgentEntry } from '../config/index.js';
|
|
4
|
+
export declare class CustomAgentAdapter extends AgentAdapter {
|
|
5
|
+
readonly type: CustomAgentType;
|
|
6
|
+
private command;
|
|
7
|
+
private mode;
|
|
8
|
+
private proactive;
|
|
9
|
+
private sessionId;
|
|
10
|
+
private agentSessionId;
|
|
11
|
+
private workDir;
|
|
12
|
+
private runId;
|
|
13
|
+
private seq;
|
|
14
|
+
private stdioProcess;
|
|
15
|
+
private spawnProcess;
|
|
16
|
+
constructor(name: string, entry: CustomAgentEntry);
|
|
17
|
+
start(sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
|
|
18
|
+
send(text: string, modelId?: string): Promise<void>;
|
|
19
|
+
resume(agentSessionId: string): Promise<void>;
|
|
20
|
+
stop(): Promise<void>;
|
|
21
|
+
private runSpawn;
|
|
22
|
+
private killSpawnProcess;
|
|
23
|
+
private startStdioProcess;
|
|
24
|
+
private sendStdio;
|
|
25
|
+
private stopStdioProcess;
|
|
26
|
+
private attachOutputHandlers;
|
|
27
|
+
private handleProtocolEvent;
|
|
28
|
+
private emitEvent;
|
|
29
|
+
}
|
|
30
|
+
export declare function registerCustomAgent(name: string, entry: CustomAgentEntry): void;
|