@devkitvault/recall 1.2.9 → 1.2.11
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 +27 -25
- package/dist/commands/import.js +155 -45
- package/dist/commands/sync.js +143 -32
- package/dist/lib/config.js +1 -1
- package/dist/lib/local-collections.js +617 -0
- package/dist/lib/local-vault.js +12 -2
- package/dist/lib/shell-history.js +94 -0
- package/package.json +3 -3
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LocalVaultSizeError = exports.LocalVaultLimitError = exports.LOCAL_SIZE_LIMITS = exports.LOCAL_SOFT_CAPS = void 0;
|
|
7
|
+
exports.listLocalGroups = listLocalGroups;
|
|
8
|
+
exports.saveLocalGroup = saveLocalGroup;
|
|
9
|
+
exports.deleteLocalGroup = deleteLocalGroup;
|
|
10
|
+
exports.renameLocalGroup = renameLocalGroup;
|
|
11
|
+
exports.groupsNeedingPush = groupsNeedingPush;
|
|
12
|
+
exports.attachGroupCloudId = attachGroupCloudId;
|
|
13
|
+
exports.mergeCloudGroups = mergeCloudGroups;
|
|
14
|
+
exports.listLocalTemplates = listLocalTemplates;
|
|
15
|
+
exports.saveLocalTemplate = saveLocalTemplate;
|
|
16
|
+
exports.updateLocalTemplate = updateLocalTemplate;
|
|
17
|
+
exports.deleteLocalTemplate = deleteLocalTemplate;
|
|
18
|
+
exports.templatesNeedingPush = templatesNeedingPush;
|
|
19
|
+
exports.attachTemplateCloudId = attachTemplateCloudId;
|
|
20
|
+
exports.mergeCloudTemplates = mergeCloudTemplates;
|
|
21
|
+
exports.listLocalSnippets = listLocalSnippets;
|
|
22
|
+
exports.saveLocalSnippet = saveLocalSnippet;
|
|
23
|
+
exports.updateLocalSnippet = updateLocalSnippet;
|
|
24
|
+
exports.deleteLocalSnippet = deleteLocalSnippet;
|
|
25
|
+
exports.snippetsNeedingPush = snippetsNeedingPush;
|
|
26
|
+
exports.attachSnippetCloudId = attachSnippetCloudId;
|
|
27
|
+
exports.mergeCloudSnippets = mergeCloudSnippets;
|
|
28
|
+
exports.listLocalEnvSets = listLocalEnvSets;
|
|
29
|
+
exports.saveLocalEnvSet = saveLocalEnvSet;
|
|
30
|
+
exports.deleteLocalEnvSet = deleteLocalEnvSet;
|
|
31
|
+
exports.updateLocalEnvSet = updateLocalEnvSet;
|
|
32
|
+
exports.envSetsNeedingPush = envSetsNeedingPush;
|
|
33
|
+
exports.attachEnvCloudId = attachEnvCloudId;
|
|
34
|
+
exports.mergeCloudEnvSets = mergeCloudEnvSets;
|
|
35
|
+
exports.listLocalAliases = listLocalAliases;
|
|
36
|
+
exports.saveLocalAlias = saveLocalAlias;
|
|
37
|
+
exports.deleteLocalAlias = deleteLocalAlias;
|
|
38
|
+
exports.aliasesNeedingPush = aliasesNeedingPush;
|
|
39
|
+
exports.attachAliasCloudId = attachAliasCloudId;
|
|
40
|
+
exports.localCollectionCounts = localCollectionCounts;
|
|
41
|
+
/**
|
|
42
|
+
* Free local collections under ~/.recall/ (separate files).
|
|
43
|
+
* Soft caps + size limits prevent vault abuse; cloud sync still requires Pro/Team
|
|
44
|
+
* and server-side plan gates.
|
|
45
|
+
*/
|
|
46
|
+
const node_crypto_1 = require("node:crypto");
|
|
47
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
48
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
49
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
50
|
+
const VAULT_DIR = node_path_1.default.join(process.env.RECALL_HOME?.trim() || node_os_1.default.homedir(), '.recall');
|
|
51
|
+
exports.LOCAL_SOFT_CAPS = {
|
|
52
|
+
commands: 500,
|
|
53
|
+
groups: 50,
|
|
54
|
+
templates: 50,
|
|
55
|
+
snippets: 50,
|
|
56
|
+
envSets: 50,
|
|
57
|
+
aliases: 100,
|
|
58
|
+
};
|
|
59
|
+
exports.LOCAL_SIZE_LIMITS = {
|
|
60
|
+
commandChars: 10_000,
|
|
61
|
+
templateChars: 10_000,
|
|
62
|
+
snippetContentChars: 100_000,
|
|
63
|
+
envVarsJsonChars: 50_000,
|
|
64
|
+
nameChars: 200,
|
|
65
|
+
aliasChars: 64,
|
|
66
|
+
};
|
|
67
|
+
class LocalVaultLimitError extends Error {
|
|
68
|
+
kind;
|
|
69
|
+
limit;
|
|
70
|
+
current;
|
|
71
|
+
constructor(kind, limit, current) {
|
|
72
|
+
super(`Local ${kind} limit reached (${current}/${limit}). Delete some or upgrade to Pro to sync a subset.`);
|
|
73
|
+
this.kind = kind;
|
|
74
|
+
this.limit = limit;
|
|
75
|
+
this.current = current;
|
|
76
|
+
this.name = 'LocalVaultLimitError';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.LocalVaultLimitError = LocalVaultLimitError;
|
|
80
|
+
class LocalVaultSizeError extends Error {
|
|
81
|
+
constructor(message) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.name = 'LocalVaultSizeError';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.LocalVaultSizeError = LocalVaultSizeError;
|
|
87
|
+
function nowIso() {
|
|
88
|
+
return new Date().toISOString();
|
|
89
|
+
}
|
|
90
|
+
function ensureDir() {
|
|
91
|
+
node_fs_1.default.mkdirSync(VAULT_DIR, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
function collectionPath(file) {
|
|
94
|
+
return node_path_1.default.join(VAULT_DIR, file);
|
|
95
|
+
}
|
|
96
|
+
function readCollection(file) {
|
|
97
|
+
const p = collectionPath(file);
|
|
98
|
+
try {
|
|
99
|
+
if (!node_fs_1.default.existsSync(p)) {
|
|
100
|
+
return { version: 1, updatedAt: nowIso(), syncedAt: null, items: [] };
|
|
101
|
+
}
|
|
102
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(p, 'utf-8'));
|
|
103
|
+
const items = Array.isArray(parsed.items)
|
|
104
|
+
? parsed.items
|
|
105
|
+
: Array.isArray(parsed.groups)
|
|
106
|
+
? parsed.groups
|
|
107
|
+
: Array.isArray(parsed.templates)
|
|
108
|
+
? parsed.templates
|
|
109
|
+
: Array.isArray(parsed.snippets)
|
|
110
|
+
? parsed.snippets
|
|
111
|
+
: Array.isArray(parsed.envSets)
|
|
112
|
+
? parsed.envSets
|
|
113
|
+
: Array.isArray(parsed.aliases)
|
|
114
|
+
? parsed.aliases
|
|
115
|
+
: [];
|
|
116
|
+
return {
|
|
117
|
+
version: 1,
|
|
118
|
+
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : nowIso(),
|
|
119
|
+
syncedAt: typeof parsed.syncedAt === 'string' ? parsed.syncedAt : null,
|
|
120
|
+
items,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return { version: 1, updatedAt: nowIso(), syncedAt: null, items: [] };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function writeCollection(file, key, items, syncedAt) {
|
|
128
|
+
ensureDir();
|
|
129
|
+
const payload = {
|
|
130
|
+
version: 1,
|
|
131
|
+
updatedAt: nowIso(),
|
|
132
|
+
syncedAt: syncedAt ?? null,
|
|
133
|
+
[key]: items,
|
|
134
|
+
items, // dual-write for forward readers
|
|
135
|
+
};
|
|
136
|
+
node_fs_1.default.writeFileSync(collectionPath(file), JSON.stringify(payload, null, 2), { mode: 0o600 });
|
|
137
|
+
}
|
|
138
|
+
function assertUnderCap(kind, current) {
|
|
139
|
+
const limit = exports.LOCAL_SOFT_CAPS[kind];
|
|
140
|
+
if (current >= limit) {
|
|
141
|
+
throw new LocalVaultLimitError(kind, limit, current);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function assertName(name) {
|
|
145
|
+
const t = name.trim();
|
|
146
|
+
if (!t)
|
|
147
|
+
throw new LocalVaultSizeError('Name is required');
|
|
148
|
+
if (t.length > exports.LOCAL_SIZE_LIMITS.nameChars) {
|
|
149
|
+
throw new LocalVaultSizeError(`Name too long (max ${exports.LOCAL_SIZE_LIMITS.nameChars})`);
|
|
150
|
+
}
|
|
151
|
+
return t;
|
|
152
|
+
}
|
|
153
|
+
function listLocalGroups() {
|
|
154
|
+
return readCollection('groups.json').items.sort((a, b) => a.name.localeCompare(b.name));
|
|
155
|
+
}
|
|
156
|
+
function saveLocalGroup(name) {
|
|
157
|
+
const n = assertName(name);
|
|
158
|
+
const file = readCollection('groups.json');
|
|
159
|
+
const existing = file.items.find((g) => g.name.toLowerCase() === n.toLowerCase());
|
|
160
|
+
if (existing) {
|
|
161
|
+
existing.name = n;
|
|
162
|
+
existing.updatedAt = nowIso();
|
|
163
|
+
writeCollection('groups.json', 'groups', file.items, file.syncedAt);
|
|
164
|
+
return existing;
|
|
165
|
+
}
|
|
166
|
+
assertUnderCap('groups', file.items.length);
|
|
167
|
+
const row = {
|
|
168
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
169
|
+
name: n,
|
|
170
|
+
createdAt: nowIso(),
|
|
171
|
+
updatedAt: nowIso(),
|
|
172
|
+
};
|
|
173
|
+
file.items.push(row);
|
|
174
|
+
writeCollection('groups.json', 'groups', file.items, file.syncedAt);
|
|
175
|
+
return row;
|
|
176
|
+
}
|
|
177
|
+
function deleteLocalGroup(id) {
|
|
178
|
+
const file = readCollection('groups.json');
|
|
179
|
+
const idx = file.items.findIndex((g) => g.id === id);
|
|
180
|
+
if (idx < 0)
|
|
181
|
+
return undefined;
|
|
182
|
+
const [removed] = file.items.splice(idx, 1);
|
|
183
|
+
writeCollection('groups.json', 'groups', file.items, file.syncedAt);
|
|
184
|
+
return removed;
|
|
185
|
+
}
|
|
186
|
+
function renameLocalGroup(id, name) {
|
|
187
|
+
const n = assertName(name);
|
|
188
|
+
const file = readCollection('groups.json');
|
|
189
|
+
const row = file.items.find((g) => g.id === id);
|
|
190
|
+
if (!row)
|
|
191
|
+
return undefined;
|
|
192
|
+
row.name = n;
|
|
193
|
+
row.updatedAt = nowIso();
|
|
194
|
+
writeCollection('groups.json', 'groups', file.items, file.syncedAt);
|
|
195
|
+
return row;
|
|
196
|
+
}
|
|
197
|
+
function groupsNeedingPush() {
|
|
198
|
+
return listLocalGroups().filter((g) => !g.cloudId);
|
|
199
|
+
}
|
|
200
|
+
function attachGroupCloudId(localId, cloudId) {
|
|
201
|
+
const file = readCollection('groups.json');
|
|
202
|
+
const row = file.items.find((g) => g.id === localId);
|
|
203
|
+
if (!row)
|
|
204
|
+
return;
|
|
205
|
+
row.cloudId = cloudId;
|
|
206
|
+
row.updatedAt = nowIso();
|
|
207
|
+
writeCollection('groups.json', 'groups', file.items, file.syncedAt);
|
|
208
|
+
}
|
|
209
|
+
function mergeCloudGroups(cloud) {
|
|
210
|
+
const file = readCollection('groups.json');
|
|
211
|
+
const stamp = nowIso();
|
|
212
|
+
let added = 0;
|
|
213
|
+
let updated = 0;
|
|
214
|
+
for (const c of cloud) {
|
|
215
|
+
const byCloud = file.items.find((g) => g.cloudId === c.id);
|
|
216
|
+
const byName = file.items.find((g) => g.name.toLowerCase() === c.name.toLowerCase());
|
|
217
|
+
const target = byCloud ?? byName;
|
|
218
|
+
if (target) {
|
|
219
|
+
target.cloudId = c.id;
|
|
220
|
+
target.name = c.name;
|
|
221
|
+
target.updatedAt = stamp;
|
|
222
|
+
updated++;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
file.items.push({
|
|
226
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
227
|
+
cloudId: c.id,
|
|
228
|
+
name: c.name,
|
|
229
|
+
createdAt: c.createdAt ?? stamp,
|
|
230
|
+
updatedAt: stamp,
|
|
231
|
+
});
|
|
232
|
+
added++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
writeCollection('groups.json', 'groups', file.items, stamp);
|
|
236
|
+
return { added, updated };
|
|
237
|
+
}
|
|
238
|
+
function listLocalTemplates() {
|
|
239
|
+
return readCollection('templates.json').items.sort((a, b) => a.name.localeCompare(b.name));
|
|
240
|
+
}
|
|
241
|
+
function saveLocalTemplate(input) {
|
|
242
|
+
const name = assertName(input.name);
|
|
243
|
+
const template = input.template.trim();
|
|
244
|
+
if (!template)
|
|
245
|
+
throw new LocalVaultSizeError('Template body is required');
|
|
246
|
+
if (template.length > exports.LOCAL_SIZE_LIMITS.templateChars) {
|
|
247
|
+
throw new LocalVaultSizeError(`Template too long (max ${exports.LOCAL_SIZE_LIMITS.templateChars})`);
|
|
248
|
+
}
|
|
249
|
+
const file = readCollection('templates.json');
|
|
250
|
+
const existing = file.items.find((t) => t.name.toLowerCase() === name.toLowerCase());
|
|
251
|
+
if (existing) {
|
|
252
|
+
existing.template = template;
|
|
253
|
+
existing.tags = input.tags?.length ? input.tags : undefined;
|
|
254
|
+
existing.updatedAt = nowIso();
|
|
255
|
+
writeCollection('templates.json', 'templates', file.items, file.syncedAt);
|
|
256
|
+
return existing;
|
|
257
|
+
}
|
|
258
|
+
assertUnderCap('templates', file.items.length);
|
|
259
|
+
const row = {
|
|
260
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
261
|
+
name,
|
|
262
|
+
template,
|
|
263
|
+
tags: input.tags?.length ? input.tags : undefined,
|
|
264
|
+
createdAt: nowIso(),
|
|
265
|
+
updatedAt: nowIso(),
|
|
266
|
+
};
|
|
267
|
+
file.items.push(row);
|
|
268
|
+
writeCollection('templates.json', 'templates', file.items, file.syncedAt);
|
|
269
|
+
return row;
|
|
270
|
+
}
|
|
271
|
+
function updateLocalTemplate(id, patch) {
|
|
272
|
+
const file = readCollection('templates.json');
|
|
273
|
+
const row = file.items.find((t) => t.id === id);
|
|
274
|
+
if (!row)
|
|
275
|
+
return undefined;
|
|
276
|
+
if (patch.name !== undefined)
|
|
277
|
+
row.name = assertName(patch.name);
|
|
278
|
+
if (patch.template !== undefined) {
|
|
279
|
+
const t = patch.template.trim();
|
|
280
|
+
if (t.length > exports.LOCAL_SIZE_LIMITS.templateChars) {
|
|
281
|
+
throw new LocalVaultSizeError(`Template too long (max ${exports.LOCAL_SIZE_LIMITS.templateChars})`);
|
|
282
|
+
}
|
|
283
|
+
row.template = t;
|
|
284
|
+
}
|
|
285
|
+
if (patch.tags !== undefined)
|
|
286
|
+
row.tags = patch.tags.length ? patch.tags : undefined;
|
|
287
|
+
row.updatedAt = nowIso();
|
|
288
|
+
writeCollection('templates.json', 'templates', file.items, file.syncedAt);
|
|
289
|
+
return row;
|
|
290
|
+
}
|
|
291
|
+
function deleteLocalTemplate(id) {
|
|
292
|
+
const file = readCollection('templates.json');
|
|
293
|
+
const idx = file.items.findIndex((t) => t.id === id);
|
|
294
|
+
if (idx < 0)
|
|
295
|
+
return undefined;
|
|
296
|
+
const [removed] = file.items.splice(idx, 1);
|
|
297
|
+
writeCollection('templates.json', 'templates', file.items, file.syncedAt);
|
|
298
|
+
return removed;
|
|
299
|
+
}
|
|
300
|
+
function templatesNeedingPush() {
|
|
301
|
+
return listLocalTemplates().filter((t) => !t.cloudId);
|
|
302
|
+
}
|
|
303
|
+
function attachTemplateCloudId(localId, cloudId) {
|
|
304
|
+
const file = readCollection('templates.json');
|
|
305
|
+
const row = file.items.find((t) => t.id === localId);
|
|
306
|
+
if (!row)
|
|
307
|
+
return;
|
|
308
|
+
row.cloudId = cloudId;
|
|
309
|
+
row.updatedAt = nowIso();
|
|
310
|
+
writeCollection('templates.json', 'templates', file.items, file.syncedAt);
|
|
311
|
+
}
|
|
312
|
+
function mergeCloudTemplates(cloud) {
|
|
313
|
+
const file = readCollection('templates.json');
|
|
314
|
+
const stamp = nowIso();
|
|
315
|
+
let added = 0;
|
|
316
|
+
let updated = 0;
|
|
317
|
+
for (const c of cloud) {
|
|
318
|
+
const byCloud = file.items.find((t) => t.cloudId === c.id);
|
|
319
|
+
const byName = file.items.find((t) => t.name.toLowerCase() === c.name.toLowerCase());
|
|
320
|
+
const target = byCloud ?? byName;
|
|
321
|
+
if (target) {
|
|
322
|
+
target.cloudId = c.id;
|
|
323
|
+
target.name = c.name;
|
|
324
|
+
target.template = c.template;
|
|
325
|
+
target.tags = c.tags?.length ? c.tags : undefined;
|
|
326
|
+
target.updatedAt = stamp;
|
|
327
|
+
updated++;
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
file.items.push({
|
|
331
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
332
|
+
cloudId: c.id,
|
|
333
|
+
name: c.name,
|
|
334
|
+
template: c.template,
|
|
335
|
+
tags: c.tags?.length ? c.tags : undefined,
|
|
336
|
+
createdAt: stamp,
|
|
337
|
+
updatedAt: stamp,
|
|
338
|
+
});
|
|
339
|
+
added++;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
writeCollection('templates.json', 'templates', file.items, stamp);
|
|
343
|
+
return { added, updated };
|
|
344
|
+
}
|
|
345
|
+
function listLocalSnippets() {
|
|
346
|
+
return readCollection('snippets.json').items.sort((a, b) => a.name.localeCompare(b.name));
|
|
347
|
+
}
|
|
348
|
+
function saveLocalSnippet(input) {
|
|
349
|
+
const name = assertName(input.name);
|
|
350
|
+
if (input.content.length > exports.LOCAL_SIZE_LIMITS.snippetContentChars) {
|
|
351
|
+
throw new LocalVaultSizeError(`Snippet too large (max ${exports.LOCAL_SIZE_LIMITS.snippetContentChars} chars)`);
|
|
352
|
+
}
|
|
353
|
+
const file = readCollection('snippets.json');
|
|
354
|
+
const existing = file.items.find((s) => s.name.toLowerCase() === name.toLowerCase());
|
|
355
|
+
if (existing) {
|
|
356
|
+
existing.content = input.content;
|
|
357
|
+
existing.tags = input.tags?.length ? input.tags : undefined;
|
|
358
|
+
existing.updatedAt = nowIso();
|
|
359
|
+
writeCollection('snippets.json', 'snippets', file.items, file.syncedAt);
|
|
360
|
+
return existing;
|
|
361
|
+
}
|
|
362
|
+
assertUnderCap('snippets', file.items.length);
|
|
363
|
+
const row = {
|
|
364
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
365
|
+
name,
|
|
366
|
+
content: input.content,
|
|
367
|
+
tags: input.tags?.length ? input.tags : undefined,
|
|
368
|
+
createdAt: nowIso(),
|
|
369
|
+
updatedAt: nowIso(),
|
|
370
|
+
};
|
|
371
|
+
file.items.push(row);
|
|
372
|
+
writeCollection('snippets.json', 'snippets', file.items, file.syncedAt);
|
|
373
|
+
return row;
|
|
374
|
+
}
|
|
375
|
+
function updateLocalSnippet(id, patch) {
|
|
376
|
+
const file = readCollection('snippets.json');
|
|
377
|
+
const row = file.items.find((s) => s.id === id);
|
|
378
|
+
if (!row)
|
|
379
|
+
return undefined;
|
|
380
|
+
if (patch.name !== undefined)
|
|
381
|
+
row.name = assertName(patch.name);
|
|
382
|
+
if (patch.content !== undefined) {
|
|
383
|
+
if (patch.content.length > exports.LOCAL_SIZE_LIMITS.snippetContentChars) {
|
|
384
|
+
throw new LocalVaultSizeError(`Snippet too large (max ${exports.LOCAL_SIZE_LIMITS.snippetContentChars} chars)`);
|
|
385
|
+
}
|
|
386
|
+
row.content = patch.content;
|
|
387
|
+
}
|
|
388
|
+
if (patch.tags !== undefined)
|
|
389
|
+
row.tags = patch.tags.length ? patch.tags : undefined;
|
|
390
|
+
row.updatedAt = nowIso();
|
|
391
|
+
writeCollection('snippets.json', 'snippets', file.items, file.syncedAt);
|
|
392
|
+
return row;
|
|
393
|
+
}
|
|
394
|
+
function deleteLocalSnippet(id) {
|
|
395
|
+
const file = readCollection('snippets.json');
|
|
396
|
+
const idx = file.items.findIndex((s) => s.id === id);
|
|
397
|
+
if (idx < 0)
|
|
398
|
+
return undefined;
|
|
399
|
+
const [removed] = file.items.splice(idx, 1);
|
|
400
|
+
writeCollection('snippets.json', 'snippets', file.items, file.syncedAt);
|
|
401
|
+
return removed;
|
|
402
|
+
}
|
|
403
|
+
function snippetsNeedingPush() {
|
|
404
|
+
return listLocalSnippets().filter((s) => !s.cloudId);
|
|
405
|
+
}
|
|
406
|
+
function attachSnippetCloudId(localId, cloudId) {
|
|
407
|
+
const file = readCollection('snippets.json');
|
|
408
|
+
const row = file.items.find((s) => s.id === localId);
|
|
409
|
+
if (!row)
|
|
410
|
+
return;
|
|
411
|
+
row.cloudId = cloudId;
|
|
412
|
+
row.updatedAt = nowIso();
|
|
413
|
+
writeCollection('snippets.json', 'snippets', file.items, file.syncedAt);
|
|
414
|
+
}
|
|
415
|
+
function mergeCloudSnippets(cloud) {
|
|
416
|
+
const file = readCollection('snippets.json');
|
|
417
|
+
const stamp = nowIso();
|
|
418
|
+
let added = 0;
|
|
419
|
+
let updated = 0;
|
|
420
|
+
for (const c of cloud) {
|
|
421
|
+
const byCloud = file.items.find((s) => s.cloudId === c.id);
|
|
422
|
+
const byName = file.items.find((s) => s.name.toLowerCase() === c.name.toLowerCase());
|
|
423
|
+
const target = byCloud ?? byName;
|
|
424
|
+
if (target) {
|
|
425
|
+
target.cloudId = c.id;
|
|
426
|
+
target.name = c.name;
|
|
427
|
+
target.content = c.content;
|
|
428
|
+
target.tags = c.tags?.length ? c.tags : undefined;
|
|
429
|
+
target.updatedAt = stamp;
|
|
430
|
+
updated++;
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
file.items.push({
|
|
434
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
435
|
+
cloudId: c.id,
|
|
436
|
+
name: c.name,
|
|
437
|
+
content: c.content,
|
|
438
|
+
tags: c.tags?.length ? c.tags : undefined,
|
|
439
|
+
createdAt: stamp,
|
|
440
|
+
updatedAt: stamp,
|
|
441
|
+
});
|
|
442
|
+
added++;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
writeCollection('snippets.json', 'snippets', file.items, stamp);
|
|
446
|
+
return { added, updated };
|
|
447
|
+
}
|
|
448
|
+
function listLocalEnvSets() {
|
|
449
|
+
return readCollection('env.json').items.sort((a, b) => a.name.localeCompare(b.name));
|
|
450
|
+
}
|
|
451
|
+
function saveLocalEnvSet(input) {
|
|
452
|
+
const name = assertName(input.name);
|
|
453
|
+
const json = JSON.stringify(input.vars);
|
|
454
|
+
if (json.length > exports.LOCAL_SIZE_LIMITS.envVarsJsonChars) {
|
|
455
|
+
throw new LocalVaultSizeError(`Env set too large (max ${exports.LOCAL_SIZE_LIMITS.envVarsJsonChars} chars)`);
|
|
456
|
+
}
|
|
457
|
+
// Reject obvious secret-stuffing patterns in keys only (values stay local; sync still Pro-gated)
|
|
458
|
+
for (const k of Object.keys(input.vars)) {
|
|
459
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) {
|
|
460
|
+
throw new LocalVaultSizeError(`Invalid env var name: ${k}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const file = readCollection('env.json');
|
|
464
|
+
const existing = file.items.find((e) => e.name.toLowerCase() === name.toLowerCase());
|
|
465
|
+
if (existing) {
|
|
466
|
+
existing.vars = input.vars;
|
|
467
|
+
existing.updatedAt = nowIso();
|
|
468
|
+
writeCollection('env.json', 'envSets', file.items, file.syncedAt);
|
|
469
|
+
return existing;
|
|
470
|
+
}
|
|
471
|
+
assertUnderCap('envSets', file.items.length);
|
|
472
|
+
const row = {
|
|
473
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
474
|
+
name,
|
|
475
|
+
vars: input.vars,
|
|
476
|
+
createdAt: nowIso(),
|
|
477
|
+
updatedAt: nowIso(),
|
|
478
|
+
};
|
|
479
|
+
file.items.push(row);
|
|
480
|
+
writeCollection('env.json', 'envSets', file.items, file.syncedAt);
|
|
481
|
+
return row;
|
|
482
|
+
}
|
|
483
|
+
function deleteLocalEnvSet(id) {
|
|
484
|
+
const file = readCollection('env.json');
|
|
485
|
+
const idx = file.items.findIndex((e) => e.id === id);
|
|
486
|
+
if (idx < 0)
|
|
487
|
+
return undefined;
|
|
488
|
+
const [removed] = file.items.splice(idx, 1);
|
|
489
|
+
writeCollection('env.json', 'envSets', file.items, file.syncedAt);
|
|
490
|
+
return removed;
|
|
491
|
+
}
|
|
492
|
+
function updateLocalEnvSet(id, patch) {
|
|
493
|
+
const file = readCollection('env.json');
|
|
494
|
+
const row = file.items.find((e) => e.id === id);
|
|
495
|
+
if (!row)
|
|
496
|
+
return undefined;
|
|
497
|
+
if (patch.name !== undefined)
|
|
498
|
+
row.name = assertName(patch.name);
|
|
499
|
+
if (patch.vars !== undefined) {
|
|
500
|
+
const json = JSON.stringify(patch.vars);
|
|
501
|
+
if (json.length > exports.LOCAL_SIZE_LIMITS.envVarsJsonChars) {
|
|
502
|
+
throw new LocalVaultSizeError(`Env set too large (max ${exports.LOCAL_SIZE_LIMITS.envVarsJsonChars} chars)`);
|
|
503
|
+
}
|
|
504
|
+
row.vars = patch.vars;
|
|
505
|
+
}
|
|
506
|
+
row.updatedAt = nowIso();
|
|
507
|
+
writeCollection('env.json', 'envSets', file.items, file.syncedAt);
|
|
508
|
+
return row;
|
|
509
|
+
}
|
|
510
|
+
function envSetsNeedingPush() {
|
|
511
|
+
return listLocalEnvSets().filter((e) => !e.cloudId);
|
|
512
|
+
}
|
|
513
|
+
function attachEnvCloudId(localId, cloudId) {
|
|
514
|
+
const file = readCollection('env.json');
|
|
515
|
+
const row = file.items.find((e) => e.id === localId);
|
|
516
|
+
if (!row)
|
|
517
|
+
return;
|
|
518
|
+
row.cloudId = cloudId;
|
|
519
|
+
row.updatedAt = nowIso();
|
|
520
|
+
writeCollection('env.json', 'envSets', file.items, file.syncedAt);
|
|
521
|
+
}
|
|
522
|
+
function mergeCloudEnvSets(cloud) {
|
|
523
|
+
const file = readCollection('env.json');
|
|
524
|
+
const stamp = nowIso();
|
|
525
|
+
let added = 0;
|
|
526
|
+
let updated = 0;
|
|
527
|
+
for (const c of cloud) {
|
|
528
|
+
if (!c.vars)
|
|
529
|
+
continue; // list endpoint may omit vars — skip incomplete
|
|
530
|
+
const byCloud = c.id ? file.items.find((e) => e.cloudId === c.id) : undefined;
|
|
531
|
+
const byName = file.items.find((e) => e.name.toLowerCase() === c.name.toLowerCase());
|
|
532
|
+
const target = byCloud ?? byName;
|
|
533
|
+
if (target) {
|
|
534
|
+
if (c.id)
|
|
535
|
+
target.cloudId = c.id;
|
|
536
|
+
target.name = c.name;
|
|
537
|
+
target.vars = c.vars;
|
|
538
|
+
target.updatedAt = stamp;
|
|
539
|
+
updated++;
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
file.items.push({
|
|
543
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
544
|
+
cloudId: c.id,
|
|
545
|
+
name: c.name,
|
|
546
|
+
vars: c.vars,
|
|
547
|
+
createdAt: stamp,
|
|
548
|
+
updatedAt: stamp,
|
|
549
|
+
});
|
|
550
|
+
added++;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
writeCollection('env.json', 'envSets', file.items, stamp);
|
|
554
|
+
return { added, updated };
|
|
555
|
+
}
|
|
556
|
+
function listLocalAliases() {
|
|
557
|
+
return readCollection('aliases.json').items.sort((a, b) => a.alias.localeCompare(b.alias));
|
|
558
|
+
}
|
|
559
|
+
function saveLocalAlias(input) {
|
|
560
|
+
const alias = input.alias.trim();
|
|
561
|
+
if (!alias || /\s/.test(alias)) {
|
|
562
|
+
throw new LocalVaultSizeError('Alias must be a single token without spaces');
|
|
563
|
+
}
|
|
564
|
+
if (alias.length > exports.LOCAL_SIZE_LIMITS.aliasChars) {
|
|
565
|
+
throw new LocalVaultSizeError(`Alias too long (max ${exports.LOCAL_SIZE_LIMITS.aliasChars})`);
|
|
566
|
+
}
|
|
567
|
+
const file = readCollection('aliases.json');
|
|
568
|
+
const existing = file.items.find((a) => a.alias.toLowerCase() === alias.toLowerCase());
|
|
569
|
+
if (existing) {
|
|
570
|
+
existing.localCommandId = input.localCommandId;
|
|
571
|
+
existing.updatedAt = nowIso();
|
|
572
|
+
writeCollection('aliases.json', 'aliases', file.items, file.syncedAt);
|
|
573
|
+
return existing;
|
|
574
|
+
}
|
|
575
|
+
assertUnderCap('aliases', file.items.length);
|
|
576
|
+
const row = {
|
|
577
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
578
|
+
alias,
|
|
579
|
+
localCommandId: input.localCommandId,
|
|
580
|
+
createdAt: nowIso(),
|
|
581
|
+
updatedAt: nowIso(),
|
|
582
|
+
};
|
|
583
|
+
file.items.push(row);
|
|
584
|
+
writeCollection('aliases.json', 'aliases', file.items, file.syncedAt);
|
|
585
|
+
return row;
|
|
586
|
+
}
|
|
587
|
+
function deleteLocalAlias(id) {
|
|
588
|
+
const file = readCollection('aliases.json');
|
|
589
|
+
const idx = file.items.findIndex((a) => a.id === id);
|
|
590
|
+
if (idx < 0)
|
|
591
|
+
return undefined;
|
|
592
|
+
const [removed] = file.items.splice(idx, 1);
|
|
593
|
+
writeCollection('aliases.json', 'aliases', file.items, file.syncedAt);
|
|
594
|
+
return removed;
|
|
595
|
+
}
|
|
596
|
+
function aliasesNeedingPush() {
|
|
597
|
+
return listLocalAliases().filter((a) => !a.cloudId);
|
|
598
|
+
}
|
|
599
|
+
function attachAliasCloudId(localId, cloudId) {
|
|
600
|
+
const file = readCollection('aliases.json');
|
|
601
|
+
const row = file.items.find((a) => a.id === localId);
|
|
602
|
+
if (!row)
|
|
603
|
+
return;
|
|
604
|
+
row.cloudId = cloudId;
|
|
605
|
+
row.updatedAt = nowIso();
|
|
606
|
+
writeCollection('aliases.json', 'aliases', file.items, file.syncedAt);
|
|
607
|
+
}
|
|
608
|
+
function localCollectionCounts() {
|
|
609
|
+
return {
|
|
610
|
+
commands: 0, // filled by caller from commands vault
|
|
611
|
+
groups: listLocalGroups().length,
|
|
612
|
+
templates: listLocalTemplates().length,
|
|
613
|
+
snippets: listLocalSnippets().length,
|
|
614
|
+
envSets: listLocalEnvSets().length,
|
|
615
|
+
aliases: listLocalAliases().length,
|
|
616
|
+
};
|
|
617
|
+
}
|
package/dist/lib/local-vault.js
CHANGED
|
@@ -20,6 +20,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
20
20
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
21
|
const node_os_1 = __importDefault(require("node:os"));
|
|
22
22
|
const node_path_1 = __importDefault(require("node:path"));
|
|
23
|
+
const local_collections_1 = require("./local-collections");
|
|
23
24
|
const VAULT_DIR = node_path_1.default.join(process.env.RECALL_HOME?.trim() || node_os_1.default.homedir(), '.recall');
|
|
24
25
|
exports.VAULT_FILE = node_path_1.default.join(VAULT_DIR, 'commands.json');
|
|
25
26
|
function nowIso() {
|
|
@@ -127,13 +128,19 @@ function findLocalByName(name) {
|
|
|
127
128
|
return readVault().commands.find((c) => c.name?.toLowerCase() === needle);
|
|
128
129
|
}
|
|
129
130
|
function saveLocalCommand(input) {
|
|
131
|
+
const command = input.command.trim();
|
|
132
|
+
if (!command)
|
|
133
|
+
throw new local_collections_1.LocalVaultSizeError('Command is required');
|
|
134
|
+
if (command.length > local_collections_1.LOCAL_SIZE_LIMITS.commandChars) {
|
|
135
|
+
throw new local_collections_1.LocalVaultSizeError(`Command too long (max ${local_collections_1.LOCAL_SIZE_LIMITS.commandChars})`);
|
|
136
|
+
}
|
|
130
137
|
const vault = readVault();
|
|
131
138
|
const stamp = nowIso();
|
|
132
139
|
const name = input.name?.trim() || undefined;
|
|
133
140
|
if (name) {
|
|
134
141
|
const existing = vault.commands.find((c) => c.name?.toLowerCase() === name.toLowerCase());
|
|
135
142
|
if (existing) {
|
|
136
|
-
existing.command =
|
|
143
|
+
existing.command = command;
|
|
137
144
|
existing.tags = input.tags?.length ? input.tags : undefined;
|
|
138
145
|
existing.updatedAt = stamp;
|
|
139
146
|
// Local edit invalidates cloud link until next push maps again
|
|
@@ -141,9 +148,12 @@ function saveLocalCommand(input) {
|
|
|
141
148
|
return existing;
|
|
142
149
|
}
|
|
143
150
|
}
|
|
151
|
+
if (vault.commands.length >= local_collections_1.LOCAL_SOFT_CAPS.commands) {
|
|
152
|
+
throw new local_collections_1.LocalVaultLimitError('commands', local_collections_1.LOCAL_SOFT_CAPS.commands, vault.commands.length);
|
|
153
|
+
}
|
|
144
154
|
const saved = {
|
|
145
155
|
id: (0, node_crypto_1.randomUUID)(),
|
|
146
|
-
command
|
|
156
|
+
command,
|
|
147
157
|
name,
|
|
148
158
|
tags: input.tags?.length ? input.tags : undefined,
|
|
149
159
|
createdAt: stamp,
|