joonecli 0.1.1 → 0.2.1

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 (235) hide show
  1. package/dist/__tests__/config.test.js +1 -0
  2. package/dist/__tests__/config.test.js.map +1 -1
  3. package/dist/__tests__/installHostDeps.test.js +45 -0
  4. package/dist/__tests__/installHostDeps.test.js.map +1 -0
  5. package/dist/__tests__/whitelistedBackend.test.js +18 -0
  6. package/dist/__tests__/whitelistedBackend.test.js.map +1 -0
  7. package/dist/cli/config.d.ts +2 -0
  8. package/dist/cli/config.js +1 -0
  9. package/dist/cli/config.js.map +1 -1
  10. package/dist/cli/index.js +84 -97
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/commands/builtinCommands.js +6 -6
  13. package/dist/commands/builtinCommands.js.map +1 -1
  14. package/dist/commands/commandRegistry.d.ts +3 -1
  15. package/dist/commands/commandRegistry.js.map +1 -1
  16. package/dist/core/agentLoop.d.ts +11 -28
  17. package/dist/core/agentLoop.js +68 -229
  18. package/dist/core/agentLoop.js.map +1 -1
  19. package/dist/core/compactor.js +2 -2
  20. package/dist/core/compactor.js.map +1 -1
  21. package/dist/core/contextGuard.d.ts +5 -0
  22. package/dist/core/contextGuard.js +30 -3
  23. package/dist/core/contextGuard.js.map +1 -1
  24. package/dist/core/events.d.ts +45 -0
  25. package/dist/core/events.js +8 -0
  26. package/dist/core/events.js.map +1 -0
  27. package/dist/core/promptBuilder.js.map +1 -1
  28. package/dist/core/sessionStore.js +3 -2
  29. package/dist/core/sessionStore.js.map +1 -1
  30. package/dist/core/tokenCounter.d.ts +8 -1
  31. package/dist/core/tokenCounter.js +28 -0
  32. package/dist/core/tokenCounter.js.map +1 -1
  33. package/dist/hitl/bridge.js +1 -27
  34. package/dist/hitl/bridge.js.map +1 -1
  35. package/dist/middleware/loopDetection.d.ts +7 -23
  36. package/dist/middleware/loopDetection.js +38 -42
  37. package/dist/middleware/loopDetection.js.map +1 -1
  38. package/dist/sandbox/whitelistedBackend.d.ts +5 -0
  39. package/dist/sandbox/whitelistedBackend.js +27 -0
  40. package/dist/sandbox/whitelistedBackend.js.map +1 -0
  41. package/dist/tools/askUser.d.ts +12 -3
  42. package/dist/tools/askUser.js +16 -28
  43. package/dist/tools/askUser.js.map +1 -1
  44. package/dist/tools/bashTool.d.ts +11 -0
  45. package/dist/tools/bashTool.js +51 -0
  46. package/dist/tools/bashTool.js.map +1 -0
  47. package/dist/tools/index.d.ts +15 -27
  48. package/dist/tools/index.js +9 -181
  49. package/dist/tools/index.js.map +1 -1
  50. package/dist/tools/installHostDeps.d.ts +8 -0
  51. package/dist/tools/installHostDeps.js +44 -0
  52. package/dist/tools/installHostDeps.js.map +1 -0
  53. package/dist/tracing/sessionTracer.d.ts +1 -0
  54. package/dist/tracing/sessionTracer.js +4 -1
  55. package/dist/tracing/sessionTracer.js.map +1 -1
  56. package/dist/ui/App.js +116 -55
  57. package/dist/ui/App.js.map +1 -1
  58. package/dist/ui/components/ActionLog.d.ts +7 -0
  59. package/dist/ui/components/ActionLog.js +63 -0
  60. package/dist/ui/components/ActionLog.js.map +1 -0
  61. package/dist/ui/components/FileBrowser.d.ts +2 -0
  62. package/dist/ui/components/FileBrowser.js +41 -0
  63. package/dist/ui/components/FileBrowser.js.map +1 -0
  64. package/dist/ui/components/MessageBubble.js +1 -1
  65. package/dist/ui/components/MessageBubble.js.map +1 -1
  66. package/package.json +8 -5
  67. package/AGENTS.md +0 -56
  68. package/Handover.md +0 -115
  69. package/PROGRESS.md +0 -160
  70. package/dist/__tests__/m55.test.js +0 -160
  71. package/dist/__tests__/m55.test.js.map +0 -1
  72. package/dist/__tests__/middleware.test.js +0 -169
  73. package/dist/__tests__/middleware.test.js.map +0 -1
  74. package/dist/__tests__/optimizations.test.d.ts +0 -1
  75. package/dist/__tests__/optimizations.test.js +0 -136
  76. package/dist/__tests__/optimizations.test.js.map +0 -1
  77. package/dist/__tests__/security.test.d.ts +0 -1
  78. package/dist/__tests__/security.test.js +0 -86
  79. package/dist/__tests__/security.test.js.map +0 -1
  80. package/dist/__tests__/streaming.test.d.ts +0 -1
  81. package/dist/__tests__/streaming.test.js +0 -71
  82. package/dist/__tests__/streaming.test.js.map +0 -1
  83. package/dist/__tests__/toolRouter.test.d.ts +0 -1
  84. package/dist/__tests__/toolRouter.test.js +0 -37
  85. package/dist/__tests__/toolRouter.test.js.map +0 -1
  86. package/dist/__tests__/tools.test.d.ts +0 -1
  87. package/dist/__tests__/tools.test.js +0 -112
  88. package/dist/__tests__/tools.test.js.map +0 -1
  89. package/dist/core/subAgent.d.ts +0 -56
  90. package/dist/core/subAgent.js +0 -240
  91. package/dist/core/subAgent.js.map +0 -1
  92. package/dist/debug_google.d.ts +0 -1
  93. package/dist/debug_google.js +0 -23
  94. package/dist/debug_google.js.map +0 -1
  95. package/dist/middleware/commandSanitizer.d.ts +0 -18
  96. package/dist/middleware/commandSanitizer.js +0 -50
  97. package/dist/middleware/commandSanitizer.js.map +0 -1
  98. package/dist/middleware/permission.d.ts +0 -17
  99. package/dist/middleware/permission.js +0 -59
  100. package/dist/middleware/permission.js.map +0 -1
  101. package/dist/middleware/pipeline.d.ts +0 -31
  102. package/dist/middleware/pipeline.js +0 -62
  103. package/dist/middleware/pipeline.js.map +0 -1
  104. package/dist/middleware/preCompletion.d.ts +0 -29
  105. package/dist/middleware/preCompletion.js +0 -82
  106. package/dist/middleware/preCompletion.js.map +0 -1
  107. package/dist/middleware/types.d.ts +0 -40
  108. package/dist/middleware/types.js +0 -8
  109. package/dist/middleware/types.js.map +0 -1
  110. package/dist/skills/loader.d.ts +0 -55
  111. package/dist/skills/loader.js +0 -132
  112. package/dist/skills/loader.js.map +0 -1
  113. package/dist/skills/tools.d.ts +0 -5
  114. package/dist/skills/tools.js +0 -78
  115. package/dist/skills/tools.js.map +0 -1
  116. package/dist/test_cache.d.ts +0 -1
  117. package/dist/test_cache.js +0 -55
  118. package/dist/test_cache.js.map +0 -1
  119. package/dist/test_google.d.ts +0 -1
  120. package/dist/test_google.js +0 -36
  121. package/dist/test_google.js.map +0 -1
  122. package/dist/tools/browser.d.ts +0 -19
  123. package/dist/tools/browser.js +0 -111
  124. package/dist/tools/browser.js.map +0 -1
  125. package/dist/tools/registry.d.ts +0 -31
  126. package/dist/tools/registry.js +0 -168
  127. package/dist/tools/registry.js.map +0 -1
  128. package/dist/tools/router.d.ts +0 -34
  129. package/dist/tools/router.js +0 -75
  130. package/dist/tools/router.js.map +0 -1
  131. package/dist/tools/security.d.ts +0 -28
  132. package/dist/tools/security.js +0 -183
  133. package/dist/tools/security.js.map +0 -1
  134. package/dist/tools/spawnAgent.d.ts +0 -19
  135. package/dist/tools/spawnAgent.js +0 -130
  136. package/dist/tools/spawnAgent.js.map +0 -1
  137. package/dist/tools/webSearch.d.ts +0 -6
  138. package/dist/tools/webSearch.js +0 -120
  139. package/dist/tools/webSearch.js.map +0 -1
  140. package/docs/01_insights_and_patterns.md +0 -27
  141. package/docs/02_edge_cases_and_mitigations.md +0 -143
  142. package/docs/03_initial_implementation_plan.md +0 -66
  143. package/docs/04_tech_stack_proposal.md +0 -20
  144. package/docs/05_prd.md +0 -87
  145. package/docs/06_user_stories.md +0 -72
  146. package/docs/07_system_architecture.md +0 -138
  147. package/docs/08_roadmap.md +0 -200
  148. package/e2b/Dockerfile +0 -26
  149. package/src/__tests__/bootstrap.test.ts +0 -111
  150. package/src/__tests__/config.test.ts +0 -97
  151. package/src/__tests__/m55.test.ts +0 -238
  152. package/src/__tests__/middleware.test.ts +0 -219
  153. package/src/__tests__/modelFactory.test.ts +0 -63
  154. package/src/__tests__/optimizations.test.ts +0 -201
  155. package/src/__tests__/promptBuilder.test.ts +0 -141
  156. package/src/__tests__/sandbox.test.ts +0 -102
  157. package/src/__tests__/security.test.ts +0 -122
  158. package/src/__tests__/streaming.test.ts +0 -82
  159. package/src/__tests__/toolRouter.test.ts +0 -52
  160. package/src/__tests__/tools.test.ts +0 -146
  161. package/src/__tests__/tracing.test.ts +0 -196
  162. package/src/agents/agentRegistry.ts +0 -69
  163. package/src/agents/agentSpec.ts +0 -67
  164. package/src/agents/builtinAgents.ts +0 -142
  165. package/src/cli/config.ts +0 -124
  166. package/src/cli/index.ts +0 -742
  167. package/src/cli/modelFactory.ts +0 -174
  168. package/src/cli/postinstall.ts +0 -28
  169. package/src/cli/providers.ts +0 -107
  170. package/src/commands/builtinCommands.ts +0 -293
  171. package/src/commands/commandRegistry.ts +0 -194
  172. package/src/core/agentLoop.d.ts.map +0 -1
  173. package/src/core/agentLoop.ts +0 -312
  174. package/src/core/autoSave.ts +0 -95
  175. package/src/core/compactor.ts +0 -252
  176. package/src/core/contextGuard.ts +0 -129
  177. package/src/core/errors.ts +0 -202
  178. package/src/core/promptBuilder.d.ts.map +0 -1
  179. package/src/core/promptBuilder.ts +0 -139
  180. package/src/core/reasoningRouter.ts +0 -121
  181. package/src/core/retry.ts +0 -75
  182. package/src/core/sessionResumer.ts +0 -90
  183. package/src/core/sessionStore.ts +0 -216
  184. package/src/core/subAgent.ts +0 -339
  185. package/src/core/tokenCounter.ts +0 -64
  186. package/src/evals/dataset.ts +0 -67
  187. package/src/evals/evaluator.ts +0 -81
  188. package/src/hitl/bridge.ts +0 -160
  189. package/src/middleware/commandSanitizer.ts +0 -60
  190. package/src/middleware/loopDetection.ts +0 -63
  191. package/src/middleware/permission.ts +0 -72
  192. package/src/middleware/pipeline.ts +0 -75
  193. package/src/middleware/preCompletion.ts +0 -94
  194. package/src/middleware/types.ts +0 -45
  195. package/src/sandbox/bootstrap.ts +0 -121
  196. package/src/sandbox/manager.ts +0 -239
  197. package/src/sandbox/sync.ts +0 -157
  198. package/src/skills/loader.ts +0 -143
  199. package/src/skills/tools.ts +0 -99
  200. package/src/skills/types.ts +0 -13
  201. package/src/test_cache.ts +0 -72
  202. package/src/tools/askUser.ts +0 -47
  203. package/src/tools/browser.ts +0 -137
  204. package/src/tools/index.d.ts.map +0 -1
  205. package/src/tools/index.ts +0 -237
  206. package/src/tools/registry.ts +0 -198
  207. package/src/tools/router.ts +0 -78
  208. package/src/tools/security.ts +0 -220
  209. package/src/tools/spawnAgent.ts +0 -158
  210. package/src/tools/webSearch.ts +0 -142
  211. package/src/tracing/analyzer.ts +0 -265
  212. package/src/tracing/langsmith.ts +0 -63
  213. package/src/tracing/sessionTracer.ts +0 -202
  214. package/src/tracing/types.ts +0 -49
  215. package/src/types/valyu.d.ts +0 -37
  216. package/src/ui/App.tsx +0 -404
  217. package/src/ui/components/HITLPrompt.tsx +0 -119
  218. package/src/ui/components/Header.tsx +0 -51
  219. package/src/ui/components/MessageBubble.tsx +0 -46
  220. package/src/ui/components/StatusBar.tsx +0 -138
  221. package/src/ui/components/StreamingText.tsx +0 -48
  222. package/src/ui/components/ToolCallPanel.tsx +0 -80
  223. package/tests/commands/commands.test.ts +0 -356
  224. package/tests/core/compactor.test.ts +0 -217
  225. package/tests/core/retryAndErrors.test.ts +0 -164
  226. package/tests/core/sessionResumer.test.ts +0 -95
  227. package/tests/core/sessionStore.test.ts +0 -84
  228. package/tests/core/stability.test.ts +0 -165
  229. package/tests/core/subAgent.test.ts +0 -238
  230. package/tests/hitl/hitlBridge.test.ts +0 -115
  231. package/tsconfig.json +0 -16
  232. package/vitest.config.ts +0 -10
  233. package/vitest.out +0 -48
  234. /package/dist/__tests__/{m55.test.d.ts → installHostDeps.test.d.ts} +0 -0
  235. /package/dist/__tests__/{middleware.test.d.ts → whitelistedBackend.test.d.ts} +0 -0
