workplane 0.3.1 → 0.4.1

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/dist/setup.js ADDED
@@ -0,0 +1,410 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // ../db/src/schema/pg.ts
13
+ var pg_exports = {};
14
+ __export(pg_exports, {
15
+ artifacts: () => artifacts,
16
+ nodes: () => nodes,
17
+ runInputEvents: () => runInputEvents,
18
+ runLogs: () => runLogs,
19
+ runs: () => runs,
20
+ tasks: () => tasks
21
+ });
22
+ import { isNull, sql } from "drizzle-orm";
23
+ import { bigint, bigserial, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
24
+ var tasks, nodes, runs, runLogs, artifacts, runInputEvents;
25
+ var init_pg = __esm({
26
+ "../db/src/schema/pg.ts"() {
27
+ "use strict";
28
+ tasks = pgTable("tasks", {
29
+ id: text("id").primaryKey(),
30
+ kind: text("kind").notNull(),
31
+ adapter: text("adapter").notNull(),
32
+ payload: jsonb("payload").$type().notNull(),
33
+ requires: text("requires").array().notNull().default(sql`'{}'::text[]`),
34
+ status: text("status").notNull(),
35
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
36
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
37
+ }, (table) => ({
38
+ statusCreatedAtIdx: index("idx_tasks_status_created_at").on(table.status, table.createdAt)
39
+ }));
40
+ nodes = pgTable("nodes", {
41
+ id: text("id").primaryKey(),
42
+ name: text("name").notNull(),
43
+ capabilities: text("capabilities").array().notNull().default(sql`'{}'::text[]`),
44
+ status: text("status").notNull().default("online"),
45
+ lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }),
46
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
47
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
48
+ });
49
+ runs = pgTable("runs", {
50
+ id: text("id").primaryKey(),
51
+ taskId: text("task_id").notNull().references(() => tasks.id),
52
+ nodeId: text("node_id").notNull().references(() => nodes.id),
53
+ attempt: integer("attempt").notNull(),
54
+ status: text("status").notNull(),
55
+ startedAt: timestamp("started_at", { withTimezone: true }),
56
+ endedAt: timestamp("ended_at", { withTimezone: true }),
57
+ error: text("error"),
58
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
59
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
60
+ }, (table) => ({
61
+ taskIdIdx: index("idx_runs_task_id").on(table.taskId),
62
+ nodeIdIdx: index("idx_runs_node_id").on(table.nodeId)
63
+ }));
64
+ runLogs = pgTable("run_logs", {
65
+ id: bigserial("id", { mode: "number" }).primaryKey(),
66
+ runId: text("run_id").notNull().references(() => runs.id),
67
+ stepName: text("step_name"),
68
+ stream: text("stream").notNull(),
69
+ message: text("message").notNull(),
70
+ timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow()
71
+ }, (table) => ({
72
+ runIdTimestampIdx: index("idx_run_logs_run_id_timestamp").on(table.runId, table.timestamp)
73
+ }));
74
+ artifacts = pgTable("artifacts", {
75
+ id: text("id").primaryKey(),
76
+ runId: text("run_id").notNull().references(() => runs.id),
77
+ type: text("type").notNull(),
78
+ name: text("name").notNull(),
79
+ path: text("path").notNull(),
80
+ metadata: jsonb("metadata").$type(),
81
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
82
+ }, (table) => ({
83
+ runIdIdx: index("idx_artifacts_run_id").on(table.runId)
84
+ }));
85
+ runInputEvents = pgTable("run_input_events", {
86
+ id: bigserial("id", { mode: "number" }).primaryKey(),
87
+ runId: text("run_id").notNull().references(() => runs.id),
88
+ sequence: bigint("sequence", { mode: "number" }).notNull(),
89
+ kind: text("kind").notNull(),
90
+ payload: jsonb("payload").$type().notNull(),
91
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
92
+ deliveredAt: timestamp("delivered_at", { withTimezone: true })
93
+ }, (table) => ({
94
+ runSequenceUniqueIdx: uniqueIndex("idx_run_input_events_run_sequence").on(table.runId, table.sequence),
95
+ undeliveredIdx: index("idx_run_input_events_undelivered").on(table.runId, table.sequence).where(isNull(table.deliveredAt))
96
+ }));
97
+ }
98
+ });
99
+
100
+ // ../db/src/pg-store.ts
101
+ import { and, asc, desc, eq, gt, sql as sql2 } from "drizzle-orm";
102
+ var init_pg_store = __esm({
103
+ "../db/src/pg-store.ts"() {
104
+ "use strict";
105
+ init_pg();
106
+ }
107
+ });
108
+
109
+ // ../db/src/schema/sqlite.ts
110
+ var sqlite_exports = {};
111
+ __export(sqlite_exports, {
112
+ artifacts: () => artifacts2,
113
+ nodes: () => nodes2,
114
+ runInputEvents: () => runInputEvents2,
115
+ runLogs: () => runLogs2,
116
+ runs: () => runs2,
117
+ tasks: () => tasks2
118
+ });
119
+ import { isNull as isNull2, sql as sql3 } from "drizzle-orm";
120
+ import { index as index2, integer as integer2, sqliteTable, text as text2, uniqueIndex as uniqueIndex2 } from "drizzle-orm/sqlite-core";
121
+ var nowDefault, tasks2, nodes2, runs2, runLogs2, artifacts2, runInputEvents2;
122
+ var init_sqlite = __esm({
123
+ "../db/src/schema/sqlite.ts"() {
124
+ "use strict";
125
+ nowDefault = sql3`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`;
126
+ tasks2 = sqliteTable("tasks", {
127
+ id: text2("id").primaryKey(),
128
+ kind: text2("kind").notNull(),
129
+ adapter: text2("adapter").notNull(),
130
+ payload: text2("payload", { mode: "json" }).$type().notNull().default({}),
131
+ requires: text2("requires", { mode: "json" }).$type().notNull().default([]),
132
+ status: text2("status").notNull(),
133
+ createdAt: text2("created_at").notNull().default(nowDefault),
134
+ updatedAt: text2("updated_at").notNull().default(nowDefault)
135
+ }, (table) => ({
136
+ statusCreatedAtIdx: index2("idx_tasks_status_created_at").on(table.status, table.createdAt)
137
+ }));
138
+ nodes2 = sqliteTable("nodes", {
139
+ id: text2("id").primaryKey(),
140
+ name: text2("name").notNull(),
141
+ capabilities: text2("capabilities", { mode: "json" }).$type().notNull().default([]),
142
+ status: text2("status").notNull().default("online"),
143
+ lastHeartbeatAt: text2("last_heartbeat_at"),
144
+ createdAt: text2("created_at").notNull().default(nowDefault),
145
+ updatedAt: text2("updated_at").notNull().default(nowDefault)
146
+ });
147
+ runs2 = sqliteTable("runs", {
148
+ id: text2("id").primaryKey(),
149
+ taskId: text2("task_id").notNull().references(() => tasks2.id),
150
+ nodeId: text2("node_id").notNull().references(() => nodes2.id),
151
+ attempt: integer2("attempt").notNull(),
152
+ status: text2("status").notNull(),
153
+ startedAt: text2("started_at"),
154
+ endedAt: text2("ended_at"),
155
+ error: text2("error"),
156
+ createdAt: text2("created_at").notNull().default(nowDefault),
157
+ updatedAt: text2("updated_at").notNull().default(nowDefault)
158
+ }, (table) => ({
159
+ taskIdIdx: index2("idx_runs_task_id").on(table.taskId),
160
+ nodeIdIdx: index2("idx_runs_node_id").on(table.nodeId)
161
+ }));
162
+ runLogs2 = sqliteTable("run_logs", {
163
+ id: integer2("id").primaryKey({ autoIncrement: true }),
164
+ runId: text2("run_id").notNull().references(() => runs2.id),
165
+ stepName: text2("step_name"),
166
+ stream: text2("stream").notNull(),
167
+ message: text2("message").notNull(),
168
+ timestamp: text2("timestamp").notNull().default(nowDefault)
169
+ }, (table) => ({
170
+ runIdTimestampIdx: index2("idx_run_logs_run_id_timestamp").on(table.runId, table.timestamp)
171
+ }));
172
+ artifacts2 = sqliteTable("artifacts", {
173
+ id: text2("id").primaryKey(),
174
+ runId: text2("run_id").notNull().references(() => runs2.id),
175
+ type: text2("type").notNull(),
176
+ name: text2("name").notNull(),
177
+ path: text2("path").notNull(),
178
+ metadata: text2("metadata", { mode: "json" }).$type(),
179
+ createdAt: text2("created_at").notNull().default(nowDefault)
180
+ }, (table) => ({
181
+ runIdIdx: index2("idx_artifacts_run_id").on(table.runId)
182
+ }));
183
+ runInputEvents2 = sqliteTable("run_input_events", {
184
+ id: integer2("id").primaryKey({ autoIncrement: true }),
185
+ runId: text2("run_id").notNull().references(() => runs2.id),
186
+ sequence: integer2("sequence").notNull(),
187
+ kind: text2("kind").notNull(),
188
+ payload: text2("payload", { mode: "json" }).$type().notNull().default({}),
189
+ createdAt: text2("created_at").notNull().default(nowDefault),
190
+ deliveredAt: text2("delivered_at")
191
+ }, (table) => ({
192
+ runSequenceUniqueIdx: uniqueIndex2("idx_run_input_events_run_sequence").on(table.runId, table.sequence),
193
+ undeliveredIdx: index2("idx_run_input_events_undelivered").on(table.runId, table.sequence).where(isNull2(table.deliveredAt))
194
+ }));
195
+ }
196
+ });
197
+
198
+ // ../db/src/sqlite-store.ts
199
+ import { and as and2, asc as asc2, desc as desc2, eq as eq2, gt as gt2 } from "drizzle-orm";
200
+ var init_sqlite_store = __esm({
201
+ "../db/src/sqlite-store.ts"() {
202
+ "use strict";
203
+ init_sqlite();
204
+ }
205
+ });
206
+
207
+ // ../db/src/client.ts
208
+ import { mkdirSync } from "fs";
209
+ import { homedir } from "os";
210
+ import { dirname, join } from "path";
211
+ import { drizzle as drizzlePg } from "drizzle-orm/node-postgres";
212
+ import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3";
213
+ import Database from "better-sqlite3";
214
+ import { Pool } from "pg";
215
+ function getPool(databaseUrl) {
216
+ const existing = pools.get(databaseUrl);
217
+ if (existing) return existing;
218
+ const pool = new Pool({ connectionString: databaseUrl });
219
+ pools.set(databaseUrl, pool);
220
+ return pool;
221
+ }
222
+ function getDrizzle(pool) {
223
+ return drizzlePg(pool, { schema: pg_exports });
224
+ }
225
+ var pools;
226
+ var init_client = __esm({
227
+ "../db/src/client.ts"() {
228
+ "use strict";
229
+ init_pg_store();
230
+ init_sqlite_store();
231
+ init_pg();
232
+ init_sqlite();
233
+ pools = /* @__PURE__ */ new Map();
234
+ }
235
+ });
236
+
237
+ // ../db/src/migration.ts
238
+ var migration_exports = {};
239
+ __export(migration_exports, {
240
+ runMigration: () => runMigration
241
+ });
242
+ import { mkdirSync as mkdirSync2 } from "fs";
243
+ import { homedir as homedir2 } from "os";
244
+ import { dirname as dirname2, join as join2, resolve } from "path";
245
+ import { fileURLToPath } from "url";
246
+ async function migratePostgres(databaseUrl) {
247
+ const adminUrl = new URL(databaseUrl);
248
+ const dbName = adminUrl.pathname.replace(/^\//, "");
249
+ if (!dbName) throw new Error("DATABASE_URL must include a database name");
250
+ adminUrl.pathname = "/postgres";
251
+ const adminPool = getPool(adminUrl.toString());
252
+ try {
253
+ const { rowCount } = await adminPool.query(
254
+ "SELECT 1 FROM pg_database WHERE datname = $1",
255
+ [dbName]
256
+ );
257
+ if (!rowCount) {
258
+ await adminPool.query(`CREATE DATABASE "${dbName.replaceAll('"', '""')}"`);
259
+ process.stdout.write(`Created database: ${dbName}
260
+ `);
261
+ }
262
+ } catch (err) {
263
+ const e = err;
264
+ if (e.code === "42501") {
265
+ throw new Error(
266
+ `Database "${dbName}" does not exist and the current role lacks CREATE DATABASE permissions.`
267
+ );
268
+ }
269
+ throw err;
270
+ } finally {
271
+ await adminPool.end();
272
+ }
273
+ const pool = getPool(databaseUrl);
274
+ const { migrate } = await import("drizzle-orm/node-postgres/migrator");
275
+ await migrate(getDrizzle(pool), { migrationsFolder: resolve(distDir, "migrations/pg") });
276
+ await pool.end();
277
+ process.stdout.write("Postgres migrations applied.\n");
278
+ }
279
+ async function migrateSqlite(databaseUrl) {
280
+ const { default: Database2 } = await import("better-sqlite3");
281
+ const { drizzle } = await import("drizzle-orm/better-sqlite3");
282
+ const { migrate } = await import("drizzle-orm/better-sqlite3/migrator");
283
+ const sqliteSchema = await Promise.resolve().then(() => (init_sqlite(), sqlite_exports));
284
+ const rawPath = databaseUrl.startsWith("sqlite://") ? databaseUrl.slice("sqlite://".length) : databaseUrl;
285
+ const filePath = !rawPath ? join2(homedir2(), ".workplane", "workplane.db") : rawPath.startsWith("~/") ? join2(homedir2(), rawPath.slice(2)) : rawPath;
286
+ mkdirSync2(dirname2(filePath), { recursive: true });
287
+ const sqlite = new Database2(filePath);
288
+ sqlite.pragma("journal_mode = WAL");
289
+ sqlite.pragma("synchronous = NORMAL");
290
+ const db = drizzle(sqlite, { schema: sqliteSchema });
291
+ migrate(db, { migrationsFolder: resolve(distDir, "migrations/sqlite") });
292
+ sqlite.close();
293
+ process.stdout.write(`SQLite migrations applied: ${filePath}
294
+ `);
295
+ }
296
+ async function runMigration(databaseUrl) {
297
+ if (databaseUrl.startsWith("postgres://") || databaseUrl.startsWith("postgresql://")) {
298
+ await migratePostgres(databaseUrl);
299
+ } else {
300
+ await migrateSqlite(databaseUrl);
301
+ }
302
+ }
303
+ var distDir;
304
+ var init_migration = __esm({
305
+ "../db/src/migration.ts"() {
306
+ "use strict";
307
+ init_client();
308
+ distDir = dirname2(fileURLToPath(import.meta.url));
309
+ }
310
+ });
311
+
312
+ // ../cli/src/setup.ts
313
+ import { randomBytes } from "crypto";
314
+ import { existsSync, mkdirSync as mkdirSync3, readFileSync, writeFileSync } from "fs";
315
+ import { homedir as homedir3 } from "os";
316
+ import { join as join3 } from "path";
317
+ import { createInterface } from "readline/promises";
318
+ var workplaneDir = join3(homedir3(), ".workplane");
319
+ var configPath = join3(workplaneDir, ".env");
320
+ function generateToken() {
321
+ return randomBytes(32).toString("hex");
322
+ }
323
+ function parseEnvFile(path) {
324
+ try {
325
+ const result = {};
326
+ for (const line of readFileSync(path, "utf8").split("\n")) {
327
+ const t = line.trim();
328
+ if (!t || t.startsWith("#")) continue;
329
+ const eq3 = t.indexOf("=");
330
+ if (eq3 !== -1) result[t.slice(0, eq3).trim()] = t.slice(eq3 + 1).trim();
331
+ }
332
+ return result;
333
+ } catch {
334
+ return {};
335
+ }
336
+ }
337
+ async function ask(rl, label, def) {
338
+ const answer = await rl.question(` ${label} [${def}]: `);
339
+ return answer.trim() || def;
340
+ }
341
+ async function main() {
342
+ const existing = parseEnvFile(configPath);
343
+ const isUpdate = existsSync(configPath);
344
+ process.stdout.write("Workplane Setup\n");
345
+ process.stdout.write(
346
+ isUpdate ? `Updating config at ${configPath}
347
+ ` : `Creating config at ${configPath}
348
+ `
349
+ );
350
+ process.stdout.write("Press Enter to accept the value shown in brackets.\n\n");
351
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
352
+ const dbUrl = await ask(
353
+ rl,
354
+ "DATABASE_URL ",
355
+ existing.DATABASE_URL ?? "sqlite://~/.workplane/workplane.db"
356
+ );
357
+ const port = await ask(
358
+ rl,
359
+ "Server port ",
360
+ existing.WORKPLANE_SERVER_PORT ?? "8787"
361
+ );
362
+ const serverUrl = await ask(
363
+ rl,
364
+ "Server URL (for nodes)",
365
+ existing.WORKPLANE_SERVER_URL ?? `http://localhost:${port}`
366
+ );
367
+ const nodeToken = await ask(
368
+ rl,
369
+ "Node token ",
370
+ existing.WORKPLANE_NODE_TOKEN ?? generateToken()
371
+ );
372
+ const operatorToken = await ask(
373
+ rl,
374
+ "Operator token ",
375
+ existing.WORKPLANE_OPERATOR_TOKEN ?? generateToken()
376
+ );
377
+ rl.close();
378
+ const config = [
379
+ `DATABASE_URL=${dbUrl}`,
380
+ `WORKPLANE_SERVER_PORT=${port}`,
381
+ `WORKPLANE_SERVER_URL=${serverUrl}`,
382
+ `WORKPLANE_NODE_TOKEN=${nodeToken}`,
383
+ `WORKPLANE_OPERATOR_TOKEN=${operatorToken}`
384
+ ].join("\n") + "\n";
385
+ mkdirSync3(workplaneDir, { recursive: true });
386
+ writeFileSync(configPath, config);
387
+ process.stdout.write(`
388
+ Config saved: ${configPath}
389
+ `);
390
+ process.stdout.write("Running migrations...\n");
391
+ const { runMigration: runMigration2 } = await Promise.resolve().then(() => (init_migration(), migration_exports));
392
+ await runMigration2(dbUrl);
393
+ process.stdout.write(`
394
+ Ready. Start your fleet:
395
+
396
+ workplane-server # control plane
397
+ workplane-node # worker (this machine or another)
398
+ workplane tasks # inspect submitted work
399
+
400
+ Tokens for remote machines:
401
+ WORKPLANE_NODE_TOKEN=${nodeToken}
402
+ WORKPLANE_OPERATOR_TOKEN=${operatorToken}
403
+ `);
404
+ }
405
+ main().catch((error) => {
406
+ process.stderr.write(`${String(error)}
407
+ `);
408
+ process.exit(1);
409
+ });
410
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../db/src/schema/pg.ts","../../db/src/pg-store.ts","../../db/src/schema/sqlite.ts","../../db/src/sqlite-store.ts","../../db/src/client.ts","../../db/src/migration.ts","../../cli/src/setup.ts"],"sourcesContent":["import { isNull, sql } from \"drizzle-orm\";\nimport { bigint, bigserial, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex } from \"drizzle-orm/pg-core\";\n\nexport const tasks = pgTable(\"tasks\", {\n id: text(\"id\").primaryKey(),\n kind: text(\"kind\").notNull(),\n adapter: text(\"adapter\").notNull(),\n payload: jsonb(\"payload\").$type<Record<string, unknown>>().notNull(),\n requires: text(\"requires\").array().notNull().default(sql`'{}'::text[]`),\n status: text(\"status\").notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true }).notNull().defaultNow(),\n}, (table) => ({\n statusCreatedAtIdx: index(\"idx_tasks_status_created_at\").on(table.status, table.createdAt),\n}));\n\nexport const nodes = pgTable(\"nodes\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\").notNull(),\n capabilities: text(\"capabilities\").array().notNull().default(sql`'{}'::text[]`),\n status: text(\"status\").notNull().default(\"online\"),\n lastHeartbeatAt: timestamp(\"last_heartbeat_at\", { withTimezone: true }),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true }).notNull().defaultNow(),\n});\n\nexport const runs = pgTable(\"runs\", {\n id: text(\"id\").primaryKey(),\n taskId: text(\"task_id\").notNull().references(() => tasks.id),\n nodeId: text(\"node_id\").notNull().references(() => nodes.id),\n attempt: integer(\"attempt\").notNull(),\n status: text(\"status\").notNull(),\n startedAt: timestamp(\"started_at\", { withTimezone: true }),\n endedAt: timestamp(\"ended_at\", { withTimezone: true }),\n error: text(\"error\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true }).notNull().defaultNow(),\n}, (table) => ({\n taskIdIdx: index(\"idx_runs_task_id\").on(table.taskId),\n nodeIdIdx: index(\"idx_runs_node_id\").on(table.nodeId),\n}));\n\nexport const runLogs = pgTable(\"run_logs\", {\n id: bigserial(\"id\", { mode: \"number\" }).primaryKey(),\n runId: text(\"run_id\").notNull().references(() => runs.id),\n stepName: text(\"step_name\"),\n stream: text(\"stream\").notNull(),\n message: text(\"message\").notNull(),\n timestamp: timestamp(\"timestamp\", { withTimezone: true }).notNull().defaultNow(),\n}, (table) => ({\n runIdTimestampIdx: index(\"idx_run_logs_run_id_timestamp\").on(table.runId, table.timestamp),\n}));\n\nexport const artifacts = pgTable(\"artifacts\", {\n id: text(\"id\").primaryKey(),\n runId: text(\"run_id\").notNull().references(() => runs.id),\n type: text(\"type\").notNull(),\n name: text(\"name\").notNull(),\n path: text(\"path\").notNull(),\n metadata: jsonb(\"metadata\").$type<Record<string, unknown>>(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n}, (table) => ({\n runIdIdx: index(\"idx_artifacts_run_id\").on(table.runId),\n}));\n\nexport const runInputEvents = pgTable(\"run_input_events\", {\n id: bigserial(\"id\", { mode: \"number\" }).primaryKey(),\n runId: text(\"run_id\").notNull().references(() => runs.id),\n sequence: bigint(\"sequence\", { mode: \"number\" }).notNull(),\n kind: text(\"kind\").notNull(),\n payload: jsonb(\"payload\").$type<Record<string, unknown>>().notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true }).notNull().defaultNow(),\n deliveredAt: timestamp(\"delivered_at\", { withTimezone: true }),\n}, (table) => ({\n runSequenceUniqueIdx: uniqueIndex(\"idx_run_input_events_run_sequence\").on(table.runId, table.sequence),\n undeliveredIdx: index(\"idx_run_input_events_undelivered\").on(table.runId, table.sequence).where(isNull(table.deliveredAt)),\n}));\n","import { and, asc, desc, eq, gt, sql } from \"drizzle-orm\";\nimport { makeId } from \"../../core/src/ids.js\";\nimport type { DrizzleDb } from \"./client.js\";\nimport { artifacts, nodes, runInputEvents, runLogs, runs, tasks } from \"./schema/pg.js\";\nimport type { WorkplaneStore } from \"./store-interface.js\";\nimport type {\n AppendInputEventInput,\n ArtifactInput,\n ArtifactRecord,\n CreateTaskInput,\n NodePollResult,\n NodeRecord,\n RunInputEvent,\n RunLogInput,\n RunLogRecord,\n RunRecord,\n TaskRecord,\n} from \"../../types/src/index.js\";\n\nfunction toTaskRecord(row: typeof tasks.$inferSelect): TaskRecord {\n return {\n id: row.id,\n kind: row.kind,\n adapter: row.adapter,\n payload: row.payload,\n requires: row.requires ?? [],\n status: row.status as TaskRecord[\"status\"],\n createdAt: row.createdAt.toISOString(),\n updatedAt: row.updatedAt.toISOString(),\n };\n}\n\nfunction toRunRecord(row: typeof runs.$inferSelect): RunRecord {\n return {\n id: row.id,\n taskId: row.taskId,\n nodeId: row.nodeId,\n attempt: row.attempt,\n status: row.status as RunRecord[\"status\"],\n startedAt: row.startedAt?.toISOString() ?? null,\n endedAt: row.endedAt?.toISOString() ?? null,\n error: row.error ?? null,\n };\n}\n\nfunction toNodeRecord(row: typeof nodes.$inferSelect): NodeRecord {\n return {\n id: row.id,\n name: row.name,\n capabilities: row.capabilities ?? [],\n status: row.status as NodeRecord[\"status\"],\n lastHeartbeatAt: row.lastHeartbeatAt?.toISOString() ?? null,\n };\n}\n\nfunction toLogRecord(row: typeof runLogs.$inferSelect): RunLogRecord {\n return {\n id: row.id,\n runId: row.runId,\n stepName: row.stepName ?? null,\n stream: row.stream as RunLogRecord[\"stream\"],\n message: row.message,\n timestamp: row.timestamp.toISOString(),\n };\n}\n\nfunction toArtifactRecord(row: typeof artifacts.$inferSelect): ArtifactRecord {\n return {\n id: row.id,\n runId: row.runId,\n type: row.type,\n name: row.name,\n path: row.path,\n metadata: row.metadata ?? null,\n createdAt: row.createdAt.toISOString(),\n };\n}\n\nfunction toInputEventRecord(row: typeof runInputEvents.$inferSelect): RunInputEvent {\n return {\n id: row.id,\n runId: row.runId,\n sequence: row.sequence,\n kind: row.kind as RunInputEvent[\"kind\"],\n payload: row.payload,\n createdAt: row.createdAt.toISOString(),\n deliveredAt: row.deliveredAt?.toISOString() ?? null,\n };\n}\n\nfunction mapRawTask(row: Record<string, unknown>): TaskRecord {\n return {\n id: String(row.id),\n kind: String(row.kind),\n adapter: String(row.adapter),\n payload: (row.payload as Record<string, unknown>) ?? {},\n requires: (row.requires as string[]) ?? [],\n status: row.status as TaskRecord[\"status\"],\n createdAt: new Date(String(row.created_at)).toISOString(),\n updatedAt: new Date(String(row.updated_at)).toISOString(),\n };\n}\n\nfunction mapRawRun(row: Record<string, unknown>): RunRecord {\n return {\n id: String(row.id),\n taskId: String(row.task_id),\n nodeId: String(row.node_id),\n attempt: Number(row.attempt),\n status: row.status as RunRecord[\"status\"],\n startedAt: row.started_at ? new Date(String(row.started_at)).toISOString() : null,\n endedAt: row.ended_at ? new Date(String(row.ended_at)).toISOString() : null,\n error: row.error ? String(row.error) : null,\n };\n}\n\nexport class PgStore implements WorkplaneStore {\n constructor(private readonly db: DrizzleDb) {}\n\n async createTask(input: CreateTaskInput): Promise<TaskRecord> {\n const taskId = makeId(\"task\");\n const [row] = await this.db.insert(tasks).values({\n id: taskId,\n kind: input.kind,\n adapter: input.adapter,\n payload: input.payload,\n requires: input.requires ?? [],\n status: \"queued\",\n }).returning();\n return toTaskRecord(row);\n }\n\n async listTasks(status?: TaskRecord[\"status\"]): Promise<TaskRecord[]> {\n const rows = await this.db\n .select()\n .from(tasks)\n .where(status ? eq(tasks.status, status) : undefined)\n .orderBy(desc(tasks.createdAt))\n .limit(100);\n return rows.map(toTaskRecord);\n }\n\n async getTask(taskId: string): Promise<TaskRecord | null> {\n const [row] = await this.db.select().from(tasks).where(eq(tasks.id, taskId));\n return row ? toTaskRecord(row) : null;\n }\n\n async listRuns(filters?: { taskId?: string; status?: RunRecord[\"status\"] }): Promise<RunRecord[]> {\n const conditions = [];\n if (filters?.taskId) conditions.push(eq(runs.taskId, filters.taskId));\n if (filters?.status) conditions.push(eq(runs.status, filters.status));\n\n const rows = await this.db\n .select()\n .from(runs)\n .where(conditions.length ? and(...conditions) : undefined)\n .orderBy(desc(runs.createdAt))\n .limit(100);\n return rows.map(toRunRecord);\n }\n\n async getRun(runId: string): Promise<RunRecord | null> {\n const [row] = await this.db.select().from(runs).where(eq(runs.id, runId));\n return row ? toRunRecord(row) : null;\n }\n\n async registerNode(name: string, capabilities: string[], preferredId?: string): Promise<NodeRecord> {\n const [existing] = await this.db.select().from(nodes).where(eq(nodes.name, name));\n\n if (existing) {\n const [updated] = await this.db.update(nodes)\n .set({ capabilities, status: \"online\", lastHeartbeatAt: sql`now()`, updatedAt: sql`now()` })\n .where(eq(nodes.id, existing.id))\n .returning();\n return toNodeRecord(updated);\n }\n\n const nodeId = preferredId ?? makeId(\"node\");\n const [inserted] = await this.db.insert(nodes).values({\n id: nodeId, name, capabilities, status: \"online\", lastHeartbeatAt: sql`now()`,\n }).returning();\n return toNodeRecord(inserted);\n }\n\n async getRunCancellationState(runId: string): Promise<{ runStatus: string; taskStatus: string } | null> {\n const [row] = await this.db\n .select({ runStatus: runs.status, taskStatus: tasks.status })\n .from(runs)\n .innerJoin(tasks, eq(tasks.id, runs.taskId))\n .where(eq(runs.id, runId));\n return row ?? null;\n }\n\n async pollNode(nodeId: string, capabilities: string[]): Promise<NodePollResult | null> {\n return await this.db.transaction(async (tx) => {\n await tx.execute(sql`\n UPDATE nodes\n SET capabilities = ${capabilities}::text[], status = 'online',\n last_heartbeat_at = now(), updated_at = now()\n WHERE id = ${nodeId}\n `);\n\n const taskResult = await tx.execute(sql`\n SELECT * FROM tasks\n WHERE status = 'queued' AND requires <@ ${capabilities}::text[]\n ORDER BY created_at ASC\n LIMIT 1\n FOR UPDATE SKIP LOCKED\n `);\n\n if (taskResult.rows.length === 0) return null;\n\n const taskRow = taskResult.rows[0] as Record<string, unknown>;\n const taskId = String(taskRow.id);\n\n const attemptResult = await tx.execute(sql`\n SELECT COALESCE(MAX(attempt), 0) AS max_attempt FROM runs WHERE task_id = ${taskId}\n `);\n const attempt = Number((attemptResult.rows[0] as Record<string, unknown>).max_attempt) + 1;\n\n const runId = makeId(\"run\");\n\n const runResult = await tx.execute(sql`\n INSERT INTO runs (id, task_id, node_id, attempt, status, started_at)\n VALUES (${runId}, ${taskId}, ${nodeId}, ${attempt}, 'assigned', now())\n RETURNING *\n `);\n\n await tx.execute(sql`\n UPDATE tasks SET status = 'assigned', updated_at = now() WHERE id = ${taskId}\n `);\n\n return {\n task: mapRawTask(taskRow),\n run: mapRawRun(runResult.rows[0] as Record<string, unknown>),\n };\n });\n }\n\n async updateRunStatus(runId: string, status: RunRecord[\"status\"], error?: string): Promise<RunRecord | null> {\n return await this.db.transaction(async (tx) => {\n const currentResult = await tx.execute(sql`\n SELECT r.*, t.status AS task_status\n FROM runs r JOIN tasks t ON t.id = r.task_id\n WHERE r.id = ${runId}\n FOR UPDATE\n `);\n\n if (currentResult.rows.length === 0) return null;\n\n const currentRow = currentResult.rows[0] as Record<string, unknown>;\n const currentTaskStatus = String(currentRow.task_status);\n\n if (currentTaskStatus === \"cancelled\" && status !== \"cancelled\") {\n return mapRawRun(currentRow);\n }\n\n const isTerminal = status === \"succeeded\" || status === \"failed\" || status === \"cancelled\";\n const [updatedRun] = await tx.update(runs)\n .set(isTerminal\n ? { status, error: error ?? null, endedAt: sql`now()`, updatedAt: sql`now()` }\n : { status, error: error ?? null, updatedAt: sql`now()` })\n .where(eq(runs.id, runId))\n .returning();\n\n if (!updatedRun) return null;\n\n const nextTaskStatus = isTerminal ? status : \"running\";\n if (currentTaskStatus !== \"cancelled\" || status === \"cancelled\") {\n await tx.update(tasks)\n .set({ status: nextTaskStatus, updatedAt: sql`now()` })\n .where(eq(tasks.id, updatedRun.taskId));\n }\n\n return toRunRecord(updatedRun);\n });\n }\n\n async appendRunLogs(runId: string, logs: RunLogInput[]): Promise<number> {\n if (logs.length === 0) return 0;\n await this.db.insert(runLogs).values(\n logs.map((log) => ({\n runId,\n stepName: log.stepName ?? null,\n stream: log.stream,\n message: log.message,\n })),\n );\n return logs.length;\n }\n\n async getRunLogs(runId: string): Promise<RunLogRecord[]> {\n const rows = await this.db\n .select()\n .from(runLogs)\n .where(eq(runLogs.runId, runId))\n .orderBy(asc(runLogs.timestamp));\n return rows.map(toLogRecord);\n }\n\n async createArtifact(runId: string, input: ArtifactInput): Promise<ArtifactRecord> {\n const artifactId = makeId(\"artifact\");\n const [row] = await this.db.insert(artifacts).values({\n id: artifactId,\n runId,\n type: input.type,\n name: input.name,\n path: input.path,\n metadata: input.metadata ?? null,\n }).returning();\n return toArtifactRecord(row);\n }\n\n async listRunArtifacts(runId: string): Promise<ArtifactRecord[]> {\n const rows = await this.db\n .select()\n .from(artifacts)\n .where(eq(artifacts.runId, runId))\n .orderBy(asc(artifacts.createdAt));\n return rows.map(toArtifactRecord);\n }\n\n async retryTask(taskId: string): Promise<TaskRecord | null> {\n const [row] = await this.db.update(tasks)\n .set({ status: \"queued\", updatedAt: sql`now()` })\n .where(and(eq(tasks.id, taskId), eq(tasks.status, \"failed\")))\n .returning();\n return row ? toTaskRecord(row) : null;\n }\n\n async cancelTask(taskId: string): Promise<TaskRecord | null> {\n return await this.db.transaction(async (tx) => {\n const taskResult = await tx.execute(sql`\n SELECT * FROM tasks WHERE id = ${taskId} FOR UPDATE\n `);\n\n if (taskResult.rows.length === 0) return null;\n\n const taskRow = taskResult.rows[0] as Record<string, unknown>;\n const taskStatus = String(taskRow.status);\n\n if (taskStatus === \"succeeded\" || taskStatus === \"failed\" || taskStatus === \"cancelled\") {\n return null;\n }\n\n if (taskStatus === \"assigned\" || taskStatus === \"running\") {\n const runResult = await tx.execute(sql`\n SELECT * FROM runs WHERE task_id = ${taskId} ORDER BY attempt DESC LIMIT 1 FOR UPDATE\n `);\n\n if (runResult.rows.length > 0) {\n const runRow = runResult.rows[0] as Record<string, unknown>;\n const runStatus = String(runRow.status);\n if (runStatus !== \"succeeded\" && runStatus !== \"failed\" && runStatus !== \"cancelled\") {\n await tx.execute(sql`\n UPDATE runs SET status = 'cancelled', ended_at = now(), updated_at = now(),\n error = COALESCE(error, 'task cancelled')\n WHERE id = ${runRow.id}\n `);\n }\n }\n }\n\n const [updatedTask] = await tx.update(tasks)\n .set({ status: \"cancelled\", updatedAt: sql`now()` })\n .where(eq(tasks.id, taskId))\n .returning();\n\n return toTaskRecord(updatedTask);\n });\n }\n\n async appendInputEvent(runId: string, input: AppendInputEventInput): Promise<RunInputEvent> {\n return await this.db.transaction(async (tx) => {\n const seqResult = await tx.execute(sql`\n SELECT COALESCE(MAX(sequence), 0) + 1 AS next_seq FROM run_input_events WHERE run_id = ${runId}\n `);\n const sequence = Number((seqResult.rows[0] as Record<string, unknown>).next_seq);\n\n const [inserted] = await tx.insert(runInputEvents)\n .values({ runId, sequence, kind: input.kind, payload: input.payload })\n .returning();\n\n return toInputEventRecord(inserted);\n });\n }\n\n async getInputEvents(runId: string, afterSequence: number): Promise<RunInputEvent[]> {\n const rows = await this.db\n .select()\n .from(runInputEvents)\n .where(and(eq(runInputEvents.runId, runId), gt(runInputEvents.sequence, afterSequence)))\n .orderBy(asc(runInputEvents.sequence));\n return rows.map(toInputEventRecord);\n }\n\n async markInputDelivered(runId: string, sequence: number): Promise<void> {\n await this.db.update(runInputEvents)\n .set({ deliveredAt: sql`now()` })\n .where(and(eq(runInputEvents.runId, runId), eq(runInputEvents.sequence, sequence)));\n }\n}\n","import { isNull, sql } from \"drizzle-orm\";\nimport { index, integer, sqliteTable, text, uniqueIndex } from \"drizzle-orm/sqlite-core\";\n\nconst nowDefault = sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`;\n\nexport const tasks = sqliteTable(\"tasks\", {\n id: text(\"id\").primaryKey(),\n kind: text(\"kind\").notNull(),\n adapter: text(\"adapter\").notNull(),\n payload: text(\"payload\", { mode: \"json\" }).$type<Record<string, unknown>>().notNull().default({}),\n requires: text(\"requires\", { mode: \"json\" }).$type<string[]>().notNull().default([]),\n status: text(\"status\").notNull(),\n createdAt: text(\"created_at\").notNull().default(nowDefault),\n updatedAt: text(\"updated_at\").notNull().default(nowDefault),\n}, (table) => ({\n statusCreatedAtIdx: index(\"idx_tasks_status_created_at\").on(table.status, table.createdAt),\n}));\n\nexport const nodes = sqliteTable(\"nodes\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\").notNull(),\n capabilities: text(\"capabilities\", { mode: \"json\" }).$type<string[]>().notNull().default([]),\n status: text(\"status\").notNull().default(\"online\"),\n lastHeartbeatAt: text(\"last_heartbeat_at\"),\n createdAt: text(\"created_at\").notNull().default(nowDefault),\n updatedAt: text(\"updated_at\").notNull().default(nowDefault),\n});\n\nexport const runs = sqliteTable(\"runs\", {\n id: text(\"id\").primaryKey(),\n taskId: text(\"task_id\").notNull().references(() => tasks.id),\n nodeId: text(\"node_id\").notNull().references(() => nodes.id),\n attempt: integer(\"attempt\").notNull(),\n status: text(\"status\").notNull(),\n startedAt: text(\"started_at\"),\n endedAt: text(\"ended_at\"),\n error: text(\"error\"),\n createdAt: text(\"created_at\").notNull().default(nowDefault),\n updatedAt: text(\"updated_at\").notNull().default(nowDefault),\n}, (table) => ({\n taskIdIdx: index(\"idx_runs_task_id\").on(table.taskId),\n nodeIdIdx: index(\"idx_runs_node_id\").on(table.nodeId),\n}));\n\nexport const runLogs = sqliteTable(\"run_logs\", {\n id: integer(\"id\").primaryKey({ autoIncrement: true }),\n runId: text(\"run_id\").notNull().references(() => runs.id),\n stepName: text(\"step_name\"),\n stream: text(\"stream\").notNull(),\n message: text(\"message\").notNull(),\n timestamp: text(\"timestamp\").notNull().default(nowDefault),\n}, (table) => ({\n runIdTimestampIdx: index(\"idx_run_logs_run_id_timestamp\").on(table.runId, table.timestamp),\n}));\n\nexport const artifacts = sqliteTable(\"artifacts\", {\n id: text(\"id\").primaryKey(),\n runId: text(\"run_id\").notNull().references(() => runs.id),\n type: text(\"type\").notNull(),\n name: text(\"name\").notNull(),\n path: text(\"path\").notNull(),\n metadata: text(\"metadata\", { mode: \"json\" }).$type<Record<string, unknown>>(),\n createdAt: text(\"created_at\").notNull().default(nowDefault),\n}, (table) => ({\n runIdIdx: index(\"idx_artifacts_run_id\").on(table.runId),\n}));\n\nexport const runInputEvents = sqliteTable(\"run_input_events\", {\n id: integer(\"id\").primaryKey({ autoIncrement: true }),\n runId: text(\"run_id\").notNull().references(() => runs.id),\n sequence: integer(\"sequence\").notNull(),\n kind: text(\"kind\").notNull(),\n payload: text(\"payload\", { mode: \"json\" }).$type<Record<string, unknown>>().notNull().default({}),\n createdAt: text(\"created_at\").notNull().default(nowDefault),\n deliveredAt: text(\"delivered_at\"),\n}, (table) => ({\n runSequenceUniqueIdx: uniqueIndex(\"idx_run_input_events_run_sequence\").on(table.runId, table.sequence),\n undeliveredIdx: index(\"idx_run_input_events_undelivered\").on(table.runId, table.sequence).where(isNull(table.deliveredAt)),\n}));\n","import { and, asc, desc, eq, gt, sql } from \"drizzle-orm\";\nimport type { BetterSQLite3Database } from \"drizzle-orm/better-sqlite3\";\nimport { makeId } from \"../../core/src/ids.js\";\nimport { artifacts, nodes, runInputEvents, runLogs, runs, tasks } from \"./schema/sqlite.js\";\nimport type * as sqliteSchema from \"./schema/sqlite.js\";\nimport type { WorkplaneStore } from \"./store-interface.js\";\nimport type {\n AppendInputEventInput,\n ArtifactInput,\n ArtifactRecord,\n CreateTaskInput,\n NodePollResult,\n NodeRecord,\n RunInputEvent,\n RunLogInput,\n RunLogRecord,\n RunRecord,\n TaskRecord,\n} from \"../../types/src/index.js\";\n\ntype SqliteDb = BetterSQLite3Database<typeof sqliteSchema>;\n\nfunction nowIso(): string {\n return new Date().toISOString();\n}\n\n// Drizzle-typed mappers for simple CRUD methods (camelCase, text timestamps)\n\nfunction toTaskRecord(row: typeof tasks.$inferSelect): TaskRecord {\n return {\n id: row.id,\n kind: row.kind,\n adapter: row.adapter,\n payload: row.payload,\n requires: row.requires ?? [],\n status: row.status as TaskRecord[\"status\"],\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nfunction toRunRecord(row: typeof runs.$inferSelect): RunRecord {\n return {\n id: row.id,\n taskId: row.taskId,\n nodeId: row.nodeId,\n attempt: row.attempt,\n status: row.status as RunRecord[\"status\"],\n startedAt: row.startedAt ?? null,\n endedAt: row.endedAt ?? null,\n error: row.error ?? null,\n };\n}\n\nfunction toNodeRecord(row: typeof nodes.$inferSelect): NodeRecord {\n return {\n id: row.id,\n name: row.name,\n capabilities: row.capabilities ?? [],\n status: row.status as NodeRecord[\"status\"],\n lastHeartbeatAt: row.lastHeartbeatAt ?? null,\n };\n}\n\nfunction toLogRecord(row: typeof runLogs.$inferSelect): RunLogRecord {\n return {\n id: row.id,\n runId: row.runId,\n stepName: row.stepName ?? null,\n stream: row.stream as RunLogRecord[\"stream\"],\n message: row.message,\n timestamp: row.timestamp,\n };\n}\n\nfunction toArtifactRecord(row: typeof artifacts.$inferSelect): ArtifactRecord {\n return {\n id: row.id,\n runId: row.runId,\n type: row.type,\n name: row.name,\n path: row.path,\n metadata: row.metadata ?? null,\n createdAt: row.createdAt,\n };\n}\n\nfunction toInputEventRecord(row: typeof runInputEvents.$inferSelect): RunInputEvent {\n return {\n id: row.id,\n runId: row.runId,\n sequence: row.sequence,\n kind: row.kind as RunInputEvent[\"kind\"],\n payload: row.payload,\n createdAt: row.createdAt,\n deliveredAt: row.deliveredAt ?? null,\n };\n}\n\n// Raw-row mappers for transaction methods that use $client directly.\n// better-sqlite3 rejects async transaction callbacks so we use the sync\n// $client API with prepared statements for the 4 methods that need atomicity.\n\ninterface RawTaskRow {\n id: string; kind: string; adapter: string;\n payload: string; requires: string; status: string;\n created_at: string; updated_at: string;\n}\n\ninterface RawRunRow {\n id: string; task_id: string; node_id: string; attempt: number; status: string;\n started_at: string | null; ended_at: string | null; error: string | null;\n created_at: string; updated_at: string;\n}\n\nfunction mapRawTask(row: RawTaskRow): TaskRecord {\n return {\n id: row.id, kind: row.kind, adapter: row.adapter,\n payload: JSON.parse(row.payload) as Record<string, unknown>,\n requires: JSON.parse(row.requires) as string[],\n status: row.status as TaskRecord[\"status\"],\n createdAt: row.created_at, updatedAt: row.updated_at,\n };\n}\n\nfunction mapRawRun(row: RawRunRow): RunRecord {\n return {\n id: row.id, taskId: row.task_id, nodeId: row.node_id,\n attempt: row.attempt, status: row.status as RunRecord[\"status\"],\n startedAt: row.started_at ?? null, endedAt: row.ended_at ?? null,\n error: row.error ?? null,\n };\n}\n\nexport class SqliteStore implements WorkplaneStore {\n constructor(private readonly db: SqliteDb) {}\n\n async createTask(input: CreateTaskInput): Promise<TaskRecord> {\n const now = nowIso();\n const [row] = await this.db.insert(tasks).values({\n id: makeId(\"task\"),\n kind: input.kind,\n adapter: input.adapter,\n payload: input.payload,\n requires: input.requires ?? [],\n status: \"queued\",\n createdAt: now,\n updatedAt: now,\n }).returning();\n return toTaskRecord(row);\n }\n\n async listTasks(status?: TaskRecord[\"status\"]): Promise<TaskRecord[]> {\n const rows = await this.db.select().from(tasks)\n .where(status ? eq(tasks.status, status) : undefined)\n .orderBy(desc(tasks.createdAt)).limit(100);\n return rows.map(toTaskRecord);\n }\n\n async getTask(taskId: string): Promise<TaskRecord | null> {\n const [row] = await this.db.select().from(tasks).where(eq(tasks.id, taskId));\n return row ? toTaskRecord(row) : null;\n }\n\n async listRuns(filters?: { taskId?: string; status?: RunRecord[\"status\"] }): Promise<RunRecord[]> {\n const conds = [];\n if (filters?.taskId) conds.push(eq(runs.taskId, filters.taskId));\n if (filters?.status) conds.push(eq(runs.status, filters.status));\n const rows = await this.db.select().from(runs)\n .where(conds.length ? and(...conds) : undefined)\n .orderBy(desc(runs.createdAt)).limit(100);\n return rows.map(toRunRecord);\n }\n\n async getRun(runId: string): Promise<RunRecord | null> {\n const [row] = await this.db.select().from(runs).where(eq(runs.id, runId));\n return row ? toRunRecord(row) : null;\n }\n\n async registerNode(name: string, capabilities: string[], preferredId?: string): Promise<NodeRecord> {\n const now = nowIso();\n const [existing] = await this.db.select().from(nodes).where(eq(nodes.name, name));\n if (existing) {\n const [updated] = await this.db.update(nodes)\n .set({ capabilities, status: \"online\", lastHeartbeatAt: now, updatedAt: now })\n .where(eq(nodes.id, existing.id)).returning();\n return toNodeRecord(updated);\n }\n const [inserted] = await this.db.insert(nodes).values({\n id: preferredId ?? makeId(\"node\"), name, capabilities,\n status: \"online\", lastHeartbeatAt: now, createdAt: now, updatedAt: now,\n }).returning();\n return toNodeRecord(inserted);\n }\n\n async getRunCancellationState(runId: string): Promise<{ runStatus: string; taskStatus: string } | null> {\n const [row] = await this.db\n .select({ runStatus: runs.status, taskStatus: tasks.status })\n .from(runs).innerJoin(tasks, eq(tasks.id, runs.taskId)).where(eq(runs.id, runId));\n return row ? { runStatus: row.runStatus, taskStatus: row.taskStatus ?? \"\" } : null;\n }\n\n // Transaction methods use this.db.$client directly because better-sqlite3\n // rejects async callbacks — all operations inside must be synchronous.\n\n async pollNode(nodeId: string, capabilities: string[]): Promise<NodePollResult | null> {\n const now = nowIso();\n await this.db.update(nodes)\n .set({ capabilities, status: \"online\", lastHeartbeatAt: now, updatedAt: now })\n .where(eq(nodes.id, nodeId));\n\n const raw = this.db.$client;\n return raw.transaction(() => {\n const capSet = new Set(capabilities);\n const queued = raw.prepare(\n \"SELECT * FROM tasks WHERE status = 'queued' ORDER BY created_at ASC\",\n ).all() as RawTaskRow[];\n\n const task = queued.find((t) =>\n (JSON.parse(t.requires) as string[]).every((r) => capSet.has(r)),\n );\n if (!task) return null;\n\n const { max_attempt } = raw.prepare(\n \"SELECT COALESCE(MAX(attempt), 0) AS max_attempt FROM runs WHERE task_id = ?\",\n ).get(task.id) as { max_attempt: number };\n\n const runId = makeId(\"run\");\n raw.prepare(\n \"INSERT INTO runs (id, task_id, node_id, attempt, status, started_at, created_at, updated_at) VALUES (?, ?, ?, ?, 'assigned', ?, ?, ?)\",\n ).run(runId, task.id, nodeId, max_attempt + 1, now, now, now);\n\n raw.prepare(\"UPDATE tasks SET status = 'assigned', updated_at = ? WHERE id = ?\").run(now, task.id);\n\n return {\n task: mapRawTask(task),\n run: mapRawRun(raw.prepare(\"SELECT * FROM runs WHERE id = ?\").get(runId) as RawRunRow),\n };\n })();\n }\n\n async updateRunStatus(runId: string, status: RunRecord[\"status\"], error?: string): Promise<RunRecord | null> {\n const raw = this.db.$client;\n return raw.transaction(() => {\n const current = raw.prepare(\n \"SELECT r.*, t.status AS task_status FROM runs r JOIN tasks t ON t.id = r.task_id WHERE r.id = ?\",\n ).get(runId) as (RawRunRow & { task_status: string }) | undefined;\n if (!current) return null;\n\n if (current.task_status === \"cancelled\" && status !== \"cancelled\") {\n return mapRawRun(current);\n }\n\n const now = nowIso();\n const isTerminal = status === \"succeeded\" || status === \"failed\" || status === \"cancelled\";\n if (isTerminal) {\n raw.prepare(\n \"UPDATE runs SET status = ?, error = ?, ended_at = ?, updated_at = ? WHERE id = ?\",\n ).run(status, error ?? null, now, now, runId);\n } else {\n raw.prepare(\n \"UPDATE runs SET status = ?, error = ?, updated_at = ? WHERE id = ?\",\n ).run(status, error ?? null, now, runId);\n }\n\n const updatedRun = raw.prepare(\"SELECT * FROM runs WHERE id = ?\").get(runId) as RawRunRow;\n const nextTaskStatus = isTerminal ? status : \"running\";\n if (current.task_status !== \"cancelled\" || status === \"cancelled\") {\n raw.prepare(\"UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?\").run(\n nextTaskStatus, now, updatedRun.task_id,\n );\n }\n return mapRawRun(updatedRun);\n })();\n }\n\n async appendRunLogs(runId: string, logs: RunLogInput[]): Promise<number> {\n if (logs.length === 0) return 0;\n const now = nowIso();\n await this.db.insert(runLogs).values(\n logs.map((log) => ({\n runId, stepName: log.stepName ?? null,\n stream: log.stream, message: log.message, timestamp: now,\n })),\n );\n return logs.length;\n }\n\n async getRunLogs(runId: string): Promise<RunLogRecord[]> {\n const rows = await this.db.select().from(runLogs)\n .where(eq(runLogs.runId, runId)).orderBy(asc(runLogs.timestamp));\n return rows.map(toLogRecord);\n }\n\n async createArtifact(runId: string, input: ArtifactInput): Promise<ArtifactRecord> {\n const now = nowIso();\n const [row] = await this.db.insert(artifacts).values({\n id: makeId(\"artifact\"), runId, type: input.type, name: input.name,\n path: input.path, metadata: input.metadata ?? null, createdAt: now,\n }).returning();\n return toArtifactRecord(row);\n }\n\n async listRunArtifacts(runId: string): Promise<ArtifactRecord[]> {\n const rows = await this.db.select().from(artifacts)\n .where(eq(artifacts.runId, runId)).orderBy(asc(artifacts.createdAt));\n return rows.map(toArtifactRecord);\n }\n\n async retryTask(taskId: string): Promise<TaskRecord | null> {\n const [row] = await this.db.update(tasks)\n .set({ status: \"queued\", updatedAt: nowIso() })\n .where(and(eq(tasks.id, taskId), eq(tasks.status, \"failed\"))).returning();\n return row ? toTaskRecord(row) : null;\n }\n\n async cancelTask(taskId: string): Promise<TaskRecord | null> {\n const raw = this.db.$client;\n return raw.transaction(() => {\n const taskRow = raw.prepare(\"SELECT * FROM tasks WHERE id = ?\").get(taskId) as RawTaskRow | undefined;\n if (!taskRow) return null;\n\n const terminal = new Set([\"succeeded\", \"failed\", \"cancelled\"]);\n if (terminal.has(taskRow.status)) return null;\n\n const now = nowIso();\n if (taskRow.status === \"assigned\" || taskRow.status === \"running\") {\n const latestRun = raw.prepare(\n \"SELECT * FROM runs WHERE task_id = ? ORDER BY attempt DESC LIMIT 1\",\n ).get(taskId) as RawRunRow | undefined;\n\n if (latestRun && !terminal.has(latestRun.status)) {\n raw.prepare(\n \"UPDATE runs SET status = 'cancelled', ended_at = ?, updated_at = ?, error = COALESCE(error, 'task cancelled') WHERE id = ?\",\n ).run(now, now, latestRun.id);\n }\n }\n\n raw.prepare(\"UPDATE tasks SET status = 'cancelled', updated_at = ? WHERE id = ?\").run(now, taskId);\n return mapRawTask(raw.prepare(\"SELECT * FROM tasks WHERE id = ?\").get(taskId) as RawTaskRow);\n })();\n }\n\n async appendInputEvent(runId: string, input: AppendInputEventInput): Promise<RunInputEvent> {\n const raw = this.db.$client;\n const now = nowIso();\n const event = raw.transaction(() => {\n const { next_seq } = raw.prepare(\n \"SELECT COALESCE(MAX(sequence), 0) + 1 AS next_seq FROM run_input_events WHERE run_id = ?\",\n ).get(runId) as { next_seq: number };\n\n raw.prepare(\n \"INSERT INTO run_input_events (run_id, sequence, kind, payload, created_at) VALUES (?, ?, ?, ?, ?)\",\n ).run(runId, next_seq, input.kind, JSON.stringify(input.payload), now);\n\n return raw.prepare(\n \"SELECT * FROM run_input_events WHERE run_id = ? AND sequence = ?\",\n ).get(runId, next_seq) as {\n id: number; run_id: string; sequence: number; kind: string;\n payload: string; created_at: string; delivered_at: string | null;\n };\n })();\n\n return {\n id: event.id, runId: event.run_id, sequence: event.sequence,\n kind: event.kind as RunInputEvent[\"kind\"],\n payload: JSON.parse(event.payload) as Record<string, unknown>,\n createdAt: event.created_at, deliveredAt: event.delivered_at ?? null,\n };\n }\n\n async getInputEvents(runId: string, afterSequence: number): Promise<RunInputEvent[]> {\n const rows = await this.db.select().from(runInputEvents)\n .where(and(eq(runInputEvents.runId, runId), gt(runInputEvents.sequence, afterSequence)))\n .orderBy(asc(runInputEvents.sequence));\n return rows.map(toInputEventRecord);\n }\n\n async markInputDelivered(runId: string, sequence: number): Promise<void> {\n await this.db.update(runInputEvents)\n .set({ deliveredAt: nowIso() })\n .where(and(eq(runInputEvents.runId, runId), eq(runInputEvents.sequence, sequence)));\n }\n}\n","import { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { drizzle as drizzlePg } from \"drizzle-orm/node-postgres\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { drizzle as drizzleSqlite } from \"drizzle-orm/better-sqlite3\";\nimport Database from \"better-sqlite3\";\nimport { Pool } from \"pg\";\nimport { PgStore } from \"./pg-store.js\";\nimport { SqliteStore } from \"./sqlite-store.js\";\nimport * as pgSchema from \"./schema/pg.js\";\nimport * as sqliteSchema from \"./schema/sqlite.js\";\nimport type { WorkplaneStore } from \"./store-interface.js\";\n\nexport type DrizzleDb = NodePgDatabase<typeof pgSchema>;\n\nexport interface StoreHandle {\n store: WorkplaneStore;\n close: () => Promise<void>;\n}\n\nconst pools = new Map<string, Pool>();\n\nexport function getPool(databaseUrl: string): Pool {\n const existing = pools.get(databaseUrl);\n if (existing) return existing;\n const pool = new Pool({ connectionString: databaseUrl });\n pools.set(databaseUrl, pool);\n return pool;\n}\n\nexport function getDrizzle(pool: Pool): DrizzleDb {\n return drizzlePg(pool, { schema: pgSchema });\n}\n\nfunction parseSqlitePath(url: string): string {\n const path = url.startsWith(\"sqlite://\") ? url.slice(\"sqlite://\".length) : url;\n if (!path) return join(homedir(), \".workplane\", \"workplane.db\");\n if (path.startsWith(\"~/\")) return join(homedir(), path.slice(2));\n return path;\n}\n\nexport function createStore(databaseUrl?: string): StoreHandle {\n const url = databaseUrl ?? \"\";\n\n if (url.startsWith(\"postgres://\") || url.startsWith(\"postgresql://\")) {\n const pool = getPool(url);\n return {\n store: new PgStore(getDrizzle(pool)),\n close: () => pool.end(),\n };\n }\n\n const filePath = parseSqlitePath(url);\n mkdirSync(dirname(filePath), { recursive: true });\n const sqlite = new Database(filePath);\n sqlite.pragma(\"journal_mode = WAL\");\n sqlite.pragma(\"synchronous = NORMAL\");\n const db = drizzleSqlite(sqlite, { schema: sqliteSchema });\n return {\n store: new SqliteStore(db),\n close: async () => { sqlite.close(); },\n };\n}\n","// Side-effect-free module — safe to import from both the migrate CLI and setup wizard.\nimport { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { getDrizzle, getPool } from \"./client.js\";\n\nconst distDir = dirname(fileURLToPath(import.meta.url));\n\nasync function migratePostgres(databaseUrl: string): Promise<void> {\n const adminUrl = new URL(databaseUrl);\n const dbName = adminUrl.pathname.replace(/^\\//, \"\");\n if (!dbName) throw new Error(\"DATABASE_URL must include a database name\");\n\n adminUrl.pathname = \"/postgres\";\n const adminPool = getPool(adminUrl.toString());\n try {\n const { rowCount } = await adminPool.query(\n \"SELECT 1 FROM pg_database WHERE datname = $1\",\n [dbName],\n );\n if (!rowCount) {\n await adminPool.query(`CREATE DATABASE \"${dbName.replaceAll('\"', '\"\"')}\"`);\n process.stdout.write(`Created database: ${dbName}\\n`);\n }\n } catch (err) {\n const e = err as { code?: string };\n if (e.code === \"42501\") {\n throw new Error(\n `Database \"${dbName}\" does not exist and the current role lacks CREATE DATABASE permissions.`,\n );\n }\n throw err;\n } finally {\n await adminPool.end();\n }\n\n const pool = getPool(databaseUrl);\n const { migrate } = await import(\"drizzle-orm/node-postgres/migrator\");\n await migrate(getDrizzle(pool), { migrationsFolder: resolve(distDir, \"migrations/pg\") });\n await pool.end();\n process.stdout.write(\"Postgres migrations applied.\\n\");\n}\n\nasync function migrateSqlite(databaseUrl: string): Promise<void> {\n const { default: Database } = await import(\"better-sqlite3\");\n const { drizzle } = await import(\"drizzle-orm/better-sqlite3\");\n const { migrate } = await import(\"drizzle-orm/better-sqlite3/migrator\");\n const sqliteSchema = await import(\"./schema/sqlite.js\");\n\n const rawPath = databaseUrl.startsWith(\"sqlite://\")\n ? databaseUrl.slice(\"sqlite://\".length)\n : databaseUrl;\n const filePath = !rawPath\n ? join(homedir(), \".workplane\", \"workplane.db\")\n : rawPath.startsWith(\"~/\") ? join(homedir(), rawPath.slice(2)) : rawPath;\n\n mkdirSync(dirname(filePath), { recursive: true });\n const sqlite = new Database(filePath);\n sqlite.pragma(\"journal_mode = WAL\");\n sqlite.pragma(\"synchronous = NORMAL\");\n const db = drizzle(sqlite, { schema: sqliteSchema });\n migrate(db, { migrationsFolder: resolve(distDir, \"migrations/sqlite\") });\n sqlite.close();\n process.stdout.write(`SQLite migrations applied: ${filePath}\\n`);\n}\n\nexport async function runMigration(databaseUrl: string): Promise<void> {\n if (databaseUrl.startsWith(\"postgres://\") || databaseUrl.startsWith(\"postgresql://\")) {\n await migratePostgres(databaseUrl);\n } else {\n await migrateSqlite(databaseUrl);\n }\n}\n","import { randomBytes } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createInterface } from \"node:readline/promises\";\n\nconst workplaneDir = join(homedir(), \".workplane\");\nconst configPath = join(workplaneDir, \".env\");\n\nfunction generateToken(): string {\n return randomBytes(32).toString(\"hex\");\n}\n\nfunction parseEnvFile(path: string): Record<string, string> {\n try {\n const result: Record<string, string> = {};\n for (const line of readFileSync(path, \"utf8\").split(\"\\n\")) {\n const t = line.trim();\n if (!t || t.startsWith(\"#\")) continue;\n const eq = t.indexOf(\"=\");\n if (eq !== -1) result[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();\n }\n return result;\n } catch {\n return {};\n }\n}\n\nasync function ask(\n rl: Awaited<ReturnType<typeof createInterface>>,\n label: string,\n def: string,\n): Promise<string> {\n const answer = await rl.question(` ${label} [${def}]: `);\n return answer.trim() || def;\n}\n\nasync function main(): Promise<void> {\n const existing = parseEnvFile(configPath);\n const isUpdate = existsSync(configPath);\n\n process.stdout.write(\"Workplane Setup\\n\");\n process.stdout.write(\n isUpdate\n ? `Updating config at ${configPath}\\n`\n : `Creating config at ${configPath}\\n`,\n );\n process.stdout.write(\"Press Enter to accept the value shown in brackets.\\n\\n\");\n\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n\n const dbUrl = await ask(\n rl, \"DATABASE_URL \",\n existing.DATABASE_URL ?? \"sqlite://~/.workplane/workplane.db\",\n );\n const port = await ask(\n rl, \"Server port \",\n existing.WORKPLANE_SERVER_PORT ?? \"8787\",\n );\n const serverUrl = await ask(\n rl, \"Server URL (for nodes)\",\n existing.WORKPLANE_SERVER_URL ?? `http://localhost:${port}`,\n );\n const nodeToken = await ask(\n rl, \"Node token \",\n existing.WORKPLANE_NODE_TOKEN ?? generateToken(),\n );\n const operatorToken = await ask(\n rl, \"Operator token \",\n existing.WORKPLANE_OPERATOR_TOKEN ?? generateToken(),\n );\n\n rl.close();\n\n const config = [\n `DATABASE_URL=${dbUrl}`,\n `WORKPLANE_SERVER_PORT=${port}`,\n `WORKPLANE_SERVER_URL=${serverUrl}`,\n `WORKPLANE_NODE_TOKEN=${nodeToken}`,\n `WORKPLANE_OPERATOR_TOKEN=${operatorToken}`,\n ].join(\"\\n\") + \"\\n\";\n\n mkdirSync(workplaneDir, { recursive: true });\n writeFileSync(configPath, config);\n process.stdout.write(`\\nConfig saved: ${configPath}\\n`);\n\n process.stdout.write(\"Running migrations...\\n\");\n const { runMigration } = await import(\"../../db/src/migration.js\");\n await runMigration(dbUrl);\n\n process.stdout.write(`\nReady. Start your fleet:\n\n workplane-server # control plane\n workplane-node # worker (this machine or another)\n workplane tasks # inspect submitted work\n\nTokens for remote machines:\n WORKPLANE_NODE_TOKEN=${nodeToken}\n WORKPLANE_OPERATOR_TOKEN=${operatorToken}\n`);\n}\n\nmain().catch((error) => {\n process.stderr.write(`${String(error)}\\n`);\n process.exit(1);\n});\n"],"mappings":"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","names":["sql","artifacts","nodes","runInputEvents","runLogs","runs","tasks","isNull","sql","index","integer","text","uniqueIndex","and","asc","desc","eq","gt","mkdirSync","homedir","dirname","join","Database","mkdirSync","homedir","join","eq","runMigration"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workplane",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Durable execution plane for routing work across trusted nodes",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,7 +26,8 @@
26
26
  "workplane": "dist/cli.js",
27
27
  "workplane-server": "dist/server.js",
28
28
  "workplane-node": "dist/node.js",
29
- "workplane-db-migrate": "dist/migrate.js"
29
+ "workplane-db-migrate": "dist/migrate.js",
30
+ "workplane-setup": "dist/setup.js"
30
31
  },
31
32
  "files": [
32
33
  "dist",
@@ -38,9 +39,14 @@
38
39
  "typescript": "^5.8.3"
39
40
  },
40
41
  "dependencies": {
42
+ "@anthropic-ai/sdk": "^0.54.0",
41
43
  "@dbos-inc/dbos-sdk": "^4.18.10",
44
+ "better-sqlite3": "^12.10.0",
42
45
  "dotenv": "^16.6.1",
46
+ "drizzle-orm": "^0.45.2",
43
47
  "node-pty": "^1.1.0",
48
+ "ollama": "^0.5.16",
49
+ "openai": "^4.103.0",
44
50
  "pg": "^8.16.0",
45
51
  "zod": "^3.25.56"
46
52
  },