umbrella-context 0.1.39 → 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/setup.js +1 -1
- 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 +18 -3
- 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
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { promises as fs } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import prompts from "prompts";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { configManager } from "../config.js";
|
|
7
|
+
import { addPendingMemory, buildLocalQueryFingerprint, ensureRepoContext, getPendingMemories, getPulledMemories, getRepoContext, listKnowledgeSources, summarizeLocalMemoryMatches, } from "../repo-state.js";
|
|
8
|
+
const SWARM_DIR = "swarm";
|
|
9
|
+
const SWARM_CONFIG_FILE = "config.json";
|
|
10
|
+
const SWARM_CACHE_FILE = "query-cache.json";
|
|
11
|
+
const DEFAULT_CACHE_TTL_MS = 15_000;
|
|
12
|
+
const DEFAULT_MAX_RESULTS = 8;
|
|
13
|
+
const OBSIDIAN_IGNORE_NAMES = new Set([".git", ".obsidian", ".trash", "templates"]);
|
|
14
|
+
const COMMON_STOP_WORDS = new Set([
|
|
15
|
+
"a",
|
|
16
|
+
"an",
|
|
17
|
+
"and",
|
|
18
|
+
"are",
|
|
19
|
+
"do",
|
|
20
|
+
"does",
|
|
21
|
+
"for",
|
|
22
|
+
"how",
|
|
23
|
+
"implemented",
|
|
24
|
+
"in",
|
|
25
|
+
"is",
|
|
26
|
+
"of",
|
|
27
|
+
"the",
|
|
28
|
+
"to",
|
|
29
|
+
"what",
|
|
30
|
+
"where",
|
|
31
|
+
"why",
|
|
32
|
+
]);
|
|
33
|
+
const SwarmFolderSchema = z.object({
|
|
34
|
+
followWikilinks: z.boolean().optional().default(true),
|
|
35
|
+
id: z.string(),
|
|
36
|
+
name: z.string(),
|
|
37
|
+
path: z.string(),
|
|
38
|
+
readOnly: z.boolean().optional().default(true),
|
|
39
|
+
});
|
|
40
|
+
const SwarmConfigSchema = z.object({
|
|
41
|
+
enrichment: z.object({
|
|
42
|
+
edges: z.array(z.object({ from: z.string(), to: z.string() })).default([]),
|
|
43
|
+
enabled: z.boolean().default(false),
|
|
44
|
+
}).default({ enabled: false, edges: [] }),
|
|
45
|
+
providers: z.object({
|
|
46
|
+
gbrain: z.object({
|
|
47
|
+
enabled: z.boolean().default(true),
|
|
48
|
+
notesPath: z.string(),
|
|
49
|
+
readOnly: z.boolean().optional().default(false),
|
|
50
|
+
}).optional(),
|
|
51
|
+
localMarkdown: z.object({
|
|
52
|
+
enabled: z.boolean().default(true),
|
|
53
|
+
folders: z.array(SwarmFolderSchema).default([]),
|
|
54
|
+
}).optional(),
|
|
55
|
+
memoryWiki: z.object({
|
|
56
|
+
enabled: z.boolean().default(true),
|
|
57
|
+
readOnly: z.boolean().optional().default(false),
|
|
58
|
+
wikiPath: z.string(),
|
|
59
|
+
}).optional(),
|
|
60
|
+
obsidian: z.object({
|
|
61
|
+
enabled: z.boolean().default(true),
|
|
62
|
+
readOnly: z.boolean().optional().default(true),
|
|
63
|
+
vaultPath: z.string(),
|
|
64
|
+
}).optional(),
|
|
65
|
+
umbrellaContext: z.object({
|
|
66
|
+
enabled: z.boolean().default(true),
|
|
67
|
+
}).default({ enabled: true }),
|
|
68
|
+
}),
|
|
69
|
+
routing: z.object({
|
|
70
|
+
cacheTtlMs: z.number().int().positive().default(DEFAULT_CACHE_TTL_MS),
|
|
71
|
+
defaultMaxResults: z.number().int().positive().default(DEFAULT_MAX_RESULTS),
|
|
72
|
+
}).default({ cacheTtlMs: DEFAULT_CACHE_TTL_MS, defaultMaxResults: DEFAULT_MAX_RESULTS }),
|
|
73
|
+
version: z.literal(1).default(1),
|
|
74
|
+
});
|
|
75
|
+
const SwarmCacheEntrySchema = z.object({
|
|
76
|
+
cacheKey: z.string(),
|
|
77
|
+
createdAt: z.string(),
|
|
78
|
+
query: z.string(),
|
|
79
|
+
result: z.any(),
|
|
80
|
+
});
|
|
81
|
+
function normalizeText(value) {
|
|
82
|
+
return value
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/[^\w\s]/g, " ")
|
|
85
|
+
.replace(/\s+/g, " ")
|
|
86
|
+
.trim();
|
|
87
|
+
}
|
|
88
|
+
function tokenize(value) {
|
|
89
|
+
return normalizeText(value)
|
|
90
|
+
.split(/\s+/)
|
|
91
|
+
.map((token) => token.trim())
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.filter((token) => !COMMON_STOP_WORDS.has(token));
|
|
94
|
+
}
|
|
95
|
+
function excerptAroundQuery(content, query, limit = 300) {
|
|
96
|
+
const normalizedContent = normalizeText(content);
|
|
97
|
+
const normalizedQuery = normalizeText(query);
|
|
98
|
+
const compact = content.replace(/\s+/g, " ").trim();
|
|
99
|
+
if (!normalizedQuery || compact.length <= limit) {
|
|
100
|
+
return compact.slice(0, limit);
|
|
101
|
+
}
|
|
102
|
+
const index = normalizedContent.indexOf(normalizedQuery);
|
|
103
|
+
if (index < 0)
|
|
104
|
+
return compact.slice(0, limit);
|
|
105
|
+
const start = Math.max(0, index - Math.floor(limit / 3));
|
|
106
|
+
const end = Math.min(compact.length, start + limit);
|
|
107
|
+
return compact.slice(start, end).trim();
|
|
108
|
+
}
|
|
109
|
+
function extractWikilinks(content) {
|
|
110
|
+
const links = [];
|
|
111
|
+
const regex = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
|
|
112
|
+
let match;
|
|
113
|
+
while ((match = regex.exec(content)) !== null) {
|
|
114
|
+
links.push(match[1].trim());
|
|
115
|
+
}
|
|
116
|
+
return links;
|
|
117
|
+
}
|
|
118
|
+
function slugifyFileName(input) {
|
|
119
|
+
return (input
|
|
120
|
+
.toLowerCase()
|
|
121
|
+
.replace(/[^\w-]+/g, "-")
|
|
122
|
+
.replace(/-+/g, "-")
|
|
123
|
+
.replace(/^-+|-+$/g, "")
|
|
124
|
+
.slice(0, 60) || `note-${Date.now()}`);
|
|
125
|
+
}
|
|
126
|
+
async function ensureDir(targetPath) {
|
|
127
|
+
await fs.mkdir(targetPath, { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
async function fileExists(targetPath) {
|
|
130
|
+
try {
|
|
131
|
+
await fs.access(targetPath);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function readJsonFile(targetPath, fallback) {
|
|
139
|
+
try {
|
|
140
|
+
const content = await fs.readFile(targetPath, "utf8");
|
|
141
|
+
return JSON.parse(content);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return fallback;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function writeJsonFile(targetPath, value) {
|
|
148
|
+
await fs.writeFile(targetPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
149
|
+
}
|
|
150
|
+
async function getSwarmDir(cwd = process.cwd()) {
|
|
151
|
+
const { repoRoot } = await getRepoContext(cwd);
|
|
152
|
+
const dir = path.join(repoRoot, ".um", SWARM_DIR);
|
|
153
|
+
await ensureDir(dir);
|
|
154
|
+
return dir;
|
|
155
|
+
}
|
|
156
|
+
export async function getSwarmConfigPath(cwd = process.cwd()) {
|
|
157
|
+
return path.join(await getSwarmDir(cwd), SWARM_CONFIG_FILE);
|
|
158
|
+
}
|
|
159
|
+
async function getSwarmCachePath(cwd = process.cwd()) {
|
|
160
|
+
return path.join(await getSwarmDir(cwd), SWARM_CACHE_FILE);
|
|
161
|
+
}
|
|
162
|
+
function buildDefaultSwarmConfig() {
|
|
163
|
+
return {
|
|
164
|
+
version: 1,
|
|
165
|
+
providers: {
|
|
166
|
+
umbrellaContext: { enabled: true },
|
|
167
|
+
},
|
|
168
|
+
enrichment: {
|
|
169
|
+
enabled: false,
|
|
170
|
+
edges: [],
|
|
171
|
+
},
|
|
172
|
+
routing: {
|
|
173
|
+
defaultMaxResults: DEFAULT_MAX_RESULTS,
|
|
174
|
+
cacheTtlMs: DEFAULT_CACHE_TTL_MS,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export async function hasSwarmConfig(cwd = process.cwd()) {
|
|
179
|
+
return fileExists(await getSwarmConfigPath(cwd));
|
|
180
|
+
}
|
|
181
|
+
export async function loadSwarmConfig(cwd = process.cwd()) {
|
|
182
|
+
const raw = await readJsonFile(await getSwarmConfigPath(cwd), null);
|
|
183
|
+
if (!raw)
|
|
184
|
+
return null;
|
|
185
|
+
return SwarmConfigSchema.parse(raw);
|
|
186
|
+
}
|
|
187
|
+
export async function saveSwarmConfig(config, cwd = process.cwd()) {
|
|
188
|
+
await writeJsonFile(await getSwarmConfigPath(cwd), SwarmConfigSchema.parse(config));
|
|
189
|
+
}
|
|
190
|
+
async function loadEffectiveSwarmConfig(cwd = process.cwd()) {
|
|
191
|
+
const saved = await loadSwarmConfig(cwd);
|
|
192
|
+
if (saved) {
|
|
193
|
+
return { config: saved, implicit: false };
|
|
194
|
+
}
|
|
195
|
+
const sources = await listKnowledgeSources(cwd);
|
|
196
|
+
if (sources.length > 0) {
|
|
197
|
+
return { config: buildDefaultSwarmConfig(), implicit: true };
|
|
198
|
+
}
|
|
199
|
+
return { config: null, implicit: false };
|
|
200
|
+
}
|
|
201
|
+
export async function ensureSwarmConfigForLinkedSources(cwd = process.cwd()) {
|
|
202
|
+
const existing = await loadSwarmConfig(cwd);
|
|
203
|
+
if (existing) {
|
|
204
|
+
return { configPath: await getSwarmConfigPath(cwd), created: false, ready: true };
|
|
205
|
+
}
|
|
206
|
+
const sources = await listKnowledgeSources(cwd);
|
|
207
|
+
if (sources.length === 0) {
|
|
208
|
+
return { configPath: await getSwarmConfigPath(cwd), created: false, ready: false };
|
|
209
|
+
}
|
|
210
|
+
const config = buildDefaultSwarmConfig();
|
|
211
|
+
await saveSwarmConfig(config, cwd);
|
|
212
|
+
return { config, configPath: await getSwarmConfigPath(cwd), created: true, ready: true };
|
|
213
|
+
}
|
|
214
|
+
async function readSwarmCache(cwd = process.cwd()) {
|
|
215
|
+
const raw = await readJsonFile(await getSwarmCachePath(cwd), []);
|
|
216
|
+
return raw
|
|
217
|
+
.map((entry) => SwarmCacheEntrySchema.safeParse(entry))
|
|
218
|
+
.filter((result) => result.success)
|
|
219
|
+
.map((result) => result.data);
|
|
220
|
+
}
|
|
221
|
+
async function writeSwarmCache(entries, cwd = process.cwd()) {
|
|
222
|
+
await writeJsonFile(await getSwarmCachePath(cwd), entries.slice(0, 20));
|
|
223
|
+
}
|
|
224
|
+
async function getCachedSwarmResult(cacheKey, ttlMs, cwd = process.cwd()) {
|
|
225
|
+
const now = Date.now();
|
|
226
|
+
const entries = await readSwarmCache(cwd);
|
|
227
|
+
const nextEntries = entries.filter((entry) => now - new Date(entry.createdAt).getTime() < ttlMs);
|
|
228
|
+
if (nextEntries.length !== entries.length) {
|
|
229
|
+
await writeSwarmCache(nextEntries, cwd);
|
|
230
|
+
}
|
|
231
|
+
return nextEntries.find((entry) => entry.cacheKey === cacheKey)?.result;
|
|
232
|
+
}
|
|
233
|
+
async function storeCachedSwarmResult(cacheKey, query, result, cwd = process.cwd()) {
|
|
234
|
+
const entries = await readSwarmCache(cwd);
|
|
235
|
+
const nextEntries = [
|
|
236
|
+
{
|
|
237
|
+
cacheKey,
|
|
238
|
+
createdAt: new Date().toISOString(),
|
|
239
|
+
query,
|
|
240
|
+
result,
|
|
241
|
+
},
|
|
242
|
+
...entries.filter((entry) => entry.cacheKey !== cacheKey),
|
|
243
|
+
];
|
|
244
|
+
await writeSwarmCache(nextEntries, cwd);
|
|
245
|
+
}
|
|
246
|
+
function buildSwarmCacheKey(query, config, localFingerprint) {
|
|
247
|
+
const configHash = createHash("sha1")
|
|
248
|
+
.update(JSON.stringify(config))
|
|
249
|
+
.digest("hex");
|
|
250
|
+
return createHash("sha1")
|
|
251
|
+
.update(`${query.trim().toLowerCase()}::${configHash}::${localFingerprint}`)
|
|
252
|
+
.digest("hex");
|
|
253
|
+
}
|
|
254
|
+
async function scanMarkdownFiles(rootPath, ignoreNames = new Set()) {
|
|
255
|
+
const documents = [];
|
|
256
|
+
async function walk(currentPath) {
|
|
257
|
+
let entries;
|
|
258
|
+
try {
|
|
259
|
+
entries = (await fs.readdir(currentPath, { encoding: "utf8", withFileTypes: true }));
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
for (const entry of entries) {
|
|
265
|
+
if (entry.name.startsWith(".") && !ignoreNames.has(entry.name)) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (ignoreNames.has(entry.name))
|
|
269
|
+
continue;
|
|
270
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
271
|
+
if (entry.isDirectory()) {
|
|
272
|
+
await walk(fullPath);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (!entry.name.toLowerCase().endsWith(".md"))
|
|
276
|
+
continue;
|
|
277
|
+
try {
|
|
278
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
279
|
+
const relativePath = path.relative(rootPath, fullPath);
|
|
280
|
+
const firstHeading = content.match(/^#\s+(.+)$/m)?.[1]?.trim();
|
|
281
|
+
documents.push({
|
|
282
|
+
content,
|
|
283
|
+
fullPath,
|
|
284
|
+
id: relativePath,
|
|
285
|
+
relativePath,
|
|
286
|
+
title: firstHeading || path.basename(fullPath, ".md"),
|
|
287
|
+
wikilinks: extractWikilinks(content),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// Skip unreadable markdown files.
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
await walk(rootPath);
|
|
296
|
+
return documents;
|
|
297
|
+
}
|
|
298
|
+
function scoreMarkdownDocument(query, document) {
|
|
299
|
+
const normalizedQuery = normalizeText(query);
|
|
300
|
+
const content = `${document.title}\n${document.relativePath}\n${document.content}`;
|
|
301
|
+
const normalizedContent = normalizeText(content);
|
|
302
|
+
const queryTokens = tokenize(query);
|
|
303
|
+
let score = 0;
|
|
304
|
+
let matched = 0;
|
|
305
|
+
if (normalizedQuery && normalizedContent.includes(normalizedQuery)) {
|
|
306
|
+
score += 6;
|
|
307
|
+
}
|
|
308
|
+
for (const token of queryTokens) {
|
|
309
|
+
if (normalizedContent.includes(token)) {
|
|
310
|
+
matched += 1;
|
|
311
|
+
score += 2;
|
|
312
|
+
if (normalizeText(document.title).includes(token)) {
|
|
313
|
+
score += 1;
|
|
314
|
+
}
|
|
315
|
+
if (normalizeText(document.relativePath).includes(token)) {
|
|
316
|
+
score += 0.5;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const coverage = queryTokens.length > 0 ? matched / queryTokens.length : 0;
|
|
321
|
+
score += coverage * 3;
|
|
322
|
+
return { coverage, score };
|
|
323
|
+
}
|
|
324
|
+
function dedupeSwarmHits(hits) {
|
|
325
|
+
const seen = new Set();
|
|
326
|
+
const result = [];
|
|
327
|
+
for (const hit of hits) {
|
|
328
|
+
const key = `${hit.provider}:${normalizeText(hit.content).slice(0, 180)}:${hit.source}`;
|
|
329
|
+
if (seen.has(key))
|
|
330
|
+
continue;
|
|
331
|
+
seen.add(key);
|
|
332
|
+
result.push(hit);
|
|
333
|
+
}
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
class MarkdownSwarmProvider {
|
|
337
|
+
type;
|
|
338
|
+
rootPath;
|
|
339
|
+
id;
|
|
340
|
+
label;
|
|
341
|
+
writable;
|
|
342
|
+
constructor(type, rootPath, label, options) {
|
|
343
|
+
this.type = type;
|
|
344
|
+
this.rootPath = rootPath;
|
|
345
|
+
this.id = options?.providerId ?? `${type}:${label.toLowerCase().replace(/\s+/g, "-")}`;
|
|
346
|
+
this.label = label;
|
|
347
|
+
this.writable = options?.writable ?? false;
|
|
348
|
+
this.ignoreNames = options?.ignoreNames ?? new Set();
|
|
349
|
+
this.followWikilinks = options?.followWikilinks ?? true;
|
|
350
|
+
}
|
|
351
|
+
followWikilinks;
|
|
352
|
+
ignoreNames;
|
|
353
|
+
async healthCheck() {
|
|
354
|
+
const exists = await fileExists(this.rootPath);
|
|
355
|
+
return {
|
|
356
|
+
available: exists,
|
|
357
|
+
detail: exists ? this.rootPath : "Path not found",
|
|
358
|
+
writable: this.writable,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
async query(query, opts) {
|
|
362
|
+
const documents = await scanMarkdownFiles(this.rootPath, this.ignoreNames);
|
|
363
|
+
const scored = documents
|
|
364
|
+
.map((document) => ({
|
|
365
|
+
document,
|
|
366
|
+
...scoreMarkdownDocument(query, document),
|
|
367
|
+
}))
|
|
368
|
+
.filter((entry) => entry.score > 0.9)
|
|
369
|
+
.sort((a, b) => b.score - a.score);
|
|
370
|
+
const direct = scored.slice(0, opts?.maxResults ?? DEFAULT_MAX_RESULTS);
|
|
371
|
+
const hitMap = new Map();
|
|
372
|
+
for (const entry of direct) {
|
|
373
|
+
hitMap.set(entry.document.id, {
|
|
374
|
+
content: excerptAroundQuery(entry.document.content, query),
|
|
375
|
+
id: `${this.id}:${entry.document.id}`,
|
|
376
|
+
matchType: "keyword",
|
|
377
|
+
provider: this.id,
|
|
378
|
+
providerLabel: this.label,
|
|
379
|
+
providerType: this.type,
|
|
380
|
+
score: Number(entry.score.toFixed(4)),
|
|
381
|
+
source: entry.document.relativePath,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (this.followWikilinks) {
|
|
385
|
+
const byRelativePath = new Map(documents.map((document) => [document.relativePath.toLowerCase(), document]));
|
|
386
|
+
for (const entry of direct) {
|
|
387
|
+
for (const link of entry.document.wikilinks) {
|
|
388
|
+
const candidates = [
|
|
389
|
+
`${link}.md`,
|
|
390
|
+
link,
|
|
391
|
+
...documents
|
|
392
|
+
.filter((document) => document.relativePath.toLowerCase().endsWith(`${link.toLowerCase()}.md`))
|
|
393
|
+
.map((document) => document.relativePath),
|
|
394
|
+
];
|
|
395
|
+
const linked = candidates
|
|
396
|
+
.map((candidate) => byRelativePath.get(candidate.toLowerCase()))
|
|
397
|
+
.find(Boolean);
|
|
398
|
+
if (!linked || hitMap.has(linked.id))
|
|
399
|
+
continue;
|
|
400
|
+
hitMap.set(linked.id, {
|
|
401
|
+
content: excerptAroundQuery(linked.content, query),
|
|
402
|
+
id: `${this.id}:${linked.id}`,
|
|
403
|
+
matchType: "graph",
|
|
404
|
+
provider: this.id,
|
|
405
|
+
providerLabel: this.label,
|
|
406
|
+
providerType: this.type,
|
|
407
|
+
score: Number((entry.score * 0.7).toFixed(4)),
|
|
408
|
+
source: linked.relativePath,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return [...hitMap.values()]
|
|
414
|
+
.sort((a, b) => b.score - a.score)
|
|
415
|
+
.slice(0, opts?.maxResults ?? DEFAULT_MAX_RESULTS);
|
|
416
|
+
}
|
|
417
|
+
async store(content) {
|
|
418
|
+
if (!this.writable) {
|
|
419
|
+
throw new Error(`${this.label} is read-only right now.`);
|
|
420
|
+
}
|
|
421
|
+
await ensureDir(this.rootPath);
|
|
422
|
+
const firstLine = content.split(/\r?\n/).find((line) => line.trim()) ?? "swarm-note";
|
|
423
|
+
const fileName = `${slugifyFileName(firstLine)}.md`;
|
|
424
|
+
let targetPath = path.join(this.rootPath, fileName);
|
|
425
|
+
let suffix = 1;
|
|
426
|
+
while (await fileExists(targetPath)) {
|
|
427
|
+
targetPath = path.join(this.rootPath, `${slugifyFileName(firstLine)}-${suffix}.md`);
|
|
428
|
+
suffix += 1;
|
|
429
|
+
}
|
|
430
|
+
await fs.writeFile(targetPath, `${content.trim()}\n`, "utf8");
|
|
431
|
+
return {
|
|
432
|
+
id: path.basename(targetPath),
|
|
433
|
+
success: true,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
class UmbrellaContextSwarmProvider {
|
|
438
|
+
id = "umbrella-context";
|
|
439
|
+
label = "Umbrella Context";
|
|
440
|
+
type = "umbrella-context";
|
|
441
|
+
writable = true;
|
|
442
|
+
async healthCheck() {
|
|
443
|
+
const config = configManager.config;
|
|
444
|
+
return {
|
|
445
|
+
available: Boolean(config),
|
|
446
|
+
detail: config
|
|
447
|
+
? `${config.companyName} / ${config.projectName} via ${config.serverUrl}`
|
|
448
|
+
: "Run umbrella-context setup first",
|
|
449
|
+
writable: true,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
async query(query, opts) {
|
|
453
|
+
const config = configManager.config;
|
|
454
|
+
if (!config)
|
|
455
|
+
return [];
|
|
456
|
+
await ensureRepoContext(config);
|
|
457
|
+
const localEntries = summarizeLocalMemoryMatches([...(await getPendingMemories()), ...(await getPulledMemories())], query);
|
|
458
|
+
const localHits = localEntries.map((entry, index) => ({
|
|
459
|
+
content: excerptAroundQuery(entry.content, query),
|
|
460
|
+
id: `local:${entry.id}:${index}`,
|
|
461
|
+
matchType: "keyword",
|
|
462
|
+
provider: this.id,
|
|
463
|
+
providerLabel: this.label,
|
|
464
|
+
providerType: this.type,
|
|
465
|
+
score: 0.86 - index * 0.03,
|
|
466
|
+
source: entry.source ?? ".um",
|
|
467
|
+
}));
|
|
468
|
+
let remoteHits = [];
|
|
469
|
+
try {
|
|
470
|
+
const response = await fetch(`${config.serverUrl}/api/memories/search?query=${encodeURIComponent(query)}`, {
|
|
471
|
+
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
472
|
+
});
|
|
473
|
+
if (response.ok) {
|
|
474
|
+
const data = await response.json();
|
|
475
|
+
remoteHits = (data.results ?? []).map((entry, index) => ({
|
|
476
|
+
content: excerptAroundQuery(String(entry.content ?? ""), query),
|
|
477
|
+
id: `remote:${String(entry.id ?? index)}`,
|
|
478
|
+
matchType: "keyword",
|
|
479
|
+
provider: this.id,
|
|
480
|
+
providerLabel: this.label,
|
|
481
|
+
providerType: this.type,
|
|
482
|
+
score: typeof entry.score === "number" ? entry.score : 0.8 - index * 0.03,
|
|
483
|
+
source: String(entry.systemType ?? "shared-memory"),
|
|
484
|
+
}));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
// Keep local results even if the server fetch fails.
|
|
489
|
+
}
|
|
490
|
+
return dedupeSwarmHits([...localHits, ...remoteHits]).slice(0, opts?.maxResults ?? DEFAULT_MAX_RESULTS);
|
|
491
|
+
}
|
|
492
|
+
async store(content) {
|
|
493
|
+
const entry = await addPendingMemory({
|
|
494
|
+
accessLevel: "space",
|
|
495
|
+
category: null,
|
|
496
|
+
content,
|
|
497
|
+
keywords: [],
|
|
498
|
+
source: "swarm",
|
|
499
|
+
systemType: "system1_knowledge",
|
|
500
|
+
tags: ["swarm"],
|
|
501
|
+
});
|
|
502
|
+
return { id: entry.id, success: true };
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
class UmbrellaSourceSwarmProvider {
|
|
506
|
+
alias;
|
|
507
|
+
sourceRoot;
|
|
508
|
+
id;
|
|
509
|
+
label;
|
|
510
|
+
type = "umbrella-source";
|
|
511
|
+
writable = false;
|
|
512
|
+
constructor(alias, sourceRoot) {
|
|
513
|
+
this.alias = alias;
|
|
514
|
+
this.sourceRoot = sourceRoot;
|
|
515
|
+
this.id = `source:${alias}`;
|
|
516
|
+
this.label = `Source: ${alias}`;
|
|
517
|
+
}
|
|
518
|
+
async healthCheck() {
|
|
519
|
+
const exists = await fileExists(path.join(this.sourceRoot, ".um", "context.json"));
|
|
520
|
+
return {
|
|
521
|
+
available: exists,
|
|
522
|
+
detail: exists ? this.sourceRoot : "Source repo is missing .um/context.json",
|
|
523
|
+
writable: false,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
async query(query, opts) {
|
|
527
|
+
const sourcePending = summarizeLocalMemoryMatches(await getPendingMemories(this.sourceRoot), query);
|
|
528
|
+
const sourcePulled = summarizeLocalMemoryMatches(await getPulledMemories(this.sourceRoot), query);
|
|
529
|
+
return dedupeSwarmHits([...sourcePending, ...sourcePulled].map((entry, index) => ({
|
|
530
|
+
content: excerptAroundQuery(entry.content, query),
|
|
531
|
+
id: `${this.id}:${entry.id}:${index}`,
|
|
532
|
+
matchType: "keyword",
|
|
533
|
+
provider: this.id,
|
|
534
|
+
providerLabel: this.label,
|
|
535
|
+
providerType: this.type,
|
|
536
|
+
score: 0.82 - index * 0.03,
|
|
537
|
+
source: `${this.alias}:${entry.source ?? ".um"}`,
|
|
538
|
+
}))).slice(0, opts?.maxResults ?? DEFAULT_MAX_RESULTS);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async function buildSwarmProviders(config) {
|
|
542
|
+
const providers = [];
|
|
543
|
+
providers.push(new UmbrellaContextSwarmProvider());
|
|
544
|
+
const sources = await listKnowledgeSources();
|
|
545
|
+
for (const source of sources) {
|
|
546
|
+
providers.push(new UmbrellaSourceSwarmProvider(source.alias, source.projectRoot));
|
|
547
|
+
}
|
|
548
|
+
if (config.providers.obsidian?.enabled) {
|
|
549
|
+
providers.push(new MarkdownSwarmProvider("obsidian", config.providers.obsidian.vaultPath, "Obsidian vault", {
|
|
550
|
+
ignoreNames: OBSIDIAN_IGNORE_NAMES,
|
|
551
|
+
providerId: "obsidian",
|
|
552
|
+
writable: !config.providers.obsidian.readOnly,
|
|
553
|
+
}));
|
|
554
|
+
}
|
|
555
|
+
if (config.providers.localMarkdown?.enabled) {
|
|
556
|
+
for (const folder of config.providers.localMarkdown.folders) {
|
|
557
|
+
providers.push(new MarkdownSwarmProvider("local-markdown", folder.path, folder.name, {
|
|
558
|
+
followWikilinks: folder.followWikilinks,
|
|
559
|
+
providerId: `local-markdown:${folder.id}`,
|
|
560
|
+
writable: !folder.readOnly,
|
|
561
|
+
}));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (config.providers.gbrain?.enabled) {
|
|
565
|
+
providers.push(new MarkdownSwarmProvider("gbrain", config.providers.gbrain.notesPath, "GBrain", {
|
|
566
|
+
providerId: "gbrain",
|
|
567
|
+
writable: !config.providers.gbrain.readOnly,
|
|
568
|
+
}));
|
|
569
|
+
}
|
|
570
|
+
if (config.providers.memoryWiki?.enabled) {
|
|
571
|
+
providers.push(new MarkdownSwarmProvider("memory-wiki", config.providers.memoryWiki.wikiPath, "Memory Wiki", {
|
|
572
|
+
providerId: "memory-wiki",
|
|
573
|
+
writable: !config.providers.memoryWiki.readOnly,
|
|
574
|
+
}));
|
|
575
|
+
}
|
|
576
|
+
return providers;
|
|
577
|
+
}
|
|
578
|
+
function chooseWriteProvider(content, providers, requestedProvider) {
|
|
579
|
+
if (requestedProvider) {
|
|
580
|
+
const exact = providers.find((provider) => provider.id === requestedProvider ||
|
|
581
|
+
provider.label.toLowerCase() === requestedProvider.toLowerCase());
|
|
582
|
+
if (!exact) {
|
|
583
|
+
throw new Error(`Swarm provider "${requestedProvider}" was not found.`);
|
|
584
|
+
}
|
|
585
|
+
if (!exact.writable) {
|
|
586
|
+
throw new Error(`Swarm provider "${exact.label}" is read-only.`);
|
|
587
|
+
}
|
|
588
|
+
return { fallback: false, provider: exact };
|
|
589
|
+
}
|
|
590
|
+
const prefersMarkdown = /[#*\-\[\]]/.test(content) || /\n/.test(content);
|
|
591
|
+
const writableExternal = providers.filter((provider) => provider.id !== "umbrella-context" && provider.writable);
|
|
592
|
+
if (prefersMarkdown && writableExternal.length > 0) {
|
|
593
|
+
return { fallback: false, provider: writableExternal[0] };
|
|
594
|
+
}
|
|
595
|
+
if (writableExternal.length > 0) {
|
|
596
|
+
return { fallback: false, provider: writableExternal[0] };
|
|
597
|
+
}
|
|
598
|
+
const umbrella = providers.find((provider) => provider.id === "umbrella-context");
|
|
599
|
+
if (!umbrella) {
|
|
600
|
+
throw new Error("Swarm has no writable provider available.");
|
|
601
|
+
}
|
|
602
|
+
return { fallback: true, provider: umbrella };
|
|
603
|
+
}
|
|
604
|
+
function sortSwarmHits(hits) {
|
|
605
|
+
return dedupeSwarmHits(hits).sort((a, b) => b.score - a.score).slice(0, 50);
|
|
606
|
+
}
|
|
607
|
+
function buildSwarmExplainLines(result) {
|
|
608
|
+
const lines = [];
|
|
609
|
+
for (const [providerId, meta] of Object.entries(result.meta.providers)) {
|
|
610
|
+
const selected = meta.selected ? "selected" : "skipped";
|
|
611
|
+
const enrichment = meta.enrichedBy ? `; enriched by ${meta.enrichedBy}` : "";
|
|
612
|
+
lines.push(`${providerId}: ${selected}; healthy=${meta.healthy ? "yes" : "no"}; results=${meta.resultCount}; latency=${meta.latencyMs}ms${enrichment}`);
|
|
613
|
+
}
|
|
614
|
+
return lines;
|
|
615
|
+
}
|
|
616
|
+
export function formatSwarmQueryText(result, options) {
|
|
617
|
+
const lines = [
|
|
618
|
+
`Swarm query: "${result.query}"`,
|
|
619
|
+
`Providers queried: ${result.meta.providerCount} | Latency: ${result.meta.totalLatencyMs}ms${result.cached ? " | cached" : ""}`,
|
|
620
|
+
"",
|
|
621
|
+
];
|
|
622
|
+
if (options?.explain) {
|
|
623
|
+
lines.push("Explain:");
|
|
624
|
+
lines.push(...buildSwarmExplainLines(result).map((line) => ` - ${line}`));
|
|
625
|
+
if (result.meta.enrichmentEdges.length > 0) {
|
|
626
|
+
lines.push(" - enrichment: " + result.meta.enrichmentEdges.map((edge) => `${edge.from} -> ${edge.to}`).join(", "));
|
|
627
|
+
}
|
|
628
|
+
lines.push("");
|
|
629
|
+
}
|
|
630
|
+
if (result.results.length === 0) {
|
|
631
|
+
lines.push("No swarm results found.");
|
|
632
|
+
lines.push('Try `umbrella-context swarm status` to verify which knowledge sources are healthy.');
|
|
633
|
+
return lines.join("\n");
|
|
634
|
+
}
|
|
635
|
+
result.results.forEach((hit, index) => {
|
|
636
|
+
lines.push(`${index + 1}. [${hit.providerLabel}] ${hit.source} (${hit.matchType}, score ${hit.score.toFixed(3)})`);
|
|
637
|
+
lines.push(` ${hit.content}`);
|
|
638
|
+
lines.push("");
|
|
639
|
+
});
|
|
640
|
+
return lines.join("\n").trimEnd();
|
|
641
|
+
}
|
|
642
|
+
export async function getSwarmStatusSummary(cwd = process.cwd()) {
|
|
643
|
+
const configPath = await getSwarmConfigPath(cwd);
|
|
644
|
+
const { config } = await loadEffectiveSwarmConfig(cwd);
|
|
645
|
+
if (!config) {
|
|
646
|
+
return {
|
|
647
|
+
configPath,
|
|
648
|
+
configured: false,
|
|
649
|
+
healthyProviders: 0,
|
|
650
|
+
providerCount: 0,
|
|
651
|
+
writableProviders: 0,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
const providers = await buildSwarmProviders(config);
|
|
655
|
+
const checks = await Promise.all(providers.map((provider) => provider.healthCheck()));
|
|
656
|
+
return {
|
|
657
|
+
configPath,
|
|
658
|
+
configured: true,
|
|
659
|
+
healthyProviders: checks.filter((check) => check.available).length,
|
|
660
|
+
providerCount: providers.length,
|
|
661
|
+
writableProviders: checks.filter((check) => check.available && check.writable).length,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
export async function getSwarmStatus(cwd = process.cwd()) {
|
|
665
|
+
const { config, implicit } = await loadEffectiveSwarmConfig(cwd);
|
|
666
|
+
if (!config) {
|
|
667
|
+
return {
|
|
668
|
+
configured: false,
|
|
669
|
+
configPath: await getSwarmConfigPath(cwd),
|
|
670
|
+
enrichmentEdges: [],
|
|
671
|
+
providers: [],
|
|
672
|
+
suggestions: [
|
|
673
|
+
'Run `umbrella-context swarm onboard` to choose which knowledge tools this repo should search together.',
|
|
674
|
+
],
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
const providers = await buildSwarmProviders(config);
|
|
678
|
+
const providerStatuses = await Promise.all(providers.map(async (provider) => {
|
|
679
|
+
const health = await provider.healthCheck();
|
|
680
|
+
return {
|
|
681
|
+
id: provider.id,
|
|
682
|
+
label: provider.label,
|
|
683
|
+
type: provider.type,
|
|
684
|
+
available: health.available,
|
|
685
|
+
detail: health.detail,
|
|
686
|
+
writable: health.writable,
|
|
687
|
+
};
|
|
688
|
+
}));
|
|
689
|
+
const suggestions = [];
|
|
690
|
+
if (!providerStatuses.some((provider) => provider.id !== "umbrella-context" && provider.available)) {
|
|
691
|
+
suggestions.push("Add at least one extra note provider so swarm can search beyond this repo's saved Context.");
|
|
692
|
+
}
|
|
693
|
+
if (implicit) {
|
|
694
|
+
suggestions.push("Swarm is using a basic auto-created config because this repo already has linked knowledge sources. Run `umbrella-context swarm onboard` if you want to add Obsidian, markdown folders, or enrichment rules.");
|
|
695
|
+
}
|
|
696
|
+
if (providerStatuses.some((provider) => !provider.available)) {
|
|
697
|
+
suggestions.push("One or more configured paths do not exist yet. Update the path or re-run swarm onboard.");
|
|
698
|
+
}
|
|
699
|
+
return {
|
|
700
|
+
configured: true,
|
|
701
|
+
configPath: await getSwarmConfigPath(cwd),
|
|
702
|
+
enrichmentEdges: config.enrichment.edges,
|
|
703
|
+
providers: providerStatuses,
|
|
704
|
+
suggestions,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
export async function executeSwarmQuery(query, opts = {}, cwd = process.cwd()) {
|
|
708
|
+
const { config } = await loadEffectiveSwarmConfig(cwd);
|
|
709
|
+
if (!config) {
|
|
710
|
+
throw new Error('Swarm is not configured yet. Run "umbrella-context swarm onboard" first.');
|
|
711
|
+
}
|
|
712
|
+
const providers = await buildSwarmProviders(config);
|
|
713
|
+
const selectedProviders = opts.provider
|
|
714
|
+
? providers.filter((provider) => provider.id === opts.provider ||
|
|
715
|
+
provider.label.toLowerCase() === opts.provider?.toLowerCase())
|
|
716
|
+
: providers;
|
|
717
|
+
if (selectedProviders.length === 0) {
|
|
718
|
+
throw new Error(`No swarm provider matched "${opts.provider}".`);
|
|
719
|
+
}
|
|
720
|
+
const maxResults = opts.maxResults ?? config.routing.defaultMaxResults;
|
|
721
|
+
const localFingerprint = await buildLocalQueryFingerprint(cwd);
|
|
722
|
+
const cacheKey = buildSwarmCacheKey(query, config, localFingerprint);
|
|
723
|
+
const cached = await getCachedSwarmResult(cacheKey, config.routing.cacheTtlMs, cwd);
|
|
724
|
+
if (cached) {
|
|
725
|
+
return { ...cached, cached: true };
|
|
726
|
+
}
|
|
727
|
+
const startedAt = Date.now();
|
|
728
|
+
const meta = {};
|
|
729
|
+
const hits = [];
|
|
730
|
+
const providersInOrder = selectedProviders.slice().sort((left, right) => {
|
|
731
|
+
if (left.id === "umbrella-context")
|
|
732
|
+
return -1;
|
|
733
|
+
if (right.id === "umbrella-context")
|
|
734
|
+
return 1;
|
|
735
|
+
return left.label.localeCompare(right.label);
|
|
736
|
+
});
|
|
737
|
+
const enrichmentEdges = config.enrichment.enabled ? config.enrichment.edges : [];
|
|
738
|
+
const topUmbrellaHints = [];
|
|
739
|
+
for (const provider of providersInOrder) {
|
|
740
|
+
const health = await provider.healthCheck();
|
|
741
|
+
if (!health.available) {
|
|
742
|
+
meta[provider.id] = {
|
|
743
|
+
available: false,
|
|
744
|
+
detail: health.detail,
|
|
745
|
+
healthy: false,
|
|
746
|
+
latencyMs: 0,
|
|
747
|
+
resultCount: 0,
|
|
748
|
+
selected: false,
|
|
749
|
+
writable: health.writable,
|
|
750
|
+
};
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
const providerStartedAt = Date.now();
|
|
754
|
+
const incomingEdge = enrichmentEdges.find((edge) => edge.to === provider.id);
|
|
755
|
+
const queryText = incomingEdge && topUmbrellaHints.length > 0
|
|
756
|
+
? `${query}\n\nContext hints:\n${topUmbrellaHints.join("\n")}`
|
|
757
|
+
: query;
|
|
758
|
+
const providerHits = await provider.query(queryText, { maxResults });
|
|
759
|
+
const latencyMs = Date.now() - providerStartedAt;
|
|
760
|
+
if (provider.id === "umbrella-context") {
|
|
761
|
+
topUmbrellaHints.push(...providerHits
|
|
762
|
+
.slice(0, 3)
|
|
763
|
+
.map((hit) => hit.content)
|
|
764
|
+
.filter(Boolean));
|
|
765
|
+
}
|
|
766
|
+
hits.push(...providerHits);
|
|
767
|
+
meta[provider.id] = {
|
|
768
|
+
available: true,
|
|
769
|
+
detail: health.detail,
|
|
770
|
+
enrichedBy: incomingEdge?.from,
|
|
771
|
+
enrichmentHints: incomingEdge && topUmbrellaHints.length > 0 ? topUmbrellaHints.slice(0, 3) : undefined,
|
|
772
|
+
healthy: true,
|
|
773
|
+
latencyMs,
|
|
774
|
+
resultCount: providerHits.length,
|
|
775
|
+
selected: true,
|
|
776
|
+
writable: health.writable,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
const result = {
|
|
780
|
+
cached: false,
|
|
781
|
+
cacheTtlMs: config.routing.cacheTtlMs,
|
|
782
|
+
meta: {
|
|
783
|
+
enrichmentEdges,
|
|
784
|
+
providerCount: Object.values(meta).filter((entry) => entry.selected).length,
|
|
785
|
+
providers: meta,
|
|
786
|
+
totalLatencyMs: Date.now() - startedAt,
|
|
787
|
+
},
|
|
788
|
+
query,
|
|
789
|
+
results: sortSwarmHits(hits).slice(0, maxResults),
|
|
790
|
+
};
|
|
791
|
+
await storeCachedSwarmResult(cacheKey, query, result, cwd);
|
|
792
|
+
return result;
|
|
793
|
+
}
|
|
794
|
+
export async function executeSwarmCurate(content, opts = {}, cwd = process.cwd()) {
|
|
795
|
+
const { config } = await loadEffectiveSwarmConfig(cwd);
|
|
796
|
+
if (!config) {
|
|
797
|
+
throw new Error('Swarm is not configured yet. Run "umbrella-context swarm onboard" first.');
|
|
798
|
+
}
|
|
799
|
+
const providers = await buildSwarmProviders(config);
|
|
800
|
+
const { fallback, provider } = chooseWriteProvider(content, providers, opts.provider);
|
|
801
|
+
const result = await provider.store?.(content);
|
|
802
|
+
if (!result?.success) {
|
|
803
|
+
throw new Error(`Swarm could not store content in ${provider.label}.`);
|
|
804
|
+
}
|
|
805
|
+
return {
|
|
806
|
+
fallback,
|
|
807
|
+
id: result.id,
|
|
808
|
+
provider: provider.label,
|
|
809
|
+
success: true,
|
|
810
|
+
targetType: provider.type,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
async function promptForPath(message) {
|
|
814
|
+
const answer = await prompts({
|
|
815
|
+
type: "text",
|
|
816
|
+
name: "value",
|
|
817
|
+
message,
|
|
818
|
+
validate: (value) => (value.trim() ? true : "Please type a path."),
|
|
819
|
+
});
|
|
820
|
+
return String(answer.value ?? "").trim();
|
|
821
|
+
}
|
|
822
|
+
export async function runSwarmOnboardWizard(cwd = process.cwd()) {
|
|
823
|
+
const defaults = buildDefaultSwarmConfig();
|
|
824
|
+
const selected = await prompts({
|
|
825
|
+
type: "multiselect",
|
|
826
|
+
name: "providers",
|
|
827
|
+
message: "Which extra knowledge tools should this repo's swarm search?",
|
|
828
|
+
instructions: false,
|
|
829
|
+
choices: [
|
|
830
|
+
{
|
|
831
|
+
title: "Umbrella Context",
|
|
832
|
+
description: "Always on. Searches this repo's saved Context and shared server memory.",
|
|
833
|
+
disabled: true,
|
|
834
|
+
selected: true,
|
|
835
|
+
value: "umbrella-context",
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
title: "Obsidian vault",
|
|
839
|
+
description: "Search a local Obsidian vault as read-only notes.",
|
|
840
|
+
value: "obsidian",
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
title: "Local markdown folder",
|
|
844
|
+
description: "Search one or more plain markdown folders on this computer.",
|
|
845
|
+
value: "local-markdown",
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
title: "GBrain-compatible notes folder",
|
|
849
|
+
description: "Treat a GBrain notes directory as another searchable markdown source.",
|
|
850
|
+
value: "gbrain",
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
title: "Memory Wiki folder",
|
|
854
|
+
description: "Search and optionally write to a Memory Wiki-style markdown folder.",
|
|
855
|
+
value: "memory-wiki",
|
|
856
|
+
},
|
|
857
|
+
],
|
|
858
|
+
});
|
|
859
|
+
const selectedProviders = new Set(selected.providers ?? []);
|
|
860
|
+
const nextConfig = {
|
|
861
|
+
...defaults,
|
|
862
|
+
enrichment: { enabled: false, edges: [] },
|
|
863
|
+
providers: { ...defaults.providers },
|
|
864
|
+
};
|
|
865
|
+
if (selectedProviders.has("obsidian")) {
|
|
866
|
+
const vaultPath = await promptForPath("Path to your Obsidian vault");
|
|
867
|
+
nextConfig.providers.obsidian = {
|
|
868
|
+
enabled: true,
|
|
869
|
+
readOnly: true,
|
|
870
|
+
vaultPath,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
if (selectedProviders.has("local-markdown")) {
|
|
874
|
+
const folders = [];
|
|
875
|
+
let addAnother = true;
|
|
876
|
+
while (addAnother) {
|
|
877
|
+
const folderPath = await promptForPath("Path to a markdown folder");
|
|
878
|
+
const folderNameAnswer = await prompts({
|
|
879
|
+
type: "text",
|
|
880
|
+
name: "value",
|
|
881
|
+
message: "Short name for this folder inside swarm",
|
|
882
|
+
initial: path.basename(folderPath),
|
|
883
|
+
});
|
|
884
|
+
const readOnlyAnswer = await prompts({
|
|
885
|
+
type: "confirm",
|
|
886
|
+
name: "value",
|
|
887
|
+
message: "Should this folder be read-only?",
|
|
888
|
+
initial: true,
|
|
889
|
+
});
|
|
890
|
+
folders.push({
|
|
891
|
+
followWikilinks: true,
|
|
892
|
+
id: slugifyFileName(folderNameAnswer.value || path.basename(folderPath)),
|
|
893
|
+
name: String(folderNameAnswer.value || path.basename(folderPath)),
|
|
894
|
+
path: folderPath,
|
|
895
|
+
readOnly: Boolean(readOnlyAnswer.value),
|
|
896
|
+
});
|
|
897
|
+
const anotherAnswer = await prompts({
|
|
898
|
+
type: "confirm",
|
|
899
|
+
name: "value",
|
|
900
|
+
message: "Add another markdown folder?",
|
|
901
|
+
initial: false,
|
|
902
|
+
});
|
|
903
|
+
addAnother = Boolean(anotherAnswer.value);
|
|
904
|
+
}
|
|
905
|
+
nextConfig.providers.localMarkdown = {
|
|
906
|
+
enabled: folders.length > 0,
|
|
907
|
+
folders,
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
if (selectedProviders.has("gbrain")) {
|
|
911
|
+
const notesPath = await promptForPath("Path to the GBrain notes or markdown folder");
|
|
912
|
+
nextConfig.providers.gbrain = {
|
|
913
|
+
enabled: true,
|
|
914
|
+
notesPath,
|
|
915
|
+
readOnly: false,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
if (selectedProviders.has("memory-wiki")) {
|
|
919
|
+
const wikiPath = await promptForPath("Path to the Memory Wiki folder");
|
|
920
|
+
const readOnlyAnswer = await prompts({
|
|
921
|
+
type: "confirm",
|
|
922
|
+
name: "value",
|
|
923
|
+
message: "Should Memory Wiki stay read-only?",
|
|
924
|
+
initial: false,
|
|
925
|
+
});
|
|
926
|
+
nextConfig.providers.memoryWiki = {
|
|
927
|
+
enabled: true,
|
|
928
|
+
readOnly: Boolean(readOnlyAnswer.value),
|
|
929
|
+
wikiPath,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
const enrichmentAnswer = await prompts({
|
|
933
|
+
type: "confirm",
|
|
934
|
+
name: "value",
|
|
935
|
+
message: "Should Umbrella Context feed its top matches into the other providers first?",
|
|
936
|
+
initial: true,
|
|
937
|
+
});
|
|
938
|
+
if (enrichmentAnswer.value) {
|
|
939
|
+
nextConfig.enrichment = {
|
|
940
|
+
enabled: true,
|
|
941
|
+
edges: [
|
|
942
|
+
...(nextConfig.providers.obsidian ? [{ from: "umbrella-context", to: "obsidian" }] : []),
|
|
943
|
+
...(nextConfig.providers.localMarkdown?.folders ?? []).map((folder) => ({
|
|
944
|
+
from: "umbrella-context",
|
|
945
|
+
to: `local-markdown:${folder.id}`,
|
|
946
|
+
})),
|
|
947
|
+
...(nextConfig.providers.gbrain ? [{ from: "umbrella-context", to: "gbrain" }] : []),
|
|
948
|
+
...(nextConfig.providers.memoryWiki ? [{ from: "umbrella-context", to: "memory-wiki" }] : []),
|
|
949
|
+
],
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
await saveSwarmConfig(nextConfig, cwd);
|
|
953
|
+
return {
|
|
954
|
+
config: nextConfig,
|
|
955
|
+
configPath: await getSwarmConfigPath(cwd),
|
|
956
|
+
};
|
|
957
|
+
}
|