@aiwg/cli 2026.10.11 → 2026.10.12

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 (132) hide show
  1. package/agentic/code/providers/model-catalog.v1.json +62 -12
  2. package/dist/src/artifacts/binding-target-identity.js +51 -0
  3. package/dist/src/artifacts/checksum-manifest.js +12 -0
  4. package/dist/src/artifacts/index-builder.js +11 -13
  5. package/dist/src/artifacts/query-engine.js +5 -11
  6. package/dist/src/cli/handlers/doctor-help.js +32 -0
  7. package/dist/src/cli/handlers/gates.js +19 -18
  8. package/dist/src/cli/handlers/models-help.js +7 -1
  9. package/dist/src/cli/handlers/models.js +3 -3
  10. package/dist/src/cli/handlers/script-runner.js +8 -5
  11. package/dist/src/cli/handlers/sessions-intake.js +104 -0
  12. package/dist/src/cli/handlers/sessions-tail.js +172 -0
  13. package/dist/src/cli/handlers/sessions.js +90 -17
  14. package/dist/src/cli/handlers/skill-lint.js +10 -3
  15. package/dist/src/cli/handlers/steward-format.js +100 -0
  16. package/dist/src/cli/handlers/steward-models.js +167 -0
  17. package/dist/src/cli/handlers/steward.js +14 -248
  18. package/dist/src/cli/handlers/use-bundle-counts.js +61 -0
  19. package/dist/src/cli/handlers/use.js +6 -24
  20. package/dist/src/cli/handlers/utilities.js +1 -24
  21. package/dist/src/config/aiwg-config.js +7 -0
  22. package/dist/src/config/authorization-cli.js +89 -0
  23. package/dist/src/config/cli-help.js +1 -0
  24. package/dist/src/config/cli.js +10 -1
  25. package/dist/src/decision/context-pruning.js +3 -2
  26. package/dist/src/decision/ensemble/contract.js +15 -4
  27. package/dist/src/decision/ensemble-study/artifacts.js +5 -0
  28. package/dist/src/decision/ensemble-study/score.js +4 -3
  29. package/dist/src/decision/feature-export.js +3 -9
  30. package/dist/src/decision/heldout/collector.js +2 -2
  31. package/dist/src/decision/issue-triage/runtime.js +3 -9
  32. package/dist/src/decision/qualification/gates.js +2 -1
  33. package/dist/src/decision/qualification/release.js +2 -3
  34. package/dist/src/decision/routing/report.js +2 -0
  35. package/dist/src/decision/sdlc-screening.js +3 -5
  36. package/dist/src/decision/sensitivity/comparative.js +5 -3
  37. package/dist/src/gates/driver.js +6 -77
  38. package/dist/src/gates/evaluate.js +4 -11
  39. package/dist/src/gates/floors.js +4 -4
  40. package/dist/src/gates/index.js +3 -3
  41. package/dist/src/gates/lattice.js +26 -0
  42. package/dist/src/gates/registry.js +2 -1
  43. package/dist/src/gates/report.js +19 -7
  44. package/dist/src/issues/audit-cli.js +120 -0
  45. package/dist/src/issues/audit-contract.js +64 -0
  46. package/dist/src/issues/audit-local.js +82 -0
  47. package/dist/src/issues/audit.js +220 -0
  48. package/dist/src/lint/rule-activation.js +15 -1
  49. package/dist/src/mission-protocol/canonical-codecs.js +270 -0
  50. package/dist/src/mission-protocol/codecs.js +22 -550
  51. package/dist/src/mission-protocol/health-trace-codecs.js +281 -0
  52. package/dist/src/mission-protocol/health-trace-types.js +43 -0
  53. package/dist/src/mission-protocol/types.js +2 -42
  54. package/dist/src/mission-protocol/worker-contract-json.js +10 -0
  55. package/dist/src/mission-protocol/worker-review-gate.js +100 -0
  56. package/dist/src/mission-protocol/worker-review-types.js +2 -0
  57. package/dist/src/models/catalog-policy.js +280 -0
  58. package/dist/src/models/index.js +1 -1
  59. package/dist/src/models/model-catalog.v1.json +62 -12
  60. package/dist/src/models/policy-resources.js +28 -0
  61. package/dist/src/models/provider-policy.js +10 -546
  62. package/dist/src/models/resolver.js +26 -8
  63. package/dist/src/models/router.js +15 -2
  64. package/dist/src/models/tiers.js +13 -0
  65. package/dist/src/models/types.js +0 -6
  66. package/dist/src/models/worker-ceilings.js +59 -0
  67. package/dist/src/models/worker-invocation-policy.js +243 -0
  68. package/dist/src/models/worker-plan.js +16 -20
  69. package/dist/src/models/worker-review-lineage.js +55 -0
  70. package/dist/src/models/worker-selection-policy.js +1 -1
  71. package/dist/src/models/wrapper-route.js +6 -3
  72. package/dist/src/policy/authorization.js +1 -0
  73. package/dist/src/policy/hitl-config.js +63 -0
  74. package/dist/src/policy/hitl-receipt.js +57 -0
  75. package/dist/src/policy/hitl-types.js +4 -0
  76. package/dist/src/policy/hitl-validation.js +39 -0
  77. package/dist/src/policy/hitl.js +117 -0
  78. package/dist/src/rules/activation.d.mts +1 -1
  79. package/dist/src/rules/activation.mjs +0 -2
  80. package/dist/src/rules/gate-coverage.js +51 -0
  81. package/dist/src/rules/gate-references.js +16 -0
  82. package/dist/src/rules/rule-frontmatter.schema.json +7 -1
  83. package/dist/src/serve/worker-command-request.js +21 -4
  84. package/dist/src/serve/worker-invocation-preparation.js +3 -1
  85. package/dist/src/serve/worker-invocation.js +21 -10
  86. package/dist/src/sessions/adapters/deepseek-harness.js +1 -0
  87. package/dist/src/sessions/adapters/grok-build-native.js +269 -0
  88. package/dist/src/sessions/adapters/grok-build.js +57 -15
  89. package/dist/src/sessions/adapters/hermes-state-db.js +252 -0
  90. package/dist/src/sessions/adapters/hermes.js +42 -3
  91. package/dist/src/sessions/adapters/warp.js +1 -1
  92. package/dist/src/sessions/batch-import.js +10 -0
  93. package/dist/src/sessions/contracts.js +1 -1
  94. package/dist/src/sessions/discovery-capability.js +38 -0
  95. package/dist/src/sessions/importer.js +8 -251
  96. package/dist/src/sessions/index.js +4 -0
  97. package/dist/src/sessions/intake-dir.js +71 -0
  98. package/dist/src/sessions/normalization.js +251 -0
  99. package/dist/src/sessions/tail.js +91 -0
  100. package/dist/src/sessions/workspace-discovery.js +83 -7
  101. package/dist/src/skills/agent-skill-sidecar.js +28 -0
  102. package/dist/src/skills/agent-skills.js +4 -23
  103. package/dist/src/skills/claude-surface-projection.js +25 -0
  104. package/dist/src/skills/deployer.js +8 -288
  105. package/dist/src/skills/deployment-policy.js +114 -0
  106. package/dist/src/skills/deployment-projection.js +152 -0
  107. package/dist/src/skills/deployment-types.js +20 -0
  108. package/dist/src/skills/exporter.js +8 -31
  109. package/dist/src/skills/provider-extensions.js +41 -0
  110. package/dist/src/skills/run.js +6 -0
  111. package/dist/src/skills/types.js +0 -8
  112. package/dist/src/skills/validator-resources.js +68 -0
  113. package/dist/src/skills/validator.js +6 -55
  114. package/dist/src/storage/cli-migrate-options.js +125 -0
  115. package/dist/src/storage/cli-migrate.js +111 -0
  116. package/dist/src/storage/cli.js +4 -279
  117. package/dist/src/storage/migration-adapter.js +79 -0
  118. package/dist/src/storage/migration-copy.js +108 -0
  119. package/dist/src/storage/migration-protocol.js +14 -197
  120. package/dist/src/storage/migration-records.js +186 -0
  121. package/dist/src/storage/migration-types.js +12 -0
  122. package/dist/src/tracker/forge-http.js +2 -1
  123. package/dist/src/tracker/issue-pages.js +110 -0
  124. package/package.json +1 -1
  125. package/schemas/mission-v1.schema.json +335 -0
  126. package/schemas/models/model-catalog.v1.schema.json +19 -1
  127. package/schemas/models/model-config-compatibility.v1.schema.json +3 -2
  128. package/schemas/models/model-policy.v1.schema.json +1 -1
  129. package/tools/agents/providers/provider-paths-staging.mjs +35 -0
  130. package/tools/agents/providers/rule-binding-targets.mjs +146 -0
  131. package/tools/agents/providers/rule-delivery.mjs +7 -126
  132. package/vscode-extension/schemas/aiwg.config.v1.json +177 -0
