@vitest-agent/cli 2.2.19 → 3.0.1
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/bin/vitest-agent.js +4 -24
- package/commands/agent.js +15 -23
- package/commands/db.js +3 -3
- package/commands/doctor.js +1 -1
- package/commands/record.js +3 -7
- package/commands/triage.js +1 -1
- package/commands/wrapup.js +1 -1
- package/index.d.ts +3 -158
- package/index.js +2 -16
- package/main.d.ts +22 -0
- package/main.js +58 -0
- package/package.json +10 -4
- package/version.js +13 -0
- package/layers/CliLive.js +0 -29
- package/layers/SidecarLive.js +0 -33
- package/lib/internal-end-agent.js +0 -27
- package/lib/internal-register-agent.js +0 -81
- package/lib/record-session.js +0 -33
- package/lib/record-tdd-artifact.js +0 -104
- package/lib/record-turn.js +0 -44
- package/lib/record-workspace-changes.js +0 -50
- package/lib/resolve-session-for-recording.js +0 -71
- package/lib/sidecar-paths.js +0 -101
package/bin/vitest-agent.js
CHANGED
|
@@ -1,34 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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 {
|
|
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
|
/**
|
package/commands/doctor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatDoctor } from "../lib/format-doctor.js";
|
|
2
|
-
import { DataReader, resolveDataPath } from "@vitest-agent/
|
|
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
|
|
package/commands/record.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import { recordSessionEnd, recordSessionStart } from "
|
|
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
|
-
|
|
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
|
-
|
|
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 },
|
package/commands/triage.js
CHANGED
package/commands/wrapup.js
CHANGED
package/index.d.ts
CHANGED
|
@@ -1,167 +1,12 @@
|
|
|
1
|
-
|
|
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 {
|
|
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
|
-
|
|
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": "
|
|
3
|
+
"version": "3.0.1",
|
|
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.
|
|
44
|
-
"@vitest-agent/
|
|
45
|
-
"@vitest-agent/
|
|
48
|
+
"@effected/workspaces": "^0.22.0",
|
|
49
|
+
"@vitest-agent/engine": "0.1.1",
|
|
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.1";
|
|
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 };
|
package/layers/SidecarLive.js
DELETED
|
@@ -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 };
|
package/lib/record-session.js
DELETED
|
@@ -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 };
|
package/lib/record-turn.js
DELETED
|
@@ -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 };
|
package/lib/sidecar-paths.js
DELETED
|
@@ -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 };
|