@aiwg/cli 2026.7.20 → 2026.7.21

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 (53) hide show
  1. package/README.md +4 -4
  2. package/dist/src/api/index.d.ts +1 -0
  3. package/dist/src/api/index.js +1 -0
  4. package/dist/src/artifacts/browser-export.js +7 -0
  5. package/dist/src/artifacts/citation-parser.js +96 -35
  6. package/dist/src/artifacts/index-builder.js +54 -17
  7. package/dist/src/artifacts/state-transfer.js +27 -0
  8. package/dist/src/artifacts/stats.js +8 -0
  9. package/dist/src/cli/cli-extension-loader.js +73 -0
  10. package/dist/src/cli/handlers/index.js +3 -1
  11. package/dist/src/cli/handlers/sessions.js +966 -0
  12. package/dist/src/cli/handlers/skill-lint.js +49 -45
  13. package/dist/src/cli/handlers/use.js +143 -60
  14. package/dist/src/cli/handlers/utilities.js +22 -8
  15. package/dist/src/cli/skill-usage.js +146 -24
  16. package/dist/src/extensions/commands/definitions.js +29 -0
  17. package/dist/src/extensions/manifest.js +29 -0
  18. package/dist/src/sessions/adapters/claude.js +357 -0
  19. package/dist/src/sessions/adapters/codex.js +521 -0
  20. package/dist/src/sessions/adapters/copilot.js +226 -0
  21. package/dist/src/sessions/adapters/cursor.js +372 -0
  22. package/dist/src/sessions/adapters/factory.js +345 -0
  23. package/dist/src/sessions/adapters/generic.js +225 -0
  24. package/dist/src/sessions/adapters/hermes.js +341 -0
  25. package/dist/src/sessions/adapters/openclaw.js +381 -0
  26. package/dist/src/sessions/adapters/opencode.js +454 -0
  27. package/dist/src/sessions/adapters/openhuman.js +315 -0
  28. package/dist/src/sessions/adapters/warp.js +160 -0
  29. package/dist/src/sessions/adapters/windsurf.js +212 -0
  30. package/dist/src/sessions/candidates.js +210 -0
  31. package/dist/src/sessions/contracts.js +310 -0
  32. package/dist/src/sessions/discovery.js +51 -0
  33. package/dist/src/sessions/fixtures.js +12 -0
  34. package/dist/src/sessions/importer.js +315 -0
  35. package/dist/src/sessions/index.js +25 -0
  36. package/dist/src/sessions/knowledge-shard.js +61 -0
  37. package/dist/src/sessions/optional-backends.js +238 -0
  38. package/dist/src/sessions/policy.js +192 -0
  39. package/dist/src/sessions/ports.js +2 -0
  40. package/dist/src/sessions/promotion.js +367 -0
  41. package/dist/src/sessions/readers.js +176 -0
  42. package/dist/src/sessions/repository.js +1551 -0
  43. package/dist/src/skills/adapters/agent-skills.js +59 -0
  44. package/dist/src/skills/adapters/local.js +19 -1
  45. package/dist/src/skills/agent-skills.js +249 -0
  46. package/dist/src/skills/cli.js +463 -7
  47. package/dist/src/skills/deployer.js +554 -0
  48. package/dist/src/skills/doctor.js +105 -0
  49. package/dist/src/skills/exporter.js +382 -0
  50. package/dist/src/skills/importer.js +921 -0
  51. package/dist/src/skills/registry.js +19 -0
  52. package/dist/src/skills/validator.js +323 -0
  53. package/package.json +2 -2
package/README.md CHANGED
@@ -247,7 +247,7 @@ rather than merely printed.
247
247
  Agents do not need to memorize the remaining command surface. AIWG discovery
248
248
  finds the relevant skill, and the skill supplies the right CLI step. Operators
249
249
  who need the complete syntax and examples can use the
