umbrella-context 0.1.2 → 0.1.32

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.
Files changed (68) hide show
  1. package/bin/um.js +2 -0
  2. package/dist/adapters/byterover-context-runtime-store.d.ts +15 -0
  3. package/dist/adapters/byterover-context-runtime-store.js +57 -0
  4. package/dist/adapters/byterover-runtime-bridge.d.ts +218 -0
  5. package/dist/adapters/byterover-runtime-bridge.js +343 -0
  6. package/dist/adapters/byterover-transport-task-store.d.ts +13 -0
  7. package/dist/adapters/byterover-transport-task-store.js +50 -0
  8. package/dist/adapters/umbrella-onboarding.d.ts +27 -0
  9. package/dist/adapters/umbrella-onboarding.js +79 -0
  10. package/dist/adapters/umbrella-provider-runtime.d.ts +38 -0
  11. package/dist/adapters/umbrella-provider-runtime.js +199 -0
  12. package/dist/adapters/vendor-byterover.d.ts +4 -0
  13. package/dist/adapters/vendor-byterover.js +19 -0
  14. package/dist/commands/activity.d.ts +2 -0
  15. package/dist/commands/activity.js +82 -0
  16. package/dist/commands/bridge.d.ts +2 -0
  17. package/dist/commands/bridge.js +40 -0
  18. package/dist/commands/catalog.d.ts +34 -0
  19. package/dist/commands/catalog.js +234 -0
  20. package/dist/commands/connect.js +14 -14
  21. package/dist/commands/connectors.d.ts +24 -0
  22. package/dist/commands/connectors.js +626 -0
  23. package/dist/commands/curate.d.ts +1 -0
  24. package/dist/commands/curate.js +48 -3
  25. package/dist/commands/debug.d.ts +2 -0
  26. package/dist/commands/debug.js +55 -0
  27. package/dist/commands/fix.js +54 -0
  28. package/dist/commands/hub.d.ts +22 -0
  29. package/dist/commands/hub.js +487 -0
  30. package/dist/commands/interactive.d.ts +2 -0
  31. package/dist/commands/interactive.js +970 -62
  32. package/dist/commands/locations.d.ts +1 -0
  33. package/dist/commands/locations.js +15 -12
  34. package/dist/commands/logout.d.ts +2 -0
  35. package/dist/commands/logout.js +34 -0
  36. package/dist/commands/model.d.ts +11 -0
  37. package/dist/commands/model.js +225 -0
  38. package/dist/commands/providers.d.ts +17 -0
  39. package/dist/commands/providers.js +379 -0
  40. package/dist/commands/pull.js +60 -1
  41. package/dist/commands/push.js +62 -2
  42. package/dist/commands/reset.d.ts +2 -0
  43. package/dist/commands/reset.js +35 -0
  44. package/dist/commands/restart.d.ts +2 -0
  45. package/dist/commands/restart.js +21 -0
  46. package/dist/commands/search.js +65 -1
  47. package/dist/commands/session.d.ts +2 -0
  48. package/dist/commands/session.js +241 -0
  49. package/dist/commands/setup.js +58 -56
  50. package/dist/commands/space.d.ts +12 -0
  51. package/dist/commands/space.js +138 -42
  52. package/dist/commands/status.d.ts +29 -0
  53. package/dist/commands/status.js +120 -19
  54. package/dist/commands/tasks.d.ts +2 -0
  55. package/dist/commands/tasks.js +95 -0
  56. package/dist/commands/transport.d.ts +2 -0
  57. package/dist/commands/transport.js +88 -0
  58. package/dist/commands/tree.d.ts +2 -0
  59. package/dist/commands/tree.js +98 -0
  60. package/dist/commands/tui.d.ts +2 -0
  61. package/dist/commands/tui.js +1273 -0
  62. package/dist/config.d.ts +23 -0
  63. package/dist/config.js +69 -0
  64. package/dist/index.js +41 -5
  65. package/dist/repo-state.d.ts +227 -1
  66. package/dist/repo-state.js +920 -4
  67. package/dist/umbrella.js +29 -5
  68. package/package.json +11 -3
@@ -1,11 +1,86 @@
1
1
  import { promises as fs } from "fs";
2
2
  import path from "path";
3
3
  import { randomUUID } from "crypto";
4
+ import { execFileSync } from "child_process";
5
+ import { configManager } from "./config.js";
4
6
  const UM_DIR = ".um";
5
7
  const CONTEXT_FILE = "context.json";
6
8
  const PENDING_MEMORIES_FILE = "pending-memories.json";
7
9
  const PULLED_MEMORIES_FILE = "pulled-memories.json";
8
10
  const PULLED_FIXES_FILE = "pulled-fixes.json";
