@north-light/crouter 0.3.217 → 0.3.218

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 (59) hide show
  1. package/dist/api/client.d.ts +5 -2
  2. package/dist/api/client.js +7 -1
  3. package/dist/api/dto/canvas.d.ts +9 -0
  4. package/dist/api/dto/nodes.d.ts +27 -1
  5. package/dist/builtin-memory/04-base-worker.md +7 -1
  6. package/dist/builtin-memory/internal/plugins.md +67 -0
  7. package/dist/builtin-memory/memory-read-orientation.md +13 -0
  8. package/dist/clients/attach/chrome/bash-jobs.js +1 -1
  9. package/dist/clients/attach/photon_rs_bg.wasm +0 -0
  10. package/dist/clients/attach/viewer.js +429 -429
  11. package/dist/commands/human/shared.js +1 -1
  12. package/dist/commands/memory/edit.js +6 -1
  13. package/dist/commands/memory/lint.d.ts +1 -6
  14. package/dist/commands/memory/lint.js +18 -126
  15. package/dist/commands/memory/list.d.ts +1 -0
  16. package/dist/commands/memory/list.js +17 -2
  17. package/dist/commands/memory/read.js +15 -6
  18. package/dist/commands/memory/shared.d.ts +32 -2
  19. package/dist/commands/memory/shared.js +179 -0
  20. package/dist/commands/memory/write.js +6 -1
  21. package/dist/commands/pkg/browse/catalog.js +2 -0
  22. package/dist/commands/pkg/browse/model.d.ts +4 -1
  23. package/dist/commands/pkg/plugin-manage.js +209 -148
  24. package/dist/core/bash-jobs.d.ts +2 -5
  25. package/dist/core/bash-jobs.js +4 -8
  26. package/dist/core/command-plugins/bundle.d.ts +4 -1
  27. package/dist/core/command-plugins/bundle.js +16 -2
  28. package/dist/core/human/component-docs.js +1 -0
  29. package/dist/core/human/scan.d.ts +5 -4
  30. package/dist/core/human/scan.js +7 -4
  31. package/dist/core/io.js +3 -2
  32. package/dist/core/manifest.d.ts +2 -0
  33. package/dist/core/manifest.js +5 -0
  34. package/dist/core/memory/extensions.d.ts +30 -0
  35. package/dist/core/memory/extensions.js +219 -0
  36. package/dist/core/preview-result-path.d.ts +4 -0
  37. package/dist/core/preview-result-path.js +25 -0
  38. package/dist/core/substrate/frontmatter-validation.d.ts +13 -0
  39. package/dist/core/substrate/frontmatter-validation.js +101 -0
  40. package/dist/core/substrate/index.d.ts +1 -0
  41. package/dist/core/substrate/index.js +1 -0
  42. package/dist/daemon/api/__tests__/nodes-activity-query.test.d.ts +1 -0
  43. package/dist/daemon/api/__tests__/nodes-activity-query.test.js +101 -0
  44. package/dist/daemon/api/handlers/canvas.js +3 -0
  45. package/dist/daemon/api/handlers/inbox.js +4 -3
  46. package/dist/daemon/api/handlers/nodes.d.ts +1 -0
  47. package/dist/daemon/api/handlers/nodes.js +101 -19
  48. package/dist/daemon/api/handlers/reports.d.ts +4 -0
  49. package/dist/daemon/api/handlers/reports.js +14 -8
  50. package/dist/daemon/crtrd.js +7 -5
  51. package/dist/daemon/manage.d.ts +3 -0
  52. package/dist/daemon/manage.js +14 -0
  53. package/dist/pi-extensions/canvas-bash-valve.d.ts +4 -3
  54. package/dist/pi-extensions/canvas-bash-valve.js +19 -6
  55. package/dist/pi-extensions/canvas-preview-result.d.ts +0 -8
  56. package/dist/pi-extensions/canvas-preview-result.js +9 -23
  57. package/dist/types.d.ts +31 -0
  58. package/package.json +1 -1
  59. package/runtime.lock.json +2 -2
