@synapsor/runner 0.1.0-alpha.16 → 0.1.0-alpha.17

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/runner.mjs CHANGED
@@ -451,6 +451,7 @@ var SOURCE_KEYS = /* @__PURE__ */ new Set([
451
451
  "engine",
452
452
  "read_url_env",
453
453
  "write_url_env",
454
+ "read_only",
454
455
  "statement_timeout_ms",
455
456
  "ssl"
456
457
  ]);
@@ -564,6 +565,7 @@ function validateRunnerCapabilityConfig(input) {
564
565
  validateTrustedContext(input.trusted_context, input.contexts, input.capabilities, input.mode, strict, errors, warnings);
565
566
  validateExecutors(input.executors, input.mode, strict, errors);
566
567
  validateCapabilities(input.capabilities, input.sources, input.contexts, input.executors, input.mode, strict, errors, warnings);
568
+ validateWritebackReadiness(input.sources, input.capabilities, input.mode, errors, warnings);
567
569
  scanForForbiddenFields(input, "$", errors);
568
570
  return { ok: errors.length === 0, errors, warnings };
569
571
  }
@@ -622,14 +624,37 @@ function validateSources(value, mode, strict, errors, warnings) {
622
624
  if (source.statement_timeout_ms !== void 0 && !isPositiveInteger(source.statement_timeout_ms)) {
623
625
  errors.push({ path: `${path4}.statement_timeout_ms`, code: "INVALID_TIMEOUT", message: "statement_timeout_ms must be a positive integer." });
624
626
  }
627
+ if (source.read_only !== void 0 && typeof source.read_only !== "boolean") {
628
+ errors.push({ path: `${path4}.read_only`, code: "INVALID_SOURCE_READ_ONLY", message: "read_only must be true or false when provided." });
629
+ }
630
+ }
631
+ }
632
+ function validateWritebackReadiness(sources, capabilities, mode, errors, warnings) {
633
+ if (mode !== "review" || !isRecord(sources) || !Array.isArray(capabilities)) return;
634
+ capabilities.forEach((capability, index) => {
635
+ if (!isRecord(capability) || capability.kind !== "proposal") return;
636
+ const sourceName = isNonEmptyString(capability.source) ? capability.source : void 0;
637
+ const source = sourceName ? sources[sourceName] : void 0;
638
+ if (!sourceName || !isRecord(source)) return;
639
+ const executor = isNonEmptyString(capability.executor) ? capability.executor : "sql_update";
640
+ const directSql = executor === "sql_update";
641
+ if (!directSql) return;
642
+ if (source.read_only === true) {
643
+ errors.push({
644
+ path: `$.capabilities[${index}].executor`,
645
+ code: "READ_ONLY_SOURCE_DIRECT_WRITEBACK",
646
+ message: `Proposal capability ${String(capability.name ?? index)} uses direct SQL writeback, but source ${sourceName} is marked read_only.`
647
+ });
648
+ return;
649
+ }
625
650
  if (source.write_url_env === void 0) {
626
651
  warnings.push({
627
- path: `${path4}.write_url_env`,
652
+ path: `$.sources.${sourceName}.write_url_env`,
628
653
  code: "WRITEBACK_DISABLED",
629
- message: "No write_url_env is configured; review-mode proposal execution cannot apply external DB changes."
654
+ message: "No write_url_env is configured; direct SQL review-mode proposal execution cannot apply external DB changes."
630
655
  });
631
656
  }
632
- }
657
+ });
633
658
  }