11
+ const CONNECTORS_FILE = "connectors.json";
12
+ const HUB_FILE = "hub.json";
13
+ const CONNECTOR_RUNS_FILE = "connector-runs.json";
14
+ const SESSION_FILE = "session.json";
15
+ const TASKS_FILE = "tasks.json";
16
+ const TRANSPORT_FILE = "transport.json";
17
+ const CONTEXT_TREE_STATE_FILE = "context-tree.json";
18
+ const HUB_ASSETS_DIR = "hub";
19
+ const CONNECTOR_ASSETS_DIR = "connectors";
20
+ const CONNECTOR_RUN_REPORTS_DIR = "connector-runs";
21
+ function normalizeSessionState(state) {
22
+ if (!state)
23
+ return null;
24
+ return {
25
+ ...state,
26
+ commandHistory: state.commandHistory ?? [],
27
+ recentQueries: state.recentQueries ?? [],
28
+ recentCurations: state.recentCurations ?? [],
29
+ lastSummary: state.lastSummary ?? null,
30
+ currentPanel: state.currentPanel ?? null,
31
+ currentFocus: state.currentFocus ?? null,
32
+ panelHistory: state.panelHistory ?? [],
33
+ recentProviderIds: state.recentProviderIds ?? [],
34
+ recentModels: state.recentModels ?? [],
35
+ recentHubSlugs: state.recentHubSlugs ?? [],
36
+ recentConnectorSources: state.recentConnectorSources ?? [],
37
+ events: state.events ?? [],
38
+ };
39
+ }
40
+ function normalizeTransportState(state) {
41
+ if (!state)
42
+ return null;
43
+ return {
44
+ version: 1,
45
+ status: state.status ?? "idle",
46
+ activeTaskId: state.activeTaskId ?? null,
47
+ queue: state.queue ?? [],
48
+ stats: state.stats ?? { queued: 0, running: 0, completed: 0, failed: 0 },
49
+ subscriptions: state.subscriptions ?? ["task:*", "sync:*", "runtime:*", "context-tree:*"],
50
+ lastPushAt: state.lastPushAt ?? null,
51
+ lastPullAt: state.lastPullAt ?? null,
52
+ lastRuntimeCheckAt: state.lastRuntimeCheckAt ?? null,
53
+ taskKinds: state.taskKinds ?? {},
54
+ updatedAt: state.updatedAt ?? new Date().toISOString(),
55
+ events: state.events ?? [],
56
+ };
57
+ }
58
+ function buildTransportTaskKinds(tasks) {
59
+ return tasks.reduce((acc, task) => {
60
+ acc[task.kind] = (acc[task.kind] ?? 0) + 1;
61
+ return acc;
62
+ }, {});
63
+ }
64
+ function buildTransportStats(tasks) {
65
+ return tasks.reduce((acc, task) => {
66
+ if (task.status === "running")
67
+ acc.running += 1;
68
+ else if (task.status === "completed")
69
+ acc.completed += 1;
70
+ else if (task.status === "failed")
71
+ acc.failed += 1;
72
+ else
73
+ acc.queued += 1;
74
+ return acc;
75
+ }, { queued: 0, running: 0, completed: 0, failed: 0 });
76
+ }
77
+ function transportStatusFromTasks(tasks) {
78
+ if (tasks.some((task) => task.status === "running"))
79
+ return "running";
80
+ if (tasks.some((task) => task.status === "failed"))
81
+ return "warning";
82
+ return "idle";
83
+ }
9
84
  async function pathExists(targetPath) {
10
85
  try {
11
86
  await fs.access(targetPath);
@@ -15,19 +90,71 @@ async function pathExists(targetPath) {
15
90
  return false;
16
91
  }
17
92
  }
93
+ async function findNearestPackageRoot(startDir = process.cwd()) {
94
+ let current = path.resolve(startDir);
95
+ while (true) {
96
+ if (await pathExists(path.join(current, "package.json")))
97
+ return current;
98
+ const parent = path.dirname(current);
99
+ if (parent === current)
100
+ return null;
101
+ current = parent;
102
+ }
103
+ }
18
104
  export async function findRepoRoot(startDir = process.cwd()) {
105
+ try {
106
+ const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
107
+ cwd: startDir,
108
+ encoding: "utf8",
109
+ stdio: ["ignore", "pipe", "ignore"],
110
+ }).trim();
111
+ if (gitRoot)
112
+ return path.resolve(gitRoot);
113
+ }
114
+ catch {
115
+ // Fall back to local heuristics when git is unavailable or the cwd is not inside a git repo.
116
+ }
19
117
  let current = path.resolve(startDir);
118
+ let nearestPackageJsonDir = null;
20
119
  while (true) {
21
120
  if (await pathExists(path.join(current, ".git")))
22
121
  return current;
23
- if (await pathExists(path.join(current, "package.json")))
24
- return current;
122
+ if (!nearestPackageJsonDir && await pathExists(path.join(current, "package.json"))) {
123
+ nearestPackageJsonDir = current;
124
+ }
25
125
  const parent = path.dirname(current);
26
126
  if (parent === current)
27
- return path.resolve(startDir);
127
+ return nearestPackageJsonDir ?? path.resolve(startDir);
28
128
  current = parent;
29
129
  }
30
130
  }
131
+ async function copyMissingFiles(sourceDir, targetDir) {
132
+ if (!(await pathExists(sourceDir)))
133
+ return;
134
+ await fs.mkdir(targetDir, { recursive: true });
135
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
136
+ for (const entry of entries) {
137
+ if (!entry.isFile())
138
+ continue;
139
+ const source = path.join(sourceDir, entry.name);
140
+ const target = path.join(targetDir, entry.name);
141
+ if (await pathExists(target))
142
+ continue;
143
+ await fs.copyFile(source, target);
144
+ }
145
+ }
146
+ function uniqueBy(items, key) {
147
+ const seen = new Set();
148
+ const result = [];
149
+ for (const item of items) {
150
+ const nextKey = key(item);
151
+ if (seen.has(nextKey))
152
+ continue;
153
+ seen.add(nextKey);
154
+ result.push(item);
155
+ }
156
+ return result;
157
+ }
31
158
  function getUmDir(repoRoot) {
32
159
  return path.join(repoRoot, UM_DIR);
33
160
  }
