@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,18 +5,27 @@
5
5
  * diagnosed without opening the TUI: Node version, Termux detection, whether
6
6
  * stdout is a real terminal, the config root, and which provider keys exist.
7
7
  */
8
+ import { existsSync, statSync } from "node:fs";
9
+ import { totalmem, freemem, tmpdir as osTmpdir } from "node:os";
8
10
  import { detectPlatform, platformLabel } from "./platform.js";
9
11
  import { readPackageMeta } from "./pkg.js";
10
12
  import { shouldUseStableTerminalUi } from "./uiMode.js";
11
13
  import { getPaths } from "../config/paths.js";
12
- import { loadConfig, resolveApiKey, resolveProviderKey } from "../config/store.js";
14
+ import { configCacheStatus, loadConfig, resolveApiKey, resolveProviderKey } from "../config/store.js";
15
+ import { debugEnabled } from "./debug.js";
13
16
  import { PROVIDERS } from "../providers/index.js";
14
- import { listInstalledSkills } from "../skills/manager.js";
17
+ import { listInstalledSkills, resolveRegistry } from "../skills/manager.js";
15
18
  import { listInstalledPlugins } from "../plugins/manager.js";
16
- import { loggingEnabled, listSessions, sessionCacheLimitBytes } from "../session/log.js";
19
+ import { findUnclosedSessions, fsyncMode, loggingEnabled, listSessions, sessionCacheLimitBytes } from "../session/log.js";
17
20
  import { isTrusted } from "../config/trust.js";
18
- import { DEFAULT_DENY_PATTERNS } from "../agent/guards.js";
21
+ import { DEFAULT_DENY_PATTERNS, loopExemptTools } from "../agent/guards.js";
19
22
  import { toolOutputLimit } from "../agent/limits.js";
23
+ import { grepMaxBytes, readManyMaxBytes, readMaxBytes, readScanMaxBytes } from "../agent/tools/readers.js";
24
+ import { normalizeEgressConfig } from "../agent/policy/egress.js";
25
+ import { compactionConfigFromEnv, heapExceeded } from "../agent/compaction.js";
26
+ import { shellPoolStats } from "../agent/shell-session.js";
27
+ import { resolveToolProtocol, toolCapabilityKey } from "../agent/protocol-mode.js";
28
+ import { getModelMeta, modelMetaOverridePath } from "./model-meta.js";
20
29
  const NODE_REQUIRED = "18.17";
