@withone/cli 1.36.0 → 1.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4334,6 +4334,7 @@ async function relayCreateCommand(options) {
4334
4334
  if (options.description) body.description = options.description;
4335
4335
  if (options.eventFilters) body.eventFilters = parseJsonArg2(options.eventFilters, "--event-filters");
4336
4336
  if (options.tags) body.tags = parseJsonArg2(options.tags, "--tags");
4337
+ if (options.metadata) body.metadata = parseJsonArg2(options.metadata, "--metadata");
4337
4338
  if (options.createWebhook) body.createWebhook = true;
4338
4339
  const result = await api.createRelayEndpoint(body);
4339
4340
  if (isAgentMode()) {
@@ -4348,6 +4349,10 @@ async function relayCreateCommand(options) {
4348
4349
  if (result.description) console.log(` ${pc8.dim("Description:")} ${result.description}`);
4349
4350
  if (result.eventFilters?.length) console.log(` ${pc8.dim("Events:")} ${result.eventFilters.join(", ")}`);
4350
4351
  if (result.webhookPayload?.id) console.log(` ${pc8.dim("Webhook ID:")} ${result.webhookPayload.id}`);
4352
+ if (result.warning) {
4353
+ console.log();
4354
+ console.log(` ${pc8.yellow("\u26A0 Warning:")} ${result.warning}`);
4355
+ }
4351
4356
  console.log();
4352
4357
  } catch (error2) {
4353
4358
  spinner6.stop("Failed to create relay endpoint");
@@ -5262,6 +5267,80 @@ function runShellHook(command, events) {
5262
5267
  });
5263
5268
  }
5264
5269
 
5270
+ // src/lib/sync/transform.ts
5271
+ import { spawn as spawn3 } from "child_process";
5272
+ var TRANSFORM_TIMEOUT_MS = 6e4;
5273
+ async function transformRecords(command, records) {
5274
+ const input = JSON.stringify(records);
5275
+ return new Promise((resolve) => {
5276
+ const child = spawn3("sh", ["-c", command], {
5277
+ stdio: ["pipe", "pipe", "pipe"]
5278
+ });
5279
+ let stdout = "";
5280
+ let stderr = "";
5281
+ child.stdout.on("data", (chunk) => {
5282
+ stdout += chunk.toString();
5283
+ });
5284
+ child.stderr.on("data", (chunk) => {
5285
+ stderr += chunk.toString();
5286
+ });
5287
+ const timer = setTimeout(() => {
5288
+ try {
5289
+ child.kill();
5290
+ } catch {
5291
+ }
5292
+ if (!isAgentMode()) {
5293
+ process.stderr.write(` Transform timed out after ${TRANSFORM_TIMEOUT_MS / 1e3}s \u2014 using original records
5294
+ `);
5295
+ }
5296
+ resolve(null);
5297
+ }, TRANSFORM_TIMEOUT_MS);
5298
+ child.on("exit", (code) => {
5299
+ clearTimeout(timer);
5300
+ if (stderr.trim() && !isAgentMode()) {
5301
+ process.stderr.write(` Transform stderr: ${stderr.trim()}
5302
+ `);
5303
+ }
5304
+ if (code !== 0) {
5305
+ if (!isAgentMode()) {
5306
+ process.stderr.write(` Transform exited with code ${code} \u2014 using original records
5307
+ `);
5308
+ }
5309
+ resolve(null);
5310
+ return;
5311
+ }
5312
+ try {
5313
+ const parsed = JSON.parse(stdout.trim());
5314
+ if (!Array.isArray(parsed)) {
5315
+ if (!isAgentMode()) {
5316
+ process.stderr.write(` Transform returned non-array JSON \u2014 using original records
5317
+ `);
5318
+ }
5319
+ resolve(null);
5320
+ return;
5321
+ }
5322
+ resolve(parsed);
5323
+ } catch {
5324
+ if (!isAgentMode()) {
5325
+ process.stderr.write(` Transform returned invalid JSON \u2014 using original records
5326
+ `);
5327
+ }
5328
+ resolve(null);
5329
+ }
5330
+ });
5331
+ child.on("error", (err) => {
5332
+ clearTimeout(timer);
5333
+ if (!isAgentMode()) {
5334
+ process.stderr.write(` Transform failed to start: ${err.message} \u2014 using original records
5335
+ `);
5336
+ }
5337
+ resolve(null);
5338
+ });
5339
+ child.stdin.write(input);
5340
+ child.stdin.end();
5341
+ });
5342
+ }
5343
+
5265
5344
  // src/lib/sync/enrich.ts
5266
5345
  var DEFAULT_CONCURRENCY = 5;
5267
5346
  var MAX_RETRIES = 3;
@@ -5351,7 +5430,7 @@ function pickFields(obj, fields) {
5351
5430
  }
5352
5431
  return result;
5353
5432
  }
