@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,77 @@
1
+ /**
2
+ * Built-in color themes. The `banner` triple is fed to gradient-string
3
+ * (start → mid → end) so each theme produces a recognizable big-text logo.
4
+ */
5
+ export const THEMES = {
6
+ fastcode: {
7
+ id: "fastcode",
8
+ label: "FastCode (default)",
9
+ banner: ["#ff7a00", "#ff2d95", "#7c4dff"],
10
+ user: "cyan",
11
+ assistant: "white",
12
+ system: "gray",
13
+ border: "magenta",
14
+ accent: "magenta",
15
+ muted: "gray",
16
+ error: "red",
17
+ success: "green",
18
+ },
19
+ neon: {
20
+ id: "neon",
21
+ label: "Neon",
22
+ banner: ["#00ffd0", "#00b3ff", "#7c4dff"],
23
+ user: "cyanBright",
24
+ assistant: "whiteBright",
25
+ system: "gray",
26
+ border: "cyan",
27
+ accent: "cyan",
28
+ muted: "gray",
29
+ error: "red",
30
+ success: "greenBright",
31
+ },
32
+ sunset: {
33
+ id: "sunset",
34
+ label: "Sunset",
35
+ banner: ["#ff5f6d", "#ffc371", "#ff9966"],
36
+ user: "yellow",
37
+ assistant: "whiteBright",
38
+ system: "gray",
39
+ border: "yellow",
40
+ accent: "yellow",
41
+ muted: "gray",
42
+ error: "redBright",
43
+ success: "green",
44
+ },
45
+ matrix: {
46
+ id: "matrix",
47
+ label: "Matrix",
48
+ banner: ["#00ff41", "#008f11", "#003b00"],
49
+ user: "greenBright",
50
+ assistant: "green",
51
+ system: "gray",
52
+ border: "green",
53
+ accent: "green",
54
+ muted: "gray",
55
+ error: "redBright",
56
+ success: "greenBright",
57
+ },
58
+ mono: {
59
+ id: "mono",
60
+ label: "Monochrome",
61
+ banner: ["#ffffff", "#cccccc", "#888888"],
62
+ user: "white",
63
+ assistant: "whiteBright",
64
+ system: "gray",
65
+ border: "white",
66
+ accent: "white",
67
+ muted: "gray",
68
+ error: "red",
69
+ success: "white",
70
+ },
71
+ };
72
+ export const DEFAULT_THEME = "fastcode";
73
+ export function getTheme(id) {
74
+ if (id && THEMES[id])
75
+ return THEMES[id];
76
+ return THEMES[DEFAULT_THEME];
77
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Project trust store.
3
+ *
4
+ * Agent tools can read, write, delete and execute inside the workspace, so a
5
+ * folder is only enabled after the user confirms it once. The list lives in
6
+ * ~/.fastcode/trusted.json and is compared by real path.
7
+ *
8
+ * FASTCODE_TRUST_ALL=1 marks every directory trusted (scripts, CI).
9
+ */
10
+ import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+ import { ensureDir, getPaths } from "./paths.js";
13
+ const TRUTHY = ["1", "true", "yes", "on"];
14
+ export function normalizeProjectPath(dir) {
15
+ const absolute = resolve(dir);
16
+ try {
17
+ return realpathSync(absolute);
18
+ }
19
+ catch {
20
+ return absolute;
21
+ }
22
+ }
23
+ export function loadTrust() {
24
+ const { trust } = getPaths();
25
+ if (!existsSync(trust))
26
+ return { trusted: [] };
27
+ try {
28
+ const raw = JSON.parse(readFileSync(trust, "utf8"));
29
+ return { trusted: Array.isArray(raw.trusted) ? raw.trusted.filter((entry) => entry && typeof entry.path === "string") : [] };
30
+ }
31
+ catch {
32
+ return { trusted: [] };
33
+ }
34
+ }
35
+ function saveTrust(state) {
36
+ const paths = getPaths();
37
+ ensureDir(paths.root);
38
+ writeFileSync(paths.trust, JSON.stringify(state, null, 2), "utf8");
39
+ }
40
+ export function trustAllEnabled() {
41
+ return TRUTHY.includes((process.env["FASTCODE_TRUST_ALL"] ?? "").trim().toLowerCase());
42
+ }
43
+ export function isTrusted(dir) {
44
+ if (trustAllEnabled())
45
+ return true;
46
+ const target = normalizeProjectPath(dir);
47
+ return loadTrust().trusted.some((entry) => entry.path === target);
48
+ }
49
+ export function trustProject(dir) {
50
+ const target = normalizeProjectPath(dir);
51
+ const state = loadTrust();
52
+ if (!state.trusted.some((entry) => entry.path === target)) {
53
+ state.trusted.push({ path: target, addedAt: new Date().toISOString() });
54
+ saveTrust(state);
55
+ }
56
+ return target;
57
+ }
58
+ export function untrustProject(dir) {
59
+ const target = normalizeProjectPath(dir);
60
+ const state = loadTrust();
61
+ const next = state.trusted.filter((entry) => entry.path !== target);
62
+ saveTrust({ trusted: next });
63
+ return state.trusted.length !== next.length;
64
+ }
65
+ export function listTrusted() {
66
+ return loadTrust().trusted;
67
+ }
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * FastCode entrypoint.
3
+ *
4
+ * Parses CLI args, then either runs a one-shot subcommand or boots the
5
+ * Ink-based TUI. We keep this file tiny so the slow imports (Ink, React)
6
+ * are not loaded for `fastcode skills list`.
7
+ */
8
+ import { parseCli } from "./cli.js";
9
+ async function main() {
10
+ const cli = await parseCli();
11
+ if (cli.done)
12
+ return;
13
+ if (!process.stdout.isTTY) {
14
+ console.error("FastCode TUI requires a TTY. Pipe-mode prompts aren't supported yet — try `fastcode \"<prompt>\"` from a real terminal.");
15
+ process.exit(1);
16
+ }
17
+ // Lazy-load Ink so subcommands don't pay the React boot cost.
18
+ const [{ render }, React, { App }] = await Promise.all([
19
+ import("ink"),
20
+ import("react"),
21
+ import("./app.js"),
22
+ ]);
23
+ // React.createElement avoids needing a separate .tsx loader for this file.
24
+ // exitOnCtrlC: false lets the app own Ctrl+C so the documented
25
+ // "press Ctrl+C twice to quit" behavior actually works.
26
+ render(React.createElement(App, { initialPrompt: cli.prompt, resume: cli.resume }), { exitOnCtrlC: false });
27
+ }
28
+ main().catch((err) => {
29
+ console.error(err);
30
+ process.exit(1);
31
+ });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Environment diagnostics for mobile / Termux troubleshooting.
3
+ *
4
+ * `fastcode doctor` prints the values below so a broken mobile install can be
5
+ * diagnosed without opening the TUI: Node version, Termux detection, whether
6
+ * stdout is a real terminal, the config root, and which provider keys exist.
7
+ */
8
+ import { detectPlatform, platformLabel } from "./platform.js";
9
+ import { readPackageMeta } from "./pkg.js";
10
+ import { shouldUseStableTerminalUi } from "./uiMode.js";
11
+ import { getPaths } from "../config/paths.js";
12
+ import { loadConfig, resolveApiKey, resolveProviderKey } from "../config/store.js";
13
+ import { PROVIDERS } from "../providers/index.js";
14
+ import { listInstalledSkills } from "../skills/manager.js";
15
+ import { listInstalledPlugins } from "../plugins/manager.js";
16
+ import { loggingEnabled, listSessions, sessionCacheLimitBytes } from "../session/log.js";
17
+ import { isTrusted } from "../config/trust.js";
18
+ import { DEFAULT_DENY_PATTERNS } from "../agent/guards.js";
19
+ import { toolOutputLimit } from "../agent/limits.js";
20
+ const NODE_REQUIRED = "18.17";
21
+ function readVersion() {
22
+ return readPackageMeta().version;
23
+ }
24
+ function nodeIsSupported() {
25
+ const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
26
+ const [reqMajor = 0, reqMinor = 0] = NODE_REQUIRED.split(".").map(Number);
27
+ return major > reqMajor || (major === reqMajor && minor >= reqMinor);
28
+ }
29
+ export function collectDiagnostics() {
30
+ const config = loadConfig();
31
+ let paths;
32
+ try {
33
+ paths = getPaths();
34
+ }
35
+ catch {
36
+ paths = undefined;
37
+ }
38
+ const provider = PROVIDERS.find((p) => p.id === config.providerId);
39
+ const keyLookup = provider && provider.authEnv.length > 0
40
+ ? resolveApiKey(provider.id, config.model, provider.authEnv)
41
+ : { status: "ok" as const };
42
+ const keyPresent = Boolean(provider) && (provider.authEnv.length === 0 || keyLookup.status === "ok");
43
+ let skillsInstalled = 0;
44
+ let pluginsInstalled = 0;
45
+ try {
46
+ skillsInstalled = listInstalledSkills().length;
47
+ }
48
+ catch {
49
+ // A broken skills dir must not break the whole report.
50
+ }
51
+ try {
52
+ pluginsInstalled = listInstalledPlugins().length;
53
+ }
54
+ catch {
55
+ // Same for plugins.
56
+ }
57
+ const platform = detectPlatform();
58
+ return {
59
+ version: readVersion(),
60
+ node: process.version,
61
+ nodeRequired: NODE_REQUIRED,
62
+ nodeOk: nodeIsSupported(),
63
+ platform,
64
+ platformLabel: platformLabel(),
65
+ termux: platform === "termux",
66
+ termuxVersion: process.env["TERMUX_VERSION"] ?? null,
67
+ stableUi: shouldUseStableTerminalUi(),
68
+ tty: Boolean(process.stdout.isTTY),
69
+ configRoot: paths?.root,
70
+ configFile: paths?.config,
71
+ authFile: paths?.auth,
72
+ providerId: config.providerId,
73
+ model: config.model,
74
+ keyPresent,
75
+ keyStatus: provider ? keyLookup.status : "missing",
76
+ keyEnv: provider?.authEnv?.[0],
77
+ skillsInstalled,
78
+ pluginsInstalled,
79
+ enabledSkills: (config.enabledSkills ?? []).length,
80
+ enabledPlugins: (config.enabledPlugins ?? []).length,
81
+ sessionLogEnabled: loggingEnabled(),
82
+ sessionCacheMb: Math.round(sessionCacheLimitBytes() / (1024 * 1024)),
83
+ sessionsDir: paths?.sessions,
84
+ sessionsCount: listSessions().length,
85
+ sessionsBytes: listSessions().reduce((sum, entry) => sum + entry.bytes, 0),
86
+ trustFile: paths?.trust,
87
+ trusted: isTrusted(process.cwd()),
88
+ shellDenyPatterns: DEFAULT_DENY_PATTERNS.length,
89
+ allowDangerous: ["1", "true", "yes", "on"].includes((process.env["FASTCODE_ALLOW_DANGEROUS"] ?? "").trim().toLowerCase()),
90
+ toolOutputLimit: toolOutputLimit(),
91
+ providers: PROVIDERS.map((p) => ({
92
+ id: p.id,
93
+ label: p.label,
94
+ env: p.authEnv,
95
+ keyPresent: p.authEnv.length === 0 || Boolean(resolveProviderKey(p.id, p.authEnv).key),
96
+ })),
97
+ };
98
+ }
99
+ export function formatDiagnostics(info) {
100
+ const lines = [];
101
+ lines.push(`FastCode ${info.version} — environment check`);
102
+ lines.push("");
103
+ lines.push(` node ${info.node} (needs >= ${info.nodeRequired}) [${info.nodeOk ? "ok" : "TOO OLD"}]`);
104
+ lines.push(` platform ${info.platformLabel}${info.termux ? " (Termux)" : ""}`);
105
+ lines.push(` terminal ${info.tty ? "interactive TTY" : "NOT a TTY — the TUI needs a real terminal"}`);
106
+ lines.push(` stable UI ${info.stableUi ? "on (anti-flicker)" : "off"}`);
107
+ lines.push(" config root " + (info.configRoot ?? "(unavailable)"));
108
+ lines.push(" config file " + (info.configFile ?? "(unavailable)"));
109
+ lines.push(" keys file " + (info.authFile ?? "(unavailable)") + " (separate, chmod 0600)");
110
+ lines.push(" trusted cwd " + (info.trusted ? "yes" : "NO - agent tools ask first or stay off") + " (" + (info.trustFile ?? "?") + ")");
111
+ lines.push(" session log " + (info.sessionLogEnabled ? "on" : "off") + " cache cap " + info.sessionCacheMb + "MB, " + info.sessionsCount + " file(s), " + (info.sessionsBytes / 1024 / 1024).toFixed(2) + "MB used");
112
+ lines.push(" session dir " + (info.sessionsDir ?? "(unavailable)"));
113
+ lines.push(" shell guard " + info.shellDenyPatterns + " deny patterns" + (info.allowDangerous ? " [FASTCODE_ALLOW_DANGEROUS is ON]" : ""));
114
+ lines.push(" tool output " + info.toolOutputLimit + " chars max (streaming cap)");
115
+ lines.push(` provider ${info.providerId} · model ${info.model}`);
116
+ let apiKeyStatus;
117
+ if (info.keyPresent)
118
+ apiKeyStatus = "present";
119
+ else if (info.keyStatus === "unbound")
120
+ apiKeyStatus = `UNBOUND — a stored ${info.providerId} key isn't tied to a model; re-bind with /login ${info.providerId} ${info.model} <key>`;
121
+ else
122
+ apiKeyStatus = `MISSING — set ${info.keyEnv ?? "the provider env var"} or /login ${info.providerId} ${info.model} <key>`;
123
+ lines.push(` api key ${apiKeyStatus}`);
124
+ lines.push(` skills ${info.skillsInstalled} installed, ${info.enabledSkills} enabled`);
125
+ lines.push(` plugins ${info.pluginsInstalled} installed, ${info.enabledPlugins} enabled`);
126
+ lines.push("");
127
+ lines.push(" provider keys:");
128
+ for (const p of info.providers) {
129
+ lines.push(` ${p.keyPresent ? "●" : "○"} ${p.id.padEnd(12)} ${p.env.join(" / ") || "(no key needed)"}`);
130
+ }
131
+ if (info.termux) {
132
+ lines.push("");
133
+ lines.push(" Termux tips:");
134
+ lines.push(" - pkg install nodejs git");
135
+ lines.push(" - keep the session alive with: termux-wake-lock");
136
+ lines.push(" - add an extra-keys row for Esc/Ctrl in ~/.termux/termux.properties");
137
+ }
138
+ return lines.join("\n");
139
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Tiny fetch helpers. We standardize on globalThis.fetch (Node 18+).
3
+ *
4
+ * The `streamSSE` helper parses Server-Sent Events for any provider that uses
5
+ * the OpenAI-style `data: {...}\n\n` chunk format. Non-SSE providers (e.g.
6
+ * Anthropic's older streaming) handle their own parsing.
7
+ */
8
+ export async function fetchJson(url: string, init: any = {}): Promise<any> {
9
+ const res = await fetch(url, {
10
+ ...init,
11
+ headers: { "content-type": "application/json", accept: "application/json", ...(init.headers ?? {}) },
12
+ });
13
+ if (!res.ok) {
14
+ const body = await res.text().catch(() => "");
15
+ throw new Error(`${res.status} ${res.statusText}: ${body.slice(0, 500)}`);
16
+ }
17
+ return (await res.json());
18
+ }
19
+ /** Async iterator over SSE events from a streaming HTTP response. */
20
+ export async function* streamSSE(res) {
21
+ if (!res.ok) {
22
+ const body = await res.text().catch(() => "");
23
+ throw new Error(`${res.status} ${res.statusText}: ${body.slice(0, 500)}`);
24
+ }
25
+ if (!res.body)
26
+ throw new Error("Empty response body");
27
+ const reader = res.body.getReader();
28
+ const decoder = new TextDecoder("utf-8");
29
+ let buffer = "";
30
+ while (true) {
31
+ const { value, done } = await reader.read();
32
+ if (done)
33
+ break;
34
+ // Normalize CR/LF so both "\n\n" and "\r\n\r\n" frame separators
35
+ // are detected, including a CR/LF split across two chunks.
36
+ buffer = (buffer + decoder.decode(value, { stream: true })).replace(/\r\n?/g, "\n");
37
+ let sep = buffer.indexOf("\n\n");
38
+ while (sep !== -1) {
39
+ const chunk = buffer.slice(0, sep);
40
+ buffer = buffer.slice(sep + 2);
41
+ const ev = parseSseChunk(chunk);
42
+ if (ev)
43
+ yield ev;
44
+ sep = buffer.indexOf("\n\n");
45
+ }
46
+ }
47
+ // Flush trailing event without final \n\n (some servers do this).
48
+ const trailing = buffer.trim();
49
+ if (trailing) {
50
+ const ev = parseSseChunk(trailing);
51
+ if (ev)
52
+ yield ev;
53
+ }
54
+ }
55
+ function parseSseChunk(chunk) {
56
+ let event;
57
+ const dataLines = [];
58
+ for (const lineRaw of chunk.split("\n")) {
59
+ const line = lineRaw.replace(/\r$/, "");
60
+ if (!line || line.startsWith(":"))
61
+ continue;
62
+ if (line.startsWith("event:"))
63
+ event = line.slice(6).trim();
64
+ else if (line.startsWith("data:"))
65
+ dataLines.push(line.slice(5).trimStart());
66
+ }
67
+ if (dataLines.length === 0)
68
+ return undefined;
69
+ return { event, data: dataLines.join("\n") };
70
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Markdown → ANSI rendering for terminal output.
3
+ *
4
+ * We use marked + marked-terminal because it gives us proper code-fence
5
+ * syntax highlighting (via cli-highlight) without us hand-rolling a
6
+ * markdown parser. The result is a fully-formatted ANSI string we can
7
+ * print directly inside an Ink <Text>.
8
+ */
9
+ import { marked } from "marked";
10
+ import { markedTerminal } from "marked-terminal";
11
+ let configured = false;
12
+ function configure() {
13
+ if (configured)
14
+ return;
15
+ marked.use(markedTerminal({
16
+ reflowText: false,
17
+ tab: 2,
18
+ }) as any);
19
+ configured = true;
20
+ }
21
+ export function renderMarkdown(md) {
22
+ configure();
23
+ try {
24
+ const out = marked.parse(md) as string;
25
+ // marked-terminal sometimes appends a stray trailing newline; trim only
26
+ // trailing whitespace so we don't lose meaningful blank lines.
27
+ return out.replace(/\s+$/, "");
28
+ }
29
+ catch {
30
+ return md;
31
+ }
32
+ }
package/src/lib/pkg.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Read this package's package.json (name + version) from any layout.
3
+ *
4
+ * The same source runs three ways, so `import.meta.url` points at different
5
+ * depths; try every plausible relative path and fall back gracefully.
6
+ *
7
+ * - tsx / dev: src/lib/pkg.ts -> ../../package.json
8
+ * - tsc output: dist/lib/pkg.js -> ../../package.json
9
+ * - esbuild bundle: dist/cli.js -> ../package.json
10
+ */
11
+ import { readFileSync } from "node:fs";
12
+
13
+ export interface PackageMeta {
14
+ name: string;
15
+ version: string;
16
+ }
17
+
18
+ const FALLBACK: PackageMeta = { name: "@datename/fastcode-termux", version: "0.0.0" };
19
+
20
+ const CANDIDATES = ["../package.json", "../../package.json", "../../../package.json"];
21
+
22
+ export function readPackageMeta(): PackageMeta {
23
+ for (const rel of CANDIDATES) {
24
+ try {
25
+ const meta = JSON.parse(readFileSync(new URL(rel, import.meta.url), "utf8"));
26
+ if (meta && typeof meta.version === "string") {
27
+ return { name: meta.name ?? FALLBACK.name, version: meta.version };
28
+ }
29
+ }
30
+ catch {
31
+ /* try the next candidate */
32
+ }
33
+ }
34
+ return FALLBACK;
35
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Cross-platform detection and small helpers.
3
+ *
4
+ * FastCode targets Termux, Linux, macOS, and Windows. Most of our
5
+ * environment-aware behavior keys off these flags rather than scattering
6
+ * `process.platform === "..."` checks across the codebase.
7
+ */
8
+ import { platform, homedir } from "node:os";
9
+ export function detectPlatform() {
10
+ // Termux exposes PREFIX=/data/data/com.termux/files/usr. The extra check
11
+ // makes us robust to users who manually export PREFIX for other reasons.
12
+ const prefix = process.env["PREFIX"];
13
+ // Termux exports TERMUX_VERSION; PREFIX=/data/data/com.termux/files/usr is a
14
+ // strong secondary signal. Neither requires the directory to exist yet.
15
+ if (process.platform === "android" ||
16
+ Boolean(process.env["TERMUX_VERSION"]) ||
17
+ (prefix && prefix.includes("com.termux"))) {
18
+ return "termux";
19
+ }
20
+ switch (process.platform) {
21
+ case "linux":
22
+ case "darwin":
23
+ case "win32":
24
+ return process.platform;
25
+ default:
26
+ return "other";
27
+ }
28
+ }
29
+ export const isTermux = () => detectPlatform() === "termux";
30
+ export const isWindows = () => process.platform === "win32";
31
+ export const isMac = () => process.platform === "darwin";
32
+ /** Human-readable label for the status bar. */
33
+ export function platformLabel() {
34
+ const p = detectPlatform();
35
+ switch (p) {
36
+ case "termux":
37
+ return "Termux";
38
+ case "linux":
39
+ return "Linux";
40
+ case "darwin":
41
+ return "macOS";
42
+ case "win32":
43
+ return "Windows";
44
+ default:
45
+ return platform();
46
+ }
47
+ }
48
+ export function safeHome() {
49
+ // os.homedir() can return undefined on weird CI containers; fall back
50
+ // to $HOME / %USERPROFILE% / cwd in that order.
51
+ const fromOs = homedir();
52
+ if (fromOs)
53
+ return fromOs;
54
+ return (process.env["HOME"] ??
55
+ process.env["USERPROFILE"] ??
56
+ process.cwd());
57
+ }
@@ -0,0 +1,25 @@
1
+ import { resolveApiKey } from "../config/store.js";
2
+ import { getProviderOrThrow } from "../providers/index.js";
3
+ export function createStartupTranscript(config) {
4
+ const messages = [
5
+ `Welcome to FastCode. Provider: **${config.providerId}** · model: **${config.model}**.\n\nType **/help** to list slash commands, or just start chatting.`,
6
+ ];
7
+ const provider = getProviderOrThrow(config.providerId);
8
+ if (provider.authEnv.length > 0) {
9
+ const lookup = resolveApiKey(provider.id, config.model, provider.authEnv);
10
+ if (lookup.status === "unbound") {
11
+ messages.push(`Stored ${provider.label} key isn't bound to a model, so it won't be used. ` +
12
+ `Re-bind it with \`/login ${provider.id} ${config.model} <key>\` (or \`/logout ${provider.id}\` to discard it).`);
13
+ }
14
+ else if (lookup.status === "missing") {
15
+ messages.push(`No API key for ${provider.label} · ${config.model}. ` +
16
+ `Run \`/login ${provider.id} ${config.model} <key>\`, or set ${provider.authEnv[0]} (provider-wide, any model).`);
17
+ }
18
+ }
19
+ return messages.map((content, index) => ({
20
+ id: index + 1,
21
+ role: "system",
22
+ content,
23
+ markdown: true,
24
+ }));
25
+ }
@@ -0,0 +1,7 @@
1
+ import { detectPlatform } from "./platform.js";
2
+ export function shouldUseStableTerminalUi(platform = detectPlatform(), env = process.env) {
3
+ return platform === "termux" || env.FASTCODE_STABLE_UI === "1";
4
+ }
5
+ export function shouldUseLiveStreamingUi(platform = detectPlatform(), env = process.env) {
6
+ return !shouldUseStableTerminalUi(platform, env);
7
+ }
@@ -0,0 +1,101 @@
1
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { getPaths } from "../config/paths.js";
4
+ export function listInstalledPlugins() {
5
+ const { plugins } = getPaths();
6
+ if (!existsSync(plugins))
7
+ return [];
8
+ return readdirSync(plugins, { withFileTypes: true })
9
+ .filter((entry) => entry.isDirectory())
10
+ .map((entry) => loadPlugin(join(plugins, entry.name), entry.name))
11
+ .filter((plugin) => plugin !== undefined)
12
+ .sort((a, b) => a.name.localeCompare(b.name));
13
+ }
14
+ export function getPlugin(name) {
15
+ return listInstalledPlugins().find((p) => p.name === name || basename(p.path) === name);
16
+ }
17
+ export function importPlugin(source, name) {
18
+ const { plugins } = getPaths();
19
+ const src = resolve(source);
20
+ if (!existsSync(src))
21
+ throw new Error(`Plugin source does not exist: ${source}`);
22
+ if (!statSync(src).isDirectory())
23
+ throw new Error("Plugin source must be a directory");
24
+ const folder = slugify(name ?? basename(src));
25
+ const target = join(plugins, folder);
26
+ if (existsSync(target))
27
+ rmSync(target, { recursive: true, force: true });
28
+ mkdirSync(target, { recursive: true });
29
+ cpSync(src, target, { recursive: true });
30
+ writeFileSync(join(target, ".fastcode-source"), src);
31
+ const loaded = loadPlugin(target, folder);
32
+ if (!loaded)
33
+ throw new Error("Plugin must include PLUGIN.md, README.md, or at least one SKILL.md");
34
+ return loaded;
35
+ }
36
+ export function removePlugin(name) {
37
+ const plugin = getPlugin(name);
38
+ if (!plugin)
39
+ return false;
40
+ rmSync(plugin.path, { recursive: true, force: true });
41
+ return true;
42
+ }
43
+ export function buildPluginsPrompt(enabled) {
44
+ if (enabled.length === 0)
45
+ return "";
46
+ const plugins = enabled.map(getPlugin).filter((p) => p !== undefined);
47
+ if (plugins.length === 0)
48
+ return "";
49
+ const chunks = plugins.map((plugin) => {
50
+ const skills = plugin.skills
51
+ .map((skill) => `<plugin-skill name="${skill.name}">\n${skill.body.trim()}\n</plugin-skill>`)
52
+ .join("\n\n");
53
+ return `<plugin name="${plugin.name}">\n${plugin.body.trim()}\n${skills ? `\n\n${skills}` : ""}\n</plugin>`;
54
+ });
55
+ return [
56
+ "",
57
+ "## Active plugins",
58
+ "The following plugins are always-active behavior packs. Follow them when relevant:",
59
+ "",
60
+ chunks.join("\n\n"),
61
+ ].join("\n");
62
+ }
63
+ function loadPlugin(path, fallbackName) {
64
+ const pluginMd = firstExisting([join(path, "PLUGIN.md"), join(path, "README.md")]);
65
+ const skillsDir = join(path, "skills");
66
+ const rootSkill = join(path, "SKILL.md");
67
+ const skills = [];
68
+ if (existsSync(rootSkill))
69
+ skills.push({ name: "root", body: readFileSync(rootSkill, "utf8") });
70
+ if (existsSync(skillsDir)) {
71
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
72
+ if (!entry.isDirectory())
73
+ continue;
74
+ const skillPath = join(skillsDir, entry.name, "SKILL.md");
75
+ if (existsSync(skillPath))
76
+ skills.push({ name: entry.name, body: readFileSync(skillPath, "utf8") });
77
+ }
78
+ }
79
+ if (!pluginMd && skills.length === 0)
80
+ return undefined;
81
+ const body = pluginMd ? readFileSync(pluginMd, "utf8") : `Plugin ${fallbackName}`;
82
+ return {
83
+ name: fallbackName,
84
+ description: firstParagraph(body) || `${fallbackName} plugin`,
85
+ path,
86
+ body,
87
+ skills,
88
+ };
89
+ }
90
+ function firstExisting(paths) {
91
+ return paths.find((path) => existsSync(path));
92
+ }
93
+ function firstParagraph(markdown) {
94
+ return markdown
95
+ .split(/\n\s*\n/)
96
+ .map((part) => part.replace(/^#+\s*/, "").trim())
97
+ .find(Boolean);
98
+ }
99
+ function slugify(value) {
100
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "plugin";
101
+ }