@datename/fastcode-termux 0.1.6 → 0.1.8-beta.0

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 (303) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +67 -16
  3. package/dist/agent/approval-json.d.ts +43 -0
  4. package/dist/agent/approval-json.js +69 -0
  5. package/dist/agent/approval-native.d.ts +3 -0
  6. package/dist/agent/approval-native.js +165 -0
  7. package/dist/agent/approval-store.d.ts +16 -0
  8. package/dist/agent/approval-store.js +21 -0
  9. package/dist/agent/approval.d.ts +83 -0
  10. package/dist/agent/approval.js +42 -0
  11. package/dist/agent/compaction.d.ts +41 -0
  12. package/dist/agent/compaction.js +270 -0
  13. package/dist/agent/context.d.ts +29 -0
  14. package/dist/agent/context.js +62 -0
  15. package/dist/agent/guards.d.ts +24 -16
  16. package/dist/agent/guards.js +1167 -18
  17. package/dist/agent/ignore.d.ts +11 -0
  18. package/dist/agent/ignore.js +129 -0
  19. package/dist/agent/policy/defaults.d.ts +16 -0
  20. package/dist/agent/policy/defaults.js +17 -0
  21. package/dist/agent/policy/egress.d.ts +32 -0
  22. package/dist/agent/policy/egress.js +280 -0
  23. package/dist/agent/process-tree.d.ts +22 -0
  24. package/dist/agent/process-tree.js +102 -0
  25. package/dist/agent/protocol-mode.d.ts +6 -0
  26. package/dist/agent/protocol-mode.js +51 -0
  27. package/dist/agent/protocol.d.ts +20 -1
  28. package/dist/agent/protocol.js +169 -39
  29. package/dist/agent/registry.d.ts +19 -0
  30. package/dist/agent/registry.js +247 -0
  31. package/dist/agent/runner.d.ts +127 -0
  32. package/dist/agent/runner.js +391 -0
  33. package/dist/agent/shell-ast.d.ts +11 -0
  34. package/dist/agent/shell-ast.js +204 -0
  35. package/dist/agent/shell-session.d.ts +51 -0
  36. package/dist/agent/shell-session.js +611 -0
  37. package/dist/agent/stream.d.ts +29 -0
  38. package/dist/agent/stream.js +103 -0
  39. package/dist/agent/tool-schemas.d.ts +2 -0
  40. package/dist/agent/tool-schemas.js +82 -0
  41. package/dist/agent/tools/append_file.d.ts +2 -0
  42. package/dist/agent/tools/append_file.js +28 -0
  43. package/dist/agent/tools/apply_patch.d.ts +10 -0
  44. package/dist/agent/tools/apply_patch.js +92 -0
  45. package/dist/agent/tools/copy_file.d.ts +2 -0
  46. package/dist/agent/tools/copy_file.js +53 -0
  47. package/dist/agent/tools/delete_file.d.ts +2 -0
  48. package/dist/agent/tools/delete_file.js +43 -0
  49. package/dist/agent/tools/grep.d.ts +2 -0
  50. package/dist/agent/tools/grep.js +54 -0
  51. package/dist/agent/tools/index.d.ts +2 -0
  52. package/dist/agent/tools/index.js +32 -0
  53. package/dist/agent/tools/list_files.d.ts +2 -0
  54. package/dist/agent/tools/list_files.js +17 -0
  55. package/dist/agent/tools/make_dir.d.ts +2 -0
  56. package/dist/agent/tools/make_dir.js +26 -0
  57. package/dist/agent/tools/move_path.d.ts +9 -0
  58. package/dist/agent/tools/move_path.js +73 -0
  59. package/dist/agent/tools/paths.d.ts +7 -0
  60. package/dist/agent/tools/paths.js +84 -0
  61. package/dist/agent/tools/pwd.d.ts +2 -0
  62. package/dist/agent/tools/pwd.js +10 -0
  63. package/dist/agent/tools/read_file.d.ts +2 -0
  64. package/dist/agent/tools/read_file.js +81 -0
  65. package/dist/agent/tools/read_many.d.ts +2 -0
  66. package/dist/agent/tools/read_many.js +43 -0
  67. package/dist/agent/tools/readers.d.ts +58 -0
  68. package/dist/agent/tools/readers.js +426 -0
  69. package/dist/agent/tools/replace_in_file.d.ts +2 -0
  70. package/dist/agent/tools/replace_in_file.js +63 -0
  71. package/dist/agent/tools/shell.d.ts +2 -0
  72. package/dist/agent/tools/shell.js +48 -0
  73. package/dist/agent/tools/stat_path.d.ts +2 -0
  74. package/dist/agent/tools/stat_path.js +31 -0
  75. package/dist/agent/tools/types.d.ts +65 -0
  76. package/dist/agent/tools/types.js +1 -0
  77. package/dist/agent/tools/validate.d.ts +19 -0
  78. package/dist/agent/tools/validate.js +92 -0
  79. package/dist/agent/tools/write_file.d.ts +2 -0
  80. package/dist/agent/tools/write_file.js +28 -0
  81. package/dist/agent/tools.d.ts +3 -1
  82. package/dist/agent/tools.js +2 -377
  83. package/dist/app.d.ts +2 -1
  84. package/dist/app.js +846 -173
  85. package/dist/cli-main.js +363 -0
  86. package/dist/cli.js +444 -333
  87. package/dist/commands/about.js +1 -1
  88. package/dist/commands/attach.d.ts +9 -0
  89. package/dist/commands/attach.js +15 -0
  90. package/dist/commands/bg.d.ts +7 -0
  91. package/dist/commands/bg.js +9 -0
  92. package/dist/commands/cancel.d.ts +10 -0
  93. package/dist/commands/cancel.js +10 -0
  94. package/dist/commands/clear.d.ts +1 -0
  95. package/dist/commands/clear.js +1 -1
  96. package/dist/commands/compact.d.ts +22 -0
  97. package/dist/commands/compact.js +34 -0
  98. package/dist/commands/context.d.ts +7 -0
  99. package/dist/commands/context.js +25 -0
  100. package/dist/commands/edit.d.ts +10 -0
  101. package/dist/commands/edit.js +36 -0
  102. package/dist/commands/effort.d.ts +25 -0
  103. package/dist/commands/effort.js +48 -0
  104. package/dist/commands/expand.d.ts +10 -0
  105. package/dist/commands/expand.js +8 -0
  106. package/dist/commands/index.js +24 -0
  107. package/dist/commands/jobs.d.ts +7 -0
  108. package/dist/commands/jobs.js +17 -0
  109. package/dist/commands/kill.d.ts +7 -0
  110. package/dist/commands/kill.js +13 -0
  111. package/dist/commands/login.d.ts +6 -4
  112. package/dist/commands/login.js +24 -22
  113. package/dist/commands/logout.js +8 -16
  114. package/dist/commands/model.d.ts +1 -0
  115. package/dist/commands/model.js +10 -22
  116. package/dist/commands/provider.d.ts +1 -2
  117. package/dist/commands/provider.js +4 -11
  118. package/dist/commands/skills.js +34 -4
  119. package/dist/commands/steer.d.ts +11 -0
  120. package/dist/commands/steer.js +13 -0
  121. package/dist/commands/threads.d.ts +9 -0
  122. package/dist/commands/threads.js +26 -0
  123. package/dist/commands/whoami.js +6 -5
  124. package/dist/config/store.d.ts +53 -12
  125. package/dist/config/store.js +163 -73
  126. package/dist/config/trust.d.ts +30 -8
  127. package/dist/config/trust.js +139 -14
  128. package/dist/headless.d.ts +7 -0
  129. package/dist/headless.js +213 -0
  130. package/dist/index.js +19 -3
  131. package/dist/lib/debug.d.ts +2 -0
  132. package/dist/lib/debug.js +22 -0
  133. package/dist/lib/diagnostics.d.ts +48 -4
  134. package/dist/lib/diagnostics.js +90 -8
  135. package/dist/lib/highlight-subset.d.ts +21 -0
  136. package/dist/lib/highlight-subset.js +88 -0
  137. package/dist/lib/http.d.ts +11 -1
  138. package/dist/lib/http.js +62 -4
  139. package/dist/lib/lifecycle.d.ts +5 -0
  140. package/dist/lib/lifecycle.js +62 -0
  141. package/dist/lib/markdown.js +22 -4
  142. package/dist/lib/model-meta.d.ts +19 -0
  143. package/dist/lib/model-meta.js +113 -0
  144. package/dist/lib/native-tools.d.ts +12 -0
  145. package/dist/lib/native-tools.js +21 -0
  146. package/dist/lib/net.d.ts +33 -0
  147. package/dist/lib/net.js +168 -0
  148. package/dist/patch/apply.d.ts +15 -0
  149. package/dist/patch/apply.js +172 -0
  150. package/dist/patch/parse.d.ts +29 -0
  151. package/dist/patch/parse.js +112 -0
  152. package/dist/patch/plan.d.ts +49 -0
  153. package/dist/patch/plan.js +273 -0
  154. package/dist/providers/anthropic.d.ts +1 -0
  155. package/dist/providers/anthropic.js +12 -0
  156. package/dist/providers/custom.d.ts +9 -0
  157. package/dist/providers/deepseek.d.ts +10 -0
  158. package/dist/providers/deepseek.js +3 -2
  159. package/dist/providers/google.d.ts +9 -0
  160. package/dist/providers/groq.d.ts +10 -0
  161. package/dist/providers/groq.js +2 -1
  162. package/dist/providers/index.d.ts +10 -201
  163. package/dist/providers/index.js +30 -0
  164. package/dist/providers/mistral.d.ts +10 -0
  165. package/dist/providers/mistral.js +2 -1
  166. package/dist/providers/moonshot.d.ts +10 -0
  167. package/dist/providers/moonshot.js +2 -1
  168. package/dist/providers/ollama.d.ts +9 -0
  169. package/dist/providers/openai.d.ts +10 -0
  170. package/dist/providers/openai.js +2 -1
  171. package/dist/providers/openaiCompatible.d.ts +33 -2
  172. package/dist/providers/openaiCompatible.js +78 -16
  173. package/dist/providers/openrouter.d.ts +10 -0
  174. package/dist/providers/openrouter.js +2 -1
  175. package/dist/providers/qwen.d.ts +10 -0
  176. package/dist/providers/qwen.js +2 -1
  177. package/dist/providers/spec.d.ts +12 -0
  178. package/dist/providers/spec.js +29 -0
  179. package/dist/providers/xai.d.ts +10 -0
  180. package/dist/providers/xai.js +2 -1
  181. package/dist/providers/zhipu.d.ts +10 -0
  182. package/dist/providers/zhipu.js +2 -1
  183. package/dist/session/log.d.ts +49 -14
  184. package/dist/session/log.js +236 -11
  185. package/dist/session/projection.d.ts +76 -0
  186. package/dist/session/projection.js +526 -0
  187. package/dist/skills/manager.d.ts +15 -0
  188. package/dist/skills/manager.js +135 -18
  189. package/dist/types.d.ts +75 -1
  190. package/dist/ui/Message.d.ts +1 -7
  191. package/dist/ui/Message.js +13 -2
  192. package/dist/ui/Picker.d.ts +8 -0
  193. package/dist/ui/Picker.js +24 -0
  194. package/dist/ui/Prompt.d.ts +1 -2
  195. package/dist/ui/Prompt.js +2 -2
  196. package/dist/ui/Spinner.js +11 -5
  197. package/dist/ui/StatusBar.d.ts +2 -1
  198. package/dist/ui/StatusBar.js +6 -5
  199. package/package.json +3 -2
  200. package/src/agent/approval-json.ts +103 -0
  201. package/src/agent/approval-native.ts +168 -0
  202. package/src/agent/approval-store.ts +38 -0
  203. package/src/agent/approval.ts +94 -0
  204. package/src/agent/compaction.ts +321 -0
  205. package/src/agent/context.ts +97 -0
  206. package/src/agent/guards.ts +1239 -19
  207. package/src/agent/ignore.ts +147 -0
  208. package/src/agent/policy/defaults.ts +34 -0
  209. package/src/agent/policy/egress.ts +335 -0
  210. package/src/agent/process-tree.ts +114 -0
  211. package/src/agent/protocol-mode.ts +59 -0
  212. package/src/agent/protocol.ts +191 -40
  213. package/src/agent/registry.ts +270 -0
  214. package/src/agent/runner.ts +486 -0
  215. package/src/agent/shell-ast.ts +227 -0
  216. package/src/agent/shell-session.ts +673 -0
  217. package/src/agent/stream.ts +143 -0
  218. package/src/agent/tool-schemas.ts +86 -0
  219. package/src/agent/tools/append_file.ts +30 -0
  220. package/src/agent/tools/apply_patch.ts +95 -0
  221. package/src/agent/tools/copy_file.ts +56 -0
  222. package/src/agent/tools/delete_file.ts +45 -0
  223. package/src/agent/tools/grep.ts +57 -0
  224. package/src/agent/tools/index.ts +34 -0
  225. package/src/agent/tools/list_files.ts +19 -0
  226. package/src/agent/tools/make_dir.ts +28 -0
  227. package/src/agent/tools/move_path.ts +83 -0
  228. package/src/agent/tools/paths.ts +92 -0
  229. package/src/agent/tools/pwd.ts +12 -0
  230. package/src/agent/tools/read_file.ts +84 -0
  231. package/src/agent/tools/read_many.ts +45 -0
  232. package/src/agent/tools/readers.ts +491 -0
  233. package/src/agent/tools/replace_in_file.ts +66 -0
  234. package/src/agent/tools/shell.ts +53 -0
  235. package/src/agent/tools/stat_path.ts +33 -0
  236. package/src/agent/tools/types.ts +84 -0
  237. package/src/agent/tools/validate.ts +104 -0
  238. package/src/agent/tools/write_file.ts +30 -0
  239. package/src/agent/tools.ts +3 -377
  240. package/src/app.ts +853 -170
  241. package/src/{cli.ts → cli-main.ts} +64 -9
  242. package/src/commands/about.ts +1 -1
  243. package/src/commands/attach.ts +16 -0
  244. package/src/commands/bg.ts +9 -0
  245. package/src/commands/cancel.ts +10 -0
  246. package/src/commands/clear.ts +1 -1
  247. package/src/commands/compact.ts +35 -0
  248. package/src/commands/context.ts +26 -0
  249. package/src/commands/edit.ts +37 -0
  250. package/src/commands/effort.ts +49 -0
  251. package/src/commands/expand.ts +8 -0
  252. package/src/commands/index.ts +24 -0
  253. package/src/commands/jobs.ts +18 -0
  254. package/src/commands/kill.ts +14 -0
  255. package/src/commands/login.ts +24 -22
  256. package/src/commands/logout.ts +8 -16
  257. package/src/commands/model.ts +10 -22
  258. package/src/commands/provider.ts +4 -11
  259. package/src/commands/skills.ts +34 -4
  260. package/src/commands/steer.ts +13 -0
  261. package/src/commands/threads.ts +28 -0
  262. package/src/commands/whoami.ts +6 -5
  263. package/src/config/store.ts +168 -77
  264. package/src/config/trust.ts +181 -22
  265. package/src/headless.ts +225 -0
  266. package/src/index.ts +19 -3
  267. package/src/lib/debug.ts +24 -0
  268. package/src/lib/diagnostics.ts +90 -8
  269. package/src/lib/highlight-subset.ts +93 -0
  270. package/src/lib/http.ts +66 -4
  271. package/src/lib/lifecycle.ts +69 -0
  272. package/src/lib/markdown.ts +22 -4
  273. package/src/lib/model-meta.ts +138 -0
  274. package/src/lib/native-tools.ts +32 -0
  275. package/src/lib/net.ts +204 -0
  276. package/src/patch/apply.ts +194 -0
  277. package/src/patch/parse.ts +131 -0
  278. package/src/patch/plan.ts +317 -0
  279. package/src/providers/anthropic.ts +13 -1
  280. package/src/providers/deepseek.ts +3 -2
  281. package/src/providers/groq.ts +2 -1
  282. package/src/providers/index.ts +32 -1
  283. package/src/providers/mistral.ts +2 -1
  284. package/src/providers/moonshot.ts +2 -1
  285. package/src/providers/openai.ts +2 -1
  286. package/src/providers/openaiCompatible.ts +104 -19
  287. package/src/providers/openrouter.ts +2 -1
  288. package/src/providers/qwen.ts +2 -1
  289. package/src/providers/spec.ts +42 -0
  290. package/src/providers/xai.ts +2 -1
  291. package/src/providers/zhipu.ts +2 -1
  292. package/src/session/log.ts +311 -29
  293. package/src/session/projection.ts +621 -0
  294. package/src/skills/manager.ts +140 -18
  295. package/src/types/highlight-shims.d.ts +14 -0
  296. package/src/types.ts +79 -2
  297. package/src/ui/Message.ts +13 -2
  298. package/src/ui/Picker.ts +24 -0
  299. package/src/ui/Prompt.ts +2 -2
  300. package/src/ui/Spinner.ts +11 -5
  301. package/src/ui/StatusBar.ts +6 -5
  302. package/tsconfig.json +1 -0
  303. /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
