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.
@@ -0,0 +1,141 @@
1
+ import chalk from "chalk";
2
+ import { addRegisteredWorktree, getRepoResolution, listRegisteredWorktrees, removeRegisteredWorktree, setSessionPanel, recordSessionEvent, } from "../repo-state.js";
3
+ function asOutputFormat(value) {
4
+ return value === "json" ? "json" : "text";
5
+ }
6
+ function normalizeCliPathInput(value) {
7
+ const trimmed = value.trim();
8
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
9
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
10
+ return trimmed.slice(1, -1).trim();
11
+ }
12
+ return trimmed;
13
+ }
14
+ export async function worktreeAddCommandAction(targetPath, opts = {}) {
15
+ const format = asOutputFormat(opts.format);
16
+ try {
17
+ await setSessionPanel("session", "worktree-add");
18
+ const result = await addRegisteredWorktree(normalizeCliPathInput(targetPath ?? process.cwd()), {
19
+ force: Boolean(opts.force),
20
+ name: opts.name,
21
+ });
22
+ await recordSessionEvent({
23
+ kind: "session",
24
+ title: "Registered a worktree",
25
+ detail: result.message,
26
+ panel: "session",
27
+ focus: "worktree-add",
28
+ status: "success",
29
+ });
30
+ if (format === "json") {
31
+ console.log(JSON.stringify({ ok: true, action: "worktree.add", ...result }, null, 2));
32
+ return;
33
+ }
34
+ console.log(chalk.green(`\n ${result.message}`));
35
+ }
36
+ catch (error) {
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ if (format === "json") {
39
+ console.log(JSON.stringify({ ok: false, action: "worktree.add", error: message }, null, 2));
40
+ return;
41
+ }
42
+ console.log(chalk.red(`\n ${message}`));
43
+ }
44
+ }
45
+ export async function worktreeListCommandAction(opts = {}) {
46
+ const format = asOutputFormat(opts.format);
47
+ try {
48
+ await setSessionPanel("session", "worktree-list");
49
+ const resolution = await getRepoResolution();
50
+ const worktrees = await listRegisteredWorktrees();
51
+ const payload = {
52
+ mode: resolution.mode,
53
+ projectRoot: resolution.projectRoot,
54
+ worktreeRoot: resolution.worktreeRoot,
55
+ worktrees,
56
+ };
57
+ if (format === "json") {
58
+ console.log(JSON.stringify({ ok: true, action: "worktree.list", ...payload }, null, 2));
59
+ return;
60
+ }
61
+ console.log(chalk.bold("\n Worktrees\n"));
62
+ if (resolution.mode === "linked") {
63
+ console.log(`Current folder is linked to: ${resolution.projectRoot}`);
64
+ console.log(`Linked worktree folder: ${resolution.worktreeRoot}`);
65
+ }
66
+ else {
67
+ console.log(`Project root: ${resolution.projectRoot}`);
68
+ }
69
+ console.log("");
70
+ if (worktrees.length === 0) {
71
+ console.log(chalk.yellow("No extra worktrees are registered yet."));
72
+ return;
73
+ }
74
+ worktrees.forEach((entry) => {
75
+ console.log(`- ${entry.name} -> ${entry.worktreePath}`);
76
+ });
77
+ }
78
+ catch (error) {
79
+ const message = error instanceof Error ? error.message : String(error);
80
+ if (format === "json") {
81
+ console.log(JSON.stringify({ ok: false, action: "worktree.list", error: message }, null, 2));
82
+ return;
83
+ }
84
+ console.log(chalk.red(`\n ${message}`));
85
+ }
86
+ }
87
+ export async function worktreeRemoveCommandAction(targetPath, opts = {}) {
88
+ const format = asOutputFormat(opts.format);
89
+ try {
90
+ await setSessionPanel("session", "worktree-remove");
91
+ const result = await removeRegisteredWorktree(normalizeCliPathInput(targetPath ?? process.cwd()));
92
+ await recordSessionEvent({
93
+ kind: "session",
94
+ title: "Removed a worktree",
95
+ detail: result.message,
96
+ panel: "session",
97
+ focus: "worktree-remove",
98
+ status: "success",
99
+ });
100
+ if (format === "json") {
101
+ console.log(JSON.stringify({ ok: true, action: "worktree.remove", ...result }, null, 2));
102
+ return;
103
+ }
104
+ console.log(chalk.green(`\n ${result.message}`));
105
+ }
106
+ catch (error) {
107
+ const message = error instanceof Error ? error.message : String(error);
108
+ if (format === "json") {
109
+ console.log(JSON.stringify({ ok: false, action: "worktree.remove", error: message }, null, 2));
110
+ return;
111
+ }
112
+ console.log(chalk.red(`\n ${message}`));
113
+ }
114
+ }
115
+ export function worktreeCommand(cli) {
116
+ cli
117
+ .command("worktree <action> [...pathParts]", "Register subfolders or sibling checkouts as worktrees of this Umbrella repo")
118
+ .example('worktree add "C:\\Dev\\Shared Repo"')
119
+ .example("worktree list")
120
+ .example("worktree remove ../other-checkout")
121
+ .option("--force", "Replace an existing .um folder in the target with a worktree link")
122
+ .option("--name <name>", "Friendly name for the worktree")
123
+ .option("--format <format>", "Output format (text or json)")
124
+ .action(async (action, pathParts = [], opts) => {
125
+ const normalized = action.trim().toLowerCase();
126
+ const targetPath = pathParts.join(" ").trim();
127
+ if (normalized === "add") {
128
+ await worktreeAddCommandAction(targetPath, opts);
129
+ return;
130
+ }
131
+ if (normalized === "list") {
132
+ await worktreeListCommandAction(opts);
133
+ return;
134
+ }
135
+ if (normalized === "remove") {
136
+ await worktreeRemoveCommandAction(targetPath, opts);
137
+ return;
138
+ }
139
+ console.log(chalk.red("Use one of: worktree add [path], worktree list, worktree remove [path]"));
140
+ });
141
+ }
package/dist/index.js CHANGED
@@ -29,6 +29,10 @@ import { tasksCommand } from "./commands/tasks.js";
29
29
  import { treeCommand } from "./commands/tree.js";
