@withone/cli 1.37.3 → 1.39.0
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/README.md +3 -1
- package/dist/index.js +132 -39
- package/package.json +3 -2
- package/profiles/hacker-news/topStories.json +11 -0
- package/skills/one/SKILL.md +2 -0
package/README.md
CHANGED
|
@@ -330,6 +330,8 @@ one sync init stripe balanceTransactions --config '{"onInsert":"one flow execute
|
|
|
330
330
|
one sync run stripe --full-refresh
|
|
331
331
|
```
|
|
332
332
|
|
|
333
|
+
> **Sync uses passthrough actions only.** Profiles referencing a custom/composer action are rejected at runtime. `sync models` already filters to passthrough-only; if a model has no passthrough list endpoint, compose a flow instead of syncing.
|
|
334
|
+
|
|
333
335
|
| Subcommand | What it does |
|
|
334
336
|
|------------|-------------|
|
|
335
337
|
| `install` / `doctor` | Install + verify the SQLite engine |
|
|
@@ -343,7 +345,7 @@ one sync run stripe --full-refresh
|
|
|
343
345
|
| `schedule add/list/status/remove/repair` | Cron-backed scheduled syncs with drift detection |
|
|
344
346
|
| `remove <platform>` | Delete local data (`--dry-run` to preview) |
|
|
345
347
|
|
|
346
|
-
Change hooks (`onInsert`, `onUpdate`, `onChange`) fire per-page during sync — pipe to a shell command, a flow, or an event log. Run `one guide sync` for the full reference.
|
|
348
|
+
Change hooks (`onInsert`, `onUpdate`, `onChange`) fire per-page during sync — pipe to a shell command, a flow, or an event log. Root-array responses (e.g. Hacker News `/v0/topstories.json` → `[9129911, 9129199, ...]`) are supported by setting `resultsPath` to `""`, `"$"`, or `"."`; primitive elements are auto-wrapped as `{ [idField]: value }`. Run `one guide sync` for the full reference.
|
|
347
349
|
|
|
348
350
|
### `one guide [topic]`
|
|
349
351
|
|
package/dist/index.js
CHANGED
|
@@ -4666,6 +4666,9 @@ function parseActionType(actionKey) {
|
|
|
4666
4666
|
if (parts.length < 5) return null;
|
|
4667
4667
|
return parts[4];
|
|
4668
4668
|
}
|
|
4669
|
+
function isCustomAction(tags) {
|
|
4670
|
+
return !!tags && tags.includes("custom");
|
|
4671
|
+
}
|
|
4669
4672
|
async function discoverModels(api, platform) {
|
|
4670
4673
|
const actions2 = await api.listAvailableActions(platform);
|
|
4671
4674
|
const listActionTypes = /* @__PURE__ */ new Set(["get_many", "list", "get_all"]);
|
|
@@ -4674,6 +4677,7 @@ async function discoverModels(api, platform) {
|
|
|
4674
4677
|
const actionType = parseActionType(action.key);
|
|
4675
4678
|
if (!actionType) continue;
|
|
4676
4679
|
if (!listActionTypes.has(actionType)) continue;
|
|
4680
|
+
if (isCustomAction(action.tags)) continue;
|
|
4677
4681
|
const modelName = action.modelName;
|
|
4678
4682
|
if (!modelName) continue;
|
|
4679
4683
|
if (modelMap.has(modelName)) {
|
|
@@ -4697,7 +4701,7 @@ async function discoverModels(api, platform) {
|
|
|
4697
4701
|
try {
|
|
4698
4702
|
const searchResults = await api.searchActions(platform, model.displayName, "execute");
|
|
4699
4703
|
const resolved = searchResults.find(
|
|
4700
|
-
(a) => a.path === model.listAction.path && a.method === model.listAction.method
|
|
4704
|
+
(a) => a.path === model.listAction.path && a.method === model.listAction.method && !isCustomAction(a.tags)
|
|
4701
4705
|
);
|
|
4702
4706
|
if (resolved?.systemId) {
|
|
4703
4707
|
model.listAction.actionId = resolved.systemId;
|
|
@@ -4706,7 +4710,8 @@ async function discoverModels(api, platform) {
|
|
|
4706
4710
|
}
|
|
4707
4711
|
})
|
|
4708
4712
|
);
|
|
4709
|
-
|
|
4713
|
+
const syncable = models.filter((m) => m.listAction.actionId.startsWith("conn_mod_def::"));
|
|
4714
|
+
return syncable.sort((a, b) => a.name.localeCompare(b.name));
|
|
4710
4715
|
}
|
|
4711
4716
|
|
|
4712
4717
|
// src/lib/sync/profile.ts
|
|
@@ -4727,12 +4732,15 @@ function readProfile(platform, model) {
|
|
|
4727
4732
|
}
|
|
4728
4733
|
}
|
|
4729
4734
|
function writeProfile(profile) {
|
|
4730
|
-
const required = ["platform", "model", "connectionKey", "actionId", "
|
|
4735
|
+
const required = ["platform", "model", "connectionKey", "actionId", "idField", "pagination"];
|
|
4731
4736
|
for (const field of required) {
|
|
4732
4737
|
if (!profile[field]) {
|
|
4733
4738
|
throw new Error(`Missing required field: ${field}`);
|
|
4734
4739
|
}
|
|
4735
4740
|
}
|
|
4741
|
+
if (profile.resultsPath === void 0) {
|
|
4742
|
+
throw new Error('Missing required field: resultsPath (use "" or "$" for root-array responses)');
|
|
4743
|
+
}
|
|
4736
4744
|
if (!profile.pagination.type) {
|
|
4737
4745
|
throw new Error("Missing required field: pagination.type");
|
|
4738
4746
|
}
|
|
@@ -5521,6 +5529,11 @@ async function enrichPhase(api, db, config2, model, idField, connectionKey, plat
|
|
|
5521
5529
|
`Enrich: could not load action ${config2.actionId}: ${err instanceof Error ? err.message : String(err)}`
|
|
5522
5530
|
);
|
|
5523
5531
|
}
|
|
5532
|
+
if (detailAction.tags?.includes("custom")) {
|
|
5533
|
+
throw new Error(
|
|
5534
|
+
`Enrich does not support custom actions. Action ${config2.actionId} is tagged "custom". Use a passthrough detail endpoint \u2014 run 'one actions search ${platform} "<model> get"' to find one.`
|
|
5535
|
+
);
|
|
5536
|
+
}
|
|
5524
5537
|
let concurrency = config2.concurrency ?? DEFAULT_CONCURRENCY;
|
|
5525
5538
|
let enriched = 0;
|
|
5526
5539
|
let skipped = 0;
|
|
@@ -5694,6 +5707,50 @@ function detectColumnType2(value) {
|
|
|
5694
5707
|
return "TEXT";
|
|
5695
5708
|
}
|
|
5696
5709
|
|
|
5710
|
+
// src/lib/sync/extract.ts
|
|
5711
|
+
var ROOT_PATH_TOKENS = /* @__PURE__ */ new Set(["", "$", "."]);
|
|
5712
|
+
function isRootPath(resultsPath) {
|
|
5713
|
+
return resultsPath === void 0 || resultsPath === null || ROOT_PATH_TOKENS.has(resultsPath);
|
|
5714
|
+
}
|
|
5715
|
+
function isPrimitive(value) {
|
|
5716
|
+
const t = typeof value;
|
|
5717
|
+
return t === "string" || t === "number" || t === "boolean";
|
|
5718
|
+
}
|
|
5719
|
+
function describeType(value) {
|
|
5720
|
+
if (value === null) return "null";
|
|
5721
|
+
if (Array.isArray(value)) return "array";
|
|
5722
|
+
return typeof value;
|
|
5723
|
+
}
|
|
5724
|
+
function extractRecords(responseData, resultsPath, idField, profileLabel) {
|
|
5725
|
+
let candidate;
|
|
5726
|
+
if (isRootPath(resultsPath)) {
|
|
5727
|
+
candidate = responseData;
|
|
5728
|
+
} else {
|
|
5729
|
+
candidate = getByDotPath(responseData, resultsPath);
|
|
5730
|
+
}
|
|
5731
|
+
if (!Array.isArray(candidate)) {
|
|
5732
|
+
const topKeys = typeof responseData === "object" && responseData !== null && !Array.isArray(responseData) ? Object.keys(responseData).slice(0, 10) : [];
|
|
5733
|
+
const pathLabel = isRootPath(resultsPath) ? "<root>" : `'${resultsPath}'`;
|
|
5734
|
+
const typeLabel = describeType(responseData);
|
|
5735
|
+
const keyHint = topKeys.length > 0 ? ` Top-level keys: [${topKeys.join(", ")}].` : "";
|
|
5736
|
+
throw new Error(
|
|
5737
|
+
`Could not find results at path ${pathLabel} for profile ${profileLabel}. Response top-level type is ${typeLabel}.${keyHint} Set resultsPath to the array field, or use "" / "$" / "." for root arrays.`
|
|
5738
|
+
);
|
|
5739
|
+
}
|
|
5740
|
+
if (candidate.length === 0) {
|
|
5741
|
+
return { records: [], wrappedPrimitives: false };
|
|
5742
|
+
}
|
|
5743
|
+
if (isPrimitive(candidate[0])) {
|
|
5744
|
+
const wrapped = [];
|
|
5745
|
+
for (const value of candidate) {
|
|
5746
|
+
if (!isPrimitive(value)) continue;
|
|
5747
|
+
wrapped.push({ [idField]: String(value) });
|
|
5748
|
+
}
|
|
5749
|
+
return { records: wrapped, wrappedPrimitives: true };
|
|
5750
|
+
}
|
|
5751
|
+
return { records: candidate, wrappedPrimitives: false };
|
|
5752
|
+
}
|
|
5753
|
+
|
|
5697
5754
|
// src/lib/sync/runner.ts
|
|
5698
5755
|
var MAX_RETRIES_PER_PAGE = 3;
|
|
5699
5756
|
var DEFAULT_SINCE_DAYS = 90;
|
|
@@ -5866,6 +5923,11 @@ async function syncModel(api, profile, options) {
|
|
|
5866
5923
|
}
|
|
5867
5924
|
}
|
|
5868
5925
|
const actionDetails = await api.getActionDetails(profile.actionId);
|
|
5926
|
+
if (actionDetails.tags?.includes("custom")) {
|
|
5927
|
+
throw new Error(
|
|
5928
|
+
`Sync does not support custom actions. Action ${profile.actionId} is tagged "custom". Use a passthrough action \u2014 run 'one actions search ${platform} "${model}"' to find one, or compose a flow that chains passthrough calls if the logic is complex.`
|
|
5929
|
+
);
|
|
5930
|
+
}
|
|
5869
5931
|
const maxPages = options.maxPages ?? Infinity;
|
|
5870
5932
|
let tableCreated = tableExists(db, model);
|
|
5871
5933
|
let currentPageQueryParams = { ...queryParams };
|
|
@@ -5938,13 +6000,13 @@ async function syncModel(api, profile, options) {
|
|
|
5938
6000
|
throw err;
|
|
5939
6001
|
}
|
|
5940
6002
|
}
|
|
5941
|
-
const
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
6003
|
+
const extraction = extractRecords(
|
|
6004
|
+
responseData,
|
|
6005
|
+
profile.resultsPath,
|
|
6006
|
+
profile.idField,
|
|
6007
|
+
`${platform}/${model}`
|
|
6008
|
+
);
|
|
6009
|
+
const records = extraction.records;
|
|
5948
6010
|
if (records.length === 0 && page === 0) {
|
|
5949
6011
|
if (!isAgentMode()) {
|
|
5950
6012
|
process.stderr.write(`No records found for ${model} with the given filters.
|
|
@@ -6243,23 +6305,36 @@ async function testSyncProfile(api, profile) {
|
|
|
6243
6305
|
return report;
|
|
6244
6306
|
}
|
|
6245
6307
|
let resolvedResultsPath = profile.resultsPath;
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
records = topObj[arrayKey];
|
|
6308
|
+
const rawCandidate = isRootPath(resolvedResultsPath) ? responseData : getByDotPath(responseData, resolvedResultsPath);
|
|
6309
|
+
let records = Array.isArray(rawCandidate) ? rawCandidate : null;
|
|
6310
|
+
if (records === null) {
|
|
6311
|
+
if (Array.isArray(responseData)) {
|
|
6312
|
+
resolvedResultsPath = "";
|
|
6313
|
+
records = responseData;
|
|
6253
6314
|
report.autoFixed = report.autoFixed ?? {};
|
|
6254
|
-
report.autoFixed.resultsPath =
|
|
6315
|
+
report.autoFixed.resultsPath = "";
|
|
6255
6316
|
checks.push({
|
|
6256
|
-
name: `resultsPath auto-discovered \u2192
|
|
6317
|
+
name: `resultsPath auto-discovered \u2192 <root>`,
|
|
6257
6318
|
ok: true,
|
|
6258
|
-
detail: `
|
|
6319
|
+
detail: `Response is a root-level array \u2014 profile had "${profile.resultsPath}"`
|
|
6259
6320
|
});
|
|
6321
|
+
} else if (typeof responseData === "object" && responseData !== null) {
|
|
6322
|
+
const topObj = responseData;
|
|
6323
|
+
const arrayKey = Object.keys(topObj).find((k) => Array.isArray(topObj[k]));
|
|
6324
|
+
if (arrayKey) {
|
|
6325
|
+
resolvedResultsPath = arrayKey;
|
|
6326
|
+
records = topObj[arrayKey];
|
|
6327
|
+
report.autoFixed = report.autoFixed ?? {};
|
|
6328
|
+
report.autoFixed.resultsPath = arrayKey;
|
|
6329
|
+
checks.push({
|
|
6330
|
+
name: `resultsPath auto-discovered \u2192 "${arrayKey}"`,
|
|
6331
|
+
ok: true,
|
|
6332
|
+
detail: `Profile had "${profile.resultsPath}" which didn't resolve; found "${arrayKey}" in response`
|
|
6333
|
+
});
|
|
6334
|
+
}
|
|
6260
6335
|
}
|
|
6261
6336
|
}
|
|
6262
|
-
if (
|
|
6337
|
+
if (records === null) {
|
|
6263
6338
|
const topKeys = typeof responseData === "object" && responseData !== null ? Object.keys(responseData) : [];
|
|
6264
6339
|
checks.push({
|
|
6265
6340
|
name: `resultsPath "${resolvedResultsPath}" \u2192 array`,
|
|
@@ -6268,10 +6343,22 @@ async function testSyncProfile(api, profile) {
|
|
|
6268
6343
|
});
|
|
6269
6344
|
return report;
|
|
6270
6345
|
}
|
|
6346
|
+
let wrappedPrimitives = false;
|
|
6347
|
+
if (records.length > 0 && typeof records[0] !== "object") {
|
|
6348
|
+
const idField = profile.idField || "id";
|
|
6349
|
+
const wrapped = [];
|
|
6350
|
+
for (const v of records) {
|
|
6351
|
+
if (typeof v === "object") continue;
|
|
6352
|
+
wrapped.push({ [idField]: String(v) });
|
|
6353
|
+
}
|
|
6354
|
+
records = wrapped;
|
|
6355
|
+
wrappedPrimitives = true;
|
|
6356
|
+
}
|
|
6357
|
+
const pathLabel = isRootPath(resolvedResultsPath) ? "<root>" : `"${resolvedResultsPath}"`;
|
|
6271
6358
|
checks.push({
|
|
6272
|
-
name: `resultsPath
|
|
6359
|
+
name: `resultsPath ${pathLabel} \u2192 array`,
|
|
6273
6360
|
ok: true,
|
|
6274
|
-
detail: `${records.length} records`
|
|
6361
|
+
detail: wrappedPrimitives ? `${records.length} primitive records (wrapped as { ${profile.idField || "id"}: value })` : `${records.length} records`
|
|
6275
6362
|
});
|
|
6276
6363
|
if (records.length === 0) {
|
|
6277
6364
|
checks.push({ name: "sample record available", ok: false, detail: "empty result set" });
|
|
@@ -8510,23 +8597,29 @@ one --agent sync profiles stripe # filter by platform
|
|
|
8510
8597
|
|
|
8511
8598
|
When a built-in exists, \`sync init\` uses it automatically \u2014 no inference needed, no manual config. The agent just needs to match the user's intent to a profile description.
|
|
8512
8599
|
|
|
8513
|
-
## Action Resolution
|
|
8600
|
+
## Action Resolution \u2014 custom actions are hard-blocked
|
|
8514
8601
|
|
|
8515
|
-
Sync
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
of that creates problems, not value.
|
|
8602
|
+
Sync refuses to run against custom/composer actions (tag \`custom\`). Both
|
|
8603
|
+
the list action and any enrich detail action in a profile MUST be passthrough.
|
|
8604
|
+
\`sync run\` loads the action's knowledge, checks for the \`custom\` tag, and
|
|
8605
|
+
aborts with a clear error pointing at the passthrough alternative.
|
|
8520
8606
|
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
4. Only fall back to custom actions when no passthrough equivalent exists
|
|
8607
|
+
Why the block:
|
|
8608
|
+
- Custom actions run on a small shared fleet that collapses under sync-scale load
|
|
8609
|
+
- Custom list endpoints often expect filters in the body and silently return
|
|
8610
|
+
unfiltered or empty results (sync sends params as query/path only by design)
|
|
8611
|
+
- The sync engine already handles pagination, retry, rate limiting, and per-record
|
|
8612
|
+
enrichment locally \u2014 server-side fan-out on top of that creates 5xx, not value
|
|
8528
8613
|
|
|
8529
|
-
|
|
8614
|
+
How to build a profile:
|
|
8615
|
+
1. \`one actions search <platform> "<model>"\` surfaces passthrough actions.
|
|
8616
|
+
\`sync init\`'s auto-infer also drops customs before offering choices.
|
|
8617
|
+
2. Prefer GET passthrough endpoints (e.g. /gmail/v1/users/{userId}/threads)
|
|
8618
|
+
over POST custom endpoints (e.g. /gmail/get-threads).
|
|
8619
|
+
3. If no passthrough list action exists for a model, that model can't be
|
|
8620
|
+
synced. Compose a flow that chains passthrough calls instead. Custom
|
|
8621
|
+
actions are for one-off agent use only \u2014 never sync, never flow.
|
|
8622
|
+
4. The enrich \`actionId\` is held to the same rule: must be passthrough.
|
|
8530
8623
|
|
|
8531
8624
|
## Workflow: init \u2192 run \u2192 query
|
|
8532
8625
|
|
|
@@ -8558,7 +8651,7 @@ one --agent sync sql stripe "SELECT count(*) FROM balanceTransactions"
|
|
|
8558
8651
|
\`sync init\` without \`--config\` does all of this automatically:
|
|
8559
8652
|
- **connectionKey** \u2014 auto-resolved when there's exactly one connection for the platform
|
|
8560
8653
|
- **Pagination** \u2014 Stripe id-pagination, Notion body-cursor, HubSpot/Google token, offset, link. Inapplicable fields stripped (no nextPath for offset, no passAs for none)
|
|
8561
|
-
- **resultsPath** \u2014 generic keys (data, results, items) + platform-specific (model name stripped of platform prefix: attioCompanies \u2192 companies)
|
|
8654
|
+
- **resultsPath** \u2014 generic keys (data, results, items) + platform-specific (model name stripped of platform prefix: attioCompanies \u2192 companies). Use \`""\`, \`"$"\`, or \`"."\` for responses that return a bare array at the root (e.g. Hacker News \`/v0/topstories.json\`); primitive array elements are auto-wrapped as \`{ [idField]: value }\`.
|
|
8562
8655
|
- **idField** \u2014 id, _id, uuid
|
|
8563
8656
|
- **pathVars** \u2014 extracted from URL template with smart defaults (calendarId="primary", userId="me"). Internal keys (INTERNAL_SIGNING_KEY) and record-level IDs (record_id) are stripped automatically
|
|
8564
8657
|
- **dateFilter** \u2014 updated_since, created_after, etc.
|
|
@@ -8723,7 +8816,7 @@ Fetches ALL records and deletes local rows whose IDs are no longer in the source
|
|
|
8723
8816
|
|-------|----------|-------------|
|
|
8724
8817
|
| connectionKey | yes | From \`one list\` |
|
|
8725
8818
|
| actionId | yes | Auto-resolved by \`sync init\` |
|
|
8726
|
-
| resultsPath | yes | Auto-inferred or auto-discovered by \`sync test\` |
|
|
8819
|
+
| resultsPath | yes | Auto-inferred or auto-discovered by \`sync test\`. Use \`""\` / \`"$"\` / \`"."\` for root-array responses |
|
|
8727
8820
|
| idField | yes | Auto-inferred or auto-discovered by \`sync test\` |
|
|
8728
8821
|
| pagination | yes | Auto-inferred (cursor/token/offset/id/link/none) |
|
|
8729
8822
|
| pathVars | no | Auto-extracted from URL template |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withone/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.0",
|
|
4
4
|
"description": "CLI for managing One",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"build": "tsup",
|
|
17
17
|
"dev": "tsup --watch",
|
|
18
18
|
"start": "node bin/cli.js",
|
|
19
|
-
"typecheck": "tsc --noEmit"
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "tsx --test \"src/**/*.test.ts\""
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"@clack/prompts": "^0.9.1",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Hacker News top story IDs — up to 500 item IDs at /v0/topstories.json (root-array response, primitives wrapped as { id })",
|
|
3
|
+
"platform": "hacker-news",
|
|
4
|
+
"model": "topStories",
|
|
5
|
+
"connectionKey": "AUTO",
|
|
6
|
+
"actionId": "conn_mod_def::GJ3108Dwmm4::avAMAq7HQtW6PT8JhPg5vA",
|
|
7
|
+
"resultsPath": "",
|
|
8
|
+
"idField": "id",
|
|
9
|
+
"pagination": { "type": "none" },
|
|
10
|
+
"limitParam": ""
|
|
11
|
+
}
|
package/skills/one/SKILL.md
CHANGED
|
@@ -173,6 +173,8 @@ one --agent sync list stripe # progress + freshness
|
|
|
173
173
|
one sync schedule add stripe --every 1h
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
**Sync rejects custom actions** — profiles must use passthrough. `sync init` only surfaces passthrough models; `sync run` aborts if the list or enrich action is tagged `custom`. If no passthrough exists, compose a flow instead.
|
|
177
|
+
|
|
176
178
|
**Advanced features** (enrich, transform, exclude, identityKey, hooks, --full-refresh, --where-sql delete, cursor resume): run `one guide sync` for the full reference.
|
|
177
179
|
|
|
178
180
|
## Beyond Single Actions
|