@@ -5,21 +5,122 @@
5
5
  * a SKILL.md file plus optional resource files. FastCode supports three
6
6
  * install sources:
7
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.
8
+ * 1. A registry configured via FASTCODE_SKILLS_REGISTRY or skillsRegistry
9
+ * `/skills install pdf` → resolves via the configured registry
10
+ * `/skills install owner/repo/skill` → fetches raw SKILL.md from the registry
11
+ * 2. Direct https URLs ending in SKILL.md (no registry required).
12
12
  * 3. Local paths (just symlinks into ~/.fastcode/skills/).
13
13
  *
14
14
  * Installed skills can be enabled per-session and are then stitched into
15
15
  * the system prompt at chat time, exactly like Claude does internally.
16
16
  */
17
+ import { createHash } from "node:crypto";
17
18
  import { readFileSync, writeFileSync, existsSync, readdirSync, rmSync, mkdirSync, } from "node:fs";
18
19
  import { join } from "node:path";
19
20
  import { getPaths } from "../config/paths.js";
21
+ import { invalidateConfigCache } from "../config/store.js";
20
22
  import { fetchJson } from "../lib/http.js";
21
23
  import { parseSkill } from "./parser.js";
22
- const DEFAULT_REGISTRY = "https://skillshub.wtf";
24
+ const SOURCE_FILE = ".fastcode-source";
25
+ const INTEGRITY_VERSION = 1;
26
+ const REGISTRY_REQUIRED = "No skills registry configured. Set one with `/skills registry <url>` or FASTCODE_SKILLS_REGISTRY.";
27
+ /**
28
+ * Pick the skills registry without treating an empty environment variable as
29
+ * "configured". `??` would keep `""` and make env overrides silently break;
30
+ * explicit null/empty checks keep `""` and `" "` on the config path. There is
31
+ * intentionally no built-in default registry.
32
+ */
33
+ export function resolveRegistry(envValue, configValue) {
34
+ const env = typeof envValue === "string" && envValue.trim() ? envValue.trim() : "";
35
+ const configured = typeof configValue === "string" && configValue.trim() ? configValue.trim() : "";
36
+ return (env || configured).replace(/\/+$/, "");
37
+ }
38
+ function requireRegistry(registry) {
39
+ if (!registry)
40
+ throw new Error(REGISTRY_REQUIRED);
41
+ return registry;
42
+ }
43
+ export function hashSkillDirectory(folder) {
44
+ const entries = skillFiles(folder).sort();
45
+ return createHash("sha256").update(entries.join("\n")).digest("hex");
46
+ }
47
+ function skillFiles(folder, prefix = "") {
48
+ const out = [];
49
+ let items;
50
+ try {
51
+ items = readdirSync(folder, { withFileTypes: true });
52
+ }
53
+ catch {
54
+ return out;
55
+ }
56
+ for (const item of items) {
57
+ if (item.name === SOURCE_FILE)
58
+ continue;
59
+ const full = join(folder, item.name);
60
+ const rel = prefix ? prefix + "/" + item.name : item.name;
61
+ if (item.isDirectory()) {
62
+ out.push(...skillFiles(full, rel));
63
+ continue;
64
+ }
65
+ if (!item.isFile())
66
+ continue;
67
+ const digest = createHash("sha256").update(readFileSync(full)).digest("hex");
68
+ out.push(rel + "\0" + digest);
69
+ }
70
+ return out;
71
+ }
72
+ function readSourceMeta(folder) {
73
+ const sourcePath = join(folder, SOURCE_FILE);
74
+ if (!existsSync(sourcePath))
75
+ return { source: "local", legacy: true };
76
+ const raw = readFileSync(sourcePath, "utf8").trim();
77
+ if (raw.startsWith("{")) {
78
+ try {
79
+ const parsed = JSON.parse(raw);
80
+ return {
81
+ source: typeof parsed.source === "string" ? parsed.source : "local",
82
+ sha256: typeof parsed.sha256 === "string" ? parsed.sha256 : undefined,
83
+ legacy: typeof parsed.sha256 !== "string",
84
+ };
85
+ }
86
+ catch {
87
+ // Fall through and treat the file as a legacy plain-text source.
88
+ }
89
+ }
90
+ return { source: raw || "local", legacy: true };
91
+ }
92
+ function writeSourceMeta(folder, source) {
93
+ const sha256 = hashSkillDirectory(folder);
94
+ const payload = {
95
+ version: INTEGRITY_VERSION,
96
+ algorithm: "sha256",
97
+ source,
98
+ sha256,
99
+ };
100
+ writeFileSync(join(folder, SOURCE_FILE), JSON.stringify(payload, null, 2));
101
+ return sha256;
102
+ }
103
+ export function rebaselineSkills(names) {
104
+ const installed = listInstalledSkills();
105
+ const wanted = names && names.length > 0 ? new Set(names) : undefined;
106
+ const results = [];
107
+ let changed = false;
108
+ for (const skill of installed) {
109
+ if (wanted && !wanted.has(skill.name) && !wanted.has(basename(skill.path)))
110
+ continue;
111
+ try {
112
+ writeSourceMeta(skill.path, skill.source === "local" ? "local" : skill.source);
113
+ results.push({ name: skill.name, ok: true });
114
+ changed = true;
115
+ }
116
+ catch (err) {
117
+ results.push({ name: skill.name, ok: false, warning: err?.message ?? String(err) });
118
+ }
119
+ }
120
+ if (changed)
121
+ invalidateConfigCache();
122
+ return results;
123
+ }
23
124
  /** List skills currently installed under ~/.fastcode/skills. */
