superlocalmemory 3.4.19 → 3.4.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +42 -34
  3. package/bin/slm +11 -0
  4. package/bin/slm.bat +12 -0
  5. package/package.json +4 -3
  6. package/pyproject.toml +4 -3
  7. package/scripts/build-slm-hook.ps1 +40 -0
  8. package/scripts/build-slm-hook.sh +45 -0
  9. package/scripts/build_entry.py +452 -0
  10. package/scripts/ci/stage5b_gate.sh +50 -0
  11. package/scripts/postinstall/validation.js +187 -0
  12. package/scripts/postinstall-interactive.js +756 -0
  13. package/scripts/postinstall_binary.js +287 -0
  14. package/scripts/release_manifest.py +273 -0
  15. package/scripts/slm-hook.spec +56 -0
  16. package/skills/slm-build-graph/SKILL.md +423 -0
  17. package/skills/slm-list-recent/SKILL.md +348 -0
  18. package/skills/slm-recall/SKILL.md +343 -0
  19. package/skills/slm-remember/SKILL.md +194 -0
  20. package/skills/slm-show-patterns/SKILL.md +224 -0
  21. package/skills/slm-status/SKILL.md +363 -0
  22. package/skills/slm-switch-profile/SKILL.md +442 -0
  23. package/src/superlocalmemory/cli/commands.py +254 -79
  24. package/src/superlocalmemory/cli/context_commands.py +192 -0
  25. package/src/superlocalmemory/cli/daemon.py +15 -1
  26. package/src/superlocalmemory/cli/db_migrate.py +80 -0
  27. package/src/superlocalmemory/cli/escape_hatch.py +220 -0
  28. package/src/superlocalmemory/cli/main.py +72 -1
  29. package/src/superlocalmemory/core/context_cache.py +397 -0
  30. package/src/superlocalmemory/core/engine.py +38 -2
  31. package/src/superlocalmemory/core/engine_wiring.py +1 -1
  32. package/src/superlocalmemory/core/ram_lock.py +111 -0
  33. package/src/superlocalmemory/core/recall_pipeline.py +433 -3
  34. package/src/superlocalmemory/core/recall_worker.py +8 -3
  35. package/src/superlocalmemory/core/security_primitives.py +635 -0
  36. package/src/superlocalmemory/core/shadow_router.py +319 -0
  37. package/src/superlocalmemory/core/slm_disabled.py +87 -0
  38. package/src/superlocalmemory/core/slmignore.py +125 -0
  39. package/src/superlocalmemory/core/topic_signature.py +143 -0
  40. package/src/superlocalmemory/core/worker_pool.py +14 -3
  41. package/src/superlocalmemory/encoding/cognitive_consolidator.py +2 -2
  42. package/src/superlocalmemory/evolution/budget.py +321 -0
  43. package/src/superlocalmemory/evolution/llm_dispatch.py +508 -0
  44. package/src/superlocalmemory/evolution/skill_evolver.py +144 -94
  45. package/src/superlocalmemory/hooks/_outcome_common.py +506 -0
  46. package/src/superlocalmemory/hooks/adapter_base.py +317 -0
  47. package/src/superlocalmemory/hooks/antigravity_adapter.py +192 -0
  48. package/src/superlocalmemory/hooks/claude_code_hooks.py +33 -1
  49. package/src/superlocalmemory/hooks/context_payload.py +312 -0
  50. package/src/superlocalmemory/hooks/copilot_adapter.py +154 -0
  51. package/src/superlocalmemory/hooks/cross_platform_connector.py +90 -0
  52. package/src/superlocalmemory/hooks/cursor_adapter.py +195 -0
  53. package/src/superlocalmemory/hooks/hook_handlers.py +109 -8
  54. package/src/superlocalmemory/hooks/ide_connector.py +25 -2
  55. package/src/superlocalmemory/hooks/post_tool_async_hook.py +165 -0
  56. package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +223 -0
  57. package/src/superlocalmemory/hooks/prewarm_auth.py +170 -0
  58. package/src/superlocalmemory/hooks/session_registry.py +186 -0
  59. package/src/superlocalmemory/hooks/stop_outcome_hook.py +134 -0
  60. package/src/superlocalmemory/hooks/sync_loop.py +114 -0
  61. package/src/superlocalmemory/hooks/user_prompt_hook.py +128 -0
  62. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +202 -0
  63. package/src/superlocalmemory/infra/backup.py +3 -3
  64. package/src/superlocalmemory/infra/cloud_backup.py +2 -2
  65. package/src/superlocalmemory/infra/event_bus.py +2 -2
  66. package/src/superlocalmemory/infra/webhook_dispatcher.py +3 -3
  67. package/src/superlocalmemory/learning/arm_catalog.py +99 -0
  68. package/src/superlocalmemory/learning/bandit.py +526 -0
  69. package/src/superlocalmemory/learning/bandit_cache.py +133 -0
  70. package/src/superlocalmemory/learning/behavioral.py +53 -1
  71. package/src/superlocalmemory/learning/consolidation_cycle.py +381 -0
  72. package/src/superlocalmemory/learning/consolidation_worker.py +188 -520
  73. package/src/superlocalmemory/learning/database.py +256 -0
  74. package/src/superlocalmemory/learning/dedup_hnsw.py +413 -0
  75. package/src/superlocalmemory/learning/ensemble.py +300 -0
  76. package/src/superlocalmemory/learning/fact_outcome_joins.py +207 -0
  77. package/src/superlocalmemory/learning/forgetting_scheduler.py +55 -0
  78. package/src/superlocalmemory/learning/hnsw_dedup.py +69 -0
  79. package/src/superlocalmemory/learning/labeler.py +87 -0
  80. package/src/superlocalmemory/learning/legacy_migration.py +277 -0
  81. package/src/superlocalmemory/learning/memory_merge.py +160 -0
  82. package/src/superlocalmemory/learning/model_cache.py +269 -0
  83. package/src/superlocalmemory/learning/model_rollback.py +278 -0
  84. package/src/superlocalmemory/learning/outcome_queue.py +284 -0
  85. package/src/superlocalmemory/learning/pattern_miner.py +415 -0
  86. package/src/superlocalmemory/learning/pattern_miner_constants.py +47 -0
  87. package/src/superlocalmemory/learning/ranker.py +225 -81
  88. package/src/superlocalmemory/learning/ranker_common.py +163 -0
  89. package/src/superlocalmemory/learning/ranker_retrain_legacy.py +202 -0
  90. package/src/superlocalmemory/learning/ranker_retrain_online.py +411 -0
  91. package/src/superlocalmemory/learning/reward.py +777 -0
  92. package/src/superlocalmemory/learning/reward_archive.py +210 -0
  93. package/src/superlocalmemory/learning/reward_boost.py +201 -0
  94. package/src/superlocalmemory/learning/reward_proxy.py +326 -0
  95. package/src/superlocalmemory/learning/shadow_test.py +524 -0
  96. package/src/superlocalmemory/learning/signal_worker.py +270 -0
  97. package/src/superlocalmemory/learning/signals.py +314 -0
  98. package/src/superlocalmemory/learning/trigram_index.py +547 -0
  99. package/src/superlocalmemory/mcp/server.py +5 -5
  100. package/src/superlocalmemory/mcp/tools_context.py +183 -0
  101. package/src/superlocalmemory/mcp/tools_core.py +92 -27
  102. package/src/superlocalmemory/parameterization/soft_prompt_generator.py +13 -0
  103. package/src/superlocalmemory/retrieval/engine.py +52 -0
  104. package/src/superlocalmemory/server/api.py +2 -2
  105. package/src/superlocalmemory/server/bandit_loops.py +140 -0
  106. package/src/superlocalmemory/server/middleware/__init__.py +11 -0
  107. package/src/superlocalmemory/server/middleware/security_headers.py +144 -0
  108. package/src/superlocalmemory/server/routes/backup.py +36 -13
  109. package/src/superlocalmemory/server/routes/behavioral.py +50 -19
  110. package/src/superlocalmemory/server/routes/brain.py +1234 -0
  111. package/src/superlocalmemory/server/routes/data_io.py +4 -4
  112. package/src/superlocalmemory/server/routes/events.py +2 -2
  113. package/src/superlocalmemory/server/routes/helpers.py +1 -1
  114. package/src/superlocalmemory/server/routes/learning.py +192 -7
  115. package/src/superlocalmemory/server/routes/memories.py +189 -1
  116. package/src/superlocalmemory/server/routes/prewarm.py +171 -0
  117. package/src/superlocalmemory/server/routes/profiles.py +3 -3
  118. package/src/superlocalmemory/server/routes/token.py +88 -0
  119. package/src/superlocalmemory/server/routes/ws.py +5 -5
  120. package/src/superlocalmemory/server/security_middleware.py +13 -7
  121. package/src/superlocalmemory/server/ui.py +2 -2
  122. package/src/superlocalmemory/server/unified_daemon.py +335 -3
  123. package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +423 -0
  124. package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +348 -0
  125. package/src/superlocalmemory/skills/slm-recall/SKILL.md +343 -0
  126. package/src/superlocalmemory/skills/slm-remember/SKILL.md +194 -0
  127. package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +224 -0
  128. package/src/superlocalmemory/skills/slm-status/SKILL.md +363 -0
  129. package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +442 -0
  130. package/src/superlocalmemory/storage/migration_runner.py +545 -0
  131. package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +67 -0
  132. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +132 -0
  133. package/src/superlocalmemory/storage/migrations/M003_migration_log.py +38 -0
  134. package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +46 -0
  135. package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +75 -0
  136. package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +75 -0
  137. package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +63 -0
  138. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +54 -0
  139. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +75 -0
  140. package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +87 -0
  141. package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +72 -0
  142. package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +55 -0
  143. package/src/superlocalmemory/storage/migrations/__init__.py +81 -0
  144. package/src/superlocalmemory/storage/models.py +4 -0
  145. package/src/superlocalmemory/ui/css/brain.css +409 -0
  146. package/src/superlocalmemory/ui/css/legacy-dashboard.css +645 -0
  147. package/src/superlocalmemory/ui/index.html +459 -1345
  148. package/src/superlocalmemory/ui/js/brain.js +1321 -0
  149. package/src/superlocalmemory/ui/js/clusters.js +123 -4
  150. package/src/superlocalmemory/ui/js/init.js +48 -39
  151. package/src/superlocalmemory/ui/js/memories.js +88 -2
  152. package/src/superlocalmemory/ui/js/modal.js +71 -1
  153. package/src/superlocalmemory/ui/js/ng-shell.js +101 -88
  154. package/src/superlocalmemory/ui/js/trust-dashboard.js +168 -25
  155. package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +2018 -0
  156. package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  157. package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  158. package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +7 -0
  159. package/src/superlocalmemory/ui/vendor/bootstrap.min.css +6 -0
  160. package/src/superlocalmemory/ui/vendor/d3.v7.min.js +2 -0
  161. package/src/superlocalmemory/ui/vendor/graphology-library.min.js +2 -0
  162. package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +2 -0
  163. package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +8 -0
  164. package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
  165. package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
  166. package/src/superlocalmemory/ui/vendor/sigma.min.js +1 -0
  167. package/src/superlocalmemory/ui/js/behavioral.js +0 -447
  168. package/src/superlocalmemory/ui/js/graph-core.js +0 -447
  169. package/src/superlocalmemory/ui/js/graph-interactions.js +0 -351
  170. package/src/superlocalmemory/ui/js/learning.js +0 -435
  171. package/src/superlocalmemory/ui/js/patterns.js +0 -93
  172. package/src/superlocalmemory.egg-info/PKG-INFO +0 -647
  173. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -335
  174. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  175. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  176. package/src/superlocalmemory.egg-info/requires.txt +0 -58
  177. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SuperLocalMemory v3.4.21 — slm-hook binary fetcher.
