@datename/fastcode-termux 0.0.0-stage → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +400 -2
  3. package/bin/fastcode.mjs +59 -0
  4. package/dist/agent/guards.d.ts +35 -0
  5. package/dist/agent/guards.js +74 -0
  6. package/dist/agent/limits.d.ts +24 -0
  7. package/dist/agent/limits.js +71 -0
  8. package/dist/agent/protocol.d.ts +4 -0
  9. package/dist/agent/protocol.js +126 -0
  10. package/dist/agent/tools.d.ts +1 -0
  11. package/dist/agent/tools.js +377 -0
  12. package/dist/app.d.ts +4 -0
  13. package/dist/app.js +476 -0
  14. package/dist/cli.d.ts +1 -0
  15. package/dist/cli.js +518 -0
  16. package/dist/commands/about.d.ts +8 -0
  17. package/dist/commands/about.js +16 -0
  18. package/dist/commands/agent.d.ts +11 -0
  19. package/dist/commands/agent.js +18 -0
  20. package/dist/commands/clear.d.ts +11 -0
  21. package/dist/commands/clear.js +9 -0
  22. package/dist/commands/config.d.ts +7 -0
  23. package/dist/commands/config.js +24 -0
  24. package/dist/commands/cost.d.ts +9 -0
  25. package/dist/commands/cost.js +13 -0
  26. package/dist/commands/cwd.d.ts +7 -0
  27. package/dist/commands/cwd.js +7 -0
  28. package/dist/commands/exit.d.ts +9 -0
  29. package/dist/commands/exit.js +8 -0
  30. package/dist/commands/help.d.ts +6 -0
  31. package/dist/commands/help.js +18 -0
  32. package/dist/commands/index.d.ts +9 -0
  33. package/dist/commands/index.js +58 -0
  34. package/dist/commands/login.d.ts +16 -0
  35. package/dist/commands/login.js +61 -0
  36. package/dist/commands/logout.d.ts +9 -0
  37. package/dist/commands/logout.js +53 -0
  38. package/dist/commands/model.d.ts +11 -0
  39. package/dist/commands/model.js +30 -0
  40. package/dist/commands/plugin.d.ts +11 -0
  41. package/dist/commands/plugin.js +74 -0
  42. package/dist/commands/provider.d.ts +11 -0
  43. package/dist/commands/provider.js +28 -0
  44. package/dist/commands/reset.d.ts +9 -0
  45. package/dist/commands/reset.js +13 -0
  46. package/dist/commands/skills.d.ts +11 -0
  47. package/dist/commands/skills.js +106 -0
  48. package/dist/commands/system.d.ts +11 -0
  49. package/dist/commands/system.js +20 -0
  50. package/dist/commands/theme.d.ts +11 -0
  51. package/dist/commands/theme.js +22 -0
  52. package/dist/commands/tools.d.ts +7 -0
  53. package/dist/commands/tools.js +29 -0
  54. package/dist/commands/whoami.d.ts +9 -0
  55. package/dist/commands/whoami.js +51 -0
  56. package/dist/config/paths.d.ts +4 -0
  57. package/dist/config/paths.js +59 -0
  58. package/dist/config/store.d.ts +69 -0
  59. package/dist/config/store.js +208 -0
  60. package/dist/config/themes.d.ts +73 -0
  61. package/dist/config/themes.js +77 -0
  62. package/dist/config/trust.d.ts +9 -0
  63. package/dist/config/trust.js +67 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +31 -0
  66. package/dist/lib/diagnostics.d.ts +41 -0
  67. package/dist/lib/diagnostics.js +139 -0
  68. package/dist/lib/http.d.ts +13 -0
  69. package/dist/lib/http.js +70 -0
  70. package/dist/lib/markdown.d.ts +1 -0
  71. package/dist/lib/markdown.js +32 -0
  72. package/dist/lib/pkg.d.ts +5 -0
  73. package/dist/lib/pkg.js +27 -0
  74. package/dist/lib/platform.d.ts +7 -0
  75. package/dist/lib/platform.js +57 -0
  76. package/dist/lib/startup.d.ts +6 -0
  77. package/dist/lib/startup.js +25 -0
  78. package/dist/lib/uiMode.d.ts +2 -0
  79. package/dist/lib/uiMode.js +7 -0
  80. package/dist/plugins/manager.d.ts +23 -0
  81. package/dist/plugins/manager.js +101 -0
  82. package/dist/providers/anthropic.d.ts +21 -0
  83. package/dist/providers/anthropic.js +107 -0
  84. package/dist/providers/custom.d.ts +24 -0
  85. package/dist/providers/custom.js +25 -0
  86. package/dist/providers/deepseek.d.ts +24 -0
  87. package/dist/providers/deepseek.js +15 -0
  88. package/dist/providers/google.d.ts +24 -0
  89. package/dist/providers/google.js +19 -0
  90. package/dist/providers/groq.d.ts +24 -0
  91. package/dist/providers/groq.js +20 -0
  92. package/dist/providers/index.d.ts +201 -0
  93. package/dist/providers/index.js +37 -0
  94. package/dist/providers/mistral.d.ts +24 -0
  95. package/dist/providers/mistral.js +19 -0
  96. package/dist/providers/moonshot.d.ts +24 -0
  97. package/dist/providers/moonshot.js +22 -0
  98. package/dist/providers/ollama.d.ts +24 -0
  99. package/dist/providers/ollama.js +22 -0
  100. package/dist/providers/openai.d.ts +24 -0
  101. package/dist/providers/openai.js +25 -0
  102. package/dist/providers/openaiCompatible.d.ts +17 -0
  103. package/dist/providers/openaiCompatible.js +110 -0
  104. package/dist/providers/openrouter.d.ts +24 -0
  105. package/dist/providers/openrouter.js +26 -0
  106. package/dist/providers/qwen.d.ts +24 -0
  107. package/dist/providers/qwen.js +23 -0
  108. package/dist/providers/xai.d.ts +24 -0
  109. package/dist/providers/xai.js +18 -0
  110. package/dist/providers/zhipu.d.ts +24 -0
  111. package/dist/providers/zhipu.js +23 -0
  112. package/dist/session/log.d.ts +29 -0
  113. package/dist/session/log.js +135 -0
  114. package/dist/skills/manager.d.ts +18 -0
  115. package/dist/skills/manager.js +201 -0
  116. package/dist/skills/parser.d.ts +18 -0
  117. package/dist/skills/parser.js +65 -0
  118. package/dist/types.d.ts +146 -0
  119. package/dist/types.js +1 -0
  120. package/dist/ui/Banner.d.ts +5 -0
  121. package/dist/ui/Banner.js +65 -0
  122. package/dist/ui/Message.d.ts +7 -0
  123. package/dist/ui/Message.js +13 -0
  124. package/dist/ui/Prompt.d.ts +10 -0
  125. package/dist/ui/Prompt.js +38 -0
  126. package/dist/ui/Spinner.d.ts +5 -0
  127. package/dist/ui/Spinner.js +33 -0
  128. package/dist/ui/StatusBar.d.ts +10 -0
  129. package/dist/ui/StatusBar.js +19 -0
  130. package/dist/ui/TrustGate.d.ts +5 -0
  131. package/dist/ui/TrustGate.js +26 -0
  132. package/package.json +96 -4
  133. package/src/agent/guards.ts +74 -0
  134. package/src/agent/limits.ts +71 -0
  135. package/src/agent/protocol.ts +126 -0
  136. package/src/agent/tools.ts +377 -0
  137. package/src/app.ts +476 -0
  138. package/src/cli.ts +308 -0
  139. package/src/commands/about.ts +16 -0
  140. package/src/commands/agent.ts +18 -0
  141. package/src/commands/clear.ts +9 -0
  142. package/src/commands/config.ts +24 -0
  143. package/src/commands/cost.ts +13 -0
  144. package/src/commands/cwd.ts +7 -0
  145. package/src/commands/exit.ts +8 -0
  146. package/src/commands/help.ts +18 -0
  147. package/src/commands/index.ts +58 -0
  148. package/src/commands/login.ts +61 -0
  149. package/src/commands/logout.ts +53 -0
  150. package/src/commands/model.ts +30 -0
  151. package/src/commands/plugin.ts +74 -0
  152. package/src/commands/provider.ts +28 -0
  153. package/src/commands/reset.ts +13 -0
  154. package/src/commands/skills.ts +106 -0
  155. package/src/commands/system.ts +20 -0
  156. package/src/commands/theme.ts +22 -0
  157. package/src/commands/tools.ts +29 -0
  158. package/src/commands/whoami.ts +51 -0
  159. package/src/config/paths.ts +59 -0
  160. package/src/config/store.ts +240 -0
  161. package/src/config/themes.ts +77 -0
  162. package/src/config/trust.ts +67 -0
  163. package/src/index.ts +31 -0
  164. package/src/lib/diagnostics.ts +139 -0
  165. package/src/lib/http.ts +70 -0
  166. package/src/lib/markdown.ts +32 -0
  167. package/src/lib/pkg.ts +35 -0
  168. package/src/lib/platform.ts +57 -0
  169. package/src/lib/startup.ts +25 -0
  170. package/src/lib/uiMode.ts +7 -0
  171. package/src/plugins/manager.ts +101 -0
  172. package/src/providers/anthropic.ts +107 -0
  173. package/src/providers/custom.ts +25 -0
  174. package/src/providers/deepseek.ts +15 -0
  175. package/src/providers/google.ts +19 -0
  176. package/src/providers/groq.ts +20 -0
  177. package/src/providers/index.ts +37 -0
  178. package/src/providers/mistral.ts +19 -0
  179. package/src/providers/moonshot.ts +22 -0
  180. package/src/providers/ollama.ts +22 -0
  181. package/src/providers/openai.ts +25 -0
  182. package/src/providers/openaiCompatible.ts +110 -0
  183. package/src/providers/openrouter.ts +26 -0
  184. package/src/providers/qwen.ts +23 -0
  185. package/src/providers/xai.ts +18 -0
  186. package/src/providers/zhipu.ts +23 -0
  187. package/src/session/log.ts +135 -0
  188. package/src/skills/manager.ts +201 -0
  189. package/src/skills/parser.ts +65 -0
  190. package/src/types.ts +143 -0
  191. package/src/ui/Banner.ts +68 -0
  192. package/src/ui/Message.ts +13 -0
  193. package/src/ui/Prompt.ts +38 -0
  194. package/src/ui/Spinner.ts +33 -0
  195. package/src/ui/StatusBar.ts +19 -0
  196. package/src/ui/TrustGate.ts +26 -0
  197. package/tsconfig.json +22 -0