24
125
  export function listInstalledSkills() {
25
126
  const { skills } = getPaths();
@@ -35,16 +136,20 @@ export function listInstalledSkills() {
35
136
  continue;
36
137
  const raw = readFileSync(md, "utf8");
37
138
  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";
139
+ const meta = readSourceMeta(folder);
140
+ const actualHash = hashSkillDirectory(folder);
141
+ const integrity = !meta.sha256
142
+ ? "unverified"
143
+ : actualHash === meta.sha256 ? "ok" : "drift";
42
144
  out.push({
43
145
  name: parsed.name || dir.name,
44
146
  description: parsed.description,
45
- source,
147
+ source: meta.source,
46
148
  path: folder,
47
149
  body: parsed.body,
150
+ integrity,
151
+ sha256: actualHash,
152
+ lockedHash: meta.sha256,
48
153
  });
49
154
  }
50
155
  return out.sort((a, b) => a.name.localeCompare(b.name));
@@ -56,7 +161,7 @@ export function getSkill(name) {
56
161
  /** Install a skill. Returns the parsed manifest on success. */
57
162
  export async function installSkill(spec, opts = {}) {
58
163
  const { skills } = getPaths();
59
- const registry = (opts.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
164
+ const registry = resolveRegistry(process.env["FASTCODE_SKILLS_REGISTRY"], opts.registry);
60
165
  let parsed;
61
166
  let source;
62
167
  let folderName;
@@ -68,7 +173,8 @@ export async function installSkill(spec, opts = {}) {
68
173
  }
69
174
  else if (looksLikeRepoPath(spec)) {
70
175
  // owner/repo or owner/repo/skill — fetch via registry's raw endpoint.
71
- const url = `${registry}/${spec}?format=md`;
176
+ const base = requireRegistry(registry);
177
+ const url = `${base}/${spec}?format=md`;
72
178
  const body = await fetchText(url);
73
179
  parsed = parseSkill(body, parts(spec).pop() ?? spec);
74
180
  source = url;
@@ -76,11 +182,12 @@ export async function installSkill(spec, opts = {}) {
76
182
  }
77
183
  else {
78
184
  // Single-token name → search the registry.
79
- const hits = await searchRegistry(spec, registry);
185
+ const base = requireRegistry(registry);
186
+ const hits = await searchRegistry(spec, base);
80
187
  const hit = hits[0];
81
188
  if (!hit)
82
- throw new Error(`No skill found for "${spec}" in ${registry}`);
83
- const url = hit.fetchUrl ?? `${registry}/${hit.slug}?format=md`;
189
+ throw new Error(`No skill found for "${spec}" in ${base}`);
190
+ const url = hit.fetchUrl ?? `${base}/${hit.slug}?format=md`;
84
191
  const body = await fetchText(url);
85
192
  parsed = parseSkill(body, hit.name);
86
193
  source = url;
@@ -94,13 +201,14 @@ export async function installSkill(spec, opts = {}) {
94
201
  rmSync(targetDir, { recursive: true, force: true });
95
202
  mkdirSync(targetDir, { recursive: true });
96
203
  writeFileSync(join(targetDir, "SKILL.md"), reassemble(parsed));
97
- writeFileSync(join(targetDir, ".fastcode-source"), source);
204
+ writeSourceMeta(targetDir, source);
98
205
  return {
99
206
  name: parsed.name || folderName,
100
207
  description: parsed.description,
101
208
  source,
102
209
  path: targetDir,
103
210
  body: parsed.body,
211
+ integrity: "ok",
104
212
  };
105
213
  }
106
214
  /** Remove an installed skill by name or folder. */
@@ -125,8 +233,10 @@ export function uninstallSkill(name) {
125
233
  return false;
126
234
  }
127
235
  /** Search the registry for skills matching a query. */
128
- export async function searchRegistry(query, registry = DEFAULT_REGISTRY) {
129
- const base = registry.replace(/\/+$/, "");
236
+ export async function searchRegistry(query, registry) {
237
+ const base = resolveRegistry(process.env["FASTCODE_SKILLS_REGISTRY"], registry);
238
+ if (!base)
239
+ return [];
130
240
  try {
131
241
  const json = await fetchJson(`${base}/api/v1/skills/search?q=${encodeURIComponent(query)}`);
132
242
  return json.data ?? [];
@@ -145,6 +255,13 @@ export function buildSkillsPrompt(enabled) {
145
255
  const skill = installed.find((s) => s.name === name || basename(s.path) === name);
146
256
  if (!skill)
147
257
  continue;
258
+ if (skill.integrity !== "ok") {
259
+ const reason = skill.integrity === "drift"
260
+ ? "content drift detected"
261
+ : "no integrity baseline (legacy install)";
262
+ process.stderr.write(`skill ${skill.name} ${reason}; disabled. Run /skills verify ${skill.name} --confirm to re-baseline.\n`);
263
+ continue;
264
+ }
148
265
  chunks.push(`<skill name="${skill.name}">\n${skill.body.trim()}\n</skill>`);
149
266
  }
150
267
  if (chunks.length === 0)
package/dist/types.d.ts CHANGED
@@ -5,10 +5,26 @@
5
5
  * what the chat loop needs (streaming completions, model list, auth).
6
6
  * Providers can layer richer behavior on top in their own modules.
7
7
  */
8
- export type Role = "system" | "user" | "assistant";
8
+ export type Role = "system" | "user" | "assistant" | "tool";
9
+ export type ReasoningEffort = "auto" | "low" | "medium" | "high" | "max";
10
+ export interface NativeToolCall {
11
+ id: string;
12
+ name: string;
13
+ arguments: string;
14
+ }
15
+ export interface ToolSchema {
16
+ type: "function";
17
+ function: {
18
+ name: string;
19
+ description: string;
20
+ parameters: Record<string, any>;
21
+ };
22
+ }
9
23
  export interface ChatMessage {
10
24
  role: Role;
11
25
  content: string;
26
+ toolCalls?: NativeToolCall[];
27
+ toolCallId?: string;
12
28
  }
13
29
  export interface ChatRequest {
14
30
  model: string;
@@ -19,14 +35,26 @@ export interface ChatRequest {
19
35
  temperature?: number;
20
36
  /** Soft cap for output tokens. */
21
37
  maxOutputTokens?: number;
38
+ /**
39
+ * Requested thinking depth. Providers that support it map this to their
40
+ * native parameter (`reasoning_effort`, `thinking.budget_tokens`, …);
41
+ * providers that don't should ignore it.
42
+ */
43
+ reasoningEffort?: ReasoningEffort;
22
44
  /** AbortSignal for cancellation (Esc / Ctrl+C). */
23
45
  signal?: AbortSignal;
46
+ /** Stable idempotency key reused across retries of the same logical request. */
47
+ clientRequestId?: string;
48
+ /** Native function-calling schemas (auto mode only). */
49
+ tools?: ToolSchema[];
24
50
  }
25
51
  export interface ChatChunk {
26
52
  /** Token text appended to the response. */
27
53
  delta: string;
28
54
  /** True on the final chunk. */
29
55
  done: boolean;
56
+ /** Complete native tool calls (OpenAI-style) on the final chunk. */
57
+ toolCalls?: NativeToolCall[];
30
58
  /** Provider-specific usage info on the final chunk. */
31
59
  usage?: {
32
60
  inputTokens?: number;
@@ -63,6 +91,25 @@ export interface Provider {
63
91
  listModels(apiKey: string | undefined): Promise<ProviderModel[]>;
64
92
  /** Stream a chat completion. */
65
93
  stream(req: ChatRequest, apiKey: string | undefined): ChatStream;
94
+ /** Whether this provider/model honors ChatRequest.reasoningEffort. */
95
+ supportsReasoning?(model: string): boolean;
96
+ }
97
+ export interface PickerItem {
98
+ value: string;
99
+ label: string;
100
+ hint?: string;
101
+ }
102
+ /**
103
+ * An interactive list a command can hand back to the app. The app renders it
104
+ * with arrow keys, number shortcuts, and Enter (Esc cancels).
105
+ */
106
+ export interface PickerRequest {
107
+ title: string;
108
+ items: PickerItem[];
109
+ /** Currently selected value, highlighted on open. */
110
+ current?: string;
111
+ onSelect(value: string, item: PickerItem): void;
112
+ onCancel?(): void;
66
113
  }
67
114
  export interface SlashCommand {
68
115
  name: string;
@@ -74,6 +121,15 @@ export interface SlashCommand {
74
121
  /** Execute the command. Return text to render, or undefined for no output. */
75
122
  run(ctx: CommandContext): Promise<CommandResult> | CommandResult;
76
123
  }
124
+ export interface CommandCompaction {
125
+ summaryRef?: string;
126
+ source: "local" | "server" | "none";
127
+ beforeTokens: number;
128
+ afterTokens: number;
129
+ droppedMessages: number;
130
+ keepMessages: number;
131
+ reason?: "threshold" | "heap" | "manual";
132
+ }
77
133
  export interface CommandResult {
78
134
  /** Text rendered as a system message in the transcript. */
79
135
  message?: string;
@@ -81,6 +137,12 @@ export interface CommandResult {
81
137
  silent?: boolean;
82
138
  /** Quit the app after this command. */
83
139
  quit?: boolean;
140
+ /** Open an interactive picker after the command runs. */
141
+ picker?: PickerRequest;
142
+ /** Manual /compact result; the app appends the matching compacted fact. */
143
+ compacted?: CommandCompaction;
144
+ /** /clear result; the app appends a cleared fact so resume does not resurrect history. */
145
+ cleared?: boolean;
84
146
  }
85
147
  export interface CommandContext {
86
148
  args: string[];
@@ -101,6 +163,10 @@ export interface AppStateSnapshot {
101
163
  enabledSkills: string[];
102
164
  enabledPlugins: string[];
103
165
  skillsRegistry: string;
166
+ reasoningEffort: ReasoningEffort;
167
+ expanded: boolean;
168
+ busy: boolean;
169
+ draft: string;
104
170
  }
105
171
  export interface AppActions {
106
172
  setProvider(id: string): void;
@@ -109,16 +175,24 @@ export interface AppActions {
109
175
  setAgentMode(enabled: boolean): void;
110
176
  setSystemPrompt(prompt: string): void;
111
177
  setSkillsRegistry(url: string): void;
178
+ setReasoningEffort(effort: ReasoningEffort): void;
179
+ setShellPoolSize(size: number): void;
112
180
  enableSkill(name: string): void;
113
181
  disableSkill(name: string): void;
114
182
  enablePlugin(name: string): void;
115
183
  disablePlugin(name: string): void;
116
184
  clearHistory(): void;
185
+ replaceHistory(messages: ChatMessage[]): void;
186
+ cancelRun(): void;
187
+ steer(text: string): void;
188
+ setExpanded(value: boolean): void;
189
+ setInput(text: string): void;
117
190
  pushSystemMessage(text: string): void;
118
191
  /** Begin the guided API-key flow (choose provider → model → key). */
119
192
  startLogin(options: {
120
193
  provider?: string;
121
194
  model?: string;
195
+ flow?: "provider" | "model" | "login";
122
196
  }): void;
123
197
  }
124
198
  export interface SkillManifest {
@@ -1,7 +1 @@
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>>;
1
+ export declare const MessageBubble: ({ theme, role, content, streaming, markdown }: any) => import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
@@ -6,8 +6,19 @@ const SIGIL = {
6
6
  assistant: "▍ FastCode",
7
7
  system: "✦ system",
8
8
  };
9
- export const MessageBubble = ({ theme, role, content, streaming, markdown, }) => {
9
+ const markdownCache = new Map();
10
+ function cachedMarkdown(md) {
11
+ const hit = markdownCache.get(md);
12
+ if (hit !== undefined)
13
+ return hit;
14
+ const rendered = renderMarkdown(md);
15
+ if (markdownCache.size > 200)
16
+ markdownCache.clear();
17
+ markdownCache.set(md, rendered);
18
+ return rendered;
19
+ }
20
+ export const MessageBubble = ({ theme, role, content, streaming, markdown }) => {
10
21
  const color = role === "user" ? theme.user : role === "assistant" ? theme.assistant : theme.system;
11
- const display = markdown ? renderMarkdown(content) : content;
22
+ const display = markdown ? cachedMarkdown(content) : content;
12
23
  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
24
  };
@@ -0,0 +1,8 @@
1
+ export declare const Picker: ({ theme, title, items, index, current, hint }: {
2
+ theme: any;
3
+ title: any;
4
+ items: any;
5
+ index: any;
6
+ current: any;
7
+ hint: any;
8
+ }) => import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,24 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Presentational picker used by /model, /provider, and /effort.
4
+ *
5
+ * Keyboard handling lives in App so it can suspend the normal prompt input and
6
+ * so every picker behaves the same. This component only renders the window of
7
+ * items around the active index.
8
+ */
9
+ import { Box, Text } from "ink";
10
+ const WINDOW = 10;
11
+ export const Picker = ({ theme, title, items, index, current, hint }) => {
12
+ const max = Math.max(0, items.length - 1);
13
+ const active = Math.max(0, Math.min(index, max));
14
+ const start = Math.max(0, Math.min(active - Math.floor(WINDOW / 2), Math.max(0, items.length - WINDOW)));
15
+ const visible = items.slice(start, start + WINDOW);
16
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: title }), ...visible.map((item, offset) => {
17
+ const itemIndex = start + offset;
18
+ const selected = itemIndex === active;
19
+ const marker = selected ? "\u276F" : " ";
20
+ const number = itemIndex < 9 ? String(itemIndex + 1) : " ";
21
+ const isCurrent = current !== undefined && item.value === current;
22
+ return (_jsxs(Text, { color: selected ? theme.accent : theme.muted, children: [marker, " ", number, ". ", isCurrent ? "\u25CF " : " ", item.label, item.hint ? _jsxs(Text, { color: theme.muted, children: [" ", item.hint] }) : null] }, item.value));
23
+ }), items.length > WINDOW ? (_jsx(Text, { color: theme.muted, children: ` … ${start + 1}-${start + visible.length} of ${items.length}` })) : null, _jsx(Text, { color: theme.muted, children: hint ?? " \u2191/\u2193 select \u00B7 1-9 jump \u00B7 Enter confirm \u00B7 Esc cancel" })] }));
24
+ };
@@ -1,10 +1,9 @@
1
- export declare const Prompt: ({ theme, value, onChange, onSubmit, disabled, placeholder, stable, mask, }: {
1
+ export declare const Prompt: ({ theme, value, onChange, onSubmit, disabled, placeholder, mask, }: {
2
2
  theme: any;
3
3
  value: any;
4
4
  onChange: any;
5
5
  onSubmit: any;
6
6
  disabled: any;
7
7
  placeholder: any;
8
- stable?: boolean;
9
8
  mask: any;
10
9
  }) => import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
package/dist/ui/Prompt.js CHANGED
@@ -15,9 +15,9 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
15
15
  import { Box, Text } from "ink";
16
16
  import TextInput from "ink-text-input";
17
17
  import { COMMANDS } from "../commands/index.js";
18
- export const Prompt = ({ theme, value, onChange, onSubmit, disabled, placeholder, stable = false, mask, }) => {
18
+ export const Prompt = ({ theme, value, onChange, onSubmit, disabled, placeholder, mask, }) => {
19
19
  const suggestion = autocomplete(value);
20
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: disabled ? theme.muted : theme.accent, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: "\u276F " }), disabled ? (_jsx(Text, { color: theme.muted, children: placeholder ?? "Working… (Esc to cancel)" })) : (_jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit, placeholder: placeholder ?? "Type a message or /help …", showCursor: !stable, mask: mask }))] }), !disabled && suggestion ? (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: theme.muted, children: ["\u21AA ", suggestion] }) })) : null] }));
20
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: disabled ? theme.muted : theme.accent, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: "\u276F " }), disabled ? (_jsx(Text, { color: theme.muted, children: placeholder ?? "Working… (Esc to cancel)" })) : (_jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit, placeholder: placeholder ?? "Type a message or /help …", showCursor: true, mask: mask }))] }), !disabled && suggestion ? (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: theme.muted, children: ["\u21AA ", suggestion] }) })) : null] }));
21
21
  };