@@ -0,0 +1,41 @@
1
+ // Classify Claude-native fields separately from portable Agent Skills metadata.
2
+ import { AIWG_SKILL_CONTROL_FIELDS, STANDARD_SKILL_FIELDS, } from './agent-skills.js';
3
+ export const CLAUDE_SKILL_FIELDS = [
4
+ 'disable-model-invocation', 'user-invocable', 'disallowed-tools', 'context',
5
+ 'agent', 'background', 'model', 'effort', 'paths', 'shell', 'argument-hint',
6
+ 'arguments',
7
+ ];
8
+ export function agentSkillDocument(frontmatter, body) {
9
+ const standard = Object.fromEntries(STANDARD_SKILL_FIELDS
10
+ .filter((key) => Object.hasOwn(frontmatter, key))
11
+ .map((key) => [key, structuredClone(frontmatter[key])]));
12
+ const aiwg = Object.fromEntries(AIWG_SKILL_CONTROL_FIELDS
13
+ .filter((key) => Object.hasOwn(frontmatter, key))
14
+ .map((key) => [key, structuredClone(frontmatter[key])]));
15
+ const claude = Object.fromEntries(CLAUDE_SKILL_FIELDS
16
+ .filter((key) => Object.hasOwn(frontmatter, key))
17
+ .map((key) => [key, structuredClone(frontmatter[key])]));
18
+ const known = new Set([
19
+ ...STANDARD_SKILL_FIELDS, ...AIWG_SKILL_CONTROL_FIELDS, ...CLAUDE_SKILL_FIELDS,
20
+ ]);
21
+ return {
22
+ standard: standard,
23
+ aiwg,
24
+ ...(Object.keys(claude).length ? { providers: { claude } } : {}),
25
+ body,
26
+ resources: [],
27
+ unknownFields: Object.entries(frontmatter)
28
+ .filter(([key]) => !known.has(key))
29
+ .map(([key, value]) => ({ key, value: structuredClone(value) })),
30
+ };
31
+ }
32
+ /** Older sidecars retained context/effort under aiwg; preserve that spelling. */
33
+ export function claudeSkillExtensions(document) {
34
+ return {
35
+ ...Object.fromEntries(CLAUDE_SKILL_FIELDS
36
+ .filter((key) => Object.hasOwn(document.aiwg, key))
37
+ .map((key) => [key, structuredClone(document.aiwg[key])])),
38
+ ...structuredClone(document.providers?.claude ?? {}),
39
+ };
40
+ }
41
+ //# sourceMappingURL=provider-extensions.js.map
@@ -159,6 +159,12 @@ export async function runSkill(opts) {
159
159
  ...(aiwgRoot ? { AIWG_ROOT: aiwgRoot } : {}),
160
160
  ...opts.env,
161
161
  };
