@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/CHANGELOG.md +32 -0
- package/README.md +107 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/runner.mjs +454 -59
- package/docs/README.md +3 -3
- package/docs/current-scope.md +6 -0
- package/docs/getting-started-own-database.md +42 -14
- package/docs/handler-helper.md +6 -6
- package/docs/local-mode.md +6 -0
- package/docs/release-notes.md +62 -13
- package/docs/release-policy.md +39 -13
- package/docs/result-envelope-v2.md +5 -2
- package/docs/rfcs/002-app-owned-handler-helper.md +1 -1
- package/docs/rfcs/003-integrator-feedback-teardown.md +1 -1
- package/examples/mcp-postgres-billing-app-handler/app-handler.mjs +1 -9
- package/package.json +1 -1
- package/schemas/onboarding-selection.v1.schema.json +2 -0
- package/schemas/synapsor.runner.schema.json +3 -0
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7039
|
-
const
|
|
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")) : "
|
|
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
|
-
|
|
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:
|
|
7128
|
-
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
|
-
|
|
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
|
|
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" :
|
|
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
|
|
7287
|
-
object_name:
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
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
|
|
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") ||
|
|
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()}
|
|
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.
|
|
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
|
|
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.
|
|
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 --
|
|
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.
|
|
13756
|
-
|
|
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
|
|
13789
|
-
${cmd} init --
|
|
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 --
|
|
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
|
-
|
|
14027
|
-
|
|
14028
|
-
|
|
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
|