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,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,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
|
+
}
|