@@ -43,9 +170,35 @@ function getPulledMemoriesFile(repoRoot) {
43
170
  function getPulledFixesFile(repoRoot) {
44
171
  return path.join(getUmDir(repoRoot), PULLED_FIXES_FILE);
45
172
  }
173
+ function getConnectorsFile(repoRoot) {
174
+ return path.join(getUmDir(repoRoot), CONNECTORS_FILE);
175
+ }
176
+ function getHubFile(repoRoot) {
177
+ return path.join(getUmDir(repoRoot), HUB_FILE);
178
+ }
179
+ function getConnectorRunsFile(repoRoot) {
180
+ return path.join(getUmDir(repoRoot), CONNECTOR_RUNS_FILE);
181
+ }
182
+ function getSessionFile(repoRoot) {
183
+ return path.join(getUmDir(repoRoot), SESSION_FILE);
184
+ }
185
+ function getTasksFile(repoRoot) {
186
+ return path.join(getUmDir(repoRoot), TASKS_FILE);
187
+ }
188
+ function getTransportFile(repoRoot) {
189
+ return path.join(getUmDir(repoRoot), TRANSPORT_FILE);
190
+ }
191
+ function getContextTreeStateFile(repoRoot) {
192
+ return path.join(getUmDir(repoRoot), CONTEXT_TREE_STATE_FILE);
193
+ }
46
194
  async function ensureUmDir(repoRoot) {
47
195
  await fs.mkdir(getUmDir(repoRoot), { recursive: true });
48
196
  }
197
+ async function ensureSubDir(repoRoot, name) {
198
+ const target = path.join(getUmDir(repoRoot), name);
199
+ await fs.mkdir(target, { recursive: true });
200
+ return target;
201
+ }
49
202
  async function readJsonFile(filePath, fallback) {
50
203
  try {
51
204
  const raw = await fs.readFile(filePath, "utf8");
@@ -58,8 +211,47 @@ async function readJsonFile(filePath, fallback) {
58
211
  async function writeJsonFile(filePath, data) {
59
212
  await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
60
213
  }
214
+ async function migrateLegacyUmDir(startDir, repoRoot) {
215
+ const legacyRoot = await findNearestPackageRoot(startDir);
216
+ if (!legacyRoot || legacyRoot === repoRoot)
217
+ return;
218
+ const legacyUmDir = getUmDir(legacyRoot);
219
+ if (!(await pathExists(legacyUmDir)))
220
+ return;
221
+ await ensureUmDir(repoRoot);
222
+ const mergeArrayFile = async (fileName, fallback, key) => {
223
+ const legacyPath = path.join(legacyUmDir, fileName);
224
+ if (!(await pathExists(legacyPath)))
225
+ return;
226
+ const rootPath = path.join(getUmDir(repoRoot), fileName);
227
+ const legacyItems = await readJsonFile(legacyPath, fallback);
228
+ const rootItems = await readJsonFile(rootPath, fallback);
229
+ const merged = uniqueBy([...rootItems, ...legacyItems], key);
230
+ await writeJsonFile(rootPath, merged);
231
+ };
232
+ const rootContextPath = getContextFile(repoRoot);
233
+ const legacyContextPath = getContextFile(legacyRoot);
234
+ if (!(await pathExists(rootContextPath)) && await pathExists(legacyContextPath)) {
235
+ await fs.copyFile(legacyContextPath, rootContextPath);
236
+ }
237
+ await mergeArrayFile(PENDING_MEMORIES_FILE, [], (item) => item.id);
238
+ await mergeArrayFile(PULLED_MEMORIES_FILE, [], (item) => item.id);
239
+ await mergeArrayFile(PULLED_FIXES_FILE, [], (item) => item.id);
240
+ await mergeArrayFile(CONNECTORS_FILE, [], (item) => item.source);
241
+ await mergeArrayFile(HUB_FILE, [], (item) => item.slug);
242
+ await mergeArrayFile(CONNECTOR_RUNS_FILE, [], (item) => item.id);
243
+ await copyMissingFiles(path.join(legacyUmDir, HUB_ASSETS_DIR), path.join(getUmDir(repoRoot), HUB_ASSETS_DIR));
244
+ await copyMissingFiles(path.join(legacyUmDir, CONNECTOR_ASSETS_DIR), path.join(getUmDir(repoRoot), CONNECTOR_ASSETS_DIR));
245
+ await copyMissingFiles(path.join(legacyUmDir, CONNECTOR_RUN_REPORTS_DIR), path.join(getUmDir(repoRoot), CONNECTOR_RUN_REPORTS_DIR));
246
+ const legacySessionPath = getSessionFile(legacyRoot);
247
+ const rootSessionPath = getSessionFile(repoRoot);
248
+ if (!(await pathExists(rootSessionPath)) && await pathExists(legacySessionPath)) {
249
+ await fs.copyFile(legacySessionPath, rootSessionPath);
250
+ }
251
+ }
61
252
  export async function ensureRepoContext(config, cwd = process.cwd()) {
62
253
  const repoRoot = await findRepoRoot(cwd);
254
+ await migrateLegacyUmDir(cwd, repoRoot);
63
255
  await ensureUmDir(repoRoot);
64
256
  const now = new Date().toISOString();
65
257
  const nextState = {
@@ -85,10 +277,13 @@ export async function ensureRepoContext(config, cwd = process.cwd()) {
85
277
  ...nextState,
86
278
  initializedAt: existing?.initializedAt ?? now,
87
279
  });
280
+ await ensureTransportState(cwd);
281
+ await rebuildContextTreeState(cwd);
88
282
  return repoRoot;
89
283
  }
90
284
  export async function getRepoContext(cwd = process.cwd()) {
91
285
  const repoRoot = await findRepoRoot(cwd);
286
+ await migrateLegacyUmDir(cwd, repoRoot);
92
287
  const state = await readJsonFile(getContextFile(repoRoot), null);
93
288
  return { repoRoot, state, umDir: getUmDir(repoRoot) };
94
289
  }
@@ -115,6 +310,7 @@ export async function addPendingMemory(input, cwd = process.cwd()) {
115
310
  ...state,
116
311
  updatedAt: new Date().toISOString(),
117
312
  }), cwd);
313
+ await rebuildContextTreeState(cwd);
118
314
  return entry;
119
315
  }
120
316
  export async function getPendingMemories(cwd = process.cwd()) {
@@ -124,6 +320,83 @@ export async function getPendingMemories(cwd = process.cwd()) {
124
320
  export async function clearPendingMemories(cwd = process.cwd()) {
125
321
  const { repoRoot } = await getRepoContext(cwd);
126
322
  await writeJsonFile(getPendingMemoriesFile(repoRoot), []);
323
+ await rebuildContextTreeState(cwd);
324
+ }
325
+ export async function resetRepoState(cwd = process.cwd()) {
326
+ const { repoRoot, state } = await getRepoContext(cwd);
327
+ await ensureUmDir(repoRoot);
328
+ await writeJsonFile(getPendingMemoriesFile(repoRoot), []);
329
+ await writeJsonFile(getPulledMemoriesFile(repoRoot), []);
330
+ await writeJsonFile(getPulledFixesFile(repoRoot), []);
331
+ await writeJsonFile(getConnectorsFile(repoRoot), []);
332
+ await writeJsonFile(getHubFile(repoRoot), []);
333
+ await writeJsonFile(getConnectorRunsFile(repoRoot), []);
334
+ await writeJsonFile(getTasksFile(repoRoot), []);
335
+ await writeJsonFile(getTransportFile(repoRoot), {
336
+ version: 1,
337
+ status: "idle",
338
+ activeTaskId: null,
339
+ queue: [],
340
+ stats: { queued: 0, running: 0, completed: 0, failed: 0 },
341
+ subscriptions: ["task:*", "sync:*", "runtime:*", "context-tree:*"],
342
+ lastPushAt: null,
343
+ lastPullAt: null,
344
+ lastRuntimeCheckAt: null,
345
+ taskKinds: {},
346
+ updatedAt: new Date().toISOString(),
347
+ events: [],
348
+ });
349
+ const hubDir = path.join(getUmDir(repoRoot), HUB_ASSETS_DIR);
350
+ const connectorDir = path.join(getUmDir(repoRoot), CONNECTOR_ASSETS_DIR);
351
+ const connectorReportsDir = path.join(getUmDir(repoRoot), CONNECTOR_RUN_REPORTS_DIR);
352
+ await fs.rm(hubDir, { recursive: true, force: true });
353
+ await fs.rm(connectorDir, { recursive: true, force: true });
354
+ await fs.rm(connectorReportsDir, { recursive: true, force: true });
355
+ const now = new Date().toISOString();
356
+ if (state) {
357
+ await writeJsonFile(getContextFile(repoRoot), {
358
+ ...state,
359
+ updatedAt: now,
360
+ lastPushAt: null,
361
+ lastPullAt: null,
362
+ });
363
+ }
364
+ const nextSession = {
365
+ version: 1,
366
+ id: randomUUID(),
367
+ startedAt: now,
368
+ updatedAt: now,
369
+ commandHistory: [],
370
+ recentQueries: [],
371
+ recentCurations: [],
372
+ lastSummary: "Reset local Context state for this repo.",
373
+ currentPanel: "reset",
374
+ currentFocus: null,
375
+ panelHistory: ["reset"],
376
+ recentProviderIds: [],
377
+ recentModels: [],
378
+ recentHubSlugs: [],
379
+ recentConnectorSources: [],
380
+ events: [
381
+ {
382
+ id: randomUUID(),
383
+ at: now,
384
+ kind: "session",
385
+ title: "Reset local Context state",
386
+ detail: "Local .um content, hub installs, connectors, runs, and pulled snapshots were cleared for this repo.",
387
+ panel: "reset",
388
+ focus: null,
389
+ status: "warning",
390
+ },
391
+ ],
392
+ };
393
+ await writeJsonFile(getSessionFile(repoRoot), nextSession);
394
+ await rebuildContextTreeState(cwd);
395
+ return {
396
+ repoRoot,
397
+ umDir: getUmDir(repoRoot),
398
+ resetAt: now,
399
+ };
127
400
  }
128
401
  export async function setPulledMemories(memories, cwd = process.cwd()) {
129
402
  const { repoRoot } = await getRepoContext(cwd);
@@ -134,6 +407,15 @@ export async function setPulledMemories(memories, cwd = process.cwd()) {
134
407
  updatedAt: new Date().toISOString(),
135
408
  lastPullAt: new Date().toISOString(),
136
409
  }), cwd);
410
+ await syncTransportFromTasks(cwd, { lastPullAt: new Date().toISOString() });
411
+ await recordTransportEvent({
412
+ kind: "sync-pull",
413
+ title: `Pulled ${memories.length} context entr${memories.length === 1 ? "y" : "ies"}`,
414
+ detail: "Updated the repo-local server snapshot.",
415
+ status: "success",
416
+ taskId: null,
417
+ }, cwd);
418
+ await rebuildContextTreeState(cwd);
137
419
  }
138
420
  export async function getPulledMemories(cwd = process.cwd()) {
139
421
  const { repoRoot } = await getRepoContext(cwd);
@@ -148,17 +430,651 @@ export async function setPulledFixes(fixes, cwd = process.cwd()) {
148
430
  updatedAt: new Date().toISOString(),
149
431
  lastPullAt: new Date().toISOString(),
150
432
  }), cwd);
