klyro 0.1.44 → 0.1.45
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/repl.js +1065 -8
- package/dist/cli/slash/parser.d.ts +182 -0
- package/dist/cli/slash/parser.js +209 -1
- package/dist/tui/app.js +25 -13
- package/package.json +1 -1
package/dist/cli/repl.js
CHANGED
|
@@ -17,6 +17,7 @@ import { buildLevel6Context } from '../context/level6.js';
|
|
|
17
17
|
import { DenyAllApprovalPrompt, StdinApprovalPrompt } from '../policy/approval.js';
|
|
18
18
|
import { TuiApprovalBridge } from '../tui/approval.js';
|
|
19
19
|
import { parseUnifiedDiff } from '../tui/diff-parser.js';
|
|
20
|
+
import { parse } from './slash/parser.js';
|
|
20
21
|
import { resolveProvider, providerHelp } from '../providers.js';
|
|
21
22
|
import { inferProviderFromBaseURL } from '../agent/registry.js';
|
|
22
23
|
import { getDefaultSessionStore } from '../persistence/session.js';
|
|
@@ -40,7 +41,7 @@ export async function startRepl(opts = {}) {
|
|
|
40
41
|
const baseUrl = resolved.baseURL;
|
|
41
42
|
const apiKey = resolved.apiKey;
|
|
42
43
|
let model = opts.model ?? resolved.model;
|
|
43
|
-
|
|
44
|
+
let cwd = opts.cwd ?? process.cwd();
|
|
44
45
|
const registry = builtinRegistry();
|
|
45
46
|
const policy = new PolicyEngine(builtinRules(), DEFAULT_POLICY_CONFIG);
|
|
46
47
|
const providerKind = inferProviderFromBaseURL(baseUrl);
|
|
@@ -60,10 +61,10 @@ export async function startRepl(opts = {}) {
|
|
|
60
61
|
: httpChatAdapter({ baseURL: url, apiKey: key, timeoutMs: 60_000 });
|
|
61
62
|
let adapter = buildAdapter(currentProvider, currentBaseUrl, currentApiKey);
|
|
62
63
|
const ctxBlock = await buildLevel6Context({ cwd });
|
|
63
|
-
|
|
64
|
-
// 4.4 KLYRO.md hierarchy
|
|
64
|
+
let ctxPrefix = ctxBlock.formatted ? `\n\n<context>\n${ctxBlock.formatted}\n</context>` : '';
|
|
65
|
+
// 4.4 KLYRO.md hierarchy (mutable — /reload refreshes)
|
|
65
66
|
const klyroMd = await import('../context/klyro-md.js').then((m) => m.loadKlyroMd(cwd)).catch(() => '');
|
|
66
|
-
|
|
67
|
+
let klyroBlock = klyroMd ? `\n\n<KLYRO.md>\n${klyroMd.slice(0, 4000)}\n</KLYRO.md>` : '';
|
|
67
68
|
// 2.3 layered system prompt
|
|
68
69
|
const systemPromptFn = (_ctx) => {
|
|
69
70
|
const base = buildSystemPrompt({ cwd, model, extraSystem: opts.systemPrompt, appendSystem: ctxPrefix + klyroBlock });
|
|
@@ -146,6 +147,85 @@ export async function startRepl(opts = {}) {
|
|
|
146
147
|
let fastMode = false;
|
|
147
148
|
let displayMode = 'default';
|
|
148
149
|
let lastAssistantText = '';
|
|
150
|
+
// P2 state (commands.md Priority 2)
|
|
151
|
+
let activeAgent = 'default';
|
|
152
|
+
let verboseMode = false;
|
|
153
|
+
let detailsMode = false;
|
|
154
|
+
let rawMode = false;
|
|
155
|
+
let lastPromptText = '';
|
|
156
|
+
const attachedFiles = new Map();
|
|
157
|
+
const bgAgentTasks = [];
|
|
158
|
+
const aliases = new Map();
|
|
159
|
+
const savedPrompts = new Map();
|
|
160
|
+
const AGENT_ROLES = ['default', 'explorer', 'implementer', 'tester', 'reviewer'];
|
|
161
|
+
const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? cwd;
|
|
162
|
+
const aliasFile = `${homeDir}/.klyro/aliases.json`.replace(/\\/g, '/');
|
|
163
|
+
const promptFile = `${homeDir}/.klyro/prompts.json`.replace(/\\/g, '/');
|
|
164
|
+
try {
|
|
165
|
+
const { readFileSync, existsSync } = await import('node:fs');
|
|
166
|
+
if (existsSync(aliasFile)) {
|
|
167
|
+
const raw = JSON.parse(readFileSync(aliasFile, 'utf-8'));
|
|
168
|
+
for (const [k, v] of Object.entries(raw))
|
|
169
|
+
aliases.set(k, v);
|
|
170
|
+
}
|
|
171
|
+
if (existsSync(promptFile)) {
|
|
172
|
+
const raw = JSON.parse(readFileSync(promptFile, 'utf-8'));
|
|
173
|
+
for (const [k, v] of Object.entries(raw))
|
|
174
|
+
savedPrompts.set(k, v);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch { /* best-effort */ }
|
|
178
|
+
function persistMap(file, m) {
|
|
179
|
+
try {
|
|
180
|
+
const fs = require('node:fs');
|
|
181
|
+
const path = require('node:path');
|
|
182
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
183
|
+
fs.writeFileSync(file, JSON.stringify(Object.fromEntries(m), null, 2), 'utf-8');
|
|
184
|
+
}
|
|
185
|
+
catch { /* best-effort */ }
|
|
186
|
+
}
|
|
187
|
+
/** Truncation budget honoring /verbose and /raw. */
|
|
188
|
+
function outCap(s) {
|
|
189
|
+
const cap = rawMode ? 12000 : verboseMode ? 8000 : 4000;
|
|
190
|
+
return s.length > cap ? s.slice(0, cap) + `\n... [truncated ${s.length - cap} chars]` : s;
|
|
191
|
+
}
|
|
192
|
+
async function execShell(command, timeoutMs = 120_000) {
|
|
193
|
+
const r = await registry.execute('shell_exec', { command, timeoutMs }, { cwd, env: process.env, nonInteractive: true });
|
|
194
|
+
if (!r.ok)
|
|
195
|
+
throw new Error(r.error.message ?? 'shell failed');
|
|
196
|
+
return r.value;
|
|
197
|
+
}
|
|
198
|
+
/** Read-only LLM answer (no tools) — powers /ask and /explain. */
|
|
199
|
+
async function answerReadOnly(question, context) {
|
|
200
|
+
const sys = systemPromptFn({ cwd, telemetry: '' }) + '\n\nAnswer read-only: do not call tools, do not edit files.';
|
|
201
|
+
const userText = context ? `${question}\n\n<context>\n${context.slice(0, 6000)}\n</context>` : question;
|
|
202
|
+
const req = {
|
|
203
|
+
model,
|
|
204
|
+
system: sys,
|
|
205
|
+
messages: [{ role: 'user', content: [{ kind: 'text', text: userText }] }],
|
|
206
|
+
tools: [],
|
|
207
|
+
signal: ac.signal,
|
|
208
|
+
};
|
|
209
|
+
queuedStatus({ status: 'running', step: 0, model });
|
|
210
|
+
let text = '';
|
|
211
|
+
try {
|
|
212
|
+
for await (const ev of adapter.stream(req)) {
|
|
213
|
+
if (ev.kind === 'text_delta') {
|
|
214
|
+
text += ev.text;
|
|
215
|
+
queuedDelta(ev.text);
|
|
216
|
+
}
|
|
217
|
+
else if (ev.kind === 'error')
|
|
218
|
+
throw new Error(ev.message);
|
|
219
|
+
}
|
|
220
|
+
lastAssistantText = text;
|
|
221
|
+
queuedStatus({ status: 'done' });
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
225
|
+
queuedAppend({ id: `ro-err-${Date.now()}`, kind: 'error', message: msg });
|
|
226
|
+
queuedStatus({ status: 'error', errorMessage: msg });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
149
229
|
function queuedClear() {
|
|
150
230
|
if (isMounted && directHooks)
|
|
151
231
|
directHooks.clearTranscript();
|
|
@@ -162,6 +242,7 @@ export async function startRepl(opts = {}) {
|
|
|
162
242
|
approvalBridge: tuiBridge,
|
|
163
243
|
isFullscreen: isAltScreen,
|
|
164
244
|
onPrompt: async (text) => {
|
|
245
|
+
lastPromptText = text;
|
|
165
246
|
inflight = runWithBridge(text);
|
|
166
247
|
await inflight;
|
|
167
248
|
inflight = null;
|
|
@@ -422,7 +503,11 @@ export async function startRepl(opts = {}) {
|
|
|
422
503
|
' project: /init /status /context /diff /plan [task] /todos /memory',
|
|
423
504
|
' perms: /permissions /mode [m] /sandbox [dir] /approve /deny',
|
|
424
505
|
' app: /login /logout /auth /version /update /cancel /shell (!cmd) /mention (@path) /tools /config /doctor',
|
|
425
|
-
'
|
|
506
|
+
' P2: /review /code-review /security-review /simplify /test /lint /build /run /fix /explain /format /ask',
|
|
507
|
+
' /undo /redo /rewind /checkpoint /accept /reject /details /verbose /raw /activity /tasks /queue /retry',
|
|
508
|
+
' /mcp /agents /subtask /background /attach /files /ls /tree /search /web /read /map /tokens',
|
|
509
|
+
' /commit /push /pull /pr /issue /theme /debug /whoami /reload /reset /prompt /alias /commands /env /deps /install',
|
|
510
|
+
' (!cmd runs shell, @path attaches a file; /commands lists everything)',
|
|
426
511
|
`provider: ${currentProvider} model: ${model} effort: ${effortLevel}${fastMode ? ' fast' : ''} (${currentMaxSteps} steps) mode: ${displayMode} cwd: ${cwd}${sessionLabel ? ` session: ${sessionLabel}` : ''}${currentBranch ? ` branch: ${currentBranch}` : ''}`,
|
|
427
512
|
].join('\n');
|
|
428
513
|
queuedAppend({ id: `help-${Date.now()}`, kind: 'text', text: helpText, role: 'assistant' });
|
|
@@ -498,7 +583,7 @@ export async function startRepl(opts = {}) {
|
|
|
498
583
|
});
|
|
499
584
|
}
|
|
500
585
|
else {
|
|
501
|
-
queuedAppend({ id: `stat2-${Date.now()}`, kind: 'text', text: `model: ${model} provider: ${currentProvider} effort: ${effortLevel} (${currentMaxSteps} steps) cwd: ${cwd}`, role: 'assistant' });
|
|
586
|
+
queuedAppend({ id: `stat2-${Date.now()}`, kind: 'text', text: `model: ${model} provider: ${currentProvider} effort: ${effortLevel}${fastMode ? '+fast' : ''} (${currentMaxSteps} steps) mode: ${displayMode} agent: ${activeAgent} cwd: ${cwd}${sessionLabel ? ` session: ${sessionLabel}` : ''}${currentBranch ? ` branch: ${currentBranch}` : ''}`, role: 'assistant' });
|
|
502
587
|
}
|
|
503
588
|
return;
|
|
504
589
|
}
|
|
@@ -1018,17 +1103,989 @@ export async function startRepl(opts = {}) {
|
|
|
1018
1103
|
queuedAppend({ id: `set-${Date.now()}`, kind: 'text', text: `config: ${getConfigPath()} (alias of /config)`, role: 'assistant' });
|
|
1019
1104
|
return;
|
|
1020
1105
|
}
|
|
1106
|
+
case 'review': {
|
|
1107
|
+
try {
|
|
1108
|
+
const r = await registry.execute('git_diff', {}, { cwd, env: process.env, nonInteractive: true });
|
|
1109
|
+
if (!r.ok) {
|
|
1110
|
+
queuedAppend({ id: `rev-err-${Date.now()}`, kind: 'error', message: `review failed: ${r.error.message ?? 'git_diff error'}` });
|
|
1111
|
+
}
|
|
1112
|
+
else {
|
|
1113
|
+
const v = r.value;
|
|
1114
|
+
const head = v.patchedFiles.length === 0 ? 'Working tree clean — nothing to review.' : `Reviewing ${v.patchedFiles.length} file(s): ${v.patchedFiles.join(', ')}`;
|
|
1115
|
+
let extra = '';
|
|
1116
|
+
if (cmd.target) {
|
|
1117
|
+
try {
|
|
1118
|
+
const fr = await registry.execute('read_file', { path: cmd.target }, { cwd, env: process.env, nonInteractive: true });
|
|
1119
|
+
if (fr.ok)
|
|
1120
|
+
extra = `\n\n--- ${cmd.target} ---\n${String(fr.value.content ?? '').slice(0, 2000)}`;
|
|
1121
|
+
}
|
|
1122
|
+
catch { /* ignore */ }
|
|
1123
|
+
}
|
|
1124
|
+
queuedAppend({ id: `rev-${Date.now()}`, kind: 'text', text: `${head}\n${v.stat.slice(0, 1500)}${extra}\n\n${outCap(v.diff).slice(0, 3000)}`, role: 'assistant' });
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
catch (err) {
|
|
1128
|
+
queuedAppend({ id: `rev-err2-${Date.now()}`, kind: 'error', message: `review failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1129
|
+
}
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
case 'code-review': {
|
|
1133
|
+
try {
|
|
1134
|
+
const { checkImports } = await import('../verification/scoped.js');
|
|
1135
|
+
const r = await registry.execute('git_diff', {}, { cwd, env: process.env, nonInteractive: true });
|
|
1136
|
+
if (!r.ok) {
|
|
1137
|
+
queuedAppend({ id: `cr-err-${Date.now()}`, kind: 'error', message: 'code-review failed: no diff available' });
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
const v = r.value;
|
|
1141
|
+
const findings = [];
|
|
1142
|
+
for (const f of v.patchedFiles.slice(0, 10)) {
|
|
1143
|
+
const ic = checkImports(cwd, f);
|
|
1144
|
+
for (const m of ic.missing)
|
|
1145
|
+
findings.push(`missing import '${m}' in ${f}`);
|
|
1146
|
+
if (f.length > 200)
|
|
1147
|
+
findings.push(`suspicious path length: ${f}`);
|
|
1148
|
+
}
|
|
1149
|
+
if (/(^|\/)\.env(\.|$)/.test(v.diff))
|
|
1150
|
+
findings.push('.env content in diff — never commit secrets');
|
|
1151
|
+
if (/console\.log|debugger/.test(v.diff))
|
|
1152
|
+
findings.push('debug leftovers (console.log/debugger) in diff');
|
|
1153
|
+
if (/\.skip\(|\.todo\(|xit\(|xtest\(/.test(v.diff))
|
|
1154
|
+
findings.push('skipped tests in diff');
|
|
1155
|
+
const verdict = findings.length === 0 ? 'No issues found.' : `Findings (${findings.length}):\n- ${findings.join('\n- ')}`;
|
|
1156
|
+
queuedAppend({ id: `cr-${Date.now()}`, kind: 'text', text: `Code review — ${v.patchedFiles.length} file(s)${cmd.options ? ` (${cmd.options})` : ''}:\n${v.stat.slice(0, 1000)}\n\n${verdict}`, role: 'assistant' });
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
catch (err) {
|
|
1160
|
+
queuedAppend({ id: `cr-err2-${Date.now()}`, kind: 'error', message: `code-review failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1161
|
+
}
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
case 'security-review': {
|
|
1165
|
+
try {
|
|
1166
|
+
const r = await registry.execute('git_diff', {}, { cwd, env: process.env, nonInteractive: true });
|
|
1167
|
+
const diff = r.ok ? r.value.diff : '';
|
|
1168
|
+
const checks = [
|
|
1169
|
+
[/sk-[A-Za-z0-9]{8,}|sk-ant-[A-Za-z0-9-]{8,}|AKIA[0-9A-Z]{10,}/, 'possible hardcoded secret in diff'],
|
|
1170
|
+
[/(^|\/)\.env(\.|$)/, '.env content in diff'],
|
|
1171
|
+
[/\beval\s*\(/, 'eval() usage in diff'],
|
|
1172
|
+
[/rm\s+-rf\s+(\/|~|\*)/, 'dangerous rm -rf in diff'],
|
|
1173
|
+
[/chmod\s+-R\s+777/, 'chmod 777 in diff'],
|
|
1174
|
+
[/curl.*\|\s*(sh|bash)/i, 'curl|sh pipe in diff'],
|
|
1175
|
+
[/password\s*=\s*["'][^"']+["']/i, 'hardcoded password in diff'],
|
|
1176
|
+
];
|
|
1177
|
+
const hits = checks.filter(([re]) => re.test(diff)).map(([, msg]) => msg);
|
|
1178
|
+
queuedAppend({ id: `sr-${Date.now()}`, kind: 'text', text: hits.length === 0 ? 'Security review: no issues found in working-tree diff.' : `Security review findings:\n- ${hits.join('\n- ')}`, role: 'assistant' });
|
|
1179
|
+
}
|
|
1180
|
+
catch (err) {
|
|
1181
|
+
queuedAppend({ id: `sr-err-${Date.now()}`, kind: 'error', message: `security-review failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1182
|
+
}
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
case 'simplify': {
|
|
1186
|
+
await runWithBridge(`Simplify ${cmd.target ?? 'recent changes'}: refactor for clarity without changing behavior. Keep the diff minimal.`);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
case 'test': {
|
|
1190
|
+
try {
|
|
1191
|
+
const { primaryVerifyCommand } = await import('../verification/registry.js');
|
|
1192
|
+
const { buildScopedCommand } = await import('../verification/scoped.js');
|
|
1193
|
+
const { verify } = await import('../verification/engine.js');
|
|
1194
|
+
const base = primaryVerifyCommand(cwd);
|
|
1195
|
+
if (!base) {
|
|
1196
|
+
queuedAppend({ id: `tst-${Date.now()}`, kind: 'text', text: 'No test command detected. Try /verify or run tests manually.', role: 'assistant' });
|
|
1197
|
+
}
|
|
1198
|
+
else {
|
|
1199
|
+
const scoped = cmd.target ? buildScopedCommand(cwd, base, [cmd.target]) ?? base : base;
|
|
1200
|
+
queuedAppend({ id: `tst-run-${Date.now()}`, kind: 'text', text: `[test] running \`${scoped}\`...`, role: 'assistant' });
|
|
1201
|
+
const res = await verify({ cwd, command: scoped, timeoutMs: 120_000 });
|
|
1202
|
+
queuedAppend({ id: `tst-res-${Date.now()}`, kind: 'text', text: res.ok ? `[test] passed (${scoped})` : `[test] failed:\n${outCap(res.stderr || res.stdout)}`, role: 'assistant' });
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
catch (err) {
|
|
1206
|
+
queuedAppend({ id: `tst-err-${Date.now()}`, kind: 'error', message: `test failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1207
|
+
}
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
case 'lint': {
|
|
1211
|
+
try {
|
|
1212
|
+
const { readFileSync, existsSync } = await import('node:fs');
|
|
1213
|
+
const { join } = await import('node:path');
|
|
1214
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'));
|
|
1215
|
+
let lintCmd = null;
|
|
1216
|
+
if (pkg.scripts?.lint)
|
|
1217
|
+
lintCmd = 'npm run lint --silent';
|
|
1218
|
+
else if (existsSync(join(cwd, 'eslint.config.js')) || existsSync(join(cwd, '.eslintrc.json')))
|
|
1219
|
+
lintCmd = 'npx eslint .';
|
|
1220
|
+
else
|
|
1221
|
+
lintCmd = 'npx tsc --noEmit';
|
|
1222
|
+
queuedAppend({ id: `lint-run-${Date.now()}`, kind: 'text', text: `[lint] running \`${lintCmd}\`...`, role: 'assistant' });
|
|
1223
|
+
const v = await execShell(lintCmd);
|
|
1224
|
+
queuedAppend({ id: `lint-res-${Date.now()}`, kind: 'text', text: v.exitCode === 0 ? `[lint] clean (${lintCmd})` : `[lint] issues (exit ${v.exitCode}):\n${outCap(v.stdout + v.stderr)}`, role: 'assistant' });
|
|
1225
|
+
}
|
|
1226
|
+
catch (err) {
|
|
1227
|
+
queuedAppend({ id: `lint-err-${Date.now()}`, kind: 'error', message: `lint failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1228
|
+
}
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
case 'build': {
|
|
1232
|
+
try {
|
|
1233
|
+
const { readFileSync } = await import('node:fs');
|
|
1234
|
+
const { join } = await import('node:path');
|
|
1235
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'));
|
|
1236
|
+
if (!pkg.scripts?.build) {
|
|
1237
|
+
queuedAppend({ id: `bld-${Date.now()}`, kind: 'text', text: 'No build script in package.json.', role: 'assistant' });
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
queuedAppend({ id: `bld-run-${Date.now()}`, kind: 'text', text: '[build] running `npm run build`...', role: 'assistant' });
|
|
1241
|
+
const v = await execShell('npm run build', 300_000);
|
|
1242
|
+
queuedAppend({ id: `bld-res-${Date.now()}`, kind: 'text', text: v.exitCode === 0 ? '[build] succeeded' : `[build] failed (exit ${v.exitCode}):\n${outCap(v.stdout + v.stderr)}`, role: 'assistant' });
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
catch (err) {
|
|
1246
|
+
queuedAppend({ id: `bld-err-${Date.now()}`, kind: 'error', message: `build failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1247
|
+
}
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
case 'run': {
|
|
1251
|
+
if (!cmd.command) {
|
|
1252
|
+
queuedAppend({ id: `run-${Date.now()}`, kind: 'text', text: 'usage: /run <command>', role: 'assistant' });
|
|
1253
|
+
}
|
|
1254
|
+
else {
|
|
1255
|
+
try {
|
|
1256
|
+
const v = await execShell(cmd.command, 300_000);
|
|
1257
|
+
queuedAppend({ id: `run2-${Date.now()}`, kind: 'text', text: `$ ${cmd.command}\nexit ${v.exitCode}\n${outCap(v.stdout + (v.stderr ? `\n[stderr]\n${v.stderr}` : '') || '(no output)')}`, role: 'assistant' });
|
|
1258
|
+
}
|
|
1259
|
+
catch (err) {
|
|
1260
|
+
queuedAppend({ id: `run-err-${Date.now()}`, kind: 'error', message: `run failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
case 'fix': {
|
|
1266
|
+
await runWithBridge(`Fix ${cmd.target ?? 'failing tests and lint errors'}: reproduce the failure, fix the source (do not edit test assertions unless the test itself is wrong), then verify.`);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
case 'explain': {
|
|
1270
|
+
if (!cmd.target) {
|
|
1271
|
+
queuedAppend({ id: `exp-${Date.now()}`, kind: 'text', text: 'usage: /explain <file|symbol>', role: 'assistant' });
|
|
1272
|
+
}
|
|
1273
|
+
else {
|
|
1274
|
+
let ctx = '';
|
|
1275
|
+
try {
|
|
1276
|
+
const fr = await registry.execute('read_file', { path: cmd.target }, { cwd, env: process.env, nonInteractive: true });
|
|
1277
|
+
if (fr.ok)
|
|
1278
|
+
ctx = `File ${cmd.target}:\n${String(fr.value.content ?? '').slice(0, 6000)}`;
|
|
1279
|
+
}
|
|
1280
|
+
catch { /* symbol — answer without file context */ }
|
|
1281
|
+
await answerReadOnly(`Explain ${cmd.target}: what it does, key logic, and gotchas.`, ctx || undefined);
|
|
1282
|
+
}
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
case 'format': {
|
|
1286
|
+
try {
|
|
1287
|
+
const v = await execShell('git status --porcelain');
|
|
1288
|
+
const files = v.stdout.split('\n').map((l) => l.slice(3).trim()).filter((f) => /\.(ts|tsx|js|jsx|json|md)$/.test(f)).slice(0, 20);
|
|
1289
|
+
if (files.length === 0) {
|
|
1290
|
+
queuedAppend({ id: `fmt-${Date.now()}`, kind: 'text', text: 'Nothing to format (no changed source files).', role: 'assistant' });
|
|
1291
|
+
}
|
|
1292
|
+
else {
|
|
1293
|
+
const check = await execShell('npx --no-install prettier --version').catch(() => null);
|
|
1294
|
+
if (!check || check.exitCode !== 0) {
|
|
1295
|
+
queuedAppend({ id: `fmt2-${Date.now()}`, kind: 'text', text: 'prettier not installed — run `npm i -D prettier` first.', role: 'assistant' });
|
|
1296
|
+
}
|
|
1297
|
+
else {
|
|
1298
|
+
const fv = await execShell(`npx prettier --write ${files.map((f) => `"${f}"`).join(' ')}`);
|
|
1299
|
+
queuedAppend({ id: `fmt3-${Date.now()}`, kind: 'text', text: fv.exitCode === 0 ? `[format] formatted ${files.length} file(s)` : `[format] failed:\n${outCap(fv.stderr || fv.stdout)}`, role: 'assistant' });
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
catch (err) {
|
|
1304
|
+
queuedAppend({ id: `fmt-err-${Date.now()}`, kind: 'error', message: `format failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1305
|
+
}
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
case 'ask': {
|
|
1309
|
+
if (!cmd.question) {
|
|
1310
|
+
queuedAppend({ id: `ask-${Date.now()}`, kind: 'text', text: 'usage: /ask <question> (read-only, no edits)', role: 'assistant' });
|
|
1311
|
+
}
|
|
1312
|
+
else {
|
|
1313
|
+
await answerReadOnly(cmd.question);
|
|
1314
|
+
}
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
case 'redo': {
|
|
1318
|
+
queuedAppend({ id: `redo-${Date.now()}`, kind: 'text', text: 'No redo stack — checkpoints support /undo and /rewind only.', role: 'assistant' });
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
case 'checkpoint': {
|
|
1322
|
+
try {
|
|
1323
|
+
const { snapshot } = await import('../checkpoints/store.js');
|
|
1324
|
+
const v = await execShell('git status --porcelain');
|
|
1325
|
+
const files = v.stdout.split('\n').map((l) => l.slice(3).trim()).filter(Boolean);
|
|
1326
|
+
if (files.length === 0) {
|
|
1327
|
+
queuedAppend({ id: `ckpt-${Date.now()}`, kind: 'text', text: 'Working tree clean — nothing to checkpoint.', role: 'assistant' });
|
|
1328
|
+
}
|
|
1329
|
+
else {
|
|
1330
|
+
const id = await snapshot(cwd, files.slice(0, 50));
|
|
1331
|
+
queuedAppend({ id: `ckpt2-${Date.now()}`, kind: 'text', text: `checkpoint ${String(id).slice(0, 8)} — ${files.length} file(s)`, role: 'assistant' });
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
catch (err) {
|
|
1335
|
+
queuedAppend({ id: `ckpt-err-${Date.now()}`, kind: 'error', message: `checkpoint failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1336
|
+
}
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
case 'accept': {
|
|
1340
|
+
const ok = tuiBridge.resolve('allow');
|
|
1341
|
+
queuedAppend({ id: `acc-${Date.now()}`, kind: 'text', text: ok ? 'accepted pending edits' : 'no pending edits to accept', role: 'assistant' });
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
case 'reject': {
|
|
1345
|
+
const ok = tuiBridge.resolve('deny');
|
|
1346
|
+
queuedAppend({ id: `rej-${Date.now()}`, kind: 'text', text: ok ? 'rejected pending edits' : 'no pending edits to reject', role: 'assistant' });
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
case 'details': {
|
|
1350
|
+
detailsMode = !detailsMode;
|
|
1351
|
+
queuedAppend({ id: `det-${Date.now()}`, kind: 'text', text: `detailed activity: ${detailsMode ? 'on (tool groups expanded by default — use ctrl+o)' : 'off'}`, role: 'assistant' });
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
case 'verbose': {
|
|
1355
|
+
verboseMode = !verboseMode;
|
|
1356
|
+
queuedAppend({ id: `verb-${Date.now()}`, kind: 'text', text: `verbose output: ${verboseMode ? 'on (8k output cap)' : 'off (4k output cap)'}`, role: 'assistant' });
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
case 'raw': {
|
|
1360
|
+
rawMode = !rawMode;
|
|
1361
|
+
queuedAppend({ id: `raw-${Date.now()}`, kind: 'text', text: `raw output: ${rawMode ? 'on (12k cap, no truncation notes)' : 'off'}`, role: 'assistant' });
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
case 'activity': {
|
|
1365
|
+
const s = lastStatus;
|
|
1366
|
+
const line = s ? `status=${s.status} step=${s.step}/${s.maxSteps} model=${s.model} tokens=${s.usageInput + s.usageOutput}` : `status=idle model=${model}`;
|
|
1367
|
+
queuedAppend({ id: `act-${Date.now()}`, kind: 'text', text: `Activity: ${line}${inflight ? ' (task running)' : ''}${tuiSessionId ? ` session=${tuiSessionId.slice(0, 8)}` : ''}`, role: 'assistant' });
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
case 'tasks':
|
|
1371
|
+
case 'ps': {
|
|
1372
|
+
const { listJobs } = await import('../tools/shell/background.js');
|
|
1373
|
+
const jobs = listJobs();
|
|
1374
|
+
if (jobs.length === 0)
|
|
1375
|
+
queuedAppend({ id: `tasks-${Date.now()}`, kind: 'text', text: 'No background jobs', role: 'assistant' });
|
|
1376
|
+
else
|
|
1377
|
+
queuedAppend({ id: `tasks2-${Date.now()}`, kind: 'text', text: `Background jobs:\n${jobs.map((j) => ` ${j.id.slice(0, 12)} [${j.running ? 'running' : 'done'}] ${j.command}`).join('\n')}\nstop via /stop <id>`, role: 'assistant' });
|
|
1378
|
+
if (bgAgentTasks.length > 0) {
|
|
1379
|
+
queuedAppend({ id: `tasks3-${Date.now()}`, kind: 'text', text: `Agent tasks:\n${bgAgentTasks.map((t) => ` ${t.id} [${t.status}] ${t.task.slice(0, 80)}`).join('\n')}`, role: 'assistant' });
|
|
1380
|
+
}
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
case 'stop':
|
|
1384
|
+
case 'kill': {
|
|
1385
|
+
const id = cmd.id?.trim();
|
|
1386
|
+
const { listJobs, killJob } = await import('../tools/shell/background.js');
|
|
1387
|
+
if (!id) {
|
|
1388
|
+
const jobs = listJobs();
|
|
1389
|
+
queuedAppend({ id: `stop-${Date.now()}`, kind: 'text', text: jobs.length === 0 ? 'No background jobs.\nusage: /stop <id>' : `usage: /stop <id>\njobs:\n${jobs.map((j) => ` ${j.id.slice(0, 12)} ${j.command}`).join('\n')}`, role: 'assistant' });
|
|
1390
|
+
}
|
|
1391
|
+
else {
|
|
1392
|
+
const match = listJobs().find((j) => j.id.startsWith(id)) ?? listJobs().find((j) => j.id.includes(id));
|
|
1393
|
+
const ag = bgAgentTasks.find((t) => t.id.startsWith(id));
|
|
1394
|
+
if (match) {
|
|
1395
|
+
try {
|
|
1396
|
+
killJob(match.id);
|
|
1397
|
+
queuedAppend({ id: `stop2-${Date.now()}`, kind: 'text', text: `stopped ${match.id.slice(0, 12)}`, role: 'assistant' });
|
|
1398
|
+
}
|
|
1399
|
+
catch (err) {
|
|
1400
|
+
queuedAppend({ id: `stop-err-${Date.now()}`, kind: 'error', message: String(err) });
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
else if (ag) {
|
|
1404
|
+
ag.status = 'stopped';
|
|
1405
|
+
queuedAppend({ id: `stop3-${Date.now()}`, kind: 'text', text: `marked agent task ${ag.id} stopped (in-flight loop finishes current step)`, role: 'assistant' });
|
|
1406
|
+
}
|
|
1407
|
+
else {
|
|
1408
|
+
queuedAppend({ id: `stop-err2-${Date.now()}`, kind: 'error', message: `no job: ${id}` });
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
case 'queue': {
|
|
1414
|
+
const pending = bgAgentTasks.filter((t) => t.status === 'running');
|
|
1415
|
+
queuedAppend({ id: `queue-${Date.now()}`, kind: 'text', text: pending.length === 0 ? 'Queue empty (input queue lives in the TUI — enter to queue while running, esc to drop).' : `Queued/running agent tasks:\n${pending.map((t) => ` ${t.id}: ${t.task.slice(0, 80)}`).join('\n')}`, role: 'assistant' });
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
case 'retry': {
|
|
1419
|
+
if (!lastPromptText) {
|
|
1420
|
+
queuedAppend({ id: `retry-err-${Date.now()}`, kind: 'error', message: 'nothing to retry yet' });
|
|
1421
|
+
}
|
|
1422
|
+
else {
|
|
1423
|
+
queuedAppend({ id: `retry-${Date.now()}`, kind: 'text', text: `retrying: ${lastPromptText.slice(0, 120)}`, role: 'assistant' });
|
|
1424
|
+
await runWithBridge(lastPromptText);
|
|
1425
|
+
}
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
case 'mcp': {
|
|
1429
|
+
try {
|
|
1430
|
+
const { readFileSync, existsSync } = await import('node:fs');
|
|
1431
|
+
const { join } = await import('node:path');
|
|
1432
|
+
const sub = cmd.sub?.trim().split(/\s+/)[0] ?? 'list';
|
|
1433
|
+
const arg = cmd.sub?.trim().split(/\s+/).slice(1).join(' ');
|
|
1434
|
+
const cfgPath = join(cwd, '.mcp.json');
|
|
1435
|
+
if (!existsSync(cfgPath)) {
|
|
1436
|
+
queuedAppend({ id: `mcp-${Date.now()}`, kind: 'text', text: 'No MCP servers configured (no .mcp.json). Add servers to .mcp.json.', role: 'assistant' });
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
const cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
|
|
1440
|
+
const servers = cfg.servers ?? {};
|
|
1441
|
+
const names = Object.keys(servers);
|
|
1442
|
+
if (sub === 'list' || sub === 'status' || !sub) {
|
|
1443
|
+
queuedAppend({ id: `mcp2-${Date.now()}`, kind: 'text', text: names.length === 0 ? 'MCP: .mcp.json has no servers.' : `MCP servers (${names.length}, lazy-connect):\n${names.map((n) => ` ${servers[n]?.disabled ? '[disabled]' : '[enabled] '} ${n}${servers[n]?.url ? ` ${servers[n].url}` : ''}`).join('\n')}`, role: 'assistant' });
|
|
1444
|
+
}
|
|
1445
|
+
else if ((sub === 'enable' || sub === 'disable') && arg) {
|
|
1446
|
+
if (!servers[arg]) {
|
|
1447
|
+
queuedAppend({ id: `mcp-err-${Date.now()}`, kind: 'error', message: `no MCP server: ${arg}` });
|
|
1448
|
+
}
|
|
1449
|
+
else {
|
|
1450
|
+
servers[arg].disabled = sub === 'disable';
|
|
1451
|
+
const { writeFileSync } = await import('node:fs');
|
|
1452
|
+
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
1453
|
+
queuedAppend({ id: `mcp3-${Date.now()}`, kind: 'text', text: `MCP server ${arg} ${sub}d`, role: 'assistant' });
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
else if (sub === 'reconnect' && arg) {
|
|
1457
|
+
queuedAppend({ id: `mcp4-${Date.now()}`, kind: 'text', text: servers[arg] ? `MCP ${arg}: reconnect queued (connections are lazy — next tool use reconnects)` : `no MCP server: ${arg}`, role: 'assistant' });
|
|
1458
|
+
}
|
|
1459
|
+
else {
|
|
1460
|
+
queuedAppend({ id: `mcp5-${Date.now()}`, kind: 'text', text: 'usage: /mcp [list|status|enable <n>|disable <n>|reconnect <n>]', role: 'assistant' });
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
catch (err) {
|
|
1464
|
+
queuedAppend({ id: `mcp-err2-${Date.now()}`, kind: 'error', message: `mcp failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1465
|
+
}
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
case 'agents': {
|
|
1469
|
+
queuedAppend({ id: `agents-${Date.now()}`, kind: 'text', text: `Agents (active: ${activeAgent}):\n${AGENT_ROLES.map((a) => ` ${a === activeAgent ? '*' : ' '} ${a}`).join('\n')}\nswitch via /agent <name>, spawn via /subtask <task>`, role: 'assistant' });
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
case 'agent': {
|
|
1473
|
+
const n = cmd.name?.trim().toLowerCase();
|
|
1474
|
+
if (!n) {
|
|
1475
|
+
queuedAppend({ id: `agent-${Date.now()}`, kind: 'text', text: `active agent: ${activeAgent}\nusage: /agent <${AGENT_ROLES.join('|')}>`, role: 'assistant' });
|
|
1476
|
+
}
|
|
1477
|
+
else if (!AGENT_ROLES.includes(n)) {
|
|
1478
|
+
queuedAppend({ id: `agent-err-${Date.now()}`, kind: 'error', message: `unknown agent: ${n} (expected ${AGENT_ROLES.join('|')})` });
|
|
1479
|
+
}
|
|
1480
|
+
else {
|
|
1481
|
+
activeAgent = n;
|
|
1482
|
+
queuedAppend({ id: `agent2-${Date.now()}`, kind: 'text', text: `active agent: ${n} (role label for future tasks)`, role: 'assistant' });
|
|
1483
|
+
}
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
case 'subagents': {
|
|
1487
|
+
queuedAppend({ id: `subagents-${Date.now()}`, kind: 'text', text: `Subagents: ${AGENT_ROLES.filter((a) => a !== 'default').join(', ')}\nspawn via /subtask <task> — runs a full agent loop and reports back.`, role: 'assistant' });
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
case 'subtask': {
|
|
1491
|
+
if (!cmd.task) {
|
|
1492
|
+
queuedAppend({ id: `subtask-${Date.now()}`, kind: 'text', text: 'usage: /subtask <task>', role: 'assistant' });
|
|
1493
|
+
}
|
|
1494
|
+
else {
|
|
1495
|
+
queuedAppend({ id: `subtask-run-${Date.now()}`, kind: 'text', text: `[subagent:${activeAgent}] starting: ${cmd.task.slice(0, 120)}`, role: 'assistant' });
|
|
1496
|
+
await runWithBridge(`[subagent task] ${cmd.task}`);
|
|
1497
|
+
}
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
case 'background': {
|
|
1501
|
+
if (!cmd.task) {
|
|
1502
|
+
queuedAppend({ id: `bg-${Date.now()}`, kind: 'text', text: bgAgentTasks.length === 0 ? 'No agent background tasks.\nusage: /background <task>' : `Agent background tasks:\n${bgAgentTasks.map((t) => ` ${t.id} [${t.status}] ${t.task.slice(0, 80)}`).join('\n')}`, role: 'assistant' });
|
|
1503
|
+
}
|
|
1504
|
+
else {
|
|
1505
|
+
const id = `bg-${Date.now().toString(36)}`;
|
|
1506
|
+
bgAgentTasks.push({ id, task: cmd.task, status: 'running' });
|
|
1507
|
+
queuedAppend({ id: `bg-run-${Date.now()}`, kind: 'text', text: `[background] ${id} started: ${cmd.task.slice(0, 120)}`, role: 'assistant' });
|
|
1508
|
+
void runWithBridge(cmd.task).then(() => {
|
|
1509
|
+
const t = bgAgentTasks.find((x) => x.id === id);
|
|
1510
|
+
if (t && t.status === 'running')
|
|
1511
|
+
t.status = 'done';
|
|
1512
|
+
}).catch(() => {
|
|
1513
|
+
const t = bgAgentTasks.find((x) => x.id === id);
|
|
1514
|
+
if (t && t.status === 'running')
|
|
1515
|
+
t.status = 'failed';
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
case 'add-dir': {
|
|
1521
|
+
const { existsSync, statSync } = await import('node:fs');
|
|
1522
|
+
const { resolve } = await import('node:path');
|
|
1523
|
+
const p = cmd.path?.trim();
|
|
1524
|
+
if (!p) {
|
|
1525
|
+
queuedAppend({ id: `adddir-${Date.now()}`, kind: 'text', text: 'usage: /add-dir <path>', role: 'assistant' });
|
|
1526
|
+
}
|
|
1527
|
+
else {
|
|
1528
|
+
const abs = resolve(cwd, p);
|
|
1529
|
+
try {
|
|
1530
|
+
if (!existsSync(abs) || !statSync(abs).isDirectory()) {
|
|
1531
|
+
queuedAppend({ id: `adddir-err-${Date.now()}`, kind: 'error', message: `not a directory: ${p}` });
|
|
1532
|
+
}
|
|
1533
|
+
else {
|
|
1534
|
+
const cfg = policy.config;
|
|
1535
|
+
if (!cfg.additionalDirs?.includes(abs))
|
|
1536
|
+
cfg.additionalDirs = [...(cfg.additionalDirs ?? []), abs];
|
|
1537
|
+
queuedAppend({ id: `adddir2-${Date.now()}`, kind: 'text', text: `added allowed directory: ${abs}`, role: 'assistant' });
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
catch (err) {
|
|
1541
|
+
queuedAppend({ id: `adddir-err2-${Date.now()}`, kind: 'error', message: String(err) });
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
case 'cd': {
|
|
1547
|
+
const { existsSync, statSync } = await import('node:fs');
|
|
1548
|
+
const { resolve } = await import('node:path');
|
|
1549
|
+
const p = cmd.path?.trim();
|
|
1550
|
+
if (!p) {
|
|
1551
|
+
queuedAppend({ id: `cd-${Date.now()}`, kind: 'text', text: `cwd: ${cwd}\nusage: /cd <path>`, role: 'assistant' });
|
|
1552
|
+
}
|
|
1553
|
+
else {
|
|
1554
|
+
const abs = resolve(cwd, p);
|
|
1555
|
+
if (!existsSync(abs) || !statSync(abs).isDirectory()) {
|
|
1556
|
+
queuedAppend({ id: `cd-err-${Date.now()}`, kind: 'error', message: `not a directory: ${p}` });
|
|
1557
|
+
}
|
|
1558
|
+
else {
|
|
1559
|
+
cwd = abs;
|
|
1560
|
+
queuedAppend({ id: `cd2-${Date.now()}`, kind: 'text', text: `cwd → ${abs} (header refreshes on restart)`, role: 'assistant' });
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
case 'attach': {
|
|
1566
|
+
if (!cmd.file) {
|
|
1567
|
+
queuedAppend({ id: `att-${Date.now()}`, kind: 'text', text: 'usage: /attach <file>', role: 'assistant' });
|
|
1568
|
+
}
|
|
1569
|
+
else {
|
|
1570
|
+
try {
|
|
1571
|
+
const r = await registry.execute('read_file', { path: cmd.file }, { cwd, env: process.env, nonInteractive: true });
|
|
1572
|
+
if (!r.ok) {
|
|
1573
|
+
queuedAppend({ id: `att-err-${Date.now()}`, kind: 'error', message: `attach failed: ${r.error.message ?? 'read error'}` });
|
|
1574
|
+
}
|
|
1575
|
+
else {
|
|
1576
|
+
const body = String(r.value.content ?? '');
|
|
1577
|
+
attachedFiles.set(cmd.file, body.length);
|
|
1578
|
+
queuedAppend({ id: `att2-${Date.now()}`, kind: 'text', text: `attached ${cmd.file} (${body.length} chars) — in context for future prompts`, role: 'assistant' });
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
catch (err) {
|
|
1582
|
+
queuedAppend({ id: `att-err2-${Date.now()}`, kind: 'error', message: String(err) });
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
case 'drop': {
|
|
1588
|
+
if (!cmd.file) {
|
|
1589
|
+
attachedFiles.clear();
|
|
1590
|
+
queuedAppend({ id: `drop-${Date.now()}`, kind: 'text', text: 'dropped all attached files', role: 'assistant' });
|
|
1591
|
+
}
|
|
1592
|
+
else if (attachedFiles.delete(cmd.file)) {
|
|
1593
|
+
queuedAppend({ id: `drop2-${Date.now()}`, kind: 'text', text: `dropped ${cmd.file}`, role: 'assistant' });
|
|
1594
|
+
}
|
|
1595
|
+
else {
|
|
1596
|
+
queuedAppend({ id: `drop-err-${Date.now()}`, kind: 'error', message: `not attached: ${cmd.file}` });
|
|
1597
|
+
}
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
case 'files': {
|
|
1601
|
+
queuedAppend({ id: `files-${Date.now()}`, kind: 'text', text: attachedFiles.size === 0 ? 'No files in context (attach via /attach, @path, or /mention).' : `Files in context:\n${[...attachedFiles.entries()].map(([f, n]) => ` ${f} (${n} chars)`).join('\n')}`, role: 'assistant' });
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
case 'image': {
|
|
1605
|
+
if (!cmd.path) {
|
|
1606
|
+
queuedAppend({ id: `img-${Date.now()}`, kind: 'text', text: 'usage: /image <path> (or @<image> inline in a prompt)', role: 'assistant' });
|
|
1607
|
+
}
|
|
1608
|
+
else {
|
|
1609
|
+
attachedFiles.set(cmd.path, 0);
|
|
1610
|
+
queuedAppend({ id: `img2-${Date.now()}`, kind: 'text', text: `attached image ${cmd.path} — reference it in your next prompt`, role: 'assistant' });
|
|
1611
|
+
}
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
case 'paste': {
|
|
1615
|
+
try {
|
|
1616
|
+
const { execSync } = await import('node:child_process');
|
|
1617
|
+
const probe = process.platform === 'win32' ? 'powershell -NoProfile -Command Get-Clipboard' : process.platform === 'darwin' ? 'pbpaste' : 'xclip -o -selection clipboard';
|
|
1618
|
+
const text = execSync(probe, { encoding: 'utf-8' }).trim();
|
|
1619
|
+
if (!text) {
|
|
1620
|
+
queuedAppend({ id: `paste-${Date.now()}`, kind: 'text', text: 'Clipboard is empty.', role: 'assistant' });
|
|
1621
|
+
}
|
|
1622
|
+
else {
|
|
1623
|
+
queuedAppend({ id: `paste2-${Date.now()}`, kind: 'text', text: `pasted ${text.length} chars into context:\n${text.slice(0, 3000)}`, role: 'assistant' });
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
catch {
|
|
1627
|
+
queuedAppend({ id: `paste-err-${Date.now()}`, kind: 'error', message: 'clipboard unavailable on this system' });
|
|
1628
|
+
}
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
case 'ls': {
|
|
1632
|
+
try {
|
|
1633
|
+
const r = await registry.execute('list_directory', { path: cmd.path || '.' }, { cwd, env: process.env, nonInteractive: true });
|
|
1634
|
+
if (!r.ok) {
|
|
1635
|
+
queuedAppend({ id: `ls-err-${Date.now()}`, kind: 'error', message: `ls failed: ${r.error.message ?? 'error'}` });
|
|
1636
|
+
}
|
|
1637
|
+
else {
|
|
1638
|
+
const v = r.value;
|
|
1639
|
+
const lines = (v.entries ?? []).map((e) => ` ${e.type === 'directory' ? e.name + '/' : e.name}`);
|
|
1640
|
+
queuedAppend({ id: `ls2-${Date.now()}`, kind: 'text', text: `${cmd.path || '.'}:\n${outCap(lines.join('\n') || '(empty)')}`, role: 'assistant' });
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
catch (err) {
|
|
1644
|
+
queuedAppend({ id: `ls-err2-${Date.now()}`, kind: 'error', message: String(err) });
|
|
1645
|
+
}
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
case 'tree': {
|
|
1649
|
+
try {
|
|
1650
|
+
const { readdirSync, statSync } = await import('node:fs');
|
|
1651
|
+
const { join, relative } = await import('node:path');
|
|
1652
|
+
const root = cmd.path ? join(cwd, cmd.path) : cwd;
|
|
1653
|
+
const out = [];
|
|
1654
|
+
const skip = new Set(['node_modules', '.git', 'dist', '.klyro', '.next']);
|
|
1655
|
+
const walk = (dir, depth) => {
|
|
1656
|
+
if (depth > 3 || out.length > 100)
|
|
1657
|
+
return;
|
|
1658
|
+
let entries = [];
|
|
1659
|
+
try {
|
|
1660
|
+
entries = readdirSync(dir);
|
|
1661
|
+
}
|
|
1662
|
+
catch {
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
for (const e of entries) {
|
|
1666
|
+
if (skip.has(e))
|
|
1667
|
+
continue;
|
|
1668
|
+
const full = join(dir, e);
|
|
1669
|
+
let isDir = false;
|
|
1670
|
+
try {
|
|
1671
|
+
isDir = statSync(full).isDirectory();
|
|
1672
|
+
}
|
|
1673
|
+
catch {
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
out.push(`${' '.repeat(depth)}${isDir ? e + '/' : e}`);
|
|
1677
|
+
if (isDir)
|
|
1678
|
+
walk(full, depth + 1);
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
walk(root, 0);
|
|
1682
|
+
queuedAppend({ id: `tree-${Date.now()}`, kind: 'text', text: `${relative(cwd, root) || '.'}/\n${out.join('\n').slice(0, 4000)}`, role: 'assistant' });
|
|
1683
|
+
}
|
|
1684
|
+
catch (err) {
|
|
1685
|
+
queuedAppend({ id: `tree-err-${Date.now()}`, kind: 'error', message: String(err) });
|
|
1686
|
+
}
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
case 'search': {
|
|
1690
|
+
if (!cmd.query) {
|
|
1691
|
+
queuedAppend({ id: `search-${Date.now()}`, kind: 'text', text: 'usage: /search <query>', role: 'assistant' });
|
|
1692
|
+
}
|
|
1693
|
+
else {
|
|
1694
|
+
try {
|
|
1695
|
+
const r = await registry.execute('grep', { pattern: cmd.query, maxResults: 50 }, { cwd, env: process.env, nonInteractive: true });
|
|
1696
|
+
if (!r.ok) {
|
|
1697
|
+
queuedAppend({ id: `search-err-${Date.now()}`, kind: 'error', message: `search failed: ${r.error.message ?? 'error'}` });
|
|
1698
|
+
}
|
|
1699
|
+
else {
|
|
1700
|
+
queuedAppend({ id: `search2-${Date.now()}`, kind: 'text', text: `Results for "${cmd.query}":\n${outCap(JSON.stringify(r.value, null, 2))}`, role: 'assistant' });
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
catch (err) {
|
|
1704
|
+
queuedAppend({ id: `search-err2-${Date.now()}`, kind: 'error', message: String(err) });
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
case 'web': {
|
|
1710
|
+
if (!cmd.url) {
|
|
1711
|
+
queuedAppend({ id: `web-${Date.now()}`, kind: 'text', text: 'usage: /web <url>', role: 'assistant' });
|
|
1712
|
+
}
|
|
1713
|
+
else {
|
|
1714
|
+
try {
|
|
1715
|
+
const ctrl = new AbortController();
|
|
1716
|
+
const t = setTimeout(() => ctrl.abort(), 15_000);
|
|
1717
|
+
const res = await fetch(cmd.url, { signal: ctrl.signal });
|
|
1718
|
+
clearTimeout(t);
|
|
1719
|
+
const text = (await res.text()).replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 6000);
|
|
1720
|
+
queuedAppend({ id: `web2-${Date.now()}`, kind: 'text', text: `${cmd.url} [${res.status}]:\n${text || '(no text content)'}`, role: 'assistant' });
|
|
1721
|
+
}
|
|
1722
|
+
catch (err) {
|
|
1723
|
+
queuedAppend({ id: `web-err-${Date.now()}`, kind: 'error', message: `fetch failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
case 'read': {
|
|
1729
|
+
if (!cmd.path) {
|
|
1730
|
+
queuedAppend({ id: `read-${Date.now()}`, kind: 'text', text: 'usage: /read <path>', role: 'assistant' });
|
|
1731
|
+
}
|
|
1732
|
+
else {
|
|
1733
|
+
try {
|
|
1734
|
+
const r = await registry.execute('read_file', { path: cmd.path }, { cwd, env: process.env, nonInteractive: true });
|
|
1735
|
+
if (!r.ok) {
|
|
1736
|
+
queuedAppend({ id: `read-err-${Date.now()}`, kind: 'error', message: `read failed: ${r.error.message ?? 'error'}` });
|
|
1737
|
+
}
|
|
1738
|
+
else {
|
|
1739
|
+
queuedAppend({ id: `read2-${Date.now()}`, kind: 'text', text: `${cmd.path}:\n${outCap(String(r.value.content ?? ''))}`, role: 'assistant' });
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
catch (err) {
|
|
1743
|
+
queuedAppend({ id: `read-err2-${Date.now()}`, kind: 'error', message: String(err) });
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
case 'map': {
|
|
1749
|
+
try {
|
|
1750
|
+
const r = await registry.execute('repo_map', {}, { cwd, env: process.env, nonInteractive: true });
|
|
1751
|
+
if (!r.ok) {
|
|
1752
|
+
queuedAppend({ id: `map-err-${Date.now()}`, kind: 'error', message: `map failed: ${r.error.message ?? 'error'}` });
|
|
1753
|
+
}
|
|
1754
|
+
else {
|
|
1755
|
+
queuedAppend({ id: `map2-${Date.now()}`, kind: 'text', text: `Repository map:\n${outCap(typeof r.value === 'string' ? r.value : JSON.stringify(r.value, null, 2))}`, role: 'assistant' });
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
catch (err) {
|
|
1759
|
+
queuedAppend({ id: `map-err2-${Date.now()}`, kind: 'error', message: String(err) });
|
|
1760
|
+
}
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
case 'tokens': {
|
|
1764
|
+
const { getModelInfo } = await import('../providers/model-info.js');
|
|
1765
|
+
const info = getModelInfo(model);
|
|
1766
|
+
const inp = lastStatus?.usageInput ?? 0;
|
|
1767
|
+
const outp = lastStatus?.usageOutput ?? 0;
|
|
1768
|
+
const total = inp + outp;
|
|
1769
|
+
const pct = ((total / info.contextWindow) * 100).toFixed(1);
|
|
1770
|
+
queuedAppend({ id: `tokens-${Date.now()}`, kind: 'text', text: `Tokens — ${model} (window ${info.contextWindow.toLocaleString()}):\n in ${inp.toLocaleString()} / out ${outp.toLocaleString()} / total ${total.toLocaleString()} (${pct}%)`, role: 'assistant' });
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
case 'commit': {
|
|
1774
|
+
if (!cmd.message) {
|
|
1775
|
+
queuedAppend({ id: `commit-${Date.now()}`, kind: 'text', text: 'usage: /commit <message> (commits staged changes only — stage with git add first)', role: 'assistant' });
|
|
1776
|
+
}
|
|
1777
|
+
else {
|
|
1778
|
+
try {
|
|
1779
|
+
const st = await execShell('git status --porcelain');
|
|
1780
|
+
const staged = st.stdout.split('\n').filter((l) => /^[MADRC]/.test(l));
|
|
1781
|
+
if (staged.length === 0) {
|
|
1782
|
+
queuedAppend({ id: `commit2-${Date.now()}`, kind: 'text', text: 'Nothing staged — stage changes with `git add` first.', role: 'assistant' });
|
|
1783
|
+
}
|
|
1784
|
+
else {
|
|
1785
|
+
const safeMsg = cmd.message.replace(/"/g, "'");
|
|
1786
|
+
const v = await execShell(`git commit -m "${safeMsg}"`);
|
|
1787
|
+
queuedAppend({ id: `commit3-${Date.now()}`, kind: 'text', text: v.exitCode === 0 ? `committed ${staged.length} file(s):\n${outCap(v.stdout)}` : `commit failed:\n${outCap(v.stderr || v.stdout)}`, role: 'assistant' });
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
catch (err) {
|
|
1791
|
+
queuedAppend({ id: `commit-err-${Date.now()}`, kind: 'error', message: String(err) });
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
case 'push': {
|
|
1797
|
+
try {
|
|
1798
|
+
queuedAppend({ id: `push-run-${Date.now()}`, kind: 'text', text: '[push] running `git push`...', role: 'assistant' });
|
|
1799
|
+
const v = await execShell('git push', 300_000);
|
|
1800
|
+
queuedAppend({ id: `push-res-${Date.now()}`, kind: 'text', text: v.exitCode === 0 ? `[push] done:\n${outCap(v.stdout + v.stderr)}` : `[push] failed:\n${outCap(v.stderr || v.stdout)}`, role: 'assistant' });
|
|
1801
|
+
}
|
|
1802
|
+
catch (err) {
|
|
1803
|
+
queuedAppend({ id: `push-err-${Date.now()}`, kind: 'error', message: String(err) });
|
|
1804
|
+
}
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
case 'pull': {
|
|
1808
|
+
try {
|
|
1809
|
+
const v = await execShell('git pull --ff-only', 300_000);
|
|
1810
|
+
queuedAppend({ id: `pull-${Date.now()}`, kind: 'text', text: v.exitCode === 0 ? `[pull] done:\n${outCap(v.stdout + v.stderr)}` : `[pull] failed:\n${outCap(v.stderr || v.stdout)}`, role: 'assistant' });
|
|
1811
|
+
}
|
|
1812
|
+
catch (err) {
|
|
1813
|
+
queuedAppend({ id: `pull-err-${Date.now()}`, kind: 'error', message: String(err) });
|
|
1814
|
+
}
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
case 'pr': {
|
|
1818
|
+
try {
|
|
1819
|
+
const hasGh = await execShell('gh --version').then(() => true).catch(() => false);
|
|
1820
|
+
if (!hasGh) {
|
|
1821
|
+
queuedAppend({ id: `pr-${Date.now()}`, kind: 'text', text: 'gh CLI not installed — install from https://cli.github.com then use /pr [create|status|view].', role: 'assistant' });
|
|
1822
|
+
}
|
|
1823
|
+
else {
|
|
1824
|
+
const v = await execShell(`gh pr ${cmd.args || 'status'}`);
|
|
1825
|
+
queuedAppend({ id: `pr2-${Date.now()}`, kind: 'text', text: outCap(v.stdout + v.stderr) || '(no output)', role: 'assistant' });
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
catch (err) {
|
|
1829
|
+
queuedAppend({ id: `pr-err-${Date.now()}`, kind: 'error', message: String(err) });
|
|
1830
|
+
}
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
case 'issue': {
|
|
1834
|
+
try {
|
|
1835
|
+
const hasGh = await execShell('gh --version').then(() => true).catch(() => false);
|
|
1836
|
+
if (!hasGh) {
|
|
1837
|
+
queuedAppend({ id: `issue-${Date.now()}`, kind: 'text', text: 'gh CLI not installed — install from https://cli.github.com.', role: 'assistant' });
|
|
1838
|
+
}
|
|
1839
|
+
else {
|
|
1840
|
+
const v = await execShell(cmd.id ? `gh issue view ${cmd.id}` : 'gh issue status');
|
|
1841
|
+
queuedAppend({ id: `issue2-${Date.now()}`, kind: 'text', text: outCap(v.stdout + v.stderr) || '(no output)', role: 'assistant' });
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
catch (err) {
|
|
1845
|
+
queuedAppend({ id: `issue-err-${Date.now()}`, kind: 'error', message: String(err) });
|
|
1846
|
+
}
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
case 'editor': {
|
|
1850
|
+
const file = cmd.file?.trim();
|
|
1851
|
+
const ed = process.env.EDITOR ?? process.env.VISUAL ?? (process.platform === 'win32' ? 'notepad' : 'vi');
|
|
1852
|
+
if (!file) {
|
|
1853
|
+
queuedAppend({ id: `ed-${Date.now()}`, kind: 'text', text: `editor: ${ed}\nusage: /editor <file>`, role: 'assistant' });
|
|
1854
|
+
}
|
|
1855
|
+
else {
|
|
1856
|
+
try {
|
|
1857
|
+
const { startBackground } = await import('../tools/shell/background.js');
|
|
1858
|
+
const { resolve } = await import('node:path');
|
|
1859
|
+
const abs = resolve(cwd, file);
|
|
1860
|
+
const opener = process.platform === 'win32' ? `start "" "${abs}"` : process.platform === 'darwin' ? `open "${abs}"` : `xdg-open "${abs}"`;
|
|
1861
|
+
startBackground(opener, cwd);
|
|
1862
|
+
queuedAppend({ id: `ed2-${Date.now()}`, kind: 'text', text: `opened ${abs} in background`, role: 'assistant' });
|
|
1863
|
+
}
|
|
1864
|
+
catch (err) {
|
|
1865
|
+
queuedAppend({ id: `ed-err-${Date.now()}`, kind: 'error', message: String(err) });
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
case 'keymap':
|
|
1871
|
+
case 'vim':
|
|
1872
|
+
case 'theme':
|
|
1873
|
+
case 'statusline':
|
|
1874
|
+
case 'output-style': {
|
|
1875
|
+
const key = cmd.kind === 'keymap' ? 'klyro.keymap' : cmd.kind === 'vim' ? 'klyro.vim' : cmd.kind === 'theme' ? 'klyro.theme' : cmd.kind === 'statusline' ? 'klyro.statusline' : 'klyro.outputStyle';
|
|
1876
|
+
const val = (cmd.kind === 'keymap' ? cmd.name : cmd.kind === 'vim' ? cmd.state : cmd.kind === 'theme' ? cmd.name : cmd.kind === 'statusline' ? cmd.format : cmd.style)?.trim();
|
|
1877
|
+
const { runConfig } = await import('./config.js');
|
|
1878
|
+
const capture = async (args) => {
|
|
1879
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
1880
|
+
let out = '';
|
|
1881
|
+
process.stdout.write = ((c) => { out += String(c); return true; });
|
|
1882
|
+
try {
|
|
1883
|
+
await runConfig(args);
|
|
1884
|
+
}
|
|
1885
|
+
finally {
|
|
1886
|
+
process.stdout.write = orig;
|
|
1887
|
+
}
|
|
1888
|
+
return out;
|
|
1889
|
+
};
|
|
1890
|
+
if (!val) {
|
|
1891
|
+
const out = await capture(['get', key]);
|
|
1892
|
+
queuedAppend({ id: `${cmd.kind}-${Date.now()}`, kind: 'text', text: out.trim() || `${cmd.kind}: (not set)\nusage: /${cmd.kind} <value>`, role: 'assistant' });
|
|
1893
|
+
}
|
|
1894
|
+
else {
|
|
1895
|
+
await capture(['set', key, val]);
|
|
1896
|
+
queuedAppend({ id: `${cmd.kind}2-${Date.now()}`, kind: 'text', text: `${cmd.kind} set to ${val}`, role: 'assistant' });
|
|
1897
|
+
}
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
case 'debug': {
|
|
1901
|
+
const info = [
|
|
1902
|
+
`klyro debug:`,
|
|
1903
|
+
` node ${process.version} platform ${process.platform}/${process.arch}`,
|
|
1904
|
+
` cwd ${cwd}`,
|
|
1905
|
+
` provider ${currentProvider} ${currentBaseUrl}`,
|
|
1906
|
+
` model ${model} effort ${effortLevel} maxSteps ${currentMaxSteps} fast ${fastMode ? 'on' : 'off'}`,
|
|
1907
|
+
` mode ${displayMode} agent ${activeAgent}`,
|
|
1908
|
+
` apiKey: ${currentApiKey ? 'set (' + currentApiKey.length + ' chars)' : 'empty'}`,
|
|
1909
|
+
` session ${tuiSessionId?.slice(0, 8) ?? '(none)'} attached ${attachedFiles.size} aliases ${aliases.size} prompts ${savedPrompts.size}`,
|
|
1910
|
+
];
|
|
1911
|
+
queuedAppend({ id: `debug-${Date.now()}`, kind: 'text', text: info.join('\n'), role: 'assistant' });
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
case 'whoami': {
|
|
1915
|
+
let user = process.env.USER ?? process.env.USERNAME ?? 'unknown';
|
|
1916
|
+
try {
|
|
1917
|
+
user = (await import('node:os')).userInfo().username;
|
|
1918
|
+
}
|
|
1919
|
+
catch { /* keep env */ }
|
|
1920
|
+
queuedAppend({ id: `who-${Date.now()}`, kind: 'text', text: `user: ${user}\nprovider: ${currentProvider} (${currentBaseUrl})\nmodel: ${model}`, role: 'assistant' });
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
case 'reload': {
|
|
1924
|
+
try {
|
|
1925
|
+
const ctx = await buildLevel6Context({ cwd });
|
|
1926
|
+
ctxPrefix = ctx.formatted ? `\n\n<context>\n${ctx.formatted}\n</context>` : '';
|
|
1927
|
+
const md = await import('../context/klyro-md.js').then((m) => m.loadKlyroMd(cwd)).catch(() => '');
|
|
1928
|
+
klyroBlock = md ? `\n\n<KLYRO.md>\n${md.slice(0, 4000)}\n</KLYRO.md>` : '';
|
|
1929
|
+
queuedAppend({ id: `reload-${Date.now()}`, kind: 'text', text: 'reloaded project context + KLYRO.md', role: 'assistant' });
|
|
1930
|
+
}
|
|
1931
|
+
catch (err) {
|
|
1932
|
+
queuedAppend({ id: `reload-err-${Date.now()}`, kind: 'error', message: String(err) });
|
|
1933
|
+
}
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
case 'reset': {
|
|
1937
|
+
effortLevel = 'medium';
|
|
1938
|
+
currentMaxSteps = EFFORT_STEPS.medium;
|
|
1939
|
+
fastMode = false;
|
|
1940
|
+
displayMode = 'default';
|
|
1941
|
+
policy.config.mode = 'default';
|
|
1942
|
+
activeAgent = 'default';
|
|
1943
|
+
verboseMode = false;
|
|
1944
|
+
detailsMode = false;
|
|
1945
|
+
rawMode = false;
|
|
1946
|
+
queuedStatus({ maxSteps: currentMaxSteps });
|
|
1947
|
+
queuedAppend({ id: `reset-${Date.now()}`, kind: 'text', text: 'settings reset to defaults (effort medium, mode default, agent default)', role: 'assistant' });
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
case 'bug': {
|
|
1951
|
+
queuedAppend({ id: `bug-${Date.now()}`, kind: 'text', text: `Report a bug: https://github.com/Siddu-lingampelli/Klyro/issues\nInclude: klyro --version, node ${process.version}, provider ${currentProvider}, steps to reproduce.`, role: 'assistant' });
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
case 'changelog': {
|
|
1955
|
+
try {
|
|
1956
|
+
const v = await execShell('git log --oneline -15');
|
|
1957
|
+
queuedAppend({ id: `cl-${Date.now()}`, kind: 'text', text: v.exitCode === 0 && v.stdout.trim() ? `Recent changes:\n${v.stdout.slice(0, 3000)}` : 'No git history here — see npm klyro versions for releases.', role: 'assistant' });
|
|
1958
|
+
}
|
|
1959
|
+
catch (err) {
|
|
1960
|
+
queuedAppend({ id: `cl-err-${Date.now()}`, kind: 'error', message: String(err) });
|
|
1961
|
+
}
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
case 'promptcmd': {
|
|
1965
|
+
const rest = cmd.args?.trim() ?? '';
|
|
1966
|
+
if (!rest) {
|
|
1967
|
+
queuedAppend({ id: `pc-${Date.now()}`, kind: 'text', text: savedPrompts.size === 0 ? 'No saved prompts.\nusage: /prompt save <name> <text> | /prompt <name>' : `Saved prompts:\n${[...savedPrompts.keys()].map((k) => ` ${k}`).join('\n')}\nrun via /prompt <name>`, role: 'assistant' });
|
|
1968
|
+
}
|
|
1969
|
+
else if (rest.startsWith('save ')) {
|
|
1970
|
+
const m = /^save\s+(\S+)\s+([\s\S]+)$/.exec(rest);
|
|
1971
|
+
if (!m) {
|
|
1972
|
+
queuedAppend({ id: `pc-err-${Date.now()}`, kind: 'error', message: 'usage: /prompt save <name> <text>' });
|
|
1973
|
+
}
|
|
1974
|
+
else {
|
|
1975
|
+
savedPrompts.set(m[1], m[2]);
|
|
1976
|
+
persistMap(promptFile, savedPrompts);
|
|
1977
|
+
queuedAppend({ id: `pc2-${Date.now()}`, kind: 'text', text: `saved prompt "${m[1]}"`, role: 'assistant' });
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
else {
|
|
1981
|
+
const name = rest.split(/\s+/)[0];
|
|
1982
|
+
const text = savedPrompts.get(name);
|
|
1983
|
+
if (!text) {
|
|
1984
|
+
queuedAppend({ id: `pc-err2-${Date.now()}`, kind: 'error', message: `no saved prompt: ${name}` });
|
|
1985
|
+
}
|
|
1986
|
+
else {
|
|
1987
|
+
await runWithBridge(text);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
case 'alias': {
|
|
1993
|
+
const rest = cmd.args?.trim() ?? '';
|
|
1994
|
+
if (!rest) {
|
|
1995
|
+
queuedAppend({ id: `al-${Date.now()}`, kind: 'text', text: aliases.size === 0 ? 'No aliases.\nusage: /alias <name> <command>' : `Aliases:\n${[...aliases.entries()].map(([k, v]) => ` /${k} → ${v}`).join('\n')}`, role: 'assistant' });
|
|
1996
|
+
}
|
|
1997
|
+
else {
|
|
1998
|
+
const m = /^(\S+)\s+([\s\S]+)$/.exec(rest);
|
|
1999
|
+
if (!m) {
|
|
2000
|
+
queuedAppend({ id: `al-err-${Date.now()}`, kind: 'error', message: 'usage: /alias <name> <command>' });
|
|
2001
|
+
}
|
|
2002
|
+
else {
|
|
2003
|
+
aliases.set(m[1], m[2]);
|
|
2004
|
+
persistMap(aliasFile, aliases);
|
|
2005
|
+
queuedAppend({ id: `al2-${Date.now()}`, kind: 'text', text: `alias /${m[1]} → ${m[2]}`, role: 'assistant' });
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
case 'commands': {
|
|
2011
|
+
const { COMMAND_DEFS } = await import('./slash/parser.js');
|
|
2012
|
+
const custom = [...aliases.keys()].map((k) => ` /${k} (alias)`);
|
|
2013
|
+
void custom;
|
|
2014
|
+
queuedAppend({ id: `cmds-${Date.now()}`, kind: 'text', text: `Commands (${COMMAND_DEFS.length + aliases.size + savedPrompts.size}):\n${COMMAND_DEFS.map((d) => ` /${d.name} — ${d.hint}`).join('\n')}${aliases.size > 0 ? `\ncustom aliases:\n${[...aliases.entries()].map(([k, v]) => ` /${k} → ${v}`).join('\n')}` : ''}${savedPrompts.size > 0 ? `\nsaved prompts: ${[...savedPrompts.keys()].join(', ')}` : ''}`, role: 'assistant' });
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
case 'env': {
|
|
2018
|
+
const { existsSync } = await import('node:fs');
|
|
2019
|
+
void existsSync;
|
|
2020
|
+
const a = cmd.args?.trim() ?? '';
|
|
2021
|
+
if (!a) {
|
|
2022
|
+
const rows = Object.keys(process.env).filter((k) => k.startsWith('KLYRO_') || ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'NO_COLOR'].includes(k)).map((k) => {
|
|
2023
|
+
const v = process.env[k] ?? '';
|
|
2024
|
+
const secret = /KEY|SECRET|TOKEN/.test(k);
|
|
2025
|
+
return ` ${k}=${secret ? (v ? '(set, ' + v.length + ' chars)' : '(empty)') : v || '(empty)'}`;
|
|
2026
|
+
});
|
|
2027
|
+
queuedAppend({ id: `env-${Date.now()}`, kind: 'text', text: rows.length === 0 ? 'No KLYRO_* env set.\nusage: /env KEY=value (session-only)' : `Environment:\n${rows.join('\n')}\nset via /env KEY=value (session-only)`, role: 'assistant' });
|
|
2028
|
+
}
|
|
2029
|
+
else {
|
|
2030
|
+
const m = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(a);
|
|
2031
|
+
if (!m) {
|
|
2032
|
+
queuedAppend({ id: `env-err-${Date.now()}`, kind: 'error', message: 'usage: /env KEY=value' });
|
|
2033
|
+
}
|
|
2034
|
+
else {
|
|
2035
|
+
process.env[m[1]] = m[2];
|
|
2036
|
+
queuedAppend({ id: `env2-${Date.now()}`, kind: 'text', text: `set ${m[1]} (session-only)`, role: 'assistant' });
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
case 'deps': {
|
|
2042
|
+
try {
|
|
2043
|
+
const { readFileSync } = await import('node:fs');
|
|
2044
|
+
const { join } = await import('node:path');
|
|
2045
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'));
|
|
2046
|
+
const d = Object.entries(pkg.dependencies ?? {}).map(([k, v]) => ` ${k}@${v}`);
|
|
2047
|
+
const dd = Object.entries(pkg.devDependencies ?? {}).map(([k, v]) => ` ${k}@${v} (dev)`);
|
|
2048
|
+
queuedAppend({ id: `deps-${Date.now()}`, kind: 'text', text: d.length + dd.length === 0 ? 'No dependencies in package.json.' : `Dependencies:\n${[...d, ...dd].join('\n').slice(0, 3000)}`, role: 'assistant' });
|
|
2049
|
+
}
|
|
2050
|
+
catch {
|
|
2051
|
+
queuedAppend({ id: `deps-err-${Date.now()}`, kind: 'error', message: 'no package.json in cwd' });
|
|
2052
|
+
}
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
case 'install': {
|
|
2056
|
+
try {
|
|
2057
|
+
const { existsSync } = await import('node:fs');
|
|
2058
|
+
const { join } = await import('node:path');
|
|
2059
|
+
const mgr = existsSync(join(cwd, 'pnpm-lock.yaml')) ? 'pnpm install' : existsSync(join(cwd, 'package-lock.json')) ? 'npm install' : existsSync(join(cwd, 'bun.lockb')) ? 'bun install' : existsSync(join(cwd, 'yarn.lock')) ? 'yarn install' : 'npm install';
|
|
2060
|
+
queuedAppend({ id: `inst-run-${Date.now()}`, kind: 'text', text: `[install] running \`${mgr}\`...`, role: 'assistant' });
|
|
2061
|
+
const v = await execShell(mgr, 600_000);
|
|
2062
|
+
queuedAppend({ id: `inst-res-${Date.now()}`, kind: 'text', text: v.exitCode === 0 ? '[install] done' : `[install] failed:\n${outCap(v.stderr || v.stdout)}`, role: 'assistant' });
|
|
2063
|
+
}
|
|
2064
|
+
catch (err) {
|
|
2065
|
+
queuedAppend({ id: `inst-err-${Date.now()}`, kind: 'error', message: String(err) });
|
|
2066
|
+
}
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
1021
2069
|
case 'prompt': {
|
|
1022
2070
|
// Regular prompts never reach onSlash — no-op for exhaustiveness.
|
|
1023
2071
|
return;
|
|
1024
2072
|
}
|
|
1025
|
-
case 'unknown':
|
|
2073
|
+
case 'unknown': {
|
|
2074
|
+
// Alias expansion: /alias <name> <command> redirects unknown commands
|
|
2075
|
+
const m = /^\/(\S+)(?:\s+([\s\S]*))?$/.exec(cmd.raw.trim());
|
|
2076
|
+
const target = m ? aliases.get(m[1].toLowerCase()) : undefined;
|
|
2077
|
+
if (m && target) {
|
|
2078
|
+
const extra = m[2] ? ` ${m[2]}` : '';
|
|
2079
|
+
await handleSlash(parse(target + extra));
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
1026
2082
|
queuedAppend({
|
|
1027
2083
|
id: `unk-${Date.now()}`,
|
|
1028
2084
|
kind: 'error',
|
|
1029
|
-
message: `unknown command: ${cmd.raw} (try /help)`,
|
|
2085
|
+
message: `unknown command: ${cmd.raw} (try /help or /commands)`,
|
|
1030
2086
|
});
|
|
1031
2087
|
return;
|
|
2088
|
+
}
|
|
1032
2089
|
}
|
|
1033
2090
|
}
|
|
1034
2091
|
// Keep process alive until user quits; resolve on unmount or SIGINT.
|