smithers-orchestrator 0.25.0 → 0.25.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smithers-orchestrator",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "description": "Public Smithers facade for durable coding-agent workflows and Gateway operation",
5
5
  "license": "MIT",
6
6
  "homepage": "https://smithers.sh",
@@ -92,28 +92,28 @@
92
92
  "incur": "^0.4.1",
93
93
  "react": "^19.2.5",
94
94
  "zod": "^4.3.6",
95
- "@smithers-orchestrator/agents": "0.25.0",
96
- "@smithers-orchestrator/components": "0.25.0",
97
- "@smithers-orchestrator/control-plane": "0.25.0",
98
- "@smithers-orchestrator/db": "0.25.0",
99
- "@smithers-orchestrator/engine": "0.25.0",
100
- "@smithers-orchestrator/driver": "0.25.0",
101
- "@smithers-orchestrator/errors": "0.25.0",
102
- "@smithers-orchestrator/gateway-client": "0.25.0",
103
- "@smithers-orchestrator/gateway-react": "0.25.0",
104
- "@smithers-orchestrator/graph": "0.25.0",
105
- "@smithers-orchestrator/memory": "0.25.0",
106
- "@smithers-orchestrator/observability": "0.25.0",
107
- "@smithers-orchestrator/openapi": "0.25.0",
108
- "@smithers-orchestrator/react-reconciler": "0.25.0",
109
- "@smithers-orchestrator/sandbox": "0.25.0",
110
- "@smithers-orchestrator/scorers": "0.25.0",
111
- "@smithers-orchestrator/scheduler": "0.25.0",
112
- "@smithers-orchestrator/server": "0.25.0",
113
- "@smithers-orchestrator/time-travel": "0.25.0",
114
- "@smithers-orchestrator/vcs": "0.25.0",
115
- "@smithers-orchestrator/cli": "0.25.0",
116
- "@smithers-orchestrator/tool-context": "0.25.0"
95
+ "@smithers-orchestrator/agents": "0.25.1",
96
+ "@smithers-orchestrator/db": "0.25.1",
97
+ "@smithers-orchestrator/engine": "0.25.1",
98
+ "@smithers-orchestrator/errors": "0.25.1",
99
+ "@smithers-orchestrator/components": "0.25.1",
100
+ "@smithers-orchestrator/cli": "0.25.1",
101
+ "@smithers-orchestrator/control-plane": "0.25.1",
102
+ "@smithers-orchestrator/memory": "0.25.1",
103
+ "@smithers-orchestrator/gateway-client": "0.25.1",
104
+ "@smithers-orchestrator/observability": "0.25.1",
105
+ "@smithers-orchestrator/driver": "0.25.1",
106
+ "@smithers-orchestrator/openapi": "0.25.1",
107
+ "@smithers-orchestrator/react-reconciler": "0.25.1",
108
+ "@smithers-orchestrator/gateway-react": "0.25.1",
109
+ "@smithers-orchestrator/sandbox": "0.25.1",
110
+ "@smithers-orchestrator/scheduler": "0.25.1",
111
+ "@smithers-orchestrator/scorers": "0.25.1",
112
+ "@smithers-orchestrator/server": "0.25.1",
113
+ "@smithers-orchestrator/time-travel": "0.25.1",
114
+ "@smithers-orchestrator/tool-context": "0.25.1",
115
+ "@smithers-orchestrator/graph": "0.25.1",
116
+ "@smithers-orchestrator/vcs": "0.25.1"
117
117
  },
