@withone/cli 1.52.3 → 1.53.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 +5 -2
- package/dist/index.js +86 -6
- package/package.json +1 -1
- package/skills/one/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -423,8 +423,11 @@ one sync run stripe --since 90d
|
|
|
423
423
|
one mem sync run stripe # identical (alias)
|
|
424
424
|
# Profiles with `enrich` run a second detail pass. It enriches each record ONCE;
|
|
425
425
|
# the list pass thereafter merges rather than replaces, so the enriched payload
|
|
426
|
-
# survives (reported as `memPreserved`). `--full-refresh`
|
|
427
|
-
#
|
|
426
|
+
# survives (reported as `memPreserved`). `--full-refresh` reconciles deletions,
|
|
427
|
+
# it does not refresh detail content. To refresh detail:
|
|
428
|
+
one sync run gmail --re-enrich # re-fetch every detail endpoint
|
|
429
|
+
# ...or set `enrich.invalidateOn` in the profile (e.g. "historyId", "updated_at")
|
|
430
|
+
# so only records that actually changed upstream are re-enriched, automatically.
|
|
428
431
|
|
|
429
432
|
# Query + search (reads from memory)
|
|
430
433
|
one sync query stripe/balanceTransactions --where "status=available" --limit 20
|
package/dist/index.js
CHANGED
|
@@ -5672,6 +5672,52 @@ function findEnrichedIds(db, model, idField, config2, records, tableCreated) {
|
|
|
5672
5672
|
}
|
|
5673
5673
|
return enriched;
|
|
5674
5674
|
}
|
|
5675
|
+
var ENRICH_FINGERPRINT_COLUMN = "_enrich_fp";
|
|
5676
|
+
function ensureColumn(db, safeTable, column, type) {
|
|
5677
|
+
const cols = db.prepare(`PRAGMA table_info("${safeTable}")`).all();
|
|
5678
|
+
if (!cols.some((c) => c.name === column)) {
|
|
5679
|
+
db.exec(`ALTER TABLE "${safeTable}" ADD COLUMN "${column}" ${type}`);
|
|
5680
|
+
}
|
|
5681
|
+
}
|
|
5682
|
+
function clearEnrichmentStamps(db, model, timestampField = "_enriched_at") {
|
|
5683
|
+
const safeTable = model.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
5684
|
+
const exists = db.prepare(
|
|
5685
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`
|
|
5686
|
+
).get(safeTable);
|
|
5687
|
+
if (!exists) return 0;
|
|
5688
|
+
const cols = db.prepare(`PRAGMA table_info("${safeTable}")`).all();
|
|
5689
|
+
if (!cols.some((c) => c.name === timestampField)) return 0;
|
|
5690
|
+
const result = db.prepare(
|
|
5691
|
+
`UPDATE "${safeTable}" SET "${timestampField}" = NULL WHERE "${timestampField}" IS NOT NULL`
|
|
5692
|
+
).run();
|
|
5693
|
+
return result.changes;
|
|
5694
|
+
}
|
|
5695
|
+
function invalidateStaleEnrichments(db, model, config2) {
|
|
5696
|
+
const fpField = config2.invalidateOn;
|
|
5697
|
+
if (!fpField) return 0;
|
|
5698
|
+
const tsField = config2.timestampField ?? "_enriched_at";
|
|
5699
|
+
const safeTable = model.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
5700
|
+
const exists = db.prepare(
|
|
5701
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`
|
|
5702
|
+
).get(safeTable);
|
|
5703
|
+
if (!exists) return 0;
|
|
5704
|
+
const cols = new Set(
|
|
5705
|
+
db.prepare(`PRAGMA table_info("${safeTable}")`).all().map((c) => c.name)
|
|
5706
|
+
);
|
|
5707
|
+
if (!cols.has(fpField)) return 0;
|
|
5708
|
+
if (!cols.has(tsField)) return 0;
|
|
5709
|
+
if (!cols.has(ENRICH_FINGERPRINT_COLUMN)) {
|
|
5710
|
+
ensureColumn(db, safeTable, ENRICH_FINGERPRINT_COLUMN, "TEXT");
|
|
5711
|
+
return 0;
|
|
5712
|
+
}
|
|
5713
|
+
const result = db.prepare(
|
|
5714
|
+
`UPDATE "${safeTable}" SET "${tsField}" = NULL
|
|
5715
|
+
WHERE "${tsField}" IS NOT NULL
|
|
5716
|
+
AND "${ENRICH_FINGERPRINT_COLUMN}" IS NOT NULL
|
|
5717
|
+
AND "${ENRICH_FINGERPRINT_COLUMN}" IS NOT CAST("${fpField}" AS TEXT)`
|
|
5718
|
+
).run();
|
|
5719
|
+
return result.changes;
|
|
5720
|
+
}
|
|
5675
5721
|
async function enrichPhase(api, db, config2, model, idField, connectionKey, platform, ctx = {}) {
|
|
5676
5722
|
const startTime = Date.now();
|
|
5677
5723
|
const tsField = config2.timestampField ?? "_enriched_at";
|
|
@@ -5740,6 +5786,10 @@ async function enrichPhase(api, db, config2, model, idField, connectionKey, plat
|
|
|
5740
5786
|
}
|
|
5741
5787
|
const merged = config2.merge !== false ? deepMerge(row, enrichedData) : { ...enrichedData, [idField]: id };
|
|
5742
5788
|
merged[tsField] = now;
|
|
5789
|
+
if (config2.invalidateOn) {
|
|
5790
|
+
const fp = merged[config2.invalidateOn] ?? row[config2.invalidateOn];
|
|
5791
|
+
merged[ENRICH_FINGERPRINT_COLUMN] = fp == null ? null : String(fp);
|
|
5792
|
+
}
|
|
5743
5793
|
pending.push({ merged, id });
|
|
5744
5794
|
} else if (result.status === "fulfilled" && result.value === null) {
|
|
5745
5795
|
rateLimited++;
|
|
@@ -5803,7 +5853,7 @@ async function enrichPhase(api, db, config2, model, idField, connectionKey, plat
|
|
|
5803
5853
|
hookEvents.push({ type: "update", platform, model, record: merged, timestamp: now });
|
|
5804
5854
|
}
|
|
5805
5855
|
}
|
|
5806
|
-
if (ctx.profile && memoryBatch.length > 0) {
|
|
5856
|
+
if (ctx.profile && ctx.writeToMemory !== false && memoryBatch.length > 0) {
|
|
5807
5857
|
try {
|
|
5808
5858
|
await writePageToMemory(ctx.profile, memoryBatch);
|
|
5809
5859
|
} catch (err) {
|
|
@@ -6458,6 +6508,19 @@ async function syncModel(api, profile, options) {
|
|
|
6458
6508
|
}
|
|
6459
6509
|
let enrichResult = null;
|
|
6460
6510
|
if (profile.enrich && db && tableCreated && !options.dryRun) {
|
|
6511
|
+
if (options.reEnrich) {
|
|
6512
|
+
const cleared = clearEnrichmentStamps(db, model, profile.enrich.timestampField);
|
|
6513
|
+
if (cleared > 0 && !isAgentMode()) {
|
|
6514
|
+
console.log(` Re-enriching ${cleared} record(s) (--re-enrich)`);
|
|
6515
|
+
}
|
|
6516
|
+
} else {
|
|
6517
|
+
const invalidated = invalidateStaleEnrichments(db, model, profile.enrich);
|
|
6518
|
+
if (invalidated > 0 && !isAgentMode()) {
|
|
6519
|
+
console.log(
|
|
6520
|
+
` ${invalidated} record(s) changed upstream (${profile.enrich.invalidateOn}) \u2014 re-enriching those`
|
|
6521
|
+
);
|
|
6522
|
+
}
|
|
6523
|
+
}
|
|
6461
6524
|
enrichResult = await enrichPhase(
|
|
6462
6525
|
api,
|
|
6463
6526
|
db,
|
|
@@ -6476,7 +6539,9 @@ async function syncModel(api, profile, options) {
|
|
|
6476
6539
|
// Pass the full profile so enrich can mirror merged rows into the
|
|
6477
6540
|
// memory store. Without this, memory holds only the pre-enrich
|
|
6478
6541
|
// list payload — see enrich.ts:memoryBatch writeback.
|
|
6479
|
-
profile
|
|
6542
|
+
profile,
|
|
6543
|
+
// Phase 1 honours --no-memory; phase 2 used to ignore it. (#174)
|
|
6544
|
+
writeToMemory: options.toMemory !== false
|
|
6480
6545
|
}
|
|
6481
6546
|
);
|
|
6482
6547
|
enrichedTotal = enrichResult.enriched;
|
|
@@ -8596,7 +8661,7 @@ function registerSyncSubcommands(sync) {
|
|
|
8596
8661
|
sync.command("suggest-searchable <platform/model>").description("Rank candidate memory.searchable paths by signal density (for profiles with memory.embed: true)").option("--limit <n>", "Top N paths to return (default 15)").action(async (platformModel, options) => {
|
|
8597
8662
|
await syncSuggestSearchableCommand(platformModel, options);
|
|
8598
8663
|
});
|
|
8599
|
-
sync.command("run <platform>").description("Run sync for a platform (syncs all configured models, or specify --models)").option("--models <models>", "Comma-separated list of models to sync").option("--since <duration>", "Sync records since duration (e.g. 90d, 30d, 7d) or date").option("--force", "Ignore existing sync state and start fresh").option("--max-pages <n>", "Maximum number of pages to fetch").option("--dry-run", "Fetch first page only, show results without persisting").option("--full-refresh", "Fetch ALL records and delete local rows no longer in the source (handles deletions)").option("--no-memory", "Skip the unified memory dual-write (default: memory is always written)").option("--embed", "Embed synced rows under the configured model, regardless of profile.memory.embed", false).option("--no-embed", "Skip embedding even if the profile opts in").option("--to-memory", "(deprecated \u2014 memory is now always written; flag kept for back-compat)").action(async (platform, options) => {
|
|
8664
|
+
sync.command("run <platform>").description("Run sync for a platform (syncs all configured models, or specify --models)").option("--models <models>", "Comma-separated list of models to sync").option("--since <duration>", "Sync records since duration (e.g. 90d, 30d, 7d) or date").option("--force", "Ignore existing sync state and start fresh").option("--max-pages <n>", "Maximum number of pages to fetch").option("--dry-run", "Fetch first page only, show results without persisting").option("--full-refresh", "Fetch ALL records and delete local rows no longer in the source (handles deletions)").option("--re-enrich", 'Re-fetch the detail endpoint for every already-enriched record (profiles with an "enrich" block). Costs one detail call per record \u2014 for routine freshness set enrich.invalidateOn in the profile instead').option("--no-memory", "Skip the unified memory dual-write (default: memory is always written)").option("--embed", "Embed synced rows under the configured model, regardless of profile.memory.embed", false).option("--no-embed", "Skip embedding even if the profile opts in").option("--to-memory", "(deprecated \u2014 memory is now always written; flag kept for back-compat)").action(async (platform, options) => {
|
|
8600
8665
|
await syncRunCommand(platform, {
|
|
8601
8666
|
models: options.models?.split(",").map((m) => m.trim()),
|
|
8602
8667
|
since: options.since,
|
|
@@ -8604,6 +8669,7 @@ function registerSyncSubcommands(sync) {
|
|
|
8604
8669
|
maxPages: options.maxPages ? parseInt(options.maxPages, 10) : void 0,
|
|
8605
8670
|
dryRun: options.dryRun,
|
|
8606
8671
|
fullRefresh: options.fullRefresh,
|
|
8672
|
+
reEnrich: options.reEnrich,
|
|
8607
8673
|
// Commander inverts `--no-memory` / `--no-embed` into options.X === false.
|
|
8608
8674
|
toMemory: options.memory !== false,
|
|
8609
8675
|
// Only override when the flag was actually passed — leaving
|
|
@@ -10740,6 +10806,7 @@ When a list endpoint returns lightweight records (e.g. just IDs), add an \`enric
|
|
|
10740
10806
|
"enrich": {
|
|
10741
10807
|
"actionId": "<get-message-action-id>",
|
|
10742
10808
|
"pathVars": { "messageId": "{{id}}" },
|
|
10809
|
+
"invalidateOn": "historyId",
|
|
10743
10810
|
"concurrency": 3,
|
|
10744
10811
|
"delayMs": 200
|
|
10745
10812
|
}
|
|
@@ -10747,7 +10814,8 @@ When a list endpoint returns lightweight records (e.g. just IDs), add an \`enric
|
|
|
10747
10814
|
\`\`\`
|
|
10748
10815
|
|
|
10749
10816
|
- \`pathVars\` / \`queryParams\` / \`body\` support \`{{field}}\` interpolation from the list record
|
|
10750
|
-
- \`
|
|
10817
|
+
- \`invalidateOn\` names a list field that acts as a change fingerprint, so records whose detail went stale are re-enriched automatically (see "Re-enriching" below). Omit it to keep enrich-exactly-once behaviour
|
|
10818
|
+
- \`concurrency\` controls parallel detail requests per page (default: 5, lower = safer for rate limits)
|
|
10751
10819
|
- \`delayMs\` is the pause between batches (default: 200ms)
|
|
10752
10820
|
- \`resultsPath\` extracts a sub-object from the detail response before merging
|
|
10753
10821
|
- \`merge: false\` replaces the record entirely instead of deep-merging
|
|
@@ -10803,7 +10871,19 @@ So for a row the mirror reports as already enriched, Phase 1 writes **non-author
|
|
|
10803
10871
|
|
|
10804
10872
|
The write still happens, so \`--embed\`, profile edits, and the store's un-archive self-heal all keep working on enriched rows. The one thing given up: a field that disappears from the *list* shape upstream no longer disappears from an enriched record, because a merge cannot delete. \`sync run\` reports the count as \`memPreserved\`.
|
|
10805
10873
|
|
|
10806
|
-
**
|
|
10874
|
+
**Re-enriching.** \`--full-refresh\` re-pulls the list but deliberately does **not** clear \`_enriched_at\` \u2014 it reconciles deletions, it is not a detail refresh. There are two ways to refresh detail content:
|
|
10875
|
+
|
|
10876
|
+
- **\`enrich.invalidateOn\` (preferred, automatic).** Name a list-endpoint field that acts as a change fingerprint \u2014 \`historyId\` for Gmail threads, \`updated_at\` for Fathom meetings. The value seen at enrich time is recorded, and on the next sync any record whose fingerprint has moved is re-enriched. Records that did not change upstream cost nothing.
|
|
10877
|
+
|
|
10878
|
+
\`\`\`json
|
|
10879
|
+
{ "enrich": { "actionId": "...", "invalidateOn": "historyId" } }
|
|
10880
|
+
\`\`\`
|
|
10881
|
+
|
|
10882
|
+
Additive by design: rows enriched before a fingerprint existed are never auto-invalidated (that would re-enrich the whole table on the first run after upgrading) \u2014 they pick one up the next time they enrich. Profiles with no sensible fingerprint field simply omit it and keep enrich-exactly-once behaviour.
|
|
10883
|
+
|
|
10884
|
+
- **\`one sync run <platform> --re-enrich\` (manual escape hatch).** Clears every enrichment stamp and re-fetches all detail endpoints. Costs one detail call per record, so it is opt-in per run and never implied by \`--full-refresh\`. Use it when the detail *shape* changed, or for profiles with no fingerprint field.
|
|
10885
|
+
|
|
10886
|
+
Deleting the mirror (\`.one/sync/data/<platform>.db\`) is no longer necessary.
|
|
10807
10887
|
|
|
10808
10888
|
## Cross-Platform Identity
|
|
10809
10889
|
|
|
@@ -10909,7 +10989,7 @@ Every \`sync X\` command is also exposed as \`mem sync X\` \u2014 same handlers,
|
|
|
10909
10989
|
| \`sync init <plat> <model>\` | Create/patch profile (seeds from built-in, auto-tests) |
|
|
10910
10990
|
| \`sync test <plat>/<model>\` | Validate profile. \`--show-searchable\` previews embedded text across 5 samples with per-path hit rates |
|
|
10911
10991
|
| \`sync suggest-searchable <plat>/<model>\` | Rank candidate \`memory.searchable\` paths by signal density; emits paste-ready config |
|
|
10912
|
-
| \`sync run <platform>\` | Sync data (\`--full-refresh\`, \`--since\`, \`--dry-run\`, \`--no-memory\`) |
|
|
10992
|
+
| \`sync run <platform>\` | Sync data (\`--full-refresh\`, \`--since\`, \`--dry-run\`, \`--no-memory\`, \`--re-enrich\`) |
|
|
10913
10993
|
| \`sync query <plat>/<model>\` | Query memory with \`--where\` (dotted paths), \`--after/before\` |
|
|
10914
10994
|
| \`sync schema <plat>/<model>\` | Inspect the JSON structure of synced records (field paths, types, examples) \u2014 run before writing \`--where\` / query paths |
|
|
10915
10995
|
| \`sync search "<query>"\` | Hybrid FTS + semantic across all synced data |
|
package/package.json
CHANGED
package/skills/one/SKILL.md
CHANGED
|
@@ -318,7 +318,7 @@ Without declared paths, the default walker concatenates every string in the reco
|
|
|
318
318
|
|
|
319
319
|
Both are queryable with `one --agent mem find-by-key <prefix>:<value>`.
|
|
320
320
|
|
|
321
|
-
**Enriching profiles** (`gmail/gmailThreads`, `fathom/meetings`) sync in two phases: a list pass, then a detail pass that fetches full bodies/transcripts. Two things follow. Enrichment happens **once per record** — phase 2 only visits rows it has never enriched, and `--full-refresh` does *not* reset that,
|
|
321
|
+
**Enriching profiles** (`gmail/gmailThreads`, `fathom/meetings`) sync in two phases: a list pass, then a detail pass that fetches full bodies/transcripts. Two things follow. Enrichment happens **once per record by default** — phase 2 only visits rows it has never enriched, and `--full-refresh` does *not* reset that (it reconciles deletions, it is not a detail refresh). To refresh detail content, either set `enrich.invalidateOn` in the profile to a list field that moves when the detail changes (`historyId`, `updated_at`) so only genuinely-changed records re-enrich automatically, or run `one sync run <platform> --re-enrich` to re-fetch every detail endpoint. And the list pass never overwrites an enriched record: `data` merges rather than replaces, and `searchable_text` / `identity_keys[]` are left alone. `sync run` reports these as `memPreserved`. So on an enriching profile without `invalidateOn`, a record whose upstream *detail* changed will look stale until you re-enrich — that is expected, not a sync failure.
|
|
322
322
|
|
|
323
323
|
**Advanced features** (enrich, transform, exclude, hooks, --full-refresh, alternative backends, embedding tuning): run `one guide memory` or `one guide sync` for the full reference.
|
|
324
324
|
|