@kdejaeger/pi-model-router 0.3.1
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/LICENSE +21 -0
- package/README.md +468 -0
- package/docs/ARCHITECTURE.md +75 -0
- package/extensions/commands.ts +452 -0
- package/extensions/config.ts +405 -0
- package/extensions/index.ts +412 -0
- package/extensions/provider.ts +442 -0
- package/extensions/routing.ts +290 -0
- package/extensions/state.ts +39 -0
- package/extensions/types.ts +74 -0
- package/extensions/ui.ts +54 -0
- package/model-router.example.json +48 -0
- package/package.json +54 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from '@earendil-works/pi-coding-agent';
|
|
5
|
+
import type { AutocompleteItem } from '@earendil-works/pi-tui';
|
|
6
|
+
import type {
|
|
7
|
+
RouterConfig,
|
|
8
|
+
RouterPinByProfile,
|
|
9
|
+
RoutingDecision,
|
|
10
|
+
} from './types';
|
|
11
|
+
import {
|
|
12
|
+
profileNames,
|
|
13
|
+
ROUTER_PIN_VALUES,
|
|
14
|
+
resolveModelFromRef,
|
|
15
|
+
isPinValue,
|
|
16
|
+
} from './config';
|
|
17
|
+
import {
|
|
18
|
+
formatPinSummary,
|
|
19
|
+
formatDecision,
|
|
20
|
+
} from './ui';
|
|
21
|
+
|
|
22
|
+
export const registerCommands = (
|
|
23
|
+
pi: ExtensionAPI,
|
|
24
|
+
state: {
|
|
25
|
+
readonly currentConfig: RouterConfig;
|
|
26
|
+
routerEnabled: boolean;
|
|
27
|
+
selectedProfile: string | undefined;
|
|
28
|
+
readonly pinnedTierByProfile: RouterPinByProfile;
|
|
29
|
+
readonly lastDecision: RoutingDecision | undefined;
|
|
30
|
+
lastNonRouterModel: string | undefined;
|
|
31
|
+
debugEnabled: boolean;
|
|
32
|
+
readonly debugHistory: RoutingDecision[];
|
|
33
|
+
readonly lastConfigWarnings: string[];
|
|
34
|
+
},
|
|
35
|
+
actions: {
|
|
36
|
+
persistState: () => void;
|
|
37
|
+
updateStatus: (ctx: ExtensionContext) => void;
|
|
38
|
+
reloadConfig: (
|
|
39
|
+
ctx?: ExtensionContext,
|
|
40
|
+
options?: { preserveDebug?: boolean },
|
|
41
|
+
) => void;
|
|
42
|
+
ensureValidActiveRouterProfile: (ctx: ExtensionContext) => Promise<void>;
|
|
43
|
+
switchToRouterProfile: (
|
|
44
|
+
profileName: string,
|
|
45
|
+
ctx: ExtensionContext,
|
|
46
|
+
strict?: boolean,
|
|
47
|
+
) => Promise<boolean>;
|
|
48
|
+
},
|
|
49
|
+
) => {
|
|
50
|
+
const SUBCOMMAND_DETAILS = [
|
|
51
|
+
{ name: 'status', desc: 'Show current router status' },
|
|
52
|
+
{ name: 'profile', desc: 'Switch to a different router profile' },
|
|
53
|
+
{ name: 'pin', desc: 'Pin routing for a profile to a tier, or clear' },
|
|
54
|
+
{ name: 'disable', desc: 'Disable the router and restore last model' },
|
|
55
|
+
|
|
56
|
+
{ name: 'debug', desc: 'Toggle or clear router debug history' },
|
|
57
|
+
{ name: 'reload', desc: 'Reload the model router configuration' },
|
|
58
|
+
{ name: 'help', desc: 'Show usage help for subcommands' },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const getSubcommandCompletions = (
|
|
62
|
+
prefix: string,
|
|
63
|
+
): AutocompleteItem[] | null => {
|
|
64
|
+
const items = SUBCOMMAND_DETAILS.filter((s) =>
|
|
65
|
+
s.name.startsWith(prefix),
|
|
66
|
+
).map((s) => ({
|
|
67
|
+
value: s.name,
|
|
68
|
+
label: s.name,
|
|
69
|
+
description: s.desc,
|
|
70
|
+
}));
|
|
71
|
+
return items.length > 0 ? items : null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const getPinCompletions = (args: string[]): AutocompleteItem[] | null => {
|
|
75
|
+
// pin [profile] <tier|clear>
|
|
76
|
+
if (args.length <= 1) {
|
|
77
|
+
const token = args[0] ?? '';
|
|
78
|
+
const pinItems = ROUTER_PIN_VALUES.filter((value) =>
|
|
79
|
+
value.startsWith(token),
|
|
80
|
+
).map((value) => ({ value, label: value }));
|
|
81
|
+
const profileItems = profileNames(state.currentConfig)
|
|
82
|
+
.filter((name) => name.startsWith(token))
|
|
83
|
+
.map((name) => ({ value: name, label: `router/${name}` }));
|
|
84
|
+
const items = [...pinItems, ...profileItems];
|
|
85
|
+
return items.length > 0 ? items : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const profileToken = args[0];
|
|
89
|
+
if (!state.currentConfig.profiles[profileToken]) return null;
|
|
90
|
+
const pinPrefix = args[1] ?? '';
|
|
91
|
+
const items = ROUTER_PIN_VALUES.filter((value) =>
|
|
92
|
+
value.startsWith(pinPrefix),
|
|
93
|
+
).map((value) => ({
|
|
94
|
+
value: `${profileToken} ${value}`,
|
|
95
|
+
label: `${profileToken} ${value}`,
|
|
96
|
+
}));
|
|
97
|
+
return items.length > 0 ? items : null;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const getActiveProfileOrWarn = (ctx: ExtensionContext): string | undefined => {
|
|
101
|
+
if (!state.selectedProfile) {
|
|
102
|
+
ctx.ui.notify('No router profile is active. Select a router model first.', 'error');
|
|
103
|
+
}
|
|
104
|
+
return state.selectedProfile;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleStatus = async (args: string[], ctx: ExtensionContext) => {
|
|
108
|
+
if (args.length > 0) {
|
|
109
|
+
ctx.ui.notify('Usage: /router status (no arguments)', 'error');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const profilePin = state.selectedProfile
|
|
113
|
+
? state.pinnedTierByProfile[state.selectedProfile] ?? 'none'
|
|
114
|
+
: 'none';
|
|
115
|
+
const lines = [
|
|
116
|
+
`Router enabled: ${state.routerEnabled ? 'yes' : 'off'}`,
|
|
117
|
+
`Selected profile: ${state.selectedProfile ?? 'none'}`,
|
|
118
|
+
`Selected profile pin: ${profilePin}`,
|
|
119
|
+
`Pins by profile: ${formatPinSummary(state.pinnedTierByProfile)}`,
|
|
120
|
+
`Available profiles: ${profileNames(state.currentConfig).join(', ')}`,
|
|
121
|
+
`Last non-router model: ${state.lastNonRouterModel ?? 'none'}`,
|
|
122
|
+
`Debug: ${state.debugEnabled ? 'on' : 'off'}`,
|
|
123
|
+
`Debug history: ${state.debugHistory.length} decisions`,
|
|
124
|
+
];
|
|
125
|
+
if (state.lastDecision) {
|
|
126
|
+
lines.push(
|
|
127
|
+
`Last routed tier: ${state.lastDecision.tier}`,
|
|
128
|
+
`Last model: ${state.lastDecision.targetProvider}/${state.lastDecision.targetModelId} (${state.lastDecision.thinking})`,
|
|
129
|
+
`Reason: ${state.lastDecision.reasoning}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (state.lastConfigWarnings.length > 0) {
|
|
133
|
+
lines.push('', '⚠️ Configuration Warnings:', ...state.lastConfigWarnings.map((w) => ` - ${w}`));
|
|
134
|
+
}
|
|
135
|
+
ctx.ui.notify(lines.join('\n'), 'info');
|
|
136
|
+
actions.updateStatus(ctx);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const handleProfile = async (args: string[], ctx: ExtensionContext) => {
|
|
140
|
+
if (args.length > 1) {
|
|
141
|
+
ctx.ui.notify('Usage: /router profile [name]', 'error');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!args[0]) {
|
|
145
|
+
ctx.ui.notify(
|
|
146
|
+
`Current profile: ${state.selectedProfile}. Available: ${profileNames(state.currentConfig).join(', ')}`,
|
|
147
|
+
'info',
|
|
148
|
+
);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const success = await actions.switchToRouterProfile(args[0], ctx);
|
|
152
|
+
if (success) {
|
|
153
|
+
ctx.ui.notify(`Switched to router profile: ${state.selectedProfile}`, 'info');
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const handlePin = async (args: string[], ctx: ExtensionContext) => {
|
|
158
|
+
const currentProfile = getActiveProfileOrWarn(ctx);
|
|
159
|
+
if (!currentProfile) return;
|
|
160
|
+
|
|
161
|
+
const pinUsage = 'Usage: /router pin [profile] <high|medium|low|clear>';
|
|
162
|
+
|
|
163
|
+
if (args.length === 0) {
|
|
164
|
+
ctx.ui.notify(
|
|
165
|
+
[
|
|
166
|
+
`Profile: ${currentProfile}`,
|
|
167
|
+
`Pinned tier: ${state.pinnedTierByProfile[currentProfile] ?? 'none'}`,
|
|
168
|
+
`Pins by profile: ${formatPinSummary(state.pinnedTierByProfile)}`,
|
|
169
|
+
pinUsage,
|
|
170
|
+
].join('\n'),
|
|
171
|
+
'info',
|
|
172
|
+
);
|
|
173
|
+
actions.updateStatus(ctx);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (args.length > 2) {
|
|
178
|
+
ctx.ui.notify(pinUsage, 'error');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let profileName: string;
|
|
183
|
+
let pinValue: string;
|
|
184
|
+
|
|
185
|
+
if (args.length === 1) {
|
|
186
|
+
// pin <value>
|
|
187
|
+
profileName = currentProfile;
|
|
188
|
+
pinValue = args[0];
|
|
189
|
+
} else if (args[0] in state.currentConfig.profiles) {
|
|
190
|
+
// pin <profile> <value>
|
|
191
|
+
[profileName, pinValue] = args;
|
|
192
|
+
} else {
|
|
193
|
+
// pin <unknown> <value> — the first arg isn't a valid profile
|
|
194
|
+
ctx.ui.notify(`Unknown router profile: ${args[0]}`, 'error');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!isPinValue(pinValue)) {
|
|
199
|
+
ctx.ui.notify(
|
|
200
|
+
`Invalid router pin: ${pinValue}. Use high, medium, low, or clear`,
|
|
201
|
+
'error',
|
|
202
|
+
);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const nextTier = pinValue === 'clear' ? undefined : pinValue;
|
|
207
|
+
if (nextTier) {
|
|
208
|
+
const profile = state.currentConfig.profiles[profileName];
|
|
209
|
+
if (!profile || !profile[nextTier]) {
|
|
210
|
+
ctx.ui.notify(
|
|
211
|
+
`Profile "${profileName}" has no "${nextTier}" tier configured. All three tiers (high, medium, low) are required.`,
|
|
212
|
+
'error',
|
|
213
|
+
);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
state.pinnedTierByProfile[profileName] = nextTier;
|
|
217
|
+
} else {
|
|
218
|
+
delete state.pinnedTierByProfile[profileName];
|
|
219
|
+
}
|
|
220
|
+
actions.persistState();
|
|
221
|
+
actions.updateStatus(ctx);
|
|
222
|
+
ctx.ui.notify(
|
|
223
|
+
nextTier
|
|
224
|
+
? `Router profile '${profileName}' pinned to ${nextTier}`
|
|
225
|
+
: `Router profile ${profileName} pin cleared; classifier routing restored`,
|
|
226
|
+
'info',
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const handleDisable = async (args: string[], ctx: ExtensionContext) => {
|
|
231
|
+
if (args.length > 0) {
|
|
232
|
+
ctx.ui.notify('Usage: /router disable (no arguments)', 'error');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (!state.lastNonRouterModel) {
|
|
236
|
+
ctx.ui.notify('No previous non-router model recorded. Use /model to pick a concrete model.', 'warning');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const targetModel = resolveModelFromRef(state.lastNonRouterModel, ctx.modelRegistry);
|
|
240
|
+
if (!targetModel) {
|
|
241
|
+
ctx.ui.notify(
|
|
242
|
+
`Recorded non-router model is unavailable: ${state.lastNonRouterModel}`,
|
|
243
|
+
'error',
|
|
244
|
+
);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const success = await pi.setModel(targetModel);
|
|
248
|
+
if (!success) {
|
|
249
|
+
ctx.ui.notify(`Failed to switch to ${state.lastNonRouterModel}`, 'error');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
state.routerEnabled = false;
|
|
253
|
+
actions.persistState();
|
|
254
|
+
pi.setThinkingLevel('off');
|
|
255
|
+
actions.updateStatus(ctx);
|
|
256
|
+
ctx.ui.notify(
|
|
257
|
+
`Router disabled. Restored ${state.lastNonRouterModel}`,
|
|
258
|
+
'info',
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const handleDebug = async (args: string[], ctx: ExtensionContext) => {
|
|
263
|
+
if (args.length > 1) {
|
|
264
|
+
ctx.ui.notify('Usage: /router debug <on|off|show|clear>', 'error');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const cmd = args[0]?.toLowerCase();
|
|
268
|
+
if (cmd === 'on') state.debugEnabled = true;
|
|
269
|
+
else if (cmd === 'off') state.debugEnabled = false;
|
|
270
|
+
else if (cmd === 'clear') state.debugHistory.length = 0;
|
|
271
|
+
else if (cmd === 'show') {
|
|
272
|
+
if (state.debugHistory.length === 0) {
|
|
273
|
+
ctx.ui.notify('No recent routing decisions.', 'info');
|
|
274
|
+
} else {
|
|
275
|
+
const history = state.debugHistory.map(formatDecision).join('\n');
|
|
276
|
+
ctx.ui.notify(`Recent Routing Decisions:\n${history}`, 'info');
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
} else {
|
|
280
|
+
state.debugEnabled = !state.debugEnabled;
|
|
281
|
+
}
|
|
282
|
+
actions.persistState();
|
|
283
|
+
actions.updateStatus(ctx);
|
|
284
|
+
ctx.ui.notify(
|
|
285
|
+
`Router debug ${state.debugEnabled ? 'enabled' : 'disabled'}.`,
|
|
286
|
+
'info',
|
|
287
|
+
);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const handleReload = async (args: string[], ctx: ExtensionContext) => {
|
|
291
|
+
if (args.length > 0) {
|
|
292
|
+
ctx.ui.notify('Usage: /router reload (no arguments)', 'error');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
actions.reloadConfig(ctx, { preserveDebug: true });
|
|
296
|
+
await actions.ensureValidActiveRouterProfile(ctx);
|
|
297
|
+
actions.updateStatus(ctx);
|
|
298
|
+
|
|
299
|
+
if (state.lastConfigWarnings.length > 0) {
|
|
300
|
+
ctx.ui.notify(`Router reload warnings:\n${state.lastConfigWarnings.join('\n')}`, 'warning');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
ctx.ui.notify(
|
|
304
|
+
`Router config reloaded. Profiles: ${profileNames(state.currentConfig).join(', ')}`,
|
|
305
|
+
'info',
|
|
306
|
+
);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
pi.registerCommand('router', {
|
|
310
|
+
description: 'Model router control center',
|
|
311
|
+
getArgumentCompletions: (prefix) => {
|
|
312
|
+
const trimmedLeft = prefix.trimStart();
|
|
313
|
+
const hasTrailingSpace = /\s$/.test(prefix);
|
|
314
|
+
const parts = trimmedLeft.length > 0 ? trimmedLeft.split(/\s+/) : [];
|
|
315
|
+
|
|
316
|
+
if (parts.length === 0) {
|
|
317
|
+
return getSubcommandCompletions('');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (parts.length === 1 && !hasTrailingSpace) {
|
|
321
|
+
return getSubcommandCompletions(parts[0]);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const subcommand = parts[0];
|
|
325
|
+
const subArgs = parts.slice(1);
|
|
326
|
+
if (hasTrailingSpace && parts.length === 1) {
|
|
327
|
+
subArgs.push('');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
switch (subcommand) {
|
|
331
|
+
case 'profile': {
|
|
332
|
+
const profilePrefix = subArgs[0] ?? '';
|
|
333
|
+
const items = profileNames(state.currentConfig)
|
|
334
|
+
.filter((name) => name.startsWith(profilePrefix))
|
|
335
|
+
.map((name) => ({
|
|
336
|
+
value: `profile ${name}`,
|
|
337
|
+
label: `router/${name}`,
|
|
338
|
+
description: `Switch to router profile "${name}"`,
|
|
339
|
+
}));
|
|
340
|
+
return items.length > 0 ? items : null;
|
|
341
|
+
}
|
|
342
|
+
case 'pin': {
|
|
343
|
+
const completions = getPinCompletions(subArgs);
|
|
344
|
+
return (
|
|
345
|
+
completions?.map((c) => {
|
|
346
|
+
// c.value is either a profile name, pin value, or "profile pinValue"
|
|
347
|
+
const spaceIdx = c.value.indexOf(' ');
|
|
348
|
+
const hasProfileAndPin = spaceIdx !== -1;
|
|
349
|
+
// c.value is a pin value (high/medium/low/clear), "profile pinValue", or bare profile name
|
|
350
|
+
const isProfile = state.currentConfig.profiles[c.value] !== undefined;
|
|
351
|
+
const desc = hasProfileAndPin
|
|
352
|
+
? c.value.slice(spaceIdx + 1) === 'clear'
|
|
353
|
+
? `Clear pin on profile '${c.value.slice(0, spaceIdx)}'`
|
|
354
|
+
: `Pin profile '${c.value.slice(0, spaceIdx)}' to ${c.value.slice(spaceIdx + 1)}`
|
|
355
|
+
: isProfile
|
|
356
|
+
? `Pin '${c.label}' to...`
|
|
357
|
+
: c.value === 'clear'
|
|
358
|
+
? 'Clear pin on current profile'
|
|
359
|
+
: `Pin current profile to ${c.label}`;
|
|
360
|
+
return { ...c, value: `pin ${c.value}`, description: desc };
|
|
361
|
+
}) ?? null
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
case 'debug': {
|
|
365
|
+
const debugPrefix = subArgs[0] ?? '';
|
|
366
|
+
const items = ['on', 'off', 'toggle', 'clear', 'show']
|
|
367
|
+
.filter((v) => v.startsWith(debugPrefix))
|
|
368
|
+
.map((v) => ({
|
|
369
|
+
value: `debug ${v}`,
|
|
370
|
+
label: v,
|
|
371
|
+
description: `Router debug: ${v}`,
|
|
372
|
+
}));
|
|
373
|
+
return items.length > 0 ? items : null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return null;
|
|
378
|
+
},
|
|
379
|
+
handler: async (args, ctx) => {
|
|
380
|
+
const parts = args?.trim().split(/\s+/) ?? [];
|
|
381
|
+
const subcommand = parts[0];
|
|
382
|
+
const subArgs = parts.slice(1);
|
|
383
|
+
|
|
384
|
+
switch (subcommand) {
|
|
385
|
+
case 'profile':
|
|
386
|
+
await handleProfile(subArgs, ctx);
|
|
387
|
+
break;
|
|
388
|
+
case 'pin':
|
|
389
|
+
await handlePin(subArgs, ctx);
|
|
390
|
+
break;
|
|
391
|
+
case 'disable':
|
|
392
|
+
await handleDisable(subArgs, ctx);
|
|
393
|
+
break;
|
|
394
|
+
case 'debug':
|
|
395
|
+
await handleDebug(subArgs, ctx);
|
|
396
|
+
break;
|
|
397
|
+
case 'reload':
|
|
398
|
+
await handleReload(subArgs, ctx);
|
|
399
|
+
break;
|
|
400
|
+
case 'status':
|
|
401
|
+
await handleStatus(subArgs, ctx);
|
|
402
|
+
break;
|
|
403
|
+
case 'help':
|
|
404
|
+
case '?':
|
|
405
|
+
if (subArgs.length > 0) {
|
|
406
|
+
ctx.ui.notify('Usage: /router help (no arguments)', 'error');
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
ctx.ui.notify(
|
|
410
|
+
[
|
|
411
|
+
'Router Subcommands:',
|
|
412
|
+
' status Show current status, profile, pin, and last decision.',
|
|
413
|
+
' profile [name] Switch to a profile (enables router if off). Lists available if no name.',
|
|
414
|
+
' pin [profile] <tier|clear> Pin to a tier (high|medium|low) or clear the pin.',
|
|
415
|
+
' disable Disable the router and restore the last used non-router model.',
|
|
416
|
+
' debug <on|off|show|clear> Control routing debug logging to notifications and history.',
|
|
417
|
+
' reload Hot-reload the configuration JSON from .pi/model-router.json.',
|
|
418
|
+
' help, ? Show this help message.',
|
|
419
|
+
].join('\n'),
|
|
420
|
+
'info',
|
|
421
|
+
);
|
|
422
|
+
break;
|
|
423
|
+
default:
|
|
424
|
+
if (subcommand) {
|
|
425
|
+
// Check if subcommand is actually a profile name (backwards compatible-ish with /router-on)
|
|
426
|
+
if (state.currentConfig.profiles[subcommand]) {
|
|
427
|
+
if (subArgs.length > 0) {
|
|
428
|
+
ctx.ui.notify(
|
|
429
|
+
`Usage: /router ${subcommand} (no extra arguments allowed)`,
|
|
430
|
+
'error',
|
|
431
|
+
);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
await actions.switchToRouterProfile(subcommand, ctx);
|
|
435
|
+
ctx.ui.notify(
|
|
436
|
+
`Router enabled with profile: ${state.selectedProfile}`,
|
|
437
|
+
'info',
|
|
438
|
+
);
|
|
439
|
+
} else {
|
|
440
|
+
ctx.ui.notify(
|
|
441
|
+
`Unknown router subcommand: ${subcommand}. Try /router help`,
|
|
442
|
+
'error',
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
await handleStatus(subArgs, ctx);
|
|
447
|
+
}
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
};
|