joonecli 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/dist/cli/index.js +4 -1
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/commands/builtinCommands.js +6 -6
  4. package/dist/commands/builtinCommands.js.map +1 -1
  5. package/dist/commands/commandRegistry.d.ts +3 -1
  6. package/dist/commands/commandRegistry.js.map +1 -1
  7. package/dist/core/agentLoop.d.ts +3 -1
  8. package/dist/core/agentLoop.js +17 -7
  9. package/dist/core/agentLoop.js.map +1 -1
  10. package/dist/core/compactor.js +2 -2
  11. package/dist/core/compactor.js.map +1 -1
  12. package/dist/core/contextGuard.d.ts +5 -0
  13. package/dist/core/contextGuard.js +30 -3
  14. package/dist/core/contextGuard.js.map +1 -1
  15. package/dist/core/events.d.ts +45 -0
  16. package/dist/core/events.js +8 -0
  17. package/dist/core/events.js.map +1 -0
  18. package/dist/core/sessionStore.js +3 -2
  19. package/dist/core/sessionStore.js.map +1 -1
  20. package/dist/core/subAgent.js +2 -2
  21. package/dist/core/subAgent.js.map +1 -1
  22. package/dist/core/tokenCounter.d.ts +8 -1
  23. package/dist/core/tokenCounter.js +28 -0
  24. package/dist/core/tokenCounter.js.map +1 -1
  25. package/dist/middleware/permission.js +1 -0
  26. package/dist/middleware/permission.js.map +1 -1
  27. package/dist/tools/browser.js +4 -1
  28. package/dist/tools/browser.js.map +1 -1
  29. package/dist/tools/index.d.ts +2 -1
  30. package/dist/tools/index.js +11 -3
  31. package/dist/tools/index.js.map +1 -1
  32. package/dist/tools/installHostDeps.d.ts +2 -0
  33. package/dist/tools/installHostDeps.js +37 -0
  34. package/dist/tools/installHostDeps.js.map +1 -0
  35. package/dist/tools/router.js +1 -0
  36. package/dist/tools/router.js.map +1 -1
  37. package/dist/tools/spawnAgent.js +3 -1
  38. package/dist/tools/spawnAgent.js.map +1 -1
  39. package/dist/tracing/sessionTracer.d.ts +1 -0
  40. package/dist/tracing/sessionTracer.js +4 -1
  41. package/dist/tracing/sessionTracer.js.map +1 -1
  42. package/dist/ui/App.js +6 -1
  43. package/dist/ui/App.js.map +1 -1
  44. package/dist/ui/components/ActionLog.d.ts +7 -0
  45. package/dist/ui/components/ActionLog.js +63 -0
  46. package/dist/ui/components/ActionLog.js.map +1 -0
  47. package/dist/ui/components/FileBrowser.d.ts +2 -0
  48. package/dist/ui/components/FileBrowser.js +41 -0
  49. package/dist/ui/components/FileBrowser.js.map +1 -0
  50. package/package.json +3 -5
  51. package/AGENTS.md +0 -56
  52. package/Handover.md +0 -115
  53. package/PROGRESS.md +0 -160
  54. package/docs/01_insights_and_patterns.md +0 -27
  55. package/docs/02_edge_cases_and_mitigations.md +0 -143
  56. package/docs/03_initial_implementation_plan.md +0 -66
  57. package/docs/04_tech_stack_proposal.md +0 -20
  58. package/docs/05_prd.md +0 -87
  59. package/docs/06_user_stories.md +0 -72
  60. package/docs/07_system_architecture.md +0 -138
  61. package/docs/08_roadmap.md +0 -200
  62. package/e2b/Dockerfile +0 -26
  63. package/src/__tests__/bootstrap.test.ts +0 -111
  64. package/src/__tests__/config.test.ts +0 -97
  65. package/src/__tests__/m55.test.ts +0 -238
  66. package/src/__tests__/middleware.test.ts +0 -219
  67. package/src/__tests__/modelFactory.test.ts +0 -63
  68. package/src/__tests__/optimizations.test.ts +0 -201
  69. package/src/__tests__/promptBuilder.test.ts +0 -141
  70. package/src/__tests__/sandbox.test.ts +0 -102
  71. package/src/__tests__/security.test.ts +0 -122
  72. package/src/__tests__/streaming.test.ts +0 -82
  73. package/src/__tests__/toolRouter.test.ts +0 -52
  74. package/src/__tests__/tools.test.ts +0 -146
  75. package/src/__tests__/tracing.test.ts +0 -196
  76. package/src/agents/agentRegistry.ts +0 -69
  77. package/src/agents/agentSpec.ts +0 -67
  78. package/src/agents/builtinAgents.ts +0 -142
  79. package/src/cli/config.ts +0 -124
  80. package/src/cli/index.ts +0 -742
  81. package/src/cli/modelFactory.ts +0 -174
  82. package/src/cli/postinstall.ts +0 -28
  83. package/src/cli/providers.ts +0 -107
  84. package/src/commands/builtinCommands.ts +0 -293
  85. package/src/commands/commandRegistry.ts +0 -194
  86. package/src/core/agentLoop.d.ts.map +0 -1
  87. package/src/core/agentLoop.ts +0 -312
  88. package/src/core/autoSave.ts +0 -95
  89. package/src/core/compactor.ts +0 -252
  90. package/src/core/contextGuard.ts +0 -129
  91. package/src/core/errors.ts +0 -202
  92. package/src/core/promptBuilder.d.ts.map +0 -1
  93. package/src/core/promptBuilder.ts +0 -139
  94. package/src/core/reasoningRouter.ts +0 -121
  95. package/src/core/retry.ts +0 -75
  96. package/src/core/sessionResumer.ts +0 -90
  97. package/src/core/sessionStore.ts +0 -216
  98. package/src/core/subAgent.ts +0 -339
  99. package/src/core/tokenCounter.ts +0 -64
  100. package/src/evals/dataset.ts +0 -67
  101. package/src/evals/evaluator.ts +0 -81
  102. package/src/hitl/bridge.ts +0 -160
  103. package/src/middleware/commandSanitizer.ts +0 -60
  104. package/src/middleware/loopDetection.ts +0 -63
  105. package/src/middleware/permission.ts +0 -72
  106. package/src/middleware/pipeline.ts +0 -75
  107. package/src/middleware/preCompletion.ts +0 -94
  108. package/src/middleware/types.ts +0 -45
  109. package/src/sandbox/bootstrap.ts +0 -121
  110. package/src/sandbox/manager.ts +0 -239
  111. package/src/sandbox/sync.ts +0 -157
  112. package/src/skills/loader.ts +0 -143
  113. package/src/skills/tools.ts +0 -99
  114. package/src/skills/types.ts +0 -13
  115. package/src/test_cache.ts +0 -72
  116. package/src/tools/askUser.ts +0 -47
  117. package/src/tools/browser.ts +0 -137
  118. package/src/tools/index.d.ts.map +0 -1
  119. package/src/tools/index.ts +0 -237
  120. package/src/tools/registry.ts +0 -198
  121. package/src/tools/router.ts +0 -78
  122. package/src/tools/security.ts +0 -220
  123. package/src/tools/spawnAgent.ts +0 -158
  124. package/src/tools/webSearch.ts +0 -142
  125. package/src/tracing/analyzer.ts +0 -265
  126. package/src/tracing/langsmith.ts +0 -63
  127. package/src/tracing/sessionTracer.ts +0 -202
  128. package/src/tracing/types.ts +0 -49
  129. package/src/types/valyu.d.ts +0 -37
  130. package/src/ui/App.tsx +0 -404
  131. package/src/ui/components/HITLPrompt.tsx +0 -119
  132. package/src/ui/components/Header.tsx +0 -51
  133. package/src/ui/components/MessageBubble.tsx +0 -46
  134. package/src/ui/components/StatusBar.tsx +0 -138
  135. package/src/ui/components/StreamingText.tsx +0 -48
  136. package/src/ui/components/ToolCallPanel.tsx +0 -80
  137. package/tests/commands/commands.test.ts +0 -356
  138. package/tests/core/compactor.test.ts +0 -217
  139. package/tests/core/retryAndErrors.test.ts +0 -164
  140. package/tests/core/sessionResumer.test.ts +0 -95
  141. package/tests/core/sessionStore.test.ts +0 -84
  142. package/tests/core/stability.test.ts +0 -165
  143. package/tests/core/subAgent.test.ts +0 -238
  144. package/tests/hitl/hitlBridge.test.ts +0 -115
  145. package/tsconfig.json +0 -16
  146. package/vitest.config.ts +0 -10
  147. package/vitest.out +0 -48
