@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.
Files changed (293) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/budget-guard.d.ts +75 -0
  4. package/dist/budget-guard.d.ts.map +1 -0
  5. package/dist/budget-guard.js +142 -0
  6. package/dist/budget-guard.js.map +1 -0
  7. package/dist/compaction/compaction.d.ts +99 -0
  8. package/dist/compaction/compaction.d.ts.map +1 -0
  9. package/dist/compaction/compaction.js +302 -0
  10. package/dist/compaction/compaction.js.map +1 -0
  11. package/dist/compaction/failsafe.d.ts +57 -0
  12. package/dist/compaction/failsafe.d.ts.map +1 -0
  13. package/dist/compaction/failsafe.js +135 -0
  14. package/dist/compaction/failsafe.js.map +1 -0
  15. package/dist/compaction/index.d.ts +381 -0
  16. package/dist/compaction/index.d.ts.map +1 -0
  17. package/dist/compaction/index.js +979 -0
  18. package/dist/compaction/index.js.map +1 -0
  19. package/dist/compaction/microcompaction.d.ts +219 -0
  20. package/dist/compaction/microcompaction.d.ts.map +1 -0
  21. package/dist/compaction/microcompaction.js +536 -0
  22. package/dist/compaction/microcompaction.js.map +1 -0
  23. package/dist/compaction/observational/buffering.d.ts +225 -0
  24. package/dist/compaction/observational/buffering.d.ts.map +1 -0
  25. package/dist/compaction/observational/buffering.js +354 -0
  26. package/dist/compaction/observational/buffering.js.map +1 -0
  27. package/dist/compaction/observational/constants.d.ts +70 -0
  28. package/dist/compaction/observational/constants.d.ts.map +1 -0
  29. package/dist/compaction/observational/constants.js +507 -0
  30. package/dist/compaction/observational/constants.js.map +1 -0
  31. package/dist/compaction/observational/index.d.ts +219 -0
  32. package/dist/compaction/observational/index.d.ts.map +1 -0
  33. package/dist/compaction/observational/index.js +641 -0
  34. package/dist/compaction/observational/index.js.map +1 -0
  35. package/dist/compaction/observational/observer.d.ts +97 -0
  36. package/dist/compaction/observational/observer.d.ts.map +1 -0
  37. package/dist/compaction/observational/observer.js +424 -0
  38. package/dist/compaction/observational/observer.js.map +1 -0
  39. package/dist/compaction/observational/recall-tool.d.ts +27 -0
  40. package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
  41. package/dist/compaction/observational/recall-tool.js +93 -0
  42. package/dist/compaction/observational/recall-tool.js.map +1 -0
  43. package/dist/compaction/observational/reflector.d.ts +94 -0
  44. package/dist/compaction/observational/reflector.d.ts.map +1 -0
  45. package/dist/compaction/observational/reflector.js +167 -0
  46. package/dist/compaction/observational/reflector.js.map +1 -0
  47. package/dist/compaction/observational/types.d.ts +271 -0
  48. package/dist/compaction/observational/types.d.ts.map +1 -0
  49. package/dist/compaction/observational/types.js +15 -0
  50. package/dist/compaction/observational/types.js.map +1 -0
  51. package/dist/context-manager.d.ts +134 -0
  52. package/dist/context-manager.d.ts.map +1 -0
  53. package/dist/context-manager.js +170 -0
  54. package/dist/context-manager.js.map +1 -0
  55. package/dist/cortex-agent.d.ts +1020 -0
  56. package/dist/cortex-agent.d.ts.map +1 -0
  57. package/dist/cortex-agent.js +3589 -0
  58. package/dist/cortex-agent.js.map +1 -0
  59. package/dist/error-classifier.d.ts +48 -0
  60. package/dist/error-classifier.d.ts.map +1 -0
  61. package/dist/error-classifier.js +152 -0
  62. package/dist/error-classifier.js.map +1 -0
  63. package/dist/event-bridge.d.ts +166 -0
  64. package/dist/event-bridge.d.ts.map +1 -0
  65. package/dist/event-bridge.js +381 -0
  66. package/dist/event-bridge.js.map +1 -0
  67. package/dist/index.d.ts +55 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +57 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/mcp-client.d.ts +119 -0
  72. package/dist/mcp-client.d.ts.map +1 -0
  73. package/dist/mcp-client.js +474 -0
  74. package/dist/mcp-client.js.map +1 -0
  75. package/dist/model-wrapper.d.ts +58 -0
  76. package/dist/model-wrapper.d.ts.map +1 -0
  77. package/dist/model-wrapper.js +86 -0
  78. package/dist/model-wrapper.js.map +1 -0
  79. package/dist/noop-logger.d.ts +4 -0
  80. package/dist/noop-logger.d.ts.map +1 -0
  81. package/dist/noop-logger.js +8 -0
  82. package/dist/noop-logger.js.map +1 -0
  83. package/dist/prompt-diagnostics.d.ts +47 -0
  84. package/dist/prompt-diagnostics.d.ts.map +1 -0
  85. package/dist/prompt-diagnostics.js +230 -0
  86. package/dist/prompt-diagnostics.js.map +1 -0
  87. package/dist/provider-manager.d.ts +224 -0
  88. package/dist/provider-manager.d.ts.map +1 -0
  89. package/dist/provider-manager.js +563 -0
  90. package/dist/provider-manager.js.map +1 -0
  91. package/dist/provider-registry.d.ts +115 -0
  92. package/dist/provider-registry.d.ts.map +1 -0
  93. package/dist/provider-registry.js +305 -0
  94. package/dist/provider-registry.js.map +1 -0
  95. package/dist/schema-converter.d.ts +20 -0
  96. package/dist/schema-converter.d.ts.map +1 -0
  97. package/dist/schema-converter.js +48 -0
  98. package/dist/schema-converter.js.map +1 -0
  99. package/dist/skill-preprocessor.d.ts +46 -0
  100. package/dist/skill-preprocessor.d.ts.map +1 -0
  101. package/dist/skill-preprocessor.js +237 -0
  102. package/dist/skill-preprocessor.js.map +1 -0
  103. package/dist/skill-registry.d.ts +107 -0
  104. package/dist/skill-registry.d.ts.map +1 -0
  105. package/dist/skill-registry.js +330 -0
  106. package/dist/skill-registry.js.map +1 -0
  107. package/dist/skill-tool.d.ts +54 -0
  108. package/dist/skill-tool.d.ts.map +1 -0
  109. package/dist/skill-tool.js +88 -0
  110. package/dist/skill-tool.js.map +1 -0
  111. package/dist/sub-agent-manager.d.ts +90 -0
  112. package/dist/sub-agent-manager.d.ts.map +1 -0
  113. package/dist/sub-agent-manager.js +192 -0
  114. package/dist/sub-agent-manager.js.map +1 -0
  115. package/dist/token-estimator.d.ts +23 -0
  116. package/dist/token-estimator.d.ts.map +1 -0
  117. package/dist/token-estimator.js +27 -0
  118. package/dist/token-estimator.js.map +1 -0
  119. package/dist/tool-contract.d.ts +68 -0
  120. package/dist/tool-contract.d.ts.map +1 -0
  121. package/dist/tool-contract.js +35 -0
  122. package/dist/tool-contract.js.map +1 -0
  123. package/dist/tool-result-persistence.d.ts +89 -0
  124. package/dist/tool-result-persistence.d.ts.map +1 -0
  125. package/dist/tool-result-persistence.js +152 -0
  126. package/dist/tool-result-persistence.js.map +1 -0
  127. package/dist/tools/bash/index.d.ts +71 -0
  128. package/dist/tools/bash/index.d.ts.map +1 -0
  129. package/dist/tools/bash/index.js +485 -0
  130. package/dist/tools/bash/index.js.map +1 -0
  131. package/dist/tools/bash/interactive.d.ts +47 -0
  132. package/dist/tools/bash/interactive.d.ts.map +1 -0
  133. package/dist/tools/bash/interactive.js +262 -0
  134. package/dist/tools/bash/interactive.js.map +1 -0
  135. package/dist/tools/bash/safety.d.ts +149 -0
  136. package/dist/tools/bash/safety.d.ts.map +1 -0
  137. package/dist/tools/bash/safety.js +1116 -0
  138. package/dist/tools/bash/safety.js.map +1 -0
  139. package/dist/tools/edit.d.ts +57 -0
  140. package/dist/tools/edit.d.ts.map +1 -0
  141. package/dist/tools/edit.js +310 -0
  142. package/dist/tools/edit.js.map +1 -0
  143. package/dist/tools/glob.d.ts +34 -0
  144. package/dist/tools/glob.d.ts.map +1 -0
  145. package/dist/tools/glob.js +268 -0
  146. package/dist/tools/glob.js.map +1 -0
  147. package/dist/tools/grep.d.ts +53 -0
  148. package/dist/tools/grep.d.ts.map +1 -0
  149. package/dist/tools/grep.js +673 -0
  150. package/dist/tools/grep.js.map +1 -0
  151. package/dist/tools/index.d.ts +62 -0
  152. package/dist/tools/index.d.ts.map +1 -0
  153. package/dist/tools/index.js +52 -0
  154. package/dist/tools/index.js.map +1 -0
  155. package/dist/tools/read.d.ts +43 -0
  156. package/dist/tools/read.d.ts.map +1 -0
  157. package/dist/tools/read.js +459 -0
  158. package/dist/tools/read.js.map +1 -0
  159. package/dist/tools/runtime.d.ts +62 -0
  160. package/dist/tools/runtime.d.ts.map +1 -0
  161. package/dist/tools/runtime.js +116 -0
  162. package/dist/tools/runtime.js.map +1 -0
  163. package/dist/tools/shared/cwd-tracker.d.ts +32 -0
  164. package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
  165. package/dist/tools/shared/cwd-tracker.js +44 -0
  166. package/dist/tools/shared/cwd-tracker.js.map +1 -0
  167. package/dist/tools/shared/edit-history.d.ts +55 -0
  168. package/dist/tools/shared/edit-history.d.ts.map +1 -0
  169. package/dist/tools/shared/edit-history.js +72 -0
  170. package/dist/tools/shared/edit-history.js.map +1 -0
  171. package/dist/tools/shared/edit-matcher.d.ts +83 -0
  172. package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
  173. package/dist/tools/shared/edit-matcher.js +359 -0
  174. package/dist/tools/shared/edit-matcher.js.map +1 -0
  175. package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
  176. package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
  177. package/dist/tools/shared/file-mutation-lock.js +35 -0
  178. package/dist/tools/shared/file-mutation-lock.js.map +1 -0
  179. package/dist/tools/shared/gitignore.d.ts +17 -0
  180. package/dist/tools/shared/gitignore.d.ts.map +1 -0
  181. package/dist/tools/shared/gitignore.js +59 -0
  182. package/dist/tools/shared/gitignore.js.map +1 -0
  183. package/dist/tools/shared/pdf-extractor.d.ts +96 -0
  184. package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
  185. package/dist/tools/shared/pdf-extractor.js +196 -0
  186. package/dist/tools/shared/pdf-extractor.js.map +1 -0
  187. package/dist/tools/shared/read-registry.d.ts +66 -0
  188. package/dist/tools/shared/read-registry.d.ts.map +1 -0
  189. package/dist/tools/shared/read-registry.js +65 -0
  190. package/dist/tools/shared/read-registry.js.map +1 -0
  191. package/dist/tools/shared/safe-env.d.ts +18 -0
  192. package/dist/tools/shared/safe-env.d.ts.map +1 -0
  193. package/dist/tools/shared/safe-env.js +70 -0
  194. package/dist/tools/shared/safe-env.js.map +1 -0
  195. package/dist/tools/sub-agent.d.ts +91 -0
  196. package/dist/tools/sub-agent.d.ts.map +1 -0
  197. package/dist/tools/sub-agent.js +89 -0
  198. package/dist/tools/sub-agent.js.map +1 -0
  199. package/dist/tools/task-output.d.ts +38 -0
  200. package/dist/tools/task-output.d.ts.map +1 -0
  201. package/dist/tools/task-output.js +186 -0
  202. package/dist/tools/task-output.js.map +1 -0
  203. package/dist/tools/tool-search/index.d.ts +40 -0
  204. package/dist/tools/tool-search/index.d.ts.map +1 -0
  205. package/dist/tools/tool-search/index.js +110 -0
  206. package/dist/tools/tool-search/index.js.map +1 -0
  207. package/dist/tools/tool-search/registry.d.ts +82 -0
  208. package/dist/tools/tool-search/registry.d.ts.map +1 -0
  209. package/dist/tools/tool-search/registry.js +238 -0
  210. package/dist/tools/tool-search/registry.js.map +1 -0
  211. package/dist/tools/undo-edit.d.ts +51 -0
  212. package/dist/tools/undo-edit.d.ts.map +1 -0
  213. package/dist/tools/undo-edit.js +231 -0
  214. package/dist/tools/undo-edit.js.map +1 -0
  215. package/dist/tools/web-fetch/cache.d.ts +49 -0
  216. package/dist/tools/web-fetch/cache.d.ts.map +1 -0
  217. package/dist/tools/web-fetch/cache.js +89 -0
  218. package/dist/tools/web-fetch/cache.js.map +1 -0
  219. package/dist/tools/web-fetch/index.d.ts +53 -0
  220. package/dist/tools/web-fetch/index.d.ts.map +1 -0
  221. package/dist/tools/web-fetch/index.js +513 -0
  222. package/dist/tools/web-fetch/index.js.map +1 -0
  223. package/dist/tools/write.d.ts +59 -0
  224. package/dist/tools/write.d.ts.map +1 -0
  225. package/dist/tools/write.js +316 -0
  226. package/dist/tools/write.js.map +1 -0
  227. package/dist/types.d.ts +881 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +16 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/working-tags.d.ts +44 -0
  232. package/dist/working-tags.d.ts.map +1 -0
  233. package/dist/working-tags.js +103 -0
  234. package/dist/working-tags.js.map +1 -0
  235. package/package.json +87 -0
  236. package/src/budget-guard.ts +170 -0
  237. package/src/compaction/compaction.ts +386 -0
  238. package/src/compaction/failsafe.ts +185 -0
  239. package/src/compaction/index.ts +1199 -0
  240. package/src/compaction/microcompaction.ts +709 -0
  241. package/src/compaction/observational/buffering.ts +430 -0
  242. package/src/compaction/observational/constants.ts +532 -0
  243. package/src/compaction/observational/index.ts +837 -0
  244. package/src/compaction/observational/observer.ts +510 -0
  245. package/src/compaction/observational/recall-tool.ts +130 -0
  246. package/src/compaction/observational/reflector.ts +221 -0
  247. package/src/compaction/observational/types.ts +343 -0
  248. package/src/context-manager.ts +237 -0
  249. package/src/cortex-agent.ts +4297 -0
  250. package/src/error-classifier.ts +199 -0
  251. package/src/event-bridge.ts +508 -0
  252. package/src/index.ts +292 -0
  253. package/src/mcp-client.ts +582 -0
  254. package/src/model-wrapper.ts +128 -0
  255. package/src/noop-logger.ts +9 -0
  256. package/src/prompt-diagnostics.ts +296 -0
  257. package/src/provider-manager.ts +823 -0
  258. package/src/provider-registry.ts +386 -0
  259. package/src/schema-converter.ts +51 -0
  260. package/src/skill-preprocessor.ts +314 -0
  261. package/src/skill-registry.ts +378 -0
  262. package/src/skill-tool.ts +130 -0
  263. package/src/sub-agent-manager.ts +236 -0
  264. package/src/token-estimator.ts +26 -0
  265. package/src/tool-contract.ts +113 -0
  266. package/src/tool-result-persistence.ts +197 -0
  267. package/src/tools/bash/index.ts +633 -0
  268. package/src/tools/bash/interactive.ts +302 -0
  269. package/src/tools/bash/safety.ts +1297 -0
  270. package/src/tools/edit.ts +422 -0
  271. package/src/tools/glob.ts +330 -0
  272. package/src/tools/grep.ts +819 -0
  273. package/src/tools/index.ts +110 -0
  274. package/src/tools/read.ts +580 -0
  275. package/src/tools/runtime.ts +173 -0
  276. package/src/tools/shared/cwd-tracker.ts +50 -0
  277. package/src/tools/shared/edit-history.ts +96 -0
  278. package/src/tools/shared/edit-matcher.ts +457 -0
  279. package/src/tools/shared/file-mutation-lock.ts +40 -0
  280. package/src/tools/shared/gitignore.ts +61 -0
  281. package/src/tools/shared/pdf-extractor.ts +290 -0
  282. package/src/tools/shared/read-registry.ts +93 -0
  283. package/src/tools/shared/safe-env.ts +82 -0
  284. package/src/tools/sub-agent.ts +171 -0
  285. package/src/tools/task-output.ts +236 -0
  286. package/src/tools/tool-search/index.ts +167 -0
  287. package/src/tools/tool-search/registry.ts +278 -0
  288. package/src/tools/undo-edit.ts +314 -0
  289. package/src/tools/web-fetch/cache.ts +112 -0
  290. package/src/tools/web-fetch/index.ts +604 -0
  291. package/src/tools/write.ts +385 -0
  292. package/src/types.ts +1057 -0
  293. package/src/working-tags.ts +118 -0
