@ruvnet/open-claude-code 2.0.1 → 2.1.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/README.md +113 -0
- package/package.json +20 -2
- package/src/config/env.mjs +0 -2
- package/src/config/settings.mjs +24 -130
- package/src/core/session.mjs +4 -145
- package/src/core/system-prompt.mjs +43 -47
- package/src/index.mjs +21 -31
- package/src/permissions/checker.mjs +8 -79
- package/src/tools/bash.mjs +3 -40
- package/src/ui/app.mjs +286 -0
- package/src/ui/commands.mjs +83 -70
- package/src/ui/components.mjs +219 -0
- package/src/ui/ink-app.mjs +13 -52
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/repl.mjs +10 -25
- package/test/test.mjs +0 -800
package/src/ui/app.mjs
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main Ink App — full React terminal UI matching Claude Code's interface.
|
|
3
|
+
*
|
|
4
|
+
* Uses React.createElement (no JSX transpiler needed).
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Scrolling messages area with assistant, user, tool, thinking messages
|
|
8
|
+
* - Persistent status bar (model, context%, cost, time, permission mode)
|
|
9
|
+
* - Text input with prompt indicator
|
|
10
|
+
* - Loading spinner during API calls
|
|
11
|
+
* - Tool execution display with spinners
|
|
12
|
+
* - Keyboard shortcuts (Escape, Ctrl+C, Ctrl+L)
|
|
13
|
+
* - Slash command support (39 commands)
|
|
14
|
+
* - Skill invocation via /skill-name
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React, { useState, useCallback, useRef } from 'react';
|
|
18
|
+
import { render, Box, Text, useInput, useApp } from 'ink';
|
|
19
|
+
import TextInput from 'ink-text-input';
|
|
20
|
+
import {
|
|
21
|
+
StatusBar,
|
|
22
|
+
Message,
|
|
23
|
+
LoadingIndicator,
|
|
24
|
+
WelcomeBanner,
|
|
25
|
+
} from './components.mjs';
|
|
26
|
+
import { executeCommand, COMMANDS } from './commands.mjs';
|
|
27
|
+
|
|
28
|
+
const h = React.createElement;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Main application component.
|
|
32
|
+
*/
|
|
33
|
+
export function App({ agentLoop, settings }) {
|
|
34
|
+
const [input, setInput] = useState('');
|
|
35
|
+
const [messages, setMessages] = useState([]);
|
|
36
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
37
|
+
const [model, setModel] = useState(agentLoop.state.model || 'claude-sonnet-4-6');
|
|
38
|
+
const [tokenCount, setTokenCount] = useState({ input: 0, output: 0 });
|
|
39
|
+
const [cost, setCost] = useState(0);
|
|
40
|
+
const [mode] = useState(settings.permissions?.defaultMode || 'default');
|
|
41
|
+
const [toolStatus, setToolStatus] = useState(null);
|
|
42
|
+
const [startTime] = useState(Date.now());
|
|
43
|
+
const loadingRef = useRef(false);
|
|
44
|
+
const { exit } = useApp();
|
|
45
|
+
|
|
46
|
+
// Calculate cost from token counts
|
|
47
|
+
const updateCost = useCallback((tokens) => {
|
|
48
|
+
const m = agentLoop.state.model || '';
|
|
49
|
+
let priceIn = 3, priceOut = 15;
|
|
50
|
+
if (m.includes('haiku')) { priceIn = 0.25; priceOut = 1.25; }
|
|
51
|
+
if (m.includes('opus')) { priceIn = 15; priceOut = 75; }
|
|
52
|
+
const costIn = (tokens.input / 1_000_000) * priceIn;
|
|
53
|
+
const costOut = (tokens.output / 1_000_000) * priceOut;
|
|
54
|
+
setCost(costIn + costOut);
|
|
55
|
+
}, [agentLoop.state.model]);
|
|
56
|
+
|
|
57
|
+
// Add a message to the display
|
|
58
|
+
const addMessage = useCallback((msg) => {
|
|
59
|
+
setMessages(prev => {
|
|
60
|
+
const next = [...prev, msg];
|
|
61
|
+
return next.length > 200 ? next.slice(-150) : next;
|
|
62
|
+
});
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
// Handle slash commands
|
|
66
|
+
const handleCommand = useCallback((text) => {
|
|
67
|
+
const cmdName = text.split(/\s+/)[0].toLowerCase();
|
|
68
|
+
|
|
69
|
+
// Check for skill invocation
|
|
70
|
+
if (!COMMANDS[cmdName] && agentLoop.state._skillsLoader) {
|
|
71
|
+
const skill = agentLoop.state._skillsLoader.get(cmdName.slice(1));
|
|
72
|
+
if (skill) {
|
|
73
|
+
const args = text.slice(cmdName.length).trim();
|
|
74
|
+
addMessage({ role: 'user', content: text });
|
|
75
|
+
runPrompt(
|
|
76
|
+
`[Skill: ${skill.name}]\n${skill.prompt}${args ? `\nArguments: ${args}` : ''}`
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { response, exit: shouldExit } = executeCommand(text, agentLoop.state);
|
|
83
|
+
if (shouldExit) {
|
|
84
|
+
exit();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
addMessage({ role: 'system', content: response });
|
|
88
|
+
|
|
89
|
+
// Sync model if it changed
|
|
90
|
+
if (agentLoop.state.model !== model) {
|
|
91
|
+
setModel(agentLoop.state.model);
|
|
92
|
+
}
|
|
93
|
+
}, [agentLoop, model, addMessage, exit]);
|
|
94
|
+
|
|
95
|
+
// Run a prompt through the agent loop
|
|
96
|
+
const runPrompt = useCallback(async (text) => {
|
|
97
|
+
setIsLoading(true);
|
|
98
|
+
loadingRef.current = true;
|
|
99
|
+
setToolStatus(null);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
for await (const event of agentLoop.run(text)) {
|
|
103
|
+
switch (event.type) {
|
|
104
|
+
case 'stream_event':
|
|
105
|
+
setMessages(prev => {
|
|
106
|
+
const last = prev[prev.length - 1];
|
|
107
|
+
if (last && last.role === 'assistant' && last._streaming) {
|
|
108
|
+
return [
|
|
109
|
+
...prev.slice(0, -1),
|
|
110
|
+
{ ...last, content: (last.content || '') + (event.text || '') },
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
return [
|
|
114
|
+
...prev,
|
|
115
|
+
{ role: 'assistant', content: event.text || '', _streaming: true },
|
|
116
|
+
];
|
|
117
|
+
});
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case 'assistant':
|
|
121
|
+
setMessages(prev => {
|
|
122
|
+
const last = prev[prev.length - 1];
|
|
123
|
+
if (last && last.role === 'assistant' && last._streaming) {
|
|
124
|
+
return [
|
|
125
|
+
...prev.slice(0, -1),
|
|
126
|
+
{ ...last, _streaming: false },
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
if (event.content) {
|
|
130
|
+
return [...prev, { role: 'assistant', content: event.content }];
|
|
131
|
+
}
|
|
132
|
+
return prev;
|
|
133
|
+
});
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
case 'thinking':
|
|
137
|
+
addMessage({ role: 'thinking', thinking: event.text });
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
case 'tool_progress':
|
|
141
|
+
setToolStatus(event.tool);
|
|
142
|
+
addMessage({
|
|
143
|
+
role: 'tool',
|
|
144
|
+
toolName: event.tool,
|
|
145
|
+
toolStatus: 'running',
|
|
146
|
+
});
|
|
147
|
+
break;
|
|
148
|
+
|
|
149
|
+
case 'result':
|
|
150
|
+
setToolStatus(null);
|
|
151
|
+
setMessages(prev => {
|
|
152
|
+
const idx = prev.findLastIndex(
|
|
153
|
+
m => m.role === 'tool' &&
|
|
154
|
+
m.toolName === event.tool &&
|
|
155
|
+
m.toolStatus === 'running'
|
|
156
|
+
);
|
|
157
|
+
if (idx >= 0) {
|
|
158
|
+
const updated = [...prev];
|
|
159
|
+
updated[idx] = {
|
|
160
|
+
...updated[idx],
|
|
161
|
+
toolResult: typeof event.result === 'string'
|
|
162
|
+
? event.result
|
|
163
|
+
: JSON.stringify(event.result),
|
|
164
|
+
toolStatus: 'done',
|
|
165
|
+
};
|
|
166
|
+
return updated;
|
|
167
|
+
}
|
|
168
|
+
return prev;
|
|
169
|
+
});
|
|
170
|
+
break;
|
|
171
|
+
|
|
172
|
+
case 'compaction':
|
|
173
|
+
addMessage({ role: 'system', content: `[compaction #${event.count}]` });
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case 'hookPermissionResult':
|
|
177
|
+
if (!event.allowed) {
|
|
178
|
+
addMessage({ role: 'system', content: `[blocked: ${event.tool}]` });
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
case 'error':
|
|
183
|
+
addMessage({ role: 'error', content: event.message });
|
|
184
|
+
break;
|
|
185
|
+
|
|
186
|
+
case 'stop':
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Update token counts
|
|
191
|
+
const usage = agentLoop.state.tokenUsage;
|
|
192
|
+
setTokenCount({ input: usage.input, output: usage.output });
|
|
193
|
+
updateCost(usage);
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
addMessage({ role: 'error', content: err.message });
|
|
197
|
+
} finally {
|
|
198
|
+
setIsLoading(false);
|
|
199
|
+
loadingRef.current = false;
|
|
200
|
+
setToolStatus(null);
|
|
201
|
+
}
|
|
202
|
+
}, [agentLoop, addMessage, updateCost]);
|
|
203
|
+
|
|
204
|
+
// Handle submit
|
|
205
|
+
const handleSubmit = useCallback((value) => {
|
|
206
|
+
const trimmed = value.trim();
|
|
207
|
+
if (!trimmed) return;
|
|
208
|
+
|
|
209
|
+
setInput('');
|
|
210
|
+
|
|
211
|
+
if (trimmed.startsWith('/')) {
|
|
212
|
+
handleCommand(trimmed);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
addMessage({ role: 'user', content: trimmed });
|
|
217
|
+
runPrompt(trimmed);
|
|
218
|
+
}, [handleCommand, addMessage, runPrompt]);
|
|
219
|
+
|
|
220
|
+
// Keyboard shortcuts
|
|
221
|
+
useInput((ch, key) => {
|
|
222
|
+
if (key.escape && loadingRef.current) {
|
|
223
|
+
setIsLoading(false);
|
|
224
|
+
setToolStatus(null);
|
|
225
|
+
addMessage({ role: 'system', content: '[cancelled]' });
|
|
226
|
+
}
|
|
227
|
+
if (key.ctrl && ch === 'c') {
|
|
228
|
+
exit();
|
|
229
|
+
}
|
|
230
|
+
if (key.ctrl && ch === 'l') {
|
|
231
|
+
setMessages([]);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const toolCount = agentLoop.state.tools?.list?.()?.length || 0;
|
|
236
|
+
|
|
237
|
+
// Build the message list
|
|
238
|
+
const messageElements = messages.map((msg, i) =>
|
|
239
|
+
h(Message, { key: i, ...msg })
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
return h(Box, { flexDirection: 'column' },
|
|
243
|
+
// Welcome banner
|
|
244
|
+
h(WelcomeBanner, { model, toolCount }),
|
|
245
|
+
|
|
246
|
+
// Messages area
|
|
247
|
+
h(Box, { flexDirection: 'column', flexGrow: 1 },
|
|
248
|
+
...messageElements,
|
|
249
|
+
isLoading ? h(LoadingIndicator, { tool: toolStatus }) : null,
|
|
250
|
+
),
|
|
251
|
+
|
|
252
|
+
// Input area
|
|
253
|
+
h(Box, { borderStyle: 'round', borderColor: 'cyan', paddingX: 1, marginTop: 1 },
|
|
254
|
+
h(Text, { color: 'cyan' }, '> '),
|
|
255
|
+
h(TextInput, {
|
|
256
|
+
value: input,
|
|
257
|
+
onChange: setInput,
|
|
258
|
+
onSubmit: handleSubmit,
|
|
259
|
+
placeholder: 'Type a message...',
|
|
260
|
+
}),
|
|
261
|
+
),
|
|
262
|
+
|
|
263
|
+
// Status bar
|
|
264
|
+
h(StatusBar, {
|
|
265
|
+
model,
|
|
266
|
+
tokens: tokenCount,
|
|
267
|
+
cost,
|
|
268
|
+
mode,
|
|
269
|
+
startTime,
|
|
270
|
+
contextMax: settings.maxContextTokens || 200000,
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Start the Ink application.
|
|
277
|
+
* @param {object} agentLoop - agent loop instance
|
|
278
|
+
* @param {object} settings - loaded settings
|
|
279
|
+
* @returns {object} Ink instance
|
|
280
|
+
*/
|
|
281
|
+
export function startInkApp(agentLoop, settings) {
|
|
282
|
+
const instance = render(
|
|
283
|
+
h(App, { agentLoop, settings })
|
|
284
|
+
);
|
|
285
|
+
return instance;
|
|
286
|
+
}
|
package/src/ui/commands.mjs
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Slash Commands — all 39 commands from Claude Code.
|
|
3
|
+
*
|
|
4
|
+
* Each command is a function(args, state) that returns a string response.
|
|
5
|
+
* Commands are invoked via /command-name in the REPL.
|
|
6
|
+
*/
|
|
7
|
+
|
|
5
8
|
import { SessionManager } from '../core/session.mjs';
|
|
6
9
|
import { CheckpointManager } from '../core/checkpoints.mjs';
|
|
7
10
|
import { PromptCache } from '../core/cache.mjs';
|
|
8
11
|
import { readEnv, listEnvVars } from '../config/env.mjs';
|
|
9
|
-
import { cronStore as _cronStore } from '../tools/cron-create.mjs';
|
|
10
12
|
import * as telemetry from '../telemetry/index.mjs';
|
|
11
13
|
|
|
12
14
|
const checkpoints = new CheckpointManager();
|
|
13
15
|
const promptCache = new PromptCache();
|
|
14
16
|
let sessionManager = null;
|
|
17
|
+
|
|
15
18
|
function getSession() {
|
|
16
19
|
if (!sessionManager) sessionManager = new SessionManager();
|
|
17
20
|
return sessionManager;
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
/**
|
|
24
|
+
* All slash commands.
|
|
25
|
+
*/
|
|
20
26
|
export const COMMANDS = {
|
|
21
27
|
'/help': {
|
|
22
28
|
description: 'Show available commands',
|
|
@@ -25,6 +31,7 @@ export const COMMANDS = {
|
|
|
25
31
|
for (const [name, cmd] of Object.entries(COMMANDS)) {
|
|
26
32
|
lines.push(` ${name.padEnd(20)} ${cmd.description}`);
|
|
27
33
|
}
|
|
34
|
+
lines.push('');
|
|
28
35
|
return lines.join('\n');
|
|
29
36
|
},
|
|
30
37
|
},
|
|
@@ -67,13 +74,11 @@ export const COMMANDS = {
|
|
|
67
74
|
description: 'Show token usage and estimated cost',
|
|
68
75
|
handler(args, state) {
|
|
69
76
|
const { input, output } = state.tokenUsage;
|
|
77
|
+
// Use model-appropriate pricing
|
|
70
78
|
const model = state.model || '';
|
|
71
79
|
let priceIn = 3, priceOut = 15; // Sonnet default
|
|
72
80
|
if (model.includes('haiku')) { priceIn = 0.25; priceOut = 1.25; }
|
|
73
81
|
if (model.includes('opus')) { priceIn = 15; priceOut = 75; }
|
|
74
|
-
if (model.includes('gpt-4o-mini')) { priceIn = 0.15; priceOut = 0.6; }
|
|
75
|
-
else if (model.includes('gpt-4o')) { priceIn = 2.5; priceOut = 10; }
|
|
76
|
-
if (model.includes('gemini')) { priceIn = 0.075; priceOut = 0.3; }
|
|
77
82
|
|
|
78
83
|
const costIn = (input / 1_000_000) * priceIn;
|
|
79
84
|
const costOut = (output / 1_000_000) * priceOut;
|
|
@@ -93,28 +98,22 @@ export const COMMANDS = {
|
|
|
93
98
|
const checks = [];
|
|
94
99
|
checks.push(`Node.js: ${process.version}`);
|
|
95
100
|
checks.push(`ANTHROPIC_API_KEY: ${process.env.ANTHROPIC_API_KEY ? 'set' : 'NOT SET'}`);
|
|
96
|
-
checks.push(`OPENAI_API_KEY: ${process.env.OPENAI_API_KEY ? 'set' : 'NOT SET'}`);
|
|
97
|
-
checks.push(`GOOGLE_API_KEY: ${process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY ? 'set' : 'NOT SET'}`);
|
|
98
101
|
checks.push(`Model: ${state.model || 'default'}`);
|
|
99
102
|
checks.push(`Tools: ${state.tools?.list?.()?.length || 0}`);
|
|
100
103
|
checks.push(`Messages: ${state.messages.length}`);
|
|
101
104
|
checks.push(`CWD: ${process.cwd()}`);
|
|
102
105
|
checks.push(`Platform: ${process.platform}`);
|
|
103
106
|
|
|
104
|
-
// API connectivity
|
|
105
|
-
let apiStatus = '
|
|
106
|
-
if (process.env.ANTHROPIC_API_KEY)
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
// Check API connectivity
|
|
108
|
+
let apiStatus = 'unchecked';
|
|
109
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
110
|
+
apiStatus = 'key present';
|
|
111
|
+
}
|
|
109
112
|
checks.push(`API: ${apiStatus}`);
|
|
110
113
|
|
|
111
114
|
// Check MCP servers
|
|
112
|
-
const
|
|
113
|
-
checks.push(`MCP servers: ${
|
|
114
|
-
for (const client of mcpClients) {
|
|
115
|
-
const status = client.connected ? 'connected' : 'disconnected';
|
|
116
|
-
checks.push(` - ${client.config?.command || 'unknown'}: ${status}`);
|
|
117
|
-
}
|
|
115
|
+
const mcpCount = state._mcpClients?.length || 0;
|
|
116
|
+
checks.push(`MCP servers: ${mcpCount}`);
|
|
118
117
|
|
|
119
118
|
return `System check:\n${checks.map(c => ` ${c}`).join('\n')}`;
|
|
120
119
|
},
|
|
@@ -136,9 +135,8 @@ export const COMMANDS = {
|
|
|
136
135
|
description: 'Show or switch model',
|
|
137
136
|
handler(args, state) {
|
|
138
137
|
if (args) {
|
|
139
|
-
const old = state.model;
|
|
140
138
|
state.model = args;
|
|
141
|
-
return `Model switched: ${
|
|
139
|
+
return `Model switched to: ${args}`;
|
|
142
140
|
}
|
|
143
141
|
return `Current model: ${state.model || 'default'}`;
|
|
144
142
|
},
|
|
@@ -168,14 +166,28 @@ export const COMMANDS = {
|
|
|
168
166
|
},
|
|
169
167
|
},
|
|
170
168
|
|
|
171
|
-
'/quit': {
|
|
172
|
-
|
|
173
|
-
|
|
169
|
+
'/quit': {
|
|
170
|
+
description: 'Exit the REPL',
|
|
171
|
+
handler() { return 'EXIT'; },
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
'/exit': {
|
|
175
|
+
description: 'Exit the REPL',
|
|
176
|
+
handler() { return 'EXIT'; },
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
'/bug': {
|
|
180
|
+
description: 'Report a bug',
|
|
181
|
+
handler() {
|
|
182
|
+
return 'Report bugs at: https://github.com/ruvnet/open-claude-code/issues';
|
|
183
|
+
},
|
|
184
|
+
},
|
|
174
185
|
|
|
175
186
|
'/review': {
|
|
176
187
|
description: 'Review recent changes',
|
|
177
188
|
handler(args, state) {
|
|
178
189
|
try {
|
|
190
|
+
const { execSync } = require('child_process');
|
|
179
191
|
const diff = execSync('git diff --stat HEAD~1 2>/dev/null || echo "No git history"', { encoding: 'utf-8' });
|
|
180
192
|
return `Recent changes:\n${diff}`;
|
|
181
193
|
} catch {
|
|
@@ -187,6 +199,8 @@ export const COMMANDS = {
|
|
|
187
199
|
'/init': {
|
|
188
200
|
description: 'Initialize Claude Code in current directory',
|
|
189
201
|
handler() {
|
|
202
|
+
const fs = require('fs');
|
|
203
|
+
const path = require('path');
|
|
190
204
|
const claudeDir = path.join(process.cwd(), '.claude');
|
|
191
205
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
192
206
|
const settingsFile = path.join(claudeDir, 'settings.json');
|
|
@@ -197,11 +211,24 @@ export const COMMANDS = {
|
|
|
197
211
|
},
|
|
198
212
|
},
|
|
199
213
|
|
|
200
|
-
'/login': {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
214
|
+
'/login': {
|
|
215
|
+
description: 'Set API key',
|
|
216
|
+
handler(args) {
|
|
217
|
+
if (args) {
|
|
218
|
+
process.env.ANTHROPIC_API_KEY = args;
|
|
219
|
+
return 'API key set.';
|
|
220
|
+
}
|
|
221
|
+
return 'Usage: /login <api-key>';
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
'/logout': {
|
|
226
|
+
description: 'Clear API key',
|
|
227
|
+
handler() {
|
|
228
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
229
|
+
return 'API key cleared.';
|
|
230
|
+
},
|
|
231
|
+
},
|
|
205
232
|
|
|
206
233
|
'/status': {
|
|
207
234
|
description: 'Show session status',
|
|
@@ -359,9 +386,10 @@ export const COMMANDS = {
|
|
|
359
386
|
'/schedule': {
|
|
360
387
|
description: 'List scheduled tasks',
|
|
361
388
|
handler() {
|
|
362
|
-
|
|
389
|
+
const { cronStore } = require('../tools/cron-create.mjs');
|
|
390
|
+
if (!cronStore || cronStore.size === 0) return 'No scheduled tasks.';
|
|
363
391
|
const lines = [];
|
|
364
|
-
for (const [, job] of
|
|
392
|
+
for (const [, job] of cronStore) {
|
|
365
393
|
lines.push(` ${job.id}: ${job.name} (${job.schedule})`);
|
|
366
394
|
}
|
|
367
395
|
return `Scheduled:\n${lines.join('\n')}`;
|
|
@@ -395,9 +423,8 @@ export const COMMANDS = {
|
|
|
395
423
|
description: 'Show git diff',
|
|
396
424
|
handler() {
|
|
397
425
|
try {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
return d.trim() || 'No changes.';
|
|
426
|
+
const { execSync } = require('child_process');
|
|
427
|
+
return execSync('git diff --stat 2>/dev/null || echo "Not in a git repo"', { encoding: 'utf-8' });
|
|
401
428
|
} catch {
|
|
402
429
|
return 'Unable to show diff.';
|
|
403
430
|
}
|
|
@@ -416,7 +443,7 @@ export const COMMANDS = {
|
|
|
416
443
|
description: 'Create a git commit with AI message',
|
|
417
444
|
handler(args) {
|
|
418
445
|
try {
|
|
419
|
-
|
|
446
|
+
const { execSync } = require('child_process');
|
|
420
447
|
const msg = args || 'Update from open-claude-code';
|
|
421
448
|
execSync('git add -A', { encoding: 'utf-8' });
|
|
422
449
|
execSync(`git commit -m "${msg}"`, { encoding: 'utf-8' });
|
|
@@ -427,45 +454,27 @@ export const COMMANDS = {
|
|
|
427
454
|
},
|
|
428
455
|
},
|
|
429
456
|
|
|
430
|
-
'/pr': {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
description: 'Resume a saved session',
|
|
435
|
-
handler(args, state) {
|
|
436
|
-
const session = getSession();
|
|
437
|
-
if (args) {
|
|
438
|
-
// Resume specific session by ID
|
|
439
|
-
const ok = session.resume(state, args.trim());
|
|
440
|
-
if (ok) {
|
|
441
|
-
return `Resumed session: ${session.sessionId} (${state.messages.length} messages, ${state.turnCount} turns)`;
|
|
442
|
-
}
|
|
443
|
-
return `Could not find session: ${args.trim()}`;
|
|
444
|
-
}
|
|
445
|
-
// Resume most recent
|
|
446
|
-
const ok = session.resume(state);
|
|
447
|
-
if (ok) {
|
|
448
|
-
return `Resumed session: ${session.sessionId} (${state.messages.length} messages, ${state.turnCount} turns)`;
|
|
449
|
-
}
|
|
450
|
-
return 'No resumable session found.';
|
|
457
|
+
'/pr': {
|
|
458
|
+
description: 'Create a pull request (stub)',
|
|
459
|
+
handler() {
|
|
460
|
+
return 'PR creation requires gh CLI. Run: gh pr create --fill';
|
|
451
461
|
},
|
|
452
462
|
},
|
|
453
463
|
|
|
454
|
-
'/
|
|
455
|
-
description: '
|
|
456
|
-
handler(
|
|
457
|
-
|
|
458
|
-
const list = session.listSessions(10);
|
|
459
|
-
if (list.length === 0) return 'No saved sessions.';
|
|
460
|
-
const lines = list.map(s =>
|
|
461
|
-
` ${s.id} | ${s.savedAt || '?'} | ${s.turnCount || 0} turns | ${s.messageCount || 0} msgs`
|
|
462
|
-
);
|
|
463
|
-
return `Recent sessions:\n${lines.join('\n')}`;
|
|
464
|
+
'/release': {
|
|
465
|
+
description: 'Create a release (stub)',
|
|
466
|
+
handler() {
|
|
467
|
+
return 'Release creation requires gh CLI. Run: gh release create <tag>';
|
|
464
468
|
},
|
|
465
469
|
},
|
|
466
470
|
};
|
|
467
471
|
|
|
468
|
-
/**
|
|
472
|
+
/**
|
|
473
|
+
* Execute a slash command.
|
|
474
|
+
* @param {string} input - full command string (e.g., "/model claude-sonnet-4-6")
|
|
475
|
+
* @param {object} state - agent loop state
|
|
476
|
+
* @returns {{ response: string, exit: boolean }}
|
|
477
|
+
*/
|
|
469
478
|
export function executeCommand(input, state) {
|
|
470
479
|
const parts = input.split(/\s+/);
|
|
471
480
|
const cmd = parts[0].toLowerCase();
|
|
@@ -480,7 +489,11 @@ export function executeCommand(input, state) {
|
|
|
480
489
|
return { response, exit: response === 'EXIT' };
|
|
481
490
|
}
|
|
482
491
|
|
|
483
|
-
/**
|
|
492
|
+
/**
|
|
493
|
+
* Get command completions for tab-complete.
|
|
494
|
+
* @param {string} partial
|
|
495
|
+
* @returns {string[]}
|
|
496
|
+
*/
|
|
484
497
|
export function getCompletions(partial) {
|
|
485
498
|
return Object.keys(COMMANDS).filter(c => c.startsWith(partial));
|
|
486
499
|
}
|