skalpel 3.4.19 → 4.0.0

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.
Files changed (55) hide show
  1. package/README.md +29 -37
  2. package/auth.mjs +179 -0
  3. package/{prosumer-hooks/bootstrap.mjs → bootstrap.mjs} +14 -8
  4. package/{prosumer-hooks/incremental-ingest.mjs → incremental-ingest.mjs} +5 -10
  5. package/{prosumer-hooks/install.mjs → install.mjs} +6 -9
  6. package/login.mjs +164 -0
  7. package/package.json +17 -57
  8. package/postinstall.mjs +68 -0
  9. package/skalpel-setup.mjs +896 -0
  10. package/{prosumer-hooks/skalpel-statusline.mjs → skalpel-statusline.mjs} +1 -2
  11. package/INSTALL.md +0 -250
  12. package/LICENSE +0 -201
  13. package/design-tokens.json +0 -52
  14. package/npm-bin/colors.js +0 -125
  15. package/npm-bin/skalpel.bug-0039.test.js +0 -232
  16. package/npm-bin/skalpel.js +0 -517
  17. package/npm-bin/skalpeld.js +0 -20
  18. package/postinstall/index.js +0 -277
  19. package/postinstall/index.test.js +0 -147
  20. package/postinstall/launchd/com.skalpel.skalpeld.plist.tmpl +0 -46
  21. package/postinstall/lib/ca-install.js +0 -268
  22. package/postinstall/lib/ca-install.test.js +0 -327
  23. package/postinstall/lib/detect-prior.js +0 -62
  24. package/postinstall/lib/env-inject.js +0 -105
  25. package/postinstall/lib/integrations.js +0 -207
  26. package/postinstall/lib/launch.js +0 -38
  27. package/postinstall/lib/log.js +0 -31
  28. package/postinstall/lib/paths.js +0 -236
  29. package/postinstall/lib/prosumer-hooks.js +0 -146
  30. package/postinstall/lib/prosumer-hooks.test.js +0 -278
  31. package/postinstall/lib/rc-edit.js +0 -168
  32. package/postinstall/lib/rc-edit.test.js +0 -243
  33. package/postinstall/lib/run-tests.js +0 -59
  34. package/postinstall/lib/service-register.js +0 -248
  35. package/postinstall/lib/sign-in.js +0 -80
  36. package/postinstall/lib/template.js +0 -36
  37. package/postinstall/preinstall.js +0 -111
  38. package/postinstall/preinstall.test.js +0 -47
  39. package/postinstall/snippets/bash.sh.tmpl +0 -12
  40. package/postinstall/snippets/fish.fish.tmpl +0 -11
  41. package/postinstall/snippets/powershell.ps1.tmpl +0 -12
  42. package/postinstall/snippets/zsh.sh.tmpl +0 -13
  43. package/postinstall/systemd/skalpeld.service.tmpl +0 -34
  44. package/postinstall/uninstall.js +0 -98
  45. package/postinstall/windows/Task.xml.tmpl +0 -42
  46. package/postinstall/windows/register-task.ps1.tmpl +0 -45
  47. package/prosumer-hooks/auth.mjs +0 -93
  48. package/prosumer-hooks/bootstrap.test.mjs +0 -93
  49. package/prosumer-hooks/incremental-ingest.test.mjs +0 -320
  50. /package/{prosumer-hooks/insights.mjs → insights.mjs} +0 -0
  51. /package/{prosumer-hooks/metrics.mjs → metrics.mjs} +0 -0
  52. /package/{prosumer-hooks/skalpel-hook-session-end.mjs → skalpel-hook-session-end.mjs} +0 -0
  53. /package/{prosumer-hooks/skalpel-hook-session.mjs → skalpel-hook-session.mjs} +0 -0
  54. /package/{prosumer-hooks/skalpel-hook.mjs → skalpel-hook.mjs} +0 -0
  55. /package/{prosumer-hooks/stats.mjs → stats.mjs} +0 -0