4
+ *
5
+ * LLD reference: .backup/active-brain/lld/LLD-06-windows-binary-and-legacy-migration.md §6.2
6
+ *
7
+ * Fails open: any network / SHA / unpack failure logs a warning and
8
+ * exits 0. The dispatcher (bin/slm) falls back to the Python path so
9
+ * the hook still works — just slower on Windows.
10
+ *
11
+ * Stdlib-only (node:https, node:crypto, node:fs, node:path, node:os).
12
+ */
13
+ 'use strict';
14
+
15
+ const fs = require('node:fs');
16
+ const fsp = require('node:fs/promises');
17
+ const path = require('node:path');
18
+ const os = require('node:os');
19
+ const crypto = require('node:crypto');
20
+ const https = require('node:https');
21
+
22
+ const SLM_HOME = path.join(os.homedir(), '.superlocalmemory');
23
+ const BIN_DIR = path.join(SLM_HOME, 'bin');
24
+ const MANIFEST_BASE_URL = process.env.SLM_MANIFEST_BASE_URL ||
25
+ 'https://github.com/qualixar/superlocalmemory/releases/download';
26
+
27
+ // Map Node os.platform() / os.arch() -> canonical manifest tuple.
28
+ const PLATFORM_MAP = {
29
+ 'darwin': 'macos',
30
+ 'linux': 'linux',
31
+ 'win32': 'windows',
32
+ };
33
+ const ARCH_MAP = {
34
+ 'arm64': 'arm64',
35
+ 'x64': 'x86_64',
36
+ 'x86_64': 'x86_64',
37
+ };
38
+
39
+ function canonicalPlatformArch(platform, arch) {
40
+ const p = PLATFORM_MAP[platform] || null;
41
+ const a = ARCH_MAP[arch] || null;
42
+ return (p && a) ? { platform: p, arch: a } : null;
43
+ }
44
+
45
+ function logInfo(msg) { console.log(`[slm-hook] ${msg}`); }
46
+ function logWarn(msg) { console.warn(`[slm-hook] ${msg}`); }
47
+
48
+ // S8-SEC-03: redirect host allow-list. Previously any 3xx ``Location``
49
+ // header would be followed blindly, so a compromised upstream (DNS
50
+ // hijack, typosquat in the package registry) could redirect the postinstall
51
+ // download to an attacker-controlled host and substitute the hook binary.
52
+ // We only follow redirects back to GitHub's own release CDN.
53
+ const ALLOWED_REDIRECT_HOSTS = new Set([
54
+ 'github.com',
55
+ 'api.github.com',
56
+ 'codeload.github.com',
57
+ 'objects.githubusercontent.com',
58
+ 'raw.githubusercontent.com',
59
+ 'release-assets.githubusercontent.com',
60
+ ]);
61
+
62
+ function redirectIsAllowed(rawLocation) {
63
+ if (typeof rawLocation !== 'string' || rawLocation.length === 0) return false;
64
+ try {
65
+ const parsed = new URL(rawLocation);
66
+ if (parsed.protocol !== 'https:') return false;
67
+ return ALLOWED_REDIRECT_HOSTS.has(parsed.hostname);
68
+ } catch (_err) {
69
+ return false;
70
+ }
71
+ }
72
+
73
+ function fetchJson(url) {
74
+ return new Promise((resolve, reject) => {
75
+ const req = https.get(url, (res) => {
76
+ if (res.statusCode && res.statusCode >= 300 &&
77
+ res.statusCode < 400 && res.headers.location) {
78
+ if (!redirectIsAllowed(res.headers.location)) {
79
+ reject(new Error('redirect host not allow-listed'));
80
+ return;
81
+ }
82
+ // Single redirect hop — GitHub releases use redirects.
83
+ fetchJson(res.headers.location).then(resolve, reject);
84
+ return;
85
+ }
86
+ if (res.statusCode !== 200) {
87
+ reject(new Error(`HTTP ${res.statusCode} for ${url}`));
88
+ return;
89
+ }
90
+ const chunks = [];
91
+ res.on('data', (c) => chunks.push(c));
92
+ res.on('end', () => {
93
+ try {
94
+ resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
95
+ } catch (err) {
96
+ reject(err);
97
+ }
98
+ });
99
+ });
100
+ req.on('error', reject);
101
+ req.setTimeout(15000, () => {
102
+ req.destroy(new Error('timeout fetching manifest'));
103
+ });
104
+ });
105
+ }
106
+
107
+ function downloadFile(url, destPath) {
108
+ return new Promise((resolve, reject) => {
109
+ const out = fs.createWriteStream(destPath);
110
+ const req = https.get(url, (res) => {
111
+ if (res.statusCode && res.statusCode >= 300 &&
112
+ res.statusCode < 400 && res.headers.location) {
113
+ if (!redirectIsAllowed(res.headers.location)) {
114
+ out.close();
115
+ fs.unlink(destPath, () => {});
116
+ reject(new Error('redirect host not allow-listed'));
117
+ return;
118
+ }
119
+ out.close();
120
+ fs.unlink(destPath, () => {
121
+ downloadFile(res.headers.location, destPath)
122
+ .then(resolve, reject);
123
+ });
124
+ return;
125
+ }
126
+ if (res.statusCode !== 200) {
127
+ out.close();
128
+ fs.unlink(destPath, () => {});
129
+ reject(new Error(`HTTP ${res.statusCode} for ${url}`));
130
+ return;
131
+ }
132
+ res.pipe(out);
133
+ out.on('finish', () => out.close(resolve));
134
+ });
135
+ req.on('error', (err) => {
136
+ out.close();
137
+ fs.unlink(destPath, () => {});
138
+ reject(err);
139
+ });
140
+ req.setTimeout(60000, () => {
141
+ req.destroy(new Error('timeout downloading asset'));
142
+ });
143
+ });
144
+ }
145
+
146
+ async function sha256File(filePath) {
147
+ const h = crypto.createHash('sha256');
148
+ const stream = fs.createReadStream(filePath);
149
+ return new Promise((resolve, reject) => {
150
+ stream.on('data', (c) => h.update(c));
151
+ stream.on('end', () => resolve(h.digest('hex')));
152
+ stream.on('error', reject);
153
+ });
154
+ }
155
+
156
+ function pickAsset(manifest, platform, arch) {
157
+ if (!manifest || !Array.isArray(manifest.assets)) return null;
158
+ // Prefer setup.exe on Windows (Inno Setup) when present, fall back
159
+ // to plain archive.
160
+ const matches = manifest.assets.filter(a =>
161
+ a.platform === platform && a.arch === arch);
162
+ if (matches.length === 0) return null;
163
+ // Deterministic ordering: setup.exe > .zip > .tar.gz
164
+ matches.sort((a, b) => {
165
+ const rank = (n) => n.endsWith('setup.exe') ? 0 :
166
+ n.endsWith('.zip') ? 1 : 2;
167
+ return rank(a.name) - rank(b.name);
168
+ });
169
+ return matches[0];
170
+ }
171
+
172
+ async function main() {
173
+ try {
174
+ const pkgJson = require(path.join(__dirname, '..', 'package.json'));
175
+ const version = pkgJson.version;
176
+ const url = `${MANIFEST_BASE_URL}/v${version}/manifest.json`;
177
+
178
+ const pa = canonicalPlatformArch(os.platform(), os.arch());
179
+ if (!pa) {
180
+ logWarn(`unsupported platform ${os.platform()}/${os.arch()}; ` +
181
+ 'Python fallback');
182
+ return 0;
183
+ }
184
+
185
+ let manifest;
186
+ try {
187
+ manifest = await fetchJson(url);
188
+ } catch (err) {
189
+ logWarn(`manifest fetch failed: ${err.message}; Python fallback`);
190
+ return 0;
191
+ }
192
+
193
+ const asset = pickAsset(manifest, pa.platform, pa.arch);
194
+ if (!asset) {
195
+ logWarn(`no prebuilt binary for ${pa.platform}/${pa.arch}; ` +
196
+ 'Python fallback');
197
+ return 0;
198
+ }
199
+
200
+ // S8-SEC-01 fix: the manifest is remote-attacker-controllable (even
201
+ // when minisigned, the signer is a release process we don't fully
202
+ // trust at the per-asset level). Reject any ``asset.name`` that
203
+ // contains a path separator, traversal segments, or is empty —
204
+ // and re-derive the safe filename from basename before joining.
205
+ // Without this, a poisoned manifest can write arbitrary paths
206
+ // (e.g., "../../.ssh/authorized_keys") even though SHA-256 validates
207
+ // the bytes, because SHA only binds the content to the claimed name.
208
+ const rawName = typeof asset.name === 'string' ? asset.name : '';
209
+ if (!rawName
210
+ || rawName.includes('/')
211
+ || rawName.includes('\\')
212
+ || rawName.includes('\0')
213
+ || rawName === '.'
214
+ || rawName === '..'
215
+ || rawName.startsWith('.')) {
216
+ logWarn(`rejected asset name (path-traversal guard): ` +
217
+ `${JSON.stringify(rawName)}; Python fallback`);
218
+ return 0;
219
+ }
220
+ const safeName = path.basename(rawName);
221
+ if (safeName !== rawName) {
222
+ logWarn(`asset name changed under basename sanitisation: ` +
223
+ `${JSON.stringify(rawName)} -> ${JSON.stringify(safeName)}; ` +
224
+ 'Python fallback');
225
+ return 0;
226
+ }
227
+
228
+ await fsp.mkdir(BIN_DIR, { recursive: true });
229
+ const tmpName = `${safeName}.part`;
230
+ const tmpPath = path.join(BIN_DIR, tmpName);
231
+ try {
232
+ await downloadFile(asset.url, tmpPath);
233
+ } catch (err) {
234
+ logWarn(`download failed: ${err.message}; Python fallback`);
235
+ await fsp.unlink(tmpPath).catch(() => {});
236
+ return 0;
237
+ }
238
+
239
+ const actual = await sha256File(tmpPath);
240
+ if (actual !== asset.sha256) {
241
+ await fsp.unlink(tmpPath).catch(() => {});
242
+ logWarn(`SHA-256 mismatch for ${asset.name}: expected ` +
243
+ `${asset.sha256}, got ${actual}; Python fallback`);
244
+ return 0;
245
+ }
246
+
247
+ const finalPath = path.join(BIN_DIR, safeName);
248
+ // Belt-and-suspenders: after resolving the final path, assert it
249
+ // sits under BIN_DIR. ``path.resolve`` normalises any '..' that
250
+ // could have slipped through; if the result escapes BIN_DIR
251
+ // (which it shouldn't after basename sanitisation, but it's
252
+ // cheap insurance), refuse to install.
253
+ const resolvedFinal = path.resolve(finalPath);
254
+ const resolvedBin = path.resolve(BIN_DIR);
255
+ if (!resolvedFinal.startsWith(resolvedBin + path.sep)
256
+ && resolvedFinal !== resolvedBin) {
257
+ await fsp.unlink(tmpPath).catch(() => {});
258
+ logWarn(`path escape detected: ${resolvedFinal} not under ` +
259
+ `${resolvedBin}; Python fallback`);
260
+ return 0;
261
+ }
262
+ await fsp.rename(tmpPath, finalPath);
263
+ logInfo(`✓ slm-hook asset fetched: ${asset.name} ` +
264
+ `(${asset.signing})`);
265
+ logInfo(` path: ${finalPath}`);
266
+ logInfo(' Note: unpack step is platform-specific; ' +
267
+ 'the installer runs it.');
268
+ return 0;
269
+ } catch (err) {
270
+ logWarn(`unexpected error: ${err.message}; Python fallback`);
271
+ return 0;
272
+ }
273
+ }
274
+
275
+ // Exports for testing; direct execution runs main().
276
+ module.exports = {
277
+ canonicalPlatformArch,
278
+ pickAsset,
279
+ sha256File,
280
+ MANIFEST_BASE_URL,
281
+ main,
282
+ };
283
+
284
+ if (require.main === module) {
285
+ main().then((code) => process.exit(code || 0),
286
+ () => process.exit(0));
287
+ }
@@ -0,0 +1,273 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4.22 — LLD-06 §6.1
4
+
5
+ """Generate the release ``manifest.json`` for ``slm-hook`` binaries.
6
+
7
+ LLD reference: ``.backup/active-brain/lld/LLD-06-windows-binary-and-legacy-migration.md``
8
+ Section 6.1 (Release manifest).
9
+
10
+ Shape (per LLD-06 §6.1):
11
+
12
+ {
13
+ "version": "3.4.22",
14
+ "released_at": "2026-04-17T00:00:00Z",
15
+ "assets": [
16
+ {
17
+ "name": "slm-hook-macos-arm64.tar.gz",
18
+ "url": "https://.../manifest.json",
19
+ "size_bytes": 5312000,
20
+ "sha256": "a7c3...",
21
+ "signing": "apple-notarized"
22
+ }, ...
23
+ ],
24
+ "manifest_sha256_self": "...",
25
+ "manifest_signature": "minisign: placeholder"
26
+ }
27
+
28
+ Minisign signing is OUT of scope at Stage 6 — this module emits a
29
+ placeholder and a ``manifest.minisig`` stub. Real signing is a CI job
30
+ that layers on top (needs the private key in GitHub Secrets).
31
+ """
32
+ from __future__ import annotations
33
+
34
+ import hashlib
35
+ import json
36
+ from dataclasses import dataclass, field
37
+ from datetime import datetime, timezone
38
+ from pathlib import Path
39
+
40
+ # Signing envelope strings — one per expected platform. Must match the
41
+ # values the dispatcher + postinstall check. Kept as constants so tests
42
+ # can lock them.
43
+ SIGNING_APPLE_NOTARIZED = "apple-notarized"
44
+ SIGNING_AUTHENTICODE = "authenticode"
45
+ SIGNING_UNSIGNED = "unsigned"
46
+
47
+ # Default base URL used when the caller doesn't supply one. Kept in a
48
+ # constant so tests can assert the release shape without a network call.
49
+ DEFAULT_BASE_URL = (
50
+ "https://github.com/qualixar/superlocalmemory/releases/download"
51
+ )
52
+
53
+ # Allowed platform/arch combos we ship binaries for. Any asset whose
54
+ # filename does not encode one of these is rejected.
55
+ _VALID_PLATFORMS: frozenset[tuple[str, str]] = frozenset({
56
+ ("macos", "arm64"),
57
+ ("macos", "x86_64"),
58
+ ("linux", "x86_64"),
59
+ ("linux", "arm64"),
60
+ ("windows", "x86_64"),
61
+ })
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class AssetSpec:
66
+ """Input spec for a single release artefact on disk."""
67
+
68
+ path: Path
69
+ platform: str
70
+ arch: str
71
+ signing: str
72
+
73
+
74
+ @dataclass(frozen=True, slots=True)
75
+ class Manifest:
76
+ """In-memory representation of the manifest to be serialized."""
77
+
78
+ version: str
79
+ released_at: str
80
+ assets: list[dict] = field(default_factory=list)
81
+ manifest_sha256_self: str = ""
82
+ manifest_signature: str = "minisign: placeholder"
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Helpers
87
+ # ---------------------------------------------------------------------------
88
+
89
+ def _sha256_file(path: Path) -> str:
90
+ h = hashlib.sha256()
91
+ with path.open("rb") as fh:
92
+ for chunk in iter(lambda: fh.read(1024 * 1024), b""):
93
+ h.update(chunk)
94
+ return h.hexdigest()
95
+
96
+
97
+ def _utcnow_iso() -> str:
98
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
99
+
100
+
101
+ def _validate_platform(platform: str, arch: str) -> None:
102
+ if (platform, arch) not in _VALID_PLATFORMS:
103
+ raise ValueError(
104
+ f"unsupported platform/arch: {platform}/{arch}"
105
+ )
106
+
107
+
108
+ def _validate_signing(signing: str) -> None:
109
+ allowed = {SIGNING_APPLE_NOTARIZED, SIGNING_AUTHENTICODE,
110
+ SIGNING_UNSIGNED}
111
+ if signing not in allowed:
112
+ raise ValueError(
113
+ f"invalid signing value: {signing!r} (allowed: {allowed})"
114
+ )
115
+
116
+
117
+ def _asset_url(base_url: str, version: str, filename: str) -> str:
118
+ base = base_url.rstrip("/")
119
+ return f"{base}/v{version}/{filename}"
120
+
121
+
122
+ # ---------------------------------------------------------------------------
123
+ # Builders
124
+ # ---------------------------------------------------------------------------
125
+
126
+ def build_asset_entry(
127
+ spec: AssetSpec,
128
+ *,
129
+ version: str,
130
+ base_url: str,
131
+ ) -> dict:
132
+ """Compute a single manifest asset entry."""
133
+ if not spec.path.is_file():
134
+ raise FileNotFoundError(f"asset missing: {spec.path}")
135
+ _validate_platform(spec.platform, spec.arch)
136
+ _validate_signing(spec.signing)
137
+
138
+ return {
139
+ "name": spec.path.name,
140
+ "url": _asset_url(base_url, version, spec.path.name),
141
+ "size_bytes": spec.path.stat().st_size,
142
+ "sha256": _sha256_file(spec.path),
143
+ "signing": spec.signing,
144
+ "platform": spec.platform,
145
+ "arch": spec.arch,
146
+ }
147
+
148
+
149
+ def build_manifest(
150
+ version: str,
151
+ assets: list[AssetSpec],
152
+ *,
153
+ base_url: str = DEFAULT_BASE_URL,
154
+ released_at: str | None = None,
155
+ ) -> Manifest:
156
+ """Assemble the full Manifest object (including self SHA)."""
157
+ if not version:
158
+ raise ValueError("version is required")
159
+ if not assets:
160
+ raise ValueError("at least one asset is required")
161
+
162
+ entries: list[dict] = [
163
+ build_asset_entry(spec, version=version, base_url=base_url)
164
+ for spec in assets
165
+ ]
166
+ payload = {
167
+ "version": version,
168
+ "released_at": released_at or _utcnow_iso(),
169
+ "assets": entries,
170
+ }
171
+ # Compute the manifest-self SHA over the canonical JSON WITHOUT the
172
+ # self-SHA field (otherwise it would be recursive).
173
+ serialized = json.dumps(payload, sort_keys=True,
174
+ separators=(",", ":")).encode("utf-8")
175
+ self_sha = hashlib.sha256(serialized).hexdigest()
176
+
177
+ return Manifest(
178
+ version=version,
179
+ released_at=payload["released_at"],
180
+ assets=entries,
181
+ manifest_sha256_self=self_sha,
182
+ manifest_signature="minisign: placeholder",
183
+ )
184
+
185
+
186
+ def serialize(manifest: Manifest) -> str:
187
+ """Deterministic JSON serialization for on-disk storage."""
188
+ doc = {
189
+ "version": manifest.version,
190
+ "released_at": manifest.released_at,
191
+ "assets": manifest.assets,
192
+ "manifest_sha256_self": manifest.manifest_sha256_self,
193
+ "manifest_signature": manifest.manifest_signature,
194
+ }
195
+ return json.dumps(doc, indent=2, sort_keys=False) + "\n"
196
+
197
+
198
+ def write_manifest(
199
+ manifest: Manifest,
200
+ dest_dir: Path,
201
+ ) -> tuple[Path, Path]:
202
+ """Write ``manifest.json`` + a stub ``manifest.minisig`` to ``dest_dir``.
203
+
204
+ Returns the two paths (for caller convenience).
205
+ """
206
+ dest_dir.mkdir(parents=True, exist_ok=True)
207
+ manifest_path = dest_dir / "manifest.json"
208
+ sig_path = dest_dir / "manifest.minisig"
209
+
210
+ manifest_path.write_text(serialize(manifest), encoding="utf-8")
211
+ # Placeholder — real minisig produced by a separate CI job.
212
+ sig_path.write_text(
213
+ "untrusted comment: placeholder - real signature added by CI\n"
214
+ f"manifest_sha256_self: {manifest.manifest_sha256_self}\n",
215
+ encoding="utf-8",
216
+ )
217
+ return manifest_path, sig_path
218
+
219
+
220
+ # ---------------------------------------------------------------------------
221
+ # Validation helpers (used by tests + postinstall)
222
+ # ---------------------------------------------------------------------------
223
+
224
+ def validate_shape(doc: dict) -> list[str]:
225
+ """Return a list of validation errors on a loaded manifest dict."""
226
+ errors: list[str] = []
227
+ for key in ("version", "released_at", "assets",
228
+ "manifest_sha256_self", "manifest_signature"):
229
+ if key not in doc:
230
+ errors.append(f"missing top-level key: {key}")
231
+
232
+ assets = doc.get("assets")
233
+ if not isinstance(assets, list):
234
+ errors.append("assets must be a list")
235
+ return errors
236
+ if not assets:
237
+ errors.append("assets list is empty")
238
+
239
+ for i, entry in enumerate(assets):
240
+ if not isinstance(entry, dict):
241
+ errors.append(f"asset[{i}] must be an object")
242
+ continue
243
+ for field_name in ("name", "url", "size_bytes",
244
+ "sha256", "signing"):
245
+ if field_name not in entry:
246
+ errors.append(
247
+ f"asset[{i}] missing field: {field_name}"
248
+ )
249
+ sha = entry.get("sha256", "")
250
+ if not isinstance(sha, str) or len(sha) != 64:
251
+ errors.append(f"asset[{i}] sha256 must be 64-char hex")
252
+ signing = entry.get("signing")
253
+ if signing is not None:
254
+ try:
255
+ _validate_signing(signing)
256
+ except ValueError as exc:
257
+ errors.append(f"asset[{i}] {exc}")
258
+ return errors
259
+
260
+
261
+ __all__ = (
262
+ "AssetSpec",
263
+ "Manifest",
264
+ "SIGNING_APPLE_NOTARIZED",
265
+ "SIGNING_AUTHENTICODE",
266
+ "SIGNING_UNSIGNED",
267
+ "DEFAULT_BASE_URL",
268
+ "build_asset_entry",
269
+ "build_manifest",
270
+ "serialize",
271
+ "write_manifest",
272
+ "validate_shape",
273
+ )
@@ -0,0 +1,56 @@
1
+ # -*- mode: python ; coding: utf-8 -*-
2
+ # LLD reference: .backup/active-brain/lld/LLD-06-windows-binary-and-legacy-migration.md §4.2
3
+ # Mode: onedir (not onefile) — LLD-06 §4.1, verification claim 7.
4
+ # console=False, upx=False, strip=True.
5
+
6
+ block_cipher = None
7
+
8
+ a = Analysis(
9
+ ['../src/superlocalmemory/hook_binary_entry.py'],
10
+ pathex=[],
11
+ binaries=[],
12
+ datas=[],
13
+ hiddenimports=[],
14
+ hookspath=[],
15
+ runtime_hooks=[],
16
+ excludes=[
17
+ # Deny-list — anything imported here would blow up cold-start and size.
18
+ 'torch', 'sentence_transformers', 'fastapi', 'uvicorn',
19
+ 'numpy', 'scipy', 'lightgbm', 'onnxruntime', 'transformers',
20
+ 'httpx', 'mcp', 'pydantic', 'lark',
21
+ 'superlocalmemory.core.engine',
22
+ 'superlocalmemory.core.embeddings',
23
+ 'superlocalmemory.retrieval',
24
+ 'superlocalmemory.learning',
25
+ ],
26
+ cipher=block_cipher,
27
+ noarchive=False,
28
+ )
29
+ pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
30
+
31
+ exe = EXE(
32
+ pyz,
33
+ a.scripts,
34
+ [],
35
+ exclude_binaries=True,
36
+ name='slm-hook',
37
+ debug=False,
38
+ bootloader_ignore_signals=False,
39
+ strip=True,
40
+ upx=False, # UPX adds 30-60 ms cold-start + AV heuristics
41
+ console=False, # no console flash on Windows
42
+ disable_windowed_traceback=False,
43
+ target_arch=None,
44
+ codesign_identity=None, # handled in scripts/sign-macos.sh post-build
45
+ entitlements_file=None,
46
+ )
47
+
48
+ coll = COLLECT(
49
+ exe,
50
+ a.binaries,
51
+ a.zipfiles,
52
+ a.datas,
53
+ strip=True,
54
+ upx=False,
55
+ name='slm-hook',
56
+ )