@@ -6,7 +6,9 @@
6
6
  import { existsSync, readdirSync, realpathSync, rmdirSync, statSync } from 'node:fs';
7
7
  import { dirname, join, resolve as resolvePath } from 'node:path';
8
8
  import { stringify as yamlStringify, parse as yamlParse } from 'yaml';
9
+ import { stateBlock } from '../../core/help.js';
9
10
  import { usage } from '../../core/errors.js';
11
+ import { memoryExtensionValidationCatalog, } from '../../core/memory/extensions.js';
10
12
  import { CRTR_DIR_NAME } from '../../types.js';
11
13
  import { scopeMemoryDir, projectScopeRoot, ensureProjectScopeRoot, resetScopeCache, } from '../../core/scope.js';
12
14
  import { loadProfileManifest, profileMemoryDir } from '../../core/profiles/manifest.js';
@@ -309,6 +311,183 @@ export function serializeMemoryDocLiteral(frontmatter, bodySegment) {
309
311
  const yamlText = yamlStringify(ordered).replace(/\n+$/, '');
310
312
  return `---\n${yamlText}\n---\n${bodySegment}`;
311
313
  }
314
+ /** Resolve declarations at the same scope and path a document will use. The
315
+ * catalog intentionally includes disabled plugins: they still authorize typed
316
+ * edits to values that stay inert until their plugin is enabled again. */
317
+ export function memoryExtensionCatalogForDoc(doc) {
318
+ return memoryExtensionValidationCatalog(doc);
319
+ }
320
+ function declarationValueDescription(declaration) {
321
+ if (declaration.type !== 'enum')
322
+ return declaration.type;
323
+ return `enum (${declaration.values.join('|')})`;
324
+ }
325
+ function extensionHelpValue(value) {
326
+ return typeof value === 'string' ? JSON.stringify(value) : String(value);
327
+ }
328
+ /** A bounded declaration catalog for the two authoring leaves. The manifest is
329
+ * the source for both the type and operation-specific guidance. */
330
+ export function memoryExtensionFieldCatalogHelp(operation) {
331
+ const scope = projectScopeRoot() === null ? 'user' : 'project';
332
+ const memoryDir = scopeMemoryDir(scope);
333
+ if (memoryDir === null)
334
+ return null;
335
+ const catalog = memoryExtensionCatalogForDoc({ scope, path: join(memoryDir, '.extension-help.md') });
336
+ const fields = Object.entries(catalog)
337
+ .flatMap(([namespace, declarations]) => Object.entries(declarations).map(([field, declaration]) => ({ namespace, field, declaration })))
338
+ .sort((left, right) => `${left.namespace}.${left.field}`.localeCompare(`${right.namespace}.${right.field}`));
339
+ if (fields.length === 0)
340
+ return null;
341
+ const maxRows = 50;
342
+ const rows = fields.slice(0, maxRows).map(({ namespace, field, declaration }) => {
343
+ const defaultValue = declaration.default === undefined ? '' : `; default ${extensionHelpValue(declaration.default)}`;
344
+ const help = operation === 'write' ? declaration.write_help : declaration.edit_help;
345
+ return `- \`extensions.${namespace}.${field}\` — ${declarationValueDescription(declaration)}${defaultValue}. ${help}`;
346
+ });
347
+ if (fields.length > maxRows)
348
+ rows.push(`- ${fields.length - maxRows} additional declared fields omitted from help.`);
349
+ return stateBlock('memory-extension-fields', { count: fields.length }, rows.join('\n'));
350
+ }
351
+ function parseExtensionPath(raw, flag) {
352
+ const match = /^extensions\.([^.]+)\.([^.]+)$/.exec(raw);
353
+ if (match === null)
354
+ return `${flag} requires the full path extensions.<plugin>.<field>: ${raw}`;
355
+ return { path: raw, namespace: match[1], field: match[2] };
356
+ }
357
+ function parseExtensionValue(path, raw, declaration) {
358
+ if (declaration.type === 'boolean') {
359
+ if (raw === 'true')
360
+ return true;
361
+ if (raw === 'false')
362
+ return false;
363
+ return `${path} expects boolean true or false, received ${JSON.stringify(raw)}`;
364
+ }
365
+ if (declaration.type === 'number') {
366
+ if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(raw)) {
367
+ return `${path} expects finite numeric syntax, received ${JSON.stringify(raw)}`;
368
+ }
369
+ const value = Number(raw);
370
+ return Number.isFinite(value) ? value : `${path} expects a finite number, received ${JSON.stringify(raw)}`;
371
+ }
372
+ if (declaration.type === 'enum' && !declaration.values.includes(raw)) {
373
+ return `${path} expects one of ${declaration.values.join('|')}, received ${JSON.stringify(raw)}`;
374
+ }
375
+ return raw;
376
+ }
377
+ /** Parse every requested extension mutation before the caller constructs any
378
+ * frontmatter. Set values are deliberately declaration-typed rather than YAML. */
379
+ export function parseRequestedExtensionChanges(setRaw, unsetRaw, catalog) {
380
+ const errors = [];
381
+ const sets = [];
382
+ const unsets = [];
383
+ const setPaths = new Set();
384
+ const unsetPaths = new Set();
385
+ for (const raw of setRaw ?? []) {
386
+ const delimiter = raw.indexOf('=');
387
+ if (delimiter < 0) {
388
+ errors.push(`--extension requires extensions.<plugin>.<field>=VALUE: ${raw}`);
389
+ continue;
390
+ }
391
+ const parsedPath = parseExtensionPath(raw.slice(0, delimiter), '--extension');
392
+ if (typeof parsedPath === 'string') {
393
+ errors.push(parsedPath);
394
+ continue;
395
+ }
396
+ if (setPaths.has(parsedPath.path)) {
397
+ errors.push(`duplicate --extension path: ${parsedPath.path}`);
398
+ continue;
399
+ }
400
+ setPaths.add(parsedPath.path);
401
+ const declaration = catalog[parsedPath.namespace]?.[parsedPath.field];
402
+ if (declaration === undefined) {
403
+ errors.push(`${parsedPath.path} is not declared by an installed plugin`);
404
+ continue;
405
+ }
406
+ const value = parseExtensionValue(parsedPath.path, raw.slice(delimiter + 1), declaration);
407
+ if (typeof value === 'string' && declaration.type !== 'string' && declaration.type !== 'enum') {
408
+ errors.push(value);
409
+ continue;
410
+ }
411
+ if (typeof value === 'string' && declaration.type === 'enum' && !declaration.values.includes(value)) {
412
+ errors.push(`${parsedPath.path} expects one of ${declaration.values.join('|')}, received ${JSON.stringify(value)}`);
413
+ continue;
414
+ }
415
+ sets.push({ ...parsedPath, value });
416
+ }
417
+ for (const raw of unsetRaw ?? []) {
418
+ const parsedPath = parseExtensionPath(raw, '--unset-extension');
419
+ if (typeof parsedPath === 'string') {
420
+ errors.push(parsedPath);
421
+ continue;
422
+ }
423
+ if (unsetPaths.has(parsedPath.path)) {
424
+ errors.push(`duplicate --unset-extension path: ${parsedPath.path}`);
425
+ continue;
426
+ }
427
+ unsetPaths.add(parsedPath.path);
428
+ if (catalog[parsedPath.namespace]?.[parsedPath.field] === undefined) {
429
+ errors.push(`${parsedPath.path} is not declared by an installed plugin`);
430
+ continue;
431
+ }
432
+ unsets.push(parsedPath);
433
+ }
434
+ for (const path of setPaths) {
435
+ if (unsetPaths.has(path))
436
+ errors.push(`${path} was requested in both --extension and --unset-extension`);
437
+ }
438
+ if (errors.length > 0) {
439
+ throw usage(`invalid plugin memory extension request:\n- ${errors.join('\n- ')}`, {
440
+ received: [...(setRaw ?? []), ...(unsetRaw ?? [])].join(', '),
441
+ next: 'Run `crtr memory write -h` or `crtr memory edit -h` for the installed field catalog and value syntax.',
442
+ });
443
+ }
444
+ return { sets, unsets };
445
+ }
446
+ /** Apply only requested extension paths. Existing unknown namespaces remain
447
+ * untouched; an unset prunes the empty namespace and empty root mapping. */
448
+ export function applyExtensionChanges(frontmatter, sets, unsets) {
449
+ if (sets.length === 0 && unsets.length === 0)
450
+ return;
451
+ const current = frontmatter['extensions'];
452
+ if (current !== undefined && (current === null || typeof current !== 'object' || Array.isArray(current))) {
453
+ throw usage('existing extensions frontmatter must be a mapping before extension fields can be changed', {
454
+ received: JSON.stringify(current),
455
+ next: 'Repair the extensions mapping, or make a core-field-only edit that preserves it unchanged.',
456
+ });
457
+ }
458
+ const extensions = { ...(current ?? {}) };
459
+ for (const set of sets) {
460
+ const existing = extensions[set.namespace];
461
+ if (existing !== undefined && (existing === null || typeof existing !== 'object' || Array.isArray(existing))) {
462
+ throw usage(`existing ${`extensions.${set.namespace}`} frontmatter must be a mapping before ${set.path} can change`, {
463
+ received: JSON.stringify(existing),
464
+ next: 'Repair the namespace mapping, or make a core-field-only edit that preserves it unchanged.',
465
+ });
466
+ }
467
+ extensions[set.namespace] = { ...(existing ?? {}), [set.field]: set.value };
468
+ }
469
+ for (const unset of unsets) {
470
+ const existing = extensions[unset.namespace];
471
+ if (existing === undefined)
472
+ continue;
473
+ if (existing === null || typeof existing !== 'object' || Array.isArray(existing)) {
474
+ throw usage(`existing ${`extensions.${unset.namespace}`} frontmatter must be a mapping before ${unset.path} can change`, {
475
+ received: JSON.stringify(existing),
476
+ next: 'Repair the namespace mapping, or make a core-field-only edit that preserves it unchanged.',
477
+ });
478
+ }
479
+ const fields = { ...existing };
480
+ delete fields[unset.field];
481
+ if (Object.keys(fields).length === 0)
482
+ delete extensions[unset.namespace];
483
+ else
484
+ extensions[unset.namespace] = fields;
485
+ }
486
+ if (Object.keys(extensions).length === 0)
487
+ delete frontmatter['extensions'];
488
+ else
489
+ frontmatter['extensions'] = extensions;
490
+ }
312
491
  // ---------------------------------------------------------------------------
