@withone/cli 1.50.0 → 1.52.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 +16 -3
- package/dist/{chunk-D6W756DN.js → chunk-K56Z5BTM.js} +38 -3
- package/dist/{chunk-4WWK3GO5.js → chunk-UV4YYDF3.js} +1 -1
- package/dist/{chunk-7RV7T5PX.js → chunk-Z4KAQGIG.js} +139 -15
- package/dist/{chunk-GXOIR3GF.js → chunk-ZOJW6IMU.js} +278 -13
- package/dist/{flow-runner-YDKAYGZW.js → flow-runner-WZIYXW47.js} +1 -1
- package/dist/index.js +416 -256
- package/dist/{migrate-TRUZVAT5.js → migrate-LXDIZMXH.js} +2 -2
- package/dist/{runtime-2PRVL2NO.js → runtime-HDZCBTWH.js} +1 -1
- package/dist/{schema-LCRWSFD5.js → schema-SYMG6SRC.js} +1 -1
- package/dist/{sql-HG7R2JML.js → sql-AZDCMY7G.js} +2 -2
- package/package.json +2 -1
- package/profiles/fathom/meetings.json +4 -0
- package/profiles/gmail/gmailThreads.json +5 -0
- package/profiles/google-calendar/events.json +5 -1
- package/skills/one/SKILL.md +48 -5
- package/skills/one/references/flows.md +5 -3
package/dist/index.js
CHANGED
|
@@ -32,19 +32,22 @@ import {
|
|
|
32
32
|
validateActionInput,
|
|
33
33
|
walkSteps,
|
|
34
34
|
writeCache
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-K56Z5BTM.js";
|
|
36
36
|
import {
|
|
37
37
|
memSqlCommand
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-UV4YYDF3.js";
|
|
39
39
|
import {
|
|
40
|
+
collectIdentityKeys,
|
|
40
41
|
countRecords,
|
|
41
42
|
deleteDatabase,
|
|
42
43
|
deleteRecords,
|
|
43
44
|
dropTable,
|
|
44
45
|
ensureTable,
|
|
45
46
|
evolveSchema,
|
|
47
|
+
extractSearchableFromPaths,
|
|
46
48
|
generateTemplate,
|
|
47
49
|
getDatabaseSize,
|
|
50
|
+
getSearchablePaths,
|
|
48
51
|
isSqliteAvailable,
|
|
49
52
|
listBuiltinProfiles,
|
|
50
53
|
listProfiles,
|
|
@@ -54,13 +57,15 @@ import {
|
|
|
54
57
|
openDatabase,
|
|
55
58
|
readProfile,
|
|
56
59
|
rebuildFtsIndex,
|
|
60
|
+
resolveEntityIdentity,
|
|
57
61
|
resolveProfileConnectionKey,
|
|
58
62
|
sanitizeTableName,
|
|
59
63
|
tableExists,
|
|
60
64
|
upsertRecords,
|
|
61
65
|
writeDraftProfile,
|
|
66
|
+
writePageToMemory,
|
|
62
67
|
writeProfile
|
|
63
|
-
} from "./chunk-
|
|
68
|
+
} from "./chunk-ZOJW6IMU.js";
|
|
64
69
|
import {
|
|
65
70
|
getByDotPath
|
|
66
71
|
} from "./chunk-44CV5IMX.js";
|
|
@@ -93,9 +98,8 @@ import {
|
|
|
93
98
|
getBackendPlugin,
|
|
94
99
|
listBackendPlugins,
|
|
95
100
|
loadBackendFromConfig,
|
|
96
|
-
updateRecord
|
|
97
|
-
|
|
98
|
-
} from "./chunk-7RV7T5PX.js";
|
|
101
|
+
updateRecord
|
|
102
|
+
} from "./chunk-Z4KAQGIG.js";
|
|
99
103
|
import {
|
|
100
104
|
DEFAULT_MEMORY_CONFIG,
|
|
101
105
|
defaultSearchableText,
|
|
@@ -1033,25 +1037,25 @@ async function loginCommand() {
|
|
|
1033
1037
|
let targetScope = "global";
|
|
1034
1038
|
const existingKey = getApiKey();
|
|
1035
1039
|
if (existingKey) {
|
|
1036
|
-
const
|
|
1040
|
+
const pc16 = (await import("picocolors")).default;
|
|
1037
1041
|
const resolved2 = resolveConfig();
|
|
1038
1042
|
const whoami2 = resolved2.config?.whoami;
|
|
1039
1043
|
const env2 = getEnvFromApiKey(existingKey);
|
|
1040
|
-
const envLabel2 = env2 === "test" ?
|
|
1041
|
-
const currentScope = resolved2.scope === "project" ?
|
|
1044
|
+
const envLabel2 = env2 === "test" ? pc16.yellow("test") : pc16.green("live");
|
|
1045
|
+
const currentScope = resolved2.scope === "project" ? pc16.cyan("local config") : pc16.magenta("global config");
|
|
1042
1046
|
const lines = ["You are already logged in.", ""];
|
|
1043
1047
|
if (whoami2) {
|
|
1044
1048
|
const contextParts2 = [];
|
|
1045
1049
|
if (whoami2.organization) contextParts2.push(whoami2.organization.name);
|
|
1046
1050
|
if (whoami2.project) contextParts2.push(whoami2.project.name);
|
|
1047
1051
|
const scopeDisplay2 = contextParts2.length > 0 ? contextParts2.join(" / ") : "Personal";
|
|
1048
|
-
lines.push(`${
|
|
1049
|
-
lines.push(`${whoami2.user.name} ${
|
|
1050
|
-
if (whoami2.organization) lines.push(`${
|
|
1051
|
-
if (whoami2.project) lines.push(`${
|
|
1052
|
+
lines.push(`${pc16.bold(scopeDisplay2)} ${pc16.dim("\xB7")} ${envLabel2}`);
|
|
1053
|
+
lines.push(`${whoami2.user.name} ${pc16.dim(`(${whoami2.user.email})`)}`);
|
|
1054
|
+
if (whoami2.organization) lines.push(`${pc16.dim("Org:")} ${whoami2.organization.name}`);
|
|
1055
|
+
if (whoami2.project) lines.push(`${pc16.dim("Project:")} ${whoami2.project.name}`);
|
|
1052
1056
|
}
|
|
1053
1057
|
lines.push("");
|
|
1054
|
-
lines.push(`${
|
|
1058
|
+
lines.push(`${pc16.dim("Stored in")} ${currentScope}`);
|
|
1055
1059
|
p2.note(lines.join("\n"));
|
|
1056
1060
|
const scopeChoice = await p2.select({
|
|
1057
1061
|
message: "Where would you like to log in?",
|
|
@@ -1074,40 +1078,40 @@ async function loginCommand() {
|
|
|
1074
1078
|
if (resolved.config) {
|
|
1075
1079
|
writeConfig({ ...resolved.config, whoami }, targetScope);
|
|
1076
1080
|
}
|
|
1077
|
-
const
|
|
1081
|
+
const pc15 = (await import("picocolors")).default;
|
|
1078
1082
|
const env = getEnvFromApiKey(apiKey);
|
|
1079
1083
|
const contextParts = [];
|
|
1080
1084
|
if (whoami.organization) contextParts.push(whoami.organization.name);
|
|
1081
1085
|
if (whoami.project) contextParts.push(whoami.project.name);
|
|
1082
1086
|
const scopeDisplay = contextParts.length > 0 ? contextParts.join(" / ") : "Personal";
|
|
1083
|
-
const envLabel = env === "test" ?
|
|
1084
|
-
const configLabel = targetScope === "project" ?
|
|
1087
|
+
const envLabel = env === "test" ? pc15.yellow("test") : pc15.green("live");
|
|
1088
|
+
const configLabel = targetScope === "project" ? pc15.cyan("local config") : pc15.magenta("global config");
|
|
1085
1089
|
const infoLines = [
|
|
1086
|
-
`${
|
|
1087
|
-
`${whoami.user.name} ${
|
|
1090
|
+
`${pc15.bold(scopeDisplay)} ${pc15.dim("\xB7")} ${envLabel}`,
|
|
1091
|
+
`${whoami.user.name} ${pc15.dim(`(${whoami.user.email})`)}`
|
|
1088
1092
|
];
|
|
1089
|
-
if (whoami.organization) infoLines.push(`${
|
|
1090
|
-
if (whoami.project) infoLines.push(`${
|
|
1093
|
+
if (whoami.organization) infoLines.push(`${pc15.dim("Org:")} ${whoami.organization.name}`);
|
|
1094
|
+
if (whoami.project) infoLines.push(`${pc15.dim("Project:")} ${whoami.project.name}`);
|
|
1091
1095
|
infoLines.push("");
|
|
1092
|
-
infoLines.push(`${
|
|
1096
|
+
infoLines.push(`${pc15.dim("Stored in")} ${configLabel}`);
|
|
1093
1097
|
p2.note(infoLines.join("\n"), "Logged in");
|
|
1094
1098
|
console.log();
|
|
1095
|
-
console.log(` ${
|
|
1096
|
-
console.log(` ${
|
|
1099
|
+
console.log(` ${pc15.dim("Next steps:")}`);
|
|
1100
|
+
console.log(` ${pc15.cyan("one add <platform>")} ${pc15.dim("\u2014")} Connect a platform (e.g. gmail, slack, stripe)`);
|
|
1097
1101
|
console.log();
|
|
1098
|
-
console.log(
|
|
1102
|
+
console.log(pc15.bold(" Copy-paste this to your AI agent:"));
|
|
1099
1103
|
console.log();
|
|
1100
|
-
console.log(
|
|
1101
|
-
console.log(
|
|
1102
|
-
console.log(
|
|
1103
|
-
console.log(
|
|
1104
|
-
console.log(
|
|
1105
|
-
console.log(
|
|
1106
|
-
console.log(
|
|
1107
|
-
console.log(
|
|
1108
|
-
console.log(
|
|
1109
|
-
console.log(
|
|
1110
|
-
console.log(
|
|
1104
|
+
console.log(pc15.cyan(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1105
|
+
console.log(pc15.cyan(" \u2502"));
|
|
1106
|
+
console.log(pc15.cyan(" \u2502 The One CLI (`one`) is now installed on this machine. It"));
|
|
1107
|
+
console.log(pc15.cyan(" \u2502 lets you interact with 400+ third-party platforms (Gmail,"));
|
|
1108
|
+
console.log(pc15.cyan(" \u2502 Slack, Shopify, HubSpot, Stripe, GitHub, etc.) through"));
|
|
1109
|
+
console.log(pc15.cyan(" \u2502 their APIs. Whenever you need to interact with any"));
|
|
1110
|
+
console.log(pc15.cyan(" \u2502 third-party platform or external service, use the `one`"));
|
|
1111
|
+
console.log(pc15.cyan(" \u2502 skill. Please save this to your project instructions or"));
|
|
1112
|
+
console.log(pc15.cyan(" \u2502 memory so you remember it in future conversations."));
|
|
1113
|
+
console.log(pc15.cyan(" \u2502"));
|
|
1114
|
+
console.log(pc15.cyan(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1111
1115
|
console.log();
|
|
1112
1116
|
p2.outro("Happy building!");
|
|
1113
1117
|
}
|
|
@@ -5480,152 +5484,6 @@ function runShellHook(command, events) {
|
|
|
5480
5484
|
});
|
|
5481
5485
|
}
|
|
5482
5486
|
|
|
5483
|
-
// src/lib/memory/sync/mem-writer.ts
|
|
5484
|
-
function resolveWildcardPath(root, path12) {
|
|
5485
|
-
const segments = path12.split("[]");
|
|
5486
|
-
if (segments.length === 1) {
|
|
5487
|
-
return [getByDotPath(root, path12)];
|
|
5488
|
-
}
|
|
5489
|
-
const recurse = (value, idx) => {
|
|
5490
|
-
if (value === null || value === void 0) return [];
|
|
5491
|
-
const segment = segments[idx];
|
|
5492
|
-
const head = segment.startsWith(".") ? segment.slice(1) : segment;
|
|
5493
|
-
if (idx === segments.length - 1) {
|
|
5494
|
-
if (!Array.isArray(value)) return [];
|
|
5495
|
-
if (head === "") return value;
|
|
5496
|
-
return value.map((el) => getByDotPath(el, head));
|
|
5497
|
-
}
|
|
5498
|
-
if (!Array.isArray(value)) return [];
|
|
5499
|
-
const results = [];
|
|
5500
|
-
for (const el of value) {
|
|
5501
|
-
const next = head === "" ? el : getByDotPath(el, head);
|
|
5502
|
-
results.push(...recurse(next, idx + 1));
|
|
5503
|
-
}
|
|
5504
|
-
return results;
|
|
5505
|
-
};
|
|
5506
|
-
const firstHead = segments[0];
|
|
5507
|
-
const firstValue = firstHead === "" ? root : getByDotPath(root, firstHead);
|
|
5508
|
-
return recurse(firstValue, 1);
|
|
5509
|
-
}
|
|
5510
|
-
function extractSearchableFromPaths(record, paths) {
|
|
5511
|
-
const parts = [];
|
|
5512
|
-
const perPath = [];
|
|
5513
|
-
for (const path12 of paths) {
|
|
5514
|
-
const values = resolveWildcardPath(record, path12);
|
|
5515
|
-
const collected = [];
|
|
5516
|
-
const absorb = (v) => {
|
|
5517
|
-
if (v === null || v === void 0) return;
|
|
5518
|
-
if (typeof v === "string") {
|
|
5519
|
-
const t = v.trim();
|
|
5520
|
-
if (t) collected.push(t);
|
|
5521
|
-
} else if (typeof v === "number" || typeof v === "boolean") {
|
|
5522
|
-
collected.push(String(v));
|
|
5523
|
-
} else if (Array.isArray(v)) {
|
|
5524
|
-
for (const inner of v) absorb(inner);
|
|
5525
|
-
}
|
|
5526
|
-
};
|
|
5527
|
-
for (const v of values) absorb(v);
|
|
5528
|
-
if (collected.length > 0) {
|
|
5529
|
-
parts.push(...collected);
|
|
5530
|
-
perPath.push({ path: path12, found: true, sample: collected.join(" ").slice(0, 80) });
|
|
5531
|
-
} else {
|
|
5532
|
-
perPath.push({ path: path12, found: false, sample: "" });
|
|
5533
|
-
}
|
|
5534
|
-
}
|
|
5535
|
-
const text5 = parts.join(" ").replace(/\s+/g, " ").trim();
|
|
5536
|
-
return { text: text5, paths: perPath };
|
|
5537
|
-
}
|
|
5538
|
-
function getSearchablePaths(profile) {
|
|
5539
|
-
const paths = profile.memory?.searchable;
|
|
5540
|
-
if (!paths || !Array.isArray(paths) || paths.length === 0) return void 0;
|
|
5541
|
-
return paths;
|
|
5542
|
-
}
|
|
5543
|
-
async function writePageToMemory(profile, records, opts = {}) {
|
|
5544
|
-
const report = {
|
|
5545
|
-
attempted: 0,
|
|
5546
|
-
inserted: 0,
|
|
5547
|
-
updated: 0,
|
|
5548
|
-
skipped: 0,
|
|
5549
|
-
sourceKeysSeen: [],
|
|
5550
|
-
inserts: [],
|
|
5551
|
-
updates: []
|
|
5552
|
-
};
|
|
5553
|
-
const type = `${profile.platform}/${profile.model}`;
|
|
5554
|
-
const identityKey = profile.identityKey;
|
|
5555
|
-
const embedFlag = opts.embedOverride ?? deriveEmbedFlag(profile);
|
|
5556
|
-
const searchablePaths = getSearchablePaths(profile);
|
|
5557
|
-
for (const record of records) {
|
|
5558
|
-
report.attempted++;
|
|
5559
|
-
const externalId = getByDotPath(record, profile.idField);
|
|
5560
|
-
if (externalId === void 0 || externalId === null || externalId === "") {
|
|
5561
|
-
report.skipped++;
|
|
5562
|
-
continue;
|
|
5563
|
-
}
|
|
5564
|
-
if (typeof externalId === "object") {
|
|
5565
|
-
report.skipped++;
|
|
5566
|
-
continue;
|
|
5567
|
-
}
|
|
5568
|
-
const sourceKey = `${type}:${String(externalId)}`;
|
|
5569
|
-
const keys = [sourceKey];
|
|
5570
|
-
if (identityKey) {
|
|
5571
|
-
const raw = getByDotPath(record, identityKey);
|
|
5572
|
-
if (raw !== null && raw !== void 0 && raw !== "") {
|
|
5573
|
-
keys.push(`${deriveIdentityPrefix(identityKey)}:${String(raw).toLowerCase().trim()}`);
|
|
5574
|
-
}
|
|
5575
|
-
}
|
|
5576
|
-
const data = { ...record };
|
|
5577
|
-
for (const k of Object.keys(data)) {
|
|
5578
|
-
if (k.startsWith("_")) delete data[k];
|
|
5579
|
-
}
|
|
5580
|
-
const searchable_text = searchablePaths ? extractSearchableFromPaths(record, searchablePaths).text : void 0;
|
|
5581
|
-
try {
|
|
5582
|
-
const res = await upsertRecord(
|
|
5583
|
-
{
|
|
5584
|
-
type,
|
|
5585
|
-
data,
|
|
5586
|
-
keys,
|
|
5587
|
-
searchable_text,
|
|
5588
|
-
sources: {
|
|
5589
|
-
[sourceKey]: {
|
|
5590
|
-
last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5591
|
-
metadata: { source: profile.platform }
|
|
5592
|
-
}
|
|
5593
|
-
},
|
|
5594
|
-
tags: ["synced", profile.platform],
|
|
5595
|
-
embed: embedFlag
|
|
5596
|
-
},
|
|
5597
|
-
// Sync must REPLACE: if a field vanished at the source (phone
|
|
5598
|
-
// number removed, Gmail thread archived) it must also vanish
|
|
5599
|
-
// from memory. The default merge behaviour is correct for
|
|
5600
|
-
// interactive `mem add` / `mem update`, wrong for sync.
|
|
5601
|
-
{ embed: embedFlag, replace: true }
|
|
5602
|
-
);
|
|
5603
|
-
report.sourceKeysSeen.push(sourceKey);
|
|
5604
|
-
if (res.action === "inserted") {
|
|
5605
|
-
report.inserted++;
|
|
5606
|
-
if (opts.capturePerAction) report.inserts.push(record);
|
|
5607
|
-
} else {
|
|
5608
|
-
report.updated++;
|
|
5609
|
-
if (opts.capturePerAction) report.updates.push(record);
|
|
5610
|
-
}
|
|
5611
|
-
} catch {
|
|
5612
|
-
report.skipped++;
|
|
5613
|
-
}
|
|
5614
|
-
}
|
|
5615
|
-
return report;
|
|
5616
|
-
}
|
|
5617
|
-
function deriveEmbedFlag(profile) {
|
|
5618
|
-
if (profile.memory && typeof profile.memory.embed === "boolean") return profile.memory.embed;
|
|
5619
|
-
return false;
|
|
5620
|
-
}
|
|
5621
|
-
function deriveIdentityPrefix(dotPath) {
|
|
5622
|
-
const lower = dotPath.toLowerCase();
|
|
5623
|
-
if (lower.includes("email")) return "email";
|
|
5624
|
-
if (lower.includes("phone")) return "phone";
|
|
5625
|
-
if (lower.includes("domain")) return "domain";
|
|
5626
|
-
return "id";
|
|
5627
|
-
}
|
|
5628
|
-
|
|
5629
5487
|
// src/lib/memory/sync/transform.ts
|
|
5630
5488
|
import { spawn as spawn3 } from "child_process";
|
|
5631
5489
|
var TRANSFORM_TIMEOUT_MS = 6e4;
|
|
@@ -5789,6 +5647,32 @@ function pickFields(obj, fields) {
|
|
|
5789
5647
|
}
|
|
5790
5648
|
return result;
|
|
5791
5649
|
}
|
|
5650
|
+
function findEnrichedIds(db, model, idField, config2, records, tableCreated) {
|
|
5651
|
+
const enriched = /* @__PURE__ */ new Set();
|
|
5652
|
+
if (!tableCreated || records.length === 0) return enriched;
|
|
5653
|
+
const tsField = config2.timestampField ?? "_enriched_at";
|
|
5654
|
+
const safeTable = model.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
5655
|
+
const safeIdField = idField.replace(/"/g, '""');
|
|
5656
|
+
const safeTsField = tsField.replace(/"/g, '""');
|
|
5657
|
+
try {
|
|
5658
|
+
const cols = db.prepare(`PRAGMA table_info("${safeTable}")`).all().map((c) => c.name);
|
|
5659
|
+
if (!cols.includes(tsField) || !cols.includes(idField)) return enriched;
|
|
5660
|
+
const ids = records.map((r) => r[idField]).filter((id) => typeof id === "string" || typeof id === "number");
|
|
5661
|
+
if (ids.length === 0) return enriched;
|
|
5662
|
+
const CHUNK = 500;
|
|
5663
|
+
for (let i = 0; i < ids.length; i += CHUNK) {
|
|
5664
|
+
const chunk = ids.slice(i, i + CHUNK);
|
|
5665
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
5666
|
+
const rows = db.prepare(
|
|
5667
|
+
`SELECT "${safeIdField}" AS id FROM "${safeTable}" WHERE "${safeTsField}" IS NOT NULL AND "${safeIdField}" IN (${placeholders})`
|
|
5668
|
+
).all(...chunk);
|
|
5669
|
+
for (const row of rows) enriched.add(String(row.id));
|
|
5670
|
+
}
|
|
5671
|
+
} catch {
|
|
5672
|
+
return /* @__PURE__ */ new Set();
|
|
5673
|
+
}
|
|
5674
|
+
return enriched;
|
|
5675
|
+
}
|
|
5792
5676
|
async function enrichPhase(api, db, config2, model, idField, connectionKey, platform, ctx = {}) {
|
|
5793
5677
|
const startTime = Date.now();
|
|
5794
5678
|
const tsField = config2.timestampField ?? "_enriched_at";
|
|
@@ -5888,9 +5772,9 @@ async function enrichPhase(api, db, config2, model, idField, connectionKey, plat
|
|
|
5888
5772
|
stripExcludedFields(merged, ctx.exclude);
|
|
5889
5773
|
}
|
|
5890
5774
|
if (ctx.identityKey) {
|
|
5891
|
-
const
|
|
5892
|
-
if (
|
|
5893
|
-
merged._identity =
|
|
5775
|
+
const identity = resolveEntityIdentity(merged, ctx.identityKey);
|
|
5776
|
+
if (identity?.value) {
|
|
5777
|
+
merged._identity = identity.value;
|
|
5894
5778
|
}
|
|
5895
5779
|
}
|
|
5896
5780
|
const setClauses = [];
|
|
@@ -6169,7 +6053,7 @@ async function syncModel(api, profile, options) {
|
|
|
6169
6053
|
updateModelState(platform, model, { status: "failed", pagesProcessed, lastCursor }),
|
|
6170
6054
|
(async () => {
|
|
6171
6055
|
try {
|
|
6172
|
-
const { getBackend: getBackend2 } = await import("./runtime-
|
|
6056
|
+
const { getBackend: getBackend2 } = await import("./runtime-HDZCBTWH.js");
|
|
6173
6057
|
const backend = await getBackend2();
|
|
6174
6058
|
await Promise.race([
|
|
6175
6059
|
backend.close(),
|
|
@@ -6200,6 +6084,7 @@ async function syncModel(api, profile, options) {
|
|
|
6200
6084
|
let enrichedTotal = 0;
|
|
6201
6085
|
let enrichSkipped = 0;
|
|
6202
6086
|
let enrichRateLimited = 0;
|
|
6087
|
+
let memPreserved = 0;
|
|
6203
6088
|
try {
|
|
6204
6089
|
if (needsSqlite) db = await openDatabase(platform);
|
|
6205
6090
|
let sinceDate = null;
|
|
@@ -6369,9 +6254,9 @@ async function syncModel(api, profile, options) {
|
|
|
6369
6254
|
}
|
|
6370
6255
|
if (profile.identityKey) {
|
|
6371
6256
|
for (const record of records) {
|
|
6372
|
-
const
|
|
6373
|
-
if (
|
|
6374
|
-
record._identity =
|
|
6257
|
+
const identity = resolveEntityIdentity(record, profile.identityKey);
|
|
6258
|
+
if (identity?.value) {
|
|
6259
|
+
record._identity = identity.value;
|
|
6375
6260
|
}
|
|
6376
6261
|
}
|
|
6377
6262
|
}
|
|
@@ -6404,6 +6289,14 @@ async function syncModel(api, profile, options) {
|
|
|
6404
6289
|
let pageReport = null;
|
|
6405
6290
|
if (options.toMemory !== false) {
|
|
6406
6291
|
try {
|
|
6292
|
+
const alreadyEnrichedIds = profile.enrich && db ? findEnrichedIds(
|
|
6293
|
+
db,
|
|
6294
|
+
model,
|
|
6295
|
+
profile.idField,
|
|
6296
|
+
profile.enrich,
|
|
6297
|
+
records,
|
|
6298
|
+
tableCreated
|
|
6299
|
+
) : void 0;
|
|
6407
6300
|
const report = await writePageToMemory(
|
|
6408
6301
|
profile,
|
|
6409
6302
|
records,
|
|
@@ -6412,10 +6305,12 @@ async function syncModel(api, profile, options) {
|
|
|
6412
6305
|
// --embed / --no-embed on `sync run` threads through as a
|
|
6413
6306
|
// per-run override so you can backfill embeddings on a
|
|
6414
6307
|
// first-synced-without-embed platform in one shot.
|
|
6415
|
-
embedOverride: options.embed
|
|
6308
|
+
embedOverride: options.embed,
|
|
6309
|
+
alreadyEnrichedIds
|
|
6416
6310
|
}
|
|
6417
6311
|
);
|
|
6418
6312
|
pageReport = report;
|
|
6313
|
+
memPreserved += report.preserved;
|
|
6419
6314
|
for (const key of report.sourceKeysSeen) seenSourceKeys.add(key);
|
|
6420
6315
|
if (hasHooks && !db) {
|
|
6421
6316
|
inserts = report.inserts;
|
|
@@ -6524,7 +6419,7 @@ async function syncModel(api, profile, options) {
|
|
|
6524
6419
|
db.exec(`DROP TABLE IF EXISTS _seen_ids`);
|
|
6525
6420
|
}
|
|
6526
6421
|
if (options.toMemory !== false) {
|
|
6527
|
-
const backend = await (await import("./runtime-
|
|
6422
|
+
const backend = await (await import("./runtime-HDZCBTWH.js")).getBackend();
|
|
6528
6423
|
const type = `${platform}/${model}`;
|
|
6529
6424
|
const existing = await backend.listKeysByType(type);
|
|
6530
6425
|
const sourcePrefix = `${type}:`;
|
|
@@ -6556,6 +6451,12 @@ async function syncModel(api, profile, options) {
|
|
|
6556
6451
|
process.stderr.write(`Syncing ${platform}/${model}... page ${pagesProcessed} (${totalRecords} records) done
|
|
6557
6452
|
`);
|
|
6558
6453
|
}
|
|
6454
|
+
if (!isAgentMode() && memPreserved > 0) {
|
|
6455
|
+
process.stderr.write(
|
|
6456
|
+
` Kept ${memPreserved} already-enriched record(s) in memory (the list shape would have replaced the enriched payload).
|
|
6457
|
+
`
|
|
6458
|
+
);
|
|
6459
|
+
}
|
|
6559
6460
|
let enrichResult = null;
|
|
6560
6461
|
if (profile.enrich && db && tableCreated && !options.dryRun) {
|
|
6561
6462
|
enrichResult = await enrichPhase(
|
|
@@ -6604,7 +6505,7 @@ async function syncModel(api, profile, options) {
|
|
|
6604
6505
|
let statusCounts;
|
|
6605
6506
|
if (options.toMemory !== false) {
|
|
6606
6507
|
try {
|
|
6607
|
-
const backend = await (await import("./runtime-
|
|
6508
|
+
const backend = await (await import("./runtime-HDZCBTWH.js")).getBackend();
|
|
6608
6509
|
const typeName = `${platform}/${model}`;
|
|
6609
6510
|
const [active, archived] = await Promise.all([
|
|
6610
6511
|
backend.count(typeName, { status: "active" }),
|
|
@@ -6635,7 +6536,12 @@ async function syncModel(api, profile, options) {
|
|
|
6635
6536
|
...reconcileSkipped ? { reconcileSkipped } : {},
|
|
6636
6537
|
...statusCounts ? { statusCounts } : {},
|
|
6637
6538
|
...hasHooks ? { hooksInserted, hooksUpdated } : {},
|
|
6638
|
-
|
|
6539
|
+
// `memPreserved` counts already-enriched rows this run wrote
|
|
6540
|
+
// non-authoritatively (enriched payload kept, list fields refreshed).
|
|
6541
|
+
// Reported to agents too — the human path only gets a stderr line, and
|
|
6542
|
+
// an agent seeing `recordsSynced: 200, enriched: 0` otherwise has no way
|
|
6543
|
+
// to tell "nothing needed enriching" from "enrich is broken".
|
|
6544
|
+
...profile.enrich ? { enriched: enrichedTotal, enrichSkipped, enrichRateLimited, ...memPreserved > 0 ? { memPreserved } : {} } : {}
|
|
6639
6545
|
};
|
|
6640
6546
|
} catch (err) {
|
|
6641
6547
|
const failedCount = db ? countRecords(db, model) : existingState?.totalRecords ?? 0;
|
|
@@ -6675,6 +6581,33 @@ async function syncModel(api, profile, options) {
|
|
|
6675
6581
|
|
|
6676
6582
|
// src/lib/memory/sync/test.ts
|
|
6677
6583
|
var SEARCHABLE_SAMPLE_SIZE = 5;
|
|
6584
|
+
function buildIdentityKeysPreview(samples, profile) {
|
|
6585
|
+
if (!profile.identityKey && !(profile.identityKeys && profile.identityKeys.length > 0)) return void 0;
|
|
6586
|
+
const perRecord = [];
|
|
6587
|
+
const sampleKeys = /* @__PURE__ */ new Set();
|
|
6588
|
+
const fanOutValues = /* @__PURE__ */ new Set();
|
|
6589
|
+
let fanOutCount = 0;
|
|
6590
|
+
for (const rec of samples) {
|
|
6591
|
+
const keys = collectIdentityKeys(rec, profile);
|
|
6592
|
+
perRecord.push(keys.length);
|
|
6593
|
+
for (const k of keys) {
|
|
6594
|
+
if (sampleKeys.size < 8) sampleKeys.add(k);
|
|
6595
|
+
}
|
|
6596
|
+
const identity = resolveEntityIdentity(rec, profile.identityKey);
|
|
6597
|
+
if (identity && identity.values.length > 1) {
|
|
6598
|
+
fanOutCount++;
|
|
6599
|
+
for (const v of identity.values) {
|
|
6600
|
+
if (fanOutValues.size < 4) fanOutValues.add(`${identity.prefix}:${v}`);
|
|
6601
|
+
}
|
|
6602
|
+
}
|
|
6603
|
+
}
|
|
6604
|
+
return {
|
|
6605
|
+
perRecord,
|
|
6606
|
+
sampleKeys: [...sampleKeys],
|
|
6607
|
+
...fanOutCount > 0 ? { entityFanOut: { count: fanOutCount, sampleValues: [...fanOutValues] } } : {},
|
|
6608
|
+
...profile.enrich && (profile.identityKeys?.length ?? 0) > 0 ? { resolvesAfterEnrich: true } : {}
|
|
6609
|
+
};
|
|
6610
|
+
}
|
|
6678
6611
|
function detectColumnType2(value) {
|
|
6679
6612
|
if (value === null || value === void 0) return "TEXT";
|
|
6680
6613
|
if (typeof value === "string") return "TEXT";
|
|
@@ -6881,6 +6814,7 @@ async function testSyncProfile(api, profile) {
|
|
|
6881
6814
|
}));
|
|
6882
6815
|
report.sample = first;
|
|
6883
6816
|
report.samples = records.slice(0, SEARCHABLE_SAMPLE_SIZE);
|
|
6817
|
+
report.identityKeysPreview = buildIdentityKeysPreview(report.samples, profile);
|
|
6884
6818
|
report.ok = checks.every((c) => c.ok);
|
|
6885
6819
|
return report;
|
|
6886
6820
|
}
|
|
@@ -8080,6 +8014,29 @@ async function syncTestCommand(platformModel, options = {}) {
|
|
|
8080
8014
|
console.log(pc9.dim(` ... and ${report.detectedColumns.length - 20} more`));
|
|
8081
8015
|
}
|
|
8082
8016
|
}
|
|
8017
|
+
if (report.identityKeysPreview) {
|
|
8018
|
+
const { perRecord, sampleKeys, entityFanOut, resolvesAfterEnrich } = report.identityKeysPreview;
|
|
8019
|
+
const total = perRecord.reduce((a, b) => a + b, 0);
|
|
8020
|
+
const min = perRecord.length ? Math.min(...perRecord) : 0;
|
|
8021
|
+
const max = perRecord.length ? Math.max(...perRecord) : 0;
|
|
8022
|
+
const mark = total === 0 ? pc9.yellow("~") : pc9.green("\u2713");
|
|
8023
|
+
console.log(`
|
|
8024
|
+
${pc9.bold("Merge + identity keys")} ${pc9.dim(`(cross-platform \u2014 #128)`)}`);
|
|
8025
|
+
console.log(` ${mark} ${perRecord.length} sample${perRecord.length === 1 ? "" : "s"}, ${min}\u2013${max} key${max === 1 ? "" : "s"} per record`);
|
|
8026
|
+
if (sampleKeys.length > 0) {
|
|
8027
|
+
console.log(` ${pc9.dim("e.g.")} ${sampleKeys.slice(0, 8).map((k) => pc9.cyan(k)).join(", ")}`);
|
|
8028
|
+
} else if (resolvesAfterEnrich) {
|
|
8029
|
+
console.log(` ${pc9.dim("note:")} 0 on these list-shape samples \u2014 this profile enriches, and its identityKeys paths resolve in the enrich phase.`);
|
|
8030
|
+
} else {
|
|
8031
|
+
console.log(` ${pc9.yellow("note:")} no identity keys resolved on these samples \u2014 check the identityKey/identityKeys paths.`);
|
|
8032
|
+
}
|
|
8033
|
+
if (entityFanOut) {
|
|
8034
|
+
console.log(` ${pc9.yellow("warn:")} identityKey resolved to MULTIPLE values on ${entityFanOut.count} of ${perRecord.length} samples \u2014 those records get NO merge key.`);
|
|
8035
|
+
console.log(` ${pc9.dim("saw:")} ${entityFanOut.sampleValues.map((v) => pc9.cyan(v)).join(", ")}`);
|
|
8036
|
+
console.log(` ${pc9.dim('A singular identityKey means "this record IS this entity", so a fan-out has no safe answer.')}`);
|
|
8037
|
+
console.log(` ${pc9.dim("Point it at a single-valued path, or move it to identityKeys[] for participant associations.")}`);
|
|
8038
|
+
}
|
|
8039
|
+
}
|
|
8083
8040
|
if (searchablePreview) {
|
|
8084
8041
|
console.log(`
|
|
8085
8042
|
${pc9.bold("Searchable preview")} ${pc9.dim(`(${searchablePreview.mode}, ${searchablePreview.sampledRecords} sample${searchablePreview.sampledRecords === 1 ? "" : "s"})`)}`);
|
|
@@ -8237,7 +8194,7 @@ ${result.total} results`);
|
|
|
8237
8194
|
}
|
|
8238
8195
|
}
|
|
8239
8196
|
async function syncSqlCommand(platformModel, sql) {
|
|
8240
|
-
const { syncSqlCommand: runSyncSql } = await import("./sql-
|
|
8197
|
+
const { syncSqlCommand: runSyncSql } = await import("./sql-AZDCMY7G.js");
|
|
8241
8198
|
await runSyncSql(platformModel, sql);
|
|
8242
8199
|
}
|
|
8243
8200
|
async function syncDeleteCommand(platformModel, options) {
|
|
@@ -8315,7 +8272,7 @@ async function syncDeleteCommand(platformModel, options) {
|
|
|
8315
8272
|
async function maybeAutoMigrateLegacy(platform, models) {
|
|
8316
8273
|
const dbSize = getDatabaseSize(platform);
|
|
8317
8274
|
if (!dbSize || dbSize === "0 B") return;
|
|
8318
|
-
const { getBackend: getBackend2 } = await import("./runtime-
|
|
8275
|
+
const { getBackend: getBackend2 } = await import("./runtime-HDZCBTWH.js");
|
|
8319
8276
|
const backend = await getBackend2();
|
|
8320
8277
|
let memoryHasData = false;
|
|
8321
8278
|
for (const model of models) {
|
|
@@ -8331,7 +8288,7 @@ async function maybeAutoMigrateLegacy(platform, models) {
|
|
|
8331
8288
|
` detected legacy .one/sync/data/${platform}.db (${dbSize}) \u2014 auto-migrating into memory before sync.
|
|
8332
8289
|
`
|
|
8333
8290
|
);
|
|
8334
|
-
const { memMigrateCommand: memMigrateCommand3 } = await import("./migrate-
|
|
8291
|
+
const { memMigrateCommand: memMigrateCommand3 } = await import("./migrate-LXDIZMXH.js");
|
|
8335
8292
|
await memMigrateCommand3({ platform, yes: true });
|
|
8336
8293
|
return;
|
|
8337
8294
|
}
|
|
@@ -8340,7 +8297,7 @@ async function maybeAutoMigrateLegacy(platform, models) {
|
|
|
8340
8297
|
initialValue: true
|
|
8341
8298
|
});
|
|
8342
8299
|
if (p7.isCancel(shouldMigrate) || !shouldMigrate) return;
|
|
8343
|
-
const { memMigrateCommand: memMigrateCommand2 } = await import("./migrate-
|
|
8300
|
+
const { memMigrateCommand: memMigrateCommand2 } = await import("./migrate-LXDIZMXH.js");
|
|
8344
8301
|
await memMigrateCommand2({ platform, yes: true });
|
|
8345
8302
|
}
|
|
8346
8303
|
async function syncSuggestSearchableCommand(platformModel, options = {}) {
|
|
@@ -8401,7 +8358,7 @@ async function syncSuggestSearchableCommand(platformModel, options = {}) {
|
|
|
8401
8358
|
async function syncListCommand(platform) {
|
|
8402
8359
|
const profiles = listProfiles(platform);
|
|
8403
8360
|
const state = await readSyncState();
|
|
8404
|
-
const { getBackend: getBackend2 } = await import("./runtime-
|
|
8361
|
+
const { getBackend: getBackend2 } = await import("./runtime-HDZCBTWH.js");
|
|
8405
8362
|
const backend = await getBackend2();
|
|
8406
8363
|
const syncs = await Promise.all(profiles.map(async (p10) => {
|
|
8407
8364
|
const modelState = state[p10.platform]?.[p10.model];
|
|
@@ -8676,7 +8633,7 @@ function registerSyncSubcommands(sync) {
|
|
|
8676
8633
|
await syncSqlCommand(platformModel, sql);
|
|
8677
8634
|
});
|
|
8678
8635
|
sync.command("schema <platform/model>").description("Inspect the JSON structure of synced records (field paths, types, examples) \u2014 useful before writing `sync sql` queries").action(async (platformModel) => {
|
|
8679
|
-
const { syncSchemaCommand } = await import("./schema-
|
|
8636
|
+
const { syncSchemaCommand } = await import("./schema-SYMG6SRC.js");
|
|
8680
8637
|
await syncSchemaCommand(platformModel);
|
|
8681
8638
|
});
|
|
8682
8639
|
sync.command("delete <platform/model>").description('Delete records from local sync data (e.g. one sync delete notion/pages --id "abc-123")').option("--id <value>", "Delete record by ID").option("--where <conditions>", 'Delete records matching conditions (e.g. "status=archived")').option("--where-sql <predicate>", `Delete using a raw SQL WHERE clause (e.g. "json_extract(data, '$.type') = 'promotion'")`).option("--yes", "Skip confirmation prompt").action(async (platformModel, options) => {
|
|
@@ -9098,6 +9055,7 @@ function parseValue(raw) {
|
|
|
9098
9055
|
}
|
|
9099
9056
|
|
|
9100
9057
|
// src/commands/mem/records.ts
|
|
9058
|
+
import pc10 from "picocolors";
|
|
9101
9059
|
async function memAddCommand(type, dataRaw, flags) {
|
|
9102
9060
|
requireMemoryInit();
|
|
9103
9061
|
const data = parseJsonArg(dataRaw, "data");
|
|
@@ -9122,8 +9080,12 @@ async function memGetCommand(id, flags) {
|
|
|
9122
9080
|
async function memUpdateCommand(id, patchRaw) {
|
|
9123
9081
|
requireMemoryInit();
|
|
9124
9082
|
const patch = parseJsonArg(patchRaw, "patch");
|
|
9125
|
-
|
|
9126
|
-
|
|
9083
|
+
const isPatchObject = !!patch && typeof patch === "object" && !Array.isArray(patch);
|
|
9084
|
+
if (isPatchObject && "keys" in patch) {
|
|
9085
|
+
error("`keys` is not a data field. Use `one mem key <id> --set <csv>` (or --add/--remove) to change a record's merge keys.");
|
|
9086
|
+
}
|
|
9087
|
+
if (isPatchObject && ("identity_keys" in patch || "identityKeys" in patch)) {
|
|
9088
|
+
error("`identity_keys` is not a data field. Participant associations are derived on sync from the profile's `identityKeys` \u2014 edit the sync profile and re-run `one sync run`, then query with `one mem find-by-key <key>`.");
|
|
9127
9089
|
}
|
|
9128
9090
|
const updated = await updateRecord(id, { data: patch });
|
|
9129
9091
|
if (!updated) error(`Record ${id} not found`);
|
|
@@ -9152,7 +9114,7 @@ async function memKeyCommand(id, flags) {
|
|
|
9152
9114
|
if (outcome.status === "not_found") error(`Record ${id} not found`);
|
|
9153
9115
|
if (outcome.status === "conflict") {
|
|
9154
9116
|
error(
|
|
9155
|
-
`Key "${outcome.key}" is already owned by active record ${outcome.recordId} (type ${outcome.recordType}).
|
|
9117
|
+
`Key "${outcome.key}" is already owned by active record ${outcome.recordId} (type ${outcome.recordType}). Merge keys must be unique across active records \u2014 archive or re-key that record first.`
|
|
9156
9118
|
);
|
|
9157
9119
|
}
|
|
9158
9120
|
printRecord(outcome.record);
|
|
@@ -9286,9 +9248,104 @@ async function memFindBySourceCommand(sourceKey) {
|
|
|
9286
9248
|
if (!record) error(`No record owns source "${sourceKey}"`);
|
|
9287
9249
|
printRecord(record);
|
|
9288
9250
|
}
|
|
9251
|
+
function summarizeRecord(r) {
|
|
9252
|
+
const d = r.data ?? {};
|
|
9253
|
+
for (const field of ["title", "subject", "name", "full_name", "display_name", "summary", "headline", "email"]) {
|
|
9254
|
+
const v = d[field];
|
|
9255
|
+
if (typeof v === "string" && v.trim()) return v.trim().slice(0, 80);
|
|
9256
|
+
}
|
|
9257
|
+
return pc10.dim("(untitled)");
|
|
9258
|
+
}
|
|
9259
|
+
function relativeTime(iso) {
|
|
9260
|
+
if (!iso) return "";
|
|
9261
|
+
const then = new Date(iso).getTime();
|
|
9262
|
+
if (Number.isNaN(then)) return "";
|
|
9263
|
+
const s = Math.max(0, Math.floor((Date.now() - then) / 1e3));
|
|
9264
|
+
if (s < 60) return `${s}s ago`;
|
|
9265
|
+
const m = Math.floor(s / 60);
|
|
9266
|
+
if (m < 60) return `${m}m ago`;
|
|
9267
|
+
const h = Math.floor(m / 60);
|
|
9268
|
+
if (h < 24) return `${h}h ago`;
|
|
9269
|
+
const d = Math.floor(h / 24);
|
|
9270
|
+
if (d < 30) return `${d}d ago`;
|
|
9271
|
+
const mo = Math.floor(d / 30);
|
|
9272
|
+
if (mo < 12) return `${mo}mo ago`;
|
|
9273
|
+
return `${Math.floor(mo / 12)}y ago`;
|
|
9274
|
+
}
|
|
9275
|
+
var FIND_BY_KEY_FETCH_CAP = 2e3;
|
|
9276
|
+
function normalizeQueryKey(key) {
|
|
9277
|
+
return key.trim().toLowerCase();
|
|
9278
|
+
}
|
|
9279
|
+
async function memFindByKeyCommand(key, secondKey, flags) {
|
|
9280
|
+
requireMemoryInit();
|
|
9281
|
+
const backend = await getBackend();
|
|
9282
|
+
const rawKeys = secondKey ? [key, secondKey] : [key];
|
|
9283
|
+
const keys = rawKeys.map(normalizeQueryKey);
|
|
9284
|
+
const perType = parsePositiveInt(flags.limit, 10, "--limit");
|
|
9285
|
+
const fetchPage = async (ks) => {
|
|
9286
|
+
const rows = await backend.findByKeys(ks, { type: flags.type, limit: FIND_BY_KEY_FETCH_CAP + 1 });
|
|
9287
|
+
return { records: rows.slice(0, FIND_BY_KEY_FETCH_CAP), truncated: rows.length > FIND_BY_KEY_FETCH_CAP };
|
|
9288
|
+
};
|
|
9289
|
+
let page = await fetchPage(keys);
|
|
9290
|
+
let matchedKeys = keys;
|
|
9291
|
+
if (page.records.length === 0 && keys.some((k, i) => k !== rawKeys[i])) {
|
|
9292
|
+
const verbatim = await fetchPage(rawKeys);
|
|
9293
|
+
if (verbatim.records.length > 0) {
|
|
9294
|
+
page = verbatim;
|
|
9295
|
+
matchedKeys = rawKeys;
|
|
9296
|
+
}
|
|
9297
|
+
}
|
|
9298
|
+
const { records, truncated } = page;
|
|
9299
|
+
const byType = /* @__PURE__ */ new Map();
|
|
9300
|
+
for (const r of records) {
|
|
9301
|
+
const list = byType.get(r.type) ?? [];
|
|
9302
|
+
list.push(r);
|
|
9303
|
+
byType.set(r.type, list);
|
|
9304
|
+
}
|
|
9305
|
+
if (isAgentMode()) {
|
|
9306
|
+
const grouped = {};
|
|
9307
|
+
for (const [type, list] of byType) {
|
|
9308
|
+
grouped[type] = { count: list.length, items: list.slice(0, perType) };
|
|
9309
|
+
}
|
|
9310
|
+
json({
|
|
9311
|
+
keys: matchedKeys,
|
|
9312
|
+
total: records.length,
|
|
9313
|
+
truncated,
|
|
9314
|
+
fetchCap: FIND_BY_KEY_FETCH_CAP,
|
|
9315
|
+
perTypeLimit: perType,
|
|
9316
|
+
byType: grouped
|
|
9317
|
+
});
|
|
9318
|
+
return;
|
|
9319
|
+
}
|
|
9320
|
+
const label = matchedKeys.map((k) => pc10.cyan(k)).join(pc10.dim(" + "));
|
|
9321
|
+
if (records.length === 0) {
|
|
9322
|
+
console.log(`
|
|
9323
|
+
No records linked to ${label}.
|
|
9324
|
+
`);
|
|
9325
|
+
return;
|
|
9326
|
+
}
|
|
9327
|
+
console.log();
|
|
9328
|
+
console.log(` ${label} ${pc10.dim("\u2014")} ${records.length}${truncated ? "+" : ""} record${records.length === 1 ? "" : "s"} across ${byType.size} type${byType.size === 1 ? "" : "s"}`);
|
|
9329
|
+
console.log();
|
|
9330
|
+
const typeWidth = Math.min(30, Math.max(...[...byType.keys()].map((t) => t.length)));
|
|
9331
|
+
for (const [type, list] of byType) {
|
|
9332
|
+
console.log(` ${pc10.bold(type.padEnd(typeWidth))} ${pc10.dim(`${list.length} record${list.length === 1 ? "" : "s"}`)}`);
|
|
9333
|
+
for (const r of list.slice(0, perType)) {
|
|
9334
|
+
console.log(` ${pc10.dim("\xB7")} ${summarizeRecord(r)} ${pc10.dim(relativeTime(r.updated_at))} ${pc10.dim(r.id)}`);
|
|
9335
|
+
}
|
|
9336
|
+
if (list.length > perType) {
|
|
9337
|
+
console.log(` ${pc10.dim(`\u2026 and ${list.length - perType} more (raise --limit)`)}`);
|
|
9338
|
+
}
|
|
9339
|
+
}
|
|
9340
|
+
if (truncated) {
|
|
9341
|
+
console.log();
|
|
9342
|
+
console.log(` ${pc10.yellow("\u26A0")} Stopped at ${FIND_BY_KEY_FETCH_CAP} matches \u2014 there are more, and types sorting after the last one shown are missing entirely. Re-run with --type <type> to see them.`);
|
|
9343
|
+
}
|
|
9344
|
+
console.log();
|
|
9345
|
+
}
|
|
9289
9346
|
|
|
9290
9347
|
// src/commands/mem/doctor.ts
|
|
9291
|
-
import
|
|
9348
|
+
import pc11 from "picocolors";
|
|
9292
9349
|
async function memDoctorCommand() {
|
|
9293
9350
|
const checks = [];
|
|
9294
9351
|
const cfg = getMemoryConfig();
|
|
@@ -9395,19 +9452,19 @@ function emit(checks, capInfo) {
|
|
|
9395
9452
|
return;
|
|
9396
9453
|
}
|
|
9397
9454
|
for (const c of checks) {
|
|
9398
|
-
const mark = c.ok ?
|
|
9399
|
-
const detail = c.detail ?
|
|
9455
|
+
const mark = c.ok ? pc11.green("\u2713") : pc11.red("\u2717");
|
|
9456
|
+
const detail = c.detail ? pc11.dim(` \u2014 ${c.detail}`) : "";
|
|
9400
9457
|
console.log(` ${mark} ${c.name}${detail}`);
|
|
9401
9458
|
}
|
|
9402
9459
|
if (!allOk) {
|
|
9403
|
-
console.log("\n" +
|
|
9460
|
+
console.log("\n" + pc11.yellow("Memory is not fully healthy."));
|
|
9404
9461
|
process.exitCode = 1;
|
|
9405
9462
|
} else {
|
|
9406
|
-
console.log("\n" +
|
|
9463
|
+
console.log("\n" + pc11.green("Memory is healthy."));
|
|
9407
9464
|
}
|
|
9408
9465
|
const line = semanticSearchUpgradeLine({ vectorSearchAvailable: capInfo.vectorSearchAvailable });
|
|
9409
9466
|
if (line) console.log(`
|
|
9410
|
-
${
|
|
9467
|
+
${pc11.dim(line)}`);
|
|
9411
9468
|
}
|
|
9412
9469
|
|
|
9413
9470
|
// src/commands/mem/export.ts
|
|
@@ -9483,6 +9540,14 @@ async function memImportCommand(file) {
|
|
|
9483
9540
|
data: record.data,
|
|
9484
9541
|
tags: record.tags,
|
|
9485
9542
|
keys,
|
|
9543
|
+
// Carry `identity_keys` through (#128). Export writes the whole row, so
|
|
9544
|
+
// the JSONL has them; omitting them here only looked harmless because
|
|
9545
|
+
// re-importing over a store that still holds the record takes the union
|
|
9546
|
+
// branch in mem_upsert_by_keys and preserves what's already there. On the
|
|
9547
|
+
// INSERT path — restore into a wiped store, or a backend swap
|
|
9548
|
+
// (pglite → postgres), which is exactly what export|import is for —
|
|
9549
|
+
// dropping them here loses every participant association permanently.
|
|
9550
|
+
identity_keys: record.identity_keys,
|
|
9486
9551
|
sources: record.sources,
|
|
9487
9552
|
searchable_text: record.searchable_text ?? null,
|
|
9488
9553
|
content_hash: record.content_hash ?? null,
|
|
@@ -9669,10 +9734,10 @@ function registerMemoryCommands(program2) {
|
|
|
9669
9734
|
mem.command("vacuum").description("Run backend maintenance (VACUUM ANALYZE on tables)").action(memVacuumCommand);
|
|
9670
9735
|
mem.command("reindex").description("Backfill embeddings (default) or searchable_text (--searchable)").option("--type <type>", "Restrict to one record type, e.g. attio/attioPeople").option("--model <name>", "Override the embedding model").option("--force", "Re-embed even rows that already have a matching embedding", false).option("--batch <n>", "OpenAI calls per batch (default 50)", "50").option("--limit <n>", "Safety cap on total records to scan (default 100000)").option("--searchable", "Regenerate searchable_text (NULL or stale vs. data) instead of embeddings", false).option("--all", "With --searchable: also rewrite rows whose text is stale, not just NULL", false).action(memReindexCommand);
|
|
9671
9736
|
mem.command("sql <query>").description("Run a read-only SELECT / WITH / EXPLAIN against the memory store").action(memSqlCommand);
|
|
9672
|
-
mem.command("add <type> <data>").description("Add a new memory record (data is JSON)").option("--tags <csv>", "Comma-separated tags").option("--keys <csv>", "Comma-separated keys (prefixed, e.g. email:x@y.com)").option("--weight <n>", "Importance 1-10 (default 5)").option("--embed", "Force embedding for this record").option("--no-embed", "Skip embedding for this record").action(memAddCommand);
|
|
9737
|
+
mem.command("add <type> <data>").description("Add a new memory record (data is JSON)").option("--tags <csv>", "Comma-separated tags").option("--keys <csv>", "Comma-separated merge keys (`keys[]`, prefixed, e.g. email:x@y.com \u2014 unique across active records)").option("--weight <n>", "Importance 1-10 (default 5)").option("--embed", "Force embedding for this record").option("--no-embed", "Skip embedding for this record").action(memAddCommand);
|
|
9673
9738
|
mem.command("get <id>").description("Get a record by id").option("--links", "Include outgoing and incoming links", false).action(memGetCommand);
|
|
9674
9739
|
mem.command("update <id> <patch>").description("Update a record (patch is JSON merged into data; regenerates searchable_text)").action(memUpdateCommand);
|
|
9675
|
-
mem.command("key <id>").description("Manage a record's
|
|
9740
|
+
mem.command("key <id>").description("Manage a record's merge keys (`keys[]` \u2014 unique across active records; these drive upsert-merge). Not participant associations \u2014 those live in `identity_keys[]`, written by a sync profile's `identityKeys`.").option("--add <csv>", "Merge keys to add (comma-separated, e.g. email:x@y.com)").option("--remove <csv>", "Merge keys to remove (comma-separated)").option("--set <csv>", "Replace all merge keys with this comma-separated list").action((id, flags) => memKeyCommand(id, flags));
|
|
9676
9741
|
mem.command("archive <id>").description("Archive a record").option("--reason <text>", "Why it was archived (user_archived | deleted_upstream | superseded | \u2026)").action(memArchiveCommand);
|
|
9677
9742
|
mem.command("weight <id> <n>").description("Set record relevance weight (1-10)").action(memWeightCommand);
|
|
9678
9743
|
mem.command("flush <id>").description("Reset access count for a record").action(memFlushCommand);
|
|
@@ -9684,6 +9749,7 @@ function registerMemoryCommands(program2) {
|
|
|
9684
9749
|
mem.command("linked <id>").description("List linked records").option("--relation <name>", "Filter by relation").option("--direction <dir>", "outgoing | incoming | both", "outgoing").action(memLinkedCommand);
|
|
9685
9750
|
mem.command("sources <id>").description("List source entries on a record").action(memSourcesCommand);
|
|
9686
9751
|
mem.command("find-by-source <sourceKey>").description('Look up the record owning "<system>/<model>:<external_id>"').action(memFindBySourceCommand);
|
|
9752
|
+
mem.command("find-by-key <key> [secondKey]").description("Records carrying a key (e.g. email:jane@acme.com) in either `keys[]` or `identity_keys[]`, grouped by type. Two keys = intersection. Keys are matched case-insensitively.").option("--type <type>", "Only this record type (e.g. gmail/gmailThreads) \u2014 also the way past the 2000-match fetch cap").option("--limit <n>", "Max records shown per type (default 10)").action((key, secondKey, flags) => memFindByKeyCommand(key, secondKey, flags));
|
|
9687
9753
|
registerSyncCommands(mem, {
|
|
9688
9754
|
description: "Sync platform data into memory (alias of `one sync`)"
|
|
9689
9755
|
});
|
|
@@ -9693,7 +9759,7 @@ function registerMemoryCommands(program2) {
|
|
|
9693
9759
|
}
|
|
9694
9760
|
|
|
9695
9761
|
// src/commands/cache.ts
|
|
9696
|
-
import
|
|
9762
|
+
import pc12 from "picocolors";
|
|
9697
9763
|
async function cacheClearCommand(actionId) {
|
|
9698
9764
|
if (actionId) {
|
|
9699
9765
|
const deleted = clearEntry(actionId);
|
|
@@ -9702,9 +9768,9 @@ async function cacheClearCommand(actionId) {
|
|
|
9702
9768
|
return;
|
|
9703
9769
|
}
|
|
9704
9770
|
if (deleted) {
|
|
9705
|
-
console.log(`Cleared cache for ${
|
|
9771
|
+
console.log(`Cleared cache for ${pc12.cyan(actionId)}`);
|
|
9706
9772
|
} else {
|
|
9707
|
-
console.log(`No cache entry found for ${
|
|
9773
|
+
console.log(`No cache entry found for ${pc12.dim(actionId)}`);
|
|
9708
9774
|
}
|
|
9709
9775
|
} else {
|
|
9710
9776
|
const count = clearAll();
|
|
@@ -9741,7 +9807,7 @@ async function cacheListCommand(options) {
|
|
|
9741
9807
|
type: e.type,
|
|
9742
9808
|
key: e.entry.key,
|
|
9743
9809
|
age: formatAge(getAge(e.entry)),
|
|
9744
|
-
status: isFresh(e.entry) ?
|
|
9810
|
+
status: isFresh(e.entry) ? pc12.green("fresh") : pc12.yellow("expired")
|
|
9745
9811
|
}));
|
|
9746
9812
|
printTable(
|
|
9747
9813
|
[
|
|
@@ -9832,13 +9898,13 @@ async function cacheUpdateAllCommand() {
|
|
|
9832
9898
|
if (errors.length > 0) {
|
|
9833
9899
|
console.log();
|
|
9834
9900
|
for (const e of errors) {
|
|
9835
|
-
console.log(` ${
|
|
9901
|
+
console.log(` ${pc12.red("\u2717")} ${e.key}: ${pc12.dim(e.error)}`);
|
|
9836
9902
|
}
|
|
9837
9903
|
}
|
|
9838
9904
|
}
|
|
9839
9905
|
|
|
9840
9906
|
// src/commands/guide.ts
|
|
9841
|
-
import
|
|
9907
|
+
import pc13 from "picocolors";
|
|
9842
9908
|
|
|
9843
9909
|
// src/lib/guide-content.ts
|
|
9844
9910
|
var GUIDE_OVERVIEW = `# One CLI \u2014 Agent Guide
|
|
@@ -9924,7 +9990,7 @@ one --agent flow list # List all workflows
|
|
|
9924
9990
|
- Code steps can reference an external \`.mjs\` module under the flow's \`lib/\` folder (stdin JSON in, stdout JSON out) \u2014 keeps JS out of JSON strings and makes flows shareable
|
|
9925
9991
|
- 12 step types: action, transform, code, condition, loop, parallel, file-read, file-write, while, flow, paginate, bash
|
|
9926
9992
|
- Data wiring via selectors: \`$.input.param\`, \`$.steps.stepId.response\`, \`$.loop.item\`
|
|
9927
|
-
- AI analysis via bash steps: \`claude --print\` with \`
|
|
9993
|
+
- AI analysis via bash steps: \`claude --print --output-format json\` with \`parseEnvelope: true\` (unwraps the CLI envelope + fences + preamble into clean parsed JSON; use instead of \`parseJson\` for claude steps)
|
|
9928
9994
|
- Use \`--allow-bash\` to enable bash steps, \`--mock\` for dry-run with realistic mock responses (uses example data from action schemas)
|
|
9929
9995
|
- Use \`--skip-validation\` to bypass input validation on action steps
|
|
9930
9996
|
- Use \`--output-file <path>\` to stream the full result to a file instead of stdout \u2014 for large results that would otherwise be truncated or hit the JSON string-size limit; stdout (and \`--agent\` output) then carries an \`outputFile\` pointer instead of inline \`steps\`
|
|
@@ -10329,15 +10395,18 @@ one --agent mem get <id> --links
|
|
|
10329
10395
|
# Update (shallow merge into data; regenerates searchable_text from the merged data)
|
|
10330
10396
|
one --agent mem update <id> '{"status":"done"}'
|
|
10331
10397
|
|
|
10332
|
-
# Manage
|
|
10333
|
-
# Unique across ACTIVE records \u2014 a clear error names the record that
|
|
10398
|
+
# Manage MERGE KEYS after creation \u2014 the \`keys[]\` column (a first-class column,
|
|
10399
|
+
# NOT data). Unique across ACTIVE records \u2014 a clear error names the record that
|
|
10400
|
+
# owns a taken key. This is the column that collapses two records into one.
|
|
10334
10401
|
one --agent mem key <id> --add email:new@x.com
|
|
10335
10402
|
one --agent mem key <id> --remove email:old@x.com
|
|
10336
|
-
one --agent mem key <id> --set 'email:a@x.com,phone:+1555' # replace
|
|
10403
|
+
one --agent mem key <id> --set 'email:a@x.com,phone:+1555' # replace ALL merge keys
|
|
10337
10404
|
# NOTE: passing keys to 'mem update' is rejected \u2014 keys are not a data field.
|
|
10405
|
+
# NOTE: 'mem key' WRITES keys[]. To READ across records, use 'mem find-by-key'
|
|
10406
|
+
# (see Identity keys below) \u2014 different command, different column coverage.
|
|
10338
10407
|
|
|
10339
|
-
# Archive / unarchive. Archiving FREES a record's keys: an archived record
|
|
10340
|
-
# longer blocks a new active record from reusing the same key (uniqueness is
|
|
10408
|
+
# Archive / unarchive. Archiving FREES a record's merge keys: an archived record
|
|
10409
|
+
# no longer blocks a new active record from reusing the same key (uniqueness is
|
|
10341
10410
|
# scoped to active records).
|
|
10342
10411
|
one --agent mem archive <id> --reason superseded
|
|
10343
10412
|
|
|
@@ -10379,6 +10448,55 @@ one --agent mem sources <id> # list source entries
|
|
|
10379
10448
|
one --agent mem find-by-source attio/attioPeople:abc-123
|
|
10380
10449
|
\`\`\`
|
|
10381
10450
|
|
|
10451
|
+
\`find-by-source\` answers "which ONE record owns this external id" \u2014 a single record, preferring the active owner. For "everything about this person", use \`find-by-key\` below.
|
|
10452
|
+
|
|
10453
|
+
## Identity keys (cross-platform join)
|
|
10454
|
+
|
|
10455
|
+
Two different columns carry prefixed keys like \`email:jane@acme.com\`, and the difference is the whole point:
|
|
10456
|
+
|
|
10457
|
+
- **\`keys[]\` \u2014 merge keys.** "This record IS this entity." Unique across active records, and \`sync\` merges any incoming record that overlaps an existing one. Written by \`mem add --keys\`, \`mem key\`, and a profile's singular \`identityKey\`.
|
|
10458
|
+
- **\`identity_keys[]\` \u2014 association keys (#128).** "This record INVOLVES these people." Deliberately exempt from both the uniqueness constraint and the overlap-merge, so a Gmail thread with 12 participants stays one thread instead of collapsing into a contact. Written by a profile's plural \`identityKeys\` (see \`one guide sync\`).
|
|
10459
|
+
|
|
10460
|
+
\`mem find-by-key\` queries BOTH columns at once, so one call surfaces the CRM record, the email threads, the calendar events, and the meeting notes for the same person:
|
|
10461
|
+
|
|
10462
|
+
\`\`\`bash
|
|
10463
|
+
one --agent mem find-by-key email:jane@acme.com # every record carrying the key
|
|
10464
|
+
one --agent mem find-by-key email:jane@acme.com --type gmail/gmailThreads # one record type
|
|
10465
|
+
one --agent mem find-by-key email:a@x.com email:b@y.com # intersection \u2014 records with BOTH
|
|
10466
|
+
one --agent mem find-by-key email:jane@acme.com --limit 25 # show more per type (default 10)
|
|
10467
|
+
\`\`\`
|
|
10468
|
+
|
|
10469
|
+
Any prefix works (\`email:\`, \`domain:\`, \`phone:\`, even a raw source key) \u2014 matching is plain array containment over the two columns combined, so a two-key query still matches when one key sits in \`keys[]\` and the other in \`identity_keys[]\`. Only ACTIVE records are returned. Lookup keys are lowercased/trimmed to match how sync writes them, with a verbatim retry for hand-written mixed-case keys, so \`email:Jane@Acme.com\` and \`email:jane@acme.com\` find the same records.
|
|
10470
|
+
|
|
10471
|
+
Agent output is grouped by record type:
|
|
10472
|
+
|
|
10473
|
+
\`\`\`json
|
|
10474
|
+
{
|
|
10475
|
+
"keys": ["email:jane@acme.com"],
|
|
10476
|
+
"total": 13,
|
|
10477
|
+
"truncated": false,
|
|
10478
|
+
"fetchCap": 2000,
|
|
10479
|
+
"perTypeLimit": 10,
|
|
10480
|
+
"byType": {
|
|
10481
|
+
"attio/attioPeople": { "count": 1, "items": [ { "id": "...", "type": "...", "data": {}, "keys": ["attio/attioPeople:J1", "email:jane@acme.com"] } ] },
|
|
10482
|
+
"gmail/gmailThreads": { "count": 12, "items": [] }
|
|
10483
|
+
}
|
|
10484
|
+
}
|
|
10485
|
+
\`\`\`
|
|
10486
|
+
|
|
10487
|
+
\`items\` are whole mem records. \`keys\` and \`identity_keys\` are **omitted, not \`[]\`**, when the record has none \u2014 the contact above matched on \`keys[]\` and carries no \`identity_keys\` field at all. Read them as \`(item.identity_keys ?? [])\`.
|
|
10488
|
+
|
|
10489
|
+
There are TWO independent truncations, and conflating them gives wrong answers:
|
|
10490
|
+
|
|
10491
|
+
- **\`--limit\` (default 10, echoed as \`perTypeLimit\`)** is display only. It slices each \`items\` array; \`total\` and \`count\` stay full match counts. \`count > items.length\` just means "raise \`--limit\` to list the rest".
|
|
10492
|
+
- **\`truncated: true\`** means the query hit the \`fetchCap\` (2000 matches) before it ran out of records. Now \`total\` and every \`count\` are floors, and because rows arrive ordered by type, any type sorting after the cut is absent from \`byType\` altogether. One high-volume type \u2014 your own address on every synced Gmail thread, say \u2014 can consume the whole budget. Re-run with \`--type <type>\` to get an accurate answer for the type you care about; the interactive (non-\`--agent\`) output prints a \`\u26A0\` and a \`2000+\` style count in the same situation.
|
|
10493
|
+
|
|
10494
|
+
\`items\` entries are whole mem records, same fields as \`mem get\`, so no follow-up \`mem get\` is needed.
|
|
10495
|
+
|
|
10496
|
+
\`keys\` echoes the key form that actually matched, which is the normalized one unless the verbatim retry won.
|
|
10497
|
+
|
|
10498
|
+
Do not confuse \`mem key\` with \`mem find-by-key\`: \`mem key\` WRITES \`keys[]\` on a single record (\`--set\` replaces the array outright and can trigger a merge), while \`find-by-key\` is a read-only query across every record.
|
|
10499
|
+
|
|
10382
10500
|
## Sync into memory
|
|
10383
10501
|
|
|
10384
10502
|
\`one mem sync\` is a full alias of \`one sync\` \u2014 same handlers, same options. Use either.
|
|
@@ -10669,9 +10787,30 @@ The transform can be any command: \`jq\`, \`python3\`, a bash script, or \`one f
|
|
|
10669
10787
|
|
|
10670
10788
|
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.
|
|
10671
10789
|
|
|
10790
|
+
**Exception \u2014 plural \`identityKeys\` on an enriching profile.** When a profile declares \`enrich\`, Phase 1 deliberately writes *no opinion* about \`identity_keys[]\` (it leaves whatever is stored untouched) and only Phase 2 sets them. Phase 1 sees the list shape, where participant paths like \`messages[].payload.headers[name=From].value\` don't exist yet \u2014 if it wrote its empty result, every re-sync would erase the participants Phase 2 had resolved. Two consequences worth knowing: a row whose enrichment is skipped or rate-limited keeps \`identity_keys\` \`NULL\` until it enriches, and a participant removed upstream is only cleared once that row re-enriches.
|
|
10791
|
+
|
|
10792
|
+
### Phase 1 never degrades an already-enriched record
|
|
10793
|
+
|
|
10794
|
+
Phase 2 only visits rows \`WHERE _enriched_at IS NULL\`, so it never revisits a row it has already enriched. That makes Phase 1's write dangerous on every run after the first: it holds only the thin list shape, and an authoritative write would replace the enriched payload with it \u2014 permanently, since Phase 2 won't come back to repair it.
|
|
10795
|
+
|
|
10796
|
+
So for a row the mirror reports as already enriched, Phase 1 writes **non-authoritatively**:
|
|
10797
|
+
|
|
10798
|
+
| | Already-enriched row | Not yet enriched |
|
|
10799
|
+
|---|---|---|
|
|
10800
|
+
| \`data\` | merged \u2014 enrich-only fields survive, list fields still refresh | replaced |
|
|
10801
|
+
| \`searchable_text\` / \`content_hash\` | kept (thin text can't overwrite the enriched text) | recomputed |
|
|
10802
|
+
| \`identity_keys[]\` | kept | written |
|
|
10803
|
+
| \`keys[]\`, \`tags\`, \`sources.last_synced_at\`, embeddings | updated normally | updated normally |
|
|
10804
|
+
|
|
10805
|
+
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\`.
|
|
10806
|
+
|
|
10807
|
+
**Known gap:** \`--full-refresh\` does **not** clear \`_enriched_at\`, so it does not re-enrich \u2014 and there is currently no way to re-enrich a record whose upstream detail content changed after its first enrichment. Delete the mirror (\`.one/sync/data/<platform>.db\`) to force a full re-enrich.
|
|
10808
|
+
|
|
10672
10809
|
## Cross-Platform Identity
|
|
10673
10810
|
|
|
10674
|
-
|
|
10811
|
+
Two ways to tag a record with a cross-platform identifier (e.g. email), depending on whether the record IS an entity or INVOLVES many:
|
|
10812
|
+
|
|
10813
|
+
**\`identityKey\` (singular)** \u2014 "this record IS this entity". The value goes into the record's \`keys[]\`, which the store uses to MERGE records for the same entity across platforms (Attio + HubSpot for the same person collapse into one):
|
|
10675
10814
|
|
|
10676
10815
|
\`\`\`json
|
|
10677
10816
|
{"platform": "hubspot", "model": "contacts", "identityKey": "properties.email"}
|
|
@@ -10679,14 +10818,34 @@ Add \`identityKey\` to a sync profile to extract a stable cross-platform identif
|
|
|
10679
10818
|
{"platform": "attio", "model": "attioPeople", "identityKey": "email_addresses[0].email_address"}
|
|
10680
10819
|
\`\`\`
|
|
10681
10820
|
|
|
10682
|
-
|
|
10821
|
+
**\`identityKeys\` (plural, #128)** \u2014 "this record INVOLVES these people". For records with N participants (Gmail thread From/To/Cc, calendar attendees, meeting invitees) where a single key can't capture everyone. These go into a separate \`identity_keys[]\` column that does NOT merge \u2014 so a thread with many participants stays its own record:
|
|
10822
|
+
|
|
10823
|
+
\`\`\`json
|
|
10824
|
+
{"platform": "google-calendar", "model": "events", "identityKeys": [
|
|
10825
|
+
{"prefix": "email", "path": "organizer.email"},
|
|
10826
|
+
{"prefix": "email", "path": "attendees[].email"}
|
|
10827
|
+
]}
|
|
10828
|
+
\`\`\`
|
|
10829
|
+
|
|
10830
|
+
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.
|
|
10831
|
+
|
|
10832
|
+
Query everything sharing an identity key, grouped by type:
|
|
10683
10833
|
|
|
10684
10834
|
\`\`\`bash
|
|
10835
|
+
one --agent mem find-by-key email:jane@acme.com # all records carrying this key
|
|
10836
|
+
one --agent mem find-by-key email:jane@acme.com --type gmail/gmailThreads
|
|
10837
|
+
one --agent mem find-by-key email:jane@acme.com email:bob@acme.com # intersection (both keys)
|
|
10838
|
+
one --agent mem find-by-key email:jane@acme.com --limit 25 # show more records per type
|
|
10839
|
+
# Look up the single record owning a source key:
|
|
10685
10840
|
one --agent mem find-by-source hubspot/contacts:<id>
|
|
10686
|
-
# Or via the dotted --where path
|
|
10687
|
-
|
|
10841
|
+
# Or filter ONE model on the underlying field via the dotted --where path. Note
|
|
10842
|
+
# this is the raw data path, NOT the identity key \u2014 for the hubspot profile above
|
|
10843
|
+
# that's 'properties.email', the same path its identityKey extracts from.
|
|
10844
|
+
one --agent sync query hubspot/contacts --where 'properties.email=jane@acme.com'
|
|
10688
10845
|
\`\`\`
|
|
10689
10846
|
|
|
10847
|
+
\`find-by-key\` spans BOTH columns \u2014 entity keys in \`keys[]\` and association keys in \`identity_keys[]\`. \`--limit\` (default 10) caps only how many records are listed per type; the per-type \`count\` and overall \`total\` are still full match counts, so \`count\` exceeding the listed items is the signal to raise it. A separate \`truncated\` flag reports the very different case where the 2000-match fetch cap was hit and whole types are missing \u2014 see \`one guide memory\` for the full response shape.
|
|
10848
|
+
|
|
10690
10849
|
\`sync sql\` was retired in the unified memory cutover \u2014 a raw-SQL surface can't safely span the embedded Postgres, remote Postgres, and third-party backends without leaking specifics. Use \`mem search\` / \`sync search\` / \`sync query\` with dotted \`--where\` paths instead.
|
|
10691
10850
|
|
|
10692
10851
|
## Exclude Fields
|
|
@@ -10774,7 +10933,8 @@ Every \`sync X\` command is also exposed as \`mem sync X\` \u2014 same handlers,
|
|
|
10774
10933
|
| limitLocation | no | "query" (default) or "body" for POST endpoints |
|
|
10775
10934
|
| enrich | no | Detail endpoint config for record enrichment (actionId, pathVars, concurrency) |
|
|
10776
10935
|
| transform | no | Shell command to transform records (stdin: JSON array, stdout: JSON array) |
|
|
10777
|
-
| identityKey | no |
|
|
10936
|
+
| identityKey | no | "This record IS this entity" \u2014 dot-path to a cross-platform id (e.g. email) \u2192 \`keys[]\` (drives entity merge) |
|
|
10937
|
+
| identityKeys | no | "This record INVOLVES these people" \u2014 \`[{prefix, path}]\` for N participants (#128) \u2192 non-merging \`identity_keys[]\`; query with \`mem find-by-key\` |
|
|
10778
10938
|
| exclude | no | Dot-path fields to strip before storing (e.g. \`["messages[].body"]\`) |
|
|
10779
10939
|
| onInsert/onUpdate/onChange | no | Change hooks (shell command, "log", or flow) |
|
|
10780
10940
|
|
|
@@ -10855,14 +11015,14 @@ async function guideCommand(topic = "all") {
|
|
|
10855
11015
|
json({ topic, title, content, availableTopics });
|
|
10856
11016
|
return;
|
|
10857
11017
|
}
|
|
10858
|
-
intro(
|
|
11018
|
+
intro(pc13.bgCyan(pc13.black(" One Guide ")));
|
|
10859
11019
|
console.log();
|
|
10860
11020
|
console.log(content);
|
|
10861
|
-
console.log(
|
|
11021
|
+
console.log(pc13.dim("\u2500".repeat(60)));
|
|
10862
11022
|
console.log(
|
|
10863
|
-
|
|
11023
|
+
pc13.dim("Available topics: ") + availableTopics.map((t) => pc13.cyan(t.topic)).join(", ")
|
|
10864
11024
|
);
|
|
10865
|
-
console.log(
|
|
11025
|
+
console.log(pc13.dim(`Run ${pc13.cyan("one guide <topic>")} for a specific section.`));
|
|
10866
11026
|
}
|
|
10867
11027
|
|
|
10868
11028
|
// src/lib/platform-meta.ts
|
|
@@ -11185,20 +11345,20 @@ function buildWorkflowIdeas(connections) {
|
|
|
11185
11345
|
// src/commands/logout.ts
|
|
11186
11346
|
import fs12 from "fs";
|
|
11187
11347
|
import * as p9 from "@clack/prompts";
|
|
11188
|
-
function formatWhoami(config2, apiKey,
|
|
11348
|
+
function formatWhoami(config2, apiKey, pc15) {
|
|
11189
11349
|
const whoami = config2.whoami;
|
|
11190
11350
|
const env = getEnvFromApiKey(apiKey);
|
|
11191
|
-
const envLabel = env === "test" ?
|
|
11351
|
+
const envLabel = env === "test" ? pc15.yellow("test") : pc15.green("live");
|
|
11192
11352
|
const lines = [];
|
|
11193
11353
|
if (whoami) {
|
|
11194
11354
|
const contextParts = [];
|
|
11195
11355
|
if (whoami.organization) contextParts.push(whoami.organization.name);
|
|
11196
11356
|
if (whoami.project) contextParts.push(whoami.project.name);
|
|
11197
11357
|
const scopeDisplay = contextParts.length > 0 ? contextParts.join(" / ") : "Personal";
|
|
11198
|
-
lines.push(`${
|
|
11199
|
-
lines.push(`${whoami.user.name} ${
|
|
11358
|
+
lines.push(`${pc15.bold(scopeDisplay)} ${pc15.dim("\xB7")} ${envLabel}`);
|
|
11359
|
+
lines.push(`${whoami.user.name} ${pc15.dim(`(${whoami.user.email})`)}`);
|
|
11200
11360
|
} else {
|
|
11201
|
-
lines.push(`${
|
|
11361
|
+
lines.push(`${pc15.dim("Key:")} ${apiKey.slice(0, 8)}... ${pc15.dim("\xB7")} ${envLabel}`);
|
|
11202
11362
|
}
|
|
11203
11363
|
return lines;
|
|
11204
11364
|
}
|
|
@@ -11227,7 +11387,7 @@ async function logoutCommand() {
|
|
|
11227
11387
|
json({ status: cleared ? "logged_out" : "not_logged_in", message: cleared ? "Credentials cleared." : "No config found." });
|
|
11228
11388
|
return;
|
|
11229
11389
|
}
|
|
11230
|
-
const
|
|
11390
|
+
const pc15 = (await import("picocolors")).default;
|
|
11231
11391
|
const globalConfig = readGlobalConfig();
|
|
11232
11392
|
const projectConfig = readProjectConfig();
|
|
11233
11393
|
const hasGlobal = globalConfig?.apiKey != null;
|
|
@@ -11236,13 +11396,13 @@ async function logoutCommand() {
|
|
|
11236
11396
|
if (hasGlobal && hasProject) {
|
|
11237
11397
|
const infoLines = ["You are logged in with multiple configs.", ""];
|
|
11238
11398
|
if (projectConfig) {
|
|
11239
|
-
infoLines.push(`${
|
|
11240
|
-
infoLines.push(...formatWhoami(projectConfig, projectConfig.apiKey,
|
|
11399
|
+
infoLines.push(`${pc15.cyan("Local config:")}`);
|
|
11400
|
+
infoLines.push(...formatWhoami(projectConfig, projectConfig.apiKey, pc15));
|
|
11241
11401
|
infoLines.push("");
|
|
11242
11402
|
}
|
|
11243
11403
|
if (globalConfig) {
|
|
11244
|
-
infoLines.push(`${
|
|
11245
|
-
infoLines.push(...formatWhoami(globalConfig, globalConfig.apiKey,
|
|
11404
|
+
infoLines.push(`${pc15.magenta("Global config:")}`);
|
|
11405
|
+
infoLines.push(...formatWhoami(globalConfig, globalConfig.apiKey, pc15));
|
|
11246
11406
|
}
|
|
11247
11407
|
p9.note(infoLines.join("\n"));
|
|
11248
11408
|
const choice = await p9.select({
|
|
@@ -11260,14 +11420,14 @@ async function logoutCommand() {
|
|
|
11260
11420
|
targetScope = choice;
|
|
11261
11421
|
} else if (hasProject) {
|
|
11262
11422
|
const infoLines = ["You are logged in.", ""];
|
|
11263
|
-
infoLines.push(`${
|
|
11264
|
-
infoLines.push(...formatWhoami(projectConfig, projectConfig.apiKey,
|
|
11423
|
+
infoLines.push(`${pc15.dim("Stored in")} ${pc15.cyan("local config")}`);
|
|
11424
|
+
infoLines.push(...formatWhoami(projectConfig, projectConfig.apiKey, pc15));
|
|
11265
11425
|
p9.note(infoLines.join("\n"));
|
|
11266
11426
|
targetScope = "project";
|
|
11267
11427
|
} else if (hasGlobal) {
|
|
11268
11428
|
const infoLines = ["You are logged in.", ""];
|
|
11269
|
-
infoLines.push(`${
|
|
11270
|
-
infoLines.push(...formatWhoami(globalConfig, globalConfig.apiKey,
|
|
11429
|
+
infoLines.push(`${pc15.dim("Stored in")} ${pc15.magenta("global config")}`);
|
|
11430
|
+
infoLines.push(...formatWhoami(globalConfig, globalConfig.apiKey, pc15));
|
|
11271
11431
|
p9.note(infoLines.join("\n"));
|
|
11272
11432
|
targetScope = "global";
|
|
11273
11433
|
} else {
|
|
@@ -11304,7 +11464,7 @@ async function logoutCommand() {
|
|
|
11304
11464
|
// src/lib/analytics.ts
|
|
11305
11465
|
import { createRequire as createRequire2 } from "module";
|
|
11306
11466
|
import { randomUUID, createHash } from "crypto";
|
|
11307
|
-
import
|
|
11467
|
+
import pc14 from "picocolors";
|
|
11308
11468
|
var require3 = createRequire2(import.meta.url);
|
|
11309
11469
|
var DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com";
|
|
11310
11470
|
var DEFAULT_POSTHOG_KEY = "phc_a9ok4w0uxiZcVoSWOISIlin85lHMXQD3vWPaYnuRlRV";
|
|
@@ -11550,7 +11710,7 @@ function maybeShowTelemetryNotice() {
|
|
|
11550
11710
|
if (telemetryNoticeShown()) return;
|
|
11551
11711
|
markTelemetryNoticeShown();
|
|
11552
11712
|
process.stderr.write(
|
|
11553
|
-
|
|
11713
|
+
pc14.dim(
|
|
11554
11714
|
"One CLI collects usage analytics (which commands run, linked to your One account) to improve the product.\nNo arguments, inputs, or secrets are ever collected. Opt out anytime with ONE_NO_TELEMETRY=1.\n"
|
|
11555
11715
|
)
|
|
11556
11716
|
);
|
|
@@ -11979,21 +12139,21 @@ program.command("whoami").description("Show the user, organization, and project
|
|
|
11979
12139
|
});
|
|
11980
12140
|
return;
|
|
11981
12141
|
}
|
|
11982
|
-
const
|
|
12142
|
+
const pc15 = (await import("picocolors")).default;
|
|
11983
12143
|
const contextParts = [];
|
|
11984
12144
|
if (whoami.organization) contextParts.push(whoami.organization.name);
|
|
11985
12145
|
if (whoami.project) contextParts.push(whoami.project.name);
|
|
11986
12146
|
const scopeDisplay = contextParts.length > 0 ? contextParts.join(" / ") : "Personal";
|
|
11987
|
-
const envLabel = env === "test" ?
|
|
11988
|
-
const configLabel = configScope === "project" ?
|
|
12147
|
+
const envLabel = env === "test" ? pc15.yellow("test") : pc15.green("live");
|
|
12148
|
+
const configLabel = configScope === "project" ? pc15.cyan("project config") : pc15.magenta("global config");
|
|
11989
12149
|
console.log();
|
|
11990
|
-
console.log(` ${
|
|
11991
|
-
console.log(` ${whoami.user.name} ${
|
|
11992
|
-
if (whoami.organization) console.log(` ${
|
|
11993
|
-
if (whoami.project) console.log(` ${
|
|
12150
|
+
console.log(` ${pc15.bold(scopeDisplay)} ${pc15.dim("\xB7")} ${envLabel}`);
|
|
12151
|
+
console.log(` ${whoami.user.name} ${pc15.dim(`(${whoami.user.email})`)}`);
|
|
12152
|
+
if (whoami.organization) console.log(` ${pc15.dim("Org:")} ${whoami.organization.name} ${pc15.dim(`(${whoami.organization.id})`)}`);
|
|
12153
|
+
if (whoami.project) console.log(` ${pc15.dim("Project:")} ${whoami.project.name} ${pc15.dim(`(${whoami.project.id})`)}`);
|
|
11994
12154
|
console.log();
|
|
11995
|
-
console.log(` ${
|
|
11996
|
-
console.log(` ${
|
|
12155
|
+
console.log(` ${pc15.dim("Using")} ${configLabel}`);
|
|
12156
|
+
console.log(` ${pc15.dim("API:")} ${apiBase}`);
|
|
11997
12157
|
console.log();
|
|
11998
12158
|
});
|
|
11999
12159
|
program.command("add [platform]").description("Shortcut for: connection add").option("--tag <name>", "Tag the new connection (disambiguates multiple connections per platform in sync/flow profiles)").action(async (platform, options) => {
|