@kigo-dev/sindri 0.1.3 → 0.1.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.
package/README.md CHANGED
@@ -27,6 +27,7 @@ names, parameters and this document are English. The contract lives in
27
27
  | `delete_app` | `app`, `confirm` | `confirm` must equal `app` (the PM typed it; the instructions forbid Claude from filling it in). Stops the preview, `DELETE /v1/apps/<app>`, then moves `~/Sindri/<app>` to `~/Sindri/.papelera/<app>-<YYYYMMDD-HHmm>` (Mexico City time) instead of deleting. A 404 from the API with the folder present still moves the folder and says so. |
28
28
  | `share` | `app` | The Slack line. |
29
29
  | `help` | – | No network. `{ summary, space_url, logged_in, apps_dir }`: the Spanish introduction Claude gives on first contact (what Sindri is, what to ask for, limits, the Mi espacio link https://sindri.kigo.dev), whether credentials are stored on this computer (keychain or file, no API call) and where the apps live. |
30
+ | `design_guide` | `app`, `topic?` | No network. Reads the `kigo-sindri-design` skill the template ships at `apps/<owner>/<app>/.claude/skills/kigo-sindri-design/`: without `topic`, `SKILL.md` plus `topics` (the file names under `references/` without `.md`); with `topic`, that reference file. `{ ok, summary, topic, topics, content }`, `content` verbatim markdown for Claude to follow (never pasted to the PM). `ok:false, code:"no_design_guide"` when the app predates the skill (Claude runs `update_template` once and retries); `code:"unknown_topic"` lists the valid topics. Also exposed as MCP resources `sindri://design-guide/<app>` and `sindri://design-guide/<app>/<topic>`. |
30
31
 
31
32
  Every result is `{ ok, summary, ...data }`. `needs_login: true` / `needs_setup: true` tell Claude
32
33
  which tool to run first. `list_apps` also returns `space_url`. The server also ships
@@ -42,7 +43,7 @@ name, typed confirmation before `delete_app`).
42
43
  **Claude Code:**
43
44
 
44
45
  ```
45
- claude mcp add sindri -- npx -y @kigo-dev/sindri@0.1.3
46
+ claude mcp add sindri -- npx -y @kigo-dev/sindri@0.1.4
46
47
  ```
47
48
 
48
49
  **Manual check without an MCP client:**
