@co0ontty/wand 0.2.1 → 0.4.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.
Files changed (48) hide show
  1. package/README.md +25 -5
  2. package/dist/acp-protocol.d.ts +67 -0
  3. package/dist/acp-protocol.js +291 -0
  4. package/dist/avatar.d.ts +14 -0
  5. package/dist/avatar.js +110 -0
  6. package/dist/claude-pty-bridge.d.ts +137 -0
  7. package/dist/claude-pty-bridge.js +619 -0
  8. package/dist/claude-stream-adapter.d.ts +35 -0
  9. package/dist/claude-stream-adapter.js +153 -0
  10. package/dist/claude-structured-runner.d.ts +27 -0
  11. package/dist/claude-structured-runner.js +106 -0
  12. package/dist/cli.d.ts +1 -1
  13. package/dist/cli.js +10 -2
  14. package/dist/config.js +8 -4
  15. package/dist/message-parser.js +16 -150
  16. package/dist/message-queue.d.ts +57 -0
  17. package/dist/message-queue.js +127 -0
  18. package/dist/middleware/path-safety.d.ts +6 -0
  19. package/dist/middleware/path-safety.js +19 -0
  20. package/dist/middleware/rate-limit.d.ts +8 -0
  21. package/dist/middleware/rate-limit.js +37 -0
  22. package/dist/process-manager.d.ts +82 -27
  23. package/dist/process-manager.js +1445 -822
  24. package/dist/pty-text-utils.d.ts +13 -0
  25. package/dist/pty-text-utils.js +84 -0
  26. package/dist/pwa.d.ts +5 -0
  27. package/dist/pwa.js +118 -0
  28. package/dist/server.js +511 -409
  29. package/dist/session-lifecycle.d.ts +81 -0
  30. package/dist/session-lifecycle.js +181 -0
  31. package/dist/session-logger.d.ts +13 -3
  32. package/dist/session-logger.js +56 -5
  33. package/dist/storage.d.ts +9 -0
  34. package/dist/storage.js +73 -7
  35. package/dist/types.d.ts +112 -6
  36. package/dist/web-ui/content/icon-192.png +0 -0
  37. package/dist/web-ui/content/icon-512.png +0 -0
  38. package/dist/web-ui/content/scripts.js +3770 -852
  39. package/dist/web-ui/content/styles.css +5505 -2779
  40. package/dist/web-ui/index.js +8 -5
  41. package/dist/web-ui/scripts.js +8 -1
  42. package/dist/ws-broadcast.d.ts +27 -0
  43. package/dist/ws-broadcast.js +160 -0
  44. package/package.json +2 -9
  45. package/dist/web-ui/utils.d.ts +0 -4
  46. package/dist/web-ui/utils.js +0 -12
  47. package/dist/web-ui.d.ts +0 -1
  48. package/dist/web-ui.js +0 -2
