@glbgpt/cli 0.1.0-test.4 → 0.1.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 (143) hide show
  1. package/README.md +79 -6
  2. package/dist/src/auth/authClient.d.ts +21 -2
  3. package/dist/src/auth/authClient.js +67 -3
  4. package/dist/src/auth/authClient.js.map +1 -1
  5. package/dist/src/auth/loginFlow.js +5 -2
  6. package/dist/src/auth/loginFlow.js.map +1 -1
  7. package/dist/src/auth/loopbackServer.d.ts +2 -1
  8. package/dist/src/auth/loopbackServer.js +6 -3
  9. package/dist/src/auth/loopbackServer.js.map +1 -1
  10. package/dist/src/commands/accountCommand.js +10 -4
  11. package/dist/src/commands/accountCommand.js.map +1 -1
  12. package/dist/src/commands/configCommand.d.ts +5 -0
  13. package/dist/src/commands/configCommand.js +15 -2
  14. package/dist/src/commands/configCommand.js.map +1 -1
  15. package/dist/src/commands/execCommand.d.ts +6 -0
  16. package/dist/src/commands/execCommand.js +61 -9
  17. package/dist/src/commands/execCommand.js.map +1 -1
  18. package/dist/src/commands/loginCommand.js +9 -6
  19. package/dist/src/commands/loginCommand.js.map +1 -1
  20. package/dist/src/commands/mediaCommand.js +3 -2
  21. package/dist/src/commands/mediaCommand.js.map +1 -1
  22. package/dist/src/commands/skillCommand.d.ts +30 -0
  23. package/dist/src/commands/skillCommand.js +160 -0
  24. package/dist/src/commands/skillCommand.js.map +1 -0
  25. package/dist/src/config/configFile.d.ts +12 -0
  26. package/dist/src/config/configFile.js +12 -0
  27. package/dist/src/config/configFile.js.map +1 -1
  28. package/dist/src/config/env.d.ts +18 -0
  29. package/dist/src/config/env.js +57 -7
  30. package/dist/src/config/env.js.map +1 -1
  31. package/dist/src/core/catalog.d.ts +8 -1
  32. package/dist/src/core/catalog.js +13 -2
  33. package/dist/src/core/catalog.js.map +1 -1
  34. package/dist/src/core/contextPrune.d.ts +13 -0
  35. package/dist/src/core/contextPrune.js +61 -0
  36. package/dist/src/core/contextPrune.js.map +1 -0
  37. package/dist/src/core/httpChatBackend.js +22 -5
  38. package/dist/src/core/httpChatBackend.js.map +1 -1
  39. package/dist/src/core/runChat.d.ts +9 -5
  40. package/dist/src/core/runChat.js +10 -7
  41. package/dist/src/core/runChat.js.map +1 -1
  42. package/dist/src/core/session.d.ts +15 -0
  43. package/dist/src/core/session.js +29 -1
  44. package/dist/src/core/session.js.map +1 -1
  45. package/dist/src/core/toolRegistry.d.ts +14 -3
  46. package/dist/src/core/toolRegistry.js +24 -15
  47. package/dist/src/core/toolRegistry.js.map +1 -1
  48. package/dist/src/core/turnLoop.d.ts +4 -0
  49. package/dist/src/core/turnLoop.js +62 -13
  50. package/dist/src/core/turnLoop.js.map +1 -1
  51. package/dist/src/core/types.d.ts +6 -0
  52. package/dist/src/core/updateCheck.d.ts +46 -0
  53. package/dist/src/core/updateCheck.js +139 -0
  54. package/dist/src/core/updateCheck.js.map +1 -0
  55. package/dist/src/exec/media.d.ts +32 -0
  56. package/dist/src/exec/media.js +70 -0
  57. package/dist/src/exec/media.js.map +1 -0
  58. package/dist/src/exec/output.d.ts +16 -0
  59. package/dist/src/exec/output.js +41 -0
  60. package/dist/src/exec/output.js.map +1 -1
  61. package/dist/src/fs/globTool.d.ts +8 -0
  62. package/dist/src/fs/globTool.js +117 -0
  63. package/dist/src/fs/globTool.js.map +1 -0
  64. package/dist/src/fs/grepTool.d.ts +4 -0
  65. package/dist/src/fs/grepTool.js +146 -0
  66. package/dist/src/fs/grepTool.js.map +1 -0
  67. package/dist/src/fs/index.d.ts +8 -0
  68. package/dist/src/fs/index.js +13 -0
  69. package/dist/src/fs/index.js.map +1 -0
  70. package/dist/src/fs/limits.d.ts +34 -0
  71. package/dist/src/fs/limits.js +74 -0
  72. package/dist/src/fs/limits.js.map +1 -0
  73. package/dist/src/fs/listDirTool.d.ts +2 -0
  74. package/dist/src/fs/listDirTool.js +68 -0
  75. package/dist/src/fs/listDirTool.js.map +1 -0
  76. package/dist/src/fs/readFileTool.d.ts +2 -0
  77. package/dist/src/fs/readFileTool.js +102 -0
  78. package/dist/src/fs/readFileTool.js.map +1 -0
  79. package/dist/src/fs/walk.d.ts +11 -0
  80. package/dist/src/fs/walk.js +38 -0
  81. package/dist/src/fs/walk.js.map +1 -0
  82. package/dist/src/fs/workspaceGuard.d.ts +27 -0
  83. package/dist/src/fs/workspaceGuard.js +96 -0
  84. package/dist/src/fs/workspaceGuard.js.map +1 -0
  85. package/dist/src/index.js +47 -3
  86. package/dist/src/index.js.map +1 -1
  87. package/dist/src/mcp/mediaTools.d.ts +63 -0
  88. package/dist/src/mcp/mediaTools.js +385 -0
  89. package/dist/src/mcp/mediaTools.js.map +1 -0
  90. package/dist/src/mcp/server.d.ts +6 -2
  91. package/dist/src/mcp/server.js +19 -9
  92. package/dist/src/mcp/server.js.map +1 -1
  93. package/dist/src/media/aiApiClient.d.ts +5 -0
  94. package/dist/src/media/aiApiClient.js +12 -1
  95. package/dist/src/media/aiApiClient.js.map +1 -1
  96. package/dist/src/media/budget.d.ts +42 -0
  97. package/dist/src/media/budget.js +95 -0
  98. package/dist/src/media/budget.js.map +1 -0
  99. package/dist/src/media/executor.d.ts +9 -0
  100. package/dist/src/media/executor.js +1 -1
  101. package/dist/src/media/executor.js.map +1 -1
  102. package/dist/src/media/generateImageTool.d.ts +20 -2
  103. package/dist/src/media/generateImageTool.js +102 -2
  104. package/dist/src/media/generateImageTool.js.map +1 -1
  105. package/dist/src/media/promptSanitizer.d.ts +11 -0
  106. package/dist/src/media/promptSanitizer.js +33 -0
  107. package/dist/src/media/promptSanitizer.js.map +1 -0
  108. package/dist/src/media/taskManager.d.ts +8 -1
  109. package/dist/src/media/taskManager.js +1 -1
  110. package/dist/src/media/taskManager.js.map +1 -1
  111. package/dist/src/tui/App.js +38 -8
  112. package/dist/src/tui/App.js.map +1 -1
  113. package/dist/src/tui/ParamEditor.d.ts +11 -2
  114. package/dist/src/tui/ParamEditor.js +48 -3
  115. package/dist/src/tui/ParamEditor.js.map +1 -1
  116. package/dist/src/tui/atPopup.d.ts +8 -0
  117. package/dist/src/tui/atPopup.js +1 -1
  118. package/dist/src/tui/atPopup.js.map +1 -1
  119. package/dist/src/tui/attachments.d.ts +12 -0
  120. package/dist/src/tui/attachments.js +12 -1
  121. package/dist/src/tui/attachments.js.map +1 -1
  122. package/dist/src/tui/chatController.d.ts +73 -2
  123. package/dist/src/tui/chatController.js +272 -43
  124. package/dist/src/tui/chatController.js.map +1 -1
  125. package/dist/src/tui/models.d.ts +6 -0
  126. package/dist/src/tui/models.js +14 -6
  127. package/dist/src/tui/models.js.map +1 -1
  128. package/dist/src/tui/run.js +55 -16
  129. package/dist/src/tui/run.js.map +1 -1
  130. package/dist/src/util/brokenPipe.d.ts +10 -0
  131. package/dist/src/util/brokenPipe.js +19 -0
  132. package/dist/src/util/brokenPipe.js.map +1 -0
  133. package/package.json +3 -2
  134. package/skills/globalgpt/SKILL.md +94 -0
  135. package/skills/globalgpt/references/troubleshooting.md +34 -0
  136. package/skills/globalgpt-coding/SKILL.md +65 -0
  137. package/skills/globalgpt-coding/references/troubleshooting.md +34 -0
  138. package/skills/globalgpt-image/SKILL.md +109 -0
  139. package/skills/globalgpt-image/references/recipes.md +52 -0
  140. package/skills/globalgpt-image/references/troubleshooting.md +49 -0
  141. package/skills/globalgpt-video/SKILL.md +109 -0
  142. package/skills/globalgpt-video/references/recipes.md +50 -0
  143. package/skills/globalgpt-video/references/troubleshooting.md +49 -0
