@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/README.md +17 -12
- package/dist/index.js +416 -163
- package/logos/chars.ans +2 -0
- package/logos/s.ans +2 -0
- package/logos/satori.ans +2 -0
- package/package.json +8 -4
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
|
|
@@ -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
|
|
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,
|
|
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
|
|
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/
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
"
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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("--
|
|
316
|
-
const
|
|
317
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
346
|
-
if (!process.stdin.isTTY) {
|
|
577
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
347
578
|
if (initialPrompt) {
|
|
348
|
-
await
|
|
579
|
+
const info2 = await getInfoDisplay();
|
|
580
|
+
console.log(info2.fullLine);
|
|
581
|
+
await processUserInput(initialPrompt, options, "cli");
|
|
349
582
|
}
|
|
350
583
|
return;
|
|
351
584
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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(
|
|
652
|
+
var modulePath = realpathSync(fileURLToPath2(import.meta.url));
|
|
400
653
|
if (entryPath === modulePath) {
|
|
401
654
|
main();
|
|
402
655
|
}
|