118
118
  "optionalDependencies": {
119
119
  "@electric-sql/pglite": "0.5.1",
@@ -0,0 +1,98 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, parse, resolve } from "node:path";
3
+
4
+ const WORKFLOW_PATH_COMMANDS = new Set([
5
+ "up",
6
+ "graph",
7
+ "fork",
8
+ "replay",
9
+ "revert",
10
+ "timetravel",
11
+ ]);
12
+ const WORKFLOW_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".mts"]);
13
+
14
+ /**
15
+ * @param {string} value
16
+ */
17
+ export function isOptionLike(value) {
18
+ return value.startsWith("-");
19
+ }
20
+
21
+ /**
22
+ * @param {string} value
23
+ */
24
+ export function looksLikeWorkflowPath(value) {
25
+ if (isOptionLike(value))
26
+ return false;
27
+ return WORKFLOW_EXTENSIONS.has(parse(value).ext);
28
+ }
29
+
30
+ /**
31
+ * @param {string[]} args
32
+ */
33
+ export function getExplicitWorkflowPath(args) {
34
+ if (args.length === 0)
35
+ return null;
36
+ if (looksLikeWorkflowPath(args[0]))
37
+ return args[0];
38
+ for (let index = 0; index < args.length; index++) {
39
+ const arg = args[index];
40
+ if (!WORKFLOW_PATH_COMMANDS.has(arg))
41
+ continue;
42
+ for (let nextIndex = index + 1; nextIndex < args.length; nextIndex++) {
43
+ const candidate = args[nextIndex];
44
+ if (looksLikeWorkflowPath(candidate))
45
+ return candidate;
46
+ }
47
+ return null;
48
+ }
49
+ for (const arg of args) {
50
+ if (looksLikeWorkflowPath(arg))
51
+ return arg;
52
+ }
53
+ return null;
54
+ }
55
+
56
+ /**
57
+ * Resolve the local `smithers-orchestrator` package's bin JS file under
58
+ * `<directory>/node_modules/`. Going through `package.json` (instead of the
59
+ * `.bin/smithers` shell shim npm/pnpm generate) is the whole point: the shim
60
+ * is `#!/bin/sh` and re-execing it with `process.execPath` (bun) makes bun
61
+ * parse shell as JavaScript, which crashes with `Expected ")" but found
62
+ * "$(echo "`.
63
+ *
64
+ * @param {string} directory
65
+ */
66
+ export function resolveLocalSmithersBinJs(directory) {
67
+ const pkgJsonPath = resolve(directory, "node_modules/smithers-orchestrator/package.json");
68
+ if (!existsSync(pkgJsonPath))
69
+ return null;
70
+ let pkg;
71
+ try {
72
+ pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
73
+ } catch {
74
+ return null;
75
+ }
76
+ const binEntry = typeof pkg?.bin === "string" ? pkg.bin : pkg?.bin?.smithers;
77
+ if (typeof binEntry !== "string" || binEntry.length === 0)
78
+ return null;
79
+ const binPath = resolve(dirname(pkgJsonPath), binEntry);
80
+ return existsSync(binPath) ? binPath : null;
81
+ }
82
+
83
+ /**
84
+ * @param {string} cwd
85
+ * @param {string} workflowPath
86
+ */
87
+ export function findNearestWorkflowLocalCli(cwd, workflowPath) {
88
+ let current = dirname(resolve(cwd, workflowPath));
89
+ while (true) {
90
+ const localBin = resolveLocalSmithersBinJs(current);
91
+ if (localBin)
92
+ return localBin;
93
+ const parent = dirname(current);
94
+ if (parent === current)
95
+ return null;
96
+ current = parent;
97
+ }
98
+ }
@@ -1,104 +1,13 @@
1
1
  #!/usr/bin/env bun
2
2
  import { spawn } from "node:child_process";
3
- import { existsSync, readFileSync, realpathSync } from "node:fs";
4
- import { dirname, parse, resolve } from "node:path";
3
+ import { realpathSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
-
7
- const WORKFLOW_PATH_COMMANDS = new Set([
8
- "up",
9
- "graph",
10
- "fork",
11
- "replay",
12
- "revert",
13
- "timetravel",
14
- ]);
15
- const WORKFLOW_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".mts"]);
16
-
17
- /**
18
- * @param {string} value
19
- */
20
- function isOptionLike(value) {
21
- return value.startsWith("-");
22
- }
23
-
24
- /**
25
- * @param {string} value
26
- */
27
- function looksLikeWorkflowPath(value) {
28
- if (isOptionLike(value))
29
- return false;
30
- return WORKFLOW_EXTENSIONS.has(parse(value).ext);
31
- }
32
-
33
- /**
34
- * @param {string[]} args
35
- */
36
- function getExplicitWorkflowPath(args) {
37
- if (args.length === 0)
38
- return null;
39
- if (looksLikeWorkflowPath(args[0]))
40
- return args[0];
41
- for (let index = 0; index < args.length; index++) {
42
- const arg = args[index];
43
- if (!WORKFLOW_PATH_COMMANDS.has(arg))
44
- continue;
45
- for (let nextIndex = index + 1; nextIndex < args.length; nextIndex++) {
46
- const candidate = args[nextIndex];
47
- if (looksLikeWorkflowPath(candidate))
48
- return candidate;
49
- }
50
- return null;
51
- }
52
- for (const arg of args) {
53
- if (looksLikeWorkflowPath(arg))
54
- return arg;
55
- }
56
- return null;
57
- }
58
-
59
- /**
60
- * Resolve the local `smithers-orchestrator` package's bin JS file under
61
- * `<directory>/node_modules/`. Going through `package.json` (instead of the
62
- * `.bin/smithers` shell shim npm/pnpm generate) is the whole point: the shim
63
- * is `#!/bin/sh` and re-execing it with `process.execPath` (bun) makes bun
64
- * parse shell as JavaScript, which crashes with `Expected ")" but found
65
- * "$(echo "`.
66
- *
67
- * @param {string} directory
68
- */
69
- function resolveLocalSmithersBinJs(directory) {
70
- const pkgJsonPath = resolve(directory, "node_modules/smithers-orchestrator/package.json");
71
- if (!existsSync(pkgJsonPath))
72
- return null;
73
- let pkg;
74
- try {
75
- pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
76
- } catch {
77
- return null;
78
- }
79
- const binEntry = typeof pkg?.bin === "string" ? pkg.bin : pkg?.bin?.smithers;
80
- if (typeof binEntry !== "string" || binEntry.length === 0)
81
- return null;
82
- const binPath = resolve(dirname(pkgJsonPath), binEntry);
83
- return existsSync(binPath) ? binPath : null;
84
- }
85
-
86
- /**
87
- * @param {string} cwd
88
- * @param {string} workflowPath
89
- */
90
- function findNearestWorkflowLocalCli(cwd, workflowPath) {
91
- let current = dirname(resolve(cwd, workflowPath));
92
- while (true) {
93
- const localBin = resolveLocalSmithersBinJs(current);
94
- if (localBin)
95
- return localBin;
96
- const parent = dirname(current);
97
- if (parent === current)
98
- return null;
99
- current = parent;
100
- }
101
- }
6
+ import {
7
+ getExplicitWorkflowPath,
8
+ findNearestWorkflowLocalCli,
9
+ resolveLocalSmithersBinJs,
10
+ } from "./smithers-delegation.js";
102
11
 