@@ -1,200 +0,0 @@
1
- # Implementation Roadmap
2
-
3
- We will tackle this project moving from the foundation outward.
4
-
5
- ## Milestone 1: The Foundation (Core Execution & Caching) ✅
6
-
7
- **Goal:** Build a basic agent that successfully executes simple loops while maintaining a 100% cache prefix validity across turns.
8
-
9
- 1. ~~**Setup Project**: Initialize the repository based on the chosen Tech Stack.~~
10
- 2. ~~**The Prompt Builder Engine**: Build the class responsible for layering static instruction strings, tools, and message arrays cleanly.~~
11
- 3. **Core Tooling**: Implement `bash_executor` and `file_reader` / `file_writer`.
12
- 4. **Basic Event Loop**: Implement a while loop that queries the LLM and runs the exact tool.
13
-
14
- ## Milestone 2: CLI Packaging & Provider Selection
15
-
16
- **Goal:** Package joone as an installable CLI tool with dynamic LLM provider configuration, streaming output, and secure API key management.
17
-
18
- ### 2a. Config Manager (`src/cli/config.ts`)
19
-
20
- 1. **`JooneConfig` interface**: Define shape: `provider`, `model`, `apiKey`, `maxTokens`, `temperature`, `streaming`.
21
- 2. **`loadConfig()`**: Reads `~/.joone/config.json`. Returns sensible defaults if file doesn't exist.
22
- 3. **`saveConfig(config)`**: Writes JSON to `~/.joone/config.json`. Sets file permissions to `600` (owner-only).
23
- 4. **Env var fallback**: If `apiKey` is missing from config, check `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.
24
-
25
- ### 2b. Model Factory (`src/cli/modelFactory.ts`)
26
-
27
- 1. **`createModel(config)`**: Factory function that switches on `config.provider`.
28
- 2. **Dynamic imports**: Uses `await import("@langchain/anthropic")` etc. to avoid bundling all providers.
29
- 3. **Missing package detection**: If the import fails, print `"Provider X requires @langchain/X. Run: npm install @langchain/X"`.
30
- 4. **API key validation**: Throws a descriptive error if the API key is missing for the selected provider.
31
- 5. **Supported providers** (9+): Anthropic, OpenAI, Google, Mistral, Groq, DeepSeek, Fireworks, Together AI, Ollama.
32
-
33
- ### 2c. CLI Entry Point (`src/cli/index.ts`)
34
-
35
- 1. **Commander.js** for command parsing.
36
- 2. **`joone` (default command)**: Loads config → creates model → starts execution harness REPL.
37
- 3. **`joone config`**: Interactive prompts (via `@inquirer/prompts`) for provider, model, API key (masked), streaming toggle.
38
- 4. **`package.json` `"bin"` field**: Maps `joone` → `./dist/cli/index.js`.
39
-
40
- ### 2d. Streaming Support
41
-
42
- 1. **`ExecutionHarness.streamStep()`**: New method using `this.llm.stream(messages)`.
43
- 2. **Text chunks**: Printed to `process.stdout` in real-time.
44
- 3. **Tool call chunks**: Buffered until the full tool call JSON is received, then executed via the middleware pipeline.
45
- 4. **Config flag**: `streaming: true` (default). Disable via `joone config` or `--no-stream` flag.
46
-
47
- ### 2e. Security Tiers (Phased)
48
-
49
- 1. **Tier 1 (Now)**: Plain `config.json` + `chmod 600` + masked input.
50
- 2. **Tier 2 (Planned)**: OS Keychain via `keytar` — user selects during onboarding.
51
- 3. **Tier 3 (Planned)**: AES-256 encrypted config with machine-derived key — user selects during onboarding.
52
-
53
- ### TDD Test Plan (Vertical Slices)
54
-
55
- | # | RED Test | GREEN Implementation |
56
- | --- | -------------------------------------------------------------------------- | ------------------------------------ |
57
- | 1 | `loadConfig` returns defaults when no file exists | `loadConfig()` with default fallback |
58
- | 2 | `saveConfig` writes JSON and `loadConfig` reads it back | `saveConfig()` implementation |
59
- | 3 | `loadConfig` falls back to env var if `apiKey` is missing | Env var fallback logic |
60
- | 4 | `createModel` returns `ChatAnthropic` when provider is `"anthropic"` | Factory Anthropic branch |
61
- | 5 | `createModel` returns `ChatOpenAI` when provider is `"openai"` | Factory OpenAI branch |
62
- | 6 | `createModel` throws descriptive error if API key missing | Key validation |
63
- | 7 | `createModel` throws with install instructions if provider package missing | Dynamic import error handling |
64
- | 8 | `streamStep` emits text chunks to provided callback | Stream handler implementation |
65
- | 9 | `streamStep` buffers tool call JSON and returns complete `AIMessage` | Tool call buffering |
66
-
67
- ---
68
-
69
- ## Milestone 3: Hybrid Sandbox Integration
70
-
71
- **Goal:** Route all agent code execution through isolated E2B cloud microVMs while keeping file I/O on the host for real-time IDE visibility.
72
-
73
- ### 3a. E2B Sandbox Lifecycle (`src/sandbox/manager.ts`)
74
-
75
- 1. **Install E2B SDK**: Add `e2b` to dependencies.
76
- 2. **`SandboxManager`**: Class that creates/destroys an E2B sandbox per session.
77
- 3. **Timeout & Cleanup**: Auto-destroy sandbox after configurable idle timeout.
78
-
79
- ### 3b. File Sync Layer (`src/sandbox/sync.ts`)
80
-
81
- 1. **Change Tracker**: Track which host files have been modified since last sync using file mtimes or a dirty set.
82
- 2. **`syncToSandbox()`**: Upload only changed files to `/workspace/` in the sandbox before each execution.
83
- 3. **Initial Sync**: On session start, upload the full project directory.
84
-
85
- ### 3c. Tool Router (`src/tools/router.ts`)
86
-
87
- 1. **Host tools**: `write_file`, `read_file` → execute via Node.js `fs` on the host.
88
- 2. **Sandbox tools**: `bash`, `run_tests`, `install_deps` → sync files, then execute via `sandbox.commands.run()`.
89
- 3. **Automatic routing**: Tool router determines target based on tool type.
90
-
91
- ### 3d. Rewire Existing Tools
92
-
93
- 1. **`BashTool`**: Remove stub, connect to `sandbox.commands.run()`.
94
- 2. **`ReadFileTool`**: Keep on host, add size guardrail.
95
- 3. **`WriteFileTool`**: Keep on host, mark file as dirty for next sync.
96
-
97
- ### TDD Test Plan
98
-
99
- | # | Test | Behavior |
100
- | --- | ------------------------------------------------ | ----------------- |
101
- | 1 | `SandboxManager.create()` initializes a sandbox | Session lifecycle |
102
- | 2 | `SandboxManager.destroy()` cleans up the sandbox | Teardown |
103
- | 3 | `syncToSandbox()` uploads dirty files | Change tracking |
104
- | 4 | Tool router sends `write_file` to host | Host routing |
105
- | 5 | Tool router sends `bash` to sandbox | Sandbox routing |
106
-
107
- ---
108
-
109
- ## Milestone 3.5: Security Scanning Tool
110
-
111
- **Goal:** Give the agent the ability to scan code for security vulnerabilities using the Gemini CLI Security Extension, with a native LLM-powered fallback.
112
-
113
- ### 3.5a. SecurityScanTool (`src/tools/security.ts`)
114
-
115
- 1. **Gemini CLI path** (preferred): Shell out to `gemini -x security:analyze` via sandbox.
116
- 2. **Native LLM fallback**: Use the agent's configured LLM with a security-focused prompt to analyze code diffs.
117
- 3. Accepts `target`: `"changes"` | `"file"` | `"deps"`, optional `path`.
118
-
119
- ### 3.5b. DepScanTool (`src/tools/depScan.ts`)
120
-
121
- 1. **OSV-Scanner**: Run `osv-scanner --json .` in sandbox, parse JSON results.
122
- 2. **Fallback**: Run `npm audit --json` if OSV-Scanner is not installed.
123
-
124
- ### 3.5c. Tool Registration
125
-
126
- 1. Add both tools to `CORE_TOOLS` in `tools/index.ts`.
127
- 2. Add `security_scan` and `dep_scan` to `SANDBOX_TOOLS` in `tools/router.ts`.
128
- 3. Add security scan stubs to `DeferredToolsDB` in `tools/registry.ts`.
129
-
130
- ### TDD Test Plan
131
-
132
- | # | Test | Behavior |
133
- | --- | --------------------------------------------------------- | ----------- |
134
- | 1 | SecurityScanTool returns report when Gemini CLI available | Shell path |
135
- | 2 | SecurityScanTool falls back to LLM analysis | Native path |
136
- | 3 | DepScanTool parses OSV-Scanner JSON output | Dep scan |
137
- | 4 | DepScanTool falls back to `npm audit` | Fallback |
138
- | 5 | ToolRouter routes both to sandbox | Routing |
139
-
140
- ## Milestone 4: Harness Engineering & Middlewares
141
-
142
- **Goal:** Make the agent resilient. Stop it from breaking itself.
143
-
144
- 1. **Middleware Pipeline Pattern**: Implement a generic pre/post execution hook system for tool calls.
145
- 2. **Build `LoopDetectionMiddleware`**: Track hashes or signatures of tool calls. Throw errors/warnings when duplicated explicitly.
146
- 3. **Build `SafeguardMiddleware`**: Prevent massive file reads.
147
- 4. **Build `PreCompletionMiddleware`**: Intercept task completion and require proof of verification (e.g. running tests).
148
-
149
- ## Milestone 5: Advanced Optimizations
150
-
151
- **Goal:** Scale the agent for complex workspaces and heavy memory.
152
-
153
- 1. **Tool Lazy Loading**: Implement the "Tool Search" mechanism for dynamic capabilities.
154
- 2. **Context Compaction**: Implement Cache-Safe Forking. When tokens hit 80% capacity, summarize earlier messages, retaining the static prefix format.
155
- 3. **Reasoning Sandwich**: Implement dynamic logic routing. Allow the agent to use `high-reasoning` mode for planning, and drop to `medium-reasoning` for mechanical typing.
156
-
157
- ## Milestone 5.5: Browser, Web Search & Skills
158
-
159
- **Goal:** Give the agent internet access, browser interaction, and extensible skill loading.
160
-
161
- ### 5.5a. Browser Tool (`agent-browser`)
162
-
163
- - Wrap Vercel Labs' `agent-browser` CLI via sandbox shell calls
164
- - Commands: `navigate`, `snapshot` (accessibility tree), `click`, `type`, `screenshot`, `scroll`
165
- - Lazy-installed in dev, pre-baked in prod template
166
-
167
- ### 5.5b. Web Search Tool (`@valyu/ai-sdk`)
168
-
169
- - AI-native search via Valyu API (runs on Host)
170
- - Sources: web, papers (arXiv/PubMed), finance, patents, SEC filings, companies
171
- - API key stored in config (`valyuApiKey`)
172
-
173
- ### 5.5c. Skills System
174
-
175
- - Discovery paths: `./skills/`, `./.agents/skills/`, `~/.joone/skills/`, `~/.agents/skills/`
176
- - SKILL.md format: YAML frontmatter (name, description) + markdown instructions
177
- - Tools: `search_skills`, `load_skill` (injects into conversation as system-reminder)
178
- - Project skills override user skills with same name
179
-
180
- ## Milestone 6: Tracing & Refinement
181
-
182
- **Goal:** Monitor performance and improve via feedback.
183
-
184
- 1. **Integrate Tracing**: (LangSmith / LangFuse / OpenTelemetry) to track exact costs, cache hit rates, and execution paths.
185
- 2. **Trace Analyzer Subagent**: Build the offline script that reads failed traces and outputs summaries for human harness engineers.
186
-
187
- ## Milestone 7: Testing & Evaluations (TDD - Ongoing)
188
-
189
- **Goal:** Ensure the context boundaries, middlewares, and tools function flawlessly before production. This milestone runs **in parallel** with all others via TDD.
190
-
191
- 1. ~~**Setup Vitest**~~
192
- 2. **Unit Testing (Red-Green-Refactor)**:
193
- - ~~`CacheOptimizedPromptBuilder` (5/5 GREEN)~~
194
- - `ConfigManager`: loadConfig, saveConfig, env fallback
195
- - `ModelFactory`: provider switching, error handling
196
- - `MiddlewarePipeline`: Loop detection, pre-completion interception
197
- - `SandboxLifecycleManager`: create/destroy lifecycle hooks
198
- 3. **E2E Evaluations (Evals)**:
199
- - Hook LangSmith datasets up to the `ExecutionHarness` to run regression tests against known code tasks.
200
- - Measure **Cache Hit Rate** assertions (e.g., Assert CacheHit > 90% over a 10-turn conversation).
package/e2b/Dockerfile DELETED
@@ -1,26 +0,0 @@
1
- # joone-base: Pre-baked E2B sandbox template for production.
2
- # All security and development tools are pre-installed for zero startup cost.
3
- #
4
- # Build command:
5
- # e2b template create --name joone-base --dockerfile ./e2b/Dockerfile
6
- #
7
- # Usage in config (~/.joone/config.json):
8
- # { "sandboxTemplate": "joone-base" }
9
-
10
- FROM e2b/base
11
-
12
- # Install Node.js tooling
13
- RUN npm install -g @google/gemini-cli
14
-
15
- # Install Gemini CLI security extension
16
- RUN gemini extensions install https://github.com/gemini-cli-extensions/security
17
-
18
- # Install OSV-Scanner for dependency vulnerability scanning
19
- RUN curl -sSfL https://github.com/google/osv-scanner/releases/latest/download/osv-scanner_linux_amd64 \
20
- -o /usr/local/bin/osv-scanner && \
21
- chmod +x /usr/local/bin/osv-scanner
22
-
23
- # Verify installations
24
- RUN gemini --version && osv-scanner --version
25
-
26
- WORKDIR /workspace
@@ -1,111 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { LazyInstaller } from "../sandbox/bootstrap.js";
3
- import { SandboxManager } from "../sandbox/manager.js";
4
-
5
- // Mock SandboxManager
6
- const createMockSandbox = () => ({
7
- exec: vi.fn(),
8
- isActive: vi.fn().mockReturnValue(true),
9
- create: vi.fn(),
10
- destroy: vi.fn(),
11
- uploadFile: vi.fn(),
12
- getSandbox: vi.fn(),
13
- });
14
-
15
- describe("LazyInstaller", () => {
16
- let mockSandbox: ReturnType<typeof createMockSandbox>;
17
-
18
- beforeEach(() => {
19
- vi.clearAllMocks();
20
- mockSandbox = createMockSandbox();
21
- });
22
-
23
- // ─── Test #34: Custom template skips all installs ───
24
-
25
- it("skips installation when using a custom template", async () => {
26
- const installer = new LazyInstaller(true);
27
-
28
- expect(installer.isGeminiCliReady()).toBe(true);
29
- expect(installer.isOsvScannerReady()).toBe(true);
30
-
31
- // Should not call exec at all
32
- const result = await installer.ensureGeminiCli(
33
- mockSandbox as unknown as SandboxManager
34
- );
35
- expect(result).toBe(true);
36
- expect(mockSandbox.exec).not.toHaveBeenCalled();
37
- });
38
-
39
- // ─── Test #35: Dev mode installs Gemini CLI on first use ───
40
-
41
- it("installs Gemini CLI on first call in dev mode", async () => {
42
- const installer = new LazyInstaller(false);
43
-
44
- // First check fails (not installed), then install succeeds, then extension succeeds
45
- mockSandbox.exec
46
- .mockRejectedValueOnce(new Error("not found")) // version check
47
- .mockResolvedValueOnce({ exitCode: 0, stdout: "installed", stderr: "" }) // npm install
48
- .mockResolvedValueOnce({ exitCode: 0, stdout: "ok", stderr: "" }); // extension install
49
-
50
- const result = await installer.ensureGeminiCli(
51
- mockSandbox as unknown as SandboxManager
52
- );
53
-
54
- expect(result).toBe(true);
55
- expect(installer.isGeminiCliReady()).toBe(true);
56
- });
57
-
58
- // ─── Test #36: Caches install state — second call is a no-op ───
59
-
60
- it("does not re-install on second call (cached)", async () => {
61
- const installer = new LazyInstaller(false);
62
-
63
- // First: fails check, succeeds install + extension
64
- mockSandbox.exec
65
- .mockRejectedValueOnce(new Error("not found"))
66
- .mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" })
67
- .mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" });
68
-
69
- await installer.ensureGeminiCli(mockSandbox as unknown as SandboxManager);
70
- mockSandbox.exec.mockClear();
71
-
72
- // Second call — should return immediately
73
- const result = await installer.ensureGeminiCli(
74
- mockSandbox as unknown as SandboxManager
75
- );
76
- expect(result).toBe(true);
77
- expect(mockSandbox.exec).not.toHaveBeenCalled();
78
- });
79
-
80
- // ─── Test #37: Returns false if install fails ───
81
-
82
- it("returns false if Gemini CLI installation fails", async () => {
83
- const installer = new LazyInstaller(false);
84
-
85
- mockSandbox.exec
86
- .mockRejectedValueOnce(new Error("not found")) // version check
87
- .mockResolvedValueOnce({ exitCode: 1, stdout: "", stderr: "error" }); // install fails
88
-
89
- const result = await installer.ensureGeminiCli(
90
- mockSandbox as unknown as SandboxManager
91
- );
92
- expect(result).toBe(false);
93
- expect(installer.isGeminiCliReady()).toBe(false);
94
- });
95
-
96
- // ─── Test #38: OSV-Scanner install attempt ───
97
-
98
- it("installs OSV-Scanner via curl when not available", async () => {
99
- const installer = new LazyInstaller(false);
100
-
101
- mockSandbox.exec
102
- .mockRejectedValueOnce(new Error("not found")) // version check
103
- .mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" }); // curl install
104
-
105
- const result = await installer.ensureOsvScanner(
106
- mockSandbox as unknown as SandboxManager
107
- );
108
- expect(result).toBe(true);
109
- expect(installer.isOsvScannerReady()).toBe(true);
110
- });
111
- });
@@ -1,97 +0,0 @@
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
-
6
- // We will import these once they exist — test-first.
7
- import { loadConfig, saveConfig, DEFAULT_CONFIG } from "../cli/config.js";
8
-
9
- describe("Config Manager", () => {
10
- // Use a temp directory so we never touch the real ~/.joone
11
- let tempDir: string;
12
- let configPath: string;
13
- let savedAnthropicKey: string | undefined;
14
-
15
- beforeEach(() => {
16
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "joone-test-"));
17
- configPath = path.join(tempDir, "config.json");
18
- // Isolate from vitest.config.ts env vars
19
- savedAnthropicKey = process.env.ANTHROPIC_API_KEY;
20
- delete process.env.ANTHROPIC_API_KEY;
21
- });
22
-
23
- afterEach(() => {
24
- fs.rmSync(tempDir, { recursive: true, force: true });
25
- // Restore env var
26
- if (savedAnthropicKey !== undefined) {
27
- process.env.ANTHROPIC_API_KEY = savedAnthropicKey;
28
- }
29
- });
30
-
31
- // ─── RED Test #1: loadConfig returns defaults when file doesn't exist ───
32
-
33
- it("returns default config when config file does not exist", () => {
34
- const config = loadConfig(configPath);
35
-
36
- expect(config.provider).toBe("anthropic");
37
- expect(config.model).toBe("claude-sonnet-4-20250514");
38
- expect(config.apiKey).toBeUndefined();
39
- expect(config.maxTokens).toBe(4096);
40
- expect(config.temperature).toBe(0);
41
- expect(config.streaming).toBe(true);
42
- });
43
-
44
- // ─── RED Test #2: saveConfig roundtrips with loadConfig ───
45
-
46
- it("saves config to disk and loads it back correctly", () => {
47
- const custom = {
48
- ...DEFAULT_CONFIG,
49
- provider: "openai",
50
- model: "gpt-4o",
51
- apiKey: "sk-test-key-123",
52
- streaming: false,
53
- };
54
-
55
- saveConfig(configPath, custom);
56
-
57
- // File should exist now
58
- expect(fs.existsSync(configPath)).toBe(true);
59
-
60
- // Load it back — should match what was saved
61
- const loaded = loadConfig(configPath);
62
- expect(loaded.provider).toBe("openai");
63
- expect(loaded.model).toBe("gpt-4o");
64
- expect(loaded.apiKey).toBe("sk-test-key-123");
65
- expect(loaded.streaming).toBe(false);
66
- // Fields we didn't override should keep defaults
67
- expect(loaded.maxTokens).toBe(4096);
68
- expect(loaded.temperature).toBe(0);
69
- });
70
-
71
- // ─── RED Test #3: loadConfig uses env var fallback for API key ───
72
-
73
- it("falls back to ANTHROPIC_API_KEY env var when apiKey is missing from config", () => {
74
- // Save a config WITHOUT an apiKey
75
- const noKeyConfig = {
76
- ...DEFAULT_CONFIG,
77
- provider: "anthropic",
78
- };
79
- saveConfig(configPath, noKeyConfig);
80
-
81
- // Set the env var
82
- const originalEnv = process.env.ANTHROPIC_API_KEY;
83
- process.env.ANTHROPIC_API_KEY = "sk-ant-from-env";
84
-
85
- try {
86
- const config = loadConfig(configPath);
87
- expect(config.apiKey).toBe("sk-ant-from-env");
88
- } finally {
89
- // Restore env
90
- if (originalEnv === undefined) {
91
- delete process.env.ANTHROPIC_API_KEY;
92
- } else {
93
- process.env.ANTHROPIC_API_KEY = originalEnv;
94
- }
95
- }
96
- });
97
- });
@@ -1,238 +0,0 @@
1
- import { describe, it, expect, vi, 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 {
9
- SearchSkillsTool,
10
- LoadSkillTool,
11
- bindSkillLoader,
12
- } from "../skills/tools.js";
13
- import { ToolRouter, ToolTarget } from "../tools/router.js";
14
-
15
- // ═══════════════════════════════════════════════════════════════════════════════
16
- // 5.5a: Browser Tool
17
- // ═══════════════════════════════════════════════════════════════════════════════
18
-
19
- describe("BrowserTool", () => {
20
- // ─── Test #70: Builds navigate command ───
21
-
22
- it("has the correct schema with all supported actions", () => {
23
- expect(BrowserTool.name).toBe("browser");
24
- expect(BrowserTool.schema.properties.action.enum).toContain("navigate");
25
- expect(BrowserTool.schema.properties.action.enum).toContain("snapshot");
26
- expect(BrowserTool.schema.properties.action.enum).toContain("click");
27
- expect(BrowserTool.schema.properties.action.enum).toContain("type");
28
- expect(BrowserTool.schema.properties.action.enum).toContain("screenshot");
29
- expect(BrowserTool.schema.properties.action.enum).toContain("scroll");
30
- });
31
-
32
- // ─── Test #71: Rejects without sandbox ───
33
-
34
- it("throws when sandbox is not active", async () => {
35
- const result = await BrowserTool.execute({ action: "navigate", url: "https://example.com" });
36
- expect(result.isError).toBe(true);
37
- expect(result.content).toMatch(/sandbox/i);
38
- });
39
- });
40
-
41
- // ═══════════════════════════════════════════════════════════════════════════════
42
- // 5.5b: Web Search Tool
43
- // ═══════════════════════════════════════════════════════════════════════════════
44
-
45
- describe("WebSearchTool", () => {
46
- // ─── Test #72: Returns error if no API key ───
47
-
48
- it("returns error when API key is not configured", async () => {
49
- bindValyuApiKey(undefined);
50
- const result = await WebSearchTool.execute({ query: "test" });
51
-
52
- expect(result.content).toMatch(/api key not configured/i);
53
- });
54
-
55
- // ─── Test #73: Schema includes all sources ───
56
-
57
- it("schema includes all search sources", () => {
58
- const sources = WebSearchTool.schema.properties.source.enum;
59
-
60
- expect(sources).toContain("web");
61
- expect(sources).toContain("papers");
62
- expect(sources).toContain("finance");
63
- expect(sources).toContain("patents");
64
- expect(sources).toContain("sec");
65
- expect(sources).toContain("companies");
66
- });
67
- });
68
-
69
- // ═══════════════════════════════════════════════════════════════════════════════
70
- // 5.5c: Skills System
71
- // ═══════════════════════════════════════════════════════════════════════════════
72
-
73
- describe("SkillLoader", () => {
74
- let tmpDir: string;
75
-
76
- beforeEach(() => {
77
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "joone-skills-test-"));
78
- });
79
-
80
- afterEach(() => {
81
- fs.rmSync(tmpDir, { recursive: true, force: true });
82
- });
83
-
84
- const createSkill = (
85
- dir: string,
86
- name: string,
87
- content: string
88
- ): void => {
89
- const skillDir = path.join(dir, name);
90
- fs.mkdirSync(skillDir, { recursive: true });
91
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), content);
92
- };
93
-
94
- // ─── Test #74: Discovers skills from project root ───
95
-
96
- it("discovers skills from a directory", () => {
97
- const skillsDir = path.join(tmpDir, "skills");
98
- createSkill(
99
- skillsDir,
100
- "deploy",
101
- "---\nname: deploy\ndescription: Deploy to Vercel\n---\n## Steps\n1. Run vercel deploy"
102
- );
103
-
104
- const loader = new SkillLoader(tmpDir);
105
- const skills = loader.discoverSkills();
106
-
107
- expect(skills.length).toBeGreaterThanOrEqual(1);
108
- const deploy = skills.find((s) => s.name === "deploy");
109
- expect(deploy).toBeDefined();
110
- expect(deploy!.description).toBe("Deploy to Vercel");
111
- expect(deploy!.source).toBe("project");
112
- });
113
-
114
- // ─── Test #75: Parses YAML frontmatter ───
115
-
116
- it("parses YAML frontmatter correctly", () => {
117
- const loader = new SkillLoader(tmpDir);
118
- const result = loader.parseFrontmatter(
119
- "---\nname: test-skill\ndescription: A test skill\n---\n# Instructions"
120
- );
121
-
122
- expect(result.name).toBe("test-skill");
123
- expect(result.description).toBe("A test skill");
124
- });
125
-
126
- // ─── Test #76: Project skills override user skills with same name ───
127
-
128
- it("project skills override user-level skills with the same name", () => {
129
- // Create user-level skill in the mocked home directory (tmpDir)
130
- const userSkillsDir = path.join(tmpDir, ".joone", "skills");
131
- createSkill(
132
- userSkillsDir,
133
- "deploy",
134
- "---\nname: deploy\ndescription: User deploy\n---\n"
135
- );
136
-
137
- // Create project-level skill
138
- const projectSkillsDir = path.join(tmpDir, "skills");
139
- createSkill(
140
- projectSkillsDir,
141
- "deploy",
142
- "---\nname: deploy\ndescription: Project deploy\n---\n"
143
- );
144
-
145
- // Pass tmpDir as both projectRoot and userHome
146
- const loader = new SkillLoader(tmpDir, tmpDir);
147
- const skills = loader.discoverSkills();
148
-
149
- const deploy = skills.find((s) => s.name === "deploy");
150
- expect(deploy).toBeDefined();
151
- expect(deploy!.source).toBe("project");
152
- expect(deploy!.description).toBe("Project deploy");
153
-
154
- // Ensure we only discovered one deploy skill
155
- const deployCount = skills.filter((s) => s.name === "deploy").length;
156
- expect(deployCount).toBe(1);
157
- });
158
-
159
- // ─── Test #77: loadSkill reads full content ───
160
-
161
- it("loads the full content of a skill", () => {
162
- const skillsDir = path.join(tmpDir, "skills");
163
- const content =
164
- "---\nname: tdd\ndescription: Test-driven development\n---\n## Red-Green-Refactor\n1. Write failing test";
165
- createSkill(skillsDir, "tdd", content);
166
-
167
- const loader = new SkillLoader(tmpDir);
168
- const loaded = loader.loadSkill("tdd");
169
-
170
- expect(loaded).toBe(content);
171
- });
172
- });
173
-
174
- describe("Skills Tools", () => {
175
- let tmpDir: string;
176
-
177
- beforeEach(() => {
178
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "joone-skills-tools-test-"));
179
-
180
- const skillsDir = path.join(tmpDir, "skills");
181
- const skillDir = path.join(skillsDir, "deploy");
182
- fs.mkdirSync(skillDir, { recursive: true });
183
- fs.writeFileSync(
184
- path.join(skillDir, "SKILL.md"),
185
- "---\nname: deploy\ndescription: Deploy to production\n---\n## Steps"
186
- );
187
-
188
- bindSkillLoader(new SkillLoader(tmpDir));
189
- });
190
-
191
- afterEach(() => {
192
- fs.rmSync(tmpDir, { recursive: true, force: true });
193
- });
194
-
195
- // ─── Test #78: search_skills returns matching skills ───
196
-
197
- it("search_skills finds matching skills", async () => {
198
- const result = await SearchSkillsTool.execute({ query: "deploy" });
199
-
200
- expect(result.content).toContain("deploy");
201
- expect(result.content).toContain("production");
202
- });
203
-
204
- // ─── Test #79: load_skill reads full content ───
205
-
206
- it("load_skill returns the full SKILL.md content", async () => {
207
- const result = await LoadSkillTool.execute({ name: "deploy" });
208
-
209
- expect(result.content).toContain("## Steps");
210
- });
211
- });
212
-
213
- // ═══════════════════════════════════════════════════════════════════════════════
214
- // Routing
215
- // ═══════════════════════════════════════════════════════════════════════════════
216
-
217
- describe("ToolRouter (M5.5 additions)", () => {
218
- const router = new ToolRouter();
219
-
220
- // ─── Test #80: Browser routes to sandbox ───
221
-
222
- it("routes browser to sandbox", () => {
223
- expect(router.getTarget("browser")).toBe(ToolTarget.SANDBOX);
224
- });
225
-
226
- // ─── Test #81: web_search routes to host ───
227
-
228
- it("routes web_search to host", () => {
229
- expect(router.getTarget("web_search")).toBe(ToolTarget.HOST);
230
- });
231
-
232
- // ─── Test #82: skills tools route to host ───
233
-
234
- it("routes search_skills and load_skill to host", () => {
235
- expect(router.getTarget("search_skills")).toBe(ToolTarget.HOST);
236
- expect(router.getTarget("load_skill")).toBe(ToolTarget.HOST);
237
- });
238
- });