@elench/testkit 0.1.91 → 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 +10 -0
- package/lib/cli/agents/index.mjs +5 -2
- package/lib/cli/agents/providers/claude.mjs +22 -1
- package/lib/cli/agents/providers/codex.mjs +18 -1
- package/lib/cli/assistant/app.mjs +13 -14
- package/lib/cli/assistant/composer.mjs +112 -0
- package/lib/cli/assistant/interactive.mjs +9 -1
- package/lib/cli/assistant/session.mjs +10 -2
- package/lib/cli/assistant/settings.mjs +98 -0
- package/lib/cli/assistant/slash-commands.mjs +45 -1
- package/lib/cli/assistant/state.mjs +138 -30
- package/lib/cli/commands/assistant.mjs +22 -1
- 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/README.md
CHANGED
|
@@ -13,6 +13,8 @@ cd my-product
|
|
|
13
13
|
|
|
14
14
|
# Launch the interactive assistant
|
|
15
15
|
npx @elench/testkit
|
|
16
|
+
npx @elench/testkit assistant --provider codex --model gpt-5.4
|
|
17
|
+
npx @elench/testkit assistant --provider claude --model sonnet --effort high
|
|
16
18
|
|
|
17
19
|
# Ask for one assistant turn non-interactively
|
|
18
20
|
npx @elench/testkit assistant --message "/status"
|
|
@@ -82,6 +84,14 @@ blocks for command execution. Natural-language turns still go through Codex or
|
|
|
82
84
|
Claude, but `testkit` owns the transcript, command execution surface, and
|
|
83
85
|
rendering around `testkit`, `npm`, and `npx` commands.
|
|
84
86
|
|
|
87
|
+
Assistant runtime settings are repo-local. Use `/provider`, `/model`,
|
|
88
|
+
`/effort`, and `/settings` inside the assistant to inspect or change the active
|
|
89
|
+
provider runtime; changes are persisted to `.testkit/assistant/settings.json`.
|
|
90
|
+
CLI flags such as `--provider`, `--model`, `--effort`, and repeatable
|
|
91
|
+
`--provider-arg` override those settings for the current launch. The composer
|
|
92
|
+
has an always-visible cursor and supports arrow keys, Home/End, Ctrl+A/Ctrl+E,
|
|
93
|
+
Backspace, Delete, and Ctrl+D.
|
|
94
|
+
|
|
85
95
|
The non-interactive `assistant --message ...` mode uses the same provider/tool
|
|
86
96
|
engine for one hosted turn at a time. It is useful in scripts and tests, but
|
|
87
97
|
it is not the primary interactive UX.
|
package/lib/cli/agents/index.mjs
CHANGED
|
@@ -57,6 +57,9 @@ export function isProviderInstalled(provider, env = process.env) {
|
|
|
57
57
|
|
|
58
58
|
export function startAgentSession({
|
|
59
59
|
provider = "auto",
|
|
60
|
+
model = null,
|
|
61
|
+
effort = null,
|
|
62
|
+
providerArgs = [],
|
|
60
63
|
cwd,
|
|
61
64
|
prompt,
|
|
62
65
|
onEvent,
|
|
@@ -66,7 +69,7 @@ export function startAgentSession({
|
|
|
66
69
|
const resolvedProvider = resolvePreferredProvider(provider, env);
|
|
67
70
|
const command = resolveProviderBinary(resolvedProvider, env);
|
|
68
71
|
if (resolvedProvider === "claude") {
|
|
69
|
-
return startClaudeHostedSession({ command, cwd, prompt, onEvent, purpose });
|
|
72
|
+
return startClaudeHostedSession({ command, cwd, prompt, onEvent, purpose, model, effort, providerArgs });
|
|
70
73
|
}
|
|
71
|
-
return startCodexHostedSession({ command, cwd, prompt, onEvent, purpose });
|
|
74
|
+
return startCodexHostedSession({ command, cwd, prompt, onEvent, purpose, model, providerArgs });
|
|
72
75
|
}
|
|
@@ -7,7 +7,16 @@ import {
|
|
|
7
7
|
extractTextFragments,
|
|
8
8
|
} from "./shared.mjs";
|
|
9
9
|
|
|
10
|
-
export function startClaudeHostedSession({
|
|
10
|
+
export function startClaudeHostedSession({
|
|
11
|
+
command = "claude",
|
|
12
|
+
cwd,
|
|
13
|
+
prompt,
|
|
14
|
+
onEvent,
|
|
15
|
+
purpose = "assistant",
|
|
16
|
+
model = null,
|
|
17
|
+
effort = null,
|
|
18
|
+
providerArgs = [],
|
|
19
|
+
} = {}) {
|
|
11
20
|
const args = [
|
|
12
21
|
"-p",
|
|
13
22
|
"--output-format",
|
|
@@ -18,6 +27,13 @@ export function startClaudeHostedSession({ command = "claude", cwd, prompt, onEv
|
|
|
18
27
|
if (purpose === "assistant") {
|
|
19
28
|
args.push("--permission-mode", "plan");
|
|
20
29
|
}
|
|
30
|
+
if (model) {
|
|
31
|
+
args.push("--model", String(model));
|
|
32
|
+
}
|
|
33
|
+
if (effort) {
|
|
34
|
+
args.push("--effort", String(effort));
|
|
35
|
+
}
|
|
36
|
+
args.push(...normalizeProviderArgs(providerArgs));
|
|
21
37
|
|
|
22
38
|
args.push(prompt);
|
|
23
39
|
|
|
@@ -39,6 +55,11 @@ export function startClaudeHostedSession({ command = "claude", cwd, prompt, onEv
|
|
|
39
55
|
});
|
|
40
56
|
}
|
|
41
57
|
|
|
58
|
+
function normalizeProviderArgs(providerArgs) {
|
|
59
|
+
if (!Array.isArray(providerArgs)) return [];
|
|
60
|
+
return providerArgs.flatMap((arg) => String(arg || "").split(/\s+/).filter(Boolean));
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
function parseClaudePayload(payload) {
|
|
43
64
|
const events = [];
|
|
44
65
|
if (!payload || typeof payload !== "object") return events;
|
|
@@ -11,7 +11,15 @@ import {
|
|
|
11
11
|
readTextFileIfPresent,
|
|
12
12
|
} from "./shared.mjs";
|
|
13
13
|
|
|
14
|
-
export function startCodexHostedSession({
|
|
14
|
+
export function startCodexHostedSession({
|
|
15
|
+
command = "codex",
|
|
16
|
+
cwd,
|
|
17
|
+
prompt,
|
|
18
|
+
onEvent,
|
|
19
|
+
purpose = "assistant",
|
|
20
|
+
model = null,
|
|
21
|
+
providerArgs = [],
|
|
22
|
+
} = {}) {
|
|
15
23
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-codex-"));
|
|
16
24
|
const outputFile = path.join(tempDir, "final-message.txt");
|
|
17
25
|
const args = ["exec", "--json", "-o", outputFile];
|
|
@@ -19,6 +27,10 @@ export function startCodexHostedSession({ command = "codex", cwd, prompt, onEven
|
|
|
19
27
|
if (purpose === "assistant") {
|
|
20
28
|
args.push("-s", "read-only");
|
|
21
29
|
}
|
|
30
|
+
if (model) {
|
|
31
|
+
args.push("--model", String(model));
|
|
32
|
+
}
|
|
33
|
+
args.push(...normalizeProviderArgs(providerArgs));
|
|
22
34
|
|
|
23
35
|
args.push(prompt);
|
|
24
36
|
|
|
@@ -49,6 +61,11 @@ export function startCodexHostedSession({ command = "codex", cwd, prompt, onEven
|
|
|
49
61
|
};
|
|
50
62
|
}
|
|
51
63
|
|
|
64
|
+
function normalizeProviderArgs(providerArgs) {
|
|
65
|
+
if (!Array.isArray(providerArgs)) return [];
|
|
66
|
+
return providerArgs.flatMap((arg) => String(arg || "").split(/\s+/).filter(Boolean));
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
function parseCodexPayload(payload) {
|
|
53
70
|
const events = [];
|
|
54
71
|
if (!payload || typeof payload !== "object") return events;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { createElement, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { Box, Text, useApp, useInput } from "ink";
|
|
3
3
|
import { bold, dim, green, red, yellow } from "../presentation/colors.mjs";
|
|
4
|
+
import { getComposerRenderParts } from "./composer.mjs";
|
|
4
5
|
|
|
5
6
|
const MAX_VISIBLE_MESSAGES = 22;
|
|
6
7
|
|
|
@@ -113,11 +114,11 @@ function AssistantInputHandler({ assistantState, snapshot, onRequestClose }) {
|
|
|
113
114
|
assistantState.moveComposerCursorToEnd();
|
|
114
115
|
return;
|
|
115
116
|
}
|
|
116
|
-
if (key.backspace
|
|
117
|
+
if (key.backspace) {
|
|
117
118
|
assistantState.backspaceComposer();
|
|
118
119
|
return;
|
|
119
120
|
}
|
|
120
|
-
if (key.ctrl && input === "d") {
|
|
121
|
+
if (key.delete || (key.ctrl && input === "d")) {
|
|
121
122
|
assistantState.deleteComposer();
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
@@ -154,19 +155,14 @@ function renderMessage(message) {
|
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
function renderComposer(snapshot) {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const after = composer.slice(cursor + (composer[cursor] ? 1 : 0));
|
|
162
|
-
const placeholder = composer.length === 0 ? dim("Ask testkit to run or inspect something...") : "";
|
|
163
|
-
if (composer.length === 0) {
|
|
164
|
-
return createElement(Text, null, placeholder);
|
|
165
|
-
}
|
|
158
|
+
const { before, current, after, empty } = getComposerRenderParts({
|
|
159
|
+
text: snapshot.composer || "",
|
|
160
|
+
cursor: snapshot.composerCursor ?? 0,
|
|
161
|
+
});
|
|
166
162
|
return createElement(
|
|
167
163
|
Text,
|
|
168
164
|
null,
|
|
169
|
-
before,
|
|
165
|
+
empty ? dim("Ask testkit to run or inspect something... ") : before,
|
|
170
166
|
createElement(Text, { inverse: true }, current),
|
|
171
167
|
after
|
|
172
168
|
);
|
|
@@ -175,8 +171,11 @@ function renderComposer(snapshot) {
|
|
|
175
171
|
function buildHeader(snapshot) {
|
|
176
172
|
const status = snapshot.busy ? snapshot.activeStatus || "working" : "ready";
|
|
177
173
|
const provider = snapshot.provider || "auto";
|
|
174
|
+
const resolvedProvider = snapshot.resolvedProvider && snapshot.resolvedProvider !== provider ? `→${snapshot.resolvedProvider}` : "";
|
|
175
|
+
const model = snapshot.model ? ` · ${snapshot.model}` : "";
|
|
176
|
+
const effort = snapshot.effort ? ` · ${snapshot.effort}` : "";
|
|
178
177
|
const context = snapshot.context?.selection?.filePath || snapshot.context?.selection?.serviceName || "no focus";
|
|
179
|
-
return `testkit assistant · ${provider} · ${status} · ${context}`;
|
|
178
|
+
return `testkit assistant · ${provider}${resolvedProvider}${model}${effort} · ${status} · ${context}`;
|
|
180
179
|
}
|
|
181
180
|
|
|
182
181
|
function buildFooter(snapshot, promptFinished) {
|
|
@@ -186,7 +185,7 @@ function buildFooter(snapshot, promptFinished) {
|
|
|
186
185
|
if (snapshot.busy) {
|
|
187
186
|
return "Enter disabled while the provider is responding · Ctrl+C quit";
|
|
188
187
|
}
|
|
189
|
-
return "Enter send ·
|
|
188
|
+
return "Enter send · arrows/Home/End move cursor · Backspace/Delete edit · /settings · q quit";
|
|
190
189
|
}
|
|
191
190
|
|
|
192
191
|
function rolePrefix(message) {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const segmenter =
|
|
2
|
+
typeof Intl !== "undefined" && typeof Intl.Segmenter === "function"
|
|
3
|
+
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
4
|
+
: null;
|
|
5
|
+
|
|
6
|
+
export function createComposerState(value = "") {
|
|
7
|
+
const text = String(value || "");
|
|
8
|
+
return {
|
|
9
|
+
text,
|
|
10
|
+
cursor: splitGraphemes(text).length,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function setComposerText(state, value) {
|
|
15
|
+
const text = String(value || "");
|
|
16
|
+
const length = splitGraphemes(text).length;
|
|
17
|
+
return {
|
|
18
|
+
text,
|
|
19
|
+
cursor: clampCursor(state?.cursor ?? length, length),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function insertComposerText(state, value) {
|
|
24
|
+
const insertText = String(value || "");
|
|
25
|
+
if (!insertText) return normalizeComposerState(state);
|
|
26
|
+
const parts = splitGraphemes(state?.text || "");
|
|
27
|
+
const cursor = clampCursor(state?.cursor ?? parts.length, parts.length);
|
|
28
|
+
const insertParts = splitGraphemes(insertText);
|
|
29
|
+
const nextParts = [...parts.slice(0, cursor), ...insertParts, ...parts.slice(cursor)];
|
|
30
|
+
return {
|
|
31
|
+
text: nextParts.join(""),
|
|
32
|
+
cursor: cursor + insertParts.length,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function backspaceComposerText(state) {
|
|
37
|
+
const parts = splitGraphemes(state?.text || "");
|
|
38
|
+
const cursor = clampCursor(state?.cursor ?? parts.length, parts.length);
|
|
39
|
+
if (cursor === 0) return { text: parts.join(""), cursor };
|
|
40
|
+
const nextParts = [...parts.slice(0, cursor - 1), ...parts.slice(cursor)];
|
|
41
|
+
return {
|
|
42
|
+
text: nextParts.join(""),
|
|
43
|
+
cursor: cursor - 1,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function deleteComposerText(state) {
|
|
48
|
+
const parts = splitGraphemes(state?.text || "");
|
|
49
|
+
const cursor = clampCursor(state?.cursor ?? parts.length, parts.length);
|
|
50
|
+
if (cursor >= parts.length) return { text: parts.join(""), cursor };
|
|
51
|
+
const nextParts = [...parts.slice(0, cursor), ...parts.slice(cursor + 1)];
|
|
52
|
+
return {
|
|
53
|
+
text: nextParts.join(""),
|
|
54
|
+
cursor,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function moveComposerCursor(state, delta) {
|
|
59
|
+
const parts = splitGraphemes(state?.text || "");
|
|
60
|
+
return {
|
|
61
|
+
text: parts.join(""),
|
|
62
|
+
cursor: clampCursor((state?.cursor ?? parts.length) + Number(delta || 0), parts.length),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function moveComposerCursorToStart(state) {
|
|
67
|
+
return {
|
|
68
|
+
text: String(state?.text || ""),
|
|
69
|
+
cursor: 0,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function moveComposerCursorToEnd(state) {
|
|
74
|
+
const text = String(state?.text || "");
|
|
75
|
+
return {
|
|
76
|
+
text,
|
|
77
|
+
cursor: splitGraphemes(text).length,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getComposerRenderParts(state) {
|
|
82
|
+
const parts = splitGraphemes(state?.text || "");
|
|
83
|
+
const cursor = clampCursor(state?.cursor ?? parts.length, parts.length);
|
|
84
|
+
return {
|
|
85
|
+
before: parts.slice(0, cursor).join(""),
|
|
86
|
+
current: parts[cursor] || " ",
|
|
87
|
+
after: parts.slice(cursor + (parts[cursor] ? 1 : 0)).join(""),
|
|
88
|
+
empty: parts.length === 0,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeComposerState(state) {
|
|
93
|
+
const text = String(state?.text || "");
|
|
94
|
+
const parts = splitGraphemes(text);
|
|
95
|
+
return {
|
|
96
|
+
text,
|
|
97
|
+
cursor: clampCursor(state?.cursor ?? parts.length, parts.length),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function splitGraphemes(value) {
|
|
102
|
+
const text = String(value || "");
|
|
103
|
+
if (!text) return [];
|
|
104
|
+
if (segmenter) {
|
|
105
|
+
return [...segmenter.segment(text)].map((entry) => entry.segment);
|
|
106
|
+
}
|
|
107
|
+
return [...text];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function clampCursor(value, length) {
|
|
111
|
+
return Math.max(0, Math.min(Number(value || 0), length));
|
|
112
|
+
}
|
|
@@ -6,7 +6,11 @@ import { loadLatestRunArtifact, resolveFileSubject } from "../viewer.mjs";
|
|
|
6
6
|
|
|
7
7
|
export async function runInteractiveAssistant({
|
|
8
8
|
productDir,
|
|
9
|
-
provider
|
|
9
|
+
provider,
|
|
10
|
+
model,
|
|
11
|
+
effort,
|
|
12
|
+
providerArgs,
|
|
13
|
+
resetSettings = false,
|
|
10
14
|
file = null,
|
|
11
15
|
service = null,
|
|
12
16
|
prompt = null,
|
|
@@ -18,6 +22,10 @@ export async function runInteractiveAssistant({
|
|
|
18
22
|
const assistantState = createAssistantState({
|
|
19
23
|
productDir,
|
|
20
24
|
provider,
|
|
25
|
+
model,
|
|
26
|
+
effort,
|
|
27
|
+
providerArgs,
|
|
28
|
+
resetSettings,
|
|
21
29
|
configs,
|
|
22
30
|
env,
|
|
23
31
|
});
|
|
@@ -9,11 +9,13 @@ export async function runAssistantConversationTurn({
|
|
|
9
9
|
transcript,
|
|
10
10
|
userMessage,
|
|
11
11
|
provider = "auto",
|
|
12
|
+
settings = null,
|
|
12
13
|
env = process.env,
|
|
13
14
|
configs,
|
|
14
15
|
commandLog,
|
|
15
16
|
onStatus,
|
|
16
17
|
onToolEvent,
|
|
18
|
+
onResolvedProvider,
|
|
17
19
|
} = {}) {
|
|
18
20
|
const tools = listAssistantTools();
|
|
19
21
|
const toolContext = {
|
|
@@ -38,10 +40,16 @@ export async function runAssistantConversationTurn({
|
|
|
38
40
|
userMessage,
|
|
39
41
|
});
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
const runtimeSettings = settings || { provider };
|
|
44
|
+
const resolvedProvider = resolvePreferredProvider(runtimeSettings.provider || provider, env);
|
|
45
|
+
onResolvedProvider?.(resolvedProvider);
|
|
46
|
+
onStatus?.(`Thinking with ${resolvedProvider}...`);
|
|
42
47
|
const events = [];
|
|
43
48
|
const session = startAgentSession({
|
|
44
|
-
provider,
|
|
49
|
+
provider: runtimeSettings.provider || provider,
|
|
50
|
+
model: runtimeSettings.model || null,
|
|
51
|
+
effort: runtimeSettings.effort || null,
|
|
52
|
+
providerArgs: runtimeSettings.providerArgs || [],
|
|
45
53
|
cwd: productDir,
|
|
46
54
|
prompt,
|
|
47
55
|
purpose: "assistant",
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export const ASSISTANT_PROVIDERS = ["auto", "codex", "claude"];
|
|
5
|
+
export const ASSISTANT_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_ASSISTANT_SETTINGS = Object.freeze({
|
|
8
|
+
provider: "auto",
|
|
9
|
+
model: null,
|
|
10
|
+
effort: null,
|
|
11
|
+
providerArgs: [],
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export function assistantSettingsPath(productDir) {
|
|
15
|
+
return path.join(productDir || process.cwd(), ".testkit", "assistant", "settings.json");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function loadAssistantSettings(productDir) {
|
|
19
|
+
const filePath = assistantSettingsPath(productDir);
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
22
|
+
return normalizeAssistantSettings(parsed);
|
|
23
|
+
} catch {
|
|
24
|
+
return { ...DEFAULT_ASSISTANT_SETTINGS };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function saveAssistantSettings(productDir, settings) {
|
|
29
|
+
const filePath = assistantSettingsPath(productDir);
|
|
30
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
31
|
+
fs.writeFileSync(filePath, `${JSON.stringify(normalizeAssistantSettings(settings), null, 2)}\n`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resetAssistantSettings(productDir) {
|
|
35
|
+
const filePath = assistantSettingsPath(productDir);
|
|
36
|
+
fs.rmSync(filePath, { force: true });
|
|
37
|
+
return { ...DEFAULT_ASSISTANT_SETTINGS };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function mergeAssistantSettings(...settingsObjects) {
|
|
41
|
+
let merged = { ...DEFAULT_ASSISTANT_SETTINGS };
|
|
42
|
+
for (const settings of settingsObjects) {
|
|
43
|
+
if (!settings || typeof settings !== "object") continue;
|
|
44
|
+
merged = normalizeAssistantSettings({
|
|
45
|
+
...merged,
|
|
46
|
+
...dropNullishSettings(settings),
|
|
47
|
+
providerArgs: settings.providerArgs == null ? merged.providerArgs : settings.providerArgs,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return merged;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizeAssistantSettings(value = {}) {
|
|
54
|
+
const provider = normalizeProvider(value.provider);
|
|
55
|
+
const effort = normalizeEffort(value.effort);
|
|
56
|
+
const model = normalizeOptionalString(value.model);
|
|
57
|
+
const providerArgs = Array.isArray(value.providerArgs)
|
|
58
|
+
? value.providerArgs.map((entry) => normalizeOptionalString(entry)).filter(Boolean)
|
|
59
|
+
: [];
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
provider,
|
|
63
|
+
model,
|
|
64
|
+
effort,
|
|
65
|
+
providerArgs,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function normalizeProvider(value) {
|
|
70
|
+
const provider = normalizeOptionalString(value) || DEFAULT_ASSISTANT_SETTINGS.provider;
|
|
71
|
+
if (!ASSISTANT_PROVIDERS.includes(provider)) {
|
|
72
|
+
throw new Error(`Assistant provider must be one of: ${ASSISTANT_PROVIDERS.join(", ")}`);
|
|
73
|
+
}
|
|
74
|
+
return provider;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function normalizeEffort(value) {
|
|
78
|
+
const effort = normalizeOptionalString(value);
|
|
79
|
+
if (!effort) return null;
|
|
80
|
+
if (!ASSISTANT_EFFORTS.includes(effort)) {
|
|
81
|
+
throw new Error(`Assistant effort must be one of: ${ASSISTANT_EFFORTS.join(", ")}`);
|
|
82
|
+
}
|
|
83
|
+
return effort;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function normalizeOptionalString(value) {
|
|
87
|
+
if (value == null) return null;
|
|
88
|
+
const stringValue = String(value).trim();
|
|
89
|
+
return stringValue || null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function dropNullishSettings(settings) {
|
|
93
|
+
const result = {};
|
|
94
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
95
|
+
if (value !== undefined) result[key] = value;
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const RUN_TYPES = new Set(["int", "e2e", "scenario", "dal", "load", "pw", "all"]);
|
|
2
|
-
|
|
2
|
+
import { ASSISTANT_EFFORTS, ASSISTANT_PROVIDERS } from "./settings.mjs";
|
|
3
|
+
|
|
4
|
+
const PROVIDERS = new Set(ASSISTANT_PROVIDERS);
|
|
5
|
+
const EFFORTS = new Set(ASSISTANT_EFFORTS);
|
|
3
6
|
|
|
4
7
|
export function parseSlashCommand(input) {
|
|
5
8
|
const trimmed = String(input || "").trim();
|
|
@@ -19,6 +22,40 @@ export function parseSlashCommand(input) {
|
|
|
19
22
|
return { type: "provider", provider };
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
if (command === "model") {
|
|
26
|
+
const model = tokens.join(" ").trim();
|
|
27
|
+
if (!model) throw new Error("/model expects a model name or default");
|
|
28
|
+
return { type: "model", model: model === "default" ? null : model };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (command === "effort") {
|
|
32
|
+
const effort = tokens[0] || "";
|
|
33
|
+
if (effort === "default") return { type: "effort", effort: null };
|
|
34
|
+
if (!EFFORTS.has(effort)) {
|
|
35
|
+
throw new Error(`/effort expects one of: ${[...EFFORTS, "default"].join(", ")}`);
|
|
36
|
+
}
|
|
37
|
+
return { type: "effort", effort };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (command === "provider-arg") {
|
|
41
|
+
const action = tokens.shift() || "list";
|
|
42
|
+
if (action === "list") return { type: "provider-args-list" };
|
|
43
|
+
if (action === "clear") return { type: "provider-args-clear" };
|
|
44
|
+
if (action === "add") {
|
|
45
|
+
const value = tokens.join(" ").trim();
|
|
46
|
+
if (!value) throw new Error("/provider-arg add expects an argument");
|
|
47
|
+
return { type: "provider-args-add", value };
|
|
48
|
+
}
|
|
49
|
+
throw new Error('/provider-arg expects "add", "list", or "clear"');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (command === "settings") {
|
|
53
|
+
const action = tokens[0] || "show";
|
|
54
|
+
if (action === "show") return { type: "settings-show" };
|
|
55
|
+
if (action === "reset") return { type: "settings-reset" };
|
|
56
|
+
throw new Error('/settings expects "show" or "reset"');
|
|
57
|
+
}
|
|
58
|
+
|
|
22
59
|
if (command === "file" || command === "focus") {
|
|
23
60
|
if (!tokens[0]) throw new Error(`/${command} expects a file path`);
|
|
24
61
|
return { type: "file", file: tokens.join(" ") };
|
|
@@ -84,6 +121,13 @@ export function formatSlashHelpLines() {
|
|
|
84
121
|
"/status",
|
|
85
122
|
"/doctor",
|
|
86
123
|
"/provider <auto|claude|codex>",
|
|
124
|
+
"/model <model|default>",
|
|
125
|
+
"/effort <low|medium|high|xhigh|max|default>",
|
|
126
|
+
"/provider-arg add <arg>",
|
|
127
|
+
"/provider-arg list",
|
|
128
|
+
"/provider-arg clear",
|
|
129
|
+
"/settings",
|
|
130
|
+
"/settings reset",
|
|
87
131
|
"/clear",
|
|
88
132
|
"/quit",
|
|
89
133
|
];
|
|
@@ -5,10 +5,31 @@ import { parseSlashCommand, formatSlashHelpLines } from "./slash-commands.mjs";
|
|
|
5
5
|
import { executeAssistantTool } from "./tool-registry.mjs";
|
|
6
6
|
import { runAssistantConversationTurn } from "./session.mjs";
|
|
7
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";
|
|
8
25
|
|
|
9
26
|
export function createAssistantState({
|
|
10
27
|
productDir,
|
|
11
|
-
provider
|
|
28
|
+
provider,
|
|
29
|
+
model,
|
|
30
|
+
effort,
|
|
31
|
+
providerArgs,
|
|
32
|
+
resetSettings = false,
|
|
12
33
|
dataSource = "artifact",
|
|
13
34
|
configs = [],
|
|
14
35
|
env = process.env,
|
|
@@ -21,11 +42,19 @@ export function createAssistantState({
|
|
|
21
42
|
|
|
22
43
|
const listeners = new Set();
|
|
23
44
|
const messages = [];
|
|
24
|
-
let
|
|
25
|
-
let composerCursor = 0;
|
|
45
|
+
let composerState = createComposerState();
|
|
26
46
|
let notice = null;
|
|
27
47
|
let busy = false;
|
|
28
|
-
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;
|
|
29
58
|
let activeStatus = null;
|
|
30
59
|
|
|
31
60
|
inspectState.subscribe(() => {
|
|
@@ -90,16 +119,12 @@ export function createAssistantState({
|
|
|
90
119
|
},
|
|
91
120
|
|
|
92
121
|
setComposer(value) {
|
|
93
|
-
|
|
94
|
-
composerCursor = Math.max(0, Math.min(composer.length, composerCursor));
|
|
122
|
+
composerState = setComposerText(composerState, value);
|
|
95
123
|
notify();
|
|
96
124
|
},
|
|
97
125
|
|
|
98
126
|
insertComposer(text) {
|
|
99
|
-
|
|
100
|
-
if (!nextText) return;
|
|
101
|
-
composer = `${composer.slice(0, composerCursor)}${nextText}${composer.slice(composerCursor)}`;
|
|
102
|
-
composerCursor += nextText.length;
|
|
127
|
+
composerState = insertComposerText(composerState, text);
|
|
103
128
|
notify();
|
|
104
129
|
},
|
|
105
130
|
|
|
@@ -108,30 +133,27 @@ export function createAssistantState({
|
|
|
108
133
|
},
|
|
109
134
|
|
|
110
135
|
backspaceComposer() {
|
|
111
|
-
|
|
112
|
-
composer = `${composer.slice(0, composerCursor - 1)}${composer.slice(composerCursor)}`;
|
|
113
|
-
composerCursor -= 1;
|
|
136
|
+
composerState = backspaceComposerText(composerState);
|
|
114
137
|
notify();
|
|
115
138
|
},
|
|
116
139
|
|
|
117
140
|
deleteComposer() {
|
|
118
|
-
|
|
119
|
-
composer = `${composer.slice(0, composerCursor)}${composer.slice(composerCursor + 1)}`;
|
|
141
|
+
composerState = deleteComposerText(composerState);
|
|
120
142
|
notify();
|
|
121
143
|
},
|
|
122
144
|
|
|
123
145
|
moveComposerCursor(delta) {
|
|
124
|
-
|
|
146
|
+
composerState = moveComposerCursorState(composerState, delta);
|
|
125
147
|
notify();
|
|
126
148
|
},
|
|
127
149
|
|
|
128
150
|
moveComposerCursorToStart() {
|
|
129
|
-
|
|
151
|
+
composerState = moveComposerCursorStateToStart(composerState);
|
|
130
152
|
notify();
|
|
131
153
|
},
|
|
132
154
|
|
|
133
155
|
moveComposerCursorToEnd() {
|
|
134
|
-
|
|
156
|
+
composerState = moveComposerCursorStateToEnd(composerState);
|
|
135
157
|
notify();
|
|
136
158
|
},
|
|
137
159
|
|
|
@@ -146,7 +168,41 @@ export function createAssistantState({
|
|
|
146
168
|
},
|
|
147
169
|
|
|
148
170
|
setProvider(nextProvider) {
|
|
149
|
-
|
|
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;
|
|
150
206
|
notify();
|
|
151
207
|
},
|
|
152
208
|
|
|
@@ -156,9 +212,8 @@ export function createAssistantState({
|
|
|
156
212
|
},
|
|
157
213
|
|
|
158
214
|
async submitCurrentComposer() {
|
|
159
|
-
const value =
|
|
160
|
-
|
|
161
|
-
composerCursor = 0;
|
|
215
|
+
const value = composerState.text.trim();
|
|
216
|
+
composerState = createComposerState();
|
|
162
217
|
notify();
|
|
163
218
|
if (!value) return;
|
|
164
219
|
await state.submitInput(value);
|
|
@@ -180,7 +235,7 @@ export function createAssistantState({
|
|
|
180
235
|
slash,
|
|
181
236
|
state,
|
|
182
237
|
productDir,
|
|
183
|
-
|
|
238
|
+
settings,
|
|
184
239
|
configs,
|
|
185
240
|
env,
|
|
186
241
|
appendMessage,
|
|
@@ -197,13 +252,13 @@ export function createAssistantState({
|
|
|
197
252
|
}
|
|
198
253
|
|
|
199
254
|
try {
|
|
200
|
-
setBusy(true, `Thinking with ${
|
|
255
|
+
setBusy(true, `Thinking with ${settings.provider === "auto" ? "provider" : settings.provider}...`);
|
|
201
256
|
const emitted = await runAssistantConversationTurn({
|
|
202
257
|
productDir,
|
|
203
258
|
inspectState,
|
|
204
259
|
transcript: messages.map((entry) => ({ role: entry.role, text: entry.text })),
|
|
205
260
|
userMessage: trimmed,
|
|
206
|
-
|
|
261
|
+
settings,
|
|
207
262
|
env,
|
|
208
263
|
configs,
|
|
209
264
|
commandLog,
|
|
@@ -211,6 +266,10 @@ export function createAssistantState({
|
|
|
211
266
|
activeStatus = status;
|
|
212
267
|
notify();
|
|
213
268
|
},
|
|
269
|
+
onResolvedProvider(provider) {
|
|
270
|
+
resolvedProviderName = provider;
|
|
271
|
+
notify();
|
|
272
|
+
},
|
|
214
273
|
});
|
|
215
274
|
for (const message of emitted) appendMessage(message);
|
|
216
275
|
} catch (error) {
|
|
@@ -233,11 +292,15 @@ export function createAssistantState({
|
|
|
233
292
|
return {
|
|
234
293
|
context: buildContextSelection(inspectState.getSnapshot()),
|
|
235
294
|
messages: [...messages],
|
|
236
|
-
composer,
|
|
237
|
-
composerCursor,
|
|
295
|
+
composer: composerState.text,
|
|
296
|
+
composerCursor: composerState.cursor,
|
|
238
297
|
notice,
|
|
239
298
|
busy,
|
|
240
|
-
provider:
|
|
299
|
+
provider: settings.provider,
|
|
300
|
+
resolvedProvider: resolvedProviderName,
|
|
301
|
+
model: settings.model,
|
|
302
|
+
effort: settings.effort,
|
|
303
|
+
providerArgs: [...settings.providerArgs],
|
|
241
304
|
activeStatus,
|
|
242
305
|
contextPaths: {
|
|
243
306
|
contextPath: commandLog.contextPath,
|
|
@@ -258,7 +321,7 @@ async function executeSlashCommand({
|
|
|
258
321
|
slash,
|
|
259
322
|
state,
|
|
260
323
|
productDir,
|
|
261
|
-
|
|
324
|
+
settings,
|
|
262
325
|
configs,
|
|
263
326
|
env,
|
|
264
327
|
appendMessage,
|
|
@@ -280,6 +343,40 @@ async function executeSlashCommand({
|
|
|
280
343
|
appendMessage({ role: "assistant", text: `Provider set to ${slash.provider}.` });
|
|
281
344
|
return;
|
|
282
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
|
+
}
|
|
283
380
|
|
|
284
381
|
const result = await executeSlashTool(slash, {
|
|
285
382
|
productDir,
|
|
@@ -292,7 +389,7 @@ async function executeSlashCommand({
|
|
|
292
389
|
state.setNotice(event.message);
|
|
293
390
|
}
|
|
294
391
|
},
|
|
295
|
-
provider:
|
|
392
|
+
provider: settings.provider,
|
|
296
393
|
});
|
|
297
394
|
appendMessage({
|
|
298
395
|
role: "tool",
|
|
@@ -303,6 +400,17 @@ async function executeSlashCommand({
|
|
|
303
400
|
});
|
|
304
401
|
}
|
|
305
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
|
+
|
|
306
414
|
async function executeSlashTool(slash, context) {
|
|
307
415
|
switch (slash.type) {
|
|
308
416
|
case "inspect":
|
|
@@ -13,7 +13,20 @@ export default class AssistantCommand extends Command {
|
|
|
13
13
|
provider: Flags.string({
|
|
14
14
|
description: "Assistant provider",
|
|
15
15
|
options: ["auto", "claude", "codex"],
|
|
16
|
-
|
|
16
|
+
}),
|
|
17
|
+
model: Flags.string({
|
|
18
|
+
description: "Assistant model",
|
|
19
|
+
}),
|
|
20
|
+
effort: Flags.string({
|
|
21
|
+
description: "Assistant effort",
|
|
22
|
+
options: ["low", "medium", "high", "xhigh", "max"],
|
|
23
|
+
}),
|
|
24
|
+
"provider-arg": Flags.string({
|
|
25
|
+
description: "Advanced provider-specific argument, repeatable",
|
|
26
|
+
multiple: true,
|
|
27
|
+
}),
|
|
28
|
+
"reset-assistant-settings": Flags.boolean({
|
|
29
|
+
description: "Reset persisted assistant provider/model settings before starting",
|
|
17
30
|
}),
|
|
18
31
|
file: Flags.string({
|
|
19
32
|
description: "Initial file selection",
|
|
@@ -47,6 +60,10 @@ export default class AssistantCommand extends Command {
|
|
|
47
60
|
const assistantState = createAssistantState({
|
|
48
61
|
productDir,
|
|
49
62
|
provider: flags.provider,
|
|
63
|
+
model: flags.model,
|
|
64
|
+
effort: flags.effort,
|
|
65
|
+
providerArgs: flags["provider-arg"],
|
|
66
|
+
resetSettings: flags["reset-assistant-settings"],
|
|
50
67
|
configs: allConfigs,
|
|
51
68
|
env: process.env,
|
|
52
69
|
});
|
|
@@ -83,6 +100,10 @@ export default class AssistantCommand extends Command {
|
|
|
83
100
|
return runInteractiveAssistant({
|
|
84
101
|
productDir,
|
|
85
102
|
provider: flags.provider,
|
|
103
|
+
model: flags.model,
|
|
104
|
+
effort: flags.effort,
|
|
105
|
+
providerArgs: flags["provider-arg"],
|
|
106
|
+
resetSettings: flags["reset-assistant-settings"],
|
|
86
107
|
file: flags.file || null,
|
|
87
108
|
service: flags.service || null,
|
|
88
109
|
prompt: flags.prompt || null,
|
package/lib/cli/entrypoint.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.92",
|
|
4
4
|
"description": "Browser bridge helpers for testkit",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@elench/testkit-protocol": "0.1.
|
|
25
|
+
"@elench/testkit-protocol": "0.1.92"
|
|
26
26
|
},
|
|
27
27
|
"private": false
|
|
28
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.92",
|
|
4
4
|
"description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -82,10 +82,10 @@
|
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
84
|
"@babel/code-frame": "^7.29.0",
|
|
85
|
-
"@elench/next-analysis": "0.1.
|
|
86
|
-
"@elench/testkit-bridge": "0.1.
|
|
87
|
-
"@elench/testkit-protocol": "0.1.
|
|
88
|
-
"@elench/ts-analysis": "0.1.
|
|
85
|
+
"@elench/next-analysis": "0.1.92",
|
|
86
|
+
"@elench/testkit-bridge": "0.1.92",
|
|
87
|
+
"@elench/testkit-protocol": "0.1.92",
|
|
88
|
+
"@elench/ts-analysis": "0.1.92",
|
|
89
89
|
"@oclif/core": "^4.10.6",
|
|
90
90
|
"esbuild": "^0.25.11",
|
|
91
91
|
"execa": "^9.5.0",
|