@datename/fastcode-termux 0.1.6-beta → 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 (165) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +50 -17
  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 +33 -0
  10. package/dist/agent/approval.js +40 -5
  11. package/dist/agent/compaction.d.ts +2 -0
  12. package/dist/agent/compaction.js +60 -17
  13. package/dist/agent/guards.d.ts +16 -16
  14. package/dist/agent/guards.js +978 -24
  15. package/dist/agent/policy/defaults.d.ts +16 -0
  16. package/dist/agent/policy/defaults.js +17 -0
  17. package/dist/agent/policy/egress.d.ts +32 -0
  18. package/dist/agent/policy/egress.js +280 -0
  19. package/dist/agent/process-tree.d.ts +20 -0
  20. package/dist/agent/process-tree.js +81 -10
  21. package/dist/agent/protocol.d.ts +20 -1
  22. package/dist/agent/protocol.js +165 -38
  23. package/dist/agent/registry.d.ts +3 -1
  24. package/dist/agent/registry.js +168 -11
  25. package/dist/agent/runner.d.ts +21 -0
  26. package/dist/agent/runner.js +44 -5
  27. package/dist/agent/shell-ast.d.ts +11 -0
  28. package/dist/agent/shell-ast.js +204 -0
  29. package/dist/agent/shell-session.d.ts +14 -0
  30. package/dist/agent/shell-session.js +251 -40
  31. package/dist/agent/tool-schemas.js +26 -7
  32. package/dist/agent/tools/append_file.js +3 -1
  33. package/dist/agent/tools/apply_patch.js +44 -4
  34. package/dist/agent/tools/copy_file.js +26 -3
  35. package/dist/agent/tools/delete_file.js +22 -3
  36. package/dist/agent/tools/grep.js +35 -4
  37. package/dist/agent/tools/make_dir.js +3 -1
  38. package/dist/agent/tools/move_path.d.ts +7 -0
  39. package/dist/agent/tools/move_path.js +31 -5
  40. package/dist/agent/tools/read_file.js +63 -3
  41. package/dist/agent/tools/read_many.js +21 -3
  42. package/dist/agent/tools/readers.d.ts +53 -1
  43. package/dist/agent/tools/readers.js +236 -26
  44. package/dist/agent/tools/replace_in_file.js +38 -5
  45. package/dist/agent/tools/shell.js +2 -2
  46. package/dist/agent/tools/types.d.ts +26 -0
  47. package/dist/agent/tools/validate.d.ts +15 -2
  48. package/dist/agent/tools/validate.js +66 -3
  49. package/dist/agent/tools/write_file.js +3 -1
  50. package/dist/app.d.ts +2 -1
  51. package/dist/app.js +526 -85
  52. package/dist/cli-main.js +363 -0
  53. package/dist/cli.js +397 -337
  54. package/dist/commands/about.js +1 -1
  55. package/dist/commands/clear.d.ts +1 -0
  56. package/dist/commands/clear.js +1 -1
  57. package/dist/commands/compact.d.ts +12 -0
  58. package/dist/commands/compact.js +14 -2
  59. package/dist/commands/context.js +5 -4
  60. package/dist/commands/index.js +2 -0
  61. package/dist/commands/jobs.js +3 -2
  62. package/dist/commands/kill.js +3 -1
  63. package/dist/commands/login.d.ts +2 -2
  64. package/dist/commands/login.js +15 -15
  65. package/dist/commands/model.d.ts +1 -13
  66. package/dist/commands/model.js +6 -38
  67. package/dist/commands/provider.d.ts +1 -15
  68. package/dist/commands/provider.js +4 -22
  69. package/dist/commands/skills.js +34 -4
  70. package/dist/commands/threads.d.ts +9 -0
  71. package/dist/commands/threads.js +26 -0
  72. package/dist/config/store.d.ts +21 -0
  73. package/dist/config/store.js +47 -9
  74. package/dist/config/trust.d.ts +9 -0
  75. package/dist/config/trust.js +41 -2
  76. package/dist/headless.d.ts +7 -0
  77. package/dist/headless.js +213 -0
  78. package/dist/index.js +19 -3
  79. package/dist/lib/debug.d.ts +2 -0
  80. package/dist/lib/debug.js +22 -0
  81. package/dist/lib/diagnostics.d.ts +19 -0
  82. package/dist/lib/diagnostics.js +32 -6
  83. package/dist/lib/highlight-subset.d.ts +21 -0
  84. package/dist/lib/highlight-subset.js +88 -0
  85. package/dist/lib/lifecycle.d.ts +5 -0
  86. package/dist/lib/lifecycle.js +62 -0
  87. package/dist/lib/markdown.js +22 -4
  88. package/dist/providers/deepseek.js +1 -1
  89. package/dist/providers/openaiCompatible.d.ts +24 -2
  90. package/dist/providers/openaiCompatible.js +20 -6
  91. package/dist/providers/spec.d.ts +12 -0
  92. package/dist/providers/spec.js +29 -0
  93. package/dist/session/log.d.ts +14 -2
  94. package/dist/session/log.js +56 -27
  95. package/dist/session/projection.d.ts +34 -1
  96. package/dist/session/projection.js +315 -46
  97. package/dist/skills/manager.d.ts +15 -0
  98. package/dist/skills/manager.js +135 -18
  99. package/dist/types.d.ts +15 -0
  100. package/dist/ui/StatusBar.js +1 -1
  101. package/package.json +3 -2
  102. package/src/agent/approval-json.ts +103 -0
  103. package/src/agent/approval-native.ts +168 -0
  104. package/src/agent/approval-store.ts +38 -0
  105. package/src/agent/approval.ts +58 -11
  106. package/src/agent/compaction.ts +62 -17
  107. package/src/agent/guards.ts +1049 -25
  108. package/src/agent/policy/defaults.ts +34 -0
  109. package/src/agent/policy/egress.ts +335 -0
  110. package/src/agent/process-tree.ts +105 -23
  111. package/src/agent/protocol.ts +187 -39
  112. package/src/agent/registry.ts +186 -14
  113. package/src/agent/runner.ts +50 -7
  114. package/src/agent/shell-ast.ts +227 -0
  115. package/src/agent/shell-session.ts +265 -41
  116. package/src/agent/tool-schemas.ts +26 -7
  117. package/src/agent/tools/append_file.ts +3 -1
  118. package/src/agent/tools/apply_patch.ts +45 -4
  119. package/src/agent/tools/copy_file.ts +27 -3
  120. package/src/agent/tools/delete_file.ts +22 -3
  121. package/src/agent/tools/grep.ts +36 -4
  122. package/src/agent/tools/make_dir.ts +3 -1
  123. package/src/agent/tools/move_path.ts +38 -5
  124. package/src/agent/tools/read_file.ts +64 -3
  125. package/src/agent/tools/read_many.ts +21 -3
  126. package/src/agent/tools/readers.ts +289 -26
  127. package/src/agent/tools/replace_in_file.ts +39 -5
  128. package/src/agent/tools/shell.ts +2 -2
  129. package/src/agent/tools/types.ts +43 -1
  130. package/src/agent/tools/validate.ts +73 -4
  131. package/src/agent/tools/write_file.ts +3 -1
  132. package/src/app.ts +533 -82
  133. package/src/{cli.ts → cli-main.ts} +34 -6
  134. package/src/commands/about.ts +1 -1
  135. package/src/commands/clear.ts +1 -1
  136. package/src/commands/compact.ts +14 -2
  137. package/src/commands/context.ts +5 -4
  138. package/src/commands/index.ts +2 -0
  139. package/src/commands/jobs.ts +3 -2
  140. package/src/commands/kill.ts +3 -1
  141. package/src/commands/login.ts +15 -15
  142. package/src/commands/model.ts +6 -38
  143. package/src/commands/provider.ts +4 -22
  144. package/src/commands/skills.ts +34 -4
  145. package/src/commands/threads.ts +28 -0
  146. package/src/config/store.ts +47 -9
  147. package/src/config/trust.ts +45 -2
  148. package/src/headless.ts +225 -0
  149. package/src/index.ts +19 -3
  150. package/src/lib/debug.ts +24 -0
  151. package/src/lib/diagnostics.ts +32 -6
  152. package/src/lib/highlight-subset.ts +93 -0
  153. package/src/lib/lifecycle.ts +69 -0
  154. package/src/lib/markdown.ts +22 -4
  155. package/src/providers/deepseek.ts +1 -1
  156. package/src/providers/openaiCompatible.ts +47 -10
  157. package/src/providers/spec.ts +42 -0
  158. package/src/session/log.ts +70 -27
  159. package/src/session/projection.ts +360 -47
  160. package/src/skills/manager.ts +140 -18
  161. package/src/types/highlight-shims.d.ts +14 -0
  162. package/src/types.ts +16 -1
  163. package/src/ui/StatusBar.ts +1 -1
  164. package/tsconfig.json +1 -0
  165. /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * FASTCODE_TRUST_ALL=1 marks every directory trusted (scripts, CI).
