@north-light/crouter 0.3.219 → 0.3.221

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 (155) hide show
  1. package/dist/api/client.d.ts +9 -0
  2. package/dist/api/client.js +10 -0
  3. package/dist/api/dto/chat-inventory.d.ts +43 -0
  4. package/dist/api/dto/chat-inventory.js +11 -0
  5. package/dist/api/dto/profiles.d.ts +19 -5
  6. package/dist/api/dto/profiles.js +2 -1
  7. package/dist/api/index.d.ts +1 -0
  8. package/dist/api/index.js +1 -0
  9. package/dist/api/routes.d.ts +1 -0
  10. package/dist/api/routes.js +1 -0
  11. package/dist/build-root.d.ts +2 -6
  12. package/dist/build-root.js +51 -4
  13. package/dist/builtin-memory/internal/agent-shaping.md +3 -1
  14. package/dist/builtin-memory/internal/memory-loading.md +4 -0
  15. package/dist/builtin-memory/plan/roadmap.md +7 -1
  16. package/dist/builtin-memory/spec/guide.md +7 -1
  17. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +1 -1
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -1
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +7 -2
  20. package/dist/clients/attach/__tests__/ref-autocomplete.test.js +1 -1
  21. package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -1
  22. package/dist/clients/attach/overlays/file-review.js +2 -2
  23. package/dist/clients/attach/session/keys.d.ts +1 -1
  24. package/dist/clients/attach/session/profile-files.js +1 -1
  25. package/dist/clients/attach/viewer.js +690 -690
  26. package/dist/clients/inbox/review/launch.d.ts +8 -4
  27. package/dist/clients/inbox/review/launch.js +55 -5
  28. package/dist/clients/inbox/review/review-client.d.ts +1 -0
  29. package/dist/clients/inbox/review/review-client.js +4 -0
  30. package/dist/clients/inbox/review-adapter.d.ts +1 -8
  31. package/dist/clients/inbox/review-adapter.js +4 -52
  32. package/dist/commands/memory/lint.js +2 -1
  33. package/dist/commands/memory/read.js +1 -0
  34. package/dist/commands/memory.js +1 -1
  35. package/dist/commands/pkg/market-manage.js +165 -75
  36. package/dist/commands/pkg/plugin-inspect.js +19 -2
  37. package/dist/commands/pkg/plugin-manage.d.ts +8 -3
  38. package/dist/commands/pkg/plugin-manage.js +72 -24
  39. package/dist/commands/profile/default.js +6 -10
  40. package/dist/commands/profile/list.js +5 -3
  41. package/dist/commands/profile/new.js +21 -8
  42. package/dist/commands/profile/project.js +25 -19
  43. package/dist/commands/profile/show.js +3 -3
  44. package/dist/commands/surface-inbox.js +1 -0
  45. package/dist/commands/sys/__tests__/migrate.test.js +16 -5
  46. package/dist/commands/sys/doctor.js +35 -5
  47. package/dist/commands/sys/migrate.js +38 -19
  48. package/dist/commands/sys/panels/broker-limits-panel.js +3 -3
  49. package/dist/commands/sys/setup-core.js +1 -1
  50. package/dist/commands/sys/sync-project-guidance.js +1 -1
  51. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +1 -1
  52. package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +24 -0
  53. package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -18
  54. package/dist/core/__tests__/fixtures/fake-engine.js +8 -1
  55. package/dist/core/__tests__/helpers/broker-clients.d.ts +1 -0
  56. package/dist/core/__tests__/helpers/broker-clients.js +1 -0
  57. package/dist/core/__tests__/inline-memory-refs.test.js +36 -2
  58. package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
  59. package/dist/core/__tests__/seam/dormancy-release.test.js +37 -1
  60. package/dist/core/__tests__/serial/broker-sdk-wiring.test.js +102 -2
  61. package/dist/core/bootstrap.js +6 -0
  62. package/dist/core/canvas/browse/app.js +5 -2
  63. package/dist/core/canvas/browse/model.d.ts +25 -15
  64. package/dist/core/canvas/browse/model.js +86 -65
  65. package/dist/core/canvas/render-source.d.ts +6 -0
  66. package/dist/core/canvas/render-source.js +7 -1
  67. package/dist/core/canvas/render.js +10 -2
  68. package/dist/core/command-hooks/artifact.d.ts +10 -0
  69. package/dist/core/command-hooks/artifact.js +129 -0
  70. package/dist/core/command-hooks/catalog.d.ts +14 -0
  71. package/dist/core/command-hooks/catalog.js +38 -0
  72. package/dist/core/command-hooks/compose.d.ts +15 -0
  73. package/dist/core/command-hooks/compose.js +99 -0
  74. package/dist/core/command-hooks/discovery.d.ts +87 -0
  75. package/dist/core/command-hooks/discovery.js +174 -0
  76. package/dist/core/command-hooks/help.d.ts +5 -0
  77. package/dist/core/command-hooks/help.js +18 -0
  78. package/dist/core/command-hooks/index.d.ts +6 -0
  79. package/dist/core/command-hooks/index.js +6 -0
  80. package/dist/core/command-hooks/report.d.ts +23 -0
  81. package/dist/core/command-hooks/report.js +19 -0
  82. package/dist/core/command-hooks/schema.d.ts +27 -0
  83. package/dist/core/command-hooks/schema.js +68 -0
  84. package/dist/core/command-hooks/transport/exec-invoke.d.ts +22 -0
  85. package/dist/core/command-hooks/transport/exec-invoke.js +274 -0
  86. package/dist/core/command-plugins/presence.d.ts +2 -0
  87. package/dist/core/command-plugins/presence.js +17 -0
  88. package/dist/core/command-plugins/transport/exec-invoke.d.ts +5 -0
  89. package/dist/core/command-plugins/transport/exec-invoke.js +58 -5
  90. package/dist/core/command.d.ts +8 -1
  91. package/dist/core/command.js +12 -10
  92. package/dist/core/help.d.ts +7 -1
  93. package/dist/core/io.d.ts +9 -1
  94. package/dist/core/io.js +44 -2
  95. package/dist/core/memory/inline-ref-inventory.d.ts +2 -1
  96. package/dist/core/memory/inline-ref-inventory.js +15 -8
  97. package/dist/core/memory-resolver.d.ts +13 -1
  98. package/dist/core/memory-resolver.js +25 -19
  99. package/dist/core/profiles/manifest.d.ts +13 -2
  100. package/dist/core/profiles/manifest.js +84 -18
  101. package/dist/core/profiles/select.js +9 -9
  102. package/dist/core/render.js +11 -0
  103. package/dist/core/runtime/advertised-command-invocation.d.ts +20 -0
  104. package/dist/core/runtime/advertised-command-invocation.js +233 -0
  105. package/dist/core/runtime/bearings.js +1 -1
  106. package/dist/core/runtime/broker/client-registry.d.ts +6 -3
  107. package/dist/core/runtime/broker/client-registry.js +6 -4
  108. package/dist/core/runtime/broker/event-projection.js +7 -0
  109. package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
  110. package/dist/core/runtime/broker/frame-dispatch.js +16 -10
  111. package/dist/core/runtime/broker/read-ops.d.ts +4 -0
  112. package/dist/core/runtime/broker/read-ops.js +6 -2
  113. package/dist/core/runtime/broker-extension-render.js +1 -1
  114. package/dist/core/runtime/broker-inventory.d.ts +5 -0
  115. package/dist/core/runtime/broker-inventory.js +191 -0
  116. package/dist/core/runtime/broker-protocol.d.ts +13 -2
  117. package/dist/core/runtime/broker.js +10 -1
  118. package/dist/core/runtime/command-surface.d.ts +33 -0
  119. package/dist/core/runtime/command-surface.js +81 -0
  120. package/dist/core/runtime/node-read.js +5 -0
  121. package/dist/core/scope.d.ts +26 -1
  122. package/dist/core/scope.js +52 -12
  123. package/dist/core/substrate/on-read.d.ts +7 -1
  124. package/dist/core/substrate/on-read.js +13 -4
  125. package/dist/core/substrate/render.js +14 -5
  126. package/dist/core/substrate/schema.d.ts +11 -1
  127. package/dist/core/substrate/schema.js +11 -2
  128. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +4 -4
  129. package/dist/daemon/api/handlers/chat-inventory.d.ts +2 -0
  130. package/dist/daemon/api/handlers/chat-inventory.js +25 -0
  131. package/dist/daemon/api/handlers/profiles.js +7 -1
  132. package/dist/daemon/api/map.d.ts +2 -1
  133. package/dist/daemon/api/map.js +3 -2
  134. package/dist/daemon/api/server.js +2 -0
  135. package/dist/hook-authoring.d.ts +75 -0
  136. package/dist/hook-authoring.js +358 -0
  137. package/dist/hook-process.d.ts +7 -0
  138. package/dist/hook-process.js +34 -0
  139. package/dist/index.d.ts +2 -0
  140. package/dist/index.js +2 -0
  141. package/dist/migrations/002-profile-project-memory.d.ts +2 -0
  142. package/dist/migrations/002-profile-project-memory.js +71 -0
  143. package/dist/migrations/profile-manifests.d.ts +30 -0
  144. package/dist/migrations/profile-manifests.js +70 -0
  145. package/dist/migrations/registry.js +10 -5
  146. package/dist/migrations/types.d.ts +28 -1
  147. package/dist/migrations/types.js +15 -9
  148. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +21 -4
  149. package/dist/pi-extensions/canvas-structured-output.js +85 -2
  150. package/dist/types.d.ts +15 -6
  151. package/dist/types.js +5 -1
  152. package/package.json +1 -1
  153. package/runtime.lock.json +2 -2
  154. package/dist/clients/attach/__tests__/file-review-focus.test.js +0 -49
  155. /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/profile-project-memory-delivery.test.d.ts} +0 -0
