claude-git-hooks 2.45.0 → 2.61.2
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/CHANGELOG.md +386 -21
- package/CLAUDE.md +11 -12
- package/README.md +7 -0
- package/lib/commands/analyze-diff.js +27 -0
- package/lib/commands/back-merge.js +95 -1
- package/lib/commands/bump-version.js +39 -0
- package/lib/commands/close-release.js +22 -0
- package/lib/commands/create-pr.js +73 -1
- package/lib/commands/create-release.js +198 -30
- package/lib/commands/help.js +16 -60
- package/lib/hooks/pre-commit.js +36 -0
- package/lib/messages/library-warnings.js +29 -0
- package/lib/utils/github-api.js +30 -0
- package/lib/utils/linter-runner.js +6 -0
- package/lib/utils/version-manager.js +6 -8
- package/package.json +83 -75
package/CLAUDE.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# claude-git-hooks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Intelligent Git hooks system integrating Claude CLI for code analysis, commit message generation, PR creation, and release workflow automation.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Library
|
|
6
6
|
|
|
7
7
|
All project knowledge lives in [`.library/`](.library/). Start at [`.library/index.md`](.library/index.md).
|
|
8
8
|
|
|
@@ -10,15 +10,16 @@ Load context based on your current task:
|
|
|
10
10
|
|
|
11
11
|
| Task | Load |
|
|
12
12
|
|------|------|
|
|
13
|
-
| Writing or modifying code |
|
|
14
|
-
| Understanding a source module |
|
|
15
|
-
| Understanding a business workflow |
|
|
16
|
-
| Adding a new CLI command |
|
|
17
|
-
| Updating the library after code changes | [`.library/README.md`](.library/README.md) — book schema, template, creation steps |
|
|
13
|
+
| Writing or modifying code | `conventions.md` (Library root) — coding standards, testing patterns |
|
|
14
|
+
| Understanding a source module | `@by-code/` — find the book for that file |
|
|
15
|
+
| Understanding a business workflow | `@by-domain/` — commit pipeline, release management, PR analysis, GitHub integration |
|
|
16
|
+
| Adding a new CLI command | `@by-task-type/add-new-command.md` — 7-book reading sequence |
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
**Programmatic access**: `fetchLibraryContent(taskOrTopic)` in `.library/librarian/index.js` loads CLAUDE.md + index.md + conventions.md as fixed context, then routes the topic to the relevant index.
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
**Budget-aware loading**: Every index carries `reading_cost_tokens`; task-type sequences split into core vs conditional books. Check token budgets before loading full sequences.
|
|
21
|
+
|
|
22
|
+
## Rules
|
|
22
23
|
|
|
23
24
|
These behavioral rules apply to all work in this repository:
|
|
24
25
|
|
|
@@ -31,5 +32,3 @@ These behavioral rules apply to all work in this repository:
|
|
|
31
32
|
7. **Do NOT use `console.log`** — always use `logger.js`: `info()`, `warning()`, `error()`, `debug()`
|
|
32
33
|
8. **Platform-specific care** — use `path.join()` (no hardcoded `/`); test on Windows, Linux, macOS
|
|
33
34
|
9. **Input sanitization** — use `sanitize.js` for user inputs in shell commands; avoid `shell: true` in `spawn()`
|
|
34
|
-
|
|
35
|
-
> See [`CLAUDE-MIGRATION.md`](CLAUDE-MIGRATION.md) for a mapping of where each original CLAUDE.md section moved.
|
package/README.md
CHANGED
|
@@ -529,6 +529,13 @@ claude-git-hooks/
|
|
|
529
529
|
│ ├── diff-analysis-orchestrator.js # Intelligent batch orchestration
|
|
530
530
|
│ ├── judge.js # Auto-fix judge (v2.20.0)
|
|
531
531
|
│ └── token-store.js # Token persistence - settings.local.json
|
|
532
|
+
├── .library/ # Code Knowledge Library - auto-generated module docs
|
|
533
|
+
│ ├── books/ # One book per source module (auto + manual sections)
|
|
534
|
+
│ ├── extractor/ # Tree-sitter AST tooling
|
|
535
|
+
│ │ ├── extract.js # Source → book auto-section generator
|
|
536
|
+
│ │ ├── parser.js # WASM parser init and grammar loading
|
|
537
|
+
│ │ └── adapters/ # Language-specific CST → normalized AST
|
|
538
|
+
│ └── templates/ # Book schema and category reference
|
|
532
539
|
├── templates/
|
|
533
540
|
│ ├── pre-commit # Bash wrapper - invokes Node.js
|
|
534
541
|
│ ├── prepare-commit-msg # Bash wrapper - invokes Node.js
|
|
@@ -93,6 +93,33 @@ export async function runAnalyzeDiff(args) {
|
|
|
93
93
|
console.log(result.testingNotes);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// Library staleness section
|
|
97
|
+
try {
|
|
98
|
+
const { runAll } = await import('../../.library/tools/staleness.js');
|
|
99
|
+
const { getSourceDir, getBooksDir } = await import('../../.library/paths.js');
|
|
100
|
+
const { getRepoRoot } = await import('../utils/git-operations.js');
|
|
101
|
+
const stalenessResult = await runAll(getBooksDir(), getSourceDir(), getRepoRoot(), false);
|
|
102
|
+
const staleCount = stalenessResult.stale.length +
|
|
103
|
+
stalenessResult.orphan_books.length +
|
|
104
|
+
stalenessResult.unbooked_sources.length;
|
|
105
|
+
if (staleCount > 0) {
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log(`📚 ${colors.yellow}Library Staleness:${colors.reset} ${staleCount} book(s) need attention`);
|
|
108
|
+
for (const item of stalenessResult.stale) {
|
|
109
|
+
console.log(` └─ stale: ${item.book}`);
|
|
110
|
+
}
|
|
111
|
+
for (const item of stalenessResult.orphan_books) {
|
|
112
|
+
console.log(` └─ orphan: ${item.book}`);
|
|
113
|
+
}
|
|
114
|
+
for (const src of stalenessResult.unbooked_sources) {
|
|
115
|
+
console.log(` └─ unbooked: ${src}`);
|
|
116
|
+
}
|
|
117
|
+
console.log(' Run: npm run library:regenerate');
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
logger.warning('📚 Library staleness check unavailable — .library/ tools not found');
|
|
121
|
+
}
|
|
122
|
+
|
|
96
123
|
// Save the results in a file with context
|
|
97
124
|
const outputData = {
|
|
98
125
|
prTitle: result.prTitle,
|
|
@@ -39,7 +39,8 @@ import {
|
|
|
39
39
|
checkoutBranch,
|
|
40
40
|
pushBranch,
|
|
41
41
|
deleteRemoteBranch,
|
|
42
|
-
createCommit
|
|
42
|
+
createCommit,
|
|
43
|
+
stageFiles
|
|
43
44
|
} from '../utils/git-operations.js';
|
|
44
45
|
import {
|
|
45
46
|
getLatestLocalTag,
|
|
@@ -650,6 +651,53 @@ export async function runBackMerge(args) {
|
|
|
650
651
|
showSuccess(`✓ Merge committed: ${commitMsg}`);
|
|
651
652
|
console.log('');
|
|
652
653
|
|
|
654
|
+
// 14b. Co-change correlation pipeline (AUT-3777)
|
|
655
|
+
let coChangeReport = null;
|
|
656
|
+
try {
|
|
657
|
+
const { coChangePipeline } = await import('../../.library/librarian/index.js');
|
|
658
|
+
|
|
659
|
+
showInfo('Running co-change correlation pipeline...');
|
|
660
|
+
const summary = coChangePipeline({ repoCtx: {} });
|
|
661
|
+
const { modifiedFiles, perStep, report, warnings, mode } = summary;
|
|
662
|
+
|
|
663
|
+
// Surface summary to stdout
|
|
664
|
+
showInfo(
|
|
665
|
+
`Co-change: mode=${mode}, detected=${perStep.detection.modifications}, ` +
|
|
666
|
+
`injected=${perStep.injection.modifications}, warnings=${warnings.length}`
|
|
667
|
+
);
|
|
668
|
+
for (const w of warnings) {
|
|
669
|
+
showWarning(w);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Save report for PR body attachment after push
|
|
673
|
+
coChangeReport = report;
|
|
674
|
+
|
|
675
|
+
if (modifiedFiles.length === 0) {
|
|
676
|
+
showInfo('Co-change pipeline produced no Library changes');
|
|
677
|
+
} else {
|
|
678
|
+
const root = getRepoRoot();
|
|
679
|
+
const absFiles = modifiedFiles.map(f => path.isAbsolute(f) ? f : path.join(root, f));
|
|
680
|
+
const stageResult = stageFiles(absFiles);
|
|
681
|
+
|
|
682
|
+
if (!stageResult.success) {
|
|
683
|
+
showWarning(`Failed to stage co-change files: ${stageResult.error}`);
|
|
684
|
+
} else {
|
|
685
|
+
const windowDesc = tagName ? `${tagName}..HEAD` : 'full history';
|
|
686
|
+
const libCommitMsg = `chore(library): co-change correlations for ${windowDesc}`;
|
|
687
|
+
const libCommit = createCommit(libCommitMsg);
|
|
688
|
+
|
|
689
|
+
if (!libCommit.success) {
|
|
690
|
+
showWarning(`Failed to commit co-change files: ${libCommit.error}`);
|
|
691
|
+
} else {
|
|
692
|
+
showSuccess(`Co-change committed: ${libCommitMsg} (${modifiedFiles.length} file(s))`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
} catch (err) {
|
|
697
|
+
logger.warning(`co-change pipeline skipped: ${err.message}`);
|
|
698
|
+
logger.debug('back-merge', 'Co-change pipeline skipped', { error: err.message });
|
|
699
|
+
}
|
|
700
|
+
|
|
653
701
|
// 15. Verify sync
|
|
654
702
|
try {
|
|
655
703
|
const postMergeDivergence = getDivergence(opts.into, `origin/${opts.from}`);
|
|
@@ -719,6 +767,52 @@ export async function runBackMerge(args) {
|
|
|
719
767
|
await _revertFollowup(rcBranchForLog, repoRoot);
|
|
720
768
|
}
|
|
721
769
|
|
|
770
|
+
// 18b. Attach co-change report to PR body (AUT-3777)
|
|
771
|
+
if (coChangeReport && pushStatus === 'pushed') {
|
|
772
|
+
try {
|
|
773
|
+
const { parseGitHubRepo } = await import('../utils/github-client.js');
|
|
774
|
+
const { findExistingPR, updatePullRequestBody } = await import('../utils/github-api.js');
|
|
775
|
+
const repoInfo = parseGitHubRepo();
|
|
776
|
+
|
|
777
|
+
const existingPR = await findExistingPR({
|
|
778
|
+
owner: repoInfo.owner,
|
|
779
|
+
repo: repoInfo.repo,
|
|
780
|
+
head: opts.from,
|
|
781
|
+
base: opts.into,
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
if (existingPR) {
|
|
785
|
+
const OPEN_MARKER = '<!-- LIBRARY_COCHANGE_REPORT -->';
|
|
786
|
+
const CLOSE_MARKER = '<!-- /LIBRARY_COCHANGE_REPORT -->';
|
|
787
|
+
const section = `${OPEN_MARKER}\n${coChangeReport}\n${CLOSE_MARKER}`;
|
|
788
|
+
const markerRegex = new RegExp(
|
|
789
|
+
`${OPEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${CLOSE_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
let newBody;
|
|
793
|
+
if (markerRegex.test(existingPR.body || '')) {
|
|
794
|
+
// Re-run: replace existing section
|
|
795
|
+
newBody = (existingPR.body).replace(markerRegex, section);
|
|
796
|
+
} else {
|
|
797
|
+
// First run: append section
|
|
798
|
+
newBody = `${existingPR.body || ''}\n\n${section}`;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
await updatePullRequestBody(repoInfo.owner, repoInfo.repo, existingPR.number, newBody);
|
|
802
|
+
showSuccess('Co-change report attached to PR body');
|
|
803
|
+
} else {
|
|
804
|
+
logger.debug('back-merge', 'No existing PR found for report attachment', {
|
|
805
|
+
head: opts.from,
|
|
806
|
+
base: opts.into,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
} catch (prErr) {
|
|
810
|
+
logger.debug('back-merge', 'Could not attach co-change report to PR', {
|
|
811
|
+
error: prErr.message,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
722
816
|
// 19. Summary
|
|
723
817
|
console.log('');
|
|
724
818
|
console.log(`${colors.green}═════════════════════════════════════════════════${colors.reset}`);
|
|
@@ -54,6 +54,10 @@ import {
|
|
|
54
54
|
} from '../utils/interactive-ui.js';
|
|
55
55
|
import logger from '../utils/logger.js';
|
|
56
56
|
import { colors, error, checkGitRepo } from './helpers.js';
|
|
57
|
+
import {
|
|
58
|
+
CONSOLE_WARNING_TEMPLATE,
|
|
59
|
+
LIBRARY_VERIFY_SKIPPED_WARNING
|
|
60
|
+
} from '../messages/library-warnings.js';
|
|
57
61
|
|
|
58
62
|
/**
|
|
59
63
|
* Validates prerequisites before version bump
|
|
@@ -374,6 +378,25 @@ function restoreSnapshot(snapshot) {
|
|
|
374
378
|
});
|
|
375
379
|
}
|
|
376
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Emit a loud Library staleness warning to stderr.
|
|
383
|
+
* Content comes from the wording constant + verify result data.
|
|
384
|
+
* Formatting uses ANSI codes for visual emphasis on TTY stderr.
|
|
385
|
+
*
|
|
386
|
+
* @param {import('../../.library/librarian/index.js').VerifyResult} result
|
|
387
|
+
* @private
|
|
388
|
+
*/
|
|
389
|
+
function _emitLibraryWarning(result) {
|
|
390
|
+
const y = colors.yellow;
|
|
391
|
+
const r = colors.reset;
|
|
392
|
+
const bar = `${y}${'='.repeat(63)}${r}`;
|
|
393
|
+
const content = CONSOLE_WARNING_TEMPLATE(result, { autoRegen: 'deferred' });
|
|
394
|
+
|
|
395
|
+
const lines = ['', bar, '', `${y}${content}${r}`, '', bar, ''];
|
|
396
|
+
|
|
397
|
+
process.stderr.write(lines.join('\n'));
|
|
398
|
+
}
|
|
399
|
+
|
|
377
400
|
/**
|
|
378
401
|
* Bump version command
|
|
379
402
|
* @param {Array<string>} args - Command arguments
|
|
@@ -441,6 +464,22 @@ export async function runBumpVersion(args) {
|
|
|
441
464
|
|
|
442
465
|
showSuccess('Prerequisites validated');
|
|
443
466
|
|
|
467
|
+
// Library verification gate — silent on clean, loud-warn on stale, never-hard-fail
|
|
468
|
+
logger.debug('bump-version', 'Running Library verification gate');
|
|
469
|
+
try {
|
|
470
|
+
const { verify } = await import('../../.library/librarian/index.js');
|
|
471
|
+
const verifyResult = await verify();
|
|
472
|
+
|
|
473
|
+
if (verifyResult.clean) {
|
|
474
|
+
logger.debug('bump-version', 'Library is clean — no warning needed');
|
|
475
|
+
} else {
|
|
476
|
+
_emitLibraryWarning(verifyResult);
|
|
477
|
+
}
|
|
478
|
+
} catch (verifyErr) {
|
|
479
|
+
const msg = `\n${colors.yellow} ${LIBRARY_VERIFY_SKIPPED_WARNING} ${verifyErr.message}${colors.reset}\n\n`;
|
|
480
|
+
process.stderr.write(msg);
|
|
481
|
+
}
|
|
482
|
+
|
|
444
483
|
// Step 2: Discover all version files
|
|
445
484
|
logger.debug('bump-version', 'Step 2: Discovering version files');
|
|
446
485
|
const discovery = discoverVersionFiles();
|
|
@@ -356,6 +356,28 @@ export async function runCloseRelease(args) {
|
|
|
356
356
|
console.log('');
|
|
357
357
|
|
|
358
358
|
try {
|
|
359
|
+
// Library staleness gate — block release closure if books are stale
|
|
360
|
+
try {
|
|
361
|
+
showInfo('📚 Checking library staleness...');
|
|
362
|
+
const { runAll, formatHuman } = await import('../../.library/tools/staleness.js');
|
|
363
|
+
const { getSourceDir, getBooksDir } = await import('../../.library/paths.js');
|
|
364
|
+
const { getRepoRoot } = await import('../utils/git-operations.js');
|
|
365
|
+
const stalenessResult = await runAll(getBooksDir(), getSourceDir(), getRepoRoot(), false);
|
|
366
|
+
const hasDrift = stalenessResult.stale.length > 0 ||
|
|
367
|
+
stalenessResult.orphan_books.length > 0 ||
|
|
368
|
+
stalenessResult.unbooked_sources.length > 0;
|
|
369
|
+
if (hasDrift) {
|
|
370
|
+
error('📚 Library staleness detected — cannot close release with stale books');
|
|
371
|
+
process.stdout.write(formatHuman(stalenessResult));
|
|
372
|
+
error('Run: npm run library:regenerate');
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
showSuccess('📚 Library books are current');
|
|
376
|
+
} catch (err) {
|
|
377
|
+
showWarning('📚 Library staleness check unavailable — .library/ tools not found');
|
|
378
|
+
logger.debug('close-release', 'Library staleness check skipped', { error: err.message });
|
|
379
|
+
}
|
|
380
|
+
|
|
359
381
|
// ── Step 7: git reset --soft origin/main ─────────────────────────────
|
|
360
382
|
|
|
361
383
|
showInfo('Resetting to origin/main (--soft)...');
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
showWarning,
|
|
23
23
|
promptConfirmation
|
|
24
24
|
} from '../utils/interactive-ui.js';
|
|
25
|
-
import { getBranchPushStatus, pushBranch } from '../utils/git-operations.js';
|
|
25
|
+
import { getBranchPushStatus, pushBranch, stageFiles, createCommit, getRepoRoot } from '../utils/git-operations.js';
|
|
26
26
|
import logger from '../utils/logger.js';
|
|
27
27
|
import { resolveLabels } from '../utils/label-resolver.js';
|
|
28
28
|
import { CostTracker } from '../utils/cost-tracker.js';
|
|
@@ -664,6 +664,78 @@ export async function runCreatePr(args) {
|
|
|
664
664
|
logger.debug('create-pr', 'No unpushed tags found, continuing');
|
|
665
665
|
}
|
|
666
666
|
|
|
667
|
+
// Step 5.8: Library maintenance pipeline (AUT-3764)
|
|
668
|
+
logger.debug('create-pr', 'Step 5.8: Running Library maintenance pipeline');
|
|
669
|
+
try {
|
|
670
|
+
showInfo('Running Library maintenance pipeline...');
|
|
671
|
+
const { createPrPipeline } = await import('../../.library/librarian/index.js');
|
|
672
|
+
const root = getRepoRoot();
|
|
673
|
+
|
|
674
|
+
const pipelineSummary = await createPrPipeline({ repoRoot: root });
|
|
675
|
+
const {
|
|
676
|
+
modifiedFiles: libraryFiles,
|
|
677
|
+
perStep,
|
|
678
|
+
pendingDueToApiDown,
|
|
679
|
+
warnings: pipelineWarnings,
|
|
680
|
+
} = pipelineSummary;
|
|
681
|
+
|
|
682
|
+
// Surface pipeline summary
|
|
683
|
+
logger.debug('create-pr', 'Pipeline completed', {
|
|
684
|
+
modifiedCount: libraryFiles.length,
|
|
685
|
+
pendingDueToApiDown,
|
|
686
|
+
warningCount: pipelineWarnings.length,
|
|
687
|
+
});
|
|
688
|
+
showInfo(`Staleness: ${perStep.staleness.staleCount} stale, ${perStep.staleness.unbookedCount} unbooked`);
|
|
689
|
+
if (perStep.regen.changed > 0) {
|
|
690
|
+
showInfo(`Regenerated: ${perStep.regen.changed} book(s)`);
|
|
691
|
+
}
|
|
692
|
+
if (perStep.addRemoveRename.created > 0) {
|
|
693
|
+
showInfo(`Created: ${perStep.addRemoveRename.created} new book(s)`);
|
|
694
|
+
}
|
|
695
|
+
if (pendingDueToApiDown > 0) {
|
|
696
|
+
showWarning(`Gotchas pending (API down): ${pendingDueToApiDown}`);
|
|
697
|
+
}
|
|
698
|
+
for (const w of pipelineWarnings) {
|
|
699
|
+
showWarning(w);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (libraryFiles.length === 0) {
|
|
703
|
+
showInfo('Library already in sync');
|
|
704
|
+
} else {
|
|
705
|
+
// Stage all modified Library files (stageFiles expects absolute paths)
|
|
706
|
+
const absFiles = libraryFiles.map(f => path.join(root, f));
|
|
707
|
+
const stageResult = stageFiles(absFiles);
|
|
708
|
+
|
|
709
|
+
if (!stageResult.success) {
|
|
710
|
+
showWarning(`Failed to stage Library files: ${stageResult.error}`);
|
|
711
|
+
} else {
|
|
712
|
+
// Commit with deterministic message — separate commit, not amend
|
|
713
|
+
const libraryCommitMsg = `chore(library): sync books for ${currentBranch}`;
|
|
714
|
+
const commitResult = createCommit(libraryCommitMsg);
|
|
715
|
+
|
|
716
|
+
if (!commitResult.success) {
|
|
717
|
+
showWarning(`Failed to commit Library changes: ${commitResult.error}`);
|
|
718
|
+
} else {
|
|
719
|
+
showSuccess(`Library committed: ${libraryCommitMsg} (${libraryFiles.length} file(s))`);
|
|
720
|
+
|
|
721
|
+
// Push the Library commit to the PR branch's remote
|
|
722
|
+
const libraryPushResult = pushBranch(currentBranch);
|
|
723
|
+
if (!libraryPushResult.success) {
|
|
724
|
+
showWarning(`Failed to push Library commit: ${libraryPushResult.error}`);
|
|
725
|
+
} else {
|
|
726
|
+
showSuccess('Library commit pushed');
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
} catch (pipelineErr) {
|
|
732
|
+
// Pipeline failure is non-blocking — log and continue with PR creation
|
|
733
|
+
logger.warning('create-pr', 'Library pipeline failed, continuing', {
|
|
734
|
+
error: pipelineErr.message,
|
|
735
|
+
});
|
|
736
|
+
showWarning(`Library pipeline unavailable: ${pipelineErr.message}`);
|
|
737
|
+
}
|
|
738
|
+
|
|
667
739
|
// Step 6: Generate PR metadata using engine
|
|
668
740
|
logger.debug('create-pr', 'Step 6: Generating PR metadata with engine');
|
|
669
741
|
showInfo('Generating PR metadata with Claude...');
|