skalpel 2.0.23 → 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 -2899
  29. package/dist/cli/index.js.map +0 -1
  30. package/dist/cli/proxy-runner.js +0 -1649
  31. package/dist/cli/proxy-runner.js.map +0 -1
  32. package/dist/index.cjs +0 -2333
  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 -2287
  37. package/dist/index.js.map +0 -1
  38. package/dist/proxy/index.cjs +0 -1782
  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 -1748
  43. package/dist/proxy/index.js.map +0 -1
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ // colors.js — Catppuccin Frappé palette wrapper for the npm-side
4
+ // surfaces (npx wizard banner, postinstall success line). Reads the
5
+ // canonical hex values from design-tokens.json (Wave 1 artifact) and
6
+ // wraps Picocolors so NO_COLOR is honoured automatically. The box()
7
+ // helper draws a rounded Unicode-art frame around content; structure
8
+ // chars are emitted unconditionally, only the ANSI sequences are
9
+ // suppressed under NO_COLOR.
10
+
11
+ const path = require('path');
12
+ const pc = require('picocolors');
13
+
14
+ const tokens = require(path.resolve(__dirname, '..', 'design-tokens.json'));
15
+
16
+ const palette = (tokens.themes && tokens.themes.frappe) || {};
17
+
18
+ function rgbFromHex(hex) {
19
+ const m = String(hex || '').replace('#', '').match(/^([0-9a-fA-F]{6})$/);
20
+ if (!m) return null;
21
+ const v = parseInt(m[1], 16);
22
+ return [(v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff];
23
+ }
24
+
25
+ function colorize(hex) {
26
+ const rgb = rgbFromHex(hex);
27
+ if (!rgb) {
28
+ return (s) => String(s);
29
+ }
30
+ return (s) => {
31
+ if (process.env.NO_COLOR) return String(s);
32
+ return `\x1b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m${s}\x1b[39m`;
33
+ };
34
+ }
35
+
36
+ const theme = {};
37
+ for (const name of Object.keys(palette)) {
38
+ theme[name] = colorize(palette[name]);
39
+ }
40
+
41
+ function attr(open, close) {
42
+ return (s) => {
43
+ if (process.env.NO_COLOR) return String(s);
44
+ return `\x1b[${open}m${s}\x1b[${close}m`;
45
+ };
46
+ }
47
+
48
+ theme.bold = attr(1, 22);
49
+ theme.italic = attr(3, 23);
50
+ theme.dim = attr(2, 22);
51
+
52
+ const semantic = (tokens.semantic) || {};
53
+
54
+ function semanticHelper(key) {
55
+ const ref = semantic[key];
56
+ if (!ref) return (s) => String(s);
57
+ const parts = ref.split('.');
58
+ if (parts.length !== 2) return (s) => String(s);
59
+ const colorName = parts[1];
60
+ return theme[colorName] || ((s) => String(s));
61
+ }
62
+
63
+ function semanticKey(name) {
64
+ return name
65
+ .replace(/[^a-zA-Z0-9]+([a-zA-Z0-9])/g, (_, c) => c.toUpperCase())
66
+ .replace(/^([A-Z])/, (m) => m.toLowerCase());
67
+ }
68
+
69
+ for (const k of Object.keys(semantic)) {
70
+ const camel = semanticKey(k);
71
+ theme[camel] = semanticHelper(k);
72
+ }
73
+
74
+ const BORDERS = {
75
+ topLeft: '╭',
76
+ topRight: '╮',
77
+ bottomLeft: '╰',
78
+ bottomRight: '╯',
79
+ horizontal: '─',
80
+ vertical: '│',
81
+ };
82
+
83
+ function visibleLength(s) {
84
+ return String(s).replace(/\x1b\[[0-9;]*m/g, '').length;
85
+ }
86
+
87
+ function box(content, borderColor, options) {
88
+ const opts = options || {};
89
+ const padding = typeof opts.padding === 'number' ? opts.padding : 1;
90
+ const lines = String(content).split('\n');
91
+
92
+ let inner;
93
+ if (opts.width === 'fit' || opts.width == null) {
94
+ inner = 0;
95
+ for (const ln of lines) {
96
+ const len = visibleLength(ln);
97
+ if (len > inner) inner = len;
98
+ }
99
+ inner += padding * 2;
100
+ } else {
101
+ inner = Math.max(0, Number(opts.width) - 2);
102
+ }
103
+
104
+ const colorOK = pc.isColorSupported && !process.env.NO_COLOR;
105
+ const colorFn = (colorOK && typeof borderColor === 'string' && theme[borderColor])
106
+ ? theme[borderColor]
107
+ : (s) => String(s);
108
+
109
+ const horiz = BORDERS.horizontal.repeat(inner);
110
+ const top = colorFn(BORDERS.topLeft + horiz + BORDERS.topRight);
111
+ const bottom = colorFn(BORDERS.bottomLeft + horiz + BORDERS.bottomRight);
112
+ const v = colorFn(BORDERS.vertical);
113
+ const pad = ' '.repeat(padding);
114
+
115
+ const rendered = [top];
116
+ for (const ln of lines) {
117
+ const visLen = visibleLength(ln);
118
+ const fill = ' '.repeat(Math.max(0, inner - padding * 2 - visLen));
119
+ rendered.push(`${v}${pad}${ln}${fill}${pad}${v}`);
120
+ }
121
+ rendered.push(bottom);
122
+ return rendered.join('\n');
123
+ }
124
+
125
+ module.exports = { theme, box, BORDERS, palette };
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ // Dispatch shim: locate the platform-specific `skalpel` binary that
3
+ // optionalDependencies installed, then exec it with the user's argv.
4
+ //
5
+ // On first interactive run we render a one-time welcome banner before
6
+ // dispatching. The "first run" predicate is the absence of a flag file
7
+ // at $XDG_STATE_HOME/skalpel/.installed (Unix) or
8
+ // %LOCALAPPDATA%\skalpel\.installed (Windows). If the flag-file write
9
+ // fails (perm denied) we log a single dim line and continue dispatch —
10
+ // banner repeats on every invocation under that condition.
11
+ //
12
+ // TTY/NO_COLOR rules:
13
+ // non-TTY (piped stdout) → suppress banner entirely
14
+ // TTY + NO_COLOR set → render box structure, no ANSI
15
+ // interactive TTY, NO_COLOR off → fully colored banner
16
+ //
17
+ // Reference banner (rendered by colors.js box helper):
18
+ //
19
+ // ╭───────────────────────────────────╮
20
+ // │ skalpel v0.0.1 │
21
+ // │ │
22
+ // │ ✓ Detecting platform… darwin-arm64│
23
+ // │ ✓ Resolving binary… found │
24
+ // ╰───────────────────────────────────╯
25
+ //
26
+ // On error a styled error block frames a ✗ glyph + title + body + hint.
27
+
28
+ 'use strict';
29
+
30
+ const path = require('path');
31
+ const fs = require('fs');
32
+ const os = require('os');
33
+ const { spawnSync } = require('child_process');
34
+
35
+ const colors = require('./colors.js');
36
+ const { theme, box } = colors;
37
+
38
+ const PLATFORM_PACKAGES = {
39
+ 'darwin-arm64': '@skalpelai/skalpel-darwin-arm64',
40
+ 'darwin-x64': '@skalpelai/skalpel-darwin-x64',
41
+ 'linux-arm64': '@skalpelai/skalpel-linux-arm64',
42
+ 'linux-x64': '@skalpelai/skalpel-linux-x64',
43
+ 'win32-x64': '@skalpelai/skalpel-win32-x64',
44
+ };
45
+
46
+ function pkgVersion() {
47
+ try {
48
+ return require('../package.json').version || '0.0.0';
49
+ } catch (_) {
50
+ return '0.0.0';
51
+ }
52
+ }
53
+
54
+ function flagFilePath() {
55
+ if (os.platform() === 'win32') {
56
+ const root = process.env.LOCALAPPDATA;
57
+ if (!root) return null;
58
+ return path.join(root, 'skalpel', '.installed');
59
+ }
60
+ const root = process.env.XDG_STATE_HOME
61
+ || (process.env.HOME ? path.join(process.env.HOME, '.local', 'state') : null);
62
+ if (!root) return null;
63
+ return path.join(root, 'skalpel', '.installed');
64
+ }
65
+
66
+ function isFirstRun() {
67
+ const p = flagFilePath();
68
+ if (!p) return false;
69
+ try {
70
+ return !fs.existsSync(p);
71
+ } catch (_) {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ function markInstalled() {
77
+ const p = flagFilePath();
78
+ if (!p) return { ok: false, path: null };
79
+ try {
80
+ fs.mkdirSync(path.dirname(p), { recursive: true });
81
+ fs.writeFileSync(p, `${new Date().toISOString()}\n`);
82
+ return { ok: true, path: p };
83
+ } catch (err) {
84
+ return { ok: false, path: p, err };
85
+ }
86
+ }
87
+
88
+ function renderWelcome(version, platformLabel, opts) {
89
+ const colored = opts && opts.colored;
90
+ const v = colored ? theme.bold(theme.mauve(`skalpel v${version}`)) : `skalpel v${version}`;
91
+ const detect = colored
92
+ ? `${theme.green('✓')} ${theme.text(`Detecting platform… ${platformLabel}`)}`
93
+ : `✓ Detecting platform… ${platformLabel}`;
94
+ const resolve = colored
95
+ ? `${theme.green('✓')} ${theme.text('Resolving binary… found')}`
96
+ : `✓ Resolving binary… found`;
97
+ const body = [v, '', detect, resolve].join('\n');
98
+ return box(body, colored ? 'mauve' : null, { padding: 1 });
99
+ }
100
+
101
+ function renderError(title, body, hint, opts) {
102
+ const colored = opts && opts.colored;
103
+ const t = colored ? theme.bold(theme.red(`✗ ${title}`)) : `✗ ${title}`;
104
+ const b = colored ? theme.dim(theme.text(body)) : body;
105
+ const h = colored ? theme.mauve(hint) : hint;
106
+ const content = [t, '', b, '', h].join('\n');
107
+ return box(content, colored ? 'red' : null, { padding: 1 });
108
+ }
109
+
110
+ function emitBanner(stream, platformLabel) {
111
+ if (!stream.isTTY) return; // non-TTY: suppress entirely
112
+ const colored = !process.env.NO_COLOR;
113
+ const banner = renderWelcome(pkgVersion(), platformLabel, { colored });
114
+ stream.write(`${banner}\n`);
115
+ const m = markInstalled();
116
+ if (!m.ok && m.path) {
117
+ const dim = colored ? theme.dim : (s) => s;
118
+ stream.write(`${dim(`(welcome shown but flag file unwritable: ${m.path})`)}\n`);
119
+ }
120
+ }
121
+
122
+ function emitError(stream, title, body, hint) {
123
+ const colored = stream.isTTY && !process.env.NO_COLOR;
124
+ const block = renderError(title, body, hint, { colored });
125
+ stream.write(`${block}\n`);
126
+ }
127
+
128
+ function platformLabel() {
129
+ return `${os.platform()}-${os.arch()}`;
130
+ }
131
+
132
+ // isInfoArg returns true for argv shapes that only request info
133
+ // (--help / --version / -h / -v / help / version). When dispatch
134
+ // fails for those args (no platform binary) we render an error
135
+ // block and exit 0, since the shim succeeded at communicating.
136
+ function isInfoArg(argv) {
137
+ if (argv.length === 0) return false;
138
+ const a = argv[0];
139
+ return a === '--help' || a === '-h' || a === 'help' ||
140
+ a === '--version' || a === '-v' || a === 'version';
141
+ }
142
+
143
+ function resolveBinary(name, argv) {
144
+ const infoOnly = isInfoArg(argv || []);
145
+ const failExit = infoOnly ? 0 : 1;
146
+ const key = `${process.platform}-${process.arch}`;
147
+ const pkg = PLATFORM_PACKAGES[key];
148
+ if (!pkg) {
149
+ const supported = Object.keys(PLATFORM_PACKAGES).join(', ');
150
+ emitError(
151
+ process.stderr,
152
+ 'Unsupported platform',
153
+ `skalpel does not ship a binary for ${key}.\nSupported: ${supported}.`,
154
+ `→ Install from GitHub Releases: https://github.com/skalpelai/Skalpelai_Client/releases`
155
+ );
156
+ process.exit(failExit);
157
+ }
158
+ const exe = process.platform === 'win32' ? `${name}.exe` : name;
159
+ let pkgRoot;
160
+ try {
161
+ pkgRoot = path.dirname(require.resolve(`${pkg}/package.json`));
162
+ } catch (_err) {
163
+ emitError(
164
+ process.stderr,
165
+ 'Platform package missing',
166
+ `${pkg} is not installed.`,
167
+ `→ Re-run \`npm install -g skalpel\` or \`npx skalpel\``
168
+ );
169
+ process.exit(failExit);
170
+ }
171
+ const candidate = path.join(pkgRoot, 'bin', exe);
172
+ if (!fs.existsSync(candidate)) {
173
+ emitError(
174
+ process.stderr,
175
+ 'Binary missing',
176
+ `Expected binary at ${candidate} but it does not exist.`,
177
+ `→ Reinstall: \`npm install -g skalpel\``
178
+ );
179
+ process.exit(failExit);
180
+ }
181
+ return candidate;
182
+ }
183
+
184
+ if (require.main === module) {
185
+ if (isFirstRun()) {
186
+ emitBanner(process.stdout, platformLabel());
187
+ }
188
+ const argv = process.argv.slice(2);
189
+ const binary = resolveBinary('skalpel', argv);
190
+ const result = spawnSync(binary, argv, {
191
+ stdio: 'inherit',
192
+ });
193
+ if (result.error) {
194
+ emitError(process.stderr, 'Spawn failed', result.error.message, '→ Check that the binary is executable.');
195
+ process.exit(1);
196
+ }
197
+ process.exit(result.status === null ? 1 : result.status);
198
+ }
199
+
200
+ module.exports = { resolveBinary, PLATFORM_PACKAGES, renderWelcome, renderError, flagFilePath };
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ // Dispatch shim for the daemon. See npm-bin/skalpel.js for the
3
+ // resolution rationale; this file delegates to the same lookup table.
4
+
5
+ 'use strict';
6
+
7
+ const { spawnSync } = require('child_process');
8
+ const { resolveBinary } = require('./skalpel.js');
9
+
10
+ if (require.main === module) {
11
+ const binary = resolveBinary('skalpeld');
12
+ const result = spawnSync(binary, process.argv.slice(2), {
13
+ stdio: 'inherit',
14
+ });
15
+ if (result.error) {
16
+ process.stderr.write(`skalpeld: ${result.error.message}\n`);
17
+ process.exit(1);
18
+ }
19
+ process.exit(result.status === null ? 1 : result.status);
20
+ }
package/package.json CHANGED
@@ -1,81 +1,63 @@
1
1
  {
2
2
  "name": "skalpel",
3
- "version": "2.0.23",
4
- "type": "module",
5
- "description": "Skalpel AI SDK — optimize your OpenAI and Anthropic API calls",
6
- "main": "./dist/index.cjs",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js",
13
- "require": "./dist/index.cjs"
14
- }
3
+ "version": "3.0.0",
4
+ "description": "Skalpel — local proxy and TUI for coding agents (skalpel + skalpeld bundle).",
5
+ "license": "Apache-2.0",
6
+ "homepage": "https://skalpel.ai",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/skalpelai/Skalpelai_Client.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/skalpelai/Skalpelai_Client/issues"
13
+ },
14
+ "author": "SkalpelAI Inc.",
15
+ "keywords": [
16
+ "coding-agent",
17
+ "proxy",
18
+ "tui",
19
+ "claude",
20
+ "cursor",
21
+ "codex"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
15
25
  },
16
26
  "bin": {
17
- "skalpel": "./dist/cli/index.js"
27
+ "skalpel": "npm-bin/skalpel.js",
28
+ "skalpeld": "npm-bin/skalpeld.js"
29
+ },
30
+ "scripts": {
31
+ "postinstall": "node postinstall/index.js",
32
+ "preuninstall": "node postinstall/index.js --uninstall",
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"
18
35
  },
19
36
  "files": [
20
- "dist",
37
+ "npm-bin/",
38
+ "postinstall/",
39
+ "design-tokens.json",
40
+ "INSTALL.md",
21
41
  "README.md",
22
42
  "LICENSE"
23
43
  ],
24
- "scripts": {
25
- "build": "tsup",
26
- "test": "vitest run",
27
- "test:watch": "vitest",
28
- "typecheck": "tsc --noEmit",
29
- "seed-demo-key": "node scripts/seed-demo-key.mjs",
30
- "prepublishOnly": "npm run build"
31
- },
32
- "peerDependencies": {
33
- "@anthropic-ai/sdk": ">=0.30.0",
34
- "openai": ">=4.0.0"
35
- },
36
- "peerDependenciesMeta": {
37
- "openai": {
38
- "optional": true
39
- },
40
- "@anthropic-ai/sdk": {
41
- "optional": true
42
- }
43
- },
44
- "devDependencies": {
45
- "@anthropic-ai/sdk": "^0.30.0",
46
- "@types/ws": "^8.18.1",
47
- "@vitest/coverage-v8": "^2.0.0",
48
- "fast-check": "^3.22.0",
49
- "openai": "^4.0.0",
50
- "pg": "^8.13.0",
51
- "tsup": "^8.0.0",
52
- "typescript": "^5.4.0",
53
- "vitest": "^2.0.0"
44
+ "dependencies": {
45
+ "picocolors": "^1.1.1"
54
46
  },
55
- "keywords": [
56
- "llm",
57
- "openai",
58
- "anthropic",
59
- "api",
60
- "gateway",
61
- "proxy",
62
- "cost-optimization",
63
- "ai",
64
- "sdk"
47
+ "os": [
48
+ "darwin",
49
+ "linux",
50
+ "win32"
65
51
  ],
66
- "repository": {
67
- "type": "git",
68
- "url": "https://github.com/skalpelai/Skalpel_User.git"
69
- },
70
- "homepage": "https://skalpel.ai",
71
- "bugs": {
72
- "url": "https://github.com/skalpelai/Skalpel_User/issues"
73
- },
74
- "license": "MIT",
75
- "dependencies": {
76
- "commander": "^14.0.3",
77
- "open": "^10.0.0",
78
- "undici": "^6.24.1",
79
- "ws": "^8.20.0"
52
+ "cpu": [
53
+ "arm64",
54
+ "x64"
55
+ ],
56
+ "optionalDependencies": {
57
+ "@skalpelai/skalpel-darwin-arm64": "3.0.0",
58
+ "@skalpelai/skalpel-darwin-x64": "3.0.0",
59
+ "@skalpelai/skalpel-linux-arm64": "3.0.0",
60
+ "@skalpelai/skalpel-linux-x64": "3.0.0",
61
+ "@skalpelai/skalpel-win32-x64": "3.0.0"
80
62
  }
81
63
  }