@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.
Files changed (197) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +400 -2
  3. package/bin/fastcode.mjs +59 -0
  4. package/dist/agent/guards.d.ts +35 -0
  5. package/dist/agent/guards.js +74 -0
  6. package/dist/agent/limits.d.ts +24 -0
  7. package/dist/agent/limits.js +71 -0
  8. package/dist/agent/protocol.d.ts +4 -0
  9. package/dist/agent/protocol.js +126 -0
  10. package/dist/agent/tools.d.ts +1 -0
  11. package/dist/agent/tools.js +377 -0
  12. package/dist/app.d.ts +4 -0
  13. package/dist/app.js +476 -0
  14. package/dist/cli.d.ts +1 -0
  15. package/dist/cli.js +518 -0
  16. package/dist/commands/about.d.ts +8 -0
  17. package/dist/commands/about.js +16 -0
  18. package/dist/commands/agent.d.ts +11 -0
  19. package/dist/commands/agent.js +18 -0
  20. package/dist/commands/clear.d.ts +11 -0
  21. package/dist/commands/clear.js +9 -0
  22. package/dist/commands/config.d.ts +7 -0
  23. package/dist/commands/config.js +24 -0
  24. package/dist/commands/cost.d.ts +9 -0
  25. package/dist/commands/cost.js +13 -0
  26. package/dist/commands/cwd.d.ts +7 -0
  27. package/dist/commands/cwd.js +7 -0
  28. package/dist/commands/exit.d.ts +9 -0
  29. package/dist/commands/exit.js +8 -0
  30. package/dist/commands/help.d.ts +6 -0
  31. package/dist/commands/help.js +18 -0
  32. package/dist/commands/index.d.ts +9 -0
  33. package/dist/commands/index.js +58 -0
  34. package/dist/commands/login.d.ts +16 -0
  35. package/dist/commands/login.js +61 -0
  36. package/dist/commands/logout.d.ts +9 -0
  37. package/dist/commands/logout.js +53 -0
  38. package/dist/commands/model.d.ts +11 -0
  39. package/dist/commands/model.js +30 -0
  40. package/dist/commands/plugin.d.ts +11 -0
  41. package/dist/commands/plugin.js +74 -0
  42. package/dist/commands/provider.d.ts +11 -0
  43. package/dist/commands/provider.js +28 -0
  44. package/dist/commands/reset.d.ts +9 -0
  45. package/dist/commands/reset.js +13 -0
  46. package/dist/commands/skills.d.ts +11 -0
  47. package/dist/commands/skills.js +106 -0
  48. package/dist/commands/system.d.ts +11 -0
  49. package/dist/commands/system.js +20 -0
  50. package/dist/commands/theme.d.ts +11 -0
  51. package/dist/commands/theme.js +22 -0
  52. package/dist/commands/tools.d.ts +7 -0
  53. package/dist/commands/tools.js +29 -0
  54. package/dist/commands/whoami.d.ts +9 -0
  55. package/dist/commands/whoami.js +51 -0
  56. package/dist/config/paths.d.ts +4 -0
  57. package/dist/config/paths.js +59 -0
  58. package/dist/config/store.d.ts +69 -0
  59. package/dist/config/store.js +208 -0
  60. package/dist/config/themes.d.ts +73 -0
  61. package/dist/config/themes.js +77 -0
  62. package/dist/config/trust.d.ts +9 -0
  63. package/dist/config/trust.js +67 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +31 -0
  66. package/dist/lib/diagnostics.d.ts +41 -0
  67. package/dist/lib/diagnostics.js +139 -0
  68. package/dist/lib/http.d.ts +13 -0
  69. package/dist/lib/http.js +70 -0
  70. package/dist/lib/markdown.d.ts +1 -0
  71. package/dist/lib/markdown.js +32 -0
  72. package/dist/lib/pkg.d.ts +5 -0
  73. package/dist/lib/pkg.js +27 -0
  74. package/dist/lib/platform.d.ts +7 -0
  75. package/dist/lib/platform.js +57 -0
  76. package/dist/lib/startup.d.ts +6 -0
  77. package/dist/lib/startup.js +25 -0
  78. package/dist/lib/uiMode.d.ts +2 -0
  79. package/dist/lib/uiMode.js +7 -0
  80. package/dist/plugins/manager.d.ts +23 -0
  81. package/dist/plugins/manager.js +101 -0
  82. package/dist/providers/anthropic.d.ts +21 -0
  83. package/dist/providers/anthropic.js +107 -0
  84. package/dist/providers/custom.d.ts +24 -0
  85. package/dist/providers/custom.js +25 -0
  86. package/dist/providers/deepseek.d.ts +24 -0
  87. package/dist/providers/deepseek.js +15 -0
  88. package/dist/providers/google.d.ts +24 -0
  89. package/dist/providers/google.js +19 -0
  90. package/dist/providers/groq.d.ts +24 -0
  91. package/dist/providers/groq.js +20 -0
  92. package/dist/providers/index.d.ts +201 -0
  93. package/dist/providers/index.js +37 -0
  94. package/dist/providers/mistral.d.ts +24 -0
  95. package/dist/providers/mistral.js +19 -0
  96. package/dist/providers/moonshot.d.ts +24 -0
  97. package/dist/providers/moonshot.js +22 -0
  98. package/dist/providers/ollama.d.ts +24 -0
  99. package/dist/providers/ollama.js +22 -0
  100. package/dist/providers/openai.d.ts +24 -0
  101. package/dist/providers/openai.js +25 -0
  102. package/dist/providers/openaiCompatible.d.ts +17 -0
  103. package/dist/providers/openaiCompatible.js +110 -0
  104. package/dist/providers/openrouter.d.ts +24 -0
  105. package/dist/providers/openrouter.js +26 -0
  106. package/dist/providers/qwen.d.ts +24 -0
  107. package/dist/providers/qwen.js +23 -0
  108. package/dist/providers/xai.d.ts +24 -0
  109. package/dist/providers/xai.js +18 -0
  110. package/dist/providers/zhipu.d.ts +24 -0
  111. package/dist/providers/zhipu.js +23 -0
  112. package/dist/session/log.d.ts +29 -0
  113. package/dist/session/log.js +135 -0
  114. package/dist/skills/manager.d.ts +18 -0
  115. package/dist/skills/manager.js +201 -0
  116. package/dist/skills/parser.d.ts +18 -0
  117. package/dist/skills/parser.js +65 -0
  118. package/dist/types.d.ts +146 -0
  119. package/dist/types.js +1 -0
  120. package/dist/ui/Banner.d.ts +5 -0
  121. package/dist/ui/Banner.js +65 -0
  122. package/dist/ui/Message.d.ts +7 -0
  123. package/dist/ui/Message.js +13 -0
  124. package/dist/ui/Prompt.d.ts +10 -0
  125. package/dist/ui/Prompt.js +38 -0
  126. package/dist/ui/Spinner.d.ts +5 -0
  127. package/dist/ui/Spinner.js +33 -0
  128. package/dist/ui/StatusBar.d.ts +10 -0
  129. package/dist/ui/StatusBar.js +19 -0
  130. package/dist/ui/TrustGate.d.ts +5 -0
  131. package/dist/ui/TrustGate.js +26 -0
  132. package/package.json +96 -4
  133. package/src/agent/guards.ts +74 -0
  134. package/src/agent/limits.ts +71 -0
  135. package/src/agent/protocol.ts +126 -0
  136. package/src/agent/tools.ts +377 -0
  137. package/src/app.ts +476 -0
  138. package/src/cli.ts +308 -0
  139. package/src/commands/about.ts +16 -0
  140. package/src/commands/agent.ts +18 -0
  141. package/src/commands/clear.ts +9 -0
  142. package/src/commands/config.ts +24 -0
  143. package/src/commands/cost.ts +13 -0
  144. package/src/commands/cwd.ts +7 -0
  145. package/src/commands/exit.ts +8 -0
  146. package/src/commands/help.ts +18 -0
  147. package/src/commands/index.ts +58 -0
  148. package/src/commands/login.ts +61 -0
  149. package/src/commands/logout.ts +53 -0
  150. package/src/commands/model.ts +30 -0
  151. package/src/commands/plugin.ts +74 -0
  152. package/src/commands/provider.ts +28 -0
  153. package/src/commands/reset.ts +13 -0
  154. package/src/commands/skills.ts +106 -0
  155. package/src/commands/system.ts +20 -0
  156. package/src/commands/theme.ts +22 -0
  157. package/src/commands/tools.ts +29 -0
  158. package/src/commands/whoami.ts +51 -0
  159. package/src/config/paths.ts +59 -0
  160. package/src/config/store.ts +240 -0
  161. package/src/config/themes.ts +77 -0
  162. package/src/config/trust.ts +67 -0
  163. package/src/index.ts +31 -0
  164. package/src/lib/diagnostics.ts +139 -0
  165. package/src/lib/http.ts +70 -0
  166. package/src/lib/markdown.ts +32 -0
  167. package/src/lib/pkg.ts +35 -0
  168. package/src/lib/platform.ts +57 -0
  169. package/src/lib/startup.ts +25 -0
  170. package/src/lib/uiMode.ts +7 -0
  171. package/src/plugins/manager.ts +101 -0
  172. package/src/providers/anthropic.ts +107 -0
  173. package/src/providers/custom.ts +25 -0
  174. package/src/providers/deepseek.ts +15 -0
  175. package/src/providers/google.ts +19 -0
  176. package/src/providers/groq.ts +20 -0
  177. package/src/providers/index.ts +37 -0
  178. package/src/providers/mistral.ts +19 -0
  179. package/src/providers/moonshot.ts +22 -0
  180. package/src/providers/ollama.ts +22 -0
  181. package/src/providers/openai.ts +25 -0
  182. package/src/providers/openaiCompatible.ts +110 -0
  183. package/src/providers/openrouter.ts +26 -0
  184. package/src/providers/qwen.ts +23 -0
  185. package/src/providers/xai.ts +18 -0
  186. package/src/providers/zhipu.ts +23 -0
  187. package/src/session/log.ts +135 -0
  188. package/src/skills/manager.ts +201 -0
  189. package/src/skills/parser.ts +65 -0
  190. package/src/types.ts +143 -0
  191. package/src/ui/Banner.ts +68 -0
  192. package/src/ui/Message.ts +13 -0
  193. package/src/ui/Prompt.ts +38 -0
  194. package/src/ui/Spinner.ts +33 -0
  195. package/src/ui/StatusBar.ts +19 -0
  196. package/src/ui/TrustGate.ts +26 -0
  197. package/tsconfig.json +22 -0
