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,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
- });