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 +80 -0
- package/dist/adapters/byterover-runtime-bridge.js +47 -35
- package/dist/adapters/umbrella-provider-runtime.d.ts +9 -2
- package/dist/adapters/umbrella-provider-runtime.js +86 -7
- package/dist/adaptive/runtime.d.ts +27 -0
- package/dist/adaptive/runtime.js +154 -0
- package/dist/commands/adaptive.d.ts +7 -0
- package/dist/commands/adaptive.js +92 -0
- package/dist/commands/connectors.js +96 -1
- package/dist/commands/curate.js +2 -0
- package/dist/commands/logout.js +1 -0
- package/dist/commands/model.js +30 -5
- package/dist/commands/providers.js +9 -3
- package/dist/commands/search.d.ts +1 -0
- package/dist/commands/search.js +51 -12
- package/dist/commands/source.d.ts +11 -0
- package/dist/commands/source.js +152 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +17 -2
- package/dist/commands/swarm.d.ts +16 -0
- package/dist/commands/swarm.js +211 -0
- package/dist/commands/tui.js +223 -52
- package/dist/commands/worktree.d.ts +12 -0
- package/dist/commands/worktree.js +141 -0
- package/dist/index.js +8 -0
- package/dist/repo-state.d.ts +71 -0
- package/dist/repo-state.js +260 -7
- package/dist/swarm/runtime.d.ts +502 -0
- package/dist/swarm/runtime.js +957 -0
- package/package.json +1 -1
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:
|
|
162
|
-
activeSpaceName:
|
|
163
|
-
companyId:
|
|
164
|
-
companyName:
|
|
166
|
+
activeSpaceId: config.projectId,
|
|
167
|
+
activeSpaceName: config.projectName,
|
|
168
|
+
companyId: config.companyId,
|
|
169
|
+
companyName: config.companyName,
|
|
165
170
|
isLoading: false,
|
|
166
|
-
spaces:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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:
|
|
182
|
-
activeSpaceName:
|
|
183
|
-
companyId
|
|
184
|
-
companyName
|
|
189
|
+
activeSpaceId: projectId,
|
|
190
|
+
activeSpaceName: projectName,
|
|
191
|
+
companyId,
|
|
192
|
+
companyName,
|
|
185
193
|
isLoading: false,
|
|
186
|
-
spaces:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
221
|
-
|
|
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:
|
|
247
|
-
name:
|
|
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:
|
|
255
|
-
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:
|
|
266
|
-
name:
|
|
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
|
|
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
|
-
{
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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:
|
|
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,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
|
+
}
|