@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 CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  'use strict';
4
4
 
@@ -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. It is not documented in --help on purpose,
37
- // it must be exactly '1', and nothing in the product sets it; setting it only lets a person who typed
38
- // 'y' at the prompt through a pipe, it never accepts a cost by itself.
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
- return Boolean(process.stdin.isTTY) || process.env.EMPIRICAL_TEST_ASSUME_TTY === '1';
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>]');