433
+ await syncTransportFromTasks(cwd, { lastPullAt: new Date().toISOString() });
434
+ await rebuildContextTreeState(cwd);
151
435
  }
152
436
  export async function getPulledFixes(cwd = process.cwd()) {
153
437
  const { repoRoot } = await getRepoContext(cwd);
154
438
  return readJsonFile(getPulledFixesFile(repoRoot), []);
155
439
  }
156
440
  export async function markPushCompleted(cwd = process.cwd()) {
157
- return updateRepoContext((state) => ({
441
+ const next = await updateRepoContext((state) => ({
158
442
  ...state,
159
443
  updatedAt: new Date().toISOString(),
160
444
  lastPushAt: new Date().toISOString(),
161
445
  }), cwd);
446
+ await syncTransportFromTasks(cwd, { lastPushAt: next.lastPushAt });
447
+ await recordTransportEvent({
448
+ kind: "sync-push",
449
+ title: "Completed Context push",
450
+ detail: "Local drafts were shared with the server and local push markers were updated.",
451
+ status: "success",
452
+ taskId: null,
453
+ }, cwd);
454
+ await rebuildContextTreeState(cwd);
455
+ return next;
456
+ }
457
+ export async function getInstalledConnectors(cwd = process.cwd()) {
458
+ const { repoRoot } = await getRepoContext(cwd);
459
+ const connectors = await readJsonFile(getConnectorsFile(repoRoot), []);
460
+ const deduped = uniqueBy(connectors, (item) => item.source);
461
+ if (deduped.length !== connectors.length) {
462
+ await writeJsonFile(getConnectorsFile(repoRoot), deduped);
463
+ }
464
+ return deduped;
465
+ }
466
+ export async function setInstalledConnectors(connectors, cwd = process.cwd()) {
467
+ const { repoRoot } = await getRepoContext(cwd);
468
+ await ensureUmDir(repoRoot);
469
+ await writeJsonFile(getConnectorsFile(repoRoot), uniqueBy(connectors, (item) => item.source));
470
+ await updateRepoContext((state) => ({
471
+ ...state,
472
+ updatedAt: new Date().toISOString(),
473
+ }), cwd);
474
+ await rebuildContextTreeState(cwd);
475
+ }
476
+ export async function getInstalledHubEntries(cwd = process.cwd()) {
477
+ const { repoRoot } = await getRepoContext(cwd);
478
+ const entries = await readJsonFile(getHubFile(repoRoot), []);
479
+ const deduped = uniqueBy(entries, (item) => item.slug);
480
+ if (deduped.length !== entries.length) {
481
+ await writeJsonFile(getHubFile(repoRoot), deduped);
482
+ }
483
+ return deduped;
484
+ }
485
+ export async function setInstalledHubEntries(entries, cwd = process.cwd()) {
486
+ const { repoRoot } = await getRepoContext(cwd);
487
+ await ensureUmDir(repoRoot);
488
+ await writeJsonFile(getHubFile(repoRoot), uniqueBy(entries, (item) => item.slug));
489
+ await updateRepoContext((state) => ({
490
+ ...state,
491
+ updatedAt: new Date().toISOString(),
492
+ }), cwd);
493
+ await rebuildContextTreeState(cwd);
494
+ }
495
+ export async function writeHubAsset(slug, content, cwd = process.cwd()) {
496
+ const { repoRoot } = await getRepoContext(cwd);
497
+ const dir = await ensureSubDir(repoRoot, HUB_ASSETS_DIR);
498
+ const filePath = path.join(dir, `${slug}.md`);
499
+ await fs.writeFile(filePath, content, "utf8");
500
+ return filePath;
501
+ }
502
+ export async function writeHubAssetFiles(slug, files, cwd = process.cwd()) {
503
+ const { repoRoot } = await getRepoContext(cwd);
504
+ const rootDir = await ensureSubDir(repoRoot, HUB_ASSETS_DIR);
505
+ const dir = path.join(rootDir, slug);
506
+ await fs.mkdir(dir, { recursive: true });
507
+ const written = [];
508
+ for (const file of files) {
509
+ const filePath = path.join(dir, file.name);
510
+ await fs.writeFile(filePath, file.content, "utf8");
511
+ written.push(filePath);
512
+ }
513
+ return written;
514
+ }
515
+ export async function writeConnectorAsset(source, content, cwd = process.cwd()) {
516
+ const { repoRoot } = await getRepoContext(cwd);
517
+ const dir = await ensureSubDir(repoRoot, CONNECTOR_ASSETS_DIR);
518
+ const filePath = path.join(dir, `${source}.json`);
519
+ await writeJsonFile(filePath, content);
520
+ return filePath;
521
+ }
522
+ export async function writeConnectorAssetFiles(source, files, cwd = process.cwd()) {
523
+ const { repoRoot } = await getRepoContext(cwd);
524
+ const rootDir = await ensureSubDir(repoRoot, CONNECTOR_ASSETS_DIR);
525
+ const dir = path.join(rootDir, source);
526
+ await fs.mkdir(dir, { recursive: true });
527
+ const written = [];
528
+ for (const file of files) {
529
+ const filePath = path.join(dir, file.name);
530
+ await fs.writeFile(filePath, file.content, "utf8");
531
+ written.push(filePath);
532
+ }
533
+ return written;
534
+ }
535
+ export async function getConnectorRuns(cwd = process.cwd()) {
536
+ const { repoRoot } = await getRepoContext(cwd);
537
+ return readJsonFile(getConnectorRunsFile(repoRoot), []);
538
+ }
539
+ export async function getTasks(cwd = process.cwd()) {
540
+ const { repoRoot } = await getRepoContext(cwd);
541
+ return readJsonFile(getTasksFile(repoRoot), []);
542
+ }
543
+ export async function getTaskById(taskId, cwd = process.cwd()) {
544
+ const tasks = await getTasks(cwd);
545
+ return tasks.find((task) => task.id === taskId) ?? null;
546
+ }
547
+ export async function ensureTransportState(cwd = process.cwd()) {
548
+ const { repoRoot } = await getRepoContext(cwd);
549
+ await ensureUmDir(repoRoot);
550
+ const existing = normalizeTransportState(await readJsonFile(getTransportFile(repoRoot), null));
551
+ if (existing) {
552
+ await writeJsonFile(getTransportFile(repoRoot), existing);
553
+ return existing;
554
+ }
555
+ const next = {
556
+ version: 1,
557
+ status: "idle",
558
+ activeTaskId: null,
559
+ queue: [],
560
+ stats: { queued: 0, running: 0, completed: 0, failed: 0 },
561
+ subscriptions: ["task:*", "sync:*", "runtime:*", "context-tree:*"],
562
+ lastPushAt: null,
563
+ lastPullAt: null,
564
+ lastRuntimeCheckAt: null,
565
+ taskKinds: {},
566
+ updatedAt: new Date().toISOString(),
567
+ events: [],
568
+ };
569
+ await writeJsonFile(getTransportFile(repoRoot), next);
570
+ return next;
571
+ }
572
+ export async function getTransportState(cwd = process.cwd()) {
573
+ return ensureTransportState(cwd);
574
+ }
575
+ export async function updateTransportState(updater, cwd = process.cwd()) {
576
+ const { repoRoot } = await getRepoContext(cwd);
577
+ const current = normalizeTransportState(await ensureTransportState(cwd));
578
+ if (!current)
579
+ throw new Error("Could not initialize repo transport state.");
580
+ const next = normalizeTransportState(await updater(current));
581
+ if (!next)
582
+ throw new Error("Could not update repo transport state.");
583
+ await writeJsonFile(getTransportFile(repoRoot), next);
584
+ return next;
585
+ }
586
+ export async function recordTransportEvent(event, cwd = process.cwd()) {
587
+ return updateTransportState((state) => ({
588
+ ...state,
589
+ updatedAt: new Date().toISOString(),
590
+ events: [
591
+ {
592
+ id: randomUUID(),
593
+ at: new Date().toISOString(),
594
+ ...event,
595
+ },
596
+ ...(state.events ?? []),
597
+ ].slice(0, 50),
598
+ }), cwd);
599
+ }
600
+ async function syncTransportFromTasks(cwd = process.cwd(), markers) {
601
+ const tasks = await getTasks(cwd);
602
+ return updateTransportState((state) => ({
603
+ ...state,
604
+ status: transportStatusFromTasks(tasks),
605
+ activeTaskId: tasks.find((task) => task.status === "running")?.id ?? null,
606
+ queue: tasks.slice(0, 25).map((task) => ({
607
+ id: task.id,
608
+ kind: task.kind,
609
+ title: task.title,
610
+ status: task.status,
611
+ })),
612
+ stats: buildTransportStats(tasks),
613
+ taskKinds: buildTransportTaskKinds(tasks),
614
+ lastPushAt: markers?.lastPushAt ?? state.lastPushAt,
615
+ lastPullAt: markers?.lastPullAt ?? state.lastPullAt,
616
+ lastRuntimeCheckAt: markers?.lastRuntimeCheckAt ?? state.lastRuntimeCheckAt,
617
+ updatedAt: new Date().toISOString(),
618
+ }), cwd);
619
+ }
620
+ export async function getContextTreeState(cwd = process.cwd()) {
621
+ const { repoRoot } = await getRepoContext(cwd);
622
+ const existing = await readJsonFile(getContextTreeStateFile(repoRoot), null);
623
+ if (existing)
624
+ return existing;
625
+ return rebuildContextTreeState(cwd);
626
+ }
627
+ export async function rebuildContextTreeState(cwd = process.cwd()) {
628
+ const { repoRoot, state } = await getRepoContext(cwd);
629
+ await ensureUmDir(repoRoot);
630
+ const activeProvider = configManager.providers.find((entry) => entry.id === configManager.config?.activeProvider) ?? null;
631
+ const [pending, pulled, fixes, hubEntries, connectors, connectorRuns, tasks, transport] = await Promise.all([
632
+ getPendingMemories(cwd),
633
+ getPulledMemories(cwd),
634
+ getPulledFixes(cwd),
635
+ getInstalledHubEntries(cwd),
636
+ getInstalledConnectors(cwd),
637
+ getConnectorRuns(cwd),
638
+ getTasks(cwd),
639
+ ensureTransportState(cwd),
640
+ ]);
641
+ const companyId = state?.companyId ?? "company";
642
+ const spaceId = state?.projectId ?? "space";
643
+ const next = {
644
+ version: 1,
645
+ repoRoot,
646
+ umDir: getUmDir(repoRoot),
647
+ generatedAt: new Date().toISOString(),
648
+ summaryHandle: `${state?.companyName ?? "Unlinked"} / ${state?.projectName ?? "Unlinked"} has ${pending.length} pending drafts, ${pulled.length} pulled notes, ${fixes.length} fixes, ${tasks.length} tasks, and ${transport.status} transport status.`,
649
+ stats: {
650
+ pendingDrafts: pending.length,
651
+ pulledContext: pulled.length,
652
+ pulledFixes: fixes.length,
653
+ hubEntries: hubEntries.length,
654
+ connectors: connectors.length,
655
+ connectorRuns: connectorRuns.length,
656
+ tasks: tasks.length,
657
+ },
658
+ runtime: {
659
+ companyName: state?.companyName ?? null,
660
+ spaceName: state?.projectName ?? null,
661
+ provider: activeProvider ? `${activeProvider.name} (${activeProvider.kind})` : null,
662
+ model: configManager.config?.activeModel ?? null,
663
+ transportStatus: transport.status,
664
+ lastPushAt: state?.lastPushAt ?? null,
665
+ lastPullAt: state?.lastPullAt ?? null,
666
+ },
667
+ nodes: [
668
+ { id: companyId, label: state?.companyName ?? "Unlinked company", kind: "company", parentId: null, detail: state ? "Current Umbrella company link" : "Repo not linked yet" },
669
+ { id: spaceId, label: state?.projectName ?? "Unlinked space", kind: "space", parentId: companyId, detail: state?.workspaceName ?? null },
670
+ { id: "repo-root", label: "Repo Root", kind: "folder", parentId: spaceId, detail: repoRoot },
671
+ { id: "um-dir", label: ".um", kind: "folder", parentId: "repo-root", detail: getUmDir(repoRoot) },
672
+ { id: "pending", label: "Local Drafts", kind: "collection", parentId: "um-dir", detail: "Repo-local curations waiting to push", count: pending.length },
673
+ { id: "pulled", label: "Pulled Context", kind: "collection", parentId: "um-dir", detail: "Latest shared server snapshot", count: pulled.length },
674
+ { id: "fixes", label: "Known Fixes", kind: "collection", parentId: "um-dir", detail: "Pulled fix knowledge", count: fixes.length },
675
+ { id: "hub", label: "Hub Entries", kind: "collection", parentId: "um-dir", detail: "Installed reusable bundles", count: hubEntries.length },
676
+ { id: "connectors", label: "Connectors", kind: "collection", parentId: "um-dir", detail: "Installed repo connectors", count: connectors.length },
677
+ { id: "runs", label: "Connector Runs", kind: "collection", parentId: "um-dir", detail: "Connector execution reports", count: connectorRuns.length },
678
+ { id: "tasks", label: "Tasks", kind: "collection", parentId: "um-dir", detail: "Local transport-aware task queue", count: tasks.length },
679
+ {
680
+ id: "runtime",
681
+ label: "Runtime",
682
+ kind: "runtime",
683
+ parentId: spaceId,
684
+ detail: `${transport.status} transport | provider ${activeProvider?.name ?? "none"} | model ${configManager.config?.activeModel ?? "none"}`,
685
+ },
686
+ {
687
+ id: "runtime-provider",
688
+ label: "Provider",
689
+ kind: "summary",
690
+ parentId: "runtime",
691
+ detail: activeProvider ? `${activeProvider.name} (${activeProvider.kind})` : "No active provider",
692
+ },
693
+ {
694
+ id: "runtime-model",
695
+ label: "Model",
696
+ kind: "summary",
697
+ parentId: "runtime",
698
+ detail: configManager.config?.activeModel ?? "No active model",
699
+ },
700
+ {
701
+ id: "runtime-transport",
702
+ label: "Transport Queue",
703
+ kind: "summary",
704
+ parentId: "runtime",
705
+ detail: `status=${transport.status} | active=${transport.activeTaskId ?? "none"} | queue=${transport.queue.length}`,
706
+ },
707
+ {
708
+ id: "runtime-transport-events",
709
+ label: "Transport Events",
710
+ kind: "summary",
711
+ parentId: "runtime-transport",
712
+ detail: `${transport.events.length} retained transport events`,
713
+ count: transport.events.length,
714
+ },
715
+ ...Object.entries(transport.taskKinds ?? {}).map(([kind, count]) => ({
716
+ id: `runtime-task-kind-${kind}`,
717
+ label: `Task Kind: ${kind}`,
718
+ kind: "summary",
719
+ parentId: "tasks",
720
+ detail: `${count} recorded ${kind} task${count === 1 ? "" : "s"}`,
721
+ count,
722
+ })),
723
+ ],
724
+ };
725
+ await writeJsonFile(getContextTreeStateFile(repoRoot), next);
726
+ return next;
727
+ }
728
+ export async function createTask(input, cwd = process.cwd()) {
729
+ const { repoRoot } = await getRepoContext(cwd);
730
+ await ensureUmDir(repoRoot);
731
+ const tasks = await readJsonFile(getTasksFile(repoRoot), []);
732
+ const now = new Date().toISOString();
733
+ const task = {
734
+ id: randomUUID(),
735
+ createdAt: now,
736
+ updatedAt: now,
737
+ finishedAt: null,
738
+ startedAt: input.status === "running" ? now : null,
739
+ input: input.focus ?? null,
740
+ result: null,
741
+ streamingContent: null,
742
+ reasoningContents: [],
743
+ toolCalls: [],
744
+ ...input,
745
+ };
746
+ tasks.unshift(task);
747
+ await writeJsonFile(getTasksFile(repoRoot), tasks.slice(0, 100));
748
+ await syncTransportFromTasks(cwd);
749
+ await recordTransportEvent({
750
+ kind: "task-created",
751
+ title: task.title,
752
+ detail: task.detail,
753
+ status: task.status === "failed" ? "failure" : task.status === "completed" ? "success" : "info",
754
+ taskId: task.id,
755
+ }, cwd);
756
+ if (task.status === "running") {
757
+ await recordTransportEvent({
758
+ kind: "task-started",
759
+ title: task.title,
760
+ detail: task.detail,
761
+ status: "info",
762
+ taskId: task.id,
763
+ }, cwd);
764
+ }
765
+ await rebuildContextTreeState(cwd);
766
+ return task;
767
+ }
768
+ export async function updateTask(taskId, updater, cwd = process.cwd()) {
769
+ const { repoRoot } = await getRepoContext(cwd);
770
+ const tasks = await readJsonFile(getTasksFile(repoRoot), []);
771
+ const nextTasks = tasks.map((task) => task.id === taskId
772
+ ? updater({
773
+ ...task,
774
+ updatedAt: new Date().toISOString(),
775
+ })
776
+ : task);
777
+ await writeJsonFile(getTasksFile(repoRoot), nextTasks);
778
+ await syncTransportFromTasks(cwd);
779
+ await rebuildContextTreeState(cwd);
780
+ return nextTasks.find((task) => task.id === taskId) ?? null;
781
+ }
782
+ export async function patchTaskLifecycle(taskId, patch, cwd = process.cwd()) {
783
+ const nextTask = await updateTask(taskId, (task) => ({
784
+ ...task,
785
+ status: patch.status,
786
+ detail: patch.detail ?? task.detail,
787
+ result: patch.result ?? task.result ?? null,
788
+ sessionId: patch.sessionId ?? task.sessionId ?? null,
789
+ startedAt: patch.status === "running" ? task.startedAt ?? new Date().toISOString() : task.startedAt ?? null,
790
+ streamingContent: patch.streamingContent ?? task.streamingContent ?? null,
791
+ finishedAt: patch.status === "completed" || patch.status === "failed"
792
+ ? new Date().toISOString()
793
+ : task.finishedAt ?? null,
794
+ updatedAt: new Date().toISOString(),
795
+ }), cwd);
796
+ if (!nextTask)
797
+ return null;
798
+ const title = nextTask.title;
799
+ if (patch.status === "running") {
800
+ await recordTransportEvent({
801
+ kind: "task-started",
802
+ title,
803
+ detail: patch.reason ?? patch.detail ?? nextTask.detail ?? null,
804
+ status: "info",
805
+ taskId,
806
+ }, cwd);
807
+ }
808
+ else if (patch.status === "completed") {
809
+ await recordTransportEvent({
810
+ kind: "task-completed",
811
+ title,
812
+ detail: patch.reason ?? patch.result ?? nextTask.result ?? null,
813
+ status: "success",
814
+ taskId,
815
+ }, cwd);
816
+ }
817
+ else if (patch.status === "failed") {
818
+ await recordTransportEvent({
819
+ kind: "task-failed",
820
+ title,
821
+ detail: patch.reason ?? patch.detail ?? nextTask.detail ?? null,
822
+ status: "failure",
823
+ taskId,
824
+ }, cwd);
825
+ }
826
+ return nextTask;
827
+ }
828
+ export async function appendTaskStreamingContent(taskId, content, cwd = process.cwd()) {
829
+ return updateTask(taskId, (task) => ({
830
+ ...task,
831
+ streamingContent: `${task.streamingContent ?? ""}${content}`,
832
+ updatedAt: new Date().toISOString(),
833
+ }), cwd);
834
+ }
835
+ export async function addTaskReasoningContent(taskId, content, isThinking = false, cwd = process.cwd()) {
836
+ return updateTask(taskId, (task) => ({
837
+ ...task,
838
+ reasoningContents: [
839
+ ...(task.reasoningContents ?? []),
840
+ {
841
+ content,
842
+ isThinking,
843
+ timestamp: new Date().toISOString(),
844
+ },
845
+ ],
846
+ updatedAt: new Date().toISOString(),
847
+ }), cwd);
848
+ }
849
+ export async function addTaskToolCall(taskId, toolCall, cwd = process.cwd()) {
850
+ return updateTask(taskId, (task) => ({
851
+ ...task,
852
+ toolCalls: [
853
+ ...(task.toolCalls ?? []),
854
+ {
855
+ ...toolCall,
856
+ timestamp: new Date().toISOString(),
857
+ },
858
+ ],
859
+ updatedAt: new Date().toISOString(),
860
+ }), cwd);
861
+ }
862
+ export async function completeTask(taskId, status, detail, cwd = process.cwd()) {
863
+ return patchTaskLifecycle(taskId, {
864
+ status,
865
+ detail: detail ?? null,
866
+ reason: detail ?? null,
867
+ result: detail ?? null,
868
+ }, cwd);
869
+ }
870
+ export async function addConnectorRun(input, cwd = process.cwd()) {
871
+ const { repoRoot } = await getRepoContext(cwd);
872
+ await ensureUmDir(repoRoot);
873
+ const runs = await readJsonFile(getConnectorRunsFile(repoRoot), []);
874
+ const entry = {
875
+ id: randomUUID(),
876
+ ranAt: new Date().toISOString(),
877
+ ...input,
878
+ };
879
+ runs.unshift(entry);
880
+ await writeJsonFile(getConnectorRunsFile(repoRoot), runs.slice(0, 25));
881
+ await updateRepoContext((state) => ({
882
+ ...state,
883
+ updatedAt: new Date().toISOString(),
884
+ }), cwd);
885
+ await syncTransportFromTasks(cwd, { lastRuntimeCheckAt: new Date().toISOString() });
886
+ await recordTransportEvent({
887
+ kind: "runtime-check",
888
+ title: `Connector run: ${entry.connectorName}`,
889
+ detail: entry.summary,
890
+ status: entry.status === "success" ? "success" : "failure",
891
+ taskId: null,
892
+ }, cwd);
893
+ await rebuildContextTreeState(cwd);
894
+ return entry;
895
+ }
896
+ export async function writeConnectorRunReport(run, cwd = process.cwd()) {
897
+ const { repoRoot } = await getRepoContext(cwd);
898
+ const dir = await ensureSubDir(repoRoot, CONNECTOR_RUN_REPORTS_DIR);
899
+ const filePath = path.join(dir, `${run.id}.md`);
900
+ const markdown = [
901
+ `# ${run.connectorName}`,
902
+ "",
903
+ `- Status: ${run.status}`,
904
+ `- Ran At: ${run.ranAt}`,
905
+ `- Connector ID: ${run.connectorId}`,
906
+ "",
907
+ "## Summary",
908
+ "",
909
+ run.summary,
910
+ "",
911
+ ].join("\n");
912
+ await fs.writeFile(filePath, markdown, "utf8");
913
+ return filePath;
914
+ }
915
+ export async function getSessionState(cwd = process.cwd()) {
916
+ const { repoRoot } = await getRepoContext(cwd);
917
+ return normalizeSessionState(await readJsonFile(getSessionFile(repoRoot), null));
918
+ }
919
+ export async function ensureSessionState(cwd = process.cwd()) {
920
+ const { repoRoot } = await getRepoContext(cwd);
921
+ await ensureUmDir(repoRoot);
922
+ const existing = normalizeSessionState(await readJsonFile(getSessionFile(repoRoot), null));
923
+ if (existing) {
924
+ await writeJsonFile(getSessionFile(repoRoot), existing);
925
+ return existing;
926
+ }
927
+ const now = new Date().toISOString();
928
+ const next = {
929
+ version: 1,
930
+ id: randomUUID(),
931
+ startedAt: now,
932
+ updatedAt: now,
933
+ commandHistory: [],
934
+ recentQueries: [],
935
+ recentCurations: [],
936
+ lastSummary: null,
937
+ currentPanel: null,
938
+ currentFocus: null,
939
+ panelHistory: [],
940
+ recentProviderIds: [],
941
+ recentModels: [],
942
+ recentHubSlugs: [],
943
+ recentConnectorSources: [],
944
+ events: [],
945
+ };
946
+ await writeJsonFile(getSessionFile(repoRoot), next);
947
+ return next;
948
+ }
949
+ export async function resetSessionState(cwd = process.cwd()) {
950
+ const { repoRoot } = await getRepoContext(cwd);
951
+ await ensureUmDir(repoRoot);
952
+ const now = new Date().toISOString();
953
+ const next = {
954
+ version: 1,
955
+ id: randomUUID(),
956
+ startedAt: now,
957
+ updatedAt: now,
958
+ commandHistory: [],
959
+ recentQueries: [],
960
+ recentCurations: [],
961
+ lastSummary: "Started a fresh Context session.",
962
+ currentPanel: null,
963
+ currentFocus: null,
964
+ panelHistory: [],
965
+ recentProviderIds: [],
966
+ recentModels: [],
967
+ recentHubSlugs: [],
968
+ recentConnectorSources: [],
969
+ events: [
970
+ {
971
+ id: randomUUID(),
972
+ at: now,
973
+ kind: "session",
974
+ title: "Started a fresh Context session",
975
+ detail: "Local .um data stayed in place, but the live shell history was reset.",
976
+ panel: "session",
977
+ focus: null,
978
+ status: "info",
979
+ },
980
+ ],
981
+ };
982
+ await writeJsonFile(getSessionFile(repoRoot), next);
983
+ return next;
984
+ }
985
+ export async function updateSessionState(updater, cwd = process.cwd()) {
986
+ const { repoRoot } = await getRepoContext(cwd);
987
+ const current = normalizeSessionState(await ensureSessionState(cwd));
988
+ if (!current) {
989
+ throw new Error("Could not initialize repo session state.");
990
+ }
991
+ const next = normalizeSessionState(await updater(current));
992
+ if (!next) {
993
+ throw new Error("Could not update repo session state.");
994
+ }
995
+ await writeJsonFile(getSessionFile(repoRoot), next);
996
+ return next;
997
+ }
998
+ export async function recordSessionCommand(command, cwd = process.cwd()) {
999
+ return updateSessionState((state) => ({
1000
+ ...state,
1001
+ updatedAt: new Date().toISOString(),
1002
+ commandHistory: [...(state.commandHistory ?? []), command].slice(-50),
1003
+ }), cwd);
1004
+ }
1005
+ export async function recordSessionQuery(query, cwd = process.cwd()) {
1006
+ return updateSessionState((state) => ({
1007
+ ...state,
1008
+ updatedAt: new Date().toISOString(),
1009
+ recentQueries: [query, ...(state.recentQueries ?? []).filter((entry) => entry !== query)].slice(0, 10),
1010
+ }), cwd);
1011
+ }
1012
+ export async function recordSessionCuration(content, cwd = process.cwd()) {
1013
+ return updateSessionState((state) => ({
1014
+ ...state,
1015
+ updatedAt: new Date().toISOString(),
1016
+ recentCurations: [content, ...(state.recentCurations ?? []).filter((entry) => entry !== content)].slice(0, 10),
1017
+ }), cwd);
1018
+ }
1019
+ export async function setSessionSummary(summary, cwd = process.cwd()) {
1020
+ return updateSessionState((state) => ({
1021
+ ...state,
1022
+ updatedAt: new Date().toISOString(),
1023
+ lastSummary: summary,
1024
+ }), cwd);
1025
+ }
1026
+ export async function setSessionPanel(panel, focus, cwd = process.cwd()) {
1027
+ return updateSessionState((state) => ({
1028
+ ...state,
1029
+ updatedAt: new Date().toISOString(),
1030
+ currentPanel: panel,
1031
+ currentFocus: focus ?? null,
1032
+ panelHistory: panel
1033
+ ? [panel, ...(state.panelHistory ?? []).filter((entry) => entry !== panel)].slice(0, 10)
1034
+ : (state.panelHistory ?? []),
1035
+ }), cwd);
1036
+ }
1037
+ export async function recordSessionProvider(providerId, cwd = process.cwd()) {
1038
+ return updateSessionState((state) => ({
1039
+ ...state,
1040
+ updatedAt: new Date().toISOString(),
1041
+ recentProviderIds: [providerId, ...(state.recentProviderIds ?? []).filter((entry) => entry !== providerId)].slice(0, 5),
1042
+ }), cwd);
1043
+ }
1044
+ export async function recordSessionModel(modelId, cwd = process.cwd()) {
1045
+ return updateSessionState((state) => ({
1046
+ ...state,
1047
+ updatedAt: new Date().toISOString(),
1048
+ recentModels: [modelId, ...(state.recentModels ?? []).filter((entry) => entry !== modelId)].slice(0, 5),
1049
+ }), cwd);
1050
+ }
1051
+ export async function recordSessionHubEntry(slug, cwd = process.cwd()) {
1052
+ return updateSessionState((state) => ({
1053
+ ...state,
1054
+ updatedAt: new Date().toISOString(),
1055
+ recentHubSlugs: [slug, ...(state.recentHubSlugs ?? []).filter((entry) => entry !== slug)].slice(0, 5),
1056
+ }), cwd);
1057
+ }
1058
+ export async function recordSessionConnector(source, cwd = process.cwd()) {
1059
+ return updateSessionState((state) => ({
1060
+ ...state,
1061
+ updatedAt: new Date().toISOString(),
1062
+ recentConnectorSources: [source, ...(state.recentConnectorSources ?? []).filter((entry) => entry !== source)].slice(0, 5),
1063
+ }), cwd);
1064
+ }
1065
+ export async function recordSessionEvent(event, cwd = process.cwd()) {
1066
+ return updateSessionState((state) => ({
1067
+ ...state,
1068
+ updatedAt: new Date().toISOString(),
1069
+ events: [
1070
+ {
1071
+ id: randomUUID(),
1072
+ at: new Date().toISOString(),
1073
+ ...event,
1074
+ },
1075
+ ...(state.events ?? []),
1076
+ ].slice(0, 30),
1077
+ }), cwd);
162
1078
  }
163
1079
  export function summarizeLocalMemoryMatches(entries, query) {
164
1080
  const needle = query.trim().toLowerCase();