103
12
  /**
104
13
  * When a workflow directory (`.smithers/`) exists in the user's cwd and it's
@@ -471,6 +471,78 @@ function writeMigrationMarker(result, sourceStats) {
471
471
  writeFileSync(result.markerPath, `${JSON.stringify(marker, null, 2)}\n`, "utf8");
472
472
  }
473
473
 
474
+ // bun:sqlite surfaces a corrupt, encrypted, or non-SQLite source file with one
475
+ // of these substrings (case-insensitive). Detecting them lets us replace the
476
+ // raw, unactionable engine text with guidance the operator can follow.
477
+ const CORRUPT_SQLITE_MARKERS = ["malformed", "not a database", "file is encrypted", "disk image is malformed"];
478
+ const UNOPENABLE_SQLITE_MARKERS = ["unable to open database file"];
479
+
480
+ /**
481
+ * @param {unknown} error
482
+ * @returns {boolean}
483
+ */
484
+ function isCorruptSqliteError(error) {
485
+ const message = (error instanceof Error ? error.message : String(error ?? "")).toLowerCase();
486
+ return CORRUPT_SQLITE_MARKERS.some((marker) => message.includes(marker));
487
+ }
488
+
489
+ /**
490
+ * @param {unknown} error
491
+ * @returns {boolean}
492
+ */
493
+ function isUnopenableSqliteError(error) {
494
+ const message = (error instanceof Error ? error.message : String(error ?? "")).toLowerCase();
495
+ return UNOPENABLE_SQLITE_MARKERS.some((marker) => message.includes(marker));
496
+ }
497
+
498
+ /**
499
+ * Open the legacy SQLite source and read its schema-level metadata. A corrupt,
500
+ * encrypted, or non-SQLite file fails here (open, BEGIN, or the first query);
501
+ * we wrap that into an actionable SmithersError so the CLI maps it to a clean
502
+ * exit code instead of leaking the raw bun:sqlite text. The source is opened
503
+ * read-only and nothing has been written to the target yet, so no partial
504
+ * output can exist when this throws.
505
+ *
506
+ * @param {string} dbPath
507
+ * @returns {{ sqlite: Database; runCount: number; schemaVersion: string; tables: Array<{ name: string; sql: string }>; indexes: Array<{ name: string; sql: string }> }}
508
+ */
509
+ function openSourceStore(dbPath) {
510
+ /** @type {Database | undefined} */
511
+ let sqlite;
512
+ try {
513
+ sqlite = new Database(dbPath, { readonly: true });
514
+ sqlite.exec("BEGIN");
515
+ const runCount = sqliteTableExists(sqlite, "_smithers_runs") ? countSqliteRows(sqlite, "_smithers_runs") : 0;
516
+ const schemaVersion = sqliteSchemaVersion(sqlite);
517
+ const tables = orderedTables(sourceTables(sqlite));
518
+ const indexes = sourceIndexes(sqlite);
519
+ return { sqlite, runCount, schemaVersion, tables, indexes };
520
+ }
521
+ catch (error) {
522
+ try {
523
+ sqlite?.exec("ROLLBACK");
524
+ }
525
+ catch {
526
+ // No open transaction to roll back; closing the handle is enough.
527
+ }
528
+ try {
529
+ sqlite?.close();
530
+ }
531
+ catch {
532
+ // Best-effort read-only source cleanup.
533
+ }
534
+ if (isCorruptSqliteError(error)) {
535
+ const original = error instanceof Error ? error.message : String(error);
536
+ throw new SmithersError("DB_QUERY_FAILED", `The legacy SQLite store at ${dbPath} appears to be corrupted (${original}). Smithers cannot migrate a corrupt store. Verify with: sqlite3 ${dbPath} 'PRAGMA integrity_check'. If it reports corruption, restore from a backup or start fresh; the original file was left untouched.`, { dbPath }, { cause: error });
537
+ }
538
+ if (isUnopenableSqliteError(error)) {
539
+ const original = error instanceof Error ? error.message : String(error);
540
+ throw new SmithersError("DB_QUERY_FAILED", `Could not open the legacy SQLite store at ${dbPath} (${original}). It may be locked by another process, have unreadable permissions, or be missing its -wal/-shm sidecar files (copy smithers.db together with smithers.db-wal and smithers.db-shm). The original file was left untouched.`, { dbPath }, { cause: error });
541
+ }
542
+ throw error;
543
+ }
544
+ }
545
+
474
546
  /**
475
547
  * @param {string} dbPath
476
548
  */
