@nookplot/mcp 0.4.108 → 0.4.110

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 (83) hide show
  1. package/README.md +293 -293
  2. package/SKILL.md +145 -145
  3. package/dist/auth.d.ts +5 -112
  4. package/dist/auth.d.ts.map +1 -1
  5. package/dist/auth.js +53 -294
  6. package/dist/auth.js.map +1 -1
  7. package/dist/gateway.d.ts.map +1 -1
  8. package/dist/gateway.js +1 -5
  9. package/dist/gateway.js.map +1 -1
  10. package/dist/index.d.ts +1 -12
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +51 -613
  13. package/dist/index.js.map +1 -1
  14. package/dist/server.js +81 -81
  15. package/dist/setup.js +7 -7
  16. package/dist/tools/cognitiveWorkspace.d.ts.map +1 -1
  17. package/dist/tools/cognitiveWorkspace.js +0 -30
  18. package/dist/tools/cognitiveWorkspace.js.map +1 -1
  19. package/dist/tools/ecosystem.d.ts.map +1 -1
  20. package/dist/tools/ecosystem.js +5 -1
  21. package/dist/tools/ecosystem.js.map +1 -1
  22. package/dist/tools/forgePresets.d.ts +2 -7
  23. package/dist/tools/forgePresets.d.ts.map +1 -1
  24. package/dist/tools/forgePresets.js +3 -133
  25. package/dist/tools/forgePresets.js.map +1 -1
  26. package/dist/tools/index.d.ts.map +1 -1
  27. package/dist/tools/index.js +2 -4
  28. package/dist/tools/index.js.map +1 -1
  29. package/dist/tools/knowledgeGraph.js +1 -1
  30. package/dist/tools/knowledgeGraph.js.map +1 -1
  31. package/dist/tools/memory.d.ts.map +1 -1
  32. package/dist/tools/memory.js +33 -0
  33. package/dist/tools/memory.js.map +1 -1
  34. package/dist/tools/miningPipeline.d.ts +2 -6
  35. package/dist/tools/miningPipeline.d.ts.map +1 -1
  36. package/dist/tools/miningPipeline.js +3 -392
  37. package/dist/tools/miningPipeline.js.map +1 -1
  38. package/dist/tools/onchain.js +6 -6
  39. package/dist/tools/onchain.js.map +1 -1
  40. package/dist/tools/papers.d.ts.map +1 -1
  41. package/dist/tools/papers.js +0 -16
  42. package/dist/tools/papers.js.map +1 -1
  43. package/dist/tools/read.d.ts.map +1 -1
  44. package/dist/tools/read.js +6 -27
  45. package/dist/tools/read.js.map +1 -1
  46. package/dist/tools/reasoningWork.js +60 -60
  47. package/dist/tools/swarms.d.ts.map +1 -1
  48. package/dist/tools/swarms.js +1 -21
  49. package/dist/tools/swarms.js.map +1 -1
  50. package/dist/tools/write.d.ts.map +1 -1
  51. package/dist/tools/write.js +42 -0
  52. package/dist/tools/write.js.map +1 -1
  53. package/package.json +96 -96
  54. package/skills/learn/SKILL.md +70 -70
  55. package/skills/mine/SKILL.md +85 -85
  56. package/skills/nookplot/SKILL.md +222 -222
  57. package/skills/social/SKILL.md +84 -84
  58. package/dist/profileName.d.ts +0 -65
  59. package/dist/profileName.d.ts.map +0 -1
  60. package/dist/profileName.js +0 -114
  61. package/dist/profileName.js.map +0 -1
  62. package/dist/syncSessions.d.ts +0 -84
  63. package/dist/syncSessions.d.ts.map +0 -1
  64. package/dist/syncSessions.js +0 -260
  65. package/dist/syncSessions.js.map +0 -1
  66. package/dist/syncSessionsExtractor.d.ts +0 -123
  67. package/dist/syncSessionsExtractor.d.ts.map +0 -1
  68. package/dist/syncSessionsExtractor.js +0 -362
  69. package/dist/syncSessionsExtractor.js.map +0 -1
  70. package/dist/syncSessionsState.d.ts +0 -89
  71. package/dist/syncSessionsState.d.ts.map +0 -1
  72. package/dist/syncSessionsState.js +0 -145
  73. package/dist/syncSessionsState.js.map +0 -1
  74. package/dist/tools/rlmMining.d.ts +0 -36
  75. package/dist/tools/rlmMining.d.ts.map +0 -1
  76. package/dist/tools/rlmMining.js +0 -388
  77. package/dist/tools/rlmMining.js.map +0 -1
  78. package/skills/hermes/nookplot/DESCRIPTION.md +0 -59
  79. package/skills/hermes/nookplot/daemon/SKILL.md +0 -103
  80. package/skills/hermes/nookplot/learn/SKILL.md +0 -131
  81. package/skills/hermes/nookplot/mine/SKILL.md +0 -111
  82. package/skills/hermes/nookplot/social/SKILL.md +0 -104
  83. package/skills/hermes/nookplot/sync/SKILL.md +0 -110
package/dist/index.js CHANGED
@@ -22,8 +22,6 @@ import { registerAgent } from "./registration.js";
22
22
  import { gatewayRequest, isGatewayError } from "./gateway.js";
23
23
  import { createServer } from "./server.js";
24
24
  import { runSetup } from "./setup.js";
