joonecli 0.1.0 → 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 (184) hide show
  1. package/README.md +12 -12
  2. package/dist/__tests__/optimizations.test.js.map +1 -1
  3. package/dist/__tests__/promptBuilder.test.js +14 -20
  4. package/dist/__tests__/promptBuilder.test.js.map +1 -1
  5. package/dist/agents/agentRegistry.d.ts +37 -0
  6. package/dist/agents/agentRegistry.js +58 -0
  7. package/dist/agents/agentRegistry.js.map +1 -0
  8. package/dist/agents/agentSpec.d.ts +54 -0
  9. package/dist/agents/agentSpec.js +9 -0
  10. package/dist/agents/agentSpec.js.map +1 -0
  11. package/dist/agents/builtinAgents.d.ts +20 -0
  12. package/{src/agents/builtinAgents.ts → dist/agents/builtinAgents.js} +84 -101
  13. package/dist/agents/builtinAgents.js.map +1 -0
  14. package/dist/cli/config.d.ts +4 -0
  15. package/dist/cli/config.js.map +1 -1
  16. package/dist/cli/index.js +29 -2
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/postinstall.d.ts +2 -0
  19. package/dist/cli/postinstall.js +25 -0
  20. package/dist/cli/postinstall.js.map +1 -0
  21. package/dist/commands/builtinCommands.d.ts +21 -0
  22. package/dist/commands/builtinCommands.js +241 -0
  23. package/dist/commands/builtinCommands.js.map +1 -0
  24. package/dist/commands/commandRegistry.d.ts +92 -0
  25. package/dist/commands/commandRegistry.js +128 -0
  26. package/dist/commands/commandRegistry.js.map +1 -0
  27. package/dist/core/agentLoop.d.ts +7 -2
  28. package/dist/core/agentLoop.js +35 -13
  29. package/dist/core/agentLoop.js.map +1 -1
  30. package/dist/core/autoSave.d.ts +41 -0
  31. package/dist/core/autoSave.js +69 -0
  32. package/dist/core/autoSave.js.map +1 -0
  33. package/dist/core/compactor.d.ts +66 -0
  34. package/dist/core/compactor.js +170 -0
  35. package/dist/core/compactor.js.map +1 -0
  36. package/dist/core/contextGuard.d.ts +38 -0
  37. package/dist/core/contextGuard.js +122 -0
  38. package/dist/core/contextGuard.js.map +1 -0
  39. package/dist/core/events.d.ts +45 -0
  40. package/dist/core/events.js +8 -0
  41. package/dist/core/events.js.map +1 -0
  42. package/dist/core/promptBuilder.d.ts +16 -1
  43. package/dist/core/promptBuilder.js +27 -14
  44. package/dist/core/promptBuilder.js.map +1 -1
  45. package/dist/core/sessionResumer.js +3 -3
  46. package/dist/core/sessionResumer.js.map +1 -1
  47. package/dist/core/sessionStore.js +3 -2
  48. package/dist/core/sessionStore.js.map +1 -1
  49. package/dist/core/subAgent.d.ts +56 -0
  50. package/dist/core/subAgent.js +240 -0
  51. package/dist/core/subAgent.js.map +1 -0
  52. package/dist/core/tokenCounter.d.ts +8 -1
  53. package/dist/core/tokenCounter.js +28 -0
  54. package/dist/core/tokenCounter.js.map +1 -1
  55. package/dist/debug_google.d.ts +1 -0
  56. package/dist/debug_google.js +23 -0
  57. package/dist/debug_google.js.map +1 -0
  58. package/dist/middleware/permission.js +1 -0
  59. package/dist/middleware/permission.js.map +1 -1
  60. package/dist/test_google.d.ts +1 -0
  61. package/dist/test_google.js +32 -89
  62. package/dist/test_google.js.map +1 -0
  63. package/dist/tools/browser.js +4 -1
  64. package/dist/tools/browser.js.map +1 -1
  65. package/dist/tools/index.d.ts +2 -1
  66. package/dist/tools/index.js +11 -3
  67. package/dist/tools/index.js.map +1 -1
  68. package/dist/tools/installHostDeps.d.ts +2 -0
  69. package/dist/tools/installHostDeps.js +37 -0
  70. package/dist/tools/installHostDeps.js.map +1 -0
  71. package/dist/tools/router.js +3 -0
  72. package/dist/tools/router.js.map +1 -1
  73. package/dist/tools/spawnAgent.d.ts +19 -0
  74. package/dist/tools/spawnAgent.js +132 -0
  75. package/dist/tools/spawnAgent.js.map +1 -0
  76. package/dist/tracing/sessionTracer.d.ts +1 -0
  77. package/dist/tracing/sessionTracer.js +4 -1
  78. package/dist/tracing/sessionTracer.js.map +1 -1
  79. package/dist/ui/App.js +94 -6
  80. package/dist/ui/App.js.map +1 -1
  81. package/dist/ui/components/ActionLog.d.ts +7 -0
  82. package/dist/ui/components/ActionLog.js +63 -0
  83. package/dist/ui/components/ActionLog.js.map +1 -0
  84. package/dist/ui/components/FileBrowser.d.ts +2 -0
  85. package/dist/ui/components/FileBrowser.js +41 -0
  86. package/dist/ui/components/FileBrowser.js.map +1 -0
  87. package/package.json +5 -6
  88. package/AGENTS.md +0 -56
  89. package/Handover.md +0 -115
  90. package/PROGRESS.md +0 -160
  91. package/docs/01_insights_and_patterns.md +0 -27
  92. package/docs/02_edge_cases_and_mitigations.md +0 -143
  93. package/docs/03_initial_implementation_plan.md +0 -66
  94. package/docs/04_tech_stack_proposal.md +0 -20
  95. package/docs/05_prd.md +0 -87
  96. package/docs/06_user_stories.md +0 -72
  97. package/docs/07_system_architecture.md +0 -138
  98. package/docs/08_roadmap.md +0 -200
  99. package/e2b/Dockerfile +0 -26
  100. package/src/__tests__/bootstrap.test.ts +0 -111
  101. package/src/__tests__/config.test.ts +0 -97
  102. package/src/__tests__/m55.test.ts +0 -238
  103. package/src/__tests__/middleware.test.ts +0 -219
  104. package/src/__tests__/modelFactory.test.ts +0 -63
  105. package/src/__tests__/optimizations.test.ts +0 -201
  106. package/src/__tests__/promptBuilder.test.ts +0 -141
  107. package/src/__tests__/sandbox.test.ts +0 -102
  108. package/src/__tests__/security.test.ts +0 -122
  109. package/src/__tests__/streaming.test.ts +0 -82
  110. package/src/__tests__/toolRouter.test.ts +0 -52
  111. package/src/__tests__/tools.test.ts +0 -146
  112. package/src/__tests__/tracing.test.ts +0 -196
  113. package/src/agents/agentRegistry.ts +0 -69
  114. package/src/agents/agentSpec.ts +0 -67
  115. package/src/cli/config.ts +0 -124
  116. package/src/cli/index.ts +0 -730
  117. package/src/cli/modelFactory.ts +0 -174
  118. package/src/cli/providers.ts +0 -107
  119. package/src/commands/builtinCommands.ts +0 -293
  120. package/src/commands/commandRegistry.ts +0 -194
  121. package/src/core/agentLoop.d.ts.map +0 -1
  122. package/src/core/agentLoop.ts +0 -312
  123. package/src/core/autoSave.ts +0 -95
  124. package/src/core/compactor.ts +0 -252
  125. package/src/core/contextGuard.ts +0 -129
  126. package/src/core/errors.ts +0 -202
  127. package/src/core/promptBuilder.d.ts.map +0 -1
  128. package/src/core/promptBuilder.ts +0 -139
  129. package/src/core/reasoningRouter.ts +0 -121
  130. package/src/core/retry.ts +0 -75
  131. package/src/core/sessionResumer.ts +0 -90
  132. package/src/core/sessionStore.ts +0 -215
  133. package/src/core/subAgent.ts +0 -339
  134. package/src/core/tokenCounter.ts +0 -64
  135. package/src/evals/dataset.ts +0 -67
  136. package/src/evals/evaluator.ts +0 -81
  137. package/src/hitl/bridge.ts +0 -160
  138. package/src/middleware/commandSanitizer.ts +0 -60
  139. package/src/middleware/loopDetection.ts +0 -63
  140. package/src/middleware/permission.ts +0 -72
  141. package/src/middleware/pipeline.ts +0 -75
  142. package/src/middleware/preCompletion.ts +0 -94
  143. package/src/middleware/types.ts +0 -45
  144. package/src/sandbox/bootstrap.ts +0 -121
  145. package/src/sandbox/manager.ts +0 -239
  146. package/src/sandbox/sync.ts +0 -157
  147. package/src/skills/loader.ts +0 -143
  148. package/src/skills/tools.ts +0 -99
  149. package/src/skills/types.ts +0 -13
  150. package/src/test_cache.ts +0 -72
  151. package/src/test_google.js +0 -40
  152. package/src/test_google.ts +0 -40
  153. package/src/tools/askUser.ts +0 -47
  154. package/src/tools/browser.ts +0 -137
  155. package/src/tools/index.d.ts.map +0 -1
  156. package/src/tools/index.ts +0 -237
  157. package/src/tools/registry.ts +0 -198
  158. package/src/tools/router.ts +0 -78
  159. package/src/tools/security.ts +0 -220
  160. package/src/tools/spawnAgent.ts +0 -158
  161. package/src/tools/webSearch.ts +0 -142
  162. package/src/tracing/analyzer.ts +0 -265
  163. package/src/tracing/langsmith.ts +0 -63
  164. package/src/tracing/sessionTracer.ts +0 -202
  165. package/src/tracing/types.ts +0 -49
  166. package/src/types/valyu.d.ts +0 -37
  167. package/src/ui/App.tsx +0 -404
  168. package/src/ui/components/HITLPrompt.tsx +0 -119
  169. package/src/ui/components/Header.tsx +0 -51
  170. package/src/ui/components/MessageBubble.tsx +0 -46
  171. package/src/ui/components/StatusBar.tsx +0 -138
  172. package/src/ui/components/StreamingText.tsx +0 -48
  173. package/src/ui/components/ToolCallPanel.tsx +0 -80
  174. package/tests/commands/commands.test.ts +0 -356
  175. package/tests/core/compactor.test.ts +0 -217
  176. package/tests/core/retryAndErrors.test.ts +0 -164
  177. package/tests/core/sessionResumer.test.ts +0 -95
  178. package/tests/core/sessionStore.test.ts +0 -84
  179. package/tests/core/stability.test.ts +0 -165
  180. package/tests/core/subAgent.test.ts +0 -238
  181. package/tests/hitl/hitlBridge.test.ts +0 -115
  182. package/tsconfig.json +0 -16
  183. package/vitest.config.ts +0 -10
  184. package/vitest.out +0 -48
