codepiper 0.1.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 (149) hide show
  1. package/.env.example +28 -0
  2. package/CHANGELOG.md +10 -0
  3. package/LEGAL_NOTICE.md +39 -0
  4. package/LICENSE +21 -0
  5. package/README.md +524 -0
  6. package/package.json +90 -0
  7. package/packages/cli/package.json +13 -0
  8. package/packages/cli/src/commands/analytics.ts +157 -0
  9. package/packages/cli/src/commands/attach.ts +299 -0
  10. package/packages/cli/src/commands/audit.ts +50 -0
  11. package/packages/cli/src/commands/auth.ts +261 -0
  12. package/packages/cli/src/commands/daemon.ts +162 -0
  13. package/packages/cli/src/commands/doctor.ts +303 -0
  14. package/packages/cli/src/commands/env-set.ts +162 -0
  15. package/packages/cli/src/commands/hook-forward.ts +268 -0
  16. package/packages/cli/src/commands/keys.ts +77 -0
  17. package/packages/cli/src/commands/kill.ts +19 -0
  18. package/packages/cli/src/commands/logs.ts +419 -0
  19. package/packages/cli/src/commands/model.ts +172 -0
  20. package/packages/cli/src/commands/policy-set.ts +185 -0
  21. package/packages/cli/src/commands/policy.ts +227 -0
  22. package/packages/cli/src/commands/providers.ts +114 -0
  23. package/packages/cli/src/commands/resize.ts +34 -0
  24. package/packages/cli/src/commands/send.ts +184 -0
  25. package/packages/cli/src/commands/sessions.ts +202 -0
  26. package/packages/cli/src/commands/slash.ts +92 -0
  27. package/packages/cli/src/commands/start.ts +243 -0
  28. package/packages/cli/src/commands/stop.ts +19 -0
  29. package/packages/cli/src/commands/tail.ts +137 -0
  30. package/packages/cli/src/commands/workflow.ts +786 -0
  31. package/packages/cli/src/commands/workspace.ts +127 -0
  32. package/packages/cli/src/lib/api.ts +78 -0
  33. package/packages/cli/src/lib/args.ts +72 -0
  34. package/packages/cli/src/lib/format.ts +93 -0
  35. package/packages/cli/src/main.ts +563 -0
  36. package/packages/core/package.json +7 -0
  37. package/packages/core/src/config.ts +30 -0
  38. package/packages/core/src/errors.ts +38 -0
  39. package/packages/core/src/eventBus.ts +56 -0
  40. package/packages/core/src/eventBusAdapter.ts +143 -0
  41. package/packages/core/src/index.ts +10 -0
  42. package/packages/core/src/sqliteEventBus.ts +336 -0
  43. package/packages/core/src/types.ts +63 -0
  44. package/packages/daemon/package.json +11 -0
  45. package/packages/daemon/src/api/analyticsRoutes.ts +343 -0
  46. package/packages/daemon/src/api/authRoutes.ts +344 -0
  47. package/packages/daemon/src/api/bodyLimit.ts +133 -0
  48. package/packages/daemon/src/api/envSetRoutes.ts +170 -0
  49. package/packages/daemon/src/api/gitRoutes.ts +409 -0
  50. package/packages/daemon/src/api/hooks.ts +588 -0
  51. package/packages/daemon/src/api/inputPolicy.ts +249 -0
  52. package/packages/daemon/src/api/notificationRoutes.ts +532 -0
  53. package/packages/daemon/src/api/policyRoutes.ts +234 -0
  54. package/packages/daemon/src/api/policySetRoutes.ts +445 -0
  55. package/packages/daemon/src/api/routeUtils.ts +28 -0
  56. package/packages/daemon/src/api/routes.ts +1004 -0
  57. package/packages/daemon/src/api/server.ts +1388 -0
  58. package/packages/daemon/src/api/settingsRoutes.ts +367 -0
  59. package/packages/daemon/src/api/sqliteErrors.ts +47 -0
  60. package/packages/daemon/src/api/stt.ts +143 -0
  61. package/packages/daemon/src/api/terminalRoutes.ts +200 -0
  62. package/packages/daemon/src/api/validation.ts +287 -0
  63. package/packages/daemon/src/api/validationRoutes.ts +174 -0
  64. package/packages/daemon/src/api/workflowRoutes.ts +567 -0
  65. package/packages/daemon/src/api/workspaceRoutes.ts +151 -0
  66. package/packages/daemon/src/api/ws.ts +1588 -0
  67. package/packages/daemon/src/auth/apiRateLimiter.ts +73 -0
  68. package/packages/daemon/src/auth/authMiddleware.ts +305 -0
  69. package/packages/daemon/src/auth/authService.ts +496 -0
  70. package/packages/daemon/src/auth/rateLimiter.ts +137 -0
  71. package/packages/daemon/src/config/pricing.ts +79 -0
  72. package/packages/daemon/src/crypto/encryption.ts +196 -0
  73. package/packages/daemon/src/db/db.ts +2745 -0
  74. package/packages/daemon/src/db/index.ts +16 -0
  75. package/packages/daemon/src/db/migrations.ts +182 -0
  76. package/packages/daemon/src/db/policyDb.ts +349 -0
  77. package/packages/daemon/src/db/schema.sql +408 -0
  78. package/packages/daemon/src/db/workflowDb.ts +464 -0
  79. package/packages/daemon/src/git/gitUtils.ts +544 -0
  80. package/packages/daemon/src/index.ts +6 -0
  81. package/packages/daemon/src/main.ts +525 -0
  82. package/packages/daemon/src/notifications/pushNotifier.ts +369 -0
  83. package/packages/daemon/src/providers/codexAppServerScaffold.ts +49 -0
  84. package/packages/daemon/src/providers/registry.ts +111 -0
  85. package/packages/daemon/src/providers/types.ts +82 -0
  86. package/packages/daemon/src/sessions/auditLogger.ts +103 -0
  87. package/packages/daemon/src/sessions/policyEngine.ts +165 -0
  88. package/packages/daemon/src/sessions/policyMatcher.ts +114 -0
  89. package/packages/daemon/src/sessions/policyTypes.ts +94 -0
  90. package/packages/daemon/src/sessions/ptyProcess.ts +141 -0
  91. package/packages/daemon/src/sessions/sessionManager.ts +1770 -0
  92. package/packages/daemon/src/sessions/tmuxSession.ts +1073 -0
  93. package/packages/daemon/src/sessions/transcriptManager.ts +110 -0
  94. package/packages/daemon/src/sessions/transcriptParser.ts +149 -0
  95. package/packages/daemon/src/sessions/transcriptTailer.ts +214 -0
  96. package/packages/daemon/src/tracking/tokenTracker.ts +168 -0
  97. package/packages/daemon/src/workflows/contextManager.ts +83 -0
  98. package/packages/daemon/src/workflows/index.ts +31 -0
  99. package/packages/daemon/src/workflows/resultExtractor.ts +118 -0
  100. package/packages/daemon/src/workflows/waitConditionPoller.ts +131 -0
  101. package/packages/daemon/src/workflows/workflowParser.ts +217 -0
  102. package/packages/daemon/src/workflows/workflowRunner.ts +969 -0
  103. package/packages/daemon/src/workflows/workflowTypes.ts +188 -0
  104. package/packages/daemon/src/workflows/workflowValidator.ts +533 -0
  105. package/packages/providers/claude-code/package.json +11 -0
  106. package/packages/providers/claude-code/src/index.ts +7 -0
  107. package/packages/providers/claude-code/src/overlaySettings.ts +198 -0
  108. package/packages/providers/claude-code/src/provider.ts +311 -0
  109. package/packages/web/dist/android-chrome-192x192.png +0 -0
  110. package/packages/web/dist/android-chrome-512x512.png +0 -0
  111. package/packages/web/dist/apple-touch-icon.png +0 -0
  112. package/packages/web/dist/assets/AnalyticsPage-BIopKWRf.js +17 -0
  113. package/packages/web/dist/assets/PoliciesPage-CjdLN3dl.js +11 -0
  114. package/packages/web/dist/assets/SessionDetailPage-BtSA0V0M.js +179 -0
  115. package/packages/web/dist/assets/SettingsPage-Dbbz4Ca5.js +37 -0
  116. package/packages/web/dist/assets/WorkflowsPage-Dv6f3GgU.js +1 -0
  117. package/packages/web/dist/assets/chart-vendor-DlOHLaCG.js +49 -0
  118. package/packages/web/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
  119. package/packages/web/dist/assets/css.worker-BvV5MPou.js +93 -0
  120. package/packages/web/dist/assets/editor.worker-CKy7Pnvo.js +26 -0
  121. package/packages/web/dist/assets/html.worker-BLJhxQJQ.js +470 -0
  122. package/packages/web/dist/assets/index-BbdhRfr2.css +1 -0
  123. package/packages/web/dist/assets/index-hgphORiw.js +204 -0
  124. package/packages/web/dist/assets/json.worker-usMZ-FED.js +58 -0
  125. package/packages/web/dist/assets/monaco-core-B_19GPAS.css +1 -0
  126. package/packages/web/dist/assets/monaco-core-DQ5Mk8AK.js +1234 -0
  127. package/packages/web/dist/assets/monaco-react-DfZNWvtW.js +11 -0
  128. package/packages/web/dist/assets/monacoSetup-DvBj52bT.js +1 -0
  129. package/packages/web/dist/assets/pencil-Dbczxz59.js +11 -0
  130. package/packages/web/dist/assets/react-vendor-B5MgMUHH.js +136 -0
  131. package/packages/web/dist/assets/refresh-cw-B0MGsYPL.js +6 -0
  132. package/packages/web/dist/assets/tabs-C8LsWiR5.js +1 -0
  133. package/packages/web/dist/assets/terminal-vendor-Cs8KPbV3.js +9 -0
  134. package/packages/web/dist/assets/terminal-vendor-LcAfv9l9.css +32 -0
  135. package/packages/web/dist/assets/trash-2-Btlg0d4l.js +6 -0
  136. package/packages/web/dist/assets/ts.worker-DGHjMaqB.js +67731 -0
  137. package/packages/web/dist/favicon.ico +0 -0
  138. package/packages/web/dist/icon.svg +1 -0
  139. package/packages/web/dist/index.html +29 -0
  140. package/packages/web/dist/manifest.json +29 -0
  141. package/packages/web/dist/og-image.png +0 -0
  142. package/packages/web/dist/originals/android-chrome-192x192.png +0 -0
  143. package/packages/web/dist/originals/android-chrome-512x512.png +0 -0
  144. package/packages/web/dist/originals/apple-touch-icon.png +0 -0
  145. package/packages/web/dist/originals/favicon.ico +0 -0
  146. package/packages/web/dist/piper.svg +1 -0
  147. package/packages/web/dist/sounds/codepiper-soft-chime.wav +0 -0
  148. package/packages/web/dist/sw.js +257 -0
  149. package/scripts/postinstall-link-workspaces.mjs +58 -0
