@kigo-dev/sindri 0.1.4 → 0.1.6

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
  }
@@ -35962,29 +35962,33 @@ function appRoot(cfg, name) {
35962
35962
  }
35963
35963
 
35964
35964
  // src/instructions.ts
35965
- 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.
35966
35966
 
35967
- Language and tone: always Spanish, "t\xFA", short sentences. Say "app" (feminine: "la app", "tu app"), never "herramienta", "proyecto", "repo", "deploy" or "build". Never paste file paths, commands, logs or stack traces; summarize in one line what happened and what you will do. The \`summary\` field of every tool result is written for the PM: repeat it as is or paraphrase it.
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.
35968
35968
 
35969
- First contact: the first time the PM speaks in a conversation, or whenever they ask what Sindri is or can do, call \`help\` and relay its \`summary\` (that is the introduction: what Sindri is, what they can ask for, the limits, the link to Mi espacio), then ask what they want to build. Do not repeat it later in the same conversation. When the PM asks where to see their apps or their versions, give them the Mi espacio link (\`space_url\`, https://sindri.kigo.dev).
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).
35970
35970
 
35971
35971
  Flow:
35972
- 1. First time on a machine: run \`setup\` (installs Node and pnpm without admin rights) and \`login\` (opens the browser on Kigo's sign-in page; the PM enters with their @cargomovil.com Google account and the browser comes back on its own with "Listo, ya entraste a Sindri"; tell them to return to Claude). \`login\` waits up to five minutes. If any tool answers \`needs_setup\` or \`needs_login\`, run that tool and retry once.
35973
- 2. New app: ask at most one question if the request is vague (for whom, what it must show), then \`create_app\` with a short lowercase name (it becomes the address https://<name>.sindri.kigo.dev) and a one-sentence description in Spanish. Then edit the files in the folder the tool returns to build what the PM asked. The template is Next.js + Drizzle; read its CLAUDE.md first; keep the UI in Spanish, "t\xFA", with the Kigo components it ships.
35974
- 3. Show before shipping: run \`preview\` and tell the PM to look at the local address; ask what to change. Edits reload live.
35975
- 4. Ship: \`publish\` with a message in the PM's words ("Filtro por fecha"). Give the URL and say the team enters with their Google de Kigo. If publish fails, the published version did not change: say what broke in one line, fix it, publish again. The published app has its own database: publish runs the migrations and, when this version changed drizzle/ or src/db/seed.ts, loads the test data again; rows the PM typed into the local preview never travel. Keep test data in the seed, not in the UI.
35976
- 5. Later: \`status\` before working on an existing app (tells you whether the local folder is behind or has unpublished edits); \`versions\` and \`rollback\` when the PM wants "como estaba ayer"; \`share\` for the Slack line.
35977
- 6. Template updates: when the PM says "actualiza la plantilla" or Sindri announces a template fix, run \`update_template\`. It replaces only the files the app never customised, publishes \xABPlantilla actualizada\xBB and brings the local folder to it; the files you had changed are listed in \`skipped\` so you can apply the same change there by hand if it matters. It refuses while there are unpublished local edits: publish them first (or tell the PM what would be lost) and retry.
35978
- 7. Renaming: an app has an address (https://<name>.sindri.kigo.dev) and a visible name (the title people see). When the PM says "c\xE1mbiale el nombre" and it is not obvious which one they mean, ask: "\xBFla direcci\xF3n o el nombre que se ve?". \`rename_app\` with \`new_name\` changes the address (Sindri renames it, the local folder follows, the old link stops working: tell the PM to share the new one); it refuses while there are unpublished edits, so publish first. \`rename_app\` with \`display_name\` changes only the visible name and publishes it as \xABAhora se llama \xAB<nombre>\xBB\xBB. Both in one call is fine.
35979
- 8. Deleting: \`delete_app\` is final in Sindri (address, versions, database). Before calling it, ask the PM to type the app's name to confirm and pass exactly what they wrote as \`confirm\`; never fill it in yourself, never guess it, never call it because you inferred they want it. The local files are moved to the Sindri papelera on their computer, not deleted; say so.
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
35980
 
35981
- Design guide: before creating or changing any page, component, layout, style or copy, call \`design_guide\` for the app, first without \`topic\` (the overview and the list of topics), then with the topic that matches what you are building (admin, landing, mini-app, mobile; tokens, writing and checklist when relevant). Follow it; it is for you, never paste its content to the PM. If it answers \`no_design_guide\`, run \`update_template\` once and call \`design_guide\` again.
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.
35982
35982
 
35983
- Rules: synthetic data only (never paste real emails, plates or phone numbers into seeds); one author per app (the PM); never touch files outside the app folder; do not edit middleware.ts, next.config.ts or package.json scripts (publish will refuse them); apps work Monday to Friday 9:05 am to midnight Mexico City time and the link says so outside those hours. When something fails, explain in one line what to do next; never blame the PM.`;
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.`;
35984
35988
 
35985
35989
  // src/tools.ts
35986
- import fs9 from "node:fs/promises";
35987
- import path9 from "node:path";
35990
+ import fs10 from "node:fs/promises";
35991
+ import path10 from "node:path";
35988
35992
 
35989
35993
  // src/api.ts
35990
35994
  import fs3 from "node:fs/promises";
@@ -36367,6 +36371,20 @@ var ApiClient = class {
36367
36371
  share(name) {
36368
36372
  return this.json("GET", `/v1/apps/${enc(name)}/share`);
36369
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
+ }
36370
36388
  /** Downloads an archive (`archive_url` from create, or /archive of a version) to a temp file. */
36371
36389
  async download(urlOrPath, version2) {
36372
36390
  const u = new URL(urlOrPath.startsWith("http") ? urlOrPath : `${this.cfg.apiUrl}${urlOrPath}`);
@@ -36423,22 +36441,126 @@ function enc(name) {
36423
36441
  return encodeURIComponent(name);
36424
36442
  }
36425
36443
 
36426
- // src/layout.ts
36444
+ // src/envfile.ts
36445
+ import { randomBytes as randomBytes2 } from "node:crypto";
36427
36446
  import fs4 from "node:fs/promises";
36428
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";
36429
36551
  async function appPaths(cfg, name, ownerHint) {
36430
36552
  const root = appRoot(cfg, name);
36431
36553
  const discovered = await ownerFromApps(root, name);
36432
36554
  const owner = discovered ?? ownerHint ?? await cachedOwner(cfg) ?? "";
36433
- 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 };
36434
36556
  }
36435
36557
  async function ownerFromApps(root, name) {
36436
36558
  try {
36437
- 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("."));
36438
36560
  const withApp = [];
36439
36561
  for (const o of owners) {
36440
36562
  try {
36441
- 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"));
36442
36564
  withApp.push(o.name);
36443
36565
  } catch {
36444
36566
  }
@@ -36451,15 +36573,15 @@ async function ownerFromApps(root, name) {
36451
36573
  }
36452
36574
  }
36453
36575
  function meFile(cfg) {
36454
- return path4.join(cfg.stateDir, "me.json");
36576
+ return path5.join(cfg.stateDir, "me.json");
36455
36577
  }
36456
36578
  async function rememberMe(cfg, me) {
36457
- await fs4.mkdir(cfg.stateDir, { recursive: true, mode: 448 });
36458
- 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 });
36459
36581
  }
36460
36582
  async function cachedOwner(cfg) {
36461
36583
  try {
36462
- const me = JSON.parse(await fs4.readFile(meFile(cfg), "utf8"));
36584
+ const me = JSON.parse(await fs5.readFile(meFile(cfg), "utf8"));
36463
36585
  return me.owner || null;
36464
36586
  } catch {
36465
36587
  return null;
@@ -36467,63 +36589,79 @@ async function cachedOwner(cfg) {
36467
36589
  }
36468
36590
  async function appPresent(p) {
36469
36591
  try {
36470
- await fs4.access(path4.join(p.appDir, "package.json"));
36592
+ await fs5.access(path5.join(p.appDir, "package.json"));
36471
36593
  return true;
36472
36594
  } catch {
36473
36595
  return false;
36474
36596
  }
36475
36597
  }
36476
- var DESIGN_GUIDE_REL = path4.join(".claude", "skills", "kigo-sindri-design");
36598
+ var DESIGN_GUIDE_REL = path5.join(".claude", "skills", "kigo-sindri-design");
36477
36599
  async function readDesignGuide(p, topic) {
36478
- const dir = path4.join(p.appDir, DESIGN_GUIDE_REL);
36600
+ const dir = path5.join(p.appDir, DESIGN_GUIDE_REL);
36479
36601
  let skill;
36480
36602
  try {
36481
- skill = await fs4.readFile(path4.join(dir, "SKILL.md"), "utf8");
36603
+ skill = await fs5.readFile(path5.join(dir, "SKILL.md"), "utf8");
36482
36604
  } catch {
36483
36605
  return null;
36484
36606
  }
36485
36607
  const topics = await designTopics(dir);
36486
36608
  if (topic === void 0) return { content: skill, topics };
36487
36609
  if (!topics.includes(topic)) return { unknown: true, topics };
36488
- return { content: await fs4.readFile(path4.join(dir, "references", `${topic}.md`), "utf8"), topics };
36610
+ return { content: await fs5.readFile(path5.join(dir, "references", `${topic}.md`), "utf8"), topics };
36489
36611
  }
36490
36612
  async function designTopics(dir) {
36491
36613
  try {
36492
- const entries = await fs4.readdir(path4.join(dir, "references"), { withFileTypes: true });
36614
+ const entries = await fs5.readdir(path5.join(dir, "references"), { withFileTypes: true });
36493
36615
  return entries.filter((e) => e.isFile() && e.name.endsWith(".md") && !e.name.startsWith(".")).map((e) => e.name.slice(0, -".md".length)).sort();
36494
36616
  } catch {
36495
36617
  return [];
36496
36618
  }
36497
36619
  }
36498
36620
  async function linkDesignGuide(p) {
36499
- const target = path4.join(p.appDir, DESIGN_GUIDE_REL);
36621
+ const target = path5.join(p.appDir, DESIGN_GUIDE_REL);
36500
36622
  try {
36501
- if (!(await fs4.stat(target)).isDirectory()) return false;
36623
+ if (!(await fs5.stat(target)).isDirectory()) return false;
36502
36624
  } catch {
36503
36625
  return false;
36504
36626
  }
36505
- const link = path4.join(p.root, DESIGN_GUIDE_REL);
36506
- const rel = path4.relative(path4.dirname(link), target);
36507
- for (const parent of [path4.join(p.root, ".claude"), path4.dirname(link)]) {
36508
- const st = await fs4.lstat(parent).catch(() => null);
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);
36509
36631
  if (st && !st.isDirectory()) return false;
36510
36632
  }
36511
- const existing = await fs4.lstat(link).catch(() => null);
36633
+ const existing = await fs5.lstat(link).catch(() => null);
36512
36634
  if (existing) {
36513
36635
  if (!existing.isSymbolicLink()) return false;
36514
- if (await fs4.readlink(link) === rel) return true;
36515
- await fs4.unlink(link);
36636
+ if (await fs5.readlink(link) === rel) return true;
36637
+ await fs5.unlink(link);
36516
36638
  }
36517
- await fs4.mkdir(path4.dirname(link), { recursive: true });
36518
- await fs4.symlink(rel, link, "dir");
36639
+ await fs5.mkdir(path5.dirname(link), { recursive: true });
36640
+ await fs5.symlink(rel, link, "dir");
36519
36641
  return true;
36520
36642
  }
36521
36643
 
36522
36644
  // src/manifest.ts
36523
- import fs5 from "node:fs/promises";
36524
- import path5 from "node:path";
36645
+ import fs6 from "node:fs/promises";
36646
+ import path6 from "node:path";
36525
36647
  var MANIFEST_FILE = "kigo-app.yaml";
36526
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
+ }
36527
36665
  function setManifestValue(text, key, value) {
36528
36666
  const line = `${key}: ${JSON.stringify(value)}`;
36529
36667
  const lines = text.split("\n");
@@ -36537,29 +36675,29 @@ function setManifestValue(text, key, value) {
36537
36675
  return lines.join("\n");
36538
36676
  }
36539
36677
  async function readManifest(appDir) {
36540
- return fs5.readFile(path5.join(appDir, MANIFEST_FILE), "utf8").catch(() => "");
36678
+ return fs6.readFile(path6.join(appDir, MANIFEST_FILE), "utf8").catch(() => "");
36541
36679
  }
36542
36680
  async function writeManifestValue(appDir, key, value) {
36543
36681
  const text = await readManifest(appDir);
36544
- 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));
36545
36683
  }
36546
36684
 
36547
36685
  // src/preview.ts
36548
- import fs7 from "node:fs/promises";
36686
+ import fs8 from "node:fs/promises";
36549
36687
  import net from "node:net";
36550
- import path7 from "node:path";
36688
+ import path8 from "node:path";
36551
36689
 
36552
36690
  // src/toolchain.ts
36553
- import fs6 from "node:fs/promises";
36554
- import path6 from "node:path";
36691
+ import fs7 from "node:fs/promises";
36692
+ import path7 from "node:path";
36555
36693
  var MIN_NODE = 20;
36556
36694
  var PNPM_ARGS = ["--config.verify-deps-before-run=false"];
36557
36695
  async function resolveToolchain(cfg, env = process.env) {
36558
36696
  const fnmBin = await fnmDefaultBin(cfg);
36559
36697
  if (fnmBin) {
36560
- const childEnv2 = { ...env, PATH: `${fnmBin}${path6.delimiter}${env.PATH ?? ""}`, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", npm_config_verify_deps_before_run: "false" };
36561
- const node2 = path6.join(fnmBin, "node");
36562
- 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");
36563
36701
  if (await versionOf(node2, childEnv2) >= MIN_NODE && await ok(pnpm, childEnv2)) {
36564
36702
  return { nodeBin: fnmBin, node: node2, pnpm, env: childEnv2, source: "fnm" };
36565
36703
  }
@@ -36567,20 +36705,20 @@ async function resolveToolchain(cfg, env = process.env) {
36567
36705
  const childEnv = { ...env, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", npm_config_verify_deps_before_run: "false" };
36568
36706
  if (await versionOf("node", childEnv) >= MIN_NODE && await ok("pnpm", childEnv)) {
36569
36707
  const which = await run("node", ["-e", "process.stdout.write(process.execPath)"], { env: childEnv, timeoutMs: 1e4 });
36570
- 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" };
36571
36709
  }
36572
36710
  return null;
36573
36711
  }
36574
36712
  async function fnmDefaultBin(cfg) {
36575
- const def = path6.join(cfg.fnmDir, "aliases", "default", "bin");
36576
- if (await exists(path6.join(def, "node"))) return def;
36577
- 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");
36578
36716
  try {
36579
- 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);
36580
36718
  const newest = names[names.length - 1];
36581
36719
  if (!newest) return null;
36582
- const bin = path6.join(versions, newest, "installation", "bin");
36583
- 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;
36584
36722
  } catch {
36585
36723
  return null;
36586
36724
  }
@@ -36605,7 +36743,7 @@ function compareVersions(a, b) {
36605
36743
  }
36606
36744
  async function exists(p) {
36607
36745
  try {
36608
- await fs6.access(p);
36746
+ await fs7.access(p);
36609
36747
  return true;
36610
36748
  } catch {
36611
36749
  return false;
@@ -36623,42 +36761,49 @@ async function setupToolchain(cfg, opts = {}) {
36623
36761
  }
36624
36762
  const node2 = (await run(tc.node, ["--version"], { env: tc.env, timeoutMs: 1e4 })).stdout.trim();
36625
36763
  const pnpm = (await run(tc.pnpm, ["--version"], { env: tc.env, timeoutMs: 2e4 })).stdout.trim();
36626
- await fs6.mkdir(cfg.home, { recursive: true });
36764
+ await fs7.mkdir(cfg.home, { recursive: true });
36627
36765
  return { installed, node: node2, pnpm, source: tc.source };
36628
36766
  }
36767
+ function fnmInstallerArgs(installDir, platform = process.platform) {
36768
+ return ["-s", "--", "--install-dir", installDir, "--skip-shell", ...platform === "darwin" ? ["--force-no-brew"] : []];
36769
+ }
36770
+ function stepFailed(step, r) {
36771
+ const out = [r.stderr.trim(), r.stdout.trim()].filter(Boolean).join("\n");
36772
+ return new Error(`${step} failed (exit ${r.code}${r.timedOut ? ", timeout" : ""}): ${out.slice(-300) || "sin salida"}`);
36773
+ }
36629
36774
  async function installFnmLts(cfg, env) {
36630
- await fs6.mkdir(cfg.fnmDir, { recursive: true });
36775
+ await fs7.mkdir(cfg.fnmDir, { recursive: true });
36631
36776
  const script = await (await fetch("https://fnm.vercel.app/install")).text();
36632
- const sh = await run("bash", ["-s", "--", "--install-dir", cfg.fnmDir, "--skip-shell"], { env, input: script, timeoutMs: 3e5 });
36633
- if (sh.code !== 0) throw new Error(`fnm installer failed: ${sh.stderr.trim().slice(-300)}`);
36634
- const fnm = path6.join(cfg.fnmDir, "fnm");
36777
+ const sh = await run("bash", fnmInstallerArgs(cfg.fnmDir), { env, input: script, timeoutMs: 3e5 });
36778
+ if (sh.code !== 0) throw stepFailed("fnm installer", sh);
36779
+ const fnm = path7.join(cfg.fnmDir, "fnm");
36635
36780
  const fnmEnv = { ...env, FNM_DIR: cfg.fnmDir };
36636
36781
  const inst = await run(fnm, ["install", "--lts"], { env: fnmEnv, timeoutMs: 6e5 });
36637
- if (inst.code !== 0) throw new Error(`fnm install --lts failed: ${inst.stderr.trim().slice(-300)}`);
36782
+ if (inst.code !== 0) throw stepFailed("fnm install --lts", inst);
36638
36783
  const def = await run(fnm, ["default", "lts-latest"], { env: fnmEnv, timeoutMs: 6e4 });
36639
- if (def.code !== 0) throw new Error(`fnm default failed: ${def.stderr.trim().slice(-300)}`);
36640
- const bin = path6.join(cfg.fnmDir, "aliases", "default", "bin");
36641
- const childEnv = { ...env, PATH: `${bin}${path6.delimiter}${env.PATH ?? ""}`, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", npm_config_verify_deps_before_run: "false" };
36642
- const enable = await run(path6.join(bin, "corepack"), ["enable", "--install-directory", bin], { env: childEnv, timeoutMs: 6e4 });
36643
- if (enable.code !== 0) throw new Error(`corepack enable failed: ${enable.stderr.trim().slice(-300)}`);
36644
- const prep = await run(path6.join(bin, "corepack"), ["prepare", `pnpm@${cfg.pnpmVersion}`, "--activate"], { env: childEnv, timeoutMs: 3e5 });
36645
- if (prep.code !== 0) throw new Error(`corepack prepare pnpm failed: ${prep.stderr.trim().slice(-300)}`);
36784
+ if (def.code !== 0) throw stepFailed("fnm default", def);
36785
+ const bin = path7.join(cfg.fnmDir, "aliases", "default", "bin");
36786
+ const childEnv = { ...env, PATH: `${bin}${path7.delimiter}${env.PATH ?? ""}`, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", npm_config_verify_deps_before_run: "false" };
36787
+ const enable = await run(path7.join(bin, "corepack"), ["enable", "--install-directory", bin], { env: childEnv, timeoutMs: 6e4 });
36788
+ if (enable.code !== 0) throw stepFailed("corepack enable", enable);
36789
+ const prep = await run(path7.join(bin, "corepack"), ["prepare", `pnpm@${cfg.pnpmVersion}`, "--activate"], { env: childEnv, timeoutMs: 3e5 });
36790
+ if (prep.code !== 0) throw stepFailed("corepack prepare pnpm", prep);
36646
36791
  }
36647
36792
 
36648
36793
  // src/preview.ts
36649
36794
  function registryFile(cfg) {
36650
- return path7.join(cfg.stateDir, "previews.json");
36795
+ return path8.join(cfg.stateDir, "previews.json");
36651
36796
  }
36652
36797
  async function readRegistry(cfg) {
36653
36798
  try {
36654
- return JSON.parse(await fs7.readFile(registryFile(cfg), "utf8"));
36799
+ return JSON.parse(await fs8.readFile(registryFile(cfg), "utf8"));
36655
36800
  } catch {
36656
36801
  return {};
36657
36802
  }
36658
36803
  }
36659
36804
  async function writeRegistry(cfg, reg) {
36660
- await fs7.mkdir(cfg.stateDir, { recursive: true, mode: 448 });
36661
- await fs7.writeFile(registryFile(cfg), JSON.stringify(reg, null, 2) + "\n");
36805
+ await fs8.mkdir(cfg.stateDir, { recursive: true, mode: 448 });
36806
+ await fs8.writeFile(registryFile(cfg), JSON.stringify(reg, null, 2) + "\n");
36662
36807
  }
36663
36808
  async function freePort(from = 3e3, to = 3099) {
36664
36809
  for (let p = from; p <= to; p++) {
@@ -36695,9 +36840,9 @@ async function startPreview(cfg, tc, app, dir, opts = {}) {
36695
36840
  const existing = await runningPreview(cfg, app, fetchImpl);
36696
36841
  if (existing) return { port: existing.port, url: `http://127.0.0.1:${existing.port}`, reused: true };
