workplane 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MindStaq
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # workplane
2
+
3
+ Route durable work—shell commands, local inference (Ollama), and batch agent harness jobs (Aider, Codex, Claude Code)—to capable machines on your private network.
4
+
5
+ Workplane is a control plane + polling node runtime backed by Postgres and [DBOS](https://www.dbos.dev/). You submit tasks from a CLI; nodes pull work, run it in isolated workspaces, and report logs and artifacts.
6
+
7
+ **Requires Node.js 20+** and **Postgres**.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install -g workplane
13
+ ```
14
+
15
+ ### Binaries
16
+
17
+ | Command | Purpose |
18
+ |---------|---------|
19
+ | `workplane` | CLI — submit and inspect tasks |
20
+ | `workplane-server` | Control plane API |
21
+ | `workplane-node` | Worker that polls and executes tasks |
22
+ | `workplane-db-migrate` | Apply database schema |
23
+
24
+ ## Quick start (single machine)
25
+
26
+ **1. Postgres** — running locally or reachable via `DATABASE_URL`.
27
+
28
+ **2. Environment** — create a `.env` file or export:
29
+
30
+ ```bash
31
+ export DATABASE_URL=postgres://postgres:postgres@localhost:5432/workplane
32
+ export WORKPLANE_SERVER_URL=http://localhost:8787
33
+
34
+ # Recommended when the server is not localhost-only:
35
+ export WORKPLANE_NODE_TOKEN=your-node-secret
36
+ export WORKPLANE_OPERATOR_TOKEN=your-operator-secret
37
+ ```
38
+
39
+ **3. Migrate, server, node** (three terminals):
40
+
41
+ ```bash
42
+ workplane-db-migrate
43
+ workplane-server
44
+ workplane-node
45
+ ```
46
+
47
+ **4. Submit a task** (fourth terminal):
48
+
49
+ ```bash
50
+ export WORKPLANE_OPERATOR_TOKEN=your-operator-secret # if auth enabled on server
51
+
52
+ workplane task submit shell --command "echo hello"
53
+ workplane tasks
54
+ workplane runs
55
+ workplane logs <runId>
56
+ ```
57
+
58
+ ## Authentication
59
+
60
+ When `WORKPLANE_NODE_TOKEN` is set on the **server**, every node must use the same value. When `WORKPLANE_OPERATOR_TOKEN` is set, the CLI must send it for `task submit`, `retry`, and `cancel`.
61
+
62
+ | Variable | Used by |
63
+ |----------|---------|
64
+ | `WORKPLANE_NODE_TOKEN` | `workplane-node` |
65
+ | `WORKPLANE_OPERATOR_TOKEN` | `workplane` CLI (mutating commands) |
66
+
67
+ Read-only CLI commands (`tasks`, `logs`, `runs`, …) work without the operator token unless you add stricter rules in your deployment.
68
+
69
+ ## CLI examples
70
+
71
+ ```bash
72
+ # Shell
73
+ workplane task submit shell --command "npm test" \
74
+ --repo https://github.com/you/your-repo.git
75
+
76
+ # Local inference (node must advertise ollama capability)
77
+ workplane task submit inference --model llama3.2 --prompt "Say hello"
78
+
79
+ # Agent harness (node must have codex or claude-code installed)
80
+ workplane task submit harness --harness codex \
81
+ --repo git@github.com:you/app.git \
82
+ --prompt "Fix failing tests"
83
+
84
+ workplane task submit aider \
85
+ --repo git@github.com:you/app.git \
86
+ --prompt "Fix the bug" \
87
+ --test-command "npm test"
88
+
89
+ workplane task retry <taskId>
90
+ workplane task cancel <taskId>
91
+ workplane artifacts <runId>
92
+ ```
93
+
94
+ ## Node capabilities
95
+
96
+ Register capabilities when starting a node:
97
+
98
+ ```bash
99
+ export WORKPLANE_NODE_CAPABILITIES=shell,git,ollama,aider,codex,claude-code
100
+ workplane-node
101
+ ```
102
+
103
+ Tasks declare `requires`; the control plane assigns work to a node whose capabilities are a superset.
104
+
105
+ | Adapter | Typical capabilities |
106
+ |---------|----------------------|
107
+ | Shell | `shell` |
108
+ | Ollama | `ollama` |
109
+ | Aider | `git`, `aider` |
110
+ | Codex | `git`, `codex` |
111
+ | Claude Code | `git`, `claude-code` |
112
+
113
+ Override harness binaries: `WORKPLANE_CODEX_BIN`, `WORKPLANE_CLAUDE_CODE_BIN`.
114
+
115
+ ## Personal fleet (home + office)
116
+
117
+ Run the control plane on an always-on host (VPS, NAS, or home server). Run nodes on each machine where tools or GPUs live. Connect machines with Tailscale, WireGuard, or a trusted LAN.
118
+
119
+ Set `WORKPLANE_SERVER_URL` to the control plane’s reachable URL on every node and CLI client.
120
+
121
+ Full deployment guide: [github.com/MindStaq/workplane/blob/main/docs/deployment/FLEET.md](https://github.com/MindStaq/workplane/blob/main/docs/deployment/FLEET.md)
122
+
123
+ ## Documentation
124
+
125
+ - [v0.1.0 specification](https://github.com/MindStaq/workplane/blob/main/docs/specs/v0.1.0/WORKPLANE_SPEC.md)
126
+ - [Source repository](https://github.com/MindStaq/workplane)
127
+ - [Issues](https://github.com/MindStaq/workplane/issues)
128
+
129
+ ## License
130
+
131
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../core/src/env.ts
4
+ import { config as dotenvConfig } from "dotenv";
5
+ var envLoaded = false;
6
+ function loadLocalEnv() {
7
+ if (envLoaded) {
8
+ return;
9
+ }
10
+ dotenvConfig({ path: ".env" });
11
+ dotenvConfig({ path: ".env.local", override: true });
12
+ envLoaded = true;
13
+ }
14
+
15
+ // ../core/src/config.ts
16
+ import { z } from "zod";
17
+ loadLocalEnv();
18
+ var serverEnvSchema = z.object({
19
+ DATABASE_URL: z.string().min(1),
20
+ WORKPLANE_SERVER_PORT: z.string().optional().default("8787").transform((value) => Number.parseInt(value, 10)),
21
+ WORKPLANE_NODE_TOKEN: z.string().optional(),
22
+ WORKPLANE_OPERATOR_TOKEN: z.string().optional(),
23
+ WORKPLANE_ENV_ALLOWLIST: z.string().optional()
24
+ });
25
+ var nodeEnvSchema = z.object({
26
+ WORKPLANE_SERVER_URL: z.string().url().optional().default("http://localhost:8787"),
27
+ WORKPLANE_NODE_NAME: z.string().optional().default("local-node-1"),
28
+ WORKPLANE_NODE_ID: z.string().optional(),
29
+ WORKPLANE_NODE_CAPABILITIES: z.string().optional().default("shell,git,node,typescript,aider,ollama,codex,claude-code"),
30
+ WORKPLANE_NODE_TOKEN: z.string().optional(),
31
+ WORKPLANE_POLL_INTERVAL_MS: z.string().optional().default("3000").transform((value) => Number.parseInt(value, 10)),
32
+ WORKPLANE_ENV_ALLOWLIST: z.string().optional()
33
+ });
34
+ function parseCsv(value) {
35
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean);
36
+ }
37
+
38
+ // ../core/src/auth.ts
39
+ function authHeaders(token) {
40
+ if (!token) {
41
+ return {};
42
+ }
43
+ return { authorization: `Bearer ${token}` };
44
+ }
45
+
46
+ // ../core/src/http-client.ts
47
+ function buildWorkplaneHeaders(options = {}) {
48
+ const token = options.nodeToken ?? options.operatorToken;
49
+ return {
50
+ "content-type": "application/json",
51
+ ...authHeaders(token)
52
+ };
53
+ }
54
+ async function workplaneFetch(serverUrl2, path, options = {}) {
55
+ const response = await fetch(`${serverUrl2}${path}`, {
56
+ method: options.method ?? "GET",
57
+ headers: buildWorkplaneHeaders(options),
58
+ body: options.body ? JSON.stringify(options.body) : void 0
59
+ });
60
+ if (!response.ok) {
61
+ const text = await response.text();
62
+ throw new Error(`${response.status} ${response.statusText} from ${path}: ${text}`);
63
+ }
64
+ return await response.json();
65
+ }
66
+
67
+ // ../cli/src/index.ts
68
+ loadLocalEnv();
69
+ var serverUrl = process.env.WORKPLANE_SERVER_URL ?? "http://localhost:8787";
70
+ var operatorToken = process.env.WORKPLANE_OPERATOR_TOKEN;
71
+ async function httpJson(path, options) {
72
+ return workplaneFetch(serverUrl, path, {
73
+ method: options?.method,
74
+ body: options?.body,
75
+ operatorToken: options?.operator ? operatorToken : void 0
76
+ });
77
+ }
78
+ function valueArg(args, flag) {
79
+ const index = args.indexOf(flag);
80
+ if (index === -1) {
81
+ return void 0;
82
+ }
83
+ return args[index + 1];
84
+ }
85
+ function printJson(payload) {
86
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
87
+ `);
88
+ }
89
+ async function resolveRunIdFromTaskId(taskId) {
90
+ const runs = await httpJson(`/runs?taskId=${encodeURIComponent(taskId)}`);
91
+ const runId = runs.runs[0]?.id;
92
+ if (!runId) {
93
+ throw new Error(`no runs found for task ${taskId}`);
94
+ }
95
+ return runId;
96
+ }
97
+ async function showLogsById(id) {
98
+ const runId = id.startsWith("task_") ? await resolveRunIdFromTaskId(id) : id;
99
+ const logs = await httpJson(`/runs/${runId}/logs`);
100
+ printJson(logs);
101
+ }
102
+ function usage() {
103
+ process.stdout.write(
104
+ [
105
+ "workplane CLI",
106
+ "",
107
+ "Env: WORKPLANE_SERVER_URL, WORKPLANE_OPERATOR_TOKEN (for task submit/retry/cancel)",
108
+ "",
109
+ "Commands:",
110
+ " server start / node start # use pnpm dev:server / dev:node",
111
+ " task submit shell --command <cmd> [--repo <url>] [--branch <b>] [--cwd <path>]",
112
+ " task submit aider --prompt <text> --repo <url> [--model <m>] [--test-command <cmd>]",
113
+ " task submit inference --model <m> --prompt <text> [--requires ollama]",
114
+ " task submit harness --harness <codex|claude-code> --repo <url> --prompt <text>",
115
+ " tasks [--status <status>]",
116
+ " task show|logs|retry|cancel <taskId>",
117
+ " runs [--task-id <id>] [--status <status>]",
118
+ " run show <runId>",
119
+ " logs <runId|taskId>",
120
+ " artifacts <runId>"
121
+ ].join("\n")
122
+ );
123
+ }
124
+ async function submitShell(args) {
125
+ const command = valueArg(args, "--command");
126
+ if (!command) {
127
+ throw new Error("--command is required");
128
+ }
129
+ const requires = parseCsv(valueArg(args, "--requires") ?? "shell");
130
+ const repo = valueArg(args, "--repo");
131
+ const branch = valueArg(args, "--branch");
132
+ const cwd = valueArg(args, "--cwd");
133
+ const task = await httpJson("/tasks", {
134
+ method: "POST",
135
+ operator: true,
136
+ body: {
137
+ kind: "shell.exec",
138
+ adapter: "shell",
139
+ requires,
140
+ payload: { command, repo, branch, cwd }
141
+ }
142
+ });
143
+ printJson(task);
144
+ }
145
+ async function submitAider(args) {
146
+ const prompt = valueArg(args, "--prompt");
147
+ if (!prompt) {
148
+ throw new Error("--prompt is required");
149
+ }
150
+ const model = valueArg(args, "--model");
151
+ const repo = valueArg(args, "--repo");
152
+ if (!repo) {
153
+ throw new Error("--repo is required");
154
+ }
155
+ const branch = valueArg(args, "--branch");
156
+ const testCommand = valueArg(args, "--test-command");
157
+ const requires = parseCsv(valueArg(args, "--requires") ?? "git,aider");
158
+ const task = await httpJson("/tasks", {
159
+ method: "POST",
160
+ operator: true,
161
+ body: {
162
+ kind: "agent.run",
163
+ adapter: "aider",
164
+ requires,
165
+ payload: { prompt, model, repo, branch, testCommand }
166
+ }
167
+ });
168
+ printJson(task);
169
+ }
170
+ async function submitInference(args) {
171
+ const model = valueArg(args, "--model");
172
+ const prompt = valueArg(args, "--prompt");
173
+ if (!model || !prompt) {
174
+ throw new Error("--model and --prompt are required");
175
+ }
176
+ const adapter = valueArg(args, "--adapter") ?? "ollama";
177
+ const requires = parseCsv(valueArg(args, "--requires") ?? adapter);
178
+ const inputFile = valueArg(args, "--input-file");
179
+ const task = await httpJson("/tasks", {
180
+ method: "POST",
181
+ operator: true,
182
+ body: {
183
+ kind: "inference.batch",
184
+ adapter,
185
+ requires,
186
+ payload: { model, prompt, inputFile }
187
+ }
188
+ });
189
+ printJson(task);
190
+ }
191
+ async function submitHarness(args) {
192
+ const harness = valueArg(args, "--harness");
193
+ const prompt = valueArg(args, "--prompt");
194
+ const repo = valueArg(args, "--repo");
195
+ if (!harness || !prompt || !repo) {
196
+ throw new Error("--harness, --prompt, and --repo are required");
197
+ }
198
+ if (harness !== "codex" && harness !== "claude-code") {
199
+ throw new Error("--harness must be codex or claude-code");
200
+ }
201
+ const model = valueArg(args, "--model");
202
+ const branch = valueArg(args, "--branch");
203
+ const testCommand = valueArg(args, "--test-command");
204
+ const requires = parseCsv(valueArg(args, "--requires") ?? `${harness},git`);
205
+ const task = await httpJson("/tasks", {
206
+ method: "POST",
207
+ operator: true,
208
+ body: {
209
+ kind: "agent.run",
210
+ adapter: harness,
211
+ requires,
212
+ payload: { prompt, repo, model, branch, testCommand }
213
+ }
214
+ });
215
+ printJson(task);
216
+ }
217
+ async function main() {
218
+ const [command, subcommand, ...rest] = process.argv.slice(2);
219
+ if (!command) {
220
+ usage();
221
+ return;
222
+ }
223
+ if (command === "server" && subcommand === "start") {
224
+ process.stdout.write("Use: pnpm dev:server\n");
225
+ return;
226
+ }
227
+ if (command === "node" && subcommand === "start") {
228
+ process.stdout.write("Use: pnpm dev:node\n");
229
+ return;
230
+ }
231
+ if (command === "task" && subcommand === "submit") {
232
+ const adapter = rest[0];
233
+ const adapterArgs = rest.slice(1);
234
+ if (adapter === "shell") {
235
+ await submitShell(adapterArgs);
236
+ return;
237
+ }
238
+ if (adapter === "aider") {
239
+ await submitAider(adapterArgs);
240
+ return;
241
+ }
242
+ if (adapter === "inference") {
243
+ await submitInference(adapterArgs);
244
+ return;
245
+ }
246
+ if (adapter === "harness") {
247
+ await submitHarness(adapterArgs);
248
+ return;
249
+ }
250
+ throw new Error(`unsupported adapter submit: ${adapter}`);
251
+ }
252
+ if (command === "tasks") {
253
+ const status = valueArg([subcommand, ...rest].filter(Boolean), "--status");
254
+ const tasks = await httpJson(status ? `/tasks?status=${encodeURIComponent(status)}` : "/tasks");
255
+ printJson(tasks);
256
+ return;
257
+ }
258
+ if (command === "task" && subcommand === "show") {
259
+ const taskId = rest[0];
260
+ if (!taskId) {
261
+ throw new Error("task id is required");
262
+ }
263
+ const task = await httpJson(`/tasks/${taskId}`);
264
+ printJson(task);
265
+ return;
266
+ }
267
+ if (command === "task" && subcommand === "retry") {
268
+ const taskId = rest[0];
269
+ if (!taskId) {
270
+ throw new Error("task id is required");
271
+ }
272
+ const task = await httpJson(`/tasks/${taskId}/retry`, { method: "POST", operator: true });
273
+ printJson(task);
274
+ return;
275
+ }
276
+ if (command === "task" && subcommand === "logs") {
277
+ const taskId = rest[0];
278
+ if (!taskId) {
279
+ throw new Error("task id is required");
280
+ }
281
+ await showLogsById(taskId);
282
+ return;
283
+ }
284
+ if (command === "task" && subcommand === "cancel") {
285
+ const taskId = rest[0];
286
+ if (!taskId) {
287
+ throw new Error("task id is required");
288
+ }
289
+ const task = await httpJson(`/tasks/${taskId}/cancel`, { method: "POST", operator: true });
290
+ printJson(task);
291
+ return;
292
+ }
293
+ if (command === "runs") {
294
+ const taskId = valueArg(rest, "--task-id");
295
+ const status = valueArg(rest, "--status");
296
+ const query = new URLSearchParams();
297
+ if (taskId) {
298
+ query.set("taskId", taskId);
299
+ }
300
+ if (status) {
301
+ query.set("status", status);
302
+ }
303
+ const runs = await httpJson(query.size > 0 ? `/runs?${query.toString()}` : "/runs");
304
+ printJson(runs);
305
+ return;
306
+ }
307
+ if (command === "run" && subcommand === "show") {
308
+ const runId = rest[0];
309
+ if (!runId) {
310
+ throw new Error("run id is required");
311
+ }
312
+ const run = await httpJson(`/runs/${runId}`);
313
+ printJson(run);
314
+ return;
315
+ }
316
+ if (command === "logs") {
317
+ const id = subcommand;
318
+ if (!id) {
319
+ throw new Error("run id or task id is required");
320
+ }
321
+ await showLogsById(id);
322
+ return;
323
+ }
324
+ if (command === "artifacts") {
325
+ const runId = subcommand;
326
+ if (!runId) {
327
+ throw new Error("run id is required");
328
+ }
329
+ const artifacts = await httpJson(`/runs/${runId}/artifacts`);
330
+ printJson(artifacts);
331
+ return;
332
+ }
333
+ usage();
334
+ }
335
+ main().catch((error) => {
336
+ process.stderr.write(`${String(error)}
337
+ `);
338
+ process.exit(1);
339
+ });
340
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../core/src/env.ts","../../core/src/config.ts","../../core/src/auth.ts","../../core/src/http-client.ts","../../cli/src/index.ts"],"sourcesContent":["import { config as dotenvConfig } from \"dotenv\";\n\nlet envLoaded = false;\n\nexport function loadLocalEnv(): void {\n if (envLoaded) {\n return;\n }\n\n dotenvConfig({ path: \".env\" });\n dotenvConfig({ path: \".env.local\", override: true });\n\n envLoaded = true;\n}\n\n","import { z } from \"zod\";\nimport { loadLocalEnv } from \"./env.js\";\n\nloadLocalEnv();\n\nconst serverEnvSchema = z.object({\n DATABASE_URL: z.string().min(1),\n WORKPLANE_SERVER_PORT: z\n .string()\n .optional()\n .default(\"8787\")\n .transform((value) => Number.parseInt(value, 10)),\n WORKPLANE_NODE_TOKEN: z.string().optional(),\n WORKPLANE_OPERATOR_TOKEN: z.string().optional(),\n WORKPLANE_ENV_ALLOWLIST: z.string().optional(),\n});\n\nconst nodeEnvSchema = z.object({\n WORKPLANE_SERVER_URL: z.string().url().optional().default(\"http://localhost:8787\"),\n WORKPLANE_NODE_NAME: z.string().optional().default(\"local-node-1\"),\n WORKPLANE_NODE_ID: z.string().optional(),\n WORKPLANE_NODE_CAPABILITIES: z\n .string()\n .optional()\n .default(\"shell,git,node,typescript,aider,ollama,codex,claude-code\"),\n WORKPLANE_NODE_TOKEN: z.string().optional(),\n WORKPLANE_POLL_INTERVAL_MS: z\n .string()\n .optional()\n .default(\"3000\")\n .transform((value) => Number.parseInt(value, 10)),\n WORKPLANE_ENV_ALLOWLIST: z.string().optional(),\n});\n\nexport interface ServerConfig {\n databaseUrl: string;\n port: number;\n nodeToken?: string;\n operatorToken?: string;\n envAllowlist: string[];\n}\n\nexport interface NodeConfig {\n serverUrl: string;\n nodeName: string;\n nodeId?: string;\n nodeCapabilities: string[];\n nodeToken?: string;\n pollIntervalMs: number;\n envAllowlist: string[];\n}\n\nexport function loadServerConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig {\n const parsed = serverEnvSchema.parse(env);\n\n return {\n databaseUrl: parsed.DATABASE_URL,\n port: parsed.WORKPLANE_SERVER_PORT,\n nodeToken: parsed.WORKPLANE_NODE_TOKEN,\n operatorToken: parsed.WORKPLANE_OPERATOR_TOKEN,\n envAllowlist: parseCsv(parsed.WORKPLANE_ENV_ALLOWLIST ?? \"\"),\n };\n}\n\nexport function loadNodeConfig(env: NodeJS.ProcessEnv = process.env): NodeConfig {\n const parsed = nodeEnvSchema.parse(env);\n\n return {\n serverUrl: parsed.WORKPLANE_SERVER_URL,\n nodeName: parsed.WORKPLANE_NODE_NAME,\n nodeId: parsed.WORKPLANE_NODE_ID,\n nodeCapabilities: parseCsv(parsed.WORKPLANE_NODE_CAPABILITIES),\n nodeToken: parsed.WORKPLANE_NODE_TOKEN,\n pollIntervalMs: parsed.WORKPLANE_POLL_INTERVAL_MS,\n envAllowlist: parseCsv(parsed.WORKPLANE_ENV_ALLOWLIST ?? \"\"),\n };\n}\n\nexport function parseCsv(value: string): string[] {\n return value\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean);\n}\n","import type { IncomingMessage } from \"node:http\";\n\nexport function parseBearerToken(authorizationHeader: string | undefined): string | undefined {\n if (!authorizationHeader) {\n return undefined;\n }\n const match = /^Bearer\\s+(.+)$/i.exec(authorizationHeader.trim());\n return match?.[1];\n}\n\nexport function isAuthorizedBearer(provided: string | undefined, expected: string | undefined): boolean {\n if (!expected) {\n return true;\n }\n if (!provided) {\n return false;\n }\n return provided === expected;\n}\n\nexport function getRequestBearer(req: IncomingMessage): string | undefined {\n const header = req.headers.authorization;\n const value = Array.isArray(header) ? header[0] : header;\n return parseBearerToken(value);\n}\n\nexport function authHeaders(token: string | undefined): Record<string, string> {\n if (!token) {\n return {};\n }\n return { authorization: `Bearer ${token}` };\n}\n","import { authHeaders } from \"./auth.js\";\n\nexport interface WorkplaneHttpOptions {\n method?: string;\n body?: unknown;\n nodeToken?: string;\n operatorToken?: string;\n}\n\nexport function buildWorkplaneHeaders(options: WorkplaneHttpOptions = {}): Record<string, string> {\n const token = options.nodeToken ?? options.operatorToken;\n return {\n \"content-type\": \"application/json\",\n ...authHeaders(token),\n };\n}\n\nexport async function workplaneFetch<T>(\n serverUrl: string,\n path: string,\n options: WorkplaneHttpOptions = {},\n): Promise<T> {\n const response = await fetch(`${serverUrl}${path}`, {\n method: options.method ?? \"GET\",\n headers: buildWorkplaneHeaders(options),\n body: options.body ? JSON.stringify(options.body) : undefined,\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`${response.status} ${response.statusText} from ${path}: ${text}`);\n }\n\n return (await response.json()) as T;\n}\n","import { loadLocalEnv } from \"../../core/src/env.js\";\nimport { parseCsv } from \"../../core/src/config.js\";\nimport { workplaneFetch } from \"../../core/src/http-client.js\";\n\nloadLocalEnv();\n\nconst serverUrl = process.env.WORKPLANE_SERVER_URL ?? \"http://localhost:8787\";\nconst operatorToken = process.env.WORKPLANE_OPERATOR_TOKEN;\n\nasync function httpJson<T>(\n path: string,\n options?: { method?: string; body?: Record<string, unknown>; operator?: boolean },\n): Promise<T> {\n return workplaneFetch<T>(serverUrl, path, {\n method: options?.method,\n body: options?.body,\n operatorToken: options?.operator ? operatorToken : undefined,\n });\n}\n\ninterface RunListResponse {\n runs: Array<{ id: string }>;\n}\n\nfunction valueArg(args: string[], flag: string): string | undefined {\n const index = args.indexOf(flag);\n if (index === -1) {\n return undefined;\n }\n return args[index + 1];\n}\n\nfunction printJson(payload: unknown): void {\n process.stdout.write(`${JSON.stringify(payload, null, 2)}\\n`);\n}\n\nasync function resolveRunIdFromTaskId(taskId: string): Promise<string> {\n const runs = await httpJson<RunListResponse>(`/runs?taskId=${encodeURIComponent(taskId)}`);\n const runId = runs.runs[0]?.id;\n if (!runId) {\n throw new Error(`no runs found for task ${taskId}`);\n }\n return runId;\n}\n\nasync function showLogsById(id: string): Promise<void> {\n const runId = id.startsWith(\"task_\") ? await resolveRunIdFromTaskId(id) : id;\n const logs = await httpJson(`/runs/${runId}/logs`);\n printJson(logs);\n}\n\nfunction usage(): void {\n process.stdout.write(\n [\n \"workplane CLI\",\n \"\",\n \"Env: WORKPLANE_SERVER_URL, WORKPLANE_OPERATOR_TOKEN (for task submit/retry/cancel)\",\n \"\",\n \"Commands:\",\n \" server start / node start # use pnpm dev:server / dev:node\",\n \" task submit shell --command <cmd> [--repo <url>] [--branch <b>] [--cwd <path>]\",\n \" task submit aider --prompt <text> --repo <url> [--model <m>] [--test-command <cmd>]\",\n \" task submit inference --model <m> --prompt <text> [--requires ollama]\",\n \" task submit harness --harness <codex|claude-code> --repo <url> --prompt <text>\",\n \" tasks [--status <status>]\",\n \" task show|logs|retry|cancel <taskId>\",\n \" runs [--task-id <id>] [--status <status>]\",\n \" run show <runId>\",\n \" logs <runId|taskId>\",\n \" artifacts <runId>\",\n ].join(\"\\n\"),\n );\n}\n\nasync function submitShell(args: string[]): Promise<void> {\n const command = valueArg(args, \"--command\");\n if (!command) {\n throw new Error(\"--command is required\");\n }\n\n const requires = parseCsv(valueArg(args, \"--requires\") ?? \"shell\");\n const repo = valueArg(args, \"--repo\");\n const branch = valueArg(args, \"--branch\");\n const cwd = valueArg(args, \"--cwd\");\n const task = await httpJson(\"/tasks\", {\n method: \"POST\",\n operator: true,\n body: {\n kind: \"shell.exec\",\n adapter: \"shell\",\n requires,\n payload: { command, repo, branch, cwd },\n },\n });\n\n printJson(task);\n}\n\nasync function submitAider(args: string[]): Promise<void> {\n const prompt = valueArg(args, \"--prompt\");\n if (!prompt) {\n throw new Error(\"--prompt is required\");\n }\n\n const model = valueArg(args, \"--model\");\n const repo = valueArg(args, \"--repo\");\n if (!repo) {\n throw new Error(\"--repo is required\");\n }\n const branch = valueArg(args, \"--branch\");\n const testCommand = valueArg(args, \"--test-command\");\n const requires = parseCsv(valueArg(args, \"--requires\") ?? \"git,aider\");\n const task = await httpJson(\"/tasks\", {\n method: \"POST\",\n operator: true,\n body: {\n kind: \"agent.run\",\n adapter: \"aider\",\n requires,\n payload: { prompt, model, repo, branch, testCommand },\n },\n });\n\n printJson(task);\n}\n\nasync function submitInference(args: string[]): Promise<void> {\n const model = valueArg(args, \"--model\");\n const prompt = valueArg(args, \"--prompt\");\n if (!model || !prompt) {\n throw new Error(\"--model and --prompt are required\");\n }\n\n const adapter = valueArg(args, \"--adapter\") ?? \"ollama\";\n const requires = parseCsv(valueArg(args, \"--requires\") ?? adapter);\n const inputFile = valueArg(args, \"--input-file\");\n\n const task = await httpJson(\"/tasks\", {\n method: \"POST\",\n operator: true,\n body: {\n kind: \"inference.batch\",\n adapter,\n requires,\n payload: { model, prompt, inputFile },\n },\n });\n\n printJson(task);\n}\n\nasync function submitHarness(args: string[]): Promise<void> {\n const harness = valueArg(args, \"--harness\");\n const prompt = valueArg(args, \"--prompt\");\n const repo = valueArg(args, \"--repo\");\n if (!harness || !prompt || !repo) {\n throw new Error(\"--harness, --prompt, and --repo are required\");\n }\n\n if (harness !== \"codex\" && harness !== \"claude-code\") {\n throw new Error(\"--harness must be codex or claude-code\");\n }\n\n const model = valueArg(args, \"--model\");\n const branch = valueArg(args, \"--branch\");\n const testCommand = valueArg(args, \"--test-command\");\n const requires = parseCsv(valueArg(args, \"--requires\") ?? `${harness},git`);\n\n const task = await httpJson(\"/tasks\", {\n method: \"POST\",\n operator: true,\n body: {\n kind: \"agent.run\",\n adapter: harness,\n requires,\n payload: { prompt, repo, model, branch, testCommand },\n },\n });\n\n printJson(task);\n}\n\nasync function main(): Promise<void> {\n const [command, subcommand, ...rest] = process.argv.slice(2);\n\n if (!command) {\n usage();\n return;\n }\n\n if (command === \"server\" && subcommand === \"start\") {\n process.stdout.write(\"Use: pnpm dev:server\\n\");\n return;\n }\n\n if (command === \"node\" && subcommand === \"start\") {\n process.stdout.write(\"Use: pnpm dev:node\\n\");\n return;\n }\n\n if (command === \"task\" && subcommand === \"submit\") {\n const adapter = rest[0];\n const adapterArgs = rest.slice(1);\n if (adapter === \"shell\") {\n await submitShell(adapterArgs);\n return;\n }\n if (adapter === \"aider\") {\n await submitAider(adapterArgs);\n return;\n }\n if (adapter === \"inference\") {\n await submitInference(adapterArgs);\n return;\n }\n if (adapter === \"harness\") {\n await submitHarness(adapterArgs);\n return;\n }\n throw new Error(`unsupported adapter submit: ${adapter}`);\n }\n\n if (command === \"tasks\") {\n const status = valueArg([subcommand, ...rest].filter(Boolean) as string[], \"--status\");\n const tasks = await httpJson(status ? `/tasks?status=${encodeURIComponent(status)}` : \"/tasks\");\n printJson(tasks);\n return;\n }\n\n if (command === \"task\" && subcommand === \"show\") {\n const taskId = rest[0];\n if (!taskId) {\n throw new Error(\"task id is required\");\n }\n const task = await httpJson(`/tasks/${taskId}`);\n printJson(task);\n return;\n }\n\n if (command === \"task\" && subcommand === \"retry\") {\n const taskId = rest[0];\n if (!taskId) {\n throw new Error(\"task id is required\");\n }\n const task = await httpJson(`/tasks/${taskId}/retry`, { method: \"POST\", operator: true });\n printJson(task);\n return;\n }\n\n if (command === \"task\" && subcommand === \"logs\") {\n const taskId = rest[0];\n if (!taskId) {\n throw new Error(\"task id is required\");\n }\n await showLogsById(taskId);\n return;\n }\n\n if (command === \"task\" && subcommand === \"cancel\") {\n const taskId = rest[0];\n if (!taskId) {\n throw new Error(\"task id is required\");\n }\n const task = await httpJson(`/tasks/${taskId}/cancel`, { method: \"POST\", operator: true });\n printJson(task);\n return;\n }\n\n if (command === \"runs\") {\n const taskId = valueArg(rest, \"--task-id\");\n const status = valueArg(rest, \"--status\");\n const query = new URLSearchParams();\n if (taskId) {\n query.set(\"taskId\", taskId);\n }\n if (status) {\n query.set(\"status\", status);\n }\n const runs = await httpJson(query.size > 0 ? `/runs?${query.toString()}` : \"/runs\");\n printJson(runs);\n return;\n }\n\n if (command === \"run\" && subcommand === \"show\") {\n const runId = rest[0];\n if (!runId) {\n throw new Error(\"run id is required\");\n }\n const run = await httpJson(`/runs/${runId}`);\n printJson(run);\n return;\n }\n\n if (command === \"logs\") {\n const id = subcommand;\n if (!id) {\n throw new Error(\"run id or task id is required\");\n }\n await showLogsById(id);\n return;\n }\n\n if (command === \"artifacts\") {\n const runId = subcommand;\n if (!runId) {\n throw new Error(\"run id is required\");\n }\n const artifacts = await httpJson(`/runs/${runId}/artifacts`);\n printJson(artifacts);\n return;\n }\n\n usage();\n}\n\nmain().catch((error) => {\n process.stderr.write(`${String(error)}\\n`);\n process.exit(1);\n});\n"],"mappings":"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","names":["serverUrl"]}
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../db/src/migrate.ts
4
+ import { readFile } from "fs/promises";
5
+ import { dirname, resolve } from "path";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // ../core/src/config.ts
9
+ import { z } from "zod";
10
+
11
+ // ../core/src/env.ts
12
+ import { config as dotenvConfig } from "dotenv";
13
+ var envLoaded = false;
14
+ function loadLocalEnv() {
15
+ if (envLoaded) {
16
+ return;
17
+ }
18
+ dotenvConfig({ path: ".env" });
19
+ dotenvConfig({ path: ".env.local", override: true });
20
+ envLoaded = true;
21
+ }
22
+
23
+ // ../core/src/config.ts
24
+ loadLocalEnv();
25
+ var serverEnvSchema = z.object({
26
+ DATABASE_URL: z.string().min(1),
27
+ WORKPLANE_SERVER_PORT: z.string().optional().default("8787").transform((value) => Number.parseInt(value, 10)),
28
+ WORKPLANE_NODE_TOKEN: z.string().optional(),
29
+ WORKPLANE_OPERATOR_TOKEN: z.string().optional(),
30
+ WORKPLANE_ENV_ALLOWLIST: z.string().optional()
31
+ });
32
+ var nodeEnvSchema = z.object({
33
+ WORKPLANE_SERVER_URL: z.string().url().optional().default("http://localhost:8787"),
34
+ WORKPLANE_NODE_NAME: z.string().optional().default("local-node-1"),
35
+ WORKPLANE_NODE_ID: z.string().optional(),
36
+ WORKPLANE_NODE_CAPABILITIES: z.string().optional().default("shell,git,node,typescript,aider,ollama,codex,claude-code"),
37
+ WORKPLANE_NODE_TOKEN: z.string().optional(),
38
+ WORKPLANE_POLL_INTERVAL_MS: z.string().optional().default("3000").transform((value) => Number.parseInt(value, 10)),
39
+ WORKPLANE_ENV_ALLOWLIST: z.string().optional()
40
+ });
41
+ function loadServerConfig(env = process.env) {
42
+ const parsed = serverEnvSchema.parse(env);
43
+ return {
44
+ databaseUrl: parsed.DATABASE_URL,
45
+ port: parsed.WORKPLANE_SERVER_PORT,
46
+ nodeToken: parsed.WORKPLANE_NODE_TOKEN,
47
+ operatorToken: parsed.WORKPLANE_OPERATOR_TOKEN,
48
+ envAllowlist: parseCsv(parsed.WORKPLANE_ENV_ALLOWLIST ?? "")
49
+ };
50
+ }
51
+ function parseCsv(value) {
52
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean);
53
+ }
54
+
55
+ // ../db/src/client.ts
56
+ import { Pool } from "pg";
57
+ var pools = /* @__PURE__ */ new Map();
58
+ function getPool(databaseUrl) {
59
+ const existing = pools.get(databaseUrl);
60
+ if (existing) {
61
+ return existing;
62
+ }
63
+ const pool = new Pool({ connectionString: databaseUrl });
64
+ pools.set(databaseUrl, pool);
65
+ return pool;
66
+ }
67
+
68
+ // ../db/src/migrate.ts
69
+ function quoteIdentifier(value) {
70
+ return `"${value.replaceAll('"', '""')}"`;
71
+ }
72
+ function parseDatabaseName(databaseUrl) {
73
+ const url = new URL(databaseUrl);
74
+ const name = url.pathname.replace(/^\//, "");
75
+ if (!name) {
76
+ throw new Error("DATABASE_URL must include a database name");
77
+ }
78
+ return name;
79
+ }
80
+ async function ensureDatabaseExists(databaseUrl) {
81
+ const databaseName = parseDatabaseName(databaseUrl);
82
+ const adminUrl = new URL(databaseUrl);
83
+ adminUrl.pathname = "/postgres";
84
+ const adminPool = getPool(adminUrl.toString());
85
+ try {
86
+ const existsResult = await adminPool.query("select 1 from pg_database where datname = $1", [databaseName]);
87
+ if (existsResult.rowCount !== 0) {
88
+ return;
89
+ }
90
+ await adminPool.query(`create database ${quoteIdentifier(databaseName)}`);
91
+ process.stdout.write(`Created database: ${databaseName}
92
+ `);
93
+ } catch (error) {
94
+ const pgError = error;
95
+ if (pgError.code === "42501") {
96
+ throw new Error(
97
+ `Database "${databaseName}" does not exist and the current role lacks CREATE DATABASE permissions.`
98
+ );
99
+ }
100
+ throw error;
101
+ } finally {
102
+ await adminPool.end();
103
+ }
104
+ }
105
+ async function main() {
106
+ const config = loadServerConfig();
107
+ await ensureDatabaseExists(config.databaseUrl);
108
+ const pool = getPool(config.databaseUrl);
109
+ const filePath = resolve(dirname(fileURLToPath(import.meta.url)), "schema.sql");
110
+ const sql = await readFile(filePath, "utf8");
111
+ await pool.query(sql);
112
+ await pool.end();
113
+ process.stdout.write("Migrations applied.\n");
114
+ }
115
+ main().catch((error) => {
116
+ process.stderr.write(`${String(error)}
117
+ `);
118
+ process.exit(1);
119
+ });
120
+ //# sourceMappingURL=migrate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../db/src/migrate.ts","../../core/src/config.ts","../../core/src/env.ts","../../db/src/client.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { loadServerConfig } from \"../../core/src/config.js\";\nimport { getPool } from \"./client.js\";\n\nfunction quoteIdentifier(value: string): string {\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\nfunction parseDatabaseName(databaseUrl: string): string {\n const url = new URL(databaseUrl);\n const name = url.pathname.replace(/^\\//, \"\");\n if (!name) {\n throw new Error(\"DATABASE_URL must include a database name\");\n }\n return name;\n}\n\nasync function ensureDatabaseExists(databaseUrl: string): Promise<void> {\n const databaseName = parseDatabaseName(databaseUrl);\n const adminUrl = new URL(databaseUrl);\n adminUrl.pathname = \"/postgres\";\n const adminPool = getPool(adminUrl.toString());\n\n try {\n const existsResult = await adminPool.query(\"select 1 from pg_database where datname = $1\", [databaseName]);\n if (existsResult.rowCount !== 0) {\n return;\n }\n await adminPool.query(`create database ${quoteIdentifier(databaseName)}`);\n process.stdout.write(`Created database: ${databaseName}\\n`);\n } catch (error) {\n const pgError = error as { code?: string; message?: string };\n if (pgError.code === \"42501\") {\n throw new Error(\n `Database \"${databaseName}\" does not exist and the current role lacks CREATE DATABASE permissions.`,\n );\n }\n throw error;\n } finally {\n await adminPool.end();\n }\n}\n\nasync function main(): Promise<void> {\n const config = loadServerConfig();\n await ensureDatabaseExists(config.databaseUrl);\n const pool = getPool(config.databaseUrl);\n const filePath = resolve(dirname(fileURLToPath(import.meta.url)), \"schema.sql\");\n const sql = await readFile(filePath, \"utf8\");\n await pool.query(sql);\n await pool.end();\n process.stdout.write(\"Migrations applied.\\n\");\n}\n\nmain().catch((error) => {\n process.stderr.write(`${String(error)}\\n`);\n process.exit(1);\n});\n","import { z } from \"zod\";\nimport { loadLocalEnv } from \"./env.js\";\n\nloadLocalEnv();\n\nconst serverEnvSchema = z.object({\n DATABASE_URL: z.string().min(1),\n WORKPLANE_SERVER_PORT: z\n .string()\n .optional()\n .default(\"8787\")\n .transform((value) => Number.parseInt(value, 10)),\n WORKPLANE_NODE_TOKEN: z.string().optional(),\n WORKPLANE_OPERATOR_TOKEN: z.string().optional(),\n WORKPLANE_ENV_ALLOWLIST: z.string().optional(),\n});\n\nconst nodeEnvSchema = z.object({\n WORKPLANE_SERVER_URL: z.string().url().optional().default(\"http://localhost:8787\"),\n WORKPLANE_NODE_NAME: z.string().optional().default(\"local-node-1\"),\n WORKPLANE_NODE_ID: z.string().optional(),\n WORKPLANE_NODE_CAPABILITIES: z\n .string()\n .optional()\n .default(\"shell,git,node,typescript,aider,ollama,codex,claude-code\"),\n WORKPLANE_NODE_TOKEN: z.string().optional(),\n WORKPLANE_POLL_INTERVAL_MS: z\n .string()\n .optional()\n .default(\"3000\")\n .transform((value) => Number.parseInt(value, 10)),\n WORKPLANE_ENV_ALLOWLIST: z.string().optional(),\n});\n\nexport interface ServerConfig {\n databaseUrl: string;\n port: number;\n nodeToken?: string;\n operatorToken?: string;\n envAllowlist: string[];\n}\n\nexport interface NodeConfig {\n serverUrl: string;\n nodeName: string;\n nodeId?: string;\n nodeCapabilities: string[];\n nodeToken?: string;\n pollIntervalMs: number;\n envAllowlist: string[];\n}\n\nexport function loadServerConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig {\n const parsed = serverEnvSchema.parse(env);\n\n return {\n databaseUrl: parsed.DATABASE_URL,\n port: parsed.WORKPLANE_SERVER_PORT,\n nodeToken: parsed.WORKPLANE_NODE_TOKEN,\n operatorToken: parsed.WORKPLANE_OPERATOR_TOKEN,\n envAllowlist: parseCsv(parsed.WORKPLANE_ENV_ALLOWLIST ?? \"\"),\n };\n}\n\nexport function loadNodeConfig(env: NodeJS.ProcessEnv = process.env): NodeConfig {\n const parsed = nodeEnvSchema.parse(env);\n\n return {\n serverUrl: parsed.WORKPLANE_SERVER_URL,\n nodeName: parsed.WORKPLANE_NODE_NAME,\n nodeId: parsed.WORKPLANE_NODE_ID,\n nodeCapabilities: parseCsv(parsed.WORKPLANE_NODE_CAPABILITIES),\n nodeToken: parsed.WORKPLANE_NODE_TOKEN,\n pollIntervalMs: parsed.WORKPLANE_POLL_INTERVAL_MS,\n envAllowlist: parseCsv(parsed.WORKPLANE_ENV_ALLOWLIST ?? \"\"),\n };\n}\n\nexport function parseCsv(value: string): string[] {\n return value\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean);\n}\n","import { config as dotenvConfig } from \"dotenv\";\n\nlet envLoaded = false;\n\nexport function loadLocalEnv(): void {\n if (envLoaded) {\n return;\n }\n\n dotenvConfig({ path: \".env\" });\n dotenvConfig({ path: \".env.local\", override: true });\n\n envLoaded = true;\n}\n\n","import { Pool } from \"pg\";\n\nconst pools = new Map<string, Pool>();\n\nexport function getPool(databaseUrl: string): Pool {\n const existing = pools.get(databaseUrl);\n if (existing) {\n return existing;\n }\n\n const pool = new Pool({ connectionString: databaseUrl });\n pools.set(databaseUrl, pool);\n return pool;\n}\n"],"mappings":"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","names":[]}