voicecc 1.0.7

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 (59) hide show
  1. package/.claude-plugin/plugin.json +6 -0
  2. package/README.md +48 -0
  3. package/bin/voicecc.js +39 -0
  4. package/dashboard/dist/assets/index-BXemFrMp.css +1 -0
  5. package/dashboard/dist/assets/index-dAYfRls7.js +11 -0
  6. package/dashboard/dist/audio-processor.js +126 -0
  7. package/dashboard/dist/index.html +13 -0
  8. package/dashboard/routes/auth.ts +119 -0
  9. package/dashboard/routes/browser-call.ts +87 -0
  10. package/dashboard/routes/claude-md.ts +50 -0
  11. package/dashboard/routes/conversations.ts +203 -0
  12. package/dashboard/routes/integrations.ts +154 -0
  13. package/dashboard/routes/mcp-servers.ts +198 -0
  14. package/dashboard/routes/settings.ts +64 -0
  15. package/dashboard/routes/tunnel.ts +66 -0
  16. package/dashboard/routes/twilio.ts +120 -0
  17. package/dashboard/routes/voice.ts +48 -0
  18. package/dashboard/routes/webrtc.ts +85 -0
  19. package/dashboard/server.ts +130 -0
  20. package/dashboard/tsconfig.json +13 -0
  21. package/init/CLAUDE.md +18 -0
  22. package/package.json +59 -0
  23. package/run.ts +68 -0
  24. package/scripts/postinstall.js +228 -0
  25. package/services/browser-call-manager.ts +106 -0
  26. package/services/device-pairing.ts +176 -0
  27. package/services/env.ts +88 -0
  28. package/services/tunnel.ts +204 -0
  29. package/services/twilio-manager.ts +126 -0
  30. package/sidecar/assets/startup.pcm +0 -0
  31. package/sidecar/audio-adapter.ts +60 -0
  32. package/sidecar/audio-capture.ts +220 -0
  33. package/sidecar/browser-audio-playback.test.ts +149 -0
  34. package/sidecar/browser-audio.ts +147 -0
  35. package/sidecar/browser-server.ts +331 -0
  36. package/sidecar/chime.test.ts +69 -0
  37. package/sidecar/chime.ts +54 -0
  38. package/sidecar/claude-session.ts +295 -0
  39. package/sidecar/endpointing.ts +163 -0
  40. package/sidecar/index.ts +83 -0
  41. package/sidecar/local-audio.ts +126 -0
  42. package/sidecar/mic-vpio +0 -0
  43. package/sidecar/mic-vpio.swift +484 -0
  44. package/sidecar/mock-tts-server-tagged.mjs +132 -0
  45. package/sidecar/narration.ts +204 -0
  46. package/sidecar/scripts/generate-startup-audio.py +79 -0
  47. package/sidecar/session-lock.ts +123 -0
  48. package/sidecar/sherpa-onnx-node.d.ts +4 -0
  49. package/sidecar/stt.ts +199 -0
  50. package/sidecar/tts-server.py +193 -0
  51. package/sidecar/tts.ts +481 -0
  52. package/sidecar/twilio-audio.ts +338 -0
  53. package/sidecar/twilio-server.ts +436 -0
  54. package/sidecar/types.ts +210 -0
  55. package/sidecar/vad.ts +101 -0
  56. package/sidecar/voice-loop-bugs.test.ts +522 -0
  57. package/sidecar/voice-session.ts +523 -0
  58. package/skills/voice/SKILL.md +26 -0
  59. package/tsconfig.json +22 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * AudioWorklet processor for browser voice calls.