36697
36842
  const port = await freePort();
36698
- await fs7.mkdir(path7.join(dir, ".sindri"), { recursive: true });
36699
- const logFile = path7.join(dir, ".sindri", "preview.log");
36700
- await fs7.writeFile(logFile, "");
36843
+ await fs8.mkdir(path8.join(dir, ".sindri"), { recursive: true });
36844
+ const logFile = path8.join(dir, ".sindri", "preview.log");
36845
+ await fs8.writeFile(logFile, "");
36701
36846
  const env = { ...tc.env, KIGO_ENV: "local", DATABASE_URL: "", PORT: String(port), CI: "1", FORCE_COLOR: "0", BROWSER: "none" };
36702
36847
  const pid = spawnDetached(tc.pnpm, [...PNPM_ARGS, "dev", "--port", String(port)], { cwd: dir, env, logFile });
36703
36848
  const reg = await readRegistry(cfg);
@@ -36713,7 +36858,7 @@ async function startPreview(cfg, tc, app, dir, opts = {}) {
36713
36858
  killTree(pid);
36714
36859
  delete reg[app];
36715
36860
  await writeRegistry(cfg, reg);
36716
- const log = await fs7.readFile(logFile, "utf8").catch(() => "");
36861
+ const log = await fs8.readFile(logFile, "utf8").catch(() => "");
36717
36862
  return { port, url: `http://127.0.0.1:${port}`, reused: false, log: log.split("\n").slice(-15).join("\n") };
36718
36863
  }
