@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 +11 -1
- package/dist/index.js +358 -3
- package/manifest.json +5 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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