@circuitwall/jarela 0.14.0 → 1.0.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 (164) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +1 -1
  3. package/.next/standalone/.next/build-manifest.json +2 -2
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  16. package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
  17. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  18. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  20. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/v1/agents/[id]/route.js +6 -1
  24. package/.next/standalone/.next/server/app/api/v1/agents/[id]/route.js.map +1 -1
  25. package/.next/standalone/.next/server/app/api/v1/agents/route.js +6 -1
  26. package/.next/standalone/.next/server/app/api/v1/agents/route.js.map +1 -1
  27. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js +9 -1
  28. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js.map +1 -1
  29. package/.next/standalone/.next/server/app/api/v1/bridges/route.js +9 -1
  30. package/.next/standalone/.next/server/app/api/v1/bridges/route.js.map +1 -1
  31. package/.next/standalone/.next/server/app/api/v1/builtin-tools/route.js +36 -29
  32. package/.next/standalone/.next/server/app/api/v1/builtin-tools/route.js.map +1 -1
  33. package/.next/standalone/.next/server/app/api/v1/events/route.js +7 -1
  34. package/.next/standalone/.next/server/app/api/v1/events/route.js.map +1 -1
  35. package/.next/standalone/.next/server/app/api/v1/extensions/route.js +3 -3
  36. package/.next/standalone/.next/server/app/api/v1/extensions/route.js.map +1 -1
  37. package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/secrets/route.js +4 -4
  38. package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/secrets/route.js.map +1 -1
  39. package/.next/standalone/.next/server/app/api/v1/health/route.js +7 -1
  40. package/.next/standalone/.next/server/app/api/v1/health/route.js.map +1 -1
  41. package/.next/standalone/.next/server/app/api/v1/mcp-servers/[name]/route.js +9 -1
  42. package/.next/standalone/.next/server/app/api/v1/mcp-servers/[name]/route.js.map +1 -1
  43. package/.next/standalone/.next/server/app/api/v1/mcp-servers/route.js +9 -1
  44. package/.next/standalone/.next/server/app/api/v1/mcp-servers/route.js.map +1 -1
  45. package/.next/standalone/.next/server/app/api/v1/models/route.js +6 -1
  46. package/.next/standalone/.next/server/app/api/v1/models/route.js.map +1 -1
  47. package/.next/standalone/.next/server/app/api/v1/page-capture/route.js +7 -1
  48. package/.next/standalone/.next/server/app/api/v1/page-capture/route.js.map +1 -1
  49. package/.next/standalone/.next/server/app/api/v1/pending-actions/[id]/approve/route.js +14 -7
  50. package/.next/standalone/.next/server/app/api/v1/pending-actions/[id]/approve/route.js.map +1 -1
  51. package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js +28 -0
  52. package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js.map +1 -1
  53. package/.next/standalone/.next/server/app/api/v1/providers/route.js +7 -1
  54. package/.next/standalone/.next/server/app/api/v1/providers/route.js.map +1 -1
  55. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/route.js +16 -2
  56. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/route.js.map +1 -1
  57. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +8 -1
  58. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js.map +1 -1
  59. package/.next/standalone/.next/server/app/api/v1/threads/route.js +6 -1
  60. package/.next/standalone/.next/server/app/api/v1/threads/route.js.map +1 -1
  61. package/.next/standalone/.next/server/app/api/v1/tools/route.js +10 -3
  62. package/.next/standalone/.next/server/app/api/v1/tools/route.js.map +1 -1
  63. package/.next/standalone/.next/server/app/index.html +2 -2
  64. package/.next/standalone/.next/server/app/index.rsc +3 -3
  65. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  66. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  67. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  68. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  69. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  70. package/.next/standalone/.next/server/app/page.js +56 -0
  71. package/.next/standalone/.next/server/app/page.js.map +1 -1
  72. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  73. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  74. package/.next/standalone/.next/server/app/setup.html +1 -1
  75. package/.next/standalone/.next/server/app/setup.rsc +2 -2
  76. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +2 -2
  77. package/.next/standalone/.next/server/app/setup.segments/_head.segment.rsc +1 -1
  78. package/.next/standalone/.next/server/app/setup.segments/_index.segment.rsc +2 -2
  79. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  80. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +1 -1
  81. package/.next/standalone/.next/server/app/setup.segments/setup.segment.rsc +1 -1
  82. package/.next/standalone/.next/server/app-paths-manifest.json +1 -1
  83. package/.next/standalone/.next/server/chunks/1683.js +2 -2
  84. package/.next/standalone/.next/server/chunks/2082.js +122 -13
  85. package/.next/standalone/.next/server/chunks/2082.js.map +1 -1
  86. package/.next/standalone/.next/server/chunks/210.js +3 -3
  87. package/.next/standalone/.next/server/chunks/210.js.map +1 -1
  88. package/.next/standalone/.next/server/chunks/239.js +1902 -1487
  89. package/.next/standalone/.next/server/chunks/239.js.map +1 -1
  90. package/.next/standalone/.next/server/chunks/2447.js +9 -1
  91. package/.next/standalone/.next/server/chunks/2447.js.map +1 -1
  92. package/.next/standalone/.next/server/chunks/423.js +125 -16
  93. package/.next/standalone/.next/server/chunks/423.js.map +1 -1
  94. package/.next/standalone/.next/server/chunks/4631.js +36 -29
  95. package/.next/standalone/.next/server/chunks/4631.js.map +1 -1
  96. package/.next/standalone/.next/server/chunks/5937.js +3 -2
  97. package/.next/standalone/.next/server/chunks/5937.js.map +1 -1
  98. package/.next/standalone/.next/server/chunks/{947.js → 8866.js} +11321 -10883
  99. package/.next/standalone/.next/server/chunks/8866.js.map +1 -0
  100. package/.next/standalone/.next/server/chunks/9032.js +3 -3
  101. package/.next/standalone/.next/server/chunks/9032.js.map +1 -1
  102. package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
  103. package/.next/standalone/.next/server/middleware.js +122 -13
  104. package/.next/standalone/.next/server/pages/404.html +2 -2
  105. package/.next/standalone/.next/server/pages/500.html +1 -1
  106. package/.next/standalone/.next/server/proxy.js.map +1 -1
  107. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  108. package/.next/standalone/.next/static/chunks/app/{page-473b39ec30c7f569.js → page-a7cae65f235e2942.js} +57 -1
  109. package/.next/standalone/.next/static/chunks/app/page-a7cae65f235e2942.js.map +1 -0
  110. package/.next/standalone/.next/static/css/{6f8b1a84bcbcd467.css → e57bdbbbb5a05779.css} +2 -2
  111. package/.next/standalone/.next/static/css/e57bdbbbb5a05779.css.map +1 -0
  112. package/.next/standalone/package.json +9 -1
  113. package/CHANGELOG.md +90 -0
  114. package/README.md +30 -2
  115. package/api/types.ts +8 -0
  116. package/app/api/v1/agents/[id]/route.ts +7 -0
  117. package/app/api/v1/agents/route.ts +7 -0
  118. package/app/api/v1/events/route.ts +8 -0
  119. package/app/api/v1/extensions/route.ts +2 -2
  120. package/app/api/v1/extensions/tools/[name]/secrets/route.ts +3 -3
  121. package/app/api/v1/health/route.ts +8 -0
  122. package/app/api/v1/models/route.ts +7 -0
  123. package/app/api/v1/page-capture/route.ts +8 -0
  124. package/app/api/v1/providers/route.ts +8 -0
  125. package/app/api/v1/threads/[thread_id]/route.ts +8 -0
  126. package/app/api/v1/threads/[thread_id]/run/route.ts +9 -0
  127. package/app/api/v1/threads/route.ts +7 -0
  128. package/app/api/v1/tools/route.ts +9 -0
  129. package/components/chat/ContextUsageBar.tsx +44 -0
  130. package/lib/agents/llm.ts +25 -2
  131. package/lib/agents/run-thread.ts +13 -1
  132. package/lib/agents/stream-collector.ts +9 -1
  133. package/lib/api/serializers.test.ts +15 -0
  134. package/lib/api/serializers.ts +8 -0
  135. package/lib/db/migrations.ts +15 -0
  136. package/lib/health/runner.test.ts +24 -2
  137. package/lib/mcp/registry.ts +14 -6
  138. package/lib/providers/anthropic.test.ts +95 -0
  139. package/lib/providers/anthropic.ts +106 -10
  140. package/lib/providers/jarela-chat-model.ts +9 -1
  141. package/lib/providers/known-context-windows.ts +21 -0
  142. package/lib/providers/types.ts +21 -1
  143. package/lib/stores/message-usage.test.ts +34 -0
  144. package/lib/stores/message-usage.ts +15 -3
  145. package/lib/stores/pricing.test.ts +52 -0
  146. package/lib/stores/pricing.ts +26 -1
  147. package/lib/tools/builtins.ts +4 -0
  148. package/lib/tools/extension-surfaces.test.ts +79 -0
  149. package/lib/tools/extension-surfaces.ts +153 -0
  150. package/lib/tools/index.ts +27 -8
  151. package/lib/tools/list-tools.test.ts +76 -0
  152. package/lib/tools/list-tools.ts +84 -0
  153. package/lib/tools/mcp-servers-info.test.ts +73 -0
  154. package/lib/tools/mcp-servers-info.ts +71 -0
  155. package/lib/tools/providers-info.test.ts +73 -0
  156. package/lib/tools/providers-info.ts +106 -0
  157. package/lib/tools/registry.ts +36 -25
  158. package/lib/tools/types.ts +13 -0
  159. package/package.json +9 -1
  160. package/.next/standalone/.next/server/chunks/947.js.map +0 -1
  161. package/.next/standalone/.next/static/chunks/app/page-473b39ec30c7f569.js.map +0 -1
  162. package/.next/standalone/.next/static/css/6f8b1a84bcbcd467.css.map +0 -1
  163. /package/.next/standalone/.next/static/{T0p2VVPsJPj44rwbmjaFb → d_vhp-lJqfdjRFpnLVIqZ}/_buildManifest.js +0 -0
  164. /package/.next/standalone/.next/static/{T0p2VVPsJPj44rwbmjaFb → d_vhp-lJqfdjRFpnLVIqZ}/_ssgManifest.js +0 -0
