@pcircle/memesh 4.9.0 → 4.9.4

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 (50) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.de.md +1 -1
  5. package/README.md +2 -2
  6. package/README.zh-TW.md +2 -2
  7. package/dashboard/dist/index.html +10 -10
  8. package/dist/core/doctor-fixes.d.ts +19 -0
  9. package/dist/core/doctor-fixes.d.ts.map +1 -0
  10. package/dist/core/doctor-fixes.js +104 -0
  11. package/dist/core/doctor-fixes.js.map +1 -0
  12. package/dist/core/doctor.d.ts +1 -1
  13. package/dist/core/doctor.d.ts.map +1 -1
  14. package/dist/core/doctor.js +3 -3
  15. package/dist/core/doctor.js.map +1 -1
  16. package/dist/core/operations.d.ts.map +1 -1
  17. package/dist/core/operations.js +2 -2
  18. package/dist/core/operations.js.map +1 -1
  19. package/dist/core/schema-export.js +1 -1
  20. package/dist/core/schema-export.js.map +1 -1
  21. package/dist/knowledge-graph.d.ts.map +1 -1
  22. package/dist/knowledge-graph.js +30 -10
  23. package/dist/knowledge-graph.js.map +1 -1
  24. package/dist/mcp/THIRD_PARTY_NOTICES.txt +2 -2
  25. package/dist/mcp/server.js +45 -16
  26. package/dist/mcp/server.js.map +1 -1
  27. package/dist/skills-manifest.json +17 -12
  28. package/dist/transports/agent-messaging.d.ts +4 -0
  29. package/dist/transports/agent-messaging.d.ts.map +1 -1
  30. package/dist/transports/agent-messaging.js +12 -2
  31. package/dist/transports/agent-messaging.js.map +1 -1
  32. package/dist/transports/cli/cli.d.ts.map +1 -1
  33. package/dist/transports/cli/cli.js +60735 -1770
  34. package/dist/transports/cli/cli.js.map +6 -1
  35. package/dist/transports/http/server.d.ts.map +1 -1
  36. package/dist/transports/http/server.js +32 -1
  37. package/dist/transports/http/server.js.map +1 -1
  38. package/dist/transports/mcp/handlers.d.ts +3 -3
  39. package/dist/transports/mcp/handlers.js +3 -3
  40. package/dist/transports/mcp/handlers.js.map +1 -1
  41. package/docs/platforms/agent-messaging.md +10 -1
  42. package/hooks/hooks.json +1 -1
  43. package/package.json +6 -3
  44. package/scripts/check-plugin-hook-artifact.mjs +212 -0
  45. package/scripts/hooks/_shared.js +150 -1
  46. package/scripts/hooks/session-start.js +90 -4
  47. package/scripts/hooks/session-summary.js +17 -4
  48. package/scripts/hooks/user-prompt-intent.js +71 -11
  49. package/scripts/lib/npm-bin.mjs +123 -0
  50. package/scripts/upgrade-plugin.sh +24 -0
@@ -19,7 +19,62 @@
19
19
  // Gated by `autoCapture` flag (same as other memesh write hooks).
20
20
 
21
21
  import { pathToFileURL } from 'url';