@@ -1,52 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { ToolRouter, ToolTarget } from "../tools/router.js";
3
-
4
- describe("Tool Router", () => {
5
- let router: ToolRouter;
6
-
7
- beforeEach(() => {
8
- router = new ToolRouter();
9
- });
10
-
11
- // ─── Test #20: Routes write_file to host ───
12
-
13
- it("routes write_file to the host", () => {
14
- expect(router.getTarget("write_file")).toBe(ToolTarget.HOST);
15
- });
16
-
17
- // ─── Test #21: Routes read_file to host ───
18
-
19
- it("routes read_file to the host", () => {
20
- expect(router.getTarget("read_file")).toBe(ToolTarget.HOST);
21
- });
22
-
23
- // ─── Test #22: Routes bash to sandbox ───
24
-
25
- it("routes bash to the sandbox", () => {
26
- expect(router.getTarget("bash")).toBe(ToolTarget.SANDBOX);
27
- });
28
-
29
- // ─── Test #23: Routes run_tests to sandbox ───
30
-
31
- it("routes run_tests to the sandbox", () => {
32
- expect(router.getTarget("run_tests")).toBe(ToolTarget.SANDBOX);
33
- });
34
-
35
- // ─── Test #24: Routes install_deps to sandbox ───
36
-
37
- it("routes install_deps to the sandbox", () => {
38
- expect(router.getTarget("install_deps")).toBe(ToolTarget.SANDBOX);
39
- });
40
-
41
- // ─── Test #25: Routes search_tools to host ───
42
-
43
- it("routes search_tools to the host", () => {
44
- expect(router.getTarget("search_tools")).toBe(ToolTarget.HOST);
45
- });
46
-
47
- // ─── Test #26: Unknown tools default to sandbox (safe) ───
48
-
49
- it("defaults unknown tools to sandbox for safety", () => {
50
- expect(router.getTarget("unknown_tool")).toBe(ToolTarget.SANDBOX);
51
- });
52
- });
@@ -1,146 +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
- import { ReadFileTool, WriteFileTool } from "../tools/index.js";
6
-
7
- describe("ReadFileTool", () => {
8
- let tmpDir: string;
9
-
10
- beforeEach(() => {
11
- tmpDir = fs.mkdtempSync(path.join(process.cwd(), ".joone-tools-test-"));
12
- });
13
-
14
- afterEach(() => {
15
- fs.rmSync(tmpDir, { recursive: true, force: true });
16
- });
17
-
18
- // ─── Test #27: Reads a normal file ───
19
-
20
- it("reads a small file and returns its content", async () => {
21
- const filePath = path.join(tmpDir, "hello.txt");
22
- fs.writeFileSync(filePath, "Hello, world!", "utf-8");
23
-
24
- const result = await ReadFileTool.execute({ path: filePath });
25
-
26
- expect(result.content).toBe("Hello, world!");
27
- });
28
-
29
- // ─── Test #28: Returns error for non-existent file ───
30
-
31
- it("returns an error message for a non-existent file", async () => {
32
- const result = await ReadFileTool.execute({
33
- path: path.join(tmpDir, "nope.txt"),
34
- });
35
-
36
- expect(result.content).toMatch(/not found/i);
37
- });
38
-
39
- // ─── Test #29: File size guardrail rejects files over 512KB ───
40
-
41
- it("rejects files larger than 512KB with a descriptive error", async () => {
42
- const filePath = path.join(tmpDir, "big.txt");
43
- // Create a 600KB file
44
- const bigContent = "x".repeat(600 * 1024);
45
- fs.writeFileSync(filePath, bigContent, "utf-8");
46
-
47
- const result = await ReadFileTool.execute({ path: filePath });
48
-
49
- expect(result.content).toMatch(/too large/i);
50
- expect(result.content).toMatch(/512/);
51
- });
52
-
53
- // ─── Test #30: Line range slicing works ───
54
-
55
- it("returns only the requested line range", async () => {
56
- const filePath = path.join(tmpDir, "lines.txt");
57
- const lines = Array.from({ length: 20 }, (_, i) => `Line ${i + 1}`);
58
- fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
59
-
60
- const result = await ReadFileTool.execute({
61
- path: filePath,
62
- startLine: 5,
63
- endLine: 7,
64
- });
65
-
66
- expect(result.content).toContain("5: Line 5");
67
- expect(result.content).toContain("6: Line 6");
68
- expect(result.content).toContain("7: Line 7");
69
- expect(result.content).not.toContain("4: Line 4");
70
- expect(result.content).not.toContain("8: Line 8");
71
- });
72
-
73
- // ─── Test #31: Line count guardrail truncates long files ───
74
-
75
- it("truncates files with more than 2000 lines", async () => {
76
- const filePath = path.join(tmpDir, "long.txt");
77
- // Create a file with 2500 short lines (under 512KB)
78
- const lines = Array.from({ length: 2500 }, (_, i) => `L${i + 1}`);
79
- fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
80
-
81
- const result = await ReadFileTool.execute({ path: filePath });
82
-
83
- expect(result.content).toMatch(/truncated at 2000 lines/i);
84
- expect(result.content).toContain("1: L1");
85
- expect(result.content).toContain("2000: L2000");
86
- expect(result.content).not.toContain("2001: L2001");
87
- });
88
-
89
- // ─── Test #X: Security Guardrail Blocks Outside Files ───
90
-
91
- it("blocks reading files outside the project workspace", async () => {
92
- // Create a file in the OS tmp directory (guaranteed outside project workspace)
93
- const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), "joone-outside-"));
94
- const filePath = path.join(outsideDir, "secret.txt");
95
- fs.writeFileSync(filePath, "secret token", "utf-8");
96
-
97
- try {
98
- const result = await ReadFileTool.execute({ path: filePath });
99
- expect(result.isError).toBe(true);
100
- expect(result.content).toMatch(/Security Error: Access Denied/i);
101
- expect(result.content).toMatch(/outside the current project workspace/i);
102
- } finally {
103
- fs.rmSync(outsideDir, { recursive: true, force: true });
104
- }
105
- });
106
- });
107
-
108
- describe("WriteFileTool", () => {
109
- let tmpDir: string;
110
-
111
- beforeEach(() => {
112
- tmpDir = fs.mkdtempSync(path.join(process.cwd(), ".joone-write-test-"));
113
- });
114
-
115
- afterEach(() => {
116
- fs.rmSync(tmpDir, { recursive: true, force: true });
117
- });
118
-
119
- // ─── Test #32: Writes a file to disk ───
120
-
121
- it("writes content to a file and confirms", async () => {
122
- const filePath = path.join(tmpDir, "output.ts");
123
-
124
- const result = await WriteFileTool.execute({
125
- path: filePath,
126
- content: "const x = 42;",
127
- });
128
-
129
- expect(result.content).toMatch(/file written/i);
130
- expect(fs.readFileSync(filePath, "utf-8")).toBe("const x = 42;");
131
- });
132
-
133
- // ─── Test #33: Creates parent directories if needed ───
134
-
135
- it("creates parent directories if they do not exist", async () => {
136
- const filePath = path.join(tmpDir, "nested", "deep", "file.ts");
137
-
138
- const result = await WriteFileTool.execute({
139
- path: filePath,
140
- content: "export {}",
141
- });
142
-
143
- expect(result.content).toMatch(/file written/i);
144
- expect(fs.existsSync(filePath)).toBe(true);
145
- });
146
- });
@@ -1,196 +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
- import { SessionTracer } from "../tracing/sessionTracer.js";
6
- import {
7
- enableLangSmith,
8
- disableLangSmith,
9
- isLangSmithEnabled,
10
- } from "../tracing/langsmith.js";
11
- import { TraceAnalyzer } from "../tracing/analyzer.js";
12
- import type { SessionTrace } from "../tracing/types.js";
13
-
14
- // ═══════════════════════════════════════════════════════════════════════════════
15
- // 6a: SessionTracer
16
- // ═══════════════════════════════════════════════════════════════════════════════
17
-
18
- describe("SessionTracer", () => {
19
- // ─── Test #83: Records LLM calls and computes totals ───
20
-
21
- it("records LLM calls and computes token totals", () => {
22
- const tracer = new SessionTracer("test-session-1");
23
-
24
- tracer.recordLLMCall({ promptTokens: 500, completionTokens: 100, cached: false, duration: 800 });
25
- tracer.recordLLMCall({ promptTokens: 400, completionTokens: 150, cached: true, duration: 600 });
26
-
27
- const summary = tracer.getSummary();
28
-
29
- expect(summary.promptTokens).toBe(900);
30
- expect(summary.completionTokens).toBe(250);
31
- expect(summary.totalTokens).toBe(1150);
32
- expect(summary.turnCount).toBe(2);
33
- });
34
-
35
- // ─── Test #84: Records tool calls and counts them ───
36
-
37
- it("records tool calls and counts them", () => {
38
- const tracer = new SessionTracer("test-session-2");
39
-
40
- tracer.recordToolCall({ name: "bash", args: { command: "ls" }, duration: 50, success: true });
41
- tracer.recordToolCall({ name: "write_file", args: { path: "a.ts" }, duration: 30, success: true });
42
- tracer.recordToolCall({ name: "bash", args: { command: "npm test" }, duration: 200, success: false });
43
-
44
- const summary = tracer.getSummary();
45
-
46
- expect(summary.toolCallCount).toBe(3);
47
- });
48
-
49
- // ─── Test #85: Computes cache hit rate correctly ───
50
-
51
- it("computes cache hit rate correctly", () => {
52
- const tracer = new SessionTracer("test-session-3");
53
-
54
- // 3 calls: 2 cached, 1 not
55
- tracer.recordLLMCall({ promptTokens: 100, completionTokens: 50, cached: true, duration: 100 });
56
- tracer.recordLLMCall({ promptTokens: 100, completionTokens: 50, cached: true, duration: 100 });
57
- tracer.recordLLMCall({ promptTokens: 100, completionTokens: 50, cached: false, duration: 100 });
58
-
59
- const summary = tracer.getSummary();
60
-
61
- // 200 cached out of 300 total prompt tokens = 66.7%
62
- expect(summary.cacheHitRate).toBeCloseTo(0.667, 2);
63
- });
64
-
65
- // ─── Test #86: export() returns valid SessionTrace ───
66
-
67
- it("export() returns a valid SessionTrace", () => {
68
- const tracer = new SessionTracer("export-test");
69
-
70
- tracer.recordLLMCall({ promptTokens: 100, completionTokens: 50, cached: true, duration: 200 });
71
- tracer.recordError({ message: "Timeout", tool: "bash" });
72
-
73
- const trace = tracer.export();
74
-
75
- expect(trace.sessionId).toBe("export-test");
76
- expect(trace.startedAt).toBeGreaterThan(0);
77
- expect(trace.endedAt).toBeGreaterThanOrEqual(trace.startedAt);
78
- expect(trace.events).toHaveLength(2);
79
- expect(trace.summary.turnCount).toBe(1);
80
- expect(trace.summary.errorCount).toBe(1);
81
- });
82
- });
83
-
84
- // ═══════════════════════════════════════════════════════════════════════════════
85
- // 6b: LangSmith Integration
86
- // ═══════════════════════════════════════════════════════════════════════════════
87
-
88
- describe("LangSmith Integration", () => {
89
- afterEach(() => {
90
- disableLangSmith();
91
- });
92
-
93
- // ─── Test #87: enableLangSmith sets correct env vars ───
94
-
95
- it("sets the correct environment variables", () => {
96
- enableLangSmith({ apiKey: "test-key-123", project: "my-project" });
97
-
98
- expect(process.env.LANGCHAIN_TRACING_V2).toBe("true");
99
- expect(process.env.LANGCHAIN_API_KEY).toBe("test-key-123");
100
- expect(process.env.LANGCHAIN_PROJECT).toBe("my-project");
101
- expect(isLangSmithEnabled()).toBe(true);
102
- });
103
-
104
- // ─── Test #88: disableLangSmith clears env vars ───
105
-
106
- it("disableLangSmith clears the environment variables", () => {
107
- enableLangSmith({ apiKey: "test-key" });
108
- disableLangSmith();
109
-
110
- expect(process.env.LANGCHAIN_TRACING_V2).toBeUndefined();
111
- expect(isLangSmithEnabled()).toBe(false);
112
- });
113
- });
114
-
115
- // ═══════════════════════════════════════════════════════════════════════════════
116
- // 6c: TraceAnalyzer
117
- // ═══════════════════════════════════════════════════════════════════════════════
118
-
119
- describe("TraceAnalyzer", () => {
120
- const createTrace = (overrides?: Partial<SessionTrace>): SessionTrace => ({
121
- sessionId: "test",
122
- startedAt: Date.now() - 10000,
123
- endedAt: Date.now(),
124
- events: [],
125
- summary: {
126
- totalTokens: 1000,
127
- promptTokens: 700,
128
- completionTokens: 300,
129
- totalCost: 0.006,
130
- cacheHitRate: 0.8,
131
- toolCallCount: 5,
132
- errorCount: 0,
133
- totalDuration: 10000,
134
- turnCount: 5,
135
- },
136
- ...overrides,
137
- });
138
- // ─── Test #89: Detects loop patterns ───
139
-
140
- it("detects doom-loop patterns in tool calls", () => {
141
- const trace = createTrace({
142
- events: [
143
- { type: "tool_call", timestamp: 1, data: { name: "bash", args: { command: "ls" } } },
144
- { type: "tool_call", timestamp: 2, data: { name: "bash", args: { command: "ls" } } },
145
- { type: "tool_call", timestamp: 3, data: { name: "bash", args: { command: "ls" } } },
146
- ],
147
- });
148
-
149
- const analyzer = new TraceAnalyzer(trace);
150
- const report = analyzer.analyze();
151
-
152
- const loopIssues = report.issues.filter((i) => i.category === "loop");
153
- expect(loopIssues.length).toBeGreaterThan(0);
154
- expect(loopIssues[0].severity).toBe("critical");
155
- });
156
-
157
- // ─── Test #90: Detects cost hotspots ───
158
-
159
- it("flags turns consuming >20% of total tokens", () => {
160
- const trace = createTrace({
161
- summary: {
162
- ...createTrace().summary,
163
- totalTokens: 1000,
164
- },
165
- events: [
166
- { type: "llm_call", timestamp: 1, data: { promptTokens: 300, completionTokens: 100, cached: false } },
167
- { type: "llm_call", timestamp: 2, data: { promptTokens: 100, completionTokens: 50, cached: true } },
168
- ],
169
- });
170
-
171
- const analyzer = new TraceAnalyzer(trace);
172
- const report = analyzer.analyze();
173
-
174
- const costIssues = report.issues.filter((i) => i.category === "cost");
175
- expect(costIssues.length).toBeGreaterThan(0);
176
- });
177
-
178
- // ─── Test #91: Warns on low cache hit rate ───
179
-
180
- it("warns when cache hit rate is below 70%", () => {
181
- const trace = createTrace({
182
- summary: {
183
- ...createTrace().summary,
184
- cacheHitRate: 0.5,
185
- turnCount: 5,
186
- },
187
- });
188
-
189
- const analyzer = new TraceAnalyzer(trace);
190
- const report = analyzer.analyze();
191
-
192
- const cacheIssues = report.issues.filter((i) => i.category === "cache");
193
- expect(cacheIssues.length).toBe(1);
194
- expect(cacheIssues[0].message).toContain("50.0%");
195
- });
196
- });
@@ -1,69 +0,0 @@
1
- /**
2
- * Agent Registry
3
- *
4
- * Central registry for named sub-agents. The registry enables:
5
- * - Decoupled agent development (add agents without touching the main loop)
6
- * - Prompt injection (registry summary included in the main agent's system prompt)
7
- * - Lookup by name for the spawn_agent tool
8
- */
9
-
10
- import { AgentSpec } from "./agentSpec.js";
11
-
12
- export class AgentRegistry {
13
- private agents: Map<string, AgentSpec> = new Map();
14
-
15
- /**
16
- * Register a new agent spec. Overwrites if name already exists.
17
- */
18
- register(spec: AgentSpec): void {
19
- this.agents.set(spec.name, spec);
20
- }
21
-
22
- /**
23
- * Look up an agent by name.
24
- */
25
- get(name: string): AgentSpec | undefined {
26
- return this.agents.get(name);
27
- }
28
-
29
- /**
30
- * Returns all registered agent specs.
31
- */
32
- getAll(): AgentSpec[] {
33
- return Array.from(this.agents.values());
34
- }
35
-
36
- /**
37
- * Returns all registered agent names.
38
- */
39
- getNames(): string[] {
40
- return Array.from(this.agents.keys());
41
- }
42
-
43
- /**
44
- * Returns true if an agent with the given name exists.
45
- */
46
- has(name: string): boolean {
47
- return this.agents.has(name);
48
- }
49
-
50
- /**
51
- * Generates a summary of all available agents, formatted for injection
52
- * into the main agent's system prompt.
53
- */
54
- getSummary(): string {
55
- if (this.agents.size === 0) {
56
- return "No sub-agents are currently registered.";
57
- }
58
-
59
- const lines = ["Available sub-agents (use spawn_agent tool to invoke):\n"];
60
-
61
- for (const spec of this.agents.values()) {
62
- const tools = spec.tools ? ` [tools: ${spec.tools.join(", ")}]` : " [all tools]";
63
- const turns = spec.maxTurns ?? 10;
64
- lines.push(` • ${spec.name}: ${spec.description}${tools} (max ${turns} turns)`);
65
- }
66
-
67
- return lines.join("\n");
68
- }
69
- }
@@ -1,67 +0,0 @@
1
- /**
2
- * Agent Specification
3
- *
4
- * Defines the shape of a sub-agent: its identity, capabilities, constraints,
5
- * and tools. This enables decoupled agent development — new agents can be
6
- * added to the registry without modifying the main agent or harness.
7
- */
8
-
9
- /**
10
- * Describes a named sub-agent with a purpose-tuned configuration.
11
- */
12
- export interface AgentSpec {
13
- /** Unique name (e.g., "script_runner", "code_reviewer"). */
14
- name: string;
15
-
16
- /** Human-readable description included in the main agent's prompt. */
17
- description: string;
18
-
19
- /** Dedicated system prompt for this sub-agent. */
20
- systemPrompt: string;
21
-
22
- /** Restrict to specific tool names. If omitted, all main-agent tools are available. */
23
- tools?: string[];
24
-
25
- /** Maximum turns before the sub-agent is forcibly stopped (doom-loop protection). Default: 10. */
26
- maxTurns?: number;
27
-
28
- /** Override model for this agent (default: FAST_MODEL_DEFAULTS from same provider). */
29
- model?: string;
30
-
31
- /** Permission behavior for this agent. */
32
- permissionMode?: "auto" | "ask_all";
33
- }
34
-
35
- /**
36
- * Structured result returned by a sub-agent after completing (or failing) a task.
37
- * Only this result is injected into the main agent's history — the sub-agent's
38
- * full conversation is discarded to save context.
39
- */
40
- export interface SubAgentResult {
41
- /** The agent name from AgentSpec. */
42
- agentName: string;
43
-
44
- /** The original task description. */
45
- taskDescription: string;
46
-
47
- /** Outcome status. */
48
- outcome: "success" | "failure" | "partial";
49
-
50
- /** The final text output from the sub-agent. */
51
- result: string;
52
-
53
- /** Files created, modified, or deleted during the sub-task. */
54
- filesModified: string[];
55
-
56
- /** Total tool calls executed. */
57
- toolCallCount: number;
58
-
59
- /** Approximate token usage. */
60
- tokenUsage: { prompt: number; completion: number };
61
-
62
- /** Wall-clock duration in milliseconds. */
63
- duration: number;
64
-
65
- /** Number of turns the sub-agent ran. */
66
- turnsUsed: number;
67
- }
package/src/cli/config.ts DELETED
@@ -1,124 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
-
4
- /**
5
- * The shape of the Joone configuration file (~/.joone/config.json).
6
- */
7
- export interface JooneConfig {
8
- provider: string;
9
- model: string;
10
- apiKey?: string;
11
- maxTokens: number;
12
- temperature: number;
13
- streaming: boolean;
14
- /** E2B sandbox template. If set, uses a pre-baked template (prod). If unset, uses default + lazy install (dev). */
15
- sandboxTemplate?: string;
16
- /** E2B API key for sandbox provisioning. */
17
- e2bApiKey?: string;
18
- /** OpenSandbox API key for sandbox fallback provisioning. */
19
- openSandboxApiKey?: string;
20
- /** OpenSandbox API Domain for fallback. */
21
- openSandboxDomain?: string;
22
- /** Gemini API key for SecurityScanTool (Gemini CLI inside sandbox). */
23
- geminiApiKey?: string;
24
- /** Valyu API key for web search. */
25
- valyuApiKey?: string;
26
- /** LangSmith API key for tracing (optional). */
27
- langsmithApiKey?: string;
28
- /** LangSmith project name (optional, default: "joone"). */
29
- langsmithProject?: string;
30
- /** Tool permission mode: 'auto' (no prompts), 'ask_dangerous' (prompt for destructive tools), 'ask_all' (prompt for everything). */
31
- permissionMode?: "auto" | "ask_dangerous" | "ask_all";
32
- /** Override model for context compaction (default: auto-selected fast model from same provider). */
33
- compactModel?: string;
34
- /** Override model for sub-agents (default: auto-selected fast model from same provider). */
35
- subAgentModel?: string;
36
- }
37
-
38
- /**
39
- * Sensible defaults — Anthropic Claude as the default provider.
40
- */
41
- export const DEFAULT_CONFIG: JooneConfig = {
42
- provider: "anthropic",
43
- model: "claude-sonnet-4-20250514",
44
- maxTokens: 4096,
45
- temperature: 0,
46
- streaming: true,
47
- permissionMode: "auto",
48
- };
49
-
50
- /**
51
- * Maps provider names to their expected environment variable for the API key.
52
- */
53
- const PROVIDER_ENV_VARS: Record<string, string> = {
54
- anthropic: "ANTHROPIC_API_KEY",
55
- openai: "OPENAI_API_KEY",
56
- google: "GOOGLE_API_KEY",
57
- mistral: "MISTRAL_API_KEY",
58
- groq: "GROQ_API_KEY",
59
- deepseek: "DEEPSEEK_API_KEY",
60
- fireworks: "FIREWORKS_API_KEY",
61
- together: "TOGETHER_API_KEY",
62
- // Ollama (local) doesn't need an API key
63
- };
64
-
65
- /**
66
- * Loads the Joone config from the specified path.
67
- * Returns DEFAULT_CONFIG if the file does not exist.
68
- * Falls back to environment variables for API key if not set in config.
69
- */
70
- export function loadConfig(configPath: string): JooneConfig {
71
- let config: JooneConfig;
72
-
73
- if (!fs.existsSync(configPath)) {
74
- config = { ...DEFAULT_CONFIG };
75
- } else {
76
- try {
77
- const raw = fs.readFileSync(configPath, "utf-8");
78
- const parsed = JSON.parse(raw) as Partial<JooneConfig>;
79
- config = { ...DEFAULT_CONFIG, ...parsed };
80
- } catch (err) {
81
- console.warn(`Warning: Failed to parse config at ${configPath}. Using defaults.`);
82
- config = { ...DEFAULT_CONFIG };
83
- }
84
- }
85
- // Env var fallback: if apiKey is missing, check the provider's env var
86
- if (!config.apiKey) {
87
- const envVar = PROVIDER_ENV_VARS[config.provider];
88
- if (envVar && process.env[envVar]) {
89
- config.apiKey = process.env[envVar];
90
- }
91
- }
92
-
93
- return config;
94
- }
95
-
96
- /**
97
- * Saves the Joone config to the specified path.
98
- * Creates the parent directory if it doesn't exist.
99
- * Sets restrictive file permissions (owner-only read/write) for security.
100
- */
101
- export function saveConfig(configPath: string, config: JooneConfig): void {
102
- const dir = path.dirname(configPath);
103
- if (!fs.existsSync(dir)) {
104
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
105
- }
106
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), {
107
- encoding: "utf-8",
108
- mode: 0o600, // Owner read/write only (Linux/macOS)
109
- });
110
-
111
- // On Unix systems, enforce permissions even if file already existed
112
- try {
113
- fs.chmodSync(configPath, 0o600);
114
- } catch {
115
- // chmod may fail on Windows — ignore silently
116
- }
117
- }
118
-
119
- /**
120
- * Returns the expected environment variable name for a provider's API key.
121
- */
122
- export function getProviderEnvVar(provider: string): string | undefined {
123
- return PROVIDER_ENV_VARS[provider];
124
- }