@context-vault/core 2.8.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/LICENSE +21 -0
- package/package.json +52 -0
- package/src/capture/file-ops.js +93 -0
- package/src/capture/formatters.js +29 -0
- package/src/capture/import-pipeline.js +46 -0
- package/src/capture/importers.js +387 -0
- package/src/capture/index.js +199 -0
- package/src/capture/ingest-url.js +252 -0
- package/src/constants.js +8 -0
- package/src/core/categories.js +51 -0
- package/src/core/config.js +127 -0
- package/src/core/files.js +108 -0
- package/src/core/frontmatter.js +120 -0
- package/src/core/status.js +146 -0
- package/src/index/db.js +268 -0
- package/src/index/embed.js +101 -0
- package/src/index/index.js +451 -0
- package/src/index.js +62 -0
- package/src/retrieve/index.js +219 -0
- package/src/server/helpers.js +31 -0
- package/src/server/tools/context-status.js +104 -0
- package/src/server/tools/delete-context.js +53 -0
- package/src/server/tools/get-context.js +235 -0
- package/src/server/tools/ingest-url.js +99 -0
- package/src/server/tools/list-context.js +134 -0
- package/src/server/tools/save-context.js +297 -0
- package/src/server/tools/submit-feedback.js +55 -0
- package/src/server/tools.js +111 -0
- package/src/sync/sync.js +235 -0
package/src/sync/sync.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sync.js — Bidirectional sync protocol
|
|
3
|
+
*
|
|
4
|
+
* v1 design:
|
|
5
|
+
* - Additive-only — no delete propagation (avoids data loss)
|
|
6
|
+
* - Last-write-wins by created_at for conflicts (both have same ID)
|
|
7
|
+
* - Push uses POST /api/vault/import/bulk
|
|
8
|
+
* - Pull uses GET /api/vault/export + local captureAndIndex()
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { captureAndIndex } from "../capture/index.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build a manifest of local vault entries (id → { id, created_at, kind, title }).
|
|
15
|
+
*
|
|
16
|
+
* @param {import('../server/types.js').BaseCtx} ctx
|
|
17
|
+
* @returns {Map<string, { id: string, created_at: string, kind: string, title: string|null }>}
|
|
18
|
+
*/
|
|
19
|
+
export function buildLocalManifest(ctx) {
|
|
20
|
+
const rows = ctx.db
|
|
21
|
+
.prepare(
|
|
22
|
+
"SELECT id, created_at, kind, title FROM vault WHERE (expires_at IS NULL OR expires_at > datetime('now'))",
|
|
23
|
+
)
|
|
24
|
+
.all();
|
|
25
|
+
|
|
26
|
+
const manifest = new Map();
|
|
27
|
+
for (const row of rows) {
|
|
28
|
+
manifest.set(row.id, {
|
|
29
|
+
id: row.id,
|
|
30
|
+
created_at: row.created_at,
|
|
31
|
+
kind: row.kind,
|
|
32
|
+
title: row.title || null,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return manifest;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fetch the remote vault manifest from the hosted API.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} hostedUrl - Base URL of hosted service
|
|
42
|
+
* @param {string} apiKey - Bearer token
|
|
43
|
+
* @returns {Promise<Map<string, { id: string, created_at: string, kind: string, title: string|null }>>}
|
|
44
|
+
*/
|
|
45
|
+
export async function fetchRemoteManifest(hostedUrl, apiKey) {
|
|
46
|
+
const response = await fetch(`${hostedUrl}/api/vault/manifest`, {
|
|
47
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new Error(`Failed to fetch remote manifest: HTTP ${response.status}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const data = await response.json();
|
|
55
|
+
const manifest = new Map();
|
|
56
|
+
|
|
57
|
+
for (const entry of data.entries || []) {
|
|
58
|
+
manifest.set(entry.id, {
|
|
59
|
+
id: entry.id,
|
|
60
|
+
created_at: entry.created_at,
|
|
61
|
+
kind: entry.kind,
|
|
62
|
+
title: entry.title || null,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return manifest;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @typedef {object} SyncPlan
|
|
71
|
+
* @property {string[]} toPush - Entry IDs that exist locally but not remotely
|
|
72
|
+
* @property {string[]} toPull - Entry IDs that exist remotely but not locally
|
|
73
|
+
* @property {string[]} upToDate - Entry IDs that exist in both
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Compute what needs to be pushed/pulled by comparing manifests.
|
|
78
|
+
* Additive-only: entries in both are considered up-to-date.
|
|
79
|
+
*
|
|
80
|
+
* @param {Map<string, object>} local
|
|
81
|
+
* @param {Map<string, object>} remote
|
|
82
|
+
* @returns {SyncPlan}
|
|
83
|
+
*/
|
|
84
|
+
export function computeSyncPlan(local, remote) {
|
|
85
|
+
const toPush = [];
|
|
86
|
+
const toPull = [];
|
|
87
|
+
const upToDate = [];
|
|
88
|
+
|
|
89
|
+
// Find local-only entries
|
|
90
|
+
for (const id of local.keys()) {
|
|
91
|
+
if (remote.has(id)) {
|
|
92
|
+
upToDate.push(id);
|
|
93
|
+
} else {
|
|
94
|
+
toPush.push(id);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Find remote-only entries
|
|
99
|
+
for (const id of remote.keys()) {
|
|
100
|
+
if (!local.has(id)) {
|
|
101
|
+
toPull.push(id);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { toPush, toPull, upToDate };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Execute a sync plan: push local entries to remote, pull remote entries to local.
|
|
110
|
+
*
|
|
111
|
+
* @param {import('../server/types.js').BaseCtx & Partial<import('../server/types.js').HostedCtxExtensions>} ctx
|
|
112
|
+
* @param {{ hostedUrl: string, apiKey: string, plan: SyncPlan, onProgress?: (phase: string, current: number, total: number) => void }} opts
|
|
113
|
+
* @returns {Promise<{ pushed: number, pulled: number, failed: number, errors: string[] }>}
|
|
114
|
+
*/
|
|
115
|
+
export async function executeSync(
|
|
116
|
+
ctx,
|
|
117
|
+
{ hostedUrl, apiKey, plan, onProgress },
|
|
118
|
+
) {
|
|
119
|
+
let pushed = 0;
|
|
120
|
+
let pulled = 0;
|
|
121
|
+
let failed = 0;
|
|
122
|
+
const errors = [];
|
|
123
|
+
|
|
124
|
+
// ── Push: upload local-only entries to remote ──
|
|
125
|
+
if (plan.toPush.length > 0) {
|
|
126
|
+
const BATCH_SIZE = 50;
|
|
127
|
+
const entries = [];
|
|
128
|
+
|
|
129
|
+
// Collect full entry data for push
|
|
130
|
+
for (const id of plan.toPush) {
|
|
131
|
+
const row = ctx.stmts.getEntryById.get(id);
|
|
132
|
+
if (!row) continue;
|
|
133
|
+
|
|
134
|
+
entries.push({
|
|
135
|
+
kind: row.kind,
|
|
136
|
+
title: row.title || null,
|
|
137
|
+
body: row.body,
|
|
138
|
+
tags: row.tags ? JSON.parse(row.tags) : [],
|
|
139
|
+
meta: row.meta ? JSON.parse(row.meta) : undefined,
|
|
140
|
+
source: row.source || "sync-push",
|
|
141
|
+
identity_key: row.identity_key || undefined,
|
|
142
|
+
expires_at: row.expires_at || undefined,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Push in batches
|
|
147
|
+
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
|
148
|
+
const batch = entries.slice(i, i + BATCH_SIZE);
|
|
149
|
+
if (onProgress) onProgress("push", i + batch.length, entries.length);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const response = await fetch(`${hostedUrl}/api/vault/import/bulk`, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: {
|
|
155
|
+
"Content-Type": "application/json",
|
|
156
|
+
Authorization: `Bearer ${apiKey}`,
|
|
157
|
+
},
|
|
158
|
+
body: JSON.stringify({ entries: batch }),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
const errData = await response.json().catch(() => ({}));
|
|
163
|
+
failed += batch.length;
|
|
164
|
+
errors.push(
|
|
165
|
+
`Push batch failed: HTTP ${response.status} — ${errData.error || "unknown"}`,
|
|
166
|
+
);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const result = await response.json();
|
|
171
|
+
pushed += result.imported || 0;
|
|
172
|
+
failed += result.failed || 0;
|
|
173
|
+
if (result.errors?.length) {
|
|
174
|
+
errors.push(...result.errors);
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
failed += batch.length;
|
|
178
|
+
errors.push(`Push batch failed: ${err.message}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Pull: download remote-only entries to local ──
|
|
184
|
+
if (plan.toPull.length > 0) {
|
|
185
|
+
if (onProgress) onProgress("pull", 0, plan.toPull.length);
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const response = await fetch(`${hostedUrl}/api/vault/export`, {
|
|
189
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (!response.ok) {
|
|
193
|
+
throw new Error(`Export failed: HTTP ${response.status}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const data = await response.json();
|
|
197
|
+
const remoteEntries = data.entries || [];
|
|
198
|
+
|
|
199
|
+
// Filter to only pull entries we need
|
|
200
|
+
const pullIds = new Set(plan.toPull);
|
|
201
|
+
const entriesToPull = remoteEntries.filter((e) => pullIds.has(e.id));
|
|
202
|
+
|
|
203
|
+
for (let i = 0; i < entriesToPull.length; i++) {
|
|
204
|
+
const entry = entriesToPull[i];
|
|
205
|
+
if (onProgress) onProgress("pull", i + 1, entriesToPull.length);
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await captureAndIndex(ctx, {
|
|
209
|
+
kind: entry.kind,
|
|
210
|
+
title: entry.title,
|
|
211
|
+
body: entry.body,
|
|
212
|
+
meta:
|
|
213
|
+
entry.meta && typeof entry.meta === "object"
|
|
214
|
+
? entry.meta
|
|
215
|
+
: undefined,
|
|
216
|
+
tags: Array.isArray(entry.tags) ? entry.tags : undefined,
|
|
217
|
+
source: entry.source || "sync-pull",
|
|
218
|
+
identity_key: entry.identity_key,
|
|
219
|
+
expires_at: entry.expires_at,
|
|
220
|
+
userId: ctx.userId || null,
|
|
221
|
+
});
|
|
222
|
+
pulled++;
|
|
223
|
+
} catch (err) {
|
|
224
|
+
failed++;
|
|
225
|
+
errors.push(`Pull "${entry.title || entry.id}": ${err.message}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
failed += plan.toPull.length;
|
|
230
|
+
errors.push(`Pull failed: ${err.message}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { pushed, pulled, failed, errors };
|
|
235
|
+
}
|