skalpel 2.0.24 → 3.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 (43) hide show
  1. package/INSTALL.md +103 -0
  2. package/LICENSE +201 -21
  3. package/README.md +12 -174
  4. package/design-tokens.json +51 -0
  5. package/npm-bin/colors.js +125 -0
  6. package/npm-bin/skalpel.js +200 -0
  7. package/npm-bin/skalpeld.js +20 -0
  8. package/package.json +50 -68
  9. package/postinstall/index.js +294 -0
  10. package/postinstall/launchd/com.skalpel.skalpeld.plist.tmpl +41 -0
  11. package/postinstall/lib/detect-prior.js +51 -0
  12. package/postinstall/lib/env-inject.js +121 -0
  13. package/postinstall/lib/launch.js +28 -0
  14. package/postinstall/lib/log.js +31 -0
  15. package/postinstall/lib/paths.js +186 -0
  16. package/postinstall/lib/rc-edit.js +167 -0
  17. package/postinstall/lib/rc-edit.test.js +196 -0
  18. package/postinstall/lib/service-register.js +293 -0
  19. package/postinstall/lib/sign-in.js +98 -0
  20. package/postinstall/lib/template.js +36 -0
  21. package/postinstall/snippets/bash.sh.tmpl +12 -0
  22. package/postinstall/snippets/fish.fish.tmpl +11 -0
  23. package/postinstall/snippets/powershell.ps1.tmpl +12 -0
  24. package/postinstall/snippets/zsh.sh.tmpl +13 -0
  25. package/postinstall/systemd/skalpeld.service.tmpl +33 -0
  26. package/postinstall/windows/Task.xml.tmpl +42 -0
  27. package/postinstall/windows/register-task.ps1.tmpl +45 -0
  28. package/dist/cli/index.js +0 -2888
  29. package/dist/cli/index.js.map +0 -1
  30. package/dist/cli/proxy-runner.js +0 -1598
  31. package/dist/cli/proxy-runner.js.map +0 -1
  32. package/dist/index.cjs +0 -2282
  33. package/dist/index.cjs.map +0 -1
  34. package/dist/index.d.cts +0 -165
  35. package/dist/index.d.ts +0 -165
  36. package/dist/index.js +0 -2236
  37. package/dist/index.js.map +0 -1
  38. package/dist/proxy/index.cjs +0 -1731
  39. package/dist/proxy/index.cjs.map +0 -1
  40. package/dist/proxy/index.d.cts +0 -39
  41. package/dist/proxy/index.d.ts +0 -39
  42. package/dist/proxy/index.js +0 -1697
  43. package/dist/proxy/index.js.map +0 -1
