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,143 @@
1
+ /**
2
+ * EventBusAdapter - Bridge between synchronous EventBus API and async SQLiteEventBus
3
+ *
4
+ * The daemon code expects synchronous emit/on API from the in-memory EventBus.
5
+ * This adapter wraps SQLiteEventBus to provide the same interface.
6
+ */
7
+
8
+ import { EventBus } from "./eventBus";
9
+ import type { Event, SQLiteEventBus } from "./sqliteEventBus";
10
+
11
+ type Handler<T> = (data: T) => void;
12
+ type Unsubscribe = () => void;
13
+
14
+ export class EventBusAdapter<
15
+ EventMap extends Record<string, any> = Record<string, any>,
16
+ > extends EventBus<EventMap> {
17
+ private unsubscribers = new Map<string, Unsubscribe[]>();
18
+ private initPromises = new Map<string, Promise<void>>();
19
+
20
+ constructor(private sqliteEventBus: SQLiteEventBus) {
21
+ super();
22
+ }
23
+
24
+ /**
25
+ * Subscribe to event channel
26
+ *
27
+ * Note: Subscription is async but we return unsubscribe function immediately.
28
+ * The subscription will be active once the promise resolves (usually < 10ms).
29
+ */
30
+ on<K extends keyof EventMap>(event: K, handler: Handler<EventMap[K]>): Unsubscribe {
31
+ const channel = String(event);
32
+
33
+ // Start subscription asynchronously
34
+ const initPromise = (async () => {
35
+ try {
36
+ const unsubscribe = await this.sqliteEventBus.subscribe(channel, async (event: Event) => {
37
+ try {
38
+ handler(event.payload as EventMap[K]);
39
+ } catch (err) {
40
+ console.error(`Error in event handler for ${channel}:`, err);
41
+ }
42
+ });
43
+
44
+ // Store unsubscriber
45
+ if (!this.unsubscribers.has(channel)) {
46
+ this.unsubscribers.set(channel, []);
47
+ }
48
+ this.unsubscribers.get(channel)?.push(unsubscribe);
49
+ } catch (err) {
50
+ console.error(`Failed to subscribe to ${channel}:`, err);
51
+ }
52
+ })();
53
+
54
+ this.initPromises.set(channel, initPromise);
55
+
56
+ // Return immediate unsubscribe function
57
+ return () => {
58
+ // Wait for init to complete before unsubscribing
59
+ initPromise
60
+ .then(() => {
61
+ const unsubscribers = this.unsubscribers.get(channel);
62
+ if (unsubscribers && unsubscribers.length > 0) {
63
+ const unsub = unsubscribers.pop();
64
+ if (unsub) unsub();
65
+ }
66
+ })
67
+ .catch((err) => {
68
+ console.error(`Error during unsubscribe from ${channel}:`, err);
69
+ });
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Subscribe once - handler called only for first event
75
+ */
76
+ once<K extends keyof EventMap>(event: K, handler: Handler<EventMap[K]>): Unsubscribe {
77
+ let unsubscribe: Unsubscribe;
78
+
79
+ const wrappedHandler = (data: EventMap[K]) => {
80
+ handler(data);
81
+ if (unsubscribe) unsubscribe();
82
+ };
83
+
84
+ unsubscribe = this.on(event, wrappedHandler);
85
+ return unsubscribe;
86
+ }
87
+
88
+ /**
89
+ * Emit event to channel
90
+ *
91
+ * Note: Publishing is async but we don't wait for it to complete.
92
+ * Events are queued in SQLite and delivered to subscribers via polling.
93
+ */
94
+ emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
95
+ const channel = String(event);
96
+
97
+ // Create event with standard structure
98
+ const eventData: Event = {
99
+ id: crypto.randomUUID(),
100
+ timestamp: Date.now(),
101
+ source: "daemon",
102
+ type: channel,
103
+ payload: data as Record<string, unknown>,
104
+ };
105
+
106
+ // Publish asynchronously (fire and forget)
107
+ this.sqliteEventBus.publish(channel, eventData).catch((err) => {
108
+ console.error(`Failed to publish event to ${channel}:`, err);
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Remove all listeners for an event (or all events)
114
+ */
115
+ removeAllListeners<K extends keyof EventMap>(event?: K): void {
116
+ if (event) {
117
+ const channel = String(event);
118
+ const unsubscribers = this.unsubscribers.get(channel);
119
+ if (unsubscribers) {
120
+ for (const unsub of unsubscribers) {
121
+ unsub();
122
+ }
123
+ this.unsubscribers.delete(channel);
124
+ }
125
+ } else {
126
+ // Remove all listeners
127
+ for (const [_channel, unsubscribers] of this.unsubscribers.entries()) {
128
+ for (const unsub of unsubscribers) {
129
+ unsub();
130
+ }
131
+ }
132
+ this.unsubscribers.clear();
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Close the underlying SQLite EventBus
138
+ */
139
+ async close(): Promise<void> {
140
+ this.removeAllListeners();
141
+ await this.sqliteEventBus.close();
142
+ }
143
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Core package exports
3
+ */
4
+
5
+ export * from "./config";
6
+ export * from "./errors";
7
+ export * from "./eventBus";
8
+ export * from "./eventBusAdapter";
9
+ export * from "./sqliteEventBus";
10
+ export * from "./types";
@@ -0,0 +1,336 @@
1
+ /**
2
+ * SQLite EventBus - Reliable message delivery without Redis dependency
3
+ *
4
+ * Why SQLite instead of Redis:
5
+ * - No external dependencies (SQLite already used for database)
6
+ * - Simpler deployment (one less service to manage)
7
+ * - Perfect for single-daemon deployments
8
+ * - At-least-once delivery via database transactions
9
+ * - Message persistence built-in
10
+ *
11
+ * Trade-offs vs Redis Streams:
12
+ * - No horizontal scaling (single daemon only)
13
+ * - Polling-based instead of push-based
14
+ * - Good enough for most use cases
15
+ */
16
+
17
+ import { Database as BunDatabase } from "bun:sqlite";
18
+
19
+ export interface Event {
20
+ id: string; // UUID
21
+ timestamp: number; // Unix milliseconds
22
+ source: "tmux" | "hook" | "transcript" | "policy" | "workflow" | "user" | "daemon";
23
+ sessionId?: string;
24
+ type: string;
25
+ payload: Record<string, unknown>;
26
+ }
27
+
28
+ export interface SQLiteEventBusOptions {
29
+ /**
30
+ * Path to SQLite database file
31
+ * @default ":memory:"
32
+ */
33
+ dbPath?: string;
34
+
35
+ /**
36
+ * Consumer group name for this instance
37
+ * @example "daemon-instance-1"
38
+ */
39
+ consumerGroup?: string;
40
+
41
+ /**
42
+ * Consumer name (unique per instance)
43
+ * @example "daemon-worker-1"
44
+ */
45
+ consumerName?: string;
46
+
47
+ /**
48
+ * Max events to keep per channel
49
+ * @default 10000
50
+ */
51
+ maxEventsPerChannel?: number;
52
+
53
+ /**
54
+ * Polling interval in ms
55
+ * @default 100
56
+ */
57
+ pollingIntervalMs?: number;
58
+ }
59
+
60
+ interface Subscription {
61
+ channel: string;
62
+ handler: (event: Event) => Promise<void>;
63
+ abortController: AbortController;
64
+ pollingInterval?: Timer;
65
+ }
66
+
67
+ export class SQLiteEventBus {
68
+ private db: BunDatabase;
69
+ private consumerGroup: string;
70
+ private consumerName: string;
71
+ private maxEventsPerChannel: number;
72
+ private pollingIntervalMs: number;
73
+ private subscriptions = new Map<string, Subscription>();
74
+
75
+ constructor(options: SQLiteEventBusOptions = {}) {
76
+ this.db = new BunDatabase(options.dbPath || ":memory:");
77
+ this.consumerGroup = options.consumerGroup || "codepiper-daemon";
78
+ this.consumerName = options.consumerName || `worker-${crypto.randomUUID().slice(0, 8)}`;
79
+ this.maxEventsPerChannel = options.maxEventsPerChannel || 10000;
80
+ this.pollingIntervalMs = options.pollingIntervalMs || 100;
81
+
82
+ this.initSchema();
83
+
84
+ console.log(
85
+ `[SQLiteEventBus] Initialized: group=${this.consumerGroup}, name=${this.consumerName}`
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Initialize database schema for event bus
91
+ */
92
+ private initSchema(): void {
93
+ // Events table (messages)
94
+ this.db.run(`
95
+ CREATE TABLE IF NOT EXISTS event_bus_messages (
96
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
97
+ channel TEXT NOT NULL,
98
+ message_id TEXT NOT NULL,
99
+ data TEXT NOT NULL,
100
+ created_at INTEGER NOT NULL
101
+ )
102
+ `);
103
+
104
+ // Create indexes
105
+ this.db.run(`CREATE INDEX IF NOT EXISTS idx_channel ON event_bus_messages(channel)`);
106
+ this.db.run(`CREATE INDEX IF NOT EXISTS idx_created_at ON event_bus_messages(created_at)`);
107
+
108
+ // Consumer tracking (for at-least-once delivery)
109
+ this.db.run(`
110
+ CREATE TABLE IF NOT EXISTS event_bus_consumers (
111
+ consumer_group TEXT NOT NULL,
112
+ consumer_name TEXT NOT NULL,
113
+ channel TEXT NOT NULL,
114
+ last_processed_id INTEGER NOT NULL DEFAULT 0,
115
+ updated_at INTEGER NOT NULL,
116
+ PRIMARY KEY (consumer_group, consumer_name, channel)
117
+ )
118
+ `);
119
+
120
+ // Pending messages (not yet acknowledged)
121
+ this.db.run(`
122
+ CREATE TABLE IF NOT EXISTS event_bus_pending (
123
+ message_id INTEGER NOT NULL,
124
+ consumer_group TEXT NOT NULL,
125
+ consumer_name TEXT NOT NULL,
126
+ claimed_at INTEGER NOT NULL,
127
+ retry_count INTEGER NOT NULL DEFAULT 0,
128
+ PRIMARY KEY (message_id, consumer_group, consumer_name),
129
+ FOREIGN KEY (message_id) REFERENCES event_bus_messages(id) ON DELETE CASCADE
130
+ )
131
+ `);
132
+ }
133
+
134
+ /**
135
+ * Check if EventBus is ready (always true for SQLite)
136
+ */
137
+ isReady(): boolean {
138
+ return true;
139
+ }
140
+
141
+ /**
142
+ * Publish event to channel
143
+ *
144
+ * @returns Message ID
145
+ */
146
+ async publish(channel: string, event: Event): Promise<string> {
147
+ const messageId = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
148
+
149
+ try {
150
+ // Insert message
151
+ this.db.run(
152
+ `INSERT INTO event_bus_messages (channel, message_id, data, created_at)
153
+ VALUES (?, ?, ?, ?)`,
154
+ [channel, messageId, JSON.stringify(event), Date.now()]
155
+ );
156
+
157
+ // Trim old messages to prevent unbounded growth
158
+ this.db.run(
159
+ `DELETE FROM event_bus_messages
160
+ WHERE channel = ?
161
+ AND id NOT IN (
162
+ SELECT id FROM event_bus_messages
163
+ WHERE channel = ?
164
+ ORDER BY id DESC
165
+ LIMIT ?
166
+ )`,
167
+ [channel, channel, this.maxEventsPerChannel]
168
+ );
169
+
170
+ return messageId;
171
+ } catch (err) {
172
+ console.error(`[SQLiteEventBus] Failed to publish to ${channel}:`, err);
173
+ throw err;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Subscribe to event channel with consumer group
179
+ *
180
+ * Consumer groups provide:
181
+ * - Each message delivered to ONE consumer in the group
182
+ * - Unacknowledged messages are retried
183
+ * - Messages survive process restarts
184
+ *
185
+ * @param channel - Channel name (e.g., "session:output" or "token:usage")
186
+ * @param handler - Async function to process events
187
+ */
188
+ async subscribe(channel: string, handler: (event: Event) => Promise<void>): Promise<() => void> {
189
+ // Initialize consumer tracking
190
+ this.db.run(
191
+ `INSERT OR IGNORE INTO event_bus_consumers (consumer_group, consumer_name, channel, last_processed_id, updated_at)
192
+ VALUES (?, ?, ?, 0, ?)`,
193
+ [this.consumerGroup, this.consumerName, channel, Date.now()]
194
+ );
195
+
196
+ // Create subscription
197
+ const abortController = new AbortController();
198
+ const subscription: Subscription = {
199
+ channel,
200
+ handler,
201
+ abortController,
202
+ };
203
+
204
+ this.subscriptions.set(channel, subscription);
205
+
206
+ // Start polling for messages
207
+ this.startPolling(subscription);
208
+
209
+ console.log(
210
+ `[SQLiteEventBus] Subscribed to ${channel} (group: ${this.consumerGroup}, consumer: ${this.consumerName})`
211
+ );
212
+
213
+ // Return unsubscribe function
214
+ return () => {
215
+ abortController.abort();
216
+ if (subscription.pollingInterval) {
217
+ clearInterval(subscription.pollingInterval);
218
+ }
219
+ this.subscriptions.delete(channel);
220
+ console.log(`[SQLiteEventBus] Unsubscribed from ${channel}`);
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Start polling for new messages
226
+ */
227
+ private startPolling(subscription: Subscription): void {
228
+ const { channel, handler, abortController } = subscription;
229
+
230
+ subscription.pollingInterval = setInterval(async () => {
231
+ if (abortController.signal.aborted) {
232
+ if (subscription.pollingInterval) {
233
+ clearInterval(subscription.pollingInterval);
234
+ }
235
+ return;
236
+ }
237
+
238
+ try {
239
+ // Get last processed ID for this consumer
240
+ const consumer = this.db
241
+ .query(
242
+ `SELECT last_processed_id FROM event_bus_consumers
243
+ WHERE consumer_group = ? AND consumer_name = ? AND channel = ?`
244
+ )
245
+ .get(this.consumerGroup, this.consumerName, channel) as any;
246
+
247
+ const lastProcessedId = consumer?.last_processed_id || 0;
248
+
249
+ // Fetch new messages
250
+ const messages = this.db
251
+ .query(
252
+ `SELECT id, message_id, data FROM event_bus_messages
253
+ WHERE channel = ? AND id > ?
254
+ ORDER BY id ASC
255
+ LIMIT 10`
256
+ )
257
+ .all(channel, lastProcessedId) as any[];
258
+
259
+ for (const message of messages) {
260
+ if (abortController.signal.aborted) break;
261
+
262
+ try {
263
+ // Parse event data
264
+ const event: Event = JSON.parse(message.data);
265
+
266
+ // Process message
267
+ await handler(event);
268
+
269
+ // Acknowledge successful processing
270
+ this.db.run(
271
+ `UPDATE event_bus_consumers
272
+ SET last_processed_id = ?, updated_at = ?
273
+ WHERE consumer_group = ? AND consumer_name = ? AND channel = ?`,
274
+ [message.id, Date.now(), this.consumerGroup, this.consumerName, channel]
275
+ );
276
+ } catch (err) {
277
+ console.error(
278
+ `[SQLiteEventBus] Error processing message ${message.id} from ${channel}:`,
279
+ err
280
+ );
281
+ // Message will be redelivered on next poll since last_processed_id not updated
282
+ }
283
+ }
284
+ } catch (err) {
285
+ if (!abortController.signal.aborted) {
286
+ console.error(`[SQLiteEventBus] Error polling channel ${channel}:`, err);
287
+ }
288
+ }
289
+ }, this.pollingIntervalMs);
290
+ }
291
+
292
+ /**
293
+ * Get channel info
294
+ */
295
+ async getChannelInfo(channel: string): Promise<{
296
+ messageCount: number;
297
+ consumers: number;
298
+ }> {
299
+ try {
300
+ const messageCount = this.db
301
+ .query(`SELECT COUNT(*) as count FROM event_bus_messages WHERE channel = ?`)
302
+ .get(channel) as any;
303
+
304
+ const consumers = this.db
305
+ .query(`SELECT COUNT(*) as count FROM event_bus_consumers WHERE channel = ?`)
306
+ .get(channel) as any;
307
+
308
+ return {
309
+ messageCount: messageCount?.count || 0,
310
+ consumers: consumers?.count || 0,
311
+ };
312
+ } catch (err) {
313
+ console.error(`[SQLiteEventBus] Error getting channel info:`, err);
314
+ return { messageCount: 0, consumers: 0 };
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Close all connections
320
+ */
321
+ async close(): Promise<void> {
322
+ // Stop all subscriptions
323
+ for (const [_channel, subscription] of this.subscriptions.entries()) {
324
+ subscription.abortController.abort();
325
+ if (subscription.pollingInterval) {
326
+ clearInterval(subscription.pollingInterval);
327
+ }
328
+ }
329
+ this.subscriptions.clear();
330
+
331
+ // Close database
332
+ this.db.close();
333
+
334
+ console.log("[SQLiteEventBus] Closed");
335
+ }
336
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Core types for the CodePiper system
3
+ */
4
+
5
+ export const SUPPORTED_PROVIDERS = ["claude-code", "codex"] as const;
6
+ export type KnownProviderId = (typeof SUPPORTED_PROVIDERS)[number];
7
+ export type ProviderId = KnownProviderId | (string & {});
8
+
9
+ export type SessionStatus =
10
+ | "STARTING"
11
+ | "RUNNING"
12
+ | "NEEDS_PERMISSION"
13
+ | "NEEDS_INPUT"
14
+ | "STOPPED"
15
+ | "CRASHED";
16
+
17
+ export type BillingMode = "subscription" | "api";
18
+
19
+ export interface SessionHandle {
20
+ id: string;
21
+ provider: ProviderId;
22
+ cwd: string;
23
+ status: SessionStatus;
24
+ createdAt: Date;
25
+ updatedAt: Date;
26
+ pid?: number;
27
+ ptyRows?: number;
28
+ ptyCols?: number;
29
+ transcriptPath?: string;
30
+ metadata?: Record<string, unknown>;
31
+ }
32
+
33
+ export interface StartSessionOptions {
34
+ id: string;
35
+ cwd: string;
36
+ env: Record<string, string>;
37
+ args?: string[];
38
+ model?: string;
39
+ billingMode?: BillingMode;
40
+ dangerousMode?: boolean;
41
+ }
42
+
43
+ export interface Provider {
44
+ id: ProviderId;
45
+
46
+ startSession(opts: StartSessionOptions): Promise<SessionHandle>;
47
+ sendText(sessionId: string, text: string): Promise<void>;
48
+ sendKeys(sessionId: string, keys: string[]): Promise<void>;
49
+ stopSession(sessionId: string): Promise<void>;
50
+
51
+ onEvent(cb: (evt: ProviderEvent) => void): void;
52
+
53
+ // Optional: Model selection (supported by claude-code)
54
+ switchModel?(sessionId: string, model: string): Promise<void>;
55
+ getCurrentModel?(sessionId: string): string | undefined;
56
+ }
57
+
58
+ export interface ProviderEvent {
59
+ sessionId: string;
60
+ type: string;
61
+ timestamp: Date;
62
+ payload: unknown;
63
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@codepiper/daemon",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "dependencies": {
8
+ "@codepiper/core": "workspace:*",
9
+ "@codepiper/provider-claude-code": "workspace:*"
10
+ }
11
+ }