250
- [AIWG CLI reference](https://docs.aiwg.io/pages/cli-reference.html).
250
+ [AIWG CLI reference](https://github.com/jmagly/aiwg/blob/main/docs/agents/cli-reference.md).
251
251
 
252
252
  ## How It Works
253
253
 
@@ -438,7 +438,7 @@ This README therefore documents the operating model, package boundary, trust
438
438
  model, and troubleshooting path instead of duplicating every command and flag.
439
439
  The complete operator reference is maintained at:
440
440
 
441
- **[AIWG CLI Reference — every command and example](https://docs.aiwg.io/pages/cli-reference.html)**
441
+ **[AIWG CLI Reference — every command and example](https://github.com/jmagly/aiwg/blob/main/docs/agents/cli-reference.md)**
442
442
 
443
443
  Keeping the command catalog in one canonical location prevents package
444
444
  documentation from drifting as the runtime grows.
@@ -492,7 +492,7 @@ security guidance.
492
492
  Do not routinely call `aiwg help` and place the full output in the model
493
493
  context. If a skill exists, use it. If a maintainer or operator needs an
494
494
  unfamiliar command, link to the canonical
495
- [CLI reference](https://docs.aiwg.io/pages/cli-reference.html) or retrieve only
495
+ [CLI reference](https://github.com/jmagly/aiwg/blob/main/docs/agents/cli-reference.md) or retrieve only
496
496
  the relevant section.
497
497
 
498
498
  ## Using AIWG from a Web-Connected Chat
@@ -1020,7 +1020,7 @@ Package invariants include:
1020
1020
 
1021
1021
  ## Documentation
1022
1022
 
1023
- - [Complete AIWG CLI reference](https://docs.aiwg.io/pages/cli-reference.html)
1023
+ - [Complete AIWG CLI reference](https://github.com/jmagly/aiwg/blob/main/docs/agents/cli-reference.md)
1024
1024
  - [AIWG documentation](https://docs.aiwg.io/)
1025
1025
  - [AIWG project README](https://github.com/jmagly/aiwg#readme)
1026
1026
  - [Web-backed resources guide](https://github.com/jmagly/aiwg/blob/main/docs/install/web-backed-resources.md)
@@ -7,4 +7,5 @@
7
7
  */
8
8
  export { run } from '../cli/router.js';
9
9
  export * from '../resources/index.js';
10
+ export * from '../sessions/index.js';
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -7,4 +7,5 @@
7
7
  */
8
8
  export { run } from '../cli/router.js';
9
9
  export * from '../resources/index.js';
10
+ export * from '../sessions/index.js';
10
11
  //# sourceMappingURL=index.js.map
@@ -508,6 +508,13 @@ function recordForEntry(cwd, entry, graphName, dependencyGraph, privacy, schemaV
508
508
  ...(entry.operationalState
509
509
  ? { operational_state: entry.operationalState }
510
510
  : {}),
511
+ ...(entry.stateTransfer
512
+ ? {
513
+ state_transfer: {
514
+ deleted_at: entry.stateTransfer.deletedAt,
515
+ },
516
+ }
517
+ : {}),
511
518
  }
512
519
  : {}),
513
520
  updated_at: entry.updated,
@@ -8,7 +8,7 @@
8
8
  * - **Incoming**: corpus papers that cite this work (column: "REF") → `cited-by` edges
9
9
  *
10
10
  * Supported node-id forms (#105):
11
- * - `REF-\d+` research-paper IDs (REF-001, REF-029, ...)
11
+ * - `REF-\d+[a-z]?` research-paper IDs (REF-001, REF-434a, ...)
12
12
  * - `PROF-[POFG]-[a-z0-9-]+` entity-profile IDs:
13
13
  * - `PROF-P-*` people, `PROF-O-*` orgs, `PROF-F-*` funders, `PROF-G-*` groups
14
14
  *
@@ -26,17 +26,43 @@ import { parseFrontmatter } from './index-builder.js';
26
26
  * Match a single node identifier (REF-* or PROF-*) anywhere in a string.
27
27
  * Used by `extractRefsFromTable` to pull every ID out of a table cell.
28
28
  */
29
- const NODE_ID_PATTERN = /(?:REF-\d+|PROF-[POFG]-[a-z0-9-]+)/g;
29
+ const NODE_ID_PATTERN = /(?:REF-\d+[a-z]?|PROF-[POFG]-[a-z0-9-]+)/g;
30
30
  /**
31
31
  * Validate that a string is a complete node identifier.
32
32
  * Used by `parseCitationSidecar` and `buildRefToPathMap` to gate
33
33
  * frontmatter `ref` values.
34
34
  */
35
- const NODE_ID_FULL = /^(?:REF-\d+|PROF-[POFG]-[a-z0-9-]+)$/;
35
+ const NODE_ID_FULL = /^(?:REF-\d+[a-z]?|PROF-[POFG]-[a-z0-9-]+)$/;
36
+ /** Explicit column aliases used by historical citation-sidecar variants. */
37
+ const CITATION_REF_COLUMNS = [
38
+ 'Inducted REF',
39
+ 'REF',
40
+ 'Corpus REF',
41
+ 'In-corpus REF',
42
+ ];
43
+ /**
44
+ * Preserve the corpus snapshot's legacy edge semantics: every node ID on a
45
+ * pipe-delimited line inside an outgoing/incoming section is a declaration.
46
+ * Explicit column parsing remains the primary path, while this compatibility
47
+ * scan covers malformed rows and prose continuations that historical corpus
48
+ * snapshots already count.
49
+ */
50
+ function extractSectionNodeIds(section) {
51
+ const refs = [];
52
+ for (const line of section.split('\n')) {
53
+ if (!line.includes('|'))
54
+ continue;
55
+ NODE_ID_PATTERN.lastIndex = 0;
56
+ const matches = line.match(NODE_ID_PATTERN);
57
+ if (matches)
58
+ refs.push(...matches);
59
+ }
60
+ return [...new Set(refs)];
61
+ }
36
62
  /**
37
63
  * Test whether a string is a valid sidecar node identifier.
38
64
  *
39
- * Accepts `REF-\d+` and `PROF-[POFG]-[a-z0-9-]+`. Returns false for any
65
+ * Accepts `REF-\d+[a-z]?` and `PROF-[POFG]-[a-z0-9-]+`. Returns false for any
40
66
  * other input (including unrelated `PROF-` prefixed strings that don't
41
67
  * match the four-letter type code form).
42
68
  */
@@ -54,34 +80,71 @@ export function isNodeId(value) {
54
80
  * @returns Array of node identifiers found
55
81
  */
56
82
  export function extractRefsFromTable(tableText, columnName) {
57
- const lines = tableText.split('\n').filter(l => l.trim().startsWith('|'));
58
- if (lines.length < 3)
59
- return []; // Need header + separator + at least one row
60
- // Parse header to find column index
61
- const headerCells = lines[0].split('|').map(c => c.trim()).filter(Boolean);
62
- const colIndex = headerCells.findIndex(h => h.toLowerCase() === columnName.toLowerCase());
63
- if (colIndex === -1)
64
- return [];
65
- // Skip header (line 0) and separator (line 1), parse data rows
83
+ const aliases = (Array.isArray(columnName) ? columnName : [columnName])
84
+ .map(name => name.trim().toLowerCase());
85
+ const tableBlocks = [];
86
+ let currentBlock = [];
87
+ for (const line of tableText.split('\n')) {
88
+ if (line.trim().startsWith('|')) {
89
+ currentBlock.push(line);
90
+ }
91
+ else if (currentBlock.length > 0) {
92
+ tableBlocks.push(currentBlock);
93
+ currentBlock = [];
94
+ }
95
+ }
96
+ if (currentBlock.length > 0)
97
+ tableBlocks.push(currentBlock);
98
+ const parseRow = (line) => {
99
+ let row = line.trim();
100
+ if (row.startsWith('|'))
101
+ row = row.slice(1);
102
+ if (row.endsWith('|'))
103
+ row = row.slice(0, -1);
104
+ return row.split('|').map(cell => cell.trim());
105
+ };
66
106
  const refs = [];
67
- for (let i = 2; i < lines.length; i++) {
68
- const cells = lines[i].split('|').map(c => c.trim()).filter(Boolean);
69
- if (colIndex >= cells.length)
107
+ for (const lines of tableBlocks) {
108
+ const headerCells = parseRow(lines[0]);
109
+ const colIndex = headerCells.findIndex(header => {
110
+ const normalized = header.toLowerCase().replace(/[`*_]/g, '').trim();
111
+ return aliases.some(alias => normalized === alias ||
112
+ normalized.startsWith(`${alias} /`) ||
113
+ normalized.startsWith(`${alias} (`));
114
+ });
115
+ if (lines.length < 3 || colIndex === -1) {
116
+ // Legacy sidecars sometimes have headerless continuation rows or a
117
+ // generic table whose rows were extended with a final REF cell. The
118
+ // corpus snapshot has always treated every in-section table REF as an
119
+ // outgoing/incoming declaration, so retain that compatibility fallback.
120
+ const dataLines = lines.length >= 3 ? lines.slice(2) : lines;
121
+ for (const line of dataLines) {
122
+ NODE_ID_PATTERN.lastIndex = 0;
123
+ const refMatches = line.match(NODE_ID_PATTERN);
124
+ if (refMatches)
125
+ refs.push(...refMatches);
126
+ }
70
127
  continue;
71
- const value = cells[colIndex].trim();
72
- // Skip empty, dash, or em-dash values
73
- if (!value || value === '—' || value === '-' || value === '–')
74
- continue;
75
- // Extract node-id pattern(s) (REF-* or PROF-*) from the cell.
76
- // Reset the lastIndex defensively — NODE_ID_PATTERN is a module-level
77
- // /g RegExp shared across calls.
78
- NODE_ID_PATTERN.lastIndex = 0;
79
- const refMatches = value.match(NODE_ID_PATTERN);
80
- if (refMatches) {
81
- refs.push(...refMatches);
128
+ }
129
+ // Skip this table's header and separator, preserving all interior cells.
130
+ for (let i = 2; i < lines.length; i++) {
131
+ const cells = parseRow(lines[i]);
132
+ if (colIndex >= cells.length)
133
+ continue;
134
+ // Historical rows may expand the named REF cell into an issue/REF pair
135
+ // or append placeholder cells without extending the header. Scan from
136
+ // the named column through the remainder of the row; columns before the
137
+ // alias (title/authors/year) remain excluded.
138
+ const value = cells.slice(colIndex).join(' | ');
139
+ if (!value || value === '—' || value === '-' || value === '–')
140
+ continue;
141
+ NODE_ID_PATTERN.lastIndex = 0;
142
+ const refMatches = value.match(NODE_ID_PATTERN);
143
+ if (refMatches)
144
+ refs.push(...refMatches);
82
145
  }
83
146
  }
84
- return refs;
147
+ return [...new Set(refs)];
85
148
  }
86
149
  /**
87
150
  * Parse a citation sidecar markdown file into structured edges.
@@ -102,17 +165,15 @@ export function parseCitationSidecar(content) {
102
165
  for (const section of sections) {
103
166
  const sectionLower = section.toLowerCase();
104
167
  if (sectionLower.startsWith('outgoing')) {
105
- // Outgoing table: extract from "Inducted REF" column
106
- cites = extractRefsFromTable(section, 'Inducted REF');
168
+ cites.push(...extractRefsFromTable(section, CITATION_REF_COLUMNS));
169
+ cites.push(...extractSectionNodeIds(section));
107
170
  }
108
171
  else if (sectionLower.startsWith('incoming')) {
109
- // Incoming table: extract from "REF" column
110
- // The incoming section may have subsections (### Corpus Cross-References)
111
- // Look for tables anywhere in this section
112
- citedBy = extractRefsFromTable(section, 'REF');
172
+ citedBy.push(...extractRefsFromTable(section, CITATION_REF_COLUMNS));
173
+ citedBy.push(...extractSectionNodeIds(section));
113
174
  }
114
175
  }
115
- return { ref, cites, citedBy };
176
+ return { ref, cites: [...new Set(cites)], citedBy: [...new Set(citedBy)] };
116
177
  }
117
178
  /**
118
179
  * Convert a CitationParseResult into TypedEdge arrays for the dependency graph.
@@ -20,6 +20,7 @@ import { loadManifest, writeManifest, statMatches, makeEntry } from './checksum-
20
20
  import { workspaceLinkedFiles } from '../smiths/context-pipeline/workspace-context.js';
21
21
  import { normalizeOperationalState } from './operational-state.js';
22
22
  import { DEFAULT_PROJECT_AIWG_DIR, resolveProjectAiwgDir, } from '../config/project-artifacts.js';
23
+ import { normalizeStateTransferProjection } from './state-transfer.js';
23
24
  function pathContains(parent, child) {
24
25
  const relative = path.relative(parent, child);
25
26
  return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
@@ -322,10 +323,10 @@ export function parseRunbookDoc(data, body, filePath) {
322
323
  /**
323
324
  * Extract trigger phrases from a SKILL.md / agent body.
324
325
  *
325
- * Skills declare alternate activation phrases under a `## Triggers`
326
- * heading; the body typically lists them as bullet points. This
327
- * function pulls each bullet's leading phrase (the part before any
328
- * `→` arrow or em-dash explanation), lowercased and trimmed.
326
+ * Skills declare alternate activation phrases in `triggers`, `aliases`,
327
+ * `deprecated_names`, or under a `## Triggers` heading. This function pulls
328
+ * those names plus each bullet's leading phrase (the part before any `→`
329
+ * arrow or em-dash explanation), lowercased and trimmed.
329
330
  *
330
331
  * Returns an empty array when no `## Triggers` section is found —
331
332
  * non-skill artifacts get `triggers: undefined` after this is wired.
@@ -334,18 +335,20 @@ export function parseRunbookDoc(data, body, filePath) {
334
335
  */
335
336
  export function extractTriggers(body, frontmatter) {
336
337
  const phrases = [];
337
- const declaredTriggers = Array.isArray(frontmatter?.triggers)
338
- ? frontmatter.triggers
339
- : [];
340
- for (const trigger of declaredTriggers) {
341
- if (typeof trigger !== 'string')
342
- continue;
343
- const phrase = trigger.trim().toLowerCase();
344
- if (phrase.length === 0)
345
- continue;
346
- if (phrase.length > 200)
347
- continue;
348
- phrases.push(phrase);
338
+ for (const field of ['triggers', 'aliases', 'deprecated_names']) {
339
+ const declaredValues = Array.isArray(frontmatter?.[field])
340
+ ? frontmatter[field]
341
+ : [];
342
+ for (const value of declaredValues) {
343
+ if (typeof value !== 'string')
344
+ continue;
345
+ const phrase = value.trim().toLowerCase();
346
+ if (phrase.length === 0)
347
+ continue;
348
+ if (phrase.length > 200)
349
+ continue;
350
+ phrases.push(phrase);
351
+ }
349
352
  }
350
353
  // Find a triggers heading (case-insensitive). Accepted variants:
351
354
  // ## Triggers
@@ -882,6 +885,7 @@ export async function buildIndex(cwd, options = {}) {
882
885
  // Script entrypoint metadata is meaningful for skills only (#1227).
883
886
  const script = type === 'skill' ? extractSkillScript(data) : undefined;
884
887
  const operationalState = normalizeOperationalState(data.operational_state);
888
+ const stateTransfer = normalizeStateTransferProjection(data.state_transfer);
885
889
  // Canonical short name (#1233) — used by the scorer to floor exact-name
886
890
  // queries to 1.0 so hyphenated kernel-skill names like `aiwg-doctor`
887
891
  // remain searchable even when the rendered title strips the hyphen.
@@ -907,6 +911,7 @@ export async function buildIndex(cwd, options = {}) {
907
911
  ...(kernel ? { kernel } : {}),
908
912
  ...(script ? { script } : {}),
909
913
  ...(operationalState ? { operationalState } : {}),
914
+ ...(stateTransfer ? { stateTransfer } : {}),
910
915
  };
911
916
  }
912
917
  entries[relativePath] = entry;
@@ -957,6 +962,7 @@ export async function buildIndex(cwd, options = {}) {
957
962
  }
958
963
  }
959
964
  // Run citation sidecar edge extraction if configured
965
+ let citationMetrics = null;
960
966
  if (graphConfig?.edgeExtraction?.parser === 'citation-sidecar') {
961
967
  // Build REF-XXX → path map from all entries with ref frontmatter
962
968
  const entryFrontmatter = new Map();
@@ -971,6 +977,10 @@ export async function buildIndex(cwd, options = {}) {
971
977
  const refToPath = buildRefToPathMap(entryFrontmatter);
972
978
  // Parse each entry as a citation sidecar and extract edges
973
979
  let citationEdgeCount = 0;
980
+ let outgoingDeclarations = 0;
981
+ let incomingDeclarations = 0;
982
+ const canonicalOutgoing = new Set();
983
+ const declaredIncoming = new Set();
974
984
  for (const entryPath of Object.keys(entries)) {
975
985
  const fullPath = absoluteEntryPath(cwd, entryPath, graph);
976
986
  if (!fs.existsSync(fullPath))
@@ -979,6 +989,14 @@ export async function buildIndex(cwd, options = {}) {
979
989
  const result = parseCitationSidecar(content);
980
990
  if (!result)
981
991
  continue;
992
+ outgoingDeclarations += result.cites.length;
993
+ incomingDeclarations += result.citedBy.length;
994
+ for (const targetRef of result.cites) {
995
+ canonicalOutgoing.add(`${result.ref}\0${targetRef}`);
996
+ }
997
+ for (const sourceRef of result.citedBy) {
998
+ declaredIncoming.add(`${sourceRef}\0${result.ref}`);
999
+ }
982
1000
  const edges = citationResultToEdges(result, refToPath);
983
1001
  if (!depGraph[entryPath]) {
984
1002
  depGraph[entryPath] = { upstream: [], downstream: [] };
@@ -1004,6 +1022,15 @@ export async function buildIndex(cwd, options = {}) {
1004
1022
  citationEdgeCount++;
1005
1023
  }
1006
1024
  }
1025
+ citationMetrics = {
1026
+ canonicalEdges: canonicalOutgoing.size,
1027
+ outgoingDeclarations,
1028
+ incomingDeclarations,
1029
+ unmirroredOutgoing: [...canonicalOutgoing]
1030
+ .filter(edge => !declaredIncoming.has(edge)).length,
1031
+ unmirroredIncoming: [...declaredIncoming]
1032
+ .filter(edge => !canonicalOutgoing.has(edge)).length,
1033
+ };
1007
1034
  if (verbose && citationEdgeCount > 0) {
1008
1035
  console.log(` citation edges: ${citationEdgeCount}`);
1009
1036
  }
@@ -1038,7 +1065,9 @@ export async function buildIndex(cwd, options = {}) {
1038
1065
  manifestStats.pruned = 0;
1039
1066
  writeManifest(indexOutputDir, nextManifest);
1040
1067
  // Write stats
1041
- const totalEdges = Object.values(depGraph).reduce((sum, node) => sum + node.downstream.length, 0);
1068
+ const downstreamEdges = Object.values(depGraph).reduce((sum, node) => sum + node.downstream.length, 0);
1069
+ const adjacencyEntries = Object.values(depGraph).reduce((sum, node) => sum + node.upstream.length + node.downstream.length, 0);
1070
+ const totalEdges = citationMetrics?.canonicalEdges ?? downstreamEdges;
1042
1071
  const orphaned = Object.entries(depGraph).filter(([, node]) => node.upstream.length === 0 && node.downstream.length === 0).length;
1043
1072
  const mostReferenced = Object.entries(depGraph)
1044
1073
  .map(([p, node]) => ({ path: p, count: node.downstream.length }))
@@ -1064,6 +1093,14 @@ export async function buildIndex(cwd, options = {}) {
1064
1093
  tagDistribution: tagDist,
1065
1094
  graphMetrics: {
1066
1095
  totalEdges,
1096
+ ...(citationMetrics ? {
1097
+ canonicalEdges: citationMetrics.canonicalEdges,
1098
+ outgoingDeclarations: citationMetrics.outgoingDeclarations,
1099
+ incomingDeclarations: citationMetrics.incomingDeclarations,
1100
+ adjacencyEntries,
1101
+ unmirroredOutgoing: citationMetrics.unmirroredOutgoing,
1102
+ unmirroredIncoming: citationMetrics.unmirroredIncoming,
1103
+ } : {}),
1067
1104
  orphanedArtifacts: orphaned,
1068
1105
  mostReferenced,
1069
1106
  },
@@ -0,0 +1,27 @@
1
+ export function normalizeStateTransferProjection(value) {
2
+ if (value === undefined)
3
+ return undefined;
4
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
5
+ throw new Error("state_transfer must be an object");
6
+ }
7
+ const record = value;
8
+ const unknown = Object.keys(record).filter((key) => key !== "deleted_at");
9
+ if (unknown.length > 0) {
10
+ throw new Error(`state_transfer has unsupported fields: ${unknown.join(", ")}`);
11
+ }
12
+ if (!Object.hasOwn(record, "deleted_at")) {
13
+ throw new Error("state_transfer.deleted_at is required");
14
+ }
15
+ const deletedAt = record.deleted_at;
16
+ if (deletedAt === null)
17
+ return { deletedAt: null };
18
+ if (deletedAt instanceof Date && !Number.isNaN(deletedAt.getTime())) {
19
+ return { deletedAt: deletedAt.toISOString() };
20
+ }
21
+ if (typeof deletedAt !== "string"
22
+ || Number.isNaN(Date.parse(deletedAt))) {
23
+ throw new Error("state_transfer.deleted_at must be null or an ISO date-time");
24
+ }
25
+ return { deletedAt: new Date(deletedAt).toISOString() };
26
+ }
27
+ //# sourceMappingURL=state-transfer.js.map
@@ -153,6 +153,14 @@ async function renderStats(cwd, stats, options, graphType) {
153
153
  // Dependency graph
154
154
  console.log('Dependency Graph:');
155
155
  console.log(` Total edges: ${stats.graphMetrics.totalEdges}`);
156
+ if (stats.graphMetrics.canonicalEdges !== undefined) {
157
+ console.log(` Canonical edges: ${stats.graphMetrics.canonicalEdges}`);
158
+ console.log(` Outgoing declares: ${stats.graphMetrics.outgoingDeclarations}`);
159
+ console.log(` Incoming declares: ${stats.graphMetrics.incomingDeclarations}`);
160
+ console.log(` Adjacency entries: ${stats.graphMetrics.adjacencyEntries}`);
161
+ console.log(` Unmirrored outgoing:${String(stats.graphMetrics.unmirroredOutgoing).padStart(3)}`);
162
+ console.log(` Unmirrored incoming:${String(stats.graphMetrics.unmirroredIncoming).padStart(3)}`);
163
+ }
156
164
  console.log(` Orphaned artifacts: ${stats.graphMetrics.orphanedArtifacts}`);
157
165
  if (stats.graphMetrics.mostReferenced) {
158
166
  console.log(` Most referenced: ${stats.graphMetrics.mostReferenced.path} (${stats.graphMetrics.mostReferenced.count} dependents)`);
@@ -41,10 +41,83 @@ export async function writeRegistry(cwd, registry) {
41
41
  * Called by `aiwg use <addon>` after reading the addon manifest.
42
42
  */
43
43
  export async function registerCliCommands(cwd, namespace, description, source, subcommands) {
44
+ if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(namespace)) {
45
+ throw new Error(`Invalid CLI namespace: ${namespace}`);
46
+ }
47
+ if (!source || !path.isAbsolute(source)) {
48
+ throw new Error('CLI extension source must be an absolute path.');
49
+ }
50
+ if (Object.keys(subcommands).length === 0) {
51
+ throw new Error(`CLI namespace '${namespace}' declares no subcommands.`);
52
+ }
53
+ for (const [name, subcommand] of Object.entries(subcommands)) {
54
+ if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(name)) {
55
+ throw new Error(`Invalid CLI subcommand: ${namespace} ${name}`);
56
+ }
57
+ if (!subcommand
58
+ || typeof subcommand.file !== 'string'
59
+ || !/^[a-zA-Z0-9_-]+\.mjs$/.test(subcommand.file)) {
60
+ throw new Error(`CLI subcommand '${namespace} ${name}' must name a local .mjs file.`);
61
+ }
62
+ if (typeof subcommand.description !== 'string' || !subcommand.description.trim()) {
63
+ throw new Error(`CLI subcommand '${namespace} ${name}' requires a description.`);
64
+ }
65
+ }
44
66
  const existing = await readRegistry(cwd) ?? {};
45
67
  existing[namespace] = { source, description, subcommands };
46
68
  await writeRegistry(cwd, existing);
47
69
  }
70
+ /**
71
+ * Read and validate an addon-shaped manifest's expandable CLI declaration.
72
+ *
73
+ * `sourceRoot` may be a bundled addon, a project-local addon, or the validated
74
+ * payload of a project-local plugin wrapper. Module files are constrained to
75
+ * the declared commands directory; registration never follows `..` paths.
76
+ */
77
+ export async function loadCliCommandsContribution(sourceRoot) {
78
+ const manifestPath = path.join(sourceRoot, 'manifest.json');
79
+ let raw;
80
+ try {
81
+ raw = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
82
+ }
83
+ catch (error) {
84
+ if (error.code === 'ENOENT')
85
+ return null;
86
+ throw new Error(`Unable to read CLI extension manifest at ${manifestPath}: ${error.message}`);
87
+ }
88
+ const candidate = raw.cli_commands;
89
+ if (candidate === undefined)
90
+ return null;
91
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
92
+ throw new Error(`Invalid cli_commands block in ${manifestPath}`);
93
+ }
94
+ const commands = candidate;
95
+ if (typeof commands.namespace !== 'string'
96
+ || typeof commands.description !== 'string'
97
+ || !commands.subcommands
98
+ || typeof commands.subcommands !== 'object'
99
+ || Array.isArray(commands.subcommands)) {
100
+ throw new Error(`Incomplete cli_commands block in ${manifestPath}`);
101
+ }
102
+ const entry = commands.entry ?? 'commands/';
103
+ if (typeof entry !== 'string'
104
+ || path.isAbsolute(entry)
105
+ || entry.split(/[\\/]+/).includes('..')) {
106
+ throw new Error(`Unsafe cli_commands.entry in ${manifestPath}`);
107
+ }
108
+ const commandsSource = path.resolve(sourceRoot, entry);
109
+ const relative = path.relative(path.resolve(sourceRoot), commandsSource);
110
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
111
+ throw new Error(`cli_commands.entry escapes its addon root in ${manifestPath}`);
112
+ }
113
+ const manifest = {
114
+ namespace: commands.namespace,
115
+ description: commands.description,
116
+ entry,
117
+ subcommands: commands.subcommands,
118
+ };
119
+ return { manifest, commandsSource };
120
+ }
48
121
  /**
49
122
  * Try to execute an addon-contributed CLI command
50
123
  *
@@ -44,6 +44,7 @@ import { serveHandler } from './serve.js';
44
44
  import { lintHandler } from './lint.js';
45
45
  import { feedbackHandler } from './feedback.js';
46
46
  import { sessionHandler } from './session.js';
47
+ import { sessionsHandler } from './sessions.js';
47
48
  import { sandboxHandler, sandboxHandlers } from './sandbox.js';
48
49
  import { diagnoseHandler } from './diagnose.js';
49
50
  import { localExecutorHandler, localExecutorServeHandler } from './local-executor.js';
@@ -62,7 +63,7 @@ helpHandler, versionHandler, doctorHandler, updateHandler, refreshHandler, regen
62
63
  // Framework management
63
64
  useHandler, listHandler, removeHandler, promoteHandler, installHandler, packagesHandler, marketplaceHandler, initHandler, setupHandler, issueHandler, issueAuditHandler, runHandler,
64
65
  // Project
65
- newBundleHandler, quickrefHandler, newProjectHandler,
66
+ newBundleHandler, quickrefHandler, newProjectHandler, sessionHandler, sessionsHandler,
66
67
  // Workspace
67
68
  statusHandler, wizardHandler, migrateWorkspaceHandler, rollbackWorkspaceHandler,
68
69
  // Subcommands
@@ -224,6 +225,7 @@ export const allHandlers = [
224
225
  feedbackHandler,
225
226
  // Session (#884)
226
227
  sessionHandler,
228
+ sessionsHandler,
227
229
  // Repo access policy (#1376)
228
230
  ...repoAccessHandlers,
229
231
  ];