@@ -0,0 +1,153 @@
1
+ // Meta-introspection: a hand-curated catalog of every place a third party
2
+ // (or the agent itself, on behalf of the user) can extend or customize the
3
+ // app. Read-only. The tool returns a structured map that points to the
4
+ // registration entrypoint, the integration guide section, and an example
5
+ // file for each surface — so the agent can answer "how do I add an X?"
6
+ // or "what can be customized" without needing the docs in its context.
7
+ //
8
+ // SOURCE OF TRUTH: this file. When a new extension surface lands, add an
9
+ // entry below — it's a single source for both the agent runtime and the
10
+ // `docs/EXTENDING.md` table-of-contents anchor.
11
+
12
+ import { tool } from "@langchain/core/tools";
13
+ import { z } from "zod";
14
+ import { registerTools } from "./registry";
15
+
16
+ interface ExtensionSurface {
17
+ id: string;
18
+ name: string;
19
+ summary: string;
20
+ registration_entrypoint: string;
21
+ doc_section: string;
22
+ example_path?: string;
23
+ introspection_tool?: string;
24
+ related_adrs: string[];
25
+ }
26
+
27
+ const SURFACES: readonly ExtensionSurface[] = [
28
+ {
29
+ id: "llm_provider_builtin",
30
+ name: "Built-in LLM provider",
31
+ summary:
32
+ "Add a new in-tree adapter (Anthropic, OpenAI, Gemini, … pattern). " +
33
+ "Implements the ModelProvider interface and is registered via the static " +
34
+ "BUILTINS map in lib/providers/index.ts.",
35
+ registration_entrypoint: "lib/providers/index.ts (BUILTINS map)",
36
+ doc_section: "docs/EXTENDING.md#adding-an-llm-provider-built-in",
37
+ example_path: "lib/providers/anthropic.ts",
38
+ introspection_tool: "list_providers",
39
+ related_adrs: ["ADR-0013"],
40
+ },
41
+ {
42
+ id: "llm_provider_external",
43
+ name: "External LLM provider plugin",
44
+ summary:
45
+ "Drop a `.cjs` plugin into ~/.jarela/providers/ that exports an object " +
46
+ "matching ModelProvider. Hot-loaded per call (no rebuild).",
47
+ registration_entrypoint: "~/.jarela/providers/<name>.cjs",
48
+ doc_section: "docs/EXTENDING.md#adding-an-llm-provider-external",
49
+ example_path: "lib/providers/template-external.cjs.example",
50
+ introspection_tool: "list_providers",
51
+ related_adrs: ["ADR-0013"],
52
+ },
53
+ {
54
+ id: "builtin_tool",
55
+ name: "Built-in tool",
56
+ summary:
57
+ "Add a tool callable by the agent. Implement with @langchain/core/tools, " +
58
+ "register with `registerTools(category, capability, [tool])`, and add a " +
59
+ "side-effect import in lib/tools/builtins.ts. Capability gating is " +
60
+ "read | write | execute.",
61
+ registration_entrypoint: "lib/tools/<name>.ts (registerTools call) + lib/tools/builtins.ts",
62
+ doc_section: "docs/EXTENDING.md#adding-a-built-in-tool",
63
+ example_path: "lib/tools/template.ts",
64
+ introspection_tool: "list_tools",
65
+ related_adrs: ["ADR-0038"],
66
+ },
67
+ {
68
+ id: "mcp_server",
69
+ name: "MCP server",
70
+ summary:
71
+ "Connect a Model Context Protocol server (stdio or http transport). " +
72
+ "Configured via the UI (mcp_servers DB table) or programmatically through " +
73
+ "lib/stores/mcp-servers.ts. Tools auto-merge into the same pool as " +
74
+ "built-ins. Online discovery via the MCP registry is also supported.",
75
+ registration_entrypoint: "lib/stores/mcp-servers.ts (upsertMcpServer)",
76
+ doc_section: "docs/EXTENDING.md#adding-an-mcp-server",
77
+ introspection_tool: "list_mcp_servers",
78
+ related_adrs: ["ADR-0014"],
79
+ },
80
+ {
81
+ id: "agent_harness",
82
+ name: "Agent harness",
83
+ summary:
84
+ "Compose system-prompt sections that shape an agent's behaviour " +
85
+ "(plan-first, self-config, capability-listing, etc.). Built-in presets " +
86
+ "live in lib/agents/harness/presets.ts; custom harnesses are stored in " +
87
+ "the memory_store under the `app-settings` namespace.",
88
+ registration_entrypoint: "lib/agents/harness/presets.ts (built-in) or memory_store (custom)",
89
+ doc_section: "docs/EXTENDING.md#adding-a-custom-harness",
90
+ related_adrs: ["ADR-0036"],
91
+ },
92
+ {
93
+ id: "integration_manifest",
94
+ name: "Integration manifest",
95
+ summary:
96
+ "Add a new agent-led setup recipe — prerequisites, ordered steps, " +
97
+ "troubleshooting hints. The agent narrates the recipe and proposes " +
98
+ "the corresponding config changes through propose_config_change.",
99
+ registration_entrypoint: "lib/integrations/registry.ts",
100
+ doc_section: "docs/EXTENDING.md#adding-an-integration-manifest",
101
+ introspection_tool: "list_integrations",
102
+ related_adrs: ["ADR-0010"],
103
+ },
104
+ {
105
+ id: "brand_overlay",
106
+ name: "Brand / app identity",
107
+ summary:
108
+ "Customize the app's name, description, and issue URL via environment " +
109
+ "variables. No code change required. Used by downstream packages that " +
110
+ "consume @circuitwall/jarela as an npm dep (e.g. brand overlays).",
111
+ registration_entrypoint: "NEXT_PUBLIC_APP_NAME, NEXT_PUBLIC_APP_DESCRIPTION, NEXT_PUBLIC_APP_ISSUE_URL",
112
+ doc_section: "docs/EXTENDING.md#branding-the-app",
113
+ related_adrs: ["ADR-0005"],
114
+ },
115
+ ];
116
+
117
+ export const describeExtensionSurfacesTool = tool(
118
+ async () => {
119
+ return JSON.stringify({
120
+ surfaces: SURFACES,
121
+ count: SURFACES.length,
122
+ guide_path: "docs/EXTENDING.md",
123
+ contract_paths: [
124
+ "lib/providers/types.ts (ModelProvider interface)",
125
+ "lib/tools/types.ts (OpenAITool, ToolContext, InvokeMessage)",
126
+ "lib/tools/registry.ts (registerTools, ToolCategory, Capability)",
127
+ "lib/mcp/registry.ts (RegistryEntry, applyVariables)",
128
+ ],
129
+ notes: [
130
+ "Each surface's introspection_tool, when set, lets you enumerate what's " +
131
+ "currently registered. Call those when the user asks 'what's available' " +
132
+ "rather than describing the type from memory.",
133
+ "A surface can have either a static example_path (for external plugins) or " +
134
+ "no example (for surfaces wired entirely through the UI / DB). When in " +
135
+ "doubt, the doc_section under EXTENDING.md is the canonical walk-through.",
136
+ ],
137
+ });
138
+ },
139
+ {
140
+ name: "describe_extension_surfaces",
141
+ description:
142
+ "Return the curated catalog of every extension surface in this app — " +
143
+ "providers, tools, MCP servers, harnesses, integration manifests, brand " +
144
+ "overlay. Each entry has a registration entrypoint, an EXTENDING.md " +
145
+ "section anchor, and (when applicable) the introspection tool that lists " +
146
+ "what's currently registered. Read-only. Call this when the user asks " +
147
+ "'how do I add an X?' or 'what can be customized?' so you can guide " +
148
+ "them with the right files and docs.",
149
+ schema: z.object({}),
150
+ },
151
+ );
152
+
153
+ registerTools("Config", "read", [describeExtensionSurfacesTool]);
@@ -47,13 +47,32 @@ export {
47
47
  type ToolGroup,
48
48
  } from "./registry";