313
492
  // Help material shared by `write` and `edit`
314
493
  //
@@ -2,7 +2,7 @@ import { defineLeaf } from '../../core/command.js';
2
2
  import { usage } from '../../core/errors.js';
3
3
  import { writeText, pathExists } from '../../core/fs-utils.js';
4
4
  import { appendHistoryRecord, buildHistoryRecord, historyLogPathFor, } from '../../core/memory/history.js';
5
- import { DOC_RATIONALE_CONSTRAINT, GUIDE_DOC_LINKS, GUIDE_PREDICATE_VOCABULARY, GUIDE_ROUTING_LINE, GUIDE_SURFACES, MEMORY_SCOPES, resolveWriteTarget, memoryFilePath, buildOrigin, coerceGate, coerceSurface, overlayParam, serializeMemoryDoc, } from './shared.js';
5
+ import { DOC_RATIONALE_CONSTRAINT, GUIDE_DOC_LINKS, GUIDE_PREDICATE_VOCABULARY, GUIDE_ROUTING_LINE, GUIDE_SURFACES, MEMORY_SCOPES, resolveWriteTarget, memoryFilePath, buildOrigin, coerceGate, coerceSurface, memoryExtensionCatalogForDoc, memoryExtensionFieldCatalogHelp, overlayParam, parseRequestedExtensionChanges, applyExtensionChanges, serializeMemoryDoc, } from './shared.js';
6
6
  // A topic's memory set stays usable only when authors treat cohesion as a creation