@@ -0,0 +1,10 @@
1
+ export declare const Prompt: ({ theme, value, onChange, onSubmit, disabled, placeholder, stable, mask, }: {
2
+ theme: any;
3
+ value: any;
4
+ onChange: any;
5
+ onSubmit: any;
6
+ disabled: any;
7
+ placeholder: any;
8
+ stable?: boolean;
9
+ mask: any;
10
+ }) => import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,38 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Bottom prompt input, with a left-side caret + slash-command autocomplete.
4
+ *
5
+ * We re-use ink-text-input for low-level keyboard handling but wrap it with
6
+ * a fixed caret indicator + autocomplete preview to mirror the look of
7
+ * Claude Code's prompt.
8
+ *
9
+ * The caret used to blink on a 500 ms setInterval. That timer re-rendered the
10
+ * whole input line twice a second, which on some terminals rewrote the line
11
+ * while the text cursor was being re-positioned and made the caret occasionally
12
+ * jump one column ahead after the first keystroke. A static caret removes the
13
+ * timer-driven redraw; ink-text-input's own cursor still tracks the real caret.
14
+ */
15
+ import { Box, Text } from "ink";
16
+ import TextInput from "ink-text-input";
17
+ import { COMMANDS } from "../commands/index.js";
18
+ export const Prompt = ({ theme, value, onChange, onSubmit, disabled, placeholder, stable = false, mask, }) => {
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] }));
21
+ };
22
+ function autocomplete(value) {
23
+ if (!value.startsWith("/") || value.includes(" "))
24
+ return undefined;
25
+ const partial = value.slice(1).toLowerCase();
26
+ if (!partial) {
27
+ return COMMANDS.filter((c) => !c.hidden)
28
+ .map((c) => `/${c.name}`)
29
+ .slice(0, 6)
30
+ .join(" ");
31
+ }
32
+ const matches = COMMANDS.filter((c) => !c.hidden &&
33
+ (c.name.startsWith(partial) ||
34
+ (c.aliases ?? []).some((a) => a.startsWith(partial))));
35
+ if (matches.length === 0)
36
+ return undefined;
37
+ return matches.map((c) => `/${c.name} — ${c.description}`).slice(0, 4).join("\n ");
38
+ }
@@ -0,0 +1,5 @@
1
+ export declare const Spinner: ({ theme, label, stable }: {
2
+ theme: any;
3
+ label: any;
4
+ stable?: boolean;
5
+ }) => import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,33 @@
1
+ import { jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Custom thinking spinner.
4
+ *
5
+ * We don't use ink-spinner's built-in spinners directly because we want to
6
+ * (1) cycle through a varied set of action verbs ("Thinking…", "Pondering…")
7
+ * and (2) keep the dots animation in sync with the verb rotation.
8
+ */
9
+ import { useEffect, useState } from "react";
10
+ import { Text } from "ink";
11
+ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
12
+ const VERBS = [
13
+ "Thinking",
14
+ "Pondering",
15
+ "Reasoning",
16
+ "Cooking",
17
+ "Crafting",
18
+ "Brewing",
19
+ "Sketching",
20
+ "Charging",
21
+ ];
22
+ export const Spinner = ({ theme, label, stable = false }) => {
23
+ const [tick, setTick] = useState(0);
24
+ useEffect(() => {
25
+ if (stable)
26
+ return;
27
+ const id = setInterval(() => setTick((t) => t + 1), 80);
28
+ return () => clearInterval(id);
29
+ }, [stable]);
30
+ const frame = stable ? "⠋" : FRAMES[tick % FRAMES.length];
31
+ const verb = label ?? VERBS[Math.floor(tick / 24) % VERBS.length];
32
+ return (_jsxs(Text, { color: theme.accent, children: [frame, " ", _jsxs(Text, { color: theme.muted, children: [verb, "\u2026"] })] }));
33
+ };
@@ -0,0 +1,10 @@
1
+ export declare const StatusBar: ({ theme, providerId, model, inputTokens, outputTokens, costUsd, enabledSkillCount, hint, }: {
2
+ theme: any;
3
+ providerId: any;
4
+ model: any;
5
+ inputTokens: any;
6
+ outputTokens: any;
7
+ costUsd: any;
8
+ enabledSkillCount: any;
9
+ hint: any;
10
+ }) => import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useStdout } from "ink";
3
+ import { platformLabel } from "../lib/platform.js";
4
+ export const StatusBar = ({ theme, providerId, model, inputTokens, outputTokens, costUsd, enabledSkillCount, hint, }) => {
5
+ const { stdout } = useStdout();
6
+ const columns = stdout?.columns ?? process.stdout.columns ?? 80;
7
+ const compact = columns < 88;
8
+ const cost = costUsd > 0 ? `$${costUsd.toFixed(4)}` : "$0.0000";
9
+ const providerModel = `${providerId}/${model}`;
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" })] }));
14
+ };
15
+ function truncate(value, max) {
16
+ if (value.length <= max)
17
+ return value;
18
+ return `${value.slice(0, Math.max(1, max - 1))}…`;
19
+ }
@@ -0,0 +1,5 @@
1
+ export declare const TrustGate: ({ theme, cwd, onDecision }: {
2
+ theme: any;
3
+ cwd: any;
4
+ onDecision: any;
5
+ }) => import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * First-run confirmation for a folder the agent is not trusted with yet.
4
+ *
5
+ * Agent tools can read, write, delete and execute in the workspace, so the user
6
+ * decides before anything loads.
7
+ */
8
+ import { Box, Text, useInput } from "ink";
9
+ export const TrustGate = ({ theme, cwd, onDecision }) => {
10
+ useInput((input) => {
11
+ const value = String(input ?? "").toLowerCase();
12
+ if (value === "y")
13
+ onDecision(true);
14
+ else if (value === "n" || value === "q")
15
+ onDecision(false);
16
+ });
17
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, marginTop: 1, children: [
18
+ _jsx(Text, { color: theme.accent, bold: true, children: "Trust this folder?" }),
19
+ _jsx(Text, { color: theme.muted, children: cwd }),
20
+ _jsx(Text, { children: "" }),
21
+ _jsx(Text, { children: "FastCode agent tools can read, write and delete files here, and run shell commands." }),
22
+ _jsx(Text, { children: "Continue only if you trust this project." }),
23
+ _jsx(Text, { children: "" }),
24
+ _jsx(Text, { children: "[y] trust and continue [n] decline (chat only) [q] quit" }),
25
+ ] }));
26
+ };
package/package.json CHANGED
@@ -1,6 +1,98 @@
1
1
  {
2
2
  "name": "@datename/fastcode-termux",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.6",
4
+ "description": "FastCode — a blazing-fast TUI coding assistant. Multi-LLM, skills.sh skills, slash commands. Works on Termux, Linux, Windows, and macOS.",
5
+ "type": "module",
6
+ "bin": {
7
+ "fastcode": "bin/fastcode.mjs",
8
+ "fc": "bin/fastcode.mjs"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "files": [
12
+ "bin",
13
+ "dist",
14
+ "src",
15
+ "tsconfig.json",
16
+ "assets",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "engines": {
21
+ "node": ">=18.17"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc -p tsconfig.json && node scripts/build.mjs",
25
+ "build:bundle": "node scripts/build.mjs",
26
+ "dev": "tsx src/index.ts",
27
+ "start": "node bin/fastcode.mjs",
28
+ "check": "npm test && npm run typecheck && npm run lint && npm run build",
29
+ "lint": "eslint \"src/**/*.{ts,tsx}\"",
30
+ "typecheck": "tsc -p tsconfig.json --noEmit",
31
+ "test": "node ../scripts/smoke-test.mjs",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "keywords": [
35
+ "tui",
36
+ "cli",
37
+ "ai",
38
+ "coding-agent",
39
+ "claude",
40
+ "claude-code",
41
+ "openclaude",
42
+ "fastcode",
43
+ "skills",
44
+ "termux",
45
+ "openai",
46
+ "anthropic",
47
+ "gemini",
48
+ "openrouter",
49
+ "ollama",
50
+ "deepseek",
51
+ "groq",
52
+ "mistral",
53
+ "agent",
54
+ "ink",
55
+ "react"
56
+ ],
57
+ "author": "SimpleLittleDev",
58
+ "license": "MIT",
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "git+https://github.com/dateName/fastcode-termux.git"
62
+ },
63
+ "homepage": "https://github.com/dateName/fastcode-termux#readme",
64
+ "bugs": {
65
+ "url": "https://github.com/dateName/fastcode-termux/issues"
66
+ },
67
+ "dependencies": {
68
+ "boxen": "^7.1.1",
69
+ "chalk": "^5.3.0",
70
+ "cli-highlight": "^2.1.11",
71
+ "figures": "^6.1.0",
72
+ "gradient-string": "^2.0.2",
73
+ "ink": "^5.0.1",
74
+ "ink-big-text": "^2.0.0",
75
+ "ink-gradient": "^3.0.0",
76
+ "ink-link": "^4.1.0",
77
+ "ink-markdown": "^1.0.1",
78
+ "ink-select-input": "^6.0.0",
79
+ "ink-spinner": "^5.0.0",
80
+ "ink-text-input": "^6.0.0",
81
+ "marked": "^12.0.2",
82
+ "marked-terminal": "^7.1.0",
83
+ "meow": "^13.2.0",
84
+ "react": "^18.3.1",
85
+ "tar": "^7.4.3"
86
+ },
87
+ "devDependencies": {
88
+ "@types/marked-terminal": "^6.1.1",
89
+ "@types/node": "^22.10.0",
90
+ "@types/react": "^18.3.12",
91
+ "@typescript-eslint/eslint-plugin": "^8.18.0",
92
+ "@typescript-eslint/parser": "^8.18.0",
93
+ "esbuild": "^0.28.2",
94
+ "eslint": "^9.16.0",
95
+ "tsx": "^4.19.2",
96
+ "typescript": "^5.6.3"
97
+ }
98
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Dangerous-command guard.
3
+ *
4
+ * Blocks destructive shell patterns before execution. Word-boundary escapes
5
+ * are written as character classes so the intent is identical without
6
+ * boundary syntax.
7
+ *
8
+ * Overrides:
9
+ * FASTCODE_ALLOW_DANGEROUS=1 bypass the deny list (reported in output)
10
+ * FASTCODE_SHELL_ALLOWLIST=a;b only commands matching these regexes run
11
+ * FASTCODE_SHELL_DENY_EXTRA=x;y extra regexes to deny
12
+ * FASTCODE_RESTRICT_TO_WORKSPACE=1 also reject ../ and .. escapes
13
+ */
14
+ export const DEFAULT_DENY_PATTERNS = [
15
+ "(^|[^a-z])rm +-[rf]{1,2}([^a-z]|$)",
16
+ "(^|[^a-z])del +/[fq]([^a-z]|$)",
17
+ "(^|[^a-z])rmdir +/s([^a-z]|$)",
18
+ "(^|[^a-z-])(format|mkfs|diskpart)([^a-z]|$)",
19
+ "(^|[^a-z])dd +if=",
20
+ "> */dev/sd",
21
+ "(^|[^a-z])(shutdown|reboot|poweroff)([^a-z]|$)",
22
+ ];
23
+ export const FORK_BOMB_MARKERS = [":(){", ":|:&"];
24
+ function enabled(name) {
25
+ const value = process.env[name];
26
+ if (value === undefined)
27
+ return false;
28
+ return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
29
+ }
30
+ function splitList(value) {
31
+ return (value ?? "").split(/[;,]/).map((entry) => entry.trim()).filter(Boolean);
32
+ }
33
+ function matches(lower, pattern) {
34
+ try {
35
+ return new RegExp(pattern).test(lower);
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ /**
42
+ * Decide whether a shell command may run.
43
+ * Returns { allow: true } or { allow: false, output: explanation }.
44
+ */
45
+ export function evaluateShellCommand(command) {
46
+ const cmd = String(command ?? "").trim();
47
+ const lower = cmd.toLowerCase();
48
+ if (enabled("FASTCODE_ALLOW_DANGEROUS"))
49
+ return { allow: true, output: undefined, bypassed: true };
50
+ const deny = DEFAULT_DENY_PATTERNS.concat(splitList(process.env["FASTCODE_SHELL_DENY_EXTRA"]));
51
+ for (const pattern of deny) {
52
+ if (matches(lower, pattern)) {
53
+ return {
54
+ allow: false,
55
+ pattern,
56
+ output: "Error: blocked by the safety guard (dangerous pattern: " + pattern + "). " +
57
+ "Set FASTCODE_ALLOW_DANGEROUS=1 to override.",
58
+ };
59
+ }
60
+ }
61
+ for (const marker of FORK_BOMB_MARKERS) {
62
+ if (lower.includes(marker))
63
+ return { allow: false, output: "Error: blocked by the safety guard (fork bomb pattern)." };
64
+ }
65
+ const allowlist = splitList(process.env["FASTCODE_SHELL_ALLOWLIST"]);
66
+ if (allowlist.length > 0 && !allowlist.some((pattern) => matches(lower, pattern)))
67
+ return { allow: false, output: "Error: blocked by the safety guard (not in FASTCODE_SHELL_ALLOWLIST)." };
68
+ if (enabled("FASTCODE_RESTRICT_TO_WORKSPACE")) {
69
+ const backslash = String.fromCharCode(92);
70
+ if (cmd.includes("../") || cmd.includes(".." + backslash))
71
+ return { allow: false, output: "Error: blocked by the safety guard (path traversal)." };
72
+ }
73
+ return { allow: true, output: undefined };
74
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Single source of truth for how much tool output reaches the model.
3
+ *
4
+ * Output is bounded while it is produced, not by slicing a finished string
5
+ * afterwards, so a runaway command or a huge directory listing cannot grow
6
+ * without limit. Override the cap with FASTCODE_TOOL_OUTPUT_LIMIT (characters).
7
+ */
8
+ export const DEFAULT_TOOL_OUTPUT_LIMIT = 30000;
9
+ export function toolOutputLimit() {
10
+ const raw = Number(process.env["FASTCODE_TOOL_OUTPUT_LIMIT"]);
11
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_TOOL_OUTPUT_LIMIT;
12
+ }
13
+ /**
14
+ * Bounded text accumulator.
15
+ *
16
+ * mode "head" keeps the first N characters (listings, search hits);
17
+ * mode "tail" keeps the last N (command logs, where the error is last).
18
+ * trimEnd()/toString() mimic a plain string so callers stay simple.
19
+ */
20
+ export function createOutputBuffer(limit = toolOutputLimit(), mode = "head") {
21
+ const NL = String.fromCharCode(10);
22
+ let text = "";
23
+ let dropped = 0;
24
+ const buffer = {
25
+ get full() {
26
+ return text.length >= limit;
27
+ },
28
+ push(chunk) {
29
+ const value = String(chunk);
30
+ if (mode === "tail") {
31
+ text += value;
32
+ if (text.length > limit) {
33
+ const over = text.length - limit;
34
+ dropped += over;
35
+ text = text.slice(over);
36
+ }
37
+ return;
38
+ }
39
+ if (text.length >= limit) {
40
+ dropped += value.length;
41
+ return;
42
+ }
43
+ const room = limit - text.length;
44
+ if (value.length <= room) {
45
+ text += value;
46
+ }
47
+ else {
48
+ text += value.slice(0, room);
49
+ dropped += value.length - room;
50
+ }
51
+ },
52
+ line(value) {
53
+ if (text.length > 0)
54
+ buffer.push(NL);
55
+ buffer.push(value);
56
+ },
57
+ value() {
58
+ if (dropped <= 0)
59
+ return text;
60
+ const note = "[" + dropped + " more characters omitted]";
61
+ return mode === "tail" ? note + NL + text : text + NL + note;
62
+ },
63
+ trimEnd() {
64
+ return buffer.value().trimEnd();
65
+ },
66
+ toString() {
67
+ return buffer.value();
68
+ },
69
+ };
70
+ return buffer;
71
+ }
@@ -0,0 +1,126 @@
1
+ const TOOL_RE = /<fastcode_tool>([\s\S]*?)<\/fastcode_tool>/g;
2
+ // Some models (notably DeepSeek) emit their native "DSML" tool-call markup
3
+ // instead of <fastcode_tool>. The markers look like `<|DSML|invoke ...>`
4
+ // (U+FF5C fullwidth bar, sometimes doubled or spaced). We canonicalize those
5
+ // markers back to plain angle-bracket tags and then parse the
6
+ // calls -> invoke -> parameter structure as a second dialect.
7
+ const DSML_OPEN_RE = /<\s*(?:[||]\s*)+DSML\s*(?:[||]\s*)+/gi;
8
+ const DSML_CLOSE_RE = /<\/\s*(?:[||]\s*)+DSML\s*(?:[||]\s*)+/gi;
9
+ const DSML_BLOCK_RE = /<([a-z_]*calls?)>([\s\S]*?)<\/\1>/gi;
10
+ const DSML_INVOKE_RE = /<invoke\s+name="([^"]+)"\s*>([\s\S]*?)<\/invoke>/gi;
11
+ const DSML_PARAM_RE = /<parameter\s+name="([^"]+)"([^>]*)>([\s\S]*?)<\/parameter>/gi;
12
+ /** Turn the DSML glyph markers into ordinary `<tag>` / `</tag>` tags. */
13
+ function canonicalizeDsml(text) {
14
+ return text.replace(DSML_CLOSE_RE, "</").replace(DSML_OPEN_RE, "<");
15
+ }
16
+ function coerceDsmlValue(attrs, raw) {
17
+ const value = raw.replace(/^\n/, "").replace(/\n$/, "");
18
+ if (/\bstring\s*=\s*"true"/i.test(attrs))
19
+ return value;
20
+ const trimmed = value.trim();
21
+ if (trimmed === "")
22
+ return "";
23
+ try {
24
+ return JSON.parse(trimmed);
25
+ }
26
+ catch {
27
+ if (trimmed === "true")
28
+ return true;
29
+ if (trimmed === "false")
30
+ return false;
31
+ const n = Number(trimmed);
32
+ return Number.isNaN(n) ? value : n;
33
+ }
34
+ }
35
+ function parseDsmlCalls(text) {
36
+ const canonical = canonicalizeDsml(text);
37
+ const bodies = [];
38
+ for (const block of canonical.matchAll(DSML_BLOCK_RE))
39
+ bodies.push(block[2] ?? "");
40
+ if (bodies.length === 0)
41
+ bodies.push(canonical);
42
+ const calls = [];
43
+ for (const body of bodies) {
44
+ for (const invoke of body.matchAll(DSML_INVOKE_RE)) {
45
+ const call = { tool: invoke[1] };
46
+ for (const param of (invoke[2] ?? "").matchAll(DSML_PARAM_RE))
47
+ call[param[1]] = coerceDsmlValue(param[2] ?? "", param[3] ?? "");
48
+ calls.push(call);
49
+ }
50
+ }
51
+ return calls;
52
+ }
53
+ export function parseToolCalls(text) {
54
+ const calls = [];
55
+ for (const match of text.matchAll(TOOL_RE)) {
56
+ const raw = match[1]?.trim();
57
+ if (!raw)
58
+ continue;
59
+ try {
60
+ const parsed = JSON.parse(raw);
61
+ if (typeof parsed.tool === "string")
62
+ calls.push(parsed);
63
+ }
64
+ catch {
65
+ // Ignore malformed tool blocks; the model can correct itself next turn.
66
+ }
67
+ }
68
+ calls.push(...parseDsmlCalls(text));
69
+ return calls;
70
+ }
71
+ export function stripToolCalls(text) {
72
+ return canonicalizeDsml(text).replace(TOOL_RE, "").replace(DSML_BLOCK_RE, "").trim();
73
+ }
74
+ export function formatToolResults(results) {
75
+ const blocks = results.map((r, index) => {
76
+ const status = r.ok ? "ok" : "error";
77
+ return [
78
+ `Tool result ${index + 1} (${status})`,
79
+ `call: ${JSON.stringify(r.call)}`,
80
+ "output:",
81
+ fence(r.output),
82
+ ].join("\n");
83
+ });
84
+ return [
85
+ "FastCode executed the requested local tools. Use these results to continue.",
86
+ ...blocks,
87
+ ].join("\n\n");
88
+ }
89
+ export const AGENT_SYSTEM_PROMPT = `
90
+ You are FastCode Agent, a local coding agent running inside the user's terminal.
91
+ Tools run inside a filesystem sandbox rooted at the current working directory.
92
+ File tools cannot access paths outside that root. Shell commands run with FASTCODE_SANDBOX=1 and FASTCODE_SANDBOX_ROOT set.
93
+
94
+ You can inspect and change the user's machine by emitting exactly one or more tool calls in this format:
95
+ <fastcode_tool>{"tool":"shell","command":"npm test"}</fastcode_tool>
96
+
97
+ Available tools:
98
+ - shell: {"tool":"shell","command":"command to run"}
99
+ - shell with timeout: {"tool":"shell","command":"npm test","timeoutMs":120000}
100
+ - pwd: {"tool":"pwd"}
101
+ - read_file: {"tool":"read_file","path":"path/to/file"}
102
+ - read_many: {"tool":"read_many","paths":["file1","file2"]}
103
+ - write_file: {"tool":"write_file","path":"path/to/file","content":"full file contents"}
104
+ - append_file: {"tool":"append_file","path":"path/to/file","content":"text to append"}
105
+ - apply_patch: {"tool":"apply_patch","patch":"*** Begin Patch\\n*** Update File: path\\n@@\\n-old\\n+new\\n*** End Patch\\n"}
106
+ - list_files: {"tool":"list_files","path":"optional/path","maxDepth":2}
107
+ - grep: {"tool":"grep","pattern":"text or regex","path":"optional/path"}
108
+ - replace_in_file: {"tool":"replace_in_file","path":"path/to/file","find":"exact text","replace":"replacement"}
109
+ - stat_path: {"tool":"stat_path","path":"path/to/file"}
110
+ - make_dir: {"tool":"make_dir","path":"path/to/dir"}
111
+ - copy_file: {"tool":"copy_file","from":"src","to":"dest"}
112
+ - move_path: {"tool":"move_path","from":"src","to":"dest"}
113
+ - delete_file: {"tool":"delete_file","path":"path/to/file"}
114
+
115
+ Rules:
116
+ - Prefer inspecting files before editing.
117
+ - Use shell for tests, builds, package scripts, git inspection, and OS commands.
118
+ - Use apply_patch or replace_in_file for focused edits.
119
+ - Use write_file only when replacing the whole file is appropriate.
120
+ - Run verification commands after code changes.
121
+ - After tool results come back, continue working until the user's request is done.
122
+ - When done, answer normally without tool calls.
123
+ `.trim();
124
+ function fence(text) {
125
+ return `\`\`\`\n${text}\n\`\`\``;
126
+ }