skalpel 3.4.15 → 3.4.16

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 CHANGED
@@ -42,8 +42,9 @@ Post-W4 (OAuth port): first run is a guided TUI walkthrough plus browser-loopbac
42
42
  1. The user installs (`npm install -g skalpel`) and runs `skalpel`.
43
43
  2. If `auth.json` is missing, `skalpel` renders a branded walkthrough in-terminal: daemon status, sign-in requirement, CA trust behavior, wrapper scope (new sessions only), and quick toggles (`skalpel off` / `skalpel on`).
44
44
  3. On Enter, `skalpel` 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.
45
- 4. 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).
46
- 5. The TUI auto-launches into the Dashboard. Once the daemon is reachable, the app surfaces live status and tabs (Dashboard / Engines / Account) with the first-run intro banner.
45
+ 4. On a verified envelope, the CLI 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).
46
+ 5. Login idempotently enables the `behavior_graph` actuator and installs or refreshes the managed Claude Code and Codex prompt/session hooks. This does not depend on `skalpel setup` or on a live daemon.
47
+ 6. Browser/paste login auto-launches the TUI and starts or resumes exactly one intent-graph worker, with progress on the Dashboard. Activation login stays TUI-free and launches the same worker in the background.
47
48
 
48
49
  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
50
 
@@ -61,18 +62,18 @@ The `&& npm install -g` step puts `skalpel` on your PATH so shell wrappers from
61
62
 
62
63
  The shell wrappers — and the `skalpel active` / `skalpel inactive` indicator each new `claude` / `codex` session prints — **require a persistent global install** (`npm install -g skalpel`). The wrapper block is gated on `command -v skalpel`, so a block written while only an ephemeral `npx` binary is on PATH never activates. If you run only the `npx … login --activation` step (without the global install), harness setup detects that no persistent `skalpel` is on PATH and **tells you to run `npm install -g skalpel@latest`** instead of silently writing an inert block. Run the global install, then `skalpel setup` (or restart your shell), and the indicator appears.
63
64
 
64
- `login --activation` is **auth-only with respect to the TUI**: it claims the token, writes `auth.json`, and never launches the TUI (the magic flow is a deliberate terminal handoff). Before exiting it chains directly into harness setup so the single pasted command also walks the user through wrapping their coding agents: in an interactive terminal it runs the same detection + checkbox prompt as `skalpel setup`; in a non-interactive shell (CI, piped) it only prints what it detected and points at `skalpel setup`, never editing shell startup files unattended. Browser-loopback `skalpel login` is unchanged and still lands the user on the dashboard.
65
+ `login --activation` is **auth-only with respect to the TUI**: it claims the token, writes `auth.json`, heals behavior hooks, starts or resumes graph learning in the background, and never launches the TUI (the magic flow is a deliberate terminal handoff). External-shell wrapping remains a separate consent boundary: in an interactive terminal activation offers the same detection + checkbox prompt as `skalpel setup`; in a non-interactive shell (CI, piped) it only prints what it detected and points at `skalpel setup`, never editing shell startup files unattended. Browser-loopback `skalpel login` still lands the user on the dashboard and shows graph progress there.
65
66
 
66
67
  A user who independently runs `npm install -g skalpel` is still supported: `postinstall` prints a best-effort pointer to `https://skalpel.ai/signup`, and the first `skalpel` run repeats that pointer where output is guaranteed.
67
68
 
68
69
  When Claude Code is already present, the persistent install also registers Skalpel's local,
69
70
  read-only `statusLine` command. It is visible immediately as `~0.0h saved` and grows only from
70
- confirmed savings. This does **not** opt the user into prompt/session hooks: those remain disabled
71
- until the explicit `skalpel setup` flow below. An existing custom Claude status line is preserved.
71
+ confirmed savings. Install alone does **not** opt the user into prompt/session hooks; the first
72
+ successful `skalpel login` activates them. An existing custom Claude status line is preserved.
72
73
 
73
- ### `skalpel setup` — detect and wrap coding agents
74
+ ### `skalpel setup` — repair wrappers and behavior hooks
74
75
 
75
- Harness setup runs automatically at the tail of `login --activation` (see above), and is also available as a standalone command for re-running later or for `npm install -g` users. `skalpel setup` detects which coding-agent harnesses are installed (cross-platform on Linux, Windows, and macOS) via PATH and per-user config probes:
76
+ Normal authentication, hook activation, and graph learning require no setup command. `skalpel setup` remains as an explicit recovery/advanced command for re-running optional external-shell wrapping and repairing managed hooks. Harness wrapping is also offered interactively at the tail of `login --activation` (see above). `skalpel setup` detects which coding-agent harnesses are installed (cross-platform on Linux, Windows, and macOS) via PATH and per-user config probes:
76
77
 
77
78
  - **Claude Code** and **Codex CLI** — detected and wrappable.
78
79
  - **Cursor** — detected for display only (no Skalpel wrapper today).
