engrm 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +33 -10
- package/dist/hooks/session-start.js +291 -0
- package/dist/hooks/stop.js +18 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1075,21 +1075,44 @@ async function openBrowser(url) {
|
|
|
1075
1075
|
return false;
|
|
1076
1076
|
}
|
|
1077
1077
|
}
|
|
1078
|
+
var PAGE_STYLE = `
|
|
1079
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
1080
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#06060e;color:#fff;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
|
|
1081
|
+
body::before{content:'';position:fixed;inset:0;z-index:-1;background:radial-gradient(ellipse at 30% 20%,rgba(0,212,255,0.06) 0%,transparent 50%),radial-gradient(ellipse at 70% 80%,rgba(123,44,191,0.06) 0%,transparent 50%)}
|
|
1082
|
+
.card{width:100%;max-width:440px;padding:48px 40px;border-radius:16px;border:1px solid rgba(255,255,255,0.07);background:rgba(255,255,255,0.02);backdrop-filter:blur(20px);text-align:center}
|
|
1083
|
+
.logo{display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:28px}
|
|
1084
|
+
.logo span{font-size:1.3rem;font-weight:700}
|
|
1085
|
+
.icon{width:56px;height:56px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 20px}
|
|
1086
|
+
.icon.success{background:rgba(16,185,129,0.12);border:2px solid rgba(16,185,129,0.3)}
|
|
1087
|
+
.icon.error{background:rgba(239,68,68,0.12);border:2px solid rgba(239,68,68,0.3)}
|
|
1088
|
+
h1{font-size:1.4rem;font-weight:700;margin-bottom:8px}
|
|
1089
|
+
p{color:rgba(255,255,255,0.6);font-size:0.9rem;line-height:1.5}
|
|
1090
|
+
.hint{margin-top:24px;padding:12px 16px;font-size:0.82rem;color:rgba(255,255,255,0.4);background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.06);border-radius:8px}
|
|
1091
|
+
a{color:#00d4ff;text-decoration:none}
|
|
1092
|
+
`;
|
|
1093
|
+
var LOGO_SVG = `<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:32px;height:32px"><rect width="40" height="40" rx="10" fill="#0c0c1e"/><rect x="0.5" y="0.5" width="39" height="39" rx="9.5" stroke="rgba(255,255,255,0.08)"/><path d="M12 12h10M12 20h8M12 28h10M12 12v16" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><circle cx="13" cy="7" r="3" fill="#00d4ff"/><circle cx="33" cy="33" r="3" fill="#7b2cbf"/></svg>`;
|
|
1078
1094
|
function successPage() {
|
|
1079
1095
|
return `<!DOCTYPE html>
|
|
1080
|
-
<html><head><title>Engrm</title>
|
|
1081
|
-
<
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
<
|
|
1096
|
+
<html><head><title>Engrm — Connected</title><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect rx='6' width='32' height='32' fill='%230c0c1e'/><text x='7' y='24' font-family='system-ui' font-weight='700' font-size='22' fill='white'>E</text><circle cx='10' cy='6' r='3' fill='%2300d4ff'/><circle cx='26' cy='26' r='3' fill='%237b2cbf'/></svg>"><style>${PAGE_STYLE}</style></head>
|
|
1097
|
+
<body><div class="card">
|
|
1098
|
+
<div class="logo">${LOGO_SVG}<span>Engrm</span></div>
|
|
1099
|
+
<div class="icon success"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round"><path d="M20 6L9 17l-5-5"/></svg></div>
|
|
1100
|
+
<h1>Connected!</h1>
|
|
1101
|
+
<p>Your device is now linked to your Engrm account. Memory will sync automatically across all your devices.</p>
|
|
1102
|
+
<div class="hint">You can close this tab and return to the terminal. Your next Claude Code session will have memory.</div>
|
|
1103
|
+
</div></body></html>`;
|
|
1085
1104
|
}
|
|
1086
1105
|
function errorPage(message) {
|
|
1106
|
+
const safeMessage = message.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
1087
1107
|
return `<!DOCTYPE html>
|
|
1088
|
-
<html><head><title>Engrm</title>
|
|
1089
|
-
<
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
<
|
|
1108
|
+
<html><head><title>Engrm — Error</title><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect rx='6' width='32' height='32' fill='%230c0c1e'/><text x='7' y='24' font-family='system-ui' font-weight='700' font-size='22' fill='white'>E</text><circle cx='10' cy='6' r='3' fill='%2300d4ff'/><circle cx='26' cy='26' r='3' fill='%237b2cbf'/></svg>"><style>${PAGE_STYLE}</style></head>
|
|
1109
|
+
<body><div class="card">
|
|
1110
|
+
<div class="logo">${LOGO_SVG}<span>Engrm</span></div>
|
|
1111
|
+
<div class="icon error"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg></div>
|
|
1112
|
+
<h1>Authorization Failed</h1>
|
|
1113
|
+
<p>${safeMessage}</p>
|
|
1114
|
+
<div class="hint">Try running <code style="color:#00d4ff">engrm init</code> again, or use <code style="color:#00d4ff">engrm init --token=cmt_xxx</code> with a provisioning token from <a href="https://engrm.dev">engrm.dev</a>.</div>
|
|
1115
|
+
</div></body></html>`;
|
|
1093
1116
|
}
|
|
1094
1117
|
|
|
1095
1118
|
// src/register.ts
|
|
@@ -1409,6 +1409,288 @@ function recommendPacks(stacks, installedPacks) {
|
|
|
1409
1409
|
return recommendations;
|
|
1410
1410
|
}
|
|
1411
1411
|
|
|
1412
|
+
// src/sync/auth.ts
|
|
1413
|
+
function getApiKey(config) {
|
|
1414
|
+
const envKey = process.env.ENGRM_TOKEN;
|
|
1415
|
+
if (envKey && envKey.startsWith("cvk_"))
|
|
1416
|
+
return envKey;
|
|
1417
|
+
if (config.candengo_api_key && config.candengo_api_key.length > 0) {
|
|
1418
|
+
return config.candengo_api_key;
|
|
1419
|
+
}
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
function getBaseUrl(config) {
|
|
1423
|
+
if (config.candengo_url && config.candengo_url.length > 0) {
|
|
1424
|
+
return config.candengo_url;
|
|
1425
|
+
}
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
function buildSourceId(config, localId, type = "obs") {
|
|
1429
|
+
return `${config.user_id}-${config.device_id}-${type}-${localId}`;
|
|
1430
|
+
}
|
|
1431
|
+
function parseSourceId(sourceId) {
|
|
1432
|
+
const obsIndex = sourceId.lastIndexOf("-obs-");
|
|
1433
|
+
if (obsIndex === -1)
|
|
1434
|
+
return null;
|
|
1435
|
+
const prefix = sourceId.slice(0, obsIndex);
|
|
1436
|
+
const localIdStr = sourceId.slice(obsIndex + 5);
|
|
1437
|
+
const localId = parseInt(localIdStr, 10);
|
|
1438
|
+
if (isNaN(localId))
|
|
1439
|
+
return null;
|
|
1440
|
+
const firstDash = prefix.indexOf("-");
|
|
1441
|
+
if (firstDash === -1)
|
|
1442
|
+
return null;
|
|
1443
|
+
return {
|
|
1444
|
+
userId: prefix.slice(0, firstDash),
|
|
1445
|
+
deviceId: prefix.slice(firstDash + 1),
|
|
1446
|
+
localId
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// src/embeddings/embedder.ts
|
|
1451
|
+
var _available = null;
|
|
1452
|
+
var _pipeline = null;
|
|
1453
|
+
var MODEL_NAME = "Xenova/all-MiniLM-L6-v2";
|
|
1454
|
+
async function embedText(text) {
|
|
1455
|
+
const pipe = await getPipeline();
|
|
1456
|
+
if (!pipe)
|
|
1457
|
+
return null;
|
|
1458
|
+
try {
|
|
1459
|
+
const output = await pipe(text, { pooling: "mean", normalize: true });
|
|
1460
|
+
return new Float32Array(output.data);
|
|
1461
|
+
} catch {
|
|
1462
|
+
return null;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
function composeEmbeddingText(obs) {
|
|
1466
|
+
const parts = [obs.title];
|
|
1467
|
+
if (obs.narrative)
|
|
1468
|
+
parts.push(obs.narrative);
|
|
1469
|
+
if (obs.facts) {
|
|
1470
|
+
try {
|
|
1471
|
+
const facts = JSON.parse(obs.facts);
|
|
1472
|
+
if (Array.isArray(facts) && facts.length > 0) {
|
|
1473
|
+
parts.push(facts.map((f) => `- ${f}`).join(`
|
|
1474
|
+
`));
|
|
1475
|
+
}
|
|
1476
|
+
} catch {
|
|
1477
|
+
parts.push(obs.facts);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
if (obs.concepts) {
|
|
1481
|
+
try {
|
|
1482
|
+
const concepts = JSON.parse(obs.concepts);
|
|
1483
|
+
if (Array.isArray(concepts) && concepts.length > 0) {
|
|
1484
|
+
parts.push(concepts.join(", "));
|
|
1485
|
+
}
|
|
1486
|
+
} catch {}
|
|
1487
|
+
}
|
|
1488
|
+
return parts.join(`
|
|
1489
|
+
|
|
1490
|
+
`);
|
|
1491
|
+
}
|
|
1492
|
+
async function getPipeline() {
|
|
1493
|
+
if (_pipeline)
|
|
1494
|
+
return _pipeline;
|
|
1495
|
+
if (_available === false)
|
|
1496
|
+
return null;
|
|
1497
|
+
try {
|
|
1498
|
+
const { pipeline } = await import("@xenova/transformers");
|
|
1499
|
+
_pipeline = await pipeline("feature-extraction", MODEL_NAME);
|
|
1500
|
+
_available = true;
|
|
1501
|
+
return _pipeline;
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
_available = false;
|
|
1504
|
+
console.error(`[engrm] Local embedding model unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// src/sync/pull.ts
|
|
1510
|
+
var PULL_CURSOR_KEY = "pull_cursor";
|
|
1511
|
+
var MAX_PAGES = 20;
|
|
1512
|
+
async function pullFromVector(db, client, config, limit = 50) {
|
|
1513
|
+
let cursor = db.getSyncState(PULL_CURSOR_KEY) ?? undefined;
|
|
1514
|
+
let totalReceived = 0;
|
|
1515
|
+
let totalMerged = 0;
|
|
1516
|
+
let totalSkipped = 0;
|
|
1517
|
+
for (let page = 0;page < MAX_PAGES; page++) {
|
|
1518
|
+
const response = await client.pullChanges(cursor, limit);
|
|
1519
|
+
const { merged, skipped } = mergeChanges(db, config, response.changes);
|
|
1520
|
+
totalReceived += response.changes.length;
|
|
1521
|
+
totalMerged += merged;
|
|
1522
|
+
totalSkipped += skipped;
|
|
1523
|
+
if (response.cursor) {
|
|
1524
|
+
db.setSyncState(PULL_CURSOR_KEY, response.cursor);
|
|
1525
|
+
cursor = response.cursor;
|
|
1526
|
+
}
|
|
1527
|
+
if (!response.has_more || response.changes.length === 0)
|
|
1528
|
+
break;
|
|
1529
|
+
}
|
|
1530
|
+
return { received: totalReceived, merged: totalMerged, skipped: totalSkipped };
|
|
1531
|
+
}
|
|
1532
|
+
function mergeChanges(db, config, changes) {
|
|
1533
|
+
let merged = 0;
|
|
1534
|
+
let skipped = 0;
|
|
1535
|
+
for (const change of changes) {
|
|
1536
|
+
const parsed = parseSourceId(change.source_id);
|
|
1537
|
+
if (parsed && parsed.deviceId === config.device_id) {
|
|
1538
|
+
skipped++;
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
const existing = db.db.query("SELECT id FROM observations WHERE remote_source_id = ?").get(change.source_id);
|
|
1542
|
+
if (existing) {
|
|
1543
|
+
skipped++;
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
const projectCanonical = change.metadata?.project_canonical ?? null;
|
|
1547
|
+
if (!projectCanonical) {
|
|
1548
|
+
skipped++;
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
let project = db.getProjectByCanonicalId(projectCanonical);
|
|
1552
|
+
if (!project) {
|
|
1553
|
+
project = db.upsertProject({
|
|
1554
|
+
canonical_id: projectCanonical,
|
|
1555
|
+
name: change.metadata?.project_name ?? projectCanonical.split("/").pop() ?? "unknown"
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
const obs = db.insertObservation({
|
|
1559
|
+
session_id: change.metadata?.session_id ?? null,
|
|
1560
|
+
project_id: project.id,
|
|
1561
|
+
type: change.metadata?.type ?? "discovery",
|
|
1562
|
+
title: change.metadata?.title ?? change.content.split(`
|
|
1563
|
+
`)[0] ?? "Untitled",
|
|
1564
|
+
narrative: extractNarrative(change.content),
|
|
1565
|
+
facts: change.metadata?.facts ? JSON.stringify(change.metadata.facts) : null,
|
|
1566
|
+
concepts: change.metadata?.concepts ? JSON.stringify(change.metadata.concepts) : null,
|
|
1567
|
+
quality: change.metadata?.quality ?? 0.5,
|
|
1568
|
+
lifecycle: "active",
|
|
1569
|
+
sensitivity: "shared",
|
|
1570
|
+
user_id: change.metadata?.user_id ?? "unknown",
|
|
1571
|
+
device_id: change.metadata?.device_id ?? "unknown",
|
|
1572
|
+
agent: change.metadata?.agent ?? "unknown"
|
|
1573
|
+
});
|
|
1574
|
+
db.db.query("UPDATE observations SET remote_source_id = ? WHERE id = ?").run(change.source_id, obs.id);
|
|
1575
|
+
if (db.vecAvailable) {
|
|
1576
|
+
embedAndInsert(db, obs).catch(() => {});
|
|
1577
|
+
}
|
|
1578
|
+
merged++;
|
|
1579
|
+
}
|
|
1580
|
+
return { merged, skipped };
|
|
1581
|
+
}
|
|
1582
|
+
async function embedAndInsert(db, obs) {
|
|
1583
|
+
const text = composeEmbeddingText(obs);
|
|
1584
|
+
const embedding = await embedText(text);
|
|
1585
|
+
if (embedding)
|
|
1586
|
+
db.vecInsert(obs.id, embedding);
|
|
1587
|
+
}
|
|
1588
|
+
function extractNarrative(content) {
|
|
1589
|
+
const lines = content.split(`
|
|
1590
|
+
`);
|
|
1591
|
+
if (lines.length <= 1)
|
|
1592
|
+
return null;
|
|
1593
|
+
const narrative = lines.slice(1).join(`
|
|
1594
|
+
`).trim();
|
|
1595
|
+
return narrative.length > 0 ? narrative : null;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/sync/client.ts
|
|
1599
|
+
class VectorClient {
|
|
1600
|
+
baseUrl;
|
|
1601
|
+
apiKey;
|
|
1602
|
+
siteId;
|
|
1603
|
+
namespace;
|
|
1604
|
+
constructor(config) {
|
|
1605
|
+
const baseUrl = getBaseUrl(config);
|
|
1606
|
+
const apiKey = getApiKey(config);
|
|
1607
|
+
if (!baseUrl || !apiKey) {
|
|
1608
|
+
throw new Error("VectorClient requires candengo_url and candengo_api_key");
|
|
1609
|
+
}
|
|
1610
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
1611
|
+
this.apiKey = apiKey;
|
|
1612
|
+
this.siteId = config.site_id;
|
|
1613
|
+
this.namespace = config.namespace;
|
|
1614
|
+
}
|
|
1615
|
+
static isConfigured(config) {
|
|
1616
|
+
return getApiKey(config) !== null && getBaseUrl(config) !== null;
|
|
1617
|
+
}
|
|
1618
|
+
async ingest(doc) {
|
|
1619
|
+
await this.request("POST", "/v1/ingest", doc);
|
|
1620
|
+
}
|
|
1621
|
+
async batchIngest(docs) {
|
|
1622
|
+
if (docs.length === 0)
|
|
1623
|
+
return;
|
|
1624
|
+
await this.request("POST", "/v1/ingest/batch", { documents: docs });
|
|
1625
|
+
}
|
|
1626
|
+
async search(query, metadataFilter, limit = 10) {
|
|
1627
|
+
const body = { query, limit };
|
|
1628
|
+
if (metadataFilter) {
|
|
1629
|
+
body.metadata_filter = metadataFilter;
|
|
1630
|
+
}
|
|
1631
|
+
return this.request("POST", "/v1/search", body);
|
|
1632
|
+
}
|
|
1633
|
+
async deleteBySourceIds(sourceIds) {
|
|
1634
|
+
if (sourceIds.length === 0)
|
|
1635
|
+
return;
|
|
1636
|
+
await this.request("POST", "/v1/documents/delete", {
|
|
1637
|
+
source_ids: sourceIds
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
async pullChanges(cursor, limit = 50) {
|
|
1641
|
+
const params = new URLSearchParams;
|
|
1642
|
+
if (cursor)
|
|
1643
|
+
params.set("cursor", cursor);
|
|
1644
|
+
params.set("namespace", this.namespace);
|
|
1645
|
+
params.set("limit", String(limit));
|
|
1646
|
+
return this.request("GET", `/v1/sync/changes?${params.toString()}`);
|
|
1647
|
+
}
|
|
1648
|
+
async sendTelemetry(beacon) {
|
|
1649
|
+
await this.request("POST", "/v1/mem/telemetry", beacon);
|
|
1650
|
+
}
|
|
1651
|
+
async health() {
|
|
1652
|
+
try {
|
|
1653
|
+
await this.request("GET", "/health");
|
|
1654
|
+
return true;
|
|
1655
|
+
} catch {
|
|
1656
|
+
return false;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
async request(method, path, body) {
|
|
1660
|
+
const url = `${this.baseUrl}${path}`;
|
|
1661
|
+
const headers = {
|
|
1662
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1663
|
+
"Content-Type": "application/json"
|
|
1664
|
+
};
|
|
1665
|
+
const init = { method, headers };
|
|
1666
|
+
if (body && method !== "GET") {
|
|
1667
|
+
init.body = JSON.stringify(body);
|
|
1668
|
+
}
|
|
1669
|
+
const response = await fetch(url, init);
|
|
1670
|
+
if (!response.ok) {
|
|
1671
|
+
const text = await response.text().catch(() => "");
|
|
1672
|
+
throw new VectorApiError(response.status, text, path);
|
|
1673
|
+
}
|
|
1674
|
+
if (response.status === 204 || response.headers.get("content-length") === "0") {
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
return response.json();
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
class VectorApiError extends Error {
|
|
1682
|
+
status;
|
|
1683
|
+
body;
|
|
1684
|
+
path;
|
|
1685
|
+
constructor(status, body, path) {
|
|
1686
|
+
super(`Vector API error ${status} on ${path}: ${body}`);
|
|
1687
|
+
this.status = status;
|
|
1688
|
+
this.body = body;
|
|
1689
|
+
this.path = path;
|
|
1690
|
+
this.name = "VectorApiError";
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1412
1694
|
// hooks/session-start.ts
|
|
1413
1695
|
async function main() {
|
|
1414
1696
|
const chunks = [];
|
|
@@ -1435,6 +1717,15 @@ async function main() {
|
|
|
1435
1717
|
process.exit(0);
|
|
1436
1718
|
}
|
|
1437
1719
|
try {
|
|
1720
|
+
if (config.sync.enabled && config.candengo_api_key) {
|
|
1721
|
+
try {
|
|
1722
|
+
const client = new VectorClient(config.candengo_url, config.candengo_api_key, config.site_id, config.namespace);
|
|
1723
|
+
const pullResult = await pullFromVector(db, client, config, 50);
|
|
1724
|
+
if (pullResult.merged > 0) {
|
|
1725
|
+
console.error(`Engrm: synced ${pullResult.merged} observation(s) from server`);
|
|
1726
|
+
}
|
|
1727
|
+
} catch {}
|
|
1728
|
+
}
|
|
1438
1729
|
const context = buildSessionContext(db, event.cwd, {
|
|
1439
1730
|
tokenBudget: 800,
|
|
1440
1731
|
scope: config.search.scope
|
package/dist/hooks/stop.js
CHANGED
|
@@ -1074,6 +1074,24 @@ function getBaseUrl(config) {
|
|
|
1074
1074
|
function buildSourceId(config, localId, type = "obs") {
|
|
1075
1075
|
return `${config.user_id}-${config.device_id}-${type}-${localId}`;
|
|
1076
1076
|
}
|
|
1077
|
+
function parseSourceId(sourceId) {
|
|
1078
|
+
const obsIndex = sourceId.lastIndexOf("-obs-");
|
|
1079
|
+
if (obsIndex === -1)
|
|
1080
|
+
return null;
|
|
1081
|
+
const prefix = sourceId.slice(0, obsIndex);
|
|
1082
|
+
const localIdStr = sourceId.slice(obsIndex + 5);
|
|
1083
|
+
const localId = parseInt(localIdStr, 10);
|
|
1084
|
+
if (isNaN(localId))
|
|
1085
|
+
return null;
|
|
1086
|
+
const firstDash = prefix.indexOf("-");
|
|
1087
|
+
if (firstDash === -1)
|
|
1088
|
+
return null;
|
|
1089
|
+
return {
|
|
1090
|
+
userId: prefix.slice(0, firstDash),
|
|
1091
|
+
deviceId: prefix.slice(firstDash + 1),
|
|
1092
|
+
localId
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1077
1095
|
|
|
1078
1096
|
// src/sync/client.ts
|
|
1079
1097
|
class VectorClient {
|