@@ -0,0 +1,153 @@
1
+ function normalizeToolInput(block) {
2
+ if (typeof block._partialJson === "string" && block._partialJson.length > 0) {
3
+ try {
4
+ const parsed = JSON.parse(block._partialJson);
5
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
6
+ return parsed;
7
+ }
8
+ }
9
+ catch {
10
+ // Keep original input if partial JSON is incomplete.
11
+ }
12
+ }
13
+ if (block.input && typeof block.input === "object" && !Array.isArray(block.input)) {
14
+ return block.input;
15
+ }
16
+ return {};
17
+ }
18
+ function appendAssistantBlock(target, block) {
19
+ if (!block.type) {
20
+ return;
21
+ }
22
+ if (block.type === "text") {
23
+ target.push({ type: "text", text: typeof block.text === "string" ? block.text : "" });
24
+ return;
25
+ }
26
+ if (block.type === "thinking") {
27
+ target.push({ type: "thinking", thinking: typeof block.thinking === "string" ? block.thinking : "" });
28
+ return;
29
+ }
30
+ if (block.type === "tool_use") {
31
+ target.push({
32
+ type: "tool_use",
33
+ id: typeof block.id === "string" ? block.id : "",
34
+ name: typeof block.name === "string" ? block.name : "",
35
+ description: typeof block.description === "string" ? block.description : undefined,
36
+ input: normalizeToolInput({ input: block.input, _partialJson: block._partialJson }),
37
+ });
38
+ return;
39
+ }
40
+ if (block.type === "tool_result") {
41
+ const content = typeof block.content === "string"
42
+ ? block.content
43
+ : Array.isArray(block.content)
44
+ ? block.content
45
+ : "";
46
+ target.push({
47
+ type: "tool_result",
48
+ tool_use_id: typeof block.tool_use_id === "string" ? block.tool_use_id : "",
49
+ content,
50
+ is_error: block.is_error === true,
51
+ });
52
+ }
53
+ }
54
+ function buildUsage(event) {
55
+ if (!event.usage && event.total_cost_usd === undefined) {
56
+ return undefined;
57
+ }
58
+ return {
59
+ inputTokens: typeof event.usage?.input_tokens === "number" ? event.usage.input_tokens : undefined,
60
+ outputTokens: typeof event.usage?.output_tokens === "number" ? event.usage.output_tokens : undefined,
61
+ cacheReadInputTokens: typeof event.usage?.cache_read_input_tokens === "number"
62
+ ? event.usage.cache_read_input_tokens
63
+ : undefined,
64
+ cacheCreationInputTokens: typeof event.usage?.cache_creation_input_tokens === "number"
65
+ ? event.usage.cache_creation_input_tokens
66
+ : undefined,
67
+ totalCostUsd: typeof event.total_cost_usd === "number" ? event.total_cost_usd : undefined,
68
+ };
69
+ }
70
+ export function updateClaudeStreamState(state, event) {
71
+ if (typeof event.session_id === "string" && event.session_id.length > 0) {
72
+ state.sessionId = event.session_id;
73
+ }
74
+ switch (event.type) {
75
+ case "assistant": {
76
+ const content = Array.isArray(event.message?.content) ? event.message?.content : [];
77
+ for (const block of content) {
78
+ if (block && typeof block === "object" && "type" in block) {
79
+ appendAssistantBlock(state.blocks, block);
80
+ }
81
+ }
82
+ break;
83
+ }
84
+ case "content_block_start": {
85
+ if (event.content_block && typeof event.content_block === "object") {
86
+ appendAssistantBlock(state.blocks, event.content_block);
87
+ }
88
+ break;
89
+ }
90
+ case "content_block_delta": {
91
+ const lastBlock = state.blocks[state.blocks.length - 1];
92
+ if (!lastBlock || !event.delta) {
93
+ break;
94
+ }
95
+ if (lastBlock.type === "text" && event.delta.type === "text_delta" && typeof event.delta.text === "string") {
96
+ lastBlock.text += event.delta.text;
97
+ }
98
+ else if (lastBlock.type === "thinking" &&
99
+ event.delta.type === "thinking_delta" &&
100
+ typeof event.delta.thinking === "string") {
101
+ lastBlock.thinking += event.delta.thinking;
102
+ }
103
+ else if (lastBlock.type === "tool_use" &&
104
+ typeof event.delta.partial_json === "string") {
105
+ const nextPartial = (lastBlock._partialJson ?? "") + event.delta.partial_json;
106
+ lastBlock._partialJson = nextPartial;
107
+ lastBlock.input = normalizeToolInput({
108
+ input: lastBlock.input,
109
+ _partialJson: nextPartial,
110
+ });
111
+ }
112
+ break;
113
+ }
114
+ case "user": {
115
+ const content = Array.isArray(event.message?.content) ? event.message?.content : [];
116
+ for (const block of content) {
117
+ if (block &&
118
+ typeof block === "object" &&
119
+ "type" in block &&
120
+ block.type === "tool_result") {
121
+ appendAssistantBlock(state.blocks, block);
122
+ }
123
+ }
124
+ break;
125
+ }
126
+ case "result": {
127
+ const usage = buildUsage(event);
128
+ if (usage) {
129
+ state.usage = usage;
130
+ }
131
+ if (typeof event.result === "string") {
132
+ const hasAssistantText = state.blocks.some((block) => block.type === "text");
133
+ if (!hasAssistantText && event.result.trim().length > 0) {
134
+ state.blocks.push({ type: "text", text: event.result });
135
+ }
136
+ }
137
+ else if (event.result && typeof event.result === "object") {
138
+ const resultContent = event.result.content;
139
+ if (Array.isArray(resultContent)) {
140
+ for (const block of resultContent) {
141
+ if (block && typeof block === "object" && "type" in block) {
142
+ appendAssistantBlock(state.blocks, block);
143
+ }
144
+ }
145
+ }
146
+ }
147
+ break;
148
+ }
149
+ default:
150
+ break;
151
+ }
152
+ return state;
153
+ }
@@ -0,0 +1,27 @@
1
+ import { ChildProcess } from "node:child_process";
2
+ import type { ContentBlock, ConversationTurn } from "./types.js";
3
+ export interface ClaudeStructuredRunnerCallbacks {
4
+ onOutput: (text: string) => void;
5
+ onBlocks: (blocks: ContentBlock[], usage?: ConversationTurn["usage"]) => void;
6
+ onSessionId: (sessionId: string) => void;
7
+ onClose: (exitCode: number | null, state: ClaudeStructuredRunnerState) => void;
8
+ onError: (error: Error) => void;
9
+ }
10
+ export interface ClaudeStructuredRunnerState {
11
+ stdoutBuffer: string;
12
+ assistantBlocks: ContentBlock[];
13
+ usage?: ConversationTurn["usage"];
14
+ sessionId: string | null;
15
+ }
16
+ export interface StartClaudeStructuredRunnerOptions {
17
+ command: string;
18
+ cwd: string;
19
+ env?: NodeJS.ProcessEnv;
20
+ callbacks: ClaudeStructuredRunnerCallbacks;
21
+ }
22
+ export interface ClaudeStructuredRunnerHandle {
23
+ child: ChildProcess;
24
+ getState: () => ClaudeStructuredRunnerState;
25
+ kill: () => void;
26
+ }
27
+ export declare function startClaudeStructuredRunner(options: StartClaudeStructuredRunnerOptions): ClaudeStructuredRunnerHandle;
@@ -0,0 +1,106 @@
1
+ import { spawn } from "node:child_process";
2
+ import { updateClaudeStreamState } from "./claude-stream-adapter.js";
3
+ function toContentBlocks(blocks) {
4
+ return blocks.map((block) => {
5
+ switch (block.type) {
6
+ case "text":
7
+ return { type: "text", text: typeof block.text === "string" ? block.text : "" };
8
+ case "thinking":
9
+ return { type: "thinking", thinking: typeof block.thinking === "string" ? block.thinking : "" };
10
+ case "tool_use":
11
+ return {
12
+ type: "tool_use",
13
+ id: typeof block.id === "string" ? block.id : "",
14
+ name: typeof block.name === "string" ? block.name : "",
15
+ description: typeof block.description === "string" ? block.description : undefined,
16
+ input: block.input && typeof block.input === "object" && !Array.isArray(block.input)
17
+ ? block.input
18
+ : {},
19
+ };
20
+ case "tool_result":
21
+ return {
22
+ type: "tool_result",
23
+ tool_use_id: typeof block.tool_use_id === "string" ? block.tool_use_id : "",
24
+ content: typeof block.content === "string"
25
+ ? block.content
26
+ : Array.isArray(block.content)
27
+ ? block.content
28
+ : "",
29
+ is_error: block.is_error === true,
30
+ };
31
+ default:
32
+ return { type: "text", text: JSON.stringify(block) };
33
+ }
34
+ });
35
+ }
36
+ export function startClaudeStructuredRunner(options) {
37
+ const state = {
38
+ stdoutBuffer: "",
39
+ assistantBlocks: [],
40
+ usage: undefined,
41
+ sessionId: null,
42
+ };
43
+ const child = spawn(options.command, [], {
44
+ cwd: options.cwd,
45
+ env: options.env,
46
+ shell: true,
47
+ stdio: ["ignore", "pipe", "pipe"],
48
+ });
49
+ child.stdout?.on("data", (chunk) => {
50
+ state.stdoutBuffer += chunk.toString();
51
+ const lines = state.stdoutBuffer.split("\n");
52
+ state.stdoutBuffer = lines.pop() || "";
53
+ for (const line of lines) {
54
+ const trimmed = line.trim();
55
+ if (!trimmed) {
56
+ continue;
57
+ }
58
+ try {
59
+ const event = JSON.parse(trimmed);
60
+ const nextState = updateClaudeStreamState({
61
+ blocks: state.assistantBlocks,
62
+ usage: state.usage,
63
+ sessionId: state.sessionId,
64
+ }, event);
65
+ state.assistantBlocks = nextState.blocks.map((block) => ({ ...block }));
66
+ state.usage = nextState.usage;
67
+ if (nextState.sessionId && nextState.sessionId !== state.sessionId) {
68
+ state.sessionId = nextState.sessionId;
69
+ options.callbacks.onSessionId(nextState.sessionId);
70
+ }
71
+ options.callbacks.onBlocks(state.assistantBlocks, state.usage);
72
+ }
73
+ catch {
74
+ options.callbacks.onOutput(trimmed + "\n");
75
+ }
76
+ }
77
+ });
78
+ child.stderr?.on("data", (chunk) => {
79
+ options.callbacks.onOutput(chunk.toString());
80
+ });
81
+ child.on("close", (code) => {
82
+ options.callbacks.onClose(code, {
83
+ stdoutBuffer: state.stdoutBuffer,
84
+ assistantBlocks: [...state.assistantBlocks],
85
+ usage: state.usage,
86
+ sessionId: state.sessionId,
87
+ });
88
+ });
89
+ child.on("error", (error) => {
90
+ options.callbacks.onError(error);
91
+ });
92
+ return {
93
+ child,
94
+ getState: () => ({
95
+ stdoutBuffer: state.stdoutBuffer,
96
+ assistantBlocks: [...state.assistantBlocks],
97
+ usage: state.usage,
98
+ sessionId: state.sessionId,
99
+ }),
100
+ kill: () => {
101
+ if (!child.killed) {
102
+ child.kill();
103
+ }
104
+ },
105
+ };
106
+ }
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2
2
  export {};
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2
2
  import process from "node:process";
