superlocalmemory 3.6.23 → 3.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +271 -71
- package/bin/slm-npm +43 -89
- package/ide/configs/antigravity-mcp.json +2 -2
- package/ide/configs/chatgpt-desktop-mcp.json +1 -1
- package/ide/configs/claude-desktop-mcp.json +2 -2
- package/ide/configs/windsurf-mcp.json +2 -2
- package/ide/hooks/context-hook.js +6 -2
- package/ide/hooks/post-recall-hook.js +7 -3
- package/ide/hooks/tool-event-hook.sh +2 -1
- package/package.json +17 -9
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/_GENERATED.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/agents/slm-memory-advisor.md +1 -1
- package/plugin-src/manifest.json +1 -1
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/pyproject.toml +40 -8
- package/scripts/postinstall-interactive.js +17 -94
- package/scripts/postinstall.js +185 -258
- package/scripts/preuninstall.js +9 -50
- package/src/superlocalmemory/__init__.py +2 -2
- package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
- package/src/superlocalmemory/attribution/signer.py +34 -19
- package/src/superlocalmemory/attribution/watermark.py +1 -1
- package/src/superlocalmemory/cli/_lazy_init.py +7 -7
- package/src/superlocalmemory/cli/commands.py +453 -191
- package/src/superlocalmemory/cli/context_commands.py +5 -4
- package/src/superlocalmemory/cli/daemon.py +282 -187
- package/src/superlocalmemory/cli/db_migrate.py +3 -1
- package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
- package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
- package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
- package/src/superlocalmemory/cli/main.py +128 -31
- package/src/superlocalmemory/cli/pending_store.py +54 -38
- package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
- package/src/superlocalmemory/cli/service_installer.py +57 -52
- package/src/superlocalmemory/cli/setup_wizard.py +142 -88
- package/src/superlocalmemory/cli/version_banner.py +2 -1
- package/src/superlocalmemory/code_graph/config.py +3 -1
- package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
- package/src/superlocalmemory/core/config.py +65 -20
- package/src/superlocalmemory/core/consolidation_engine.py +9 -7
- package/src/superlocalmemory/core/context_cache.py +56 -8
- package/src/superlocalmemory/core/derivation_lineage.py +246 -0
- package/src/superlocalmemory/core/embedding_worker.py +32 -20
- package/src/superlocalmemory/core/embeddings.py +54 -18
- package/src/superlocalmemory/core/engine.py +150 -104
- package/src/superlocalmemory/core/engine_ingestion.py +513 -0
- package/src/superlocalmemory/core/engine_wiring.py +32 -23
- package/src/superlocalmemory/core/evidence_bundle.py +526 -0
- package/src/superlocalmemory/core/fact_consolidator.py +5 -11
- package/src/superlocalmemory/core/graph_analyzer.py +2 -2
- package/src/superlocalmemory/core/health_monitor.py +4 -2
- package/src/superlocalmemory/core/ingestion_command.py +636 -0
- package/src/superlocalmemory/core/injection.py +69 -18
- package/src/superlocalmemory/core/lifecycle_state.py +153 -0
- package/src/superlocalmemory/core/maintenance.py +1 -1
- package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
- package/src/superlocalmemory/core/mutations.py +143 -0
- package/src/superlocalmemory/core/platform_utils.py +7 -4
- package/src/superlocalmemory/core/ram_lock.py +16 -5
- package/src/superlocalmemory/core/rate_limit.py +1 -1
- package/src/superlocalmemory/core/recall_pipeline.py +60 -101
- package/src/superlocalmemory/core/recall_worker.py +76 -59
- package/src/superlocalmemory/core/registry.py +1 -1
- package/src/superlocalmemory/core/scale_engine.py +293 -0
- package/src/superlocalmemory/core/score_contract.py +62 -0
- package/src/superlocalmemory/core/security_primitives.py +3 -1
- package/src/superlocalmemory/core/slm_disabled.py +3 -5
- package/src/superlocalmemory/core/store_pipeline.py +172 -40
- package/src/superlocalmemory/core/tier_manager.py +32 -20
- package/src/superlocalmemory/core/worker_pool.py +13 -4
- package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
- package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
- package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
- package/src/superlocalmemory/encoding/auto_linker.py +1 -1
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
- package/src/superlocalmemory/encoding/consolidator.py +22 -5
- package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
- package/src/superlocalmemory/encoding/foresight.py +2 -0
- package/src/superlocalmemory/encoding/graph_builder.py +1 -1
- package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
- package/src/superlocalmemory/evaluation/__init__.py +13 -0
- package/src/superlocalmemory/evaluation/calibration.py +308 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
- package/src/superlocalmemory/graph/cozo_backend.py +256 -23
- package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
- package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
- package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
- package/src/superlocalmemory/hooks/auto_recall.py +31 -6
- package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
- package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
- package/src/superlocalmemory/hooks/claude_code_hooks.py +123 -35
- package/src/superlocalmemory/hooks/codex_assets.py +59 -0
- package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
- package/src/superlocalmemory/hooks/context_payload.py +1 -1
- package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
- package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
- package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
- package/src/superlocalmemory/hooks/hook_handlers.py +219 -32
- package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
- package/src/superlocalmemory/hooks/session_registry.py +15 -8
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
- package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
- package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
- package/src/superlocalmemory/infra/auth_middleware.py +38 -5
- package/src/superlocalmemory/infra/backup.py +7 -5
- package/src/superlocalmemory/infra/cloud_backup.py +18 -8
- package/src/superlocalmemory/infra/daemon_identity.py +248 -0
- package/src/superlocalmemory/infra/data_root.py +199 -0
- package/src/superlocalmemory/infra/event_bus.py +3 -1
- package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
- package/src/superlocalmemory/infra/process_reaper.py +23 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
- package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
- package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
- package/src/superlocalmemory/ingestion/credentials.py +14 -7
- package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
- package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
- package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
- package/src/superlocalmemory/learning/ensemble.py +11 -0
- package/src/superlocalmemory/learning/entity_compiler.py +1 -1
- package/src/superlocalmemory/learning/feedback.py +1 -1
- package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
- package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
- package/src/superlocalmemory/learning/ranker.py +4 -1
- package/src/superlocalmemory/learning/source_quality.py +1 -1
- package/src/superlocalmemory/learning/trigram_index.py +3 -2
- package/src/superlocalmemory/llm/backbone.py +1 -1
- package/src/superlocalmemory/math/ebbinghaus.py +1 -1
- package/src/superlocalmemory/math/fisher.py +1 -1
- package/src/superlocalmemory/math/fisher_quantized.py +1 -1
- package/src/superlocalmemory/math/hopfield.py +1 -1
- package/src/superlocalmemory/math/langevin.py +1 -1
- package/src/superlocalmemory/math/polar_quant.py +3 -4
- package/src/superlocalmemory/math/qjl.py +1 -1
- package/src/superlocalmemory/math/sheaf.py +1 -1
- package/src/superlocalmemory/math/turbo_quant.py +3 -2
- package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
- package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
- package/src/superlocalmemory/mcp/http_transport.py +53 -0
- package/src/superlocalmemory/mcp/server.py +39 -13
- package/src/superlocalmemory/mcp/shared.py +69 -3
- package/src/superlocalmemory/mcp/tools_active.py +141 -31
- package/src/superlocalmemory/mcp/tools_core.py +130 -31
- package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
- package/src/superlocalmemory/mcp/tools_learning.py +42 -2
- package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
- package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
- package/src/superlocalmemory/mcp/tools_v28.py +23 -2
- package/src/superlocalmemory/mcp/tools_v3.py +26 -1
- package/src/superlocalmemory/mcp/tools_v33.py +56 -17
- package/src/superlocalmemory/mesh/broker.py +2 -0
- package/src/superlocalmemory/mesh/remote_sync.py +50 -12
- package/src/superlocalmemory/optimize/cache/manager.py +77 -1
- package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
- package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
- package/src/superlocalmemory/optimize/compress/router.py +6 -1
- package/src/superlocalmemory/optimize/config/__init__.py +5 -0
- package/src/superlocalmemory/optimize/config/store.py +6 -4
- package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
- package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
- package/src/superlocalmemory/optimize/proxy/server.py +2 -2
- package/src/superlocalmemory/optimize/storage/db.py +12 -12
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/ann_index.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
- package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
- package/src/superlocalmemory/retrieval/engine.py +169 -79
- package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
- package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
- package/src/superlocalmemory/retrieval/fusion.py +1 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
- package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
- package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
- package/src/superlocalmemory/retrieval/reranker.py +56 -20
- package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
- package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
- package/src/superlocalmemory/retrieval/strategy.py +1 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
- package/src/superlocalmemory/retrieval/vector_store.py +1 -1
- package/src/superlocalmemory/server/api.py +10 -7
- package/src/superlocalmemory/server/bandit_loops.py +4 -2
- package/src/superlocalmemory/server/recall_serializer.py +24 -0
- package/src/superlocalmemory/server/route_mutations.py +84 -0
- package/src/superlocalmemory/server/routes/agents.py +8 -6
- package/src/superlocalmemory/server/routes/brain.py +14 -12
- package/src/superlocalmemory/server/routes/chat.py +29 -12
- package/src/superlocalmemory/server/routes/data_io.py +55 -24
- package/src/superlocalmemory/server/routes/helpers.py +8 -63
- package/src/superlocalmemory/server/routes/ingest.py +53 -36
- package/src/superlocalmemory/server/routes/memories.py +104 -43
- package/src/superlocalmemory/server/routes/mesh.py +31 -0
- package/src/superlocalmemory/server/routes/profiles.py +26 -4
- package/src/superlocalmemory/server/routes/tiers.py +43 -11
- package/src/superlocalmemory/server/routes/timeline.py +5 -1
- package/src/superlocalmemory/server/routes/v3_api.py +76 -21
- package/src/superlocalmemory/server/security_middleware.py +1 -1
- package/src/superlocalmemory/server/unified_daemon.py +680 -293
- package/src/superlocalmemory/server/write_identity.py +147 -0
- package/src/superlocalmemory/storage/access_log.py +4 -3
- package/src/superlocalmemory/storage/database.py +118 -25
- package/src/superlocalmemory/storage/migration_runner.py +84 -1
- package/src/superlocalmemory/storage/migration_v33.py +1 -1
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
- package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
- package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
- package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
- package/src/superlocalmemory/storage/models.py +16 -0
- package/src/superlocalmemory/storage/quantized_store.py +20 -3
- package/src/superlocalmemory/storage/v2_migrator.py +5 -3
- package/src/superlocalmemory/ui/favicon.svg +5 -0
- package/src/superlocalmemory/ui/index.html +1 -0
- package/src/superlocalmemory/ui/js/compliance.js +1 -1
- package/src/superlocalmemory/ui/js/core.js +49 -8
- package/src/superlocalmemory/ui/js/dashboard.js +23 -2
- package/src/superlocalmemory/ui/js/feedback.js +1 -1
- package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
- package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
- package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
- package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
- package/src/superlocalmemory/ui/js/settings.js +4 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
- package/bin/slm +0 -59
- package/bin/slm.bat +0 -77
- package/bin/slm.cmd +0 -5
- package/ide/integrations/langchain/README.md +0 -106
- package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
- package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
- package/ide/integrations/langchain/pyproject.toml +0 -38
- package/ide/integrations/langchain/tests/__init__.py +0 -3
- package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
- package/ide/integrations/langchain/tests/test_security.py +0 -117
- package/ide/integrations/llamaindex/README.md +0 -81
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
- package/ide/integrations/llamaindex/pyproject.toml +0 -43
- package/ide/integrations/llamaindex/tests/__init__.py +0 -3
- package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
- package/ide/integrations/llamaindex/tests/test_security.py +0 -241
- package/scripts/__tests__/build-plugin.test.mjs +0 -613
- package/scripts/_savings_math.py +0 -270
- package/scripts/build-dmg.sh +0 -417
- package/scripts/build-plugin.js +0 -742
- package/scripts/build-slm-hook.ps1 +0 -40
- package/scripts/build-slm-hook.sh +0 -45
- package/scripts/build_entry.py +0 -452
- package/scripts/ci/stage5b_gate.sh +0 -50
- package/scripts/dogfood_savings.py +0 -490
- package/scripts/generate-thumbnails.py +0 -218
- package/scripts/install-skills.ps1 +0 -4
- package/scripts/install-skills.sh +0 -5
- package/scripts/install.ps1 +0 -701
- package/scripts/install.sh +0 -1015
- package/scripts/postinstall_binary.js +0 -287
- package/scripts/prepack.js +0 -33
- package/scripts/release_manifest.py +0 -273
- package/scripts/slm-hook.spec +0 -56
- package/scripts/start-dashboard.ps1 +0 -52
- package/scripts/start-dashboard.sh +0 -41
- package/scripts/sync-wiki.ps1 +0 -127
- package/scripts/sync-wiki.sh +0 -82
- package/scripts/test-dmg.sh +0 -161
- package/scripts/test-npm-package.ps1 +0 -252
- package/scripts/test-npm-package.sh +0 -207
- package/scripts/verify-install.ps1 +0 -294
- package/scripts/verify-install.sh +0 -266
- package/scripts/verify-v27.ps1 +0 -301
- package/scripts/verify-v27.sh +0 -233
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -516
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
- package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
- package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
- package/src/superlocalmemory.egg-info/requires.txt +0 -71
- package/src/superlocalmemory.egg-info/top_level.txt +0 -1
package/scripts/build-plugin.js
DELETED
|
@@ -1,742 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* build-plugin.js — SLM v3.6.14 plugin-src single-source build generator
|
|
4
|
-
*
|
|
5
|
-
* DOC-CORRECT layout (verified from official Claude Code plugin docs):
|
|
6
|
-
* .claude-plugin/marketplace.json (repo root; source:"./plugin")
|
|
7
|
-
* plugin/ (PLUGIN ROOT)
|
|
8
|
-
* .claude-plugin/plugin.json (manifest ONLY here; version ONLY here)
|
|
9
|
-
* skills/<7>/SKILL.md
|
|
10
|
-
* agents/<2>.md
|
|
11
|
-
* .mcp.json (SLM_MCP_PROFILE=code)
|
|
12
|
-
* hooks/hooks.json
|
|
13
|
-
* scripts/ensure-venv.sh (+x)
|
|
14
|
-
* settings.json · requirements.txt · CLAUDE.md
|
|
15
|
-
*
|
|
16
|
-
* Node >= 18. Zero external dependencies (only node:fs/path/crypto/process).
|
|
17
|
-
* Exit codes: 0 = success/in-sync 1 = fatal error 2 = --check drift detected
|
|
18
|
-
*
|
|
19
|
-
* Usage:
|
|
20
|
-
* node scripts/build-plugin.js [--check] [--quiet] [--help]
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
'use strict';
|
|
24
|
-
|
|
25
|
-
import fs from 'node:fs';
|
|
26
|
-
import path from 'node:path';
|
|
27
|
-
import crypto from 'node:crypto';
|
|
28
|
-
import process from 'node:process';
|
|
29
|
-
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Constants
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
const VERSION = '3.6.19';
|
|
34
|
-
const MANIFEST_REL = 'plugin-src/manifest.json';
|
|
35
|
-
const GENERATED_BANNER = `# _GENERATED — DO NOT HAND-EDIT
|
|
36
|
-
|
|
37
|
-
This directory is generated by \`scripts/build-plugin.js\` from \`plugin-src/\`.
|
|
38
|
-
Run \`npm run build:plugin\` to regenerate.
|
|
39
|
-
Run \`npm run check:plugin\` (or \`--check\`) in CI to verify in-sync.
|
|
40
|
-
Source of truth: \`plugin-src/skills/\`, \`plugin-src/manifest.json\`.
|
|
41
|
-
`;
|
|
42
|
-
|
|
43
|
-
const _SKIP_DIRS = new Set(['.backup', '.git', 'node_modules', 'build', 'venv', '__pycache__']);
|
|
44
|
-
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// Pure utilities
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Normalize line endings to LF, ensure exactly one trailing newline.
|
|
51
|
-
* @param {string} s
|
|
52
|
-
* @returns {string}
|
|
53
|
-
*/
|
|
54
|
-
export function normalizeNewlines(s) {
|
|
55
|
-
const normalized = s.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
56
|
-
return normalized.replace(/\n*$/, '\n');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Stable JSON serializer (recursive sorted keys, 2-space indent, LF, trailing newline).
|
|
61
|
-
* Prevents the array-replacer bug that silently drops nested keys.
|
|
62
|
-
* @param {unknown} obj
|
|
63
|
-
* @returns {string}
|
|
64
|
-
*/
|
|
65
|
-
export function stableStringify(obj) {
|
|
66
|
-
function sort(val) {
|
|
67
|
-
if (Array.isArray(val)) return val.map(sort);
|
|
68
|
-
if (val !== null && typeof val === 'object') {
|
|
69
|
-
const out = {};
|
|
70
|
-
for (const k of Object.keys(val).sort()) {
|
|
71
|
-
out[k] = sort(val[k]);
|
|
72
|
-
}
|
|
73
|
-
return out;
|
|
74
|
-
}
|
|
75
|
-
return val;
|
|
76
|
-
}
|
|
77
|
-
return JSON.stringify(sort(obj), null, 2) + '\n';
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Replace the frontmatter `version:` line with the given version string.
|
|
82
|
-
* ONLY touches the YAML frontmatter block (between leading `---`).
|
|
83
|
-
* Throws if no version line found in frontmatter.
|
|
84
|
-
* @param {string} md
|
|
85
|
-
* @param {string} ver
|
|
86
|
-
* @returns {string}
|
|
87
|
-
*/
|
|
88
|
-
export function stampVersion(md, ver) {
|
|
89
|
-
// Match frontmatter: opening ---, content, closing ---
|
|
90
|
-
const fmMatch = md.match(/^(---\n)([\s\S]*?)(^---\n)/m);
|
|
91
|
-
if (!fmMatch) {
|
|
92
|
-
throw new Error('stampVersion: no YAML frontmatter found');
|
|
93
|
-
}
|
|
94
|
-
const [fullMatch, open, body, close] = fmMatch;
|
|
95
|
-
if (!/^version:/m.test(body)) {
|
|
96
|
-
throw new Error('stampVersion: no version: line in frontmatter');
|
|
97
|
-
}
|
|
98
|
-
const newBody = body.replace(/^version:.*$/m, `version: "${ver}"`);
|
|
99
|
-
const newFm = open + newBody + close;
|
|
100
|
-
return md.replace(fullMatch, newFm);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* OQ-2 attribution normalization — ANCHORED to attribution footer patterns only.
|
|
105
|
-
* Does NOT rewrite prose like "API v2" or "the MIT license".
|
|
106
|
-
*
|
|
107
|
-
* Patterns fixed (in footer block):
|
|
108
|
-
* **Project:** SuperLocalMemory V2 => **Project:** SuperLocalMemory v3.6.14
|
|
109
|
-
* **License:** MIT (see ...) => **License:** AGPL-3.0-or-later
|
|
110
|
-
* **Repository:** https://github.com/varun369/SuperLocalMemoryV2
|
|
111
|
-
* => **Repository:** https://github.com/qualixar/superlocalmemory
|
|
112
|
-
*
|
|
113
|
-
* Also fixes the corrupted L87 table row in slm-recall.
|
|
114
|
-
* @param {string} md
|
|
115
|
-
* @param {string} ver
|
|
116
|
-
* @returns {string}
|
|
117
|
-
*/
|
|
118
|
-
function normalizeAttribution(md, ver) {
|
|
119
|
-
let out = md;
|
|
120
|
-
|
|
121
|
-
// Fix attribution footer lines (prefixed with **Project:**, **License:**, **Repository:**)
|
|
122
|
-
// These are anchored to line-start with **...: patterns.
|
|
123
|
-
out = out.replace(
|
|
124
|
-
/^(\*\*Project:\*\*\s+SuperLocalMemory\s+)V2/mg,
|
|
125
|
-
`$1v${ver}`
|
|
126
|
-
);
|
|
127
|
-
out = out.replace(
|
|
128
|
-
/^(\*\*License:\*\*\s+)MIT(\s*\(see[^)]*\))?/mg,
|
|
129
|
-
`$1AGPL-3.0-or-later`
|
|
130
|
-
);
|
|
131
|
-
out = out.replace(
|
|
132
|
-
/^(\*\*Repository:\*\*\s+)https:\/\/github\.com\/varun369\/SuperLocalMemoryV2/mg,
|
|
133
|
-
`$1https://github.com/qualixar/superlocalmemory`
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
// Fix corrupted L87 table row in slm-recall
|
|
137
|
-
// Original: | `--min-score` | float | No | 0.3 | Minimum relevance (0.0-1-tags` | string | No | None | Filter by tags (comma-separated) |
|
|
138
|
-
// Fix: split into two correct rows
|
|
139
|
-
out = out.replace(
|
|
140
|
-
/\| `--min-score` \| float \| No \| 0\.3 \| Minimum relevance \(0\.0-1-tags` \| string \| No \| None \| Filter by tags \(comma-separated\) \|/g,
|
|
141
|
-
'| `--min-score` | float | No | 0.3 | Minimum relevance (0.0-1.0) |\n| `--tags` | string | No | None | Filter by tags (comma-separated) |'
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
return out;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Full skill file rendering: normalizeNewlines → stampVersion → normalizeAttribution → normalizeNewlines.
|
|
149
|
-
* Skills without a `version:` line in frontmatter are copied verbatim (no stamp).
|
|
150
|
-
* @param {string} src raw source content
|
|
151
|
-
* @param {string} ver version to stamp
|
|
152
|
-
* @returns {string}
|
|
153
|
-
*/
|
|
154
|
-
export function renderSkillFile(src, ver) {
|
|
155
|
-
let out = normalizeNewlines(src);
|
|
156
|
-
// Only stamp if there is a version: line in frontmatter — new skills may not have one
|
|
157
|
-
const fmMatch = out.match(/^(---\n)([\s\S]*?)(^---\n)/m);
|
|
158
|
-
if (fmMatch && /^version:/m.test(fmMatch[2])) {
|
|
159
|
-
out = stampVersion(out, ver);
|
|
160
|
-
}
|
|
161
|
-
out = normalizeAttribution(out, ver);
|
|
162
|
-
out = normalizeNewlines(out);
|
|
163
|
-
return out;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Render plugin.json — full §5 schema. version ONLY here.
|
|
168
|
-
* Paths are relative to the plugin root (= plugin/).
|
|
169
|
-
* Uses stableStringify — no nested key dropping.
|
|
170
|
-
* @param {object} manifest
|
|
171
|
-
* @returns {string}
|
|
172
|
-
*/
|
|
173
|
-
export function renderPluginJson(manifest) {
|
|
174
|
-
const obj = {
|
|
175
|
-
author: {
|
|
176
|
-
name: 'Qualixar',
|
|
177
|
-
url: manifest.repository || 'https://github.com/qualixar/superlocalmemory',
|
|
178
|
-
},
|
|
179
|
-
description: manifest.marketplace.description,
|
|
180
|
-
displayName: manifest.displayName || manifest.pluginName,
|
|
181
|
-
hooks: './.mcp.json' === manifest.targets.plugin ? './hooks/hooks.json' : './hooks/hooks.json',
|
|
182
|
-
keywords: manifest.keywords || [],
|
|
183
|
-
mcpServers: './.mcp.json',
|
|
184
|
-
name: manifest.pluginName,
|
|
185
|
-
repository: manifest.repository || '',
|
|
186
|
-
version: manifest.version,
|
|
187
|
-
};
|
|
188
|
-
// hooks pointer is always ./hooks/hooks.json relative to plugin root
|
|
189
|
-
obj.hooks = './hooks/hooks.json';
|
|
190
|
-
obj.mcpServers = './.mcp.json';
|
|
191
|
-
return stableStringify(obj);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Render marketplace.json — at repo root .claude-plugin/marketplace.json.
|
|
196
|
-
* NO version key in plugin entry. source = "./plugin".
|
|
197
|
-
* @param {object} manifest
|
|
198
|
-
* @returns {string}
|
|
199
|
-
*/
|
|
200
|
-
export function renderMarketplaceJson(manifest) {
|
|
201
|
-
const ownerObj = { name: 'Qualixar' };
|
|
202
|
-
if (manifest.marketplace.ownerEmail) {
|
|
203
|
-
ownerObj.email = manifest.marketplace.ownerEmail;
|
|
204
|
-
}
|
|
205
|
-
const obj = {
|
|
206
|
-
name: 'qualixar',
|
|
207
|
-
owner: ownerObj,
|
|
208
|
-
plugins: [
|
|
209
|
-
{
|
|
210
|
-
author: { name: 'Qualixar' },
|
|
211
|
-
description: manifest.marketplace.description,
|
|
212
|
-
name: manifest.pluginName,
|
|
213
|
-
source: './plugin',
|
|
214
|
-
},
|
|
215
|
-
],
|
|
216
|
-
};
|
|
217
|
-
return stableStringify(obj);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ---------------------------------------------------------------------------
|
|
221
|
-
// IO: loadManifest
|
|
222
|
-
// ---------------------------------------------------------------------------
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Load and validate plugin-src/manifest.json.
|
|
226
|
-
* Throws (→ exit 1) on any IO or validation error.
|
|
227
|
-
* @param {string} root repository root
|
|
228
|
-
* @returns {object} parsed manifest
|
|
229
|
-
*/
|
|
230
|
-
export function loadManifest(root) {
|
|
231
|
-
const p = path.join(root, MANIFEST_REL);
|
|
232
|
-
let raw;
|
|
233
|
-
try {
|
|
234
|
-
raw = fs.readFileSync(p, 'utf8');
|
|
235
|
-
} catch (err) {
|
|
236
|
-
throw new Error(`loadManifest: cannot read ${p}: ${err.message}`);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
let manifest;
|
|
240
|
-
try {
|
|
241
|
-
manifest = JSON.parse(raw);
|
|
242
|
-
} catch (err) {
|
|
243
|
-
throw new Error(`loadManifest: invalid JSON in ${p}: ${err.message}`);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Validate required fields
|
|
247
|
-
if (!manifest.version) throw new Error('loadManifest: manifest.version is required');
|
|
248
|
-
if (!manifest.pluginName) throw new Error('loadManifest: manifest.pluginName is required');
|
|
249
|
-
if (!Array.isArray(manifest.skills) || manifest.skills.length === 0) {
|
|
250
|
-
throw new Error('loadManifest: manifest.skills must be a non-empty array');
|
|
251
|
-
}
|
|
252
|
-
if (!manifest.targets || typeof manifest.targets !== 'object') {
|
|
253
|
-
throw new Error('loadManifest: manifest.targets must be an object');
|
|
254
|
-
}
|
|
255
|
-
if (!manifest.targets.plugin || typeof manifest.targets.plugin !== 'string') {
|
|
256
|
-
throw new Error('loadManifest: manifest.targets.plugin is required and must be a string');
|
|
257
|
-
}
|
|
258
|
-
if (path.isAbsolute(manifest.targets.plugin)) {
|
|
259
|
-
throw new Error('loadManifest: manifest.targets.plugin must be relative (absolute paths forbidden)');
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return manifest;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// ---------------------------------------------------------------------------
|
|
266
|
-
// Core: derivePluginRoot — the plugin/ subdir from targets.plugin
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Derive the plugin root directory (plugin/) from manifest.targets.plugin.
|
|
271
|
-
* targets.plugin = "plugin/.claude-plugin/plugin.json"
|
|
272
|
-
* pluginRoot = path.resolve(root, "plugin")
|
|
273
|
-
* @param {string} root
|
|
274
|
-
* @param {object} manifest
|
|
275
|
-
* @returns {string} absolute path to plugin root
|
|
276
|
-
*/
|
|
277
|
-
export function derivePluginRoot(root, manifest) {
|
|
278
|
-
// targets.plugin = "plugin/.claude-plugin/plugin.json"
|
|
279
|
-
// dirname twice: "plugin/.claude-plugin" -> "plugin"
|
|
280
|
-
const pluginJsonRel = manifest.targets.plugin; // "plugin/.claude-plugin/plugin.json"
|
|
281
|
-
const pluginClaudeDir = path.dirname(pluginJsonRel); // "plugin/.claude-plugin"
|
|
282
|
-
const pluginRootRel = path.dirname(pluginClaudeDir); // "plugin"
|
|
283
|
-
return path.resolve(root, pluginRootRel);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Return the set of absolute managed root directories.
|
|
288
|
-
* Now only manages plugin/ dir.
|
|
289
|
-
* @param {string} root
|
|
290
|
-
* @param {object} manifest
|
|
291
|
-
* @returns {Set<string>}
|
|
292
|
-
*/
|
|
293
|
-
export function deriveManagedRoots(root, manifest) {
|
|
294
|
-
const roots = new Set();
|
|
295
|
-
roots.add(derivePluginRoot(root, manifest));
|
|
296
|
-
return roots;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// ---------------------------------------------------------------------------
|
|
300
|
-
// Core: buildPlan — pure, returns Map<absPath, content>
|
|
301
|
-
// ---------------------------------------------------------------------------
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Build the full plan as a Map from absolute path to file content.
|
|
305
|
-
* Pure: no IO side effects (reads plugin-src source files).
|
|
306
|
-
* @param {string} root repository root
|
|
307
|
-
* @param {object} manifest parsed manifest
|
|
308
|
-
* @returns {Map<string, string>}
|
|
309
|
-
*/
|
|
310
|
-
export function buildPlan(root, manifest) {
|
|
311
|
-
const plan = new Map();
|
|
312
|
-
|
|
313
|
-
const pluginRoot = derivePluginRoot(root, manifest);
|
|
314
|
-
const pluginClaudeDir = path.join(pluginRoot, '.claude-plugin');
|
|
315
|
-
|
|
316
|
-
const sortedSkills = [...manifest.skills].sort((a, b) => a.name.localeCompare(b.name));
|
|
317
|
-
|
|
318
|
-
// ---------------------------------------------------------------------------
|
|
319
|
-
// Skills → plugin/skills/<name>/SKILL.md
|
|
320
|
-
// ---------------------------------------------------------------------------
|
|
321
|
-
for (const skill of sortedSkills) {
|
|
322
|
-
const srcSkillDir = path.join(root, 'plugin-src', 'skills', skill.name);
|
|
323
|
-
const srcSkillPath = path.join(srcSkillDir, 'SKILL.md');
|
|
324
|
-
|
|
325
|
-
let srcContent;
|
|
326
|
-
try {
|
|
327
|
-
srcContent = fs.readFileSync(srcSkillPath, 'utf8');
|
|
328
|
-
} catch (err) {
|
|
329
|
-
throw new Error(`buildPlan: cannot read source skill ${srcSkillPath}: ${err.message}`);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const rendered = renderSkillFile(srcContent, manifest.version);
|
|
333
|
-
plan.set(path.join(pluginRoot, 'skills', skill.name, 'SKILL.md'), rendered);
|
|
334
|
-
|
|
335
|
-
// README verbatim (no stamp) if present
|
|
336
|
-
if (skill.hasReadme) {
|
|
337
|
-
const srcReadme = path.join(srcSkillDir, 'README.md');
|
|
338
|
-
let readmeContent;
|
|
339
|
-
try {
|
|
340
|
-
readmeContent = fs.readFileSync(srcReadme, 'utf8');
|
|
341
|
-
} catch (err) {
|
|
342
|
-
throw new Error(`buildPlan: cannot read README ${srcReadme}: ${err.message}`);
|
|
343
|
-
}
|
|
344
|
-
plan.set(path.join(pluginRoot, 'skills', skill.name, 'README.md'), readmeContent);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// ---------------------------------------------------------------------------
|
|
349
|
-
// _GENERATED.md banner in plugin root
|
|
350
|
-
// ---------------------------------------------------------------------------
|
|
351
|
-
plan.set(path.join(pluginRoot, '_GENERATED.md'), GENERATED_BANNER);
|
|
352
|
-
|
|
353
|
-
// ---------------------------------------------------------------------------
|
|
354
|
-
// plugin/.claude-plugin/plugin.json
|
|
355
|
-
// ---------------------------------------------------------------------------
|
|
356
|
-
plan.set(path.join(pluginClaudeDir, 'plugin.json'), renderPluginJson(manifest));
|
|
357
|
-
|
|
358
|
-
// ---------------------------------------------------------------------------
|
|
359
|
-
// .claude-plugin/marketplace.json at REPO ROOT (not plugin root)
|
|
360
|
-
// ---------------------------------------------------------------------------
|
|
361
|
-
plan.set(path.join(root, '.claude-plugin', 'marketplace.json'), renderMarketplaceJson(manifest));
|
|
362
|
-
|
|
363
|
-
// ---------------------------------------------------------------------------
|
|
364
|
-
// Agents (verbatim, no commands/)
|
|
365
|
-
// ---------------------------------------------------------------------------
|
|
366
|
-
const agentsSrcPath = path.join(root, 'plugin-src', 'agents');
|
|
367
|
-
if (fs.existsSync(agentsSrcPath)) {
|
|
368
|
-
for (const fname of fs.readdirSync(agentsSrcPath)) {
|
|
369
|
-
if (fname === '.gitkeep') continue;
|
|
370
|
-
const srcFile = path.join(agentsSrcPath, fname);
|
|
371
|
-
if (!fs.statSync(srcFile).isFile()) continue;
|
|
372
|
-
const content = fs.readFileSync(srcFile, 'utf8');
|
|
373
|
-
plan.set(path.join(pluginRoot, 'agents', fname), content);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// ---------------------------------------------------------------------------
|
|
378
|
-
// hooks: plugin/hooks/
|
|
379
|
-
// ---------------------------------------------------------------------------
|
|
380
|
-
const hooksPath = path.join(root, 'plugin-src', 'hooks');
|
|
381
|
-
if (fs.existsSync(hooksPath)) {
|
|
382
|
-
for (const fname of fs.readdirSync(hooksPath)) {
|
|
383
|
-
if (fname === '.gitkeep') continue;
|
|
384
|
-
const srcFile = path.join(hooksPath, fname);
|
|
385
|
-
if (!fs.statSync(srcFile).isFile()) continue;
|
|
386
|
-
const content = fs.readFileSync(srcFile, 'utf8');
|
|
387
|
-
plan.set(path.join(pluginRoot, 'hooks', fname), content);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// ---------------------------------------------------------------------------
|
|
392
|
-
// Root-level plugin-src files → plugin/ (.mcp.json, settings.json, requirements.txt)
|
|
393
|
-
// ---------------------------------------------------------------------------
|
|
394
|
-
const rootFiles = ['.mcp.json', 'settings.json', 'requirements.txt'];
|
|
395
|
-
for (const fname of rootFiles) {
|
|
396
|
-
const srcFile = path.join(root, 'plugin-src', fname);
|
|
397
|
-
if (fs.existsSync(srcFile)) {
|
|
398
|
-
const content = fs.readFileSync(srcFile, 'utf8');
|
|
399
|
-
plan.set(path.join(pluginRoot, fname), content);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// ---------------------------------------------------------------------------
|
|
404
|
-
// scripts/ → plugin/scripts/
|
|
405
|
-
// ---------------------------------------------------------------------------
|
|
406
|
-
const scriptsSrcPath = path.join(root, 'plugin-src', 'scripts');
|
|
407
|
-
if (fs.existsSync(scriptsSrcPath)) {
|
|
408
|
-
for (const fname of fs.readdirSync(scriptsSrcPath)) {
|
|
409
|
-
if (fname === '.gitkeep') continue;
|
|
410
|
-
const srcFile = path.join(scriptsSrcPath, fname);
|
|
411
|
-
if (!fs.statSync(srcFile).isFile()) continue;
|
|
412
|
-
const content = fs.readFileSync(srcFile, 'utf8');
|
|
413
|
-
plan.set(path.join(pluginRoot, 'scripts', fname), content);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// ---------------------------------------------------------------------------
|
|
418
|
-
// CLAUDE.md from rules/CLAUDE.md.fragment → plugin/CLAUDE.md
|
|
419
|
-
// ---------------------------------------------------------------------------
|
|
420
|
-
const claudeFragmentPath = path.join(root, 'plugin-src', 'rules', 'CLAUDE.md.fragment');
|
|
421
|
-
if (fs.existsSync(claudeFragmentPath)) {
|
|
422
|
-
const fragmentContent = fs.readFileSync(claudeFragmentPath, 'utf8');
|
|
423
|
-
plan.set(path.join(pluginRoot, 'CLAUDE.md'), fragmentContent);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return plan;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// ---------------------------------------------------------------------------
|
|
430
|
-
// Pure utility: _isExecutable — determines if a file should have +x bit set.
|
|
431
|
-
// Shell scripts (.sh) and extensionless launcher scripts (e.g. slm-launch)
|
|
432
|
-
// in scripts/ directories need the executable bit.
|
|
433
|
-
// ---------------------------------------------------------------------------
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Return true if the file at absPath should receive the executable (+x) bit.
|
|
437
|
-
* Rules:
|
|
438
|
-
* - Any file ending in .sh
|
|
439
|
-
* - Any extensionless file inside a scripts/ directory (cross-platform launchers)
|
|
440
|
-
* @param {string} absPath
|
|
441
|
-
* @returns {boolean}
|
|
442
|
-
*/
|
|
443
|
-
function _isExecutable(absPath) {
|
|
444
|
-
if (absPath.endsWith('.sh')) return true;
|
|
445
|
-
// Extensionless files in a scripts/ directory (e.g. slm-launch)
|
|
446
|
-
const base = path.basename(absPath);
|
|
447
|
-
if (!base.includes('.') && absPath.includes(`${path.sep}scripts${path.sep}`)) {
|
|
448
|
-
return true;
|
|
449
|
-
}
|
|
450
|
-
return false;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// ---------------------------------------------------------------------------
|
|
454
|
-
// IO: applyPlan — write-only-if-changed via sha256
|
|
455
|
-
// ---------------------------------------------------------------------------
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Apply the plan: write files only when content has changed (by sha256).
|
|
459
|
-
* Creates parent directories as needed.
|
|
460
|
-
* IMPORTANT: If the target path is a symlink, unlink it first before writing.
|
|
461
|
-
* This prevents writes from propagating through symlinks to legacy source files.
|
|
462
|
-
* @param {Map<string, string>} plan
|
|
463
|
-
* @returns {number} number of files written
|
|
464
|
-
*/
|
|
465
|
-
export function applyPlan(plan) {
|
|
466
|
-
let written = 0;
|
|
467
|
-
for (const [absPath, content] of plan) {
|
|
468
|
-
const newHash = sha256(content);
|
|
469
|
-
let existingHash = null;
|
|
470
|
-
|
|
471
|
-
// Check using lstat (not stat) to detect symlinks without following them
|
|
472
|
-
if (fs.existsSync(absPath)) {
|
|
473
|
-
const stat = fs.lstatSync(absPath);
|
|
474
|
-
if (stat.isSymbolicLink()) {
|
|
475
|
-
// Unlink the symlink before writing — prevents write-through to legacy source
|
|
476
|
-
fs.unlinkSync(absPath);
|
|
477
|
-
// existingHash stays null → will write
|
|
478
|
-
} else {
|
|
479
|
-
existingHash = sha256(fs.readFileSync(absPath, 'utf8'));
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (existingHash !== newHash) {
|
|
484
|
-
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
485
|
-
fs.writeFileSync(absPath, content, 'utf8');
|
|
486
|
-
// Preserve executable bit for shell scripts and extensionless launchers
|
|
487
|
-
if (_isExecutable(absPath)) {
|
|
488
|
-
const mode = fs.statSync(absPath).mode;
|
|
489
|
-
fs.chmodSync(absPath, mode | 0o111);
|
|
490
|
-
}
|
|
491
|
-
written++;
|
|
492
|
-
} else if (_isExecutable(absPath)) {
|
|
493
|
-
// Ensure executable bit even if content unchanged
|
|
494
|
-
const mode = fs.statSync(absPath).mode;
|
|
495
|
-
if (!(mode & 0o100)) {
|
|
496
|
-
fs.chmodSync(absPath, mode | 0o111);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
return written;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function sha256(content) {
|
|
504
|
-
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// ---------------------------------------------------------------------------
|
|
508
|
-
// IO: checkPlan — compare plan vs disk, return { stale, missing, extra }
|
|
509
|
-
// ---------------------------------------------------------------------------
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Compare plan against disk. Returns arrays of stale/missing/extra paths.
|
|
513
|
-
* @param {Map<string, string>} plan
|
|
514
|
-
* @param {string} root
|
|
515
|
-
* @param {object} manifest
|
|
516
|
-
* @returns {{ stale: string[], missing: string[], extra: string[] }}
|
|
517
|
-
*/
|
|
518
|
-
export function checkPlan(plan, root, manifest) {
|
|
519
|
-
const stale = [];
|
|
520
|
-
const missing = [];
|
|
521
|
-
const extra = [];
|
|
522
|
-
|
|
523
|
-
for (const [absPath, content] of plan) {
|
|
524
|
-
if (!fs.existsSync(absPath)) {
|
|
525
|
-
missing.push(absPath);
|
|
526
|
-
} else {
|
|
527
|
-
const disk = sha256(fs.readFileSync(absPath, 'utf8'));
|
|
528
|
-
const expected = sha256(content);
|
|
529
|
-
if (disk !== expected) stale.push(absPath);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Find extra files in managed roots (only plugin/)
|
|
534
|
-
const managedRoots = deriveManagedRoots(root, manifest);
|
|
535
|
-
for (const managedRoot of managedRoots) {
|
|
536
|
-
if (!fs.existsSync(managedRoot)) continue;
|
|
537
|
-
for (const absPath of walkDir(managedRoot)) {
|
|
538
|
-
if (!plan.has(absPath)) {
|
|
539
|
-
extra.push(absPath);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
return { stale, missing, extra };
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// ---------------------------------------------------------------------------
|
|
548
|
-
// IO: pruneOrphans — GUARDED: plan.size>0, path in managed root
|
|
549
|
-
// ---------------------------------------------------------------------------
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Remove files in managed roots that are NOT in the plan (orphan pruning).
|
|
553
|
-
* Guards:
|
|
554
|
-
* 1. plan.size > 0 (CRIT-1: refuse if plan empty)
|
|
555
|
-
* 2. fullPath must be exactly managedRoot or start with managedRoot + sep (escape guard)
|
|
556
|
-
* 3. Only files (not dirs) removed; dirs cleaned up if empty
|
|
557
|
-
* @param {string} root
|
|
558
|
-
* @param {Map<string, string>} plan
|
|
559
|
-
* @param {Set<string>} managedRoots
|
|
560
|
-
*/
|
|
561
|
-
export function pruneOrphans(root, plan, managedRoots) {
|
|
562
|
-
// CRIT-1: refuse if plan is empty
|
|
563
|
-
if (plan.size === 0) {
|
|
564
|
-
throw new Error('pruneOrphans: refusing to prune — plan is empty (safety guard)');
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
for (const managedRoot of managedRoots) {
|
|
568
|
-
if (!fs.existsSync(managedRoot)) continue;
|
|
569
|
-
for (const absPath of walkDir(managedRoot)) {
|
|
570
|
-
// Escape guard: path must be within managedRoot
|
|
571
|
-
if (
|
|
572
|
-
absPath !== managedRoot &&
|
|
573
|
-
!absPath.startsWith(managedRoot + path.sep)
|
|
574
|
-
) {
|
|
575
|
-
throw new Error(
|
|
576
|
-
`pruneOrphans: escape guard — ${absPath} is not under ${managedRoot}`
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
if (!plan.has(absPath)) {
|
|
581
|
-
fs.unlinkSync(absPath);
|
|
582
|
-
// Remove empty parent dirs (up to managedRoot)
|
|
583
|
-
cleanEmptyDirs(path.dirname(absPath), managedRoot);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Walk a directory recursively, skipping _SKIP_DIRS.
|
|
591
|
-
* Yields absolute paths of FILES only.
|
|
592
|
-
* @param {string} dir
|
|
593
|
-
* @returns {Generator<string>}
|
|
594
|
-
*/
|
|
595
|
-
export function* walkDir(dir) {
|
|
596
|
-
if (!fs.existsSync(dir)) return;
|
|
597
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
598
|
-
for (const entry of entries) {
|
|
599
|
-
if (_SKIP_DIRS.has(entry.name)) continue;
|
|
600
|
-
const full = path.join(dir, entry.name);
|
|
601
|
-
if (entry.isDirectory()) {
|
|
602
|
-
yield* walkDir(full);
|
|
603
|
-
} else if (entry.isFile()) {
|
|
604
|
-
yield full;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
function cleanEmptyDirs(dir, stopAt) {
|
|
610
|
-
if (dir === stopAt || !dir.startsWith(stopAt + path.sep)) return;
|
|
611
|
-
try {
|
|
612
|
-
const entries = fs.readdirSync(dir);
|
|
613
|
-
if (entries.length === 0) {
|
|
614
|
-
fs.rmdirSync(dir);
|
|
615
|
-
cleanEmptyDirs(path.dirname(dir), stopAt);
|
|
616
|
-
}
|
|
617
|
-
} catch {
|
|
618
|
-
// ignore — dir may not exist
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// ---------------------------------------------------------------------------
|
|
623
|
-
// IO: listTargetFiles — for --check extra detection
|
|
624
|
-
// ---------------------------------------------------------------------------
|
|
625
|
-
|
|
626
|
-
/**
|
|
627
|
-
* List all files in managed roots.
|
|
628
|
-
* @param {string} root
|
|
629
|
-
* @param {object} manifest
|
|
630
|
-
* @returns {string[]}
|
|
631
|
-
*/
|
|
632
|
-
export function listTargetFiles(root, manifest) {
|
|
633
|
-
const result = [];
|
|
634
|
-
for (const managedRoot of deriveManagedRoots(root, manifest)) {
|
|
635
|
-
for (const f of walkDir(managedRoot)) {
|
|
636
|
-
result.push(f);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
return result;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// ---------------------------------------------------------------------------
|
|
643
|
-
// CLI: main
|
|
644
|
-
// ---------------------------------------------------------------------------
|
|
645
|
-
|
|
646
|
-
/**
|
|
647
|
-
* Main entry point.
|
|
648
|
-
* @param {string[]} argv
|
|
649
|
-
*/
|
|
650
|
-
export function main(argv) {
|
|
651
|
-
const args = argv.slice(2);
|
|
652
|
-
|
|
653
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
654
|
-
console.log(`
|
|
655
|
-
Usage: node scripts/build-plugin.js [options]
|
|
656
|
-
|
|
657
|
-
Options:
|
|
658
|
-
--check Check if generated files are in-sync (exit 2 if not)
|
|
659
|
-
--quiet Suppress non-error output
|
|
660
|
-
--help, -h Show this help
|
|
661
|
-
|
|
662
|
-
Output topology:
|
|
663
|
-
.claude-plugin/marketplace.json (repo root; source:"./plugin")
|
|
664
|
-
plugin/ (plugin root)
|
|
665
|
-
.claude-plugin/plugin.json (manifest; version only here)
|
|
666
|
-
skills/<7>/SKILL.md
|
|
667
|
-
agents/*.md
|
|
668
|
-
.mcp.json hooks/ scripts/ settings.json requirements.txt CLAUDE.md
|
|
669
|
-
|
|
670
|
-
Exit codes: 0=success/in-sync 1=fatal 2=check-drift
|
|
671
|
-
`.trimStart());
|
|
672
|
-
process.exit(0);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
const isCheck = args.includes('--check');
|
|
676
|
-
const isQuiet = args.includes('--quiet');
|
|
677
|
-
|
|
678
|
-
const root = process.cwd();
|
|
679
|
-
|
|
680
|
-
let manifest;
|
|
681
|
-
try {
|
|
682
|
-
manifest = loadManifest(root);
|
|
683
|
-
} catch (err) {
|
|
684
|
-
console.error(`build-plugin: ${err.message}`);
|
|
685
|
-
process.exit(1);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
let plan;
|
|
689
|
-
try {
|
|
690
|
-
plan = buildPlan(root, manifest);
|
|
691
|
-
} catch (err) {
|
|
692
|
-
console.error(`build-plugin: ${err.message}`);
|
|
693
|
-
process.exit(1);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
if (isCheck) {
|
|
697
|
-
// Never prune in --check mode
|
|
698
|
-
const { stale, missing, extra } = checkPlan(plan, root, manifest);
|
|
699
|
-
if (stale.length > 0 || missing.length > 0 || extra.length > 0) {
|
|
700
|
-
if (!isQuiet) {
|
|
701
|
-
if (stale.length) console.log('STALE:\n' + stale.map(f => ` ${f}`).join('\n'));
|
|
702
|
-
if (missing.length) console.log('MISSING:\n' + missing.map(f => ` ${f}`).join('\n'));
|
|
703
|
-
if (extra.length) console.log('EXTRA:\n' + extra.map(f => ` ${f}`).join('\n'));
|
|
704
|
-
}
|
|
705
|
-
process.exit(2);
|
|
706
|
-
}
|
|
707
|
-
if (!isQuiet) console.log('build-plugin --check: in-sync');
|
|
708
|
-
process.exit(0);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// Build mode
|
|
712
|
-
let written;
|
|
713
|
-
try {
|
|
714
|
-
written = applyPlan(plan);
|
|
715
|
-
} catch (err) {
|
|
716
|
-
console.error(`build-plugin: applyPlan failed: ${err.message}`);
|
|
717
|
-
process.exit(1);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// Prune orphans in plugin/ (CRIT-1: guarded inside pruneOrphans)
|
|
721
|
-
try {
|
|
722
|
-
const managedRoots = deriveManagedRoots(root, manifest);
|
|
723
|
-
pruneOrphans(root, plan, managedRoots);
|
|
724
|
-
} catch (err) {
|
|
725
|
-
console.error(`build-plugin: pruneOrphans failed: ${err.message}`);
|
|
726
|
-
process.exit(1);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
if (!isQuiet) {
|
|
730
|
-
console.log(`build-plugin: done (${written} file(s) written)`);
|
|
731
|
-
}
|
|
732
|
-
process.exit(0);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// ---------------------------------------------------------------------------
|
|
736
|
-
// Entry point
|
|
737
|
-
// ---------------------------------------------------------------------------
|
|
738
|
-
// Check if this is the main module (ESM way)
|
|
739
|
-
const isMain = process.argv[1] === new URL(import.meta.url).pathname;
|
|
740
|
-
if (isMain) {
|
|
741
|
-
main(process.argv);
|
|
742
|
-
}
|