@@ -92,6 +93,15 @@ whole root on `create_app`/`rollback`/`update_template` but keeps every `node_mo
92
93
  and `.env*`. The owner is the single folder under `apps/`, else the `owner` from `GET /v1/me`
93
94
  (cached in `~/.sindri/me.json`).
94
95
 
96
+ Claude Code loads project skills from the folder it is opened in, and a PM opens the root
97
+ (`~/Sindri/<name>`), not the app dir. So after every operation that writes the tree
98
+ (`create_app`, `rollback`, `update_template`, `rename_app`) the connector puts a relative symlink at
99
+ `~/Sindri/<name>/.claude/skills/kigo-sindri-design` pointing to
100
+ `apps/<owner>/<name>/.claude/skills/kigo-sindri-design` when that folder exists (`linkDesignGuide`
101
+ in `layout.ts`). It is idempotent, skips silently when the app has no skill and never replaces a
102
+ real directory at that place. The root is neither hashed nor packed, so the link changes nothing
103
+ for `status` or `publish`; the app's own `.claude/` is part of the tree and travels to Sindri.
104
+
95
105
  `rename_app` with `new_name` moves the root and the app dir in place (nothing is downloaded) and
96
106
  then applies the one edit the builder made in its copy, `name:` in `kigo-app.yaml`, written as
97
107
  `name: "<new_name>"` (JSON-quoted, the shape `mcp` `renderManifest` uses). The tree hash the
package/dist/index.js CHANGED
@@ -34702,6 +34702,228 @@ var McpZodTypeKind;
34702
34702
  McpZodTypeKind2["Completable"] = "McpCompletable";
34703
34703
  })(McpZodTypeKind || (McpZodTypeKind = {}));
34704
34704
 
34705
+ // node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@4.5.4/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/uriTemplate.js
34706
+ var MAX_TEMPLATE_LENGTH = 1e6;
34707
+ var MAX_VARIABLE_LENGTH = 1e6;
34708
+ var MAX_TEMPLATE_EXPRESSIONS = 1e4;
34709
+ var MAX_REGEX_LENGTH = 1e6;
34710
+ var UriTemplate = class _UriTemplate {
34711
+ /**
34712
+ * Returns true if the given string contains any URI template expressions.
34713
+ * A template expression is a sequence of characters enclosed in curly braces,
34714
+ * like {foo} or {?bar}.
34715
+ */
34716
+ static isTemplate(str) {
34717
+ return /\{[^}\s]+\}/.test(str);
34718
+ }
34719
+ static validateLength(str, max, context) {
34720
+ if (str.length > max) {
34721
+ throw new Error(`${context} exceeds maximum length of ${max} characters (got ${str.length})`);
34722
+ }
34723
+ }
34724
+ get variableNames() {
34725
+ return this.parts.flatMap((part) => typeof part === "string" ? [] : part.names);
34726
+ }
34727
+ constructor(template) {
34728
+ _UriTemplate.validateLength(template, MAX_TEMPLATE_LENGTH, "Template");
34729
+ this.template = template;
34730
+ this.parts = this.parse(template);
34731
+ }
34732
+ toString() {
34733
+ return this.template;
34734
+ }
34735
+ parse(template) {
34736
+ const parts = [];
34737
+ let currentText = "";
34738
+ let i = 0;
34739
+ let expressionCount = 0;
34740
+ while (i < template.length) {
34741
+ if (template[i] === "{") {
34742
+ if (currentText) {
34743
+ parts.push(currentText);
34744
+ currentText = "";
34745
+ }
34746
+ const end = template.indexOf("}", i);
34747
+ if (end === -1)
34748
+ throw new Error("Unclosed template expression");
34749
+ expressionCount++;
34750
+ if (expressionCount > MAX_TEMPLATE_EXPRESSIONS) {
34751
+ throw new Error(`Template contains too many expressions (max ${MAX_TEMPLATE_EXPRESSIONS})`);
34752
+ }
34753
+ const expr = template.slice(i + 1, end);
34754
+ const operator = this.getOperator(expr);
34755
+ const exploded = expr.includes("*");
34756
+ const names = this.getNames(expr);
34757
+ const name = names[0];
34758
+ for (const name2 of names) {
34759
+ _UriTemplate.validateLength(name2, MAX_VARIABLE_LENGTH, "Variable name");
34760
+ }
34761
+ parts.push({ name, operator, names, exploded });
34762
+ i = end + 1;
34763
+ } else {
34764
+ currentText += template[i];
34765
+ i++;
34766
+ }
34767
+ }
34768
+ if (currentText) {
34769
+ parts.push(currentText);
34770
+ }
34771
+ return parts;
34772
+ }
34773
+ getOperator(expr) {
34774
+ const operators = ["+", "#", ".", "/", "?", "&"];
34775
+ return operators.find((op) => expr.startsWith(op)) || "";
34776
+ }
34777
+ getNames(expr) {
34778
+ const operator = this.getOperator(expr);
34779
+ return expr.slice(operator.length).split(",").map((name) => name.replace("*", "").trim()).filter((name) => name.length > 0);
34780
+ }
34781
+ encodeValue(value, operator) {
34782
+ _UriTemplate.validateLength(value, MAX_VARIABLE_LENGTH, "Variable value");
34783
+ if (operator === "+" || operator === "#") {
34784
+ return encodeURI(value);
34785
+ }
34786
+ return encodeURIComponent(value);
34787
+ }
34788
+ expandPart(part, variables) {
34789
+ if (part.operator === "?" || part.operator === "&") {
34790
+ const pairs = part.names.map((name) => {
34791
+ const value2 = variables[name];
34792
+ if (value2 === void 0)
34793
+ return "";
34794
+ const encoded2 = Array.isArray(value2) ? value2.map((v) => this.encodeValue(v, part.operator)).join(",") : this.encodeValue(value2.toString(), part.operator);
34795
+ return `${name}=${encoded2}`;
34796
+ }).filter((pair) => pair.length > 0);
34797
+ if (pairs.length === 0)
34798
+ return "";
34799
+ const separator = part.operator === "?" ? "?" : "&";
34800
+ return separator + pairs.join("&");
34801
+ }
34802
+ if (part.names.length > 1) {
34803
+ const values2 = part.names.map((name) => variables[name]).filter((v) => v !== void 0);
34804
+ if (values2.length === 0)
34805
+ return "";
34806
+ return values2.map((v) => Array.isArray(v) ? v[0] : v).join(",");
34807
+ }
34808
+ const value = variables[part.name];
34809
+ if (value === void 0)
34810
+ return "";
34811
+ const values = Array.isArray(value) ? value : [value];
34812
+ const encoded = values.map((v) => this.encodeValue(v, part.operator));
34813
+ switch (part.operator) {
34814
+ case "":
34815
+ return encoded.join(",");
34816
+ case "+":
34817
+ return encoded.join(",");
34818
+ case "#":
34819
+ return "#" + encoded.join(",");
34820
+ case ".":
34821
+ return "." + encoded.join(".");
34822
+ case "/":
34823
+ return "/" + encoded.join("/");
34824
+ default:
34825
+ return encoded.join(",");
34826
+ }
34827
+ }
34828
+ expand(variables) {
34829
+ let result = "";
34830
+ let hasQueryParam = false;
34831
+ for (const part of this.parts) {
34832
+ if (typeof part === "string") {
34833
+ result += part;
34834
+ continue;
34835
+ }
34836
+ const expanded = this.expandPart(part, variables);
34837
+ if (!expanded)
34838
+ continue;
34839
+ if ((part.operator === "?" || part.operator === "&") && hasQueryParam) {
34840
+ result += expanded.replace("?", "&");
34841
+ } else {
34842
+ result += expanded;
34843
+ }
34844
+ if (part.operator === "?" || part.operator === "&") {
34845
+ hasQueryParam = true;
34846
+ }
34847
+ }
34848
+ return result;
34849
+ }
34850
+ escapeRegExp(str) {
34851
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
34852
+ }
34853
+ partToRegExp(part) {
34854
+ const patterns = [];
34855
+ for (const name2 of part.names) {
34856
+ _UriTemplate.validateLength(name2, MAX_VARIABLE_LENGTH, "Variable name");
34857
+ }
34858
+ if (part.operator === "?" || part.operator === "&") {
34859
+ for (let i = 0; i < part.names.length; i++) {
34860
+ const name2 = part.names[i];
34861
+ const prefix = i === 0 ? "\\" + part.operator : "&";
34862
+ patterns.push({
34863
+ pattern: prefix + this.escapeRegExp(name2) + "=([^&]+)",
34864
+ name: name2
34865
+ });
34866
+ }
34867
+ return patterns;
34868
+ }
34869
+ let pattern;
34870
+ const name = part.name;
34871
+ switch (part.operator) {
34872
+ case "":
34873
+ pattern = part.exploded ? "([^/,]+(?:,[^/,]+)*)" : "([^/,]+)";
34874
+ break;
34875
+ case "+":
34876
+ case "#":
34877
+ pattern = "(.+)";
34878
+ break;
34879
+ case ".":
34880
+ pattern = "\\.([^/,]+)";
34881
+ break;
34882
+ case "/":
34883
+ pattern = "/" + (part.exploded ? "([^/,]+(?:,[^/,]+)*)" : "([^/,]+)");
34884
+ break;
34885
+ default:
34886
+ pattern = "([^/]+)";
34887
+ }
34888
+ patterns.push({ pattern, name });
34889
+ return patterns;
34890
+ }
34891
+ match(uri) {
34892
+ _UriTemplate.validateLength(uri, MAX_TEMPLATE_LENGTH, "URI");
34893
+ let pattern = "^";
34894
+ const names = [];
34895
+ for (const part of this.parts) {
34896
+ if (typeof part === "string") {
34897
+ pattern += this.escapeRegExp(part);
34898
+ } else {
34899
+ const patterns = this.partToRegExp(part);
34900
+ for (const { pattern: partPattern, name } of patterns) {
34901
+ pattern += partPattern;
34902
+ names.push({ name, exploded: part.exploded });
34903
+ }
34904
+ }
34905
+ }
34906
+ pattern += "$";
34907
+ _UriTemplate.validateLength(pattern, MAX_REGEX_LENGTH, "Generated regex pattern");
34908
+ const regex = new RegExp(pattern);
34909
+ const match = uri.match(regex);
34910
+ if (!match)
34911
+ return null;
34912
+ const result = {};
34913
+ for (let i = 0; i < names.length; i++) {
34914
+ const { name, exploded } = names[i];
34915
+ const value = match[i + 1];
34916
+ const cleanName = name.replace("*", "");
34917
+ if (exploded && value.includes(",")) {
34918
+ result[cleanName] = value.split(",");
34919
+ } else {
34920
+ result[cleanName] = value;
34921
+ }
34922
+ }
34923
+ return result;
34924
+ }
34925
+ };
34926
+
34705
34927
  // node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@4.5.4/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/toolNameValidation.js
34706
34928
  var TOOL_NAME_REGEX = /^[A-Za-z0-9._-]{1,128}$/;
34707
34929
  function validateToolName(name) {
@@ -35491,6 +35713,30 @@ var McpServer = class {
35491
35713
  }
35492
35714
  }
35493
35715
  };
35716
+ var ResourceTemplate = class {
35717
+ constructor(uriTemplate, _callbacks) {
35718
+ this._callbacks = _callbacks;
35719
+ this._uriTemplate = typeof uriTemplate === "string" ? new UriTemplate(uriTemplate) : uriTemplate;
35720
+ }
35721
+ /**
35722
+ * Gets the URI template pattern.
35723
+ */
35724
+ get uriTemplate() {
35725
+ return this._uriTemplate;
35726
+ }
35727
+ /**
35728
+ * Gets the list callback, if one was provided.
35729
+ */
35730
+ get listCallback() {
35731
+ return this._callbacks.list;
35732
+ }
35733
+ /**
35734
+ * Gets the callback for completing a specific URI template variable, if one was provided.
35735
+ */
35736
+ completeCallback(variable) {
35737
+ return this._callbacks.complete?.[variable];
35738
+ }
35739
+ };
35494
35740
  var EMPTY_OBJECT_JSON_SCHEMA = {
35495
35741
  type: "object",
35496
35742
  properties: {}
@@ -35732,6 +35978,8 @@ Flow:
35732
35978
  7. Renaming: an app has an address (https://<name>.sindri.kigo.dev) and a visible name (the title people see). When the PM says "c\xE1mbiale el nombre" and it is not obvious which one they mean, ask: "\xBFla direcci\xF3n o el nombre que se ve?". \`rename_app\` with \`new_name\` changes the address (Sindri renames it, the local folder follows, the old link stops working: tell the PM to share the new one); it refuses while there are unpublished edits, so publish first. \`rename_app\` with \`display_name\` changes only the visible name and publishes it as \xABAhora se llama \xAB<nombre>\xBB\xBB. Both in one call is fine.
35733
35979
  8. Deleting: \`delete_app\` is final in Sindri (address, versions, database). Before calling it, ask the PM to type the app's name to confirm and pass exactly what they wrote as \`confirm\`; never fill it in yourself, never guess it, never call it because you inferred they want it. The local files are moved to the Sindri papelera on their computer, not deleted; say so.
35734
35980
 
35981
+ Design guide: before creating or changing any page, component, layout, style or copy, call \`design_guide\` for the app, first without \`topic\` (the overview and the list of topics), then with the topic that matches what you are building (admin, landing, mini-app, mobile; tokens, writing and checklist when relevant). Follow it; it is for you, never paste its content to the PM. If it answers \`no_design_guide\`, run \`update_template\` once and call \`design_guide\` again.
35982
+
35735
35983
  Rules: synthetic data only (never paste real emails, plates or phone numbers into seeds); one author per app (the PM); never touch files outside the app folder; do not edit middleware.ts, next.config.ts or package.json scripts (publish will refuse them); apps work Monday to Friday 9:05 am to midnight Mexico City time and the link says so outside those hours. When something fails, explain in one line what to do next; never blame the PM.`;
