@satori-sh/cli 0.0.12 → 0.0.14

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/dist/index.js CHANGED
@@ -3,8 +3,9 @@
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
5
  import { readFileSync, realpathSync } from "fs";
6
- import { dirname, join as join2 } from "path";
7
- import { fileURLToPath } from "url";
6
+ import { execFile } from "child_process";
7
+ import { dirname as dirname2, join as join3 } from "path";
8
+ import { fileURLToPath as fileURLToPath2 } from "url";
8
9
  import chalk from "chalk";
9
10
 
10
11
  // src/config.ts
@@ -35,6 +36,13 @@ async function saveApiKey(apiKey) {
35
36
  throw new Error(`Failed to save API key: ${error instanceof Error ? error.message : error}`);
36
37
  }
37
38
  }
39
+ async function getStoredApiKey() {
40
+ const data = await loadConfigFile();
41
+ if (typeof data.api_key === "string") {
42
+ return data.api_key;
43
+ }
44
+ return null;
45
+ }
38
46
  async function saveMemoryId(memoryId) {
39
47
  const { promises: fs } = await import("fs");
40
48
  await checkWriteAccess();
@@ -66,6 +74,7 @@ async function getConfig() {
66
74
  }
67
75
  let apiKey = null;
68
76
  let memoryId = void 0;
77
+ const openaiApiKey = process.env.OPENAI_API_KEY || null;
69
78
  try {
70
79
  const data = await loadConfigFile();
71
80
  if (typeof data.api_key === "string") {
@@ -79,12 +88,21 @@ async function getConfig() {
79
88
  if (!apiKey) {
80
89
  apiKey = process.env.SATORI_API_KEY || null;
81
90
  }
82
- const baseUrl = process.env.SATORI_BASE_URL || "https://api.satori.sh";
91
+ const nodeEnv = process.env.NODE_ENV;
92
+ const baseUrl = process.env.SATORI_BASE_URL || (nodeEnv !== "production" ? "http://localhost:8000" : "https://api.satori.sh");
93
+ const checkoutUrl = process.env.SATORI_CHECKOUT_URL || (nodeEnv === "production" ? "https://buy.stripe.com/aFabJ03GIez6bH53Ckew800" : "https://buy.stripe.com/test_14AeVc5TZc2GapagZp0gw00");
83
94
  try {
84
95
  new URL(baseUrl);
85
96
  } catch {
86
97
  throw new Error("Invalid SATORI_BASE_URL format");
87
98
  }
99
+ if (checkoutUrl) {
100
+ try {
101
+ new URL(checkoutUrl);
102
+ } catch {
103
+ throw new Error("Invalid SATORI_CHECKOUT_URL format");
104
+ }
105
+ }
88
106
  if (!apiKey) {
89
107
  try {
90
108
  const response = await fetch(`${baseUrl}/orgs`, {
@@ -104,14 +122,10 @@ async function getConfig() {
104
122
  throw new Error(`Failed to generate API key: ${error instanceof Error ? error.message : error}`);
105
123
  }
106
124
  }
107
- const provider = process.env.SATORI_PROVIDER || "openai";
108
- const model = process.env.SATORI_MODEL || "gpt-4o";
109
- const openaiKey = process.env.OPENAI_API_KEY;
110
- const anthropicKey = process.env.ANTHROPIC_API_KEY;
111
125
  if (!memoryId) {
112
126
  memoryId = process.env.SATORI_MEMORY_ID;
113
127
  }
114
- return { apiKey, baseUrl, provider, model, openaiKey, anthropicKey, memoryId };
128
+ return { apiKey, openaiApiKey, baseUrl, checkoutUrl, memoryId };
115
129
  }
116
130
 
117
131
  // src/search.ts
@@ -130,6 +144,10 @@ async function searchMemories(query) {
130
144
  },
131
145
  body: JSON.stringify({ query })
132
146
  });
147
+ if (response.status === 402) {
148
+ console.error("Free tier exceeded. Run `npx @satori-sh sub` to subscribe");
149
+ return;
150
+ }
133
151
  if (!response.ok) {
134
152
  console.error(`HTTP error: ${response.status} ${response.statusText}`);
135
153
  return;
@@ -156,6 +174,10 @@ async function addMemories(text, options = {}) {
156
174
  },
157
175
  body: JSON.stringify({ messages: [{ role: "user", content: text }], ...options.memoryId && { memory_id: options.memoryId } })
158
176
  });
177
+ if (response.status === 402) {
178
+ console.error("Free tier exceeded. Run `npx @satori-sh sub` to subscribe");
179
+ return;
180
+ }
159
181
  if (!response.ok) {
160
182
  console.error(`HTTP error: ${response.status} ${response.statusText}`);
161
183
  return;
@@ -168,7 +190,7 @@ async function addMemories(text, options = {}) {
168
190
 
169
191
  // src/memory.ts
170
192
  import { generate } from "random-words";
171
- async function buildMemoryContext(prompt, options = {}) {
193
+ async function resolveMemoryId(options = {}) {
172
194
  const config = await getConfig();
173
195
  let memoryId;
174
196
  let generated = false;
@@ -183,37 +205,13 @@ async function buildMemoryContext(prompt, options = {}) {
183
205
  memoryId = words.join("-");
184
206
  generated = true;
185
207
  }
186
- const topK = options.topK || 5;
187
- const url = `${config.baseUrl}/search`;
188
- const headers = {
189
- "Content-Type": "application/json",
190
- "Authorization": `Bearer ${config.apiKey}`
191
- };
192
- const body = JSON.stringify({
193
- query: prompt,
194
- memory_id: memoryId,
195
- top_k: topK
196
- });
197
- const response = await fetch(url, {
198
- method: "POST",
199
- headers,
200
- body
201
- });
202
- if (!response.ok) {
203
- throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
204
- }
205
- const data = await response.json();
206
208
  const instruction = generated ? `Memory session id: ${memoryId}.` : void 0;
207
209
  if (generated) {
208
210
  saveMemoryId(memoryId).catch((err) => {
209
211
  console.error("Failed to save memory ID:", err);
210
212
  });
211
213
  }
212
- return {
213
- results: data.results,
214
- memoryId,
215
- instruction
216
- };
214
+ return { memoryId, instruction };
217
215
  }
218
216
  function enhanceMessagesWithMemory(messages, memoryContext) {
219
217
  const validResults = memoryContext.results.filter((r) => r.memory && r.memory.trim() !== "" && r.memory !== "undefined");
@@ -229,159 +227,373 @@ ${memoryText}`
229
227
  return [systemMessage, ...messages];
230
228
  }
231
229
 
232
- // src/providers.ts
233
- async function _callOpenAI(messages, options) {
234
- const config = await getConfig();
235
- const apiKey = config.openaiKey;
236
- if (!apiKey) {
237
- throw new Error("Missing API key for OPENAI_API_KEY");
238
- }
239
- const url = "https://api.openai.com/v1/chat/completions";
240
- const body = {
241
- model: config.model,
242
- messages,
243
- temperature: options.temperature ?? 0.7,
244
- max_tokens: options.maxTokens ?? 1e3,
245
- stream: options.stream ?? false
246
- };
247
- const headers = {
248
- "Content-Type": "application/json",
249
- "Authorization": `Bearer ${apiKey}`
250
- };
251
- const response = await fetch(url, {
252
- method: "POST",
253
- headers,
254
- body: JSON.stringify(body)
255
- });
256
- if (!response.ok) {
257
- throw new Error(`OpenAI API error: ${response.status} ${response.statusText}`);
258
- }
259
- const data = await response.json();
260
- return data.choices[0].message.content;
230
+ // src/ui.tsx
231
+ import { render, useRenderer, useTerminalDimensions } from "@opentui/solid";
232
+ import { For, Show, createSignal, onMount, onCleanup } from "solid-js";
233
+ import cliSpinners from "cli-spinners";
234
+
235
+ // src/logo.ts
236
+ import { dirname, join as join2 } from "path";
237
+ import { fileURLToPath } from "url";
238
+ async function loadLogo() {
239
+ const { default: fs } = await import("fs");
240
+ const __dirname = dirname(fileURLToPath(import.meta.url));
241
+ const logoPath = join2(__dirname, "..", "logos", "satori.ans");
242
+ return fs.readFileSync(logoPath, "utf8");
261
243
  }
262
- async function _callAnthropic(messages, options) {
263
- const config = await getConfig();
264
- const apiKey = config.anthropicKey;
265
- if (!apiKey) {
266
- throw new Error("Missing API key for ANTHROPIC_API_KEY");
267
- }
268
- const url = "https://api.anthropic.com/v1/messages";
269
- const body = {
270
- model: config.model,
271
- messages,
272
- temperature: options.temperature ?? 0.7,
273
- max_tokens: options.maxTokens ?? 1e3,
274
- stream: options.stream ?? false
244
+
245
+ // src/ui.tsx
246
+ async function runInteractiveApp({
247
+ initialPrompt,
248
+ options,
249
+ processUserInput,
250
+ infoLine,
251
+ infoDisplay
252
+ }) {
253
+ const logo = await loadLogo();
254
+ console.log(` ${logo}`);
255
+ const rows = process.stdout.rows ?? 24;
256
+ const logoHeight = logo.endsWith("\n") ? logo.slice(0, -1).split("\n").length : logo.split("\n").length;
257
+ const splitHeight = Math.max(1, rows - logoHeight - 1);
258
+ render(
259
+ () => /* @__PURE__ */ React.createElement(
260
+ App,
261
+ {
262
+ initialPrompt,
263
+ options,
264
+ processUserInput,
265
+ infoLine,
266
+ infoDisplay
267
+ }
268
+ ),
269
+ {
270
+ useAlternateScreen: false,
271
+ exitOnCtrlC: true,
272
+ useMouse: true,
273
+ enableMouseMovement: true,
274
+ experimental_splitHeight: splitHeight
275
+ }
276
+ );
277
+ }
278
+ function App({ initialPrompt, options, processUserInput, infoLine, infoDisplay }) {
279
+ const renderer = useRenderer();
280
+ const dimensions = useTerminalDimensions();
281
+ const [messages, setMessages] = createSignal([]);
282
+ const [inputValue, setInputValue] = createSignal("");
283
+ const [showIntro, setShowIntro] = createSignal(true);
284
+ const [isFullScreen, setIsFullScreen] = createSignal(false);
285
+ const [spinnerFrame, setSpinnerFrame] = createSignal(0);
286
+ const [isLoading, setIsLoading] = createSignal(false);
287
+ const promptFg = "#00ffff";
288
+ const responseFg = "#ffffff";
289
+ const promptBg = "#2b2b2b";
290
+ let inputRef;
291
+ let currentMemoryId = options.memoryId;
292
+ let messageId = 0;
293
+ const usageText = infoDisplay?.usageLine ?? infoLine ?? "";
294
+ const versionText = infoDisplay?.versionLine ?? "";
295
+ const modelText = infoDisplay?.modelLine ?? "";
296
+ const appendMessage = (role, text) => {
297
+ setMessages((prev) => [...prev, { id: messageId++, role, text }]);
298
+ };
299
+ const exitApp = () => {
300
+ renderer.destroy();
301
+ process.exit(0);
275
302
  };
276
- const headers = {
277
- "Content-Type": "application/json",
278
- "Authorization": `Bearer ${apiKey}`,
279
- "anthropic-version": "2023-06-01"
303
+ const submitPrompt = async (raw) => {
304
+ const trimmed = raw.trim();
305
+ if (!trimmed) return;
306
+ if (trimmed.toLowerCase() === "exit" || trimmed.toLowerCase() === "quit") {
307
+ exitApp();
308
+ return;
309
+ }
310
+ if (showIntro()) {
311
+ setShowIntro(false);
312
+ }
313
+ if (!isFullScreen()) {
314
+ setIsFullScreen(true);
315
+ }
316
+ setInputValue("");
317
+ if (inputRef) {
318
+ inputRef.value = "";
319
+ }
320
+ appendMessage("prompt", trimmed);
321
+ try {
322
+ setIsLoading(true);
323
+ const result = await processUserInput(trimmed, { ...options, memoryId: currentMemoryId }, "tui");
324
+ currentMemoryId = result.memoryId;
325
+ appendMessage("response", result.response);
326
+ if (result.instruction) {
327
+ appendMessage("response", result.instruction);
328
+ }
329
+ } catch (error) {
330
+ const message = error instanceof Error ? error.message : String(error);
331
+ appendMessage("response", `Error: ${message}`);
332
+ } finally {
333
+ setIsLoading(false);
334
+ }
280
335
  };
281
- const response = await fetch(url, {
282
- method: "POST",
283
- headers,
284
- body: JSON.stringify(body)
336
+ onMount(async () => {
337
+ const spinner = cliSpinners.dots;
338
+ const timer = setInterval(() => {
339
+ if (isLoading()) {
340
+ setSpinnerFrame((prev) => (prev + 1) % spinner.frames.length);
341
+ }
342
+ }, spinner.interval);
343
+ onCleanup(() => clearInterval(timer));
344
+ if (initialPrompt) {
345
+ await submitPrompt(initialPrompt);
346
+ }
347
+ if (inputRef) {
348
+ inputRef.focus();
349
+ }
285
350
  });
286
- if (!response.ok) {
287
- throw new Error(`Anthropic API error: ${response.status} ${response.statusText}`);
288
- }
289
- const data = await response.json();
290
- return data.content[0].text;
291
- }
292
- async function _callProviderAPI(messages, options, provider) {
293
- if (provider === "openai") {
294
- return _callOpenAI(messages, options);
295
- } else if (provider === "anthropic") {
296
- return _callAnthropic(messages, options);
297
- } else {
298
- throw new Error(`Unsupported provider: ${provider}`);
299
- }
351
+ const inputBoxWidth = () => Math.max(1, Math.round(dimensions().width * 0.6));
352
+ const inputBoxLeft = () => Math.max(0, Math.round(dimensions().width * 0.15));
353
+ const inputBoxHeight = () => isFullScreen() ? 7 : 14;
354
+ const inputBoxTop = () => isFullScreen() ? Math.max(1, dimensions().height - inputBoxHeight() - 2) : Math.max(1, Math.round(dimensions().height * 0.666));
355
+ const messagesTop = () => 1;
356
+ const messagesHeight = () => Math.max(1, inputBoxTop() - messagesTop() - 1);
357
+ const messagesWidth = () => Math.min(dimensions().width - 2, inputBoxWidth() + 10);
358
+ const messagesLeft = () => Math.max(1, inputBoxLeft() - 5);
359
+ return /* @__PURE__ */ React.createElement("box", { width: "100%", height: "100%", flexDirection: "column" }, /* @__PURE__ */ React.createElement(
360
+ "scrollbox",
361
+ {
362
+ id: "messages",
363
+ width: messagesWidth(),
364
+ height: messagesHeight(),
365
+ position: "absolute",
366
+ left: messagesLeft(),
367
+ top: messagesTop(),
368
+ paddingLeft: 1,
369
+ paddingRight: 1,
370
+ focused: true,
371
+ stickyScroll: true,
372
+ stickyStart: "bottom"
373
+ },
374
+ /* @__PURE__ */ React.createElement("box", { width: "100%", flexDirection: "column" }, /* @__PURE__ */ React.createElement(For, { each: messages() }, (message) => /* @__PURE__ */ React.createElement(
375
+ "box",
376
+ {
377
+ width: "100%",
378
+ flexDirection: "row",
379
+ justifyContent: message.role === "prompt" ? "flex-start" : "flex-end",
380
+ marginBottom: 1
381
+ },
382
+ /* @__PURE__ */ React.createElement(
383
+ "box",
384
+ {
385
+ paddingLeft: 1,
386
+ paddingRight: 1,
387
+ paddingTop: 1,
388
+ paddingBottom: 1,
389
+ backgroundColor: message.role === "prompt" ? promptBg : void 0
390
+ },
391
+ /* @__PURE__ */ React.createElement(
392
+ "text",
393
+ {
394
+ fg: message.role === "prompt" ? promptFg : responseFg,
395
+ width: "100%",
396
+ wrapMode: "word",
397
+ selectable: false
398
+ },
399
+ message.text
400
+ )
401
+ )
402
+ )))
403
+ ), /* @__PURE__ */ React.createElement(
404
+ "box",
405
+ {
406
+ id: "input-box",
407
+ width: inputBoxWidth(),
408
+ height: inputBoxHeight(),
409
+ position: "absolute",
410
+ left: inputBoxLeft(),
411
+ top: inputBoxTop(),
412
+ paddingLeft: 1,
413
+ paddingRight: 1,
414
+ paddingTop: 1,
415
+ flexDirection: "column"
416
+ },
417
+ /* @__PURE__ */ React.createElement(For, { each: !isFullScreen() && showIntro() ? [
418
+ "Use Satori just like you would use ChatGPT.",
419
+ "Except, it stores your conversations in a long term memory.",
420
+ "The memories you store here can be accessed through the SDK."
421
+ ] : [] }, (line) => /* @__PURE__ */ React.createElement("text", { fg: "cyan" }, line)),
422
+ /* @__PURE__ */ React.createElement(
423
+ "box",
424
+ {
425
+ id: "input-box",
426
+ width: inputBoxWidth(),
427
+ height: 5,
428
+ backgroundColor: "#1a1a1a",
429
+ flexDirection: "column",
430
+ justifyContent: "center"
431
+ },
432
+ /* @__PURE__ */ React.createElement(
433
+ "input",
434
+ {
435
+ id: "input",
436
+ width: "100%",
437
+ height: 1,
438
+ placeholder: "Type a message and press Enter...",
439
+ focusedBackgroundColor: "#1a1a1a",
440
+ onInput: (value) => setInputValue(value),
441
+ onSubmit: () => submitPrompt(inputValue()),
442
+ ref: (r) => {
443
+ inputRef = r;
444
+ }
445
+ }
446
+ ),
447
+ /* @__PURE__ */ React.createElement("box", { flexDirection: "row", flexShrink: 0, paddingTop: 1 }, /* @__PURE__ */ React.createElement("text", { fg: "#ffffff" }, modelText))
448
+ )
449
+ ), /* @__PURE__ */ React.createElement(Show, { when: isLoading() }, /* @__PURE__ */ React.createElement(
450
+ "box",
451
+ {
452
+ id: "spinner",
453
+ position: "absolute",
454
+ left: inputBoxLeft(),
455
+ top: inputBoxTop() + inputBoxHeight(),
456
+ paddingLeft: 1
457
+ },
458
+ /* @__PURE__ */ React.createElement("text", { fg: "#00ffff" }, cliSpinners.dots.frames[spinnerFrame()])
459
+ )), /* @__PURE__ */ React.createElement(
460
+ "box",
461
+ {
462
+ id: "footer",
463
+ width: dimensions().width,
464
+ height: 1,
465
+ position: "absolute",
466
+ bottom: 0,
467
+ left: 0,
468
+ backgroundColor: "#000000",
469
+ paddingLeft: 1,
470
+ paddingRight: 1,
471
+ flexDirection: "row",
472
+ justifyContent: "space-between",
473
+ alignItems: "center"
474
+ },
475
+ /* @__PURE__ */ React.createElement("text", { fg: "#00ffff", wrapMode: "none", width: "100%" }, usageText),
476
+ /* @__PURE__ */ React.createElement("box", { flexShrink: 0, paddingLeft: 1 }, /* @__PURE__ */ React.createElement("text", { fg: "#00ffff" }, versionText))
477
+ ));
300
478
  }
301
479
 
302
480
  // src/index.ts
303
481
  async function main() {
304
- try {
305
- await getConfig();
306
- } catch (error) {
307
- console.error(error instanceof Error ? error.message : "Configuration error");
308
- process.exit(1);
482
+ const argv = process.argv.slice(2);
483
+ const isGetApiKey = argv[0] === "get" && argv[1] === "api-key";
484
+ if (!isGetApiKey) {
485
+ try {
486
+ await getConfig();
487
+ } catch (error) {
488
+ console.error(error instanceof Error ? error.message : "Configuration error");
489
+ process.exit(1);
490
+ }
309
491
  }
310
- const __dirname = dirname(fileURLToPath(import.meta.url));
311
- const logoPath = join2(__dirname, "..", "logo.txt");
312
- console.log(chalk.cyan(readFileSync(logoPath, "utf8")));
313
492
  const program = new Command();
314
493
  program.name("satori").description("CLI tool for Satori memory server").version("0.0.1");
315
- program.option("--provider <provider>", "Provider to use (openai or anthropic)", "openai").option("--model <model>", "Model to use", "gpt-4o").option("--memory-id <id>", "Memory ID for scoping");
316
- const processUserInput = async (input, options, isInteractive = false) => {
317
- let memoryContext;
494
+ program.option("--memory-id <id>", "Memory ID for scoping");
495
+ const DEFAULT_LLM_MODEL = "gpt-4o";
496
+ const getCliVersion = () => {
497
+ const __dirname = dirname2(fileURLToPath2(import.meta.url));
498
+ const packagePath = join3(__dirname, "..", "package.json");
499
+ const raw = readFileSync(packagePath, "utf8");
500
+ const data = JSON.parse(raw);
501
+ return data.version ?? "unknown";
502
+ };
503
+ const fetchOrgUsage = async () => {
318
504
  try {
319
- memoryContext = await buildMemoryContext(input, { memoryId: options.memoryId });
320
- } catch (memoryError) {
321
- memoryContext = { results: [], memoryId: options.memoryId, instruction: void 0 };
505
+ const config = await getConfig();
506
+ const response = await fetch(`${config.baseUrl}/org/usage`, {
507
+ method: "GET",
508
+ headers: {
509
+ "Authorization": `Bearer ${config.apiKey}`
510
+ }
511
+ });
512
+ if (!response.ok) {
513
+ return null;
514
+ }
515
+ return await response.json();
516
+ } catch {
517
+ return null;
322
518
  }
323
- const userMessage = { role: "user", content: input };
324
- const enhancedMessages = enhanceMessagesWithMemory([userMessage], { results: memoryContext.results });
325
- const response = await _callProviderAPI(enhancedMessages, {
326
- temperature: 0.7,
327
- maxTokens: 1e3
328
- }, options.provider);
329
- if (isInteractive) {
330
- console.log(`Assistant: ${response}`);
331
- } else {
332
- console.log(response);
519
+ };
520
+ const getInfoDisplay = async () => {
521
+ const usage = await fetchOrgUsage();
522
+ const tokens = usage ? usage.total_tokens.toLocaleString() : "unknown";
523
+ const memoryId = usage?.memory_id ?? "unknown";
524
+ const version = getCliVersion();
525
+ return {
526
+ usageLine: `Tokens Used ${tokens} | Memory Layer: ${memoryId}`,
527
+ versionLine: `${version}`,
528
+ modelLine: `Model: ${DEFAULT_LLM_MODEL}`,
529
+ fullLine: `Model: ${DEFAULT_LLM_MODEL} | Tokens: ${tokens} | Memory ID: ${memoryId} | CLI: ${version}`
530
+ };
531
+ };
532
+ const callAskAPI = async (prompt, memoryId) => {
533
+ const config = await getConfig();
534
+ const headers = {
535
+ "Content-Type": "application/json",
536
+ "Authorization": `Bearer ${config.apiKey}`
537
+ };
538
+ if (config.openaiApiKey) {
539
+ headers["X-OpenAI-Key"] = config.openaiApiKey;
333
540
  }
334
- if (memoryContext.instruction) {
335
- console.log(`
336
- ${memoryContext.instruction}`);
541
+ const response = await fetch(`${config.baseUrl}/ask`, {
542
+ method: "POST",
543
+ headers,
544
+ body: JSON.stringify({
545
+ prompt,
546
+ memory_id: memoryId,
547
+ llm_model: DEFAULT_LLM_MODEL
548
+ })
549
+ });
550
+ if (response.status === 402) {
551
+ console.error("Free tier exceeded. Run `npx @satori-sh sub` to subscribe");
552
+ process.exit(1);
337
553
  }
338
- addMemories(input, { memoryId: memoryContext.memoryId }).catch((err) => {
554
+ if (!response.ok) {
555
+ throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
556
+ }
557
+ const data = await response.json();
558
+ return data.response;
559
+ };
560
+ const processUserInput = async (input, options, outputMode) => {
561
+ const { memoryId, instruction } = await resolveMemoryId({ memoryId: options.memoryId });
562
+ const response = await callAskAPI(input, memoryId);
563
+ if (outputMode === "cli") {
564
+ console.log(response);
565
+ if (instruction) {
566
+ console.log(`
567
+ ${instruction}`);
568
+ }
569
+ }
570
+ addMemories(input, { memoryId }).catch((err) => {
339
571
  console.error("Failed to save memory:", err);
340
572
  });
341
- return { response, instruction: memoryContext.instruction, memoryId: memoryContext.memoryId };
573
+ return { response, instruction, memoryId };
342
574
  };
343
575
  program.argument("[prompt]", "initial prompt for chat session (optional)").action(async (initialPrompt, options) => {
344
576
  try {
345
- let memoryId = options.memoryId;
346
- if (!process.stdin.isTTY) {
577
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
347
578
  if (initialPrompt) {
348
- await processUserInput(initialPrompt, options, false);
579
+ const info2 = await getInfoDisplay();
580
+ console.log(info2.fullLine);
581
+ await processUserInput(initialPrompt, options, "cli");
349
582
  }
350
583
  return;
351
584
  }
352
- if (initialPrompt) {
353
- const result = await processUserInput(initialPrompt, options, true);
354
- memoryId = result.memoryId;
355
- }
356
- const { createInterface } = await import("readline");
357
- const rl = createInterface({
358
- input: process.stdin,
359
- output: process.stdout
585
+ const info = await getInfoDisplay();
586
+ await runInteractiveApp({
587
+ initialPrompt,
588
+ options,
589
+ processUserInput,
590
+ infoLine: info.fullLine,
591
+ infoDisplay: {
592
+ usageLine: info.usageLine,
593
+ versionLine: info.versionLine,
594
+ modelLine: info.modelLine
595
+ }
360
596
  });
361
- const chatLoop = async () => {
362
- rl.question(chalk.cyan("> "), async (input) => {
363
- if (input.toLowerCase() === "exit" || input.toLowerCase() === "quit") {
364
- console.log("Goodbye!");
365
- rl.close();
366
- return;
367
- }
368
- if (!input.trim()) {
369
- chatLoop();
370
- return;
371
- }
372
- try {
373
- const result = await processUserInput(input, { ...options, memoryId }, true);
374
- memoryId = result.memoryId;
375
- } catch (error) {
376
- console.error("Chat error:", error instanceof Error ? error.message : error);
377
- }
378
- chatLoop();
379
- });
380
- };
381
- console.log(chalk.magenta("\nSatori is memory for AI. It remembers your sessions forever. Think of it like 'infinite context' for AI.\n"));
382
- console.log(chalk.cyan("You're in interactive mode. Use interactive mode just like you would use ChatGPT."));
383
- console.log(chalk.cyan('Type "exit" or "quit" to end the session.\n'));
384
- chatLoop();
385
597
  } catch (error) {
386
598
  console.error("Chat error:", error instanceof Error ? error.message : error);
387
599
  process.exit(1);
@@ -393,10 +605,51 @@ ${memoryContext.instruction}`);
393
605
  program.command("search").description("search memories").argument("<query>", "search query for memories").action(async (query) => {
394
606
  await searchMemories(query);
395
607
  });
608
+ program.command("sub").description("open the subscription checkout").action(async () => {
609
+ const config = await getConfig();
610
+ const checkoutUrlRaw = config.checkoutUrl;
611
+ if (!checkoutUrlRaw) {
612
+ console.error("SATORI_CHECKOUT_URL is not set");
613
+ process.exit(1);
614
+ }
615
+ let checkoutUrl;
616
+ try {
617
+ checkoutUrl = new URL(checkoutUrlRaw).toString();
618
+ } catch {
619
+ console.error("SATORI_CHECKOUT_URL is not a valid URL");
620
+ process.exit(1);
621
+ }
622
+ if (process.platform !== "darwin") {
623
+ console.log(checkoutUrl);
624
+ return;
625
+ }
626
+ await new Promise((resolve) => {
627
+ execFile("open", [checkoutUrl], (error) => {
628
+ if (error) {
629
+ console.log(checkoutUrl);
630
+ }
631
+ resolve();
632
+ });
633
+ });
634
+ });
635
+ program.command("get").description("get config value (e.g. `get api-key`)").argument("<key>", "key to get").action(async (key) => {
636
+ if (key !== "api-key") {
637
+ console.error("Unknown key");
638
+ process.exit(1);
639
+ }
640
+ const apiKey = await getStoredApiKey();
641
+ if (!apiKey) {
642
+ console.error("API key not found in config");
643
+ process.exit(1);
644
+ }
645
+ console.log(chalk.magenta(`
646
+ ${apiKey}
647
+ `));
648
+ });
396
649
  program.parse();
397
650
  }
398
651
  var entryPath = process.argv[1] ? realpathSync(process.argv[1]) : "";
399
- var modulePath = realpathSync(fileURLToPath(import.meta.url));
652
+ var modulePath = realpathSync(fileURLToPath2(import.meta.url));
400
653
  if (entryPath === modulePath) {
401
654
  main();
402
655
  }