@vitest-agent/cli 2.2.19 → 3.0.0

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,34 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { CliLive } from "../layers/CliLive.js";
3
- import { agentCommand } from "../commands/agent.js";
4
- import { dbCommand } from "../commands/db.js";
5
- import { doctorCommand } from "../commands/doctor.js";
6
- import * as NodeServices from "@effect/platform-node/NodeServices";
7
- import { PathResolutionLive, formatFatalError, resolveDataPath, resolveLogFile, resolveLogLevel } from "@vitest-agent/sdk";
8
- import { Cause, Console, Effect } from "effect";
9
- import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
10
- import { Command } from "effect/unstable/cli";
2
+ import { main } from "../main.js";
11
3
 
12
4
  //#region src/bin.ts
13
5
  /**
14
- * CLI entry point for vitest-agent.
6
+ * CLI entry point for vitest-agent. Thin shim over `main.ts` — the
7
+ * assembled program that owns the process.
15
8
  *
16
9
  * @packageDocumentation
17
10
  */
18
- const rootCommand = Command.make("vitest-agent").pipe(Command.withSubcommands([
19
- dbCommand,
20
- doctorCommand,
21
- agentCommand
22
- ]));
23
- const cli = Command.run(rootCommand, { version: "0.0.0" });
24
- const logLevel = resolveLogLevel();
25
- const logFile = resolveLogFile();
26
- const projectDir = process.env.VITEST_AGENT_PROJECT_DIR ?? process.cwd();
27
- const main = resolveDataPath(projectDir).pipe(Effect.flatMap((dbPath) => cli.pipe(Effect.provide(CliLive(dbPath, logLevel, logFile)))), Effect.provide(PathResolutionLive(projectDir)), Effect.provide(NodeServices.layer), Effect.catchCause((cause) => {
28
- if (cause.reasons.filter(Cause.isDieReason).length > 0) return Console.error(`vitest-agent: ${formatFatalError(cause)}`).pipe(Effect.andThen(Effect.failCause(cause)));
29
- return Effect.failCause(cause);
30
- }));
31
- NodeRuntime.runMain(main);
11
+ main();
32
12
 
33
13
  //#endregion
34
14
  export { };
package/commands/agent.js CHANGED
@@ -1,16 +1,13 @@
1
- import { SidecarLive } from "../layers/SidecarLive.js";
2
- import { registerAgentEffect } from "../lib/internal-register-agent.js";
3
- import { DATA_DB_FILENAME, REGISTRY_DB_FILENAME, resolveProjectDataDir, resolveRegistryDir, resolveSessionMapPath } from "../lib/sidecar-paths.js";
4
- import { endAgentEffect } from "../lib/internal-end-agent.js";
5
1
  import { recordCommand } from "./record.js";
6
2
  import { triageCommand } from "./triage.js";
7
3
  import { wrapupCommand } from "./wrapup.js";
8
4
  import * as NodeServices from "@effect/platform-node/NodeServices";
9
- import { classifyTestPath, detectNonDefaultDiscoverStrategy, findOwningWorkspace, resolveProjectKeyFromCwd } from "@vitest-agent/sdk";
5
+ import { SidecarPlatformLive, endAgentEffect, registerAgentEffect, resolveHookPaths, resolveProjectKeyFromCwd } from "@vitest-agent/engine";
6
+ import { classifyTestPath, detectNonDefaultDiscoverStrategy, findOwningWorkspace } from "@vitest-agent/sdk";
10
7
  import { Cause, Effect, Option } from "effect";
8
+ import { Argument, Command, Flag } from "effect/unstable/cli";
11
9
  import { existsSync, readFileSync } from "node:fs";
12
10
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
13
- import { Argument, Command, Flag } from "effect/unstable/cli";
14
11
  import { findWorkspaceRootSync, getWorkspacePackagesSync } from "@effected/workspaces";
15
12
  import { nodeSyncOps } from "@effected/workspaces/node-sync";
16
13
  import { exitCodeForTag, injectEnv } from "@vitest-agent/sdk/dispatch";
