@animus-labs/cortex 0.2.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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/budget-guard.d.ts +75 -0
- package/dist/budget-guard.d.ts.map +1 -0
- package/dist/budget-guard.js +142 -0
- package/dist/budget-guard.js.map +1 -0
- package/dist/compaction/compaction.d.ts +99 -0
- package/dist/compaction/compaction.d.ts.map +1 -0
- package/dist/compaction/compaction.js +302 -0
- package/dist/compaction/compaction.js.map +1 -0
- package/dist/compaction/failsafe.d.ts +57 -0
- package/dist/compaction/failsafe.d.ts.map +1 -0
- package/dist/compaction/failsafe.js +135 -0
- package/dist/compaction/failsafe.js.map +1 -0
- package/dist/compaction/index.d.ts +381 -0
- package/dist/compaction/index.d.ts.map +1 -0
- package/dist/compaction/index.js +979 -0
- package/dist/compaction/index.js.map +1 -0
- package/dist/compaction/microcompaction.d.ts +219 -0
- package/dist/compaction/microcompaction.d.ts.map +1 -0
- package/dist/compaction/microcompaction.js +536 -0
- package/dist/compaction/microcompaction.js.map +1 -0
- package/dist/compaction/observational/buffering.d.ts +225 -0
- package/dist/compaction/observational/buffering.d.ts.map +1 -0
- package/dist/compaction/observational/buffering.js +354 -0
- package/dist/compaction/observational/buffering.js.map +1 -0
- package/dist/compaction/observational/constants.d.ts +70 -0
- package/dist/compaction/observational/constants.d.ts.map +1 -0
- package/dist/compaction/observational/constants.js +507 -0
- package/dist/compaction/observational/constants.js.map +1 -0
- package/dist/compaction/observational/index.d.ts +219 -0
- package/dist/compaction/observational/index.d.ts.map +1 -0
- package/dist/compaction/observational/index.js +641 -0
- package/dist/compaction/observational/index.js.map +1 -0
- package/dist/compaction/observational/observer.d.ts +97 -0
- package/dist/compaction/observational/observer.d.ts.map +1 -0
- package/dist/compaction/observational/observer.js +424 -0
- package/dist/compaction/observational/observer.js.map +1 -0
- package/dist/compaction/observational/recall-tool.d.ts +27 -0
- package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
- package/dist/compaction/observational/recall-tool.js +93 -0
- package/dist/compaction/observational/recall-tool.js.map +1 -0
- package/dist/compaction/observational/reflector.d.ts +94 -0
- package/dist/compaction/observational/reflector.d.ts.map +1 -0
- package/dist/compaction/observational/reflector.js +167 -0
- package/dist/compaction/observational/reflector.js.map +1 -0
- package/dist/compaction/observational/types.d.ts +271 -0
- package/dist/compaction/observational/types.d.ts.map +1 -0
- package/dist/compaction/observational/types.js +15 -0
- package/dist/compaction/observational/types.js.map +1 -0
- package/dist/context-manager.d.ts +134 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +170 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/cortex-agent.d.ts +1020 -0
- package/dist/cortex-agent.d.ts.map +1 -0
- package/dist/cortex-agent.js +3589 -0
- package/dist/cortex-agent.js.map +1 -0
- package/dist/error-classifier.d.ts +48 -0
- package/dist/error-classifier.d.ts.map +1 -0
- package/dist/error-classifier.js +152 -0
- package/dist/error-classifier.js.map +1 -0
- package/dist/event-bridge.d.ts +166 -0
- package/dist/event-bridge.d.ts.map +1 -0
- package/dist/event-bridge.js +381 -0
- package/dist/event-bridge.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +119 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +474 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/model-wrapper.d.ts +58 -0
- package/dist/model-wrapper.d.ts.map +1 -0
- package/dist/model-wrapper.js +86 -0
- package/dist/model-wrapper.js.map +1 -0
- package/dist/noop-logger.d.ts +4 -0
- package/dist/noop-logger.d.ts.map +1 -0
- package/dist/noop-logger.js +8 -0
- package/dist/noop-logger.js.map +1 -0
- package/dist/prompt-diagnostics.d.ts +47 -0
- package/dist/prompt-diagnostics.d.ts.map +1 -0
- package/dist/prompt-diagnostics.js +230 -0
- package/dist/prompt-diagnostics.js.map +1 -0
- package/dist/provider-manager.d.ts +224 -0
- package/dist/provider-manager.d.ts.map +1 -0
- package/dist/provider-manager.js +563 -0
- package/dist/provider-manager.js.map +1 -0
- package/dist/provider-registry.d.ts +115 -0
- package/dist/provider-registry.d.ts.map +1 -0
- package/dist/provider-registry.js +305 -0
- package/dist/provider-registry.js.map +1 -0
- package/dist/schema-converter.d.ts +20 -0
- package/dist/schema-converter.d.ts.map +1 -0
- package/dist/schema-converter.js +48 -0
- package/dist/schema-converter.js.map +1 -0
- package/dist/skill-preprocessor.d.ts +46 -0
- package/dist/skill-preprocessor.d.ts.map +1 -0
- package/dist/skill-preprocessor.js +237 -0
- package/dist/skill-preprocessor.js.map +1 -0
- package/dist/skill-registry.d.ts +107 -0
- package/dist/skill-registry.d.ts.map +1 -0
- package/dist/skill-registry.js +330 -0
- package/dist/skill-registry.js.map +1 -0
- package/dist/skill-tool.d.ts +54 -0
- package/dist/skill-tool.d.ts.map +1 -0
- package/dist/skill-tool.js +88 -0
- package/dist/skill-tool.js.map +1 -0
- package/dist/sub-agent-manager.d.ts +90 -0
- package/dist/sub-agent-manager.d.ts.map +1 -0
- package/dist/sub-agent-manager.js +192 -0
- package/dist/sub-agent-manager.js.map +1 -0
- package/dist/token-estimator.d.ts +23 -0
- package/dist/token-estimator.d.ts.map +1 -0
- package/dist/token-estimator.js +27 -0
- package/dist/token-estimator.js.map +1 -0
- package/dist/tool-contract.d.ts +68 -0
- package/dist/tool-contract.d.ts.map +1 -0
- package/dist/tool-contract.js +35 -0
- package/dist/tool-contract.js.map +1 -0
- package/dist/tool-result-persistence.d.ts +89 -0
- package/dist/tool-result-persistence.d.ts.map +1 -0
- package/dist/tool-result-persistence.js +152 -0
- package/dist/tool-result-persistence.js.map +1 -0
- package/dist/tools/bash/index.d.ts +71 -0
- package/dist/tools/bash/index.d.ts.map +1 -0
- package/dist/tools/bash/index.js +485 -0
- package/dist/tools/bash/index.js.map +1 -0
- package/dist/tools/bash/interactive.d.ts +47 -0
- package/dist/tools/bash/interactive.d.ts.map +1 -0
- package/dist/tools/bash/interactive.js +262 -0
- package/dist/tools/bash/interactive.js.map +1 -0
- package/dist/tools/bash/safety.d.ts +149 -0
- package/dist/tools/bash/safety.d.ts.map +1 -0
- package/dist/tools/bash/safety.js +1116 -0
- package/dist/tools/bash/safety.js.map +1 -0
- package/dist/tools/edit.d.ts +57 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +310 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/glob.d.ts +34 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +268 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/grep.d.ts +53 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +673 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +43 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +459 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/runtime.d.ts +62 -0
- package/dist/tools/runtime.d.ts.map +1 -0
- package/dist/tools/runtime.js +116 -0
- package/dist/tools/runtime.js.map +1 -0
- package/dist/tools/shared/cwd-tracker.d.ts +32 -0
- package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
- package/dist/tools/shared/cwd-tracker.js +44 -0
- package/dist/tools/shared/cwd-tracker.js.map +1 -0
- package/dist/tools/shared/edit-history.d.ts +55 -0
- package/dist/tools/shared/edit-history.d.ts.map +1 -0
- package/dist/tools/shared/edit-history.js +72 -0
- package/dist/tools/shared/edit-history.js.map +1 -0
- package/dist/tools/shared/edit-matcher.d.ts +83 -0
- package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
- package/dist/tools/shared/edit-matcher.js +359 -0
- package/dist/tools/shared/edit-matcher.js.map +1 -0
- package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
- package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
- package/dist/tools/shared/file-mutation-lock.js +35 -0
- package/dist/tools/shared/file-mutation-lock.js.map +1 -0
- package/dist/tools/shared/gitignore.d.ts +17 -0
- package/dist/tools/shared/gitignore.d.ts.map +1 -0
- package/dist/tools/shared/gitignore.js +59 -0
- package/dist/tools/shared/gitignore.js.map +1 -0
- package/dist/tools/shared/pdf-extractor.d.ts +96 -0
- package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
- package/dist/tools/shared/pdf-extractor.js +196 -0
- package/dist/tools/shared/pdf-extractor.js.map +1 -0
- package/dist/tools/shared/read-registry.d.ts +66 -0
- package/dist/tools/shared/read-registry.d.ts.map +1 -0
- package/dist/tools/shared/read-registry.js +65 -0
- package/dist/tools/shared/read-registry.js.map +1 -0
- package/dist/tools/shared/safe-env.d.ts +18 -0
- package/dist/tools/shared/safe-env.d.ts.map +1 -0
- package/dist/tools/shared/safe-env.js +70 -0
- package/dist/tools/shared/safe-env.js.map +1 -0
- package/dist/tools/sub-agent.d.ts +91 -0
- package/dist/tools/sub-agent.d.ts.map +1 -0
- package/dist/tools/sub-agent.js +89 -0
- package/dist/tools/sub-agent.js.map +1 -0
- package/dist/tools/task-output.d.ts +38 -0
- package/dist/tools/task-output.d.ts.map +1 -0
- package/dist/tools/task-output.js +186 -0
- package/dist/tools/task-output.js.map +1 -0
- package/dist/tools/tool-search/index.d.ts +40 -0
- package/dist/tools/tool-search/index.d.ts.map +1 -0
- package/dist/tools/tool-search/index.js +110 -0
- package/dist/tools/tool-search/index.js.map +1 -0
- package/dist/tools/tool-search/registry.d.ts +82 -0
- package/dist/tools/tool-search/registry.d.ts.map +1 -0
- package/dist/tools/tool-search/registry.js +238 -0
- package/dist/tools/tool-search/registry.js.map +1 -0
- package/dist/tools/undo-edit.d.ts +51 -0
- package/dist/tools/undo-edit.d.ts.map +1 -0
- package/dist/tools/undo-edit.js +231 -0
- package/dist/tools/undo-edit.js.map +1 -0
- package/dist/tools/web-fetch/cache.d.ts +49 -0
- package/dist/tools/web-fetch/cache.d.ts.map +1 -0
- package/dist/tools/web-fetch/cache.js +89 -0
- package/dist/tools/web-fetch/cache.js.map +1 -0
- package/dist/tools/web-fetch/index.d.ts +53 -0
- package/dist/tools/web-fetch/index.d.ts.map +1 -0
- package/dist/tools/web-fetch/index.js +513 -0
- package/dist/tools/web-fetch/index.js.map +1 -0
- package/dist/tools/write.d.ts +59 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +316 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/types.d.ts +881 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/working-tags.d.ts +44 -0
- package/dist/working-tags.d.ts.map +1 -0
- package/dist/working-tags.js +103 -0
- package/dist/working-tags.js.map +1 -0
- package/package.json +87 -0
- package/src/budget-guard.ts +170 -0
- package/src/compaction/compaction.ts +386 -0
- package/src/compaction/failsafe.ts +185 -0
- package/src/compaction/index.ts +1199 -0
- package/src/compaction/microcompaction.ts +709 -0
- package/src/compaction/observational/buffering.ts +430 -0
- package/src/compaction/observational/constants.ts +532 -0
- package/src/compaction/observational/index.ts +837 -0
- package/src/compaction/observational/observer.ts +510 -0
- package/src/compaction/observational/recall-tool.ts +130 -0
- package/src/compaction/observational/reflector.ts +221 -0
- package/src/compaction/observational/types.ts +343 -0
- package/src/context-manager.ts +237 -0
- package/src/cortex-agent.ts +4297 -0
- package/src/error-classifier.ts +199 -0
- package/src/event-bridge.ts +508 -0
- package/src/index.ts +292 -0
- package/src/mcp-client.ts +582 -0
- package/src/model-wrapper.ts +128 -0
- package/src/noop-logger.ts +9 -0
- package/src/prompt-diagnostics.ts +296 -0
- package/src/provider-manager.ts +823 -0
- package/src/provider-registry.ts +386 -0
- package/src/schema-converter.ts +51 -0
- package/src/skill-preprocessor.ts +314 -0
- package/src/skill-registry.ts +378 -0
- package/src/skill-tool.ts +130 -0
- package/src/sub-agent-manager.ts +236 -0
- package/src/token-estimator.ts +26 -0
- package/src/tool-contract.ts +113 -0
- package/src/tool-result-persistence.ts +197 -0
- package/src/tools/bash/index.ts +633 -0
- package/src/tools/bash/interactive.ts +302 -0
- package/src/tools/bash/safety.ts +1297 -0
- package/src/tools/edit.ts +422 -0
- package/src/tools/glob.ts +330 -0
- package/src/tools/grep.ts +819 -0
- package/src/tools/index.ts +110 -0
- package/src/tools/read.ts +580 -0
- package/src/tools/runtime.ts +173 -0
- package/src/tools/shared/cwd-tracker.ts +50 -0
- package/src/tools/shared/edit-history.ts +96 -0
- package/src/tools/shared/edit-matcher.ts +457 -0
- package/src/tools/shared/file-mutation-lock.ts +40 -0
- package/src/tools/shared/gitignore.ts +61 -0
- package/src/tools/shared/pdf-extractor.ts +290 -0
- package/src/tools/shared/read-registry.ts +93 -0
- package/src/tools/shared/safe-env.ts +82 -0
- package/src/tools/sub-agent.ts +171 -0
- package/src/tools/task-output.ts +236 -0
- package/src/tools/tool-search/index.ts +167 -0
- package/src/tools/tool-search/registry.ts +278 -0
- package/src/tools/undo-edit.ts +314 -0
- package/src/tools/web-fetch/cache.ts +112 -0
- package/src/tools/web-fetch/index.ts +604 -0
- package/src/tools/write.ts +385 -0
- package/src/types.ts +1057 -0
- package/src/working-tags.ts +118 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool barrel export and factory registration.
|
|
3
|
+
*
|
|
4
|
+
* All built-in tools are registered here. The CortexAgent maps tool names
|
|
5
|
+
* to factory functions, creates each tool with the appropriate config,
|
|
6
|
+
* and registers them on the pi-agent-core Agent.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Shared infrastructure
|
|
10
|
+
export { ReadRegistry } from './shared/read-registry.js';
|
|
11
|
+
export type { ReadState } from './shared/read-registry.js';
|
|
12
|
+
export { EditHistory, MAX_STACK_DEPTH as EDIT_HISTORY_MAX_STACK_DEPTH } from './shared/edit-history.js';
|
|
13
|
+
export type { EditHistoryEntry } from './shared/edit-history.js';
|
|
14
|
+
export { CwdTracker } from './shared/cwd-tracker.js';
|
|
15
|
+
export {
|
|
16
|
+
CortexToolRuntime,
|
|
17
|
+
BackgroundTaskStore,
|
|
18
|
+
WebFetchRuntimeState,
|
|
19
|
+
globalBackgroundTaskStore,
|
|
20
|
+
attachRuntimeAwareTool,
|
|
21
|
+
getRuntimeAwareToolMetadata,
|
|
22
|
+
cloneRuntimeAwareTool,
|
|
23
|
+
} from './runtime.js';
|
|
24
|
+
export type {
|
|
25
|
+
BackgroundTask,
|
|
26
|
+
RuntimeAwareToolMetadata,
|
|
27
|
+
} from './runtime.js';
|
|
28
|
+
|
|
29
|
+
// Tool factories
|
|
30
|
+
export { createReadTool } from './read.js';
|
|
31
|
+
export type { ReadToolConfig, ReadDetails, ReadParamsType } from './read.js';
|
|
32
|
+
export { ReadParams } from './read.js';
|
|
33
|
+
|
|
34
|
+
export { createWriteTool } from './write.js';
|
|
35
|
+
export type { WriteToolConfig, WriteDetails, WriteParamsType, DiffHunk } from './write.js';
|
|
36
|
+
export { WriteParams } from './write.js';
|
|
37
|
+
|
|
38
|
+
export { createEditTool } from './edit.js';
|
|
39
|
+
export type { EditToolConfig, EditDetails, EditParamsType } from './edit.js';
|
|
40
|
+
export { EditParams } from './edit.js';
|
|
41
|
+
|
|
42
|
+
export { createUndoEditTool } from './undo-edit.js';
|
|
43
|
+
export type {
|
|
44
|
+
UndoEditToolConfig,
|
|
45
|
+
UndoEditDetails,
|
|
46
|
+
UndoEditParamsType,
|
|
47
|
+
} from './undo-edit.js';
|
|
48
|
+
export { UndoEditParams } from './undo-edit.js';
|
|
49
|
+
|
|
50
|
+
export { createGlobTool } from './glob.js';
|
|
51
|
+
export type { GlobToolConfig, GlobDetails, GlobParamsType } from './glob.js';
|
|
52
|
+
export { GlobParams } from './glob.js';
|
|
53
|
+
|
|
54
|
+
export { createGrepTool } from './grep.js';
|
|
55
|
+
export type { GrepToolConfig, GrepDetails, GrepParamsType } from './grep.js';
|
|
56
|
+
export { GrepParams } from './grep.js';
|
|
57
|
+
|
|
58
|
+
export { createBashTool, getBackgroundTask, getAllBackgroundTasks } from './bash/index.js';
|
|
59
|
+
export type { BashToolConfig, BashDetails, BashStreamUpdate, BashParamsType } from './bash/index.js';
|
|
60
|
+
export { BashParams } from './bash/index.js';
|
|
61
|
+
|
|
62
|
+
export { createTaskOutputTool } from './task-output.js';
|
|
63
|
+
export type { TaskOutputDetails, TaskOutputParamsType, TaskOutputToolConfig } from './task-output.js';
|
|
64
|
+
export { TaskOutputParams } from './task-output.js';
|
|
65
|
+
|
|
66
|
+
export { createWebFetchTool, isPrivateIp } from './web-fetch/index.js';
|
|
67
|
+
export type { WebFetchToolConfig, WebFetchDetails, WebFetchParamsType } from './web-fetch/index.js';
|
|
68
|
+
export { WebFetchParams } from './web-fetch/index.js';
|
|
69
|
+
|
|
70
|
+
export { WebFetchCache } from './web-fetch/cache.js';
|
|
71
|
+
export type { CacheEntry } from './web-fetch/cache.js';
|
|
72
|
+
|
|
73
|
+
export { createSubAgentTool, SUB_AGENT_TOOL_NAME } from './sub-agent.js';
|
|
74
|
+
export type { SubAgentToolConfig, SubAgentDetails, SubAgentParamsType } from './sub-agent.js';
|
|
75
|
+
export { SubAgentParams } from './sub-agent.js';
|
|
76
|
+
|
|
77
|
+
// Safety layers
|
|
78
|
+
export {
|
|
79
|
+
buildSafeEnv,
|
|
80
|
+
isCriticalPath,
|
|
81
|
+
classifyCommand,
|
|
82
|
+
splitOnShellOperators,
|
|
83
|
+
checkObfuscation,
|
|
84
|
+
stripInvisibleChars,
|
|
85
|
+
checkScriptPreflight,
|
|
86
|
+
checkAutoModeClassifier,
|
|
87
|
+
runSafetyChecks,
|
|
88
|
+
validateWritePaths,
|
|
89
|
+
extractWritePaths,
|
|
90
|
+
} from './bash/safety.js';
|
|
91
|
+
export type { CommandClassification, SafetyCheckResult } from './bash/safety.js';
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Tool name constants
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
export const TOOL_NAMES = {
|
|
98
|
+
Read: 'Read',
|
|
99
|
+
Write: 'Write',
|
|
100
|
+
Edit: 'Edit',
|
|
101
|
+
UndoEdit: 'UndoEdit',
|
|
102
|
+
Glob: 'Glob',
|
|
103
|
+
Grep: 'Grep',
|
|
104
|
+
Bash: 'Bash',
|
|
105
|
+
TaskOutput: 'TaskOutput',
|
|
106
|
+
WebFetch: 'WebFetch',
|
|
107
|
+
SubAgent: 'SubAgent',
|
|
108
|
+
} as const;
|
|
109
|
+
|
|
110
|
+
export type BuiltInToolName = keyof typeof TOOL_NAMES;
|
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read tool: read file contents from the local filesystem.
|
|
3
|
+
*
|
|
4
|
+
* Returns file content with line numbers in `cat -n` format.
|
|
5
|
+
* Handles text files, images (base64 ImageContent), and
|
|
6
|
+
* detects binary files.
|
|
7
|
+
*
|
|
8
|
+
* Reference: docs/cortex/tools/read.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as crypto from 'node:crypto';
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import { Type, type Static } from 'typebox';
|
|
15
|
+
import type { ReadRegistry } from './shared/read-registry.js';
|
|
16
|
+
import type { ToolContentDetails } from '../types.js';
|
|
17
|
+
import type { CortexToolRuntime } from './runtime.js';
|
|
18
|
+
import { attachRuntimeAwareTool } from './runtime.js';
|
|
19
|
+
import { estimateTokens } from '../token-estimator.js';
|
|
20
|
+
import { extractPdfText } from './shared/pdf-extractor.js';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Schema
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export const ReadParams = Type.Object({
|
|
27
|
+
file_path: Type.String({ description: 'Absolute path to the file to read' }),
|
|
28
|
+
offset: Type.Optional(
|
|
29
|
+
Type.Number({ description: 'Line number to start reading from (1-based). Only provide if the file is too large to read at once.' }),
|
|
30
|
+
),
|
|
31
|
+
limit: Type.Optional(
|
|
32
|
+
Type.Number({ description: 'Maximum number of lines to read. Only provide if the file is too large to read at once.' }),
|
|
33
|
+
),
|
|
34
|
+
pages: Type.Optional(
|
|
35
|
+
Type.String({ description: 'Page range for PDF files (e.g., "1-5", "3", "10-20"). Only applicable to PDF files. Max 20 pages per request.' }),
|
|
36
|
+
),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export type ReadParamsType = Static<typeof ReadParams>;
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Constants
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
const DEFAULT_LIMIT = 2000;
|
|
46
|
+
const MAX_LINE_LENGTH = 2000;
|
|
47
|
+
|
|
48
|
+
/** Pre-read gate for full reads (no offset/limit provided). */
|
|
49
|
+
const MAX_FULL_READ_BYTES = 256 * 1024; // 256 KB
|
|
50
|
+
|
|
51
|
+
/** Hard ceiling even with offset/limit. Beyond this, use Bash. */
|
|
52
|
+
const MAX_READABLE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
53
|
+
|
|
54
|
+
/** Post-read token ceiling on formatted output. */
|
|
55
|
+
const MAX_OUTPUT_TOKENS = 25_000;
|
|
56
|
+
|
|
57
|
+
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp']);
|
|
58
|
+
const IMAGE_MIME_TYPES: Record<string, string> = {
|
|
59
|
+
'.png': 'image/png',
|
|
60
|
+
'.jpg': 'image/jpeg',
|
|
61
|
+
'.jpeg': 'image/jpeg',
|
|
62
|
+
'.gif': 'image/gif',
|
|
63
|
+
'.webp': 'image/webp',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Device files that would hang the process: infinite output or blocking input.
|
|
68
|
+
* Checked by path only (no I/O).
|
|
69
|
+
*/
|
|
70
|
+
const BLOCKED_DEVICE_PATHS = new Set([
|
|
71
|
+
// Infinite output
|
|
72
|
+
'/dev/zero',
|
|
73
|
+
'/dev/random',
|
|
74
|
+
'/dev/urandom',
|
|
75
|
+
'/dev/full',
|
|
76
|
+
// Blocks waiting for input
|
|
77
|
+
'/dev/stdin',
|
|
78
|
+
'/dev/tty',
|
|
79
|
+
'/dev/console',
|
|
80
|
+
// Nonsensical to read
|
|
81
|
+
'/dev/stdout',
|
|
82
|
+
'/dev/stderr',
|
|
83
|
+
// fd aliases for stdin/stdout/stderr
|
|
84
|
+
'/dev/fd/0',
|
|
85
|
+
'/dev/fd/1',
|
|
86
|
+
'/dev/fd/2',
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
function isBlockedDevicePath(filePath: string): boolean {
|
|
90
|
+
if (BLOCKED_DEVICE_PATHS.has(filePath)) return true;
|
|
91
|
+
// /proc/self/fd/0-2 and /proc/<pid>/fd/0-2 are Linux aliases for stdio
|
|
92
|
+
if (
|
|
93
|
+
filePath.startsWith('/proc/') &&
|
|
94
|
+
(filePath.endsWith('/fd/0') ||
|
|
95
|
+
filePath.endsWith('/fd/1') ||
|
|
96
|
+
filePath.endsWith('/fd/2'))
|
|
97
|
+
)
|
|
98
|
+
return true;
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Details type
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
export interface ReadDetails {
|
|
107
|
+
filePath: string;
|
|
108
|
+
totalLines: number;
|
|
109
|
+
byteSize: number;
|
|
110
|
+
truncated: boolean;
|
|
111
|
+
truncatedLines: boolean;
|
|
112
|
+
truncatedChars: boolean;
|
|
113
|
+
/** Starting line number (1-based) for the content returned. */
|
|
114
|
+
startLine: number;
|
|
115
|
+
/** True when the read was rejected by a size/token gate (content is an error message, not file data). */
|
|
116
|
+
rejected?: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Config
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
export interface ReadToolConfig {
|
|
124
|
+
runtime?: CortexToolRuntime | undefined;
|
|
125
|
+
readRegistry?: ReadRegistry | undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Helpers
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Detect if a buffer contains binary content.
|
|
134
|
+
* A file is considered binary if it contains null bytes in the first 8KB.
|
|
135
|
+
*/
|
|
136
|
+
function isBinaryBuffer(buffer: Buffer): boolean {
|
|
137
|
+
const checkLength = Math.min(buffer.length, 8192);
|
|
138
|
+
for (let i = 0; i < checkLength; i++) {
|
|
139
|
+
if (buffer[i] === 0) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Try to detect and decode file content with common encodings.
|
|
148
|
+
* Handles UTF-8, UTF-16 LE/BE (via BOM), and falls back to Latin-1.
|
|
149
|
+
*/
|
|
150
|
+
function decodeFileContent(buffer: Buffer): string {
|
|
151
|
+
// Check for UTF-16 BOM
|
|
152
|
+
if (buffer.length >= 2) {
|
|
153
|
+
if (buffer[0] === 0xff && buffer[1] === 0xfe) {
|
|
154
|
+
return buffer.toString('utf16le');
|
|
155
|
+
}
|
|
156
|
+
if (buffer[0] === 0xfe && buffer[1] === 0xff) {
|
|
157
|
+
// UTF-16 BE: swap bytes and decode as UTF-16 LE
|
|
158
|
+
const swapped = Buffer.alloc(buffer.length);
|
|
159
|
+
for (let i = 0; i < buffer.length - 1; i += 2) {
|
|
160
|
+
swapped[i] = buffer[i + 1]!;
|
|
161
|
+
swapped[i + 1] = buffer[i]!;
|
|
162
|
+
}
|
|
163
|
+
return swapped.toString('utf16le');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check for UTF-8 BOM
|
|
168
|
+
if (buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) {
|
|
169
|
+
return buffer.toString('utf8').slice(1); // Skip the BOM character
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Try UTF-8 first (most common)
|
|
173
|
+
const utf8 = buffer.toString('utf8');
|
|
174
|
+
|
|
175
|
+
// Check for replacement characters that suggest bad UTF-8 decoding
|
|
176
|
+
// Only fall back to Latin-1 if there are many replacement chars
|
|
177
|
+
const replacementCount = (utf8.match(/\ufffd/g) ?? []).length;
|
|
178
|
+
if (replacementCount > 0 && replacementCount > buffer.length * 0.01) {
|
|
179
|
+
return buffer.toString('latin1');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return utf8;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Format lines with `cat -n` style line numbers.
|
|
187
|
+
* Format: spaces + line_number + tab + content
|
|
188
|
+
*/
|
|
189
|
+
function formatWithLineNumbers(
|
|
190
|
+
lines: string[],
|
|
191
|
+
startLine: number,
|
|
192
|
+
): string {
|
|
193
|
+
const maxLineNum = startLine + lines.length - 1;
|
|
194
|
+
const width = String(maxLineNum).length;
|
|
195
|
+
|
|
196
|
+
return lines
|
|
197
|
+
.map((line, i) => {
|
|
198
|
+
const lineNum = startLine + i;
|
|
199
|
+
const paddedNum = String(lineNum).padStart(width + 2);
|
|
200
|
+
// Truncate long lines
|
|
201
|
+
const truncatedLine =
|
|
202
|
+
line.length > MAX_LINE_LENGTH
|
|
203
|
+
? line.slice(0, MAX_LINE_LENGTH) + '... [truncated]'
|
|
204
|
+
: line;
|
|
205
|
+
return `${paddedNum}\t${truncatedLine}`;
|
|
206
|
+
})
|
|
207
|
+
.join('\n');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Format byte count as a human-readable string (KB or MB).
|
|
212
|
+
*/
|
|
213
|
+
function formatBytes(bytes: number): string {
|
|
214
|
+
if (bytes >= 1024 * 1024) {
|
|
215
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
216
|
+
}
|
|
217
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Build a rejection result for size/token gate failures.
|
|
222
|
+
* Returns an error message as tool content with `rejected: true` in details.
|
|
223
|
+
*/
|
|
224
|
+
function makeRejection(filePath: string, byteSize: number, message: string): ToolContentDetails<ReadDetails> {
|
|
225
|
+
return {
|
|
226
|
+
content: [{ type: 'text', text: message }],
|
|
227
|
+
details: {
|
|
228
|
+
filePath,
|
|
229
|
+
totalLines: 0,
|
|
230
|
+
byteSize,
|
|
231
|
+
truncated: false,
|
|
232
|
+
truncatedLines: false,
|
|
233
|
+
truncatedChars: false,
|
|
234
|
+
startLine: 1,
|
|
235
|
+
rejected: true,
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Tool factory
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
export function createReadTool(config: ReadToolConfig): {
|
|
245
|
+
name: string;
|
|
246
|
+
description: string;
|
|
247
|
+
parameters: typeof ReadParams;
|
|
248
|
+
execute: (params: ReadParamsType) => Promise<ToolContentDetails<ReadDetails>>;
|
|
249
|
+
} {
|
|
250
|
+
const readRegistry = config.runtime?.readRegistry ?? config.readRegistry;
|
|
251
|
+
if (!readRegistry) {
|
|
252
|
+
throw new Error('createReadTool requires either runtime or readRegistry');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const tool = {
|
|
256
|
+
name: 'Read',
|
|
257
|
+
description: [
|
|
258
|
+
'Read file contents from the local filesystem.',
|
|
259
|
+
'Returns content with line numbers in cat -n format.',
|
|
260
|
+
'',
|
|
261
|
+
'Size limits:',
|
|
262
|
+
`- Files up to ${formatBytes(MAX_FULL_READ_BYTES)}: read in full (no offset/limit needed)`,
|
|
263
|
+
`- Files ${formatBytes(MAX_FULL_READ_BYTES)} to ${formatBytes(MAX_READABLE_BYTES)}: must provide offset and limit`,
|
|
264
|
+
`- Files over ${formatBytes(MAX_READABLE_BYTES)}: use Bash (head, tail, sed) instead`,
|
|
265
|
+
`- Output capped at ~${MAX_OUTPUT_TOKENS.toLocaleString()} tokens; reduce limit if exceeded`,
|
|
266
|
+
'',
|
|
267
|
+
'For searching file contents, use Grep instead of reading the whole file.',
|
|
268
|
+
].join('\n'),
|
|
269
|
+
parameters: ReadParams,
|
|
270
|
+
|
|
271
|
+
async execute(params: ReadParamsType): Promise<ToolContentDetails<ReadDetails>> {
|
|
272
|
+
const filePath = path.resolve(params.file_path);
|
|
273
|
+
const offset = params.offset ?? 1;
|
|
274
|
+
const limit = params.limit ?? DEFAULT_LIMIT;
|
|
275
|
+
|
|
276
|
+
// Block device paths that would hang (infinite output or blocking input)
|
|
277
|
+
if (isBlockedDevicePath(filePath)) {
|
|
278
|
+
return {
|
|
279
|
+
content: [{ type: 'text', text: `Cannot read '${params.file_path}': this device file would block or produce infinite output.` }],
|
|
280
|
+
details: {
|
|
281
|
+
filePath,
|
|
282
|
+
totalLines: 0,
|
|
283
|
+
byteSize: 0,
|
|
284
|
+
truncated: false,
|
|
285
|
+
truncatedLines: false,
|
|
286
|
+
truncatedChars: false,
|
|
287
|
+
startLine: 1,
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Check if path exists
|
|
293
|
+
let stat: fs.Stats;
|
|
294
|
+
try {
|
|
295
|
+
stat = await fs.promises.stat(filePath);
|
|
296
|
+
} catch (err: unknown) {
|
|
297
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
298
|
+
if (code === 'ENOENT') {
|
|
299
|
+
return {
|
|
300
|
+
content: [{ type: 'text', text: `File does not exist: ${filePath}` }],
|
|
301
|
+
details: {
|
|
302
|
+
filePath,
|
|
303
|
+
totalLines: 0,
|
|
304
|
+
byteSize: 0,
|
|
305
|
+
truncated: false,
|
|
306
|
+
truncatedLines: false,
|
|
307
|
+
truncatedChars: false,
|
|
308
|
+
startLine: 1,
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (code === 'EACCES') {
|
|
313
|
+
return {
|
|
314
|
+
content: [{ type: 'text', text: `Permission denied: ${filePath}` }],
|
|
315
|
+
details: {
|
|
316
|
+
filePath,
|
|
317
|
+
totalLines: 0,
|
|
318
|
+
byteSize: 0,
|
|
319
|
+
truncated: false,
|
|
320
|
+
truncatedLines: false,
|
|
321
|
+
truncatedChars: false,
|
|
322
|
+
startLine: 1,
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
throw err;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Cannot read directories
|
|
330
|
+
if (stat.isDirectory()) {
|
|
331
|
+
return {
|
|
332
|
+
content: [{ type: 'text', text: 'Cannot read a directory. Use `ls` via Bash.' }],
|
|
333
|
+
details: {
|
|
334
|
+
filePath,
|
|
335
|
+
totalLines: 0,
|
|
336
|
+
byteSize: 0,
|
|
337
|
+
truncated: false,
|
|
338
|
+
truncatedLines: false,
|
|
339
|
+
truncatedChars: false,
|
|
340
|
+
startLine: 1,
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Gate 1: Absolute size ceiling - reject files > 10 MB entirely
|
|
346
|
+
if (stat.size > MAX_READABLE_BYTES) {
|
|
347
|
+
return makeRejection(
|
|
348
|
+
filePath,
|
|
349
|
+
stat.size,
|
|
350
|
+
`File is too large to read (${formatBytes(stat.size)}, limit ${formatBytes(MAX_READABLE_BYTES)}). Use Bash with head, tail, or sed to extract specific sections.`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
355
|
+
|
|
356
|
+
// Handle image files
|
|
357
|
+
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
358
|
+
const buffer = await fs.promises.readFile(filePath);
|
|
359
|
+
const mimeType = IMAGE_MIME_TYPES[ext] ?? 'application/octet-stream';
|
|
360
|
+
const base64 = buffer.toString('base64');
|
|
361
|
+
|
|
362
|
+
readRegistry.markRead(filePath, { timestamp: stat.mtimeMs });
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
content: [{ type: 'image', data: base64, mimeType }],
|
|
366
|
+
details: {
|
|
367
|
+
filePath,
|
|
368
|
+
totalLines: 0,
|
|
369
|
+
byteSize: stat.size,
|
|
370
|
+
truncated: false,
|
|
371
|
+
truncatedLines: false,
|
|
372
|
+
truncatedChars: false,
|
|
373
|
+
startLine: 1,
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Handle PDF files. The extractor (shared/pdf-extractor.ts) wraps
|
|
379
|
+
// unpdf and returns a structured result: the Read tool's only
|
|
380
|
+
// responsibility is to decide how to surface each outcome.
|
|
381
|
+
if (ext === '.pdf') {
|
|
382
|
+
const pdfBuffer = await fs.promises.readFile(filePath);
|
|
383
|
+
const extraction = await extractPdfText({
|
|
384
|
+
data: pdfBuffer,
|
|
385
|
+
pagesSpec: params.pages,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (
|
|
389
|
+
extraction.kind === 'error' ||
|
|
390
|
+
extraction.kind === 'invalid-range' ||
|
|
391
|
+
extraction.kind === 'empty'
|
|
392
|
+
) {
|
|
393
|
+
// All three are read failures from the caller's perspective:
|
|
394
|
+
// there is no usable content to hand to the model. Flag as
|
|
395
|
+
// rejected so consumers can surface them uniformly and the
|
|
396
|
+
// model can retry (with a different pages spec, OCR, etc.).
|
|
397
|
+
return makeRejection(filePath, stat.size, extraction.message);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Line-number the rendered output to match the cat -n style
|
|
401
|
+
// used for text files. Line numbers reset to 1 per call; PDFs
|
|
402
|
+
// don't map cleanly to the file-wide offset/limit model.
|
|
403
|
+
const renderedLines = extraction.rendered.split('\n');
|
|
404
|
+
const formatted = formatWithLineNumbers(renderedLines, 1);
|
|
405
|
+
|
|
406
|
+
// Gate 3: token ceiling on the formatted output.
|
|
407
|
+
const pdfTokenCount = estimateTokens(formatted);
|
|
408
|
+
if (pdfTokenCount > MAX_OUTPUT_TOKENS) {
|
|
409
|
+
const requestedPages = extraction.lastPage - extraction.firstPage + 1;
|
|
410
|
+
const suggestedPages = Math.max(
|
|
411
|
+
1,
|
|
412
|
+
Math.floor(requestedPages * MAX_OUTPUT_TOKENS / pdfTokenCount),
|
|
413
|
+
);
|
|
414
|
+
return makeRejection(
|
|
415
|
+
filePath,
|
|
416
|
+
stat.size,
|
|
417
|
+
`PDF extraction too large (estimated ~${pdfTokenCount.toLocaleString()} tokens, limit ${MAX_OUTPUT_TOKENS.toLocaleString()}). ` +
|
|
418
|
+
`Narrow the \`pages\` range (try ~${suggestedPages} page${suggestedPages === 1 ? '' : 's'} per call).`,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
readRegistry.markRead(filePath, { timestamp: stat.mtimeMs });
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
content: [{ type: 'text', text: formatted }],
|
|
426
|
+
details: {
|
|
427
|
+
filePath,
|
|
428
|
+
totalLines: renderedLines.length,
|
|
429
|
+
byteSize: stat.size,
|
|
430
|
+
truncated: false,
|
|
431
|
+
truncatedLines: false,
|
|
432
|
+
truncatedChars: false,
|
|
433
|
+
startLine: 1,
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Gate 2: Full-read size gate - reject full reads of files > 256 KB
|
|
439
|
+
const hasExplicitRange = params.offset !== undefined || params.limit !== undefined;
|
|
440
|
+
if (!hasExplicitRange && stat.size > MAX_FULL_READ_BYTES) {
|
|
441
|
+
return makeRejection(
|
|
442
|
+
filePath,
|
|
443
|
+
stat.size,
|
|
444
|
+
`File is too large to read in full (${formatBytes(stat.size)}, limit ${formatBytes(MAX_FULL_READ_BYTES)}). Provide offset and limit to read a specific range, or use Grep to search for specific content.`,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// File-unchanged dedup: if we already read this exact range and the
|
|
449
|
+
// file hasn't changed on disk, return a stub. The earlier Read result
|
|
450
|
+
// is still in context, so re-sending wastes tokens.
|
|
451
|
+
const existingState = readRegistry.getState(filePath);
|
|
452
|
+
if (existingState && existingState.offset !== undefined) {
|
|
453
|
+
const rangeMatch =
|
|
454
|
+
existingState.offset === offset && existingState.limit === limit;
|
|
455
|
+
if (rangeMatch && stat.mtimeMs === existingState.timestamp) {
|
|
456
|
+
return {
|
|
457
|
+
content: [{ type: 'text', text: `[File unchanged since last read: ${filePath}]` }],
|
|
458
|
+
details: {
|
|
459
|
+
filePath,
|
|
460
|
+
totalLines: 0,
|
|
461
|
+
byteSize: stat.size,
|
|
462
|
+
truncated: false,
|
|
463
|
+
truncatedLines: false,
|
|
464
|
+
truncatedChars: false,
|
|
465
|
+
startLine: 1,
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Read the raw buffer
|
|
472
|
+
const buffer = await fs.promises.readFile(filePath);
|
|
473
|
+
|
|
474
|
+
// Binary detection (not image, not PDF)
|
|
475
|
+
if (isBinaryBuffer(buffer)) {
|
|
476
|
+
return {
|
|
477
|
+
content: [{ type: 'text', text: 'Binary file detected. Cannot display as text.' }],
|
|
478
|
+
details: {
|
|
479
|
+
filePath,
|
|
480
|
+
totalLines: 0,
|
|
481
|
+
byteSize: stat.size,
|
|
482
|
+
truncated: false,
|
|
483
|
+
truncatedLines: false,
|
|
484
|
+
truncatedChars: false,
|
|
485
|
+
startLine: 1,
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Decode and split into lines
|
|
491
|
+
const content = decodeFileContent(buffer);
|
|
492
|
+
const allLines = content.split('\n');
|
|
493
|
+
const totalLines = allLines.length;
|
|
494
|
+
|
|
495
|
+
// Handle empty file
|
|
496
|
+
if (totalLines === 0 || (totalLines === 1 && allLines[0] === '')) {
|
|
497
|
+
readRegistry.markRead(filePath, { timestamp: stat.mtimeMs, offset, limit });
|
|
498
|
+
return {
|
|
499
|
+
content: [{ type: 'text', text: `[File is empty: ${filePath}]` }],
|
|
500
|
+
details: {
|
|
501
|
+
filePath,
|
|
502
|
+
totalLines: 0,
|
|
503
|
+
byteSize: stat.size,
|
|
504
|
+
truncated: false,
|
|
505
|
+
truncatedLines: false,
|
|
506
|
+
truncatedChars: false,
|
|
507
|
+
startLine: 1,
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Apply offset and limit
|
|
513
|
+
const startIdx = Math.max(0, offset - 1); // Convert 1-based to 0-based
|
|
514
|
+
const endIdx = Math.min(totalLines, startIdx + limit);
|
|
515
|
+
const selectedLines = allLines.slice(startIdx, endIdx);
|
|
516
|
+
|
|
517
|
+
const truncatedLines = endIdx < totalLines;
|
|
518
|
+
const truncatedChars = selectedLines.some((line) => line.length > MAX_LINE_LENGTH);
|
|
519
|
+
|
|
520
|
+
// Format with line numbers
|
|
521
|
+
const formatted = formatWithLineNumbers(selectedLines, startIdx + 1);
|
|
522
|
+
|
|
523
|
+
// Gate 3: Post-read token estimation
|
|
524
|
+
const estimatedTokenCount = estimateTokens(formatted);
|
|
525
|
+
if (estimatedTokenCount > MAX_OUTPUT_TOKENS) {
|
|
526
|
+
const suggestedLimit = Math.floor(limit * MAX_OUTPUT_TOKENS / estimatedTokenCount);
|
|
527
|
+
return makeRejection(
|
|
528
|
+
filePath,
|
|
529
|
+
stat.size,
|
|
530
|
+
`Read result too large (estimated ~${estimatedTokenCount.toLocaleString()} tokens, limit ${MAX_OUTPUT_TOKENS.toLocaleString()}). ` +
|
|
531
|
+
`The file has ${totalLines} lines. Use a smaller limit (try limit: ${Math.max(1, suggestedLimit)}) ` +
|
|
532
|
+
`or use Grep to find the specific content you need.`,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Only mark as read after passing all gates, so rejected reads
|
|
537
|
+
// can be retried without hitting the dedup stub.
|
|
538
|
+
// For full, non-truncated reads, record a content hash so the
|
|
539
|
+
// Edit/Write tools can distinguish real file modifications from
|
|
540
|
+
// mtime-only changes (formatters, cloud sync, antivirus, etc.).
|
|
541
|
+
const isFullRead = !hasExplicitRange && !truncatedLines;
|
|
542
|
+
const contentHash = isFullRead
|
|
543
|
+
? crypto.createHash('sha256').update(buffer).digest('hex')
|
|
544
|
+
: undefined;
|
|
545
|
+
readRegistry.markRead(filePath, {
|
|
546
|
+
timestamp: stat.mtimeMs,
|
|
547
|
+
offset,
|
|
548
|
+
limit,
|
|
549
|
+
...(contentHash !== undefined ? { contentHash } : {}),
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
let text = formatted;
|
|
553
|
+
if (truncatedLines) {
|
|
554
|
+
text += `\n\n[Showing lines ${startIdx + 1}-${endIdx} of ${totalLines} total. Use offset/limit to read more.]`;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
content: [{ type: 'text', text }],
|
|
559
|
+
details: {
|
|
560
|
+
filePath,
|
|
561
|
+
totalLines,
|
|
562
|
+
byteSize: stat.size,
|
|
563
|
+
truncated: truncatedLines || truncatedChars,
|
|
564
|
+
truncatedLines,
|
|
565
|
+
truncatedChars,
|
|
566
|
+
startLine: offset,
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
return attachRuntimeAwareTool(tool, {
|
|
573
|
+
toolKind: 'Read',
|
|
574
|
+
cloneForRuntime: (runtime) => createReadTool({
|
|
575
|
+
...config,
|
|
576
|
+
runtime,
|
|
577
|
+
readRegistry: runtime.readRegistry,
|
|
578
|
+
}),
|
|
579
|
+
});
|
|
580
|
+
}
|