skalpel 3.0.24 → 3.0.26
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/INSTALL.md +8 -8
- package/README.md +3 -3
- package/package.json +8 -7
- package/postinstall/index.test.js +147 -0
- package/postinstall/lib/integrations.js +178 -0
- package/postinstall/lib/rc-edit.js +6 -102
- package/postinstall/lib/rc-edit.test.js +11 -0
package/INSTALL.md
CHANGED
|
@@ -37,19 +37,19 @@ All three paths produce identical binaries — same SHAs, same cosign signatures
|
|
|
37
37
|
|
|
38
38
|
## First-run flow
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
Post-W4 (OAuth port): the first-run flow is a single browser-loopback Cognito sign-in. The historical `sk-skalpel-*` API-key paste path is gone — the daemon no longer persists or sends api_keys on any code path. The flow is:
|
|
41
41
|
|
|
42
|
-
1. The user runs `npx skalpel` (or `skalpel` after a global install). The npm package is materialized; both binaries are present.
|
|
43
|
-
2.
|
|
44
|
-
3.
|
|
45
|
-
4. The
|
|
42
|
+
1. The user runs `npx skalpel login` (or `skalpel login` after a global install). The npm package is materialized; both binaries are present.
|
|
43
|
+
2. `skalpel login` opens a browser to the Skalpel hosted-UI sign-in page (Cognito), waits on a loopback HTTP callback, and validates the signed envelope returned by the callback.
|
|
44
|
+
3. On a verified envelope, the daemon writes `auth.json` to the per-OS configuration directory (per `SPEC.md` §7). The file is owned by the user with `0600` permissions and carries the Cognito JWT bundle (access_token, refresh_token, expires_at).
|
|
45
|
+
4. The user runs `skalpel` (or `npx skalpel`). The TUI detects a usable `auth.json`, invokes `skalpeld` to start the daemon, and shows the `Starting skalpeld…` splash for up to three seconds (per spec §8.5).
|
|
46
46
|
5. Once the daemon is reachable, the TUI lands the user on the Engines tab. The matrix renders for whatever agents the daemon has detected so far. If no agents have been opened since installation, the empty-state message from spec §3.2 is shown.
|
|
47
47
|
|
|
48
48
|
This flow corresponds to Journey 1 — First launch, fresh authentication — in `SPEC.md`. That narrative describes what the user feels at each step; this document describes what the install machinery actually does.
|
|
49
49
|
|
|
50
|
-
If the user runs `skalpel` without first
|
|
50
|
+
If the user runs `skalpel` without first running `skalpel login`, the TUI exits per spec §8.4 with a stderr pointer at `skalpel login`. To sign out of an existing account on this machine, run `skalpel logout` from a shell — the CLI revokes the backend session (best-effort) and removes `auth.json` via `internal/auth.Delete`.
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Historical note: prior versions of skalpel supported pasting a `sk-skalpel-*` API key copied from the web dashboard as an alternative to `skalpel login`. The W4 OAuth-port grep-delete removed both the api_key-paste wizard and the daemon-side api_key sending path. Users on stale auth.json files (api_key-only, no Cognito bundle) need to run `skalpel login` once to refresh.
|
|
53
53
|
|
|
54
54
|
## Daemon lifecycle on subsequent launches
|
|
55
55
|
|
|
@@ -123,7 +123,7 @@ The handshake is also where the TUI reads the daemon's engine list and the daemo
|
|
|
123
123
|
A small set of bundling questions are recorded here for the build phase. Each will be resolved before v1 ships; none affect the design commitments above.
|
|
124
124
|
|
|
125
125
|
- **`npx` cache eviction and version-pinning UX.** `npx skalpel` caches per-version; a user who runs `npx skalpel@1.2.3` after `1.2.4` has shipped continues to execute `1.2.3` until npm's cache resolves. The auto-update notice surfaces in the TUI, but the underlying npx semantics are an open ergonomic question.
|
|
126
|
-
-
|
|
126
|
+
- ~~**Signed-link pre-population of the API key.**~~ Obsolete post-W4 (OAuth-port grep-delete): the api_key-paste wizard the question referred to is gone. The first-run flow is browser-loopback Cognito sign-in via `skalpel login`. Left here as a historical breadcrumb for anyone tracing the pre-W4 design dockets.
|
|
127
127
|
- **Bundle size budget.** The two binaries together set a download-size budget for first-time `npx` users; whether to ship a slim TUI front-end and a separately-downloaded `skalpeld` payload via a `postinstall` fetch (still in the same npm transaction) or to ship both in the package tarball directly is a build-phase decision. Either approach preserves the bundling commitment because both binaries are still resolved and authenticated within the same install operation.
|
|
128
128
|
- **Install-time telemetry.** Whether the install path emits any telemetry event (e.g., "first install completed") is open. The TUI itself does not phone home in v1 (per the privacy stance in `SPEC.md` §05); whether the install wizard shares that posture or whether anonymous install counts are acceptable for release-quality measurement is a build-phase decision.
|
|
129
129
|
- **Windows install ergonomics.** Whether `npx skalpel` on Windows requires Windows Terminal or a recent PowerShell to render the Bubble Tea TUI's adaptive colours and key bindings cleanly, and whether a fallback message should explain the requirement, is open. The bundling commitment is platform-agnostic; the first-run experience may not be.
|
package/README.md
CHANGED
|
@@ -4,13 +4,13 @@ Skalpel is the local proxy and terminal UI for coding agents — Cursor, Claude
|
|
|
4
4
|
|
|
5
5
|
## Install and first run
|
|
6
6
|
|
|
7
|
-
After signing up at [skalpel.ai](https://skalpel.ai)
|
|
7
|
+
After signing up at [skalpel.ai](https://skalpel.ai):
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
npx skalpel
|
|
10
|
+
npx skalpel login
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
`npx` materializes both binaries
|
|
13
|
+
`npx skalpel login` materializes both binaries and runs a browser-loopback sign-in against Cognito; the daemon writes the Cognito JWT bundle to `auth.json` and the next `skalpel` launch lands you on the Engines tab. To install persistently:
|
|
14
14
|
|
|
15
15
|
```
|
|
16
16
|
npm install -g skalpel
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skalpel",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.26",
|
|
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",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"postinstall": "node postinstall/index.js",
|
|
32
32
|
"preuninstall": "node postinstall/uninstall.js",
|
|
33
33
|
"test": "echo 'no top-level tests; run make test or npm run test:rc-edit' && exit 0",
|
|
34
|
-
"test:rc-edit": "node postinstall/lib/rc-edit.test.js"
|
|
34
|
+
"test:rc-edit": "node postinstall/lib/rc-edit.test.js",
|
|
35
|
+
"test:postinstall": "node --test postinstall/index.test.js"
|
|
35
36
|
},
|
|
36
37
|
"files": [
|
|
37
38
|
"npm-bin/",
|
|
@@ -54,10 +55,10 @@
|
|
|
54
55
|
"x64"
|
|
55
56
|
],
|
|
56
57
|
"optionalDependencies": {
|
|
57
|
-
"@skalpelai/skalpel-darwin-arm64": "3.0.
|
|
58
|
-
"@skalpelai/skalpel-darwin-x64": "3.0.
|
|
59
|
-
"@skalpelai/skalpel-linux-arm64": "3.0.
|
|
60
|
-
"@skalpelai/skalpel-linux-x64": "3.0.
|
|
61
|
-
"@skalpelai/skalpel-win32-x64": "3.0.
|
|
58
|
+
"@skalpelai/skalpel-darwin-arm64": "3.0.26",
|
|
59
|
+
"@skalpelai/skalpel-darwin-x64": "3.0.26",
|
|
60
|
+
"@skalpelai/skalpel-linux-arm64": "3.0.26",
|
|
61
|
+
"@skalpelai/skalpel-linux-x64": "3.0.26",
|
|
62
|
+
"@skalpelai/skalpel-win32-x64": "3.0.26"
|
|
62
63
|
}
|
|
63
64
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Postinstall wizard regression — exercises the entry-point shipped
|
|
3
|
+
// to every `npm install skalpel` user. Tests run under Node's built-in
|
|
4
|
+
// `node --test` runner (stdlib only; matches the convention set by
|
|
5
|
+
// postinstall/lib/rc-edit.test.js).
|
|
6
|
+
//
|
|
7
|
+
// Scope (closes GAP-5):
|
|
8
|
+
// - parseArgs maps flags, env-var dry-run, and help correctly
|
|
9
|
+
// - dry-run install completes with exit 0 and writes nothing
|
|
10
|
+
// - re-running dry-run install is idempotent (no observable drift)
|
|
11
|
+
// - --help returns 0 and prints usage
|
|
12
|
+
//
|
|
13
|
+
// We exercise the wizard as a child process so the existing entry-
|
|
14
|
+
// point IIFE in index.js does not need to be refactored; tests stay
|
|
15
|
+
// hermetic by setting SKALPEL_INSTALL_DRY_RUN=1 and forcing all
|
|
16
|
+
// service / rc paths to writable t.TempDir() so a regression cannot
|
|
17
|
+
// touch a developer's real shell rc.
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const { test } = require('node:test');
|
|
22
|
+
const assert = require('node:assert/strict');
|
|
23
|
+
const { spawnSync } = require('node:child_process');
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const os = require('node:os');
|
|
26
|
+
const path = require('node:path');
|
|
27
|
+
|
|
28
|
+
const indexPath = path.join(__dirname, 'index.js');
|
|
29
|
+
|
|
30
|
+
function mkTempEnv() {
|
|
31
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'skalpel-postinstall-test-'));
|
|
32
|
+
const xdgConfig = path.join(home, '.config');
|
|
33
|
+
fs.mkdirSync(xdgConfig, { recursive: true });
|
|
34
|
+
return {
|
|
35
|
+
HOME: home,
|
|
36
|
+
USERPROFILE: home,
|
|
37
|
+
XDG_CONFIG_HOME: xdgConfig,
|
|
38
|
+
SKALPEL_INSTALL_DRY_RUN: '1',
|
|
39
|
+
SKALPEL_BIN_DIR: path.join(home, 'bin'),
|
|
40
|
+
NO_COLOR: '1',
|
|
41
|
+
PATH: process.env.PATH || '',
|
|
42
|
+
// Force npx detection off so the wizard reaches its full step
|
|
43
|
+
// sequence rather than short-circuiting.
|
|
44
|
+
npm_config_user_agent: 'npm/test',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function runWizard(extraArgs = []) {
|
|
49
|
+
const env = mkTempEnv();
|
|
50
|
+
return spawnSync(process.execPath, [indexPath, ...extraArgs], {
|
|
51
|
+
env,
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
timeout: 15_000,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test('parseArgs maps short and long flags', () => {
|
|
58
|
+
// require()-time evaluation must not run the IIFE: index.js gates
|
|
59
|
+
// it with `if (require.main === module)`, so require() is safe.
|
|
60
|
+
const mod = require('./index');
|
|
61
|
+
assert.equal(typeof mod.parseArgs, 'function', 'parseArgs export missing');
|
|
62
|
+
const dry = mod.parseArgs(['node', 'index.js', '--dry-run']);
|
|
63
|
+
assert.equal(dry.dryRun, true);
|
|
64
|
+
const help = mod.parseArgs(['node', 'index.js', '-h']);
|
|
65
|
+
assert.equal(help.help, true);
|
|
66
|
+
const verbose = mod.parseArgs(['node', 'index.js', '--verbose']);
|
|
67
|
+
assert.equal(verbose.verbose, true);
|
|
68
|
+
const skipBundle = mod.parseArgs(['node', 'index.js', '--skip-bundle']);
|
|
69
|
+
assert.equal(skipBundle.skipBundle, true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('SKALPEL_INSTALL_DRY_RUN=1 forces dry-run even without --dry-run', () => {
|
|
73
|
+
const prev = process.env.SKALPEL_INSTALL_DRY_RUN;
|
|
74
|
+
process.env.SKALPEL_INSTALL_DRY_RUN = '1';
|
|
75
|
+
try {
|
|
76
|
+
const mod = require('./index');
|
|
77
|
+
const opts = mod.parseArgs(['node', 'index.js']);
|
|
78
|
+
assert.equal(opts.dryRun, true);
|
|
79
|
+
} finally {
|
|
80
|
+
if (prev === undefined) delete process.env.SKALPEL_INSTALL_DRY_RUN;
|
|
81
|
+
else process.env.SKALPEL_INSTALL_DRY_RUN = prev;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('--help exits 0 and prints usage', () => {
|
|
86
|
+
const r = runWizard(['--help']);
|
|
87
|
+
assert.equal(r.status, 0, `stderr: ${r.stderr}`);
|
|
88
|
+
assert.match(r.stdout, /postinstall wizard/i);
|
|
89
|
+
assert.match(r.stdout, /usage:/i);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('dry-run install completes with exit 0', () => {
|
|
93
|
+
const r = runWizard(['--dry-run']);
|
|
94
|
+
// Wizard contract: even on critical step failure, return 0 so npm
|
|
95
|
+
// install never aborts. Tests assert the *invariant*, not the path.
|
|
96
|
+
assert.equal(r.status, 0, `non-zero exit: stderr=${r.stderr}\nstdout=${r.stdout}`);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('dry-run install is idempotent: second run mirrors first', () => {
|
|
100
|
+
// Same temp HOME both runs; the wizard should not leave behind
|
|
101
|
+
// state that perturbs the second run. We compare exit codes; output
|
|
102
|
+
// diffs are tolerated because timestamps / random suffixes vary.
|
|
103
|
+
const env = mkTempEnv();
|
|
104
|
+
function once() {
|
|
105
|
+
return spawnSync(process.execPath, [indexPath, '--dry-run'], {
|
|
106
|
+
env,
|
|
107
|
+
encoding: 'utf8',
|
|
108
|
+
timeout: 15_000,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const r1 = once();
|
|
112
|
+
const r2 = once();
|
|
113
|
+
assert.equal(r1.status, 0, `first run: stderr=${r1.stderr}`);
|
|
114
|
+
assert.equal(r2.status, 0, `second run: stderr=${r2.stderr}`);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('dry-run install does not write to HOME', () => {
|
|
118
|
+
const env = mkTempEnv();
|
|
119
|
+
const home = env.HOME;
|
|
120
|
+
// Capture initial file inventory.
|
|
121
|
+
function snapshot(dir) {
|
|
122
|
+
const acc = [];
|
|
123
|
+
(function walk(p) {
|
|
124
|
+
let entries;
|
|
125
|
+
try { entries = fs.readdirSync(p, { withFileTypes: true }); }
|
|
126
|
+
catch { return; }
|
|
127
|
+
for (const e of entries) {
|
|
128
|
+
const full = path.join(p, e.name);
|
|
129
|
+
acc.push(full);
|
|
130
|
+
if (e.isDirectory()) walk(full);
|
|
131
|
+
}
|
|
132
|
+
})(dir);
|
|
133
|
+
return acc.sort();
|
|
134
|
+
}
|
|
135
|
+
const before = snapshot(home);
|
|
136
|
+
spawnSync(process.execPath, [indexPath, '--dry-run'], {
|
|
137
|
+
env,
|
|
138
|
+
encoding: 'utf8',
|
|
139
|
+
timeout: 15_000,
|
|
140
|
+
});
|
|
141
|
+
const after = snapshot(home);
|
|
142
|
+
// Dry-run must not create *new* files in HOME beyond what mkTempEnv
|
|
143
|
+
// pre-seeded (.config). Any drift is a regression on the dry-run
|
|
144
|
+
// contract.
|
|
145
|
+
const added = after.filter((p) => !before.includes(p));
|
|
146
|
+
assert.deepEqual(added, [], `dry-run wrote files: ${added.join(', ')}`);
|
|
147
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function envBlockValues(port) {
|
|
4
|
+
const p = String(port || 7878);
|
|
5
|
+
const root = `http://127.0.0.1:${p}`;
|
|
6
|
+
return {
|
|
7
|
+
ANTHROPIC_API_URL: root,
|
|
8
|
+
ANTHROPIC_BASE_URL: root,
|
|
9
|
+
OPENAI_BASE_URL: `${root}/v1`,
|
|
10
|
+
OPENAI_API_BASE: `${root}/v1`,
|
|
11
|
+
SKALPEL_PROXY_URL: root,
|
|
12
|
+
SKALPEL_CODEX_OPENAI_BASE_URL: `${root}/v1`,
|
|
13
|
+
SKALPEL_CODEX_CHATGPT_BASE_URL: `${root}/backend-api`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const claudeCode = {
|
|
18
|
+
name: 'claude-code',
|
|
19
|
+
wrappers: {
|
|
20
|
+
posix: `
|
|
21
|
+
# Pre-launch status hint + fail-open for Claude Code.
|
|
22
|
+
# Set SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
23
|
+
if [ -z "\${SKALPEL_NO_AGENT_WRAP:-}" ] && ! alias claude >/dev/null 2>&1 && command -v skalpel >/dev/null 2>&1; then
|
|
24
|
+
claude() {
|
|
25
|
+
if skalpel status >&2; then
|
|
26
|
+
command claude "$@"
|
|
27
|
+
else
|
|
28
|
+
ANTHROPIC_API_URL= ANTHROPIC_BASE_URL= OPENAI_BASE_URL= OPENAI_API_BASE= SKALPEL_PROXY_URL= SKALPEL_CODEX_OPENAI_BASE_URL= SKALPEL_CODEX_CHATGPT_BASE_URL= command claude "$@"
|
|
29
|
+
fi
|
|
30
|
+
}
|
|
31
|
+
fi`,
|
|
32
|
+
fish: `
|
|
33
|
+
# Pre-launch status hint + fail-open for Claude Code.
|
|
34
|
+
# Set SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
35
|
+
if not set -q SKALPEL_NO_AGENT_WRAP; and not functions -q claude; and command -q skalpel
|
|
36
|
+
function claude
|
|
37
|
+
if skalpel status 1>&2
|
|
38
|
+
command claude $argv
|
|
39
|
+
else
|
|
40
|
+
env -u ANTHROPIC_API_URL -u ANTHROPIC_BASE_URL -u OPENAI_BASE_URL -u OPENAI_API_BASE -u SKALPEL_PROXY_URL -u SKALPEL_CODEX_OPENAI_BASE_URL -u SKALPEL_CODEX_CHATGPT_BASE_URL command claude $argv
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end`,
|
|
44
|
+
powershell: `
|
|
45
|
+
# Pre-launch status hint + fail-open for Claude Code.
|
|
46
|
+
# Set $env:SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
47
|
+
$_skalpelOrigClaude = Get-Command claude.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
48
|
+
$_skalpelStatusBin = Get-Command skalpel -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
49
|
+
if (-not $env:SKALPEL_NO_AGENT_WRAP -and $_skalpelOrigClaude -and $_skalpelStatusBin) {
|
|
50
|
+
function global:claude {
|
|
51
|
+
& $script:_skalpelStatusBin.Source status 1>&2
|
|
52
|
+
if ($LASTEXITCODE -eq 0) {
|
|
53
|
+
& $script:_skalpelOrigClaude.Source @args
|
|
54
|
+
} else {
|
|
55
|
+
$_savedAnthropicApi = $env:ANTHROPIC_API_URL
|
|
56
|
+
$_savedAnthropicBase = $env:ANTHROPIC_BASE_URL
|
|
57
|
+
$_savedOpenAiBase = $env:OPENAI_BASE_URL
|
|
58
|
+
$_savedOpenAiApiBase = $env:OPENAI_API_BASE
|
|
59
|
+
$_savedSkalpelProxy = $env:SKALPEL_PROXY_URL
|
|
60
|
+
$_savedCodexOpenAi = $env:SKALPEL_CODEX_OPENAI_BASE_URL
|
|
61
|
+
$_savedCodexChatGPT = $env:SKALPEL_CODEX_CHATGPT_BASE_URL
|
|
62
|
+
$env:ANTHROPIC_API_URL = ''
|
|
63
|
+
$env:ANTHROPIC_BASE_URL = ''
|
|
64
|
+
$env:OPENAI_BASE_URL = ''
|
|
65
|
+
$env:OPENAI_API_BASE = ''
|
|
66
|
+
$env:SKALPEL_PROXY_URL = ''
|
|
67
|
+
$env:SKALPEL_CODEX_OPENAI_BASE_URL = ''
|
|
68
|
+
$env:SKALPEL_CODEX_CHATGPT_BASE_URL = ''
|
|
69
|
+
try { & $script:_skalpelOrigClaude.Source @args }
|
|
70
|
+
finally {
|
|
71
|
+
$env:ANTHROPIC_API_URL = $_savedAnthropicApi
|
|
72
|
+
$env:ANTHROPIC_BASE_URL = $_savedAnthropicBase
|
|
73
|
+
$env:OPENAI_BASE_URL = $_savedOpenAiBase
|
|
74
|
+
$env:OPENAI_API_BASE = $_savedOpenAiApiBase
|
|
75
|
+
$env:SKALPEL_PROXY_URL = $_savedSkalpelProxy
|
|
76
|
+
$env:SKALPEL_CODEX_OPENAI_BASE_URL = $_savedCodexOpenAi
|
|
77
|
+
$env:SKALPEL_CODEX_CHATGPT_BASE_URL = $_savedCodexChatGPT
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}`,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const codex = {
|
|
86
|
+
name: 'codex',
|
|
87
|
+
wrappers: {
|
|
88
|
+
posix: `
|
|
89
|
+
# Codex CLI/App integration. Config overrides are native Codex -c knobs.
|
|
90
|
+
# Set SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
91
|
+
if [ -z "\${SKALPEL_NO_AGENT_WRAP:-}" ] && ! alias codex >/dev/null 2>&1 && command -v codex >/dev/null 2>&1 && command -v skalpel >/dev/null 2>&1; then
|
|
92
|
+
codex() {
|
|
93
|
+
if skalpel status >&2; then
|
|
94
|
+
_skalpel_codex_home="\${CODEX_HOME:-$HOME/.codex}"
|
|
95
|
+
_skalpel_codex_auth_mode="$(awk -F'"' '/"auth_mode"/ { print $4; exit }' "\${_skalpel_codex_home}/auth.json" 2>/dev/null)"
|
|
96
|
+
case "\${_skalpel_codex_auth_mode}" in
|
|
97
|
+
apikey|api_key|api)
|
|
98
|
+
command codex -c "openai_base_url='\${SKALPEL_CODEX_OPENAI_BASE_URL}'" "$@"
|
|
99
|
+
;;
|
|
100
|
+
*)
|
|
101
|
+
command codex -c "chatgpt_base_url='\${SKALPEL_CODEX_CHATGPT_BASE_URL}'" "$@"
|
|
102
|
+
;;
|
|
103
|
+
esac
|
|
104
|
+
else
|
|
105
|
+
command codex "$@"
|
|
106
|
+
fi
|
|
107
|
+
}
|
|
108
|
+
fi`,
|
|
109
|
+
fish: `
|
|
110
|
+
# Codex CLI/App integration. Config overrides are native Codex -c knobs.
|
|
111
|
+
# Set SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
112
|
+
if not set -q SKALPEL_NO_AGENT_WRAP; and not functions -q codex; and command -q codex; and command -q skalpel
|
|
113
|
+
function codex
|
|
114
|
+
if skalpel status 1>&2
|
|
115
|
+
set -l _skalpel_codex_home "$HOME/.codex"
|
|
116
|
+
if set -q CODEX_HOME
|
|
117
|
+
set _skalpel_codex_home "$CODEX_HOME"
|
|
118
|
+
end
|
|
119
|
+
set -l _skalpel_codex_auth_mode (awk -F'"' '/"auth_mode"/ { print $4; exit }' "$_skalpel_codex_home/auth.json" 2>/dev/null)
|
|
120
|
+
switch "$_skalpel_codex_auth_mode"
|
|
121
|
+
case apikey api_key api
|
|
122
|
+
command codex -c "openai_base_url='$SKALPEL_CODEX_OPENAI_BASE_URL'" $argv
|
|
123
|
+
case '*'
|
|
124
|
+
command codex -c "chatgpt_base_url='$SKALPEL_CODEX_CHATGPT_BASE_URL'" $argv
|
|
125
|
+
end
|
|
126
|
+
else
|
|
127
|
+
command codex $argv
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end`,
|
|
131
|
+
powershell: `
|
|
132
|
+
# Codex CLI/App integration. Config overrides are native Codex -c knobs.
|
|
133
|
+
# Set $env:SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
134
|
+
$_skalpelOrigCodex = Get-Command codex.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
135
|
+
$_skalpelStatusBin = Get-Command skalpel -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
136
|
+
if (-not $env:SKALPEL_NO_AGENT_WRAP -and $_skalpelOrigCodex -and $_skalpelStatusBin) {
|
|
137
|
+
function global:codex {
|
|
138
|
+
& $script:_skalpelStatusBin.Source status 1>&2
|
|
139
|
+
if ($LASTEXITCODE -eq 0) {
|
|
140
|
+
$_skalpelCodexHome = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $HOME '.codex' }
|
|
141
|
+
$_skalpelCodexAuthMode = ''
|
|
142
|
+
try {
|
|
143
|
+
$_skalpelCodexAuthPath = Join-Path $_skalpelCodexHome 'auth.json'
|
|
144
|
+
if (Test-Path $_skalpelCodexAuthPath) {
|
|
145
|
+
$_skalpelCodexAuthMode = (Get-Content -Raw $_skalpelCodexAuthPath | ConvertFrom-Json).auth_mode
|
|
146
|
+
}
|
|
147
|
+
} catch {}
|
|
148
|
+
if ($_skalpelCodexAuthMode -in @('apikey', 'api_key', 'api')) {
|
|
149
|
+
& $script:_skalpelOrigCodex.Source -c "openai_base_url='$env:SKALPEL_CODEX_OPENAI_BASE_URL'" @args
|
|
150
|
+
} else {
|
|
151
|
+
& $script:_skalpelOrigCodex.Source -c "chatgpt_base_url='$env:SKALPEL_CODEX_CHATGPT_BASE_URL'" @args
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
& $script:_skalpelOrigCodex.Source @args
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}`,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const registry = [claudeCode, codex];
|
|
162
|
+
|
|
163
|
+
function wrapperBlock(shell) {
|
|
164
|
+
const key = shell === 'fish'
|
|
165
|
+
? 'fish'
|
|
166
|
+
: shell === 'powershell' || shell === 'powershell-legacy'
|
|
167
|
+
? 'powershell'
|
|
168
|
+
: 'posix';
|
|
169
|
+
return registry.map((integration) => integration.wrappers[key]).join('\n');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
claudeCode,
|
|
174
|
+
codex,
|
|
175
|
+
envBlockValues,
|
|
176
|
+
registry,
|
|
177
|
+
wrapperBlock,
|
|
178
|
+
};
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
|
+
const integrations = require('./integrations');
|
|
13
14
|
|
|
14
15
|
// Decision D260 / B30 — vendor-prefixed fence format:
|
|
15
16
|
// # >>> @skalpelai/skalpel begin (managed)
|
|
@@ -20,15 +21,7 @@ const FENCE_BEGIN_PS = '# >>> @skalpelai/skalpel begin (managed) >>>';
|
|
|
20
21
|
const FENCE_END_PS = '# <<< @skalpelai/skalpel end (managed) <<<';
|
|
21
22
|
|
|
22
23
|
function envBlockValues(port) {
|
|
23
|
-
|
|
24
|
-
const root = `http://127.0.0.1:${p}`;
|
|
25
|
-
return {
|
|
26
|
-
ANTHROPIC_API_URL: root,
|
|
27
|
-
ANTHROPIC_BASE_URL: root,
|
|
28
|
-
OPENAI_BASE_URL: `${root}/v1`,
|
|
29
|
-
OPENAI_API_BASE: `${root}/v1`,
|
|
30
|
-
SKALPEL_PROXY_URL: root,
|
|
31
|
-
};
|
|
24
|
+
return integrations.envBlockValues(port);
|
|
32
25
|
}
|
|
33
26
|
|
|
34
27
|
function fenceFor(shell) {
|
|
@@ -48,96 +41,6 @@ function shellEscapePsSQ(v) {
|
|
|
48
41
|
return String(v).replace(/'/g, "''");
|
|
49
42
|
}
|
|
50
43
|
|
|
51
|
-
// agentWrapPosix is a tiny bash/zsh-portable shim that prints a
|
|
52
|
-
// one-line "skalpel active/inactive" hint right before launching
|
|
53
|
-
// `claude`. The status is computed by `skalpel status` which checks
|
|
54
|
-
// both the daemon socket AND the compressor toggle in config.toml so
|
|
55
|
-
// the line accurately reflects whether anything will happen on the
|
|
56
|
-
// next request.
|
|
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
|
-
//
|
|
64
|
-
// Self-disabling knobs:
|
|
65
|
-
// - SKALPEL_NO_AGENT_WRAP=1 → operator opt-out (env var, anywhere).
|
|
66
|
-
// - existing `claude` alias → we don't override aliases.
|
|
67
|
-
// - `skalpel` not on PATH → wrapper installs nothing.
|
|
68
|
-
const agentWrapPosix = `
|
|
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.
|
|
73
|
-
# Set SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
74
|
-
if [ -z "\${SKALPEL_NO_AGENT_WRAP:-}" ] && ! alias claude >/dev/null 2>&1 && command -v skalpel >/dev/null 2>&1; then
|
|
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
|
-
}
|
|
82
|
-
fi`;
|
|
83
|
-
|
|
84
|
-
// agentWrapFish is the fish-shell port of agentWrapPosix.
|
|
85
|
-
const agentWrapFish = `
|
|
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.
|
|
90
|
-
# Set SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
91
|
-
if not set -q SKALPEL_NO_AGENT_WRAP; and not functions -q claude; and command -q skalpel
|
|
92
|
-
function claude
|
|
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
|
|
98
|
-
end
|
|
99
|
-
end`;
|
|
100
|
-
|
|
101
|
-
// agentWrapPosh is the PowerShell port. Same delegation: ask the
|
|
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.
|
|
106
|
-
const agentWrapPosh = `
|
|
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.
|
|
110
|
-
# Set $env:SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
111
|
-
$_skalpelOrigClaude = Get-Command claude.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
112
|
-
$_skalpelStatusBin = Get-Command skalpel -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
113
|
-
if (-not $env:SKALPEL_NO_AGENT_WRAP -and $_skalpelOrigClaude -and $_skalpelStatusBin) {
|
|
114
|
-
function global:claude {
|
|
115
|
-
& $script:_skalpelStatusBin.Source status 1>&2
|
|
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
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}`;
|
|
140
|
-
|
|
141
44
|
function bodyFor(shell, env) {
|
|
142
45
|
const note =
|
|
143
46
|
'This block is managed by skalpel install. Do not edit by hand;\n' +
|
|
@@ -162,7 +65,7 @@ function bodyFor(shell, env) {
|
|
|
162
65
|
...Object.entries(env).map(
|
|
163
66
|
([k, v]) => ` set -gx ${k} "${shellEscapePosixDQ(v)}"`
|
|
164
67
|
),
|
|
165
|
-
|
|
68
|
+
integrations.wrapperBlock(shell).replace(/^/gm, ' ').replace(/^ $/gm, ''),
|
|
166
69
|
'end',
|
|
167
70
|
].join('\n');
|
|
168
71
|
case 'powershell':
|
|
@@ -175,7 +78,7 @@ function bodyFor(shell, env) {
|
|
|
175
78
|
...Object.entries(env).map(
|
|
176
79
|
([k, v]) => ` $env:${k} = '${shellEscapePsSQ(v)}'`
|
|
177
80
|
),
|
|
178
|
-
|
|
81
|
+
integrations.wrapperBlock(shell).replace(/^/gm, ' ').replace(/^ $/gm, ''),
|
|
179
82
|
'}',
|
|
180
83
|
].join('\n');
|
|
181
84
|
default:
|
|
@@ -187,7 +90,7 @@ function bodyFor(shell, env) {
|
|
|
187
90
|
...Object.entries(env).map(
|
|
188
91
|
([k, v]) => ` export ${k}="${shellEscapePosixDQ(v)}"`
|
|
189
92
|
),
|
|
190
|
-
|
|
93
|
+
integrations.wrapperBlock(shell).replace(/^/gm, ' ').replace(/^ $/gm, ''),
|
|
191
94
|
'fi',
|
|
192
95
|
].join('\n');
|
|
193
96
|
}
|
|
@@ -257,6 +160,7 @@ module.exports = {
|
|
|
257
160
|
shellEscapePosixDQ,
|
|
258
161
|
shellEscapePsSQ,
|
|
259
162
|
xmlEscape,
|
|
163
|
+
integrations,
|
|
260
164
|
FENCE_BEGIN_POSIX,
|
|
261
165
|
FENCE_END_POSIX,
|
|
262
166
|
FENCE_BEGIN_PS,
|
|
@@ -187,6 +187,17 @@ function run() {
|
|
|
187
187
|
}
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
+
test('TestRcEdit_Body_Has_Codex_Wrapper', () => {
|
|
191
|
+
const env = rc.envBlockValues(7878);
|
|
192
|
+
const block = rc.buildBlock('bash', env);
|
|
193
|
+
assert.ok(block.includes('codex()'), 'missing codex wrapper');
|
|
194
|
+
assert.ok(block.includes('openai_base_url'), 'missing Codex OpenAI base config');
|
|
195
|
+
assert.ok(block.includes('chatgpt_base_url'), 'missing Codex ChatGPT base config');
|
|
196
|
+
assert.ok(block.includes('auth_mode'), 'missing Codex auth-mode routing');
|
|
197
|
+
assert.ok(block.includes('http://127.0.0.1:7878/backend-api'), 'missing Codex ChatGPT backend base');
|
|
198
|
+
assert.ok(!block.includes('network.proxy_url'), 'Codex wrapper should not generic-proxy metadata routes');
|
|
199
|
+
});
|
|
200
|
+
|
|
190
201
|
process.stdout.write(`\n pass=${pass} fail=${fail}\n`);
|
|
191
202
|
return fail === 0 ? 0 : 1;
|
|
192
203
|
}
|