@@ -0,0 +1,293 @@
1
+ // Step 3: per-OS service registration.
2
+ //
3
+ // SPEC.md §9.2 (launchd), §9.3 (systemd), §9.4 (Task Scheduler).
4
+ //
5
+ // On every supported OS we:
6
+ // 1. Render the template with paths.templateValues().
7
+ // 2. Write the rendered file to its OS-specific destination.
8
+ // 3. Invoke the OS tool (launchctl / systemctl / schtasks) to load
9
+ // the unit.
10
+ //
11
+ // Idempotence: writing replaces any prior file at the same path.
12
+ // launchctl bootout / systemctl daemon-reload / schtasks /Delete are
13
+ // run before the new file is loaded so a stale unit is replaced.
14
+ //
15
+ // Dry-run: no filesystem writes, no exec — only log lines describing
16
+ // what would happen.
17
+
18
+ 'use strict';
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const crypto = require('crypto');
23
+ const { spawnSync } = require('child_process');
24
+
25
+ const log = require('./log');
26
+ const paths = require('./paths');
27
+ const template = require('./template');
28
+ const rcEdit = require('./rc-edit');
29
+
30
+ // B14: launchctl absolute path. SSH'd users may have a stripped PATH
31
+ // that does not include /bin; fail fast with a real path so the
32
+ // spawn error is "binary not found at known location" rather than
33
+ // "PATH lookup failed".
34
+ const LAUNCHCTL_PATH = '/bin/launchctl';
35
+
36
+ // B10: launchd target (gui/<uid> vs system/) depends on whether a
37
+ // graphical session is attached. Headless macOS (SSH login, CI box)
38
+ // must use system/ because gui/<uid> requires Aqua.
39
+ function launchdTarget() {
40
+ const headless = !!process.env.SSH_CLIENT || !process.env.DISPLAY;
41
+ if (headless) {
42
+ return 'system';
43
+ }
44
+ return `gui/${process.getuid()}`;
45
+ }
46
+
47
+ function templatePath() {
48
+ switch (process.platform) {
49
+ case 'darwin':
50
+ return path.join(template.templateDir('launchd'), 'com.skalpel.skalpeld.plist.tmpl');
51
+ case 'win32':
52
+ return path.join(template.templateDir('windows'), 'Task.xml.tmpl');
53
+ default:
54
+ return path.join(template.templateDir('systemd'), 'skalpeld.service.tmpl');
55
+ }
56
+ }
57
+
58
+ // renderedPowerShellPath is where service-register stages the rendered
59
+ // register-task.ps1 (B8). The .tmpl on disk is never executed directly.
60
+ function renderedPowerShellPath() {
61
+ return path.join(paths.configDir(), 'register-task.ps1');
62
+ }
63
+
64
+ function xmlEscapedValues(values) {
65
+ const out = {};
66
+ for (const [k, v] of Object.entries(values)) {
67
+ out[k] = rcEdit.xmlEscape(v);
68
+ }
69
+ return out;
70
+ }
71
+
72
+ function loadCommands() {
73
+ const dest = paths.servicePath();
74
+ switch (process.platform) {
75
+ case 'darwin': {
76
+ const target = launchdTarget(); // B10
77
+ const fullTarget =
78
+ target === 'system' ? 'system/ai.skalpel.daemon' : `${target}/ai.skalpel.daemon`;
79
+ const bootstrap = target === 'system' ? 'system' : target;
80
+ return [
81
+ [LAUNCHCTL_PATH, ['bootout', fullTarget], { allowFail: true }],
82
+ [LAUNCHCTL_PATH, ['bootstrap', bootstrap, dest]],
83
+ [LAUNCHCTL_PATH, ['enable', fullTarget]],
84
+ [LAUNCHCTL_PATH, ['kickstart', '-k', fullTarget]],
85
+ ];
86
+ }
87
+ case 'win32': {
88
+ const ps1 = renderedPowerShellPath();
89
+ return [
90
+ ['powershell.exe', ['-NoProfile', '-File', ps1, '-Action', 'register', '-XmlPath', dest]],
91
+ ];
92
+ }
93
+ default:
94
+ return [
95
+ ['systemctl', ['--user', 'daemon-reload']],
96
+ ['systemctl', ['--user', 'enable', 'skalpel-daemon.service']],
97
+ ['systemctl', ['--user', 'start', 'skalpel-daemon.service']],
98
+ ];
99
+ }
100
+ }
101
+
102
+ function unloadCommands() {
103
+ switch (process.platform) {
104
+ case 'darwin': {
105
+ const target = launchdTarget();
106
+ const fullTarget =
107
+ target === 'system' ? 'system/ai.skalpel.daemon' : `${target}/ai.skalpel.daemon`;
108
+ return [
109
+ [LAUNCHCTL_PATH, ['bootout', fullTarget], { allowFail: true }],
110
+ ];
111
+ }
112
+ case 'win32': {
113
+ const ps1 = renderedPowerShellPath();
114
+ return [
115
+ ['powershell.exe', ['-NoProfile', '-File', ps1, '-Action', 'unregister'], { allowFail: true }],
116
+ ];
117
+ }
118
+ default:
119
+ return [
120
+ ['systemctl', ['--user', 'disable', '--now', 'skalpel-daemon.service'], { allowFail: true }],
121
+ ];
122
+ }
123
+ }
124
+
125
+ // B42: validate the daemon binary exists at the path the service
126
+ // unit will exec. SHA256 verification is deferred (TODO below) until
127
+ // dist.shasum is populated in package.json. The existence check is
128
+ // not deferred — running a service unit pointing at a missing binary
129
+ // guarantees a startup failure.
130
+ function verifyBinary() {
131
+ const bin = paths.binPath('skalpeld');
132
+ let stat;
133
+ try {
134
+ stat = fs.statSync(bin);
135
+ } catch (err) {
136
+ throw new Error(`service-register: skalpeld binary missing at ${bin}: ${err.message}`);
137
+ }
138
+ if (!stat.isFile()) {
139
+ throw new Error(`service-register: ${bin} is not a regular file`);
140
+ }
141
+ // TODO(B42): wire shasum verify when package.json/dist.shasum is populated.
142
+ // Compute SHA-256 over the binary and compare to the published shasum
143
+ // so an attacker cannot swap the binary post-install.
144
+ let pkg;
145
+ try {
146
+ pkg = require(path.join(__dirname, '..', '..', 'package.json'));
147
+ } catch (_) {
148
+ pkg = {};
149
+ }
150
+ const expected = pkg && pkg.dist && pkg.dist.shasum;
151
+ if (expected) {
152
+ const h = crypto.createHash('sha256');
153
+ h.update(fs.readFileSync(bin));
154
+ const got = h.digest('hex');
155
+ if (got !== expected) {
156
+ throw new Error(
157
+ `service-register: skalpeld shasum mismatch (got ${got}, want ${expected})`
158
+ );
159
+ }
160
+ }
161
+ return { bin, stat };
162
+ }
163
+
164
+ // B50: two-phase unregister with rollback markers. Phase 1 collects
165
+ // what would be undone; phase 2 executes each step and on failure
166
+ // runs the prior-step undo so the system does not end up in a
167
+ // half-detached state.
168
+ function unregister({ dryRun }) {
169
+ const dest = paths.servicePath();
170
+ if (dryRun) {
171
+ log.dryRun(`uninstall: would unregister service at ${dest}`);
172
+ for (const [bin, args] of unloadCommands()) {
173
+ log.dryRun(` would run: ${bin} ${args.join(' ')}`);
174
+ }
175
+ log.dryRun(` would rm ${dest}`);
176
+ return { dryRun: true };
177
+ }
178
+
179
+ // Phase 1: plan the steps.
180
+ const steps = unloadCommands().map(([bin, args, opts]) => ({
181
+ bin,
182
+ args,
183
+ allowFail: !!(opts && opts.allowFail),
184
+ done: false,
185
+ }));
186
+ const errors = [];
187
+
188
+ // Phase 2: execute. On hard failure, log every prior step (we do not
189
+ // attempt to re-bootstrap because that would re-install the daemon).
190
+ for (const step of steps) {
191
+ const r = spawnSync(step.bin, step.args, { stdio: 'inherit' });
192
+ if (r.status !== 0 && !step.allowFail) {
193
+ const msg = `uninstall: ${step.bin} ${step.args.join(' ')} exited ${r.status}`;
194
+ errors.push(msg);
195
+ log.warn(`${msg}; rolling forward to file removal`);
196
+ } else {
197
+ step.done = true;
198
+ }
199
+ }
200
+ log.info(
201
+ `uninstall: phase 2 complete: ${steps.filter((s) => s.done).length}/${steps.length} ok` +
202
+ (errors.length ? ` (${errors.length} errors)` : '')
203
+ );
204
+
205
+ if (fs.existsSync(dest)) {
206
+ fs.unlinkSync(dest);
207
+ log.info(`uninstall: removed ${dest}`);
208
+ }
209
+ // Also remove the rendered helper PS1 on Windows.
210
+ const ps1 = renderedPowerShellPath();
211
+ if (process.platform === 'win32' && fs.existsSync(ps1)) {
212
+ fs.unlinkSync(ps1);
213
+ log.info(`uninstall: removed ${ps1}`);
214
+ }
215
+ return { removed: true, errors };
216
+ }
217
+
218
+ function run({ dryRun }) {
219
+ const tmpl = templatePath();
220
+ const dest = paths.servicePath();
221
+ const values = paths.templateValues();
222
+ const warnings = [];
223
+
224
+ if (dryRun) {
225
+ log.dryRun(`step 3 service-register: would render ${tmpl} → ${dest}`);
226
+ for (const [bin, args] of loadCommands()) {
227
+ log.dryRun(` would run: ${bin} ${args.join(' ')}`);
228
+ }
229
+ return { skipped: false, dryRun: true, warnings };
230
+ }
231
+
232
+ if (!fs.existsSync(tmpl)) {
233
+ log.warn(
234
+ `service-register: template ${tmpl} missing — skipping service ` +
235
+ 'registration. Daemon will still launch on first TUI invocation.'
236
+ );
237
+ return { skipped: true, reason: 'no-template', warnings };
238
+ }
239
+
240
+ // B42: verify the daemon binary is present. Log + record a
241
+ // warning when missing but continue writing the service file so
242
+ // the wizard remains best-effort under npm postinstall. The
243
+ // service unit itself will fail to start at boot, which is the
244
+ // signal an operator notices — better than silently writing a
245
+ // unit that may have a stale path baked in.
246
+ try {
247
+ verifyBinary();
248
+ } catch (err) {
249
+ log.warn(`service-register: ${err.message}; continuing — service unit will surface this at boot`);
250
+ warnings.push(err.message);
251
+ }
252
+
253
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
254
+ // B49: explicit chmod after recursive mkdir — Node's mkdirSync
255
+ // honours mode only for the *leaf* directory, not the intermediate
256
+ // ones. The configDir + logsDir need a final chmod to land at
257
+ // 0o700 reliably across platforms.
258
+ fs.mkdirSync(paths.configDir(), { recursive: true, mode: 0o700 });
259
+ fs.chmodSync(paths.configDir(), 0o700);
260
+ fs.mkdirSync(paths.logsDir(), { recursive: true, mode: 0o700 });
261
+ fs.chmodSync(paths.logsDir(), 0o700);
262
+
263
+ const renderValues = process.platform === 'win32' ? xmlEscapedValues(values) : values;
264
+ const rendered = template.render(tmpl, renderValues);
265
+ fs.writeFileSync(dest, rendered, { mode: 0o644 });
266
+ log.info(`service-register: wrote ${dest}`);
267
+
268
+ if (process.platform === 'win32') {
269
+ const ps1Tmpl = path.join(template.templateDir('windows'), 'register-task.ps1.tmpl');
270
+ const ps1Path = renderedPowerShellPath();
271
+ const ps1Body = template.render(ps1Tmpl, values);
272
+ fs.writeFileSync(ps1Path, ps1Body, { mode: 0o755 });
273
+ log.info(`service-register: wrote ${ps1Path}`);
274
+ }
275
+
276
+ // B13: collect warnings on non-fatal failures and return them so
277
+ // the caller (postinstall/index.js) can decide whether to treat
278
+ // the wizard run as ok-with-warnings or fail.
279
+ for (const [bin, args, opts] of loadCommands()) {
280
+ const r = spawnSync(bin, args, { stdio: 'inherit' });
281
+ if (r.status !== 0 && !(opts && opts.allowFail)) {
282
+ const msg =
283
+ `service-register: ${bin} ${args.join(' ')} exited ${r.status}; ` +
284
+ 'continuing without daemon-on-boot';
285
+ log.warn(msg);
286
+ warnings.push(msg);
287
+ return { skipped: false, registered: false, warnings };
288
+ }
289
+ }
290
+ return { skipped: false, registered: true, warnings };
291
+ }
292
+
293
+ module.exports = { run, unregister, templatePath, loadCommands, unloadCommands, launchdTarget };
@@ -0,0 +1,98 @@
1
+ // Step 2: sign in.
2
+ //
3
+ // SPEC.md §9.6 step 3 — run `skalpel login` in browser-loopback mode
4
+ // inline. Manual-paste fallback if loopback bind fails.
5
+ //
6
+ // During npm postinstall the user's shell has not yet been re-sourced
7
+ // so the `skalpel` command may not be on PATH. We resolve the
8
+ // dispatch shim via require.resolve() (B7) so a renamed top-level
9
+ // directory does not break the lookup, then invoke it via `node`.
10
+ //
11
+ // In dry-run mode we never spawn anything; we only log the action.
12
+
13
+ 'use strict';
14
+
15
+ const path = require('path');
16
+ const fs = require('fs');
17
+ const { spawnSync } = require('child_process');
18
+
19
+ const log = require('./log');
20
+ const paths = require('./paths');
21
+
22
+ // SIGN_IN_TIMEOUT_MS caps how long we wait for the spawned `skalpel
23
+ // login` subprocess. SPEC.md §4.5 sets the loopback timeout at 5 min;
24
+ // we add a small slack so the user has time to complete the browser
25
+ // flow.
26
+ const SIGN_IN_TIMEOUT_MS = 5 * 60 * 1000 + 30 * 1000;
27
+
28
+ // resolveDispatcher returns the path to the dispatch shim. B7:
29
+ // require.resolve('../../npm-bin/skalpel.js') is anchored to the
30
+ // installed package layout — works whether the package is installed
31
+ // globally or linked locally.
32
+ function resolveDispatcher() {
33
+ // Try require.resolve first (handles symlinked / deduped layouts).
34
+ try {
35
+ return require.resolve('../../npm-bin/skalpel.js');
36
+ } catch (_) {
37
+ // Fall back to a path-join lookup so dev environments without a
38
+ // resolvable layout still work.
39
+ const guess = path.join(__dirname, '..', '..', 'npm-bin', 'skalpel.js');
40
+ if (fs.existsSync(guess)) {
41
+ return guess;
42
+ }
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function run({ dryRun, prior }) {
48
+ if (prior) {
49
+ log.info('sign-in: prior auth.json detected — skipping browser flow');
50
+ return { skipped: true };
51
+ }
52
+
53
+ const dispatcher = resolveDispatcher();
54
+
55
+ if (dryRun) {
56
+ log.dryRun(
57
+ `step 2 sign-in: would exec \`node ${dispatcher || '<dispatcher-missing>'} login\` ` +
58
+ '(loopback browser flow with manual-paste fallback)'
59
+ );
60
+ return { skipped: false, dryRun: true };
61
+ }
62
+
63
+ if (!dispatcher) {
64
+ log.warn(
65
+ 'sign-in: dispatch shim not resolvable; user must run `skalpel login` ' +
66
+ 'manually after install'
67
+ );
68
+ return { skipped: true, reason: 'no-dispatcher' };
69
+ }
70
+
71
+ // B6: actually spawn `skalpel login`. The subprocess opens the
72
+ // browser, waits for the loopback callback, and writes auth.json.
73
+ // We propagate stdio so the user can see the prompt; we cap at
74
+ // SIGN_IN_TIMEOUT_MS so a runaway hang does not block npm install.
75
+ log.info(
76
+ 'sign-in: launching `skalpel login` (loopback browser flow); ' +
77
+ `timeout ${Math.round(SIGN_IN_TIMEOUT_MS / 1000)}s`
78
+ );
79
+ const r = spawnSync(process.execPath, [dispatcher, 'login'], {
80
+ stdio: 'inherit',
81
+ timeout: SIGN_IN_TIMEOUT_MS,
82
+ });
83
+ if (r.error) {
84
+ log.warn(`sign-in: spawn error: ${r.error.message}; user can re-run \`skalpel login\``);
85
+ return { skipped: false, error: r.error.message };
86
+ }
87
+ if (r.status !== 0) {
88
+ log.warn(
89
+ `sign-in: \`skalpel login\` exited ${r.status}; user can re-run after install ` +
90
+ `(config dir: ${paths.configDir()})`
91
+ );
92
+ return { skipped: false, exitCode: r.status };
93
+ }
94
+ log.info(`sign-in: ok (config dir: ${paths.configDir()})`);
95
+ return { skipped: false, ok: true };
96
+ }
97
+
98
+ module.exports = { run, resolveDispatcher };
@@ -0,0 +1,36 @@
1
+ // Minimal placeholder substitution for postinstall templates.
2
+ //
3
+ // Templates live alongside this file (postinstall/launchd/...,
4
+ // postinstall/systemd/..., postinstall/windows/...). Each template
5
+ // uses {{NAME}} placeholders; we replace them with the values from
6
+ // paths.templateValues().
7
+ //
8
+ // We deliberately do not pull in handlebars/mustache: stdlib only
9
+ // keeps the npm install footprint at zero, which matters for a
10
+ // postinstall hook.
11
+
12
+ 'use strict';
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ function render(templatePath, values) {
18
+ const raw = fs.readFileSync(templatePath, 'utf8');
19
+ return substitute(raw, values);
20
+ }
21
+
22
+ function substitute(body, values) {
23
+ return body.replace(/\{\{([A-Z_][A-Z0-9_]*)\}\}/g, (m, key) => {
24
+ if (Object.prototype.hasOwnProperty.call(values, key)) {
25
+ return String(values[key]);
26
+ }
27
+ // Leave unknown placeholders untouched so the operator sees them.
28
+ return m;
29
+ });
30
+ }
31
+
32
+ function templateDir(subdir) {
33
+ return path.join(__dirname, '..', subdir);
34
+ }
35
+
36
+ module.exports = { render, substitute, templateDir };
@@ -0,0 +1,12 @@
1
+ # Reference snippet — bash form of the managed block. Identical body
2
+ # to zsh.sh.tmpl per SPEC.md §9.5; the only difference is the rc-file
3
+ # path the editor writes to (~/.bashrc or ~/.bash_profile).
4
+ # >>> @skalpelai/skalpel begin (managed)
5
+ # This block is managed by skalpel install. Do not edit by hand;
6
+ # re-run `skalpel install` or `npx skalpel` to update.
7
+ export ANTHROPIC_API_URL="http://127.0.0.1:7878"
8
+ export ANTHROPIC_BASE_URL="http://127.0.0.1:7878"
9
+ export OPENAI_BASE_URL="http://127.0.0.1:7878/v1"
10
+ export OPENAI_API_BASE="http://127.0.0.1:7878/v1"
11
+ export SKALPEL_PROXY_URL="http://127.0.0.1:7878"
12
+ # <<< @skalpelai/skalpel end (managed)
@@ -0,0 +1,11 @@
1
+ # Reference snippet — fish form of the managed block. SPEC.md §9.5
2
+ # also allows the conf.d variant at ~/.config/fish/conf.d/skalpel.fish;
3
+ # the editor uses the rc-file form to keep the four shells uniform.
4
+ # >>> @skalpelai/skalpel begin (managed)
5
+ # Managed by skalpel install; safe to delete.
6
+ set -gx ANTHROPIC_API_URL "http://127.0.0.1:7878"
7
+ set -gx ANTHROPIC_BASE_URL "http://127.0.0.1:7878"
8
+ set -gx OPENAI_BASE_URL "http://127.0.0.1:7878/v1"
9
+ set -gx OPENAI_API_BASE "http://127.0.0.1:7878/v1"
10
+ set -gx SKALPEL_PROXY_URL "http://127.0.0.1:7878"
11
+ # <<< @skalpelai/skalpel end (managed)
@@ -0,0 +1,12 @@
1
+ # Reference snippet — PowerShell form of the managed block. The fence
2
+ # uses the trailing >>> / <<< sigils per SPEC.md §9.5 so the marker
3
+ # parses cleanly against PowerShell's $env:NAME prefix style.
4
+ # >>> @skalpelai/skalpel begin (managed) >>>
5
+ # This block is managed by skalpel install. Do not edit by hand;
6
+ # re-run `skalpel install` or `npx skalpel` to update.
7
+ $env:ANTHROPIC_API_URL = 'http://127.0.0.1:7878'
8
+ $env:ANTHROPIC_BASE_URL = 'http://127.0.0.1:7878'
9
+ $env:OPENAI_BASE_URL = 'http://127.0.0.1:7878/v1'
10
+ $env:OPENAI_API_BASE = 'http://127.0.0.1:7878/v1'
11
+ $env:SKALPEL_PROXY_URL = 'http://127.0.0.1:7878'
12
+ # <<< @skalpelai/skalpel end (managed) <<<
@@ -0,0 +1,13 @@
1
+ # Reference snippet — zsh form of the managed block emitted by
2
+ # postinstall/lib/rc-edit.js. The runtime body is computed in code
3
+ # (so the active port is filled in from <CONFIG_DIR>/skalpeld.lock);
4
+ # this template is for documentation/diff comparisons.
5
+ # >>> @skalpelai/skalpel begin (managed)
6
+ # This block is managed by skalpel install. Do not edit by hand;
7
+ # re-run `skalpel install` or `npx skalpel` to update.
8
+ export ANTHROPIC_API_URL="http://127.0.0.1:7878"
9
+ export ANTHROPIC_BASE_URL="http://127.0.0.1:7878"
10
+ export OPENAI_BASE_URL="http://127.0.0.1:7878/v1"
11
+ export OPENAI_API_BASE="http://127.0.0.1:7878/v1"
12
+ export SKALPEL_PROXY_URL="http://127.0.0.1:7878"
13
+ # <<< @skalpelai/skalpel end (managed)
@@ -0,0 +1,33 @@
1
+ # systemd user unit for the Skalpel local proxy daemon.
2
+ # SPEC.md §9.3 — installed at ~/.config/systemd/user/skalpel-daemon.service
3
+ # Postinstall placeholders: {{USER}} {{HOME}} {{BIN}} {{CONFIG_DIR}}.
4
+ [Unit]
5
+ Description=Skalpel daemon (local proxy) — user={{USER}}
6
+ Documentation=https://skalpel.ai/docs/cli
7
+ After=default.target
8
+
9
+ [Service]
10
+ Type=simple
11
+ ExecStart="{{BIN}}/skalpeld" --service-mode
12
+ Restart=on-failure
13
+ RestartSec=10s
14
+
15
+ # Process hygiene
16
+ StandardOutput=append:{{CONFIG_DIR}}/logs/skalpeld.log
17
+ StandardError=append:{{CONFIG_DIR}}/logs/skalpeld.log
18
+ WorkingDirectory={{HOME}}
19
+
20
+ # Sandboxing (best-effort; user units can't enforce all of these).
21
+ PrivateTmp=true
22
+ ProtectSystem=strict
23
+ ReadWritePaths={{CONFIG_DIR}}
24
+ NoNewPrivileges=true
25
+
26
+ # Allow bind to 127.0.0.1:<port> (privileged binds not needed; ports >= 1024).
27
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
28
+
29
+ Environment="SKALPEL_CONFIG_DIR={{CONFIG_DIR}}"
30
+ Environment="HOME={{HOME}}"
31
+
32
+ [Install]
33
+ WantedBy=default.target
@@ -0,0 +1,42 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
3
+ <RegistrationInfo>
4
+ <Date>2026-05-03T10:00:00</Date>
5
+ <Author>skalpel</Author>
6
+ <Description>Skalpel daemon (local proxy) — user={{USER}}</Description>
7
+ <URI>\Skalpel\skalpel-daemon</URI>
8
+ </RegistrationInfo>
9
+ <Triggers>
10
+ <LogonTrigger>
11
+ <Enabled>true</Enabled>
12
+ <UserId>{{USER}}</UserId>
13
+ </LogonTrigger>
14
+ </Triggers>
15
+ <Principals>
16
+ <Principal id="Author">
17
+ <UserId>{{USER}}</UserId>
18
+ <LogonType>InteractiveToken</LogonType>
19
+ <RunLevel>LeastPrivilege</RunLevel>
20
+ </Principal>
21
+ </Principals>
22
+ <Settings>
23
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
24
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
25
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
26
+ <AllowHardTerminate>false</AllowHardTerminate>
27
+ <RestartOnFailure>
28
+ <Interval>PT10S</Interval>
29
+ <Count>3</Count>
30
+ </RestartOnFailure>
31
+ <Hidden>true</Hidden>
32
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
33
+ </Settings>
34
+ <Actions Context="Author">
35
+ <Exec>
36
+ <Command>{{BIN}}\skalpeld.exe</Command>
37
+ <Arguments>--service-mode</Arguments>
38
+ <WorkingDirectory>{{CONFIG_DIR}}</WorkingDirectory>
39
+ </Exec>
40
+ </Actions>
41
+ <!-- Home reference for postinstall renderer: {{HOME}} -->
42
+ </Task>
@@ -0,0 +1,45 @@
1
+ # register-task.ps1 — register or unregister the Skalpel daemon Scheduled Task.
2
+ #
3
+ # Invoked by postinstall/lib/service-register.js. Runs as the current
4
+ # user (no elevation); the task itself runs as the same user at logon.
5
+ #
6
+ # Placeholders rendered by postinstall/lib/template.js:
7
+ # {{USER}} — interactive user (also written into Task.xml)
8
+ # {{HOME}} — user's home directory
9
+ # {{BIN}} — directory containing skalpeld.exe
10
+ # {{CONFIG_DIR}} — %APPDATA%\skalpel
11
+ #
12
+ # This file is a TEMPLATE; the renderer substitutes the placeholders
13
+ # above before the script is invoked.
14
+
15
+ [CmdletBinding()]
16
+ param(
17
+ [Parameter(Mandatory = $true)]
18
+ [ValidateSet('register', 'unregister')]
19
+ [string]$Action,
20
+
21
+ [Parameter()]
22
+ [string]$XmlPath = '{{CONFIG_DIR}}\Task.xml',
23
+
24
+ [Parameter()]
25
+ [string]$TaskName = 'Skalpel\skalpel-daemon'
26
+ )
27
+
28
+ $ErrorActionPreference = 'Stop'
29
+
30
+ Write-Host "skalpel install: register-task.ps1 user={{USER}} home={{HOME}} bin={{BIN}}"
31
+
32
+ switch ($Action) {
33
+ 'register' {
34
+ if (-not (Test-Path -LiteralPath $XmlPath)) {
35
+ Write-Error "Task XML not found at $XmlPath"
36
+ exit 1
37
+ }
38
+ & schtasks.exe /Create /XML $XmlPath /TN $TaskName /F | Out-Host
39
+ & schtasks.exe /Run /TN $TaskName | Out-Host
40
+ }
41
+ 'unregister' {
42
+ & schtasks.exe /End /TN $TaskName 2>$null | Out-Host
43
+ & schtasks.exe /Delete /TN $TaskName /F | Out-Host
44
+ }
45
+ }