22
- import { isAutoCaptureEnabled } from './_shared.js';
22
+ import {
23
+ findAutoUpdateConsent,
24
+ isAutoCaptureEnabled,
25
+ parseAutoUpdateConsent,
26
+ readUpdateCheckCache,
27
+ resolvePluginRoot,
28
+ writeAutoUpdateConsent,
29
+ } from './_shared.js';
30
+ import { join } from 'path';
31
+ import { existsSync, readFileSync } from 'fs';
32
+
33
+ let installChannelMod = null;
34
+ try {
35
+ const pluginRoot = resolvePluginRoot(import.meta.url);
36
+ const modulePath = join(pluginRoot, 'dist/core/install-channel.js');
37
+ if (existsSync(modulePath)) installChannelMod = await import(pathToFileURL(modulePath).href);
38
+ } catch {
39
+ // Source checkouts without a build remain non-blocking and cannot self-update.
40
+ }
41
+
42
+ function currentInstallChannel() {
43
+ try {
44
+ return installChannelMod?.getCurrentInstallChannel({
45
+ packageRoot: resolvePluginRoot(import.meta.url),
46
+ }) ?? 'unknown';
47
+ } catch {
48
+ return 'unknown';
49
+ }
50
+ }
51
+
52
+ function currentInstalledVersion() {
53
+ try {
54
+ const pkg = JSON.parse(readFileSync(join(resolvePluginRoot(import.meta.url), 'package.json'), 'utf8'));
55
+ return typeof pkg.version === 'string' ? pkg.version : null;
56
+ } catch { return null; }
57
+ }
58
+
59
+ function recordUpdateConsent(sessionId, prompt) {
60
+ const current = currentInstalledVersion();
61
+ if (!current || !sessionId) return null;
62
+ const channel = currentInstallChannel();
63
+ // Only npm-global has a hook-owned installer. Other channels receive an
64
+ // actionable notice at SessionStart and must not turn an "Upgrade" word
65
+ // into a misleading approval marker for a different installation path.
66
+ if (channel !== 'npm-global') return null;
67
+ const cache = readUpdateCheckCache(current);
68
+ const latest = cache?.latestVersion;
69
+ if (typeof latest !== 'string' || !latest) return null;
70
+ const pending = findAutoUpdateConsent(sessionId, current, latest, channel);
71
+ if (!pending || !['pending'].includes(pending.decision)) return null;
72
+ const decision = parseAutoUpdateConsent(prompt);
73
+ if (!decision) return null;
74
+ return writeAutoUpdateConsent(
75
+ sessionId, current, latest, pending.channel ?? 'unknown', decision,
76
+ ) ? decision : null;
77
+ }
23
78
 
24
79
  // Patterns compiled at module load — invalid regex MUST fail loudly. Do
25
80
  // NOT move into a try block "for safety": a regex compile error is a
