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.
- package/.env.example +28 -0
- package/CHANGELOG.md +10 -0
- package/LEGAL_NOTICE.md +39 -0
- package/LICENSE +21 -0
- package/README.md +524 -0
- package/package.json +90 -0
- package/packages/cli/package.json +13 -0
- package/packages/cli/src/commands/analytics.ts +157 -0
- package/packages/cli/src/commands/attach.ts +299 -0
- package/packages/cli/src/commands/audit.ts +50 -0
- package/packages/cli/src/commands/auth.ts +261 -0
- package/packages/cli/src/commands/daemon.ts +162 -0
- package/packages/cli/src/commands/doctor.ts +303 -0
- package/packages/cli/src/commands/env-set.ts +162 -0
- package/packages/cli/src/commands/hook-forward.ts +268 -0
- package/packages/cli/src/commands/keys.ts +77 -0
- package/packages/cli/src/commands/kill.ts +19 -0
- package/packages/cli/src/commands/logs.ts +419 -0
- package/packages/cli/src/commands/model.ts +172 -0
- package/packages/cli/src/commands/policy-set.ts +185 -0
- package/packages/cli/src/commands/policy.ts +227 -0
- package/packages/cli/src/commands/providers.ts +114 -0
- package/packages/cli/src/commands/resize.ts +34 -0
- package/packages/cli/src/commands/send.ts +184 -0
- package/packages/cli/src/commands/sessions.ts +202 -0
- package/packages/cli/src/commands/slash.ts +92 -0
- package/packages/cli/src/commands/start.ts +243 -0
- package/packages/cli/src/commands/stop.ts +19 -0
- package/packages/cli/src/commands/tail.ts +137 -0
- package/packages/cli/src/commands/workflow.ts +786 -0
- package/packages/cli/src/commands/workspace.ts +127 -0
- package/packages/cli/src/lib/api.ts +78 -0
- package/packages/cli/src/lib/args.ts +72 -0
- package/packages/cli/src/lib/format.ts +93 -0
- package/packages/cli/src/main.ts +563 -0
- package/packages/core/package.json +7 -0
- package/packages/core/src/config.ts +30 -0
- package/packages/core/src/errors.ts +38 -0
- package/packages/core/src/eventBus.ts +56 -0
- package/packages/core/src/eventBusAdapter.ts +143 -0
- package/packages/core/src/index.ts +10 -0
- package/packages/core/src/sqliteEventBus.ts +336 -0
- package/packages/core/src/types.ts +63 -0
- package/packages/daemon/package.json +11 -0
- package/packages/daemon/src/api/analyticsRoutes.ts +343 -0
- package/packages/daemon/src/api/authRoutes.ts +344 -0
- package/packages/daemon/src/api/bodyLimit.ts +133 -0
- package/packages/daemon/src/api/envSetRoutes.ts +170 -0
- package/packages/daemon/src/api/gitRoutes.ts +409 -0
- package/packages/daemon/src/api/hooks.ts +588 -0
- package/packages/daemon/src/api/inputPolicy.ts +249 -0
- package/packages/daemon/src/api/notificationRoutes.ts +532 -0
- package/packages/daemon/src/api/policyRoutes.ts +234 -0
- package/packages/daemon/src/api/policySetRoutes.ts +445 -0
- package/packages/daemon/src/api/routeUtils.ts +28 -0
- package/packages/daemon/src/api/routes.ts +1004 -0
- package/packages/daemon/src/api/server.ts +1388 -0
- package/packages/daemon/src/api/settingsRoutes.ts +367 -0
- package/packages/daemon/src/api/sqliteErrors.ts +47 -0
- package/packages/daemon/src/api/stt.ts +143 -0
- package/packages/daemon/src/api/terminalRoutes.ts +200 -0
- package/packages/daemon/src/api/validation.ts +287 -0
- package/packages/daemon/src/api/validationRoutes.ts +174 -0
- package/packages/daemon/src/api/workflowRoutes.ts +567 -0
- package/packages/daemon/src/api/workspaceRoutes.ts +151 -0
- package/packages/daemon/src/api/ws.ts +1588 -0
- package/packages/daemon/src/auth/apiRateLimiter.ts +73 -0
- package/packages/daemon/src/auth/authMiddleware.ts +305 -0
- package/packages/daemon/src/auth/authService.ts +496 -0
- package/packages/daemon/src/auth/rateLimiter.ts +137 -0
- package/packages/daemon/src/config/pricing.ts +79 -0
- package/packages/daemon/src/crypto/encryption.ts +196 -0
- package/packages/daemon/src/db/db.ts +2745 -0
- package/packages/daemon/src/db/index.ts +16 -0
- package/packages/daemon/src/db/migrations.ts +182 -0
- package/packages/daemon/src/db/policyDb.ts +349 -0
- package/packages/daemon/src/db/schema.sql +408 -0
- package/packages/daemon/src/db/workflowDb.ts +464 -0
- package/packages/daemon/src/git/gitUtils.ts +544 -0
- package/packages/daemon/src/index.ts +6 -0
- package/packages/daemon/src/main.ts +525 -0
- package/packages/daemon/src/notifications/pushNotifier.ts +369 -0
- package/packages/daemon/src/providers/codexAppServerScaffold.ts +49 -0
- package/packages/daemon/src/providers/registry.ts +111 -0
- package/packages/daemon/src/providers/types.ts +82 -0
- package/packages/daemon/src/sessions/auditLogger.ts +103 -0
- package/packages/daemon/src/sessions/policyEngine.ts +165 -0
- package/packages/daemon/src/sessions/policyMatcher.ts +114 -0
- package/packages/daemon/src/sessions/policyTypes.ts +94 -0
- package/packages/daemon/src/sessions/ptyProcess.ts +141 -0
- package/packages/daemon/src/sessions/sessionManager.ts +1770 -0
- package/packages/daemon/src/sessions/tmuxSession.ts +1073 -0
- package/packages/daemon/src/sessions/transcriptManager.ts +110 -0
- package/packages/daemon/src/sessions/transcriptParser.ts +149 -0
- package/packages/daemon/src/sessions/transcriptTailer.ts +214 -0
- package/packages/daemon/src/tracking/tokenTracker.ts +168 -0
- package/packages/daemon/src/workflows/contextManager.ts +83 -0
- package/packages/daemon/src/workflows/index.ts +31 -0
- package/packages/daemon/src/workflows/resultExtractor.ts +118 -0
- package/packages/daemon/src/workflows/waitConditionPoller.ts +131 -0
- package/packages/daemon/src/workflows/workflowParser.ts +217 -0
- package/packages/daemon/src/workflows/workflowRunner.ts +969 -0
- package/packages/daemon/src/workflows/workflowTypes.ts +188 -0
- package/packages/daemon/src/workflows/workflowValidator.ts +533 -0
- package/packages/providers/claude-code/package.json +11 -0
- package/packages/providers/claude-code/src/index.ts +7 -0
- package/packages/providers/claude-code/src/overlaySettings.ts +198 -0
- package/packages/providers/claude-code/src/provider.ts +311 -0
- package/packages/web/dist/android-chrome-192x192.png +0 -0
- package/packages/web/dist/android-chrome-512x512.png +0 -0
- package/packages/web/dist/apple-touch-icon.png +0 -0
- package/packages/web/dist/assets/AnalyticsPage-BIopKWRf.js +17 -0
- package/packages/web/dist/assets/PoliciesPage-CjdLN3dl.js +11 -0
- package/packages/web/dist/assets/SessionDetailPage-BtSA0V0M.js +179 -0
- package/packages/web/dist/assets/SettingsPage-Dbbz4Ca5.js +37 -0
- package/packages/web/dist/assets/WorkflowsPage-Dv6f3GgU.js +1 -0
- package/packages/web/dist/assets/chart-vendor-DlOHLaCG.js +49 -0
- package/packages/web/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/packages/web/dist/assets/css.worker-BvV5MPou.js +93 -0
- package/packages/web/dist/assets/editor.worker-CKy7Pnvo.js +26 -0
- package/packages/web/dist/assets/html.worker-BLJhxQJQ.js +470 -0
- package/packages/web/dist/assets/index-BbdhRfr2.css +1 -0
- package/packages/web/dist/assets/index-hgphORiw.js +204 -0
- package/packages/web/dist/assets/json.worker-usMZ-FED.js +58 -0
- package/packages/web/dist/assets/monaco-core-B_19GPAS.css +1 -0
- package/packages/web/dist/assets/monaco-core-DQ5Mk8AK.js +1234 -0
- package/packages/web/dist/assets/monaco-react-DfZNWvtW.js +11 -0
- package/packages/web/dist/assets/monacoSetup-DvBj52bT.js +1 -0
- package/packages/web/dist/assets/pencil-Dbczxz59.js +11 -0
- package/packages/web/dist/assets/react-vendor-B5MgMUHH.js +136 -0
- package/packages/web/dist/assets/refresh-cw-B0MGsYPL.js +6 -0
- package/packages/web/dist/assets/tabs-C8LsWiR5.js +1 -0
- package/packages/web/dist/assets/terminal-vendor-Cs8KPbV3.js +9 -0
- package/packages/web/dist/assets/terminal-vendor-LcAfv9l9.css +32 -0
- package/packages/web/dist/assets/trash-2-Btlg0d4l.js +6 -0
- package/packages/web/dist/assets/ts.worker-DGHjMaqB.js +67731 -0
- package/packages/web/dist/favicon.ico +0 -0
- package/packages/web/dist/icon.svg +1 -0
- package/packages/web/dist/index.html +29 -0
- package/packages/web/dist/manifest.json +29 -0
- package/packages/web/dist/og-image.png +0 -0
- package/packages/web/dist/originals/android-chrome-192x192.png +0 -0
- package/packages/web/dist/originals/android-chrome-512x512.png +0 -0
- package/packages/web/dist/originals/apple-touch-icon.png +0 -0
- package/packages/web/dist/originals/favicon.ico +0 -0
- package/packages/web/dist/piper.svg +1 -0
- package/packages/web/dist/sounds/codepiper-soft-chime.wav +0 -0
- package/packages/web/dist/sw.js +257 -0
- 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
|
+
}
|