package/README.md CHANGED
@@ -10,7 +10,7 @@ After signing up at [skalpel.ai](https://skalpel.ai):
10
10
  npx skalpel login
11
11
  ```
12
12
 
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:
13
+ `npx skalpel login` materializes both binaries and runs a browser-loopback sign-in against Cognito. A successful login writes the Cognito JWT bundle, idempotently activates the managed Claude Code and Codex behavior hooks, and starts or resumes intent-graph learning; browser login then opens the TUI so progress is visible. To install persistently:
14
14
 
15
15
  ```
16
16
  npm install -g skalpel
@@ -37,7 +37,8 @@ For details on what gets installed where, per-OS service registration, updates,
37
37
  `skalpel` accepts a small set of subcommands:
38
38
 
39
39
  - `skalpel` — launch the Bubble Tea TUI (default).
40
- - `skalpel login` — browser-loopback sign-in; writes `auth.json`.
40
+ - `skalpel login` — sign in, heal behavior hooks, and start/resume intent-graph learning.
41
+ - `skalpel setup` — repair optional external-shell wrappers and behavior hooks; not required for normal login/TUI onboarding.
41
42
  - `skalpel logout` — revoke the active session (best-effort) and delete `auth.json`; supports `--yes` to skip the `[y/N]` confirm.
42
43
  - `skalpel uninstall` — clean up skalpel state on this machine.
43
44
  - `skalpel --version` / `skalpel --help` — utilities.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skalpel",
3
- "version": "3.4.15",
3
+ "version": "3.4.16",
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",
@@ -59,10 +59,10 @@
59
59
  "x64"
60
60
  ],
61
61
  "optionalDependencies": {
62
- "@skalpelai/skalpel-darwin-arm64": "3.4.15",
63
- "@skalpelai/skalpel-darwin-x64": "3.4.15",
64
- "@skalpelai/skalpel-linux-arm64": "3.4.15",
65
- "@skalpelai/skalpel-linux-x64": "3.4.15",
66
- "@skalpelai/skalpel-win32-x64": "3.4.15"
62
+ "@skalpelai/skalpel-darwin-arm64": "3.4.16",
63
+ "@skalpelai/skalpel-darwin-x64": "3.4.16",
64
+ "@skalpelai/skalpel-linux-arm64": "3.4.16",
65
+ "@skalpelai/skalpel-linux-x64": "3.4.16",
66
+ "@skalpelai/skalpel-win32-x64": "3.4.16"
67
67
  }
68
68
  }
@@ -174,10 +174,10 @@ async function main(argv) {
174
174
  } else if (hooks.statuslineManaged) {
175
175
  log.info(
176
176
  'behavior-hooks: runtime staged; Claude status line enabled at 0.0h; ' +
177
- 'prompt hooks remain opt-in via `skalpel setup`'
177
+ 'prompt hooks activate automatically after `skalpel login`'
178
178
  );
179
179
  } else {
180
- log.info('behavior-hooks: runtime staged; activation remains opt-in via `skalpel setup`');
180
+ log.info('behavior-hooks: runtime staged; hooks activate automatically after `skalpel login`');
181
181
  }
