@kigo-dev/sindri 0.1.0 → 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/dist/index.js CHANGED
@@ -3262,8 +3262,8 @@ var require_utils = __commonJS({
3262
3262
  }
3263
3263
  return ind;
3264
3264
  }
3265
- function removeDotSegments(path9) {
3266
- let input2 = path9;
3265
+ function removeDotSegments(path10) {
3266
+ let input2 = path10;
3267
3267
  const output2 = [];
3268
3268
  let nextSlash = -1;
3269
3269
  let len = 0;
@@ -3672,8 +3672,8 @@ var require_schemes = __commonJS({
3672
3672
  }
3673
3673
  if (wsComponent.resourceName) {
3674
3674
  const queryIndex = wsComponent.resourceName.indexOf("?");
3675
- const path9 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
3676
- wsComponent.path = path9 && path9 !== "/" ? path9 : void 0;
3675
+ const path10 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
3676
+ wsComponent.path = path10 && path10 !== "/" ? path10 : void 0;
3677
3677
  wsComponent.query = queryIndex === -1 ? void 0 : wsComponent.resourceName.slice(queryIndex + 1);
3678
3678
  wsComponent.resourceName = void 0;
3679
3679
  }
@@ -7185,12 +7185,12 @@ var require_dist = __commonJS({
7185
7185
  throw new Error(`Unknown format "${name}"`);
7186
7186
  return f;
7187
7187
  };
7188
- function addFormats(ajv, list, fs9, exportName) {
7188
+ function addFormats(ajv, list, fs10, exportName) {
7189
7189
  var _a3;
7190
7190
  var _b;
7191
7191
  (_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
7192
7192
  for (const f of list)
7193
- ajv.addFormat(f, fs9[f]);
7193
+ ajv.addFormat(f, fs10[f]);
7194
7194
  }
7195
7195
  module.exports = exports = formatsPlugin;
7196
7196
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -7572,8 +7572,8 @@ function getErrorMap() {
7572
7572
 
7573
7573
  // node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/helpers/parseUtil.js
7574
7574
  var makeIssue = (params) => {
7575
- const { data, path: path9, errorMaps, issueData } = params;
7576
- const fullPath = [...path9, ...issueData.path || []];
7575
+ const { data, path: path10, errorMaps, issueData } = params;
7576
+ const fullPath = [...path10, ...issueData.path || []];
7577
7577
  const fullIssue = {
7578
7578
  ...issueData,
7579
7579
  path: fullPath
@@ -7688,11 +7688,11 @@ var errorUtil;
7688
7688
 
7689
7689
  // node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/types.js
7690
7690
  var ParseInputLazyPath = class {
7691
- constructor(parent, value, path9, key) {
7691
+ constructor(parent, value, path10, key) {
7692
7692
  this._cachedPath = [];
7693
7693
  this.parent = parent;
7694
7694
  this.data = value;
7695
- this._path = path9;
7695
+ this._path = path10;
7696
7696
  this._key = key;
7697
7697
  }
7698
7698
  get path() {
@@ -11580,10 +11580,10 @@ function mergeDefs(...defs) {
11580
11580
  function cloneDef(schema) {
11581
11581
  return mergeDefs(schema._zod.def);
11582
11582
  }
11583
- function getElementAtPath(obj, path9) {
11584
- if (!path9)
11583
+ function getElementAtPath(obj, path10) {
11584
+ if (!path10)
11585
11585
  return obj;
11586
- return path9.reduce((acc, key) => acc?.[key], obj);
11586
+ return path10.reduce((acc, key) => acc?.[key], obj);
11587
11587
  }
11588
11588
  function promiseAllObject(promisesObj) {
11589
11589
  const keys = Object.keys(promisesObj);
@@ -11995,11 +11995,11 @@ function explicitlyAborted(x, startIndex = 0) {
11995
11995
  }
11996
11996
  return false;
11997
11997
  }
11998
- function prefixIssues(path9, issues) {
11998
+ function prefixIssues(path10, issues) {
11999
11999
  return issues.map((iss) => {
12000
12000
  var _a3;
12001
12001
  (_a3 = iss).path ?? (_a3.path = []);
12002
- iss.path.unshift(path9);
12002
+ iss.path.unshift(path10);
12003
12003
  return iss;
12004
12004
  });
12005
12005
  }
@@ -12432,16 +12432,16 @@ function flattenError(error61, mapper = (issue2) => issue2.message) {
12432
12432
  }
12433
12433
  function formatError(error61, mapper = (issue2) => issue2.message) {
12434
12434
  const fieldErrors = { _errors: [] };
12435
- const processError = (error62, path9 = []) => {
12435
+ const processError = (error62, path10 = []) => {
12436
12436
  for (const issue2 of error62.issues) {
12437
12437
  if (issue2.code === "invalid_union" && issue2.errors.length) {
12438
- issue2.errors.map((issues) => processError({ issues }, [...path9, ...issue2.path]));
12438
+ issue2.errors.map((issues) => processError({ issues }, [...path10, ...issue2.path]));
12439
12439
  } else if (issue2.code === "invalid_key") {
12440
- processError({ issues: issue2.issues }, [...path9, ...issue2.path]);
12440
+ processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
12441
12441
  } else if (issue2.code === "invalid_element") {
12442
- processError({ issues: issue2.issues }, [...path9, ...issue2.path]);
12442
+ processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
12443
12443
  } else {
12444
- const fullpath = [...path9, ...issue2.path];
12444
+ const fullpath = [...path10, ...issue2.path];
12445
12445
  if (fullpath.length === 0) {
12446
12446
  fieldErrors._errors.push(mapper(issue2));
12447
12447
  } else {
@@ -12480,17 +12480,17 @@ function formatError(error61, mapper = (issue2) => issue2.message) {
12480
12480
  }
12481
12481
  function treeifyError(error61, mapper = (issue2) => issue2.message) {
12482
12482
  const result = { errors: [] };
12483
- const processError = (error62, path9 = []) => {
12483
+ const processError = (error62, path10 = []) => {
12484
12484
  var _a3;
12485
12485
  for (const issue2 of error62.issues) {
12486
12486
  if (issue2.code === "invalid_union" && issue2.errors.length) {
12487
- issue2.errors.map((issues) => processError({ issues }, [...path9, ...issue2.path]));
12487
+ issue2.errors.map((issues) => processError({ issues }, [...path10, ...issue2.path]));
12488
12488
  } else if (issue2.code === "invalid_key") {
12489
- processError({ issues: issue2.issues }, [...path9, ...issue2.path]);
12489
+ processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
12490
12490
  } else if (issue2.code === "invalid_element") {
12491
- processError({ issues: issue2.issues }, [...path9, ...issue2.path]);
12491
+ processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
12492
12492
  } else {
12493
- const fullpath = [...path9, ...issue2.path];
12493
+ const fullpath = [...path10, ...issue2.path];
12494
12494
  if (fullpath.length === 0) {
12495
12495
  result.errors.push(mapper(issue2));
12496
12496
  continue;
@@ -12529,8 +12529,8 @@ function treeifyError(error61, mapper = (issue2) => issue2.message) {
12529
12529
  }
12530
12530
  function toDotPath(_path) {
12531
12531
  const segs = [];
12532
- const path9 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
12533
- for (const seg of path9) {
12532
+ const path10 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
12533
+ for (const seg of path10) {
12534
12534
  if (typeof seg === "number")
12535
12535
  segs.push(`[${seg}]`);
12536
12536
  else if (typeof seg === "symbol")
@@ -27511,11 +27511,11 @@ function normalizeObjectSchema(schema) {
27511
27511
  }
27512
27512
  return void 0;
27513
27513
  }
27514
- function getDotPath(path9) {
27515
- if (path9.length === 0) {
27514
+ function getDotPath(path10) {
27515
+ if (path10.length === 0) {
27516
27516
  return "object root";
27517
27517
  }
27518
- return path9.reduce((acc, seg, index) => {
27518
+ return path10.reduce((acc, seg, index) => {
27519
27519
  if (index === 0) {
27520
27520
  return String(seg);
27521
27521
  }
@@ -29678,13 +29678,13 @@ function resolveRef(ref, ctx) {
29678
29678
  if (!ref.startsWith("#")) {
29679
29679
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
29680
29680
  }
29681
- const path9 = ref.slice(1).split("/").filter(Boolean);
29682
- if (path9.length === 0) {
29681
+ const path10 = ref.slice(1).split("/").filter(Boolean);
29682
+ if (path10.length === 0) {
29683
29683
  return ctx.rootSchema;
29684
29684
  }
29685
29685
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
29686
- if (path9[0] === defsKey) {
29687
- const key = path9[1] === void 0 ? void 0 : decodeJSONPointerSegment(path9[1]);
29686
+ if (path10[0] === defsKey) {
29687
+ const key = path10[1] === void 0 ? void 0 : decodeJSONPointerSegment(path10[1]);
29688
29688
  if (!key || !ctx.defs[key]) {
29689
29689
  throw new Error(`Reference not found: ${ref}`);
29690
29690
  }
@@ -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: {}
@@ -35677,6 +35923,7 @@ var StdioServerTransport = class {
35677
35923
  // src/config.ts
35678
35924
  import os from "node:os";
35679
35925
  import path from "node:path";
35926
+ var SPACE_URL = "https://sindri.kigo.dev";
35680
35927
  var DEFAULTS = {
35681
35928
  apiUrl: "https://api.sindri.kigo.dev",
35682
35929
  issuer: "https://id.kigo.dev",
@@ -35719,18 +35966,25 @@ var INSTRUCTIONS = `You are helping a Kigo product manager (PM) build small inte
35719
35966
 
35720
35967
  Language and tone: always Spanish, "t\xFA", short sentences. Say "app" (feminine: "la app", "tu app"), never "herramienta", "proyecto", "repo", "deploy" or "build". Never paste file paths, commands, logs or stack traces; summarize in one line what happened and what you will do. The \`summary\` field of every tool result is written for the PM: repeat it as is or paraphrase it.
35721
35968
 
35969
+ First contact: the first time the PM speaks in a conversation, or whenever they ask what Sindri is or can do, call \`help\` and relay its \`summary\` (that is the introduction: what Sindri is, what they can ask for, the limits, the link to Mi espacio), then ask what they want to build. Do not repeat it later in the same conversation. When the PM asks where to see their apps or their versions, give them the Mi espacio link (\`space_url\`, https://sindri.kigo.dev).
35970
+
35722
35971
  Flow:
35723
35972
  1. First time on a machine: run \`setup\` (installs Node and pnpm without admin rights) and \`login\` (opens the browser on Kigo's sign-in page; the PM enters with their @cargomovil.com Google account and the browser comes back on its own with "Listo, ya entraste a Sindri"; tell them to return to Claude). \`login\` waits up to five minutes. If any tool answers \`needs_setup\` or \`needs_login\`, run that tool and retry once.
35724
35973
  2. New app: ask at most one question if the request is vague (for whom, what it must show), then \`create_app\` with a short lowercase name (it becomes the address https://<name>.sindri.kigo.dev) and a one-sentence description in Spanish. Then edit the files in the folder the tool returns to build what the PM asked. The template is Next.js + Drizzle; read its CLAUDE.md first; keep the UI in Spanish, "t\xFA", with the Kigo components it ships.
35725
35974
  3. Show before shipping: run \`preview\` and tell the PM to look at the local address; ask what to change. Edits reload live.
35726
- 4. Ship: \`publish\` with a message in the PM's words ("Filtro por fecha"). Give the URL and say the team enters with their Google de Kigo. If publish fails, the published version did not change: say what broke in one line, fix it, publish again.
35975
+ 4. Ship: \`publish\` with a message in the PM's words ("Filtro por fecha"). Give the URL and say the team enters with their Google de Kigo. If publish fails, the published version did not change: say what broke in one line, fix it, publish again. The published app has its own database: publish runs the migrations and, when this version changed drizzle/ or src/db/seed.ts, loads the test data again; rows the PM typed into the local preview never travel. Keep test data in the seed, not in the UI.
35727
35976
  5. Later: \`status\` before working on an existing app (tells you whether the local folder is behind or has unpublished edits); \`versions\` and \`rollback\` when the PM wants "como estaba ayer"; \`share\` for the Slack line.
35977
+ 6. Template updates: when the PM says "actualiza la plantilla" or Sindri announces a template fix, run \`update_template\`. It replaces only the files the app never customised, publishes \xABPlantilla actualizada\xBB and brings the local folder to it; the files you had changed are listed in \`skipped\` so you can apply the same change there by hand if it matters. It refuses while there are unpublished local edits: publish them first (or tell the PM what would be lost) and retry.
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.
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.
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.
35728
35982
 
35729
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.`;
35730
35984
 
35731
35985
  // src/tools.ts
35732
- import fs8 from "node:fs/promises";
35733
- import path8 from "node:path";
35986
+ import fs9 from "node:fs/promises";
35987
+ import path9 from "node:path";
35734
35988
 
35735
35989
  // src/api.ts
35736
35990
  import fs3 from "node:fs/promises";
@@ -35814,8 +36068,7 @@ var KEYCHAIN_SERVICE = "sindri";
35814
36068
  async function loadCredentials(cfg) {
35815
36069
  if (cfg.credentialStore === "keychain") {
35816
36070
  const r = await run("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", cfg.issuer, "-w"], { timeoutMs: 1e4 });
35817
- if (r.code !== 0) return null;
35818
- return parse3(r.stdout.trim());
36071
+ if (r.code === 0) return parse3(r.stdout.trim());
35819
36072
  }
35820
36073
  try {
35821
36074
  return parse3(await fs2.readFile(credentialsFile(cfg), "utf8"));
@@ -35861,7 +36114,7 @@ async function deviceStart(cfg, fetchImpl = fetch) {
35861
36114
  if (!discovery.device_authorization_endpoint) throw new Error("Identity does not offer the device flow");
35862
36115
  const body = new URLSearchParams({
35863
36116
  client_id: cfg.clientId,
35864
- scope: "openid profile email offline_access",
36117
+ scope: SCOPES,
35865
36118
  audience: cfg.audience
35866
36119
  });
35867
36120
  const res = await fetchImpl(discovery.device_authorization_endpoint, {
@@ -35903,12 +36156,16 @@ async function devicePoll(cfg, discovery, start, opts = {}) {
35903
36156
  }
35904
36157
  throw new Error("expired");
35905
36158
  }
36159
+ var SCOPES = "openid profile email offline_access";
35906
36160
  async function refresh(cfg, creds, fetchImpl = fetch) {
35907
36161
  if (!creds.refresh_token) throw new NeedsLogin();
35908
36162
  const discovery = await discover(cfg, fetchImpl);
35909
36163
  const res = await fetchImpl(discovery.token_endpoint, {
35910
36164
  method: "POST",
35911
36165
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
36166
+ // No `scope` here: Identity rejects it on refresh (invalid_scope). It also issues the refreshed
36167
+ // token without scopes (bug reported); the API copes by remembering the email per subject and
36168
+ // `no_email` is treated as needs_login (api.ts).
35912
36169
  body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: creds.refresh_token, client_id: cfg.clientId })
35913
36170
  });
35914
36171
  if (!res.ok) throw new NeedsLogin();
@@ -35977,7 +36234,7 @@ async function authCodeLogin(cfg, opts) {
35977
36234
  authorize.search = new URLSearchParams({
35978
36235
  client_id: cfg.clientId,
35979
36236
  response_type: "code",
35980
- scope: "openid profile email offline_access",
36237
+ scope: SCOPES,
35981
36238
  redirect_uri: redirectUri,
35982
36239
  state,
35983
36240
  code_challenge: challenge,
@@ -36095,6 +36352,18 @@ var ApiClient = class {
36095
36352
  rollback(name, version2) {
36096
36353
  return this.json("POST", `/v1/apps/${enc(name)}/rollback`, { version: version2 }, 12e4);
36097
36354
  }
36355
+ /** Brings the app's never-customised files up to the current template; `skipped` are the ones Claude changed. */
36356
+ updateTemplate(name) {
36357
+ return this.json("POST", `/v1/apps/${enc(name)}/template`, {}, 32e4);
36358
+ }
36359
+ /** Changes the app's address; the builder renames branch and folder, the old address stops resolving. */
36360
+ rename(name, newName) {
36361
+ return this.json("POST", `/v1/apps/${enc(name)}/rename`, { new_name: newName }, 15e4);
36362
+ }
36363
+ /** Removes the app from Sindri. `confirm` must equal the name; the API refuses otherwise. */
36364
+ deleteApp(name, confirm) {
36365
+ return this.json("DELETE", `/v1/apps/${enc(name)}`, { confirm }, 12e4);
36366
+ }
36098
36367
  share(name) {
36099
36368
  return this.json("GET", `/v1/apps/${enc(name)}/share`);
36100
36369
  }
@@ -36143,6 +36412,7 @@ var ApiClient = class {
36143
36412
  const raw = await res.text();
36144
36413
  try {
36145
36414
  const j = JSON.parse(raw);
36415
+ if (res.status === 403 && j.code === "no_email") return new NeedsLogin();
36146
36416
  if (j.error) return new ApiError(res.status, j.error, j.code);
36147
36417
  } catch {
36148
36418
  }
@@ -36203,99 +36473,87 @@ async function appPresent(p) {
36203
36473
  return false;
36204
36474
  }
36205
36475
  }
36206
-
36207
- // src/preview.ts
36208
- import fs5 from "node:fs/promises";
36209
- import net from "node:net";
36210
- import path5 from "node:path";
36211
- function registryFile(cfg) {
36212
- return path5.join(cfg.stateDir, "previews.json");
36213
- }
36214
- async function readRegistry(cfg) {
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;
36215
36480
  try {
36216
- return JSON.parse(await fs5.readFile(registryFile(cfg), "utf8"));
36481
+ skill = await fs4.readFile(path4.join(dir, "SKILL.md"), "utf8");
36217
36482
  } catch {
36218
- return {};
36483
+ return null;
36219
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 };
36220
36489
  }
36221
- async function writeRegistry(cfg, reg) {
36222
- await fs5.mkdir(cfg.stateDir, { recursive: true, mode: 448 });
36223
- await fs5.writeFile(registryFile(cfg), JSON.stringify(reg, null, 2) + "\n");
36224
- }
36225
- async function freePort(from = 3e3, to = 3099) {
36226
- for (let p = from; p <= to; p++) {
36227
- if (await portFree(p)) return p;
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 [];
36228
36496
  }
36229
- throw new Error("no free port between 3000 and 3099");
36230
36497
  }
36231
- function portFree(port) {
36232
- return new Promise((resolve) => {
36233
- const srv = net.createServer();
36234
- srv.once("error", () => resolve(false));
36235
- srv.listen(port, "127.0.0.1", () => srv.close(() => resolve(true)));
36236
- });
36237
- }
36238
- async function healthy(port, fetchImpl = fetch) {
36498
+ async function linkDesignGuide(p) {
36499
+ const target = path4.join(p.appDir, DESIGN_GUIDE_REL);
36239
36500
  try {
36240
- const res = await fetchImpl(`http://127.0.0.1:${port}/healthz`, { signal: AbortSignal.timeout(3e3) });
36241
- return res.ok;
36501
+ if (!(await fs4.stat(target)).isDirectory()) return false;
36242
36502
  } catch {
36243
36503
  return false;
36244
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;
36245
36520
  }
36246
- async function runningPreview(cfg, app, fetchImpl = fetch) {
36247
- const reg = await readRegistry(cfg);
36248
- const e = reg[app];
36249
- if (!e) return null;
36250
- if (alive(e.pid) && await healthy(e.port, fetchImpl)) return e;
36251
- delete reg[app];
36252
- await writeRegistry(cfg, reg);
36253
- return null;
36254
- }
36255
- async function startPreview(cfg, tc, app, dir, opts = {}) {
36256
- const fetchImpl = opts.fetchImpl ?? fetch;
36257
- const existing = await runningPreview(cfg, app, fetchImpl);
36258
- if (existing) return { port: existing.port, url: `http://127.0.0.1:${existing.port}`, reused: true };
36259
- const port = await freePort();
36260
- await fs5.mkdir(path5.join(dir, ".sindri"), { recursive: true });
36261
- const logFile = path5.join(dir, ".sindri", "preview.log");
36262
- await fs5.writeFile(logFile, "");
36263
- const env = { ...tc.env, KIGO_ENV: "local", DATABASE_URL: "", PORT: String(port), CI: "1", FORCE_COLOR: "0", BROWSER: "none" };
36264
- const pid = spawnDetached(tc.pnpm, ["dev", "--port", String(port)], { cwd: dir, env, logFile });
36265
- const reg = await readRegistry(cfg);
36266
- reg[app] = { pid, port, dir, started_at: (/* @__PURE__ */ new Date()).toISOString() };
36267
- await writeRegistry(cfg, reg);
36268
- const deadline = Date.now() + (opts.waitMs ?? 6e4);
36269
- const sleepImpl = opts.sleepImpl ?? sleep;
36270
- while (Date.now() < deadline) {
36271
- if (await healthy(port, fetchImpl)) return { port, url: `http://127.0.0.1:${port}`, reused: false };
36272
- if (!alive(pid)) break;
36273
- await sleepImpl(1e3);
36521
+
36522
+ // src/manifest.ts
36523
+ import fs5 from "node:fs/promises";
36524
+ import path5 from "node:path";
36525
+ var MANIFEST_FILE = "kigo-app.yaml";
36526
+ var KEY_RE = /^([a-z_]+):\s*(.*)$/;
36527
+ function setManifestValue(text, key, value) {
36528
+ const line = `${key}: ${JSON.stringify(value)}`;
36529
+ const lines = text.split("\n");
36530
+ const idx = lines.findIndex((l) => KEY_RE.exec(l)?.[1] === key);
36531
+ if (idx >= 0) {
36532
+ lines[idx] = line;
36533
+ } else {
36534
+ if (lines.at(-1) === "") lines.splice(lines.length - 1, 0, line);
36535
+ else lines.push(line, "");
36274
36536
  }
36275
- killTree(pid);
36276
- delete reg[app];
36277
- await writeRegistry(cfg, reg);
36278
- const log = await fs5.readFile(logFile, "utf8").catch(() => "");
36279
- return { port, url: `http://127.0.0.1:${port}`, reused: false, log: log.split("\n").slice(-15).join("\n") };
36537
+ return lines.join("\n");
36280
36538
  }
36281
- async function stopPreview(cfg, app) {
36282
- const reg = await readRegistry(cfg);
36283
- const e = reg[app];
36284
- if (!e) return false;
36285
- killTree(e.pid);
36286
- delete reg[app];
36287
- await writeRegistry(cfg, reg);
36288
- return true;
36539
+ async function readManifest(appDir) {
36540
+ return fs5.readFile(path5.join(appDir, MANIFEST_FILE), "utf8").catch(() => "");
36289
36541
  }
36290
- async function openInBrowser(cfg, url2) {
36291
- if (!cfg.opener) return;
36292
- await run(cfg.opener, [url2], { timeoutMs: 1e4 });
36542
+ async function writeManifestValue(appDir, key, value) {
36543
+ const text = await readManifest(appDir);
36544
+ await fs5.writeFile(path5.join(appDir, MANIFEST_FILE), setManifestValue(text, key, value));
36293
36545
  }
36294
36546
 
36547
+ // src/preview.ts
36548
+ import fs7 from "node:fs/promises";
36549
+ import net from "node:net";
36550
+ import path7 from "node:path";
36551
+
36295
36552
  // src/toolchain.ts
36296
36553
  import fs6 from "node:fs/promises";
36297
36554
  import path6 from "node:path";
36298
36555
  var MIN_NODE = 20;
36556
+ var PNPM_ARGS = ["--config.verify-deps-before-run=false"];
36299
36557
  async function resolveToolchain(cfg, env = process.env) {
36300
36558
  const fnmBin = await fnmDefaultBin(cfg);
36301
36559
  if (fnmBin) {
@@ -36387,11 +36645,96 @@ async function installFnmLts(cfg, env) {
36387
36645
  if (prep.code !== 0) throw new Error(`corepack prepare pnpm failed: ${prep.stderr.trim().slice(-300)}`);
36388
36646
  }
36389
36647
 
36648
+ // src/preview.ts
36649
+ function registryFile(cfg) {
36650
+ return path7.join(cfg.stateDir, "previews.json");
36651
+ }
36652
+ async function readRegistry(cfg) {
36653
+ try {
36654
+ return JSON.parse(await fs7.readFile(registryFile(cfg), "utf8"));
36655
+ } catch {
36656
+ return {};
36657
+ }
36658
+ }
36659
+ async function writeRegistry(cfg, reg) {
36660
+ await fs7.mkdir(cfg.stateDir, { recursive: true, mode: 448 });
36661
+ await fs7.writeFile(registryFile(cfg), JSON.stringify(reg, null, 2) + "\n");
36662
+ }
36663
+ async function freePort(from = 3e3, to = 3099) {
36664
+ for (let p = from; p <= to; p++) {
36665
+ if (await portFree(p)) return p;
36666
+ }
36667
+ throw new Error("no free port between 3000 and 3099");
36668
+ }
36669
+ function portFree(port) {
36670
+ return new Promise((resolve) => {
36671
+ const srv = net.createServer();
36672
+ srv.once("error", () => resolve(false));
36673
+ srv.listen(port, "127.0.0.1", () => srv.close(() => resolve(true)));
36674
+ });
36675
+ }
36676
+ async function healthy(port, fetchImpl = fetch) {
36677
+ try {
36678
+ const res = await fetchImpl(`http://127.0.0.1:${port}/healthz`, { signal: AbortSignal.timeout(3e3) });
36679
+ return res.ok;
36680
+ } catch {
36681
+ return false;
36682
+ }
36683
+ }
36684
+ async function runningPreview(cfg, app, fetchImpl = fetch) {
36685
+ const reg = await readRegistry(cfg);
36686
+ const e = reg[app];
36687
+ if (!e) return null;
36688
+ if (alive(e.pid) && await healthy(e.port, fetchImpl)) return e;
36689
+ delete reg[app];
36690
+ await writeRegistry(cfg, reg);
36691
+ return null;
36692
+ }
36693
+ async function startPreview(cfg, tc, app, dir, opts = {}) {
36694
+ const fetchImpl = opts.fetchImpl ?? fetch;
36695
+ const existing = await runningPreview(cfg, app, fetchImpl);
36696
+ if (existing) return { port: existing.port, url: `http://127.0.0.1:${existing.port}`, reused: true };
36697
+ const port = await freePort();
36698
+ await fs7.mkdir(path7.join(dir, ".sindri"), { recursive: true });
36699
+ const logFile = path7.join(dir, ".sindri", "preview.log");
36700
+ await fs7.writeFile(logFile, "");
36701
+ const env = { ...tc.env, KIGO_ENV: "local", DATABASE_URL: "", PORT: String(port), CI: "1", FORCE_COLOR: "0", BROWSER: "none" };
36702
+ const pid = spawnDetached(tc.pnpm, [...PNPM_ARGS, "dev", "--port", String(port)], { cwd: dir, env, logFile });
36703
+ const reg = await readRegistry(cfg);
36704
+ reg[app] = { pid, port, dir, started_at: (/* @__PURE__ */ new Date()).toISOString() };
36705
+ await writeRegistry(cfg, reg);
36706
+ const deadline = Date.now() + (opts.waitMs ?? 6e4);
36707
+ const sleepImpl = opts.sleepImpl ?? sleep;
36708
+ while (Date.now() < deadline) {
36709
+ if (await healthy(port, fetchImpl)) return { port, url: `http://127.0.0.1:${port}`, reused: false };
36710
+ if (!alive(pid)) break;
36711
+ await sleepImpl(1e3);
36712
+ }
36713
+ killTree(pid);
36714
+ delete reg[app];
36715
+ await writeRegistry(cfg, reg);
36716
+ const log = await fs7.readFile(logFile, "utf8").catch(() => "");
36717
+ return { port, url: `http://127.0.0.1:${port}`, reused: false, log: log.split("\n").slice(-15).join("\n") };
36718
+ }
36719
+ async function stopPreview(cfg, app) {
36720
+ const reg = await readRegistry(cfg);
36721
+ const e = reg[app];
36722
+ if (!e) return false;
36723
+ killTree(e.pid);
36724
+ delete reg[app];
36725
+ await writeRegistry(cfg, reg);
36726
+ return true;
36727
+ }
36728
+ async function openInBrowser(cfg, url2) {
36729
+ if (!cfg.opener) return;
36730
+ await run(cfg.opener, [url2], { timeoutMs: 1e4 });
36731
+ }
36732
+
36390
36733
  // src/tree.ts
36391
36734
  import { createHash as createHash2 } from "node:crypto";
36392
- import fs7 from "node:fs/promises";
36735
+ import fs8 from "node:fs/promises";
36393
36736
  import os3 from "node:os";
36394
- import path7 from "node:path";
36737
+ import path8 from "node:path";
36395
36738
  var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".sindri", "dist", ".turbo", ".git"]);
36396
36739
  function excluded(name) {
36397
36740
  if (EXCLUDED_DIRS.has(name)) return true;
@@ -36402,11 +36745,11 @@ function excluded(name) {
36402
36745
  async function listFiles(root) {
36403
36746
  const out = [];
36404
36747
  async function walk(dir, rel) {
36405
- const entries = await fs7.readdir(dir, { withFileTypes: true });
36748
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
36406
36749
  for (const e of entries) {
36407
36750
  if (excluded(e.name)) continue;
36408
36751
  const r = rel ? `${rel}/${e.name}` : e.name;
36409
- if (e.isDirectory()) await walk(path7.join(dir, e.name), r);
36752
+ if (e.isDirectory()) await walk(path8.join(dir, e.name), r);
36410
36753
  else if (e.isFile()) out.push(r);
36411
36754
  }
36412
36755
  }
@@ -36417,7 +36760,7 @@ async function treeHash(root) {
36417
36760
  const files = await listFiles(root);
36418
36761
  const h = createHash2("sha256");
36419
36762
  for (const rel of files) {
36420
- const fh = createHash2("sha256").update(await fs7.readFile(path7.join(root, rel))).digest("hex");
36763
+ const fh = createHash2("sha256").update(await fs8.readFile(path8.join(root, rel))).digest("hex");
36421
36764
  h.update(`${rel}\0${fh}
36422
36765
  `);
36423
36766
  }
@@ -36426,49 +36769,49 @@ async function treeHash(root) {
36426
36769
  async function packTree(root) {
36427
36770
  const files = await listFiles(root);
36428
36771
  if (files.length === 0) throw new Error("empty tree");
36429
- const listFile = path7.join(await fs7.mkdtemp(path7.join(os3.tmpdir(), "sindri-pack-")), "files.txt");
36430
- await fs7.writeFile(listFile, files.join("\n") + "\n");
36431
- const out = path7.join(path7.dirname(listFile), "app.tar.gz");
36772
+ const listFile = path8.join(await fs8.mkdtemp(path8.join(os3.tmpdir(), "sindri-pack-")), "files.txt");
36773
+ await fs8.writeFile(listFile, files.join("\n") + "\n");
36774
+ const out = path8.join(path8.dirname(listFile), "app.tar.gz");
36432
36775
  const r = await run("tar", ["-czf", out, "-C", root, "-T", listFile], { timeoutMs: 12e4 });
36433
36776
  if (r.code !== 0) throw new Error(`tar failed: ${r.stderr.trim()}`);
36434
- const st = await fs7.stat(out);
36777
+ const st = await fs8.stat(out);
36435
36778
  return { file: out, files: files.length, bytes: st.size };
36436
36779
  }
36437
36780
  async function replaceTree(root, archive) {
36438
- const tmp = await fs7.mkdtemp(path7.join(os3.tmpdir(), "sindri-unpack-"));
36781
+ const tmp = await fs8.mkdtemp(path8.join(os3.tmpdir(), "sindri-unpack-"));
36439
36782
  const r = await run("tar", ["-xzf", archive, "-C", tmp], { timeoutMs: 12e4 });
36440
36783
  if (r.code !== 0) throw new Error(`tar failed: ${r.stderr.trim()}`);
36441
36784
  const src = await singleRoot(tmp);
36442
- await fs7.mkdir(root, { recursive: true });
36785
+ await fs8.mkdir(root, { recursive: true });
36443
36786
  await prune(root, 0);
36444
- await fs7.cp(src, root, { recursive: true, force: true, filter: (s) => path7.basename(s) !== "node_modules" && path7.basename(s) !== ".DS_Store" });
36445
- await fs7.rm(tmp, { recursive: true, force: true });
36787
+ await fs8.cp(src, root, { recursive: true, force: true, filter: (s) => path8.basename(s) !== "node_modules" && path8.basename(s) !== ".DS_Store" });
36788
+ await fs8.rm(tmp, { recursive: true, force: true });
36446
36789
  }
36447
36790
  async function prune(dir, depth) {
36448
- const entries = await fs7.readdir(dir, { withFileTypes: true });
36791
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
36449
36792
  for (const e of entries) {
36450
- const p = path7.join(dir, e.name);
36793
+ const p = path8.join(dir, e.name);
36451
36794
  if (e.name === "node_modules") continue;
36452
36795
  if (depth === 3 && (e.name === ".sindri" || /^\.env(\..*)?$/.test(e.name))) continue;
36453
36796
  if (e.isDirectory()) {
36454
36797
  await prune(p, depth + 1);
36455
- const left = await fs7.readdir(p);
36456
- if (left.length === 0) await fs7.rmdir(p);
36798
+ const left = await fs8.readdir(p);
36799
+ if (left.length === 0) await fs8.rmdir(p);
36457
36800
  } else {
36458
- await fs7.rm(p, { force: true });
36801
+ await fs8.rm(p, { force: true });
36459
36802
  }
36460
36803
  }
36461
36804
  }
36462
36805
  async function singleRoot(dir) {
36463
- const entries = (await fs7.readdir(dir, { withFileTypes: true })).filter((e) => e.name !== ".DS_Store");
36464
- if (entries.length === 1 && entries[0]?.isDirectory() && !await exists2(path7.join(dir, "package.json"))) {
36465
- return path7.join(dir, entries[0].name);
36806
+ const entries = (await fs8.readdir(dir, { withFileTypes: true })).filter((e) => e.name !== ".DS_Store");
36807
+ if (entries.length === 1 && entries[0]?.isDirectory() && !await exists2(path8.join(dir, "package.json"))) {
36808
+ return path8.join(dir, entries[0].name);
36466
36809
  }
36467
36810
  return dir;
36468
36811
  }
36469
36812
  async function exists2(p) {
36470
36813
  try {
36471
- await fs7.access(p);
36814
+ await fs8.access(p);
36472
36815
  return true;
36473
36816
  } catch {
36474
36817
  return false;
@@ -36476,14 +36819,14 @@ async function exists2(p) {
36476
36819
  }
36477
36820
  async function readPublished(root) {
36478
36821
  try {
36479
- return JSON.parse(await fs7.readFile(path7.join(root, ".sindri", "last-published.json"), "utf8"));
36822
+ return JSON.parse(await fs8.readFile(path8.join(root, ".sindri", "last-published.json"), "utf8"));
36480
36823
  } catch {
36481
36824
  return null;
36482
36825
  }
36483
36826
  }
36484
36827
  async function writePublished(root, p) {
36485
- await fs7.mkdir(path7.join(root, ".sindri"), { recursive: true });
36486
- await fs7.writeFile(path7.join(root, ".sindri", "last-published.json"), JSON.stringify(p, null, 2) + "\n");
36828
+ await fs8.mkdir(path8.join(root, ".sindri"), { recursive: true });
36829
+ await fs8.writeFile(path8.join(root, ".sindri", "last-published.json"), JSON.stringify(p, null, 2) + "\n");
36487
36830
  }
36488
36831
 
36489
36832
  // src/tools.ts
@@ -36522,7 +36865,22 @@ var SCHEMAS = {
36522
36865
  list_apps: {},
36523
36866
  versions: { app: appName },
36524
36867
  rollback: { app: appName, version: external_exports.string().min(1).describe("Version id from `versions`.") },
36525
- share: { app: appName }
36868
+ update_template: { app: appName },
36869
+ rename_app: {
36870
+ app: appName,
36871
+ new_name: appName.optional().describe("New address: lowercase letters, digits and hyphens (2 to 31 chars). Becomes https://<new_name>.sindri.kigo.dev; the old address stops working."),
36872
+ display_name: external_exports.string().min(2).max(60).optional().describe("New visible name (the title people see), in Spanish, as the PM wrote it. The address does not change.")
36873
+ },
36874
+ delete_app: {
36875
+ app: appName,
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.")
36877
+ },
36878
+ share: { app: appName },
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
+ }
36526
36884
  };
36527
36885
  var DESCRIPTIONS = {
36528
36886
  setup: {
@@ -36555,7 +36913,27 @@ var DESCRIPTIONS = {
36555
36913
  title: "Go back to a version",
36556
36914
  description: "Publishes a previous version and brings the local folder to it. Nothing is deleted: the newer versions stay in the history."
36557
36915
  },
36558
- share: { title: "Share", description: "The line to paste in Slack: address, test-data notice and how the team enters." }
36916
+ update_template: {
36917
+ title: "Update the template",
36918
+ description: "Brings the app up to the current Kigo template: files the app never customised are replaced, files you changed are left alone and listed. Publishes a version \xABPlantilla actualizada\xBB and brings the local folder to it. Use it when the PM asks to update the template or Sindri announces a template fix. Refuses while there are unpublished local edits: publish (or discard) them first."
36919
+ },
36920
+ rename_app: {
36921
+ title: "Rename an app",
36922
+ description: 'Two different things, pass at least one. `new_name` changes the address (https://<new_name>.sindri.kigo.dev): Sindri renames it, the local folder is moved to match and the old address stops working; refuses while the folder has unpublished edits (publish first). `display_name` changes only the visible name: it is written to kigo-app.yaml and published as \xABAhora se llama \xAB<display_name>\xBB\xBB. When the PM says "c\xE1mbiale el nombre" and it is unclear which one they mean, ask: \xBFla direcci\xF3n o el nombre que se ve? Both together are allowed (visible name first, then the address).'
36923
+ },
36924
+ delete_app: {
36925
+ title: "Delete an app",
36926
+ description: "Removes the app from Sindri (address, published versions and its database) after the PM typed its name: `confirm` must equal `app`, ask them to write it, never guess it. Stops the preview and moves the local folder to ~/Sindri/.papelera/<app>-<date> instead of deleting it, so the PM can still recover the files by hand."
36927
+ },
36928
+ share: { title: "Share", description: "The line to paste in Slack: address, test-data notice and how the team enters." },
36929
+ help: {
36930
+ title: "What Sindri is",
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."
36936
+ }
36559
36937
  };
36560
36938
  function createTools(deps) {
36561
36939
  const { cfg } = deps;
@@ -36594,9 +36972,11 @@ function createTools(deps) {
36594
36972
  const login = guard(async () => {
36595
36973
  try {
36596
36974
  const creds2 = await accessToken(cfg, fetchImpl);
36975
+ await api.me();
36597
36976
  return { ok: true, summary: `Ya est\xE1s dentro como ${creds2.email ?? "tu cuenta de Kigo"}.`, email: creds2.email ?? null, already: true };
36598
36977
  } catch (err) {
36599
36978
  if (!(err instanceof NeedsLogin)) throw err;
36979
+ await clearCredentials(cfg).catch(() => void 0);
36600
36980
  }
36601
36981
  const openUrl = deps.openUrl ?? defaultOpenUrl(cfg);
36602
36982
  let creds;
@@ -36647,15 +37027,15 @@ function createTools(deps) {
36647
37027
  }
36648
37028
  const created = await api.createApp(a.name, a.description.trim());
36649
37029
  const archive = await api.download(created.archive_url);
36650
- await fs8.mkdir(p.root, { recursive: true });
37030
+ await fs9.mkdir(p.root, { recursive: true });
36651
37031
  await replaceTree(p.root, archive);
36652
- await fs8.rm(path8.dirname(archive), { recursive: true, force: true });
37032
+ await fs9.rm(path9.dirname(archive), { recursive: true, force: true });
36653
37033
  p = await appPaths(cfg, a.name, me.owner);
36654
37034
  if (!await appPresent(p)) {
36655
37035
  return { ok: false, summary: `Cre\xE9 "${a.name}" en Sindri pero lo que baj\xF3 no trae la app donde la espero (apps/${p.owner || me.owner}/${a.name}). Escribe en Slack, canal #sindri.`, path: p.root, url: created.url };
36656
37036
  }
36657
- await fs8.writeFile(path8.join(p.appDir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
36658
- const install = await run(tc.pnpm, ["install", "--no-frozen-lockfile"], { cwd: p.root, env: { ...tc.env, CI: "1", FORCE_COLOR: "0" }, timeoutMs: 6e5 });
37037
+ await fs9.writeFile(path9.join(p.appDir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
37038
+ const install = await run(tc.pnpm, [...PNPM_ARGS, "install", "--no-frozen-lockfile"], { cwd: p.root, env: { ...tc.env, CI: "1", FORCE_COLOR: "0" }, timeoutMs: 6e5 });
36659
37039
  if (install.code !== 0) {
36660
37040
  const why = lastLine(install.stderr || install.stdout);
36661
37041
  return { ok: false, summary: `Cre\xE9 "${a.name}" en Sindri pero no pude preparar la carpeta en tu computadora (${why}). Vuelve a intentarlo con create_app o p\xEDdeme que lo revise.`, path: p.appDir, url: created.url, detail: why };
@@ -36666,6 +37046,7 @@ function createTools(deps) {
36666
37046
  }
36667
37047
  const app = await api.app(a.name).catch(() => null);
36668
37048
  await writePublished(p.appDir, { version: app?.last_version ?? null, tree_hash: await treeHash(p.appDir), url: created.url });
37049
+ await linkDesignGuide(p);
36669
37050
  return {
36670
37051
  ok: true,
36671
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.`,
@@ -36683,8 +37064,8 @@ function createTools(deps) {
36683
37064
  }
36684
37065
  const tc = await toolchain();
36685
37066
  if (!tc) return NEEDS_SETUP;
36686
- if (!await exists2(path8.join(p.root, "node_modules"))) {
36687
- const install = await run(tc.pnpm, ["install", "--no-frozen-lockfile"], { cwd: p.root, env: { ...tc.env, CI: "1", FORCE_COLOR: "0" }, timeoutMs: 6e5 });
37067
+ if (!await exists2(path9.join(p.root, "node_modules"))) {
37068
+ const install = await run(tc.pnpm, [...PNPM_ARGS, "install", "--no-frozen-lockfile"], { cwd: p.root, env: { ...tc.env, CI: "1", FORCE_COLOR: "0" }, timeoutMs: 6e5 });
36688
37069
  if (install.code !== 0) return { ok: false, summary: `No pude preparar la app para correrla en tu computadora (${lastLine(install.stderr || install.stdout)}). Vuelve a intentarlo.`, detail: lastLine(install.stderr || install.stdout) };
36689
37070
  }
36690
37071
  if (!await runningPreview(cfg, a.app, fetchImpl)) {
@@ -36712,7 +37093,7 @@ function createTools(deps) {
36712
37093
  const tc = await toolchain();
36713
37094
  if (!tc) return NEEDS_SETUP;
36714
37095
  const message = a.message.trim().slice(0, MESSAGE_MAX);
36715
- const tsc = await run(tc.pnpm, ["exec", "tsc", "--noEmit", "--pretty", "false"], { cwd: dir, env: { ...tc.env, CI: "1", FORCE_COLOR: "0" }, timeoutMs: 18e4 });
37096
+ const tsc = await run(tc.pnpm, [...PNPM_ARGS, "exec", "tsc", "--noEmit", "--pretty", "false"], { cwd: dir, env: { ...tc.env, CI: "1", FORCE_COLOR: "0" }, timeoutMs: 18e4 });
36716
37097
  if (tsc.code !== 0) {
36717
37098
  const first = (tsc.stdout + tsc.stderr).split("\n").find((l) => /error TS\d+/.test(l))?.trim().slice(0, 200);
36718
37099
  return { ok: false, summary: "No la publiqu\xE9: hay un error en el c\xF3digo que impedir\xEDa que abra. Corr\xEDgelo y vuelve a publicar.", detail: first ?? lastLine(tsc.stderr || tsc.stdout) };
@@ -36724,14 +37105,14 @@ function createTools(deps) {
36724
37105
  }
36725
37106
  const packed = await packTree(dir);
36726
37107
  if (packed.bytes > 20 * 1024 * 1024) {
36727
- await fs8.rm(path8.dirname(packed.file), { recursive: true, force: true });
37108
+ await fs9.rm(path9.dirname(packed.file), { recursive: true, force: true });
36728
37109
  return { ok: false, summary: "La app pesa m\xE1s de 20 MB sin contar sus piezas instaladas. Quita archivos grandes (im\xE1genes, videos) antes de publicar.", bytes: packed.bytes };
36729
37110
  }
36730
37111
  let result;
36731
37112
  try {
36732
37113
  result = await api.publish(a.app, packed.file, message);
36733
37114
  } finally {
36734
- await fs8.rm(path8.dirname(packed.file), { recursive: true, force: true });
37115
+ await fs9.rm(path9.dirname(packed.file), { recursive: true, force: true });
36735
37116
  }
36736
37117
  await writePublished(dir, { version: result.version, tree_hash: await treeHash(dir), url: result.url });
36737
37118
  return {
@@ -36754,11 +37135,11 @@ function createTools(deps) {
36754
37135
  });
36755
37136
  const list_apps = guard(async () => {
36756
37137
  const apps = await api.listApps();
36757
- if (apps.length === 0) return { ok: true, summary: "Todav\xEDa no tienes apps. Cuando quieras, crea la primera.", apps: [] };
37138
+ if (apps.length === 0) return { ok: true, summary: "Todav\xEDa no tienes apps. Cuando quieras, crea la primera.", apps: [], space_url: SPACE_URL };
36758
37139
  const rows = [];
36759
37140
  for (const app of apps) rows.push({ ...app, local: await localState(cfg, app.name) });
36760
37141
  const names = rows.map((r) => `${r.name} (${r.url})`).join(", ");
36761
- return { ok: true, summary: `Tienes ${rows.length === 1 ? "una app" : `${rows.length} apps`}: ${names}.`, apps: rows };
37142
+ return { ok: true, summary: `Tienes ${rows.length === 1 ? "una app" : `${rows.length} apps`}: ${names}.`, apps: rows, space_url: SPACE_URL };
36762
37143
  });
36763
37144
  const versions = guard(async (a) => {
36764
37145
  if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
@@ -36773,15 +37154,16 @@ function createTools(deps) {
36773
37154
  let p = await appPaths(cfg, a.app);
36774
37155
  await stopPreview(cfg, a.app);
36775
37156
  const archive = await api.download(`/v1/apps/${encodeURIComponent(a.app)}/archive`, result.version.id);
36776
- await fs8.mkdir(p.root, { recursive: true });
37157
+ await fs9.mkdir(p.root, { recursive: true });
36777
37158
  await replaceTree(p.root, archive);
36778
- await fs8.rm(path8.dirname(archive), { recursive: true, force: true });
37159
+ await fs9.rm(path9.dirname(archive), { recursive: true, force: true });
36779
37160
  p = await appPaths(cfg, a.app, p.owner || void 0);
36780
37161
  const dir = p.appDir;
36781
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 };
36782
- if (!await exists2(path8.join(dir, ".env.local"))) await fs8.writeFile(path8.join(dir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
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);
36783
37165
  const tc = await toolchain();
36784
- if (tc && await exists2(path8.join(p.root, "node_modules"))) {
37166
+ if (tc && await exists2(path9.join(p.root, "node_modules"))) {
36785
37167
  const db = await prepareLocalDb(tc, dir);
36786
37168
  if (!db.ok) process.stderr.write(`sindri: local db after rollback: ${db.detail}
36787
37169
  `);
@@ -36794,24 +37176,193 @@ function createTools(deps) {
36794
37176
  url: result.url
36795
37177
  };
36796
37178
  });
37179
+ const update_template = guard(async (a) => {
37180
+ if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
37181
+ const before = await localState(cfg, a.app);
37182
+ if (before.exists && before.unpublished) {
37183
+ return { ok: false, summary: "Publica o descarta tus cambios antes de actualizar la plantilla.", code: "unpublished_changes", local: before };
37184
+ }
37185
+ const result = await api.updateTemplate(a.app);
37186
+ const files = describeTemplateFiles(result);
37187
+ const data = { updated: result.updated, skipped: result.skipped, version: result.version, url: result.url, published: result.published };
37188
+ if (!result.published) {
37189
+ return { ok: true, summary: `"${a.app}" ya est\xE1 al d\xEDa con la plantilla; no cambi\xE9 nada.${files}`, ...data };
37190
+ }
37191
+ let p = await appPaths(cfg, a.app);
37192
+ if (before.exists) {
37193
+ await stopPreview(cfg, a.app);
37194
+ const archive = await api.download(`/v1/apps/${encodeURIComponent(a.app)}/archive`, result.version?.id);
37195
+ await replaceTree(p.root, archive);
37196
+ await fs9.rm(path9.dirname(archive), { recursive: true, force: true });
37197
+ p = await appPaths(cfg, a.app, p.owner || void 0);
37198
+ const dir = p.appDir;
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 };
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);
37202
+ const tc = await toolchain();
37203
+ if (tc && await exists2(path9.join(p.root, "node_modules"))) {
37204
+ const db = await prepareLocalDb(tc, dir);
37205
+ if (!db.ok) process.stderr.write(`sindri: local db after update_template: ${db.detail}
37206
+ `);
37207
+ }
37208
+ await writePublished(dir, { version: result.version, tree_hash: await treeHash(dir), url: result.url });
37209
+ }
37210
+ const where = before.exists ? `en ${result.url} y en tu computadora` : `en ${result.url}`;
37211
+ return { ok: true, summary: `Listo, actualic\xE9 la plantilla de "${a.app}" ${where}.${files}`, ...data };
37212
+ });
36797
37213
  const share = guard(async (a) => {
36798
37214
  if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
36799
37215
  const r = await api.share(a.app);
36800
37216
  return { ok: true, summary: `Para Slack: ${r.text}`, text: r.text };
36801
37217
  });
37218
+ const rename_app = guard(async (a) => {
37219
+ if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
37220
+ const newName = a.new_name?.trim().toLowerCase();
37221
+ const displayName = a.display_name?.trim();
37222
+ if (!newName && !displayName) return { ok: false, summary: "Dime qu\xE9 quieres cambiar: la direcci\xF3n o el nombre que se ve.", code: "nothing_to_change" };
37223
+ if (newName !== void 0 && !validAppName(newName)) {
37224
+ return { ok: false, summary: "La direcci\xF3n nueva debe ir en min\xFAsculas, con n\xFAmeros o guiones, de 2 a 31 letras (por ejemplo: visitas-plaza)." };
37225
+ }
37226
+ if (newName === a.app) return { ok: false, summary: `"${a.app}" ya est\xE1 en esa direcci\xF3n.`, code: "same_name" };
37227
+ const parts = [];
37228
+ const data = {};
37229
+ if (displayName) {
37230
+ const p = await appPaths(cfg, a.app);
37231
+ if (!await appPresent(p)) {
37232
+ return { ok: false, summary: `No encuentro la carpeta de "${a.app}" en tu computadora y el nombre que se ve se publica desde aqu\xED. Usa rollback a su \xFAltima versi\xF3n para bajarla y vuelve a intentarlo.`, path: p.appDir };
37233
+ }
37234
+ await writeManifestValue(p.appDir, "display_name", displayName);
37235
+ const pub = await publish({ app: a.app, message: `Ahora se llama \xAB${displayName}\xBB` });
37236
+ if (!pub.ok) return { ...pub, summary: `El nombre \xAB${displayName}\xBB qued\xF3 en tu computadora pero a\xFAn no est\xE1 publicado. ${pub.summary}` };
37237
+ parts.push(`Listo, ahora se llama \xAB${displayName}\xBB y ya se ve as\xED en ${pub.url}.`);
37238
+ Object.assign(data, { display_name: displayName, version: pub.version, url: pub.url });
37239
+ }
37240
+ if (newName) {
37241
+ const before = await localState(cfg, a.app);
37242
+ if (before.exists && before.unpublished) {
37243
+ return { ok: false, summary: `Publica tus cambios antes de cambiar la direcci\xF3n de "${a.app}".`, code: "unpublished_changes", local: before };
37244
+ }
37245
+ if (await exists2(appRoot(cfg, newName))) {
37246
+ return { ok: false, summary: `Ya tienes una carpeta llamada "${newName}" en Sindri; elige otra direcci\xF3n.`, code: "folder_exists" };
37247
+ }
37248
+ const result = await api.rename(a.app, newName);
37249
+ const oldUrl = `https://${a.app}.${cfg.appsDomain}/`;
37250
+ if (before.exists) {
37251
+ const p = await appPaths(cfg, a.app);
37252
+ await stopPreview(cfg, a.app);
37253
+ const newRoot = appRoot(cfg, newName);
37254
+ await moveDir(p.root, newRoot);
37255
+ const newDir = path9.join(newRoot, "apps", p.owner, newName);
37256
+ await moveDir(path9.join(newRoot, "apps", p.owner, a.app), newDir);
37257
+ await writeManifestValue(newDir, "name", newName);
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 });
37260
+ data.path = newDir;
37261
+ }
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.`);
37263
+ Object.assign(data, { name: result.name, old_name: a.app, url: result.url, version: result.version });
37264
+ }
37265
+ return { ok: true, summary: parts.join(" "), ...data };
37266
+ });
37267
+ const delete_app = guard(async (a) => {
37268
+ if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
37269
+ if (a.confirm.trim() !== a.app) {
37270
+ return { ok: false, summary: `Para borrar "${a.app}" necesito que escribas su nombre tal cual. No la toqu\xE9.`, code: "confirm_mismatch" };
37271
+ }
37272
+ const root = appRoot(cfg, a.app);
37273
+ const present = await exists2(root);
37274
+ await stopPreview(cfg, a.app);
37275
+ let inSindri = true;
37276
+ try {
37277
+ await api.deleteApp(a.app, a.app);
37278
+ } catch (err) {
37279
+ if (err instanceof ApiError && err.status === 404 && present) inSindri = false;
37280
+ else throw err;
37281
+ }
37282
+ let trash = null;
37283
+ if (present) {
37284
+ trash = path9.join(cfg.home, ".papelera", `${a.app}-${stamp()}`);
37285
+ await moveDir(root, trash);
37286
+ }
37287
+ const kept = "Una copia de sus archivos queda en tu computadora, en la papelera de Sindri; puedes borrarla a mano cuando quieras.";
37288
+ let summary;
37289
+ if (!inSindri) summary = `"${a.app}" ya no exist\xEDa en Sindri. ${kept}`;
37290
+ else if (trash) summary = `Borr\xE9 "${a.app}" de Sindri: su direcci\xF3n y sus versiones ya no existen. ${kept}`;
37291
+ else summary = `Borr\xE9 "${a.app}" de Sindri: su direcci\xF3n y sus versiones ya no existen. En esta computadora no ten\xEDa carpeta.`;
37292
+ return { ok: true, summary, name: a.app, removed_from_sindri: inSindri, trash_path: trash };
37293
+ });
37294
+ const help = guard(async () => {
37295
+ const logged_in = await loadCredentials(cfg) !== null;
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.`;
37297
+ return { ok: true, summary, space_url: SPACE_URL, logged_in, apps_dir: cfg.home };
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
+ });
36802
37327
  const logout = guard(async () => {
36803
37328
  await clearCredentials(cfg);
36804
37329
  return { ok: true, summary: "Saliste de tu cuenta de Kigo en esta computadora." };
36805
37330
  });
36806
- return { setup, login, create_app, preview, publish, status, list_apps, versions, rollback, share, 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 };
37332
+ }
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.";
37334
+ async function moveDir(from, to) {
37335
+ await fs9.mkdir(path9.dirname(to), { recursive: true });
37336
+ try {
37337
+ await fs9.rename(from, to);
37338
+ } catch (err) {
37339
+ if (err.code !== "EXDEV") throw err;
37340
+ await fs9.cp(from, to, { recursive: true });
37341
+ await fs9.rm(from, { recursive: true, force: true });
37342
+ }
37343
+ }
37344
+ function stamp(d = /* @__PURE__ */ new Date()) {
37345
+ const parts = new Intl.DateTimeFormat("en-CA", { timeZone: "America/Mexico_City", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).formatToParts(d);
37346
+ const g = (t) => parts.find((p) => p.type === t)?.value ?? "";
37347
+ return `${g("year")}${g("month")}${g("day")}-${g("hour")}${g("minute")}`;
37348
+ }
37349
+ function describeTemplateFiles(r) {
37350
+ const parts = [];
37351
+ const n = r.updated.length;
37352
+ if (n > 0) parts.push(n === 1 ? "Cambi\xF3 un archivo de la plantilla." : `Cambiaron ${n} archivos de la plantilla.`);
37353
+ if (r.skipped.length > 0) {
37354
+ const list = r.skipped.length <= 5 ? r.skipped.join(", ") : `${r.skipped.slice(0, 5).join(", ")} y ${r.skipped.length - 5} m\xE1s`;
37355
+ parts.push(`Dej\xE9 como estaban ${r.skipped.length === 1 ? "uno que" : `${r.skipped.length} que`} Claude ya hab\xEDa cambiado (${list}); rev\xEDsalos si quieres el cambio ah\xED tambi\xE9n.`);
37356
+ }
37357
+ return parts.length ? ` ${parts.join(" ")}` : "";
36807
37358
  }
36808
37359
  async function prepareLocalDb(tc, dir, opts = {}) {
36809
37360
  const env = { ...tc.env, KIGO_ENV: "local", DATABASE_URL: "", CI: "1", FORCE_COLOR: "0" };
36810
- const fresh = !await exists2(path8.join(dir, ".sindri", "pglite"));
36811
- const migrate = await run(tc.pnpm, ["db:migrate"], { cwd: dir, env, timeoutMs: 18e4 });
37361
+ const fresh = !await exists2(path9.join(dir, ".sindri", "pglite"));
37362
+ const migrate = await run(tc.pnpm, [...PNPM_ARGS, "db:migrate"], { cwd: dir, env, timeoutMs: 18e4 });
36812
37363
  if (migrate.code !== 0) return { ok: false, detail: lastLine(migrate.stderr || migrate.stdout) };
36813
37364
  if (opts.seed ?? fresh) {
36814
- const seed = await run(tc.pnpm, ["db:seed"], { cwd: dir, env, timeoutMs: 18e4 });
37365
+ const seed = await run(tc.pnpm, [...PNPM_ARGS, "db:seed"], { cwd: dir, env, timeoutMs: 18e4 });
36815
37366
  if (seed.code !== 0) return { ok: false, detail: lastLine(seed.stderr || seed.stdout) };
36816
37367
  }
36817
37368
  return { ok: true };
@@ -36839,7 +37390,7 @@ function when(iso, now = /* @__PURE__ */ new Date()) {
36839
37390
  }
36840
37391
 
36841
37392
  // src/index.ts
36842
- var VERSION = "0.1.0";
37393
+ var VERSION = "0.1.4";
36843
37394
  function registerTools(server, tools) {
36844
37395
  for (const name of Object.keys(SCHEMAS)) {
36845
37396
  const meta3 = DESCRIPTIONS[name];
@@ -36854,9 +37405,30 @@ function registerTools(server, tools) {
36854
37405
  });
36855
37406
  }
36856
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
+ }
36857
37428
  function createServer(tools) {
36858
37429
  const server = new McpServer({ name: "sindri", version: VERSION }, { instructions: INSTRUCTIONS });
36859
37430
  registerTools(server, tools);
37431
+ registerResources(server, tools);
36860
37432
  return server;
36861
37433
  }
36862
37434
  async function callTool(tools, name, rawArgs) {
@@ -36895,5 +37467,6 @@ export {
36895
37467
  VERSION,
36896
37468
  callTool,
36897
37469
  createServer,
37470
+ registerResources,
36898
37471
  registerTools
36899
37472
  };