@aistoragedepot/mcp 0.2.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 +28 -0
  2. package/index.mjs +19 -1
  3. package/package.json +3 -2
  4. package/pull.mjs +130 -0
package/README.md CHANGED
@@ -49,6 +49,34 @@ Your stored content shows up where you actually work:
49
49
  | `AISD_BASE_URL` | no | `https://www.aistoragedepot.com` | Point at a self-hosted / dev instance if needed. |
50
50
  | `AISD_WORKSPACES` | no | your personal library | Which workspaces expose **prompts** (slash-commands). Comma-separated workspace names (e.g. `My Library,Engineering`), or `all`. Keeps the slash menu short; **search and resources still cover every workspace** regardless. |
51
51
 
52
+ ## Slash-commands (`pull`)
53
+
54
+ Not every client turns MCP prompts into `/` slash-commands (some only use the tools). To get
55
+ your library as **native** slash-commands that always work, sync it to command files:
56
+
57
+ ```bash
58
+ npx -y @aistoragedepot/mcp pull --token=aisd_your_token
59
+ ```
60
+
61
+ This writes `~/.claude/commands/aistoragedepot/*.md` — start a new chat and type
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.
71
+
72
+ | Option | Default | |
73
+ | --- | --- | --- |
74
+ | `--token=…` | `AISD_TOKEN` env | Your API token. |
75
+ | `--types=…` | `skill,prompt` | Which item types become commands (e.g. `skill` for skills only). |
76
+ | `--workspaces="A,B"` | your personal library | Which workspaces to pull (names, or `all`). |
77
+ | `--project` | off | Write to `./.claude/commands/…` (this repo) instead of your home dir. |
78
+ | `--dir=<path>` | `~/.claude/commands/aistoragedepot` | Write somewhere else entirely. |
79
+
52
80
  ## Notes
53
81
 
54
82
  - Read-only in v0.1 (it surfaces and fetches your content; it doesn't write back).
package/index.mjs CHANGED
@@ -22,6 +22,24 @@ import {
22
22
  } from "@modelcontextprotocol/sdk/types.js";
23
23
 
24
24
  const BASE_URL = (process.env.AISD_BASE_URL || "https://www.aistoragedepot.com").replace(/\/+$/, "");
25
+
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.
30
+ if (process.argv[2] === "pull") {
31
+ const { pull } = await import("./pull.mjs");
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);
41
+ }
42
+
25
43
  const TOKEN = process.env.AISD_TOKEN;
26
44
 
27
45
  if (!TOKEN) {
@@ -123,7 +141,7 @@ function fillBody(body, placeholders, args) {
123
141
  }
124
142
 
125
143
  const server = new Server(
126
- { name: "aistoragedepot", version: "0.2.0" },
144
+ { name: "aistoragedepot", version: "0.4.0" },
127
145
  { capabilities: { resources: {}, prompts: {}, tools: {} } },
128
146
  );
129
147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aistoragedepot/mcp",
3
- "version": "0.2.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": {
@@ -9,7 +9,8 @@
9
9
  "files": [
10
10
  "index.mjs",
11
11
  "README.md",
12
- "LICENSE"
12
+ "LICENSE",
13
+ "pull.mjs"
13
14
  ],
14
15
  "engines": {
15
16
  "node": ">=18"
package/pull.mjs ADDED
@@ -0,0 +1,130 @@
1
+ // `aistoragedepot-mcp pull` — sync your library into native slash-command files.
2
+ //
3
+ // MCP prompts-as-slash-commands are supported inconsistently across clients, so this writes
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 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.
10
+ import { mkdir, writeFile, rm } from "node:fs/promises";
11
+ import { homedir } from "node:os";
12
+ import { join, resolve } from "node:path";
13
+
14
+ const slug = (s) =>
15
+ String(s).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "item";
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
+ }
41
+
42
+ function parseArgs(argv) {
43
+ const o = {};
44
+ for (const a of argv) {
45
+ const m = a.match(/^--([^=]+)(?:=(.*))?$/);
46
+ if (m) o[m[1]] = m[2] ?? true;
47
+ }
48
+ return o;
49
+ }
50
+
51
+ export async function pull({ BASE_URL, argv }) {
52
+ const o = parseArgs(argv);
53
+ const token = o.token || process.env.AISD_TOKEN;
54
+ if (!token) {
55
+ console.error(
56
+ "A token is required. Pass --token=aisd_… (create one in AIStorageDepot → Settings → API tokens), or set AISD_TOKEN.",
57
+ );
58
+ process.exit(1);
59
+ }
60
+ const api = async (p) => {
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
+ });
66
+ if (r.status === 401) throw new Error("Token rejected (401) — check it in Settings → API tokens.");
67
+ if (!r.ok) throw new Error(`${p} → HTTP ${r.status}`);
68
+ return r.json();
69
+ };
70
+
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);
73
+ const wantType = (it) => {
74
+ const t = typeOf(it);
75
+ const isSkill = t === "skill";
76
+ const isPrompt = isPromptItem(it);
77
+ if (TYPES.includes("all")) return isSkill || isPrompt;
78
+ return (TYPES.includes("skill") && isSkill) || (TYPES.includes("prompt") && isPrompt) || TYPES.includes(t);
79
+ };
80
+
81
+ const workspaces = await api("/api/workspaces");
82
+ // Scope: default every workspace; --workspaces="A,B" | "personal" | "all".
83
+ const names = String(o.workspaces || "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
84
+ let scope;
85
+ if (names.includes("all") || names.includes("*")) scope = workspaces;
86
+ else if (names.length) scope = workspaces.filter((w) => names.includes((w.name || "").toLowerCase()));
87
+ else scope = workspaces.filter((w) => w.type === "PERSONAL"); // default: just your personal library
88
+
89
+ const libs = await Promise.all(
90
+ scope.map((w) => api(`/api/library?workspace=${encodeURIComponent(w.id)}`).catch(() => ({ items: [] }))),
91
+ );
92
+ const items = libs.flatMap((l) => (l.items || []).filter(wantType));
93
+
94
+ const dir = o.dir
95
+ ? resolve(String(o.dir))
96
+ : o.project
97
+ ? resolve(".claude", "commands", "aistoragedepot")
98
+ : join(homedir(), ".claude", "commands", "aistoragedepot");
99
+ await rm(dir, { recursive: true, force: true }); // clean re-sync of our namespaced folder only
100
+ await mkdir(dir, { recursive: true });
101
+
102
+ const used = new Set();
103
+ let n = 0;
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.
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
+ }
112
+ while (used.has(name)) name = `${name}-${it.id.slice(-4)}`;
113
+ used.add(name);
114
+ const body = it.body || "";
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`;
123
+ await writeFile(join(dir, `${name}.md`), content, "utf8");
124
+ n++;
125
+ }
126
+
127
+ console.log(`✓ Synced ${n} item${n === 1 ? "" : "s"} (${TYPES.join(", ")}) → ${dir}`);
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.`);
130
+ }