@elench/testkit 0.1.90 → 0.1.91
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 +10 -12
- package/lib/cli/agents/index.mjs +0 -31
- package/lib/cli/assistant/app.mjs +210 -0
- package/lib/cli/assistant/context-pack.mjs +191 -0
- package/lib/cli/assistant/interactive.mjs +30 -29
- package/lib/cli/assistant/prompt-builder.mjs +4 -2
- package/lib/cli/assistant/session.mjs +3 -0
- package/lib/cli/assistant/state.mjs +130 -44
- package/lib/cli/assistant/tool-registry.mjs +216 -226
- package/lib/cli/commands/assistant.mjs +7 -1
- 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,6 +4,7 @@ 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";
|
|
7
8
|
|
|
8
9
|
export function createAssistantState({
|
|
9
10
|
productDir,
|
|
@@ -13,16 +14,24 @@ export function createAssistantState({
|
|
|
13
14
|
env = process.env,
|
|
14
15
|
} = {}) {
|
|
15
16
|
const inspectState = createInspectState({ dataSource });
|
|
17
|
+
const commandLog = prepareAssistantContextPack({
|
|
18
|
+
productDir,
|
|
19
|
+
inspectState,
|
|
20
|
+
});
|
|
16
21
|
|
|
17
22
|
const listeners = new Set();
|
|
18
23
|
const messages = [];
|
|
19
24
|
let composer = "";
|
|
25
|
+
let composerCursor = 0;
|
|
20
26
|
let notice = null;
|
|
21
27
|
let busy = false;
|
|
22
28
|
let providerName = provider;
|
|
23
29
|
let activeStatus = null;
|
|
24
30
|
|
|
25
|
-
inspectState.subscribe(() =>
|
|
31
|
+
inspectState.subscribe(() => {
|
|
32
|
+
commandLog.refresh();
|
|
33
|
+
notify();
|
|
34
|
+
});
|
|
26
35
|
|
|
27
36
|
function notify() {
|
|
28
37
|
for (const callback of listeners) callback();
|
|
@@ -42,8 +51,13 @@ export function createAssistantState({
|
|
|
42
51
|
notify();
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
|
|
54
|
+
function refreshContextPack() {
|
|
55
|
+
commandLog.refresh();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const state = {
|
|
46
59
|
inspectState,
|
|
60
|
+
commandLog,
|
|
47
61
|
|
|
48
62
|
async loadLatestArtifact() {
|
|
49
63
|
try {
|
|
@@ -51,6 +65,7 @@ export function createAssistantState({
|
|
|
51
65
|
} catch {
|
|
52
66
|
// No artifact yet.
|
|
53
67
|
}
|
|
68
|
+
refreshContextPack();
|
|
54
69
|
},
|
|
55
70
|
|
|
56
71
|
async loadCurrentArtifact() {
|
|
@@ -59,28 +74,64 @@ export function createAssistantState({
|
|
|
59
74
|
} catch {
|
|
60
75
|
// No artifact yet.
|
|
61
76
|
}
|
|
77
|
+
refreshContextPack();
|
|
62
78
|
},
|
|
63
79
|
|
|
64
80
|
revealFile(serviceName, filePath) {
|
|
65
|
-
|
|
81
|
+
const revealed = inspectState.revealFile(serviceName, filePath);
|
|
82
|
+
refreshContextPack();
|
|
83
|
+
return revealed;
|
|
66
84
|
},
|
|
67
85
|
|
|
68
86
|
revealService(serviceName) {
|
|
69
|
-
|
|
87
|
+
const revealed = inspectState.revealService(serviceName);
|
|
88
|
+
refreshContextPack();
|
|
89
|
+
return revealed;
|
|
70
90
|
},
|
|
71
91
|
|
|
72
92
|
setComposer(value) {
|
|
73
93
|
composer = String(value || "");
|
|
94
|
+
composerCursor = Math.max(0, Math.min(composer.length, composerCursor));
|
|
74
95
|
notify();
|
|
75
96
|
},
|
|
76
97
|
|
|
77
|
-
|
|
78
|
-
|
|
98
|
+
insertComposer(text) {
|
|
99
|
+
const nextText = String(text || "");
|
|
100
|
+
if (!nextText) return;
|
|
101
|
+
composer = `${composer.slice(0, composerCursor)}${nextText}${composer.slice(composerCursor)}`;
|
|
102
|
+
composerCursor += nextText.length;
|
|
79
103
|
notify();
|
|
80
104
|
},
|
|
81
105
|
|
|
106
|
+
appendComposer(text) {
|
|
107
|
+
state.insertComposer(text);
|
|
108
|
+
},
|
|
109
|
+
|
|
82
110
|
backspaceComposer() {
|
|
83
|
-
|
|
111
|
+
if (composerCursor === 0) return;
|
|
112
|
+
composer = `${composer.slice(0, composerCursor - 1)}${composer.slice(composerCursor)}`;
|
|
113
|
+
composerCursor -= 1;
|
|
114
|
+
notify();
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
deleteComposer() {
|
|
118
|
+
if (composerCursor >= composer.length) return;
|
|
119
|
+
composer = `${composer.slice(0, composerCursor)}${composer.slice(composerCursor + 1)}`;
|
|
120
|
+
notify();
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
moveComposerCursor(delta) {
|
|
124
|
+
composerCursor = Math.max(0, Math.min(composer.length, composerCursor + delta));
|
|
125
|
+
notify();
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
moveComposerCursorToStart() {
|
|
129
|
+
composerCursor = 0;
|
|
130
|
+
notify();
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
moveComposerCursorToEnd() {
|
|
134
|
+
composerCursor = composer.length;
|
|
84
135
|
notify();
|
|
85
136
|
},
|
|
86
137
|
|
|
@@ -107,9 +158,10 @@ export function createAssistantState({
|
|
|
107
158
|
async submitCurrentComposer() {
|
|
108
159
|
const value = composer.trim();
|
|
109
160
|
composer = "";
|
|
161
|
+
composerCursor = 0;
|
|
110
162
|
notify();
|
|
111
163
|
if (!value) return;
|
|
112
|
-
await
|
|
164
|
+
await state.submitInput(value);
|
|
113
165
|
},
|
|
114
166
|
|
|
115
167
|
async submitInput(input) {
|
|
@@ -119,29 +171,19 @@ export function createAssistantState({
|
|
|
119
171
|
|
|
120
172
|
const slash = parseSlashCommandSafe(trimmed);
|
|
121
173
|
if (slash?.type === "__error__") {
|
|
122
|
-
appendMessage({
|
|
123
|
-
role: "system",
|
|
124
|
-
text: slash.error,
|
|
125
|
-
});
|
|
174
|
+
appendMessage({ role: "system", text: slash.error });
|
|
126
175
|
return;
|
|
127
176
|
}
|
|
128
177
|
if (slash) {
|
|
129
178
|
try {
|
|
130
179
|
await executeSlashCommand({
|
|
131
180
|
slash,
|
|
181
|
+
state,
|
|
132
182
|
productDir,
|
|
133
|
-
|
|
183
|
+
providerName,
|
|
134
184
|
configs,
|
|
135
|
-
|
|
136
|
-
providerName = value;
|
|
137
|
-
},
|
|
185
|
+
env,
|
|
138
186
|
appendMessage,
|
|
139
|
-
setNotice: (value) => {
|
|
140
|
-
notice = value;
|
|
141
|
-
},
|
|
142
|
-
clearMessages: () => {
|
|
143
|
-
messages.length = 0;
|
|
144
|
-
},
|
|
145
187
|
});
|
|
146
188
|
} catch (error) {
|
|
147
189
|
appendMessage({
|
|
@@ -149,12 +191,13 @@ export function createAssistantState({
|
|
|
149
191
|
text: error instanceof Error ? error.message : String(error),
|
|
150
192
|
});
|
|
151
193
|
}
|
|
194
|
+
refreshContextPack();
|
|
152
195
|
notify();
|
|
153
196
|
return;
|
|
154
197
|
}
|
|
155
198
|
|
|
156
199
|
try {
|
|
157
|
-
setBusy(true, "
|
|
200
|
+
setBusy(true, `Thinking with ${providerName === "auto" ? "provider" : providerName}...`);
|
|
158
201
|
const emitted = await runAssistantConversationTurn({
|
|
159
202
|
productDir,
|
|
160
203
|
inspectState,
|
|
@@ -163,6 +206,7 @@ export function createAssistantState({
|
|
|
163
206
|
provider: providerName,
|
|
164
207
|
env,
|
|
165
208
|
configs,
|
|
209
|
+
commandLog,
|
|
166
210
|
onStatus(status) {
|
|
167
211
|
activeStatus = status;
|
|
168
212
|
notify();
|
|
@@ -175,6 +219,7 @@ export function createAssistantState({
|
|
|
175
219
|
text: error instanceof Error ? error.message : String(error),
|
|
176
220
|
});
|
|
177
221
|
} finally {
|
|
222
|
+
refreshContextPack();
|
|
178
223
|
setBusy(false, null);
|
|
179
224
|
}
|
|
180
225
|
},
|
|
@@ -189,47 +234,65 @@ export function createAssistantState({
|
|
|
189
234
|
context: buildContextSelection(inspectState.getSnapshot()),
|
|
190
235
|
messages: [...messages],
|
|
191
236
|
composer,
|
|
237
|
+
composerCursor,
|
|
192
238
|
notice,
|
|
193
239
|
busy,
|
|
194
240
|
provider: providerName,
|
|
195
241
|
activeStatus,
|
|
242
|
+
contextPaths: {
|
|
243
|
+
contextPath: commandLog.contextPath,
|
|
244
|
+
summaryPath: commandLog.summaryPath,
|
|
245
|
+
selectionPath: commandLog.selectionPath,
|
|
246
|
+
commandsPath: commandLog.commandsPath,
|
|
247
|
+
commandLogPath: commandLog.commandLogPath,
|
|
248
|
+
},
|
|
196
249
|
};
|
|
197
250
|
},
|
|
198
251
|
};
|
|
252
|
+
|
|
253
|
+
refreshContextPack();
|
|
254
|
+
return state;
|
|
199
255
|
}
|
|
200
256
|
|
|
201
257
|
async function executeSlashCommand({
|
|
202
258
|
slash,
|
|
259
|
+
state,
|
|
203
260
|
productDir,
|
|
204
|
-
|
|
261
|
+
providerName,
|
|
205
262
|
configs,
|
|
206
|
-
|
|
263
|
+
env,
|
|
207
264
|
appendMessage,
|
|
208
|
-
setNotice,
|
|
209
|
-
clearMessages,
|
|
210
265
|
} = {}) {
|
|
211
266
|
if (slash.type === "help") {
|
|
212
267
|
appendMessage({ role: "assistant", text: formatSlashHelpLines().join("\n") });
|
|
213
268
|
return;
|
|
214
269
|
}
|
|
215
270
|
if (slash.type === "clear") {
|
|
216
|
-
clearMessages
|
|
271
|
+
state.clearMessages();
|
|
217
272
|
return;
|
|
218
273
|
}
|
|
219
274
|
if (slash.type === "quit") {
|
|
220
|
-
setNotice
|
|
275
|
+
state.setNotice("Use q or Ctrl+C to quit the interactive assistant.");
|
|
221
276
|
return;
|
|
222
277
|
}
|
|
223
278
|
if (slash.type === "provider") {
|
|
224
|
-
setProvider(slash.provider);
|
|
279
|
+
state.setProvider(slash.provider);
|
|
225
280
|
appendMessage({ role: "assistant", text: `Provider set to ${slash.provider}.` });
|
|
226
281
|
return;
|
|
227
282
|
}
|
|
228
283
|
|
|
229
284
|
const result = await executeSlashTool(slash, {
|
|
230
285
|
productDir,
|
|
231
|
-
inspectState,
|
|
286
|
+
inspectState: state.inspectState,
|
|
232
287
|
configs,
|
|
288
|
+
env,
|
|
289
|
+
commandLog: state.commandLog,
|
|
290
|
+
onEvent(event) {
|
|
291
|
+
if (event.type === "tool-status") {
|
|
292
|
+
state.setNotice(event.message);
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
provider: providerName,
|
|
233
296
|
});
|
|
234
297
|
appendMessage({
|
|
235
298
|
role: "tool",
|
|
@@ -242,33 +305,56 @@ async function executeSlashCommand({
|
|
|
242
305
|
|
|
243
306
|
async function executeSlashTool(slash, context) {
|
|
244
307
|
switch (slash.type) {
|
|
245
|
-
case "file":
|
|
246
|
-
return executeAssistantTool("focus_file", { file: slash.file }, context);
|
|
247
308
|
case "inspect":
|
|
248
|
-
return
|
|
249
|
-
|
|
250
|
-
:
|
|
309
|
+
return executeAssistantTool(
|
|
310
|
+
"read_context",
|
|
311
|
+
{ file: slash.file || null, mode: "detail" },
|
|
312
|
+
context
|
|
313
|
+
);
|
|
251
314
|
case "logs":
|
|
252
|
-
return executeAssistantTool("
|
|
315
|
+
return executeAssistantTool("read_context", { service: slash.service || null, mode: "logs" }, context);
|
|
253
316
|
case "artifacts":
|
|
254
|
-
return executeAssistantTool("
|
|
317
|
+
return executeAssistantTool("read_context", { file: slash.file || null, mode: "artifacts" }, context);
|
|
255
318
|
case "setup":
|
|
256
|
-
return executeAssistantTool("
|
|
319
|
+
return executeAssistantTool("read_context", { service: slash.service || null, mode: "setup" }, context);
|
|
320
|
+
case "file":
|
|
321
|
+
return executeAssistantTool("read_file", { path: slash.file }, context);
|
|
257
322
|
case "service":
|
|
258
|
-
return executeAssistantTool("
|
|
323
|
+
return executeAssistantTool("read_context", { service: slash.service, mode: "detail" }, context);
|
|
259
324
|
case "status":
|
|
260
|
-
return executeAssistantTool("
|
|
325
|
+
return executeAssistantTool("shell_exec", { command: "testkit status --dir ." }, context);
|
|
261
326
|
case "discover":
|
|
262
|
-
return executeAssistantTool("
|
|
327
|
+
return executeAssistantTool("shell_exec", { command: "testkit discover --dir ." }, context);
|
|
263
328
|
case "doctor":
|
|
264
|
-
return executeAssistantTool("
|
|
329
|
+
return executeAssistantTool("shell_exec", { command: "testkit doctor --dir ." }, context);
|
|
265
330
|
case "run":
|
|
266
|
-
return executeAssistantTool("
|
|
331
|
+
return executeAssistantTool("shell_exec", { command: buildRunSlashCommand(slash.options) }, context);
|
|
267
332
|
default:
|
|
268
333
|
throw new Error(`Unsupported slash command "${slash.type}"`);
|
|
269
334
|
}
|
|
270
335
|
}
|
|
271
336
|
|
|
337
|
+
function buildRunSlashCommand(options = {}) {
|
|
338
|
+
const parts = ["testkit", "run", "--dir", "."];
|
|
339
|
+
for (const type of options.type || []) {
|
|
340
|
+
parts.push("--type", type);
|
|
341
|
+
}
|
|
342
|
+
for (const suite of options.suite || []) {
|
|
343
|
+
parts.push("--suite", suite);
|
|
344
|
+
}
|
|
345
|
+
for (const file of options.file || []) {
|
|
346
|
+
parts.push("--file", file);
|
|
347
|
+
}
|
|
348
|
+
if (options.service) parts.push("--service", options.service);
|
|
349
|
+
return parts.map(shellEscapeArg).join(" ");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function shellEscapeArg(value) {
|
|
353
|
+
const stringValue = String(value);
|
|
354
|
+
if (/^[a-zA-Z0-9._:/-]+$/.test(stringValue)) return stringValue;
|
|
355
|
+
return `'${stringValue.replace(/'/g, `'\\''`)}'`;
|
|
356
|
+
}
|
|
357
|
+
|
|
272
358
|
function parseSlashCommandSafe(input) {
|
|
273
359
|
try {
|
|
274
360
|
return parseSlashCommand(input);
|