@@ -498,6 +570,16 @@ export async function migrateSmithersStore(opts = {}) {
498
570
  });
499
571
  }
500
572
  const target = normalizeTarget(opts.to);
573
+ // Validate the Postgres connection target BEFORE opening the source store, so
574
+ // `migrate --to postgres` with no url fails fast with a clear INVALID_INPUT
575
+ // instead of being masked by an unrelated source-open error (e.g. an
576
+ // unreadable or sidecar-less smithers.db).
577
+ const postgresUrl = target === "postgres" ? (opts.url ?? env.SMITHERS_POSTGRES_URL ?? env.DATABASE_URL) : undefined;
578
+ if (target === "postgres" && !postgresUrl) {
579
+ throw new SmithersError("INVALID_INPUT", "smithers migrate --to postgres requires --url, SMITHERS_POSTGRES_URL, or DATABASE_URL.", {
580
+ target,
581
+ });
582
+ }
501
583
  const markerPath = markerPathFor(dbPath, workspaceRoot);
502
584
  const batchSize = Math.max(1, Math.floor(opts.batchSize ?? DEFAULT_BATCH_SIZE));
503
585
  const keepSqlite = opts.keepSqlite ?? true;
@@ -507,12 +589,9 @@ export async function migrateSmithersStore(opts = {}) {
507
589
  /** @type {(import("./CreateSmithersApi.ts").CreateSmithersApi<Record<string, import("zod").ZodObject<any>>> & { close?: () => Promise<void> }) | undefined} */
508
590
  let targetApi;
509
591
  try {
510
- sqlite = new Database(dbPath, { readonly: true });
511
- sqlite.exec("BEGIN");
512
- const runCount = sqliteTableExists(sqlite, "_smithers_runs") ? countSqliteRows(sqlite, "_smithers_runs") : 0;
513
- const schemaVersion = sqliteSchemaVersion(sqlite);
514
- const tables = orderedTables(sourceTables(sqlite));
515
- const indexes = sourceIndexes(sqlite);
592
+ const source = openSourceStore(dbPath);
593
+ sqlite = source.sqlite;
594
+ const { runCount, schemaVersion, tables, indexes } = source;
516
595
  await emitMigrationLog("info", "smithers.migration.started", {
517
596
  dbPath,
518
597
  target,
@@ -521,15 +600,9 @@ export async function migrateSmithersStore(opts = {}) {
521
600
  schemaVersion,
522
601
  }, "smithers:migrate");
523
602
  if (target === "postgres") {
524
- const connectionString = opts.url ?? env.SMITHERS_POSTGRES_URL ?? env.DATABASE_URL;
525
- if (!connectionString) {
526
- throw new SmithersError("INVALID_INPUT", "smithers migrate --to postgres requires --url, SMITHERS_POSTGRES_URL, or DATABASE_URL.", {
527
- target,
528
- });
529
- }
530
603
  targetApi = await createSmithersPostgres({}, {
531
604
  provider: "postgres",
532
- connectionString,
605
+ connectionString: postgresUrl,
533
606
  });
534
607
  }
535
608
  else {