@@ -0,0 +1,290 @@
1
+ /**
2
+ * PDF text extraction.
3
+ *
4
+ * Wraps `unpdf` (pure-ESM, zero native deps) behind a narrow, well-typed
5
+ * boundary so the Read tool never touches pdfjs directly. Swapping the
6
+ * backend later is a one-file change.
7
+ *
8
+ * Responsibilities:
9
+ * - Parse the caller's `pages` spec and clamp it to the document and
10
+ * the per-call page cap.
11
+ * - Extract per-page text.
12
+ * - Detect "no extractable text" (scanned / image-only PDFs) and
13
+ * return a structured signal rather than silently-empty output.
14
+ * - Render the extracted text with `[Page N]` markers so the caller
15
+ * can line-number it exactly like any other file content.
16
+ *
17
+ * Pure-ish: does no filesystem I/O. Callers are expected to have
18
+ * already loaded the PDF bytes.
19
+ */
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ export interface PdfExtractionRequest {
26
+ /** PDF bytes. Accepts a Node Buffer or any Uint8Array. */
27
+ data: Buffer | Uint8Array;
28
+ /**
29
+ * Page spec: `"N"`, `"N-M"`, or undefined. When undefined, extracts
30
+ * the first `maxPages` pages starting at page 1.
31
+ */
32
+ pagesSpec?: string | undefined;
33
+ /** Upper bound on how many pages may be extracted in one call. */
34
+ maxPages?: number;
35
+ }
36
+
37
+ export interface PdfExtractionOk {
38
+ kind: 'ok';
39
+ totalPages: number;
40
+ /** First page extracted (1-based, inclusive). */
41
+ firstPage: number;
42
+ /** Last page extracted (1-based, inclusive). */
43
+ lastPage: number;
44
+ /** Per-page text. `pages[i].pageNumber` is 1-based. */
45
+ pages: Array<{ pageNumber: number; text: string }>;
46
+ /**
47
+ * Full rendered text with `[Page N]` markers separating pages, ready
48
+ * to be handed to the same line-numbering pipeline Read uses for
49
+ * plain text files.
50
+ */
51
+ rendered: string;
52
+ }
53
+
54
+ export interface PdfExtractionEmpty {
55
+ kind: 'empty';
56
+ totalPages: number;
57
+ firstPage: number;
58
+ lastPage: number;
59
+ message: string;
60
+ }
61
+
62
+ export interface PdfExtractionInvalidRange {
63
+ kind: 'invalid-range';
64
+ totalPages: number;
65
+ message: string;
66
+ }
67
+
68
+ export interface PdfExtractionError {
69
+ kind: 'error';
70
+ message: string;
71
+ }
72
+
73
+ export type PdfExtractionResult =
74
+ | PdfExtractionOk
75
+ | PdfExtractionEmpty
76
+ | PdfExtractionInvalidRange
77
+ | PdfExtractionError;
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Constants
81
+ // ---------------------------------------------------------------------------
82
+
83
+ /**
84
+ * Default cap on pages extracted per call. Matches the advertised
85
+ * schema default for Read's `pages` param ("max 20 pages per request").
86
+ */
87
+ export const DEFAULT_MAX_PAGES = 20;
88
+
89
+ /**
90
+ * Total-text length below which we treat the document as image-based.
91
+ * 20 characters allows for a stray page number or watermark without
92
+ * pretending we extracted meaningful content.
93
+ */
94
+ const EMPTY_TEXT_THRESHOLD = 20;
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Pages-spec parsing (pure, unit-testable)
98
+ // ---------------------------------------------------------------------------
99
+
100
+ export type PagesSpecResult =
101
+ | { ok: true; first: number; last: number }
102
+ | { ok: false; message: string };
103
+
104
+ /**
105
+ * Parse a pages-spec string against a document's page count and the
106
+ * per-call cap. Enforces:
107
+ * - Format: `"N"` or `"N-M"` (base 10, whitespace tolerant)
108
+ * - 1 <= first <= last <= totalPages
109
+ * - (last - first + 1) <= maxPages
110
+ *
111
+ * Returns `{ ok: false }` with an actionable message on any failure.
112
+ */
113
+ export function parsePagesSpec(
114
+ spec: string,
115
+ totalPages: number,
116
+ maxPages: number,
117
+ ): PagesSpecResult {
118
+ const trimmed = spec.trim();
119
+ const rangeMatch = /^(\d+)-(\d+)$/u.exec(trimmed);
120
+ const singleMatch = /^(\d+)$/u.exec(trimmed);
121
+
122
+ let first: number;
123
+ let last: number;
124
+ if (rangeMatch) {
125
+ first = Number.parseInt(rangeMatch[1]!, 10);
126
+ last = Number.parseInt(rangeMatch[2]!, 10);
127
+ } else if (singleMatch) {
128
+ first = Number.parseInt(singleMatch[1]!, 10);
129
+ last = first;
130
+ } else {
131
+ return {
132
+ ok: false,
133
+ message: `Invalid pages spec "${spec}". Use "N" or "N-M" (e.g. "1-5", "3").`,
134
+ };
135
+ }
136
+
137
+ if (first < 1) {
138
+ return { ok: false, message: `Page numbers are 1-based; got first page ${first}.` };
139
+ }
140
+ if (last < first) {
141
+ return {
142
+ ok: false,
143
+ message: `Invalid pages spec "${spec}": last page (${last}) is before first page (${first}).`,
144
+ };
145
+ }
146
+ if (last > totalPages) {
147
+ return {
148
+ ok: false,
149
+ message: `Pages spec "${spec}" exceeds document (has ${totalPages} page${totalPages === 1 ? '' : 's'}).`,
150
+ };
151
+ }
152
+ const count = last - first + 1;
153
+ if (count > maxPages) {
154
+ return {
155
+ ok: false,
156
+ message: `Pages spec "${spec}" requests ${count} pages; the per-call limit is ${maxPages}. Narrow the range.`,
157
+ };
158
+ }
159
+
160
+ return { ok: true, first, last };
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Buffer normalization
165
+ // ---------------------------------------------------------------------------
166
+
167
+ /**
168
+ * Convert input bytes to a fresh, owned `Uint8Array`.
169
+ *
170
+ * `unpdf` rejects `Buffer` inputs outright ("Please provide binary data
171
+ * as `Uint8Array`, rather than `Buffer`"), and its PDF.js worker path
172
+ * may transfer the backing buffer during postMessage — leaving a shared
173
+ * view detached for subsequent calls. Making a full copy here keeps
174
+ * the caller's buffer usable and makes repeat extractions on the same
175
+ * bytes safe across tests and sessions.
176
+ */
177
+ function toUint8Array(data: Buffer | Uint8Array): Uint8Array {
178
+ return new Uint8Array(data);
179
+ }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Extraction
183
+ // ---------------------------------------------------------------------------
184
+
185
+ /**
186
+ * Extract text from a PDF buffer. Never throws — all failure modes are
187
+ * returned as structured results so the caller can render them as
188
+ * tool-content messages.
189
+ */
190
+ export async function extractPdfText(
191
+ req: PdfExtractionRequest,
192
+ ): Promise<PdfExtractionResult> {
193
+ const maxPages = req.maxPages ?? DEFAULT_MAX_PAGES;
194
+ const bytes = toUint8Array(req.data);
195
+
196
+ let extractResult: { totalPages: number; text: string[] };
197
+ try {
198
+ const { extractText } = await import('unpdf');
199
+ const raw = await extractText(bytes, { mergePages: false });
200
+ extractResult = {
201
+ totalPages: raw.totalPages,
202
+ text: Array.isArray(raw.text) ? raw.text : [raw.text],
203
+ };
204
+ } catch (err: unknown) {
205
+ const msg = err instanceof Error ? err.message : String(err);
206
+ return {
207
+ kind: 'error',
208
+ message: `Failed to parse PDF: ${msg}`,
209
+ };
210
+ }
211
+
212
+ const { totalPages, text: allPages } = extractResult;
213
+
214
+ if (totalPages === 0) {
215
+ return {
216
+ kind: 'empty',
217
+ totalPages: 0,
218
+ firstPage: 0,
219
+ lastPage: 0,
220
+ message: 'PDF contains no pages.',
221
+ };
222
+ }
223
+
224
+ // Resolve the page range.
225
+ let first: number;
226
+ let last: number;
227
+ if (req.pagesSpec !== undefined) {
228
+ const parsed = parsePagesSpec(req.pagesSpec, totalPages, maxPages);
229
+ if (!parsed.ok) {
230
+ return { kind: 'invalid-range', totalPages, message: parsed.message };
231
+ }
232
+ first = parsed.first;
233
+ last = parsed.last;
234
+ } else {
235
+ first = 1;
236
+ last = Math.min(maxPages, totalPages);
237
+ }
238
+
239
+ const pages: Array<{ pageNumber: number; text: string }> = [];
240
+ for (let pageNumber = first; pageNumber <= last; pageNumber++) {
241
+ const raw = allPages[pageNumber - 1] ?? '';
242
+ pages.push({ pageNumber, text: raw });
243
+ }
244
+
245
+ const totalLen = pages.reduce((n, p) => n + p.text.trim().length, 0);
246
+ if (totalLen < EMPTY_TEXT_THRESHOLD) {
247
+ return {
248
+ kind: 'empty',
249
+ totalPages,
250
+ firstPage: first,
251
+ lastPage: last,
252
+ message:
253
+ totalPages > 0 && totalLen === 0
254
+ ? 'PDF has no extractable text (likely scanned or image-only). Use an OCR tool to process it.'
255
+ : 'PDF yielded almost no extractable text (likely scanned or image-heavy). Use an OCR tool for full content.',
256
+ };
257
+ }
258
+
259
+ const rendered = renderPages(pages, first, last, totalPages);
260
+
261
+ return {
262
+ kind: 'ok',
263
+ totalPages,
264
+ firstPage: first,
265
+ lastPage: last,
266
+ pages,
267
+ rendered,
268
+ };
269
+ }
270
+
271
+ /**
272
+ * Render the per-page text into a single string with `[Page N]`
273
+ * markers. A leading summary line is included so the model knows how
274
+ * many pages the document has and which subset it's seeing.
275
+ */
276
+ function renderPages(
277
+ pages: Array<{ pageNumber: number; text: string }>,
278
+ firstPage: number,
279
+ lastPage: number,
280
+ totalPages: number,
281
+ ): string {
282
+ const header =
283
+ firstPage === 1 && lastPage === totalPages
284
+ ? `[PDF: ${totalPages} page${totalPages === 1 ? '' : 's'}]`
285
+ : `[PDF: showing pages ${firstPage}-${lastPage} of ${totalPages}]`;
286
+ const body = pages
287
+ .map((p) => `[Page ${p.pageNumber}]\n${p.text.trim()}`)
288
+ .join('\n\n');
289
+ return `${header}\n\n${body}\n`;
290
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Loop-scoped file read tracking.
3
+ *
4
+ * Shared by the Read, Write, and Edit tools to enforce
5
+ * the read-before-write/edit contract. Tracks which files
6
+ * have been read during the current agentic loop, along with
7
+ * metadata (mtime, offset, limit) for file-unchanged dedup.
8
+ *
9
+ * Created once per CortexAgent and cleared at the start
10
+ * of each agentic loop via clear().
11
+ */
12
+
13
+ import * as path from 'node:path';
14
+
15
+ export interface ReadState {
16
+ /** File mtime at time of read (ms since epoch). */
17
+ timestamp: number;
18
+ /** 1-based offset used for the read (undefined = full read). */
19
+ offset?: number;
20
+ /** Line limit used for the read (undefined = default/full). */
21
+ limit?: number;
22
+ /**
23
+ * SHA-256 hex digest of the raw file bytes at the time of read.
24
+ * Populated only for non-truncated, full reads; used as a fallback
25
+ * on mtime mismatch to allow writes when the on-disk bytes are
26
+ * actually unchanged (e.g. a formatter or cloud-sync tool touched
27
+ * the mtime without modifying content).
28
+ */
29
+ contentHash?: string;
30
+ }
31
+
32
+ export class ReadRegistry {
33
+ private readonly entries = new Map<string, ReadState>();
34
+
35
+ /**
36
+ * Mark a file as read with metadata for dedup.
37
+ * The path is normalized to an absolute, platform-canonical form.
38
+ */
39
+ markRead(filePath: string, state?: ReadState): void {
40
+ this.entries.set(
41
+ this.normalize(filePath),
42
+ state ?? { timestamp: Date.now() },
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Check whether a file has been read in the current agentic loop.
48
+ */
49
+ hasBeenRead(filePath: string): boolean {
50
+ return this.entries.has(this.normalize(filePath));
51
+ }
52
+
53
+ /**
54
+ * Get the read state for a file, or undefined if not read.
55
+ */
56
+ getState(filePath: string): ReadState | undefined {
57
+ return this.entries.get(this.normalize(filePath));
58
+ }
59
+
60
+ /**
61
+ * Invalidate a single file's read state.
62
+ * Called when the on-disk mtime diverges from the recorded read
63
+ * state (external modification), forcing a fresh Read before
64
+ * the next mutation. Successful Edit/Write calls instead call
65
+ * markRead() with the new mtime, since the agent's own mutation
66
+ * is authoritative knowledge of current file contents.
67
+ */
68
+ invalidate(filePath: string): void {
69
+ this.entries.delete(this.normalize(filePath));
70
+ }
71
+
72
+ /**
73
+ * Clear all read tracking. Called at the start of each agentic loop.
74
+ */
75
+ clear(): void {
76
+ this.entries.clear();
77
+ }
78
+
79
+ /**
80
+ * Get the number of tracked files (for diagnostics).
81
+ */
82
+ get size(): number {
83
+ return this.entries.size;
84
+ }
85
+
86
+ /**
87
+ * Normalize a file path for consistent comparison.
88
+ * Resolves to absolute and normalizes separators.
89
+ */
90
+ private normalize(filePath: string): string {
91
+ return path.resolve(filePath);
92
+ }
93
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Shared environment sanitization for child processes.
3
+ *
4
+ * Used by both the Bash tool (safety.ts) and MCP client (mcp-client.ts)
5
+ * to strip dangerous environment variables before spawning subprocesses.
6
+ */
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Blocked variables
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const BLOCKED_ENV_PREFIXES = ['LD_', 'DYLD_', 'BASH_FUNC_'];
13
+
14
+ const BLOCKED_ENV_VARS = new Set([
15
+ // Runtime loaders
16
+ 'NODE_OPTIONS', 'NODE_PATH',
17
+ 'PYTHONPATH', 'PYTHONHOME',
18
+ 'PERL5LIB', 'PERL5OPT',
19
+ 'RUBYLIB', 'RUBYOPT',
20
+ // Shell startup injection
21
+ 'BASH_ENV', 'ENV', 'SHELLOPTS', 'PS4', 'IFS', 'PROMPT_COMMAND', 'ZDOTDIR',
22
+ // Git execution
23
+ 'GIT_EXTERNAL_DIFF', 'GIT_EXEC_PATH', 'GIT_SSH_COMMAND',
24
+ // Security-sensitive
25
+ 'SSLKEYLOGFILE', 'GCONV_PATH', 'OPENSSL_CONF', 'CURL_HOME', 'WGETRC',
26
+ ]);
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Public API
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /**
33
+ * Build a safe environment for child processes by stripping dangerous variables.
34
+ *
35
+ * @param parentEnv - The source environment (typically process.env or a consumer-supplied map)
36
+ * @param marker - Optional context marker added as CORTEX_SHELL. Pass undefined to skip.
37
+ * @param overrides - Optional key-value pairs merged ON TOP of the sanitized env, bypassing
38
+ * the blocklist. Used for consumer-set variables that must propagate (e.g., macOS dock
39
+ * icon suppression vars like DYLD_INSERT_LIBRARIES).
40
+ * @returns A new object with dangerous variables removed and overrides applied
41
+ */
42
+ export function buildSafeEnv(
43
+ parentEnv: NodeJS.ProcessEnv | Record<string, string>,
44
+ marker?: string | undefined,
45
+ overrides?: Record<string, string> | undefined,
46
+ ): Record<string, string> {
47
+ const env: Record<string, string> = {};
48
+
49
+ for (const [key, value] of Object.entries(parentEnv)) {
50
+ if (value === undefined) continue;
51
+
52
+ // Check exact match
53
+ if (BLOCKED_ENV_VARS.has(key)) continue;
54
+
55
+ // Check prefix match
56
+ let blocked = false;
57
+ for (const prefix of BLOCKED_ENV_PREFIXES) {
58
+ if (key.startsWith(prefix)) {
59
+ blocked = true;
60
+ break;
61
+ }
62
+ }
63
+ if (blocked) continue;
64
+
65
+ env[key] = value;
66
+ }
67
+
68
+ if (marker !== undefined) {
69
+ env['CORTEX_SHELL'] = marker;
70
+ }
71
+
72
+ // Merge overrides ON TOP of the sanitized env, bypassing the blocklist.
73
+ // This allows consumers to restore specific blocked variables (e.g.,
74
+ // DYLD_INSERT_LIBRARIES for macOS dock icon suppression).
75
+ if (overrides) {
76
+ for (const [key, value] of Object.entries(overrides)) {
77
+ env[key] = value;
78
+ }
79
+ }
80
+
81
+ return env;
82
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * SubAgent tool: spawn independent cortex-based sub-agents for delegated work.
3
+ *
4
+ * Supports foreground (blocking) and background (async) execution modes.
5
+ * Each sub-agent is an independent CortexAgent with its own message array
6
+ * and empty context slots.
7
+ *
8
+ * The SubAgent tool is ALWAYS excluded from child agents to prevent
9
+ * recursive spawning.
10
+ *
11
+ * References:
12
+ * - docs/cortex/tools/sub-agent.md
13
+ * - docs/cortex/plans/phase-4-sub-agents-and-skills.md
14
+ */
15
+
16
+ import { Type, type Static } from 'typebox';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Schema
20
+ // ---------------------------------------------------------------------------
21
+
22
+ export const SubAgentParams = Type.Object({
23
+ instructions: Type.String({
24
+ description: 'What the sub-agent should do. This becomes the sub-agent\'s initial prompt.',
25
+ }),
26
+ tools: Type.Optional(Type.Array(Type.String(), {
27
+ description: 'Tool names to make available. Default: inherits parent\'s registered tools.',
28
+ })),
29
+ systemPrompt: Type.Optional(Type.String({
30
+ description: 'Custom system prompt. Default: inherits parent\'s full system prompt.',
31
+ })),
32
+ maxTurns: Type.Optional(Type.Number({
33
+ description: 'Maximum LLM turns. Default: inherits parent\'s budget guard config.',
34
+ })),
35
+ maxCost: Type.Optional(Type.Number({
36
+ description: 'Maximum cost in USD. Default: inherits parent\'s budget guard config.',
37
+ })),
38
+ background: Type.Optional(Type.Boolean({
39
+ description: 'Run asynchronously. Default: false (blocks until complete).',
40
+ })),
41
+ });
42
+
43
+ export type SubAgentParamsType = Static<typeof SubAgentParams>;
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Details type (for UI/logs)
47
+ // ---------------------------------------------------------------------------
48
+
49
+ export interface SubAgentDetails {
50
+ taskId: string;
51
+ background: boolean;
52
+ status: string;
53
+ durationMs: number | null;
54
+ turns: number | null;
55
+ cost: number | null;
56
+ /** Model ID used by the sub-agent (inherited from parent). */
57
+ modelId?: string;
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Config
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /**
65
+ * Configuration passed to the SubAgent tool factory.
66
+ * The CortexAgent provides all of these at tool registration time.
67
+ */
68
+ export interface SubAgentToolConfig {
69
+ /**
70
+ * Spawn a sub-agent and run it. Returns the result when complete.
71
+ * The factory function handles CortexAgent creation, budget guard
72
+ * inheritance, tool filtering, and lifecycle management.
73
+ */
74
+ spawnSubAgent: (params: SubAgentParamsType) => Promise<{
75
+ taskId: string;
76
+ output: string;
77
+ status: string;
78
+ usage: { turns: number; cost: number; durationMs: number };
79
+ }>;
80
+
81
+ /**
82
+ * Spawn a background sub-agent. Returns the task ID immediately.
83
+ */
84
+ spawnBackgroundSubAgent: (params: SubAgentParamsType) => Promise<{
85
+ taskId: string;
86
+ }>;
87
+
88
+ /**
89
+ * Check if another sub-agent can be spawned.
90
+ */
91
+ canSpawn: () => boolean;
92
+
93
+ /**
94
+ * Get concurrency info for error messages.
95
+ */
96
+ getConcurrencyInfo: () => { active: number; limit: number };
97
+
98
+ /**
99
+ * Get the model ID for the child agent.
100
+ * Child agents inherit the parent's primary model.
101
+ */
102
+ getModelId: () => string;
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Tool name constant
107
+ // ---------------------------------------------------------------------------
108
+
109
+ export const SUB_AGENT_TOOL_NAME = 'SubAgent';
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Factory
113
+ // ---------------------------------------------------------------------------
114
+
115
+ /**
116
+ * Create the SubAgent tool.
117
+ *
118
+ * Returns a Cortex-native tool. CortexAgent adapts it to pi-agent-core's
119
+ * execute signature when synchronizing the tool inventory.
120
+ */
121
+ export function createSubAgentTool(config: SubAgentToolConfig): {
122
+ name: string;
123
+ description: string;
124
+ parameters: typeof SubAgentParams;
125
+ execute: (args: unknown) => Promise<unknown>;
126
+ } {
127
+ return {
128
+ name: SUB_AGENT_TOOL_NAME,
129
+ description: `Spawn a sub-agent to handle a delegated task independently. Use for tasks that are complex, long-running, or can proceed in parallel with your main work.
130
+
131
+ Foreground mode (default): Blocks until the sub-agent completes and returns its result directly. Use for quick, focused tasks where you need the result to continue.
132
+
133
+ Background mode (background: true): Returns a task ID immediately. The sub-agent runs independently. You will be notified when it completes. Use for long-running research, analysis, or multi-step work.
134
+
135
+ Sub-agents are independent: they have their own conversation, do not share your context, and cannot spawn further sub-agents. Give them clear, self-contained instructions.`,
136
+
137
+ parameters: SubAgentParams,
138
+
139
+ execute: async (args: unknown): Promise<unknown> => {
140
+ const params = args as SubAgentParamsType;
141
+
142
+ // Check concurrency limit
143
+ if (!config.canSpawn()) {
144
+ const info = config.getConcurrencyInfo();
145
+ return `Cannot spawn sub-agent: concurrency limit reached (${info.active}/${info.limit} active). Wait for a running sub-agent to complete or cancel one to free a slot.`;
146
+ }
147
+
148
+ // Background mode: spawn and return immediately
149
+ if (params.background) {
150
+ const { taskId } = await config.spawnBackgroundSubAgent(params);
151
+ return `Sub-agent spawned in background. Task ID: ${taskId}\nYou will be notified when it completes. Continue with other work.`;
152
+ }
153
+
154
+ // Foreground mode: block until complete
155
+ const result = await config.spawnSubAgent(params);
156
+
157
+ // Format result for the parent agent
158
+ const statusLine = result.status === 'completed'
159
+ ? 'Sub-agent completed successfully.'
160
+ : `Sub-agent finished with status: ${result.status}`;
161
+
162
+ const usageLine = `(${result.usage.turns} turns, $${result.usage.cost.toFixed(4)}, ${(result.usage.durationMs / 1000).toFixed(1)}s)`;
163
+
164
+ if (result.output) {
165
+ return `${statusLine} ${usageLine}\n\n${result.output}`;
166
+ }
167
+
168
+ return `${statusLine} ${usageLine}\n\nNo output was produced.`;
169
+ },
170
+ };
171
+ }