36719
36864
  async function stopPreview(cfg, app) {
@@ -36730,11 +36875,200 @@ async function openInBrowser(cfg, url2) {
36730
36875
  await run(cfg.opener, [url2], { timeoutMs: 1e4 });
36731
36876
  }
36732
36877
 
36878
+ // src/schedules.ts
36879
+ var SCHEDULE_NAME_RE = /^[a-z][a-z0-9-]{1,31}$/;
36880
+ var SCHEDULE_EVERY = ["5m", "10m", "15m", "30m", "1h", "2h", "6h", "12h", "1d"];
36881
+ var SCHEDULES_MAX = 5;
36882
+ function readSchedules(manifestText) {
36883
+ const raw = manifestValue(manifestText, "schedules");
36884
+ if (raw === void 0 || raw.trim() === "") return { ok: true, tasks: [] };
36885
+ let parsed;
36886
+ try {
36887
+ parsed = JSON.parse(raw);
36888
+ } catch {
36889
+ return { ok: false, error: "La l\xEDnea schedules de kigo-app.yaml no es una lista JSON v\xE1lida." };
36890
+ }
36891
+ if (!Array.isArray(parsed)) return { ok: false, error: "La l\xEDnea schedules de kigo-app.yaml debe ser una lista JSON." };
36892
+ if (parsed.length > SCHEDULES_MAX) return { ok: false, error: `Una app puede declarar hasta ${SCHEDULES_MAX} tareas.` };
36893
+ const tasks = [];
36894
+ for (const item of parsed) {
36895
+ if (!item || typeof item !== "object") return { ok: false, error: "Cada tarea de schedules debe ser un objeto {name, path, every}." };
36896
+ const { name, path: path11, every } = item;
36897
+ if (typeof name !== "string" || !SCHEDULE_NAME_RE.test(name)) {
36898
+ 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.` };
36899
+ }
36900
+ if (tasks.some((t) => t.name === name)) return { ok: false, error: `La tarea \xAB${name}\xBB est\xE1 declarada dos veces.` };
36901
+ if (typeof path11 !== "string" || !path11.startsWith("/api/") || path11.includes("?") || path11.includes("#")) {
36902
+ return { ok: false, error: `La ruta de la tarea \xAB${name}\xBB debe empezar con /api/ y no llevar par\xE1metros.` };
36903
+ }
36904
+ if (typeof every !== "string" || !SCHEDULE_EVERY.includes(every)) {
36905
+ return { ok: false, error: `La frecuencia de la tarea \xAB${name}\xBB debe ser una de: ${SCHEDULE_EVERY.join(", ")}.` };
36906
+ }
36907
+ tasks.push({ name, path: path11, every });
36908
+ }
36909
+ return { ok: true, tasks };
36910
+ }
36911
+
36912
+ // src/secretpage.ts
36913
+ import { randomBytes as randomBytes3 } from "node:crypto";
36914
+ import http2 from "node:http";
36915
+ var SECRET_MAX_BYTES = 8 * 1024;
36916
+ var SecretPageBrowserUnavailable = class extends Error {
36917
+ constructor() {
36918
+ super("browser unavailable");
36919
+ }
36920
+ };
36921
+ async function collectSecret(opts) {
36922
+ const nonce = randomBytes3(16).toString("base64url");
36923
+ const server = http2.createServer();
36924
+ await new Promise((resolve, reject) => {
36925
+ server.once("error", reject);
36926
+ server.listen(0, "127.0.0.1", () => resolve());
36927
+ });
36928
+ const port = server.address().port;
36929
+ const origin = `http://127.0.0.1:${port}`;
36930
+ const route = `/s/${nonce}`;
36931
+ const url2 = `${origin}${route}`;
36932
+ let timer;
36933
+ let settled = null;
36934
+ let used = false;
36935
+ const done = new Promise((res0, rej0) => {
36936
+ const resolve = () => {
36937
+ settled = { ok: true };
36938
+ res0();
36939
+ };
36940
+ const reject = (e) => {
36941
+ settled = { err: e };
36942
+ rej0(e);
36943
+ };
36944
+ timer = setTimeout(() => reject(new Error("expired")), opts.timeoutMs ?? 5 * 6e4);
36945
+ server.on("request", (req, res) => {
36946
+ const html = (status, body) => {
36947
+ res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "X-Frame-Options": "DENY" });
36948
+ res.end(body);
36949
+ };
36950
+ const u = new URL(req.url ?? "/", origin);
36951
+ if (u.pathname !== route || used) {
36952
+ html(404, PAGE("Esta p\xE1gina ya no est\xE1 disponible.", "Vuelve a Claude y pide guardar el secreto otra vez."));
36953
+ return;
36954
+ }
36955
+ if (req.method === "GET") {
36956
+ html(200, FORM_PAGE(opts.app, opts.name, opts.hint));
36957
+ return;
36958
+ }
36959
+ if (req.method !== "POST") {
36960
+ res.writeHead(405, { Allow: "GET, POST" }).end();
36961
+ return;
36962
+ }
36963
+ const from = req.headers.origin;
36964
+ if (from && from !== origin) {
36965
+ html(403, PAGE("Esa petici\xF3n no viene de esta p\xE1gina.", "Vuelve a Claude y pide guardar el secreto otra vez."));
36966
+ return;
36967
+ }
36968
+ const chunks = [];
36969
+ let size = 0;
36970
+ let tooBig = false;
36971
+ req.on("data", (c) => {
36972
+ size += c.length;
36973
+ if (size > SECRET_MAX_BYTES * 3 + 1024) {
36974
+ tooBig = true;
36975
+ req.destroy();
36976
+ return;
36977
+ }
36978
+ chunks.push(c);
36979
+ });
36980
+ req.on("end", () => {
36981
+ if (tooBig) return;
36982
+ const body = new URLSearchParams(Buffer.concat(chunks).toString("utf8"));
36983
+ let value = body.get("value") ?? "";
36984
+ value = value.replace(/\r?\n$/, "");
36985
+ if (value === "") {
36986
+ html(400, FORM_PAGE(opts.app, opts.name, opts.hint, "Pega el valor antes de guardar."));
36987
+ return;
36988
+ }
36989
+ if (Buffer.byteLength(value, "utf8") > SECRET_MAX_BYTES) {
36990
+ html(400, FORM_PAGE(opts.app, opts.name, opts.hint, "El valor es demasiado largo (m\xE1ximo 8 KB)."));
36991
+ return;
36992
+ }
36993
+ used = true;
36994
+ opts.onValue(value).then(() => {
36995
+ 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.`));
36996
+ clearTimeout(timer);
36997
+ resolve();
36998
+ }).catch((err) => {
36999
+ 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."));
37000
+ clearTimeout(timer);
37001
+ reject(err instanceof Error ? err : new Error(String(err)));
37002
+ });
37003
+ });
37004
+ });
37005
+ });
37006
+ done.catch(() => void 0);
37007
+ try {
37008
+ if (!await opts.openUrl(url2)) {
37009
+ const state0 = settled;
37010
+ if (state0?.err) throw state0.err;
37011
+ if (!state0) throw new SecretPageBrowserUnavailable();
37012
+ }
37013
+ await done;
37014
+ return { url: url2 };
37015
+ } finally {
37016
+ clearTimeout(timer);
37017
+ server.close();
37018
+ server.closeAllConnections?.();
37019
+ }
37020
+ }
37021
+ function escapeHtml(s) {
37022
+ return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
37023
+ }
37024
+ var STYLE = `
37025
+ :root{--primary:#FF6900;--bg:#FEF9F8;--card:#fff;--fg:#0f172a;--muted:#64748b;--border:#e7e0dc;--danger:#b91c1c}
37026
+ *{box-sizing:border-box}
37027
+ 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}
37028
+ main{width:min(30rem,calc(100vw - 2rem));background:var(--card);border:1px solid var(--border);border-radius:20px;padding:2rem}
37029
+ .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}
37030
+ .brand i{display:inline-block;width:10px;height:10px;border-radius:50%;background:var(--primary)}
37031
+ h1{font-size:20px;margin:0 0 .5rem;font-weight:600}
37032
+ p{margin:0 0 1rem;color:var(--muted)}
37033
+ code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#f5efec;padding:.1rem .4rem;border-radius:6px;color:var(--fg)}
37034
+ label{display:block;font-size:13px;font-weight:600;margin:1rem 0 .35rem}
37035
+ .field{display:flex;gap:.5rem}
37036
+ 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)}
37037
+ input:focus-visible,button:focus-visible{outline:2px solid var(--primary);outline-offset:2px}
37038
+ 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}
37039
+ button.primary{background:var(--primary);border-color:var(--primary);color:#fff;width:100%;margin-top:1.25rem}
37040
+ .error{color:var(--danger);font-size:13px;margin:.5rem 0 0}
37041
+ .note{font-size:13px;margin-top:1rem}
37042
+ `;
37043
+ function shell(title, body) {
37044
+ 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>`;
37045
+ }
37046
+ function FORM_PAGE(app, name, hint, error61) {
37047
+ const title = `Guardar secreto para \xAB${app}\xBB`;
37048
+ return shell(
37049
+ title,
37050
+ `<h1>${escapeHtml(title)}</h1>
37051
+ <p>Clave: <code>${escapeHtml(name)}</code></p>
37052
+ ${hint ? `<p>${escapeHtml(hint)}</p>` : ""}
37053
+ <form method="post" autocomplete="off">
37054
+ <label for="value">Valor</label>
37055
+ <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>
37056
+ ${error61 ? `<p class="error" role="alert">${escapeHtml(error61)}</p>` : ""}
37057
+ <button type="submit" class="primary">Guardar</button>
37058
+ </form>
37059
+ <p class="note">Se guarda en tu computadora y en Sindri. Claude no lo ve ni lo repite.</p>
37060
+ <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>`
37061
+ );
37062
+ }
37063
+ function PAGE(title, text) {
37064
+ return shell(title, `<h1>${escapeHtml(title)}</h1><p>${text}</p>`);
37065
+ }
37066
+
36733
37067
  // src/tree.ts
36734
37068
  import { createHash as createHash2 } from "node:crypto";
36735
- import fs8 from "node:fs/promises";
37069
+ import fs9 from "node:fs/promises";
36736
37070
  import os3 from "node:os";
36737
- import path8 from "node:path";
37071
+ import path9 from "node:path";
36738
37072
  var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".sindri", "dist", ".turbo", ".git"]);
36739
37073
  function excluded(name) {
36740
37074
  if (EXCLUDED_DIRS.has(name)) return true;
@@ -36745,11 +37079,11 @@ function excluded(name) {
36745
37079
  async function listFiles(root) {
36746
37080
  const out = [];
36747
37081
  async function walk(dir, rel) {
36748
- const entries = await fs8.readdir(dir, { withFileTypes: true });
37082
+ const entries = await fs9.readdir(dir, { withFileTypes: true });
36749
37083
  for (const e of entries) {
36750
37084
  if (excluded(e.name)) continue;
36751
37085
  const r = rel ? `${rel}/${e.name}` : e.name;
36752
- if (e.isDirectory()) await walk(path8.join(dir, e.name), r);
37086
+ if (e.isDirectory()) await walk(path9.join(dir, e.name), r);
36753
37087
  else if (e.isFile()) out.push(r);
36754
37088
  }
36755
37089
  }
@@ -36760,7 +37094,7 @@ async function treeHash(root) {
36760
37094
  const files = await listFiles(root);
36761
37095
  const h = createHash2("sha256");
36762
37096
  for (const rel of files) {
36763
- const fh = createHash2("sha256").update(await fs8.readFile(path8.join(root, rel))).digest("hex");
37097
+ const fh = createHash2("sha256").update(await fs9.readFile(path9.join(root, rel))).digest("hex");
36764
37098
  h.update(`${rel}\0${fh}
36765
37099
  `);
36766
37100
  }
@@ -36769,49 +37103,49 @@ async function treeHash(root) {
36769
37103
  async function packTree(root) {
36770
37104
  const files = await listFiles(root);
36771
37105
  if (files.length === 0) throw new Error("empty tree");
36772
- const listFile = path8.join(await fs8.mkdtemp(path8.join(os3.tmpdir(), "sindri-pack-")), "files.txt");
36773
- await fs8.writeFile(listFile, files.join("\n") + "\n");
36774
- const out = path8.join(path8.dirname(listFile), "app.tar.gz");
37106
+ const listFile = path9.join(await fs9.mkdtemp(path9.join(os3.tmpdir(), "sindri-pack-")), "files.txt");
37107
+ await fs9.writeFile(listFile, files.join("\n") + "\n");
37108
+ const out = path9.join(path9.dirname(listFile), "app.tar.gz");
36775
37109
  const r = await run("tar", ["-czf", out, "-C", root, "-T", listFile], { timeoutMs: 12e4 });
36776
37110
  if (r.code !== 0) throw new Error(`tar failed: ${r.stderr.trim()}`);
36777
- const st = await fs8.stat(out);
37111
+ const st = await fs9.stat(out);
36778
37112
  return { file: out, files: files.length, bytes: st.size };
36779
37113
  }
36780
37114
  async function replaceTree(root, archive) {
36781
- const tmp = await fs8.mkdtemp(path8.join(os3.tmpdir(), "sindri-unpack-"));
37115
+ const tmp = await fs9.mkdtemp(path9.join(os3.tmpdir(), "sindri-unpack-"));
36782
37116
  const r = await run("tar", ["-xzf", archive, "-C", tmp], { timeoutMs: 12e4 });
36783
37117
  if (r.code !== 0) throw new Error(`tar failed: ${r.stderr.trim()}`);
36784
37118
  const src = await singleRoot(tmp);
36785
- await fs8.mkdir(root, { recursive: true });
37119
+ await fs9.mkdir(root, { recursive: true });
36786
37120
  await prune(root, 0);
36787
- await fs8.cp(src, root, { recursive: true, force: true, filter: (s) => path8.basename(s) !== "node_modules" && path8.basename(s) !== ".DS_Store" });
36788
- await fs8.rm(tmp, { recursive: true, force: true });
37121
+ await fs9.cp(src, root, { recursive: true, force: true, filter: (s) => path9.basename(s) !== "node_modules" && path9.basename(s) !== ".DS_Store" });
37122
+ await fs9.rm(tmp, { recursive: true, force: true });
36789
37123
  }
36790
37124
  async function prune(dir, depth) {
36791
- const entries = await fs8.readdir(dir, { withFileTypes: true });
37125
+ const entries = await fs9.readdir(dir, { withFileTypes: true });
36792
37126
  for (const e of entries) {
36793
- const p = path8.join(dir, e.name);
37127
+ const p = path9.join(dir, e.name);
36794
37128
  if (e.name === "node_modules") continue;
36795
37129
  if (depth === 3 && (e.name === ".sindri" || /^\.env(\..*)?$/.test(e.name))) continue;
36796
37130
  if (e.isDirectory()) {
36797
37131
  await prune(p, depth + 1);
36798
- const left = await fs8.readdir(p);
36799
- if (left.length === 0) await fs8.rmdir(p);
37132
+ const left = await fs9.readdir(p);
37133
+ if (left.length === 0) await fs9.rmdir(p);
36800
37134
  } else {
36801
- await fs8.rm(p, { force: true });
37135
+ await fs9.rm(p, { force: true });
36802
37136
  }
36803
37137
  }
36804
37138
  }
36805
37139
  async function singleRoot(dir) {
36806
- const entries = (await fs8.readdir(dir, { withFileTypes: true })).filter((e) => e.name !== ".DS_Store");
36807
- if (entries.length === 1 && entries[0]?.isDirectory() && !await exists2(path8.join(dir, "package.json"))) {
36808
- return path8.join(dir, entries[0].name);
37140
+ const entries = (await fs9.readdir(dir, { withFileTypes: true })).filter((e) => e.name !== ".DS_Store");
37141
+ if (entries.length === 1 && entries[0]?.isDirectory() && !await exists2(path9.join(dir, "package.json"))) {
37142
+ return path9.join(dir, entries[0].name);
36809
37143
  }
36810
37144
  return dir;
36811
37145
  }
36812
37146
  async function exists2(p) {
36813
37147
  try {
36814
- await fs8.access(p);
37148
+ await fs9.access(p);
36815
37149
  return true;
36816
37150
  } catch {
36817
37151
  return false;
@@ -36819,14 +37153,14 @@ async function exists2(p) {
36819
37153
  }
36820
37154
  async function readPublished(root) {
36821
37155
  try {
36822
- return JSON.parse(await fs8.readFile(path8.join(root, ".sindri", "last-published.json"), "utf8"));
37156
+ return JSON.parse(await fs9.readFile(path9.join(root, ".sindri", "last-published.json"), "utf8"));
36823
37157
  } catch {
36824
37158
  return null;
36825
37159
  }
36826
37160
  }
36827
37161
  async function writePublished(root, p) {
36828
- await fs8.mkdir(path8.join(root, ".sindri"), { recursive: true });
36829
- await fs8.writeFile(path8.join(root, ".sindri", "last-published.json"), JSON.stringify(p, null, 2) + "\n");
37162
+ await fs9.mkdir(path9.join(root, ".sindri"), { recursive: true });
37163
+ await fs9.writeFile(path9.join(root, ".sindri", "last-published.json"), JSON.stringify(p, null, 2) + "\n");
36830
37164
  }
36831
37165
 
36832
37166
  // src/tools.ts
@@ -36859,7 +37193,7 @@ var SCHEMAS = {
36859
37193
  preview: { app: appName },
36860
37194
  publish: {
36861
37195
  app: appName,
36862
- message: external_exports.string().min(3).max(MESSAGE_MAX).describe("What changed, in Spanish, as the PM would say it (becomes the version label).")
37196
+ message: external_exports.string().min(3).max(MESSAGE_MAX).describe("What changed, in Spanish, as the builder would say it (becomes the version label).")
36863
37197
  },
36864
37198
  status: { app: appName },
36865
37199
  list_apps: {},
@@ -36869,27 +37203,42 @@ var SCHEMAS = {
36869
37203
  rename_app: {
36870
37204
  app: appName,
36871
37205
  new_name: appName.optional().describe("New address: lowercase letters, digits and hyphens (2 to 31 chars). Becomes https://<new_name>.sindri.kigo.dev; the old address stops working."),
36872
- display_name: external_exports.string().min(2).max(60).optional().describe("New visible name (the title people see), in Spanish, as the PM wrote it. The address does not change.")
37206
+ 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.")
36873
37207
  },
36874
37208
  delete_app: {
36875
37209
  app: appName,
36876
- confirm: external_exports.string().describe("The app name exactly as the PM typed it to confirm. Must equal `app`; never fill it in for them.")
37210
+ 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.")
36877
37211
  },
36878
37212
  share: { app: appName },
36879
37213
  help: {},
36880
37214
  design_guide: {
36881
37215
  app: appName,
36882
37216
  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.")
37217
+ },
37218
+ set_secret: {
37219
+ app: appName,
37220
+ 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."),
37221
+ 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).")
37222
+ },
37223
+ list_secrets: { app: appName },
37224
+ delete_secret: {
37225
+ app: appName,
37226
+ name: external_exports.string().min(2).max(64).describe("The secret's name, as listed by list_secrets.")
37227
+ },
37228
+ run_task: {
37229
+ app: appName,
37230
+ task: external_exports.string().min(2).max(32).describe("Task name as declared in the `schedules` line of kigo-app.yaml."),
37231
+ 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.")
36883
37232
  }
36884
37233
  };
36885
37234
  var DESCRIPTIONS = {
36886
37235
  setup: {
36887
37236
  title: "Set up this computer",
36888
- 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."
37237
+ 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."
36889
37238
  },
36890
37239
  login: {
36891
37240
  title: "Sign in to Kigo",
36892
- 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."
37241
+ 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."
36893
37242
  },
36894
37243
  create_app: {
36895
37244
  title: "Create an app",
@@ -36897,7 +37246,7 @@ var DESCRIPTIONS = {
36897
37246
  },
36898
37247
  preview: {
36899
37248
  title: "Preview locally",
36900
- 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."
37249
+ 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."
36901
37250
  },
36902
37251
  publish: {
36903
37252
  title: "Publish to Sindri",
@@ -36907,7 +37256,7 @@ var DESCRIPTIONS = {
36907
37256
  title: "App status",
36908
37257
  description: "Address, last published version, whether the local folder exists and whether it has unpublished changes."
36909
37258
  },
36910
- list_apps: { title: "List apps", description: "The PM's apps in Sindri, with address, last version and whether each has a local folder." },
37259
+ list_apps: { title: "List apps", description: "The builder's apps in Sindri, with address, last version and whether each has a local folder." },
36911
37260
  versions: { title: "Versions", description: "Published versions of an app, most recent first (id, date, label)." },
36912
37261
  rollback: {
36913
37262
  title: "Go back to a version",
@@ -36915,24 +37264,40 @@ var DESCRIPTIONS = {
36915
37264
  },
36916
37265
  update_template: {
36917
37266
  title: "Update the template",
36918
- description: "Brings the app up to the current Kigo template: files the app never customised are replaced, files you changed are left alone and listed. Publishes a version \xABPlantilla actualizada\xBB and brings the local folder to it. Use it when the PM asks to update the template or Sindri announces a template fix. Refuses while there are unpublished local edits: publish (or discard) them first."
37267
+ 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."
36919
37268
  },
36920
37269
  rename_app: {
36921
37270
  title: "Rename an app",
36922
- description: 'Two different things, pass at least one. `new_name` changes the address (https://<new_name>.sindri.kigo.dev): Sindri renames it, the local folder is moved to match and the old address stops working; refuses while the folder has unpublished edits (publish first). `display_name` changes only the visible name: it is written to kigo-app.yaml and published as \xABAhora se llama \xAB<display_name>\xBB\xBB. When the PM says "c\xE1mbiale el nombre" and it is unclear which one they mean, ask: \xBFla direcci\xF3n o el nombre que se ve? Both together are allowed (visible name first, then the address).'
37271
+ 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).'
36923
37272
  },
36924
37273
  delete_app: {
36925
37274
  title: "Delete an app",
36926
- description: "Removes the app from Sindri (address, published versions and its database) after the PM typed its name: `confirm` must equal `app`, ask them to write it, never guess it. Stops the preview and moves the local folder to ~/Sindri/.papelera/<app>-<date> instead of deleting it, so the PM can still recover the files by hand."
37275
+ 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."
36927
37276
  },
36928
37277
  share: { title: "Share", description: "The line to paste in Slack: address, test-data notice and how the team enters." },
36929
37278
  help: {
36930
37279
  title: "What Sindri is",
36931
- description: "No network. The Spanish introduction to give the PM the first time they speak in a conversation or whenever they ask what Sindri can do: what it is, what they can ask for, the limits and the link to Mi espacio. Also says whether they are already signed in on this computer (`logged_in`) and where their apps live (`apps_dir`)."
37280
+ 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`)."
36932
37281
  },
36933
37282
  design_guide: {
36934
37283
  title: "Design guide",
36935
- description: "No network. Kigo's design guide for Sindri apps, read from the app folder (the kigo-sindri-design skill the template ships). Call it before creating or changing any page, component, layout, style or copy: first without `topic` (the overview plus the list of `topics`), then with the topic that matches what you are building (admin, landing, mini-app, mobile). `content` is markdown for you to follow, not to paste to the PM. If it answers `no_design_guide`, the app predates the guide: run update_template once and retry."
37284
+ 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."
37285
+ },
37286
+ set_secret: {
37287
+ title: "Save a secret",
37288
+ 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."
37289
+ },
37290
+ list_secrets: {
37291
+ title: "List secrets",
37292
+ 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."
37293
+ },
37294
+ delete_secret: {
37295
+ title: "Delete a secret",
37296
+ 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."
37297
+ },
37298
+ run_task: {
37299
+ title: "Run a scheduled task now",
37300
+ 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.'
36936
37301
  }
36937
37302
  };
36938
37303
  function createTools(deps) {
@@ -36958,7 +37323,15 @@ function createTools(deps) {
36958
37323
  };
36959
37324
  }
36960
37325
  const setup = guard(async () => {
36961
- const report = await setupToolchain(cfg, { env, ...deps.installer ? { installer: deps.installer } : {} });
37326
+ let report;
37327
+ try {
37328
+ report = await setupToolchain(cfg, { env, ...deps.installer ? { installer: deps.installer } : {} });
37329
+ } catch (err) {
37330
+ const msg = err instanceof Error ? err.message : String(err);
37331
+ process.stderr.write(`sindri: ${msg}
37332
+ `);
37333
+ return { ok: false, summary: "No pude instalar Node y pnpm en tu computadora. Escribe en Slack, canal #sindri, y pega el detalle.", detail: msg.slice(0, 400) };
37334
+ }
36962
37335
  const what = report.installed.length ? `Instal\xE9 ${report.installed.join(" y ")}.` : "Tu computadora ya ten\xEDa lo necesario.";
36963
37336
  return {
36964
37337
  ok: true,
@@ -37027,14 +37400,14 @@ function createTools(deps) {
37027
37400
  }
37028
37401
  const created = await api.createApp(a.name, a.description.trim());
37029
37402
  const archive = await api.download(created.archive_url);
37030
- await fs9.mkdir(p.root, { recursive: true });
37403
+ await fs10.mkdir(p.root, { recursive: true });
37031
37404
  await replaceTree(p.root, archive);
37032
- await fs9.rm(path9.dirname(archive), { recursive: true, force: true });
37405
+ await fs10.rm(path10.dirname(archive), { recursive: true, force: true });
37033
37406
  p = await appPaths(cfg, a.name, me.owner);
37034
37407
  if (!await appPresent(p)) {
37035
37408
  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 };
37036
37409
  }
37037
- await fs9.writeFile(path9.join(p.appDir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
37410
+ await ensureLocalEnv(p.appDir, a.name);
37038
37411
  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 });
37039
37412
  if (install.code !== 0) {
37040
37413
  const why = lastLine(install.stderr || install.stdout);
@@ -37064,7 +37437,7 @@ function createTools(deps) {
37064
37437
  }
37065
37438
  const tc = await toolchain();
37066
37439
  if (!tc) return NEEDS_SETUP;
37067
- if (!await exists2(path9.join(p.root, "node_modules"))) {
37440
+ if (!await exists2(path10.join(p.root, "node_modules"))) {
37068
37441
  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 });
37069
37442
  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) };
37070
37443
  }
@@ -37105,14 +37478,14 @@ function createTools(deps) {
37105
37478
  }
37106
37479
  const packed = await packTree(dir);
37107
37480
  if (packed.bytes > 20 * 1024 * 1024) {
37108
- await fs9.rm(path9.dirname(packed.file), { recursive: true, force: true });
37481
+ await fs10.rm(path10.dirname(packed.file), { recursive: true, force: true });
37109
37482
  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 };
37110
37483
  }
37111
37484
  let result;
37112
37485
  try {
37113
37486
  result = await api.publish(a.app, packed.file, message);
37114
37487
  } finally {
37115
- await fs9.rm(path9.dirname(packed.file), { recursive: true, force: true });
37488
+ await fs10.rm(path10.dirname(packed.file), { recursive: true, force: true });
37116
37489
  }
37117
37490
  await writePublished(dir, { version: result.version, tree_hash: await treeHash(dir), url: result.url });
37118
37491
  return {
@@ -37131,6 +37504,8 @@ function createTools(deps) {
37131
37504
  if (!local.exists) parts.push("En esta computadora no est\xE1 su carpeta.");
37132
37505
  else if (local.unpublished) parts.push("Tienes cambios en tu computadora que a\xFAn no publicas.");
37133
37506
  else parts.push("Tu carpeta est\xE1 al d\xEDa con lo publicado.");
37507
+ const tasks = describeTasks(info);
37508
+ if (tasks) parts.push(tasks);
37134
37509
  return { ok: true, summary: parts.join(" "), app: info, local };
37135
37510
  });
37136
37511
  const list_apps = guard(async () => {
@@ -37154,16 +37529,16 @@ function createTools(deps) {
37154
37529
  let p = await appPaths(cfg, a.app);
37155
37530
  await stopPreview(cfg, a.app);
37156
37531
  const archive = await api.download(`/v1/apps/${encodeURIComponent(a.app)}/archive`, result.version.id);
37157
- await fs9.mkdir(p.root, { recursive: true });
37532
+ await fs10.mkdir(p.root, { recursive: true });
37158
37533
  await replaceTree(p.root, archive);
37159
- await fs9.rm(path9.dirname(archive), { recursive: true, force: true });
37534
+ await fs10.rm(path10.dirname(archive), { recursive: true, force: true });
37160
37535
  p = await appPaths(cfg, a.app, p.owner || void 0);
37161
37536
  const dir = p.appDir;
37162
37537
  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 };
37163
- if (!await exists2(path9.join(dir, ".env.local"))) await fs9.writeFile(path9.join(dir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
37538
+ await ensureLocalEnv(dir, a.app);
37164
37539
  await linkDesignGuide(p);
37165
37540
  const tc = await toolchain();
37166
- if (tc && await exists2(path9.join(p.root, "node_modules"))) {
37541
+ if (tc && await exists2(path10.join(p.root, "node_modules"))) {
37167
37542
  const db = await prepareLocalDb(tc, dir);
37168
37543
  if (!db.ok) process.stderr.write(`sindri: local db after rollback: ${db.detail}
37169
37544
  `);
@@ -37193,14 +37568,14 @@ function createTools(deps) {
37193
37568
  await stopPreview(cfg, a.app);
37194
37569
  const archive = await api.download(`/v1/apps/${encodeURIComponent(a.app)}/archive`, result.version?.id);
37195
37570
  await replaceTree(p.root, archive);
37196
- await fs9.rm(path9.dirname(archive), { recursive: true, force: true });
37571
+ await fs10.rm(path10.dirname(archive), { recursive: true, force: true });
37197
37572
  p = await appPaths(cfg, a.app, p.owner || void 0);
37198
37573
  const dir = p.appDir;
37199
37574
  if (!await appPresent(p)) return { ok: false, summary: `Sindri actualiz\xF3 la plantilla pero lo que baj\xF3 no trae la app donde la espero. Escribe en Slack, canal #sindri.`, ...data };
37200
- if (!await exists2(path9.join(dir, ".env.local"))) await fs9.writeFile(path9.join(dir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
37575
+ await ensureLocalEnv(dir, a.app);
37201
37576
  await linkDesignGuide(p);
37202
37577
  const tc = await toolchain();
37203
- if (tc && await exists2(path9.join(p.root, "node_modules"))) {
37578
+ if (tc && await exists2(path10.join(p.root, "node_modules"))) {
37204
37579
  const db = await prepareLocalDb(tc, dir);
37205
37580
  if (!db.ok) process.stderr.write(`sindri: local db after update_template: ${db.detail}
37206
37581
  `);
@@ -37252,9 +37627,10 @@ function createTools(deps) {
37252
37627
  await stopPreview(cfg, a.app);
37253
37628
  const newRoot = appRoot(cfg, newName);
37254
37629
  await moveDir(p.root, newRoot);
37255
- const newDir = path9.join(newRoot, "apps", p.owner, newName);
37256
- await moveDir(path9.join(newRoot, "apps", p.owner, a.app), newDir);
37630
+ const newDir = path10.join(newRoot, "apps", p.owner, newName);
37631
+ await moveDir(path10.join(newRoot, "apps", p.owner, a.app), newDir);
37257
37632
  await writeManifestValue(newDir, "name", newName);
37633
+ await writeEnvFile(newDir, upsertEnv(await readEnvFile(newDir), "KIGO_APP_NAME", newName));
37258
37634
  await writePublished(newDir, { version: result.version ?? before.published_version, tree_hash: await treeHash(newDir), url: result.url });
37259
37635
  await linkDesignGuide({ root: newRoot, appDir: newDir, owner: p.owner });
37260
37636
  data.path = newDir;
@@ -37281,7 +37657,7 @@ function createTools(deps) {
37281
37657
  }
37282
37658
  let trash = null;
37283
37659
  if (present) {
37284
- trash = path9.join(cfg.home, ".papelera", `${a.app}-${stamp()}`);
37660
+ trash = path10.join(cfg.home, ".papelera", `${a.app}-${stamp()}`);
37285
37661
  await moveDir(root, trash);
37286
37662
  }
37287
37663
  const kept = "Una copia de sus archivos queda en tu computadora, en la papelera de Sindri; puedes borrarla a mano cuando quieras.";
@@ -37324,21 +37700,147 @@ function createTools(deps) {
37324
37700
  content: guide.content
37325
37701
  };
37326
37702
  });
37703
+ async function restartPreview(app, dir) {
37704
+ if (!await runningPreview(cfg, app, fetchImpl)) return false;
37705
+ const tc = await toolchain();
37706
+ if (!tc) return false;
37707
+ await stopPreview(cfg, app);
37708
+ const r = await startPreview(cfg, tc, app, dir, { fetchImpl, ...deps.sleepImpl ? { sleepImpl: deps.sleepImpl } : {} });
37709
+ return r.log === void 0;
37710
+ }
37711
+ function checkSecretName(name) {
37712
+ if (!validSecretName(name)) {
37713
+ 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" };
37714
+ }
37715
+ 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" };
37716
+ return null;
37717
+ }
37718
+ const set_secret = guard(async (a) => {
37719
+ if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
37720
+ const name = a.name.trim();
37721
+ const bad = checkSecretName(name);
37722
+ if (bad) return bad;
37723
+ await api.app(a.app);
37724
+ const p = await appPaths(cfg, a.app);
37725
+ const local = await appPresent(p);
37726
+ const openUrl = deps.openUrl ?? defaultOpenUrl(cfg);
37727
+ let restarted = false;
37728
+ try {
37729
+ await collectSecret({
37730
+ app: a.app,
37731
+ name,
37732
+ ...a.hint ? { hint: a.hint.trim() } : {},
37733
+ openUrl,
37734
+ ...deps.secretTimeoutMs ? { timeoutMs: deps.secretTimeoutMs } : {},
37735
+ onValue: async (value) => {
37736
+ if (local) await writeEnvFile(p.appDir, upsertEnv(await readEnvFile(p.appDir), name, value));
37737
+ restarted = (await api.putSecret(a.app, name, value)).restarted;
37738
+ }
37739
+ });
37740
+ } catch (err) {
37741
+ 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" };
37742
+ if (err instanceof Error && err.message === "expired") {
37743
+ 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 };
37744
+ }
37745
+ throw err;
37746
+ }
37747
+ const previewRestarted = local ? await restartPreview(a.app, p.appDir) : false;
37748
+ const where = local ? "en tu computadora y en Sindri" : "en Sindri (en esta computadora no est\xE1 la carpeta de la app)";
37749
+ const applied = previewRestarted ? " Reinici\xE9 la vista previa para que lo tome." : "";
37750
+ return { ok: true, summary: `Listo, ${name} qued\xF3 guardado ${where}.${applied}`, name, restarted, preview_restarted: previewRestarted, local };
37751
+ });
37752
+ const list_secrets = guard(async (a) => {
37753
+ if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
37754
+ const names = (await api.listSecrets(a.app)).sort();
37755
+ const p = await appPaths(cfg, a.app);
37756
+ const localNames = await appPresent(p) ? secretKeys(await readEnvFile(p.appDir)).sort() : [];
37757
+ const missingLocal = names.filter((n) => !localNames.includes(n));
37758
+ const missingInSindri = localNames.filter((n) => !names.includes(n));
37759
+ 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(", ")}.`];
37760
+ if (missingLocal.length) parts.push(`En tu computadora ${missingLocal.length === 1 ? "falta" : "faltan"} ${missingLocal.join(", ")}.`);
37761
+ if (missingInSindri.length) parts.push(`En Sindri ${missingInSindri.length === 1 ? "falta" : "faltan"} ${missingInSindri.join(", ")} (solo est\xE1 en tu computadora).`);
37762
+ return { ok: true, summary: parts.join(" "), names, local_names: localNames, missing_local: missingLocal, missing_in_sindri: missingInSindri };
37763
+ });
37764
+ const delete_secret = guard(async (a) => {
37765
+ if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
37766
+ const name = a.name.trim();
37767
+ const bad = checkSecretName(name);
37768
+ if (bad) return bad;
37769
+ let inSindri = true;
37770
+ try {
37771
+ await api.deleteSecret(a.app, name);
37772
+ } catch (err) {
37773
+ if (err instanceof ApiError && err.status === 404) inSindri = false;
37774
+ else throw err;
37775
+ }
37776
+ const p = await appPaths(cfg, a.app);
37777
+ let hadLocal = false;
37778
+ if (await appPresent(p)) {
37779
+ const text = await readEnvFile(p.appDir);
37780
+ hadLocal = envValue(text, name) !== void 0;
37781
+ if (hadLocal) await writeEnvFile(p.appDir, removeEnv(text, name));
37782
+ }
37783
+ const previewRestarted = hadLocal ? await restartPreview(a.app, p.appDir) : false;
37784
+ const where = inSindri && hadLocal ? "de Sindri y de tu computadora" : inSindri ? "de Sindri" : hadLocal ? "de tu computadora (en Sindri no estaba)" : "";
37785
+ const summary = where ? `Borr\xE9 ${name} ${where}.${previewRestarted ? " Reinici\xE9 la vista previa." : ""}` : `${name} no estaba guardado ni en Sindri ni en tu computadora.`;
37786
+ return { ok: true, summary, name, removed_from_sindri: inSindri, removed_locally: hadLocal, preview_restarted: previewRestarted };
37787
+ });
37788
+ const run_task = guard(async (a) => {
37789
+ if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
37790
+ const task = a.task.trim();
37791
+ const p = await appPaths(cfg, a.app);
37792
+ const running = await runningPreview(cfg, a.app, fetchImpl);
37793
+ const where = a.where ?? (running ? "local" : "sindri");
37794
+ if (where === "sindri") {
37795
+ const r2 = await api.runTask(a.app, task);
37796
+ 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 };
37797
+ }
37798
+ 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" };
37799
+ const schedules = readSchedules(await readManifest(p.appDir));
37800
+ if (!schedules.ok) return { ok: false, summary: schedules.error, code: "bad_schedules" };
37801
+ const declared = schedules.tasks.find((t) => t.name === task);
37802
+ if (!declared) {
37803
+ const names = schedules.tasks.map((t) => t.name);
37804
+ 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 };
37805
+ }
37806
+ const ensured = await ensureLocalEnv(p.appDir, a.app);
37807
+ 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" };
37808
+ if (ensured.created_token) await restartPreview(a.app, p.appDir);
37809
+ const token = envValue(await readEnvFile(p.appDir), "SINDRI_TASK_TOKEN") ?? "";
37810
+ const port = (await runningPreview(cfg, a.app, fetchImpl))?.port ?? running.port;
37811
+ const started = Date.now();
37812
+ let res = null;
37813
+ let timedOut = false;
37814
+ try {
37815
+ res = await fetchImpl(`http://127.0.0.1:${port}${declared.path}`, {
37816
+ method: "POST",
37817
+ headers: { "X-Sindri-Task": task, Authorization: `Bearer ${token}`, Accept: "application/json" },
37818
+ signal: AbortSignal.timeout(LOCAL_TASK_TIMEOUT_MS)
37819
+ });
37820
+ } catch (err) {
37821
+ timedOut = err instanceof Error && err.name === "TimeoutError";
37822
+ if (!timedOut) throw err;
37823
+ }
37824
+ const duration_ms = Date.now() - started;
37825
+ const body = res ? await res.text().catch(() => "") : "";
37826
+ 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) };
37827
+ 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 };
37828
+ });
37327
37829
  const logout = guard(async () => {
37328
37830
  await clearCredentials(cfg);
37329
37831
  return { ok: true, summary: "Saliste de tu cuenta de Kigo en esta computadora." };
37330
37832
  });
37331
- return { setup, login, create_app, preview, publish, status, list_apps, versions, rollback, update_template, rename_app, delete_app, share, help, design_guide, logout };
37833
+ 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 };
37332
37834
  }
37333
37835
  var HELP_SUMMARY = "Sindri te ayuda a crear apps internas peque\xF1as para tu equipo en minutos, con datos de prueba y sin escribir c\xF3digo: t\xFA me cuentas qu\xE9 necesitas y yo la construyo. Puedes pedirme crear una app, verla en tu computadora antes de publicarla, publicarla, cambiarle el nombre o la direcci\xF3n, volver a una versi\xF3n anterior, compartirla en Slack, actualizar su plantilla o borrarla. Ten en cuenta: las apps trabajan con datos sint\xE9ticos, nunca reales; funcionan de lunes a viernes de 9:05 a 24:00, hora de la Ciudad de M\xE9xico; cada app la edita solo quien la cre\xF3; y lo que deba llegar a toda la empresa se pide a desarrollo. Tus apps y sus versiones est\xE1n en https://sindri.kigo.dev.";
37334
37836
  async function moveDir(from, to) {
37335
- await fs9.mkdir(path9.dirname(to), { recursive: true });
37837
+ await fs10.mkdir(path10.dirname(to), { recursive: true });
37336
37838
  try {
37337
- await fs9.rename(from, to);
37839
+ await fs10.rename(from, to);
37338
37840
  } catch (err) {
37339
37841
  if (err.code !== "EXDEV") throw err;
37340
- await fs9.cp(from, to, { recursive: true });
37341
- await fs9.rm(from, { recursive: true, force: true });
37842
+ await fs10.cp(from, to, { recursive: true });
37843
+ await fs10.rm(from, { recursive: true, force: true });
37342
37844
  }
37343
37845
  }
37344
37846
  function stamp(d = /* @__PURE__ */ new Date()) {
@@ -37357,8 +37859,8 @@ function describeTemplateFiles(r) {
37357
37859
  return parts.length ? ` ${parts.join(" ")}` : "";
37358
37860
  }
37359
37861
  async function prepareLocalDb(tc, dir, opts = {}) {
37360
- const env = { ...tc.env, KIGO_ENV: "local", DATABASE_URL: "", CI: "1", FORCE_COLOR: "0" };
37361
- const fresh = !await exists2(path9.join(dir, ".sindri", "pglite"));
37862
+ const env = { ...tc.env, KIGO_ENV: "local", KIGO_APP_NAME: path10.basename(dir), DATABASE_URL: "", CI: "1", FORCE_COLOR: "0" };
37863
+ const fresh = !await exists2(path10.join(dir, ".sindri", "pglite"));
37362
37864
  const migrate = await run(tc.pnpm, [...PNPM_ARGS, "db:migrate"], { cwd: dir, env, timeoutMs: 18e4 });
37363
37865
  if (migrate.code !== 0) return { ok: false, detail: lastLine(migrate.stderr || migrate.stdout) };
37364
37866
  if (opts.seed ?? fresh) {
@@ -37375,6 +37877,23 @@ async function localState(cfg, name) {
37375
37877
  const hash2 = await treeHash(dir);
37376
37878
  return { exists: true, path: dir, unpublished: !published || published.tree_hash !== hash2, published_version: published?.version ?? null };
37377
37879
  }
37880
+ var LOCAL_TASK_TIMEOUT_MS = 5 * 6e4;
37881
+ function describeTasks(info) {
37882
+ const tasks = info.tasks ?? [];
37883
+ if (tasks.length === 0) return "";
37884
+ const one = (t) => {
37885
+ 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";
37886
+ return `${t.name} cada ${t.every} (${last})`;
37887
+ };
37888
+ return `${tasks.length === 1 ? "Tarea" : "Tareas"}: ${tasks.map(one).join("; ")}.`;
37889
+ }
37890
+ var STATUS_WORD = { ok: "bien", error: "fall\xF3", timeout: "se pas\xF3 de tiempo" };
37891
+ function describeRun(task, where, r) {
37892
+ const secs = (r.duration_ms / 1e3).toFixed(r.duration_ms < 1e4 ? 1 : 0).replace(".", ",");
37893
+ if (r.status === "ok") return `La tarea \xAB${task}\xBB corri\xF3 ${where} en ${secs} s y termin\xF3 bien.`;
37894
+ 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.`;
37895
+ 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.`;
37896
+ }
37378
37897
  function describeVersion(info) {
37379
37898
  if (!info.last_version) return `"${info.name}" a\xFAn no tiene versiones publicadas; su direcci\xF3n ser\xE1 ${info.url}.`;
37380
37899
  return `"${info.name}" est\xE1 en ${info.url}; \xFAltima versi\xF3n ${when(info.last_version.at)}, \xAB${info.last_version.label}\xBB.`;
@@ -37390,7 +37909,7 @@ function when(iso, now = /* @__PURE__ */ new Date()) {
37390
37909
  }
37391
37910
 
37392
37911
  // src/index.ts
37393
- var VERSION = "0.1.4";
37912
+ var VERSION = "0.1.6";
37394
37913
  function registerTools(server, tools) {
37395
37914
  for (const name of Object.keys(SCHEMAS)) {
37396
37915
  const meta3 = DESCRIPTIONS[name];