22
22
  function autocomplete(value) {
23
23
  if (!value.startsWith("/") || value.includes(" "))
@@ -21,13 +21,19 @@ const VERBS = [
21
21
  ];
22
22
  export const Spinner = ({ theme, label, stable = false }) => {
23
23
  const [tick, setTick] = useState(0);
24
+ // stable UI only lowers the redraw rate; it must still animate. A frozen
25
+ // frame made long waits look like a hang.
26
+ const configured = Number(process.env["FASTCODE_SPINNER_MS"]);
27
+ const interval = Number.isFinite(configured) && configured >= 60
28
+ ? configured
29
+ : stable
30
+ ? 120
31
+ : 80;
24
32
  useEffect(() => {
25
- if (stable)
26
- return;
27
- const id = setInterval(() => setTick((t) => t + 1), 80);
33
+ const id = setInterval(() => setTick((t) => t + 1), interval);
28
34
  return () => clearInterval(id);
29
- }, [stable]);
30
- const frame = stable ? "⠋" : FRAMES[tick % FRAMES.length];
35
+ }, [interval]);
36
+ const frame = FRAMES[tick % FRAMES.length];
31
37
  const verb = label ?? VERBS[Math.floor(tick / 24) % VERBS.length];
32
38
  return (_jsxs(Text, { color: theme.accent, children: [frame, " ", _jsxs(Text, { color: theme.muted, children: [verb, "\u2026"] })] }));
