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
package/src/cli/index.ts DELETED
@@ -1,742 +0,0 @@
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
- const startNow = await confirm({
283
- message: "Would you like to start Joone now?",
284
- initialValue: true
285
- });
286
-
287
- if (startNow && !isCancel(startNow)) {
288
- // Programmatically invoke the start command
289
- await program.parseAsync(["node", "joone", "start"]);
290
- } else {
291
- console.log(chalk.dim("\n Run `joone` anytime to start an agent session.\n"));
292
- }
293
- });
294
-
295
- // ─── joone provider ────────────────────────────────────────────────────────────
296
-
297
- const providerCmd = program.command("provider").description("Manage dynamic LLM provider packages");
298
-
299
- providerCmd
300
- .command("add <providerName>")
301
- .description("Install a provider package")
302
- .action(async (providerName) => {
303
- const s = spinner();
304
- s.start(`Installing ${providerName}...`);
305
- try {
306
- await installProvider(providerName);
307
- s.stop(`Successfully installed ${providerName}`);
308
- } catch (e: any) {
309
- s.stop(`Failed to install ${providerName}`);
310
- console.error(chalk.red(`\n ✗ ${e.message}\n`));
311
- process.exit(1);
312
- }
313
- });
314
-
315
- providerCmd
316
- .command("remove <providerName>")
317
- .description("Uninstall a provider package")
318
- .action(async (providerName) => {
319
- const s = spinner();
320
- s.start(`Uninstalling ${providerName}...`);
321
- try {
322
- await uninstallProvider(providerName);
323
- s.stop(`Successfully uninstalled ${providerName}`);
324
- } catch (e: any) {
325
- s.stop(`Failed to uninstall ${providerName}`);
326
- console.error(chalk.red(`\n ✗ ${e.message}\n`));
327
- process.exit(1);
328
- }
329
- });
330
-
331
- // ─── joone cleanup ─────────────────────────────────────────────────────────────
332
-
333
- program
334
- .command("cleanup")
335
- .description("Remove all Joone user data, settings, and dynamically installed providers")
336
- .action(async () => {
337
- const isConfirmed = await confirm({
338
- message: `Are you sure you want to delete all Joone data and settings in ${chalk.bold("~/.joone")}?`,
339
- });
340
-
341
- if (isCancel(isConfirmed) || !isConfirmed) {
342
- cancel("Cleanup aborted.");
343
- process.exit(0);
344
- }
345
-
346
- const jooneDir = path.join(os.homedir(), ".joone");
347
- const s = spinner();
348
- s.start("Deleting ~/.joone directory...");
349
-
350
- try {
351
- if (fs.existsSync(jooneDir)) {
352
- fs.rmSync(jooneDir, { recursive: true, force: true });
353
- s.stop("Cleanup complete.");
354
- console.log(
355
- chalk.green(`\n ✓ Successfully removed ${jooneDir}\n`) +
356
- chalk.dim(` To finish removing Joone entirely, uninstall it via your package manager:\n`) +
357
- chalk.dim(` e.g., \`npm uninstall -g joone\` or \`brew uninstall joone\`\n`)
358
- );
359
- } else {
360
- s.stop("Nothing to clean up.");
361
- console.log(chalk.dim(`\n Directory ${jooneDir} does not exist.\n`));
362
- }
363
- } catch (e: any) {
364
- s.stop("Cleanup failed.");
365
- console.error(chalk.red(`\n ✗ Error deleting directory: ${e.message}\n`));
366
- process.exit(1);
367
- }
368
- });
369
-
370
- // ─── joone (default) ───────────────────────────────────────────────────────────
371
-
372
- program
373
- .command("start", { isDefault: true })
374
- .description("Start a new Joone agent session")
375
- .option("--no-stream", "Disable streaming output")
376
- .option("-r, --resume <sessionId>", "Resume a previous session by ID")
377
- .action(async (options) => {
378
- let config = loadConfig(CONFIG_PATH);
379
-
380
- // Auto-trigger onboarding if no API key is configured
381
- if (!config.apiKey && config.provider !== "ollama") {
382
- console.log(
383
- chalk.yellow("\n ⚠ No configuration found.") +
384
- chalk.dim(" Let's set up Joone!\n")
385
- );
386
- config = await runOnboarding();
387
- }
388
-
389
- if (options.stream === false) {
390
- config.streaming = false;
391
- }
392
-
393
- const tracingEnabled = tryEnableLangSmithFromConfig(config);
394
-
395
- console.log(
396
- chalk.cyan("\n ◆ joone") +
397
- chalk.dim(" v0.1.0\n") +
398
- chalk.dim(" ├ Provider: ") + chalk.white(config.provider) + "\n" +
399
- chalk.dim(" ├ Model: ") + chalk.white(config.model) + "\n" +
400
- chalk.dim(" ├ Stream: ") + chalk.white(config.streaming ? "on" : "off") + "\n" +
401
- chalk.dim(" └ Tracing: ") + (tracingEnabled ? chalk.green("LangSmith") : chalk.dim("local only")) + "\n"
402
- );
403
-
404
- try {
405
- const model = await createModel(config);
406
-
407
- const pipeline = new MiddlewarePipeline();
408
- pipeline.use(new LoopDetectionMiddleware(3));
409
- pipeline.use(new CommandSanitizerMiddleware());
410
- pipeline.use(new PermissionMiddleware(config.permissionMode ?? "auto"));
411
- const tracer = new SessionTracer();
412
-
413
- const { bindSandbox } = await import("../tools/index.js");
414
-
415
- const s = spinner();
416
- s.start("Initializing Sandbox Environment...");
417
- const sandboxManager = new SandboxManager({
418
- template: config.sandboxTemplate,
419
- apiKey: config.e2bApiKey,
420
- openSandboxApiKey: config.openSandboxApiKey,
421
- openSandboxDomain: config.openSandboxDomain,
422
- });
423
- await sandboxManager.create();
424
-
425
- const { FileSync } = await import("../sandbox/sync.js");
426
- const fileSync = new FileSync(process.cwd());
427
- bindSandbox(sandboxManager, fileSync);
428
-
429
- // Sync user-level skills into the sandbox
430
- const { SkillLoader } = await import("../skills/loader.js");
431
- const skillLoader = new SkillLoader();
432
- const skillPaths = skillLoader.getDiscoveryPaths();
433
- await fileSync.syncSkillsToSandbox(sandboxManager, skillPaths);
434
-
435
- s.stop("Sandbox initialized");
436
-
437
- // For the CLI, we start by loading the CORE tools
438
- // Advanced tools (search, browser, etc.) will be dynamically loaded by the agent later
439
- // via the SearchToolsTool when the registry is fully integrated
440
- const { CORE_TOOLS } = await import("../tools/index.js");
441
- let tools = [...CORE_TOOLS, AskUserQuestionTool] as import("../tools/index.js").DynamicToolInterface[];
442
-
443
- // Initialize Sub-Agent Orchestration
444
- const agentRegistry = createDefaultAgentRegistry();
445
- const subAgentModelName = config.subAgentModel ?? config.model;
446
-
447
- // Use the config to determine sub-agent model, with FAST_MODEL_DEFAULTS fallback
448
- const { resolveFastModel } = await import("../core/compactor.js");
449
- const resolvedSubModel = resolveFastModel(config.provider, config.model, config.subAgentModel);
450
-
451
- const subAgentLlm = await createModel({ ...config, model: resolvedSubModel });
452
- const subAgentManager = new SubAgentManager(agentRegistry, tools, subAgentLlm);
453
- const spawnAgentTools = createSpawnAgentTools(subAgentManager, agentRegistry);
454
-
455
- tools = [...tools, ...spawnAgentTools];
456
-
457
- let initialState;
458
- let sessionId: string | undefined = undefined;
459
-
460
- if (options.resume) {
461
- const s = spinner();
462
- s.start(`Loading session ${chalk.bold(options.resume)}...`);
463
- const store = new SessionStore();
464
- try {
465
- const payload = await store.loadSession(options.resume);
466
- const resumer = new SessionResumer(process.cwd());
467
- initialState = resumer.prepareForResume(payload);
468
- sessionId = options.resume;
469
- s.stop(`Session resumed`);
470
- } catch (e: any) {
471
- s.stop(`Failed to load session`);
472
- console.error(chalk.red(`\n ✗ ${e.message}\n`));
473
- process.exit(1);
474
- }
475
- } else {
476
- initialState = {
477
- globalSystemInstructions: `You are Joone, a highly capable autonomous coding agent.
478
- 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.
479
- Always use 'bash' to run terminal commands. Never read or write outside the current project directory unless explicitly requested.
480
-
481
- IMPORTANT CAPABILITIES:
482
- - You have access to an 'ask_user_question' tool. Use it to ask the user for clarification, preferences, or approval before making significant changes.
483
- - 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.
484
- - You have access to Skills — reusable instruction sets for specialized tasks. Use 'search_skills' to discover them and 'load_skill' to activate their instructions.`,
485
- projectMemory: "No project context loaded yet.",
486
- sessionContext: `Environment: ${process.platform}\nCWD: ${process.cwd()}`,
487
- conversationHistory: []
488
- };
489
- }
490
-
491
- const harness = new ExecutionHarness(model, tools, pipeline, tracer, config.provider, config.model, sessionId);
492
-
493
- const { render } = await import("ink");
494
- const React = await import("react");
495
- const { App } = await import("../ui/App.js");
496
-
497
- const { waitUntilExit } = render(
498
- // @ts-ignore (App is imported dynamically, JSX resolution might complain but it works)
499
- React.createElement(App, {
500
- provider: config.provider,
501
- model: config.model,
502
- streaming: config.streaming,
503
- harness,
504
- initialState,
505
- maxTokens: config.maxTokens,
506
- })
507
- );
508
-
509
- await waitUntilExit();
510
-
511
- // Cleanup
512
- tracer.save();
513
- await sandboxManager.destroy();
514
- } catch (error: unknown) {
515
- if (error instanceof Error) {
516
- console.error(chalk.red(`\n ✗ ${error.stack}\n`));
517
- } else {
518
- console.error(chalk.red(`\n ✗ ${String(error)}\n`));
519
- }
520
- process.exit(1);
521
- }
522
- });
523
-
524
- // ─── joone sessions ────────────────────────────────────────────────────────────
525
-
526
- program
527
- .command("sessions")
528
- .description("List all persistent sessions available for resumption")
529
- .action(async () => {
530
- const store = new SessionStore();
531
- const sessions = await store.listSessions();
532
-
533
- if (sessions.length === 0) {
534
- console.log(chalk.dim("\n No saved sessions found.\n"));
535
- return;
536
- }
537
-
538
- console.log(chalk.bold("\n Recent Sessions:"));
539
- console.log(chalk.dim(" ─────────────────────────────────────────────────────────"));
540
-
541
- for (const session of sessions) {
542
- const date = new Date(session.lastSavedAt).toLocaleString();
543
- console.log(
544
- ` ${chalk.cyan(session.sessionId)} ` +
545
- chalk.dim(`[${date}] `) +
546
- chalk.grey(`(${session.model})\n`) +
547
- ` ↳ ${chalk.white(session.description)}\n`
548
- );
549
- }
550
- });
551
-
552
- // ─── joone analyze ─────────────────────────────────────────────────────────────
553
-
554
- program
555
- .command("analyze [sessionId]")
556
- .description("Analyze a session trace for performance insights")
557
- .action((sessionId) => {
558
- let tracePath;
559
- const tracesDir = path.join(os.homedir(), ".joone", "traces");
560
-
561
- if (sessionId) {
562
- tracePath = path.join(tracesDir, sessionId.endsWith(".json") ? sessionId : `${sessionId}.json`);
563
- } else {
564
- // Find the most recent trace
565
- if (!fs.existsSync(tracesDir)) {
566
- console.error(chalk.red("\n ✗ No traces directory found.\n"));
567
- process.exit(1);
568
- }
569
- const files = fs.readdirSync(tracesDir)
570
- .filter(f => f.endsWith(".json"))
571
- .map(f => ({ name: f, time: fs.statSync(path.join(tracesDir, f)).mtimeMs }))
572
- .sort((a, b) => b.time - a.time); // newest first
573
-
574
- if (files.length === 0) {
575
- console.error(chalk.red("\n ✗ No trace files found.\n"));
576
- process.exit(1);
577
- }
578
- tracePath = path.join(tracesDir, files[0].name);
579
- }
580
-
581
- if (!fs.existsSync(tracePath)) {
582
- console.error(chalk.red(`\n ✗ Trace file not found: ${tracePath}\n`));
583
- process.exit(1);
584
- }
585
-
586
- try {
587
- const trace = SessionTracer.load(tracePath);
588
- const analyzer = new TraceAnalyzer(trace);
589
- const report = analyzer.analyze();
590
- console.log(TraceAnalyzer.formatReport(report));
591
- } catch (e: any) {
592
- console.error(chalk.red(`\n ✗ Error analyzing trace: ${e.message}\n`));
593
- process.exit(1);
594
- }
595
- });
596
-
597
- // ─── joone eval ────────────────────────────────────────────────────────────────
598
-
599
- program
600
- .command("eval")
601
- .description("Run automated offline evaluation against the baseline LangSmith dataset")
602
- .action(async () => {
603
- let config = loadConfig(CONFIG_PATH);
604
-
605
- if (!config.langsmithApiKey) {
606
- console.error(chalk.red("\n ✗ LangSmith API key is missing. Run `joone config` to set it.\n"));
607
- process.exit(1);
608
- }
609
-
610
- tryEnableLangSmithFromConfig(config);
611
-
612
- console.log(chalk.cyan("\n ◆ joone evals") + chalk.dim(` (Model: ${config.model})\n`));
613
-
614
- try {
615
- const { evaluate } = await import("langsmith/evaluation");
616
- const { ensureBaselineDataset } = await import("../evals/dataset.js");
617
- const {
618
- successEvaluator,
619
- cacheEfficiencyEvaluator,
620
- filePresenceEvaluator
621
- } = await import("../evals/evaluator.js");
622
-
623
- const s = spinner();
624
- s.start("Verifying baseline dataset...");
625
- const datasetName = await ensureBaselineDataset();
626
- s.stop(`Dataset verified: ${chalk.white(datasetName)}`);
627
-
628
- const model = await createModel(config);
629
- const pipeline = new MiddlewarePipeline();
630
- pipeline.use(new LoopDetectionMiddleware(3));
631
- pipeline.use(new CommandSanitizerMiddleware());
632
- const tracer = new SessionTracer();
633
-
634
- const { bindSandbox, CORE_TOOLS } = await import("../tools/index.js");
635
- const tools = [...CORE_TOOLS] as import("../tools/index.js").DynamicToolInterface[];
636
-
637
- s.start("Running evaluations across dataset (this may take a few minutes)...");
638
-
639
- // We define a target function that the generic `evaluate` engine will call for each example
640
- const agentTargetFn = async (inputs: Record<string, any>) => {
641
- const runTracer = new SessionTracer();
642
- const harness = new ExecutionHarness(model, tools, pipeline, runTracer);
643
-
644
- // Initialize an empty sandbox just for this run
645
- const sandboxManager = new SandboxManager({
646
- template: config.sandboxTemplate,
647
- apiKey: config.e2bApiKey,
648
- openSandboxApiKey: config.openSandboxApiKey,
649
- openSandboxDomain: config.openSandboxDomain,
650
- });
651
- await sandboxManager.create();
652
- const { FileSync } = await import("../sandbox/sync.js");
653
- const fileSync = new FileSync(process.cwd());
654
- bindSandbox(sandboxManager, fileSync);
655
-
656
- const { HumanMessage, AIMessage, ToolMessage } = await import("@langchain/core/messages");
657
-
658
- let conversationHistory: any[] = [
659
- new HumanMessage(inputs.instruction)
660
- ];
661
-
662
- let finalOutput = "";
663
- let turnCount = 0;
664
- const MAX_TURNS = 15; // Anti-doom-loop for evals
665
-
666
- try {
667
- while (turnCount < MAX_TURNS) {
668
- turnCount++;
669
- const state = {
670
- globalSystemInstructions: `You are Joone, a highly capable autonomous coding agent.
671
- 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.
672
- Always use 'bash' to run terminal commands. Never read or write outside the current project directory unless explicitly requested.`,
673
- projectMemory: "Evaluation run.",
674
- sessionContext: `Environment: ${process.platform}\nCWD: ${process.cwd()}`,
675
- conversationHistory
676
- };
677
-
678
- const response = await harness.step(state);
679
- conversationHistory.push(response);
680
-
681
- if (response.content && typeof response.content === "string") {
682
- finalOutput += response.content + "\n";
683
- }
684
-
685
- if (!response.tool_calls || response.tool_calls.length === 0) {
686
- break; // Task complete
687
- }
688
-
689
- const toolResults = await harness.executeToolCalls(response, state);
690
- conversationHistory.push(...toolResults);
691
- }
692
- } catch (e: any) {
693
- await sandboxManager.destroy();
694
- throw e; // LangSmith catches this for the error evaluation
695
- }
696
-
697
- // Gather metrics
698
- const summary = runTracer.getSummary();
699
- const metrics = {
700
- promptTokens: summary.promptTokens,
701
- completionTokens: summary.completionTokens,
702
- cacheCreationTokens: summary.promptTokens * (summary.cacheHitRate), // LangChain doesn't expose explicit creation tokens directly yet, estimating for eval.
703
- cacheReadTokens: summary.promptTokens * summary.cacheHitRate,
704
- totalTokens: summary.totalTokens,
705
- };
706
-
707
- // Check sandbox for uploaded/created files before ripping it down
708
- let fileManifest: string[] = [];
709
- try {
710
- const result = await sandboxManager.exec(`find /workspace -type f`);
711
- if (result.stdout) {
712
- fileManifest = result.stdout.split('\n').map((l: string) => l.trim()).filter(Boolean);
713
- }
714
- } catch {
715
- // Ignore, fallback to empty array
716
- }
717
-
718
- await sandboxManager.destroy();
719
-
720
- return {
721
- finalOutput,
722
- metrics,
723
- fileManifest,
724
- };
725
- };
726
-
727
- const results = await evaluate(agentTargetFn, {
728
- data: datasetName,
729
- evaluators: [successEvaluator, cacheEfficiencyEvaluator, filePresenceEvaluator],
730
- experimentPrefix: `joone-eval-${config.model.split('/').pop()}`,
731
- });
732
-
733
- s.stop("Evaluations completed!");
734
- // LangSmith automatically prints the web URL to the interactive results dashboard here.
735
-
736
- } catch (e: any) {
737
- console.error(chalk.red(`\n ✗ Evaluation failed: ${e.message}\n`));
738
- process.exit(1);
739
- }
740
- });
741
-
742
- program.parse();