@co0ontty/wand 1.39.0 → 1.40.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.
@@ -1,329 +0,0 @@
1
- import { stripAnsi, isNoiseLine } from "./pty-text-utils.js";
2
- function isCodexCommand(command) {
3
- return /^codex\b/.test((command ?? "").trim());
4
- }
5
- const CODEX_FOOTER_RE = /\bgpt-\d+(?:\.\d+)?(?:\s+[a-z0-9.-]+)?\s+·\s+\d+%\s+left\s+·\s+(?:\/|~\/).+/i;
6
- const CODEX_ACTIVITY_RE = /^(?:thinking|working|running|planning|applying|reading|searching|inspecting|reviewing|summarizing|editing|updating|writing|completed)\b/i;
7
- function stripCodexSegment(raw) {
8
- return raw
9
- .replace(/\x1b\][^\x07]*(\x07|\x1b\\)/g, "")
10
- .replace(/\x1b\[(\d+)C/g, (_match, count) => " ".repeat(Number(count) || 1))
11
- .replace(/\x1b\[[0-9;?]*[AB]/g, "\n")
12
- .replace(/\x1b\[[0-9;?]*[su]/g, "")
13
- .replace(/\x1b\[[0-9;?]*[HfJKr]/g, "\n")
14
- .replace(/\x1bM/g, "\n")
15
- .replace(/\x1b\[[0-9;?]*[ST]/g, "\n")
16
- .replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "")
17
- .replace(/\x1b[><=ePX^_]/g, "")
18
- .replace(/[\u00a0\u200b-\u200d\ufeff]/g, " ")
19
- // eslint-disable-next-line no-control-regex
20
- .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "")
21
- .replace(/[ \t]+\n/g, "\n");
22
- }
23
- function normalizeCodexText(text) {
24
- return text
25
- .replace(/\s+/g, " ")
26
- .replace(/[M]+$/g, "")
27
- .trim();
28
- }
29
- function isLikelyAssistantTailArtifact(longer, shorter) {
30
- if (!longer.startsWith(shorter))
31
- return false;
32
- const suffix = longer.slice(shorter.length);
33
- return /^[a-z]{1,4}$/i.test(suffix);
34
- }
35
- function parseCodexMessages(output) {
36
- const messages = [];
37
- function normalizeCodexAssistantLine(line) {
38
- return line
39
- .replace(/^[•◦·]\s*/, "")
40
- .replace(/^⏺\s+/, "")
41
- .replace(/^│\s*/, "")
42
- .trim();
43
- }
44
- function normalizeCodexPromptLine(line) {
45
- return line
46
- .replace(/^›\s*/, "")
47
- .replace(/^>\s*/, "")
48
- .trim();
49
- }
50
- function shouldIgnoreCodexLine(line) {
51
- const trimmed = line.trim();
52
- if (!trimmed)
53
- return true;
54
- if (isNoiseLine(trimmed))
55
- return true;
56
- if (CODEX_FOOTER_RE.test(trimmed))
57
- return true;
58
- if (/^[╭╰│┌└┐┘├┤┬┴┼─═]/.test(trimmed))
59
- return true;
60
- if (/^\[>[0-9;?]*u$/i.test(trimmed))
61
- return true;
62
- if (/^M+$/i.test(trimmed))
63
- return true;
64
- if (/^(?:OpenAI Codex|Codex)\b/i.test(trimmed))
65
- return true;
66
- if (/^(?:tokens?|context window|remaining context|approvals?|sandbox|provider|session id):\s*/i.test(trimmed))
67
- return true;
68
- if (/^(?:thinking|working)\s*(?:\.\.\.|…)?$/i.test(trimmed))
69
- return true;
70
- if (/^[•◦·]\s+(?:thinking|working|running|planning|applying|reading|searching|inspecting|reviewing|summarizing|editing|updating|writing|completed)\b/i.test(trimmed))
71
- return true;
72
- if (/^(?:model|directory|tip|context|cwd|path):\s+/i.test(trimmed))
73
- return true;
74
- return false;
75
- }
76
- function extractCodexPromptCandidate(line) {
77
- const trimmed = line.trim();
78
- if (!/^›(?:\s|$)/.test(trimmed))
79
- return null;
80
- if (CODEX_FOOTER_RE.test(trimmed))
81
- return null;
82
- const prompt = normalizeCodexText(normalizeCodexPromptLine(trimmed));
83
- if (!prompt || shouldIgnoreCodexLine(prompt))
84
- return null;
85
- return prompt;
86
- }
87
- function extractCodexAssistantCandidate(line) {
88
- const trimmed = line.trim();
89
- if (!/^[•◦·⏺]/.test(trimmed))
90
- return null;
91
- let assistant = normalizeCodexAssistantLine(trimmed);
92
- if (!assistant || /^[•◦·⏺]$/.test(assistant))
93
- return null;
94
- assistant = assistant.replace(/\s*\(\d+[smh]?\s*•\s*esc to interrupt\)[\s\S]*$/i, "");
95
- assistant = assistant.replace(/(?:[a-z]{1,6})?›[\s\S]*$/, "");
96
- assistant = assistant.replace(/\s{2,}gpt-\d[\s\S]*$/i, "");
97
- assistant = assistant.replace(/\b(?:OpenAI Codex|model:|directory:|Tip:)\b[\s\S]*$/i, "");
98
- assistant = normalizeCodexText(assistant);
99
- if (!assistant || assistant.length < 2 || CODEX_ACTIVITY_RE.test(assistant) || shouldIgnoreCodexLine(assistant)) {
100
- return null;
101
- }
102
- return assistant;
103
- }
104
- function extractCodexEchoCandidate(line) {
105
- const trimmed = normalizeCodexText(line);
106
- if (!trimmed || shouldIgnoreCodexLine(trimmed))
107
- return null;
108
- if (/^[•◦·⏺›]/.test(trimmed))
109
- return null;
110
- if (/^[\[\]<>0-9;?]+u?$/i.test(trimmed))
111
- return null;
112
- if (/^[╭╰│┌└┐┘├┤┬┴┼─═]/.test(trimmed))
113
- return null;
114
- if (trimmed.length > 500)
115
- return null;
116
- return trimmed;
117
- }
118
- function collectCodexCandidates() {
119
- const candidates = [];
120
- let order = 0;
121
- for (const rawSegment of output.replace(/\r\n?/g, "\n").split("\n")) {
122
- const cleanedSegment = stripCodexSegment(rawSegment);
123
- const pieces = cleanedSegment.split("\n");
124
- for (const piece of pieces) {
125
- const line = piece.trim();
126
- if (!line)
127
- continue;
128
- const prompt = extractCodexPromptCandidate(line);
129
- if (prompt) {
130
- candidates.push({ kind: "user", order, text: prompt });
131
- order += 1;
132
- continue;
133
- }
134
- const assistant = extractCodexAssistantCandidate(line);
135
- if (assistant) {
136
- candidates.push({ kind: "assistant", order, text: assistant });
137
- order += 1;
138
- continue;
139
- }
140
- const echo = extractCodexEchoCandidate(line);
141
- if (echo) {
142
- candidates.push({ kind: "echo", order, text: echo });
143
- order += 1;
144
- }
145
- }
146
- }
147
- return candidates.filter((candidate, index, list) => {
148
- const previous = list[index - 1];
149
- return !previous || previous.kind !== candidate.kind || previous.text !== candidate.text;
150
- });
151
- }
152
- function coalesceAssistantLines(lines) {
153
- const collected = [];
154
- for (const line of lines) {
155
- const normalized = normalizeCodexText(line);
156
- if (!normalized || normalized.length < 2 || shouldIgnoreCodexLine(normalized))
157
- continue;
158
- const previous = collected[collected.length - 1];
159
- if (!previous) {
160
- collected.push(normalized);
161
- continue;
162
- }
163
- if (normalized === previous)
164
- continue;
165
- if (normalized.startsWith(previous)) {
166
- collected[collected.length - 1] = normalized;
167
- continue;
168
- }
169
- if (previous.startsWith(normalized)) {
170
- if (isLikelyAssistantTailArtifact(previous, normalized)) {
171
- collected[collected.length - 1] = normalized;
172
- }
173
- continue;
174
- }
175
- collected.push(normalized);
176
- }
177
- return collected.join("\n").trim();
178
- }
179
- function extractVisiblePrompt(lines) {
180
- for (let index = 0; index < lines.length; index += 1) {
181
- const line = lines[index].trim();
182
- if (!line)
183
- continue;
184
- const inlinePrompt = extractCodexPromptCandidate(line);
185
- if (inlinePrompt)
186
- return inlinePrompt;
187
- if (line === "›") {
188
- for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex += 1) {
189
- const nextLine = normalizeCodexText(lines[nextIndex]);
190
- if (!nextLine || CODEX_FOOTER_RE.test(nextLine) || shouldIgnoreCodexLine(nextLine))
191
- continue;
192
- return nextLine;
193
- }
194
- }
195
- }
196
- return null;
197
- }
198
- function extractVisibleAssistantLines(lines) {
199
- const assistantLines = [];
200
- let collecting = false;
201
- for (const rawLine of lines) {
202
- const line = rawLine.trim();
203
- if (!line) {
204
- if (collecting)
205
- break;
206
- continue;
207
- }
208
- const assistant = extractCodexAssistantCandidate(line);
209
- if (assistant) {
210
- assistantLines.push(assistant);
211
- collecting = true;
212
- continue;
213
- }
214
- if (collecting) {
215
- if (line === "›" ||
216
- /^›(?:\s|$)/.test(line) ||
217
- CODEX_FOOTER_RE.test(line) ||
218
- shouldIgnoreCodexLine(line)) {
219
- break;
220
- }
221
- assistantLines.push(normalizeCodexText(line));
222
- }
223
- }
224
- return assistantLines;
225
- }
226
- const candidates = collectCodexCandidates();
227
- const explicitUsers = candidates.filter((candidate) => candidate.kind === "user");
228
- const assistantCandidates = candidates.filter((candidate) => candidate.kind === "assistant");
229
- const echoCandidates = candidates.filter((candidate) => candidate.kind === "echo");
230
- const strippedOutput = stripAnsi(output);
231
- const strippedLines = strippedOutput.split("\n").map((line) => line.trimEnd());
232
- const visiblePrompt = extractVisiblePrompt(strippedLines);
233
- const latestExplicitUser = explicitUsers[explicitUsers.length - 1] ?? null;
234
- const echoedUserCandidates = echoCandidates
235
- .map((candidate) => candidate.text)
236
- .filter((text) => text.length >= 3);
237
- const latestEchoUser = [...echoedUserCandidates].reverse().find((text) => text !== visiblePrompt) ?? echoedUserCandidates[echoedUserCandidates.length - 1] ?? null;
238
- const currentUser = latestExplicitUser?.text ?? latestEchoUser;
239
- const rawAssistantLines = assistantCandidates
240
- .filter((candidate) => !latestExplicitUser || candidate.order > latestExplicitUser.order)
241
- .map((candidate) => candidate.text);
242
- const visibleAssistantFallback = [...strippedOutput.matchAll(/^[ \t]*[•◦·⏺][ \t]*(.+)$/gm)]
243
- .map((match) => normalizeCodexText(match[1] ?? ""))
244
- .filter((line) => (!!line
245
- && !CODEX_ACTIVITY_RE.test(line)
246
- && !CODEX_FOOTER_RE.test(line)
247
- && !/\b(?:OpenAI Codex|model:|directory:|Tip:|esc to interrupt)\b/i.test(line)));
248
- const assistant = coalesceAssistantLines(rawAssistantLines)
249
- || coalesceAssistantLines(extractVisibleAssistantLines(strippedLines))
250
- || visibleAssistantFallback[visibleAssistantFallback.length - 1]
251
- || null;
252
- if (currentUser) {
253
- messages.push({ role: "user", content: currentUser });
254
- }
255
- if (assistant) {
256
- messages.push({ role: "assistant", content: assistant });
257
- }
258
- if (!messages.length && latestExplicitUser) {
259
- messages.push({ role: "user", content: latestExplicitUser.text });
260
- }
261
- else if (!messages.length && latestEchoUser) {
262
- messages.push({ role: "user", content: latestEchoUser });
263
- }
264
- const deduped = [];
265
- for (const message of messages) {
266
- const previous = deduped[deduped.length - 1];
267
- if (!previous || previous.role !== message.role || previous.content !== message.content) {
268
- deduped.push(message);
269
- }
270
- }
271
- return deduped;
272
- }
273
- export function parseMessages(output, command) {
274
- const messages = [];
275
- if (!output)
276
- return messages;
277
- if (isCodexCommand(command)) {
278
- return parseCodexMessages(output);
279
- }
280
- const stripped = stripAnsi(output).replace(/\r/g, "\n");
281
- const lines = stripped.split("\n");
282
- const cleaned = lines.filter((line) => !isNoiseLine(line.trim()));
283
- if (!cleaned.length)
284
- return messages;
285
- const turns = [];
286
- let currentUserText = null;
287
- let currentAssistantLines = [];
288
- for (const rawLine of cleaned) {
289
- const line = rawLine.trim();
290
- if (line.startsWith("❯")) {
291
- const afterPrompt = line.replace(/^❯\s*/, "").trim();
292
- if (afterPrompt.startsWith("Try"))
293
- continue;
294
- if (currentUserText !== null && currentAssistantLines.length > 0) {
295
- turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
296
- currentAssistantLines = [];
297
- }
298
- if (afterPrompt) {
299
- currentUserText = afterPrompt;
300
- }
301
- else {
302
- if (currentUserText !== null && currentAssistantLines.length > 0) {
303
- turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
304
- currentAssistantLines = [];
305
- }
306
- currentUserText = null;
307
- }
308
- }
309
- else if (currentUserText !== null) {
310
- const contentLine = rawLine.startsWith("⏺") ? rawLine.slice(1) : rawLine;
311
- currentAssistantLines.push(contentLine);
312
- }
313
- }
314
- if (currentUserText !== null && currentAssistantLines.length > 0) {
315
- turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
316
- }
317
- else if (currentUserText !== null) {
318
- // User input exists but no assistant response yet — still record the turn
319
- turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
320
- }
321
- for (const turn of turns) {
322
- messages.push({ role: "user", content: turn.user });
323
- const content = turn.assistantLines.join("\n").replace(/[ \t]+\n/g, "\n").replace(/[\n\s]+$/, "");
324
- if (content) {
325
- messages.push({ role: "assistant", content });
326
- }
327
- }
328
- return messages;
329
- }
@@ -1,57 +0,0 @@
1
- /**
2
- * Message Queue for managing user inputs
3
- * Inspired by Happy's MessageQueue2 implementation
4
- */
5
- import type { QueuedMessage, AutonomyPolicy, ApprovalPolicy, EscalationScope } from "./types.js";
6
- export interface MessageQueueOptions {
7
- autonomyPolicy?: AutonomyPolicy;
8
- approvalPolicy?: ApprovalPolicy;
9
- allowedScopes?: EscalationScope[];
10
- }
11
- export declare class MessageQueue {
12
- private queue;
13
- private processing;
14
- private onMessageCallback;
15
- private lastMessageId;
16
- /**
17
- * Add a message to the queue
18
- */
19
- enqueue(content: string, options?: MessageQueueOptions): QueuedMessage;
20
- /**
21
- * Add a high-priority message (like /compact, /clear)
22
- */
23
- enqueuePriority(content: string, options?: MessageQueueOptions): QueuedMessage;
24
- /**
25
- * Clear the queue and add a new message
26
- * Used for /compact and /clear commands
27
- */
28
- clearAndEnqueue(content: string, options?: MessageQueueOptions): QueuedMessage;
29
- /**
30
- * Set the message handler
31
- */
32
- onMessage(handler: (message: QueuedMessage) => Promise<void>): void;
33
- /**
34
- * Process the next message in the queue
35
- */
36
- private processNext;
37
- /**
38
- * Get the current queue length
39
- */
40
- get length(): number;
41
- /**
42
- * Check if the queue is empty
43
- */
44
- get isEmpty(): boolean;
45
- /**
46
- * Check if a message is being processed
47
- */
48
- get isProcessing(): boolean;
49
- /**
50
- * Clear all pending messages
51
- */
52
- clear(): void;
53
- /**
54
- * Get all pending messages (for debugging)
55
- */
56
- getPending(): QueuedMessage[];
57
- }
@@ -1,127 +0,0 @@
1
- /**
2
- * Message Queue for managing user inputs
3
- * Inspired by Happy's MessageQueue2 implementation
4
- */
5
- export class MessageQueue {
6
- queue = [];
7
- processing = false;
8
- onMessageCallback = null;
9
- lastMessageId = 0;
10
- /**
11
- * Add a message to the queue
12
- */
13
- enqueue(content, options = {}) {
14
- const message = {
15
- id: `msg-${++this.lastMessageId}-${Date.now()}`,
16
- content,
17
- priority: 1,
18
- timestamp: Date.now(),
19
- metadata: {
20
- autonomyPolicy: options.autonomyPolicy,
21
- approvalPolicy: options.approvalPolicy,
22
- allowedScopes: options.allowedScopes,
23
- },
24
- };
25
- this.queue.push(message);
26
- this.processNext();
27
- return message;
28
- }
29
- /**
30
- * Add a high-priority message (like /compact, /clear)
31
- */
32
- enqueuePriority(content, options = {}) {
33
- const message = {
34
- id: `msg-priority-${++this.lastMessageId}-${Date.now()}`,
35
- content,
36
- priority: 10,
37
- timestamp: Date.now(),
38
- isolate: true,
39
- metadata: {
40
- autonomyPolicy: options.autonomyPolicy,
41
- approvalPolicy: options.approvalPolicy,
42
- allowedScopes: options.allowedScopes,
43
- },
44
- };
45
- // Insert at the beginning of the queue
46
- this.queue.unshift(message);
47
- this.processNext();
48
- return message;
49
- }
50
- /**
51
- * Clear the queue and add a new message
52
- * Used for /compact and /clear commands
53
- */
54
- clearAndEnqueue(content, options = {}) {
55
- const clearedCount = this.queue.length;
56
- this.queue = [];
57
- if (clearedCount > 0) {
58
- console.error(`[MessageQueue] Cleared ${clearedCount} pending messages`);
59
- }
60
- return this.enqueuePriority(content, options);
61
- }
62
- /**
63
- * Set the message handler
64
- */
65
- onMessage(handler) {
66
- this.onMessageCallback = handler;
67
- this.processNext();
68
- }
69
- /**
70
- * Process the next message in the queue
71
- */
72
- async processNext() {
73
- if (this.processing || this.queue.length === 0 || !this.onMessageCallback) {
74
- return;
75
- }
76
- this.processing = true;
77
- // Sort by priority (higher first), then by timestamp
78
- this.queue.sort((a, b) => {
79
- if (a.priority !== b.priority) {
80
- return b.priority - a.priority;
81
- }
82
- return a.timestamp - b.timestamp;
83
- });
84
- const message = this.queue.shift();
85
- try {
86
- await this.onMessageCallback(message);
87
- }
88
- catch (error) {
89
- console.error(`[MessageQueue] Error processing message ${message.id}:`, error);
90
- }
91
- finally {
92
- this.processing = false;
93
- // Process next message
94
- this.processNext();
95
- }
96
- }
97
- /**
98
- * Get the current queue length
99
- */
100
- get length() {
101
- return this.queue.length;
102
- }
103
- /**
104
- * Check if the queue is empty
105
- */
106
- get isEmpty() {
107
- return this.queue.length === 0;
108
- }
109
- /**
110
- * Check if a message is being processed
111
- */
112
- get isProcessing() {
113
- return this.processing;
114
- }
115
- /**
116
- * Clear all pending messages
117
- */
118
- clear() {
119
- this.queue = [];
120
- }
121
- /**
122
- * Get all pending messages (for debugging)
123
- */
124
- getPending() {
125
- return [...this.queue];
126
- }
127
- }
@@ -1,81 +0,0 @@
1
- /**
2
- * Session Lifecycle Manager
3
- * Inspired by Happy's session lifecycle management
4
- */
5
- import type { SessionLifecycleState, SessionLifecycle } from "./types.js";
6
- export interface SessionLifecycleEvents {
7
- onStateChange?: (sessionId: string, oldState: SessionLifecycleState, newState: SessionLifecycleState) => void;
8
- onIdle?: (sessionId: string) => void;
9
- onArchived?: (sessionId: string, reason: string) => void;
10
- }
11
- export declare class SessionLifecycleManager {
12
- private sessions;
13
- private events;
14
- private idleTimeout;
15
- private archiveTimeout;
16
- private checkInterval;
17
- constructor(events?: SessionLifecycleEvents, options?: {
18
- idleTimeout?: number;
19
- archiveTimeout?: number;
20
- });
21
- /**
22
- * Register a new session
23
- */
24
- register(sessionId: string, initialState?: SessionLifecycleState): void;
25
- /**
26
- * Update session state
27
- */
28
- setState(sessionId: string, newState: SessionLifecycleState): void;
29
- /**
30
- * Update last activity timestamp
31
- */
32
- touch(sessionId: string): void;
33
- /**
34
- * Mark session as thinking
35
- */
36
- startThinking(sessionId: string): void;
37
- /**
38
- * Mark session as done thinking
39
- */
40
- stopThinking(sessionId: string): void;
41
- /**
42
- * Mark session as waiting for input
43
- */
44
- waitingInput(sessionId: string): void;
45
- /**
46
- * Archive a session
47
- */
48
- archive(sessionId: string, reason: string, by?: "user" | "timeout" | "error"): void;
49
- /**
50
- * Unregister a session
51
- */
52
- unregister(sessionId: string): void;
53
- /**
54
- * Get session lifecycle
55
- */
56
- get(sessionId: string): SessionLifecycle | undefined;
57
- /**
58
- * Get all sessions
59
- */
60
- getAll(): Map<string, SessionLifecycle>;
61
- /**
62
- * Get sessions by state
63
- */
64
- getByState(state: SessionLifecycleState): string[];
65
- /**
66
- * Start periodic check for idle/archived sessions
67
- */
68
- private startPeriodicCheck;
69
- /**
70
- * Stop periodic check
71
- */
72
- stopPeriodicCheck(): void;
73
- /**
74
- * Check sessions for idle/archived status
75
- */
76
- private checkSessions;
77
- /**
78
- * Cleanup all sessions
79
- */
80
- cleanup(): void;
81
- }