@agentclientprotocol/claude-agent-acp 0.85.2-preview.3 → 0.85.2-preview.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/acp-agent.d.ts +41 -9
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +254 -107
- package/dist/air-extension.d.ts +2 -0
- package/dist/air-extension.d.ts.map +1 -1
- package/dist/air-extension.js +2 -0
- package/dist/mcp-command.d.ts +51 -0
- package/dist/mcp-command.d.ts.map +1 -0
- package/dist/mcp-command.js +321 -0
- package/dist/resumed-session.d.ts +16 -6
- package/dist/resumed-session.d.ts.map +1 -1
- package/dist/resumed-session.js +91 -25
- package/dist/session-mode.d.ts +14 -0
- package/dist/session-mode.d.ts.map +1 -1
- package/dist/session-mode.js +22 -0
- package/dist/usage-markdown.d.ts +2 -0
- package/dist/usage-markdown.d.ts.map +1 -1
- package/dist/usage-markdown.js +2 -1
- package/dist/utils.d.ts +12 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +28 -0
- package/package.json +2 -2
package/dist/air-extension.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export declare const AIR_SKILL_KEY = "skill";
|
|
|
17
17
|
export declare const AIR_CONTEXT_COMPACTION_KEY = "contextCompaction";
|
|
18
18
|
export declare const AIR_GOAL_KEY = "goal";
|
|
19
19
|
export declare const AIR_KIND_KEY = "kind";
|
|
20
|
+
/** The absolute path of the `SKILL.md` of a skill command in `available_commands_update`. */
|
|
21
|
+
export declare const AIR_SKILL_PATH_KEY = "skillPath";
|
|
20
22
|
export declare const AIR_PERMISSION_KEY = "permission";
|
|
21
23
|
export declare const AIR_CUSTOM_ANSWER_KEY = "customAnswer";
|
|
22
24
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"air-extension.d.ts","sourceRoot":"","sources":["../src/air-extension.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,uCAAuC,2BAA2B,CAAC;AAChF,eAAO,MAAM,0BAA0B,eAAe,CAAC;AACvD,eAAO,MAAM,8BAA8B,mBAAmB,CAAC;AAC/D,eAAO,MAAM,uCAAuC,qBAAqB,CAAC;AAC1E,eAAO,MAAM,yBAAyB,cAAc,CAAC;AACrD,4EAA4E;AAC5E,eAAO,MAAM,kCAAkC,sBAAsB,CAAC;AACtE;;;GAGG;AACH,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD,8EAA8E;AAC9E,eAAO,MAAM,qBAAqB,iBAAiB,CAAC;AACpD,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAC3C,eAAO,MAAM,aAAa,UAAU,CAAC;AACrC,eAAO,MAAM,0BAA0B,sBAAsB,CAAC;AAC9D,eAAO,MAAM,YAAY,SAAS,CAAC;AACnC,eAAO,MAAM,YAAY,SAAS,CAAC;AACnC,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAC/C,eAAO,MAAM,qBAAqB,iBAAiB,CAAC;AACpD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAQ3E,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,GAAG,YAAY,EAAE,MAAM,EAAE,2BAE1D;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,EAChD,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAczB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,YAAY,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,OAAO,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GACpC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAErC;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAKnF;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAY9F"}
|
|
1
|
+
{"version":3,"file":"air-extension.d.ts","sourceRoot":"","sources":["../src/air-extension.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,uCAAuC,2BAA2B,CAAC;AAChF,eAAO,MAAM,0BAA0B,eAAe,CAAC;AACvD,eAAO,MAAM,8BAA8B,mBAAmB,CAAC;AAC/D,eAAO,MAAM,uCAAuC,qBAAqB,CAAC;AAC1E,eAAO,MAAM,yBAAyB,cAAc,CAAC;AACrD,4EAA4E;AAC5E,eAAO,MAAM,kCAAkC,sBAAsB,CAAC;AACtE;;;GAGG;AACH,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD,8EAA8E;AAC9E,eAAO,MAAM,qBAAqB,iBAAiB,CAAC;AACpD,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAC3C,eAAO,MAAM,aAAa,UAAU,CAAC;AACrC,eAAO,MAAM,0BAA0B,sBAAsB,CAAC;AAC9D,eAAO,MAAM,YAAY,SAAS,CAAC;AACnC,eAAO,MAAM,YAAY,SAAS,CAAC;AACnC,6FAA6F;AAC7F,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAC9C,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAC/C,eAAO,MAAM,qBAAqB,iBAAiB,CAAC;AACpD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAQ3E,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,GAAG,YAAY,EAAE,MAAM,EAAE,2BAE1D;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,EAChD,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAczB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,YAAY,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,OAAO,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GACpC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAErC;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAKnF;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAY9F"}
|
package/dist/air-extension.js
CHANGED
|
@@ -17,6 +17,8 @@ export const AIR_SKILL_KEY = "skill";
|
|
|
17
17
|
export const AIR_CONTEXT_COMPACTION_KEY = "contextCompaction";
|
|
18
18
|
export const AIR_GOAL_KEY = "goal";
|
|
19
19
|
export const AIR_KIND_KEY = "kind";
|
|
20
|
+
/** The absolute path of the `SKILL.md` of a skill command in `available_commands_update`. */
|
|
21
|
+
export const AIR_SKILL_PATH_KEY = "skillPath";
|
|
20
22
|
export const AIR_PERMISSION_KEY = "permission";
|
|
21
23
|
export const AIR_CUSTOM_ANSWER_KEY = "customAnswer";
|
|
22
24
|
/**
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AvailableCommand } from "@agentclientprotocol/sdk";
|
|
2
|
+
import type { McpServerStatus, Query } from "@anthropic-ai/claude-agent-sdk";
|
|
3
|
+
/** The `/mcp` entry of `available_commands_update`. The adapter shows the
|
|
4
|
+
* servers as a list, and runs a reconnect, an enable, or a disable through
|
|
5
|
+
* the SDK control API. */
|
|
6
|
+
export declare const MCP_AVAILABLE_COMMAND: AvailableCommand;
|
|
7
|
+
/** A `/mcp` prompt that gets a replacement for its Claude Code text. `all` is
|
|
8
|
+
* true for `all` and for a reconnect without a server name, as in Claude
|
|
9
|
+
* Code. `server` is undefined for an enable or a disable without a name. */
|
|
10
|
+
export type McpCommand = {
|
|
11
|
+
action: "status";
|
|
12
|
+
} | {
|
|
13
|
+
action: "reconnect" | "enable" | "disable";
|
|
14
|
+
all: boolean;
|
|
15
|
+
server?: string;
|
|
16
|
+
};
|
|
17
|
+
/** Parse a prompt that is exactly `/mcp`, or `/mcp reconnect`, `/mcp enable`,
|
|
18
|
+
* or `/mcp disable` with an optional server name or `all`. Any other `/mcp`
|
|
19
|
+
* argument gives null, and the prompt gets no replacement. */
|
|
20
|
+
export declare function parseMcpCommand(text: string): McpCommand | null;
|
|
21
|
+
/** Read the MCP server status for the list after a `/mcp` command. Resolves
|
|
22
|
+
* to null when the read fails, takes too long, or `signal` aborts. Then the
|
|
23
|
+
* turn keeps the text of Claude Code. */
|
|
24
|
+
export declare function readMcpServerStatus(query: Pick<Query, "mcpServerStatus">, signal: AbortSignal, logError: (message: string) => void): Promise<McpServerStatus[] | null>;
|
|
25
|
+
/** Make an MCP error short and readable for one list item. The result has
|
|
26
|
+
* no leading wrapper segments, at most two sentences, and at most
|
|
27
|
+
* `MAX_ERROR_LENGTH` code points. A wrapper before an error code stays, so
|
|
28
|
+
* an error code is never alone. The result is empty for an error with only
|
|
29
|
+
* whitespace. */
|
|
30
|
+
export declare function cleanMcpError(error: string): string;
|
|
31
|
+
/** Markdown for `/mcp`: a summary line, one group of servers for each
|
|
32
|
+
* status, and the disabled servers on one line. A list reads better than a
|
|
33
|
+
* table in a narrow chat. */
|
|
34
|
+
export declare function formatMcpStatus(statuses: McpServerStatus[]): string;
|
|
35
|
+
/** The SDK query methods that `/mcp` uses. */
|
|
36
|
+
export type McpControlQuery = Pick<Query, "mcpServerStatus" | "reconnectMcpServer" | "toggleMcpServer">;
|
|
37
|
+
/** The result of {@link runMcpCommand}. `markdown` is null when the turn
|
|
38
|
+
* keeps the text of Claude Code. `reconnected` holds the servers that the
|
|
39
|
+
* adapter tried to reconnect. */
|
|
40
|
+
export type McpCommandOutcome = {
|
|
41
|
+
markdown: string | null;
|
|
42
|
+
reconnected: string[];
|
|
43
|
+
};
|
|
44
|
+
/** Run a `/mcp` command after Claude Code sent `originalOutput`, its text for
|
|
45
|
+
* the command. Claude Code refuses a reconnect, an enable, and a disable in
|
|
46
|
+
* SDK mode, so the adapter runs them through the SDK control API. The result
|
|
47
|
+
* of each server goes above the server list. An error of one server does
|
|
48
|
+
* not stop the others. When the first status read fails or `signal`
|
|
49
|
+
* aborts, the turn keeps the text of Claude Code. */
|
|
50
|
+
export declare function runMcpCommand(query: McpControlQuery, command: McpCommand, originalOutput: string, signal: AbortSignal, logError: (message: string) => void): Promise<McpCommandOutcome>;
|
|
51
|
+
//# sourceMappingURL=mcp-command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-command.d.ts","sourceRoot":"","sources":["../src/mcp-command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AAI7E;;2BAE2B;AAC3B,eAAO,MAAM,qBAAqB,EAAE,gBAInC,CAAC;AAQF;;6EAE6E;AAC7E,MAAM,MAAM,UAAU,GAClB;IAAE,MAAM,EAAE,QAAQ,CAAA;CAAE,GACpB;IAAE,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IAAC,GAAG,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAElF;;+DAE+D;AAC/D,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAW/D;AAED;;0CAE0C;AAC1C,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,iBAAiB,CAAC,EACrC,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAClC,OAAO,CAAC,eAAe,EAAE,GAAG,IAAI,CAAC,CAkBnC;AAuED;;;;kBAIkB;AAClB,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiBnD;AA2DD;;8BAE8B;AAC9B,wBAAgB,eAAe,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,MAAM,CA6BnE;AAED,8CAA8C;AAC9C,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,KAAK,EACL,iBAAiB,GAAG,oBAAoB,GAAG,iBAAiB,CAC7D,CAAC;AAEF;;kCAEkC;AAClC,MAAM,MAAM,iBAAiB,GAAG;IAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AA4DnF;;;;;sDAKsD;AACtD,wBAAsB,aAAa,CACjC,KAAK,EAAE,eAAe,EACtB,OAAO,EAAE,UAAU,EACnB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAClC,OAAO,CAAC,iBAAiB,CAAC,CAoD5B"}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { escapeMarkdown } from "./usage-markdown.js";
|
|
2
|
+
import { raceTimeoutAndAbort } from "./utils.js";
|
|
3
|
+
/** The `/mcp` entry of `available_commands_update`. The adapter shows the
|
|
4
|
+
* servers as a list, and runs a reconnect, an enable, or a disable through
|
|
5
|
+
* the SDK control API. */
|
|
6
|
+
export const MCP_AVAILABLE_COMMAND = {
|
|
7
|
+
name: "mcp",
|
|
8
|
+
description: "Show the MCP servers and their status, or reconnect, enable, or disable a server",
|
|
9
|
+
input: { hint: "[reconnect|enable|disable [<server>|all]]" },
|
|
10
|
+
};
|
|
11
|
+
/** The time that the status read after a `/mcp` command can take. */
|
|
12
|
+
const MCP_STATUS_TIMEOUT_MS = 5_000;
|
|
13
|
+
/** The time that one reconnect, enable, or disable can take. */
|
|
14
|
+
const MCP_ACTION_TIMEOUT_MS = 30_000;
|
|
15
|
+
/** Parse a prompt that is exactly `/mcp`, or `/mcp reconnect`, `/mcp enable`,
|
|
16
|
+
* or `/mcp disable` with an optional server name or `all`. Any other `/mcp`
|
|
17
|
+
* argument gives null, and the prompt gets no replacement. */
|
|
18
|
+
export function parseMcpCommand(text) {
|
|
19
|
+
const words = text.trim().split(/\s+/);
|
|
20
|
+
if (words[0] !== "/mcp")
|
|
21
|
+
return null;
|
|
22
|
+
if (words.length === 1)
|
|
23
|
+
return { action: "status" };
|
|
24
|
+
const action = words[1]?.toLowerCase();
|
|
25
|
+
if (action !== "reconnect" && action !== "enable" && action !== "disable")
|
|
26
|
+
return null;
|
|
27
|
+
const server = words.slice(2).join(" ");
|
|
28
|
+
if (server === "all" || (server === "" && action === "reconnect")) {
|
|
29
|
+
return { action, all: true };
|
|
30
|
+
}
|
|
31
|
+
return server === "" ? { action, all: false } : { action, all: false, server };
|
|
32
|
+
}
|
|
33
|
+
/** Read the MCP server status for the list after a `/mcp` command. Resolves
|
|
34
|
+
* to null when the read fails, takes too long, or `signal` aborts. Then the
|
|
35
|
+
* turn keeps the text of Claude Code. */
|
|
36
|
+
export async function readMcpServerStatus(query, signal, logError) {
|
|
37
|
+
if (signal.aborted)
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
const outcome = await raceTimeoutAndAbort(query.mcpServerStatus(), MCP_STATUS_TIMEOUT_MS, signal);
|
|
41
|
+
if (outcome.type === "timeout") {
|
|
42
|
+
logError("The MCP server status read timed out; keeping the Claude Code text of /mcp");
|
|
43
|
+
}
|
|
44
|
+
return outcome.type === "done" ? outcome.value : null;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (!signal.aborted) {
|
|
48
|
+
logError(`The MCP server status read failed; keeping the Claude Code text of /mcp: ${error}`);
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** The known statuses in the order of the groups and of the summary counts.
|
|
54
|
+
* A disabled server is not a group. It is on one line at the end. */
|
|
55
|
+
const GROUP_ORDER = ["failed", "needs-auth", "pending", "connected"];
|
|
56
|
+
const GROUP_TITLES = {
|
|
57
|
+
failed: "Failed",
|
|
58
|
+
"needs-auth": "Needs authentication",
|
|
59
|
+
pending: "Connecting",
|
|
60
|
+
connected: "Connected",
|
|
61
|
+
};
|
|
62
|
+
const SUMMARY_LABELS = {
|
|
63
|
+
connected: "connected",
|
|
64
|
+
failed: "failed",
|
|
65
|
+
"needs-auth": "need authentication",
|
|
66
|
+
pending: "connecting",
|
|
67
|
+
disabled: "disabled",
|
|
68
|
+
};
|
|
69
|
+
const KNOWN_STATUSES = [...GROUP_ORDER, "disabled"];
|
|
70
|
+
const MAX_ERROR_LENGTH = 160;
|
|
71
|
+
/** The longest segment that can be a wrapper. A longer segment is content. */
|
|
72
|
+
const MAX_WRAPPER_LENGTH = 60;
|
|
73
|
+
/** True for a server that a `/mcp reconnect` without a name must retry. */
|
|
74
|
+
function needsReconnect(status) {
|
|
75
|
+
return (status.status === "failed" || status.status === "pending" || status.status === "needs-auth");
|
|
76
|
+
}
|
|
77
|
+
/** The text as a Markdown code span. The fence is longer than the longest
|
|
78
|
+
* backtick run in the text, so the span shows the text unchanged. The user
|
|
79
|
+
* can copy a server name from it into `/mcp reconnect`. */
|
|
80
|
+
function codeSpan(text) {
|
|
81
|
+
const longestRun = Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length));
|
|
82
|
+
const fence = "`".repeat(longestRun + 1);
|
|
83
|
+
// CommonMark strips one space from each end of a span, so a pad keeps the text unchanged.
|
|
84
|
+
const pad = text.startsWith("`") || text.endsWith("`") || /^ .*[^ ].* $/.test(text) ? " " : "";
|
|
85
|
+
return `${fence}${pad}${text}${pad}${fence}`;
|
|
86
|
+
}
|
|
87
|
+
function isErrorCode(segment) {
|
|
88
|
+
return /^-?\d+$/.test(segment);
|
|
89
|
+
}
|
|
90
|
+
/** True for a segment that only wraps the real error, such as
|
|
91
|
+
* `MCP startup failed`, `McpError`, `MCP error -32001`, an error code, or a
|
|
92
|
+
* Rust or TypeScript type path. */
|
|
93
|
+
function isWrapperSegment(segment) {
|
|
94
|
+
return (isErrorCode(segment) ||
|
|
95
|
+
segment.includes("::") ||
|
|
96
|
+
(segment.length <= MAX_WRAPPER_LENGTH &&
|
|
97
|
+
!/[.!?]/.test(segment) &&
|
|
98
|
+
/(failed|error|exception)(\s+-?\d+)?$/i.test(segment)));
|
|
99
|
+
}
|
|
100
|
+
/** Cut the text to `MAX_ERROR_LENGTH` code points, so a surrogate pair stays whole. */
|
|
101
|
+
function truncate(text) {
|
|
102
|
+
const chars = Array.from(text);
|
|
103
|
+
if (chars.length <= MAX_ERROR_LENGTH)
|
|
104
|
+
return text;
|
|
105
|
+
const cut = chars.slice(0, MAX_ERROR_LENGTH - 1).join("");
|
|
106
|
+
return `${cut.trimEnd()}…`;
|
|
107
|
+
}
|
|
108
|
+
/** Make an MCP error short and readable for one list item. The result has
|
|
109
|
+
* no leading wrapper segments, at most two sentences, and at most
|
|
110
|
+
* `MAX_ERROR_LENGTH` code points. A wrapper before an error code stays, so
|
|
111
|
+
* an error code is never alone. The result is empty for an error with only
|
|
112
|
+
* whitespace. */
|
|
113
|
+
export function cleanMcpError(error) {
|
|
114
|
+
const segments = error.replace(/\s+/g, " ").trim().split(": ");
|
|
115
|
+
let start = 0;
|
|
116
|
+
while (start < segments.length - 1 &&
|
|
117
|
+
isWrapperSegment(segments[start]) &&
|
|
118
|
+
!segments.slice(start + 1).every(isErrorCode)) {
|
|
119
|
+
start++;
|
|
120
|
+
}
|
|
121
|
+
const content = segments.slice(start).join(": ");
|
|
122
|
+
return truncate(content
|
|
123
|
+
.split(/(?<=[.!?]) /)
|
|
124
|
+
.slice(0, 2)
|
|
125
|
+
.join(" "));
|
|
126
|
+
}
|
|
127
|
+
function toolCount(status) {
|
|
128
|
+
if (!status.tools)
|
|
129
|
+
return undefined;
|
|
130
|
+
return status.tools.length === 1 ? "1 tool" : `${status.tools.length} tools`;
|
|
131
|
+
}
|
|
132
|
+
/** The server name as inline code. Claude Code keys the servers by name, so
|
|
133
|
+
* two servers have the same name only from different scopes. Then the
|
|
134
|
+
* scope tells them apart. */
|
|
135
|
+
function serverName(status, duplicates) {
|
|
136
|
+
const origin = status.scope ?? status.source;
|
|
137
|
+
return duplicates.has(status.name) && origin
|
|
138
|
+
? `${codeSpan(status.name)} (${escapeMarkdown(origin)})`
|
|
139
|
+
: codeSpan(status.name);
|
|
140
|
+
}
|
|
141
|
+
function serverItem(status, duplicates) {
|
|
142
|
+
const error = status.error ? cleanMcpError(status.error) : "";
|
|
143
|
+
const details = [error === "" ? undefined : escapeMarkdown(error), toolCount(status)]
|
|
144
|
+
.filter((detail) => detail !== undefined)
|
|
145
|
+
.join("; ");
|
|
146
|
+
const name = serverName(status, duplicates);
|
|
147
|
+
return details === "" ? `- ${name}` : `- ${name}: ${details}`;
|
|
148
|
+
}
|
|
149
|
+
function summaryLine(statuses) {
|
|
150
|
+
// The counts follow the group order. An unknown status also counts, so the counts add up to the total.
|
|
151
|
+
const counts = [...GROUP_ORDER, ...unknownStatuses(statuses), "disabled"]
|
|
152
|
+
.map((state) => ({
|
|
153
|
+
state,
|
|
154
|
+
count: statuses.filter((status) => status.status === state).length,
|
|
155
|
+
}))
|
|
156
|
+
.filter(({ count }) => count > 0);
|
|
157
|
+
const parts = counts.map(({ state, count }) => count === 1 && state === "needs-auth"
|
|
158
|
+
? "1 needs authentication"
|
|
159
|
+
: `${count} ${SUMMARY_LABELS[state] ?? escapeMarkdown(state)}`);
|
|
160
|
+
return `**MCP servers:** ${statuses.length} (${parts.join(", ")})`;
|
|
161
|
+
}
|
|
162
|
+
/** The statuses that this adapter does not know, in the SDK order. The SDK
|
|
163
|
+
* can report a new status before the adapter knows it. */
|
|
164
|
+
function unknownStatuses(statuses) {
|
|
165
|
+
return [
|
|
166
|
+
...new Set(statuses
|
|
167
|
+
.map((status) => status.status)
|
|
168
|
+
.filter((state) => !KNOWN_STATUSES.includes(state))),
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
/** The group title of a status that the adapter does not know. It starts with a capital letter, as a known title does. */
|
|
172
|
+
function unknownTitle(state) {
|
|
173
|
+
return escapeMarkdown(state.charAt(0).toUpperCase() + state.slice(1));
|
|
174
|
+
}
|
|
175
|
+
/** Markdown for `/mcp`: a summary line, one group of servers for each
|
|
176
|
+
* status, and the disabled servers on one line. A list reads better than a
|
|
177
|
+
* table in a narrow chat. */
|
|
178
|
+
export function formatMcpStatus(statuses) {
|
|
179
|
+
if (statuses.length === 0)
|
|
180
|
+
return "No MCP servers are configured.";
|
|
181
|
+
const blocks = [];
|
|
182
|
+
const names = statuses.map((status) => status.name);
|
|
183
|
+
const duplicates = new Set(names.filter((name, index) => names.indexOf(name) !== index));
|
|
184
|
+
blocks.push(summaryLine(statuses));
|
|
185
|
+
const groups = [
|
|
186
|
+
...GROUP_ORDER.map((state) => [state, GROUP_TITLES[state]]),
|
|
187
|
+
...unknownStatuses(statuses).map((state) => [state, unknownTitle(state)]),
|
|
188
|
+
];
|
|
189
|
+
for (const [state, title] of groups) {
|
|
190
|
+
const members = statuses.filter((status) => status.status === state);
|
|
191
|
+
if (members.length === 0)
|
|
192
|
+
continue;
|
|
193
|
+
blocks.push([`**${title}**`, ...members.map((status) => serverItem(status, duplicates))].join("\n"));
|
|
194
|
+
}
|
|
195
|
+
const disabled = statuses.filter((status) => status.status === "disabled");
|
|
196
|
+
if (disabled.length > 0) {
|
|
197
|
+
blocks.push(`**Disabled:** ${disabled.map((status) => serverName(status, duplicates)).join(", ")}`);
|
|
198
|
+
}
|
|
199
|
+
if (statuses.some(needsReconnect)) {
|
|
200
|
+
blocks.push("Run `/mcp reconnect <server>` to reconnect one server, or `/mcp reconnect` to reconnect every server that is not connected and not disabled.");
|
|
201
|
+
}
|
|
202
|
+
return blocks.join("\n\n");
|
|
203
|
+
}
|
|
204
|
+
/** Wait for one control call. The wait stops when `signal` aborts or the
|
|
205
|
+
* call takes more than {@link MCP_ACTION_TIMEOUT_MS}. */
|
|
206
|
+
async function awaitCall(call, signal) {
|
|
207
|
+
if (signal.aborted)
|
|
208
|
+
return { type: "aborted" };
|
|
209
|
+
try {
|
|
210
|
+
const outcome = await raceTimeoutAndAbort(call(), MCP_ACTION_TIMEOUT_MS, signal);
|
|
211
|
+
if (outcome.type === "timeout")
|
|
212
|
+
return { type: "failed", error: "The request timed out." };
|
|
213
|
+
return outcome.type === "done" ? { type: "done" } : outcome;
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
return signal.aborted
|
|
217
|
+
? { type: "aborted" }
|
|
218
|
+
: { type: "failed", error: error instanceof Error ? error.message : String(error) };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const ACTION_DONE = { reconnect: "Reconnected", enable: "Enabled", disable: "Disabled" };
|
|
222
|
+
/** The Claude Code sentence that refuses an action in SDK mode. */
|
|
223
|
+
const UNAVAILABLE_SENTENCE = /aren't available in this session/i;
|
|
224
|
+
/** A Claude Code sentence that points to a terminal, which an ACP client
|
|
225
|
+
* does not have. */
|
|
226
|
+
function isTerminalSentence(sentence) {
|
|
227
|
+
return /in the terminal/i.test(sentence) || sentence.includes("`/mcp reconnect all` here");
|
|
228
|
+
}
|
|
229
|
+
function sentences(text) {
|
|
230
|
+
return text
|
|
231
|
+
.trim()
|
|
232
|
+
.split(/(?<=[.!?])\s+/)
|
|
233
|
+
.filter((sentence) => sentence !== "");
|
|
234
|
+
}
|
|
235
|
+
/** The servers that the action changes. A named server is always a target,
|
|
236
|
+
* so the control API reports its own error. */
|
|
237
|
+
function actionTargets(command, statuses) {
|
|
238
|
+
if (!command.all)
|
|
239
|
+
return command.server === undefined ? [] : [command.server];
|
|
240
|
+
const names = statuses
|
|
241
|
+
.filter((status) => {
|
|
242
|
+
if (command.action === "reconnect")
|
|
243
|
+
return needsReconnect(status);
|
|
244
|
+
if (command.action === "enable")
|
|
245
|
+
return status.status === "disabled";
|
|
246
|
+
return status.status !== "disabled";
|
|
247
|
+
})
|
|
248
|
+
.map((status) => status.name);
|
|
249
|
+
return [...new Set(names)];
|
|
250
|
+
}
|
|
251
|
+
function nothingToDoLine(action) {
|
|
252
|
+
if (action === "enable")
|
|
253
|
+
return "Every MCP server is already enabled.";
|
|
254
|
+
if (action === "disable")
|
|
255
|
+
return "Every MCP server is already disabled.";
|
|
256
|
+
return "Every MCP server is connected or disabled. There is nothing to reconnect.";
|
|
257
|
+
}
|
|
258
|
+
/** Run a `/mcp` command after Claude Code sent `originalOutput`, its text for
|
|
259
|
+
* the command. Claude Code refuses a reconnect, an enable, and a disable in
|
|
260
|
+
* SDK mode, so the adapter runs them through the SDK control API. The result
|
|
261
|
+
* of each server goes above the server list. An error of one server does
|
|
262
|
+
* not stop the others. When the first status read fails or `signal`
|
|
263
|
+
* aborts, the turn keeps the text of Claude Code. */
|
|
264
|
+
export async function runMcpCommand(query, command, originalOutput, signal, logError) {
|
|
265
|
+
const reconnected = [];
|
|
266
|
+
const keep = { markdown: null, reconnected };
|
|
267
|
+
if (command.action === "status") {
|
|
268
|
+
// The list replaces the Claude Code summary line.
|
|
269
|
+
const statuses = await readMcpServerStatus(query, signal, logError);
|
|
270
|
+
return { markdown: statuses ? formatMcpStatus(statuses) : null, reconnected };
|
|
271
|
+
}
|
|
272
|
+
const before = await readMcpServerStatus(query, signal, logError);
|
|
273
|
+
if (!before)
|
|
274
|
+
return keep;
|
|
275
|
+
const cliSentences = sentences(originalOutput).filter((sentence) => !isTerminalSentence(sentence));
|
|
276
|
+
const cliNamesUnknown = cliSentences.some((sentence) => /no MCP server named/i.test(sentence));
|
|
277
|
+
const results = [];
|
|
278
|
+
if (!command.all && command.server === undefined) {
|
|
279
|
+
results.push(`Name a server or \`all\`, for example \`/mcp ${command.action} <server>\`.`);
|
|
280
|
+
}
|
|
281
|
+
const known = new Set(before.map((status) => status.name));
|
|
282
|
+
const targets = actionTargets(command, before);
|
|
283
|
+
if (command.all && targets.length === 0)
|
|
284
|
+
results.push(nothingToDoLine(command.action));
|
|
285
|
+
for (const server of targets) {
|
|
286
|
+
if (!known.has(server)) {
|
|
287
|
+
// Claude Code checks the name first. Its sentence then stays.
|
|
288
|
+
if (!cliNamesUnknown)
|
|
289
|
+
results.push(`There's no MCP server named ${codeSpan(server)}.`);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (command.action === "reconnect")
|
|
293
|
+
reconnected.push(server);
|
|
294
|
+
const call = command.action === "reconnect"
|
|
295
|
+
? () => query.reconnectMcpServer(server)
|
|
296
|
+
: () => query.toggleMcpServer(server, command.action === "enable");
|
|
297
|
+
const result = await awaitCall(call, signal);
|
|
298
|
+
if (result.type === "aborted")
|
|
299
|
+
return keep;
|
|
300
|
+
if (result.type === "done") {
|
|
301
|
+
results.push(`- ${ACTION_DONE[command.action]} ${codeSpan(server)}`);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const error = cleanMcpError(result.error);
|
|
305
|
+
const failure = `- Couldn't ${command.action} ${codeSpan(server)}`;
|
|
306
|
+
results.push(error === "" ? failure : `${failure}: ${escapeMarkdown(error)}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const blocks = [];
|
|
310
|
+
const kept = cliSentences.filter((sentence) => !UNAVAILABLE_SENTENCE.test(sentence));
|
|
311
|
+
if (kept.length > 0)
|
|
312
|
+
blocks.push(kept.join(" "));
|
|
313
|
+
if (results.length > 0)
|
|
314
|
+
blocks.push(results.join("\n"));
|
|
315
|
+
const after = await readMcpServerStatus(query, signal, logError);
|
|
316
|
+
if (signal.aborted)
|
|
317
|
+
return keep;
|
|
318
|
+
if (after)
|
|
319
|
+
blocks.push(formatMcpStatus(after));
|
|
320
|
+
return { markdown: blocks.join("\n\n"), reconnected };
|
|
321
|
+
}
|
|
@@ -15,15 +15,25 @@ export declare function resumedModelFromTranscript(messages: SessionMessage[]):
|
|
|
15
15
|
/** Read the resume model from the local transcript without starting a Claude
|
|
16
16
|
* control request. This is intentionally on the load critical path. */
|
|
17
17
|
export declare function readResumedSession(sessionId: string, logger?: ResumeLogger): Promise<ResumedSessionSnapshot>;
|
|
18
|
+
/** What a resume restores from the end of the local transcript. */
|
|
19
|
+
export type ResumedTail = {
|
|
20
|
+
model?: string;
|
|
21
|
+
/** The last permission mode of the main thread. See {@link TailScan}. */
|
|
22
|
+
permissionMode?: string;
|
|
23
|
+
};
|
|
18
24
|
/**
|
|
19
|
-
* Read the resume model of a session from the end
|
|
25
|
+
* Read the resume model and the permission mode of a session from the end
|
|
26
|
+
* of its local transcript.
|
|
27
|
+
*
|
|
28
|
+
* A resume needs only these two values, not the messages. The transcript file
|
|
29
|
+
* is read backwards until both are known, so the cost does not grow with the
|
|
30
|
+
* length of the session. A session without a local transcript file falls back
|
|
31
|
+
* to {@link readResumedSession} for the model.
|
|
20
32
|
*
|
|
21
|
-
* A
|
|
22
|
-
*
|
|
23
|
-
* the cost does not grow with the length of the session. A session without a
|
|
24
|
-
* local transcript file falls back to {@link readResumedSession}.
|
|
33
|
+
* A caller that already has the model passes `withModel: false`. The read then
|
|
34
|
+
* looks for the permission mode only and has no fallback.
|
|
25
35
|
*/
|
|
26
|
-
export declare function
|
|
36
|
+
export declare function readResumedTail(sessionId: string, logger?: ResumeLogger, withModel?: boolean): Promise<ResumedTail>;
|
|
27
37
|
/** The local transcript of a session in any project directory, as the SDK looks it up. */
|
|
28
38
|
export declare function findTranscript(sessionId: string): Promise<string | undefined>;
|
|
29
39
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resumed-session.d.ts","sourceRoot":"","sources":["../src/resumed-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,gCAAgC,CAAC;AASzF,KAAK,YAAY,GAAG;IAClB,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IAClC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;iDAGiD;AACjD,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,MAAM,GAAG,SAAS,CAgBzF;AASD;uEACuE;AACvE,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,sBAAsB,CAAC,CAejC;AAED
|
|
1
|
+
{"version":3,"file":"resumed-session.d.ts","sourceRoot":"","sources":["../src/resumed-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,gCAAgC,CAAC;AASzF,KAAK,YAAY,GAAG;IAClB,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IAClC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;iDAGiD;AACjD,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,MAAM,GAAG,SAAS,CAgBzF;AASD;uEACuE;AACvE,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,sBAAsB,CAAC,CAejC;AAED,mEAAmE;AACnE,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,YAAY,EACrB,SAAS,UAAO,GACf,OAAO,CAAC,WAAW,CAAC,CAiBtB;AAED,0FAA0F;AAC1F,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAkBnF"}
|
package/dist/resumed-session.js
CHANGED
|
@@ -52,28 +52,32 @@ export async function readResumedSession(sessionId, logger) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
|
-
* Read the resume model of a session from the end
|
|
55
|
+
* Read the resume model and the permission mode of a session from the end
|
|
56
|
+
* of its local transcript.
|
|
56
57
|
*
|
|
57
|
-
* A resume needs only
|
|
58
|
-
* read backwards until
|
|
59
|
-
*
|
|
60
|
-
*
|
|
58
|
+
* A resume needs only these two values, not the messages. The transcript file
|
|
59
|
+
* is read backwards until both are known, so the cost does not grow with the
|
|
60
|
+
* length of the session. A session without a local transcript file falls back
|
|
61
|
+
* to {@link readResumedSession} for the model.
|
|
62
|
+
*
|
|
63
|
+
* A caller that already has the model passes `withModel: false`. The read then
|
|
64
|
+
* looks for the permission mode only and has no fallback.
|
|
61
65
|
*/
|
|
62
|
-
export async function
|
|
66
|
+
export async function readResumedTail(sessionId, logger, withModel = true) {
|
|
63
67
|
const timing = new SessionTiming(logger, "models", sessionId);
|
|
64
68
|
try {
|
|
65
69
|
const filePath = await findTranscript(sessionId);
|
|
66
70
|
if (filePath) {
|
|
67
|
-
const
|
|
68
|
-
timing.phase("read-transcript-tail", ` model=${model ?? "unknown"}`);
|
|
69
|
-
return
|
|
71
|
+
const tail = await lastTailRecords(filePath, withModel);
|
|
72
|
+
timing.phase("read-transcript-tail", ` model=${tail.model ?? "unknown"} permissionMode=${tail.permissionMode ?? "unknown"}`);
|
|
73
|
+
return tail;
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
catch (error) {
|
|
73
77
|
timing.phase("read-transcript-tail", " outcome=error");
|
|
74
78
|
logger?.error(`Failed to read the transcript tail of resumed session ${sessionId}:`, error);
|
|
75
79
|
}
|
|
76
|
-
return (await readResumedSession(sessionId, logger)).model;
|
|
80
|
+
return withModel ? { model: (await readResumedSession(sessionId, logger)).model } : {};
|
|
77
81
|
}
|
|
78
82
|
/** The local transcript of a session in any project directory, as the SDK looks it up. */
|
|
79
83
|
export async function findTranscript(sessionId) {
|
|
@@ -97,9 +101,11 @@ export async function findTranscript(sessionId) {
|
|
|
97
101
|
}
|
|
98
102
|
return undefined;
|
|
99
103
|
}
|
|
100
|
-
/** The model of the last real assistant record
|
|
101
|
-
|
|
104
|
+
/** The model of the last real assistant record and the last permission mode
|
|
105
|
+
* of the main thread in a JSONL transcript. */
|
|
106
|
+
async function lastTailRecords(filePath, withModel) {
|
|
102
107
|
const handle = await open(filePath, "r");
|
|
108
|
+
const scan = new TailScan(withModel);
|
|
103
109
|
try {
|
|
104
110
|
let end = (await handle.stat()).size;
|
|
105
111
|
// The bytes of the line whose start is not read yet, in file order.
|
|
@@ -109,36 +115,96 @@ async function lastAssistantModel(filePath) {
|
|
|
109
115
|
let chunk = Buffer.alloc(end - start);
|
|
110
116
|
await handle.read(chunk, 0, chunk.length, start);
|
|
111
117
|
for (let at = chunk.lastIndexOf(0x0a); at >= 0; at = chunk.lastIndexOf(0x0a)) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return model;
|
|
118
|
+
if (scan.visit(Buffer.concat([chunk.subarray(at + 1), ...pending])))
|
|
119
|
+
return scan.tail;
|
|
115
120
|
pending = [];
|
|
116
121
|
chunk = chunk.subarray(0, at);
|
|
117
122
|
}
|
|
118
123
|
pending.unshift(chunk);
|
|
119
124
|
end = start;
|
|
120
125
|
}
|
|
121
|
-
|
|
126
|
+
scan.visit(Buffer.concat(pending));
|
|
127
|
+
return scan.tail;
|
|
122
128
|
}
|
|
123
129
|
finally {
|
|
124
130
|
await handle.close();
|
|
125
131
|
}
|
|
126
132
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Reads the lines of a transcript from the end and keeps what a resume needs.
|
|
135
|
+
*
|
|
136
|
+
* Claude Code records the permission mode on each prompt record of type `user`.
|
|
137
|
+
* The `permission-mode` records of the CLI metadata block are not used: they
|
|
138
|
+
* can disagree with the prompts, and the SDK does not write them.
|
|
139
|
+
* The SDK writes no record when it leaves plan mode through ExitPlanMode.
|
|
140
|
+
* It writes a `plan_mode_exit` attachment only, without the new mode. Thus a
|
|
141
|
+
* `plan` mode before a later plan exit is not the current mode, and the scan
|
|
142
|
+
* gives no mode.
|
|
143
|
+
*
|
|
144
|
+
* The mode search stops at the first mode record or at the last human prompt.
|
|
145
|
+
* Each human prompt of a current Claude Code has the mode, so a prompt without
|
|
146
|
+
* it means that the transcript does not record modes. A compaction does not
|
|
147
|
+
* stop the search: the mode of the prompt before it is still current.
|
|
148
|
+
*/
|
|
149
|
+
class TailScan {
|
|
150
|
+
withModel;
|
|
151
|
+
tail = {};
|
|
152
|
+
modeDone = false;
|
|
153
|
+
planExited = false;
|
|
154
|
+
constructor(withModel) {
|
|
155
|
+
this.withModel = withModel;
|
|
156
|
+
}
|
|
157
|
+
/** Visit the previous line. Return true when the scan has all it needs. */
|
|
158
|
+
visit(line) {
|
|
159
|
+
let parsed;
|
|
160
|
+
// Most lines have no marker of interest. The check skips their JSON parse.
|
|
161
|
+
const record = (marker) => {
|
|
162
|
+
if (line.indexOf(marker) < 0)
|
|
163
|
+
return undefined;
|
|
164
|
+
if (parsed === undefined)
|
|
165
|
+
parsed = parseMainThreadRecord(line);
|
|
166
|
+
return parsed ?? undefined;
|
|
167
|
+
};
|
|
168
|
+
if (this.withModel && this.tail.model === undefined) {
|
|
169
|
+
const entry = record('"assistant"');
|
|
170
|
+
if (entry?.type === "assistant")
|
|
171
|
+
this.tail.model = concreteModel(entry.message?.model);
|
|
172
|
+
}
|
|
173
|
+
if (!this.modeDone)
|
|
174
|
+
this.visitForMode(record);
|
|
175
|
+
return (!this.withModel || this.tail.model !== undefined) && this.modeDone;
|
|
176
|
+
}
|
|
177
|
+
visitForMode(record) {
|
|
178
|
+
const modeEntry = record('"permissionMode"');
|
|
179
|
+
if (modeEntry?.type === "user" &&
|
|
180
|
+
typeof modeEntry.permissionMode === "string" &&
|
|
181
|
+
modeEntry.permissionMode.trim() !== "") {
|
|
182
|
+
const mode = modeEntry.permissionMode.trim();
|
|
183
|
+
this.modeDone = true;
|
|
184
|
+
if (!(this.planExited && mode === "plan"))
|
|
185
|
+
this.tail.permissionMode = mode;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (record('"plan_mode_exit"')?.attachment?.type === "plan_mode_exit") {
|
|
189
|
+
this.planExited = true;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const human = record('"human"');
|
|
193
|
+
if (human?.type === "user" && human.origin?.kind === "human")
|
|
194
|
+
this.modeDone = true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** The parsed record of one line, or null for a subagent record or a bad line. */
|
|
198
|
+
function parseMainThreadRecord(line) {
|
|
131
199
|
let entry;
|
|
132
200
|
try {
|
|
133
201
|
entry = JSON.parse(line.toString("utf8"));
|
|
134
202
|
}
|
|
135
203
|
catch {
|
|
136
|
-
return
|
|
204
|
+
return null;
|
|
137
205
|
}
|
|
138
206
|
if (!entry || typeof entry !== "object")
|
|
139
|
-
return
|
|
207
|
+
return null;
|
|
140
208
|
const record = entry;
|
|
141
|
-
|
|
142
|
-
return undefined;
|
|
143
|
-
return concreteModel(record.message?.model);
|
|
209
|
+
return record.isSidechain === true ? null : record;
|
|
144
210
|
}
|
package/dist/session-mode.d.ts
CHANGED
|
@@ -41,6 +41,20 @@ type InitializeSessionModeParams = {
|
|
|
41
41
|
/** When false, bypassPermissions is omitted from the mode catalog. */
|
|
42
42
|
allowBypass: boolean;
|
|
43
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* The permission mode a new Claude query starts in.
|
|
46
|
+
*
|
|
47
|
+
* An explicit mode wins. A resume or a load continues in the mode of the last
|
|
48
|
+
* user record of the transcript. If that mode is unknown or not available now,
|
|
49
|
+
* for example bypass, the session starts in `defaultMode` of the settings.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveInitialPermissionMode(sources: {
|
|
52
|
+
explicit?: PermissionMode;
|
|
53
|
+
resumed?: string;
|
|
54
|
+
defaultMode?: unknown;
|
|
55
|
+
}, logger: {
|
|
56
|
+
error: (...args: unknown[]) => void;
|
|
57
|
+
}, allowBypass: boolean): PermissionMode;
|
|
44
58
|
/** Owns session-mode policy and the ACP/SDK synchronization it requires. */
|
|
45
59
|
export declare class SessionModeManager<S extends SessionMode> {
|
|
46
60
|
private readonly options;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-mode.d.ts","sourceRoot":"","sources":["../src/session-mode.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC5B,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EACV,SAAS,EACT,cAAc,EACd,gBAAgB,EAChB,KAAK,EACN,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"session-mode.d.ts","sourceRoot":"","sources":["../src/session-mode.ts"],"names":[],"mappings":"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"}
|