35736
35984
 
35737
35985
  // src/tools.ts
@@ -36225,6 +36473,51 @@ async function appPresent(p) {
36225
36473
  return false;
36226
36474
  }
36227
36475
  }
36476
+ var DESIGN_GUIDE_REL = path4.join(".claude", "skills", "kigo-sindri-design");
36477
+ async function readDesignGuide(p, topic) {
36478
+ const dir = path4.join(p.appDir, DESIGN_GUIDE_REL);
36479
+ let skill;
36480
+ try {
36481
+ skill = await fs4.readFile(path4.join(dir, "SKILL.md"), "utf8");
36482
+ } catch {
36483
+ return null;
36484
+ }
36485
+ const topics = await designTopics(dir);
36486
+ if (topic === void 0) return { content: skill, topics };
36487
+ if (!topics.includes(topic)) return { unknown: true, topics };
36488
+ return { content: await fs4.readFile(path4.join(dir, "references", `${topic}.md`), "utf8"), topics };
36489
+ }
36490
+ async function designTopics(dir) {
36491
+ try {
36492
+ const entries = await fs4.readdir(path4.join(dir, "references"), { withFileTypes: true });
36493
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".md") && !e.name.startsWith(".")).map((e) => e.name.slice(0, -".md".length)).sort();
36494
+ } catch {
36495
+ return [];
36496
+ }
36497
+ }
36498
+ async function linkDesignGuide(p) {
36499
+ const target = path4.join(p.appDir, DESIGN_GUIDE_REL);
36500
+ try {
36501
+ if (!(await fs4.stat(target)).isDirectory()) return false;
36502
+ } catch {
36503
+ return false;
36504
+ }
36505
+ const link = path4.join(p.root, DESIGN_GUIDE_REL);
36506
+ const rel = path4.relative(path4.dirname(link), target);
36507
+ for (const parent of [path4.join(p.root, ".claude"), path4.dirname(link)]) {
36508
+ const st = await fs4.lstat(parent).catch(() => null);
36509
+ if (st && !st.isDirectory()) return false;
36510
+ }
36511
+ const existing = await fs4.lstat(link).catch(() => null);
36512
+ if (existing) {
36513
+ if (!existing.isSymbolicLink()) return false;
36514
+ if (await fs4.readlink(link) === rel) return true;
36515
+ await fs4.unlink(link);
36516
+ }
36517
+ await fs4.mkdir(path4.dirname(link), { recursive: true });
36518
+ await fs4.symlink(rel, link, "dir");
36519
+ return true;
36520
+ }
36228
36521
 