49
49
 
50
- const ALL_BUILTINS: StructuredToolInterface[] = registeredTools();
51
- export const BUILTIN_TOOL_NAMES: ReadonlySet<string> = registeredNames();
50
+ // Live accessors DO NOT snapshot at module-load time. Some tool modules
51
+ // import back from this file (for `getAllToolsAsync` etc.) and would create
52
+ // a circular import cycle: their `registerTools(...)` call runs AFTER this
53
+ // module finishes evaluating, so any captured snapshot here would miss them
54
+ // and they'd silently disappear from the agent's tool pool. Live calls dodge
55
+ // the problem — by the time anyone INVOKES `getAllTools()` / etc., every
56
+ // builtin has registered.
57
+ function allBuiltins(): StructuredToolInterface[] {
58
+ return registeredTools();
59
+ }
60
+ function builtinNames(): ReadonlySet<string> {
61
+ return registeredNames();
62
+ }
63
+
64
+ // Backwards-compatible export. Callers that import this Set get its current
65
+ // contents at the moment they read it (a fresh Set each access). Internal
66
+ // code prefers `builtinNames()` directly; this stays for external consumers
67
+ // like the extensions API route.
68
+ export function getBuiltinToolNames(): ReadonlySet<string> {
69
+ return builtinNames();
70
+ }
52
71
 
