@empirical-gauzza/cli 1.0.37 → 1.0.38
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/bin/empirical.js +1 -1
- package/lib/attachmentCommands.js +388 -0
- package/lib/constants.js +50 -0
- package/lib/empiricalTokensFormat.js +6 -4
- package/lib/help.js +12 -0
- package/lib/http.js +160 -0
- package/lib/installers/hooks.js +1312 -1312
- package/lib/installers/skill-variants/SKILL.both.md +20 -0
- package/lib/installers/skill-variants/SKILL.cli.md +231 -221
- package/lib/installers/skill-variants/SKILL.mcp.md +18 -0
- package/lib/memoryCommands.js +1267 -1222
- package/lib/memoryResolve.js +112 -0
- package/lib/setupCommands.js +191 -14
- package/lib/workspaceCommands.js +72 -5
- package/lib/workspaceContext.js +176 -176
- package/package.json +33 -33
package/bin/empirical.js
CHANGED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `empirical memory attachment <list|add|get|delete|restore>` over the four REST routes in
|
|
4
|
+
// packages/server/src/routes/memories.ts (POST/GET/DELETE /:id/attachments[/:attachmentId] and
|
|
5
|
+
// the restore POST). Attachments shipped REST-only; this is the first non-REST door.
|
|
6
|
+
//
|
|
7
|
+
// Two things here are not style choices:
|
|
8
|
+
// 1. `get` writes BYTES. It never goes through httpJson, which decodes utf8 and would corrupt
|
|
9
|
+
// every non-text file, and it never prints the body to stdout unless asked (--out -), so a
|
|
10
|
+
// stray download cannot dump a binary into a terminal or a piped JSON stream.
|
|
11
|
+
// 2. The per-file cap is checked locally BEFORE the upload. The server refuses an oversized
|
|
12
|
+
// body at ingress, but only after the client has pushed 25MB+ up the wire for nothing.
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { log, warn } = require('./ui');
|
|
17
|
+
const { httpJson, httpMultipart, httpDownload, printApiError } = require('./http');
|
|
18
|
+
const { parseOption, parseFlag } = require('./env');
|
|
19
|
+
const { requireAccessToken, resolveApiBaseUrl, buildApiAuthHeaders } = require('./auth');
|
|
20
|
+
const { resolveMemoryByQuery, parseMatchFilters, printMemoryResolveError } = require('./memoryResolve');
|
|
21
|
+
|
|
22
|
+
// Mirrors MAX_ATTACHMENT_BYTES / MAX_ATTACHMENTS_PER_MEMORY in packages/shared/src/
|
|
23
|
+
// subscriptionPlans.ts. The CLI is published as its own npm package and cannot depend on the
|
|
24
|
+
// workspace, so these are copied; they are used for a friendlier local error only, never to
|
|
25
|
+
// decide anything the server does not re-check.
|
|
26
|
+
const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
27
|
+
|
|
28
|
+
// Enough to label the common cases. The server sniffs magic bytes and overrides anything
|
|
29
|
+
// renderable or executable, so a wrong guess here is corrected, never trusted.
|
|
30
|
+
const MIME_BY_EXTENSION = {
|
|
31
|
+
'.txt': 'text/plain',
|
|
32
|
+
'.md': 'text/markdown',
|
|
33
|
+
'.json': 'application/json',
|
|
34
|
+
'.csv': 'text/csv',
|
|
35
|
+
'.pdf': 'application/pdf',
|
|
36
|
+
'.png': 'image/png',
|
|
37
|
+
'.jpg': 'image/jpeg',
|
|
38
|
+
'.jpeg': 'image/jpeg',
|
|
39
|
+
'.gif': 'image/gif',
|
|
40
|
+
'.webp': 'image/webp',
|
|
41
|
+
'.svg': 'image/svg+xml',
|
|
42
|
+
'.zip': 'application/zip',
|
|
43
|
+
'.mp3': 'audio/mpeg',
|
|
44
|
+
'.m4a': 'audio/mp4',
|
|
45
|
+
'.wav': 'audio/wav',
|
|
46
|
+
'.mp4': 'video/mp4'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const ATTACHMENT_SUBCOMMAND_USAGE = {
|
|
50
|
+
list: 'Usage: empirical memory attachment list (--memory-id <id> | --match <query>) [--include-deleted] [--format table] [--pretty]',
|
|
51
|
+
add: 'Usage: empirical memory attachment add (--memory-id <id> | --match <query>) --file <path> [--author-type user|agent] [--min-score 0.35]',
|
|
52
|
+
get: 'Usage: empirical memory attachment get (--memory-id <id> | --match <query>) --attachment-id <id> [--out <path>] [--force]\n --out - writes the bytes to stdout.',
|
|
53
|
+
delete: 'Usage: empirical memory attachment delete (--memory-id <id> | --match <query>) --attachment-id <id> [--confirm]\n Soft delete: the file is restorable for 30 days, then purged.',
|
|
54
|
+
restore: 'Usage: empirical memory attachment restore (--memory-id <id> | --match <query>) --attachment-id <id>'
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function printAttachmentHelp(sub) {
|
|
58
|
+
if (sub && ATTACHMENT_SUBCOMMAND_USAGE[sub]) {
|
|
59
|
+
log(ATTACHMENT_SUBCOMMAND_USAGE[sub]);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
log('Files attached to a memory. Up to 20 per memory, 25MB each; a delete is reversible for 30 days.');
|
|
63
|
+
log('');
|
|
64
|
+
for (const usage of Object.values(ATTACHMENT_SUBCOMMAND_USAGE)) log(usage);
|
|
65
|
+
log('');
|
|
66
|
+
log('Shorthands: `memory attach` = `memory attachment add`, `memory attachments` = `memory attachment list`.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function guessContentType(filename) {
|
|
70
|
+
return MIME_BY_EXTENSION[path.extname(String(filename || '')).toLowerCase()] || 'application/octet-stream';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fail(code, message, extra = {}) {
|
|
74
|
+
warn(JSON.stringify({ error: true, code, message, ...extra }));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Every subcommand takes the same target: an explicit id, or a --match query resolved the same
|
|
79
|
+
// way `memory note` and `memory delete` resolve theirs.
|
|
80
|
+
async function resolveTargetMemoryId(args, accessToken, baseUrl, usage) {
|
|
81
|
+
const memoryId = parseOption(args, '--memory-id', '');
|
|
82
|
+
if (memoryId) return memoryId;
|
|
83
|
+
|
|
84
|
+
const match = parseOption(args, '--match', '');
|
|
85
|
+
if (!match) {
|
|
86
|
+
warn(usage);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const minScore = Number(parseOption(args, '--min-score', '0.35'));
|
|
91
|
+
const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl, parseMatchFilters(args));
|
|
92
|
+
if (!result.found) {
|
|
93
|
+
printMemoryResolveError(result);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
return result.memory.id;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function attachmentsUrl(baseUrl, memoryId, suffix = '') {
|
|
100
|
+
return new URL(`/api/v1/memories/${encodeURIComponent(memoryId)}/attachments${suffix}`, baseUrl).toString();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function renderAttachmentsTable(rows) {
|
|
104
|
+
const header = ['ID', 'FILENAME', 'BYTES', 'TYPE', 'STATE'];
|
|
105
|
+
const body = rows.map((row) => [
|
|
106
|
+
String(row.id || ''),
|
|
107
|
+
String(row.filename || ''),
|
|
108
|
+
String(row.bytes != null ? row.bytes : ''),
|
|
109
|
+
String(row.mimeType || ''),
|
|
110
|
+
row.deletedAt ? `deleted, purges ${String(row.purgeAfter || '').slice(0, 10)}` : 'live'
|
|
111
|
+
]);
|
|
112
|
+
const widths = header.map((_h, i) => Math.max(header[i].length, ...body.map((r) => r[i].length), 1));
|
|
113
|
+
const line = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(' ').trimEnd();
|
|
114
|
+
log(line(header));
|
|
115
|
+
log(widths.map((w) => '-'.repeat(w)).join(' '));
|
|
116
|
+
for (const row of body) log(line(row));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function runAttachmentList(args) {
|
|
120
|
+
const accessToken = requireAccessToken();
|
|
121
|
+
const baseUrl = resolveApiBaseUrl();
|
|
122
|
+
const memoryId = await resolveTargetMemoryId(args, accessToken, baseUrl, ATTACHMENT_SUBCOMMAND_USAGE.list);
|
|
123
|
+
|
|
124
|
+
const url = attachmentsUrl(baseUrl, memoryId) + (parseFlag(args, '--include-deleted') ? '?includeDeleted=true' : '');
|
|
125
|
+
|
|
126
|
+
let response;
|
|
127
|
+
try {
|
|
128
|
+
response = await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
|
|
129
|
+
} catch (error) {
|
|
130
|
+
printApiError(error);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// This route answers with `attachments` at the TOP level, not under `data` like most of the
|
|
135
|
+
// memory routes; reading response.data here would silently print nothing.
|
|
136
|
+
const attachments = Array.isArray(response && response.attachments) ? response.attachments : [];
|
|
137
|
+
|
|
138
|
+
if (parseOption(args, '--format', '') === 'table') {
|
|
139
|
+
if (!attachments.length) {
|
|
140
|
+
log('No attachments on this memory.');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
renderAttachmentsTable(attachments);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const payload = {
|
|
148
|
+
memoryId: response.memoryId || memoryId,
|
|
149
|
+
attachmentsCount: response.attachmentsCount != null ? response.attachmentsCount : attachments.length,
|
|
150
|
+
attachments
|
|
151
|
+
};
|
|
152
|
+
log(parseFlag(args, '--pretty') ? JSON.stringify(payload, null, 2) : JSON.stringify(payload));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function runAttachmentAdd(args) {
|
|
156
|
+
const filePath = parseOption(args, '--file', '');
|
|
157
|
+
if (!filePath) {
|
|
158
|
+
warn(ATTACHMENT_SUBCOMMAND_USAGE.add);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const resolved = path.resolve(filePath);
|
|
163
|
+
let stat;
|
|
164
|
+
try {
|
|
165
|
+
stat = fs.statSync(resolved);
|
|
166
|
+
} catch (_error) {
|
|
167
|
+
fail('file_not_found', `No such file: ${resolved}`);
|
|
168
|
+
}
|
|
169
|
+
if (!stat.isFile()) {
|
|
170
|
+
fail('not_a_file', `Not a file: ${resolved}`);
|
|
171
|
+
}
|
|
172
|
+
if (stat.size === 0) {
|
|
173
|
+
fail('empty_file', `File is empty: ${resolved}`);
|
|
174
|
+
}
|
|
175
|
+
// Local pre-check, see note 2 at the top of this file.
|
|
176
|
+
if (stat.size > MAX_ATTACHMENT_BYTES) {
|
|
177
|
+
fail('attachment_too_large', `File is ${stat.size} bytes; the per-file limit is ${MAX_ATTACHMENT_BYTES} bytes (25MB).`, {
|
|
178
|
+
bytes: stat.size,
|
|
179
|
+
maxBytes: MAX_ATTACHMENT_BYTES
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const authorType = parseOption(args, '--author-type', '');
|
|
184
|
+
if (authorType && !['user', 'agent', 'system'].includes(authorType)) {
|
|
185
|
+
fail('invalid_author_type', `Invalid --author-type "${authorType}". Valid: user, agent, system`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const accessToken = requireAccessToken();
|
|
189
|
+
const baseUrl = resolveApiBaseUrl();
|
|
190
|
+
const memoryId = await resolveTargetMemoryId(args, accessToken, baseUrl, ATTACHMENT_SUBCOMMAND_USAGE.add);
|
|
191
|
+
|
|
192
|
+
const filename = path.basename(resolved);
|
|
193
|
+
let response;
|
|
194
|
+
try {
|
|
195
|
+
response = await httpMultipart(
|
|
196
|
+
attachmentsUrl(baseUrl, memoryId),
|
|
197
|
+
'POST',
|
|
198
|
+
{
|
|
199
|
+
// Same authorship contract as `memory record`: `source` is observable truth, authorType
|
|
200
|
+
// is only declared when the caller says so, never guessed.
|
|
201
|
+
fields: { source: 'cli', ...(authorType ? { authorType } : {}) },
|
|
202
|
+
file: { field: 'file', filename, contentType: guessContentType(filename), buffer: fs.readFileSync(resolved) }
|
|
203
|
+
},
|
|
204
|
+
buildApiAuthHeaders(accessToken)
|
|
205
|
+
);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
printApiError(error);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const data = (response && response.data) ? response.data : response;
|
|
212
|
+
log(JSON.stringify({ attached: true, ...data, ...(response.warnings ? { warnings: response.warnings } : {}) }));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Content-Disposition from the download route is always the RFC 5987 form
|
|
216
|
+
// (filename*=UTF-8''<pct-encoded>), so that is what is parsed; the plain filename="..." form is
|
|
217
|
+
// accepted too in case a proxy rewrites it.
|
|
218
|
+
function filenameFromContentDisposition(header) {
|
|
219
|
+
const value = String(header || '');
|
|
220
|
+
const extended = value.match(/filename\*=UTF-8''([^;]+)/i);
|
|
221
|
+
if (extended) {
|
|
222
|
+
try {
|
|
223
|
+
return decodeURIComponent(extended[1]);
|
|
224
|
+
} catch (_error) {
|
|
225
|
+
// Fall through to the plain form below.
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const plain = value.match(/filename="([^"]+)"/i);
|
|
229
|
+
return plain ? plain[1] : '';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function runAttachmentGet(args) {
|
|
233
|
+
const attachmentId = parseOption(args, '--attachment-id', '');
|
|
234
|
+
if (!attachmentId) {
|
|
235
|
+
warn(ATTACHMENT_SUBCOMMAND_USAGE.get);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const accessToken = requireAccessToken();
|
|
240
|
+
const baseUrl = resolveApiBaseUrl();
|
|
241
|
+
const memoryId = await resolveTargetMemoryId(args, accessToken, baseUrl, ATTACHMENT_SUBCOMMAND_USAGE.get);
|
|
242
|
+
|
|
243
|
+
let result;
|
|
244
|
+
try {
|
|
245
|
+
result = await httpDownload(
|
|
246
|
+
attachmentsUrl(baseUrl, memoryId, `/${encodeURIComponent(attachmentId)}`),
|
|
247
|
+
buildApiAuthHeaders(accessToken)
|
|
248
|
+
);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
printApiError(error);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const out = parseOption(args, '--out', '');
|
|
255
|
+
if (out === '-') {
|
|
256
|
+
process.stdout.write(result.buffer);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// The server's filename is only ever used as a BASENAME. It is attacker-supplied as far as
|
|
261
|
+
// this process is concerned, and joining a path that carries .. or a drive letter straight
|
|
262
|
+
// onto cwd would let a download write outside the directory the user chose.
|
|
263
|
+
const suggested = path.basename(filenameFromContentDisposition(result.headers['content-disposition']) || '') || 'attachment';
|
|
264
|
+
let target = out ? path.resolve(out) : path.resolve(process.cwd(), suggested);
|
|
265
|
+
if (out) {
|
|
266
|
+
try {
|
|
267
|
+
if (fs.statSync(target).isDirectory()) target = path.join(target, suggested);
|
|
268
|
+
} catch (_error) {
|
|
269
|
+
// --out names a file that does not exist yet, which is the normal case.
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (fs.existsSync(target) && !parseFlag(args, '--force')) {
|
|
274
|
+
fail('file_exists', `${target} already exists. Pass --force to overwrite, or --out <path> to write elsewhere.`, { path: target });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
fs.writeFileSync(target, result.buffer);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
fail('write_failed', `Could not write ${target}: ${error.message}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
log(JSON.stringify({ downloaded: true, memoryId, attachmentId, path: target, bytes: result.buffer.length }));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function runAttachmentDelete(args) {
|
|
287
|
+
const attachmentId = parseOption(args, '--attachment-id', '');
|
|
288
|
+
if (!attachmentId) {
|
|
289
|
+
warn(ATTACHMENT_SUBCOMMAND_USAGE.delete);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const accessToken = requireAccessToken();
|
|
294
|
+
const baseUrl = resolveApiBaseUrl();
|
|
295
|
+
const memoryId = await resolveTargetMemoryId(args, accessToken, baseUrl, ATTACHMENT_SUBCOMMAND_USAGE.delete);
|
|
296
|
+
|
|
297
|
+
// Same gate as `memory delete`: without --confirm this only reports what it would do.
|
|
298
|
+
if (!parseFlag(args, '--confirm')) {
|
|
299
|
+
log(JSON.stringify({ dryRun: true, wouldDelete: true, memoryId, attachmentId, note: 'Re-run with --confirm. The file stays restorable for 30 days.' }));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let response;
|
|
304
|
+
try {
|
|
305
|
+
response = await httpJson(
|
|
306
|
+
attachmentsUrl(baseUrl, memoryId, `/${encodeURIComponent(attachmentId)}`),
|
|
307
|
+
'DELETE',
|
|
308
|
+
null,
|
|
309
|
+
buildApiAuthHeaders(accessToken)
|
|
310
|
+
);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
printApiError(error);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const data = (response && response.data) ? response.data : response;
|
|
317
|
+
log(JSON.stringify({ deleted: true, ...data }));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function runAttachmentRestore(args) {
|
|
321
|
+
const attachmentId = parseOption(args, '--attachment-id', '');
|
|
322
|
+
if (!attachmentId) {
|
|
323
|
+
warn(ATTACHMENT_SUBCOMMAND_USAGE.restore);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const accessToken = requireAccessToken();
|
|
328
|
+
const baseUrl = resolveApiBaseUrl();
|
|
329
|
+
const memoryId = await resolveTargetMemoryId(args, accessToken, baseUrl, ATTACHMENT_SUBCOMMAND_USAGE.restore);
|
|
330
|
+
|
|
331
|
+
let response;
|
|
332
|
+
try {
|
|
333
|
+
response = await httpJson(
|
|
334
|
+
attachmentsUrl(baseUrl, memoryId, `/${encodeURIComponent(attachmentId)}/restore`),
|
|
335
|
+
'POST',
|
|
336
|
+
{},
|
|
337
|
+
buildApiAuthHeaders(accessToken)
|
|
338
|
+
);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
printApiError(error);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const data = (response && response.data) ? response.data : response;
|
|
345
|
+
log(JSON.stringify({ restored: true, ...data }));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const ATTACHMENT_SUBCOMMANDS = {
|
|
349
|
+
list: runAttachmentList,
|
|
350
|
+
ls: runAttachmentList,
|
|
351
|
+
add: runAttachmentAdd,
|
|
352
|
+
upload: runAttachmentAdd,
|
|
353
|
+
get: runAttachmentGet,
|
|
354
|
+
download: runAttachmentGet,
|
|
355
|
+
delete: runAttachmentDelete,
|
|
356
|
+
rm: runAttachmentDelete,
|
|
357
|
+
remove: runAttachmentDelete,
|
|
358
|
+
restore: runAttachmentRestore
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
async function runAttachmentCommand(subArgs) {
|
|
362
|
+
const sub = subArgs[0] && !subArgs[0].startsWith('-') ? subArgs[0] : undefined;
|
|
363
|
+
if (!sub || subArgs.includes('--help') || subArgs.includes('-h')) {
|
|
364
|
+
printAttachmentHelp(sub);
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const handler = ATTACHMENT_SUBCOMMANDS[sub];
|
|
369
|
+
if (!handler) {
|
|
370
|
+
warn('Unknown attachment subcommand: ' + sub);
|
|
371
|
+
printAttachmentHelp();
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return handler(subArgs.slice(1));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
module.exports = {
|
|
379
|
+
ATTACHMENT_SUBCOMMAND_USAGE,
|
|
380
|
+
guessContentType,
|
|
381
|
+
filenameFromContentDisposition,
|
|
382
|
+
runAttachmentList,
|
|
383
|
+
runAttachmentAdd,
|
|
384
|
+
runAttachmentGet,
|
|
385
|
+
runAttachmentDelete,
|
|
386
|
+
runAttachmentRestore,
|
|
387
|
+
runAttachmentCommand
|
|
388
|
+
};
|
package/lib/constants.js
CHANGED
|
@@ -183,6 +183,46 @@ Bulk delete by tag, category, or everything (exactly one selector):
|
|
|
183
183
|
Without \`--confirm\`, always a dry-run that prints the count that would be deleted; no writes
|
|
184
184
|
happen. Re-run with \`--confirm\` to actually delete.
|
|
185
185
|
|
|
186
|
+
### tree
|
|
187
|
+
One item and everything nested under it, as a readable document. The server assembles it in
|
|
188
|
+
one query, so there is no walking the graph level by level.
|
|
189
|
+
|
|
190
|
+
empirical memory tree (--memory-id <id> | --match <q>) [--depth 10] [--max-nodes 500] [--no-notes] [--format markdown|json] [--out <path>]
|
|
191
|
+
|
|
192
|
+
Flags:
|
|
193
|
+
- \`--depth\`: how many levels below the starting item to include, default 10. 0 is the item alone
|
|
194
|
+
- \`--max-nodes\`: cap on items in the document, default 500. When it bites, stderr carries a
|
|
195
|
+
truncated warning and the document itself stays clean for redirection
|
|
196
|
+
- \`--no-notes\`: leave each item's notes out, for a bare outline
|
|
197
|
+
- \`--format json\`: print the nodes and metadata instead of the document (markdown is the default
|
|
198
|
+
here, unlike other memory subcommands, because the point of this one is something to read)
|
|
199
|
+
- \`--out <path>\`: write the document to a file instead of stdout
|
|
200
|
+
|
|
201
|
+
Items nest only in a workspace that has a shape. For an ordinary memory this returns that one
|
|
202
|
+
memory as a one-section document.
|
|
203
|
+
|
|
204
|
+
### attachment
|
|
205
|
+
Files attached to a memory. Up to 20 files per memory, 25MB each. Every subcommand takes the
|
|
206
|
+
target memory as \`--memory-id <id>\` or \`--match <query>\`, the same way \`note\` and \`update\` do.
|
|
207
|
+
|
|
208
|
+
empirical memory attachment list (--memory-id <id> | --match <q>) [--include-deleted] [--format table] [--pretty]
|
|
209
|
+
empirical memory attachment add (--memory-id <id> | --match <q>) --file <path> [--author-type user|agent]
|
|
210
|
+
empirical memory attachment get (--memory-id <id> | --match <q>) --attachment-id <id> [--out <path>] [--force]
|
|
211
|
+
empirical memory attachment delete (--memory-id <id> | --match <q>) --attachment-id <id> [--confirm]
|
|
212
|
+
empirical memory attachment restore (--memory-id <id> | --match <q>) --attachment-id <id>
|
|
213
|
+
|
|
214
|
+
\`empirical memory attach\` is shorthand for \`attachment add\`, and \`empirical memory attachments\`
|
|
215
|
+
for \`attachment list\`.
|
|
216
|
+
|
|
217
|
+
Notes:
|
|
218
|
+
- \`delete\` is a soft delete: the file stays restorable for 30 days, then the purge worker
|
|
219
|
+
removes the bytes. Without \`--confirm\` it is a dry run, like \`memory delete\`.
|
|
220
|
+
- \`list --include-deleted\` shows deleted rows with the date their bytes get purged.
|
|
221
|
+
- \`get\` writes bytes to a file, not to stdout: \`--out <path>\` chooses where (a directory is
|
|
222
|
+
allowed), and without it the server's filename is used in the current directory. An existing
|
|
223
|
+
file is never overwritten without \`--force\`. \`--out -\` writes the bytes to stdout for piping.
|
|
224
|
+
- Attachments need a paid plan; the free tier's attachment pool is 0 bytes.
|
|
225
|
+
|
|
186
226
|
### export / import
|
|
187
227
|
Export memories to a versioned, round-trippable JSON document and re-import them later.
|
|
188
228
|
|
|
@@ -297,6 +337,16 @@ Note: workspace selection may be admin-locked for some OAuth clients.
|
|
|
297
337
|
|
|
298
338
|
Creates a team workspace using your existing plan quota and prints its workspace ID.
|
|
299
339
|
|
|
340
|
+
### Set up or re-shape a workspace by interview
|
|
341
|
+
|
|
342
|
+
empirical workspace create --interview
|
|
343
|
+
empirical workspace shape --workspace-id <id>
|
|
344
|
+
empirical setup abandon <sessionId>
|
|
345
|
+
|
|
346
|
+
The interview asks what the workspace is for, offers kinds of item, statuses, fields and rules to pick from, shows a draft, and creates the workspace (or saves the new shape) and emails any invites only after you confirm. At the first review type next to choose who to invite (a bare yes is not a choice there), or confirm to finish with nobody invited, or type a change. On a new workspace a change can be its name (for example: change the name to Wellness on Balance); the name of an existing workspace is changed in its settings, not here. After the invitations, a yes confirms. In a re-shape the workspace's current kinds of item, statuses, fields and rules are listed first as Now in use: say keep, or only add things after a bar, to start from them as they are. Adding works; taking something away is not possible yet. In a script, the draft answer may be a list sent in order, for example for a new workspace {"draft": ["change the name to Leads", "next"], "invites": "ana@example.com writer", "confirm": "confirm"}. \`create --interview\` always makes a new workspace and takes no name. \`shape\` re-shapes an existing team workspace you manage: pass \`--workspace-id\`. Without it, at a terminal only, the command names the workspace this directory is bound to and asks you to confirm it; in a script (\`--answers\`), with \`--accept-cost\`, or with no terminal it refuses and needs \`--workspace-id\`. \`--answers <file.json>\` runs it from a map of question key to answer, and \`--accept-cost\` agrees to the Empirical Tokens cost in a script (a person at a terminal is asked instead).
|
|
347
|
+
|
|
348
|
+
A new interview is charged. \`setup abandon\` stops one but does not return what it cost. When the interview is not switched on for your account, the start answers that nothing was started or charged.
|
|
349
|
+
|
|
300
350
|
### Use a workspace
|
|
301
351
|
|
|
302
352
|
empirical workspace use <number|name|id>
|
|
@@ -33,11 +33,13 @@ function resetLabel(resetsAt, timeZone) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// A charge is offered only to a real terminal; piped input never accepts one. EMPIRICAL_TEST_ASSUME_TTY=1
|
|
36
|
-
// exists only so tests can drive the prompt through a pipe
|
|
37
|
-
//
|
|
38
|
-
//
|
|
36
|
+
// exists only so tests can drive the prompt through a pipe, and it applies ONLY when NODE_ENV is 'test':
|
|
37
|
+
// a stray copy of it in a real shell cannot switch off the non-interactive refusals (a paid re-shape from
|
|
38
|
+
// a binding, a cost accepted with no one at the keyboard). It is not documented in --help on purpose and
|
|
39
|
+
// nothing in the product sets it.
|
|
39
40
|
function isInteractiveTerminal() {
|
|
40
|
-
|
|
41
|
+
if (process.stdin.isTTY) return true;
|
|
42
|
+
return process.env.NODE_ENV === 'test' && process.env.EMPIRICAL_TEST_ASSUME_TTY === '1';
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
// A priced start answered 200 with status 'cost_confirmation_required' and started nothing. This
|
package/lib/help.js
CHANGED
|
@@ -46,6 +46,13 @@ function showHelp() {
|
|
|
46
46
|
log(' empirical workspace unbind Remove this project\'s workspace binding');
|
|
47
47
|
log(' empirical workspace create <name> [--description <text>]');
|
|
48
48
|
log(' Create a new shared team workspace');
|
|
49
|
+
log(' empirical workspace create --interview [--answers <file.json>] [--accept-cost]');
|
|
50
|
+
log(' Set up a new shared workspace by interview: it offers options, shows a');
|
|
51
|
+
log(' draft, and creates the workspace and invites only when you say yes');
|
|
52
|
+
log(' empirical workspace shape [--workspace-id <id>] [--answers <file.json>] [--accept-cost]');
|
|
53
|
+
log(' Re-shape a team workspace you manage by interview. Without --workspace-id it');
|
|
54
|
+
log(' confirms the workspace bound to this directory with you, at a terminal only;');
|
|
55
|
+
log(' in a script or with --accept-cost it needs --workspace-id. A new interview is charged');
|
|
49
56
|
log(' empirical workspace invite create [--role reader|writer|manager] [--expires-in-days <n>] [--max-uses <n>]');
|
|
50
57
|
log(' Create a shareable invite link for the active team workspace');
|
|
51
58
|
log(' empirical workspace invite list List active invites for the active team workspace');
|
|
@@ -61,6 +68,7 @@ function showHelp() {
|
|
|
61
68
|
log(' empirical setup answer <sessionId> <key> <v> Answer a question');
|
|
62
69
|
log(' empirical setup confirm <sessionId> <hash> Confirm and install a reviewed draft');
|
|
63
70
|
log(' empirical setup finish <sessionId> Finish a scripted setup early');
|
|
71
|
+
log(' empirical setup abandon <sessionId> Stop a workspace interview (what it cost is not returned)');
|
|
64
72
|
log('');
|
|
65
73
|
log('Options:');
|
|
66
74
|
log(' install Interactive installer for Codex/Copilot/Claude CLI integration');
|
|
@@ -89,6 +97,10 @@ function showHelp() {
|
|
|
89
97
|
log(' empirical memory note Add a note to an existing memory');
|
|
90
98
|
log(' empirical memory update Update an existing memory');
|
|
91
99
|
log(' empirical memory delete Delete an existing memory');
|
|
100
|
+
log(' empirical memory attach Attach a file to a memory [--file <path>]');
|
|
101
|
+
log(' empirical memory attachments List the files on a memory [--include-deleted] [--format table]');
|
|
102
|
+
log(' empirical memory attachment Full file commands: list, add, get, delete, restore');
|
|
103
|
+
log(' empirical memory tree One item plus everything nested under it, as a document');
|
|
92
104
|
log(' empirical memory duplicates List duplicate candidates for a memory');
|
|
93
105
|
log(' empirical memory merge Merge a duplicate memory into a canonical one');
|
|
94
106
|
log(' empirical memory export Export memories as portable JSON [--scope <personal|workspace>] [--out <path>]');
|