30
30
  import { transportCommand } from "./commands/transport.js";
31
31
  import { bridgeCommand } from "./commands/bridge.js";
32
+ import { swarmCommand } from "./commands/swarm.js";
33
+ import { worktreeCommand } from "./commands/worktree.js";
34
+ import { sourceCommand } from "./commands/source.js";
35
+ import { adaptiveCommand } from "./commands/adaptive.js";
32
36
  import { curateViewCommandAction } from "./commands/curate.js";
33
37
  function readCliVersion() {
34
38
  try {
@@ -72,6 +76,10 @@ tasksCommand(cli);
72
76
  treeCommand(cli);
73
77
  transportCommand(cli);
74
78
  bridgeCommand(cli);
79
+ swarmCommand(cli);
80
+ worktreeCommand(cli);
81
+ sourceCommand(cli);
82
+ adaptiveCommand(cli);
75
83
  cli
76
84
  .command("query [...args]", `Alias for search
77
85
 
@@ -98,6 +98,24 @@ export type LocalConnectorRun = {
98
98
  summary: string;
99
99
  ranAt: string;
100
100
  };
101
+ export type LocalWorktreeLink = {
102
+ version: 1;
103
+ projectRoot: string;
104
+ linkedAt: string;
105
+ source: "umbrella-worktree";
106
+ };
107
+ export type LocalWorktreeEntry = {
108
+ id: string;
109
+ name: string;
110
+ registeredAt: string;
111
+ worktreePath: string;
112
+ };
113
+ export type LocalKnowledgeSourceEntry = {
114
+ addedAt: string;
115
+ alias: string;
116
+ id: string;
117
+ projectRoot: string;
118
+ };
101
119
  export type RepoSessionState = {
102
120
  version: 1;
103
121
  id: string;
@@ -248,6 +266,12 @@ export type RepoContextTreeState = {
248
266
  };
249
267
  nodes: ContextTreeNode[];
250
268
  };
269
+ export type RepoRootResolution = {
270
+ mode: "direct" | "linked";
271
+ projectRoot: string;
272
+ worktreeRoot: string | null;
273
+ };
274
+ export declare function resolveRepoRoot(startDir?: string): Promise<RepoRootResolution>;
251
275
  export declare function findRepoRoot(startDir?: string): Promise<string>;
252
276
  export declare function ensureRepoContext(config: AgentMemoryConfig, cwd?: string): Promise<string>;
253
277
  export declare function getRepoContext(cwd?: string): Promise<{
@@ -255,6 +279,53 @@ export declare function getRepoContext(cwd?: string): Promise<{
255
279
  state: RepoContextState | null;
256
280
  umDir: string;
257
281
  }>;
282
+ export declare function getRepoResolution(cwd?: string): Promise<RepoRootResolution>;
283
+ export declare function getWorktreeLink(cwd?: string): Promise<{
284
+ projectRoot: string;
285
+ version: 1;
286
+ linkedAt: string;
287
+ source: "umbrella-worktree";
288
+ } | null>;
289
+ export declare function listRegisteredWorktrees(cwd?: string): Promise<{
290
+ worktreePath: string;
291
+ id: string;
292
+ name: string;
293
+ registeredAt: string;
294
+ }[]>;
295
+ export declare function addRegisteredWorktree(worktreePath: string, opts?: {
296
+ force?: boolean;
297
+ name?: string;
298
+ }, cwd?: string): Promise<{
299
+ changed: boolean;
300
+ message: string;
301
+ projectRoot: string;
302
+ worktreePath: string;
303
+ }>;
304
+ export declare function removeRegisteredWorktree(worktreePath: string, cwd?: string): Promise<{
305
+ changed: boolean;
306
+ message: string;
307
+ projectRoot: string;
308
+ worktreePath: string;
309
+ }>;
310
+ export declare function listKnowledgeSources(cwd?: string): Promise<{
311
+ valid: boolean;
312
+ projectRoot: string;
313
+ addedAt: string;
314
+ alias: string;
315
+ id: string;
316
+ }[]>;
317
+ export declare function addKnowledgeSource(targetPath: string, opts?: {
318
+ alias?: string;
319
+ }, cwd?: string): Promise<{
320
+ added: boolean;
321
+ alias: string;
322
+ message: string;
323
+ projectRoot: string;
324
+ }>;
325
+ export declare function removeKnowledgeSource(aliasOrPath: string, cwd?: string): Promise<{
326
+ message: string;
327
+ removed: boolean;
328
+ }>;
258
329
  export declare function updateRepoContext(updater: (current: RepoContextState) => RepoContextState | Promise<RepoContextState>, cwd?: string): Promise<RepoContextState>;
259
330
  export declare function addPendingMemory(input: Omit<LocalMemoryEntry, "createdAt" | "id">, cwd?: string): Promise<LocalMemoryEntry>;
260
331
  export declare function getPendingMemories(cwd?: string): Promise<LocalMemoryEntry[]>;
@@ -16,6 +16,9 @@ const TASKS_FILE = "tasks.json";
16
16
  const TRANSPORT_FILE = "transport.json";
17
17
  const CONTEXT_TREE_STATE_FILE = "context-tree.json";
18
18
  const QUERY_CACHE_FILE = "query-cache.json";
19
+ const WORKTREE_LINK_FILE = "worktree-link.json";
20
+ const WORKTREES_FILE = "worktrees.json";
21
+ const SOURCES_FILE = "sources.json";
19
22
  const HUB_ASSETS_DIR = "hub";
20
23
  const CONNECTOR_ASSETS_DIR = "connectors";
21
24
  const CONNECTOR_RUN_REPORTS_DIR = "connector-runs";
@@ -139,15 +142,52 @@ async function findNearestPackageRoot(startDir = process.cwd()) {
139
142
  current = parent;
140
143
  }
141
144
  }
142
- export async function findRepoRoot(startDir = process.cwd()) {
145
+ async function readWorktreeLink(candidateDir) {
146
+ const linkPath = path.join(candidateDir, UM_DIR, WORKTREE_LINK_FILE);
147
+ if (!(await pathExists(linkPath)))
148
+ return null;
149
+ const link = await readJsonFile(linkPath, null);
150
+ if (!link?.projectRoot)
151
+ return null;
152
+ return {
153
+ link,
154
+ linkPath,
155
+ };
156
+ }
157
+ async function resolveRepoRootFromWorktreeLink(startDir = process.cwd()) {
158
+ let current = path.resolve(startDir);
159
+ while (true) {
160
+ const linked = await readWorktreeLink(current);
161
+ if (linked) {
162
+ return {
163
+ mode: "linked",
164
+ projectRoot: path.resolve(linked.link.projectRoot),
165
+ worktreeRoot: current,
166
+ };
167
+ }
168
+ const parent = path.dirname(current);
169
+ if (parent === current)
170
+ return null;
171
+ current = parent;
172
+ }
173
+ }
174
+ export async function resolveRepoRoot(startDir = process.cwd()) {
175
+ const linked = await resolveRepoRootFromWorktreeLink(startDir);
176
+ if (linked)
177
+ return linked;
143
178
  try {
144
179
  const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
145
180
  cwd: startDir,
146
181
  encoding: "utf8",
147
182
  stdio: ["ignore", "pipe", "ignore"],
148
183
  }).trim();
149
- if (gitRoot)
150
- return path.resolve(gitRoot);
184
+ if (gitRoot) {
185
+ return {
186
+ mode: "direct",
187
+ projectRoot: path.resolve(gitRoot),
188
+ worktreeRoot: null,
189
+ };
190
+ }
151
191
  }
152
192
  catch {
153
193
  // Fall back to local heuristics when git is unavailable or the cwd is not inside a git repo.
@@ -155,17 +195,30 @@ export async function findRepoRoot(startDir = process.cwd()) {
155
195
  let current = path.resolve(startDir);
156
196
  let nearestPackageJsonDir = null;
157
197
  while (true) {
158
- if (await pathExists(path.join(current, ".git")))
159
- return current;
198
+ if (await pathExists(path.join(current, ".git"))) {
199
+ return {
200
+ mode: "direct",
201
+ projectRoot: current,
202
+ worktreeRoot: null,
203
+ };
204
+ }
160
205
  if (!nearestPackageJsonDir && await pathExists(path.join(current, "package.json"))) {
161
206
  nearestPackageJsonDir = current;
162
207
  }
163
208
  const parent = path.dirname(current);
164
- if (parent === current)
165
- return nearestPackageJsonDir ?? path.resolve(startDir);
209
+ if (parent === current) {
210
+ return {
211
+ mode: "direct",
212
+ projectRoot: nearestPackageJsonDir ?? path.resolve(startDir),
213
+ worktreeRoot: null,
214
+ };
215
+ }
166
216
  current = parent;
167
217
  }
168
218
  }
219
+ export async function findRepoRoot(startDir = process.cwd()) {
220
+ return (await resolveRepoRoot(startDir)).projectRoot;
221
+ }
169
222
  async function copyMissingFiles(sourceDir, targetDir) {
170
223
  if (!(await pathExists(sourceDir)))
171
224
  return;
@@ -232,6 +285,15 @@ function getContextTreeStateFile(repoRoot) {
232
285
  function getQueryCacheFile(repoRoot) {
233
286
  return path.join(getUmDir(repoRoot), QUERY_CACHE_FILE);
234
287
  }
288
+ function getWorktreeLinkFile(repoRoot) {
289
+ return path.join(getUmDir(repoRoot), WORKTREE_LINK_FILE);
290
+ }
291
+ function getWorktreesFile(repoRoot) {
292
+ return path.join(getUmDir(repoRoot), WORKTREES_FILE);
293
+ }
294
+ function getSourcesFile(repoRoot) {
295
+ return path.join(getUmDir(repoRoot), SOURCES_FILE);
296
+ }
235
297
  async function ensureUmDir(repoRoot) {
236
298
  await fs.mkdir(getUmDir(repoRoot), { recursive: true });
237
299
  }
@@ -328,6 +390,197 @@ export async function getRepoContext(cwd = process.cwd()) {
328
390
  const state = await readJsonFile(getContextFile(repoRoot), null);
329
391
  return { repoRoot, state, umDir: getUmDir(repoRoot) };
330
392
  }
393
+ export async function getRepoResolution(cwd = process.cwd()) {
394
+ const resolution = await resolveRepoRoot(cwd);
395
+ await migrateLegacyUmDir(cwd, resolution.projectRoot);
396
+ return resolution;
397
+ }
398
+ export async function getWorktreeLink(cwd = process.cwd()) {
399
+ const resolution = await resolveRepoRoot(cwd);
400
+ if (resolution.mode !== "linked" || !resolution.worktreeRoot)
401
+ return null;
402
+ const raw = await readJsonFile(getWorktreeLinkFile(resolution.worktreeRoot), null);
403
+ return raw && raw.projectRoot
404
+ ? {
405
+ ...raw,
406
+ projectRoot: path.resolve(raw.projectRoot),
407
+ }
408
+ : null;
409
+ }
410
+ export async function listRegisteredWorktrees(cwd = process.cwd()) {
411
+ const repoRoot = await findRepoRoot(cwd);
412
+ const entries = await readJsonFile(getWorktreesFile(repoRoot), []);
413
+ return uniqueBy(entries.map((entry) => ({
414
+ ...entry,
415
+ worktreePath: path.resolve(entry.worktreePath),
416
+ })), (entry) => entry.worktreePath.toLowerCase());
417
+ }
418
+ export async function addRegisteredWorktree(worktreePath, opts = {}, cwd = process.cwd()) {
419
+ const repoRoot = await findRepoRoot(cwd);
420
+ const targetPath = path.resolve(worktreePath);
421
+ if (targetPath === repoRoot) {
422
+ throw new Error("The current repo root is already the main project, so it does not need a worktree link.");
423
+ }
424
+ const targetUmDir = getUmDir(targetPath);
425
+ const existingLink = await readJsonFile(getWorktreeLinkFile(targetPath), null);
426
+ if (existingLink?.projectRoot && path.resolve(existingLink.projectRoot) === repoRoot) {
427
+ return {
428
+ changed: false,
429
+ message: `${targetPath} is already linked to ${repoRoot}.`,
430
+ projectRoot: repoRoot,
431
+ worktreePath: targetPath,
432
+ };
433
+ }
434
+ const backupPath = path.join(targetPath, ".um-backup");
435
+ if ((await pathExists(targetUmDir)) && !existingLink) {
436
+ if (!opts.force) {
437
+ throw new Error(`"${targetPath}" already has its own .um folder. Re-run with --force to back it up and link it as a worktree.`);
438
+ }
439
+ if (await pathExists(backupPath)) {
440
+ throw new Error(`"${targetPath}" already has a .um-backup folder. Please clean that up before forcing a new worktree link.`);
441
+ }
442
+ await fs.rename(targetUmDir, backupPath);
443
+ }
444
+ await ensureUmDir(targetPath);
445
+ const link = {
446
+ version: 1,
447
+ projectRoot: repoRoot,
448
+ linkedAt: new Date().toISOString(),
449
+ source: "umbrella-worktree",
450
+ };
451
+ await writeJsonFile(getWorktreeLinkFile(targetPath), link);
452
+ const entries = await listRegisteredWorktrees(cwd);
453
+ const nextEntry = {
454
+ id: existingLink?.projectRoot ? existingLink.projectRoot : randomUUID(),
455
+ name: opts.name?.trim() || path.basename(targetPath),
456
+ registeredAt: new Date().toISOString(),
457
+ worktreePath: targetPath,
458
+ };
459
+ const nextEntries = uniqueBy([nextEntry, ...entries.filter((entry) => entry.worktreePath.toLowerCase() !== targetPath.toLowerCase())], (entry) => entry.worktreePath.toLowerCase());
460
+ await writeJsonFile(getWorktreesFile(repoRoot), nextEntries);
461
+ return {
462
+ changed: true,
463
+ message: `Linked ${targetPath} to ${repoRoot} as a worktree.`,
464
+ projectRoot: repoRoot,
465
+ worktreePath: targetPath,
466
+ };
467
+ }
468
+ export async function removeRegisteredWorktree(worktreePath, cwd = process.cwd()) {
469
+ const targetPath = path.resolve(worktreePath);
470
+ const linked = await readJsonFile(getWorktreeLinkFile(targetPath), null);
471
+ if (!linked?.projectRoot) {
472
+ const repoRoot = await findRepoRoot(cwd);
473
+ const entries = await readJsonFile(getWorktreesFile(repoRoot), []);
474
+ const normalizedInput = worktreePath.trim().toLowerCase();
475
+ const matchingEntry = entries.find((entry) => path.resolve(entry.worktreePath).toLowerCase() === targetPath.toLowerCase() ||
476
+ entry.name.toLowerCase() === normalizedInput);
477
+ if (!matchingEntry) {
478
+ throw new Error(`"${targetPath}" is not linked as an Umbrella worktree.`);
479
+ }
480
+ const nextEntries = entries.filter((entry) => entry.id !== matchingEntry.id);
481
+ await writeJsonFile(getWorktreesFile(repoRoot), nextEntries);
482
+ return {
483
+ changed: true,
484
+ message: await pathExists(targetPath)
485
+ ? `Removed the stale worktree registration for ${targetPath}. The folder is still there, but its link file is missing.`
486
+ : `Removed the stale worktree registration for missing folder ${targetPath}.`,
487
+ projectRoot: repoRoot,
488
+ worktreePath: targetPath,
489
+ };
490
+ }
491
+ const repoRoot = path.resolve(linked.projectRoot);
492
+ const entries = await readJsonFile(getWorktreesFile(repoRoot), []);
493
+ const nextEntries = entries.filter((entry) => path.resolve(entry.worktreePath).toLowerCase() !== targetPath.toLowerCase());
494
+ await writeJsonFile(getWorktreesFile(repoRoot), nextEntries);
495
+ const linkFile = getWorktreeLinkFile(targetPath);
496
+ if (await pathExists(linkFile)) {
497
+ await fs.rm(linkFile, { force: true });
498
+ }
499
+ const targetUmDir = getUmDir(targetPath);
500
+ const backupPath = path.join(targetPath, ".um-backup");
501
+ if (await pathExists(backupPath)) {
502
+ try {
503
+ const remaining = await fs.readdir(targetUmDir);
504
+ if (remaining.length === 0) {
505
+ await fs.rm(targetUmDir, { recursive: true, force: true });
506
+ }
507
+ }
508
+ catch {
509
+ // Ignore cleanup issues before restore.
510
+ }
511
+ if (!(await pathExists(targetUmDir))) {
512
+ await fs.rename(backupPath, targetUmDir);
513
+ }
514
+ }
515
+ return {
516
+ changed: true,
517
+ message: `Removed the worktree link for ${targetPath}.`,
518
+ projectRoot: repoRoot,
519
+ worktreePath: targetPath,
520
+ };
521
+ }
522
+ export async function listKnowledgeSources(cwd = process.cwd()) {
523
+ const repoRoot = await findRepoRoot(cwd);
524
+ const entries = await readJsonFile(getSourcesFile(repoRoot), []);
525
+ const withStatus = await Promise.all(uniqueBy(entries.map((entry) => ({
526
+ ...entry,
527
+ projectRoot: path.resolve(entry.projectRoot),
528
+ })), (entry) => entry.projectRoot.toLowerCase()).map(async (entry) => ({
529
+ ...entry,
530
+ valid: await pathExists(getContextFile(entry.projectRoot)),
531
+ })));
532
+ return withStatus;
533
+ }
534
+ export async function addKnowledgeSource(targetPath, opts = {}, cwd = process.cwd()) {
535
+ const repoRoot = await findRepoRoot(cwd);
536
+ const sourceRoot = await findRepoRoot(path.resolve(targetPath));
537
+ if (sourceRoot.toLowerCase() === repoRoot.toLowerCase()) {
538
+ throw new Error("A repo cannot add itself as a knowledge source.");
539
+ }
540
+ if (!(await pathExists(getContextFile(sourceRoot)))) {
541
+ throw new Error(`"${sourceRoot}" does not look like a configured Umbrella repo yet. Run setup there first.`);
542
+ }
543
+ const alias = (opts.alias?.trim() || path.basename(sourceRoot)).replace(/\s+/g, "-");
544
+ const entries = await readJsonFile(getSourcesFile(repoRoot), []);
545
+ const existing = entries.find((entry) => entry.alias.toLowerCase() === alias.toLowerCase() ||
546
+ path.resolve(entry.projectRoot).toLowerCase() === sourceRoot.toLowerCase());
547
+ if (existing) {
548
+ return {
549
+ added: false,
550
+ alias: existing.alias,
551
+ message: `${existing.alias} is already linked as a knowledge source.`,
552
+ projectRoot: sourceRoot,
553
+ };
554
+ }
555
+ const nextEntry = {
556
+ addedAt: new Date().toISOString(),
557
+ alias,
558
+ id: randomUUID(),
559
+ projectRoot: sourceRoot,
560
+ };
561
+ await writeJsonFile(getSourcesFile(repoRoot), [...entries, nextEntry]);
562
+ return {
563
+ added: true,
564
+ alias,
565
+ message: `Added ${alias} as a read-only knowledge source from ${sourceRoot}.`,
566
+ projectRoot: sourceRoot,
567
+ };
568
+ }
569
+ export async function removeKnowledgeSource(aliasOrPath, cwd = process.cwd()) {
570
+ const repoRoot = await findRepoRoot(cwd);
571
+ const entries = await readJsonFile(getSourcesFile(repoRoot), []);
572
+ const normalizedInput = path.resolve(aliasOrPath).toLowerCase();
573
+ const nextEntries = entries.filter((entry) => entry.alias.toLowerCase() !== aliasOrPath.trim().toLowerCase() &&
574
+ path.resolve(entry.projectRoot).toLowerCase() !== normalizedInput);
575
+ if (nextEntries.length === entries.length) {
576
+ throw new Error(`Could not find a knowledge source matching "${aliasOrPath}".`);
577
+ }
578
+ await writeJsonFile(getSourcesFile(repoRoot), nextEntries);
579
+ return {
580
+ message: `Removed knowledge source "${aliasOrPath}".`,
581
+ removed: true,
582
+ };
583
+ }
331
584
  export async function updateRepoContext(updater, cwd = process.cwd()) {
332
585
  const { repoRoot, state } = await getRepoContext(cwd);
333
586
  if (!state)