@vitest-agent/cli 1.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.
@@ -0,0 +1,204 @@
1
+ import { recordSessionEnd, recordSessionStart } from "../lib/record-session.js";
2
+ import { recordTddArtifactEffect } 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";
6
+ import { Effect, Option } from "effect";
7
+ import { Args, Command, Options } from "@effect/cli";
8
+
9
+ //#region src/commands/record.ts
10
+ /**
11
+ * CLI record command -- write session/turn data to the database.
12
+ *
13
+ * Hook scripts in plugin/hooks/ shell out to these subcommands. The
14
+ * record-turn and record-session libs (in ../lib) implement the actual
15
+ * write effects; commands here are thin \@effect/cli wrappers.
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+ const chatId = Options.text("chat-id").pipe(Options.withDescription("Host chat id (`session_id` in the Claude Code hook envelope; equivalent in other clients)"));
20
+ const occurredAt = Options.text("occurred-at").pipe(Options.withDefault((/* @__PURE__ */ new Date()).toISOString()), Options.withDescription("ISO 8601 timestamp; defaults to now"));
21
+ const payloadArg = Args.text({ name: "payload-json" }).pipe(Args.withDescription("Stringified JSON payload (validated against TurnPayload)"));
22
+ const project = Options.text("project");
23
+ const cwd = Options.text("cwd");
24
+ const projectOptional = Options.optional(Options.text("project"));
25
+ const cwdOptional = Options.optional(Options.text("cwd"));
26
+ const turnSubcommand = Command.make("turn", {
27
+ chatId,
28
+ occurredAt,
29
+ project: projectOptional,
30
+ cwd: cwdOptional,
31
+ payload: payloadArg
32
+ }, ({ chatId, occurredAt, project, cwd, payload }) => recordTurnEffect({
33
+ chatId,
34
+ payloadJson: payload,
35
+ occurredAt,
36
+ ...project._tag === "Some" && { project: project.value },
37
+ ...cwd._tag === "Some" && { cwd: cwd.value }
38
+ }).pipe(Effect.flatMap((result) => Effect.sync(() => process.stdout.write(`${JSON.stringify(result)}\n`))), Effect.catchAll((err) => Effect.sync(() => {
39
+ process.stderr.write(`record turn: ${err instanceof Error ? err.message : String(err)}\n`);
40
+ process.exit(1);
41
+ })))).pipe(Command.withDescription("Validate a TurnPayload JSON and write a turn row"));
42
+ const agentKind = Options.choice("agent-kind", ["main", "subagent"]).pipe(Options.withDefault("main"));
43
+ const agentType = Options.optional(Options.text("agent-type"));
44
+ const parentChatId = Options.optional(Options.text("parent-chat-id"));
45
+ const triageWasNonEmpty = Options.boolean("triage-was-non-empty").pipe(Options.withDefault(false));
46
+ const startedAt = Options.text("started-at").pipe(Options.withDefault((/* @__PURE__ */ new Date()).toISOString()));
47
+ const sessionStartSubcommand = Command.make("session-start", {
48
+ chatId,
49
+ project,
50
+ cwd,
51
+ agentKind,
52
+ agentType,
53
+ parentChatId,
54
+ triageWasNonEmpty,
55
+ startedAt
56
+ }, (opts) => recordSessionStart({
57
+ chatId: opts.chatId,
58
+ project: opts.project,
59
+ cwd: opts.cwd,
60
+ agentKind: opts.agentKind,
61
+ ...opts.agentType._tag === "Some" && { agentType: opts.agentType.value },
62
+ ...opts.parentChatId._tag === "Some" && { parentChatId: opts.parentChatId.value },
63
+ triageWasNonEmpty: opts.triageWasNonEmpty,
64
+ startedAt: opts.startedAt
65
+ }).pipe(Effect.flatMap((result) => Effect.sync(() => process.stdout.write(`${JSON.stringify(result)}\n`))), Effect.catchAll((err) => Effect.sync(() => {
66
+ process.stderr.write(`record session-start: ${err instanceof Error ? err.message : String(err)}\n`);
67
+ process.exit(1);
68
+ })))).pipe(Command.withDescription("Insert a new sessions row"));
69
+ const endedAt = Options.text("ended-at").pipe(Options.withDefault((/* @__PURE__ */ new Date()).toISOString()));
70
+ const endReason = Options.optional(Options.text("end-reason"));
71
+ const sessionEndSubcommand = Command.make("session-end", {
72
+ chatId,
73
+ endedAt,
74
+ endReason
75
+ }, (opts) => recordSessionEnd({
76
+ chatId: opts.chatId,
77
+ endedAt: opts.endedAt,
78
+ endReason: opts.endReason._tag === "Some" ? opts.endReason.value : null
79
+ }).pipe(Effect.flatMap(() => Effect.sync(() => process.stdout.write(`{"ok":true}\n`))), Effect.catchAll((err) => Effect.sync(() => {
80
+ process.stderr.write(`record session-end: ${err instanceof Error ? err.message : String(err)}\n`);
81
+ process.exit(1);
82
+ })))).pipe(Command.withDescription("Update sessions.ended_at + end_reason"));
83
+ const artifactKindOpt = Options.choice("artifact-kind", [
84
+ "test_written",
85
+ "test_failed_run",
86
+ "code_written",
87
+ "test_passed_run",
88
+ "refactor",
89
+ "test_weakened"
90
+ ]);
91
+ const filePathOpt = Options.optional(Options.text("file-path"));
92
+ const testCaseIdOpt = Options.optional(Options.integer("test-case-id"));
93
+ const testRunIdOpt = Options.optional(Options.integer("test-run-id"));
94
+ const testFirstFailureRunIdOpt = Options.optional(Options.integer("test-first-failure-run-id"));
95
+ const diffExcerptOpt = Options.optional(Options.text("diff-excerpt"));
96
+ const recordedAtOpt = Options.text("recorded-at").pipe(Options.withDefault((/* @__PURE__ */ new Date()).toISOString()));
97
+ const tddArtifactSubcommand = Command.make("tdd-artifact", {
98
+ chatId,
99
+ project: projectOptional,
100
+ cwd: cwdOptional,
101
+ artifactKind: artifactKindOpt,
102
+ filePath: filePathOpt,
103
+ testCaseId: testCaseIdOpt,
104
+ testRunId: testRunIdOpt,
105
+ testFirstFailureRunId: testFirstFailureRunIdOpt,
106
+ diffExcerpt: diffExcerptOpt,
107
+ recordedAt: recordedAtOpt
108
+ }, (opts) => Effect.gen(function* () {
109
+ let fileId;
110
+ if (opts.filePath._tag === "Some") fileId = yield* (yield* DataStore).ensureFile(opts.filePath.value);
111
+ return yield* recordTddArtifactEffect({
112
+ chatId: opts.chatId,
113
+ ...opts.project._tag === "Some" && { project: opts.project.value },
114
+ ...opts.cwd._tag === "Some" && { cwd: opts.cwd.value },
115
+ artifactKind: opts.artifactKind,
116
+ ...fileId !== void 0 && { fileId },
117
+ ...opts.testCaseId._tag === "Some" && { testCaseId: opts.testCaseId.value },
118
+ ...opts.testRunId._tag === "Some" && { testRunId: opts.testRunId.value },
119
+ ...opts.testFirstFailureRunId._tag === "Some" && { testFirstFailureRunId: opts.testFirstFailureRunId.value },
120
+ ...opts.diffExcerpt._tag === "Some" && { diffExcerpt: opts.diffExcerpt.value },
121
+ recordedAt: opts.recordedAt
122
+ });
123
+ }).pipe(Effect.flatMap((result) => Effect.sync(() => process.stdout.write(`${JSON.stringify(result)}\n`))), Effect.catchAll((err) => Effect.sync(() => {
124
+ process.stderr.write(`record tdd-artifact: ${err instanceof Error ? err.message : String(err)}\n`);
125
+ process.exit(1);
126
+ })))).pipe(Command.withDescription("Record a TDD artifact (D7: CLI-only)"));
127
+ const testCaseTurnsSubcommand = Command.make("test-case-turns", { chatId }, ({ chatId }) => Effect.gen(function* () {
128
+ const store = yield* DataStore;
129
+ const reader = yield* DataReader;
130
+ const updated = yield* store.backfillTestCaseTurns(chatId);
131
+ const latestId = yield* reader.getLatestTestCaseForSession(chatId);
132
+ return {
133
+ updated,
134
+ latestTestCaseId: Option.getOrNull(latestId)
135
+ };
136
+ }).pipe(Effect.flatMap((result) => Effect.sync(() => process.stdout.write(`${JSON.stringify(result)}\n`))), Effect.catchAll((err) => Effect.sync(() => {
137
+ process.stderr.write(`record test-case-turns: ${err instanceof Error ? err.message : String(err)}\n`);
138
+ process.exit(1);
139
+ })))).pipe(Command.withDescription("Backfill test_cases.created_turn_id from file_edits in the current session (BUG-2 fix)"));
140
+ const invocationMethodOpt = Options.choice("invocation-method", [
141
+ "bash",
142
+ "mcp",
143
+ "cli"
144
+ ]).pipe(Options.withDescription("How tests were invoked: \"bash\", \"mcp\", or \"cli\""), Options.withDefault("bash"));
145
+ const runTriggerSubcommand = Command.make("run-trigger", {
146
+ chatId,
147
+ invocationMethod: invocationMethodOpt
148
+ }, ({ chatId, invocationMethod }) => Effect.gen(function* () {
149
+ yield* (yield* DataStore).associateLatestRunWithSession({
150
+ chatId,
151
+ invocationMethod
152
+ });
153
+ }).pipe(Effect.catchAll((err) => Effect.sync(() => {
154
+ process.stderr.write(`record run-trigger: ${err instanceof Error ? err.message : String(err)}\n`);
155
+ process.exit(1);
156
+ })))).pipe(Command.withDescription("Associate the latest test run with the current Claude Code session"));
157
+ const shaOpt = Options.text("sha");
158
+ const parentShaOpt = Options.optional(Options.text("parent-sha"));
159
+ const messageOpt = Options.optional(Options.text("message"));
160
+ const authorOpt = Options.optional(Options.text("author"));
161
+ const committedAtOpt = Options.optional(Options.text("committed-at"));
162
+ const branchOpt = Options.optional(Options.text("branch"));
163
+ const projectOpt = Options.optional(Options.text("project"));
164
+ const filesArg = Args.text({ name: "files-json" }).pipe(Args.withDescription("JSON array of {\"filePath\",\"changeKind\"} objects"));
165
+ const runWorkspaceChangesSubcommand = Command.make("run-workspace-changes", {
166
+ sha: shaOpt,
167
+ parentSha: parentShaOpt,
168
+ message: messageOpt,
169
+ author: authorOpt,
170
+ committedAt: committedAtOpt,
171
+ branch: branchOpt,
172
+ project: projectOpt,
173
+ files: filesArg
174
+ }, (opts) => Effect.gen(function* () {
175
+ const parsed = yield* Effect.try({
176
+ try: () => JSON.parse(opts.files),
177
+ catch: (e) => /* @__PURE__ */ new Error(`Invalid files-json: ${e instanceof Error ? e.message : String(e)}`)
178
+ });
179
+ return yield* recordRunWorkspaceChangesEffect({
180
+ sha: opts.sha,
181
+ ...opts.parentSha._tag === "Some" && { parentSha: opts.parentSha.value },
182
+ ...opts.message._tag === "Some" && { message: opts.message.value },
183
+ ...opts.author._tag === "Some" && { author: opts.author.value },
184
+ ...opts.committedAt._tag === "Some" && { committedAt: opts.committedAt.value },
185
+ ...opts.branch._tag === "Some" && { branch: opts.branch.value },
186
+ ...opts.project._tag === "Some" && { project: opts.project.value },
187
+ files: parsed
188
+ });
189
+ }).pipe(Effect.flatMap((result) => Effect.sync(() => process.stdout.write(`${JSON.stringify(result)}\n`))), Effect.catchAll((err) => Effect.sync(() => {
190
+ process.stderr.write(`record run-workspace-changes: ${err instanceof Error ? err.message : String(err)}\n`);
191
+ process.exit(1);
192
+ })))).pipe(Command.withDescription("Record a commit + its changed files (driven by post-commit hook)"));
193
+ const recordCommand = Command.make("record").pipe(Command.withSubcommands([
194
+ turnSubcommand,
195
+ sessionStartSubcommand,
196
+ sessionEndSubcommand,
197
+ tddArtifactSubcommand,
198
+ runWorkspaceChangesSubcommand,
199
+ runTriggerSubcommand,
200
+ testCaseTurnsSubcommand
201
+ ]), Command.withDescription("Hook write surface (Decision D3): turn, session-start, session-end, tdd-artifact, run-workspace-changes"));
202
+
203
+ //#endregion
204
+ export { recordCommand };
@@ -0,0 +1,40 @@
1
+ import { formatTriageEffect } from "@vitest-agent/sdk";
2
+ import { Effect } from "effect";
3
+ import { Command, Options } from "@effect/cli";
4
+
5
+ //#region src/commands/triage.ts
6
+ /**
7
+ * CLI triage command -- emits the W3 orientation triage brief.
8
+ *
9
+ * Calls the shared formatTriageEffect generator, which the MCP
10
+ * triage_brief tool also uses. The plugin's SessionStart hook runs
11
+ * this and pipes the result into Claude Code's additionalContext.
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+ const formatOption = Options.withDefault(Options.choice("format", [
16
+ "markdown",
17
+ "json",
18
+ "silent"
19
+ ]), "markdown");
20
+ const projectOption = Options.optional(Options.text("project"));
21
+ const maxLinesOption = Options.optional(Options.integer("max-lines"));
22
+ const triageCommand = Command.make("triage", {
23
+ format: formatOption,
24
+ project: projectOption,
25
+ maxLines: maxLinesOption
26
+ }, (opts) => Effect.gen(function* () {
27
+ const md = yield* formatTriageEffect({
28
+ ...opts.project._tag === "Some" && { project: opts.project.value },
29
+ ...opts.maxLines._tag === "Some" && { maxLines: opts.maxLines.value }
30
+ });
31
+ if (opts.format === "silent") return;
32
+ if (opts.format === "json") {
33
+ yield* Effect.sync(() => process.stdout.write(`${JSON.stringify({ triage: md })}\n`));
34
+ return;
35
+ }
36
+ yield* Effect.sync(() => process.stdout.write(md.length > 0 ? `${md}\n` : ""));
37
+ })).pipe(Command.withDescription("Emit the W3 orientation triage brief for SessionStart"));
38
+
39
+ //#endregion
40
+ export { triageCommand };
@@ -0,0 +1,48 @@
1
+ import { formatWrapupEffect } from "@vitest-agent/sdk";
2
+ import { Effect } from "effect";
3
+ import { Command, Options } from "@effect/cli";
4
+
5
+ //#region src/commands/wrapup.ts
6
+ /**
7
+ * CLI wrapup command -- emits the W5 wrap-up prompt for a session.
8
+ *
9
+ * Drives the four interpretive hooks (Stop / SessionEnd / PreCompact /
10
+ * UserPromptSubmit). Hooks invoke the bin with --kind set; humans on
11
+ * the terminal can also run it on demand with --chat-id (host chat UUID)
12
+ * or --row-id (internal integer FK, mostly for debugging).
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+ const rowIdOption = Options.optional(Options.integer("row-id"));
17
+ const chatIdOption = Options.optional(Options.text("chat-id"));
18
+ const kindOption = Options.withDefault(Options.choice("kind", [
19
+ "stop",
20
+ "session_end",
21
+ "pre_compact",
22
+ "tdd_handoff",
23
+ "user_prompt_nudge"
24
+ ]), "session_end");
25
+ const userPromptHintOption = Options.optional(Options.text("user-prompt-hint"));
26
+ const formatOption = Options.withDefault(Options.choice("format", ["markdown", "json"]), "markdown");
27
+ const wrapupCommand = Command.make("wrapup", {
28
+ rowId: rowIdOption,
29
+ chatId: chatIdOption,
30
+ kind: kindOption,
31
+ userPromptHint: userPromptHintOption,
32
+ format: formatOption
33
+ }, (opts) => Effect.gen(function* () {
34
+ const md = yield* formatWrapupEffect({
35
+ ...opts.rowId._tag === "Some" && { sessionId: opts.rowId.value },
36
+ ...opts.chatId._tag === "Some" && { chatId: opts.chatId.value },
37
+ kind: opts.kind,
38
+ ...opts.userPromptHint._tag === "Some" && { userPromptHint: opts.userPromptHint.value }
39
+ });
40
+ if (opts.format === "json") {
41
+ yield* Effect.sync(() => process.stdout.write(`${JSON.stringify({ wrapup: md })}\n`));
42
+ return;
43
+ }
44
+ yield* Effect.sync(() => process.stdout.write(md.length > 0 ? `${md}\n` : ""));
45
+ })).pipe(Command.withDescription("Emit the W5 wrap-up prompt for a session"));
46
+
47
+ //#endregion
48
+ export { wrapupCommand };
package/index.d.ts ADDED
@@ -0,0 +1,169 @@
1
+ import * as NodeContext from "@effect/platform-node/NodeContext";
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
+
6
+ //#region src/layers/CliLive.d.ts
7
+ /**
8
+ * Composition layer for the CLI runtime.
9
+ *
10
+ * Wires `DataReader`, `ProjectDiscovery`, `HistoryTracker`,
11
+ * `OutputPipeline`, `SqliteClient`, the DB migrator, `NodeContext`,
12
+ * `NodeFileSystem`, and `Logger` into a single layer the `vitest-agent`
13
+ * bin provides to `Command.run`.
14
+ *
15
+ * @param dbPath - absolute path to the per-project `data.db`
16
+ * @param logLevel - optional log level override; defaults to `Info`
17
+ * @param logFile - optional path for structured log output
18
+ * @public
19
+ */
20
+ declare const CliLive: (dbPath: string, logLevel?: LogLevel.LogLevel, logFile?: string) => Layer.Layer<import("@vitest-agent/sdk").DataStore | import("@vitest-agent/sdk").DataReader | import("@effect/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | NodeContext.NodeContext | import("@vitest-agent/sdk").ProjectDiscovery | import("@vitest-agent/sdk").HistoryTracker | import("@vitest-agent/sdk").DetailResolver | import("@vitest-agent/sdk").EnvironmentDetector | import("@vitest-agent/sdk").ExecutorResolver | import("@vitest-agent/sdk").FormatSelector | import("@vitest-agent/sdk").OutputRenderer, import("effect/ConfigError").ConfigError | import("@effect/sql/SqlError").SqlError | SqliteMigrator.MigrationError, never>;
21
+ //#endregion
22
+ //#region src/layers/SidecarLive.d.ts
23
+ /**
24
+ * SQLite database paths consumed by {@link SidecarLive}.
25
+ *
26
+ * @public
27
+ */
28
+ interface SidecarPaths {
29
+ /** Absolute path to the per-project `data.db`. */
30
+ readonly perProjectDbPath: string;
31
+ /** Absolute path to the per-client `sessions.db`. */
32
+ readonly sessionMapDbPath: string;
33
+ /** Absolute path to the global `registry.db`. */
34
+ readonly registryDbPath: string;
35
+ }
36
+ /**
37
+ * Build the sidecar Live layer for the supplied SQLite paths.
38
+ *
39
+ * Each store gets its own `SqlClient` connection (separate scopes,
40
+ * independent migrators) so concurrent operations on the three
41
+ * stores don't share lock state.
42
+ *
43
+ * @param paths - the three SQLite database paths to open
44
+ * @public
45
+ */
46
+ declare const SidecarLive: (paths: SidecarPaths) => Layer.Layer<import("@vitest-agent/sdk").DataStore | import("@vitest-agent/sdk").DataReader | import("@vitest-agent/sdk").PerClientSessionMapWriter | import("@vitest-agent/sdk").RunContext$ | NodeContext.NodeContext | import("@vitest-agent/sdk").PerClientSessionMapReader | import("@vitest-agent/sdk").DiscoveryRegistry, import("effect/ConfigError").ConfigError | import("@effect/sql/SqlError").SqlError | SqliteMigrator.MigrationError, never>;
47
+ //#endregion
48
+ //#region src/lib/internal-register-agent.d.ts
49
+ /**
50
+ * Input for the end-to-end agent registration effect.
51
+ *
52
+ * @public
53
+ */
54
+ interface RegisterAgentInput {
55
+ /** The host's native session identifier (e.g. Claude's `session_id`). */
56
+ readonly hostSessionId: string;
57
+ /** Absolute path to the host's conversation transcript file. */
58
+ readonly transcriptPath: string;
59
+ /** Working directory of the agent process. */
60
+ readonly cwd: string;
61
+ /** Host kind string (e.g. `"claude-code"`). */
62
+ readonly hostKind: string;
63
+ /** Agent type label (e.g. `"main"` or `"subagent"`). */
64
+ readonly agentType: string;
65
+ /** Normalized project key derived from the workspace root `package.json#name`. */
66
+ readonly projectKey: string;
67
+ /** Agent ID of the parent when registering a subagent. */
68
+ readonly parentAgentId?: string;
69
+ /** Optional idempotency nonce; derived from `hostSessionId + agentType + parentAgentId` when omitted. */
70
+ readonly clientNonce?: string;
71
+ }
72
+ /**
73
+ * Output of the end-to-end agent registration effect.
74
+ *
75
+ * @public
76
+ */
77
+ interface RegisterAgentOutput {
78
+ /** Canonical UUID assigned to this agent in the per-project store. */
79
+ readonly agentId: string;
80
+ /** UUID of the conversation this agent belongs to. */
81
+ readonly conversationId: string;
82
+ /** Idempotency key used for the `registerAgent` upsert. */
83
+ readonly idempotencyKey: string;
84
+ /** `true` when an existing agent row was recovered rather than inserted. */
85
+ readonly idempotencyHit: boolean;
86
+ }
87
+ /**
88
+ * End-to-end agent registration.
89
+ *
90
+ * Returns the canonical `agentId` + the conversation it belongs to +
91
+ * the resolved `idempotencyKey`. The `idempotencyHit` flag is `true`
92
+ * when the per-project store recovered an existing agent row instead
93
+ * of inserting a new one.
94
+ *
95
+ * @param input - registration inputs including host identifiers and agent metadata
96
+ * @returns an Effect resolving to `RegisterAgentOutput`
97
+ * @public
98
+ */
99
+ declare const registerAgentEffect: (input: RegisterAgentInput) => Effect.Effect<{
100
+ agentId: string;
101
+ conversationId: string;
102
+ mainAgentId: string;
103
+ idempotencyKey: string;
104
+ idempotencyHit: boolean;
105
+ }, import("@vitest-agent/sdk").DataStoreError | import("@vitest-agent/sdk").RegistrationConflictError, DataStore | DataReader | PerClientSessionMapWriter | import("@vitest-agent/sdk").RunContext$>;
106
+ //#endregion
107
+ //#region src/lib/sidecar-paths.d.ts
108
+ /**
109
+ * Filename of the per-project test-data SQLite database.
110
+ *
111
+ * @public
112
+ */
113
+ declare const DATA_DB_FILENAME = "data.db";
114
+ /**
115
+ * Filename of the per-client session-map SQLite database.
116
+ *
117
+ * @public
118
+ */
119
+ declare const SESSIONS_DB_FILENAME = "sessions.db";
120
+ /**
121
+ * Filename of the global discovery-registry SQLite database.
122
+ *
123
+ * @public
124
+ */
125
+ declare const REGISTRY_DB_FILENAME = "registry.db";
126
+ /**
127
+ * Resolve (and create) the per-project data directory for the supplied
128
+ * normalized `projectKey`. Returns the directory; callers join
129
+ * {@link DATA_DB_FILENAME} onto it for the `data.db` path.
130
+ *
131
+ * @param projectKey - normalized project key (e.g. `@org__pkg`)
132
+ * @returns absolute path to the resolved (and created) data directory
133
+ * @public
134
+ */
135
+ declare const resolveProjectDataDir: (projectKey: string) => string;
136
+ /**
137
+ * Resolve (and create) the directory holding the global `registry.db`.
138
+ * Callers join {@link REGISTRY_DB_FILENAME} onto it.
139
+ *
140
+ * @returns absolute path to the resolved (and created) registry directory
141
+ * @public
142
+ */
143
+ declare const resolveRegistryDir: () => string;
144
+ /**
145
+ * Resolve the per-client `sessions.db` path.
146
+ *
147
+ * Precedence: the `CLAUDE_PLUGIN_DATA` env var, then
148
+ * `VITEST_AGENT_SESSION_MAP_DIR`, then `~/.vitest-agent/`. Fails
149
+ * with `ProjectIdentityNotResolvableError` when no home directory
150
+ * is resolvable.
151
+ *
152
+ * @returns an Effect resolving to the absolute `sessions.db` path
153
+ * @public
154
+ */
155
+ declare const resolveSessionMapPath: () => Effect.Effect<string, ProjectIdentityNotResolvableError>;
156
+ //#endregion
157
+ //#region src/index.d.ts
158
+ /**
159
+ * The version of this package, inlined at build time from
160
+ * `package.json#version` via rslib-builder's `__PACKAGE_VERSION__` substitution.
161
+ * Compared against `CURRENT_SDK_VERSION` at CLI bin init to surface
162
+ * partially-upgraded installs as a single stderr warning.
163
+ *
164
+ * @public
165
+ */
166
+ declare const CURRENT_CLI_VERSION: string;
167
+ //#endregion
168
+ export { CURRENT_CLI_VERSION, CliLive, DATA_DB_FILENAME, REGISTRY_DB_FILENAME, type RegisterAgentInput, type RegisterAgentOutput, SESSIONS_DB_FILENAME, SidecarLive, type SidecarPaths, registerAgentEffect, resolveProjectDataDir, resolveRegistryDir, resolveSessionMapPath };
169
+ //# sourceMappingURL=index.d.ts.map
package/index.js ADDED
@@ -0,0 +1,18 @@
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";
5
+
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
+ * Compared against `CURRENT_SDK_VERSION` at CLI bin init to surface
11
+ * partially-upgraded installs as a single stderr warning.
12
+ *
13
+ * @public
14
+ */
15
+ const CURRENT_CLI_VERSION = "1.0.0";
16
+
17
+ //#endregion
18
+ export { CURRENT_CLI_VERSION, CliLive, DATA_DB_FILENAME, REGISTRY_DB_FILENAME, SESSIONS_DB_FILENAME, SidecarLive, registerAgentEffect, resolveProjectDataDir, resolveRegistryDir, resolveSessionMapPath };
@@ -0,0 +1,30 @@
1
+ import { NodeFileSystem } from "@effect/platform-node";
2
+ import * as NodeContext$1 from "@effect/platform-node/NodeContext";
3
+ import { layer } from "@effect/sql-sqlite-node/SqliteClient";
4
+ import * as SqliteMigrator from "@effect/sql-sqlite-node/SqliteMigrator";
5
+ import { DataReaderLive, DataStoreLive, HistoryTrackerLive, LoggerLive, OutputPipelineLive, ProjectDiscoveryLive, migration0001 } from "@vitest-agent/sdk";
6
+ import { Layer } from "effect";
7
+
8
+ //#region src/layers/CliLive.ts
9
+ /**
10
+ * Composition layer for the CLI runtime.
11
+ *
12
+ * Wires `DataReader`, `ProjectDiscovery`, `HistoryTracker`,
13
+ * `OutputPipeline`, `SqliteClient`, the DB migrator, `NodeContext`,
14
+ * `NodeFileSystem`, and `Logger` into a single layer the `vitest-agent`
15
+ * bin provides to `Command.run`.
16
+ *
17
+ * @param dbPath - absolute path to the per-project `data.db`
18
+ * @param logLevel - optional log level override; defaults to `Info`
19
+ * @param logFile - optional path for structured log output
20
+ * @public
21
+ */
22
+ const CliLive = (dbPath, logLevel, logFile) => {
23
+ const SqliteLayer = layer({ filename: dbPath });
24
+ const PlatformLayer = NodeContext$1.layer;
25
+ const MigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord({ "0001_initial": migration0001 }) }).pipe(Layer.provide(Layer.merge(SqliteLayer, PlatformLayer)));
26
+ return Layer.mergeAll(ProjectDiscoveryLive, HistoryTrackerLive, OutputPipelineLive).pipe(Layer.provideMerge(DataReaderLive), Layer.provideMerge(DataStoreLive), Layer.provideMerge(MigratorLayer), Layer.provideMerge(SqliteLayer), Layer.provideMerge(PlatformLayer), Layer.provideMerge(NodeFileSystem.layer), Layer.provideMerge(LoggerLive(logLevel, logFile)));
27
+ };
28
+
29
+ //#endregion
30
+ export { CliLive };
@@ -0,0 +1,34 @@
1
+ import { NodeFileSystem } from "@effect/platform-node";
2
+ import * as NodeContext$1 from "@effect/platform-node/NodeContext";
3
+ import { layer } from "@effect/sql-sqlite-node/SqliteClient";
4
+ import * as SqliteMigrator from "@effect/sql-sqlite-node/SqliteMigrator";
5
+ import { DataReaderLive, DataStoreLive, DiscoveryRegistryLive, LoggerLive, PerClientSessionMapWriterLive, RunContextLive, migration0001, registryMigration0001, sessionMapMigration0001 } from "@vitest-agent/sdk";
6
+ import { Layer } from "effect";
7
+
8
+ //#region src/layers/SidecarLive.ts
9
+ /**
10
+ * Build the sidecar Live layer for the supplied SQLite paths.
11
+ *
12
+ * Each store gets its own `SqlClient` connection (separate scopes,
13
+ * independent migrators) so concurrent operations on the three
14
+ * stores don't share lock state.
15
+ *
16
+ * @param paths - the three SQLite database paths to open
17
+ * @public
18
+ */
19
+ const SidecarLive = (paths) => {
20
+ const PlatformLayer = NodeContext$1.layer;
21
+ const ProjectSqliteLayer = layer({ filename: paths.perProjectDbPath });
22
+ const ProjectMigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord({ "0001_initial": migration0001 }) }).pipe(Layer.provide(Layer.merge(ProjectSqliteLayer, PlatformLayer)));
23
+ const ProjectStoreLayer = Layer.mergeAll(DataStoreLive.pipe(Layer.provide(ProjectSqliteLayer)), DataReaderLive.pipe(Layer.provide(ProjectSqliteLayer)), ProjectMigratorLayer);
24
+ const SessionMapSqliteLayer = layer({ filename: paths.sessionMapDbPath });
25
+ const SessionMapMigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord({ "0001_initial": sessionMapMigration0001 }) }).pipe(Layer.provide(Layer.merge(SessionMapSqliteLayer, PlatformLayer)));
26
+ const SessionMapLayer = Layer.mergeAll(PerClientSessionMapWriterLive.pipe(Layer.provide(SessionMapSqliteLayer)), SessionMapMigratorLayer);
27
+ const RegistrySqliteLayer = layer({ filename: paths.registryDbPath });
28
+ const RegistryMigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord({ "0001_initial": registryMigration0001 }) }).pipe(Layer.provide(Layer.merge(RegistrySqliteLayer, PlatformLayer)));
29
+ const RegistryLayer = Layer.mergeAll(DiscoveryRegistryLive.pipe(Layer.provide(RegistrySqliteLayer)), RegistryMigratorLayer);
30
+ return Layer.mergeAll(ProjectStoreLayer, SessionMapLayer, RegistryLayer, RunContextLive).pipe(Layer.provideMerge(PlatformLayer), Layer.provideMerge(NodeFileSystem.layer), Layer.provideMerge(LoggerLive()));
31
+ };
32
+
33
+ //#endregion
34
+ export { SidecarLive };
@@ -0,0 +1,26 @@
1
+ //#region src/lib/format-db-query.ts
2
+ const renderCell = (value) => {
3
+ if (value === null || value === void 0) return "NULL";
4
+ if (typeof value === "object") return JSON.stringify(value);
5
+ return String(value);
6
+ };
7
+ /**
8
+ * Render query rows as whitespace-padded tabular text or a JSON array.
9
+ *
10
+ * Table format: column headers in the first line, one row per line,
11
+ * each cell padded to its column's widest value. An empty result set
12
+ * renders as `(0 rows)`. JSON format emits a single array of row
13
+ * objects keyed by column name; an empty result set is `[]`.
14
+ */
15
+ const formatDbQuery = (rows, format) => {
16
+ if (format === "json") return JSON.stringify(rows);
17
+ if (rows.length === 0) return "(0 rows)";
18
+ const columns = Object.keys(rows[0]);
19
+ const cells = rows.map((row) => columns.map((col) => renderCell(row[col])));
20
+ const widths = columns.map((col, i) => Math.max(col.length, ...cells.map((row) => row[i].length)));
21
+ const renderLine = (values) => values.map((value, i) => value.padEnd(widths[i])).join(" ").trimEnd();
22
+ return [renderLine(columns), ...cells.map(renderLine)].join("\n");
23
+ };
24
+
25
+ //#endregion
26
+ export { formatDbQuery };
@@ -0,0 +1,14 @@
1
+ //#region src/lib/format-doctor.ts
2
+ function formatDoctor(results) {
3
+ const lines = [];
4
+ lines.push("## Doctor\n");
5
+ for (const result of results) {
6
+ const icon = result.passed ? "[x]" : "[ ]";
7
+ lines.push(`- ${icon} ${result.name}: ${result.detail}`);
8
+ }
9
+ if (results.some((r) => !r.passed)) lines.push("\nSuggestion: Run `vitest-agent db reset` then re-run tests.");
10
+ return lines.join("\n");
11
+ }
12
+
13
+ //#endregion
14
+ export { formatDoctor };
@@ -0,0 +1,27 @@
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 };