53
72
  // Per-call recompute so files dropped in $JARELA_TOOLS_DIR are picked up
54
73
  // without restart. loadExternalTools cache-busts require() per file.
55
74
  function loadExternal() {
56
- return loadExternalTools(BUILTIN_TOOL_NAMES);
75
+ return loadExternalTools(builtinNames());
57
76
  }
58
77
 
59
78
  export type ToolSource = "builtin" | "external" | "mcp";
@@ -63,7 +82,7 @@ export type ToolSource = "builtin" | "external" | "mcp";
63
82
  // neither a registered built-in nor an external (JARELA_TOOLS_DIR) tool —
64
83
  // matches today's behavior where MCP tools are everything else.
65
84
  export function getToolSource(name: string): ToolSource {
66
- if (BUILTIN_TOOL_NAMES.has(name)) return "builtin";
85
+ if (builtinNames().has(name)) return "builtin";
67
86
  if (loadExternal().tools.some((t) => t.name === name)) return "external";
68
87
  return "mcp";
69
88
  }
@@ -120,7 +139,7 @@ function applyCategoryToggles(tools: StructuredToolInterface[]): StructuredToolI
120
139
  // and any code path that can't await.
121
140
  export function getAllTools(policy?: ToolPolicy): StructuredToolInterface[] {
122
141
  return applyPolicy(
123
- [...applyCategoryToggles(ALL_BUILTINS), ...loadExternal().tools],
142
+ [...applyCategoryToggles(allBuiltins()), ...loadExternal().tools],
124
143
  policy,
125
144
  );
126
145
  }
