@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 +25 -5
- package/dist/acp-protocol.d.ts +67 -0
- package/dist/acp-protocol.js +291 -0
- package/dist/claude-pty-bridge.d.ts +139 -0
- package/dist/claude-pty-bridge.js +649 -0
- package/dist/claude-stream-adapter.d.ts +35 -0
- package/dist/claude-stream-adapter.js +153 -0
- package/dist/claude-structured-runner.d.ts +27 -0
- package/dist/claude-structured-runner.js +106 -0
- package/dist/config.js +2 -2
- package/dist/message-parser.js +12 -66
- package/dist/message-queue.d.ts +57 -0
- package/dist/message-queue.js +127 -0
- package/dist/process-manager.d.ts +32 -25
- package/dist/process-manager.js +503 -780
- package/dist/server.js +366 -51
- package/dist/session-lifecycle.d.ts +81 -0
- package/dist/session-lifecycle.js +176 -0
- package/dist/storage.js +12 -1
- package/dist/types.d.ts +105 -5
- package/dist/web-ui/content/scripts.js +2307 -658
- package/dist/web-ui/content/styles.css +5284 -2771
- package/dist/web-ui/index.js +8 -5
- package/dist/web-ui/scripts.js +8 -1
- package/package.json +3 -10
- package/dist/web-ui/utils.d.ts +0 -4
- package/dist/web-ui/utils.js +0 -12
- package/dist/web-ui.d.ts +0 -1
- package/dist/web-ui.js +0 -2
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Lifecycle Manager
|
|
3
|
+
* Inspired by Happy's session lifecycle management
|
|
4
|
+
*/
|
|
5
|
+
export class SessionLifecycleManager {
|
|
6
|
+
sessions = new Map();
|
|
7
|
+
events;
|
|
8
|
+
idleTimeout;
|
|
9
|
+
archiveTimeout;
|
|
10
|
+
checkInterval = null;
|
|
11
|
+
constructor(events = {}, options = {}) {
|
|
12
|
+
this.events = events;
|
|
13
|
+
this.idleTimeout = options.idleTimeout ?? 5 * 60 * 1000; // 5 minutes
|
|
14
|
+
this.archiveTimeout = options.archiveTimeout ?? 30 * 60 * 1000; // 30 minutes
|
|
15
|
+
// Start periodic check
|
|
16
|
+
this.startPeriodicCheck();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Register a new session
|
|
20
|
+
*/
|
|
21
|
+
register(sessionId, initialState = "initializing") {
|
|
22
|
+
const lifecycle = {
|
|
23
|
+
state: initialState,
|
|
24
|
+
stateSince: Date.now(),
|
|
25
|
+
lastActivityAt: Date.now(),
|
|
26
|
+
};
|
|
27
|
+
this.sessions.set(sessionId, lifecycle);
|
|
28
|
+
console.error(`[Lifecycle] Session ${sessionId} registered with state: ${initialState}`);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Update session state
|
|
32
|
+
*/
|
|
33
|
+
setState(sessionId, newState) {
|
|
34
|
+
const lifecycle = this.sessions.get(sessionId);
|
|
35
|
+
if (!lifecycle) {
|
|
36
|
+
console.error(`[Lifecycle] Session ${sessionId} not found`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const oldState = lifecycle.state;
|
|
40
|
+
if (oldState === newState) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
lifecycle.state = newState;
|
|
44
|
+
lifecycle.stateSince = Date.now();
|
|
45
|
+
lifecycle.lastActivityAt = Date.now();
|
|
46
|
+
console.error(`[Lifecycle] Session ${sessionId} state changed: ${oldState} -> ${newState}`);
|
|
47
|
+
// Emit state change event
|
|
48
|
+
this.events.onStateChange?.(sessionId, oldState, newState);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Update last activity timestamp
|
|
52
|
+
*/
|
|
53
|
+
touch(sessionId) {
|
|
54
|
+
const lifecycle = this.sessions.get(sessionId);
|
|
55
|
+
if (lifecycle) {
|
|
56
|
+
lifecycle.lastActivityAt = Date.now();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Mark session as thinking
|
|
61
|
+
*/
|
|
62
|
+
startThinking(sessionId) {
|
|
63
|
+
this.setState(sessionId, "thinking");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Mark session as done thinking
|
|
67
|
+
*/
|
|
68
|
+
stopThinking(sessionId) {
|
|
69
|
+
const lifecycle = this.sessions.get(sessionId);
|
|
70
|
+
if (lifecycle?.state === "thinking") {
|
|
71
|
+
this.setState(sessionId, "idle");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Mark session as waiting for input
|
|
76
|
+
*/
|
|
77
|
+
waitingInput(sessionId) {
|
|
78
|
+
this.setState(sessionId, "waiting-input");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Archive a session
|
|
82
|
+
*/
|
|
83
|
+
archive(sessionId, reason, by = "user") {
|
|
84
|
+
const lifecycle = this.sessions.get(sessionId);
|
|
85
|
+
if (!lifecycle) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
lifecycle.state = "archived";
|
|
89
|
+
lifecycle.stateSince = Date.now();
|
|
90
|
+
lifecycle.archivedBy = by;
|
|
91
|
+
lifecycle.archiveReason = reason;
|
|
92
|
+
console.error(`[Lifecycle] Session ${sessionId} archived: ${reason} (by: ${by})`);
|
|
93
|
+
// Emit archived event
|
|
94
|
+
this.events.onArchived?.(sessionId, reason);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Unregister a session
|
|
98
|
+
*/
|
|
99
|
+
unregister(sessionId) {
|
|
100
|
+
this.sessions.delete(sessionId);
|
|
101
|
+
console.error(`[Lifecycle] Session ${sessionId} unregistered`);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get session lifecycle
|
|
105
|
+
*/
|
|
106
|
+
get(sessionId) {
|
|
107
|
+
return this.sessions.get(sessionId);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get all sessions
|
|
111
|
+
*/
|
|
112
|
+
getAll() {
|
|
113
|
+
return new Map(this.sessions);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get sessions by state
|
|
117
|
+
*/
|
|
118
|
+
getByState(state) {
|
|
119
|
+
const result = [];
|
|
120
|
+
for (const [sessionId, lifecycle] of this.sessions) {
|
|
121
|
+
if (lifecycle.state === state) {
|
|
122
|
+
result.push(sessionId);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Start periodic check for idle/archived sessions
|
|
129
|
+
*/
|
|
130
|
+
startPeriodicCheck() {
|
|
131
|
+
if (this.checkInterval) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this.checkInterval = setInterval(() => {
|
|
135
|
+
this.checkSessions();
|
|
136
|
+
}, 60 * 1000); // Check every minute
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Stop periodic check
|
|
140
|
+
*/
|
|
141
|
+
stopPeriodicCheck() {
|
|
142
|
+
if (this.checkInterval) {
|
|
143
|
+
clearInterval(this.checkInterval);
|
|
144
|
+
this.checkInterval = null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Check sessions for idle/archived status
|
|
149
|
+
*/
|
|
150
|
+
checkSessions() {
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
for (const [sessionId, lifecycle] of this.sessions) {
|
|
153
|
+
if (lifecycle.state === "archived") {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const timeSinceLastActivity = now - lifecycle.lastActivityAt;
|
|
157
|
+
// Check for archive timeout
|
|
158
|
+
if (timeSinceLastActivity > this.archiveTimeout) {
|
|
159
|
+
this.archive(sessionId, "Session timed out", "timeout");
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// Check for idle timeout
|
|
163
|
+
if (timeSinceLastActivity > this.idleTimeout && lifecycle.state !== "idle") {
|
|
164
|
+
this.setState(sessionId, "idle");
|
|
165
|
+
this.events.onIdle?.(sessionId);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Cleanup all sessions
|
|
171
|
+
*/
|
|
172
|
+
cleanup() {
|
|
173
|
+
this.stopPeriodicCheck();
|
|
174
|
+
this.sessions.clear();
|
|
175
|
+
}
|
|
176
|
+
}
|
package/dist/storage.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
function parseStoredMessages(raw) {
|
|
5
|
+
if (!raw) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(raw);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
4
15
|
export const DEFAULT_DB_FILE = "wand.db";
|
|
5
16
|
export function resolveDatabasePath(configPath) {
|
|
6
17
|
return path.resolve(path.dirname(configPath), DEFAULT_DB_FILE);
|
|
@@ -156,7 +167,7 @@ export class WandStorage {
|
|
|
156
167
|
archived: Boolean(row.archived),
|
|
157
168
|
archivedAt: row.archived_at,
|
|
158
169
|
claudeSessionId: row.claude_session_id,
|
|
159
|
-
messages:
|
|
170
|
+
messages: parseStoredMessages(row.messages)
|
|
160
171
|
}));
|
|
161
172
|
}
|
|
162
173
|
deleteSession(id) {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
|
-
export type ExecutionMode = "
|
|
1
|
+
export type ExecutionMode = "assist" | "agent" | "agent-max" | "default" | "auto-edit" | "full-access" | "native" | "managed";
|
|
2
|
+
export type AutonomyPolicy = "assist" | "agent" | "agent-max";
|
|
3
|
+
export type ApprovalPolicy = "ask-every-time" | "approve-once" | "remember-this-turn";
|
|
4
|
+
export type EscalationScope = "write_file" | "run_command" | "network" | "outside_workspace" | "dangerous_shell" | "unknown";
|
|
5
|
+
export type EscalationRunner = "json" | "pty";
|
|
6
|
+
export type EscalationResolution = "approve_once" | "approve_turn" | "deny" | "fallback_manual";
|
|
7
|
+
export type EscalationSource = "tool_permission_request" | "sandbox_hard_block" | "workspace_policy_limit" | "cli_capability_limit" | "unknown";
|
|
8
|
+
export interface EscalationRequest {
|
|
9
|
+
requestId: string;
|
|
10
|
+
scope: EscalationScope;
|
|
11
|
+
runner: EscalationRunner;
|
|
12
|
+
source: EscalationSource;
|
|
13
|
+
resolution?: EscalationResolution;
|
|
14
|
+
target?: string;
|
|
15
|
+
reason: string;
|
|
16
|
+
}
|
|
17
|
+
export interface TurnRequest {
|
|
18
|
+
message: string;
|
|
19
|
+
autonomyPolicy?: AutonomyPolicy;
|
|
20
|
+
approvalPolicy?: ApprovalPolicy;
|
|
21
|
+
allowedScopes?: EscalationScope[];
|
|
22
|
+
}
|
|
23
|
+
export interface EscalationDecisionRequest {
|
|
24
|
+
resolution?: Extract<EscalationResolution, "approve_once" | "approve_turn" | "deny">;
|
|
25
|
+
}
|
|
2
26
|
export interface CommandPreset {
|
|
3
27
|
label: string;
|
|
4
28
|
command: string;
|
|
@@ -25,8 +49,12 @@ export interface CommandRequest {
|
|
|
25
49
|
}
|
|
26
50
|
export interface InputRequest {
|
|
27
51
|
input?: string;
|
|
28
|
-
/** Current UI view: "chat" or "terminal".
|
|
52
|
+
/** Current UI view: "chat" or "terminal". Chat view uses PTY-derived structured messages. */
|
|
29
53
|
view?: "chat" | "terminal";
|
|
54
|
+
autonomyPolicy?: AutonomyPolicy;
|
|
55
|
+
approvalPolicy?: ApprovalPolicy;
|
|
56
|
+
allowedScopes?: EscalationScope[];
|
|
57
|
+
turn?: TurnRequest;
|
|
30
58
|
}
|
|
31
59
|
export interface ResizeRequest {
|
|
32
60
|
cols?: number;
|
|
@@ -70,14 +98,17 @@ export interface ToolUseBlock {
|
|
|
70
98
|
export interface ToolResultBlock {
|
|
71
99
|
type: "tool_result";
|
|
72
100
|
tool_use_id: string;
|
|
73
|
-
content: string
|
|
101
|
+
content: string | Array<{
|
|
102
|
+
type: string;
|
|
103
|
+
[key: string]: unknown;
|
|
104
|
+
}>;
|
|
74
105
|
is_error?: boolean;
|
|
75
106
|
}
|
|
76
107
|
export type ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock;
|
|
77
108
|
export interface ConversationTurn {
|
|
78
109
|
role: "user" | "assistant";
|
|
79
110
|
content: ContentBlock[];
|
|
80
|
-
/**
|
|
111
|
+
/** Optional usage metadata when available from the underlying tool. */
|
|
81
112
|
usage?: {
|
|
82
113
|
inputTokens?: number;
|
|
83
114
|
outputTokens?: number;
|
|
@@ -91,6 +122,9 @@ export interface SessionSnapshot {
|
|
|
91
122
|
command: string;
|
|
92
123
|
cwd: string;
|
|
93
124
|
mode: ExecutionMode;
|
|
125
|
+
autonomyPolicy?: AutonomyPolicy;
|
|
126
|
+
approvalPolicy?: ApprovalPolicy;
|
|
127
|
+
allowedScopes?: EscalationScope[];
|
|
94
128
|
status: "running" | "exited" | "failed" | "stopped";
|
|
95
129
|
exitCode: number | null;
|
|
96
130
|
startedAt: string;
|
|
@@ -98,8 +132,74 @@ export interface SessionSnapshot {
|
|
|
98
132
|
output: string;
|
|
99
133
|
archived: boolean;
|
|
100
134
|
archivedAt: string | null;
|
|
135
|
+
/** Backward-compatible derived flag from pendingEscalation */
|
|
136
|
+
permissionBlocked?: boolean;
|
|
137
|
+
pendingEscalation?: EscalationRequest | null;
|
|
138
|
+
lastEscalationResult?: {
|
|
139
|
+
requestId: string;
|
|
140
|
+
resolution: EscalationResolution;
|
|
141
|
+
reason: string;
|
|
142
|
+
} | null;
|
|
101
143
|
/** Claude Code 会话 ID,用于 --resume 恢复会话 */
|
|
102
144
|
claudeSessionId: string | null;
|
|
103
|
-
/** Structured conversation messages
|
|
145
|
+
/** Structured conversation messages derived from PTY output. */
|
|
104
146
|
messages?: ConversationTurn[];
|
|
147
|
+
/** Session lifecycle state */
|
|
148
|
+
lifecycleState?: "running" | "idle" | "archived";
|
|
149
|
+
/** Last activity timestamp */
|
|
150
|
+
lastActivityAt?: string;
|
|
151
|
+
}
|
|
152
|
+
export type SessionLifecycleState = "initializing" | "running" | "idle" | "thinking" | "waiting-input" | "archived";
|
|
153
|
+
export interface SessionLifecycle {
|
|
154
|
+
state: SessionLifecycleState;
|
|
155
|
+
stateSince: number;
|
|
156
|
+
lastActivityAt: number;
|
|
157
|
+
archivedBy?: "user" | "timeout" | "error";
|
|
158
|
+
archiveReason?: string;
|
|
159
|
+
}
|
|
160
|
+
/** Unified event type emitted by ClaudePtyBridge for WebSocket broadcast */
|
|
161
|
+
export type SessionEventType = "output.raw" | "output.chat" | "chat.turn" | "permission.prompt" | "permission.resolved" | "session.id" | "task" | "ended";
|
|
162
|
+
export interface SessionEvent {
|
|
163
|
+
type: SessionEventType;
|
|
164
|
+
sessionId: string;
|
|
165
|
+
timestamp: number;
|
|
166
|
+
data?: unknown;
|
|
167
|
+
}
|
|
168
|
+
export interface RawOutputData {
|
|
169
|
+
chunk: string;
|
|
170
|
+
/** Full accumulated output for terminal view */
|
|
171
|
+
output: string;
|
|
172
|
+
}
|
|
173
|
+
export interface ChatOutputData {
|
|
174
|
+
/** Current messages array */
|
|
175
|
+
messages: ConversationTurn[];
|
|
176
|
+
/** Index of the message being streamed */
|
|
177
|
+
streamingIndex?: number;
|
|
178
|
+
/** Whether assistant is currently responding */
|
|
179
|
+
isResponding: boolean;
|
|
180
|
+
}
|
|
181
|
+
export interface ChatTurnData {
|
|
182
|
+
/** The completed turn */
|
|
183
|
+
turn: ConversationTurn;
|
|
184
|
+
/** Full messages array */
|
|
185
|
+
messages: ConversationTurn[];
|
|
186
|
+
}
|
|
187
|
+
export interface PermissionPromptData {
|
|
188
|
+
/** Detected prompt text */
|
|
189
|
+
prompt: string;
|
|
190
|
+
/** Inferred scope */
|
|
191
|
+
scope: EscalationScope;
|
|
192
|
+
/** Target if detected */
|
|
193
|
+
target?: string;
|
|
194
|
+
}
|
|
195
|
+
export interface SessionIdData {
|
|
196
|
+
/** Claude CLI session UUID */
|
|
197
|
+
claudeSessionId: string;
|
|
198
|
+
}
|
|
199
|
+
export interface TaskData {
|
|
200
|
+
title: string;
|
|
201
|
+
tool?: string;
|
|
202
|
+
}
|
|
203
|
+
export interface SessionEndData {
|
|
204
|
+
exitCode: number | null;
|
|
105
205
|
}
|