5354
- async function enrichPhase(api, db, config2, model, idField, connectionKey, platform) {
5433
+ async function enrichPhase(api, db, config2, model, idField, connectionKey, platform, ctx = {}) {
5355
5434
  const startTime = Date.now();
5356
5435
  const tsField = config2.timestampField ?? "_enriched_at";
5357
5436
  const safeTable = model.replace(/[^a-zA-Z0-9_]/g, "_");
@@ -5399,6 +5478,7 @@ async function enrichPhase(api, db, config2, model, idField, connectionKey, plat
5399
5478
  );
5400
5479
  let batchHitRateLimit = false;
5401
5480
  const now = (/* @__PURE__ */ new Date()).toISOString();
5481
+ const pending = [];
5402
5482
  for (let j = 0; j < results.length; j++) {
5403
5483
  const result = results[j];
5404
5484
  const row = batch[j];
@@ -5413,28 +5493,7 @@ async function enrichPhase(api, db, config2, model, idField, connectionKey, plat
5413
5493
  }
5414
5494
  const merged = config2.merge !== false ? deepMerge(row, enrichedData) : { ...enrichedData, [idField]: id };
5415
5495
  merged[tsField] = now;
5416
- const setClauses = [];
5417
- const values = [];
5418
- for (const [key, val] of Object.entries(merged)) {
5419
- if (key === idField) continue;
5420
- setClauses.push(`"${key}" = ?`);
5421
- values.push(prepareValue2(val));
5422
- }
5423
- values.push(prepareValue2(id));
5424
- if (setClauses.length > 0) {
5425
- const existingCols = new Set(db.prepare(`PRAGMA table_info("${safeTable}")`).all().map((c) => c.name));
5426
- for (const [key, val] of Object.entries(merged)) {
5427
- if (!existingCols.has(key)) {
5428
- const colType = detectColumnType2(val);
5429
- db.exec(`ALTER TABLE "${safeTable}" ADD COLUMN "${key}" ${colType}`);
5430
- existingCols.add(key);
5431
- }
5432
- }
5433
- db.prepare(
5434
- `UPDATE "${safeTable}" SET ${setClauses.join(", ")} WHERE "${safeIdField}" = ?`
5435
- ).run(...values);
5436
- }
5437
- enriched++;
5496
+ pending.push({ merged, id });
5438
5497
  } else if (result.status === "fulfilled" && result.value === null) {
5439
5498
  rateLimited++;
5440
5499
  skipped++;
@@ -5443,6 +5502,62 @@ async function enrichPhase(api, db, config2, model, idField, connectionKey, plat
5443
5502
  skipped++;
5444
5503
  }
5445
5504
  }
5505
+ let writes = pending;
5506
+ if (ctx.transform && pending.length > 0) {
5507
+ const transformed = await transformRecords(ctx.transform, pending.map((p10) => p10.merged));
5508
+ if (transformed) {
5509
+ const byId = /* @__PURE__ */ new Map();
5510
+ for (const r of transformed) byId.set(r[idField], r);
5511
+ writes = [];
5512
+ for (const p10 of pending) {
5513
+ const t = byId.get(p10.id);
5514
+ if (!t) continue;
5515
+ if (!t[tsField]) t[tsField] = now;
5516
+ writes.push({ merged: t, id: p10.id });
5517
+ }
5518
+ }
5519
+ }
5520
+ const hookEvents = [];
5521
+ for (const { merged, id } of writes) {
5522
+ if (ctx.exclude && ctx.exclude.length > 0) {
5523
+ stripExcludedFields(merged, ctx.exclude);
5524
+ }
5525
+ if (ctx.identityKey) {
5526
+ const raw = getByDotPath2(merged, ctx.identityKey);
5527
+ if (raw !== null && raw !== void 0) {
5528
+ merged._identity = String(raw).toLowerCase().trim();
5529
+ }
5530
+ }
5531
+ const setClauses = [];
5532
+ const values = [];
5533
+ for (const [key, val] of Object.entries(merged)) {
5534
+ if (key === idField) continue;
5535
+ setClauses.push(`"${key}" = ?`);
5536
+ values.push(prepareValue2(val));
5537
+ }
5538
+ values.push(prepareValue2(id));
5539
+ if (setClauses.length > 0) {
5540
+ const existingCols = new Set(db.prepare(`PRAGMA table_info("${safeTable}")`).all().map((c) => c.name));
5541
+ for (const [key, val] of Object.entries(merged)) {
5542
+ if (!existingCols.has(key)) {
5543
+ const colType = detectColumnType2(val);
5544
+ db.exec(`ALTER TABLE "${safeTable}" ADD COLUMN "${key}" ${colType}`);
5545
+ existingCols.add(key);
5546
+ }
5547
+ }
5548
+ db.prepare(
5549
+ `UPDATE "${safeTable}" SET ${setClauses.join(", ")} WHERE "${safeIdField}" = ?`
5550
+ ).run(...values);
5551
+ }
5552
+ enriched++;
5553
+ if (ctx.onUpdate || ctx.onChange) {
5554
+ hookEvents.push({ type: "update", platform, model, record: merged, timestamp: now });
5555
+ }
5556
+ }
5557
+ if (hookEvents.length > 0) {
5558
+ const hook = ctx.onUpdate || ctx.onChange;
5559
+ if (hook) await fireHooks(hook, hookEvents);
5560
+ }
5446
5561
  if (batchHitRateLimit) {
5447
5562
  concurrency = Math.max(1, Math.floor(concurrency / 2));
5448
5563
  if (!isAgentMode()) {
@@ -5522,80 +5637,6 @@ function detectColumnType2(value) {
5522
5637
  return "TEXT";
5523
5638
  }
5524
5639
 
5525
- // src/lib/sync/transform.ts
5526
- import { spawn as spawn3 } from "child_process";
5527
- var TRANSFORM_TIMEOUT_MS = 6e4;
5528
- async function transformRecords(command, records) {
5529
- const input = JSON.stringify(records);
5530
- return new Promise((resolve) => {
5531
- const child = spawn3("sh", ["-c", command], {
5532
- stdio: ["pipe", "pipe", "pipe"]
5533
- });
5534
- let stdout = "";
5535
- let stderr = "";
5536
- child.stdout.on("data", (chunk) => {
5537
- stdout += chunk.toString();
5538
- });
5539
- child.stderr.on("data", (chunk) => {
5540
- stderr += chunk.toString();
5541
- });
5542
- const timer = setTimeout(() => {
5543
- try {
5544
- child.kill();
5545
- } catch {
5546
- }
5547
- if (!isAgentMode()) {
5548
- process.stderr.write(` Transform timed out after ${TRANSFORM_TIMEOUT_MS / 1e3}s \u2014 using original records
5549
- `);
5550
- }
5551
- resolve(null);
5552
- }, TRANSFORM_TIMEOUT_MS);
5553
- child.on("exit", (code) => {
5554
- clearTimeout(timer);
5555
- if (stderr.trim() && !isAgentMode()) {
5556
- process.stderr.write(` Transform stderr: ${stderr.trim()}
5557
- `);
5558
- }
5559
- if (code !== 0) {
5560
- if (!isAgentMode()) {
5561
- process.stderr.write(` Transform exited with code ${code} \u2014 using original records
5562
- `);
5563
- }
5564
- resolve(null);
5565
- return;
5566
- }
5567
- try {
5568
- const parsed = JSON.parse(stdout.trim());
5569
- if (!Array.isArray(parsed)) {
5570
- if (!isAgentMode()) {
5571
- process.stderr.write(` Transform returned non-array JSON \u2014 using original records
5572
- `);
5573
- }
5574
- resolve(null);
5575
- return;
5576
- }
5577
- resolve(parsed);
5578
- } catch {
5579
- if (!isAgentMode()) {
5580
- process.stderr.write(` Transform returned invalid JSON \u2014 using original records
5581
- `);
5582
- }
5583
- resolve(null);
5584
- }
5585
- });
5586
- child.on("error", (err) => {
5587
- clearTimeout(timer);
5588
- if (!isAgentMode()) {
5589
- process.stderr.write(` Transform failed to start: ${err.message} \u2014 using original records
5590
- `);
5591
- }
5592
- resolve(null);
5593
- });
5594
- child.stdin.write(input);
5595
- child.stdin.end();
5596
- });
5597
- }
5598
-
5599
5640
  // src/lib/sync/runner.ts
5600
5641
  var MAX_RETRIES_PER_PAGE = 3;
5601
5642
  var DEFAULT_SINCE_DAYS = 90;
@@ -5995,11 +6036,22 @@ async function syncModel(api, profile, options) {
5995
6036
  model,
5996
6037
  profile.idField,
5997
6038
  profile.connectionKey,
5998
- platform
6039
+ platform,
6040
+ {
6041
+ transform: profile.transform,
6042
+ exclude: profile.exclude,
6043
+ identityKey: profile.identityKey,
6044
+ onInsert: profile.onInsert,
6045
+ onUpdate: profile.onUpdate,
6046
+ onChange: profile.onChange
6047
+ }
5999
6048
  );
6000
6049
  enrichedTotal = enrichResult.enriched;
6001
6050
  enrichSkipped = enrichResult.skipped;
6002
6051
  enrichRateLimited = enrichResult.rateLimited;
6052
+ if (hasHooks && (profile.onUpdate || profile.onChange)) {
6053
+ hooksUpdated += enrichResult.enriched;
6054
+ }
6003
6055
  if (enrichResult.enriched > 0) {
6004
6056
  rebuildFtsIndex(db, model);
6005
6057
  }
@@ -8201,8 +8253,32 @@ one --agent relay deliveries --endpoint-id <id> # Check delivery status
8201
8253
  2. **Get event types** \u2014 \`one --agent relay event-types <platform>\`
8202
8254
  3. **Get source knowledge** \u2014 understand the incoming webhook payload shape (\`{{payload.*}}\` paths)
8203
8255
  4. **Get destination knowledge** \u2014 understand the outgoing API body shape
8204
- 5. **Create endpoint** \u2014 with \`--create-webhook\` and \`--event-filters\`
8205
- 6. **Activate** \u2014 with passthrough action mapping source fields to destination fields
8256
+ 5. **Create endpoint** \u2014 with \`--create-webhook\`, \`--event-filters\`, and \`--metadata\` if the source platform requires it
8257
+ 6. **Activate** \u2014 with passthrough action mapping source fields to destination fields. **Do NOT pass \`--webhook-secret\`** when the endpoint was created with \`--create-webhook\` \u2014 the correct secret is auto-stored, and supplying a wrong one silently drops every delivery (events arrive, 0 deliveries).
8258
+
8259
+ ## Platform-Specific Metadata (\`--metadata\`)
8260
+
8261
+ Some source platforms need extra identifiers to register a webhook. Pass these via \`--metadata '<json>'\` on \`relay create\`. Without them, \`--create-webhook\` silently fails:
8262
+
8263
+ | Platform | Required metadata keys |
8264
+ |---|---|
8265
+ | \`github\` | \`GITHUB_OWNER\`, \`GITHUB_REPOSITORY\` |
8266
+ | \`typeform\` | \`TYPEFORM_FORM_ID\` |
8267
+ | \`stripe\` | (none) |
8268
+ | \`airtable\` | (none) |
8269
+ | \`attio\` | (none) |
8270
+ | \`google-calendar\` | (none) |
8271
+
8272
+ Example (GitHub):
8273
+
8274
+ \`\`\`bash
8275
+ one --agent relay create \\
8276
+ --connection-key "live::github::default::<key>" \\
8277
+ --event-filters '["issues","pull_request"]' \\
8278
+ --metadata '{"GITHUB_OWNER":"my-org","GITHUB_REPOSITORY":"my-repo"}' \\
8279
+ --description "GitHub relay" \\
8280
+ --create-webhook
8281
+ \`\`\`
8206
8282
 
8207
8283
  ## Template Context
8208
8284
 
@@ -8253,6 +8329,8 @@ Any connected platform can be a destination via passthrough actions.
8253
8329
  2. \`relay events --platform <p>\` \u2014 check events are arriving
8254
8330
  3. \`relay deliveries --event-id <id>\` \u2014 check delivery status and errors
8255
8331
  4. \`relay event <id>\` \u2014 inspect full payload to verify template paths
8332
+
8333
+ **If events arrive but 0 deliveries succeed**: you likely passed a wrong \`--webhook-secret\` on \`relay activate\`. When you created the endpoint with \`--create-webhook\`, the secret was registered with the source platform and stored automatically \u2014 do not pass it again on activate. Signature verification will fail silently and every event will be dropped.
8256
8334
  `;
8257
8335
  var GUIDE_CACHE = `# One Cache \u2014 Reference
8258
8336
 
@@ -8478,6 +8556,8 @@ The transform can be any command: \`jq\`, \`python3\`, a bash script, or \`one f
8478
8556
 
8479
8557
  **Pipeline order:** fetch \u2192 enrich \u2192 transform \u2192 **exclude** \u2192 create table \u2192 schema evolution \u2192 upsert \u2192 hooks
8480
8558
 
8559
+ Transform, exclude, identityKey, and hooks all fire in **both** phases. In Phase 1 they run on the raw list page; in Phase 2 they run again on the merged (list + enriched) record so that transforms can extract columns from fields that only appear after enrichment. Phase 2 fires \`onUpdate\`/\`onChange\` for every row it writes \u2014 \`onInsert\` is Phase-1-only because the row already exists in SQL by the time enrichment runs.
8560
+
8481
8561
  ## Cross-Platform Identity
8482
8562
 
8483
8563
  Add \`identityKey\` to a sync profile to extract a stable cross-platform identifier (e.g. email) into a normalized \`_identity\` column:
@@ -9416,7 +9496,7 @@ flow.command("scaffold [template]").description("Generate a workflow scaffold (t
9416
9496
  await flowScaffoldCommand(template);
9417
9497
  });
9418
9498
  var relay = program.command("relay").alias("r").description("Receive webhooks from platforms and relay them via passthrough actions");
9419
- relay.command("create").description("Create a new relay endpoint for a connection").requiredOption("--connection-key <key>", "Connection key for the source platform").option("--description <desc>", "Description of the relay endpoint").option("--event-filters <json>", `JSON array of event types to filter (e.g. '["customer.created"]')`).option("--tags <json>", "JSON array of tags").option("--create-webhook", "Automatically register the webhook with the source platform").action(async (options) => {
9499
+ relay.command("create").description("Create a new relay endpoint for a connection").requiredOption("--connection-key <key>", "Connection key for the source platform").option("--description <desc>", "Description of the relay endpoint").option("--event-filters <json>", `JSON array of event types to filter (e.g. '["customer.created"]')`).option("--tags <json>", "JSON array of tags").option("--metadata <json>", `JSON object of platform-specific metadata required to register the webhook (e.g. GitHub: '{"GITHUB_OWNER":"org","GITHUB_REPOSITORY":"repo"}', Typeform: '{"TYPEFORM_FORM_ID":"abc"}')`).option("--create-webhook", "Automatically register the webhook with the source platform").action(async (options) => {
9420
9500
  await relayCreateCommand(options);
9421
9501
  });
9422
9502
  relay.command("list").alias("ls").description("List all relay endpoints").option("--limit <n>", "Max results per page").option("--page <n>", "Page number").action(async (options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withone/cli",
3
- "version": "1.36.0",
3
+ "version": "1.37.1",
4
4
  "description": "CLI for managing One",
5
5
  "type": "module",
6
6
  "files": [
@@ -52,6 +52,25 @@ one --agent relay create \
52
52
 
53
53
  Always use `--create-webhook` — it registers the webhook URL with the source platform automatically.
54
54
 
55
+ **Some source platforms require extra identifiers via `--metadata`:**
56
+
57
+ | Platform | Required metadata keys |
58
+ |---|---|
59
+ | `github` | `GITHUB_OWNER`, `GITHUB_REPOSITORY` |
60
+ | `typeform` | `TYPEFORM_FORM_ID` |
61
+ | `stripe`, `airtable`, `attio`, `google-calendar` | (none) |
62
+
63
+ Without metadata, `--create-webhook` silently fails for these platforms. Example for GitHub:
64
+
65
+ ```bash
66
+ one --agent relay create \
67
+ --connection-key "live::github::default::<key>" \
68
+ --event-filters '["issues","pull_request"]' \
69
+ --metadata '{"GITHUB_OWNER":"my-org","GITHUB_REPOSITORY":"my-repo"}' \
70
+ --description "GitHub relay" \
71
+ --create-webhook
72
+ ```
73
+
55
74
  ### Step 6: Activate with a passthrough action
56
75
 
57
76
  ```bash
@@ -66,6 +85,8 @@ one --agent relay activate <relay-id> --actions '[{
66
85
  }]'
67
86
  ```
68
87
 
88
+ **Do NOT pass `--webhook-secret` on activate** when you created the endpoint with `--create-webhook`. The correct secret is registered with the source platform and stored automatically. Supplying a wrong one does not error — events arrive but every delivery is dropped during signature verification (0 deliveries). If you don't have a reason to override the secret, omit the flag.
89
+
69
90
  ## Template Context
70
91
 
71
92
  | Variable | Description |
@@ -177,3 +198,5 @@ one --agent relay deliveries --event-id <id>
177
198
  - Event filters on both the endpoint and individual actions must match
178
199
  - Multiple actions can be attached to a single relay endpoint
179
200
  - Missing template variables resolve to empty strings — verify `{{payload.*}}` paths against the actual payload
201
+ - GitHub and Typeform relays require `--metadata` on create; without it `--create-webhook` silently fails
202
+ - Never pass `--webhook-secret` on activate when the endpoint was created with `--create-webhook` — the auto-stored secret is correct, and a wrong one causes signature verification to drop every event silently