634
659
  function validateCloud(value, mode, strict, errors) {
635
660
  if (mode !== "cloud") {
@@ -4466,6 +4491,7 @@ import { Pool as Pool3 } from "pg";
4466
4491
  var TENANT_COLUMNS = /* @__PURE__ */ new Set(["tenant_id", "account_id", "organization_id", "org_id", "workspace_id", "customer_id"]);
4467
4492
  var CONFLICT_COLUMNS = /* @__PURE__ */ new Set(["updated_at", "modified_at", "row_version", "version", "lock_version", "etag"]);
4468
4493
  var IMMUTABLE_COLUMNS = /* @__PURE__ */ new Set(["id", "uuid", "created_at", "created_by"]);
4494
+ var DEFAULT_RESULT_FORMAT = 2;
4469
4495
  var SENSITIVE_PATTERNS = [
4470
4496
  /password/i,
4471
4497
  /password_hash/i,
@@ -4574,13 +4600,14 @@ function generateRunnerConfigFromSpec(spec) {
4574
4600
  const config = {
4575
4601
  version: 1,
4576
4602
  mode,
4577
- ...spec.result_format ? { result_format: spec.result_format } : {},
4603
+ result_format: spec.result_format ?? DEFAULT_RESULT_FORMAT,
4578
4604
  storage: { sqlite_path: "./.synapsor/local.db" },
4579
4605
  sources: {
4580
4606
  [sourceName]: {
4581
4607
  engine: spec.engine,
4582
4608
  read_url_env: readUrlEnv,
4583
4609
  ...mode === "review" && writeUrlEnv ? { write_url_env: writeUrlEnv } : {},
4610
+ ...mode === "review" && writeback2.executor !== "sql_update" && !writeUrlEnv ? { read_only: true } : {},
4584
4611
  statement_timeout_ms: spec.statement_timeout_ms ?? 3e3
4585
4612
  }
4586
4613
  },
@@ -6865,11 +6892,16 @@ ${cliCommandName()} --help
6865
6892
  return 2;
6866
6893
  }
6867
6894
  async function init(args) {
6895
+ const answersPath = optionalArg(args, "--answers");
6896
+ if (answersPath) {
6897
+ return initFromAnswers(args, answersPath);
6898
+ }
6868
6899
  const specPath = optionalArg(args, "--spec");
6869
6900
  if (specPath) {
6870
6901
  return initFromSpec(args, specPath);
6871
6902
  }
6872
- if (args.includes("--wizard") || process2.stdin.isTTY && process2.stdout.isTTY && !args.includes("--starter")) {
6903
+ const scripted = isScriptedOnboardingArgs(args);
6904
+ if (args.includes("--wizard") || process2.stdin.isTTY && process2.stdout.isTTY && !args.includes("--starter") && !scripted) {
6873
6905
  return runInitWizard(args);
6874
6906
  }
6875
6907
  const inspectionJson = optionalArg(args, "--inspection-json");
@@ -7035,9 +7067,22 @@ async function runInitWizard(args, options = {}) {
7035
7067
  if (transitionFromColumns.length > 0) ensureColumnsExist(transitionFromColumns, columns, "transition from");
7036
7068
  }
7037
7069
  }
7038
- const namespace = await askDefault(ask, "Capability namespace", optionalArg(args, "--namespace") ?? recipeSpec?.namespace ?? "source");
7039
- const objectName = await askDefault(ask, "Business object name", optionalArg(args, "--object-name") ?? recipeSpec?.object_name ?? safeObjectName(table.name));
7070
+ const inferredObjectName = recipeSpec?.object_name ?? safeObjectName(table.name);
7071
+ const namespace = await askDefault(ask, "Capability namespace", optionalArg(args, "--namespace") ?? recipeSpec?.namespace ?? inferCapabilityNamespace(table.name));
7072
+ const objectName = await askDefault(ask, "Business object name", optionalArg(args, "--object-name") ?? inferredObjectName);
7040
7073
  const lookupArg = await askDefault(ask, "Model-visible object id argument", optionalArg(args, "--lookup-arg") ?? recipeSpec?.lookup_arg ?? `${objectName}_id`);
7074
+ const defaultInspectToolName = recipeSpec?.inspect_tool_name ?? `${namespace}.inspect_${objectName}`;
7075
+ const inspectToolName = await askDefault(
7076
+ ask,
7077
+ "Read capability name",
7078
+ optionalArg(args, "--read-tool") ?? optionalArg(args, "--inspect-tool-name") ?? defaultInspectToolName
7079
+ );
7080
+ const defaultProposalToolName = recipeSpec?.proposal_tool_name ?? `${namespace}.propose_${objectName}_update`;
7081
+ const proposalToolName = mode === "read_only" ? void 0 : await askDefault(
7082
+ ask,
7083
+ "Proposal capability name",
7084
+ optionalArg(args, "--proposal-tool") ?? optionalArg(args, "--proposal-tool-name") ?? defaultProposalToolName
7085
+ );
7041
7086
  const smokeObjectId = await askDefault(ask, "Optional real object id for a first smoke call", optionalArg(args, "--smoke-id") ?? "");
7042
7087
  const objectLabel = objectName.replace(/_/g, " ");
7043
7088
  const inspectDescription = await askDefault(
@@ -7060,7 +7105,7 @@ async function runInitWizard(args, options = {}) {
7060
7105
  "Proposal capability returns hint",
7061
7106
  optionalArg(args, "--proposal-returns-hint") ?? "Returns a proposal id, exact before/after diff, evidence handle, approval status, and source_database_changed:false."
7062
7107
  );
7063
- const resultFormatAnswer = await askChoice(ask, "MCP result envelope", optionalArg(args, "--result-format") ? normalizeResultFormatAnswer(optionalArg(args, "--result-format")) : "default", ["default", "v1", "v2"]);
7108
+ const resultFormatAnswer = await askChoice(ask, "MCP result envelope", optionalArg(args, "--result-format") ? normalizeResultFormatAnswer(optionalArg(args, "--result-format")) : "v2", ["v2", "v1", "default"]);
7064
7109
  const resultFormat = resultFormatAnswer === "v1" ? 1 : resultFormatAnswer === "v2" ? 2 : void 0;
7065
7110
  let writeUrlEnv = optionalArg(args, "--write-url-env");
7066
7111
  let writeback2;
@@ -7090,7 +7135,7 @@ async function runInitWizard(args, options = {}) {
7090
7135
  const writeTemplate = await askChoice(ask, "Write starter app-owned handler template", args.includes("--skip-handler-template") ? "no" : "yes", ["yes", "no"]);
7091
7136
  if (writeTemplate === "yes") {
7092
7137
  const template = await askChoice(ask, "Handler template", optionalArg(args, "--handler-template") ?? "node-fastify", ["node-fastify", "python-fastapi"]);
7093
- const output = await askDefault(ask, "Handler template output", optionalArg(args, "--handler-template-output") ?? handlerTemplateDefinitions[template].fileName);
7138
+ const output = await askDefault(ask, "Handler template output", optionalArg(args, "--handler-output") ?? optionalArg(args, "--handler-template-output") ?? handlerTemplateDefinitions[template].fileName);
7094
7139
  generatedHandlerTemplate = { name: template, output };
7095
7140
  }
7096
7141
  } else {
@@ -7103,13 +7148,13 @@ async function runInitWizard(args, options = {}) {
7103
7148
  };
7104
7149
  const writeTemplate = await askChoice(ask, "Write starter app-owned handler template", args.includes("--skip-handler-template") ? "no" : "yes", ["yes", "no"]);
7105
7150
  if (writeTemplate === "yes") {
7106
- const output = await askDefault(ask, "Handler template output", optionalArg(args, "--handler-template-output") ?? handlerTemplateDefinitions.command.fileName);
7151
+ const output = await askDefault(ask, "Handler template output", optionalArg(args, "--handler-output") ?? optionalArg(args, "--handler-template-output") ?? handlerTemplateDefinitions.command.fileName);
7107
7152
  generatedHandlerTemplate = { name: "command", output };
7108
7153
  }
7109
7154
  }
7110
7155
  }
7111
7156
  const approvalRole = mode === "read_only" ? "local_reviewer" : await askDefault(ask, "Required approval role", optionalArg(args, "--approval-role") ?? recipeSpec?.approval?.required_role ?? "local_reviewer");
7112
- const spec = {
7157
+ let spec = {
7113
7158
  version: 1,
7114
7159
  engine: inspection.engine,
7115
7160
  mode,
@@ -7124,8 +7169,8 @@ async function runInitWizard(args, options = {}) {
7124
7169
  conflict_column: conflictAnswer || void 0,
7125
7170
  namespace,
7126
7171
  object_name: objectName,
7127
- inspect_tool_name: recipeSpec?.inspect_tool_name,
7128
- proposal_tool_name: recipeSpec?.proposal_tool_name,
7172
+ inspect_tool_name: inspectToolName,
7173
+ proposal_tool_name: proposalToolName,
7129
7174
  inspect_description: inspectDescription,
7130
7175
  inspect_returns_hint: inspectReturnsHint,
7131
7176
  proposal_description: proposalDescription,
@@ -7147,30 +7192,39 @@ async function runInitWizard(args, options = {}) {
7147
7192
  },
7148
7193
  writeback: writeback2
7149
7194
  };
7150
- const generated = generateRunnerConfigFromSpec(spec);
7151
- const generatedCapabilities = generated.config.capabilities;
7152
- const tools2 = generatedCapabilities.map((capability) => `${capability.name} (${capability.kind})`);
7153
- const smokeToolName = generatedCapabilities[0]?.name ?? "<inspect_tool>";
7195
+ let generated = generateRunnerConfigFromSpec(spec);
7154
7196
  stdout.write("\nPreview:\n");
7155
- stdout.write(` trusted context: tenant from ${tenantEnv}${singleTenantDev ? " (single-tenant dev source)" : tenantAnswer ? ` via ${tenantAnswer}` : ""}; principal from ${principalEnv}
7156
- `);
7157
- stdout.write(` source: ${inspection.engine} ${table.schema}.${table.name}
7158
- `);
7159
- stdout.write(` mode: ${mode}
7160
- `);
7161
- stdout.write(` result envelope: ${resultFormat ? `v${resultFormat}` : "default"}
7162
- `);
7163
- stdout.write(` writeback path: ${writeback2?.executor ?? (mode === "review" ? "sql_update" : "none")}
7164
- `);
7165
- stdout.write(` exposed tools: ${tools2.join(", ")}
7166
- `);
7167
- stdout.write(" not exposed: execute_sql, approval tools, commit tools, database URLs, write credentials, model-controlled tenant authority\n");
7197
+ printWizardContractPreview(stdout, { spec, generated, engine: inspection.engine, table });
7168
7198
  if (generatedHandlerTemplate) {
7169
7199
  stdout.write(` handler template: ${generatedHandlerTemplate.output}
7170
7200
  `);
7171
7201
  stdout.write(`${handlerSecurityWarning}
7172
7202
  `);
7173
7203
  }
7204
+ const editPreview = await askDefault(ask, "Edit visible fields or capability names before writing? Type yes to edit", "no");
7205
+ if (editPreview.toLowerCase() === "yes") {
7206
+ const updatedVisible = parseColumnList(await askDefault(
7207
+ ask,
7208
+ "Final visible columns",
7209
+ spec.visible_columns?.join(",") ?? visibleColumns.join(",")
7210
+ ));
7211
+ ensureColumnsExist(updatedVisible, columns, "visible");
7212
+ const currentReadTool = spec.inspect_tool_name ?? generated.config.capabilities.find((capability) => capability.kind === "read")?.name ?? inspectToolName;
7213
+ const updatedReadTool = await askDefault(ask, "Final read capability name", currentReadTool);
7214
+ const currentProposalTool = spec.proposal_tool_name ?? generated.config.capabilities.find((capability) => capability.kind === "proposal")?.name ?? proposalToolName ?? "";
7215
+ const updatedProposalTool = spec.mode === "read_only" ? void 0 : await askDefault(ask, "Final proposal capability name", currentProposalTool);
7216
+ spec = {
7217
+ ...spec,
7218
+ visible_columns: updatedVisible,
7219
+ inspect_tool_name: updatedReadTool,
7220
+ proposal_tool_name: updatedProposalTool
7221
+ };
7222
+ generated = generateRunnerConfigFromSpec(spec);
7223
+ stdout.write("\nUpdated preview:\n");
7224
+ printWizardContractPreview(stdout, { spec, generated, engine: inspection.engine, table });
7225
+ }
7226
+ const generatedCapabilities = generated.config.capabilities;
7227
+ const smokeToolName = generatedCapabilities[0]?.name ?? "<inspect_tool>";
7174
7228
  const confirmed = await askDefault(ask, "Write generated config and MCP snippets? Type yes to continue", "no");
7175
7229
  if (confirmed.toLowerCase() !== "yes") throw new Error("guided init canceled before writing files");
7176
7230
  const outputPath = outputArg(args) ?? "synapsor.runner.json";
@@ -7213,6 +7267,37 @@ async function runInitWizard(args, options = {}) {
7213
7267
  `);
7214
7268
  return 0;
7215
7269
  }
7270
+ function printWizardContractPreview(stdout, input) {
7271
+ const capabilities = input.generated.config.capabilities;
7272
+ const tools2 = capabilities.map((capability) => `${capability.name} (${capability.kind})`);
7273
+ const readCapability = capabilities.find((capability) => capability.kind === "read")?.name ?? input.spec.inspect_tool_name ?? "<read_tool>";
7274
+ const proposalCapability = capabilities.find((capability) => capability.kind === "proposal")?.name ?? input.spec.proposal_tool_name;
7275
+ const visibleColumns = input.spec.visible_columns ?? [];
7276
+ const tenantEnv = input.spec.trusted_context?.tenant_id_env ?? "SYNAPSOR_TENANT_ID";
7277
+ const principalEnv = input.spec.trusted_context?.principal_env ?? "SYNAPSOR_PRINCIPAL";
7278
+ const visiblePreview = visibleColumns.length <= 12 ? visibleColumns.join(", ") : `${visibleColumns.slice(0, 12).join(", ")} (+${visibleColumns.length - 12} more)`;
7279
+ stdout.write(` trusted context: tenant from ${tenantEnv}${input.spec.single_tenant_dev ? " (single-tenant dev source)" : input.spec.tenant_key ? ` via ${input.spec.tenant_key}` : ""}; principal from ${principalEnv}
7280
+ `);
7281
+ stdout.write(` source: ${input.engine} ${input.table.schema}.${input.table.name}
7282
+ `);
7283
+ stdout.write(` primary key: ${input.spec.primary_key}${input.spec.conflict_column ? `; conflict guard: ${input.spec.conflict_column}` : ""}
7284
+ `);
7285
+ stdout.write(` visible fields: ${visiblePreview || "none"}
7286
+ `);
7287
+ stdout.write(` mode: ${input.spec.mode}
7288
+ `);
7289
+ stdout.write(` result envelope: ${input.spec.result_format ? `v${input.spec.result_format}` : "default"}
7290
+ `);
7291
+ stdout.write(` writeback path: ${input.spec.writeback?.executor ?? (input.spec.mode === "review" ? "sql_update" : "none")}
7292
+ `);
7293
+ stdout.write(` read capability: ${readCapability}
7294
+ `);
7295
+ if (proposalCapability) stdout.write(` proposal capability: ${proposalCapability}
7296
+ `);
7297
+ stdout.write(` exposed tools: ${tools2.join(", ")}
7298
+ `);
7299
+ stdout.write(" not exposed: execute_sql, approval tools, commit tools, database URLs, write credentials, model-controlled tenant authority\n");
7300
+ }
7216
7301
  async function initFromSpec(args, specPath) {
7217
7302
  if (!args.includes("--non-interactive")) {
7218
7303
  throw new Error("init --spec requires --non-interactive so reviewed selections are explicit.");
@@ -7221,9 +7306,123 @@ async function initFromSpec(args, specPath) {
7221
7306
  const force = args.includes("--force");
7222
7307
  const spec = JSON.parse(await fs3.readFile(specPath, "utf8"));
7223
7308
  const generated = generateRunnerConfigFromSpec(spec);
7309
+ if (args.includes("--dry-run")) {
7310
+ process2.stdout.write(`${JSON.stringify(generated.config, null, 2)}
7311
+ `);
7312
+ return 0;
7313
+ }
7224
7314
  await writeGeneratedOnboardingFiles(output, generated, force);
7225
7315
  return 0;
7226
7316
  }
7317
+ async function initFromAnswers(args, answersPath) {
7318
+ const output = outputArg(args) ?? "synapsor.runner.json";
7319
+ const force = args.includes("--force");
7320
+ const raw = JSON.parse(await fs3.readFile(answersPath, "utf8"));
7321
+ const spec = answersToSelectionSpec(raw);
7322
+ const generated = generateRunnerConfigFromSpec(spec);
7323
+ if (args.includes("--dry-run")) {
7324
+ process2.stdout.write(`${JSON.stringify(generated.config, null, 2)}
7325
+ `);
7326
+ return 0;
7327
+ }
7328
+ await writeGeneratedOnboardingFiles(output, generated, force);
7329
+ await maybeWriteHandlerTemplateForArgs(args, spec.writeback);
7330
+ return 0;
7331
+ }
7332
+ function isScriptedOnboardingArgs(args) {
7333
+ return args.includes("--yes") || args.includes("--non-interactive") || args.includes("--dry-run") || Boolean(optionalArg(args, "--answers")) || Boolean(optionalArg(args, "--inspection-json")) || Boolean(optionalArg(args, "--table"));
7334
+ }
7335
+ function answersToSelectionSpec(raw) {
7336
+ if (!isRecord6(raw)) throw new Error("--answers file must contain a JSON object");
7337
+ const mode = stringValue(raw.mode) ?? "review";
7338
+ if (!["read_only", "shadow", "review"].includes(mode)) throw new Error("answers.mode must be read_only, shadow, or review");
7339
+ const engine = stringValue(raw.engine) ?? "postgres";
7340
+ if (engine !== "postgres" && engine !== "mysql") throw new Error("answers.engine must be postgres or mysql");
7341
+ const table = requiredAnswerString(raw.table, "table");
7342
+ const objectName = stringValue(raw.object_name) ?? safeObjectName(table);
7343
+ const namespace = stringValue(raw.namespace) ?? inferCapabilityNamespace(table);
7344
+ const writebackRaw = stringValue(raw.writeback) ?? "sql_update";
7345
+ if (!["sql_update", "http_handler", "command_handler"].includes(writebackRaw)) throw new Error("answers.writeback must be sql_update, http_handler, or command_handler");
7346
+ const writeback2 = writebackRaw === "sql_update" ? { executor: "sql_update" } : writebackRaw === "http_handler" ? {
7347
+ executor: "http_handler",
7348
+ handler_url_env: stringValue(raw.handler_url_env) ?? "SYNAPSOR_APP_WRITEBACK_URL",
7349
+ ...stringValue(raw.handler_token_env) ? { handler_token_env: stringValue(raw.handler_token_env) } : {},
7350
+ ...stringValue(raw.handler_signing_secret_env) ? { handler_signing_secret_env: stringValue(raw.handler_signing_secret_env) } : {}
7351
+ } : {
7352
+ executor: "command_handler",
7353
+ handler_command_env: stringValue(raw.handler_command_env) ?? "SYNAPSOR_APP_WRITEBACK_COMMAND"
7354
+ };
7355
+ const patch = parsePatchBindings(arrayOrStringList(raw.patch), "--answers.patch");
7356
+ const allowedColumns = arrayOrStringList(raw.allowed_columns);
7357
+ return {
7358
+ version: 1,
7359
+ engine,
7360
+ mode,
7361
+ source_name: stringValue(raw.source_name),
7362
+ read_url_env: stringValue(raw.read_url_env) ?? stringValue(raw.database_url_env) ?? "DATABASE_URL",
7363
+ write_url_env: writeback2.executor === "sql_update" ? stringValue(raw.write_url_env) ?? "SYNAPSOR_DATABASE_WRITE_URL" : stringValue(raw.write_url_env),
7364
+ schema: requiredAnswerString(raw.schema, "schema"),
7365
+ table,
7366
+ primary_key: requiredAnswerString(raw.primary_key, "primary_key"),
7367
+ tenant_key: stringValue(raw.tenant_column) ?? stringValue(raw.tenant_key),
7368
+ single_tenant_dev: raw.single_tenant_dev === true,
7369
+ conflict_column: stringValue(raw.conflict_column),
7370
+ namespace,
7371
+ object_name: objectName,
7372
+ inspect_tool_name: stringValue(raw.read_tool) ?? stringValue(raw.inspect_tool_name),
7373
+ proposal_tool_name: stringValue(raw.proposal_tool) ?? stringValue(raw.proposal_tool_name),
7374
+ lookup_arg: stringValue(raw.id_arg) ?? stringValue(raw.lookup_arg),
7375
+ inspect_description: stringValue(raw.read_description) ?? stringValue(raw.inspect_description),
7376
+ inspect_returns_hint: stringValue(raw.read_returns_hint) ?? stringValue(raw.inspect_returns_hint),
7377
+ proposal_description: stringValue(raw.proposal_description),
7378
+ proposal_returns_hint: stringValue(raw.proposal_returns_hint),
7379
+ result_format: resultFormatFromAnswerValue(raw.result_format),
7380
+ visible_columns: arrayOrStringList(raw.visible_columns),
7381
+ allowed_columns: allowedColumns.length > 0 ? allowedColumns : void 0,
7382
+ patch,
7383
+ numeric_bounds: parseNumericBoundsInput(arrayOrStringList(raw.patch_bounds ?? raw.numeric_bounds).join(",")),
7384
+ transition_guards: parseTransitionGuardsInput(arrayOrStringList(raw.status_guards ?? raw.transition_guards).join(",")),
7385
+ trusted_context: {
7386
+ tenant_id_env: stringValue(raw.tenant_env) ?? "SYNAPSOR_TENANT_ID",
7387
+ principal_env: stringValue(raw.principal_env) ?? "SYNAPSOR_PRINCIPAL"
7388
+ },
7389
+ approval: {
7390
+ required_role: stringValue(raw.approval_role) ?? "local_reviewer"
7391
+ },
7392
+ writeback: writeback2
7393
+ };
7394
+ }
7395
+ async function maybeWriteHandlerTemplateForArgs(args, writeback2) {
7396
+ if (!writeback2 || writeback2.executor === "sql_update" || args.includes("--no-emit-handler") || args.includes("--skip-handler-template")) return;
7397
+ if (!args.includes("--emit-handler") && !optionalArg(args, "--handler-template") && !optionalArg(args, "--handler-output") && !optionalArg(args, "--handler-template-output")) return;
7398
+ const defaultTemplate = writeback2.executor === "command_handler" ? "command" : "node-fastify";
7399
+ const template = resolveHandlerTemplateName(optionalArg(args, "--handler-template") ?? defaultTemplate);
7400
+ const output = optionalArg(args, "--handler-output") ?? optionalArg(args, "--handler-template-output") ?? handlerTemplateDefinitions[template].fileName;
7401
+ await writeHandlerTemplateFile(template, output, args.includes("--force"));
7402
+ process2.stdout.write(`created ${output}
7403
+ `);
7404
+ process2.stdout.write(`${handlerSecurityWarning}
7405
+ `);
7406
+ }
7407
+ function stringValue(value) {
7408
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
7409
+ }
7410
+ function requiredAnswerString(value, field) {
7411
+ const result = stringValue(value);
7412
+ if (!result) throw new Error(`--answers missing ${field}`);
7413
+ return result;
7414
+ }
7415
+ function arrayOrStringList(value) {
7416
+ if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean);
7417
+ if (typeof value === "string") return value.split(",").map((item) => item.trim()).filter(Boolean);
7418
+ return [];
7419
+ }
7420
+ function resultFormatFromAnswerValue(value) {
7421
+ if (value === void 0 || value === null || value === "" || value === "default") return void 0;
7422
+ if (value === 1 || value === "1" || value === "v1") return 1;
7423
+ if (value === 2 || value === "2" || value === "v2") return 2;
7424
+ throw new Error("result_format must be default, v1, v2, 1, or 2");
7425
+ }
7227
7426
  async function initFromInspection(args, inspection, databaseUrlEnv) {
7228
7427
  const tableName = optionalArg(args, "--table");
7229
7428
  if (!tableName) {
@@ -7247,7 +7446,7 @@ async function initFromInspection(args, inspection, databaseUrlEnv) {
7247
7446
  process2.stderr.write(`warning: no database primary-key constraint detected for ${table.schema}.${table.name}; using candidate column ${primaryKey}. Verify uniqueness before enabling writeback.
7248
7447
  `);
7249
7448
  }
7250
- const tenantKey = optionalArg(args, "--tenant-key") ?? table.suggestions.tenant_columns[0];
7449
+ const tenantKey = optionalArg(args, "--tenant-column") ?? optionalArg(args, "--tenant-key") ?? table.suggestions.tenant_columns[0];
7251
7450
  const singleTenantDev = args.includes("--single-tenant-dev");
7252
7451
  if (!tenantKey && !singleTenantDev) {
7253
7452
  throw new Error(`--tenant-key is required for ${table.schema}.${table.name}, or pass --single-tenant-dev for a reviewed single-tenant dev source.`);
@@ -7270,24 +7469,28 @@ async function initFromInspection(args, inspection, databaseUrlEnv) {
7270
7469
  const allowedColumns = listArg(args, "--allowed-columns") ?? Object.keys(patch);
7271
7470
  const writeback2 = writebackSpecFromArgs(args);
7272
7471
  const sqlWriteback = (writeback2?.executor ?? "sql_update") === "sql_update";
7472
+ const objectName = optionalArg(args, "--object-name") ?? safeObjectName(table.name);
7473
+ const namespace = optionalArg(args, "--namespace") ?? inferCapabilityNamespace(table.name);
7273
7474
  const spec = {
7274
7475
  version: 1,
7275
7476
  engine: inspection.engine,
7276
7477
  mode,
7277
7478
  source_name: optionalArg(args, "--source-name"),
7278
7479
  read_url_env: databaseUrlEnv,
7279
- write_url_env: sqlWriteback ? optionalArg(args, "--write-url-env") ?? "SYNAPSOR_DATABASE_WRITE_URL" : void 0,
7480
+ write_url_env: sqlWriteback ? optionalArg(args, "--write-url-env") ?? "SYNAPSOR_DATABASE_WRITE_URL" : optionalArg(args, "--write-url-env"),
7280
7481
  schema: table.schema,
7281
7482
  table: table.name,
7282
7483
  primary_key: primaryKey,
7283
7484
  tenant_key: tenantKey,
7284
7485
  single_tenant_dev: singleTenantDev,
7285
7486
  conflict_column: conflictColumn,
7286
- namespace: optionalArg(args, "--namespace") ?? "source",
7287
- object_name: optionalArg(args, "--object-name"),
7288
- lookup_arg: optionalArg(args, "--lookup-arg"),
7289
- inspect_description: optionalArg(args, "--inspect-description"),
7290
- inspect_returns_hint: optionalArg(args, "--inspect-returns-hint"),
7487
+ namespace,
7488
+ object_name: objectName,
7489
+ inspect_tool_name: optionalArg(args, "--read-tool") ?? optionalArg(args, "--inspect-tool-name"),
7490
+ proposal_tool_name: optionalArg(args, "--proposal-tool") ?? optionalArg(args, "--proposal-tool-name"),
7491
+ lookup_arg: optionalArg(args, "--id-arg") ?? optionalArg(args, "--lookup-arg"),
7492
+ inspect_description: optionalArg(args, "--read-description") ?? optionalArg(args, "--inspect-description"),
7493
+ inspect_returns_hint: optionalArg(args, "--read-returns-hint") ?? optionalArg(args, "--inspect-returns-hint"),
7291
7494
  proposal_description: optionalArg(args, "--proposal-description"),
7292
7495
  proposal_returns_hint: optionalArg(args, "--proposal-returns-hint"),
7293
7496
  result_format: resultFormatOption(args),
@@ -7306,7 +7509,13 @@ async function initFromInspection(args, inspection, databaseUrlEnv) {
7306
7509
  writeback: writeback2
7307
7510
  };
7308
7511
  const generated = generateRunnerConfigFromSpec(spec);
7512
+ if (args.includes("--dry-run")) {
7513
+ process2.stdout.write(`${JSON.stringify(generated.config, null, 2)}
7514
+ `);
7515
+ return 0;
7516
+ }
7309
7517
  await writeGeneratedOnboardingFiles(outputArg(args) ?? "synapsor.runner.json", generated, args.includes("--force"));
7518
+ await maybeWriteHandlerTemplateForArgs(args, writeback2);
7310
7519
  process2.stdout.write(`selected ${table.schema}.${table.name} from ${inspection.engine} inspection
7311
7520
  `);
7312
7521
  process2.stdout.write(`exposed tools: ${generated.config.capabilities.map((capability) => capability.name).join(", ")}
@@ -7522,6 +7731,11 @@ function safeObjectName(tableName) {
7522
7731
  const base = tableName.replace(/[^A-Za-z0-9_]/g, "_").replace(/s$/, "");
7523
7732
  return /^[A-Za-z_]/.test(base) ? base : `record_${base}`;
7524
7733
  }
7734
+ function inferCapabilityNamespace(tableName) {
7735
+ const objectName = safeObjectName(tableName);
7736
+ const [firstPart] = objectName.split("_").filter(Boolean);
7737
+ return firstPart ?? objectName;
7738
+ }
7525
7739
  function requiredWritebackEngine(args) {
7526
7740
  const value = optionalArg(args, "--engine") ?? firstPositional(args);
7527
7741
  if (value === "postgres" || value === "mysql") return value;
@@ -7626,6 +7840,7 @@ function repeatedArgs(args, flag) {
7626
7840
  }
7627
7841
  function parsePatchFlags(args) {
7628
7842
  const patch = {};
7843
+ Object.assign(patch, parsePatchBindings(repeatedArgs(args, "--patch"), "--patch"));
7629
7844
  for (const binding of repeatedArgs(args, "--patch-fixed")) {
7630
7845
  const [column, ...rest] = binding.split("=");
7631
7846
  const value = rest.join("=");
@@ -7640,8 +7855,27 @@ function parsePatchFlags(args) {
7640
7855
  }
7641
7856
  return patch;
7642
7857
  }
7858
+ function parsePatchBindings(bindings, label) {
7859
+ const patch = {};
7860
+ for (const rawBinding of bindings.flatMap((binding) => binding.split(",")).map((item) => item.trim()).filter(Boolean)) {
7861
+ const [column, ...rest] = rawBinding.split("=");
7862
+ const expression = rest.join("=");
7863
+ if (!column || !expression) throw new Error(`${label} must use column=fixed:value or column=arg:name`);
7864
+ const [kind, ...valueParts] = expression.split(":");
7865
+ const value = valueParts.join(":");
7866
+ if (!valueParts.length || !value) throw new Error(`${label} must use column=fixed:value or column=arg:name`);
7867
+ if (kind === "fixed") {
7868
+ patch[column] = { fixed: parseFixedPatchValue(value) };
7869
+ } else if (kind === "arg") {
7870
+ patch[column] = { from_arg: value };
7871
+ } else {
7872
+ throw new Error(`${label} patch kind for ${column} must be fixed or arg`);
7873
+ }
7874
+ }
7875
+ return patch;
7876
+ }
7643
7877
  function parseNumericBoundsFlags(args) {
7644
- return parseNumericBoundsInput(repeatedArgs(args, "--numeric-bound").join(","));
7878
+ return parseNumericBoundsInput([...repeatedArgs(args, "--numeric-bound"), ...repeatedArgs(args, "--patch-bounds")].join(","));
7645
7879
  }
7646
7880
  function parseNumericBoundsInput(input) {
7647
7881
  const bounds = {};
@@ -7673,7 +7907,7 @@ function formatNumericBounds(bounds) {
7673
7907
  return Object.entries(bounds).map(([column, bound]) => `${column}=${bound.minimum ?? ""}:${bound.maximum ?? ""}`).join(",");
7674
7908
  }
7675
7909
  function parseTransitionGuardFlags(args) {
7676
- return parseTransitionGuardsInput(repeatedArgs(args, "--transition-guard").join(","));
7910
+ return parseTransitionGuardsInput([...repeatedArgs(args, "--transition-guard"), ...repeatedArgs(args, "--status-guards")].join(","));
7677
7911
  }
7678
7912
  function parseTransitionGuardsInput(input) {
7679
7913
  const guards = {};
@@ -9248,6 +9482,8 @@ async function up(args = []) {
9248
9482
  "--config",
9249
9483
  "--store",
9250
9484
  "--transport",
9485
+ "--serve",
9486
+ "--with-handler",
9251
9487
  "--host",
9252
9488
  "--port",
9253
9489
  "--auth-token-env",
@@ -9267,10 +9503,14 @@ async function up(args = []) {
9267
9503
  const configPath = optionalArg(args, "--config") ?? defaultConfigPath;
9268
9504
  const config = await readRuntimeConfig(configPath);
9269
9505
  const storePath = optionalArg(args, "--store") ?? config.storage?.sqlite_path ?? defaultStorePath;
9270
- const transport = optionalArg(args, "--transport") ?? "stdio";
9506
+ const serveRequested = args.includes("--serve");
9507
+ const transport = optionalArg(args, "--transport") ?? (serveRequested ? "streamable-http" : "stdio");
9271
9508
  if (transport !== "stdio" && transport !== "streamable-http") {
9272
9509
  throw new Error("--transport must be stdio or streamable-http");
9273
9510
  }
9511
+ if (serveRequested && transport === "stdio") {
9512
+ throw new Error("up --serve starts the Streamable HTTP MCP server. Omit --transport or use --transport streamable-http; for stdio, use mcp client-config so the client launches Runner.");
9513
+ }
9274
9514
  const port = Number(optionalArg(args, "--port") ?? "8766");
9275
9515
  if (transport === "streamable-http" && (!Number.isInteger(port) || port <= 0 || port > 65535)) {
9276
9516
  throw new Error("--port must be an integer from 1 to 65535");
@@ -9304,15 +9544,17 @@ async function up(args = []) {
9304
9544
  openUi: args.includes("--open-ui"),
9305
9545
  port,
9306
9546
  resultFormat,
9547
+ serveRequested,
9307
9548
  storePath,
9308
9549
  transport
9309
9550
  }));
9310
- if (args.includes("--handler-check")) {
9551
+ if (args.includes("--with-handler") || args.includes("--handler-check")) {
9311
9552
  process2.stdout.write("\nHandler check:\n");
9312
9553
  const doctorCode = await doctor(["--config", configPath, "--store", storePath, "--check-handlers"]);
9313
9554
  if (doctorCode !== 0) return doctorCode;
9314
9555
  }
9315
- if (args.includes("--dry-run") || transport === "stdio") return boundary.ok ? 0 : 1;
9556
+ if (args.includes("--dry-run") || !serveRequested) return boundary.ok ? 0 : 1;
9557
+ if (!boundary.ok) return 1;
9316
9558
  const serveArgs = [
9317
9559
  "--config",
9318
9560
  configPath,
@@ -9341,6 +9583,7 @@ function formatReviewModeUp(input) {
9341
9583
  `Store: ${input.storePath}`,
9342
9584
  `Mode: ${input.config.mode}`,
9343
9585
  `Transport: ${input.transport}`,
9586
+ `Serve now: ${input.serveRequested ? "yes" : "no"}`,
9344
9587
  `Alias mode: ${input.aliasMode}`,
9345
9588
  `Result format: ${input.resultFormat ? `v${input.resultFormat}` : configResultFormat(input.config)}`,
9346
9589
  `Dry run: ${input.dryRun ? "yes" : "no"}`,
@@ -9366,7 +9609,7 @@ function formatReviewModeUp(input) {
9366
9609
  lines.push(
9367
9610
  ` Streamable HTTP endpoint: http://${input.host}:${input.port}/mcp`,
9368
9611
  ` Auth token env: ${input.authTokenEnv} (${process2.env[input.authTokenEnv] ? "set" : "missing"})`,
9369
- ` Start command: ${cliCommandName()} mcp serve-streamable-http --config ${input.configPath} --store ${input.storePath} --port ${input.port} --auth-token-env ${input.authTokenEnv} --alias-mode ${input.aliasMode}`
9612
+ input.serveRequested ? input.dryRun ? " Status: dry run only; server not started." : " Status: starting after this checklist." : ` Start command: ${cliCommandName()} up --serve --config ${input.configPath} --store ${input.storePath} --port ${input.port} --auth-token-env ${input.authTokenEnv} --alias-mode ${input.aliasMode}`
9370
9613
  );
9371
9614
  }
9372
9615
  if (input.openUi) {
@@ -9477,7 +9720,7 @@ async function cloudConnect(args) {
9477
9720
  return 1;
9478
9721
  }
9479
9722
  const runnerId = String(parsed.cloud.runner_id || process2.env.SYNAPSOR_RUNNER_ID || "synapsor_runner_local").trim();
9480
- const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0-alpha.16").trim();
9723
+ const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0-alpha.17").trim();
9481
9724
  const engines = normalizeEngines(parsed.cloud.engines);
9482
9725
  const capabilities = normalizeCapabilities(parsed.cloud.capabilities);
9483
9726
  const client = new ControlPlaneClient({
@@ -9702,8 +9945,10 @@ async function onboard(args) {
9702
9945
  process2.stdout.write("You will inspect metadata, choose one table/view, confirm safety rules, and generate semantic MCP tools without writing JSON by hand.\n\n");
9703
9946
  const outputPath = outputArg(rest) ?? "synapsor.runner.json";
9704
9947
  const storePath = optionalArg(rest, "--store") ?? "./.synapsor/local.db";
9705
- const result = await runInitWizard(["--wizard", ...rest]);
9948
+ const scripted = isScriptedOnboardingArgs(rest);
9949
+ const result = scripted ? await init(["--non-interactive", ...rest]) : await runInitWizard(["--wizard", ...rest]);
9706
9950
  if (result !== 0) return result;
9951
+ if (rest.includes("--dry-run")) return 0;
9707
9952
  process2.stdout.write("\nValidation:\n");
9708
9953
  const configCode = await configValidate(["--config", outputPath]);
9709
9954
  const smokeCode = await mcpSmoke(["--config", outputPath, "--store", storePath]);
@@ -11444,6 +11689,7 @@ async function activity(args) {
11444
11689
  async function events(args) {
11445
11690
  const [subcommand, ...rest] = args;
11446
11691
  if (subcommand === "tail") return eventsTail(rest);
11692
+ if (subcommand === "webhook" || subcommand === "push") return eventsWebhook(rest);
11447
11693
  usage(["events"]);
11448
11694
  return 2;
11449
11695
  }
@@ -12018,6 +12264,75 @@ async function eventsTail(args) {
12018
12264
  });
12019
12265
  return 0;
12020
12266
  }
12267
+ async function eventsWebhook(args) {
12268
+ assertKnownOptions(args, eventWebhookAllowedOptions, "events webhook");
12269
+ const url = optionalArg(args, "--url") ?? envValue(optionalArg(args, "--url-env"));
12270
+ if (!url) throw new Error("events webhook requires --url <https://...> or --url-env <ENV>");
12271
+ const endpoint = new URL(url);
12272
+ if (!["http:", "https:"].includes(endpoint.protocol)) throw new Error("events webhook URL must use http or https");
12273
+ const follow = args.includes("--follow");
12274
+ const dryRun = args.includes("--dry-run");
12275
+ const jsonOutput = args.includes("--json");
12276
+ if (follow && jsonOutput) throw new Error("events webhook --follow does not support --json");
12277
+ const storePath = optionalArg(args, "--store");
12278
+ const intervalMs = Number(optionalArg(args, "--interval-ms") ?? "1000");
12279
+ const timeoutMs = Number(optionalArg(args, "--timeout-ms") ?? "5000");
12280
+ const sinceEventId = optionalPositiveIntegerArg(args, "--since-event-id");
12281
+ if (!Number.isFinite(intervalMs) || intervalMs < 250) throw new Error("--interval-ms must be at least 250");
12282
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 250) throw new Error("--timeout-ms must be at least 250");
12283
+ const token = envValue(optionalArg(args, "--auth-token-env"));
12284
+ const filters = eventFiltersFromArgs(args);
12285
+ const seen = /* @__PURE__ */ new Set();
12286
+ const pushOnce = async () => {
12287
+ const store = await openLocalStore(["--store", storePath ?? "./.synapsor/local.db"]);
12288
+ try {
12289
+ const rows = store.listEvents(filters).filter((event) => sinceEventId === void 0 || event.event_id > sinceEventId).sort((left, right) => left.event_id - right.event_id).filter((event) => !seen.has(event.event_id));
12290
+ rows.forEach((event) => seen.add(event.event_id));
12291
+ let delivered = 0;
12292
+ const payloads = [];
12293
+ for (const event of rows) {
12294
+ const payload = localEventWebhookPayload(event, store.path);
12295
+ payloads.push(payload);
12296
+ if (dryRun) {
12297
+ if (!jsonOutput) process2.stdout.write(`${JSON.stringify(payload, null, 2)}
12298
+ `);
12299
+ } else {
12300
+ await postLocalEventWebhook(endpoint, payload, { token, timeoutMs });
12301
+ if (!jsonOutput) process2.stdout.write(`pushed event ${event.event_id} ${event.kind} for ${event.proposal_id} to ${redactWebhookUrl(endpoint)}
12302
+ `);
12303
+ }
12304
+ delivered += 1;
12305
+ }
12306
+ if (rows.length === 0 && !follow && !jsonOutput) process2.stdout.write(dryRun ? "No local events matched for dry-run.\n" : "No local events matched.\n");
12307
+ return { delivered, payloads };
12308
+ } finally {
12309
+ store.close();
12310
+ }
12311
+ };
12312
+ const first = await pushOnce();
12313
+ if (jsonOutput && !follow) {
12314
+ process2.stdout.write(`${JSON.stringify({ ok: true, dry_run: dryRun, delivered: first.delivered, webhook: redactWebhookUrl(endpoint), events: first.payloads }, null, 2)}
12315
+ `);
12316
+ }
12317
+ if (!follow) return 0;
12318
+ await new Promise((resolve) => {
12319
+ const timer = setInterval(() => {
12320
+ void pushOnce().catch((error) => {
12321
+ process2.stderr.write(`events webhook error: ${safeErrorMessage(error)}
12322
+ `);
12323
+ });
12324
+ }, intervalMs);
12325
+ const stop = () => {
12326
+ clearInterval(timer);
12327
+ process2.off("SIGINT", stop);
12328
+ process2.off("SIGTERM", stop);
12329
+ resolve();
12330
+ };
12331
+ process2.once("SIGINT", stop);
12332
+ process2.once("SIGTERM", stop);
12333
+ });
12334
+ return 0;
12335
+ }
12021
12336
  async function storeStats(args) {
12022
12337
  assertKnownOptions(args, storeStatsAllowedOptions, "store stats");
12023
12338
  const store = await openLocalStore(args);
@@ -12167,6 +12482,15 @@ var eventTailAllowedOptions = /* @__PURE__ */ new Set([
12167
12482
  "--follow",
12168
12483
  "--interval-ms"
12169
12484
  ]);
12485
+ var eventWebhookAllowedOptions = /* @__PURE__ */ new Set([
12486
+ ...eventTailAllowedOptions,
12487
+ "--url",
12488
+ "--url-env",
12489
+ "--auth-token-env",
12490
+ "--timeout-ms",
12491
+ "--since-event-id",
12492
+ "--dry-run"
12493
+ ]);
12170
12494
  var replayShowAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--proposal", "--replay", "--evidence"]);
12171
12495
  var replayExportAllowedOptions = /* @__PURE__ */ new Set([...replayShowAllowedOptions, "--output", "--out", "--format"]);
12172
12496
  var replayListAllowedOptions = /* @__PURE__ */ new Set([
@@ -12369,6 +12693,17 @@ function eventFiltersFromArgs(args) {
12369
12693
  limit: limitFromArgs(args)
12370
12694
  };
12371
12695
  }
12696
+ function optionalPositiveIntegerArg(args, flag) {
12697
+ const value = optionalArg(args, flag);
12698
+ if (!value) return void 0;
12699
+ const parsed = Number(value);
12700
+ if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${flag} must be a non-negative integer`);
12701
+ return parsed;
12702
+ }
12703
+ function envValue(name) {
12704
+ if (!name) return void 0;
12705
+ return process2.env[name];
12706
+ }
12372
12707
  function linkedProposalFilter(args, store, options = {}) {
12373
12708
  const noLinkedProposal = "__synapsor_no_linked_proposal__";
12374
12709
  const replay2 = optionalArg(args, "--replay");
@@ -12702,6 +13037,10 @@ function firstPositional(args) {
12702
13037
  "--reason",
12703
13038
  "--recipe",
12704
13039
  "--receipt",
13040
+ "--read-tool",
13041
+ "--inspect-tool-name",
13042
+ "--proposal-tool",
13043
+ "--proposal-tool-name",
12705
13044
  "--replay",
12706
13045
  "--runner",
12707
13046
  "--schema",
@@ -13342,6 +13681,48 @@ function formatEventLine(event, details = false) {
13342
13681
  return `${lines.join("\n")}
13343
13682
  `;
13344
13683
  }
13684
+ function localEventWebhookPayload(event, storePath) {
13685
+ return {
13686
+ schema_version: "synapsor.local-event-webhook.v1",
13687
+ delivered_at: (/* @__PURE__ */ new Date()).toISOString(),
13688
+ source: {
13689
+ kind: "local_store",
13690
+ store_path: storePath
13691
+ },
13692
+ event
13693
+ };
13694
+ }
13695
+ async function postLocalEventWebhook(endpoint, payload, options) {
13696
+ const controller = new AbortController();
13697
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
13698
+ try {
13699
+ const headers = {
13700
+ "content-type": "application/json",
13701
+ accept: "application/json",
13702
+ "user-agent": "synapsor-runner-events-webhook"
13703
+ };
13704
+ if (options.token) headers.authorization = `Bearer ${options.token}`;
13705
+ const response = await fetch(endpoint, {
13706
+ method: "POST",
13707
+ headers,
13708
+ body: JSON.stringify(payload),
13709
+ signal: controller.signal
13710
+ });
13711
+ if (!response.ok) {
13712
+ const text = await response.text().catch(() => "");
13713
+ throw new Error(`webhook returned HTTP ${response.status}${text ? `: ${text.slice(0, 200)}` : ""}`);
13714
+ }
13715
+ } finally {
13716
+ clearTimeout(timeout);
13717
+ }
13718
+ }
13719
+ function redactWebhookUrl(endpoint) {
13720
+ const copy = new URL(endpoint.toString());
13721
+ copy.username = "";
13722
+ copy.password = "";
13723
+ copy.search = "";
13724
+ return copy.toString();
13725
+ }
13345
13726
  function safeErrorMessage(error) {
13346
13727
  return error instanceof Error ? error.message : String(error);
13347
13728
  }
@@ -13629,7 +14010,7 @@ function starterCloudConfig() {
13629
14010
  base_url_env: "SYNAPSOR_CLOUD_BASE_URL",
13630
14011
  runner_token_env: "SYNAPSOR_RUNNER_TOKEN",
13631
14012
  runner_id: "synapsor_runner_local",
13632
- runner_version: "0.1.0-alpha.16",
14013
+ runner_version: "0.1.0-alpha.17",
13633
14014
  project_id: "token_scope",
13634
14015
  adapter_id: "mcp.your_adapter",
13635
14016
  source_id: "src_replace_me",
@@ -13722,7 +14103,7 @@ Commands:
13722
14103
  query-audit Inspect local query audit records
13723
14104
  receipts Inspect guarded writeback receipts
13724
14105
  activity Search local evidence/replay ledger
13725
- events Tail local proposal/writeback lifecycle events
14106
+ events Tail or push local proposal/writeback lifecycle events
13726
14107
  store Inspect and maintain the local SQLite ledger
13727
14108
  apply Apply an approved proposal with guarded writeback
13728
14109
  replay Show what happened
@@ -13744,7 +14125,7 @@ Examples:
13744
14125
  `,
13745
14126
  up: `Usage:
13746
14127
  ${cmd} up --config ./synapsor.runner.json --store ./.synapsor/local.db [--transport stdio|streamable-http]
13747
- ${cmd} up --config ./synapsor.runner.json --store ./.synapsor/local.db --transport streamable-http --port 8766 --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
14128
+ ${cmd} up --serve --config ./synapsor.runner.json --store ./.synapsor/local.db --port 8766 --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
13748
14129
  ${cmd} up --config ./synapsor.runner.json --store ./.synapsor/local.db --handler-check --dry-run
13749
14130
 
13750
14131
  Validate the local Runner config and store, summarize model-facing tools,
@@ -13752,13 +14133,16 @@ explain direct SQL versus app-owned executor writeback, and print the next
13752
14133
  smoke/approve/apply/replay commands.
13753
14134
 
13754
14135
  With --transport stdio, \`${cmd} up\` prints MCP client wiring because stdio is
13755
- launched by the client. With --transport streamable-http and without --dry-run,
13756
- it starts the standard Streamable HTTP MCP server.
14136
+ launched by the client. \`${cmd} up --serve\` starts the standard Streamable HTTP
14137
+ MCP server after the checklist. Use --with-handler to run the handler doctor
14138
+ before serving app-owned writeback configs.
13757
14139
 
13758
14140
  Options:
14141
+ --serve
13759
14142
  --alias-mode canonical|openai|both
13760
14143
  --result-format v1|v2
13761
14144
  --handler-check
14145
+ --with-handler
13762
14146
  --open-ui
13763
14147
  --dry-run
13764
14148
  `,
@@ -13785,12 +14169,17 @@ Inspect schema metadata without mutating the database or printing credentials.
13785
14169
  init: `Usage:
13786
14170
  ${cmd} init --wizard --from-env DATABASE_URL [--mode read_only|review|shadow] [--out synapsor.runner.json]
13787
14171
  ${cmd} init --engine postgres --url-env DATABASE_URL --mode review --table public.invoices
13788
- ${cmd} init --inspection-json schema.json --table invoices --mode review --patch-from-arg waiver_reason=reason
13789
- ${cmd} init --inspection-json schema.json --table invoices --mode review --writeback http_handler --handler-url-env APP_WRITEBACK_URL [--handler-signing-secret-env APP_WRITEBACK_SIGNING_SECRET]
14172
+ ${cmd} init --inspection-json schema.json --table invoices --mode review --patch late_fee_cents=fixed:0,waiver_reason=arg:reason
14173
+ ${cmd} init --answers answers.json --yes
14174
+ ${cmd} init --inspection-json schema.json --table invoices --mode review --writeback http_handler --handler-url-env APP_WRITEBACK_URL --emit-handler [--handler-signing-secret-env APP_WRITEBACK_SIGNING_SECRET]
13790
14175
 
13791
14176
  Generate a reviewed Synapsor Runner contract. Defaults to read-only in the wizard.
13792
14177
  Review mode writeback choices: sql_update, http_handler, command_handler.
13793
- `,
14178
+ If --namespace is omitted, init derives one from the table name instead of using source.*.
14179
+ Use --read-tool and --proposal-tool to override the exact model-facing capability names.
14180
+ The guided wizard shows a final preview and lets you revise visible fields or capability names before writing files.
14181
+ Use --yes/--non-interactive plus explicit flags, or --answers, for script/agent-friendly setup without prompts.
14182
+ `,
13794
14183
  mcp: `Usage:
13795
14184
  ${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
13796
14185
  ${cmd} mcp serve --transport streamable-http --config ./synapsor.runner.json --store ./.synapsor/local.db --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
@@ -13917,9 +14306,12 @@ The template receives an approved proposal writeback request and must return an
13917
14306
  `,
13918
14307
  onboard: `Usage:
13919
14308
  ${cmd} onboard db --from-env DATABASE_URL [--schema public] [--mode read_only|shadow|review]
13920
- ${cmd} onboard db --from-env DATABASE_URL --mode review --writeback http_handler --handler-url-env APP_WRITEBACK_URL
14309
+ ${cmd} onboard db --from-env DATABASE_URL --table invoices --mode review --patch late_fee_cents=fixed:0 --write-url-env SYNAPSOR_DATABASE_WRITE_URL --yes
14310
+ ${cmd} onboard db --from-env DATABASE_URL --table invoices --mode review --writeback http_handler --handler-url-env APP_WRITEBACK_URL --emit-handler --yes
14311
+ ${cmd} onboard db --answers answers.json --yes
13921
14312
 
13922
14313
  Guided own-database setup: inspect schema, choose one object, create trusted context, choose read-only/shadow/review writeback mode, generate semantic tools, validate config, run a tool-boundary smoke check, and print the MCP/UI next steps.
14314
+ Use --yes/--non-interactive with explicit flags, or --answers, when CI or an LLM agent must run without prompts.
13923
14315
  `,
13924
14316
  propose: `Usage:
13925
14317
  ${cmd} propose <capability-name> --sample [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
@@ -14023,12 +14415,15 @@ Search the local SQLite evidence/replay ledger across proposals, evidence, query
14023
14415
  `,
14024
14416
  events: `Usage:
14025
14417
  ${cmd} events tail --store ./.synapsor/local.db
14026
- ${cmd} events tail --proposal wrp_...
14027
- ${cmd} events tail --kind writeback_applied
14028
- ${cmd} events tail --follow --interval-ms 1000
14418
+ ${cmd} events tail --proposal wrp_...
14419
+ ${cmd} events tail --kind writeback_applied
14420
+ ${cmd} events tail --follow --interval-ms 1000
14421
+ ${cmd} events webhook --url http://127.0.0.1:8788/synapsor/events --kind proposal_created
14422
+ ${cmd} events webhook --url-env SYNAPSOR_EVENT_WEBHOOK_URL --auth-token-env SYNAPSOR_EVENT_WEBHOOK_TOKEN --follow
14423
+ ${cmd} events webhook --url http://127.0.0.1:8788/synapsor/events --dry-run
14029
14424
 
14030
- Show local proposal/writeback lifecycle events such as proposal_created, proposal_approved, writeback_applied, writeback_conflict, and writeback_failed.
14031
- `,
14425
+ Show or push local proposal/writeback lifecycle events such as proposal_created, proposal_approved, writeback_applied, writeback_conflict, and writeback_failed. Webhook delivery POSTs one local event envelope per event and never exposes database credentials.
14426
+ `,
14032
14427
  store: `Usage:
14033
14428
  ${cmd} store stats --store ./.synapsor/local.db
14034
14429
  ${cmd} store vacuum --store ./.synapsor/local.db