@@ -137,7 +156,7 @@ export async function getAllToolsAsync(policy?: ToolPolicy): Promise<StructuredT
137
156
  console.error("[tools] MCP load failed, continuing with built-ins only:", err);
138
157
  }
139
158
  return applyPolicy(
140
- [...applyCategoryToggles(ALL_BUILTINS), ...loadExternal().tools, ...mcpTools],
159
+ [...applyCategoryToggles(allBuiltins()), ...loadExternal().tools, ...mcpTools],
141
160
  policy,
142
161
  );
143
162
  }
@@ -161,7 +180,7 @@ export async function executeTool(
161
180
  args: Record<string, unknown>,
162
181
  context: ToolContext = {},
163
182
  ): Promise<unknown> {
164
- let t = ALL_BUILTINS.find((x) => x.name === name);
183
+ let t = allBuiltins().find((x) => x.name === name);
165
184
  if (t) {
166
185
  const cat = registeredCategory(name);
167
186
  if (cat && disabledCategories().has(cat)) {
@@ -204,7 +223,7 @@ export function initTools(): InitToolsSummary {
204
223
  const toolsDir = getToolsDir();
205
224
  const result = loadExternal();
206
225
  const summary: InitToolsSummary = {
207
- builtinCount: ALL_BUILTINS.length,
226
+ builtinCount: allBuiltins().length,
208
227
  externalCount: result.tools.length,
209
228
  errors: result.errors,
210
229
  toolsDir,
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect, afterAll } from "vitest";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ const tmpRoot = mkdtempSync(join(tmpdir(), "jarela-test-list-tools-"));
7
+ process.env.HOME = tmpRoot;
8
+ process.env.USERPROFILE = tmpRoot;
9
+ process.env.JARELA_DB_DIR = join(tmpRoot, ".jarela-dbdir");
10
+ afterAll(() => {
11
+ try { rmSync(tmpRoot, { recursive: true, force: true }); } catch {}
12
+ });
13
+
14
+ const { listToolsTool } = await import("./list-tools");
15
+
16
+ interface Result {
17
+ tools: Array<{
18
+ name: string;
19
+ description: string;
20
+ category: string;
21
+ capability: string;
22
+ source: string;
23
+ group: string | null;
24
+ }>;
25
+ counts: {
26
+ total: number;
27
+ by_source: Record<string, number>;
28
+ by_capability: Record<string, number>;
29
+ };
30
+ }
31
+
32
+ function parse(s: string): Result {
33
+ return JSON.parse(s) as Result;
34
+ }
35
+
36
+ describe("list_tools", () => {
37
+ it("returns every built-in tool with category, capability, and source", async () => {
38
+ const out = parse(await listToolsTool.invoke({}));
39
+ expect(out.tools.length).toBeGreaterThan(0);
40
+ expect(out.counts.total).toBe(out.tools.length);
41
+
42
+ // Every entry has the documented shape.
43
+ for (const t of out.tools) {
44
+ expect(typeof t.name).toBe("string");
45
+ expect(typeof t.description).toBe("string");
46
+ expect(["read", "write", "execute"]).toContain(t.capability);
47
+ expect(["builtin", "external", "mcp"]).toContain(t.source);
48
+ }
49
+
50
+ // Sanity: list_tools itself is registered as builtin/Config/read.
51
+ const self = out.tools.find((t) => t.name === "list_tools");
52
+ expect(self).toBeDefined();
53
+ expect(self?.source).toBe("builtin");
54
+ expect(self?.category).toBe("Config");
55
+ expect(self?.capability).toBe("read");
56
+ });
57
+
58
+ it("filters by capability", async () => {
59
+ const out = parse(await listToolsTool.invoke({ capability: "read" }));
60
+ expect(out.tools.every((t) => t.capability === "read")).toBe(true);
61
+ expect(out.counts.total).toBe(out.tools.length);
62
+ });
63
+
64
+ it("filters by source", async () => {
65
+ const out = parse(await listToolsTool.invoke({ source: "builtin" }));
66
+ expect(out.tools.every((t) => t.source === "builtin")).toBe(true);
67
+ });
68
+
69
+ it("returns empty list (not error) when filters match nothing", async () => {
70
+ const out = parse(await listToolsTool.invoke({ source: "external", capability: "read" }));
71
+ // The clean-tmpdir HOME has no JARELA_TOOLS_DIR, so external is empty
72
+ // regardless of capability — verifying empty-result is a normal shape.
73
+ expect(out.tools).toEqual([]);
74
+ expect(out.counts.total).toBe(0);
75
+ });
76
+ });
@@ -0,0 +1,84 @@
1
+ // Read-only introspection tool — lets the agent enumerate every tool it has
2
+ // access to right now (built-in + external + MCP), so it can answer
3
+ // "what's in my toolbox / is X available" without the user having to
4
+ // describe the project's tool surface in the prompt.
5
+
6
+ import { tool } from "@langchain/core/tools";
7
+ import { z } from "zod";
8
+ import {
9
+ getAllToolsAsync,
10
+ getToolCategory,
11
+ getToolCapability,
12
+ getToolGroup,
13
+ getToolSource,
14
+ type ToolSource,
15
+ } from "./index";
16
+ import type { Capability, ToolCategory } from "./registry";
17
+ import { registerTools } from "./registry";
18
+
19
+ interface ToolSummary {
20
+ name: string;
21
+ description: string;
22
+ category: ToolCategory;
23
+ capability: Capability;
24
+ source: ToolSource;
25
+ group: string | null;
26
+ }
27
+
28
+ export const listToolsTool = tool(
29
+ async ({ category, capability, source }) => {
30
+ const all = await getAllToolsAsync();
31
+ const summaries: ToolSummary[] = all.map((t) => ({
32
+ name: t.name,
33
+ description: typeof t.description === "string" ? t.description : "",
34
+ category: getToolCategory(t.name),
35
+ capability: getToolCapability(t.name),
36
+ source: getToolSource(t.name),
37
+ group: getToolGroup(t.name),
38
+ }));
39
+
40
+ const filtered = summaries.filter((s) =>
41
+ (!category || s.category === category) &&
42
+ (!capability || s.capability === capability) &&
43
+ (!source || s.source === source),
44
+ );
45
+
46
+ const counts = {
47
+ total: filtered.length,
48
+ by_source: { builtin: 0, external: 0, mcp: 0 } as Record<ToolSource, number>,
49
+ by_capability: { read: 0, write: 0, execute: 0 } as Record<Capability, number>,
50
+ };
51
+ for (const s of filtered) {
52
+ counts.by_source[s.source]++;
53
+ counts.by_capability[s.capability]++;
54
+ }
55
+
56
+ return JSON.stringify({ tools: filtered, counts });
57
+ },
58
+ {
59
+ name: "list_tools",
60
+ description:
61
+ "List every tool currently available to this agent — built-in tools, " +
62
+ "external (~/.jarela/providers JS plugins), and MCP server tools — with " +
63
+ "category, capability (read/write/execute), and source. Read-only. " +
64
+ "Use this when the user asks 'what can you do?', when picking between " +
65
+ "tools for a task, or when troubleshooting whether a specific tool is " +
66
+ "registered. Optional filters narrow by category, capability, or source.",
67
+ schema: z.object({
68
+ category: z
69
+ .string()
70
+ .optional()
71
+ .describe("Optional category filter (e.g. 'Files', 'Mail', 'GitHub', 'MCP')"),
72
+ capability: z
73
+ .enum(["read", "write", "execute"])
74
+ .optional()
75
+ .describe("Optional capability filter — the safety class of the tool"),
76
+ source: z
77
+ .enum(["builtin", "external", "mcp"])
78
+ .optional()
79
+ .describe("Optional source filter — where the tool came from"),
80
+ }),
81
+ },
82
+ );
83
+
84
+ registerTools("Config", "read", [listToolsTool]);
@@ -0,0 +1,73 @@
1
+ import { describe, it, expect, afterAll } from "vitest";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ const tmpRoot = mkdtempSync(join(tmpdir(), "jarela-test-mcp-info-"));
7
+ process.env.HOME = tmpRoot;
8
+ process.env.USERPROFILE = tmpRoot;
9
+ process.env.JARELA_DB_DIR = join(tmpRoot, ".jarela-dbdir");
10
+ afterAll(() => {
11
+ try { rmSync(tmpRoot, { recursive: true, force: true }); } catch {}
12
+ });
13
+
14
+ const { listMcpServersTool } = await import("./mcp-servers-info");
15
+ const { upsertMcpServer } = await import("@/lib/stores/mcp-servers");
16
+
17
+ interface Result {
18
+ servers: Array<{
19
+ name: string;
20
+ transport: string;
21
+ enabled: boolean;
22
+ last_error: string | null;
23
+ tool_count: number;
24
+ }>;
25
+ count: number;
26
+ enabled_count: number;
27
+ total_mcp_tool_count: number;
28
+ notes: string[];
29
+ }
30
+
31
+ function parse(s: string): Result {
32
+ return JSON.parse(s) as Result;
33
+ }
34
+
35
+ describe("list_mcp_servers", () => {
36
+ it("returns empty list with zero counts when no servers are configured", async () => {
37
+ const out = parse(await listMcpServersTool.invoke({}));
38
+ expect(out.servers).toEqual([]);
39
+ expect(out.count).toBe(0);
40
+ expect(out.enabled_count).toBe(0);
41
+ expect(out.notes.length).toBeGreaterThan(0);
42
+ });
43
+
44
+ it("surfaces configured servers with enabled state and transport", async () => {
45
+ upsertMcpServer({
46
+ name: "fake-stdio-server",
47
+ transport: "stdio",
48
+ spec: { command: "echo", args: ["hello"] },
49
+ enabled: false,
50
+ });
51
+ upsertMcpServer({
52
+ name: "fake-http-server",
53
+ transport: "http",
54
+ spec: { url: "http://localhost:9999" },
55
+ enabled: true,
56
+ });
57
+
58
+ const out = parse(await listMcpServersTool.invoke({}));
59
+ const names = out.servers.map((s) => s.name).sort();
60
+ expect(names).toEqual(["fake-http-server", "fake-stdio-server"]);
61
+
62
+ const stdioRow = out.servers.find((s) => s.name === "fake-stdio-server");
63
+ expect(stdioRow?.transport).toBe("stdio");
64
+ expect(stdioRow?.enabled).toBe(false);
65
+
66
+ const httpRow = out.servers.find((s) => s.name === "fake-http-server");
67
+ expect(httpRow?.transport).toBe("http");
68
+ expect(httpRow?.enabled).toBe(true);
69
+
70
+ expect(out.enabled_count).toBe(1);
71
+ expect(out.count).toBe(2);
72
+ });
73
+ });
@@ -0,0 +1,71 @@
1
+ // Read-only introspection of configured MCP servers — names, transports,
2
+ // enabled/disabled state, last connection error, and the count of tools each
3
+ // server contributes to the live tool pool. Lets the agent answer "why is
4
+ // my Jira tool unavailable?" without bouncing the user through the UI.
5
+
6
+ import { tool } from "@langchain/core/tools";
7
+ import { z } from "zod";
8
+ import { listMcpServers } from "@/lib/stores/mcp-servers";
9
+ import { getAllToolsAsync } from "./index";
10
+ import { getToolSource } from "./index";
11
+ import { registerTools } from "./registry";
12
+
13
+ interface McpServerSummary {
14
+ name: string;
15
+ transport: "stdio" | "http";
16
+ enabled: boolean;
17
+ last_error: string | null;
18
+ tool_count: number;
19
+ created_at: string;
20
+ updated_at: string;
21
+ }
22
+
23
+ export const listMcpServersTool = tool(
24
+ async () => {
25
+ const rows = listMcpServers();
26
+ const allTools = await getAllToolsAsync();
27
+ // MCP tools don't carry their server name as a structured field — for
28
+ // the per-server count we count every tool whose source is "mcp", and
29
+ // we surface the total separately. Per-server attribution would require
30
+ // a deeper hook into MultiServerMCPClient's tool-namespace metadata,
31
+ // which is a bigger change than this introspection step warrants today.
32
+ const totalMcp = allTools.filter((t) => getToolSource(t.name) === "mcp").length;
33
+
34
+ const servers: McpServerSummary[] = rows.map((r) => ({
35
+ name: r.name,
36
+ transport: r.transport,
37
+ enabled: r.enabled === 1,
38
+ last_error: r.last_error,
39
+ // Per-server tool count not yet attributable; surface total on every
40
+ // row for now and document the limitation in `notes` below.
41
+ tool_count: r.enabled === 1 ? totalMcp : 0,
42
+ created_at: r.created_at,
43
+ updated_at: r.updated_at,
44
+ }));
45
+
46
+ return JSON.stringify({
47
+ servers,
48
+ count: servers.length,
49
+ enabled_count: servers.filter((s) => s.enabled).length,
50
+ total_mcp_tool_count: totalMcp,
51
+ notes: [
52
+ "tool_count is an aggregate across all enabled MCP servers — per-server " +
53
+ "attribution requires deeper namespace tracking that's not implemented yet.",
54
+ "last_error shows the most recent connection error if any; null means " +
55
+ "the server has never failed since last config change.",
56
+ ],
57
+ });
58
+ },
59
+ {
60
+ name: "list_mcp_servers",
61
+ description:
62
+ "List every configured MCP server (stdio or http transport) with its " +
63
+ "enabled state, last connection error, and the aggregate count of MCP " +
64
+ "tools currently in the agent's pool. Read-only. Use this when " +
65
+ "diagnosing 'my <X> tool isn't working' — check enabled and last_error " +
66
+ "before assuming the tool itself is broken.",
67
+ schema: z.object({}),
68
+ },
69
+ );
70
+
71
+ registerTools("Config", "read", [listMcpServersTool]);
@@ -0,0 +1,73 @@
1
+ import { describe, it, expect, afterAll } from "vitest";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ const tmpRoot = mkdtempSync(join(tmpdir(), "jarela-test-providers-info-"));
7
+ process.env.HOME = tmpRoot;
8
+ process.env.USERPROFILE = tmpRoot;
9
+ process.env.JARELA_DB_DIR = join(tmpRoot, ".jarela-dbdir");
10
+ afterAll(() => {
11
+ try { rmSync(tmpRoot, { recursive: true, force: true }); } catch {}
12
+ });
13
+
14
+ const { listProvidersTool, describeProviderTool } = await import("./providers-info");
15
+
16
+ function parse<T = unknown>(s: string): T {
17
+ return JSON.parse(s) as T;
18
+ }
19
+
20
+ describe("list_providers", () => {
21
+ it("returns all built-in providers with source label", async () => {
22
+ const out = parse<{
23
+ providers: Array<{ name: string; source: string }>;
24
+ count: number;
25
+ builtin_count: number;
26
+ external_count: number;
27
+ }>(await listProvidersTool.invoke({}));
28
+
29
+ const names = out.providers.map((p) => p.name);
30
+ expect(names).toContain("anthropic");
31
+ expect(names).toContain("openai");
32
+ expect(names).toContain("gemini");
33
+ expect(out.providers.every((p) => p.source === "builtin" || p.source === "external")).toBe(true);
34
+ expect(out.count).toBe(out.providers.length);
35
+ expect(out.builtin_count + out.external_count).toBe(out.count);
36
+ });
37
+ });
38
+
39
+ describe("describe_provider", () => {
40
+ it("returns capability flags and known models for anthropic", async () => {
41
+ const out = parse<{
42
+ name: string;
43
+ source: string;
44
+ capabilities: Record<string, boolean>;
45
+ known_models: Array<{ model_id: string; context_length: number }>;
46
+ }>(await describeProviderTool.invoke({ name: "anthropic" }));
47
+
48
+ expect(out.name).toBe("anthropic");
49
+ expect(out.source).toBe("builtin");
50
+ expect(out.capabilities.chat).toBe(true);
51
+ expect(out.capabilities.invoke).toBe(true);
52
+ expect(out.capabilities.stream_invoke).toBe(true);
53
+ // Anthropic adapter does not implement embed today.
54
+ expect(out.capabilities.embed).toBe(false);
55
+ expect(out.known_models.length).toBeGreaterThan(0);
56
+ expect(out.known_models.some((m) => m.model_id.startsWith("claude-"))).toBe(true);
57
+ });
58
+
59
+ it("returns an error object (not throw) for unknown provider", async () => {
60
+ const out = parse<{ error?: string; hint?: string }>(
61
+ await describeProviderTool.invoke({ name: "nonexistent-provider-xyz" }),
62
+ );
63
+ expect(out.error).toBeDefined();
64
+ expect(out.hint).toMatch(/list_providers/);
65
+ });
66
+
67
+ it("returns an empty known_models list for providers without static catalog", async () => {
68
+ const out = parse<{ known_models: unknown[] }>(
69
+ await describeProviderTool.invoke({ name: "langchain" }),
70
+ );
71
+ expect(out.known_models).toEqual([]);
72
+ });
73
+ });