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
|
|
46
|
-
5.
|
|
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).
|
|
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.
|
|
71
|
-
|
|
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` —
|
|
74
|
+
### `skalpel setup` — repair wrappers and behavior hooks
|
|
74
75
|
|
|
75
|
-
|
|
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
|
|
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` —
|
|
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.
|
|
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.
|
|
63
|
-
"@skalpelai/skalpel-darwin-x64": "3.4.
|
|
64
|
-
"@skalpelai/skalpel-linux-arm64": "3.4.
|
|
65
|
-
"@skalpelai/skalpel-linux-x64": "3.4.
|
|
66
|
-
"@skalpelai/skalpel-win32-x64": "3.4.
|
|
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
|
}
|
package/postinstall/index.js
CHANGED
|
@@ -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
|
|
177
|
+
'prompt hooks activate automatically after `skalpel login`'
|
|
178
178
|
);
|
|
179
179
|
} else {
|
|
180
|
-
log.info('behavior-hooks: runtime staged;
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
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
|
|
362
|
+
: "installed. Behavior hooks activate automatically after `skalpel login`.",
|
|
362
363
|
);
|
|
363
364
|
} else {
|
|
364
365
|
console.log(
|