joonecli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/AGENTS.md +56 -0
  2. package/Handover.md +115 -0
  3. package/LICENSE +201 -0
  4. package/PROGRESS.md +160 -0
  5. package/README.md +114 -0
  6. package/dist/__tests__/bootstrap.test.d.ts +1 -0
  7. package/dist/__tests__/bootstrap.test.js +76 -0
  8. package/dist/__tests__/bootstrap.test.js.map +1 -0
  9. package/dist/__tests__/config.test.d.ts +1 -0
  10. package/dist/__tests__/config.test.js +84 -0
  11. package/dist/__tests__/config.test.js.map +1 -0
  12. package/dist/__tests__/m55.test.d.ts +1 -0
  13. package/dist/__tests__/m55.test.js +160 -0
  14. package/dist/__tests__/m55.test.js.map +1 -0
  15. package/dist/__tests__/middleware.test.d.ts +1 -0
  16. package/dist/__tests__/middleware.test.js +169 -0
  17. package/dist/__tests__/middleware.test.js.map +1 -0
  18. package/dist/__tests__/modelFactory.test.d.ts +1 -0
  19. package/dist/__tests__/modelFactory.test.js +50 -0
  20. package/dist/__tests__/modelFactory.test.js.map +1 -0
  21. package/dist/__tests__/optimizations.test.d.ts +1 -0
  22. package/dist/__tests__/optimizations.test.js +136 -0
  23. package/dist/__tests__/optimizations.test.js.map +1 -0
  24. package/dist/__tests__/promptBuilder.test.d.ts +1 -0
  25. package/dist/__tests__/promptBuilder.test.js +108 -0
  26. package/dist/__tests__/promptBuilder.test.js.map +1 -0
  27. package/dist/__tests__/sandbox.test.d.ts +1 -0
  28. package/dist/__tests__/sandbox.test.js +78 -0
  29. package/dist/__tests__/sandbox.test.js.map +1 -0
  30. package/dist/__tests__/security.test.d.ts +1 -0
  31. package/dist/__tests__/security.test.js +86 -0
  32. package/dist/__tests__/security.test.js.map +1 -0
  33. package/dist/__tests__/streaming.test.d.ts +1 -0
  34. package/dist/__tests__/streaming.test.js +71 -0
  35. package/dist/__tests__/streaming.test.js.map +1 -0
  36. package/dist/__tests__/toolRouter.test.d.ts +1 -0
  37. package/dist/__tests__/toolRouter.test.js +37 -0
  38. package/dist/__tests__/toolRouter.test.js.map +1 -0
  39. package/dist/__tests__/tools.test.d.ts +1 -0
  40. package/dist/__tests__/tools.test.js +112 -0
  41. package/dist/__tests__/tools.test.js.map +1 -0
  42. package/dist/__tests__/tracing.test.d.ts +1 -0
  43. package/dist/__tests__/tracing.test.js +147 -0
  44. package/dist/__tests__/tracing.test.js.map +1 -0
  45. package/dist/cli/config.d.ts +49 -0
  46. package/dist/cli/config.js +86 -0
  47. package/dist/cli/config.js.map +1 -0
  48. package/dist/cli/index.d.ts +2 -0
  49. package/dist/cli/index.js +625 -0
  50. package/dist/cli/index.js.map +1 -0
  51. package/dist/cli/modelFactory.d.ts +9 -0
  52. package/dist/cli/modelFactory.js +154 -0
  53. package/dist/cli/modelFactory.js.map +1 -0
  54. package/dist/cli/providers.d.ts +18 -0
  55. package/dist/cli/providers.js +94 -0
  56. package/dist/cli/providers.js.map +1 -0
  57. package/dist/core/agentLoop.d.ts +43 -0
  58. package/dist/core/agentLoop.js +245 -0
  59. package/dist/core/agentLoop.js.map +1 -0
  60. package/dist/core/errors.d.ts +62 -0
  61. package/dist/core/errors.js +139 -0
  62. package/dist/core/errors.js.map +1 -0
  63. package/dist/core/promptBuilder.d.ts +49 -0
  64. package/dist/core/promptBuilder.js +84 -0
  65. package/dist/core/promptBuilder.js.map +1 -0
  66. package/dist/core/reasoningRouter.d.ts +62 -0
  67. package/dist/core/reasoningRouter.js +102 -0
  68. package/dist/core/reasoningRouter.js.map +1 -0
  69. package/dist/core/retry.d.ts +25 -0
  70. package/dist/core/retry.js +49 -0
  71. package/dist/core/retry.js.map +1 -0
  72. package/dist/core/sessionResumer.d.ts +17 -0
  73. package/dist/core/sessionResumer.js +78 -0
  74. package/dist/core/sessionResumer.js.map +1 -0
  75. package/dist/core/sessionStore.d.ts +45 -0
  76. package/dist/core/sessionStore.js +167 -0
  77. package/dist/core/sessionStore.js.map +1 -0
  78. package/dist/core/tokenCounter.d.ts +17 -0
  79. package/dist/core/tokenCounter.js +54 -0
  80. package/dist/core/tokenCounter.js.map +1 -0
  81. package/dist/evals/dataset.d.ts +4 -0
  82. package/dist/evals/dataset.js +61 -0
  83. package/dist/evals/dataset.js.map +1 -0
  84. package/dist/evals/evaluator.d.ts +21 -0
  85. package/dist/evals/evaluator.js +68 -0
  86. package/dist/evals/evaluator.js.map +1 -0
  87. package/dist/hitl/bridge.d.ts +65 -0
  88. package/dist/hitl/bridge.js +120 -0
  89. package/dist/hitl/bridge.js.map +1 -0
  90. package/dist/middleware/commandSanitizer.d.ts +18 -0
  91. package/dist/middleware/commandSanitizer.js +50 -0
  92. package/dist/middleware/commandSanitizer.js.map +1 -0
  93. package/dist/middleware/loopDetection.d.ts +28 -0
  94. package/dist/middleware/loopDetection.js +49 -0
  95. package/dist/middleware/loopDetection.js.map +1 -0
  96. package/dist/middleware/permission.d.ts +17 -0
  97. package/dist/middleware/permission.js +59 -0
  98. package/dist/middleware/permission.js.map +1 -0
  99. package/dist/middleware/pipeline.d.ts +31 -0
  100. package/dist/middleware/pipeline.js +62 -0
  101. package/dist/middleware/pipeline.js.map +1 -0
  102. package/dist/middleware/preCompletion.d.ts +29 -0
  103. package/dist/middleware/preCompletion.js +82 -0
  104. package/dist/middleware/preCompletion.js.map +1 -0
  105. package/dist/middleware/types.d.ts +40 -0
  106. package/dist/middleware/types.js +8 -0
  107. package/dist/middleware/types.js.map +1 -0
  108. package/dist/sandbox/bootstrap.d.ts +38 -0
  109. package/dist/sandbox/bootstrap.js +107 -0
  110. package/dist/sandbox/bootstrap.js.map +1 -0
  111. package/dist/sandbox/manager.d.ts +72 -0
  112. package/dist/sandbox/manager.js +180 -0
  113. package/dist/sandbox/manager.js.map +1 -0
  114. package/dist/sandbox/sync.d.ts +55 -0
  115. package/dist/sandbox/sync.js +135 -0
  116. package/dist/sandbox/sync.js.map +1 -0
  117. package/dist/skills/loader.d.ts +55 -0
  118. package/dist/skills/loader.js +132 -0
  119. package/dist/skills/loader.js.map +1 -0
  120. package/dist/skills/tools.d.ts +5 -0
  121. package/dist/skills/tools.js +78 -0
  122. package/dist/skills/tools.js.map +1 -0
  123. package/dist/skills/types.d.ts +13 -0
  124. package/dist/skills/types.js +2 -0
  125. package/dist/skills/types.js.map +1 -0
  126. package/dist/test_cache.d.ts +1 -0
  127. package/dist/test_cache.js +55 -0
  128. package/dist/test_cache.js.map +1 -0
  129. package/dist/test_google.js +93 -0
  130. package/dist/tools/askUser.d.ts +10 -0
  131. package/dist/tools/askUser.js +42 -0
  132. package/dist/tools/askUser.js.map +1 -0
  133. package/dist/tools/browser.d.ts +19 -0
  134. package/dist/tools/browser.js +111 -0
  135. package/dist/tools/browser.js.map +1 -0
  136. package/dist/tools/index.d.ts +27 -0
  137. package/dist/tools/index.js +184 -0
  138. package/dist/tools/index.js.map +1 -0
  139. package/dist/tools/registry.d.ts +31 -0
  140. package/dist/tools/registry.js +168 -0
  141. package/dist/tools/registry.js.map +1 -0
  142. package/dist/tools/router.d.ts +34 -0
  143. package/dist/tools/router.js +73 -0
  144. package/dist/tools/router.js.map +1 -0
  145. package/dist/tools/security.d.ts +28 -0
  146. package/dist/tools/security.js +183 -0
  147. package/dist/tools/security.js.map +1 -0
  148. package/dist/tools/webSearch.d.ts +6 -0
  149. package/dist/tools/webSearch.js +120 -0
  150. package/dist/tools/webSearch.js.map +1 -0
  151. package/dist/tracing/analyzer.d.ts +58 -0
  152. package/dist/tracing/analyzer.js +190 -0
  153. package/dist/tracing/analyzer.js.map +1 -0
  154. package/dist/tracing/langsmith.d.ts +38 -0
  155. package/dist/tracing/langsmith.js +50 -0
  156. package/dist/tracing/langsmith.js.map +1 -0
  157. package/dist/tracing/sessionTracer.d.ts +73 -0
  158. package/dist/tracing/sessionTracer.js +157 -0
  159. package/dist/tracing/sessionTracer.js.map +1 -0
  160. package/dist/tracing/types.d.ts +46 -0
  161. package/dist/tracing/types.js +5 -0
  162. package/dist/tracing/types.js.map +1 -0
  163. package/dist/ui/App.d.ts +24 -0
  164. package/dist/ui/App.js +172 -0
  165. package/dist/ui/App.js.map +1 -0
  166. package/dist/ui/components/HITLPrompt.d.ts +15 -0
  167. package/dist/ui/components/HITLPrompt.js +35 -0
  168. package/dist/ui/components/HITLPrompt.js.map +1 -0
  169. package/dist/ui/components/Header.d.ts +8 -0
  170. package/dist/ui/components/Header.js +6 -0
  171. package/dist/ui/components/Header.js.map +1 -0
  172. package/dist/ui/components/MessageBubble.d.ts +13 -0
  173. package/dist/ui/components/MessageBubble.js +17 -0
  174. package/dist/ui/components/MessageBubble.js.map +1 -0
  175. package/dist/ui/components/StatusBar.d.ts +21 -0
  176. package/dist/ui/components/StatusBar.js +34 -0
  177. package/dist/ui/components/StatusBar.js.map +1 -0
  178. package/dist/ui/components/StreamingText.d.ts +13 -0
  179. package/dist/ui/components/StreamingText.js +24 -0
  180. package/dist/ui/components/StreamingText.js.map +1 -0
  181. package/dist/ui/components/ToolCallPanel.d.ts +15 -0
  182. package/dist/ui/components/ToolCallPanel.js +18 -0
  183. package/dist/ui/components/ToolCallPanel.js.map +1 -0
  184. package/docs/01_insights_and_patterns.md +27 -0
  185. package/docs/02_edge_cases_and_mitigations.md +143 -0
  186. package/docs/03_initial_implementation_plan.md +66 -0
  187. package/docs/04_tech_stack_proposal.md +20 -0
  188. package/docs/05_prd.md +87 -0
  189. package/docs/06_user_stories.md +72 -0
  190. package/docs/07_system_architecture.md +138 -0
  191. package/docs/08_roadmap.md +200 -0
  192. package/e2b/Dockerfile +26 -0
  193. package/package.json +57 -0
  194. package/src/__tests__/bootstrap.test.ts +111 -0
  195. package/src/__tests__/config.test.ts +97 -0
  196. package/src/__tests__/m55.test.ts +238 -0
  197. package/src/__tests__/middleware.test.ts +219 -0
  198. package/src/__tests__/modelFactory.test.ts +63 -0
  199. package/src/__tests__/optimizations.test.ts +201 -0
  200. package/src/__tests__/promptBuilder.test.ts +141 -0
  201. package/src/__tests__/sandbox.test.ts +102 -0
  202. package/src/__tests__/security.test.ts +122 -0
  203. package/src/__tests__/streaming.test.ts +82 -0
  204. package/src/__tests__/toolRouter.test.ts +52 -0
  205. package/src/__tests__/tools.test.ts +146 -0
  206. package/src/__tests__/tracing.test.ts +196 -0
  207. package/src/agents/agentRegistry.ts +69 -0
  208. package/src/agents/agentSpec.ts +67 -0
  209. package/src/agents/builtinAgents.ts +142 -0
  210. package/src/cli/config.ts +124 -0
  211. package/src/cli/index.ts +730 -0
  212. package/src/cli/modelFactory.ts +174 -0
  213. package/src/cli/providers.ts +107 -0
  214. package/src/commands/builtinCommands.ts +293 -0
  215. package/src/commands/commandRegistry.ts +194 -0
  216. package/src/core/agentLoop.d.ts.map +1 -0
  217. package/src/core/agentLoop.ts +312 -0
  218. package/src/core/autoSave.ts +95 -0
  219. package/src/core/compactor.ts +252 -0
  220. package/src/core/contextGuard.ts +129 -0
  221. package/src/core/errors.ts +202 -0
  222. package/src/core/promptBuilder.d.ts.map +1 -0
  223. package/src/core/promptBuilder.ts +139 -0
  224. package/src/core/reasoningRouter.ts +121 -0
  225. package/src/core/retry.ts +75 -0
  226. package/src/core/sessionResumer.ts +90 -0
  227. package/src/core/sessionStore.ts +215 -0
  228. package/src/core/subAgent.ts +339 -0
  229. package/src/core/tokenCounter.ts +64 -0
  230. package/src/evals/dataset.ts +67 -0
  231. package/src/evals/evaluator.ts +81 -0
  232. package/src/hitl/bridge.ts +160 -0
  233. package/src/middleware/commandSanitizer.ts +60 -0
  234. package/src/middleware/loopDetection.ts +63 -0
  235. package/src/middleware/permission.ts +72 -0
  236. package/src/middleware/pipeline.ts +75 -0
  237. package/src/middleware/preCompletion.ts +94 -0
  238. package/src/middleware/types.ts +45 -0
  239. package/src/sandbox/bootstrap.ts +121 -0
  240. package/src/sandbox/manager.ts +239 -0
  241. package/src/sandbox/sync.ts +157 -0
  242. package/src/skills/loader.ts +143 -0
  243. package/src/skills/tools.ts +99 -0
  244. package/src/skills/types.ts +13 -0
  245. package/src/test_cache.ts +72 -0
  246. package/src/test_google.js +40 -0
  247. package/src/test_google.ts +40 -0
  248. package/src/tools/askUser.ts +47 -0
  249. package/src/tools/browser.ts +137 -0
  250. package/src/tools/index.d.ts.map +1 -0
  251. package/src/tools/index.ts +237 -0
  252. package/src/tools/registry.ts +198 -0
  253. package/src/tools/router.ts +78 -0
  254. package/src/tools/security.ts +220 -0
  255. package/src/tools/spawnAgent.ts +158 -0
  256. package/src/tools/webSearch.ts +142 -0
  257. package/src/tracing/analyzer.ts +265 -0
  258. package/src/tracing/langsmith.ts +63 -0
  259. package/src/tracing/sessionTracer.ts +202 -0
  260. package/src/tracing/types.ts +49 -0
  261. package/src/types/valyu.d.ts +37 -0
  262. package/src/ui/App.tsx +404 -0
  263. package/src/ui/components/HITLPrompt.tsx +119 -0
  264. package/src/ui/components/Header.tsx +51 -0
  265. package/src/ui/components/MessageBubble.tsx +46 -0
  266. package/src/ui/components/StatusBar.tsx +138 -0
  267. package/src/ui/components/StreamingText.tsx +48 -0
  268. package/src/ui/components/ToolCallPanel.tsx +80 -0
  269. package/tests/commands/commands.test.ts +356 -0
  270. package/tests/core/compactor.test.ts +217 -0
  271. package/tests/core/retryAndErrors.test.ts +164 -0
  272. package/tests/core/sessionResumer.test.ts +95 -0
  273. package/tests/core/sessionStore.test.ts +84 -0
  274. package/tests/core/stability.test.ts +165 -0
  275. package/tests/core/subAgent.test.ts +238 -0
  276. package/tests/hitl/hitlBridge.test.ts +115 -0
  277. package/tsconfig.json +16 -0
  278. package/vitest.config.ts +10 -0
  279. package/vitest.out +48 -0
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ <div align="center">
2
+
3
+ # ⚡ Joone
4
+
5
+ **An autonomous coding agent powered by prompt caching, harness engineering, and secure sandboxing.**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/joone.svg)](https://npmjs.org/package/joone)
8
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
9
+
10
+ </div>
11
+
12
+ ---
13
+
14
+ **Joone** is a highly capable autonomous AI coding assistant that runs directly in your terminal. It leverages a hybrid environment: it has read/write access to your local project files for seamless editing, but all code execution, testing, and dependency installations happen securely inside an isolated cloud sandbox (powered by E2B).
15
+
16
+ ## ✨ Features
17
+
18
+ - **🧠 Pluggable Intelligence**: Seamlessly switch between Anthropic (Claude 3.5 Sonnet, Opus), OpenAI (GPT-4o, o1), Google (Gemini 1.5/pro), Mistral, Groq, local Ollama models, and more.
19
+ - **🔌 User-Local Provider Plugins**: Heavy LLM SDKs (like `@langchain/google-genai`) are dynamically installed into `~/.joone/providers`. This keeps the base Joone installation incredibly lightweight while supporting every major AI provider.
20
+ - **🛡️ Secure Execution Sandbox**: Joone cannot accidentally delete your local database or run malicious `npm install` scripts on your host machine. All execution happens in an isolated E2B cloud sandbox that syncs seamlessly with your local workspace.
21
+ - **🖥️ Beautiful Terminal UI**: Built with React and Ink, Joone provides a rich, interactive TUI (Terminal User Interface) with spinners, syntax highlighting, and progress tracking.
22
+ - **🔍 Deep Insights**: Integrated with LangSmith for comprehensive session tracing, token counting, and performance analysis.
23
+ - **🔁 Agent Resilience**: Includes loop detection, command sanitization, backoff retries, and a human-in-the-loop (HITL) permission middleware (`auto`, `ask_dangerous`, `ask_all`).
24
+
25
+ ---
26
+
27
+ ## 🚀 Getting Started
28
+
29
+ ### Installation
30
+
31
+ Install Joone globally via npm:
32
+
33
+ ```bash
34
+ npm install -g joone
35
+ ```
36
+
37
+ ### Configuration
38
+
39
+ Run the automated onboarding wizard to configure your preferred LLM provider, models, and API keys:
40
+
41
+ ```bash
42
+ joone config
43
+ ```
44
+
45
+ _This will prompt you for your LLM API Key and optionally your E2B Sandbox API key. Keys are securely stored in `~/.joone/config.json`._
46
+
47
+ ### Start Joone
48
+
49
+ To start an autonomous session in your current project directory:
50
+
51
+ ```bash
52
+ joone start
53
+ ```
54
+
55
+ Or, if you want to skip global installation altogether, run it on-demand:
56
+
57
+ ```bash
58
+ npx joone@latest start
59
+ ```
60
+
61
+ ### Uninstallation
62
+
63
+ Since Joone manages its own user-local plugins and settings, completely removing Joone from your system is a two-step process:
64
+
65
+ 1. **Wipe User Data**: First, use Joone's built-in cleanup command to safely delete your configurations, traces, and dynamically installed LLM provider dependencies stored in `~/.joone`:
66
+ ```bash
67
+ joone cleanup
68
+ ```
69
+ 2. **Remove the App**: Next, uninstall the base package using the package manager you originally used:
70
+ ```bash
71
+ npm uninstall -g joone
72
+ # OR
73
+ brew uninstall joone
74
+ ```
75
+
76
+ ---
77
+
78
+ ## 🛠️ Commands
79
+
80
+ | Command | Description |
81
+ | ------------------------------ | ------------------------------------------------------------------------------- |
82
+ | `joone start` | Start a new Joone agent session in the current directory. |
83
+ | `joone start --resume <id>` | Resume a previously saved persistent session. |
84
+ | `joone config` | Run the configuration wizard (Providers, API Keys, etc.). |
85
+ | `joone sessions` | List all available persistent sessions for resumption. |
86
+ | `joone provider add <name>` | Manually download a dynamic LLM provider package (e.g., `google`, `anthropic`). |
87
+ | `joone provider remove <name>` | Uninstall a provider package locally. |
88
+ | `joone analyze [sessionId]` | Analyze a session trace for token usage and performance insights. |
89
+ | `joone eval` | Run automated offline evaluation against the LangSmith dataset. |
90
+ | `joone cleanup` | Wipe all Joone configurations, keys, traces, and plugins from your machine. |
91
+
92
+ ---
93
+
94
+ ## 🏗️ Architecture
95
+
96
+ Joone is built around the **Execution Harness** pattern.
97
+
98
+ 1. **Prompt Builder**: Dynamically constructs LLM prompts using Anthropic/LangChain's prompt caching features to save tokens over long sessions.
99
+ 2. **Middleware Pipeline**: Tool calls pass through a robust middleware stack (Loop Detection, Command Sanitization, Permissions) before executing.
100
+ 3. **Dynamic Sandbox**: Tools executing terminal commands or running dev servers are routed via `@langchain/core` directly into a temporary E2B sandbox. File modifications are synchronized back to your local machine via a bidirectional sync layer.
101
+
102
+ ## 🤝 Contributing
103
+
104
+ We welcome contributions!
105
+
106
+ 1. Clone the repository
107
+ 2. Run `npm install`
108
+ 3. Make your changes in `src/`
109
+ 4. Compile with `npm run build`
110
+ 5. Test your changes locally using `npm run dev -- start`
111
+
112
+ ## 📝 License
113
+
114
+ ISC License. See `LICENSE` for more information.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { LazyInstaller } from "../sandbox/bootstrap.js";
3
+ // Mock SandboxManager
4
+ const createMockSandbox = () => ({
5
+ exec: vi.fn(),
6
+ isActive: vi.fn().mockReturnValue(true),
7
+ create: vi.fn(),
8
+ destroy: vi.fn(),
9
+ uploadFile: vi.fn(),
10
+ getSandbox: vi.fn(),
11
+ });
12
+ describe("LazyInstaller", () => {
13
+ let mockSandbox;
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ mockSandbox = createMockSandbox();
17
+ });
18
+ // ─── Test #34: Custom template skips all installs ───
19
+ it("skips installation when using a custom template", async () => {
20
+ const installer = new LazyInstaller(true);
21
+ expect(installer.isGeminiCliReady()).toBe(true);
22
+ expect(installer.isOsvScannerReady()).toBe(true);
23
+ // Should not call exec at all
24
+ const result = await installer.ensureGeminiCli(mockSandbox);
25
+ expect(result).toBe(true);
26
+ expect(mockSandbox.exec).not.toHaveBeenCalled();
27
+ });
28
+ // ─── Test #35: Dev mode installs Gemini CLI on first use ───
29
+ it("installs Gemini CLI on first call in dev mode", async () => {
30
+ const installer = new LazyInstaller(false);
31
+ // First check fails (not installed), then install succeeds, then extension succeeds
32
+ mockSandbox.exec
33
+ .mockRejectedValueOnce(new Error("not found")) // version check
34
+ .mockResolvedValueOnce({ exitCode: 0, stdout: "installed", stderr: "" }) // npm install
35
+ .mockResolvedValueOnce({ exitCode: 0, stdout: "ok", stderr: "" }); // extension install
36
+ const result = await installer.ensureGeminiCli(mockSandbox);
37
+ expect(result).toBe(true);
38
+ expect(installer.isGeminiCliReady()).toBe(true);
39
+ });
40
+ // ─── Test #36: Caches install state — second call is a no-op ───
41
+ it("does not re-install on second call (cached)", async () => {
42
+ const installer = new LazyInstaller(false);
43
+ // First: fails check, succeeds install + extension
44
+ mockSandbox.exec
45
+ .mockRejectedValueOnce(new Error("not found"))
46
+ .mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" })
47
+ .mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" });
48
+ await installer.ensureGeminiCli(mockSandbox);
49
+ mockSandbox.exec.mockClear();
50
+ // Second call — should return immediately
51
+ const result = await installer.ensureGeminiCli(mockSandbox);
52
+ expect(result).toBe(true);
53
+ expect(mockSandbox.exec).not.toHaveBeenCalled();
54
+ });
55
+ // ─── Test #37: Returns false if install fails ───
56
+ it("returns false if Gemini CLI installation fails", async () => {
57
+ const installer = new LazyInstaller(false);
58
+ mockSandbox.exec
59
+ .mockRejectedValueOnce(new Error("not found")) // version check
60
+ .mockResolvedValueOnce({ exitCode: 1, stdout: "", stderr: "error" }); // install fails
61
+ const result = await installer.ensureGeminiCli(mockSandbox);
62
+ expect(result).toBe(false);
63
+ expect(installer.isGeminiCliReady()).toBe(false);
64
+ });
65
+ // ─── Test #38: OSV-Scanner install attempt ───
66
+ it("installs OSV-Scanner via curl when not available", async () => {
67
+ const installer = new LazyInstaller(false);
68
+ mockSandbox.exec
69
+ .mockRejectedValueOnce(new Error("not found")) // version check
70
+ .mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" }); // curl install
71
+ const result = await installer.ensureOsvScanner(mockSandbox);
72
+ expect(result).toBe(true);
73
+ expect(installer.isOsvScannerReady()).toBe(true);
74
+ });
75
+ });
76
+ //# sourceMappingURL=bootstrap.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.test.js","sourceRoot":"","sources":["../../src/__tests__/bootstrap.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,84 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import * as os from "node:os";
5
+ // We will import these once they exist — test-first.
6
+ import { loadConfig, saveConfig, DEFAULT_CONFIG } from "../cli/config.js";
7
+ describe("Config Manager", () => {
8
+ // Use a temp directory so we never touch the real ~/.joone
9
+ let tempDir;
10
+ let configPath;
11
+ let savedAnthropicKey;
12
+ beforeEach(() => {
13
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "joone-test-"));
14
+ configPath = path.join(tempDir, "config.json");
15
+ // Isolate from vitest.config.ts env vars
16
+ savedAnthropicKey = process.env.ANTHROPIC_API_KEY;
17
+ delete process.env.ANTHROPIC_API_KEY;
18
+ });
19
+ afterEach(() => {
20
+ fs.rmSync(tempDir, { recursive: true, force: true });
21
+ // Restore env var
22
+ if (savedAnthropicKey !== undefined) {
23
+ process.env.ANTHROPIC_API_KEY = savedAnthropicKey;
24
+ }
25
+ });
26
+ // ─── RED Test #1: loadConfig returns defaults when file doesn't exist ───
27
+ it("returns default config when config file does not exist", () => {
28
+ const config = loadConfig(configPath);
29
+ expect(config.provider).toBe("anthropic");
30
+ expect(config.model).toBe("claude-sonnet-4-20250514");
31
+ expect(config.apiKey).toBeUndefined();
32
+ expect(config.maxTokens).toBe(4096);
33
+ expect(config.temperature).toBe(0);
34
+ expect(config.streaming).toBe(true);
35
+ });
36
+ // ─── RED Test #2: saveConfig roundtrips with loadConfig ───
37
+ it("saves config to disk and loads it back correctly", () => {
38
+ const custom = {
39
+ ...DEFAULT_CONFIG,
40
+ provider: "openai",
41
+ model: "gpt-4o",
42
+ apiKey: "sk-test-key-123",
43
+ streaming: false,
44
+ };
45
+ saveConfig(configPath, custom);
46
+ // File should exist now
47
+ expect(fs.existsSync(configPath)).toBe(true);
48
+ // Load it back — should match what was saved
49
+ const loaded = loadConfig(configPath);
50
+ expect(loaded.provider).toBe("openai");
51
+ expect(loaded.model).toBe("gpt-4o");
52
+ expect(loaded.apiKey).toBe("sk-test-key-123");
53
+ expect(loaded.streaming).toBe(false);
54
+ // Fields we didn't override should keep defaults
55
+ expect(loaded.maxTokens).toBe(4096);
56
+ expect(loaded.temperature).toBe(0);
57
+ });
58
+ // ─── RED Test #3: loadConfig uses env var fallback for API key ───
59
+ it("falls back to ANTHROPIC_API_KEY env var when apiKey is missing from config", () => {
60
+ // Save a config WITHOUT an apiKey
61
+ const noKeyConfig = {
62
+ ...DEFAULT_CONFIG,
63
+ provider: "anthropic",
64
+ };
65
+ saveConfig(configPath, noKeyConfig);
66
+ // Set the env var
67
+ const originalEnv = process.env.ANTHROPIC_API_KEY;
68
+ process.env.ANTHROPIC_API_KEY = "sk-ant-from-env";
69
+ try {
70
+ const config = loadConfig(configPath);
71
+ expect(config.apiKey).toBe("sk-ant-from-env");
72
+ }
73
+ finally {
74
+ // Restore env
75
+ if (originalEnv === undefined) {
76
+ delete process.env.ANTHROPIC_API_KEY;
77
+ }
78
+ else {
79
+ process.env.ANTHROPIC_API_KEY = originalEnv;
80
+ }
81
+ }
82
+ });
83
+ });
84
+ //# sourceMappingURL=config.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../src/__tests__/config.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,160 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import * as os from "node:os";
5
+ import { BrowserTool } from "../tools/browser.js";
6
+ import { WebSearchTool, bindValyuApiKey } from "../tools/webSearch.js";
7
+ import { SkillLoader } from "../skills/loader.js";
8
+ import { SearchSkillsTool, LoadSkillTool, bindSkillLoader, } from "../skills/tools.js";
9
+ import { ToolRouter, ToolTarget } from "../tools/router.js";
10
+ // ═══════════════════════════════════════════════════════════════════════════════
11
+ // 5.5a: Browser Tool
12
+ // ═══════════════════════════════════════════════════════════════════════════════
13
+ describe("BrowserTool", () => {
14
+ // ─── Test #70: Builds navigate command ───
15
+ it("has the correct schema with all supported actions", () => {
16
+ expect(BrowserTool.name).toBe("browser");
17
+ expect(BrowserTool.schema.properties.action.enum).toContain("navigate");
18
+ expect(BrowserTool.schema.properties.action.enum).toContain("snapshot");
19
+ expect(BrowserTool.schema.properties.action.enum).toContain("click");
20
+ expect(BrowserTool.schema.properties.action.enum).toContain("type");
21
+ expect(BrowserTool.schema.properties.action.enum).toContain("screenshot");
22
+ expect(BrowserTool.schema.properties.action.enum).toContain("scroll");
23
+ });
24
+ // ─── Test #71: Rejects without sandbox ───
25
+ it("throws when sandbox is not active", async () => {
26
+ const result = await BrowserTool.execute({ action: "navigate", url: "https://example.com" });
27
+ expect(result.isError).toBe(true);
28
+ expect(result.content).toMatch(/sandbox/i);
29
+ });
30
+ });
31
+ // ═══════════════════════════════════════════════════════════════════════════════
32
+ // 5.5b: Web Search Tool
33
+ // ═══════════════════════════════════════════════════════════════════════════════
34
+ describe("WebSearchTool", () => {
35
+ // ─── Test #72: Returns error if no API key ───
36
+ it("returns error when API key is not configured", async () => {
37
+ bindValyuApiKey(undefined);
38
+ const result = await WebSearchTool.execute({ query: "test" });
39
+ expect(result.content).toMatch(/api key not configured/i);
40
+ });
41
+ // ─── Test #73: Schema includes all sources ───
42
+ it("schema includes all search sources", () => {
43
+ const sources = WebSearchTool.schema.properties.source.enum;
44
+ expect(sources).toContain("web");
45
+ expect(sources).toContain("papers");
46
+ expect(sources).toContain("finance");
47
+ expect(sources).toContain("patents");
48
+ expect(sources).toContain("sec");
49
+ expect(sources).toContain("companies");
50
+ });
51
+ });
52
+ // ═══════════════════════════════════════════════════════════════════════════════
53
+ // 5.5c: Skills System
54
+ // ═══════════════════════════════════════════════════════════════════════════════
55
+ describe("SkillLoader", () => {
56
+ let tmpDir;
57
+ beforeEach(() => {
58
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "joone-skills-test-"));
59
+ });
60
+ afterEach(() => {
61
+ fs.rmSync(tmpDir, { recursive: true, force: true });
62
+ });
63
+ const createSkill = (dir, name, content) => {
64
+ const skillDir = path.join(dir, name);
65
+ fs.mkdirSync(skillDir, { recursive: true });
66
+ fs.writeFileSync(path.join(skillDir, "SKILL.md"), content);
67
+ };
68
+ // ─── Test #74: Discovers skills from project root ───
69
+ it("discovers skills from a directory", () => {
70
+ const skillsDir = path.join(tmpDir, "skills");
71
+ createSkill(skillsDir, "deploy", "---\nname: deploy\ndescription: Deploy to Vercel\n---\n## Steps\n1. Run vercel deploy");
72
+ const loader = new SkillLoader(tmpDir);
73
+ const skills = loader.discoverSkills();
74
+ expect(skills.length).toBeGreaterThanOrEqual(1);
75
+ const deploy = skills.find((s) => s.name === "deploy");
76
+ expect(deploy).toBeDefined();
77
+ expect(deploy.description).toBe("Deploy to Vercel");
78
+ expect(deploy.source).toBe("project");
79
+ });
80
+ // ─── Test #75: Parses YAML frontmatter ───
81
+ it("parses YAML frontmatter correctly", () => {
82
+ const loader = new SkillLoader(tmpDir);
83
+ const result = loader.parseFrontmatter("---\nname: test-skill\ndescription: A test skill\n---\n# Instructions");
84
+ expect(result.name).toBe("test-skill");
85
+ expect(result.description).toBe("A test skill");
86
+ });
87
+ // ─── Test #76: Project skills override user skills with same name ───
88
+ it("project skills override user-level skills with the same name", () => {
89
+ // Create user-level skill in the mocked home directory (tmpDir)
90
+ const userSkillsDir = path.join(tmpDir, ".joone", "skills");
91
+ createSkill(userSkillsDir, "deploy", "---\nname: deploy\ndescription: User deploy\n---\n");
92
+ // Create project-level skill
93
+ const projectSkillsDir = path.join(tmpDir, "skills");
94
+ createSkill(projectSkillsDir, "deploy", "---\nname: deploy\ndescription: Project deploy\n---\n");
95
+ // Pass tmpDir as both projectRoot and userHome
96
+ const loader = new SkillLoader(tmpDir, tmpDir);
97
+ const skills = loader.discoverSkills();
98
+ const deploy = skills.find((s) => s.name === "deploy");
99
+ expect(deploy).toBeDefined();
100
+ expect(deploy.source).toBe("project");
101
+ expect(deploy.description).toBe("Project deploy");
102
+ // Ensure we only discovered one deploy skill
103
+ const deployCount = skills.filter((s) => s.name === "deploy").length;
104
+ expect(deployCount).toBe(1);
105
+ });
106
+ // ─── Test #77: loadSkill reads full content ───
107
+ it("loads the full content of a skill", () => {
108
+ const skillsDir = path.join(tmpDir, "skills");
109
+ const content = "---\nname: tdd\ndescription: Test-driven development\n---\n## Red-Green-Refactor\n1. Write failing test";
110
+ createSkill(skillsDir, "tdd", content);
111
+ const loader = new SkillLoader(tmpDir);
112
+ const loaded = loader.loadSkill("tdd");
113
+ expect(loaded).toBe(content);
114
+ });
115
+ });
116
+ describe("Skills Tools", () => {
117
+ let tmpDir;
118
+ beforeEach(() => {
119
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "joone-skills-tools-test-"));
120
+ const skillsDir = path.join(tmpDir, "skills");
121
+ const skillDir = path.join(skillsDir, "deploy");
122
+ fs.mkdirSync(skillDir, { recursive: true });
123
+ fs.writeFileSync(path.join(skillDir, "SKILL.md"), "---\nname: deploy\ndescription: Deploy to production\n---\n## Steps");
124
+ bindSkillLoader(new SkillLoader(tmpDir));
125
+ });
126
+ afterEach(() => {
127
+ fs.rmSync(tmpDir, { recursive: true, force: true });
128
+ });
129
+ // ─── Test #78: search_skills returns matching skills ───
130
+ it("search_skills finds matching skills", async () => {
131
+ const result = await SearchSkillsTool.execute({ query: "deploy" });
132
+ expect(result.content).toContain("deploy");
133
+ expect(result.content).toContain("production");
134
+ });
135
+ // ─── Test #79: load_skill reads full content ───
136
+ it("load_skill returns the full SKILL.md content", async () => {
137
+ const result = await LoadSkillTool.execute({ name: "deploy" });
138
+ expect(result.content).toContain("## Steps");
139
+ });
140
+ });
141
+ // ═══════════════════════════════════════════════════════════════════════════════
142
+ // Routing
143
+ // ═══════════════════════════════════════════════════════════════════════════════
144
+ describe("ToolRouter (M5.5 additions)", () => {
145
+ const router = new ToolRouter();
146
+ // ─── Test #80: Browser routes to sandbox ───
147
+ it("routes browser to sandbox", () => {
148
+ expect(router.getTarget("browser")).toBe(ToolTarget.SANDBOX);
149
+ });
150
+ // ─── Test #81: web_search routes to host ───
151
+ it("routes web_search to host", () => {
152
+ expect(router.getTarget("web_search")).toBe(ToolTarget.HOST);
153
+ });
154
+ // ─── Test #82: skills tools route to host ───
155
+ it("routes search_skills and load_skill to host", () => {
156
+ expect(router.getTarget("search_skills")).toBe(ToolTarget.HOST);
157
+ expect(router.getTarget("load_skill")).toBe(ToolTarget.HOST);
158
+ });
159
+ });
160
+ //# sourceMappingURL=m55.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"m55.test.js","sourceRoot":"","sources":["../../src/__tests__/m55.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { MiddlewarePipeline } from "../middleware/pipeline.js";
3
+ import { LoopDetectionMiddleware } from "../middleware/loopDetection.js";
4
+ import { CommandSanitizerMiddleware } from "../middleware/commandSanitizer.js";
5
+ import { PreCompletionMiddleware } from "../middleware/preCompletion.js";
6
+ // ═══════════════════════════════════════════════════════════════════════════════
7
+ // Pipeline Core
8
+ // ═══════════════════════════════════════════════════════════════════════════════
9
+ describe("MiddlewarePipeline", () => {
10
+ const makeCtx = (overrides) => ({
11
+ toolName: "bash",
12
+ args: { command: "echo hello" },
13
+ callId: "call-1",
14
+ ...overrides,
15
+ });
16
+ // ─── Test #44: Runs before/after hooks in order ───
17
+ it("runs before hooks in registration order and after hooks in reverse", async () => {
18
+ const order = [];
19
+ const pipeline = new MiddlewarePipeline();
20
+ pipeline.use({
21
+ name: "A",
22
+ before: (ctx) => { order.push("A:before"); return ctx; },
23
+ after: (_ctx, r) => { order.push("A:after"); return r; },
24
+ });
25
+ pipeline.use({
26
+ name: "B",
27
+ before: (ctx) => { order.push("B:before"); return ctx; },
28
+ after: (_ctx, r) => { order.push("B:after"); return r; },
29
+ });
30
+ const executeFn = vi.fn(async () => ({ content: "result" }));
31
+ await pipeline.run(makeCtx(), executeFn);
32
+ expect(order).toEqual(["A:before", "B:before", "B:after", "A:after"]);
33
+ expect(executeFn).toHaveBeenCalledOnce();
34
+ });
35
+ // ─── Test #45: Short-circuits when before returns string ───
36
+ it("short-circuits and does NOT execute the tool when before returns a string", async () => {
37
+ const pipeline = new MiddlewarePipeline();
38
+ pipeline.use({
39
+ name: "Blocker",
40
+ before: () => "⚠ Blocked!",
41
+ });
42
+ const executeFn = vi.fn(async () => ({ content: "should not reach this" }));
43
+ const result = await pipeline.run(makeCtx(), executeFn);
44
+ expect(result).toBe("⚠ Blocked!");
45
+ expect(executeFn).not.toHaveBeenCalled();
46
+ });
47
+ // ─── Test #46: After hooks can transform the result ───
48
+ it("after hooks can transform the tool result", async () => {
49
+ const pipeline = new MiddlewarePipeline();
50
+ pipeline.use({
51
+ name: "Uppercaser",
52
+ after: (_ctx, result) => { result.content = result.content.toUpperCase(); return result; },
53
+ });
54
+ const result = await pipeline.run(makeCtx(), async () => ({ content: "hello" }));
55
+ expect(result).toBe("HELLO");
56
+ });
57
+ });
58
+ // ═══════════════════════════════════════════════════════════════════════════════
59
+ // LoopDetectionMiddleware
60
+ // ═══════════════════════════════════════════════════════════════════════════════
61
+ describe("LoopDetectionMiddleware", () => {
62
+ const makeCtx = (cmd = "echo hello") => ({
63
+ toolName: "bash",
64
+ args: { command: cmd },
65
+ callId: "call-x",
66
+ });
67
+ // ─── Test #47: Allows first 2 identical calls ───
68
+ it("allows calls below the threshold", () => {
69
+ const mw = new LoopDetectionMiddleware(3);
70
+ expect(mw.before(makeCtx())).toEqual(makeCtx());
71
+ expect(mw.before(makeCtx())).toEqual(makeCtx());
72
+ });
73
+ // ─── Test #48: Blocks on 3rd identical call ───
74
+ it("blocks on the Nth identical consecutive call", () => {
75
+ const mw = new LoopDetectionMiddleware(3);
76
+ mw.before(makeCtx());
77
+ mw.before(makeCtx());
78
+ const result = mw.before(makeCtx());
79
+ expect(typeof result).toBe("string");
80
+ expect(result).toMatch(/loop detected/i);
81
+ });
82
+ // ─── Test #49: Resets when args change ───
83
+ it("resets the count when a different call is made", () => {
84
+ const mw = new LoopDetectionMiddleware(3);
85
+ mw.before(makeCtx("echo a"));
86
+ mw.before(makeCtx("echo a"));
87
+ // Different call breaks the streak
88
+ mw.before(makeCtx("echo b"));
89
+ // Back to "echo a" — only 1 in a row now
90
+ const result = mw.before(makeCtx("echo a"));
91
+ expect(typeof result).not.toBe("string");
92
+ });
93
+ });
94
+ // ═══════════════════════════════════════════════════════════════════════════════
95
+ // CommandSanitizerMiddleware
96
+ // ═══════════════════════════════════════════════════════════════════════════════
97
+ describe("CommandSanitizerMiddleware", () => {
98
+ const mw = new CommandSanitizerMiddleware();
99
+ const makeCtx = (cmd) => ({
100
+ toolName: "bash",
101
+ args: { command: cmd },
102
+ callId: "call-x",
103
+ });
104
+ // ─── Test #50: Blocks rm -rf / ───
105
+ it("blocks rm -rf /", () => {
106
+ const result = mw.before(makeCtx("rm -rf /"));
107
+ expect(typeof result).toBe("string");
108
+ expect(result).toMatch(/blocked/i);
109
+ });
110
+ // ─── Test #51: Blocks interactive commands ───
111
+ it("blocks interactive commands like vim", () => {
112
+ const result = mw.before(makeCtx("vim src/index.ts"));
113
+ expect(typeof result).toBe("string");
114
+ expect(result).toMatch(/interactive/i);
115
+ });
116
+ // ─── Test #52: Allows safe commands ───
117
+ it("allows safe commands through", () => {
118
+ const result = mw.before(makeCtx("npm test"));
119
+ expect(result).toEqual(makeCtx("npm test"));
120
+ });
121
+ // ─── Test #53: Ignores non-bash tools ───
122
+ it("ignores non-bash tool calls entirely", () => {
123
+ const ctx = {
124
+ toolName: "read_file",
125
+ args: { path: "/etc/passwd" },
126
+ callId: "call-x",
127
+ };
128
+ expect(mw.before(ctx)).toEqual(ctx);
129
+ });
130
+ });
131
+ // ═══════════════════════════════════════════════════════════════════════════════
132
+ // PreCompletionMiddleware
133
+ // ═══════════════════════════════════════════════════════════════════════════════
134
+ describe("PreCompletionMiddleware", () => {
135
+ // ─── Test #54: Blocks completion without tests ───
136
+ it("blocks task_complete when no tests have been run", () => {
137
+ const mw = new PreCompletionMiddleware();
138
+ const ctx = {
139
+ toolName: "task_complete",
140
+ args: {},
141
+ callId: "call-x",
142
+ };
143
+ const result = mw.before(ctx);
144
+ expect(typeof result).toBe("string");
145
+ expect(result).toMatch(/must run tests/i);
146
+ });
147
+ // ─── Test #55: Allows completion after tests ───
148
+ it("allows task_complete after a test command has been run", () => {
149
+ const mw = new PreCompletionMiddleware();
150
+ // Simulate running tests
151
+ const testCtx = {
152
+ toolName: "bash",
153
+ args: { command: "npm test" },
154
+ callId: "call-1",
155
+ };
156
+ mw.before(testCtx);
157
+ mw.after(testCtx, { content: "tests passed", metadata: { exitCode: 0 }, isError: false });
158
+ expect(mw.hasPassedTests()).toBe(true);
159
+ // Now try completion
160
+ const completeCtx = {
161
+ toolName: "task_complete",
162
+ args: {},
163
+ callId: "call-2",
164
+ };
165
+ const result = mw.before(completeCtx);
166
+ expect(result).toEqual(completeCtx);
167
+ });
168
+ });
169
+ //# sourceMappingURL=middleware.test.js.map