@@ -1,4 +1,5 @@
1
1
  import { type InstalledPlugin, type Scope } from '../types.js';
2
+ import type { ProfileProjectMemory } from '../api/dto/profiles.js';
2
3
  /**
3
4
  * Thin memory-document resolver for the document substrate. Resolution has
4
5
  * two passes. First, scopes/sources are tried NEAREST-FIRST (node > project
@@ -17,6 +18,11 @@ import { type InstalledPlugin, type Scope } from '../types.js';
17
18
  * a selected profile's `projects` manifest entries — see `findProjectScopeRoots`
18
19
  * in scope.ts), profile is the selected profile's own singleton `memory/` store
19
20
  * (from `CRTR_PROFILE_ID`), and user/builtin remain singleton scopes.
21
+ *
22
+ * Every doc also carries its project store's profile relationship ceiling
23
+ * (`projectMemory`) for the automatic delivery paths to clamp against.
24
+ * Resolution itself never consults it: the full corpus resolves and lists
25
+ * regardless of the ceiling.
20
26
  */
21
27
  export interface MemoryDoc {
22
28
  /** Resolver identity: the doc's explicit frontmatter `name` when present,
@@ -41,6 +47,12 @@ export interface MemoryDoc {
41
47
  * managed by `crtr pkg`, not writable/deletable as scope docs — consumers
42
48
  * that mutate the store gate on this. */
43
49
  plugin?: string;
50
+ /** The selected profile relationship's ceiling for the project store this doc
51
+ * came from: the maximum rung AUTOMATIC boot and workspace-open delivery may
52
+ * disclose it at. Always present; `content` (neutral, i.e. uncapped) for
53
+ * every non-project scope and for a project root no profile entry
54
+ * contributes. Resolution, listing, and targeted reads ignore it entirely. */
55
+ projectMemory: ProfileProjectMemory;
44
56
  }
