claude-code-hub 1.21.0 → 1.22.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-hub",
3
- "version": "1.21.0",
3
+ "version": "1.22.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.5.0",
50
- "claude-code-kanban": "7.16.0",
50
+ "claude-code-kanban": "7.17.0",
51
51
  "claude-code-marketplace": "1.6.0",
52
52
  "claude-code-memory-explorer": "1.3.2",
53
53
  "express": "^4.21.0",
@@ -97,6 +97,8 @@
97
97
  let origin = null;
98
98
  let welcome = null;
99
99
  let forward = null;
100
+ let appOrigins = new Set();
101
+ let linkOpened = null;
100
102
  let themeBinding = null;
101
103
  let lastTheme = null;
102
104
  let swatchesPainted = false;
@@ -178,6 +180,7 @@
178
180
  themes: Array.isArray(m.themes) ? m.themes : [],
179
181
  };
180
182
  forward = strings(m.forward);
183
+ appOrigins = strings(m.origins);
181
184
  setStatus('live');
182
185
  if (welcome.themes.length) for (const fn of themesFns) fn(pickerThemes());
183
186
  themesFns.clear();
@@ -246,8 +249,46 @@
246
249
  resolve({ ok: true, handledBy: 'standalone' });
247
250
  }
248
251
 
252
+ // A link with no target loads in the app's frame, and most sites refuse framing; in the installed
253
+ // window a _blank one opens nothing. Capture on the window runs before a modal's stopPropagation
254
+ // can hide the click. The hub page refuses framing, so a link to it opens outside too.
255
+ function onLinkClick(e) {
256
+ if (!welcome || e.defaultPrevented || (e.type === 'auxclick') !== (e.button === 1)) return;
257
+ const a = e.target?.closest?.('a[href]');
258
+ if (!a || a.hasAttribute?.('download')) return;
259
+ const target = a.getAttribute?.('target') || '';
260
+ if (target && !target.startsWith('_')) return;
261
+ let url;
262
+ try {
263
+ // An SVG <a> has an SVGAnimatedString href, so the attribute is read instead.
264
+ url = new URL(a.getAttribute?.('href') ?? a.href, doc.baseURI);
265
+ } catch (_) {
266
+ return;
267
+ }
268
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return;
269
+ const inApp = url.origin === win.location.origin || appOrigins.has(url.origin);
270
+ if (inApp && target !== '_blank') return;
271
+ e.preventDefault();
272
+ hub.openExternal(url.href);
273
+ // Apps written before this listener open the same link from their own click handler.
274
+ linkOpened = url.href;
275
+ win.setTimeout(() => {
276
+ linkOpened = null;
277
+ });
278
+ }
279
+
280
+ function sameUrl(url, href) {
281
+ try {
282
+ return new URL(String(url), doc.baseURI).href === href;
283
+ } catch (_) {
284
+ return false;
285
+ }
286
+ }
287
+
249
288
  win.addEventListener('message', onMessage);
250
289
  doc.addEventListener('keydown', onKeydown);
290
+ win.addEventListener('click', onLinkClick, true);
291
+ win.addEventListener('auxclick', onLinkClick, true);
251
292
 
252
293
  const loaded =
253
294
  doc.readyState === 'complete'
@@ -378,6 +419,7 @@
378
419
  },
379
420
  // In the hub's installed PWA window, a framed page's own _blank open does nothing.
380
421
  openExternal(url) {
422
+ if (linkOpened !== null && sameUrl(url, linkOpened)) return;
381
423
  if (origin) post({ type: 'hub:openExternal', url });
382
424
  else win.open(url, '_blank', 'noopener');
383
425
  },
package/public/app.js CHANGED
@@ -559,6 +559,7 @@ function welcomeFor(data) {
559
559
  keys: actionKeys(),
560
560
  themes: hubThemes,
561
561
  actions: Object.keys(actions),
562
+ origins: Object.keys(apps).map(originOf),
562
563
  };
563
564
  }
564
565