@@ -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,11 @@
1
+ export declare const systemCommand: {
2
+ name: string;
3
+ description: string;
4
+ run({ raw, state, actions }: {
5
+ raw: any;
6
+ state: any;
7
+ actions: any;
8
+ }): {
9
+ message: string;
10
+ };
11
+ };
@@ -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,11 @@
1
+ export declare const themeCommand: {
2
+ name: string;
3
+ description: string;
4
+ run({ args, state, actions }: {
5
+ args: any;
6
+ state: any;
7
+ actions: any;
8
+ }): {
9
+ message: string;
10
+ };
11
+ };
@@ -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,7 @@
1
+ export declare const toolsCommand: {
2
+ name: string;
3
+ description: string;
4
+ run(): {
5
+ message: string;
6
+ };
7
+ };
@@ -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,9 @@
1
+ export declare const whoamiCommand: {
2
+ name: string;
3
+ description: string;
4
+ run({ state }: {
5
+ state: any;
6
+ }): {
7
+ message: string;
8
+ };
9
+ };
@@ -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,4 @@
1
+ export declare function getPaths(): any;
2
+ export declare function ensureDir(dir: any): void;
3
+ /** Tighten permissions on a file storing secrets where the OS supports it. */
4
+ export declare function lockdown(file: any): void;
@@ -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,69 @@
1
+ export declare const DEFAULT_CONFIG: {
2
+ providerId: string;
3
+ model: string;
4
+ theme: string;
5
+ agentMode: boolean;
6
+ systemPrompt: string;
7
+ enabledSkills: any[];
8
+ enabledPlugins: any[];
9
+ skillsRegistry: string;
10
+ lifetimeCostUsd: number;
11
+ };
12
+ export interface StoredCredential {
13
+ provider: string;
14
+ model: string;
15
+ key: string;
16
+ addedAt: string;
17
+ }
18
+ export interface AuthStore {
19
+ version: number;
20
+ /** Scoped credentials, keyed by `provider::model`. */
21
+ keys: Record<string, StoredCredential>;
22
+ /** Old provider-only keys we refuse to use until the user re-binds them. */
23
+ legacy: Record<string, string>;
24
+ }
25
+ export type KeyLookup = {
26
+ status: "ok";
27
+ key: string;
28
+ source: "file" | "env";
29
+ envVar?: string;
30
+ } | {
31
+ status: "unbound";
32
+ provider: string;
33
+ legacyKey: string;
34
+ } | {
35
+ status: "missing";
36
+ };
37
+ export declare function loadConfig(): any;
38
+ export declare function saveConfig(cfg: any): void;
39
+ /** Composite scope id for a provider + model credential. */
40
+ export declare function scopeId(providerId: any, model: any): string;
41
+ export declare function loadAuth(): AuthStore;
42
+ export declare function saveAuth(store: AuthStore): void;
43
+ export declare function getCredential(providerId: any, model: any): StoredCredential | undefined;
44
+ export declare function listCredentials(): StoredCredential[];
45
+ export declare function listLegacy(): Array<{
46
+ provider: string;
47
+ key: string;
48
+ }>;
49
+ export declare function getLegacy(providerId: any): string | undefined;
50
+ /** Save/replace the credential for one provider+model scope (drops the legacy key). */
51
+ export declare function setCredential(providerId: any, model: any, key: any): StoredCredential;
52
+ export declare function removeCredential(providerId: any, model: any): boolean;
53
+ /** Remove every credential for a provider (all models) plus its legacy key. */
54
+ export declare function removeCredentialsForProvider(providerId: any): number;
55
+ /** Turn a key into a safe terminal echo: `sk-****abcd`. */
56
+ export declare function maskKey(key: any): string;
57
+ /**
58
+ * Resolve the key to use for a specific provider+model:
59
+ * 1. the scoped credential in auth.json (exact provider+model match), then
60
+ * 2. the provider's environment variable (provider-scoped, any model), and
61
+ * 3. report an "unbound" legacy key instead of using it silently.
62
+ */
63
+ export declare function resolveApiKey(providerId: any, model: any, envVars: any): KeyLookup;
64
+ /** Best-effort key for a provider regardless of model (used to list models). */
65
+ export declare function resolveProviderKey(providerId: any, envVars: any): {
66
+ key?: string;
67
+ source?: "file" | "env";
68
+ envVar?: string;
69
+ };
@@ -0,0 +1,208 @@
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 function loadConfig() {
34
+ const { config } = getPaths();
35
+ if (!existsSync(config))
36
+ return { ...DEFAULT_CONFIG };
37
+ try {
38
+ const raw = JSON.parse(readFileSync(config, "utf8"));
39
+ return { ...DEFAULT_CONFIG, ...raw };
40
+ }
41
+ catch {
42
+ return { ...DEFAULT_CONFIG };
43
+ }
44
+ }
45
+ export function saveConfig(cfg) {
46
+ const { config } = getPaths();
47
+ atomicWrite(config, JSON.stringify(cfg, null, 2));
48
+ }
49
+ /** Composite scope id for a provider + model credential. */
50
+ export function scopeId(providerId, model) {
51
+ return `${providerId}::${model}`;
52
+ }
53
+ export function loadAuth() {
54
+ const { auth } = getPaths();
55
+ const empty = { version: 2, keys: {}, legacy: {} };
56
+ if (!existsSync(auth))
57
+ return empty;
58
+ try {
59
+ const raw = JSON.parse(readFileSync(auth, "utf8"));
60
+ const keys = {};
61
+ const legacy = {};
62
+ const source = raw && typeof raw === "object" ? (raw.keys ?? {}) : {};
63
+ for (const [id, value] of Object.entries(source)) {
64
+ if (value && typeof value === "object" && typeof value.key === "string") {
65
+ const [idProvider, idModel] = id.split("::");
66
+ const provider = value.provider || idProvider || id;
67
+ const model = value.model || idModel || "";
68
+ keys[scopeId(provider, model)] = {
69
+ provider,
70
+ model,
71
+ key: value.key,
72
+ addedAt: value.addedAt ?? "",
73
+ };
74
+ }
75
+ else if (typeof value === "string" && value.trim()) {
76
+ legacy[id] = value;
77
+ }
78
+ }
79
+ if (raw && typeof raw === "object" && raw.legacy && typeof raw.legacy === "object") {
80
+ for (const [id, value] of Object.entries(raw.legacy)) {
81
+ if (typeof value === "string" && value.trim())
82
+ legacy[id] = value;
83
+ }
84
+ }
85
+ return { version: 2, keys, legacy };
86
+ }
87
+ catch {
88
+ return empty;
89
+ }
90
+ }
91
+ export function saveAuth(store) {
92
+ const { auth } = getPaths();
93
+ const payload = { version: 2, keys: store.keys };
94
+ if (Object.keys(store.legacy).length > 0)
95
+ payload.legacy = store.legacy;
96
+ atomicWrite(auth, JSON.stringify(payload, null, 2));
97
+ lockdown(auth);
98
+ }
99
+ export function getCredential(providerId, model) {
100
+ return loadAuth().keys[scopeId(providerId, model)];
101
+ }
102
+ export function listCredentials() {
103
+ return Object.values(loadAuth().keys);
104
+ }
105
+ export function listLegacy() {
106
+ return Object.entries(loadAuth().legacy).map(([provider, key]) => ({ provider, key }));
107
+ }
108
+ export function getLegacy(providerId) {
109
+ return loadAuth().legacy[providerId];
110
+ }
111
+ /** Save/replace the credential for one provider+model scope (drops the legacy key). */
112
+ export function setCredential(providerId, model, key) {
113
+ const store = loadAuth();
114
+ const credential = {
115
+ provider: providerId,
116
+ model,
117
+ key: key.trim(),
118
+ addedAt: new Date().toISOString(),
119
+ };
120
+ store.keys[scopeId(providerId, model)] = credential;
121
+ delete store.legacy[providerId];
122
+ saveAuth(store);
123
+ return credential;
124
+ }
125
+ export function removeCredential(providerId, model) {
126
+ const store = loadAuth();
127
+ const id = scopeId(providerId, model);
128
+ if (!store.keys[id])
129
+ return false;
130
+ delete store.keys[id];
131
+ saveAuth(store);
132
+ return true;
133
+ }
134
+ /** Remove every credential for a provider (all models) plus its legacy key. */
135
+ export function removeCredentialsForProvider(providerId) {
136
+ const store = loadAuth();
137
+ let removed = 0;
138
+ for (const id of Object.keys(store.keys)) {
139
+ if (store.keys[id].provider === providerId) {
140
+ delete store.keys[id];
141
+ removed += 1;
142
+ }
143
+ }
144
+ if (store.legacy[providerId]) {
145
+ delete store.legacy[providerId];
146
+ removed += 1;
147
+ }
148
+ if (removed > 0)
149
+ saveAuth(store);
150
+ return removed;
151
+ }
152
+ /** Turn a key into a safe terminal echo: `sk-****abcd`. */
153
+ export function maskKey(key) {
154
+ const value = (key ?? "").trim();
155
+ if (!value)
156
+ return "";
157
+ if (value.length <= 8)
158
+ return "*".repeat(value.length);
159
+ return value.slice(0, 3) + "****" + value.slice(-4);
160
+ }
161
+ /**
162
+ * Resolve the key to use for a specific provider+model:
163
+ * 1. the scoped credential in auth.json (exact provider+model match), then
164
+ * 2. the provider's environment variable (provider-scoped, any model), and
165
+ * 3. report an "unbound" legacy key instead of using it silently.
166
+ */
167
+ export function resolveApiKey(providerId, model, envVars) {
168
+ const store = loadAuth();
169
+ const scoped = store.keys[scopeId(providerId, model)];
170
+ if (scoped && scoped.key)
171
+ return { status: "ok", key: scoped.key, source: "file" };
172
+ for (const envVar of envVars ?? []) {
173
+ const value = process.env[envVar];
174
+ if (value && value.trim())
175
+ return { status: "ok", key: value.trim(), source: "env", envVar };
176
+ }
177
+ if (store.legacy[providerId])
178
+ return { status: "unbound", provider: providerId, legacyKey: store.legacy[providerId] };
179
+ return { status: "missing" };
180
+ }
181
+ /** Best-effort key for a provider regardless of model (used to list models). */
182
+ export function resolveProviderKey(providerId, envVars) {
183
+ const store = loadAuth();
184
+ for (const credential of Object.values(store.keys)) {
185
+ if (credential.provider === providerId && credential.key)
186
+ return { key: credential.key, source: "file" };
187
+ }
188
+ for (const envVar of envVars ?? []) {
189
+ const value = process.env[envVar];
190
+ if (value && value.trim())
191
+ return { key: value.trim(), source: "env", envVar };
192
+ }
193
+ return {};
194
+ }
195
+ /**
196
+ * Write-then-rename so a Ctrl+C mid-write never leaves a corrupt file.
197
+ *
198
+ * The temp file MUST live in the same directory as the target so the rename
199
+ * is a same-filesystem operation. On many Linux distros `/tmp` is a tmpfs
200
+ * mount, which would make `renameSync` throw EXDEV across filesystems.
201
+ */
202
+ function atomicWrite(target, contents) {
203
+ const dir = dirname(target);
204
+ ensureDir(dir);
205
+ const tmp = join(dir, `.fastcode-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);
206
+ writeFileSync(tmp, contents, { encoding: "utf8" });
207
+ renameSync(tmp, target);
208
+ }