36229
36522
  // src/manifest.ts
36230
36523
  import fs5 from "node:fs/promises";
@@ -36583,7 +36876,11 @@ var SCHEMAS = {
36583
36876
  confirm: external_exports.string().describe("The app name exactly as the PM typed it to confirm. Must equal `app`; never fill it in for them.")
36584
36877
  },
36585
36878
  share: { app: appName },
36586
- help: {}
36879
+ help: {},
36880
+ design_guide: {
36881
+ app: appName,
36882
+ topic: external_exports.string().min(1).max(60).optional().describe("A reference topic from `topics` (admin, landing, mini-app, mobile, tokens, writing, checklist). Omit for the overview (SKILL.md) and the list of topics.")
36883
+ }
36587
36884
  };
36588
36885
  var DESCRIPTIONS = {
36589
36886
  setup: {
@@ -36632,6 +36929,10 @@ var DESCRIPTIONS = {
36632
36929
  help: {
36633
36930
  title: "What Sindri is",
36634
36931
  description: "No network. The Spanish introduction to give the PM the first time they speak in a conversation or whenever they ask what Sindri can do: what it is, what they can ask for, the limits and the link to Mi espacio. Also says whether they are already signed in on this computer (`logged_in`) and where their apps live (`apps_dir`)."
36932
+ },
36933
+ design_guide: {
36934
+ title: "Design guide",
36935
+ description: "No network. Kigo's design guide for Sindri apps, read from the app folder (the kigo-sindri-design skill the template ships). Call it before creating or changing any page, component, layout, style or copy: first without `topic` (the overview plus the list of `topics`), then with the topic that matches what you are building (admin, landing, mini-app, mobile). `content` is markdown for you to follow, not to paste to the PM. If it answers `no_design_guide`, the app predates the guide: run update_template once and retry."
36635
36936
  }
36636
36937
  };
36637
36938
  function createTools(deps) {
@@ -36745,6 +37046,7 @@ function createTools(deps) {
36745
37046
  }
36746
37047
  const app = await api.app(a.name).catch(() => null);
36747
37048
  await writePublished(p.appDir, { version: app?.last_version ?? null, tree_hash: await treeHash(p.appDir), url: created.url });
37049
+ await linkDesignGuide(p);
36748
37050
  return {
36749
37051
  ok: true,
36750
37052
  summary: `Cre\xE9 la app "${a.name}". Su direcci\xF3n ser\xE1 ${created.url} cuando la publiques. Ahora edita los archivos de su carpeta para construirla; cuando quieras verla, usa preview.`,
@@ -36859,6 +37161,7 @@ function createTools(deps) {
36859
37161
  const dir = p.appDir;
36860
37162
  if (!await appPresent(p)) return { ok: false, summary: `Sindri volvi\xF3 a la versi\xF3n pero lo que baj\xF3 no trae la app donde la espero. Escribe en Slack, canal #sindri.`, version: result.version, url: result.url };
36861
37163
  if (!await exists2(path9.join(dir, ".env.local"))) await fs9.writeFile(path9.join(dir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
37164
+ await linkDesignGuide(p);
36862
37165
  const tc = await toolchain();
36863
37166
  if (tc && await exists2(path9.join(p.root, "node_modules"))) {
36864
37167
  const db = await prepareLocalDb(tc, dir);
@@ -36895,6 +37198,7 @@ function createTools(deps) {
36895
37198
  const dir = p.appDir;
36896
37199
  if (!await appPresent(p)) return { ok: false, summary: `Sindri actualiz\xF3 la plantilla pero lo que baj\xF3 no trae la app donde la espero. Escribe en Slack, canal #sindri.`, ...data };
36897
37200
  if (!await exists2(path9.join(dir, ".env.local"))) await fs9.writeFile(path9.join(dir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
37201
+ await linkDesignGuide(p);
36898
37202
  const tc = await toolchain();
36899
37203
  if (tc && await exists2(path9.join(p.root, "node_modules"))) {
36900
37204
  const db = await prepareLocalDb(tc, dir);
@@ -36952,6 +37256,7 @@ function createTools(deps) {
36952
37256
  await moveDir(path9.join(newRoot, "apps", p.owner, a.app), newDir);
36953
37257
  await writeManifestValue(newDir, "name", newName);
36954
37258
  await writePublished(newDir, { version: result.version ?? before.published_version, tree_hash: await treeHash(newDir), url: result.url });
37259
+ await linkDesignGuide({ root: newRoot, appDir: newDir, owner: p.owner });
36955
37260
  data.path = newDir;
36956
37261
  }
36957
37262
  parts.push(`Listo, "${a.app}" ahora vive en ${result.url}. La direcci\xF3n anterior (${oldUrl}) ya no funciona: comparte la nueva con tu equipo.`);
@@ -36991,11 +37296,39 @@ function createTools(deps) {
36991
37296
  const summary = logged_in ? HELP_SUMMARY : `${HELP_SUMMARY} Todav\xEDa no has entrado con tu cuenta de Kigo en esta computadora; cuando quieras empezar, te abro la p\xE1gina.`;
36992
37297
  return { ok: true, summary, space_url: SPACE_URL, logged_in, apps_dir: cfg.home };
36993
37298
  });
37299
+ const design_guide = guard(async (a) => {
37300
+ if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
37301
+ const p = await appPaths(cfg, a.app);
37302
+ if (!await appPresent(p)) {
37303
+ return { ok: false, summary: `No encuentro la carpeta de "${a.app}" en tu computadora. Si la app existe en Sindri, usa rollback a su \xFAltima versi\xF3n para bajarla.`, code: "no_app", path: p.appDir };
37304
+ }
37305
+ const topic = a.topic?.trim().toLowerCase().replace(/\.md$/, "");
37306
+ const guide = await readDesignGuide(p, topic || void 0);
37307
+ if (guide === null) {
37308
+ return {
37309
+ ok: false,
37310
+ summary: `"${a.app}" todav\xEDa no trae la gu\xEDa de dise\xF1o de Sindri. Corre update_template una vez para traerla y vuelve a llamar a design_guide.`,
37311
+ code: "no_design_guide",
37312
+ topics: []
37313
+ };
37314
+ }
37315
+ if ("unknown" in guide) {
37316
+ return { ok: false, summary: `No hay un tema "${topic}" en la gu\xEDa de dise\xF1o. Los temas disponibles son: ${guide.topics.join(", ")}.`, code: "unknown_topic", topics: guide.topics };
37317
+ }
37318
+ await linkDesignGuide(p);
37319
+ return {
37320
+ ok: true,
37321
+ summary: topic ? `Gu\xEDa de dise\xF1o de Sindri (tema: ${topic}).` : `Gu\xEDa de dise\xF1o de Sindri (temas: ${guide.topics.join(", ")}).`,
37322
+ topic: topic ?? null,
37323
+ topics: guide.topics,
37324
+ content: guide.content
37325
+ };
37326
+ });
36994
37327
  const logout = guard(async () => {
36995
37328
  await clearCredentials(cfg);
36996
37329
  return { ok: true, summary: "Saliste de tu cuenta de Kigo en esta computadora." };
36997
37330
  });
36998
- return { setup, login, create_app, preview, publish, status, list_apps, versions, rollback, update_template, rename_app, delete_app, share, help, logout };
37331
+ return { setup, login, create_app, preview, publish, status, list_apps, versions, rollback, update_template, rename_app, delete_app, share, help, design_guide, logout };
36999
37332
  }
37000
37333
  var HELP_SUMMARY = "Sindri te ayuda a crear apps internas peque\xF1as para tu equipo en minutos, con datos de prueba y sin escribir c\xF3digo: t\xFA me cuentas qu\xE9 necesitas y yo la construyo. Puedes pedirme crear una app, verla en tu computadora antes de publicarla, publicarla, cambiarle el nombre o la direcci\xF3n, volver a una versi\xF3n anterior, compartirla en Slack, actualizar su plantilla o borrarla. Ten en cuenta: las apps trabajan con datos sint\xE9ticos, nunca reales; funcionan de lunes a viernes de 9:05 a 24:00, hora de la Ciudad de M\xE9xico; cada app la edita solo quien la cre\xF3; y lo que deba llegar a toda la empresa se pide a desarrollo. Tus apps y sus versiones est\xE1n en https://sindri.kigo.dev.";
37001
37334
  async function moveDir(from, to) {
@@ -37057,7 +37390,7 @@ function when(iso, now = /* @__PURE__ */ new Date()) {
37057
37390
  }
37058
37391
 
37059
37392
  // src/index.ts
37060
- var VERSION = "0.1.3";
37393
+ var VERSION = "0.1.4";
37061
37394
  function registerTools(server, tools) {
37062
37395
  for (const name of Object.keys(SCHEMAS)) {
37063
37396
  const meta3 = DESCRIPTIONS[name];
@@ -37072,9 +37405,30 @@ function registerTools(server, tools) {
37072
37405
  });
37073
37406
  }
37074
37407
  }
37408
+ function registerResources(server, tools) {
37409
+ const read = async (uri, app, topic) => {
37410
+ const r = await tools.design_guide({ app, ...topic ? { topic } : {} });
37411
+ if (!r.ok) throw new Error(r.summary);
37412
+ return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: r.content }] };
37413
+ };
37414
+ const one = (v) => Array.isArray(v) ? v[0] ?? "" : v ?? "";
37415
+ server.registerResource(
37416
+ "design-guide",
37417
+ new ResourceTemplate("sindri://design-guide/{app}", { list: void 0 }),
37418
+ { title: "Sindri design guide", description: "Kigo's design guide for an app (overview and topics), read from the app folder.", mimeType: "text/markdown" },
37419
+ (uri, vars) => read(uri, one(vars.app))
37420
+ );
37421
+ server.registerResource(
37422
+ "design-guide-topic",
37423
+ new ResourceTemplate("sindri://design-guide/{app}/{topic}", { list: void 0 }),
37424
+ { title: "Sindri design guide topic", description: "One topic of Kigo's design guide (admin, landing, mini-app, mobile, tokens, writing, checklist).", mimeType: "text/markdown" },
37425
+ (uri, vars) => read(uri, one(vars.app), one(vars.topic))
37426
+ );
37427
+ }
37075
37428
  function createServer(tools) {
37076
37429
  const server = new McpServer({ name: "sindri", version: VERSION }, { instructions: INSTRUCTIONS });
37077
37430
  registerTools(server, tools);
37431
+ registerResources(server, tools);
37078
37432
  return server;
37079
37433
  }
37080
37434
  async function callTool(tools, name, rawArgs) {
@@ -37113,5 +37467,6 @@ export {
37113
37467
  VERSION,
37114
37468
  callTool,
37115
37469
  createServer,
37470
+ registerResources,
37116
37471
  registerTools
37117
37472
  };
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "manifest_version": "0.2",
3
3
  "name": "sindri",
4
4
  "display_name": "Kigo Sindri",
5
- "version": "0.1.3",
5
+ "version": "0.1.4",
6
6
  "description": "Create, preview and publish Kigo internal apps from your Claude.",
7
7
  "long_description": "Sindri lets a Kigo product manager build small internal apps by talking to Claude. Claude edits the app on your computer; this connector installs what is needed, runs the app locally with test data, publishes it at <name>.sindri.kigo.dev and keeps versions. Sign in with your @cargomovil.com account.",
8
8
  "author": {
@@ -86,6 +86,10 @@
86
86
  {
87
87
  "name": "help",
88
88
  "description": "What Sindri is and what you can ask for"
89
+ },
90
+ {
91
+ "name": "design_guide",
92
+ "description": "Kigo's design guide for an app, read from its folder"
89
93
  }
90
94
  ],
91
95
  "keywords": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kigo-dev/sindri",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Sindri connector: an MCP server that lets a Kigo PM's Claude create, preview and publish internal apps on Kigo Sindri",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",