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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skalpel",
3
- "version": "3.0.20",
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.20",
57
- "@skalpelai/skalpel-darwin-x64": "3.0.20",
58
- "@skalpelai/skalpel-linux-arm64": "3.0.20",
59
- "@skalpelai/skalpel-linux-x64": "3.0.20",
60
- "@skalpelai/skalpel-win32-x64": "3.0.20"
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 launched in this shell.
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() { skalpel status >&2; command 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 launched in this shell.
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
- command claude $argv
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 launched in this shell.
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
- & $script:_skalpelOrigClaude.Source @args
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]) => `$env:${k} = '${shellEscapePsSQ(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
+ })();