@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.
@@ -1,52 +1,61 @@
1
- import path from "path";
2
- import { createInspectState } from "../tui/inspect-state.mjs";
1
+ import React, { createElement } from "react";
2
+ import { render } from "ink";
3
+ import { createAssistantState } from "./state.mjs";
4
+ import { AssistantApp } from "./app.mjs";
3
5
  import { loadLatestRunArtifact, resolveFileSubject } from "../viewer.mjs";
4
- import { startInteractiveProviderSession } from "../agents/index.mjs";
5
- import { prepareAssistantBootstrap } from "./bootstrap.mjs";
6
6
 
7
7
  export async function runInteractiveAssistant({
8
8
  productDir,
9
- provider = "auto",
9
+ provider,
10
+ model,
11
+ effort,
12
+ providerArgs,
13
+ resetSettings = false,
10
14
  file = null,
11
15
  service = null,
12
16
  prompt = null,
13
17
  env = process.env,
18
+ configs = [],
19
+ stdout = process.stdout,
20
+ stderr = process.stderr,
14
21
  } = {}) {
15
- const inspectState = createInspectState({ dataSource: "artifact" });
16
- try {
17
- inspectState.hydrateFromArtifact(loadLatestRunArtifact(productDir));
18
- } catch {
19
- // No persisted artifact yet.
20
- }
22
+ const assistantState = createAssistantState({
23
+ productDir,
24
+ provider,
25
+ model,
26
+ effort,
27
+ providerArgs,
28
+ resetSettings,
29
+ configs,
30
+ env,
31
+ });
21
32
 
33
+ await assistantState.loadLatestArtifact();
22
34
  if (file) {
23
35
  try {
24
36
  const artifact = loadLatestRunArtifact(productDir);
25
37
  const subject = resolveFileSubject(artifact, file, service || null);
26
- inspectState.revealFile(subject.service.name, subject.file.path);
38
+ assistantState.revealFile(subject.service.name, subject.file.path);
27
39
  } catch {
28
40
  // Ignore unresolved focus.
29
41
  }
30
42
  } else if (service) {
31
- inspectState.revealService(service);
43
+ assistantState.revealService(service);
32
44
  }
33
45
 
34
- const bootstrap = prepareAssistantBootstrap({
35
- productDir,
36
- inspectState,
37
- initialPrompt: prompt,
38
- });
39
-
40
- const sessionEnv = {
41
- ...env,
42
- ...bootstrap.env,
43
- PATH: [bootstrap.binDir, env.PATH || ""].filter(Boolean).join(path.delimiter),
44
- };
46
+ const app = render(
47
+ createElement(AssistantApp, {
48
+ assistantState,
49
+ initialPrompt: prompt,
50
+ exitAfterInitialPrompt: Boolean(prompt),
51
+ inputEnabled: Boolean(process.stdin.isTTY),
52
+ }),
53
+ {
54
+ stdout,
55
+ stderr,
56
+ exitOnCtrlC: false,
57
+ }
58
+ );
45
59
 
46
- return startInteractiveProviderSession({
47
- provider,
48
- cwd: productDir,
49
- prompt: bootstrap.prompt,
50
- env: sessionEnv,
51
- });
60
+ return app.waitUntilExit();
52
61
  }
@@ -14,8 +14,10 @@ export function buildAssistantPrompt({
14
14
 
15
15
  return [
16
16
  "You are Testkit Assistant.",
17
- "You help users run tests, inspect failures, read artifacts/logs, and explain the current local test state.",
18
- "Prefer using tools when the user is asking testkit to inspect or run something.",
17
+ "You help users run tests, inspect failures, read logs and artifacts, and navigate the current local test state.",
18
+ "All user natural-language requests must be handled through your own reasoning plus the available tools.",
19
+ "Prefer real repository commands through shell_exec when the user asks to run tests or inspect the working repo.",
20
+ "Use read_context before repeating artifact/log inspection work, and use read_file/search_repo when you need codebase context.",
19
21
  buildAssistantResponseContract({ tools }),
20
22
  "",
21
23
  "Current run summary:",
@@ -9,16 +9,21 @@ 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,
15
+ commandLog,
14
16
  onStatus,
15
17
  onToolEvent,
18
+ onResolvedProvider,
16
19
  } = {}) {
17
20
  const tools = listAssistantTools();
18
21
  const toolContext = {
19
22
  productDir,
20
23
  inspectState,
21
24
  configs,
25
+ env,
26
+ commandLog,
22
27
  onEvent: onToolEvent,
23
28
  };
24
29
 
@@ -35,10 +40,16 @@ export async function runAssistantConversationTurn({
35
40
  userMessage,
36
41
  });
37
42
 
38
- onStatus?.(`Thinking with ${resolvePreferredProvider(provider, env)}...`);
43
+ const runtimeSettings = settings || { provider };
44
+ const resolvedProvider = resolvePreferredProvider(runtimeSettings.provider || provider, env);
45
+ onResolvedProvider?.(resolvedProvider);
46
+ onStatus?.(`Thinking with ${resolvedProvider}...`);
39
47
  const events = [];
40
48
  const session = startAgentSession({
41
- provider,
49
+ provider: runtimeSettings.provider || provider,
50
+ model: runtimeSettings.model || null,
51
+ effort: runtimeSettings.effort || null,
52
+ providerArgs: runtimeSettings.providerArgs || [],
42
53
  cwd: productDir,
43
54
  prompt,
44
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
- const PROVIDERS = new Set(["auto", "claude", "codex"]);
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
  ];