@elench/testkit 0.1.90 → 0.1.92
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/README.md +20 -12
- package/lib/cli/agents/index.mjs +5 -33
- package/lib/cli/agents/providers/claude.mjs +22 -1
- package/lib/cli/agents/providers/codex.mjs +18 -1
- package/lib/cli/assistant/app.mjs +209 -0
- package/lib/cli/assistant/composer.mjs +112 -0
- package/lib/cli/assistant/context-pack.mjs +191 -0
- package/lib/cli/assistant/interactive.mjs +39 -30
- package/lib/cli/assistant/prompt-builder.mjs +4 -2
- package/lib/cli/assistant/session.mjs +13 -2
- package/lib/cli/assistant/settings.mjs +98 -0
- package/lib/cli/assistant/slash-commands.mjs +45 -1
- package/lib/cli/assistant/state.mjs +248 -54
- package/lib/cli/assistant/tool-registry.mjs +216 -226
- package/lib/cli/commands/assistant.mjs +29 -2
- package/lib/cli/entrypoint.mjs +3 -0
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +5 -5
- package/lib/cli/assistant/bootstrap.mjs +0 -248
- package/lib/cli/assistant/tool-run-reporter.mjs +0 -80
|
@@ -4,25 +4,63 @@ import { buildContextSelection } from "../context-resources.mjs";
|
|
|
4
4
|
import { parseSlashCommand, formatSlashHelpLines } from "./slash-commands.mjs";
|
|
5
5
|
import { executeAssistantTool } from "./tool-registry.mjs";
|
|
6
6
|
import { runAssistantConversationTurn } from "./session.mjs";
|
|
7
|
+
import { prepareAssistantContextPack } from "./context-pack.mjs";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_ASSISTANT_SETTINGS,
|
|
10
|
+
loadAssistantSettings,
|
|
11
|
+
mergeAssistantSettings,
|
|
12
|
+
resetAssistantSettings,
|
|
13
|
+
saveAssistantSettings,
|
|
14
|
+
} from "./settings.mjs";
|
|
15
|
+
import {
|
|
16
|
+
backspaceComposerText,
|
|
17
|
+
createComposerState,
|
|
18
|
+
deleteComposerText,
|
|
19
|
+
insertComposerText,
|
|
20
|
+
moveComposerCursor as moveComposerCursorState,
|
|
21
|
+
moveComposerCursorToEnd as moveComposerCursorStateToEnd,
|
|
22
|
+
moveComposerCursorToStart as moveComposerCursorStateToStart,
|
|
23
|
+
setComposerText,
|
|
24
|
+
} from "./composer.mjs";
|
|
7
25
|
|
|
8
26
|
export function createAssistantState({
|
|
9
27
|
productDir,
|
|
10
|
-
provider
|
|
28
|
+
provider,
|
|
29
|
+
model,
|
|
30
|
+
effort,
|
|
31
|
+
providerArgs,
|
|
32
|
+
resetSettings = false,
|
|
11
33
|
dataSource = "artifact",
|
|
12
34
|
configs = [],
|
|
13
35
|
env = process.env,
|
|
14
36
|
} = {}) {
|
|
15
37
|
const inspectState = createInspectState({ dataSource });
|
|
38
|
+
const commandLog = prepareAssistantContextPack({
|
|
39
|
+
productDir,
|
|
40
|
+
inspectState,
|
|
41
|
+
});
|
|
16
42
|
|
|
17
43
|
const listeners = new Set();
|
|
18
44
|
const messages = [];
|
|
19
|
-
let
|
|
45
|
+
let composerState = createComposerState();
|
|
20
46
|
let notice = null;
|
|
21
47
|
let busy = false;
|
|
22
|
-
let
|
|
48
|
+
let settings = mergeAssistantSettings(
|
|
49
|
+
resetSettings ? resetAssistantSettings(productDir) : loadAssistantSettings(productDir),
|
|
50
|
+
{
|
|
51
|
+
provider,
|
|
52
|
+
model,
|
|
53
|
+
effort,
|
|
54
|
+
providerArgs,
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
let resolvedProviderName = null;
|
|
23
58
|
let activeStatus = null;
|
|
24
59
|
|
|
25
|
-
inspectState.subscribe(() =>
|
|
60
|
+
inspectState.subscribe(() => {
|
|
61
|
+
commandLog.refresh();
|
|
62
|
+
notify();
|
|
63
|
+
});
|
|
26
64
|
|
|
27
65
|
function notify() {
|
|
28
66
|
for (const callback of listeners) callback();
|
|
@@ -42,8 +80,13 @@ export function createAssistantState({
|
|
|
42
80
|
notify();
|
|
43
81
|
}
|
|
44
82
|
|
|
45
|
-
|
|
83
|
+
function refreshContextPack() {
|
|
84
|
+
commandLog.refresh();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const state = {
|
|
46
88
|
inspectState,
|
|
89
|
+
commandLog,
|
|
47
90
|
|
|
48
91
|
async loadLatestArtifact() {
|
|
49
92
|
try {
|
|
@@ -51,6 +94,7 @@ export function createAssistantState({
|
|
|
51
94
|
} catch {
|
|
52
95
|
// No artifact yet.
|
|
53
96
|
}
|
|
97
|
+
refreshContextPack();
|
|
54
98
|
},
|
|
55
99
|
|
|
56
100
|
async loadCurrentArtifact() {
|
|
@@ -59,28 +103,57 @@ export function createAssistantState({
|
|
|
59
103
|
} catch {
|
|
60
104
|
// No artifact yet.
|
|
61
105
|
}
|
|
106
|
+
refreshContextPack();
|
|
62
107
|
},
|
|
63
108
|
|
|
64
109
|
revealFile(serviceName, filePath) {
|
|
65
|
-
|
|
110
|
+
const revealed = inspectState.revealFile(serviceName, filePath);
|
|
111
|
+
refreshContextPack();
|
|
112
|
+
return revealed;
|
|
66
113
|
},
|
|
67
114
|
|
|
68
115
|
revealService(serviceName) {
|
|
69
|
-
|
|
116
|
+
const revealed = inspectState.revealService(serviceName);
|
|
117
|
+
refreshContextPack();
|
|
118
|
+
return revealed;
|
|
70
119
|
},
|
|
71
120
|
|
|
72
121
|
setComposer(value) {
|
|
73
|
-
|
|
122
|
+
composerState = setComposerText(composerState, value);
|
|
74
123
|
notify();
|
|
75
124
|
},
|
|
76
125
|
|
|
77
|
-
|
|
78
|
-
|
|
126
|
+
insertComposer(text) {
|
|
127
|
+
composerState = insertComposerText(composerState, text);
|
|
79
128
|
notify();
|
|
80
129
|
},
|
|
81
130
|
|
|
131
|
+
appendComposer(text) {
|
|
132
|
+
state.insertComposer(text);
|
|
133
|
+
},
|
|
134
|
+
|
|
82
135
|
backspaceComposer() {
|
|
83
|
-
|
|
136
|
+
composerState = backspaceComposerText(composerState);
|
|
137
|
+
notify();
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
deleteComposer() {
|
|
141
|
+
composerState = deleteComposerText(composerState);
|
|
142
|
+
notify();
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
moveComposerCursor(delta) {
|
|
146
|
+
composerState = moveComposerCursorState(composerState, delta);
|
|
147
|
+
notify();
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
moveComposerCursorToStart() {
|
|
151
|
+
composerState = moveComposerCursorStateToStart(composerState);
|
|
152
|
+
notify();
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
moveComposerCursorToEnd() {
|
|
156
|
+
composerState = moveComposerCursorStateToEnd(composerState);
|
|
84
157
|
notify();
|
|
85
158
|
},
|
|
86
159
|
|
|
@@ -95,7 +168,41 @@ export function createAssistantState({
|
|
|
95
168
|
},
|
|
96
169
|
|
|
97
170
|
setProvider(nextProvider) {
|
|
98
|
-
|
|
171
|
+
settings = mergeAssistantSettings(settings, { provider: nextProvider || DEFAULT_ASSISTANT_SETTINGS.provider });
|
|
172
|
+
resolvedProviderName = null;
|
|
173
|
+
saveAssistantSettings(productDir, settings);
|
|
174
|
+
notify();
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
setModel(nextModel) {
|
|
178
|
+
settings = mergeAssistantSettings(settings, { model: nextModel || null });
|
|
179
|
+
saveAssistantSettings(productDir, settings);
|
|
180
|
+
notify();
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
setEffort(nextEffort) {
|
|
184
|
+
settings = mergeAssistantSettings(settings, { effort: nextEffort || null });
|
|
185
|
+
saveAssistantSettings(productDir, settings);
|
|
186
|
+
notify();
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
addProviderArg(value) {
|
|
190
|
+
const arg = String(value || "").trim();
|
|
191
|
+
if (!arg) return;
|
|
192
|
+
settings = mergeAssistantSettings(settings, { providerArgs: [...settings.providerArgs, arg] });
|
|
193
|
+
saveAssistantSettings(productDir, settings);
|
|
194
|
+
notify();
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
clearProviderArgs() {
|
|
198
|
+
settings = mergeAssistantSettings(settings, { providerArgs: [] });
|
|
199
|
+
saveAssistantSettings(productDir, settings);
|
|
200
|
+
notify();
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
resetSettings() {
|
|
204
|
+
settings = resetAssistantSettings(productDir);
|
|
205
|
+
resolvedProviderName = null;
|
|
99
206
|
notify();
|
|
100
207
|
},
|
|
101
208
|
|
|
@@ -105,11 +212,11 @@ export function createAssistantState({
|
|
|
105
212
|
},
|
|
106
213
|
|
|
107
214
|
async submitCurrentComposer() {
|
|
108
|
-
const value =
|
|
109
|
-
|
|
215
|
+
const value = composerState.text.trim();
|
|
216
|
+
composerState = createComposerState();
|
|
110
217
|
notify();
|
|
111
218
|
if (!value) return;
|
|
112
|
-
await
|
|
219
|
+
await state.submitInput(value);
|
|
113
220
|
},
|
|
114
221
|
|
|
115
222
|
async submitInput(input) {
|
|
@@ -119,29 +226,19 @@ export function createAssistantState({
|
|
|
119
226
|
|
|
120
227
|
const slash = parseSlashCommandSafe(trimmed);
|
|
121
228
|
if (slash?.type === "__error__") {
|
|
122
|
-
appendMessage({
|
|
123
|
-
role: "system",
|
|
124
|
-
text: slash.error,
|
|
125
|
-
});
|
|
229
|
+
appendMessage({ role: "system", text: slash.error });
|
|
126
230
|
return;
|
|
127
231
|
}
|
|
128
232
|
if (slash) {
|
|
129
233
|
try {
|
|
130
234
|
await executeSlashCommand({
|
|
131
235
|
slash,
|
|
236
|
+
state,
|
|
132
237
|
productDir,
|
|
133
|
-
|
|
238
|
+
settings,
|
|
134
239
|
configs,
|
|
135
|
-
|
|
136
|
-
providerName = value;
|
|
137
|
-
},
|
|
240
|
+
env,
|
|
138
241
|
appendMessage,
|
|
139
|
-
setNotice: (value) => {
|
|
140
|
-
notice = value;
|
|
141
|
-
},
|
|
142
|
-
clearMessages: () => {
|
|
143
|
-
messages.length = 0;
|
|
144
|
-
},
|
|
145
242
|
});
|
|
146
243
|
} catch (error) {
|
|
147
244
|
appendMessage({
|
|
@@ -149,24 +246,30 @@ export function createAssistantState({
|
|
|
149
246
|
text: error instanceof Error ? error.message : String(error),
|
|
150
247
|
});
|
|
151
248
|
}
|
|
249
|
+
refreshContextPack();
|
|
152
250
|
notify();
|
|
153
251
|
return;
|
|
154
252
|
}
|
|
155
253
|
|
|
156
254
|
try {
|
|
157
|
-
setBusy(true, "
|
|
255
|
+
setBusy(true, `Thinking with ${settings.provider === "auto" ? "provider" : settings.provider}...`);
|
|
158
256
|
const emitted = await runAssistantConversationTurn({
|
|
159
257
|
productDir,
|
|
160
258
|
inspectState,
|
|
161
259
|
transcript: messages.map((entry) => ({ role: entry.role, text: entry.text })),
|
|
162
260
|
userMessage: trimmed,
|
|
163
|
-
|
|
261
|
+
settings,
|
|
164
262
|
env,
|
|
165
263
|
configs,
|
|
264
|
+
commandLog,
|
|
166
265
|
onStatus(status) {
|
|
167
266
|
activeStatus = status;
|
|
168
267
|
notify();
|
|
169
268
|
},
|
|
269
|
+
onResolvedProvider(provider) {
|
|
270
|
+
resolvedProviderName = provider;
|
|
271
|
+
notify();
|
|
272
|
+
},
|
|
170
273
|
});
|
|
171
274
|
for (const message of emitted) appendMessage(message);
|
|
172
275
|
} catch (error) {
|
|
@@ -175,6 +278,7 @@ export function createAssistantState({
|
|
|
175
278
|
text: error instanceof Error ? error.message : String(error),
|
|
176
279
|
});
|
|
177
280
|
} finally {
|
|
281
|
+
refreshContextPack();
|
|
178
282
|
setBusy(false, null);
|
|
179
283
|
}
|
|
180
284
|
},
|
|
@@ -188,48 +292,104 @@ export function createAssistantState({
|
|
|
188
292
|
return {
|
|
189
293
|
context: buildContextSelection(inspectState.getSnapshot()),
|
|
190
294
|
messages: [...messages],
|
|
191
|
-
composer,
|
|
295
|
+
composer: composerState.text,
|
|
296
|
+
composerCursor: composerState.cursor,
|
|
192
297
|
notice,
|
|
193
298
|
busy,
|
|
194
|
-
provider:
|
|
299
|
+
provider: settings.provider,
|
|
300
|
+
resolvedProvider: resolvedProviderName,
|
|
301
|
+
model: settings.model,
|
|
302
|
+
effort: settings.effort,
|
|
303
|
+
providerArgs: [...settings.providerArgs],
|
|
195
304
|
activeStatus,
|
|
305
|
+
contextPaths: {
|
|
306
|
+
contextPath: commandLog.contextPath,
|
|
307
|
+
summaryPath: commandLog.summaryPath,
|
|
308
|
+
selectionPath: commandLog.selectionPath,
|
|
309
|
+
commandsPath: commandLog.commandsPath,
|
|
310
|
+
commandLogPath: commandLog.commandLogPath,
|
|
311
|
+
},
|
|
196
312
|
};
|
|
197
313
|
},
|
|
198
314
|
};
|
|
315
|
+
|
|
316
|
+
refreshContextPack();
|
|
317
|
+
return state;
|
|
199
318
|
}
|
|
200
319
|
|
|
201
320
|
async function executeSlashCommand({
|
|
202
321
|
slash,
|
|
322
|
+
state,
|
|
203
323
|
productDir,
|
|
204
|
-
|
|
324
|
+
settings,
|
|
205
325
|
configs,
|
|
206
|
-
|
|
326
|
+
env,
|
|
207
327
|
appendMessage,
|
|
208
|
-
setNotice,
|
|
209
|
-
clearMessages,
|
|
210
328
|
} = {}) {
|
|
211
329
|
if (slash.type === "help") {
|
|
212
330
|
appendMessage({ role: "assistant", text: formatSlashHelpLines().join("\n") });
|
|
213
331
|
return;
|
|
214
332
|
}
|
|
215
333
|
if (slash.type === "clear") {
|
|
216
|
-
clearMessages
|
|
334
|
+
state.clearMessages();
|
|
217
335
|
return;
|
|
218
336
|
}
|
|
219
337
|
if (slash.type === "quit") {
|
|
220
|
-
setNotice
|
|
338
|
+
state.setNotice("Use q or Ctrl+C to quit the interactive assistant.");
|
|
221
339
|
return;
|
|
222
340
|
}
|
|
223
341
|
if (slash.type === "provider") {
|
|
224
|
-
setProvider(slash.provider);
|
|
342
|
+
state.setProvider(slash.provider);
|
|
225
343
|
appendMessage({ role: "assistant", text: `Provider set to ${slash.provider}.` });
|
|
226
344
|
return;
|
|
227
345
|
}
|
|
346
|
+
if (slash.type === "model") {
|
|
347
|
+
state.setModel(slash.model);
|
|
348
|
+
appendMessage({ role: "assistant", text: slash.model ? `Model set to ${slash.model}.` : "Model reset to provider default." });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (slash.type === "effort") {
|
|
352
|
+
state.setEffort(slash.effort);
|
|
353
|
+
appendMessage({ role: "assistant", text: slash.effort ? `Effort set to ${slash.effort}.` : "Effort reset to provider default." });
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (slash.type === "provider-args-add") {
|
|
357
|
+
state.addProviderArg(slash.value);
|
|
358
|
+
appendMessage({ role: "assistant", text: `Provider arg added: ${slash.value}` });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (slash.type === "provider-args-clear") {
|
|
362
|
+
state.clearProviderArgs();
|
|
363
|
+
appendMessage({ role: "assistant", text: "Provider args cleared." });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (slash.type === "provider-args-list") {
|
|
367
|
+
const args = state.getSnapshot().providerArgs;
|
|
368
|
+
appendMessage({ role: "assistant", text: args.length ? args.map((arg) => `- ${arg}`).join("\n") : "No provider args configured." });
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (slash.type === "settings-show") {
|
|
372
|
+
appendMessage({ role: "assistant", text: formatSettings(state.getSnapshot()) });
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (slash.type === "settings-reset") {
|
|
376
|
+
state.resetSettings();
|
|
377
|
+
appendMessage({ role: "assistant", text: "Assistant settings reset." });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
228
380
|
|
|
229
381
|
const result = await executeSlashTool(slash, {
|
|
230
382
|
productDir,
|
|
231
|
-
inspectState,
|
|
383
|
+
inspectState: state.inspectState,
|
|
232
384
|
configs,
|
|
385
|
+
env,
|
|
386
|
+
commandLog: state.commandLog,
|
|
387
|
+
onEvent(event) {
|
|
388
|
+
if (event.type === "tool-status") {
|
|
389
|
+
state.setNotice(event.message);
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
provider: settings.provider,
|
|
233
393
|
});
|
|
234
394
|
appendMessage({
|
|
235
395
|
role: "tool",
|
|
@@ -240,35 +400,69 @@ async function executeSlashCommand({
|
|
|
240
400
|
});
|
|
241
401
|
}
|
|
242
402
|
|
|
403
|
+
function formatSettings(snapshot) {
|
|
404
|
+
const rows = [
|
|
405
|
+
["Provider", snapshot.provider || "auto"],
|
|
406
|
+
["Resolved", snapshot.resolvedProvider || "not resolved yet"],
|
|
407
|
+
["Model", snapshot.model || "provider default"],
|
|
408
|
+
["Effort", snapshot.effort || "provider default"],
|
|
409
|
+
["Provider args", snapshot.providerArgs?.length ? snapshot.providerArgs.join(" ") : "none"],
|
|
410
|
+
];
|
|
411
|
+
return rows.map(([label, value]) => `${label}: ${value}`).join("\n");
|
|
412
|
+
}
|
|
413
|
+
|
|
243
414
|
async function executeSlashTool(slash, context) {
|
|
244
415
|
switch (slash.type) {
|
|
245
|
-
case "file":
|
|
246
|
-
return executeAssistantTool("focus_file", { file: slash.file }, context);
|
|
247
416
|
case "inspect":
|
|
248
|
-
return
|
|
249
|
-
|
|
250
|
-
:
|
|
417
|
+
return executeAssistantTool(
|
|
418
|
+
"read_context",
|
|
419
|
+
{ file: slash.file || null, mode: "detail" },
|
|
420
|
+
context
|
|
421
|
+
);
|
|
251
422
|
case "logs":
|
|
252
|
-
return executeAssistantTool("
|
|
423
|
+
return executeAssistantTool("read_context", { service: slash.service || null, mode: "logs" }, context);
|
|
253
424
|
case "artifacts":
|
|
254
|
-
return executeAssistantTool("
|
|
425
|
+
return executeAssistantTool("read_context", { file: slash.file || null, mode: "artifacts" }, context);
|
|
255
426
|
case "setup":
|
|
256
|
-
return executeAssistantTool("
|
|
427
|
+
return executeAssistantTool("read_context", { service: slash.service || null, mode: "setup" }, context);
|
|
428
|
+
case "file":
|
|
429
|
+
return executeAssistantTool("read_file", { path: slash.file }, context);
|
|
257
430
|
case "service":
|
|
258
|
-
return executeAssistantTool("
|
|
431
|
+
return executeAssistantTool("read_context", { service: slash.service, mode: "detail" }, context);
|
|
259
432
|
case "status":
|
|
260
|
-
return executeAssistantTool("
|
|
433
|
+
return executeAssistantTool("shell_exec", { command: "testkit status --dir ." }, context);
|
|
261
434
|
case "discover":
|
|
262
|
-
return executeAssistantTool("
|
|
435
|
+
return executeAssistantTool("shell_exec", { command: "testkit discover --dir ." }, context);
|
|
263
436
|
case "doctor":
|
|
264
|
-
return executeAssistantTool("
|
|
437
|
+
return executeAssistantTool("shell_exec", { command: "testkit doctor --dir ." }, context);
|
|
265
438
|
case "run":
|
|
266
|
-
return executeAssistantTool("
|
|
439
|
+
return executeAssistantTool("shell_exec", { command: buildRunSlashCommand(slash.options) }, context);
|
|
267
440
|
default:
|
|
268
441
|
throw new Error(`Unsupported slash command "${slash.type}"`);
|
|
269
442
|
}
|
|
270
443
|
}
|
|
271
444
|
|
|
445
|
+
function buildRunSlashCommand(options = {}) {
|
|
446
|
+
const parts = ["testkit", "run", "--dir", "."];
|
|
447
|
+
for (const type of options.type || []) {
|
|
448
|
+
parts.push("--type", type);
|
|
449
|
+
}
|
|
450
|
+
for (const suite of options.suite || []) {
|
|
451
|
+
parts.push("--suite", suite);
|
|
452
|
+
}
|
|
453
|
+
for (const file of options.file || []) {
|
|
454
|
+
parts.push("--file", file);
|
|
455
|
+
}
|
|
456
|
+
if (options.service) parts.push("--service", options.service);
|
|
457
|
+
return parts.map(shellEscapeArg).join(" ");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function shellEscapeArg(value) {
|
|
461
|
+
const stringValue = String(value);
|
|
462
|
+
if (/^[a-zA-Z0-9._:/-]+$/.test(stringValue)) return stringValue;
|
|
463
|
+
return `'${stringValue.replace(/'/g, `'\\''`)}'`;
|
|
464
|
+
}
|
|
465
|
+
|
|
272
466
|
function parseSlashCommandSafe(input) {
|
|
273
467
|
try {
|
|
274
468
|
return parseSlashCommand(input);
|