21
30
  function readVersion() {
22
31
  return readPackageMeta().version;
@@ -28,6 +37,7 @@ function nodeIsSupported() {
28
37
  }
29
38
  export function collectDiagnostics() {
30
39
  const config = loadConfig();
40
+ const egress = normalizeEgressConfig(config.egress);
31
41
  let paths;
32
42
  try {
33
43
  paths = getPaths();
@@ -55,6 +65,16 @@ export function collectDiagnostics() {
55
65
  // Same for plugins.
56
66
  }
57
67
  const platform = detectPlatform();
68
+ const compaction = compactionConfigFromEnv();
69
+ const shells = shellPoolStats();
70
+ let authMode = "missing";
71
+ try {
72
+ if (paths?.auth && existsSync(paths.auth))
73
+ authMode = (statSync(paths.auth).mode & 0o777).toString(8).padStart(4, "0");
74
+ }
75
+ catch {
76
+ authMode = "unreadable";
77
+ }
58
78
  return {
59
79
  version: readVersion(),
60
80
  node: process.version,
@@ -69,17 +89,26 @@ export function collectDiagnostics() {
69
89
  configRoot: paths?.root,
70
90
  configFile: paths?.config,
71
91
  authFile: paths?.auth,
92
+ authMode,
93
+ tmpdir: process.env["TMPDIR"] || osTmpdir(),
94
+ memTotalMb: totalmem() / 1024 / 1024,
95
+ memFreeMb: freemem() / 1024 / 1024,
72
96
  providerId: config.providerId,
73
97
  model: config.model,
74
98
  keyPresent,
75
99
  keyStatus: provider ? keyLookup.status : "missing",
76
100
  keyEnv: provider?.authEnv?.[0],
77
101
  skillsInstalled,
102
+ skillsRegistry: resolveRegistry(process.env["FASTCODE_SKILLS_REGISTRY"], config.skillsRegistry),
103
+ debug: debugEnabled(),
104
+ configCache: configCacheStatus().cached,
78
105
  pluginsInstalled,
79
106
  enabledSkills: (config.enabledSkills ?? []).length,
80
107
  enabledPlugins: (config.enabledPlugins ?? []).length,
81
108
  sessionLogEnabled: loggingEnabled(),
82
109
  sessionCacheMb: Math.round(sessionCacheLimitBytes() / (1024 * 1024)),
110
+ sessionFsync: fsyncMode(),
111
+ unclosedSessions: findUnclosedSessions().length,
83
112
  sessionsDir: paths?.sessions,
84
113
  sessionsCount: listSessions().length,
85
114
  sessionsBytes: listSessions().reduce((sum, entry) => sum + entry.bytes, 0),
@@ -88,6 +117,41 @@ export function collectDiagnostics() {
88
117
  shellDenyPatterns: DEFAULT_DENY_PATTERNS.length,
89
118
  allowDangerous: ["1", "true", "yes", "on"].includes((process.env["FASTCODE_ALLOW_DANGEROUS"] ?? "").trim().toLowerCase()),
90
119
  toolOutputLimit: toolOutputLimit(),
120
+ readMaxBytes: readMaxBytes(),
121
+ readScanMaxBytes: readScanMaxBytes(),
122
+ readManyMaxBytes: readManyMaxBytes(),
123
+ grepMaxBytes: grepMaxBytes(),
124
+ egress: {
125
+ enabled: egress.enabled,
126
+ mode: egress.mode,
127
+ maxScanBytes: egress.maxScanBytes,
128
+ denyPaths: egress.denyPaths.length,
129
+ denyGlobs: egress.denyGlobs.length,
130
+ denyContentMarkers: egress.denyContentMarkers.length,
131
+ scanToolOutput: egress.scanToolOutput,
132
+ },
133
+ tokenBudget: positiveEnv("FASTCODE_TOKEN_BUDGET"),
134
+ costBudgetUsd: positiveEnv("FASTCODE_COST_BUDGET_USD"),
135
+ compactionEnabled: compaction.enabled,
136
+ compactionRatio: compaction.localThresholdRatio,
137
+ compactionKeepTurns: compaction.keepTurns,
138
+ compactionSummaryTokens: compaction.maxSummaryTokens,
139
+ compactionHeapMb: compaction.heapLimitMb,
140
+ compactionHeapUsedMb: process.memoryUsage().heapUsed / 1024 / 1024,
141
+ compactionForced: heapExceeded(compaction),
142
+ serverCompactionAware: compaction.serverCompactionAware,
143
+ shellActive: shells.active,
144
+ shellPool: shells.pool,
145
+ shellPoolMax: shells.maxPool,
146
+ shellSpawns: shells.spawns,
147
+ shellMaxSpawn: shells.maxSpawnPerTurn,
148
+ sseIdleMs: positiveEnv("FASTCODE_SSE_IDLE_MS") || 25_000,
149
+ toolProtocol: resolveToolProtocol(config, config.providerId, config.model),
150
+ toolProtocolMode: (process.env["FASTCODE_TOOL_PROTOCOL"] ?? "legacy").trim().toLowerCase(),
151
+ toolCapability: config.capabilities?.toolCalling?.[toolCapabilityKey(config.providerId, config.model)] ?? "unknown",
152
+ loopExempt: Array.from(loopExemptTools()),
153
+ modelMetaFile: modelMetaOverridePath(),
154
+ modelMeta: getModelMeta(config.providerId, config.model),
91
155
  providers: PROVIDERS.map((p) => ({
92
156
  id: p.id,
93
157
  label: p.label,
@@ -96,6 +160,10 @@ export function collectDiagnostics() {
96
160
  })),
97
161
  };
98
162
  }
163
+ function positiveEnv(name) {
164
+ const value = Number(process.env[name]);
165
+ return Number.isFinite(value) && value > 0 ? value : 0;
166
+ }
99
167
  export function formatDiagnostics(info) {
100
168
  const lines = [];
101
169
  lines.push(`FastCode ${info.version} — environment check`);
@@ -106,13 +174,25 @@ export function formatDiagnostics(info) {
106
174
  lines.push(` stable UI ${info.stableUi ? "on (anti-flicker)" : "off"}`);
107
175
  lines.push(" config root " + (info.configRoot ?? "(unavailable)"));
108
176
  lines.push(" config file " + (info.configFile ?? "(unavailable)"));
109
- lines.push(" keys file " + (info.authFile ?? "(unavailable)") + " (separate, chmod 0600)");
177
+ lines.push(" keys file " + (info.authFile ?? "(unavailable)") + " mode " + info.authMode + (info.authMode !== "missing" && info.authMode !== "0600" ? " [WARN: expected 0600]" : ""));
178
+ lines.push(" tmpdir " + info.tmpdir + (info.termux && !String(info.tmpdir).startsWith("/data/data/com.termux") ? " [WARN: use $PREFIX/tmp]" : ""));
179
+ lines.push(" memory " + info.memFreeMb.toFixed(0) + "MB free / " + info.memTotalMb.toFixed(0) + "MB total");
110
180
  lines.push(" trusted cwd " + (info.trusted ? "yes" : "NO - agent tools ask first or stay off") + " (" + (info.trustFile ?? "?") + ")");
111
181
  lines.push(" session log " + (info.sessionLogEnabled ? "on" : "off") + " cache cap " + info.sessionCacheMb + "MB, " + info.sessionsCount + " file(s), " + (info.sessionsBytes / 1024 / 1024).toFixed(2) + "MB used");
112
- lines.push(" session dir " + (info.sessionsDir ?? "(unavailable)"));
113
- lines.push(" shell guard " + info.shellDenyPatterns + " deny patterns" + (info.allowDangerous ? " [FASTCODE_ALLOW_DANGEROUS is ON]" : ""));
182
+ lines.push(" session fsync " + info.sessionFsync + " unclosed " + info.unclosedSessions + " " + (info.sessionsDir ?? "(unavailable)"));
183
+ lines.push(" shell guard " + info.shellDenyPatterns + " legacy soft patterns" + (info.allowDangerous ? " [FASTCODE_ALLOW_DANGEROUS is ON]" : ""));
114
184
  lines.push(" tool output " + info.toolOutputLimit + " chars max (streaming cap)");
185
+ lines.push(` read limits read ${info.readMaxBytes}B · scan ${info.readScanMaxBytes}B · read_many ${info.readManyMaxBytes}B · grep ${info.grepMaxBytes}B`);
186
+ lines.push(` egress ${info.egress.enabled ? "enforce" : "audit-only"} · mode ${info.egress.mode} · scan ${info.egress.maxScanBytes}B · paths ${info.egress.denyPaths} · globs ${info.egress.denyGlobs} · markers ${info.egress.denyContentMarkers}`);
187
+ lines.push(` budgets ${info.tokenBudget > 0 ? "tokens ≤ " + info.tokenBudget : "token budget off"} · ${info.costBudgetUsd > 0 ? "$" + info.costBudgetUsd : "cost budget off"}`);
188
+ lines.push(` compaction ${info.compactionEnabled ? "on" : "off"} · ${Math.round(info.compactionRatio * 100)}% · keep ${info.compactionKeepTurns} turns · summary ${info.compactionSummaryTokens} tok · heap ${info.compactionHeapUsedMb.toFixed(1)}/${info.compactionHeapMb}MB${info.compactionForced ? " [FORCED]" : ""}${info.serverCompactionAware ? " · server-aware" : ""}`);
189
+ lines.push(` shell pool ${info.shellActive} active · ${info.shellPool}/${info.shellPoolMax} resident · ${info.shellSpawns} spawns · cap ${info.shellMaxSpawn}/turn`);
190
+ lines.push(` loop exempt ${info.loopExempt.length > 0 ? info.loopExempt.join(", ") : "(none)"}`);
191
+ lines.push(` model meta ctx ${info.modelMeta.contextWindow} · ~${info.modelMeta.charsPerToken} chars/token · ${info.modelMeta.supportsTools ? "tools" : "no tools"} · ${info.modelMetaFile}`);
192
+ lines.push(` sse timeout ${info.sseIdleMs}ms idle`);
193
+ lines.push(` tool protocol ${info.toolProtocol} (${info.toolProtocolMode}) · cached ${info.toolCapability}`);
115
194
  lines.push(` provider ${info.providerId} · model ${info.model}`);
195
+ lines.push(` config cache ${info.configCache ? "warm" : "cold"} · debug ${info.debug ? "on" : "off"}`);
116
196
  let apiKeyStatus;
117
197
  if (info.keyPresent)
118
198
  apiKeyStatus = "present";
@@ -121,7 +201,7 @@ export function formatDiagnostics(info) {
121
201
  else
122
202
  apiKeyStatus = `MISSING — set ${info.keyEnv ?? "the provider env var"} or /login ${info.providerId} ${info.model} <key>`;
123
203
  lines.push(` api key ${apiKeyStatus}`);
124
- lines.push(` skills ${info.skillsInstalled} installed, ${info.enabledSkills} enabled`);
204
+ lines.push(` skills ${info.skillsInstalled} installed, ${info.enabledSkills} enabled (${info.skillsRegistry || "no registry configured"})`);
125
205
  lines.push(` plugins ${info.pluginsInstalled} installed, ${info.enabledPlugins} enabled`);
126
206
  lines.push("");
127
207
  lines.push(" provider keys:");
@@ -134,6 +214,8 @@ export function formatDiagnostics(info) {
134
214
  lines.push(" - pkg install nodejs git");
135
215
  lines.push(" - keep the session alive with: termux-wake-lock");
136
216
  lines.push(" - add an extra-keys row for Esc/Ctrl in ~/.termux/termux.properties");
217
+ lines.push(" - long tasks: export TMPDIR=$PREFIX/tmp and use termux-wake-lock");
218
+ lines.push(` - Android 12+ caps phantom processes (~32); background shells are pooled (${info.shellPool} resident, cap ${info.shellMaxSpawn}/turn)`);
137
219
  }
138
220
  return lines.join("\n");
139
221
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Trimmed highlight.js registry.
3
+ *
4
+ * cli-highlight imports the full `highlight.js` package, which registers every
5
+ * language and adds roughly a megabyte of language definitions to the bundle
6
+ * for a TUI that mostly renders JS/TS/JSON/sh/Python. The esbuild plugin in
7
+ * scripts/esbuild-plugins.mjs aliases the bare `highlight.js` import to this
8
+ * module so only the common set below is bundled.
9
+ *
10
+ * Unknown languages still work: cli-highlight throws, marked-terminal catches
11
+ * and renders the fence as plain styled text. `renderMarkdown` adds a short
12
+ * dimmed hint instead of failing or blanking the message.
13
+ */
14
+ import hljs from "highlight.js/lib/core.js";
15
+ export declare const COMMON_HIGHLIGHT_LANGUAGES: string[];
16
+ export declare function supportsHighlightLanguage(language: string | undefined): boolean;
17
+ export declare const highlight: any;
18
+ export declare const highlightAuto: any;
19
+ export declare const listLanguages: any;
20
+ export declare const getLanguage: any;
21
+ export default hljs;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Trimmed highlight.js registry.
3
+ *
4
+ * cli-highlight imports the full `highlight.js` package, which registers every
5
+ * language and adds roughly a megabyte of language definitions to the bundle
6
+ * for a TUI that mostly renders JS/TS/JSON/sh/Python. The esbuild plugin in
7
+ * scripts/esbuild-plugins.mjs aliases the bare `highlight.js` import to this
8
+ * module so only the common set below is bundled.
9
+ *
10
+ * Unknown languages still work: cli-highlight throws, marked-terminal catches
11
+ * and renders the fence as plain styled text. `renderMarkdown` adds a short
12
+ * dimmed hint instead of failing or blanking the message.
13
+ */
14
+ import hljs from "highlight.js/lib/core.js";
15
+ import javascript from "highlight.js/lib/languages/javascript.js";
16
+ import typescript from "highlight.js/lib/languages/typescript.js";
17
+ import json from "highlight.js/lib/languages/json.js";
18
+ import bash from "highlight.js/lib/languages/bash.js";
19
+ import python from "highlight.js/lib/languages/python.js";
20
+ import go from "highlight.js/lib/languages/go.js";
21
+ import rust from "highlight.js/lib/languages/rust.js";
22
+ import java from "highlight.js/lib/languages/java.js";
23
+ import c from "highlight.js/lib/languages/c.js";
24
+ import cpp from "highlight.js/lib/languages/cpp.js";
25
+ import csharp from "highlight.js/lib/languages/csharp.js";
26
+ import css from "highlight.js/lib/languages/css.js";
27
+ import xml from "highlight.js/lib/languages/xml.js";
28
+ import yaml from "highlight.js/lib/languages/yaml.js";
29
+ import markdown from "highlight.js/lib/languages/markdown.js";
30
+ import sql from "highlight.js/lib/languages/sql.js";
31
+ import diff from "highlight.js/lib/languages/diff.js";
32
+ import dockerfile from "highlight.js/lib/languages/dockerfile.js";
33
+ import powershell from "highlight.js/lib/languages/powershell.js";
34
+ import kotlin from "highlight.js/lib/languages/kotlin.js";
35
+ import swift from "highlight.js/lib/languages/swift.js";
36
+ import ruby from "highlight.js/lib/languages/ruby.js";
37
+ import php from "highlight.js/lib/languages/php.js";
38
+ import lua from "highlight.js/lib/languages/lua.js";
39
+ import r from "highlight.js/lib/languages/r.js";
40
+ import scala from "highlight.js/lib/languages/scala.js";
41
+ import ini from "highlight.js/lib/languages/ini.js";
42
+ import makefile from "highlight.js/lib/languages/makefile.js";
43
+ import plaintext from "highlight.js/lib/languages/plaintext.js";
44
+ const LANGUAGES = {
45
+ javascript,
46
+ typescript,
47
+ json,
48
+ bash,
49
+ python,
50
+ go,
51
+ rust,
52
+ java,
53
+ c,
54
+ cpp,
55
+ csharp,
56
+ css,
57
+ xml,
58
+ yaml,
59
+ markdown,
60
+ sql,
61
+ diff,
62
+ dockerfile,
63
+ powershell,
64
+ kotlin,
65
+ swift,
66
+ ruby,
67
+ php,
68
+ lua,
69
+ r,
70
+ scala,
71
+ ini,
72
+ makefile,
73
+ plaintext,
74
+ };
75
+ for (const [name, definition] of Object.entries(LANGUAGES))
76
+ hljs.registerLanguage(name, definition);
77
+ export const COMMON_HIGHLIGHT_LANGUAGES = Object.keys(LANGUAGES);
78
+ export function supportsHighlightLanguage(language) {
79
+ if (!language)
80
+ return true;
81
+ const name = language.trim().toLowerCase().split(/\s+/)[0] ?? "";
82
+ return Boolean(name) && Boolean(hljs.getLanguage(name));
83
+ }
84
+ export const highlight = hljs.highlight.bind(hljs);
85
+ export const highlightAuto = hljs.highlightAuto.bind(hljs);
86
+ export const listLanguages = hljs.listLanguages.bind(hljs);
87
+ export const getLanguage = hljs.getLanguage.bind(hljs);
88
+ export default hljs;
@@ -6,8 +6,18 @@
6
6
  * Anthropic's older streaming) handle their own parsing.
7
7
  */
8
8
  export declare function fetchJson(url: string, init?: any): Promise<any>;
9
+ export declare class NetworkStallError extends Error {
10
+ readonly code = "NETWORK_STALL";
11
+ readonly bytesReceived: number;
12
+ constructor(message: string, bytesReceived?: number);
13
+ }
14
+ export interface StreamSseOptions {
15
+ idleTimeoutMs?: number;
16
+ totalTimeoutMs?: number;
17
+ }
9
18
  /** Async iterator over SSE events from a streaming HTTP response. */
10
- export declare function streamSSE(res: any): AsyncGenerator<{
19
+ export declare function streamSSE(res: any, options?: StreamSseOptions): AsyncGenerator<{
11
20
  event: any;
12
21
  data: string;
13
22
  }, void, unknown>;
23
+ export declare function probeConnectivity(url: string, timeoutMs?: number): Promise<boolean>;
package/dist/lib/http.js CHANGED
@@ -16,8 +16,17 @@ export async function fetchJson(url, init = {}) {
16
16
  }
17
17
  return (await res.json());
18
18
  }
19
+ export class NetworkStallError extends Error {
20
+ code = "NETWORK_STALL";
21
+ bytesReceived;
22
+ constructor(message, bytesReceived = 0) {
23
+ super(message);
24
+ this.name = "NetworkStallError";
25
+ this.bytesReceived = bytesReceived;
26
+ }
27
+ }
19
28
  /** Async iterator over SSE events from a streaming HTTP response. */
20
- export async function* streamSSE(res) {
29
+ export async function* streamSSE(res, options = {}) {
21
30
  if (!res.ok) {
22
31
  const body = await res.text().catch(() => "");
23
32
  throw new Error(`${res.status} ${res.statusText}: ${body.slice(0, 500)}`);
@@ -26,19 +35,50 @@ export async function* streamSSE(res) {
26
35
  throw new Error("Empty response body");
27
36
  const reader = res.body.getReader();
28
37
  const decoder = new TextDecoder("utf-8");
38
+ const envIdle = Number(process.env["FASTCODE_SSE_IDLE_MS"]);
39
+ const idleMs = positive(options.idleTimeoutMs, positive(envIdle, 25_000));
40
+ const totalMs = positive(options.totalTimeoutMs, 0);
41
+ const startedAt = Date.now();
42
+ let bytesReceived = 0;
29
43
  let buffer = "";
30
44
  while (true) {
31
- const { value, done } = await reader.read();
45
+ const remaining = totalMs > 0 ? totalMs - (Date.now() - startedAt) : Number.POSITIVE_INFINITY;
46
+ if (remaining <= 0)
47
+ throw new NetworkStallError(`SSE total timeout after ${totalMs}ms`, bytesReceived);
48
+ const boundedByTotal = remaining <= idleMs;
49
+ const waitMs = boundedByTotal ? Math.max(1, remaining) : idleMs;
50
+ let timer;
51
+ const timeout = new Promise((_resolve, reject) => {
52
+ timer = setTimeout(() => {
53
+ reject(new NetworkStallError(boundedByTotal
54
+ ? `SSE total timeout after ${totalMs}ms`
55
+ : `SSE idle timeout after ${idleMs}ms (no data received; set FASTCODE_SSE_IDLE_MS to wait longer)`, bytesReceived));
56
+ }, waitMs);
57
+ });
58
+ let chunk;
59
+ try {
60
+ chunk = await Promise.race([reader.read(), timeout]);
61
+ }
62
+ catch (err) {
63
+ await reader.cancel().catch(() => undefined);
64
+ throw err;
65
+ }
66
+ finally {
67
+ if (timer)
68
+ clearTimeout(timer);
69
+ }
70
+ const { value, done } = chunk;
32
71
  if (done)
33
72
  break;
73
+ bytesReceived += value?.byteLength ?? 0;
34
74
  // Normalize CR/LF so both "\n\n" and "\r\n\r\n" frame separators
35
75
  // are detected, including a CR/LF split across two chunks.
36
76
  buffer = (buffer + decoder.decode(value, { stream: true })).replace(/\r\n?/g, "\n");
37
77
  let sep = buffer.indexOf("\n\n");
38
78
  while (sep !== -1) {
39
- const chunk = buffer.slice(0, sep);
79
+ const chunkText = buffer.slice(0, sep);
40
80
  buffer = buffer.slice(sep + 2);
41
- const ev = parseSseChunk(chunk);
81
+ const ev = parseSseChunk(chunkText);
42
82
  if (ev)
43
83
  yield ev;
44
84
  sep = buffer.indexOf("\n\n");
@@ -52,6 +92,24 @@ export async function* streamSSE(res) {
52
92
  yield ev;
53
93
  }
54
94
  }
95
+ export async function probeConnectivity(url, timeoutMs = 3000) {
96
+ const controller = new AbortController();
97
+ const timer = setTimeout(() => controller.abort(), positive(timeoutMs, 3000));
98
+ try {
99
+ const res = await fetch(url, { signal: controller.signal });
100
+ await res.body?.cancel().catch(() => undefined);
101
+ return res.ok;
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ finally {
107
+ clearTimeout(timer);
108
+ }
109
+ }
110
+ function positive(value, fallback) {
111
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
112
+ }
55
113
  function parseSseChunk(chunk) {
56
114
  let event;
57
115
  const dataLines = [];
@@ -0,0 +1,5 @@
1
+ export declare function cleanupProcesses(): void;
2
+ export interface ProcessGuardOptions {
3
+ onFatal?: (error: unknown) => void;
4
+ }
5
+ export declare function installProcessGuards(options?: ProcessGuardOptions): () => void;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Process-level shutdown guards.
3
+ *
4
+ * The TUI already calls shutdownShells() from its React cleanup, but fatal
5
+ * errors and SIGTERM/SIGHUP can bypass that path. These guards are best-effort
6
+ * and idempotent; they do not turn FastCode into a kernel-level sandbox.
7
+ */
8
+ import { shutdownShells } from "../agent/shell-session.js";
9
+ let installed = false;
10
+ let cleaned = false;
11
+ export function cleanupProcesses() {
12
+ if (cleaned)
13
+ return;
14
+ cleaned = true;
15
+ try {
16
+ shutdownShells();
17
+ }
18
+ catch {
19
+ // Best effort: never mask the original failure.
20
+ }
21
+ }
22
+ export function installProcessGuards(options = {}) {
23
+ if (installed)
24
+ return () => undefined;
25
+ installed = true;
26
+ const onSignal = (signal) => {
27
+ process.stderr.write(`[fastcode] ${signal} received; shutting down.\n`);
28
+ cleanupProcesses();
29
+ process.exit(signal === "SIGINT" ? 130 : 143);
30
+ };
31
+ const onFatal = (error) => {
32
+ const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
33
+ process.stderr.write(`[fastcode] fatal: ${detail}\n`);
34
+ cleanupProcesses();
35
+ options.onFatal?.(error);
36
+ process.exit(1);
37
+ };
38
+ const signals = ["SIGTERM", "SIGHUP"];
39
+ for (const signal of signals) {
40
+ try {
41
+ process.on(signal, onSignal);
42
+ }
43
+ catch {
44
+ // Some platforms do not expose every signal.
45
+ }
46
+ }
47
+ process.on("uncaughtException", onFatal);
48
+ process.on("unhandledRejection", onFatal);
49
+ return () => {
50
+ for (const signal of signals) {
51
+ try {
52
+ process.removeListener(signal, onSignal);
53
+ }
54
+ catch {
55
+ // Ignore unsupported signals.
56
+ }
57
+ }
58
+ process.removeListener("uncaughtException", onFatal);
59
+ process.removeListener("unhandledRejection", onFatal);
60
+ installed = false;
61
+ };
62
+ }
@@ -6,8 +6,10 @@
6
6
  * markdown parser. The result is a fully-formatted ANSI string we can
7
7
  * print directly inside an Ink <Text>.
8
8
  */
9
+ import chalk from "chalk";
9
10
  import { marked } from "marked";
10
11
  import { markedTerminal } from "marked-terminal";
12
+ import { supportsHighlightLanguage } from "./highlight-subset.js";
11
13
  let configured = false;
12
14
  function configure() {
13
15
  if (configured)
@@ -15,16 +17,32 @@ function configure() {
15
17
  marked.use(markedTerminal({
16
18
  reflowText: false,
17
19
  tab: 2,
20
+ // marked-terminal calls this only when highlighting fails; keep the
21
+ // cold-language fallback as plain text instead of colored source.
22
+ code: (text) => text,
18
23
  }));
19
24
  configured = true;
20
25
  }
26
+ function unsupportedFenceLanguages(md) {
27
+ const missing = new Set();
28
+ for (const match of md.matchAll(/(?:^|\n)\s*(?:`{3,}|~{3,})([^\n]*)/g)) {
29
+ const info = (match[1] ?? "").trim();
30
+ if (!info)
31
+ continue;
32
+ const language = info.split(/\s+/)[0] ?? "";
33
+ if (language && !supportsHighlightLanguage(language))
34
+ missing.add(language);
35
+ }
36
+ return Array.from(missing);
37
+ }
21
38
  export function renderMarkdown(md) {
22
39
  configure();
23
40
  try {
24
- const out = marked.parse(md);
25
- // marked-terminal sometimes appends a stray trailing newline; trim only
26
- // trailing whitespace so we don't lose meaningful blank lines.
27
- return out.replace(/\s+$/, "");
41
+ const out = marked.parse(md).replace(/\s+$/, "");
42
+ const missing = unsupportedFenceLanguages(String(md));
43
+ if (missing.length === 0)
44
+ return out;
45
+ return out + "\n\n" + chalk.dim(`[fastcode] no syntax highlighting for ${missing.join(", ")}; rendered as plain text.`);
28
46
  }
29
47
  catch {
30
48
  return md;
@@ -0,0 +1,19 @@
1
+ export interface ModelMeta {
2
+ contextWindow: number;
3
+ supportsTools: boolean;
4
+ charsPerToken: number;
5
+ maxOutputTokens?: number;
6
+ }
7
+ export interface EstTokenOptions {
8
+ providerId?: string;
9
+ model?: string;
10
+ charsPerToken?: number;
11
+ }
12
+ export declare const UNKNOWN_MODEL_META: ModelMeta;
13
+ export declare function modelMetaOverridePath(): string;
14
+ export declare function clearModelMetaCache(): void;
15
+ export declare function getModelMeta(providerId?: string, model?: string): ModelMeta;
16
+ export declare function estTokens(text: string, options?: number | EstTokenOptions): number;
17
+ export declare function estMessagesTokens(messages: Array<{
18
+ content: string;
19
+ }>, options?: EstTokenOptions): number;
@@ -0,0 +1,113 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ export const UNKNOWN_MODEL_META = {
5
+ contextWindow: 32_768,
6
+ supportsTools: false,
7
+ charsPerToken: 2.5,
8
+ };
9
+ const PROVIDER_META = {
10
+ anthropic: { contextWindow: 1_000_000, supportsTools: true, charsPerToken: 3.8, maxOutputTokens: 64_000 },
11
+ openai: { contextWindow: 400_000, supportsTools: true, charsPerToken: 3.8, maxOutputTokens: 32_000 },
12
+ google: { contextWindow: 1_048_576, supportsTools: true, charsPerToken: 3.6, maxOutputTokens: 65_536 },
13
+ deepseek: { contextWindow: 128_000, supportsTools: true, charsPerToken: 1.7, maxOutputTokens: 8_192 },
14
+ openrouter: { contextWindow: 128_000, supportsTools: true, charsPerToken: 2.5, maxOutputTokens: 16_384 },
15
+ groq: { contextWindow: 128_000, supportsTools: true, charsPerToken: 3.8, maxOutputTokens: 8_192 },
16
+ mistral: { contextWindow: 128_000, supportsTools: true, charsPerToken: 3.8, maxOutputTokens: 8_192 },
17
+ xai: { contextWindow: 256_000, supportsTools: true, charsPerToken: 3.8, maxOutputTokens: 16_384 },
18
+ moonshot: { contextWindow: 256_000, supportsTools: true, charsPerToken: 2.0, maxOutputTokens: 16_384 },
19
+ zhipu: { contextWindow: 128_000, supportsTools: true, charsPerToken: 1.7, maxOutputTokens: 8_192 },
20
+ qwen: { contextWindow: 128_000, supportsTools: true, charsPerToken: 1.7, maxOutputTokens: 8_192 },
21
+ ollama: { contextWindow: 32_768, supportsTools: true, charsPerToken: 2.5, maxOutputTokens: 4_096 },
22
+ custom: { contextWindow: 32_768, supportsTools: false, charsPerToken: 2.5 },
23
+ };
24
+ const MODEL_OVERRIDES = {
25
+ "claude-haiku-5.5": { contextWindow: 200_000 },
26
+ "claude-haiku-4.5": { contextWindow: 200_000 },
27
+ "claude-sonnet-5": { contextWindow: 1_000_000 },
28
+ "claude-opus-5": { contextWindow: 1_000_000 },
29
+ "claude-sonnet-4.6": { contextWindow: 1_000_000 },
30
+ "claude-opus-4.6": { contextWindow: 1_000_000 },
31
+ "gpt-4o": { contextWindow: 128_000 },
32
+ "gpt-4o-mini": { contextWindow: 128_000 },
33
+ "gpt-4.1": { contextWindow: 1_000_000 },
34
+ o3: { contextWindow: 200_000 },
35
+ "o4-mini": { contextWindow: 200_000 },
36
+ "gemini-3.5-flash-lite": { contextWindow: 1_048_576 },
37
+ "gemini-3.1-flash-lite": { contextWindow: 1_048_576 },
38
+ "deepseek-v4-pro": { contextWindow: 128_000, charsPerToken: 1.7 },
39
+ "deepseek-flash": { contextWindow: 128_000, charsPerToken: 1.7 },
40
+ "glm-5.3": { contextWindow: 128_000, charsPerToken: 1.7 },
41
+ "glm-5.2": { contextWindow: 128_000, charsPerToken: 1.7 },
42
+ "qwen3.8-max": { contextWindow: 128_000, charsPerToken: 1.7 },
43
+ "qwen3.8-flash": { contextWindow: 128_000, charsPerToken: 1.7 },
44
+ "kimi-k3": { contextWindow: 256_000, charsPerToken: 2.0 },
45
+ "grok-4.7": { contextWindow: 256_000, charsPerToken: 3.8 },
46
+ "mistral-large-latest": { contextWindow: 128_000, charsPerToken: 3.8 },
47
+ };
48
+ let overrideCache;
49
+ export function modelMetaOverridePath() {
50
+ const root = process.env["FASTCODE_HOME"] ?? join(homedir(), ".fastcode");
51
+ return join(root, "model-meta.json");
52
+ }
53
+ export function clearModelMetaCache() {
54
+ overrideCache = undefined;
55
+ }
56
+ function loadOverrides() {
57
+ if (overrideCache)
58
+ return overrideCache;
59
+ const file = modelMetaOverridePath();
60
+ if (!existsSync(file)) {
61
+ overrideCache = {};
62
+ return overrideCache;
63
+ }
64
+ try {
65
+ const raw = JSON.parse(readFileSync(file, "utf8"));
66
+ overrideCache = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
67
+ }
68
+ catch {
69
+ overrideCache = {};
70
+ }
71
+ return overrideCache;
72
+ }
73
+ function usable(meta) {
74
+ return Boolean(meta) && typeof meta === "object";
75
+ }
76
+ function apply(base, ...layers) {
77
+ const next = { ...base };
78
+ for (const layer of layers) {
79
+ if (!usable(layer))
80
+ continue;
81
+ if (typeof layer.contextWindow === "number" && layer.contextWindow > 0)
82
+ next.contextWindow = Math.floor(layer.contextWindow);
83
+ if (typeof layer.charsPerToken === "number" && layer.charsPerToken > 0)
84
+ next.charsPerToken = layer.charsPerToken;
85
+ if (typeof layer.supportsTools === "boolean")
86
+ next.supportsTools = layer.supportsTools;
87
+ if (typeof layer.maxOutputTokens === "number" && layer.maxOutputTokens > 0)
88
+ next.maxOutputTokens = Math.floor(layer.maxOutputTokens);
89
+ }
90
+ return next;
91
+ }
92
+ export function getModelMeta(providerId, model) {
93
+ const provider = providerId && PROVIDER_META[providerId] ? PROVIDER_META[providerId] : UNKNOWN_MODEL_META;
94
+ const overrides = loadOverrides();
95
+ const keys = [
96
+ providerId && model ? `${providerId}/${model}` : undefined,
97
+ providerId && model ? `${providerId}::${model}` : undefined,
98
+ model,
99
+ ].filter((key) => Boolean(key));
100
+ const layers = keys.map((key) => overrides[key]);
101
+ return apply(provider, model ? MODEL_OVERRIDES[model] : undefined, ...layers);
102
+ }
103
+ export function estTokens(text, options = {}) {
104
+ if (!text)
105
+ return 0;
106
+ const ratio = typeof options === "number"
107
+ ? options
108
+ : options.charsPerToken ?? getModelMeta(options.providerId, options.model).charsPerToken;
109
+ return Math.ceil(text.length / Math.max(0.1, ratio));
110
+ }
111
+ export function estMessagesTokens(messages, options = {}) {
112
+ return messages.reduce((sum, message) => sum + estTokens(message.content ?? "", options), 0);
113
+ }