9
9
  */
10
- import { existsSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
10
+ import { existsSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
11
11
  import { resolve } from "node:path";
12
12
  import { ensureDir, getPaths } from "./paths.js";
13
13
  const TRUTHY = ["1", "true", "yes", "on"];
@@ -111,7 +111,20 @@ export function loadTrust() {
111
111
  function saveTrust(state) {
112
112
  const paths = getPaths();
113
113
  ensureDir(paths.root);
114
- writeFileSync(paths.trust, JSON.stringify(state, null, 2), "utf8");
114
+ const tmp = paths.trust + "." + process.pid + ".tmp";
115
+ try {
116
+ writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8");
117
+ renameSync(tmp, paths.trust);
118
+ }
119
+ catch (err) {
120
+ try {
121
+ rmSync(tmp, { force: true });
122
+ }
123
+ catch {
124
+ // best effort
125
+ }
126
+ throw err;
127
+ }
115
128
  try {
116
129
  const info = statSync(paths.trust);
117
130
  cache = { file: paths.trust, key: `${info.mtimeMs}:${info.size}`, state };
@@ -151,3 +164,29 @@ export function untrustProject(dir) {
151
164
  export function listTrusted() {
152
165
  return Object.entries(loadTrust().paths).map(([path, entry]) => ({ path, addedAt: entry.grantedAt }));
153
166
  }
167
+ /** Check a tool/fingerprint approval bound to the current tool catalog. */
168
+ export function isToolApproved(key, catalogHash) {
169
+ const entry = loadTrust().tools[key];
170
+ return Boolean(entry && entry.catalogHash === catalogHash);
171
+ }
172
+ export function rememberToolApproval(key, catalogHash) {
173
+ const state = loadTrust();
174
+ const at = new Date().toISOString();
175
+ state.tools[key] = { grantedAt: at, lastUsed: at, catalogHash };
176
+ saveTrust(state);
177
+ }
178
+ export function forgetToolApproval(key) {
179
+ const state = loadTrust();
180
+ if (!state.tools[key])
181
+ return false;
182
+ delete state.tools[key];
183
+ saveTrust(state);
184
+ return true;
185
+ }
186
+ export function listToolApprovals() {
187
+ return Object.entries(loadTrust().tools).map(([key, entry]) => ({
188
+ key,
189
+ grantedAt: entry.grantedAt,
190
+ catalogHash: entry.catalogHash,
191
+ }));
192
+ }
@@ -0,0 +1,7 @@
1
+ interface HeadlessOptions {
2
+ prompt: string;
3
+ ui: "json" | "plain";
4
+ resume?: string;
5
+ }
6
+ export declare function runHeadless(options: HeadlessOptions): Promise<void>;
7
+ export {};
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Non-Ink headless runner for `--ui json` / non-TTY `--ui plain`.
3
+ *
4
+ * stdout is reserved for the protocol/answer; diagnostics go to stderr.
5
+ */
6
+ import readline from "node:readline";
7
+ import { AGENT_SYSTEM_PROMPT } from "./agent/protocol.js";
8
+ import { compactionConfigFromEnv } from "./agent/compaction.js";
9
+ import { resolveToolProtocol } from "./agent/protocol-mode.js";
10
+ import { runAgent } from "./agent/runner.js";
11
+ import { setShellPoolSize } from "./agent/shell-session.js";
12
+ import { toolSchemas } from "./agent/tool-schemas.js";
13
+ import { encodeGateAsk, parseGateDecision } from "./agent/approval-json.js";
14
+ import { decisionForOption, parseApprovalChoice } from "./agent/approval.js";
15
+ import { buildPluginsPrompt } from "./plugins/manager.js";
16
+ import { buildSkillsPrompt } from "./skills/manager.js";
17
+ import { loadConfig, resolveApiKey } from "./config/store.js";
18
+ import { getProviderOrThrow } from "./providers/index.js";
19
+ import { resumeSession } from "./session/projection.js";
20
+ let eventSeq = 0;
21
+ function writeEvent(value) {
22
+ eventSeq += 1;
23
+ process.stdout.write(JSON.stringify({ v: 1, seq: eventSeq, ts: new Date().toISOString(), ...value }) + "\n");
24
+ }
25
+ function optionalBudget(name) {
26
+ const value = Number(process.env[name]);
27
+ return Number.isFinite(value) && value > 0 ? value : undefined;
28
+ }
29
+ export async function runHeadless(options) {
30
+ const config = loadConfig();
31
+ setShellPoolSize(config.shellPoolSize);
32
+ const provider = getProviderOrThrow(config.providerId);
33
+ let apiKey;
34
+ if (provider.authEnv.length > 0) {
35
+ const lookup = resolveApiKey(provider.id, config.model, provider.authEnv);
36
+ if (lookup.status !== "ok")
37
+ throw new Error(`No usable API key for ${provider.id}.`);
38
+ apiKey = lookup.key;
39
+ }
40
+ const history = options.resume ? resumeSession(options.resume, process.cwd()).messages : [];
41
+ const system = [
42
+ config.systemPrompt,
43
+ buildSkillsPrompt(config.enabledSkills),
44
+ buildPluginsPrompt(config.enabledPlugins),
45
+ config.agentMode ? AGENT_SYSTEM_PROMPT : "",
46
+ ].filter((part) => part.trim()).join("\n\n");
47
+ const protocol = config.agentMode ? resolveToolProtocol(config, config.providerId, config.model) : "legacy";
48
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
49
+ const queued = [];
50
+ const waiters = [];
51
+ let eof = false;
52
+ rl.on("line", (line) => {
53
+ const waiter = waiters.shift();
54
+ if (waiter)
55
+ waiter(line);
56
+ else
57
+ queued.push(line);
58
+ });
59
+ rl.on("close", () => {
60
+ eof = true;
61
+ while (waiters.length > 0)
62
+ waiters.shift()(undefined);
63
+ });
64
+ const nextLine = (timeoutMs) => {
65
+ if (queued.length > 0)
66
+ return Promise.resolve(queued.shift());
67
+ if (eof)
68
+ return Promise.resolve(undefined);
69
+ return new Promise((resolve) => {
70
+ const waiter = (line) => {
71
+ clearTimeout(timer);
72
+ resolve(line);
73
+ };
74
+ const timer = setTimeout(() => {
75
+ const index = waiters.indexOf(waiter);
76
+ if (index >= 0)
77
+ waiters.splice(index, 1);
78
+ resolve(undefined);
79
+ }, Math.max(1000, timeoutMs));
80
+ timer.unref?.();
81
+ waiters.push(waiter);
82
+ });
83
+ };
84
+ const requestApproval = async (request) => {
85
+ if (options.ui === "json") {
86
+ writeEvent(encodeGateAsk(request));
87
+ const line = await nextLine(request.timeoutMs ?? 60_000);
88
+ if (line === undefined)
89
+ return { kind: "deny", reason: "approval timeout/EOF" };
90
+ const parsed = parseGateDecision(request, line);
91
+ if (parsed.error) {
92
+ writeEvent({ event: "gate:error", version: 1, id: request.id, error: parsed.error });
93
+ return parsed.decision;
94
+ }
95
+ return parsed.decision;
96
+ }
97
+ process.stderr.write(`${request.prompt ?? "Approval required"}\n`);
98
+ request.options.forEach((option, index) => process.stderr.write(`${index + 1}. ${option.label}\n`));
99
+ process.stderr.write(`Choose 1-${request.options.length}: `);
100
+ const line = await nextLine(request.timeoutMs ?? 60_000);
101
+ const option = line === undefined ? undefined : parseApprovalChoice(request, line);
102
+ if (!option)
103
+ return { kind: "deny", reason: "approval cancelled/invalid/timeout" };
104
+ if (option.kind === "feedback") {
105
+ process.stderr.write(`${option.prompt || "Why should this be denied?"}\n> `);
106
+ const feedbackLine = await nextLine(request.timeoutMs ?? 60_000);
107
+ const feedback = feedbackLine?.trim();
108
+ return feedback
109
+ ? { kind: "feedback", text: feedback }
110
+ : { kind: "deny", reason: "feedback cancelled/empty (fail closed)" };
111
+ }
112
+ return decisionForOption(option);
113
+ };
114
+ const abort = new AbortController();
115
+ const onSigint = () => abort.abort();
116
+ const onTerminate = () => abort.abort();
117
+ process.once("SIGINT", onSigint);
118
+ process.once("SIGTERM", onTerminate);
119
+ process.once("SIGHUP", onTerminate);
120
+ try {
121
+ for await (const event of runAgent({
122
+ provider,
123
+ apiKey,
124
+ model: config.model,
125
+ messages: history,
126
+ prompt: options.prompt,
127
+ system,
128
+ maxTurns: config.agentMode ? Math.max(1, Math.floor(Number(config.maxAgentTurns) || 24)) : 1,
129
+ allowTools: config.agentMode,
130
+ tokenBudget: optionalBudget("FASTCODE_TOKEN_BUDGET"),
131
+ costBudgetUsd: optionalBudget("FASTCODE_COST_BUDGET_USD"),
132
+ signal: abort.signal,
133
+ cwd: process.cwd(),
134
+ idleTimeoutMs: config.requestIdleTimeoutMs,
135
+ compaction: compactionConfigFromEnv(),
136
+ reasoningEffort: config.reasoningEffort,
137
+ protocol,
138
+ tools: protocol === "native" ? toolSchemas() : undefined,
139
+ requestApproval,
140
+ })) {
141
+ if (options.ui === "json") {
142
+ if (event.type === "assistant_text")
143
+ writeEvent({ event: "assistant", text: event.text });
144
+ else if (event.type === "tool_call")
145
+ writeEvent({
146
+ event: "tool_call",
147
+ id: event.id,
148
+ tool: event.call.tool,
149
+ args: event.call,
150
+ ...(event.native ? { native: true } : {}),
151
+ });
152
+ else if (event.type === "egress_denied")
153
+ writeEvent({
154
+ event: "egress_denied",
155
+ outlet: event.outlet,
156
+ ...(event.target ? { target: event.target } : {}),
157
+ rule: event.rule,
158
+ mode: event.mode,
159
+ would_block: event.wouldBlock,
160
+ code: event.code,
161
+ reason: event.reason,
162
+ });
163
+ else if (event.type === "tool_result")
164
+ writeEvent({
165
+ event: "tool_result",
166
+ id: event.id,
167
+ tool: event.call.tool,
168
+ ok: event.ok,
169
+ output: event.output,
170
+ ...(event.sideEffects ? { side_effects: event.sideEffects } : {}),
171
+ ...(event.code ? { code: event.code } : {}),
172
+ ...(event.hint ? { hint: event.hint } : {}),
173
+ ...(event.truncated !== undefined ? { truncated: event.truncated } : {}),
174
+ ...(event.totalLines !== undefined ? { total_lines: event.totalLines } : {}),
175
+ ...(event.returnedLines !== undefined ? { returned_lines: event.returnedLines } : {}),
176
+ ...(event.totalBytes !== undefined ? { total_bytes: event.totalBytes } : {}),
177
+ ...(event.returnedBytes !== undefined ? { returned_bytes: event.returnedBytes } : {}),
178
+ ...(event.filesRead !== undefined ? { files_read: event.filesRead } : {}),
179
+ ...(event.filesSkipped !== undefined ? { files_skipped: event.filesSkipped } : {}),
180
+ ...(event.matches !== undefined ? { matches: event.matches } : {}),
181
+ });
182
+ else if (event.type === "usage")
183
+ writeEvent({
184
+ event: "usage",
185
+ input_tokens: event.usage.inputTokens ?? 0,
186
+ output_tokens: event.usage.outputTokens ?? 0,
187
+ cost_usd: event.usage.costUsd ?? 0,
188
+ });
189
+ else if (event.type === "notice")
190
+ writeEvent({ event: "notice", text: event.text });
191
+ else if (event.type === "error")
192
+ writeEvent({ event: "error", kind: event.kind, code: event.kind, message: event.message });
193
+ else if (event.type === "done")
194
+ writeEvent({ event: "done", stopReason: event.stopReason, finalText: event.finalText });
195
+ }
196
+ else if (event.type === "assistant_text") {
197
+ process.stdout.write(event.text);
198
+ }
199
+ else if (event.type === "notice") {
200
+ process.stderr.write(`[fastcode] ${event.text}\n`);
201
+ }
202
+ else if (event.type === "done" && event.finalText) {
203
+ process.stdout.write(event.finalText + "\n");
204
+ }
205
+ }
206
+ }
207
+ finally {
208
+ process.removeListener("SIGINT", onSigint);
209
+ process.removeListener("SIGTERM", onTerminate);
210
+ process.removeListener("SIGHUP", onTerminate);
211
+ rl.close();
212
+ }
213
+ }
package/dist/index.js CHANGED
@@ -5,13 +5,29 @@
5
5
  * Ink-based TUI. We keep this file tiny so the slow imports (Ink, React)
6
6
  * are not loaded for `fastcode skills list`.
7
7
  */
8
- import { parseCli } from "./cli.js";
8
+ import { parseCli } from "./cli-main.js";
9
+ import { installProcessGuards } from "./lib/lifecycle.js";
9
10
  async function main() {
11
+ installProcessGuards();
10
12
  const cli = await parseCli();
11
13
  if (cli.done)
12
14
  return;
15
+ if (cli.ui === "json") {
16
+ if (!cli.prompt) {
17
+ console.error("--ui json requires a prompt: fastcode --ui json \"<prompt>\"");
18
+ process.exit(1);
19
+ }
20
+ const { runHeadless } = await import("./headless.js");
21
+ await runHeadless({ prompt: cli.prompt, ui: "json", resume: cli.resume });
22
+ return;
23
+ }
13
24
  if (!process.stdout.isTTY) {
14
- console.error("FastCode TUI requires a TTY. Pipe-mode prompts aren't supported yet — try `fastcode \"<prompt>\"` from a real terminal.");
25
+ if (cli.ui === "plain" && cli.prompt) {
26
+ const { runHeadless } = await import("./headless.js");
27
+ await runHeadless({ prompt: cli.prompt, ui: "plain", resume: cli.resume });
28
+ return;
29
+ }
30
+ console.error("FastCode TUI requires a TTY. Use `fastcode --ui plain|json \"<prompt>\"` for pipe mode.");
15
31
  process.exit(1);
16
32
  }
17
33
  // Lazy-load Ink so subcommands don't pay the React boot cost.
@@ -23,7 +39,7 @@ async function main() {
23
39
  // React.createElement avoids needing a separate .tsx loader for this file.
24
40
  // exitOnCtrlC: false lets the app own Ctrl+C so the documented
25
41
  // "press Ctrl+C twice to quit" behavior actually works.
26
- render(React.createElement(App, { initialPrompt: cli.prompt, resume: cli.resume }), { exitOnCtrlC: false });
42
+ render(React.createElement(App, { initialPrompt: cli.prompt, resume: cli.resume, ui: cli.ui ?? "tty" }), { exitOnCtrlC: false });
27
43
  }
28
44
  main().catch((err) => {
29
45
  console.error(err);
@@ -0,0 +1,2 @@
1
+ export declare function debugEnabled(): boolean;
2
+ export declare function debugLog(scope: string, message: string): void;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Minimal debug logging.
3
+ *
4
+ * There is no logging framework in this project: normal output goes through
5
+ * Ink and lifecycle warnings go straight to stderr. `FASTCODE_DEBUG=1` opts
6
+ * into low-volume stderr diagnostics (for example timeout clamping or config
7
+ * cache invalidation) without changing the default experience.
8
+ */
9
+ const TRUTHY = new Set(["1", "true", "yes", "on"]);
10
+ export function debugEnabled() {
11
+ return TRUTHY.has((process.env["FASTCODE_DEBUG"] ?? "").trim().toLowerCase());
12
+ }
13
+ export function debugLog(scope, message) {
14
+ if (!debugEnabled())
15
+ return;
16
+ try {
17
+ process.stderr.write(`[fastcode:${scope}] ${message}\n`);
18
+ }
19
+ catch {
20
+ // Debug logging must never break the operation it observes.
21
+ }
22
+ }
@@ -22,6 +22,9 @@ export declare function collectDiagnostics(): {
22
22
  keyStatus: "ok" | "missing" | "unbound";
23
23
  keyEnv: string;
24
24
  skillsInstalled: number;
25
+ skillsRegistry: string;
26
+ debug: boolean;
27
+ configCache: boolean;
25
28
  pluginsInstalled: number;
26
29
  enabledSkills: any;
27
30
  enabledPlugins: any;
@@ -37,16 +40,32 @@ export declare function collectDiagnostics(): {
37
40
  shellDenyPatterns: number;
38
41
  allowDangerous: boolean;
39
42
  toolOutputLimit: number;
43
+ readMaxBytes: number;
44
+ readScanMaxBytes: number;
45
+ readManyMaxBytes: number;
46
+ grepMaxBytes: number;
47
+ egress: {
48
+ enabled: boolean;
49
+ mode: import("../agent/policy/defaults.js").EgressMode;
50
+ maxScanBytes: number;
51
+ denyPaths: number;
52
+ denyGlobs: number;
53
+ denyContentMarkers: number;
54
+ scanToolOutput: boolean;
55
+ };
40
56
  tokenBudget: number;
41
57
  costBudgetUsd: number;
42
58
  compactionEnabled: boolean;
43
59
  compactionRatio: number;
60
+ compactionKeepTurns: number;
61
+ compactionSummaryTokens: number;
44
62
  compactionHeapMb: number;
45
63
  compactionHeapUsedMb: number;
46
64
  compactionForced: boolean;
47
65
  serverCompactionAware: boolean;
48
66
  shellActive: number;
49
67
  shellPool: number;
68
+ shellPoolMax: number;
50
69
  shellSpawns: number;
51
70
  shellMaxSpawn: number;
52
71
  sseIdleMs: number;
@@ -11,14 +11,17 @@ import { detectPlatform, platformLabel } from "./platform.js";
11
11
  import { readPackageMeta } from "./pkg.js";
12
12
  import { shouldUseStableTerminalUi } from "./uiMode.js";
13
13
  import { getPaths } from "../config/paths.js";
14
- import { loadConfig, resolveApiKey, resolveProviderKey } from "../config/store.js";
14
+ import { configCacheStatus, loadConfig, resolveApiKey, resolveProviderKey } from "../config/store.js";
15
+ import { debugEnabled } from "./debug.js";
15
16
  import { PROVIDERS } from "../providers/index.js";
16
- import { listInstalledSkills } from "../skills/manager.js";
17
+ import { listInstalledSkills, resolveRegistry } from "../skills/manager.js";
17
18
  import { listInstalledPlugins } from "../plugins/manager.js";
18
19
  import { findUnclosedSessions, fsyncMode, loggingEnabled, listSessions, sessionCacheLimitBytes } from "../session/log.js";
19
20
  import { isTrusted } from "../config/trust.js";
20
21
  import { DEFAULT_DENY_PATTERNS, loopExemptTools } from "../agent/guards.js";
21
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";
22
25
  import { compactionConfigFromEnv, heapExceeded } from "../agent/compaction.js";
23
26
  import { shellPoolStats } from "../agent/shell-session.js";
24
27
  import { resolveToolProtocol, toolCapabilityKey } from "../agent/protocol-mode.js";
@@ -34,6 +37,7 @@ function nodeIsSupported() {
34
37
  }
35
38
  export function collectDiagnostics() {
36
39
  const config = loadConfig();
40
+ const egress = normalizeEgressConfig(config.egress);
37
41
  let paths;
38
42
  try {
39
43
  paths = getPaths();
@@ -95,6 +99,9 @@ export function collectDiagnostics() {
95
99
  keyStatus: provider ? keyLookup.status : "missing",
96
100
  keyEnv: provider?.authEnv?.[0],
97
101
  skillsInstalled,
102
+ skillsRegistry: resolveRegistry(process.env["FASTCODE_SKILLS_REGISTRY"], config.skillsRegistry),
103
+ debug: debugEnabled(),
104
+ configCache: configCacheStatus().cached,
98
105
  pluginsInstalled,
99
106
  enabledSkills: (config.enabledSkills ?? []).length,
100
107
  enabledPlugins: (config.enabledPlugins ?? []).length,
@@ -110,16 +117,32 @@ export function collectDiagnostics() {
110
117
  shellDenyPatterns: DEFAULT_DENY_PATTERNS.length,
111
118
  allowDangerous: ["1", "true", "yes", "on"].includes((process.env["FASTCODE_ALLOW_DANGEROUS"] ?? "").trim().toLowerCase()),
112
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
+ },
113
133
  tokenBudget: positiveEnv("FASTCODE_TOKEN_BUDGET"),
114
134
  costBudgetUsd: positiveEnv("FASTCODE_COST_BUDGET_USD"),
115
135
  compactionEnabled: compaction.enabled,
116
136
  compactionRatio: compaction.localThresholdRatio,
137
+ compactionKeepTurns: compaction.keepTurns,
138
+ compactionSummaryTokens: compaction.maxSummaryTokens,
117
139
  compactionHeapMb: compaction.heapLimitMb,
118
140
  compactionHeapUsedMb: process.memoryUsage().heapUsed / 1024 / 1024,
119
141
  compactionForced: heapExceeded(compaction),
120
142
  serverCompactionAware: compaction.serverCompactionAware,
121
143
  shellActive: shells.active,
122
144
  shellPool: shells.pool,
145
+ shellPoolMax: shells.maxPool,
123
146
  shellSpawns: shells.spawns,
124
147
  shellMaxSpawn: shells.maxSpawnPerTurn,
125
148
  sseIdleMs: positiveEnv("FASTCODE_SSE_IDLE_MS") || 25_000,
@@ -157,16 +180,19 @@ export function formatDiagnostics(info) {
157
180
  lines.push(" trusted cwd " + (info.trusted ? "yes" : "NO - agent tools ask first or stay off") + " (" + (info.trustFile ?? "?") + ")");
158
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");
159
182
  lines.push(" session fsync " + info.sessionFsync + " unclosed " + info.unclosedSessions + " " + (info.sessionsDir ?? "(unavailable)"));
160
- lines.push(" shell guard " + info.shellDenyPatterns + " deny patterns" + (info.allowDangerous ? " [FASTCODE_ALLOW_DANGEROUS is ON]" : ""));
183
+ lines.push(" shell guard " + info.shellDenyPatterns + " legacy soft patterns" + (info.allowDangerous ? " [FASTCODE_ALLOW_DANGEROUS is ON]" : ""));
161
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}`);
162
187
  lines.push(` budgets ${info.tokenBudget > 0 ? "tokens ≤ " + info.tokenBudget : "token budget off"} · ${info.costBudgetUsd > 0 ? "$" + info.costBudgetUsd : "cost budget off"}`);
163
- lines.push(` compaction ${info.compactionEnabled ? "on" : "off"} · ${Math.round(info.compactionRatio * 100)}% · heap ${info.compactionHeapUsedMb.toFixed(1)}/${info.compactionHeapMb}MB${info.compactionForced ? " [FORCED]" : ""}${info.serverCompactionAware ? " · server-aware" : ""}`);
164
- lines.push(` shell pool ${info.shellActive} active · ${info.shellPool} resident · ${info.shellSpawns} spawns · cap ${info.shellMaxSpawn}/turn`);
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`);
165
190
  lines.push(` loop exempt ${info.loopExempt.length > 0 ? info.loopExempt.join(", ") : "(none)"}`);
166
191
  lines.push(` model meta ctx ${info.modelMeta.contextWindow} · ~${info.modelMeta.charsPerToken} chars/token · ${info.modelMeta.supportsTools ? "tools" : "no tools"} · ${info.modelMetaFile}`);
167
192
  lines.push(` sse timeout ${info.sseIdleMs}ms idle`);
168
193
  lines.push(` tool protocol ${info.toolProtocol} (${info.toolProtocolMode}) · cached ${info.toolCapability}`);
169
194
  lines.push(` provider ${info.providerId} · model ${info.model}`);
195
+ lines.push(` config cache ${info.configCache ? "warm" : "cold"} · debug ${info.debug ? "on" : "off"}`);
170
196
  let apiKeyStatus;
171
197
  if (info.keyPresent)
172
198
  apiKeyStatus = "present";
@@ -175,7 +201,7 @@ export function formatDiagnostics(info) {
175
201
  else
176
202
  apiKeyStatus = `MISSING — set ${info.keyEnv ?? "the provider env var"} or /login ${info.providerId} ${info.model} <key>`;
177
203
  lines.push(` api key ${apiKeyStatus}`);
178
- lines.push(` skills ${info.skillsInstalled} installed, ${info.enabledSkills} enabled`);
204
+ lines.push(` skills ${info.skillsInstalled} installed, ${info.enabledSkills} enabled (${info.skillsRegistry || "no registry configured"})`);
179
205
  lines.push(` plugins ${info.pluginsInstalled} installed, ${info.enabledPlugins} enabled`);
180
206
  lines.push("");
181
207
  lines.push(" provider keys:");
@@ -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;
@@ -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;