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.
- package/dist/cli/index.js +4 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/builtinCommands.js +6 -6
- package/dist/commands/builtinCommands.js.map +1 -1
- package/dist/commands/commandRegistry.d.ts +3 -1
- package/dist/commands/commandRegistry.js.map +1 -1
- package/dist/core/agentLoop.d.ts +3 -1
- package/dist/core/agentLoop.js +17 -7
- package/dist/core/agentLoop.js.map +1 -1
- package/dist/core/compactor.js +2 -2
- package/dist/core/compactor.js.map +1 -1
- package/dist/core/contextGuard.d.ts +5 -0
- package/dist/core/contextGuard.js +30 -3
- package/dist/core/contextGuard.js.map +1 -1
- package/dist/core/events.d.ts +45 -0
- package/dist/core/events.js +8 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/sessionStore.js +3 -2
- package/dist/core/sessionStore.js.map +1 -1
- package/dist/core/subAgent.js +2 -2
- package/dist/core/subAgent.js.map +1 -1
- package/dist/core/tokenCounter.d.ts +8 -1
- package/dist/core/tokenCounter.js +28 -0
- package/dist/core/tokenCounter.js.map +1 -1
- package/dist/middleware/permission.js +1 -0
- package/dist/middleware/permission.js.map +1 -1
- package/dist/tools/browser.js +4 -1
- package/dist/tools/browser.js.map +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +11 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/installHostDeps.d.ts +2 -0
- package/dist/tools/installHostDeps.js +37 -0
- package/dist/tools/installHostDeps.js.map +1 -0
- package/dist/tools/router.js +1 -0
- package/dist/tools/router.js.map +1 -1
- package/dist/tools/spawnAgent.js +3 -1
- package/dist/tools/spawnAgent.js.map +1 -1
- package/dist/tracing/sessionTracer.d.ts +1 -0
- package/dist/tracing/sessionTracer.js +4 -1
- package/dist/tracing/sessionTracer.js.map +1 -1
- package/dist/ui/App.js +6 -1
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/ActionLog.d.ts +7 -0
- package/dist/ui/components/ActionLog.js +63 -0
- package/dist/ui/components/ActionLog.js.map +1 -0
- package/dist/ui/components/FileBrowser.d.ts +2 -0
- package/dist/ui/components/FileBrowser.js +41 -0
- package/dist/ui/components/FileBrowser.js.map +1 -0
- package/package.json +3 -5
- package/AGENTS.md +0 -56
- package/Handover.md +0 -115
- package/PROGRESS.md +0 -160
- package/docs/01_insights_and_patterns.md +0 -27
- package/docs/02_edge_cases_and_mitigations.md +0 -143
- package/docs/03_initial_implementation_plan.md +0 -66
- package/docs/04_tech_stack_proposal.md +0 -20
- package/docs/05_prd.md +0 -87
- package/docs/06_user_stories.md +0 -72
- package/docs/07_system_architecture.md +0 -138
- package/docs/08_roadmap.md +0 -200
- package/e2b/Dockerfile +0 -26
- package/src/__tests__/bootstrap.test.ts +0 -111
- package/src/__tests__/config.test.ts +0 -97
- package/src/__tests__/m55.test.ts +0 -238
- package/src/__tests__/middleware.test.ts +0 -219
- package/src/__tests__/modelFactory.test.ts +0 -63
- package/src/__tests__/optimizations.test.ts +0 -201
- package/src/__tests__/promptBuilder.test.ts +0 -141
- package/src/__tests__/sandbox.test.ts +0 -102
- package/src/__tests__/security.test.ts +0 -122
- package/src/__tests__/streaming.test.ts +0 -82
- package/src/__tests__/toolRouter.test.ts +0 -52
- package/src/__tests__/tools.test.ts +0 -146
- package/src/__tests__/tracing.test.ts +0 -196
- package/src/agents/agentRegistry.ts +0 -69
- package/src/agents/agentSpec.ts +0 -67
- package/src/agents/builtinAgents.ts +0 -142
- package/src/cli/config.ts +0 -124
- package/src/cli/index.ts +0 -742
- package/src/cli/modelFactory.ts +0 -174
- package/src/cli/postinstall.ts +0 -28
- package/src/cli/providers.ts +0 -107
- package/src/commands/builtinCommands.ts +0 -293
- package/src/commands/commandRegistry.ts +0 -194
- package/src/core/agentLoop.d.ts.map +0 -1
- package/src/core/agentLoop.ts +0 -312
- package/src/core/autoSave.ts +0 -95
- package/src/core/compactor.ts +0 -252
- package/src/core/contextGuard.ts +0 -129
- package/src/core/errors.ts +0 -202
- package/src/core/promptBuilder.d.ts.map +0 -1
- package/src/core/promptBuilder.ts +0 -139
- package/src/core/reasoningRouter.ts +0 -121
- package/src/core/retry.ts +0 -75
- package/src/core/sessionResumer.ts +0 -90
- package/src/core/sessionStore.ts +0 -216
- package/src/core/subAgent.ts +0 -339
- package/src/core/tokenCounter.ts +0 -64
- package/src/evals/dataset.ts +0 -67
- package/src/evals/evaluator.ts +0 -81
- package/src/hitl/bridge.ts +0 -160
- package/src/middleware/commandSanitizer.ts +0 -60
- package/src/middleware/loopDetection.ts +0 -63
- package/src/middleware/permission.ts +0 -72
- package/src/middleware/pipeline.ts +0 -75
- package/src/middleware/preCompletion.ts +0 -94
- package/src/middleware/types.ts +0 -45
- package/src/sandbox/bootstrap.ts +0 -121
- package/src/sandbox/manager.ts +0 -239
- package/src/sandbox/sync.ts +0 -157
- package/src/skills/loader.ts +0 -143
- package/src/skills/tools.ts +0 -99
- package/src/skills/types.ts +0 -13
- package/src/test_cache.ts +0 -72
- package/src/tools/askUser.ts +0 -47
- package/src/tools/browser.ts +0 -137
- package/src/tools/index.d.ts.map +0 -1
- package/src/tools/index.ts +0 -237
- package/src/tools/registry.ts +0 -198
- package/src/tools/router.ts +0 -78
- package/src/tools/security.ts +0 -220
- package/src/tools/spawnAgent.ts +0 -158
- package/src/tools/webSearch.ts +0 -142
- package/src/tracing/analyzer.ts +0 -265
- package/src/tracing/langsmith.ts +0 -63
- package/src/tracing/sessionTracer.ts +0 -202
- package/src/tracing/types.ts +0 -49
- package/src/types/valyu.d.ts +0 -37
- package/src/ui/App.tsx +0 -404
- package/src/ui/components/HITLPrompt.tsx +0 -119
- package/src/ui/components/Header.tsx +0 -51
- package/src/ui/components/MessageBubble.tsx +0 -46
- package/src/ui/components/StatusBar.tsx +0 -138
- package/src/ui/components/StreamingText.tsx +0 -48
- package/src/ui/components/ToolCallPanel.tsx +0 -80
- package/tests/commands/commands.test.ts +0 -356
- package/tests/core/compactor.test.ts +0 -217
- package/tests/core/retryAndErrors.test.ts +0 -164
- package/tests/core/sessionResumer.test.ts +0 -95
- package/tests/core/sessionStore.test.ts +0 -84
- package/tests/core/stability.test.ts +0 -165
- package/tests/core/subAgent.test.ts +0 -238
- package/tests/hitl/hitlBridge.test.ts +0 -115
- package/tsconfig.json +0 -16
- package/vitest.config.ts +0 -10
- 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
|
-
});
|