umbrella-context 0.1.40 → 0.1.41

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 CHANGED
@@ -52,6 +52,16 @@ The setup flow will:
52
52
  ```bash
53
53
  umbrella-context query "What do we already know?"
54
54
  umbrella-context curate "We learned that..."
55
+ umbrella-context swarm onboard
56
+ umbrella-context swarm status
57
+ umbrella-context swarm query "How are auth cookies refreshed?"
58
+ umbrella-context swarm curate "JWT refresh notes"
59
+ umbrella-context worktree add ../other-checkout
60
+ umbrella-context worktree list
61
+ umbrella-context source add ../shared-lib --alias shared-lib
62
+ umbrella-context source list
63
+ umbrella-context adaptive refresh
64
+ umbrella-context adaptive status
55
65
  umbrella-context push
56
66
  umbrella-context pull
57
67
  umbrella-context fix "ETIMEDOUT"
@@ -61,3 +71,73 @@ umbrella-context
61
71
 
62
72
  The old `agent-memory` command still works as a compatibility alias.
63
73
  The short `um` command works too.
74
+
75
+ ## Swarm
76
+
77
+ Swarm lets one repo search more than just its own saved Context.
78
+
79
+ Think of it like this:
80
+ - `umbrella-context query` searches Umbrella Context
81
+ - `umbrella-context swarm query` searches Umbrella Context plus any extra note folders you connected
82
+
83
+ Typical flow:
84
+
85
+ ```bash
86
+ umbrella-context swarm onboard
87
+ umbrella-context swarm status
88
+ umbrella-context swarm query "How do auth cookies refresh?"
89
+ umbrella-context swarm curate "New JWT refresh notes"
90
+ ```
91
+
92
+ Right now this version supports:
93
+ - Umbrella Context
94
+ - read-only Umbrella repo sources linked with `source add`
95
+ - Obsidian vaults
96
+ - local markdown folders
97
+ - GBrain-style markdown folders
98
+ - Memory Wiki folders
99
+
100
+ ## Worktrees and Sources
101
+
102
+ Use `worktree` when one Umbrella repo has more than one checkout or sub-worktree:
103
+
104
+ ```bash
105
+ umbrella-context worktree add ../other-checkout
106
+ umbrella-context worktree list
107
+ umbrella-context worktree remove ../other-checkout
108
+ ```
109
+
110
+ Use `source` when you want this repo to read another Umbrella repo as a read-only knowledge source:
111
+
112
+ ```bash
113
+ umbrella-context source add ../shared-lib --alias shared-lib
114
+ umbrella-context source list
115
+ umbrella-context source remove shared-lib
116
+ ```
117
+
118
+ Those sources automatically show up inside swarm queries.
119
+
120
+ ## Adaptive Knowledge
121
+
122
+ Adaptive knowledge keeps lightweight `.abstract.md` and `.overview.md` files inside `.um/adaptive/`.
123
+ It also tracks which saved notes get reused the most, so frequently helpful notes can rise over time.
124
+
125
+ ```bash
126
+ umbrella-context adaptive refresh
127
+ umbrella-context adaptive status
128
+ umbrella-context query "How is authentication implemented?" --timeout 900
129
+ ```
130
+
131
+ `--timeout` lets longer provider-backed queries wait longer before failing.
132
+
133
+ ## Claude Desktop Connector
134
+
135
+ Claude Desktop is now available as a first-class connector:
136
+
137
+ ```bash
138
+ umbrella-context connectors install claude-desktop-mcp
139
+ umbrella-context connectors run claude-desktop-mcp
140
+ ```
141
+
142
+ On Windows and macOS, this writes Claude Desktop's MCP config so Claude can talk to `umbrella-context mcp`.
143
+ After install, fully quit Claude Desktop from the tray or menu bar, then reopen it.
@@ -157,38 +157,50 @@ async function buildSpaceSnapshot(cwd = process.cwd()) {
157
157
  const repoContext = await getRepoContext(cwd);
158
158
  const config = configManager.config;
159
159
  if (!config?.umbrellaUrl) {
160
+ return buildFallbackSpaceSnapshot(null, repoContext.state ?? null);
161
+ }
162
+ try {
163
+ const summary = await getCompanyContextSummary(config.umbrellaUrl, config.companyId);
164
+ const spaces = toContextSpaces(summary);
160
165
  return {
161
- activeSpaceId: repoContext.state?.projectId ?? null,
162
- activeSpaceName: repoContext.state?.projectName ?? null,
163
- companyId: repoContext.state?.companyId ?? null,
164
- companyName: repoContext.state?.companyName ?? null,
166
+ activeSpaceId: config.projectId,
167
+ activeSpaceName: config.projectName,
168
+ companyId: config.companyId,
169
+ companyName: config.companyName,
165
170
  isLoading: false,
166
- spaces: repoContext.state
167
- ? [
168
- {
169
- id: repoContext.state.projectId,
170
- isPrimary: true,
171
- isSelected: true,
172
- name: repoContext.state.projectName,
173
- },
174
- ]
175
- : [],
171
+ spaces: spaces.map((space) => ({
172
+ id: space.id,
173
+ isPrimary: space.isPrimary,
174
+ isSelected: space.id === config.projectId,
175
+ name: space.name,
176
+ })),
176
177
  };
177
178
  }
178
- const summary = await getCompanyContextSummary(config.umbrellaUrl, config.companyId);
179
- const spaces = toContextSpaces(summary);
179
+ catch {
180
+ return buildFallbackSpaceSnapshot(config, repoContext.state ?? null);
181
+ }
182
+ }
183
+ function buildFallbackSpaceSnapshot(config, repoState) {
184
+ const companyId = config?.companyId ?? repoState?.companyId ?? null;
185
+ const companyName = config?.companyName ?? repoState?.companyName ?? null;
186
+ const projectId = config?.projectId ?? repoState?.projectId ?? null;
187
+ const projectName = config?.projectName ?? repoState?.projectName ?? null;
180
188
  return {
181
- activeSpaceId: config.projectId,
182
- activeSpaceName: config.projectName,
183
- companyId: config.companyId,
184
- companyName: config.companyName,
189
+ activeSpaceId: projectId,
190
+ activeSpaceName: projectName,
191
+ companyId,
192
+ companyName,
185
193
  isLoading: false,
186
- spaces: spaces.map((space) => ({
187
- id: space.id,
188
- isPrimary: space.isPrimary,
189
- isSelected: space.id === config.projectId,
190
- name: space.name,
191
- })),
194
+ spaces: projectId && projectName
195
+ ? [
196
+ {
197
+ id: projectId,
198
+ isPrimary: true,
199
+ isSelected: true,
200
+ name: projectName,
201
+ },
202
+ ]
203
+ : [],
192
204
  };
193
205
  }
194
206
  async function buildHubSnapshot(cwd = process.cwd()) {
@@ -217,8 +229,8 @@ async function buildConnectorsSnapshot(cwd = process.cwd()) {
217
229
  };
218
230
  }
219
231
  export async function buildVendorRuntimeBridgeSnapshot(cwd = process.cwd()) {
220
- const [repoContext, pending, pulled, fixes, hubEntries, connectors, connectorRuns, localTasks, transport, contextTree, providerStore, modelStore, spaceStore, hubStore, connectorsStore,] = await Promise.all([
221
- getRepoContext(cwd),
232
+ const baseRepoContext = await getRepoContext(cwd);
233
+ const [pending, pulled, fixes, hubEntries, connectors, connectorRuns, localTasks, transport, contextTree, providerStore, modelStore, spaceStore, hubStore, connectorsStore,] = await Promise.all([
222
234
  getPendingMemories(cwd),
223
235
  getPulledMemories(cwd),
224
236
  getPulledFixes(cwd),
@@ -230,7 +242,7 @@ export async function buildVendorRuntimeBridgeSnapshot(cwd = process.cwd()) {
230
242
  getContextTreeState(cwd),
231
243
  buildProviderSnapshot(),
232
244
  buildModelSnapshot(),
233
- buildSpaceSnapshot(cwd),
245
+ buildSpaceSnapshot(cwd).catch(() => buildFallbackSpaceSnapshot(configManager.config, baseRepoContext.state ?? null)),
234
246
  buildHubSnapshot(cwd),
235
247
  buildConnectorsSnapshot(cwd),
236
248
  ]);
@@ -243,16 +255,16 @@ export async function buildVendorRuntimeBridgeSnapshot(cwd = process.cwd()) {
243
255
  };
244
256
  return {
245
257
  company: {
246
- id: repoContext.state?.companyId ?? null,
247
- name: repoContext.state?.companyName ?? null,
258
+ id: baseRepoContext.state?.companyId ?? null,
259
+ name: baseRepoContext.state?.companyName ?? null,
248
260
  },
249
261
  generatedAt: new Date().toISOString(),
250
262
  repo: {
251
263
  pendingDrafts: pending.length,
252
264
  pulledContext: pulled.length,
253
265
  pulledFixes: fixes.length,
254
- repoRoot: repoContext.repoRoot,
255
- umDir: repoContext.umDir,
266
+ repoRoot: baseRepoContext.repoRoot,
267
+ umDir: baseRepoContext.umDir,
256
268
  },
257
269
  services: {
258
270
  activeModel: configManager.config?.activeModel ?? null,
@@ -262,8 +274,8 @@ export async function buildVendorRuntimeBridgeSnapshot(cwd = process.cwd()) {
262
274
  hubEntriesInstalled: hubEntries.length,
263
275
  },
264
276
  space: {
265
- id: repoContext.state?.projectId ?? null,
266
- name: repoContext.state?.projectName ?? null,
277
+ id: baseRepoContext.state?.projectId ?? null,
278
+ name: baseRepoContext.state?.projectName ?? null,
267
279
  },
268
280
  stores: {
269
281
  connectors: connectorsStore,
@@ -1,14 +1,21 @@
1
1
  import { type SavedProvider } from "../config.js";
2
- export declare const UMBRELLA_PROVIDER_CHOICES: Array<{
2
+ export type ProviderChoice = {
3
3
  kind: string;
4
4
  name: string;
5
- }>;
5
+ description: string;
6
+ suggestedLabel: string;
7
+ baseUrl?: string;
8
+ requiresBaseUrlInput?: boolean;
9
+ exampleModels: string[];
10
+ };
11
+ export declare const UMBRELLA_PROVIDER_CHOICES: ProviderChoice[];
6
12
  export type ProviderConnectDraft = {
7
13
  apiKey: string;
8
14
  baseUrl: string;
9
15
  kind: string | null;
10
16
  label: string;
11
17
  };
18
+ export declare function getProviderChoice(kind?: string | null): ProviderChoice | null;
12
19
  export declare function createEmptyProviderConnectDraft(): ProviderConnectDraft;
13
20
  export declare function resolveSavedProvider(target?: string | null): SavedProvider | null;
14
21
  export declare function getActiveSavedProvider(): SavedProvider | null;
@@ -2,12 +2,90 @@ import { randomUUID } from "crypto";
2
2
  import { configManager } from "../config.js";
3
3
  import { getSessionState, recordSessionEvent, recordSessionProvider, setSessionPanel } from "../repo-state.js";
4
4
  export const UMBRELLA_PROVIDER_CHOICES = [
5
- { kind: "anthropic", name: "Anthropic" },
6
- { kind: "openai", name: "OpenAI" },
7
- { kind: "google", name: "Google" },
8
- { kind: "openrouter", name: "OpenRouter" },
9
- { kind: "openai-compatible", name: "OpenAI-Compatible" },
5
+ {
6
+ kind: "openai",
7
+ name: "OpenAI / Codex",
8
+ description: "Best when you want OpenAI models like GPT-5 and Codex-style coding workflows.",
9
+ suggestedLabel: "OpenAI",
10
+ exampleModels: ["gpt-5.4", "gpt-5.4-mini", "gpt-5"],
11
+ },
12
+ {
13
+ kind: "anthropic",
14
+ name: "Anthropic",
15
+ description: "Claude models for strong reasoning and coding.",
16
+ suggestedLabel: "Anthropic",
17
+ exampleModels: ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"],
18
+ },
19
+ {
20
+ kind: "google",
21
+ name: "Google Gemini",
22
+ description: "Gemini models through Google's API.",
23
+ suggestedLabel: "Google Gemini",
24
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai/",
25
+ exampleModels: ["gemini-2.5-pro", "gemini-2.5-flash"],
26
+ },
27
+ {
28
+ kind: "openrouter",
29
+ name: "OpenRouter",
30
+ description: "One provider that can route to many model families.",
31
+ suggestedLabel: "OpenRouter",
32
+ baseUrl: "https://openrouter.ai/api/v1",
33
+ exampleModels: ["openai/gpt-5", "anthropic/claude-sonnet-4-6", "google/gemini-2.5-pro"],
34
+ },
35
+ {
36
+ kind: "minimax",
37
+ name: "MiniMax",
38
+ description: "MiniMax coding and chat models through its OpenAI-compatible endpoint.",
39
+ suggestedLabel: "MiniMax",
40
+ baseUrl: "https://api.minimax.io/v1",
41
+ exampleModels: ["MiniMax-M2.7", "MiniMax-M2.5", "custom-model"],
42
+ },
43
+ {
44
+ kind: "groq",
45
+ name: "Groq",
46
+ description: "Fast OpenAI-compatible inference for supported models.",
47
+ suggestedLabel: "Groq",
48
+ baseUrl: "https://api.groq.com/openai/v1",
49
+ exampleModels: ["custom-model"],
50
+ },
51
+ {
52
+ kind: "together",
53
+ name: "Together AI",
54
+ description: "OpenAI-compatible access to many open coding models.",
55
+ suggestedLabel: "Together AI",
56
+ baseUrl: "https://api.together.xyz/v1",
57
+ exampleModels: ["custom-model"],
58
+ },
59
+ {
60
+ kind: "deepseek",
61
+ name: "DeepSeek",
62
+ description: "DeepSeek reasoning and chat models through its compatible endpoint.",
63
+ suggestedLabel: "DeepSeek",
64
+ baseUrl: "https://api.deepseek.com",
65
+ exampleModels: ["deepseek-chat", "deepseek-reasoner", "custom-model"],
66
+ },
67
+ {
68
+ kind: "fireworks",
69
+ name: "Fireworks AI",
70
+ description: "OpenAI-compatible hosted inference for many model families.",
71
+ suggestedLabel: "Fireworks AI",
72
+ baseUrl: "https://api.fireworks.ai/inference/v1",
73
+ exampleModels: ["custom-model"],
74
+ },
75
+ {
76
+ kind: "openai-compatible",
77
+ name: "Custom OpenAI-Compatible",
78
+ description: "Use this when your provider gives you an OpenAI-compatible base URL that is not listed here.",
79
+ suggestedLabel: "Custom Compatible",
80
+ requiresBaseUrlInput: true,
81
+ exampleModels: ["custom-model"],
82
+ },
10
83
  ];
84
+ export function getProviderChoice(kind) {
85
+ if (!kind)
86
+ return null;
87
+ return UMBRELLA_PROVIDER_CHOICES.find((entry) => entry.kind === kind) ?? null;
88
+ }
11
89
  export function createEmptyProviderConnectDraft() {
12
90
  return {
13
91
  apiKey: "",
@@ -60,18 +138,19 @@ export async function connectSavedProviderFromDraft(draft) {
60
138
  const nextKey = draft.apiKey.trim();
61
139
  const nextKind = draft.kind ?? "openai";
62
140
  const nextBaseUrl = draft.baseUrl.trim();
141
+ const choice = getProviderChoice(nextKind);
63
142
  if (!nextLabel) {
64
143
  throw new Error("Type a provider label first.");
65
144
  }
66
145
  if (!nextKey) {
67
146
  throw new Error("Paste an API key first.");
68
147
  }
69
- if (nextKind === "openai-compatible" && !nextBaseUrl) {
148
+ if ((choice?.requiresBaseUrlInput || nextKind === "openai-compatible") && !nextBaseUrl) {
70
149
  throw new Error("Type the compatible base URL first.");
71
150
  }
72
151
  const provider = {
73
152
  apiKey: nextKey,
74
- baseUrl: nextKind === "openai-compatible" ? nextBaseUrl : undefined,
153
+ baseUrl: nextBaseUrl || choice?.baseUrl || undefined,
75
154
  id: randomUUID(),
76
155
  kind: nextKind,
77
156
  name: nextLabel,
@@ -0,0 +1,27 @@
1
+ export type AdaptiveKnowledgeEntry = {
2
+ abstractPath: string;
3
+ consultationCount: number;
4
+ hotness: number;
5
+ id: string;
6
+ lastConsultedAt: string | null;
7
+ overviewPath: string;
8
+ sourceLabel: string;
9
+ title: string;
10
+ updatedAt: string;
11
+ };
12
+ export type AdaptiveKnowledgeState = {
13
+ entries: Record<string, AdaptiveKnowledgeEntry>;
14
+ generatedAt: string;
15
+ sessionPatternsPath?: string | null;
16
+ version: 1;
17
+ };
18
+ export declare function loadAdaptiveKnowledgeState(cwd?: string): Promise<AdaptiveKnowledgeState>;
19
+ export declare function refreshAdaptiveKnowledge(cwd?: string): Promise<AdaptiveKnowledgeState>;
20
+ export declare function recordAdaptiveConsultations(memoryIds: string[], cwd?: string): Promise<AdaptiveKnowledgeState>;
21
+ export declare function getAdaptiveKnowledgeBoost(memoryId: string, cwd?: string): Promise<number>;
22
+ export declare function getAdaptiveKnowledgeSummary(cwd?: string): Promise<{
23
+ generatedAt: string;
24
+ entryCount: number;
25
+ hottestEntries: AdaptiveKnowledgeEntry[];
26
+ sessionPatternsPath: string | null;
27
+ }>;
@@ -0,0 +1,154 @@
1
+ import { promises as fs } from "fs";
2
+ import path from "path";
3
+ import { getPendingMemories, getPulledMemories, getRepoContext, getSessionState, } from "../repo-state.js";
4
+ const ADAPTIVE_DIR = "adaptive";
5
+ const ADAPTIVE_STATE_FILE = "knowledge.json";
6
+ function sanitizeFileName(value) {
7
+ return (value
8
+ .toLowerCase()
9
+ .replace(/[^\w-]+/g, "-")
10
+ .replace(/-+/g, "-")
11
+ .replace(/^-+|-+$/g, "")
12
+ .slice(0, 48) || "note");
13
+ }
14
+ function pickTitle(entry) {
15
+ const firstLine = entry.content.split(/\r?\n/).find((line) => line.trim().length > 0)?.trim() ?? "Untitled note";
16
+ return firstLine.length > 80 ? `${firstLine.slice(0, 77).trim()}...` : firstLine;
17
+ }
18
+ function buildAbstract(entry) {
19
+ const compact = entry.content.replace(/\s+/g, " ").trim();
20
+ return compact.length > 220 ? `${compact.slice(0, 217).trim()}...` : compact;
21
+ }
22
+ function buildOverview(entry) {
23
+ const lines = [
24
+ `# ${pickTitle(entry)}`,
25
+ "",
26
+ `- Source: ${entry.source}`,
27
+ `- System type: ${entry.systemType}`,
28
+ `- Access: ${entry.accessLevel}`,
29
+ `- Category: ${entry.category ?? "uncategorized"}`,
30
+ `- Tags: ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}`,
31
+ `- Keywords: ${entry.keywords.length > 0 ? entry.keywords.join(", ") : "none"}`,
32
+ `- Created: ${entry.createdAt}`,
33
+ "",
34
+ "## Abstract",
35
+ "",
36
+ buildAbstract(entry),
37
+ "",
38
+ ];
39
+ return lines.join("\n");
40
+ }
41
+ async function ensureAdaptiveDir(cwd = process.cwd()) {
42
+ const { repoRoot } = await getRepoContext(cwd);
43
+ const dir = path.join(repoRoot, ".um", ADAPTIVE_DIR);
44
+ await fs.mkdir(dir, { recursive: true });
45
+ return dir;
46
+ }
47
+ async function getAdaptiveStatePath(cwd = process.cwd()) {
48
+ return path.join(await ensureAdaptiveDir(cwd), ADAPTIVE_STATE_FILE);
49
+ }
50
+ async function readJsonFile(filePath, fallback) {
51
+ try {
52
+ return JSON.parse(await fs.readFile(filePath, "utf8"));
53
+ }
54
+ catch {
55
+ return fallback;
56
+ }
57
+ }
58
+ async function writeJsonFile(filePath, value) {
59
+ await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
60
+ }
61
+ export async function loadAdaptiveKnowledgeState(cwd = process.cwd()) {
62
+ const raw = await readJsonFile(await getAdaptiveStatePath(cwd), null);
63
+ return raw ?? {
64
+ version: 1,
65
+ generatedAt: new Date(0).toISOString(),
66
+ entries: {},
67
+ sessionPatternsPath: null,
68
+ };
69
+ }
70
+ export async function refreshAdaptiveKnowledge(cwd = process.cwd()) {
71
+ const adaptiveDir = await ensureAdaptiveDir(cwd);
72
+ const state = await loadAdaptiveKnowledgeState(cwd);
73
+ const session = await getSessionState(cwd);
74
+ const entries = [...(await getPendingMemories(cwd)), ...(await getPulledMemories(cwd))];
75
+ const nextEntries = {};
76
+ for (const entry of entries) {
77
+ const title = pickTitle(entry);
78
+ const safeBase = `${sanitizeFileName(title)}-${entry.id.slice(0, 8)}`;
79
+ const abstractPath = path.join(adaptiveDir, `${safeBase}.abstract.md`);
80
+ const overviewPath = path.join(adaptiveDir, `${safeBase}.overview.md`);
81
+ await fs.writeFile(abstractPath, `${buildAbstract(entry)}\n`, "utf8");
82
+ await fs.writeFile(overviewPath, buildOverview(entry), "utf8");
83
+ const previous = state.entries[entry.id];
84
+ nextEntries[entry.id] = {
85
+ id: entry.id,
86
+ title,
87
+ sourceLabel: entry.source,
88
+ abstractPath,
89
+ overviewPath,
90
+ consultationCount: previous?.consultationCount ?? 0,
91
+ hotness: previous?.hotness ?? 0,
92
+ lastConsultedAt: previous?.lastConsultedAt ?? null,
93
+ updatedAt: new Date().toISOString(),
94
+ };
95
+ }
96
+ const sessionPatternsPath = path.join(adaptiveDir, "session-patterns.md");
97
+ const recentQueries = session?.recentQueries.slice(0, 5) ?? [];
98
+ const recentCurations = session?.recentCurations.slice(0, 5) ?? [];
99
+ const patternLines = [
100
+ "# Session Patterns",
101
+ "",
102
+ "## Recent questions",
103
+ "",
104
+ ...(recentQueries.length > 0 ? recentQueries.map((query, index) => `${index + 1}. ${query}`) : ["No recent questions yet."]),
105
+ "",
106
+ "## Recent curations",
107
+ "",
108
+ ...(recentCurations.length > 0
109
+ ? recentCurations.map((entry, index) => `${index + 1}. ${entry.replace(/\s+/g, " ").trim()}`)
110
+ : ["No recent curations yet."]),
111
+ "",
112
+ ];
113
+ await fs.writeFile(sessionPatternsPath, `${patternLines.join("\n")}\n`, "utf8");
114
+ const nextState = {
115
+ version: 1,
116
+ generatedAt: new Date().toISOString(),
117
+ entries: nextEntries,
118
+ sessionPatternsPath,
119
+ };
120
+ await writeJsonFile(await getAdaptiveStatePath(cwd), nextState);
121
+ return nextState;
122
+ }
123
+ export async function recordAdaptiveConsultations(memoryIds, cwd = process.cwd()) {
124
+ if (memoryIds.length === 0)
125
+ return loadAdaptiveKnowledgeState(cwd);
126
+ const uniqueIds = [...new Set(memoryIds)];
127
+ const state = await refreshAdaptiveKnowledge(cwd);
128
+ const now = new Date().toISOString();
129
+ for (const id of uniqueIds) {
130
+ const entry = state.entries[id];
131
+ if (!entry)
132
+ continue;
133
+ entry.consultationCount += 1;
134
+ entry.hotness = Number((entry.hotness * 0.82 + 1).toFixed(4));
135
+ entry.lastConsultedAt = now;
136
+ entry.updatedAt = now;
137
+ }
138
+ await writeJsonFile(await getAdaptiveStatePath(cwd), state);
139
+ return state;
140
+ }
141
+ export async function getAdaptiveKnowledgeBoost(memoryId, cwd = process.cwd()) {
142
+ const state = await loadAdaptiveKnowledgeState(cwd);
143
+ return state.entries[memoryId]?.hotness ?? 0;
144
+ }
145
+ export async function getAdaptiveKnowledgeSummary(cwd = process.cwd()) {
146
+ const state = await loadAdaptiveKnowledgeState(cwd);
147
+ const entries = Object.values(state.entries).sort((left, right) => right.hotness - left.hotness);
148
+ return {
149
+ generatedAt: state.generatedAt,
150
+ entryCount: entries.length,
151
+ hottestEntries: entries.slice(0, 3),
152
+ sessionPatternsPath: state.sessionPatternsPath ?? null,
153
+ };
154
+ }
@@ -0,0 +1,7 @@
1
+ export declare function adaptiveStatusCommandAction(opts?: {
2
+ format?: string;
3
+ }): Promise<void>;
4
+ export declare function adaptiveRefreshCommandAction(opts?: {
5
+ format?: string;
6
+ }): Promise<void>;
7
+ export declare function adaptiveCommand(cli: any): void;
@@ -0,0 +1,92 @@
1
+ import chalk from "chalk";
2
+ import { getAdaptiveKnowledgeSummary, refreshAdaptiveKnowledge } from "../adaptive/runtime.js";
3
+ import { recordSessionEvent, setSessionPanel } from "../repo-state.js";
4
+ function asOutputFormat(value) {
5
+ return value === "json" ? "json" : "text";
6
+ }
7
+ export async function adaptiveStatusCommandAction(opts = {}) {
8
+ const format = asOutputFormat(opts.format);
9
+ try {
10
+ await setSessionPanel("session", "adaptive-status");
11
+ const summary = await getAdaptiveKnowledgeSummary();
12
+ if (format === "json") {
13
+ console.log(JSON.stringify({ ok: true, action: "adaptive.status", ...summary }, null, 2));
14
+ return;
15
+ }
16
+ console.log(chalk.bold("\n Adaptive Knowledge\n"));
17
+ console.log(` Entries with abstracts: ${summary.entryCount}`);
18
+ console.log(` Last refresh: ${summary.generatedAt}`);
19
+ console.log(` Session patterns file: ${summary.sessionPatternsPath ?? "Not generated yet"}`);
20
+ console.log("");
21
+ if (summary.hottestEntries.length === 0) {
22
+ console.log(chalk.yellow("No adaptive entries have been generated yet. Run 'umbrella-context adaptive refresh' first."));
23
+ return;
24
+ }
25
+ console.log(chalk.bold(" Hottest notes"));
26
+ summary.hottestEntries.forEach((entry, index) => {
27
+ console.log(` - ${index + 1}. ${entry.title} (hotness ${entry.hotness.toFixed(2)}, consultations ${entry.consultationCount})`);
28
+ });
29
+ }
30
+ catch (error) {
31
+ const message = error instanceof Error ? error.message : String(error);
32
+ if (format === "json") {
33
+ console.log(JSON.stringify({ ok: false, action: "adaptive.status", error: message }, null, 2));
34
+ return;
35
+ }
36
+ console.log(chalk.red(`\n ${message}`));
37
+ }
38
+ }
39
+ export async function adaptiveRefreshCommandAction(opts = {}) {
40
+ const format = asOutputFormat(opts.format);
41
+ try {
42
+ await setSessionPanel("session", "adaptive-refresh");
43
+ const state = await refreshAdaptiveKnowledge();
44
+ await recordSessionEvent({
45
+ kind: "session",
46
+ title: "Refreshed adaptive knowledge",
47
+ detail: `Generated adaptive artifacts for ${Object.keys(state.entries).length} note${Object.keys(state.entries).length === 1 ? "" : "s"}.`,
48
+ panel: "session",
49
+ focus: "adaptive-refresh",
50
+ status: "success",
51
+ });
52
+ if (format === "json") {
53
+ console.log(JSON.stringify({
54
+ ok: true,
55
+ action: "adaptive.refresh",
56
+ entryCount: Object.keys(state.entries).length,
57
+ generatedAt: state.generatedAt,
58
+ sessionPatternsPath: state.sessionPatternsPath ?? null,
59
+ }, null, 2));
60
+ return;
61
+ }
62
+ console.log(chalk.green(`\n Refreshed adaptive knowledge for ${Object.keys(state.entries).length} note${Object.keys(state.entries).length === 1 ? "" : "s"}.`));
63
+ console.log(chalk.gray(` Session patterns: ${state.sessionPatternsPath ?? "none"}`));
64
+ }
65
+ catch (error) {
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ if (format === "json") {
68
+ console.log(JSON.stringify({ ok: false, action: "adaptive.refresh", error: message }, null, 2));
69
+ return;
70
+ }
71
+ console.log(chalk.red(`\n ${message}`));
72
+ }
73
+ }
74
+ export function adaptiveCommand(cli) {
75
+ cli
76
+ .command("adaptive <action>", "Inspect and refresh adaptive knowledge artifacts for this repo")
77
+ .example("adaptive status")
78
+ .example("adaptive refresh")
79
+ .option("--format <format>", "Output format (text or json)")
80
+ .action(async (action, opts) => {
81
+ const normalized = action.trim().toLowerCase();
82
+ if (normalized === "status") {
83
+ await adaptiveStatusCommandAction(opts);
84
+ return;
85
+ }
86
+ if (normalized === "refresh") {
87
+ await adaptiveRefreshCommandAction(opts);
88
+ return;
89
+ }
90
+ console.log(chalk.red("Use one of: adaptive status, adaptive refresh"));
91
+ });
92
+ }