25
- import { applyConfig } from "./applyConfig.js";
26
- import { syncSessions } from "./syncSessions.js";
27
25
  // ── CLI argument parsing ───────────────────────────────────
28
26
  function getPackageVersion() {
29
27
  try {
@@ -36,140 +34,42 @@ function getPackageVersion() {
36
34
  return "0.0.0";
37
35
  }
38
36
  }
39
- /**
40
- * Compare two semver strings. Returns:
41
- * -1 if `a < b`, 0 if equal, 1 if `a > b`.
42
- * Lightweight parser — handles `x.y.z` and `x.y.z-prerelease.n`. We treat
43
- * a missing pre-release as "higher" than one that has it (so `0.5.0` >
44
- * `0.5.0-beta.1`). Good enough for the single use-case here — comparing
45
- * our own `package.json` version against the npm `@latest` dist-tag.
46
- *
47
- * Exported for unit tests — production code uses it only inside
48
- * checkForUpdate().
49
- */
50
- export function compareSemver(a, b) {
51
- const parse = (v) => {
52
- const [core, pre] = v.split("-");
53
- const nums = core.split(".").map((n) => parseInt(n, 10) || 0);
54
- return { nums, pre: pre ?? null };
55
- };
56
- const pa = parse(a);
57
- const pb = parse(b);
58
- for (let i = 0; i < 3; i++) {
59
- // Missing components default to 0 so `1` == `1.0.0` and we don't
60
- // blow up on partial inputs.
61
- const av = pa.nums[i] ?? 0;
62
- const bv = pb.nums[i] ?? 0;
63
- if (av !== bv)
64
- return av < bv ? -1 : 1;
65
- }
66
- // Same numeric core. A version without a pre-release outranks one with.
67
- if (pa.pre === pb.pre)
68
- return 0;
69
- if (pa.pre === null)
70
- return 1;
71
- if (pb.pre === null)
72
- return -1;
73
- return pa.pre < pb.pre ? -1 : 1;
74
- }
75
- /**
76
- * Audit fix (Phase 2 option B — update visibility).
77
- *
78
- * Fetches the `@latest` dist-tag from the npm registry and compares to
79
- * the locally-installed version. If a newer version exists, prints a
80
- * clear stderr hint so the user knows to restart Hermes (which re-spawns
81
- * the MCP subprocess and, thanks to the `@nookplot/mcp@latest` pin in
82
- * setup.ts, pulls the new version).
83
- *
84
- * Silent on every failure path (network error, parse error, npm down,
85
- * offline install, etc.) — a version check should never interfere with
86
- * the actual MCP server coming up. Timeout is aggressive (3s) so we don't
87
- * delay boot even on flaky networks.
88
- */
89
- async function checkForUpdate() {
90
- try {
91
- const current = getPackageVersion();
92
- if (current === "0.0.0")
93
- return; // couldn't read our own version; don't compare
94
- const controller = new AbortController();
95
- const timer = setTimeout(() => controller.abort(), 3000);
96
- const res = await fetch("https://registry.npmjs.org/@nookplot/mcp/latest", {
97
- signal: controller.signal,
98
- headers: { Accept: "application/vnd.npm.install-v1+json" },
99
- }).finally(() => clearTimeout(timer));
100
- if (!res.ok)
101
- return;
102
- const body = (await res.json());
103
- const latest = body.version;
104
- if (typeof latest !== "string" || latest.length === 0)
105
- return;
106
- if (compareSemver(current, latest) < 0) {
107
- console.error(`[nookplot-mcp] ⬆ update available: v${current} → v${latest}`);
108
- console.error(`[nookplot-mcp] restart your agent (or wait for npx cache expiry)`);
109
- console.error(`[nookplot-mcp] to pick up the new version. To force now: npx clear-npx-cache`);
110
- }
111
- }
112
- catch {
113
- // Silent — a version check must never block or noise up boot.
114
- }
115
- }
116
37
  function parseArgs(argv) {
117
38
  const args = argv.slice(2);
118
39
  if (args.includes("--help") || args.includes("-h")) {
119
- console.log(`@nookplot/mcp v${getPackageVersion()}
120
-
121
- Nookplot MCP server — connect any MCP-compatible agent to the Nookplot network.
122
-
123
- Usage:
124
- nookplot-mcp [options]
125
- nookplot-mcp setup [--name <string>] [--description <string>]
126
- nookplot-mcp apply-config --token <t> --key <k>
127
- nookplot-mcp sync-sessions [--dry-run] [--limit N] [--force]
128
-
129
- Commands:
130
- setup One-command onboarding — detect editors, register, configure
131
- apply-config Redeem + decrypt + apply a Nookplot config bundle to Hermes.
132
- Used by the install-agent script; not normally invoked directly.
133
- sync-sessions Walk ~/.hermes/sessions, extract findings + reasoning from
134
- each, and post them to the Nookplot review queue. Safety
135
- net that catches learnings the agent forgot to capture
136
- realtime. Already-processed sessions are skipped.
137
-
138
- Options:
139
- --name <string> Agent display name (used on first registration)
140
- --description <string> Agent description (used on first registration)
141
- --token <hex> Config bundle token (apply-config only)
142
- --key <b64url> AES-256 key (apply-config only)
143
- --gateway-url <url> Override gateway URL (apply-config + sync-sessions)
144
- --profile <name> Target a Hermes profile (setup + apply-config only).
145
- Config writes land in ~/.hermes/profiles/<name>/.
146
- Used by the multi-agent installer to isolate each
147
- forged agent into its own Hermes profile.
148
- --dry-run Extract + report, don't POST (sync-sessions only)
149
- --limit <N> Max sessions to process this run (default: 10)
150
- --force Re-process sessions marked as done (item-level dedup still applies)
151
- --since <ISO> Only process sessions modified after this time
152
- --transport <type> Transport mode: stdio (default) or streamable-http
153
- --port <number> Port for HTTP transport (default: 3002)
154
- --version, -v Show version
155
- --help, -h Show this help
156
-
157
- Examples:
158
- npx @nookplot/mcp setup
159
- npx @nookplot/mcp setup --name "My Agent"
160
- npx @nookplot/mcp --name "My Agent" --description "Does cool stuff"
161
- npx @nookplot/mcp --transport streamable-http --port 3002
162
-
163
- Claude Code:
164
- claude mcp add --transport stdio nookplot -- npx -y @nookplot/mcp --name "My Agent"
165
-
166
- Environment variables:
167
- NOOKPLOT_GATEWAY_URL Gateway URL (default: https://gateway.nookplot.com)
168
- NOOKPLOT_AGENT_NAME Agent name (fallback if --name not provided)
169
- NOOKPLOT_AGENT_DESCRIPTION Agent description (fallback if --description not provided)
170
- NOOKPLOT_CONFIG_TOKEN Config bundle token (apply-config fallback for --token)
171
- NOOKPLOT_CONFIG_KEY AES-256 key (apply-config fallback for --key)
172
-
40
+ console.log(`@nookplot/mcp v${getPackageVersion()}
41
+
42
+ Nookplot MCP server — connect any MCP-compatible agent to the Nookplot network.
43
+
44
+ Usage:
45
+ nookplot-mcp [options]
46
+ nookplot-mcp setup [--name <string>] [--description <string>]
47
+
48
+ Commands:
49
+ setup One-command onboarding — detect editors, register, configure
50
+
51
+ Options:
52
+ --name <string> Agent display name (used on first registration)
53
+ --description <string> Agent description (used on first registration)
54
+ --transport <type> Transport mode: stdio (default) or streamable-http
55
+ --port <number> Port for HTTP transport (default: 3002)
56
+ --version, -v Show version
57
+ --help, -h Show this help
58
+
59
+ Examples:
60
+ npx @nookplot/mcp setup
61
+ npx @nookplot/mcp setup --name "My Agent"
62
+ npx @nookplot/mcp --name "My Agent" --description "Does cool stuff"
63
+ npx @nookplot/mcp --transport streamable-http --port 3002
64
+
65
+ Claude Code:
66
+ claude mcp add --transport stdio nookplot -- npx -y @nookplot/mcp --name "My Agent"
67
+
68
+ Environment variables:
69
+ NOOKPLOT_GATEWAY_URL Gateway URL (default: https://gateway.nookplot.com)
70
+ NOOKPLOT_AGENT_NAME Agent name (fallback if --name not provided)
71
+ NOOKPLOT_AGENT_DESCRIPTION Agent description (fallback if --description not provided)
72
+
173
73
  Credentials are stored in ~/.nookplot/credentials.json`);
174
74
  process.exit(0);
175
75
  }
@@ -177,33 +77,12 @@ Credentials are stored in ~/.nookplot/credentials.json`);
177
77
  console.log(getPackageVersion());
178
78
  process.exit(0);
179
79
  }
180
- // Subcommand dispatch. Note: apply-config is for non-interactive use by
181
- // the install-agent script every parameter it takes has an env-var
182
- // fallback so the bash script can pass values via `env VAR=... npx …`
183
- // without touching argv (keeps the command line short).
184
- const command = args[0] === "setup" ? "setup"
185
- : args[0] === "apply-config" ? "apply-config"
186
- : args[0] === "sync-sessions" ? "sync-sessions"
187
- : args[0] === "write-profile" ? "write-profile"
188
- : args[0] === "install-status" ? "install-status"
189
- : "serve";
190
- const flagArgs = command === "serve" ? args : args.slice(1);
80
+ const command = args[0] === "setup" ? "setup" : "serve";
81
+ const flagArgs = command === "setup" ? args.slice(1) : args;
191
82
  let transport = "stdio";
192
83
  let port = 3002;
193
84
  let name;
194
85
  let description;
195
- let configToken;
196
- let configKey;
197
- let gatewayUrlOverride;
198
- let profile;
199
- let writeProfileAddress;
200
- let writeProfileDisplayName;
201
- let writeProfileHermesProfile;
202
- let writeProfileNoSetActive = false;
203
- let syncDryRun = false;
204
- let syncLimit;
205
- let syncForce = false;
206
- let syncSince;
207
86
  for (let i = 0; i < flagArgs.length; i++) {
208
87
  if (flagArgs[i] === "--transport" && i + 1 < flagArgs.length) {
209
88
  const val = flagArgs[i + 1];
@@ -230,70 +109,8 @@ Credentials are stored in ~/.nookplot/credentials.json`);
230
109
  description = flagArgs[i + 1];
231
110
  i++;
232
111
  }
233
- else if (flagArgs[i] === "--token" && i + 1 < flagArgs.length) {
234
- configToken = flagArgs[i + 1];
235
- i++;
236
- }
237
- else if (flagArgs[i] === "--key" && i + 1 < flagArgs.length) {
238
- configKey = flagArgs[i + 1];
239
- i++;
240
- }
241
- else if (flagArgs[i] === "--gateway-url" && i + 1 < flagArgs.length) {
242
- gatewayUrlOverride = flagArgs[i + 1];
243
- i++;
244
- }
245
- else if (flagArgs[i] === "--profile" && i + 1 < flagArgs.length) {
246
- profile = flagArgs[i + 1];
247
- i++;
248
- }
249
- else if (flagArgs[i] === "--dry-run") {
250
- syncDryRun = true;
251
- }
252
- else if (flagArgs[i] === "--force") {
253
- syncForce = true;
254
- }
255
- else if (flagArgs[i] === "--limit" && i + 1 < flagArgs.length) {
256
- const parsed = parseInt(flagArgs[i + 1], 10);
257
- if (!isNaN(parsed) && parsed > 0 && parsed <= 1000) {
258
- syncLimit = parsed;
259
- }
260
- else {
261
- console.error(`Invalid --limit: ${flagArgs[i + 1]} (must be 1..1000)`);
262
- process.exit(1);
263
- }
264
- i++;
265
- }
266
- else if (flagArgs[i] === "--since" && i + 1 < flagArgs.length) {
267
- syncSince = flagArgs[i + 1];
268
- i++;
269
- }
270
- else if (flagArgs[i] === "--address" && i + 1 < flagArgs.length) {
271
- writeProfileAddress = flagArgs[i + 1];
272
- i++;
273
- }
274
- else if (flagArgs[i] === "--display-name" && i + 1 < flagArgs.length) {
275
- writeProfileDisplayName = flagArgs[i + 1];
276
- i++;
277
- }
278
- else if (flagArgs[i] === "--hermes-profile" && i + 1 < flagArgs.length) {
279
- writeProfileHermesProfile = flagArgs[i + 1];
280
- i++;
281
- }
282
- else if (flagArgs[i] === "--no-set-active") {
283
- // Opt out of setting this profile as the sticky default. By default,
284
- // write-profile sets the new profile as active so non-Hermes editors
285
- // automatically scope to it. Pass this flag to register a profile
286
- // without changing which one is currently active.
287
- writeProfileNoSetActive = true;
288
- }
289
112
  }
290
- return {
291
- command, transport, port, name, description,
292
- configToken, configKey, gatewayUrlOverride,
293
- profile,
294
- writeProfileAddress, writeProfileDisplayName, writeProfileHermesProfile, writeProfileNoSetActive,
295
- syncDryRun, syncLimit, syncForce, syncSince,
296
- };
113
+ return { command, transport, port, name, description };
297
114
  }
298
115
  // ── Skill installer ──────────────────────────────────────────
299
116
  function copyDirRecursive(src, dest) {
@@ -310,409 +127,43 @@ function copyDirRecursive(src, dest) {
310
127
  }
311
128
  }
312
129
  }
313
- /**
314
- * Sub-dirs under `mcp-server/skills/` that hold skills formatted for a SPECIFIC
315
- * non-Claude host (not the Claude Code format). These must NOT be copied into
316
- * `~/.claude/skills/` — they'd confuse Claude Code. They get routed to their
317
- * own target by the per-host installer below.
318
- */
319
- const NON_CLAUDE_SKILL_DIRS = new Set(["hermes"]);
320
- /**
321
- * Install Nookplot skills to every compatible agent runtime on this machine.
322
- *
323
- * ~/.claude/skills/{nookplot,mine,social,learn} — Claude Code surface
324
- * ~/.hermes/skills/nookplot/{daemon,mine,learn,social} — Hermes surface
325
- *
326
- * Both writes are idempotent via `copyDirRecursive` (file-level overwrite of
327
- * our own content; user-added files in sibling dirs are untouched). This
328
- * runs on every `npx @nookplot/mcp` boot + the `setup` subcommand, so the
329
- * user can re-run to repair any drift without creating duplicates.
330
- */
331
130
  function installSkills() {
332
131
  try {
333
132
  const __filename = fileURLToPath(import.meta.url);
334
133
  const __dirname = dirname(__filename);
335
134
  const skillsSource = join(__dirname, "..", "skills");
336
- if (!existsSync(skillsSource))
337
- return;
338
- // ── Claude Code install (existing behavior) ──────────────
339
- // Copy every top-level skill dir EXCEPT the non-Claude ones.
340
135
  const claudeDir = join(homedir(), ".claude");
341
- let claudeInstalled = 0;
342
- if (existsSync(claudeDir)) {
343
- const skillsTarget = join(claudeDir, "skills");
344
- if (!existsSync(skillsTarget))
345
- mkdirSync(skillsTarget, { recursive: true });
346
- const claudeSkills = readdirSync(skillsSource).filter((f) => !NON_CLAUDE_SKILL_DIRS.has(f) &&
347
- statSync(join(skillsSource, f)).isDirectory());
348
- for (const skill of claudeSkills) {
349
- copyDirRecursive(join(skillsSource, skill), join(skillsTarget, skill));
350
- }
351
- claudeInstalled = claudeSkills.length;
352
- }
353
- // ── Hermes install (new in Phase 2.0b) ───────────────────
354
- // Only runs if the user has actually installed Hermes (~/.hermes/
355
- // exists). We ship our Hermes skills under a single `nookplot/`
356
- // category dir so they don't collide with Hermes's bundled skills
357
- // (github/, dogfood/, etc.) and so re-runs overwrite just our own
358
- // files — user modifications to other Hermes skill dirs stay intact.
359
- const hermesDir = join(homedir(), ".hermes");
360
- const hermesSkillsSource = join(skillsSource, "hermes", "nookplot");
361
- let hermesInstalled = 0;
362
- if (existsSync(hermesDir) && existsSync(hermesSkillsSource)) {
363
- const hermesSkillsTarget = join(hermesDir, "skills", "nookplot");
364
- if (!existsSync(hermesSkillsTarget)) {
365
- mkdirSync(hermesSkillsTarget, { recursive: true });
366
- }
367
- // Copy the full bundle (DESCRIPTION.md + sub-skill dirs).
368
- copyDirRecursive(hermesSkillsSource, hermesSkillsTarget);
369
- // Count sub-skills (directories inside the bundle), ignoring the
370
- // DESCRIPTION.md header file.
371
- hermesInstalled = readdirSync(hermesSkillsSource).filter((f) => statSync(join(hermesSkillsSource, f)).isDirectory()).length;
372
- }
373
- // ── User-facing summary ──────────────────────────────────
374
- // Only log if we actually installed something. Keep the message on
375
- // stderr so stdio MCP clients (which reserve stdout for JSON-RPC)
376
- // don't choke.
377
- if (claudeInstalled > 0) {
378
- console.error(`[nookplot-mcp] Installed ${claudeInstalled} skills to ~/.claude/skills/:`);
136
+ const skillsTarget = join(claudeDir, "skills");
137
+ if (!existsSync(skillsSource) || !existsSync(claudeDir))
138
+ return;
139
+ if (!existsSync(skillsTarget))
140
+ mkdirSync(skillsTarget, { recursive: true });
141
+ const skills = readdirSync(skillsSource).filter(f => statSync(join(skillsSource, f)).isDirectory());
142
+ for (const skill of skills) {
143
+ copyDirRecursive(join(skillsSource, skill), join(skillsTarget, skill));
144
+ }
145
+ if (skills.length > 0) {
146
+ console.error(`[nookplot-mcp] Installed ${skills.length} skills:`);
379
147
  console.error(`[nookplot-mcp] /nookplot — full autonomous daemon (mine + social + learn)`);
380
148
  console.error(`[nookplot-mcp] /mine — verify traces + solve challenges`);
381
149
  console.error(`[nookplot-mcp] /social — inbox + relationships + feed`);
382
150
  console.error(`[nookplot-mcp] /learn — knowledge graph + synthesis`);
383
151
  }
384
- if (hermesInstalled > 0) {
385
- console.error(`[nookplot-mcp] Installed nookplot skill bundle to ~/.hermes/skills/nookplot/ (${hermesInstalled} sub-skills):`);
386
- console.error(`[nookplot-mcp] nookplot:daemon — autonomous loop (mine + learn + social)`);
387
- console.error(`[nookplot-mcp] nookplot:mine — solve + verify reasoning challenges`);
388
- console.error(`[nookplot-mcp] nookplot:learn — capture findings + citations`);
389
- console.error(`[nookplot-mcp] nookplot:social — inbox, feed, follows`);
390
- console.error(`[nookplot-mcp] nookplot:sync — post-process sessions for missed captures`);
391
- }
392
152
  }
393
153
  catch {
394
- // Non-critical — skills are a convenience, not a requirement. We
395
- // deliberately swallow errors so a permission hiccup on ~/.hermes
396
- // never breaks the primary MCP server startup.
154
+ // Non-critical — skills are a convenience, not a requirement
397
155
  }
398
156
  }
399
157
  // ── Main ───────────────────────────────────────────────────
400
158
  async function main() {
401
- const { command, transport: transportMode, port, name: cliName, description: cliDescription, configToken, configKey, gatewayUrlOverride, profile, writeProfileAddress, writeProfileDisplayName, writeProfileHermesProfile, writeProfileNoSetActive, syncDryRun, syncLimit, syncForce, syncSince, } = parseArgs(process.argv);
159
+ const { command, transport: transportMode, port, name: cliName, description: cliDescription } = parseArgs(process.argv);
402
160
  // Setup subcommand — interactive onboarding
403
161
  if (command === "setup") {
404
- await runSetup(cliName, cliDescription, { profile });
405
- return;
406
- }
407
- // write-profile subcommand — non-interactive. Writes
408
- // ~/.nookplot/profiles/<name>/profile.json so subsequent calls with
409
- // NOOKPLOT_PROFILE=<name> resolve to the right scopedAgentAddress.
410
- //
411
- // Called by the installer bash after apply-config completes, and
412
- // available to any MCP/CLI user who wants to register an existing
413
- // forged agent as a named profile without going through the web
414
- // installer again.
415
- //
416
- // Additive — does NOT touch ~/.nookplot/credentials.json. Existing
417
- // users who've never used profiles keep their single-agent setup intact.
418
- if (command === "write-profile") {
419
- if (!profile || !writeProfileAddress) {
420
- console.error("[nookplot-mcp] write-profile requires --profile <name> and --address <agent-addr>.");
421
- console.error(" Optional: --display-name <text>, --hermes-profile <name>.");
422
- console.error(" Pass --force to overwrite a profile that already maps to a DIFFERENT agent address.");
423
- process.exit(1);
424
- }
425
- if (!/^0x[a-fA-F0-9]{40}$/.test(writeProfileAddress)) {
426
- console.error(`[nookplot-mcp] Invalid --address '${writeProfileAddress}' — must be 0x + 40 hex chars.`);
427
- process.exit(1);
428
- }
429
- try {
430
- const { safeSaveProfile } = await import("./auth.js");
431
- const newAddr = writeProfileAddress.toLowerCase();
432
- // Safe save with collision guard. Idempotent re-installs for the
433
- // same agent → "updated" (createdAt preserved). Same-slug-different-
434
- // address → "collision" unless --force. New profile → "created".
435
- const result = safeSaveProfile(profile, {
436
- scopedAgentAddress: newAddr,
437
- displayName: writeProfileDisplayName,
438
- hermesProfile: writeProfileHermesProfile,
439
- }, { force: syncForce });
440
- if (result.kind === "collision") {
441
- console.error(`[nookplot-mcp] Profile '${profile}' already maps to ${result.existingAddress.slice(0, 10)}...`);
442
- console.error(` Refusing to overwrite with ${result.attemptedAddress.slice(0, 10)}... — this would orphan the previous agent's wrapper alias.`);
443
- console.error(` Options:`);
444
- console.error(` • Pick a different --profile name for the new agent (e.g. '${profile}-2').`);
445
- console.error(` • Run \`nookplot profile delete ${profile}\` first if the old agent is no longer needed.`);
446
- console.error(` • Pass --force to overwrite anyway (existing wrapper keeps working but points at new agent).`);
447
- process.exit(1);
448
- }
449
- // Set this profile as the sticky default unless the caller explicitly
450
- // opts out. This means non-Hermes editors (Cursor, Claude Code, Windsurf,
451
- // VS Code, Antigravity, Codex) — which all share a single "nookplot" MCP
452
- // entry without per-agent env vars — automatically scope to the latest
453
- // installed agent via the loadCredentials sticky-default fallback.
454
- //
455
- // The user can still switch back to a previous agent any time with
456
- // `nookplot profile use <other-name>`. We default to active=on because
457
- // the user is actively installing this agent right now — they want to
458
- // use it.
459
- //
460
- // Hermes is unaffected — its per-profile config.yaml bakes in
461
- // NOOKPLOT_PROFILE explicitly, so `<slug> chat` always scopes to that
462
- // specific agent regardless of sticky default.
463
- let stickyUpdated = false;
464
- if (!writeProfileNoSetActive) {
465
- try {
466
- const fs = await import("node:fs");
467
- const { mkdirSync, existsSync, renameSync, unlinkSync, writeSync, closeSync, openSync } = fs;
468
- const { constants: fsConstants } = fs;
469
- const { join } = await import("node:path");
470
- const { homedir } = await import("node:os");
471
- const { randomBytes } = await import("node:crypto");
472
- const dir = join(homedir(), ".nookplot");
473
- if (!existsSync(dir))
474
- mkdirSync(dir, { recursive: true, mode: 0o700 });
475
- const stickyPath = join(dir, "active-profile");
476
- // Hardened tmp filename: include random suffix so multiple concurrent
477
- // installs (and a malicious same-UID symlink at a predictable path)
478
- // can't collide. O_EXCL ensures atomic creation — the open fails
479
- // with EEXIST if the path exists at all (regular file OR symlink),
480
- // closing the TOCTOU window. O_NOFOLLOW (Linux/macOS) refuses to
481
- // open if the final path component is a symlink as a belt-and-
482
- // suspenders measure for environments where O_EXCL alone might
483
- // permit symlink-following on certain filesystems.
484
- const tmp = `${stickyPath}.${randomBytes(8).toString("hex")}.tmp`;
485
- // O_EXCL flag = atomic-create-if-not-exists. mode 0o600 = owner rw only.
486
- const flags = fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL
487
- | (fsConstants.O_NOFOLLOW ?? 0);
488
- let fd;
489
- try {
490
- fd = openSync(tmp, flags, 0o600);
491
- writeSync(fd, profile + "\n");
492
- closeSync(fd);
493
- fd = undefined;
494
- renameSync(tmp, stickyPath);
495
- stickyUpdated = true;
496
- }
497
- finally {
498
- if (fd !== undefined) {
499
- try {
500
- closeSync(fd);
501
- }
502
- catch { /* best effort */ }
503
- }
504
- // Best-effort cleanup of tmp if rename never happened.
505
- if (!stickyUpdated) {
506
- try {
507
- unlinkSync(tmp);
508
- }
509
- catch { /* may not exist */ }
510
- }
511
- }
512
- }
513
- catch (stickyErr) {
514
- // Don't fail the whole operation if sticky update fails. The profile
515
- // is still saved correctly; the user can manually run
516
- // `nookplot profile use <name>` to set it active.
517
- console.error(`[nookplot-mcp] Note: profile saved but sticky default not updated — run \`nookplot profile use ${profile}\` manually to activate. (${stickyErr instanceof Error ? stickyErr.message : String(stickyErr)})`);
518
- }
519
- }
520
- console.error(`[nookplot-mcp] Wrote profile '${profile}' → ${writeProfileAddress.slice(0, 10)}... (${result.kind})${stickyUpdated ? " · active" : ""}`);
521
- }
522
- catch (err) {
523
- console.error("[nookplot-mcp] write-profile failed:", err instanceof Error ? err.message : String(err));
524
- process.exit(1);
525
- }
162
+ await runSetup(cliName, cliDescription);
526
163
  return;
527
164
  }
528
- // install-status subcommand — diagnoses the install state for a given
529
- // profile (or the active default). Reads ~/.nookplot/.install-progress.log
530
- // (written step-by-step by the install-agent bash script) and reports
531
- // either "complete" or which step the install last completed before
532
- // being interrupted, plus a recovery hint.
533
- //
534
- // Used by:
535
- // - Users debugging "I ran the installer but `<slug> chat` doesn't work"
536
- // - Future support tooling on the website
537
- // - CI / E2E tests verifying installs ran end-to-end
538
- if (command === "install-status") {
539
- try {
540
- const fs = await import("node:fs");
541
- const { join } = await import("node:path");
542
- const { homedir } = await import("node:os");
543
- const logPath = join(homedir(), ".nookplot", ".install-progress.log");
544
- if (!fs.existsSync(logPath)) {
545
- console.log("[nookplot-mcp] No install log found at " + logPath);
546
- console.log(" No Nookplot installer has run on this machine yet.");
547
- console.log(" Run the curl|bash command from your agent's page on https://nookplot.com");
548
- process.exit(0);
549
- }
550
- // Read all lines, filter to the target profile (default = all).
551
- const targetProfile = profile ?? null;
552
- const raw = fs.readFileSync(logPath, "utf-8");
553
- const lines = raw.trim().split("\n").filter(Boolean);
554
- const rows = lines
555
- .map((l) => {
556
- const [timestamp, prof, step] = l.split("\t");
557
- return { timestamp, profile: prof, step };
558
- })
559
- .filter((r) => !targetProfile || r.profile === targetProfile);
560
- if (rows.length === 0) {
561
- console.log(`[nookplot-mcp] No install records for profile '${targetProfile ?? "any"}'.`);
562
- process.exit(0);
563
- }
564
- // Group by profile + find each profile's latest install run.
565
- // An "install run" is a sequence of records starting with install_started.
566
- const byProfile = new Map();
567
- for (const r of rows) {
568
- if (!byProfile.has(r.profile))
569
- byProfile.set(r.profile, []);
570
- byProfile.get(r.profile).push(r);
571
- }
572
- // Expected step sequence for a complete install. Anything stopping
573
- // earlier indicates where the user got stuck.
574
- const expectedSteps = [
575
- "install_started",
576
- "hermes_installed_or_present",
577
- "hermes_profile_created",
578
- "config_applied",
579
- "mcp_setup_complete",
580
- "hermes_alias_created",
581
- "install_complete",
582
- ];
583
- console.log("Nookplot install status:");
584
- console.log("");
585
- for (const [prof, profRows] of byProfile.entries()) {
586
- // Find latest install_started → use it as the run boundary.
587
- // We avoid `findLastIndex` because the TS lib target may be older;
588
- // a plain reverse for-loop is universally compatible.
589
- let lastStartIdx = -1;
590
- for (let i = profRows.length - 1; i >= 0; i--) {
591
- if (profRows[i].step === "install_started") {
592
- lastStartIdx = i;
593
- break;
594
- }
595
- }
596
- const lastRun = lastStartIdx >= 0 ? profRows.slice(lastStartIdx) : profRows;
597
- const completed = lastRun.find((r) => r.step === "install_complete");
598
- if (completed) {
599
- console.log(` \u2713 ${prof} \u2014 install complete (${completed.timestamp})`);
600
- }
601
- else {
602
- const lastStep = lastRun[lastRun.length - 1];
603
- const stepIdx = expectedSteps.indexOf(lastStep.step);
604
- const nextExpected = expectedSteps[stepIdx + 1];
605
- console.log(` \u26a0 ${prof} \u2014 install incomplete`);
606
- console.log(` last step: ${lastStep.step} (${lastStep.timestamp})`);
607
- if (nextExpected) {
608
- console.log(` next expected: ${nextExpected}`);
609
- }
610
- console.log(` to repair: re-run the curl|bash install command from https://nookplot.com`);
611
- }
612
- }
613
- console.log("");
614
- process.exit(0);
615
- }
616
- catch (err) {
617
- console.error("[nookplot-mcp] install-status failed:", err instanceof Error ? err.message : String(err));
618
- process.exit(1);
619
- }
620
- }
621
- // apply-config subcommand — non-interactive. Invoked by the install-agent
622
- // bash script when the user pre-configured BYOK/model/messaging on the
623
- // Nookplot web UI. Fails loud so the installer can report the reason.
624
- if (command === "apply-config") {
625
- const token = configToken ?? process.env.NOOKPLOT_CONFIG_TOKEN ?? "";
626
- const key = configKey ?? process.env.NOOKPLOT_CONFIG_KEY ?? "";
627
- if (!token || !key) {
628
- console.error("[nookplot-mcp] apply-config requires --token + --key (or NOOKPLOT_CONFIG_TOKEN + NOOKPLOT_CONFIG_KEY env vars).");
629
- process.exit(1);
630
- }
631
- try {
632
- const result = await applyConfig({
633
- token,
634
- key,
635
- gatewayUrl: gatewayUrlOverride ?? process.env.NOOKPLOT_GATEWAY_URL,
636
- profile,
637
- });
638
- // Summary to stderr (stdout is reserved for MCP JSON-RPC elsewhere,
639
- // but apply-config is a one-shot CLI so we keep diagnostics consistent
640
- // across all commands).
641
- console.error(`[nookplot-mcp] Applied ${result.applied} config entries to Hermes (agent: ${result.agentAddress.slice(0, 10)}...).`);
642
- if (result.failures.length > 0) {
643
- console.error(`[nookplot-mcp] ${result.failures.length} entries failed:`);
644
- for (const f of result.failures) {
645
- console.error(` - ${f.key}: ${f.error}`);
646
- }
647
- // Partial success is still success — the installer can continue.
648
- }
649
- return;
650
- }
651
- catch (err) {
652
- console.error("[nookplot-mcp] apply-config failed:", err instanceof Error ? err.message : String(err));
653
- // Exit non-zero so the bash installer's `set -e` surfaces the failure.
654
- process.exit(1);
655
- }
656
- }
657
- // sync-sessions subcommand — Phase 2b post-processor. Walks Hermes session
658
- // files, extracts findings + reasoning, POSTs to the capture queue. Needs
659
- // real credentials because each capture is attributed to the agent.
660
- if (command === "sync-sessions") {
661
- const creds = loadCredentials();
662
- if (!creds) {
663
- console.error("[nookplot-mcp] sync-sessions needs registered credentials. Run `nookplot-mcp setup` first.");
664
- process.exit(1);
665
- }
666
- let since;
667
- if (syncSince) {
668
- const parsed = new Date(syncSince);
669
- if (isNaN(parsed.getTime())) {
670
- console.error(`[nookplot-mcp] Invalid --since value: ${syncSince}`);
671
- process.exit(1);
672
- }
673
- since = parsed;
674
- }
675
- try {
676
- const result = await syncSessions({
677
- credentials: creds,
678
- gatewayUrl: gatewayUrlOverride ?? getGatewayUrl(creds),
679
- dryRun: syncDryRun,
680
- limit: syncLimit,
681
- force: syncForce,
682
- since,
683
- });
684
- // Human-readable summary — stderr so pipeline consumers can redirect.
685
- console.error(`[nookplot-mcp] sync-sessions: ${result.inspected} inspected, ` +
686
- `${result.processed} processed, ${result.skipped} skipped, ` +
687
- `${result.failed} failed, ${result.capturesCreated} captures ${syncDryRun ? "would be" : "created"}.`);
688
- if (syncDryRun || result.failed > 0) {
689
- for (const s of result.perSession) {
690
- if (s.status === "failed") {
691
- console.error(` FAILED ${s.sessionId}: ${s.errors.join("; ")}`);
692
- }
693
- else if (s.status === "processed" && s.errors.length > 0) {
694
- console.error(` PARTIAL ${s.sessionId}: ${s.errors.join("; ")}`);
695
- }
696
- else if (syncDryRun && s.status === "processed") {
697
- console.error(` DRY ${s.sessionId}: would capture ${s.captured} item(s)`);
698
- }
699
- }
700
- }
701
- return;
702
- }
703
- catch (err) {
704
- console.error("[nookplot-mcp] sync-sessions failed:", err instanceof Error ? err.message : String(err));
705
- process.exit(1);
706
- }
707
- }
708
165
  // All diagnostic output goes to stderr (stdout is reserved for MCP JSON-RPC in stdio mode)
709
- console.error(`[nookplot-mcp] Starting Nookplot MCP server (v${getPackageVersion()})...`);
710
- // Background version check — fire-and-forget against the npm registry.
711
- // If a newer `@latest` is published and the user's npx cache has an
712
- // older tarball (common within the first hours after publish), surface
713
- // a hint to stderr so they know to restart or wait for cache expiry.
714
- // Silent on success or network error (no noise if we can't reach npm).
715
- void checkForUpdate();
166
+ console.error("[nookplot-mcp] Starting Nookplot MCP server...");
716
167
  // 1. Load or create credentials
717
168
  let creds = loadCredentials();
718
169
  const gatewayUrl = getGatewayUrl(creds);
@@ -779,19 +230,6 @@ async function main() {
779
230
  }
780
231
  const agent = meResult.data;
781
232
  console.error(`[nookplot-mcp] Connected as ${agent.display_name || agent.address}`);
782
- // Surface profile + scope so users (and anyone debugging a support
783
- // ticket) can see at a glance which forged agent this MCP session
784
- // is acting as.
785
- if (creds.profileName || creds.scopedAgentAddress) {
786
- const profileNote = creds.profileName ? `profile: ${creds.profileName}` : "";
787
- const scopeNote = creds.scopedAgentAddress
788
- ? `scoped to ${creds.scopedAgentAddress.slice(0, 10)}...`
789
- : "";
790
- const parts = [profileNote, scopeNote].filter(Boolean).join(" · ");
791
- console.error(`[nookplot-mcp] ${parts}`);
792
- }
793
- // 2b. Install Claude Code skills (idempotent — updates on version bumps)
794
- installSkills();
795
233
  // 2b. Install Claude Code skills (idempotent — updates on version bumps)
796
234
  installSkills();
797
235
  // 3. Check for pending signals