@openagents-org/agent-launcher 0.2.171 → 0.2.173

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.
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Antigravity CLI (agy) adapter for OpenAgents workspace.
3
+ *
4
+ * Successor to the Gemini CLI adapter: Google transitioned Gemini CLI into
5
+ * Antigravity CLI in May 2026 and cut individual-account access to the old
6
+ * CLI that June. agy is a closed-source Go binary — no npm runtime prefix and
7
+ * no .cmd/.js shim resolution needed — driven headless via
8
+ * `agy -p <prompt> --output-format stream-json`.
9
+ *
10
+ * Stream interpretation, argv building, and failure classification live in
11
+ * ./antigravity-stream.js (pure, unit-tested); this file owns process
12
+ * lifecycle and workspace I/O, structurally mirroring gemini.js.
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ const fs = require('fs');
18
+ const os = require('os');
19
+ const path = require('path');
20
+ const { execSync, spawn } = require('child_process');
21
+
22
+ const BaseAdapter = require('./base');
23
+ const { whereBinary } = require('../paths');
24
+ const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
25
+ const { buildClaudeSystemPrompt } = require('./workspace-prompt');
26
+ const {
27
+ buildAgyArgv,
28
+ parseAgyEvent,
29
+ AgyRunState,
30
+ classifyAgyFailure,
31
+ agyBinaryCandidates,
32
+ } = require('./antigravity-stream');
33
+
34
+ const IS_WINDOWS = process.platform === 'win32';
35
+
36
+ class AntigravityAdapter extends BaseAdapter {
37
+ /**
38
+ * @param {object} opts - BaseAdapter opts plus:
39
+ * @param {Set} [opts.disabledModules]
40
+ * @param {string} [opts.workingDir]
41
+ */
42
+ constructor(opts) {
43
+ super(opts);
44
+ this.disabledModules = opts.disabledModules || new Set();
45
+ // Read-only pinned context, same as gemini: agy has no MCP bridge to the
46
+ // workspace knowledge tools, so it can see the decision log but not write it.
47
+ this._usesPinnedContext = true;
48
+ this._channelConversations = {}; // channel → agy conversation_id
49
+ this._channelProcesses = {}; // channel → child process
50
+ this._sessionsFile = path.join(
51
+ os.homedir(), '.openagents', 'sessions',
52
+ `${this.workspaceId}_${this.agentName}_antigravity.json`
53
+ );
54
+ this._loadSessions();
55
+ }
56
+
57
+ _loadSessions() {
58
+ try {
59
+ if (fs.existsSync(this._sessionsFile)) {
60
+ const data = JSON.parse(fs.readFileSync(this._sessionsFile, 'utf-8'));
61
+ if (data && typeof data === 'object') {
62
+ Object.assign(this._channelConversations, data);
63
+ this._log(`Loaded ${Object.keys(data).length} conversation(s)`);
64
+ }
65
+ }
66
+ } catch {
67
+ this._log('Could not load sessions file, starting fresh');
68
+ }
69
+ }
70
+
71
+ _saveSessions() {
72
+ try {
73
+ const dir = path.dirname(this._sessionsFile);
74
+ fs.mkdirSync(dir, { recursive: true });
75
+ fs.writeFileSync(this._sessionsFile, JSON.stringify(this._channelConversations));
76
+ } catch {}
77
+ }
78
+
79
+ async _onControlAction(action, _payload) {
80
+ if (action === 'stop') {
81
+ await this._stopAllProcesses();
82
+ }
83
+ }
84
+
85
+ async _stopProcess(proc) {
86
+ if (!proc || proc.exitCode !== null) return;
87
+ try {
88
+ if (IS_WINDOWS) {
89
+ try { execSync(`taskkill /F /T /PID ${proc.pid}`, { timeout: 5000 }); } catch {}
90
+ } else {
91
+ try { process.kill(-proc.pid, 'SIGTERM'); } catch {
92
+ proc.kill('SIGTERM');
93
+ }
94
+ await new Promise((resolve) => {
95
+ const timeout = setTimeout(() => {
96
+ try { process.kill(-proc.pid, 'SIGKILL'); } catch {
97
+ proc.kill('SIGKILL');
98
+ }
99
+ resolve();
100
+ }, 5000);
101
+ proc.on('exit', () => { clearTimeout(timeout); resolve(); });
102
+ });
103
+ }
104
+ } catch {}
105
+ }
106
+
107
+ async _stopAllProcesses() {
108
+ const entries = Object.entries(this._channelProcesses);
109
+ if (!entries.length) return;
110
+ this._log(`Stopping ${entries.length} running process(es)...`);
111
+ for (const [channel, proc] of entries) {
112
+ await this._stopProcess(proc);
113
+ delete this._channelProcesses[channel];
114
+ delete this._channelQueues[channel];
115
+ try {
116
+ await this.sendStatus(channel, 'Execution stopped by user');
117
+ } catch {}
118
+ }
119
+ }
120
+
121
+ _findAgyBinary() {
122
+ // PATH first (codepage-safe lookup, see whereBinary), then the install
123
+ // script's fixed destinations.
124
+ const viaWhere = whereBinary('agy');
125
+ if (viaWhere) return viaWhere;
126
+ const candidates = agyBinaryCandidates({
127
+ home: os.homedir(),
128
+ isWindows: IS_WINDOWS,
129
+ localAppData: process.env.LOCALAPPDATA,
130
+ });
131
+ for (const c of candidates) {
132
+ if (fs.existsSync(c)) return c;
133
+ }
134
+ return null;
135
+ }
136
+
137
+ /**
138
+ * agy ignores a bare GEMINI_API_KEY unless the settings file also selects
139
+ * the gemini provider — and, inversely, refuses to start when the provider
140
+ * is selected but the key is missing. When the user configured a key for
141
+ * this agent, select the provider for them; never touch the file otherwise
142
+ * (a keyring/OAuth sign-in needs no provider entry and must not gain one,
143
+ * or the CLI stops starting the day the key is removed).
144
+ */
145
+ _ensureAgyAuth() {
146
+ if (this._agyAuthReady) return;
147
+ this._agyAuthReady = true;
148
+ try {
149
+ const env = this.agentEnv || process.env;
150
+ const apiKey = (env.GEMINI_API_KEY || '').trim();
151
+ if (!apiKey) return; // OAuth / keyring — leave the user's setup alone.
152
+ const dir = path.join(os.homedir(), '.gemini', 'antigravity-cli');
153
+ fs.mkdirSync(dir, { recursive: true });
154
+ const file = path.join(dir, 'settings.json');
155
+ let settings = {};
156
+ try { settings = JSON.parse(fs.readFileSync(file, 'utf-8')) || {}; } catch {}
157
+ if (settings.modelProvider === 'gemini') return; // already selected.
158
+ settings.modelProvider = 'gemini';
159
+ fs.writeFileSync(file, JSON.stringify(settings, null, 2));
160
+ this._log('Configured Antigravity CLI for API-key auth (~/.gemini/antigravity-cli/settings.json)');
161
+ } catch (e) {
162
+ this._log(`agy auth setup skipped: ${e.message}`);
163
+ }
164
+ }
165
+
166
+ _buildAgyCmd(prompt, channelName, { skipResume = false } = {}) {
167
+ this._ensureAgyAuth();
168
+ const agyBin = this._findAgyBinary();
169
+ if (!agyBin) {
170
+ throw new Error(
171
+ 'agy CLI not found. Install with: curl -fsSL https://antigravity.google/cli/install.sh | bash'
172
+ );
173
+ }
174
+
175
+ const pinned = this.pinnedPromptOpts(channelName);
176
+ const systemPrompt = '\n' + buildClaudeSystemPrompt({
177
+ agentName: this.agentName,
178
+ workspaceId: this.workspaceId,
179
+ channelName,
180
+ mode: this._mode,
181
+ decisionLog: pinned.decisionLog ? { ...pinned.decisionLog, writeAccess: false } : null,
182
+ glossary: pinned.glossary ? { ...pinned.glossary, writeAccess: false } : null,
183
+ });
184
+
185
+ // No system-prompt flag in agy's print mode either — prepend it.
186
+ const fullPrompt = `${systemPrompt}\n\n---\n\nUser message:\n${prompt}`;
187
+
188
+ const env = this.agentEnv || process.env;
189
+ const model = (env.ANTIGRAVITY_MODEL || env.AGY_MODEL || '').trim();
190
+
191
+ const args = buildAgyArgv({
192
+ prompt: fullPrompt,
193
+ model,
194
+ conversationId: this._channelConversations[channelName],
195
+ skipResume,
196
+ });
197
+ return { cmd: [agyBin, ...args] };
198
+ }
199
+
200
+ async _handleMessage(msg) {
201
+ let content = (msg.content || '').trim();
202
+ const attachments = msg.attachments || [];
203
+
204
+ const attText = formatAttachmentsForPrompt(attachments);
205
+ if (attText) {
206
+ content = content ? content + attText : attText.trim();
207
+ }
208
+
209
+ if (!content) return;
210
+
211
+ const msgChannel = msg.sessionId || this.channelName;
212
+ const sender = msg.senderName || msg.senderType || 'user';
213
+ this._log(`Processing message from ${sender} in ${msgChannel}: ${content.slice(0, 80)}...`);
214
+
215
+ if (!this._titledSessions.has(msgChannel)) {
216
+ this._titledSessions.add(msgChannel);
217
+ try {
218
+ const info = await this.client.getSession(this.workspaceId, msgChannel, this.token);
219
+ const resumeFrom = info.resumeFrom;
220
+ if (resumeFrom && !this._channelConversations[msgChannel]) {
221
+ const sourceConversation = this._channelConversations[resumeFrom];
222
+ if (sourceConversation) {
223
+ this._channelConversations[msgChannel] = sourceConversation;
224
+ this._saveSessions();
225
+ this._log(`Resuming channel ${msgChannel} from ${resumeFrom}`);
226
+ }
227
+ }
228
+ const title = generateSessionTitle(content);
229
+ if (title && !info.titleManuallySet && SESSION_DEFAULT_RE.test(info.title || '')) {
230
+ await this.client.updateSession(
231
+ this.workspaceId, msgChannel, this.token,
232
+ { title, autoTitle: true }
233
+ );
234
+ }
235
+ } catch {}
236
+ }
237
+
238
+ await this.sendStatus(msgChannel, 'thinking...');
239
+
240
+ let cmd;
241
+ const cleanEnv = { ...(this.agentEnv || process.env) };
242
+
243
+ let _shouldRetry = false;
244
+ for (let attempt = 0; attempt < 2; attempt++) {
245
+ try {
246
+ const built = this._buildAgyCmd(content, msgChannel, { skipResume: attempt > 0 });
247
+ cmd = built.cmd;
248
+ } catch (e) {
249
+ await this.sendError(msgChannel, e.message);
250
+ return;
251
+ }
252
+
253
+ try {
254
+ const proc = spawn(cmd[0], cmd.slice(1), {
255
+ stdio: ['ignore', 'pipe', 'pipe'],
256
+ env: cleanEnv,
257
+ cwd: this.workingDir,
258
+ detached: !IS_WINDOWS,
259
+ windowsHide: true,
260
+ });
261
+ this._channelProcesses[msgChannel] = proc;
262
+
263
+ const run = new AgyRunState();
264
+ let stderrBuf = '';
265
+ let lineBuffer = '';
266
+ let _pendingLines = Promise.resolve();
267
+
268
+ if (proc.stderr) {
269
+ proc.stderr.on('data', (chunk) => { stderrBuf += chunk.toString('utf-8'); });
270
+ }
271
+
272
+ _shouldRetry = await new Promise((resolve, reject) => {
273
+ let consecutiveTimeouts = 0;
274
+ let lastDataTime = Date.now();
275
+ let timeoutTimer = null;
276
+
277
+ const resetTimeout = () => {
278
+ consecutiveTimeouts = 0;
279
+ lastDataTime = Date.now();
280
+ };
281
+
282
+ const startTimeoutMonitor = () => {
283
+ timeoutTimer = setInterval(async () => {
284
+ const elapsed = Date.now() - lastDataTime;
285
+ if (elapsed >= 15000) {
286
+ consecutiveTimeouts++;
287
+ lastDataTime = Date.now();
288
+ if (consecutiveTimeouts === 2) {
289
+ try { await this.sendStatus(msgChannel, 'Processing...'); } catch {}
290
+ }
291
+ if (consecutiveTimeouts >= 20) {
292
+ this._log(`Process idle for ${consecutiveTimeouts * 15}s, killing...`);
293
+ await this._stopProcess(proc);
294
+ }
295
+ }
296
+ }, 15000);
297
+ };
298
+ startTimeoutMonitor();
299
+
300
+ const processLine = async (line) => {
301
+ const event = parseAgyEvent(line);
302
+ if (!event) return;
303
+ resetTimeout();
304
+ for (const action of run.consume(event)) {
305
+ if (action.type === 'conversation') {
306
+ this._channelConversations[msgChannel] = action.id;
307
+ this._saveSessions();
308
+ } else if (action.type === 'thinking') {
309
+ try { await this.sendThinking(msgChannel, action.text); } catch {}
310
+ } else if (action.type === 'tool') {
311
+ try { await this.sendStatus(msgChannel, `${action.name} › ${action.preview}`); } catch {}
312
+ }
313
+ // 'result' is read from `run` after exit.
314
+ }
315
+ };
316
+
317
+ proc.on('exit', async (code) => {
318
+ if (timeoutTimer) clearInterval(timeoutTimer);
319
+
320
+ try { await _pendingLines; } catch {}
321
+
322
+ const lines = lineBuffer.split('\n');
323
+ for (const line of lines) {
324
+ try { await processLine(line); } catch {}
325
+ }
326
+
327
+ delete this._channelProcesses[msgChannel];
328
+
329
+ if (code !== 0) {
330
+ this._log(`CLI exited with code ${code}`);
331
+ if (stderrBuf.trim()) {
332
+ this._log(`stderr: ${stderrBuf.trim().slice(0, 500)}`);
333
+ }
334
+ }
335
+
336
+ const response = run.finalResponse();
337
+ const failed = code !== 0 || run.status === 'ERROR';
338
+
339
+ if (response && !failed) {
340
+ try { await this.sendResponse(msgChannel, response); } catch {}
341
+ resolve(false);
342
+ } else if (failed && this._channelConversations[msgChannel] && attempt === 0) {
343
+ // A stored conversation id the CLI no longer knows fails the
344
+ // whole run — clear it and retry once from scratch.
345
+ this._log(`Stale conversation for ${msgChannel}, clearing and retrying without resume`);
346
+ delete this._channelConversations[msgChannel];
347
+ this._saveSessions();
348
+ resolve(true);
349
+ } else if (failed) {
350
+ // Surface the real reason (auth, provider config, bad model)
351
+ // instead of a generic retry prompt — the gemini adapter's
352
+ // silent-stderr dead end is what this branch fixes.
353
+ const { kind, message } = classifyAgyFailure({ code, stderr: stderrBuf, error: run.error });
354
+ this._log(`run failed (${kind})`);
355
+ try { await this.sendError(msgChannel, message); } catch {}
356
+ resolve(false);
357
+ } else {
358
+ if (!run.sawText()) {
359
+ try { await this.sendResponse(msgChannel, 'No response generated. Please try again.'); } catch {}
360
+ }
361
+ resolve(false);
362
+ }
363
+ });
364
+
365
+ proc.on('error', (err) => {
366
+ if (timeoutTimer) clearInterval(timeoutTimer);
367
+ reject(err);
368
+ });
369
+
370
+ proc.stdout.on('data', (chunk) => {
371
+ lineBuffer += chunk.toString('utf-8');
372
+ resetTimeout();
373
+ const lines = lineBuffer.split('\n');
374
+ lineBuffer = lines.pop();
375
+ for (const line of lines) {
376
+ _pendingLines = _pendingLines.then(() => processLine(line)).catch(() => {});
377
+ }
378
+ });
379
+ });
380
+ } catch (e) {
381
+ this._log(`Error handling message: ${e.message}`);
382
+ await this.sendError(msgChannel, `Error processing message: ${e.message}`);
383
+ break;
384
+ }
385
+ if (!_shouldRetry) break;
386
+ }
387
+ }
388
+ }
389
+
390
+ module.exports = AntigravityAdapter;
@@ -81,6 +81,10 @@ class BaseAdapter {
81
81
  this._processedIds = new Set();
82
82
  this._titledSessions = new Set();
83
83
  this._mode = 'execute';
84
+ // Workspace-selected model id (WorkspaceMember.model). Synced from
85
+ // discover at startup and via the `model.set` control event; null means
86
+ // the agent CLI runs with its own default.
87
+ this.workspaceModel = null;
84
88
  this._lastControlId = null;
85
89
  this._controlWake = null;
86
90
  // Per-channel task tracking for parallel execution
@@ -212,6 +216,10 @@ class BaseAdapter {
212
216
  this.disabledModules = skillsToDisabledModules(self.enabledSkills);
213
217
  this._log(`Synced skills from workspace: disabled=[${[...this.disabledModules].join(',')}]`);
214
218
  }
219
+ if (self && self.model) {
220
+ this.workspaceModel = self.model;
221
+ this._log(`Synced model from workspace: ${this.workspaceModel}`);
222
+ }
215
223
  } catch (e) {
216
224
  this._log(`Warning: skill sync failed (non-fatal): ${e.message}`);
217
225
  }