45
57
  /** The memory-only scope union: the global `Scope` (`user|project|builtin`)
46
58
  * plus `profile` and `node`. Confined to the memory resolver/list/read/write/
@@ -107,7 +119,7 @@ export declare function listMemoryDocs(scope: MemoryScope, quiet?: boolean): Mem
107
119
  * namespace. Walks `pluginMemoryDir(plugin)` recursively for *.md, deriving each
108
120
  * doc's name exactly as `listMemoryDocs` does (path-relative, no extension,
109
121
  * slash-separated) then prefixing the plugin name. Builtin has no plugins. */
110
- export declare function listPluginMemoryDocs(plugin: InstalledPlugin, scope: MemoryScope, quiet?: boolean): MemoryDoc[];
122
+ export declare function listPluginMemoryDocs(plugin: InstalledPlugin, scope: MemoryScope, quiet?: boolean, projectMemory?: ProfileProjectMemory): MemoryDoc[];
111
123
  /** All project-scoped docs visible from an explicit node workspace/profile — the
112
124
  * project-only slice of a `MemoryTarget` view, used where only workspace docs
113
125
  * are wanted (a workspace-open render). For a full-precedence target-addressed
@@ -6,7 +6,7 @@ import { parseFrontmatterGeneric } from './frontmatter.js';
6
6
  import { listInstalledPlugins, listInstalledPluginsInRoot, parseSkillQualifier } from './resolver.js';
7
7
  import { ambiguous, notFound, usage } from './errors.js';
8
8
  import { warn } from './output.js';
9
- import { pluginMemoryDir, projectScopeRoots, scopeMemoryDir } from './scope.js';
9
+ import { NEUTRAL_PROJECT_MEMORY, pluginMemoryDir, projectScopeRoots, projectScopeRootsWithMemory, scopeMemoryDir } from './scope.js';
10
10
  import { effectiveDocKind, normalizeDocName, normalizeNameSegment, resolveDocName } from './substrate/schema.js';
11
11
  import { loadProfileManifest, profileMemoryDir } from './profiles/manifest.js';
12
12
  import { memoryDir as nodeMemoryDir } from './runtime/memory.js';
@@ -74,22 +74,26 @@ function memorySourcesInPrecedence(target, scope, includeDescendants = false) {
74
74
  const out = [];
75
75
  for (const s of scopesInPrecedence(target, scope)) {
76
76
  if (s === 'project') {
77
- const roots = projectScopeRoots(target.cwd, target.profileId);
78
- for (const root of roots) {
77
+ const annotated = projectScopeRootsWithMemory(target.cwd, target.profileId);
78
+ for (const { root, memory } of annotated) {
79
79
  out.push({
80
80
  scope: 'project',
81
81
  memoryDir: join(root, 'memory'),
82
82
  plugins: listInstalledPluginsInRoot('project', root).filter((p) => p.enabled),
83
+ projectMemory: memory,
83
84
  });
84
85
  }
85
86
  // Descendant stores rank after every ancestor source (and before
86
87
  // profile): an ancestor doc always beats a nested doc on a name tie.
88
+ // They carry the neutral ceiling — no profile entry names them, and
89
+ // descendant discovery is off for every automatic delivery path.
87
90
  if (includeDescendants) {
88
- for (const root of descendantStoreRoots(roots)) {
91
+ for (const root of descendantStoreRoots(annotated.map(({ root: r }) => r))) {
89
92
  out.push({
90
93
  scope: 'project',
91
94
  memoryDir: join(root, 'memory'),
92
95
  plugins: listInstalledPluginsInRoot('project', root).filter((p) => p.enabled),
96
+ projectMemory: NEUTRAL_PROJECT_MEMORY,
93
97
  });
94
98
  }
95
99
  }
@@ -99,7 +103,7 @@ function memorySourcesInPrecedence(target, scope, includeDescendants = false) {
99
103
  if (profileId !== '') {
100
104
  try {
101
105
  const { profileId: resolvedId } = loadProfileManifest(profileId);
102
- out.push({ scope: 'profile', memoryDir: profileMemoryDir(resolvedId), plugins: [] });
106
+ out.push({ scope: 'profile', memoryDir: profileMemoryDir(resolvedId), plugins: [], projectMemory: NEUTRAL_PROJECT_MEMORY });
103
107
  }
104
108
  catch {
105
109
  // Missing/deleted/invalid selected profile: no profile memory source.
@@ -111,27 +115,28 @@ function memorySourcesInPrecedence(target, scope, includeDescendants = false) {
111
115
  // Silently absent when the target names no node.
112
116
  const nodeId = target.nodeId ?? '';
113
117
  if (nodeId !== '')
114
- out.push({ scope: 'node', memoryDir: nodeMemoryDir(nodeId), plugins: [] });
118
+ out.push({ scope: 'node', memoryDir: nodeMemoryDir(nodeId), plugins: [], projectMemory: NEUTRAL_PROJECT_MEMORY });
115
119
  }
116
120
  else {
117
121
  out.push({
118
122
  scope: s,
119
123
  memoryDir: scopeMemoryDir(s),
120
124
  plugins: listInstalledPlugins(s).filter((p) => p.enabled),
125
+ projectMemory: NEUTRAL_PROJECT_MEMORY,
121
126
  });
122
127
  }
123
128
  }
124
129
  return out;
125
130
  }
126
- function loadMemoryDoc(scope, root, path, fallbackName) {
131
+ function loadMemoryDoc(scope, root, path, fallbackName, projectMemory = NEUTRAL_PROJECT_MEMORY) {
127
132
  const { data, body } = parseFrontmatterGeneric(readText(path));
128
- return { name: resolveDocName(data, fallbackName), scope, path, root, frontmatter: data, body };
133
+ return { name: resolveDocName(data, fallbackName), scope, path, root, frontmatter: data, body, projectMemory };
129
134
  }
130
135
  /** All memory docs in one memory/ dir, scanned recursively for *.md (topical
131
136
  * subdirs supported), sorted by resolver identity (explicit frontmatter name
132
137
  * when present, else path-derived). SKILL.md bundles are legacy Agent Skills
133
138
  * and are ignored; crouter memory docs are plain .md files under memory/. */
