@aria-cli/tools 1.0.9 → 1.0.11

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 (241) hide show
  1. package/package.json +9 -5
  2. package/src/__tests__/web-fetch-download.test.ts +0 -433
  3. package/src/__tests__/web-tools.test.ts +0 -619
  4. package/src/ask-user-interaction.ts +0 -33
  5. package/src/cache/web-cache.ts +0 -110
  6. package/src/definitions/arion.ts +0 -118
  7. package/src/definitions/browser/browser.ts +0 -502
  8. package/src/definitions/browser/index.ts +0 -5
  9. package/src/definitions/browser/pw-downloads.ts +0 -142
  10. package/src/definitions/browser/pw-interactions.ts +0 -282
  11. package/src/definitions/browser/pw-responses.ts +0 -98
  12. package/src/definitions/browser/pw-session.ts +0 -405
  13. package/src/definitions/browser/pw-shared.ts +0 -85
  14. package/src/definitions/browser/pw-snapshot.ts +0 -383
  15. package/src/definitions/browser/pw-state.ts +0 -101
  16. package/src/definitions/browser/types.ts +0 -203
  17. package/src/definitions/code-intelligence.ts +0 -526
  18. package/src/definitions/core.ts +0 -118
  19. package/src/definitions/delegation.ts +0 -567
  20. package/src/definitions/deploy.ts +0 -73
  21. package/src/definitions/filesystem.ts +0 -217
  22. package/src/definitions/frg.ts +0 -67
  23. package/src/definitions/index.ts +0 -28
  24. package/src/definitions/memory.ts +0 -150
  25. package/src/definitions/messaging.ts +0 -734
  26. package/src/definitions/meta.ts +0 -392
  27. package/src/definitions/network.ts +0 -179
  28. package/src/definitions/outlook.ts +0 -318
  29. package/src/definitions/patch/apply-patch.ts +0 -235
  30. package/src/definitions/patch/fuzzy-match.ts +0 -217
  31. package/src/definitions/patch/index.ts +0 -1
  32. package/src/definitions/patch/patch-parser.ts +0 -297
  33. package/src/definitions/patch/sandbox-paths.ts +0 -129
  34. package/src/definitions/process/index.ts +0 -5
  35. package/src/definitions/process/process-registry.ts +0 -303
  36. package/src/definitions/process/process.ts +0 -456
  37. package/src/definitions/process/pty-keys.ts +0 -298
  38. package/src/definitions/process/session-slug.ts +0 -147
  39. package/src/definitions/quip.ts +0 -225
  40. package/src/definitions/search.ts +0 -67
  41. package/src/definitions/session-history.ts +0 -79
  42. package/src/definitions/shell.ts +0 -202
  43. package/src/definitions/slack.ts +0 -211
  44. package/src/definitions/web.ts +0 -119
  45. package/src/executors/apply-patch.ts +0 -1035
  46. package/src/executors/arion.ts +0 -199
  47. package/src/executors/code-intelligence.ts +0 -1179
  48. package/src/executors/deploy.ts +0 -1066
  49. package/src/executors/filesystem.ts +0 -1428
  50. package/src/executors/frg-freshness.ts +0 -743
  51. package/src/executors/frg.ts +0 -394
  52. package/src/executors/index.ts +0 -280
  53. package/src/executors/learning-meta.ts +0 -1367
  54. package/src/executors/lsp-client.ts +0 -355
  55. package/src/executors/memory.ts +0 -978
  56. package/src/executors/meta.ts +0 -293
  57. package/src/executors/process-registry.ts +0 -570
  58. package/src/executors/pty-session-store.ts +0 -43
  59. package/src/executors/pty.ts +0 -342
  60. package/src/executors/restart.ts +0 -133
  61. package/src/executors/search-freshness.ts +0 -249
  62. package/src/executors/search-types.ts +0 -98
  63. package/src/executors/search.ts +0 -89
  64. package/src/executors/self-diagnose.ts +0 -552
  65. package/src/executors/session-history.ts +0 -435
  66. package/src/executors/shell-safety.ts +0 -519
  67. package/src/executors/shell.ts +0 -1243
  68. package/src/executors/utils.ts +0 -40
  69. package/src/executors/web.ts +0 -786
  70. package/src/extraction/content-extraction.ts +0 -281
  71. package/src/extraction/index.ts +0 -5
  72. package/src/headless-control-contract.ts +0 -1149
  73. package/src/index.ts +0 -788
  74. package/src/local-control-http-auth.ts +0 -2
  75. package/src/mcp/client.ts +0 -218
  76. package/src/mcp/connection.ts +0 -568
  77. package/src/mcp/index.ts +0 -11
  78. package/src/mcp/jsonrpc.ts +0 -195
  79. package/src/mcp/types.ts +0 -199
  80. package/src/network-control-adapter.ts +0 -88
  81. package/src/network-runtime/address-types.ts +0 -218
  82. package/src/network-runtime/db-owner-fencing.ts +0 -91
  83. package/src/network-runtime/delivery-receipts.ts +0 -372
  84. package/src/network-runtime/direct-endpoint-authority.ts +0 -35
  85. package/src/network-runtime/index.ts +0 -316
  86. package/src/network-runtime/local-control-contract.ts +0 -784
  87. package/src/network-runtime/node-store-contract.ts +0 -46
  88. package/src/network-runtime/pair-route-contract.ts +0 -97
  89. package/src/network-runtime/peer-capabilities.ts +0 -48
  90. package/src/network-runtime/peer-principal-ref.ts +0 -20
  91. package/src/network-runtime/peer-state-machine.ts +0 -160
  92. package/src/network-runtime/protocol-schemas.ts +0 -265
  93. package/src/network-runtime/runtime-bootstrap-contract.ts +0 -83
  94. package/src/outlook/desktop-session.ts +0 -409
  95. package/src/policy.ts +0 -171
  96. package/src/providers/brave.ts +0 -80
  97. package/src/providers/duckduckgo.ts +0 -199
  98. package/src/providers/exa.ts +0 -85
  99. package/src/providers/firecrawl.ts +0 -77
  100. package/src/providers/index.ts +0 -8
  101. package/src/providers/jina.ts +0 -70
  102. package/src/providers/router.ts +0 -121
  103. package/src/providers/search-provider.ts +0 -74
  104. package/src/providers/tavily.ts +0 -74
  105. package/src/quip/desktop-session.ts +0 -435
  106. package/src/registry/index.ts +0 -1
  107. package/src/registry/registry.ts +0 -905
  108. package/src/runtime-socket-local-control-client.ts +0 -632
  109. package/src/security/dns-normalization.ts +0 -34
  110. package/src/security/dns-pinning.ts +0 -138
  111. package/src/security/external-content.ts +0 -129
  112. package/src/security/ssrf.ts +0 -207
  113. package/src/slack/desktop-session.ts +0 -493
  114. package/src/tool-factory.ts +0 -91
  115. package/src/types.ts +0 -1341
  116. package/src/utils/retry.ts +0 -163
  117. package/src/utils/safe-parse-json.ts +0 -176
  118. package/src/utils/url.ts +0 -20
  119. package/tests/benchmarks/registry.bench.ts +0 -57
  120. package/tests/cache/web-cache.test.ts +0 -147
  121. package/tests/critical-integration.test.ts +0 -1465
  122. package/tests/definitions/apply-patch.test.ts +0 -586
  123. package/tests/definitions/browser.test.ts +0 -495
  124. package/tests/definitions/delegation-pause-resume.test.ts +0 -758
  125. package/tests/definitions/execution.test.ts +0 -671
  126. package/tests/definitions/messaging-inbox-scope.test.ts +0 -229
  127. package/tests/definitions/messaging.test.ts +0 -1468
  128. package/tests/definitions/outlook.test.ts +0 -30
  129. package/tests/definitions/process.test.ts +0 -469
  130. package/tests/definitions/slack.test.ts +0 -28
  131. package/tests/definitions/tool-inventory.test.ts +0 -218
  132. package/tests/e2e/delegation-quest-orchestration.e2e.test.ts +0 -433
  133. package/tests/e2e/memory-tool-discovery-contract.e2e.test.ts +0 -81
  134. package/tests/executors/apply-patch.test.ts +0 -538
  135. package/tests/executors/arion.test.ts +0 -309
  136. package/tests/executors/conversation-primitives.test.ts +0 -250
  137. package/tests/executors/deploy.test.ts +0 -746
  138. package/tests/executors/filesystem-tools.test.ts +0 -357
  139. package/tests/executors/filesystem.test.ts +0 -959
  140. package/tests/executors/frg-freshness.test.ts +0 -136
  141. package/tests/executors/frg-merge.test.ts +0 -70
  142. package/tests/executors/frg-session-content.test.ts +0 -40
  143. package/tests/executors/frg.test.ts +0 -56
  144. package/tests/executors/memory-bugfixes.test.ts +0 -257
  145. package/tests/executors/memory-real-memoria.integration.test.ts +0 -316
  146. package/tests/executors/memory.test.ts +0 -853
  147. package/tests/executors/meta-tools.test.ts +0 -411
  148. package/tests/executors/meta.test.ts +0 -683
  149. package/tests/executors/path-containment.test.ts +0 -51
  150. package/tests/executors/process-registry.test.ts +0 -505
  151. package/tests/executors/pty.test.ts +0 -664
  152. package/tests/executors/quest-security.test.ts +0 -249
  153. package/tests/executors/read-file-media.test.ts +0 -230
  154. package/tests/executors/recall-knowledge-schema.test.ts +0 -209
  155. package/tests/executors/recall-tags.test.ts +0 -278
  156. package/tests/executors/remember-null-safety.contract.test.ts +0 -41
  157. package/tests/executors/restart.test.ts +0 -67
  158. package/tests/executors/search-unified.test.ts +0 -381
  159. package/tests/executors/session-history.test.ts +0 -340
  160. package/tests/executors/session-transcript.test.ts +0 -561
  161. package/tests/executors/shell-abort.test.ts +0 -416
  162. package/tests/executors/shell-env-blocklist.test.ts +0 -648
  163. package/tests/executors/shell-env-process.test.ts +0 -245
  164. package/tests/executors/shell-process-registry.test.ts +0 -334
  165. package/tests/executors/shell-tools.test.ts +0 -393
  166. package/tests/executors/shell.test.ts +0 -690
  167. package/tests/executors/web-abort-vs-timeout.test.ts +0 -213
  168. package/tests/executors/web-integration.test.ts +0 -633
  169. package/tests/executors/web-symlink.test.ts +0 -18
  170. package/tests/executors/web.test.ts +0 -1400
  171. package/tests/executors/write-stdin.test.ts +0 -145
  172. package/tests/extraction/content-extraction.test.ts +0 -153
  173. package/tests/guards/tools-default-test-lane.integration.test.ts +0 -21
  174. package/tests/guards/tools-package-test-commands.e2e.test.ts +0 -43
  175. package/tests/guards/tools-test-lane-manifest.contract.test.ts +0 -76
  176. package/tests/guards/tools-vitest-workspace-alias.contract.test.ts +0 -63
  177. package/tests/helpers/async-waits.ts +0 -53
  178. package/tests/integration/headless-control-contract.integration.test.ts +0 -153
  179. package/tests/integration/memory-tool-schema-parity.integration.test.ts +0 -67
  180. package/tests/integration/meta-tools-round-trip.integration.test.ts +0 -506
  181. package/tests/integration/quest-round-trip.test.ts +0 -303
  182. package/tests/integration/registry-executor-flow.test.ts +0 -85
  183. package/tests/integration.test.ts +0 -177
  184. package/tests/loading-tier.test.ts +0 -126
  185. package/tests/mcp/client-reconnect.test.ts +0 -267
  186. package/tests/mcp/connection.test.ts +0 -846
  187. package/tests/mcp/injectable-logger.test.ts +0 -83
  188. package/tests/mcp/jsonrpc.test.ts +0 -109
  189. package/tests/mcp/lifecycle.test.ts +0 -879
  190. package/tests/network-runtime/address-types.contract.test.ts +0 -143
  191. package/tests/network-runtime/continuity-bind-schema.contract.test.ts +0 -203
  192. package/tests/network-runtime/local-control-contract.test.ts +0 -869
  193. package/tests/network-runtime/local-control-invite-token.contract.test.ts +0 -146
  194. package/tests/network-runtime/node-store-contract.test.ts +0 -11
  195. package/tests/network-runtime/pair-protocol-nodeid.contract.test.ts +0 -15
  196. package/tests/network-runtime/peer-state-machine.contract.test.ts +0 -148
  197. package/tests/network-runtime/protocol-schemas.contract.test.ts +0 -512
  198. package/tests/network-runtime/relay-pending-nodeid.contract.test.ts +0 -62
  199. package/tests/network-runtime/runtime-bootstrap-contract.test.ts +0 -227
  200. package/tests/network-runtime/runtime-socket-local-control-client.test.ts +0 -621
  201. package/tests/network-runtime/wait-for-message-script.test.ts +0 -288
  202. package/tests/parallel.test.ts +0 -71
  203. package/tests/policy.test.ts +0 -184
  204. package/tests/print-default-test-lane.ts +0 -14
  205. package/tests/print-test-lane-manifest.ts +0 -22
  206. package/tests/providers/brave.test.ts +0 -159
  207. package/tests/providers/duckduckgo.test.ts +0 -207
  208. package/tests/providers/exa.test.ts +0 -175
  209. package/tests/providers/firecrawl.test.ts +0 -168
  210. package/tests/providers/jina.test.ts +0 -144
  211. package/tests/providers/router.test.ts +0 -328
  212. package/tests/providers/tavily.test.ts +0 -165
  213. package/tests/registry/discovery.test.ts +0 -154
  214. package/tests/registry/injectable-logger.test.ts +0 -230
  215. package/tests/registry/input-validation.test.ts +0 -361
  216. package/tests/registry/interface-completeness.test.ts +0 -85
  217. package/tests/registry/mcp-integration.test.ts +0 -103
  218. package/tests/registry/mcp-read-only-hint.test.ts +0 -60
  219. package/tests/registry/memoria-discovery.test.ts +0 -390
  220. package/tests/registry/nested-validation.test.ts +0 -283
  221. package/tests/registry/pseudo-tool-filtering.test.ts +0 -258
  222. package/tests/registry/registration-lifecycle.test.ts +0 -133
  223. package/tests/registry-validation.test.ts +0 -424
  224. package/tests/registry.test.ts +0 -460
  225. package/tests/security/dns-pinning.test.ts +0 -162
  226. package/tests/security/external-content.test.ts +0 -144
  227. package/tests/security/ssrf.test.ts +0 -118
  228. package/tests/shell-safety-integration.test.ts +0 -32
  229. package/tests/shell-safety.test.ts +0 -365
  230. package/tests/slack/desktop-session.test.ts +0 -50
  231. package/tests/test-lane-manifest.ts +0 -440
  232. package/tests/test-utils.ts +0 -27
  233. package/tests/tool-factory.test.ts +0 -188
  234. package/tests/utils/retry.test.ts +0 -231
  235. package/tests/utils/url.test.ts +0 -63
  236. package/tsconfig.cjs.json +0 -24
  237. package/tsconfig.json +0 -12
  238. package/vitest.config.ts +0 -55
  239. package/vitest.e2e.config.ts +0 -24
  240. package/vitest.integration.config.ts +0 -24
  241. package/vitest.native.config.ts +0 -24