@@ -360,6 +368,11 @@ class BaseAdapter {
360
368
  await this._handleSkillInstall(payload);
361
369
  } else if (action === 'skill.uninstall') {
362
370
  await this._handleSkillUninstall(payload);
371
+ } else if (action === 'model.set') {
372
+ // Adapters read workspaceModel when (re)spawning their CLI; ones with
373
+ // a staleness check (claude.js) respawn on the next message.
374
+ this.workspaceModel = (payload && payload.model) || null;
375
+ this._log(`Workspace model set to ${this.workspaceModel || '(default)'}`);
363
376
  }
364
377
  }
365
378
 
@@ -471,6 +471,10 @@ class ClaudeAdapter extends BaseAdapter {
471
471
  const cmd = [claudeBin, '-p', prompt, '--output-format', 'stream-json', '--verbose'];
472
472
 
473
473
  cmd.push('--append-system-prompt', systemPrompt);
474
+ // Workspace-picked model (agent profile panel). Absent = CLI's own default.
475
+ if (this.workspaceModel) {
476
+ cmd.push('--model', this.workspaceModel);
477
+ }
474
478
  const disallowedTools = ['AskUserQuestion', 'CronCreate', 'CronDelete', 'CronList', 'ScheduleWakeup'];
475
479
  if (browserEnabled) {
476
480
  // Hard-ban the native WebFetch: it can't render JS and bypasses the
@@ -1165,13 +1169,16 @@ class ClaudeAdapter extends BaseAdapter {
1165
1169
  // read-only plan process serving execute requests.
1166
1170
  const modeStale = existingPP.spawnMode !== this._mode;
1167
1171
  const pinnedStale = pinnedHash !== null && existingPP.pinnedHash !== pinnedHash;
1168
- if (modeStale || pinnedStale) {
1172
+ const modelStale = (existingPP.spawnModel || null) !== (this.workspaceModel || null);
1173
+ if (modeStale || pinnedStale || modelStale) {
1169
1174
  // Kill it and fall through to a fresh spawn: --resume keeps the
1170
1175
  // conversation (it lives in the CLI transcript), while the new spawn
1171
1176
  // carries the current mode and re-pins the current knowledge.
1172
1177
  this._log(modeStale
1173
1178
  ? `Mode changed to ${this._mode} for ${msgChannel} — respawning with resume`
1174
- : `Pinned knowledge changed for ${msgChannel} — respawning with resume to re-pin it`);
1179
+ : modelStale
1180
+ ? `Model changed to ${this.workspaceModel || '(default)'} for ${msgChannel} — respawning with resume`
1181
+ : `Pinned knowledge changed for ${msgChannel} — respawning with resume to re-pin it`);
1175
1182
  await this._killPersistentProc(msgChannel);
1176
1183
  } else {
1177
1184
  this._log(`Reusing persistent process for ${msgChannel}`);
@@ -1258,6 +1265,7 @@ class ClaudeAdapter extends BaseAdapter {
1258
1265
  { entryId: glossaryOpt && glossaryOpt.entryId, content: glossaryOpt && glossaryOpt.content },
1259
1266
  ]);
1260
1267
  pp.spawnMode = this._mode;
1268
+ pp.spawnModel = this.workspaceModel || null;
1261
1269
  this._log(`Spawned persistent process for ${msgChannel} (attempt ${attempt + 1})`);
1262
1270
 
1263
1271
  const result = await this._sendToPersistentProc(pp, effectiveContent);
@@ -22,6 +22,7 @@ const AmpAdapter = require('./amp');
22
22
  const MiniSweAgentAdapter = require('./mini');
23
23
  const PiAdapter = require('./pi');
24
24
  const DeepSeekAdapter = require('./deepseek');
25
+ const AntigravityAdapter = require('./antigravity');
25
26
 
26
27
  const ADAPTER_MAP = {
27
28
  openclaw: OpenClawAdapter,
@@ -41,11 +42,12 @@ const ADAPTER_MAP = {
41
42
  'mini-swe-agent': MiniSweAgentAdapter,
42
43
  pi: PiAdapter,
43
44
  deepseek: DeepSeekAdapter,
45
+ antigravity: AntigravityAdapter,
44
46
  };
45
47
 
46
48
  /**
47
49
  * Create an adapter instance for the given agent type.
48
- * @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes, gemini, kimi, aider, goose, copilot, cline, amp, mini-swe-agent, pi, deepseek)
50
+ * @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes, gemini, kimi, aider, goose, copilot, cline, amp, mini-swe-agent, pi, deepseek, antigravity)
49
51
  * @param {object} opts - Adapter constructor options
50
52
  * @returns {BaseAdapter}
51
53
  */
@@ -76,6 +78,7 @@ module.exports = {
76
78
  MiniSweAgentAdapter,
77
79
  PiAdapter,
78
80
  DeepSeekAdapter,
81
+ AntigravityAdapter,
79
82
  createAdapter,
80
83
  ADAPTER_MAP,
81
84
  };
package/src/cli.js CHANGED
@@ -806,6 +806,37 @@ async function cmdTestLLM(connector, _flags, positional) {
806
806
  }
807
807
  }
808
808
 
809
+ async function cmdProbe(connector, flags, positional) {
810
+ const target = positional[0];
811
+ if (!target) { print('Usage: agn probe <type|agent> [--json]'); return; }
812
+
813
+ // Accept an agent name as a convenience — probe its type.
814
+ let type = target;
815
+ try {
816
+ const agent = connector.config.getAgent(target);
817
+ if (agent) type = agent.type || 'openclaw';
818
+ } catch {}
819
+
820
+ const result = await connector.probeAgentType(type);
821
+
822
+ if (flags && flags.json) {
823
+ // Machine consumers (the daemon's probe_agent node command) read ONLY
824
+ // stdout JSON — same contract as `agn runtimes --json`.
825
+ process.stdout.write(JSON.stringify(result));
826
+ if (!result.ok) process.exitCode = 1;
827
+ return;
828
+ }
829
+
830
+ if (result.ok) {
831
+ print(`OK (${result.method}, ${result.durationMs}ms)${result.reply ? ` — ${result.reply}` : ''}`);
832
+ if (result.code === 'static_only') print(result.message);
833
+ } else {
834
+ print(`FAILED [${result.code}] ${result.message || ''}`.trim());
835
+ for (const line of result.guidance || []) print(` → ${line}`);
836
+ process.exitCode = 1;
837
+ }
838
+ }
839
+
809
840
  async function cmdVersion() {
810
841
  const pkg = require('../package.json');
811
842
  print(`${pkg.name} v${pkg.version}`);
@@ -874,6 +905,7 @@ Commands:
874
905
  tool-mode [agent] [mode] View/set tool mode (mcp or skills)
875
906
  autostart [--disable] Enable/disable auto-start on login
876
907
  test-llm <type> Test LLM connection
908
+ probe <type|agent> [--json] Smoke-test an agent (tiny end-to-end prompt)
877
909
  logs [agent] [--lines N] View daemon logs
878
910
  workspace create [name] Create a new workspace
879
911
  workspace join <token> Join workspace with token
@@ -962,6 +994,7 @@ async function main() {
962
994
  skills: () => cmdSkills(connector, flags, positional),
963
995
  'tool-mode': () => cmdToolMode(connector, flags, positional),
964
996
  'test-llm': () => cmdTestLLM(connector, flags, positional),
997
+ probe: () => cmdProbe(connector, flags, positional),
965
998
  update: () => cmdUpdate(connector),
966
999
  'mcp-server': () => {
967
1000
  const { runMcpServer } = require('./mcp-server');