@north-light/crouter 0.3.220 → 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.
- package/dist/api/client.d.ts +9 -0
- package/dist/api/client.js +10 -0
- package/dist/api/dto/chat-inventory.d.ts +43 -0
- package/dist/api/dto/chat-inventory.js +11 -0
- package/dist/api/dto/profiles.d.ts +19 -5
- package/dist/api/dto/profiles.js +2 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/build-root.d.ts +2 -6
- package/dist/build-root.js +51 -4
- package/dist/builtin-memory/internal/agent-shaping.md +3 -1
- package/dist/builtin-memory/internal/memory-loading.md +4 -0
- package/dist/builtin-memory/plan/roadmap.md +7 -1
- package/dist/builtin-memory/spec/guide.md +7 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +7 -2
- package/dist/clients/attach/__tests__/ref-autocomplete.test.js +1 -1
- package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -1
- package/dist/clients/attach/overlays/file-review.js +2 -2
- package/dist/clients/attach/session/keys.d.ts +1 -1
- package/dist/clients/attach/session/profile-files.js +1 -1
- package/dist/clients/attach/viewer.js +690 -690
- package/dist/clients/inbox/review/launch.d.ts +8 -4
- package/dist/clients/inbox/review/launch.js +55 -5
- package/dist/clients/inbox/review/review-client.d.ts +1 -0
- package/dist/clients/inbox/review/review-client.js +4 -0
- package/dist/clients/inbox/review-adapter.d.ts +1 -8
- package/dist/clients/inbox/review-adapter.js +4 -52
- package/dist/commands/memory/lint.js +2 -1
- package/dist/commands/memory/read.js +1 -0
- package/dist/commands/memory.js +1 -1
- package/dist/commands/pkg/market-manage.js +165 -75
- package/dist/commands/pkg/plugin-inspect.js +19 -2
- package/dist/commands/pkg/plugin-manage.d.ts +8 -3
- package/dist/commands/pkg/plugin-manage.js +72 -24
- package/dist/commands/profile/default.js +6 -10
- package/dist/commands/profile/list.js +5 -3
- package/dist/commands/profile/new.js +21 -8
- package/dist/commands/profile/project.js +25 -19
- package/dist/commands/profile/show.js +3 -3
- package/dist/commands/surface-inbox.js +1 -0
- package/dist/commands/sys/__tests__/migrate.test.js +16 -5
- package/dist/commands/sys/doctor.js +35 -5
- package/dist/commands/sys/migrate.js +38 -19
- package/dist/commands/sys/setup-core.js +1 -1
- package/dist/commands/sys/sync-project-guidance.js +1 -1
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +1 -1
- package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +24 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -18
- package/dist/core/__tests__/fixtures/fake-engine.js +8 -1
- package/dist/core/__tests__/inline-memory-refs.test.js +36 -2
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
- package/dist/core/__tests__/serial/broker-sdk-wiring.test.js +102 -2
- package/dist/core/bootstrap.js +6 -0
- package/dist/core/canvas/browse/app.js +5 -2
- package/dist/core/canvas/browse/model.d.ts +25 -15
- package/dist/core/canvas/browse/model.js +86 -65
- package/dist/core/canvas/render-source.d.ts +6 -0
- package/dist/core/canvas/render-source.js +7 -1
- package/dist/core/canvas/render.js +10 -2
- package/dist/core/command-hooks/artifact.d.ts +10 -0
- package/dist/core/command-hooks/artifact.js +129 -0
- package/dist/core/command-hooks/catalog.d.ts +14 -0
- package/dist/core/command-hooks/catalog.js +38 -0
- package/dist/core/command-hooks/compose.d.ts +15 -0
- package/dist/core/command-hooks/compose.js +99 -0
- package/dist/core/command-hooks/discovery.d.ts +87 -0
- package/dist/core/command-hooks/discovery.js +174 -0
- package/dist/core/command-hooks/help.d.ts +5 -0
- package/dist/core/command-hooks/help.js +18 -0
- package/dist/core/command-hooks/index.d.ts +6 -0
- package/dist/core/command-hooks/index.js +6 -0
- package/dist/core/command-hooks/report.d.ts +23 -0
- package/dist/core/command-hooks/report.js +19 -0
- package/dist/core/command-hooks/schema.d.ts +27 -0
- package/dist/core/command-hooks/schema.js +68 -0
- package/dist/core/command-hooks/transport/exec-invoke.d.ts +22 -0
- package/dist/core/command-hooks/transport/exec-invoke.js +274 -0
- package/dist/core/command-plugins/presence.d.ts +2 -0
- package/dist/core/command-plugins/presence.js +17 -0
- package/dist/core/command-plugins/transport/exec-invoke.d.ts +5 -0
- package/dist/core/command-plugins/transport/exec-invoke.js +58 -5
- package/dist/core/command.d.ts +8 -1
- package/dist/core/command.js +12 -10
- package/dist/core/help.d.ts +7 -1
- package/dist/core/io.d.ts +9 -1
- package/dist/core/io.js +44 -2
- package/dist/core/memory/inline-ref-inventory.d.ts +2 -1
- package/dist/core/memory/inline-ref-inventory.js +15 -8
- package/dist/core/memory-resolver.d.ts +13 -1
- package/dist/core/memory-resolver.js +25 -19
- package/dist/core/profiles/manifest.d.ts +13 -2
- package/dist/core/profiles/manifest.js +84 -18
- package/dist/core/profiles/select.js +9 -9
- package/dist/core/render.js +11 -0
- package/dist/core/runtime/advertised-command-invocation.d.ts +20 -0
- package/dist/core/runtime/advertised-command-invocation.js +233 -0
- package/dist/core/runtime/bearings.js +1 -1
- package/dist/core/runtime/broker/event-projection.js +7 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
- package/dist/core/runtime/broker/frame-dispatch.js +15 -10
- package/dist/core/runtime/broker/read-ops.d.ts +4 -0
- package/dist/core/runtime/broker/read-ops.js +6 -2
- package/dist/core/runtime/broker-extension-render.js +1 -1
- package/dist/core/runtime/broker-inventory.d.ts +5 -0
- package/dist/core/runtime/broker-inventory.js +191 -0
- package/dist/core/runtime/broker-protocol.d.ts +9 -2
- package/dist/core/runtime/broker.js +10 -1
- package/dist/core/runtime/command-surface.d.ts +33 -0
- package/dist/core/runtime/command-surface.js +81 -0
- package/dist/core/runtime/node-read.js +5 -0
- package/dist/core/scope.d.ts +26 -1
- package/dist/core/scope.js +52 -12
- package/dist/core/substrate/on-read.d.ts +7 -1
- package/dist/core/substrate/on-read.js +13 -4
- package/dist/core/substrate/render.js +14 -5
- package/dist/core/substrate/schema.d.ts +11 -1
- package/dist/core/substrate/schema.js +11 -2
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +4 -4
- package/dist/daemon/api/handlers/chat-inventory.d.ts +2 -0
- package/dist/daemon/api/handlers/chat-inventory.js +25 -0
- package/dist/daemon/api/handlers/profiles.js +7 -1
- package/dist/daemon/api/map.d.ts +2 -1
- package/dist/daemon/api/map.js +3 -2
- package/dist/daemon/api/server.js +2 -0
- package/dist/hook-authoring.d.ts +75 -0
- package/dist/hook-authoring.js +358 -0
- package/dist/hook-process.d.ts +7 -0
- package/dist/hook-process.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/migrations/002-profile-project-memory.d.ts +2 -0
- package/dist/migrations/002-profile-project-memory.js +71 -0
- package/dist/migrations/profile-manifests.d.ts +30 -0
- package/dist/migrations/profile-manifests.js +70 -0
- package/dist/migrations/registry.js +10 -5
- package/dist/migrations/types.d.ts +28 -1
- package/dist/migrations/types.js +15 -9
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +21 -4
- package/dist/pi-extensions/canvas-structured-output.js +85 -2
- package/dist/types.d.ts +15 -6
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/clients/attach/__tests__/file-review-focus.test.js +0 -49
- /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/profile-project-memory-delivery.test.d.ts} +0 -0
package/dist/core/help.d.ts
CHANGED
|
@@ -216,6 +216,12 @@ export interface LeafHelp {
|
|
|
216
216
|
* default preview behaviour. */
|
|
217
217
|
preview?: PreviewMeta;
|
|
218
218
|
}
|
|
219
|
+
/** Invocation-local help view for one hook-eligible core leaf. It shares every
|
|
220
|
+
* non-effects field with the frozen core help and owns a separately frozen
|
|
221
|
+
* effects list that describes the effective pipeline. */
|
|
222
|
+
export type EffectiveLeafHelp = Readonly<Omit<LeafHelp, 'effects'> & {
|
|
223
|
+
effects: readonly string[];
|
|
224
|
+
}>;
|
|
219
225
|
/** Build a self-named runtime-state element: `<tag attr="v">body</tag>`. The
|
|
220
226
|
* subtree that owns the state authors it through this, so the tag name and any
|
|
221
227
|
* scalar metadata (e.g. a count) travel with the data and render identically
|
|
@@ -225,4 +231,4 @@ export interface LeafHelp {
|
|
|
225
231
|
export declare function stateBlock(tag: string, attrs: Record<string, string | number>, body: string): string;
|
|
226
232
|
export declare function renderRoot(h: RootHelp): string;
|
|
227
233
|
export declare function renderBranch(h: BranchHelp): string;
|
|
228
|
-
export declare function renderLeafArgv(h: LeafHelp): string;
|
|
234
|
+
export declare function renderLeafArgv(h: LeafHelp | EffectiveLeafHelp): string;
|
package/dist/core/io.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export declare function setJsonOutput(v: boolean): void;
|
|
|
30
30
|
export declare function isJsonOutput(): boolean;
|
|
31
31
|
/** Structured error payload. `error` is a stable code the agent branches on;
|
|
32
32
|
* `next` is the recovery road sign. */
|
|
33
|
-
|
|
33
|
+
interface ErrorPayloadBase {
|
|
34
34
|
error: string;
|
|
35
35
|
message: string;
|
|
36
36
|
received?: unknown;
|
|
@@ -41,6 +41,13 @@ export interface ErrorPayload {
|
|
|
41
41
|
http_status?: number;
|
|
42
42
|
next: string;
|
|
43
43
|
}
|
|
44
|
+
export type ErrorPayload = ErrorPayloadBase & ({
|
|
45
|
+
primaryCompleted: true;
|
|
46
|
+
result: Record<string, unknown>;
|
|
47
|
+
} | {
|
|
48
|
+
primaryCompleted?: never;
|
|
49
|
+
result?: never;
|
|
50
|
+
});
|
|
44
51
|
/** A command-level failure: surfaces as the JSON response on stdout. */
|
|
45
52
|
export declare class InputError extends CrtrError {
|
|
46
53
|
payload: ErrorPayload;
|
|
@@ -94,3 +101,4 @@ export declare function apiErrorToCliError(err: ApiError, next?: string): CrtrEr
|
|
|
94
101
|
* Runtime/internal failures go to stderr as `{error:"internal"}` — raw traces
|
|
95
102
|
* never reach the agent. Exits non-zero either way. */
|
|
96
103
|
export declare function handle(e: unknown): void;
|
|
104
|
+
export {};
|
package/dist/core/io.js
CHANGED
|
@@ -193,16 +193,58 @@ function payloadOf(e) {
|
|
|
193
193
|
if (e instanceof InputError)
|
|
194
194
|
return e.payload;
|
|
195
195
|
const d = (e.details !== undefined ? e.details : {});
|
|
196
|
+
const result = d.primaryCompleted === true
|
|
197
|
+
&& d.result !== null
|
|
198
|
+
&& typeof d.result === 'object'
|
|
199
|
+
&& !Array.isArray(d.result)
|
|
200
|
+
? jsonSafeResult(d.result)
|
|
201
|
+
: undefined;
|
|
196
202
|
const next = d.next !== undefined
|
|
197
203
|
? d.next
|
|
198
204
|
: 'Inspect the error and adjust the call. See -h for the schema.';
|
|
199
|
-
|
|
205
|
+
const base = {
|
|
200
206
|
error: e.code,
|
|
201
207
|
message: e.message,
|
|
202
208
|
received: d.received,
|
|
203
209
|
field: d.field,
|
|
204
210
|
...(typeof d.http_status === 'number' ? { http_status: d.http_status } : {}),
|
|
205
|
-
|
|
211
|
+
};
|
|
212
|
+
return result === undefined
|
|
213
|
+
? { ...base, next }
|
|
214
|
+
: { ...base, primaryCompleted: true, result, next };
|
|
215
|
+
}
|
|
216
|
+
/** Detach completed results from getters/prototypes and keep every error sink
|
|
217
|
+
* JSON-safe. Tagged values preserve a truthful representation when a trusted
|
|
218
|
+
* primary violates its declared JSON result contract. */
|
|
219
|
+
function jsonSafeResult(result) {
|
|
220
|
+
const seen = new WeakSet();
|
|
221
|
+
try {
|
|
222
|
+
const encoded = JSON.stringify(result, (_key, value) => {
|
|
223
|
+
if (typeof value === 'bigint')
|
|
224
|
+
return { $crtrType: 'bigint', value: value.toString() };
|
|
225
|
+
if (typeof value === 'number' && !Number.isFinite(value))
|
|
226
|
+
return { $crtrType: 'number', value: String(value) };
|
|
227
|
+
if (typeof value === 'undefined' || typeof value === 'function' || typeof value === 'symbol') {
|
|
228
|
+
return { $crtrType: typeof value };
|
|
229
|
+
}
|
|
230
|
+
if (value !== null && typeof value === 'object') {
|
|
231
|
+
if (seen.has(value))
|
|
232
|
+
return { $crtrType: 'circular' };
|
|
233
|
+
seen.add(value);
|
|
234
|
+
}
|
|
235
|
+
return value;
|
|
236
|
+
});
|
|
237
|
+
if (encoded !== undefined) {
|
|
238
|
+
const decoded = JSON.parse(encoded);
|
|
239
|
+
if (decoded !== null && typeof decoded === 'object' && !Array.isArray(decoded)) {
|
|
240
|
+
return decoded;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch { /* a throwing getter/toJSON must not suppress the partial failure */ }
|
|
245
|
+
return {
|
|
246
|
+
$crtrType: 'unavailable',
|
|
247
|
+
message: 'The completed primary result could not be represented as JSON.',
|
|
206
248
|
};
|
|
207
249
|
}
|
|
208
250
|
// ---------------------------------------------------------------------------
|
|
@@ -6,7 +6,8 @@ import type { RefMeta } from '../runtime/broker-protocol.js';
|
|
|
6
6
|
* (`refs`, stably sorted by winning scope then name) plus the resolution set
|
|
7
7
|
* (`names` — doc names plus every proper directory prefix, because a bare-dir
|
|
8
8
|
* ref is a legal listing link) that both the broker's per-submission resolver
|
|
9
|
-
* and `buildGuidance` test tokens against.
|
|
9
|
+
* and `buildGuidance` test tokens against. `refs` additionally labels Gateway
|
|
10
|
+
* eligibility; the terminal viewer still receives every referenceable row.
|
|
10
11
|
*/
|
|
11
12
|
export declare function buildRefInventory(): {
|
|
12
13
|
refs: RefMeta[];
|
|
@@ -30,8 +30,9 @@
|
|
|
30
30
|
// `/reload` — this module itself holds no cache beyond the session parse
|
|
31
31
|
// cache it reads through.
|
|
32
32
|
import { listAllMemoryDocs } from '../memory-resolver.js';
|
|
33
|
+
import { memoryExtensionEffectiveCatalog, projectEffectiveMemoryExtensions } from './extensions.js';
|
|
33
34
|
import { parseSubstrateDoc } from '../substrate/schema.js';
|
|
34
|
-
import { cachedSubstrateDocsInclusive } from '../substrate/session-cache.js';
|
|
35
|
+
import { cachedAllMemoryDocsInclusive, cachedSubstrateDocsInclusive } from '../substrate/session-cache.js';
|
|
35
36
|
// Display-sort weight matching resolution precedence (node > project >
|
|
36
37
|
// profile > user > builtin) — display-only; resolution itself is
|
|
37
38
|
// name-only (`names.has(candidate)`), never scope-aware.
|
|
@@ -49,21 +50,27 @@ const SCOPE_SORT_RANK = {
|
|
|
49
50
|
* (`refs`, stably sorted by winning scope then name) plus the resolution set
|
|
50
51
|
* (`names` — doc names plus every proper directory prefix, because a bare-dir
|
|
51
52
|
* ref is a legal listing link) that both the broker's per-submission resolver
|
|
52
|
-
* and `buildGuidance` test tokens against.
|
|
53
|
+
* and `buildGuidance` test tokens against. `refs` additionally labels Gateway
|
|
54
|
+
* eligibility; the terminal viewer still receives every referenceable row.
|
|
53
55
|
*/
|
|
54
56
|
export function buildRefInventory() {
|
|
55
|
-
const
|
|
57
|
+
const memoryDocs = cachedAllMemoryDocsInclusive(listAllMemoryDocs);
|
|
58
|
+
const substrateDocs = cachedSubstrateDocsInclusive(listAllMemoryDocs, parseSubstrateDoc);
|
|
59
|
+
const substrateDocsByPath = new Map(substrateDocs.map((doc) => [doc.path, doc]));
|
|
56
60
|
// First-wins by canonical name: docs arrive already in precedence order
|
|
57
61
|
// (listAllMemoryDocs' nearest-first source ordering), so the first doc seen
|
|
58
62
|
// for a name is the winner.
|
|
59
63
|
const winners = new Map();
|
|
60
|
-
for (const
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
for (const memoryDoc of memoryDocs) {
|
|
65
|
+
const doc = substrateDocsByPath.get(memoryDoc.path);
|
|
66
|
+
if (doc !== undefined && !winners.has(doc.name))
|
|
67
|
+
winners.set(doc.name, { doc, memoryDoc });
|
|
63
68
|
}
|
|
64
69
|
const refs = [];
|
|
65
|
-
for (const [name, doc] of winners) {
|
|
66
|
-
|
|
70
|
+
for (const [name, { doc, memoryDoc }] of winners) {
|
|
71
|
+
const gatewayVisible = doc.scope !== 'builtin'
|
|
72
|
+
&& projectEffectiveMemoryExtensions(memoryDoc.frontmatter?.['extensions'], memoryExtensionEffectiveCatalog(memoryDoc))['northlight']?.['visibility'] !== false;
|
|
73
|
+
refs.push({ name, kind: doc.kind, scope: doc.scope, shortForm: doc.shortForm, gatewayVisible });
|
|
67
74
|
}
|
|
68
75
|
refs.sort((a, b) => {
|
|
69
76
|
const scopeDelta = SCOPE_SORT_RANK[a.scope] - SCOPE_SORT_RANK[b.scope];
|
|
@@ -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
|
|
78
|
-
for (const root of
|
|
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(
|
|
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
|
|
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?:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
238
|
-
if (seen.has(
|
|
292
|
+
for (const entry of entries) {
|
|
293
|
+
if (seen.has(entry.path))
|
|
239
294
|
continue;
|
|
240
|
-
seen.add(
|
|
241
|
-
out.push(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|