umbrella-context 0.1.38 → 0.1.40

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.
@@ -6,6 +6,8 @@ type HubEntry = {
6
6
  includes: string[];
7
7
  nextSteps: string[];
8
8
  installsConnector?: string;
9
+ registryName?: string;
10
+ registryUrl?: string;
9
11
  };
10
12
  export type HubCommandResult = {
11
13
  action: "list" | "install" | "inspect" | "installed" | "registry" | "browse";
@@ -16,7 +18,7 @@ export type HubCommandResult = {
16
18
  recommendedConnector?: string;
17
19
  };
18
20
  export type HubRegistryEntry = HubEntry;
19
- export declare function listHubRegistryEntries(): HubRegistryEntry[];
21
+ export declare function listHubRegistryEntries(): Promise<HubRegistryEntry[]>;
20
22
  export declare function hubCommandAction(action: string, subAction?: string): Promise<HubCommandResult | void>;
21
23
  export declare function hubCommand(cli: any): void;
22
24
  export {};
@@ -2,59 +2,8 @@ import { randomUUID } from "crypto";
2
2
  import chalk from "chalk";
3
3
  import prompts from "prompts";
4
4
  import { configManager } from "../config.js";
5
- import { getInstalledHubEntries, getSessionState, recordSessionEvent, recordSessionHubEntry, setInstalledHubEntries, setSessionPanel, writeHubAsset, writeHubAssetFiles } from "../repo-state.js";
5
+ import { getInstalledHubEntries, getSessionState, recordSessionEvent, recordSessionHubEntry, setInstalledHubEntries, setSessionPanel, writeHubAsset, writeHubAssetFiles, } from "../repo-state.js";
6
6
  import { connectorsCommandAction } from "./connectors.js";
7
- async function runHubBrowseFlow() {
8
- const answer = await prompts({
9
- type: "select",
10
- name: "value",
11
- message: "Choose a hub entry to browse",
12
- choices: DEFAULT_HUB_ENTRIES.map((entry) => ({
13
- title: `${entry.name} (${entry.type})`,
14
- description: entry.description,
15
- value: entry.slug,
16
- })),
17
- });
18
- if (!answer.value) {
19
- console.log(chalk.yellow("\n No hub entry selected."));
20
- return;
21
- }
22
- const inspectResult = await hubCommandAction("inspect", answer.value);
23
- if (!inspectResult?.installedEntry)
24
- return;
25
- const next = await prompts({
26
- type: "select",
27
- name: "value",
28
- message: `What do you want to do with ${inspectResult.installedEntry.name}?`,
29
- choices: [
30
- { title: "Install it into this repo", value: "install" },
31
- ...(inspectResult.recommendedConnector
32
- ? [{ title: `Install and run ${inspectResult.recommendedConnector}`, value: "connector" }]
33
- : []),
34
- { title: "Back", value: "back" },
35
- ],
36
- });
37
- if (!next.value || next.value === "back") {
38
- return {
39
- action: "browse",
40
- changed: false,
41
- installedEntry: inspectResult.installedEntry,
42
- recommendedConnector: inspectResult.recommendedConnector,
43
- };
44
- }
45
- const installResult = await hubCommandAction("install", inspectResult.installedEntry.slug);
46
- if (next.value === "connector" && inspectResult.recommendedConnector) {
47
- await connectorsCommandAction("run", inspectResult.recommendedConnector);
48
- }
49
- return {
50
- action: "browse",
51
- changed: Boolean(installResult?.changed),
52
- installedEntry: installResult?.installedEntry ?? inspectResult.installedEntry,
53
- assetPath: installResult?.assetPath,
54
- filePaths: installResult?.filePaths,
55
- recommendedConnector: inspectResult.recommendedConnector,
56
- };
57
- }
58
7
  const DEFAULT_HUB_ENTRIES = [
59
8
  {
60
9
  slug: "quick-ping-growth-bundle",
@@ -93,8 +42,108 @@ const DEFAULT_HUB_ENTRIES = [
93
42
  installsConnector: "codex-mcp",
94
43
  },
95
44
  ];
96
- export function listHubRegistryEntries() {
97
- return [...DEFAULT_HUB_ENTRIES];
45
+ function normalizeRemoteHubEntry(raw, registry) {
46
+ if (!raw.slug || !raw.name) {
47
+ return null;
48
+ }
49
+ if (raw.type !== "bundle" && raw.type !== "skill" && raw.type !== "connector") {
50
+ return null;
51
+ }
52
+ return {
53
+ slug: raw.slug,
54
+ name: raw.name,
55
+ type: raw.type,
56
+ description: raw.description ?? "No description provided.",
57
+ includes: Array.isArray(raw.includes) ? raw.includes : [],
58
+ nextSteps: Array.isArray(raw.nextSteps) ? raw.nextSteps : [],
59
+ installsConnector: raw.installsConnector,
60
+ registryName: registry.name,
61
+ registryUrl: registry.url,
62
+ };
63
+ }
64
+ function buildRegistryHeaders(registry) {
65
+ const token = registry.token?.trim();
66
+ const scheme = registry.authScheme ?? "none";
67
+ if (!token || scheme === "none") {
68
+ return undefined;
69
+ }
70
+ if (scheme === "bearer") {
71
+ return { Authorization: `Bearer ${token}` };
72
+ }
73
+ if (scheme === "token") {
74
+ return { Authorization: `Token ${token}` };
75
+ }
76
+ if (scheme === "basic") {
77
+ return { Authorization: `Basic ${token}` };
78
+ }
79
+ if (scheme === "custom-header") {
80
+ return { [registry.headerName?.trim() || "X-Registry-Token"]: token };
81
+ }
82
+ return undefined;
83
+ }
84
+ async function fetchRegistryStatus(registry) {
85
+ if (registry.url.startsWith("https://hub.umbrella.local/")) {
86
+ return {
87
+ registry,
88
+ status: "ok",
89
+ entryCount: DEFAULT_HUB_ENTRIES.length,
90
+ entries: DEFAULT_HUB_ENTRIES.map((entry) => ({
91
+ ...entry,
92
+ registryName: registry.name,
93
+ registryUrl: registry.url,
94
+ })),
95
+ };
96
+ }
97
+ try {
98
+ const response = await fetch(registry.url, {
99
+ headers: buildRegistryHeaders(registry),
100
+ });
101
+ if (!response.ok) {
102
+ return {
103
+ registry,
104
+ status: "error",
105
+ entryCount: 0,
106
+ error: `HTTP ${response.status}`,
107
+ entries: [],
108
+ };
109
+ }
110
+ const payload = (await response.json());
111
+ const entries = (payload.entries ?? [])
112
+ .map((entry) => normalizeRemoteHubEntry(entry, registry))
113
+ .filter((entry) => Boolean(entry));
114
+ return {
115
+ registry,
116
+ status: "ok",
117
+ entryCount: entries.length,
118
+ entries,
119
+ };
120
+ }
121
+ catch (error) {
122
+ return {
123
+ registry,
124
+ status: "error",
125
+ entryCount: 0,
126
+ error: error instanceof Error ? error.message : "Unknown error",
127
+ entries: [],
128
+ };
129
+ }
130
+ }
131
+ async function getHubCatalog() {
132
+ const statuses = await Promise.all(configManager.hubRegistries.map((registry) => fetchRegistryStatus(registry)));
133
+ const deduped = new Map();
134
+ for (const entry of statuses.flatMap((status) => status.entries)) {
135
+ if (!deduped.has(entry.slug)) {
136
+ deduped.set(entry.slug, entry);
137
+ }
138
+ }
139
+ return {
140
+ statuses,
141
+ entries: [...deduped.values()],
142
+ };
143
+ }
144
+ export async function listHubRegistryEntries() {
145
+ const { entries } = await getHubCatalog();
146
+ return entries;
98
147
  }
99
148
  function buildHubAsset(entry) {
100
149
  return [
@@ -102,6 +151,7 @@ function buildHubAsset(entry) {
102
151
  "",
103
152
  `Type: ${entry.type}`,
104
153
  `Slug: ${entry.slug}`,
154
+ `Registry: ${entry.registryName ?? "Umbrella Hub"}`,
105
155
  "",
106
156
  "## Why this exists",
107
157
  "",
@@ -246,22 +296,86 @@ function printInstalled(installed) {
246
296
  console.log(chalk.gray(` Installed from ${entry.registryName} at ${new Date(entry.installedAt).toLocaleString()}`));
247
297
  });
248
298
  }
249
- function printRegistryList() {
299
+ function printRegistryList(statuses) {
250
300
  console.log(chalk.bold("\n Hub Registries\n"));
251
- configManager.hubRegistries.forEach((registry, index) => {
301
+ statuses.forEach(({ registry, status, entryCount, error }, index) => {
302
+ const authLabel = registry.authScheme && registry.authScheme !== "none"
303
+ ? ` [${registry.authScheme}]`
304
+ : "";
305
+ const tokenLabel = registry.token ? " (authenticated)" : "";
306
+ const statusLabel = status === "ok"
307
+ ? `${entryCount} entr${entryCount === 1 ? "y" : "ies"}`
308
+ : `error: ${error ?? "unknown"}`;
252
309
  console.log(` ${index + 1}. ${registry.name}`);
253
- console.log(chalk.gray(` ${registry.url}`));
310
+ console.log(chalk.gray(` ${registry.url}${authLabel}${tokenLabel}`));
311
+ console.log(chalk.gray(` ${statusLabel}`));
254
312
  });
255
313
  }
314
+ async function runHubBrowseFlow() {
315
+ const { entries } = await getHubCatalog();
316
+ const answer = await prompts({
317
+ type: "select",
318
+ name: "value",
319
+ message: "Choose a hub entry to browse",
320
+ choices: entries.map((entry) => ({
321
+ title: `${entry.name} (${entry.type})`,
322
+ description: entry.registryName ? `${entry.description} [${entry.registryName}]` : entry.description,
323
+ value: entry.slug,
324
+ })),
325
+ });
326
+ if (!answer.value) {
327
+ console.log(chalk.yellow("\n No hub entry selected."));
328
+ return;
329
+ }
330
+ const inspectResult = await hubCommandAction("inspect", answer.value);
331
+ if (!inspectResult?.installedEntry)
332
+ return;
333
+ const next = await prompts({
334
+ type: "select",
335
+ name: "value",
336
+ message: `What do you want to do with ${inspectResult.installedEntry.name}?`,
337
+ choices: [
338
+ { title: "Install it into this repo", value: "install" },
339
+ ...(inspectResult.recommendedConnector
340
+ ? [{ title: `Install and run ${inspectResult.recommendedConnector}`, value: "connector" }]
341
+ : []),
342
+ { title: "Back", value: "back" },
343
+ ],
344
+ });
345
+ if (!next.value || next.value === "back") {
346
+ return {
347
+ action: "browse",
348
+ changed: false,
349
+ installedEntry: inspectResult.installedEntry,
350
+ recommendedConnector: inspectResult.recommendedConnector,
351
+ };
352
+ }
353
+ const installResult = await hubCommandAction("install", inspectResult.installedEntry.slug);
354
+ if (next.value === "connector" && inspectResult.recommendedConnector) {
355
+ await connectorsCommandAction("run", inspectResult.recommendedConnector);
356
+ }
357
+ return {
358
+ action: "browse",
359
+ changed: Boolean(installResult?.changed),
360
+ installedEntry: installResult?.installedEntry ?? inspectResult.installedEntry,
361
+ assetPath: installResult?.assetPath,
362
+ filePaths: installResult?.filePaths,
363
+ recommendedConnector: inspectResult.recommendedConnector,
364
+ };
365
+ }
256
366
  export async function hubCommandAction(action, subAction) {
257
367
  const normalized = action.toLowerCase();
258
368
  await setSessionPanel("hub", subAction ?? normalized);
259
369
  if (normalized === "list") {
260
370
  await setSessionPanel("hub", "list");
371
+ const { entries } = await getHubCatalog();
261
372
  console.log(chalk.bold("\n Hub Catalog\n"));
262
- DEFAULT_HUB_ENTRIES.forEach((entry, index) => {
373
+ entries.forEach((entry, index) => {
263
374
  console.log(` ${index + 1}. ${entry.name} [${entry.type}]`);
264
375
  console.log(chalk.gray(` ${entry.description}`));
376
+ if (entry.registryName) {
377
+ console.log(chalk.gray(` Source: ${entry.registryName}`));
378
+ }
265
379
  });
266
380
  const installed = await getInstalledHubEntries();
267
381
  if (installed.length > 0) {
@@ -274,20 +388,21 @@ export async function hubCommandAction(action, subAction) {
274
388
  return runHubBrowseFlow();
275
389
  }
276
390
  if (normalized === "install") {
391
+ const { entries } = await getHubCatalog();
277
392
  const installed = await getInstalledHubEntries();
278
- let selected = subAction ? DEFAULT_HUB_ENTRIES.find((entry) => entry.slug === subAction) : undefined;
393
+ let selected = subAction ? entries.find((entry) => entry.slug === subAction) : undefined;
279
394
  if (!selected) {
280
395
  const answer = await prompts({
281
396
  type: "select",
282
397
  name: "value",
283
398
  message: "Choose a hub entry to install into this repo",
284
- choices: DEFAULT_HUB_ENTRIES.map((entry) => ({
399
+ choices: entries.map((entry) => ({
285
400
  title: `${entry.name} (${entry.type})`,
286
- description: entry.description,
401
+ description: entry.registryName ? `${entry.description} [${entry.registryName}]` : entry.description,
287
402
  value: entry.slug,
288
403
  })),
289
404
  });
290
- selected = DEFAULT_HUB_ENTRIES.find((entry) => entry.slug === answer.value);
405
+ selected = entries.find((entry) => entry.slug === answer.value);
291
406
  }
292
407
  if (!selected) {
293
408
  console.log(chalk.yellow("\n No hub entry selected."));
@@ -311,7 +426,6 @@ export async function hubCommandAction(action, subAction) {
311
426
  installedEntry: selected,
312
427
  };
313
428
  }
314
- const registry = configManager.hubRegistries[0];
315
429
  await setInstalledHubEntries([
316
430
  ...installed,
317
431
  {
@@ -321,7 +435,7 @@ export async function hubCommandAction(action, subAction) {
321
435
  type: selected.type,
322
436
  description: selected.description,
323
437
  installedAt: new Date().toISOString(),
324
- registryName: registry?.name ?? "Umbrella Hub",
438
+ registryName: selected.registryName ?? "Umbrella Hub",
325
439
  },
326
440
  ]);
327
441
  const assetPath = await writeHubAsset(selected.slug, buildHubAsset(selected));
@@ -353,9 +467,8 @@ export async function hubCommandAction(action, subAction) {
353
467
  };
354
468
  }
355
469
  if (normalized === "inspect") {
356
- const target = subAction
357
- ? DEFAULT_HUB_ENTRIES.find((entry) => entry.slug === subAction)
358
- : undefined;
470
+ const { entries } = await getHubCatalog();
471
+ const target = subAction ? entries.find((entry) => entry.slug === subAction) : undefined;
359
472
  if (!target) {
360
473
  console.log(chalk.red("Use: hub inspect <slug>"));
361
474
  return;
@@ -376,6 +489,7 @@ export async function hubCommandAction(action, subAction) {
376
489
  console.log(` Type: ${target.type}`);
377
490
  console.log(` Slug: ${target.slug}`);
378
491
  console.log(` Installed: ${isInstalled ? "Yes" : "No"}`);
492
+ console.log(` Registry: ${target.registryName ?? "Umbrella Hub"}`);
379
493
  console.log(chalk.gray(` ${target.description}`));
380
494
  console.log("");
381
495
  console.log(chalk.cyan(" Includes"));
@@ -417,13 +531,38 @@ export async function hubCommandAction(action, subAction) {
417
531
  await setSessionPanel("hub", `registry:${subAction ?? "list"}`);
418
532
  const mode = (subAction ?? "list").toLowerCase();
419
533
  if (mode === "list") {
420
- printRegistryList();
534
+ const { statuses } = await getHubCatalog();
535
+ printRegistryList(statuses);
421
536
  return { action: "registry", changed: false };
422
537
  }
423
538
  if (mode === "add") {
424
539
  const answer = await prompts([
425
540
  { type: "text", name: "name", message: "Registry name" },
426
541
  { type: "text", name: "url", message: "Registry URL" },
542
+ {
543
+ type: "select",
544
+ name: "authScheme",
545
+ message: "Authentication",
546
+ choices: [
547
+ { title: "No auth", value: "none" },
548
+ { title: "Bearer token", value: "bearer" },
549
+ { title: "Token header", value: "token" },
550
+ { title: "Basic header", value: "basic" },
551
+ { title: "Custom header", value: "custom-header" },
552
+ ],
553
+ initial: 0,
554
+ },
555
+ {
556
+ type: (_prev, values) => values.authScheme && values.authScheme !== "none" ? "password" : null,
557
+ name: "token",
558
+ message: "Registry token",
559
+ },
560
+ {
561
+ type: (_prev, values) => values.authScheme === "custom-header" ? "text" : null,
562
+ name: "headerName",
563
+ message: "Custom header name",
564
+ initial: "X-Registry-Token",
565
+ },
427
566
  ]);
428
567
  if (!answer.name || !answer.url) {
429
568
  console.log(chalk.yellow("\n Registry add cancelled."));
@@ -433,6 +572,9 @@ export async function hubCommandAction(action, subAction) {
433
572
  id: randomUUID(),
434
573
  name: answer.name,
435
574
  url: answer.url,
575
+ authScheme: answer.authScheme ?? "none",
576
+ token: answer.token || undefined,
577
+ headerName: answer.headerName || undefined,
436
578
  updatedAt: new Date().toISOString(),
437
579
  });
438
580
  await recordSessionEvent({
@@ -444,6 +586,14 @@ export async function hubCommandAction(action, subAction) {
444
586
  status: "success",
445
587
  });
446
588
  console.log(chalk.green(`\n Added registry ${answer.name}.`));
589
+ const { statuses } = await getHubCatalog();
590
+ const addedStatus = statuses.find((status) => status.registry.url === answer.url);
591
+ if (addedStatus?.status === "ok") {
592
+ console.log(chalk.gray(` Found ${addedStatus.entryCount} hub entr${addedStatus.entryCount === 1 ? "y" : "ies"} immediately.`));
593
+ }
594
+ else if (addedStatus?.error) {
595
+ console.log(chalk.yellow(` Registry saved, but the first fetch failed: ${addedStatus.error}`));
596
+ }
447
597
  return { action: "registry", changed: true };
448
598
  }
449
599
  if (mode === "remove") {
@@ -25,7 +25,7 @@ function detectMixedSavedConnection(umbrellaUrl, serverUrl) {
25
25
  const backend = new URL(serverUrl);
26
26
  const umbrellaIsLoopback = isLocalOnlyServerUrl(umbrellaUrl);
27
27
  const backendIsLoopback = isLocalOnlyServerUrl(serverUrl);
28
- const differentHosts = umbrella.hostname !== backend.hostname || umbrella.port !== backend.port;
28
+ const differentHosts = umbrella.hostname !== backend.hostname;
29
29
  if (!differentHosts && umbrellaIsLoopback === backendIsLoopback) {
30
30
  return null;
31
31
  }
@@ -27,7 +27,7 @@ export function buildConnectionPresentation(config, authSnapshot = getStoredUmbr
27
27
  const backend = new URL(config.serverUrl);
28
28
  const umbrellaIsLoopback = ["127.0.0.1", "localhost"].includes(umbrella.hostname.toLowerCase());
29
29
  const backendIsLoopback = ["127.0.0.1", "localhost"].includes(backend.hostname.toLowerCase());
30
- if (umbrella.hostname !== backend.hostname || umbrella.port !== backend.port) {
30
+ if (umbrella.hostname !== backend.hostname) {
31
31
  warnings.push(`The saved Umbrella app URL (${config.umbrellaUrl}) and Context backend URL (${config.serverUrl}) point to different places.`);
32
32
  }
33
33
  if (umbrellaIsLoopback !== backendIsLoopback) {
@@ -297,6 +297,7 @@ function App() {
297
297
  const [modelPanelRecentModel, setModelPanelRecentModel] = useState(null);
298
298
  const [hubPanelMode, setHubPanelMode] = useState("browse");
299
299
  const [hubPanelSelectedIndex, setHubPanelSelectedIndex] = useState(0);
300
+ const [hubRegistryEntries, setHubRegistryEntries] = useState([]);
300
301
  const [connectorsPanelMode, setConnectorsPanelMode] = useState("browse");
301
302
  const [connectorsPanelSelectedIndex, setConnectorsPanelSelectedIndex] = useState(0);
302
303
  const [tasksPanelMode, setTasksPanelMode] = useState("browse");
@@ -404,11 +405,14 @@ function App() {
404
405
  useEffect(() => {
405
406
  if (activePanel !== "hub" || busyLabel)
406
407
  return;
407
- const registryEntries = listHubRegistryEntries();
408
- const recentSlug = vendorHubStore?.recentSlug ?? null;
409
- const recentIndex = registryEntries.findIndex((entry) => entry.slug === recentSlug);
410
- setHubPanelSelectedIndex(recentIndex >= 0 ? recentIndex : 0);
411
- setHubPanelMode("browse");
408
+ void (async () => {
409
+ const registryEntries = await listHubRegistryEntries();
410
+ setHubRegistryEntries(registryEntries);
411
+ const recentSlug = vendorHubStore?.recentSlug ?? null;
412
+ const recentIndex = registryEntries.findIndex((entry) => entry.slug === recentSlug);
413
+ setHubPanelSelectedIndex(recentIndex >= 0 ? recentIndex : 0);
414
+ setHubPanelMode("browse");
415
+ })();
412
416
  }, [activePanel, refreshTick, busyLabel, vendorHubStore?.recentSlug]);
413
417
  useEffect(() => {
414
418
  if (activePanel !== "connectors" || busyLabel)
@@ -1298,7 +1302,7 @@ function App() {
1298
1302
  if (activePanel === "hub") {
1299
1303
  if (navigationFocus === "sidebar")
1300
1304
  return;
1301
- const registryEntries = listHubRegistryEntries();
1305
+ const registryEntries = hubRegistryEntries;
1302
1306
  if (key.upArrow || input === "k") {
1303
1307
  setHubPanelSelectedIndex((value) => Math.max(0, value - 1));
1304
1308
  return;
@@ -1495,8 +1499,7 @@ function App() {
1495
1499
  mainContent = (_jsx(SpacePanelView, { companyName: vendorContextSummary.companyName ?? config.companyName, currentSpaceName: vendorContextSummary.spaceName ?? config.projectName, draftName: spacePanelDraftName, mode: spacePanelMode, onDraftChange: setSpacePanelDraftName, selectedIndex: spacePanelSelectedIndex, spaces: vendorSpaces }));
1496
1500
  }
1497
1501
  else if (activePanel === "hub") {
1498
- const registryEntries = listHubRegistryEntries();
1499
- mainContent = (_jsx(HubPanelView, { installedSlugs: vendorHubStore?.installedSlugs ?? [], recentSlug: vendorHubStore?.recentSlug ?? null, registryEntries: registryEntries, selectedIndex: hubPanelSelectedIndex }));
1502
+ mainContent = (_jsx(HubPanelView, { installedSlugs: vendorHubStore?.installedSlugs ?? [], recentSlug: vendorHubStore?.recentSlug ?? null, registryEntries: hubRegistryEntries, selectedIndex: hubPanelSelectedIndex }));
1500
1503
  }
1501
1504
  else if (activePanel === "connectors") {
1502
1505
  const registryEntries = listConnectorTemplates();
package/dist/config.d.ts CHANGED
@@ -24,6 +24,9 @@ export interface HubRegistry {
24
24
  id: string;
25
25
  name: string;
26
26
  url: string;
27
+ authScheme?: "none" | "bearer" | "token" | "basic" | "custom-header";
28
+ token?: string;
29
+ headerName?: string;
27
30
  updatedAt: string;
28
31
  }
29
32
  export interface StoredCliState {
package/dist/config.js CHANGED
@@ -120,6 +120,7 @@ export class ConfigManager {
120
120
  id: "umbrella-default",
121
121
  name: "Umbrella Hub",
122
122
  url: "https://hub.umbrella.local/default",
123
+ authScheme: "none",
123
124
  updatedAt: new Date(0).toISOString(),
124
125
  },
125
126
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "umbrella-context",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "description": "Umbrella Context CLI for connecting a device to company context spaces, querying saved context, and syncing MCP access.",
5
5
  "type": "module",
6
6
  "bin": {