@datename/fastcode-termux 0.0.0-stage → 0.1.6
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/LICENSE +21 -0
- package/README.md +400 -2
- package/bin/fastcode.mjs +59 -0
- package/dist/agent/guards.d.ts +35 -0
- package/dist/agent/guards.js +74 -0
- package/dist/agent/limits.d.ts +24 -0
- package/dist/agent/limits.js +71 -0
- package/dist/agent/protocol.d.ts +4 -0
- package/dist/agent/protocol.js +126 -0
- package/dist/agent/tools.d.ts +1 -0
- package/dist/agent/tools.js +377 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.js +476 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +518 -0
- package/dist/commands/about.d.ts +8 -0
- package/dist/commands/about.js +16 -0
- package/dist/commands/agent.d.ts +11 -0
- package/dist/commands/agent.js +18 -0
- package/dist/commands/clear.d.ts +11 -0
- package/dist/commands/clear.js +9 -0
- package/dist/commands/config.d.ts +7 -0
- package/dist/commands/config.js +24 -0
- package/dist/commands/cost.d.ts +9 -0
- package/dist/commands/cost.js +13 -0
- package/dist/commands/cwd.d.ts +7 -0
- package/dist/commands/cwd.js +7 -0
- package/dist/commands/exit.d.ts +9 -0
- package/dist/commands/exit.js +8 -0
- package/dist/commands/help.d.ts +6 -0
- package/dist/commands/help.js +18 -0
- package/dist/commands/index.d.ts +9 -0
- package/dist/commands/index.js +58 -0
- package/dist/commands/login.d.ts +16 -0
- package/dist/commands/login.js +61 -0
- package/dist/commands/logout.d.ts +9 -0
- package/dist/commands/logout.js +53 -0
- package/dist/commands/model.d.ts +11 -0
- package/dist/commands/model.js +30 -0
- package/dist/commands/plugin.d.ts +11 -0
- package/dist/commands/plugin.js +74 -0
- package/dist/commands/provider.d.ts +11 -0
- package/dist/commands/provider.js +28 -0
- package/dist/commands/reset.d.ts +9 -0
- package/dist/commands/reset.js +13 -0
- package/dist/commands/skills.d.ts +11 -0
- package/dist/commands/skills.js +106 -0
- package/dist/commands/system.d.ts +11 -0
- package/dist/commands/system.js +20 -0
- package/dist/commands/theme.d.ts +11 -0
- package/dist/commands/theme.js +22 -0
- package/dist/commands/tools.d.ts +7 -0
- package/dist/commands/tools.js +29 -0
- package/dist/commands/whoami.d.ts +9 -0
- package/dist/commands/whoami.js +51 -0
- package/dist/config/paths.d.ts +4 -0
- package/dist/config/paths.js +59 -0
- package/dist/config/store.d.ts +69 -0
- package/dist/config/store.js +208 -0
- package/dist/config/themes.d.ts +73 -0
- package/dist/config/themes.js +77 -0
- package/dist/config/trust.d.ts +9 -0
- package/dist/config/trust.js +67 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +31 -0
- package/dist/lib/diagnostics.d.ts +41 -0
- package/dist/lib/diagnostics.js +139 -0
- package/dist/lib/http.d.ts +13 -0
- package/dist/lib/http.js +70 -0
- package/dist/lib/markdown.d.ts +1 -0
- package/dist/lib/markdown.js +32 -0
- package/dist/lib/pkg.d.ts +5 -0
- package/dist/lib/pkg.js +27 -0
- package/dist/lib/platform.d.ts +7 -0
- package/dist/lib/platform.js +57 -0
- package/dist/lib/startup.d.ts +6 -0
- package/dist/lib/startup.js +25 -0
- package/dist/lib/uiMode.d.ts +2 -0
- package/dist/lib/uiMode.js +7 -0
- package/dist/plugins/manager.d.ts +23 -0
- package/dist/plugins/manager.js +101 -0
- package/dist/providers/anthropic.d.ts +21 -0
- package/dist/providers/anthropic.js +107 -0
- package/dist/providers/custom.d.ts +24 -0
- package/dist/providers/custom.js +25 -0
- package/dist/providers/deepseek.d.ts +24 -0
- package/dist/providers/deepseek.js +15 -0
- package/dist/providers/google.d.ts +24 -0
- package/dist/providers/google.js +19 -0
- package/dist/providers/groq.d.ts +24 -0
- package/dist/providers/groq.js +20 -0
- package/dist/providers/index.d.ts +201 -0
- package/dist/providers/index.js +37 -0
- package/dist/providers/mistral.d.ts +24 -0
- package/dist/providers/mistral.js +19 -0
- package/dist/providers/moonshot.d.ts +24 -0
- package/dist/providers/moonshot.js +22 -0
- package/dist/providers/ollama.d.ts +24 -0
- package/dist/providers/ollama.js +22 -0
- package/dist/providers/openai.d.ts +24 -0
- package/dist/providers/openai.js +25 -0
- package/dist/providers/openaiCompatible.d.ts +17 -0
- package/dist/providers/openaiCompatible.js +110 -0
- package/dist/providers/openrouter.d.ts +24 -0
- package/dist/providers/openrouter.js +26 -0
- package/dist/providers/qwen.d.ts +24 -0
- package/dist/providers/qwen.js +23 -0
- package/dist/providers/xai.d.ts +24 -0
- package/dist/providers/xai.js +18 -0
- package/dist/providers/zhipu.d.ts +24 -0
- package/dist/providers/zhipu.js +23 -0
- package/dist/session/log.d.ts +29 -0
- package/dist/session/log.js +135 -0
- package/dist/skills/manager.d.ts +18 -0
- package/dist/skills/manager.js +201 -0
- package/dist/skills/parser.d.ts +18 -0
- package/dist/skills/parser.js +65 -0
- package/dist/types.d.ts +146 -0
- package/dist/types.js +1 -0
- package/dist/ui/Banner.d.ts +5 -0
- package/dist/ui/Banner.js +65 -0
- package/dist/ui/Message.d.ts +7 -0
- package/dist/ui/Message.js +13 -0
- package/dist/ui/Prompt.d.ts +10 -0
- package/dist/ui/Prompt.js +38 -0
- package/dist/ui/Spinner.d.ts +5 -0
- package/dist/ui/Spinner.js +33 -0
- package/dist/ui/StatusBar.d.ts +10 -0
- package/dist/ui/StatusBar.js +19 -0
- package/dist/ui/TrustGate.d.ts +5 -0
- package/dist/ui/TrustGate.js +26 -0
- package/package.json +96 -4
- package/src/agent/guards.ts +74 -0
- package/src/agent/limits.ts +71 -0
- package/src/agent/protocol.ts +126 -0
- package/src/agent/tools.ts +377 -0
- package/src/app.ts +476 -0
- package/src/cli.ts +308 -0
- package/src/commands/about.ts +16 -0
- package/src/commands/agent.ts +18 -0
- package/src/commands/clear.ts +9 -0
- package/src/commands/config.ts +24 -0
- package/src/commands/cost.ts +13 -0
- package/src/commands/cwd.ts +7 -0
- package/src/commands/exit.ts +8 -0
- package/src/commands/help.ts +18 -0
- package/src/commands/index.ts +58 -0
- package/src/commands/login.ts +61 -0
- package/src/commands/logout.ts +53 -0
- package/src/commands/model.ts +30 -0
- package/src/commands/plugin.ts +74 -0
- package/src/commands/provider.ts +28 -0
- package/src/commands/reset.ts +13 -0
- package/src/commands/skills.ts +106 -0
- package/src/commands/system.ts +20 -0
- package/src/commands/theme.ts +22 -0
- package/src/commands/tools.ts +29 -0
- package/src/commands/whoami.ts +51 -0
- package/src/config/paths.ts +59 -0
- package/src/config/store.ts +240 -0
- package/src/config/themes.ts +77 -0
- package/src/config/trust.ts +67 -0
- package/src/index.ts +31 -0
- package/src/lib/diagnostics.ts +139 -0
- package/src/lib/http.ts +70 -0
- package/src/lib/markdown.ts +32 -0
- package/src/lib/pkg.ts +35 -0
- package/src/lib/platform.ts +57 -0
- package/src/lib/startup.ts +25 -0
- package/src/lib/uiMode.ts +7 -0
- package/src/plugins/manager.ts +101 -0
- package/src/providers/anthropic.ts +107 -0
- package/src/providers/custom.ts +25 -0
- package/src/providers/deepseek.ts +15 -0
- package/src/providers/google.ts +19 -0
- package/src/providers/groq.ts +20 -0
- package/src/providers/index.ts +37 -0
- package/src/providers/mistral.ts +19 -0
- package/src/providers/moonshot.ts +22 -0
- package/src/providers/ollama.ts +22 -0
- package/src/providers/openai.ts +25 -0
- package/src/providers/openaiCompatible.ts +110 -0
- package/src/providers/openrouter.ts +26 -0
- package/src/providers/qwen.ts +23 -0
- package/src/providers/xai.ts +18 -0
- package/src/providers/zhipu.ts +23 -0
- package/src/session/log.ts +135 -0
- package/src/skills/manager.ts +201 -0
- package/src/skills/parser.ts +65 -0
- package/src/types.ts +143 -0
- package/src/ui/Banner.ts +68 -0
- package/src/ui/Message.ts +13 -0
- package/src/ui/Prompt.ts +38 -0
- package/src/ui/Spinner.ts +33 -0
- package/src/ui/StatusBar.ts +19 -0
- package/src/ui/TrustGate.ts +26 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { streamSSE } from "../lib/http.js";
|
|
2
|
+
const STATIC_MODELS = [
|
|
3
|
+
{ id: "claude-sonnet-5.5", label: "Claude Sonnet 5.5", contextWindow: 1000000, pricing: { inputUsdPerM: 3, outputUsdPerM: 15 } },
|
|
4
|
+
{ id: "claude-opus-5.5", label: "Claude Opus 5.5", contextWindow: 1000000, pricing: { inputUsdPerM: 15, outputUsdPerM: 75 } },
|
|
5
|
+
{ id: "claude-haiku-5.5", label: "Claude Haiku 5.5", contextWindow: 200000, pricing: { inputUsdPerM: 0.8, outputUsdPerM: 4 } },
|
|
6
|
+
{ id: "claude-sonnet-5", label: "Claude Sonnet 5" },
|
|
7
|
+
{ id: "claude-opus-5", label: "Claude Opus 5" },
|
|
8
|
+
{ id: "claude-sonnet-4.6", label: "Claude Sonnet 4.6" },
|
|
9
|
+
{ id: "claude-opus-4.6", label: "Claude Opus 4.6" },
|
|
10
|
+
{ id: "claude-haiku-4.5", label: "Claude Haiku 4.5" },
|
|
11
|
+
];
|
|
12
|
+
const BASE = "https://api.anthropic.com/v1";
|
|
13
|
+
export const anthropicProvider = {
|
|
14
|
+
id: "anthropic",
|
|
15
|
+
label: "Anthropic (Claude)",
|
|
16
|
+
authEnv: ["ANTHROPIC_API_KEY", "CLAUDE_API_KEY"],
|
|
17
|
+
baseUrl: BASE,
|
|
18
|
+
defaultModel: "claude-sonnet-5.5",
|
|
19
|
+
async listModels(apiKey) {
|
|
20
|
+
// Query the live model list; fall back to the built-in list offline.
|
|
21
|
+
if (apiKey) {
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(BASE + "/models", {
|
|
24
|
+
headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
25
|
+
});
|
|
26
|
+
if (res.ok) {
|
|
27
|
+
const json: any = await res.json();
|
|
28
|
+
const ids = (json.data ?? [])
|
|
29
|
+
.map((m) => m.id)
|
|
30
|
+
.filter((id) => typeof id === "string" && id.length > 0);
|
|
31
|
+
if (ids.length > 0) {
|
|
32
|
+
const known = new Map(STATIC_MODELS.map((m) => [m.id, m]));
|
|
33
|
+
return ids.map((id) => known.get(id) ?? { id });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// network/auth issue → fall through to the static list
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return STATIC_MODELS;
|
|
42
|
+
},
|
|
43
|
+
async *stream(req, apiKey) {
|
|
44
|
+
if (!apiKey) {
|
|
45
|
+
throw new Error("Missing ANTHROPIC_API_KEY. Bind one with /login anthropic <model> <key>, or set the env var.");
|
|
46
|
+
}
|
|
47
|
+
const messages = req.messages.map((m) => ({ role: m.role, content: m.content }));
|
|
48
|
+
const body = {
|
|
49
|
+
model: req.model,
|
|
50
|
+
messages,
|
|
51
|
+
...(req.system ? { system: req.system } : {}),
|
|
52
|
+
stream: true,
|
|
53
|
+
max_tokens: req.maxOutputTokens ?? 4096,
|
|
54
|
+
...(req.temperature !== undefined ? { temperature: req.temperature } : {}),
|
|
55
|
+
};
|
|
56
|
+
const res = await fetch(BASE + "/messages", {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
"content-type": "application/json",
|
|
60
|
+
"x-api-key": apiKey,
|
|
61
|
+
"anthropic-version": "2023-06-01",
|
|
62
|
+
accept: "text/event-stream",
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify(body),
|
|
65
|
+
signal: req.signal,
|
|
66
|
+
});
|
|
67
|
+
let usageInput = 0;
|
|
68
|
+
let usageOutput = 0;
|
|
69
|
+
for await (const ev of streamSSE(res)) {
|
|
70
|
+
let data;
|
|
71
|
+
try {
|
|
72
|
+
data = JSON.parse(ev.data);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const t = data.type;
|
|
78
|
+
if (t === "content_block_delta") {
|
|
79
|
+
const delta = data.delta?.text;
|
|
80
|
+
if (delta)
|
|
81
|
+
yield { delta, done: false };
|
|
82
|
+
}
|
|
83
|
+
else if (t === "message_start") {
|
|
84
|
+
const u = data.message?.usage;
|
|
85
|
+
if (u?.input_tokens)
|
|
86
|
+
usageInput = u.input_tokens;
|
|
87
|
+
}
|
|
88
|
+
else if (t === "message_delta") {
|
|
89
|
+
const u = data.usage;
|
|
90
|
+
if (u?.output_tokens)
|
|
91
|
+
usageOutput = u.output_tokens;
|
|
92
|
+
}
|
|
93
|
+
else if (t === "message_stop") {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const cost = priceFor(req.model, usageInput, usageOutput);
|
|
98
|
+
yield { delta: "", done: true, usage: { inputTokens: usageInput, outputTokens: usageOutput, costUsd: cost } };
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
function priceFor(modelId, input, output) {
|
|
102
|
+
const m = STATIC_MODELS.find((x) => x.id === modelId);
|
|
103
|
+
const p = m?.pricing;
|
|
104
|
+
if (!p?.inputUsdPerM || !p.outputUsdPerM)
|
|
105
|
+
return undefined;
|
|
106
|
+
return (input / 1000000) * p.inputUsdPerM + (output / 1000000) * p.outputUsdPerM;
|
|
107
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { listModelsHttp, streamChatCompletion } from "./openaiCompatible.js";
|
|
2
|
+
function baseUrl() {
|
|
3
|
+
return (process.env["FASTCODE_BASE_URL"] ?? "").trim().replace(/[/]+$/, "");
|
|
4
|
+
}
|
|
5
|
+
function defaultModel() {
|
|
6
|
+
return (process.env["FASTCODE_MODEL"] ?? "").trim() || "gpt-4o-mini";
|
|
7
|
+
}
|
|
8
|
+
function requireBaseUrl() {
|
|
9
|
+
const base = baseUrl();
|
|
10
|
+
if (!base) {
|
|
11
|
+
throw new Error("FASTCODE_BASE_URL is not set. Example: export FASTCODE_BASE_URL=https://api.example.com/v1");
|
|
12
|
+
}
|
|
13
|
+
return base;
|
|
14
|
+
}
|
|
15
|
+
export const customProvider = {
|
|
16
|
+
id: "custom",
|
|
17
|
+
label: "Custom (OpenAI-compatible)",
|
|
18
|
+
authEnv: [],
|
|
19
|
+
get baseUrl() {
|
|
20
|
+
return baseUrl();
|
|
21
|
+
},
|
|
22
|
+
defaultModel: defaultModel(),
|
|
23
|
+
listModels: () => listModelsHttp({ baseUrl: requireBaseUrl(), apiKey: process.env["FASTCODE_API_KEY"], staticModels: [] }),
|
|
24
|
+
stream: (req) => streamChatCompletion(req, { baseUrl: requireBaseUrl(), apiKey: process.env["FASTCODE_API_KEY"], staticModels: [] }),
|
|
25
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { listModelsHttp, streamChatCompletion } from "./openaiCompatible.js";
|
|
2
|
+
const BASE = "https://api.deepseek.com/v1";
|
|
3
|
+
const STATIC = [
|
|
4
|
+
{ id: "deepseek-v4-pro", label: "DeepSeek V4 Pro" },
|
|
5
|
+
{ id: "deepseek-flash", label: "DeepSeek Flash" },
|
|
6
|
+
];
|
|
7
|
+
export const deepseekProvider = {
|
|
8
|
+
id: "deepseek",
|
|
9
|
+
label: "DeepSeek",
|
|
10
|
+
authEnv: ["DEEPSEEK_API_KEY"],
|
|
11
|
+
baseUrl: BASE,
|
|
12
|
+
defaultModel: "deepseek-v4-pro",
|
|
13
|
+
listModels: (apiKey) => listModelsHttp({ baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
14
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
15
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { listModelsHttp, streamChatCompletion } from "./openaiCompatible.js";
|
|
2
|
+
const BASE = "https://generativelanguage.googleapis.com/v1beta/openai";
|
|
3
|
+
const STATIC = [
|
|
4
|
+
{ id: "gemini-3.8-flash", label: "Gemini 3.8 Flash" },
|
|
5
|
+
{ id: "gemini-3.7-flash", label: "Gemini 3.7 Flash" },
|
|
6
|
+
{ id: "gemini-3.5-flash", label: "Gemini 3.5 Flash" },
|
|
7
|
+
{ id: "gemini-3.5-flash-lite", label: "Gemini 3.5 Flash Lite" },
|
|
8
|
+
{ id: "gemini-3.1-flash-lite", label: "Gemini 3.1 Flash Lite" },
|
|
9
|
+
{ id: "gemini-4-argon", label: "Gemini 4 Argon" },
|
|
10
|
+
];
|
|
11
|
+
export const googleProvider = {
|
|
12
|
+
id: "google",
|
|
13
|
+
label: "Google Gemini",
|
|
14
|
+
authEnv: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
|
15
|
+
baseUrl: BASE,
|
|
16
|
+
defaultModel: "gemini-3.8-flash",
|
|
17
|
+
listModels: (apiKey) => listModelsHttp({ baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
18
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
19
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { listModelsHttp, streamChatCompletion } from "./openaiCompatible.js";
|
|
2
|
+
const BASE = "https://api.groq.com/openai/v1";
|
|
3
|
+
const STATIC = [
|
|
4
|
+
{ id: "llama-3.3-70b-versatile", label: "Llama 3.3 70B" },
|
|
5
|
+
{ id: "meta-llama/llama-4-maverick-17b-128e-instruct", label: "Llama 4 Maverick" },
|
|
6
|
+
{ id: "meta-llama/llama-4-scout-17b-16e-instruct", label: "Llama 4 Scout" },
|
|
7
|
+
{ id: "moonshotai/kimi-k2-instruct", label: "Kimi K2" },
|
|
8
|
+
{ id: "openai/gpt-oss-120b", label: "GPT-OSS 120B" },
|
|
9
|
+
{ id: "qwen/qwen3-32b", label: "Qwen3 32B" },
|
|
10
|
+
{ id: "llama-3.1-8b-instant", label: "Llama 3.1 8B instant" },
|
|
11
|
+
];
|
|
12
|
+
export const groqProvider = {
|
|
13
|
+
id: "groq",
|
|
14
|
+
label: "Groq",
|
|
15
|
+
authEnv: ["GROQ_API_KEY"],
|
|
16
|
+
baseUrl: BASE,
|
|
17
|
+
defaultModel: "llama-3.3-70b-versatile",
|
|
18
|
+
listModels: (apiKey) => listModelsHttp({ baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
19
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
20
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { anthropicProvider } from "./anthropic.js";
|
|
2
|
+
import { openaiProvider } from "./openai.js";
|
|
3
|
+
import { googleProvider } from "./google.js";
|
|
4
|
+
import { openrouterProvider } from "./openrouter.js";
|
|
5
|
+
import { groqProvider } from "./groq.js";
|
|
6
|
+
import { deepseekProvider } from "./deepseek.js";
|
|
7
|
+
import { mistralProvider } from "./mistral.js";
|
|
8
|
+
import { ollamaProvider } from "./ollama.js";
|
|
9
|
+
import { xaiProvider } from "./xai.js";
|
|
10
|
+
import { moonshotProvider } from "./moonshot.js";
|
|
11
|
+
import { zhipuProvider } from "./zhipu.js";
|
|
12
|
+
import { qwenProvider } from "./qwen.js";
|
|
13
|
+
import { customProvider } from "./custom.js";
|
|
14
|
+
export const PROVIDERS = [
|
|
15
|
+
anthropicProvider,
|
|
16
|
+
openaiProvider,
|
|
17
|
+
googleProvider,
|
|
18
|
+
deepseekProvider,
|
|
19
|
+
openrouterProvider,
|
|
20
|
+
groqProvider,
|
|
21
|
+
mistralProvider,
|
|
22
|
+
xaiProvider,
|
|
23
|
+
moonshotProvider,
|
|
24
|
+
zhipuProvider,
|
|
25
|
+
qwenProvider,
|
|
26
|
+
ollamaProvider,
|
|
27
|
+
customProvider,
|
|
28
|
+
];
|
|
29
|
+
export function getProvider(id) {
|
|
30
|
+
return PROVIDERS.find((p) => p.id === id);
|
|
31
|
+
}
|
|
32
|
+
export function getProviderOrThrow(id) {
|
|
33
|
+
const p = getProvider(id);
|
|
34
|
+
if (!p)
|
|
35
|
+
throw new Error("Unknown provider: " + id);
|
|
36
|
+
return p;
|
|
37
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { listModelsHttp, streamChatCompletion } from "./openaiCompatible.js";
|
|
2
|
+
const BASE = "https://api.mistral.ai/v1";
|
|
3
|
+
const STATIC = [
|
|
4
|
+
{ id: "mistral-large-latest", label: "Mistral Large" },
|
|
5
|
+
{ id: "mistral-medium-latest", label: "Mistral Medium" },
|
|
6
|
+
{ id: "mistral-small-latest", label: "Mistral Small" },
|
|
7
|
+
{ id: "codestral-latest", label: "Codestral" },
|
|
8
|
+
{ id: "devstral-medium-latest", label: "Devstral" },
|
|
9
|
+
{ id: "magistral-medium-latest", label: "Magistral" },
|
|
10
|
+
];
|
|
11
|
+
export const mistralProvider = {
|
|
12
|
+
id: "mistral",
|
|
13
|
+
label: "Mistral",
|
|
14
|
+
authEnv: ["MISTRAL_API_KEY"],
|
|
15
|
+
baseUrl: BASE,
|
|
16
|
+
defaultModel: "mistral-small-latest",
|
|
17
|
+
listModels: (apiKey) => listModelsHttp({ baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
18
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
19
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { listModelsHttp, streamChatCompletion } from "./openaiCompatible.js";
|
|
2
|
+
function baseUrl() {
|
|
3
|
+
return (process.env["MOONSHOT_BASE_URL"] ?? "").trim() || "https://api.moonshot.cn/v1";
|
|
4
|
+
}
|
|
5
|
+
const STATIC = [
|
|
6
|
+
{ id: "kimi-k3", label: "Kimi K3" },
|
|
7
|
+
{ id: "kimi-k2.7-code", label: "Kimi K2.7 Code" },
|
|
8
|
+
{ id: "kimi-k2.6", label: "Kimi K2.6" },
|
|
9
|
+
{ id: "kimi-k2-thinking", label: "Kimi K2 Thinking" },
|
|
10
|
+
{ id: "kimi-k2-0905", label: "Kimi K2 0905" },
|
|
11
|
+
];
|
|
12
|
+
export const moonshotProvider = {
|
|
13
|
+
id: "moonshot",
|
|
14
|
+
label: "Moonshot (Kimi)",
|
|
15
|
+
authEnv: ["MOONSHOT_API_KEY"],
|
|
16
|
+
get baseUrl() {
|
|
17
|
+
return baseUrl();
|
|
18
|
+
},
|
|
19
|
+
defaultModel: "kimi-k3",
|
|
20
|
+
listModels: (apiKey) => listModelsHttp({ baseUrl: baseUrl(), apiKey, staticModels: STATIC }),
|
|
21
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: baseUrl(), apiKey, staticModels: STATIC }),
|
|
22
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { listModelsHttp, streamChatCompletion } from "./openaiCompatible.js";
|
|
2
|
+
function baseUrl() {
|
|
3
|
+
const host = (process.env["OLLAMA_HOST"] ?? "").trim() || "http://localhost:11434";
|
|
4
|
+
return host.endsWith("/v1") ? host : host + "/v1";
|
|
5
|
+
}
|
|
6
|
+
const STATIC = [
|
|
7
|
+
{ id: "qwen3-coder", label: "qwen3-coder (local)" },
|
|
8
|
+
{ id: "llama3.2", label: "llama3.2 (local)" },
|
|
9
|
+
{ id: "gpt-oss", label: "gpt-oss (local)" },
|
|
10
|
+
{ id: "deepseek-r1", label: "deepseek-r1 (local)" },
|
|
11
|
+
];
|
|
12
|
+
export const ollamaProvider = {
|
|
13
|
+
id: "ollama",
|
|
14
|
+
label: "Ollama (local)",
|
|
15
|
+
authEnv: [],
|
|
16
|
+
get baseUrl() {
|
|
17
|
+
return baseUrl();
|
|
18
|
+
},
|
|
19
|
+
defaultModel: "qwen3-coder",
|
|
20
|
+
listModels: () => listModelsHttp({ baseUrl: baseUrl(), apiKey: undefined, staticModels: STATIC }),
|
|
21
|
+
stream: (req) => streamChatCompletion(req, { baseUrl: baseUrl(), apiKey: undefined, staticModels: STATIC }),
|
|
22
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { listModelsHttp, streamChatCompletion } from "./openaiCompatible.js";
|
|
2
|
+
const BASE = "https://api.openai.com/v1";
|
|
3
|
+
const STATIC = [
|
|
4
|
+
{ id: "gpt-6.1-sol", label: "GPT-6.1 Sol" },
|
|
5
|
+
{ id: "gpt-6.1-sol-pro", label: "GPT-6.1 Sol Pro" },
|
|
6
|
+
{ id: "gpt-6-luna", label: "GPT-6 Luna" },
|
|
7
|
+
{ id: "gpt-6-luna-pro", label: "GPT-6 Luna Pro" },
|
|
8
|
+
{ id: "gpt-6-sol", label: "GPT-6 Sol" },
|
|
9
|
+
{ id: "gpt-5.6-terra", label: "GPT-5.6 Terra" },
|
|
10
|
+
{ id: "gpt-5.6-luna", label: "GPT-5.6 Luna" },
|
|
11
|
+
{ id: "gpt-4.1", label: "GPT-4.1", pricing: { inputUsdPerM: 2, outputUsdPerM: 8 } },
|
|
12
|
+
{ id: "gpt-4o", label: "GPT-4o", pricing: { inputUsdPerM: 2.5, outputUsdPerM: 10 } },
|
|
13
|
+
{ id: "gpt-4o-mini", label: "GPT-4o mini", pricing: { inputUsdPerM: 0.15, outputUsdPerM: 0.6 } },
|
|
14
|
+
{ id: "o4-mini", label: "o4-mini" },
|
|
15
|
+
{ id: "o3", label: "o3" },
|
|
16
|
+
];
|
|
17
|
+
export const openaiProvider = {
|
|
18
|
+
id: "openai",
|
|
19
|
+
label: "OpenAI",
|
|
20
|
+
authEnv: ["OPENAI_API_KEY"],
|
|
21
|
+
baseUrl: BASE,
|
|
22
|
+
defaultModel: "gpt-6-luna",
|
|
23
|
+
listModels: (apiKey) => listModelsHttp({ baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
24
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
25
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { fetchJson, streamSSE } from "../lib/http.js";
|
|
2
|
+
/** Build a chat-completions body in the classic or the newer (compat) dialect. */
|
|
3
|
+
function buildBody(req, compat) {
|
|
4
|
+
const messages = [];
|
|
5
|
+
if (req.system)
|
|
6
|
+
messages.push({ role: "system", content: req.system });
|
|
7
|
+
for (const m of req.messages)
|
|
8
|
+
messages.push({ role: m.role, content: m.content });
|
|
9
|
+
const body: any = { model: req.model, messages, stream: true };
|
|
10
|
+
if (req.maxOutputTokens !== undefined) {
|
|
11
|
+
if (compat)
|
|
12
|
+
body.max_completion_tokens = req.maxOutputTokens;
|
|
13
|
+
else
|
|
14
|
+
body.max_tokens = req.maxOutputTokens;
|
|
15
|
+
}
|
|
16
|
+
if (req.temperature !== undefined && !compat)
|
|
17
|
+
body.temperature = req.temperature;
|
|
18
|
+
return body;
|
|
19
|
+
}
|
|
20
|
+
export async function* streamChatCompletion(req, opts) {
|
|
21
|
+
const send = async (compat) => {
|
|
22
|
+
const headers = {
|
|
23
|
+
"content-type": "application/json",
|
|
24
|
+
accept: "text/event-stream",
|
|
25
|
+
...(opts.extraHeaders ?? {}),
|
|
26
|
+
};
|
|
27
|
+
if (opts.apiKey)
|
|
28
|
+
headers["authorization"] = (opts.authHeader ?? "Bearer") + " " + opts.apiKey;
|
|
29
|
+
return await fetch(trim(opts.baseUrl) + "/chat/completions", {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers,
|
|
32
|
+
body: JSON.stringify(buildBody(req, compat)),
|
|
33
|
+
signal: req.signal,
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
let res = await send(false);
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
const detail = await res.text().catch(() => "");
|
|
39
|
+
// Newer OpenAI-style models reject max_tokens and temperature. Retry once
|
|
40
|
+
// in the compatibility dialect before surfacing the real error.
|
|
41
|
+
if (res.status === 400 && /max_tokens|max_completion_tokens|temperature|unsupported|unrecognized|unknown parameter/i.test(detail)) {
|
|
42
|
+
res = await send(true);
|
|
43
|
+
}
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const body = await res.text().catch(() => "");
|
|
46
|
+
throw new Error(res.status + " " + res.statusText + ": " + body.slice(0, 500));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let usage;
|
|
50
|
+
for await (const ev of streamSSE(res)) {
|
|
51
|
+
if (ev.data === "[DONE]")
|
|
52
|
+
break;
|
|
53
|
+
let payload;
|
|
54
|
+
try {
|
|
55
|
+
payload = JSON.parse(ev.data);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const obj = payload;
|
|
61
|
+
const choice = obj.choices?.[0];
|
|
62
|
+
const raw = choice?.delta?.content ?? choice?.text;
|
|
63
|
+
const reasoning = choice?.delta?.reasoning_content ?? choice?.delta?.reasoning;
|
|
64
|
+
if (obj.usage) {
|
|
65
|
+
usage = {
|
|
66
|
+
inputTokens: obj.usage.prompt_tokens,
|
|
67
|
+
outputTokens: obj.usage.completion_tokens,
|
|
68
|
+
totalTokens: obj.usage.total_tokens,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const text = typeof raw === "string"
|
|
72
|
+
? raw
|
|
73
|
+
: Array.isArray(raw)
|
|
74
|
+
? raw.map((part) => (part?.text ?? "")).join("")
|
|
75
|
+
: "";
|
|
76
|
+
if (text)
|
|
77
|
+
yield { delta: text, done: false };
|
|
78
|
+
else if (typeof reasoning === "string" && reasoning)
|
|
79
|
+
yield { delta: "", done: false, reasoning };
|
|
80
|
+
}
|
|
81
|
+
yield { delta: "", done: true, usage };
|
|
82
|
+
}
|
|
83
|
+
export async function listModelsHttp(opts) {
|
|
84
|
+
const staticModels = opts.staticModels ?? [];
|
|
85
|
+
if (!opts.apiKey && staticModels.length)
|
|
86
|
+
return staticModels;
|
|
87
|
+
try {
|
|
88
|
+
const headers = {};
|
|
89
|
+
if (opts.apiKey)
|
|
90
|
+
headers["authorization"] = (opts.authHeader ?? "Bearer") + " " + opts.apiKey;
|
|
91
|
+
const json = await fetchJson(trim(opts.baseUrl) + "/models", { headers });
|
|
92
|
+
const ids = (json.data ?? [])
|
|
93
|
+
.map((m) => m.id)
|
|
94
|
+
.filter((id) => typeof id === "string" && id.length > 0);
|
|
95
|
+
if (ids.length === 0)
|
|
96
|
+
return staticModels;
|
|
97
|
+
// The provider's live list is authoritative: show exactly what it
|
|
98
|
+
// returns (enriched with our labels/pricing where the id is known).
|
|
99
|
+
// Static models the provider no longer returns are dropped so delisted
|
|
100
|
+
// models don't linger in the picker.
|
|
101
|
+
const known = new Map(staticModels.map((m) => [m.id, m]));
|
|
102
|
+
return ids.map((id) => known.get(id) ?? { id });
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return staticModels;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function trim(url) {
|
|
109
|
+
return url.replace(/[/]+$/, "");
|
|
110
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { listModelsHttp, streamChatCompletion } from "./openaiCompatible.js";
|
|
2
|
+
const BASE = "https://openrouter.ai/api/v1";
|
|
3
|
+
const STATIC = [
|
|
4
|
+
{ id: "anthropic/claude-sonnet-5.5", label: "Claude Sonnet 5.5" },
|
|
5
|
+
{ id: "openai/gpt-6-luna", label: "GPT-6 Luna" },
|
|
6
|
+
{ id: "google/gemini-3.8-flash", label: "Gemini 3.8 Flash" },
|
|
7
|
+
{ id: "deepseek/deepseek-v4-pro", label: "DeepSeek V4 Pro" },
|
|
8
|
+
{ id: "x-ai/grok-4.7", label: "Grok 4.7" },
|
|
9
|
+
{ id: "moonshotai/kimi-k3", label: "Kimi K3" },
|
|
10
|
+
{ id: "z-ai/glm-5.3", label: "GLM 5.3" },
|
|
11
|
+
{ id: "qwen/qwen3.8-max", label: "Qwen3.8 Max" },
|
|
12
|
+
{ id: "meta-llama/llama-4-maverick", label: "Llama 4 Maverick" },
|
|
13
|
+
];
|
|
14
|
+
const EXTRA_HEADERS = {
|
|
15
|
+
"http-referer": "https://github.com/dateName/fastcode-termux",
|
|
16
|
+
"x-title": "FastCode",
|
|
17
|
+
};
|
|
18
|
+
export const openrouterProvider = {
|
|
19
|
+
id: "openrouter",
|
|
20
|
+
label: "OpenRouter (400+ models)",
|
|
21
|
+
authEnv: ["OPENROUTER_API_KEY"],
|
|
22
|
+
baseUrl: BASE,
|
|
23
|
+
defaultModel: "anthropic/claude-sonnet-5.5",
|
|
24
|
+
listModels: (apiKey) => listModelsHttp({ baseUrl: BASE, apiKey, staticModels: STATIC, extraHeaders: EXTRA_HEADERS }),
|
|
25
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC, extraHeaders: EXTRA_HEADERS }),
|
|
26
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { listModelsHttp, streamChatCompletion } from "./openaiCompatible.js";
|
|
2
|
+
function baseUrl() {
|
|
3
|
+
return (process.env["DASHSCOPE_BASE_URL"] ?? "").trim() || "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
4
|
+
}
|
|
5
|
+
const STATIC = [
|
|
6
|
+
{ id: "qwen3.8-max", label: "Qwen3.8 Max" },
|
|
7
|
+
{ id: "qwen3.8-flash", label: "Qwen3.8 Flash" },
|
|
8
|
+
{ id: "qwen3.7-max", label: "Qwen3.7 Max" },
|
|
9
|
+
{ id: "qwen3.7-plus", label: "Qwen3.7 Plus" },
|
|
10
|
+
{ id: "qwen3-coder-next", label: "Qwen3 Coder Next" },
|
|
11
|
+
{ id: "qwen3.6-27b", label: "Qwen3.6 27B" },
|
|
12
|
+
];
|
|
13
|
+
export const qwenProvider = {
|
|
14
|
+
id: "qwen",
|
|
15
|
+
label: "Qwen (DashScope)",
|
|
16
|
+
authEnv: ["DASHSCOPE_API_KEY", "QWEN_API_KEY"],
|
|
17
|
+
get baseUrl() {
|
|
18
|
+
return baseUrl();
|
|
19
|
+
},
|
|
20
|
+
defaultModel: "qwen3.8-max",
|
|
21
|
+
listModels: (apiKey) => listModelsHttp({ baseUrl: baseUrl(), apiKey, staticModels: STATIC }),
|
|
22
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: baseUrl(), apiKey, staticModels: STATIC }),
|
|
23
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { listModelsHttp, streamChatCompletion } from "./openaiCompatible.js";
|
|
2
|
+
const BASE = "https://api.x.ai/v1";
|
|
3
|
+
const STATIC = [
|
|
4
|
+
{ id: "grok-4.7", label: "Grok 4.7" },
|
|
5
|
+
{ id: "grok-4.6", label: "Grok 4.6" },
|
|
6
|
+
{ id: "grok-4.5", label: "Grok 4.5" },
|
|
7
|
+
{ id: "grok-4.3", label: "Grok 4.3" },
|
|
8
|
+
{ id: "grok-4.20", label: "Grok 4.20" },
|
|
9
|
+
];
|
|
10
|
+
export const xaiProvider = {
|
|
11
|
+
id: "xai",
|
|
12
|
+
label: "xAI (Grok)",
|
|
13
|
+
authEnv: ["XAI_API_KEY", "GROK_API_KEY"],
|
|
14
|
+
baseUrl: BASE,
|
|
15
|
+
defaultModel: "grok-4.7",
|
|
16
|
+
listModels: (apiKey) => listModelsHttp({ baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
17
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC }),
|
|
18
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { listModelsHttp, streamChatCompletion } from "./openaiCompatible.js";
|
|
2
|
+
function baseUrl() {
|
|
3
|
+
return (process.env["ZHIPU_BASE_URL"] ?? "").trim() || "https://open.bigmodel.cn/api/paas/v4";
|
|
4
|
+
}
|
|
5
|
+
const STATIC = [
|
|
6
|
+
{ id: "glm-5.3", label: "GLM 5.3" },
|
|
7
|
+
{ id: "glm-5.2", label: "GLM 5.2" },
|
|
8
|
+
{ id: "glm-5.1", label: "GLM 5.1" },
|
|
9
|
+
{ id: "glm-5", label: "GLM 5" },
|
|
10
|
+
{ id: "glm-4.7", label: "GLM 4.7" },
|
|
11
|
+
{ id: "glm-4.6", label: "GLM 4.6" },
|
|
12
|
+
];
|
|
13
|
+
export const zhipuProvider = {
|
|
14
|
+
id: "zhipu",
|
|
15
|
+
label: "Zhipu (GLM)",
|
|
16
|
+
authEnv: ["ZHIPUAI_API_KEY", "GLM_API_KEY", "ZHIPU_API_KEY"],
|
|
17
|
+
get baseUrl() {
|
|
18
|
+
return baseUrl();
|
|
19
|
+
},
|
|
20
|
+
defaultModel: "glm-5.3",
|
|
21
|
+
listModels: (apiKey) => listModelsHttp({ baseUrl: baseUrl(), apiKey, staticModels: STATIC }),
|
|
22
|
+
stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: baseUrl(), apiKey, staticModels: STATIC }),
|
|
23
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only JSONL session log.
|
|
3
|
+
*
|
|
4
|
+
* Every entry is flushed as its own line, so a crash (or a kill from the
|
|
5
|
+
* Android task switcher) still leaves a readable transcript that can be
|
|
6
|
+
* resumed. A size cap keeps the sessions directory bounded; the oldest logs are
|
|
7
|
+
* pruned first.
|
|
8
|
+
*
|
|
9
|
+
* FASTCODE_SESSION_LOG=0 disable logging
|
|
10
|
+
* FASTCODE_SESSION_CACHE_MB=N sessions cache cap in MB (default 256)
|
|
11
|
+
*/
|
|
12
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { getPaths } from "../config/paths.js";
|
|
15
|
+
const NL = String.fromCharCode(10);
|
|
16
|
+
export function sessionCacheLimitBytes() {
|
|
17
|
+
const mb = Number(process.env["FASTCODE_SESSION_CACHE_MB"]);
|
|
18
|
+
return (Number.isFinite(mb) && mb > 0 ? mb : 256) * 1024 * 1024;
|
|
19
|
+
}
|
|
20
|
+
export function loggingEnabled() {
|
|
21
|
+
return process.env["FASTCODE_SESSION_LOG"] !== "0";
|
|
22
|
+
}
|
|
23
|
+
function newId() {
|
|
24
|
+
return new Date().toISOString().replace(/[:.]/g, "-") + "-" + Math.random().toString(36).slice(2, 6);
|
|
25
|
+
}
|
|
26
|
+
function openLog(id, file, meta) {
|
|
27
|
+
let closed = false;
|
|
28
|
+
const append = (entry) => {
|
|
29
|
+
if (closed || !loggingEnabled())
|
|
30
|
+
return;
|
|
31
|
+
try {
|
|
32
|
+
appendFileSync(file, JSON.stringify({ ts: Date.now(), ...entry }) + NL, "utf8");
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// A read-only or full filesystem must not break the TUI.
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
append({ type: "session_start", id, cwd: process.cwd(), ...meta });
|
|
39
|
+
return {
|
|
40
|
+
id,
|
|
41
|
+
file,
|
|
42
|
+
append,
|
|
43
|
+
close() {
|
|
44
|
+
append({ type: "session_end" });
|
|
45
|
+
closed = true;
|
|
46
|
+
try {
|
|
47
|
+
pruneSessions();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// best effort
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function createSessionLog(meta = {}) {
|
|
56
|
+
const { sessions } = getPaths();
|
|
57
|
+
mkdirSync(sessions, { recursive: true });
|
|
58
|
+
const id = newId();
|
|
59
|
+
const file = join(sessions, id + ".jsonl");
|
|
60
|
+
return openLog(id, file, meta);
|
|
61
|
+
}
|
|
62
|
+
export function openSessionLog(file) {
|
|
63
|
+
const { sessions } = getPaths();
|
|
64
|
+
mkdirSync(sessions, { recursive: true });
|
|
65
|
+
return openLog(undefined, existsSync(file) ? file : join(sessions, file), {});
|
|
66
|
+
}
|
|
67
|
+
export function listSessions() {
|
|
68
|
+
const { sessions } = getPaths();
|
|
69
|
+
if (!existsSync(sessions))
|
|
70
|
+
return [];
|
|
71
|
+
return readdirSync(sessions)
|
|
72
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
73
|
+
.map((name) => {
|
|
74
|
+
const file = join(sessions, name);
|
|
75
|
+
let bytes = 0;
|
|
76
|
+
let modified = "";
|
|
77
|
+
let messages = 0;
|
|
78
|
+
try {
|
|
79
|
+
const info = statSync(file);
|
|
80
|
+
bytes = info.size;
|
|
81
|
+
modified = info.mtime.toISOString();
|
|
82
|
+
messages = readFileSync(file, "utf8").split(NL).filter((line) => line.includes('"type":"message"')).length;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// unreadable: report what we have
|
|
86
|
+
}
|
|
87
|
+
return { id: name.replace(/[.]jsonl$/, ""), file, bytes, modified, messages };
|
|
88
|
+
})
|
|
89
|
+
.sort((a, b) => (a.modified < b.modified ? 1 : -1));
|
|
90
|
+
}
|
|
91
|
+
export function resolveSessionFile(idOrFile) {
|
|
92
|
+
const { sessions } = getPaths();
|
|
93
|
+
const looksLikePath = idOrFile.includes("/") || idOrFile.includes(String.fromCharCode(92)) || idOrFile.endsWith(".jsonl");
|
|
94
|
+
return looksLikePath ? idOrFile : join(sessions, idOrFile + ".jsonl");
|
|
95
|
+
}
|
|
96
|
+
export function readSession(idOrFile) {
|
|
97
|
+
const file = resolveSessionFile(idOrFile);
|
|
98
|
+
if (!existsSync(file))
|
|
99
|
+
return [];
|
|
100
|
+
return readFileSync(file, "utf8")
|
|
101
|
+
.split(NL)
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.map((line) => {
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(line);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
.filter(Boolean);
|
|
112
|
+
}
|
|
113
|
+
export function loadSessionMessages(idOrFile) {
|
|
114
|
+
return readSession(idOrFile)
|
|
115
|
+
.filter((entry) => entry.type === "message" && typeof entry.content === "string")
|
|
116
|
+
.map((entry) => ({ role: entry.role, content: entry.content }));
|
|
117
|
+
}
|
|
118
|
+
export function latestSessionFile() {
|
|
119
|
+
const all = listSessions();
|
|
120
|
+
return all.length > 0 ? all[0].file : undefined;
|
|
121
|
+
}
|
|
122
|
+
export function pruneSessions(limit = sessionCacheLimitBytes()) {
|
|
123
|
+
const all = listSessions();
|
|
124
|
+
let total = all.reduce((sum, entry) => sum + entry.bytes, 0);
|
|
125
|
+
for (let i = all.length - 1; i >= 0 && total > limit; i -= 1) {
|
|
126
|
+
try {
|
|
127
|
+
rmSync(all[i].file, { force: true });
|
|
128
|
+
total -= all[i].bytes;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// keep pruning the rest
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return total;
|
|
135
|
+
}
|