162
+ // Native source owner shares the same indexed script declaration and project CWD.
163
+ if (entry.path.replaceAll('\\', '/').endsWith('agentic/code/frameworks/sdlc-complete/skills/issue-audit/SKILL.md')
164
+ && entry.script.entrypoint === 'scripts/audit.mjs') {
165
+ const audit = await import('../issues/audit-cli.js');
166
+ return audit.main(opts.args, runCwd, { env });
167
+ }
162
168
  const fullArgs = [...invocation.prefixArgs, entrypointPath, ...opts.args];
163
169
  return new Promise((resolve) => {
164
170
  const child = spawn(invocation.command, fullArgs, {
@@ -1,10 +1,2 @@
1
- /**
2
- * Skills Registry Types
3
- *
4
- * Defines the RegistryAdapter interface and result types for
5
- * provider-agnostic skill search, install, and publish operations.
6
- *
7
- * @implements #539
8
- */
9
1
  export {};
10
2
  //# sourceMappingURL=types.js.map
@@ -0,0 +1,68 @@
1
+ // Diagnose bundled skill resource references without changing parser policy.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { AGENT_SKILLS_BASELINE } from './agent-skills.js';
5
+ const MARKDOWN_LINK = /!?\[[^\]]*]\(\s*(?:<([^>]+)>|([^\s)]+))(?:\s+["'][^)]*["'])?\s*\)/g;
6
+ function diagnostic(code, severity, file, yamlPath, message, remediation) {
7
+ return {
8
+ code,
9
+ severity,
10
+ file,
11
+ yamlPath,
12
+ message,
13
+ upstreamBaseline: AGENT_SKILLS_BASELINE.revision,
14
+ remediation,
15
+ };
16
+ }
17
+ export function resourceDiagnostics(content, file, skillRoot, checkResources) {
18
+ const diagnostics = [];
19
+ const references = new Set();
20
+ for (const match of content.matchAll(MARKDOWN_LINK)) {
21
+ const raw = (match[1] ?? match[2] ?? '').trim();
22
+ if (raw.length === 0
23
+ || raw.startsWith('#')
24
+ || /^(?:https?|mailto|data):/i.test(raw)) {
25
+ continue;
26
+ }
27
+ const withoutFragment = raw.split('#', 1)[0] ?? '';
28
+ const decoded = (() => {
29
+ try {
30
+ return decodeURIComponent(withoutFragment);
31
+ }
32
+ catch {
33
+ return withoutFragment;
34
+ }
35
+ })();
36
+ references.add(decoded);
37
+ }
38
+ for (const reference of [...references].sort()) {
39
+ const normalized = reference.replaceAll('\\', '/');
40
+ if (path.isAbsolute(reference)
41
+ || normalized === '..'
42
+ || normalized.startsWith('../')
43
+ || normalized.includes('/../')) {
44
+ diagnostics.push(diagnostic('AS_RESOURCE_PATH', 'warning', file, '$.body', `resource reference "${reference}" is not an in-skill relative path`, 'Reference a file relative to SKILL.md without parent traversal.'));
45
+ continue;
46
+ }
47
+ const segments = normalized.split('/').filter(Boolean);
48
+ if (segments.length > 2) {
49
+ diagnostics.push(diagnostic('AS_ADVISORY_RESOURCE_DEPTH', 'warning', file, '$.body', `resource reference "${reference}" is deeper than one resource-directory level`, 'Prefer resources such as references/topic.md, scripts/run.sh, or assets/example.json.'));
50
+ }
51
+ if (!checkResources || !skillRoot)
52
+ continue;
53
+ const resolved = path.resolve(skillRoot, reference);
54
+ const relative = path.relative(skillRoot, resolved);
55
+ if (relative.startsWith('..')
56
+ || path.isAbsolute(relative)
57
+ || !fs.existsSync(resolved)) {
58
+ diagnostics.push(diagnostic('AS_RESOURCE_MISSING', 'warning', file, '$.body', `referenced resource "${reference}" does not exist`, 'Add the referenced file or correct the relative resource path.'));
59
+ continue;
60
+ }
61
+ const stat = fs.lstatSync(resolved);
62
+ if (stat.isSymbolicLink() || !stat.isFile()) {
63
+ diagnostics.push(diagnostic('AS_RESOURCE_TYPE', 'warning', file, '$.body', `referenced resource "${reference}" is not a regular file`, 'Replace the reference target with an in-tree regular file.'));
64
+ }
65
+ }
66
+ return diagnostics;
67
+ }
68
+ //# sourceMappingURL=validator-resources.js.map
@@ -10,11 +10,13 @@
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import { parseDocument } from 'yaml';
13
+ import { resourceDiagnostics } from './validator-resources.js';
14
+ import { CLAUDE_SKILL_FIELDS } from './provider-extensions.js';
13
15
  import { AGENT_SKILLS_BASELINE, AGENT_SKILL_VALIDATION_PROFILES, AIWG_SKILL_CONTROL_FIELDS, STANDARD_SKILL_FIELDS, validateCompatibleAgentSkillMetadata, } from './agent-skills.js';
14
16
  const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
15
- const MARKDOWN_LINK = /!?\[[^\]]*]\(\s*(?:<([^>]+)>|([^\s)]+))(?:\s+["'][^)]*["'])?\s*\)/g;
16
17
  const STANDARD_FIELDS = new Set(STANDARD_SKILL_FIELDS);
17
18
  const AIWG_FIELDS = new Set(AIWG_SKILL_CONTROL_FIELDS);
19
+ const CLAUDE_FIELDS = new Set(CLAUDE_SKILL_FIELDS);
18
20
  function diagnostic(code, severity, file, yamlPath, message, remediation) {
19
21
  return {
20
22
  code,
@@ -49,64 +51,13 @@ function normalizeMetadataDiagnostics(diagnostics, frontmatter, profile, file) {
49
51
  });
50
52
  if (!policy.recognizedAiwgFields) {
51
53
  for (const key of Object.keys(frontmatter).sort()) {
52
- if (!AIWG_FIELDS.has(key))
54
+ if (!AIWG_FIELDS.has(key) && !CLAUDE_FIELDS.has(key))
53
55
  continue;
54
- normalized.push(diagnostic('AS_FIELD_EXTENSION', 'error', file, `$.${key}`, `AIWG extension field "${key}" is not allowed by the strict profile`, 'Remove the extension field or validate with the compatible profile.'));
56
+ normalized.push(diagnostic('AS_FIELD_EXTENSION', 'error', file, `$.${key}`, `Provider or AIWG extension field "${key}" is not allowed by the strict profile`, 'Remove the extension field or validate with the compatible profile.'));
55
57
  }
56
58
  }
57
59
  return normalized;
58
60
  }
59
- function resourceDiagnostics(content, file, skillRoot, checkResources) {
60
- const diagnostics = [];
61
- const references = new Set();
62
- for (const match of content.matchAll(MARKDOWN_LINK)) {
63
- const raw = (match[1] ?? match[2] ?? '').trim();
64
- if (raw.length === 0
65
- || raw.startsWith('#')
66
- || /^(?:https?|mailto|data):/i.test(raw)) {
67
- continue;
68
- }
69
- const withoutFragment = raw.split('#', 1)[0] ?? '';
70
- const decoded = (() => {
71
- try {
72
- return decodeURIComponent(withoutFragment);
73
- }
74
- catch {
75
- return withoutFragment;
76
- }
77
- })();
78
- references.add(decoded);
79
- }
80
- for (const reference of [...references].sort()) {
81
- const normalized = reference.replaceAll('\\', '/');
82
- if (path.isAbsolute(reference)
83
- || normalized === '..'
84
- || normalized.startsWith('../')
85
- || normalized.includes('/../')) {
86
- diagnostics.push(diagnostic('AS_RESOURCE_PATH', 'warning', file, '$.body', `resource reference "${reference}" is not an in-skill relative path`, 'Reference a file relative to SKILL.md without parent traversal.'));
87
- continue;
88
- }
89
- const segments = normalized.split('/').filter(Boolean);
90
- if (segments.length > 2) {
91
- diagnostics.push(diagnostic('AS_ADVISORY_RESOURCE_DEPTH', 'warning', file, '$.body', `resource reference "${reference}" is deeper than one resource-directory level`, 'Prefer resources such as references/topic.md, scripts/run.sh, or assets/example.json.'));
92
- }
93
- if (!checkResources || !skillRoot)
94
- continue;
95
- const resolved = path.resolve(skillRoot, reference);
96
- const relative = path.relative(skillRoot, resolved);
97
- if (relative.startsWith('..')
98
- || path.isAbsolute(relative)
99
- || !fs.existsSync(resolved)) {
100
- diagnostics.push(diagnostic('AS_RESOURCE_MISSING', 'warning', file, '$.body', `referenced resource "${reference}" does not exist`, 'Add the referenced file or correct the relative resource path.'));
101
- continue;
102
- }
103
- const stat = fs.lstatSync(resolved);
104
- if (stat.isSymbolicLink() || !stat.isFile()) {
105
- diagnostics.push(diagnostic('AS_RESOURCE_TYPE', 'warning', file, '$.body', `referenced resource "${reference}" is not a regular file`, 'Replace the reference target with an in-tree regular file.'));
106
- }
107
- }
108
- return diagnostics;
109
- }
110
61
  function resultState(diagnostics, profile) {
111
62
  const errors = diagnostics.filter((item) => item.severity === 'error');
112
63
  if (errors.length > 0) {
@@ -213,7 +164,7 @@ export function validateAgentSkillContent(content, options = {}) {
213
164
  // Defensive policy check: a future field added to one allow-list must not
214
165
  // silently escape classification in this parser.
215
166
  for (const key of Object.keys(frontmatter).sort()) {
216
- if (STANDARD_FIELDS.has(key) || AIWG_FIELDS.has(key))
167
+ if (STANDARD_FIELDS.has(key) || AIWG_FIELDS.has(key) || CLAUDE_FIELDS.has(key))
217
168
  continue;
218
169
  if (!diagnostics.some((item) => item.yamlPath === `$.${key}`)) {
219
170
  diagnostics.push(diagnostic('AS_FIELD_UNKNOWN', AGENT_SKILL_VALIDATION_PROFILES[profile].unknownField, file, `$.${key}`, `unrecognized top-level field "${key}"`, 'Remove the field or map it explicitly before granting it policy meaning.'));
@@ -0,0 +1,125 @@
1
+ // Parses migration flags and resolves existing storage adapters.
2
+ import { join, resolve as resolvePath } from 'node:path';
3
+ import { BACKEND_TYPES, SUBSYSTEM_KEYS, FilesystemAdapter, ObsidianAdapter, LogseqAdapter, FortemiAdapter, } from './index.js';
4
+ export function parseMigrateArgs(args) {
5
+ let subsystem;
6
+ let from;
7
+ let to;
8
+ let fromFolder;
9
+ let toFolder;
10
+ let dryRun = false;
11
+ let textOnly = false;
12
+ for (let i = 0; i < args.length; i++) {
13
+ const a = args[i];
14
+ if (a === '--from')
15
+ from = args[++i];
16
+ else if (a === '--to')
17
+ to = args[++i];
18
+ else if (a === '--from-folder')
19
+ fromFolder = args[++i];
20
+ else if (a === '--to-folder')
21
+ toFolder = args[++i];
22
+ else if (a === '--dry-run')
23
+ dryRun = true;
24
+ else if (a === '--text-only')
25
+ textOnly = true;
26
+ else if (!a.startsWith('--') && !subsystem)
27
+ subsystem = a;
28
+ else
29
+ throw new Error(`Unknown migrate flag: ${a}`);
30
+ }
31
+ if (!subsystem || !SUBSYSTEM_KEYS.includes(subsystem)) {
32
+ throw new Error(`Usage: aiwg storage migrate <subsystem> --from <type>:<location> --to <type>:<location> [--dry-run]\n Valid subsystems: ${SUBSYSTEM_KEYS.join(', ')}`);
33
+ }
34
+ if (!from)
35
+ throw new Error('storage migrate: --from <type>:<location> is required');
36
+ if (!to)
37
+ throw new Error('storage migrate: --to <type>:<location> is required');
38
+ return {
39
+ subsystem: subsystem,
40
+ from: { ...parseSpec(from), ...(fromFolder ? { folder: fromFolder } : {}) },
41
+ to: { ...parseSpec(to), ...(toFolder ? { folder: toFolder } : {}) },
42
+ dryRun,
43
+ textOnly,
44
+ };
45
+ }
46
+ export function parseSpec(raw) {
47
+ const idx = raw.indexOf(':');
48
+ if (idx === -1) {
49
+ throw new Error(`Invalid backend spec "${raw}" — expected <type>:<location>`);
50
+ }
51
+ const type = raw.slice(0, idx);
52
+ const location = raw.slice(idx + 1);
53
+ if (!BACKEND_TYPES.includes(type)) {
54
+ throw new Error(`Unknown backend type "${type}" in spec "${raw}". Valid: ${BACKEND_TYPES.join(', ')}`);
55
+ }
56
+ if (location.length === 0) {
57
+ throw new Error(`Empty location in backend spec "${raw}"`);
58
+ }
59
+ return { type: type, location };
60
+ }
61
+ export function buildAdapter(subsystem, spec, projectRoot) {
62
+ switch (spec.type) {
63
+ case 'fs': {
64
+ const root = expandFsLocation(spec.location, projectRoot);
65
+ return new FilesystemAdapter(root);
66
+ }
67
+ case 'obsidian':
68
+ return new ObsidianAdapter({
69
+ type: 'obsidian',
70
+ vault: expandFsLocation(spec.location, projectRoot),
71
+ ...(spec.folder ? { folder: spec.folder } : {}),
72
+ useCli: false,
73
+ });
74
+ case 'logseq':
75
+ return new LogseqAdapter({
76
+ type: 'logseq',
77
+ graph: expandFsLocation(spec.location, projectRoot),
78
+ useApi: false,
79
+ });
80
+ case 'fortemi':
81
+ return new FortemiAdapter({
82
+ subsystem,
83
+ config: { type: 'fortemi', mcpServer: spec.location },
84
+ });
85
+ case 'notion':
86
+ case 'anythingllm':
87
+ case 's3':
88
+ case 'webdav':
89
+ throw new Error(`storage migrate: backend "${spec.type}" not yet implemented. ` +
90
+ `See issues #959, #960, #962, #963 for tracking.`);
91
+ default: {
92
+ const _exhaustive = spec.type;
93
+ void _exhaustive;
94
+ throw new Error(`Unhandled backend type ${spec.type}`);
95
+ }
96
+ }
97
+ }
98
+ export function expandFsLocation(loc, projectRoot) {
99
+ if (loc.startsWith('~/')) {
100
+ const { homedir } = require('os');
101
+ return join(homedir(), loc.slice(2));
102
+ }
103
+ if (loc === '~') {
104
+ const { homedir } = require('os');
105
+ return homedir();
106
+ }
107
+ if (loc.startsWith('/'))
108
+ return loc;
109
+ return resolvePath(projectRoot, loc);
110
+ }
111
+ export function locationsEqual(a, b, projectRoot) {
112
+ if (a.type !== b.type)
113
+ return false;
114
+ if (a.type === 'fortemi')
115
+ return a.location === b.location;
116
+ // For file-shaped backends, compare resolved fs paths
117
+ return expandFsLocation(a.location, projectRoot) === expandFsLocation(b.location, projectRoot);
118
+ }
119
+ export function describeSpec(spec) {
120
+ return spec.folder ? `${spec.type}:${spec.location} (folder=${spec.folder})` : `${spec.type}:${spec.location}`;
121
+ }
122
+ export function specSlug(spec) {
123
+ return `${spec.type}-${spec.location.replace(/[^a-zA-Z0-9]/g, '_').slice(0, 32)}`;
124
+ }
125
+ //# sourceMappingURL=cli-migrate-options.js.map
@@ -0,0 +1,111 @@
1
+ // Runs storage migrate through the protocol copy coordinator and preserves CLI output.
2
+ import { randomUUID } from 'node:crypto';
3
+ import { appendFile, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
4
+ import { dirname } from 'node:path';
5
+ import { projectAiwgPath } from '../config/project-artifacts.js';
6
+ import { STORAGE_MIGRATION_PROTOCOL, StorageMigrationCoordinator } from './migration-protocol.js';
7
+ import { isTextCorpusEntry, migrationEndpoint } from './migration-adapter.js';
8
+ import { sha256, stableStringify } from './migration-records.js';
9
+ import { buildAdapter, describeSpec, expandFsLocation, locationsEqual, parseMigrateArgs, specSlug, } from './cli-migrate-options.js';
10
+ export async function handleMigrate(projectRoot, args) {
11
+ const opts = parseMigrateArgs(args);
12
+ if (locationsEqual(opts.from, opts.to, projectRoot)) {
13
+ throw new Error(`Refusing to migrate: source and destination resolve to the same location.\n ${describeSpec(opts.from)}\n ${describeSpec(opts.to)}`);
14
+ }
15
+ const source = buildAdapter(opts.subsystem, opts.from, projectRoot);
16
+ const destination = buildAdapter(opts.subsystem, opts.to, projectRoot);
17
+ const identity = (spec) => ({
18
+ backend: spec.type, instance: stableStringify({
19
+ location: spec.type === 'fortemi' ? spec.location : expandFsLocation(spec.location, projectRoot),
20
+ folder: spec.folder ?? '',
21
+ }), tenant: projectRoot, subsystem: opts.subsystem, schemaVersion: '1',
22
+ });
23
+ const sourceEndpoint = migrationEndpoint(source, identity(opts.from));
24
+ const destinationEndpoint = migrationEndpoint(destination, identity(opts.to));
25
+ const migrationLogPath = projectAiwgPath(projectRoot, '.storage-cache', 'migrations', `${opts.subsystem}-${specSlug(opts.from)}-to-${specSlug(opts.to)}.jsonl`);
26
+ // Bind checkpoint filenames to full, resolved identities, folders and selection policy.
27
+ const binding = sha256(stableStringify([sourceEndpoint.identity, destinationEndpoint.identity, opts.textOnly]));
28
+ const checkpointPath = `${migrationLogPath}.${binding}.checkpoint.json`;
29
+ const receiptPath = `${checkpointPath}.receipt.json`;
30
+ try {
31
+ if (source.init)
32
+ await source.init();
33
+ if (!opts.dryRun && destination.init)
34
+ await destination.init();
35
+ console.log(`storage migrate (${opts.dryRun ? 'DRY RUN' : 'live'})`);
36
+ console.log(` subsystem: ${opts.subsystem}`);
37
+ console.log(` from: ${describeSpec(opts.from)}`);
38
+ console.log(` to: ${describeSpec(opts.to)}`);
39
+ console.log('');
40
+ const checkpoint = await loadCheckpoint(checkpointPath);
41
+ const result = await StorageMigrationCoordinator.copy({
42
+ source: sourceEndpoint, destination: destinationEndpoint, checkpoint, dryRun: opts.dryRun,
43
+ exclude: path => opts.textOnly && !isTextCorpusEntry(path) ? 'non-text attachment skipped' : undefined,
44
+ async save(value) {
45
+ await atomicJson(checkpointPath, value);
46
+ await atomicJson(receiptPath, { protocol: value.protocol, migrationId: value.migrationId, ...value.receipt });
47
+ },
48
+ async report(action, item) {
49
+ if (action === 'errored')
50
+ console.log(` ✗ ${item.path} (${item.reason})`);
51
+ else if (action === 'skipped') {
52
+ const marker = item.reason === 'already migrated, skipped' ? '✓' : '·';
53
+ console.log(` ${marker} ${item.path} (${item.reason})`);
54
+ }
55
+ else if (opts.dryRun) {
56
+ console.log(` → ${item.path} (would ${action === 'deleted' ? 'delete' : 'copy'})`);
57
+ }
58
+ else {
59
+ console.log(` ✓ ${item.path}${action === 'copied' ? '' : ` (${action}: ${item.reason})`}`);
60
+ await mkdir(dirname(migrationLogPath), { recursive: true });
61
+ await appendFile(migrationLogPath, JSON.stringify({
62
+ path: item.path, ts: new Date().toISOString(), action, reason: item.reason, protocol: STORAGE_MIGRATION_PROTOCOL,
63
+ }) + '\n', 'utf-8');
64
+ }
65
+ },
66
+ });
67
+ const receipt = result.receipt;
68
+ const unsupported = receipt.skipped.filter(item => item.reason === 'non-text attachment skipped').length;
69
+ const copied = receipt.copied.length + receipt.recopied.length;
70
+ const skipped = receipt.skipped.length - unsupported;
71
+ if (!result.plan.length)
72
+ console.log('No entries to migrate.');
73
+ console.log('');
74
+ console.log(`Summary: copied=${copied} skipped=${skipped} unsupported=${unsupported} ` +
75
+ `errored=${receipt.errored.length} total=${result.plan.length} ` +
76
+ `recopied=${receipt.recopied.length} deleted=${receipt.deleted.length}`);
77
+ if (!opts.dryRun) {
78
+ console.log(`Migration log: ${migrationLogPath}`);
79
+ console.log(`Migration checkpoint: ${checkpointPath}`);
80
+ console.log(`Migration receipt: ${receiptPath}`);
81
+ }
82
+ if (receipt.errored.length) {
83
+ throw new Error(`${receipt.errored.length} entr${receipt.errored.length === 1 ? 'y' : 'ies'} failed to migrate.`);
84
+ }
85
+ }
86
+ finally {
87
+ await Promise.allSettled([source.close?.(), destination.close?.()]);
88
+ }
89
+ }
90
+ async function loadCheckpoint(path) {
91
+ try {
92
+ return JSON.parse(await readFile(path, 'utf-8'));
93
+ }
94
+ catch (error) {
95
+ if (error.code === 'ENOENT')
96
+ return undefined;
97
+ throw error;
98
+ }
99
+ }
100
+ async function atomicJson(path, value) {
101
+ await mkdir(dirname(path), { recursive: true });
102
+ const temporary = `${path}.${randomUUID()}.tmp`;
103
+ try {
104
+ await writeFile(temporary, JSON.stringify(value, null, 2) + '\n', 'utf-8');
105
+ await rename(temporary, path);
106
+ }
107
+ finally {
108
+ await rm(temporary, { force: true });
109
+ }
110
+ }
111
+ //# sourceMappingURL=cli-migrate.js.map