@aistoragedepot/mcp 0.3.0 → 0.4.0

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 (4) hide show
  1. package/README.md +11 -3
  2. package/index.mjs +14 -4
  3. package/package.json +1 -1
  4. package/pull.mjs +52 -12
package/README.md CHANGED
@@ -52,19 +52,27 @@ Your stored content shows up where you actually work:
52
52
  ## Slash-commands (`pull`)
53
53
 
54
54
  Not every client turns MCP prompts into `/` slash-commands (some only use the tools). To get
55
- your prompts as **native** slash-commands that always work, sync them to command files:
55
+ your library as **native** slash-commands that always work, sync it to command files:
56
56
 
57
57
  ```bash
58
58
  npx -y @aistoragedepot/mcp pull --token=aisd_your_token
59
59
  ```
60
60
 
61
61
  This writes `~/.claude/commands/aistoragedepot/*.md` — start a new chat and type
62
- **`/aistoragedepot`** to see them. Re-run any time; it only clears and rewrites its own
63
- namespaced folder.
62
+ **`/aistoragedepot`** to see them. **Skills** run as-is. **Prompts** take input: their
63
+ `[fields]` show as an argument hint, and whatever you type after the command fills them —
64
+
65
+ ```
66
+ /aistoragedepot:cold-outreach Jane at Globex, about our payments API
67
+ ```
68
+
69
+ — with Claude asking for any value you left out. Re-run any time; it only clears and
70
+ rewrites its own namespaced folder.
64
71
 
65
72
  | Option | Default | |
66
73
  | --- | --- | --- |
67
74
  | `--token=…` | `AISD_TOKEN` env | Your API token. |
75
+ | `--types=…` | `skill,prompt` | Which item types become commands (e.g. `skill` for skills only). |
68
76
  | `--workspaces="A,B"` | your personal library | Which workspaces to pull (names, or `all`). |
69
77
  | `--project` | off | Write to `./.claude/commands/…` (this repo) instead of your home dir. |
70
78
  | `--dir=<path>` | `~/.claude/commands/aistoragedepot` | Write somewhere else entirely. |