@@ -1,905 +0,0 @@
1
- /**
2
- * Tool Registry - Manages tool registration and discovery
3
- */
4
-
5
- import type {
6
- Tool,
7
- ToolCategory,
8
- ToolContext,
9
- ToolResult,
10
- ToolMiddleware,
11
- RegisterOptions,
12
- ToolInfo,
13
- Logger,
14
- } from "../types.js";
15
- import type { ToolItem } from "@aria-cli/types";
16
- import { MCPClient } from "../mcp/client.js";
17
- import type { MCPServerConfig, MCPTool } from "../mcp/types.js";
18
- import { executeBash } from "../executors/shell.js";
19
-
20
- /**
21
- * Validate tool input against a JSON Schema object.
22
- *
23
- * Lightweight validation that covers the schemas produced by our tool factory
24
- * (Zod -> JSON Schema): checks top-level type, required properties, property
25
- * types, enums, patterns, numeric/string/array bounds, additionalProperties,
26
- * and recursively validates nested object/array schemas.
27
- * Does not pull in a full JSON Schema validator like Ajv to keep the dependency
28
- * footprint minimal.
29
- *
30
- * @returns null if valid, or a human-readable error string
31
- */
32
- function isPlainObject(value: unknown): value is Record<string, unknown> {
33
- return typeof value === "object" && value !== null && !Array.isArray(value);
34
- }
35
-
36
- function valueType(value: unknown): string {
37
- if (value === null) return "null";
38
- if (Array.isArray(value)) return "array";
39
- return typeof value;
40
- }
41
-
42
- function matchesJsonSchemaType(value: unknown, schemaType: string): boolean {
43
- if (schemaType === "integer") return typeof value === "number" && Number.isInteger(value);
44
- if (schemaType === "number") return typeof value === "number";
45
- if (schemaType === "string") return typeof value === "string";
46
- if (schemaType === "boolean") return typeof value === "boolean";
47
- if (schemaType === "array") return Array.isArray(value);
48
- if (schemaType === "object") return isPlainObject(value);
49
- if (schemaType === "null") return value === null;
50
- return true;
51
- }
52
-
53
- function pathLabel(path: string): string {
54
- return path ? `Property "${path}"` : "Value";
55
- }
56
-
57
- function validateValueAgainstSchema(
58
- value: unknown,
59
- schema: Record<string, unknown>,
60
- path: string,
61
- ): string | null {
62
- const expectedType = schema.type as string | string[] | undefined;
63
- if (expectedType) {
64
- const types = Array.isArray(expectedType) ? expectedType : [expectedType];
65
- const typeMatches = types.some((typeName) => matchesJsonSchemaType(value, typeName));
66
- if (!typeMatches) {
67
- return `${pathLabel(path)} expected type ${types.join(" | ")}; got ${valueType(value)}`;
68
- }
69
- }
70
-
71
- if (Array.isArray(schema.enum) && schema.enum.length > 0) {
72
- const allowed = schema.enum;
73
- const hasValue = allowed.some((candidate) => Object.is(candidate, value));
74
- if (!hasValue) {
75
- return `${pathLabel(path)} must be one of: ${allowed.map((item) => String(item)).join(", ")}`;
76
- }
77
- }
78
-
79
- if ("const" in schema && !Object.is(schema.const, value)) {
80
- return `${pathLabel(path)} must equal ${String(schema.const)}`;
81
- }
82
-
83
- if (typeof value === "string") {
84
- if (typeof schema.minLength === "number" && value.length < schema.minLength) {
85
- return `${pathLabel(path)} must have length >= ${schema.minLength}`;
86
- }
87
- if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
88
- return `${pathLabel(path)} must have length <= ${schema.maxLength}`;
89
- }
90
- if (typeof schema.pattern === "string") {
91
- try {
92
- const pattern = new RegExp(schema.pattern);
93
- if (!pattern.test(value)) {
94
- return `${pathLabel(path)} does not match required pattern`;
95
- }
96
- } catch {
97
- // Invalid schema pattern: ignore rather than failing all validation.
98
- }
99
- }
100
- }
101
-
102
- if (typeof value === "number") {
103
- if (typeof schema.minimum === "number" && value < schema.minimum) {
104
- return `${pathLabel(path)} must be >= ${schema.minimum}`;
105
- }
106
- if (typeof schema.maximum === "number" && value > schema.maximum) {
107
- return `${pathLabel(path)} must be <= ${schema.maximum}`;
108
- }
109
- if (typeof schema.exclusiveMinimum === "number" && value <= schema.exclusiveMinimum) {
110
- return `${pathLabel(path)} must be > ${schema.exclusiveMinimum}`;
111
- }
112
- if (typeof schema.exclusiveMaximum === "number" && value >= schema.exclusiveMaximum) {
113
- return `${pathLabel(path)} must be < ${schema.exclusiveMaximum}`;
114
- }
115
- if (typeof schema.multipleOf === "number" && schema.multipleOf > 0) {
116
- const quotient = value / schema.multipleOf;
117
- if (!Number.isInteger(quotient)) {
118
- return `${pathLabel(path)} must be a multiple of ${schema.multipleOf}`;
119
- }
120
- }
121
- }
122
-
123
- if (Array.isArray(value)) {
124
- if (typeof schema.minItems === "number" && value.length < schema.minItems) {
125
- return `${pathLabel(path)} must contain at least ${schema.minItems} item(s)`;
126
- }
127
- if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
128
- return `${pathLabel(path)} must contain at most ${schema.maxItems} item(s)`;
129
- }
130
-
131
- if (isPlainObject(schema.items)) {
132
- for (let index = 0; index < value.length; index++) {
133
- const itemError = validateValueAgainstSchema(
134
- value[index],
135
- schema.items,
136
- path ? `${path}[${index}]` : `[${index}]`,
137
- );
138
- if (itemError) return itemError;
139
- }
140
- }
141
- }
142
-
143
- if (isPlainObject(value)) {
144
- const required = Array.isArray(schema.required)
145
- ? schema.required.filter((item): item is string => typeof item === "string")
146
- : [];
147
- if (required.length > 0) {
148
- const missing = required.filter((key) => !(key in value) || value[key] === undefined);
149
- if (missing.length > 0) {
150
- return `${path ? `${path}: ` : ""}Missing required properties: ${missing.join(", ")}`;
151
- }
152
- }
153
-
154
- const properties = isPlainObject(schema.properties)
155
- ? (schema.properties as Record<string, Record<string, unknown>>)
156
- : undefined;
157
-
158
- if (properties) {
159
- for (const [key, propSchema] of Object.entries(properties)) {
160
- if (!(key in value) || value[key] === undefined) continue;
161
- if (!isPlainObject(propSchema)) continue;
162
- const childPath = path ? `${path}.${key}` : key;
163
- const nestedError = validateValueAgainstSchema(value[key], propSchema, childPath);
164
- if (nestedError) return nestedError;
165
- }
166
- }
167
-
168
- const additionalProperties = schema.additionalProperties;
169
- if (additionalProperties === false) {
170
- const allowed = new Set(properties ? Object.keys(properties) : []);
171
- const unknown = Object.keys(value).filter((key) => !allowed.has(key));
172
- if (unknown.length > 0) {
173
- if (path) {
174
- return `${path}: Unknown properties: ${unknown.join(", ")}`;
175
- }
176
- return `Unknown properties: ${unknown.join(", ")}`;
177
- }
178
- } else if (isPlainObject(additionalProperties)) {
179
- const allowed = new Set(properties ? Object.keys(properties) : []);
180
- for (const [extraKey, extraValue] of Object.entries(value)) {
181
- if (allowed.has(extraKey)) continue;
182
- const childPath = path ? `${path}.${extraKey}` : extraKey;
183
- const apError = validateValueAgainstSchema(extraValue, additionalProperties, childPath);
184
- if (apError) return apError;
185
- }
186
- }
187
- }
188
-
189
- return null;
190
- }
191
-
192
- export function validateToolInput(
193
- input: unknown,
194
- schema: Record<string, unknown>,
195
- path = "",
196
- ): string | null {
197
- // Only validate object-type schemas (all tool parameters are objects)
198
- if (schema.type !== "object") return null;
199
-
200
- const prefix = path ? `${path}: ` : "";
201
-
202
- // Input must be an object (or nullish, treated as empty object by many callers)
203
- if (input === null || input === undefined) {
204
- // If schema has required fields, this is invalid
205
- const required = schema.required as string[] | undefined;
206
- if (required && required.length > 0) {
207
- return `${prefix}Expected an object with required properties: ${required.join(", ")}; got ${input === null ? "null" : "undefined"}`;
208
- }
209
- return null;
210
- }
211
-
212
- if (typeof input !== "object" || Array.isArray(input)) {
213
- return `${prefix}Expected an object; got ${Array.isArray(input) ? "array" : typeof input}`;
214
- }
215
-
216
- return validateValueAgainstSchema(input, schema, path);
217
- }
218
-
219
- function isMcpToolReadOnly(tool: MCPTool): boolean {
220
- return tool.annotations?.readOnlyHint === true;
221
- }
222
-
223
- /**
224
- * Memoria-like interface for tool discovery
225
- */
226
- interface MemoriaLike {
227
- recallTools(options: {
228
- query: string;
229
- limit?: number;
230
- offset?: number;
231
- minConfidence?: number;
232
- matchAll?: boolean;
233
- updateAccessStats?: boolean;
234
- }): Promise<ToolItem[]>;
235
- rememberTool(
236
- tool: { name: string; description: string } & Record<string, unknown>,
237
- ): Promise<string>;
238
- }
239
-
240
- function normalizeDiscoveryIssues(rawFailures: unknown): string[] | undefined {
241
- if (!Array.isArray(rawFailures)) return undefined;
242
-
243
- const issues: string[] = [];
244
- for (const failure of rawFailures) {
245
- if (typeof failure === "string") {
246
- const trimmed = failure.trim();
247
- if (trimmed) issues.push(trimmed);
248
- continue;
249
- }
250
-
251
- if (!failure || typeof failure !== "object") continue;
252
- const record = failure as Record<string, unknown>;
253
-
254
- const primary =
255
- typeof record.error === "string" && record.error.trim()
256
- ? record.error.trim()
257
- : typeof record.message === "string" && record.message.trim()
258
- ? record.message.trim()
259
- : undefined;
260
-
261
- if (primary) issues.push(primary);
262
- }
263
-
264
- if (issues.length === 0) return undefined;
265
- return [...new Set(issues)];
266
- }
267
-
268
- function isShellTemplate(template: string): boolean {
269
- return template.trimStart().toLowerCase().startsWith("bash:");
270
- }
271
-
272
- function shellEscape(value: string): string {
273
- return `'${value.replace(/'/g, `'\"'\"'`)}'`;
274
- }
275
-
276
- function renderTemplate(
277
- template: string,
278
- inputObj: Record<string, unknown>,
279
- options?: { escapeForShell?: boolean },
280
- ): string {
281
- return template.replace(/\{\{(\w+)\}\}/g, (_match: string, key: string) => {
282
- const value = inputObj[key];
283
- if (value === undefined || value === null) return "";
284
- const stringValue = String(value);
285
- return options?.escapeForShell ? shellEscape(stringValue) : stringValue;
286
- });
287
- }
288
-
289
- export class ToolRegistry {
290
- private tools = new Map<string, Tool>();
291
- private middleware: ToolMiddleware[] = [];
292
- private mcpClient?: MCPClient;
293
- private logger: Logger;
294
-
295
- constructor(options?: { logger?: Logger }) {
296
- this.logger = options?.logger ?? console;
297
- }
298
-
299
- /**
300
- * Register a middleware to wrap tool execution.
301
- * Middlewares are applied in registration order (first registered = outermost).
302
- */
303
- use(mw: ToolMiddleware): void {
304
- this.middleware.push(mw);
305
- }
306
-
307
- /**
308
- * Register a tool
309
- * @throws Error if tool already registered and override not set
310
- */
311
- register(tool: Tool, options: RegisterOptions = {}): void {
312
- if (this.tools.has(tool.name) && !options.override) {
313
- throw new Error(
314
- `Tool "${tool.name}" is already registered. Use { override: true } to replace.`,
315
- );
316
- }
317
- this.tools.set(tool.name, tool);
318
- }
319
-
320
- /**
321
- * Get a tool by name
322
- */
323
- get(name: string): Tool | undefined {
324
- return this.tools.get(name);
325
- }
326
-
327
- /**
328
- * Check if a tool is registered
329
- */
330
- has(name: string): boolean {
331
- return this.tools.has(name);
332
- }
333
-
334
- /**
335
- * List all tools, optionally filtered by category
336
- */
337
- list(category?: ToolCategory): Tool[] {
338
- const all = Array.from(this.tools.values());
339
- if (!category) return all;
340
- return all.filter((tool) => tool.category === category);
341
- }
342
-
343
- /**
344
- * Unregister a tool
345
- * @returns true if tool was removed, false if not found
346
- */
347
- unregister(name: string): boolean {
348
- return this.tools.delete(name);
349
- }
350
-
351
- /**
352
- * Get count of registered tools
353
- */
354
- get size(): number {
355
- return this.tools.size;
356
- }
357
-
358
- /**
359
- * Clear all registered tools
360
- */
361
- clear(): void {
362
- this.tools.clear();
363
- }
364
-
365
- /**
366
- * Get all registered tools
367
- */
368
- getAll(): Tool[] {
369
- return Array.from(this.tools.values());
370
- }
371
-
372
- /**
373
- * Execute a tool by name with input validation.
374
- *
375
- * Validates `input` against the tool's JSON Schema `parameters` before
376
- * delegating to the tool's handler. Returns a structured error result
377
- * for unknown tools or validation failures instead of throwing.
378
- *
379
- * Tools without a `parameters` schema (or with a non-object schema)
380
- * skip validation and pass input directly to the handler.
381
- */
382
- async execute(name: string, input: unknown, context: ToolContext): Promise<ToolResult> {
383
- const tool = this.tools.get(name);
384
- if (!tool) {
385
- return {
386
- success: false,
387
- message: `Tool "${name}" is not registered.`,
388
- };
389
- }
390
-
391
- // Validate input against JSON Schema if the tool declares parameters
392
- if (tool.parameters && typeof tool.parameters === "object") {
393
- const schema = tool.parameters as Record<string, unknown>;
394
- const error = validateToolInput(input, schema);
395
- if (error) {
396
- return {
397
- success: false,
398
- message: `Invalid input for tool "${name}": ${error}`,
399
- };
400
- }
401
- }
402
-
403
- // If no middleware, execute directly
404
- if (this.middleware.length === 0) {
405
- return tool.execute(input, context);
406
- }
407
-
408
- // Run through middleware chain (first registered = outermost)
409
- const chain = this.middleware.reduceRight(
410
- (next: () => Promise<ToolResult>, mw: ToolMiddleware) => () => mw(tool, input, context, next),
411
- () => tool.execute(input, context),
412
- );
413
- return chain();
414
- }
415
-
416
- /**
417
- * Convert a Tool to ToolInfo for system prompt
418
- */
419
- private toolToInfo(tool: Tool): ToolInfo {
420
- const parameters: ToolInfo["parameters"] = [];
421
-
422
- if (tool.parameters && typeof tool.parameters === "object") {
423
- const schema = tool.parameters as {
424
- properties?: Record<string, { type?: string; description?: string }>;
425
- required?: string[];
426
- };
427
- if (schema.properties) {
428
- for (const [name, prop] of Object.entries(schema.properties)) {
429
- parameters.push({
430
- name,
431
- type: String(prop.type || "unknown"),
432
- required: schema.required?.includes(name) ?? false,
433
- description: prop.description,
434
- });
435
- }
436
- }
437
- }
438
-
439
- return {
440
- name: tool.name,
441
- description: tool.description,
442
- requiresConfirmation: tool.requiresConfirmation ?? tool.riskLevel === "dangerous",
443
- parameters,
444
- };
445
- }
446
-
447
- /**
448
- * Get tool info for all tools (for system prompt)
449
- */
450
- getToolInfos(): ToolInfo[] {
451
- return this.getAll().map((tool) => this.toolToInfo(tool));
452
- }
453
-
454
- /**
455
- * Search for tools by name or description
456
- */
457
- search(query: string): Tool[] {
458
- const queryLower = query.toLowerCase();
459
- return Array.from(this.tools.values()).filter(
460
- (tool) =>
461
- tool.name.toLowerCase().includes(queryLower) ||
462
- tool.description.toLowerCase().includes(queryLower),
463
- );
464
- }
465
-
466
- /**
467
- * Discover and load tools from Memoria knowledge base
468
- *
469
- * @param memoria - Memoria instance to query
470
- * @returns Number of tools discovered and loaded
471
- */
472
- async discoverFromMemoria(memoria: MemoriaLike): Promise<number> {
473
- let count = 0;
474
- let offset = 0;
475
- let pagesLoaded = 0;
476
- const seenPageSignatures = new Set<string>();
477
- const DISCOVERY_PAGE_SIZE = 200;
478
- const MAX_DISCOVERY_PAGES = 250;
479
-
480
- // Valid values for validation
481
- const validCategories = new Set([
482
- "filesystem",
483
- "code",
484
- "shell",
485
- "web",
486
- "data",
487
- "memory",
488
- "meta",
489
- "arion",
490
- ]);
491
- const validRiskLevels = new Set(["safe", "moderate", "dangerous"]);
492
-
493
- try {
494
- while (pagesLoaded < MAX_DISCOVERY_PAGES) {
495
- const tools = await memoria.recallTools({
496
- query: "",
497
- limit: DISCOVERY_PAGE_SIZE,
498
- offset,
499
- matchAll: true,
500
- updateAccessStats: false,
501
- });
502
- pagesLoaded += 1;
503
- if (tools.length === 0) break;
504
- const pageSignature = tools.map((tool) => String(tool.id ?? tool.name)).join("|");
505
- if (pageSignature && seenPageSignatures.has(pageSignature)) {
506
- this.logger.warn(
507
- "Memoria discovery received a duplicate page; stopping to avoid pagination loop",
508
- );
509
- break;
510
- }
511
- if (pageSignature) {
512
- seenPageSignatures.add(pageSignature);
513
- }
514
-
515
- for (const k of tools) {
516
- try {
517
- if (typeof k.name !== "string" || k.name.trim().length === 0) {
518
- this.logger.warn("Skipping Memoria tool with missing name");
519
- continue;
520
- }
521
- if (typeof k.description !== "string" || k.description.trim().length === 0) {
522
- this.logger.warn(`Skipping Memoria tool "${k.name}" with missing description`);
523
- continue;
524
- }
525
- const category =
526
- typeof k.category === "string" && validCategories.has(k.category)
527
- ? (k.category as Tool["category"])
528
- : "meta";
529
- if (category === "meta" && k.category !== "meta") {
530
- this.logger.warn(
531
- `Memoria tool "${k.name}" has invalid/missing category; defaulting to "meta"`,
532
- );
533
- }
534
- const parameters =
535
- k.parameters && typeof k.parameters === "object"
536
- ? (k.parameters as Record<string, unknown>)
537
- : { type: "object", properties: {}, additionalProperties: true };
538
- if (!k.parameters || typeof k.parameters !== "object") {
539
- this.logger.warn(
540
- `Memoria tool "${k.name}" has invalid/missing parameters; defaulting to permissive schema`,
541
- );
542
- }
543
-
544
- // Register only tools with explicit response templates.
545
- // Knowledge-only records are treated as non-executable metadata.
546
- const hasResponseTemplate =
547
- typeof k.responseTemplate === "string" && k.responseTemplate.trim().length > 0;
548
- if (!hasResponseTemplate) {
549
- this.logger.warn(
550
- `Skipping Memoria tool "${k.name}" without executable responseTemplate`,
551
- );
552
- continue;
553
- }
554
-
555
- const declaredRiskLevel =
556
- typeof k.riskLevel === "string" && validRiskLevels.has(k.riskLevel)
557
- ? k.riskLevel
558
- : "moderate";
559
- const isBashTemplate =
560
- typeof k.responseTemplate === "string" && isShellTemplate(k.responseTemplate);
561
- // Memoria bash templates execute shell commands and must always
562
- // flow through dangerous-risk approval paths.
563
- const riskLevel = isBashTemplate ? "dangerous" : declaredRiskLevel;
564
- const issues = normalizeDiscoveryIssues(
565
- (k as unknown as Record<string, unknown>).failures,
566
- );
567
-
568
- const toolDef: Record<string, unknown> = {
569
- description: k.description,
570
- category,
571
- parameters,
572
- riskLevel,
573
- };
574
- if (issues && issues.length > 0) toolDef.issues = issues;
575
- // Pass through executor-relevant fields from the tool definition.
576
- if (typeof k.responseTemplate === "string")
577
- toolDef.responseTemplate = k.responseTemplate;
578
-
579
- const tool: Tool = {
580
- name: k.name,
581
- description: k.description,
582
- category: category as Tool["category"],
583
- parameters: parameters as Tool["parameters"],
584
- riskLevel: riskLevel as Tool["riskLevel"],
585
- issues,
586
- loadingTier: "deferred",
587
- execute: this.createMemoriaToolExecutor(k.name, toolDef),
588
- };
589
-
590
- if (this.tools.has(tool.name)) {
591
- // Don't override core tools with Memoria-discovered tools
592
- continue;
593
- }
594
- this.register(tool);
595
- count++;
596
- } catch {
597
- // Skip invalid tool definitions
598
- this.logger.warn(`Invalid tool definition in Memoria: ${k.name}`);
599
- }
600
- }
601
-
602
- offset += tools.length;
603
- if (tools.length < DISCOVERY_PAGE_SIZE) break;
604
- }
605
-
606
- if (pagesLoaded >= MAX_DISCOVERY_PAGES) {
607
- this.logger.warn(
608
- `Stopped Memoria discovery after ${MAX_DISCOVERY_PAGES} pages to avoid unbounded paging`,
609
- );
610
- }
611
- } catch (error) {
612
- this.logger.error("Error discovering tools from Memoria:", error);
613
- }
614
-
615
- return count;
616
- }
617
-
618
- /**
619
- * Save a tool definition to Memoria
620
- *
621
- * @param name - Name of the tool to save
622
- * @param memoria - Memoria instance to save to
623
- */
624
- async saveToMemoria(name: string, memoria: MemoriaLike): Promise<void> {
625
- const tool = this.get(name);
626
- if (!tool) {
627
- throw new Error(`Tool "${name}" not found`);
628
- }
629
-
630
- const issues =
631
- Array.isArray(tool.issues) && tool.issues.length > 0
632
- ? tool.issues
633
- .map((issue) => (typeof issue === "string" ? issue.trim() : ""))
634
- .filter(Boolean)
635
- : [];
636
- const failures =
637
- issues.length > 0
638
- ? issues.map((issue) => ({
639
- timestamp: new Date(),
640
- error: issue,
641
- }))
642
- : undefined;
643
- const rawResponseTemplate = (tool as { responseTemplate?: unknown }).responseTemplate;
644
- const responseTemplate =
645
- typeof rawResponseTemplate === "string" && rawResponseTemplate.trim().length > 0
646
- ? rawResponseTemplate
647
- : undefined;
648
-
649
- const memoriaRecord: Record<string, unknown> = {
650
- name: tool.name,
651
- description: tool.description,
652
- category: tool.category,
653
- parameters: tool.parameters,
654
- riskLevel: tool.riskLevel,
655
- ...(failures ? { failures } : {}),
656
- };
657
-
658
- if (responseTemplate) {
659
- memoriaRecord.responseTemplate = responseTemplate;
660
- }
661
-
662
- await memoria.rememberTool(memoriaRecord as Parameters<MemoriaLike["rememberTool"]>[0]);
663
- }
664
-
665
- /**
666
- * Create an executor for a tool loaded from Memoria.
667
- *
668
- * Supports template execution only (no eval, no arbitrary code):
669
- *
670
- * **Response template**: If `toolDef.responseTemplate` is a string,
671
- * `{{key}}` placeholders are replaced with values from the input object.
672
- *
673
- * Input is validated against the full declared JSON Schema before interpolation.
674
- */
675
- private createMemoriaToolExecutor(
676
- name: string,
677
- toolDef: Record<string, unknown>,
678
- ): Tool["execute"] {
679
- return async (input: unknown, _context: ToolContext): Promise<ToolResult> => {
680
- try {
681
- const inputObj = (input && typeof input === "object" ? input : {}) as Record<
682
- string,
683
- unknown
684
- >;
685
-
686
- // Validate full input schema if one is present.
687
- const params = toolDef.parameters as Record<string, unknown> | undefined;
688
- if (params && typeof params === "object") {
689
- const validationError = validateToolInput(input, params);
690
- if (validationError) {
691
- return {
692
- success: false,
693
- message: `Invalid input for tool "${name}": ${validationError}`,
694
- };
695
- }
696
- }
697
-
698
- // Template mode with {{key}} interpolation
699
- if (typeof toolDef.responseTemplate === "string") {
700
- const template = toolDef.responseTemplate;
701
- const isBashTemplate = isShellTemplate(template);
702
- const rendered = renderTemplate(template, inputObj, {
703
- escapeForShell: isBashTemplate,
704
- });
705
- if (isBashTemplate) {
706
- const bashPrefixIndex = rendered.toLowerCase().indexOf("bash:");
707
- const command = rendered.slice(bashPrefixIndex + "bash:".length).trim();
708
- if (!command) {
709
- return {
710
- success: false,
711
- message: `Tool "${name}" has empty bash command template`,
712
- };
713
- }
714
- const bashResult = await executeBash(
715
- {
716
- command,
717
- cwd: _context.workingDir,
718
- env: _context.env,
719
- timeout: 30_000,
720
- },
721
- _context,
722
- );
723
- if (!bashResult.success) {
724
- return {
725
- success: false,
726
- message: `Tool "${name}" command failed: ${bashResult.message}`,
727
- };
728
- }
729
- const shellData =
730
- bashResult.data && typeof bashResult.data === "object"
731
- ? (bashResult.data as { stdout?: string; stderr?: string })
732
- : undefined;
733
- const output = [shellData?.stdout, shellData?.stderr].filter(Boolean).join("\n").trim();
734
- return {
735
- success: true,
736
- message: `Tool "${name}" executed command template.`,
737
- data: output,
738
- };
739
- }
740
- return {
741
- success: true,
742
- message: `Tool "${name}" executed with template response.`,
743
- data: rendered,
744
- };
745
- }
746
-
747
- // No executable content at all
748
- return {
749
- success: false,
750
- message:
751
- `Tool "${name}" was loaded from Memoria but has no executable content. ` +
752
- `Add a "responseTemplate" field to the tool definition.`,
753
- };
754
- } catch (error) {
755
- return {
756
- success: false,
757
- message: `Tool "${name}" execution failed: ${error instanceof Error ? error.message : String(error)}`,
758
- };
759
- }
760
- };
761
- }
762
-
763
- /**
764
- * Connect to MCP servers and register their tools
765
- */
766
- async connectMCP(configs: MCPServerConfig[]): Promise<{
767
- tools: number;
768
- resources: number;
769
- prompts: number;
770
- }> {
771
- if (this.mcpClient) {
772
- await this.disconnectMCP();
773
- }
774
- this.mcpClient = new MCPClient({ logger: this.logger });
775
-
776
- // Connect to all servers
777
- const results = await Promise.allSettled(configs.map((c) => this.mcpClient!.connect(c)));
778
- const failures = results
779
- .map((r, i) => (r.status === "rejected" ? configs[i]!.name : null))
780
- .filter(Boolean);
781
- if (failures.length > 0) {
782
- this.logger.warn(`Failed to connect MCP servers: ${failures.join(", ")}`);
783
- }
784
-
785
- // Register MCP tools
786
- const mcpTools = await this.mcpClient.listAllTools();
787
- for (const tool of mcpTools) {
788
- const isReadOnly = isMcpToolReadOnly(tool);
789
- this.register({
790
- name: `mcp__${tool.server}__${tool.name}`,
791
- description: tool.description || `MCP tool from ${tool.server}`,
792
- category: "meta",
793
- riskLevel: "moderate",
794
- isReadOnly,
795
- loadingTier: "always",
796
- parameters: tool.inputSchema,
797
- execute: async (args, ctx) => {
798
- if (ctx.abortSignal?.aborted) {
799
- return { success: false, message: "Cancelled" };
800
- }
801
- return this.mcpClient!.callTool(tool.server, tool.name, args, ctx.abortSignal);
802
- },
803
- });
804
- }
805
-
806
- // Listen for tool list changes
807
- this.mcpClient.on("toolsChanged", async (serverName: string) => {
808
- try {
809
- await this.refreshMCPTools(serverName);
810
- } catch (error) {
811
- this.logger.error(
812
- `Failed to refresh MCP tools for ${serverName}:`,
813
- (error as Error).message,
814
- );
815
- }
816
- });
817
-
818
- const resources = await this.mcpClient.listAllResources();
819
- const prompts = await this.mcpClient.listAllPrompts();
820
-
821
- return {
822
- tools: mcpTools.length,
823
- resources: resources.length,
824
- prompts: prompts.length,
825
- };
826
- }
827
-
828
- /**
829
- * Refresh tools from a specific MCP server
830
- */
831
- private async refreshMCPTools(serverName: string): Promise<void> {
832
- // Remove existing tools from this server
833
- const prefix = `mcp__${serverName}__`;
834
- for (const name of this.tools.keys()) {
835
- if (name.startsWith(prefix)) {
836
- this.unregister(name);
837
- }
838
- }
839
-
840
- // Re-register tools
841
- const tools = await this.mcpClient!.listAllTools();
842
- const serverTools = tools.filter((t) => t.server === serverName);
843
-
844
- for (const tool of serverTools) {
845
- const isReadOnly = isMcpToolReadOnly(tool);
846
- this.register({
847
- name: `mcp__${tool.server}__${tool.name}`,
848
- description: tool.description || `MCP tool from ${tool.server}`,
849
- category: "meta",
850
- riskLevel: "moderate",
851
- isReadOnly,
852
- loadingTier: "always",
853
- parameters: tool.inputSchema,
854
- execute: async (args, ctx) => {
855
- if (ctx.abortSignal?.aborted) {
856
- return { success: false, message: "Cancelled" };
857
- }
858
- return this.mcpClient!.callTool(tool.server, tool.name, args, ctx.abortSignal);
859
- },
860
- });
861
- }
862
- }
863
-
864
- /**
865
- * Disconnect from all MCP servers
866
- */
867
- async disconnectMCP(): Promise<void> {
868
- // Remove all MCP tools
869
- for (const name of this.tools.keys()) {
870
- if (name.startsWith("mcp__")) {
871
- this.unregister(name);
872
- }
873
- }
874
- await this.mcpClient?.disconnectAll();
875
- this.mcpClient = undefined;
876
- }
877
-
878
- /**
879
- * Get the MCP client for direct resource/prompt access
880
- */
881
- getMCPClient(): MCPClient | undefined {
882
- return this.mcpClient;
883
- }
884
-
885
- /**
886
- * Dispose of the registry, cleaning up all MCP connections.
887
- *
888
- * Shuts down every MCP server connection and clears all registered tools.
889
- * Safe to call multiple times (idempotent).
890
- */
891
- async dispose(): Promise<void> {
892
- await this.disconnectMCP();
893
- this.tools.clear();
894
- }
895
-
896
- /**
897
- * Async disposable support for `await using registry = new ToolRegistry()`.
898
- *
899
- * Delegates to dispose() so that ToolRegistry works with the Explicit
900
- * Resource Management proposal (using/await using).
901
- */
902
- async [Symbol.asyncDispose](): Promise<void> {
903
- await this.dispose();
904
- }
905
- }