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,210 @@
|
|
|
1
|
+
// @arch docs/architecture/cli/agent-adapters.md#custom-agents
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { AgentAdapter, registerAgent } from './adapter.js';
|
|
5
|
+
import { spawnCommandString } from './command-spec.js';
|
|
6
|
+
export class CustomAgentAdapter extends AgentAdapter {
|
|
7
|
+
type;
|
|
8
|
+
command;
|
|
9
|
+
mode;
|
|
10
|
+
proactive;
|
|
11
|
+
sessionId = null;
|
|
12
|
+
agentSessionId = null;
|
|
13
|
+
workDir = null;
|
|
14
|
+
runId = '';
|
|
15
|
+
seq = 0;
|
|
16
|
+
// stdio mode: single long-lived process
|
|
17
|
+
stdioProcess = null;
|
|
18
|
+
// spawn mode: current single-turn process
|
|
19
|
+
spawnProcess = null;
|
|
20
|
+
constructor(name, entry) {
|
|
21
|
+
super();
|
|
22
|
+
this.type = `custom:${name}`;
|
|
23
|
+
this.command = entry.command;
|
|
24
|
+
this.mode = entry.caps.mode;
|
|
25
|
+
this.proactive = entry.caps.proactive ?? false;
|
|
26
|
+
}
|
|
27
|
+
async start(sessionId, workDir, agentSessionId) {
|
|
28
|
+
this.sessionId = sessionId;
|
|
29
|
+
this.workDir = workDir;
|
|
30
|
+
this.seq = 0;
|
|
31
|
+
if (agentSessionId)
|
|
32
|
+
this.agentSessionId = agentSessionId;
|
|
33
|
+
if (this.mode === 'stdio') {
|
|
34
|
+
await this.startStdioProcess();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async send(text, modelId) {
|
|
38
|
+
this.runId = randomUUID();
|
|
39
|
+
this.seq = 0;
|
|
40
|
+
if (this.mode === 'stdio') {
|
|
41
|
+
await this.sendStdio(text, modelId);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
await this.runSpawn(text, modelId);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async resume(agentSessionId) {
|
|
48
|
+
if (this.mode === 'stdio') {
|
|
49
|
+
const msg = JSON.stringify({
|
|
50
|
+
method: 'resume',
|
|
51
|
+
id: randomUUID(),
|
|
52
|
+
params: { sessionId: this.sessionId, agentSessionId },
|
|
53
|
+
});
|
|
54
|
+
this.stdioProcess?.stdin?.write(msg + '\n');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async stop() {
|
|
58
|
+
if (this.mode === 'stdio') {
|
|
59
|
+
await this.stopStdioProcess();
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
await this.killSpawnProcess();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ─── Spawn mode ───────────────────────────────────────────────────────────
|
|
66
|
+
async runSpawn(text, modelId) {
|
|
67
|
+
await this.killSpawnProcess();
|
|
68
|
+
const args = ['/run', '--workdir', this.workDir ?? process.cwd()];
|
|
69
|
+
if (this.sessionId)
|
|
70
|
+
args.push('--session', this.sessionId);
|
|
71
|
+
if (this.agentSessionId)
|
|
72
|
+
args.push('--resume', this.agentSessionId);
|
|
73
|
+
if (modelId)
|
|
74
|
+
args.push('--model', modelId);
|
|
75
|
+
const proc = spawnCommandString(this.command, args, {
|
|
76
|
+
cwd: this.workDir ?? undefined,
|
|
77
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
78
|
+
env: { ...process.env },
|
|
79
|
+
});
|
|
80
|
+
this.spawnProcess = proc;
|
|
81
|
+
this.attachOutputHandlers(proc, () => {
|
|
82
|
+
if (this.spawnProcess === proc)
|
|
83
|
+
this.spawnProcess = null;
|
|
84
|
+
});
|
|
85
|
+
proc.stdin?.write(text);
|
|
86
|
+
proc.stdin?.end();
|
|
87
|
+
}
|
|
88
|
+
async killSpawnProcess() {
|
|
89
|
+
const proc = this.spawnProcess;
|
|
90
|
+
if (!proc)
|
|
91
|
+
return;
|
|
92
|
+
this.spawnProcess = null;
|
|
93
|
+
proc.kill('SIGTERM');
|
|
94
|
+
await new Promise((resolve) => {
|
|
95
|
+
proc.on('close', resolve);
|
|
96
|
+
setTimeout(() => { proc.kill('SIGKILL'); resolve(); }, 3000);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// ─── Stdio mode ───────────────────────────────────────────────────────────
|
|
100
|
+
async startStdioProcess() {
|
|
101
|
+
const proc = spawnCommandString(this.command, ['/start', '--workdir', this.workDir ?? process.cwd()], {
|
|
102
|
+
cwd: this.workDir ?? undefined,
|
|
103
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
104
|
+
env: { ...process.env },
|
|
105
|
+
});
|
|
106
|
+
this.stdioProcess = proc;
|
|
107
|
+
this.attachOutputHandlers(proc, () => {
|
|
108
|
+
if (this.stdioProcess === proc) {
|
|
109
|
+
this.stdioProcess = null;
|
|
110
|
+
this.emit('error', new Error('Custom agent process exited unexpectedly'));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async sendStdio(text, modelId) {
|
|
115
|
+
if (!this.stdioProcess) {
|
|
116
|
+
await this.startStdioProcess();
|
|
117
|
+
}
|
|
118
|
+
const msg = JSON.stringify({
|
|
119
|
+
method: 'send',
|
|
120
|
+
id: randomUUID(),
|
|
121
|
+
params: {
|
|
122
|
+
sessionId: this.sessionId,
|
|
123
|
+
text,
|
|
124
|
+
modelId,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
this.stdioProcess?.stdin?.write(msg + '\n');
|
|
128
|
+
}
|
|
129
|
+
async stopStdioProcess() {
|
|
130
|
+
const proc = this.stdioProcess;
|
|
131
|
+
if (!proc)
|
|
132
|
+
return;
|
|
133
|
+
const stopMsg = JSON.stringify({ method: 'stop', id: randomUUID() });
|
|
134
|
+
proc.stdin?.write(stopMsg + '\n');
|
|
135
|
+
await new Promise((resolve) => {
|
|
136
|
+
const timer = setTimeout(() => {
|
|
137
|
+
proc.kill('SIGTERM');
|
|
138
|
+
setTimeout(() => { proc.kill('SIGKILL'); resolve(); }, 3000);
|
|
139
|
+
}, 5000);
|
|
140
|
+
proc.on('close', () => {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
resolve();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
this.stdioProcess = null;
|
|
146
|
+
}
|
|
147
|
+
// ─── Shared output parsing ────────────────────────────────────────────────
|
|
148
|
+
attachOutputHandlers(proc, onClose) {
|
|
149
|
+
const rl = createInterface({ input: proc.stdout });
|
|
150
|
+
rl.on('line', (line) => {
|
|
151
|
+
if (!line.trim())
|
|
152
|
+
return;
|
|
153
|
+
try {
|
|
154
|
+
const event = JSON.parse(line);
|
|
155
|
+
this.handleProtocolEvent(event);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// ignore malformed lines
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
let stderrBuf = '';
|
|
162
|
+
proc.stderr?.on('data', (chunk) => { stderrBuf += chunk.toString(); });
|
|
163
|
+
proc.on('close', (code) => {
|
|
164
|
+
onClose();
|
|
165
|
+
if (code !== 0 && code !== null) {
|
|
166
|
+
const msg = stderrBuf.trim() || `Agent exited with code ${code}`;
|
|
167
|
+
this.emitEvent({ state: 'error', message: msg });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
proc.on('error', (err) => {
|
|
171
|
+
onClose();
|
|
172
|
+
this.emit('error', err);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
handleProtocolEvent(event) {
|
|
176
|
+
const { state } = event;
|
|
177
|
+
if (!state)
|
|
178
|
+
return;
|
|
179
|
+
switch (state) {
|
|
180
|
+
case 'delta':
|
|
181
|
+
this.emitEvent({ state: 'delta', text: event.text ?? '', thinking: event.thinking });
|
|
182
|
+
break;
|
|
183
|
+
case 'final':
|
|
184
|
+
if (event.agentSessionId)
|
|
185
|
+
this.agentSessionId = event.agentSessionId;
|
|
186
|
+
this.emitEvent({ state: 'final', usage: event.usage, agentSessionId: this.agentSessionId ?? undefined });
|
|
187
|
+
break;
|
|
188
|
+
case 'error':
|
|
189
|
+
this.emitEvent({ state: 'error', message: event.message ?? 'unknown error' });
|
|
190
|
+
break;
|
|
191
|
+
case 'tool-call':
|
|
192
|
+
this.emitEvent({ state: 'tool-call', name: event.name ?? '', args: event.args });
|
|
193
|
+
break;
|
|
194
|
+
case 'tool-result':
|
|
195
|
+
this.emitEvent({ state: 'tool-result', name: event.name ?? '', result: event.result ?? '' });
|
|
196
|
+
break;
|
|
197
|
+
case 'notify':
|
|
198
|
+
this.emitEvent({ state: 'notify', text: event.text ?? '', title: event.title, source: event.source });
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
emitEvent(partial) {
|
|
203
|
+
const event = { ...partial, runId: this.runId, seq: this.seq++ };
|
|
204
|
+
this.emit('agentEvent', event);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export function registerCustomAgent(name, entry) {
|
|
208
|
+
const agentType = `custom:${name}`;
|
|
209
|
+
registerAgent(agentType, () => new CustomAgentAdapter(name, entry));
|
|
210
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AVAILABLE_BUILTIN_AGENT_TYPES } from '@shennian/wire';
|
|
2
|
+
import { loadConfig } from '../config/index.js';
|
|
3
|
+
import { getCommandVersion, resolveBuiltinCommand } from './command-spec.js';
|
|
4
|
+
const AGENTS = AVAILABLE_BUILTIN_AGENT_TYPES.filter((agent) => agent !== 'pi');
|
|
5
|
+
function getCustomAgentModels(caps) {
|
|
6
|
+
const ids = caps.models?.length
|
|
7
|
+
? caps.models
|
|
8
|
+
: caps.model
|
|
9
|
+
? [caps.model]
|
|
10
|
+
: [];
|
|
11
|
+
const defaultModel = caps.defaultModel ?? caps.model;
|
|
12
|
+
return ids.map((id) => ({
|
|
13
|
+
id,
|
|
14
|
+
name: id,
|
|
15
|
+
isDefault: id === defaultModel,
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
export function detectAgents() {
|
|
19
|
+
// Pi agent is always available (built-in, platform-powered)
|
|
20
|
+
const detected = [
|
|
21
|
+
{ type: 'pi', command: 'shennian', args: [], version: undefined },
|
|
22
|
+
];
|
|
23
|
+
for (const agent of AGENTS) {
|
|
24
|
+
const spec = resolveBuiltinCommand(agent);
|
|
25
|
+
if (!spec)
|
|
26
|
+
continue;
|
|
27
|
+
const version = getCommandVersion(spec);
|
|
28
|
+
detected.push({
|
|
29
|
+
type: agent,
|
|
30
|
+
command: spec.command,
|
|
31
|
+
args: spec.args,
|
|
32
|
+
version,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const config = loadConfig();
|
|
36
|
+
for (const [name, entry] of Object.entries(config.customAgents ?? {})) {
|
|
37
|
+
detected.push({
|
|
38
|
+
type: `custom:${name}`,
|
|
39
|
+
command: entry.command,
|
|
40
|
+
args: [],
|
|
41
|
+
version: entry.caps.version,
|
|
42
|
+
models: getCustomAgentModels(entry.caps),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return detected;
|
|
46
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AgentAdapter } from './adapter.js';
|
|
2
|
+
export declare class GeminiAdapter extends AgentAdapter {
|
|
3
|
+
readonly type: "gemini";
|
|
4
|
+
private process;
|
|
5
|
+
private agentSessionId;
|
|
6
|
+
private workDir;
|
|
7
|
+
private seq;
|
|
8
|
+
private runId;
|
|
9
|
+
start(_sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
|
|
10
|
+
send(text: string, modelId?: string): Promise<void>;
|
|
11
|
+
resume(agentSessionId: string): Promise<void>;
|
|
12
|
+
stop(): Promise<void>;
|
|
13
|
+
private spawnAndParse;
|
|
14
|
+
private handleStreamEvent;
|
|
15
|
+
private emitEvent;
|
|
16
|
+
private killProcess;
|
|
17
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { AgentAdapter, registerAgent } from './adapter.js';
|
|
4
|
+
import { resolveBuiltinCommand, spawnResolvedCommand } from './command-spec.js';
|
|
5
|
+
function num(v) {
|
|
6
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
7
|
+
}
|
|
8
|
+
function usageFromStats(stats) {
|
|
9
|
+
if (!stats)
|
|
10
|
+
return undefined;
|
|
11
|
+
const input = num(stats.input_tokens) ??
|
|
12
|
+
num(stats.prompt_tokens) ??
|
|
13
|
+
num(stats.inputTokens) ??
|
|
14
|
+
num(stats.promptTokenCount);
|
|
15
|
+
const output = num(stats.output_tokens) ??
|
|
16
|
+
num(stats.completion_tokens) ??
|
|
17
|
+
num(stats.outputTokens) ??
|
|
18
|
+
num(stats.candidatesTokenCount) ??
|
|
19
|
+
num(stats.responseTokenCount);
|
|
20
|
+
if (input === undefined && output === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
return {
|
|
23
|
+
inputTokens: input ?? 0,
|
|
24
|
+
outputTokens: output ?? 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export class GeminiAdapter extends AgentAdapter {
|
|
28
|
+
type = 'gemini';
|
|
29
|
+
process = null;
|
|
30
|
+
agentSessionId = null;
|
|
31
|
+
workDir = null;
|
|
32
|
+
seq = 0;
|
|
33
|
+
runId = '';
|
|
34
|
+
async start(_sessionId, workDir, agentSessionId) {
|
|
35
|
+
this.workDir = workDir;
|
|
36
|
+
this.seq = 0;
|
|
37
|
+
if (agentSessionId)
|
|
38
|
+
this.agentSessionId = agentSessionId;
|
|
39
|
+
}
|
|
40
|
+
async send(text, modelId) {
|
|
41
|
+
await this.killProcess();
|
|
42
|
+
this.runId = randomUUID();
|
|
43
|
+
this.seq = 0;
|
|
44
|
+
const args = ['-p', text, '--output-format', 'stream-json'];
|
|
45
|
+
if (modelId) {
|
|
46
|
+
args.push('-m', modelId);
|
|
47
|
+
}
|
|
48
|
+
if (this.agentSessionId) {
|
|
49
|
+
args.push('--resume', this.agentSessionId);
|
|
50
|
+
}
|
|
51
|
+
this.spawnAndParse(args);
|
|
52
|
+
}
|
|
53
|
+
async resume(agentSessionId) {
|
|
54
|
+
await this.killProcess();
|
|
55
|
+
this.agentSessionId = agentSessionId;
|
|
56
|
+
this.runId = randomUUID();
|
|
57
|
+
this.seq = 0;
|
|
58
|
+
this.spawnAndParse([
|
|
59
|
+
'--resume', agentSessionId,
|
|
60
|
+
'--output-format', 'stream-json',
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
async stop() {
|
|
64
|
+
await this.killProcess();
|
|
65
|
+
}
|
|
66
|
+
spawnAndParse(args) {
|
|
67
|
+
const spec = resolveBuiltinCommand('gemini');
|
|
68
|
+
if (!spec) {
|
|
69
|
+
this.emit('error', new Error('Command "gemini" not found. Is Gemini CLI installed?'));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const proc = spawnResolvedCommand(spec, args, {
|
|
73
|
+
cwd: this.workDir ?? undefined,
|
|
74
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
75
|
+
env: { ...process.env },
|
|
76
|
+
});
|
|
77
|
+
this.process = proc;
|
|
78
|
+
const rl = createInterface({ input: proc.stdout });
|
|
79
|
+
rl.on('line', (line) => {
|
|
80
|
+
if (!line.trim())
|
|
81
|
+
return;
|
|
82
|
+
try {
|
|
83
|
+
const obj = JSON.parse(line);
|
|
84
|
+
this.handleStreamEvent(obj);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
this.emitEvent({ state: 'delta', text: line });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
let stderrBuf = '';
|
|
91
|
+
proc.stderr?.on('data', (chunk) => {
|
|
92
|
+
stderrBuf += chunk.toString();
|
|
93
|
+
});
|
|
94
|
+
proc.on('close', (code) => {
|
|
95
|
+
if (this.process !== proc)
|
|
96
|
+
return;
|
|
97
|
+
this.process = null;
|
|
98
|
+
if (code !== 0 && code !== null) {
|
|
99
|
+
const msg = stderrBuf.trim() || `gemini exited with code ${code}`;
|
|
100
|
+
this.emitEvent({ state: 'error', message: msg });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
proc.on('error', (err) => {
|
|
104
|
+
if (this.process !== proc)
|
|
105
|
+
return;
|
|
106
|
+
this.process = null;
|
|
107
|
+
if (err.code === 'ENOENT') {
|
|
108
|
+
this.emit('error', new Error('Command "gemini" not found. Is Gemini CLI installed?'));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
this.emit('error', err);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
handleStreamEvent(obj) {
|
|
116
|
+
switch (obj.type) {
|
|
117
|
+
case 'init': {
|
|
118
|
+
if (obj.session_id) {
|
|
119
|
+
this.agentSessionId = obj.session_id;
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case 'message': {
|
|
124
|
+
if (obj.role === 'assistant' && obj.content) {
|
|
125
|
+
this.emitEvent({ state: 'delta', text: obj.content });
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case 'result': {
|
|
130
|
+
if (obj.status === 'success') {
|
|
131
|
+
this.emitEvent({
|
|
132
|
+
state: 'final',
|
|
133
|
+
agentSessionId: this.agentSessionId ?? undefined,
|
|
134
|
+
usage: usageFromStats(obj.stats),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
else if (obj.status === 'error') {
|
|
138
|
+
const msg = (typeof obj.error === 'string' && obj.error) ||
|
|
139
|
+
(typeof obj.message === 'string' && obj.message) ||
|
|
140
|
+
'gemini result error';
|
|
141
|
+
this.emitEvent({ state: 'error', message: msg });
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
default:
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
emitEvent(partial) {
|
|
150
|
+
const event = {
|
|
151
|
+
...partial,
|
|
152
|
+
runId: this.runId,
|
|
153
|
+
seq: this.seq++,
|
|
154
|
+
};
|
|
155
|
+
this.emit('agentEvent', event);
|
|
156
|
+
}
|
|
157
|
+
async killProcess() {
|
|
158
|
+
const proc = this.process;
|
|
159
|
+
if (!proc)
|
|
160
|
+
return;
|
|
161
|
+
this.process = null;
|
|
162
|
+
proc.kill('SIGTERM');
|
|
163
|
+
await new Promise((resolve) => {
|
|
164
|
+
proc.on('close', resolve);
|
|
165
|
+
setTimeout(() => {
|
|
166
|
+
proc.kill('SIGKILL');
|
|
167
|
+
resolve();
|
|
168
|
+
}, 3000);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
registerAgent('gemini', () => new GeminiAdapter());
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// @arch docs/architecture/cli/model-discovery.md
|
|
2
|
+
// @test src/__tests__/model-switching.test.ts
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { CACHE_TTL_MS, CACHE_VERSION, MODEL_CACHE_PATH } from './types.js';
|
|
6
|
+
function ensureDirExists() {
|
|
7
|
+
fs.mkdirSync(path.dirname(MODEL_CACHE_PATH), { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
export function readCache() {
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(fs.readFileSync(MODEL_CACHE_PATH, 'utf-8'));
|
|
12
|
+
if (parsed.version === CACHE_VERSION && parsed.agents && typeof parsed.agents === 'object') {
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// fall through
|
|
18
|
+
}
|
|
19
|
+
return { version: CACHE_VERSION, agents: {} };
|
|
20
|
+
}
|
|
21
|
+
export function writeCache(cache) {
|
|
22
|
+
try {
|
|
23
|
+
ensureDirExists();
|
|
24
|
+
fs.writeFileSync(MODEL_CACHE_PATH, JSON.stringify(cache, null, 2));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Cache is best-effort. Discovery should still succeed when local
|
|
28
|
+
// filesystem writes are unavailable (e.g. restricted sandboxes).
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function isCacheFresh(entry, version) {
|
|
32
|
+
if (!entry)
|
|
33
|
+
return false;
|
|
34
|
+
if (entry.version !== version)
|
|
35
|
+
return false;
|
|
36
|
+
return Date.now() - entry.refreshedAt < CACHE_TTL_MS;
|
|
37
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ModelInfo } from '@shennian/wire';
|
|
2
|
+
import type { DetectedAgent } from '../detect.js';
|
|
3
|
+
import { type DiscoveryContext } from './types.js';
|
|
4
|
+
export declare function discoverModelsForAgent(agent: DetectedAgent, context: DiscoveryContext): Promise<ModelInfo[]>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// @arch docs/architecture/cli/model-discovery.md
|
|
2
|
+
// @test src/__tests__/model-switching.test.ts
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { resolveBuiltinCommand, spawnResolvedCommand } from '../command-spec.js';
|
|
5
|
+
import { fallbackClaudeAliasModels, fallbackGeminiModels, parseCodexAppServerModels, parseCursorModels, parseOpenClawModels, } from './parsers.js';
|
|
6
|
+
import { runResolvedCommand } from './runner.js';
|
|
7
|
+
import { DISCOVERY_WORKDIR } from './types.js';
|
|
8
|
+
function sendAppServerRpc(proc, pending, id, method, params, timeoutMs) {
|
|
9
|
+
if (!proc.stdin)
|
|
10
|
+
return Promise.reject(new Error('codex app-server stdin unavailable'));
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const timer = setTimeout(() => {
|
|
13
|
+
pending.delete(id);
|
|
14
|
+
reject(new Error(`codex app-server request timed out: ${method}`));
|
|
15
|
+
}, timeoutMs);
|
|
16
|
+
pending.set(id, { resolve, reject, timer });
|
|
17
|
+
proc.stdin.write(`${JSON.stringify({ id, method, params })}\n`, (error) => {
|
|
18
|
+
if (!error)
|
|
19
|
+
return;
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
pending.delete(id);
|
|
22
|
+
reject(error);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function discoverCodexModelsViaAppServer(spec) {
|
|
27
|
+
const proc = spawnResolvedCommand(spec, ['app-server', '--listen', 'stdio://'], {
|
|
28
|
+
cwd: DISCOVERY_WORKDIR,
|
|
29
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
30
|
+
env: { ...process.env, NO_COLOR: '1' },
|
|
31
|
+
});
|
|
32
|
+
const pending = new Map();
|
|
33
|
+
let seq = 1;
|
|
34
|
+
const rl = createInterface({ input: proc.stdout });
|
|
35
|
+
rl.on('line', (line) => {
|
|
36
|
+
if (!line.trim())
|
|
37
|
+
return;
|
|
38
|
+
let msg;
|
|
39
|
+
try {
|
|
40
|
+
msg = JSON.parse(line);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (msg.id == null)
|
|
46
|
+
return;
|
|
47
|
+
const request = pending.get(msg.id);
|
|
48
|
+
if (!request)
|
|
49
|
+
return;
|
|
50
|
+
clearTimeout(request.timer);
|
|
51
|
+
pending.delete(msg.id);
|
|
52
|
+
if (msg.error) {
|
|
53
|
+
request.reject(new Error(msg.error.message || `codex app-server error ${msg.error.code ?? ''}`.trim()));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
request.resolve(msg.result);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
await sendAppServerRpc(proc, pending, seq++, 'initialize', {
|
|
61
|
+
clientInfo: { name: 'shennian', title: 'Shennian', version: '0.0.0' },
|
|
62
|
+
capabilities: { experimentalApi: true },
|
|
63
|
+
}, 10_000);
|
|
64
|
+
const [modelList, configRead] = await Promise.all([
|
|
65
|
+
sendAppServerRpc(proc, pending, seq++, 'model/list', {}, 15_000),
|
|
66
|
+
sendAppServerRpc(proc, pending, seq++, 'config/read', {}, 15_000).catch(() => null),
|
|
67
|
+
]);
|
|
68
|
+
const config = typeof configRead === 'object' && configRead !== null && 'config' in configRead
|
|
69
|
+
? configRead.config
|
|
70
|
+
: undefined;
|
|
71
|
+
return parseCodexAppServerModels({
|
|
72
|
+
...(typeof modelList === 'object' && modelList !== null ? modelList : {}),
|
|
73
|
+
config,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
for (const [id, request] of pending) {
|
|
78
|
+
clearTimeout(request.timer);
|
|
79
|
+
request.reject(new Error('codex app-server stopped'));
|
|
80
|
+
pending.delete(id);
|
|
81
|
+
}
|
|
82
|
+
proc.kill('SIGTERM');
|
|
83
|
+
setTimeout(() => proc.kill('SIGKILL'), 1000).unref();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function discoverCursorModels() {
|
|
87
|
+
const spec = resolveBuiltinCommand('cursor');
|
|
88
|
+
if (!spec)
|
|
89
|
+
return [];
|
|
90
|
+
const result = await runResolvedCommand(spec, ['--list-models']);
|
|
91
|
+
return parseCursorModels(result.stdout);
|
|
92
|
+
}
|
|
93
|
+
async function discoverOpenClawModels() {
|
|
94
|
+
const spec = resolveBuiltinCommand('openclaw');
|
|
95
|
+
if (!spec)
|
|
96
|
+
return [];
|
|
97
|
+
const result = await runResolvedCommand(spec, ['models', 'list', '--json']);
|
|
98
|
+
return parseOpenClawModels(result.stdout);
|
|
99
|
+
}
|
|
100
|
+
async function discoverClaudeModels() {
|
|
101
|
+
if (!resolveBuiltinCommand('claude'))
|
|
102
|
+
return [];
|
|
103
|
+
return fallbackClaudeAliasModels();
|
|
104
|
+
}
|
|
105
|
+
async function discoverCodexModels() {
|
|
106
|
+
const spec = resolveBuiltinCommand('codex');
|
|
107
|
+
if (!spec)
|
|
108
|
+
return [];
|
|
109
|
+
const appServerModels = await discoverCodexModelsViaAppServer(spec).catch(() => []);
|
|
110
|
+
return appServerModels;
|
|
111
|
+
}
|
|
112
|
+
async function discoverGeminiModels() {
|
|
113
|
+
if (!resolveBuiltinCommand('gemini'))
|
|
114
|
+
return [];
|
|
115
|
+
return fallbackGeminiModels();
|
|
116
|
+
}
|
|
117
|
+
const fallbackPiModels = [
|
|
118
|
+
{ id: 'qwen3.6-plus', name: 'Qwen 3.6 Plus', provider: 'dashscope', isDefault: true },
|
|
119
|
+
{ id: 'qwen3.6-flash', name: 'Qwen 3.6 Flash', provider: 'dashscope' },
|
|
120
|
+
];
|
|
121
|
+
async function discoverPiModels(context) {
|
|
122
|
+
if (!context.serverUrl || !context.authToken)
|
|
123
|
+
return fallbackPiModels;
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetch(`${context.serverUrl.replace(/\/$/, '')}/api/models`, {
|
|
126
|
+
headers: {
|
|
127
|
+
Authorization: `Bearer ${context.authToken}`,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
if (!response.ok)
|
|
131
|
+
return fallbackPiModels;
|
|
132
|
+
const data = (await response.json());
|
|
133
|
+
const models = (data.models ?? []).map((model) => ({
|
|
134
|
+
id: model.id,
|
|
135
|
+
name: model.name?.trim() || model.id,
|
|
136
|
+
provider: model.provider,
|
|
137
|
+
isDefault: model.id === data.default,
|
|
138
|
+
}));
|
|
139
|
+
return models.length > 0 ? models : fallbackPiModels;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return fallbackPiModels;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function discoverBuiltinModels(type, context) {
|
|
146
|
+
switch (type) {
|
|
147
|
+
case 'claude':
|
|
148
|
+
return discoverClaudeModels();
|
|
149
|
+
case 'codex':
|
|
150
|
+
return discoverCodexModels();
|
|
151
|
+
case 'gemini':
|
|
152
|
+
return discoverGeminiModels();
|
|
153
|
+
case 'cursor':
|
|
154
|
+
return discoverCursorModels();
|
|
155
|
+
case 'openclaw':
|
|
156
|
+
return discoverOpenClawModels();
|
|
157
|
+
case 'pi':
|
|
158
|
+
return discoverPiModels(context);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export async function discoverModelsForAgent(agent, context) {
|
|
162
|
+
if (agent.type.startsWith('custom:')) {
|
|
163
|
+
return agent.models ?? [];
|
|
164
|
+
}
|
|
165
|
+
return discoverBuiltinModels(agent.type, context);
|
|
166
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ModelInfo } from '@shennian/wire';
|
|
2
|
+
export declare function stripAnsi(text: string): string;
|
|
3
|
+
export declare function parseCursorModels(raw: string): ModelInfo[];
|
|
4
|
+
export declare function parseOpenClawModels(raw: string): ModelInfo[];
|
|
5
|
+
export declare function parseClaudeModels(raw: string): ModelInfo[];
|
|
6
|
+
export declare function parseClaudeBinaryModels(raw: string): ModelInfo[];
|
|
7
|
+
export declare function fallbackClaudeAliasModels(): ModelInfo[];
|
|
8
|
+
export declare function parseCodexModels(raw: string): ModelInfo[];
|
|
9
|
+
export declare function parseCodexAppServerModels(raw: unknown): ModelInfo[];
|
|
10
|
+
export declare function parseGeminiModels(raw: string): ModelInfo[];
|
|
11
|
+
export declare function fallbackGeminiModels(): ModelInfo[];
|