claude-code-hub 1.28.0 → 1.30.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.
- package/README.md +3 -0
- package/lib/cli.js +82 -0
- package/lib/ext.js +45 -43
- package/lib/mods.js +1 -9
- package/package.json +2 -2
- package/server.js +17 -3
package/README.md
CHANGED
|
@@ -183,12 +183,15 @@ The shortcuts also work when focus is inside a tool. Kanban keeps <kbd>Ctrl</kbd
|
|
|
183
183
|
--host <addr> Bind address (default: 127.0.0.1)
|
|
184
184
|
--allowed-hosts <list> Extra host names to accept, comma-separated
|
|
185
185
|
--open Open the browser on start
|
|
186
|
+
--show-token Print the URL with its token also when output is not a terminal
|
|
186
187
|
--detached Keep running when stdin closes (the macOS tray uses it)
|
|
187
188
|
--tray Windows, macOS: run the hub from a tray icon
|
|
188
189
|
--autostart Windows, macOS: same as --tray, and start the tray at logon
|
|
189
190
|
--no-autostart Windows, macOS: stop starting the tray at logon
|
|
190
191
|
--tray-status Windows, macOS: show whether autostart is on and the tray and hub are running
|
|
191
192
|
--app-id <id> Windows, macOS, with --tray or --autostart: the installed app that Open Hub starts
|
|
193
|
+
-v, --version Print the version
|
|
194
|
+
-h, --help Print the flags
|
|
192
195
|
```
|
|
193
196
|
|
|
194
197
|
If a port is busy, the hub uses a free port and reports it.
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { version: HUB_VERSION } = require('../package.json');
|
|
4
|
+
|
|
5
|
+
// Every flag the hub reads, and whether it takes a value. `--<app id>-port` is open-ended, because
|
|
6
|
+
// config.json can add apps.
|
|
7
|
+
const FLAGS = {
|
|
8
|
+
port: true,
|
|
9
|
+
'hub-dir': true,
|
|
10
|
+
'pool-size': true,
|
|
11
|
+
'disable-terminal': false,
|
|
12
|
+
'no-mods': false,
|
|
13
|
+
host: true,
|
|
14
|
+
'allowed-hosts': true,
|
|
15
|
+
open: false,
|
|
16
|
+
'show-token': false,
|
|
17
|
+
detached: false,
|
|
18
|
+
install: false,
|
|
19
|
+
uninstall: false,
|
|
20
|
+
dir: true,
|
|
21
|
+
tray: false,
|
|
22
|
+
autostart: false,
|
|
23
|
+
'no-autostart': false,
|
|
24
|
+
'tray-status': false,
|
|
25
|
+
'app-id': true,
|
|
26
|
+
version: false,
|
|
27
|
+
help: false,
|
|
28
|
+
};
|
|
29
|
+
const SHORT = { '-v': 'version', '-h': 'help' };
|
|
30
|
+
const APP_PORT_RE = /^[a-z0-9-]+-port$/;
|
|
31
|
+
|
|
32
|
+
const HELP = `Claude Code Hub ${HUB_VERSION}
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
claude-code-hub [options] Start the hub, or print the URL of the one that runs
|
|
36
|
+
claude-code-hub ext <command> Manage extensions (claude-code-hub ext --help)
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--port <n> Hub port (default 3540)
|
|
40
|
+
--<app>-port <n> Port of an app: kanban, marketplace, cost, memory
|
|
41
|
+
--hub-dir <path> Hub dir (default ~/.claude-hub)
|
|
42
|
+
--pool-size <n> Config dirs kept running (default 3)
|
|
43
|
+
--disable-terminal Turn off the embedded terminal in Kanban
|
|
44
|
+
--no-mods Start without mods
|
|
45
|
+
--host <addr> Bind address (default 127.0.0.1)
|
|
46
|
+
--allowed-hosts <list> Extra host names to accept, comma-separated
|
|
47
|
+
--open Open the hub in the browser
|
|
48
|
+
--show-token Print the hub URL with its token also when output is not a terminal
|
|
49
|
+
--detached Keep running when stdin closes
|
|
50
|
+
--install, --uninstall Install or remove the hub plugin (--dir <config dir>)
|
|
51
|
+
--tray, --autostart, --no-autostart, --tray-status, --app-id <id>
|
|
52
|
+
Tray and start at login (Windows and macOS)
|
|
53
|
+
-v, --version Print the version
|
|
54
|
+
-h, --help Print this help`;
|
|
55
|
+
|
|
56
|
+
// {exit, out} when the command line is answered here: the version, the help, or a usage error.
|
|
57
|
+
// {} when the hub should go on. `ext` keeps its own parser.
|
|
58
|
+
function checkArgs(args) {
|
|
59
|
+
if (args[0] === 'ext') return {};
|
|
60
|
+
const bad = (what) => ({ exit: 1, out: `${what}\n\n${HELP}` });
|
|
61
|
+
for (let i = 0; i < args.length; i++) {
|
|
62
|
+
const arg = args[i];
|
|
63
|
+
if (!arg.startsWith('-')) return bad(i === 0 ? `Unknown command: ${arg}` : `Unexpected argument: ${arg}`);
|
|
64
|
+
const [flag, value] = arg.startsWith('--') ? [arg.slice(2).split('=')[0], arg.includes('=')] : [SHORT[arg], false];
|
|
65
|
+
const takes = FLAGS[flag] ?? (APP_PORT_RE.test(flag ?? '') || undefined);
|
|
66
|
+
if (takes === undefined) return bad(`Unknown option: ${arg}`);
|
|
67
|
+
if (flag === 'version') return { exit: 0, out: HUB_VERSION };
|
|
68
|
+
if (flag === 'help') return { exit: 0, out: HELP };
|
|
69
|
+
if (takes && !value) i++;
|
|
70
|
+
}
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The hub URL as printed. Off a terminal (an agent's shell, a pipe, the tray's hub.log) the output is
|
|
75
|
+
// kept or read by others, so the token is hidden unless --show-token asks for it.
|
|
76
|
+
function printedUrl(line, url, { show, tokenFile }) {
|
|
77
|
+
if (show) return `${line} ${url}`;
|
|
78
|
+
const hidden = url.replace(/([?&]token=)[^&]*/, '$1<hidden>');
|
|
79
|
+
return `${line} ${hidden}\n The token is hidden. Open the hub with --open, print it with --show-token, or read ${tokenFile}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { HUB_VERSION, FLAGS, HELP, checkArgs, printedUrl };
|
package/lib/ext.js
CHANGED
|
@@ -9,7 +9,7 @@ const { APPS, HUB_ROOT, ID_RE, selectApps, appRoot, readApp } = require('./apps'
|
|
|
9
9
|
const { isContained } = require('./contain');
|
|
10
10
|
const { readConfig, writeConfigFile } = require('./hub-config');
|
|
11
11
|
const { claudeEnv, hasClaude, installPlugin, reinstallPlugin, uninstallPlugin, report, run } = require('./install');
|
|
12
|
-
const { BUNDLED, bundledRoot, entryRoot,
|
|
12
|
+
const { BUNDLED, bundledRoot, entryRoot, readMod } = require('./mods');
|
|
13
13
|
const { pluginChecker } = require('./plugin-status');
|
|
14
14
|
const { isNewer } = require('./update-check');
|
|
15
15
|
|
|
@@ -31,12 +31,15 @@ const USAGE = `Usage: claude-code-hub ext <command>
|
|
|
31
31
|
|
|
32
32
|
A mod (mod.json) adds CSS and JS to the hub page and the apps, and can bring color themes.
|
|
33
33
|
The hub ships some mods (xmas): ext install <id> finds them before the registry.
|
|
34
|
-
A mod with JS runs only when you type y, at install and when ext update changes its code.
|
|
35
34
|
Open the hub at /?nomods to load it with no mod. "modsEnabled": false in config.json
|
|
36
35
|
or --no-mods turns every mod off.
|
|
37
36
|
|
|
38
37
|
--hub-dir <path> The hub dir (default CLAUDE_HUB_DIR, else ~/.claude-hub)
|
|
39
|
-
|
|
38
|
+
An extension from GitHub or a folder runs code with your rights: its server, its plugin's hooks
|
|
39
|
+
and its JS in the hub's pages. ext install and ext update say what runs and ask y/N first.
|
|
40
|
+
An extension the hub ships (xmas, voice) does not ask.
|
|
41
|
+
|
|
42
|
+
--yes Install or update without asking
|
|
40
43
|
--dir <path> The config dir for plugin and dock steps (default CLAUDE_CONFIG_DIR, else ~/.claude)`;
|
|
41
44
|
|
|
42
45
|
const RESTART = ' Restart the hub to load the change.';
|
|
@@ -248,6 +251,10 @@ const KINDS = {
|
|
|
248
251
|
file: 'hub-app.json',
|
|
249
252
|
read: readApp,
|
|
250
253
|
step: (ctx, _id, ext, step, root) => !ext?.plugin || pluginStep(ctx, ext.plugin, step, root),
|
|
254
|
+
code: (ext) => [
|
|
255
|
+
['a server the hub starts', 'a server'],
|
|
256
|
+
...(ext.plugin ? [[`Claude Code plugin ${ext.plugin.id}, in your sessions`, `plugin ${ext.plugin.id}`]] : []),
|
|
257
|
+
],
|
|
251
258
|
stepName: 'Plugin install',
|
|
252
259
|
removeName: 'Plugin removal',
|
|
253
260
|
unread: 'The plugin is not removed, because the manifest does not name it.',
|
|
@@ -261,6 +268,7 @@ const KINDS = {
|
|
|
261
268
|
file: 'addon.json',
|
|
262
269
|
read: readAddon,
|
|
263
270
|
step: dockStep,
|
|
271
|
+
code: (ext) => (ext.manifest.run ? [['a server Kanban starts', 'a server']] : []),
|
|
264
272
|
stepName: 'Putting it on the dock',
|
|
265
273
|
removeName: 'Taking it off the dock',
|
|
266
274
|
unread: 'It is taken off the dock by its id.',
|
|
@@ -273,8 +281,12 @@ const KINDS = {
|
|
|
273
281
|
list: 'mods',
|
|
274
282
|
file: 'mod.json',
|
|
275
283
|
read: readMod,
|
|
276
|
-
step:
|
|
277
|
-
|
|
284
|
+
step: () => true,
|
|
285
|
+
// A mod's JS runs in the page's own origin: in Kanban's it can read the terminal token and type into sessions.
|
|
286
|
+
code: (ext) => {
|
|
287
|
+
const js = jsTargets(ext.manifest);
|
|
288
|
+
return js.length ? [[`JS in ${js.join(', ')}`, `JS in ${js.join(', ')}`]] : [];
|
|
289
|
+
},
|
|
278
290
|
removeName: 'Removing the mod',
|
|
279
291
|
unread: 'It is removed by its id.',
|
|
280
292
|
installed: RESTART,
|
|
@@ -305,13 +317,17 @@ function askTerminal(question) {
|
|
|
305
317
|
}
|
|
306
318
|
}
|
|
307
319
|
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
function
|
|
320
|
+
// Code from a clone or a folder runs only when the user types yes, at install and at each update that
|
|
321
|
+
// moves it. An extension with no code, such as a CSS-only mod, does not ask.
|
|
322
|
+
function consent(ctx, { head, code, verb, update, refused }) {
|
|
323
|
+
if (!code.length) return true;
|
|
324
|
+
console.log(`\n ${head}`);
|
|
325
|
+
if (update) console.log(` It runs with your rights: ${code.map(([, s]) => s).join(', ')}`);
|
|
326
|
+
else console.log(` It runs with your rights:\n${code.map(([l]) => ` ${l}`).join('\n')}`);
|
|
311
327
|
if (ctx.yes) return true;
|
|
312
|
-
const answer = (ctx.ask ?? askTerminal)(`
|
|
328
|
+
const answer = (ctx.ask ?? askTerminal)(` ${verb} it? [y/N] `);
|
|
313
329
|
if (answer === null) {
|
|
314
|
-
console.log(`\n There is no terminal to ask on. Pass --yes to
|
|
330
|
+
console.log(`\n There is no terminal to ask on. Pass --yes to ${verb.toLowerCase()} it.`);
|
|
315
331
|
return false;
|
|
316
332
|
}
|
|
317
333
|
if (/^y(es)?$/i.test(answer)) return true;
|
|
@@ -319,33 +335,8 @@ function allowJs(ctx, id, js, refused) {
|
|
|
319
335
|
return false;
|
|
320
336
|
}
|
|
321
337
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (step !== 'install' || !ext) return true;
|
|
325
|
-
const { targets } = ext.manifest;
|
|
326
|
-
console.log(`\n Mod ${id} changes: ${Object.keys(targets).join(', ')}`);
|
|
327
|
-
const js = jsTargets(ext.manifest);
|
|
328
|
-
if (!js.length) return true;
|
|
329
|
-
console.log(` It runs code in ${js.join(', ')}, with the same access as the app's own code.`);
|
|
330
|
-
return allowJs(ctx, id, js, `${id} is not installed.`);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function jsHashOf(root, id) {
|
|
334
|
-
try {
|
|
335
|
-
return modJsHash(root, readMod(root, id).manifest);
|
|
336
|
-
} catch {
|
|
337
|
-
return null;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function updateAllowed(ctx, id, kind, root, before) {
|
|
342
|
-
if (kind !== KINDS.mod) return true;
|
|
343
|
-
const { manifest } = readMod(root, id);
|
|
344
|
-
const js = jsTargets(manifest);
|
|
345
|
-
if (!js.length || modJsHash(root, manifest) === before) return true;
|
|
346
|
-
console.log(`\n The new version of ${id} changes the code it runs in ${js.join(', ')}.`);
|
|
347
|
-
return allowJs(ctx, id, js, `${id} keeps the code it had.`);
|
|
348
|
-
}
|
|
338
|
+
const askInstall = (ctx, id, kind, ext, from) =>
|
|
339
|
+
consent(ctx, { head: `${id} from ${from}`, code: kind.code(ext), verb: 'Install', refused: `${id} is not installed.` });
|
|
349
340
|
|
|
350
341
|
// An id the hub runs gets its plugin installed. A folder is added to the hub, with its plugin, unless the
|
|
351
342
|
// hub already runs it from there; then only the plugin is installed. Any other name is looked up in the registry.
|
|
@@ -482,6 +473,10 @@ function installGithub(ctx, target, file, config, name) {
|
|
|
482
473
|
return true;
|
|
483
474
|
}
|
|
484
475
|
checkRequires(ctx, ext.manifest, config);
|
|
476
|
+
if (!askInstall(ctx, id, kind, ext, `${src.source} at ${short(sha)}`)) {
|
|
477
|
+
removeDir(dir);
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
485
480
|
} catch (e) {
|
|
486
481
|
removeDir(dir);
|
|
487
482
|
throw e;
|
|
@@ -545,6 +540,7 @@ function installFolder(ctx, target, file, config) {
|
|
|
545
540
|
return true;
|
|
546
541
|
}
|
|
547
542
|
const linked = root.replaceAll('\\', '/');
|
|
543
|
+
if (!askInstall(ctx, id, kind, ext, linked)) return false;
|
|
548
544
|
if (!addExtension(ctx, root, file, config, { path: linked, source: 'local' }, kind)) return false;
|
|
549
545
|
console.log(`\n Linked ${id} to ${linked}.\n${kind.installed}`);
|
|
550
546
|
return true;
|
|
@@ -578,17 +574,22 @@ function update(ctx, id) {
|
|
|
578
574
|
const [entry, kind] = managedEntry(config, id, 'update');
|
|
579
575
|
const root = rootOf(ctx, entry);
|
|
580
576
|
let sha;
|
|
577
|
+
let ext;
|
|
581
578
|
if (isGithub(entry)) {
|
|
582
579
|
const clone = cloneOf(ctx, entry);
|
|
583
|
-
const before = kind === KINDS.mod ? jsHashOf(root, id) : null;
|
|
584
580
|
const was = git(['rev-parse', 'HEAD'], clone);
|
|
585
581
|
git(['fetch', '--quiet', 'origin'], clone);
|
|
586
582
|
sha = checkout(clone, entry.ref);
|
|
587
583
|
const back = () => was !== sha && checkout(clone, was);
|
|
588
|
-
let allowed;
|
|
584
|
+
let allowed = true;
|
|
589
585
|
try {
|
|
590
|
-
|
|
591
|
-
|
|
586
|
+
ext = kind.read(root, id);
|
|
587
|
+
checkRequires(ctx, ext.manifest, config);
|
|
588
|
+
if (sha !== was) {
|
|
589
|
+
const n = git(['rev-list', '--count', `${was}..${sha}`], clone);
|
|
590
|
+
const head = `${id} ${short(was)} -> ${short(sha)} (${n} commit${n === '1' ? '' : 's'})`;
|
|
591
|
+
allowed = consent(ctx, { head, code: kind.code(ext), verb: 'Update', update: true, refused: `${id} keeps the code it had.` });
|
|
592
|
+
}
|
|
592
593
|
} catch (e) {
|
|
593
594
|
back();
|
|
594
595
|
throw e;
|
|
@@ -598,9 +599,10 @@ function update(ctx, id) {
|
|
|
598
599
|
return false;
|
|
599
600
|
}
|
|
600
601
|
} else {
|
|
601
|
-
|
|
602
|
+
ext = kind.read(root, id);
|
|
603
|
+
checkRequires(ctx, ext.manifest, config);
|
|
602
604
|
}
|
|
603
|
-
if (!kind.step(ctx, id,
|
|
605
|
+
if (!kind.step(ctx, id, ext, 'reinstall', root, config)) return false;
|
|
604
606
|
if (sha && sha !== entry.sha) writeConfigFile(file, withList(config, kind, (list) => list.map((e) => (e === entry ? { ...e, sha } : e))));
|
|
605
607
|
const at = sha ? ` to ${short(sha)}` : '';
|
|
606
608
|
console.log(`\n Updated ${id}${at}. ${kind.updated}`);
|
package/lib/mods.js
CHANGED
|
@@ -76,14 +76,6 @@ function readSurface(root) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
// Changes with any JS file of the mod, and with the page each one runs in. `ext update` asks again when it does.
|
|
80
|
-
function modJsHash(root, manifest) {
|
|
81
|
-
const hash = crypto.createHash('sha1');
|
|
82
|
-
for (const [target, parts] of Object.entries(manifest.targets))
|
|
83
|
-
for (const f of parts.js ?? []) hash.update(`${target}\0${f}\0`).update(fs.readFileSync(path.join(root, f))).update('\0');
|
|
84
|
-
return hash.digest('hex');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
79
|
// The mods of `config.mods`, read once at hub start. `version` changes with any file of the mod,
|
|
88
80
|
// so the browser fetches the new files after a hub restart and caches them until then.
|
|
89
81
|
// `surfaces` maps "hub" and each enabled app id to its surface version, or null when it has none.
|
|
@@ -153,4 +145,4 @@ function hubModScript(assets) {
|
|
|
153
145
|
].join('\n');
|
|
154
146
|
}
|
|
155
147
|
|
|
156
|
-
module.exports = { BUNDLED, readMod, readSurface,
|
|
148
|
+
module.exports = { BUNDLED, readMod, readSurface, entryRoot, bundledRoot, resolveMods, modAssets, appModEnv, hubModScript };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-hub",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
4
4
|
"description": "Unified hub for Claude Code tools — Marketplace + Kanban in one PWA",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"homepage": "https://github.com/NikiforovAll/claude-code-hub#readme",
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"claude-code-cost": "1.6.1",
|
|
50
|
-
"claude-code-kanban": "7.
|
|
50
|
+
"claude-code-kanban": "7.27.0",
|
|
51
51
|
"claude-code-marketplace": "1.7.2",
|
|
52
52
|
"claude-code-memory-explorer": "1.3.5",
|
|
53
53
|
"express": "^4.21.0",
|
package/server.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
const { HUB_VERSION, checkArgs, printedUrl } = require('./lib/cli');
|
|
4
|
+
|
|
5
|
+
// Before the other requires: -v and -h answer at once, and a bad flag never reaches the running-hub check,
|
|
6
|
+
// which prints the hub URL with its token.
|
|
7
|
+
const cli = checkArgs(process.argv.slice(2));
|
|
8
|
+
if (cli.exit !== undefined) {
|
|
9
|
+
(cli.exit ? console.error : console.log)(cli.out);
|
|
10
|
+
process.exit(cli.exit);
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
const { spawn } = require('child_process');
|
|
4
14
|
const crypto = require('crypto');
|
|
5
15
|
const express = require('express');
|
|
@@ -33,7 +43,6 @@ const {
|
|
|
33
43
|
MAX_POOL_SIZE,
|
|
34
44
|
settingValues,
|
|
35
45
|
} = require('./lib/settings');
|
|
36
|
-
const { version: HUB_VERSION } = require('./package.json');
|
|
37
46
|
|
|
38
47
|
function getArg(name) {
|
|
39
48
|
const idx = process.argv.findIndex((a) => a.startsWith(`--${name}`));
|
|
@@ -1464,7 +1473,7 @@ if (!TRAY_FLAG) {
|
|
|
1464
1473
|
listenWithFallback(app, HUB_PORT, onReady, '');
|
|
1465
1474
|
return;
|
|
1466
1475
|
}
|
|
1467
|
-
|
|
1476
|
+
printUrl(`Claude Code Hub already runs for ${HUB_DIR} at`, running.port);
|
|
1468
1477
|
if (process.argv.includes('--open')) {
|
|
1469
1478
|
import('open').then((m) => m.default(accessUrl(running.port))).finally(() => process.exit(0));
|
|
1470
1479
|
} else {
|
|
@@ -1477,8 +1486,13 @@ function accessUrl(port) {
|
|
|
1477
1486
|
return `http://localhost:${port}/?token=${HUB_TOKEN}`;
|
|
1478
1487
|
}
|
|
1479
1488
|
|
|
1489
|
+
function printUrl(line, port) {
|
|
1490
|
+
const show = process.argv.includes('--show-token') || require('tty').isatty(1);
|
|
1491
|
+
console.log(printedUrl(line, accessUrl(port), { show, tokenFile: HUB_TOKEN_FILE }));
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1480
1494
|
function printBanner(port) {
|
|
1481
|
-
|
|
1495
|
+
printUrl('Claude Code Hub running at', port);
|
|
1482
1496
|
const warning = net.exposureWarning();
|
|
1483
1497
|
if (warning) console.log(warning);
|
|
1484
1498
|
console.log('Type "q" or "exit" to stop the server');
|