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