memtrace 0.3.34 → 0.3.35

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.
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+
3
+ // Skill frontmatter upgrader.
4
+ //
5
+ // Anthropic's Claude Code docs (code.claude.com/docs/en/skills.md) name two
6
+ // frontmatter fields that drive auto-invocation reliability:
7
+ // * `description` — leads with the use case, ideally in user-prompt language
8
+ // * `when_to_use` — appended to description, holds concrete trigger phrases
9
+ //
10
+ // Combined cap is 1,536 chars. Our skills shipped with `description` only,
11
+ // leading with "Always use…" — strong directive but missing the trigger-phrase
12
+ // signal Claude reaches for when deciding whether to auto-invoke.
13
+ //
14
+ // This module:
15
+ // 1. parses a skill file's YAML frontmatter without depending on a yaml lib
16
+ // (the frontmatter we ship is a tightly-controlled subset — top-level
17
+ // key/value pairs plus one `allowed-tools` list)
18
+ // 2. upgrades it: adds `when_to_use`, adds `paths`, leaves existing keys
19
+ // untouched
20
+ // 3. round-trips byte-identical for a skill that's already been upgraded
21
+ //
22
+ // Tested in test/skill-metadata.test.js.
23
+
24
+ // ── Parser ────────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Parse a skill markdown file with YAML frontmatter.
28
+ *
29
+ * @param {string} content
30
+ * @returns {{frontmatter: object, frontmatterRaw: string, body: string}}
31
+ */
32
+ function parseSkillFile(content) {
33
+ if (typeof content !== "string") {
34
+ throw new TypeError("parseSkillFile: content must be a string");
35
+ }
36
+ if (!content.startsWith("---")) {
37
+ return { frontmatter: {}, frontmatterRaw: "", body: content };
38
+ }
39
+ // Find the closing `---` for the frontmatter.
40
+ const endMarker = content.indexOf("\n---", 3);
41
+ if (endMarker === -1) {
42
+ return { frontmatter: {}, frontmatterRaw: "", body: content };
43
+ }
44
+ const frontmatterRaw = content.slice(3, endMarker).replace(/^\n/, "");
45
+ // Body starts after `\n---\n`. Some files have `\n---\r\n`; handle both.
46
+ const afterClose = endMarker + 4;
47
+ let bodyStart = afterClose;
48
+ if (content[afterClose] === "\r") bodyStart += 1;
49
+ if (content[bodyStart] === "\n") bodyStart += 1;
50
+ const body = content.slice(bodyStart);
51
+ const frontmatter = parseFrontmatter(frontmatterRaw);
52
+ return { frontmatter, frontmatterRaw, body };
53
+ }
54
+
55
+ /**
56
+ * Minimal YAML subset parser for our skills' frontmatter shape.
57
+ * Handles:
58
+ * - quoted and unquoted scalar values
59
+ * - YAML list values (`allowed-tools:` followed by ` - item` lines)
60
+ * - keeps unknown keys intact in insertion order
61
+ *
62
+ * @param {string} raw
63
+ * @returns {Record<string, string|string[]|boolean>}
64
+ */
65
+ function parseFrontmatter(raw) {
66
+ const out = {};
67
+ const lines = raw.split(/\r?\n/);
68
+ let i = 0;
69
+ while (i < lines.length) {
70
+ const line = lines[i];
71
+ if (!line || /^\s*#/.test(line)) {
72
+ i++;
73
+ continue;
74
+ }
75
+ const m = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
76
+ if (!m) {
77
+ i++;
78
+ continue;
79
+ }
80
+ const key = m[1];
81
+ const valueRaw = m[2];
82
+ if (valueRaw === "") {
83
+ // Look ahead for a list
84
+ const list = [];
85
+ i++;
86
+ while (i < lines.length && /^\s+-\s+/.test(lines[i])) {
87
+ list.push(lines[i].replace(/^\s+-\s+/, "").trim());
88
+ i++;
89
+ }
90
+ out[key] = list;
91
+ continue;
92
+ }
93
+ out[key] = parseScalar(valueRaw);
94
+ i++;
95
+ }
96
+ return out;
97
+ }
98
+
99
+ function parseScalar(raw) {
100
+ const t = raw.trim();
101
+ if (t === "true") return true;
102
+ if (t === "false") return false;
103
+ if ((t.startsWith('"') && t.endsWith('"')) ||
104
+ (t.startsWith("'") && t.endsWith("'"))) {
105
+ return t.slice(1, -1);
106
+ }
107
+ return t;
108
+ }
109
+
110
+ // ── Serializer ────────────────────────────────────────────────────────
111
+
112
+ /**
113
+ * Serialize back to `--- ... ---\n\nbody`. Order:
114
+ * 1. name (always first if present)
115
+ * 2. description
116
+ * 3. when_to_use
117
+ * 4. paths
118
+ * 5. allowed-tools (list)
119
+ * 6. user-invocable
120
+ * 7. anything else, in original order
121
+ *
122
+ * Quoting rule: scalar values containing colons, hashes, or newlines get
123
+ * double-quoted; lists and bare identifiers don't.
124
+ *
125
+ * @param {{frontmatter: object, body: string}} parsed
126
+ * @returns {string}
127
+ */
128
+ function serializeSkill(parsed) {
129
+ const fm = parsed.frontmatter || {};
130
+ const body = parsed.body || "";
131
+ const orderedKeys = [
132
+ "name",
133
+ "description",
134
+ "when_to_use",
135
+ "paths",
136
+ "allowed-tools",
137
+ "user-invocable",
138
+ ];
139
+ const seen = new Set();
140
+ const lines = ["---"];
141
+ for (const k of orderedKeys) {
142
+ if (k in fm) {
143
+ lines.push(...formatPair(k, fm[k]));
144
+ seen.add(k);
145
+ }
146
+ }
147
+ for (const k of Object.keys(fm)) {
148
+ if (!seen.has(k)) {
149
+ lines.push(...formatPair(k, fm[k]));
150
+ }
151
+ }
152
+ lines.push("---");
153
+ return lines.join("\n") + (body.startsWith("\n") ? "" : "\n") + body;
154
+ }
155
+
156
+ function formatPair(key, value) {
157
+ if (Array.isArray(value)) {
158
+ return [`${key}:`, ...value.map((v) => ` - ${v}`)];
159
+ }
160
+ if (typeof value === "boolean") {
161
+ return [`${key}: ${value}`];
162
+ }
163
+ const s = String(value);
164
+ if (s.includes(":") || s.includes("#") || s.includes("\n") || s.includes('"')) {
165
+ return [`${key}: "${s.replace(/"/g, '\\"')}"`];
166
+ }
167
+ return [`${key}: ${s}`];
168
+ }
169
+
170
+ // ── Upgrader ──────────────────────────────────────────────────────────
171
+
172
+ /**
173
+ * Upgrade a skill's frontmatter to v0.3.35 best practices.
174
+ *
175
+ * - Adds `when_to_use` from the trigger map (does not overwrite existing).
176
+ * - Adds `paths` (does not overwrite existing).
177
+ * - Leaves `description` as-is — operators are responsible for reframing
178
+ * the description text manually since it requires per-skill judgement.
179
+ *
180
+ * @param {object} parsed output of parseSkillFile
181
+ * @param {string} skillName e.g. "memtrace-first"
182
+ * @param {object} triggers { [skillName]: { whenToUse, paths } }
183
+ * @returns {object} updated parsed
184
+ */
185
+ function upgradeSkill(parsed, skillName, triggers) {
186
+ const fm = { ...(parsed.frontmatter || {}) };
187
+ const t = triggers[skillName];
188
+ if (!t) {
189
+ return parsed; // unknown skill — leave alone
190
+ }
191
+ if (!fm.when_to_use && t.whenToUse) {
192
+ fm.when_to_use = t.whenToUse;
193
+ }
194
+ if (!fm.paths && t.paths) {
195
+ fm.paths = t.paths;
196
+ }
197
+ return { ...parsed, frontmatter: fm };
198
+ }
199
+
200
+ // ── Default trigger map ───────────────────────────────────────────────
201
+ //
202
+ // Concrete trigger phrases per skill. The pool is biased toward language
203
+ // users actually type, plus the imperatives the agent's planner uses
204
+ // internally ("trace through", "find the function that", etc.).
205
+
206
+ const CODE_PATHS = "**/*.{py,js,jsx,ts,tsx,mjs,cjs,rs,go,java,rb,c,cc,cpp,cxx,h,hpp,hh,cs,php,swift,kt,kts,scala,clj,cljs,ex,exs,erl,hrl,ml,mli,fs,fsx,r,jl,lua,dart,m,mm,asm,sql,gql,graphql,proto,sh,bash,zsh,fish,vue,svelte}";
207
+
208
+ const DEFAULT_TRIGGERS = {
209
+ "memtrace-first": {
210
+ whenToUse:
211
+ "Triggered when user says 'where is', 'how does', 'what calls', 'why does', 'find the function that', 'trace through', 'show me', 'explain this code', 'investigate', 'debug', 'understand', 'audit', 'fix bug', 'refactor', 'review', 'check for'. Use BEFORE Read/Grep/Glob/find/rg for any code-discovery question in an indexed repo. Treat zero results as a query-broadening or reindex prompt — never as permission to grep.",
212
+ paths: CODE_PATHS,
213
+ },
214
+ "memtrace-search": {
215
+ whenToUse:
216
+ "Triggered by 'find function', 'where is X defined', 'locate', 'look up', 'search for', 'find class', 'find type', 'find struct', 'find interface'. Also when user references a constant, error string, or magic value and asks where it's used.",
217
+ paths: CODE_PATHS,
218
+ },
219
+ "memtrace-relationships": {
220
+ whenToUse:
221
+ "Triggered by 'who calls', 'callers of', 'callees', 'what does X call', 'who imports', 'where is X used', 'references to', 'inheritance', 'overrides', 'implementations of'. Use to walk the call graph rather than greping for symbol names.",
222
+ paths: CODE_PATHS,
223
+ },
224
+ "memtrace-impact": {
225
+ whenToUse:
226
+ "Triggered by 'what breaks if', 'blast radius', 'impact of changing', 'safe to remove', 'safe to rename', 'dependencies on', 'who depends on', 'before refactor', 'before delete', 'risk of changing'.",
227
+ paths: CODE_PATHS,
228
+ },
229
+ "memtrace-evolution": {
230
+ whenToUse:
231
+ "Triggered by 'when did this change', 'what changed in', 'recent changes', 'evolution of', 'history of', 'who modified', 'why does this look like', 'before refactor X', 'after Y was added', 'last week's changes'.",
232
+ paths: CODE_PATHS,
233
+ },
234
+ "memtrace-cochange": {
235
+ whenToUse:
236
+ "Triggered by 'what changes together', 'co-change', 'historical coupling', 'hidden dependency', 'usually moves with', 'should I also touch', 'what else needs updating'.",
237
+ paths: CODE_PATHS,
238
+ },
239
+ "memtrace-graph": {
240
+ whenToUse:
241
+ "Triggered by 'most important', 'central to', 'critical functions', 'chokepoints', 'bridges', 'communities', 'modules', 'subsystems', 'hubs', 'depends on most', 'most depended on', 'architecture overview'.",
242
+ paths: CODE_PATHS,
243
+ },
244
+ "memtrace-quality": {
245
+ whenToUse:
246
+ "Triggered by 'dead code', 'unused functions', 'zero callers', 'complex functions', 'hotspots', 'cyclomatic complexity', 'cognitive complexity', 'code smells', 'refactoring candidates', 'tech debt'.",
247
+ paths: CODE_PATHS,
248
+ },
249
+ "memtrace-api-topology": {
250
+ whenToUse:
251
+ "Triggered by 'API endpoints', 'HTTP routes', 'who calls /endpoint', 'cross-service calls', 'service dependency', 'route handlers', 'fetch calls', 'REST surface', 'service diagram', 'call from frontend to backend'.",
252
+ paths: CODE_PATHS,
253
+ },
254
+ "memtrace-index": {
255
+ whenToUse:
256
+ "Triggered by 'index this repo', 'add to memtrace', 'parse this codebase', 'reindex', 'watch directory', 'enable live indexing', 'set up memtrace for'. Use when bringing a new project under code-intelligence coverage.",
257
+ paths: CODE_PATHS,
258
+ },
259
+ "memtrace-change-impact-analysis": {
260
+ whenToUse:
261
+ "Triggered before edits, refactors, API changes, renames, removals, or PR review when user asks about 'what will break', 'what does this affect', 'is this safe', 'callers I need to update', 'before I merge'. Use to combine search + impact + change detection.",
262
+ paths: CODE_PATHS,
263
+ },
264
+ "memtrace-codebase-exploration": {
265
+ whenToUse:
266
+ "Triggered by 'tour of this codebase', 'how is this organized', 'where do I start', 'onboard me', 'explain the architecture', 'main flows', 'main modules', 'key classes', 'I just cloned this'.",
267
+ paths: CODE_PATHS,
268
+ },
269
+ "memtrace-continuous-memory": {
270
+ whenToUse:
271
+ "Triggered by 'watch this folder', 'live indexing', 'incremental memtrace', 'always-on memory', 'keep up to date', 'pick up my saves', 'reflect uncommitted changes', 'why isn't memtrace seeing my edits'.",
272
+ paths: CODE_PATHS,
273
+ },
274
+ "memtrace-episode-replay": {
275
+ whenToUse:
276
+ "Triggered by 'replay history', 'why does this look like this', 'evolution of this design', 'past attempts at', 'we tried X before', 'reverted approach', 'how did we get here'.",
277
+ paths: CODE_PATHS,
278
+ },
279
+ "memtrace-incident-investigation": {
280
+ whenToUse:
281
+ "Triggered by 'production broke', 'incident', 'regression', 'something is failing', 'why is X slow', 'root cause', 'what changed when', 'last working state', 'bisect', 'broken since deploy'.",
282
+ paths: CODE_PATHS,
283
+ },
284
+ "memtrace-refactoring-guide": {
285
+ whenToUse:
286
+ "Triggered by 'refactor this', 'reduce complexity', 'split this function', 'extract module', 'clean up tech debt', 'reorganize', 'where should I start refactoring', 'priority for cleanup'.",
287
+ paths: CODE_PATHS,
288
+ },
289
+ "memtrace-session-continuity": {
290
+ whenToUse:
291
+ "Triggered at session start or resume — 'catch me up', 'what changed while I was away', 'resume', 'pick up where I left off', 'recap', 'orient', 'summary since yesterday', 'since last commit'.",
292
+ paths: CODE_PATHS,
293
+ },
294
+ };
295
+
296
+ module.exports = {
297
+ parseSkillFile,
298
+ parseFrontmatter,
299
+ serializeSkill,
300
+ upgradeSkill,
301
+ DEFAULT_TRIGGERS,
302
+ CODE_PATHS,
303
+ };
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+
3
+ // Shared spawn helpers for the npm shim.
4
+ //
5
+ // Why this exists: Node 18.20+ / 20.12+ / 21.7+ refuse to spawn `.cmd`
6
+ // and `.bat` files on Windows without `shell: true`, as part of the
7
+ // CVE-2024-27980 mitigation. The release notes from Node phrase this
8
+ // as a "policy change" but in practice it surfaces as
9
+ // `Error: spawn npm.cmd EINVAL` on first install — which is exactly
10
+ // what Orbit hit on Windows during a fresh global install.
11
+ //
12
+ // The fix is small: when targeting `.cmd` / `.bat` on Windows, pass
13
+ // `shell: true`. macOS and Linux don't need it (npm is a real binary
14
+ // on PATH there).
15
+ //
16
+ // Pulled into its own module so we can TDD it without spawning real
17
+ // processes. The unit + property tests live in
18
+ // `npm/memtrace/test/spawn-helper.test.js`.
19
+
20
+ /**
21
+ * Pick the platform-specific binary name for a tool that ships as a
22
+ * `.cmd` shim on Windows. Used for `npm`, `memtrace` itself, and
23
+ * anything else that has the same dual-name shape.
24
+ *
25
+ * @param {string} unixName - the bare tool name on macOS/Linux ("npm")
26
+ * @param {string} platform - usually `process.platform`
27
+ * @returns {string} - "npm" or "npm.cmd"
28
+ */
29
+ function platformBinary(unixName, platform) {
30
+ if (typeof unixName !== "string" || unixName.length === 0) {
31
+ throw new TypeError("platformBinary: unixName must be a non-empty string");
32
+ }
33
+ return platform === "win32" ? unixName + ".cmd" : unixName;
34
+ }
35
+
36
+ /**
37
+ * Build a spawn options object that's safe for the given platform.
38
+ *
39
+ * On Windows, returns options with `shell: true` set, which is
40
+ * mandatory for spawning `.cmd` / `.bat` files since the
41
+ * CVE-2024-27980 mitigation. On non-Windows platforms, returns the
42
+ * options unchanged so we don't accidentally re-tokenise arguments
43
+ * through a shell that doesn't need it.
44
+ *
45
+ * Pure: does not mutate `baseOpts`. Always returns a new object.
46
+ *
47
+ * @param {string} platform - usually `process.platform`
48
+ * @param {object} [baseOpts] - any options to merge in
49
+ * @returns {object}
50
+ */
51
+ function spawnOptionsForPlatform(platform, baseOpts) {
52
+ const base = baseOpts && typeof baseOpts === "object" ? baseOpts : {};
53
+ const merged = Object.assign({}, base);
54
+ if (platform === "win32") {
55
+ merged.shell = true;
56
+ }
57
+ return merged;
58
+ }
59
+
60
+ module.exports = {
61
+ platformBinary,
62
+ spawnOptionsForPlatform,
63
+ };
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // One-shot upgrader: walks every skill file under both
5
+ // `npm/memtrace/skills/` and `npm/memtrace/installer/skills/` and
6
+ // adds `when_to_use` + `paths` from the trigger map. Idempotent;
7
+ // safe to re-run.
8
+ //
9
+ // Usage: node npm/memtrace/lib/upgrade-skills.js
10
+ //
11
+ // Exit code 0 on success.
12
+
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+ const {
16
+ parseSkillFile,
17
+ serializeSkill,
18
+ upgradeSkill,
19
+ DEFAULT_TRIGGERS,
20
+ } = require("./skill-metadata");
21
+
22
+ const ROOTS = [
23
+ path.join(__dirname, "..", "skills"),
24
+ path.join(__dirname, "..", "installer", "skills"),
25
+ ];
26
+
27
+ let totalScanned = 0;
28
+ let totalUpgraded = 0;
29
+ let totalSkipped = 0;
30
+
31
+ function walk(root) {
32
+ if (!fs.existsSync(root)) return;
33
+ const entries = fs.readdirSync(root, { withFileTypes: true });
34
+ for (const e of entries) {
35
+ const full = path.join(root, e.name);
36
+ if (e.isDirectory()) {
37
+ walk(full);
38
+ } else if (e.isFile() && e.name.endsWith(".md")) {
39
+ processSkillFile(full);
40
+ }
41
+ }
42
+ }
43
+
44
+ function processSkillFile(filePath) {
45
+ totalScanned++;
46
+ const original = fs.readFileSync(filePath, "utf8");
47
+ const parsed = parseSkillFile(original);
48
+ const skillName = parsed.frontmatter.name || path.basename(filePath, ".md");
49
+
50
+ if (!DEFAULT_TRIGGERS[skillName]) {
51
+ totalSkipped++;
52
+ return;
53
+ }
54
+
55
+ const upgraded = upgradeSkill(parsed, skillName, DEFAULT_TRIGGERS);
56
+ const serialized = serializeSkill(upgraded);
57
+
58
+ if (serialized === original) {
59
+ totalSkipped++;
60
+ return;
61
+ }
62
+
63
+ fs.writeFileSync(filePath, serialized);
64
+ totalUpgraded++;
65
+ console.log(`upgraded: ${path.relative(process.cwd(), filePath)}`);
66
+ }
67
+
68
+ for (const root of ROOTS) {
69
+ walk(root);
70
+ }
71
+
72
+ console.log(`\nscanned ${totalScanned} skill files · upgraded ${totalUpgraded} · skipped ${totalSkipped}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memtrace",
3
- "version": "0.3.34",
3
+ "version": "0.3.35",
4
4
  "description": "Code intelligence graph — MCP server + AI agent skills + visualization UI",
5
5
  "keywords": [
6
6
  "mcp",
@@ -22,6 +22,8 @@
22
22
  },
23
23
  "files": [
24
24
  "bin/",
25
+ "lib/",
26
+ "hooks/",
25
27
  "skills/",
26
28
  "installer/",
27
29
  "install.js",
@@ -37,9 +39,9 @@
37
39
  "fs-extra": "^11.0.0"
38
40
  },
39
41
  "optionalDependencies": {
40
- "@memtrace/darwin-arm64": "0.3.34",
41
- "@memtrace/linux-x64": "0.3.34",
42
- "@memtrace/win32-x64": "0.3.34"
42
+ "@memtrace/darwin-arm64": "0.3.35",
43
+ "@memtrace/linux-x64": "0.3.35",
44
+ "@memtrace/win32-x64": "0.3.35"
43
45
  },
44
46
  "engines": {
45
47
  "node": ">=18"