33
39
  };
@@ -1,4 +1,4 @@
1
- export declare const StatusBar: ({ theme, providerId, model, inputTokens, outputTokens, costUsd, enabledSkillCount, hint, }: {
1
+ export declare const StatusBar: ({ theme, providerId, model, inputTokens, outputTokens, costUsd, enabledSkillCount, reasoningEffort, hint, }: {
2
2
  theme: any;
3
3
  providerId: any;
4
4
  model: any;
@@ -6,5 +6,6 @@ export declare const StatusBar: ({ theme, providerId, model, inputTokens, output
6
6
  outputTokens: any;
7
7
  costUsd: any;
8
8
  enabledSkillCount: any;
9
+ reasoningEffort: any;
9
10
  hint: any;
10
11
  }) => import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
@@ -1,19 +1,20 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text, useStdout } from "ink";
3
3
  import { platformLabel } from "../lib/platform.js";
4
- export const StatusBar = ({ theme, providerId, model, inputTokens, outputTokens, costUsd, enabledSkillCount, hint, }) => {
4
+ export const StatusBar = ({ theme, providerId, model, inputTokens, outputTokens, costUsd, enabledSkillCount, reasoningEffort, hint, }) => {
5
5
  const { stdout } = useStdout();
6
6
  const columns = stdout?.columns ?? process.stdout.columns ?? 80;
7
7
  const compact = columns < 88;
8
8
  const cost = costUsd > 0 ? `$${costUsd.toFixed(4)}` : "$0.0000";
9
9
  const providerModel = `${providerId}/${model}`;
10
10
  const label = compact ? truncate(providerModel, Math.max(18, columns - 28)) : providerModel;
11
- return (_jsxs(Box, { flexDirection: compact ? "column" : "row", justifyContent: "space-between", borderStyle: "round", borderColor: theme.border, paddingX: 1, children: [_jsxs(Text, { color: theme.muted, children: [_jsx(Text, { color: theme.accent, children: "\u25CF" }), " ", label, _jsx(Text, { color: theme.muted, children: " \u00B7 " }), inputTokens, "\u2191 ", outputTokens, "\u2193 ", cost, !compact ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: " \u00B7 " }), "skills: ", enabledSkillCount, _jsx(Text, { color: theme.muted, children: " \u00B7 " }), platformLabel()] })) : null] }), _jsx(Text, { color: theme.muted, children: compact
12
- ? `${platformLabel()} · skills:${enabledSkillCount} · ${hint ?? "/help · esc · ctrl+c"}`
13
- : hint ?? "/help · esc cancel · ctrl+c quit" })] }));
11
+ const effort = reasoningEffort && reasoningEffort !== "auto" ? `effort:${reasoningEffort}` : "";
12
+ return (_jsxs(Box, { flexDirection: compact ? "column" : "row", justifyContent: "space-between", borderStyle: "round", borderColor: theme.border, paddingX: 1, children: [_jsxs(Text, { color: theme.muted, children: [_jsx(Text, { color: theme.accent, children: "\u25CF" }), " ", label, _jsx(Text, { color: theme.muted, children: " \u00B7 " }), inputTokens, "\u2191 ", outputTokens, "\u2193 ", cost, !compact && effort ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: " \u00B7 " }), effort] })) : null, !compact ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: " \u00B7 " }), "skills: ", enabledSkillCount, _jsx(Text, { color: theme.muted, children: " \u00B7 " }), platformLabel()] })) : null] }), _jsx(Text, { color: theme.muted, children: compact
13
+ ? `${effort ? effort + " \u00B7 " : ""}${platformLabel()} \u00B7 skills:${enabledSkillCount} \u00B7 ${hint ?? "/help \u00B7 esc \u00B7 ctrl+c"}`
14
+ : hint ?? "/help \u00B7 esc cancel \u00B7 ctrl+c quit" })] }));
14
15
  };
