freeharness 0.4.1 → 0.4.3
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/NOTICE +2 -1
- package/README.md +24 -19
- package/canonical/HARVEST.yaml +27 -16
- package/canonical/PROVENANCE.yaml +3 -3
- package/canonical/agents/README.md +77 -36
- package/canonical/agents/agentic-architect.md +2 -1
- package/canonical/agents/database-reviewer.md +6 -8
- package/canonical/agents/explorer-ai-engineer-analyst.md +2 -6
- package/canonical/agents/implementer.md +30 -0
- package/canonical/agents/pr-reviewer.md +9 -11
- package/canonical/agents/python-reviewer.md +7 -9
- package/canonical/agents/report-formatter.md +7 -3
- package/canonical/agents/security-reviewer.md +14 -9
- package/canonical/agents/test-writer.md +5 -2
- package/canonical/hooks/cost-prices.json +11 -9
- package/canonical/hooks/freeharness-config-guard.mjs +4155 -93
- package/canonical/hooks/freeharness-context-monitor.mjs +88 -54
- package/canonical/hooks/freeharness-converge.mjs +78 -23
- package/canonical/hooks/freeharness-cost-tracker.mjs +1116 -218
- package/canonical/hooks/freeharness-memory-guard.mjs +20 -23
- package/canonical/hooks/freeharness-precompact.mjs +206 -87
- package/canonical/hooks/freeharness-quota-tap.mjs +173 -0
- package/canonical/hooks/freeharness-reload-context.mjs +197 -0
- package/canonical/hooks/freeharness-repeat-tool.mjs +1 -1
- package/canonical/hooks/freeharness-sdd-context.mjs +959 -0
- package/canonical/hooks/freeharness-session-start +297 -59
- package/canonical/hooks/hooks.claude-code.json +1 -1
- package/canonical/hooks/hooks.config-guard.claude-code.json +2 -2
- package/canonical/hooks/hooks.context-monitor.claude-code.json +1 -1
- package/canonical/hooks/hooks.converge.claude-code.json +1 -1
- package/canonical/hooks/hooks.memory-guard.claude-code.json +1 -1
- package/canonical/hooks/hooks.reload-context.claude-code.json +15 -0
- package/canonical/openspec/schema.spec-driven.yaml +29 -8
- package/canonical/openspec/templates/follow-ups.md +3 -2
- package/canonical/openspec/templates/proposal.md +1 -1
- package/canonical/openspec/templates/tasks.md +8 -0
- package/canonical/rules/CLAUDE.md +5 -4
- package/canonical/rules/routing.md +10 -9
- package/canonical/skills/agent-evaluation/SKILL.md +17 -6
- package/canonical/skills/agent-evaluation/evals/cases.yaml +38 -23
- package/canonical/skills/agentic-architecture/SKILL.md +4 -0
- package/canonical/skills/agentic-architecture/evals/cases.yaml +27 -0
- package/canonical/skills/code-review/SKILL.md +10 -3
- package/canonical/skills/code-review/evals/cases.yaml +1118 -27
- package/canonical/skills/git-workflow/SKILL.md +2 -0
- package/canonical/skills/local-code/SKILL.md +2 -2
- package/canonical/skills/local-code/evals/cases.yaml +1 -1
- package/canonical/skills/report-formatting/SKILL.md +10 -2
- package/canonical/skills/report-formatting/reference/ARCHITECTURE.md +3 -3
- package/canonical/skills/report-formatting/reference/HELPERS.md +10 -5
- package/canonical/skills/report-formatting/reference/RELEASE.md +1 -2
- package/canonical/skills/report-formatting/reference/TEMPLATES.md +3 -2
- package/canonical/skills/report-formatting/reference/authoring.md +25 -37
- package/canonical/skills/report-formatting/scripts/build-architecture.js +1 -1
- package/canonical/skills/report-formatting/scripts/build-md-claro.js +5 -8
- package/canonical/skills/report-formatting/scripts/check-env.js +0 -4
- package/canonical/skills/report-formatting/scripts/docmodel.js +3 -11
- package/canonical/skills/report-formatting/scripts/example-build.js +8 -10
- package/canonical/skills/report-formatting/scripts/example_build_ArchitectureReview_docx.js +8 -10
- package/canonical/skills/report-formatting/scripts/example_build_ExecutiveReport_docx.js +8 -10
- package/canonical/skills/report-formatting/scripts/theme-claro.js +39 -10
- package/canonical/skills/subagent-driven-development/SKILL.md +35 -28
- package/canonical/skills/subagent-driven-development/evals/cases.yaml +3625 -19
- package/canonical/skills/subagent-driven-development/reference/brief.md +187 -35
- package/canonical/skills/subagent-driven-development/reference/handoff.md +96 -2
- package/canonical/skills/subagent-driven-development/reference/loop.md +56 -0
- package/canonical/skills/subagent-driven-development/reference/trees.md +40 -0
- package/canonical/skills/subagent-driven-development/reference/workspace.md +31 -0
- package/canonical/skills/subagent-driven-development/scripts/validate-handoff.mjs +4035 -327
- package/dist/adapters/harness/claude-code.d.ts +26 -3
- package/dist/adapters/harness/claude-code.js +214 -43
- package/dist/adapters/harness/opencode.js +17 -3
- package/dist/adapters/harness/plugin.js +28 -17
- package/dist/adapters/memory/doctor.d.ts +172 -33
- package/dist/adapters/memory/doctor.js +750 -139
- package/dist/adapters/memory/migrate.d.ts +95 -0
- package/dist/adapters/memory/migrate.js +497 -0
- package/dist/adapters/memory/paths.d.ts +15 -0
- package/dist/adapters/memory/paths.js +47 -0
- package/dist/adapters/memory/vendored.d.ts +52 -30
- package/dist/adapters/memory/vendored.js +161 -169
- package/dist/adapters/memory/worker-control.d.ts +9 -0
- package/dist/adapters/memory/worker-control.js +30 -0
- package/dist/adapters/sync/diff.js +4 -1
- package/dist/cli/cleanup-prompt.d.ts +35 -0
- package/dist/cli/cleanup-prompt.js +68 -0
- package/dist/cli/cleanup.d.ts +22 -24
- package/dist/cli/cleanup.js +204 -75
- package/dist/cli/cost-quota.d.ts +158 -0
- package/dist/cli/cost-quota.js +645 -0
- package/dist/cli/cost.d.ts +408 -46
- package/dist/cli/cost.js +1731 -57
- package/dist/cli/doctor.d.ts +52 -13
- package/dist/cli/doctor.js +199 -45
- package/dist/cli/hard-cleanup.d.ts +93 -0
- package/dist/cli/hard-cleanup.js +552 -0
- package/dist/cli/install-facts.d.ts +29 -0
- package/dist/cli/install-facts.js +68 -0
- package/dist/cli/json-key-edit.d.ts +18 -0
- package/dist/cli/json-key-edit.js +118 -0
- package/dist/cli/memory-import.d.ts +74 -0
- package/dist/cli/memory-import.js +469 -0
- package/dist/cli/plugin-validate.d.ts +12 -0
- package/dist/cli/plugin-validate.js +43 -0
- package/dist/cli/profile.d.ts +1 -1
- package/dist/cli/profile.js +10 -10
- package/dist/cli/uninstall.d.ts +1 -1
- package/dist/cli/uninstall.js +2 -2
- package/dist/cli/upstream-launchers.d.ts +49 -0
- package/dist/cli/upstream-launchers.js +287 -0
- package/dist/index.js +252 -73
- package/dist/infra/fsutil.d.ts +30 -3
- package/dist/infra/fsutil.js +68 -6
- package/dist/infra/manifest.d.ts +2 -1
- package/dist/infra/manifest.js +3 -2
- package/dist/infra/prices.d.ts +18 -0
- package/dist/infra/prices.js +64 -0
- package/node_modules/@freeharness/core/dist/registry.d.ts +1 -1
- package/node_modules/@freeharness/core/dist/registry.js +3 -3
- package/node_modules/@freeharness/core/dist/tiers.d.ts +10 -1
- package/node_modules/@freeharness/core/dist/tiers.js +24 -2
- package/node_modules/@freeharness/core/package.json +1 -1
- package/node_modules/@freeharness/sdd/PROVENANCE.yaml +279 -2
- package/node_modules/@freeharness/sdd/dist/src/bin/openspec.js +34 -8
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.d.ts +7 -6
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.js +72 -64
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.d.ts +48 -16
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.js +168 -70
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.d.ts +36 -6
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.js +110 -49
- package/node_modules/@freeharness/sdd/dist/src/shim/list.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/list.js +57 -14
- package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.d.ts +7 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.js +118 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/new-change.js +3 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.js +94 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.d.ts +17 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.js +182 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/status.d.ts +2 -2
- package/node_modules/@freeharness/sdd/dist/src/shim/status.js +34 -5
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.js +49 -41
- package/node_modules/@freeharness/sdd/dist/src/shim/warnings.d.ts +21 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/warnings.js +66 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.d.ts +4 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.js +17 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/specs-apply.js +95 -22
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.js +14 -6
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.js +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.d.ts +3 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.js +24 -4
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/task-progress.js +4 -7
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/types.js +13 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/apply-change.js +21 -6
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/archive-change.js +80 -19
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/explore.js +42 -22
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.d.ts +32 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.js +39 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/propose.js +16 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/sync-specs.js +36 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/update-change.js +65 -30
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.js +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.d.ts +8 -13
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.js +79 -102
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/markdown-parser.js +4 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.d.ts +50 -10
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.js +165 -37
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.d.ts +24 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.js +55 -8
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.js +4 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.js +19 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.d.ts +10 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.js +115 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.d.ts +3 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.js +20 -20
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.js +75 -14
- package/node_modules/@freeharness/sdd/package.json +1 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/schema.upstream.yaml +47 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-apply-change/SKILL.md +25 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-archive-change/SKILL.md +50 -11
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-bulk-archive-change/SKILL.md +48 -10
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-continue-change/SKILL.md +27 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-explore/SKILL.md +25 -12
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-ff-change/SKILL.md +17 -5
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-new-change/SKILL.md +12 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-onboard/SKILL.md +47 -33
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-propose/SKILL.md +17 -5
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-sync-specs/SKILL.md +29 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-update-change/SKILL.md +31 -13
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-verify-change/SKILL.md +75 -27
- package/package.json +6 -4
- package/provenance/canonical.PROVENANCE.yaml +3 -3
- package/provenance/memory.PROVENANCE.yaml +472 -6
- package/provenance/sdd.PROVENANCE.yaml +279 -2
- package/canonical/hooks/freeharness-auto-memory.mjs +0 -158
- package/canonical/hooks/hooks.auto-memory.claude-code.json +0 -16
- package/canonical/openspec/config.base.yaml +0 -89
- package/canonical/rules/safety.md +0 -41
|
@@ -16,7 +16,7 @@ export declare const UPSTREAM_PLUGIN_KEY = "claude-mem@thedotmack";
|
|
|
16
16
|
* canonicalDir: The resolved FreeHarness canonical directory (packages/cli/canonical).
|
|
17
17
|
*
|
|
18
18
|
* Returns:
|
|
19
|
-
* Absolute engine path, or null when the vendored tree is absent (
|
|
19
|
+
* Absolute engine path, or null when the vendored tree is absent (memory is then skipped).
|
|
20
20
|
*/
|
|
21
21
|
export declare function vendoredMemoryEngineDir(canonicalDir: string): string | null;
|
|
22
22
|
/**
|
|
@@ -49,19 +49,44 @@ export interface ClaudePluginPaths {
|
|
|
49
49
|
* The path set mirrored from upstream utils/paths.ts.
|
|
50
50
|
*/
|
|
51
51
|
export declare function claudePluginPaths(version: string): ClaudePluginPaths;
|
|
52
|
+
/** Built bundles a plugin dir needs before it may replace a cached build. */
|
|
53
|
+
export declare const PLUGIN_BUNDLES: readonly ["bun-runner.js", "worker-service.cjs", "mcp-server.cjs"];
|
|
52
54
|
/**
|
|
53
|
-
*
|
|
54
|
-
* path order
|
|
55
|
-
*
|
|
56
|
-
* marker file itself, so re-staging never invalidates its own guard.
|
|
55
|
+
* Digest of a plugin bundle over every entry's relpath, type, mode and content or link target,
|
|
56
|
+
* in path order. Walks with lstat and never follows a symlink. Skips `node_modules` and the
|
|
57
|
+
* unstaged entries, so the vendored plugin and its staged copy digest alike.
|
|
57
58
|
*
|
|
58
59
|
* Args:
|
|
59
60
|
* dir: Directory to digest recursively (absent dir digests as empty).
|
|
60
61
|
*
|
|
61
62
|
* Returns:
|
|
62
|
-
* Lowercase hex SHA-256
|
|
63
|
+
* Lowercase hex SHA-256.
|
|
63
64
|
*/
|
|
64
65
|
export declare function bundleDigest(dir: string): string;
|
|
66
|
+
export interface StageResult {
|
|
67
|
+
staged: boolean;
|
|
68
|
+
digest: string;
|
|
69
|
+
notes: string[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Stage a plugin dir into a cache dir unless the marker and the cache's own digest both equal
|
|
73
|
+
* the plugin digest and the declared deps resolve; `staged` is true for a (would-be) re-stage.
|
|
74
|
+
*/
|
|
75
|
+
export declare function stagePluginCache(pluginDir: string, cacheDir: string, opts: {
|
|
76
|
+
dryRun: boolean;
|
|
77
|
+
}): StageResult;
|
|
78
|
+
/** Identity of the cache marker file (absent: null); every re-stage writes a new one. */
|
|
79
|
+
export declare function stageStamp(cacheDir: string): string | null;
|
|
80
|
+
export declare function missingPluginBundles(pluginDir: string): string[];
|
|
81
|
+
/** Whether a plugin dir's package.json declares runtime deps, which only Bun installs. */
|
|
82
|
+
export declare function declaresPluginDeps(dir: string): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Declared plugin deps that do not resolve from `<dir>/node_modules` (upstream
|
|
85
|
+
* verifyCriticalModules), plus the zod subpaths the worker requires when zod is declared.
|
|
86
|
+
*/
|
|
87
|
+
export declare function missingPluginDeps(dir: string): string[];
|
|
88
|
+
/** Install a staged cache's runtime deps (the hooks run from there); throws when one stays missing. */
|
|
89
|
+
export declare function installPluginDeps(dir: string, bunPath: string): void;
|
|
65
90
|
/** Probe result for one runtime tool. */
|
|
66
91
|
export interface ToolProbe {
|
|
67
92
|
path: string | null;
|
|
@@ -74,31 +99,39 @@ export interface ToolProbe {
|
|
|
74
99
|
* Path + version, or nulls when absent.
|
|
75
100
|
*/
|
|
76
101
|
export declare function probeBun(): ToolProbe;
|
|
77
|
-
/**
|
|
78
|
-
* Probe uv (Chroma vector store runtime; upstream setup-runtime getUvPath).
|
|
79
|
-
*
|
|
80
|
-
* Returns:
|
|
81
|
-
* Path + version, or nulls when absent.
|
|
82
|
-
*/
|
|
83
|
-
export declare function probeUv(): ToolProbe;
|
|
84
102
|
/**
|
|
85
103
|
* Whether an upstream registration (UPSTREAM_PLUGIN_KEY) is still live: present in
|
|
86
|
-
* installed_plugins.json, or enabledPlugins carries it at a value other than `false`.
|
|
87
|
-
*
|
|
88
|
-
* the backup/disable branch (idempotent on re-run).
|
|
104
|
+
* installed_plugins.json, or enabledPlugins carries it at a value other than `false`. Read-only:
|
|
105
|
+
* doctor warns on it and the data copy refuses while it is live.
|
|
89
106
|
*
|
|
90
107
|
* Args:
|
|
91
108
|
* installed: Parsed installed_plugins.json (or `{}` when absent).
|
|
92
109
|
* settings: Parsed settings.json (or `{}` when absent).
|
|
93
110
|
*
|
|
94
111
|
* Returns:
|
|
95
|
-
* True when the upstream
|
|
112
|
+
* True when the upstream plugin may still load beside free-mem.
|
|
96
113
|
*/
|
|
97
114
|
export declare function isUpstreamRegistered(installed: {
|
|
98
115
|
plugins?: Record<string, unknown>;
|
|
99
116
|
}, settings: {
|
|
100
117
|
enabledPlugins?: Record<string, boolean>;
|
|
101
118
|
}): boolean;
|
|
119
|
+
/** Write the `.install-version` marker that tells our build apart; never writes through a symlink. */
|
|
120
|
+
export declare function writeInstallMarker(dir: string, version: string, bun: string): string;
|
|
121
|
+
/**
|
|
122
|
+
* Version from a plugin dir's manifest ("0.0.0" when absent). The version names a cache
|
|
123
|
+
* directory that staging removes, so an empty or path-like value must never reach a path.
|
|
124
|
+
*
|
|
125
|
+
* Args:
|
|
126
|
+
* pluginDir: Plugin directory holding `.claude-plugin/plugin.json`.
|
|
127
|
+
*
|
|
128
|
+
* Returns:
|
|
129
|
+
* The semver version.
|
|
130
|
+
*
|
|
131
|
+
* Raises:
|
|
132
|
+
* Error: When the manifest version is not a semver version.
|
|
133
|
+
*/
|
|
134
|
+
export declare function pluginVersion(pluginDir: string): string;
|
|
102
135
|
/** MemoryPort implementation backed by the vendored claude-mem plugin build. */
|
|
103
136
|
export declare class VendoredMemory implements MemoryPort {
|
|
104
137
|
readonly engineDir: string;
|
|
@@ -115,7 +148,7 @@ export declare class VendoredMemory implements MemoryPort {
|
|
|
115
148
|
version(): string;
|
|
116
149
|
private result;
|
|
117
150
|
/**
|
|
118
|
-
* Probe Bun
|
|
151
|
+
* Probe Bun; instruct when missing. Never installs it (user's machine).
|
|
119
152
|
*
|
|
120
153
|
* Returns:
|
|
121
154
|
* Notes only. A missing Bun is reported as "MISSING" and blocks install().
|
|
@@ -131,20 +164,9 @@ export declare class VendoredMemory implements MemoryPort {
|
|
|
131
164
|
* Register the plugin with Claude Code (upstream registerMarketplace / registerPlugin /
|
|
132
165
|
* enablePluginInClaudeSettings). All three writes are additive merges: other
|
|
133
166
|
* marketplaces, plugins, hooks and settings keys are preserved byte-for-byte in shape.
|
|
134
|
-
*
|
|
135
|
-
* When an upstream registration (UPSTREAM_PLUGIN_KEY) is found (installed_plugins.json
|
|
136
|
-
* or a still-true enabledPlugins entry), this also backs up the three files below with a
|
|
137
|
-
* shared timestamp before the first write and disables (never deletes) the old key; the
|
|
138
|
-
* old cache directory is left in place for `freeharness cleanup` to offer later.
|
|
167
|
+
* An upstream registration (UPSTREAM_PLUGIN_KEY) is never written: doctor warns about it.
|
|
139
168
|
*/
|
|
140
169
|
registerHooks(_scope: Scope, ctx: InstallContext): Promise<AdapterResult>;
|
|
141
|
-
/**
|
|
142
|
-
* Keep an existing ~/.claude-mem data directory from a delegated install. The
|
|
143
|
-
* plugin resolves it by convention (CLAUDE_MEM_DATA_DIR or ~/.claude-mem), so no
|
|
144
|
-
* write is needed and nothing is deleted.
|
|
145
|
-
*/
|
|
146
170
|
migrateExisting(_ctx: InstallContext): Promise<AdapterResult>;
|
|
147
171
|
private run;
|
|
148
|
-
private missingPluginDeps;
|
|
149
|
-
private verifyPluginDeps;
|
|
150
172
|
}
|
|
@@ -1,26 +1,17 @@
|
|
|
1
1
|
// MemoryPort adapter for the vendored claude-mem engine (FreeHarness V3-P0 memory-runtime).
|
|
2
2
|
//
|
|
3
|
-
// Installs free-mem as a real Claude Code plugin from OUR build, reproducing the
|
|
4
|
-
//
|
|
5
|
-
// utils/paths.ts + install/setup-runtime.ts) in TypeScript:
|
|
3
|
+
// Installs free-mem as a real Claude Code plugin from OUR build, reproducing the upstream `npx claude-mem
|
|
4
|
+
// install` mechanics (src/npx-cli/commands/install.ts + utils/paths.ts + install/setup-runtime.ts) in TypeScript:
|
|
6
5
|
//
|
|
7
|
-
// ensurePrerequisites Bun (build + hook runtime)
|
|
8
|
-
// install bun run build:plugin -> copyPluginToCache(version)
|
|
9
|
-
// ->
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// all ADDITIVE merges (other entries survive). When an
|
|
14
|
-
// upstream registration (UPSTREAM_PLUGIN_KEY) is found, it
|
|
15
|
-
// also backs up the three registration files, registers the
|
|
16
|
-
// new key and disables (never deletes) the old one.
|
|
17
|
-
// migrateExisting an existing ~/.claude-mem data dir is kept; the plugin
|
|
18
|
-
// already resolves it by convention (nothing is deleted).
|
|
6
|
+
// ensurePrerequisites Bun probe (build + hook runtime), the only prerequisite.
|
|
7
|
+
// install bun run build:plugin -> copyPluginToCache(version) -> copyPluginToMarketplace
|
|
8
|
+
// -> plugin deps install -> .install-version markers.
|
|
9
|
+
// registerHooks known_marketplaces.json + installed_plugins.json + enabledPlugins
|
|
10
|
+
// ["free-mem@freeharness"], all ADDITIVE merges; the upstream key
|
|
11
|
+
// (UPSTREAM_PLUGIN_KEY) is never written; doctor warns while it is live.
|
|
19
12
|
//
|
|
20
|
-
// The FreeHarness build declares its own identity (marketplace freeharness, plugin
|
|
21
|
-
//
|
|
22
|
-
// pre-rename install; the data directory stays ~/.claude-mem by convention on
|
|
23
|
-
// both sides. Every write honors ctx.dryRun (writes nothing, reports the plan).
|
|
13
|
+
// The FreeHarness build declares its own identity (marketplace freeharness, plugin free-mem) so `doctor` and
|
|
14
|
+
// `cleanup` can tell it apart from an upstream or pre-rename install. Every write honors ctx.dryRun.
|
|
24
15
|
import crypto from "node:crypto";
|
|
25
16
|
import fs from "node:fs";
|
|
26
17
|
import os from "node:os";
|
|
@@ -45,7 +36,7 @@ const CAPABILITY_ID = "memory";
|
|
|
45
36
|
* canonicalDir: The resolved FreeHarness canonical directory (packages/cli/canonical).
|
|
46
37
|
*
|
|
47
38
|
* Returns:
|
|
48
|
-
* Absolute engine path, or null when the vendored tree is absent (
|
|
39
|
+
* Absolute engine path, or null when the vendored tree is absent (memory is then skipped).
|
|
49
40
|
*/
|
|
50
41
|
export function vendoredMemoryEngineDir(canonicalDir) {
|
|
51
42
|
const engineDir = path.join(canonicalDir, "..", "..", "memory", "vendor", UPSTREAM_PLUGIN_NAME, "engine");
|
|
@@ -63,7 +54,7 @@ export function vendoredMemoryEngineDir(canonicalDir) {
|
|
|
63
54
|
export function memoryBuildActions(engineDir) {
|
|
64
55
|
return [
|
|
65
56
|
{
|
|
66
|
-
description: "Build the
|
|
57
|
+
description: "Build the free-mem plugin bundles (worker, mcp, context, watcher) from the vendored engine with Bun",
|
|
67
58
|
command: "bun install && bun run build:plugin",
|
|
68
59
|
cwd: engineDir,
|
|
69
60
|
optional: true,
|
|
@@ -93,42 +84,127 @@ export function claudePluginPaths(version) {
|
|
|
93
84
|
};
|
|
94
85
|
}
|
|
95
86
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
|
|
87
|
+
* Top-level entries the cache gets after the copy (our markers, the deps lockfile, Claude
|
|
88
|
+
* Code's in-use and orphan markers): never copied and never digested, so staging cannot write
|
|
89
|
+
* through a plugin symlink and an install never reads its own bookkeeping as tampering.
|
|
90
|
+
*/
|
|
91
|
+
const UNSTAGED_ENTRIES = new Set([BUNDLE_MARKER_NAME, ".install-version", "bun.lock", "bun.lockb", ".in_use", ".orphaned_at"]);
|
|
92
|
+
const S_IFMT = 0o170000;
|
|
93
|
+
/** Built bundles a plugin dir needs before it may replace a cached build. */
|
|
94
|
+
export const PLUGIN_BUNDLES = ["bun-runner.js", "worker-service.cjs", "mcp-server.cjs"];
|
|
95
|
+
/**
|
|
96
|
+
* Digest of a plugin bundle over every entry's relpath, type, mode and content or link target,
|
|
97
|
+
* in path order. Walks with lstat and never follows a symlink. Skips `node_modules` and the
|
|
98
|
+
* unstaged entries, so the vendored plugin and its staged copy digest alike.
|
|
100
99
|
*
|
|
101
100
|
* Args:
|
|
102
101
|
* dir: Directory to digest recursively (absent dir digests as empty).
|
|
103
102
|
*
|
|
104
103
|
* Returns:
|
|
105
|
-
* Lowercase hex SHA-256
|
|
104
|
+
* Lowercase hex SHA-256.
|
|
106
105
|
*/
|
|
107
106
|
export function bundleDigest(dir) {
|
|
108
|
-
const
|
|
107
|
+
const entries = [];
|
|
109
108
|
const walk = (rel) => {
|
|
110
|
-
for (const
|
|
111
|
-
if (
|
|
109
|
+
for (const name of fs.readdirSync(path.join(dir, rel))) {
|
|
110
|
+
if (name === "node_modules" || (!rel && UNSTAGED_ENTRIES.has(name)))
|
|
112
111
|
continue;
|
|
113
|
-
const childRel = rel ? `${rel}/${
|
|
114
|
-
|
|
112
|
+
const childRel = rel ? `${rel}/${name}` : name;
|
|
113
|
+
const abs = path.join(dir, childRel);
|
|
114
|
+
const stat = fs.lstatSync(abs);
|
|
115
|
+
if (!stat.isFile() && !stat.isDirectory() && !stat.isSymbolicLink())
|
|
116
|
+
continue;
|
|
117
|
+
entries.push({ rel: childRel, abs, stat });
|
|
118
|
+
if (stat.isDirectory())
|
|
115
119
|
walk(childRel);
|
|
116
|
-
else if (entry.isFile())
|
|
117
|
-
files.push(childRel);
|
|
118
120
|
}
|
|
119
121
|
};
|
|
120
122
|
if (fs.existsSync(dir))
|
|
121
123
|
walk("");
|
|
122
|
-
|
|
124
|
+
entries.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
|
|
123
125
|
const hash = crypto.createHash("sha256");
|
|
124
|
-
for (const rel of
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
for (const { rel, abs, stat } of entries) {
|
|
127
|
+
const type = stat.isSymbolicLink() ? "l" : stat.isDirectory() ? "d" : "f";
|
|
128
|
+
// Windows has no exec bits and symlink permissions carry no meaning.
|
|
129
|
+
const mode = IS_WINDOWS || type === "l" ? stat.mode & S_IFMT : stat.mode;
|
|
130
|
+
for (const part of [rel, type, mode.toString(8)]) {
|
|
131
|
+
hash.update(part, "utf-8");
|
|
132
|
+
hash.update("\0");
|
|
133
|
+
}
|
|
134
|
+
if (type === "f")
|
|
135
|
+
hash.update(fs.readFileSync(abs));
|
|
136
|
+
else if (type === "l")
|
|
137
|
+
hash.update(fs.readlinkSync(abs), "utf-8");
|
|
128
138
|
hash.update("\0");
|
|
129
139
|
}
|
|
130
140
|
return hash.digest("hex");
|
|
131
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Stage a plugin dir into a cache dir unless the marker and the cache's own digest both equal
|
|
144
|
+
* the plugin digest and the declared deps resolve; `staged` is true for a (would-be) re-stage.
|
|
145
|
+
*/
|
|
146
|
+
export function stagePluginCache(pluginDir, cacheDir, opts) {
|
|
147
|
+
const digest = bundleDigest(pluginDir);
|
|
148
|
+
const short = digest.slice(0, 8);
|
|
149
|
+
const markerFile = path.join(cacheDir, BUNDLE_MARKER_NAME);
|
|
150
|
+
const marker = fs.existsSync(markerFile) ? fs.readFileSync(markerFile, "utf-8").trim() : null;
|
|
151
|
+
if (marker === digest && bundleDigest(cacheDir) === marker && missingPluginDeps(cacheDir).length === 0) {
|
|
152
|
+
return { staged: false, digest, notes: [`plugin cache up to date (bundle ${short})`] };
|
|
153
|
+
}
|
|
154
|
+
if (opts.dryRun)
|
|
155
|
+
return { staged: true, digest, notes: [`Would re-stage the plugin cache at ${cacheDir} (bundle ${short}).`] };
|
|
156
|
+
fs.rmSync(cacheDir, { recursive: true, force: true });
|
|
157
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
158
|
+
const root = path.resolve(pluginDir);
|
|
159
|
+
const unstaged = (src) => path.basename(src) === "node_modules" || (path.resolve(path.dirname(src)) === root && UNSTAGED_ENTRIES.has(path.basename(src)));
|
|
160
|
+
fs.cpSync(pluginDir, cacheDir, { recursive: true, force: true, verbatimSymlinks: true, filter: (src) => !unstaged(src) });
|
|
161
|
+
fs.writeFileSync(markerFile, `${digest}\n`, { encoding: "utf-8", flag: "wx" });
|
|
162
|
+
return { staged: true, digest, notes: [`Re-staged the plugin cache at ${cacheDir} (bundle ${short}).`] };
|
|
163
|
+
}
|
|
164
|
+
/** Identity of the cache marker file (absent: null); every re-stage writes a new one. */
|
|
165
|
+
export function stageStamp(cacheDir) {
|
|
166
|
+
const markerFile = path.join(cacheDir, BUNDLE_MARKER_NAME);
|
|
167
|
+
if (!fs.existsSync(markerFile))
|
|
168
|
+
return null;
|
|
169
|
+
const st = fs.lstatSync(markerFile);
|
|
170
|
+
return `${st.ino}:${st.mtimeMs}:${fs.readFileSync(markerFile, "utf-8").trim()}`;
|
|
171
|
+
}
|
|
172
|
+
export function missingPluginBundles(pluginDir) {
|
|
173
|
+
return PLUGIN_BUNDLES.filter((f) => !fs.existsSync(path.join(pluginDir, "scripts", f)));
|
|
174
|
+
}
|
|
175
|
+
/** Whether a plugin dir's package.json declares runtime deps, which only Bun installs. */
|
|
176
|
+
export function declaresPluginDeps(dir) {
|
|
177
|
+
const pkg = readJsonSafe(path.join(dir, "package.json"), {});
|
|
178
|
+
return Object.keys(pkg.dependencies ?? {}).length > 0;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Declared plugin deps that do not resolve from `<dir>/node_modules` (upstream
|
|
182
|
+
* verifyCriticalModules), plus the zod subpaths the worker requires when zod is declared.
|
|
183
|
+
*/
|
|
184
|
+
export function missingPluginDeps(dir) {
|
|
185
|
+
const pkg = readJsonSafe(path.join(dir, "package.json"), {});
|
|
186
|
+
const deps = Object.keys(pkg.dependencies ?? {});
|
|
187
|
+
const nm = path.join(dir, "node_modules");
|
|
188
|
+
const missing = deps.filter((dep) => !fs.existsSync(path.join(nm, dep, "package.json")));
|
|
189
|
+
if (deps.includes("zod")) {
|
|
190
|
+
for (const sub of ["v3", "v4", "v4-mini"]) {
|
|
191
|
+
if (!fs.existsSync(path.join(nm, "zod", sub)))
|
|
192
|
+
missing.push(`zod/${sub}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return missing;
|
|
196
|
+
}
|
|
197
|
+
/** Install a staged cache's runtime deps (the hooks run from there); throws when one stays missing. */
|
|
198
|
+
export function installPluginDeps(dir, bunPath) {
|
|
199
|
+
const r = spawnSync(bunPath, ["install", "--ignore-scripts"], { cwd: dir, stdio: "inherit", windowsHide: true, env: process.env });
|
|
200
|
+
if (r.error)
|
|
201
|
+
throw new Error(`bun install (plugin deps) failed: ${r.error.message}`);
|
|
202
|
+
if (r.status !== 0)
|
|
203
|
+
throw new Error(`bun install (plugin deps) failed (exit ${r.status})`);
|
|
204
|
+
const missing = missingPluginDeps(dir);
|
|
205
|
+
if (missing.length > 0)
|
|
206
|
+
throw new Error(`plugin deps missing after install in ${dir}: ${missing.join(", ")}`);
|
|
207
|
+
}
|
|
132
208
|
function probeTool(command, commonPaths) {
|
|
133
209
|
const candidates = [command, ...commonPaths];
|
|
134
210
|
for (const candidate of candidates) {
|
|
@@ -137,7 +213,7 @@ function probeTool(command, commonPaths) {
|
|
|
137
213
|
try {
|
|
138
214
|
const r = spawnSync(candidate, ["--version"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
|
|
139
215
|
if (r.status === 0) {
|
|
140
|
-
// "1.3.14"
|
|
216
|
+
// "1.3.14" or "tool 0.9.2 (hash date)" -> first semver-looking token.
|
|
141
217
|
const semver = r.stdout.match(/\d+\.\d+(?:\.\d+)?/);
|
|
142
218
|
return { path: candidate, version: semver ? semver[0] : r.stdout.trim() };
|
|
143
219
|
}
|
|
@@ -161,31 +237,17 @@ export function probeBun() {
|
|
|
161
237
|
: [path.join(home, ".bun", "bin", "bun"), "/usr/local/bin/bun", "/opt/homebrew/bin/bun", "/home/linuxbrew/.linuxbrew/bin/bun"];
|
|
162
238
|
return probeTool("bun", common);
|
|
163
239
|
}
|
|
164
|
-
/**
|
|
165
|
-
* Probe uv (Chroma vector store runtime; upstream setup-runtime getUvPath).
|
|
166
|
-
*
|
|
167
|
-
* Returns:
|
|
168
|
-
* Path + version, or nulls when absent.
|
|
169
|
-
*/
|
|
170
|
-
export function probeUv() {
|
|
171
|
-
const home = os.homedir();
|
|
172
|
-
const common = IS_WINDOWS
|
|
173
|
-
? [path.join(home, ".local", "bin", "uv.exe"), path.join(home, ".cargo", "bin", "uv.exe")]
|
|
174
|
-
: [path.join(home, ".local", "bin", "uv"), path.join(home, ".cargo", "bin", "uv"), "/usr/local/bin/uv", "/opt/homebrew/bin/uv"];
|
|
175
|
-
return probeTool("uv", common);
|
|
176
|
-
}
|
|
177
240
|
/**
|
|
178
241
|
* Whether an upstream registration (UPSTREAM_PLUGIN_KEY) is still live: present in
|
|
179
|
-
* installed_plugins.json, or enabledPlugins carries it at a value other than `false`.
|
|
180
|
-
*
|
|
181
|
-
* the backup/disable branch (idempotent on re-run).
|
|
242
|
+
* installed_plugins.json, or enabledPlugins carries it at a value other than `false`. Read-only:
|
|
243
|
+
* doctor warns on it and the data copy refuses while it is live.
|
|
182
244
|
*
|
|
183
245
|
* Args:
|
|
184
246
|
* installed: Parsed installed_plugins.json (or `{}` when absent).
|
|
185
247
|
* settings: Parsed settings.json (or `{}` when absent).
|
|
186
248
|
*
|
|
187
249
|
* Returns:
|
|
188
|
-
* True when the upstream
|
|
250
|
+
* True when the upstream plugin may still load beside free-mem.
|
|
189
251
|
*/
|
|
190
252
|
export function isUpstreamRegistered(installed, settings) {
|
|
191
253
|
if (settings.enabledPlugins?.[UPSTREAM_PLUGIN_KEY] === false)
|
|
@@ -212,12 +274,36 @@ function writeJsonAtomic(file, value) {
|
|
|
212
274
|
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", "utf-8");
|
|
213
275
|
fs.renameSync(tmp, file);
|
|
214
276
|
}
|
|
215
|
-
|
|
277
|
+
/** Write the `.install-version` marker that tells our build apart; never writes through a symlink. */
|
|
278
|
+
export function writeInstallMarker(dir, version, bun) {
|
|
216
279
|
const marker = path.join(dir, ".install-version");
|
|
217
280
|
fs.mkdirSync(dir, { recursive: true });
|
|
218
|
-
fs.
|
|
281
|
+
fs.rmSync(marker, { force: true });
|
|
282
|
+
fs.writeFileSync(marker, JSON.stringify({ version, bun, uv: "unused", installedAt: new Date().toISOString() }), { flag: "wx" });
|
|
219
283
|
return marker;
|
|
220
284
|
}
|
|
285
|
+
const PLUGIN_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
286
|
+
/**
|
|
287
|
+
* Version from a plugin dir's manifest ("0.0.0" when absent). The version names a cache
|
|
288
|
+
* directory that staging removes, so an empty or path-like value must never reach a path.
|
|
289
|
+
*
|
|
290
|
+
* Args:
|
|
291
|
+
* pluginDir: Plugin directory holding `.claude-plugin/plugin.json`.
|
|
292
|
+
*
|
|
293
|
+
* Returns:
|
|
294
|
+
* The semver version.
|
|
295
|
+
*
|
|
296
|
+
* Raises:
|
|
297
|
+
* Error: When the manifest version is not a semver version.
|
|
298
|
+
*/
|
|
299
|
+
export function pluginVersion(pluginDir) {
|
|
300
|
+
const manifest = path.join(pluginDir, ".claude-plugin", "plugin.json");
|
|
301
|
+
const version = readJsonSafe(manifest, {}).version ?? "0.0.0";
|
|
302
|
+
if (typeof version !== "string" || !PLUGIN_SEMVER.test(version)) {
|
|
303
|
+
throw new Error(`plugin version ${JSON.stringify(version)} in ${manifest} is not a semver version; refusing to stage the plugin cache`);
|
|
304
|
+
}
|
|
305
|
+
return version;
|
|
306
|
+
}
|
|
221
307
|
/** MemoryPort implementation backed by the vendored claude-mem plugin build. */
|
|
222
308
|
export class VendoredMemory {
|
|
223
309
|
engineDir;
|
|
@@ -237,15 +323,13 @@ export class VendoredMemory {
|
|
|
237
323
|
}
|
|
238
324
|
/** Plugin version from the vendored manifest (upstream readPluginVersion). */
|
|
239
325
|
version() {
|
|
240
|
-
|
|
241
|
-
const parsed = readJsonSafe(manifest, {});
|
|
242
|
-
return parsed.version ?? "0.0.0";
|
|
326
|
+
return pluginVersion(this.pluginDir);
|
|
243
327
|
}
|
|
244
328
|
result() {
|
|
245
329
|
return { capabilityId: CAPABILITY_ID, target: this.target, written: [], actions: [], notes: [] };
|
|
246
330
|
}
|
|
247
331
|
/**
|
|
248
|
-
* Probe Bun
|
|
332
|
+
* Probe Bun; instruct when missing. Never installs it (user's machine).
|
|
249
333
|
*
|
|
250
334
|
* Returns:
|
|
251
335
|
* Notes only. A missing Bun is reported as "MISSING" and blocks install().
|
|
@@ -253,11 +337,7 @@ export class VendoredMemory {
|
|
|
253
337
|
async ensurePrerequisites(_ctx) {
|
|
254
338
|
const res = this.result();
|
|
255
339
|
const bun = probeBun();
|
|
256
|
-
const uv = probeUv();
|
|
257
340
|
res.notes.push(bun.path ? `Bun ${bun.version} found (${bun.path}).` : "MISSING: Bun — install from https://bun.sh (curl -fsSL https://bun.sh/install | bash), then re-run.");
|
|
258
|
-
res.notes.push(uv.path
|
|
259
|
-
? `uv ${uv.version} found (${uv.path}).`
|
|
260
|
-
: "MISSING: uv — Chroma vector search needs it at runtime (https://docs.astral.sh/uv). Install is not blocked; recall degrades to SQLite-only until uv is present.");
|
|
261
341
|
return res;
|
|
262
342
|
}
|
|
263
343
|
/**
|
|
@@ -274,7 +354,6 @@ export class VendoredMemory {
|
|
|
274
354
|
res.notes.push("Install skipped: Bun is required to build the plugin and to run its hooks.");
|
|
275
355
|
return res;
|
|
276
356
|
}
|
|
277
|
-
const uv = probeUv();
|
|
278
357
|
const scriptsDir = path.join(this.pluginDir, "scripts");
|
|
279
358
|
const marketplacePlugin = path.join(P.marketplaceDir, "plugin");
|
|
280
359
|
// 1. Build (bun install once, then build:plugin).
|
|
@@ -282,8 +361,7 @@ export class VendoredMemory {
|
|
|
282
361
|
res.notes.push(`Would build the plugin bundles: bun run build:plugin (in ${this.engineDir}).`);
|
|
283
362
|
}
|
|
284
363
|
else {
|
|
285
|
-
const
|
|
286
|
-
const reuse = process.env.FREEHARNESS_MEMORY_SKIP_BUILD === "1" && bundles.every((f) => fs.existsSync(path.join(scriptsDir, f)));
|
|
364
|
+
const reuse = process.env.FREEHARNESS_MEMORY_SKIP_BUILD === "1" && missingPluginBundles(this.pluginDir).length === 0;
|
|
287
365
|
if (reuse) {
|
|
288
366
|
res.notes.push(`Reused the built plugin bundles in ${scriptsDir} (FREEHARNESS_MEMORY_SKIP_BUILD=1).`);
|
|
289
367
|
}
|
|
@@ -291,34 +369,17 @@ export class VendoredMemory {
|
|
|
291
369
|
if (!fs.existsSync(path.join(this.engineDir, "node_modules")))
|
|
292
370
|
this.run(bun.path, ["install"], this.engineDir, "bun install (engine)");
|
|
293
371
|
this.run(bun.path, ["run", "build:plugin"], this.engineDir, "bun run build:plugin");
|
|
294
|
-
for (const f of
|
|
295
|
-
|
|
296
|
-
throw new Error(`build:plugin did not produce ${f}`);
|
|
297
|
-
}
|
|
372
|
+
for (const f of missingPluginBundles(this.pluginDir))
|
|
373
|
+
throw new Error(`build:plugin did not produce ${f}`);
|
|
298
374
|
res.notes.push(`Built plugin bundles v${version} into ${scriptsDir}.`);
|
|
299
375
|
}
|
|
300
376
|
}
|
|
301
|
-
// 2. copyPluginToCache: rm + cp plugin/ -> cache/<owner>/<name>/<version>/, skipped
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const digest = bundleDigest(this.pluginDir);
|
|
306
|
-
const markerFile = path.join(P.cacheDir, BUNDLE_MARKER_NAME);
|
|
307
|
-
const markerMatches = fs.existsSync(P.cacheDir) && fs.existsSync(markerFile) && fs.readFileSync(markerFile, "utf-8").trim() === digest;
|
|
308
|
-
const cacheUpToDate = markerMatches && fs.existsSync(path.join(P.cacheDir, "node_modules")) && this.missingPluginDeps(P.cacheDir).length === 0;
|
|
377
|
+
// 2. copyPluginToCache: rm + cp plugin/ -> cache/<owner>/<name>/<version>/, skipped when
|
|
378
|
+
// the cache already holds this exact bundle; a broken node_modules takes the re-stage path.
|
|
379
|
+
const stage = stagePluginCache(this.pluginDir, P.cacheDir, { dryRun: ctx.dryRun });
|
|
380
|
+
const cacheUpToDate = !stage.staged;
|
|
309
381
|
res.written.push(P.cacheDir);
|
|
310
|
-
|
|
311
|
-
res.notes.push(`plugin cache up to date (bundle ${digest.slice(0, 8)})`);
|
|
312
|
-
}
|
|
313
|
-
else if (ctx.dryRun) {
|
|
314
|
-
res.notes.push(`Would re-stage the plugin cache at ${P.cacheDir} (bundle ${digest.slice(0, 8)}).`);
|
|
315
|
-
}
|
|
316
|
-
else {
|
|
317
|
-
fs.rmSync(P.cacheDir, { recursive: true, force: true });
|
|
318
|
-
fs.mkdirSync(P.cacheDir, { recursive: true });
|
|
319
|
-
fs.cpSync(this.pluginDir, P.cacheDir, { recursive: true, force: true, filter: (src) => path.basename(src) !== "node_modules" });
|
|
320
|
-
fs.writeFileSync(markerFile, `${digest}\n`, "utf-8");
|
|
321
|
-
}
|
|
382
|
+
res.notes.push(...stage.notes);
|
|
322
383
|
// 3. copyPluginToMarketplace: plugin/ + marketplace manifest + package bits.
|
|
323
384
|
res.written.push(marketplacePlugin);
|
|
324
385
|
if (!ctx.dryRun) {
|
|
@@ -343,8 +404,7 @@ export class VendoredMemory {
|
|
|
343
404
|
res.notes.push(`Would install plugin runtime deps in ${P.cacheDir} (bun install --ignore-scripts).`);
|
|
344
405
|
}
|
|
345
406
|
else {
|
|
346
|
-
|
|
347
|
-
this.verifyPluginDeps(P.cacheDir);
|
|
407
|
+
installPluginDeps(P.cacheDir, bun.path);
|
|
348
408
|
res.notes.push("Plugin runtime deps installed (zod resolvable from plugin/node_modules).");
|
|
349
409
|
}
|
|
350
410
|
// 5. Install markers (cache + marketplace + marketplace/plugin, as upstream); the
|
|
@@ -353,11 +413,11 @@ export class VendoredMemory {
|
|
|
353
413
|
for (const dir of [P.marketplaceDir, marketplacePlugin]) {
|
|
354
414
|
res.written.push(path.join(dir, ".install-version"));
|
|
355
415
|
if (!ctx.dryRun)
|
|
356
|
-
writeInstallMarker(dir, version, bun.version ?? "unknown"
|
|
416
|
+
writeInstallMarker(dir, version, bun.version ?? "unknown");
|
|
357
417
|
}
|
|
358
418
|
res.written.push(path.join(P.cacheDir, ".install-version"));
|
|
359
419
|
if (!cacheUpToDate && !ctx.dryRun)
|
|
360
|
-
writeInstallMarker(P.cacheDir, version, bun.version ?? "unknown"
|
|
420
|
+
writeInstallMarker(P.cacheDir, version, bun.version ?? "unknown");
|
|
361
421
|
res.notes.push(`Plugin v${version} staged at ${P.cacheDir} (marketplace: ${P.marketplaceDir}).`);
|
|
362
422
|
return res;
|
|
363
423
|
}
|
|
@@ -365,30 +425,13 @@ export class VendoredMemory {
|
|
|
365
425
|
* Register the plugin with Claude Code (upstream registerMarketplace / registerPlugin /
|
|
366
426
|
* enablePluginInClaudeSettings). All three writes are additive merges: other
|
|
367
427
|
* marketplaces, plugins, hooks and settings keys are preserved byte-for-byte in shape.
|
|
368
|
-
*
|
|
369
|
-
* When an upstream registration (UPSTREAM_PLUGIN_KEY) is found (installed_plugins.json
|
|
370
|
-
* or a still-true enabledPlugins entry), this also backs up the three files below with a
|
|
371
|
-
* shared timestamp before the first write and disables (never deletes) the old key; the
|
|
372
|
-
* old cache directory is left in place for `freeharness cleanup` to offer later.
|
|
428
|
+
* An upstream registration (UPSTREAM_PLUGIN_KEY) is never written: doctor warns about it.
|
|
373
429
|
*/
|
|
374
430
|
async registerHooks(_scope, ctx) {
|
|
375
431
|
const res = this.result();
|
|
376
432
|
const version = this.version();
|
|
377
433
|
const P = claudePluginPaths(version);
|
|
378
434
|
const now = new Date().toISOString();
|
|
379
|
-
const installedBefore = readJsonSafe(P.installedPlugins, {});
|
|
380
|
-
const settingsBefore = readJsonSafe(P.settings, {});
|
|
381
|
-
const migrating = isUpstreamRegistered(installedBefore, settingsBefore);
|
|
382
|
-
if (migrating && !ctx.dryRun) {
|
|
383
|
-
const stamp = now.replace(/[:.]/g, "-");
|
|
384
|
-
for (const f of [P.installedPlugins, P.knownMarketplaces, P.settings]) {
|
|
385
|
-
if (!fs.existsSync(f))
|
|
386
|
-
continue;
|
|
387
|
-
const backup = `${f}.${stamp}.bak`;
|
|
388
|
-
fs.copyFileSync(f, backup);
|
|
389
|
-
res.written.push(backup);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
435
|
// known_marketplaces.json: { "<owner>": { source, installLocation, lastUpdated, autoUpdate } }
|
|
393
436
|
const known = readJsonSafe(P.knownMarketplaces, {});
|
|
394
437
|
known[MARKETPLACE_OWNER] = {
|
|
@@ -401,8 +444,6 @@ export class VendoredMemory {
|
|
|
401
444
|
if (!ctx.dryRun)
|
|
402
445
|
writeJsonAtomic(P.knownMarketplaces, known);
|
|
403
446
|
// installed_plugins.json: { version: 2, plugins: { "<name>@<owner>": [ {scope, installPath, version, installedAt, lastUpdated} ] } }
|
|
404
|
-
// The upstream entry (if any) is never removed here: invariant 3 forbids deleting a
|
|
405
|
-
// foreign key, and the cache directory it points at stays reachable for a rollback.
|
|
406
447
|
const installed = readJsonSafe(P.installedPlugins, {});
|
|
407
448
|
if (!installed.version)
|
|
408
449
|
installed.version = 2;
|
|
@@ -412,49 +453,21 @@ export class VendoredMemory {
|
|
|
412
453
|
res.written.push(P.installedPlugins);
|
|
413
454
|
if (!ctx.dryRun)
|
|
414
455
|
writeJsonAtomic(P.installedPlugins, installed);
|
|
415
|
-
// settings.json: enabledPlugins[PLUGIN_KEY] = true (everything else kept)
|
|
416
|
-
// also sets enabledPlugins[UPSTREAM_PLUGIN_KEY] = false so Claude Code stops loading it.
|
|
456
|
+
// settings.json: enabledPlugins[PLUGIN_KEY] = true (everything else kept).
|
|
417
457
|
const settings = readJsonSafe(P.settings, {});
|
|
418
458
|
if (!settings.enabledPlugins)
|
|
419
459
|
settings.enabledPlugins = {};
|
|
420
460
|
const already = settings.enabledPlugins[PLUGIN_KEY] === true;
|
|
421
461
|
settings.enabledPlugins[PLUGIN_KEY] = true;
|
|
422
|
-
if (migrating)
|
|
423
|
-
settings.enabledPlugins[UPSTREAM_PLUGIN_KEY] = false;
|
|
424
462
|
res.written.push(P.settings);
|
|
425
463
|
if (!ctx.dryRun)
|
|
426
464
|
writeJsonAtomic(P.settings, settings);
|
|
427
|
-
|
|
428
|
-
res.notes.push(ctx.dryRun
|
|
429
|
-
? `Would migrate from ${UPSTREAM_PLUGIN_KEY}: back up installed_plugins.json, known_marketplaces.json and settings.json before writing, register ${PLUGIN_KEY} v${version}, and set enabledPlugins["${UPSTREAM_PLUGIN_KEY}"] to false. The old cache directory is left in place.`
|
|
430
|
-
: `Migrated from ${UPSTREAM_PLUGIN_KEY}: backed up installed_plugins.json, known_marketplaces.json and settings.json before the first write, registered ${PLUGIN_KEY} v${version}, and set enabledPlugins["${UPSTREAM_PLUGIN_KEY}"] to false. The old cache directory is left in place.`);
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
res.notes.push(`${ctx.dryRun ? "Would register" : "Registered"} plugin ${PLUGIN_KEY} v${version}: marketplace + installed_plugins + enabledPlugins (additive)${already ? "; was already enabled" : ""}.`);
|
|
434
|
-
}
|
|
465
|
+
res.notes.push(`${ctx.dryRun ? "Would register" : "Registered"} plugin ${PLUGIN_KEY} v${version}: marketplace + installed_plugins + enabledPlugins (additive)${already ? "; was already enabled" : ""}.`);
|
|
435
466
|
res.notes.push("Lifecycle hooks and the mem-search MCP come from the plugin's own hooks.json / .mcp.json (no settings.json hook entries are written).");
|
|
436
467
|
return res;
|
|
437
468
|
}
|
|
438
|
-
/**
|
|
439
|
-
* Keep an existing ~/.claude-mem data directory from a delegated install. The
|
|
440
|
-
* plugin resolves it by convention (CLAUDE_MEM_DATA_DIR or ~/.claude-mem), so no
|
|
441
|
-
* write is needed and nothing is deleted.
|
|
442
|
-
*/
|
|
443
469
|
async migrateExisting(_ctx) {
|
|
444
|
-
|
|
445
|
-
const dataDir = process.env.CLAUDE_MEM_DATA_DIR || path.join(os.homedir(), ".claude-mem");
|
|
446
|
-
if (!fs.existsSync(dataDir)) {
|
|
447
|
-
res.notes.push(`No existing claude-mem data dir at ${dataDir}; the worker creates it on first start.`);
|
|
448
|
-
return res;
|
|
449
|
-
}
|
|
450
|
-
const db = path.join(dataDir, "claude-mem.db");
|
|
451
|
-
const settings = readJsonSafe(path.join(dataDir, "settings.json"), {});
|
|
452
|
-
const redirected = typeof settings.CLAUDE_MEM_DATA_DIR === "string" ? settings.CLAUDE_MEM_DATA_DIR : null;
|
|
453
|
-
res.notes.push(`Existing claude-mem data kept at ${dataDir}${fs.existsSync(db) ? " (claude-mem.db present)" : ""}; the vendored plugin reads it by convention${redirected ? ` (settings redirect data to ${redirected})` : ""}. Nothing deleted.`);
|
|
454
|
-
const legacyMarker = path.join(dataDir, ".install-version");
|
|
455
|
-
if (fs.existsSync(legacyMarker))
|
|
456
|
-
res.notes.push("A legacy install marker exists in the data dir; the plugin markers now live in the plugin cache.");
|
|
457
|
-
return res;
|
|
470
|
+
return this.result();
|
|
458
471
|
}
|
|
459
472
|
run(cmd, args, cwd, label) {
|
|
460
473
|
const r = spawnSync(cmd, args, { cwd, stdio: "inherit", windowsHide: true, env: process.env });
|
|
@@ -463,25 +476,4 @@ export class VendoredMemory {
|
|
|
463
476
|
if (r.status !== 0)
|
|
464
477
|
throw new Error(`${label} failed (exit ${r.status})`);
|
|
465
478
|
}
|
|
466
|
-
// upstream verifyCriticalModules: every declared plugin dep must resolve from
|
|
467
|
-
// plugin/node_modules, plus the zod subpaths the worker bundle requires.
|
|
468
|
-
missingPluginDeps(dir) {
|
|
469
|
-
const pkg = readJsonSafe(path.join(dir, "package.json"), {});
|
|
470
|
-
const nm = path.join(dir, "node_modules");
|
|
471
|
-
const missing = [];
|
|
472
|
-
for (const dep of Object.keys(pkg.dependencies ?? {})) {
|
|
473
|
-
if (!fs.existsSync(path.join(nm, dep, "package.json")))
|
|
474
|
-
missing.push(dep);
|
|
475
|
-
}
|
|
476
|
-
for (const sub of ["v3", "v4", "v4-mini"]) {
|
|
477
|
-
if (!fs.existsSync(path.join(nm, "zod", sub)))
|
|
478
|
-
missing.push(`zod/${sub}`);
|
|
479
|
-
}
|
|
480
|
-
return missing;
|
|
481
|
-
}
|
|
482
|
-
verifyPluginDeps(dir) {
|
|
483
|
-
const missing = this.missingPluginDeps(dir);
|
|
484
|
-
if (missing.length > 0)
|
|
485
|
-
throw new Error(`plugin deps missing after install in ${dir}: ${missing.join(", ")}`);
|
|
486
|
-
}
|
|
487
479
|
}
|