package/index.mjs CHANGED
@@ -23,11 +23,21 @@ import {
23
23
 
24
24
  const BASE_URL = (process.env.AISD_BASE_URL || "https://www.aistoragedepot.com").replace(/\/+$/, "");
25
25
 
26
- // CLI mode: `aistoragedepot-mcp pull` syncs prompts to native slash-command files, then exits.
26
+ // CLI mode: `aistoragedepot-mcp pull` syncs the library to native slash-command files, then
27
+ // exits. The brief settle delay lets undici finish closing its sockets (Connection: close)
28
+ // before process.exit — a hard exit mid-teardown trips a libuv assert on Windows (Node 24)
29
+ // that prints a scary-but-harmless message after the sync succeeds.
27
30
  if (process.argv[2] === "pull") {
28
31
  const { pull } = await import("./pull.mjs");
29
- await pull({ BASE_URL, argv: process.argv.slice(3) });
30
- process.exit(0);
32
+ let code = 0;
33
+ try {
34
+ await pull({ BASE_URL, argv: process.argv.slice(3) });
35
+ } catch (err) {
36
+ console.error(err?.message || err);
37
+ code = 1;
38
+ }
39
+ await new Promise((r) => setTimeout(r, 50));
40
+ process.exit(code);
31
41
  }
32
42
 
33
43
  const TOKEN = process.env.AISD_TOKEN;
@@ -131,7 +141,7 @@ function fillBody(body, placeholders, args) {
131
141
  }
132
142
 
133
143
  const server = new Server(
134
- { name: "aistoragedepot", version: "0.3.0" },
144
+ { name: "aistoragedepot", version: "0.4.0" },
135
145
  { capabilities: { resources: {}, prompts: {}, tools: {} } },
136
146
  );
137
147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aistoragedepot/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Model Context Protocol server for your AIStorageDepot library — exposes your prompts, rules, docs, and skills to MCP-aware AI clients (Claude, Cursor, Cline, …).",
5
5
  "type": "module",
6
6
  "bin": {
package/pull.mjs CHANGED
@@ -2,9 +2,11 @@
2
2
  //
3
3
  // MCP prompts-as-slash-commands are supported inconsistently across clients, so this writes
4
4
  // real command files that any Claude Code client picks up: ~/.claude/commands/aistoragedepot/*.md
5
- // (a /aistoragedepot:<name> command each). Defaults to the **skills** in your personal library
6
- // (self-contained instructions that run cleanly as commands); widen with --workspaces / include
7
- // prompts with --types. Re-run any time it clears and rewrites only its own namespaced folder.
5
+ // (a /aistoragedepot:<name> command each). Defaults to the **skills and prompts** in your
6
+ // personal library. Skills run as-is; prompts become commands that take input their [fields]
7
+ // are declared as an argument-hint and filled from whatever you type after the command
8
+ // ($ARGUMENTS), with Claude asking for anything missing. Narrow with --types / widen with
9
+ // --workspaces. Re-run any time — it clears and rewrites only its own namespaced folder.
8
10
  import { mkdir, writeFile, rm } from "node:fs/promises";
9
11
  import { homedir } from "node:os";
10
12
  import { join, resolve } from "node:path";
@@ -12,6 +14,30 @@ import { join, resolve } from "node:path";
12
14
  const slug = (s) =>
13
15
  String(s).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "item";
14
16
  const typeOf = (it) => (it.type?.slug || it.type?.name || "").toLowerCase();
17
+ const isPromptItem = (it) => it.type?.format === "prompt" || typeOf(it) === "prompt";
18
+
19
+ // Prompts become commands that take input: the [fields] in the body are declared as an
20
+ // argument-hint (shown in the / picker) and filled from whatever the user types after the
21
+ // command ($ARGUMENTS); Claude asks for any value that's missing.
22
+ const FIELD_RE = /\[([A-Za-z0-9 ]+)\]/g;
23
+ const yq = (s) => String(s).replace(/"/g, "'").replace(/\s+/g, " ").trim(); // YAML-safe one-liner
24
+
25
+ function promptCommand(it) {
26
+ const body = String(it.body || "").trim();
27
+ const fields = [...new Set([...body.matchAll(FIELD_RE)].map((m) => m[1]))];
28
+ const head = ["---", `description: "${yq(`Prompt — ${it.title} (AIStorageDepot)`)}"`];
29
+ if (fields.length) head.push(`argument-hint: "${yq(fields.map((f) => `[${f}]`).join(" "))}"`);
30
+ head.push("---");
31
+ const tail = fields.length
32
+ ? [
33
+ "",
34
+ "Input: $ARGUMENTS",
35
+ "",
36
+ `Fill the bracketed ${fields.length === 1 ? "field" : "fields"} in the prompt above from the input — ask me for any value that's missing — then carry out the prompt.`,
37
+ ]
38
+ : ["", "$ARGUMENTS"]; // no fields: whatever the user types just flows in after the prompt
39
+ return [...head, "", body, ...tail, ""].join("\n");
40
+ }
15
41
 
16
42
  function parseArgs(argv) {
17
43
  const o = {};
@@ -32,18 +58,22 @@ export async function pull({ BASE_URL, argv }) {
32
58
  process.exit(1);
33
59
  }
34
60
  const api = async (p) => {
35
- const r = await fetch(BASE_URL + p, { headers: { Authorization: `Bearer ${token}`, Accept: "application/json" } });
61
+ // Connection: close keep-alive sockets left open at exit trip a libuv assert on
62
+ // Windows (Node 24), printing a scary (but harmless) message after the sync finishes.
63
+ const r = await fetch(BASE_URL + p, {
64
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json", Connection: "close" },
65
+ });
36
66
  if (r.status === 401) throw new Error("Token rejected (401) — check it in Settings → API tokens.");
37
67
  if (!r.ok) throw new Error(`${p} → HTTP ${r.status}`);
38
68
  return r.json();
39
69
  };
40
70
 
41
- // Which item types to pull. Default: skills only. --types=skill,prompt | all.
42
- const TYPES = String(o.types || "skill").toLowerCase().split(",").map((s) => s.trim()).filter(Boolean);
71
+ // Which item types to pull. Default: skills + prompts. --types=skill | prompt | all.
72
+ const TYPES = String(o.types || "skill,prompt").toLowerCase().split(",").map((s) => s.trim()).filter(Boolean);
43
73
  const wantType = (it) => {
44
74
  const t = typeOf(it);
45
75
  const isSkill = t === "skill";
46
- const isPrompt = it.type?.format === "prompt" || t === "prompt";
76
+ const isPrompt = isPromptItem(it);
47
77
  if (TYPES.includes("all")) return isSkill || isPrompt;
48
78
  return (TYPES.includes("skill") && isSkill) || (TYPES.includes("prompt") && isPrompt) || TYPES.includes(t);
49
79
  };
@@ -72,19 +102,29 @@ export async function pull({ BASE_URL, argv }) {
72
102
  const used = new Set();
73
103
  let n = 0;
74
104
  for (const it of items) {
105
+ // Clean names by default; on a title collision, try the type as a tiebreaker
106
+ // (code-review vs code-review-prompt) before falling back to an id suffix.
75
107
  let name = slug(it.title);
108
+ if (used.has(name)) {
109
+ const alt = slug(`${it.title} ${typeOf(it) || "item"}`);
110
+ name = used.has(alt) ? `${slug(it.title)}-${it.id.slice(-4)}` : alt;
111
+ }
76
112
  while (used.has(name)) name = `${name}-${it.id.slice(-4)}`;
77
113
  used.add(name);
78
114
  const body = it.body || "";
79
- // Skills already carry their own YAML frontmatter (name/description) — keep it as-is; for anything
80
- // without frontmatter (e.g. prompts), add a description so it shows nicely in the / menu.
81
- const content = /^---\r?\n/.test(body)
82
- ? body.endsWith("\n") ? body : `${body}\n`
83
- : `---\ndescription: "${`${it.title} (AIStorageDepot)`.replace(/"/g, "'").replace(/\s+/g, " ").trim()}"\n---\n\n${body}\n`;
115
+ // Prompts get the argument treatment. Skills already carry their own YAML frontmatter
116
+ // (name/description) keep them as-is; anything else without frontmatter gets a
117
+ // description so it shows nicely in the / menu.
118
+ const content = isPromptItem(it)
119
+ ? promptCommand(it)
120
+ : /^---\r?\n/.test(body)
121
+ ? body.endsWith("\n") ? body : `${body}\n`
122
+ : `---\ndescription: "${yq(`${it.title} (AIStorageDepot)`)}"\n---\n\n${body}\n`;
84
123
  await writeFile(join(dir, `${name}.md`), content, "utf8");
85
124
  n++;
86
125
  }
87
126
 
88
127
  console.log(`✓ Synced ${n} item${n === 1 ? "" : "s"} (${TYPES.join(", ")}) → ${dir}`);
89
128
  console.log(` Start a new chat and type /aistoragedepot to use them.`);
129
+ console.log(` Skills run as-is; prompts take input — type values right after the command.`);
90
130
  }