@@ -0,0 +1,146 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import vm from 'node:vm';
4
+ import { GuardError, resolveInWorkspace, sanitizeFsError } from './workspaceGuard.js';
5
+ import { walkFiles } from './walk.js';
6
+ import { globToRegExp } from './globTool.js';
7
+ import { GREP_TIMEOUT_MS, MAX_FILE_SIZE_BYTES, MAX_GREP_LINE_CHARS, MAX_GREP_MATCHES, } from './limits.js';
8
+ function looksBinary(buf) {
9
+ return buf.subarray(0, 8192).includes(0);
10
+ }
11
+ /**
12
+ * Per-file matcher, run inside a vm context so the call deadline can interrupt
13
+ * catastrophic regex backtracking (vm timeout = V8 TerminateExecution, which
14
+ * does stop a spinning RegExp — see GREP_TIMEOUT_MS in limits.ts). The script
15
+ * is fixed; the model supplies only the regex SOURCE string, which the RegExp
16
+ * constructor treats as data, never as code.
17
+ */
18
+ const MATCH_SCRIPT = new vm.Script(`{
19
+ const re = new RegExp(source);
20
+ const hits = [];
21
+ for (let i = 0; i < lines.length; i++) if (re.test(lines[i])) hits.push(i);
22
+ hits;
23
+ }`);
24
+ function fmtTimeout(ms) {
25
+ return ms >= 1000 ? `${Math.round(ms / 1000)}s` : `${ms}ms`;
26
+ }
27
+ export function createGrepTool(root, opts = {}) {
28
+ const timeoutMs = opts.timeoutMs ?? GREP_TIMEOUT_MS;
29
+ return {
30
+ name: 'grep',
31
+ readOnly: true,
32
+ description: 'Search file contents inside the workspace with a regular expression (JavaScript syntax). ' +
33
+ `Returns "file:line: text" matches (first ${MAX_GREP_MATCHES}; larger result sets are truncated ` +
34
+ 'with a note). Binary files and files over 1MB are skipped. ' +
35
+ 'Use include (e.g. "*.md") to restrict which files are searched. ' +
36
+ 'Searches are time-boxed; on timeout you get any partial results plus a note (timeout does NOT mean no matches).',
37
+ parameters: {
38
+ type: 'object',
39
+ properties: {
40
+ pattern: { type: 'string', description: 'Regular expression to search for.' },
41
+ path: { type: 'string', description: 'Directory to search from (default workspace root).' },
42
+ include: { type: 'string', description: 'Glob filter on file names, e.g. "*.md".' },
43
+ },
44
+ required: ['pattern'],
45
+ },
46
+ async execute(args) {
47
+ if (typeof args.pattern !== 'string' || !args.pattern.trim()) {
48
+ return { error: 'Missing required "pattern" argument.' };
49
+ }
50
+ try {
51
+ new RegExp(args.pattern); // validity check only — matching happens in the vm (see MATCH_SCRIPT)
52
+ }
53
+ catch (e) {
54
+ return { error: `Invalid regex: ${e instanceof Error ? e.message : String(e)}` };
55
+ }
56
+ const includeRx = typeof args.include === 'string' && args.include.trim()
57
+ ? globToRegExp(args.include)
58
+ : null;
59
+ const startArg = typeof args.path === 'string' && args.path.trim() ? args.path : '.';
60
+ try {
61
+ const start = await resolveInWorkspace(root, startArg);
62
+ const out = [];
63
+ let truncated = false;
64
+ let timedOut = false;
65
+ const deadline = Date.now() + timeoutMs;
66
+ const ctx = vm.createContext(Object.create(null));
67
+ outer: for await (const f of walkFiles(root, start)) {
68
+ if (includeRx && !includeRx.test(path.basename(f.rel)))
69
+ continue;
70
+ const stat = await fs.stat(f.abs);
71
+ if (stat.size > MAX_FILE_SIZE_BYTES)
72
+ continue;
73
+ const buf = await fs.readFile(f.abs);
74
+ if (looksBinary(buf))
75
+ continue;
76
+ const lines = buf.toString('utf8').split('\n');
77
+ const remaining = deadline - Date.now();
78
+ if (remaining <= 0) {
79
+ timedOut = true;
80
+ break;
81
+ }
82
+ let hits;
83
+ try {
84
+ ctx.source = args.pattern;
85
+ ctx.lines = lines;
86
+ hits = MATCH_SCRIPT.runInContext(ctx, { timeout: Math.max(1, remaining) });
87
+ }
88
+ catch (err) {
89
+ if (err.code === 'ERR_SCRIPT_EXECUTION_TIMEOUT') {
90
+ timedOut = true;
91
+ break;
92
+ }
93
+ throw err;
94
+ }
95
+ for (const i of hits) {
96
+ const text = lines[i].length > MAX_GREP_LINE_CHARS
97
+ ? `${lines[i].slice(0, MAX_GREP_LINE_CHARS)}…`
98
+ : lines[i];
99
+ out.push(`${f.rel}:${i + 1}: ${text.trim()}`);
100
+ // Discovery results are cheap — stop at the cap and report truncation
101
+ // instead of erroring, saving the model a retry round-trip.
102
+ if (out.length >= MAX_GREP_MATCHES) {
103
+ truncated = true;
104
+ break outer;
105
+ }
106
+ }
107
+ }
108
+ if (timedOut) {
109
+ // Claude Code's RipgrepTimeoutError lesson: a timeout must be
110
+ // distinguishable from "no matches", or the model draws wrong conclusions.
111
+ const t = fmtTimeout(timeoutMs);
112
+ if (out.length === 0) {
113
+ return {
114
+ error: `Search timed out after ${t} before completing (the pattern may backtrack ` +
115
+ `catastrophically or the tree is large). This is NOT "no matches" — simplify ` +
116
+ 'the pattern or narrow the path/include filter and retry.',
117
+ };
118
+ }
119
+ const note = `(Search timed out after ${t} before completing — results above are PARTIAL. ` +
120
+ 'Simplify the pattern or narrow the path/include filter to search the rest.)';
121
+ return {
122
+ summary: `${out.length} match${out.length === 1 ? '' : 'es'} (timed out, partial)`,
123
+ content: [...out, note].join('\n'),
124
+ };
125
+ }
126
+ if (out.length === 0)
127
+ return { error: `No matches for: ${args.pattern}` };
128
+ if (truncated) {
129
+ const note = `(Results truncated at ${MAX_GREP_MATCHES} matches. Narrow the pattern, path, or include filter.)`;
130
+ return {
131
+ summary: `${MAX_GREP_MATCHES} matches (truncated)`,
132
+ content: [...out, note].join('\n'),
133
+ };
134
+ }
135
+ return { summary: `${out.length} match${out.length === 1 ? '' : 'es'}`, content: out.join('\n') };
136
+ }
137
+ catch (err) {
138
+ if (err instanceof GuardError)
139
+ return { error: err.message };
140
+ const message = err instanceof Error ? err.message : String(err);
141
+ return { error: `grep failed: ${sanitizeFsError(message, root)}` };
142
+ }
143
+ },
144
+ };
145
+ }
146
+ //# sourceMappingURL=grepTool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grepTool.js","sourceRoot":"","sources":["../../../src/fs/grepTool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /** Public surface of the fs read tools (phase A). */
2
+ import type { ClientTool } from '../core/types.js';
3
+ export { workspaceRoot } from './workspaceGuard.js';
4
+ export { MAX_TOOL_CALLS_PER_TURN } from './limits.js';
5
+ /** The four read-only tools, jailed to `root`. Registration order = model-visible order. */
6
+ /** The fs tool names — source of truth for context pruning's "prunable" check. */
7
+ export declare const FS_TOOL_NAMES: ReadonlySet<string>;
8
+ export declare function createFsTools(root: string): ClientTool[];
@@ -0,0 +1,13 @@
1
+ import { createReadFileTool } from './readFileTool.js';
2
+ import { createListDirTool } from './listDirTool.js';
3
+ import { createGlobTool } from './globTool.js';
4
+ import { createGrepTool } from './grepTool.js';
5
+ export { workspaceRoot } from './workspaceGuard.js';
6
+ export { MAX_TOOL_CALLS_PER_TURN } from './limits.js';
7
+ /** The four read-only tools, jailed to `root`. Registration order = model-visible order. */
8
+ /** The fs tool names — source of truth for context pruning's "prunable" check. */
9
+ export const FS_TOOL_NAMES = new Set(['read_file', 'list_dir', 'glob', 'grep']);
10
+ export function createFsTools(root) {
11
+ return [createReadFileTool(root), createListDirTool(root), createGlobTool(root), createGrepTool(root)];
12
+ }
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/fs/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAEtD,4FAA4F;AAC5F,kFAAkF;AAClF,MAAM,CAAC,MAAM,aAAa,GAAwB,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAErG,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,iBAAiB,CAAC,IAAI,CAAC,EAAE,cAAc,CAAC,IAAI,CAAC,EAAE,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC;AACzG,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Every cap and over-limit behavior for the fs read tools, in one auditable place.
3
+ * These are BILLING decisions, not implementation details, and the policy splits
4
+ * by content cost (2026-07-19 revision, Claude Code parity):
5
+ * - READ content (expensive, thousands of tokens): over-limit = ERROR (~100 bytes)
6
+ * — inherited from Claude Code's reverted experiment (#21841): truncation
7
+ * lowered tool error rates but RAISED mean token cost.
8
+ * - DISCOVERY results (cheap path/match lists): over-limit = TRUNCATE + note —
9
+ * erroring only wasted the model a retry round-trip.
10
+ */
11
+ export declare const MAX_FILE_SIZE_BYTES: number;
12
+ export declare const MAX_RANGED_FILE_SIZE_BYTES: number;
13
+ export declare const MAX_READ_LINES = 2000;
14
+ export declare const MAX_READ_OUTPUT_BYTES: number;
15
+ export declare const MAX_LINE_CHARS = 2000;
16
+ export declare const MAX_DIR_ENTRIES = 200;
17
+ export declare const MAX_GLOB_RESULTS = 100;
18
+ export declare const MAX_GLOB_SCAN = 2000;
19
+ export declare const MAX_GREP_MATCHES = 100;
20
+ export declare const MAX_GREP_LINE_CHARS = 200;
21
+ export declare const GREP_TIMEOUT_MS = 5000;
22
+ export declare const MAX_TOOL_CALLS_PER_TURN = 15;
23
+ /** Directory names never listed, walked, or matched (noise + huge). */
24
+ export declare const IGNORED_DIRS: Set<string>;
25
+ /** File names never listed or matched. */
26
+ export declare const IGNORED_FILES: Set<string>;
27
+ export declare const BUDGET_EXHAUSTED_MESSAGE = "Tool budget for this turn is exhausted. Summarize with what you have.";
28
+ export declare const PRUNE_TRIGGER_TOKENS = 30000;
29
+ export declare const PRUNE_KEEP_RECENT = 3;
30
+ export declare const PRUNE_MIN_CONTENT_BYTES = 200;
31
+ export declare const PRUNE_PLACEHOLDER = "[Old tool result cleared to save context \u2014 re-run the tool if needed.]";
32
+ export declare const DEFAULT_SESSION_MAX_CREDITS = 4000;
33
+ export declare const MEDIA_GRACE_WAIT_MS = 45000;
34
+ export declare const MEDIA_POLL_HINT_SECONDS = 0;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Every cap and over-limit behavior for the fs read tools, in one auditable place.
3
+ * These are BILLING decisions, not implementation details, and the policy splits
4
+ * by content cost (2026-07-19 revision, Claude Code parity):
5
+ * - READ content (expensive, thousands of tokens): over-limit = ERROR (~100 bytes)
6
+ * — inherited from Claude Code's reverted experiment (#21841): truncation
7
+ * lowered tool error rates but RAISED mean token cost.
8
+ * - DISCOVERY results (cheap path/match lists): over-limit = TRUNCATE + note —
9
+ * erroring only wasted the model a retry round-trip.
10
+ */
11
+ export const MAX_FILE_SIZE_BYTES = 1024 * 1024; // read_file / grep skip-threshold
12
+ export const MAX_RANGED_FILE_SIZE_BYTES = 8 * 1024 * 1024; // absolute ceiling — refuse even range reads above this
13
+ // NOTE 2026-07-19: the old DEFAULT_READ_LINES (1000-line default-read error) was removed.
14
+ // Dogfood showed it costs MORE than it saves: the model reads the whole file anyway via
15
+ // error + N range reads (4 round-trips for a 2226-line file). Default reads are now bounded
16
+ // by MAX_FILE_SIZE_BYTES (pre-read) and MAX_READ_OUTPUT_BYTES (post-build) only —
17
+ // Claude Code FileReadTool parity. Over-limit=error stays for READ content (expensive);
18
+ // discovery tools (glob/grep) truncate with a note instead (cheap path lists).
19
+ export const MAX_READ_LINES = 2000; // read_file explicit-limit ceiling
20
+ // 100KB ≈ 25k tokens — Claude Code FileReadTool's output-token cap, live-validated
21
+ // 2026-07-19: the old 256KB cap let a 118KB package-lock.json burn ~33k tokens
22
+ // that then sat in the session context, re-billed on every subsequent turn.
23
+ export const MAX_READ_OUTPUT_BYTES = 100 * 1024; // hard output ceiling for any read_file call
24
+ export const MAX_LINE_CHARS = 2000; // single line cap (truncated with marker)
25
+ export const MAX_DIR_ENTRIES = 200; // list_dir
26
+ export const MAX_GLOB_RESULTS = 100; // glob: shown results (over-cap truncates with a note)
27
+ export const MAX_GLOB_SCAN = 2000; // glob: hard bound on matches collected before sorting
28
+ export const MAX_GREP_MATCHES = 100; // grep
29
+ export const MAX_GREP_LINE_CHARS = 200; // grep match line preview cap
30
+ // grep executes MODEL-supplied regex; JS RegExp backtracks, so an evil pattern
31
+ // ((a+)+$) can hang V8 forever. Matching therefore runs inside a vm script whose
32
+ // timeout (V8 TerminateExecution) interrupts even mid-backtrack — verified live
33
+ // 2026-07-20. The budget is per grep CALL, and because vm runs synchronously it
34
+ // also bounds the worst-case main-thread stall: Claude Code allows 20s but its
35
+ // ripgrep runs out-of-process; in-process, 5s keeps a frozen TUI recoverable.
36
+ export const GREP_TIMEOUT_MS = 5_000;
37
+ export const MAX_TOOL_CALLS_PER_TURN = 15; // TurnLoop budget (counts ALL tools)
38
+ /** Directory names never listed, walked, or matched (noise + huge). */
39
+ export const IGNORED_DIRS = new Set([
40
+ 'node_modules',
41
+ '.git',
42
+ 'dist',
43
+ 'build',
44
+ '.next',
45
+ '__pycache__',
46
+ '.venv',
47
+ ]);
48
+ /** File names never listed or matched. */
49
+ export const IGNORED_FILES = new Set(['.DS_Store']);
50
+ export const BUDGET_EXHAUSTED_MESSAGE = 'Tool budget for this turn is exhausted. Summarize with what you have.';
51
+ // --- Context pruning (microcompact, spec 2026-07-19-context-pruning-design.md) ---
52
+ // Trigger-based batch clearing, NEVER a sliding window: editing an old message
53
+ // invalidates the upstream prompt cache from that point, so continuous pruning
54
+ // would re-bill the whole suffix at full upstream price every turn. User-facing
55
+ // billing is flat per prompt_tokens (verified in yanfa UsageCapture/settlement),
56
+ // so pruning is pure user savings; cache stability protects latency and margin.
57
+ export const PRUNE_TRIGGER_TOKENS = 30_000; // last prompt_tokens above this → prune batch
58
+ export const PRUNE_KEEP_RECENT = 3; // most recent fs results kept intact
59
+ export const PRUNE_MIN_CONTENT_BYTES = 200; // smaller results are cheaper kept than cleared
60
+ export const PRUNE_PLACEHOLDER = '[Old tool result cleared to save context — re-run the tool if needed.]';
61
+ // --- Unattended media budget (spec 2026-07-19-mcp-media-tools-design.md) ---
62
+ // MCP and exec have no human confirmation dialog; the session budget IS the spend
63
+ // control. 4000 ≈ ten default-model images (400 each) or 2-3 mid-range videos
64
+ // (500-1500 each) — measured live via `--estimate` on 2026-07-19.
65
+ export const DEFAULT_SESSION_MAX_CREDITS = 4000;
66
+ // Server-side wait slices: generate AND check_task each absorb up to this much waiting
67
+ // before answering "generating". Real-client finding (2026-07-20, headless Claude Code):
68
+ // agents without a sleep capability END THEIR TURN when told to wait between polls,
69
+ // orphaning the paid task — so the server waits and the agent re-calls immediately
70
+ // (poll hint 0). 45s slices stay inside conservative client timeouts; Claude Code's own
71
+ // MCP tool timeout defaults to ~27.8h, but other clients are far stingier.
72
+ export const MEDIA_GRACE_WAIT_MS = 45_000;
73
+ export const MEDIA_POLL_HINT_SECONDS = 0; // re-call immediately — the server does the waiting
74
+ //# sourceMappingURL=limits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limits.js","sourceRoot":"","sources":["../../../src/fs/limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,kCAAkC;AAClF,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,wDAAwD;AACnH,0FAA0F;AAC1F,wFAAwF;AACxF,4FAA4F;AAC5F,kFAAkF;AAClF,wFAAwF;AACxF,+EAA+E;AAC/E,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,CAAC,mCAAmC;AACvE,mFAAmF;AACnF,+EAA+E;AAC/E,4EAA4E;AAC5E,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC,6CAA6C;AAC9F,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,CAAC,0CAA0C;AAC9E,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAC,CAAC,WAAW;AAC/C,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAC,CAAC,uDAAuD;AAC5F,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC,CAAC,uDAAuD;AAC1F,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAC,CAAC,OAAO;AAC5C,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC,CAAC,8BAA8B;AACtE,+EAA+E;AAC/E,iFAAiF;AACjF,gFAAgF;AAChF,gFAAgF;AAChF,+EAA+E;AAC/E,8EAA8E;AAC9E,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC;AACrC,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,CAAC,CAAC,qCAAqC;AAEhF,uEAAuE;AACvE,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC;IAClC,cAAc;IACd,MAAM;IACN,MAAM;IACN,OAAO;IACP,OAAO;IACP,aAAa;IACb,OAAO;CACR,CAAC,CAAC;AACH,0CAA0C;AAC1C,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;AAEpD,MAAM,CAAC,MAAM,wBAAwB,GACnC,uEAAuE,CAAC;AAE1E,oFAAoF;AACpF,+EAA+E;AAC/E,+EAA+E;AAC/E,gFAAgF;AAChF,iFAAiF;AACjF,gFAAgF;AAChF,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC,CAAC,8CAA8C;AAC1F,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,CAAC,qCAAqC;AACzE,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC,CAAC,gDAAgD;AAC5F,MAAM,CAAC,MAAM,iBAAiB,GAC5B,wEAAwE,CAAC;AAE3E,8EAA8E;AAC9E,kFAAkF;AAClF,8EAA8E;AAC9E,kEAAkE;AAClE,MAAM,CAAC,MAAM,2BAA2B,GAAG,IAAI,CAAC;AAChD,uFAAuF;AACvF,yFAAyF;AACzF,oFAAoF;AACpF,mFAAmF;AACnF,wFAAwF;AACxF,2EAA2E;AAC3E,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAC1C,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,CAAC,oDAAoD"}
@@ -0,0 +1,2 @@
1
+ import type { ClientTool } from '../core/types.js';
2
+ export declare function createListDirTool(root: string): ClientTool;
@@ -0,0 +1,68 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { GuardError, resolveInWorkspace, sanitizeFsError } from './workspaceGuard.js';
4
+ import { IGNORED_DIRS, IGNORED_FILES, MAX_DIR_ENTRIES } from './limits.js';
5
+ /** 14 → "14B", 2048 → "2.0KB", 3145728 → "3.0MB". */
6
+ function formatSize(bytes) {
7
+ if (bytes < 1024)
8
+ return `${bytes}B`;
9
+ if (bytes < 1024 * 1024)
10
+ return `${(bytes / 1024).toFixed(1)}KB`;
11
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
12
+ }
13
+ export function createListDirTool(root) {
14
+ return {
15
+ name: 'list_dir',
16
+ readOnly: true,
17
+ description: 'List the entries of a directory inside the workspace (the directory the CLI was started in). ' +
18
+ 'Directories are suffixed with "/"; files include their size, so questions like "the largest file" ' +
19
+ 'are answerable directly. Common noise (node_modules, .git, dist) is omitted. ' +
20
+ 'Use this to explore before reading files.',
21
+ parameters: {
22
+ type: 'object',
23
+ properties: {
24
+ path: {
25
+ type: 'string',
26
+ description: 'Directory to list, relative to the workspace root. Defaults to ".".',
27
+ },
28
+ },
29
+ },
30
+ async execute(args) {
31
+ const userPath = typeof args.path === 'string' && args.path.trim() ? args.path : '.';
32
+ try {
33
+ const dir = await resolveInWorkspace(root, userPath);
34
+ const stat = await fs.stat(dir);
35
+ if (!stat.isDirectory())
36
+ return { error: `Not a directory: ${userPath}` };
37
+ const entries = (await fs.readdir(dir, { withFileTypes: true }))
38
+ .filter((e) => !(e.isDirectory() ? IGNORED_DIRS.has(e.name) : IGNORED_FILES.has(e.name)));
39
+ if (entries.length > MAX_DIR_ENTRIES) {
40
+ return {
41
+ error: `Directory has ${entries.length} entries (>${MAX_DIR_ENTRIES}). List a subdirectory or use glob with a narrower pattern.`,
42
+ };
43
+ }
44
+ entries.sort((a, b) => a.isDirectory() === b.isDirectory() ? a.name.localeCompare(b.name) : a.isDirectory() ? -1 : 1);
45
+ const lines = await Promise.all(entries.map(async (e) => {
46
+ if (e.isDirectory())
47
+ return `${e.name}/`;
48
+ try {
49
+ const s = await fs.stat(path.join(dir, e.name));
50
+ return `${e.name} ${formatSize(s.size)}`;
51
+ }
52
+ catch {
53
+ // Dangling symlink or a file deleted mid-listing: name only, never fatal.
54
+ return e.name;
55
+ }
56
+ }));
57
+ return { summary: `${lines.length} entries`, content: lines.join('\n') };
58
+ }
59
+ catch (err) {
60
+ if (err instanceof GuardError)
61
+ return { error: err.message };
62
+ const message = err instanceof Error ? err.message : String(err);
63
+ return { error: `list_dir failed: ${sanitizeFsError(message, root)}` };
64
+ }
65
+ },
66
+ };
67
+ }
68
+ //# sourceMappingURL=listDirTool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listDirTool.js","sourceRoot":"","sources":["../../../src/fs/listDirTool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { ClientTool } from '../core/types.js';
2
+ export declare function createReadFileTool(root: string): ClientTool;
@@ -0,0 +1,102 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { GuardError, resolveInWorkspace, sanitizeFsError } from './workspaceGuard.js';
3
+ import { MAX_FILE_SIZE_BYTES, MAX_LINE_CHARS, MAX_RANGED_FILE_SIZE_BYTES, MAX_READ_LINES, MAX_READ_OUTPUT_BYTES, } from './limits.js';
4
+ /** NUL byte in the first 8KB → treat as binary (same heuristic as git). */
5
+ function looksBinary(buf) {
6
+ return buf.subarray(0, 8192).includes(0);
7
+ }
8
+ export function createReadFileTool(root) {
9
+ return {
10
+ name: 'read_file',
11
+ readOnly: true,
12
+ description: 'Read a text file inside the workspace. Returns numbered lines ("cat -n" style) so you can cite line numbers. ' +
13
+ 'Reads the whole file by default when it fits (file up to 1MB, output up to 100KB); for bigger files ' +
14
+ `read a range with offset/limit (up to ${MAX_READ_LINES} lines per read; range reads work on files up to 8MB). ` +
15
+ 'For machine-generated files (lockfiles, minified bundles, large logs) read a small range or grep ' +
16
+ 'instead of the whole file — their bulk rarely helps and the tokens are wasted. ' +
17
+ 'Binary files cannot be read — images can be attached instead (@path in the TUI, --image in exec).',
18
+ parameters: {
19
+ type: 'object',
20
+ properties: {
21
+ path: { type: 'string', description: 'File path relative to the workspace root.' },
22
+ offset: { type: 'number', description: '1-based first line to read (default 1).' },
23
+ limit: {
24
+ type: 'number',
25
+ description: `Max lines to return (up to ${MAX_READ_LINES}). Omit to read the whole file.`,
26
+ },
27
+ },
28
+ required: ['path'],
29
+ },
30
+ async execute(args) {
31
+ if (typeof args.path !== 'string' || !args.path.trim()) {
32
+ return { error: 'Missing required "path" argument.' };
33
+ }
34
+ const explicitLimit = typeof args.limit === 'number' && args.limit > 0;
35
+ const offset = typeof args.offset === 'number' && args.offset >= 1 ? Math.floor(args.offset) : 1;
36
+ const limit = explicitLimit ? Math.floor(args.limit) : undefined;
37
+ if (explicitLimit && limit !== undefined && limit > MAX_READ_LINES) {
38
+ return {
39
+ error: `Requested ${limit} lines (max ${MAX_READ_LINES} per read). Read in chunks with offset/limit.`,
40
+ };
41
+ }
42
+ try {
43
+ const file = await resolveInWorkspace(root, args.path);
44
+ const stat = await fs.stat(file);
45
+ if (stat.isDirectory())
46
+ return { error: `Is a directory, not a file: ${args.path}` };
47
+ // A FIFO/socket/device would make fs.readFile hang forever waiting for a peer — and
48
+ // dispatch here is not abortable. Reject anything that isn't a plain file up front.
49
+ if (!stat.isFile())
50
+ return { error: `Not a regular file: ${args.path}` };
51
+ if (explicitLimit) {
52
+ if (stat.size > MAX_RANGED_FILE_SIZE_BYTES) {
53
+ return {
54
+ error: `File is ${stat.size} bytes (>8MB) — too large even for range reads. Use grep to locate the relevant part.`,
55
+ };
56
+ }
57
+ }
58
+ else if (stat.size > MAX_FILE_SIZE_BYTES) {
59
+ return {
60
+ error: `File is ${stat.size} bytes (>1MB limit). Use grep to find the relevant part, or read a line range with offset/limit.`,
61
+ };
62
+ }
63
+ const buf = await fs.readFile(file);
64
+ if (looksBinary(buf)) {
65
+ return {
66
+ error: 'Binary file (not text). If this is an image, attach it instead (@path in the TUI, --image in exec).',
67
+ };
68
+ }
69
+ const all = buf.toString('utf8').split('\n');
70
+ // Trailing newline produces a phantom empty last element; drop it.
71
+ if (all.length > 1 && all[all.length - 1] === '')
72
+ all.pop();
73
+ // No default line-count gate: a default read returns the whole file, bounded by the
74
+ // 1MB pre-read and 100KB output checks. Erroring at N lines just forced the model
75
+ // into error + multiple range reads for the same total tokens (2026-07-19 dogfood).
76
+ const slice = limit === undefined ? all.slice(offset - 1) : all.slice(offset - 1, offset - 1 + limit);
77
+ if (slice.length === 0)
78
+ return { error: `Offset ${offset} is past the end (${all.length} lines).` };
79
+ const content = slice
80
+ .map((line, i) => {
81
+ const text = line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS)}… [line truncated]` : line;
82
+ return `${String(offset + i).padStart(6)}\t${text}`;
83
+ })
84
+ .join('\n');
85
+ const outputBytes = Buffer.byteLength(content, 'utf8');
86
+ if (outputBytes > MAX_READ_OUTPUT_BYTES) {
87
+ return {
88
+ error: `This range is ${outputBytes} bytes (max ${MAX_READ_OUTPUT_BYTES} per read). Request a smaller range with offset/limit.`,
89
+ };
90
+ }
91
+ return { summary: `${slice.length} lines`, content };
92
+ }
93
+ catch (err) {
94
+ if (err instanceof GuardError)
95
+ return { error: err.message };
96
+ const message = err instanceof Error ? err.message : String(err);
97
+ return { error: `read_file failed: ${sanitizeFsError(message, root)}` };
98
+ }
99
+ },
100
+ };
101
+ }
102
+ //# sourceMappingURL=readFileTool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readFileTool.js","sourceRoot":"","sources":["../../../src/fs/readFileTool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Depth-first file walker shared by glob and grep. Yields regular files only,
3
+ * with workspace-relative paths. Skips IGNORED_DIRS/IGNORED_FILES. Symlinks —
4
+ * file or directory — are never yielded or followed (Dirent.isFile() is false
5
+ * for them): consumers read yielded paths directly without a guard re-check,
6
+ * so a yielded symlink could leak content from outside the workspace.
7
+ */
8
+ export declare function walkFiles(root: string, startDir: string): AsyncGenerator<{
9
+ abs: string;
10
+ rel: string;
11
+ }>;
@@ -0,0 +1,38 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { IGNORED_DIRS, IGNORED_FILES } from './limits.js';
4
+ /**
5
+ * Depth-first file walker shared by glob and grep. Yields regular files only,
6
+ * with workspace-relative paths. Skips IGNORED_DIRS/IGNORED_FILES. Symlinks —
7
+ * file or directory — are never yielded or followed (Dirent.isFile() is false
8
+ * for them): consumers read yielded paths directly without a guard re-check,
9
+ * so a yielded symlink could leak content from outside the workspace.
10
+ */
11
+ export async function* walkFiles(root, startDir) {
12
+ let entries;
13
+ try {
14
+ entries = await fs.readdir(startDir, { withFileTypes: true });
15
+ }
16
+ catch {
17
+ // An unreadable directory (EPERM/EACCES on macOS ~/.Trash, vanished mid-walk)
18
+ // is skipped, not fatal — a home-dir workspace is full of these and one must
19
+ // not kill the whole glob/grep call (2026-07-19 TUI smoke finding).
20
+ return;
21
+ }
22
+ entries.sort((a, b) => a.name.localeCompare(b.name));
23
+ for (const e of entries) {
24
+ const abs = path.join(startDir, e.name);
25
+ if (e.isDirectory()) {
26
+ if (IGNORED_DIRS.has(e.name))
27
+ continue;
28
+ yield* walkFiles(root, abs);
29
+ }
30
+ else if (e.isFile()) {
31
+ if (IGNORED_FILES.has(e.name))
32
+ continue;
33
+ yield { abs, rel: path.relative(root, abs) };
34
+ }
35
+ // symlinks (isSymbolicLink) are intentionally not followed here
36
+ }
37
+ }
38
+ //# sourceMappingURL=walk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"walk.js","sourceRoot":"","sources":["../../../src/fs/walk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE1D;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,SAAS,CAC9B,IAAY,EACZ,QAAgB;IAEhB,IAAI,OAAO,CAAC;IACZ,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,8EAA8E;QAC9E,6EAA6E;QAC7E,oEAAoE;QACpE,OAAO;IACT,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACrD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;YACpB,IAAI,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;gBAAE,SAAS;YACvC,KAAK,CAAC,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC9B,CAAC;aAAM,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;YACtB,IAAI,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;gBAAE,SAAS;YACxC,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;QAC/C,CAAC;QACD,gEAAgE;IAClE,CAAC;AACH,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The ONLY safety module for the fs read tools. Every path a tool touches goes
3
+ * through resolveInWorkspace(); the tools themselves contain zero safety code.
4
+ * Phase-B write tools MUST route through this same gate.
5
+ */
6
+ /** A path-safety violation or lookup failure, with a model-readable message. */
7
+ export declare class GuardError extends Error {
8
+ }
9
+ /** The workspace root: realpath of the CLI start directory, anchored ONCE at startup. */
10
+ export declare function workspaceRoot(cwd?: string): Promise<string>;
11
+ /**
12
+ * Resolve a user/model-supplied path and prove it stays inside the workspace.
13
+ *
14
+ * Order matters: resolve against root -> realpath (follows symlinks) -> prefix
15
+ * check WITH a trailing separator (so `/ws-evil` cannot pass as `/ws`). The
16
+ * denial message never echoes the resolved real path — that would leak
17
+ * outside-workspace structure to the model.
18
+ */
19
+ export declare function resolveInWorkspace(root: string, userPath: string): Promise<string>;
20
+ /**
21
+ * Strip the workspace root out of a raw error message before it reaches the model. Node fs
22
+ * errors embed the absolute path (e.g. `EACCES: permission denied, open '/Users/eric/proj/f'`),
23
+ * which would otherwise leak the local machine's directory layout and username to the backend.
24
+ * Used by every tool's generic catch-all — GuardError messages never need this, they are
25
+ * already written to omit the resolved real path (see DENIED above).
26
+ */
27
+ export declare function sanitizeFsError(message: string, root: string): string;