@kigo-dev/sindri 0.1.3 → 0.1.5

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(path10) {
3266
- let input2 = path10;
3265
+ function removeDotSegments(path11) {
3266
+ let input2 = path11;
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 path10 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
3676
- wsComponent.path = path10 && path10 !== "/" ? path10 : void 0;
3675
+ const path11 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
3676
+ wsComponent.path = path11 && path11 !== "/" ? path11 : void 0;
3677
3677
  wsComponent.query = queryIndex === -1 ? void 0 : wsComponent.resourceName.slice(queryIndex + 1);
3678
3678
  wsComponent.resourceName = void 0;
3679
3679
  }
@@ -3732,7 +3732,7 @@ var require_schemes = __commonJS({
3732
3732
  urnComponent.nss = (uuidComponent.uuid || "").toLowerCase();
3733
3733
  return urnComponent;
3734
3734
  }
3735
- var http2 = (
3735
+ var http3 = (
3736
3736
  /** @type {SchemeHandler} */
3737
3737
  {
3738
3738
  scheme: "http",
@@ -3745,7 +3745,7 @@ var require_schemes = __commonJS({
3745
3745
  /** @type {SchemeHandler} */
3746
3746
  {
3747
3747
  scheme: "https",
3748
- domainHost: http2.domainHost,
3748
+ domainHost: http3.domainHost,
3749
3749
  parse: httpParse,
3750
3750
  serialize: httpSerialize
3751
3751
  }
@@ -3789,7 +3789,7 @@ var require_schemes = __commonJS({
3789
3789
  var SCHEMES = (
3790
3790
  /** @type {Record<SchemeName, SchemeHandler>} */
3791
3791
  {
3792
- http: http2,
3792
+ http: http3,
3793
3793
  https,
3794
3794
  ws,
3795
3795
  wss,
@@ -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, fs10, exportName) {
7188
+ function addFormats(ajv, list, fs11, 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, fs10[f]);
7193
+ ajv.addFormat(f, fs11[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: path10, errorMaps, issueData } = params;
7576
- const fullPath = [...path10, ...issueData.path || []];
7575
+ const { data, path: path11, errorMaps, issueData } = params;
7576
+ const fullPath = [...path11, ...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, path10, key) {
7691
+ constructor(parent, value, path11, key) {
7692
7692
  this._cachedPath = [];
7693
7693
  this.parent = parent;
7694
7694
  this.data = value;
7695
- this._path = path10;
7695
+ this._path = path11;
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, path10) {
11584
- if (!path10)
11583
+ function getElementAtPath(obj, path11) {
11584
+ if (!path11)
11585
11585
  return obj;
11586
- return path10.reduce((acc, key) => acc?.[key], obj);
11586
+ return path11.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(path10, issues) {
11998
+ function prefixIssues(path11, issues) {
11999
11999
  return issues.map((iss) => {
12000
12000
  var _a3;
12001
12001
  (_a3 = iss).path ?? (_a3.path = []);
12002
- iss.path.unshift(path10);
12002
+ iss.path.unshift(path11);
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, path10 = []) => {
12435
+ const processError = (error62, path11 = []) => {
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 }, [...path10, ...issue2.path]));
12438
+ issue2.errors.map((issues) => processError({ issues }, [...path11, ...issue2.path]));
12439
12439
  } else if (issue2.code === "invalid_key") {
12440
- processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
12440
+ processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
12441
12441
  } else if (issue2.code === "invalid_element") {
12442
- processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
12442
+ processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
12443
12443
  } else {
12444
- const fullpath = [...path10, ...issue2.path];
12444
+ const fullpath = [...path11, ...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, path10 = []) => {
12483
+ const processError = (error62, path11 = []) => {
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 }, [...path10, ...issue2.path]));
12487
+ issue2.errors.map((issues) => processError({ issues }, [...path11, ...issue2.path]));
12488
12488
  } else if (issue2.code === "invalid_key") {
12489
- processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
12489
+ processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
12490
12490
  } else if (issue2.code === "invalid_element") {
12491
- processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
12491
+ processError({ issues: issue2.issues }, [...path11, ...issue2.path]);
12492
12492
  } else {
12493
- const fullpath = [...path10, ...issue2.path];
12493
+ const fullpath = [...path11, ...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 path10 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
12533
- for (const seg of path10) {
12532
+ const path11 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
12533
+ for (const seg of path11) {
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(path10) {
27515
- if (path10.length === 0) {
27514
+ function getDotPath(path11) {
27515
+ if (path11.length === 0) {
27516
27516
  return "object root";
27517
27517
  }
27518
- return path10.reduce((acc, seg, index) => {
27518
+ return path11.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 path10 = ref.slice(1).split("/").filter(Boolean);
29682
- if (path10.length === 0) {
29681
+ const path11 = ref.slice(1).split("/").filter(Boolean);
29682
+ if (path11.length === 0) {
29683
29683
  return ctx.rootSchema;
29684
29684
  }
29685
29685
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
29686
- if (path10[0] === defsKey) {
29687
- const key = path10[1] === void 0 ? void 0 : decodeJSONPointerSegment(path10[1]);
29686
+ if (path11[0] === defsKey) {
29687
+ const key = path11[1] === void 0 ? void 0 : decodeJSONPointerSegment(path11[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: {}
@@ -35716,27 +35962,33 @@ function appRoot(cfg, name) {
35716
35962
  }
35717
35963
 
35718
35964
  // src/instructions.ts
35719
- var INSTRUCTIONS = `You are helping a Kigo product manager (PM) build small internal apps with Sindri. The PM is not technical: they never see code, git, terminals or ports. You write the code in the app folder on their computer; the sindri tools install, preview, publish and keep versions.
35965
+ var INSTRUCTIONS = `You are helping a person at Kigo build small internal apps with Sindri. Call them the builder: anyone in the company (product, operations, CX, finance, a developer). Assume they are not technical unless they show otherwise: they never see code, git, terminals or ports. You write the code in the app folder on their computer; the sindri tools install, preview, publish and keep versions.
35720
35966
 
35721
- 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.
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 builder: repeat it as is or paraphrase it.
35722
35968
 
35723
- 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).
35969
+ First contact: the first time the builder 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 builder asks where to see their apps or their versions, give them the Mi espacio link (\`space_url\`, https://sindri.kigo.dev).
35724
35970
 
35725
35971
  Flow:
35726
- 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.
35727
- 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.
35728
- 3. Show before shipping: run \`preview\` and tell the PM to look at the local address; ask what to change. Edits reload live.
35729
- 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.
35730
- 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.
35731
- 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.
35732
- 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
- 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.
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 builder 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.
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 builder 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.
35974
+ 3. Show before shipping: run \`preview\` and tell the builder to look at the local address; ask what to change. Edits reload live.
35975
+ 4. Ship: \`publish\` with a message in the builder'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 builder typed into the local preview never travel. Keep test data in the seed, not in the UI.
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 builder wants "como estaba ayer"; \`share\` for the Slack line.
35977
+ 6. Template updates: when the builder 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 builder 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 builder 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 builder 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 builder 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
+ Secrets: when the app needs a credential (an API key, a token, a password for another service), never ask the builder to paste it in the chat and never write it in code, seeds or comments. Call \`set_secret\` with the variable name (UPPER_SNAKE_CASE, e.g. LINEAR_API_KEY) and a one-line \`hint\` in Spanish saying what to paste; tell the builder "se abri\xF3 una p\xE1gina en tu navegador para pegarlo" and wait: the tool returns when they saved it (or after five minutes). The value is stored in the app's .env.local on their computer and in Sindri; the running preview restarts so the code sees it. In code read it as \`process.env.NAME\` (server side only: route handlers, server components, server actions) and handle the missing case with a clear Spanish message. \`list_secrets\` tells you which names exist on each side (never values); \`delete_secret\` removes one after the builder agrees. Never read .env.local (or any .env* file) and never repeat its contents to the builder, not even partially.
35734
35982
 
35735
- 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.`;
35983
+ Tasks: when something must happen on its own on a schedule (sync data from another service, send a summary, recompute a table), declare it in kigo-app.yaml with ONE line \`schedules: [{"name":"sync-linear","path":"/api/tareas/sync-linear","every":"30m"}]\` (a JSON list: \`name\` lowercase with hyphens and unique, \`path\` under /api/ without query, \`every\` one of 5m, 10m, 15m, 30m, 1h, 2h, 6h, 12h, 1d; at most 5 tasks). The route at that path must be built with the template's \`taskHandler(name, fn)\` from src/lib/tasks.ts (see the app's CLAUDE.md and the design guide topic tasks-and-secrets): it only accepts the platform's signed calls, so never call it from the UI. Test it with \`run_task\` locally (preview running) before publishing, then \`run_task\` with \`where: "sindri"\` once published; \`status\` shows the last run of each task. Remind the builder that in Sindri tasks run only in business hours (Monday to Friday, 9:05 am to midnight, Mexico City), never on weekends or at night.
35984
+
35985
+ 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 builder. If it answers \`no_design_guide\`, run \`update_template\` once and call \`design_guide\` again.
35986
+
35987
+ Rules: synthetic data only (never paste real emails, plates or phone numbers into seeds); one author per app (the builder); never touch files outside the app folder; never read .env.local back to the builder; 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 builder.`;
35736
35988
 
35737
35989
  // src/tools.ts
35738
- import fs9 from "node:fs/promises";
35739
- import path9 from "node:path";
35990
+ import fs10 from "node:fs/promises";
35991
+ import path10 from "node:path";
35740
35992
 
35741
35993
  // src/api.ts
35742
35994
  import fs3 from "node:fs/promises";
@@ -36119,6 +36371,20 @@ var ApiClient = class {
36119
36371
  share(name) {
36120
36372
  return this.json("GET", `/v1/apps/${enc(name)}/share`);
36121
36373
  }
36374
+ listSecrets(name) {
36375
+ return this.json("GET", `/v1/apps/${enc(name)}/secrets`).then((r) => r.names ?? []);
36376
+ }
36377
+ /** Stores one secret in the app's workspace; `restarted` says whether the app was restarted to pick it up. */
36378
+ putSecret(name, key, value) {
36379
+ return this.json("PUT", `/v1/apps/${enc(name)}/secrets/${enc(key)}`, { value }, 12e4);
36380
+ }
36381
+ deleteSecret(name, key) {
36382
+ return this.json("DELETE", `/v1/apps/${enc(name)}/secrets/${enc(key)}`, void 0, 12e4);
36383
+ }
36384
+ /** Runs a declared task now in the workspace and waits for it (the router caps the run). */
36385
+ runTask(name, task) {
36386
+ return this.json("POST", `/v1/apps/${enc(name)}/tasks/${enc(task)}/run`, {}, 32e4);
36387
+ }
36122
36388
  /** Downloads an archive (`archive_url` from create, or /archive of a version) to a temp file. */
36123
36389
  async download(urlOrPath, version2) {
36124
36390
  const u = new URL(urlOrPath.startsWith("http") ? urlOrPath : `${this.cfg.apiUrl}${urlOrPath}`);
@@ -36175,22 +36441,126 @@ function enc(name) {
36175
36441
  return encodeURIComponent(name);
36176
36442
  }
36177
36443
 
36178
- // src/layout.ts
36444
+ // src/envfile.ts
36445
+ import { randomBytes as randomBytes2 } from "node:crypto";
36179
36446
  import fs4 from "node:fs/promises";
36180
36447
  import path4 from "node:path";
36448
+ var ENV_FILE = ".env.local";
36449
+ var PLATFORM_KEYS = ["KIGO_ENV", "KIGO_APP_NAME", "DATABASE_URL", "PGLITE_DATA_DIR", "SINDRI_TASK_TOKEN"];
36450
+ var SECRET_NAME_RE = /^[A-Z][A-Z0-9_]{1,63}$/;
36451
+ function validSecretName(name) {
36452
+ return typeof name === "string" && SECRET_NAME_RE.test(name);
36453
+ }
36454
+ function reservedKey(name) {
36455
+ return PLATFORM_KEYS.includes(name);
36456
+ }
36457
+ var LINE_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
36458
+ function envKeys(text) {
36459
+ const out = [];
36460
+ for (const line of text.split("\n")) {
36461
+ const m = LINE_RE.exec(line);
36462
+ if (m?.[1] && !out.includes(m[1])) out.push(m[1]);
36463
+ }
36464
+ return out;
36465
+ }
36466
+ function envValue(text, key) {
36467
+ for (const line of text.split("\n")) {
36468
+ const m = LINE_RE.exec(line);
36469
+ if (!m || m[1] !== key) continue;
36470
+ const raw = (m[2] ?? "").trim();
36471
+ const q = raw[0];
36472
+ if ((q === '"' || q === "'" || q === "`") && raw.length >= 2) {
36473
+ const end = raw.indexOf(q, 1);
36474
+ const inner = end > 0 ? raw.slice(1, end) : raw.slice(1);
36475
+ return q === '"' ? inner.replace(/\\n/g, "\n") : inner;
36476
+ }
36477
+ return raw.replace(/\s+#.*$/, "").trim();
36478
+ }
36479
+ return void 0;
36480
+ }
36481
+ function envLine(key, value) {
36482
+ return `${key}=${quoteEnvValue(value)}`;
36483
+ }
36484
+ function quoteEnvValue(value) {
36485
+ if (value !== "" && /^[A-Za-z0-9_.\/:@+=,\-]+$/.test(value)) return value;
36486
+ const order = value.includes("$") ? ["'", '"', "`"] : ['"', "'", "`"];
36487
+ for (const q of order) {
36488
+ if (value.includes(q)) continue;
36489
+ if (q === '"') return `"${value.replace(/\r?\n/g, "\\n")}"`;
36490
+ if (value.includes("\n")) continue;
36491
+ return `${q}${value}${q}`;
36492
+ }
36493
+ throw new Error("value cannot be quoted for .env.local");
36494
+ }
36495
+ function upsertEnv(text, key, value) {
36496
+ const line = envLine(key, value);
36497
+ const lines = text.split("\n");
36498
+ const hadTrailing = lines.at(-1) === "";
36499
+ if (hadTrailing) lines.pop();
36500
+ let done = false;
36501
+ const out = [];
36502
+ for (const l of lines) {
36503
+ if (LINE_RE.exec(l)?.[1] === key) {
36504
+ if (!done) out.push(line);
36505
+ done = true;
36506
+ continue;
36507
+ }
36508
+ out.push(l);
36509
+ }
36510
+ if (!done) out.push(line);
36511
+ return `${out.join("\n")}
36512
+ `;
36513
+ }
36514
+ function removeEnv(text, key) {
36515
+ const lines = text.split("\n");
36516
+ const hadTrailing = lines.at(-1) === "";
36517
+ if (hadTrailing) lines.pop();
36518
+ const out = lines.filter((l) => LINE_RE.exec(l)?.[1] !== key);
36519
+ return out.length === 0 ? "" : `${out.join("\n")}
36520
+ `;
36521
+ }
36522
+ function newTaskToken() {
36523
+ return randomBytes2(32).toString("hex");
36524
+ }
36525
+ function envFile(appDir) {
36526
+ return path4.join(appDir, ENV_FILE);
36527
+ }
36528
+ async function readEnvFile(appDir) {
36529
+ return fs4.readFile(envFile(appDir), "utf8").catch(() => "");
36530
+ }
36531
+ async function writeEnvFile(appDir, text) {
36532
+ await fs4.writeFile(envFile(appDir), text, { mode: 384 });
36533
+ }
36534
+ async function ensureLocalEnv(appDir, app) {
36535
+ const before = await readEnvFile(appDir);
36536
+ let text = before;
36537
+ if (envValue(text, "KIGO_ENV") === void 0) text = upsertEnv(text, "KIGO_ENV", "local");
36538
+ if (envValue(text, "KIGO_APP_NAME") === void 0) text = upsertEnv(text, "KIGO_APP_NAME", app);
36539
+ const createdToken = !envValue(text, "SINDRI_TASK_TOKEN");
36540
+ if (createdToken) text = upsertEnv(text, "SINDRI_TASK_TOKEN", newTaskToken());
36541
+ if (text !== before) await writeEnvFile(appDir, text);
36542
+ return { created_token: createdToken };
36543
+ }
36544
+ function secretKeys(text) {
36545
+ return envKeys(text).filter((k) => !reservedKey(k));
36546
+ }
36547
+
36548
+ // src/layout.ts
36549
+ import fs5 from "node:fs/promises";
36550
+ import path5 from "node:path";
36181
36551
  async function appPaths(cfg, name, ownerHint) {
36182
36552
  const root = appRoot(cfg, name);
36183
36553
  const discovered = await ownerFromApps(root, name);
36184
36554
  const owner = discovered ?? ownerHint ?? await cachedOwner(cfg) ?? "";
36185
- return { root, appDir: owner ? path4.join(root, "apps", owner, name) : path4.join(root, "apps", "_", name), owner };
36555
+ return { root, appDir: owner ? path5.join(root, "apps", owner, name) : path5.join(root, "apps", "_", name), owner };
36186
36556
  }
36187
36557
  async function ownerFromApps(root, name) {
36188
36558
  try {
36189
- const owners = (await fs4.readdir(path4.join(root, "apps"), { withFileTypes: true })).filter((e) => e.isDirectory() && !e.name.startsWith("."));
36559
+ const owners = (await fs5.readdir(path5.join(root, "apps"), { withFileTypes: true })).filter((e) => e.isDirectory() && !e.name.startsWith("."));
36190
36560
  const withApp = [];
36191
36561
  for (const o of owners) {
36192
36562
  try {
36193
- await fs4.access(path4.join(root, "apps", o.name, name, "package.json"));
36563
+ await fs5.access(path5.join(root, "apps", o.name, name, "package.json"));
36194
36564
  withApp.push(o.name);
36195
36565
  } catch {
36196
36566
  }
@@ -36203,15 +36573,15 @@ async function ownerFromApps(root, name) {
36203
36573
  }
36204
36574
  }
36205
36575
  function meFile(cfg) {
36206
- return path4.join(cfg.stateDir, "me.json");
36576
+ return path5.join(cfg.stateDir, "me.json");
36207
36577
  }
36208
36578
  async function rememberMe(cfg, me) {
36209
- await fs4.mkdir(cfg.stateDir, { recursive: true, mode: 448 });
36210
- await fs4.writeFile(meFile(cfg), JSON.stringify(me), { mode: 384 });
36579
+ await fs5.mkdir(cfg.stateDir, { recursive: true, mode: 448 });
36580
+ await fs5.writeFile(meFile(cfg), JSON.stringify(me), { mode: 384 });
36211
36581
  }
36212
36582
  async function cachedOwner(cfg) {
36213
36583
  try {
36214
- const me = JSON.parse(await fs4.readFile(meFile(cfg), "utf8"));
36584
+ const me = JSON.parse(await fs5.readFile(meFile(cfg), "utf8"));
36215
36585
  return me.owner || null;
36216
36586
  } catch {
36217
36587
  return null;
@@ -36219,18 +36589,79 @@ async function cachedOwner(cfg) {
36219
36589
  }
36220
36590
  async function appPresent(p) {
36221
36591
  try {
36222
- await fs4.access(path4.join(p.appDir, "package.json"));
36592
+ await fs5.access(path5.join(p.appDir, "package.json"));
36223
36593
  return true;
36224
36594
  } catch {
36225
36595
  return false;
36226
36596
  }
36227
36597
  }
36598
+ var DESIGN_GUIDE_REL = path5.join(".claude", "skills", "kigo-sindri-design");
36599
+ async function readDesignGuide(p, topic) {
36600
+ const dir = path5.join(p.appDir, DESIGN_GUIDE_REL);
36601
+ let skill;
36602
+ try {
36603
+ skill = await fs5.readFile(path5.join(dir, "SKILL.md"), "utf8");
36604
+ } catch {
36605
+ return null;
36606
+ }
36607
+ const topics = await designTopics(dir);
36608
+ if (topic === void 0) return { content: skill, topics };
36609
+ if (!topics.includes(topic)) return { unknown: true, topics };
36610
+ return { content: await fs5.readFile(path5.join(dir, "references", `${topic}.md`), "utf8"), topics };
36611
+ }
36612
+ async function designTopics(dir) {
36613
+ try {
36614
+ const entries = await fs5.readdir(path5.join(dir, "references"), { withFileTypes: true });
36615
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".md") && !e.name.startsWith(".")).map((e) => e.name.slice(0, -".md".length)).sort();
36616
+ } catch {
36617
+ return [];
36618
+ }
36619
+ }
36620
+ async function linkDesignGuide(p) {
36621
+ const target = path5.join(p.appDir, DESIGN_GUIDE_REL);
36622
+ try {
36623
+ if (!(await fs5.stat(target)).isDirectory()) return false;
36624
+ } catch {
36625
+ return false;
36626
+ }
36627
+ const link = path5.join(p.root, DESIGN_GUIDE_REL);
36628
+ const rel = path5.relative(path5.dirname(link), target);
36629
+ for (const parent of [path5.join(p.root, ".claude"), path5.dirname(link)]) {
36630
+ const st = await fs5.lstat(parent).catch(() => null);
36631
+ if (st && !st.isDirectory()) return false;
36632
+ }
36633
+ const existing = await fs5.lstat(link).catch(() => null);
36634
+ if (existing) {
36635
+ if (!existing.isSymbolicLink()) return false;
36636
+ if (await fs5.readlink(link) === rel) return true;
36637
+ await fs5.unlink(link);
36638
+ }
36639
+ await fs5.mkdir(path5.dirname(link), { recursive: true });
36640
+ await fs5.symlink(rel, link, "dir");
36641
+ return true;
36642
+ }
36228
36643
 
36229
36644
  // src/manifest.ts
36230
- import fs5 from "node:fs/promises";
36231
- import path5 from "node:path";
36645
+ import fs6 from "node:fs/promises";
36646
+ import path6 from "node:path";
36232
36647
  var MANIFEST_FILE = "kigo-app.yaml";
36233
36648
  var KEY_RE = /^([a-z_]+):\s*(.*)$/;
36649
+ function manifestValue(text, key) {
36650
+ for (const line of text.split("\n")) {
36651
+ const m = KEY_RE.exec(line);
36652
+ if (!m || m[1] !== key) continue;
36653
+ const raw = (m[2] ?? "").trim();
36654
+ if (raw.startsWith('"')) {
36655
+ try {
36656
+ return JSON.parse(raw);
36657
+ } catch {
36658
+ return raw.slice(1, -1);
36659
+ }
36660
+ }
36661
+ return raw;
36662
+ }
36663
+ return void 0;
36664
+ }
36234
36665
  function setManifestValue(text, key, value) {
36235
36666
  const line = `${key}: ${JSON.stringify(value)}`;
36236
36667
  const lines = text.split("\n");
@@ -36244,29 +36675,29 @@ function setManifestValue(text, key, value) {
36244
36675
  return lines.join("\n");
36245
36676
  }
36246
36677
  async function readManifest(appDir) {
36247
- return fs5.readFile(path5.join(appDir, MANIFEST_FILE), "utf8").catch(() => "");
36678
+ return fs6.readFile(path6.join(appDir, MANIFEST_FILE), "utf8").catch(() => "");
36248
36679
  }
36249
36680
  async function writeManifestValue(appDir, key, value) {
36250
36681
  const text = await readManifest(appDir);
36251
- await fs5.writeFile(path5.join(appDir, MANIFEST_FILE), setManifestValue(text, key, value));
36682
+ await fs6.writeFile(path6.join(appDir, MANIFEST_FILE), setManifestValue(text, key, value));
36252
36683
  }
36253
36684
 
36254
36685
  // src/preview.ts
36255
- import fs7 from "node:fs/promises";
36686
+ import fs8 from "node:fs/promises";
36256
36687
  import net from "node:net";
36257
- import path7 from "node:path";
36688
+ import path8 from "node:path";
36258
36689
 
36259
36690
  // src/toolchain.ts
36260
- import fs6 from "node:fs/promises";
36261
- import path6 from "node:path";
36691
+ import fs7 from "node:fs/promises";
36692
+ import path7 from "node:path";
36262
36693
  var MIN_NODE = 20;
36263
36694
  var PNPM_ARGS = ["--config.verify-deps-before-run=false"];
36264
36695
  async function resolveToolchain(cfg, env = process.env) {
36265
36696
  const fnmBin = await fnmDefaultBin(cfg);
36266
36697
  if (fnmBin) {
36267
- const childEnv2 = { ...env, PATH: `${fnmBin}${path6.delimiter}${env.PATH ?? ""}`, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", npm_config_verify_deps_before_run: "false" };
36268
- const node2 = path6.join(fnmBin, "node");
36269
- const pnpm = path6.join(fnmBin, "pnpm");
36698
+ const childEnv2 = { ...env, PATH: `${fnmBin}${path7.delimiter}${env.PATH ?? ""}`, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", npm_config_verify_deps_before_run: "false" };
36699
+ const node2 = path7.join(fnmBin, "node");
36700
+ const pnpm = path7.join(fnmBin, "pnpm");
36270
36701
  if (await versionOf(node2, childEnv2) >= MIN_NODE && await ok(pnpm, childEnv2)) {
36271
36702
  return { nodeBin: fnmBin, node: node2, pnpm, env: childEnv2, source: "fnm" };
36272
36703
  }
@@ -36274,20 +36705,20 @@ async function resolveToolchain(cfg, env = process.env) {
36274
36705
  const childEnv = { ...env, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", npm_config_verify_deps_before_run: "false" };
36275
36706
  if (await versionOf("node", childEnv) >= MIN_NODE && await ok("pnpm", childEnv)) {
36276
36707
  const which = await run("node", ["-e", "process.stdout.write(process.execPath)"], { env: childEnv, timeoutMs: 1e4 });
36277
- return { nodeBin: path6.dirname(which.stdout || "node"), node: "node", pnpm: "pnpm", env: childEnv, source: "path" };
36708
+ return { nodeBin: path7.dirname(which.stdout || "node"), node: "node", pnpm: "pnpm", env: childEnv, source: "path" };
36278
36709
  }
36279
36710
  return null;
36280
36711
  }
36281
36712
  async function fnmDefaultBin(cfg) {
36282
- const def = path6.join(cfg.fnmDir, "aliases", "default", "bin");
36283
- if (await exists(path6.join(def, "node"))) return def;
36284
- const versions = path6.join(cfg.fnmDir, "node-versions");
36713
+ const def = path7.join(cfg.fnmDir, "aliases", "default", "bin");
36714
+ if (await exists(path7.join(def, "node"))) return def;
36715
+ const versions = path7.join(cfg.fnmDir, "node-versions");
36285
36716
  try {
36286
- const names = (await fs6.readdir(versions)).filter((n) => n.startsWith("v")).sort(compareVersions);
36717
+ const names = (await fs7.readdir(versions)).filter((n) => n.startsWith("v")).sort(compareVersions);
36287
36718
  const newest = names[names.length - 1];
36288
36719
  if (!newest) return null;
36289
- const bin = path6.join(versions, newest, "installation", "bin");
36290
- return await exists(path6.join(bin, "node")) ? bin : null;
36720
+ const bin = path7.join(versions, newest, "installation", "bin");
36721
+ return await exists(path7.join(bin, "node")) ? bin : null;
36291
36722
  } catch {
36292
36723
  return null;
36293
36724
  }
@@ -36312,7 +36743,7 @@ function compareVersions(a, b) {
36312
36743
  }
36313
36744
  async function exists(p) {
36314
36745
  try {
36315
- await fs6.access(p);
36746
+ await fs7.access(p);
36316
36747
  return true;
36317
36748
  } catch {
36318
36749
  return false;
@@ -36330,42 +36761,42 @@ async function setupToolchain(cfg, opts = {}) {
36330
36761
  }
36331
36762
  const node2 = (await run(tc.node, ["--version"], { env: tc.env, timeoutMs: 1e4 })).stdout.trim();
36332
36763
  const pnpm = (await run(tc.pnpm, ["--version"], { env: tc.env, timeoutMs: 2e4 })).stdout.trim();
36333
- await fs6.mkdir(cfg.home, { recursive: true });
36764
+ await fs7.mkdir(cfg.home, { recursive: true });
36334
36765
  return { installed, node: node2, pnpm, source: tc.source };
36335
36766
  }
36336
36767
  async function installFnmLts(cfg, env) {
36337
- await fs6.mkdir(cfg.fnmDir, { recursive: true });
36768
+ await fs7.mkdir(cfg.fnmDir, { recursive: true });
36338
36769
  const script = await (await fetch("https://fnm.vercel.app/install")).text();
36339
36770
  const sh = await run("bash", ["-s", "--", "--install-dir", cfg.fnmDir, "--skip-shell"], { env, input: script, timeoutMs: 3e5 });
36340
36771
  if (sh.code !== 0) throw new Error(`fnm installer failed: ${sh.stderr.trim().slice(-300)}`);
36341
- const fnm = path6.join(cfg.fnmDir, "fnm");
36772
+ const fnm = path7.join(cfg.fnmDir, "fnm");
36342
36773
  const fnmEnv = { ...env, FNM_DIR: cfg.fnmDir };
36343
36774
  const inst = await run(fnm, ["install", "--lts"], { env: fnmEnv, timeoutMs: 6e5 });
36344
36775
  if (inst.code !== 0) throw new Error(`fnm install --lts failed: ${inst.stderr.trim().slice(-300)}`);
36345
36776
  const def = await run(fnm, ["default", "lts-latest"], { env: fnmEnv, timeoutMs: 6e4 });
36346
36777
  if (def.code !== 0) throw new Error(`fnm default failed: ${def.stderr.trim().slice(-300)}`);
36347
- const bin = path6.join(cfg.fnmDir, "aliases", "default", "bin");
36348
- const childEnv = { ...env, PATH: `${bin}${path6.delimiter}${env.PATH ?? ""}`, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", npm_config_verify_deps_before_run: "false" };
36349
- const enable = await run(path6.join(bin, "corepack"), ["enable", "--install-directory", bin], { env: childEnv, timeoutMs: 6e4 });
36778
+ const bin = path7.join(cfg.fnmDir, "aliases", "default", "bin");
36779
+ const childEnv = { ...env, PATH: `${bin}${path7.delimiter}${env.PATH ?? ""}`, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", npm_config_verify_deps_before_run: "false" };
36780
+ const enable = await run(path7.join(bin, "corepack"), ["enable", "--install-directory", bin], { env: childEnv, timeoutMs: 6e4 });
36350
36781
  if (enable.code !== 0) throw new Error(`corepack enable failed: ${enable.stderr.trim().slice(-300)}`);
36351
- const prep = await run(path6.join(bin, "corepack"), ["prepare", `pnpm@${cfg.pnpmVersion}`, "--activate"], { env: childEnv, timeoutMs: 3e5 });
36782
+ const prep = await run(path7.join(bin, "corepack"), ["prepare", `pnpm@${cfg.pnpmVersion}`, "--activate"], { env: childEnv, timeoutMs: 3e5 });
36352
36783
  if (prep.code !== 0) throw new Error(`corepack prepare pnpm failed: ${prep.stderr.trim().slice(-300)}`);
36353
36784
  }
36354
36785
 
36355
36786
  // src/preview.ts
36356
36787
  function registryFile(cfg) {
36357
- return path7.join(cfg.stateDir, "previews.json");
36788
+ return path8.join(cfg.stateDir, "previews.json");
36358
36789
  }
36359
36790
  async function readRegistry(cfg) {
36360
36791
  try {
36361
- return JSON.parse(await fs7.readFile(registryFile(cfg), "utf8"));
36792
+ return JSON.parse(await fs8.readFile(registryFile(cfg), "utf8"));
36362
36793
  } catch {
36363
36794
  return {};
36364
36795
  }
36365
36796
  }
36366
36797
  async function writeRegistry(cfg, reg) {
36367
- await fs7.mkdir(cfg.stateDir, { recursive: true, mode: 448 });
36368
- await fs7.writeFile(registryFile(cfg), JSON.stringify(reg, null, 2) + "\n");
36798
+ await fs8.mkdir(cfg.stateDir, { recursive: true, mode: 448 });
36799
+ await fs8.writeFile(registryFile(cfg), JSON.stringify(reg, null, 2) + "\n");
36369
36800
  }
36370
36801
  async function freePort(from = 3e3, to = 3099) {
36371
36802
  for (let p = from; p <= to; p++) {
@@ -36402,9 +36833,9 @@ async function startPreview(cfg, tc, app, dir, opts = {}) {
36402
36833
  const existing = await runningPreview(cfg, app, fetchImpl);
36403
36834
  if (existing) return { port: existing.port, url: `http://127.0.0.1:${existing.port}`, reused: true };
36404
36835
  const port = await freePort();
36405
- await fs7.mkdir(path7.join(dir, ".sindri"), { recursive: true });
36406
- const logFile = path7.join(dir, ".sindri", "preview.log");
36407
- await fs7.writeFile(logFile, "");
36836
+ await fs8.mkdir(path8.join(dir, ".sindri"), { recursive: true });
36837
+ const logFile = path8.join(dir, ".sindri", "preview.log");
36838
+ await fs8.writeFile(logFile, "");
36408
36839
  const env = { ...tc.env, KIGO_ENV: "local", DATABASE_URL: "", PORT: String(port), CI: "1", FORCE_COLOR: "0", BROWSER: "none" };
36409
36840
  const pid = spawnDetached(tc.pnpm, [...PNPM_ARGS, "dev", "--port", String(port)], { cwd: dir, env, logFile });
36410
36841
  const reg = await readRegistry(cfg);
@@ -36420,7 +36851,7 @@ async function startPreview(cfg, tc, app, dir, opts = {}) {
36420
36851
  killTree(pid);
36421
36852
  delete reg[app];
36422
36853
  await writeRegistry(cfg, reg);
36423
- const log = await fs7.readFile(logFile, "utf8").catch(() => "");
36854
+ const log = await fs8.readFile(logFile, "utf8").catch(() => "");
36424
36855
  return { port, url: `http://127.0.0.1:${port}`, reused: false, log: log.split("\n").slice(-15).join("\n") };
36425
36856
  }
36426
36857
  async function stopPreview(cfg, app) {
@@ -36437,11 +36868,200 @@ async function openInBrowser(cfg, url2) {
36437
36868
  await run(cfg.opener, [url2], { timeoutMs: 1e4 });
36438
36869
  }
36439
36870
 
36871
+ // src/schedules.ts
36872
+ var SCHEDULE_NAME_RE = /^[a-z][a-z0-9-]{1,31}$/;
36873
+ var SCHEDULE_EVERY = ["5m", "10m", "15m", "30m", "1h", "2h", "6h", "12h", "1d"];
36874
+ var SCHEDULES_MAX = 5;
36875
+ function readSchedules(manifestText) {
36876
+ const raw = manifestValue(manifestText, "schedules");
36877
+ if (raw === void 0 || raw.trim() === "") return { ok: true, tasks: [] };
36878
+ let parsed;
36879
+ try {
36880
+ parsed = JSON.parse(raw);
36881
+ } catch {
36882
+ return { ok: false, error: "La l\xEDnea schedules de kigo-app.yaml no es una lista JSON v\xE1lida." };
36883
+ }
36884
+ if (!Array.isArray(parsed)) return { ok: false, error: "La l\xEDnea schedules de kigo-app.yaml debe ser una lista JSON." };
36885
+ if (parsed.length > SCHEDULES_MAX) return { ok: false, error: `Una app puede declarar hasta ${SCHEDULES_MAX} tareas.` };
36886
+ const tasks = [];
36887
+ for (const item of parsed) {
36888
+ if (!item || typeof item !== "object") return { ok: false, error: "Cada tarea de schedules debe ser un objeto {name, path, every}." };
36889
+ const { name, path: path11, every } = item;
36890
+ if (typeof name !== "string" || !SCHEDULE_NAME_RE.test(name)) {
36891
+ return { ok: false, error: `El nombre de tarea ${JSON.stringify(name)} no es v\xE1lido: min\xFAsculas, d\xEDgitos y guiones, de 2 a 32 letras.` };
36892
+ }
36893
+ if (tasks.some((t) => t.name === name)) return { ok: false, error: `La tarea \xAB${name}\xBB est\xE1 declarada dos veces.` };
36894
+ if (typeof path11 !== "string" || !path11.startsWith("/api/") || path11.includes("?") || path11.includes("#")) {
36895
+ return { ok: false, error: `La ruta de la tarea \xAB${name}\xBB debe empezar con /api/ y no llevar par\xE1metros.` };
36896
+ }
36897
+ if (typeof every !== "string" || !SCHEDULE_EVERY.includes(every)) {
36898
+ return { ok: false, error: `La frecuencia de la tarea \xAB${name}\xBB debe ser una de: ${SCHEDULE_EVERY.join(", ")}.` };
36899
+ }
36900
+ tasks.push({ name, path: path11, every });
36901
+ }
36902
+ return { ok: true, tasks };
36903
+ }
36904
+
36905
+ // src/secretpage.ts
36906
+ import { randomBytes as randomBytes3 } from "node:crypto";
36907
+ import http2 from "node:http";
36908
+ var SECRET_MAX_BYTES = 8 * 1024;
36909
+ var SecretPageBrowserUnavailable = class extends Error {
36910
+ constructor() {
36911
+ super("browser unavailable");
36912
+ }
36913
+ };
36914
+ async function collectSecret(opts) {
36915
+ const nonce = randomBytes3(16).toString("base64url");
36916
+ const server = http2.createServer();
36917
+ await new Promise((resolve, reject) => {
36918
+ server.once("error", reject);
36919
+ server.listen(0, "127.0.0.1", () => resolve());
36920
+ });
36921
+ const port = server.address().port;
36922
+ const origin = `http://127.0.0.1:${port}`;
36923
+ const route = `/s/${nonce}`;
36924
+ const url2 = `${origin}${route}`;
36925
+ let timer;
36926
+ let settled = null;
36927
+ let used = false;
36928
+ const done = new Promise((res0, rej0) => {
36929
+ const resolve = () => {
36930
+ settled = { ok: true };
36931
+ res0();
36932
+ };
36933
+ const reject = (e) => {
36934
+ settled = { err: e };
36935
+ rej0(e);
36936
+ };
36937
+ timer = setTimeout(() => reject(new Error("expired")), opts.timeoutMs ?? 5 * 6e4);
36938
+ server.on("request", (req, res) => {
36939
+ const html = (status, body) => {
36940
+ res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "X-Frame-Options": "DENY" });
36941
+ res.end(body);
36942
+ };
36943
+ const u = new URL(req.url ?? "/", origin);
36944
+ if (u.pathname !== route || used) {
36945
+ html(404, PAGE("Esta p\xE1gina ya no est\xE1 disponible.", "Vuelve a Claude y pide guardar el secreto otra vez."));
36946
+ return;
36947
+ }
36948
+ if (req.method === "GET") {
36949
+ html(200, FORM_PAGE(opts.app, opts.name, opts.hint));
36950
+ return;
36951
+ }
36952
+ if (req.method !== "POST") {
36953
+ res.writeHead(405, { Allow: "GET, POST" }).end();
36954
+ return;
36955
+ }
36956
+ const from = req.headers.origin;
36957
+ if (from && from !== origin) {
36958
+ html(403, PAGE("Esa petici\xF3n no viene de esta p\xE1gina.", "Vuelve a Claude y pide guardar el secreto otra vez."));
36959
+ return;
36960
+ }
36961
+ const chunks = [];
36962
+ let size = 0;
36963
+ let tooBig = false;
36964
+ req.on("data", (c) => {
36965
+ size += c.length;
36966
+ if (size > SECRET_MAX_BYTES * 3 + 1024) {
36967
+ tooBig = true;
36968
+ req.destroy();
36969
+ return;
36970
+ }
36971
+ chunks.push(c);
36972
+ });
36973
+ req.on("end", () => {
36974
+ if (tooBig) return;
36975
+ const body = new URLSearchParams(Buffer.concat(chunks).toString("utf8"));
36976
+ let value = body.get("value") ?? "";
36977
+ value = value.replace(/\r?\n$/, "");
36978
+ if (value === "") {
36979
+ html(400, FORM_PAGE(opts.app, opts.name, opts.hint, "Pega el valor antes de guardar."));
36980
+ return;
36981
+ }
36982
+ if (Buffer.byteLength(value, "utf8") > SECRET_MAX_BYTES) {
36983
+ html(400, FORM_PAGE(opts.app, opts.name, opts.hint, "El valor es demasiado largo (m\xE1ximo 8 KB)."));
36984
+ return;
36985
+ }
36986
+ used = true;
36987
+ opts.onValue(value).then(() => {
36988
+ html(200, PAGE("Listo, ya puedes volver a Claude.", `El secreto ${escapeHtml(opts.name)} qued\xF3 guardado para \xAB${escapeHtml(opts.app)}\xBB. Puedes cerrar esta pesta\xF1a.`));
36989
+ clearTimeout(timer);
36990
+ resolve();
36991
+ }).catch((err) => {
36992
+ html(500, PAGE("No se pudo guardar el secreto.", "Vuelve a Claude: ah\xED te digo qu\xE9 pas\xF3 y lo intentamos de nuevo."));
36993
+ clearTimeout(timer);
36994
+ reject(err instanceof Error ? err : new Error(String(err)));
36995
+ });
36996
+ });
36997
+ });
36998
+ });
36999
+ done.catch(() => void 0);
37000
+ try {
37001
+ if (!await opts.openUrl(url2)) {
37002
+ const state0 = settled;
37003
+ if (state0?.err) throw state0.err;
37004
+ if (!state0) throw new SecretPageBrowserUnavailable();
37005
+ }
37006
+ await done;
37007
+ return { url: url2 };
37008
+ } finally {
37009
+ clearTimeout(timer);
37010
+ server.close();
37011
+ server.closeAllConnections?.();
37012
+ }
37013
+ }
37014
+ function escapeHtml(s) {
37015
+ return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
37016
+ }
37017
+ var STYLE = `
37018
+ :root{--primary:#FF6900;--bg:#FEF9F8;--card:#fff;--fg:#0f172a;--muted:#64748b;--border:#e7e0dc;--danger:#b91c1c}
37019
+ *{box-sizing:border-box}
37020
+ body{margin:0;min-height:100vh;display:grid;place-items:center;background:var(--bg);color:var(--fg);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
37021
+ main{width:min(30rem,calc(100vw - 2rem));background:var(--card);border:1px solid var(--border);border-radius:20px;padding:2rem}
37022
+ .brand{display:flex;align-items:center;gap:.5rem;color:var(--muted);font-size:12px;letter-spacing:.06em;text-transform:uppercase;font-weight:600;margin-bottom:1.25rem}
37023
+ .brand i{display:inline-block;width:10px;height:10px;border-radius:50%;background:var(--primary)}
37024
+ h1{font-size:20px;margin:0 0 .5rem;font-weight:600}
37025
+ p{margin:0 0 1rem;color:var(--muted)}
37026
+ code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#f5efec;padding:.1rem .4rem;border-radius:6px;color:var(--fg)}
37027
+ label{display:block;font-size:13px;font-weight:600;margin:1rem 0 .35rem}
37028
+ .field{display:flex;gap:.5rem}
37029
+ input{flex:1;min-width:0;height:44px;padding:0 .75rem;font:inherit;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;border:1px solid var(--border);border-radius:12px;background:#fff;color:var(--fg)}
37030
+ input:focus-visible,button:focus-visible{outline:2px solid var(--primary);outline-offset:2px}
37031
+ button{height:44px;padding:0 1rem;font:inherit;font-weight:600;border-radius:12px;border:1px solid var(--border);background:#fff;color:var(--fg);cursor:pointer}
37032
+ button.primary{background:var(--primary);border-color:var(--primary);color:#fff;width:100%;margin-top:1.25rem}
37033
+ .error{color:var(--danger);font-size:13px;margin:.5rem 0 0}
37034
+ .note{font-size:13px;margin-top:1rem}
37035
+ `;
37036
+ function shell(title, body) {
37037
+ return `<!doctype html><html lang="es"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="referrer" content="no-referrer"><title>${escapeHtml(title)}</title><style>${STYLE}</style></head><body><main><div class="brand"><i></i>Kigo Sindri</div>${body}</main></body></html>`;
37038
+ }
37039
+ function FORM_PAGE(app, name, hint, error61) {
37040
+ const title = `Guardar secreto para \xAB${app}\xBB`;
37041
+ return shell(
37042
+ title,
37043
+ `<h1>${escapeHtml(title)}</h1>
37044
+ <p>Clave: <code>${escapeHtml(name)}</code></p>
37045
+ ${hint ? `<p>${escapeHtml(hint)}</p>` : ""}
37046
+ <form method="post" autocomplete="off">
37047
+ <label for="value">Valor</label>
37048
+ <div class="field"><input id="value" name="value" type="password" autofocus autocomplete="off" autocapitalize="off" spellcheck="false" required maxlength="8192"><button type="button" id="toggle" aria-pressed="false">mostrar</button></div>
37049
+ ${error61 ? `<p class="error" role="alert">${escapeHtml(error61)}</p>` : ""}
37050
+ <button type="submit" class="primary">Guardar</button>
37051
+ </form>
37052
+ <p class="note">Se guarda en tu computadora y en Sindri. Claude no lo ve ni lo repite.</p>
37053
+ <script>(function(){var i=document.getElementById("value"),b=document.getElementById("toggle");b.addEventListener("click",function(){var show=i.type==="password";i.type=show?"text":"password";b.textContent=show?"ocultar":"mostrar";b.setAttribute("aria-pressed",String(show));i.focus();});})();</script>`
37054
+ );
37055
+ }
37056
+ function PAGE(title, text) {
37057
+ return shell(title, `<h1>${escapeHtml(title)}</h1><p>${text}</p>`);
37058
+ }
37059
+
36440
37060
  // src/tree.ts
36441
37061
  import { createHash as createHash2 } from "node:crypto";
36442
- import fs8 from "node:fs/promises";
37062
+ import fs9 from "node:fs/promises";
36443
37063
  import os3 from "node:os";
36444
- import path8 from "node:path";
37064
+ import path9 from "node:path";
36445
37065
  var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".sindri", "dist", ".turbo", ".git"]);
36446
37066
  function excluded(name) {
36447
37067
  if (EXCLUDED_DIRS.has(name)) return true;
@@ -36452,11 +37072,11 @@ function excluded(name) {
36452
37072
  async function listFiles(root) {
36453
37073
  const out = [];
36454
37074
  async function walk(dir, rel) {
36455
- const entries = await fs8.readdir(dir, { withFileTypes: true });
37075
+ const entries = await fs9.readdir(dir, { withFileTypes: true });
36456
37076
  for (const e of entries) {
36457
37077
  if (excluded(e.name)) continue;
36458
37078
  const r = rel ? `${rel}/${e.name}` : e.name;
36459
- if (e.isDirectory()) await walk(path8.join(dir, e.name), r);
37079
+ if (e.isDirectory()) await walk(path9.join(dir, e.name), r);
36460
37080
  else if (e.isFile()) out.push(r);
36461
37081
  }
36462
37082
  }
@@ -36467,7 +37087,7 @@ async function treeHash(root) {
36467
37087
  const files = await listFiles(root);
36468
37088
  const h = createHash2("sha256");
36469
37089
  for (const rel of files) {
36470
- const fh = createHash2("sha256").update(await fs8.readFile(path8.join(root, rel))).digest("hex");
37090
+ const fh = createHash2("sha256").update(await fs9.readFile(path9.join(root, rel))).digest("hex");
36471
37091
  h.update(`${rel}\0${fh}
36472
37092
  `);
36473
37093
  }
@@ -36476,49 +37096,49 @@ async function treeHash(root) {
36476
37096
  async function packTree(root) {
36477
37097
  const files = await listFiles(root);
36478
37098
  if (files.length === 0) throw new Error("empty tree");
36479
- const listFile = path8.join(await fs8.mkdtemp(path8.join(os3.tmpdir(), "sindri-pack-")), "files.txt");
36480
- await fs8.writeFile(listFile, files.join("\n") + "\n");
36481
- const out = path8.join(path8.dirname(listFile), "app.tar.gz");
37099
+ const listFile = path9.join(await fs9.mkdtemp(path9.join(os3.tmpdir(), "sindri-pack-")), "files.txt");
37100
+ await fs9.writeFile(listFile, files.join("\n") + "\n");
37101
+ const out = path9.join(path9.dirname(listFile), "app.tar.gz");
36482
37102
  const r = await run("tar", ["-czf", out, "-C", root, "-T", listFile], { timeoutMs: 12e4 });
36483
37103
  if (r.code !== 0) throw new Error(`tar failed: ${r.stderr.trim()}`);
36484
- const st = await fs8.stat(out);
37104
+ const st = await fs9.stat(out);
36485
37105
  return { file: out, files: files.length, bytes: st.size };
36486
37106
  }
36487
37107
  async function replaceTree(root, archive) {
36488
- const tmp = await fs8.mkdtemp(path8.join(os3.tmpdir(), "sindri-unpack-"));
37108
+ const tmp = await fs9.mkdtemp(path9.join(os3.tmpdir(), "sindri-unpack-"));
36489
37109
  const r = await run("tar", ["-xzf", archive, "-C", tmp], { timeoutMs: 12e4 });
36490
37110
  if (r.code !== 0) throw new Error(`tar failed: ${r.stderr.trim()}`);
36491
37111
  const src = await singleRoot(tmp);
36492
- await fs8.mkdir(root, { recursive: true });
37112
+ await fs9.mkdir(root, { recursive: true });
36493
37113
  await prune(root, 0);
36494
- await fs8.cp(src, root, { recursive: true, force: true, filter: (s) => path8.basename(s) !== "node_modules" && path8.basename(s) !== ".DS_Store" });
36495
- await fs8.rm(tmp, { recursive: true, force: true });
37114
+ await fs9.cp(src, root, { recursive: true, force: true, filter: (s) => path9.basename(s) !== "node_modules" && path9.basename(s) !== ".DS_Store" });
37115
+ await fs9.rm(tmp, { recursive: true, force: true });
36496
37116
  }
36497
37117
  async function prune(dir, depth) {
36498
- const entries = await fs8.readdir(dir, { withFileTypes: true });
37118
+ const entries = await fs9.readdir(dir, { withFileTypes: true });
36499
37119
  for (const e of entries) {
36500
- const p = path8.join(dir, e.name);
37120
+ const p = path9.join(dir, e.name);
36501
37121
  if (e.name === "node_modules") continue;
36502
37122
  if (depth === 3 && (e.name === ".sindri" || /^\.env(\..*)?$/.test(e.name))) continue;
36503
37123
  if (e.isDirectory()) {
36504
37124
  await prune(p, depth + 1);
36505
- const left = await fs8.readdir(p);
36506
- if (left.length === 0) await fs8.rmdir(p);
37125
+ const left = await fs9.readdir(p);
37126
+ if (left.length === 0) await fs9.rmdir(p);
36507
37127
  } else {
36508
- await fs8.rm(p, { force: true });
37128
+ await fs9.rm(p, { force: true });
36509
37129
  }
36510
37130
  }
36511
37131
  }
36512
37132
  async function singleRoot(dir) {
36513
- const entries = (await fs8.readdir(dir, { withFileTypes: true })).filter((e) => e.name !== ".DS_Store");
36514
- if (entries.length === 1 && entries[0]?.isDirectory() && !await exists2(path8.join(dir, "package.json"))) {
36515
- return path8.join(dir, entries[0].name);
37133
+ const entries = (await fs9.readdir(dir, { withFileTypes: true })).filter((e) => e.name !== ".DS_Store");
37134
+ if (entries.length === 1 && entries[0]?.isDirectory() && !await exists2(path9.join(dir, "package.json"))) {
37135
+ return path9.join(dir, entries[0].name);
36516
37136
  }
36517
37137
  return dir;
36518
37138
  }
36519
37139
  async function exists2(p) {
36520
37140
  try {
36521
- await fs8.access(p);
37141
+ await fs9.access(p);
36522
37142
  return true;
36523
37143
  } catch {
36524
37144
  return false;
@@ -36526,14 +37146,14 @@ async function exists2(p) {
36526
37146
  }
36527
37147
  async function readPublished(root) {
36528
37148
  try {
36529
- return JSON.parse(await fs8.readFile(path8.join(root, ".sindri", "last-published.json"), "utf8"));
37149
+ return JSON.parse(await fs9.readFile(path9.join(root, ".sindri", "last-published.json"), "utf8"));
36530
37150
  } catch {
36531
37151
  return null;
36532
37152
  }
36533
37153
  }
36534
37154
  async function writePublished(root, p) {
36535
- await fs8.mkdir(path8.join(root, ".sindri"), { recursive: true });
36536
- await fs8.writeFile(path8.join(root, ".sindri", "last-published.json"), JSON.stringify(p, null, 2) + "\n");
37155
+ await fs9.mkdir(path9.join(root, ".sindri"), { recursive: true });
37156
+ await fs9.writeFile(path9.join(root, ".sindri", "last-published.json"), JSON.stringify(p, null, 2) + "\n");
36537
37157
  }
36538
37158
 
36539
37159
  // src/tools.ts
@@ -36566,7 +37186,7 @@ var SCHEMAS = {
36566
37186
  preview: { app: appName },
36567
37187
  publish: {
36568
37188
  app: appName,
36569
- message: external_exports.string().min(3).max(MESSAGE_MAX).describe("What changed, in Spanish, as the PM would say it (becomes the version label).")
37189
+ message: external_exports.string().min(3).max(MESSAGE_MAX).describe("What changed, in Spanish, as the builder would say it (becomes the version label).")
36570
37190
  },
36571
37191
  status: { app: appName },
36572
37192
  list_apps: {},
@@ -36576,23 +37196,42 @@ var SCHEMAS = {
36576
37196
  rename_app: {
36577
37197
  app: appName,
36578
37198
  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."),
36579
- 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.")
37199
+ display_name: external_exports.string().min(2).max(60).optional().describe("New visible name (the title people see), in Spanish, as the builder wrote it. The address does not change.")
36580
37200
  },
36581
37201
  delete_app: {
36582
37202
  app: appName,
36583
- 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.")
37203
+ confirm: external_exports.string().describe("The app name exactly as the builder typed it to confirm. Must equal `app`; never fill it in for them.")
36584
37204
  },
36585
37205
  share: { app: appName },
36586
- help: {}
37206
+ help: {},
37207
+ design_guide: {
37208
+ app: appName,
37209
+ 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.")
37210
+ },
37211
+ set_secret: {
37212
+ app: appName,
37213
+ name: external_exports.string().min(2).max(64).describe("Environment variable name the code reads as process.env.NAME: uppercase letters, digits and underscores (e.g. LINEAR_API_KEY). Platform keys are refused."),
37214
+ hint: external_exports.string().min(3).max(200).optional().describe("One line in Spanish shown on the page telling the builder what to paste (e.g. \xABla clave de API de Linear, solo lectura\xBB).")
37215
+ },
37216
+ list_secrets: { app: appName },
37217
+ delete_secret: {
37218
+ app: appName,
37219
+ name: external_exports.string().min(2).max(64).describe("The secret's name, as listed by list_secrets.")
37220
+ },
37221
+ run_task: {
37222
+ app: appName,
37223
+ task: external_exports.string().min(2).max(32).describe("Task name as declared in the `schedules` line of kigo-app.yaml."),
37224
+ where: external_exports.enum(["local", "sindri"]).optional().describe("Where to run it: `local` calls the route of the running preview on this computer; `sindri` runs it in the published app. Default: local when a preview is running, else sindri.")
37225
+ }
36587
37226
  };
36588
37227
  var DESCRIPTIONS = {
36589
37228
  setup: {
36590
37229
  title: "Set up this computer",
36591
- description: "Installs what Sindri needs on the PM's computer (Node.js and pnpm, in the user's home, no admin rights) and creates the Sindri folder. Run it once per machine, before the first create_app, or whenever another tool answers needs_setup. Safe to repeat."
37230
+ description: "Installs what Sindri needs on the builder's computer (Node.js and pnpm, in the user's home, no admin rights) and creates the Sindri folder. Run it once per machine, before the first create_app, or whenever another tool answers needs_setup. Safe to repeat."
36592
37231
  },
36593
37232
  login: {
36594
37233
  title: "Sign in to Kigo",
36595
- description: "Signs the PM in with their Kigo account (opens the browser; they enter with their @cargomovil.com Google account). Run it when another tool answers needs_login. Credentials are stored on this computer."
37234
+ description: "Signs the builder in with their Kigo account (opens the browser; they enter with their @cargomovil.com Google account). Run it when another tool answers needs_login. Credentials are stored on this computer."
36596
37235
  },
36597
37236
  create_app: {
36598
37237
  title: "Create an app",
@@ -36600,7 +37239,7 @@ var DESCRIPTIONS = {
36600
37239
  },
36601
37240
  preview: {
36602
37241
  title: "Preview locally",
36603
- description: "Runs the app on this computer with test data and opens it in the browser (http://127.0.0.1:<port>). Reuses a running preview. Use it to show the PM changes before publishing; edits reload automatically."
37242
+ description: "Runs the app on this computer with test data and opens it in the browser (http://127.0.0.1:<port>). Reuses a running preview. Use it to show the builder changes before publishing; edits reload automatically."
36604
37243
  },
36605
37244
  publish: {
36606
37245
  title: "Publish to Sindri",
@@ -36610,7 +37249,7 @@ var DESCRIPTIONS = {
36610
37249
  title: "App status",
36611
37250
  description: "Address, last published version, whether the local folder exists and whether it has unpublished changes."
36612
37251
  },
36613
- list_apps: { title: "List apps", description: "The PM's apps in Sindri, with address, last version and whether each has a local folder." },
37252
+ list_apps: { title: "List apps", description: "The builder's apps in Sindri, with address, last version and whether each has a local folder." },
36614
37253
  versions: { title: "Versions", description: "Published versions of an app, most recent first (id, date, label)." },
36615
37254
  rollback: {
36616
37255
  title: "Go back to a version",
@@ -36618,20 +37257,40 @@ var DESCRIPTIONS = {
36618
37257
  },
36619
37258
  update_template: {
36620
37259
  title: "Update the template",
36621
- 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."
37260
+ 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 builder asks to update the template or Sindri announces a template fix. Refuses while there are unpublished local edits: publish (or discard) them first."
36622
37261
  },
36623
37262
  rename_app: {
36624
37263
  title: "Rename an app",
36625
- 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).'
37264
+ 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 builder 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).'
36626
37265
  },
36627
37266
  delete_app: {
36628
37267
  title: "Delete an app",
36629
- 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."
37268
+ description: "Removes the app from Sindri (address, published versions and its database) after the builder 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 builder can still recover the files by hand."
36630
37269
  },
36631
37270
  share: { title: "Share", description: "The line to paste in Slack: address, test-data notice and how the team enters." },
36632
37271
  help: {
36633
37272
  title: "What Sindri is",
36634
- 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`)."
37273
+ description: "No network. The Spanish introduction to give the builder 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`)."
37274
+ },
37275
+ design_guide: {
37276
+ title: "Design guide",
37277
+ 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 builder. If it answers `no_design_guide`, the app predates the guide: run update_template once and retry."
37278
+ },
37279
+ set_secret: {
37280
+ title: "Save a secret",
37281
+ description: "Asks the builder for a credential (API key, token, password) without it ever passing through the conversation: opens a page in their browser where they paste the value, stores it in the app's .env.local on this computer and in Sindri, and restarts the running preview so the code sees it. Call it whenever the app needs a credential; never ask the builder to paste it in chat and never write it in code. Refer to it in code as process.env.NAME. Waits up to five minutes. The result never contains the value."
37282
+ },
37283
+ list_secrets: {
37284
+ title: "List secrets",
37285
+ description: "Names (never values) of the app's secrets in Sindri (`names`) and in the .env.local on this computer (`local_names`), so you can tell when one side is missing. Platform keys are not listed."
37286
+ },
37287
+ delete_secret: {
37288
+ title: "Delete a secret",
37289
+ description: "Removes a secret from the app's .env.local on this computer and from Sindri, and restarts the running preview. Ask the builder before calling it."
37290
+ },
37291
+ run_task: {
37292
+ title: "Run a scheduled task now",
37293
+ description: 'Runs one of the app\'s scheduled tasks (declared in the `schedules` line of kigo-app.yaml) and reports status, HTTP status, duration and the tail of its output. `where: "local"` calls the route of the preview running on this computer with the local task token; `where: "sindri"` runs it in the published app. Default: local when a preview is running, else sindri. Test a task locally before publishing it.'
36635
37294
  }
36636
37295
  };
36637
37296
  function createTools(deps) {
@@ -36726,14 +37385,14 @@ function createTools(deps) {
36726
37385
  }
36727
37386
  const created = await api.createApp(a.name, a.description.trim());
36728
37387
  const archive = await api.download(created.archive_url);
36729
- await fs9.mkdir(p.root, { recursive: true });
37388
+ await fs10.mkdir(p.root, { recursive: true });
36730
37389
  await replaceTree(p.root, archive);
36731
- await fs9.rm(path9.dirname(archive), { recursive: true, force: true });
37390
+ await fs10.rm(path10.dirname(archive), { recursive: true, force: true });
36732
37391
  p = await appPaths(cfg, a.name, me.owner);
36733
37392
  if (!await appPresent(p)) {
36734
37393
  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 };
36735
37394
  }
36736
- await fs9.writeFile(path9.join(p.appDir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
37395
+ await ensureLocalEnv(p.appDir, a.name);
36737
37396
  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 });
36738
37397
  if (install.code !== 0) {
36739
37398
  const why = lastLine(install.stderr || install.stdout);
@@ -36745,6 +37404,7 @@ function createTools(deps) {
36745
37404
  }
36746
37405
  const app = await api.app(a.name).catch(() => null);
36747
37406
  await writePublished(p.appDir, { version: app?.last_version ?? null, tree_hash: await treeHash(p.appDir), url: created.url });
37407
+ await linkDesignGuide(p);
36748
37408
  return {
36749
37409
  ok: true,
36750
37410
  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.`,
@@ -36762,7 +37422,7 @@ function createTools(deps) {
36762
37422
  }
36763
37423
  const tc = await toolchain();
36764
37424
  if (!tc) return NEEDS_SETUP;
36765
- if (!await exists2(path9.join(p.root, "node_modules"))) {
37425
+ if (!await exists2(path10.join(p.root, "node_modules"))) {
36766
37426
  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 });
36767
37427
  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) };
36768
37428
  }
@@ -36803,14 +37463,14 @@ function createTools(deps) {
36803
37463
  }
36804
37464
  const packed = await packTree(dir);
36805
37465
  if (packed.bytes > 20 * 1024 * 1024) {
36806
- await fs9.rm(path9.dirname(packed.file), { recursive: true, force: true });
37466
+ await fs10.rm(path10.dirname(packed.file), { recursive: true, force: true });
36807
37467
  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 };
36808
37468
  }
36809
37469
  let result;
36810
37470
  try {
36811
37471
  result = await api.publish(a.app, packed.file, message);
36812
37472
  } finally {
36813
- await fs9.rm(path9.dirname(packed.file), { recursive: true, force: true });
37473
+ await fs10.rm(path10.dirname(packed.file), { recursive: true, force: true });
36814
37474
  }
36815
37475
  await writePublished(dir, { version: result.version, tree_hash: await treeHash(dir), url: result.url });
36816
37476
  return {
@@ -36829,6 +37489,8 @@ function createTools(deps) {
36829
37489
  if (!local.exists) parts.push("En esta computadora no est\xE1 su carpeta.");
36830
37490
  else if (local.unpublished) parts.push("Tienes cambios en tu computadora que a\xFAn no publicas.");
36831
37491
  else parts.push("Tu carpeta est\xE1 al d\xEDa con lo publicado.");
37492
+ const tasks = describeTasks(info);
37493
+ if (tasks) parts.push(tasks);
36832
37494
  return { ok: true, summary: parts.join(" "), app: info, local };
36833
37495
  });
36834
37496
  const list_apps = guard(async () => {
@@ -36852,15 +37514,16 @@ function createTools(deps) {
36852
37514
  let p = await appPaths(cfg, a.app);
36853
37515
  await stopPreview(cfg, a.app);
36854
37516
  const archive = await api.download(`/v1/apps/${encodeURIComponent(a.app)}/archive`, result.version.id);
36855
- await fs9.mkdir(p.root, { recursive: true });
37517
+ await fs10.mkdir(p.root, { recursive: true });
36856
37518
  await replaceTree(p.root, archive);
36857
- await fs9.rm(path9.dirname(archive), { recursive: true, force: true });
37519
+ await fs10.rm(path10.dirname(archive), { recursive: true, force: true });
36858
37520
  p = await appPaths(cfg, a.app, p.owner || void 0);
36859
37521
  const dir = p.appDir;
36860
37522
  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
- if (!await exists2(path9.join(dir, ".env.local"))) await fs9.writeFile(path9.join(dir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
37523
+ await ensureLocalEnv(dir, a.app);
37524
+ await linkDesignGuide(p);
36862
37525
  const tc = await toolchain();
36863
- if (tc && await exists2(path9.join(p.root, "node_modules"))) {
37526
+ if (tc && await exists2(path10.join(p.root, "node_modules"))) {
36864
37527
  const db = await prepareLocalDb(tc, dir);
36865
37528
  if (!db.ok) process.stderr.write(`sindri: local db after rollback: ${db.detail}
36866
37529
  `);
@@ -36890,13 +37553,14 @@ function createTools(deps) {
36890
37553
  await stopPreview(cfg, a.app);
36891
37554
  const archive = await api.download(`/v1/apps/${encodeURIComponent(a.app)}/archive`, result.version?.id);
36892
37555
  await replaceTree(p.root, archive);
36893
- await fs9.rm(path9.dirname(archive), { recursive: true, force: true });
37556
+ await fs10.rm(path10.dirname(archive), { recursive: true, force: true });
36894
37557
  p = await appPaths(cfg, a.app, p.owner || void 0);
36895
37558
  const dir = p.appDir;
36896
37559
  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
- if (!await exists2(path9.join(dir, ".env.local"))) await fs9.writeFile(path9.join(dir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
37560
+ await ensureLocalEnv(dir, a.app);
37561
+ await linkDesignGuide(p);
36898
37562
  const tc = await toolchain();
36899
- if (tc && await exists2(path9.join(p.root, "node_modules"))) {
37563
+ if (tc && await exists2(path10.join(p.root, "node_modules"))) {
36900
37564
  const db = await prepareLocalDb(tc, dir);
36901
37565
  if (!db.ok) process.stderr.write(`sindri: local db after update_template: ${db.detail}
36902
37566
  `);
@@ -36948,10 +37612,12 @@ function createTools(deps) {
36948
37612
  await stopPreview(cfg, a.app);
36949
37613
  const newRoot = appRoot(cfg, newName);
36950
37614
  await moveDir(p.root, newRoot);
36951
- const newDir = path9.join(newRoot, "apps", p.owner, newName);
36952
- await moveDir(path9.join(newRoot, "apps", p.owner, a.app), newDir);
37615
+ const newDir = path10.join(newRoot, "apps", p.owner, newName);
37616
+ await moveDir(path10.join(newRoot, "apps", p.owner, a.app), newDir);
36953
37617
  await writeManifestValue(newDir, "name", newName);
37618
+ await writeEnvFile(newDir, upsertEnv(await readEnvFile(newDir), "KIGO_APP_NAME", newName));
36954
37619
  await writePublished(newDir, { version: result.version ?? before.published_version, tree_hash: await treeHash(newDir), url: result.url });
37620
+ await linkDesignGuide({ root: newRoot, appDir: newDir, owner: p.owner });
36955
37621
  data.path = newDir;
36956
37622
  }
36957
37623
  parts.push(`Listo, "${a.app}" ahora vive en ${result.url}. La direcci\xF3n anterior (${oldUrl}) ya no funciona: comparte la nueva con tu equipo.`);
@@ -36976,7 +37642,7 @@ function createTools(deps) {
36976
37642
  }
36977
37643
  let trash = null;
36978
37644
  if (present) {
36979
- trash = path9.join(cfg.home, ".papelera", `${a.app}-${stamp()}`);
37645
+ trash = path10.join(cfg.home, ".papelera", `${a.app}-${stamp()}`);
36980
37646
  await moveDir(root, trash);
36981
37647
  }
36982
37648
  const kept = "Una copia de sus archivos queda en tu computadora, en la papelera de Sindri; puedes borrarla a mano cuando quieras.";
@@ -36991,21 +37657,175 @@ function createTools(deps) {
36991
37657
  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
37658
  return { ok: true, summary, space_url: SPACE_URL, logged_in, apps_dir: cfg.home };
36993
37659
  });
37660
+ const design_guide = guard(async (a) => {
37661
+ if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
37662
+ const p = await appPaths(cfg, a.app);
37663
+ if (!await appPresent(p)) {
37664
+ 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 };
37665
+ }
37666
+ const topic = a.topic?.trim().toLowerCase().replace(/\.md$/, "");
37667
+ const guide = await readDesignGuide(p, topic || void 0);
37668
+ if (guide === null) {
37669
+ return {
37670
+ ok: false,
37671
+ 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.`,
37672
+ code: "no_design_guide",
37673
+ topics: []
37674
+ };
37675
+ }
37676
+ if ("unknown" in guide) {
37677
+ 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 };
37678
+ }
37679
+ await linkDesignGuide(p);
37680
+ return {
37681
+ ok: true,
37682
+ summary: topic ? `Gu\xEDa de dise\xF1o de Sindri (tema: ${topic}).` : `Gu\xEDa de dise\xF1o de Sindri (temas: ${guide.topics.join(", ")}).`,
37683
+ topic: topic ?? null,
37684
+ topics: guide.topics,
37685
+ content: guide.content
37686
+ };
37687
+ });
37688
+ async function restartPreview(app, dir) {
37689
+ if (!await runningPreview(cfg, app, fetchImpl)) return false;
37690
+ const tc = await toolchain();
37691
+ if (!tc) return false;
37692
+ await stopPreview(cfg, app);
37693
+ const r = await startPreview(cfg, tc, app, dir, { fetchImpl, ...deps.sleepImpl ? { sleepImpl: deps.sleepImpl } : {} });
37694
+ return r.log === void 0;
37695
+ }
37696
+ function checkSecretName(name) {
37697
+ if (!validSecretName(name)) {
37698
+ return { ok: false, summary: `"${name}" no sirve como nombre de secreto: va en MAY\xDASCULAS, con n\xFAmeros o guiones bajos, de 2 a 64 letras (por ejemplo LINEAR_API_KEY).`, code: "bad_name" };
37699
+ }
37700
+ if (reservedKey(name)) return { ok: false, summary: `${name} es una clave de la plataforma; no se puede guardar como secreto. Elige otro nombre.`, code: "reserved_key" };
37701
+ return null;
37702
+ }
37703
+ const set_secret = guard(async (a) => {
37704
+ if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
37705
+ const name = a.name.trim();
37706
+ const bad = checkSecretName(name);
37707
+ if (bad) return bad;
37708
+ await api.app(a.app);
37709
+ const p = await appPaths(cfg, a.app);
37710
+ const local = await appPresent(p);
37711
+ const openUrl = deps.openUrl ?? defaultOpenUrl(cfg);
37712
+ let restarted = false;
37713
+ try {
37714
+ await collectSecret({
37715
+ app: a.app,
37716
+ name,
37717
+ ...a.hint ? { hint: a.hint.trim() } : {},
37718
+ openUrl,
37719
+ ...deps.secretTimeoutMs ? { timeoutMs: deps.secretTimeoutMs } : {},
37720
+ onValue: async (value) => {
37721
+ if (local) await writeEnvFile(p.appDir, upsertEnv(await readEnvFile(p.appDir), name, value));
37722
+ restarted = (await api.putSecret(a.app, name, value)).restarted;
37723
+ }
37724
+ });
37725
+ } catch (err) {
37726
+ if (err instanceof SecretPageBrowserUnavailable) return { ok: false, summary: "No pude abrir tu navegador para pedirte el secreto. \xC1brelo t\xFA y vuelve a intentarlo.", code: "no_browser" };
37727
+ if (err instanceof Error && err.message === "expired") {
37728
+ return { ok: false, summary: `Pasaron cinco minutos sin que se guardara ${name}. Cuando lo tengas a mano, p\xEDdeme guardarlo de nuevo.`, code: "expired", name };
37729
+ }
37730
+ throw err;
37731
+ }
37732
+ const previewRestarted = local ? await restartPreview(a.app, p.appDir) : false;
37733
+ const where = local ? "en tu computadora y en Sindri" : "en Sindri (en esta computadora no est\xE1 la carpeta de la app)";
37734
+ const applied = previewRestarted ? " Reinici\xE9 la vista previa para que lo tome." : "";
37735
+ return { ok: true, summary: `Listo, ${name} qued\xF3 guardado ${where}.${applied}`, name, restarted, preview_restarted: previewRestarted, local };
37736
+ });
37737
+ const list_secrets = guard(async (a) => {
37738
+ if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
37739
+ const names = (await api.listSecrets(a.app)).sort();
37740
+ const p = await appPaths(cfg, a.app);
37741
+ const localNames = await appPresent(p) ? secretKeys(await readEnvFile(p.appDir)).sort() : [];
37742
+ const missingLocal = names.filter((n) => !localNames.includes(n));
37743
+ const missingInSindri = localNames.filter((n) => !names.includes(n));
37744
+ const parts = [names.length === 0 ? `"${a.app}" no tiene secretos en Sindri.` : `"${a.app}" tiene ${names.length === 1 ? "un secreto" : `${names.length} secretos`} en Sindri: ${names.join(", ")}.`];
37745
+ if (missingLocal.length) parts.push(`En tu computadora ${missingLocal.length === 1 ? "falta" : "faltan"} ${missingLocal.join(", ")}.`);
37746
+ if (missingInSindri.length) parts.push(`En Sindri ${missingInSindri.length === 1 ? "falta" : "faltan"} ${missingInSindri.join(", ")} (solo est\xE1 en tu computadora).`);
37747
+ return { ok: true, summary: parts.join(" "), names, local_names: localNames, missing_local: missingLocal, missing_in_sindri: missingInSindri };
37748
+ });
37749
+ const delete_secret = guard(async (a) => {
37750
+ if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
37751
+ const name = a.name.trim();
37752
+ const bad = checkSecretName(name);
37753
+ if (bad) return bad;
37754
+ let inSindri = true;
37755
+ try {
37756
+ await api.deleteSecret(a.app, name);
37757
+ } catch (err) {
37758
+ if (err instanceof ApiError && err.status === 404) inSindri = false;
37759
+ else throw err;
37760
+ }
37761
+ const p = await appPaths(cfg, a.app);
37762
+ let hadLocal = false;
37763
+ if (await appPresent(p)) {
37764
+ const text = await readEnvFile(p.appDir);
37765
+ hadLocal = envValue(text, name) !== void 0;
37766
+ if (hadLocal) await writeEnvFile(p.appDir, removeEnv(text, name));
37767
+ }
37768
+ const previewRestarted = hadLocal ? await restartPreview(a.app, p.appDir) : false;
37769
+ const where = inSindri && hadLocal ? "de Sindri y de tu computadora" : inSindri ? "de Sindri" : hadLocal ? "de tu computadora (en Sindri no estaba)" : "";
37770
+ const summary = where ? `Borr\xE9 ${name} ${where}.${previewRestarted ? " Reinici\xE9 la vista previa." : ""}` : `${name} no estaba guardado ni en Sindri ni en tu computadora.`;
37771
+ return { ok: true, summary, name, removed_from_sindri: inSindri, removed_locally: hadLocal, preview_restarted: previewRestarted };
37772
+ });
37773
+ const run_task = guard(async (a) => {
37774
+ if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
37775
+ const task = a.task.trim();
37776
+ const p = await appPaths(cfg, a.app);
37777
+ const running = await runningPreview(cfg, a.app, fetchImpl);
37778
+ const where = a.where ?? (running ? "local" : "sindri");
37779
+ if (where === "sindri") {
37780
+ const r2 = await api.runTask(a.app, task);
37781
+ return { ok: r2.ok, summary: describeRun(task, "en Sindri", r2), where, status: r2.status, http_status: r2.http_status, duration_ms: r2.duration_ms, output_tail: r2.output_tail };
37782
+ }
37783
+ if (!await appPresent(p)) return { ok: false, summary: `No encuentro la carpeta de "${a.app}" en tu computadora; corre la tarea en Sindri (where: "sindri").`, code: "no_app" };
37784
+ const schedules = readSchedules(await readManifest(p.appDir));
37785
+ if (!schedules.ok) return { ok: false, summary: schedules.error, code: "bad_schedules" };
37786
+ const declared = schedules.tasks.find((t) => t.name === task);
37787
+ if (!declared) {
37788
+ const names = schedules.tasks.map((t) => t.name);
37789
+ return { ok: false, summary: names.length ? `La tarea \xAB${task}\xBB no est\xE1 declarada en kigo-app.yaml de "${a.app}". Las declaradas son: ${names.join(", ")}.` : `"${a.app}" no declara tareas en kigo-app.yaml todav\xEDa; a\xF1ade la l\xEDnea schedules y vuelve a intentarlo.`, code: "task_not_declared", tasks: names };
37790
+ }
37791
+ const ensured = await ensureLocalEnv(p.appDir, a.app);
37792
+ if (!running) return { ok: false, summary: `La vista previa de "${a.app}" no est\xE1 corriendo. Arr\xE1ncala con preview y vuelve a correr la tarea, o c\xF3rrela en Sindri (where: "sindri").`, code: "no_preview" };
37793
+ if (ensured.created_token) await restartPreview(a.app, p.appDir);
37794
+ const token = envValue(await readEnvFile(p.appDir), "SINDRI_TASK_TOKEN") ?? "";
37795
+ const port = (await runningPreview(cfg, a.app, fetchImpl))?.port ?? running.port;
37796
+ const started = Date.now();
37797
+ let res = null;
37798
+ let timedOut = false;
37799
+ try {
37800
+ res = await fetchImpl(`http://127.0.0.1:${port}${declared.path}`, {
37801
+ method: "POST",
37802
+ headers: { "X-Sindri-Task": task, Authorization: `Bearer ${token}`, Accept: "application/json" },
37803
+ signal: AbortSignal.timeout(LOCAL_TASK_TIMEOUT_MS)
37804
+ });
37805
+ } catch (err) {
37806
+ timedOut = err instanceof Error && err.name === "TimeoutError";
37807
+ if (!timedOut) throw err;
37808
+ }
37809
+ const duration_ms = Date.now() - started;
37810
+ const body = res ? await res.text().catch(() => "") : "";
37811
+ const r = { ok: Boolean(res?.ok), status: timedOut ? "timeout" : res?.ok ? "ok" : "error", http_status: res?.status ?? 0, duration_ms, output_tail: body.slice(-2e3) };
37812
+ return { ok: r.ok, summary: describeRun(task, "en tu computadora", r), where, status: r.status, http_status: r.http_status, duration_ms: r.duration_ms, output_tail: r.output_tail, path: declared.path };
37813
+ });
36994
37814
  const logout = guard(async () => {
36995
37815
  await clearCredentials(cfg);
36996
37816
  return { ok: true, summary: "Saliste de tu cuenta de Kigo en esta computadora." };
36997
37817
  });
36998
- return { setup, login, create_app, preview, publish, status, list_apps, versions, rollback, update_template, rename_app, delete_app, share, help, logout };
37818
+ return { setup, login, create_app, preview, publish, status, list_apps, versions, rollback, update_template, rename_app, delete_app, share, help, design_guide, set_secret, list_secrets, delete_secret, run_task, logout };
36999
37819
  }
37000
37820
  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
37821
  async function moveDir(from, to) {
37002
- await fs9.mkdir(path9.dirname(to), { recursive: true });
37822
+ await fs10.mkdir(path10.dirname(to), { recursive: true });
37003
37823
  try {
37004
- await fs9.rename(from, to);
37824
+ await fs10.rename(from, to);
37005
37825
  } catch (err) {
37006
37826
  if (err.code !== "EXDEV") throw err;
37007
- await fs9.cp(from, to, { recursive: true });
37008
- await fs9.rm(from, { recursive: true, force: true });
37827
+ await fs10.cp(from, to, { recursive: true });
37828
+ await fs10.rm(from, { recursive: true, force: true });
37009
37829
  }
37010
37830
  }
37011
37831
  function stamp(d = /* @__PURE__ */ new Date()) {
@@ -37024,8 +37844,8 @@ function describeTemplateFiles(r) {
37024
37844
  return parts.length ? ` ${parts.join(" ")}` : "";
37025
37845
  }
37026
37846
  async function prepareLocalDb(tc, dir, opts = {}) {
37027
- const env = { ...tc.env, KIGO_ENV: "local", DATABASE_URL: "", CI: "1", FORCE_COLOR: "0" };
37028
- const fresh = !await exists2(path9.join(dir, ".sindri", "pglite"));
37847
+ const env = { ...tc.env, KIGO_ENV: "local", KIGO_APP_NAME: path10.basename(dir), DATABASE_URL: "", CI: "1", FORCE_COLOR: "0" };
37848
+ const fresh = !await exists2(path10.join(dir, ".sindri", "pglite"));
37029
37849
  const migrate = await run(tc.pnpm, [...PNPM_ARGS, "db:migrate"], { cwd: dir, env, timeoutMs: 18e4 });
37030
37850
  if (migrate.code !== 0) return { ok: false, detail: lastLine(migrate.stderr || migrate.stdout) };
37031
37851
  if (opts.seed ?? fresh) {
@@ -37042,6 +37862,23 @@ async function localState(cfg, name) {
37042
37862
  const hash2 = await treeHash(dir);
37043
37863
  return { exists: true, path: dir, unpublished: !published || published.tree_hash !== hash2, published_version: published?.version ?? null };
37044
37864
  }
37865
+ var LOCAL_TASK_TIMEOUT_MS = 5 * 6e4;
37866
+ function describeTasks(info) {
37867
+ const tasks = info.tasks ?? [];
37868
+ if (tasks.length === 0) return "";
37869
+ const one = (t) => {
37870
+ const last = t.last_run_at ? `\xFAltima ${when(t.last_run_at)}: ${STATUS_WORD[t.last_status ?? "ok"] ?? t.last_status}` : "a\xFAn no corre";
37871
+ return `${t.name} cada ${t.every} (${last})`;
37872
+ };
37873
+ return `${tasks.length === 1 ? "Tarea" : "Tareas"}: ${tasks.map(one).join("; ")}.`;
37874
+ }
37875
+ var STATUS_WORD = { ok: "bien", error: "fall\xF3", timeout: "se pas\xF3 de tiempo" };
37876
+ function describeRun(task, where, r) {
37877
+ const secs = (r.duration_ms / 1e3).toFixed(r.duration_ms < 1e4 ? 1 : 0).replace(".", ",");
37878
+ if (r.status === "ok") return `La tarea \xAB${task}\xBB corri\xF3 ${where} en ${secs} s y termin\xF3 bien.`;
37879
+ if (r.status === "timeout") return `La tarea \xAB${task}\xBB ${where} se pas\xF3 de tiempo (${secs} s) y se cort\xF3. Revisa qu\xE9 la detiene; el detalle est\xE1 en output_tail.`;
37880
+ return `La tarea \xAB${task}\xBB ${where} fall\xF3${r.http_status ? ` (HTTP ${r.http_status})` : ""} tras ${secs} s. Revisa el \xFAltimo cambio; el detalle est\xE1 en output_tail.`;
37881
+ }
37045
37882
  function describeVersion(info) {
37046
37883
  if (!info.last_version) return `"${info.name}" a\xFAn no tiene versiones publicadas; su direcci\xF3n ser\xE1 ${info.url}.`;
37047
37884
  return `"${info.name}" est\xE1 en ${info.url}; \xFAltima versi\xF3n ${when(info.last_version.at)}, \xAB${info.last_version.label}\xBB.`;
@@ -37057,7 +37894,7 @@ function when(iso, now = /* @__PURE__ */ new Date()) {
37057
37894
  }
37058
37895
 
37059
37896
  // src/index.ts
37060
- var VERSION = "0.1.3";
37897
+ var VERSION = "0.1.5";
37061
37898
  function registerTools(server, tools) {
37062
37899
  for (const name of Object.keys(SCHEMAS)) {
37063
37900
  const meta3 = DESCRIPTIONS[name];
@@ -37072,9 +37909,30 @@ function registerTools(server, tools) {
37072
37909
  });
37073
37910
  }
37074
37911
  }
37912
+ function registerResources(server, tools) {
37913
+ const read = async (uri, app, topic) => {
37914
+ const r = await tools.design_guide({ app, ...topic ? { topic } : {} });
37915
+ if (!r.ok) throw new Error(r.summary);
37916
+ return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: r.content }] };
37917
+ };
37918
+ const one = (v) => Array.isArray(v) ? v[0] ?? "" : v ?? "";
37919
+ server.registerResource(
37920
+ "design-guide",
37921
+ new ResourceTemplate("sindri://design-guide/{app}", { list: void 0 }),
37922
+ { title: "Sindri design guide", description: "Kigo's design guide for an app (overview and topics), read from the app folder.", mimeType: "text/markdown" },
37923
+ (uri, vars) => read(uri, one(vars.app))
37924
+ );
37925
+ server.registerResource(
37926
+ "design-guide-topic",
37927
+ new ResourceTemplate("sindri://design-guide/{app}/{topic}", { list: void 0 }),
37928
+ { title: "Sindri design guide topic", description: "One topic of Kigo's design guide (admin, landing, mini-app, mobile, tokens, writing, checklist).", mimeType: "text/markdown" },
37929
+ (uri, vars) => read(uri, one(vars.app), one(vars.topic))
37930
+ );
37931
+ }
37075
37932
  function createServer(tools) {
37076
37933
  const server = new McpServer({ name: "sindri", version: VERSION }, { instructions: INSTRUCTIONS });
37077
37934
  registerTools(server, tools);
37935
+ registerResources(server, tools);
37078
37936
  return server;
37079
37937
  }
37080
37938
  async function callTool(tools, name, rawArgs) {
@@ -37113,5 +37971,6 @@ export {
37113
37971
  VERSION,
37114
37972
  callTool,
37115
37973
  createServer,
37974
+ registerResources,
37116
37975
  registerTools
37117
37976
  };