freeharness 0.4.1 → 0.4.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/README.md +8 -7
- package/canonical/HARVEST.yaml +25 -14
- package/canonical/PROVENANCE.yaml +1 -1
- 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 +7 -8
- package/canonical/agents/test-writer.md +5 -2
- package/canonical/hooks/cost-prices.json +11 -9
- package/canonical/hooks/freeharness-config-guard.mjs +8072 -87
- package/canonical/hooks/freeharness-context-monitor.mjs +88 -54
- package/canonical/hooks/freeharness-converge.mjs +78 -23
- package/canonical/hooks/freeharness-cost-tracker.mjs +1129 -218
- package/canonical/hooks/freeharness-memory-guard.mjs +20 -23
- package/canonical/hooks/freeharness-precompact.mjs +217 -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 +970 -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 +3 -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 +3 -2
- 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 +9 -2
- 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 +3496 -19
- package/canonical/skills/subagent-driven-development/reference/brief.md +185 -35
- package/canonical/skills/subagent-driven-development/reference/handoff.md +82 -2
- package/canonical/skills/subagent-driven-development/reference/loop.md +57 -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 +4771 -326
- package/dist/adapters/harness/claude-code.d.ts +19 -3
- package/dist/adapters/harness/claude-code.js +204 -39
- package/dist/adapters/harness/opencode.js +16 -2
- package/dist/adapters/harness/plugin.js +28 -17
- package/dist/adapters/memory/doctor.d.ts +174 -29
- package/dist/adapters/memory/doctor.js +758 -134
- package/dist/adapters/memory/migrate.d.ts +95 -0
- package/dist/adapters/memory/migrate.js +497 -0
- package/dist/adapters/memory/paths.d.ts +22 -0
- package/dist/adapters/memory/paths.js +85 -0
- package/dist/adapters/memory/vendored.d.ts +56 -29
- package/dist/adapters/memory/vendored.js +191 -154
- 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 +182 -68
- 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 +193 -44
- package/dist/cli/hard-cleanup.d.ts +93 -0
- package/dist/cli/hard-cleanup.js +552 -0
- package/dist/cli/install-facts.d.ts +28 -0
- package/dist/cli/install-facts.js +66 -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/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 +247 -70
- package/dist/infra/fsutil.d.ts +31 -3
- package/dist/infra/fsutil.js +69 -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/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 +1 -1
- package/provenance/memory.PROVENANCE.yaml +458 -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
|
@@ -4,28 +4,29 @@
|
|
|
4
4
|
// upstream `npx claude-mem install` mechanics (src/npx-cli/commands/install.ts +
|
|
5
5
|
// utils/paths.ts + install/setup-runtime.ts) in TypeScript:
|
|
6
6
|
//
|
|
7
|
-
// ensurePrerequisites Bun (build + hook runtime)
|
|
7
|
+
// ensurePrerequisites Bun probe (build + hook runtime), the only prerequisite.
|
|
8
8
|
// install bun run build:plugin -> copyPluginToCache(version)
|
|
9
9
|
// -> copyPluginToMarketplace -> plugin deps install
|
|
10
10
|
// -> .install-version markers.
|
|
11
11
|
// registerHooks known_marketplaces.json + installed_plugins.json +
|
|
12
12
|
// settings.enabledPlugins["free-mem@freeharness"]=true,
|
|
13
|
-
// all ADDITIVE merges (other entries survive).
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// already resolves it by convention (nothing is deleted).
|
|
13
|
+
// all ADDITIVE merges (other entries survive). The upstream
|
|
14
|
+
// key (UPSTREAM_PLUGIN_KEY) is never written; doctor warns
|
|
15
|
+
// while it is live.
|
|
16
|
+
// migrateExisting copies a legacy ~/.claude-mem forward into ~/.free-mem
|
|
17
|
+
// (migrate.ts); the legacy directory is only read.
|
|
19
18
|
//
|
|
20
19
|
// The FreeHarness build declares its own identity (marketplace freeharness, plugin
|
|
21
20
|
// free-mem) so `doctor` and `cleanup` can tell it apart from an upstream or
|
|
22
|
-
// pre-rename install
|
|
23
|
-
// both sides. Every write honors ctx.dryRun (writes nothing, reports the plan).
|
|
21
|
+
// pre-rename install. Every write honors ctx.dryRun (writes nothing, reports the plan).
|
|
24
22
|
import crypto from "node:crypto";
|
|
25
23
|
import fs from "node:fs";
|
|
26
24
|
import os from "node:os";
|
|
27
25
|
import path from "node:path";
|
|
28
26
|
import { spawnSync } from "node:child_process";
|
|
27
|
+
import { cliVersion } from "../../infra/paths.js";
|
|
28
|
+
import { applyMemoryMigration, bunIntegrityCheck, describePlan, describeResult, planMemoryMigration } from "./migrate.js";
|
|
29
|
+
import { FREE_DIR_NAME, LEGACY_DIR_NAME, resolveMemoryPaths } from "./paths.js";
|
|
29
30
|
const IS_WINDOWS = process.platform === "win32";
|
|
30
31
|
/** Name of the digest marker written into the plugin cache directory after a copy. */
|
|
31
32
|
const BUNDLE_MARKER_NAME = "bundle.sha256";
|
|
@@ -45,7 +46,7 @@ const CAPABILITY_ID = "memory";
|
|
|
45
46
|
* canonicalDir: The resolved FreeHarness canonical directory (packages/cli/canonical).
|
|
46
47
|
*
|
|
47
48
|
* Returns:
|
|
48
|
-
* Absolute engine path, or null when the vendored tree is absent (
|
|
49
|
+
* Absolute engine path, or null when the vendored tree is absent (memory is then skipped).
|
|
49
50
|
*/
|
|
50
51
|
export function vendoredMemoryEngineDir(canonicalDir) {
|
|
51
52
|
const engineDir = path.join(canonicalDir, "..", "..", "memory", "vendor", UPSTREAM_PLUGIN_NAME, "engine");
|
|
@@ -63,7 +64,7 @@ export function vendoredMemoryEngineDir(canonicalDir) {
|
|
|
63
64
|
export function memoryBuildActions(engineDir) {
|
|
64
65
|
return [
|
|
65
66
|
{
|
|
66
|
-
description: "Build the
|
|
67
|
+
description: "Build the free-mem plugin bundles (worker, mcp, context, watcher) from the vendored engine with Bun",
|
|
67
68
|
command: "bun install && bun run build:plugin",
|
|
68
69
|
cwd: engineDir,
|
|
69
70
|
optional: true,
|
|
@@ -93,42 +94,127 @@ export function claudePluginPaths(version) {
|
|
|
93
94
|
};
|
|
94
95
|
}
|
|
95
96
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
|
|
97
|
+
* Top-level entries the cache gets after the copy (our markers, the deps lockfile, Claude
|
|
98
|
+
* Code's in-use and orphan markers): never copied and never digested, so staging cannot write
|
|
99
|
+
* through a plugin symlink and an install never reads its own bookkeeping as tampering.
|
|
100
|
+
*/
|
|
101
|
+
const UNSTAGED_ENTRIES = new Set([BUNDLE_MARKER_NAME, ".install-version", "bun.lock", "bun.lockb", ".in_use", ".orphaned_at"]);
|
|
102
|
+
const S_IFMT = 0o170000;
|
|
103
|
+
/** Built bundles a plugin dir needs before it may replace a cached build. */
|
|
104
|
+
export const PLUGIN_BUNDLES = ["bun-runner.js", "worker-service.cjs", "mcp-server.cjs"];
|
|
105
|
+
/**
|
|
106
|
+
* Digest of a plugin bundle over every entry's relpath, type, mode and content or link target,
|
|
107
|
+
* in path order. Walks with lstat and never follows a symlink. Skips `node_modules` and the
|
|
108
|
+
* unstaged entries, so the vendored plugin and its staged copy digest alike.
|
|
100
109
|
*
|
|
101
110
|
* Args:
|
|
102
111
|
* dir: Directory to digest recursively (absent dir digests as empty).
|
|
103
112
|
*
|
|
104
113
|
* Returns:
|
|
105
|
-
* Lowercase hex SHA-256
|
|
114
|
+
* Lowercase hex SHA-256.
|
|
106
115
|
*/
|
|
107
116
|
export function bundleDigest(dir) {
|
|
108
|
-
const
|
|
117
|
+
const entries = [];
|
|
109
118
|
const walk = (rel) => {
|
|
110
|
-
for (const
|
|
111
|
-
if (
|
|
119
|
+
for (const name of fs.readdirSync(path.join(dir, rel))) {
|
|
120
|
+
if (name === "node_modules" || (!rel && UNSTAGED_ENTRIES.has(name)))
|
|
112
121
|
continue;
|
|
113
|
-
const childRel = rel ? `${rel}/${
|
|
114
|
-
|
|
122
|
+
const childRel = rel ? `${rel}/${name}` : name;
|
|
123
|
+
const abs = path.join(dir, childRel);
|
|
124
|
+
const stat = fs.lstatSync(abs);
|
|
125
|
+
if (!stat.isFile() && !stat.isDirectory() && !stat.isSymbolicLink())
|
|
126
|
+
continue;
|
|
127
|
+
entries.push({ rel: childRel, abs, stat });
|
|
128
|
+
if (stat.isDirectory())
|
|
115
129
|
walk(childRel);
|
|
116
|
-
else if (entry.isFile())
|
|
117
|
-
files.push(childRel);
|
|
118
130
|
}
|
|
119
131
|
};
|
|
120
132
|
if (fs.existsSync(dir))
|
|
121
133
|
walk("");
|
|
122
|
-
|
|
134
|
+
entries.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
|
|
123
135
|
const hash = crypto.createHash("sha256");
|
|
124
|
-
for (const rel of
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
for (const { rel, abs, stat } of entries) {
|
|
137
|
+
const type = stat.isSymbolicLink() ? "l" : stat.isDirectory() ? "d" : "f";
|
|
138
|
+
// Windows has no exec bits and symlink permissions carry no meaning.
|
|
139
|
+
const mode = IS_WINDOWS || type === "l" ? stat.mode & S_IFMT : stat.mode;
|
|
140
|
+
for (const part of [rel, type, mode.toString(8)]) {
|
|
141
|
+
hash.update(part, "utf-8");
|
|
142
|
+
hash.update("\0");
|
|
143
|
+
}
|
|
144
|
+
if (type === "f")
|
|
145
|
+
hash.update(fs.readFileSync(abs));
|
|
146
|
+
else if (type === "l")
|
|
147
|
+
hash.update(fs.readlinkSync(abs), "utf-8");
|
|
128
148
|
hash.update("\0");
|
|
129
149
|
}
|
|
130
150
|
return hash.digest("hex");
|
|
131
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Stage a plugin dir into a cache dir unless the marker and the cache's own digest both equal
|
|
154
|
+
* the plugin digest and the declared deps resolve; `staged` is true for a (would-be) re-stage.
|
|
155
|
+
*/
|
|
156
|
+
export function stagePluginCache(pluginDir, cacheDir, opts) {
|
|
157
|
+
const digest = bundleDigest(pluginDir);
|
|
158
|
+
const short = digest.slice(0, 8);
|
|
159
|
+
const markerFile = path.join(cacheDir, BUNDLE_MARKER_NAME);
|
|
160
|
+
const marker = fs.existsSync(markerFile) ? fs.readFileSync(markerFile, "utf-8").trim() : null;
|
|
161
|
+
if (marker === digest && bundleDigest(cacheDir) === marker && missingPluginDeps(cacheDir).length === 0) {
|
|
162
|
+
return { staged: false, digest, notes: [`plugin cache up to date (bundle ${short})`] };
|
|
163
|
+
}
|
|
164
|
+
if (opts.dryRun)
|
|
165
|
+
return { staged: true, digest, notes: [`Would re-stage the plugin cache at ${cacheDir} (bundle ${short}).`] };
|
|
166
|
+
fs.rmSync(cacheDir, { recursive: true, force: true });
|
|
167
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
168
|
+
const root = path.resolve(pluginDir);
|
|
169
|
+
const unstaged = (src) => path.basename(src) === "node_modules" || (path.resolve(path.dirname(src)) === root && UNSTAGED_ENTRIES.has(path.basename(src)));
|
|
170
|
+
fs.cpSync(pluginDir, cacheDir, { recursive: true, force: true, verbatimSymlinks: true, filter: (src) => !unstaged(src) });
|
|
171
|
+
fs.writeFileSync(markerFile, `${digest}\n`, { encoding: "utf-8", flag: "wx" });
|
|
172
|
+
return { staged: true, digest, notes: [`Re-staged the plugin cache at ${cacheDir} (bundle ${short}).`] };
|
|
173
|
+
}
|
|
174
|
+
/** Identity of the cache marker file (absent: null); every re-stage writes a new one. */
|
|
175
|
+
export function stageStamp(cacheDir) {
|
|
176
|
+
const markerFile = path.join(cacheDir, BUNDLE_MARKER_NAME);
|
|
177
|
+
if (!fs.existsSync(markerFile))
|
|
178
|
+
return null;
|
|
179
|
+
const st = fs.lstatSync(markerFile);
|
|
180
|
+
return `${st.ino}:${st.mtimeMs}:${fs.readFileSync(markerFile, "utf-8").trim()}`;
|
|
181
|
+
}
|
|
182
|
+
export function missingPluginBundles(pluginDir) {
|
|
183
|
+
return PLUGIN_BUNDLES.filter((f) => !fs.existsSync(path.join(pluginDir, "scripts", f)));
|
|
184
|
+
}
|
|
185
|
+
/** Whether a plugin dir's package.json declares runtime deps, which only Bun installs. */
|
|
186
|
+
export function declaresPluginDeps(dir) {
|
|
187
|
+
const pkg = readJsonSafe(path.join(dir, "package.json"), {});
|
|
188
|
+
return Object.keys(pkg.dependencies ?? {}).length > 0;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Declared plugin deps that do not resolve from `<dir>/node_modules` (upstream
|
|
192
|
+
* verifyCriticalModules), plus the zod subpaths the worker requires when zod is declared.
|
|
193
|
+
*/
|
|
194
|
+
export function missingPluginDeps(dir) {
|
|
195
|
+
const pkg = readJsonSafe(path.join(dir, "package.json"), {});
|
|
196
|
+
const deps = Object.keys(pkg.dependencies ?? {});
|
|
197
|
+
const nm = path.join(dir, "node_modules");
|
|
198
|
+
const missing = deps.filter((dep) => !fs.existsSync(path.join(nm, dep, "package.json")));
|
|
199
|
+
if (deps.includes("zod")) {
|
|
200
|
+
for (const sub of ["v3", "v4", "v4-mini"]) {
|
|
201
|
+
if (!fs.existsSync(path.join(nm, "zod", sub)))
|
|
202
|
+
missing.push(`zod/${sub}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return missing;
|
|
206
|
+
}
|
|
207
|
+
/** Install a staged cache's runtime deps (the hooks run from there); throws when one stays missing. */
|
|
208
|
+
export function installPluginDeps(dir, bunPath) {
|
|
209
|
+
const r = spawnSync(bunPath, ["install", "--ignore-scripts"], { cwd: dir, stdio: "inherit", windowsHide: true, env: process.env });
|
|
210
|
+
if (r.error)
|
|
211
|
+
throw new Error(`bun install (plugin deps) failed: ${r.error.message}`);
|
|
212
|
+
if (r.status !== 0)
|
|
213
|
+
throw new Error(`bun install (plugin deps) failed (exit ${r.status})`);
|
|
214
|
+
const missing = missingPluginDeps(dir);
|
|
215
|
+
if (missing.length > 0)
|
|
216
|
+
throw new Error(`plugin deps missing after install in ${dir}: ${missing.join(", ")}`);
|
|
217
|
+
}
|
|
132
218
|
function probeTool(command, commonPaths) {
|
|
133
219
|
const candidates = [command, ...commonPaths];
|
|
134
220
|
for (const candidate of candidates) {
|
|
@@ -137,7 +223,7 @@ function probeTool(command, commonPaths) {
|
|
|
137
223
|
try {
|
|
138
224
|
const r = spawnSync(candidate, ["--version"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
|
|
139
225
|
if (r.status === 0) {
|
|
140
|
-
// "1.3.14"
|
|
226
|
+
// "1.3.14" or "tool 0.9.2 (hash date)" -> first semver-looking token.
|
|
141
227
|
const semver = r.stdout.match(/\d+\.\d+(?:\.\d+)?/);
|
|
142
228
|
return { path: candidate, version: semver ? semver[0] : r.stdout.trim() };
|
|
143
229
|
}
|
|
@@ -161,31 +247,17 @@ export function probeBun() {
|
|
|
161
247
|
: [path.join(home, ".bun", "bin", "bun"), "/usr/local/bin/bun", "/opt/homebrew/bin/bun", "/home/linuxbrew/.linuxbrew/bin/bun"];
|
|
162
248
|
return probeTool("bun", common);
|
|
163
249
|
}
|
|
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
250
|
/**
|
|
178
251
|
* 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).
|
|
252
|
+
* installed_plugins.json, or enabledPlugins carries it at a value other than `false`. Read-only:
|
|
253
|
+
* doctor warns on it and the data copy refuses while it is live.
|
|
182
254
|
*
|
|
183
255
|
* Args:
|
|
184
256
|
* installed: Parsed installed_plugins.json (or `{}` when absent).
|
|
185
257
|
* settings: Parsed settings.json (or `{}` when absent).
|
|
186
258
|
*
|
|
187
259
|
* Returns:
|
|
188
|
-
* True when the upstream
|
|
260
|
+
* True when the upstream plugin may still load beside free-mem.
|
|
189
261
|
*/
|
|
190
262
|
export function isUpstreamRegistered(installed, settings) {
|
|
191
263
|
if (settings.enabledPlugins?.[UPSTREAM_PLUGIN_KEY] === false)
|
|
@@ -212,12 +284,36 @@ function writeJsonAtomic(file, value) {
|
|
|
212
284
|
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", "utf-8");
|
|
213
285
|
fs.renameSync(tmp, file);
|
|
214
286
|
}
|
|
215
|
-
|
|
287
|
+
/** Write the `.install-version` marker that tells our build apart; never writes through a symlink. `uv` stays for readers of the old shape. */
|
|
288
|
+
export function writeInstallMarker(dir, version, bun) {
|
|
216
289
|
const marker = path.join(dir, ".install-version");
|
|
217
290
|
fs.mkdirSync(dir, { recursive: true });
|
|
218
|
-
fs.
|
|
291
|
+
fs.rmSync(marker, { force: true });
|
|
292
|
+
fs.writeFileSync(marker, JSON.stringify({ version, bun, uv: "unused", installedAt: new Date().toISOString() }), { flag: "wx" });
|
|
219
293
|
return marker;
|
|
220
294
|
}
|
|
295
|
+
const PLUGIN_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
296
|
+
/**
|
|
297
|
+
* Version from a plugin dir's manifest ("0.0.0" when absent). The version names a cache
|
|
298
|
+
* directory that staging removes, so an empty or path-like value must never reach a path.
|
|
299
|
+
*
|
|
300
|
+
* Args:
|
|
301
|
+
* pluginDir: Plugin directory holding `.claude-plugin/plugin.json`.
|
|
302
|
+
*
|
|
303
|
+
* Returns:
|
|
304
|
+
* The semver version.
|
|
305
|
+
*
|
|
306
|
+
* Raises:
|
|
307
|
+
* Error: When the manifest version is not a semver version.
|
|
308
|
+
*/
|
|
309
|
+
export function pluginVersion(pluginDir) {
|
|
310
|
+
const manifest = path.join(pluginDir, ".claude-plugin", "plugin.json");
|
|
311
|
+
const version = readJsonSafe(manifest, {}).version ?? "0.0.0";
|
|
312
|
+
if (typeof version !== "string" || !PLUGIN_SEMVER.test(version)) {
|
|
313
|
+
throw new Error(`plugin version ${JSON.stringify(version)} in ${manifest} is not a semver version; refusing to stage the plugin cache`);
|
|
314
|
+
}
|
|
315
|
+
return version;
|
|
316
|
+
}
|
|
221
317
|
/** MemoryPort implementation backed by the vendored claude-mem plugin build. */
|
|
222
318
|
export class VendoredMemory {
|
|
223
319
|
engineDir;
|
|
@@ -237,15 +333,13 @@ export class VendoredMemory {
|
|
|
237
333
|
}
|
|
238
334
|
/** Plugin version from the vendored manifest (upstream readPluginVersion). */
|
|
239
335
|
version() {
|
|
240
|
-
|
|
241
|
-
const parsed = readJsonSafe(manifest, {});
|
|
242
|
-
return parsed.version ?? "0.0.0";
|
|
336
|
+
return pluginVersion(this.pluginDir);
|
|
243
337
|
}
|
|
244
338
|
result() {
|
|
245
339
|
return { capabilityId: CAPABILITY_ID, target: this.target, written: [], actions: [], notes: [] };
|
|
246
340
|
}
|
|
247
341
|
/**
|
|
248
|
-
* Probe Bun
|
|
342
|
+
* Probe Bun; instruct when missing. Never installs it (user's machine).
|
|
249
343
|
*
|
|
250
344
|
* Returns:
|
|
251
345
|
* Notes only. A missing Bun is reported as "MISSING" and blocks install().
|
|
@@ -253,11 +347,7 @@ export class VendoredMemory {
|
|
|
253
347
|
async ensurePrerequisites(_ctx) {
|
|
254
348
|
const res = this.result();
|
|
255
349
|
const bun = probeBun();
|
|
256
|
-
const uv = probeUv();
|
|
257
350
|
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
351
|
return res;
|
|
262
352
|
}
|
|
263
353
|
/**
|
|
@@ -274,7 +364,6 @@ export class VendoredMemory {
|
|
|
274
364
|
res.notes.push("Install skipped: Bun is required to build the plugin and to run its hooks.");
|
|
275
365
|
return res;
|
|
276
366
|
}
|
|
277
|
-
const uv = probeUv();
|
|
278
367
|
const scriptsDir = path.join(this.pluginDir, "scripts");
|
|
279
368
|
const marketplacePlugin = path.join(P.marketplaceDir, "plugin");
|
|
280
369
|
// 1. Build (bun install once, then build:plugin).
|
|
@@ -282,8 +371,7 @@ export class VendoredMemory {
|
|
|
282
371
|
res.notes.push(`Would build the plugin bundles: bun run build:plugin (in ${this.engineDir}).`);
|
|
283
372
|
}
|
|
284
373
|
else {
|
|
285
|
-
const
|
|
286
|
-
const reuse = process.env.FREEHARNESS_MEMORY_SKIP_BUILD === "1" && bundles.every((f) => fs.existsSync(path.join(scriptsDir, f)));
|
|
374
|
+
const reuse = process.env.FREEHARNESS_MEMORY_SKIP_BUILD === "1" && missingPluginBundles(this.pluginDir).length === 0;
|
|
287
375
|
if (reuse) {
|
|
288
376
|
res.notes.push(`Reused the built plugin bundles in ${scriptsDir} (FREEHARNESS_MEMORY_SKIP_BUILD=1).`);
|
|
289
377
|
}
|
|
@@ -291,34 +379,17 @@ export class VendoredMemory {
|
|
|
291
379
|
if (!fs.existsSync(path.join(this.engineDir, "node_modules")))
|
|
292
380
|
this.run(bun.path, ["install"], this.engineDir, "bun install (engine)");
|
|
293
381
|
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
|
-
}
|
|
382
|
+
for (const f of missingPluginBundles(this.pluginDir))
|
|
383
|
+
throw new Error(`build:plugin did not produce ${f}`);
|
|
298
384
|
res.notes.push(`Built plugin bundles v${version} into ${scriptsDir}.`);
|
|
299
385
|
}
|
|
300
386
|
}
|
|
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;
|
|
387
|
+
// 2. copyPluginToCache: rm + cp plugin/ -> cache/<owner>/<name>/<version>/, skipped when
|
|
388
|
+
// the cache already holds this exact bundle; a broken node_modules takes the re-stage path.
|
|
389
|
+
const stage = stagePluginCache(this.pluginDir, P.cacheDir, { dryRun: ctx.dryRun });
|
|
390
|
+
const cacheUpToDate = !stage.staged;
|
|
309
391
|
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
|
-
}
|
|
392
|
+
res.notes.push(...stage.notes);
|
|
322
393
|
// 3. copyPluginToMarketplace: plugin/ + marketplace manifest + package bits.
|
|
323
394
|
res.written.push(marketplacePlugin);
|
|
324
395
|
if (!ctx.dryRun) {
|
|
@@ -343,8 +414,7 @@ export class VendoredMemory {
|
|
|
343
414
|
res.notes.push(`Would install plugin runtime deps in ${P.cacheDir} (bun install --ignore-scripts).`);
|
|
344
415
|
}
|
|
345
416
|
else {
|
|
346
|
-
|
|
347
|
-
this.verifyPluginDeps(P.cacheDir);
|
|
417
|
+
installPluginDeps(P.cacheDir, bun.path);
|
|
348
418
|
res.notes.push("Plugin runtime deps installed (zod resolvable from plugin/node_modules).");
|
|
349
419
|
}
|
|
350
420
|
// 5. Install markers (cache + marketplace + marketplace/plugin, as upstream); the
|
|
@@ -353,11 +423,11 @@ export class VendoredMemory {
|
|
|
353
423
|
for (const dir of [P.marketplaceDir, marketplacePlugin]) {
|
|
354
424
|
res.written.push(path.join(dir, ".install-version"));
|
|
355
425
|
if (!ctx.dryRun)
|
|
356
|
-
writeInstallMarker(dir, version, bun.version ?? "unknown"
|
|
426
|
+
writeInstallMarker(dir, version, bun.version ?? "unknown");
|
|
357
427
|
}
|
|
358
428
|
res.written.push(path.join(P.cacheDir, ".install-version"));
|
|
359
429
|
if (!cacheUpToDate && !ctx.dryRun)
|
|
360
|
-
writeInstallMarker(P.cacheDir, version, bun.version ?? "unknown"
|
|
430
|
+
writeInstallMarker(P.cacheDir, version, bun.version ?? "unknown");
|
|
361
431
|
res.notes.push(`Plugin v${version} staged at ${P.cacheDir} (marketplace: ${P.marketplaceDir}).`);
|
|
362
432
|
return res;
|
|
363
433
|
}
|
|
@@ -365,30 +435,13 @@ export class VendoredMemory {
|
|
|
365
435
|
* Register the plugin with Claude Code (upstream registerMarketplace / registerPlugin /
|
|
366
436
|
* enablePluginInClaudeSettings). All three writes are additive merges: other
|
|
367
437
|
* 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.
|
|
438
|
+
* An upstream registration (UPSTREAM_PLUGIN_KEY) is never written: doctor warns about it.
|
|
373
439
|
*/
|
|
374
440
|
async registerHooks(_scope, ctx) {
|
|
375
441
|
const res = this.result();
|
|
376
442
|
const version = this.version();
|
|
377
443
|
const P = claudePluginPaths(version);
|
|
378
444
|
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
445
|
// known_marketplaces.json: { "<owner>": { source, installLocation, lastUpdated, autoUpdate } }
|
|
393
446
|
const known = readJsonSafe(P.knownMarketplaces, {});
|
|
394
447
|
known[MARKETPLACE_OWNER] = {
|
|
@@ -401,8 +454,6 @@ export class VendoredMemory {
|
|
|
401
454
|
if (!ctx.dryRun)
|
|
402
455
|
writeJsonAtomic(P.knownMarketplaces, known);
|
|
403
456
|
// 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
457
|
const installed = readJsonSafe(P.installedPlugins, {});
|
|
407
458
|
if (!installed.version)
|
|
408
459
|
installed.version = 2;
|
|
@@ -412,48 +463,55 @@ export class VendoredMemory {
|
|
|
412
463
|
res.written.push(P.installedPlugins);
|
|
413
464
|
if (!ctx.dryRun)
|
|
414
465
|
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.
|
|
466
|
+
// settings.json: enabledPlugins[PLUGIN_KEY] = true (everything else kept).
|
|
417
467
|
const settings = readJsonSafe(P.settings, {});
|
|
418
468
|
if (!settings.enabledPlugins)
|
|
419
469
|
settings.enabledPlugins = {};
|
|
420
470
|
const already = settings.enabledPlugins[PLUGIN_KEY] === true;
|
|
421
471
|
settings.enabledPlugins[PLUGIN_KEY] = true;
|
|
422
|
-
if (migrating)
|
|
423
|
-
settings.enabledPlugins[UPSTREAM_PLUGIN_KEY] = false;
|
|
424
472
|
res.written.push(P.settings);
|
|
425
473
|
if (!ctx.dryRun)
|
|
426
474
|
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
|
-
}
|
|
475
|
+
res.notes.push(`${ctx.dryRun ? "Would register" : "Registered"} plugin ${PLUGIN_KEY} v${version}: marketplace + installed_plugins + enabledPlugins (additive)${already ? "; was already enabled" : ""}.`);
|
|
435
476
|
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
477
|
return res;
|
|
437
478
|
}
|
|
438
479
|
/**
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
480
|
+
* Copy a legacy ~/.claude-mem forward into ~/.free-mem: pass 1 prints the plan, pass 2 applies
|
|
481
|
+
* it. A worker it stops is restarted by `update` once the bundle is staged, else at SessionStart.
|
|
482
|
+
* The copy is never recorded in `written`, since uninstall removes what the manifest lists.
|
|
442
483
|
*/
|
|
443
|
-
async migrateExisting(
|
|
484
|
+
async migrateExisting(ctx) {
|
|
444
485
|
const res = this.result();
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
486
|
+
const home = os.homedir();
|
|
487
|
+
const plan = planMemoryMigration(home, process.env);
|
|
488
|
+
if (!plan) {
|
|
489
|
+
const p = resolveMemoryPaths(process.env, home);
|
|
490
|
+
const freeDir = path.join(home, FREE_DIR_NAME);
|
|
491
|
+
const db = fs.existsSync(p.dbFile) ? ` (${path.basename(p.dbFile)} present)` : "";
|
|
492
|
+
if (p.source !== "default")
|
|
493
|
+
res.notes.push(`Existing memory data kept at ${p.dataDir}${db}; the ${PLUGIN_NAME} plugin reads it through ${p.source}, so no copy is planned. Nothing deleted.`);
|
|
494
|
+
else if (fs.existsSync(freeDir))
|
|
495
|
+
res.notes.push(`Memory data already at ${freeDir}; no copy planned.`);
|
|
496
|
+
else
|
|
497
|
+
res.notes.push(`No legacy memory data at ${path.join(home, LEGACY_DIR_NAME)}; the worker creates ${freeDir} on first start.`);
|
|
498
|
+
return res;
|
|
499
|
+
}
|
|
500
|
+
if (ctx.dryRun) {
|
|
501
|
+
res.notes.push(...describePlan(plan));
|
|
448
502
|
return res;
|
|
449
503
|
}
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
504
|
+
const result = await applyMemoryMigration(plan, {
|
|
505
|
+
stopWorker: async () => (await import("./doctor.js")).stopWorker("shutdown"),
|
|
506
|
+
now: Date.now,
|
|
507
|
+
integrityCheck: bunIntegrityCheck(probeBun().path),
|
|
508
|
+
version: cliVersion(),
|
|
509
|
+
});
|
|
510
|
+
const lines = describeResult(plan, result);
|
|
511
|
+
res.notes.push(...lines);
|
|
512
|
+
// The executor prints only the preview's notes, so the outcome of pass 2 is printed here.
|
|
513
|
+
for (const l of lines)
|
|
514
|
+
console.log(`memory: ${l}`);
|
|
457
515
|
return res;
|
|
458
516
|
}
|
|
459
517
|
run(cmd, args, cwd, label) {
|
|
@@ -463,25 +521,4 @@ export class VendoredMemory {
|
|
|
463
521
|
if (r.status !== 0)
|
|
464
522
|
throw new Error(`${label} failed (exit ${r.status})`);
|
|
465
523
|
}
|
|
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
524
|
}
|
|
@@ -87,7 +87,10 @@ function manifest(target, repo, fork) {
|
|
|
87
87
|
label: "plugin/.mcp.json", vendored: path.join(v, "plugin/.mcp.json"), upstream: path.join(fork, "plugin/.mcp.json"), contract: true,
|
|
88
88
|
divergent: "rewritten to spawn ${CLAUDE_PLUGIN_ROOT}/scripts/mcp-server.cjs directly (memory-slim)",
|
|
89
89
|
},
|
|
90
|
-
{
|
|
90
|
+
{
|
|
91
|
+
label: "mem-search/SKILL.md", vendored: path.join(v, "plugin/skills/mem-search/SKILL.md"), upstream: path.join(fork, "plugin/skills/mem-search/SKILL.md"), contract: true,
|
|
92
|
+
divergent: "rewritten to name only the tools the slim MCP server registers (radar-w43-fixes)",
|
|
93
|
+
},
|
|
91
94
|
];
|
|
92
95
|
}
|
|
93
96
|
throw new Error(`Unknown sync target: ${target}. Valid: sdd, memory.`);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PromptPort } from "@freeharness/core";
|
|
2
|
+
import { type CleanupItem, type CleanupPlan, type CleanupResult } from "./cleanup.js";
|
|
3
|
+
import { type HardPlan, type HardResult } from "./hard-cleanup.js";
|
|
4
|
+
export interface CleanupAnswers {
|
|
5
|
+
main: boolean;
|
|
6
|
+
backups: boolean;
|
|
7
|
+
hard: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface CleanupDecision {
|
|
10
|
+
abort?: string;
|
|
11
|
+
remove: (item: CleanupItem) => boolean;
|
|
12
|
+
moveHard: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Maps the answers to pass-2 actions; the hard answer counts only with --hard.
|
|
16
|
+
* @param offersMoves The cleanup plan holds an archive item, so the abort line names moves too.
|
|
17
|
+
*/
|
|
18
|
+
export declare function cleanupDecision(answers: CleanupAnswers, withHard: boolean, offersMoves?: boolean): CleanupDecision;
|
|
19
|
+
/** The third cleanup confirmation, for the hard items. */
|
|
20
|
+
export declare function hardPrompt(count: number, archiveRoot: string): string;
|
|
21
|
+
export interface CleanupPass2 {
|
|
22
|
+
plan: CleanupPlan;
|
|
23
|
+
hard?: HardPlan;
|
|
24
|
+
/** `--yes`: every prompt is answered yes and none is shown. */
|
|
25
|
+
yes: boolean;
|
|
26
|
+
prompt: Pick<PromptPort, "confirm">;
|
|
27
|
+
log: (line: string) => void;
|
|
28
|
+
}
|
|
29
|
+
/** `archived` is absent when the hard plan was not confirmed. */
|
|
30
|
+
export interface CleanupPass2Result {
|
|
31
|
+
done: CleanupResult;
|
|
32
|
+
archived?: HardResult;
|
|
33
|
+
}
|
|
34
|
+
/** Asks the three confirmations, then applies what was confirmed; undefined when all were no. */
|
|
35
|
+
export declare function runCleanupPass2(input: CleanupPass2): Promise<CleanupPass2Result | undefined>;
|