@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,135 @@
1
+ /**
2
+ * Append-only JSONL session log.
3
+ *
4
+ * Every entry is flushed as its own line, so a crash (or a kill from the
5
+ * Android task switcher) still leaves a readable transcript that can be
6
+ * resumed. A size cap keeps the sessions directory bounded; the oldest logs are
7
+ * pruned first.
8
+ *
9
+ * FASTCODE_SESSION_LOG=0 disable logging
10
+ * FASTCODE_SESSION_CACHE_MB=N sessions cache cap in MB (default 256)
11
+ */
12
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { getPaths } from "../config/paths.js";
15
+ const NL = String.fromCharCode(10);
16
+ export function sessionCacheLimitBytes() {
17
+ const mb = Number(process.env["FASTCODE_SESSION_CACHE_MB"]);
18
+ return (Number.isFinite(mb) && mb > 0 ? mb : 256) * 1024 * 1024;
19
+ }
20
+ export function loggingEnabled() {
21
+ return process.env["FASTCODE_SESSION_LOG"] !== "0";
22
+ }
23
+ function newId() {
24
+ return new Date().toISOString().replace(/[:.]/g, "-") + "-" + Math.random().toString(36).slice(2, 6);
25
+ }
26
+ function openLog(id, file, meta) {
27
+ let closed = false;
28
+ const append = (entry) => {
29
+ if (closed || !loggingEnabled())
30
+ return;
31
+ try {
32
+ appendFileSync(file, JSON.stringify({ ts: Date.now(), ...entry }) + NL, "utf8");
33
+ }
34
+ catch {
35
+ // A read-only or full filesystem must not break the TUI.
36
+ }
37
+ };
38
+ append({ type: "session_start", id, cwd: process.cwd(), ...meta });
39
+ return {
40
+ id,
41
+ file,
42
+ append,
43
+ close() {
44
+ append({ type: "session_end" });
45
+ closed = true;
46
+ try {
47
+ pruneSessions();
48
+ }
49
+ catch {
50
+ // best effort
51
+ }
52
+ },
53
+ };
54
+ }
55
+ export function createSessionLog(meta = {}) {
56
+ const { sessions } = getPaths();
57
+ mkdirSync(sessions, { recursive: true });
58
+ const id = newId();
59
+ const file = join(sessions, id + ".jsonl");
60
+ return openLog(id, file, meta);
61
+ }
62
+ export function openSessionLog(file) {
63
+ const { sessions } = getPaths();
64
+ mkdirSync(sessions, { recursive: true });
65
+ return openLog(undefined, existsSync(file) ? file : join(sessions, file), {});
66
+ }
67
+ export function listSessions() {
68
+ const { sessions } = getPaths();
69
+ if (!existsSync(sessions))
70
+ return [];
71
+ return readdirSync(sessions)
72
+ .filter((name) => name.endsWith(".jsonl"))
73
+ .map((name) => {
74
+ const file = join(sessions, name);
75
+ let bytes = 0;
76
+ let modified = "";
77
+ let messages = 0;
78
+ try {
79
+ const info = statSync(file);
80
+ bytes = info.size;
81
+ modified = info.mtime.toISOString();
82
+ messages = readFileSync(file, "utf8").split(NL).filter((line) => line.includes('"type":"message"')).length;
83
+ }
84
+ catch {
85
+ // unreadable: report what we have
86
+ }
87
+ return { id: name.replace(/[.]jsonl$/, ""), file, bytes, modified, messages };
88
+ })
89
+ .sort((a, b) => (a.modified < b.modified ? 1 : -1));
90
+ }
91
+ export function resolveSessionFile(idOrFile) {
92
+ const { sessions } = getPaths();
93
+ const looksLikePath = idOrFile.includes("/") || idOrFile.includes(String.fromCharCode(92)) || idOrFile.endsWith(".jsonl");
94
+ return looksLikePath ? idOrFile : join(sessions, idOrFile + ".jsonl");
95
+ }
96
+ export function readSession(idOrFile) {
97
+ const file = resolveSessionFile(idOrFile);
98
+ if (!existsSync(file))
99
+ return [];
100
+ return readFileSync(file, "utf8")
101
+ .split(NL)
102
+ .filter(Boolean)
103
+ .map((line) => {
104
+ try {
105
+ return JSON.parse(line);
106
+ }
107
+ catch {
108
+ return undefined;
109
+ }
110
+ })
111
+ .filter(Boolean);
112
+ }
113
+ export function loadSessionMessages(idOrFile) {
114
+ return readSession(idOrFile)
115
+ .filter((entry) => entry.type === "message" && typeof entry.content === "string")
116
+ .map((entry) => ({ role: entry.role, content: entry.content }));
117
+ }
118
+ export function latestSessionFile() {
119
+ const all = listSessions();
120
+ return all.length > 0 ? all[0].file : undefined;
121
+ }
122
+ export function pruneSessions(limit = sessionCacheLimitBytes()) {
123
+ const all = listSessions();
124
+ let total = all.reduce((sum, entry) => sum + entry.bytes, 0);
125
+ for (let i = all.length - 1; i >= 0 && total > limit; i -= 1) {
126
+ try {
127
+ rmSync(all[i].file, { force: true });
128
+ total -= all[i].bytes;
129
+ }
130
+ catch {
131
+ // keep pruning the rest
132
+ }
133
+ }
134
+ return total;
135
+ }
@@ -0,0 +1,18 @@
1
+ /** List skills currently installed under ~/.fastcode/skills. */
2
+ export declare function listInstalledSkills(): any[];
3
+ /** Look up a skill by name (matches folder name OR frontmatter name). */
4
+ export declare function getSkill(name: any): any;
5
+ /** Install a skill. Returns the parsed manifest on success. */
6
+ export declare function installSkill(spec: any, opts?: any): Promise<{
7
+ name: any;
8
+ description: any;
9
+ source: any;
10
+ path: string;
11
+ body: any;
12
+ }>;
13
+ /** Remove an installed skill by name or folder. */
14
+ export declare function uninstallSkill(name: any): boolean;
15
+ /** Search the registry for skills matching a query. */
16
+ export declare function searchRegistry(query: any, registry?: string): Promise<any>;
17
+ /** Compose the system-prompt addendum for a list of enabled skill names. */
18
+ export declare function buildSkillsPrompt(enabled: any): string;
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Skill manager.
3
+ *
4
+ * Skills follow Anthropic's "Agent Skills" convention: a folder containing
5
+ * a SKILL.md file plus optional resource files. FastCode supports three
6
+ * install sources:
7
+ *
8
+ * 1. skills.sh / skillshub.wtf API (default registry)
9
+ * `/skills install pdf` → resolves via the registry
10
+ * `/skills install owner/repo/skill` → fetches raw SKILL.md
11
+ * 2. Direct https URLs ending in SKILL.md.
12
+ * 3. Local paths (just symlinks into ~/.fastcode/skills/).
13
+ *
14
+ * Installed skills can be enabled per-session and are then stitched into
15
+ * the system prompt at chat time, exactly like Claude does internally.
16
+ */
17
+ import { readFileSync, writeFileSync, existsSync, readdirSync, rmSync, mkdirSync, } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { getPaths } from "../config/paths.js";
20
+ import { fetchJson } from "../lib/http.js";
21
+ import { parseSkill } from "./parser.js";
22
+ const DEFAULT_REGISTRY = "https://skillshub.wtf";
23
+ /** List skills currently installed under ~/.fastcode/skills. */
24
+ export function listInstalledSkills() {
25
+ const { skills } = getPaths();
26
+ if (!existsSync(skills))
27
+ return [];
28
+ const out = [];
29
+ for (const dir of readdirSync(skills, { withFileTypes: true })) {
30
+ if (!dir.isDirectory())
31
+ continue;
32
+ const folder = join(skills, dir.name);
33
+ const md = join(folder, "SKILL.md");
34
+ if (!existsSync(md))
35
+ continue;
36
+ const raw = readFileSync(md, "utf8");
37
+ const parsed = parseSkill(raw, dir.name);
38
+ const sourcePath = join(folder, ".fastcode-source");
39
+ const source = existsSync(sourcePath)
40
+ ? readFileSync(sourcePath, "utf8").trim()
41
+ : "local";
42
+ out.push({
43
+ name: parsed.name || dir.name,
44
+ description: parsed.description,
45
+ source,
46
+ path: folder,
47
+ body: parsed.body,
48
+ });
49
+ }
50
+ return out.sort((a, b) => a.name.localeCompare(b.name));
51
+ }
52
+ /** Look up a skill by name (matches folder name OR frontmatter name). */
53
+ export function getSkill(name) {
54
+ return listInstalledSkills().find((s) => s.name === name || basename(s.path) === name);
55
+ }
56
+ /** Install a skill. Returns the parsed manifest on success. */
57
+ export async function installSkill(spec, opts = {}) {
58
+ const { skills } = getPaths();
59
+ const registry = (opts.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
60
+ let parsed;
61
+ let source;
62
+ let folderName;
63
+ if (spec.startsWith("http://") || spec.startsWith("https://")) {
64
+ const body = await fetchText(spec);
65
+ parsed = parseSkill(body, slugify(spec));
66
+ source = spec;
67
+ folderName = slugify(parsed.name || slugify(spec));
68
+ }
69
+ else if (looksLikeRepoPath(spec)) {
70
+ // owner/repo or owner/repo/skill — fetch via registry's raw endpoint.
71
+ const url = `${registry}/${spec}?format=md`;
72
+ const body = await fetchText(url);
73
+ parsed = parseSkill(body, parts(spec).pop() ?? spec);
74
+ source = url;
75
+ folderName = slugify(parsed.name || parts(spec).pop() || spec);
76
+ }
77
+ else {
78
+ // Single-token name → search the registry.
79
+ const hits = await searchRegistry(spec, registry);
80
+ const hit = hits[0];
81
+ if (!hit)
82
+ throw new Error(`No skill found for "${spec}" in ${registry}`);
83
+ const url = hit.fetchUrl ?? `${registry}/${hit.slug}?format=md`;
84
+ const body = await fetchText(url);
85
+ parsed = parseSkill(body, hit.name);
86
+ source = url;
87
+ folderName = slugify(parsed.name || hit.name);
88
+ }
89
+ const targetDir = join(skills, folderName);
90
+ if (existsSync(targetDir) && !opts.force) {
91
+ throw new Error(`Skill "${folderName}" is already installed. Use --force to overwrite.`);
92
+ }
93
+ if (existsSync(targetDir))
94
+ rmSync(targetDir, { recursive: true, force: true });
95
+ mkdirSync(targetDir, { recursive: true });
96
+ writeFileSync(join(targetDir, "SKILL.md"), reassemble(parsed));
97
+ writeFileSync(join(targetDir, ".fastcode-source"), source);
98
+ return {
99
+ name: parsed.name || folderName,
100
+ description: parsed.description,
101
+ source,
102
+ path: targetDir,
103
+ body: parsed.body,
104
+ };
105
+ }
106
+ /** Remove an installed skill by name or folder. */
107
+ export function uninstallSkill(name) {
108
+ const { skills } = getPaths();
109
+ const candidates = readdirSync(skills, { withFileTypes: true });
110
+ for (const dir of candidates) {
111
+ if (!dir.isDirectory())
112
+ continue;
113
+ if (dir.name !== name) {
114
+ // Also match by frontmatter name.
115
+ const md = join(skills, dir.name, "SKILL.md");
116
+ if (!existsSync(md))
117
+ continue;
118
+ const parsed = parseSkill(readFileSync(md, "utf8"), dir.name);
119
+ if (parsed.name !== name)
120
+ continue;
121
+ }
122
+ rmSync(join(skills, dir.name), { recursive: true, force: true });
123
+ return true;
124
+ }
125
+ return false;
126
+ }
127
+ /** Search the registry for skills matching a query. */
128
+ export async function searchRegistry(query, registry = DEFAULT_REGISTRY) {
129
+ const base = registry.replace(/\/+$/, "");
130
+ try {
131
+ const json = await fetchJson(`${base}/api/v1/skills/search?q=${encodeURIComponent(query)}`);
132
+ return json.data ?? [];
133
+ }
134
+ catch {
135
+ return [];
136
+ }
137
+ }
138
+ /** Compose the system-prompt addendum for a list of enabled skill names. */
139
+ export function buildSkillsPrompt(enabled) {
140
+ if (enabled.length === 0)
141
+ return "";
142
+ const installed = listInstalledSkills();
143
+ const chunks = [];
144
+ for (const name of enabled) {
145
+ const skill = installed.find((s) => s.name === name || basename(s.path) === name);
146
+ if (!skill)
147
+ continue;
148
+ chunks.push(`<skill name="${skill.name}">\n${skill.body.trim()}\n</skill>`);
149
+ }
150
+ if (chunks.length === 0)
151
+ return "";
152
+ return [
153
+ "",
154
+ "## Active skills",
155
+ "The following skills are loaded for this session. Follow their instructions when relevant:",
156
+ "",
157
+ chunks.join("\n\n"),
158
+ ].join("\n");
159
+ }
160
+ // ---------- helpers ----------
161
+ async function fetchText(url) {
162
+ const res = await fetch(url);
163
+ if (!res.ok)
164
+ throw new Error(`Fetch ${url} failed: ${res.status} ${res.statusText}`);
165
+ return await res.text();
166
+ }
167
+ function reassemble(parsed) {
168
+ const fmEntries = Object.entries(parsed.frontmatter);
169
+ if (fmEntries.length === 0 && parsed.name && parsed.description) {
170
+ return [
171
+ "---",
172
+ `name: ${parsed.name}`,
173
+ `description: ${parsed.description}`,
174
+ "---",
175
+ "",
176
+ parsed.body.trim(),
177
+ "",
178
+ ].join("\n");
179
+ }
180
+ if (fmEntries.length === 0)
181
+ return parsed.body;
182
+ const fmLines = fmEntries.map(([k, v]) => `${k}: ${v}`);
183
+ return ["---", ...fmLines, "---", "", parsed.body.trim(), ""].join("\n");
184
+ }
185
+ function slugify(s) {
186
+ return s
187
+ .toLowerCase()
188
+ .replace(/[^a-z0-9]+/g, "-")
189
+ .replace(/^-+|-+$/g, "")
190
+ .slice(0, 64) || "skill";
191
+ }
192
+ function looksLikeRepoPath(spec) {
193
+ return /^[\w-]+\/[\w.-]+(\/[\w.-]+)?$/.test(spec) && !spec.startsWith("/");
194
+ }
195
+ function parts(spec) {
196
+ return spec.split("/").filter(Boolean);
197
+ }
198
+ function basename(p) {
199
+ const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
200
+ return i === -1 ? p : p.slice(i + 1);
201
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * SKILL.md parser. Skills follow Anthropic's Agent Skills convention:
3
+ *
4
+ * ---
5
+ * name: my-skill
6
+ * description: ...
7
+ * ---
8
+ * <markdown body>
9
+ *
10
+ * We accept both `---` and `+++` fences and tolerate missing frontmatter
11
+ * (treating the first H1 + paragraph as a fallback name/description).
12
+ */
13
+ export declare function parseSkill(source: any, fallbackName: any): {
14
+ name: any;
15
+ description: any;
16
+ body: any;
17
+ frontmatter: {};
18
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * SKILL.md parser. Skills follow Anthropic's Agent Skills convention:
3
+ *
4
+ * ---
5
+ * name: my-skill
6
+ * description: ...
7
+ * ---
8
+ * <markdown body>
9
+ *
10
+ * We accept both `---` and `+++` fences and tolerate missing frontmatter
11
+ * (treating the first H1 + paragraph as a fallback name/description).
12
+ */
13
+ export function parseSkill(source, fallbackName) {
14
+ const fmMatch = source.match(/^(?:---|\+\+\+)\s*\n([\s\S]*?)\n(?:---|\+\+\+)\s*\n?/);
15
+ let frontmatter = {};
16
+ let body = source;
17
+ if (fmMatch) {
18
+ frontmatter = parseFrontmatter(fmMatch[1] ?? "");
19
+ body = source.slice(fmMatch[0].length).trimStart();
20
+ }
21
+ const name = (frontmatter["name"] ?? fallbackName).trim();
22
+ const description = (frontmatter["description"] ??
23
+ extractFirstParagraph(body) ??
24
+ "").trim();
25
+ return { name, description, body, frontmatter };
26
+ }
27
+ function parseFrontmatter(yaml) {
28
+ // Tiny YAML-ish parser: only handles `key: value` lines, which is what
29
+ // SKILL.md frontmatter uses in practice. This avoids pulling in a YAML lib.
30
+ const out = {};
31
+ for (const lineRaw of yaml.split("\n")) {
32
+ const line = lineRaw.trim();
33
+ if (!line || line.startsWith("#"))
34
+ continue;
35
+ const colon = line.indexOf(":");
36
+ if (colon === -1)
37
+ continue;
38
+ const key = line.slice(0, colon).trim();
39
+ let value = line.slice(colon + 1).trim();
40
+ if ((value.startsWith("\"") && value.endsWith("\"")) ||
41
+ (value.startsWith("'") && value.endsWith("'"))) {
42
+ value = value.slice(1, -1);
43
+ }
44
+ out[key] = value;
45
+ }
46
+ return out;
47
+ }
48
+ function extractFirstParagraph(body) {
49
+ const lines = body.split("\n");
50
+ let buf = [];
51
+ for (const line of lines) {
52
+ if (line.startsWith("#")) {
53
+ if (buf.length)
54
+ break;
55
+ continue;
56
+ }
57
+ if (!line.trim()) {
58
+ if (buf.length)
59
+ break;
60
+ continue;
61
+ }
62
+ buf.push(line.trim());
63
+ }
64
+ return buf.length ? buf.join(" ") : undefined;
65
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Shared types used across FastCode.
3
+ *
4
+ * The provider abstraction is intentionally narrow — it only exposes
5
+ * what the chat loop needs (streaming completions, model list, auth).
6
+ * Providers can layer richer behavior on top in their own modules.
7
+ */
8
+ export type Role = "system" | "user" | "assistant";
9
+ export interface ChatMessage {
10
+ role: Role;
11
+ content: string;
12
+ }
13
+ export interface ChatRequest {
14
+ model: string;
15
+ messages: ChatMessage[];
16
+ /** Optional system prompt. Some providers fold this into messages. */
17
+ system?: string;
18
+ /** Sampling temperature. */
19
+ temperature?: number;
20
+ /** Soft cap for output tokens. */
21
+ maxOutputTokens?: number;
22
+ /** AbortSignal for cancellation (Esc / Ctrl+C). */
23
+ signal?: AbortSignal;
24
+ }
25
+ export interface ChatChunk {
26
+ /** Token text appended to the response. */
27
+ delta: string;
28
+ /** True on the final chunk. */
29
+ done: boolean;
30
+ /** Provider-specific usage info on the final chunk. */
31
+ usage?: {
32
+ inputTokens?: number;
33
+ outputTokens?: number;
34
+ totalTokens?: number;
35
+ /** Approximate cost in USD if known. */
36
+ costUsd?: number;
37
+ };
38
+ }
39
+ export type ChatStream = AsyncIterable<ChatChunk>;
40
+ export interface ProviderModel {
41
+ id: string;
42
+ label?: string;
43
+ /** Optional context window in tokens. */
44
+ contextWindow?: number;
45
+ /** Pricing per 1M tokens, used for cost estimation. */
46
+ pricing?: {
47
+ inputUsdPerM?: number;
48
+ outputUsdPerM?: number;
49
+ };
50
+ }
51
+ export interface Provider {
52
+ /** Unique id, e.g. "anthropic". */
53
+ id: string;
54
+ /** Human label. */
55
+ label: string;
56
+ /** ENV var(s) that, when set, satisfy auth. */
57
+ authEnv: string[];
58
+ /** Base URL — many providers expose OpenAI-compatible endpoints. */
59
+ baseUrl: string;
60
+ /** Default model id when the user hasn't picked one. */
61
+ defaultModel: string;
62
+ /** Static or dynamic list of supported models. */
63
+ listModels(apiKey: string | undefined): Promise<ProviderModel[]>;
64
+ /** Stream a chat completion. */
65
+ stream(req: ChatRequest, apiKey: string | undefined): ChatStream;
66
+ }
67
+ export interface SlashCommand {
68
+ name: string;
69
+ description: string;
70
+ /** Optional aliases users can also type. */
71
+ aliases?: string[];
72
+ /** Hide from /help (useful for power-user commands). */
73
+ hidden?: boolean;
74
+ /** Execute the command. Return text to render, or undefined for no output. */
75
+ run(ctx: CommandContext): Promise<CommandResult> | CommandResult;
76
+ }
77
+ export interface CommandResult {
78
+ /** Text rendered as a system message in the transcript. */
79
+ message?: string;
80
+ /** Mark this turn as silent (no echo of the slash itself). */
81
+ silent?: boolean;
82
+ /** Quit the app after this command. */
83
+ quit?: boolean;
84
+ }
85
+ export interface CommandContext {
86
+ args: string[];
87
+ raw: string;
88
+ state: AppStateSnapshot;
89
+ actions: AppActions;
90
+ }
91
+ export interface AppStateSnapshot {
92
+ providerId: string;
93
+ model: string;
94
+ theme: string;
95
+ agentMode: boolean;
96
+ history: ChatMessage[];
97
+ systemPrompt: string;
98
+ totalCostUsd: number;
99
+ totalInputTokens: number;
100
+ totalOutputTokens: number;
101
+ enabledSkills: string[];
102
+ enabledPlugins: string[];
103
+ skillsRegistry: string;
104
+ }
105
+ export interface AppActions {
106
+ setProvider(id: string): void;
107
+ setModel(id: string): void;
108
+ setTheme(id: string): void;
109
+ setAgentMode(enabled: boolean): void;
110
+ setSystemPrompt(prompt: string): void;
111
+ setSkillsRegistry(url: string): void;
112
+ enableSkill(name: string): void;
113
+ disableSkill(name: string): void;
114
+ enablePlugin(name: string): void;
115
+ disablePlugin(name: string): void;
116
+ clearHistory(): void;
117
+ pushSystemMessage(text: string): void;
118
+ /** Begin the guided API-key flow (choose provider → model → key). */
119
+ startLogin(options: {
120
+ provider?: string;
121
+ model?: string;
122
+ }): void;
123
+ }
124
+ export interface SkillManifest {
125
+ name: string;
126
+ description: string;
127
+ /** Where the skill came from (e.g. "skills.sh", "anthropics/skills"). */
128
+ source: string;
129
+ /** Local on-disk path to the skill folder. */
130
+ path: string;
131
+ /** Raw SKILL.md body (the prompt content). */
132
+ body: string;
133
+ }
134
+ export interface Theme {
135
+ id: string;
136
+ label: string;
137
+ banner: [string, string, string];
138
+ user: string;
139
+ assistant: string;
140
+ system: string;
141
+ border: string;
142
+ accent: string;
143
+ muted: string;
144
+ error: string;
145
+ success: string;
146
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export declare const Banner: ({ theme, subtitle, stable }: {
2
+ theme: any;
3
+ subtitle: any;
4
+ stable?: boolean;
5
+ }) => import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Animated FastCode banner shown at startup.
4
+ *
5
+ * We render the wordmark with ink-big-text + a gradient that slowly cycles
6
+ * through the theme's banner triple. The animation runs for ~1s and then
7
+ * settles on a stable gradient so we don't waste GPU cycles forever.
8
+ *
9
+ * The font is chosen to fit the terminal: `block` needs ~74 columns, so on a
10
+ * narrow phone terminal we fall back to progressively smaller fonts (see
11
+ * BANNER_FONTS). If even the smallest font is wider than the terminal, the
12
+ * banner is skipped entirely.
13
+ */
14
+ import { useEffect, useState } from "react";
15
+ import { Box, Text, useStdout } from "ink";
16
+ import BigText from "ink-big-text";
17
+ import Gradient from "ink-gradient";
18
+ const FRAMES = 16;
19
+ // (font, columns) needed to render the literal text "FastCode" with
20
+ // letterSpacing 1 and space:false. Measured with cfonts; largest first.
21
+ const BANNER_FONTS = [
22
+ { font: "block", columns: 74 },
23
+ { font: "simple", columns: 56 },
24
+ { font: "slick", columns: 49 },
25
+ { font: "shade", columns: 39 },
26
+ { font: "chrome", columns: 32 },
27
+ { font: "console", columns: 16 },
28
+ ];
29
+ /** Pick the largest font that fits `columns` (2 columns reserved for padding). */
30
+ function pickBannerFont(columns) {
31
+ const usable = columns - 2;
32
+ for (const entry of BANNER_FONTS) {
33
+ if (entry.columns <= usable)
34
+ return entry.font;
35
+ }
36
+ return null;
37
+ }
38
+ export const Banner = ({ theme, subtitle, stable = false }) => {
39
+ const [frame, setFrame] = useState(0);
40
+ const { stdout } = useStdout();
41
+ const columns = stdout?.columns ?? process.stdout.columns ?? 80;
42
+ const font = pickBannerFont(columns);
43
+ useEffect(() => {
44
+ if (stable)
45
+ return;
46
+ if (frame >= FRAMES)
47
+ return;
48
+ const t = setTimeout(() => setFrame((f) => f + 1), 60);
49
+ return () => clearTimeout(t);
50
+ }, [frame, stable]);
51
+ if (!font)
52
+ return null;
53
+ const t = stable ? 1 : Math.min(1, frame / FRAMES);
54
+ const colors = rotate(theme.banner, t);
55
+ return (_jsxs(Box, { flexDirection: "column", alignItems: "flex-start", marginBottom: 1, children: [_jsx(Gradient, { colors: colors, children: _jsx(BigText, { text: "FastCode", font: font, space: false }) }), _jsx(Text, { color: theme.muted, children: subtitle ?? "multi-LLM TUI · skills.sh · slash commands · /help to start" })] }));
56
+ };
57
+ /** Cycle the gradient stops by `t ∈ [0,1]` for a subtle shimmer effect. */
58
+ function rotate(stops, t) {
59
+ const [a, b, c] = stops;
60
+ if (t < 0.33)
61
+ return [a, b, c];
62
+ if (t < 0.66)
63
+ return [b, c, a];
64
+ return [c, a, b];
65
+ }
@@ -0,0 +1,7 @@
1
+ export declare const MessageBubble: ({ theme, role, content, streaming, markdown, }: {
2
+ theme: any;
3
+ role: any;
4
+ content: any;
5
+ streaming: any;
6
+ markdown: any;
7
+ }) => import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { renderMarkdown } from "../lib/markdown.js";
4
+ const SIGIL = {
5
+ user: "▎ you",
6
+ assistant: "▍ FastCode",
7
+ system: "✦ system",
8
+ };
9
+ export const MessageBubble = ({ theme, role, content, streaming, markdown, }) => {
10
+ const color = role === "user" ? theme.user : role === "assistant" ? theme.assistant : theme.system;
11
+ const display = markdown ? renderMarkdown(content) : content;
12
+ return (_jsxs(Box, { flexDirection: "column", marginY: 0, children: [_jsxs(Text, { color: color, bold: true, children: [SIGIL[role], streaming ? _jsx(Text, { color: theme.muted, children: " (streaming)" }) : null] }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: _jsx(Text, { color: color, children: display || (streaming ? "…" : "") }) })] }));
13
+ };