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.
- package/AGENTS.md +56 -0
- package/Handover.md +115 -0
- package/LICENSE +201 -0
- package/PROGRESS.md +160 -0
- package/README.md +114 -0
- package/dist/__tests__/bootstrap.test.d.ts +1 -0
- package/dist/__tests__/bootstrap.test.js +76 -0
- package/dist/__tests__/bootstrap.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +84 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/m55.test.d.ts +1 -0
- package/dist/__tests__/m55.test.js +160 -0
- package/dist/__tests__/m55.test.js.map +1 -0
- package/dist/__tests__/middleware.test.d.ts +1 -0
- package/dist/__tests__/middleware.test.js +169 -0
- package/dist/__tests__/middleware.test.js.map +1 -0
- package/dist/__tests__/modelFactory.test.d.ts +1 -0
- package/dist/__tests__/modelFactory.test.js +50 -0
- package/dist/__tests__/modelFactory.test.js.map +1 -0
- package/dist/__tests__/optimizations.test.d.ts +1 -0
- package/dist/__tests__/optimizations.test.js +136 -0
- package/dist/__tests__/optimizations.test.js.map +1 -0
- package/dist/__tests__/promptBuilder.test.d.ts +1 -0
- package/dist/__tests__/promptBuilder.test.js +108 -0
- package/dist/__tests__/promptBuilder.test.js.map +1 -0
- package/dist/__tests__/sandbox.test.d.ts +1 -0
- package/dist/__tests__/sandbox.test.js +78 -0
- package/dist/__tests__/sandbox.test.js.map +1 -0
- package/dist/__tests__/security.test.d.ts +1 -0
- package/dist/__tests__/security.test.js +86 -0
- package/dist/__tests__/security.test.js.map +1 -0
- package/dist/__tests__/streaming.test.d.ts +1 -0
- package/dist/__tests__/streaming.test.js +71 -0
- package/dist/__tests__/streaming.test.js.map +1 -0
- package/dist/__tests__/toolRouter.test.d.ts +1 -0
- package/dist/__tests__/toolRouter.test.js +37 -0
- package/dist/__tests__/toolRouter.test.js.map +1 -0
- package/dist/__tests__/tools.test.d.ts +1 -0
- package/dist/__tests__/tools.test.js +112 -0
- package/dist/__tests__/tools.test.js.map +1 -0
- package/dist/__tests__/tracing.test.d.ts +1 -0
- package/dist/__tests__/tracing.test.js +147 -0
- package/dist/__tests__/tracing.test.js.map +1 -0
- package/dist/cli/config.d.ts +49 -0
- package/dist/cli/config.js +86 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +625 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/modelFactory.d.ts +9 -0
- package/dist/cli/modelFactory.js +154 -0
- package/dist/cli/modelFactory.js.map +1 -0
- package/dist/cli/providers.d.ts +18 -0
- package/dist/cli/providers.js +94 -0
- package/dist/cli/providers.js.map +1 -0
- package/dist/core/agentLoop.d.ts +43 -0
- package/dist/core/agentLoop.js +245 -0
- package/dist/core/agentLoop.js.map +1 -0
- package/dist/core/errors.d.ts +62 -0
- package/dist/core/errors.js +139 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/promptBuilder.d.ts +49 -0
- package/dist/core/promptBuilder.js +84 -0
- package/dist/core/promptBuilder.js.map +1 -0
- package/dist/core/reasoningRouter.d.ts +62 -0
- package/dist/core/reasoningRouter.js +102 -0
- package/dist/core/reasoningRouter.js.map +1 -0
- package/dist/core/retry.d.ts +25 -0
- package/dist/core/retry.js +49 -0
- package/dist/core/retry.js.map +1 -0
- package/dist/core/sessionResumer.d.ts +17 -0
- package/dist/core/sessionResumer.js +78 -0
- package/dist/core/sessionResumer.js.map +1 -0
- package/dist/core/sessionStore.d.ts +45 -0
- package/dist/core/sessionStore.js +167 -0
- package/dist/core/sessionStore.js.map +1 -0
- package/dist/core/tokenCounter.d.ts +17 -0
- package/dist/core/tokenCounter.js +54 -0
- package/dist/core/tokenCounter.js.map +1 -0
- package/dist/evals/dataset.d.ts +4 -0
- package/dist/evals/dataset.js +61 -0
- package/dist/evals/dataset.js.map +1 -0
- package/dist/evals/evaluator.d.ts +21 -0
- package/dist/evals/evaluator.js +68 -0
- package/dist/evals/evaluator.js.map +1 -0
- package/dist/hitl/bridge.d.ts +65 -0
- package/dist/hitl/bridge.js +120 -0
- package/dist/hitl/bridge.js.map +1 -0
- package/dist/middleware/commandSanitizer.d.ts +18 -0
- package/dist/middleware/commandSanitizer.js +50 -0
- package/dist/middleware/commandSanitizer.js.map +1 -0
- package/dist/middleware/loopDetection.d.ts +28 -0
- package/dist/middleware/loopDetection.js +49 -0
- package/dist/middleware/loopDetection.js.map +1 -0
- package/dist/middleware/permission.d.ts +17 -0
- package/dist/middleware/permission.js +59 -0
- package/dist/middleware/permission.js.map +1 -0
- package/dist/middleware/pipeline.d.ts +31 -0
- package/dist/middleware/pipeline.js +62 -0
- package/dist/middleware/pipeline.js.map +1 -0
- package/dist/middleware/preCompletion.d.ts +29 -0
- package/dist/middleware/preCompletion.js +82 -0
- package/dist/middleware/preCompletion.js.map +1 -0
- package/dist/middleware/types.d.ts +40 -0
- package/dist/middleware/types.js +8 -0
- package/dist/middleware/types.js.map +1 -0
- package/dist/sandbox/bootstrap.d.ts +38 -0
- package/dist/sandbox/bootstrap.js +107 -0
- package/dist/sandbox/bootstrap.js.map +1 -0
- package/dist/sandbox/manager.d.ts +72 -0
- package/dist/sandbox/manager.js +180 -0
- package/dist/sandbox/manager.js.map +1 -0
- package/dist/sandbox/sync.d.ts +55 -0
- package/dist/sandbox/sync.js +135 -0
- package/dist/sandbox/sync.js.map +1 -0
- package/dist/skills/loader.d.ts +55 -0
- package/dist/skills/loader.js +132 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/tools.d.ts +5 -0
- package/dist/skills/tools.js +78 -0
- package/dist/skills/tools.js.map +1 -0
- package/dist/skills/types.d.ts +13 -0
- package/dist/skills/types.js +2 -0
- package/dist/skills/types.js.map +1 -0
- package/dist/test_cache.d.ts +1 -0
- package/dist/test_cache.js +55 -0
- package/dist/test_cache.js.map +1 -0
- package/dist/test_google.js +93 -0
- package/dist/tools/askUser.d.ts +10 -0
- package/dist/tools/askUser.js +42 -0
- package/dist/tools/askUser.js.map +1 -0
- package/dist/tools/browser.d.ts +19 -0
- package/dist/tools/browser.js +111 -0
- package/dist/tools/browser.js.map +1 -0
- package/dist/tools/index.d.ts +27 -0
- package/dist/tools/index.js +184 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/registry.d.ts +31 -0
- package/dist/tools/registry.js +168 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/router.d.ts +34 -0
- package/dist/tools/router.js +73 -0
- package/dist/tools/router.js.map +1 -0
- package/dist/tools/security.d.ts +28 -0
- package/dist/tools/security.js +183 -0
- package/dist/tools/security.js.map +1 -0
- package/dist/tools/webSearch.d.ts +6 -0
- package/dist/tools/webSearch.js +120 -0
- package/dist/tools/webSearch.js.map +1 -0
- package/dist/tracing/analyzer.d.ts +58 -0
- package/dist/tracing/analyzer.js +190 -0
- package/dist/tracing/analyzer.js.map +1 -0
- package/dist/tracing/langsmith.d.ts +38 -0
- package/dist/tracing/langsmith.js +50 -0
- package/dist/tracing/langsmith.js.map +1 -0
- package/dist/tracing/sessionTracer.d.ts +73 -0
- package/dist/tracing/sessionTracer.js +157 -0
- package/dist/tracing/sessionTracer.js.map +1 -0
- package/dist/tracing/types.d.ts +46 -0
- package/dist/tracing/types.js +5 -0
- package/dist/tracing/types.js.map +1 -0
- package/dist/ui/App.d.ts +24 -0
- package/dist/ui/App.js +172 -0
- package/dist/ui/App.js.map +1 -0
- package/dist/ui/components/HITLPrompt.d.ts +15 -0
- package/dist/ui/components/HITLPrompt.js +35 -0
- package/dist/ui/components/HITLPrompt.js.map +1 -0
- package/dist/ui/components/Header.d.ts +8 -0
- package/dist/ui/components/Header.js +6 -0
- package/dist/ui/components/Header.js.map +1 -0
- package/dist/ui/components/MessageBubble.d.ts +13 -0
- package/dist/ui/components/MessageBubble.js +17 -0
- package/dist/ui/components/MessageBubble.js.map +1 -0
- package/dist/ui/components/StatusBar.d.ts +21 -0
- package/dist/ui/components/StatusBar.js +34 -0
- package/dist/ui/components/StatusBar.js.map +1 -0
- package/dist/ui/components/StreamingText.d.ts +13 -0
- package/dist/ui/components/StreamingText.js +24 -0
- package/dist/ui/components/StreamingText.js.map +1 -0
- package/dist/ui/components/ToolCallPanel.d.ts +15 -0
- package/dist/ui/components/ToolCallPanel.js +18 -0
- package/dist/ui/components/ToolCallPanel.js.map +1 -0
- package/docs/01_insights_and_patterns.md +27 -0
- package/docs/02_edge_cases_and_mitigations.md +143 -0
- package/docs/03_initial_implementation_plan.md +66 -0
- package/docs/04_tech_stack_proposal.md +20 -0
- package/docs/05_prd.md +87 -0
- package/docs/06_user_stories.md +72 -0
- package/docs/07_system_architecture.md +138 -0
- package/docs/08_roadmap.md +200 -0
- package/e2b/Dockerfile +26 -0
- package/package.json +57 -0
- package/src/__tests__/bootstrap.test.ts +111 -0
- package/src/__tests__/config.test.ts +97 -0
- package/src/__tests__/m55.test.ts +238 -0
- package/src/__tests__/middleware.test.ts +219 -0
- package/src/__tests__/modelFactory.test.ts +63 -0
- package/src/__tests__/optimizations.test.ts +201 -0
- package/src/__tests__/promptBuilder.test.ts +141 -0
- package/src/__tests__/sandbox.test.ts +102 -0
- package/src/__tests__/security.test.ts +122 -0
- package/src/__tests__/streaming.test.ts +82 -0
- package/src/__tests__/toolRouter.test.ts +52 -0
- package/src/__tests__/tools.test.ts +146 -0
- package/src/__tests__/tracing.test.ts +196 -0
- package/src/agents/agentRegistry.ts +69 -0
- package/src/agents/agentSpec.ts +67 -0
- package/src/agents/builtinAgents.ts +142 -0
- package/src/cli/config.ts +124 -0
- package/src/cli/index.ts +730 -0
- package/src/cli/modelFactory.ts +174 -0
- package/src/cli/providers.ts +107 -0
- package/src/commands/builtinCommands.ts +293 -0
- package/src/commands/commandRegistry.ts +194 -0
- package/src/core/agentLoop.d.ts.map +1 -0
- package/src/core/agentLoop.ts +312 -0
- package/src/core/autoSave.ts +95 -0
- package/src/core/compactor.ts +252 -0
- package/src/core/contextGuard.ts +129 -0
- package/src/core/errors.ts +202 -0
- package/src/core/promptBuilder.d.ts.map +1 -0
- package/src/core/promptBuilder.ts +139 -0
- package/src/core/reasoningRouter.ts +121 -0
- package/src/core/retry.ts +75 -0
- package/src/core/sessionResumer.ts +90 -0
- package/src/core/sessionStore.ts +215 -0
- package/src/core/subAgent.ts +339 -0
- package/src/core/tokenCounter.ts +64 -0
- package/src/evals/dataset.ts +67 -0
- package/src/evals/evaluator.ts +81 -0
- package/src/hitl/bridge.ts +160 -0
- package/src/middleware/commandSanitizer.ts +60 -0
- package/src/middleware/loopDetection.ts +63 -0
- package/src/middleware/permission.ts +72 -0
- package/src/middleware/pipeline.ts +75 -0
- package/src/middleware/preCompletion.ts +94 -0
- package/src/middleware/types.ts +45 -0
- package/src/sandbox/bootstrap.ts +121 -0
- package/src/sandbox/manager.ts +239 -0
- package/src/sandbox/sync.ts +157 -0
- package/src/skills/loader.ts +143 -0
- package/src/skills/tools.ts +99 -0
- package/src/skills/types.ts +13 -0
- package/src/test_cache.ts +72 -0
- package/src/test_google.js +40 -0
- package/src/test_google.ts +40 -0
- package/src/tools/askUser.ts +47 -0
- package/src/tools/browser.ts +137 -0
- package/src/tools/index.d.ts.map +1 -0
- package/src/tools/index.ts +237 -0
- package/src/tools/registry.ts +198 -0
- package/src/tools/router.ts +78 -0
- package/src/tools/security.ts +220 -0
- package/src/tools/spawnAgent.ts +158 -0
- package/src/tools/webSearch.ts +142 -0
- package/src/tracing/analyzer.ts +265 -0
- package/src/tracing/langsmith.ts +63 -0
- package/src/tracing/sessionTracer.ts +202 -0
- package/src/tracing/types.ts +49 -0
- package/src/types/valyu.d.ts +37 -0
- package/src/ui/App.tsx +404 -0
- package/src/ui/components/HITLPrompt.tsx +119 -0
- package/src/ui/components/Header.tsx +51 -0
- package/src/ui/components/MessageBubble.tsx +46 -0
- package/src/ui/components/StatusBar.tsx +138 -0
- package/src/ui/components/StreamingText.tsx +48 -0
- package/src/ui/components/ToolCallPanel.tsx +80 -0
- package/tests/commands/commands.test.ts +356 -0
- package/tests/core/compactor.test.ts +217 -0
- package/tests/core/retryAndErrors.test.ts +164 -0
- package/tests/core/sessionResumer.test.ts +95 -0
- package/tests/core/sessionStore.test.ts +84 -0
- package/tests/core/stability.test.ts +165 -0
- package/tests/core/subAgent.test.ts +238 -0
- package/tests/hitl/hitlBridge.test.ts +115 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +10 -0
- package/vitest.out +48 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import {
|
|
8
|
+
intro,
|
|
9
|
+
outro,
|
|
10
|
+
select,
|
|
11
|
+
text,
|
|
12
|
+
password,
|
|
13
|
+
confirm,
|
|
14
|
+
spinner,
|
|
15
|
+
isCancel,
|
|
16
|
+
cancel,
|
|
17
|
+
} from "@clack/prompts";
|
|
18
|
+
import { loadConfig, saveConfig, JooneConfig, DEFAULT_CONFIG } from "./config.js";
|
|
19
|
+
import { createModel } from "./modelFactory.js";
|
|
20
|
+
import { installProvider, uninstallProvider, getProviderDir } from "./providers.js";
|
|
21
|
+
import { tryEnableLangSmithFromConfig } from "../tracing/langsmith.js";
|
|
22
|
+
import { TraceAnalyzer } from "../tracing/analyzer.js";
|
|
23
|
+
import { SessionTracer } from "../tracing/sessionTracer.js";
|
|
24
|
+
import { SandboxManager } from "../sandbox/manager.js";
|
|
25
|
+
import { ToolRouter } from "../tools/router.js";
|
|
26
|
+
import { MiddlewarePipeline } from "../middleware/pipeline.js";
|
|
27
|
+
import { LoopDetectionMiddleware } from "../middleware/loopDetection.js";
|
|
28
|
+
import { CommandSanitizerMiddleware } from "../middleware/commandSanitizer.js";
|
|
29
|
+
import { PreCompletionMiddleware } from "../middleware/preCompletion.js";
|
|
30
|
+
import { ExecutionHarness } from "../core/agentLoop.js";
|
|
31
|
+
import { SessionStore } from "../core/sessionStore.js";
|
|
32
|
+
import { SessionResumer } from "../core/sessionResumer.js";
|
|
33
|
+
import { PermissionMiddleware } from "../middleware/permission.js";
|
|
34
|
+
import { AskUserQuestionTool } from "../tools/askUser.js";
|
|
35
|
+
import { createDefaultAgentRegistry } from "../agents/builtinAgents.js";
|
|
36
|
+
import { SubAgentManager } from "../core/subAgent.js";
|
|
37
|
+
import { createSpawnAgentTools } from "../tools/spawnAgent.js";
|
|
38
|
+
|
|
39
|
+
const CONFIG_PATH = path.join(os.homedir(), ".joone", "config.json");
|
|
40
|
+
|
|
41
|
+
const SUPPORTED_PROVIDERS = [
|
|
42
|
+
{ value: "anthropic", label: "Anthropic", hint: "Claude 4, 3.5 Sonnet, Opus, Haiku" },
|
|
43
|
+
{ value: "openai", label: "OpenAI", hint: "GPT-4o, o1, o3-mini" },
|
|
44
|
+
{ value: "google", label: "Google", hint: "Gemini 3.1 Pro-preview, 3 Flash-preview" },
|
|
45
|
+
{ value: "mistral", label: "Mistral", hint: "Mistral Large, Codestral" },
|
|
46
|
+
{ value: "groq", label: "Groq", hint: "Llama 3.1 70B, Mixtral" },
|
|
47
|
+
{ value: "deepseek", label: "DeepSeek", hint: "DeepSeek Chat, Reasoner" },
|
|
48
|
+
{ value: "fireworks", label: "Fireworks AI", hint: "Llama 3.1 70B Instruct" },
|
|
49
|
+
{ value: "together", label: "Together AI", hint: "Llama 3.1 Turbo" },
|
|
50
|
+
{ value: "ollama", label: "Ollama (Local)", hint: "No API key needed" },
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
const PROVIDER_MODELS: Record<string, { value: string; label: string }[]> = {
|
|
54
|
+
anthropic: [
|
|
55
|
+
{ value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4" },
|
|
56
|
+
{ value: "claude-3-5-sonnet-20241022", label: "Claude 3.5 Sonnet" },
|
|
57
|
+
{ value: "claude-3-opus-20240229", label: "Claude 3 Opus" },
|
|
58
|
+
{ value: "claude-3-haiku-20240307", label: "Claude 3 Haiku" },
|
|
59
|
+
],
|
|
60
|
+
openai: [
|
|
61
|
+
{ value: "gpt-4o", label: "GPT-4o" },
|
|
62
|
+
{ value: "gpt-4o-mini", label: "GPT-4o Mini" },
|
|
63
|
+
{ value: "o1", label: "o1" },
|
|
64
|
+
{ value: "o3-mini", label: "o3-mini" },
|
|
65
|
+
],
|
|
66
|
+
google: [
|
|
67
|
+
{ value: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro Preview" },
|
|
68
|
+
{ value: "gemini-3-flash-preview", label: "Gemini 3 Flash Preview" },
|
|
69
|
+
{ value: "gemini-2.5-flash", label: "Gemini 2.5 Flash" },
|
|
70
|
+
{ value: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
|
|
71
|
+
{ value: "gemini-1.5-pro", label: "Gemini 1.5 Pro" },
|
|
72
|
+
{ value: "gemini-1.5-flash", label: "Gemini 1.5 Flash" },
|
|
73
|
+
],
|
|
74
|
+
mistral: [
|
|
75
|
+
{ value: "mistral-large-latest", label: "Mistral Large" },
|
|
76
|
+
{ value: "codestral-latest", label: "Codestral" },
|
|
77
|
+
{ value: "mistral-small-latest", label: "Mistral Small" },
|
|
78
|
+
],
|
|
79
|
+
groq: [
|
|
80
|
+
{ value: "llama-3.1-70b-versatile", label: "Llama 3.1 70B" },
|
|
81
|
+
{ value: "mixtral-8x7b-32768", label: "Mixtral 8x7B" },
|
|
82
|
+
],
|
|
83
|
+
deepseek: [
|
|
84
|
+
{ value: "deepseek-chat", label: "DeepSeek Chat" },
|
|
85
|
+
{ value: "deepseek-reasoner", label: "DeepSeek Reasoner" },
|
|
86
|
+
],
|
|
87
|
+
fireworks: [
|
|
88
|
+
{ value: "accounts/fireworks/models/llama-v3p1-70b-instruct", label: "Llama 3.1 70B Instruct" },
|
|
89
|
+
],
|
|
90
|
+
together: [
|
|
91
|
+
{ value: "meta-llama/Llama-3.1-70B-Instruct-Turbo", label: "Llama 3.1 70B Turbo" },
|
|
92
|
+
],
|
|
93
|
+
ollama: [
|
|
94
|
+
{ value: "llama3", label: "Llama 3" },
|
|
95
|
+
{ value: "codellama", label: "Code Llama" },
|
|
96
|
+
{ value: "mistral", label: "Mistral" },
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const program = new Command();
|
|
101
|
+
|
|
102
|
+
program
|
|
103
|
+
.name("joone")
|
|
104
|
+
.description("An autonomous coding agent powered by prompt caching and harness engineering")
|
|
105
|
+
.version("0.1.0");
|
|
106
|
+
|
|
107
|
+
// ─── Shared onboarding flow ────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Interactive onboarding wizard. Prompts the user for provider, model, API keys,
|
|
111
|
+
* and optional service keys. Saves the config to disk and returns it.
|
|
112
|
+
*
|
|
113
|
+
* Called by `joone config` directly, and auto-triggered by `joone start` when
|
|
114
|
+
* no valid configuration is found.
|
|
115
|
+
*/
|
|
116
|
+
async function runOnboarding(): Promise<JooneConfig> {
|
|
117
|
+
intro(chalk.bgCyan.black(" 🔧 joone setup "));
|
|
118
|
+
|
|
119
|
+
const existingConfig = loadConfig(CONFIG_PATH);
|
|
120
|
+
|
|
121
|
+
// ── Provider ───────────────────────────────
|
|
122
|
+
const provider = await select({
|
|
123
|
+
message: "Select your LLM provider",
|
|
124
|
+
options: SUPPORTED_PROVIDERS.map((p) => ({
|
|
125
|
+
value: p.value,
|
|
126
|
+
label: p.label,
|
|
127
|
+
hint: p.hint,
|
|
128
|
+
})),
|
|
129
|
+
initialValue: existingConfig.provider,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (isCancel(provider)) {
|
|
133
|
+
cancel("Configuration cancelled.");
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Model ─────────────────────────────────
|
|
138
|
+
const models = PROVIDER_MODELS[provider] || [];
|
|
139
|
+
let model: string | symbol;
|
|
140
|
+
|
|
141
|
+
if (models.length > 0) {
|
|
142
|
+
model = await select({
|
|
143
|
+
message: "Select your model",
|
|
144
|
+
options: models,
|
|
145
|
+
initialValue: existingConfig.model,
|
|
146
|
+
});
|
|
147
|
+
} else {
|
|
148
|
+
model = await text({
|
|
149
|
+
message: "Enter the model name",
|
|
150
|
+
defaultValue: existingConfig.model,
|
|
151
|
+
placeholder: existingConfig.model,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (isCancel(model)) {
|
|
156
|
+
cancel("Configuration cancelled.");
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── API Key ───────────────────────────────
|
|
161
|
+
let apiKey: string | symbol | undefined;
|
|
162
|
+
if (provider !== "ollama") {
|
|
163
|
+
apiKey = await password({
|
|
164
|
+
message: `Enter your ${chalk.bold(provider.toUpperCase())} API key`,
|
|
165
|
+
mask: "•",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (isCancel(apiKey)) {
|
|
169
|
+
cancel("Configuration cancelled.");
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!apiKey || apiKey.trim() === "") {
|
|
174
|
+
apiKey = existingConfig.apiKey;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Optional Service Keys (skip with Enter) ──
|
|
179
|
+
|
|
180
|
+
const sectionMsg = chalk.dim("\n Optional service keys (press Enter to skip):\n");
|
|
181
|
+
console.log(sectionMsg);
|
|
182
|
+
|
|
183
|
+
// E2B (Sandbox)
|
|
184
|
+
let e2bKey = await password({
|
|
185
|
+
message: `E2B API key ${chalk.dim("(primary sandbox)")}`,
|
|
186
|
+
mask: "•",
|
|
187
|
+
});
|
|
188
|
+
if (isCancel(e2bKey)) { cancel("Configuration cancelled."); process.exit(0); }
|
|
189
|
+
if (!e2bKey || (e2bKey as string).trim() === "") e2bKey = existingConfig.e2bApiKey ?? "";
|
|
190
|
+
|
|
191
|
+
// OpenSandbox (Fallback)
|
|
192
|
+
let osKey = await password({
|
|
193
|
+
message: `OpenSandbox API key ${chalk.dim("(fallback sandbox)")}`,
|
|
194
|
+
mask: "•",
|
|
195
|
+
});
|
|
196
|
+
if (isCancel(osKey)) { cancel("Configuration cancelled."); process.exit(0); }
|
|
197
|
+
if (!osKey || (osKey as string).trim() === "") osKey = existingConfig.openSandboxApiKey ?? "";
|
|
198
|
+
|
|
199
|
+
let osDomain = await text({
|
|
200
|
+
message: `OpenSandbox Domain ${chalk.dim("(fallback sandbox domain, default: localhost:8080)")}`,
|
|
201
|
+
placeholder: "localhost:8080",
|
|
202
|
+
defaultValue: existingConfig.openSandboxDomain ?? "",
|
|
203
|
+
});
|
|
204
|
+
if (isCancel(osDomain)) { cancel("Configuration cancelled."); process.exit(0); }
|
|
205
|
+
if (!osDomain || (osDomain as string).trim() === "") osDomain = existingConfig.openSandboxDomain ?? "";
|
|
206
|
+
|
|
207
|
+
// Gemini (Security scanning)
|
|
208
|
+
let geminiKey = await password({
|
|
209
|
+
message: `Gemini API key ${chalk.dim("(security scan)")}`,
|
|
210
|
+
mask: "•",
|
|
211
|
+
});
|
|
212
|
+
if (isCancel(geminiKey)) { cancel("Configuration cancelled."); process.exit(0); }
|
|
213
|
+
if (!geminiKey || (geminiKey as string).trim() === "") geminiKey = existingConfig.geminiApiKey ?? "";
|
|
214
|
+
|
|
215
|
+
// Valyu (Web search)
|
|
216
|
+
let valyuKey = await password({
|
|
217
|
+
message: `Valyu API key ${chalk.dim("(web search)")}`,
|
|
218
|
+
mask: "•",
|
|
219
|
+
});
|
|
220
|
+
if (isCancel(valyuKey)) { cancel("Configuration cancelled."); process.exit(0); }
|
|
221
|
+
if (!valyuKey || (valyuKey as string).trim() === "") valyuKey = existingConfig.valyuApiKey ?? "";
|
|
222
|
+
|
|
223
|
+
// LangSmith (Tracing)
|
|
224
|
+
let langsmithKey = await password({
|
|
225
|
+
message: `LangSmith API key ${chalk.dim("(tracing)")}`,
|
|
226
|
+
mask: "•",
|
|
227
|
+
});
|
|
228
|
+
if (isCancel(langsmithKey)) { cancel("Configuration cancelled."); process.exit(0); }
|
|
229
|
+
if (!langsmithKey || (langsmithKey as string).trim() === "") langsmithKey = existingConfig.langsmithApiKey ?? "";
|
|
230
|
+
|
|
231
|
+
const s = spinner();
|
|
232
|
+
try {
|
|
233
|
+
s.start(`Downloading and installing the ${provider} provider package...`);
|
|
234
|
+
await installProvider(provider as string);
|
|
235
|
+
s.stop(`Installed ${provider} provider package!`);
|
|
236
|
+
} catch (err: any) {
|
|
237
|
+
s.stop(`Failed to install ${provider} package.`);
|
|
238
|
+
console.error(chalk.yellow(`\n ⚠ Could not auto-install the provider package: ${err.message}`));
|
|
239
|
+
console.log(chalk.dim(` Try running: joone provider add ${provider}\n`));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const s2 = spinner();
|
|
243
|
+
s2.start("Saving configuration...");
|
|
244
|
+
|
|
245
|
+
const newConfig: JooneConfig = {
|
|
246
|
+
provider: provider as string,
|
|
247
|
+
model: model as string,
|
|
248
|
+
apiKey: typeof apiKey === "string" ? apiKey : undefined,
|
|
249
|
+
maxTokens: existingConfig.maxTokens,
|
|
250
|
+
temperature: existingConfig.temperature,
|
|
251
|
+
streaming: existingConfig.streaming,
|
|
252
|
+
sandboxTemplate: existingConfig.sandboxTemplate,
|
|
253
|
+
e2bApiKey: typeof e2bKey === "string" ? e2bKey : undefined,
|
|
254
|
+
openSandboxApiKey: typeof osKey === "string" ? osKey : undefined,
|
|
255
|
+
openSandboxDomain: typeof osDomain === "string" ? osDomain : undefined,
|
|
256
|
+
geminiApiKey: typeof geminiKey === "string" ? geminiKey : undefined,
|
|
257
|
+
valyuApiKey: typeof valyuKey === "string" ? valyuKey : undefined,
|
|
258
|
+
langsmithApiKey: typeof langsmithKey === "string" ? langsmithKey : undefined,
|
|
259
|
+
langsmithProject: existingConfig.langsmithProject,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
saveConfig(CONFIG_PATH, newConfig);
|
|
263
|
+
s2.stop("Configuration saved!");
|
|
264
|
+
|
|
265
|
+
outro(
|
|
266
|
+
chalk.green("✓") +
|
|
267
|
+
` Config written to ${chalk.dim(CONFIG_PATH)}\n` +
|
|
268
|
+
` Starting Joone session...`
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
return newConfig;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ─── joone config ──────────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
program
|
|
277
|
+
.command("config")
|
|
278
|
+
.description("Configure your LLM provider, model, and API key")
|
|
279
|
+
.action(async () => {
|
|
280
|
+
await runOnboarding();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// ─── joone provider ────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
const providerCmd = program.command("provider").description("Manage dynamic LLM provider packages");
|
|
286
|
+
|
|
287
|
+
providerCmd
|
|
288
|
+
.command("add <providerName>")
|
|
289
|
+
.description("Install a provider package")
|
|
290
|
+
.action(async (providerName) => {
|
|
291
|
+
const s = spinner();
|
|
292
|
+
s.start(`Installing ${providerName}...`);
|
|
293
|
+
try {
|
|
294
|
+
await installProvider(providerName);
|
|
295
|
+
s.stop(`Successfully installed ${providerName}`);
|
|
296
|
+
} catch (e: any) {
|
|
297
|
+
s.stop(`Failed to install ${providerName}`);
|
|
298
|
+
console.error(chalk.red(`\n ✗ ${e.message}\n`));
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
providerCmd
|
|
304
|
+
.command("remove <providerName>")
|
|
305
|
+
.description("Uninstall a provider package")
|
|
306
|
+
.action(async (providerName) => {
|
|
307
|
+
const s = spinner();
|
|
308
|
+
s.start(`Uninstalling ${providerName}...`);
|
|
309
|
+
try {
|
|
310
|
+
await uninstallProvider(providerName);
|
|
311
|
+
s.stop(`Successfully uninstalled ${providerName}`);
|
|
312
|
+
} catch (e: any) {
|
|
313
|
+
s.stop(`Failed to uninstall ${providerName}`);
|
|
314
|
+
console.error(chalk.red(`\n ✗ ${e.message}\n`));
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ─── joone cleanup ─────────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
program
|
|
322
|
+
.command("cleanup")
|
|
323
|
+
.description("Remove all Joone user data, settings, and dynamically installed providers")
|
|
324
|
+
.action(async () => {
|
|
325
|
+
const isConfirmed = await confirm({
|
|
326
|
+
message: `Are you sure you want to delete all Joone data and settings in ${chalk.bold("~/.joone")}?`,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (isCancel(isConfirmed) || !isConfirmed) {
|
|
330
|
+
cancel("Cleanup aborted.");
|
|
331
|
+
process.exit(0);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const jooneDir = path.join(os.homedir(), ".joone");
|
|
335
|
+
const s = spinner();
|
|
336
|
+
s.start("Deleting ~/.joone directory...");
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
if (fs.existsSync(jooneDir)) {
|
|
340
|
+
fs.rmSync(jooneDir, { recursive: true, force: true });
|
|
341
|
+
s.stop("Cleanup complete.");
|
|
342
|
+
console.log(
|
|
343
|
+
chalk.green(`\n ✓ Successfully removed ${jooneDir}\n`) +
|
|
344
|
+
chalk.dim(` To finish removing Joone entirely, uninstall it via your package manager:\n`) +
|
|
345
|
+
chalk.dim(` e.g., \`npm uninstall -g joone\` or \`brew uninstall joone\`\n`)
|
|
346
|
+
);
|
|
347
|
+
} else {
|
|
348
|
+
s.stop("Nothing to clean up.");
|
|
349
|
+
console.log(chalk.dim(`\n Directory ${jooneDir} does not exist.\n`));
|
|
350
|
+
}
|
|
351
|
+
} catch (e: any) {
|
|
352
|
+
s.stop("Cleanup failed.");
|
|
353
|
+
console.error(chalk.red(`\n ✗ Error deleting directory: ${e.message}\n`));
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// ─── joone (default) ───────────────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
program
|
|
361
|
+
.command("start", { isDefault: true })
|
|
362
|
+
.description("Start a new Joone agent session")
|
|
363
|
+
.option("--no-stream", "Disable streaming output")
|
|
364
|
+
.option("-r, --resume <sessionId>", "Resume a previous session by ID")
|
|
365
|
+
.action(async (options) => {
|
|
366
|
+
let config = loadConfig(CONFIG_PATH);
|
|
367
|
+
|
|
368
|
+
// Auto-trigger onboarding if no API key is configured
|
|
369
|
+
if (!config.apiKey && config.provider !== "ollama") {
|
|
370
|
+
console.log(
|
|
371
|
+
chalk.yellow("\n ⚠ No configuration found.") +
|
|
372
|
+
chalk.dim(" Let's set up Joone!\n")
|
|
373
|
+
);
|
|
374
|
+
config = await runOnboarding();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (options.stream === false) {
|
|
378
|
+
config.streaming = false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const tracingEnabled = tryEnableLangSmithFromConfig(config);
|
|
382
|
+
|
|
383
|
+
console.log(
|
|
384
|
+
chalk.cyan("\n ◆ joone") +
|
|
385
|
+
chalk.dim(" v0.1.0\n") +
|
|
386
|
+
chalk.dim(" ├ Provider: ") + chalk.white(config.provider) + "\n" +
|
|
387
|
+
chalk.dim(" ├ Model: ") + chalk.white(config.model) + "\n" +
|
|
388
|
+
chalk.dim(" ├ Stream: ") + chalk.white(config.streaming ? "on" : "off") + "\n" +
|
|
389
|
+
chalk.dim(" └ Tracing: ") + (tracingEnabled ? chalk.green("LangSmith") : chalk.dim("local only")) + "\n"
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
const model = await createModel(config);
|
|
394
|
+
|
|
395
|
+
const pipeline = new MiddlewarePipeline();
|
|
396
|
+
pipeline.use(new LoopDetectionMiddleware(3));
|
|
397
|
+
pipeline.use(new CommandSanitizerMiddleware());
|
|
398
|
+
pipeline.use(new PermissionMiddleware(config.permissionMode ?? "auto"));
|
|
399
|
+
const tracer = new SessionTracer();
|
|
400
|
+
|
|
401
|
+
const { bindSandbox } = await import("../tools/index.js");
|
|
402
|
+
|
|
403
|
+
const s = spinner();
|
|
404
|
+
s.start("Initializing Sandbox Environment...");
|
|
405
|
+
const sandboxManager = new SandboxManager({
|
|
406
|
+
template: config.sandboxTemplate,
|
|
407
|
+
apiKey: config.e2bApiKey,
|
|
408
|
+
openSandboxApiKey: config.openSandboxApiKey,
|
|
409
|
+
openSandboxDomain: config.openSandboxDomain,
|
|
410
|
+
});
|
|
411
|
+
await sandboxManager.create();
|
|
412
|
+
|
|
413
|
+
const { FileSync } = await import("../sandbox/sync.js");
|
|
414
|
+
const fileSync = new FileSync(process.cwd());
|
|
415
|
+
bindSandbox(sandboxManager, fileSync);
|
|
416
|
+
|
|
417
|
+
// Sync user-level skills into the sandbox
|
|
418
|
+
const { SkillLoader } = await import("../skills/loader.js");
|
|
419
|
+
const skillLoader = new SkillLoader();
|
|
420
|
+
const skillPaths = skillLoader.getDiscoveryPaths();
|
|
421
|
+
await fileSync.syncSkillsToSandbox(sandboxManager, skillPaths);
|
|
422
|
+
|
|
423
|
+
s.stop("Sandbox initialized");
|
|
424
|
+
|
|
425
|
+
// For the CLI, we start by loading the CORE tools
|
|
426
|
+
// Advanced tools (search, browser, etc.) will be dynamically loaded by the agent later
|
|
427
|
+
// via the SearchToolsTool when the registry is fully integrated
|
|
428
|
+
const { CORE_TOOLS } = await import("../tools/index.js");
|
|
429
|
+
let tools = [...CORE_TOOLS, AskUserQuestionTool] as import("../tools/index.js").DynamicToolInterface[];
|
|
430
|
+
|
|
431
|
+
// Initialize Sub-Agent Orchestration
|
|
432
|
+
const agentRegistry = createDefaultAgentRegistry();
|
|
433
|
+
const subAgentModelName = config.subAgentModel ?? config.model;
|
|
434
|
+
|
|
435
|
+
// Use the config to determine sub-agent model, with FAST_MODEL_DEFAULTS fallback
|
|
436
|
+
const { resolveFastModel } = await import("../core/compactor.js");
|
|
437
|
+
const resolvedSubModel = resolveFastModel(config.provider, config.model, config.subAgentModel);
|
|
438
|
+
|
|
439
|
+
const subAgentLlm = await createModel({ ...config, model: resolvedSubModel });
|
|
440
|
+
const subAgentManager = new SubAgentManager(agentRegistry, tools, subAgentLlm);
|
|
441
|
+
const spawnAgentTools = createSpawnAgentTools(subAgentManager, agentRegistry);
|
|
442
|
+
|
|
443
|
+
tools = [...tools, ...spawnAgentTools];
|
|
444
|
+
|
|
445
|
+
let initialState;
|
|
446
|
+
let sessionId: string | undefined = undefined;
|
|
447
|
+
|
|
448
|
+
if (options.resume) {
|
|
449
|
+
const s = spinner();
|
|
450
|
+
s.start(`Loading session ${chalk.bold(options.resume)}...`);
|
|
451
|
+
const store = new SessionStore();
|
|
452
|
+
try {
|
|
453
|
+
const payload = await store.loadSession(options.resume);
|
|
454
|
+
const resumer = new SessionResumer(process.cwd());
|
|
455
|
+
initialState = resumer.prepareForResume(payload);
|
|
456
|
+
sessionId = options.resume;
|
|
457
|
+
s.stop(`Session resumed`);
|
|
458
|
+
} catch (e: any) {
|
|
459
|
+
s.stop(`Failed to load session`);
|
|
460
|
+
console.error(chalk.red(`\n ✗ ${e.message}\n`));
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
initialState = {
|
|
465
|
+
globalSystemInstructions: `You are Joone, a highly capable autonomous coding agent.
|
|
466
|
+
You run in a hybrid environment: you have read/write access to the host machine for code edits, but all code execution, testing, and dependency installation MUST happen in the isolated E2B sandbox for safety.
|
|
467
|
+
Always use 'bash' to run terminal commands. Never read or write outside the current project directory unless explicitly requested.
|
|
468
|
+
|
|
469
|
+
IMPORTANT CAPABILITIES:
|
|
470
|
+
- You have access to an 'ask_user_question' tool. Use it to ask the user for clarification, preferences, or approval before making significant changes.
|
|
471
|
+
- Some tool calls may require user approval before execution, depending on the user's permission settings. If a tool call is denied, try an alternative approach or ask the user for guidance.
|
|
472
|
+
- You have access to Skills — reusable instruction sets for specialized tasks. Use 'search_skills' to discover them and 'load_skill' to activate their instructions.`,
|
|
473
|
+
projectMemory: "No project context loaded yet.",
|
|
474
|
+
sessionContext: `Environment: ${process.platform}\nCWD: ${process.cwd()}`,
|
|
475
|
+
conversationHistory: []
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const harness = new ExecutionHarness(model, tools, pipeline, tracer, config.provider, config.model, sessionId);
|
|
480
|
+
|
|
481
|
+
const { render } = await import("ink");
|
|
482
|
+
const React = await import("react");
|
|
483
|
+
const { App } = await import("../ui/App.js");
|
|
484
|
+
|
|
485
|
+
const { waitUntilExit } = render(
|
|
486
|
+
// @ts-ignore (App is imported dynamically, JSX resolution might complain but it works)
|
|
487
|
+
React.createElement(App, {
|
|
488
|
+
provider: config.provider,
|
|
489
|
+
model: config.model,
|
|
490
|
+
streaming: config.streaming,
|
|
491
|
+
harness,
|
|
492
|
+
initialState,
|
|
493
|
+
maxTokens: config.maxTokens,
|
|
494
|
+
})
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
await waitUntilExit();
|
|
498
|
+
|
|
499
|
+
// Cleanup
|
|
500
|
+
tracer.save();
|
|
501
|
+
await sandboxManager.destroy();
|
|
502
|
+
} catch (error: unknown) {
|
|
503
|
+
if (error instanceof Error) {
|
|
504
|
+
console.error(chalk.red(`\n ✗ ${error.stack}\n`));
|
|
505
|
+
} else {
|
|
506
|
+
console.error(chalk.red(`\n ✗ ${String(error)}\n`));
|
|
507
|
+
}
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// ─── joone sessions ────────────────────────────────────────────────────────────
|
|
513
|
+
|
|
514
|
+
program
|
|
515
|
+
.command("sessions")
|
|
516
|
+
.description("List all persistent sessions available for resumption")
|
|
517
|
+
.action(async () => {
|
|
518
|
+
const store = new SessionStore();
|
|
519
|
+
const sessions = await store.listSessions();
|
|
520
|
+
|
|
521
|
+
if (sessions.length === 0) {
|
|
522
|
+
console.log(chalk.dim("\n No saved sessions found.\n"));
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
console.log(chalk.bold("\n Recent Sessions:"));
|
|
527
|
+
console.log(chalk.dim(" ─────────────────────────────────────────────────────────"));
|
|
528
|
+
|
|
529
|
+
for (const session of sessions) {
|
|
530
|
+
const date = new Date(session.lastSavedAt).toLocaleString();
|
|
531
|
+
console.log(
|
|
532
|
+
` ${chalk.cyan(session.sessionId)} ` +
|
|
533
|
+
chalk.dim(`[${date}] `) +
|
|
534
|
+
chalk.grey(`(${session.model})\n`) +
|
|
535
|
+
` ↳ ${chalk.white(session.description)}\n`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// ─── joone analyze ─────────────────────────────────────────────────────────────
|
|
541
|
+
|
|
542
|
+
program
|
|
543
|
+
.command("analyze [sessionId]")
|
|
544
|
+
.description("Analyze a session trace for performance insights")
|
|
545
|
+
.action((sessionId) => {
|
|
546
|
+
let tracePath;
|
|
547
|
+
const tracesDir = path.join(os.homedir(), ".joone", "traces");
|
|
548
|
+
|
|
549
|
+
if (sessionId) {
|
|
550
|
+
tracePath = path.join(tracesDir, sessionId.endsWith(".json") ? sessionId : `${sessionId}.json`);
|
|
551
|
+
} else {
|
|
552
|
+
// Find the most recent trace
|
|
553
|
+
if (!fs.existsSync(tracesDir)) {
|
|
554
|
+
console.error(chalk.red("\n ✗ No traces directory found.\n"));
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
const files = fs.readdirSync(tracesDir)
|
|
558
|
+
.filter(f => f.endsWith(".json"))
|
|
559
|
+
.map(f => ({ name: f, time: fs.statSync(path.join(tracesDir, f)).mtimeMs }))
|
|
560
|
+
.sort((a, b) => b.time - a.time); // newest first
|
|
561
|
+
|
|
562
|
+
if (files.length === 0) {
|
|
563
|
+
console.error(chalk.red("\n ✗ No trace files found.\n"));
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
tracePath = path.join(tracesDir, files[0].name);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (!fs.existsSync(tracePath)) {
|
|
570
|
+
console.error(chalk.red(`\n ✗ Trace file not found: ${tracePath}\n`));
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
const trace = SessionTracer.load(tracePath);
|
|
576
|
+
const analyzer = new TraceAnalyzer(trace);
|
|
577
|
+
const report = analyzer.analyze();
|
|
578
|
+
console.log(TraceAnalyzer.formatReport(report));
|
|
579
|
+
} catch (e: any) {
|
|
580
|
+
console.error(chalk.red(`\n ✗ Error analyzing trace: ${e.message}\n`));
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// ─── joone eval ────────────────────────────────────────────────────────────────
|
|
586
|
+
|
|
587
|
+
program
|
|
588
|
+
.command("eval")
|
|
589
|
+
.description("Run automated offline evaluation against the baseline LangSmith dataset")
|
|
590
|
+
.action(async () => {
|
|
591
|
+
let config = loadConfig(CONFIG_PATH);
|
|
592
|
+
|
|
593
|
+
if (!config.langsmithApiKey) {
|
|
594
|
+
console.error(chalk.red("\n ✗ LangSmith API key is missing. Run `joone config` to set it.\n"));
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
tryEnableLangSmithFromConfig(config);
|
|
599
|
+
|
|
600
|
+
console.log(chalk.cyan("\n ◆ joone evals") + chalk.dim(` (Model: ${config.model})\n`));
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
const { evaluate } = await import("langsmith/evaluation");
|
|
604
|
+
const { ensureBaselineDataset } = await import("../evals/dataset.js");
|
|
605
|
+
const {
|
|
606
|
+
successEvaluator,
|
|
607
|
+
cacheEfficiencyEvaluator,
|
|
608
|
+
filePresenceEvaluator
|
|
609
|
+
} = await import("../evals/evaluator.js");
|
|
610
|
+
|
|
611
|
+
const s = spinner();
|
|
612
|
+
s.start("Verifying baseline dataset...");
|
|
613
|
+
const datasetName = await ensureBaselineDataset();
|
|
614
|
+
s.stop(`Dataset verified: ${chalk.white(datasetName)}`);
|
|
615
|
+
|
|
616
|
+
const model = await createModel(config);
|
|
617
|
+
const pipeline = new MiddlewarePipeline();
|
|
618
|
+
pipeline.use(new LoopDetectionMiddleware(3));
|
|
619
|
+
pipeline.use(new CommandSanitizerMiddleware());
|
|
620
|
+
const tracer = new SessionTracer();
|
|
621
|
+
|
|
622
|
+
const { bindSandbox, CORE_TOOLS } = await import("../tools/index.js");
|
|
623
|
+
const tools = [...CORE_TOOLS] as import("../tools/index.js").DynamicToolInterface[];
|
|
624
|
+
|
|
625
|
+
s.start("Running evaluations across dataset (this may take a few minutes)...");
|
|
626
|
+
|
|
627
|
+
// We define a target function that the generic `evaluate` engine will call for each example
|
|
628
|
+
const agentTargetFn = async (inputs: Record<string, any>) => {
|
|
629
|
+
const runTracer = new SessionTracer();
|
|
630
|
+
const harness = new ExecutionHarness(model, tools, pipeline, runTracer);
|
|
631
|
+
|
|
632
|
+
// Initialize an empty sandbox just for this run
|
|
633
|
+
const sandboxManager = new SandboxManager({
|
|
634
|
+
template: config.sandboxTemplate,
|
|
635
|
+
apiKey: config.e2bApiKey,
|
|
636
|
+
openSandboxApiKey: config.openSandboxApiKey,
|
|
637
|
+
openSandboxDomain: config.openSandboxDomain,
|
|
638
|
+
});
|
|
639
|
+
await sandboxManager.create();
|
|
640
|
+
const { FileSync } = await import("../sandbox/sync.js");
|
|
641
|
+
const fileSync = new FileSync(process.cwd());
|
|
642
|
+
bindSandbox(sandboxManager, fileSync);
|
|
643
|
+
|
|
644
|
+
const { HumanMessage, AIMessage, ToolMessage } = await import("@langchain/core/messages");
|
|
645
|
+
|
|
646
|
+
let conversationHistory: any[] = [
|
|
647
|
+
new HumanMessage(inputs.instruction)
|
|
648
|
+
];
|
|
649
|
+
|
|
650
|
+
let finalOutput = "";
|
|
651
|
+
let turnCount = 0;
|
|
652
|
+
const MAX_TURNS = 15; // Anti-doom-loop for evals
|
|
653
|
+
|
|
654
|
+
try {
|
|
655
|
+
while (turnCount < MAX_TURNS) {
|
|
656
|
+
turnCount++;
|
|
657
|
+
const state = {
|
|
658
|
+
globalSystemInstructions: `You are Joone, a highly capable autonomous coding agent.
|
|
659
|
+
You run in a hybrid environment: you have read/write access to the host machine for code edits, but all code execution, testing, and dependency installation MUST happen in the isolated E2B sandbox for safety.
|
|
660
|
+
Always use 'bash' to run terminal commands. Never read or write outside the current project directory unless explicitly requested.`,
|
|
661
|
+
projectMemory: "Evaluation run.",
|
|
662
|
+
sessionContext: `Environment: ${process.platform}\nCWD: ${process.cwd()}`,
|
|
663
|
+
conversationHistory
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const response = await harness.step(state);
|
|
667
|
+
conversationHistory.push(response);
|
|
668
|
+
|
|
669
|
+
if (response.content && typeof response.content === "string") {
|
|
670
|
+
finalOutput += response.content + "\n";
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (!response.tool_calls || response.tool_calls.length === 0) {
|
|
674
|
+
break; // Task complete
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const toolResults = await harness.executeToolCalls(response, state);
|
|
678
|
+
conversationHistory.push(...toolResults);
|
|
679
|
+
}
|
|
680
|
+
} catch (e: any) {
|
|
681
|
+
await sandboxManager.destroy();
|
|
682
|
+
throw e; // LangSmith catches this for the error evaluation
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Gather metrics
|
|
686
|
+
const summary = runTracer.getSummary();
|
|
687
|
+
const metrics = {
|
|
688
|
+
promptTokens: summary.promptTokens,
|
|
689
|
+
completionTokens: summary.completionTokens,
|
|
690
|
+
cacheCreationTokens: summary.promptTokens * (summary.cacheHitRate), // LangChain doesn't expose explicit creation tokens directly yet, estimating for eval.
|
|
691
|
+
cacheReadTokens: summary.promptTokens * summary.cacheHitRate,
|
|
692
|
+
totalTokens: summary.totalTokens,
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// Check sandbox for uploaded/created files before ripping it down
|
|
696
|
+
let fileManifest: string[] = [];
|
|
697
|
+
try {
|
|
698
|
+
const result = await sandboxManager.exec(`find /workspace -type f`);
|
|
699
|
+
if (result.stdout) {
|
|
700
|
+
fileManifest = result.stdout.split('\n').map((l: string) => l.trim()).filter(Boolean);
|
|
701
|
+
}
|
|
702
|
+
} catch {
|
|
703
|
+
// Ignore, fallback to empty array
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
await sandboxManager.destroy();
|
|
707
|
+
|
|
708
|
+
return {
|
|
709
|
+
finalOutput,
|
|
710
|
+
metrics,
|
|
711
|
+
fileManifest,
|
|
712
|
+
};
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
const results = await evaluate(agentTargetFn, {
|
|
716
|
+
data: datasetName,
|
|
717
|
+
evaluators: [successEvaluator, cacheEfficiencyEvaluator, filePresenceEvaluator],
|
|
718
|
+
experimentPrefix: `joone-eval-${config.model.split('/').pop()}`,
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
s.stop("Evaluations completed!");
|
|
722
|
+
// LangSmith automatically prints the web URL to the interactive results dashboard here.
|
|
723
|
+
|
|
724
|
+
} catch (e: any) {
|
|
725
|
+
console.error(chalk.red(`\n ✗ Evaluation failed: ${e.message}\n`));
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
program.parse();
|