@withone/cli 1.52.3 → 1.54.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
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
|
|
@@ -143,6 +143,15 @@ function loadBuiltinProfile(platform, model) {
|
|
|
143
143
|
return null;
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
var CAPABILITY_FIELDS = ["identityKeys", "identityKey", "enrich", "dateFilter", "memory"];
|
|
147
|
+
function findMissingBuiltinCapabilities(platform, model, installed) {
|
|
148
|
+
if (!installed) return [];
|
|
149
|
+
const builtin = loadBuiltinProfile(platform, model);
|
|
150
|
+
if (!builtin) return [];
|
|
151
|
+
return CAPABILITY_FIELDS.filter(
|
|
152
|
+
(field) => builtin[field] !== void 0 && installed[field] === void 0
|
|
153
|
+
);
|
|
154
|
+
}
|
|
146
155
|
function listBuiltinProfiles(platform) {
|
|
147
156
|
const dir = getProfilesDir();
|
|
148
157
|
if (!dir) return [];
|
|
@@ -1070,6 +1079,7 @@ export {
|
|
|
1070
1079
|
resolveEntityIdentity,
|
|
1071
1080
|
collectIdentityKeys,
|
|
1072
1081
|
loadBuiltinProfile,
|
|
1082
|
+
findMissingBuiltinCapabilities,
|
|
1073
1083
|
listBuiltinProfiles,
|
|
1074
1084
|
memMigrateCommand,
|
|
1075
1085
|
buildIdentityMap,
|
package/dist/index.js
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
ensureTable,
|
|
46
46
|
evolveSchema,
|
|
47
47
|
extractSearchableFromPaths,
|
|
48
|
+
findMissingBuiltinCapabilities,
|
|
48
49
|
generateTemplate,
|
|
49
50
|
getDatabaseSize,
|
|
50
51
|
getSearchablePaths,
|
|
@@ -65,7 +66,7 @@ import {
|
|
|
65
66
|
writeDraftProfile,
|
|
66
67
|
writePageToMemory,
|
|
67
68
|
writeProfile
|
|
68
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-XN4ZWMLX.js";
|
|
69
70
|
import {
|
|
70
71
|
getByDotPath
|
|
71
72
|
} from "./chunk-44CV5IMX.js";
|
|
@@ -5672,6 +5673,52 @@ function findEnrichedIds(db, model, idField, config2, records, tableCreated) {
|
|
|
5672
5673
|
}
|
|
5673
5674
|
return enriched;
|
|
5674
5675
|
}
|
|
5676
|
+
var ENRICH_FINGERPRINT_COLUMN = "_enrich_fp";
|
|
5677
|
+
function ensureColumn(db, safeTable, column, type) {
|
|
5678
|
+
const cols = db.prepare(`PRAGMA table_info("${safeTable}")`).all();
|
|
5679
|
+
if (!cols.some((c) => c.name === column)) {
|
|
5680
|
+
db.exec(`ALTER TABLE "${safeTable}" ADD COLUMN "${column}" ${type}`);
|
|
5681
|
+
}
|
|
5682
|
+
}
|
|
5683
|
+
function clearEnrichmentStamps(db, model, timestampField = "_enriched_at") {
|
|
5684
|
+
const safeTable = model.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
5685
|
+
const exists = db.prepare(
|
|
5686
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`
|
|
5687
|
+
).get(safeTable);
|
|
5688
|
+
if (!exists) return 0;
|
|
5689
|
+
const cols = db.prepare(`PRAGMA table_info("${safeTable}")`).all();
|
|
5690
|
+
if (!cols.some((c) => c.name === timestampField)) return 0;
|
|
5691
|
+
const result = db.prepare(
|
|
5692
|
+
`UPDATE "${safeTable}" SET "${timestampField}" = NULL WHERE "${timestampField}" IS NOT NULL`
|
|
5693
|
+
).run();
|
|
5694
|
+
return result.changes;
|
|
5695
|
+
}
|
|
5696
|
+
function invalidateStaleEnrichments(db, model, config2) {
|
|
5697
|
+
const fpField = config2.invalidateOn;
|
|
5698
|
+
if (!fpField) return 0;
|
|
5699
|
+
const tsField = config2.timestampField ?? "_enriched_at";
|
|
5700
|
+
const safeTable = model.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
5701
|
+
const exists = db.prepare(
|
|
5702
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`
|
|
5703
|
+
).get(safeTable);
|
|
5704
|
+
if (!exists) return 0;
|
|
5705
|
+
const cols = new Set(
|
|
5706
|
+
db.prepare(`PRAGMA table_info("${safeTable}")`).all().map((c) => c.name)
|
|
5707
|
+
);
|
|
5708
|
+
if (!cols.has(fpField)) return 0;
|
|
5709
|
+
if (!cols.has(tsField)) return 0;
|
|
5710
|
+
if (!cols.has(ENRICH_FINGERPRINT_COLUMN)) {
|
|
5711
|
+
ensureColumn(db, safeTable, ENRICH_FINGERPRINT_COLUMN, "TEXT");
|
|
5712
|
+
return 0;
|
|
5713
|
+
}
|
|
5714
|
+
const result = db.prepare(
|
|
5715
|
+
`UPDATE "${safeTable}" SET "${tsField}" = NULL
|
|
5716
|
+
WHERE "${tsField}" IS NOT NULL
|
|
5717
|
+
AND "${ENRICH_FINGERPRINT_COLUMN}" IS NOT NULL
|
|
5718
|
+
AND "${ENRICH_FINGERPRINT_COLUMN}" IS NOT CAST("${fpField}" AS TEXT)`
|
|
5719
|
+
).run();
|
|
5720
|
+
return result.changes;
|
|
5721
|
+
}
|
|
5675
5722
|
async function enrichPhase(api, db, config2, model, idField, connectionKey, platform, ctx = {}) {
|
|
5676
5723
|
const startTime = Date.now();
|
|
5677
5724
|
const tsField = config2.timestampField ?? "_enriched_at";
|
|
@@ -5740,6 +5787,10 @@ async function enrichPhase(api, db, config2, model, idField, connectionKey, plat
|
|
|
5740
5787
|
}
|
|
5741
5788
|
const merged = config2.merge !== false ? deepMerge(row, enrichedData) : { ...enrichedData, [idField]: id };
|
|
5742
5789
|
merged[tsField] = now;
|
|
5790
|
+
if (config2.invalidateOn) {
|
|
5791
|
+
const fp = merged[config2.invalidateOn] ?? row[config2.invalidateOn];
|
|
5792
|
+
merged[ENRICH_FINGERPRINT_COLUMN] = fp == null ? null : String(fp);
|
|
5793
|
+
}
|
|
5743
5794
|
pending.push({ merged, id });
|
|
5744
5795
|
} else if (result.status === "fulfilled" && result.value === null) {
|
|
5745
5796
|
rateLimited++;
|
|
@@ -5803,7 +5854,7 @@ async function enrichPhase(api, db, config2, model, idField, connectionKey, plat
|
|
|
5803
5854
|
hookEvents.push({ type: "update", platform, model, record: merged, timestamp: now });
|
|
5804
5855
|
}
|
|
5805
5856
|
}
|
|
5806
|
-
if (ctx.profile && memoryBatch.length > 0) {
|
|
5857
|
+
if (ctx.profile && ctx.writeToMemory !== false && memoryBatch.length > 0) {
|
|
5807
5858
|
try {
|
|
5808
5859
|
await writePageToMemory(ctx.profile, memoryBatch);
|
|
5809
5860
|
} catch (err) {
|
|
@@ -6458,6 +6509,19 @@ async function syncModel(api, profile, options) {
|
|
|
6458
6509
|
}
|
|
6459
6510
|
let enrichResult = null;
|
|
6460
6511
|
if (profile.enrich && db && tableCreated && !options.dryRun) {
|
|
6512
|
+
if (options.reEnrich) {
|
|
6513
|
+
const cleared = clearEnrichmentStamps(db, model, profile.enrich.timestampField);
|
|
6514
|
+
if (cleared > 0 && !isAgentMode()) {
|
|
6515
|
+
console.log(` Re-enriching ${cleared} record(s) (--re-enrich)`);
|
|
6516
|
+
}
|
|
6517
|
+
} else {
|
|
6518
|
+
const invalidated = invalidateStaleEnrichments(db, model, profile.enrich);
|
|
6519
|
+
if (invalidated > 0 && !isAgentMode()) {
|
|
6520
|
+
console.log(
|
|
6521
|
+
` ${invalidated} record(s) changed upstream (${profile.enrich.invalidateOn}) \u2014 re-enriching those`
|
|
6522
|
+
);
|
|
6523
|
+
}
|
|
6524
|
+
}
|
|
6461
6525
|
enrichResult = await enrichPhase(
|
|
6462
6526
|
api,
|
|
6463
6527
|
db,
|
|
@@ -6476,7 +6540,9 @@ async function syncModel(api, profile, options) {
|
|
|
6476
6540
|
// Pass the full profile so enrich can mirror merged rows into the
|
|
6477
6541
|
// memory store. Without this, memory holds only the pre-enrich
|
|
6478
6542
|
// list payload — see enrich.ts:memoryBatch writeback.
|
|
6479
|
-
profile
|
|
6543
|
+
profile,
|
|
6544
|
+
// Phase 1 honours --no-memory; phase 2 used to ignore it. (#174)
|
|
6545
|
+
writeToMemory: options.toMemory !== false
|
|
6480
6546
|
}
|
|
6481
6547
|
);
|
|
6482
6548
|
enrichedTotal = enrichResult.enriched;
|
|
@@ -7764,7 +7830,12 @@ async function syncProfilesCommand(platform) {
|
|
|
7764
7830
|
model: p10.model,
|
|
7765
7831
|
description: p10.description,
|
|
7766
7832
|
hasEnrich: !!p10.enrich,
|
|
7767
|
-
|
|
7833
|
+
// `identityKey` (singular) is the entity-merge key; `identityKeys`
|
|
7834
|
+
// (plural, #167) are non-merging association keys. Reading only the
|
|
7835
|
+
// singular reported hasIdentityKey:false for gmail/gcal/fathom — the
|
|
7836
|
+
// three profiles the plural form was added for. (#129/#130)
|
|
7837
|
+
hasIdentityKey: !!p10.identityKey || !!p10.identityKeys,
|
|
7838
|
+
hasIdentityKeys: !!p10.identityKeys
|
|
7768
7839
|
})),
|
|
7769
7840
|
total: profiles.length,
|
|
7770
7841
|
_hint: profiles.length > 0 ? "Use a built-in profile: one --agent sync init <platform> <model>" : "No built-in profiles found. Use sync init to auto-infer from action knowledge."
|
|
@@ -7781,7 +7852,7 @@ async function syncProfilesCommand(platform) {
|
|
|
7781
7852
|
for (const p10 of profiles) {
|
|
7782
7853
|
const extras = [];
|
|
7783
7854
|
if (p10.enrich) extras.push("enrich");
|
|
7784
|
-
if (p10.identityKey) extras.push("identity");
|
|
7855
|
+
if (p10.identityKey || p10.identityKeys) extras.push("identity");
|
|
7785
7856
|
if (p10.dateFilter) extras.push("incremental");
|
|
7786
7857
|
const tags = extras.length > 0 ? ` ${pc9.dim(`[${extras.join(", ")}]`)}` : "";
|
|
7787
7858
|
console.log(` ${pc9.bold(`${p10.platform}/${p10.model}`.padEnd(35))} ${p10.description}${tags}`);
|
|
@@ -8093,6 +8164,15 @@ async function syncRunCommand(platform, options) {
|
|
|
8093
8164
|
if (!options.dryRun) {
|
|
8094
8165
|
await maybeAutoMigrateLegacy(platform, toSync.map((p10) => p10.model));
|
|
8095
8166
|
}
|
|
8167
|
+
const profileDrift = toSync.map((p10) => ({ model: p10.model, missing: findMissingBuiltinCapabilities(platform, p10.model, p10) })).filter((d) => d.missing.length > 0);
|
|
8168
|
+
if (profileDrift.length > 0 && !isAgentMode()) {
|
|
8169
|
+
for (const d of profileDrift) {
|
|
8170
|
+
console.log(
|
|
8171
|
+
` ${pc9.yellow("!")} ${platform}/${d.model} is missing ${d.missing.map((f) => pc9.bold(f)).join(", ")} from the current built-in profile.
|
|
8172
|
+
Run ${pc9.bold(`one sync init ${platform} ${d.model}`)} to pick ${d.missing.length > 1 ? "them" : "it"} up.`
|
|
8173
|
+
);
|
|
8174
|
+
}
|
|
8175
|
+
}
|
|
8096
8176
|
const results = [];
|
|
8097
8177
|
for (const profile of toSync) {
|
|
8098
8178
|
try {
|
|
@@ -8116,7 +8196,19 @@ async function syncRunCommand(platform, options) {
|
|
|
8116
8196
|
}
|
|
8117
8197
|
}
|
|
8118
8198
|
if (isAgentMode()) {
|
|
8119
|
-
json({
|
|
8199
|
+
json({
|
|
8200
|
+
platform,
|
|
8201
|
+
results,
|
|
8202
|
+
// Surfaced to agents too: a profile silently missing a capability the
|
|
8203
|
+
// built-in now declares is invisible in the record counts. (#129/#130)
|
|
8204
|
+
...profileDrift.length > 0 ? {
|
|
8205
|
+
profileDrift: profileDrift.map((d) => ({
|
|
8206
|
+
model: d.model,
|
|
8207
|
+
missingFields: d.missing,
|
|
8208
|
+
fix: `one sync init ${platform} ${d.model}`
|
|
8209
|
+
}))
|
|
8210
|
+
} : {}
|
|
8211
|
+
});
|
|
8120
8212
|
return;
|
|
8121
8213
|
}
|
|
8122
8214
|
for (const r of results) {
|
|
@@ -8287,7 +8379,7 @@ async function maybeAutoMigrateLegacy(platform, models) {
|
|
|
8287
8379
|
` detected legacy .one/sync/data/${platform}.db (${dbSize}) \u2014 auto-migrating into memory before sync.
|
|
8288
8380
|
`
|
|
8289
8381
|
);
|
|
8290
|
-
const { memMigrateCommand: memMigrateCommand3 } = await import("./migrate-
|
|
8382
|
+
const { memMigrateCommand: memMigrateCommand3 } = await import("./migrate-UGDDHWGY.js");
|
|
8291
8383
|
await memMigrateCommand3({ platform, yes: true });
|
|
8292
8384
|
return;
|
|
8293
8385
|
}
|
|
@@ -8296,7 +8388,7 @@ async function maybeAutoMigrateLegacy(platform, models) {
|
|
|
8296
8388
|
initialValue: true
|
|
8297
8389
|
});
|
|
8298
8390
|
if (p7.isCancel(shouldMigrate) || !shouldMigrate) return;
|
|
8299
|
-
const { memMigrateCommand: memMigrateCommand2 } = await import("./migrate-
|
|
8391
|
+
const { memMigrateCommand: memMigrateCommand2 } = await import("./migrate-UGDDHWGY.js");
|
|
8300
8392
|
await memMigrateCommand2({ platform, yes: true });
|
|
8301
8393
|
}
|
|
8302
8394
|
async function syncSuggestSearchableCommand(platformModel, options = {}) {
|
|
@@ -8596,7 +8688,7 @@ function registerSyncSubcommands(sync) {
|
|
|
8596
8688
|
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
8689
|
await syncSuggestSearchableCommand(platformModel, options);
|
|
8598
8690
|
});
|
|
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) => {
|
|
8691
|
+
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
8692
|
await syncRunCommand(platform, {
|
|
8601
8693
|
models: options.models?.split(",").map((m) => m.trim()),
|
|
8602
8694
|
since: options.since,
|
|
@@ -8604,6 +8696,7 @@ function registerSyncSubcommands(sync) {
|
|
|
8604
8696
|
maxPages: options.maxPages ? parseInt(options.maxPages, 10) : void 0,
|
|
8605
8697
|
dryRun: options.dryRun,
|
|
8606
8698
|
fullRefresh: options.fullRefresh,
|
|
8699
|
+
reEnrich: options.reEnrich,
|
|
8607
8700
|
// Commander inverts `--no-memory` / `--no-embed` into options.X === false.
|
|
8608
8701
|
toMemory: options.memory !== false,
|
|
8609
8702
|
// Only override when the flag was actually passed — leaving
|
|
@@ -10740,6 +10833,7 @@ When a list endpoint returns lightweight records (e.g. just IDs), add an \`enric
|
|
|
10740
10833
|
"enrich": {
|
|
10741
10834
|
"actionId": "<get-message-action-id>",
|
|
10742
10835
|
"pathVars": { "messageId": "{{id}}" },
|
|
10836
|
+
"invalidateOn": "historyId",
|
|
10743
10837
|
"concurrency": 3,
|
|
10744
10838
|
"delayMs": 200
|
|
10745
10839
|
}
|
|
@@ -10747,7 +10841,8 @@ When a list endpoint returns lightweight records (e.g. just IDs), add an \`enric
|
|
|
10747
10841
|
\`\`\`
|
|
10748
10842
|
|
|
10749
10843
|
- \`pathVars\` / \`queryParams\` / \`body\` support \`{{field}}\` interpolation from the list record
|
|
10750
|
-
- \`
|
|
10844
|
+
- \`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
|
|
10845
|
+
- \`concurrency\` controls parallel detail requests per page (default: 5, lower = safer for rate limits)
|
|
10751
10846
|
- \`delayMs\` is the pause between batches (default: 200ms)
|
|
10752
10847
|
- \`resultsPath\` extracts a sub-object from the detail response before merging
|
|
10753
10848
|
- \`merge: false\` replaces the record entirely instead of deep-merging
|
|
@@ -10803,7 +10898,19 @@ So for a row the mirror reports as already enriched, Phase 1 writes **non-author
|
|
|
10803
10898
|
|
|
10804
10899
|
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
10900
|
|
|
10806
|
-
**
|
|
10901
|
+
**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:
|
|
10902
|
+
|
|
10903
|
+
- **\`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.
|
|
10904
|
+
|
|
10905
|
+
\`\`\`json
|
|
10906
|
+
{ "enrich": { "actionId": "...", "invalidateOn": "historyId" } }
|
|
10907
|
+
\`\`\`
|
|
10908
|
+
|
|
10909
|
+
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.
|
|
10910
|
+
|
|
10911
|
+
- **\`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.
|
|
10912
|
+
|
|
10913
|
+
Deleting the mirror (\`.one/sync/data/<platform>.db\`) is no longer necessary.
|
|
10807
10914
|
|
|
10808
10915
|
## Cross-Platform Identity
|
|
10809
10916
|
|
|
@@ -10828,6 +10935,10 @@ Two ways to tag a record with a cross-platform identifier (e.g. email), dependin
|
|
|
10828
10935
|
|
|
10829
10936
|
Each \`path\` supports \`[]\` wildcards (one key per element) and a \`[name=From]\` equality filter (e.g. Gmail \`messages[].payload.headers[name=From].value\`). \`email\`-prefixed values are email-extracted, so display-name headers (\`"Jane <jane@acme.com>"\`) and comma-lists normalize cleanly. Values are lowercased/trimmed/deduped. \`sync test\` previews how many identity keys each record resolves.
|
|
10830
10937
|
|
|
10938
|
+
The built-in \`gmail/gmailThreads\` profile collects From/To/**Cc/Bcc**. Gmail only returns a \`Bcc\` header on messages the authenticated user sent \u2014 it is stripped for recipients \u2014 so Bcc keys appear on your own sent threads and nowhere else.
|
|
10939
|
+
|
|
10940
|
+
**Upgrading an existing profile.** \`sync run\` reads only your on-disk profile at \`.one/sync/profiles/<platform>_<model>.json\` \u2014 it never merges built-in updates, so a profile created before a capability shipped will not have it. \`sync run\` now warns when the shipped built-in declares \`identityKeys\`, \`identityKey\`, \`enrich\`, \`dateFilter\`, or \`memory\` that your copy lacks, and \`--agent\` output carries a \`profileDrift\` array with the same information. Run \`one sync init <platform> <model>\` to pick the fields up (it patches, preserving your edits).
|
|
10941
|
+
|
|
10831
10942
|
Query everything sharing an identity key, grouped by type:
|
|
10832
10943
|
|
|
10833
10944
|
\`\`\`bash
|
|
@@ -10909,7 +11020,7 @@ Every \`sync X\` command is also exposed as \`mem sync X\` \u2014 same handlers,
|
|
|
10909
11020
|
| \`sync init <plat> <model>\` | Create/patch profile (seeds from built-in, auto-tests) |
|
|
10910
11021
|
| \`sync test <plat>/<model>\` | Validate profile. \`--show-searchable\` previews embedded text across 5 samples with per-path hit rates |
|
|
10911
11022
|
| \`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\`) |
|
|
11023
|
+
| \`sync run <platform>\` | Sync data (\`--full-refresh\`, \`--since\`, \`--dry-run\`, \`--no-memory\`, \`--re-enrich\`) |
|
|
10913
11024
|
| \`sync query <plat>/<model>\` | Query memory with \`--where\` (dotted paths), \`--after/before\` |
|
|
10914
11025
|
| \`sync schema <plat>/<model>\` | Inspect the JSON structure of synced records (field paths, types, examples) \u2014 run before writing \`--where\` / query paths |
|
|
10915
11026
|
| \`sync search "<query>"\` | Hybrid FTS + semantic across all synced data |
|
package/package.json
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"identityKeys": [
|
|
19
19
|
{ "prefix": "email", "path": "messages[].payload.headers[name=From].value" },
|
|
20
20
|
{ "prefix": "email", "path": "messages[].payload.headers[name=To].value" },
|
|
21
|
-
{ "prefix": "email", "path": "messages[].payload.headers[name=Cc].value" }
|
|
21
|
+
{ "prefix": "email", "path": "messages[].payload.headers[name=Cc].value" },
|
|
22
|
+
{ "prefix": "email", "path": "messages[].payload.headers[name=Bcc].value" }
|
|
22
23
|
],
|
|
23
24
|
"enrich": {
|
|
24
25
|
"actionId": "conn_mod_def::GJ3ok0Eq0R8::AAzgZVLqTg2iBuITKpJLZg",
|
package/skills/one/SKILL.md
CHANGED
|
@@ -311,6 +311,8 @@ Without declared paths, the default walker concatenates every string in the reco
|
|
|
311
311
|
|
|
312
312
|
**Connections are late-bound** — profiles use `"connection": { "platform": "<name>" }`, not literal `connectionKey` strings. The key is resolved at sync time, so `one add <platform>` (re-auth) doesn't break the profile. For multi-account platforms, add `"tag": "<connection-tag>"` to disambiguate, and create the tagged connection with `one add <platform> --tag <name>`. Don't hardcode connection keys in profiles.
|
|
313
313
|
|
|
314
|
+
**Installed profiles do not auto-update.** `sync run` reads only `.one/sync/profiles/<platform>_<model>.json` and never merges the shipped built-in, so a profile created before a capability shipped silently lacks it — a pre-#167 gmail profile writes zero identity keys, forever, with no change in record counts. `sync run` warns when the built-in declares `identityKeys` / `identityKey` / `enrich` / `dateFilter` / `memory` that your copy lacks (agent mode: a `profileDrift` array). Fix with `one sync init <platform> <model>`, which patches rather than overwrites.
|
|
315
|
+
|
|
314
316
|
**Cross-platform identity on a profile.** Two separate fields, and picking the wrong one silently mangles data:
|
|
315
317
|
|
|
316
318
|
- `"identityKey": "properties.email"` — singular. "This record IS this entity." One dot-path; the value lands in `keys[]` and MERGES records for the same entity across platforms (HubSpot + Attio for one person collapse into a single record).
|
|
@@ -318,7 +320,7 @@ Without declared paths, the default walker concatenates every string in the reco
|
|
|
318
320
|
|
|
319
321
|
Both are queryable with `one --agent mem find-by-key <prefix>:<value>`.
|
|
320
322
|
|
|
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,
|
|
323
|
+
**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
324
|
|
|
323
325
|
**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
326
|
|