shmakk 1.2.1 → 1.2.3
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 +40 -0
- package/package.json +1 -1
- package/src/agent.js +54 -9
- package/src/cli.js +193 -78
- package/src/correction.js +6 -0
- package/src/endpoints.js +6 -0
- package/src/hooks/bash.js +17 -2
- package/src/hooks/fish.js +21 -2
- package/src/hooks/zsh.js +31 -2
- package/src/index.js +11 -2
- package/src/llm.js +2 -2
- package/src/mcp-client.js +7 -1
- package/src/notify.js +6 -3
- package/src/pty.js +2 -2
- package/src/review.js +3 -3
- package/src/self-commands.js +96 -16
- package/src/session.js +14 -5
- package/src/shell.js +39 -19
- package/src/ssh.js +255 -0
- package/src/system-prompt.js +3 -1
- package/src/tools.js +105 -1
package/src/hooks/bash.js
CHANGED
|
@@ -3,6 +3,21 @@ const os = require('os');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
5
|
const INIT = `
|
|
6
|
+
# Source login scripts first (equivalent to bash -l).
|
|
7
|
+
# When --rcfile is used, bash skips the normal login sequence, so we must
|
|
8
|
+
# replicate it manually. Order matters: profile.d → /etc/profile, then
|
|
9
|
+
# first found of: ~/.bash_profile, ~/.bash_login, ~/.profile, ~/.bashrc.
|
|
10
|
+
[ -d /etc/profile.d ] && for f in /etc/profile.d/*.sh; do [ -r "$f" ] && . "$f"; done
|
|
11
|
+
[ -f /etc/profile ] && . /etc/profile
|
|
12
|
+
if [ -f "$HOME/.bash_profile" ]; then
|
|
13
|
+
. "$HOME/.bash_profile"
|
|
14
|
+
elif [ -f "$HOME/.bash_login" ]; then
|
|
15
|
+
. "$HOME/.bash_login"
|
|
16
|
+
elif [ -f "$HOME/.profile" ]; then
|
|
17
|
+
. "$HOME/.profile"
|
|
18
|
+
fi
|
|
19
|
+
# Then interactive rc files (these may be already sourced above, but
|
|
20
|
+
# sourcing twice is harmless for well-behaved scripts).
|
|
6
21
|
[ -f /etc/bash.bashrc ] && . /etc/bash.bashrc
|
|
7
22
|
[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"
|
|
8
23
|
|
|
@@ -13,13 +28,13 @@ __shmakk_preexec() {
|
|
|
13
28
|
[ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return
|
|
14
29
|
__shmakk_armed=
|
|
15
30
|
local cmd
|
|
16
|
-
cmd=$(printf '%s' "$BASH_COMMAND" | base64 -w0 2>/dev/null ||
|
|
31
|
+
cmd=$(printf '%s' "$BASH_COMMAND" | base64 -w0 2>/dev/null || base64 -b 0 2>/dev/null || base64 | tr -d '\n')
|
|
17
32
|
printf '\\e]6973;B;%s\\a' "$cmd"
|
|
18
33
|
}
|
|
19
34
|
__shmakk_precmd() {
|
|
20
35
|
local ec=$?
|
|
21
36
|
local p
|
|
22
|
-
p=$(printf '%s' "$PWD" | base64 -w0 2>/dev/null ||
|
|
37
|
+
p=$(printf '%s' "$PWD" | base64 -w0 2>/dev/null || base64 -b 0 2>/dev/null || base64 | tr -d '\n')
|
|
23
38
|
printf '\\e]6973;C;%s\\a' "$ec"
|
|
24
39
|
printf '\\e]6973;D;%s\\a' "$p"
|
|
25
40
|
__shmakk_armed=1
|
package/src/hooks/fish.js
CHANGED
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
// Returns { args, env, cleanup } for spawning fish with markers wired up.
|
|
2
2
|
// fish supports `-C COMMAND` to run init code after config.fish.
|
|
3
|
+
//
|
|
4
|
+
// base64 encoding: try `-w0` (GNU coreutils), fall back to `-b 0` (BSD/macOS),
|
|
5
|
+
// then plain `base64` as last resort. `tr -d '\n'` strips any line wrapping
|
|
6
|
+
// so the OSC marker payload stays on one line.
|
|
3
7
|
|
|
4
8
|
const INIT = `
|
|
5
9
|
function __shmakk_pre --on-event fish_preexec
|
|
6
|
-
set -l c (printf '%s' "$argv" | base64 -w0 2>/dev/null
|
|
10
|
+
set -l c (printf '%s' "$argv" | base64 -w0 2>/dev/null || base64 -b 0 2>/dev/null || base64 | tr -d '\n')
|
|
7
11
|
printf '\\e]6973;B;%s\\a' "$c"
|
|
8
12
|
end
|
|
9
13
|
function __shmakk_post --on-event fish_postexec
|
|
10
14
|
set -l ec $status
|
|
11
|
-
set -l p (printf '%s' "$PWD" | base64 -w0 2>/dev/null
|
|
15
|
+
set -l p (printf '%s' "$PWD" | base64 -w0 2>/dev/null || base64 -b 0 2>/dev/null || base64 | tr -d '\n')
|
|
12
16
|
printf '\\e]6973;C;%s\\a' $ec
|
|
13
17
|
printf '\\e]6973;D;%s\\a' "$p"
|
|
14
18
|
end
|
|
19
|
+
# Override shmakk binary inside a session so "shmakk <cmd>" routes to
|
|
20
|
+
# local self-commands instead of forking a nested shmakk process.
|
|
21
|
+
# Passes through --flags to the real shmakk binary.
|
|
22
|
+
function shmakk
|
|
23
|
+
if set -q argv[1]; and string match -qr '^--' -- "$argv[1]"
|
|
24
|
+
command shmakk $argv
|
|
25
|
+
return $status
|
|
26
|
+
end
|
|
27
|
+
set -l raw (printf '%s' "shmakk $argv" | base64 -w0 2>/dev/null || base64 -b 0 2>/dev/null || base64 | tr -d '\n')
|
|
28
|
+
set -l pwd_b64 (printf '%s' "$PWD" | base64 -w0 2>/dev/null || base64 -b 0 2>/dev/null || base64 | tr -d '\n')
|
|
29
|
+
printf '\\e]6973;B;%s\\a' "$raw"
|
|
30
|
+
printf '\\e]6973;C;127\\a'
|
|
31
|
+
printf '\\e]6973;D;%s\\a' "$pwd_b64"
|
|
32
|
+
return 127
|
|
33
|
+
end
|
|
15
34
|
`.trim();
|
|
16
35
|
|
|
17
36
|
function configure() {
|
package/src/hooks/zsh.js
CHANGED
|
@@ -2,6 +2,28 @@ const fs = require('fs');
|
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
|
+
// zsh under a custom ZDOTDIR sources .zshenv, .zprofile, .zshrc, .zlogin from
|
|
6
|
+
// that directory. We create all four so nothing from the user's real ZDOTDIR
|
|
7
|
+
// is skipped.
|
|
8
|
+
|
|
9
|
+
const ZSHENV = `
|
|
10
|
+
# Source the real .zshenv so PATH and env vars are available.
|
|
11
|
+
if [ -n "$SHMAKK_REAL_ZDOTDIR" ] && [ -f "$SHMAKK_REAL_ZDOTDIR/.zshenv" ]; then
|
|
12
|
+
source "$SHMAKK_REAL_ZDOTDIR/.zshenv"
|
|
13
|
+
elif [ -f "$HOME/.zshenv" ]; then
|
|
14
|
+
source "$HOME/.zshenv"
|
|
15
|
+
fi
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
const ZPROFILE = `
|
|
19
|
+
# Source the real .zprofile (login shell initialization).
|
|
20
|
+
if [ -n "$SHMAKK_REAL_ZDOTDIR" ] && [ -f "$SHMAKK_REAL_ZDOTDIR/.zprofile" ]; then
|
|
21
|
+
source "$SHMAKK_REAL_ZDOTDIR/.zprofile"
|
|
22
|
+
elif [ -f "$HOME/.zprofile" ]; then
|
|
23
|
+
source "$HOME/.zprofile"
|
|
24
|
+
fi
|
|
25
|
+
`;
|
|
26
|
+
|
|
5
27
|
const ZSHRC = `
|
|
6
28
|
# preserve real ZDOTDIR so user config is sourced
|
|
7
29
|
if [ -n "$SHMAKK_REAL_ZDOTDIR" ]; then
|
|
@@ -12,13 +34,13 @@ fi
|
|
|
12
34
|
|
|
13
35
|
__shmakk_preexec() {
|
|
14
36
|
local cmd
|
|
15
|
-
cmd=$(printf '%s' "$1" | base64 -w0 2>/dev/null ||
|
|
37
|
+
cmd=$(printf '%s' "$1" | base64 -w0 2>/dev/null || base64 -b 0 2>/dev/null || base64 | tr -d '\n')
|
|
16
38
|
printf '\\e]6973;B;%s\\a' "$cmd"
|
|
17
39
|
}
|
|
18
40
|
__shmakk_precmd() {
|
|
19
41
|
local ec=$?
|
|
20
42
|
local p
|
|
21
|
-
p=$(printf '%s' "$PWD" | base64 -w0 2>/dev/null ||
|
|
43
|
+
p=$(printf '%s' "$PWD" | base64 -w0 2>/dev/null || base64 -b 0 2>/dev/null || base64 | tr -d '\n')
|
|
22
44
|
printf '\\e]6973;C;%s\\a' "$ec"
|
|
23
45
|
printf '\\e]6973;D;%s\\a' "$p"
|
|
24
46
|
}
|
|
@@ -30,7 +52,14 @@ precmd_functions+=(__shmakk_precmd)
|
|
|
30
52
|
function configure() {
|
|
31
53
|
const dir = path.join(os.tmpdir(), `shmakk-zsh-${process.pid}`);
|
|
32
54
|
fs.mkdirSync(dir, { recursive: true });
|
|
55
|
+
// zsh under a custom ZDOTDIR sources .zshenv, .zprofile, .zshrc, .zlogin
|
|
56
|
+
// from that directory. We must provide all four so the user's environment
|
|
57
|
+
// is complete.
|
|
58
|
+
fs.writeFileSync(path.join(dir, '.zshenv'), ZSHENV, { mode: 0o600 });
|
|
59
|
+
fs.writeFileSync(path.join(dir, '.zprofile'), ZPROFILE, { mode: 0o600 });
|
|
33
60
|
fs.writeFileSync(path.join(dir, '.zshrc'), ZSHRC, { mode: 0o600 });
|
|
61
|
+
// No .zlogin needed — zsh docs say .zlogin is for commands to run at the
|
|
62
|
+
// start of an interactive login shell; .zprofile already covers env setup.
|
|
34
63
|
const realZ = process.env.ZDOTDIR || '';
|
|
35
64
|
return {
|
|
36
65
|
args: ['-i'],
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { parseArgs, HELP } = require('./cli');
|
|
1
|
+
const { parseArgs, HELP, resolveHelp } = require('./cli');
|
|
2
2
|
const { normalizeProfile, resolveProfile } = require('./profiles');
|
|
3
3
|
const { applyEndpoint, getCurrentEndpoint, getCurrentEndpointName } = require('./endpoints');
|
|
4
4
|
const { ensureModelRuntime } = require('./llm');
|
|
@@ -66,7 +66,7 @@ async function main() {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (opts.help) {
|
|
69
|
-
process.stdout.write(
|
|
69
|
+
process.stdout.write(resolveHelp(opts.helpCategory));
|
|
70
70
|
process.exit(0);
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -204,6 +204,15 @@ async function main() {
|
|
|
204
204
|
if (opts.ttsVoice) process.env.SHMAKK_TTS_VOICE = opts.ttsVoice;
|
|
205
205
|
|
|
206
206
|
const { start } = require('./orchestrator');
|
|
207
|
+
|
|
208
|
+
// Refuse to nest sessions: launching shmakk inside shmakk would
|
|
209
|
+
// create a recursive PTY tree with no benefit.
|
|
210
|
+
if (process.env.SHMAKK === '1') {
|
|
211
|
+
process.stderr.write('[shmakk] already inside an shmakk session (SHMAKK=1).\n');
|
|
212
|
+
process.stderr.write('[shmakk] use --help to see in-session commands, or exit the current session first.\n');
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
207
216
|
const exitCode = await start(opts);
|
|
208
217
|
process.exit(exitCode);
|
|
209
218
|
}
|
package/src/llm.js
CHANGED
|
@@ -4,7 +4,7 @@ try { OpenAI = require('openai'); } catch { OpenAI = null; }
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const fs = require('fs');
|
|
7
|
-
const { getCurrentEndpoint, getCurrentEndpointName, getModelRegistry } = require('./endpoints');
|
|
7
|
+
const { getCurrentEndpoint, getCurrentEndpointName, getModelRegistry, supportsVision } = require('./endpoints');
|
|
8
8
|
|
|
9
9
|
function parseHeaders(s) {
|
|
10
10
|
const out = {};
|
|
@@ -536,4 +536,4 @@ function getDeepSeekOptions(taskType) {
|
|
|
536
536
|
};
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
-
module.exports = { makeClient, modelFor, isConfigured, ensureModelRuntime, getDeepSeekOptions, isDeepSeekProvider };
|
|
539
|
+
module.exports = { makeClient, modelFor, isConfigured, ensureModelRuntime, getDeepSeekOptions, isDeepSeekProvider, supportsVision };
|
package/src/mcp-client.js
CHANGED
|
@@ -212,9 +212,15 @@ class MCPServer {
|
|
|
212
212
|
if (item.type === 'text') {
|
|
213
213
|
texts.push(item.text);
|
|
214
214
|
} else if (item.type === 'image') {
|
|
215
|
+
// Preserve base64 image data so vision-capable providers can process it.
|
|
216
|
+
// Cap at ~2MB of base64 to avoid blowing out context windows.
|
|
217
|
+
const raw = String(item.data || '');
|
|
218
|
+
const capped = raw.length > 2_000_000 ? raw.slice(0, 2_000_000) : raw;
|
|
215
219
|
images.push({
|
|
216
220
|
mimeType: item.mimeType || 'image/png',
|
|
217
|
-
|
|
221
|
+
data: capped,
|
|
222
|
+
dataLength: raw.length,
|
|
223
|
+
truncated: raw.length > 2_000_000,
|
|
218
224
|
});
|
|
219
225
|
} else if (item.type === 'resource') {
|
|
220
226
|
texts.push(`[resource: ${item.resource?.uri || 'unknown'}]`);
|
package/src/notify.js
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
// Falls back silently if notify-send is not available or no notification
|
|
3
3
|
// daemon is running.
|
|
4
4
|
|
|
5
|
-
const { execFile } = require('child_process');
|
|
5
|
+
const { execFile, execFileSync } = require('child_process');
|
|
6
|
+
const { existsSync } = require('fs');
|
|
6
7
|
|
|
7
8
|
const NOTIFY_BIN = 'notify-send';
|
|
8
9
|
|
|
9
10
|
function available() {
|
|
10
11
|
try {
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
// Prefer direct path check; fall back to `command -v` if not at known paths
|
|
13
|
+
if (existsSync('/usr/bin/notify-send')) return true;
|
|
14
|
+
if (existsSync('/usr/local/bin/notify-send')) return true;
|
|
15
|
+
execFileSync('command', ['-v', NOTIFY_BIN], { stdio: 'ignore' });
|
|
13
16
|
return true;
|
|
14
17
|
} catch {
|
|
15
18
|
return false;
|
package/src/pty.js
CHANGED
|
@@ -13,8 +13,8 @@ function getSize() {
|
|
|
13
13
|
|
|
14
14
|
const VOICE_HOTKEY = 0x0f; // Ctrl+O — triggers voice recording
|
|
15
15
|
|
|
16
|
-
function startSession({ debug = false, voiceEnabled = false } = {}) {
|
|
17
|
-
const shell = detectShell();
|
|
16
|
+
function startSession({ debug = false, voiceEnabled = false, shellOverride = null } = {}) {
|
|
17
|
+
const shell = detectShell(shellOverride);
|
|
18
18
|
const cfg = configureForShell(shell.name);
|
|
19
19
|
const { cols, rows } = getSize();
|
|
20
20
|
|
package/src/review.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// optional `{ onCancel }` callback that fires when the user hits Ctrl-C.
|
|
3
3
|
|
|
4
4
|
function makePrompter(pty, write, opts = {}) {
|
|
5
|
-
const { onNotify } = opts;
|
|
6
5
|
return function ask(question, defaultYes, { onCancel, onWhy } = {}) {
|
|
7
6
|
return new Promise((resolve) => {
|
|
8
|
-
if (
|
|
7
|
+
if (opts.notify) {
|
|
9
8
|
try {
|
|
9
|
+
const { notify } = require('./notify');
|
|
10
10
|
const body = typeof question === 'string' ? question.replace(/\x1b\[[0-9;]*m/g, '') : String(question || '');
|
|
11
|
-
|
|
11
|
+
notify('shmakk needs your attention', body.slice(0, 120));
|
|
12
12
|
} catch {}
|
|
13
13
|
}
|
|
14
14
|
const tag = defaultYes ? '[Y/n/?]' : '[y/N/?]';
|
package/src/self-commands.js
CHANGED
|
@@ -110,12 +110,14 @@ const SELF_COMMANDS = [
|
|
|
110
110
|
},
|
|
111
111
|
|
|
112
112
|
// ── Session ──
|
|
113
|
+
// NOTE: Bare "status", "stats" etc. are NOT intercepted — they may be
|
|
114
|
+
// real shell commands. Use /status, /stats, shmakk status, etc. instead.
|
|
113
115
|
{
|
|
114
|
-
patterns: [/^
|
|
116
|
+
patterns: [/^status$/i],
|
|
115
117
|
action: 'status',
|
|
116
118
|
},
|
|
117
119
|
{
|
|
118
|
-
patterns: [/^
|
|
120
|
+
patterns: [/^stats$/i, /^session\s+stats$/i],
|
|
119
121
|
action: 'stats',
|
|
120
122
|
},
|
|
121
123
|
{
|
|
@@ -164,8 +166,8 @@ const SELF_COMMANDS = [
|
|
|
164
166
|
},
|
|
165
167
|
{
|
|
166
168
|
patterns: [
|
|
167
|
-
/^(?:show\s+)?last\s+sessions?$/i,
|
|
168
|
-
/^
|
|
169
|
+
/^(?:show\s+)?(?:last\s+|recent\s+)?sessions?$/i,
|
|
170
|
+
/^sessions?$/i,
|
|
169
171
|
],
|
|
170
172
|
action: 'last-sessions',
|
|
171
173
|
},
|
|
@@ -320,6 +322,22 @@ const SELF_COMMANDS = [
|
|
|
320
322
|
action: 'disable-yes-files',
|
|
321
323
|
},
|
|
322
324
|
|
|
325
|
+
// ── Notify ──
|
|
326
|
+
{
|
|
327
|
+
patterns: [
|
|
328
|
+
/^(?:enable|turn\s+on)\s+notify$/i,
|
|
329
|
+
/^notify\s+on$/i,
|
|
330
|
+
],
|
|
331
|
+
action: 'enable-notify',
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
patterns: [
|
|
335
|
+
/^(?:disable|turn\s+off|no)\s+notify$/i,
|
|
336
|
+
/^notify\s+off$/i,
|
|
337
|
+
],
|
|
338
|
+
action: 'disable-notify',
|
|
339
|
+
},
|
|
340
|
+
|
|
323
341
|
// ── Colors ──
|
|
324
342
|
{
|
|
325
343
|
patterns: [
|
|
@@ -399,20 +417,65 @@ const SELF_COMMANDS = [
|
|
|
399
417
|
},
|
|
400
418
|
];
|
|
401
419
|
|
|
420
|
+
// Self-command prefixes accepted by the shell:
|
|
421
|
+
// /cmd — e.g. /status, /sessions, /compact
|
|
422
|
+
// shmakk cmd — e.g. shmakk status, shmakk show sessions
|
|
423
|
+
// Bare words like "status" are NOT intercepted (they go to the shell).
|
|
424
|
+
const SELF_PREFIX_RE = /^\/(.+)$/;
|
|
425
|
+
const SHMAKK_PREFIX_RE = /^shmakk\s+(.+)$/i;
|
|
426
|
+
|
|
427
|
+
function hasSelfCommandPrefix(input) {
|
|
428
|
+
const text = String(input || '').trim();
|
|
429
|
+
return SELF_PREFIX_RE.test(text) || SHMAKK_PREFIX_RE.test(text);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function stripSelfCommandPrefix(input) {
|
|
433
|
+
const text = String(input || '').trim();
|
|
434
|
+
let m = SELF_PREFIX_RE.exec(text);
|
|
435
|
+
if (m) return m[1].trim();
|
|
436
|
+
m = SHMAKK_PREFIX_RE.exec(text);
|
|
437
|
+
if (m) return m[1].trim();
|
|
438
|
+
return text;
|
|
439
|
+
}
|
|
440
|
+
|
|
402
441
|
function matchSelfCommand(input) {
|
|
403
442
|
const text = String(input || '').trim();
|
|
404
443
|
if (!text) return { matched: false };
|
|
405
444
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
445
|
+
// Try matching with prefix stripped first (for /status, shmakk status, etc.)
|
|
446
|
+
const stripped = stripSelfCommandPrefix(text);
|
|
447
|
+
if (stripped !== text) {
|
|
448
|
+
for (const entry of SELF_COMMANDS) {
|
|
449
|
+
for (const pattern of entry.patterns) {
|
|
450
|
+
const m = pattern.exec(stripped);
|
|
451
|
+
if (m) {
|
|
452
|
+
return {
|
|
453
|
+
matched: true,
|
|
454
|
+
action: entry.action,
|
|
455
|
+
arg: entry.needsArg && m[1] ? m[1].trim() : null,
|
|
456
|
+
confirm: !!entry.confirm,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return { matched: false };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Multi-word natural-language commands (no prefix needed).
|
|
465
|
+
// Single bare words are NOT matched — they could be real shell commands.
|
|
466
|
+
const wordCount = text.split(/\s+/).length;
|
|
467
|
+
if (wordCount >= 2) {
|
|
468
|
+
for (const entry of SELF_COMMANDS) {
|
|
469
|
+
for (const pattern of entry.patterns) {
|
|
470
|
+
const m = pattern.exec(text);
|
|
471
|
+
if (m) {
|
|
472
|
+
return {
|
|
473
|
+
matched: true,
|
|
474
|
+
action: entry.action,
|
|
475
|
+
arg: entry.needsArg && m[1] ? m[1].trim() : null,
|
|
476
|
+
confirm: !!entry.confirm,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
416
479
|
}
|
|
417
480
|
}
|
|
418
481
|
}
|
|
@@ -422,6 +485,9 @@ function matchSelfCommand(input) {
|
|
|
422
485
|
|
|
423
486
|
// ctx is optional: { opts, HELP, setColors }
|
|
424
487
|
function executeSelfCommand(match, write, ctx = {}) {
|
|
488
|
+
// Update terminal tab title so self-command activity is visible from other tabs
|
|
489
|
+
const label = match.action.replace(/-/g, ' ');
|
|
490
|
+
write(`\x1b]0;${label} — shmakk\x07`);
|
|
425
491
|
const ctl = require('./control');
|
|
426
492
|
const opts = ctx.opts || {};
|
|
427
493
|
|
|
@@ -429,7 +495,7 @@ function executeSelfCommand(match, write, ctx = {}) {
|
|
|
429
495
|
|
|
430
496
|
// ── Help ──
|
|
431
497
|
case 'show-help': {
|
|
432
|
-
const helpText = ctx.HELP || '[shmakk] help text not available';
|
|
498
|
+
const helpText = ctx.HELP_SESSION_SUMMARY || ctx.HELP_SUMMARY || ctx.HELP || '[shmakk] help text not available';
|
|
433
499
|
write(helpText.replace(/\n/g, '\r\n'));
|
|
434
500
|
break;
|
|
435
501
|
}
|
|
@@ -803,6 +869,18 @@ function executeSelfCommand(match, write, ctx = {}) {
|
|
|
803
869
|
break;
|
|
804
870
|
}
|
|
805
871
|
|
|
872
|
+
// ── Notify ──
|
|
873
|
+
case 'enable-notify': {
|
|
874
|
+
if (ctx.opts) ctx.opts.notify = true;
|
|
875
|
+
write('[shmakk] desktop notifications enabled\r\n');
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
case 'disable-notify': {
|
|
879
|
+
if (ctx.opts) ctx.opts.notify = false;
|
|
880
|
+
write('[shmakk] desktop notifications disabled\r\n');
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
|
|
806
884
|
// ── Colors ──
|
|
807
885
|
case 'enable-colors': {
|
|
808
886
|
if (ctx.opts) ctx.opts.colors = true;
|
|
@@ -858,6 +936,8 @@ function executeSelfCommand(match, write, ctx = {}) {
|
|
|
858
936
|
default:
|
|
859
937
|
write(`[shmakk] unknown self-command: ${match.action}\r\n`);
|
|
860
938
|
}
|
|
939
|
+
// Clear terminal title — shell will restore normal title on next prompt
|
|
940
|
+
write('\x1b]0;\x07');
|
|
861
941
|
}
|
|
862
942
|
|
|
863
|
-
module.exports = { matchSelfCommand, executeSelfCommand, SELF_COMMANDS };
|
|
943
|
+
module.exports = { matchSelfCommand, executeSelfCommand, hasSelfCommandPrefix, stripSelfCommandPrefix, SELF_COMMANDS };
|
package/src/session.js
CHANGED
|
@@ -20,7 +20,7 @@ const { runTeam, looksMultiDomain } = require('./team');
|
|
|
20
20
|
const { addPlanTasks, markTaskComplete, markTaskSkipped } = require('./task-file');
|
|
21
21
|
const { captureGitSha, runPostPlanReview } = require('./code-reviewer');
|
|
22
22
|
const sessionSearch = require('./session-search');
|
|
23
|
-
const { HELP } = require('./cli');
|
|
23
|
+
const { HELP, HELP_SUMMARY, HELP_SESSION_SUMMARY } = require('./cli');
|
|
24
24
|
const audit = require('./audit');
|
|
25
25
|
const { setMaxListeners } = require('events');
|
|
26
26
|
|
|
@@ -163,13 +163,11 @@ function makeToolConfirm(opts, ask, out, getAbort) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
async function runOneSession(opts, registerSession) {
|
|
166
|
-
const session = startSession({ debug: opts.debug, voiceEnabled: !!opts.voice });
|
|
166
|
+
const session = startSession({ debug: opts.debug, voiceEnabled: !!opts.voice, shellOverride: opts.shell });
|
|
167
167
|
let colorsEnabled = opts.colors !== false;
|
|
168
168
|
let markdownEnabled = opts.markdown !== false;
|
|
169
169
|
const out = (s) => session.stdoutWrite(colorsEnabled ? s : stripAnsi(s));
|
|
170
|
-
const ask = makePrompter(session, out,
|
|
171
|
-
onNotify: opts.notify ? (summary, body) => notify(summary, body) : null,
|
|
172
|
-
});
|
|
170
|
+
const ask = makePrompter(session, out, opts);
|
|
173
171
|
const glossary = loadGlossary();
|
|
174
172
|
// Workspace tracking: explicit --workspace is "pinned"; otherwise cwd
|
|
175
173
|
// floats with the inner shell's `cd`. When both pinned and cwd differ,
|
|
@@ -373,6 +371,8 @@ async function runOneSession(opts, registerSession) {
|
|
|
373
371
|
executeSelfCommand(voiceSelfCmd, out, {
|
|
374
372
|
opts,
|
|
375
373
|
HELP,
|
|
374
|
+
HELP_SUMMARY,
|
|
375
|
+
HELP_SESSION_SUMMARY,
|
|
376
376
|
setColors: (v) => { colorsEnabled = v; },
|
|
377
377
|
});
|
|
378
378
|
return;
|
|
@@ -672,11 +672,20 @@ async function runOneSession(opts, registerSession) {
|
|
|
672
672
|
executeSelfCommand(selfCmd, out, {
|
|
673
673
|
opts,
|
|
674
674
|
HELP,
|
|
675
|
+
HELP_SUMMARY,
|
|
676
|
+
HELP_SESSION_SUMMARY,
|
|
675
677
|
setColors: (v) => { colorsEnabled = v; },
|
|
676
678
|
});
|
|
677
679
|
session.childWrite('\r');
|
|
678
680
|
return;
|
|
679
681
|
}
|
|
682
|
+
// /-prefixed and "shmakk ..." commands that didn't match a known
|
|
683
|
+
// self-command are invalid shmakk commands. Don't send them to the
|
|
684
|
+
// correction engine — the user was explicitly addressing shmakk.
|
|
685
|
+
if (/^\//.test(lastCmd) || /^shmakk\s/i.test(lastCmd)) {
|
|
686
|
+
flushPending();
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
680
689
|
}
|
|
681
690
|
|
|
682
691
|
// Determine the command to feed forward.
|
package/src/shell.js
CHANGED
|
@@ -1,32 +1,52 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Map shell name to the preferred executable path.
|
|
5
|
+
// Resolve's dctl paths on Arch-like systems can put bash in /usr/bin instead of /bin.
|
|
6
|
+
// Map shell name to candidate executable paths.
|
|
7
|
+
// Ordered by likelihood on the current platform; first existing path wins.
|
|
8
|
+
const SHELL_PATH_CANDIDATES = {
|
|
9
|
+
fish: ['/usr/bin/fish', '/opt/homebrew/bin/fish', '/usr/local/bin/fish', '/bin/fish'],
|
|
10
|
+
bash: ['/usr/bin/bash', '/bin/bash', '/opt/homebrew/bin/bash', '/usr/local/bin/bash'],
|
|
11
|
+
zsh: ['/usr/bin/zsh', '/bin/zsh', '/opt/homebrew/bin/zsh', '/usr/local/bin/zsh'],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function shellPath(name) {
|
|
15
|
+
// Name given explicitly (--shell flag): try known paths first, then PATH.
|
|
16
|
+
const candidates = SHELL_PATH_CANDIDATES[name];
|
|
17
|
+
if (candidates) {
|
|
18
|
+
for (const candidate of candidates) {
|
|
19
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Fall back to PATH search for the requested shell.
|
|
24
|
+
const { execSync } = require('child_process');
|
|
25
|
+
try {
|
|
26
|
+
const p = execSync(`command -v ${name}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
27
|
+
if (p && fs.existsSync(p)) return p;
|
|
28
|
+
} catch {}
|
|
29
|
+
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function detectShell(shellOverride) {
|
|
34
|
+
// Explicit --shell flag overrides everything.
|
|
35
|
+
if (shellOverride) {
|
|
36
|
+
const p = shellPath(shellOverride);
|
|
37
|
+
if (p) return { path: p, name: shellOverride };
|
|
38
|
+
process.stderr.write(`[shmakk] shell "${shellOverride}" not found, falling back to default\n`);
|
|
39
|
+
}
|
|
40
|
+
|
|
5
41
|
const env = process.env.SHELL;
|
|
6
42
|
if (env && fs.existsSync(env)) {
|
|
7
43
|
return { path: env, name: path.basename(env) };
|
|
8
44
|
}
|
|
9
|
-
const fallbacks = ['/bin/bash', '/usr/bin/bash', '/bin/sh'];
|
|
45
|
+
const fallbacks = ['/bin/bash', '/usr/bin/bash', '/opt/homebrew/bin/bash', '/usr/local/bin/bash', '/bin/sh'];
|
|
10
46
|
for (const f of fallbacks) {
|
|
11
47
|
if (fs.existsSync(f)) return { path: f, name: path.basename(f) };
|
|
12
48
|
}
|
|
13
49
|
return { path: '/bin/sh', name: 'sh' };
|
|
14
50
|
}
|
|
15
51
|
|
|
16
|
-
|
|
17
|
-
// Login + interactive so the user's normal init runs.
|
|
18
|
-
// We deliberately keep this minimal: do NOT inject rc files,
|
|
19
|
-
// do NOT alter prompt. Phase 2 will add hooks for command metadata.
|
|
20
|
-
switch (name) {
|
|
21
|
-
case 'fish':
|
|
22
|
-
return ['-i', '-l'];
|
|
23
|
-
case 'zsh':
|
|
24
|
-
return ['-i', '-l'];
|
|
25
|
-
case 'bash':
|
|
26
|
-
return ['-i', '-l'];
|
|
27
|
-
default:
|
|
28
|
-
return ['-i'];
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
module.exports = { detectShell, shellArgs };
|
|
52
|
+
module.exports = { detectShell, shellPath };
|