@@ -1,277 +0,0 @@
1
- #!/usr/bin/env node
2
- // Postinstall wizard.
3
- //
4
- // 5-step flow per SPEC.md §9.6:
5
- // 1. detect-prior — probe configDir for prior install
6
- // 2. sign-in — `skalpel login` (or hint to run it)
7
- // 3. service-register — render + load launchd / systemd / Task
8
- // 4. env-inject — managed-block edit of shell rc files
9
- // 5. launch — print next-step hint
10
- //
11
- // Dry-run: SKALPEL_INSTALL_DRY_RUN=1 (env) or --dry-run (argv) makes
12
- // the wizard log every action it would take and write nothing.
13
- //
14
- // Idempotence: the wizard is safe to re-run. Each step replaces or
15
- // skips rather than duplicating.
16
-
17
- 'use strict';
18
-
19
- const fs = require('fs');
20
- const path = require('path');
21
-
22
- const colors = require('../npm-bin/colors');
23
- const log = require('./lib/log');
24
- const paths = require('./lib/paths');
25
- const detectPrior = require('./lib/detect-prior');
26
- const signIn = require('./lib/sign-in');
27
- const serviceRegister = require('./lib/service-register');
28
- const envInject = require('./lib/env-inject');
29
- const launch = require('./lib/launch');
30
- const caInstall = require('./lib/ca-install');
31
- const prosumerHooks = require('./lib/prosumer-hooks');
32
-
33
- function printStyledSuccess() {
34
- if (!process.stdout.isTTY) return;
35
- const colored = !process.env.NO_COLOR;
36
- const platformLabel = `${process.platform}-${process.arch}`;
37
- const check = colored ? colors.theme.mint('✓') : '✓';
38
- const text = colored
39
- ? colors.theme.text(` skalpel installed for ${platformLabel}`)
40
- : ` skalpel installed for ${platformLabel}`;
41
- process.stdout.write(`${check}${text}\n`);
42
- }
43
-
44
- function parseArgs(argv) {
45
- const out = {
46
- dryRun: false,
47
- skipBundle: false,
48
- verbose: false,
49
- };
50
- for (const a of argv.slice(2)) {
51
- switch (a) {
52
- case '--dry-run':
53
- out.dryRun = true;
54
- break;
55
- case '--verbose':
56
- out.verbose = true;
57
- break;
58
- case '--skip-bundle':
59
- out.skipBundle = true;
60
- break;
61
- case '--help':
62
- case '-h':
63
- out.help = true;
64
- break;
65
- default:
66
- // Unknown args are ignored for npm-postinstall robustness;
67
- // the user's npm CLI may pass extra flags.
68
- break;
69
- }
70
- }
71
- if (process.env.SKALPEL_INSTALL_DRY_RUN === '1') {
72
- out.dryRun = true;
73
- }
74
- return out;
75
- }
76
-
77
- function helpText() {
78
- return [
79
- 'skalpel postinstall wizard',
80
- '',
81
- 'usage: node postinstall/index.js [--dry-run] [--verbose]',
82
- '',
83
- 'Run automatically by npm install. The wizard performs:',
84
- ' 1. detect-prior probe configDir for an existing install',
85
- ' 2. behavior-hooks stage the local graph hook runtime',
86
- ' 3. sign-in start `skalpel login` (or hint at it)',
87
- ' 4. service-register register the per-OS daemon entry',
88
- ' 5. ca-install install the local interception CA',
89
- ' 6. shell-activation defer wrapping until `skalpel setup`',
90
- ' 7. launch print the run-skalpel hint',
91
- '',
92
- 'Uninstall is owned by the Go binary now — run `skalpel uninstall`',
93
- 'or `npx skalpel uninstall` to reverse this wizard.',
94
- '',
95
- 'Dry-run mode (SKALPEL_INSTALL_DRY_RUN=1 or --dry-run) makes every',
96
- 'step log what it would do and write nothing.',
97
- ].join('\n');
98
- }
99
-
100
- async function runCAInstallStep(opts) {
101
- // Resolve CA path by spawning the just-installed `skalpel ca-path`.
102
- // (The Go binary is on PATH by the time postinstall runs; see
103
- // npm-bin/skalpel.js for the resolver.)
104
- const cp = require('child_process');
105
- let caPath = '';
106
- try {
107
- const out = cp.spawnSync('skalpel', ['ca-path'], {
108
- stdio: ['ignore', 'pipe', 'pipe'],
109
- timeout: 5_000,
110
- });
111
- if (out.status === 0) {
112
- caPath = String(out.stdout || '').trim();
113
- }
114
- } catch (_) {
115
- // ignored — fall through to deferred-no-ca branch
116
- }
117
- if (!caPath) {
118
- log.warn('ca-install: could not resolve `skalpel ca-path`; deferring trust-store install to first login');
119
- return { ok: false, reason: 'no-binary' };
120
- }
121
- if (opts.dryRun) {
122
- log.info(`ca-install: dry-run; would install ${caPath} into OS trust store`);
123
- return { ok: true, skipped: true, reason: 'dry-run' };
124
- }
125
- return caInstall.installDaemonCA(caPath);
126
- }
127
-
128
- async function main(argv) {
129
- const opts = parseArgs(argv);
130
- if (opts.help) {
131
- process.stdout.write(`${helpText()}\n`);
132
- return 0;
133
- }
134
-
135
- // npx mode: skip the install wizard. The wizard's service-register
136
- // step would bake the npx cache path (~/.npm/_npx/<hash>/...) into
137
- // the OS service unit, and that path is evicted by npx's cache GC.
138
- // Exit 0 instead of 1 so npm install completes and the binary
139
- // becomes invokable. (Uninstall is owned by the Go binary now and
140
- // is unaffected.)
141
- if (paths.isNpxInvocation()) {
142
- log.info(
143
- 'npx mode detected — skipping install wizard (service-register / ' +
144
- 'env-inject would bake transient npx cache paths into your system).'
145
- );
146
- log.info(
147
- 'For a persistent install with daemon-on-boot and shell env vars, ' +
148
- 'run: `npm install -g skalpel`'
149
- );
150
- return 0;
151
- }
152
-
153
- const total = 7;
154
- const mode = opts.dryRun ? 'dry-run' : 'live';
155
- log.info(`postinstall wizard starting (${mode} install) on ${process.platform}`);
156
-
157
- // B28: classify failures. Critical = re-throw and exit 0 (we do
158
- // not want npm install to abort, but the operator should know).
159
- // Warning = collect and log at end.
160
- const allWarnings = [];
161
-
162
- let prior = false;
163
- let critical = null;
164
- try {
165
- log.step(1, total, 'detect-prior', 'probing configDir');
166
- const r = detectPrior.run({ dryRun: opts.dryRun });
167
- prior = r.prior;
168
-
169
- log.step(2, total, 'behavior-hooks', 'staging CLI-owned runtime');
170
- try {
171
- const hooks = prosumerHooks.stageAndHeal({ dryRun: opts.dryRun });
172
- if (hooks.healed) {
173
- log.info('behavior-hooks: refreshed an existing skalpel hook installation');
174
- } else if (hooks.statuslineManaged) {
175
- log.info(
176
- 'behavior-hooks: runtime staged; Claude status line enabled at 0.0h; ' +
177
- 'prompt hooks activate automatically after `skalpel login`'
178
- );
179
- } else {
180
- log.info('behavior-hooks: runtime staged; hooks activate automatically after `skalpel login`');
181
- }
182
- } catch (err) {
183
- log.warn(`behavior-hooks: ${err.message}; continuing`);
184
- allWarnings.push(`behavior-hooks: ${err.message}`);
185
- }
186
-
187
- log.step(3, total, 'sign-in', prior ? 'skipping (prior auth)' : 'browser flow');
188
- // sign-in is non-critical: a missing dispatcher / login failure
189
- // is recoverable — user can run `skalpel login` later.
190
- try {
191
- const sr = signIn.run({ dryRun: opts.dryRun, prior });
192
- if (sr && sr.error) {
193
- allWarnings.push(`sign-in: ${sr.error}`);
194
- }
195
- } catch (err) {
196
- log.warn(`sign-in: ${err.message}; continuing`);
197
- allWarnings.push(`sign-in: ${err.message}`);
198
- }
199
-
200
- log.step(4, total, 'service-register', `OS=${process.platform}`);
201
- // service-register is critical: a stale service unit is a real
202
- // problem at boot. Errors propagate to the catch block below.
203
- const sr = serviceRegister.run({ dryRun: opts.dryRun }) || {};
204
- if (Array.isArray(sr.warnings) && sr.warnings.length) {
205
- allWarnings.push(...sr.warnings);
206
- }
207
-
208
- log.step(5, total, 'ca-install', `trust store on ${process.platform}`);
209
- // ca-install is NON-CRITICAL: declined prompts or missing sudo must NOT
210
- // fail postinstall. The deferred-install sentinel ensures first
211
- // `skalpel login` / `codex` invocation retries the trust install.
212
- try {
213
- const caRes = await runCAInstallStep(opts);
214
- if (caRes && !caRes.ok && caRes.reason) {
215
- allWarnings.push(`ca-install: ${caRes.reason}`);
216
- }
217
- } catch (err) {
218
- log.warn(`ca-install: ${err.message}; continuing`);
219
- allWarnings.push(`ca-install: ${err.message}`);
220
- }
221
-
222
- // Step 5 — shell activation is OPT-IN (incident 2026-07-05). A global
223
- // `npm install -g skalpel` must NOT modify the user's shell rc files. That
224
- // was the blast radius of the incident: the managed block exported
225
- // ANTHROPIC_BASE_URL into every shell and broke Claude Code, and even a
226
- // poison-free wrapper is too invasive to write into a user's shell without
227
- // consent for an agent harness. The daemon registered in step 3 sits IDLE
228
- // and intercepts nothing until the user explicitly opts in with
229
- // `skalpel setup`, which wraps the detected agents (setupCore -> the same
230
- // managed block, now poison-free). Set SKALPEL_ACTIVATE_SHELL=1 to restore
231
- // the old auto-wrap-on-install behavior.
232
- if (process.env.SKALPEL_ACTIVATE_SHELL === '1') {
233
- log.step(6, total, 'env-inject', 'managed-block edit per shell (SKALPEL_ACTIVATE_SHELL=1)');
234
- envInject.run({ dryRun: opts.dryRun });
235
- } else {
236
- log.step(6, total, 'shell-activation', 'deferred — opt-in via `skalpel setup`');
237
- log.info('Shell integration is opt-in. Run `skalpel setup` to route Claude/Codex through skalpel.');
238
- }
239
-
240
- log.step(7, total, 'launch', 'next-step hint');
241
- launch.run({ dryRun: opts.dryRun });
242
- } catch (err) {
243
- critical = err;
244
- log.error(`postinstall failed: ${err.message}`);
245
- if (opts.verbose) {
246
- process.stderr.write(`${err.stack}\n`);
247
- }
248
- }
249
-
250
- if (allWarnings.length) {
251
- log.warn(`postinstall wizard finished (${mode}) with ${allWarnings.length} warning(s):`);
252
- for (const w of allWarnings) {
253
- log.warn(` - ${w}`);
254
- }
255
- } else if (!critical) {
256
- log.info(`postinstall wizard finished (${mode})`);
257
- if (!opts.dryRun) {
258
- printStyledSuccess();
259
- }
260
- }
261
- // Always exit 0 so npm install does not abort. The user can
262
- // re-run `skalpel install` if a critical step failed.
263
- return 0;
264
- }
265
-
266
- if (require.main === module) {
267
- Promise.resolve(main(process.argv)).then(
268
- (code) => process.exit(code || 0),
269
- (err) => {
270
- log.error(`postinstall crashed: ${err && err.stack ? err.stack : err}`);
271
- // Wizard contract: exit 0 even on crash so npm install never aborts.
272
- process.exit(0);
273
- }
274
- );
275
- }
276
-
277
- module.exports = { main, parseArgs };
@@ -1,147 +0,0 @@
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
- });
@@ -1,46 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
3
- "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4
- <plist version="1.0">
5
- <dict>
6
- <key>Label</key> <string>ai.skalpel.daemon</string>
7
-
8
- <key>ProgramArguments</key>
9
- <array>
10
- <string>{{BIN}}/skalpeld</string>
11
- <string>--service-mode</string>
12
- </array>
13
-
14
- <key>RunAtLoad</key> <true/>
15
- <!-- Unconditional KeepAlive: launchd restarts the daemon after any
16
- exit (clean or otherwise), respecting ThrottleInterval. The
17
- previous conditional form (SuccessfulExit=false + Crashed=true)
18
- did NOT restart after a graceful SIGTERM drain that exits with
19
- status 0 — the daemon stayed dead until some unrelated XPC nudge
20
- eventually woke launchd up (observed gaps of 1-25 min on the
21
- test machine), breaking Claude Code sessions routed through the
22
- proxy for that whole window. Uninstall continues to work via
23
- `launchctl unload`, which bypasses KeepAlive entirely. -->
24
- <key>KeepAlive</key> <true/>
25
-
26
- <key>ThrottleInterval</key> <integer>10</integer>
27
-
28
- <key>StandardOutPath</key> <string>{{CONFIG_DIR}}/logs/skalpeld.log</string>
29
- <key>StandardErrorPath</key> <string>{{CONFIG_DIR}}/logs/skalpeld.log</string>
30
-
31
- <key>WorkingDirectory</key> <string>{{HOME}}</string>
32
-
33
- <key>EnvironmentVariables</key>
34
- <dict>
35
- <key>SKALPEL_CONFIG_DIR</key> <string>{{CONFIG_DIR}}</string>
36
- <key>HOME</key> <string>{{HOME}}</string>
37
- <key>USER</key> <string>{{USER}}</string>
38
- </dict>
39
-
40
- <key>ProcessType</key> <string>Interactive</string>
41
- <!-- audit-2026-05-09 §6 LOW: dropped LimitLoadToSessionType=Aqua so
42
- the daemon also loads under headless macOS sessions (CI runners
43
- driving via SSH land in LoginWindow / Background sessions; the
44
- Aqua restriction blocked load there). -->
45
- </dict>
46
- </plist>