@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.
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/bin/vitest-agent.js +39 -0
- package/commands/agent.js +159 -0
- package/commands/db.js +123 -0
- package/commands/doctor.js +111 -0
- package/commands/record.js +204 -0
- package/commands/triage.js +40 -0
- package/commands/wrapup.js +48 -0
- package/index.d.ts +169 -0
- package/index.js +18 -0
- package/layers/CliLive.js +30 -0
- package/layers/SidecarLive.js +34 -0
- package/lib/format-db-query.js +26 -0
- package/lib/format-doctor.js +14 -0
- package/lib/internal-end-agent.js +27 -0
- package/lib/internal-register-agent.js +76 -0
- package/lib/record-session.js +33 -0
- package/lib/record-tdd-artifact.js +40 -0
- package/lib/record-turn.js +44 -0
- package/lib/record-workspace-changes.js +64 -0
- package/lib/resolve-session-for-recording.js +71 -0
- package/lib/sidecar-paths.js +101 -0
- package/package.json +54 -0
- package/tsdoc-metadata.json +11 -0
|
@@ -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 };
|