superlocalmemory 4.1.21 → 4.1.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.
- package/.claude-plugin/marketplace.json +3 -3
- package/CHANGELOG.md +111 -0
- package/README.md +53 -8
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.cursor-plugin/plugin.json +24 -0
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/assets/logo.svg +36 -0
- package/plugin/cursor-skills/slm-bot-memory/SKILL.md +139 -0
- package/plugin/cursor-skills/slm-getting-started-bot/SKILL.md +138 -0
- package/plugin/cursor-skills/slm-recall/SKILL.md +333 -0
- package/plugin/cursor-skills/slm-remember/SKILL.md +273 -0
- package/plugin/cursor-skills/slm-scope/SKILL.md +176 -0
- package/plugin/cursor-skills/slm-session/SKILL.md +271 -0
- package/plugin/hooks/hooks.json +9 -9
- package/plugin/mcp.cursor.json +23 -0
- package/plugin/requirements-cpu-torch.txt +13 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/ensure-venv.sh +45 -3
- package/plugin/scripts/slm-launch +93 -9
- package/plugin/scripts/torch-cpu-resolve.sh +86 -0
- package/plugin/skills/slm-bot-memory/SKILL.md +139 -0
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-getting-started-bot/SKILL.md +138 -0
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +3 -2
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/agents/slm-memory-advisor.md +1 -1
- package/plugin-src/agents/slm-optimize-advisor.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-bot-memory/SKILL.md +139 -0
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-getting-started-bot/SKILL.md +138 -0
- package/plugin-src/skills/slm-governance/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +1 -1
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/SKILL.md +3 -2
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/scripts/postinstall.js +107 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: slm-session
|
|
3
|
+
description: Manage SuperLocalMemory session lifecycle — call session_init once at the start of every fresh session to load relevant project context and get a session_id; call close_session when work is meaningfully complete to commit temporal summaries. Correct lifecycle hygiene is what makes SLM's learning loop work.
|
|
4
|
+
when_to_use: |
|
|
5
|
+
- At the start of every session (auto-trigger on first user message in a project context)
|
|
6
|
+
- When the user says "start a new session" or "initialize memory"
|
|
7
|
+
- When meaningful work completes and context should be committed
|
|
8
|
+
- When the user says "close session" or "end session"
|
|
9
|
+
allowed-tools: session_init, close_session, Bash
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# slm-session — Session Lifecycle Hygiene
|
|
13
|
+
|
|
14
|
+
Session lifecycle is the mechanism that makes SuperLocalMemory's learning loop
|
|
15
|
+
work. Without it, recall signals are not attributed and temporal summaries are
|
|
16
|
+
not written. This is not optional housekeeping — it is load-bearing.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## The lifecycle in one diagram
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Session starts
|
|
24
|
+
|
|
|
25
|
+
v
|
|
26
|
+
session_init(project_path, query)
|
|
27
|
+
|--- returns session_id, context, memories
|
|
28
|
+
|
|
|
29
|
+
v
|
|
30
|
+
Use session_id in every recall() and remember() call
|
|
31
|
+
|
|
|
32
|
+
v
|
|
33
|
+
Work completes
|
|
34
|
+
|
|
|
35
|
+
v
|
|
36
|
+
close_session(session_id)
|
|
37
|
+
|--- writes temporal summaries to DB
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## session_init — call once per fresh session
|
|
43
|
+
|
|
44
|
+
### When to call
|
|
45
|
+
|
|
46
|
+
Call `session_init` exactly once at the start of every fresh session, before
|
|
47
|
+
any `recall` or `remember`. Never call it twice in a session — the second call
|
|
48
|
+
would generate a new `session_id` and break signal attribution for any prior
|
|
49
|
+
recalls or remembers in that session.
|
|
50
|
+
|
|
51
|
+
### Signature
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
session_init(
|
|
55
|
+
project_path: str = "", # working directory path, e.g. "~/projects/foo"
|
|
56
|
+
query: str = "", # topic override; if omitted, derived from project_path
|
|
57
|
+
max_results: int = 10, # max memories to return (default: 10)
|
|
58
|
+
max_age_days: int = 30, # suppress memories older than N days unless score >= 0.7
|
|
59
|
+
# set to 0 to disable the age gate entirely
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### What it does
|
|
64
|
+
|
|
65
|
+
1. Derives a search query from `project_path` (or uses your explicit `query`).
|
|
66
|
+
2. Runs a 2-tier recall: full daemon retrieval (primary) or FTS5 BM25
|
|
67
|
+
(emergency fallback if daemon is unreachable).
|
|
68
|
+
3. Merges any pinned "core memory" facts with the recall results.
|
|
69
|
+
4. Applies an age gate — memories older than `max_age_days` are suppressed
|
|
70
|
+
unless their relevance score is 0.70 or above (architectural decisions that
|
|
71
|
+
remain permanently relevant still surface).
|
|
72
|
+
5. Returns a pre-formatted `context` block and a structured `memories` array
|
|
73
|
+
for your session.
|
|
74
|
+
6. Generates a stable `session_id` (`slm-YYYYMMDD-<8hex>`) and returns it.
|
|
75
|
+
|
|
76
|
+
### Real response shape
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"success": true,
|
|
81
|
+
"session_id": "slm-20260616-a3f8c1d2",
|
|
82
|
+
"context": "# Relevant Memory Context\n\n- JWT tokens use 1h expiry ...",
|
|
83
|
+
"memories": [
|
|
84
|
+
{
|
|
85
|
+
"fact_id": "f8a2bc91",
|
|
86
|
+
"content": "JWT tokens use 1h expiry for API auth (2026-06-10)",
|
|
87
|
+
"score": 0.87,
|
|
88
|
+
"is_core": false
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"memory_count": 3,
|
|
92
|
+
"core_memory": [],
|
|
93
|
+
"degraded_mode": false,
|
|
94
|
+
"retrieval_mode": "full_6_channel",
|
|
95
|
+
"calibration_status": "uncalibrated",
|
|
96
|
+
"calibration_id": null,
|
|
97
|
+
"answer_confidence": null,
|
|
98
|
+
"abstained": false,
|
|
99
|
+
"abstention_reason": null,
|
|
100
|
+
"learning": {
|
|
101
|
+
"feedback_signals": 37,
|
|
102
|
+
"phase": 1,
|
|
103
|
+
"status": "collecting"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Check `degraded_mode`.** When `true`, the daemon was unreachable and only
|
|
109
|
+
FTS5 BM25 was used — semantic, graph, temporal, and structural channels were
|
|
110
|
+
unavailable. The context is still usable; note the degradation if relevant.
|
|
111
|
+
|
|
112
|
+
**Check `abstained`.** If `true`, the memories returned in `context` and
|
|
113
|
+
`memories` do not answer the query that seeded this session — say so, or
|
|
114
|
+
ask, rather than presenting them as settled fact. This is most relevant
|
|
115
|
+
when you passed an explicit `query`; the default project-context query
|
|
116
|
+
rarely has a single "right answer" for a judge to evaluate.
|
|
117
|
+
`answer_confidence` is a measurement, not a guarantee. `calibration_status:
|
|
118
|
+
"uncalibrated"` means no judge is configured, and `abstained` then only
|
|
119
|
+
reflects the older "nothing found" signal. See `slm-recall` for the full
|
|
120
|
+
rule, which applies identically here.
|
|
121
|
+
|
|
122
|
+
**Check `learning.phase`:**
|
|
123
|
+
- Phase 1 (< 50 signals): collecting baseline feedback
|
|
124
|
+
- Phase 2 (50–199 signals): active learning
|
|
125
|
+
- Phase 3 (≥ 200 signals): full ML-driven ranking
|
|
126
|
+
|
|
127
|
+
### How to use the returned session_id
|
|
128
|
+
|
|
129
|
+
Store it and thread it into every `recall` and `remember` call in this session:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
session_id = "<value from session_init>"
|
|
133
|
+
|
|
134
|
+
recall(query="auth strategy", session_id=session_id, limit=10)
|
|
135
|
+
remember(content="...", session_id=session_id, tags="auth,decision", project="myapp")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
This attribution is what allows the ranker to learn which recalls led to useful
|
|
139
|
+
outcomes for this project. It also gives the session a small working set, so
|
|
140
|
+
successive recalls in one conversation build on what the earlier ones surfaced
|
|
141
|
+
instead of each starting cold.
|
|
142
|
+
|
|
143
|
+
**Do not synthesise a session id.** An id beginning `http:`, `mcp:`, `cli:` or
|
|
144
|
+
`probe:` is read as a synthetic per-request label rather than a conversation and
|
|
145
|
+
is deliberately excluded from that working set. Use the one `session_init`
|
|
146
|
+
returned, unchanged, for the whole session.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## close_session — call when work is meaningfully complete
|
|
151
|
+
|
|
152
|
+
### When to call
|
|
153
|
+
|
|
154
|
+
Call `close_session` when a meaningful unit of work is done — end of a coding
|
|
155
|
+
session, after shipping a feature, after a design review. You do not need to
|
|
156
|
+
call it after every small interaction. The signal is "this session's work is
|
|
157
|
+
committed and should be summarised."
|
|
158
|
+
|
|
159
|
+
Do not call it at the start of a new session as a cleanup step — `session_init`
|
|
160
|
+
is the correct opener and it does not require a prior close.
|
|
161
|
+
|
|
162
|
+
### Signature
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
close_session(
|
|
166
|
+
session_id: str = "", # the session_id from session_init; if omitted,
|
|
167
|
+
# the system queries the DB for the most recent session
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### What it does
|
|
172
|
+
|
|
173
|
+
Aggregates facts written during the session into per-entity temporal summary
|
|
174
|
+
events. These summaries enable future queries like "what happened during session
|
|
175
|
+
X?" and contribute to the temporal channel in retrieval.
|
|
176
|
+
|
|
177
|
+
### Real response shape
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"success": true,
|
|
182
|
+
"session_id": "slm-20260616-a3f8c1d2",
|
|
183
|
+
"summary_events_created": 4
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`summary_events_created: 0` is normal for short sessions where no new facts
|
|
188
|
+
were written. It is not an error.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Why this matters
|
|
193
|
+
|
|
194
|
+
Every `recall` call with a `session_id` enqueues engagement signals — which
|
|
195
|
+
results were shown, which were acted on. The learning ranker processes these
|
|
196
|
+
signals to gradually up-weight channels and facts that prove useful for your
|
|
197
|
+
project. Without `session_id`, signals land on a fallback identifier and are
|
|
198
|
+
never attributed to a project or agent. Over many sessions this compounds:
|
|
199
|
+
projects where lifecycle is respected have measurably better retrieval quality
|
|
200
|
+
than projects where session_init is skipped.
|
|
201
|
+
|
|
202
|
+
Within a single session it compounds faster. The session's working set holds the
|
|
203
|
+
memories its recalls have already surfaced, and later recalls rank those higher,
|
|
204
|
+
so a long conversation converges on the material it is actually about.
|
|
205
|
+
|
|
206
|
+
### Closing the loop explicitly
|
|
207
|
+
|
|
208
|
+
Engagement signals say a memory was *shown*. `report_outcome` says it was
|
|
209
|
+
*right*:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
report_outcome(memory_ids="<ids you actually used>", outcome="success")
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Send it when a recalled memory changed what you did, and send `failure` when a
|
|
216
|
+
confidently-returned memory turned out to be wrong — that is the only signal
|
|
217
|
+
that stops a stale memory from being promoted. See the `slm-recall` skill.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## CLI fallback (when MCP is unavailable)
|
|
222
|
+
|
|
223
|
+
There are no direct `session_init` or `close_session` CLI subcommands.
|
|
224
|
+
When MCP is unavailable, use `slm status` to check system health and
|
|
225
|
+
`slm recall` / `slm remember` directly. Session attribution will not be
|
|
226
|
+
available in degraded CLI-only mode.
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
slm status [--json] # check mode, profile, DB size, fact count
|
|
230
|
+
slm doctor [--json] # preflight check including daemon and embedding worker
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Common mistakes
|
|
236
|
+
|
|
237
|
+
| Mistake | Consequence | Fix |
|
|
238
|
+
|---------|-------------|-----|
|
|
239
|
+
| Calling `session_init` twice in one session | Two session IDs; signals split across them | Call once; store the returned ID |
|
|
240
|
+
| Omitting `session_id` from `recall` / `remember` | No learning attribution, and every turn starts cold | Always pass the stored `session_id` |
|
|
241
|
+
| Inventing a `session_id` such as `mcp:agent` or `http:1234` | Read as synthetic, excluded from the working set | Use the id `session_init` returned |
|
|
242
|
+
| Never reporting an outcome | Ranking cannot tell a useful memory from a merely returned one | `report_outcome` after a recall that changed what you did |
|
|
243
|
+
| Never calling `close_session` | Temporal summaries not written | Call at end of each meaningful work unit |
|
|
244
|
+
| Calling `close_session` without a `session_id` when no prior writes exist | Returns error "No session_id found" | Pass the explicit `session_id` from `session_init` |
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Profile context (v3.8.0+)
|
|
249
|
+
|
|
250
|
+
`session_init` operates on the currently active profile. If you need to start
|
|
251
|
+
a session on a different workspace, call `switch_profile` (requires `code`,
|
|
252
|
+
`full`, or `power` MCP profile) before `session_init`, then call `session_init`
|
|
253
|
+
for that workspace. See `slm-profile` for workspace switching.
|
|
254
|
+
|
|
255
|
+
The returned `memories` array reflects facts stored in the active profile only.
|
|
256
|
+
To also surface shared or global facts in the initial context, pass the query
|
|
257
|
+
explicitly and call `recall` with `include_global`/`include_shared` after
|
|
258
|
+
`session_init`. See `slm-scope` for the sharing model.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Related skills
|
|
263
|
+
|
|
264
|
+
- `slm-recall` — multi-channel retrieval during the session
|
|
265
|
+
- `slm-remember` — store durable facts during the session
|
|
266
|
+
- `slm-profile` — workspace isolation and profile switching
|
|
267
|
+
- `slm-scope` — multi-scope sharing model
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
*SuperLocalMemory v4.1.22 · Qualixar · AGPL-3.0-or-later*
|
package/plugin/hooks/hooks.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"type": "command",
|
|
8
8
|
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/slm-run\" hook mandate || true",
|
|
9
|
-
"timeout":
|
|
9
|
+
"timeout": 5
|
|
10
10
|
}
|
|
11
11
|
]
|
|
12
12
|
},
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
{
|
|
24
24
|
"type": "command",
|
|
25
25
|
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/slm-run\" hook start || true",
|
|
26
|
-
"timeout":
|
|
26
|
+
"timeout": 15
|
|
27
27
|
}
|
|
28
28
|
]
|
|
29
29
|
}
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
{
|
|
36
36
|
"type": "command",
|
|
37
37
|
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/slm-run\" hook checkpoint || true",
|
|
38
|
-
"timeout":
|
|
38
|
+
"timeout": 5
|
|
39
39
|
}
|
|
40
40
|
]
|
|
41
41
|
},
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
{
|
|
46
46
|
"type": "command",
|
|
47
47
|
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/slm-run\" hook post_tool_outcome || true",
|
|
48
|
-
"timeout":
|
|
48
|
+
"timeout": 5
|
|
49
49
|
}
|
|
50
50
|
]
|
|
51
51
|
}
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
{
|
|
57
57
|
"type": "command",
|
|
58
58
|
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/slm-run\" hook user_prompt_rehash || true",
|
|
59
|
-
"timeout":
|
|
59
|
+
"timeout": 5
|
|
60
60
|
}
|
|
61
61
|
]
|
|
62
62
|
},
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
{
|
|
66
66
|
"type": "command",
|
|
67
67
|
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/slm-run\" hook topic_shift || true",
|
|
68
|
-
"timeout":
|
|
68
|
+
"timeout": 3
|
|
69
69
|
}
|
|
70
70
|
]
|
|
71
71
|
}
|
|
@@ -76,12 +76,12 @@
|
|
|
76
76
|
{
|
|
77
77
|
"type": "command",
|
|
78
78
|
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/slm-run\" hook stop || true",
|
|
79
|
-
"timeout":
|
|
79
|
+
"timeout": 10
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
"type": "command",
|
|
83
83
|
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/slm-run\" hook stop_outcome || true",
|
|
84
|
-
"timeout":
|
|
84
|
+
"timeout": 10
|
|
85
85
|
}
|
|
86
86
|
]
|
|
87
87
|
}
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
{
|
|
94
94
|
"type": "command",
|
|
95
95
|
"command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/slm-run\" hook before_web || true",
|
|
96
|
-
"timeout":
|
|
96
|
+
"timeout": 5
|
|
97
97
|
}
|
|
98
98
|
]
|
|
99
99
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"superlocalmemory": {
|
|
4
|
+
"args": [
|
|
5
|
+
"--from",
|
|
6
|
+
"superlocalmemory==4.1.22",
|
|
7
|
+
"slm",
|
|
8
|
+
"mcp"
|
|
9
|
+
],
|
|
10
|
+
"command": "uvx",
|
|
11
|
+
"env": {
|
|
12
|
+
"SLM_AGENT_ID": "cursor_plugin",
|
|
13
|
+
"SLM_MAX_EMBEDDING_WORKERS": "1",
|
|
14
|
+
"SLM_MCP_PROFILE": "core",
|
|
15
|
+
"SLM_NON_INTERACTIVE": "1",
|
|
16
|
+
"SLM_RERANKER_ENABLED": "false",
|
|
17
|
+
"SLM_RERANKER_IDLE_TIMEOUT": "120",
|
|
18
|
+
"TOKENIZERS_PARALLELISM": "false",
|
|
19
|
+
"UV_TORCH_BACKEND": "cpu"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# CPU-only torch pin for the Linux-no-GPU path (ensure-venv.sh).
|
|
2
|
+
#
|
|
3
|
+
# Installed from https://download.pytorch.org/whl/cpu, which publishes the
|
|
4
|
+
# same public version with a `+cpu` local segment — PEP 440 ignores the local
|
|
5
|
+
# segment when matching a bare `==X.Y.Z` specifier, so this satisfies the
|
|
6
|
+
# `torch==X.Y.Z` requirement that requirements.txt pulls in transitively via
|
|
7
|
+
# superlocalmemory, without ever contacting the CUDA-wheel-carrying default
|
|
8
|
+
# index for it.
|
|
9
|
+
#
|
|
10
|
+
# Kept equal to pyproject.toml's `torch==` dependency by
|
|
11
|
+
# tests/test_plugin_src/test_torch_cpu_pin_matches_pyproject.py — bump both
|
|
12
|
+
# together, not just this file.
|
|
13
|
+
torch==2.13.0
|
package/plugin/requirements.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
superlocalmemory==4.1.
|
|
1
|
+
superlocalmemory==4.1.22
|
|
@@ -60,18 +60,36 @@ fi
|
|
|
60
60
|
# Paths
|
|
61
61
|
# ---------------------------------------------------------------------------
|
|
62
62
|
REQ="${CLAUDE_PLUGIN_ROOT}/requirements.txt"
|
|
63
|
+
TORCH_CPU_PIN_FILE="${CLAUDE_PLUGIN_ROOT}/requirements-cpu-torch.txt"
|
|
63
64
|
VENV="${CLAUDE_PLUGIN_DATA}/venv"
|
|
64
65
|
SENTINEL="${CLAUDE_PLUGIN_DATA}/.venv-reqs.sha256"
|
|
65
66
|
VENV_TMP="${CLAUDE_PLUGIN_DATA}/venv.tmp"
|
|
66
67
|
|
|
67
68
|
# ---------------------------------------------------------------------------
|
|
68
|
-
# Compute sha256 of requirements.txt
|
|
69
|
+
# Compute sha256 of requirements.txt, and of the CPU-torch pin file when it
|
|
70
|
+
# exists (a pin bump alone must also trigger a rebuild).
|
|
71
|
+
# (cross-platform: prefer sha256sum, fall back to shasum -a 256)
|
|
69
72
|
# ---------------------------------------------------------------------------
|
|
73
|
+
_sha256_of() {
|
|
74
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
75
|
+
sha256sum "$1" | awk '{print $1}'
|
|
76
|
+
else
|
|
77
|
+
shasum -a 256 "$1" | awk '{print $1}'
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
|
|
70
81
|
hash_req() {
|
|
82
|
+
local combined
|
|
83
|
+
combined="$(_sha256_of "${REQ}")"
|
|
84
|
+
if [ -f "${TORCH_CPU_PIN_FILE}" ]; then
|
|
85
|
+
combined="${combined}:$(_sha256_of "${TORCH_CPU_PIN_FILE}")"
|
|
86
|
+
fi
|
|
87
|
+
# Fold the (possibly combined) digest down to one hash again so the
|
|
88
|
+
# sentinel stays a single opaque token either way.
|
|
71
89
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
72
|
-
|
|
90
|
+
printf '%s' "${combined}" | sha256sum | awk '{print $1}'
|
|
73
91
|
else
|
|
74
|
-
|
|
92
|
+
printf '%s' "${combined}" | shasum -a 256 | awk '{print $1}'
|
|
75
93
|
fi
|
|
76
94
|
}
|
|
77
95
|
|
|
@@ -108,6 +126,30 @@ python3 -m venv "${VENV_TMP}"
|
|
|
108
126
|
# Upgrade pip first (prefer binary to avoid source builds)
|
|
109
127
|
"${VENV_TMP}/bin/pip" install --upgrade pip --prefer-binary --quiet
|
|
110
128
|
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# CPU-only torch on CPU-only Linux. Plain PyPI resolution of the
|
|
131
|
+
# `torch==X.Y.Z` requirement that requirements.txt pulls in transitively
|
|
132
|
+
# installs the CUDA build on Linux (torch + triton + ~18 nvidia-* packages,
|
|
133
|
+
# about 2.6 GiB) even when there is no GPU to use it. Pre-installing the
|
|
134
|
+
# pinned version from the official CPU index first means the later
|
|
135
|
+
# `pip install -r requirements.txt` below finds it already satisfied and
|
|
136
|
+
# never touches the CUDA wheels. macOS, Windows, a host with a GPU, and a
|
|
137
|
+
# user who already set SLM_TORCH_BACKEND or their own pip index are all left
|
|
138
|
+
# exactly as before (see torch-cpu-resolve.sh).
|
|
139
|
+
# shellcheck source=torch-cpu-resolve.sh
|
|
140
|
+
. "$(dirname "${BASH_SOURCE[0]}")/torch-cpu-resolve.sh"
|
|
141
|
+
if torch_cpu_should_force; then
|
|
142
|
+
if TORCH_PIN="$(torch_cpu_pin "${TORCH_CPU_PIN_FILE}")"; then
|
|
143
|
+
echo "SLM plugin: Linux, no GPU detected — installing ${TORCH_PIN} from ${TORCH_CPU_INDEX_URL} (set SLM_TORCH_BACKEND=cuda to opt out)." >&2
|
|
144
|
+
"${VENV_TMP}/bin/pip" install \
|
|
145
|
+
--require-virtualenv \
|
|
146
|
+
--prefer-binary \
|
|
147
|
+
--quiet \
|
|
148
|
+
--index-url "${TORCH_CPU_INDEX_URL}" \
|
|
149
|
+
"${TORCH_PIN}"
|
|
150
|
+
fi
|
|
151
|
+
fi
|
|
152
|
+
|
|
111
153
|
# Install requirements
|
|
112
154
|
"${VENV_TMP}/bin/pip" install \
|
|
113
155
|
--require-virtualenv \
|
|
@@ -25,17 +25,61 @@
|
|
|
25
25
|
# plugin — require the plugin's own venv
|
|
26
26
|
# <path> — an explicit slm binary (leading ~ expanded)
|
|
27
27
|
# SLM_DATA_DIR SLM data dir (default: ~/.superlocalmemory)
|
|
28
|
-
# CLAUDE_PLUGIN_DATA where a plugin-owned venv lives
|
|
28
|
+
# CLAUDE_PLUGIN_DATA where a plugin-owned venv lives (then CURSOR_PLUGIN_DATA,
|
|
29
|
+
# PLUGIN_DATA, then ~/.local/share/superlocalmemory-plugin)
|
|
30
|
+
# SLM_LAUNCHER_BOOTSTRAP=1 build that venv here when nothing else can run
|
|
31
|
+
#
|
|
32
|
+
# HOSTS THAT DO NOT EXPAND THEIR VARIABLES
|
|
33
|
+
#
|
|
34
|
+
# Some hosts (Grok Bot's plugin computer) pass ${CLAUDE_PLUGIN_ROOT} and
|
|
35
|
+
# ${CLAUDE_PLUGIN_DATA} through literally. So this script finds itself from
|
|
36
|
+
# BASH_SOURCE, and treats any value still containing "${" as unset.
|
|
37
|
+
#
|
|
38
|
+
# Which slm runs, in order: SLM_LAUNCHER; an slm on PATH; the plugin venv, if
|
|
39
|
+
# built; uvx, pinned to this plugin's release (CPU-only torch on Linux); and,
|
|
40
|
+
# only with SLM_LAUNCHER_BOOTSTRAP=1, a plugin venv built on the spot.
|
|
41
|
+
# The daemon is started first and `slm serve start` waits (up to 60 s) for its
|
|
42
|
+
# /health, so the first remember after a cold start is not "daemon unavailable".
|
|
29
43
|
#
|
|
30
44
|
# Deliberately NOT set here: the MCP profile. Which tools a host exposes is the
|
|
31
45
|
# user's decision, and a plugin that narrows it takes away tools they configured.
|
|
32
46
|
|
|
47
|
+
# Where this script lives. Never from ${CLAUDE_PLUGIN_ROOT}: not every host sets it.
|
|
48
|
+
_slm_self="${BASH_SOURCE[0]}"
|
|
49
|
+
while [ -h "${_slm_self}" ]; do
|
|
50
|
+
_slm_link="$(readlink "${_slm_self}")"
|
|
51
|
+
case "${_slm_link}" in
|
|
52
|
+
/*) _slm_self="${_slm_link}" ;;
|
|
53
|
+
*) _slm_self="$(dirname "${_slm_self}")/${_slm_link}" ;;
|
|
54
|
+
esac
|
|
55
|
+
done
|
|
56
|
+
_SLM_SCRIPTS="$(cd "$(dirname "${_slm_self}")" && pwd)"
|
|
57
|
+
_SLM_PLUGIN_ROOT="$(dirname "${_SLM_SCRIPTS}")"
|
|
58
|
+
|
|
59
|
+
# A value that is empty, or a placeholder the host never replaced, is not a value.
|
|
60
|
+
_slm_usable() {
|
|
61
|
+
case "$1" in
|
|
62
|
+
''|*'${'*) return 1 ;;
|
|
63
|
+
esac
|
|
64
|
+
return 0
|
|
65
|
+
}
|
|
66
|
+
[ -n "${HOME:-}" ] || { HOME="$(cd ~ 2>/dev/null && pwd)"; export HOME; }
|
|
67
|
+
_slm_usable "${SLM_DATA_DIR:-}" || unset SLM_DATA_DIR
|
|
68
|
+
_slm_usable "${SLM_LAUNCHER:-}" || unset SLM_LAUNCHER
|
|
69
|
+
if ! _slm_usable "${CLAUDE_PLUGIN_DATA:-}"; then
|
|
70
|
+
unset CLAUDE_PLUGIN_DATA
|
|
71
|
+
for _slm_d in "${CURSOR_PLUGIN_DATA:-}" "${PLUGIN_DATA:-}" \
|
|
72
|
+
"${XDG_DATA_HOME:-${HOME}/.local/share}/superlocalmemory-plugin"; do
|
|
73
|
+
if _slm_usable "${_slm_d}"; then CLAUDE_PLUGIN_DATA="${_slm_d}"; break; fi
|
|
74
|
+
done
|
|
75
|
+
fi
|
|
76
|
+
|
|
33
77
|
_SLM_DATA="${SLM_DATA_DIR:-$HOME/.superlocalmemory}"
|
|
34
78
|
|
|
35
79
|
# Which slm to run is decided in ONE place, shared with the hooks and the venv
|
|
36
80
|
# bootstrap, so the three can never disagree about which install is in play.
|
|
37
81
|
# shellcheck source=slm-resolve.sh
|
|
38
|
-
. "$
|
|
82
|
+
. "${_SLM_SCRIPTS}/slm-resolve.sh"
|
|
39
83
|
_PLUGIN_BIN="$(slm_plugin_bin)"
|
|
40
84
|
|
|
41
85
|
# Is a daemon already up for this data dir? If so we join it rather than boot a
|
|
@@ -46,10 +90,45 @@ if [ -f "${_SLM_DATA}/daemon.pid" ] \
|
|
|
46
90
|
_daemon_running=1
|
|
47
91
|
fi
|
|
48
92
|
|
|
49
|
-
# Start a daemon with the chosen
|
|
93
|
+
# Start a daemon with the chosen command unless one is already serving.
|
|
50
94
|
_ensure_daemon() {
|
|
51
95
|
[ "${_daemon_running}" -eq 1 ] && return 0
|
|
52
|
-
"
|
|
96
|
+
"$@" serve start >&2
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# The release this plugin tree ships (requirements.txt is stamped every release).
|
|
100
|
+
_slm_pin() {
|
|
101
|
+
sed -n 's/^\(superlocalmemory==[0-9][0-9A-Za-z.+!-]*\)[[:space:]]*$/\1/p' \
|
|
102
|
+
"${_SLM_PLUGIN_ROOT}/requirements.txt" 2>/dev/null | head -n 1
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# No installed slm and no plugin venv: run this release through uvx. Returns
|
|
106
|
+
# only when it cannot (no uvx, or no pin); otherwise it becomes the server.
|
|
107
|
+
_slm_run_uvx() {
|
|
108
|
+
local _pin
|
|
109
|
+
command -v uvx >/dev/null 2>&1 || return 1
|
|
110
|
+
_pin="$(_slm_pin)"
|
|
111
|
+
if [ -z "${_pin}" ]; then
|
|
112
|
+
echo "SLM plugin: not using uvx: no superlocalmemory pin in ${_SLM_PLUGIN_ROOT}/requirements.txt." >&2
|
|
113
|
+
return 1
|
|
114
|
+
fi
|
|
115
|
+
# PyPI's Linux torch is the CUDA build; macOS and Windows get CPU wheels anyway.
|
|
116
|
+
if [ "$(uname -s 2>/dev/null)" = "Linux" ]; then
|
|
117
|
+
export UV_TORCH_BACKEND="${UV_TORCH_BACKEND:-cpu}"
|
|
118
|
+
fi
|
|
119
|
+
export TOKENIZERS_PARALLELISM="${TOKENIZERS_PARALLELISM:-false}"
|
|
120
|
+
echo "SLM plugin: no installed slm; running ${_pin} through uvx (downloaded once, then cached)." >&2
|
|
121
|
+
_ensure_daemon uvx --from "${_pin}" slm || \
|
|
122
|
+
echo "SLM plugin: daemon did not start; the MCP server will retry on first use." >&2
|
|
123
|
+
exec uvx --from "${_pin}" slm mcp
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Opt-in: build the plugin venv now, with the same bootstrap SessionStart runs.
|
|
127
|
+
_slm_bootstrap() {
|
|
128
|
+
[ "${SLM_LAUNCHER_BOOTSTRAP:-0}" = "1" ] || return 1
|
|
129
|
+
CLAUDE_PLUGIN_ROOT="${_SLM_PLUGIN_ROOT}" CLAUDE_PLUGIN_DATA="${CLAUDE_PLUGIN_DATA}" \
|
|
130
|
+
bash "${_SLM_SCRIPTS}/ensure-venv.sh" >&2 || return 1
|
|
131
|
+
[ -x "${SLM_CHOICE_BIN}" ]
|
|
53
132
|
}
|
|
54
133
|
|
|
55
134
|
if ! slm_choose; then
|
|
@@ -88,12 +167,17 @@ case "${SLM_LAUNCHER:-auto}:${SLM_CHOICE}" in
|
|
|
88
167
|
;;
|
|
89
168
|
auto:plugin)
|
|
90
169
|
# No system install. Use the plugin's own venv, once SessionStart has
|
|
91
|
-
# built it.
|
|
92
|
-
#
|
|
170
|
+
# built it. A host that runs no hooks never builds it: then uvx, then
|
|
171
|
+
# (opt-in) a venv built here. If none of that works, say what was
|
|
172
|
+
# looked for, so the fix is obvious.
|
|
93
173
|
if [ "${SLM_CHOICE_READY}" -ne 1 ]; then
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
174
|
+
_slm_run_uvx
|
|
175
|
+
if ! _slm_bootstrap; then
|
|
176
|
+
slm_not_found_message >&2
|
|
177
|
+
echo >&2
|
|
178
|
+
echo " Or install uv (https://docs.astral.sh/uv/): the plugin then runs SuperLocalMemory through uvx." >&2
|
|
179
|
+
exit 1
|
|
180
|
+
fi
|
|
97
181
|
fi
|
|
98
182
|
if ! _ensure_daemon "${SLM_CHOICE_BIN}"; then
|
|
99
183
|
echo "SLM plugin: unable to start the owned daemon; refusing a direct MCP writer." >&2
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# torch-cpu-resolve.sh — should this install force a CPU-only torch wheel?
|
|
3
|
+
#
|
|
4
|
+
# PyPI ships CPU-only torch wheels for macOS and Windows, but on Linux the
|
|
5
|
+
# plain `torch==X.Y.Z` wheel pulls the CUDA runtime plus ~18 nvidia-* packages
|
|
6
|
+
# (triton included) — about 2.6 GiB on a host that may have no GPU at all
|
|
7
|
+
# (reference: https://docs.astral.sh/uv/guides/integration/pytorch/, which
|
|
8
|
+
# documents the same PyPI default for `uv pip`; plain pip resolves the same
|
|
9
|
+
# index). The official CPU-only index is documented at
|
|
10
|
+
# https://pytorch.org/get-started/locally/ as:
|
|
11
|
+
# pip install torch --index-url https://download.pytorch.org/whl/cpu
|
|
12
|
+
#
|
|
13
|
+
# Sourced by ensure-venv.sh (never run directly). Pure decision logic — makes
|
|
14
|
+
# makes no HTTP request and runs no pip — so it is unit-testable without the 2 GiB
|
|
15
|
+
# download it exists to avoid (tests/test_plugin_src/test_torch_cpu_resolve.sh).
|
|
16
|
+
#
|
|
17
|
+
# Environment (all optional):
|
|
18
|
+
# SLM_TORCH_BACKEND cpu — force the CPU index regardless of GPU detection
|
|
19
|
+
# <anything else, e.g. cuda/cu124/auto> — leave pip's
|
|
20
|
+
# default resolution alone; the user has already decided
|
|
21
|
+
# PIP_INDEX_URL / PIP_EXTRA_INDEX_URL — if the user already points pip at
|
|
22
|
+
# their own index, this never overrides it
|
|
23
|
+
#
|
|
24
|
+
# Safe under `set -euo pipefail`: every variable read has a default.
|
|
25
|
+
|
|
26
|
+
TORCH_CPU_INDEX_URL="https://download.pytorch.org/whl/cpu"
|
|
27
|
+
|
|
28
|
+
# torch_cpu_should_force — exit 0 (true, in bash's backwards convention) when
|
|
29
|
+
# this host should get the CPU-only wheel instead of whatever pip would
|
|
30
|
+
# otherwise pick. False (exit 1) covers: not Linux, a GPU is visible, or the
|
|
31
|
+
# user already steered pip (their own index) or torch (SLM_TORCH_BACKEND set
|
|
32
|
+
# to anything but "cpu") themselves.
|
|
33
|
+
torch_cpu_should_force() {
|
|
34
|
+
local platform
|
|
35
|
+
platform="$(uname -s 2>/dev/null || echo unknown)"
|
|
36
|
+
if [ "${platform}" != "Linux" ]; then
|
|
37
|
+
# macOS (Darwin) already gets CPU-only wheels from plain PyPI; Windows
|
|
38
|
+
# too. Nothing to override there.
|
|
39
|
+
return 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
case "${SLM_TORCH_BACKEND:-}" in
|
|
43
|
+
cpu)
|
|
44
|
+
return 0
|
|
45
|
+
;;
|
|
46
|
+
"")
|
|
47
|
+
: # unset — fall through to auto-detection below
|
|
48
|
+
;;
|
|
49
|
+
*)
|
|
50
|
+
# cuda / cuXXX / auto / anything else: the user already decided.
|
|
51
|
+
return 1
|
|
52
|
+
;;
|
|
53
|
+
esac
|
|
54
|
+
|
|
55
|
+
if [ -n "${PIP_INDEX_URL:-}" ] || [ -n "${PIP_EXTRA_INDEX_URL:-}" ]; then
|
|
56
|
+
# The user already pointed pip somewhere specific. Do not fight it.
|
|
57
|
+
return 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if command -v nvidia-smi >/dev/null 2>&1; then
|
|
61
|
+
return 1
|
|
62
|
+
fi
|
|
63
|
+
if [ -e /proc/driver/nvidia/version ] || [ -e /dev/nvidia0 ]; then
|
|
64
|
+
return 1
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
return 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# torch_cpu_pin <pin_file> — print the "torch==X.Y.Z" line from pin_file.
|
|
71
|
+
# Returns 1 (and prints nothing) when the file is missing or has no such
|
|
72
|
+
# line: fail-open, so a missing/stale pin file can never block an install
|
|
73
|
+
# that would otherwise succeed — it just means pip falls back to its
|
|
74
|
+
# ordinary resolution (the CUDA wheel on Linux) for this run.
|
|
75
|
+
torch_cpu_pin() {
|
|
76
|
+
local pin_file="$1"
|
|
77
|
+
if [ ! -f "${pin_file}" ]; then
|
|
78
|
+
return 1
|
|
79
|
+
fi
|
|
80
|
+
local line
|
|
81
|
+
line="$(grep -m1 '^torch==' "${pin_file}" 2>/dev/null || true)"
|
|
82
|
+
if [ -z "${line}" ]; then
|
|
83
|
+
return 1
|
|
84
|
+
fi
|
|
85
|
+
printf '%s\n' "${line}"
|
|
86
|
+
}
|