3
3
  import { ensureConfig, hasConfigFile, isExecutionMode, resolveConfigPath, saveConfig } from "./config.js";
4
4
  import { startServer } from "./server.js";
@@ -105,12 +105,20 @@ function setConfigValue(config, key, value) {
105
105
  };
106
106
  case "defaultMode":
107
107
  if (!isExecutionMode(value)) {
108
- throw new Error("defaultMode must be auto-edit, default, or full-access");
108
+ throw new Error(`defaultMode must be one of: assist, agent, agent-max, auto-edit, default, full-access, managed, native`);
109
109
  }
110
110
  return {
111
111
  ...config,
112
112
  defaultMode: value
113
113
  };
114
+ case "https":
115
+ if (value !== "true" && value !== "false") {
116
+ throw new Error("https must be 'true' or 'false'");
117
+ }
118
+ return {
119
+ ...config,
120
+ https: value === "true"
121
+ };
114
122
  default:
115
123
  throw new Error(`Unsupported config key: ${key}`);
116
124
  }
package/dist/config.js CHANGED
@@ -5,9 +5,9 @@ import process from "node:process";
5
5
  const DEFAULT_CONFIG_DIR = ".wand";
6
6
  const DEFAULT_CONFIG_FILE = "config.json";
7
7
  export const defaultConfig = () => ({
8
- host: "0.0.0.0",
8
+ host: "127.0.0.1",
9
9
  port: 8443,
10
- https: true,
10
+ https: false,
11
11
  password: "change-me",
12
12
  defaultMode: "default",
13
13
  shell: process.env.SHELL || "/bin/bash",
@@ -60,7 +60,11 @@ export async function ensureConfig(configPath) {
60
60
  try {
61
61
  const raw = await readFile(configPath, "utf8");
62
62
  const merged = mergeWithDefaults(JSON.parse(raw));
63
- await writeFile(configPath, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
63
+ const normalized = `${JSON.stringify(merged, null, 2)}\n`;
64
+ // Only write if the file content actually changed
65
+ if (raw.trimEnd() !== normalized.trimEnd()) {
66
+ await writeFile(configPath, normalized, "utf8");
67
+ }
64
68
  return merged;
65
69
  }
66
70
  catch {
@@ -102,7 +106,7 @@ function mergeWithDefaults(input) {
102
106
  };
103
107
  }
104
108
  export function isExecutionMode(value) {
105
- return value === "auto-edit" || value === "default" || value === "full-access" || value === "native" || value === "managed";
109
+ return value === "assist" || value === "agent" || value === "agent-max" || value === "auto-edit" || value === "default" || value === "full-access" || value === "native" || value === "managed";
106
110
  }
107
111
  function normalizePresetCommand(command) {
108
112
  const trimmed = command.trim();
@@ -1,158 +1,22 @@
1
- /** Strip ANSI escape sequences from raw PTY output */
2
- function stripAnsi(text) {
3
- let stripped = "";
4
- for (let i = 0; i < text.length; i++) {
5
- const ch = text.charCodeAt(i);
6
- if (ch === 27) {
7
- i++;
8
- if (i >= text.length)
9
- break;
10
- const next = text.charCodeAt(i);
11
- if (next === 91) {
12
- // CSI sequence: skip until final byte (64-126)
13
- i++;
14
- while (i < text.length) {
15
- const c = text.charCodeAt(i);
16
- if (c >= 64 && c <= 126)
17
- break;
18
- i++;
19
- }
20
- }
21
- else if (next === 93) {
22
- // OSC sequence: skip until BEL (7) or ESC\ (27 92)
23
- i++;
24
- while (i < text.length) {
25
- if (text.charCodeAt(i) === 7)
26
- break;
27
- if (text.charCodeAt(i) === 27 && i + 1 < text.length && text.charCodeAt(i + 1) === 92) {
28
- i++;
29
- break;
30
- }
31
- i++;
32
- }
33
- }
34
- // Other escape sequences: skip the next character
35
- continue;
36
- }
37
- // Skip control characters except \n, \r, \t
38
- if (ch < 32 && ch !== 10 && ch !== 13 && ch !== 9)
39
- continue;
40
- stripped += text.charAt(i);
41
- }
42
- return stripped;
43
- }
44
- /** Check if a line is noise from Claude TUI */
45
- function isNoiseLine(line) {
46
- if (!line)
47
- return true;
48
- if (line.startsWith("────"))
49
- return true;
50
- if (line === "❯")
51
- return true;
52
- if (line.includes("esc to interrupt"))
53
- return true;
54
- if (line.includes("Claude Code v"))
55
- return true;
56
- if (/^Sonnet\b/.test(line))
57
- return true;
58
- if (line.startsWith("~/"))
59
- return true;
60
- if (line.includes("● high"))
61
- return true;
62
- if (line.includes("Failed to install Anthropic"))
63
- return true;
64
- if (line.includes("Claude Code has switched"))
65
- return true;
66
- if (line.includes("Fluttering"))
67
- return true;
68
- if (line.includes("? for shortcuts"))
69
- return true;
70
- if (line.startsWith("0;") || line.startsWith("9;"))
71
- return true;
72
- if (line.includes("Claude is waiting"))
73
- return true;
74
- if (/[✢✳✶✻✽]/.test(line))
75
- return true;
76
- if (/^[▐▝▘]/.test(line))
77
- return true;
78
- const singleCharNoise = ["lu", "ue", "tr", "ti", "g", "n", "i…", "…", "uts", "lt", "rg", "·"];
79
- if (singleCharNoise.includes(line) && line.length < 4)
80
- return true;
81
- if (line.startsWith("✽F") || line.startsWith("✻F"))
82
- return true;
83
- if (line.includes("[wand]"))
84
- return true;
85
- if (line.includes("⏵"))
86
- return true;
87
- if (line.includes("acceptedit"))
88
- return true;
89
- if (line.includes("shift+tab"))
90
- return true;
91
- if (line.includes("tabtocycle"))
92
- return true;
93
- if (line.includes("ctrl+g"))
94
- return true;
95
- if (line.includes("/effort"))
96
- return true;
97
- if (line.includes("Haiku"))
98
- return true;
99
- if (line.includes("to cycle"))
100
- return true;
101
- if (/\bhigh\s*·/.test(line) || /\bmedium\s*·/.test(line) || /\blow\s*·/.test(line))
102
- return true;
103
- if (line.includes("npm WARN") || line.includes("npm notice"))
104
- return true;
105
- if (/^Using .* for .* session/.test(line))
106
- return true;
107
- if (line.includes("Permissions") && line.includes("mode"))
108
- return true;
109
- if (line.startsWith("Press ") && line.includes(" for"))
110
- return true;
111
- if (line.startsWith("type ") && line.includes(" to "))
112
- return true;
113
- if (line.length < 3 && !/^[a-zA-Z]{3}$/.test(line))
114
- return true;
115
- return false;
116
- }
117
- /** Filter assistant content line */
118
- function isAssistantContent(line) {
119
- if (line.includes("⏺"))
120
- return true;
121
- if (line.length < 8)
122
- return false;
123
- if (/[✢✳✶✻✽]/.test(line))
124
- return false;
125
- if (/^[▐▝▘]/.test(line))
126
- return false;
127
- if (line.startsWith("❯"))
128
- return false;
129
- if (line.includes("esctointerrupt"))
130
- return false;
131
- if (line.startsWith("?for") || line.startsWith("? for"))
132
- return false;
133
- return true;
134
- }
1
+ import { stripAnsi, isNoiseLine } from "./pty-text-utils.js";
135
2
  export function parseMessages(output) {
136
3
  const messages = [];
137
4
  if (!output)
138
5
  return messages;
139
- // Strip ANSI and normalize
140
6
  const stripped = stripAnsi(output).replace(/\r/g, "\n");
141
- const lines = stripped.split("\n").map((l) => l.trim()).filter(Boolean);
142
- // Filter noise
143
- const cleaned = lines.filter((line) => !isNoiseLine(line));
7
+ const lines = stripped.split("\n");
8
+ const cleaned = lines.filter((line) => !isNoiseLine(line.trim()));
144
9
  if (!cleaned.length)
145
10
  return messages;
146
11
  const turns = [];
147
12
  let currentUserText = null;
148
13
  let currentAssistantLines = [];
149
- for (const line of cleaned) {
14
+ for (const rawLine of cleaned) {
15
+ const line = rawLine.trim();
150
16
  if (line.startsWith("❯")) {
151
17
  const afterPrompt = line.replace(/^❯\s*/, "").trim();
152
- // Skip prompt suggestions
153
18
  if (afterPrompt.startsWith("Try"))
154
19
  continue;
155
- // Finalize previous turn
156
20
  if (currentUserText !== null && currentAssistantLines.length > 0) {
157
21
  turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
158
22
  currentAssistantLines = [];
@@ -161,7 +25,6 @@ export function parseMessages(output) {
161
25
  currentUserText = afterPrompt;
162
26
  }
163
27
  else {
164
- // Standalone ❯ — finalize and reset
165
28
  if (currentUserText !== null && currentAssistantLines.length > 0) {
166
29
  turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
167
30
  currentAssistantLines = [];
@@ -169,21 +32,24 @@ export function parseMessages(output) {
169
32
  currentUserText = null;
170
33
  }
171
34
  }
172
- else if (currentUserText !== null && isAssistantContent(line)) {
173
- // Cleanprefix
174
- const cleanLine = line.startsWith("⏺") ? line.slice(1).trim() : line;
175
- if (cleanLine)
176
- currentAssistantLines.push(cleanLine);
35
+ else if (currentUserText !== null) {
36
+ const contentLine = rawLine.startsWith("") ? rawLine.slice(1) : rawLine;
37
+ currentAssistantLines.push(contentLine);
177
38
  }
178
39
  }
179
- // Finalize last turn
180
40
  if (currentUserText !== null && currentAssistantLines.length > 0) {
181
41
  turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
182
42
  }
183
- // Convert to messages
43
+ else if (currentUserText !== null) {
44
+ // User input exists but no assistant response yet — still record the turn
45
+ turns.push({ user: currentUserText, assistantLines: currentAssistantLines });
46
+ }
184
47
  for (const turn of turns) {
185
48
  messages.push({ role: "user", content: turn.user });
186
- messages.push({ role: "assistant", content: turn.assistantLines.join("\n") });
49
+ const content = turn.assistantLines.join("\n").replace(/[ \t]+\n/g, "\n").replace(/[\n\s]+$/, "");
50
+ if (content) {
51
+ messages.push({ role: "assistant", content });
52
+ }
187
53
  }
188
54
  return messages;
189
55
  }
@@ -0,0 +1,57 @@
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
+ }