umbrella-context 0.1.37 → 0.1.39

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.
@@ -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") {
@@ -46,7 +46,7 @@ function printWelcomeHeader(providerLabel, modelLabel) {
46
46
  console.log(` ${chalk.yellow("/reset")} Reset this repo's local Context state`);
47
47
  console.log(` ${chalk.yellow("/")} Full command list`);
48
48
  console.log(divider());
49
- console.log(` ${chalk.gray("Shortcuts:")} type plain text to query, or start with ${chalk.yellow("+")} to curate quickly.`);
49
+ console.log(` ${chalk.gray("Shortcuts:")} start with ${chalk.yellow("?")} to query, or start with ${chalk.yellow("+")} to curate quickly.`);
50
50
  console.log("");
51
51
  }
52
52
  async function printRuntimeSnapshot() {
@@ -794,7 +794,7 @@ export async function interactiveCommand(_args) {
794
794
  await setSessionPanel("home", configManager.config.projectName);
795
795
  await printRuntimeSnapshot();
796
796
  await printHomeView(configManager.config);
797
- console.log(chalk.gray(" Type / to see commands. Use plain text to query or start with + to curate.\n"));
797
+ console.log(chalk.gray(" Type / to see commands. Use ? to query or + to curate.\n"));
798
798
  while (true) {
799
799
  const currentConfig = configManager.config;
800
800
  if (!currentConfig) {
@@ -837,21 +837,32 @@ export async function interactiveCommand(_args) {
837
837
  }
838
838
  continue;
839
839
  }
840
- const { command, args } = parseInteractiveInput(raw);
841
- const spec = resolveCommand(command);
842
- if (!raw.startsWith("/") && !spec) {
843
- commandHistory.push(`/query ${raw}`);
844
- await recordSessionCommand(`/query ${raw}`);
845
- await recordSessionQuery(raw);
846
- await setSessionSummary(`Queried Context: ${raw.length > 48 ? `${raw.slice(0, 48)}...` : raw}`);
840
+ if (raw.startsWith("?")) {
841
+ const query = raw.slice(1).trim();
842
+ if (!query) {
843
+ console.log(chalk.yellow("Use ? followed by a real question, for example ? what do we already know about auth timeouts"));
844
+ continue;
845
+ }
846
+ commandHistory.push(`/query ${query}`);
847
+ await recordSessionCommand(`/query ${query}`);
848
+ await recordSessionQuery(query);
849
+ await setSessionSummary(`Queried Context: ${query.length > 48 ? `${query.slice(0, 48)}...` : query}`);
847
850
  try {
848
- await searchCommandAction(raw);
851
+ await searchCommandAction(query);
849
852
  }
850
853
  catch (err) {
851
854
  console.log(chalk.red(`Command failed: ${err.message}`));
852
855
  }
853
856
  continue;
854
857
  }
858
+ const { command, args } = parseInteractiveInput(raw);
859
+ const spec = resolveCommand(command);
860
+ if (!raw.startsWith("/") && !spec) {
861
+ console.log(chalk.yellow("That text was not run automatically."));
862
+ console.log(chalk.gray("Use /query <question> or start with ? to search Context."));
863
+ console.log(chalk.gray("Use /curate <note> or start with + to save something locally."));
864
+ continue;
865
+ }
855
866
  if (!spec) {
856
867
  console.log(chalk.yellow(`Unknown command: ${raw}. Type / to see available commands.`));
857
868
  continue;
@@ -1,2 +1,7 @@
1
- export declare function locationsCommandAction(): Promise<void>;
1
+ type OutputFormat = "text" | "json";
2
+ export declare function locationsCommandAction(opts?: {
3
+ format?: OutputFormat;
4
+ detail?: boolean;
5
+ }): Promise<void>;
2
6
  export declare function locationsCommand(cli: any): void;
7
+ export {};
@@ -1,20 +1,106 @@
1
1
  import chalk from "chalk";
2
+ import { promises as fs } from "fs";
3
+ import path from "path";
2
4
  import { configManager } from "../config.js";
3
- export async function locationsCommandAction() {
4
- const locations = configManager.locations;
5
+ import { findRepoRoot, getRepoContext } from "../repo-state.js";
6
+ async function getCurrentRepoRoot() {
7
+ try {
8
+ const { repoRoot } = await getRepoContext();
9
+ return repoRoot;
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ async function summarizeSavedLocations() {
16
+ const currentRepoRoot = await getCurrentRepoRoot();
17
+ const storedLocations = configManager.locations;
18
+ const normalizedLocations = await Promise.all(storedLocations.map(async (location) => {
19
+ try {
20
+ const canonicalRepoRoot = await findRepoRoot(location.repoRoot);
21
+ return canonicalRepoRoot === location.repoRoot ? location : { ...location, repoRoot: canonicalRepoRoot };
22
+ }
23
+ catch {
24
+ return location;
25
+ }
26
+ }));
27
+ const dedupedLocations = Array.from(new Map(normalizedLocations.map((location) => [
28
+ location.repoRoot,
29
+ location,
30
+ ])).values());
31
+ const locationChanged = dedupedLocations.length !== storedLocations.length
32
+ || dedupedLocations.some((location, index) => {
33
+ const original = storedLocations[index];
34
+ return !original || original.repoRoot !== location.repoRoot;
35
+ });
36
+ if (locationChanged) {
37
+ configManager.replaceLocations(dedupedLocations);
38
+ }
39
+ const snapshots = await Promise.all(dedupedLocations.map(async (location) => {
40
+ const umDir = path.join(location.repoRoot, ".um");
41
+ const contextFile = path.join(umDir, "context.json");
42
+ const [umDirStat, contextStat] = await Promise.allSettled([fs.stat(umDir), fs.stat(contextFile)]);
43
+ const hasUmDir = umDirStat.status === "fulfilled" && umDirStat.value.isDirectory();
44
+ const hasContextFile = contextStat.status === "fulfilled" && contextStat.value.isFile();
45
+ return {
46
+ ...location,
47
+ isCurrentRepo: currentRepoRoot !== null && currentRepoRoot === location.repoRoot,
48
+ hasUmDir,
49
+ hasContextFile,
50
+ isLinked: hasUmDir && hasContextFile,
51
+ };
52
+ }));
53
+ return snapshots.sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
54
+ }
55
+ function printLocationsJson(locations) {
56
+ console.log(JSON.stringify({
57
+ ok: true,
58
+ action: "locations",
59
+ count: locations.length,
60
+ locations,
61
+ }, null, 2));
62
+ }
63
+ export async function locationsCommandAction(opts = {}) {
64
+ const locations = await summarizeSavedLocations();
5
65
  if (locations.length === 0) {
66
+ if (opts.format === "json") {
67
+ printLocationsJson([]);
68
+ return;
69
+ }
6
70
  console.log(chalk.yellow("\n No repo locations saved yet."));
7
71
  return;
8
72
  }
73
+ if (opts.format === "json") {
74
+ printLocationsJson(locations);
75
+ return;
76
+ }
9
77
  console.log(chalk.bold("\n Saved Context Locations\n"));
10
78
  locations.forEach((location, index) => {
11
- console.log(chalk.cyan(` ${index + 1}. ${location.companyName} / ${location.projectName}`));
79
+ const badges = [];
80
+ if (location.isCurrentRepo)
81
+ badges.push(chalk.green("CURRENT"));
82
+ if (location.isLinked)
83
+ badges.push(chalk.cyan("LINKED"));
84
+ else
85
+ badges.push(chalk.yellow("SAVED ONLY"));
86
+ console.log(chalk.cyan(` ${index + 1}. ${location.companyName} / ${location.projectName} ${badges.join(" ")}`));
12
87
  console.log(chalk.gray(` ${location.repoRoot}`));
13
88
  console.log(chalk.gray(` Updated: ${new Date(location.updatedAt).toLocaleString()}`));
89
+ if (opts.detail) {
90
+ console.log(chalk.gray(` .um folder: ${location.hasUmDir ? "Yes" : "No"}`));
91
+ console.log(chalk.gray(` .um/context.json: ${location.hasContextFile ? "Yes" : "No"}`));
92
+ }
14
93
  });
15
94
  }
16
95
  export function locationsCommand(cli) {
17
- cli.command("locations", "List repo folders that have been connected on this machine").action(async () => {
18
- await locationsCommandAction();
96
+ cli
97
+ .command("locations", "List repo folders that have been connected on this machine")
98
+ .option("--format <format>", "Output format (text or json)")
99
+ .option("--detail", "Show whether each saved repo still has a linked .um folder and context file")
100
+ .example("locations")
101
+ .example("locations --detail")
102
+ .example("locations --format json")
103
+ .action(async (opts) => {
104
+ await locationsCommandAction(opts);
19
105
  });
20
106
  }
@@ -22,11 +22,16 @@ export function mcpCommand(cli) {
22
22
  const data = await res.json();
23
23
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
24
24
  });
25
- server.tool("memory_push", "Save a memory or note to the current context space", { content: z.string().describe("The memory content"), tags: z.array(z.string()).optional() }, async ({ content, tags }) => {
25
+ server.tool("memory_push", "Save a memory or note to the current context space", {
26
+ content: z.string().describe("The memory content"),
27
+ tags: z.array(z.string()).optional(),
28
+ category: z.enum(["project", "team", "convention", "environment", "preference", "incident", "other"]).optional(),
29
+ accessLevel: z.enum(["space", "company"]).optional(),
30
+ }, async ({ content, tags, category, accessLevel }) => {
26
31
  const res = await fetch(`${config.serverUrl}/api/memories`, {
27
32
  method: "POST",
28
33
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.apiKey}` },
29
- body: JSON.stringify({ content, tags: tags || [], source: "mcp" }),
34
+ body: JSON.stringify({ content, tags: tags || [], category, accessLevel, source: "mcp" }),
30
35
  });
31
36
  const data = await res.json();
32
37
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
@@ -1,2 +1,7 @@
1
- export declare function pullCommandAction(): Promise<void>;
1
+ type OutputFormat = "text" | "json";
2
+ export declare function pullCommandAction(opts?: {
3
+ format?: OutputFormat;
4
+ dryRun?: boolean;
5
+ }): Promise<void>;
2
6
  export declare function pullCommand(cli: any): void;
7
+ export {};