7
7
  // gate; otherwise small corrections accumulate as overlapping leaves whose routing
8
8
  // lines cannot tell a future reader which one owns the topic.
@@ -38,6 +38,7 @@ export const writeLeaf = defineLeaf({
38
38
  overlayParam('gate'),
39
39
  overlayParam('slash'),
40
40
  { kind: 'flag', name: 'rationale', type: 'string', required: false, constraint: DOC_RATIONALE_CONSTRAINT },
41
+ { kind: 'flag', name: 'extension', type: 'string', required: false, repeatable: true, constraint: 'Set one declared plugin field as `extensions.<plugin>.<field>=VALUE`. The path is required in full; booleans accept only true or false, numbers require finite numeric syntax, and strings/enums preserve the literal text after the first =. Every requested field validates before the document is written.' },
41
42
  { kind: 'flag', name: 'scope', type: 'enum', choices: [...MEMORY_SCOPES], required: false, constraint: 'Target scope. Default: project when inside a project, else user. `project` resolves to the NEAREST ancestor `.crouter/` walking up from cwd — in a nested workspace that can be a parent’s store, not the dir you are standing in; pass --dir to pin the exact project directory. `profile` requires a selected profile (CRTR_PROFILE_ID) or an explicit --profile. `node` writes to the this-node store (`nodes/<CRTR_NODE_ID>/context/memory/`) — the nearest scope, seen only by this running node, requires a node context.' },
42
43
  { kind: 'flag', name: 'dir', type: 'string', required: false, constraint: 'Exact project directory to write under — targets `<dir>/.crouter/memory/` regardless of cwd or ancestor stores, scaffolding `.crouter/` there if absent. THE way to place a doc in a specific project’s store (e.g. another project in your profile’s purview) without cd’ing there, and the only way to target a dir shadowed by an ancestor store. Implies --scope project; rejects --scope user/profile.' },
43
44
  { kind: 'flag', name: 'profile', type: 'string', required: false, constraint: 'Profile id or name to write under, for --scope profile. Default: the process CRTR_PROFILE_ID (the node\u2019s selected profile). Resolved through the same profile lookup as `crtr profile show`. Rejected (usage error) unless --scope profile is also selected \u2014 never silently ignored for another scope.' },
@@ -54,6 +55,7 @@ export const writeLeaf = defineLeaf({
54
55
  { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands — read it back, revise it, or list the inventory.' },
55
56
  ],
56
57
  outputKind: 'object',
58
+ dynamicState: () => memoryExtensionFieldCatalogHelp('write'),
57
59
  effects: [
58
60
  'Creates memory/<name>.md at the resolved scope with the given frontmatter fields + stdin body, and opens memory/.history/<name>.jsonl with a `create` record.',
59
61
  ],
@@ -67,6 +69,7 @@ export const writeLeaf = defineLeaf({
67
69
  const dirArg = input['dir'];
68
70
  const { scope, memoryDir } = resolveWriteTarget(scopeArg, profileArg, dirArg);
69
71
  const path = memoryFilePath(memoryDir, name);
72
+ const extensionChanges = parseRequestedExtensionChanges(input['extension'], undefined, memoryExtensionCatalogForDoc({ scope, path }));
70
73
  // Create-only. Revision goes through `edit`, which requires a rationale and
71
74
  // records the change — a write that still overwrote would be a standing
72
75
  // bypass of the audit trail. The error is the migration path.
@@ -97,6 +100,7 @@ export const writeLeaf = defineLeaf({
97
100
  if (input['slash'] === true)
98
101
  frontmatter['slash'] = true;
99
102
  setIf('rationale', input['rationale']);
103
+ applyExtensionChanges(frontmatter, extensionChanges.sets, extensionChanges.unsets);
100
104
  // One field consumers read for recency, live from the first byte.
101
105
  frontmatter['last-updated'] = origin['created'];
102
106
  // A workspace-open entry fires only when cwd/profile mounts the doc's
@@ -124,6 +128,7 @@ export const writeLeaf = defineLeaf({
124
128
  ...(input['gate'] !== undefined ? ['gate'] : []),
125
129
  ...(input['slash'] === true ? ['slash'] : []),
126
130
  ...(input['rationale'] !== undefined ? ['rationale'] : []),
131
+ ...(extensionChanges.sets.length > 0 ? ['extensions'] : []),
127
132
  ];
128
133
  const selectedFrontmatter = selectedKeys.map((key) => ({ key, value: frontmatter[key] }));
129
134
  return {
@@ -8,6 +8,7 @@ import { qualifiedTopLevelCommandName } from "../../../core/command-manifests/re
8
8
  import { listDirs, pathExists, walkFiles } from "../../../core/fs-utils.js";
9
9
  import { currentSha, gitSync } from "../../../core/git.js";
10
10
  import { listPluginMemoryDocs } from "../../../core/memory-resolver.js";
11
+ import { memoryExtensionEffectiveCatalog, projectEffectiveMemoryExtensions } from "../../../core/memory/extensions.js";
11
12
  import { readMarketplaceManifest, readPluginManifest } from "../../../core/manifest.js";
12
13
  import { readConfig } from "../../../core/config.js";
13
14
  import { parseSubstrateDoc } from "../../../core/substrate/schema.js";
@@ -365,6 +366,7 @@ function docDetails(plugin) {
365
366
  slash: parsed.slash,
366
367
  ...(parsed.gate !== undefined ? { gate: parsed.gate } : {}),
367
368
  ...(parsed.rationale !== undefined ? { rationale: parsed.rationale } : {}),
369
+ extensions: projectEffectiveMemoryExtensions(doc.frontmatter?.["extensions"], memoryExtensionEffectiveCatalog(doc)),
368
370
  body: parsed.body,
369
371
  }];
370
372
  });
@@ -1,6 +1,6 @@
1
1
  import type { Field, InputParam } from "../../../core/help.js";
2
2
  import type { SurfaceEntry } from "../../../core/substrate/schema.js";
3
- import type { OwnerRef, Scope } from "../../../types.js";
3
+ import type { MemoryExtensionScalar, OwnerRef, Scope } from "../../../types.js";
4
4
  /** The scopes that can hold a marketplace, and that a plugin can be installed
5
5
  * into. Narrower than `Scope`: `builtin` names plugins that ship with crtr, and
6
6
  * `listAllMarketplaces()` never yields it, so a builtin marketplace or a
@@ -100,6 +100,9 @@ export interface DocDetail {
100
100
  /** Frontmatter `rationale`: the observed failure the doc exists to close.
101
101
  * Maintainer-facing — never delivered to a reading agent. */
102
102
  rationale?: string;
103
+ /** Effective valid plugin-owned metadata. Enabled declaration defaults appear
104
+ * here without materializing in the memory document. */
105
+ extensions: Record<string, Record<string, MemoryExtensionScalar>>;
103
106
  /** Markdown body, frontmatter stripped. */
104
107
  body: string;
105
108
  }