182
182
  } catch (err) {
183
183
  log.warn(`behavior-hooks: ${err.message}; continuing`);
@@ -12,6 +12,7 @@ const { spawnSync } = require('child_process');
12
12
 
13
13
  const hooksLib = path.join(__dirname, 'prosumer-hooks.js');
14
14
  const statusline = path.resolve(__dirname, '..', '..', 'prosumer-hooks', 'skalpel-statusline.mjs');
15
+ const bootstrap = path.resolve(__dirname, '..', '..', 'prosumer-hooks', 'bootstrap.mjs');
15
16
  const packageJson = path.resolve(__dirname, '..', '..', 'package.json');
16
17
 
17
18
  let pass = 0;
@@ -65,6 +66,11 @@ function settings(home) {
65
66
  return JSON.parse(fs.readFileSync(path.join(home, '.claude', 'settings.json'), 'utf8'));
66
67
  }
67
68
 
69
+ function fakeJwt(claims) {
70
+ const encode = (value) => Buffer.from(JSON.stringify(value)).toString('base64url');
71
+ return `${encode({ alg: 'none' })}.${encode(claims)}.test-signature`;
72
+ }
73
+
68
74
  function run() {
69
75
  process.stdout.write('prosumer-hooks tests:\n');
70
76
 
@@ -100,6 +106,49 @@ function run() {
100
106
  assert.ok(fs.existsSync(statusline), 'status-line source is missing from the packaged directory');
101
107
  });
102
108
 
109
+ test('graph bootstrap never owns or mutates behavior hooks', () => {
110
+ const home = tempHome({ claude: true });
111
+ try {
112
+ const configDir = path.join(home, '.config', 'skalpel');
113
+ fs.mkdirSync(configDir, { recursive: true });
114
+ fs.writeFileSync(
115
+ path.join(configDir, 'auth.json'),
116
+ JSON.stringify({
117
+ subject: { user_id: 'user-123' },
118
+ cognito: {
119
+ id_token: fakeJwt({ sub: 'user-123', exp: 4_102_444_800 }),
120
+ refresh_token: 'unused',
121
+ expires_at: '2100-01-01T00:00:00Z',
122
+ },
123
+ }),
124
+ );
125
+ const result = spawnSync(process.execPath, [bootstrap], {
126
+ env: {
127
+ ...childEnv(home),
128
+ SKALPEL_CONFIG_DIR: configDir,
129
+ SKALPEL_USER: '',
130
+ },
131
+ encoding: 'utf8',
132
+ timeout: 10_000,
133
+ });
134
+ assert.strictEqual(result.status, 0, result.stderr);
135
+ const rows = result.stdout
136
+ .trim()
137
+ .split('\n')
138
+ .filter(Boolean)
139
+ .map((line) => JSON.parse(line));
140
+ assert.ok(rows.some((row) => row.state === 'complete'), result.stdout);
141
+ assert.ok(!rows.some((row) => row.state === 'wiring'), result.stdout);
142
+ assert.strictEqual(
143
+ fs.existsSync(path.join(home, '.claude', 'settings.json')),
144
+ false,
145
+ 'graph-only worker must not install hooks; successful login owns that mutation',
146
+ );
147
+ } finally {
148
+ fs.rmSync(home, { recursive: true, force: true });
149
+ }
150
+ });
151
+
103
152
  test('fresh npm-style staging registers only the Claude status line', () => {
104
153
  const home = tempHome({ claude: true });
105
154
  try {
@@ -17,10 +17,8 @@ import {
17
17
  writeFileSync,
18
18
  } from "node:fs";
19
19
  import { homedir } from "node:os";
20
- import { basename, dirname, join } from "node:path";
20
+ import { basename, join } from "node:path";
21
21
  import { createInterface } from "node:readline";
22
- import { fileURLToPath } from "node:url";
23
- import { spawnSync } from "node:child_process";
24
22
  import { gzipSync } from "node:zlib";
25
23
  import { identity } from "./auth.mjs";
26
24
  import { recordInsight } from "./insights.mjs";
@@ -28,7 +26,6 @@ import { recordInsight } from "./insights.mjs";
28
26
  const DIR = join(homedir(), ".skalpel");
29
27
  const STATE_PATH = join(DIR, "bootstrap.json");
30
28
  const LOCK_PATH = join(DIR, "bootstrap.lock");
31
- const HERE = dirname(fileURLToPath(import.meta.url));
32
29
  const FORCE = process.argv.includes("--force");
33
30
  const LOOKBACK_DAYS = Math.max(1, Number.parseInt(process.env.SKALPEL_LOOKBACK_DAYS || "30", 10));
34
31
  const MIN_USER_TURNS = Math.max(
@@ -367,10 +364,6 @@ async function main() {
367
364
  return;
368
365
  }
369
366
 
370
- emit("wiring", "Wiring hooks into Claude Code + Codex");
371
- const wired = spawnSync(process.execPath, [join(HERE, "install.mjs")], { stdio: "ignore" });
372
- if (wired.error || wired.status !== 0) throw new Error("could not activate coding-agent hooks");
373
-
374
367
  if (prior.user_id === auth.uid && prior.job_id && prior.state !== "done") {
375
368
  emit("resuming", "Resuming the existing graph build");
376
369
  const sessions = await pollJob(prior.job_id, auth.uid, auth.token);
@@ -9,7 +9,8 @@ import { fileURLToPath } from "node:url";
9
9
 
10
10
  const uninstall = process.argv.includes("--uninstall");
11
11
  // npm postinstall uses this mode to make the local, read-only status indicator visible without
12
- // opting the user into prompt/session hooks. `skalpel setup` still runs the full installer.
12
+ // opting the user into prompt/session hooks. A successful `skalpel login` runs the full installer;
13
+ // `skalpel setup` remains an explicit repair path.
13
14
  const statuslineOnly = !uninstall && process.argv.includes("--statusline-only");
14
15
 
15
16
  // Where this installer's own files live — the globally-installed package, a source checkout, OR the
@@ -30,7 +31,7 @@ const STATUSLINE_FILE = join(HOOKS_DIR, "skalpel-statusline.mjs");
30
31
  // Copy the hook runtime (the two entrypoints + their shared auth.mjs) into ~/.skalpel/hooks so the
31
32
  // wired absolute path is always valid. Overwrites on every install so upgrades refresh the code.
32
33
  function stageHookRuntime() {
33
- // `skalpel setup` invokes the already-staged copy. Avoid copying a file onto itself.
34
+ // Login and setup repair paths invoke the already-staged copy. Avoid copying a file onto itself.
34
35
  if (PKG_DIR === HOOKS_DIR) return;
35
36
  mkdirSync(HOOKS_DIR, { recursive: true });
36
37
  // insights.mjs is staged BEFORE the entrypoints that import it — a mid-copy ENOENT
@@ -358,7 +359,7 @@ if (statuslineOnly) {
358
359
  console.log(
359
360
  claudeResult === "preserved"
360
361
  ? "custom Claude status line preserved."
361
- : "installed. Behavior hooks remain opt-in via `skalpel setup`.",
362
+ : "installed. Behavior hooks activate automatically after `skalpel login`.",
362
363
  );
363
364
  } else {
364
365
  console.log(