@vitest-agent/mcp 3.0.3 → 4.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/README.md +7 -6
- package/annotations.js +18 -0
- package/bin/vitest-agent-mcp.js +5 -140
- package/{middleware/idempotency.js → idempotency.js} +48 -49
- package/index.d.ts +5610 -1549
- package/index.js +35 -17
- package/main.d.ts +31 -0
- package/main.js +144 -0
- package/package.json +9 -6
- package/prompts/layer.js +108 -0
- package/register-toolkit.js +311 -0
- package/server.js +24 -894
- package/{context.js → session.js} +38 -22
- package/toolkit.js +86 -0
- package/tools/acceptance-metrics.js +26 -14
- package/tools/cache-health.js +28 -17
- package/tools/commit-changes.js +33 -10
- package/tools/configure.js +33 -10
- package/tools/coverage.js +34 -5
- package/tools/errors.js +36 -26
- package/tools/failure-signature-get.js +33 -10
- package/tools/file-coverage.js +37 -13
- package/tools/help.js +45 -4
- package/tools/history.js +39 -19
- package/tools/hypothesis.js +118 -102
- package/tools/inventory.js +149 -146
- package/tools/note.js +131 -113
- package/tools/overview.js +33 -10
- package/tools/ping.js +22 -10
- package/tools/register-agent.js +88 -62
- package/tools/run-tests.js +76 -23
- package/tools/settings-list.js +27 -10
- package/tools/status.js +35 -10
- package/tools/tdd-artifact.js +37 -22
- package/tools/tdd-behavior.js +120 -108
- package/tools/tdd-goal.js +98 -85
- package/tools/tdd-phase-transition-request.js +201 -166
- package/tools/tdd-progress-push.js +102 -0
- package/tools/tdd-task.js +140 -138
- package/tools/test.js +152 -138
- package/tools/trends.js +37 -18
- package/tools/triage-brief.js +34 -15
- package/tools/turn-search.js +39 -16
- package/tools/wrapup-prompt.js +37 -17
- package/utils/crash-guards.js +0 -22
- package/utils/replay-marker.js +12 -0
- package/utils/safe-format-fatal-error.js +0 -16
- package/utils/tool-error-envelope.js +3 -3
- package/version.js +13 -0
- package/layers/McpLive.js +0 -29
- package/prompts/index.js +0 -89
- package/router.js +0 -74
- package/session-env.js +0 -112
- package/utils/effect-to-zod.js +0 -158
package/utils/crash-guards.js
CHANGED
|
@@ -1,27 +1,5 @@
|
|
|
1
1
|
//#region src/utils/crash-guards.ts
|
|
2
2
|
/**
|
|
3
|
-
* Pure decision logic backing `bin.ts`'s `process.on("uncaughtException")`
|
|
4
|
-
* guard (issue #191, sub-item A).
|
|
5
|
-
*
|
|
6
|
-
* Node's own guidance for `uncaughtException` is "do not resume normal
|
|
7
|
-
* operation" because arbitrary in-process state may be corrupt. This
|
|
8
|
-
* package accepts that residual risk *after* the stdio transport is
|
|
9
|
-
* connected: it holds no long-lived mutable state outside SQLite
|
|
10
|
-
* itself (every `DataStore`/`DataReader` call is a self-contained
|
|
11
|
-
* transaction via the shared `ManagedRuntime`), so a synchronous throw
|
|
12
|
-
* that escapes even the MCP SDK's own per-tool-call try/catch cannot
|
|
13
|
-
* leave this process's own bookkeeping half-mutated in a way that
|
|
14
|
-
* would corrupt the *next* call — and the alternative, silent process
|
|
15
|
-
* death mid-TDD-session (deregistering every tool from the client), is
|
|
16
|
-
* strictly worse for a long-running dev tool.
|
|
17
|
-
*
|
|
18
|
-
* Before the transport connects there is no client session to
|
|
19
|
-
* preserve by staying alive, so failing fast and loud is the better
|
|
20
|
-
* default rather than spinning forever in a half-initialized state.
|
|
21
|
-
*
|
|
22
|
-
* @packageDocumentation
|
|
23
|
-
*/
|
|
24
|
-
/**
|
|
25
3
|
* @param transportConnected - whether `server.connect(transport)` has
|
|
26
4
|
* already resolved for this process
|
|
27
5
|
* @returns `true` when the process should exit rather than continue
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/replay-marker.ts
|
|
4
|
+
/**
|
|
5
|
+
* Fields to spread into a replayable success struct.
|
|
6
|
+
*
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
const IdempotentReplayMarker = { _idempotentReplay: Schema.optionalKey(Schema.Literal(true)).annotate({ description: "Present (true) when this response was replayed from the idempotency cache instead of re-running the mutation." }) };
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { IdempotentReplayMarker };
|
|
@@ -2,22 +2,6 @@ import { formatFatalError } from "@vitest-agent/sdk";
|
|
|
2
2
|
|
|
3
3
|
//#region src/utils/safe-format-fatal-error.ts
|
|
4
4
|
/**
|
|
5
|
-
* Crash-handler-safe wrapper around `@vitest-agent/sdk`'s
|
|
6
|
-
* {@link formatFatalError}.
|
|
7
|
-
*
|
|
8
|
-
* `bin.ts`'s `unhandledRejection` / `uncaughtException` handlers exist so
|
|
9
|
-
* a stray throw cannot kill a live MCP session (issue #191). Calling the
|
|
10
|
-
* formatter directly from inside them reopened exactly that hole:
|
|
11
|
-
* `formatFatalError` introspects the value it is given — `Symbol.for(...)
|
|
12
|
-
* in reason`, `err instanceof Error`, `JSON.stringify(err)` — and every
|
|
13
|
-
* one of those is hijackable by a `Proxy` whose `has` / `getPrototypeOf`
|
|
14
|
-
* / `get` trap throws. A throw *inside* an `uncaughtException` handler is
|
|
15
|
-
* fatal to the process with no second chance to report it, so the crash
|
|
16
|
-
* guard would have crashed the process it exists to protect (issue #243).
|
|
17
|
-
*
|
|
18
|
-
* @packageDocumentation
|
|
19
|
-
*/
|
|
20
|
-
/**
|
|
21
5
|
* Fixed fallback emitted when the formatter itself throws. Deliberately a
|
|
22
6
|
* constant: anything derived from the offending value could throw again
|
|
23
7
|
* on the recovery path.
|
|
@@ -35,9 +35,9 @@ function coerceThrownMessage(err) {
|
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* Builds the structured envelope a tool's catch-all wrapper returns
|
|
38
|
-
* when its
|
|
38
|
+
* when its handler throws unexpectedly.
|
|
39
39
|
*
|
|
40
|
-
* @param toolName - the MCP tool name under which the
|
|
40
|
+
* @param toolName - the MCP tool name under which the handler was registered
|
|
41
41
|
* @param err - the value thrown or the rejection reason
|
|
42
42
|
* @public
|
|
43
43
|
*/
|
|
@@ -51,7 +51,7 @@ function buildUnexpectedToolErrorEnvelope(toolName, err) {
|
|
|
51
51
|
remediation: {
|
|
52
52
|
suggestedTool: toolName,
|
|
53
53
|
suggestedArgs: {},
|
|
54
|
-
humanHint: `The "${toolName}" tool's
|
|
54
|
+
humanHint: `The "${toolName}" tool's handler threw before producing a result (unrelated to your input in most cases). Retry the call; if it persists, check the MCP server's stderr for the logged error.`
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
};
|
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 the bundler's `__PACKAGE_VERSION__` substitution.
|
|
5
|
+
* Exported for version introspection by downstream tooling, and consumed by
|
|
6
|
+
* `main.ts` to back the advertised `serverInfo.version`.
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
const CURRENT_MCP_VERSION = "4.0.0";
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { CURRENT_MCP_VERSION };
|
package/layers/McpLive.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { Layer } from "effect";
|
|
2
|
-
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
3
|
-
import { layer } from "@effect/sql-sqlite-node/SqliteClient";
|
|
4
|
-
import * as SqliteMigrator from "@effect/sql-sqlite-node/SqliteMigrator";
|
|
5
|
-
import { DataReaderLive, DataStoreLive, LoggerLive, OutputPipelineLive, PROJECT_MIGRATIONS, ProjectDiscoveryLive } from "@vitest-agent/sdk";
|
|
6
|
-
|
|
7
|
-
//#region src/layers/McpLive.ts
|
|
8
|
-
/**
|
|
9
|
-
* Builds the Effect Layer that provides all services required by the MCP server.
|
|
10
|
-
*
|
|
11
|
-
* Composes DataReader, DataStore, ProjectDiscovery, OutputPipeline, SQLite
|
|
12
|
-
* client, migrator, NodeServices, and the logger into a single
|
|
13
|
-
* layer suitable for `ManagedRuntime.make`.
|
|
14
|
-
*
|
|
15
|
-
* @param dbPath - absolute path to the SQLite database file
|
|
16
|
-
* @param logLevel - optional log level; defaults to the logger's own default
|
|
17
|
-
* @param logFile - optional path to write structured log output
|
|
18
|
-
* @returns an Effect Layer providing all MCP runtime services
|
|
19
|
-
* @public
|
|
20
|
-
*/
|
|
21
|
-
const McpLive = (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(DataReaderLive, DataStoreLive, ProjectDiscoveryLive, OutputPipelineLive).pipe(Layer.provideMerge(MigratorLayer), Layer.provideMerge(SqliteLayer), Layer.provideMerge(PlatformLayer), Layer.provideMerge(LoggerLive(logLevel, logFile)));
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
//#endregion
|
|
29
|
-
export { McpLive };
|
package/prompts/index.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { explainFailurePrompt } from "./explain-failure.js";
|
|
2
|
-
import { regressionSincePassPrompt } from "./regression-since-pass.js";
|
|
3
|
-
import { tddResumePrompt } from "./tdd-resume.js";
|
|
4
|
-
import { triagePrompt } from "./triage.js";
|
|
5
|
-
import { whyFlakyPrompt } from "./why-flaky.js";
|
|
6
|
-
import { wrapupPrompt } from "./wrapup.js";
|
|
7
|
-
import { z } from "zod";
|
|
8
|
-
|
|
9
|
-
//#region src/prompts/index.ts
|
|
10
|
-
function toMessages(messages) {
|
|
11
|
-
return messages.map((m) => ({
|
|
12
|
-
role: m.role,
|
|
13
|
-
content: {
|
|
14
|
-
type: "text",
|
|
15
|
-
text: m.content.text
|
|
16
|
-
}
|
|
17
|
-
}));
|
|
18
|
-
}
|
|
19
|
-
function registerAllPrompts(server) {
|
|
20
|
-
server.registerPrompt("triage", {
|
|
21
|
-
title: "Triage Recent Failures",
|
|
22
|
-
description: "Orient toward a triage workflow over the most recent test run; compose triage_brief, failure_signature_get, hypothesis_record.",
|
|
23
|
-
argsSchema: { project: z.optional(z.string()).describe("Filter to a specific project") }
|
|
24
|
-
}, (args) => {
|
|
25
|
-
return { messages: toMessages(triagePrompt(args.project !== void 0 ? { project: args.project } : {}).messages) };
|
|
26
|
-
});
|
|
27
|
-
server.registerPrompt("why-flaky", {
|
|
28
|
-
title: "Diagnose a Flaky Test",
|
|
29
|
-
description: "Diagnose why a named test is flaky; compose test_history and failure_signature_get with timing/shared-state framing.",
|
|
30
|
-
argsSchema: {
|
|
31
|
-
test: z.string().describe("Full hierarchical test name (e.g. 'Suite > nested > test')"),
|
|
32
|
-
project: z.optional(z.string()).describe("Filter to a specific project")
|
|
33
|
-
}
|
|
34
|
-
}, (args) => {
|
|
35
|
-
return { messages: toMessages(whyFlakyPrompt(args.project !== void 0 ? {
|
|
36
|
-
test: args.test,
|
|
37
|
-
project: args.project
|
|
38
|
-
} : { test: args.test }).messages) };
|
|
39
|
-
});
|
|
40
|
-
server.registerPrompt("regression-since-pass", {
|
|
41
|
-
title: "Find What Broke a Test",
|
|
42
|
-
description: "Walk back from the test's most recent passing run to identify the change that broke it; compose test_history, commit_changes, turn_search.",
|
|
43
|
-
argsSchema: {
|
|
44
|
-
test: z.string().describe("Full hierarchical test name"),
|
|
45
|
-
project: z.optional(z.string()).describe("Filter to a specific project")
|
|
46
|
-
}
|
|
47
|
-
}, (args) => {
|
|
48
|
-
return { messages: toMessages(regressionSincePassPrompt(args.project !== void 0 ? {
|
|
49
|
-
test: args.test,
|
|
50
|
-
project: args.project
|
|
51
|
-
} : { test: args.test }).messages) };
|
|
52
|
-
});
|
|
53
|
-
server.registerPrompt("explain-failure", {
|
|
54
|
-
title: "Explain a Failure Class",
|
|
55
|
-
description: "Synthesize a root-cause explanation from the recurrence history of a failure signature.",
|
|
56
|
-
argsSchema: { signature: z.string().describe("16-char failure signature hex") }
|
|
57
|
-
}, (args) => {
|
|
58
|
-
return { messages: toMessages(explainFailurePrompt({ signature: args.signature }).messages) };
|
|
59
|
-
});
|
|
60
|
-
server.registerPrompt("tdd-resume", {
|
|
61
|
-
title: "Resume TDD Work",
|
|
62
|
-
description: "Resume the active TDD task from its current phase; iron-law reminder for evidence-bound transitions.",
|
|
63
|
-
argsSchema: { sessionId: z.optional(z.string()).describe("Host session id (defaults to MCP server's recovered SessionContext)") }
|
|
64
|
-
}, (args) => {
|
|
65
|
-
return { messages: toMessages(tddResumePrompt(args.sessionId !== void 0 ? { sessionId: args.sessionId } : {}).messages) };
|
|
66
|
-
});
|
|
67
|
-
server.registerPrompt("wrapup", {
|
|
68
|
-
title: "Generate a Session Wrapup",
|
|
69
|
-
description: "Surface the same wrapup content the post-hooks emit automatically.",
|
|
70
|
-
argsSchema: {
|
|
71
|
-
kind: z.optional(z.enum([
|
|
72
|
-
"stop",
|
|
73
|
-
"session_end",
|
|
74
|
-
"pre_compact",
|
|
75
|
-
"tdd_handoff",
|
|
76
|
-
"user_prompt_nudge"
|
|
77
|
-
])).describe("Wrapup variant (default: user_prompt_nudge)"),
|
|
78
|
-
since: z.optional(z.string()).describe("ISO 8601 timestamp lower bound for activity to summarize")
|
|
79
|
-
}
|
|
80
|
-
}, (args) => {
|
|
81
|
-
const wrapupArgs = {};
|
|
82
|
-
if (args.kind !== void 0) wrapupArgs.kind = args.kind;
|
|
83
|
-
if (args.since !== void 0) wrapupArgs.since = args.since;
|
|
84
|
-
return { messages: toMessages(wrapupPrompt(wrapupArgs).messages) };
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
//#endregion
|
|
89
|
-
export { registerAllPrompts };
|
package/router.js
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { router } from "./context.js";
|
|
2
|
-
import { acceptanceMetrics } from "./tools/acceptance-metrics.js";
|
|
3
|
-
import { cacheHealth } from "./tools/cache-health.js";
|
|
4
|
-
import { commitChanges } from "./tools/commit-changes.js";
|
|
5
|
-
import { configure } from "./tools/configure.js";
|
|
6
|
-
import { testCoverage } from "./tools/coverage.js";
|
|
7
|
-
import { testErrors } from "./tools/errors.js";
|
|
8
|
-
import { failureSignatureGet } from "./tools/failure-signature-get.js";
|
|
9
|
-
import { fileCoverage } from "./tools/file-coverage.js";
|
|
10
|
-
import { help } from "./tools/help.js";
|
|
11
|
-
import { testHistory } from "./tools/history.js";
|
|
12
|
-
import { hypothesis } from "./tools/hypothesis.js";
|
|
13
|
-
import { inventory } from "./tools/inventory.js";
|
|
14
|
-
import { note } from "./tools/note.js";
|
|
15
|
-
import { testOverview } from "./tools/overview.js";
|
|
16
|
-
import { ping } from "./tools/ping.js";
|
|
17
|
-
import { registerAgent } from "./tools/register-agent.js";
|
|
18
|
-
import { runTests } from "./tools/run-tests.js";
|
|
19
|
-
import { settingsList } from "./tools/settings-list.js";
|
|
20
|
-
import { testStatus } from "./tools/status.js";
|
|
21
|
-
import { tddArtifactList } from "./tools/tdd-artifact.js";
|
|
22
|
-
import { tddBehavior } from "./tools/tdd-behavior.js";
|
|
23
|
-
import { tddGoal } from "./tools/tdd-goal.js";
|
|
24
|
-
import { tddPhaseTransitionRequest } from "./tools/tdd-phase-transition-request.js";
|
|
25
|
-
import { tddTask } from "./tools/tdd-task.js";
|
|
26
|
-
import { test } from "./tools/test.js";
|
|
27
|
-
import { testTrends } from "./tools/trends.js";
|
|
28
|
-
import { triageBrief } from "./tools/triage-brief.js";
|
|
29
|
-
import { turnSearch } from "./tools/turn-search.js";
|
|
30
|
-
import { wrapupPrompt } from "./tools/wrapup-prompt.js";
|
|
31
|
-
|
|
32
|
-
//#region src/router.ts
|
|
33
|
-
/**
|
|
34
|
-
* The tRPC router aggregating all MCP tool procedures.
|
|
35
|
-
*
|
|
36
|
-
* Pass to `createCallerFactory` in tests, or to `createCallerFactory(appRouter)`
|
|
37
|
-
* followed by `startMcpServer` in the bin entry to start the MCP server.
|
|
38
|
-
*
|
|
39
|
-
* @public
|
|
40
|
-
*/
|
|
41
|
-
const appRouter = router({
|
|
42
|
-
help,
|
|
43
|
-
test_status: testStatus,
|
|
44
|
-
test_overview: testOverview,
|
|
45
|
-
test_coverage: testCoverage,
|
|
46
|
-
test_history: testHistory,
|
|
47
|
-
test_trends: testTrends,
|
|
48
|
-
test_errors: testErrors,
|
|
49
|
-
test,
|
|
50
|
-
file_coverage: fileCoverage,
|
|
51
|
-
run_tests: runTests,
|
|
52
|
-
register_agent: registerAgent,
|
|
53
|
-
cache_health: cacheHealth,
|
|
54
|
-
configure,
|
|
55
|
-
inventory,
|
|
56
|
-
settings_list: settingsList,
|
|
57
|
-
note,
|
|
58
|
-
turn_search: turnSearch,
|
|
59
|
-
failure_signature_get: failureSignatureGet,
|
|
60
|
-
tdd_task: tddTask,
|
|
61
|
-
tdd_phase_transition_request: tddPhaseTransitionRequest,
|
|
62
|
-
tdd_goal: tddGoal,
|
|
63
|
-
tdd_behavior: tddBehavior,
|
|
64
|
-
tdd_artifact_list: tddArtifactList,
|
|
65
|
-
hypothesis,
|
|
66
|
-
acceptance_metrics: acceptanceMetrics,
|
|
67
|
-
triage_brief: triageBrief,
|
|
68
|
-
wrapup_prompt: wrapupPrompt,
|
|
69
|
-
commit_changes: commitChanges,
|
|
70
|
-
ping
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
//#endregion
|
|
74
|
-
export { appRouter };
|
package/session-env.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
4
|
-
|
|
5
|
-
//#region src/session-env.ts
|
|
6
|
-
/**
|
|
7
|
-
* Call-time SessionContext recovery from the per-session env files the
|
|
8
|
-
* plugin's SessionStart hook writes to `~/.claude/session-env/`.
|
|
9
|
-
*
|
|
10
|
-
* The boot-time env recovery (`sessionContextFromEnv`) depends on Claude
|
|
11
|
-
* Code auto-sourcing `CLAUDE_ENV_FILE` into the MCP child — which loses
|
|
12
|
-
* two races:
|
|
13
|
-
*
|
|
14
|
-
* 1. **Boot race.** On a fresh Claude Code launch the MCP child can spawn
|
|
15
|
-
* before the SessionStart hook has written `CLAUDE_ENV_FILE`, so the
|
|
16
|
-
* child's `process.env` never carries the canonical UUIDs (observed
|
|
17
|
-
* live: MCP spawn at 00:41:50, env file written 00:41:51).
|
|
18
|
-
* 2. **`/reload-plugins`.** A plugin reload restarts the MCP server
|
|
19
|
-
* mid-session with a fresh environment that has no session exports.
|
|
20
|
-
*
|
|
21
|
-
* In both cases the SessionStart hook has (or will have) written the same
|
|
22
|
-
* exports to a second, known-name surface:
|
|
23
|
-
* `~/.claude/session-env/<chat_id>/vitest-agent-hook.sh`. This module
|
|
24
|
-
* reads that surface directly, so a null boot context can be recovered
|
|
25
|
-
* lazily at the first tool call that needs it.
|
|
26
|
-
*
|
|
27
|
-
* Selection rule: among all session dirs whose exports name this server's
|
|
28
|
-
* `projectDir`, the newest-mtime file wins — the most recently started
|
|
29
|
-
* session for this project. With two live Claude Code windows on the same
|
|
30
|
-
* project this can name the other window's session; that ambiguity is
|
|
31
|
-
* inherent to a per-project (not per-process) surface and is accepted —
|
|
32
|
-
* the pre-existing alternative was no attribution at all.
|
|
33
|
-
*
|
|
34
|
-
* @packageDocumentation
|
|
35
|
-
*/
|
|
36
|
-
const EXPORT_LINE = /^export ([A-Z_][A-Z0-9_]*)=(.*)$/;
|
|
37
|
-
/**
|
|
38
|
-
* Undo the `printf '%q'` quoting the SessionStart hook applies to export
|
|
39
|
-
* values. UUIDs and plain paths arrive bare; values with specials arrive
|
|
40
|
-
* as `$'...'`, `'...'`, `"..."`, or backslash-escaped words.
|
|
41
|
-
*/
|
|
42
|
-
const unquote = (raw) => {
|
|
43
|
-
let v = raw.trim();
|
|
44
|
-
if (v.startsWith("$'") && v.endsWith("'") && v.length >= 3) v = v.slice(2, -1);
|
|
45
|
-
else if (v.startsWith("'") && v.endsWith("'") || v.startsWith("\"") && v.endsWith("\"")) {
|
|
46
|
-
if (v.length >= 2) v = v.slice(1, -1);
|
|
47
|
-
}
|
|
48
|
-
return v.replace(/\\(.)/g, "$1");
|
|
49
|
-
};
|
|
50
|
-
/**
|
|
51
|
-
* Parse `export KEY=value` lines from a session-env hook file into a
|
|
52
|
-
* plain record. Non-export lines are ignored.
|
|
53
|
-
*
|
|
54
|
-
* @param content - the raw text of a session-env hook file
|
|
55
|
-
* @returns a record of export names to unquoted values
|
|
56
|
-
* @public
|
|
57
|
-
*/
|
|
58
|
-
const parseSessionEnvExports = (content) => {
|
|
59
|
-
const out = {};
|
|
60
|
-
for (const line of content.split("\n")) {
|
|
61
|
-
const m = EXPORT_LINE.exec(line.trim());
|
|
62
|
-
if (m?.[1] !== void 0 && m[2] !== void 0) out[m[1]] = unquote(m[2]);
|
|
63
|
-
}
|
|
64
|
-
return out;
|
|
65
|
-
};
|
|
66
|
-
/**
|
|
67
|
-
* Recover a {@link SessionContext} from the newest session-env hook file
|
|
68
|
-
* whose `VITEST_AGENT_PROJECT_DIR` matches `projectDir`.
|
|
69
|
-
*
|
|
70
|
-
* Returns `null` when the session-env root is missing, unreadable, or no
|
|
71
|
-
* session dir matches the project. Never throws — recovery is best-effort
|
|
72
|
-
* and callers fall back to their existing null-context behavior.
|
|
73
|
-
*
|
|
74
|
-
* @param opts - `projectDir` to match against; `sessionEnvRoot` overrides
|
|
75
|
-
* the default `~/.claude/session-env` (tests)
|
|
76
|
-
* @public
|
|
77
|
-
*/
|
|
78
|
-
const recoverSessionContextFromSessionEnv = (opts) => {
|
|
79
|
-
const root = opts.sessionEnvRoot ?? join(homedir(), ".claude", "session-env");
|
|
80
|
-
const wantDir = resolve(opts.projectDir);
|
|
81
|
-
let entries;
|
|
82
|
-
try {
|
|
83
|
-
entries = readdirSync(root);
|
|
84
|
-
} catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
let best = null;
|
|
88
|
-
for (const entry of entries) {
|
|
89
|
-
const file = join(root, entry, "vitest-agent-hook.sh");
|
|
90
|
-
try {
|
|
91
|
-
const st = statSync(file);
|
|
92
|
-
const env = parseSessionEnvExports(readFileSync(file, "utf8"));
|
|
93
|
-
const chatId = env.VITEST_AGENT_CHAT_ID;
|
|
94
|
-
const conversationId = env.VITEST_AGENT_CONVERSATION_ID;
|
|
95
|
-
const mainAgentId = env.VITEST_AGENT_MAIN_AGENT_ID ?? env.VITEST_AGENT_AGENT_ID;
|
|
96
|
-
const fileProjectDir = env.VITEST_AGENT_PROJECT_DIR;
|
|
97
|
-
if (chatId === void 0 || chatId.length === 0 || conversationId === void 0 || conversationId.length === 0 || mainAgentId === void 0 || mainAgentId.length === 0 || fileProjectDir === void 0 || resolve(fileProjectDir) !== wantDir) continue;
|
|
98
|
-
if (best === null || st.mtimeMs > best.mtimeMs) best = {
|
|
99
|
-
mtimeMs: st.mtimeMs,
|
|
100
|
-
ctx: {
|
|
101
|
-
chatId,
|
|
102
|
-
conversationId,
|
|
103
|
-
mainAgentId
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
} catch {}
|
|
107
|
-
}
|
|
108
|
-
return best === null ? null : best.ctx;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
//#endregion
|
|
112
|
-
export { parseSessionEnvExports, recoverSessionContextFromSessionEnv };
|
package/utils/effect-to-zod.js
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
|
|
4
|
-
//#region src/utils/effect-to-zod.ts
|
|
5
|
-
/**
|
|
6
|
-
* Bridge an Effect Schema to a zod schema by routing through JSON
|
|
7
|
-
* Schema. Used at the MCP `registerTool` boundary so a tool can keep
|
|
8
|
-
* Effect Schema as the canonical source of truth for its output shape
|
|
9
|
-
* while the SDK still receives the zod instance it expects in the
|
|
10
|
-
* `outputSchema` field.
|
|
11
|
-
*
|
|
12
|
-
* @packageDocumentation
|
|
13
|
-
*/
|
|
14
|
-
/**
|
|
15
|
-
* Convert an Effect `Schema.Codec<A, I>` to a zod schema by
|
|
16
|
-
* serializing it to JSON Schema (`Schema.toJsonSchemaDocument`) and ingesting the
|
|
17
|
-
* result via zod 4's `z.fromJSONSchema`.
|
|
18
|
-
*
|
|
19
|
-
* Trade-offs:
|
|
20
|
-
* - Effect-Schema-only refinements (custom predicates, Brand types)
|
|
21
|
-
* erase to plain JSON Schema primitives during the round-trip,
|
|
22
|
-
* so the resulting zod schema does not enforce them. Tools that
|
|
23
|
-
* need refinement enforcement at the MCP boundary should declare
|
|
24
|
-
* zod directly.
|
|
25
|
-
* - `Schema.NullOr(...)` round-trips correctly via JSON Schema's
|
|
26
|
-
* `oneOf` / nullable representation that zod understands.
|
|
27
|
-
* - `z.fromJSONSchema` is marked experimental in zod 4. The bridge
|
|
28
|
-
* contains a smoke-test in the corresponding test file so an
|
|
29
|
-
* incompatible upgrade surfaces immediately instead of in
|
|
30
|
-
* production tool registrations.
|
|
31
|
-
*
|
|
32
|
-
* Implementation note: zod 4's `z.fromJSONSchema` does not resolve
|
|
33
|
-
* `$ref` lookups into `$defs` — every `{ $ref: "#/$defs/X" }` it
|
|
34
|
-
* encounters throws "Reference not found". Effect's `Schema.toJsonSchemaDocument`
|
|
35
|
-
* emits a `$ref`-and-`$defs` representation whenever a Schema carries
|
|
36
|
-
* an `identifier` annotation. The bridge therefore inlines every
|
|
37
|
-
* `$ref` in the document before handing it to zod (recursive
|
|
38
|
-
* substitution, then drop `$defs`). The schemas don't use
|
|
39
|
-
* `Schema.suspend`, so the substitution is acyclic.
|
|
40
|
-
*
|
|
41
|
-
* Annotation lifting: Effect lowers a checked numeric schema
|
|
42
|
-
* (`Schema.Finite`, `Schema.Int`) to `{ type, allOf: [{ description }] }`
|
|
43
|
-
* rather than putting the annotation on the node itself, and
|
|
44
|
-
* `z.fromJSONSchema` preserves that nesting verbatim. A tool's
|
|
45
|
-
* `outputSchema` would then advertise no description for the field. The
|
|
46
|
-
* bridge therefore lifts annotation-only `allOf` members onto their
|
|
47
|
-
* parent before handing the document to zod. Only the JSON Schema
|
|
48
|
-
* annotation vocabulary is lifted -- a member carrying any constraint
|
|
49
|
-
* keyword (`pattern`, `minimum`, ...) stays put so validation semantics
|
|
50
|
-
* are never changed.
|
|
51
|
-
*
|
|
52
|
-
* MCP SDK constraint: `outputSchema` must normalise to a Zod object
|
|
53
|
-
* schema (`normalizeObjectSchema` returns `undefined` for unions, then
|
|
54
|
-
* `safeParseAsync(undefined, ...)` crashes with "Cannot read properties
|
|
55
|
-
* of undefined (reading '_zod')"). When the resulting zod schema is not
|
|
56
|
-
* object-typed (e.g. came from `Schema.Union` of discriminated
|
|
57
|
-
* variants), the bridge wraps it in a permissive `z.object({}).catchall(z.unknown())`
|
|
58
|
-
* so the SDK accepts it. The structured content the tool emits still
|
|
59
|
-
* conforms to the original Effect Schema; consumers just don't get a
|
|
60
|
-
* rich JSON Schema for the union in the tool listing. Restructure the
|
|
61
|
-
* source schema as a single `Schema.Struct` with a discriminator field
|
|
62
|
-
* if the rich listing matters.
|
|
63
|
-
*/
|
|
64
|
-
const effectToZodSchema = (schema) => {
|
|
65
|
-
const document = Schema.toJsonSchemaDocument(schema);
|
|
66
|
-
const jsonSchema = {
|
|
67
|
-
...document.schema,
|
|
68
|
-
$defs: document.definitions
|
|
69
|
-
};
|
|
70
|
-
const inlined = inlineAllRefs(jsonSchema);
|
|
71
|
-
const zodSchema = z.fromJSONSchema(inlined);
|
|
72
|
-
if (isObjectLike(zodSchema)) return zodSchema;
|
|
73
|
-
return z.object({}).catchall(z.unknown());
|
|
74
|
-
};
|
|
75
|
-
const isObjectLike = (schema) => {
|
|
76
|
-
const def = schema._zod?.def;
|
|
77
|
-
return def?.type === "object" || def?.shape !== void 0;
|
|
78
|
-
};
|
|
79
|
-
const REF_PREFIX = "#/$defs/";
|
|
80
|
-
/**
|
|
81
|
-
* The JSON Schema annotation vocabulary -- keywords that describe a
|
|
82
|
-
* schema without constraining what it accepts. Only these are safe to
|
|
83
|
-
* hoist out of an `allOf` member onto the parent node.
|
|
84
|
-
*/
|
|
85
|
-
const ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
|
|
86
|
-
"title",
|
|
87
|
-
"description",
|
|
88
|
-
"examples",
|
|
89
|
-
"default",
|
|
90
|
-
"deprecated",
|
|
91
|
-
"readOnly",
|
|
92
|
-
"writeOnly",
|
|
93
|
-
"contentEncoding",
|
|
94
|
-
"contentMediaType"
|
|
95
|
-
]);
|
|
96
|
-
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
97
|
-
/**
|
|
98
|
-
* Hoist the annotation keywords out of every `allOf` member onto
|
|
99
|
-
* `node`, leaving each member's constraint keywords exactly where they
|
|
100
|
-
* were. A member reduced to nothing is dropped, and `allOf` disappears
|
|
101
|
-
* once every member has been consumed.
|
|
102
|
-
*
|
|
103
|
-
* Keys already present on the node win -- a nested annotation never
|
|
104
|
-
* overwrites a more specific one -- and the first member to supply a
|
|
105
|
-
* given key wins among siblings.
|
|
106
|
-
*/
|
|
107
|
-
const liftAnnotations = (node) => {
|
|
108
|
-
const members = node.allOf;
|
|
109
|
-
if (!Array.isArray(members)) return node;
|
|
110
|
-
const kept = [];
|
|
111
|
-
const lifted = {};
|
|
112
|
-
for (const member of members) {
|
|
113
|
-
if (!isPlainObject(member)) {
|
|
114
|
-
kept.push(member);
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
const constraints = {};
|
|
118
|
-
for (const [key, value] of Object.entries(member)) if (!ANNOTATION_KEYWORDS.has(key)) constraints[key] = value;
|
|
119
|
-
else if (!(key in node) && !(key in lifted)) lifted[key] = value;
|
|
120
|
-
if (Object.keys(constraints).length > 0) kept.push(constraints);
|
|
121
|
-
}
|
|
122
|
-
if (Object.keys(lifted).length === 0) return node;
|
|
123
|
-
const out = {
|
|
124
|
-
...node,
|
|
125
|
-
...lifted
|
|
126
|
-
};
|
|
127
|
-
if (kept.length > 0) out.allOf = kept;
|
|
128
|
-
else delete out.allOf;
|
|
129
|
-
return out;
|
|
130
|
-
};
|
|
131
|
-
/**
|
|
132
|
-
* Walk a JSON Schema tree and replace every `$ref: "#/$defs/X"` node
|
|
133
|
-
* with the contents of `$defs.X`, recursively. The `$defs` table is
|
|
134
|
-
* dropped from the returned root.
|
|
135
|
-
*/
|
|
136
|
-
const inlineAllRefs = (root) => {
|
|
137
|
-
const defs = root.$defs ?? {};
|
|
138
|
-
const visit = (value) => {
|
|
139
|
-
if (Array.isArray(value)) return value.map(visit);
|
|
140
|
-
if (value === null || typeof value !== "object") return value;
|
|
141
|
-
const obj = value;
|
|
142
|
-
if (typeof obj.$ref === "string" && obj.$ref.startsWith(REF_PREFIX)) {
|
|
143
|
-
const defName = obj.$ref.slice(8);
|
|
144
|
-
const target = defs[defName];
|
|
145
|
-
if (target !== void 0) return visit(target);
|
|
146
|
-
}
|
|
147
|
-
const out = {};
|
|
148
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
149
|
-
if (k === "$defs") continue;
|
|
150
|
-
out[k] = visit(v);
|
|
151
|
-
}
|
|
152
|
-
return liftAnnotations(out);
|
|
153
|
-
};
|
|
154
|
-
return visit(root);
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
//#endregion
|
|
158
|
-
export { effectToZodSchema };
|