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,184 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { readErrorJson, readJson, responseErrorMessage } from "../lib/api";
5
+ import { getRequiredValue } from "../lib/args";
6
+
7
+ export interface SendOptions {
8
+ sessionId: string;
9
+ text: string;
10
+ socket: string;
11
+ newline: boolean;
12
+ image?: string;
13
+ }
14
+
15
+ const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB
16
+
17
+ export function parseSendOptions(args: string[]): SendOptions {
18
+ let sessionId: string | undefined;
19
+ let socket = "/tmp/codepiper.sock";
20
+ let newline = true;
21
+ let image: string | undefined;
22
+ const textParts: string[] = [];
23
+
24
+ for (let i = 0; i < args.length; i++) {
25
+ const arg = args[i];
26
+ if (arg === undefined) {
27
+ continue;
28
+ }
29
+
30
+ if (arg === "--socket" || arg === "-s") {
31
+ socket = getRequiredValue(args, i, arg);
32
+ i++;
33
+ } else if (arg === "--newline" || arg === "-n") {
34
+ newline = true;
35
+ } else if (arg === "--no-newline") {
36
+ newline = false;
37
+ } else if (arg === "--image" || arg === "-i") {
38
+ image = getRequiredValue(args, i, arg);
39
+ i++;
40
+ } else if (!arg.startsWith("-")) {
41
+ if (!sessionId) {
42
+ sessionId = arg;
43
+ } else {
44
+ textParts.push(arg);
45
+ }
46
+ }
47
+ }
48
+
49
+ if (!sessionId) {
50
+ throw new Error("session-id is required");
51
+ }
52
+
53
+ const text = textParts.join(" ");
54
+ if (!(text || image)) {
55
+ throw new Error("text or --image is required");
56
+ }
57
+
58
+ const options: SendOptions = {
59
+ sessionId,
60
+ text,
61
+ socket,
62
+ newline,
63
+ };
64
+ if (image !== undefined) {
65
+ options.image = image;
66
+ }
67
+
68
+ return options;
69
+ }
70
+
71
+ export async function uploadImage(
72
+ sessionId: string,
73
+ filePath: string,
74
+ socket: string
75
+ ): Promise<string> {
76
+ const resolved = path.resolve(filePath);
77
+ const stat = fs.statSync(resolved);
78
+ if (stat.size > MAX_IMAGE_SIZE) {
79
+ throw new Error(`Image too large (${(stat.size / 1024 / 1024).toFixed(1)}MB, max 10MB)`);
80
+ }
81
+
82
+ const file = Bun.file(resolved);
83
+ const formData = new FormData();
84
+ const blob = new Blob([await file.arrayBuffer()], {
85
+ type: file.type || "application/octet-stream",
86
+ });
87
+ formData.append("image", blob, path.basename(resolved));
88
+
89
+ const response = await fetch(`http://localhost/sessions/${sessionId}/upload-image`, {
90
+ unix: socket,
91
+ method: "POST",
92
+ body: formData,
93
+ });
94
+
95
+ if (!response.ok) {
96
+ const errorData = await readErrorJson(response);
97
+ throw new Error(responseErrorMessage(response, errorData));
98
+ }
99
+
100
+ const result = await readJson<{ path: string }>(response);
101
+ return result.path;
102
+ }
103
+
104
+ export async function downloadUrl(url: string): Promise<string> {
105
+ const response = await fetch(url);
106
+ if (!response.ok) {
107
+ throw new Error(`Failed to download ${url}: HTTP ${response.status}`);
108
+ }
109
+
110
+ const contentType = response.headers.get("content-type") || "";
111
+ let ext = path.extname(new URL(url).pathname) || ".png";
112
+ if (contentType.includes("jpeg") || contentType.includes("jpg")) ext = ".jpg";
113
+ else if (contentType.includes("png")) ext = ".png";
114
+ else if (contentType.includes("gif")) ext = ".gif";
115
+ else if (contentType.includes("webp")) ext = ".webp";
116
+
117
+ const tmpPath = path.join(os.tmpdir(), `codepiper-dl-${Date.now()}${ext}`);
118
+ await Bun.write(tmpPath, response);
119
+ return tmpPath;
120
+ }
121
+
122
+ export async function sendText(options: SendOptions): Promise<void> {
123
+ const payload = {
124
+ text: options.text,
125
+ newline: options.newline,
126
+ };
127
+
128
+ try {
129
+ const response = await fetch(`http://localhost/sessions/${options.sessionId}/send`, {
130
+ unix: options.socket,
131
+ method: "POST",
132
+ headers: {
133
+ "Content-Type": "application/json",
134
+ },
135
+ body: JSON.stringify(payload),
136
+ });
137
+
138
+ if (!response.ok) {
139
+ const errorData = await readErrorJson(response);
140
+ throw new Error(responseErrorMessage(response, errorData));
141
+ }
142
+ } catch (error: any) {
143
+ if (error.code === "ENOENT" || error.message?.includes("ENOENT")) {
144
+ throw new Error(`Failed to connect to daemon at ${options.socket}. Is the daemon running?`);
145
+ }
146
+ throw error;
147
+ }
148
+ }
149
+
150
+ export async function runSendCommand(args: string[]): Promise<void> {
151
+ const options = parseSendOptions(args);
152
+ let tmpFile: string | undefined;
153
+
154
+ try {
155
+ if (options.image) {
156
+ let localPath: string;
157
+ const isUrl = options.image.startsWith("http://") || options.image.startsWith("https://");
158
+
159
+ if (isUrl) {
160
+ localPath = await downloadUrl(options.image);
161
+ tmpFile = localPath;
162
+ } else {
163
+ localPath = path.resolve(options.image);
164
+ if (!fs.existsSync(localPath)) {
165
+ throw new Error(`Image file not found: ${localPath}`);
166
+ }
167
+ }
168
+
169
+ const uploadedPath = await uploadImage(options.sessionId, localPath, options.socket);
170
+ options.text = options.text ? `${options.text}\n\n${uploadedPath}` : uploadedPath;
171
+ }
172
+
173
+ await sendText(options);
174
+ console.log(`Sent to session ${options.sessionId}`);
175
+ } finally {
176
+ if (tmpFile) {
177
+ try {
178
+ fs.unlinkSync(tmpFile);
179
+ } catch {
180
+ // ignore cleanup errors
181
+ }
182
+ }
183
+ }
184
+ }
@@ -0,0 +1,202 @@
1
+ import type { ProviderId, SessionHandle, SessionStatus } from "@codepiper/core";
2
+ import { readErrorJson, readJson, responseErrorMessage } from "../lib/api";
3
+ import { getRequiredValue } from "../lib/args";
4
+
5
+ export interface SessionsOptions {
6
+ socket: string;
7
+ format: "table" | "json";
8
+ provider?: ProviderId;
9
+ status?: SessionStatus;
10
+ }
11
+
12
+ const VALID_FORMATS = ["table", "json"];
13
+
14
+ export function parseSessionsOptions(args: string[]): SessionsOptions {
15
+ let socket = "/tmp/codepiper.sock";
16
+ let format: "table" | "json" = "table";
17
+ let provider: ProviderId | undefined;
18
+ let status: SessionStatus | undefined;
19
+
20
+ for (let i = 0; i < args.length; i++) {
21
+ const arg = args[i];
22
+ if (arg === undefined) {
23
+ continue;
24
+ }
25
+
26
+ if (arg === "--socket" || arg === "-s") {
27
+ socket = getRequiredValue(args, i, arg);
28
+ i++;
29
+ } else if (arg === "--format" || arg === "-f") {
30
+ const formatValue = getRequiredValue(args, i, arg);
31
+ i++;
32
+ if (!VALID_FORMATS.includes(formatValue)) {
33
+ throw new Error(
34
+ `Invalid format: ${formatValue}. Valid options: ${VALID_FORMATS.join(", ")}`
35
+ );
36
+ }
37
+ format = formatValue as "table" | "json";
38
+ } else if (arg === "--provider" || arg === "-p") {
39
+ provider = getRequiredValue(args, i, arg) as ProviderId;
40
+ i++;
41
+ } else if (arg === "--status") {
42
+ status = getRequiredValue(args, i, arg) as SessionStatus;
43
+ i++;
44
+ }
45
+ }
46
+
47
+ const options: SessionsOptions = {
48
+ socket,
49
+ format,
50
+ };
51
+ if (provider !== undefined) {
52
+ options.provider = provider;
53
+ }
54
+ if (status !== undefined) {
55
+ options.status = status;
56
+ }
57
+
58
+ return options;
59
+ }
60
+
61
+ export async function listSessions(options: SessionsOptions): Promise<SessionHandle[]> {
62
+ const params = new URLSearchParams();
63
+ if (options.provider) {
64
+ params.append("provider", options.provider);
65
+ }
66
+ if (options.status) {
67
+ params.append("status", options.status);
68
+ }
69
+
70
+ const queryString = params.toString();
71
+ const url = `http://localhost/sessions${queryString ? `?${queryString}` : ""}`;
72
+
73
+ try {
74
+ const response = await fetch(url, {
75
+ unix: options.socket,
76
+ method: "GET",
77
+ });
78
+
79
+ if (!response.ok) {
80
+ const errorData = await readErrorJson(response);
81
+ throw new Error(responseErrorMessage(response, errorData));
82
+ }
83
+
84
+ const data = await readJson<{ sessions?: any[] }>(response);
85
+ const sessions = data.sessions || [];
86
+
87
+ return sessions.map((s: any) => ({
88
+ id: s.id,
89
+ provider: s.provider,
90
+ cwd: s.cwd,
91
+ status: s.status,
92
+ createdAt: new Date(s.createdAt),
93
+ updatedAt: new Date(s.updatedAt),
94
+ pid: s.pid,
95
+ transcriptPath: s.transcriptPath,
96
+ metadata: s.metadata,
97
+ }));
98
+ } catch (error: any) {
99
+ if (error.code === "ENOENT" || error.message?.includes("ENOENT")) {
100
+ throw new Error(`Failed to connect to daemon at ${options.socket}. Is the daemon running?`);
101
+ }
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ // ANSI color codes
107
+ const colors = {
108
+ reset: "\x1b[0m",
109
+ bold: "\x1b[1m",
110
+ dim: "\x1b[2m",
111
+ // Status colors
112
+ green: "\x1b[32m",
113
+ yellow: "\x1b[33m",
114
+ red: "\x1b[31m",
115
+ blue: "\x1b[34m",
116
+ cyan: "\x1b[36m",
117
+ gray: "\x1b[90m",
118
+ };
119
+
120
+ function getStatusColor(status: SessionStatus): string {
121
+ switch (status) {
122
+ case "RUNNING":
123
+ return colors.green;
124
+ case "STARTING":
125
+ return colors.yellow;
126
+ case "STOPPED":
127
+ return colors.gray;
128
+ case "CRASHED":
129
+ return colors.red;
130
+ case "NEEDS_PERMISSION":
131
+ return colors.blue;
132
+ case "NEEDS_INPUT":
133
+ return colors.cyan;
134
+ default:
135
+ return colors.reset;
136
+ }
137
+ }
138
+
139
+ function formatTable(sessions: SessionHandle[]): void {
140
+ if (sessions.length === 0) {
141
+ console.log(`${colors.dim}No sessions found.${colors.reset}`);
142
+ return;
143
+ }
144
+
145
+ // Calculate column widths
146
+ const idWidth = Math.max(8, ...sessions.map((s) => s.id.slice(0, 8).length));
147
+ const providerWidth = Math.max(12, ...sessions.map((s) => s.provider.length));
148
+ const statusWidth = Math.max(8, ...sessions.map((s) => s.status.length));
149
+
150
+ // Print header with bold
151
+ console.log(
152
+ `${colors.bold}${"ID".padEnd(idWidth)} ${"PROVIDER".padEnd(providerWidth)} ${"STATUS".padEnd(statusWidth)} ${"DIRECTORY".padEnd(35)} ${"CREATED"}${colors.reset}`
153
+ );
154
+ console.log(
155
+ colors.dim +
156
+ "─".repeat(idWidth + 2 + providerWidth + 2 + statusWidth + 2 + 35 + 2 + 25) +
157
+ colors.reset
158
+ );
159
+
160
+ // Print rows with colored status
161
+ for (const session of sessions) {
162
+ const shortId = session.id.slice(0, 8);
163
+ const cwd = session.cwd.length > 35 ? `...${session.cwd.slice(-32)}` : session.cwd;
164
+ const created = session.createdAt.toLocaleString();
165
+ const statusColor = getStatusColor(session.status);
166
+
167
+ console.log(
168
+ `${colors.cyan}${shortId.padEnd(idWidth)}${colors.reset} ${session.provider.padEnd(providerWidth)} ${statusColor}${session.status.padEnd(statusWidth)}${colors.reset} ${colors.dim}${cwd.padEnd(35)}${colors.reset} ${colors.dim}${created}${colors.reset}`
169
+ );
170
+ }
171
+
172
+ // Summary with counts by status
173
+ const statusCounts = sessions.reduce(
174
+ (acc, s) => {
175
+ acc[s.status] = (acc[s.status] || 0) + 1;
176
+ return acc;
177
+ },
178
+ {} as Record<string, number>
179
+ );
180
+
181
+ const statusSummary = Object.entries(statusCounts)
182
+ .map(([status, count]) => {
183
+ const color = getStatusColor(status as SessionStatus);
184
+ return `${color}${count} ${status.toLowerCase()}${colors.reset}`;
185
+ })
186
+ .join(", ");
187
+
188
+ console.log(
189
+ `\n${colors.bold}Total:${colors.reset} ${sessions.length} session(s) (${statusSummary})`
190
+ );
191
+ }
192
+
193
+ export async function runSessionsCommand(args: string[]): Promise<void> {
194
+ const options = parseSessionsOptions(args);
195
+ const sessions = await listSessions(options);
196
+
197
+ if (options.format === "json") {
198
+ console.log(JSON.stringify(sessions, null, 2));
199
+ } else {
200
+ formatTable(sessions);
201
+ }
202
+ }
@@ -0,0 +1,92 @@
1
+ import { readErrorJson, responseErrorMessage } from "../lib/api";
2
+ import { getRequiredValue } from "../lib/args";
3
+
4
+ export interface SlashOptions {
5
+ sessionId: string;
6
+ command: string;
7
+ args?: string[];
8
+ socket: string;
9
+ }
10
+
11
+ export function parseSlashOptions(args: string[]): SlashOptions {
12
+ let sessionId: string | undefined;
13
+ let command: string | undefined;
14
+ let socket = "/tmp/codepiper.sock";
15
+ const commandArgs: string[] = [];
16
+
17
+ for (let i = 0; i < args.length; i++) {
18
+ const arg = args[i];
19
+ if (arg === undefined) {
20
+ continue;
21
+ }
22
+
23
+ if (arg === "--socket" || arg === "-s") {
24
+ socket = getRequiredValue(args, i, arg);
25
+ i++;
26
+ } else if (!arg.startsWith("-")) {
27
+ if (!sessionId) {
28
+ sessionId = arg;
29
+ } else if (!command) {
30
+ command = arg;
31
+ } else {
32
+ commandArgs.push(arg);
33
+ }
34
+ }
35
+ }
36
+
37
+ if (!sessionId) {
38
+ throw new Error("session-id is required");
39
+ }
40
+
41
+ if (!command) {
42
+ throw new Error("command is required");
43
+ }
44
+
45
+ const options: SlashOptions = {
46
+ sessionId,
47
+ command,
48
+ socket,
49
+ };
50
+ if (commandArgs.length > 0) {
51
+ options.args = commandArgs;
52
+ }
53
+
54
+ return options;
55
+ }
56
+
57
+ export async function sendSlashCommand(options: SlashOptions): Promise<void> {
58
+ const payload = {
59
+ command: options.command,
60
+ args: options.args,
61
+ };
62
+
63
+ try {
64
+ const response = await fetch(`http://localhost/sessions/${options.sessionId}/slash`, {
65
+ unix: options.socket,
66
+ method: "POST",
67
+ headers: {
68
+ "Content-Type": "application/json",
69
+ },
70
+ body: JSON.stringify(payload),
71
+ });
72
+
73
+ if (!response.ok) {
74
+ const errorData = await readErrorJson(response);
75
+ throw new Error(responseErrorMessage(response, errorData));
76
+ }
77
+ } catch (error: any) {
78
+ if (error.code === "ENOENT" || error.message?.includes("ENOENT")) {
79
+ throw new Error(`Failed to connect to daemon at ${options.socket}. Is the daemon running?`);
80
+ }
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ export async function runSlashCommand(args: string[]): Promise<void> {
86
+ const options = parseSlashOptions(args);
87
+
88
+ await sendSlashCommand(options);
89
+
90
+ const argsStr = options.args ? ` ${options.args.join(" ")}` : "";
91
+ console.log(`Slash command sent to session ${options.sessionId}: /${options.command}${argsStr}`);
92
+ }