@co0ontty/wand 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,33 @@
1
1
  # wand
2
2
 
3
- wand 是一个通过浏览器访问的本地终端工具,支持 Claude 等命令行工具。
3
+ 通过浏览器访问本地终端,支持 Claude 等命令行工具。
4
4
 
5
- A browser-accessible local terminal for running CLI tools like Claude.
5
+ ## 功能
6
6
 
7
- ## 启动 / Start
7
+ - Web 终端 / Chat 模式双视图
8
+ - 会话持久化与恢复
9
+ - Claude Code 集成
10
+ - 文件浏览器
11
+ - HTTPS 安全连接
12
+
13
+ ## 快速开始
8
14
 
9
15
  ```bash
10
- npm install @co0ontty/wand && node dist/cli.js init && node dist/cli.js web
16
+ npm install -g @co0ontty/wand
17
+ wand init
18
+ wand web
11
19
  ```
12
20
 
13
- 配置文件 / Config: `~/.wand/config.json`
21
+ 配置文件:`~/.wand/config.json`
22
+
23
+ ## 开发
24
+
25
+ ```bash
26
+ npm install
27
+ npm run build
28
+ npm run dev
29
+ ```
30
+
31
+ ## License
32
+
33
+ MIT
@@ -0,0 +1,67 @@
1
+ /**
2
+ * ACP (Agent Communication Protocol) implementation
3
+ * Inspired by Happy's unified agent communication format
4
+ */
5
+ import type { ACPMessage, ACPTextMessage, ACPReasoningMessage, ACPToolCallMessage, ACPToolResultMessage, ACPFileEditMessage, ACPTerminalOutputMessage, ACPPermissionRequestMessage, ACPSessionStartMessage, ACPSessionEndMessage, ACPErrorMessage, ConversationTurn } from "./types.js";
6
+ /**
7
+ * Create a text message
8
+ */
9
+ export declare function createTextMessage(sessionId: string, role: "user" | "assistant" | "system", content: string): ACPTextMessage;
10
+ /**
11
+ * Create a reasoning message
12
+ */
13
+ export declare function createReasoningMessage(sessionId: string, content: string, isStreaming?: boolean): ACPReasoningMessage;
14
+ /**
15
+ * Create a tool call message
16
+ */
17
+ export declare function createToolCallMessage(sessionId: string, toolCallId: string, name: string, input: Record<string, unknown>, description?: string): ACPToolCallMessage;
18
+ /**
19
+ * Create a tool result message
20
+ */
21
+ export declare function createToolResultMessage(sessionId: string, toolCallId: string, output: string | Array<{
22
+ type: string;
23
+ [key: string]: unknown;
24
+ }>, isError?: boolean): ACPToolResultMessage;
25
+ /**
26
+ * Create a file edit message
27
+ */
28
+ export declare function createFileEditMessage(sessionId: string, filePath: string, description: string, options?: {
29
+ oldContent?: string;
30
+ newContent?: string;
31
+ diff?: string;
32
+ }): ACPFileEditMessage;
33
+ /**
34
+ * Create a terminal output message
35
+ */
36
+ export declare function createTerminalOutputMessage(sessionId: string, data: string, toolCallId?: string): ACPTerminalOutputMessage;
37
+ /**
38
+ * Create a permission request message
39
+ */
40
+ export declare function createPermissionRequestMessage(sessionId: string, permissionId: string, toolName: string, description: string): ACPPermissionRequestMessage;
41
+ /**
42
+ * Create a session start message
43
+ */
44
+ export declare function createSessionStartMessage(sessionId: string, workingDirectory: string, agent?: string): ACPSessionStartMessage;
45
+ /**
46
+ * Create a session end message
47
+ */
48
+ export declare function createSessionEndMessage(sessionId: string, exitCode: number | null): ACPSessionEndMessage;
49
+ /**
50
+ * Create an error message
51
+ */
52
+ export declare function createErrorMessage(sessionId: string, error: string, details?: string): ACPErrorMessage;
53
+ /**
54
+ * Convert ConversationTurn to ACP messages
55
+ */
56
+ export declare function conversationTurnToACP(sessionId: string, turn: ConversationTurn): ACPMessage[];
57
+ /**
58
+ * Convert ACP messages to ConversationTurn
59
+ */
60
+ export declare function acpToConversationTurns(messages: ACPMessage[]): ConversationTurn[];
61
+ /**
62
+ * Parse Claude stream-json event to ACP message
63
+ */
64
+ export declare function parseClaudeStreamEvent(sessionId: string, event: {
65
+ type?: string;
66
+ [key: string]: unknown;
67
+ }): ACPMessage | null;
@@ -0,0 +1,291 @@
1
+ /**
2
+ * ACP (Agent Communication Protocol) implementation
3
+ * Inspired by Happy's unified agent communication format
4
+ */
5
+ /**
6
+ * Generate a unique message ID
7
+ */
8
+ function generateId() {
9
+ return `acp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
10
+ }
11
+ /**
12
+ * Create a text message
13
+ */
14
+ export function createTextMessage(sessionId, role, content) {
15
+ return {
16
+ id: generateId(),
17
+ type: "message",
18
+ timestamp: Date.now(),
19
+ sessionId,
20
+ role,
21
+ content,
22
+ };
23
+ }
24
+ /**
25
+ * Create a reasoning message
26
+ */
27
+ export function createReasoningMessage(sessionId, content, isStreaming = false) {
28
+ return {
29
+ id: generateId(),
30
+ type: "reasoning",
31
+ timestamp: Date.now(),
32
+ sessionId,
33
+ content,
34
+ isStreaming,
35
+ };
36
+ }
37
+ /**
38
+ * Create a tool call message
39
+ */
40
+ export function createToolCallMessage(sessionId, toolCallId, name, input, description) {
41
+ return {
42
+ id: generateId(),
43
+ type: "tool-call",
44
+ timestamp: Date.now(),
45
+ sessionId,
46
+ toolCallId,
47
+ name,
48
+ input,
49
+ description,
50
+ };
51
+ }
52
+ /**
53
+ * Create a tool result message
54
+ */
55
+ export function createToolResultMessage(sessionId, toolCallId, output, isError = false) {
56
+ return {
57
+ id: generateId(),
58
+ type: "tool-result",
59
+ timestamp: Date.now(),
60
+ sessionId,
61
+ toolCallId,
62
+ output,
63
+ isError,
64
+ };
65
+ }
66
+ /**
67
+ * Create a file edit message
68
+ */
69
+ export function createFileEditMessage(sessionId, filePath, description, options = {}) {
70
+ return {
71
+ id: generateId(),
72
+ type: "file-edit",
73
+ timestamp: Date.now(),
74
+ sessionId,
75
+ filePath,
76
+ description,
77
+ ...options,
78
+ };
79
+ }
80
+ /**
81
+ * Create a terminal output message
82
+ */
83
+ export function createTerminalOutputMessage(sessionId, data, toolCallId) {
84
+ return {
85
+ id: generateId(),
86
+ type: "terminal-output",
87
+ timestamp: Date.now(),
88
+ sessionId,
89
+ data,
90
+ toolCallId,
91
+ };
92
+ }
93
+ /**
94
+ * Create a permission request message
95
+ */
96
+ export function createPermissionRequestMessage(sessionId, permissionId, toolName, description) {
97
+ return {
98
+ id: generateId(),
99
+ type: "permission-request",
100
+ timestamp: Date.now(),
101
+ sessionId,
102
+ permissionId,
103
+ toolName,
104
+ description,
105
+ };
106
+ }
107
+ /**
108
+ * Create a session start message
109
+ */
110
+ export function createSessionStartMessage(sessionId, workingDirectory, agent) {
111
+ return {
112
+ id: generateId(),
113
+ type: "session-start",
114
+ timestamp: Date.now(),
115
+ sessionId,
116
+ workingDirectory,
117
+ agent,
118
+ };
119
+ }
120
+ /**
121
+ * Create a session end message
122
+ */
123
+ export function createSessionEndMessage(sessionId, exitCode) {
124
+ return {
125
+ id: generateId(),
126
+ type: "session-end",
127
+ timestamp: Date.now(),
128
+ sessionId,
129
+ exitCode,
130
+ };
131
+ }
132
+ /**
133
+ * Create an error message
134
+ */
135
+ export function createErrorMessage(sessionId, error, details) {
136
+ return {
137
+ id: generateId(),
138
+ type: "error",
139
+ timestamp: Date.now(),
140
+ sessionId,
141
+ error,
142
+ details,
143
+ };
144
+ }
145
+ /**
146
+ * Convert ConversationTurn to ACP messages
147
+ */
148
+ export function conversationTurnToACP(sessionId, turn) {
149
+ const messages = [];
150
+ for (const block of turn.content) {
151
+ switch (block.type) {
152
+ case "text":
153
+ messages.push(createTextMessage(sessionId, turn.role, block.text));
154
+ break;
155
+ case "thinking":
156
+ messages.push(createReasoningMessage(sessionId, block.thinking));
157
+ break;
158
+ case "tool_use":
159
+ messages.push(createToolCallMessage(sessionId, block.id, block.name, block.input, block.description));
160
+ break;
161
+ case "tool_result":
162
+ messages.push(createToolResultMessage(sessionId, block.tool_use_id, block.content, block.is_error ?? false));
163
+ break;
164
+ }
165
+ }
166
+ return messages;
167
+ }
168
+ /**
169
+ * Convert ACP messages to ConversationTurn
170
+ */
171
+ export function acpToConversationTurns(messages) {
172
+ const turns = [];
173
+ let currentTurn = null;
174
+ for (const msg of messages) {
175
+ switch (msg.type) {
176
+ case "message":
177
+ if (msg.role === "user") {
178
+ // Start a new user turn
179
+ if (currentTurn) {
180
+ turns.push(currentTurn);
181
+ }
182
+ currentTurn = {
183
+ role: "user",
184
+ content: [{ type: "text", text: msg.content }],
185
+ };
186
+ }
187
+ else if (msg.role === "assistant") {
188
+ if (currentTurn?.role === "assistant") {
189
+ // Add to existing assistant turn
190
+ currentTurn.content.push({ type: "text", text: msg.content });
191
+ }
192
+ else {
193
+ // Start new assistant turn
194
+ if (currentTurn) {
195
+ turns.push(currentTurn);
196
+ }
197
+ currentTurn = {
198
+ role: "assistant",
199
+ content: [{ type: "text", text: msg.content }],
200
+ };
201
+ }
202
+ }
203
+ break;
204
+ case "reasoning":
205
+ if (currentTurn?.role === "assistant") {
206
+ currentTurn.content.push({ type: "thinking", thinking: msg.content });
207
+ }
208
+ break;
209
+ case "tool-call":
210
+ if (currentTurn?.role === "assistant") {
211
+ currentTurn.content.push({
212
+ type: "tool_use",
213
+ id: msg.toolCallId,
214
+ name: msg.name,
215
+ input: msg.input,
216
+ description: msg.description,
217
+ });
218
+ }
219
+ break;
220
+ case "tool-result":
221
+ if (currentTurn?.role === "assistant") {
222
+ currentTurn.content.push({
223
+ type: "tool_result",
224
+ tool_use_id: msg.toolCallId,
225
+ content: msg.output,
226
+ is_error: msg.isError,
227
+ });
228
+ }
229
+ break;
230
+ }
231
+ }
232
+ if (currentTurn) {
233
+ turns.push(currentTurn);
234
+ }
235
+ return turns;
236
+ }
237
+ /**
238
+ * Parse Claude stream-json event to ACP message
239
+ */
240
+ export function parseClaudeStreamEvent(sessionId, event) {
241
+ switch (event.type) {
242
+ case "content_block_start": {
243
+ const block = event.content_block;
244
+ if (!block)
245
+ return null;
246
+ if (block.type === "text") {
247
+ return createTextMessage(sessionId, "assistant", "");
248
+ }
249
+ if (block.type === "thinking") {
250
+ return createReasoningMessage(sessionId, "", true);
251
+ }
252
+ if (block.type === "tool_use") {
253
+ return createToolCallMessage(sessionId, block.id ?? "", block.name ?? "", block.input ?? {});
254
+ }
255
+ return null;
256
+ }
257
+ case "content_block_delta": {
258
+ const delta = event.delta;
259
+ if (!delta)
260
+ return null;
261
+ if (delta.type === "text_delta" && delta.text) {
262
+ return createTextMessage(sessionId, "assistant", delta.text);
263
+ }
264
+ if (delta.type === "thinking_delta" && delta.thinking) {
265
+ return createReasoningMessage(sessionId, delta.thinking, true);
266
+ }
267
+ return null;
268
+ }
269
+ case "content_block_stop": {
270
+ // Block completed - could emit a completion event
271
+ return null;
272
+ }
273
+ case "message_start": {
274
+ const message = event.message;
275
+ if (message?.role === "assistant") {
276
+ return createTextMessage(sessionId, "assistant", "");
277
+ }
278
+ return null;
279
+ }
280
+ case "message_stop": {
281
+ // Message completed
282
+ return null;
283
+ }
284
+ case "error": {
285
+ const error = event.error;
286
+ return createErrorMessage(sessionId, error?.message ?? "Unknown error");
287
+ }
288
+ default:
289
+ return null;
290
+ }
291
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * ClaudePtyBridge - PTY output parsing and event bridge
3
+ *
4
+ * Transforms raw PTY output into structured events for WebSocket broadcast.
5
+ * Maintains two parallel output streams:
6
+ * 1. Raw output for terminal view (passthrough)
7
+ * 2. Structured messages for chat view (parsed)
8
+ */
9
+ import { EventEmitter } from "node:events";
10
+ import type { ApprovalPolicy, ConversationTurn, EscalationScope, TaskData } from "./types.js";
11
+ interface PermissionState {
12
+ /** Rolling window for prompt detection */
13
+ window: string;
14
+ /** Currently blocked */
15
+ isBlocked: boolean;
16
+ /** Last detected prompt text */
17
+ lastPrompt: string | null;
18
+ /** Last detected scope */
19
+ lastScope: EscalationScope | null;
20
+ /** Last detected target */
21
+ lastTarget: string | null;
22
+ /** Timestamp of last auto-confirm to prevent rapid repeats */
23
+ lastAutoConfirmAt: number;
24
+ }
25
+ /** Permission resolution result */
26
+ export type PermissionResolution = "approve_once" | "approve_turn" | "deny";
27
+ export interface ClaudePtyBridgeOptions {
28
+ sessionId: string;
29
+ /** Initial messages from storage (for restart recovery) */
30
+ initialMessages?: ConversationTurn[];
31
+ /** Initial raw output from storage */
32
+ initialOutput?: string;
33
+ /** Whether this is a Claude CLI command (enables permission detection) */
34
+ isClaudeCommand?: boolean;
35
+ /** Whether to auto-approve permission prompts */
36
+ autoApprove?: boolean;
37
+ /** Approval policy for permission handling */
38
+ approvalPolicy?: ApprovalPolicy;
39
+ /** PTY write function for sending approval input */
40
+ ptyWrite?: (input: string) => void;
41
+ }
42
+ /**
43
+ * ClaudePtyBridge transforms raw PTY output into structured events.
44
+ *
45
+ * Events emitted:
46
+ * - "output.raw" - Raw PTY output for terminal view
47
+ * - "output.chat" - Structured chat content update
48
+ * - "chat.turn" - Conversation turn completed
49
+ * - "permission.prompt" - Permission request detected
50
+ * - "permission.resolved" - Permission resolved
51
+ * - "session.id" - Claude session ID captured
52
+ * - "task" - Task info update
53
+ * - "ended" - Session ended
54
+ */
55
+ export declare class ClaudePtyBridge extends EventEmitter {
56
+ readonly sessionId: string;
57
+ private rawOutput;
58
+ private messages;
59
+ private chatState;
60
+ private permissionState;
61
+ private sessionIdWindow;
62
+ private claudeSessionId;
63
+ private currentTask;
64
+ private taskDebounceTimer;
65
+ private lastEmittedTask;
66
+ private isClaudeCommand;
67
+ private autoApprove;
68
+ private approvalPolicy;
69
+ private ptyWrite;
70
+ /** Set to true once onExit() has been called; guards against post-exit method calls */
71
+ private _exited;
72
+ private rememberedScopes;
73
+ private rememberedTargets;
74
+ constructor(options: ClaudePtyBridgeOptions);
75
+ /**
76
+ * Process a raw PTY chunk.
77
+ * Emits events via EventEmitter.
78
+ */
79
+ processChunk(chunk: string): void;
80
+ /**
81
+ * Called when user sends input.
82
+ * Starts tracking a new assistant response.
83
+ */
84
+ onUserInput(input: string): void;
85
+ /**
86
+ * Called when PTY process exits.
87
+ * Finalizes any pending response.
88
+ */
89
+ onExit(exitCode: number | null): void;
90
+ getMessages(): ConversationTurn[];
91
+ getRawOutput(): string;
92
+ getClaudeSessionId(): string | null;
93
+ isPermissionBlocked(): boolean;
94
+ getPermissionState(): PermissionState;
95
+ getCurrentTask(): TaskData | null;
96
+ /**
97
+ * Set the PTY write function for sending approval input.
98
+ */
99
+ setPtyWrite(fn: (input: string) => void): void;
100
+ /**
101
+ * Resolve the current permission prompt.
102
+ * @param resolution - How to resolve the permission
103
+ */
104
+ resolvePermission(resolution: PermissionResolution): void;
105
+ /**
106
+ * Check if a permission scope/target should be auto-approved based on remembered decisions.
107
+ */
108
+ shouldAutoApprove(scope: EscalationScope, target?: string): boolean;
109
+ /**
110
+ * Clear remembered permissions (call at the start of a new turn).
111
+ */
112
+ clearRememberedPermissions(): void;
113
+ /**
114
+ * Clear permission blocked state (called when permission is resolved externally).
115
+ */
116
+ clearPermissionBlocked(): void;
117
+ private emitEvent;
118
+ private isRealChatInput;
119
+ private captureSessionId;
120
+ private detectPermission;
121
+ private isPermissionPromptDetected;
122
+ private extractPromptText;
123
+ private extractPermissionTarget;
124
+ private inferScope;
125
+ private parseChatResponse;
126
+ private detectCompletion;
127
+ private updateAssistantContent;
128
+ private finalizeResponse;
129
+ private stripAnsi;
130
+ /**
131
+ * Find the end index of the echoed user input in the PTY buffer.
132
+ * The echo may contain ANSI codes between characters.
133
+ * Returns the index after the last character of the echo.
134
+ */
135
+ private findEchoEndIndex;
136
+ private isStatusLine;
137
+ private cleanForChat;
138
+ }
139
+ export {};