satoridb 1.2.6 → 1.2.7

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.
Files changed (3) hide show
  1. package/cli.js +559 -134
  2. package/package.json +1 -1
  3. package/postinstall.js +20 -2
package/cli.js CHANGED
@@ -1,12 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * Satori Interactive CLI (Improved)
5
- * - Command registry (easy to extend)
6
- * - Robust arg parsing (quotes, escapes)
7
- * - Fixed bugs (lecture args check, set_mindspace double call)
8
- * - Better UX (history, tab completion, prompt context)
9
- * - Centralized error handling
4
+ * Satori Interactive CLI
10
5
  */
11
6
  var __importDefault = (this && this.__importDefault) || function (mod) {
12
7
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -18,11 +13,28 @@ const os_1 = __importDefault(require("os"));
18
13
  const fs_1 = __importDefault(require("fs"));
19
14
  const child_process_1 = require("child_process");
20
15
  const readline_1 = __importDefault(require("readline"));
21
- // -------------------------------
22
- // Globals / State
23
- // -------------------------------
24
- const binName = os_1.default.platform() === "win32" ? "satori.exe" : "satori";
25
- const binPath = path_1.default.join(os_1.default.homedir(), ".satori", "bin", binName);
16
+ // ── ANSI helpers ───────────────────────────────────────────────────────────────
17
+ const c = {
18
+ reset: '\x1b[0m',
19
+ dim: '\x1b[2m',
20
+ bold: '\x1b[1m',
21
+ cyan: '\x1b[96m',
22
+ green: '\x1b[92m',
23
+ yellow: '\x1b[93m',
24
+ red: '\x1b[91m',
25
+ blue: '\x1b[94m',
26
+ magenta: '\x1b[95m',
27
+ };
28
+ // ── State ──────────────────────────────────────────────────────────────────────
29
+ let VERSION = '1.2.6';
30
+ try {
31
+ VERSION = require('./package.json').version;
32
+ }
33
+ catch { }
34
+ const binName = os_1.default.platform() === 'win32' ? 'satori.exe' : 'satori';
35
+ const binPath = path_1.default.join(os_1.default.homedir(), '.satori', 'bin', binName);
36
+ const historyDir = path_1.default.join(os_1.default.homedir(), '.satori');
37
+ const historyFile = path_1.default.join(historyDir, '.cli_history');
26
38
  let satori = null;
27
39
  let session = {
28
40
  host: null,
@@ -30,21 +42,75 @@ let session = {
30
42
  password: null,
31
43
  mindspace: null,
32
44
  };
33
- // -------------------------------
34
- // Utils
35
- // -------------------------------
45
+ // ── Print helpers ──────────────────────────────────────────────────────────────
46
+ const ok = (msg) => console.log(` ${c.green}✓${c.reset} ${msg}`);
47
+ const fail = (msg) => console.error(` ${c.red}✗${c.reset} ${msg}`);
48
+ const info = (msg) => console.log(` ${c.dim}${msg}${c.reset}`);
49
+ function colorJson(obj) {
50
+ if (obj === null || obj === undefined)
51
+ return `${c.dim}null${c.reset}`;
52
+ if (typeof obj === 'string')
53
+ return obj;
54
+ if (typeof obj === 'number')
55
+ return `${c.yellow}${obj}${c.reset}`;
56
+ if (typeof obj === 'boolean')
57
+ return `${c.magenta}${obj}${c.reset}`;
58
+ return JSON.stringify(obj, null, 2)
59
+ .replace(/"([^"]+)":/g, `${c.cyan}"$1"${c.reset}:`)
60
+ .replace(/: "([^"]*)"/g, `: ${c.green}"$1"${c.reset}`)
61
+ .replace(/: (-?\d+(?:\.\d+)?)/g, `: ${c.yellow}$1${c.reset}`)
62
+ .replace(/: (true|false)/g, `: ${c.magenta}$1${c.reset}`)
63
+ .replace(/: null/g, `: ${c.dim}null${c.reset}`);
64
+ }
65
+ function print(obj) {
66
+ if (obj === null || obj === undefined)
67
+ return;
68
+ const s = colorJson(obj);
69
+ console.log('\n' + s.split('\n').map(l => ` ${l}`).join('\n') + '\n');
70
+ }
71
+ function spinner(text) {
72
+ if (!process.stdout.isTTY)
73
+ return () => { };
74
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
75
+ let i = 0;
76
+ const id = setInterval(() => {
77
+ process.stdout.write(`\r ${c.dim}${frames[i++ % frames.length]}${c.reset} ${c.dim}${text}${c.reset} `);
78
+ }, 80);
79
+ return () => {
80
+ clearInterval(id);
81
+ process.stdout.write('\r' + ' '.repeat(text.length + 10) + '\r');
82
+ };
83
+ }
84
+ // ── Banner ─────────────────────────────────────────────────────────────────────
85
+ function showBanner() {
86
+ const logo = [
87
+ ` ___ _ _ ____ ___ ____ ____`,
88
+ `/ __|| | |/ _ |/ || _ \\|_ _|`,
89
+ `\\__ \\| | |\\__ | / \\ || / | | `,
90
+ `|___/|___|/____||_| |_||_|\\_\\ |_| `,
91
+ ];
92
+ console.log();
93
+ logo.forEach(l => console.log(` ${c.cyan}${c.bold}${l}${c.reset}`));
94
+ console.log();
95
+ console.log(` ${c.dim}AI-Native Distributed Object Database · v${VERSION}${c.reset}`);
96
+ console.log(` ${c.dim}Type ${c.reset}${c.cyan}help${c.reset}` +
97
+ `${c.dim} for commands · ${c.reset}${c.cyan}connect <host>${c.reset}${c.dim} to begin${c.reset}`);
98
+ console.log();
99
+ }
100
+ // ── Utilities ──────────────────────────────────────────────────────────────────
36
101
  function assertInstalled() {
37
102
  if (!fs_1.default.existsSync(binPath)) {
38
- console.error("❌ Satori binary not found. Reinstall the package.");
103
+ fail('Satori binary not found. Run: npm install -g satoridb');
39
104
  process.exit(1);
40
105
  }
41
106
  }