@@ -121,8 +176,6 @@ if (isMainModule) {
121
176
  process.stdin.on('data', (chunk) => { input += chunk; });
122
177
  process.stdin.on('end', () => {
123
178
  try {
124
- if (!isAutoCaptureEnabled(process.env)) return process.exit(0);
125
-
126
179
  // Distinguish empty stdin (legitimate degenerate event) from malformed
127
180
  // input (protocol drift). Both stay non-blocking, but only malformed
128
181
  // input is logged — empty is normal, garbage indicates a real bug.
@@ -142,14 +195,21 @@ if (isMainModule) {
142
195
  // we accept either name to survive a similar rename. If both are absent
143
196
  // or non-string, detectRememberIntent's type guard returns false safely.
144
197
  const prompt = data.prompt ?? data.user_prompt ?? '';
145
- if (!detectRememberIntent(prompt)) return process.exit(0);
146
-
147
- const out = {
148
- hookSpecificOutput: {
149
- hookEventName: 'UserPromptSubmit',
150
- additionalContext: buildHint(),
151
- },
152
- };
198
+ const updateDecision = recordUpdateConsent(data.session_id, prompt);
199
+ const rememberIntent = detectRememberIntent(prompt);
200
+ if (!rememberIntent && !updateDecision) return process.exit(0);
201
+ // Update consent is a user-authorized control decision, not memory
202
+ // capture; it must still be recorded when auto-capture is disabled.
203
+ if (!isAutoCaptureEnabled(process.env) && !updateDecision) return process.exit(0);
204
+
205
+ const contexts = [];
206
+ if (updateDecision) {
207
+ contexts.push(updateDecision === 'approved'
208
+ ? 'The user explicitly approved the MeMesh upgrade. The Stop hook may now update the consented installation.'
209
+ : 'The user declined the MeMesh upgrade for this session. Do not install it or ask again in this session.');
210
+ }
211
+ if (rememberIntent) contexts.push(buildHint());
212
+ const out = { hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: contexts.join('\n\n') } };
153
213
  process.stdout.write(JSON.stringify(out));
154
214
  process.exit(0);
155
215
  } catch (err) {
@@ -0,0 +1,123 @@
1
+ import { execFileSync, execSync } from 'node:child_process';
2
+
3
+ /**
4
+ * Run npm / npx from a build script, on every platform we support.
5
+ *
6
+ * Two separate Windows problems, and fixing only the first is what made this
7
+ * take two attempts:
8
+ *
9
+ * 1. npm ships on Windows as `npm.cmd`. `execFileSync('npm', …)` does not
10
+ * consult `PATHEXT` — that is a shell behaviour and execFile uses no
11
+ * shell — so it fails with `spawnSync npm ENOENT` while the identical
12
+ * command typed into a terminal works.
13
+ *
14
+ * 2. Naming it `npm.cmd` then fails with `spawnSync npm.cmd EINVAL`. Since
15
+ * the fix for CVE-2024-27980, Node refuses to spawn `.cmd` and `.bat`
16
+ * files without `shell: true`, because the Windows command interpreter
17
+ * re-parses the argument list. On Windows there is no shell-free way to
18
+ * invoke npm.
19
+ *
20
+ * So `shell: true` is required there, not a shortcut — and it is scoped to
21
+ * Windows, so macOS and Linux keep passing arguments as an array with no
22
+ * interpreter in the path at all.
23
+ *
24
+ * What `shell: true` costs is argument re-parsing, which matters only for
25
+ * arguments that are not literals. There is exactly one — the tarball name
26
+ * `npm pack` prints — and `assertSafeShellArg()` below is how it is made safe
27
+ * rather than assumed safe. Call it on anything that did not come from this
28
+ * repository's own source.
29
+ *
30
+ * Both scripts on the publish path had written this by hand:
31
+ *
32
+ * check-consumer-audit.mjs -> audit:prod -> verify:release -> prepublishOnly
33
+ * run-tests-isolated.mjs -> test:isolated -> prepublishOnly
34
+ *
35
+ * One owner, so a third caller cannot get it wrong a third way.
36
+ */
37
+ const isWindows = process.platform === 'win32';
38
+
39
+ export const NPM = isWindows ? 'npm.cmd' : 'npm';
40
+ export const NPX = isWindows ? 'npx.cmd' : 'npx';
41
+
42
+ /**
43
+ * Reject anything that could mean something to a command interpreter.
44
+ *
45
+ * Deliberately an allow-list. A deny-list of shell metacharacters has to be
46
+ * complete to be correct, and `cmd.exe` gives `%`, `^` and `&` meanings that a
47
+ * POSIX-shaped deny-list would miss.
48
+ */
49
+ export function assertSafeShellArg(value, what) {
50
+ // `:` and `~` are here for one reason: Windows absolute paths
51
+ // (`C:\Users\RUNNER~1\AppData\Local\Temp\...` — 8.3 short names carry the `~`,
52
+ // and that is literally what `os.tmpdir()` returns on a Windows runner).
53
+ // `smoke-packed-artifact.mjs` passes an absolute `os.tmpdir()` path to
54
+ // `npm pack --pack-destination` and to `npm install`, so without it
55
+ // `npm run test:packaged` throws on Windows before packing anything —
56
+ // fail-closed, but it means the packaged smoke test cannot run there at all.
57
+ // Neither has meaning to cmd.exe (`~` is a POSIX-shell nicety, and the POSIX
58
+ // branch never reaches here — it uses execFileSync with no shell at all).
59
+ // Every character that DOES mean something to cmd.exe (`%^&|<>"'` and
60
+ // whitespace) stays excluded.
61
+ if (typeof value !== 'string' || !/^[A-Za-z0-9._@:~\-+=/\\]+$/.test(value)) {
62
+ throw new Error(
63
+ `${what} is not a safe argument to pass through a shell: ${JSON.stringify(value)}`
64
+ );
65
+ }
66
+ return value;
67
+ }
68
+
69
+ /**
70
+ * Spawn `bin` with `args`.
71
+ *
72
+ * POSIX: no shell, arguments stay an array, nothing is re-parsed.
73
+ *
74
+ * Windows: every argument is validated, then the command line is built here and
75
+ * handed to `execSync`. Passing an ARRAY together with `shell: true` is
76
+ * deprecated (DEP0190) precisely because the arguments are concatenated without
77
+ * escaping — doing the concatenation ourselves, after checking each part, is
78
+ * the same operation with the check that makes it sound, and it does not print
79
+ * a deprecation warning on every release gate.
80
+ *
81
+ * The usual advice — "use execFile with an argument array, never build a shell
82
+ * string" — is right, and is what the POSIX branch does. It does not apply to
83
+ * the Windows branch because there is no execFile that works: npm is a `.cmd`,
84
+ * and Node refuses to exec one without a shell. The choice there is not
85
+ * array-vs-string, it is checked-vs-unchecked. Every argument reaching the
86
+ * concatenation has passed `assertSafeShellArg`, which is an allow-list, so no
87
+ * character with meaning to `cmd.exe` can be in one. `tests/consumer-audit-
88
+ * gate.test.ts` pins that, including the `%` and `^` forms a POSIX-shaped
89
+ * deny-list would miss.
90
+ */
91
+ function runSync(bin, args, opts) {
92
+ if (!isWindows) return execFileSync(bin, args, opts);
93
+ const safeArgs = args.map((arg, i) => assertSafeShellArg(arg, `${bin} argument ${i} (${arg})`));
94
+ return execSync([bin, ...safeArgs].join(' '), opts);
95
+ }
96
+
97
+ /** `npm <args>`, spawned correctly for the platform. */
98
+ export function npmSync(args, opts = {}) {
99
+ return runSync(NPM, args, opts);
100
+ }
101
+
102
+ /** `npx <args>`, spawned correctly for the platform. */
103
+ export function npxSync(args, opts = {}) {
104
+ return runSync(NPX, args, opts);
105
+ }
106
+
107
+ /**
108
+ * `baseEnv` with every spelling of `npm_config_cache` removed and the private
109
+ * cache set. Windows environment names are case-insensitive and Node keeps
110
+ * the lexicographically first of two keys that differ only in case (`N`
111
+ * before `n`); Vitest adds an upper-cased copy of every variable to its
112
+ * Windows workers. So `{ ...process.env, npm_config_cache }` handed npm the
113
+ * runner's `NPM_CONFIG_CACHE=C:\npm\cache` and never the private directory —
114
+ * measured on windows-latest with the fake npm in
115
+ * `tests/consumer-audit-gate.test.ts`.
116
+ */
117
+ export function envWithNpmCache(cacheDir, baseEnv = process.env) {
118
+ const env = Object.fromEntries(
119
+ Object.entries(baseEnv).filter(([key]) => key.toLowerCase() !== 'npm_config_cache'),
120
+ );
121
+ env.npm_config_cache = cacheDir;
122
+ return env;
123
+ }
@@ -91,6 +91,11 @@ MARKETPLACE_DIR="$CLAUDE_CONFIG_ROOT/plugins/marketplaces/pcircle-memesh"
91
91
  INSTALL_REGISTRY="$CLAUDE_CONFIG_ROOT/plugins/installed_plugins.json"
92
92
  CACHE_ROOT="$CLAUDE_CONFIG_ROOT/plugins/cache/pcircle-memesh/memesh"
93
93
  LOCK_DIR="$CACHE_ROOT.lock"
94
+ # Use the checker shipped beside this upgrade script, not a file newly staged
95
+ # from the target commit. This keeps upgrades from legacy marketplace commits
96
+ # testable and prevents a target archive from disabling its own validation.
97
+ UPGRADE_SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
98
+ PLUGIN_ARTIFACT_CHECKER="$UPGRADE_SCRIPT_DIR/check-plugin-hook-artifact.mjs"
94
99
 
95
100
  # ─── Pre-flight ────────────────────────────────────────────────────────────
96
101
  if [ ! -d "$MARKETPLACE_DIR" ]; then
@@ -514,6 +519,25 @@ echo "==> Installing runtime deps (this may take a minute)..."
514
519
  exit 1
515
520
  }
516
521
 
522
+ # Validate a real staged plugin before moving it into the live cache. The
523
+ # host registry may load hooks immediately after the swap; a manifest that
524
+ # names a missing script would otherwise surface as a frightening `127` in the
525
+ # user's next session. Tiny legacy marketplace fixtures (and genuinely old
526
+ # plugin archives) have no plugin wiring to validate, so preserve their
527
+ # historical cache-refresh behavior; once wiring is present, the checker is
528
+ # mandatory and fail-closed. The checker itself comes from this script's
529
+ # installed release, never from the target archive being validated.
530
+ if [ -f "$STAGE_PATH/hooks/hooks.json" ] || [ -f "$STAGE_PATH/.claude-plugin/plugin.json" ] || [ -f "$STAGE_PATH/.codex-plugin/plugin.json" ]; then
531
+ if [ ! -f "$PLUGIN_ARTIFACT_CHECKER" ]; then
532
+ echo "ERROR: plugin artifact checker is missing beside the upgrade script — the live cache at $NEW_INSTALL_PATH was not touched" >&2
533
+ exit 1
534
+ fi
535
+ if ! node "$PLUGIN_ARTIFACT_CHECKER" --root "$STAGE_PATH" --skip-pack; then
536
+ echo "ERROR: staged plugin artifact integrity check failed — the live cache at $NEW_INSTALL_PATH was not touched" >&2
537
+ exit 1
538
+ fi
539
+ fi
540
+
517
541
  # ─── 5. Swap the staged copy in ───────────────────────────────────────────
518
542
  if [ -e "$NEW_INSTALL_PATH" ] || [ -L "$NEW_INSTALL_PATH" ]; then
519
543
  HAD_LIVE_CACHE=1