3
+ *
4
+ * Runs in the browser's audio rendering thread. Handles two jobs:
5
+ * - Mic capture: accumulates input samples into chunks, posts them to main thread
6
+ * - Speaker playback: reads from a chunk queue fed by main thread, writes to output
7
+ *
8
+ * Responsibilities:
9
+ * - Buffer incoming mic audio and emit fixed-size chunks to main thread
10
+ * - Accept playback audio from main thread and enqueue into chunk queue
11
+ * - Provide "clear" support to flush the chunk queue on interruption
12
+ *
13
+ * Must be plain JavaScript -- AudioWorklet modules cannot be bundled by Vite.
14
+ */
15
+
16
+ // ============================================================================
17
+ // CONSTANTS
18
+ // ============================================================================
19
+
20
+ /** Number of mic samples to accumulate before posting to main thread */
21
+ const CHUNK_SIZE = 512;
22
+
23
+ // ============================================================================
24
+ // PROCESSOR
25
+ // ============================================================================
26
+
27
+ class AudioProcessor extends AudioWorkletProcessor {
28
+ constructor() {
29
+ super();
30
+
31
+ // Mic capture buffer
32
+ this._micBuffer = new Float32Array(CHUNK_SIZE);
33
+ this._micBufferIndex = 0;
34
+
35
+ // Speaker playback chunk queue
36
+ this._chunks = [];
37
+ this._chunkIndex = 0; // read position within current chunk
38
+
39
+ // Handle messages from main thread
40
+ this.port.onmessage = (event) => {
41
+ const { type, samples } = event.data;
42
+
43
+ if (type === "playback" && samples) {
44
+ this._chunks.push(samples);
45
+ } else if (type === "clear") {
46
+ this._chunks.length = 0;
47
+ this._chunkIndex = 0;
48
+ }
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Read samples from the chunk queue into the output array.
54
+ * Writes silence (0) when the queue is empty.
55
+ *
56
+ * @param {Float32Array} output - Destination array to fill
57
+ */
58
+ _readFromQueue(output) {
59
+ let written = 0;
60
+
61
+ while (written < output.length) {
62
+ if (this._chunks.length === 0) {
63
+ // Queue empty -- fill remaining with silence
64
+ for (let i = written; i < output.length; i++) {
65
+ output[i] = 0;
66
+ }
67
+ return;
68
+ }
69
+
70
+ const chunk = this._chunks[0];
71
+ const available = chunk.length - this._chunkIndex;
72
+ const needed = output.length - written;
73
+ const toCopy = Math.min(available, needed);
74
+
75
+ for (let i = 0; i < toCopy; i++) {
76
+ output[written++] = chunk[this._chunkIndex++];
77
+ }
78
+
79
+ if (this._chunkIndex >= chunk.length) {
80
+ this._chunks.shift();
81
+ this._chunkIndex = 0;
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Called by the audio rendering thread for each 128-sample frame.
88
+ *
89
+ * @param {Float32Array[][]} inputs - Input audio channels (mic)
90
+ * @param {Float32Array[][]} outputs - Output audio channels (speaker)
91
+ * @param {Record<string, Float32Array>} parameters - AudioParam values (unused)
92
+ * @returns {boolean} true to keep the processor alive
93
+ */
94
+ process(inputs, outputs, parameters) {
95
+ // -- Mic capture: accumulate input samples and post chunks --
96
+ const input = inputs[0];
97
+ if (input && input[0]) {
98
+ const inputChannel = input[0];
99
+ for (let i = 0; i < inputChannel.length; i++) {
100
+ this._micBuffer[this._micBufferIndex++] = inputChannel[i];
101
+
102
+ if (this._micBufferIndex >= CHUNK_SIZE) {
103
+ // Post a copy to main thread
104
+ this.port.postMessage({
105
+ type: "audio",
106
+ samples: this._micBuffer.slice(),
107
+ });
108
+ this._micBufferIndex = 0;
109
+ }
110
+ }
111
+ }
112
+
113
+ // -- Speaker playback: read from chunk queue into output (mono -> all channels) --
114
+ const output = outputs[0];
115
+ if (output && output[0]) {
116
+ this._readFromQueue(output[0]);
117
+ for (let ch = 1; ch < output.length; ch++) {
118
+ output[ch].set(output[0]);
119
+ }
120
+ }
121
+
122
+ return true;
123
+ }
124
+ }
125
+
126
+ registerProcessor("audio-processor", AudioProcessor);
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Claude Voice</title>
7
+ <script type="module" crossorigin src="/assets/index-dAYfRls7.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-BXemFrMp.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Claude Code authentication probe route.
3
+ *
4
+ * Runs `claude -p "hi" --output-format json` to check whether the user
5
+ * is authenticated. When logged out this returns instantly (~30ms) with
6
+ * exit code 1 and `is_error: true`. When logged in it starts an API call,
7
+ * so we use a short timeout and kill the process -- if it's still running
8
+ * after the deadline, the user is authenticated.
9
+ *
10
+ * - GET / -- probe and return { authenticated: boolean }
11
+ * - POST /login -- open Terminal.app with `claude` for interactive login
12
+ */
13
+
14
+ import { Hono } from "hono";
15
+ import { execFile, spawn } from "child_process";
16
+ import { homedir } from "os";
17
+ import { join } from "path";
18
+
19
+ // ============================================================================
20
+ // CONSTANTS
21
+ // ============================================================================
22
+
23
+ const CLAUDE_BIN = join(homedir(), ".local", "bin", "claude");
24
+ const PROBE_TIMEOUT_MS = 5_000;
25
+
26
+ // ============================================================================
27
+ // HELPER FUNCTIONS
28
+ // ============================================================================
29
+
30
+ /**
31
+ * Run `claude -p "hi" --output-format json` with stdin closed.
32
+ *
33
+ * - Logged out: exits 1 instantly (~30ms) with "Not logged in" in the JSON result.
34
+ * - Logged in: completes the API call and exits 0 with a JSON response.
35
+ * - Safety timeout at 5s: kills the process and assumes authenticated.
36
+ *
37
+ * @returns true if authenticated, false otherwise
38
+ */
39
+ async function probeClaudeAuth(): Promise<boolean> {
40
+ return new Promise((resolve) => {
41
+ let resolved = false;
42
+ const done = (value: boolean) => {
43
+ if (resolved) return;
44
+ resolved = true;
45
+ resolve(value);
46
+ };
47
+
48
+ const child = spawn(CLAUDE_BIN, ["-p", "hi", "--output-format", "json"], {
49
+ stdio: ["ignore", "pipe", "pipe"],
50
+ });
51
+
52
+ const timer = setTimeout(() => {
53
+ child.kill();
54
+ done(true);
55
+ }, PROBE_TIMEOUT_MS);
56
+
57
+ let stdout = "";
58
+ child.stdout.on("data", (chunk: Buffer) => { stdout += chunk.toString(); });
59
+
60
+ child.on("close", (code) => {
61
+ clearTimeout(timer);
62
+
63
+ if (code === 0) {
64
+ done(true);
65
+ return;
66
+ }
67
+
68
+ try {
69
+ const json = JSON.parse(stdout);
70
+ if (json.is_error && typeof json.result === "string" && json.result.includes("Not logged in")) {
71
+ done(false);
72
+ return;
73
+ }
74
+ } catch {
75
+ // couldn't parse JSON
76
+ }
77
+
78
+ done(false);
79
+ });
80
+ });
81
+ }
82
+
83
+ // ============================================================================
84
+ // ROUTES
85
+ // ============================================================================
86
+
87
+ /**
88
+ * Create Hono route group for Claude Code auth testing.
89
+ *
90
+ * @returns Hono instance with GET / route
91
+ */
92
+ export function authRoutes(): Hono {
93
+ const app = new Hono();
94
+
95
+ app.get("/", async (c) => {
96
+ const authenticated = await probeClaudeAuth();
97
+ return c.json({ authenticated });
98
+ });
99
+
100
+ /** Open Terminal.app with `claude` for interactive login */
101
+ app.post("/login", async (c) => {
102
+ const script = `tell application "Terminal"
103
+ activate
104
+ do script "${CLAUDE_BIN}"
105
+ end tell`;
106
+
107
+ return new Promise((resolve) => {
108
+ execFile("osascript", ["-e", script], (err) => {
109
+ if (err) {
110
+ resolve(c.json({ error: err.message }, 500));
111
+ return;
112
+ }
113
+ resolve(c.json({ success: true }));
114
+ });
115
+ });
116
+ });
117
+
118
+ return app;
119
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Browser call server management API routes.
3
+ *
4
+ * Manages the lifecycle of the browser-server (direct WebSocket audio):
5
+ * - GET /status -- browser-server running state + tunnel URL
6
+ * - POST /start -- start tunnel + browser-server (rejects if Twilio server holds the port)
7
+ * - POST /stop -- stop browser-server + tunnel
8
+ */
9
+
10
+ import { Hono } from "hono";
11
+ import { startBrowserCallServer, stopBrowserCallServer, getBrowserCallStatus, isBrowserCallRunning } from "../../services/browser-call-manager.js";
12
+ import { startTunnel, stopTunnel, getTunnelUrl, isTunnelRunning } from "../../services/tunnel.js";
13
+ import { readEnv } from "../../services/env.js";
14
+ import { isRunning as isTwilioRunning } from "../../services/twilio-manager.js";
15
+
16
+ // ============================================================================
17
+ // STATE
18
+ // ============================================================================
19
+
20
+ /** Dashboard port -- set by server.ts when calling setDashboardPort */
21
+ let dashboardPort = 0;
22
+
23
+ /**
24
+ * Set the dashboard port for browser-server proxying.
25
+ * Called by server.ts after the Hono server starts listening.
26
+ *
27
+ * @param port - The dashboard server port
28
+ */
29
+ export function setDashboardPort(port: number): void {
30
+ dashboardPort = port;
31
+ }
32
+
33
+ // ============================================================================
34
+ // ROUTES
35
+ // ============================================================================
36
+
37
+ /**
38
+ * Create Hono route group for browser call operations.
39
+ *
40
+ * @returns Hono instance with status, start, stop routes
41
+ */
42
+ export function browserCallRoutes(): Hono {
43
+ const app = new Hono();
44
+
45
+ /** Get browser call server status + tunnel URL */
46
+ app.get("/status", (c) => {
47
+ const status = getBrowserCallStatus();
48
+ return c.json({ ...status, tunnelUrl: getTunnelUrl() });
49
+ });
50
+
51
+ /** Start tunnel + browser call server */
52
+ app.post("/start", async (c) => {
53
+ try {
54
+ // Port conflict check: Twilio server uses the same port
55
+ if (isTwilioRunning()) {
56
+ return c.json({ error: "Twilio server is already running on this port" }, 409);
57
+ }
58
+
59
+ const envVars = await readEnv();
60
+ const port = parseInt(envVars.TWILIO_PORT || "8080", 10);
61
+
62
+ if (!isTunnelRunning()) {
63
+ await startTunnel(port);
64
+ }
65
+
66
+ if (!isBrowserCallRunning()) {
67
+ await startBrowserCallServer(dashboardPort);
68
+ }
69
+
70
+ return c.json({ success: true });
71
+ } catch (err) {
72
+ const message = err instanceof Error ? err.message : "Failed to start";
73
+ return c.json({ error: message }, 500);
74
+ }
75
+ });
76
+
77
+ /** Stop browser call server. Only stops tunnel if Twilio is also stopped. */
78
+ app.post("/stop", (c) => {
79
+ stopBrowserCallServer();
80
+ if (!isTwilioRunning()) {
81
+ stopTunnel();
82
+ }
83
+ return c.json({ success: true });
84
+ });
85
+
86
+ return app;
87
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * CLAUDE.md read/write API routes.
3
+ *
4
+ * Exposes endpoints to read and write the project's CLAUDE.md file:
5
+ * - GET / -- read the current CLAUDE.md content
6
+ * - POST / -- write new content to CLAUDE.md
7
+ */
8
+
9
+ import { Hono } from "hono";
10
+ import { readFile, writeFile } from "fs/promises";
11
+ import { join } from "path";
12
+
13
+ // ============================================================================
14
+ // CONSTANTS
15
+ // ============================================================================
16
+
17
+ const CLAUDE_MD_PATH = join(process.cwd(), "CLAUDE.md");
18
+
19
+ // ============================================================================
20
+ // ROUTES
21
+ // ============================================================================
22
+
23
+ /**
24
+ * Create Hono route group for CLAUDE.md operations.
25
+ *
26
+ * @returns Hono instance with GET / and POST / routes
27
+ */
28
+ export function claudeMdRoutes(): Hono {
29
+ const app = new Hono();
30
+
31
+ /** Read the CLAUDE.md file */
32
+ app.get("/", async (c) => {
33
+ const content = await readFile(CLAUDE_MD_PATH, "utf-8");
34
+ return c.json({ content });
35
+ });
36
+
37
+ /** Write new content to CLAUDE.md */
38
+ app.post("/", async (c) => {
39
+ const body = await c.req.json<{ content?: string }>();
40
+
41
+ if (body.content === undefined) {
42
+ return c.json({ error: "Missing 'content' in request body" }, 400);
43
+ }
44
+
45
+ await writeFile(CLAUDE_MD_PATH, body.content, "utf-8");
46
+ return c.json({ success: true });
47
+ });
48
+
49
+ return app;
50
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Conversation session API routes.
3
+ *
4
+ * Lists and reads Claude Code conversation sessions from JSONL log files:
5
+ * - GET / -- list all sessions with summaries
6
+ * - GET /:sessionId -- get all messages for a specific session
7
+ */
8
+
9
+ import { Hono } from "hono";
10
+ import { readdir, stat, access } from "fs/promises";
11
+ import { join, basename } from "path";
12
+ import { homedir } from "os";
13
+ import { createReadStream } from "fs";
14
+ import { createInterface } from "readline";
15
+
16
+ // ============================================================================
17
+ // TYPES
18
+ // ============================================================================
19
+
20
+ /** Summary of a conversation session */
21
+ interface ConversationSummary {
22
+ sessionId: string;
23
+ firstMessage: string;
24
+ timestamp: string;
25
+ messageCount: number;
26
+ }
27
+
28
+ /** A single conversation turn */
29
+ interface ConversationMessage {
30
+ role: "user" | "assistant";
31
+ content: string;
32
+ timestamp: string;
33
+ }
34
+
35
+ // ============================================================================
36
+ // CONSTANTS
37
+ // ============================================================================
38
+
39
+ /** Claude Code encodes the project path by replacing "/" with "-" */
40
+ const PROJECT_DIR_NAME = process.cwd().replace(/\//g, "-");
41
+ const SESSIONS_DIR = join(homedir(), ".claude", "projects", PROJECT_DIR_NAME);
42
+
43
+ // ============================================================================
44
+ // ROUTES
45
+ // ============================================================================
46
+
47
+ /**
48
+ * Create Hono route group for conversation operations.
49
+ *
50
+ * @returns Hono instance with GET / (list) and GET /:sessionId (detail)
51
+ */
52
+ export function conversationRoutes(): Hono {
53
+ const app = new Hono();
54
+
55
+ /** List all conversation sessions with summaries */
56
+ app.get("/", async (c) => {
57
+ const files = await readdir(SESSIONS_DIR);
58
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
59
+
60
+ const summaries: ConversationSummary[] = [];
61
+
62
+ for (const file of jsonlFiles) {
63
+ const filePath = join(SESSIONS_DIR, file);
64
+ const fileStat = await stat(filePath);
65
+ const sessionId = basename(file, ".jsonl");
66
+
67
+ const { firstUserMessage, messageCount } = await extractSessionSummary(filePath);
68
+
69
+ summaries.push({
70
+ sessionId,
71
+ firstMessage: firstUserMessage,
72
+ timestamp: fileStat.mtime.toISOString(),
73
+ messageCount,
74
+ });
75
+ }
76
+
77
+ summaries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
78
+ return c.json(summaries);
79
+ });
80
+
81
+ /** Get all messages for a specific session */
82
+ app.get("/:sessionId", async (c) => {
83
+ const sessionId = c.req.param("sessionId");
84
+ const filePath = join(SESSIONS_DIR, `${sessionId}.jsonl`);
85
+
86
+ try {
87
+ await access(filePath);
88
+ } catch {
89
+ return c.json({ error: "Session not found" }, 404);
90
+ }
91
+
92
+ const messages = await parseSessionMessages(filePath);
93
+ return c.json(messages);
94
+ });
95
+
96
+ return app;
97
+ }
98
+
99
+ // ============================================================================
100
+ // HELPER FUNCTIONS
101
+ // ============================================================================
102
+
103
+ /**
104
+ * Read the first user message and count total messages in a session file.
105
+ *
106
+ * @param filePath - Absolute path to the JSONL file
107
+ * @returns The first user message text and total message count
108
+ */
109
+ async function extractSessionSummary(filePath: string): Promise<{ firstUserMessage: string; messageCount: number }> {
110
+ let firstUserMessage = "(empty)";
111
+ let messageCount = 0;
112
+ let foundFirst = false;
113
+
114
+ const rl = createInterface({ input: createReadStream(filePath, "utf-8"), crlfDelay: Infinity });
115
+
116
+ for await (const line of rl) {
117
+ if (!line.trim()) continue;
118
+ try {
119
+ const entry = JSON.parse(line);
120
+ if (entry.type !== "user" && entry.type !== "assistant") continue;
121
+
122
+ messageCount++;
123
+
124
+ if (!foundFirst && entry.type === "user") {
125
+ const content = entry.message?.content;
126
+ if (typeof content === "string") {
127
+ firstUserMessage = content.slice(0, 120);
128
+ }
129
+ foundFirst = true;
130
+ }
131
+ } catch {
132
+ // Skip malformed lines
133
+ }
134
+ }
135
+
136
+ return { firstUserMessage, messageCount };
137
+ }
138
+
139
+ /**
140
+ * Parse all user and assistant messages from a session JSONL file.
141
+ * Deduplicates assistant messages by requestId.
142
+ *
143
+ * @param filePath - Absolute path to the JSONL file
144
+ * @returns Array of ConversationMessage sorted by timestamp
145
+ */
146
+ async function parseSessionMessages(filePath: string): Promise<ConversationMessage[]> {
147
+ const messages: ConversationMessage[] = [];
148
+ const seenUserUuids = new Set<string>();
149
+ const assistantTexts = new Map<string, { text: string; timestamp: string }>();
150
+
151
+ const rl = createInterface({ input: createReadStream(filePath, "utf-8"), crlfDelay: Infinity });
152
+
153
+ for await (const line of rl) {
154
+ if (!line.trim()) continue;
155
+ try {
156
+ const entry = JSON.parse(line);
157
+
158
+ if (entry.type === "user" && entry.message?.role === "user") {
159
+ if (seenUserUuids.has(entry.uuid)) continue;
160
+ seenUserUuids.add(entry.uuid);
161
+
162
+ const content = entry.message.content;
163
+ if (typeof content === "string" && content.trim()) {
164
+ messages.push({ role: "user", content, timestamp: entry.timestamp });
165
+ }
166
+ continue;
167
+ }
168
+
169
+ if (entry.type === "assistant" && entry.message?.content) {
170
+ const requestId = entry.requestId;
171
+ if (!requestId) continue;
172
+
173
+ const blocks = entry.message.content;
174
+ if (!Array.isArray(blocks)) continue;
175
+
176
+ const textParts: string[] = [];
177
+ for (const block of blocks) {
178
+ if (block.type === "text" && block.text?.trim()) {
179
+ textParts.push(block.text);
180
+ }
181
+ }
182
+
183
+ if (textParts.length > 0) {
184
+ const combined = textParts.join("");
185
+ const existing = assistantTexts.get(requestId);
186
+ if (!existing || combined.length > existing.text.length) {
187
+ assistantTexts.set(requestId, { text: combined, timestamp: entry.timestamp });
188
+ }
189
+ }
190
+ continue;
191
+ }
192
+ } catch {
193
+ // Skip malformed lines
194
+ }
195
+ }
196
+
197
+ for (const [, { text, timestamp }] of assistantTexts) {
198
+ messages.push({ role: "assistant", content: text, timestamp });
199
+ }
200
+
201
+ messages.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
202
+ return messages;
203
+ }