42
107
  function runBinary(args) {
43
108
  assertInstalled();
44
- const r = (0, child_process_1.spawnSync)(binPath, args, { stdio: "inherit" });
109
+ const r = (0, child_process_1.spawnSync)(binPath, args, { stdio: 'inherit' });
45
110
  if (r.error)
46
111
  throw r.error;
47
112
  }
113
+ /** Tokenises a shell-style line respecting single/double quotes and backslash escapes. */
48
114
  function parseArgs(line) {
49
115
  const re = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|(\S+)/g;
50
116
  const out = [];
@@ -53,17 +119,18 @@ function parseArgs(line) {
53
119
  out.push(m[1] ?? m[2] ?? m[3]);
54
120
  return out.map(v => v.replace(/\\"/g, '"').replace(/\\'/g, "'"));
55
121
  }
122
+ /** Auto-casts string tokens to bool / number / JSON when unambiguous. */
56
123
  function parseData(v) {
57
- if (typeof v !== "string")
124
+ if (typeof v !== 'string')
58
125
  return v;
59
126
  const t = v.trim();
60
- if (t === "true")
127
+ if (t === 'true')
61
128
  return true;
62
- if (t === "false")
129
+ if (t === 'false')
63
130
  return false;
64
- if (!isNaN(Number(t)) && t !== "")
131
+ if (t !== '' && !isNaN(Number(t)))
65
132
  return Number(t);
66
- if (t.startsWith("{") || t.startsWith("[")) {
133
+ if (t.startsWith('{') || t.startsWith('[')) {
67
134
  try {
68
135
  return JSON.parse(t);
69
136
  }
@@ -73,8 +140,17 @@ function parseData(v) {
73
140
  }
74
141
  function requireConn() {
75
142
  if (!satori)
76
- throw new Error("No active connection. Use: connect <host> [user] [password]");
143
+ throw new Error('Not connected use: connect <host>');
77
144
  }
145
+ function requireMindspace() {
146
+ requireConn();
147
+ if (!session.mindspace) {
148
+ throw new Error('No mindspace selected — use: mindspace select <id>');
149
+ }
150
+ }
151
+ // Returns `any` so the extra auth fields don't clash with typed SDK payloads.
152
+ // The satori-node already injects username/password via its constructor, but
153
+ // some server builds also accept them in the command body — keep for compat.
78
154
  function withCreds(p = {}) {
79
155
  if (session.user)
80
156
  p.user = session.user;
@@ -82,205 +158,554 @@ function withCreds(p = {}) {
82
158
  p.password = session.password;
83
159
  return p;
84
160
  }
161
+ /**
162
+ * Plain-text prompt — ANSI codes inside the prompt string break readline's
163
+ * cursor-position tracking, so keep it clean.
164
+ */
85
165
  function promptLabel() {
86
- return `satori${session.mindspace ? `:${session.mindspace}` : ""}> `;
166
+ const host = session.host
167
+ ? session.host.replace(/^wss?:\/\//, '').replace(/:\d+$/, '')
168
+ : null;
169
+ const ms = session.mindspace ? `[${session.mindspace}]` : '';
170
+ return host ? `${host}${ms} › ` : 'satori › ';
171
+ }
172
+ // ── History ────────────────────────────────────────────────────────────────────
173
+ function loadHistory() {
174
+ try {
175
+ if (fs_1.default.existsSync(historyFile)) {
176
+ return fs_1.default.readFileSync(historyFile, 'utf8')
177
+ .split('\n').filter(Boolean).reverse().slice(0, 500);
178
+ }
179
+ }
180
+ catch { }
181
+ return [];
182
+ }
183
+ function saveHistory(history) {
184
+ try {
185
+ fs_1.default.mkdirSync(historyDir, { recursive: true });
186
+ fs_1.default.writeFileSync(historyFile, [...history].reverse().join('\n') + '\n');
187
+ }
188
+ catch { }
87
189
  }
88
- // -------------------------------
89
- // Commands Registry
90
- // -------------------------------
91
190
  const commands = {
92
- help: {
93
- desc: "Show help",
94
- run() { showHelp(); }
95
- },
96
- exit: { run() { process.exit(0); } },
97
- quit: { run() { process.exit(0); } },
98
- clear: { run() { console.clear(); } },
191
+ // ── Connection ───────────────────────────────────────────────────────────────
99
192
  connect: {
100
- desc: "connect <host> [user] [password]",
101
- async run([host, user = null, password = null]) {
193
+ group: 'Connection', noConn: true,
194
+ desc: 'connect <host> [user] [password]',
195
+ async run([host, user, password]) {
102
196
  if (!host)
103
- throw new Error("Usage: connect <host> [user] [password]");
104
- console.log(`🔗 Connecting to ${host}...`);
105
- if (user != null && password != null) {
106
- satori = new satori_node_1.Satori({ host, username: user, password });
197
+ throw new Error('Usage: connect <host> [user] [password]');
198
+ const stop = spinner(`Connecting to ${host}`);
199
+ try {
200
+ satori = (user && password)
201
+ ? new satori_node_1.Satori({ host, username: user, password })
202
+ : new satori_node_1.Satori({ host, username: '', password: '' });
203
+ await satori.connect();
204
+ stop();
205
+ Object.assign(session, { host, user: user ?? null, password: password ?? null });
206
+ ok(`Connected ${c.dim}${host}${c.reset}`);
107
207
  }
108
- else {
109
- satori = new satori_node_1.Satori({ host: host, username: "", password: "" });
208
+ catch (e) {
209
+ stop();
210
+ satori = null;
211
+ throw new Error(`Connection failed: ${e.message ?? e}`);
110
212
  }
111
- await satori.connect();
112
- Object.assign(session, { host, user, password });
113
- console.log("✅ Connected");
114
213
  }
115
214
  },
116
- mindspace: {
117
- desc: "mindspace select <id>",
118
- run([sub, id]) {
119
- if (sub !== "select" || !id)
120
- throw new Error("Usage: mindspace select <id>");
121
- session.mindspace = id;
122
- console.log(`🧠 Mindspace selected: ${id}`);
215
+ disconnect: {
216
+ group: 'Connection',
217
+ desc: 'Disconnect from server',
218
+ run() {
219
+ requireConn();
220
+ const host = session.host;
221
+ satori = null;
222
+ Object.assign(session, { host: null, user: null, password: null, mindspace: null });
223
+ ok(`Disconnected from ${host}`);
123
224
  }
124
225
  },
226
+ status: {
227
+ group: 'Connection', noConn: true,
228
+ desc: 'Show connection info',
229
+ run() {
230
+ if (!satori) {
231
+ info('Not connected');
232
+ return;
233
+ }
234
+ console.log();
235
+ console.log(` ${c.dim}Host ${c.reset}${c.cyan}${session.host}${c.reset}`);
236
+ if (session.user)
237
+ console.log(` ${c.dim}User ${c.reset}${session.user}`);
238
+ if (session.mindspace)
239
+ console.log(` ${c.dim}Mindspace ${c.reset}${c.green}${session.mindspace}${c.reset}`);
240
+ console.log(` ${c.dim}Status ${c.reset}${c.green}● Connected${c.reset}`);
241
+ console.log();
242
+ }
243
+ },
244
+ // ── Data ─────────────────────────────────────────────────────────────────────
125
245
  set: {
126
- desc: "set <key> <data>",
246
+ group: 'Data',
247
+ desc: 'set <key> <data>',
127
248
  async run([key, ...rest]) {
128
249
  requireConn();
129
250
  if (!key || !rest.length)
130
- throw new Error("Usage: set <key> <data>");
131
- const data = parseData(rest.join(" "));
132
- await satori.set(withCreds({ key, data }));
133
- console.log(`✅ Saved: ${key}`);
251
+ throw new Error('Usage: set <key> <data>');
252
+ const stop = spinner('Writing...');
253
+ await satori.set(withCreds({ key, data: parseData(rest.join(' ')) }));
254
+ stop();
255
+ ok(`Saved ${c.cyan}${key}${c.reset}`);
134
256
  }
135
257
  },
136
258
  get: {
137
- desc: "get <key>",
259
+ group: 'Data',
260
+ desc: 'get [key]',
138
261
  async run([key]) {
139
262
  requireConn();
263
+ const stop = spinner('Fetching...');
140
264
  const r = await satori.get(withCreds(key ? { key } : {}));
141
- console.log(JSON.stringify(r, null, 2));
265
+ stop();
266
+ print(r);
142
267
  }
143
268
  },
144
269
  put: {
145
- desc: "put <key> <field> <value>",
146
- async run([key, field, value]) {
270
+ group: 'Data',
271
+ desc: 'put <key> <field> <value>',
272
+ async run([key, field, ...rest]) {
147
273
  requireConn();
148
- if (!key || !field)
149
- throw new Error("Usage: put <key> <field> <value>");
150
- await satori.put(withCreds({ key, replace_field: field, replace_value: parseData(value) }));
151
- console.log("✅ Updated");
274
+ if (!key || !field || !rest.length)
275
+ throw new Error('Usage: put <key> <field> <value>');
276
+ const stop = spinner('Updating...');
277
+ await satori.put(withCreds({
278
+ key,
279
+ replace_field: field,
280
+ replace_value: parseData(rest.join(' ')),
281
+ }));
282
+ stop();
283
+ ok(`Updated ${c.cyan}${key}${c.reset}.${field}`);
152
284
  }
153
285
  },
154
286
  delete: {
155
- desc: "delete <key>",
287
+ group: 'Data',
288
+ desc: 'delete <key>',
156
289
  async run([key]) {
157
290
  requireConn();
158
291
  if (!key)
159
- throw new Error("Usage: delete <key>");
292
+ throw new Error('Usage: delete <key>');
293
+ const stop = spinner('Deleting...');
160
294
  await satori.delete(withCreds({ key }));
161
- console.log(`🗑️ Deleted: ${key}`);
295
+ stop();
296
+ ok(`Deleted ${c.cyan}${key}${c.reset}`);
297
+ }
298
+ },
299
+ push: {
300
+ group: 'Data',
301
+ desc: 'push <key> <array> <value> – append to array field',
302
+ async run([key, array, ...rest]) {
303
+ requireConn();
304
+ if (!key || !array || !rest.length)
305
+ throw new Error('Usage: push <key> <array> <value>');
306
+ const stop = spinner('Pushing...');
307
+ const r = await satori.push(withCreds({ key, array, value: parseData(rest.join(' ')) }));
308
+ stop();
309
+ print(r);
310
+ }
311
+ },
312
+ pop: {
313
+ group: 'Data',
314
+ desc: 'pop <key> <array> – remove last array element',
315
+ async run([key, array]) {
316
+ requireConn();
317
+ if (!key || !array)
318
+ throw new Error('Usage: pop <key> <array>');
319
+ const stop = spinner('Popping...');
320
+ const r = await satori.pop(withCreds({ key, array }));
321
+ stop();
322
+ print(r);
323
+ }
324
+ },
325
+ splice: {
326
+ group: 'Data',
327
+ desc: 'splice <key> <array> – remove first array element',
328
+ async run([key, array]) {
329
+ requireConn();
330
+ if (!key || !array)
331
+ throw new Error('Usage: splice <key> <array>');
332
+ const stop = spinner('Splicing...');
333
+ const r = await satori.splice(withCreds({ key, array }));
334
+ stop();
335
+ print(r);
336
+ }
337
+ },
338
+ remove: {
339
+ group: 'Data',
340
+ desc: 'remove <key> <array> <value> – remove specific value from array',
341
+ async run([key, array, ...rest]) {
342
+ requireConn();
343
+ if (!key || !array || !rest.length)
344
+ throw new Error('Usage: remove <key> <array> <value>');
345
+ const stop = spinner('Removing...');
346
+ const r = await satori.remove(withCreds({ key, array, value: parseData(rest.join(' ')) }));
347
+ stop();
348
+ print(r);
349
+ }
350
+ },
351
+ // ── Graph ─────────────────────────────────────────────────────────────────────
352
+ 'set-vertex': {
353
+ group: 'Graph',
354
+ desc: 'set-vertex <from> <to> [weight]',
355
+ async run([from, to, weight]) {
356
+ requireConn();
357
+ if (!from || !to)
358
+ throw new Error('Usage: set-vertex <from> <to> [weight]');
359
+ const stop = spinner('Creating edge...');
360
+ await satori.setVertex(withCreds({
361
+ key: from, vertex: to,
362
+ weight: weight !== undefined ? Number(weight) : 1,
363
+ }));
364
+ stop();
365
+ ok(`Edge ${c.cyan}${from}${c.reset} → ${c.cyan}${to}${c.reset}`);
366
+ }
367
+ },
368
+ 'get-vertex': {
369
+ group: 'Graph',
370
+ desc: 'get-vertex <key>',
371
+ async run([key]) {
372
+ requireConn();
373
+ if (!key)
374
+ throw new Error('Usage: get-vertex <key>');
375
+ const stop = spinner('Fetching edges...');
376
+ const r = await satori.getVertex(withCreds({ key }));
377
+ stop();
378
+ print(r);
379
+ }
380
+ },
381
+ 'delete-vertex': {
382
+ group: 'Graph',
383
+ desc: 'delete-vertex <from> <to>',
384
+ async run([from, to]) {
385
+ requireConn();
386
+ if (!from || !to)
387
+ throw new Error('Usage: delete-vertex <from> <to>');
388
+ const stop = spinner('Removing edge...');
389
+ await satori.deleteVertex(withCreds({ key: from, vertex: to }));
390
+ stop();
391
+ ok(`Edge removed ${c.cyan}${from}${c.reset} → ${c.cyan}${to}${c.reset}`);
162
392
  }
163
393
  },
394
+ dfs: {
395
+ group: 'Graph',
396
+ desc: 'dfs <node> – depth-first traversal',
397
+ async run([node]) {
398
+ requireConn();
399
+ if (!node)
400
+ throw new Error('Usage: dfs <node>');
401
+ const stop = spinner('Traversing (DFS)...');
402
+ const r = await satori.dfs(withCreds({ node }));
403
+ stop();
404
+ print(r);
405
+ }
406
+ },
407
+ bfs: {
408
+ group: 'Graph',
409
+ desc: 'bfs <node> – breadth-first traversal',
410
+ async run([node]) {
411
+ requireConn();
412
+ if (!node)
413
+ throw new Error('Usage: bfs <node>');
414
+ const stop = spinner('Traversing (BFS)...');
415
+ const r = await satori.graphBfs(withCreds({ node }));
416
+ stop();
417
+ print(r);
418
+ }
419
+ },
420
+ 'shortest-path': {
421
+ group: 'Graph',
422
+ desc: 'shortest-path <node> <target>',
423
+ async run([node, target]) {
424
+ requireConn();
425
+ if (!node || !target)
426
+ throw new Error('Usage: shortest-path <node> <target>');
427
+ const stop = spinner('Computing...');
428
+ const r = await satori.graphShortestPath(withCreds({ node, target }));
429
+ stop();
430
+ print(r);
431
+ }
432
+ },
433
+ // ── AI ───────────────────────────────────────────────────────────────────────
164
434
  ask: {
165
- desc: "ask \"<question>\"",
166
- async run([q, backend = null]) {
435
+ group: 'AI',
436
+ desc: 'ask "<question>" [backend]',
437
+ async run([q, backend]) {
167
438
  requireConn();
168
439
  if (!q)
169
- throw new Error("Usage: ask \"<question>\"");
170
- const r = await satori.ask(withCreds({ question: q, backend }));
171
- console.log(JSON.stringify(r, null, 2));
440
+ throw new Error('Usage: ask "<question>" [backend]');
441
+ const stop = spinner('Thinking...');
442
+ const r = await satori.ask(withCreds({
443
+ question: q,
444
+ ...(backend ? { backend } : {}),
445
+ }));
446
+ stop();
447
+ const answer = r?.message ?? r?.data ?? r;
448
+ print(answer);
172
449
  }
173
450
  },
174
451
  query: {
175
- desc: "query \"<query>\"",
176
- async run([q, backend = null]) {
452
+ group: 'AI',
453
+ desc: 'query "<text>" [backend]',
454
+ async run([q, backend]) {
177
455
  requireConn();
178
456
  if (!q)
179
- throw new Error("Usage: query \"<query>\"");
180
- const r = await satori.query(withCreds({ query: q, backend }));
181
- console.log(JSON.stringify(r, null, 2));
457
+ throw new Error('Usage: query "<text>" [backend]');
458
+ const stop = spinner('Searching...');
459
+ const r = await satori.query(withCreds({
460
+ query: q,
461
+ ...(backend ? { backend } : {}),
462
+ }));
463
+ stop();
464
+ print(r);
465
+ }
466
+ },
467
+ // ── Mindspace ─────────────────────────────────────────────────────────────────
468
+ mindspace: {
469
+ group: 'Mindspace', noConn: true,
470
+ desc: 'mindspace select <id> – set active mindspace (local only)',
471
+ run([sub, id]) {
472
+ if (sub !== 'select' || !id)
473
+ throw new Error('Usage: mindspace select <id>');
474
+ session.mindspace = id;
475
+ ok(`Active mindspace: ${c.green}${id}${c.reset}`);
476
+ }
477
+ },
478
+ 'set-mindspace': {
479
+ group: 'Mindspace',
480
+ desc: 'set-mindspace [id] – create / activate mindspace on server',
481
+ async run([id]) {
482
+ requireConn();
483
+ const mid = id ?? session.mindspace;
484
+ if (!mid)
485
+ throw new Error('Provide an ID or select one first: mindspace select <id>');
486
+ const stop = spinner(`Activating ${mid}...`);
487
+ await satori.setMindspace(withCreds({ mindspace_id: mid, config: '' }));
488
+ stop();
489
+ session.mindspace = mid;
490
+ ok(`Mindspace active: ${c.green}${mid}${c.reset}`);
491
+ }
492
+ },
493
+ 'delete-mindspace': {
494
+ group: 'Mindspace',
495
+ desc: 'delete-mindspace <id>',
496
+ async run([id]) {
497
+ requireConn();
498
+ if (!id)
499
+ throw new Error('Usage: delete-mindspace <id>');
500
+ const stop = spinner(`Deleting ${id}...`);
501
+ await satori.deleteMindspace(withCreds({ mindspace_id: id }));
502
+ stop();
503
+ if (session.mindspace === id)
504
+ session.mindspace = null;
505
+ ok(`Deleted mindspace: ${c.cyan}${id}${c.reset}`);
182
506
  }
183
507
  },
184
508
  chat: {
185
- desc: "chat \"<message>\"",
509
+ group: 'Mindspace',
510
+ desc: 'chat "<message>"',
186
511
  async run([msg]) {
187
- requireConn();
188
- if (!session.mindspace)
189
- throw new Error("Select a mindspace first");
190
- const r = await satori.chatMindspace({ minspace_id: session.mindspace, message: msg });
191
- console.log(r);
512
+ requireMindspace();
513
+ if (!msg)
514
+ throw new Error('Usage: chat "<message>"');
515
+ const stop = spinner('Thinking...');
516
+ const r = await satori.chatMindspace(withCreds({ minspace_id: session.mindspace, message: msg }));
517
+ stop();
518
+ print(r);
192
519
  }
193
520
  },
194
521
  lecture: {
195
- desc: "lecture \"<corpus>\"",
522
+ group: 'Mindspace',
523
+ desc: 'lecture "<corpus>"',
196
524
  async run([corpus]) {
525
+ requireMindspace();
526
+ if (!corpus)
527
+ throw new Error('Usage: lecture "<corpus>"');
528
+ const stop = spinner('Ingesting corpus...');
529
+ const r = await satori.lectureMindspace(withCreds({ mindspace_id: session.mindspace, corpus }));
530
+ stop();
531
+ const msg = r?.message;
532
+ if (msg)
533
+ ok(msg);
534
+ else
535
+ ok('Corpus ingested');
536
+ }
537
+ },
538
+ // ── Crypto ────────────────────────────────────────────────────────────────────
539
+ encrypt: {
540
+ group: 'Crypto',
541
+ desc: 'encrypt <key> <encryption_key>',
542
+ async run([key, encryption_key]) {
543
+ requireConn();
544
+ if (!key || !encryption_key)
545
+ throw new Error('Usage: encrypt <key> <encryption_key>');
546
+ const stop = spinner('Encrypting...');
547
+ const r = await satori.encrypt(withCreds({ key, encryption_key }));
548
+ stop();
549
+ print(r);
550
+ }
551
+ },
552
+ decrypt: {
553
+ group: 'Crypto',
554
+ desc: 'decrypt <key> <encryption_key>',
555
+ async run([key, encryption_key]) {
556
+ requireConn();
557
+ if (!key || !encryption_key)
558
+ throw new Error('Usage: decrypt <key> <encryption_key>');
559
+ const stop = spinner('Decrypting...');
560
+ const r = await satori.decrypt(withCreds({ key, encryption_key }));
561
+ stop();
562
+ print(r);
563
+ }
564
+ },
565
+ // ── Analytics ─────────────────────────────────────────────────────────────────
566
+ operations: {
567
+ group: 'Analytics',
568
+ desc: 'operations – recent operation log',
569
+ async run() {
197
570
  requireConn();
198
- if (!session.mindspace)
199
- throw new Error("Select a mindspace first");
200
- const r = await satori.lectureMindspace({ mindspace_id: session.mindspace, corpus });
201
- console.log(r.message);
571
+ const stop = spinner('Fetching...');
572
+ const r = await satori.getOperations();
573
+ stop();
574
+ print(r);
202
575
  }
203
576
  },
204
- set_mindspace: {
205
- desc: "set_mindspace [mindspace_id]",
206
- async run([mindspace_id = null]) {
577
+ freq: {
578
+ group: 'Analytics',
579
+ desc: 'freq <key> – access frequency for a key',
580
+ async run([key]) {
207
581
  requireConn();
208
- if (!satori)
209
- throw new Error("No active connection");
210
- let payload = { config: {} };
211
- if (mindspace_id)
212
- payload.mindspace_id = mindspace_id;
213
- else if (session.mindspace)
214
- payload.mindspace_id = session.mindspace;
215
- const res = await satori.setMindspace(payload);
216
- if (payload.mindspace_id)
217
- session.mindspace = payload.mindspace_id;
218
- console.log(`🧠 Mindspace configured: ${payload.mindspace_id ?? res.data}`);
582
+ if (!key)
583
+ throw new Error('Usage: freq <key>');
584
+ const stop = spinner('Fetching...');
585
+ const r = await satori.getAccessFrequency(key);
586
+ stop();
587
+ print(r);
219
588
  }
220
589
  },
590
+ // ── Utility ───────────────────────────────────────────────────────────────────
591
+ help: {
592
+ group: 'Utility', noConn: true,
593
+ desc: 'Show help',
594
+ run() { showHelp(); }
595
+ },
596
+ clear: {
597
+ group: 'Utility', noConn: true,
598
+ desc: 'Clear screen',
599
+ run() { console.clear(); showBanner(); }
600
+ },
601
+ exit: {
602
+ group: 'Utility', noConn: true,
603
+ desc: 'Exit',
604
+ run() { process.exit(0); }
605
+ },
606
+ quit: {
607
+ group: 'Utility', noConn: true,
608
+ desc: 'Exit',
609
+ run() { process.exit(0); }
610
+ },
611
+ };
612
+ // Backward-compat aliases (old underscore variants kept for muscle memory)
613
+ const aliases = {
614
+ set_mindspace: 'set-mindspace',
615
+ delete_mindspace: 'delete-mindspace',
616
+ set_vertex: 'set-vertex',
617
+ get_vertex: 'get-vertex',
618
+ delete_vertex: 'delete-vertex',
619
+ shortest_path: 'shortest-path',
221
620
  };
222
- // -------------------------------
223
- // Help
224
- // -------------------------------
621
+ for (const [alias, canonical] of Object.entries(aliases)) {
622
+ commands[alias] = commands[canonical];
623
+ }
624
+ // ── Help ───────────────────────────────────────────────────────────────────────
225
625
  function showHelp() {
226
- console.log("\n📚 Commands:\n");
227
- Object.entries(commands).forEach(([k, v]) => {
228
- if ('desc' in v && v.desc)
229
- console.log(` ${k.padEnd(12)} ${v.desc}`);
230
- });
231
- console.log("");
626
+ var _a;
627
+ const hidden = new Set(['quit', ...Object.keys(aliases)]);
628
+ const groups = {};
629
+ for (const [name, cmd] of Object.entries(commands)) {
630
+ if (hidden.has(name))
631
+ continue;
632
+ (groups[_a = cmd.group] ?? (groups[_a] = [])).push([name, cmd.desc]);
633
+ }
634
+ console.log();
635
+ for (const [group, entries] of Object.entries(groups)) {
636
+ console.log(` ${c.yellow}${group}${c.reset}`);
637
+ for (const [name, desc] of entries) {
638
+ const argsPart = desc.startsWith(name) ? desc.slice(name.length).trim() : desc;
639
+ console.log(` ${c.cyan}${name.padEnd(20)}${c.reset}` +
640
+ `${c.dim}${argsPart}${c.reset}`);
641
+ }
642
+ console.log();
643
+ }
232
644
  }
233
- // -------------------------------
234
- // Interactive Shell
235
- // -------------------------------
645
+ // ── REPL ───────────────────────────────────────────────────────────────────────
236
646
  async function startREPL() {
647
+ showBanner();
648
+ // Tab-completion list: canonical names only (no aliases, no quit duplicate)
649
+ const completionList = Object.keys(commands).filter(k => !aliases[k] && k !== 'quit');
237
650
  const rl = readline_1.default.createInterface({
238
651
  input: process.stdin,
239
652
  output: process.stdout,
240
653
  historySize: 500,
241
654
  completer(line) {
242
- const hits = Object.keys(commands).filter(c => c.startsWith(line));
243
- return [hits.length ? hits : Object.keys(commands), line];
244
- }
655
+ const hits = completionList.filter(k => k.startsWith(line));
656
+ return [hits.length ? hits : completionList, line];
657
+ },
245
658
  });
659
+ // Restore saved history into readline's internal array
660
+ const history = loadHistory();
661
+ if (history.length && rl.history) {
662
+ rl.history.push(...history);
663
+ }
246
664
  const refreshPrompt = () => rl.setPrompt(promptLabel());
247
665
  refreshPrompt();
248
666
  rl.prompt();
249
- rl.on("line", async (line) => {
250
- const parts = parseArgs(line.trim());
251
- if (!parts.length)
252
- return rl.prompt();
667
+ rl.on('line', async (raw) => {
668
+ const line = raw.trim();
669
+ if (!line) {
670
+ rl.prompt();
671
+ return;
672
+ }
673
+ const parts = parseArgs(line);
253
674
  const [cmd, ...args] = parts;
254
675
  const entry = commands[cmd];
255
676
  try {
256
- if (!entry)
257
- throw new Error("Unknown command. Type 'help'.");
258
- if (satori == null) {
259
- console.error("❌ You are not connected");
677
+ if (!entry) {
678
+ fail(`Unknown command: ${c.cyan}${cmd}${c.reset} — type ${c.cyan}help${c.reset}`);
679
+ }
680
+ else if (!entry.noConn && !satori) {
681
+ fail(`Not connected — use: ${c.cyan}connect <host>${c.reset}`);
260
682
  }
261
683
  else {
262
684
  await entry.run(args);
263
685
  }
264
686
  }
265
687
  catch (e) {
266
- console.error("❌", e.message);
688
+ fail(e.message ?? String(e));
267
689
  }
268
690
  refreshPrompt();
269
691
  rl.prompt();
270
692
  });
693
+ rl.on('close', () => {
694
+ saveHistory(rl.history ?? []);
695
+ console.log();
696
+ process.exit(0);
697
+ });
271
698
  }
272
- // -------------------------------
273
- // Entry
274
- // -------------------------------
699
+ // ── Entry ──────────────────────────────────────────────────────────────────────
275
700
  const argv = process.argv.slice(2);
276
- if (argv[0] === "run") {
701
+ if (argv[0] === 'run') {
277
702
  runBinary(argv.slice(1));
278
703
  }
279
- else if (argv[0] === "update") {
280
- require("./postinstall");
704
+ else if (argv[0] === 'update') {
705
+ require('./postinstall');
281
706
  }
282
- else if (argv[0] === "verify") {
283
- require("./verify-install");
707
+ else if (argv[0] === 'verify') {
708
+ require('./verify-install');
284
709
  }
285
710
  else {
286
711
  startREPL();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "satoridb",
3
- "version": "1.2.6",
3
+ "version": "1.2.7",
4
4
  "description": "Install satori",
5
5
  "bin": {
6
6
  "satoridb": "./cli.js"
package/postinstall.js CHANGED
@@ -10,14 +10,32 @@ let { spawnSync } = require("child_process");
10
10
  let platform = os.platform();
11
11
  let arch = os.arch();
12
12
 
13
+ // ── CLI-only mode ──────────────────────────────────────────────────────────────
14
+ // Set SATORI_NO_BINARY=1 to skip the binary download entirely.
15
+ // The interactive shell (satoridb) still works — it only needs a running
16
+ // Satori server reachable over WebSocket.
17
+ //
18
+ // SATORI_NO_BINARY=1 npm install -g satoridb
19
+ //
20
+ if (process.env.SATORI_NO_BINARY === "1") {
21
+ console.log(" ℹ Skipping Satori binary download (SATORI_NO_BINARY=1)");
22
+ console.log(" · The CLI shell is ready: satoridb");
23
+ console.log(" · To download the binary later: satoridb update");
24
+ process.exit(0);
25
+ }
26
+
13
27
  let baseURL = "https://www.satoridb.com";
14
28
  let fileName;
15
29
 
16
30
  if (platform === "linux") fileName = "lin/satori-linux.zip";
17
31
  else if (platform === "win32") fileName = "win/satori-win.zip";
18
32
  else {
19
- console.log("❌ Platform not supported:", platform);
20
- process.exit(1);
33
+ // Unsupported platform — warn but do not fail; CLI shell still works.
34
+ console.log(` ⚠ No pre-built binary for platform: ${platform}`);
35
+ console.log(" · The CLI shell is still available: satoridb");
36
+ console.log(" · Connect to any Satori server via: connect <ws://host:port>");
37
+ console.log(" · To skip this message next time: SATORI_NO_BINARY=1 npm install -g satoridb");
38
+ process.exit(0);
21
39
  }
22
40
 
23
41
  let binName = platform === "win32" ? "satori.exe" : "satori";