15
16
  function truncate(value, max) {
16
17
  if (value.length <= max)
17
18
  return value;
18
- return `${value.slice(0, Math.max(1, max - 1))}…`;
19
+ return `${value.slice(0, Math.max(1, max - 1))}\u2026`;
19
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datename/fastcode-termux",
3
- "version": "0.1.6",
3
+ "version": "0.1.8-beta.0",
4
4
  "description": "FastCode — a blazing-fast TUI coding assistant. Multi-LLM, skills.sh skills, slash commands. Works on Termux, Linux, Windows, and macOS.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  "node": ">=18.17"
22
22
  },
23
23
  "scripts": {
24
- "build": "tsc -p tsconfig.json && node scripts/build.mjs",
24
+ "build": "tsc -p tsconfig.build.json && node scripts/build.mjs",
25
25
  "build:bundle": "node scripts/build.mjs",
26
26
  "dev": "tsx src/index.ts",
27
27
  "start": "node bin/fastcode.mjs",
@@ -65,6 +65,7 @@
65
65
  "url": "https://github.com/dateName/fastcode-termux/issues"
66
66
  },
67
67
  "dependencies": {
68
+ "bash-parser": "^0.5.0",
68
69
  "boxen": "^7.1.1",
69
70
  "chalk": "^5.3.0",
70
71
  "cli-highlight": "^2.1.11",