@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,74 @@
|
|
|
1
|
+
import { getPlugin, importPlugin, listInstalledPlugins, removePlugin, } from "../plugins/manager.js";
|
|
2
|
+
export const pluginCommand = {
|
|
3
|
+
name: "plugin",
|
|
4
|
+
description: "Manage always-active plugin packs. Usage: /plugin [list|import|enable|disable|remove|info]",
|
|
5
|
+
run({ args, state, actions }) {
|
|
6
|
+
const sub = (args[0] ?? "list").toLowerCase();
|
|
7
|
+
if (sub === "list") {
|
|
8
|
+
const plugins = listInstalledPlugins();
|
|
9
|
+
if (plugins.length === 0)
|
|
10
|
+
return { message: "No plugins installed. Import one with /plugin import <path> [name]." };
|
|
11
|
+
return {
|
|
12
|
+
message: [
|
|
13
|
+
"Installed plugins:",
|
|
14
|
+
"",
|
|
15
|
+
...plugins.map((p) => {
|
|
16
|
+
const enabled = state.enabledPlugins.includes(p.name) ? "●" : " ";
|
|
17
|
+
return `${enabled} ${p.name} — ${p.description}`;
|
|
18
|
+
}),
|
|
19
|
+
].join("\n"),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (sub === "import") {
|
|
23
|
+
const source = args[1];
|
|
24
|
+
if (!source)
|
|
25
|
+
return { message: "Usage: /plugin import <folder-path> [name]" };
|
|
26
|
+
const plugin = importPlugin(source, args[2]);
|
|
27
|
+
actions.enablePlugin(plugin.name);
|
|
28
|
+
return { message: `Imported and enabled plugin ${plugin.name}.` };
|
|
29
|
+
}
|
|
30
|
+
if (sub === "enable") {
|
|
31
|
+
const name = args[1];
|
|
32
|
+
if (!name)
|
|
33
|
+
return { message: "Usage: /plugin enable <name>" };
|
|
34
|
+
const plugin = getPlugin(name);
|
|
35
|
+
if (!plugin)
|
|
36
|
+
return { message: `Plugin not found: ${name}` };
|
|
37
|
+
actions.enablePlugin(plugin.name);
|
|
38
|
+
return { message: `Enabled plugin ${plugin.name}.` };
|
|
39
|
+
}
|
|
40
|
+
if (sub === "disable") {
|
|
41
|
+
const name = args[1];
|
|
42
|
+
if (!name)
|
|
43
|
+
return { message: "Usage: /plugin disable <name>" };
|
|
44
|
+
actions.disablePlugin(name);
|
|
45
|
+
return { message: `Disabled plugin ${name}.` };
|
|
46
|
+
}
|
|
47
|
+
if (sub === "remove") {
|
|
48
|
+
const name = args[1];
|
|
49
|
+
if (!name)
|
|
50
|
+
return { message: "Usage: /plugin remove <name>" };
|
|
51
|
+
actions.disablePlugin(name);
|
|
52
|
+
return { message: removePlugin(name) ? `Removed plugin ${name}.` : `Plugin not found: ${name}` };
|
|
53
|
+
}
|
|
54
|
+
if (sub === "info") {
|
|
55
|
+
const name = args[1];
|
|
56
|
+
if (!name)
|
|
57
|
+
return { message: "Usage: /plugin info <name>" };
|
|
58
|
+
const plugin = getPlugin(name);
|
|
59
|
+
if (!plugin)
|
|
60
|
+
return { message: `Plugin not found: ${name}` };
|
|
61
|
+
return {
|
|
62
|
+
message: [
|
|
63
|
+
`Plugin: ${plugin.name}`,
|
|
64
|
+
`Path: ${plugin.path}`,
|
|
65
|
+
`Description: ${plugin.description}`,
|
|
66
|
+
`Skills: ${plugin.skills.map((s) => s.name).join(", ") || "none"}`,
|
|
67
|
+
"",
|
|
68
|
+
plugin.body.slice(0, 4000),
|
|
69
|
+
].join("\n"),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return { message: "Usage: /plugin [list|import|enable|disable|remove|info]" };
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { PROVIDERS, getProvider } from "../providers/index.js";
|
|
2
|
+
export const providerCommand = {
|
|
3
|
+
name: "provider",
|
|
4
|
+
description: "List or switch the active LLM provider. Usage: /provider [id]",
|
|
5
|
+
run({ args, actions, state }) {
|
|
6
|
+
if (args.length === 0) {
|
|
7
|
+
const lines = ["Available providers:"];
|
|
8
|
+
for (const p of PROVIDERS) {
|
|
9
|
+
const marker = p.id === state.providerId ? "● " : " ";
|
|
10
|
+
const auth = p.authEnv.length === 0 ? "(no key needed)" : `[${p.authEnv.join(", ")}]`;
|
|
11
|
+
lines.push(`${marker}${p.id.padEnd(12)} ${p.label} ${auth}`);
|
|
12
|
+
}
|
|
13
|
+
lines.push("");
|
|
14
|
+
lines.push("Switch with `/provider <id>` (e.g. `/provider openrouter`).");
|
|
15
|
+
return { message: lines.join("\n") };
|
|
16
|
+
}
|
|
17
|
+
const target = args[0];
|
|
18
|
+
const next = getProvider(target);
|
|
19
|
+
if (!next) {
|
|
20
|
+
return { message: `Unknown provider "${target}". Try /provider with no args.` };
|
|
21
|
+
}
|
|
22
|
+
actions.setProvider(next.id);
|
|
23
|
+
actions.setModel(next.defaultModel);
|
|
24
|
+
return {
|
|
25
|
+
message: `Provider set to ${next.label}. Default model: ${next.defaultModel}. Use /model to change.`,
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DEFAULT_CONFIG, saveConfig } from "../config/store.js";
|
|
2
|
+
export const resetCommand = {
|
|
3
|
+
name: "reset",
|
|
4
|
+
description: "Reset config to defaults (does not touch API keys).",
|
|
5
|
+
run({ actions }) {
|
|
6
|
+
saveConfig({ ...DEFAULT_CONFIG });
|
|
7
|
+
actions.setProvider(DEFAULT_CONFIG.providerId);
|
|
8
|
+
actions.setModel(DEFAULT_CONFIG.model);
|
|
9
|
+
actions.setTheme(DEFAULT_CONFIG.theme);
|
|
10
|
+
actions.setSystemPrompt(DEFAULT_CONFIG.systemPrompt);
|
|
11
|
+
return { message: "Config reset to defaults." };
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { installSkill, uninstallSkill, listInstalledSkills, searchRegistry, getSkill, } from "../skills/manager.js";
|
|
2
|
+
export const skillsCommand = {
|
|
3
|
+
name: "skills",
|
|
4
|
+
description: "Manage Agent Skills (download from skills.sh and toggle).",
|
|
5
|
+
async run({ args, state, actions }) {
|
|
6
|
+
const sub = (args[0] ?? "list").toLowerCase();
|
|
7
|
+
const rest = args.slice(1);
|
|
8
|
+
if (sub === "list" || sub === "ls") {
|
|
9
|
+
const installed = listInstalledSkills();
|
|
10
|
+
if (installed.length === 0) {
|
|
11
|
+
return {
|
|
12
|
+
message: "No skills installed. Try `/skills search <query>` or `/skills install <name>`.",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const lines = ["Installed skills:"];
|
|
16
|
+
for (const s of installed) {
|
|
17
|
+
const enabled = state.enabledSkills.includes(s.name) ? "● " : " ";
|
|
18
|
+
lines.push(`${enabled}${s.name} — ${truncate(s.description, 80)}`);
|
|
19
|
+
lines.push(` source: ${s.source}`);
|
|
20
|
+
}
|
|
21
|
+
lines.push("");
|
|
22
|
+
lines.push("Toggle with `/skills enable <name>` or `/skills disable <name>`.");
|
|
23
|
+
return { message: lines.join("\n") };
|
|
24
|
+
}
|
|
25
|
+
if (sub === "search") {
|
|
26
|
+
if (rest.length === 0)
|
|
27
|
+
return { message: "Usage: /skills search <query>" };
|
|
28
|
+
const hits = await searchRegistry(rest.join(" "), state.skillsRegistry);
|
|
29
|
+
if (hits.length === 0) {
|
|
30
|
+
return { message: `No skills found for "${rest.join(" ")}".` };
|
|
31
|
+
}
|
|
32
|
+
const lines = [`Skills matching "${rest.join(" ")}":`];
|
|
33
|
+
for (const h of hits.slice(0, 20)) {
|
|
34
|
+
lines.push(` • ${h.slug} — ${truncate(h.description ?? "", 80)}`);
|
|
35
|
+
}
|
|
36
|
+
lines.push("");
|
|
37
|
+
lines.push("Install with `/skills install <slug>`.");
|
|
38
|
+
return { message: lines.join("\n") };
|
|
39
|
+
}
|
|
40
|
+
if (sub === "install" || sub === "add") {
|
|
41
|
+
if (rest.length === 0)
|
|
42
|
+
return { message: "Usage: /skills install <name|owner/repo|url>" };
|
|
43
|
+
const force = rest.includes("--force");
|
|
44
|
+
const spec = rest.filter((r) => r !== "--force").join(" ");
|
|
45
|
+
try {
|
|
46
|
+
const skill = await installSkill(spec, { registry: state.skillsRegistry, force });
|
|
47
|
+
return {
|
|
48
|
+
message: `Installed skill "${skill.name}" → ${skill.path}\n${truncate(skill.description, 200)}\n\nEnable with /skills enable ${skill.name}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
return { message: `Install failed: ${err.message}` };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (sub === "uninstall" || sub === "remove" || sub === "rm") {
|
|
56
|
+
if (rest.length === 0)
|
|
57
|
+
return { message: "Usage: /skills uninstall <name>" };
|
|
58
|
+
const ok = uninstallSkill(rest[0]);
|
|
59
|
+
if (ok)
|
|
60
|
+
actions.disableSkill(rest[0]);
|
|
61
|
+
return { message: ok ? `Uninstalled "${rest[0]}".` : `No skill named "${rest[0]}".` };
|
|
62
|
+
}
|
|
63
|
+
if (sub === "enable" || sub === "use") {
|
|
64
|
+
if (rest.length === 0)
|
|
65
|
+
return { message: "Usage: /skills enable <name>" };
|
|
66
|
+
const skill = getSkill(rest[0]);
|
|
67
|
+
if (!skill)
|
|
68
|
+
return { message: `Skill "${rest[0]}" is not installed.` };
|
|
69
|
+
actions.enableSkill(skill.name);
|
|
70
|
+
return { message: `Enabled skill "${skill.name}".` };
|
|
71
|
+
}
|
|
72
|
+
if (sub === "disable" || sub === "off") {
|
|
73
|
+
if (rest.length === 0)
|
|
74
|
+
return { message: "Usage: /skills disable <name>" };
|
|
75
|
+
actions.disableSkill(rest[0]);
|
|
76
|
+
return { message: `Disabled skill "${rest[0]}".` };
|
|
77
|
+
}
|
|
78
|
+
if (sub === "info" || sub === "show") {
|
|
79
|
+
if (rest.length === 0)
|
|
80
|
+
return { message: "Usage: /skills info <name>" };
|
|
81
|
+
const skill = getSkill(rest[0]);
|
|
82
|
+
if (!skill)
|
|
83
|
+
return { message: `Skill "${rest[0]}" is not installed.` };
|
|
84
|
+
const head = `# ${skill.name}\n_${skill.description}_\n\nSource: ${skill.source}\n\n---\n\n`;
|
|
85
|
+
return { message: head + skill.body };
|
|
86
|
+
}
|
|
87
|
+
if (sub === "registry") {
|
|
88
|
+
if (rest.length === 0) {
|
|
89
|
+
return { message: `Skills registry: ${state.skillsRegistry}` };
|
|
90
|
+
}
|
|
91
|
+
// Mutate via AppActions so the in-memory React state stays in sync —
|
|
92
|
+
// direct saveConfig() would be silently overwritten by the next state
|
|
93
|
+
// change because app.tsx persists in-memory config on every update.
|
|
94
|
+
actions.setSkillsRegistry(rest[0]);
|
|
95
|
+
return { message: `Skills registry set to ${rest[0]}.` };
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
message: "Unknown subcommand. Try: list, search, install, uninstall, enable, disable, info, registry.",
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
function truncate(s, n) {
|
|
103
|
+
if (s.length <= n)
|
|
104
|
+
return s;
|
|
105
|
+
return s.slice(0, n - 1) + "…";
|
|
106
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const systemCommand = {
|
|
2
|
+
name: "system",
|
|
3
|
+
description: "Show or set the system prompt. Usage: /system [new prompt]",
|
|
4
|
+
run({ raw, state, actions }) {
|
|
5
|
+
// Strip the leading "/system" verb keeping the rest verbatim, including
|
|
6
|
+
// whitespace, so multi-paragraph prompts survive.
|
|
7
|
+
const stripped = raw.replace(/^\/system\s*/i, "");
|
|
8
|
+
if (!stripped.trim()) {
|
|
9
|
+
return {
|
|
10
|
+
message: [
|
|
11
|
+
"Current system prompt:",
|
|
12
|
+
"",
|
|
13
|
+
state.systemPrompt,
|
|
14
|
+
].join("\n"),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
actions.setSystemPrompt(stripped);
|
|
18
|
+
return { message: "System prompt updated." };
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { THEMES } from "../config/themes.js";
|
|
2
|
+
export const themeCommand = {
|
|
3
|
+
name: "theme",
|
|
4
|
+
description: "List or switch the color theme. Usage: /theme [id]",
|
|
5
|
+
run({ args, state, actions }) {
|
|
6
|
+
if (args.length === 0) {
|
|
7
|
+
const lines = ["Themes:"];
|
|
8
|
+
for (const id of Object.keys(THEMES)) {
|
|
9
|
+
const marker = id === state.theme ? "● " : " ";
|
|
10
|
+
lines.push(`${marker}${id} — ${THEMES[id].label}`);
|
|
11
|
+
}
|
|
12
|
+
lines.push("");
|
|
13
|
+
lines.push("Switch with `/theme <id>`.");
|
|
14
|
+
return { message: lines.join("\n") };
|
|
15
|
+
}
|
|
16
|
+
const id = args[0];
|
|
17
|
+
if (!THEMES[id])
|
|
18
|
+
return { message: `Unknown theme "${id}".` };
|
|
19
|
+
actions.setTheme(id);
|
|
20
|
+
return { message: `Theme set to ${id}.` };
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const toolsCommand = {
|
|
2
|
+
name: "tools",
|
|
3
|
+
description: "Show local coding-agent tools.",
|
|
4
|
+
run() {
|
|
5
|
+
return {
|
|
6
|
+
message: [
|
|
7
|
+
"FastCode agent tools:",
|
|
8
|
+
"",
|
|
9
|
+
" shell Run terminal commands in the sandbox root",
|
|
10
|
+
" pwd Print sandbox root",
|
|
11
|
+
" read_file Read one file",
|
|
12
|
+
" read_many Read multiple files",
|
|
13
|
+
" write_file Replace/create a file",
|
|
14
|
+
" append_file Append to a file",
|
|
15
|
+
" apply_patch Apply focused patch hunks",
|
|
16
|
+
" replace_in_file Replace exact text in a file",
|
|
17
|
+
" list_files List files recursively",
|
|
18
|
+
" grep Search files",
|
|
19
|
+
" stat_path Inspect file metadata",
|
|
20
|
+
" make_dir Create directories",
|
|
21
|
+
" copy_file Copy a file",
|
|
22
|
+
" move_path Move/rename a path",
|
|
23
|
+
" delete_file Delete a path",
|
|
24
|
+
"",
|
|
25
|
+
"Sandbox: file tools are rooted at the current working directory.",
|
|
26
|
+
].join("\n"),
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { PROVIDERS, getProvider } from "../providers/index.js";
|
|
2
|
+
import { listCredentials, listLegacy, maskKey, resolveApiKey } from "../config/store.js";
|
|
3
|
+
export const whoamiCommand = {
|
|
4
|
+
name: "whoami",
|
|
5
|
+
description: "Show which provider/model keys are configured.",
|
|
6
|
+
run({ state }) {
|
|
7
|
+
const lines = ["Authentication status:", ` current scope: ${state.providerId} · ${state.model}`];
|
|
8
|
+
const current = getProvider(state.providerId);
|
|
9
|
+
if (current)
|
|
10
|
+
lines.push(" " + describeLookup(current, resolveApiKey(current.id, state.model, current.authEnv)));
|
|
11
|
+
const credentials = listCredentials();
|
|
12
|
+
lines.push("");
|
|
13
|
+
lines.push(`Stored keys (${credentials.length}):`);
|
|
14
|
+
if (credentials.length === 0)
|
|
15
|
+
lines.push(" (none) — add one with /login");
|
|
16
|
+
for (const c of credentials) {
|
|
17
|
+
const marker = c.provider === state.providerId && c.model === state.model ? "● " : " ";
|
|
18
|
+
lines.push(`${marker}${c.provider}::${c.model} ${maskKey(c.key)}`);
|
|
19
|
+
}
|
|
20
|
+
const legacy = listLegacy();
|
|
21
|
+
if (legacy.length > 0) {
|
|
22
|
+
lines.push("");
|
|
23
|
+
lines.push("Unbound legacy keys (NOT used — re-bind them to a model):");
|
|
24
|
+
for (const l of legacy)
|
|
25
|
+
lines.push(` ${l.provider} ${maskKey(l.key)} → /login ${l.provider} <model> <key>`);
|
|
26
|
+
}
|
|
27
|
+
const envLines = [];
|
|
28
|
+
for (const p of PROVIDERS) {
|
|
29
|
+
const envVar = p.authEnv.find((v) => process.env[v]);
|
|
30
|
+
if (envVar)
|
|
31
|
+
envLines.push(` ${envVar} (${p.id}, any model)`);
|
|
32
|
+
}
|
|
33
|
+
if (envLines.length > 0) {
|
|
34
|
+
lines.push("");
|
|
35
|
+
lines.push("Environment keys:");
|
|
36
|
+
lines.push(...envLines);
|
|
37
|
+
}
|
|
38
|
+
return { message: lines.join("\n") };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
function describeLookup(provider, lookup) {
|
|
42
|
+
if (provider.authEnv.length === 0)
|
|
43
|
+
return `${provider.id}: no key needed`;
|
|
44
|
+
if (lookup.status === "ok")
|
|
45
|
+
return lookup.source === "env"
|
|
46
|
+
? `${provider.id}: env ${lookup.envVar}`
|
|
47
|
+
: `${provider.id}: stored ${maskKey(lookup.key)}`;
|
|
48
|
+
if (lookup.status === "unbound")
|
|
49
|
+
return `${provider.id}: NO bound key (a stored legacy key needs re-binding)`;
|
|
50
|
+
return `${provider.id}: no key — /login ${provider.id} <model> <key>`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem layout for FastCode user data.
|
|
3
|
+
*
|
|
4
|
+
* ~/.fastcode/
|
|
5
|
+
* config.json global preferences (provider, model, theme, …)
|
|
6
|
+
* auth.json per-provider API keys (mode 0600)
|
|
7
|
+
* skills/ installed SKILL.md folders
|
|
8
|
+
* plugins/ installed plugin folders
|
|
9
|
+
* cache/ transient downloads & registry caches
|
|
10
|
+
* sessions/ conversation transcripts (future)
|
|
11
|
+
* logs/ debug logs (future)
|
|
12
|
+
*
|
|
13
|
+
* On Windows we keep using `~/.fastcode` rather than %APPDATA% to match the
|
|
14
|
+
* dotfile convention most TUIs use; users can override via $FASTCODE_HOME.
|
|
15
|
+
*/
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { mkdirSync, existsSync, chmodSync } from "node:fs";
|
|
18
|
+
import { safeHome, isWindows } from "../lib/platform.js";
|
|
19
|
+
let cached;
|
|
20
|
+
export function getPaths() {
|
|
21
|
+
if (cached)
|
|
22
|
+
return cached;
|
|
23
|
+
const root = process.env["FASTCODE_HOME"] ?? join(safeHome(), ".fastcode");
|
|
24
|
+
const paths = {
|
|
25
|
+
root,
|
|
26
|
+
config: join(root, "config.json"),
|
|
27
|
+
auth: join(root, "auth.json"),
|
|
28
|
+
trust: join(root, "trusted.json"),
|
|
29
|
+
skills: join(root, "skills"),
|
|
30
|
+
plugins: join(root, "plugins"),
|
|
31
|
+
cache: join(root, "cache"),
|
|
32
|
+
sessions: join(root, "sessions"),
|
|
33
|
+
logs: join(root, "logs"),
|
|
34
|
+
};
|
|
35
|
+
ensureDir(paths.root);
|
|
36
|
+
ensureDir(paths.skills);
|
|
37
|
+
ensureDir(paths.plugins);
|
|
38
|
+
ensureDir(paths.cache);
|
|
39
|
+
ensureDir(paths.sessions);
|
|
40
|
+
ensureDir(paths.logs);
|
|
41
|
+
cached = paths;
|
|
42
|
+
return paths;
|
|
43
|
+
}
|
|
44
|
+
export function ensureDir(dir) {
|
|
45
|
+
if (!existsSync(dir))
|
|
46
|
+
mkdirSync(dir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
/** Tighten permissions on a file storing secrets where the OS supports it. */
|
|
49
|
+
export function lockdown(file) {
|
|
50
|
+
if (isWindows())
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
chmodSync(file, 0o600);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// best effort; some filesystems (FAT, Termux on internal storage) don't
|
|
57
|
+
// honor chmod and that's fine.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent config & auth store.
|
|
3
|
+
*
|
|
4
|
+
* We deliberately keep the format human-readable JSON so users on Termux can
|
|
5
|
+
* edit it from `nano ~/.fastcode/config.json` if needed. Atomic writes prevent
|
|
6
|
+
* partial files on Ctrl+C.
|
|
7
|
+
*
|
|
8
|
+
* API keys are NOT a single global string: each one is scoped to a concrete
|
|
9
|
+
* "provider + model" pair. auth.json stores one credential per
|
|
10
|
+
* `<provider>::<model>` scope, and a stored key is only used when the current
|
|
11
|
+
* provider AND model match that scope. The old provider-only format is read as
|
|
12
|
+
* "legacy": it is shown back to the user but never used silently, because a
|
|
13
|
+
* provider-only key does not tell us which model it was meant for.
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync, renameSync } from "node:fs";
|
|
16
|
+
import { join, dirname } from "node:path";
|
|
17
|
+
import { getPaths, lockdown, ensureDir } from "./paths.js";
|
|
18
|
+
import { DEFAULT_THEME } from "./themes.js";
|
|
19
|
+
export const DEFAULT_CONFIG = {
|
|
20
|
+
providerId: "anthropic",
|
|
21
|
+
model: "claude-sonnet-5.5",
|
|
22
|
+
theme: DEFAULT_THEME,
|
|
23
|
+
agentMode: true,
|
|
24
|
+
systemPrompt: "You are FastCode, a fast, friendly coding assistant in a terminal UI. " +
|
|
25
|
+
"Prefer concise responses. Use markdown when helpful (especially fenced " +
|
|
26
|
+
"code blocks). When the user asks for code changes, output complete files " +
|
|
27
|
+
"or unified diffs the user can apply.",
|
|
28
|
+
enabledSkills: [],
|
|
29
|
+
enabledPlugins: [],
|
|
30
|
+
skillsRegistry: "https://skillshub.wtf",
|
|
31
|
+
lifetimeCostUsd: 0,
|
|
32
|
+
};
|
|
33
|
+
export interface StoredCredential {
|
|
34
|
+
provider: string;
|
|
35
|
+
model: string;
|
|
36
|
+
key: string;
|
|
37
|
+
addedAt: string;
|
|
38
|
+
}
|
|
39
|
+
export interface AuthStore {
|
|
40
|
+
version: number;
|
|
41
|
+
/** Scoped credentials, keyed by `provider::model`. */
|
|
42
|
+
keys: Record<string, StoredCredential>;
|
|
43
|
+
/** Old provider-only keys we refuse to use until the user re-binds them. */
|
|
44
|
+
legacy: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
export type KeyLookup = {
|
|
47
|
+
status: "ok";
|
|
48
|
+
key: string;
|
|
49
|
+
source: "file" | "env";
|
|
50
|
+
envVar?: string;
|
|
51
|
+
} | {
|
|
52
|
+
status: "unbound";
|
|
53
|
+
provider: string;
|
|
54
|
+
legacyKey: string;
|
|
55
|
+
} | {
|
|
56
|
+
status: "missing";
|
|
57
|
+
};
|
|
58
|
+
export function loadConfig() {
|
|
59
|
+
const { config } = getPaths();
|
|
60
|
+
if (!existsSync(config))
|
|
61
|
+
return { ...DEFAULT_CONFIG };
|
|
62
|
+
try {
|
|
63
|
+
const raw = JSON.parse(readFileSync(config, "utf8"));
|
|
64
|
+
return { ...DEFAULT_CONFIG, ...raw };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return { ...DEFAULT_CONFIG };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function saveConfig(cfg) {
|
|
71
|
+
const { config } = getPaths();
|
|
72
|
+
atomicWrite(config, JSON.stringify(cfg, null, 2));
|
|
73
|
+
}
|
|
74
|
+
/** Composite scope id for a provider + model credential. */
|
|
75
|
+
export function scopeId(providerId, model) {
|
|
76
|
+
return `${providerId}::${model}`;
|
|
77
|
+
}
|
|
78
|
+
export function loadAuth(): AuthStore {
|
|
79
|
+
const { auth } = getPaths();
|
|
80
|
+
const empty: AuthStore = { version: 2, keys: {}, legacy: {} };
|
|
81
|
+
if (!existsSync(auth))
|
|
82
|
+
return empty;
|
|
83
|
+
try {
|
|
84
|
+
const raw = JSON.parse(readFileSync(auth, "utf8"));
|
|
85
|
+
const keys: Record<string, StoredCredential> = {};
|
|
86
|
+
const legacy: Record<string, string> = {};
|
|
87
|
+
const source = raw && typeof raw === "object" ? (raw.keys ?? {}) : {};
|
|
88
|
+
for (const [id, value] of Object.entries<any>(source)) {
|
|
89
|
+
if (value && typeof value === "object" && typeof value.key === "string") {
|
|
90
|
+
const [idProvider, idModel] = id.split("::");
|
|
91
|
+
const provider = value.provider || idProvider || id;
|
|
92
|
+
const model = value.model || idModel || "";
|
|
93
|
+
keys[scopeId(provider, model)] = {
|
|
94
|
+
provider,
|
|
95
|
+
model,
|
|
96
|
+
key: value.key,
|
|
97
|
+
addedAt: value.addedAt ?? "",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
else if (typeof value === "string" && value.trim()) {
|
|
101
|
+
legacy[id] = value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (raw && typeof raw === "object" && raw.legacy && typeof raw.legacy === "object") {
|
|
105
|
+
for (const [id, value] of Object.entries<any>(raw.legacy)) {
|
|
106
|
+
if (typeof value === "string" && value.trim())
|
|
107
|
+
legacy[id] = value;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { version: 2, keys, legacy };
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return empty;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function saveAuth(store: AuthStore) {
|
|
117
|
+
const { auth } = getPaths();
|
|
118
|
+
const payload: any = { version: 2, keys: store.keys };
|
|
119
|
+
if (Object.keys(store.legacy).length > 0)
|
|
120
|
+
payload.legacy = store.legacy;
|
|
121
|
+
atomicWrite(auth, JSON.stringify(payload, null, 2));
|
|
122
|
+
lockdown(auth);
|
|
123
|
+
}
|
|
124
|
+
export function getCredential(providerId, model): StoredCredential | undefined {
|
|
125
|
+
return loadAuth().keys[scopeId(providerId, model)];
|
|
126
|
+
}
|
|
127
|
+
export function listCredentials(): StoredCredential[] {
|
|
128
|
+
return Object.values(loadAuth().keys);
|
|
129
|
+
}
|
|
130
|
+
export function listLegacy(): Array<{
|
|
131
|
+
provider: string;
|
|
132
|
+
key: string;
|
|
133
|
+
}> {
|
|
134
|
+
return Object.entries<any>(loadAuth().legacy).map(([provider, key]) => ({ provider, key }));
|
|
135
|
+
}
|
|
136
|
+
export function getLegacy(providerId): string | undefined {
|
|
137
|
+
return loadAuth().legacy[providerId];
|
|
138
|
+
}
|
|
139
|
+
/** Save/replace the credential for one provider+model scope (drops the legacy key). */
|
|
140
|
+
export function setCredential(providerId, model, key): StoredCredential {
|
|
141
|
+
const store = loadAuth();
|
|
142
|
+
const credential = {
|
|
143
|
+
provider: providerId,
|
|
144
|
+
model,
|
|
145
|
+
key: key.trim(),
|
|
146
|
+
addedAt: new Date().toISOString(),
|
|
147
|
+
};
|
|
148
|
+
store.keys[scopeId(providerId, model)] = credential;
|
|
149
|
+
delete store.legacy[providerId];
|
|
150
|
+
saveAuth(store);
|
|
151
|
+
return credential;
|
|
152
|
+
}
|
|
153
|
+
export function removeCredential(providerId, model): boolean {
|
|
154
|
+
const store = loadAuth();
|
|
155
|
+
const id = scopeId(providerId, model);
|
|
156
|
+
if (!store.keys[id])
|
|
157
|
+
return false;
|
|
158
|
+
delete store.keys[id];
|
|
159
|
+
saveAuth(store);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
/** Remove every credential for a provider (all models) plus its legacy key. */
|
|
163
|
+
export function removeCredentialsForProvider(providerId): number {
|
|
164
|
+
const store = loadAuth();
|
|
165
|
+
let removed = 0;
|
|
166
|
+
for (const id of Object.keys(store.keys)) {
|
|
167
|
+
if (store.keys[id].provider === providerId) {
|
|
168
|
+
delete store.keys[id];
|
|
169
|
+
removed += 1;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (store.legacy[providerId]) {
|
|
173
|
+
delete store.legacy[providerId];
|
|
174
|
+
removed += 1;
|
|
175
|
+
}
|
|
176
|
+
if (removed > 0)
|
|
177
|
+
saveAuth(store);
|
|
178
|
+
return removed;
|
|
179
|
+
}
|
|
180
|
+
/** Turn a key into a safe terminal echo: `sk-****abcd`. */
|
|
181
|
+
export function maskKey(key) {
|
|
182
|
+
const value = (key ?? "").trim();
|
|
183
|
+
if (!value)
|
|
184
|
+
return "";
|
|
185
|
+
if (value.length <= 8)
|
|
186
|
+
return "*".repeat(value.length);
|
|
187
|
+
return value.slice(0, 3) + "****" + value.slice(-4);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Resolve the key to use for a specific provider+model:
|
|
191
|
+
* 1. the scoped credential in auth.json (exact provider+model match), then
|
|
192
|
+
* 2. the provider's environment variable (provider-scoped, any model), and
|
|
193
|
+
* 3. report an "unbound" legacy key instead of using it silently.
|
|
194
|
+
*/
|
|
195
|
+
export function resolveApiKey(providerId, model, envVars): KeyLookup {
|
|
196
|
+
const store = loadAuth();
|
|
197
|
+
const scoped = store.keys[scopeId(providerId, model)];
|
|
198
|
+
if (scoped && scoped.key)
|
|
199
|
+
return { status: "ok", key: scoped.key, source: "file" };
|
|
200
|
+
for (const envVar of envVars ?? []) {
|
|
201
|
+
const value = process.env[envVar];
|
|
202
|
+
if (value && value.trim())
|
|
203
|
+
return { status: "ok", key: value.trim(), source: "env", envVar };
|
|
204
|
+
}
|
|
205
|
+
if (store.legacy[providerId])
|
|
206
|
+
return { status: "unbound", provider: providerId, legacyKey: store.legacy[providerId] };
|
|
207
|
+
return { status: "missing" };
|
|
208
|
+
}
|
|
209
|
+
/** Best-effort key for a provider regardless of model (used to list models). */
|
|
210
|
+
export function resolveProviderKey(providerId, envVars): {
|
|
211
|
+
key?: string;
|
|
212
|
+
source?: "file" | "env";
|
|
213
|
+
envVar?: string;
|
|
214
|
+
} {
|
|
215
|
+
const store = loadAuth();
|
|
216
|
+
for (const credential of Object.values(store.keys)) {
|
|
217
|
+
if (credential.provider === providerId && credential.key)
|
|
218
|
+
return { key: credential.key, source: "file" };
|
|
219
|
+
}
|
|
220
|
+
for (const envVar of envVars ?? []) {
|
|
221
|
+
const value = process.env[envVar];
|
|
222
|
+
if (value && value.trim())
|
|
223
|
+
return { key: value.trim(), source: "env", envVar };
|
|
224
|
+
}
|
|
225
|
+
return {};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Write-then-rename so a Ctrl+C mid-write never leaves a corrupt file.
|
|
229
|
+
*
|
|
230
|
+
* The temp file MUST live in the same directory as the target so the rename
|
|
231
|
+
* is a same-filesystem operation. On many Linux distros `/tmp` is a tmpfs
|
|
232
|
+
* mount, which would make `renameSync` throw EXDEV across filesystems.
|
|
233
|
+
*/
|
|
234
|
+
function atomicWrite(target, contents) {
|
|
235
|
+
const dir = dirname(target);
|
|
236
|
+
ensureDir(dir);
|
|
237
|
+
const tmp = join(dir, `.fastcode-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);
|
|
238
|
+
writeFileSync(tmp, contents, { encoding: "utf8" });
|
|
239
|
+
renameSync(tmp, target);
|
|
240
|
+
}
|