@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,127 @@
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
+
27
+ export type SkillIntegrity = "ok" | "drift" | "unverified";
28
+
29
+ const REGISTRY_REQUIRED = "No skills registry configured. Set one with `/skills registry <url>` or FASTCODE_SKILLS_REGISTRY.";
30
+
31
+ /**
32
+ * Pick the skills registry without treating an empty environment variable as
33
+ * "configured". `??` would keep `""` and make env overrides silently break;
34
+ * explicit null/empty checks keep `""` and `" "` on the config path. There is
35
+ * intentionally no built-in default registry.
36
+ */
37
+ export function resolveRegistry(envValue?: string | null, configValue?: string | null): string {
38
+ const env = typeof envValue === "string" && envValue.trim() ? envValue.trim() : "";
39
+ const configured = typeof configValue === "string" && configValue.trim() ? configValue.trim() : "";
40
+ return (env || configured).replace(/\/+$/, "");
41
+ }
42
+
43
+ function requireRegistry(registry: string): string {
44
+ if (!registry)
45
+ throw new Error(REGISTRY_REQUIRED);
46
+ return registry;
47
+ }
48
+ export function hashSkillDirectory(folder: string): string {
49
+ const entries = skillFiles(folder).sort();
50
+ return createHash("sha256").update(entries.join("\n")).digest("hex");
51
+ }
52
+ function skillFiles(folder: string, prefix = ""): string[] {
53
+ const out: string[] = [];
54
+ let items;
55
+ try {
56
+ items = readdirSync(folder, { withFileTypes: true });
57
+ }
58
+ catch {
59
+ return out;
60
+ }
61
+ for (const item of items) {
62
+ if (item.name === SOURCE_FILE)
63
+ continue;
64
+ const full = join(folder, item.name);
65
+ const rel = prefix ? prefix + "/" + item.name : item.name;
66
+ if (item.isDirectory()) {
67
+ out.push(...skillFiles(full, rel));
68
+ continue;
69
+ }
70
+ if (!item.isFile())
71
+ continue;
72
+ const digest = createHash("sha256").update(readFileSync(full)).digest("hex");
73
+ out.push(rel + "\0" + digest);
74
+ }
75
+ return out;
76
+ }
77
+ function readSourceMeta(folder: string): { source: string; sha256?: string; legacy: boolean } {
78
+ const sourcePath = join(folder, SOURCE_FILE);
79
+ if (!existsSync(sourcePath))
80
+ return { source: "local", legacy: true };
81
+ const raw = readFileSync(sourcePath, "utf8").trim();
82
+ if (raw.startsWith("{")) {
83
+ try {
84
+ const parsed = JSON.parse(raw);
85
+ return {
86
+ source: typeof parsed.source === "string" ? parsed.source : "local",
87
+ sha256: typeof parsed.sha256 === "string" ? parsed.sha256 : undefined,
88
+ legacy: typeof parsed.sha256 !== "string",
89
+ };
90
+ }
91
+ catch {
92
+ // Fall through and treat the file as a legacy plain-text source.
93
+ }
94
+ }
95
+ return { source: raw || "local", legacy: true };
96
+ }
97
+ function writeSourceMeta(folder: string, source: string): string {
98
+ const sha256 = hashSkillDirectory(folder);
99
+ const payload = {
100
+ version: INTEGRITY_VERSION,
101
+ algorithm: "sha256",
102
+ source,
103
+ sha256,
104
+ };
105
+ writeFileSync(join(folder, SOURCE_FILE), JSON.stringify(payload, null, 2));
106
+ return sha256;
107
+ }
108
+ export function rebaselineSkills(names?: string[]): Array<{ name: string; ok: boolean; warning?: string }> {
109
+ const installed = listInstalledSkills();
110
+ const wanted = names && names.length > 0 ? new Set(names) : undefined;
111
+ const results: Array<{ name: string; ok: boolean; warning?: string }> = [];
112
+ let changed = false;
113
+ for (const skill of installed) {
114
+ if (wanted && !wanted.has(skill.name) && !wanted.has(basename(skill.path)))
115
+ continue;
116
+ try {
117
+ writeSourceMeta(skill.path, skill.source === "local" ? "local" : skill.source);
118
+ results.push({ name: skill.name, ok: true });
119
+ changed = true;
120
+ }
121
+ catch (err: any) {
122
+ results.push({ name: skill.name, ok: false, warning: err?.message ?? String(err) });
123
+ }
124
+ }
125
+ if (changed)
126
+ invalidateConfigCache();
127
+ return results;
128
+ }
23
129
  /** List skills currently installed under ~/.fastcode/skills. */