@@ -1,138 +0,0 @@
1
- import React from "react";
2
- import { Box, Text } from "ink";
3
-
4
- interface StatusBarProps {
5
- /** Current tokens used in the context window (estimated). */
6
- contextTokens?: number;
7
- /** Maximum context window size for the model. */
8
- maxContextTokens?: number;
9
- /** Total tokens consumed across all LLM calls (prompt + completion). */
10
- totalTokens?: number;
11
- /** Cache hit rate (0–1). */
12
- cacheHitRate?: number;
13
- /** Elapsed session time. */
14
- elapsed?: string;
15
- /** Total tool calls executed. */
16
- toolCalls?: number;
17
- /** Number of LLM turns. */
18
- turns?: number;
19
- /** Estimated cost in USD. */
20
- cost?: number;
21
- }
22
-
23
- /**
24
- * Renders a visual capacity bar for the context window.
25
- *
26
- * Example: ▓▓▓▓▓▓▓▓░░░░░░░ 52%
27
- */
28
- function ContextBar({
29
- used,
30
- max,
31
- width = 16,
32
- }: {
33
- used: number;
34
- max: number;
35
- width?: number;
36
- }) {
37
- const ratio = max > 0 ? Math.min(used / max, 1) : 0;
38
- const filled = Math.round(ratio * width);
39
- const empty = width - filled;
40
- const pct = Math.round(ratio * 100);
41
-
42
- // Color: green < 60%, yellow 60-80%, red > 80%
43
- const barColor = ratio < 0.6 ? "green" : ratio < 0.8 ? "yellow" : "red";
44
-
45
- return (
46
- <Text>
47
- <Text color={barColor}>{"▓".repeat(filled)}</Text>
48
- <Text dimColor>{"░".repeat(empty)}</Text>
49
- <Text> </Text>
50
- <Text color={barColor}>{pct}%</Text>
51
- </Text>
52
- );
53
- }
54
-
55
- /**
56
- * Formats a token count for display (e.g., 3241 → "3.2K", 128000 → "128K").
57
- */
58
- function formatTokens(n: number): string {
59
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
60
- if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
61
- return `${n}`;
62
- }
63
-
64
- export const StatusBar: React.FC<StatusBarProps> = ({
65
- contextTokens = 0,
66
- maxContextTokens = 200_000,
67
- totalTokens = 0,
68
- cacheHitRate,
69
- elapsed = "0s",
70
- toolCalls = 0,
71
- turns = 0,
72
- cost,
73
- }) => {
74
- return (
75
- <Box
76
- flexDirection="column"
77
- borderStyle="single"
78
- borderColor="gray"
79
- paddingX={1}
80
- >
81
- {/* Row 1: Context Window */}
82
- <Box justifyContent="space-between">
83
- <Box>
84
- <Text dimColor>ctx </Text>
85
- <ContextBar used={contextTokens} max={maxContextTokens} />
86
- <Text dimColor>
87
- {" "}
88
- {formatTokens(contextTokens)}/{formatTokens(maxContextTokens)}
89
- </Text>
90
- </Box>
91
- {cacheHitRate !== undefined && (
92
- <Text>
93
- <Text dimColor>cache </Text>
94
- <Text
95
- color={
96
- cacheHitRate > 0.8
97
- ? "green"
98
- : cacheHitRate > 0.5
99
- ? "yellow"
100
- : "red"
101
- }
102
- >
103
- {(cacheHitRate * 100).toFixed(0)}%
104
- </Text>
105
- </Text>
106
- )}
107
- </Box>
108
-
109
- {/* Row 2: Session Metrics */}
110
- <Box justifyContent="space-between">
111
- <Text>
112
- <Text dimColor>tokens </Text>
113
- <Text color="white">{formatTokens(totalTokens)}</Text>
114
- </Text>
115
- <Text>
116
- <Text dimColor>turns </Text>
117
- <Text color="white">{turns}</Text>
118
- </Text>
119
- <Text>
120
- <Text dimColor>tools </Text>
121
- <Text color="white">{toolCalls}</Text>
122
- </Text>
123
- {cost !== undefined && cost > 0 && (
124
- <Text>
125
- <Text dimColor>cost </Text>
126
- <Text color="white">
127
- ${cost < 0.01 ? cost.toFixed(4) : cost.toFixed(2)}
128
- </Text>
129
- </Text>
130
- )}
131
- <Text>
132
- <Text dimColor>elapsed </Text>
133
- <Text color="white">{elapsed}</Text>
134
- </Text>
135
- </Box>
136
- </Box>
137
- );
138
- };
@@ -1,48 +0,0 @@
1
- import React, { useState, useEffect } from "react";
2
- import { Text } from "ink";
3
-
4
- interface StreamingTextProps {
5
- /** Array of tokens that have been received so far. */
6
- tokens: string[];
7
- /** Whether the stream is still active. */
8
- isStreaming: boolean;
9
- }
10
-
11
- /**
12
- * StreamingText renders incoming tokens with a blinking cursor
13
- * while the stream is active. Once streaming stops, the cursor disappears.
14
- */
15
- export const StreamingText: React.FC<StreamingTextProps> = ({
16
- tokens,
17
- isStreaming,
18
- }) => {
19
- const [cursorVisible, setCursorVisible] = useState(true);
20
-
21
- useEffect(() => {
22
- if (!isStreaming) {
23
- setCursorVisible(false);
24
- return;
25
- }
26
-
27
- setCursorVisible(true);
28
-
29
- const interval = setInterval(() => {
30
- setCursorVisible((v) => !v);
31
- }, 500);
32
-
33
- return () => clearInterval(interval);
34
- }, [isStreaming]);
35
-
36
- const fullText = tokens.join("");
37
-
38
- return (
39
- <Text>
40
- <Text color="white">{fullText}</Text>
41
- {isStreaming && (
42
- <Text color="cyan" bold>
43
- {cursorVisible ? "▊" : " "}
44
- </Text>
45
- )}
46
- </Text>
47
- );
48
- };
@@ -1,80 +0,0 @@
1
- import React from "react";
2
- import { Box, Text } from "ink";
3
- import Spinner from "ink-spinner";
4
-
5
- export type ToolCallStatus = "running" | "success" | "error";
6
-
7
- interface ToolCallPanelProps {
8
- toolName: string;
9
- args?: Record<string, unknown>;
10
- status: ToolCallStatus;
11
- result?: string;
12
- }
13
-
14
- /**
15
- * Styled panel that appears when the agent invokes a tool.
16
- * Shows the tool name, arguments, a spinner while executing,
17
- * and the result once complete.
18
- */
19
- export const ToolCallPanel: React.FC<ToolCallPanelProps> = ({
20
- toolName,
21
- args,
22
- status,
23
- result,
24
- }) => {
25
- const borderColor =
26
- status === "running" ? "yellow" : status === "success" ? "green" : "red";
27
-
28
- const statusIcon =
29
- status === "running" ? (
30
- <Text color="yellow">
31
- <Spinner type="dots" />
32
- </Text>
33
- ) : status === "success" ? (
34
- <Text color="green">✓</Text>
35
- ) : (
36
- <Text color="red">✗</Text>
37
- );
38
-
39
- return (
40
- <Box
41
- flexDirection="column"
42
- borderStyle="round"
43
- borderColor={borderColor}
44
- paddingX={1}
45
- marginY={0}
46
- >
47
- <Box gap={1}>
48
- {statusIcon}
49
- <Text bold color={borderColor}>
50
- {toolName}
51
- </Text>
52
- </Box>
53
-
54
- {args && Object.keys(args).length > 0 && (
55
- <Box marginLeft={2} flexDirection="column">
56
- {Object.entries(args).map(([key, value]) => (
57
- <Text key={key}>
58
- <Text dimColor>{key}:</Text>{" "}
59
- <Text color="white">
60
- {typeof value === "string"
61
- ? value.length > 80
62
- ? value.slice(0, 77) + "..."
63
- : value
64
- : JSON.stringify(value)}
65
- </Text>
66
- </Text>
67
- ))}
68
- </Box>
69
- )}
70
-
71
- {result && (
72
- <Box marginTop={0} marginLeft={2}>
73
- <Text dimColor>
74
- {result.length > 120 ? result.slice(0, 117) + "..." : result}
75
- </Text>
76
- </Box>
77
- )}
78
- </Box>
79
- );
80
- };
@@ -1,356 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import {
3
- CommandRegistry,
4
- levenshteinDistance,
5
- } from "../../src/commands/commandRegistry.js";
6
- import {
7
- createDefaultRegistry,
8
- HelpCommand,
9
- ModelCommand,
10
- ClearCommand,
11
- CompactCommand,
12
- TokensCommand,
13
- StatusCommand,
14
- HistoryCommand,
15
- UndoCommand,
16
- ExitCommand,
17
- } from "../../src/commands/builtinCommands.js";
18
- import { CommandContext } from "../../src/commands/commandRegistry.js";
19
- import { HumanMessage, AIMessage, SystemMessage } from "@langchain/core/messages";
20
-
21
- // ─── Test Helpers ───────────────────────────────────────────────────────────────
22
-
23
- function makeContext(overrides: Partial<CommandContext> = {}): CommandContext {
24
- return {
25
- config: {
26
- provider: "anthropic",
27
- model: "claude-sonnet-4-20250514",
28
- maxTokens: 4096,
29
- temperature: 0,
30
- streaming: true,
31
- } as any,
32
- configPath: "/tmp/test-config.json",
33
- harness: {} as any,
34
- contextState: {
35
- globalSystemInstructions: "You are a test agent.",
36
- projectMemory: "Test project.",
37
- sessionContext: "Test session.",
38
- conversationHistory: [],
39
- },
40
- setContextState: () => {},
41
- addSystemMessage: () => {},
42
- provider: "anthropic",
43
- model: "claude-sonnet-4-20250514",
44
- maxTokens: 4096,
45
- ...overrides,
46
- };
47
- }
48
-
49
- // ─── Levenshtein Distance ───────────────────────────────────────────────────────
50
-
51
- describe("levenshteinDistance", () => {
52
- it("returns 0 for identical strings", () => {
53
- expect(levenshteinDistance("model", "model")).toBe(0);
54
- });
55
-
56
- it("returns correct distance for single edit", () => {
57
- expect(levenshteinDistance("model", "modle")).toBe(2); // transposition = 2 edits
58
- expect(levenshteinDistance("help", "helo")).toBe(1);
59
- });
60
-
61
- it("handles empty strings", () => {
62
- expect(levenshteinDistance("", "abc")).toBe(3);
63
- expect(levenshteinDistance("abc", "")).toBe(3);
64
- });
65
- });
66
-
67
- // ─── CommandRegistry ────────────────────────────────────────────────────────────
68
-
69
- describe("CommandRegistry", () => {
70
- let registry: CommandRegistry;
71
-
72
- beforeEach(() => {
73
- registry = new CommandRegistry();
74
- });
75
-
76
- it("detects slash commands", () => {
77
- expect(registry.isCommand("/help")).toBe(true);
78
- expect(registry.isCommand("/model gpt-4")).toBe(true);
79
- expect(registry.isCommand(" /tokens")).toBe(true);
80
- expect(registry.isCommand("Hello world")).toBe(false);
81
- expect(registry.isCommand("")).toBe(false);
82
- });
83
-
84
- it("registers and executes a command", async () => {
85
- registry.register({
86
- name: "ping",
87
- description: "Test command",
88
- execute: async () => "pong",
89
- });
90
-
91
- const result = await registry.execute("/ping", makeContext());
92
- expect(result).toBe("pong");
93
- });
94
-
95
- it("resolves aliases to primary command", async () => {
96
- registry.register({
97
- name: "help",
98
- aliases: ["h", "?"],
99
- description: "Show help",
100
- execute: async () => "help text",
101
- });
102
-
103
- expect(await registry.execute("/h", makeContext())).toBe("help text");
104
- expect(await registry.execute("/?", makeContext())).toBe("help text");
105
- });
106
-
107
- it("returns error with suggestions for unknown commands", async () => {
108
- registry.register({
109
- name: "model",
110
- description: "Switch model",
111
- execute: async () => "ok",
112
- });
113
-
114
- const result = await registry.execute("/modle", makeContext());
115
- expect(result).toContain("Unknown command");
116
- expect(result).toContain("/model");
117
- });
118
-
119
- it("parses command args correctly", async () => {
120
- let receivedArgs = "";
121
- registry.register({
122
- name: "echo",
123
- description: "Echo args",
124
- execute: async (args) => {
125
- receivedArgs = args;
126
- return args;
127
- },
128
- });
129
-
130
- await registry.execute("/echo hello world", makeContext());
131
- expect(receivedArgs).toBe("hello world");
132
- });
133
-
134
- it("handles commands with no args", async () => {
135
- let receivedArgs = "";
136
- registry.register({
137
- name: "noargs",
138
- description: "No args",
139
- execute: async (args) => {
140
- receivedArgs = args;
141
- return "ok";
142
- },
143
- });
144
-
145
- await registry.execute("/noargs", makeContext());
146
- expect(receivedArgs).toBe("");
147
- });
148
-
149
- it("getHelp() returns formatted text", () => {
150
- registry.register({
151
- name: "foo",
152
- aliases: ["f"],
153
- description: "Do foo",
154
- execute: async () => {},
155
- });
156
- registry.register({
157
- name: "bar",
158
- description: "Do bar",
159
- execute: async () => {},
160
- });
161
-
162
- const help = registry.getHelp();
163
- expect(help).toContain("/foo");
164
- expect(help).toContain("/f");
165
- expect(help).toContain("Do foo");
166
- expect(help).toContain("/bar");
167
- });
168
-
169
- it("is case-insensitive for command names", async () => {
170
- registry.register({
171
- name: "help",
172
- description: "Help",
173
- execute: async () => "ok",
174
- });
175
-
176
- expect(await registry.execute("/HELP", makeContext())).toBe("ok");
177
- expect(await registry.execute("/Help", makeContext())).toBe("ok");
178
- });
179
- });
180
-
181
- // ─── Built-in Commands ──────────────────────────────────────────────────────────
182
-
183
- describe("Built-in Commands", () => {
184
- describe("/help", () => {
185
- it("returns help text with all commands listed", async () => {
186
- const result = await HelpCommand.execute("", makeContext());
187
- expect(result).toContain("/help");
188
- expect(result).toContain("/model");
189
- expect(result).toContain("/clear");
190
- expect(result).toContain("/tokens");
191
- expect(result).toContain("/exit");
192
- });
193
- });
194
-
195
- describe("/model", () => {
196
- it("shows current model when no args", async () => {
197
- const result = await ModelCommand.execute("", makeContext());
198
- expect(result).toContain("claude-sonnet-4-20250514");
199
- expect(result).toContain("anthropic");
200
- });
201
-
202
- it("advises restart when model name provided", async () => {
203
- const result = await ModelCommand.execute("gpt-4o", makeContext());
204
- expect(result).toContain("gpt-4o");
205
- expect(result).toContain("restart");
206
- });
207
- });
208
-
209
- describe("/clear", () => {
210
- it("clears conversation history", async () => {
211
- let capturedState: any = null;
212
- const ctx = makeContext({
213
- contextState: {
214
- globalSystemInstructions: "test",
215
- projectMemory: "test",
216
- sessionContext: "test",
217
- conversationHistory: [
218
- new HumanMessage("hello"),
219
- new AIMessage("hi"),
220
- ],
221
- },
222
- setContextState: (s) => { capturedState = s; },
223
- });
224
-
225
- const result = await ClearCommand.execute("", ctx);
226
- expect(result).toContain("2 messages");
227
- expect(capturedState.conversationHistory).toHaveLength(0);
228
- });
229
- });
230
-
231
- describe("/tokens", () => {
232
- it("shows token usage info", async () => {
233
- const result = await TokensCommand.execute("", makeContext());
234
- expect(result).toContain("Token Usage");
235
- expect(result).toContain("System prompt");
236
- expect(result).toContain("Conversation");
237
- });
238
- });
239
-
240
- describe("/status", () => {
241
- it("shows session status", async () => {
242
- const result = await StatusCommand.execute("", makeContext());
243
- expect(result).toContain("anthropic");
244
- expect(result).toContain("claude-sonnet-4-20250514");
245
- expect(result).toContain("Session Status");
246
- });
247
- });
248
-
249
- describe("/history", () => {
250
- it("returns empty message for no history", async () => {
251
- const result = await HistoryCommand.execute("", makeContext());
252
- expect(result).toContain("No conversation history");
253
- });
254
-
255
- it("shows messages when history exists", async () => {
256
- const ctx = makeContext({
257
- contextState: {
258
- globalSystemInstructions: "",
259
- projectMemory: "",
260
- sessionContext: "",
261
- conversationHistory: [
262
- new HumanMessage("Hello"),
263
- new AIMessage("Hi there!"),
264
- ],
265
- },
266
- });
267
-
268
- const result = await HistoryCommand.execute("", ctx);
269
- expect(result).toContain("2 messages");
270
- expect(result).toContain("Hello");
271
- expect(result).toContain("Hi there!");
272
- });
273
- });
274
-
275
- describe("/undo", () => {
276
- it("removes last user+agent exchange", async () => {
277
- let capturedState: any = null;
278
- const ctx = makeContext({
279
- contextState: {
280
- globalSystemInstructions: "",
281
- projectMemory: "",
282
- sessionContext: "",
283
- conversationHistory: [
284
- new HumanMessage("First"),
285
- new AIMessage("Response 1"),
286
- new HumanMessage("Second"),
287
- new AIMessage("Response 2"),
288
- ],
289
- },
290
- setContextState: (s) => { capturedState = s; },
291
- });
292
-
293
- const result = await UndoCommand.execute("", ctx);
294
- expect(result).toContain("2 message(s)");
295
- expect(capturedState.conversationHistory).toHaveLength(2);
296
- });
297
-
298
- it("returns message for empty history", async () => {
299
- const result = await UndoCommand.execute("", makeContext());
300
- expect(result).toContain("Nothing to undo");
301
- });
302
- });
303
-
304
- describe("/exit", () => {
305
- it("returns __EXIT__ signal", async () => {
306
- const result = await ExitCommand.execute("", makeContext());
307
- expect(result).toBe("__EXIT__");
308
- });
309
- });
310
-
311
- describe("/compact", () => {
312
- it("rejects when history is too short", async () => {
313
- const result = await CompactCommand.execute("", makeContext({
314
- contextState: {
315
- globalSystemInstructions: "",
316
- projectMemory: "",
317
- sessionContext: "",
318
- conversationHistory: [new HumanMessage("hi")],
319
- },
320
- }));
321
- expect(result).toContain("Not enough history");
322
- });
323
- });
324
- });
325
-
326
- // ─── Default Registry ───────────────────────────────────────────────────────────
327
-
328
- describe("createDefaultRegistry", () => {
329
- it("creates a registry with all built-in commands", () => {
330
- const registry = createDefaultRegistry();
331
- const all = registry.getAll();
332
-
333
- const names = all.map((c) => c.name);
334
- expect(names).toContain("help");
335
- expect(names).toContain("model");
336
- expect(names).toContain("clear");
337
- expect(names).toContain("compact");
338
- expect(names).toContain("tokens");
339
- expect(names).toContain("status");
340
- expect(names).toContain("history");
341
- expect(names).toContain("undo");
342
- expect(names).toContain("exit");
343
- });
344
-
345
- it("resolves aliases correctly", async () => {
346
- const registry = createDefaultRegistry();
347
-
348
- // /h → /help
349
- const result = await registry.execute("/h", makeContext());
350
- expect(result).toContain("/help");
351
-
352
- // /q → /exit
353
- const exitResult = await registry.execute("/q", makeContext());
354
- expect(exitResult).toBe("__EXIT__");
355
- });
356
- });