134
- function listMemoryDocsInDir(scope, dir, quiet = false) {
139
+ function listMemoryDocsInDir(scope, dir, quiet = false, projectMemory = NEUTRAL_PROJECT_MEMORY) {
135
140
  if (!dir || !pathExists(dir))
136
141
  return [];
137
142
  const docs = [];
@@ -171,7 +176,7 @@ function listMemoryDocsInDir(scope, dir, quiet = false) {
171
176
  // `quiet` suppresses the notice for a targeted resolve (a leaf-name read),
172
177
  // where another doc's health is irrelevant noise before the result.
173
178
  try {
174
- docs.push(loadMemoryDoc(scope, dir, path, name));
179
+ docs.push(loadMemoryDoc(scope, dir, path, name, projectMemory));
175
180
  }
176
181
  catch (e) {
177
182
  const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
@@ -214,7 +219,7 @@ export function listMemoryDocs(scope, quiet = false) {
214
219
  * namespace. Walks `pluginMemoryDir(plugin)` recursively for *.md, deriving each
215
220
  * doc's name exactly as `listMemoryDocs` does (path-relative, no extension,
216
221
  * slash-separated) then prefixing the plugin name. Builtin has no plugins. */
217
- export function listPluginMemoryDocs(plugin, scope, quiet = false) {
222
+ export function listPluginMemoryDocs(plugin, scope, quiet = false, projectMemory = NEUTRAL_PROJECT_MEMORY) {
218
223
  const dir = pluginMemoryDir(plugin);
219
224
  if (!pathExists(dir))
220
225
  return [];
@@ -225,7 +230,7 @@ export function listPluginMemoryDocs(plugin, scope, quiet = false) {
225
230
  continue;
226
231
  const name = `${plugin.name}/${derived}`;
227
232
  try {
228
- docs.push({ ...loadMemoryDoc(scope, dir, file, name), plugin: plugin.name });
233
+ docs.push({ ...loadMemoryDoc(scope, dir, file, name, projectMemory), plugin: plugin.name });
229
234
  }
230
235
  catch (e) {
231
236
  const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
@@ -241,8 +246,8 @@ export function listPluginMemoryDocs(plugin, scope, quiet = false) {
241
246
  * (consults one source's docs at a time, nearest source first). */
242
247
  function sourceMemoryDocs(source, quiet = false) {
243
248
  return [
244
- ...listMemoryDocsInDir(source.scope, source.memoryDir, quiet),
245
- ...source.plugins.flatMap((p) => listPluginMemoryDocs(p, source.scope, quiet)),
249
+ ...listMemoryDocsInDir(source.scope, source.memoryDir, quiet, source.projectMemory),
250
+ ...source.plugins.flatMap((p) => listPluginMemoryDocs(p, source.scope, quiet, source.projectMemory)),
246
251
  ];
247
252
  }
248
253
  /** All project-scoped docs visible from an explicit node workspace/profile — the
@@ -250,10 +255,11 @@ function sourceMemoryDocs(source, quiet = false) {
250
255
  * are wanted (a workspace-open render). For a full-precedence target-addressed
251
256
  * lookup, use `resolveMemoryDocForTarget`. */
252
257
  export function listProjectMemoryDocs(startDir = process.cwd(), profileId = selectedProfileId() || null, quiet = false) {
253
- return projectScopeRoots(startDir, profileId).flatMap((root) => sourceMemoryDocs({
258
+ return projectScopeRootsWithMemory(startDir, profileId).flatMap(({ root, memory }) => sourceMemoryDocs({
254
259
  scope: 'project',
255
260
  memoryDir: join(root, 'memory'),
256
261
  plugins: listInstalledPluginsInRoot('project', root).filter((plugin) => plugin.enabled),
262
+ projectMemory: memory,
257
263
  }, quiet));
258
264
  }
259
265
  /** All memory docs across the resolved sources, in precedence order: each
@@ -351,11 +357,11 @@ function findMemoryMatchInSource(name, segments, source, legacyDirectoryIndex =
351
357
  if (dir) {
352
358
  const { filePath, dirPath } = resolveNormalizedPath(dir, segments);
353
359
  if (!isLegacySkillDoc && filePath !== null)
354
- return loadMemoryDoc(source.scope, dir, filePath, name);
360
+ return loadMemoryDoc(source.scope, dir, filePath, name, source.projectMemory);
355
361
  if (legacyDirectoryIndex && dirPath !== null) {
356
362
  const indexPath = join(dirPath, 'INDEX.md');
357
363
  if (pathExists(indexPath))
358
- return loadMemoryDoc(source.scope, dir, indexPath, name);
364
+ return loadMemoryDoc(source.scope, dir, indexPath, name, source.projectMemory);
359
365
  }
360
366
  }
361
367
  // Plugin memory dir: a `<plugin>/<rest>` name resolves against that enabled
@@ -374,14 +380,14 @@ function findMemoryMatchInSource(name, segments, source, legacyDirectoryIndex =
374
380
  if (restSegments.length > 0) {
375
381
  const { filePath } = resolveNormalizedPath(pdir, restSegments);
376
382
  if (restSegments.at(-1) !== 'SKILL' && filePath !== null)
377
- return loadMemoryDoc(source.scope, pdir, filePath, name);
383
+ return loadMemoryDoc(source.scope, pdir, filePath, name, source.projectMemory);
378
384
  }
379
385
  if (legacyDirectoryIndex) {
380
386
  const indexDir = rest === '' ? pdir : resolveNormalizedPath(pdir, restSegments).dirPath;
381
387
  if (indexDir !== null) {
382
388
  const indexPath = join(indexDir, 'INDEX.md');
383
389
  if (pathExists(indexPath))
384
- return loadMemoryDoc(source.scope, pdir, indexPath, name);
390
+ return loadMemoryDoc(source.scope, pdir, indexPath, name, source.projectMemory);
385
391
  }
386
392
  }
387
393
  }
@@ -1,9 +1,16 @@
1
1
  import type { ProfileManifest } from '../../types.js';
2
+ import type { ProfileProject, ProfileProjectMemory } from '../../api/dto/profiles.js';
2
3
  export declare const ROOT_PROFILE_ID = "root-00000000";
3
4
  export declare function profilesRoot(): string;
4
5
  export declare function profileRoot(profileId: string): string;
5
6
  export declare function profileManifestPath(profileId: string): string;
6
7
  export declare function profileMemoryDir(profileId: string): string;
8
+ /** Reject anything a valid project array may not carry. Shape only: takes
9
+ * `unknown` because an API body and a hand-edited manifest both prove nothing
10
+ * at runtime, and deliberately says nothing about whether a path exists —
11
+ * existence and realpath resolution belong to the create/add paths alone, so
12
+ * a request naming a since-deleted directory still validates. */
13
+ export declare function assertProfileProjects(entries: unknown): asserts entries is ProfileProject[];
7
14
  /** The directory a node running under `profileId` is pinned to — the profile's
8
15
  * home, but only while it still exists on disk (a node pinned to a deleted dir
9
16
  * cannot boot). Null for the root profile, a profile with no projects, a home
@@ -52,7 +59,7 @@ export declare function assertProfileMetadata(entries: unknown): asserts entries
52
59
  * Never throws; on an env-name collision the first entry wins (matching
53
60
  * injection, where a deterministic winner beats insertion-order luck). */
54
61
  export declare function sanitizeProfileMetadata(stored: unknown): Record<string, string>;
55
- export declare function createProfile(name: string, projects?: string[], opts?: {
62
+ export declare function createProfile(name: string, projects?: ProfileProject[], opts?: {
56
63
  defaultKind?: string;
57
64
  metadata?: Record<string, string>;
58
65
  }): ProfileEntry;
@@ -80,7 +87,11 @@ export declare function updateProfileMetadata(profileId: string, set: Record<str
80
87
  * over a bad profile id (mirrors `readProfileEnvVars`). */
81
88
  export declare function readProfileMetadata(profileId: string | null): Record<string, string>;
82
89
  export declare function renameProfile(profileId: string, name: string): ProfileEntry;
83
- export declare function addProfileProject(profileId: string, dir: string, opts?: {
90
+ /** Upsert: a new path appends, an existing one keeps its position and takes
91
+ * the supplied memory value. Every caller names a memory value — a profile
92
+ * relationship never acquires one by default. */
93
+ export declare function addProfileProject(profileId: string, dir: string, opts: {
94
+ memory: ProfileProjectMemory;
84
95
  home?: boolean;
85
96
  }): ProfileEntry;
86
97
  export declare function removeProfileProject(profileId: string, dir: string): ProfileEntry;
@@ -9,6 +9,7 @@ import { randomUUID } from 'node:crypto';
9
9
  import { existsSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
10
10
  import { dirname, join, relative, resolve as resolvePath, sep } from 'node:path';
11
11
  import { SCHEMA_VERSION, PROFILE_DIR } from '../../types.js';
12
+ import { PROFILE_PROJECT_MEMORY_VALUES } from '../../api/dto/profiles.js';
12
13
  import { userScopeRoot, resetScopeCache } from '../scope.js';
13
14
  import { ensureDir, nowIso } from '../fs-utils.js';
14
15
  import { usage, notFound, ambiguous, general } from '../errors.js';
@@ -84,15 +85,68 @@ export function profileMemoryDir(profileId) {
84
85
  * the user started them, not in whatever dir was last added to its purview. */
85
86
  function normalizeHome(profileId, manifest) {
86
87
  const stored = typeof manifest.home === 'string' && manifest.home !== '' ? manifest.home : null;
87
- const home = profileId === ROOT_PROFILE_ID ? stored : stored ?? manifest.projects[0] ?? null;
88
+ const home = profileId === ROOT_PROFILE_ID ? stored : stored ?? manifest.projects[0]?.path ?? null;
88
89
  const pausedAt = typeof manifest.paused_at === 'string' && manifest.paused_at !== '' ? manifest.paused_at : null;
89
90
  return { ...manifest, home, paused_at: pausedAt };
90
91
  }
92
+ /** Reject anything a valid project array may not carry. Shape only: takes
93
+ * `unknown` because an API body and a hand-edited manifest both prove nothing
94
+ * at runtime, and deliberately says nothing about whether a path exists —
95
+ * existence and realpath resolution belong to the create/add paths alone, so
96
+ * a request naming a since-deleted directory still validates. */
97
+ export function assertProfileProjects(entries) {
98
+ const memories = PROFILE_PROJECT_MEMORY_VALUES.join(', ');
99
+ if (!Array.isArray(entries)) {
100
+ throw usage('projects must be an array of {path, memory} objects', {
101
+ received: typeof entries,
102
+ field: 'projects',
103
+ next: `Pass entries like {"path": "/abs/dir", "memory": "content"} — memory is one of: ${memories}.`,
104
+ });
105
+ }
106
+ for (const entry of entries) {
107
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
108
+ throw usage(`invalid project entry: ${JSON.stringify(entry)}`, {
109
+ field: 'projects',
110
+ next: 'Every project is an object like {"path": "/abs/dir", "memory": "content"}.',
111
+ });
112
+ }
113
+ const { path, memory } = entry;
114
+ if (typeof path !== 'string' || path.trim() === '') {
115
+ throw usage(`project entry has no path: ${JSON.stringify(entry)}`, {
116
+ field: 'projects',
117
+ next: 'Give every project entry a non-empty directory path.',
118
+ });
119
+ }
120
+ if (typeof memory !== 'string' || !PROFILE_PROJECT_MEMORY_VALUES.includes(memory)) {
121
+ throw usage(`invalid project memory value: ${JSON.stringify(memory)}`, {
122
+ received: String(memory),
123
+ field: 'projects',
124
+ next: `Set memory on ${path} to one of: ${memories}.`,
125
+ });
126
+ }
127
+ }
128
+ }
129
+ function hasCurrentProjectShape(projects) {
130
+ try {
131
+ assertProfileProjects(projects);
132
+ return true;
133
+ }
134
+ catch {
135
+ return false;
136
+ }
137
+ }
138
+ /** A manifest whose projects are not the current object shape is unreadable —
139
+ * the same treatment as corrupt JSON, so nothing downstream ever guesses a
140
+ * memory value. `crtr sys migrate` reads raw bytes and is the only converter
141
+ * of pre-object manifests. */
91
142
  function readManifestFile(path, profileId) {
92
143
  if (!existsSync(path))
93
144
  return null;
94
145
  try {
95
- return normalizeHome(profileId, JSON.parse(readFileSync(path, 'utf8')));
146
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
147
+ if (!hasCurrentProjectShape(parsed.projects))
148
+ return null;
149
+ return normalizeHome(profileId, parsed);
96
150
  }
97
151
  catch {
98
152
  return null;
@@ -231,14 +285,15 @@ function resolveExistingProjectDir(dir) {
231
285
  return abs;
232
286
  }
233
287
  }
234
- function dedupeOrdered(values) {
288
+ /** First-wins by resolved path, manifest order preserved. */
289
+ function dedupeProjects(entries) {
235
290
  const seen = new Set();
236
291
  const out = [];
237
- for (const v of values) {
238
- if (seen.has(v))
292
+ for (const entry of entries) {
293
+ if (seen.has(entry.path))
239
294
  continue;
240
- seen.add(v);
241
- out.push(v);
295
+ seen.add(entry.path);
296
+ out.push(entry);
242
297
  }
243
298
  return out;
244
299
  }
@@ -374,14 +429,15 @@ export function createProfile(name, projects = [], opts = {}) {
374
429
  throw usage('profile name must not be empty');
375
430
  if (opts.metadata !== undefined)
376
431
  assertProfileMetadata(opts.metadata);
377
- const resolvedProjects = dedupeOrdered(projects.map(resolveExistingProjectDir));
432
+ assertProfileProjects(projects);
433
+ const resolvedProjects = dedupeProjects(projects.map((p) => ({ path: resolveExistingProjectDir(p.path), memory: p.memory })));
378
434
  const profileId = generateProfileId(trimmed);
379
435
  return withProfileManifestLock(profileId, () => {
380
436
  const manifest = {
381
437
  schema_version: SCHEMA_VERSION,
382
438
  name: trimmed,
383
439
  projects: resolvedProjects,
384
- home: resolvedProjects[0] ?? null,
440
+ home: resolvedProjects[0]?.path ?? null,
385
441
  paused_at: null,
386
442
  ...(opts.defaultKind !== undefined ? { default_kind: opts.defaultKind } : {}),
387
443
  ...(opts.metadata !== undefined && Object.keys(opts.metadata).length > 0 ? { metadata: { ...opts.metadata } } : {}),
@@ -510,13 +566,23 @@ export function renameProfile(profileId, name) {
510
566
  resetScopeCache();
511
567
  return result;
512
568
  }
513
- export function addProfileProject(profileId, dir, opts = {}) {
569
+ /** Upsert: a new path appends, an existing one keeps its position and takes
570
+ * the supplied memory value. Every caller names a memory value — a profile
571
+ * relationship never acquires one by default. */
572
+ export function addProfileProject(profileId, dir, opts) {
573
+ assertProfileProjects([{ path: dir, memory: opts.memory }]);
514
574
  const resolved = resolveExistingProjectDir(dir);
515
- const result = mutateManifest(profileId, (m) => ({
516
- ...m,
517
- projects: dedupeOrdered([...m.projects, resolved]),
518
- ...(opts.home === true ? { home: resolved } : {}),
519
- }));
575
+ const result = mutateManifest(profileId, (m) => {
576
+ const existing = m.projects.some((p) => p.path === resolved);
577
+ const projects = existing
578
+ ? m.projects.map((p) => (p.path === resolved ? { ...p, memory: opts.memory } : p))
579
+ : dedupeProjects([...m.projects, { path: resolved, memory: opts.memory }]);
580
+ return {
581
+ ...m,
582
+ projects,
583
+ ...(opts.home === true ? { home: resolved } : {}),
584
+ };
585
+ });
520
586
  resetScopeCache();
521
587
  return result;
522
588
  }
@@ -534,15 +600,15 @@ export function removeProfileProject(profileId, dir) {
534
600
  /* dir no longer exists — fall back to comparing by its resolved path */
535
601
  }
536
602
  const result = mutateManifest(profileId, (m) => {
537
- const filtered = m.projects.filter((p) => p !== real && p !== abs);
603
+ const filtered = m.projects.filter((p) => p.path !== real && p.path !== abs);
538
604
  // Dropping the home dir from purview falls the home back to the first
539
605
  // remaining project, exactly as `normalizeHome` would for a manifest that
540
606
  // never named one — never leaves the profile pointing outside its purview.
541
- const home = m.home === real || m.home === abs ? filtered[0] ?? null : m.home;
607
+ const home = m.home === real || m.home === abs ? filtered[0]?.path ?? null : m.home;
542
608
  if (filtered.length === m.projects.length) {
543
609
  throw notFound(`project not found on profile: ${dir}`, {
544
610
  received: dir,
545
- next: `Current projects: ${m.projects.length > 0 ? m.projects.join(', ') : '(none)'}.`,
611
+ next: `Current projects: ${m.projects.length > 0 ? m.projects.map((p) => p.path).join(', ') : '(none)'}.`,
546
612
  });
547
613
  }
548
614
  return { ...m, projects: filtered, home };
@@ -79,7 +79,7 @@ function projectCovers(cwd, project) {
79
79
  * `crtr profile default set`, which rejects pinning a profile that doesn't
80
80
  * cover cwd (the selector would ignore/self-heal such a pin anyway). */
81
81
  export function profileCoversCwd(entry, cwd) {
82
- return entry.manifest.projects.some((p) => projectCovers(cwd, p));
82
+ return entry.manifest.projects.some((p) => projectCovers(cwd, p.path));
83
83
  }
84
84
  /** `a` more recent than `b`, treating null as the oldest possible value (never
85
85
  * wins over a real timestamp). ISO 8601 strings compare correctly lexically. */
@@ -161,7 +161,7 @@ function purviewSummary(entry) {
161
161
  const first = entry.manifest.projects[0];
162
162
  if (first === undefined)
163
163
  return relativeUsed(entry.manifest.last_used_at);
164
- return n > 1 ? `${tildify(first)} \u00b7 ${n} dirs total` : tildify(first);
164
+ return n > 1 ? `${tildify(first.path)} \u00b7 ${n} dirs total` : tildify(first.path);
165
165
  }
166
166
  /** ▸ marks the highlighted row. */
167
167
  const HILITE = accent('\u25b8');
@@ -330,7 +330,7 @@ async function offerAddDirToProfile(rl, entry, cwd) {
330
330
  .toLowerCase();
331
331
  if (choice === 'n' || choice === 'no')
332
332
  return;
333
- addProfileProject(entry.profileId, cwd);
333
+ addProfileProject(entry.profileId, cwd, { memory: 'content' });
334
334
  process.stdout.write(` ${accent('\u2713')} ${dim(`added this directory to "${entry.manifest.name}"`)}\n`);
335
335
  }
336
336
  /** Search-and-pick among EVERY existing profile (root excluded — it is the
@@ -355,7 +355,7 @@ async function promptSelectExistingProfile(bindings) {
355
355
  const matchesOf = () => filter === ''
356
356
  ? all
357
357
  : all.filter((e) => e.manifest.name.toLowerCase().includes(filter) ||
358
- e.manifest.projects.some((p) => tildify(p).toLowerCase().includes(filter)));
358
+ e.manifest.projects.some((p) => tildify(p.path).toLowerCase().includes(filter)));
359
359
  return new Promise((resolve) => {
360
360
  let prevLines = 0;
361
361
  const draw = () => {
@@ -447,7 +447,7 @@ async function createProfileHere(rl, cwd) {
447
447
  const defaultName = basename(cwd) || 'profile';
448
448
  const nameInput = (await rl.question(caret(`name the new profile [${defaultName}]`))).trim();
449
449
  const name = nameInput !== '' ? nameInput : defaultName;
450
- const profileId = createProfile(name, [cwd]).profileId;
450
+ const profileId = createProfile(name, [{ path: cwd, memory: 'content' }]).profileId;
451
451
  process.stdout.write(` ${accent('\u2713')} ${dim(`created profile "${name}"`)}\n`);
452
452
  return profileId;
453
453
  }
@@ -524,9 +524,9 @@ async function promptPickProfileOrCreate(candidates, cwd, exact, pinnedId, bindi
524
524
  const detail = exact
525
525
  ? `${relativeUsed(entry.manifest.last_used_at)}${n > 1 ? ` \u00b7 ${n} dirs total` : ''}`
526
526
  : (() => {
527
- const md = entry.manifest.projects.find((p) => projectCovers(cwd, p)) ??
527
+ const md = entry.manifest.projects.find((p) => projectCovers(cwd, p.path)) ??
528
528
  entry.manifest.projects[0];
529
- return n > 1 ? `matches ${tildify(md)} \u00b7 ${n} dirs total` : `matches ${tildify(md)}`;
529
+ return n > 1 ? `matches ${tildify(md.path)} \u00b7 ${n} dirs total` : `matches ${tildify(md.path)}`;
530
530
  })();
531
531
  const defaultText = pinned !== null && ordered[0] === pinned ? 'default (pinned)' : 'default';
532
532
  return {
@@ -573,7 +573,7 @@ async function promptPickProfileOrCreate(candidates, cwd, exact, pinnedId, bindi
573
573
  process.stdout.write(` ${accent('\u2713')} ${dim(`"${picked.manifest.name}" is now the default here`)}\n`);
574
574
  }
575
575
  else if (res.action === 'crtr.profile.menu.add-directory') {
576
- addProfileProject(picked.profileId, cwd);
576
+ addProfileProject(picked.profileId, cwd, { memory: 'content' });
577
577
  process.stdout.write(` ${accent('\u2713')} ${dim(`added this directory to "${picked.manifest.name}"`)}\n`);
578
578
  }
579
579
  return picked.profileId;
@@ -621,7 +621,7 @@ export async function selectProfileForCwd(cwd, explicitProfile, forcePicker = fa
621
621
  return entry.profileId;
622
622
  }
623
623
  const covering = listProfiles().filter((p) => profileCoversCwd(p, resolvedCwd));
624
- const exact = covering.filter((p) => p.manifest.projects.some((proj) => proj === resolvedCwd));
624
+ const exact = covering.filter((p) => p.manifest.projects.some((proj) => proj.path === resolvedCwd));
625
625
  // A per-cwd pinned default (set from the menu with `d`) is the user's
626
626
  // explicit "default to THIS profile HERE". It outranks both the exact-single
627
627
  // auto-pick and global-MRU, so long as it still covers cwd; a stale pin
@@ -32,6 +32,13 @@ function scalarStr(v) {
32
32
  return JSON.stringify(v);
33
33
  return String(v);
34
34
  }
35
+ function fencedJson(value) {
36
+ const json = JSON.stringify(value, null, 2);
37
+ let fence = '```';
38
+ while (json.includes(fence))
39
+ fence += '`';
40
+ return `${fence}json\n${json}\n${fence}`;
41
+ }
35
42
  /** A string field is prose when it is named as such, spans lines, or is long
36
43
  * enough that it reads as a sentence rather than an identifier. */
37
44
  function isProse(name, v) {
@@ -122,6 +129,10 @@ export function renderError(p) {
122
129
  // the value the caller supplied.
123
130
  if (p.http_status !== undefined)
124
131
  lines.push(`http status: ${p.http_status}`);
132
+ if ('primaryCompleted' in p && p.primaryCompleted === true) {
133
+ lines.push('primaryCompleted: true');
134
+ lines.push(`Primary result (the command already completed):\n${fencedJson(p.result)}`);
135
+ }
125
136
  lines.push(`Next: ${p.next}`);
126
137
  return `<error code="${attrEsc(p.error)}">\n${lines.join('\n')}\n</error>`;
127
138
  }
@@ -0,0 +1,20 @@
1
+ import type { BrokerSession } from './broker/read-ops.js';
2
+ export interface AdvertisedCommandInvocationMetadata {
3
+ crouterAdvertisedCommandInvocation: {
4
+ raw: string;
5
+ };
6
+ }
7
+ /** Restore extension-owned details while projecting persisted invocation
8
+ * metadata onto the runtime custom message. */
9
+ export declare function hydratePersistedAdvertisedCommandMessages(messages: readonly unknown[]): void;
10
+ /** Add live invocation metadata without changing extension-owned content or
11
+ * details. EventProjection calls this before relaying a custom message. */
12
+ export declare function hydrateLiveAdvertisedCommandMessage(message: unknown): void;
13
+ /** Bind the imminent message_end persistence call to the exact queued app
14
+ * message rather than to a payload-equality search across pending queues. */
15
+ export declare function prepareAdvertisedCommandPersistence(session: BrokerSession, message: unknown): void;
16
+ /** Install persistence/reload plumbing once on a newly built broker session. */
17
+ export declare function installAdvertisedCommandInvocationContract(session: BrokerSession): void;
18
+ /** Preserve a chat-advertised extension command's exact submitted text on the
19
+ * first displayed custom message its handler persists. */
20
+ export declare function promptWithAdvertisedCommandInvocation<T>(session: BrokerSession, raw: string, runPrompt: () => Promise<T>): Promise<T>;