skalpel 3.0.20 → 3.0.21
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/package.json +7 -6
- package/postinstall/lib/rc-edit.js +82 -16
- package/postinstall/uninstall.js +90 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skalpel",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.21",
|
|
4
4
|
"description": "Skalpel — local proxy and TUI for coding agents (skalpel + skalpeld bundle).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://skalpel.ai",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"postinstall": "node postinstall/index.js",
|
|
32
|
+
"preuninstall": "node postinstall/uninstall.js",
|
|
32
33
|
"test": "echo 'no top-level tests; run make test or npm run test:rc-edit' && exit 0",
|
|
33
34
|
"test:rc-edit": "node postinstall/lib/rc-edit.test.js"
|
|
34
35
|
},
|
|
@@ -53,10 +54,10 @@
|
|
|
53
54
|
"x64"
|
|
54
55
|
],
|
|
55
56
|
"optionalDependencies": {
|
|
56
|
-
"@skalpelai/skalpel-darwin-arm64": "3.0.
|
|
57
|
-
"@skalpelai/skalpel-darwin-x64": "3.0.
|
|
58
|
-
"@skalpelai/skalpel-linux-arm64": "3.0.
|
|
59
|
-
"@skalpelai/skalpel-linux-x64": "3.0.
|
|
60
|
-
"@skalpelai/skalpel-win32-x64": "3.0.
|
|
57
|
+
"@skalpelai/skalpel-darwin-arm64": "3.0.21",
|
|
58
|
+
"@skalpelai/skalpel-darwin-x64": "3.0.21",
|
|
59
|
+
"@skalpelai/skalpel-linux-arm64": "3.0.21",
|
|
60
|
+
"@skalpelai/skalpel-linux-x64": "3.0.21",
|
|
61
|
+
"@skalpelai/skalpel-win32-x64": "3.0.21"
|
|
61
62
|
}
|
|
62
63
|
}
|
|
@@ -55,42 +55,86 @@ function shellEscapePsSQ(v) {
|
|
|
55
55
|
// the line accurately reflects whether anything will happen on the
|
|
56
56
|
// next request.
|
|
57
57
|
//
|
|
58
|
+
// Fail-open: if `skalpel status` exits non-zero (daemon down /
|
|
59
|
+
// compressor off / config corrupt), we run claude with the proxy env
|
|
60
|
+
// vars unset so requests reach Anthropic directly. Without this, a
|
|
61
|
+
// crashed daemon would leave ANTHROPIC_BASE_URL pointing at a closed
|
|
62
|
+
// port and every claude invocation would hang on connection refused.
|
|
63
|
+
//
|
|
58
64
|
// Self-disabling knobs:
|
|
59
65
|
// - SKALPEL_NO_AGENT_WRAP=1 → operator opt-out (env var, anywhere).
|
|
60
66
|
// - existing `claude` alias → we don't override aliases.
|
|
61
67
|
// - `skalpel` not on PATH → wrapper installs nothing.
|
|
62
68
|
const agentWrapPosix = `
|
|
63
|
-
# Pre-launch status hint for coding agents
|
|
64
|
-
# Status comes from \`skalpel status\` (checks daemon + engine config)
|
|
69
|
+
# Pre-launch status hint + fail-open for coding agents in this shell.
|
|
70
|
+
# Status comes from \`skalpel status\` (checks daemon + engine config);
|
|
71
|
+
# non-zero exit ⇒ unset proxy env vars so claude reaches Anthropic
|
|
72
|
+
# directly instead of hanging on a closed proxy port.
|
|
65
73
|
# Set SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
66
74
|
if [ -z "\${SKALPEL_NO_AGENT_WRAP:-}" ] && ! alias claude >/dev/null 2>&1 && command -v skalpel >/dev/null 2>&1; then
|
|
67
|
-
claude() {
|
|
75
|
+
claude() {
|
|
76
|
+
if skalpel status >&2; then
|
|
77
|
+
command claude "$@"
|
|
78
|
+
else
|
|
79
|
+
ANTHROPIC_API_URL= ANTHROPIC_BASE_URL= OPENAI_BASE_URL= OPENAI_API_BASE= SKALPEL_PROXY_URL= command claude "$@"
|
|
80
|
+
fi
|
|
81
|
+
}
|
|
68
82
|
fi`;
|
|
69
83
|
|
|
70
84
|
// agentWrapFish is the fish-shell port of agentWrapPosix.
|
|
71
85
|
const agentWrapFish = `
|
|
72
|
-
# Pre-launch status hint for coding agents
|
|
73
|
-
# Status comes from \`skalpel status\` (checks daemon + engine config)
|
|
86
|
+
# Pre-launch status hint + fail-open for coding agents in this shell.
|
|
87
|
+
# Status comes from \`skalpel status\` (checks daemon + engine config);
|
|
88
|
+
# non-zero exit ⇒ unset proxy env vars so claude reaches Anthropic
|
|
89
|
+
# directly instead of hanging on a closed proxy port.
|
|
74
90
|
# Set SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
75
91
|
if not set -q SKALPEL_NO_AGENT_WRAP; and not functions -q claude; and command -q skalpel
|
|
76
92
|
function claude
|
|
77
|
-
skalpel status 1>&2
|
|
78
|
-
|
|
93
|
+
if skalpel status 1>&2
|
|
94
|
+
command claude $argv
|
|
95
|
+
else
|
|
96
|
+
env -u ANTHROPIC_API_URL -u ANTHROPIC_BASE_URL -u OPENAI_BASE_URL -u OPENAI_API_BASE -u SKALPEL_PROXY_URL command claude $argv
|
|
97
|
+
end
|
|
79
98
|
end
|
|
80
99
|
end`;
|
|
81
100
|
|
|
82
101
|
// agentWrapPosh is the PowerShell port. Same delegation: ask the
|
|
83
102
|
// `skalpel` binary for the status, write it to stderr, then forward.
|
|
103
|
+
// On non-zero exit, scrub the proxy env vars from the child's
|
|
104
|
+
// environment for the one invocation (Remove-Item Env:… inside a
|
|
105
|
+
// scoped script block), so claude reaches Anthropic directly.
|
|
84
106
|
const agentWrapPosh = `
|
|
85
|
-
# Pre-launch status hint for coding agents
|
|
86
|
-
# Status comes from \`skalpel status\` (checks daemon + engine config)
|
|
107
|
+
# Pre-launch status hint + fail-open for coding agents in this shell.
|
|
108
|
+
# Status comes from \`skalpel status\` (checks daemon + engine config);
|
|
109
|
+
# non-zero exit ⇒ unset proxy env vars for the one claude invocation.
|
|
87
110
|
# Set $env:SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
88
111
|
$_skalpelOrigClaude = Get-Command claude.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
89
112
|
$_skalpelStatusBin = Get-Command skalpel -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
90
113
|
if (-not $env:SKALPEL_NO_AGENT_WRAP -and $_skalpelOrigClaude -and $_skalpelStatusBin) {
|
|
91
114
|
function global:claude {
|
|
92
115
|
& $script:_skalpelStatusBin.Source status 1>&2
|
|
93
|
-
|
|
116
|
+
if ($LASTEXITCODE -eq 0) {
|
|
117
|
+
& $script:_skalpelOrigClaude.Source @args
|
|
118
|
+
} else {
|
|
119
|
+
$_savedAnthropicApi = $env:ANTHROPIC_API_URL
|
|
120
|
+
$_savedAnthropicBase = $env:ANTHROPIC_BASE_URL
|
|
121
|
+
$_savedOpenAiBase = $env:OPENAI_BASE_URL
|
|
122
|
+
$_savedOpenAiApiBase = $env:OPENAI_API_BASE
|
|
123
|
+
$_savedSkalpelProxy = $env:SKALPEL_PROXY_URL
|
|
124
|
+
$env:ANTHROPIC_API_URL = ''
|
|
125
|
+
$env:ANTHROPIC_BASE_URL = ''
|
|
126
|
+
$env:OPENAI_BASE_URL = ''
|
|
127
|
+
$env:OPENAI_API_BASE = ''
|
|
128
|
+
$env:SKALPEL_PROXY_URL = ''
|
|
129
|
+
try { & $script:_skalpelOrigClaude.Source @args }
|
|
130
|
+
finally {
|
|
131
|
+
$env:ANTHROPIC_API_URL = $_savedAnthropicApi
|
|
132
|
+
$env:ANTHROPIC_BASE_URL = $_savedAnthropicBase
|
|
133
|
+
$env:OPENAI_BASE_URL = $_savedOpenAiBase
|
|
134
|
+
$env:OPENAI_API_BASE = $_savedOpenAiApiBase
|
|
135
|
+
$env:SKALPEL_PROXY_URL = $_savedSkalpelProxy
|
|
136
|
+
}
|
|
137
|
+
}
|
|
94
138
|
}
|
|
95
139
|
}`;
|
|
96
140
|
|
|
@@ -98,31 +142,53 @@ function bodyFor(shell, env) {
|
|
|
98
142
|
const note =
|
|
99
143
|
'This block is managed by skalpel install. Do not edit by hand;\n' +
|
|
100
144
|
're-run `skalpel install` or `npx skalpel` to update.';
|
|
145
|
+
// The env exports themselves are gated on `skalpel` being on PATH.
|
|
146
|
+
// Why: npm v7+ silently drops the `preuninstall` lifecycle script,
|
|
147
|
+
// so `npm uninstall -g skalpel` removes the binary but leaves this
|
|
148
|
+
// managed block in place — without the gate, every NEW shell would
|
|
149
|
+
// re-export ANTHROPIC_BASE_URL pointing at a closed proxy port and
|
|
150
|
+
// every `claude` invocation would hang. The gate makes uninstall-
|
|
151
|
+
// by-any-mechanism (npm, brew, manual rm) auto-restore plain
|
|
152
|
+
// claude on next shell sourcing. The block itself becomes inert
|
|
153
|
+
// (cosmetic comment) but does no harm. `skalpel uninstall` is
|
|
154
|
+
// still the canonical full-cleanup entry point.
|
|
101
155
|
switch (shell) {
|
|
102
156
|
case 'fish':
|
|
103
157
|
return [
|
|
104
158
|
'# Managed by skalpel install; safe to delete.',
|
|
159
|
+
'# Gated on `skalpel` being on PATH so an uninstalled binary',
|
|
160
|
+
'# leaves no broken proxy env vars behind.',
|
|
161
|
+
'if command -q skalpel',
|
|
105
162
|
...Object.entries(env).map(
|
|
106
|
-
([k, v]) => `set -gx ${k} "${shellEscapePosixDQ(v)}"`
|
|
163
|
+
([k, v]) => ` set -gx ${k} "${shellEscapePosixDQ(v)}"`
|
|
107
164
|
),
|
|
108
|
-
agentWrapFish,
|
|
165
|
+
agentWrapFish.replace(/^/gm, ' ').replace(/^ $/gm, ''),
|
|
166
|
+
'end',
|
|
109
167
|
].join('\n');
|
|
110
168
|
case 'powershell':
|
|
111
169
|
case 'powershell-legacy':
|
|
112
170
|
return [
|
|
113
171
|
...note.split('\n').map((l) => `# ${l}`),
|
|
172
|
+
'# Gated on `skalpel` being on PATH so an uninstalled binary',
|
|
173
|
+
'# leaves no broken proxy env vars behind.',
|
|
174
|
+
'if (Get-Command skalpel -CommandType Application -ErrorAction SilentlyContinue) {',
|
|
114
175
|
...Object.entries(env).map(
|
|
115
|
-
([k, v]) =>
|
|
176
|
+
([k, v]) => ` $env:${k} = '${shellEscapePsSQ(v)}'`
|
|
116
177
|
),
|
|
117
|
-
agentWrapPosh,
|
|
178
|
+
agentWrapPosh.replace(/^/gm, ' ').replace(/^ $/gm, ''),
|
|
179
|
+
'}',
|
|
118
180
|
].join('\n');
|
|
119
181
|
default:
|
|
120
182
|
return [
|
|
121
183
|
...note.split('\n').map((l) => `# ${l}`),
|
|
184
|
+
'# Gated on `skalpel` being on PATH so an uninstalled binary',
|
|
185
|
+
'# leaves no broken proxy env vars behind.',
|
|
186
|
+
'if command -v skalpel >/dev/null 2>&1; then',
|
|
122
187
|
...Object.entries(env).map(
|
|
123
|
-
([k, v]) => `export ${k}="${shellEscapePosixDQ(v)}"`
|
|
188
|
+
([k, v]) => ` export ${k}="${shellEscapePosixDQ(v)}"`
|
|
124
189
|
),
|
|
125
|
-
agentWrapPosix,
|
|
190
|
+
agentWrapPosix.replace(/^/gm, ' ').replace(/^ $/gm, ''),
|
|
191
|
+
'fi',
|
|
126
192
|
].join('\n');
|
|
127
193
|
}
|
|
128
194
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// npm `preuninstall` hook — runs before `npm uninstall -g skalpel`
|
|
3
|
+
// removes the package files. Without this, the managed rc-file block
|
|
4
|
+
// (export ANTHROPIC_BASE_URL=http://127.0.0.1:7878 …) stays behind in
|
|
5
|
+
// every shell config and leaves the user's claude CLI pointing at a
|
|
6
|
+
// dead proxy port.
|
|
7
|
+
//
|
|
8
|
+
// Strategy: locate the platform-specific `skalpel` binary (which still
|
|
9
|
+
// exists on disk at preuninstall time) and shell out to:
|
|
10
|
+
//
|
|
11
|
+
// skalpel uninstall --keep-package --keep-data
|
|
12
|
+
//
|
|
13
|
+
// `--keep-package` skips any package-removal logic on the Go side
|
|
14
|
+
// (npm is already doing that). `--keep-data` preserves auth.json /
|
|
15
|
+
// config.toml so a follow-up `npm install -g skalpel` picks the user
|
|
16
|
+
// back up where they left off; an explicit `skalpel uninstall` is
|
|
17
|
+
// still the canonical "wipe everything" entry point.
|
|
18
|
+
//
|
|
19
|
+
// Best effort: any failure (missing platform package, exec error,
|
|
20
|
+
// non-zero exit) prints a hint and exits 0 so the npm uninstall
|
|
21
|
+
// itself is never blocked. The user can always re-run
|
|
22
|
+
// `skalpel uninstall` if the binary is reachable.
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const { spawnSync } = require('child_process');
|
|
29
|
+
|
|
30
|
+
const PLATFORM_PACKAGES = {
|
|
31
|
+
'darwin-arm64': '@skalpelai/skalpel-darwin-arm64',
|
|
32
|
+
'darwin-x64': '@skalpelai/skalpel-darwin-x64',
|
|
33
|
+
'linux-arm64': '@skalpelai/skalpel-linux-arm64',
|
|
34
|
+
'linux-x64': '@skalpelai/skalpel-linux-x64',
|
|
35
|
+
'win32-x64': '@skalpelai/skalpel-win32-x64',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function findBinary() {
|
|
39
|
+
const exe = process.platform === 'win32' ? 'skalpel.exe' : 'skalpel';
|
|
40
|
+
|
|
41
|
+
// Local-dev override (mirrors npm-bin/skalpel.js): when set, look
|
|
42
|
+
// there first so a `npm link`-ed dev install can still uninstall.
|
|
43
|
+
const overrideDir = process.env.SKALPEL_BIN_DIR;
|
|
44
|
+
if (overrideDir) {
|
|
45
|
+
const c = path.join(overrideDir, exe);
|
|
46
|
+
if (fs.existsSync(c)) return c;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const key = `${process.platform}-${process.arch}`;
|
|
50
|
+
const pkg = PLATFORM_PACKAGES[key];
|
|
51
|
+
if (!pkg) return null;
|
|
52
|
+
let pkgRoot;
|
|
53
|
+
try {
|
|
54
|
+
pkgRoot = path.dirname(require.resolve(`${pkg}/package.json`));
|
|
55
|
+
} catch (_err) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const c = path.join(pkgRoot, 'bin', exe);
|
|
59
|
+
return fs.existsSync(c) ? c : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
(function main() {
|
|
63
|
+
// Allow operators to skip this hook entirely (e.g. CI tearing down
|
|
64
|
+
// a transient install on a shared box where rc edits don't matter).
|
|
65
|
+
if (process.env.SKALPEL_NO_PREUNINSTALL) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const bin = findBinary();
|
|
69
|
+
if (!bin) {
|
|
70
|
+
process.stderr.write(
|
|
71
|
+
'[skalpel] preuninstall: could not locate platform binary; ' +
|
|
72
|
+
'shell-rc env vars NOT removed. Run `skalpel uninstall` manually if the binary is still on PATH.\n'
|
|
73
|
+
);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const args = ['uninstall', '--keep-package', '--keep-data'];
|
|
77
|
+
const r = spawnSync(bin, args, { stdio: 'inherit' });
|
|
78
|
+
if (r.error) {
|
|
79
|
+
process.stderr.write(
|
|
80
|
+
`[skalpel] preuninstall: failed to exec ${bin}: ${r.error.message}\n`
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (typeof r.status === 'number' && r.status !== 0) {
|
|
85
|
+
process.stderr.write(
|
|
86
|
+
`[skalpel] preuninstall: \`${bin} ${args.join(' ')}\` exited ${r.status}; ` +
|
|
87
|
+
'rc/service cleanup may be incomplete.\n'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
})();
|