shmakk 1.2.0 → 1.2.2
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 +68 -2
- package/package.json +2 -2
- package/scripts/demo/record.py +196 -0
- package/scripts/demo/scenes.html +913 -0
- package/skills/media-video-compose.md +320 -0
- package/skills/media-video-script.md +204 -0
- package/skills/media-video-voice.md +184 -0
- package/src/agent-overview.js +320 -0
- package/src/agent-roster.js +53 -0
- package/src/agent.js +178 -18
- package/src/cli.js +220 -86
- package/src/completions.js +3 -1
- package/src/correction.js +11 -4
- package/src/endpoints.js +94 -31
- package/src/guard.js +101 -0
- package/src/index.js +19 -5
- package/src/llm.js +462 -52
- package/src/markdown.js +217 -0
- package/src/notify.js +34 -0
- package/src/pty.js +1 -1
- package/src/review.js +8 -1
- package/src/self-commands.js +108 -2
- package/src/session.js +58 -2
- package/src/ssh.js +255 -0
- package/src/subagent.js +12 -1
- package/src/taskClassifier.js +2 -2
- package/src/team.js +22 -0
- package/src/tools.js +487 -1
- package/src/workflows.js +32 -0
package/src/markdown.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Markdown-to-ANSI renderer for terminal output.
|
|
2
|
+
// Converts common markdown syntax to ANSI escape sequences for
|
|
3
|
+
// bold, italic, headers, lists, blockquotes, links, code, etc.
|
|
4
|
+
//
|
|
5
|
+
// Designed for streaming-friendly line-by-line rendering:
|
|
6
|
+
// renderLine(line, opts) — renders a single line
|
|
7
|
+
// renderBlock(text, opts) — renders a full text block (calls renderLine)
|
|
8
|
+
|
|
9
|
+
// ── ANSI helpers ────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const BOLD = '\x1b[1m';
|
|
12
|
+
const DIM = '\x1b[2m';
|
|
13
|
+
const ITALIC = '\x1b[3m';
|
|
14
|
+
const UNDERLINE = '\x1b[4m';
|
|
15
|
+
const RESET = '\x1b[0m';
|
|
16
|
+
|
|
17
|
+
const COLORS = {
|
|
18
|
+
black: '\x1b[30m',
|
|
19
|
+
red: '\x1b[31m',
|
|
20
|
+
green: '\x1b[32m',
|
|
21
|
+
yellow: '\x1b[33m',
|
|
22
|
+
blue: '\x1b[34m',
|
|
23
|
+
magenta: '\x1b[35m',
|
|
24
|
+
cyan: '\x1b[36m',
|
|
25
|
+
white: '\x1b[37m',
|
|
26
|
+
brightBlack: '\x1b[90m',
|
|
27
|
+
brightRed: '\x1b[91m',
|
|
28
|
+
brightGreen: '\x1b[92m',
|
|
29
|
+
brightYellow: '\x1b[93m',
|
|
30
|
+
brightBlue: '\x1b[94m',
|
|
31
|
+
brightCyan: '\x1b[96m',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ── Inline formatting ───────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function renderInline(text, enabled) {
|
|
37
|
+
if (!enabled) {
|
|
38
|
+
// Strip markdown markers when colors are off
|
|
39
|
+
return text
|
|
40
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, '$1')
|
|
41
|
+
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
42
|
+
.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '$1')
|
|
43
|
+
.replace(/`(.+?)`/g, '$1');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let result = text;
|
|
47
|
+
|
|
48
|
+
// Bold + italic: ***text***
|
|
49
|
+
result = result.replace(/\*\*\*(.+?)\*\*\*/g, `${BOLD}${ITALIC}$1${RESET}`);
|
|
50
|
+
|
|
51
|
+
// Bold: **text**
|
|
52
|
+
result = result.replace(/\*\*(.+?)\*\*/g, `${BOLD}$1${RESET}`);
|
|
53
|
+
|
|
54
|
+
// Italic: *text* (single asterisk, not part of **)
|
|
55
|
+
result = result.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, `${ITALIC}$1${RESET}`);
|
|
56
|
+
|
|
57
|
+
// Inline code: `text`
|
|
58
|
+
result = result.replace(/`([^`]+)`/g, `${COLORS.brightBlack}${BOLD}$1${RESET}`);
|
|
59
|
+
|
|
60
|
+
// Links: [text](url) — keep text, show URL dimmed
|
|
61
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => {
|
|
62
|
+
if (label === url) return `${UNDERLINE}${url}${RESET}`;
|
|
63
|
+
return `${UNDERLINE}${label}${RESET} ${DIM}(${url})${RESET}`;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Block-level rendering ───────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
const HORIZONTAL_RULE_RE = /^[-*_]{3,}\s*$/;
|
|
72
|
+
const HEADER_RE = /^(#{1,6})\s+(.+)$/;
|
|
73
|
+
const UNORDERED_LIST_RE = /^(\s*)[-*+]\s+(.+)$/;
|
|
74
|
+
const ORDERED_LIST_RE = /^(\s*)(\d+)\.\s+(.+)$/;
|
|
75
|
+
const BLOCKQUOTE_RE = /^>\s?(.*)$/;
|
|
76
|
+
const CODE_FENCE_RE = /^```(\S*)$/;
|
|
77
|
+
|
|
78
|
+
function renderLine(line, opts = {}) {
|
|
79
|
+
const { enabled = true, colors = true } = opts;
|
|
80
|
+
const trim = line.trimEnd();
|
|
81
|
+
if (!trim) return '';
|
|
82
|
+
|
|
83
|
+
// Code fence — pass through as-is (handled by renderBlock)
|
|
84
|
+
if (CODE_FENCE_RE.test(trim)) {
|
|
85
|
+
if (!enabled || !colors) return line;
|
|
86
|
+
return `${COLORS.brightBlack}${trim}${RESET}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Horizontal rule
|
|
90
|
+
if (HORIZONTAL_RULE_RE.test(trim)) {
|
|
91
|
+
if (!enabled || !colors) return '---';
|
|
92
|
+
const width = Math.min(process.stdout.columns || 80, 80);
|
|
93
|
+
return DIM + '\u2500'.repeat(width - 1) + RESET;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Headers
|
|
97
|
+
const hMatch = trim.match(HEADER_RE);
|
|
98
|
+
if (hMatch) {
|
|
99
|
+
const level = hMatch[1].length;
|
|
100
|
+
const text = renderInline(hMatch[2], enabled && colors);
|
|
101
|
+
if (!enabled || !colors) {
|
|
102
|
+
// Structural only: uppercase headers
|
|
103
|
+
return level <= 2 ? text.toUpperCase() : text;
|
|
104
|
+
}
|
|
105
|
+
if (level <= 2) {
|
|
106
|
+
return `${BOLD}${UNDERLINE}${text}${RESET}`;
|
|
107
|
+
}
|
|
108
|
+
return `${BOLD}${text}${RESET}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Blockquote
|
|
112
|
+
const bqMatch = trim.match(BLOCKQUOTE_RE);
|
|
113
|
+
if (bqMatch) {
|
|
114
|
+
const text = renderInline(bqMatch[1], enabled && colors);
|
|
115
|
+
if (!enabled || !colors) return ` ${text}`;
|
|
116
|
+
return `${COLORS.brightBlack}\u2502 ${text}${RESET}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Unordered list
|
|
120
|
+
const ulMatch = trim.match(UNORDERED_LIST_RE);
|
|
121
|
+
if (ulMatch) {
|
|
122
|
+
const indent = ulMatch[1];
|
|
123
|
+
const text = renderInline(ulMatch[2], enabled && colors);
|
|
124
|
+
if (!enabled || !colors) return ` ${indent}\u2022 ${text}`;
|
|
125
|
+
return `${indent}${COLORS.cyan}\u2022${RESET} ${text}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Ordered list
|
|
129
|
+
const olMatch = trim.match(ORDERED_LIST_RE);
|
|
130
|
+
if (olMatch) {
|
|
131
|
+
const indent = olMatch[1];
|
|
132
|
+
const num = olMatch[2];
|
|
133
|
+
const text = renderInline(olMatch[3], enabled && colors);
|
|
134
|
+
if (!enabled || !colors) return ` ${indent}${num}. ${text}`;
|
|
135
|
+
return `${indent}${COLORS.cyan}${num}.${RESET} ${text}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Regular line — apply inline formatting
|
|
139
|
+
return renderInline(trim, enabled && colors);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderBlock(text, opts = {}) {
|
|
143
|
+
const { enabled = true, colors = true } = opts;
|
|
144
|
+
const src = String(text || '');
|
|
145
|
+
if (!src) return src;
|
|
146
|
+
|
|
147
|
+
const lines = src.split('\n');
|
|
148
|
+
const out = [];
|
|
149
|
+
let inCodeBlock = false;
|
|
150
|
+
let codeLang = '';
|
|
151
|
+
let codeLines = [];
|
|
152
|
+
|
|
153
|
+
function flushCode() {
|
|
154
|
+
if (!codeLines.length) return;
|
|
155
|
+
if (!enabled || !colors) {
|
|
156
|
+
out.push(`[${codeLang || 'code'}]`);
|
|
157
|
+
for (const l of codeLines) out.push(` ${l}`);
|
|
158
|
+
codeLines = [];
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const head = `${COLORS.brightCyan}${BOLD}${codeLang || 'code'}${RESET}`;
|
|
162
|
+
out.push(head);
|
|
163
|
+
for (const l of codeLines) {
|
|
164
|
+
out.push(`${COLORS.brightBlack}${l}${RESET}`);
|
|
165
|
+
}
|
|
166
|
+
codeLines = [];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < lines.length; i++) {
|
|
170
|
+
const raw = lines[i];
|
|
171
|
+
const fenceMatch = raw.match(CODE_FENCE_RE);
|
|
172
|
+
|
|
173
|
+
if (fenceMatch && !inCodeBlock) {
|
|
174
|
+
// Opening fence
|
|
175
|
+
flushCode();
|
|
176
|
+
// Flush any accumulated regular lines first is handled above
|
|
177
|
+
inCodeBlock = true;
|
|
178
|
+
codeLang = fenceMatch[1] || '';
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (fenceMatch && inCodeBlock) {
|
|
183
|
+
// Closing fence
|
|
184
|
+
flushCode();
|
|
185
|
+
inCodeBlock = false;
|
|
186
|
+
codeLang = '';
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (inCodeBlock) {
|
|
191
|
+
codeLines.push(raw);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Code fences might contain ``` with trailing text that isn't a fence
|
|
196
|
+
// Handle ``` that appears mid-line (unusual, but possible)
|
|
197
|
+
if (raw.startsWith('```') && !fenceMatch) {
|
|
198
|
+
// This is caught by the regex above, but just in case:
|
|
199
|
+
codeLines.push(raw);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const rendered = renderLine(raw, { enabled, colors });
|
|
204
|
+
out.push(rendered);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// If file ends inside a code block, still flush it
|
|
208
|
+
flushCode();
|
|
209
|
+
|
|
210
|
+
return out.join('\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
renderInline,
|
|
215
|
+
renderLine,
|
|
216
|
+
renderBlock,
|
|
217
|
+
};
|
package/src/notify.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Desktop notification support via notify-send (libnotify).
|
|
2
|
+
// Falls back silently if notify-send is not available or no notification
|
|
3
|
+
// daemon is running.
|
|
4
|
+
|
|
5
|
+
const { execFile } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const NOTIFY_BIN = 'notify-send';
|
|
8
|
+
|
|
9
|
+
function available() {
|
|
10
|
+
try {
|
|
11
|
+
const { execFileSync } = require('child_process');
|
|
12
|
+
execFileSync('which', [NOTIFY_BIN], { stdio: 'ignore' });
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Fire a desktop notification. Never throws — failures are silent.
|
|
20
|
+
// Delegates to a subprocess so the main loop isn't blocked.
|
|
21
|
+
function notify(summary, body, urgency) {
|
|
22
|
+
if (!available()) return;
|
|
23
|
+
const args = [summary || 'shmakk'];
|
|
24
|
+
if (body) args.push(body);
|
|
25
|
+
args.push('--app-name=shmakk', '--category=im.received');
|
|
26
|
+
if (urgency === 'critical') args.push('--urgency=critical');
|
|
27
|
+
if (urgency === 'low') args.push('--urgency=low');
|
|
28
|
+
execFile(NOTIFY_BIN, args, (err) => {
|
|
29
|
+
// Silently ignore — notification daemon may not be running.
|
|
30
|
+
void err;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { notify, available };
|
package/src/pty.js
CHANGED
package/src/review.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
// Y/n prompt with cooperative cancellation. The returned `ask` accepts an
|
|
2
2
|
// optional `{ onCancel }` callback that fires when the user hits Ctrl-C.
|
|
3
3
|
|
|
4
|
-
function makePrompter(pty, write) {
|
|
4
|
+
function makePrompter(pty, write, opts = {}) {
|
|
5
|
+
const { onNotify } = opts;
|
|
5
6
|
return function ask(question, defaultYes, { onCancel, onWhy } = {}) {
|
|
6
7
|
return new Promise((resolve) => {
|
|
8
|
+
if (onNotify) {
|
|
9
|
+
try {
|
|
10
|
+
const body = typeof question === 'string' ? question.replace(/\x1b\[[0-9;]*m/g, '') : String(question || '');
|
|
11
|
+
onNotify('shmakk needs your attention', body.slice(0, 120));
|
|
12
|
+
} catch {}
|
|
13
|
+
}
|
|
7
14
|
const tag = defaultYes ? '[Y/n/?]' : '[y/N/?]';
|
|
8
15
|
write(`${question} ${tag} `);
|
|
9
16
|
let buf = '';
|
package/src/self-commands.js
CHANGED
|
@@ -80,6 +80,35 @@ const SELF_COMMANDS = [
|
|
|
80
80
|
action: 'show-plan',
|
|
81
81
|
},
|
|
82
82
|
|
|
83
|
+
// ── Agent overview ──
|
|
84
|
+
{
|
|
85
|
+
patterns: [
|
|
86
|
+
/^(?:show\s+)?(?:agent\s+)?overview$/i,
|
|
87
|
+
/^what\s+(?:agents?|specialists?)\s+(?:are|is)\s+(?:working|running|active)[\s?]*$/i,
|
|
88
|
+
/^who\s+(?:is\s+)?working[\s?]*$/i,
|
|
89
|
+
/^team\s+(?:status|overview)$/i,
|
|
90
|
+
],
|
|
91
|
+
action: 'agent-overview',
|
|
92
|
+
},
|
|
93
|
+
// "agent skills" — show skills used by agents
|
|
94
|
+
{
|
|
95
|
+
patterns: [
|
|
96
|
+
/^agent\s+skills?$/i,
|
|
97
|
+
/^(?:show\s+)?agent\s+skills?$/i,
|
|
98
|
+
],
|
|
99
|
+
action: 'agent-skills',
|
|
100
|
+
},
|
|
101
|
+
// "agent <role|id>" — drill into a specific agent
|
|
102
|
+
{
|
|
103
|
+
patterns: [
|
|
104
|
+
/^agent\s+(\S[\s\S]*)$/i,
|
|
105
|
+
/^show\s+agent\s+(\S[\s\S]*)$/i,
|
|
106
|
+
/^(?:detail|inspect|follow)\s+agent\s+(\S[\s\S]*)$/i,
|
|
107
|
+
],
|
|
108
|
+
action: 'agent-detail',
|
|
109
|
+
needsArg: true,
|
|
110
|
+
},
|
|
111
|
+
|
|
83
112
|
// ── Session ──
|
|
84
113
|
{
|
|
85
114
|
patterns: [/^(?:shmakk\s+)?status$/i],
|
|
@@ -355,6 +384,19 @@ const SELF_COMMANDS = [
|
|
|
355
384
|
],
|
|
356
385
|
action: 'review-edits',
|
|
357
386
|
},
|
|
387
|
+
|
|
388
|
+
// ── Sidebar (meta / out-of-band query) ──
|
|
389
|
+
// "Sidebar: what files did you touch?" runs the agent with full context
|
|
390
|
+
// but the query and response are never added to conversation history.
|
|
391
|
+
// Use this for meta-questions, status checks, and side-channel queries.
|
|
392
|
+
{
|
|
393
|
+
patterns: [
|
|
394
|
+
/^Sidebar:\s*(.+)$/i,
|
|
395
|
+
/^sidebar\s+(.+)$/i,
|
|
396
|
+
],
|
|
397
|
+
action: 'sidebar-query',
|
|
398
|
+
needsArg: true,
|
|
399
|
+
},
|
|
358
400
|
];
|
|
359
401
|
|
|
360
402
|
function matchSelfCommand(input) {
|
|
@@ -404,6 +446,70 @@ function executeSelfCommand(match, write, ctx = {}) {
|
|
|
404
446
|
// ── Plan ──
|
|
405
447
|
case 'show-plan': ctl.showPlan(); break;
|
|
406
448
|
|
|
449
|
+
// ── Agent overview ──
|
|
450
|
+
case 'agent-overview': {
|
|
451
|
+
const overview = require('./agent-overview');
|
|
452
|
+
const agents = overview.getAll();
|
|
453
|
+
const lines = overview.formatOverview(agents);
|
|
454
|
+
for (const line of lines) write(line + '\r\n');
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
case 'agent-detail': {
|
|
458
|
+
const overview = require('./agent-overview');
|
|
459
|
+
const query = (match.arg || '').trim();
|
|
460
|
+
// Try exact id match first, then role match
|
|
461
|
+
let agent = overview.get(query);
|
|
462
|
+
if (!agent) {
|
|
463
|
+
const byRole = overview.findByRole(query);
|
|
464
|
+
if (byRole.length === 1) {
|
|
465
|
+
agent = byRole[0];
|
|
466
|
+
} else if (byRole.length > 1) {
|
|
467
|
+
write(`\x1b[36m[shmakk]\x1b[0m ${byRole.length} agents match "\x1b[1m${query}\x1b[0m":\r\n`);
|
|
468
|
+
for (const a of byRole) {
|
|
469
|
+
const icon = overview.statusIcon(a.status);
|
|
470
|
+
write(` ${icon} \x1b[36m${a.id}\x1b[0m \x1b[2m${a.role} · ${a.status}\x1b[0m\r\n`);
|
|
471
|
+
}
|
|
472
|
+
write(`\r\n\x1b[2mUse "agent <id>" to drill into a specific one.\x1b[0m\r\n`);
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (!agent) {
|
|
477
|
+
write(`\x1b[33m[shmakk] no agent found matching "\x1b[1m${query}\x1b[0m"\r\n`);
|
|
478
|
+
write(`\x1b[2mTry "agent overview" to see all agents.\x1b[0m\r\n`);
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
const lines = overview.formatAgentDetail(agent);
|
|
482
|
+
for (const line of lines) write(line + '\r\n');
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
case 'agent-skills': {
|
|
486
|
+
const overview = require('./agent-overview');
|
|
487
|
+
const agents = overview.getAll();
|
|
488
|
+
if (!agents.length) {
|
|
489
|
+
write('\x1b[2mNo agents registered.\x1b[0m\r\n');
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
const skillMap = new Map();
|
|
493
|
+
for (const a of agents) {
|
|
494
|
+
if (!a.skill) continue;
|
|
495
|
+
if (!skillMap.has(a.skill)) {
|
|
496
|
+
skillMap.set(a.skill, { skill: a.skill, source: a.skillSource, agents: [] });
|
|
497
|
+
}
|
|
498
|
+
skillMap.get(a.skill).agents.push(a.role);
|
|
499
|
+
}
|
|
500
|
+
if (!skillMap.size) {
|
|
501
|
+
write('\x1b[2mNo skills used by agents (all using roster hints).\x1b[0m\r\n');
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
write(`\x1b[1mAgent Skills\x1b[0m\r\n\r\n`);
|
|
505
|
+
for (const [name, info] of skillMap) {
|
|
506
|
+
const roles = [...new Set(info.agents)].join(', ');
|
|
507
|
+
write(` \x1b[36m${name}\x1b[0m \x1b[2mused by: ${roles}\x1b[0m\r\n`);
|
|
508
|
+
if (info.source) write(` \x1b[2msource: ${info.source}\x1b[0m\r\n`);
|
|
509
|
+
}
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
|
|
407
513
|
// ── Session ──
|
|
408
514
|
case 'status': ctl.status(); break;
|
|
409
515
|
case 'stats': ctl.stats(); break;
|
|
@@ -637,10 +743,10 @@ function executeSelfCommand(match, write, ctx = {}) {
|
|
|
637
743
|
const list = listEndpoints(opts.workspace || process.cwd());
|
|
638
744
|
const current = getCurrentEndpointName();
|
|
639
745
|
if (!list.length) {
|
|
640
|
-
write('[shmakk] no endpoints configured in ~/.config/shmakk/endpoints.
|
|
746
|
+
write('[shmakk] no endpoints configured in ~/.config/shmakk/endpoints.json\r\n');
|
|
641
747
|
break;
|
|
642
748
|
}
|
|
643
|
-
write('[shmakk] available endpoints:\r\n');
|
|
749
|
+
write('[shmakk] available model endpoints:\r\n');
|
|
644
750
|
for (const ep of list) {
|
|
645
751
|
const marker = ep === current ? ' \x1b[1m*\x1b[0m ' : ' ';
|
|
646
752
|
write(`${marker}${ep}\r\n`);
|
package/src/session.js
CHANGED
|
@@ -11,6 +11,7 @@ const { clearIndex } = require('./workspace-index');
|
|
|
11
11
|
const { loadGlossary } = require('./glossary');
|
|
12
12
|
const { isConfigured } = require('./llm');
|
|
13
13
|
const { makePrompter, decisionBanner } = require('./review');
|
|
14
|
+
const { notify } = require('./notify');
|
|
14
15
|
const { workspaceWarning } = require('./safety');
|
|
15
16
|
const { createMCPManager } = require('./mcp-client');
|
|
16
17
|
const { clearEdits } = require('./edit-tracker');
|
|
@@ -164,8 +165,11 @@ function makeToolConfirm(opts, ask, out, getAbort) {
|
|
|
164
165
|
async function runOneSession(opts, registerSession) {
|
|
165
166
|
const session = startSession({ debug: opts.debug, voiceEnabled: !!opts.voice });
|
|
166
167
|
let colorsEnabled = opts.colors !== false;
|
|
168
|
+
let markdownEnabled = opts.markdown !== false;
|
|
167
169
|
const out = (s) => session.stdoutWrite(colorsEnabled ? s : stripAnsi(s));
|
|
168
|
-
const ask = makePrompter(session, out
|
|
170
|
+
const ask = makePrompter(session, out, {
|
|
171
|
+
onNotify: opts.notify ? (summary, body) => notify(summary, body) : null,
|
|
172
|
+
});
|
|
169
173
|
const glossary = loadGlossary();
|
|
170
174
|
// Workspace tracking: explicit --workspace is "pinned"; otherwise cwd
|
|
171
175
|
// floats with the inner shell's `cd`. When both pinned and cwd differ,
|
|
@@ -392,6 +396,7 @@ async function runOneSession(opts, registerSession) {
|
|
|
392
396
|
history,
|
|
393
397
|
profile: opts.profile || voiceRouting.profile || 'balanced',
|
|
394
398
|
colors: colorsEnabled,
|
|
399
|
+
markdown: markdownEnabled,
|
|
395
400
|
voiceMode: true,
|
|
396
401
|
specialistHint: voiceRouting.specialistHint,
|
|
397
402
|
mcpManager,
|
|
@@ -622,6 +627,44 @@ async function runOneSession(opts, registerSession) {
|
|
|
622
627
|
audit.append({ kind: 'self-command', cmd: lastCmd, action: selfCmd.action, cwd });
|
|
623
628
|
// Clear any stale correction state so it doesn't leak into next command
|
|
624
629
|
correctionOrigin = null;
|
|
630
|
+
|
|
631
|
+
// ── Sidebar query: run the agent with context but don't persist to history ──
|
|
632
|
+
if (selfCmd.action === 'sidebar-query' && selfCmd.arg) {
|
|
633
|
+
if (!isConfigured()) {
|
|
634
|
+
out('\r\n\x1b[33m[shmakk] LLM not configured — sidebar query ignored\x1b[0m\r\n');
|
|
635
|
+
session.childWrite('\r');
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
await withAI(async (ctrl) => {
|
|
639
|
+
const sidebarRouting = routeToSpecialist(selfCmd.arg, [...history, { role: 'user', content: selfCmd.arg }]);
|
|
640
|
+
out('\x1b[36m[shmakk sidebar] (Ctrl-C to interrupt)\x1b[0m\r\n');
|
|
641
|
+
try {
|
|
642
|
+
const updated = await runAgent({
|
|
643
|
+
input: selfCmd.arg,
|
|
644
|
+
roots: currentRoots(),
|
|
645
|
+
glossary,
|
|
646
|
+
confirmTool: makeToolConfirm(opts, ask, out, () => ctrl.abort()),
|
|
647
|
+
write: out,
|
|
648
|
+
signal: ctrl.signal,
|
|
649
|
+
history,
|
|
650
|
+
profile: opts.profile || sidebarRouting.profile || 'balanced',
|
|
651
|
+
colors: colorsEnabled,
|
|
652
|
+
markdown: markdownEnabled,
|
|
653
|
+
specialistHint: sidebarRouting.specialistHint,
|
|
654
|
+
mcpManager,
|
|
655
|
+
});
|
|
656
|
+
// Don't update history — sidebar queries are out-of-band.
|
|
657
|
+
// (runAgent already records turns in session search internally.)
|
|
658
|
+
} catch (e) {
|
|
659
|
+
if (!isAbortError(e)) {
|
|
660
|
+
out(`\x1b[31m[shmakk sidebar] error: ${e.message}\x1b[0m\r\n`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
session.childWrite('\r');
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
625
668
|
if (selfCmd.confirm) {
|
|
626
669
|
const go = await ask(`Run ${selfCmd.action}?`, true, { onCancel: () => {} });
|
|
627
670
|
if (!go) { session.childWrite('\r'); return; }
|
|
@@ -641,11 +684,18 @@ async function runOneSession(opts, registerSession) {
|
|
|
641
684
|
// - Failed (code != 0): give agent the original command the user typed, not any
|
|
642
685
|
// corrected variant — if a correction was applied but also failed, correctionOrigin
|
|
643
686
|
// still holds the user's original input and that's what the agent should reason about.
|
|
687
|
+
//
|
|
688
|
+
// After a correction was applied (whether it succeeded or failed), we MUST skip
|
|
689
|
+
// re-running correction on the original — otherwise it would propose the same fix
|
|
690
|
+
// again and enter an infinite loop (correct → feed corrected → succeed/fail →
|
|
691
|
+
// agent sees original → correction proposes same thing → feed again → ...).
|
|
644
692
|
let cmd = lastCmd;
|
|
693
|
+
let correctionAlreadyTried = false;
|
|
645
694
|
if (code === 0) {
|
|
646
695
|
if (correctionOrigin && !opts.noAi) {
|
|
647
696
|
cmd = correctionOrigin;
|
|
648
697
|
correctionOrigin = null;
|
|
698
|
+
correctionAlreadyTried = true;
|
|
649
699
|
} else {
|
|
650
700
|
flushPending();
|
|
651
701
|
return;
|
|
@@ -657,14 +707,19 @@ async function runOneSession(opts, registerSession) {
|
|
|
657
707
|
if (correctionOrigin) {
|
|
658
708
|
cmd = correctionOrigin;
|
|
659
709
|
correctionOrigin = null;
|
|
710
|
+
correctionAlreadyTried = true;
|
|
660
711
|
}
|
|
661
712
|
}
|
|
662
713
|
|
|
663
714
|
audit.append({ kind: 'failed-command', cmd, exit: code, cwd });
|
|
664
715
|
|
|
665
716
|
// ── Correction runs standalone (no LLM needed) ──
|
|
717
|
+
// Skip correction if one was already applied for this input —
|
|
718
|
+
// re-running correction on the same original would just propose the same fix.
|
|
666
719
|
let decision;
|
|
667
|
-
if (
|
|
720
|
+
if (correctionAlreadyTried) {
|
|
721
|
+
decision = { category: 'not_a_correction', proposed: null, safety: 'uncertain', reason: 'correction already tried — routing to agent' };
|
|
722
|
+
} else if (opts.noCorrection) {
|
|
668
723
|
decision = { category: 'not_a_correction', proposed: null, safety: 'uncertain', reason: 'correction disabled' };
|
|
669
724
|
} else {
|
|
670
725
|
try {
|
|
@@ -761,6 +816,7 @@ async function runOneSession(opts, registerSession) {
|
|
|
761
816
|
signal: ctrl.signal,
|
|
762
817
|
profile: agentProfile,
|
|
763
818
|
colors: colorsEnabled,
|
|
819
|
+
markdown: markdownEnabled,
|
|
764
820
|
specialistHint: routing.specialistHint,
|
|
765
821
|
mcpManager,
|
|
766
822
|
};
|