@synapsor/runner 0.1.0-alpha.15 → 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 +56 -6
- package/README.md +126 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/runner.mjs +787 -51
- package/docs/README.md +3 -3
- package/docs/app-owned-executors.md +7 -0
- package/docs/current-scope.md +6 -0
- package/docs/getting-started-own-database.md +42 -14
- package/docs/handler-helper.md +13 -6
- package/docs/local-mode.md +6 -0
- package/docs/release-notes.md +90 -7
- package/docs/release-policy.md +53 -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/docs/writeback-executors.md +13 -0
- package/examples/app-owned-writeback/README.md +7 -0
- package/examples/app-owned-writeback/command-handler.mjs +9 -0
- package/examples/app-owned-writeback/node-fastify-handler.mjs +9 -0
- package/examples/app-owned-writeback/python-fastapi-handler.py +9 -0
- package/examples/mcp-postgres-billing-app-handler/README.md +7 -0
- package/examples/mcp-postgres-billing-app-handler/app-handler.mjs +7 -9
- package/package.json +1 -1
- package/schemas/onboarding-selection.v1.schema.json +7 -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,
|
|
@@ -4525,15 +4551,18 @@ function generateRunnerConfigFromSpec(spec) {
|
|
|
4525
4551
|
const lookupArg = spec.lookup_arg ?? `${objectName}_id`;
|
|
4526
4552
|
const inspectToolName = spec.inspect_tool_name ?? `${spec.namespace}.inspect_${objectName}`;
|
|
4527
4553
|
const proposalToolName = spec.proposal_tool_name ?? `${spec.namespace}.propose_${objectName}_update`;
|
|
4554
|
+
const objectLabel = objectName.replace(/_/g, " ");
|
|
4528
4555
|
const visibleColumns = unique([spec.primary_key, spec.tenant_key, spec.conflict_column, ...spec.visible_columns].filter((value) => Boolean(value)));
|
|
4529
4556
|
const readCapability = {
|
|
4530
4557
|
name: inspectToolName,
|
|
4531
4558
|
kind: "read",
|
|
4559
|
+
description: spec.inspect_description ?? `Inspect one ${objectLabel} in trusted tenant scope before answering or proposing a change.`,
|
|
4560
|
+
returns_hint: spec.inspect_returns_hint ?? `Returns reviewed ${objectLabel} fields, evidence handle, query audit, and source_database_changed:false.`,
|
|
4532
4561
|
source: sourceName,
|
|
4533
4562
|
context: "local_operator",
|
|
4534
4563
|
target: target(spec),
|
|
4535
4564
|
args: {
|
|
4536
|
-
[lookupArg]: { type: "string", required: true, max_length: 128 }
|
|
4565
|
+
[lookupArg]: { type: "string", required: true, max_length: 128, description: `${capitalize(objectLabel)} id from the user request or trusted app context.` }
|
|
4537
4566
|
},
|
|
4538
4567
|
lookup: { id_from_arg: lookupArg },
|
|
4539
4568
|
visible_columns: visibleColumns,
|
|
@@ -4546,6 +4575,8 @@ function generateRunnerConfigFromSpec(spec) {
|
|
|
4546
4575
|
capabilities.push({
|
|
4547
4576
|
name: proposalToolName,
|
|
4548
4577
|
kind: "proposal",
|
|
4578
|
+
description: spec.proposal_description ?? `Create a review-required proposal to update one ${objectLabel}. The source database remains unchanged until approval and writeback.`,
|
|
4579
|
+
returns_hint: spec.proposal_returns_hint ?? "Returns a proposal id, exact before/after diff, evidence handle, approval status, and source_database_changed:false.",
|
|
4549
4580
|
source: sourceName,
|
|
4550
4581
|
context: "local_operator",
|
|
4551
4582
|
...writeback2.executor !== "sql_update" ? { executor: writeback2.executorName } : {},
|
|
@@ -4569,12 +4600,14 @@ function generateRunnerConfigFromSpec(spec) {
|
|
|
4569
4600
|
const config = {
|
|
4570
4601
|
version: 1,
|
|
4571
4602
|
mode,
|
|
4603
|
+
result_format: spec.result_format ?? DEFAULT_RESULT_FORMAT,
|
|
4572
4604
|
storage: { sqlite_path: "./.synapsor/local.db" },
|
|
4573
4605
|
sources: {
|
|
4574
4606
|
[sourceName]: {
|
|
4575
4607
|
engine: spec.engine,
|
|
4576
4608
|
read_url_env: readUrlEnv,
|
|
4577
4609
|
...mode === "review" && writeUrlEnv ? { write_url_env: writeUrlEnv } : {},
|
|
4610
|
+
...mode === "review" && writeback2.executor !== "sql_update" && !writeUrlEnv ? { read_only: true } : {},
|
|
4578
4611
|
statement_timeout_ms: spec.statement_timeout_ms ?? 3e3
|
|
4579
4612
|
}
|
|
4580
4613
|
},
|
|
@@ -5017,6 +5050,7 @@ function validateSelectionSpec(spec) {
|
|
|
5017
5050
|
if (spec.version !== void 0 && spec.version !== 1) throw new Error("onboarding selection version must be 1.");
|
|
5018
5051
|
if (spec.engine !== "postgres" && spec.engine !== "mysql") throw new Error("selection engine must be postgres or mysql.");
|
|
5019
5052
|
if (!["read_only", "shadow", "review", void 0].includes(spec.mode)) throw new Error("selection mode must be read_only, shadow, or review.");
|
|
5053
|
+
if (spec.result_format !== void 0 && spec.result_format !== 1 && spec.result_format !== 2) throw new Error("selection result_format must be 1 or 2.");
|
|
5020
5054
|
if (spec.writeback?.executor && !["sql_update", "http_handler", "command_handler"].includes(spec.writeback.executor)) {
|
|
5021
5055
|
throw new Error("selection writeback.executor must be sql_update, http_handler, or command_handler.");
|
|
5022
5056
|
}
|
|
@@ -5121,6 +5155,10 @@ function singularize(value) {
|
|
|
5121
5155
|
function safeName(value) {
|
|
5122
5156
|
return value.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "record";
|
|
5123
5157
|
}
|
|
5158
|
+
function capitalize(value) {
|
|
5159
|
+
if (!value) return value;
|
|
5160
|
+
return `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
|
|
5161
|
+
}
|
|
5124
5162
|
function assertSafeIdentifier(identifier) {
|
|
5125
5163
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
5126
5164
|
throw new Error(`unsafe identifier in selection: ${identifier}`);
|
|
@@ -6573,6 +6611,19 @@ var defaultConfigPath = "synapsor.runner.json";
|
|
|
6573
6611
|
var defaultStorePath = "./.synapsor/local.db";
|
|
6574
6612
|
var quickDemoStorePath = "./.synapsor/quick-demo.db";
|
|
6575
6613
|
var generatedSmokeInputPath = "./.synapsor/smoke-input.json";
|
|
6614
|
+
var handlerSecurityWarning = [
|
|
6615
|
+
"IMPORTANT: your app handler owns the final business write.",
|
|
6616
|
+
"Runner creates the proposal and calls your handler only after approval, but your handler must still enforce:",
|
|
6617
|
+
"- tenant/scope check;",
|
|
6618
|
+
"- expected-version or conflict guard;",
|
|
6619
|
+
"- idempotency key;",
|
|
6620
|
+
"- allowed business action;",
|
|
6621
|
+
"- transaction/rollback;",
|
|
6622
|
+
"- safe error receipt.",
|
|
6623
|
+
"",
|
|
6624
|
+
"If you skip those checks, you can reintroduce cross-tenant writes, lost updates, or duplicate writes.",
|
|
6625
|
+
"Use the generated template/helper pattern and keep handler credentials out of MCP."
|
|
6626
|
+
].join("\n");
|
|
6576
6627
|
var handlerTemplateDefinitions = {
|
|
6577
6628
|
"node-fastify": {
|
|
6578
6629
|
aliases: ["node", "fastify"],
|
|
@@ -6608,6 +6659,15 @@ app.post("/synapsor/writeback", async (request, reply) => {
|
|
|
6608
6659
|
}
|
|
6609
6660
|
|
|
6610
6661
|
/*
|
|
6662
|
+
* IMPORTANT: your app handler owns the final business write.
|
|
6663
|
+
* Runner creates the proposal and calls your handler only after approval,
|
|
6664
|
+
* but your handler must still enforce tenant/scope, expected-version or
|
|
6665
|
+
* conflict guard, idempotency key, allowed business action,
|
|
6666
|
+
* transaction/rollback, and safe error receipt.
|
|
6667
|
+
*
|
|
6668
|
+
* If you skip those checks, you can reintroduce cross-tenant writes,
|
|
6669
|
+
* lost updates, or duplicate writes. Keep handler credentials out of MCP.
|
|
6670
|
+
*
|
|
6611
6671
|
* Put your app-owned transaction here.
|
|
6612
6672
|
*
|
|
6613
6673
|
* Examples:
|
|
@@ -6665,6 +6725,15 @@ async def synapsor_writeback(body: dict, authorization: str | None = Header(defa
|
|
|
6665
6725
|
|
|
6666
6726
|
# Put your app-owned transaction here.
|
|
6667
6727
|
#
|
|
6728
|
+
# IMPORTANT: your app handler owns the final business write.
|
|
6729
|
+
# Runner creates the proposal and calls your handler only after approval,
|
|
6730
|
+
# but your handler must still enforce tenant/scope, expected-version or
|
|
6731
|
+
# conflict guard, idempotency key, allowed business action,
|
|
6732
|
+
# transaction/rollback, and safe error receipt.
|
|
6733
|
+
#
|
|
6734
|
+
# If you skip those checks, you can reintroduce cross-tenant writes,
|
|
6735
|
+
# lost updates, or duplicate writes. Keep handler credentials out of MCP.
|
|
6736
|
+
#
|
|
6668
6737
|
# Examples:
|
|
6669
6738
|
# - insert a refund_review row;
|
|
6670
6739
|
# - insert an account_credit row;
|
|
@@ -6716,6 +6785,15 @@ if (request.dry_run) {
|
|
|
6716
6785
|
}
|
|
6717
6786
|
|
|
6718
6787
|
/*
|
|
6788
|
+
* IMPORTANT: your app handler owns the final business write.
|
|
6789
|
+
* Runner creates the proposal and calls your handler only after approval,
|
|
6790
|
+
* but your handler must still enforce tenant/scope, expected-version or
|
|
6791
|
+
* conflict guard, idempotency key, allowed business action,
|
|
6792
|
+
* transaction/rollback, and safe error receipt.
|
|
6793
|
+
*
|
|
6794
|
+
* If you skip those checks, you can reintroduce cross-tenant writes,
|
|
6795
|
+
* lost updates, or duplicate writes. Keep handler credentials out of MCP.
|
|
6796
|
+
*
|
|
6719
6797
|
* Put your app-owned command transaction here.
|
|
6720
6798
|
*
|
|
6721
6799
|
* Examples:
|
|
@@ -6784,6 +6862,7 @@ ${cliCommandName()} --help
|
|
|
6784
6862
|
if (command === "propose") return propose(rest);
|
|
6785
6863
|
if (command === "audit") return audit(rest);
|
|
6786
6864
|
if (command === "start") return start(rest);
|
|
6865
|
+
if (command === "up") return up(rest);
|
|
6787
6866
|
if (command === "runner") return runnerCommand(rest);
|
|
6788
6867
|
if (command === "cloud") return cloud(rest);
|
|
6789
6868
|
if (command === "mcp") return mcp(rest);
|
|
@@ -6813,11 +6892,16 @@ ${cliCommandName()} --help
|
|
|
6813
6892
|
return 2;
|
|
6814
6893
|
}
|
|
6815
6894
|
async function init(args) {
|
|
6895
|
+
const answersPath = optionalArg(args, "--answers");
|
|
6896
|
+
if (answersPath) {
|
|
6897
|
+
return initFromAnswers(args, answersPath);
|
|
6898
|
+
}
|
|
6816
6899
|
const specPath = optionalArg(args, "--spec");
|
|
6817
6900
|
if (specPath) {
|
|
6818
6901
|
return initFromSpec(args, specPath);
|
|
6819
6902
|
}
|
|
6820
|
-
|
|
6903
|
+
const scripted = isScriptedOnboardingArgs(args);
|
|
6904
|
+
if (args.includes("--wizard") || process2.stdin.isTTY && process2.stdout.isTTY && !args.includes("--starter") && !scripted) {
|
|
6821
6905
|
return runInitWizard(args);
|
|
6822
6906
|
}
|
|
6823
6907
|
const inspectionJson = optionalArg(args, "--inspection-json");
|
|
@@ -6983,12 +7067,49 @@ async function runInitWizard(args, options = {}) {
|
|
|
6983
7067
|
if (transitionFromColumns.length > 0) ensureColumnsExist(transitionFromColumns, columns, "transition from");
|
|
6984
7068
|
}
|
|
6985
7069
|
}
|
|
6986
|
-
const
|
|
6987
|
-
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);
|
|
6988
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
|
+
);
|
|
6989
7086
|
const smokeObjectId = await askDefault(ask, "Optional real object id for a first smoke call", optionalArg(args, "--smoke-id") ?? "");
|
|
7087
|
+
const objectLabel = objectName.replace(/_/g, " ");
|
|
7088
|
+
const inspectDescription = await askDefault(
|
|
7089
|
+
ask,
|
|
7090
|
+
"Read capability description",
|
|
7091
|
+
optionalArg(args, "--inspect-description") ?? `Inspect one ${objectLabel} in trusted tenant scope before answering or proposing a change.`
|
|
7092
|
+
);
|
|
7093
|
+
const inspectReturnsHint = await askDefault(
|
|
7094
|
+
ask,
|
|
7095
|
+
"Read capability returns hint",
|
|
7096
|
+
optionalArg(args, "--inspect-returns-hint") ?? `Returns reviewed ${objectLabel} fields, evidence handle, query audit, and source_database_changed:false.`
|
|
7097
|
+
);
|
|
7098
|
+
const proposalDescription = mode === "read_only" ? void 0 : await askDefault(
|
|
7099
|
+
ask,
|
|
7100
|
+
"Proposal capability description",
|
|
7101
|
+
optionalArg(args, "--proposal-description") ?? `Create a review-required proposal to update one ${objectLabel}. The source database remains unchanged until approval and writeback.`
|
|
7102
|
+
);
|
|
7103
|
+
const proposalReturnsHint = mode === "read_only" ? void 0 : await askDefault(
|
|
7104
|
+
ask,
|
|
7105
|
+
"Proposal capability returns hint",
|
|
7106
|
+
optionalArg(args, "--proposal-returns-hint") ?? "Returns a proposal id, exact before/after diff, evidence handle, approval status, and source_database_changed:false."
|
|
7107
|
+
);
|
|
7108
|
+
const resultFormatAnswer = await askChoice(ask, "MCP result envelope", optionalArg(args, "--result-format") ? normalizeResultFormatAnswer(optionalArg(args, "--result-format")) : "v2", ["v2", "v1", "default"]);
|
|
7109
|
+
const resultFormat = resultFormatAnswer === "v1" ? 1 : resultFormatAnswer === "v2" ? 2 : void 0;
|
|
6990
7110
|
let writeUrlEnv = optionalArg(args, "--write-url-env");
|
|
6991
7111
|
let writeback2;
|
|
7112
|
+
let generatedHandlerTemplate;
|
|
6992
7113
|
if (mode === "review") {
|
|
6993
7114
|
const writebackPath = await askChoice(
|
|
6994
7115
|
ask,
|
|
@@ -7011,6 +7132,12 @@ async function runInitWizard(args, options = {}) {
|
|
|
7011
7132
|
...signingSecretEnv ? { handler_signing_secret_env: signingSecretEnv } : {},
|
|
7012
7133
|
timeout_ms: positiveIntegerOption(args, "--handler-timeout-ms")
|
|
7013
7134
|
};
|
|
7135
|
+
const writeTemplate = await askChoice(ask, "Write starter app-owned handler template", args.includes("--skip-handler-template") ? "no" : "yes", ["yes", "no"]);
|
|
7136
|
+
if (writeTemplate === "yes") {
|
|
7137
|
+
const template = await askChoice(ask, "Handler template", optionalArg(args, "--handler-template") ?? "node-fastify", ["node-fastify", "python-fastapi"]);
|
|
7138
|
+
const output = await askDefault(ask, "Handler template output", optionalArg(args, "--handler-output") ?? optionalArg(args, "--handler-template-output") ?? handlerTemplateDefinitions[template].fileName);
|
|
7139
|
+
generatedHandlerTemplate = { name: template, output };
|
|
7140
|
+
}
|
|
7014
7141
|
} else {
|
|
7015
7142
|
const commandEnv = await askEnvName(ask, "App-owned command handler env var", optionalArg(args, "--handler-command-env") ?? "SYNAPSOR_APP_WRITEBACK_COMMAND");
|
|
7016
7143
|
writeback2 = {
|
|
@@ -7019,10 +7146,15 @@ async function runInitWizard(args, options = {}) {
|
|
|
7019
7146
|
handler_command_env: commandEnv,
|
|
7020
7147
|
timeout_ms: positiveIntegerOption(args, "--handler-timeout-ms")
|
|
7021
7148
|
};
|
|
7149
|
+
const writeTemplate = await askChoice(ask, "Write starter app-owned handler template", args.includes("--skip-handler-template") ? "no" : "yes", ["yes", "no"]);
|
|
7150
|
+
if (writeTemplate === "yes") {
|
|
7151
|
+
const output = await askDefault(ask, "Handler template output", optionalArg(args, "--handler-output") ?? optionalArg(args, "--handler-template-output") ?? handlerTemplateDefinitions.command.fileName);
|
|
7152
|
+
generatedHandlerTemplate = { name: "command", output };
|
|
7153
|
+
}
|
|
7022
7154
|
}
|
|
7023
7155
|
}
|
|
7024
7156
|
const approvalRole = mode === "read_only" ? "local_reviewer" : await askDefault(ask, "Required approval role", optionalArg(args, "--approval-role") ?? recipeSpec?.approval?.required_role ?? "local_reviewer");
|
|
7025
|
-
|
|
7157
|
+
let spec = {
|
|
7026
7158
|
version: 1,
|
|
7027
7159
|
engine: inspection.engine,
|
|
7028
7160
|
mode,
|
|
@@ -7037,9 +7169,14 @@ async function runInitWizard(args, options = {}) {
|
|
|
7037
7169
|
conflict_column: conflictAnswer || void 0,
|
|
7038
7170
|
namespace,
|
|
7039
7171
|
object_name: objectName,
|
|
7040
|
-
inspect_tool_name:
|
|
7041
|
-
proposal_tool_name:
|
|
7172
|
+
inspect_tool_name: inspectToolName,
|
|
7173
|
+
proposal_tool_name: proposalToolName,
|
|
7174
|
+
inspect_description: inspectDescription,
|
|
7175
|
+
inspect_returns_hint: inspectReturnsHint,
|
|
7176
|
+
proposal_description: proposalDescription,
|
|
7177
|
+
proposal_returns_hint: proposalReturnsHint,
|
|
7042
7178
|
lookup_arg: lookupArg,
|
|
7179
|
+
result_format: resultFormat,
|
|
7043
7180
|
visible_columns: visibleColumns,
|
|
7044
7181
|
allowed_columns: allowedColumns,
|
|
7045
7182
|
patch,
|
|
@@ -7055,26 +7192,48 @@ async function runInitWizard(args, options = {}) {
|
|
|
7055
7192
|
},
|
|
7056
7193
|
writeback: writeback2
|
|
7057
7194
|
};
|
|
7058
|
-
|
|
7059
|
-
const generatedCapabilities = generated.config.capabilities;
|
|
7060
|
-
const tools2 = generatedCapabilities.map((capability) => `${capability.name} (${capability.kind})`);
|
|
7061
|
-
const smokeToolName = generatedCapabilities[0]?.name ?? "<inspect_tool>";
|
|
7195
|
+
let generated = generateRunnerConfigFromSpec(spec);
|
|
7062
7196
|
stdout.write("\nPreview:\n");
|
|
7063
|
-
stdout
|
|
7197
|
+
printWizardContractPreview(stdout, { spec, generated, engine: inspection.engine, table });
|
|
7198
|
+
if (generatedHandlerTemplate) {
|
|
7199
|
+
stdout.write(` handler template: ${generatedHandlerTemplate.output}
|
|
7064
7200
|
`);
|
|
7065
|
-
|
|
7066
|
-
`);
|
|
7067
|
-
stdout.write(` mode: ${mode}
|
|
7068
|
-
`);
|
|
7069
|
-
stdout.write(` writeback path: ${writeback2?.executor ?? (mode === "review" ? "sql_update" : "none")}
|
|
7070
|
-
`);
|
|
7071
|
-
stdout.write(` exposed tools: ${tools2.join(", ")}
|
|
7201
|
+
stdout.write(`${handlerSecurityWarning}
|
|
7072
7202
|
`);
|
|
7073
|
-
|
|
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>";
|
|
7074
7228
|
const confirmed = await askDefault(ask, "Write generated config and MCP snippets? Type yes to continue", "no");
|
|
7075
7229
|
if (confirmed.toLowerCase() !== "yes") throw new Error("guided init canceled before writing files");
|
|
7076
7230
|
const outputPath = outputArg(args) ?? "synapsor.runner.json";
|
|
7077
7231
|
await writeGeneratedOnboardingFiles(outputPath, generated, args.includes("--force"), { printNext: false });
|
|
7232
|
+
if (generatedHandlerTemplate) {
|
|
7233
|
+
await writeHandlerTemplateFile(generatedHandlerTemplate.name, generatedHandlerTemplate.output, args.includes("--force"));
|
|
7234
|
+
stdout.write(`created ${generatedHandlerTemplate.output}
|
|
7235
|
+
`);
|
|
7236
|
+
}
|
|
7078
7237
|
if (smokeObjectId) {
|
|
7079
7238
|
await writeGeneratedSmokeInputFile(lookupArg, smokeObjectId, args.includes("--force"));
|
|
7080
7239
|
stdout.write(`created ${generatedSmokeInputPath}
|
|
@@ -7103,9 +7262,42 @@ async function runInitWizard(args, options = {}) {
|
|
|
7103
7262
|
`);
|
|
7104
7263
|
}
|
|
7105
7264
|
stdout.write(` 3. Serve MCP tools: ${cliCommandName()} mcp serve --config ${outputPath} --store ${defaultStorePath}
|
|
7265
|
+
`);
|
|
7266
|
+
stdout.write(` OpenAI Agents SDK: use ${cliCommandName()} mcp serve-streamable-http --config ${outputPath} --store ${defaultStorePath} --alias-mode openai
|
|
7106
7267
|
`);
|
|
7107
7268
|
return 0;
|
|
7108
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
|
+
}
|
|
7109
7301
|
async function initFromSpec(args, specPath) {
|
|
7110
7302
|
if (!args.includes("--non-interactive")) {
|
|
7111
7303
|
throw new Error("init --spec requires --non-interactive so reviewed selections are explicit.");
|
|
@@ -7114,9 +7306,123 @@ async function initFromSpec(args, specPath) {
|
|
|
7114
7306
|
const force = args.includes("--force");
|
|
7115
7307
|
const spec = JSON.parse(await fs3.readFile(specPath, "utf8"));
|
|
7116
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
|
+
}
|
|
7314
|
+
await writeGeneratedOnboardingFiles(output, generated, force);
|
|
7315
|
+
return 0;
|
|
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
|
+
}
|
|
7117
7328
|
await writeGeneratedOnboardingFiles(output, generated, force);
|
|
7329
|
+
await maybeWriteHandlerTemplateForArgs(args, spec.writeback);
|
|
7118
7330
|
return 0;
|
|
7119
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
|
+
}
|
|
7120
7426
|
async function initFromInspection(args, inspection, databaseUrlEnv) {
|
|
7121
7427
|
const tableName = optionalArg(args, "--table");
|
|
7122
7428
|
if (!tableName) {
|
|
@@ -7140,7 +7446,7 @@ async function initFromInspection(args, inspection, databaseUrlEnv) {
|
|
|
7140
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.
|
|
7141
7447
|
`);
|
|
7142
7448
|
}
|
|
7143
|
-
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];
|
|
7144
7450
|
const singleTenantDev = args.includes("--single-tenant-dev");
|
|
7145
7451
|
if (!tenantKey && !singleTenantDev) {
|
|
7146
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.`);
|
|
@@ -7163,22 +7469,31 @@ async function initFromInspection(args, inspection, databaseUrlEnv) {
|
|
|
7163
7469
|
const allowedColumns = listArg(args, "--allowed-columns") ?? Object.keys(patch);
|
|
7164
7470
|
const writeback2 = writebackSpecFromArgs(args);
|
|
7165
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);
|
|
7166
7474
|
const spec = {
|
|
7167
7475
|
version: 1,
|
|
7168
7476
|
engine: inspection.engine,
|
|
7169
7477
|
mode,
|
|
7170
7478
|
source_name: optionalArg(args, "--source-name"),
|
|
7171
7479
|
read_url_env: databaseUrlEnv,
|
|
7172
|
-
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"),
|
|
7173
7481
|
schema: table.schema,
|
|
7174
7482
|
table: table.name,
|
|
7175
7483
|
primary_key: primaryKey,
|
|
7176
7484
|
tenant_key: tenantKey,
|
|
7177
7485
|
single_tenant_dev: singleTenantDev,
|
|
7178
7486
|
conflict_column: conflictColumn,
|
|
7179
|
-
namespace
|
|
7180
|
-
object_name:
|
|
7181
|
-
|
|
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"),
|
|
7494
|
+
proposal_description: optionalArg(args, "--proposal-description"),
|
|
7495
|
+
proposal_returns_hint: optionalArg(args, "--proposal-returns-hint"),
|
|
7496
|
+
result_format: resultFormatOption(args),
|
|
7182
7497
|
visible_columns: visibleColumns,
|
|
7183
7498
|
allowed_columns: allowedColumns,
|
|
7184
7499
|
patch,
|
|
@@ -7194,7 +7509,13 @@ async function initFromInspection(args, inspection, databaseUrlEnv) {
|
|
|
7194
7509
|
writeback: writeback2
|
|
7195
7510
|
};
|
|
7196
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
|
+
}
|
|
7197
7517
|
await writeGeneratedOnboardingFiles(outputArg(args) ?? "synapsor.runner.json", generated, args.includes("--force"));
|
|
7518
|
+
await maybeWriteHandlerTemplateForArgs(args, writeback2);
|
|
7198
7519
|
process2.stdout.write(`selected ${table.schema}.${table.name} from ${inspection.engine} inspection
|
|
7199
7520
|
`);
|
|
7200
7521
|
process2.stdout.write(`exposed tools: ${generated.config.capabilities.map((capability) => capability.name).join(", ")}
|
|
@@ -7410,6 +7731,11 @@ function safeObjectName(tableName) {
|
|
|
7410
7731
|
const base = tableName.replace(/[^A-Za-z0-9_]/g, "_").replace(/s$/, "");
|
|
7411
7732
|
return /^[A-Za-z_]/.test(base) ? base : `record_${base}`;
|
|
7412
7733
|
}
|
|
7734
|
+
function inferCapabilityNamespace(tableName) {
|
|
7735
|
+
const objectName = safeObjectName(tableName);
|
|
7736
|
+
const [firstPart] = objectName.split("_").filter(Boolean);
|
|
7737
|
+
return firstPart ?? objectName;
|
|
7738
|
+
}
|
|
7413
7739
|
function requiredWritebackEngine(args) {
|
|
7414
7740
|
const value = optionalArg(args, "--engine") ?? firstPositional(args);
|
|
7415
7741
|
if (value === "postgres" || value === "mysql") return value;
|
|
@@ -7514,6 +7840,7 @@ function repeatedArgs(args, flag) {
|
|
|
7514
7840
|
}
|
|
7515
7841
|
function parsePatchFlags(args) {
|
|
7516
7842
|
const patch = {};
|
|
7843
|
+
Object.assign(patch, parsePatchBindings(repeatedArgs(args, "--patch"), "--patch"));
|
|
7517
7844
|
for (const binding of repeatedArgs(args, "--patch-fixed")) {
|
|
7518
7845
|
const [column, ...rest] = binding.split("=");
|
|
7519
7846
|
const value = rest.join("=");
|
|
@@ -7528,8 +7855,27 @@ function parsePatchFlags(args) {
|
|
|
7528
7855
|
}
|
|
7529
7856
|
return patch;
|
|
7530
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
|
+
}
|
|
7531
7877
|
function parseNumericBoundsFlags(args) {
|
|
7532
|
-
return parseNumericBoundsInput(repeatedArgs(args, "--numeric-bound").join(","));
|
|
7878
|
+
return parseNumericBoundsInput([...repeatedArgs(args, "--numeric-bound"), ...repeatedArgs(args, "--patch-bounds")].join(","));
|
|
7533
7879
|
}
|
|
7534
7880
|
function parseNumericBoundsInput(input) {
|
|
7535
7881
|
const bounds = {};
|
|
@@ -7561,7 +7907,7 @@ function formatNumericBounds(bounds) {
|
|
|
7561
7907
|
return Object.entries(bounds).map(([column, bound]) => `${column}=${bound.minimum ?? ""}:${bound.maximum ?? ""}`).join(",");
|
|
7562
7908
|
}
|
|
7563
7909
|
function parseTransitionGuardFlags(args) {
|
|
7564
|
-
return parseTransitionGuardsInput(repeatedArgs(args, "--transition-guard").join(","));
|
|
7910
|
+
return parseTransitionGuardsInput([...repeatedArgs(args, "--transition-guard"), ...repeatedArgs(args, "--status-guards")].join(","));
|
|
7565
7911
|
}
|
|
7566
7912
|
function parseTransitionGuardsInput(input) {
|
|
7567
7913
|
const guards = {};
|
|
@@ -9131,9 +9477,211 @@ async function start(args = []) {
|
|
|
9131
9477
|
await startPolling(config, adapters, controller.signal);
|
|
9132
9478
|
return 0;
|
|
9133
9479
|
}
|
|
9480
|
+
async function up(args = []) {
|
|
9481
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
9482
|
+
"--config",
|
|
9483
|
+
"--store",
|
|
9484
|
+
"--transport",
|
|
9485
|
+
"--serve",
|
|
9486
|
+
"--with-handler",
|
|
9487
|
+
"--host",
|
|
9488
|
+
"--port",
|
|
9489
|
+
"--auth-token-env",
|
|
9490
|
+
"--alias-mode",
|
|
9491
|
+
"--tool-name-style",
|
|
9492
|
+
"--openai-tool-aliases",
|
|
9493
|
+
"--result-format",
|
|
9494
|
+
"--handler-check",
|
|
9495
|
+
"--open-ui",
|
|
9496
|
+
"--print-next",
|
|
9497
|
+
"--dry-run",
|
|
9498
|
+
"--dev-no-auth",
|
|
9499
|
+
"--cors-origin",
|
|
9500
|
+
"--allow-concurrent-store"
|
|
9501
|
+
]);
|
|
9502
|
+
assertKnownOptions(args, allowed, "up");
|
|
9503
|
+
const configPath = optionalArg(args, "--config") ?? defaultConfigPath;
|
|
9504
|
+
const config = await readRuntimeConfig(configPath);
|
|
9505
|
+
const storePath = optionalArg(args, "--store") ?? config.storage?.sqlite_path ?? defaultStorePath;
|
|
9506
|
+
const serveRequested = args.includes("--serve");
|
|
9507
|
+
const transport = optionalArg(args, "--transport") ?? (serveRequested ? "streamable-http" : "stdio");
|
|
9508
|
+
if (transport !== "stdio" && transport !== "streamable-http") {
|
|
9509
|
+
throw new Error("--transport must be stdio or streamable-http");
|
|
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
|
+
}
|
|
9514
|
+
const port = Number(optionalArg(args, "--port") ?? "8766");
|
|
9515
|
+
if (transport === "streamable-http" && (!Number.isInteger(port) || port <= 0 || port > 65535)) {
|
|
9516
|
+
throw new Error("--port must be an integer from 1 to 65535");
|
|
9517
|
+
}
|
|
9518
|
+
const aliasMode = toolNameStyleOption(args);
|
|
9519
|
+
const resultFormat = resultFormatOption(args);
|
|
9520
|
+
const validation = validateRunnerCapabilityConfig(config);
|
|
9521
|
+
if (!validation.ok) {
|
|
9522
|
+
throw new Error(`cannot bring Runner up with invalid config: ${validation.errors.map((error) => `${error.path} ${error.code}`).join("; ")}`);
|
|
9523
|
+
}
|
|
9524
|
+
if (storePath !== ":memory:") {
|
|
9525
|
+
await fs3.mkdir(path3.dirname(path3.resolve(storePath)), { recursive: true });
|
|
9526
|
+
}
|
|
9527
|
+
await assertNoActiveStoreLease(storePath, args.includes("--allow-concurrent-store"), "review-mode up");
|
|
9528
|
+
const boundary = await inspectMcpToolBoundary([
|
|
9529
|
+
"--config",
|
|
9530
|
+
configPath,
|
|
9531
|
+
"--store",
|
|
9532
|
+
storePath,
|
|
9533
|
+
"--alias-mode",
|
|
9534
|
+
aliasMode
|
|
9535
|
+
]);
|
|
9536
|
+
process2.stdout.write(formatReviewModeUp({
|
|
9537
|
+
aliasMode,
|
|
9538
|
+
authTokenEnv: optionalArg(args, "--auth-token-env") ?? "SYNAPSOR_RUNNER_HTTP_TOKEN",
|
|
9539
|
+
boundary,
|
|
9540
|
+
config,
|
|
9541
|
+
configPath,
|
|
9542
|
+
dryRun: args.includes("--dry-run"),
|
|
9543
|
+
host: optionalArg(args, "--host") ?? "127.0.0.1",
|
|
9544
|
+
openUi: args.includes("--open-ui"),
|
|
9545
|
+
port,
|
|
9546
|
+
resultFormat,
|
|
9547
|
+
serveRequested,
|
|
9548
|
+
storePath,
|
|
9549
|
+
transport
|
|
9550
|
+
}));
|
|
9551
|
+
if (args.includes("--with-handler") || args.includes("--handler-check")) {
|
|
9552
|
+
process2.stdout.write("\nHandler check:\n");
|
|
9553
|
+
const doctorCode = await doctor(["--config", configPath, "--store", storePath, "--check-handlers"]);
|
|
9554
|
+
if (doctorCode !== 0) return doctorCode;
|
|
9555
|
+
}
|
|
9556
|
+
if (args.includes("--dry-run") || !serveRequested) return boundary.ok ? 0 : 1;
|
|
9557
|
+
if (!boundary.ok) return 1;
|
|
9558
|
+
const serveArgs = [
|
|
9559
|
+
"--config",
|
|
9560
|
+
configPath,
|
|
9561
|
+
"--store",
|
|
9562
|
+
storePath,
|
|
9563
|
+
"--host",
|
|
9564
|
+
optionalArg(args, "--host") ?? "127.0.0.1",
|
|
9565
|
+
"--port",
|
|
9566
|
+
String(port),
|
|
9567
|
+
"--auth-token-env",
|
|
9568
|
+
optionalArg(args, "--auth-token-env") ?? "SYNAPSOR_RUNNER_HTTP_TOKEN",
|
|
9569
|
+
"--alias-mode",
|
|
9570
|
+
aliasMode,
|
|
9571
|
+
...resultFormat ? ["--result-format", String(resultFormat)] : [],
|
|
9572
|
+
...args.includes("--dev-no-auth") ? ["--dev-no-auth"] : [],
|
|
9573
|
+
...optionalArg(args, "--cors-origin") ? ["--cors-origin", optionalArg(args, "--cors-origin")] : [],
|
|
9574
|
+
...args.includes("--allow-concurrent-store") ? ["--allow-concurrent-store"] : []
|
|
9575
|
+
];
|
|
9576
|
+
return mcpServeStreamableHttp(serveArgs);
|
|
9577
|
+
}
|
|
9578
|
+
function formatReviewModeUp(input) {
|
|
9579
|
+
const lines = [
|
|
9580
|
+
"Synapsor Runner review-mode up",
|
|
9581
|
+
"",
|
|
9582
|
+
`Config: ${input.configPath}`,
|
|
9583
|
+
`Store: ${input.storePath}`,
|
|
9584
|
+
`Mode: ${input.config.mode}`,
|
|
9585
|
+
`Transport: ${input.transport}`,
|
|
9586
|
+
`Serve now: ${input.serveRequested ? "yes" : "no"}`,
|
|
9587
|
+
`Alias mode: ${input.aliasMode}`,
|
|
9588
|
+
`Result format: ${input.resultFormat ? `v${input.resultFormat}` : configResultFormat(input.config)}`,
|
|
9589
|
+
`Dry run: ${input.dryRun ? "yes" : "no"}`,
|
|
9590
|
+
"",
|
|
9591
|
+
"Model-facing tools:",
|
|
9592
|
+
...formatUpToolLines(input.boundary),
|
|
9593
|
+
"",
|
|
9594
|
+
"Writeback paths:",
|
|
9595
|
+
...formatUpWritebackLines(input.config)
|
|
9596
|
+
];
|
|
9597
|
+
const handlerLines = formatUpHandlerLines(input.config);
|
|
9598
|
+
if (handlerLines.length > 0) {
|
|
9599
|
+
lines.push("", "App-owned handler requirements:", ...handlerLines, "", handlerSecurityWarning);
|
|
9600
|
+
}
|
|
9601
|
+
lines.push("", "Server guidance:");
|
|
9602
|
+
if (input.transport === "stdio") {
|
|
9603
|
+
lines.push(
|
|
9604
|
+
" stdio mode is launched by an MCP client. This command does not hold a protocol session open.",
|
|
9605
|
+
` Print client config: ${cliCommandName()} mcp client-config --client claude-desktop --config ${input.configPath} --store ${input.storePath}`,
|
|
9606
|
+
` Serve command used by clients: ${cliCommandName()} mcp serve --config ${input.configPath} --store ${input.storePath} --alias-mode ${input.aliasMode}`
|
|
9607
|
+
);
|
|
9608
|
+
} else {
|
|
9609
|
+
lines.push(
|
|
9610
|
+
` Streamable HTTP endpoint: http://${input.host}:${input.port}/mcp`,
|
|
9611
|
+
` Auth token env: ${input.authTokenEnv} (${process2.env[input.authTokenEnv] ? "set" : "missing"})`,
|
|
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}`
|
|
9613
|
+
);
|
|
9614
|
+
}
|
|
9615
|
+
if (input.openUi) {
|
|
9616
|
+
lines.push("", "Local review UI:", ` ${cliCommandName()} ui --open --tour --config ${input.configPath} --store ${input.storePath}`);
|
|
9617
|
+
}
|
|
9618
|
+
lines.push("", "Next commands:", ...formatUpNextCommands(input.config, input.configPath, input.storePath), "");
|
|
9619
|
+
return `${lines.join("\n")}
|
|
9620
|
+
`;
|
|
9621
|
+
}
|
|
9622
|
+
function formatUpToolLines(boundary) {
|
|
9623
|
+
if (boundary.exposures.length === 0) return [" - (none)"];
|
|
9624
|
+
return boundary.exposures.map((item) => item.isAlias ? ` - ${item.exposedName} -> ${item.canonicalName}` : ` - ${item.exposedName}`);
|
|
9625
|
+
}
|
|
9626
|
+
function formatUpWritebackLines(config) {
|
|
9627
|
+
const proposals2 = (config.capabilities ?? []).filter((capability) => capability.kind === "proposal");
|
|
9628
|
+
if (proposals2.length === 0) return [" - no proposal capabilities; this config is read-only from Runner's perspective"];
|
|
9629
|
+
return proposals2.map((capability) => {
|
|
9630
|
+
const executorName = capability.executor ?? "sql_update";
|
|
9631
|
+
if (executorName === "sql_update") {
|
|
9632
|
+
const source = config.sources?.[capability.source];
|
|
9633
|
+
const envName = source?.write_url_env ?? "SYNAPSOR_DATABASE_URL";
|
|
9634
|
+
return ` - ${capability.name}: direct guarded one-row UPDATE via ${envName} (${process2.env[envName] ? "set" : "missing"})`;
|
|
9635
|
+
}
|
|
9636
|
+
const executor = config.executors?.[executorName];
|
|
9637
|
+
return ` - ${capability.name}: app-owned ${String(executor?.type ?? "executor")} ${executorName}`;
|
|
9638
|
+
});
|
|
9639
|
+
}
|
|
9640
|
+
function formatUpHandlerLines(config) {
|
|
9641
|
+
const lines = [];
|
|
9642
|
+
for (const [name, executor] of Object.entries(config.executors ?? {})) {
|
|
9643
|
+
if (!isRecord6(executor)) continue;
|
|
9644
|
+
if (executor.type === "http_handler") {
|
|
9645
|
+
const urlEnv = typeof executor.url_env === "string" ? executor.url_env : "";
|
|
9646
|
+
const auth = isRecord6(executor.auth) ? executor.auth : void 0;
|
|
9647
|
+
const tokenEnv = typeof auth?.token_env === "string" ? auth.token_env : void 0;
|
|
9648
|
+
const signingSecretEnv = typeof executor.signing_secret_env === "string" ? executor.signing_secret_env : void 0;
|
|
9649
|
+
lines.push(` - ${name}: http_handler`);
|
|
9650
|
+
if (urlEnv) lines.push(` url env: ${urlEnv} (${process2.env[urlEnv] ? "set" : "missing"})`);
|
|
9651
|
+
if (tokenEnv) lines.push(` bearer token env: ${tokenEnv} (${process2.env[tokenEnv] ? "set" : "missing"})`);
|
|
9652
|
+
if (signingSecretEnv) lines.push(` signing secret env: ${signingSecretEnv} (${process2.env[signingSecretEnv] ? "set" : "missing"})`);
|
|
9653
|
+
if (!signingSecretEnv) lines.push(" signing secret env: not configured (recommended unless loopback-only)");
|
|
9654
|
+
} else if (executor.type === "command_handler") {
|
|
9655
|
+
const commandEnv = typeof executor.command_env === "string" ? executor.command_env : "";
|
|
9656
|
+
lines.push(` - ${name}: command_handler`);
|
|
9657
|
+
if (commandEnv) lines.push(` command env: ${commandEnv} (${process2.env[commandEnv] ? "set" : "missing"})`);
|
|
9658
|
+
}
|
|
9659
|
+
}
|
|
9660
|
+
return lines;
|
|
9661
|
+
}
|
|
9662
|
+
function configResultFormat(config) {
|
|
9663
|
+
return config.result_format === 2 ? "v2" : config.result_format === 1 ? "v1" : "default";
|
|
9664
|
+
}
|
|
9665
|
+
function formatUpNextCommands(config, configPath, storePath) {
|
|
9666
|
+
const firstTool = (config.capabilities ?? [])[0]?.name ?? "<capability>";
|
|
9667
|
+
const hasHandlers = Object.keys(config.executors ?? {}).length > 0;
|
|
9668
|
+
return [
|
|
9669
|
+
` - Preview tools: ${cliCommandName()} tools preview --config ${configPath} --store ${storePath}`,
|
|
9670
|
+
` - Smoke call: ${cliCommandName()} smoke call ${firstTool} --sample --config ${configPath} --store ${storePath}`,
|
|
9671
|
+
` - List proposals: ${cliCommandName()} proposals list --store ${storePath}`,
|
|
9672
|
+
` - Show proposal: ${cliCommandName()} proposals show latest --store ${storePath}`,
|
|
9673
|
+
` - Approve proposal: ${cliCommandName()} proposals approve latest --yes --store ${storePath}`,
|
|
9674
|
+
` - Apply approved proposal: ${cliCommandName()} apply latest --config ${configPath} --store ${storePath}`,
|
|
9675
|
+
` - Replay: ${cliCommandName()} replay show latest --store ${storePath}`,
|
|
9676
|
+
` - Tail events: ${cliCommandName()} events tail --store ${storePath}`,
|
|
9677
|
+
` - Direct writeback doctor: ${cliCommandName()} doctor --config ${configPath} --check-writeback`,
|
|
9678
|
+
...hasHandlers ? [` - Handler doctor: ${cliCommandName()} doctor --config ${configPath} --check-handlers`] : []
|
|
9679
|
+
];
|
|
9680
|
+
}
|
|
9134
9681
|
async function runnerCommand(args) {
|
|
9135
9682
|
const [subcommand, ...rest] = args;
|
|
9136
9683
|
if (subcommand === "start") return start(rest);
|
|
9684
|
+
if (subcommand === "up") return up(rest);
|
|
9137
9685
|
if (subcommand === "doctor") return doctor(rest);
|
|
9138
9686
|
usage();
|
|
9139
9687
|
return 2;
|
|
@@ -9172,7 +9720,7 @@ async function cloudConnect(args) {
|
|
|
9172
9720
|
return 1;
|
|
9173
9721
|
}
|
|
9174
9722
|
const runnerId = String(parsed.cloud.runner_id || process2.env.SYNAPSOR_RUNNER_ID || "synapsor_runner_local").trim();
|
|
9175
|
-
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();
|
|
9176
9724
|
const engines = normalizeEngines(parsed.cloud.engines);
|
|
9177
9725
|
const capabilities = normalizeCapabilities(parsed.cloud.capabilities);
|
|
9178
9726
|
const client = new ControlPlaneClient({
|
|
@@ -9277,21 +9825,28 @@ async function handlerTemplate(args) {
|
|
|
9277
9825
|
return 0;
|
|
9278
9826
|
}
|
|
9279
9827
|
const output = outputArg(args) ?? definition.fileName;
|
|
9280
|
-
await
|
|
9281
|
-
if (name === "command" || output.endsWith(".mjs") || output.endsWith(".js")) {
|
|
9282
|
-
await fs3.chmod(path3.resolve(output), 493).catch(() => void 0);
|
|
9283
|
-
}
|
|
9828
|
+
await writeHandlerTemplateFile(name, output, args.includes("--force"));
|
|
9284
9829
|
process2.stdout.write(`created ${output}
|
|
9285
9830
|
`);
|
|
9286
|
-
process2.stdout.write(
|
|
9831
|
+
process2.stdout.write(`${handlerSecurityWarning}
|
|
9832
|
+
`);
|
|
9287
9833
|
return 0;
|
|
9288
9834
|
}
|
|
9835
|
+
async function writeHandlerTemplateFile(name, output, force) {
|
|
9836
|
+
const definition = handlerTemplateDefinitions[name];
|
|
9837
|
+
await writeFileGuarded(output, definition.content, force);
|
|
9838
|
+
if (name === "command" || output.endsWith(".mjs") || output.endsWith(".js")) {
|
|
9839
|
+
await fs3.chmod(path3.resolve(output), 493).catch(() => void 0);
|
|
9840
|
+
}
|
|
9841
|
+
}
|
|
9289
9842
|
function formatHandlerTemplateList() {
|
|
9290
9843
|
return [
|
|
9291
9844
|
"Synapsor app-owned writeback handler templates",
|
|
9292
9845
|
"",
|
|
9293
9846
|
...Object.entries(handlerTemplateDefinitions).map(([name, definition]) => `- ${name}: ${definition.description}`),
|
|
9294
9847
|
"",
|
|
9848
|
+
handlerSecurityWarning,
|
|
9849
|
+
"",
|
|
9295
9850
|
"Examples:",
|
|
9296
9851
|
` ${cliCommandName()} handler template node-fastify --output ./synapsor-writeback-handler.mjs`,
|
|
9297
9852
|
` ${cliCommandName()} handler template python-fastapi --output ./synapsor_writeback_handler.py`,
|
|
@@ -9390,8 +9945,10 @@ async function onboard(args) {
|
|
|
9390
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");
|
|
9391
9946
|
const outputPath = outputArg(rest) ?? "synapsor.runner.json";
|
|
9392
9947
|
const storePath = optionalArg(rest, "--store") ?? "./.synapsor/local.db";
|
|
9393
|
-
const
|
|
9948
|
+
const scripted = isScriptedOnboardingArgs(rest);
|
|
9949
|
+
const result = scripted ? await init(["--non-interactive", ...rest]) : await runInitWizard(["--wizard", ...rest]);
|
|
9394
9950
|
if (result !== 0) return result;
|
|
9951
|
+
if (rest.includes("--dry-run")) return 0;
|
|
9395
9952
|
process2.stdout.write("\nValidation:\n");
|
|
9396
9953
|
const configCode = await configValidate(["--config", outputPath]);
|
|
9397
9954
|
const smokeCode = await mcpSmoke(["--config", outputPath, "--store", storePath]);
|
|
@@ -10040,7 +10597,7 @@ async function assertNoActiveStoreLease(storePath, force, operation) {
|
|
|
10040
10597
|
await fs3.rm(storeLeasePath(resolved), { force: true });
|
|
10041
10598
|
return;
|
|
10042
10599
|
}
|
|
10043
|
-
const message = `Local store appears active for ${lease.mode}/${lease.transport} (pid ${lease.pid}, started ${lease.started_at}). Refusing ${operation}. Stop the server or rerun with --force if you have verified it is safe.`;
|
|
10600
|
+
const message = `Local store appears active for ${lease.mode}/${lease.transport} (pid ${lease.pid}, started ${lease.started_at}). Refusing ${operation}. Stop the server or rerun with --allow-concurrent-store/--force if you have verified it is safe.`;
|
|
10044
10601
|
if (!force) throw new Error(message);
|
|
10045
10602
|
process2.stderr.write(`Warning: ${message}
|
|
10046
10603
|
`);
|
|
@@ -10104,6 +10661,12 @@ function resultFormatOption(args) {
|
|
|
10104
10661
|
if (requested === "2" || requested === "v2") return 2;
|
|
10105
10662
|
throw new Error("--result-format must be v1, 1, v2, or 2");
|
|
10106
10663
|
}
|
|
10664
|
+
function normalizeResultFormatAnswer(value) {
|
|
10665
|
+
if (value === "1" || value === "v1") return "v1";
|
|
10666
|
+
if (value === "2" || value === "v2") return "v2";
|
|
10667
|
+
if (value === "default") return "default";
|
|
10668
|
+
throw new Error("--result-format must be default, v1, 1, v2, or 2");
|
|
10669
|
+
}
|
|
10107
10670
|
async function mcpAudit(args) {
|
|
10108
10671
|
const format = optionalArg(args, "--format") ?? (args.includes("--json") ? "json" : "text");
|
|
10109
10672
|
if (!["text", "json", "markdown"].includes(format)) {
|
|
@@ -11126,6 +11689,7 @@ async function activity(args) {
|
|
|
11126
11689
|
async function events(args) {
|
|
11127
11690
|
const [subcommand, ...rest] = args;
|
|
11128
11691
|
if (subcommand === "tail") return eventsTail(rest);
|
|
11692
|
+
if (subcommand === "webhook" || subcommand === "push") return eventsWebhook(rest);
|
|
11129
11693
|
usage(["events"]);
|
|
11130
11694
|
return 2;
|
|
11131
11695
|
}
|
|
@@ -11700,6 +12264,75 @@ async function eventsTail(args) {
|
|
|
11700
12264
|
});
|
|
11701
12265
|
return 0;
|
|
11702
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
|
+
}
|
|
11703
12336
|
async function storeStats(args) {
|
|
11704
12337
|
assertKnownOptions(args, storeStatsAllowedOptions, "store stats");
|
|
11705
12338
|
const store = await openLocalStore(args);
|
|
@@ -11849,6 +12482,15 @@ var eventTailAllowedOptions = /* @__PURE__ */ new Set([
|
|
|
11849
12482
|
"--follow",
|
|
11850
12483
|
"--interval-ms"
|
|
11851
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
|
+
]);
|
|
11852
12494
|
var replayShowAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--proposal", "--replay", "--evidence"]);
|
|
11853
12495
|
var replayExportAllowedOptions = /* @__PURE__ */ new Set([...replayShowAllowedOptions, "--output", "--out", "--format"]);
|
|
11854
12496
|
var replayListAllowedOptions = /* @__PURE__ */ new Set([
|
|
@@ -12051,6 +12693,17 @@ function eventFiltersFromArgs(args) {
|
|
|
12051
12693
|
limit: limitFromArgs(args)
|
|
12052
12694
|
};
|
|
12053
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
|
+
}
|
|
12054
12707
|
function linkedProposalFilter(args, store, options = {}) {
|
|
12055
12708
|
const noLinkedProposal = "__synapsor_no_linked_proposal__";
|
|
12056
12709
|
const replay2 = optionalArg(args, "--replay");
|
|
@@ -12231,8 +12884,8 @@ async function prepareReferenceDemo(args) {
|
|
|
12231
12884
|
].join("\n"));
|
|
12232
12885
|
const down = spawnSync("docker", ["compose", "-f", composePath, "down", "-v", "--remove-orphans"], { stdio: "inherit", env: process2.env });
|
|
12233
12886
|
if (down.status !== 0) return down.status ?? 1;
|
|
12234
|
-
const
|
|
12235
|
-
if (
|
|
12887
|
+
const up2 = spawnSync("docker", ["compose", "-f", composePath, "up", "-d"], { stdio: "inherit", env: process2.env });
|
|
12888
|
+
if (up2.status !== 0) return up2.status ?? 1;
|
|
12236
12889
|
await waitForReferenceDemoDatabase();
|
|
12237
12890
|
await fs3.copyFile(path3.join(demoDir, "synapsor.runner.json"), configPath);
|
|
12238
12891
|
process2.stdout.write([
|
|
@@ -12384,6 +13037,10 @@ function firstPositional(args) {
|
|
|
12384
13037
|
"--reason",
|
|
12385
13038
|
"--recipe",
|
|
12386
13039
|
"--receipt",
|
|
13040
|
+
"--read-tool",
|
|
13041
|
+
"--inspect-tool-name",
|
|
13042
|
+
"--proposal-tool",
|
|
13043
|
+
"--proposal-tool-name",
|
|
12387
13044
|
"--replay",
|
|
12388
13045
|
"--runner",
|
|
12389
13046
|
"--schema",
|
|
@@ -13024,6 +13681,48 @@ function formatEventLine(event, details = false) {
|
|
|
13024
13681
|
return `${lines.join("\n")}
|
|
13025
13682
|
`;
|
|
13026
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
|
+
}
|
|
13027
13726
|
function safeErrorMessage(error) {
|
|
13028
13727
|
return error instanceof Error ? error.message : String(error);
|
|
13029
13728
|
}
|
|
@@ -13311,7 +14010,7 @@ function starterCloudConfig() {
|
|
|
13311
14010
|
base_url_env: "SYNAPSOR_CLOUD_BASE_URL",
|
|
13312
14011
|
runner_token_env: "SYNAPSOR_RUNNER_TOKEN",
|
|
13313
14012
|
runner_id: "synapsor_runner_local",
|
|
13314
|
-
runner_version: "0.1.0-alpha.
|
|
14013
|
+
runner_version: "0.1.0-alpha.17",
|
|
13315
14014
|
project_id: "token_scope",
|
|
13316
14015
|
adapter_id: "mcp.your_adapter",
|
|
13317
14016
|
source_id: "src_replace_me",
|
|
@@ -13346,6 +14045,7 @@ function isKnownTopLevelCommand(command) {
|
|
|
13346
14045
|
"propose",
|
|
13347
14046
|
"audit",
|
|
13348
14047
|
"start",
|
|
14048
|
+
"up",
|
|
13349
14049
|
"runner",
|
|
13350
14050
|
"cloud",
|
|
13351
14051
|
"mcp",
|
|
@@ -13388,6 +14088,7 @@ Usage:
|
|
|
13388
14088
|
Commands:
|
|
13389
14089
|
inspect Inspect a Postgres/MySQL schema
|
|
13390
14090
|
start Start guided own-database setup, or no-arg legacy worker polling
|
|
14091
|
+
up Bring up local review mode guidance/server
|
|
13391
14092
|
init Generate a Synapsor capability contract
|
|
13392
14093
|
mcp Serve safe semantic tools over MCP
|
|
13393
14094
|
onboard One-command own-database setup
|
|
@@ -13402,7 +14103,7 @@ Commands:
|
|
|
13402
14103
|
query-audit Inspect local query audit records
|
|
13403
14104
|
receipts Inspect guarded writeback receipts
|
|
13404
14105
|
activity Search local evidence/replay ledger
|
|
13405
|
-
events Tail local proposal/writeback lifecycle events
|
|
14106
|
+
events Tail or push local proposal/writeback lifecycle events
|
|
13406
14107
|
store Inspect and maintain the local SQLite ledger
|
|
13407
14108
|
apply Apply an approved proposal with guarded writeback
|
|
13408
14109
|
replay Show what happened
|
|
@@ -13411,6 +14112,7 @@ Commands:
|
|
|
13411
14112
|
|
|
13412
14113
|
Examples:
|
|
13413
14114
|
${cmd} start --from-env DATABASE_URL
|
|
14115
|
+
${cmd} up --config ./synapsor.runner.json --store ./.synapsor/local.db --dry-run
|
|
13414
14116
|
${cmd} onboard db --from-env DATABASE_URL
|
|
13415
14117
|
${cmd} inspect --from-env DATABASE_URL
|
|
13416
14118
|
${cmd} init --wizard --from-env DATABASE_URL
|
|
@@ -13420,6 +14122,29 @@ Examples:
|
|
|
13420
14122
|
${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
13421
14123
|
${cmd} propose billing.propose_late_fee_waiver --sample
|
|
13422
14124
|
${cmd} audit ./synapsor.runner.json
|
|
14125
|
+
`,
|
|
14126
|
+
up: `Usage:
|
|
14127
|
+
${cmd} up --config ./synapsor.runner.json --store ./.synapsor/local.db [--transport stdio|streamable-http]
|
|
14128
|
+
${cmd} up --serve --config ./synapsor.runner.json --store ./.synapsor/local.db --port 8766 --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
|
|
14129
|
+
${cmd} up --config ./synapsor.runner.json --store ./.synapsor/local.db --handler-check --dry-run
|
|
14130
|
+
|
|
14131
|
+
Validate the local Runner config and store, summarize model-facing tools,
|
|
14132
|
+
explain direct SQL versus app-owned executor writeback, and print the next
|
|
14133
|
+
smoke/approve/apply/replay commands.
|
|
14134
|
+
|
|
14135
|
+
With --transport stdio, \`${cmd} up\` prints MCP client wiring because stdio is
|
|
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.
|
|
14139
|
+
|
|
14140
|
+
Options:
|
|
14141
|
+
--serve
|
|
14142
|
+
--alias-mode canonical|openai|both
|
|
14143
|
+
--result-format v1|v2
|
|
14144
|
+
--handler-check
|
|
14145
|
+
--with-handler
|
|
14146
|
+
--open-ui
|
|
14147
|
+
--dry-run
|
|
13423
14148
|
`,
|
|
13424
14149
|
start: `Usage:
|
|
13425
14150
|
${cmd} start --from-env DATABASE_URL [--schema public] [--mode read_only|shadow|review]
|
|
@@ -13444,12 +14169,17 @@ Inspect schema metadata without mutating the database or printing credentials.
|
|
|
13444
14169
|
init: `Usage:
|
|
13445
14170
|
${cmd} init --wizard --from-env DATABASE_URL [--mode read_only|review|shadow] [--out synapsor.runner.json]
|
|
13446
14171
|
${cmd} init --engine postgres --url-env DATABASE_URL --mode review --table public.invoices
|
|
13447
|
-
${cmd} init --inspection-json schema.json --table invoices --mode review --patch
|
|
13448
|
-
${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]
|
|
13449
14175
|
|
|
13450
14176
|
Generate a reviewed Synapsor Runner contract. Defaults to read-only in the wizard.
|
|
13451
14177
|
Review mode writeback choices: sql_update, http_handler, command_handler.
|
|
13452
|
-
|
|
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
|
+
`,
|
|
13453
14183
|
mcp: `Usage:
|
|
13454
14184
|
${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
13455
14185
|
${cmd} mcp serve --transport streamable-http --config ./synapsor.runner.json --store ./.synapsor/local.db --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
|
|
@@ -13576,9 +14306,12 @@ The template receives an approved proposal writeback request and must return an
|
|
|
13576
14306
|
`,
|
|
13577
14307
|
onboard: `Usage:
|
|
13578
14308
|
${cmd} onboard db --from-env DATABASE_URL [--schema public] [--mode read_only|shadow|review]
|
|
13579
|
-
${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
|
|
13580
14312
|
|
|
13581
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.
|
|
13582
14315
|
`,
|
|
13583
14316
|
propose: `Usage:
|
|
13584
14317
|
${cmd} propose <capability-name> --sample [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
|
|
@@ -13682,12 +14415,15 @@ Search the local SQLite evidence/replay ledger across proposals, evidence, query
|
|
|
13682
14415
|
`,
|
|
13683
14416
|
events: `Usage:
|
|
13684
14417
|
${cmd} events tail --store ./.synapsor/local.db
|
|
13685
|
-
|
|
13686
|
-
|
|
13687
|
-
|
|
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
|
|
13688
14424
|
|
|
13689
|
-
Show local proposal/writeback lifecycle events such as proposal_created, proposal_approved, writeback_applied, writeback_conflict, and writeback_failed.
|
|
13690
|
-
`,
|
|
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
|
+
`,
|
|
13691
14427
|
store: `Usage:
|
|
13692
14428
|
${cmd} store stats --store ./.synapsor/local.db
|
|
13693
14429
|
${cmd} store vacuum --store ./.synapsor/local.db
|