@glbgpt/cli 0.1.0-test.5 → 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.
- package/README.md +79 -6
- package/dist/src/auth/authClient.d.ts +21 -2
- package/dist/src/auth/authClient.js +62 -2
- package/dist/src/auth/authClient.js.map +1 -1
- package/dist/src/auth/loginFlow.js +5 -2
- package/dist/src/auth/loginFlow.js.map +1 -1
- package/dist/src/auth/loopbackServer.d.ts +2 -1
- package/dist/src/auth/loopbackServer.js +6 -3
- package/dist/src/auth/loopbackServer.js.map +1 -1
- package/dist/src/commands/accountCommand.js +10 -4
- package/dist/src/commands/accountCommand.js.map +1 -1
- package/dist/src/commands/configCommand.d.ts +5 -0
- package/dist/src/commands/configCommand.js +15 -2
- package/dist/src/commands/configCommand.js.map +1 -1
- package/dist/src/commands/execCommand.d.ts +6 -0
- package/dist/src/commands/execCommand.js +61 -9
- package/dist/src/commands/execCommand.js.map +1 -1
- package/dist/src/commands/loginCommand.js +9 -6
- package/dist/src/commands/loginCommand.js.map +1 -1
- package/dist/src/commands/mediaCommand.js +3 -2
- package/dist/src/commands/mediaCommand.js.map +1 -1
- package/dist/src/commands/skillCommand.d.ts +30 -0
- package/dist/src/commands/skillCommand.js +160 -0
- package/dist/src/commands/skillCommand.js.map +1 -0
- package/dist/src/config/configFile.d.ts +12 -0
- package/dist/src/config/configFile.js +12 -0
- package/dist/src/config/configFile.js.map +1 -1
- package/dist/src/config/env.d.ts +18 -0
- package/dist/src/config/env.js +57 -7
- package/dist/src/config/env.js.map +1 -1
- package/dist/src/core/catalog.d.ts +8 -1
- package/dist/src/core/catalog.js +13 -2
- package/dist/src/core/catalog.js.map +1 -1
- package/dist/src/core/contextPrune.d.ts +13 -0
- package/dist/src/core/contextPrune.js +61 -0
- package/dist/src/core/contextPrune.js.map +1 -0
- package/dist/src/core/httpChatBackend.js +22 -5
- package/dist/src/core/httpChatBackend.js.map +1 -1
- package/dist/src/core/runChat.d.ts +9 -5
- package/dist/src/core/runChat.js +10 -7
- package/dist/src/core/runChat.js.map +1 -1
- package/dist/src/core/session.d.ts +11 -0
- package/dist/src/core/session.js +20 -1
- package/dist/src/core/session.js.map +1 -1
- package/dist/src/core/toolRegistry.d.ts +14 -3
- package/dist/src/core/toolRegistry.js +24 -15
- package/dist/src/core/toolRegistry.js.map +1 -1
- package/dist/src/core/turnLoop.d.ts +4 -0
- package/dist/src/core/turnLoop.js +54 -13
- package/dist/src/core/turnLoop.js.map +1 -1
- package/dist/src/core/types.d.ts +6 -0
- package/dist/src/core/updateCheck.d.ts +46 -0
- package/dist/src/core/updateCheck.js +139 -0
- package/dist/src/core/updateCheck.js.map +1 -0
- package/dist/src/exec/media.d.ts +32 -0
- package/dist/src/exec/media.js +70 -0
- package/dist/src/exec/media.js.map +1 -0
- package/dist/src/exec/output.d.ts +16 -0
- package/dist/src/exec/output.js +41 -0
- package/dist/src/exec/output.js.map +1 -1
- package/dist/src/fs/globTool.d.ts +8 -0
- package/dist/src/fs/globTool.js +117 -0
- package/dist/src/fs/globTool.js.map +1 -0
- package/dist/src/fs/grepTool.d.ts +4 -0
- package/dist/src/fs/grepTool.js +146 -0
- package/dist/src/fs/grepTool.js.map +1 -0
- package/dist/src/fs/index.d.ts +8 -0
- package/dist/src/fs/index.js +13 -0
- package/dist/src/fs/index.js.map +1 -0
- package/dist/src/fs/limits.d.ts +34 -0
- package/dist/src/fs/limits.js +74 -0
- package/dist/src/fs/limits.js.map +1 -0
- package/dist/src/fs/listDirTool.d.ts +2 -0
- package/dist/src/fs/listDirTool.js +68 -0
- package/dist/src/fs/listDirTool.js.map +1 -0
- package/dist/src/fs/readFileTool.d.ts +2 -0
- package/dist/src/fs/readFileTool.js +102 -0
- package/dist/src/fs/readFileTool.js.map +1 -0
- package/dist/src/fs/walk.d.ts +11 -0
- package/dist/src/fs/walk.js +38 -0
- package/dist/src/fs/walk.js.map +1 -0
- package/dist/src/fs/workspaceGuard.d.ts +27 -0
- package/dist/src/fs/workspaceGuard.js +96 -0
- package/dist/src/fs/workspaceGuard.js.map +1 -0
- package/dist/src/index.js +47 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/mediaTools.d.ts +63 -0
- package/dist/src/mcp/mediaTools.js +385 -0
- package/dist/src/mcp/mediaTools.js.map +1 -0
- package/dist/src/mcp/server.d.ts +6 -2
- package/dist/src/mcp/server.js +19 -9
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/media/aiApiClient.d.ts +5 -0
- package/dist/src/media/aiApiClient.js +12 -1
- package/dist/src/media/aiApiClient.js.map +1 -1
- package/dist/src/media/budget.d.ts +42 -0
- package/dist/src/media/budget.js +95 -0
- package/dist/src/media/budget.js.map +1 -0
- package/dist/src/media/executor.d.ts +9 -0
- package/dist/src/media/executor.js +1 -1
- package/dist/src/media/executor.js.map +1 -1
- package/dist/src/media/generateImageTool.d.ts +8 -2
- package/dist/src/media/generateImageTool.js +51 -3
- package/dist/src/media/generateImageTool.js.map +1 -1
- package/dist/src/media/promptSanitizer.d.ts +11 -0
- package/dist/src/media/promptSanitizer.js +33 -0
- package/dist/src/media/promptSanitizer.js.map +1 -0
- package/dist/src/media/taskManager.d.ts +8 -1
- package/dist/src/media/taskManager.js +1 -1
- package/dist/src/media/taskManager.js.map +1 -1
- package/dist/src/tui/App.js +30 -21
- package/dist/src/tui/App.js.map +1 -1
- package/dist/src/tui/ParamEditor.d.ts +11 -2
- package/dist/src/tui/ParamEditor.js +17 -6
- package/dist/src/tui/ParamEditor.js.map +1 -1
- package/dist/src/tui/atPopup.d.ts +8 -0
- package/dist/src/tui/atPopup.js +1 -1
- package/dist/src/tui/atPopup.js.map +1 -1
- package/dist/src/tui/chatController.d.ts +58 -2
- package/dist/src/tui/chatController.js +231 -40
- package/dist/src/tui/chatController.js.map +1 -1
- package/dist/src/tui/models.d.ts +6 -0
- package/dist/src/tui/models.js +14 -6
- package/dist/src/tui/models.js.map +1 -1
- package/dist/src/tui/run.js +50 -16
- package/dist/src/tui/run.js.map +1 -1
- package/dist/src/util/brokenPipe.d.ts +10 -0
- package/dist/src/util/brokenPipe.js +19 -0
- package/dist/src/util/brokenPipe.js.map +1 -0
- package/package.json +3 -2
- package/skills/globalgpt/SKILL.md +94 -0
- package/skills/globalgpt/references/troubleshooting.md +34 -0
- package/skills/globalgpt-coding/SKILL.md +65 -0
- package/skills/globalgpt-coding/references/troubleshooting.md +34 -0
- package/skills/globalgpt-image/SKILL.md +109 -0
- package/skills/globalgpt-image/references/recipes.md +52 -0
- package/skills/globalgpt-image/references/troubleshooting.md +49 -0
- package/skills/globalgpt-video/SKILL.md +109 -0
- package/skills/globalgpt-video/references/recipes.md +50 -0
- package/skills/globalgpt-video/references/troubleshooting.md +49 -0
|
@@ -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,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,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;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* The ONLY safety module for the fs read tools. Every path a tool touches goes
|
|
5
|
+
* through resolveInWorkspace(); the tools themselves contain zero safety code.
|
|
6
|
+
* Phase-B write tools MUST route through this same gate.
|
|
7
|
+
*/
|
|
8
|
+
/** A path-safety violation or lookup failure, with a model-readable message. */
|
|
9
|
+
export class GuardError extends Error {
|
|
10
|
+
}
|
|
11
|
+
const DENIED = 'Access denied: path is outside the workspace (CLI start directory)';
|
|
12
|
+
/** The workspace root: realpath of the CLI start directory, anchored ONCE at startup. */
|
|
13
|
+
export async function workspaceRoot(cwd = process.cwd()) {
|
|
14
|
+
return fs.realpath(cwd);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Walk up from `dir` to the nearest ancestor that actually exists on disk and
|
|
18
|
+
* return ITS realpath. Used by the ENOENT branch below: the lexical candidate
|
|
19
|
+
* cannot be trusted for the containment check because a directory component
|
|
20
|
+
* partway up the path may itself be a symlink pointing outside the
|
|
21
|
+
* workspace — realpath-ing only the missing leaf would miss that.
|
|
22
|
+
*/
|
|
23
|
+
async function nearestExistingAncestorRealpath(dir) {
|
|
24
|
+
for (;;) {
|
|
25
|
+
try {
|
|
26
|
+
return await fs.realpath(dir);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
if (err.code !== 'ENOENT') {
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
const parent = path.dirname(dir);
|
|
33
|
+
if (parent === dir) {
|
|
34
|
+
// Reached the filesystem root without finding an existing ancestor.
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
dir = parent;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a user/model-supplied path and prove it stays inside the workspace.
|
|
43
|
+
*
|
|
44
|
+
* Order matters: resolve against root -> realpath (follows symlinks) -> prefix
|
|
45
|
+
* check WITH a trailing separator (so `/ws-evil` cannot pass as `/ws`). The
|
|
46
|
+
* denial message never echoes the resolved real path — that would leak
|
|
47
|
+
* outside-workspace structure to the model.
|
|
48
|
+
*/
|
|
49
|
+
export async function resolveInWorkspace(root, userPath) {
|
|
50
|
+
const candidate = path.resolve(root, userPath);
|
|
51
|
+
let real;
|
|
52
|
+
try {
|
|
53
|
+
real = await fs.realpath(candidate);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
if (err.code === 'ENOENT') {
|
|
57
|
+
// Read-only tools never need nonexistent paths — but only say "not found"
|
|
58
|
+
// when the path would have been INSIDE the workspace; otherwise deny, so
|
|
59
|
+
// probing outside paths cannot distinguish "missing" from "forbidden".
|
|
60
|
+
//
|
|
61
|
+
// The lexical `candidate` is not trustworthy for that decision: a
|
|
62
|
+
// directory earlier in the path may itself be a symlink escaping the
|
|
63
|
+
// workspace (e.g. `root/escapedir -> /outside`), in which case probing
|
|
64
|
+
// `escapedir/missing.txt` vs `escapedir/exists.txt` would otherwise leak
|
|
65
|
+
// whether files exist outside the workspace. So resolve the nearest
|
|
66
|
+
// ancestor that actually exists and judge containment from ITS realpath.
|
|
67
|
+
let ancestorReal;
|
|
68
|
+
try {
|
|
69
|
+
ancestorReal = await nearestExistingAncestorRealpath(path.dirname(candidate));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new GuardError(DENIED);
|
|
73
|
+
}
|
|
74
|
+
if (ancestorReal === root || ancestorReal.startsWith(root + path.sep)) {
|
|
75
|
+
throw new GuardError(`Not found: ${userPath}`);
|
|
76
|
+
}
|
|
77
|
+
throw new GuardError(DENIED);
|
|
78
|
+
}
|
|
79
|
+
throw new GuardError(`Cannot access: ${userPath}`);
|
|
80
|
+
}
|
|
81
|
+
if (real !== root && !real.startsWith(root + path.sep)) {
|
|
82
|
+
throw new GuardError(DENIED);
|
|
83
|
+
}
|
|
84
|
+
return real;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Strip the workspace root out of a raw error message before it reaches the model. Node fs
|
|
88
|
+
* errors embed the absolute path (e.g. `EACCES: permission denied, open '/Users/eric/proj/f'`),
|
|
89
|
+
* which would otherwise leak the local machine's directory layout and username to the backend.
|
|
90
|
+
* Used by every tool's generic catch-all — GuardError messages never need this, they are
|
|
91
|
+
* already written to omit the resolved real path (see DENIED above).
|
|
92
|
+
*/
|
|
93
|
+
export function sanitizeFsError(message, root) {
|
|
94
|
+
return message.split(root).join('.');
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=workspaceGuard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspaceGuard.js","sourceRoot":"","sources":["../../../src/fs/workspaceGuard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;;GAIG;AAEH,gFAAgF;AAChF,MAAM,OAAO,UAAW,SAAQ,KAAK;CAAG;AAExC,MAAM,MAAM,GACV,oEAAoE,CAAC;AAEvE,yFAAyF;AACzF,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IAC7D,OAAO,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,+BAA+B,CAAC,GAAW;IACxD,SAAS,CAAC;QACR,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAChC,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACrD,MAAM,GAAG,CAAC;YACZ,CAAC;YACD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;gBACnB,oEAAoE;gBACpE,MAAM,GAAG,CAAC;YACZ,CAAC;YACD,GAAG,GAAG,MAAM,CAAC;QACf,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,IAAY,EAAE,QAAgB;IACrE,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC/C,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IACtC,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACtB,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACrD,0EAA0E;YAC1E,yEAAyE;YACzE,uEAAuE;YACvE,EAAE;YACF,kEAAkE;YAClE,qEAAqE;YACrE,uEAAuE;YACvE,yEAAyE;YACzE,oEAAoE;YACpE,yEAAyE;YACzE,IAAI,YAAoB,CAAC;YACzB,IAAI,CAAC;gBACH,YAAY,GAAG,MAAM,+BAA+B,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;YAChF,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;YAC/B,CAAC;YACD,IAAI,YAAY,KAAK,IAAI,IAAI,YAAY,CAAC,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBACtE,MAAM,IAAI,UAAU,CAAC,cAAc,QAAQ,EAAE,CAAC,CAAC;YACjD,CAAC;YACD,MAAM,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;QAC/B,CAAC;QACD,MAAM,IAAI,UAAU,CAAC,kBAAkB,QAAQ,EAAE,CAAC,CAAC;IACrD,CAAC;IACD,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACvD,MAAM,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe,EAAE,IAAY;IAC3D,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACvC,CAAC"}
|
package/dist/src/index.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { basename } from 'node:path';
|
|
4
4
|
import { VERSION } from './version.js';
|
|
5
|
+
import { guardBrokenPipe } from './util/brokenPipe.js';
|
|
6
|
+
// `glbgpt exec ... | head` must end quietly when the pipe closes, not crash (EPIPE).
|
|
7
|
+
guardBrokenPipe(process.stdout);
|
|
8
|
+
guardBrokenPipe(process.stderr);
|
|
5
9
|
/**
|
|
6
10
|
* glbgpt CLI entry (scaffold).
|
|
7
11
|
*
|
|
@@ -15,6 +19,18 @@ const program = new Command();
|
|
|
15
19
|
// The CLI installs two bins — `glbgpt` and its alias `globalgpt` — so help/usage should
|
|
16
20
|
// name whichever the user actually invoked (fall back to the canonical name).
|
|
17
21
|
const invokedName = process.argv[1] ? basename(process.argv[1]).replace(/\.js$/, '') : 'glbgpt';
|
|
22
|
+
/**
|
|
23
|
+
* Parse the raw `--max-credits` flag value from commander. `undefined` (flag omitted) passes
|
|
24
|
+
* through unchanged; a provided value that isn't a non-negative integer is a usage error
|
|
25
|
+
* ('invalid') the caller must reject before running the command — 0 itself is valid and means
|
|
26
|
+
* "no media spending this session" (resolveMaxCredits/SessionBudget handle that).
|
|
27
|
+
*/
|
|
28
|
+
function parseMaxCreditsFlag(raw) {
|
|
29
|
+
if (raw === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
const n = Number(raw);
|
|
32
|
+
return Number.isFinite(n) && Number.isInteger(n) && n >= 0 ? n : 'invalid';
|
|
33
|
+
}
|
|
18
34
|
program
|
|
19
35
|
.name(invokedName === 'globalgpt' ? 'globalgpt' : 'glbgpt')
|
|
20
36
|
.description('GlobalGPT in your terminal: chat, image and video generation.')
|
|
@@ -67,7 +83,14 @@ program
|
|
|
67
83
|
.option('--json', 'Emit a single final result object to stdout')
|
|
68
84
|
.option('--jsonl', 'Emit streaming protocol events, one JSON per line')
|
|
69
85
|
.option('-o, --output-last-message <file>', 'Write the final message text to a file')
|
|
86
|
+
.option('--max-credits <n>', 'Session credit budget for media generation (default 4000; 0 disables media generation)')
|
|
70
87
|
.action(async (prompt, opts) => {
|
|
88
|
+
const maxCredits = parseMaxCreditsFlag(opts.maxCredits);
|
|
89
|
+
if (maxCredits === 'invalid') {
|
|
90
|
+
console.error('Invalid --max-credits: must be a non-negative integer.');
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
71
94
|
const { runExec } = await import('./commands/execCommand.js');
|
|
72
95
|
const code = await runExec(prompt, {
|
|
73
96
|
model: opts.model,
|
|
@@ -78,15 +101,36 @@ program
|
|
|
78
101
|
json: opts.json,
|
|
79
102
|
jsonl: opts.jsonl,
|
|
80
103
|
outputLastMessage: opts.outputLastMessage,
|
|
104
|
+
maxCredits,
|
|
81
105
|
});
|
|
82
106
|
process.exitCode = code;
|
|
83
107
|
});
|
|
84
108
|
program
|
|
85
109
|
.command('mcp')
|
|
86
|
-
.description('Run an MCP server (stdio) exposing GlobalGPT chat
|
|
87
|
-
.
|
|
110
|
+
.description('Run an MCP server (stdio) exposing GlobalGPT chat and media generation to external agents')
|
|
111
|
+
.option('--max-credits <n>', 'Session credit budget for media generation (default 4000; 0 disables media generation)')
|
|
112
|
+
.action(async (opts) => {
|
|
113
|
+
const maxCredits = parseMaxCreditsFlag(opts.maxCredits);
|
|
114
|
+
if (maxCredits === 'invalid') {
|
|
115
|
+
console.error('Invalid --max-credits: must be a non-negative integer.');
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
88
119
|
const { runMcpServer } = await import('./mcp/server.js');
|
|
89
|
-
await runMcpServer();
|
|
120
|
+
await runMcpServer({ maxCredits });
|
|
121
|
+
});
|
|
122
|
+
const skill = program.command('skill').description('Manage the bundled Agent Skills for coding agents');
|
|
123
|
+
skill
|
|
124
|
+
.command('install')
|
|
125
|
+
.description('Install the bundled skills into your agent skill directories')
|
|
126
|
+
.option('--skill <name>', 'Install only this skill (default: all)')
|
|
127
|
+
.option('--copy', 'Copy files instead of symlinking')
|
|
128
|
+
.action(async (opts) => {
|
|
129
|
+
const { runSkillInstall } = await import('./commands/skillCommand.js');
|
|
130
|
+
process.exitCode = await runSkillInstall({
|
|
131
|
+
skill: opts.skill,
|
|
132
|
+
copy: opts.copy,
|
|
133
|
+
});
|
|
90
134
|
});
|
|
91
135
|
program
|
|
92
136
|
.command('resume [target]')
|