@satori-sh/cli 0.0.13 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/index.js +372 -65
- package/logos/chars.ans +2 -0
- package/logos/s.ans +2 -0
- package/logos/satori.ans +2 -0
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -37,6 +37,13 @@ Add new memories:
|
|
|
37
37
|
satori add "I like pizza"
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
### Subscribe
|
|
41
|
+
|
|
42
|
+
Open checkout in the browser:
|
|
43
|
+
```bash
|
|
44
|
+
npx @satori-sh sub
|
|
45
|
+
```
|
|
46
|
+
|
|
40
47
|
**Options & Memory:**
|
|
41
48
|
- `--memory-id <id>` (scopes conversations)
|
|
42
49
|
|
|
@@ -53,8 +60,16 @@ satori chat "Follow up question"
|
|
|
53
60
|
|
|
54
61
|
**Optional:**
|
|
55
62
|
- `SATORI_BASE_URL` (default: http://localhost:8000)
|
|
63
|
+
- `SATORI_CHECKOUT_URL` - Stripe checkout link for `npx @satori-sh sub`
|
|
56
64
|
- `SATORI_MEMORY_ID` - Session scoping
|
|
57
65
|
- `SATORI_MOCK` - Enable mock mode
|
|
66
|
+
- `OPENAI_API_KEY` - Pass-through OpenAI key for `/ask` (sent as `X-OpenAI-Key`)
|
|
67
|
+
- `NODE_ENV` - Set to `development` to default to `http://localhost:8000`
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
```bash
|
|
71
|
+
NODE_ENV=development bun run src/index.ts
|
|
72
|
+
```
|
|
58
73
|
|
|
59
74
|
## Troubleshooting
|
|
60
75
|
|
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 {
|
|
7
|
-
import {
|
|
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
|
|
@@ -73,6 +74,7 @@ async function getConfig() {
|
|
|
73
74
|
}
|
|
74
75
|
let apiKey = null;
|
|
75
76
|
let memoryId = void 0;
|
|
77
|
+
const openaiApiKey = process.env.OPENAI_API_KEY || null;
|
|
76
78
|
try {
|
|
77
79
|
const data = await loadConfigFile();
|
|
78
80
|
if (typeof data.api_key === "string") {
|
|
@@ -86,12 +88,21 @@ async function getConfig() {
|
|
|
86
88
|
if (!apiKey) {
|
|
87
89
|
apiKey = process.env.SATORI_API_KEY || null;
|
|
88
90
|
}
|
|
89
|
-
const
|
|
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");
|
|
90
94
|
try {
|
|
91
95
|
new URL(baseUrl);
|
|
92
96
|
} catch {
|
|
93
97
|
throw new Error("Invalid SATORI_BASE_URL format");
|
|
94
98
|
}
|
|
99
|
+
if (checkoutUrl) {
|
|
100
|
+
try {
|
|
101
|
+
new URL(checkoutUrl);
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error("Invalid SATORI_CHECKOUT_URL format");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
95
106
|
if (!apiKey) {
|
|
96
107
|
try {
|
|
97
108
|
const response = await fetch(`${baseUrl}/orgs`, {
|
|
@@ -114,7 +125,7 @@ async function getConfig() {
|
|
|
114
125
|
if (!memoryId) {
|
|
115
126
|
memoryId = process.env.SATORI_MEMORY_ID;
|
|
116
127
|
}
|
|
117
|
-
return { apiKey, baseUrl, memoryId };
|
|
128
|
+
return { apiKey, openaiApiKey, baseUrl, checkoutUrl, memoryId };
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
// src/search.ts
|
|
@@ -133,6 +144,10 @@ async function searchMemories(query) {
|
|
|
133
144
|
},
|
|
134
145
|
body: JSON.stringify({ query })
|
|
135
146
|
});
|
|
147
|
+
if (response.status === 402) {
|
|
148
|
+
console.error("Free tier exceeded. Run `npx @satori-sh sub` to subscribe");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
136
151
|
if (!response.ok) {
|
|
137
152
|
console.error(`HTTP error: ${response.status} ${response.statusText}`);
|
|
138
153
|
return;
|
|
@@ -159,6 +174,10 @@ async function addMemories(text, options = {}) {
|
|
|
159
174
|
},
|
|
160
175
|
body: JSON.stringify({ messages: [{ role: "user", content: text }], ...options.memoryId && { memory_id: options.memoryId } })
|
|
161
176
|
});
|
|
177
|
+
if (response.status === 402) {
|
|
178
|
+
console.error("Free tier exceeded. Run `npx @satori-sh sub` to subscribe");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
162
181
|
if (!response.ok) {
|
|
163
182
|
console.error(`HTTP error: ${response.status} ${response.statusText}`);
|
|
164
183
|
return;
|
|
@@ -208,10 +227,260 @@ ${memoryText}`
|
|
|
208
227
|
return [systemMessage, ...messages];
|
|
209
228
|
}
|
|
210
229
|
|
|
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");
|
|
243
|
+
}
|
|
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);
|
|
302
|
+
};
|
|
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
|
+
}
|
|
335
|
+
};
|
|
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
|
+
}
|
|
350
|
+
});
|
|
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
|
+
));
|
|
478
|
+
}
|
|
479
|
+
|
|
211
480
|
// src/index.ts
|
|
212
481
|
async function main() {
|
|
213
482
|
const argv = process.argv.slice(2);
|
|
214
|
-
const isGetApiKey = argv[0] === "get" && argv[1] === "
|
|
483
|
+
const isGetApiKey = argv[0] === "get" && argv[1] === "api-key";
|
|
215
484
|
if (!isGetApiKey) {
|
|
216
485
|
try {
|
|
217
486
|
await getConfig();
|
|
@@ -220,44 +489,83 @@ async function main() {
|
|
|
220
489
|
process.exit(1);
|
|
221
490
|
}
|
|
222
491
|
}
|
|
223
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
224
|
-
const logoPath = join2(__dirname, "..", "logo.txt");
|
|
225
|
-
console.log(chalk.cyan(readFileSync(logoPath, "utf8")));
|
|
226
492
|
const program = new Command();
|
|
227
493
|
program.name("satori").description("CLI tool for Satori memory server").version("0.0.1");
|
|
228
494
|
program.option("--memory-id <id>", "Memory ID for scoping");
|
|
229
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 () => {
|
|
504
|
+
try {
|
|
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;
|
|
518
|
+
}
|
|
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
|
+
};
|
|
230
532
|
const callAskAPI = async (prompt, memoryId) => {
|
|
231
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;
|
|
540
|
+
}
|
|
232
541
|
const response = await fetch(`${config.baseUrl}/ask`, {
|
|
233
542
|
method: "POST",
|
|
234
|
-
headers
|
|
235
|
-
"Content-Type": "application/json",
|
|
236
|
-
"Authorization": `Bearer ${config.apiKey}`
|
|
237
|
-
},
|
|
543
|
+
headers,
|
|
238
544
|
body: JSON.stringify({
|
|
239
545
|
prompt,
|
|
240
546
|
memory_id: memoryId,
|
|
241
547
|
llm_model: DEFAULT_LLM_MODEL
|
|
242
548
|
})
|
|
243
549
|
});
|
|
550
|
+
if (response.status === 402) {
|
|
551
|
+
console.error("Free tier exceeded. Run `npx @satori-sh sub` to subscribe");
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
244
554
|
if (!response.ok) {
|
|
245
555
|
throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
|
|
246
556
|
}
|
|
247
557
|
const data = await response.json();
|
|
248
558
|
return data.response;
|
|
249
559
|
};
|
|
250
|
-
const processUserInput = async (input, options,
|
|
560
|
+
const processUserInput = async (input, options, outputMode) => {
|
|
251
561
|
const { memoryId, instruction } = await resolveMemoryId({ memoryId: options.memoryId });
|
|
252
562
|
const response = await callAskAPI(input, memoryId);
|
|
253
|
-
if (
|
|
254
|
-
console.log(`Assistant: ${response}`);
|
|
255
|
-
} else {
|
|
563
|
+
if (outputMode === "cli") {
|
|
256
564
|
console.log(response);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
console.log(`
|
|
565
|
+
if (instruction) {
|
|
566
|
+
console.log(`
|
|
260
567
|
${instruction}`);
|
|
568
|
+
}
|
|
261
569
|
}
|
|
262
570
|
addMemories(input, { memoryId }).catch((err) => {
|
|
263
571
|
console.error("Failed to save memory:", err);
|
|
@@ -266,54 +574,26 @@ ${instruction}`);
|
|
|
266
574
|
};
|
|
267
575
|
program.argument("[prompt]", "initial prompt for chat session (optional)").action(async (initialPrompt, options) => {
|
|
268
576
|
try {
|
|
269
|
-
|
|
270
|
-
if (!process.stdin.isTTY) {
|
|
577
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
271
578
|
if (initialPrompt) {
|
|
272
|
-
await
|
|
579
|
+
const info2 = await getInfoDisplay();
|
|
580
|
+
console.log(info2.fullLine);
|
|
581
|
+
await processUserInput(initialPrompt, options, "cli");
|
|
273
582
|
}
|
|
274
583
|
return;
|
|
275
584
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (!memoryId) {
|
|
287
|
-
const resolved = await resolveMemoryId({ memoryId: options.memoryId });
|
|
288
|
-
memoryId = resolved.memoryId;
|
|
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
|
|
289
595
|
}
|
|
290
|
-
|
|
291
|
-
};
|
|
292
|
-
const chatLoop = async () => {
|
|
293
|
-
const prompt = await getPrompt();
|
|
294
|
-
rl.question(prompt, async (input) => {
|
|
295
|
-
if (input.toLowerCase() === "exit" || input.toLowerCase() === "quit") {
|
|
296
|
-
console.log("Goodbye!");
|
|
297
|
-
rl.close();
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
if (!input.trim()) {
|
|
301
|
-
chatLoop();
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
try {
|
|
305
|
-
const result = await processUserInput(input, { ...options, memoryId }, true);
|
|
306
|
-
memoryId = result.memoryId;
|
|
307
|
-
} catch (error) {
|
|
308
|
-
console.error("Chat error:", error instanceof Error ? error.message : error);
|
|
309
|
-
}
|
|
310
|
-
chatLoop();
|
|
311
|
-
});
|
|
312
|
-
};
|
|
313
|
-
console.log(chalk.magenta("\nSatori is memory for AI. It remembers your sessions forever. Think of it like 'infinite context' for AI.\n"));
|
|
314
|
-
console.log(chalk.cyan("You're in interactive mode. Use interactive mode just like you would use ChatGPT."));
|
|
315
|
-
console.log(chalk.cyan('Type "exit" or "quit" to end the session.\n'));
|
|
316
|
-
chatLoop();
|
|
596
|
+
});
|
|
317
597
|
} catch (error) {
|
|
318
598
|
console.error("Chat error:", error instanceof Error ? error.message : error);
|
|
319
599
|
process.exit(1);
|
|
@@ -325,8 +605,35 @@ ${instruction}`);
|
|
|
325
605
|
program.command("search").description("search memories").argument("<query>", "search query for memories").action(async (query) => {
|
|
326
606
|
await searchMemories(query);
|
|
327
607
|
});
|
|
328
|
-
program.command("
|
|
329
|
-
|
|
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") {
|
|
330
637
|
console.error("Unknown key");
|
|
331
638
|
process.exit(1);
|
|
332
639
|
}
|
|
@@ -342,7 +649,7 @@ ${instruction}`);
|
|
|
342
649
|
program.parse();
|
|
343
650
|
}
|
|
344
651
|
var entryPath = process.argv[1] ? realpathSync(process.argv[1]) : "";
|
|
345
|
-
var modulePath = realpathSync(
|
|
652
|
+
var modulePath = realpathSync(fileURLToPath2(import.meta.url));
|
|
346
653
|
if (entryPath === modulePath) {
|
|
347
654
|
main();
|
|
348
655
|
}
|