@lucascouts/claude-agent-tui 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +1 -1
- package/README.md +1 -1
- package/dist/acp-agent.d.ts +62 -8
- package/dist/acp-agent.js +130 -15
- package/dist/agent-catalog.d.ts +0 -1
- package/dist/ansi-mirror.d.ts +0 -1
- package/dist/besteffort.d.ts +0 -1
- package/dist/billing/entrypoint-guard.d.ts +0 -1
- package/dist/claude-path.d.ts +0 -1
- package/dist/command-catalog.d.ts +84 -0
- package/dist/command-catalog.js +339 -0
- package/dist/diff-enriched-reader.d.ts +0 -1
- package/dist/diff-source.d.ts +0 -1
- package/dist/drift-checks.d.ts +0 -1
- package/dist/end-of-turn.d.ts +0 -1
- package/dist/engine-lifecycle.d.ts +0 -1
- package/dist/engine-pty.d.ts +0 -1
- package/dist/engine-watcher.d.ts +0 -1
- package/dist/engine.d.ts +0 -1
- package/dist/event-switch.d.ts +0 -1
- package/dist/gate/port.d.ts +0 -1
- package/dist/gate/settings-writer.d.ts +0 -1
- package/dist/image-input.d.ts +0 -1
- package/dist/image-vision-smoke.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/jsonl.d.ts +0 -1
- package/dist/lib.d.ts +0 -1
- package/dist/linearize.d.ts +1 -2
- package/dist/linearize.js +1 -1
- package/dist/live-diff-env.d.ts +0 -1
- package/dist/live-subagent-env.d.ts +0 -1
- package/dist/mcp-config-writer.d.ts +0 -1
- package/dist/model-catalog.d.ts +39 -1
- package/dist/model-catalog.js +77 -7
- package/dist/permissions/allow-inject.d.ts +0 -1
- package/dist/permissions/deny.d.ts +12 -1
- package/dist/permissions/deny.js +18 -0
- package/dist/permissions/elicitation-bridge.d.ts +71 -0
- package/dist/permissions/elicitation-bridge.js +146 -0
- package/dist/permissions/gate-wiring.d.ts +23 -3
- package/dist/permissions/gate-wiring.js +123 -1
- package/dist/permissions/hook-server.d.ts +11 -3
- package/dist/permissions/hook-server.js +10 -1
- package/dist/permissions/permission-mode.d.ts +0 -1
- package/dist/permissions/request-permission.d.ts +0 -1
- package/dist/settings.d.ts +0 -1
- package/dist/stop-reason-map.d.ts +0 -1
- package/dist/subagent-gate.d.ts +0 -1
- package/dist/subagent-source.d.ts +0 -1
- package/dist/subagent-watcher.d.ts +0 -1
- package/dist/tools.d.ts +0 -1
- package/dist/usage-env.d.ts +0 -1
- package/dist/usage.d.ts +0 -1
- package/dist/utils.d.ts +0 -1
- package/dist/zed-register.d.ts +0 -1
- package/package.json +6 -3
- package/dist/acp-agent.d.ts.map +0 -1
- package/dist/agent-catalog.d.ts.map +0 -1
- package/dist/ansi-mirror.d.ts.map +0 -1
- package/dist/besteffort.d.ts.map +0 -1
- package/dist/billing/entrypoint-guard.d.ts.map +0 -1
- package/dist/claude-path.d.ts.map +0 -1
- package/dist/diff-enriched-reader.d.ts.map +0 -1
- package/dist/diff-source.d.ts.map +0 -1
- package/dist/drift-checks.d.ts.map +0 -1
- package/dist/end-of-turn.d.ts.map +0 -1
- package/dist/engine-lifecycle.d.ts.map +0 -1
- package/dist/engine-pty.d.ts.map +0 -1
- package/dist/engine-watcher.d.ts.map +0 -1
- package/dist/engine.d.ts.map +0 -1
- package/dist/event-switch.d.ts.map +0 -1
- package/dist/gate/port.d.ts.map +0 -1
- package/dist/gate/settings-writer.d.ts.map +0 -1
- package/dist/image-input.d.ts.map +0 -1
- package/dist/image-vision-smoke.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/jsonl.d.ts.map +0 -1
- package/dist/lib.d.ts.map +0 -1
- package/dist/linearize.d.ts.map +0 -1
- package/dist/live-diff-env.d.ts.map +0 -1
- package/dist/live-subagent-env.d.ts.map +0 -1
- package/dist/mcp-config-writer.d.ts.map +0 -1
- package/dist/model-catalog.d.ts.map +0 -1
- package/dist/permissions/allow-inject.d.ts.map +0 -1
- package/dist/permissions/deny.d.ts.map +0 -1
- package/dist/permissions/gate-wiring.d.ts.map +0 -1
- package/dist/permissions/hook-server.d.ts.map +0 -1
- package/dist/permissions/permission-mode.d.ts.map +0 -1
- package/dist/permissions/request-permission.d.ts.map +0 -1
- package/dist/settings.d.ts.map +0 -1
- package/dist/stop-reason-map.d.ts.map +0 -1
- package/dist/subagent-gate.d.ts.map +0 -1
- package/dist/subagent-source.d.ts.map +0 -1
- package/dist/subagent-watcher.d.ts.map +0 -1
- package/dist/tools.d.ts.map +0 -1
- package/dist/usage-env.d.ts.map +0 -1
- package/dist/usage.d.ts.map +0 -1
- package/dist/utils.d.ts.map +0 -1
- package/dist/zed-register.d.ts.map +0 -1
package/NOTICE
CHANGED
|
@@ -10,5 +10,5 @@ This product is a derivative work of:
|
|
|
10
10
|
|
|
11
11
|
The engine was rewritten to drive the Claude Code subscription TUI over a
|
|
12
12
|
pseudo-terminal (PTY) instead of calling the Claude Agent SDK. See
|
|
13
|
-
.fork-provenance.json (fork point: v0.
|
|
13
|
+
.fork-provenance.json (fork point: v0.53.0) and CHANGELOG.md for the scope
|
|
14
14
|
of modifications.
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
An [ACP](https://agentclientprotocol.com)-compatible agent that drives the **Claude Code subscription TUI** over a PTY, so your Claude Code threads render natively in [Zed](https://zed.dev) and other ACP clients.
|
|
6
6
|
|
|
7
|
-
> **Fork** of [`@agentclientprotocol/claude-agent-acp`](https://github.com/agentclientprotocol/claude-agent-acp) v0.
|
|
7
|
+
> **Fork** of [`@agentclientprotocol/claude-agent-acp`](https://github.com/agentclientprotocol/claude-agent-acp) v0.53.0. Where the upstream adapter calls the Claude Agent **SDK**, this fork spawns the `claude` **subscription CLI** in a pseudo-terminal and translates its JSONL transcript into ACP `session/update` notifications. See [`.fork-provenance.json`](.fork-provenance.json) for the exact fork point.
|
|
8
8
|
|
|
9
9
|
## Why this exists
|
|
10
10
|
|
package/dist/acp-agent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Agent, AgentSideConnection, AuthenticateRequest, CancelNotification, ClientCapabilities, ForkSessionRequest, ForkSessionResponse, InitializeRequest, InitializeResponse, ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, LoadSessionResponse, NewSessionRequest, NewSessionResponse, PromptRequest, PromptResponse, ReadTextFileRequest, ReadTextFileResponse, ResumeSessionRequest, ResumeSessionResponse, SessionConfigOption, SessionModeState, SessionNotification, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest, SetSessionModeResponse, CloseSessionRequest, CloseSessionResponse, DeleteSessionRequest, DeleteSessionResponse, TerminalHandle, TerminalOutputResponse, WriteTextFileRequest, WriteTextFileResponse } from "@agentclientprotocol/sdk";
|
|
1
|
+
import { Agent, AgentSideConnection, AuthenticateRequest, CancelNotification, ClientCapabilities, ForkSessionRequest, ForkSessionResponse, InitializeRequest, InitializeResponse, ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, LoadSessionResponse, NewSessionRequest, NewSessionResponse, PromptRequest, PromptResponse, ReadTextFileRequest, ReadTextFileResponse, ResumeSessionRequest, ResumeSessionResponse, SessionConfigOption, SessionModeState, SessionNotification, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest, SetSessionModeResponse, CloseSessionRequest, CloseSessionResponse, DeleteSessionRequest, DeleteSessionResponse, LogoutRequest, LogoutResponse, TerminalHandle, TerminalOutputResponse, WriteTextFileRequest, WriteTextFileResponse, AvailableCommand } from "@agentclientprotocol/sdk";
|
|
2
2
|
import { ModelInfo, Options, PermissionMode, PermissionUpdate, SDKMessageOrigin, SDKPartialAssistantMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
3
3
|
import { ContentBlockParam } from "@anthropic-ai/sdk/resources";
|
|
4
4
|
import { BetaContentBlock, BetaRawContentBlockDelta } from "@anthropic-ai/sdk/resources/beta.mjs";
|
|
@@ -31,6 +31,7 @@ type AccumulatedUsage = {
|
|
|
31
31
|
cachedReadTokens: number;
|
|
32
32
|
cachedWriteTokens: number;
|
|
33
33
|
};
|
|
34
|
+
export declare const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
34
35
|
type Session = {
|
|
35
36
|
/** The live PTY handle (story 013/014) running the subscription `claude` TUI for this session. */
|
|
36
37
|
pty: IPty;
|
|
@@ -107,11 +108,14 @@ type Session = {
|
|
|
107
108
|
*/
|
|
108
109
|
agents?: AgentCatalogEntry[];
|
|
109
110
|
configOptions: SessionConfigOption[];
|
|
110
|
-
/** Context window size
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
111
|
+
/** Context window size for the session, carried across prompts so mid-stream
|
|
112
|
+
* usage_update notifications report a correct `size` before the turn's first
|
|
113
|
+
* result message arrives. Seeded by `inferContextWindowFromModel` (the static
|
|
114
|
+
* `MODEL_CONTEXT_WINDOWS` curation) and re-resolved when the user switches the
|
|
115
|
+
* session's model. NOTE (story 068): there is NO `result.modelUsage` refresh —
|
|
116
|
+
* the JSONL `usage` block carries only token counts, never a window; the window
|
|
117
|
+
* comes from static curation (the Models API `max_input_tokens` is the real
|
|
118
|
+
* authority, which this PTY/JSONL fork does not call). */
|
|
115
119
|
contextWindowSize: number;
|
|
116
120
|
/** Accumulated task list for the session, keyed by task ID. Task IDs are
|
|
117
121
|
* per-session, so this state must not be shared across sessions. */
|
|
@@ -415,6 +419,13 @@ export interface AgentDeps {
|
|
|
415
419
|
* `~/.claude/agents`. Production passes nothing → the real disk glob.
|
|
416
420
|
*/
|
|
417
421
|
discoverAgents?: (cwd: string) => AgentCatalogEntry[];
|
|
422
|
+
/**
|
|
423
|
+
* Story 063 (R1/R1.1) — override the offline command discovery `sendAvailableCommandsUpdate` sources
|
|
424
|
+
* the `available_commands_update` set from (default: the disk-only {@link discoverCommands}, keyed on
|
|
425
|
+
* the session cwd). Injected by the wiring test with an in-memory fake so the surface is exercised
|
|
426
|
+
* hermetically, never touching the real `~/.claude`. Production passes nothing → the real disk scan.
|
|
427
|
+
*/
|
|
428
|
+
discoverCommands?: (cwd: string) => AvailableCommand[];
|
|
418
429
|
/**
|
|
419
430
|
* Story 056 (#812) — override the SDK session-metadata reader the end-of-turn `session_info_update`
|
|
420
431
|
* push sources the title from (default: the pure {@link getSessionInfo} from the agent SDK, which
|
|
@@ -560,6 +571,14 @@ export declare class ClaudeAcpAgent implements Agent {
|
|
|
560
571
|
[key: string]: BackgroundTerminal;
|
|
561
572
|
};
|
|
562
573
|
clientCapabilities?: ClientCapabilities;
|
|
574
|
+
/**
|
|
575
|
+
* Story 065 (R1/R3) — did the client advertise `clientCapabilities.elicitation.form`
|
|
576
|
+
* at initialize? Presence-based (a present `form` may legitimately be an empty `{}`,
|
|
577
|
+
* so this is derived with `!= null`, NOT property truthiness). The 065 gate (task 3.1)
|
|
578
|
+
* reads this to decide relay-via-elicitation (R1) vs the story-064 deny fallback (R3).
|
|
579
|
+
* Defaults `false` so a client that never advertised elicitation falls back safely.
|
|
580
|
+
*/
|
|
581
|
+
clientSupportsElicitationForm: boolean;
|
|
563
582
|
logger: Logger;
|
|
564
583
|
gatewayAuthRequest?: GatewayAuthRequest;
|
|
565
584
|
engine: Engine;
|
|
@@ -587,6 +606,8 @@ export declare class ClaudeAcpAgent implements Agent {
|
|
|
587
606
|
private readonly gateOptions?;
|
|
588
607
|
/** Story 056 (R3.2) — main-thread agent-persona discovery seam; see {@link AgentDeps.discoverAgents}. */
|
|
589
608
|
private readonly discoverAgents;
|
|
609
|
+
/** Story 063 (R1/R1.1) — offline `available_commands` discovery seam; see {@link AgentDeps.discoverCommands}. */
|
|
610
|
+
private readonly discoverCommands;
|
|
590
611
|
/** Story 056 (#812) — SDK session-metadata reader for the end-of-turn title push; see
|
|
591
612
|
* {@link AgentDeps.getSessionInfo}. */
|
|
592
613
|
private readonly getSessionInfo;
|
|
@@ -600,6 +621,17 @@ export declare class ClaudeAcpAgent implements Agent {
|
|
|
600
621
|
loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse>;
|
|
601
622
|
listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse>;
|
|
602
623
|
authenticate(_params: AuthenticateRequest): Promise<void>;
|
|
624
|
+
/**
|
|
625
|
+
* ACP `logout` (acp-sdk 1.0.0, acp.d.ts:1646). Under the PTY engine the bridge
|
|
626
|
+
* authenticates lazily and only tracks an in-memory `gatewayAuthRequest`; the
|
|
627
|
+
* interactive `claude` TUI owns the on-disk credential lifecycle. So `logout`
|
|
628
|
+
* here drops the in-memory auth intent and re-offers a clean handshake on the
|
|
629
|
+
* next `initialize()` (authMethods are recomputed there, unconditioned by this
|
|
630
|
+
* field). It does NOT read/write/delete `~/.claude` (billing seam — story 062
|
|
631
|
+
* R2) and never bridges `/logout` to the PTY (R3). Idempotent with no prior
|
|
632
|
+
* authenticate() (R4); active sessions are untouched (R6).
|
|
633
|
+
*/
|
|
634
|
+
logout(_params: LogoutRequest): Promise<LogoutResponse | void>;
|
|
603
635
|
prompt(params: PromptRequest): Promise<PromptResponse>;
|
|
604
636
|
/**
|
|
605
637
|
* Story 056 (#812) — push the sanitized session title to the client via `session_info_update`,
|
|
@@ -617,7 +649,7 @@ export declare class ClaudeAcpAgent implements Agent {
|
|
|
617
649
|
/** Tear down all active sessions. Called when the ACP connection closes. */
|
|
618
650
|
dispose(): Promise<void>;
|
|
619
651
|
closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse>;
|
|
620
|
-
|
|
652
|
+
deleteSession(params: DeleteSessionRequest): Promise<DeleteSessionResponse>;
|
|
621
653
|
setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse>;
|
|
622
654
|
setSessionConfigOption(params: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse>;
|
|
623
655
|
/**
|
|
@@ -895,5 +927,27 @@ export declare function runAcp(deps?: AgentDeps): {
|
|
|
895
927
|
connection: AgentSideConnection;
|
|
896
928
|
agent: ClaudeAcpAgent;
|
|
897
929
|
};
|
|
930
|
+
/** Resolve a model alias's context window (the usage_update `size` denominator).
|
|
931
|
+
* NOTE (story 068): there is NO `result.modelUsage` window to refresh from — the
|
|
932
|
+
* JSONL `usage` carries only token counts; the window comes from static curation
|
|
933
|
+
* (the Models API `max_input_tokens` is the real authority, which this fork does
|
|
934
|
+
* not call), as detailed below.
|
|
935
|
+
*
|
|
936
|
+
* Story 068 (R1, R1.1, R1.2): consults the static {@link MODEL_CONTEXT_WINDOWS}
|
|
937
|
+
* alias→window map FIRST (an exact catalog-`value` hit — `opus`=1M, `sonnet`=200K,
|
|
938
|
+
* `sonnet[1m]`=1M, `haiku`=200K, `default`/`opusplan`=200K conservative). This
|
|
939
|
+
* fixes `opus` having wrongly reported 200K. An alias absent from the map then
|
|
940
|
+
* falls back to the legacy `\b1m\b` inference: Anthropic 1M-context variants
|
|
941
|
+
* encode "1m" as a distinct token in the SDK model ID (e.g., "claude-opus-4-6-1m"),
|
|
942
|
+
* which `\b1m\b` catches without also matching "10m" or embedded substrings.
|
|
943
|
+
* `null` (fully unknown) is intentional — the two call sites apply
|
|
944
|
+
* `?? DEFAULT_CONTEXT_WINDOW`. */
|
|
945
|
+
export declare function inferContextWindowFromModel(model: string): number | null;
|
|
946
|
+
/** Story 069 (R1) — AUTHORITATIVE context window from a turn's REAL model ID (the JSONL `model`
|
|
947
|
+
* field), used by the pump to refine the alias seed once the model is known. Exact-ID lookup first
|
|
948
|
+
* (MODEL_ID_CONTEXT_WINDOWS), then a family+version heuristic for dated snapshots / future variants
|
|
949
|
+
* (Opus is NOT uniform: 4.6 and earlier = 200K, 4.7+ = 1M; Sonnet 4.x = 200K but Sonnet 5+ = 1M;
|
|
950
|
+
* haiku = 200K; fable = 1M — story 071), then a
|
|
951
|
+
* `\b1m\b` suffix, then null (R1.3: a missing / non-string id never refines). */
|
|
952
|
+
export declare function inferContextWindowFromModelId(id: string): number | null;
|
|
898
953
|
export {};
|
|
899
|
-
//# sourceMappingURL=acp-agent.d.ts.map
|
package/dist/acp-agent.js
CHANGED
|
@@ -24,12 +24,16 @@ import { guardEvent } from "./billing/entrypoint-guard.js";
|
|
|
24
24
|
import { usageUpdatesFor } from "./usage.js";
|
|
25
25
|
import { createTurnResolver } from "./end-of-turn.js";
|
|
26
26
|
import { sendPrompt } from "./engine-pty.js";
|
|
27
|
-
import { MODEL_CATALOG, DEFAULT_MODEL_INFO, ULTRACODE_EFFORT, ULTRACODE_EFFORT_LEVEL, ULTRACODE_EFFORT_LABEL, } from "./model-catalog.js";
|
|
27
|
+
import { MODEL_CATALOG, MODEL_CONTEXT_WINDOWS, MODEL_ID_CONTEXT_WINDOWS, modelSelectorDescription, DEFAULT_MODEL_INFO, ULTRACODE_EFFORT, ULTRACODE_EFFORT_LEVEL, ULTRACODE_EFFORT_LABEL, } from "./model-catalog.js";
|
|
28
28
|
// Story 060 (R2.2/R2.3/R3.2) — the declarative spawn-time complement to the live ultracode keyword:
|
|
29
29
|
// toggle the {ultracode,ultracodeKeywordTrigger} keys in the gate's per-session scratch settings file
|
|
30
30
|
// (preserving the hook + every other key). Lives in the gate's settings-writer so it reuses durableWrite.
|
|
31
31
|
import { applyUltracodeSettings } from "./gate/settings-writer.js";
|
|
32
32
|
import { discoverAgents } from "./agent-catalog.js";
|
|
33
|
+
// Story 063 (R1) — OFFLINE disk discovery of the `available_commands` set (custom slash-commands +
|
|
34
|
+
// skills + enabled-plugin surfaces + built-ins), keyed on the session cwd. Populates the
|
|
35
|
+
// `available_commands_update` the session emits at creation in place of the old unconditional `[]`.
|
|
36
|
+
import { discoverCommands } from "./command-catalog.js";
|
|
33
37
|
import { setupSessionGate } from "./permissions/gate-wiring.js";
|
|
34
38
|
// Story 057 / Task 2.3 — MCP scratch-file lifecycle (translate ACP servers → claude `--mcp-config`
|
|
35
39
|
// JSON, durable 0600 write, idempotent teardown removal). Mirrors the gate's settings-scratch
|
|
@@ -50,7 +54,7 @@ function sanitizeTitle(text) {
|
|
|
50
54
|
}
|
|
51
55
|
return sanitized.slice(0, MAX_TITLE_LENGTH - 1) + "…";
|
|
52
56
|
}
|
|
53
|
-
const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
57
|
+
export const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
54
58
|
/**
|
|
55
59
|
* No-op {@link IPty} stub for the Degrau-1 replay-only load path: there is no live `claude` process,
|
|
56
60
|
* but the session record's `pty` field is typed `IPty`. Every method is inert — teardown's `kill()`
|
|
@@ -532,6 +536,14 @@ function deriveSubagentLabel(messages, parentId) {
|
|
|
532
536
|
export class ClaudeAcpAgent {
|
|
533
537
|
constructor(client, logger, engine = createStubEngine(), deps = {}) {
|
|
534
538
|
this.backgroundTerminals = {};
|
|
539
|
+
/**
|
|
540
|
+
* Story 065 (R1/R3) — did the client advertise `clientCapabilities.elicitation.form`
|
|
541
|
+
* at initialize? Presence-based (a present `form` may legitimately be an empty `{}`,
|
|
542
|
+
* so this is derived with `!= null`, NOT property truthiness). The 065 gate (task 3.1)
|
|
543
|
+
* reads this to decide relay-via-elicitation (R1) vs the story-064 deny fallback (R3).
|
|
544
|
+
* Defaults `false` so a client that never advertised elicitation falls back safely.
|
|
545
|
+
*/
|
|
546
|
+
this.clientSupportsElicitationForm = false;
|
|
535
547
|
/** Live PTY-engine registry shared with the per-session engines (story 014 cleanup map). */
|
|
536
548
|
this.engines = new Map();
|
|
537
549
|
this.sessions = {};
|
|
@@ -571,12 +583,27 @@ export class ClaudeAcpAgent {
|
|
|
571
583
|
// Story 056 (R3.2): main-thread agent-persona discovery — defaults to the glob-only
|
|
572
584
|
// discoverAgents; tests inject an in-memory fake so the `agent` surface is hermetic.
|
|
573
585
|
this.discoverAgents = deps.discoverAgents ?? discoverAgents;
|
|
586
|
+
// Story 063 (R1/R1.1): offline `available_commands` discovery — defaults to the disk-only
|
|
587
|
+
// discoverCommands; tests inject an in-memory fake so the surface is hermetic (no real ~/.claude read).
|
|
588
|
+
this.discoverCommands = deps.discoverCommands ?? discoverCommands;
|
|
574
589
|
// Story 056 (#812): end-of-turn session_info_update title source — defaults to the pure SDK
|
|
575
590
|
// getSessionInfo; tests inject an in-memory fake so the push is hermetic (no real ~/.claude read).
|
|
576
591
|
this.getSessionInfo = deps.getSessionInfo ?? getSessionInfo;
|
|
577
592
|
}
|
|
578
593
|
async initialize(request) {
|
|
579
594
|
this.clientCapabilities = request.clientCapabilities;
|
|
595
|
+
// Story 065 (R1/R3): capability negotiation — a present (non-null) `elicitation.form`
|
|
596
|
+
// means the client supports form elicitation. Presence-based detection is deliberate: both
|
|
597
|
+
// `undefined` and `null` are unsupported, and an empty `{}` `form` IS supported (the UNSTABLE
|
|
598
|
+
// ElicitationFormCapabilities type carries only an optional `_meta`, so a present `form` is
|
|
599
|
+
// legitimately `{}` — detection MUST be presence-based, not truthiness). Written as explicit
|
|
600
|
+
// `!== undefined && !== null` (not `!= null`) to satisfy the eqeqeq lint rule.
|
|
601
|
+
const elicitationForm = request.clientCapabilities?.elicitation?.form;
|
|
602
|
+
this.clientSupportsElicitationForm = elicitationForm !== undefined && elicitationForm !== null;
|
|
603
|
+
// Story 065 (Task 6.1 live-probe): make the negotiated capability observable in the Zed logs so
|
|
604
|
+
// the in-Zed verdict (form rendered vs. gated-dormant behind the 064 deny) is deterministic. Goes
|
|
605
|
+
// to STDERR via logger.error — NEVER stdout, which carries the ACP ndJson stream.
|
|
606
|
+
this.logger.error(`[065] clientCapabilities.elicitation.form advertised: ${this.clientSupportsElicitationForm}`);
|
|
580
607
|
// Bypasses standard auth by routing requests through a custom Anthropic-protocol gateway.
|
|
581
608
|
// Only offered when the client advertises `auth._meta.gateway` capability.
|
|
582
609
|
const supportsGatewayAuth = request.clientCapabilities?.auth?._meta?.gateway === true;
|
|
@@ -780,6 +807,19 @@ export class ClaudeAcpAgent {
|
|
|
780
807
|
}
|
|
781
808
|
throw new Error("Method not implemented.");
|
|
782
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* ACP `logout` (acp-sdk 1.0.0, acp.d.ts:1646). Under the PTY engine the bridge
|
|
812
|
+
* authenticates lazily and only tracks an in-memory `gatewayAuthRequest`; the
|
|
813
|
+
* interactive `claude` TUI owns the on-disk credential lifecycle. So `logout`
|
|
814
|
+
* here drops the in-memory auth intent and re-offers a clean handshake on the
|
|
815
|
+
* next `initialize()` (authMethods are recomputed there, unconditioned by this
|
|
816
|
+
* field). It does NOT read/write/delete `~/.claude` (billing seam — story 062
|
|
817
|
+
* R2) and never bridges `/logout` to the PTY (R3). Idempotent with no prior
|
|
818
|
+
* authenticate() (R4); active sessions are untouched (R6).
|
|
819
|
+
*/
|
|
820
|
+
async logout(_params) {
|
|
821
|
+
this.gatewayAuthRequest = undefined;
|
|
822
|
+
}
|
|
783
823
|
async prompt(params) {
|
|
784
824
|
const sessionRecord = this.sessions[params.sessionId];
|
|
785
825
|
if (!sessionRecord) {
|
|
@@ -983,6 +1023,8 @@ export class ClaudeAcpAgent {
|
|
|
983
1023
|
}
|
|
984
1024
|
/** Tear down all active sessions. Called when the ACP connection closes. */
|
|
985
1025
|
async dispose() {
|
|
1026
|
+
// Drop the in-memory auth intent on teardown (story 062 R7) — same clear as logout().
|
|
1027
|
+
this.gatewayAuthRequest = undefined;
|
|
986
1028
|
await Promise.all(Object.keys(this.sessions).map((id) => this.teardownSession(id)));
|
|
987
1029
|
}
|
|
988
1030
|
async closeSession(params) {
|
|
@@ -992,7 +1034,7 @@ export class ClaudeAcpAgent {
|
|
|
992
1034
|
await this.teardownSession(params.sessionId);
|
|
993
1035
|
return {};
|
|
994
1036
|
}
|
|
995
|
-
async
|
|
1037
|
+
async deleteSession(params) {
|
|
996
1038
|
// Tear down any active in-memory state first so the on-disk file isn't
|
|
997
1039
|
// recreated by an outstanding query writing to it.
|
|
998
1040
|
if (this.sessions[params.sessionId]) {
|
|
@@ -1585,6 +1627,14 @@ export class ClaudeAcpAgent {
|
|
|
1585
1627
|
// in pumpUpdates, so the replay-only load never emitted it).
|
|
1586
1628
|
if (session && !session.usageDisabled) {
|
|
1587
1629
|
const carrier = turn.message.message ?? {};
|
|
1630
|
+
// Story 069 (R1) — refine the window from the turn's REAL model (the JSONL `model`), authoritatively
|
|
1631
|
+
// correcting the alias seed (e.g. default → claude-opus-4-8[1m] → 1M). A missing / non-string model or
|
|
1632
|
+
// an unknown id leaves the current value unchanged (R1.3 — never overwrite with null).
|
|
1633
|
+
const realModel = carrier.model;
|
|
1634
|
+
if (typeof realModel === "string" && realModel.length > 0) {
|
|
1635
|
+
session.contextWindowSize =
|
|
1636
|
+
inferContextWindowFromModelId(realModel) ?? session.contextWindowSize;
|
|
1637
|
+
}
|
|
1588
1638
|
for (const usageUpdate of usageUpdatesFor(carrier, {
|
|
1589
1639
|
usageUpdate: this.usageUpdate,
|
|
1590
1640
|
contextWindowSize: session.contextWindowSize,
|
|
@@ -1914,15 +1964,26 @@ export class ClaudeAcpAgent {
|
|
|
1914
1964
|
const session = this.sessions[sessionId];
|
|
1915
1965
|
if (!session)
|
|
1916
1966
|
return;
|
|
1917
|
-
// === SEAM(023) Group 1:
|
|
1918
|
-
// `query.supportedCommands()`
|
|
1919
|
-
//
|
|
1920
|
-
//
|
|
1967
|
+
// === SEAM(023) Group 1: `available_commands_update`. Historically the SDK
|
|
1968
|
+
// `query.supportedCommands()` was dropped (slash commands are owned by the interactive TUI and are
|
|
1969
|
+
// not enumerable over the read-only JSONL path), so Degrau-1 emitted a static empty set.
|
|
1970
|
+
// Story 063 (R1/R1.1) now POPULATES this set OFFLINE from disk — `discoverCommands(session.cwd)`
|
|
1971
|
+
// scans the cwd/user `.claude/{commands,skills}`, the enabled-plugin surfaces, and the built-in
|
|
1972
|
+
// tier — instead of the unconditional `[]`. Discovery is SYNCHRONOUS but the 4 call-sites invoke
|
|
1973
|
+
// this method fire-and-forget (`setTimeout(0)`), so it never blocks session creation (R4).
|
|
1974
|
+
// Degrau 2 (030/032): PTY-backed control — surface the TUI's real live command set. ===
|
|
1975
|
+
let availableCommands;
|
|
1976
|
+
try {
|
|
1977
|
+
availableCommands = this.discoverCommands(session.cwd);
|
|
1978
|
+
}
|
|
1979
|
+
catch {
|
|
1980
|
+
availableCommands = []; // R4 — discovery must NEVER crash the session; degrade to []
|
|
1981
|
+
}
|
|
1921
1982
|
await this.client.sessionUpdate({
|
|
1922
1983
|
sessionId,
|
|
1923
1984
|
update: {
|
|
1924
1985
|
sessionUpdate: "available_commands_update",
|
|
1925
|
-
availableCommands
|
|
1986
|
+
availableCommands,
|
|
1926
1987
|
},
|
|
1927
1988
|
});
|
|
1928
1989
|
}
|
|
@@ -2113,6 +2174,11 @@ export class ClaudeAcpAgent {
|
|
|
2113
2174
|
gate = await setupSessionGate({
|
|
2114
2175
|
...this.gateOptions,
|
|
2115
2176
|
client: this.client,
|
|
2177
|
+
// Story 065 (R1/R3): negotiated in initialize() from clientCapabilities.elicitation.form. When
|
|
2178
|
+
// true the gate drives AskUserQuestion through a real ACP form elicitation; when false it keeps
|
|
2179
|
+
// the story-064 fail-closed deny-guard. this.client (AgentSideConnection) already satisfies the
|
|
2180
|
+
// broadened client type (it has unstable_createElicitation).
|
|
2181
|
+
clientSupportsElicitationForm: this.clientSupportsElicitationForm,
|
|
2116
2182
|
onWarn: (m) => this.logger.error(m),
|
|
2117
2183
|
});
|
|
2118
2184
|
}
|
|
@@ -2326,7 +2392,9 @@ function buildConfigOptions(modes, currentModelId, modelInfos, currentEffortLeve
|
|
|
2326
2392
|
options: modelInfos.map((m) => ({
|
|
2327
2393
|
value: m.value,
|
|
2328
2394
|
name: m.displayName,
|
|
2329
|
-
|
|
2395
|
+
// Story 072 — prepend the version/context label ("Opus 4.8 with 1M context · <tagline>"),
|
|
2396
|
+
// mirroring the live `/model` picker; bare tagline when no label (e.g. opusplan).
|
|
2397
|
+
description: modelSelectorDescription(m) || undefined,
|
|
2330
2398
|
})),
|
|
2331
2399
|
},
|
|
2332
2400
|
];
|
|
@@ -2955,13 +3023,60 @@ export function runAcp(deps) {
|
|
|
2955
3023
|
}, stream);
|
|
2956
3024
|
return { connection, agent };
|
|
2957
3025
|
}
|
|
2958
|
-
/**
|
|
2959
|
-
*
|
|
2960
|
-
*
|
|
2961
|
-
*
|
|
2962
|
-
*
|
|
2963
|
-
|
|
3026
|
+
/** Resolve a model alias's context window (the usage_update `size` denominator).
|
|
3027
|
+
* NOTE (story 068): there is NO `result.modelUsage` window to refresh from — the
|
|
3028
|
+
* JSONL `usage` carries only token counts; the window comes from static curation
|
|
3029
|
+
* (the Models API `max_input_tokens` is the real authority, which this fork does
|
|
3030
|
+
* not call), as detailed below.
|
|
3031
|
+
*
|
|
3032
|
+
* Story 068 (R1, R1.1, R1.2): consults the static {@link MODEL_CONTEXT_WINDOWS}
|
|
3033
|
+
* alias→window map FIRST (an exact catalog-`value` hit — `opus`=1M, `sonnet`=200K,
|
|
3034
|
+
* `sonnet[1m]`=1M, `haiku`=200K, `default`/`opusplan`=200K conservative). This
|
|
3035
|
+
* fixes `opus` having wrongly reported 200K. An alias absent from the map then
|
|
3036
|
+
* falls back to the legacy `\b1m\b` inference: Anthropic 1M-context variants
|
|
3037
|
+
* encode "1m" as a distinct token in the SDK model ID (e.g., "claude-opus-4-6-1m"),
|
|
3038
|
+
* which `\b1m\b` catches without also matching "10m" or embedded substrings.
|
|
3039
|
+
* `null` (fully unknown) is intentional — the two call sites apply
|
|
3040
|
+
* `?? DEFAULT_CONTEXT_WINDOW`. */
|
|
3041
|
+
export function inferContextWindowFromModel(model) {
|
|
3042
|
+
const mapped = MODEL_CONTEXT_WINDOWS[model];
|
|
3043
|
+
if (mapped !== undefined)
|
|
3044
|
+
return mapped; // exact alias hit (!== undefined, NOT truthiness)
|
|
2964
3045
|
if (/\b1m\b/i.test(model))
|
|
3046
|
+
return 1_000_000; // unknown alias that still encodes a 1m token
|
|
3047
|
+
return null; // caller applies ?? DEFAULT_CONTEXT_WINDOW
|
|
3048
|
+
}
|
|
3049
|
+
/** Story 069 (R1) — AUTHORITATIVE context window from a turn's REAL model ID (the JSONL `model`
|
|
3050
|
+
* field), used by the pump to refine the alias seed once the model is known. Exact-ID lookup first
|
|
3051
|
+
* (MODEL_ID_CONTEXT_WINDOWS), then a family+version heuristic for dated snapshots / future variants
|
|
3052
|
+
* (Opus is NOT uniform: 4.6 and earlier = 200K, 4.7+ = 1M; Sonnet 4.x = 200K but Sonnet 5+ = 1M;
|
|
3053
|
+
* haiku = 200K; fable = 1M — story 071), then a
|
|
3054
|
+
* `\b1m\b` suffix, then null (R1.3: a missing / non-string id never refines). */
|
|
3055
|
+
export function inferContextWindowFromModelId(id) {
|
|
3056
|
+
if (typeof id !== "string" || id.length === 0)
|
|
3057
|
+
return null;
|
|
3058
|
+
const exact = MODEL_ID_CONTEXT_WINDOWS[id];
|
|
3059
|
+
if (exact !== undefined)
|
|
3060
|
+
return exact;
|
|
3061
|
+
// An explicit long-context `[1m]`/`-1m` suffix wins over the family heuristic
|
|
3062
|
+
// (`claude-sonnet-…[1m]` = 1M, not 200K; `/model default` resolves to `claude-opus-4-8[1m]`).
|
|
3063
|
+
if (/\b1m\b/i.test(id))
|
|
3064
|
+
return 1_000_000;
|
|
3065
|
+
const opus = id.match(/claude-opus-(\d+)-(\d+)/);
|
|
3066
|
+
if (opus) {
|
|
3067
|
+
const major = Number(opus[1]);
|
|
3068
|
+
const minor = Number(opus[2]);
|
|
3069
|
+
return major > 4 || (major === 4 && minor >= 7) ? 1_000_000 : 200_000;
|
|
3070
|
+
}
|
|
3071
|
+
if (/claude-fable/.test(id))
|
|
2965
3072
|
return 1_000_000;
|
|
3073
|
+
// Sonnet is NOT uniform across generations (story 071): the subscription CLI serves Sonnet 4.x
|
|
3074
|
+
// at 200K but Sonnet 5+ natively at 1M (Sonnet 5 has no smaller context variant). Version-aware,
|
|
3075
|
+
// like the Opus 4-6 vs 4-7/4-8 split above; dated snapshots (`claude-sonnet-5-<date>`) match too.
|
|
3076
|
+
const sonnet = id.match(/claude-sonnet-(\d+)/);
|
|
3077
|
+
if (sonnet)
|
|
3078
|
+
return Number(sonnet[1]) >= 5 ? 1_000_000 : 200_000;
|
|
3079
|
+
if (/claude-haiku/.test(id))
|
|
3080
|
+
return 200_000;
|
|
2966
3081
|
return null;
|
|
2967
3082
|
}
|
package/dist/agent-catalog.d.ts
CHANGED
package/dist/ansi-mirror.d.ts
CHANGED
package/dist/besteffort.d.ts
CHANGED
|
@@ -94,4 +94,3 @@ export declare function guardEvent(event: WatchedMessage, hooks: GuardHooks): Gu
|
|
|
94
94
|
* is the spawn helper's anti-recusa concern, distinct from this billing check — §10).
|
|
95
95
|
*/
|
|
96
96
|
export declare function assertCleanBillingEnv(env: Record<string, string | undefined>): void;
|
|
97
|
-
//# sourceMappingURL=entrypoint-guard.d.ts.map
|
package/dist/claude-path.d.ts
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { AvailableCommand } from "@agentclientprotocol/sdk";
|
|
2
|
+
/**
|
|
3
|
+
* The curated built-in slash-command tier (R9) — the interactive `claude` TUI's own built-ins, which are
|
|
4
|
+
* baked into the binary and therefore NOT disk-discoverable. This is a CURATED APPROXIMATION (C1), NOT a
|
|
5
|
+
* live probe: it stands beside the same static-curation idea as `MODEL_CATALOG`. It is the LOWEST-
|
|
6
|
+
* precedence tier, so any disk command/skill of the same name shadows a built-in (R1/R9). Names are
|
|
7
|
+
* lowercase (R3) and every entry carries a non-empty `description` (the SDK requires it). The list is
|
|
8
|
+
* intentionally small and conservative — a documented approximation, not an exhaustive enumeration.
|
|
9
|
+
*/
|
|
10
|
+
export declare const BUILTIN_COMMANDS: readonly AvailableCommand[];
|
|
11
|
+
/**
|
|
12
|
+
* Injectable seams for {@link discoverCommands} — defaults wire the `node:` stdlib and the built-in
|
|
13
|
+
* tier. Tests pass fakes so discovery is exercised against an in-memory fs and an isolated (`[]`)
|
|
14
|
+
* built-in tier, never the real disk or the real `~/.claude` (R5).
|
|
15
|
+
*/
|
|
16
|
+
export interface DiscoverCommandsDeps {
|
|
17
|
+
/** Resolve the user's home directory (default: `os.homedir`). */
|
|
18
|
+
homedir?: () => string;
|
|
19
|
+
/** List the `*.md` filenames in `dir`; MUST return `[]` when `dir` is missing/unreadable. */
|
|
20
|
+
readdirMd?: (dir: string) => string[];
|
|
21
|
+
/**
|
|
22
|
+
* List the IMMEDIATE sub-directory names of `dir` (for the skills tier — R7); MUST return `[]` when
|
|
23
|
+
* `dir` is missing/unreadable. Default: {@link defaultReaddirDirs}.
|
|
24
|
+
*/
|
|
25
|
+
readdirDirs?: (dir: string) => string[];
|
|
26
|
+
/** Read a file's UTF-8 contents (default: `fs.readFileSync(p, "utf8")`). */
|
|
27
|
+
readFile?: (path: string) => string;
|
|
28
|
+
/** The built-in command tier (lowest precedence). Default: {@link BUILTIN_COMMANDS}. */
|
|
29
|
+
builtins?: readonly AvailableCommand[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The command-name allowlist (R3). A single un-namespaced segment: LOWERCASE letters/digits/underscore/
|
|
33
|
+
* hyphen only (no spaces, no uppercase, no shell metacharacters, no path separators). Slash-command
|
|
34
|
+
* names are conventionally lowercase, so — unlike `agent-catalog.ts`'s {@link SAFE_AGENT_NAME} — this
|
|
35
|
+
* allowlist excludes uppercase.
|
|
36
|
+
*/
|
|
37
|
+
export declare const SAFE_COMMAND_NAME: RegExp;
|
|
38
|
+
/** True iff `name` is a non-empty string matching {@link SAFE_COMMAND_NAME} (R3). */
|
|
39
|
+
export declare function isSafeCommandName(name: unknown): name is string;
|
|
40
|
+
/** The frontmatter fields we extract — `description` and (hyphenated) `argument-hint`. */
|
|
41
|
+
interface CommandFrontmatter {
|
|
42
|
+
description?: string;
|
|
43
|
+
argumentHint?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Minimal `---`-fenced frontmatter line-parser (NOT a full YAML dep). Reads the leading `---\n … \n---`
|
|
47
|
+
* block and pulls the `description` and `argument-hint` `key: value` lines; a single pair of surrounding
|
|
48
|
+
* quotes on a value is stripped. The frontmatter key `argument-hint` (a HYPHEN) is returned as the
|
|
49
|
+
* camelCase {@link CommandFrontmatter.argumentHint}. A file whose first line is not `---` yields `{}`.
|
|
50
|
+
* Tiny + pure on purpose, mirroring `agent-catalog.ts`'s `parseFrontmatter` — the command frontmatter we
|
|
51
|
+
* consume is flat `key: value`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function parseCommandFrontmatter(content: string): CommandFrontmatter;
|
|
54
|
+
/**
|
|
55
|
+
* Discover the custom slash-commands to advertise via ACP `available_commands` — FULLY OFFLINE (no
|
|
56
|
+
* subprocess, no network).
|
|
57
|
+
*
|
|
58
|
+
* TIERS, highest precedence first:
|
|
59
|
+
* 1. `<cwd>/.claude/commands/*.md` (cwd commands — R1.1 precedence)
|
|
60
|
+
* 2. `<cwd>/.claude/skills/<name>/SKILL.md` (cwd skills — R7)
|
|
61
|
+
* 3. `~/.claude/commands/*.md` (user commands)
|
|
62
|
+
* 4. `~/.claude/skills/<name>/SKILL.md` (user skills — R7)
|
|
63
|
+
* 5. `~/.claude/plugins/marketplaces/<m>/{commands,skills}` (ENABLED-plugin tier — R8; a plugin
|
|
64
|
+
* surface loses a name collision to any higher tier — R8.1)
|
|
65
|
+
* 6. {@link DiscoverCommandsDeps.builtins} (the R9 built-in tier — LOWEST; a built-in loses a name
|
|
66
|
+
* collision to any disk entry)
|
|
67
|
+
*
|
|
68
|
+
* Each command `*.md` file yields `{ name, description, input? }` (R2): `name` = basename sans `.md`,
|
|
69
|
+
* `description` = frontmatter `description` (else `""`), `input: { hint }` from `argument-hint` (omitted
|
|
70
|
+
* when absent). Each skill `<name>/SKILL.md` yields `{ name, description }` (no `input`): `name` = the
|
|
71
|
+
* DIRECTORY name, `description` = the `SKILL.md` frontmatter (R7 / R7.1). Every name is dropped if it
|
|
72
|
+
* fails the R3 {@link SAFE_COMMAND_NAME} allowlist. Names are deduped FIRST-WINS across the ordered
|
|
73
|
+
* surfaces — so a cwd command out-ranks a cwd skill (cmd > skill, same scope), any cwd surface
|
|
74
|
+
* out-ranks any user surface, an enabled-plugin surface out-ranks the built-ins but LOSES to any
|
|
75
|
+
* cwd/user surface (R8.1) — then the merged set is returned FLAT ALPHABETICAL by `name`.
|
|
76
|
+
*
|
|
77
|
+
* Never throws (R3 / R5): a missing/unreadable dir or file yields `[]` for that surface; a skills subdir
|
|
78
|
+
* without a readable `SKILL.md` is skipped.
|
|
79
|
+
*
|
|
80
|
+
* @param cwd the SESSION cwd (project dir) whose `.claude/{commands,skills}` take precedence.
|
|
81
|
+
* @param deps injectable fs/home/builtins seams (default: `node:` stdlib + {@link BUILTIN_COMMANDS}).
|
|
82
|
+
*/
|
|
83
|
+
export declare function discoverCommands(cwd: string, deps?: DiscoverCommandsDeps): AvailableCommand[];
|
|
84
|
+
export {};
|