@aistoragedepot/mcp 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -49,6 +49,26 @@ 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 prompts as **native** slash-commands that always work, sync them 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. Re-run any time; it only clears and rewrites its own
63
+ namespaced folder.
64
+
65
+ | Option | Default | |
66
+ | --- | --- | --- |
67
+ | `--token=…` | `AISD_TOKEN` env | Your API token. |
68
+ | `--workspaces="A,B"` | your personal library | Which workspaces to pull (names, or `all`). |
69
+ | `--project` | off | Write to `./.claude/commands/…` (this repo) instead of your home dir. |
70
+ | `--dir=<path>` | `~/.claude/commands/aistoragedepot` | Write somewhere else entirely. |
71
+
52
72
  ## Notes
53
73
 
54
74
  - Read-only in v0.1 (it surfaces and fetches your content; it doesn't write back).
package/index.mjs CHANGED
@@ -22,6 +22,14 @@ 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 prompts to native slash-command files, then exits.
27
+ if (process.argv[2] === "pull") {
28
+ const { pull } = await import("./pull.mjs");
29
+ await pull({ BASE_URL, argv: process.argv.slice(3) });
30
+ process.exit(0);
31
+ }
32
+
25
33
  const TOKEN = process.env.AISD_TOKEN;
26
34
 
27
35
  if (!TOKEN) {
@@ -123,7 +131,7 @@ function fillBody(body, placeholders, args) {
123
131
  }
124
132
 
125
133
  const server = new Server(
126
- { name: "aistoragedepot", version: "0.2.0" },
134
+ { name: "aistoragedepot", version: "0.3.0" },
127
135
  { capabilities: { resources: {}, prompts: {}, tools: {} } },
128
136
  );
129
137
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aistoragedepot/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.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,90 @@
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** 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.
8
+ import { mkdir, writeFile, rm } from "node:fs/promises";
9
+ import { homedir } from "node:os";
10
+ import { join, resolve } from "node:path";
11
+
12
+ const slug = (s) =>
13
+ String(s).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "item";
14
+ const typeOf = (it) => (it.type?.slug || it.type?.name || "").toLowerCase();
15
+
16
+ function parseArgs(argv) {
17
+ const o = {};
18
+ for (const a of argv) {
19
+ const m = a.match(/^--([^=]+)(?:=(.*))?$/);
20
+ if (m) o[m[1]] = m[2] ?? true;
21
+ }
22
+ return o;
23
+ }
24
+
25
+ export async function pull({ BASE_URL, argv }) {
26
+ const o = parseArgs(argv);
27
+ const token = o.token || process.env.AISD_TOKEN;
28
+ if (!token) {
29
+ console.error(
30
+ "A token is required. Pass --token=aisd_… (create one in AIStorageDepot → Settings → API tokens), or set AISD_TOKEN.",
31
+ );
32
+ process.exit(1);
33
+ }
34
+ const api = async (p) => {
35
+ const r = await fetch(BASE_URL + p, { headers: { Authorization: `Bearer ${token}`, Accept: "application/json" } });
36
+ if (r.status === 401) throw new Error("Token rejected (401) — check it in Settings → API tokens.");
37
+ if (!r.ok) throw new Error(`${p} → HTTP ${r.status}`);
38
+ return r.json();
39
+ };
40
+
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);
43
+ const wantType = (it) => {
44
+ const t = typeOf(it);
45
+ const isSkill = t === "skill";
46
+ const isPrompt = it.type?.format === "prompt" || t === "prompt";
47
+ if (TYPES.includes("all")) return isSkill || isPrompt;
48
+ return (TYPES.includes("skill") && isSkill) || (TYPES.includes("prompt") && isPrompt) || TYPES.includes(t);
49
+ };
50
+
51
+ const workspaces = await api("/api/workspaces");
52
+ // Scope: default every workspace; --workspaces="A,B" | "personal" | "all".
53
+ const names = String(o.workspaces || "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
54
+ let scope;
55
+ if (names.includes("all") || names.includes("*")) scope = workspaces;
56
+ else if (names.length) scope = workspaces.filter((w) => names.includes((w.name || "").toLowerCase()));
57
+ else scope = workspaces.filter((w) => w.type === "PERSONAL"); // default: just your personal library
58
+
59
+ const libs = await Promise.all(
60
+ scope.map((w) => api(`/api/library?workspace=${encodeURIComponent(w.id)}`).catch(() => ({ items: [] }))),
61
+ );
62
+ const items = libs.flatMap((l) => (l.items || []).filter(wantType));
63
+
64
+ const dir = o.dir
65
+ ? resolve(String(o.dir))
66
+ : o.project
67
+ ? resolve(".claude", "commands", "aistoragedepot")
68
+ : join(homedir(), ".claude", "commands", "aistoragedepot");
69
+ await rm(dir, { recursive: true, force: true }); // clean re-sync of our namespaced folder only
70
+ await mkdir(dir, { recursive: true });
71
+
72
+ const used = new Set();
73
+ let n = 0;
74
+ for (const it of items) {
75
+ let name = slug(it.title);
76
+ while (used.has(name)) name = `${name}-${it.id.slice(-4)}`;
77
+ used.add(name);
78
+ 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`;
84
+ await writeFile(join(dir, `${name}.md`), content, "utf8");
85
+ n++;
86
+ }
87
+
88
+ console.log(`✓ Synced ${n} item${n === 1 ? "" : "s"} (${TYPES.join(", ")}) → ${dir}`);
89
+ console.log(` Start a new chat and type /aistoragedepot to use them.`);
90
+ }