@@ -0,0 +1,419 @@
1
+ import { readErrorJson, readJson, responseErrorMessage } from "../lib/api";
2
+ import { getRequiredValue } from "../lib/args";
3
+
4
+ export interface LogsOptions {
5
+ sessionId: string;
6
+ socket: string;
7
+ follow?: boolean;
8
+ tail: number;
9
+ since?: string;
10
+ format: "pretty" | "json" | "text";
11
+ source?: string; // Filter by source: hook, transcript, pty, statusline
12
+ type?: string; // Filter by event type
13
+ showMessages?: boolean; // Extract and show only assistant/user messages
14
+ }
15
+
16
+ interface Event {
17
+ id: number;
18
+ sessionId: string;
19
+ timestamp: string; // ISO 8601 timestamp from database
20
+ source: string;
21
+ type: string;
22
+ payload: any;
23
+ }
24
+
25
+ const VALID_FORMATS = ["pretty", "json", "text"];
26
+
27
+ export function parseLogsOptions(args: string[]): LogsOptions {
28
+ let sessionId: string | undefined;
29
+ let socket = "/tmp/codepiper.sock";
30
+ let follow = false;
31
+ let tail = 100;
32
+ let since: string | undefined;
33
+ let format: "pretty" | "json" | "text" = "pretty";
34
+ let source: string | undefined;
35
+ let type: string | undefined;
36
+ let showMessages = false;
37
+
38
+ for (let i = 0; i < args.length; i++) {
39
+ const arg = args[i];
40
+ if (arg === undefined) {
41
+ continue;
42
+ }
43
+
44
+ if (arg === "--socket" || arg === "-s") {
45
+ socket = getRequiredValue(args, i, arg);
46
+ i++;
47
+ } else if (arg === "--follow" || arg === "-f") {
48
+ follow = true;
49
+ } else if (arg === "--tail" || arg === "-n") {
50
+ const tailValue = getRequiredValue(args, i, arg);
51
+ tail = parseInt(tailValue, 10);
52
+ i++;
53
+ } else if (arg === "--since") {
54
+ since = getRequiredValue(args, i, arg);
55
+ i++;
56
+ } else if (arg === "--format") {
57
+ const formatValue = getRequiredValue(args, i, arg);
58
+ i++;
59
+ if (!VALID_FORMATS.includes(formatValue)) {
60
+ throw new Error(
61
+ `Invalid format: ${formatValue}. Valid options: ${VALID_FORMATS.join(", ")}`
62
+ );
63
+ }
64
+ format = formatValue as "pretty" | "json" | "text";
65
+ } else if (arg === "--source") {
66
+ source = getRequiredValue(args, i, arg);
67
+ i++;
68
+ } else if (arg === "--type") {
69
+ type = getRequiredValue(args, i, arg);
70
+ i++;
71
+ } else if (arg === "--messages" || arg === "-m") {
72
+ showMessages = true;
73
+ } else if (!(arg.startsWith("-") || sessionId)) {
74
+ sessionId = arg;
75
+ }
76
+ }
77
+
78
+ if (!sessionId) {
79
+ throw new Error("session-id is required");
80
+ }
81
+
82
+ const options: LogsOptions = {
83
+ sessionId,
84
+ socket,
85
+ follow,
86
+ tail,
87
+ format,
88
+ showMessages,
89
+ };
90
+ if (since !== undefined) {
91
+ options.since = since;
92
+ }
93
+ if (source !== undefined) {
94
+ options.source = source;
95
+ }
96
+ if (type !== undefined) {
97
+ options.type = type;
98
+ }
99
+
100
+ return options;
101
+ }
102
+
103
+ export async function fetchLogs(options: LogsOptions): Promise<Event[]> {
104
+ const params = new URLSearchParams();
105
+ if (options.since) {
106
+ params.append("since", options.since);
107
+ }
108
+ if (options.tail) {
109
+ params.append("limit", options.tail.toString());
110
+ }
111
+ if (options.source) {
112
+ params.append("source", options.source);
113
+ }
114
+ if (options.type) {
115
+ params.append("type", options.type);
116
+ }
117
+
118
+ const queryString = params.toString();
119
+ const url = `http://localhost/sessions/${options.sessionId}/events${queryString ? `?${queryString}` : ""}`;
120
+
121
+ try {
122
+ const response = await fetch(url, {
123
+ unix: options.socket,
124
+ method: "GET",
125
+ });
126
+
127
+ if (!response.ok) {
128
+ const errorData = await readErrorJson(response);
129
+ throw new Error(responseErrorMessage(response, errorData));
130
+ }
131
+
132
+ const data = await readJson<{ events?: Event[] }>(response);
133
+ return data.events || [];
134
+ } catch (error: any) {
135
+ if (error.code === "ENOENT" || error.message?.includes("ENOENT")) {
136
+ throw new Error(`Failed to connect to daemon at ${options.socket}. Is the daemon running?`);
137
+ }
138
+ throw error;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Extract assistant/user messages from transcript events
144
+ */
145
+ function extractMessages(
146
+ events: Event[]
147
+ ): Array<{ role: string; content: string; timestamp: string }> {
148
+ const messages: Array<{ role: string; content: string; timestamp: string }> = [];
149
+
150
+ for (const event of events) {
151
+ if (event.source !== "transcript") continue;
152
+
153
+ // User messages
154
+ if (event.type === "user" && event.payload?.message?.content) {
155
+ messages.push({
156
+ role: "user",
157
+ content: event.payload.message.content,
158
+ timestamp: event.timestamp,
159
+ });
160
+ }
161
+
162
+ // Assistant messages
163
+ if (event.type === "assistant" && event.payload?.message?.content) {
164
+ const content = event.payload.message.content;
165
+ let text = "";
166
+
167
+ if (Array.isArray(content)) {
168
+ // Extract text from content array (skip thinking blocks)
169
+ for (const block of content) {
170
+ if (block.type === "text") {
171
+ text += block.text;
172
+ }
173
+ }
174
+ } else if (typeof content === "string") {
175
+ text = content;
176
+ }
177
+
178
+ if (text.trim()) {
179
+ messages.push({
180
+ role: "assistant",
181
+ content: text,
182
+ timestamp: event.timestamp,
183
+ });
184
+ }
185
+ }
186
+ }
187
+
188
+ return messages;
189
+ }
190
+
191
+ /**
192
+ * Format events as conversation text (user/assistant exchanges only)
193
+ */
194
+ function formatText(events: Event[]): void {
195
+ const messages = extractMessages(events);
196
+
197
+ if (messages.length === 0) {
198
+ console.log("No messages found.");
199
+ return;
200
+ }
201
+
202
+ for (const msg of messages) {
203
+ const timestamp = new Date(msg.timestamp).toLocaleString();
204
+ const role = msg.role === "user" ? "User" : "Assistant";
205
+ console.log(`\n[${timestamp}] ${role}:`);
206
+ console.log(msg.content);
207
+ console.log("─".repeat(80));
208
+ }
209
+
210
+ console.log(`\nTotal: ${messages.length} message(s)`);
211
+ }
212
+
213
+ /**
214
+ * Format events as pretty console output
215
+ */
216
+ function formatPretty(events: Event[], showMessages = false): void {
217
+ if (events.length === 0) {
218
+ console.log("No events found.");
219
+ return;
220
+ }
221
+
222
+ // If --messages flag is set, show conversation flow
223
+ if (showMessages) {
224
+ formatText(events);
225
+ return;
226
+ }
227
+
228
+ for (const event of events) {
229
+ const timestamp = new Date(event.timestamp).toLocaleString();
230
+ const source = event.source.padEnd(10);
231
+ const type = event.type.padEnd(20);
232
+
233
+ console.log(`${timestamp} [${source}] ${type}`);
234
+
235
+ // For assistant messages, extract and show just the text
236
+ if (event.type === "assistant" && event.payload?.message?.content) {
237
+ const content = event.payload.message.content;
238
+ let text = "";
239
+
240
+ if (Array.isArray(content)) {
241
+ for (const block of content) {
242
+ if (block.type === "text") {
243
+ text += block.text;
244
+ }
245
+ }
246
+ } else if (typeof content === "string") {
247
+ text = content;
248
+ }
249
+
250
+ if (text.trim()) {
251
+ // Truncate if too long
252
+ const preview = text.length > 200 ? `${text.substring(0, 200)}...` : text;
253
+ console.log(` 📝 ${preview}`);
254
+ }
255
+ }
256
+ // For user messages, show the content
257
+ else if (event.type === "user" && event.payload?.message?.content) {
258
+ console.log(` 👤 ${event.payload.message.content}`);
259
+ }
260
+ // For hook events, show key fields
261
+ else if (event.source === "hook") {
262
+ const hookEvent = event.payload.hook_event_name || event.payload.event;
263
+ if (hookEvent) {
264
+ console.log(` 🪝 Event: ${hookEvent}`);
265
+ }
266
+ }
267
+ // For other events, show compact payload
268
+ else {
269
+ const payloadStr = JSON.stringify(event.payload);
270
+ if (payloadStr.length < 100 && payloadStr !== "{}") {
271
+ console.log(` ${payloadStr}`);
272
+ } else if (payloadStr !== "{}") {
273
+ console.log(` ${JSON.stringify(event.payload, null, 2)}`);
274
+ }
275
+ }
276
+
277
+ console.log(); // blank line between events
278
+ }
279
+
280
+ console.log(`Total: ${events.length} event(s)`);
281
+ }
282
+
283
+ export async function runLogsCommand(args: string[]): Promise<void> {
284
+ const options = parseLogsOptions(args);
285
+
286
+ if (options.follow) {
287
+ console.log(`Following logs for session ${options.sessionId}...`);
288
+ console.log("Press Ctrl+C to stop.\n");
289
+
290
+ await followLogs(options);
291
+ return;
292
+ }
293
+
294
+ const events = await fetchLogs(options);
295
+
296
+ if (options.format === "json") {
297
+ console.log(JSON.stringify(events, null, 2));
298
+ } else if (options.format === "text") {
299
+ formatText(events);
300
+ } else {
301
+ formatPretty(events, options.showMessages);
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Follow logs in near real-time by polling the daemon over Unix socket.
307
+ * This avoids hardcoded WS ports and works regardless of daemon auth mode.
308
+ */
309
+ async function followLogs(options: LogsOptions): Promise<void> {
310
+ let stopping = false;
311
+ let since = options.since;
312
+ const pollLimit = Math.max(options.tail, 200);
313
+
314
+ const sigintHandler = () => {
315
+ if (!stopping) {
316
+ stopping = true;
317
+ console.log("\nStopping...");
318
+ }
319
+ };
320
+
321
+ process.on("SIGINT", sigintHandler);
322
+
323
+ try {
324
+ while (!stopping) {
325
+ const pollOptions: LogsOptions = {
326
+ ...options,
327
+ tail: pollLimit,
328
+ };
329
+ if (since !== undefined) {
330
+ pollOptions.since = since;
331
+ }
332
+ const events = await fetchLogs(pollOptions);
333
+
334
+ for (const event of events) {
335
+ formatSingleEvent(event, options);
336
+ if (typeof event.id === "number") {
337
+ since = String(event.id);
338
+ }
339
+ }
340
+
341
+ if (stopping) {
342
+ break;
343
+ }
344
+ await new Promise((resolve) => setTimeout(resolve, 1000));
345
+ }
346
+ } finally {
347
+ process.off("SIGINT", sigintHandler);
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Format and print a single event (for follow mode)
353
+ */
354
+ function formatSingleEvent(event: Event, options: LogsOptions): void {
355
+ if (options.format === "json") {
356
+ console.log(JSON.stringify(event, null, 2));
357
+ return;
358
+ }
359
+
360
+ if (options.format === "text") {
361
+ const messages = extractMessages([event]);
362
+ if (messages.length > 0) {
363
+ const msg = messages[0];
364
+ if (!msg) {
365
+ return;
366
+ }
367
+ const timestamp = new Date(msg.timestamp).toLocaleString();
368
+ const role = msg.role === "user" ? "User" : "Assistant";
369
+ console.log(`\n[${timestamp}] ${role}:`);
370
+ console.log(msg.content);
371
+ console.log("─".repeat(80));
372
+ }
373
+ return;
374
+ }
375
+
376
+ // Pretty format (default)
377
+ const timestamp = new Date(event.timestamp).toLocaleString();
378
+ const source = event.source.padEnd(10);
379
+ const type = event.type.padEnd(20);
380
+
381
+ console.log(`${timestamp} [${source}] ${type}`);
382
+
383
+ // Show event details (same as formatPretty logic)
384
+ if (event.type === "assistant" && event.payload?.message?.content) {
385
+ const content = event.payload.message.content;
386
+ let text = "";
387
+
388
+ if (Array.isArray(content)) {
389
+ for (const block of content) {
390
+ if (block.type === "text") {
391
+ text += block.text;
392
+ }
393
+ }
394
+ } else if (typeof content === "string") {
395
+ text = content;
396
+ }
397
+
398
+ if (text.trim()) {
399
+ const preview = text.length > 200 ? `${text.substring(0, 200)}...` : text;
400
+ console.log(` 📝 ${preview}`);
401
+ }
402
+ } else if (event.type === "user" && event.payload?.message?.content) {
403
+ console.log(` 👤 ${event.payload.message.content}`);
404
+ } else if (event.source === "hook") {
405
+ const hookEvent = event.payload.hook_event_name || event.payload.event;
406
+ if (hookEvent) {
407
+ console.log(` 🪝 Event: ${hookEvent}`);
408
+ }
409
+ } else {
410
+ const payloadStr = JSON.stringify(event.payload);
411
+ if (payloadStr.length < 100 && payloadStr !== "{}") {
412
+ console.log(` ${payloadStr}`);
413
+ } else if (payloadStr !== "{}") {
414
+ console.log(` ${JSON.stringify(event.payload, null, 2)}`);
415
+ }
416
+ }
417
+
418
+ console.log(); // blank line
419
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Model command - switch model for an active session or get current model
3
+ */
4
+
5
+ import { readJson } from "../lib/api";
6
+ import { getRequiredValue } from "../lib/args";
7
+
8
+ /**
9
+ * Available Claude models
10
+ */
11
+ const AVAILABLE_MODELS = [
12
+ "sonnet",
13
+ "opus",
14
+ "haiku",
15
+ "opusplan",
16
+ "claude-sonnet-4-5",
17
+ "claude-opus-4-6",
18
+ "claude-haiku-4-5",
19
+ ] as const;
20
+
21
+ /**
22
+ * Parse command line arguments
23
+ */
24
+ function parseArgs(args: string[]): {
25
+ sessionId?: string;
26
+ model?: string;
27
+ socketPath: string;
28
+ action: "get" | "set";
29
+ } {
30
+ const result: {
31
+ sessionId?: string;
32
+ model?: string;
33
+ socketPath: string;
34
+ action: "get" | "set";
35
+ } = {
36
+ socketPath:
37
+ process.env.CODEPIPER_SOCKET || process.env.CODEPIPER_UNIX_SOCK || "/tmp/codepiper.sock",
38
+ action: "get",
39
+ };
40
+
41
+ let i = 0;
42
+
43
+ // First positional arg: session ID
44
+ const sessionIdArg = args[i];
45
+ if (sessionIdArg !== undefined && !sessionIdArg.startsWith("-")) {
46
+ result.sessionId = sessionIdArg;
47
+ i++;
48
+ }
49
+
50
+ // Second positional arg (if present): model name
51
+ const modelArg = args[i];
52
+ if (modelArg !== undefined && !modelArg.startsWith("-")) {
53
+ result.model = modelArg;
54
+ result.action = "set";
55
+ i++;
56
+ }
57
+
58
+ // Parse flags
59
+ while (i < args.length) {
60
+ const arg = args[i];
61
+ if (arg === undefined) {
62
+ i++;
63
+ continue;
64
+ }
65
+
66
+ if (arg === "-s" || arg === "--socket") {
67
+ result.socketPath = getRequiredValue(args, i, arg);
68
+ i += 2;
69
+ } else {
70
+ i++;
71
+ }
72
+ }
73
+
74
+ return result;
75
+ }
76
+
77
+ /**
78
+ * Model command entry point
79
+ */
80
+ export async function runModelCommand(args: string[]): Promise<void> {
81
+ const parsed = parseArgs(args);
82
+
83
+ if (!parsed.sessionId) {
84
+ console.error("Error: Missing required argument: <session-id>");
85
+ console.error("\nUsage: codepiper model <session-id> [model]");
86
+ console.error(`\nAvailable models: ${AVAILABLE_MODELS.join(", ")}`);
87
+ console.error("\nExamples:");
88
+ console.error(" codepiper model abc123def # Get current model");
89
+ console.error(" codepiper model abc123def sonnet # Switch to sonnet");
90
+ console.error(" codepiper model abc123def claude-opus-4-6 # Switch to opus");
91
+ process.exit(1);
92
+ }
93
+
94
+ if (parsed.action === "get") {
95
+ await getModel(parsed.sessionId, parsed.socketPath);
96
+ } else {
97
+ if (!parsed.model) {
98
+ console.error("Error: Missing required argument: <model>");
99
+ console.error(`\nAvailable models: ${AVAILABLE_MODELS.join(", ")}`);
100
+ process.exit(1);
101
+ }
102
+
103
+ // Keep guidance for Claude defaults while allowing provider-specific model IDs.
104
+ if (!AVAILABLE_MODELS.includes(parsed.model as any)) {
105
+ console.warn(
106
+ `Warning: '${parsed.model}' is not in the default Claude model list; sending as-is.`
107
+ );
108
+ }
109
+
110
+ await switchModel(parsed.sessionId, parsed.model, parsed.socketPath);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Get current model for a session
116
+ */
117
+ async function getModel(sessionId: string, socketPath: string): Promise<void> {
118
+ try {
119
+ const response = await fetch(`http://localhost/sessions/${sessionId}/model`, {
120
+ method: "GET",
121
+ unix: socketPath,
122
+ });
123
+
124
+ if (!response.ok) {
125
+ const error = await response.text();
126
+ console.error(`Failed to get model: ${error}`);
127
+ process.exit(1);
128
+ }
129
+
130
+ const data = await readJson<{
131
+ model?: string;
132
+ provider?: string;
133
+ supportsModelSwitch?: boolean;
134
+ }>(response);
135
+ console.log(`Current model: ${data.model || "unknown"}`);
136
+ if (data.supportsModelSwitch === false) {
137
+ console.log(
138
+ `Model switching is not supported for provider ${data.provider ?? "unknown"} in this session.`
139
+ );
140
+ }
141
+ } catch (error: any) {
142
+ console.error(`Error getting model: ${error.message}`);
143
+ process.exit(1);
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Switch model for a session
149
+ */
150
+ async function switchModel(sessionId: string, model: string, socketPath: string): Promise<void> {
151
+ try {
152
+ const response = await fetch(`http://localhost/sessions/${sessionId}/model`, {
153
+ method: "PUT",
154
+ headers: {
155
+ "Content-Type": "application/json",
156
+ },
157
+ body: JSON.stringify({ model }),
158
+ unix: socketPath,
159
+ });
160
+
161
+ if (!response.ok) {
162
+ const error = await response.text();
163
+ console.error(`Failed to switch model: ${error}`);
164
+ process.exit(1);
165
+ }
166
+
167
+ console.log(`✓ Switched model to ${model} for session ${sessionId}`);
168
+ } catch (error: any) {
169
+ console.error(`Error switching model: ${error.message}`);
170
+ process.exit(1);
171
+ }
172
+ }