mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-11-10 11:21:06 +00:00
Compare commits
5 Commits
v0.102.1-p
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1d83c6031 | ||
|
|
966aee0f04 | ||
|
|
66adc1a4c9 | ||
|
|
bc031f362c | ||
|
|
566e2652a9 |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.102.0"
|
||||
".": "0.103.0"
|
||||
}
|
||||
|
||||
15
AGENTS.md
15
AGENTS.md
@@ -3,10 +3,9 @@
|
||||
An ACP-compatible coding agent powered by the Claude Agent SDK. Source in `src/`,
|
||||
TypeScript, ESM, Node >= 22. `src/v1/` holds the v1 agent (`ClaudeAcpAgent` in
|
||||
`src/v1/acp-agent.ts`), `src/v2/` the v2 agent, and the rest of `src/` the modules
|
||||
both share. `src/acp-agent.ts` only re-exports `src/v1/acp-agent.ts` for the
|
||||
published `dist/acp-agent.js` path. ESLint rejects imports between `src/v1/` and
|
||||
both share. The package exports only `src/lib.ts`. ESLint rejects imports between `src/v1/` and
|
||||
`src/v2/`, and from shared modules into either agent. The entry points (`index.ts`, `lib.ts`,
|
||||
`serve.ts`, `binary-entry.ts`, `acp-agent.ts`) are exempt.
|
||||
`serve.ts`, `binary-entry.ts`) are exempt.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -86,9 +85,13 @@ opening a PR.
|
||||
|
||||
## Testing guidelines
|
||||
|
||||
- Before adding a test, search `src/tests/` for existing tests of the same API
|
||||
and scenario, and add to their file and `describe` block. Use the helpers in
|
||||
`helpers.ts` and `session-doubles.ts` instead of copying setup.
|
||||
- Tests live in `src/tests/`: `v1/` holds the tests that drive the v1 agent,
|
||||
`v2/` the tests of the v2 agent, and the top level the tests of shared
|
||||
modules, with `helpers.ts`, the scenario harness (`acp-scenarios/`) and
|
||||
`fixtures/`. Before adding a test, search `src/tests/` for existing tests of
|
||||
the same API and scenario, and add to their file and `describe` block. Use
|
||||
the helpers in `helpers.ts` and `v1/session-doubles.ts` instead of copying
|
||||
setup.
|
||||
- Don't add cases that take the same code path. Tests that differ only in input
|
||||
and expected output belong in one `it.each`. Don't repeat another test's
|
||||
assertions; assert only what this test is about.
|
||||
|
||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,5 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## [0.103.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.102.0...v0.103.0) (2026-11-10)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* deep imports of `dist/` files fail with `ERR_PACKAGE_PATH_NOT_EXPORTED`, and `lib.ts` exports only `serveAcp`, `AUTH_STATUS_UPDATE_METHOD` and their types.
|
||||
|
||||
### Features
|
||||
|
||||
* serve ACP v2 with a direct v2 agent and remove the v1 bridge ([#1560](https://github.com/agentclientprotocol/claude-agent-acp/issues/1560)) ([cb285cd](https://github.com/agentclientprotocol/claude-agent-acp/commit/cb285cd2857924c07038c129cc836da007e2737f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* give subagent checklists their own plan ID ([#1558](https://github.com/agentclientprotocol/claude-agent-acp/issues/1558)) ([2f7e386](https://github.com/agentclientprotocol/claude-agent-acp/commit/2f7e386f3600658d905906f3b37301120b07605f))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* export only lib.ts, with serveAcp and the authStatus wire names ([#1566](https://github.com/agentclientprotocol/claude-agent-acp/issues/1566)) ([566e265](https://github.com/agentclientprotocol/claude-agent-acp/commit/566e2652a9da5a31dab57a23ea1b06e9fff2ead8))
|
||||
|
||||
## [0.102.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.101.1...v0.102.0) (2026-11-09)
|
||||
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ A page with more sessions after it has a `nextCursor`, as ACP defines.
|
||||
A client that does not follow `nextCursor` sees only the newest 1000 sessions of the directory.
|
||||
An unknown cursor gives an `invalidParams` error.
|
||||
|
||||
`src/tests/acp-scenarios.test.ts` compares the traffic of a plain client and of Zed with recordings of origin/main.
|
||||
`src/tests/v1/acp-scenarios.test.ts` compares the traffic of a plain client and of Zed with recordings of origin/main.
|
||||
|
||||
## Negotiation
|
||||
|
||||
@@ -557,7 +557,7 @@ AIR puts the hooks into `_meta.claudeCode.options.settings.hooks` of `session/ne
|
||||
```
|
||||
|
||||
- The settings go to the flag settings tier of that session only. Other sessions and the settings files do not change.
|
||||
- Claude Code runs the hooks of every settings tier, so the user, project and IDE hooks of one event all run. `src/tests/session-hooks.test.ts` checks this with the Claude Code binary.
|
||||
- Claude Code runs the hooks of every settings tier, so the user, project and IDE hooks of one event all run. `src/tests/v1/session-hooks.test.ts` checks this with the Claude Code binary.
|
||||
- `CLAUDE_MODEL_CONFIG` stays in effect (see [Model configuration](model-configuration.md#precedence)).
|
||||
- A `session/load` with different settings recreates the query, so it applies the new hooks.
|
||||
- Claude asks for no hook trust step.
|
||||
|
||||
@@ -61,11 +61,11 @@ export default [
|
||||
curly: ["error", "all"],
|
||||
},
|
||||
},
|
||||
// `src/v2/` must not import the v1 agent (`src/v1/`, or `src/acp-agent.ts`, which re-exports it).
|
||||
// `src/v2/` must not import the v1 agent.
|
||||
{
|
||||
files: ["src/v2/**/*.ts"],
|
||||
rules: protocolBoundary(
|
||||
"^(\\.\\./)+(v1/|acp-agent\\.js$)",
|
||||
"^(\\.\\./)+v1/",
|
||||
"src/v2/ must not import the v1 agent. Move what both versions need into a shared module in src/.",
|
||||
),
|
||||
},
|
||||
@@ -88,11 +88,10 @@ export default [
|
||||
"src/lib.ts",
|
||||
"src/serve.ts",
|
||||
"src/binary-entry.ts",
|
||||
"src/acp-agent.ts",
|
||||
],
|
||||
rules: protocolBoundary(
|
||||
"^(\\./|(\\.\\./)+)(v1/|v2/|acp-agent\\.js$)",
|
||||
"Shared modules must not import src/v1/, src/v2/ or src/acp-agent.ts. Move the code into the shared module, or pass it in from the agent.",
|
||||
"^(\\./|(\\.\\./)+)(v1|v2)/",
|
||||
"Shared modules must not import src/v1/ or src/v2/. Move the code into the shared module, or pass it in from the agent.",
|
||||
),
|
||||
},
|
||||
prettierConfig,
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.102.0",
|
||||
"version": "0.103.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.102.0",
|
||||
"version": "0.103.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "2.5.0",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.102.0",
|
||||
"version": "0.103.0",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
|
||||
"main": "dist/lib.js",
|
||||
"types": "dist/lib.d.ts",
|
||||
@@ -16,7 +16,7 @@
|
||||
"types": "./dist/lib.d.ts",
|
||||
"import": "./dist/lib.js"
|
||||
},
|
||||
"./*": "./*"
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// The v1 agent lives in `v1/acp-agent.ts`. This module keeps the published
|
||||
// `dist/acp-agent.js` path that consumers deep-import.
|
||||
export * from "./v1/acp-agent.js";
|
||||
23
src/lib.ts
23
src/lib.ts
@@ -1,19 +1,7 @@
|
||||
// Export the main agent class and utilities for library usage
|
||||
export { ClaudeAcpAgent, runAcp } from "./v1/acp-agent.js";
|
||||
export { isLocalCommandMetadata, stripLocalCommandMetadata } from "./message-filters.js";
|
||||
export { toAcpNotifications, streamEventToAcpNotifications } from "./sdk-to-acp.js";
|
||||
export type { NewSessionMeta, SDKMessageFilter } from "./new-session-meta.js";
|
||||
export type { ToolUpdateMeta } from "./tool-calls/renderer.js";
|
||||
export { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
|
||||
export {
|
||||
toolInfoFromToolUse,
|
||||
toDisplayPath,
|
||||
planEntries,
|
||||
toolUpdateFromToolResult,
|
||||
} from "./tools.js";
|
||||
export { SettingsManager, type SettingsManagerOptions } from "./settings.js";
|
||||
// The `authStatus` extension's wire surface: the notification's method name and
|
||||
// the payload types needed to read the traffic. The mappers stay internal.
|
||||
// The package's public API: serve ACP (v1 and v2) on stdio, and the wire
|
||||
// names of the `authStatus` extension that a client needs to read its traffic.
|
||||
export { serveAcp, type AcpProtocols } from "./serve.js";
|
||||
export type { Logger } from "./logger.js";
|
||||
export {
|
||||
AUTH_STATUS_UPDATE_METHOD,
|
||||
type AuthStatus,
|
||||
@@ -21,6 +9,3 @@ export {
|
||||
type AuthStatusKind,
|
||||
type AuthStatusUpdateNotification,
|
||||
} from "./auth-status.js";
|
||||
|
||||
// Export types
|
||||
export type { ClaudePlanEntry } from "./tools.js";
|
||||
|
||||
@@ -139,10 +139,6 @@ export function stripLocalCommandMetadata(content: unknown): unknown | null {
|
||||
return kept;
|
||||
}
|
||||
|
||||
export function isLocalCommandMetadata(content: unknown): boolean {
|
||||
return stripLocalCommandMetadata(content) === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* True for the synthetic assistant message the CLI injects into the transcript
|
||||
* when a turn fails authentication (e.g. "Not logged in · Please run /login",
|
||||
|
||||
@@ -84,7 +84,7 @@ export type ReplayChild = {
|
||||
responseModel?: string;
|
||||
};
|
||||
|
||||
type SendMessageCall = {
|
||||
export type SendMessageCall = {
|
||||
toolUseId: string;
|
||||
to: string;
|
||||
message: string;
|
||||
@@ -97,34 +97,27 @@ type SendMessageCall = {
|
||||
outgoingSent: boolean;
|
||||
};
|
||||
|
||||
export class RfdSubagentReplay {
|
||||
/** The children by the id of the tool use that launched each. */
|
||||
private readonly children = new Map<string, ReplayChild>();
|
||||
private readonly byAgentId = new Map<string, ReplayChild>();
|
||||
private readonly sendMessages: SendMessageCall[] = [];
|
||||
/** Launches without metadata, which the replay leaves out. */
|
||||
private readonly omitted = new Set<string>();
|
||||
/** Children whose transcript could not be read: only their association is replayed. */
|
||||
private withoutHistory = 0;
|
||||
/** The SubagentHandback calls, with their report and whether their result failed. */
|
||||
private readonly handbacks = new Map<
|
||||
string,
|
||||
{ message: string; failed: boolean; sent: boolean }
|
||||
>();
|
||||
/** A SubagentHandback call: its report, whether its result failed, and whether the replay sent it. */
|
||||
type ReplayHandback = { message: string; failed: boolean; sent: boolean };
|
||||
|
||||
constructor(
|
||||
private readonly history: SubagentHistory,
|
||||
private readonly publish: Publish,
|
||||
private readonly models?: SubagentModels,
|
||||
/**
|
||||
* Whether the replay maps a model to the session's model list as it goes.
|
||||
* A load lacks the list until the CLI started, so it maps only in
|
||||
* {@link currentStates}, after the load response.
|
||||
*/
|
||||
private readonly mapsDuringReplay = true,
|
||||
/** The working directory of the root session, which a child inherits unless it reports its own. */
|
||||
private readonly rootCwd?: string,
|
||||
) {}
|
||||
/**
|
||||
* What both replays read from the transcripts before they send anything: the
|
||||
* children that launches with metadata started, the launches without metadata
|
||||
* (left out), the SendMessage calls, and the SubagentHandback calls. It sends
|
||||
* nothing; `RfdSubagentReplay` (v1) and `src/v2/subagent-replay.ts` send the
|
||||
* replay from it.
|
||||
*/
|
||||
export class ReplayTranscript<Child extends ReplayChild = ReplayChild> {
|
||||
/** The children by the id of the tool use that launched each. */
|
||||
readonly children = new Map<string, Child>();
|
||||
readonly byAgentId = new Map<string, Child>();
|
||||
readonly sendMessages: SendMessageCall[] = [];
|
||||
/** Launches without metadata, which the replay leaves out. */
|
||||
readonly omitted = new Set<string>();
|
||||
/** The SubagentHandback calls, by tool use id. */
|
||||
readonly handbacks = new Map<string, ReplayHandback>();
|
||||
|
||||
constructor(private readonly history: SubagentHistory) {}
|
||||
|
||||
/**
|
||||
* Registers the launches, the launch results, and the SendMessage calls in
|
||||
@@ -165,9 +158,64 @@ export class RfdSubagentReplay {
|
||||
}
|
||||
}
|
||||
|
||||
private registerLaunch(
|
||||
toolUseId: string,
|
||||
input: Record<string, unknown>,
|
||||
parentSessionId: string,
|
||||
): void {
|
||||
if (this.children.has(toolUseId)) return;
|
||||
const record = this.history.agents.get(toolUseId);
|
||||
if (!record) {
|
||||
this.omitted.add(toolUseId);
|
||||
return;
|
||||
}
|
||||
// A child type adds only optional fields, which a new child leaves unset.
|
||||
const child = {
|
||||
launchId: toolUseId,
|
||||
sessionId: record.agentId,
|
||||
record,
|
||||
parentSessionId,
|
||||
input,
|
||||
announced: false,
|
||||
promptSent: false,
|
||||
} as Child;
|
||||
this.children.set(toolUseId, child);
|
||||
this.byAgentId.set(record.agentId, child);
|
||||
}
|
||||
}
|
||||
|
||||
export class RfdSubagentReplay {
|
||||
private readonly transcript: ReplayTranscript;
|
||||
/** Children whose transcript could not be read: only their association is replayed. */
|
||||
private withoutHistory = 0;
|
||||
|
||||
constructor(
|
||||
private readonly history: SubagentHistory,
|
||||
private readonly publish: Publish,
|
||||
private readonly models?: SubagentModels,
|
||||
/**
|
||||
* Whether the replay maps a model to the session's model list as it goes.
|
||||
* A load lacks the list until the CLI started, so it maps only in
|
||||
* {@link currentStates}, after the load response.
|
||||
*/
|
||||
private readonly mapsDuringReplay = true,
|
||||
/** The working directory of the root session, which a child inherits unless it reports its own. */
|
||||
private readonly rootCwd?: string,
|
||||
) {
|
||||
this.transcript = new ReplayTranscript(history);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the launches, the launch results, and the SendMessage calls in
|
||||
* `messages`, the transcript of the session `ownerSessionId`.
|
||||
*/
|
||||
scan(messages: readonly SessionMessage[], ownerSessionId: string): void {
|
||||
this.transcript.scan(messages, ownerSessionId);
|
||||
}
|
||||
|
||||
/** The child that the tool use `toolUseId` launched, when the replay shows it. */
|
||||
childOfLaunch(toolUseId: string): ReplayChild | undefined {
|
||||
return this.children.get(toolUseId);
|
||||
return this.transcript.children.get(toolUseId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,11 +259,11 @@ export class RfdSubagentReplay {
|
||||
*/
|
||||
private effectiveCwd(sessionId: string): string | undefined {
|
||||
const seen = new Set<string>();
|
||||
let child = this.byAgentId.get(sessionId);
|
||||
let child = this.transcript.byAgentId.get(sessionId);
|
||||
while (child && !seen.has(child.sessionId)) {
|
||||
if (child.cwd) return child.cwd;
|
||||
seen.add(child.sessionId);
|
||||
child = this.byAgentId.get(child.parentSessionId);
|
||||
child = this.transcript.byAgentId.get(child.parentSessionId);
|
||||
}
|
||||
return this.rootCwd;
|
||||
}
|
||||
@@ -299,7 +347,9 @@ export class RfdSubagentReplay {
|
||||
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
|
||||
return false;
|
||||
}
|
||||
const sendMessage = this.sendMessages.find((call) => call.toolUseId === update.toolCallId);
|
||||
const sendMessage = this.transcript.sendMessages.find(
|
||||
(call) => call.toolUseId === update.toolCallId,
|
||||
);
|
||||
if (sendMessage) {
|
||||
if (sendMessage.recipientSessionId === undefined) {
|
||||
sendMessage.shownAsToolCall = true;
|
||||
@@ -317,7 +367,7 @@ export class RfdSubagentReplay {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const handback = owner ? this.handbacks.get(update.toolCallId) : undefined;
|
||||
const handback = owner ? this.transcript.handbacks.get(update.toolCallId) : undefined;
|
||||
if (owner && handback && !handback.failed) {
|
||||
if (!handback.sent) {
|
||||
handback.sent = true;
|
||||
@@ -333,7 +383,7 @@ export class RfdSubagentReplay {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const child = this.children.get(update.toolCallId);
|
||||
const child = this.transcript.children.get(update.toolCallId);
|
||||
if (!child) return false;
|
||||
if (
|
||||
update.status === "completed" &&
|
||||
@@ -352,29 +402,29 @@ export class RfdSubagentReplay {
|
||||
* its task id is the agent id.
|
||||
*/
|
||||
taskNotification(taskId: string, status: string): boolean {
|
||||
const child = this.byAgentId.get(taskId);
|
||||
const child = this.transcript.byAgentId.get(taskId);
|
||||
if (!child) return false;
|
||||
child.notified = nativeSubagentState(status) ?? child.notified;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the end of every announced child (see `endState`), and a notice when
|
||||
* Sends the end of every announced child (see `replayEndState`), and a notice when
|
||||
* subagent history is missing.
|
||||
*/
|
||||
async finish(supportsNotices: boolean, rootSessionId: string): Promise<void> {
|
||||
for (const child of [...this.children.values()].reverse()) {
|
||||
for (const child of [...this.transcript.children.values()].reverse()) {
|
||||
if (!child.announced) continue;
|
||||
await this.publish({
|
||||
sessionId: child.parentSessionId,
|
||||
update: {
|
||||
sessionUpdate: "subagent_update",
|
||||
sessionId: child.sessionId,
|
||||
state: endState(child),
|
||||
state: replayEndState(child),
|
||||
},
|
||||
});
|
||||
}
|
||||
const missing = this.omitted.size + this.withoutHistory;
|
||||
const missing = this.transcript.omitted.size + this.withoutHistory;
|
||||
if (missing > 0 && supportsNotices) {
|
||||
await this.publish({ sessionId: rootSessionId, update: missingHistoryNotice(missing) });
|
||||
}
|
||||
@@ -399,12 +449,14 @@ export class RfdSubagentReplay {
|
||||
live: (sessionId: string) => WorkState["state"] | undefined,
|
||||
liveConfiguration?: (sessionId: string) => SessionConfigOption[] | undefined,
|
||||
): AcpSessionNotification[] {
|
||||
return [...this.children.values()]
|
||||
return [...this.transcript.children.values()]
|
||||
.filter((child) => child.announced)
|
||||
.flatMap((child): AcpSessionNotification[] => {
|
||||
const liveState = live(child.sessionId);
|
||||
const state: WorkState =
|
||||
liveState === undefined || liveState === "idle" ? endState(child) : { state: liveState };
|
||||
liveState === undefined || liveState === "idle"
|
||||
? replayEndState(child)
|
||||
: { state: liveState };
|
||||
// The session's model list may be known only now, after the load response.
|
||||
const entry = responseModelEntry(child.responseModel, this.models);
|
||||
const replayed = child.model ? JSON.stringify([childModelOption(child.model)]) : undefined;
|
||||
@@ -435,30 +487,6 @@ export class RfdSubagentReplay {
|
||||
});
|
||||
}
|
||||
|
||||
private registerLaunch(
|
||||
toolUseId: string,
|
||||
input: Record<string, unknown>,
|
||||
parentSessionId: string,
|
||||
): void {
|
||||
if (this.children.has(toolUseId)) return;
|
||||
const record = this.history.agents.get(toolUseId);
|
||||
if (!record) {
|
||||
this.omitted.add(toolUseId);
|
||||
return;
|
||||
}
|
||||
const child: ReplayChild = {
|
||||
launchId: toolUseId,
|
||||
sessionId: record.agentId,
|
||||
record,
|
||||
parentSessionId,
|
||||
input,
|
||||
announced: false,
|
||||
promptSent: false,
|
||||
};
|
||||
this.children.set(toolUseId, child);
|
||||
this.byAgentId.set(record.agentId, child);
|
||||
}
|
||||
|
||||
/** The prompt of the launch, with the launch's id, to the parent and the child, as live. */
|
||||
private async sendPrompt(child: ReplayChild, text: string): Promise<void> {
|
||||
child.promptSent = true;
|
||||
@@ -489,7 +517,7 @@ export class RfdSubagentReplay {
|
||||
text: string,
|
||||
): Promise<void> {
|
||||
const call = deliveredCall(
|
||||
this.sendMessages,
|
||||
this.transcript.sendMessages,
|
||||
[child.sessionId, child.record.name, child.input.name],
|
||||
text,
|
||||
);
|
||||
@@ -553,7 +581,7 @@ export function replaySubagentTerminalState(
|
||||
* `cancelled` for a child the user stopped, else the result of its foreground
|
||||
* launch, else `idle` with no stop reason.
|
||||
*/
|
||||
function endState(child: ReplayChild): WorkState {
|
||||
export function replayEndState(child: ReplayChild): WorkState {
|
||||
const end = child.notified ?? (child.record.stoppedByUser ? "cancelled" : child.launchEnd);
|
||||
return end ? rfdEndState(end, undefined) : { state: "idle" };
|
||||
}
|
||||
@@ -586,7 +614,7 @@ function inputOf(block: Record<string, unknown>): Record<string, unknown> {
|
||||
}
|
||||
|
||||
/** The text of user message content: a string, or its text blocks. */
|
||||
function textOf(content: unknown): string {
|
||||
export function textOf(content: unknown): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
return content
|
||||
@@ -598,7 +626,7 @@ function textOf(content: unknown): string {
|
||||
}
|
||||
|
||||
/** Whether `dir` is an existing directory. */
|
||||
function isDirectory(dir: string): boolean {
|
||||
export function isDirectory(dir: string): boolean {
|
||||
try {
|
||||
return statSync(dir).isDirectory();
|
||||
} catch {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { randomUUID } from "crypto";
|
||||
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { Pushable } from "../utils.js";
|
||||
import { discoverCustomAgents, BUILTIN_AGENT_NAMES } from "../v1/acp-agent.js";
|
||||
import { discoverCustomAgents, BUILTIN_AGENT_NAMES } from "../session-config-options.js";
|
||||
|
||||
// `discoverCustomAgents` distinguishes user/plugin-configured agents from
|
||||
// Claude Code's built-in subagents by name (BUILTIN_AGENT_NAMES), since the
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
} from "../diff.js";
|
||||
import { AcpToolCallRenderer } from "../tool-calls/renderer.js";
|
||||
import { buildClaudePermissionPresentation } from "../permissions/presentation.js";
|
||||
import { toolInfoFromToolUse } from "../tools.js";
|
||||
import { WriteReporter } from "../tool-calls/reporters/file-edit.js";
|
||||
import { toolInfoFromToolUse } from "./tool-render.js";
|
||||
|
||||
const tempDirectories: string[] = [];
|
||||
|
||||
|
||||
@@ -1,74 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import packageJson from "../../package.json" with { type: "json" };
|
||||
|
||||
describe("module runtime exports", () => {
|
||||
/**
|
||||
* The public runtime exports of lib.ts must change only deliberately; consumers import these names.
|
||||
* The public runtime exports of lib.ts, the package's only entry point, must change only deliberately; consumers import these names.
|
||||
*/
|
||||
it("lib.ts exports the expected names", async () => {
|
||||
expect(Object.keys(await import("../lib.js")).sort()).toEqual([
|
||||
"AUTH_STATUS_UPDATE_METHOD",
|
||||
"ClaudeAcpAgent",
|
||||
"Pushable",
|
||||
"SettingsManager",
|
||||
"isLocalCommandMetadata",
|
||||
"nodeToWebReadable",
|
||||
"nodeToWebWritable",
|
||||
"planEntries",
|
||||
"runAcp",
|
||||
"streamEventToAcpNotifications",
|
||||
"stripLocalCommandMetadata",
|
||||
"toAcpNotifications",
|
||||
"toDisplayPath",
|
||||
"toolInfoFromToolUse",
|
||||
"toolUpdateFromToolResult",
|
||||
"unreachable",
|
||||
"serveAcp",
|
||||
]);
|
||||
});
|
||||
|
||||
/**
|
||||
* Moving code out of acp-agent.ts must keep every name re-exported, since consumers deep-import
|
||||
* dist/acp-agent.js; change this list only deliberately.
|
||||
*/
|
||||
it("acp-agent.ts exports the expected names", async () => {
|
||||
expect(Object.keys(await import("../acp-agent.js")).sort()).toEqual([
|
||||
"AGENT_CONFIG_ID",
|
||||
"AUTONOMOUS_RESULT_ORIGINS",
|
||||
"BUILTIN_AGENT_NAMES",
|
||||
"ClaudeAcpAgent",
|
||||
"DEFAULT_AGENT_ID",
|
||||
"EFFORT_CONFIG_ID",
|
||||
"FAST_MODE_CONFIG_ID",
|
||||
"FAST_MODE_OFF",
|
||||
"FAST_MODE_ON",
|
||||
"MODEL_CONFIG_ID",
|
||||
"MODE_CONFIG_ID",
|
||||
"applyAvailableModelsAllowlist",
|
||||
"buildConfigOptions",
|
||||
"clientSupportsBooleanConfigOptions",
|
||||
"clientSupportsRecommendedConfigValue",
|
||||
"computeSessionFingerprint",
|
||||
"createFastModeConfigOption",
|
||||
"discoverCustomAgents",
|
||||
"fastModeStateEnabled",
|
||||
"getAvailableSlashCommands",
|
||||
"isAbortedResult",
|
||||
"isLocalCommandMetadata",
|
||||
"isLoginRequiredText",
|
||||
"isReplayHiddenMetaMessage",
|
||||
"isSyntheticLoginMessage",
|
||||
"matchResumedModel",
|
||||
"messageIdForGrouping",
|
||||
"normalizeFastModeDisabledReason",
|
||||
"promptToClaude",
|
||||
"resolveFastModeEnabled",
|
||||
"resolveModelPreference",
|
||||
"runAcp",
|
||||
"runPromptWithCancellation",
|
||||
"settingsEffortForModel",
|
||||
"streamEventToAcpNotifications",
|
||||
"stripLocalCommandMetadata",
|
||||
"toAcpNotifications",
|
||||
"v1AgentApp",
|
||||
]);
|
||||
it("package.json exposes lib.ts as the only module entry point", () => {
|
||||
expect(Object.keys(packageJson.exports)).toEqual([".", "./package.json"]);
|
||||
expect(packageJson.exports["."]).toEqual({
|
||||
types: "./dist/lib.d.ts",
|
||||
import: "./dist/lib.js",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ClientCapabilities } from "@agentclientprotocol/sdk/unstable";
|
||||
import type { ModelInfo } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { buildConfigOptions, clientSupportsRecommendedConfigValue } from "../v1/acp-agent.js";
|
||||
import {
|
||||
buildConfigOptions,
|
||||
clientSupportsRecommendedConfigValue,
|
||||
} from "../session-config-options.js";
|
||||
import { AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY } from "../air-extension.js";
|
||||
|
||||
const MODES = {
|
||||
|
||||
63
src/tests/tool-render.ts
Normal file
63
src/tests/tool-render.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Test shorthands for the shared tool call renderer: one tool use or tool
|
||||
* result, rendered for a client with the given capabilities. Production code
|
||||
* renders through {@link AcpToolCallRenderer} directly.
|
||||
*/
|
||||
import type {
|
||||
ToolCallContent,
|
||||
ToolCallLocation,
|
||||
ToolKind,
|
||||
} from "@agentclientprotocol/sdk/unstable";
|
||||
import { stripLocalCommandMetadata } from "../message-filters.js";
|
||||
import { ClientCapabilities } from "../tool-calls/client-capabilities.js";
|
||||
import type { ToolResultBlock } from "../tool-calls/content.js";
|
||||
import { AcpToolCallRenderer, type RenderedResult } from "../tool-calls/renderer.js";
|
||||
|
||||
/**
|
||||
* The title, kind, content, and locations of a tool use, for a client with the
|
||||
* given terminal and patch capabilities. The {@link AcpToolCallRenderer} builds
|
||||
* them from the facts of the tool reporter.
|
||||
*/
|
||||
export function toolInfoFromToolUse(
|
||||
toolUse: any,
|
||||
supportsTerminalOutput: boolean = false,
|
||||
cwd?: string,
|
||||
supportsDiffPatch: boolean = false,
|
||||
): {
|
||||
title: string;
|
||||
kind: ToolKind;
|
||||
content: ToolCallContent[];
|
||||
locations?: ToolCallLocation[];
|
||||
} {
|
||||
const renderer = new AcpToolCallRenderer(
|
||||
new ClientCapabilities(supportsTerminalOutput, false, supportsDiffPatch),
|
||||
);
|
||||
return renderer.toolInfo({ id: toolUse?.id, name: toolUse?.name, input: toolUse?.input }, cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
* The result fields of a tool result, for a client with the given terminal
|
||||
* capabilities. The {@link AcpToolCallRenderer} builds them from the facts of
|
||||
* the tool reporter.
|
||||
*/
|
||||
export function toolUpdateFromToolResult(
|
||||
toolResult: ToolResultBlock,
|
||||
toolUse: any | undefined,
|
||||
supportsTerminalOutput: boolean = false,
|
||||
toolUseResult?: unknown,
|
||||
preferTerminalOutputDelta: boolean = false,
|
||||
): RenderedResult {
|
||||
const renderer = new AcpToolCallRenderer(
|
||||
new ClientCapabilities(supportsTerminalOutput, preferTerminalOutputDelta),
|
||||
);
|
||||
return renderer.resultFields(
|
||||
{ id: toolUse?.id, name: toolUse?.name ?? "", input: toolUse?.input },
|
||||
toolResult as Parameters<AcpToolCallRenderer["resultFields"]>[1],
|
||||
toolUseResult,
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether `content` is only Claude Code's local-command metadata, which the agent drops. */
|
||||
export function isLocalCommandMetadata(content: unknown): boolean {
|
||||
return stripLocalCommandMetadata(content) === null;
|
||||
}
|
||||
@@ -26,12 +26,10 @@ import { AcpClient } from "../acp-client.js";
|
||||
import { toAcpNotifications, ToolUseCache } from "../sdk-to-acp.js";
|
||||
import { Logger } from "../logger.js";
|
||||
import {
|
||||
toolUpdateFromToolResult,
|
||||
createPostToolUseHook,
|
||||
createTaskHook,
|
||||
clearHookCallbacks,
|
||||
registerHookCallback,
|
||||
toolInfoFromToolUse,
|
||||
planEntries,
|
||||
applyTaskCreate,
|
||||
applyTaskList,
|
||||
@@ -43,6 +41,7 @@ import {
|
||||
} from "../tools.js";
|
||||
import { ClientCapabilities as ToolCallCapabilities } from "../tool-calls/client-capabilities.js";
|
||||
import { AcpToolCallRenderer } from "../tool-calls/renderer.js";
|
||||
import { toolInfoFromToolUse, toolUpdateFromToolResult } from "./tool-render.js";
|
||||
|
||||
/** An AIR client: it gets the ACP tool call contract (`docs/air-extensions.md`). */
|
||||
const AIR_CLIENT: ClientCapabilities = {
|
||||
|
||||
@@ -2,9 +2,9 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { makeMockQuery } from "./helpers.js";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import { makeMockQuery } from "../helpers.js";
|
||||
|
||||
const { querySpy } = vi.hoisted(() => ({
|
||||
querySpy: vi.fn(),
|
||||
@@ -85,7 +85,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
|
||||
const { getCapturedOptions } = mockQuery();
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -129,7 +129,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
applyFlagSettings,
|
||||
}),
|
||||
);
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent = new ClaudeAcpAgent(createMockClient());
|
||||
if (recommended) {
|
||||
(agent as any).clientCapabilities = {
|
||||
@@ -180,7 +180,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
applyFlagSettings,
|
||||
}),
|
||||
);
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent = new ClaudeAcpAgent(createMockClient());
|
||||
(agent as any).clientCapabilities = {
|
||||
_meta: { jetbrains: { air: { version: 1, capabilities: ["recommendedValue"] } } },
|
||||
@@ -227,7 +227,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
applyFlagSettings,
|
||||
}),
|
||||
);
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent = new ClaudeAcpAgent(createMockClient());
|
||||
(agent as any).clientCapabilities = {
|
||||
_meta: { jetbrains: { air: { version: 1, capabilities: ["recommendedValue"] } } },
|
||||
@@ -271,7 +271,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
|
||||
const { getCapturedOptions } = mockQuery();
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -296,7 +296,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
|
||||
const { getCapturedOptions } = mockQuery();
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -319,7 +319,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
|
||||
const { getCapturedOptions } = mockQuery();
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
(agent as any).logger = { log: () => {}, error: () => {} };
|
||||
|
||||
@@ -363,7 +363,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
|
||||
const { getCapturedOptions } = mockQuery();
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
(agent as any).logger = { log: () => {}, error: () => {} };
|
||||
|
||||
@@ -388,7 +388,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
|
||||
const { getCapturedOptions } = mockQuery();
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -414,7 +414,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
|
||||
mockQuery();
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
(agent as any).logger = { log: () => {}, error: () => {} };
|
||||
|
||||
@@ -448,7 +448,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
);
|
||||
|
||||
const { getCapturedOptions, setModelSpy } = mockQuery(models);
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
(agent as any).logger = { log: () => {}, error: () => {} };
|
||||
return { agent, projectDir, getCapturedOptions, setModelSpy };
|
||||
@@ -647,7 +647,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
|
||||
const { getCapturedOptions } = mockQuery();
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -674,7 +674,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
|
||||
const { setModelSpy } = mockQuery();
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -729,7 +729,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -756,7 +756,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -808,7 +808,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
{ value: "claude-haiku-4-5", displayName: "Claude Haiku", description: "Fast" },
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -845,7 +845,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -892,7 +892,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -957,7 +957,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
{ value: "haiku", displayName: "Haiku", description: "Fast" },
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -997,7 +997,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
{ value: "haiku", displayName: "Haiku", description: "Fast" },
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -1029,7 +1029,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
{ value: "haiku", displayName: "Haiku", description: "Fast" },
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -1051,7 +1051,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
{ value: "haiku", displayName: "Haiku", description: "Fast" },
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -1085,7 +1085,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
{ value: "haiku", displayName: "Haiku", description: "Fast" },
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -1136,7 +1136,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -1197,7 +1197,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -1249,7 +1249,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -1304,7 +1304,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -1355,7 +1355,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
{ value: "sonnet", displayName: "Sonnet", description: "Claude Sonnet 4.6" },
|
||||
]);
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -1410,7 +1410,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
}) as any;
|
||||
});
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -1451,7 +1451,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
});
|
||||
|
||||
try {
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -1502,7 +1502,7 @@ describe("ClaudeAcpAgent settings", () => {
|
||||
}) as any;
|
||||
});
|
||||
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
|
||||
|
||||
const response = await (agent as any).createSession({
|
||||
@@ -22,48 +22,44 @@ import {
|
||||
WriteTextFileRequest,
|
||||
WriteTextFileResponse,
|
||||
} from "@agentclientprotocol/sdk/unstable";
|
||||
import type { AcpSessionNotification } from "../acp-subagents.js";
|
||||
import { NativeSubagentRuntime } from "../native-subagents.js";
|
||||
import { nodeToWebWritable, nodeToWebReadable } from "../utils.js";
|
||||
import {
|
||||
createPostToolUseHook,
|
||||
markdownEscape,
|
||||
toolInfoFromToolUse,
|
||||
toDisplayPath,
|
||||
toolUpdateFromToolResult,
|
||||
} from "../tools.js";
|
||||
import { toolUpdateFromDiffToolResponse } from "../diff.js";
|
||||
import { ToolCallFieldTracker } from "../tool-calls/field-tracker.js";
|
||||
import type { AcpSessionNotification } from "../../acp-subagents.js";
|
||||
import { NativeSubagentRuntime } from "../../native-subagents.js";
|
||||
import { nodeToWebWritable, nodeToWebReadable } from "../../utils.js";
|
||||
import { createPostToolUseHook, markdownEscape, toDisplayPath } from "../../tools.js";
|
||||
import { toolUpdateFromDiffToolResponse } from "../../diff.js";
|
||||
import { ToolCallFieldTracker } from "../../tool-calls/field-tracker.js";
|
||||
import {
|
||||
ClaudeAcpAgent,
|
||||
computeSessionFingerprint,
|
||||
buildConfigOptions,
|
||||
createFastModeConfigOption,
|
||||
discoverCustomAgents,
|
||||
runPromptWithCancellation,
|
||||
type SteerRequest,
|
||||
} from "../v1/acp-agent.js";
|
||||
} from "../../v1/acp-agent.js";
|
||||
import { buildConfigOptions, discoverCustomAgents } from "../../session-config-options.js";
|
||||
import { createFastModeConfigOption } from "../../session-fast-mode.js";
|
||||
import {
|
||||
toAcpNotifications,
|
||||
streamEventToAcpNotifications,
|
||||
messageIdForGrouping,
|
||||
type StreamedToolInputCache,
|
||||
} from "../sdk-to-acp.js";
|
||||
import { promptToClaude } from "../prompt-to-claude.js";
|
||||
} from "../../sdk-to-acp.js";
|
||||
import { promptToClaude } from "../../prompt-to-claude.js";
|
||||
import {
|
||||
isLocalCommandMetadata,
|
||||
isReplayHiddenMetaMessage,
|
||||
isSyntheticLoginMessage,
|
||||
stripLocalCommandMetadata,
|
||||
} from "../message-filters.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import type { SDKMessageFilter } from "../new-session-meta.js";
|
||||
import { SessionTitles } from "../session-titles.js";
|
||||
import { SessionBackgroundWork } from "../v1/background-work.js";
|
||||
import { formatUsageResponse, isUsageCommandText, parseUsageResponse } from "../usage-markdown.js";
|
||||
import { Pushable } from "../utils.js";
|
||||
import { heldRequest, initializeClient, makeMockQuery } from "./helpers.js";
|
||||
import { acpProtocolRouter } from "../serve.js";
|
||||
} from "../../message-filters.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import type { SDKMessageFilter } from "../../new-session-meta.js";
|
||||
import { SessionTitles } from "../../session-titles.js";
|
||||
import { SessionBackgroundWork } from "../../v1/background-work.js";
|
||||
import {
|
||||
formatUsageResponse,
|
||||
isUsageCommandText,
|
||||
parseUsageResponse,
|
||||
} from "../../usage-markdown.js";
|
||||
import { Pushable } from "../../utils.js";
|
||||
import { heldRequest, initializeClient, makeMockQuery } from "../helpers.js";
|
||||
import { acpProtocolRouter } from "../../serve.js";
|
||||
import * as agentSdk from "@anthropic-ai/claude-agent-sdk";
|
||||
import {
|
||||
deleteSession,
|
||||
@@ -95,7 +91,7 @@ import {
|
||||
goalUpdateFromPrompt,
|
||||
parseGoalRequest,
|
||||
toGoalSnapshot,
|
||||
} from "../v1/goal-extension.js";
|
||||
} from "../../v1/goal-extension.js";
|
||||
|
||||
vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@anthropic-ai/claude-agent-sdk")>();
|
||||
@@ -120,7 +116,12 @@ import type {
|
||||
BetaWebFetchToolResultBlockParam,
|
||||
BetaCodeExecutionToolResultBlockParam,
|
||||
} from "@anthropic-ai/sdk/resources/beta.mjs";
|
||||
import { validateRecorded } from "./acp-scenarios/schema.js";
|
||||
import { validateRecorded } from "../acp-scenarios/schema.js";
|
||||
import {
|
||||
isLocalCommandMetadata,
|
||||
toolInfoFromToolUse,
|
||||
toolUpdateFromToolResult,
|
||||
} from "../tool-render.js";
|
||||
|
||||
/** The capabilities of an AIR client. Only AIR gets the AIR extensions of
|
||||
* `docs/air-extensions.md`. */
|
||||
@@ -3605,7 +3606,7 @@ describe("usage-limit failure replay", () => {
|
||||
|
||||
it("recognizes a sanitized raw transcript through the real SDK parser", async () => {
|
||||
const raw = await readFile(
|
||||
new URL("./fixtures/usage-limit-session.jsonl", import.meta.url),
|
||||
new URL("../fixtures/usage-limit-session.jsonl", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const entries = raw
|
||||
@@ -5,11 +5,11 @@
|
||||
* - Recordings are JSON Lines: one outbound message on each line, with sorted
|
||||
* keys. `normalize` in `harness.ts` replaces only exact run-specific values:
|
||||
* the generated ids, the paths, and the argv and version of this process.
|
||||
* - AIR golden files: `acp-scenarios/__snapshots__/air/<scenario>.jsonl`.
|
||||
* Run `npx vitest run src/tests/acp-scenarios.test.ts -u` to update them.
|
||||
* - AIR golden files: `../acp-scenarios/__snapshots__/air/<scenario>.jsonl`.
|
||||
* Run `npx vitest run src/tests/v1/acp-scenarios.test.ts -u` to update them.
|
||||
* - Schema: every message is valid against the ACP schema of the SDK.
|
||||
* - Plain and Zed: the same information as origin/main
|
||||
* (`acp-scenarios/origin-main/<profile>/<scenario>.jsonl`). `compare.ts`
|
||||
* (`../acp-scenarios/origin-main/<profile>/<scenario>.jsonl`). `compare.ts`
|
||||
* allows only the documented differences. These profiles have no golden
|
||||
* files of their own. `origin-main/zed/` holds a scenario only when its
|
||||
* recording differs from the plain client.
|
||||
@@ -17,9 +17,9 @@
|
||||
* - AIR: the AIR extensions of `docs/air-extensions.md`, each fact once.
|
||||
*
|
||||
* To record the origin/main baseline again, copy `src/tests/acp-scenarios/`
|
||||
* and this file into a checkout of origin/main, and run this file there with
|
||||
* and this file to the same paths in a checkout of origin/main, and run this file there with
|
||||
* `ACP_SCENARIO_BASELINE_DIR=<dir>`. Then copy `<dir>/plain` and `<dir>/zed`
|
||||
* to `acp-scenarios/origin-main/`.
|
||||
* to `src/tests/acp-scenarios/origin-main/`.
|
||||
*/
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import * as fs from "node:fs";
|
||||
@@ -41,20 +41,20 @@ import {
|
||||
streamMessage,
|
||||
toolCall,
|
||||
toolResult,
|
||||
} from "./acp-scenarios/harness.js";
|
||||
import { SCENARIOS } from "./acp-scenarios/scenarios.js";
|
||||
} from "../acp-scenarios/harness.js";
|
||||
import { SCENARIOS } from "../acp-scenarios/scenarios.js";
|
||||
import {
|
||||
AIR_ONLY_CLAUDE_CODE_KEYS,
|
||||
AIR_ONLY_META_KEYS,
|
||||
canonical,
|
||||
compareWithBaseline,
|
||||
} from "./acp-scenarios/compare.js";
|
||||
import { EXTENSION_SESSION_UPDATES, validateRecorded } from "./acp-scenarios/schema.js";
|
||||
import { LEGACY_AIR_CUSTOM_ANSWER_KEY } from "../air-extension.js";
|
||||
} from "../acp-scenarios/compare.js";
|
||||
import { EXTENSION_SESSION_UPDATES, validateRecorded } from "../acp-scenarios/schema.js";
|
||||
import { LEGACY_AIR_CUSTOM_ANSWER_KEY } from "../../air-extension.js";
|
||||
|
||||
vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@anthropic-ai/claude-agent-sdk")>();
|
||||
const harness = await import("./acp-scenarios/harness.js");
|
||||
const harness = await import("../acp-scenarios/harness.js");
|
||||
return {
|
||||
...actual,
|
||||
query: harness.mockedQuery,
|
||||
@@ -66,7 +66,7 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
|
||||
// learns each id that `randomUUID` returns.
|
||||
vi.mock("node:crypto", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:crypto")>();
|
||||
const harness = await import("./acp-scenarios/harness.js");
|
||||
const harness = await import("../acp-scenarios/harness.js");
|
||||
return {
|
||||
...actual,
|
||||
randomUUID: (...args: Parameters<typeof actual.randomUUID>) => {
|
||||
@@ -97,7 +97,7 @@ beforeAll(async () => {
|
||||
vi.stubEnv("CLAUDE_CODE_EXECUTABLE", "/usr/bin/false");
|
||||
configDir = fs.mkdtempSync(path.join(os.tmpdir(), "acp-scenario-config-"));
|
||||
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
Agent = ClaudeAcpAgent;
|
||||
for (const profile of profiles) {
|
||||
for (const scenario of SCENARIOS) {
|
||||
@@ -189,7 +189,7 @@ function metaObjects(value: unknown, at = "$"): { at: string; meta: Record<strin
|
||||
/** The origin/main recording of a scenario. Zed has a file only where it differs from the plain client. */
|
||||
function readBaseline(profile: "plain" | "zed", scenario: string): string {
|
||||
const file = (name: string) =>
|
||||
path.join(here, "acp-scenarios", "origin-main", name, `${scenario}.jsonl`);
|
||||
path.join(here, "..", "acp-scenarios", "origin-main", name, `${scenario}.jsonl`);
|
||||
return fs.readFileSync(
|
||||
profile === "zed" && !fs.existsSync(file("zed")) ? file("plain") : file(profile),
|
||||
"utf8",
|
||||
@@ -219,7 +219,7 @@ describe.skipIf(baselineDir)("ACP scenarios", () => {
|
||||
const second = "cccccccc-0000-4000-8000-000000000003";
|
||||
const scripted = "00000000-0000-4000-8000-00000000c001";
|
||||
const version = JSON.parse(
|
||||
fs.readFileSync(path.join(here, "..", "..", "package.json"), "utf8"),
|
||||
fs.readFileSync(path.join(here, "..", "..", "..", "package.json"), "utf8"),
|
||||
).version;
|
||||
const argv = process.argv.slice(1);
|
||||
|
||||
@@ -322,7 +322,7 @@ describe.skipIf(baselineDir)("ACP scenarios", () => {
|
||||
describe("air golden files", () => {
|
||||
it.each(SCENARIOS.map((scenario) => scenario.name))("%s", async (scenario) => {
|
||||
await expect(toJsonLines(run("air", scenario).normalized)).toMatchFileSnapshot(
|
||||
path.join(here, "acp-scenarios", "__snapshots__", "air", `${scenario}.jsonl`),
|
||||
path.join(here, "..", "acp-scenarios", "__snapshots__", "air", `${scenario}.jsonl`),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -3,8 +3,8 @@ import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { SessionNotification } from "@agentclientprotocol/sdk/unstable";
|
||||
import type { Options } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
let capturedOptions: Options | undefined;
|
||||
@@ -12,7 +12,7 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
|
||||
const actual = await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
|
||||
"@anthropic-ai/claude-agent-sdk",
|
||||
);
|
||||
const { makeMockQuery } = await import("./helpers.js");
|
||||
const { makeMockQuery } = await import("../helpers.js");
|
||||
return {
|
||||
...actual,
|
||||
query: ({ options }: { options: Options }) => {
|
||||
@@ -32,8 +32,8 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("../tools.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../tools.js")>("../tools.js")),
|
||||
vi.mock("../../tools.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../../tools.js")>("../../tools.js")),
|
||||
registerHookCallback: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -47,7 +47,7 @@ describe("additionalRoots", () => {
|
||||
capturedOptions = undefined;
|
||||
tempDirs.length = 0;
|
||||
vi.resetModules();
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
agent = new ClaudeAcpAgent({
|
||||
sessionUpdate: async (_notification: SessionNotification) => {},
|
||||
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
|
||||
@@ -1,12 +1,17 @@
|
||||
import type { Mock } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import type { AuthStatus } from "../auth-status.js";
|
||||
import { fromAccountInfo, fromCliStatus, mergeAuthStatus, sameIdentity } from "../auth-status.js";
|
||||
import { DEFAULT_CONTEXT_USAGE, makeMockQuery } from "./helpers.js";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import type { Logger } from "../../logger.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import type { AuthStatus } from "../../auth-status.js";
|
||||
import {
|
||||
fromAccountInfo,
|
||||
fromCliStatus,
|
||||
mergeAuthStatus,
|
||||
sameIdentity,
|
||||
} from "../../auth-status.js";
|
||||
import { DEFAULT_CONTEXT_USAGE, makeMockQuery } from "../helpers.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
@@ -4,16 +4,16 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, Mock, vi, afterEach, beforeEach } from "vitest";
|
||||
import type { AccountInfo } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import { AcpClient } from "../acp-client.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import { AcpClient } from "../../acp-client.js";
|
||||
import {
|
||||
billsClaudeSubscription,
|
||||
CLAUDE_LOGIN_REQUIRED_MESSAGE,
|
||||
CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_MESSAGE,
|
||||
CLAUDE_SUBSCRIPTION_NOT_SUPPORTED_REASON,
|
||||
} from "../hide-claude-auth.js";
|
||||
import { SessionFailureController } from "../session-failure-extension.js";
|
||||
import { DEFAULT_CONTEXT_USAGE, makeMockQuery } from "./helpers.js";
|
||||
} from "../../hide-claude-auth.js";
|
||||
import { SessionFailureController } from "../../session-failure-extension.js";
|
||||
import { DEFAULT_CONTEXT_USAGE, makeMockQuery } from "../helpers.js";
|
||||
|
||||
const mockQuery = vi.hoisted(() => vi.fn());
|
||||
|
||||
@@ -1713,7 +1713,7 @@ describe("authorization", () => {
|
||||
describe("session failure reason field", () => {
|
||||
it("carries reason only when the publisher passes one", async () => {
|
||||
const { SessionFailureController, createSessionFailureState } =
|
||||
await import("../session-failure-extension.js");
|
||||
await import("../../session-failure-extension.js");
|
||||
const capabilities = {
|
||||
_meta: { jetbrains: { air: { version: 1, capabilities: ["sessionFailure"] } } },
|
||||
} as any;
|
||||
@@ -11,11 +11,11 @@ import * as fsSync from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { SessionInfo } from "@agentclientprotocol/sdk/unstable";
|
||||
import type { ActivityState } from "../v1/session-index/activity.js";
|
||||
import { endedTaskId, searchBackgroundTasks } from "../v1/session-index/background-tasks.js";
|
||||
import { encodeProjectPath } from "../v1/session-index/project-dirs.js";
|
||||
import { SessionIndex } from "../v1/session-index/session-index.js";
|
||||
import { SessionIndexService } from "../v1/session-index/service.js";
|
||||
import type { ActivityState } from "../../v1/session-index/activity.js";
|
||||
import { endedTaskId, searchBackgroundTasks } from "../../v1/session-index/background-tasks.js";
|
||||
import { encodeProjectPath } from "../../v1/session-index/project-dirs.js";
|
||||
import { SessionIndex } from "../../v1/session-index/session-index.js";
|
||||
import { SessionIndexService } from "../../v1/session-index/service.js";
|
||||
|
||||
let configDir: string;
|
||||
let workspace: string;
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
} from "@agentclientprotocol/sdk/unstable";
|
||||
import type { ClientCapabilities } from "@agentclientprotocol/sdk/unstable";
|
||||
import { getSessionMessages, type Options } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import type { AcpSessionNotification } from "../acp-subagents.js";
|
||||
import { ALLOW_BYPASS } from "../permissions/modes.js";
|
||||
import { NativeSubagentRuntime } from "../native-subagents.js";
|
||||
import { startupFailure } from "./helpers.js";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import type { AcpSessionNotification } from "../../acp-subagents.js";
|
||||
import { ALLOW_BYPASS } from "../../permissions/modes.js";
|
||||
import { NativeSubagentRuntime } from "../../native-subagents.js";
|
||||
import { startupFailure } from "../helpers.js";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
@@ -38,7 +38,7 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
|
||||
const actual = await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
|
||||
"@anthropic-ai/claude-agent-sdk",
|
||||
);
|
||||
const { makeMockQuery, DEFAULT_CONTEXT_USAGE } = await import("./helpers.js");
|
||||
const { makeMockQuery, DEFAULT_CONTEXT_USAGE } = await import("../helpers.js");
|
||||
return {
|
||||
...actual,
|
||||
query: (args: { prompt: unknown; options: Options }) => {
|
||||
@@ -77,8 +77,8 @@ function initializationResponse() {
|
||||
};
|
||||
}
|
||||
|
||||
vi.mock("../tools.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../tools.js")>("../tools.js");
|
||||
vi.mock("../../tools.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../tools.js")>("../../tools.js");
|
||||
return {
|
||||
...actual,
|
||||
registerHookCallback: vi.fn(),
|
||||
@@ -118,7 +118,7 @@ describe("createSession options merging", () => {
|
||||
});
|
||||
|
||||
vi.resetModules();
|
||||
const acpAgent = await import("../v1/acp-agent.js");
|
||||
const acpAgent = await import("../../v1/acp-agent.js");
|
||||
ClaudeAcpAgent = acpAgent.ClaudeAcpAgent;
|
||||
|
||||
agent = new ClaudeAcpAgent(createMockClient());
|
||||
@@ -1,10 +1,10 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { SessionNotification } from "@agentclientprotocol/sdk/unstable";
|
||||
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import { Writable } from "node:stream";
|
||||
import { EventLoopYielder, nodeToWebWritable, Pushable } from "../utils.js";
|
||||
import { EventLoopYielder, nodeToWebWritable, Pushable } from "../../utils.js";
|
||||
import {
|
||||
mockSessionState,
|
||||
successfulResultMessage,
|
||||
@@ -1,9 +1,12 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import type { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk/unstable";
|
||||
import type { FastModeDisabledReason, ModelInfo } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import {
|
||||
buildConfigOptions,
|
||||
clientSupportsBooleanConfigOptions,
|
||||
} from "../../session-config-options.js";
|
||||
import {
|
||||
createFastModeConfigOption,
|
||||
fastModeStateEnabled,
|
||||
normalizeFastModeDisabledReason,
|
||||
@@ -11,9 +14,8 @@ import {
|
||||
FAST_MODE_CONFIG_ID,
|
||||
FAST_MODE_ON,
|
||||
FAST_MODE_OFF,
|
||||
ClaudeAcpAgent,
|
||||
} from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
} from "../../session-fast-mode.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
|
||||
const MODES = {
|
||||
currentModeId: "default",
|
||||
@@ -3,13 +3,13 @@ import type { SessionNotification } from "@agentclientprotocol/sdk/unstable";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import * as path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import {
|
||||
AGENT_FILE_CHANGE_REPORT_CAPABILITY,
|
||||
type AgentFileChangeReportResult,
|
||||
} from "../file-change-audit.js";
|
||||
import type { Pushable } from "../utils.js";
|
||||
} from "../../file-change-audit.js";
|
||||
import type { Pushable } from "../../utils.js";
|
||||
|
||||
type Scenario =
|
||||
| "reported"
|
||||
@@ -89,8 +89,8 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../tools.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../tools.js")>("../tools.js")),
|
||||
vi.mock("../../tools.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../../tools.js")>("../../tools.js")),
|
||||
registerHookCallback: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -258,7 +258,7 @@ describe("native file-change report integration", () => {
|
||||
readTextFile: async () => ({ content: "" }),
|
||||
writeTextFile: async () => ({}),
|
||||
} as unknown as AcpClient;
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(client, {
|
||||
log: () => {},
|
||||
error: () => {},
|
||||
@@ -20,11 +20,11 @@ import {
|
||||
type Scenario,
|
||||
system,
|
||||
toolCall,
|
||||
} from "./acp-scenarios/harness.js";
|
||||
} from "../acp-scenarios/harness.js";
|
||||
|
||||
vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@anthropic-ai/claude-agent-sdk")>();
|
||||
const harness = await import("./acp-scenarios/harness.js");
|
||||
const harness = await import("../acp-scenarios/harness.js");
|
||||
return {
|
||||
...actual,
|
||||
query: harness.mockedQuery,
|
||||
@@ -34,7 +34,7 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
|
||||
|
||||
vi.mock("node:crypto", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:crypto")>();
|
||||
const harness = await import("./acp-scenarios/harness.js");
|
||||
const harness = await import("../acp-scenarios/harness.js");
|
||||
return {
|
||||
...actual,
|
||||
randomUUID: (...args: Parameters<typeof actual.randomUUID>) => {
|
||||
@@ -170,7 +170,7 @@ beforeAll(async () => {
|
||||
vi.stubEnv("CLAUDE_CODE_EXECUTABLE", "/usr/bin/false");
|
||||
configDir = fs.mkdtempSync(path.join(os.tmpdir(), "acp-scenario-config-"));
|
||||
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
for (const profile of runProfiles) {
|
||||
resetIds();
|
||||
const run = await unlessHung(runScenario(ClaudeAcpAgent, profile, scenario));
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { Pushable } from "../utils.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import { Pushable } from "../../utils.js";
|
||||
import {
|
||||
mockSessionState,
|
||||
successfulResultMessage,
|
||||
@@ -4,9 +4,9 @@ import { createHash, randomUUID } from "node:crypto";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { messageIdForGrouping } from "../sdk-to-acp.js";
|
||||
import { forkSession } from "../fork-session.js";
|
||||
import { encodeProjectPath } from "../v1/session-index/project-dirs.js";
|
||||
import { messageIdForGrouping } from "../../sdk-to-acp.js";
|
||||
import { forkSession } from "../../fork-session.js";
|
||||
import { encodeProjectPath } from "../../v1/session-index/project-dirs.js";
|
||||
|
||||
// Use the real SDK against temporary transcripts, without starting Claude Code.
|
||||
let root: string;
|
||||
@@ -9,9 +9,9 @@ import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Options, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { initializeClient } from "./helpers.js";
|
||||
import type { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import { initializeClient } from "../helpers.js";
|
||||
import { successfulResultMessage } from "./session-doubles.js";
|
||||
|
||||
const execFileSpy = vi.hoisted(() => vi.fn());
|
||||
@@ -37,7 +37,7 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
|
||||
const actual = await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
|
||||
"@anthropic-ai/claude-agent-sdk",
|
||||
);
|
||||
const { makeMockQuery } = await import("./helpers.js");
|
||||
const { makeMockQuery } = await import("../helpers.js");
|
||||
return {
|
||||
...actual,
|
||||
query: ({ prompt, options }: { prompt: AsyncIterable<SDKUserMessage>; options: Options }) => {
|
||||
@@ -52,8 +52,8 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../tools.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../tools.js")>("../tools.js")),
|
||||
vi.mock("../../tools.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../../tools.js")>("../../tools.js")),
|
||||
registerHookCallback: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -228,7 +228,7 @@ function scriptClaudeCode({
|
||||
|
||||
async function connect(clientCapabilities: Record<string, unknown> = {}) {
|
||||
vi.resetModules();
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const updates: Array<{ update: Record<string, any> }> = [];
|
||||
const client = {
|
||||
sessionUpdate: async (notification: { update: Record<string, any> }) => {
|
||||
@@ -1,8 +1,8 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { clearHookCallbacks, createPostToolUseHook, hasHookCallback } from "../tools.js";
|
||||
import { Pushable } from "../utils.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import { clearHookCallbacks, createPostToolUseHook, hasHookCallback } from "../../tools.js";
|
||||
import { Pushable } from "../../utils.js";
|
||||
import {
|
||||
mockSessionState,
|
||||
successfulResultMessage,
|
||||
@@ -9,8 +9,8 @@ import type {
|
||||
SessionNotification,
|
||||
} from "@agentclientprotocol/sdk/unstable";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
|
||||
type JsonObject = Record<string, unknown>;
|
||||
|
||||
@@ -9,8 +9,8 @@ import type {
|
||||
SessionNotification,
|
||||
} from "@agentclientprotocol/sdk/unstable";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
|
||||
type JsonObject = Record<string, unknown>;
|
||||
|
||||
@@ -2,17 +2,17 @@ import { randomUUID } from "node:crypto";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { McpServerStatus } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { SessionNotification } from "@agentclientprotocol/sdk/unstable";
|
||||
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import { stripLocalCommandMetadata } from "../message-filters.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import { stripLocalCommandMetadata } from "../../message-filters.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import {
|
||||
cleanMcpError,
|
||||
formatMcpStatus,
|
||||
MCP_AVAILABLE_COMMAND,
|
||||
parseMcpCommand,
|
||||
} from "../mcp-command.js";
|
||||
import { Pushable } from "../utils.js";
|
||||
import { initializeClient } from "./helpers.js";
|
||||
} from "../../mcp-command.js";
|
||||
import { Pushable } from "../../utils.js";
|
||||
import { initializeClient } from "../helpers.js";
|
||||
import {
|
||||
mockSessionState,
|
||||
successfulResultMessage,
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { ClientCapabilities } from "@agentclientprotocol/sdk/unstable";
|
||||
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { promptSuggestionsMeta, promptSuggestionText } from "../prompt-suggestions.js";
|
||||
import { Pushable } from "../utils.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import { promptSuggestionsMeta, promptSuggestionText } from "../../prompt-suggestions.js";
|
||||
import { Pushable } from "../../utils.js";
|
||||
import {
|
||||
mockSessionState,
|
||||
successfulResultMessage,
|
||||
@@ -3,10 +3,11 @@ import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ModelInfo, Options } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { ClaudeAcpAgent, FAST_MODE_CONFIG_ID } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY } from "../air-extension.js";
|
||||
import { initializeClient, makeMockQuery } from "./helpers.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import { FAST_MODE_CONFIG_ID } from "../../session-fast-mode.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import { AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY } from "../../air-extension.js";
|
||||
import { initializeClient, makeMockQuery } from "../helpers.js";
|
||||
import { successfulResultMessage, userEcho } from "./session-doubles.js";
|
||||
|
||||
const sdk = vi.hoisted(() => ({
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it, Mock, vi, afterEach, beforeEach } from "vitest";
|
||||
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import { AcpClient } from "../acp-client.js";
|
||||
import { loggableBaseUrl } from "../providers.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import { AcpClient } from "../../acp-client.js";
|
||||
import { loggableBaseUrl } from "../../providers.js";
|
||||
|
||||
const mockQuery = vi.hoisted(() =>
|
||||
vi.fn(() => ({
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import { mockSessionState } from "./session-doubles.js";
|
||||
|
||||
/** A stream that stays open, like the real one: a consumer that reaches its end
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { Pushable } from "../utils.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import { Pushable } from "../../utils.js";
|
||||
import { mockSessionState, successfulResultMessage, wrapQuery } from "./session-doubles.js";
|
||||
|
||||
const HUMAN = { kind: "human" };
|
||||
@@ -1,16 +1,16 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { SessionNotification } from "@agentclientprotocol/sdk/unstable";
|
||||
import type { ModelInfo } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { makeMockQuery } from "./helpers.js";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import { makeMockQuery } from "../helpers.js";
|
||||
|
||||
const { registerHookCallbackSpy } = vi.hoisted(() => ({
|
||||
registerHookCallbackSpy: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../tools.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../tools.js")>("../tools.js");
|
||||
vi.mock("../../tools.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../tools.js")>("../../tools.js");
|
||||
return {
|
||||
...actual,
|
||||
registerHookCallback: registerHookCallbackSpy,
|
||||
@@ -133,7 +133,7 @@ describe("session config options", () => {
|
||||
registerHookCallbackSpy.mockClear();
|
||||
|
||||
vi.resetModules();
|
||||
const acpAgent = await import("../v1/acp-agent.js");
|
||||
const acpAgent = await import("../../v1/acp-agent.js");
|
||||
ClaudeAcpAgent = acpAgent.ClaudeAcpAgent;
|
||||
|
||||
agent = new ClaudeAcpAgent(createMockClient());
|
||||
@@ -1134,7 +1134,7 @@ describe("session config options", () => {
|
||||
// model option lists it while it is current, so a client can send it back,
|
||||
// and drops it once the session switches to a listed model.
|
||||
it("lists an off-list current model until the session switches away", async () => {
|
||||
const { buildModelConfigOption } = await import("../session-model.js");
|
||||
const { buildModelConfigOption } = await import("../../session-model.js");
|
||||
const session = (agent as unknown as { sessions: Record<string, any> }).sessions[SESSION_ID];
|
||||
session.models = { ...session.models, currentModelId: "claude-offlist-9" };
|
||||
session.modelInfos = [
|
||||
@@ -7,9 +7,9 @@
|
||||
|
||||
import { vi } from "vitest";
|
||||
import { randomUUID } from "crypto";
|
||||
import { SessionBackgroundWork } from "../v1/background-work.js";
|
||||
import { PromptSuggestions } from "../prompt-suggestions.js";
|
||||
import { SessionTitles } from "../session-titles.js";
|
||||
import { SessionBackgroundWork } from "../../v1/background-work.js";
|
||||
import { PromptSuggestions } from "../../prompt-suggestions.js";
|
||||
import { SessionTitles } from "../../session-titles.js";
|
||||
|
||||
/** Stand-in agent for a `SessionTitles` whose test doesn't care about titles:
|
||||
* swallows the publish and the log, and never matches the identity check. */
|
||||
@@ -11,8 +11,8 @@ import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import type { NewSessionRequest } from "@agentclientprotocol/sdk/unstable";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
|
||||
function sse(type: string, data: object): string {
|
||||
return `event: ${type}\ndata: ${JSON.stringify({ type, ...data })}\n\n`;
|
||||
@@ -2,28 +2,28 @@ import { describe, expect, it } from "vitest";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { deriveActivity, selectCost, type ActivityState } from "../v1/session-index/activity.js";
|
||||
import { LiveSessionRegistry, type LiveRecord } from "../v1/session-index/live-registry.js";
|
||||
import { deriveActivity, selectCost, type ActivityState } from "../../v1/session-index/activity.js";
|
||||
import { LiveSessionRegistry, type LiveRecord } from "../../v1/session-index/live-registry.js";
|
||||
import {
|
||||
encodeProjectPath,
|
||||
isSessionId,
|
||||
pathAndAncestors,
|
||||
projectDirMatches,
|
||||
} from "../v1/session-index/project-dirs.js";
|
||||
} from "../../v1/session-index/project-dirs.js";
|
||||
import {
|
||||
decodeListCursor,
|
||||
encodeListCursor,
|
||||
parseListOptions,
|
||||
parseRenameSessionRequest,
|
||||
type ListScope,
|
||||
} from "../v1/session-index/service.js";
|
||||
} from "../../v1/session-index/service.js";
|
||||
import {
|
||||
effectiveTitle,
|
||||
isArchivedTitle,
|
||||
storedTitle,
|
||||
titleRecords,
|
||||
visibleTitle,
|
||||
} from "../v1/session-index/archive-title.js";
|
||||
} from "../../v1/session-index/archive-title.js";
|
||||
import {
|
||||
agentWorktreeOf,
|
||||
continuedInSessionId,
|
||||
@@ -33,11 +33,11 @@ import {
|
||||
sdkTitles,
|
||||
transcriptAgentName,
|
||||
type TranscriptFacts,
|
||||
} from "../v1/session-index/transcript-scan.js";
|
||||
import { firstPrompt } from "../v1/session-index/first-prompt.js";
|
||||
import { DirListings, statFiles } from "../v1/session-index/dir-listing.js";
|
||||
import { SessionIndex, type IndexRow } from "../v1/session-index/session-index.js";
|
||||
import { changeSignature, sessionInfoOf } from "../v1/session-index/session-info.js";
|
||||
} from "../../v1/session-index/transcript-scan.js";
|
||||
import { firstPrompt } from "../../v1/session-index/first-prompt.js";
|
||||
import { DirListings, statFiles } from "../../v1/session-index/dir-listing.js";
|
||||
import { SessionIndex, type IndexRow } from "../../v1/session-index/session-index.js";
|
||||
import { changeSignature, sessionInfoOf } from "../../v1/session-index/session-info.js";
|
||||
|
||||
const SESSION = "11111111-1111-4111-8111-111111111111";
|
||||
const lines = (...entries: object[]) => entries.map((entry) => JSON.stringify(entry)).join("\n");
|
||||
@@ -16,14 +16,14 @@ import {
|
||||
listSessions,
|
||||
} from "@anthropic-ai/claude-agent-sdk";
|
||||
import { spawn } from "node:child_process";
|
||||
import { ClaudeAcpAgent, v1AgentApp } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { ClaudeAcpAgent, v1AgentApp } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import {
|
||||
client as acpClient,
|
||||
type InitializeResponse,
|
||||
type SessionInfo,
|
||||
} from "@agentclientprotocol/sdk/unstable";
|
||||
import { encodeProjectPath } from "../v1/session-index/project-dirs.js";
|
||||
import { encodeProjectPath } from "../../v1/session-index/project-dirs.js";
|
||||
import {
|
||||
archiveInsteadOfDelete,
|
||||
archiveTo,
|
||||
@@ -31,25 +31,25 @@ import {
|
||||
parseBatchGetSessionsRequest,
|
||||
SessionIndexService,
|
||||
writeCustomTitleSidecar,
|
||||
} from "../v1/session-index/service.js";
|
||||
import { scanTranscriptFile } from "../v1/session-index/transcript-scan.js";
|
||||
import { repositoryWorktrees } from "../v1/session-index/worktrees.js";
|
||||
} from "../../v1/session-index/service.js";
|
||||
import { scanTranscriptFile } from "../../v1/session-index/transcript-scan.js";
|
||||
import { repositoryWorktrees } from "../../v1/session-index/worktrees.js";
|
||||
import {
|
||||
ListSubscriptions,
|
||||
MAX_SUBSCRIPTIONS,
|
||||
parseListSubscribeRequest,
|
||||
parseListUnsubscribeRequest,
|
||||
type ListChanges,
|
||||
} from "../v1/session-index/list-subscriptions.js";
|
||||
import { SessionTitles } from "../session-titles.js";
|
||||
import { initializeClient } from "./helpers.js";
|
||||
} from "../../v1/session-index/list-subscriptions.js";
|
||||
import { SessionTitles } from "../../session-titles.js";
|
||||
import { initializeClient } from "../helpers.js";
|
||||
import {
|
||||
mockSessionState,
|
||||
successfulResultMessage,
|
||||
userEcho,
|
||||
wrapQuery,
|
||||
} from "./session-doubles.js";
|
||||
import { Pushable } from "../utils.js";
|
||||
import { Pushable } from "../../utils.js";
|
||||
|
||||
vi.mock("node:fs/promises", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof fs>();
|
||||
@@ -1854,7 +1854,7 @@ describe("_session/list/subscribe", () => {
|
||||
});
|
||||
|
||||
it("clears the failed turn of a session that runs here when a new turn starts", async () => {
|
||||
const { noteSessionState } = await import("../v1/session-index/connection.js");
|
||||
const { noteSessionState } = await import("../../v1/session-index/connection.js");
|
||||
const session = { lastTurnFailed: true } as any;
|
||||
noteSessionState(session, "idle", "running");
|
||||
expect(session.lastTurnFailed).toBe(false);
|
||||
@@ -2,9 +2,9 @@ import { describe, it, expect } from "vitest";
|
||||
import { randomUUID } from "crypto";
|
||||
import { RequestError, SessionNotification } from "@agentclientprotocol/sdk/unstable";
|
||||
import { query, getSessionMessages } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import { AcpClient } from "../acp-client.js";
|
||||
import { Pushable } from "../utils.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import { AcpClient } from "../../acp-client.js";
|
||||
import { Pushable } from "../../utils.js";
|
||||
import type { SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
|
||||
function createMockClient(
|
||||
@@ -1,8 +1,8 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import type { SessionNotification } from "@agentclientprotocol/sdk/unstable";
|
||||
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { makeMockQuery } from "./helpers.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import { makeMockQuery } from "../helpers.js";
|
||||
|
||||
const SESSION_ID = "test-session-id";
|
||||
|
||||
@@ -2,10 +2,10 @@ import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import { appendTitleContext, sanitizeTitle } from "../session-titles.js";
|
||||
import { Pushable } from "../utils.js";
|
||||
import { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import { appendTitleContext, sanitizeTitle } from "../../session-titles.js";
|
||||
import { Pushable } from "../../utils.js";
|
||||
import { getSessionInfo } from "@anthropic-ai/claude-agent-sdk";
|
||||
import {
|
||||
mockSessionState,
|
||||
@@ -3,14 +3,14 @@ import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { ClientCapabilities } from "@agentclientprotocol/sdk/unstable";
|
||||
import { AcpClient } from "../acp-client.js";
|
||||
import { toAcpNotifications, ToolUseCache } from "../sdk-to-acp.js";
|
||||
import { ClientCapabilities as ToolCallCapabilities } from "../tool-calls/client-capabilities.js";
|
||||
import { backgroundedBashToolCall, withRunningTerminal } from "../tool-calls/background.js";
|
||||
import { ChangedMetaFilter } from "../v1/tool-calls/changed-meta-filter.js";
|
||||
import { ToolCallFieldTracker } from "../tool-calls/field-tracker.js";
|
||||
import { AcpToolCallRenderer, mcpToolName } from "../tool-calls/renderer.js";
|
||||
import { changedTaskPlanEntries, createTaskHook, type TaskState } from "../tools.js";
|
||||
import { AcpClient } from "../../acp-client.js";
|
||||
import { toAcpNotifications, ToolUseCache } from "../../sdk-to-acp.js";
|
||||
import { ClientCapabilities as ToolCallCapabilities } from "../../tool-calls/client-capabilities.js";
|
||||
import { backgroundedBashToolCall, withRunningTerminal } from "../../tool-calls/background.js";
|
||||
import { ChangedMetaFilter } from "../../v1/tool-calls/changed-meta-filter.js";
|
||||
import { ToolCallFieldTracker } from "../../tool-calls/field-tracker.js";
|
||||
import { AcpToolCallRenderer, mcpToolName } from "../../tool-calls/renderer.js";
|
||||
import { changedTaskPlanEntries, createTaskHook, type TaskState } from "../../tools.js";
|
||||
|
||||
const logger = { log: () => {}, error: () => {} };
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { SessionNotification } from "@agentclientprotocol/sdk/unstable";
|
||||
import type { ClaudeAcpAgent } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import type { TurnEvents } from "../v1/turn-events.js";
|
||||
import { DEFAULT_CONTEXT_USAGE } from "./helpers.js";
|
||||
import type { ClaudeAcpAgent } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import type { TurnEvents } from "../../v1/turn-events.js";
|
||||
import { DEFAULT_CONTEXT_USAGE } from "../helpers.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
const mockQuery = vi.hoisted(() => vi.fn());
|
||||
@@ -27,8 +27,8 @@ vi.mock("node:child_process", async () => ({
|
||||
(args[args.length - 1] as (error: Error) => void)(new Error("no CLI in tests")),
|
||||
}));
|
||||
|
||||
vi.mock("../tools.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../tools.js")>("../tools.js")),
|
||||
vi.mock("../../tools.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../../tools.js")>("../../tools.js")),
|
||||
registerHookCallback: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -199,7 +199,7 @@ describe("turn events", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
onSessionUpdate = () => {};
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
agent = new ClaudeAcpAgent(
|
||||
{
|
||||
sessionUpdate: async (notification: SessionNotification) => onSessionUpdate(notification),
|
||||
@@ -4,8 +4,8 @@ import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { ClientCapabilities } from "@agentclientprotocol/sdk/unstable";
|
||||
import type { ModelInfo, Query } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import type { ClaudeAcpAgent as ClaudeAcpAgentType } from "../../v1/acp-agent.js";
|
||||
import type { AcpClient } from "../../acp-client.js";
|
||||
import {
|
||||
createUltracodeConfigOption,
|
||||
modelSupportsUltracode,
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
ULTRACODE_ON,
|
||||
ultracodeSupported,
|
||||
ultracodeWorkflowsEnabled,
|
||||
} from "../session-ultracode.js";
|
||||
import { makeMockQuery } from "./helpers.js";
|
||||
} from "../../session-ultracode.js";
|
||||
import { makeMockQuery } from "../helpers.js";
|
||||
|
||||
const { querySpy } = vi.hoisted(() => ({ querySpy: vi.fn() }));
|
||||
|
||||
@@ -169,7 +169,7 @@ describe("ultracode session config option", () => {
|
||||
...(opts.getSettings ? { getSettings: opts.getSettings } : {}),
|
||||
}),
|
||||
);
|
||||
const { ClaudeAcpAgent } = await import("../v1/acp-agent.js");
|
||||
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
|
||||
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent({
|
||||
sessionUpdate: async () => {},
|
||||
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
|
||||
@@ -11,29 +11,29 @@
|
||||
* - Tool calls: the first report of each tool call has a title. v2 has no
|
||||
* `tool_call`, so a report of a tool call that the client has not seen
|
||||
* creates it, where v1 drops it.
|
||||
* - Golden files: `acp-scenarios/__snapshots__/v2/<scenario>.jsonl` for each
|
||||
* - Golden files: `../acp-scenarios/__snapshots__/v2/<scenario>.jsonl` for each
|
||||
* passing scenario, every message of the run. Run
|
||||
* `npx vitest run src/tests/acp-scenarios-v2.test.ts -u` to update them.
|
||||
* `npx vitest run src/tests/v2/acp-scenarios-v2.test.ts -u` to update them.
|
||||
*/
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { AgentHandle } from "../v2/agent.js";
|
||||
import type { AgentHandle } from "../../v2/agent.js";
|
||||
import {
|
||||
resetIds,
|
||||
runScenarioV2,
|
||||
type ScenarioRunV2,
|
||||
type WireRecorded,
|
||||
} from "./acp-scenarios/harness.js";
|
||||
import { SCENARIOS } from "./acp-scenarios/scenarios.js";
|
||||
import { canonical } from "./acp-scenarios/compare.js";
|
||||
import { validateV2Message } from "./acp-scenarios/schema.js";
|
||||
} from "../acp-scenarios/harness.js";
|
||||
import { SCENARIOS } from "../acp-scenarios/scenarios.js";
|
||||
import { canonical } from "../acp-scenarios/compare.js";
|
||||
import { validateV2Message } from "../acp-scenarios/schema.js";
|
||||
|
||||
vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@anthropic-ai/claude-agent-sdk")>();
|
||||
const harness = await import("./acp-scenarios/harness.js");
|
||||
const harness = await import("../acp-scenarios/harness.js");
|
||||
return {
|
||||
...actual,
|
||||
query: harness.mockedQuery,
|
||||
@@ -46,7 +46,7 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
|
||||
// learns each id that `randomUUID` returns.
|
||||
vi.mock("node:crypto", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:crypto")>();
|
||||
const harness = await import("./acp-scenarios/harness.js");
|
||||
const harness = await import("../acp-scenarios/harness.js");
|
||||
return {
|
||||
...actual,
|
||||
randomUUID: (...args: Parameters<typeof actual.randomUUID>) => {
|
||||
@@ -84,7 +84,7 @@ beforeAll(async () => {
|
||||
vi.stubEnv("CLAUDE_CODE_EXECUTABLE", "/usr/bin/false");
|
||||
configDir = fs.mkdtempSync(path.join(os.tmpdir(), "acp-scenario-config-"));
|
||||
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
|
||||
const { acpProtocolRouter } = await import("../serve.js");
|
||||
const { acpProtocolRouter } = await import("../../serve.js");
|
||||
for (const scenario of SCENARIOS) {
|
||||
resetIds();
|
||||
const agents: { dispose(): Promise<void> }[] = [];
|
||||
@@ -168,7 +168,7 @@ describe("ACP v2 scenarios", () => {
|
||||
(scenario) => statusOf(scenario) === PASSES,
|
||||
);
|
||||
const golden = (scenario: string) =>
|
||||
path.join(here, "acp-scenarios", "__snapshots__", "v2", `${scenario}.jsonl`);
|
||||
path.join(here, "..", "acp-scenarios", "__snapshots__", "v2", `${scenario}.jsonl`);
|
||||
it.each(passing)("%s", async (scenario) => {
|
||||
await expect(toJsonLines(run(scenario).normalized)).toMatchFileSnapshot(golden(scenario));
|
||||
});
|
||||
@@ -18,25 +18,25 @@ import {
|
||||
type Options,
|
||||
type SDKUserMessage,
|
||||
} from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { AuthStatusUpdateNotification } from "../auth-status.js";
|
||||
import { clearContextWindowCache } from "../context-window.js";
|
||||
import { v2DiffContent } from "../diff.js";
|
||||
import { type AcpProtocols, acpProtocolRouter, acpProtocolsFromEnv } from "../serve.js";
|
||||
import { v1PermissionResponse, v2PermissionRequest } from "../v2/permissions.js";
|
||||
import { v1PromptRequest } from "../v2/prompt.js";
|
||||
import { V2Terminals } from "../v2/terminal.js";
|
||||
import { V2ToolCallMeta } from "../v2/tool-call-meta.js";
|
||||
import { v2RestartedToolCall } from "../v2/tool-call.js";
|
||||
import { v2SessionUpdate } from "../v2/session-update.js";
|
||||
import type { AgentHandle } from "../v2/agent.js";
|
||||
import type { AuthStatusUpdateNotification } from "../../auth-status.js";
|
||||
import { clearContextWindowCache } from "../../context-window.js";
|
||||
import { v2DiffContent } from "../../diff.js";
|
||||
import { type AcpProtocols, acpProtocolRouter, acpProtocolsFromEnv } from "../../serve.js";
|
||||
import { v1PermissionResponse, v2PermissionRequest } from "../../v2/permissions.js";
|
||||
import { v1PromptRequest } from "../../v2/prompt.js";
|
||||
import { V2Terminals } from "../../v2/terminal.js";
|
||||
import { V2ToolCallMeta } from "../../v2/tool-call-meta.js";
|
||||
import { v2RestartedToolCall } from "../../v2/tool-call.js";
|
||||
import { v2SessionUpdate } from "../../v2/session-update.js";
|
||||
import type { AgentHandle } from "../../v2/agent.js";
|
||||
import {
|
||||
v1ForkSessionRequest,
|
||||
v1NewSessionRequest,
|
||||
v1SetSessionConfigOptionRequest,
|
||||
v2ConfigOptions,
|
||||
} from "../v2/session-requests.js";
|
||||
import { startupFailure } from "./helpers.js";
|
||||
import packageJson from "../../package.json" with { type: "json" };
|
||||
} from "../../v2/session-requests.js";
|
||||
import { startupFailure } from "../helpers.js";
|
||||
import packageJson from "../../../package.json" with { type: "json" };
|
||||
|
||||
const execFileSpy = vi.hoisted(() => vi.fn());
|
||||
|
||||
@@ -118,7 +118,7 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
|
||||
const actual = await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
|
||||
"@anthropic-ai/claude-agent-sdk",
|
||||
);
|
||||
const { makeMockQuery } = await import("./helpers.js");
|
||||
const { makeMockQuery } = await import("../helpers.js");
|
||||
return {
|
||||
...actual,
|
||||
query: ({ prompt, options }: { prompt: AsyncIterable<SDKUserMessage>; options: Options }) => {
|
||||
@@ -161,8 +161,8 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../tools.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../tools.js")>("../tools.js")),
|
||||
vi.mock("../../tools.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../../tools.js")>("../../tools.js")),
|
||||
registerHookCallback: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import * as v2 from "@agentclientprotocol/sdk/experimental/v2/unstable";
|
||||
import { nodeToWebReadable, nodeToWebWritable } from "../utils.js";
|
||||
import { nodeToWebReadable, nodeToWebWritable } from "../../utils.js";
|
||||
|
||||
/**
|
||||
* The ACP v2 core (`src/v2/`) against the bundled Claude Code
|
||||
@@ -20,10 +20,10 @@ import {
|
||||
type Options,
|
||||
type SDKUserMessage,
|
||||
} from "@anthropic-ai/claude-agent-sdk";
|
||||
import { acpProtocolRouter } from "../serve.js";
|
||||
import { SettingsManager } from "../settings.js";
|
||||
import type { AgentHandle } from "../v2/agent.js";
|
||||
import { StoppedWork } from "../v2/stopped-work.js";
|
||||
import { acpProtocolRouter } from "../../serve.js";
|
||||
import { SettingsManager } from "../../settings.js";
|
||||
import type { AgentHandle } from "../../v2/agent.js";
|
||||
import { StoppedWork } from "../../v2/stopped-work.js";
|
||||
|
||||
/** One Claude Code process: the prompts it reads and the messages it sends. */
|
||||
type FakeClaudeCode = {
|
||||
@@ -160,8 +160,8 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../diff.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../diff.js")>("../diff.js");
|
||||
vi.mock("../../diff.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../diff.js")>("../../diff.js");
|
||||
return {
|
||||
...actual,
|
||||
previewPatchContent: async (...args: Parameters<typeof actual.previewPatchContent>) => {
|
||||
@@ -172,14 +172,16 @@ vi.mock("../diff.js", async () => {
|
||||
});
|
||||
|
||||
// The subagents that a replayed transcript launched, as their metadata files name them.
|
||||
vi.mock("../subagent-history.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../subagent-history.js")>("../subagent-history.js")),
|
||||
vi.mock("../../subagent-history.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../../subagent-history.js")>(
|
||||
"../../subagent-history.js",
|
||||
)),
|
||||
subagentHistory: vi.fn(async () => ({ agents: sdk.subagents })),
|
||||
readSubagentMessages: vi.fn(async () => []),
|
||||
}));
|
||||
|
||||
vi.mock("../tools.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../tools.js")>("../tools.js")),
|
||||
vi.mock("../../tools.js", async () => ({
|
||||
...(await vi.importActual<typeof import("../../tools.js")>("../../tools.js")),
|
||||
registerHookCallback: vi.fn(),
|
||||
}));
|
||||
|
||||
54
src/tools.ts
54
src/tools.ts
@@ -1,9 +1,4 @@
|
||||
import type {
|
||||
PlanEntry,
|
||||
ToolCallContent,
|
||||
ToolCallLocation,
|
||||
ToolKind,
|
||||
} from "@agentclientprotocol/sdk/unstable";
|
||||
import type { PlanEntry } from "@agentclientprotocol/sdk/unstable";
|
||||
import { HookCallback } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type {
|
||||
TaskCreateInput,
|
||||
@@ -12,56 +7,9 @@ import type {
|
||||
TaskUpdateInput,
|
||||
TaskUpdateOutput,
|
||||
} from "@anthropic-ai/claude-agent-sdk/sdk-tools.js";
|
||||
import { ClientCapabilities } from "./tool-calls/client-capabilities.js";
|
||||
import type { ToolResultBlock } from "./tool-calls/content.js";
|
||||
import { AcpToolCallRenderer, type RenderedResult } from "./tool-calls/renderer.js";
|
||||
|
||||
export { markdownEscape, toDisplayPath } from "./tool-calls/content.js";
|
||||
|
||||
/**
|
||||
* The title, kind, content, and locations of a tool use, for a client with the
|
||||
* given terminal and patch capabilities. The {@link AcpToolCallRenderer} builds
|
||||
* them from the facts of the tool reporter.
|
||||
*/
|
||||
export function toolInfoFromToolUse(
|
||||
toolUse: any,
|
||||
supportsTerminalOutput: boolean = false,
|
||||
cwd?: string,
|
||||
supportsDiffPatch: boolean = false,
|
||||
): {
|
||||
title: string;
|
||||
kind: ToolKind;
|
||||
content: ToolCallContent[];
|
||||
locations?: ToolCallLocation[];
|
||||
} {
|
||||
const renderer = new AcpToolCallRenderer(
|
||||
new ClientCapabilities(supportsTerminalOutput, false, supportsDiffPatch),
|
||||
);
|
||||
return renderer.toolInfo({ id: toolUse?.id, name: toolUse?.name, input: toolUse?.input }, cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
* The result fields of a tool result, for a client with the given terminal
|
||||
* capabilities. The {@link AcpToolCallRenderer} builds them from the facts of
|
||||
* the tool reporter.
|
||||
*/
|
||||
export function toolUpdateFromToolResult(
|
||||
toolResult: ToolResultBlock,
|
||||
toolUse: any | undefined,
|
||||
supportsTerminalOutput: boolean = false,
|
||||
toolUseResult?: unknown,
|
||||
preferTerminalOutputDelta: boolean = false,
|
||||
): RenderedResult {
|
||||
const renderer = new AcpToolCallRenderer(
|
||||
new ClientCapabilities(supportsTerminalOutput, preferTerminalOutputDelta),
|
||||
);
|
||||
return renderer.resultFields(
|
||||
{ id: toolUse?.id, name: toolUse?.name ?? "", input: toolUse?.input },
|
||||
toolResult as Parameters<AcpToolCallRenderer["resultFields"]>[1],
|
||||
toolUseResult,
|
||||
);
|
||||
}
|
||||
|
||||
export type ClaudePlanEntry = {
|
||||
content: string;
|
||||
status: "pending" | "in_progress" | "completed";
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
LoadSessionResponse,
|
||||
LogoutRequest,
|
||||
methods,
|
||||
ndJsonStream,
|
||||
NewSessionRequest,
|
||||
NewSessionResponse,
|
||||
PromptRequest,
|
||||
@@ -312,14 +311,7 @@ import {
|
||||
isFinalToolCallStatus,
|
||||
type ToolCallUpdate,
|
||||
} from "../tool-calls/renderer.js";
|
||||
import {
|
||||
messageLoopYielder,
|
||||
nodeToWebReadable,
|
||||
nodeToWebWritable,
|
||||
Pushable,
|
||||
raceTimeoutAndAbort,
|
||||
unreachable,
|
||||
} from "../utils.js";
|
||||
import { messageLoopYielder, Pushable, raceTimeoutAndAbort, unreachable } from "../utils.js";
|
||||
import {
|
||||
acceptedPlanToolResult,
|
||||
ExitPlanCoordinator,
|
||||
@@ -330,7 +322,6 @@ import { AGENT_CONFIG_ID, DEFAULT_AGENT_ID } from "../session-config-ids.js";
|
||||
import { type PersistedTaskNotification, taskNotificationsOf } from "../task-notifications.js";
|
||||
import { formatUsageResponse, isUsageCommandText, parseUsageResponse } from "../usage-markdown.js";
|
||||
|
||||
export { DEFAULT_AGENT_ID } from "../session-config-ids.js";
|
||||
import { parseMcpCommand, runMcpCommand } from "../mcp-command.js";
|
||||
import { configuredSettings, resolveThinkingConfig } from "../session-env.js";
|
||||
import {
|
||||
@@ -408,33 +399,6 @@ import { STARTUP_FAILURE_RESULTS_ENV, startupFailureError } from "../startup-fai
|
||||
import { recordStreamedDelta, unstreamedRemainder, type StreamedBlock } from "../streamed-text.js";
|
||||
import { subagentHookOptions } from "../subagent-configuration.js";
|
||||
|
||||
export {
|
||||
AGENT_CONFIG_ID,
|
||||
BUILTIN_AGENT_NAMES,
|
||||
buildConfigOptions,
|
||||
clientSupportsBooleanConfigOptions,
|
||||
clientSupportsRecommendedConfigValue,
|
||||
type ConfigOptionPresentation,
|
||||
discoverCustomAgents,
|
||||
MODE_CONFIG_ID,
|
||||
} from "../session-config-options.js";
|
||||
export {
|
||||
createFastModeConfigOption,
|
||||
FAST_MODE_CONFIG_ID,
|
||||
FAST_MODE_OFF,
|
||||
FAST_MODE_ON,
|
||||
fastModeStateEnabled,
|
||||
type FastModeOptionState,
|
||||
normalizeFastModeDisabledReason,
|
||||
resolveFastModeEnabled,
|
||||
} from "../session-fast-mode.js";
|
||||
export { EFFORT_CONFIG_ID, settingsEffortForModel } from "../session-effort.js";
|
||||
export {
|
||||
applyAvailableModelsAllowlist,
|
||||
matchResumedModel,
|
||||
MODEL_CONFIG_ID,
|
||||
resolveModelPreference,
|
||||
} from "../session-model.js";
|
||||
import type { AcpClient } from "../acp-client.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import {
|
||||
@@ -456,28 +420,6 @@ import {
|
||||
} from "../sdk-to-acp.js";
|
||||
import { getAvailableSlashCommands } from "../slash-commands.js";
|
||||
|
||||
// Moved to their own modules; kept as exports for consumers that deep-import dist/acp-agent.js
|
||||
// (re-exported by `../acp-agent.ts`).
|
||||
export { AUTONOMOUS_RESULT_ORIGINS, isAbortedResult, isLoginRequiredText } from "../result-end.js";
|
||||
export type { AcpClient } from "../acp-client.js";
|
||||
export type { Logger } from "../logger.js";
|
||||
export {
|
||||
isLocalCommandMetadata,
|
||||
isReplayHiddenMetaMessage,
|
||||
isSyntheticLoginMessage,
|
||||
stripLocalCommandMetadata,
|
||||
} from "../message-filters.js";
|
||||
export type { NewSessionMeta, SDKMessageFilter } from "../new-session-meta.js";
|
||||
export { promptToClaude } from "../prompt-to-claude.js";
|
||||
export {
|
||||
messageIdForGrouping,
|
||||
streamEventToAcpNotifications,
|
||||
toAcpNotifications,
|
||||
type StreamedToolInputCache,
|
||||
type ToolUseCache,
|
||||
} from "../sdk-to-acp.js";
|
||||
export { getAvailableSlashCommands } from "../slash-commands.js";
|
||||
|
||||
/** Claude CLI emits this synthetic result when an interrupted cycle ends on
|
||||
* queued user input before producing any assistant content. It is a hand-off
|
||||
* marker, not the outcome of the replacement prompt that may already be
|
||||
@@ -1746,8 +1688,6 @@ export function computeSessionFingerprint(params: {
|
||||
* cannot drift from the errors this agent throws. */
|
||||
const AUTH_REQUIRED_CODE = RequestError.authRequired().code;
|
||||
|
||||
export type { ToolUpdateMeta } from "../tool-calls/renderer.js";
|
||||
|
||||
const SUBAGENT_TRANSCRIPT_CAPABILITY = "subagent-transcript";
|
||||
|
||||
function supportsSubagentTranscript(capabilities?: ClientCapabilities | null): boolean {
|
||||
@@ -11319,15 +11259,6 @@ export function v1AgentApp(
|
||||
return sessionIndex.onSessionIndexRequests(app, () => agent);
|
||||
}
|
||||
|
||||
/** Serves ACP v1 on stdio. */
|
||||
export function runAcp(logger?: Logger) {
|
||||
const stream = ndJsonStream(nodeToWebWritable(process.stdout), nodeToWebReadable(process.stdin));
|
||||
let agent!: ClaudeAcpAgent;
|
||||
// `connect` runs the connect handlers before it returns, so `agent` is set.
|
||||
const connection = v1AgentApp(logger, (created) => (agent = created)).connect(stream);
|
||||
return { connection, agent };
|
||||
}
|
||||
|
||||
/**
|
||||
* What a `command_lifecycle` "started" frame says started the command: a
|
||||
* ScheduleWakeup (dynamic `/loop`) wakeup, with the time it was set for in
|
||||
|
||||
@@ -35,40 +35,36 @@
|
||||
* parent, and a notice at the end says that subagent history is missing, as
|
||||
* it does for a child whose transcript cannot be read.
|
||||
*/
|
||||
import * as fs from "node:fs";
|
||||
import type * as v2 from "@agentclientprotocol/sdk/experimental/v2/unstable";
|
||||
import type { SessionConfigOption } from "@agentclientprotocol/sdk/unstable";
|
||||
import type { SessionMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { Logger } from "../logger.js";
|
||||
import type { AcpSessionNotification, SubagentState } from "../acp-subagents.js";
|
||||
import type { AcpSessionNotification } from "../acp-subagents.js";
|
||||
import {
|
||||
childModelOption,
|
||||
HANDBACK_TOOL,
|
||||
handbackMessage,
|
||||
isNativeSubagentControlTool,
|
||||
launchReply,
|
||||
linkedSessionMessages,
|
||||
nativeSubagentState,
|
||||
responseModelEntry,
|
||||
rfdEndState,
|
||||
rfdLabels,
|
||||
type ModelEntry,
|
||||
type SubagentModels,
|
||||
type WorkState,
|
||||
} from "../native-subagents.js";
|
||||
import {
|
||||
readSubagentMessages,
|
||||
subagentHistory,
|
||||
subagentLaunchIds,
|
||||
type SubagentHistory,
|
||||
type SubagentRecord,
|
||||
} from "../subagent-history.js";
|
||||
import {
|
||||
deliveredCall,
|
||||
firstResponseModel,
|
||||
isDelivery,
|
||||
isDirectory,
|
||||
missingHistoryNotice,
|
||||
replaySubagentTerminalState,
|
||||
ReplayTranscript,
|
||||
replayEndState,
|
||||
textOf,
|
||||
type ReplayChild as SharedReplayChild,
|
||||
} from "../subagent-replay.js";
|
||||
import { taskNotificationsOf } from "../task-notifications.js";
|
||||
import type { V2UpdateTranslator } from "./update-translator.js";
|
||||
@@ -96,53 +92,11 @@ export interface LiveChildren {
|
||||
|
||||
const NO_LIVE_CHILDREN: LiveChildren = { tracks: () => false, configuration: () => undefined };
|
||||
|
||||
type ReplayChild = {
|
||||
launchId: string;
|
||||
sessionId: string;
|
||||
record: SubagentRecord;
|
||||
parentSessionId: string;
|
||||
input: Record<string, unknown>;
|
||||
/** How the launch's tool result ended a foreground child's run. */
|
||||
launchEnd?: "completed" | "failed" | "cancelled";
|
||||
/** The state of the child's last persisted task notification. */
|
||||
notified?: SubagentState;
|
||||
announced: boolean;
|
||||
promptSent: boolean;
|
||||
/** Whether it handed a report to its parent with a SubagentHandback call. */
|
||||
handedBack?: boolean;
|
||||
/** The `model` option entry that the replay sent. */
|
||||
model?: ModelEntry;
|
||||
/** The model id of its latest replayed response. */
|
||||
responseModel?: string;
|
||||
/** The `cwd` it was announced with, when not its parent's. */
|
||||
cwd?: string;
|
||||
output?: SessionOutput;
|
||||
};
|
||||
|
||||
type SendMessageCall = {
|
||||
toolUseId: string;
|
||||
to: string;
|
||||
message: string;
|
||||
senderSessionId: string;
|
||||
/** The child the replay delivered the message to. */
|
||||
recipientSessionId?: string;
|
||||
/** The replay reached the call before the delivery, so it stays a tool call. */
|
||||
shownAsToolCall: boolean;
|
||||
/** The sender's view of the message went out, at the call. */
|
||||
outgoingSent: boolean;
|
||||
};
|
||||
/** A replayed child, with the output of its session once announced. */
|
||||
type ReplayChild = SharedReplayChild & { output?: SessionOutput };
|
||||
|
||||
export class SubagentReplay {
|
||||
/** The children, by the id of the call that launched each. */
|
||||
private readonly children = new Map<string, ReplayChild>();
|
||||
private readonly sendMessages: SendMessageCall[] = [];
|
||||
/** The SubagentHandback calls, with their report and whether their result failed. */
|
||||
private readonly handbacks = new Map<
|
||||
string,
|
||||
{ message: string; failed: boolean; sent: boolean }
|
||||
>();
|
||||
/** Launches without metadata, which the replay leaves out. */
|
||||
private readonly omitted = new Set<string>();
|
||||
private readonly transcript: ReplayTranscript<ReplayChild>;
|
||||
/** Children whose transcript could not be read: only their association is replayed. */
|
||||
private withoutHistory = 0;
|
||||
private rootOutput: SessionOutput | undefined;
|
||||
@@ -153,7 +107,9 @@ export class SubagentReplay {
|
||||
/** The transcript of each child, by agent id. */
|
||||
private readonly transcripts: Map<string, SessionMessage[]>,
|
||||
private readonly live: LiveChildren,
|
||||
) {}
|
||||
) {
|
||||
this.transcript = new ReplayTranscript(history);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the subagent history that `messages`, the session's transcript,
|
||||
@@ -183,7 +139,7 @@ export class SubagentReplay {
|
||||
/** The hooks of the replay of the root session's history (`messages`). */
|
||||
hooks(output: SessionOutput, messages: SessionMessage[]): ReplayHooks {
|
||||
this.rootOutput = output;
|
||||
this.scan(messages, output.sessionId);
|
||||
this.transcript.scan(messages, output.sessionId);
|
||||
return this.ownerHooks(output, undefined);
|
||||
}
|
||||
|
||||
@@ -192,24 +148,24 @@ export class SubagentReplay {
|
||||
* stream, and the notice when subagent history is missing.
|
||||
*/
|
||||
finish(): void {
|
||||
const announced = [...this.children.values()].filter((child) => child.announced);
|
||||
const announced = [...this.transcript.children.values()].filter((child) => child.announced);
|
||||
for (const child of [...announced].reverse()) {
|
||||
this.host.send({
|
||||
sessionId: child.parentSessionId,
|
||||
update: {
|
||||
sessionUpdate: "subagent_update",
|
||||
sessionId: child.sessionId,
|
||||
state: endState(child) as v2.StateUpdate,
|
||||
state: replayEndState(child) as v2.StateUpdate,
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const child of announced) {
|
||||
this.host.send({
|
||||
sessionId: child.sessionId,
|
||||
update: { sessionUpdate: "state_update", ...endState(child) } as v2.SessionUpdate,
|
||||
update: { sessionUpdate: "state_update", ...replayEndState(child) } as v2.SessionUpdate,
|
||||
});
|
||||
}
|
||||
const missing = this.omitted.size + this.withoutHistory;
|
||||
const missing = this.transcript.omitted.size + this.withoutHistory;
|
||||
if (missing > 0) {
|
||||
this.rootOutput?.emitV1([
|
||||
{ sessionId: this.host.sessionId, update: missingHistoryNotice(missing) },
|
||||
@@ -227,7 +183,7 @@ export class SubagentReplay {
|
||||
* the session's model list.
|
||||
*/
|
||||
afterResponse(): void {
|
||||
for (const child of this.children.values()) {
|
||||
for (const child of this.transcript.children.values()) {
|
||||
if (!child.announced) continue;
|
||||
if (!this.live.tracks(child.sessionId)) this.sendEndState(child);
|
||||
this.sendConfiguration(child);
|
||||
@@ -249,7 +205,7 @@ export class SubagentReplay {
|
||||
}
|
||||
|
||||
private sendEndState(child: ReplayChild): void {
|
||||
const state = endState(child);
|
||||
const state = replayEndState(child);
|
||||
this.host.send({
|
||||
sessionId: child.parentSessionId,
|
||||
update: {
|
||||
@@ -280,63 +236,6 @@ export class SubagentReplay {
|
||||
* Registers the launches, the launch results, the SendMessage calls and the
|
||||
* SubagentHandback calls in `messages`, of the session `ownerSessionId`.
|
||||
*/
|
||||
private scan(messages: readonly SessionMessage[], ownerSessionId: string): void {
|
||||
for (const message of messages) {
|
||||
for (const block of contentBlocks(message)) {
|
||||
if (block.type === "tool_use" && typeof block.id === "string") {
|
||||
if (isNativeSubagentControlTool(block.name)) {
|
||||
this.registerLaunch(block.id, inputOf(block), ownerSessionId);
|
||||
} else if (block.name === HANDBACK_TOOL) {
|
||||
const report = handbackMessage(block.input);
|
||||
if (report)
|
||||
this.handbacks.set(block.id, { message: report, failed: false, sent: false });
|
||||
} else if (block.name === "SendMessage") {
|
||||
const input = inputOf(block);
|
||||
if (typeof input.to === "string" && typeof input.message === "string") {
|
||||
this.sendMessages.push({
|
||||
toolUseId: block.id,
|
||||
to: input.to,
|
||||
message: input.message,
|
||||
senderSessionId: ownerSessionId,
|
||||
shownAsToolCall: false,
|
||||
outgoingSent: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
|
||||
const handback = this.handbacks.get(block.tool_use_id);
|
||||
if (handback && block.is_error === true) handback.failed = true;
|
||||
const child = this.children.get(block.tool_use_id);
|
||||
// A background launch answers at once; its result is not the end.
|
||||
if (child && !child.record.background)
|
||||
child.launchEnd = replaySubagentTerminalState(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private registerLaunch(
|
||||
launchId: string,
|
||||
input: Record<string, unknown>,
|
||||
parentSessionId: string,
|
||||
): void {
|
||||
if (this.children.has(launchId)) return;
|
||||
const record = this.history.agents.get(launchId);
|
||||
if (!record) {
|
||||
this.omitted.add(launchId);
|
||||
return;
|
||||
}
|
||||
this.children.set(launchId, {
|
||||
launchId,
|
||||
sessionId: record.agentId,
|
||||
record,
|
||||
parentSessionId,
|
||||
input,
|
||||
announced: false,
|
||||
promptSent: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A replayed update of a tool call of `output`, the session of `owner`
|
||||
* when it is a child's. The client is not shown the launch of a replayed
|
||||
@@ -354,7 +253,9 @@ export class SubagentReplay {
|
||||
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
|
||||
return [notification];
|
||||
}
|
||||
const sendMessage = this.sendMessages.find((call) => call.toolUseId === update.toolCallId);
|
||||
const sendMessage = this.transcript.sendMessages.find(
|
||||
(call) => call.toolUseId === update.toolCallId,
|
||||
);
|
||||
if (sendMessage) {
|
||||
if (sendMessage.recipientSessionId === undefined) {
|
||||
sendMessage.shownAsToolCall = true;
|
||||
@@ -370,7 +271,7 @@ export class SubagentReplay {
|
||||
);
|
||||
return [outgoing!];
|
||||
}
|
||||
const handback = owner ? this.handbacks.get(update.toolCallId) : undefined;
|
||||
const handback = owner ? this.transcript.handbacks.get(update.toolCallId) : undefined;
|
||||
if (owner && handback && !handback.failed) {
|
||||
if (handback.sent) return [];
|
||||
handback.sent = true;
|
||||
@@ -386,7 +287,7 @@ export class SubagentReplay {
|
||||
this.outputOf(owner.parentSessionId)?.emitV1([incoming!]);
|
||||
return [];
|
||||
}
|
||||
const child = this.children.get(update.toolCallId);
|
||||
const child = this.transcript.children.get(update.toolCallId);
|
||||
if (!child) return [notification];
|
||||
if (
|
||||
update.status === "completed" &&
|
||||
@@ -412,7 +313,9 @@ export class SubagentReplay {
|
||||
return;
|
||||
}
|
||||
for (const notification of taskNotificationsOf(inner?.content)) {
|
||||
const child = [...this.children.values()].find((c) => c.sessionId === notification.task_id);
|
||||
const child = [...this.transcript.children.values()].find(
|
||||
(c) => c.sessionId === notification.task_id,
|
||||
);
|
||||
if (child) child.notified = nativeSubagentState(notification.status) ?? child.notified;
|
||||
}
|
||||
}
|
||||
@@ -421,13 +324,13 @@ export class SubagentReplay {
|
||||
private replayed(message: SessionMessage, parent: SessionOutput): void {
|
||||
if (message.type !== "assistant") return;
|
||||
for (const launchId of launchIdsOf(message)) {
|
||||
const child = this.children.get(launchId);
|
||||
const child = this.transcript.children.get(launchId);
|
||||
if (!child || child.announced) continue;
|
||||
const messages = this.transcripts.get(child.sessionId) ?? [];
|
||||
if (messages.length === 0) this.withoutHistory++;
|
||||
this.announce(child);
|
||||
this.sendModel(child, firstResponseModel(messages));
|
||||
this.scan(messages, child.sessionId);
|
||||
this.transcript.scan(messages, child.sessionId);
|
||||
child.output!.replay(messages, this.ownerHooks(child.output!, child));
|
||||
if (!child.promptSent && typeof child.input.prompt === "string") {
|
||||
this.sendPrompt(child, parent, child.input.prompt);
|
||||
@@ -517,7 +420,7 @@ export class SubagentReplay {
|
||||
*/
|
||||
private sendDelivery(child: ReplayChild, message: SessionMessage, text: string): void {
|
||||
const call = deliveredCall(
|
||||
this.sendMessages,
|
||||
this.transcript.sendMessages,
|
||||
[child.sessionId, child.record.name, child.input.name],
|
||||
text,
|
||||
);
|
||||
@@ -553,63 +456,18 @@ export class SubagentReplay {
|
||||
|
||||
private outputOf(sessionId: string): SessionOutput | undefined {
|
||||
if (sessionId === this.host.sessionId) return this.rootOutput;
|
||||
return [...this.children.values()].find((child) => child.sessionId === sessionId)?.output;
|
||||
return [...this.transcript.children.values()].find((child) => child.sessionId === sessionId)
|
||||
?.output;
|
||||
}
|
||||
|
||||
private cwdOf(sessionId: string): string {
|
||||
const child = [...this.children.values()].find((c) => c.sessionId === sessionId);
|
||||
const child = [...this.transcript.children.values()].find((c) => c.sessionId === sessionId);
|
||||
if (!child) return this.host.cwd;
|
||||
return child.cwd ?? this.cwdOf(child.parentSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How a replayed child's work ended: the state of its last task
|
||||
* notification, else a stop by the user, else its foreground launch's result.
|
||||
*/
|
||||
function endState(child: ReplayChild): WorkState {
|
||||
const end =
|
||||
child.notified ??
|
||||
nativeSubagentState(child.record.stoppedByUser ? "cancelled" : child.launchEnd);
|
||||
return end ? rfdEndState(end, undefined) : { state: "idle" };
|
||||
}
|
||||
|
||||
function launchIdsOf(message: SessionMessage): string[] {
|
||||
if (message.type !== "assistant") return [];
|
||||
return subagentLaunchIds((message.message as { content?: unknown } | undefined)?.content);
|
||||
}
|
||||
|
||||
function contentBlocks(message: SessionMessage): Record<string, unknown>[] {
|
||||
const content = (message as { message?: { content?: unknown } }).message?.content;
|
||||
return Array.isArray(content)
|
||||
? content.filter(
|
||||
(block): block is Record<string, unknown> => typeof block === "object" && block !== null,
|
||||
)
|
||||
: [];
|
||||
}
|
||||
|
||||
function inputOf(block: Record<string, unknown>): Record<string, unknown> {
|
||||
return typeof block.input === "object" && block.input !== null
|
||||
? (block.input as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
/** The text of user message content: a string, or its text blocks. */
|
||||
function textOf(content: unknown): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
return content
|
||||
.flatMap((block) =>
|
||||
typeof block === "object" && block !== null && block.type === "text" ? [block.text] : [],
|
||||
)
|
||||
.filter((text): text is string => typeof text === "string")
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function isDirectory(dir: string): boolean {
|
||||
try {
|
||||
return fs.statSync(dir).isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user