24
130
  export function listInstalledSkills() {
25
131
  const { skills } = getPaths();
@@ -35,16 +141,20 @@ export function listInstalledSkills() {
35
141
  continue;
36
142
  const raw = readFileSync(md, "utf8");
37
143
  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";
144
+ const meta = readSourceMeta(folder);
145
+ const actualHash = hashSkillDirectory(folder);
146
+ const integrity: SkillIntegrity = !meta.sha256
147
+ ? "unverified"
148
+ : actualHash === meta.sha256 ? "ok" : "drift";
42
149
  out.push({
43
150
  name: parsed.name || dir.name,
44
151
  description: parsed.description,
45
- source,
152
+ source: meta.source,
46
153
  path: folder,
47
154
  body: parsed.body,
155
+ integrity,
156
+ sha256: actualHash,
157
+ lockedHash: meta.sha256,
48
158
  });
49
159
  }
50
160
  return out.sort((a, b) => a.name.localeCompare(b.name));
@@ -56,7 +166,7 @@ export function getSkill(name) {
56
166
  /** Install a skill. Returns the parsed manifest on success. */
57
167
  export async function installSkill(spec, opts: any = {}) {
58
168
  const { skills } = getPaths();
59
- const registry = (opts.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
169
+ const registry = resolveRegistry(process.env["FASTCODE_SKILLS_REGISTRY"], opts.registry);
60
170
  let parsed;
61
171
  let source;
62
172
  let folderName;
@@ -68,7 +178,8 @@ export async function installSkill(spec, opts: any = {}) {
68
178
  }
69
179
  else if (looksLikeRepoPath(spec)) {
70
180
  // owner/repo or owner/repo/skill — fetch via registry's raw endpoint.
71
- const url = `${registry}/${spec}?format=md`;
181
+ const base = requireRegistry(registry);
182
+ const url = `${base}/${spec}?format=md`;
72
183
  const body = await fetchText(url);
73
184
  parsed = parseSkill(body, parts(spec).pop() ?? spec);
74
185
  source = url;
@@ -76,11 +187,12 @@ export async function installSkill(spec, opts: any = {}) {
76
187
  }
77
188
  else {
78
189
  // Single-token name → search the registry.
79
- const hits = await searchRegistry(spec, registry);
190
+ const base = requireRegistry(registry);
191
+ const hits = await searchRegistry(spec, base);
80
192
  const hit = hits[0];
81
193
  if (!hit)
82
- throw new Error(`No skill found for "${spec}" in ${registry}`);
83
- const url = hit.fetchUrl ?? `${registry}/${hit.slug}?format=md`;
194
+ throw new Error(`No skill found for "${spec}" in ${base}`);
195
+ const url = hit.fetchUrl ?? `${base}/${hit.slug}?format=md`;
84
196
  const body = await fetchText(url);
85
197
  parsed = parseSkill(body, hit.name);
86
198
  source = url;
@@ -94,13 +206,14 @@ export async function installSkill(spec, opts: any = {}) {
94
206
  rmSync(targetDir, { recursive: true, force: true });
95
207
  mkdirSync(targetDir, { recursive: true });
96
208
  writeFileSync(join(targetDir, "SKILL.md"), reassemble(parsed));
97
- writeFileSync(join(targetDir, ".fastcode-source"), source);
209
+ writeSourceMeta(targetDir, source);
98
210
  return {
99
211
  name: parsed.name || folderName,
100
212
  description: parsed.description,
101
213
  source,
102
214
  path: targetDir,
103
215
  body: parsed.body,
216
+ integrity: "ok" as SkillIntegrity,
104
217
  };
105
218
  }
106
219
  /** Remove an installed skill by name or folder. */
@@ -125,8 +238,10 @@ export function uninstallSkill(name) {
125
238
  return false;
126
239
  }
127
240
  /** Search the registry for skills matching a query. */
128
- export async function searchRegistry(query, registry = DEFAULT_REGISTRY) {
129
- const base = registry.replace(/\/+$/, "");
241
+ export async function searchRegistry(query, registry?: string) {
242
+ const base = resolveRegistry(process.env["FASTCODE_SKILLS_REGISTRY"], registry);
243
+ if (!base)
244
+ return [];
130
245
  try {
131
246
  const json = await fetchJson(`${base}/api/v1/skills/search?q=${encodeURIComponent(query)}`);
132
247
  return json.data ?? [];
@@ -145,6 +260,13 @@ export function buildSkillsPrompt(enabled) {
145
260
  const skill = installed.find((s) => s.name === name || basename(s.path) === name);
146
261
  if (!skill)
147
262
  continue;
263
+ if (skill.integrity !== "ok") {
264
+ const reason = skill.integrity === "drift"
265
+ ? "content drift detected"
266
+ : "no integrity baseline (legacy install)";
267
+ process.stderr.write(`skill ${skill.name} ${reason}; disabled. Run /skills verify ${skill.name} --confirm to re-baseline.\n`);
268
+ continue;
269
+ }
148
270
  chunks.push(`<skill name="${skill.name}">\n${skill.body.trim()}\n</skill>`);
149
271
  }
150
272
  if (chunks.length === 0)
@@ -0,0 +1,14 @@
1
+ declare module "bash-parser" {
2
+ const parse: (code: string, options?: any) => any;
3
+ export default parse;
4
+ }
5
+
6
+ declare module "highlight.js/lib/core.js" {
7
+ const hljs: any;
8
+ export default hljs;
9
+ }
10
+
11
+ declare module "highlight.js/lib/languages/*.js" {
12
+ const language: any;
13
+ export default language;
14
+ }
package/src/types.ts CHANGED
@@ -5,10 +5,29 @@
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
+
11
+ export interface NativeToolCall {
12
+ id: string;
13
+ name: string;
14
+ arguments: string;
15
+ }
16
+
17
+ export interface ToolSchema {
18
+ type: "function";
19
+ function: {
20
+ name: string;
21
+ description: string;
22
+ parameters: Record<string, any>;
23
+ };
24
+ }
25
+
9
26
  export interface ChatMessage {
10
27
  role: Role;
11
28
  content: string;
29
+ toolCalls?: NativeToolCall[];
30
+ toolCallId?: string;
12
31
  }
13
32
  export interface ChatRequest {
14
33
  model: string;
@@ -19,14 +38,26 @@ export interface ChatRequest {
19
38
  temperature?: number;
20
39
  /** Soft cap for output tokens. */
21
40
  maxOutputTokens?: number;
41
+ /**
42
+ * Requested thinking depth. Providers that support it map this to their
43
+ * native parameter (`reasoning_effort`, `thinking.budget_tokens`, …);
44
+ * providers that don't should ignore it.
45
+ */
46
+ reasoningEffort?: ReasoningEffort;
22
47
  /** AbortSignal for cancellation (Esc / Ctrl+C). */
23
48
  signal?: AbortSignal;
49
+ /** Stable idempotency key reused across retries of the same logical request. */
50
+ clientRequestId?: string;
51
+ /** Native function-calling schemas (auto mode only). */
52
+ tools?: ToolSchema[];
24
53
  }
25
54
  export interface ChatChunk {
26
55
  /** Token text appended to the response. */
27
56
  delta: string;
28
57
  /** True on the final chunk. */
29
58
  done: boolean;
59
+ /** Complete native tool calls (OpenAI-style) on the final chunk. */
60
+ toolCalls?: NativeToolCall[];
30
61
  /** Provider-specific usage info on the final chunk. */
31
62
  usage?: {
32
63
  inputTokens?: number;
@@ -63,6 +94,25 @@ export interface Provider {
63
94
  listModels(apiKey: string | undefined): Promise<ProviderModel[]>;
64
95
  /** Stream a chat completion. */
65
96
  stream(req: ChatRequest, apiKey: string | undefined): ChatStream;
97
+ /** Whether this provider/model honors ChatRequest.reasoningEffort. */
98
+ supportsReasoning?(model: string): boolean;
99
+ }
100
+ export interface PickerItem {
101
+ value: string;
102
+ label: string;
103
+ hint?: string;
104
+ }
105
+ /**
106
+ * An interactive list a command can hand back to the app. The app renders it
107
+ * with arrow keys, number shortcuts, and Enter (Esc cancels).
108
+ */
109
+ export interface PickerRequest {
110
+ title: string;
111
+ items: PickerItem[];
112
+ /** Currently selected value, highlighted on open. */
113
+ current?: string;
114
+ onSelect(value: string, item: PickerItem): void;
115
+ onCancel?(): void;
66
116
  }
67
117
  export interface SlashCommand {
68
118
  name: string;
@@ -74,6 +124,16 @@ export interface SlashCommand {
74
124
  /** Execute the command. Return text to render, or undefined for no output. */
75
125
  run(ctx: CommandContext): Promise<CommandResult> | CommandResult;
76
126
  }
127
+ export interface CommandCompaction {
128
+ summaryRef?: string;
129
+ source: "local" | "server" | "none";
130
+ beforeTokens: number;
131
+ afterTokens: number;
132
+ droppedMessages: number;
133
+ keepMessages: number;
134
+ reason?: "threshold" | "heap" | "manual";
135
+ }
136
+
77
137
  export interface CommandResult {
78
138
  /** Text rendered as a system message in the transcript. */
79
139
  message?: string;
@@ -81,6 +141,12 @@ export interface CommandResult {
81
141
  silent?: boolean;
82
142
  /** Quit the app after this command. */
83
143
  quit?: boolean;
144
+ /** Open an interactive picker after the command runs. */
145
+ picker?: PickerRequest;
146
+ /** Manual /compact result; the app appends the matching compacted fact. */
147
+ compacted?: CommandCompaction;
148
+ /** /clear result; the app appends a cleared fact so resume does not resurrect history. */
149
+ cleared?: boolean;
84
150
  }
85
151
  export interface CommandContext {
86
152
  args: string[];
@@ -101,6 +167,10 @@ export interface AppStateSnapshot {
101
167
  enabledSkills: string[];
102
168
  enabledPlugins: string[];
103
169
  skillsRegistry: string;
170
+ reasoningEffort: ReasoningEffort;
171
+ expanded: boolean;
172
+ busy: boolean;
173
+ draft: string;
104
174
  }
105
175
  export interface AppActions {
106
176
  setProvider(id: string): void;
@@ -109,14 +179,21 @@ export interface AppActions {
109
179
  setAgentMode(enabled: boolean): void;
110
180
  setSystemPrompt(prompt: string): void;
111
181
  setSkillsRegistry(url: string): void;
182
+ setReasoningEffort(effort: ReasoningEffort): void;
183
+ setShellPoolSize(size: number): void;
112
184
  enableSkill(name: string): void;
113
185
  disableSkill(name: string): void;
114
186
  enablePlugin(name: string): void;
115
187
  disablePlugin(name: string): void;
116
188
  clearHistory(): void;
189
+ replaceHistory(messages: ChatMessage[]): void;
190
+ cancelRun(): void;
191
+ steer(text: string): void;
192
+ setExpanded(value: boolean): void;
193
+ setInput(text: string): void;
117
194
  pushSystemMessage(text: string): void;
118
195
  /** Begin the guided API-key flow (choose provider → model → key). */
119
- startLogin(options: { provider?: string; model?: string }): void;
196
+ startLogin(options: { provider?: string; model?: string; flow?: "provider" | "model" | "login" }): void;
120
197
  }
121
198
  export interface SkillManifest {
122
199
  name: string;
package/src/ui/Message.ts CHANGED
@@ -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 }: any) => {
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,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
+ };
package/src/ui/Prompt.ts 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(" "))
package/src/ui/Spinner.ts CHANGED
@@ -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,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/tsconfig.json CHANGED
@@ -6,6 +6,7 @@
6
6
  "lib": ["ES2023"],
7
7
  "rootDir": "src",
8
8
  "outDir": "dist",
9
+ "noEmit": true,
9
10
  "declaration": true,
10
11
  "sourceMap": false,
11
12
  "removeComments": false,
File without changes