@@ -85,14 +82,11 @@ const registerAgentSubcommand = Command.make("register-agent", {
85
82
  projectKeyOverride: projectKeyOverrideOpt
86
83
  }, (opts) => Effect.gen(function* () {
87
84
  const projectKey = Option.isSome(opts.projectKeyOverride) ? opts.projectKeyOverride.value : resolveProjectKeyFromCwd(opts.cwd);
88
- const perProjectDbPath = join(resolveProjectDataDir(projectKey), DATA_DB_FILENAME);
89
- const registryDbPath = join(resolveRegistryDir(), REGISTRY_DB_FILENAME);
90
- const sessionMapDbPath = yield* resolveSessionMapPath().pipe(Effect.catchCause(mapDefectToExit));
91
- const sidecar = SidecarLive({
92
- perProjectDbPath,
93
- sessionMapDbPath,
94
- registryDbPath
95
- });
85
+ const paths = yield* resolveHookPaths({
86
+ env: process.env,
87
+ projectKey
88
+ }).pipe(Effect.catchCause(mapDefectToExit));
89
+ const sidecar = SidecarPlatformLive(paths, process.env);
96
90
  const result = yield* registerAgentEffect({
97
91
  hostSessionId: opts.hostSessionId,
98
92
  transcriptPath: opts.transcriptPath,
@@ -124,14 +118,11 @@ const endAgentSubcommand = Command.make("end-agent", {
124
118
  projectKeyOverride: endProjectKeyOverrideOpt
125
119
  }, (opts) => Effect.gen(function* () {
126
120
  const projectKey = Option.isSome(opts.projectKeyOverride) ? opts.projectKeyOverride.value : resolveProjectKeyFromCwd(opts.cwd);
127
- const perProjectDbPath = join(resolveProjectDataDir(projectKey), DATA_DB_FILENAME);
128
- const registryDbPath = join(resolveRegistryDir(), REGISTRY_DB_FILENAME);
129
- const sessionMapDbPath = yield* resolveSessionMapPath().pipe(Effect.catchCause(mapDefectToExit));
130
- const sidecar = SidecarLive({
131
- perProjectDbPath,
132
- sessionMapDbPath,
133
- registryDbPath
134
- });
121
+ const paths = yield* resolveHookPaths({
122
+ env: process.env,
123
+ projectKey
124
+ }).pipe(Effect.catchCause(mapDefectToExit));
125
+ const sidecar = SidecarPlatformLive(paths, process.env);
135
126
  const endedAt = Option.isSome(opts.endedAt) ? opts.endedAt.value : Math.floor(Date.now() / 1e3);
136
127
  yield* endAgentEffect({
137
128
  agentId: opts.agentId,
@@ -148,7 +139,8 @@ const injectEnvSubcommand = Command.make("inject-env", {
148
139
  const out = injectEnv({
149
140
  command: opts.command,
150
141
  cwd: opts.cwd,
151
- env: process.env
142
+ env: process.env,
143
+ readFile: (path) => readFileSync(path, "utf-8")
152
144
  });
153
145
  process.stdout.write(`${out}\n`);
154
146
  })).pipe(Command.withDescription("Rewrite a Bash command to prepend VITEST_AGENT_* env vars when it invokes Vitest"));
package/commands/db.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { formatDbQuery } from "../lib/format-db-query.js";
2
2
  import * as NodeServices from "@effect/platform-node/NodeServices";
3
- import { layer } from "@effect/sql-sqlite-node/SqliteClient";
4
- import { DataStore, resolveDataPath } from "@vitest-agent/sdk";
3
+ import { DataStore, resolveDataPath } from "@vitest-agent/engine";
5
4
  import { Effect, FileSystem } from "effect";
6
5
  import { Argument, Command, Flag } from "effect/unstable/cli";
7
- import { SqlClient } from "effect/unstable/sql/SqlClient";
8
6
  import * as readline from "node:readline";
7
+ import { layer } from "@effect/sql-sqlite-node/SqliteClient";
8
+ import { SqlClient } from "effect/unstable/sql/SqlClient";
9
9
 
10
10
  //#region src/commands/db.ts
11
11
  /**
@@ -1,5 +1,5 @@
1
1
  import { formatDoctor } from "../lib/format-doctor.js";
2
- import { DataReader, resolveDataPath } from "@vitest-agent/sdk";
2
+ import { DataReader, resolveDataPath } from "@vitest-agent/engine";
3
3
  import { Effect, Option } from "effect";
4
4
  import { Command, Flag } from "effect/unstable/cli";
5
5
 
@@ -1,8 +1,4 @@
1
- import { recordSessionEnd, recordSessionStart } from "../lib/record-session.js";
2
- import { dispatchRecordTddArtifactEffect } from "../lib/record-tdd-artifact.js";
3
- import { recordTurnEffect } from "../lib/record-turn.js";
4
- import { recordRunWorkspaceChangesEffect } from "../lib/record-workspace-changes.js";
5
- import { DataReader, DataStore } from "@vitest-agent/sdk";
1
+ import { DataReader, DataStore, dispatchRecordTddArtifactEffect, recordRunWorkspaceChangesEffect, recordSessionEnd, recordSessionStart, recordTurnEffect } from "@vitest-agent/engine";
6
2
  import { Effect, Option } from "effect";
7
3
  import { Argument, Command, Flag } from "effect/unstable/cli";
8
4
 
@@ -25,7 +21,7 @@ const turnSubcommand = Command.make("turn", {
25
21
  payloadJson: payload,
26
22
  occurredAt,
27
23
  ...project._tag === "Some" && { project: project.value },
28
- ...cwd._tag === "Some" && { cwd: cwd.value }
24
+ cwd: cwd._tag === "Some" ? cwd.value : process.cwd()
29
25
  }).pipe(Effect.flatMap((result) => Effect.sync(() => process.stdout.write(`${JSON.stringify(result)}\n`))), Effect.catch((err) => Effect.sync(() => {
30
26
  process.stderr.write(`record turn: ${err instanceof Error ? err.message : String(err)}\n`);
31
27
  process.exit(1);
@@ -108,7 +104,7 @@ const tddArtifactSubcommand = Command.make("tdd-artifact", {
108
104
  ...opts.chatId._tag === "Some" && { chatId: opts.chatId.value },
109
105
  ...opts.tddTaskId._tag === "Some" && { tddTaskId: opts.tddTaskId.value },
110
106
  ...opts.project._tag === "Some" && { project: opts.project.value },
111
- ...opts.cwd._tag === "Some" && { cwd: opts.cwd.value },
107
+ cwd: opts.cwd._tag === "Some" ? opts.cwd.value : process.cwd(),
112
108
  artifactKind: opts.artifactKind,
113
109
  ...fileId !== void 0 && { fileId },
114
110
  ...opts.testCaseId._tag === "Some" && { testCaseId: opts.testCaseId.value },
@@ -1,4 +1,4 @@
1
- import { formatTriageEffect } from "@vitest-agent/sdk";
1
+ import { formatTriageEffect } from "@vitest-agent/engine";
2
2
  import { Effect } from "effect";
3
3
  import { Command, Flag } from "effect/unstable/cli";
4
4
 
@@ -1,4 +1,4 @@
1
- import { formatWrapupEffect } from "@vitest-agent/sdk";
1
+ import { formatWrapupEffect } from "@vitest-agent/engine";
2
2
  import { Effect } from "effect";
3
3
  import { Command, Flag } from "effect/unstable/cli";
4
4
 
package/index.d.ts CHANGED
@@ -1,167 +1,12 @@
1
- import * as NodeServices from "@effect/platform-node/NodeServices";
2
- import * as SqliteMigrator from "@effect/sql-sqlite-node/SqliteMigrator";
3
- import { Effect, Layer, LogLevel } from "effect";
4
- import { DataReader, DataStore, PerClientSessionMapWriter, ProjectIdentityNotResolvableError } from "@vitest-agent/sdk";
5
- //#region src/layers/CliLive.d.ts
6
- /**
7
- * Composition layer for the CLI runtime.
8
- *
9
- * Wires `DataReader`, `ProjectDiscovery`, `HistoryTracker`,
10
- * `OutputPipeline`, `SqliteClient`, the DB migrator, the Node platform
11
- * services, and `Logger` into a single layer the `vitest-agent`
12
- * bin provides to `Command.run`.
13
- *
14
- * @param dbPath - absolute path to the per-project `data.db`
15
- * @param logLevel - optional log level override; defaults to `Info`
16
- * @param logFile - optional path for structured log output
17
- * @public
18
- */
19
- export declare const CliLive: (dbPath: string, logLevel?: LogLevel.LogLevel, logFile?: string) => Layer.Layer<import("@vitest-agent/sdk").DataReader | import("@vitest-agent/sdk").DataStore | import("@vitest-agent/sdk").DetailResolver | import("@vitest-agent/sdk").EnvironmentDetector | import("@vitest-agent/sdk").ExecutorResolver | import("@vitest-agent/sdk").FormatSelector | import("@vitest-agent/sdk").HistoryTracker | import("@vitest-agent/sdk").OutputRenderer | import("@vitest-agent/sdk").ProjectDiscovery | import("effect/unstable/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | NodeServices.NodeServices, SqliteMigrator.MigrationError | import("effect/unstable/sql/SqlError").SqlError, never>;
20
- //#endregion
21
- //#region src/layers/SidecarLive.d.ts
22
- /**
23
- * SQLite database paths consumed by {@link SidecarLive}.
24
- *
25
- * @public
26
- */
27
- interface SidecarPaths {
28
- /** Absolute path to the per-project `data.db`. */
29
- readonly perProjectDbPath: string;
30
- /** Absolute path to the per-client `sessions.db`. */
31
- readonly sessionMapDbPath: string;
32
- /** Absolute path to the global `registry.db`. */
33
- readonly registryDbPath: string;
34
- }
35
- /**
36
- * Build the sidecar Live layer for the supplied SQLite paths.
37
- *
38
- * Each store gets its own `SqlClient` connection (separate scopes,
39
- * independent migrators) so concurrent operations on the three
40
- * stores don't share lock state.
41
- *
42
- * @param paths - the three SQLite database paths to open
43
- * @public
44
- */
45
- export declare const SidecarLive: (paths: SidecarPaths) => Layer.Layer<import("@vitest-agent/sdk").DataReader | import("@vitest-agent/sdk").DataStore | import("@vitest-agent/sdk").DiscoveryRegistry | import("@vitest-agent/sdk").PerClientSessionMapReader | import("@vitest-agent/sdk").PerClientSessionMapWriter | import("@vitest-agent/sdk").RunContext$ | NodeServices.NodeServices, SqliteMigrator.MigrationError | import("effect/unstable/sql/SqlError").SqlError, never>;
46
- //#endregion
47
- //#region src/lib/internal-register-agent.d.ts
48
- /**
49
- * Input for the end-to-end agent registration effect.
50
- *
51
- * @public
52
- */
53
- interface RegisterAgentInput {
54
- /** The host's native session identifier (e.g. Claude's `session_id`). */
55
- readonly hostSessionId: string;
56
- /** Absolute path to the host's conversation transcript file. */
57
- readonly transcriptPath: string;
58
- /** Working directory of the agent process. */
59
- readonly cwd: string;
60
- /** Host kind string (e.g. `"claude-code"`). */
61
- readonly hostKind: string;
62
- /** Agent type label (e.g. `"main"` or `"subagent"`). */
63
- readonly agentType: string;
64
- /** Normalized project key derived from the workspace root `package.json#name`. */
65
- readonly projectKey: string;
66
- /** Agent ID of the parent when registering a subagent. */
67
- readonly parentAgentId?: string;
68
- /** Optional idempotency nonce; derived from `hostSessionId + agentType + parentAgentId` when omitted. */
69
- readonly clientNonce?: string;
70
- }
71
- /**
72
- * Output of the end-to-end agent registration effect.
73
- *
74
- * @public
75
- */
76
- interface RegisterAgentOutput {
77
- /** Canonical UUID assigned to this agent in the per-project store. */
78
- readonly agentId: string;
79
- /** UUID of the conversation this agent belongs to. */
80
- readonly conversationId: string;
81
- /** Idempotency key used for the `registerAgent` upsert. */
82
- readonly idempotencyKey: string;
83
- /** `true` when an existing agent row was recovered rather than inserted. */
84
- readonly idempotencyHit: boolean;
85
- }
86
- /**
87
- * End-to-end agent registration.
88
- *
89
- * Returns the canonical `agentId` + the conversation it belongs to +
90
- * the resolved `idempotencyKey`. The `idempotencyHit` flag is `true`
91
- * when the per-project store recovered an existing agent row instead
92
- * of inserting a new one.
93
- *
94
- * @param input - registration inputs including host identifiers and agent metadata
95
- * @returns an Effect resolving to `RegisterAgentOutput`
96
- * @public
97
- */
98
- export declare const registerAgentEffect: (input: RegisterAgentInput) => Effect.Effect<{
99
- agentId: string;
100
- conversationId: string;
101
- mainAgentId: string;
102
- idempotencyKey: string;
103
- idempotencyHit: boolean;
104
- }, import("@vitest-agent/sdk").DataStoreError | import("@vitest-agent/sdk").RegistrationConflictError, DataReader | DataStore | PerClientSessionMapWriter | import("@vitest-agent/sdk").RunContext$>;
105
- //#endregion
106
- //#region src/lib/sidecar-paths.d.ts
107
- /**
108
- * Filename of the per-project test-data SQLite database.
109
- *
110
- * @public
111
- */
112
- export declare const DATA_DB_FILENAME = "data.db";
113
- /**
114
- * Filename of the per-client session-map SQLite database.
115
- *
116
- * @public
117
- */
118
- export declare const SESSIONS_DB_FILENAME = "sessions.db";
119
- /**
120
- * Filename of the global discovery-registry SQLite database.
121
- *
122
- * @public
123
- */
124
- export declare const REGISTRY_DB_FILENAME = "registry.db";
125
- /**
126
- * Resolve (and create) the per-project data directory for the supplied
127
- * normalized `projectKey`. Returns the directory; callers join
128
- * {@link DATA_DB_FILENAME} onto it for the `data.db` path.
129
- *
130
- * @param projectKey - normalized project key (e.g. `@org__pkg`)
131
- * @returns absolute path to the resolved (and created) data directory
132
- * @public
133
- */
134
- export declare const resolveProjectDataDir: (projectKey: string) => string;
135
- /**
136
- * Resolve (and create) the directory holding the global `registry.db`.
137
- * Callers join {@link REGISTRY_DB_FILENAME} onto it.
138
- *
139
- * @returns absolute path to the resolved (and created) registry directory
140
- * @public
141
- */
142
- export declare const resolveRegistryDir: () => string;
143
- /**
144
- * Resolve the per-client `sessions.db` path.
145
- *
146
- * Precedence: the `CLAUDE_PLUGIN_DATA` env var, then
147
- * `VITEST_AGENT_SESSION_MAP_DIR`, then `~/.vitest-agent/`. Fails
148
- * with `ProjectIdentityNotResolvableError` when no home directory
149
- * is resolvable.
150
- *
151
- * @returns an Effect resolving to the absolute `sessions.db` path
152
- * @public
153
- */
154
- export declare const resolveSessionMapPath: () => Effect.Effect<string, ProjectIdentityNotResolvableError>;
155
- //#endregion
156
- //#region src/index.d.ts
1
+ //#region src/version.d.ts
157
2
  /**
158
3
  * The version of this package, inlined at build time from
159
4
  * `package.json#version` via rslib-builder's `__PACKAGE_VERSION__` substitution.
160
- * Exported for version introspection by downstream tooling.
5
+ * Exported for version introspection by downstream tooling, and consumed by
6
+ * `main.ts` to back `Command.run`'s `version` option.
161
7
  *
162
8
  * @public
163
9
  */
164
10
  export declare const CURRENT_CLI_VERSION: string;
165
11
  //#endregion
166
- export type { RegisterAgentInput, RegisterAgentOutput, SidecarPaths };
167
12
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,17 +1,3 @@
1
- import { CliLive } from "./layers/CliLive.js";
2
- import { SidecarLive } from "./layers/SidecarLive.js";
3
- import { registerAgentEffect } from "./lib/internal-register-agent.js";
4
- import { DATA_DB_FILENAME, REGISTRY_DB_FILENAME, SESSIONS_DB_FILENAME, resolveProjectDataDir, resolveRegistryDir, resolveSessionMapPath } from "./lib/sidecar-paths.js";
1
+ import { CURRENT_CLI_VERSION } from "./version.js";
5
2
 
6
- //#region src/index.ts
7
- /**
8
- * The version of this package, inlined at build time from
9
- * `package.json#version` via rslib-builder's `__PACKAGE_VERSION__` substitution.
10
- * Exported for version introspection by downstream tooling.
11
- *
12
- * @public
13
- */
14
- const CURRENT_CLI_VERSION = "2.2.19";
15
-
16
- //#endregion
17
- export { CURRENT_CLI_VERSION, CliLive, DATA_DB_FILENAME, REGISTRY_DB_FILENAME, SESSIONS_DB_FILENAME, SidecarLive, registerAgentEffect, resolveProjectDataDir, resolveRegistryDir, resolveSessionMapPath };
3
+ export { CURRENT_CLI_VERSION };
package/main.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ //#region src/main.d.ts
2
+ /**
3
+ * Assembled CLI program. Owns the process: resolves the data path, wires the
4
+ * engine's platform layers, and runs the root command via
5
+ * `NodeRuntime.runMain`.
6
+ *
7
+ * `bin.ts` is the published bin shim (`#!/usr/bin/env node` +
8
+ * `main()`); this module is also published as the `./main` subpath so a
9
+ * consumer can invoke the assembled program without going through the shim.
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+ /**
14
+ * Assembles and runs the CLI program, taking over the process. Not
15
+ * re-exported from `index.ts` — a library consumer's import graph must not
16
+ * pull in the process-owning module.
17
+ *
18
+ * @public
19
+ */
20
+ export declare const main: () => void;
21
+ //#endregion
22
+ //# sourceMappingURL=main.d.ts.map
package/main.js ADDED
@@ -0,0 +1,58 @@
1
+ import { CURRENT_CLI_VERSION } from "./version.js";
2
+ import { agentCommand } from "./commands/agent.js";
3
+ import { dbCommand } from "./commands/db.js";
4
+ import { doctorCommand } from "./commands/doctor.js";
5
+ import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
6
+ import * as NodeServices from "@effect/platform-node/NodeServices";
7
+ import { PathResolutionLive, PlatformLive, resolveDataPath, resolveLogFile, resolveLogLevel, resolveProjectDir } from "@vitest-agent/engine";
8
+ import { formatFatalError } from "@vitest-agent/sdk";
9
+ import { Cause, Console, Effect } from "effect";
10
+ import { Command } from "effect/unstable/cli";
11
+
12
+ //#region src/main.ts
13
+ /**
14
+ * Assembled CLI program. Owns the process: resolves the data path, wires the
15
+ * engine's platform layers, and runs the root command via
16
+ * `NodeRuntime.runMain`.
17
+ *
18
+ * `bin.ts` is the published bin shim (`#!/usr/bin/env node` +
19
+ * `main()`); this module is also published as the `./main` subpath so a
20
+ * consumer can invoke the assembled program without going through the shim.
21
+ *
22
+ * @packageDocumentation
23
+ */
24
+ const rootCommand = Command.make("vitest-agent").pipe(Command.withSubcommands([
25
+ dbCommand,
26
+ doctorCommand,
27
+ agentCommand
28
+ ]));
29
+ /**
30
+ * Assembles and runs the CLI program, taking over the process. Not
31
+ * re-exported from `index.ts` — a library consumer's import graph must not
32
+ * pull in the process-owning module.
33
+ *
34
+ * @public
35
+ */
36
+ const main = () => {
37
+ const env = process.env;
38
+ const logLevel = resolveLogLevel(env);
39
+ const logFile = resolveLogFile(env);
40
+ const projectDir = resolveProjectDir({
41
+ env,
42
+ cwd: process.cwd()
43
+ });
44
+ const cli = Command.run(rootCommand, { version: CURRENT_CLI_VERSION });
45
+ const program = resolveDataPath(projectDir).pipe(Effect.flatMap((dbPath) => cli.pipe(Effect.provide(PlatformLive({
46
+ dbPath,
47
+ env,
48
+ logLevel,
49
+ logFile
50
+ })))), Effect.provide(PathResolutionLive(projectDir)), Effect.provide(NodeServices.layer), Effect.catchCause((cause) => {
51
+ if (cause.reasons.filter(Cause.isDieReason).length > 0) return Console.error(`vitest-agent: ${formatFatalError(cause)}`).pipe(Effect.andThen(Effect.failCause(cause)));
52
+ return Effect.failCause(cause);
53
+ }));
54
+ NodeRuntime.runMain(program);
55
+ };
56
+
57
+ //#endregion
58
+ export { main };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/cli",
3
- "version": "2.2.19",
3
+ "version": "3.0.0",
4
4
  "private": false,
5
5
  "description": "On-demand CLI for vitest-agent. Reads cached test data and reports status, overview, coverage, history, trends, and cache health.",
6
6
  "keywords": [
@@ -32,6 +32,11 @@
32
32
  "import": "./index.js",
33
33
  "default": "./index.js"
34
34
  },
35
+ "./main": {
36
+ "types": "./main.d.ts",
37
+ "import": "./main.js",
38
+ "default": "./main.js"
39
+ },
35
40
  "./package.json": "./package.json"
36
41
  },
37
42
  "bin": {
@@ -40,9 +45,10 @@
40
45
  "dependencies": {
41
46
  "@effect/platform-node": "4.0.0-rc.115",
42
47
  "@effect/sql-sqlite-node": "4.0.0-rc.115",
43
- "@effected/workspaces": "^0.21.0",
44
- "@vitest-agent/sdk": "3.1.2",
45
- "@vitest-agent/sidecar": "2.1.19",
48
+ "@effected/workspaces": "^0.21.1",
49
+ "@vitest-agent/engine": "0.1.0",
50
+ "@vitest-agent/sdk": "4.0.0",
51
+ "@vitest-agent/sidecar": "2.1.20",
46
52
  "effect": "4.0.0-rc.115"
47
53
  },
48
54
  "engines": {
package/version.js ADDED
@@ -0,0 +1,13 @@
1
+ //#region src/version.ts
2
+ /**
3
+ * The version of this package, inlined at build time from
4
+ * `package.json#version` via rslib-builder's `__PACKAGE_VERSION__` substitution.
5
+ * Exported for version introspection by downstream tooling, and consumed by
6
+ * `main.ts` to back `Command.run`'s `version` option.
7
+ *
8
+ * @public
9
+ */
10
+ const CURRENT_CLI_VERSION = "3.0.0";
11
+
12
+ //#endregion
13
+ export { CURRENT_CLI_VERSION };
package/layers/CliLive.js DELETED
@@ -1,29 +0,0 @@
1
- import * as NodeServices from "@effect/platform-node/NodeServices";
2
- import { layer } from "@effect/sql-sqlite-node/SqliteClient";
3
- import * as SqliteMigrator from "@effect/sql-sqlite-node/SqliteMigrator";
4
- import { DataReaderLive, DataStoreLive, HistoryTrackerLive, LoggerLive, OutputPipelineLive, PROJECT_MIGRATIONS, ProjectDiscoveryLive } from "@vitest-agent/sdk";
5
- import { Layer } from "effect";
6
-
7
- //#region src/layers/CliLive.ts
8
- /**
9
- * Composition layer for the CLI runtime.
10
- *
11
- * Wires `DataReader`, `ProjectDiscovery`, `HistoryTracker`,
12
- * `OutputPipeline`, `SqliteClient`, the DB migrator, the Node platform
13
- * services, and `Logger` into a single layer the `vitest-agent`
14
- * bin provides to `Command.run`.
15
- *
16
- * @param dbPath - absolute path to the per-project `data.db`
17
- * @param logLevel - optional log level override; defaults to `Info`
18
- * @param logFile - optional path for structured log output
19
- * @public
20
- */
21
- const CliLive = (dbPath, logLevel, logFile) => {
22
- const SqliteLayer = layer({ filename: dbPath });
23
- const PlatformLayer = NodeServices.layer;
24
- const MigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord(PROJECT_MIGRATIONS) }).pipe(Layer.provide(Layer.merge(SqliteLayer, PlatformLayer)));
25
- return Layer.mergeAll(ProjectDiscoveryLive, HistoryTrackerLive, OutputPipelineLive).pipe(Layer.provideMerge(DataReaderLive), Layer.provideMerge(DataStoreLive), Layer.provideMerge(MigratorLayer), Layer.provideMerge(SqliteLayer), Layer.provideMerge(PlatformLayer), Layer.provideMerge(LoggerLive(logLevel, logFile)));
26
- };
27
-
28
- //#endregion
29
- export { CliLive };
@@ -1,33 +0,0 @@
1
- import * as NodeServices from "@effect/platform-node/NodeServices";
2
- import { layer } from "@effect/sql-sqlite-node/SqliteClient";
3
- import * as SqliteMigrator from "@effect/sql-sqlite-node/SqliteMigrator";
4
- import { DataReaderLive, DataStoreLive, DiscoveryRegistryLive, LoggerLive, PROJECT_MIGRATIONS, PerClientSessionMapWriterLive, RunContextLive, registryMigration0001, sessionMapMigration0001 } from "@vitest-agent/sdk";
5
- import { Layer } from "effect";
6
-
7
- //#region src/layers/SidecarLive.ts
8
- /**
9
- * Build the sidecar Live layer for the supplied SQLite paths.
10
- *
11
- * Each store gets its own `SqlClient` connection (separate scopes,
12
- * independent migrators) so concurrent operations on the three
13
- * stores don't share lock state.
14
- *
15
- * @param paths - the three SQLite database paths to open
16
- * @public
17
- */
18
- const SidecarLive = (paths) => {
19
- const PlatformLayer = NodeServices.layer;
20
- const ProjectSqliteLayer = layer({ filename: paths.perProjectDbPath });
21
- const ProjectMigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord(PROJECT_MIGRATIONS) }).pipe(Layer.provide(Layer.merge(ProjectSqliteLayer, PlatformLayer)));
22
- const ProjectStoreLayer = Layer.mergeAll(DataStoreLive.pipe(Layer.provide(ProjectSqliteLayer)), DataReaderLive.pipe(Layer.provide(ProjectSqliteLayer)), ProjectMigratorLayer);
23
- const SessionMapSqliteLayer = layer({ filename: paths.sessionMapDbPath });
24
- const SessionMapMigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord({ "0001_initial": sessionMapMigration0001 }) }).pipe(Layer.provide(Layer.merge(SessionMapSqliteLayer, PlatformLayer)));
25
- const SessionMapLayer = Layer.mergeAll(PerClientSessionMapWriterLive.pipe(Layer.provide(SessionMapSqliteLayer)), SessionMapMigratorLayer);
26
- const RegistrySqliteLayer = layer({ filename: paths.registryDbPath });
27
- const RegistryMigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord({ "0001_initial": registryMigration0001 }) }).pipe(Layer.provide(Layer.merge(RegistrySqliteLayer, PlatformLayer)));
28
- const RegistryLayer = Layer.mergeAll(DiscoveryRegistryLive.pipe(Layer.provide(RegistrySqliteLayer)), RegistryMigratorLayer);
29
- return Layer.mergeAll(ProjectStoreLayer, SessionMapLayer, RegistryLayer, RunContextLive).pipe(Layer.provideMerge(PlatformLayer), Layer.provideMerge(LoggerLive()));
30
- };
31
-
32
- //#endregion
33
- export { SidecarLive };
@@ -1,27 +0,0 @@
1
- import { DataStore, PerClientSessionMapWriter } from "@vitest-agent/sdk";
2
- import { Effect } from "effect";
3
-
4
- //#region src/lib/internal-end-agent.ts
5
- /**
6
- * Sidecar `_internal end-agent` implementation.
7
- *
8
- * Sets `agents.ended_at` on the per-project store. For main-agent
9
- * stops (SessionEnd), the caller also passes `--host-session-id` so
10
- * the per-client session map's `ended_at` is updated and
11
- * `lookupByProjectDir` no longer returns the row as the active
12
- * session. For subagent stops (SubagentStop), the host session stays
13
- * open — only the subagent's `agents` row is closed.
14
- *
15
- * @packageDocumentation
16
- */
17
- /**
18
- * End an agent. Closes the `agents` row, optionally also closes the
19
- * `session_map` row.
20
- */
21
- const endAgentEffect = (input) => Effect.gen(function* () {
22
- yield* (yield* DataStore).endAgent(input.agentId, input.endedAt);
23
- if (input.hostSessionId !== void 0) yield* (yield* PerClientSessionMapWriter).endSession(input.hostSessionId, input.endedAt);
24
- });
25
-
26
- //#endregion
27
- export { endAgentEffect };
@@ -1,81 +0,0 @@
1
- import { DataReader, DataStore, PerClientSessionMapWriter, RunContextService, deriveIdempotencyKey } from "@vitest-agent/sdk";
2
- import { Effect, Option } from "effect";
3
-
4
- //#region src/lib/internal-register-agent.ts
5
- const deriveDefaultClientNonce = (input) => {
6
- const parent = input.parentAgentId ?? "__ROOT__";
7
- return `${input.hostSessionId}|${input.agentType}|${parent}`;
8
- };
9
- /**
10
- * End-to-end agent registration.
11
- *
12
- * Returns the canonical `agentId` + the conversation it belongs to +
13
- * the resolved `idempotencyKey`. The `idempotencyHit` flag is `true`
14
- * when the per-project store recovered an existing agent row instead
15
- * of inserting a new one.
16
- *
17
- * @param input - registration inputs including host identifiers and agent metadata
18
- * @returns an Effect resolving to `RegisterAgentOutput`
19
- * @public
20
- */
21
- const registerAgentEffect = (input) => Effect.gen(function* () {
22
- const sessionMap = yield* PerClientSessionMapWriter;
23
- const reader = yield* DataReader;
24
- const store = yield* DataStore;
25
- const ctx = yield* RunContextService;
26
- const conversationId = yield* sessionMap.mapConversation(input.transcriptPath);
27
- const sessionMapping = yield* sessionMap.mapSession({
28
- hostSessionId: input.hostSessionId,
29
- conversationId,
30
- projectKey: input.projectKey,
31
- projectDir: input.cwd
32
- });
33
- const existingSession = yield* reader.getSessionByChatId(input.hostSessionId);
34
- const sessionRowId = yield* Option.match(existingSession, {
35
- onNone: () => store.writeSession({
36
- chatId: input.hostSessionId,
37
- project: input.projectKey,
38
- cwd: input.cwd,
39
- agentKind: input.parentAgentId === void 0 ? "main" : "subagent",
40
- agentType: input.agentType,
41
- triageWasNonEmpty: false,
42
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
43
- conversationId
44
- }),
45
- onSome: (s) => Effect.succeed(s.id)
46
- });
47
- yield* store.setSessionConversationIdIfNull({
48
- sessionId: sessionRowId,
49
- conversationId
50
- });
51
- const agentContext = yield* ctx.captureAgentContext(input.cwd);
52
- const clientNonce = input.clientNonce ?? deriveDefaultClientNonce(input);
53
- const idempotencyKey = deriveIdempotencyKey({
54
- agentType: input.agentType,
55
- parentAgentId: input.parentAgentId ?? null,
56
- clientNonce
57
- });
58
- const result = yield* store.registerAgent({
59
- sessionId: sessionRowId,
60
- agentType: input.agentType,
61
- parentAgentId: input.parentAgentId ?? null,
62
- conversationId,
63
- startedAt: Math.floor(Date.now() / 1e3),
64
- ...agentContext.startGitBranch !== null && { startGitBranch: agentContext.startGitBranch },
65
- ...agentContext.startGitCommitSha !== null && { startGitCommitSha: agentContext.startGitCommitSha },
66
- ...agentContext.startWorktreeDir !== null && { startWorktreeDir: agentContext.startWorktreeDir },
67
- idempotencyKey,
68
- agentId: sessionMapping.mainAgentId
69
- });
70
- const isHit = result._tag === "IdempotencyHit";
71
- return {
72
- agentId: isHit ? result.existingAgentId : result.agentId,
73
- conversationId,
74
- mainAgentId: sessionMapping.mainAgentId,
75
- idempotencyKey,
76
- idempotencyHit: isHit
77
- };
78
- });
79
-
80
- //#endregion
81
- export { registerAgentEffect };
@@ -1,33 +0,0 @@
1
- import { DataReader, DataStore } from "@vitest-agent/sdk";
2
- import { Effect } from "effect";
3
-
4
- //#region src/lib/record-session.ts
5
- const recordSessionStart = (input) => Effect.gen(function* () {
6
- const reader = yield* DataReader;
7
- const store = yield* DataStore;
8
- let parentSessionId;
9
- if (input.parentChatId !== void 0) {
10
- const parent = yield* reader.getSessionByChatId(input.parentChatId);
11
- if (parent._tag === "Some") parentSessionId = parent.value.id;
12
- }
13
- return { sessionId: yield* store.upsertSession({
14
- chatId: input.chatId,
15
- project: input.project,
16
- cwd: input.cwd,
17
- agentKind: input.agentKind,
18
- ...input.agentType !== void 0 && { agentType: input.agentType },
19
- ...parentSessionId !== void 0 && { parentSessionId },
20
- triageWasNonEmpty: input.triageWasNonEmpty,
21
- startedAt: input.startedAt
22
- }) };
23
- });
24
- const recordSessionEnd = (input) => Effect.gen(function* () {
25
- const reader = yield* DataReader;
26
- const store = yield* DataStore;
27
- if ((yield* reader.getSessionByChatId(input.chatId))._tag === "None") return yield* Effect.fail(/* @__PURE__ */ new Error(`Unknown chatId: ${input.chatId}`));
28
- yield* store.endSession(input.chatId, input.endedAt, input.endReason);
29
- return { ok: true };
30
- });
31
-
32
- //#endregion
33
- export { recordSessionEnd, recordSessionStart };
@@ -1,104 +0,0 @@
1
- import { resolveSessionForRecording } from "./resolve-session-for-recording.js";
2
- import { DataReader, DataStore } from "@vitest-agent/sdk";
3
- import { Effect, Option } from "effect";
4
-
5
- //#region src/lib/record-tdd-artifact.ts
6
- /**
7
- * Resolve (auto-opening a `spike` phase if needed) the current open
8
- * phase for `tddTaskId`, then write the artifact under it. Shared by
9
- * both `recordTddArtifactEffect` (chat-id → session → task resolution)
10
- * and `recordTddArtifactByTaskIdEffect` (explicit task-id escape hatch).
11
- */
12
- const writeArtifactUnderOpenPhase = (tddTaskId, input) => Effect.gen(function* () {
13
- const reader = yield* DataReader;
14
- const store = yield* DataStore;
15
- const phaseOpt = yield* reader.getCurrentTddPhase(tddTaskId);
16
- const phaseId = Option.isSome(phaseOpt) ? phaseOpt.value.id : (yield* store.writeTddPhase({
17
- tddTaskId,
18
- phase: "spike",
19
- startedAt: input.recordedAt,
20
- transitionReason: "auto-opened by record tdd-artifact (no prior phase)"
21
- })).id;
22
- return {
23
- id: yield* store.writeTddArtifact({
24
- phaseId,
25
- artifactKind: input.artifactKind,
26
- ...input.fileId !== void 0 && { fileId: input.fileId },
27
- ...input.testCaseId !== void 0 && { testCaseId: input.testCaseId },
28
- ...input.testRunId !== void 0 && { testRunId: input.testRunId },
29
- ...input.testFirstFailureRunId !== void 0 && { testFirstFailureRunId: input.testFirstFailureRunId },
30
- ...input.diffExcerpt !== void 0 && { diffExcerpt: input.diffExcerpt },
31
- ...input.suite !== void 0 && { suite: input.suite },
32
- recordedAt: input.recordedAt
33
- }),
34
- phaseId
35
- };
36
- });
37
- const recordTddArtifactEffect = (input) => Effect.gen(function* () {
38
- const reader = yield* DataReader;
39
- const session = yield* resolveSessionForRecording({
40
- chatId: input.chatId,
41
- recordedAt: input.recordedAt,
42
- ...input.project !== void 0 && { project: input.project },
43
- ...input.cwd !== void 0 && { cwd: input.cwd }
44
- });
45
- const openTdd = (yield* reader.listTddTasksForSession(session.id, {
46
- walkParents: true,
47
- walkConversation: true
48
- })).find((t) => t.endedAt === null);
49
- if (openTdd === void 0) return yield* Effect.fail(/* @__PURE__ */ new Error(`No open TDD task under chat_id ${input.chatId}. Call tdd_task start first.`));
50
- return yield* writeArtifactUnderOpenPhase(openTdd.id, input);
51
- });
52
- /**
53
- * Explicit task-id escape hatch (issue #144). Bypasses `chat_id` →
54
- * session → task resolution entirely — the caller already knows which
55
- * TDD task the artifact belongs to (e.g. a hook seeded with
56
- * `VITEST_AGENT_TDD_TASK_ID` in a detached-session environment where
57
- * neither the parent walk nor the conversation-id fallback resolves
58
- * the right task). Fails loudly when the task does not exist or has
59
- * already ended — writing to a closed task's phase would silently
60
- * corrupt the evidence trail.
61
- */
62
- const recordTddArtifactByTaskIdEffect = (input) => Effect.gen(function* () {
63
- const taskOpt = yield* (yield* DataReader).getTddTaskById(input.tddTaskId);
64
- if (Option.isNone(taskOpt)) return yield* Effect.fail(/* @__PURE__ */ new Error(`No TDD task found for id ${input.tddTaskId}.`));
65
- if (taskOpt.value.endedAt !== null) return yield* Effect.fail(/* @__PURE__ */ new Error(`TDD task ${input.tddTaskId} has already ended (endedAt=${taskOpt.value.endedAt}).`));
66
- return yield* writeArtifactUnderOpenPhase(input.tddTaskId, input);
67
- });
68
- /**
69
- * CLI-command-facing dispatcher (issue #144). Keeps `commands/record.ts`
70
- * a thin flag-parsing wrapper per this package's convention. `tddTaskId`
71
- * takes priority when both are supplied — it is the explicit escape
72
- * hatch and should never silently fall back to (weaker) session
73
- * resolution.
74
- */
75
- const dispatchRecordTddArtifactEffect = (input) => {
76
- if (input.tddTaskId !== void 0) return recordTddArtifactByTaskIdEffect({
77
- tddTaskId: input.tddTaskId,
78
- artifactKind: input.artifactKind,
79
- ...input.fileId !== void 0 && { fileId: input.fileId },
80
- ...input.testCaseId !== void 0 && { testCaseId: input.testCaseId },
81
- ...input.testRunId !== void 0 && { testRunId: input.testRunId },
82
- ...input.testFirstFailureRunId !== void 0 && { testFirstFailureRunId: input.testFirstFailureRunId },
83
- ...input.diffExcerpt !== void 0 && { diffExcerpt: input.diffExcerpt },
84
- ...input.suite !== void 0 && { suite: input.suite },
85
- recordedAt: input.recordedAt
86
- });
87
- if (input.chatId !== void 0) return recordTddArtifactEffect({
88
- chatId: input.chatId,
89
- artifactKind: input.artifactKind,
90
- ...input.fileId !== void 0 && { fileId: input.fileId },
91
- ...input.testCaseId !== void 0 && { testCaseId: input.testCaseId },
92
- ...input.testRunId !== void 0 && { testRunId: input.testRunId },
93
- ...input.testFirstFailureRunId !== void 0 && { testFirstFailureRunId: input.testFirstFailureRunId },
94
- ...input.diffExcerpt !== void 0 && { diffExcerpt: input.diffExcerpt },
95
- ...input.suite !== void 0 && { suite: input.suite },
96
- recordedAt: input.recordedAt,
97
- ...input.cwd !== void 0 && { cwd: input.cwd },
98
- ...input.project !== void 0 && { project: input.project }
99
- });
100
- return Effect.fail(/* @__PURE__ */ new Error("record tdd-artifact requires either --chat-id or --tdd-task-id."));
101
- };
102
-
103
- //#endregion
104
- export { dispatchRecordTddArtifactEffect, recordTddArtifactByTaskIdEffect, recordTddArtifactEffect };
@@ -1,44 +0,0 @@
1
- import { resolveSessionForRecording } from "./resolve-session-for-recording.js";
2
- import { DataStore, TurnPayload } from "@vitest-agent/sdk";
3
- import { Effect, Schema } from "effect";
4
-
5
- //#region src/lib/record-turn.ts
6
- const parseAndValidateTurnPayload = (raw) => {
7
- let parsed;
8
- try {
9
- parsed = JSON.parse(raw);
10
- } catch (e) {
11
- return {
12
- ok: false,
13
- error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}`
14
- };
15
- }
16
- const decoded = Schema.decodeUnknownResult(TurnPayload)(parsed);
17
- if (decoded._tag === "Failure") return {
18
- ok: false,
19
- error: `Invalid TurnPayload: ${decoded.failure.message}`
20
- };
21
- return {
22
- ok: true,
23
- payload: decoded.success
24
- };
25
- };
26
- const recordTurnEffect = (input) => Effect.gen(function* () {
27
- const parse = parseAndValidateTurnPayload(input.payloadJson);
28
- if (!parse.ok) return yield* Effect.fail(new Error(parse.error));
29
- const session = yield* resolveSessionForRecording({
30
- chatId: input.chatId,
31
- recordedAt: input.occurredAt,
32
- ...input.project !== void 0 && { project: input.project },
33
- ...input.cwd !== void 0 && { cwd: input.cwd }
34
- });
35
- return { turnId: yield* (yield* DataStore).writeTurn({
36
- sessionId: session.id,
37
- type: parse.payload.type,
38
- payload: input.payloadJson,
39
- occurredAt: input.occurredAt
40
- }) };
41
- });
42
-
43
- //#endregion
44
- export { parseAndValidateTurnPayload, recordTurnEffect };
@@ -1,50 +0,0 @@
1
- import { DataStore } from "@vitest-agent/sdk";
2
- import { Effect } from "effect";
3
- import { SqlClient } from "effect/unstable/sql/SqlClient";
4
-
5
- //#region src/lib/record-workspace-changes.ts
6
- /**
7
- * `ProjectRunSummary` doesn't expose `lastRunId`, so we query it directly
8
- * via SqlClient. This is the run id of the most-recent test run for a
9
- * given project (or any project, when `project` is unspecified). Used
10
- * only as a best-effort association target for `run_changed_files`.
11
- */
12
- const findLatestRunId = (project) => Effect.gen(function* () {
13
- const sql = yield* SqlClient;
14
- const rows = project !== void 0 ? yield* sql`
15
- SELECT id FROM test_runs
16
- WHERE project = ${project}
17
- ORDER BY timestamp DESC LIMIT 1
18
- ` : yield* sql`
19
- SELECT id FROM test_runs
20
- ORDER BY timestamp DESC LIMIT 1
21
- `;
22
- return rows.length === 0 ? null : rows[0].id;
23
- }).pipe(Effect.orElseSucceed(() => null));
24
- const recordRunWorkspaceChangesEffect = (input) => Effect.gen(function* () {
25
- const store = yield* DataStore;
26
- yield* store.writeCommit({
27
- sha: input.sha,
28
- ...input.parentSha !== void 0 && { parentSha: input.parentSha },
29
- ...input.message !== void 0 && { message: input.message },
30
- ...input.author !== void 0 && { author: input.author },
31
- ...input.committedAt !== void 0 && { committedAt: input.committedAt },
32
- ...input.branch !== void 0 && { branch: input.branch }
33
- });
34
- const runId = yield* findLatestRunId(input.project);
35
- if (runId !== null && input.files.length > 0) yield* store.writeRunChangedFiles({
36
- runId,
37
- files: input.files.map((f) => ({
38
- filePath: f.filePath,
39
- changeKind: f.changeKind,
40
- commitSha: input.sha
41
- }))
42
- });
43
- return {
44
- sha: input.sha,
45
- fileRowsWritten: runId !== null ? input.files.length : 0
46
- };
47
- });
48
-
49
- //#endregion
50
- export { recordRunWorkspaceChangesEffect };
@@ -1,71 +0,0 @@
1
- import { DataReader, DataStore } from "@vitest-agent/sdk";
2
- import { Effect, Option } from "effect";
3
- import { readFileSync } from "node:fs";
4
-
5
- //#region src/lib/resolve-session-for-recording.ts
6
- /**
7
- * Shared session-resolution helper for hook-driven recording paths.
8
- *
9
- * Background: Claude Code can rotate the chat id (host `session_id`)
10
- * mid-window — after a continuation, compaction, or `/mcp` reconnect —
11
- * without the `SessionStart` hook firing for the new id. PostToolUse
12
- * hooks then deliver tool calls under a `chat_id` that has no row in
13
- * `sessions`, and the strict `getSessionByChatId` lookup fails with
14
- * "Unknown chat_id". A second flavour of the same bug: the
15
- * SubagentStart hook mints a synthetic per-dispatch key
16
- * (`<parentChatId>-subagent-<ts>-<pid>`) for the subagent row, but
17
- * subsequent PostToolUse hooks under that subagent receive the bare
18
- * parent chat id. Exact-match lookup of the bare id misses the
19
- * synthetic suffix.
20
- *
21
- * This resolver applies a three-step fallback that recovers either
22
- * case without crashing the recording path:
23
- *
24
- * 1. Exact match by `chat_id`.
25
- * 2. Prefix match `<chatId>-subagent-` to recover the synthetic
26
- * subagent row (most recent first).
27
- * 3. Idempotent bootstrap of a `main` session row keyed on the
28
- * input `chat_id`, so future calls in this window resolve via
29
- * step 1.
30
- *
31
- * Step 3 means recording paths can no longer fail with "Unknown
32
- * chat_id". Bootstrapped rows carry `agent_kind = "main"` and
33
- * `triage_was_non_empty = false` because the SessionStart triage
34
- * pipeline never ran for them; downstream metrics consumers should
35
- * treat triage-empty bootstrapped rows the same as any other
36
- * SessionStart-less row.
37
- *
38
- * @packageDocumentation
39
- */
40
- const resolveSessionForRecording = (input) => Effect.gen(function* () {
41
- const reader = yield* DataReader;
42
- const store = yield* DataStore;
43
- const exact = yield* reader.getSessionByChatId(input.chatId);
44
- if (Option.isSome(exact)) return exact.value;
45
- const synthetic = yield* reader.findSessionsByChatPrefix(`${input.chatId}-subagent-`);
46
- if (synthetic.length > 0) return synthetic[0];
47
- const cwd = input.cwd ?? process.cwd();
48
- const project = input.project ?? readPackageName(cwd) ?? "unknown";
49
- yield* store.upsertSession({
50
- chatId: input.chatId,
51
- project,
52
- cwd,
53
- agentKind: "main",
54
- triageWasNonEmpty: false,
55
- startedAt: input.recordedAt
56
- });
57
- const created = yield* reader.getSessionByChatId(input.chatId);
58
- return Option.getOrThrowWith(created, () => /* @__PURE__ */ new Error(`upsertSession succeeded but row not found for ${input.chatId}`));
59
- });
60
- const readPackageName = (cwd) => {
61
- try {
62
- const raw = readFileSync(`${cwd}/package.json`, "utf8");
63
- const parsed = JSON.parse(raw);
64
- return typeof parsed.name === "string" ? parsed.name : void 0;
65
- } catch {
66
- return;
67
- }
68
- };
69
-
70
- //#endregion
71
- export { resolveSessionForRecording };
@@ -1,101 +0,0 @@
1
- import { ProjectIdentityNotResolvableError } from "@vitest-agent/sdk";
2
- import { Effect } from "effect";
3
- import { mkdirSync } from "node:fs";
4
- import { homedir } from "node:os";
5
- import { join } from "node:path";
6
-
7
- //#region src/lib/sidecar-paths.ts
8
- const APP_NAMESPACE = "vitest-agent";
9
- /**
10
- * Filename of the per-project test-data SQLite database.
11
- *
12
- * @public
13
- */
14
- const DATA_DB_FILENAME = "data.db";
15
- /**
16
- * Filename of the per-client session-map SQLite database.
17
- *
18
- * @public
19
- */
20
- const SESSIONS_DB_FILENAME = "sessions.db";
21
- /**
22
- * Filename of the global discovery-registry SQLite database.
23
- *
24
- * @public
25
- */
26
- const REGISTRY_DB_FILENAME = "registry.db";
27
- const resolveXdgDataHome = () => {
28
- const xdg = process.env.XDG_DATA_HOME;
29
- if (xdg !== void 0 && xdg.length > 0) return xdg;
30
- return join(homedir(), ".local", "share");
31
- };
32
- /**
33
- * Resolve (and create) the per-project data directory for the supplied
34
- * normalized `projectKey`. Returns the directory; callers join
35
- * {@link DATA_DB_FILENAME} onto it for the `data.db` path.
36
- *
37
- * @param projectKey - normalized project key (e.g. `@org__pkg`)
38
- * @returns absolute path to the resolved (and created) data directory
39
- * @public
40
- */
41
- const resolveProjectDataDir = (projectKey) => {
42
- const dir = join(resolveXdgDataHome(), APP_NAMESPACE, projectKey);
43
- mkdirSync(dir, { recursive: true });
44
- return dir;
45
- };
46
- /**
47
- * Resolve (and create) the directory holding the global `registry.db`.
48
- * Callers join {@link REGISTRY_DB_FILENAME} onto it.
49
- *
50
- * @returns absolute path to the resolved (and created) registry directory
51
- * @public
52
- */
53
- const resolveRegistryDir = () => {
54
- const dir = join(resolveXdgDataHome(), APP_NAMESPACE);
55
- mkdirSync(dir, { recursive: true });
56
- return dir;
57
- };
58
- /**
59
- * Resolve the per-client `sessions.db` path.
60
- *
61
- * Precedence: the `CLAUDE_PLUGIN_DATA` env var, then
62
- * `VITEST_AGENT_SESSION_MAP_DIR`, then `~/.vitest-agent/`. Fails
63
- * with `ProjectIdentityNotResolvableError` when no home directory
64
- * is resolvable.
65
- *
66
- * @returns an Effect resolving to the absolute `sessions.db` path
67
- * @public
68
- */
69
- const resolveSessionMapPath = () => Effect.sync(() => {
70
- const claudePluginData = process.env.CLAUDE_PLUGIN_DATA;
71
- if (claudePluginData !== void 0 && claudePluginData.length > 0) {
72
- mkdirSync(claudePluginData, { recursive: true });
73
- return join(claudePluginData, SESSIONS_DB_FILENAME);
74
- }
75
- const overrideDir = process.env.VITEST_AGENT_SESSION_MAP_DIR;
76
- if (overrideDir !== void 0 && overrideDir.length > 0) {
77
- mkdirSync(overrideDir, { recursive: true });
78
- return join(overrideDir, SESSIONS_DB_FILENAME);
79
- }
80
- const home = process.env.HOME ?? process.env.USERPROFILE;
81
- if (home === void 0 || home.length === 0) return null;
82
- const fallbackDir = join(home, ".vitest-agent");
83
- mkdirSync(fallbackDir, { recursive: true });
84
- return join(fallbackDir, SESSIONS_DB_FILENAME);
85
- }).pipe(Effect.flatMap((path) => path !== null ? Effect.succeed(path) : Effect.fail(new ProjectIdentityNotResolvableError({ tried: [
86
- {
87
- source: "CLAUDE_PLUGIN_DATA",
88
- reason: "env var not set"
89
- },
90
- {
91
- source: "VITEST_AGENT_SESSION_MAP_DIR",
92
- reason: "env var not set"
93
- },
94
- {
95
- source: "HOME",
96
- reason: "env var not set (USERPROFILE also unset)"
97
- }
98
- ] }))));
99
-
100
- //#endregion
101
- export { DATA_DB_FILENAME, REGISTRY_DB_FILENAME, SESSIONS_DB_FILENAME, resolveProjectDataDir, resolveRegistryDir, resolveSessionMapPath };