@kigo-dev/sindri 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,10 +22,16 @@ names, parameters and this document are English. The contract lives in
22
22
  | `list_apps` | – | The PM's apps, with local folder state. |
23
23
  | `versions` | `app` | Published versions, most recent first. |
24
24
  | `rollback` | `app`, `version` | `POST rollback`, then downloads that version's archive and replaces the local tree (keeps `node_modules`, `.sindri`, `.env.local`). Stops the preview first: PGlite allows one process per data dir. |
25
+ | `update_template` | `app` | `POST /v1/apps/<app>/template`: Sindri replaces the files the app never customised with the current template (customised ones come back in `skipped`) and publishes «Plantilla actualizada». When `published`, downloads the new archive and replaces the local tree like `rollback` (stops the preview, re-migrates, rewrites `last-published.json`). Refuses with "Publica o descarta tus cambios antes de actualizar la plantilla" while the folder has unpublished edits. |
26
+ | `rename_app` | `app`, `new_name?`, `display_name?` | At least one. `display_name`: writes `display_name:` in the app's `kigo-app.yaml` (line edit, no YAML library) and runs the `publish` flow with the message «Ahora se llama «<display_name>»». `new_name`: refuses with "Publica tus cambios antes de cambiar la dirección" while the folder has unpublished edits (same check as `status`), `POST /v1/apps/<app>/rename`, then stops the preview, moves `~/Sindri/<app>` to `~/Sindri/<new_name>` and `apps/<owner>/<app>` to `apps/<owner>/<new_name>`, rewrites `name:` in `kigo-app.yaml` exactly like the builder and rewrites `.sindri/last-published.json` so `status` is clean. Both together: visible name first (a publish under the old address), then the address. |
27
+ | `delete_app` | `app`, `confirm` | `confirm` must equal `app` (the PM typed it; the instructions forbid Claude from filling it in). Stops the preview, `DELETE /v1/apps/<app>`, then moves `~/Sindri/<app>` to `~/Sindri/.papelera/<app>-<YYYYMMDD-HHmm>` (Mexico City time) instead of deleting. A 404 from the API with the folder present still moves the folder and says so. |
25
28
  | `share` | `app` | The Slack line. |
29
+ | `help` | – | No network. `{ summary, space_url, logged_in, apps_dir }`: the Spanish introduction Claude gives on first contact (what Sindri is, what to ask for, limits, the Mi espacio link https://sindri.kigo.dev), whether credentials are stored on this computer (keychain or file, no API call) and where the apps live. |
26
30
 
27
31
  Every result is `{ ok, summary, ...data }`. `needs_login: true` / `needs_setup: true` tell Claude
28
- which tool to run first. The server also ships `instructions` describing how to talk to the PM.
32
+ which tool to run first. `list_apps` also returns `space_url`. The server also ships
33
+ `instructions` describing how to talk to the PM (first contact via `help`, address vs visible
34
+ name, typed confirmation before `delete_app`).
29
35
 
30
36
  ## Install
31
37
 
@@ -36,7 +42,7 @@ which tool to run first. The server also ships `instructions` describing how to
36
42
  **Claude Code:**
37
43
 
38
44
  ```
39
- claude mcp add sindri -- npx -y @kigo-dev/sindri
45
+ claude mcp add sindri -- npx -y @kigo-dev/sindri@0.1.3
40
46
  ```
41
47
 
42
48
  **Manual check without an MCP client:**
@@ -82,12 +88,22 @@ archive ships it (`package.json`, `pnpm-workspace.yaml`, `pnpm-lock.yaml`, `tsco
82
88
  and cannot install standalone. `pnpm install` runs at that root; `preview`, `db:migrate`,
83
89
  `db:seed`, `tsc`, the tree hash, `.env.local`, `.sindri/last-published.json` and the `publish`
84
90
  tarball (paths relative to the app) all live at `apps/<owner>/<name>`. `replaceTree` swaps the
85
- whole root on `create_app`/`rollback` but keeps every `node_modules` and the app's `.sindri/`
91
+ whole root on `create_app`/`rollback`/`update_template` but keeps every `node_modules` and the app's `.sindri/`
86
92
  and `.env*`. The owner is the single folder under `apps/`, else the `owner` from `GET /v1/me`
87
93
  (cached in `~/.sindri/me.json`).
88
94
 
95
+ `rename_app` with `new_name` moves the root and the app dir in place (nothing is downloaded) and
96
+ then applies the one edit the builder made in its copy, `name:` in `kigo-app.yaml`, written as
97
+ `name: "<new_name>"` (JSON-quoted, the shape `mcp` `renderManifest` uses). The tree hash the
98
+ connector stores in `last-published.json` is over the same file set `publish` uploads (everything
99
+ except `node_modules`, `.next`, `.sindri`, `dist`, `.turbo`, `.git`, `.env*`, `.DS_Store`), so a
100
+ file the builder ignores but the connector hashes would make `status` report unpublished edits
101
+ after a rename; `test/tools.test.ts` builds the builder's post-rename tree (old archive, moved,
102
+ `name:` edited) and asserts both hashes are equal. Deleted apps are never removed from disk: they
103
+ go to `~/Sindri/.papelera/`, which no tool scans (`.papelera` is not a valid app name).
104
+
89
105
  ## Local database
90
106
 
91
107
  The template runs on PGlite when `KIGO_ENV=local` and no `DATABASE_URL` is set; data lives in
92
108
  `<app>/.sindri/pglite/`. PGlite is single-process: the connector never runs migrations while a
93
- preview is up, and stops the preview before a rollback replaces the tree.
109
+ preview is up, and stops the preview before a rollback or a template update replaces the tree.
package/dist/index.js CHANGED
@@ -3262,8 +3262,8 @@ var require_utils = __commonJS({
3262
3262
  }
3263
3263
  return ind;
3264
3264
  }
3265
- function removeDotSegments(path9) {
3266
- let input2 = path9;
3265
+ function removeDotSegments(path10) {
3266
+ let input2 = path10;
3267
3267
  const output2 = [];
3268
3268
  let nextSlash = -1;
3269
3269
  let len = 0;
@@ -3672,8 +3672,8 @@ var require_schemes = __commonJS({
3672
3672
  }
3673
3673
  if (wsComponent.resourceName) {
3674
3674
  const queryIndex = wsComponent.resourceName.indexOf("?");
3675
- const path9 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
3676
- wsComponent.path = path9 && path9 !== "/" ? path9 : void 0;
3675
+ const path10 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
3676
+ wsComponent.path = path10 && path10 !== "/" ? path10 : void 0;
3677
3677
  wsComponent.query = queryIndex === -1 ? void 0 : wsComponent.resourceName.slice(queryIndex + 1);
3678
3678
  wsComponent.resourceName = void 0;
3679
3679
  }
@@ -7185,12 +7185,12 @@ var require_dist = __commonJS({
7185
7185
  throw new Error(`Unknown format "${name}"`);
7186
7186
  return f;
7187
7187
  };
7188
- function addFormats(ajv, list, fs9, exportName) {
7188
+ function addFormats(ajv, list, fs10, exportName) {
7189
7189
  var _a3;
7190
7190
  var _b;
7191
7191
  (_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
7192
7192
  for (const f of list)
7193
- ajv.addFormat(f, fs9[f]);
7193
+ ajv.addFormat(f, fs10[f]);
7194
7194
  }
7195
7195
  module.exports = exports = formatsPlugin;
7196
7196
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -7572,8 +7572,8 @@ function getErrorMap() {
7572
7572
 
7573
7573
  // node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/helpers/parseUtil.js
7574
7574
  var makeIssue = (params) => {
7575
- const { data, path: path9, errorMaps, issueData } = params;
7576
- const fullPath = [...path9, ...issueData.path || []];
7575
+ const { data, path: path10, errorMaps, issueData } = params;
7576
+ const fullPath = [...path10, ...issueData.path || []];
7577
7577
  const fullIssue = {
7578
7578
  ...issueData,
7579
7579
  path: fullPath
@@ -7688,11 +7688,11 @@ var errorUtil;
7688
7688
 
7689
7689
  // node_modules/.pnpm/zod@4.5.4/node_modules/zod/v3/types.js
7690
7690
  var ParseInputLazyPath = class {
7691
- constructor(parent, value, path9, key) {
7691
+ constructor(parent, value, path10, key) {
7692
7692
  this._cachedPath = [];
7693
7693
  this.parent = parent;
7694
7694
  this.data = value;
7695
- this._path = path9;
7695
+ this._path = path10;
7696
7696
  this._key = key;
7697
7697
  }
7698
7698
  get path() {
@@ -11580,10 +11580,10 @@ function mergeDefs(...defs) {
11580
11580
  function cloneDef(schema) {
11581
11581
  return mergeDefs(schema._zod.def);
11582
11582
  }
11583
- function getElementAtPath(obj, path9) {
11584
- if (!path9)
11583
+ function getElementAtPath(obj, path10) {
11584
+ if (!path10)
11585
11585
  return obj;
11586
- return path9.reduce((acc, key) => acc?.[key], obj);
11586
+ return path10.reduce((acc, key) => acc?.[key], obj);
11587
11587
  }
11588
11588
  function promiseAllObject(promisesObj) {
11589
11589
  const keys = Object.keys(promisesObj);
@@ -11995,11 +11995,11 @@ function explicitlyAborted(x, startIndex = 0) {
11995
11995
  }
11996
11996
  return false;
11997
11997
  }
11998
- function prefixIssues(path9, issues) {
11998
+ function prefixIssues(path10, issues) {
11999
11999
  return issues.map((iss) => {
12000
12000
  var _a3;
12001
12001
  (_a3 = iss).path ?? (_a3.path = []);
12002
- iss.path.unshift(path9);
12002
+ iss.path.unshift(path10);
12003
12003
  return iss;
12004
12004
  });
12005
12005
  }
@@ -12432,16 +12432,16 @@ function flattenError(error61, mapper = (issue2) => issue2.message) {
12432
12432
  }
12433
12433
  function formatError(error61, mapper = (issue2) => issue2.message) {
12434
12434
  const fieldErrors = { _errors: [] };
12435
- const processError = (error62, path9 = []) => {
12435
+ const processError = (error62, path10 = []) => {
12436
12436
  for (const issue2 of error62.issues) {
12437
12437
  if (issue2.code === "invalid_union" && issue2.errors.length) {
12438
- issue2.errors.map((issues) => processError({ issues }, [...path9, ...issue2.path]));
12438
+ issue2.errors.map((issues) => processError({ issues }, [...path10, ...issue2.path]));
12439
12439
  } else if (issue2.code === "invalid_key") {
12440
- processError({ issues: issue2.issues }, [...path9, ...issue2.path]);
12440
+ processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
12441
12441
  } else if (issue2.code === "invalid_element") {
12442
- processError({ issues: issue2.issues }, [...path9, ...issue2.path]);
12442
+ processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
12443
12443
  } else {
12444
- const fullpath = [...path9, ...issue2.path];
12444
+ const fullpath = [...path10, ...issue2.path];
12445
12445
  if (fullpath.length === 0) {
12446
12446
  fieldErrors._errors.push(mapper(issue2));
12447
12447
  } else {
@@ -12480,17 +12480,17 @@ function formatError(error61, mapper = (issue2) => issue2.message) {
12480
12480
  }
12481
12481
  function treeifyError(error61, mapper = (issue2) => issue2.message) {
12482
12482
  const result = { errors: [] };
12483
- const processError = (error62, path9 = []) => {
12483
+ const processError = (error62, path10 = []) => {
12484
12484
  var _a3;
12485
12485
  for (const issue2 of error62.issues) {
12486
12486
  if (issue2.code === "invalid_union" && issue2.errors.length) {
12487
- issue2.errors.map((issues) => processError({ issues }, [...path9, ...issue2.path]));
12487
+ issue2.errors.map((issues) => processError({ issues }, [...path10, ...issue2.path]));
12488
12488
  } else if (issue2.code === "invalid_key") {
12489
- processError({ issues: issue2.issues }, [...path9, ...issue2.path]);
12489
+ processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
12490
12490
  } else if (issue2.code === "invalid_element") {
12491
- processError({ issues: issue2.issues }, [...path9, ...issue2.path]);
12491
+ processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
12492
12492
  } else {
12493
- const fullpath = [...path9, ...issue2.path];
12493
+ const fullpath = [...path10, ...issue2.path];
12494
12494
  if (fullpath.length === 0) {
12495
12495
  result.errors.push(mapper(issue2));
12496
12496
  continue;
@@ -12529,8 +12529,8 @@ function treeifyError(error61, mapper = (issue2) => issue2.message) {
12529
12529
  }
12530
12530
  function toDotPath(_path) {
12531
12531
  const segs = [];
12532
- const path9 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
12533
- for (const seg of path9) {
12532
+ const path10 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
12533
+ for (const seg of path10) {
12534
12534
  if (typeof seg === "number")
12535
12535
  segs.push(`[${seg}]`);
12536
12536
  else if (typeof seg === "symbol")
@@ -27511,11 +27511,11 @@ function normalizeObjectSchema(schema) {
27511
27511
  }
27512
27512
  return void 0;
27513
27513
  }
27514
- function getDotPath(path9) {
27515
- if (path9.length === 0) {
27514
+ function getDotPath(path10) {
27515
+ if (path10.length === 0) {
27516
27516
  return "object root";
27517
27517
  }
27518
- return path9.reduce((acc, seg, index) => {
27518
+ return path10.reduce((acc, seg, index) => {
27519
27519
  if (index === 0) {
27520
27520
  return String(seg);
27521
27521
  }
@@ -29678,13 +29678,13 @@ function resolveRef(ref, ctx) {
29678
29678
  if (!ref.startsWith("#")) {
29679
29679
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
29680
29680
  }
29681
- const path9 = ref.slice(1).split("/").filter(Boolean);
29682
- if (path9.length === 0) {
29681
+ const path10 = ref.slice(1).split("/").filter(Boolean);
29682
+ if (path10.length === 0) {
29683
29683
  return ctx.rootSchema;
29684
29684
  }
29685
29685
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
29686
- if (path9[0] === defsKey) {
29687
- const key = path9[1] === void 0 ? void 0 : decodeJSONPointerSegment(path9[1]);
29686
+ if (path10[0] === defsKey) {
29687
+ const key = path10[1] === void 0 ? void 0 : decodeJSONPointerSegment(path10[1]);
29688
29688
  if (!key || !ctx.defs[key]) {
29689
29689
  throw new Error(`Reference not found: ${ref}`);
29690
29690
  }
@@ -35677,6 +35677,7 @@ var StdioServerTransport = class {
35677
35677
  // src/config.ts
35678
35678
  import os from "node:os";
35679
35679
  import path from "node:path";
35680
+ var SPACE_URL = "https://sindri.kigo.dev";
35680
35681
  var DEFAULTS = {
35681
35682
  apiUrl: "https://api.sindri.kigo.dev",
35682
35683
  issuer: "https://id.kigo.dev",
@@ -35719,18 +35720,23 @@ var INSTRUCTIONS = `You are helping a Kigo product manager (PM) build small inte
35719
35720
 
35720
35721
  Language and tone: always Spanish, "t\xFA", short sentences. Say "app" (feminine: "la app", "tu app"), never "herramienta", "proyecto", "repo", "deploy" or "build". Never paste file paths, commands, logs or stack traces; summarize in one line what happened and what you will do. The \`summary\` field of every tool result is written for the PM: repeat it as is or paraphrase it.
35721
35722
 
35723
+ First contact: the first time the PM speaks in a conversation, or whenever they ask what Sindri is or can do, call \`help\` and relay its \`summary\` (that is the introduction: what Sindri is, what they can ask for, the limits, the link to Mi espacio), then ask what they want to build. Do not repeat it later in the same conversation. When the PM asks where to see their apps or their versions, give them the Mi espacio link (\`space_url\`, https://sindri.kigo.dev).
35724
+
35722
35725
  Flow:
35723
35726
  1. First time on a machine: run \`setup\` (installs Node and pnpm without admin rights) and \`login\` (opens the browser on Kigo's sign-in page; the PM enters with their @cargomovil.com Google account and the browser comes back on its own with "Listo, ya entraste a Sindri"; tell them to return to Claude). \`login\` waits up to five minutes. If any tool answers \`needs_setup\` or \`needs_login\`, run that tool and retry once.
35724
35727
  2. New app: ask at most one question if the request is vague (for whom, what it must show), then \`create_app\` with a short lowercase name (it becomes the address https://<name>.sindri.kigo.dev) and a one-sentence description in Spanish. Then edit the files in the folder the tool returns to build what the PM asked. The template is Next.js + Drizzle; read its CLAUDE.md first; keep the UI in Spanish, "t\xFA", with the Kigo components it ships.
35725
35728
  3. Show before shipping: run \`preview\` and tell the PM to look at the local address; ask what to change. Edits reload live.
35726
- 4. Ship: \`publish\` with a message in the PM's words ("Filtro por fecha"). Give the URL and say the team enters with their Google de Kigo. If publish fails, the published version did not change: say what broke in one line, fix it, publish again.
35729
+ 4. Ship: \`publish\` with a message in the PM's words ("Filtro por fecha"). Give the URL and say the team enters with their Google de Kigo. If publish fails, the published version did not change: say what broke in one line, fix it, publish again. The published app has its own database: publish runs the migrations and, when this version changed drizzle/ or src/db/seed.ts, loads the test data again; rows the PM typed into the local preview never travel. Keep test data in the seed, not in the UI.
35727
35730
  5. Later: \`status\` before working on an existing app (tells you whether the local folder is behind or has unpublished edits); \`versions\` and \`rollback\` when the PM wants "como estaba ayer"; \`share\` for the Slack line.
35731
+ 6. Template updates: when the PM says "actualiza la plantilla" or Sindri announces a template fix, run \`update_template\`. It replaces only the files the app never customised, publishes \xABPlantilla actualizada\xBB and brings the local folder to it; the files you had changed are listed in \`skipped\` so you can apply the same change there by hand if it matters. It refuses while there are unpublished local edits: publish them first (or tell the PM what would be lost) and retry.
35732
+ 7. Renaming: an app has an address (https://<name>.sindri.kigo.dev) and a visible name (the title people see). When the PM says "c\xE1mbiale el nombre" and it is not obvious which one they mean, ask: "\xBFla direcci\xF3n o el nombre que se ve?". \`rename_app\` with \`new_name\` changes the address (Sindri renames it, the local folder follows, the old link stops working: tell the PM to share the new one); it refuses while there are unpublished edits, so publish first. \`rename_app\` with \`display_name\` changes only the visible name and publishes it as \xABAhora se llama \xAB<nombre>\xBB\xBB. Both in one call is fine.
35733
+ 8. Deleting: \`delete_app\` is final in Sindri (address, versions, database). Before calling it, ask the PM to type the app's name to confirm and pass exactly what they wrote as \`confirm\`; never fill it in yourself, never guess it, never call it because you inferred they want it. The local files are moved to the Sindri papelera on their computer, not deleted; say so.
35728
35734
 
35729
35735
  Rules: synthetic data only (never paste real emails, plates or phone numbers into seeds); one author per app (the PM); never touch files outside the app folder; do not edit middleware.ts, next.config.ts or package.json scripts (publish will refuse them); apps work Monday to Friday 9:05 am to midnight Mexico City time and the link says so outside those hours. When something fails, explain in one line what to do next; never blame the PM.`;
35730
35736
 
35731
35737
  // src/tools.ts
35732
- import fs8 from "node:fs/promises";
35733
- import path8 from "node:path";
35738
+ import fs9 from "node:fs/promises";
35739
+ import path9 from "node:path";
35734
35740
 
35735
35741
  // src/api.ts
35736
35742
  import fs3 from "node:fs/promises";
@@ -35814,8 +35820,7 @@ var KEYCHAIN_SERVICE = "sindri";
35814
35820
  async function loadCredentials(cfg) {
35815
35821
  if (cfg.credentialStore === "keychain") {
35816
35822
  const r = await run("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", cfg.issuer, "-w"], { timeoutMs: 1e4 });
35817
- if (r.code !== 0) return null;
35818
- return parse3(r.stdout.trim());
35823
+ if (r.code === 0) return parse3(r.stdout.trim());
35819
35824
  }
35820
35825
  try {
35821
35826
  return parse3(await fs2.readFile(credentialsFile(cfg), "utf8"));
@@ -35861,7 +35866,7 @@ async function deviceStart(cfg, fetchImpl = fetch) {
35861
35866
  if (!discovery.device_authorization_endpoint) throw new Error("Identity does not offer the device flow");
35862
35867
  const body = new URLSearchParams({
35863
35868
  client_id: cfg.clientId,
35864
- scope: "openid profile email offline_access",
35869
+ scope: SCOPES,
35865
35870
  audience: cfg.audience
35866
35871
  });
35867
35872
  const res = await fetchImpl(discovery.device_authorization_endpoint, {
@@ -35903,12 +35908,16 @@ async function devicePoll(cfg, discovery, start, opts = {}) {
35903
35908
  }
35904
35909
  throw new Error("expired");
35905
35910
  }
35911
+ var SCOPES = "openid profile email offline_access";
35906
35912
  async function refresh(cfg, creds, fetchImpl = fetch) {
35907
35913
  if (!creds.refresh_token) throw new NeedsLogin();
35908
35914
  const discovery = await discover(cfg, fetchImpl);
35909
35915
  const res = await fetchImpl(discovery.token_endpoint, {
35910
35916
  method: "POST",
35911
35917
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
35918
+ // No `scope` here: Identity rejects it on refresh (invalid_scope). It also issues the refreshed
35919
+ // token without scopes (bug reported); the API copes by remembering the email per subject and
35920
+ // `no_email` is treated as needs_login (api.ts).
35912
35921
  body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: creds.refresh_token, client_id: cfg.clientId })
35913
35922
  });
35914
35923
  if (!res.ok) throw new NeedsLogin();
@@ -35977,7 +35986,7 @@ async function authCodeLogin(cfg, opts) {
35977
35986
  authorize.search = new URLSearchParams({
35978
35987
  client_id: cfg.clientId,
35979
35988
  response_type: "code",
35980
- scope: "openid profile email offline_access",
35989
+ scope: SCOPES,
35981
35990
  redirect_uri: redirectUri,
35982
35991
  state,
35983
35992
  code_challenge: challenge,
@@ -36095,6 +36104,18 @@ var ApiClient = class {
36095
36104
  rollback(name, version2) {
36096
36105
  return this.json("POST", `/v1/apps/${enc(name)}/rollback`, { version: version2 }, 12e4);
36097
36106
  }
36107
+ /** Brings the app's never-customised files up to the current template; `skipped` are the ones Claude changed. */
36108
+ updateTemplate(name) {
36109
+ return this.json("POST", `/v1/apps/${enc(name)}/template`, {}, 32e4);
36110
+ }
36111
+ /** Changes the app's address; the builder renames branch and folder, the old address stops resolving. */
36112
+ rename(name, newName) {
36113
+ return this.json("POST", `/v1/apps/${enc(name)}/rename`, { new_name: newName }, 15e4);
36114
+ }
36115
+ /** Removes the app from Sindri. `confirm` must equal the name; the API refuses otherwise. */
36116
+ deleteApp(name, confirm) {
36117
+ return this.json("DELETE", `/v1/apps/${enc(name)}`, { confirm }, 12e4);
36118
+ }
36098
36119
  share(name) {
36099
36120
  return this.json("GET", `/v1/apps/${enc(name)}/share`);
36100
36121
  }
@@ -36143,6 +36164,7 @@ var ApiClient = class {
36143
36164
  const raw = await res.text();
36144
36165
  try {
36145
36166
  const j = JSON.parse(raw);
36167
+ if (res.status === 403 && j.code === "no_email") return new NeedsLogin();
36146
36168
  if (j.error) return new ApiError(res.status, j.error, j.code);
36147
36169
  } catch {
36148
36170
  }
@@ -36204,98 +36226,41 @@ async function appPresent(p) {
36204
36226
  }
36205
36227
  }
36206
36228
 
36207
- // src/preview.ts
36229
+ // src/manifest.ts
36208
36230
  import fs5 from "node:fs/promises";
36209
- import net from "node:net";
36210
36231
  import path5 from "node:path";
36211
- function registryFile(cfg) {
36212
- return path5.join(cfg.stateDir, "previews.json");
36213
- }
36214
- async function readRegistry(cfg) {
36215
- try {
36216
- return JSON.parse(await fs5.readFile(registryFile(cfg), "utf8"));
36217
- } catch {
36218
- return {};
36219
- }
36220
- }
36221
- async function writeRegistry(cfg, reg) {
36222
- await fs5.mkdir(cfg.stateDir, { recursive: true, mode: 448 });
36223
- await fs5.writeFile(registryFile(cfg), JSON.stringify(reg, null, 2) + "\n");
36224
- }
36225
- async function freePort(from = 3e3, to = 3099) {
36226
- for (let p = from; p <= to; p++) {
36227
- if (await portFree(p)) return p;
36228
- }
36229
- throw new Error("no free port between 3000 and 3099");
36230
- }
36231
- function portFree(port) {
36232
- return new Promise((resolve) => {
36233
- const srv = net.createServer();
36234
- srv.once("error", () => resolve(false));
36235
- srv.listen(port, "127.0.0.1", () => srv.close(() => resolve(true)));
36236
- });
36237
- }
36238
- async function healthy(port, fetchImpl = fetch) {
36239
- try {
36240
- const res = await fetchImpl(`http://127.0.0.1:${port}/healthz`, { signal: AbortSignal.timeout(3e3) });
36241
- return res.ok;
36242
- } catch {
36243
- return false;
36244
- }
36245
- }
36246
- async function runningPreview(cfg, app, fetchImpl = fetch) {
36247
- const reg = await readRegistry(cfg);
36248
- const e = reg[app];
36249
- if (!e) return null;
36250
- if (alive(e.pid) && await healthy(e.port, fetchImpl)) return e;
36251
- delete reg[app];
36252
- await writeRegistry(cfg, reg);
36253
- return null;
36254
- }
36255
- async function startPreview(cfg, tc, app, dir, opts = {}) {
36256
- const fetchImpl = opts.fetchImpl ?? fetch;
36257
- const existing = await runningPreview(cfg, app, fetchImpl);
36258
- if (existing) return { port: existing.port, url: `http://127.0.0.1:${existing.port}`, reused: true };
36259
- const port = await freePort();
36260
- await fs5.mkdir(path5.join(dir, ".sindri"), { recursive: true });
36261
- const logFile = path5.join(dir, ".sindri", "preview.log");
36262
- await fs5.writeFile(logFile, "");
36263
- const env = { ...tc.env, KIGO_ENV: "local", DATABASE_URL: "", PORT: String(port), CI: "1", FORCE_COLOR: "0", BROWSER: "none" };
36264
- const pid = spawnDetached(tc.pnpm, ["dev", "--port", String(port)], { cwd: dir, env, logFile });
36265
- const reg = await readRegistry(cfg);
36266
- reg[app] = { pid, port, dir, started_at: (/* @__PURE__ */ new Date()).toISOString() };
36267
- await writeRegistry(cfg, reg);
36268
- const deadline = Date.now() + (opts.waitMs ?? 6e4);
36269
- const sleepImpl = opts.sleepImpl ?? sleep;
36270
- while (Date.now() < deadline) {
36271
- if (await healthy(port, fetchImpl)) return { port, url: `http://127.0.0.1:${port}`, reused: false };
36272
- if (!alive(pid)) break;
36273
- await sleepImpl(1e3);
36232
+ var MANIFEST_FILE = "kigo-app.yaml";
36233
+ var KEY_RE = /^([a-z_]+):\s*(.*)$/;
36234
+ function setManifestValue(text, key, value) {
36235
+ const line = `${key}: ${JSON.stringify(value)}`;
36236
+ const lines = text.split("\n");
36237
+ const idx = lines.findIndex((l) => KEY_RE.exec(l)?.[1] === key);
36238
+ if (idx >= 0) {
36239
+ lines[idx] = line;
36240
+ } else {
36241
+ if (lines.at(-1) === "") lines.splice(lines.length - 1, 0, line);
36242
+ else lines.push(line, "");
36274
36243
  }
36275
- killTree(pid);
36276
- delete reg[app];
36277
- await writeRegistry(cfg, reg);
36278
- const log = await fs5.readFile(logFile, "utf8").catch(() => "");
36279
- return { port, url: `http://127.0.0.1:${port}`, reused: false, log: log.split("\n").slice(-15).join("\n") };
36244
+ return lines.join("\n");
36280
36245
  }
36281
- async function stopPreview(cfg, app) {
36282
- const reg = await readRegistry(cfg);
36283
- const e = reg[app];
36284
- if (!e) return false;
36285
- killTree(e.pid);
36286
- delete reg[app];
36287
- await writeRegistry(cfg, reg);
36288
- return true;
36246
+ async function readManifest(appDir) {
36247
+ return fs5.readFile(path5.join(appDir, MANIFEST_FILE), "utf8").catch(() => "");
36289
36248
  }
36290
- async function openInBrowser(cfg, url2) {
36291
- if (!cfg.opener) return;
36292
- await run(cfg.opener, [url2], { timeoutMs: 1e4 });
36249
+ async function writeManifestValue(appDir, key, value) {
36250
+ const text = await readManifest(appDir);
36251
+ await fs5.writeFile(path5.join(appDir, MANIFEST_FILE), setManifestValue(text, key, value));
36293
36252
  }
36294
36253
 
36254
+ // src/preview.ts
36255
+ import fs7 from "node:fs/promises";
36256
+ import net from "node:net";
36257
+ import path7 from "node:path";
36258
+
36295
36259
  // src/toolchain.ts
36296
36260
  import fs6 from "node:fs/promises";
36297
36261
  import path6 from "node:path";
36298
36262
  var MIN_NODE = 20;
36263
+ var PNPM_ARGS = ["--config.verify-deps-before-run=false"];
36299
36264
  async function resolveToolchain(cfg, env = process.env) {
36300
36265
  const fnmBin = await fnmDefaultBin(cfg);
36301
36266
  if (fnmBin) {
@@ -36387,11 +36352,96 @@ async function installFnmLts(cfg, env) {
36387
36352
  if (prep.code !== 0) throw new Error(`corepack prepare pnpm failed: ${prep.stderr.trim().slice(-300)}`);
36388
36353
  }
36389
36354
 
36355
+ // src/preview.ts
36356
+ function registryFile(cfg) {
36357
+ return path7.join(cfg.stateDir, "previews.json");
36358
+ }
36359
+ async function readRegistry(cfg) {
36360
+ try {
36361
+ return JSON.parse(await fs7.readFile(registryFile(cfg), "utf8"));
36362
+ } catch {
36363
+ return {};
36364
+ }
36365
+ }
36366
+ async function writeRegistry(cfg, reg) {
36367
+ await fs7.mkdir(cfg.stateDir, { recursive: true, mode: 448 });
36368
+ await fs7.writeFile(registryFile(cfg), JSON.stringify(reg, null, 2) + "\n");
36369
+ }
36370
+ async function freePort(from = 3e3, to = 3099) {
36371
+ for (let p = from; p <= to; p++) {
36372
+ if (await portFree(p)) return p;
36373
+ }
36374
+ throw new Error("no free port between 3000 and 3099");
36375
+ }
36376
+ function portFree(port) {
36377
+ return new Promise((resolve) => {
36378
+ const srv = net.createServer();
36379
+ srv.once("error", () => resolve(false));
36380
+ srv.listen(port, "127.0.0.1", () => srv.close(() => resolve(true)));
36381
+ });
36382
+ }
36383
+ async function healthy(port, fetchImpl = fetch) {
36384
+ try {
36385
+ const res = await fetchImpl(`http://127.0.0.1:${port}/healthz`, { signal: AbortSignal.timeout(3e3) });
36386
+ return res.ok;
36387
+ } catch {
36388
+ return false;
36389
+ }
36390
+ }
36391
+ async function runningPreview(cfg, app, fetchImpl = fetch) {
36392
+ const reg = await readRegistry(cfg);
36393
+ const e = reg[app];
36394
+ if (!e) return null;
36395
+ if (alive(e.pid) && await healthy(e.port, fetchImpl)) return e;
36396
+ delete reg[app];
36397
+ await writeRegistry(cfg, reg);
36398
+ return null;
36399
+ }
36400
+ async function startPreview(cfg, tc, app, dir, opts = {}) {
36401
+ const fetchImpl = opts.fetchImpl ?? fetch;
36402
+ const existing = await runningPreview(cfg, app, fetchImpl);
36403
+ if (existing) return { port: existing.port, url: `http://127.0.0.1:${existing.port}`, reused: true };
36404
+ const port = await freePort();
36405
+ await fs7.mkdir(path7.join(dir, ".sindri"), { recursive: true });
36406
+ const logFile = path7.join(dir, ".sindri", "preview.log");
36407
+ await fs7.writeFile(logFile, "");
36408
+ const env = { ...tc.env, KIGO_ENV: "local", DATABASE_URL: "", PORT: String(port), CI: "1", FORCE_COLOR: "0", BROWSER: "none" };
36409
+ const pid = spawnDetached(tc.pnpm, [...PNPM_ARGS, "dev", "--port", String(port)], { cwd: dir, env, logFile });
36410
+ const reg = await readRegistry(cfg);
36411
+ reg[app] = { pid, port, dir, started_at: (/* @__PURE__ */ new Date()).toISOString() };
36412
+ await writeRegistry(cfg, reg);
36413
+ const deadline = Date.now() + (opts.waitMs ?? 6e4);
36414
+ const sleepImpl = opts.sleepImpl ?? sleep;
36415
+ while (Date.now() < deadline) {
36416
+ if (await healthy(port, fetchImpl)) return { port, url: `http://127.0.0.1:${port}`, reused: false };
36417
+ if (!alive(pid)) break;
36418
+ await sleepImpl(1e3);
36419
+ }
36420
+ killTree(pid);
36421
+ delete reg[app];
36422
+ await writeRegistry(cfg, reg);
36423
+ const log = await fs7.readFile(logFile, "utf8").catch(() => "");
36424
+ return { port, url: `http://127.0.0.1:${port}`, reused: false, log: log.split("\n").slice(-15).join("\n") };
36425
+ }
36426
+ async function stopPreview(cfg, app) {
36427
+ const reg = await readRegistry(cfg);
36428
+ const e = reg[app];
36429
+ if (!e) return false;
36430
+ killTree(e.pid);
36431
+ delete reg[app];
36432
+ await writeRegistry(cfg, reg);
36433
+ return true;
36434
+ }
36435
+ async function openInBrowser(cfg, url2) {
36436
+ if (!cfg.opener) return;
36437
+ await run(cfg.opener, [url2], { timeoutMs: 1e4 });
36438
+ }
36439
+
36390
36440
  // src/tree.ts
36391
36441
  import { createHash as createHash2 } from "node:crypto";
36392
- import fs7 from "node:fs/promises";
36442
+ import fs8 from "node:fs/promises";
36393
36443
  import os3 from "node:os";
36394
- import path7 from "node:path";
36444
+ import path8 from "node:path";
36395
36445
  var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".sindri", "dist", ".turbo", ".git"]);
36396
36446
  function excluded(name) {
36397
36447
  if (EXCLUDED_DIRS.has(name)) return true;
@@ -36402,11 +36452,11 @@ function excluded(name) {
36402
36452
  async function listFiles(root) {
36403
36453
  const out = [];
36404
36454
  async function walk(dir, rel) {
36405
- const entries = await fs7.readdir(dir, { withFileTypes: true });
36455
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
36406
36456
  for (const e of entries) {
36407
36457
  if (excluded(e.name)) continue;
36408
36458
  const r = rel ? `${rel}/${e.name}` : e.name;
36409
- if (e.isDirectory()) await walk(path7.join(dir, e.name), r);
36459
+ if (e.isDirectory()) await walk(path8.join(dir, e.name), r);
36410
36460
  else if (e.isFile()) out.push(r);
36411
36461
  }
36412
36462
  }
@@ -36417,7 +36467,7 @@ async function treeHash(root) {
36417
36467
  const files = await listFiles(root);
36418
36468
  const h = createHash2("sha256");
36419
36469
  for (const rel of files) {
36420
- const fh = createHash2("sha256").update(await fs7.readFile(path7.join(root, rel))).digest("hex");
36470
+ const fh = createHash2("sha256").update(await fs8.readFile(path8.join(root, rel))).digest("hex");
36421
36471
  h.update(`${rel}\0${fh}
36422
36472
  `);
36423
36473
  }
@@ -36426,49 +36476,49 @@ async function treeHash(root) {
36426
36476
  async function packTree(root) {
36427
36477
  const files = await listFiles(root);
36428
36478
  if (files.length === 0) throw new Error("empty tree");
36429
- const listFile = path7.join(await fs7.mkdtemp(path7.join(os3.tmpdir(), "sindri-pack-")), "files.txt");
36430
- await fs7.writeFile(listFile, files.join("\n") + "\n");
36431
- const out = path7.join(path7.dirname(listFile), "app.tar.gz");
36479
+ const listFile = path8.join(await fs8.mkdtemp(path8.join(os3.tmpdir(), "sindri-pack-")), "files.txt");
36480
+ await fs8.writeFile(listFile, files.join("\n") + "\n");
36481
+ const out = path8.join(path8.dirname(listFile), "app.tar.gz");
36432
36482
  const r = await run("tar", ["-czf", out, "-C", root, "-T", listFile], { timeoutMs: 12e4 });
36433
36483
  if (r.code !== 0) throw new Error(`tar failed: ${r.stderr.trim()}`);
36434
- const st = await fs7.stat(out);
36484
+ const st = await fs8.stat(out);
36435
36485
  return { file: out, files: files.length, bytes: st.size };
36436
36486
  }
36437
36487
  async function replaceTree(root, archive) {
36438
- const tmp = await fs7.mkdtemp(path7.join(os3.tmpdir(), "sindri-unpack-"));
36488
+ const tmp = await fs8.mkdtemp(path8.join(os3.tmpdir(), "sindri-unpack-"));
36439
36489
  const r = await run("tar", ["-xzf", archive, "-C", tmp], { timeoutMs: 12e4 });
36440
36490
  if (r.code !== 0) throw new Error(`tar failed: ${r.stderr.trim()}`);
36441
36491
  const src = await singleRoot(tmp);
36442
- await fs7.mkdir(root, { recursive: true });
36492
+ await fs8.mkdir(root, { recursive: true });
36443
36493
  await prune(root, 0);
36444
- await fs7.cp(src, root, { recursive: true, force: true, filter: (s) => path7.basename(s) !== "node_modules" && path7.basename(s) !== ".DS_Store" });
36445
- await fs7.rm(tmp, { recursive: true, force: true });
36494
+ await fs8.cp(src, root, { recursive: true, force: true, filter: (s) => path8.basename(s) !== "node_modules" && path8.basename(s) !== ".DS_Store" });
36495
+ await fs8.rm(tmp, { recursive: true, force: true });
36446
36496
  }
36447
36497
  async function prune(dir, depth) {
36448
- const entries = await fs7.readdir(dir, { withFileTypes: true });
36498
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
36449
36499
  for (const e of entries) {
36450
- const p = path7.join(dir, e.name);
36500
+ const p = path8.join(dir, e.name);
36451
36501
  if (e.name === "node_modules") continue;
36452
36502
  if (depth === 3 && (e.name === ".sindri" || /^\.env(\..*)?$/.test(e.name))) continue;
36453
36503
  if (e.isDirectory()) {
36454
36504
  await prune(p, depth + 1);
36455
- const left = await fs7.readdir(p);
36456
- if (left.length === 0) await fs7.rmdir(p);
36505
+ const left = await fs8.readdir(p);
36506
+ if (left.length === 0) await fs8.rmdir(p);
36457
36507
  } else {
36458
- await fs7.rm(p, { force: true });
36508
+ await fs8.rm(p, { force: true });
36459
36509
  }
36460
36510
  }
36461
36511
  }
36462
36512
  async function singleRoot(dir) {
36463
- const entries = (await fs7.readdir(dir, { withFileTypes: true })).filter((e) => e.name !== ".DS_Store");
36464
- if (entries.length === 1 && entries[0]?.isDirectory() && !await exists2(path7.join(dir, "package.json"))) {
36465
- return path7.join(dir, entries[0].name);
36513
+ const entries = (await fs8.readdir(dir, { withFileTypes: true })).filter((e) => e.name !== ".DS_Store");
36514
+ if (entries.length === 1 && entries[0]?.isDirectory() && !await exists2(path8.join(dir, "package.json"))) {
36515
+ return path8.join(dir, entries[0].name);
36466
36516
  }
36467
36517
  return dir;
36468
36518
  }
36469
36519
  async function exists2(p) {
36470
36520
  try {
36471
- await fs7.access(p);
36521
+ await fs8.access(p);
36472
36522
  return true;
36473
36523
  } catch {
36474
36524
  return false;
@@ -36476,14 +36526,14 @@ async function exists2(p) {
36476
36526
  }
36477
36527
  async function readPublished(root) {
36478
36528
  try {
36479
- return JSON.parse(await fs7.readFile(path7.join(root, ".sindri", "last-published.json"), "utf8"));
36529
+ return JSON.parse(await fs8.readFile(path8.join(root, ".sindri", "last-published.json"), "utf8"));
36480
36530
  } catch {
36481
36531
  return null;
36482
36532
  }
36483
36533
  }
36484
36534
  async function writePublished(root, p) {
36485
- await fs7.mkdir(path7.join(root, ".sindri"), { recursive: true });
36486
- await fs7.writeFile(path7.join(root, ".sindri", "last-published.json"), JSON.stringify(p, null, 2) + "\n");
36535
+ await fs8.mkdir(path8.join(root, ".sindri"), { recursive: true });
36536
+ await fs8.writeFile(path8.join(root, ".sindri", "last-published.json"), JSON.stringify(p, null, 2) + "\n");
36487
36537
  }
36488
36538
 
36489
36539
  // src/tools.ts
@@ -36522,7 +36572,18 @@ var SCHEMAS = {
36522
36572
  list_apps: {},
36523
36573
  versions: { app: appName },
36524
36574
  rollback: { app: appName, version: external_exports.string().min(1).describe("Version id from `versions`.") },
36525
- share: { app: appName }
36575
+ update_template: { app: appName },
36576
+ rename_app: {
36577
+ app: appName,
36578
+ new_name: appName.optional().describe("New address: lowercase letters, digits and hyphens (2 to 31 chars). Becomes https://<new_name>.sindri.kigo.dev; the old address stops working."),
36579
+ display_name: external_exports.string().min(2).max(60).optional().describe("New visible name (the title people see), in Spanish, as the PM wrote it. The address does not change.")
36580
+ },
36581
+ delete_app: {
36582
+ app: appName,
36583
+ confirm: external_exports.string().describe("The app name exactly as the PM typed it to confirm. Must equal `app`; never fill it in for them.")
36584
+ },
36585
+ share: { app: appName },
36586
+ help: {}
36526
36587
  };
36527
36588
  var DESCRIPTIONS = {
36528
36589
  setup: {
@@ -36555,7 +36616,23 @@ var DESCRIPTIONS = {
36555
36616
  title: "Go back to a version",
36556
36617
  description: "Publishes a previous version and brings the local folder to it. Nothing is deleted: the newer versions stay in the history."
36557
36618
  },
36558
- share: { title: "Share", description: "The line to paste in Slack: address, test-data notice and how the team enters." }
36619
+ update_template: {
36620
+ title: "Update the template",
36621
+ description: "Brings the app up to the current Kigo template: files the app never customised are replaced, files you changed are left alone and listed. Publishes a version \xABPlantilla actualizada\xBB and brings the local folder to it. Use it when the PM asks to update the template or Sindri announces a template fix. Refuses while there are unpublished local edits: publish (or discard) them first."
36622
+ },
36623
+ rename_app: {
36624
+ title: "Rename an app",
36625
+ description: 'Two different things, pass at least one. `new_name` changes the address (https://<new_name>.sindri.kigo.dev): Sindri renames it, the local folder is moved to match and the old address stops working; refuses while the folder has unpublished edits (publish first). `display_name` changes only the visible name: it is written to kigo-app.yaml and published as \xABAhora se llama \xAB<display_name>\xBB\xBB. When the PM says "c\xE1mbiale el nombre" and it is unclear which one they mean, ask: \xBFla direcci\xF3n o el nombre que se ve? Both together are allowed (visible name first, then the address).'
36626
+ },
36627
+ delete_app: {
36628
+ title: "Delete an app",
36629
+ description: "Removes the app from Sindri (address, published versions and its database) after the PM typed its name: `confirm` must equal `app`, ask them to write it, never guess it. Stops the preview and moves the local folder to ~/Sindri/.papelera/<app>-<date> instead of deleting it, so the PM can still recover the files by hand."
36630
+ },
36631
+ share: { title: "Share", description: "The line to paste in Slack: address, test-data notice and how the team enters." },
36632
+ help: {
36633
+ title: "What Sindri is",
36634
+ description: "No network. The Spanish introduction to give the PM the first time they speak in a conversation or whenever they ask what Sindri can do: what it is, what they can ask for, the limits and the link to Mi espacio. Also says whether they are already signed in on this computer (`logged_in`) and where their apps live (`apps_dir`)."
36635
+ }
36559
36636
  };
36560
36637
  function createTools(deps) {
36561
36638
  const { cfg } = deps;
@@ -36594,9 +36671,11 @@ function createTools(deps) {
36594
36671
  const login = guard(async () => {
36595
36672
  try {
36596
36673
  const creds2 = await accessToken(cfg, fetchImpl);
36674
+ await api.me();
36597
36675
  return { ok: true, summary: `Ya est\xE1s dentro como ${creds2.email ?? "tu cuenta de Kigo"}.`, email: creds2.email ?? null, already: true };
36598
36676
  } catch (err) {
36599
36677
  if (!(err instanceof NeedsLogin)) throw err;
36678
+ await clearCredentials(cfg).catch(() => void 0);
36600
36679
  }
36601
36680
  const openUrl = deps.openUrl ?? defaultOpenUrl(cfg);
36602
36681
  let creds;
@@ -36647,15 +36726,15 @@ function createTools(deps) {
36647
36726
  }
36648
36727
  const created = await api.createApp(a.name, a.description.trim());
36649
36728
  const archive = await api.download(created.archive_url);
36650
- await fs8.mkdir(p.root, { recursive: true });
36729
+ await fs9.mkdir(p.root, { recursive: true });
36651
36730
  await replaceTree(p.root, archive);
36652
- await fs8.rm(path8.dirname(archive), { recursive: true, force: true });
36731
+ await fs9.rm(path9.dirname(archive), { recursive: true, force: true });
36653
36732
  p = await appPaths(cfg, a.name, me.owner);
36654
36733
  if (!await appPresent(p)) {
36655
36734
  return { ok: false, summary: `Cre\xE9 "${a.name}" en Sindri pero lo que baj\xF3 no trae la app donde la espero (apps/${p.owner || me.owner}/${a.name}). Escribe en Slack, canal #sindri.`, path: p.root, url: created.url };
36656
36735
  }
36657
- await fs8.writeFile(path8.join(p.appDir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
36658
- const install = await run(tc.pnpm, ["install", "--no-frozen-lockfile"], { cwd: p.root, env: { ...tc.env, CI: "1", FORCE_COLOR: "0" }, timeoutMs: 6e5 });
36736
+ await fs9.writeFile(path9.join(p.appDir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
36737
+ const install = await run(tc.pnpm, [...PNPM_ARGS, "install", "--no-frozen-lockfile"], { cwd: p.root, env: { ...tc.env, CI: "1", FORCE_COLOR: "0" }, timeoutMs: 6e5 });
36659
36738
  if (install.code !== 0) {
36660
36739
  const why = lastLine(install.stderr || install.stdout);
36661
36740
  return { ok: false, summary: `Cre\xE9 "${a.name}" en Sindri pero no pude preparar la carpeta en tu computadora (${why}). Vuelve a intentarlo con create_app o p\xEDdeme que lo revise.`, path: p.appDir, url: created.url, detail: why };
@@ -36683,8 +36762,8 @@ function createTools(deps) {
36683
36762
  }
36684
36763
  const tc = await toolchain();
36685
36764
  if (!tc) return NEEDS_SETUP;
36686
- if (!await exists2(path8.join(p.root, "node_modules"))) {
36687
- const install = await run(tc.pnpm, ["install", "--no-frozen-lockfile"], { cwd: p.root, env: { ...tc.env, CI: "1", FORCE_COLOR: "0" }, timeoutMs: 6e5 });
36765
+ if (!await exists2(path9.join(p.root, "node_modules"))) {
36766
+ const install = await run(tc.pnpm, [...PNPM_ARGS, "install", "--no-frozen-lockfile"], { cwd: p.root, env: { ...tc.env, CI: "1", FORCE_COLOR: "0" }, timeoutMs: 6e5 });
36688
36767
  if (install.code !== 0) return { ok: false, summary: `No pude preparar la app para correrla en tu computadora (${lastLine(install.stderr || install.stdout)}). Vuelve a intentarlo.`, detail: lastLine(install.stderr || install.stdout) };
36689
36768
  }
36690
36769
  if (!await runningPreview(cfg, a.app, fetchImpl)) {
@@ -36712,7 +36791,7 @@ function createTools(deps) {
36712
36791
  const tc = await toolchain();
36713
36792
  if (!tc) return NEEDS_SETUP;
36714
36793
  const message = a.message.trim().slice(0, MESSAGE_MAX);
36715
- const tsc = await run(tc.pnpm, ["exec", "tsc", "--noEmit", "--pretty", "false"], { cwd: dir, env: { ...tc.env, CI: "1", FORCE_COLOR: "0" }, timeoutMs: 18e4 });
36794
+ const tsc = await run(tc.pnpm, [...PNPM_ARGS, "exec", "tsc", "--noEmit", "--pretty", "false"], { cwd: dir, env: { ...tc.env, CI: "1", FORCE_COLOR: "0" }, timeoutMs: 18e4 });
36716
36795
  if (tsc.code !== 0) {
36717
36796
  const first = (tsc.stdout + tsc.stderr).split("\n").find((l) => /error TS\d+/.test(l))?.trim().slice(0, 200);
36718
36797
  return { ok: false, summary: "No la publiqu\xE9: hay un error en el c\xF3digo que impedir\xEDa que abra. Corr\xEDgelo y vuelve a publicar.", detail: first ?? lastLine(tsc.stderr || tsc.stdout) };
@@ -36724,14 +36803,14 @@ function createTools(deps) {
36724
36803
  }
36725
36804
  const packed = await packTree(dir);
36726
36805
  if (packed.bytes > 20 * 1024 * 1024) {
36727
- await fs8.rm(path8.dirname(packed.file), { recursive: true, force: true });
36806
+ await fs9.rm(path9.dirname(packed.file), { recursive: true, force: true });
36728
36807
  return { ok: false, summary: "La app pesa m\xE1s de 20 MB sin contar sus piezas instaladas. Quita archivos grandes (im\xE1genes, videos) antes de publicar.", bytes: packed.bytes };
36729
36808
  }
36730
36809
  let result;
36731
36810
  try {
36732
36811
  result = await api.publish(a.app, packed.file, message);
36733
36812
  } finally {
36734
- await fs8.rm(path8.dirname(packed.file), { recursive: true, force: true });
36813
+ await fs9.rm(path9.dirname(packed.file), { recursive: true, force: true });
36735
36814
  }
36736
36815
  await writePublished(dir, { version: result.version, tree_hash: await treeHash(dir), url: result.url });
36737
36816
  return {
@@ -36754,11 +36833,11 @@ function createTools(deps) {
36754
36833
  });
36755
36834
  const list_apps = guard(async () => {
36756
36835
  const apps = await api.listApps();
36757
- if (apps.length === 0) return { ok: true, summary: "Todav\xEDa no tienes apps. Cuando quieras, crea la primera.", apps: [] };
36836
+ if (apps.length === 0) return { ok: true, summary: "Todav\xEDa no tienes apps. Cuando quieras, crea la primera.", apps: [], space_url: SPACE_URL };
36758
36837
  const rows = [];
36759
36838
  for (const app of apps) rows.push({ ...app, local: await localState(cfg, app.name) });
36760
36839
  const names = rows.map((r) => `${r.name} (${r.url})`).join(", ");
36761
- return { ok: true, summary: `Tienes ${rows.length === 1 ? "una app" : `${rows.length} apps`}: ${names}.`, apps: rows };
36840
+ return { ok: true, summary: `Tienes ${rows.length === 1 ? "una app" : `${rows.length} apps`}: ${names}.`, apps: rows, space_url: SPACE_URL };
36762
36841
  });
36763
36842
  const versions = guard(async (a) => {
36764
36843
  if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
@@ -36773,15 +36852,15 @@ function createTools(deps) {
36773
36852
  let p = await appPaths(cfg, a.app);
36774
36853
  await stopPreview(cfg, a.app);
36775
36854
  const archive = await api.download(`/v1/apps/${encodeURIComponent(a.app)}/archive`, result.version.id);
36776
- await fs8.mkdir(p.root, { recursive: true });
36855
+ await fs9.mkdir(p.root, { recursive: true });
36777
36856
  await replaceTree(p.root, archive);
36778
- await fs8.rm(path8.dirname(archive), { recursive: true, force: true });
36857
+ await fs9.rm(path9.dirname(archive), { recursive: true, force: true });
36779
36858
  p = await appPaths(cfg, a.app, p.owner || void 0);
36780
36859
  const dir = p.appDir;
36781
36860
  if (!await appPresent(p)) return { ok: false, summary: `Sindri volvi\xF3 a la versi\xF3n pero lo que baj\xF3 no trae la app donde la espero. Escribe en Slack, canal #sindri.`, version: result.version, url: result.url };
36782
- if (!await exists2(path8.join(dir, ".env.local"))) await fs8.writeFile(path8.join(dir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
36861
+ if (!await exists2(path9.join(dir, ".env.local"))) await fs9.writeFile(path9.join(dir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
36783
36862
  const tc = await toolchain();
36784
- if (tc && await exists2(path8.join(p.root, "node_modules"))) {
36863
+ if (tc && await exists2(path9.join(p.root, "node_modules"))) {
36785
36864
  const db = await prepareLocalDb(tc, dir);
36786
36865
  if (!db.ok) process.stderr.write(`sindri: local db after rollback: ${db.detail}
36787
36866
  `);
@@ -36794,24 +36873,163 @@ function createTools(deps) {
36794
36873
  url: result.url
36795
36874
  };
36796
36875
  });
36876
+ const update_template = guard(async (a) => {
36877
+ if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
36878
+ const before = await localState(cfg, a.app);
36879
+ if (before.exists && before.unpublished) {
36880
+ return { ok: false, summary: "Publica o descarta tus cambios antes de actualizar la plantilla.", code: "unpublished_changes", local: before };
36881
+ }
36882
+ const result = await api.updateTemplate(a.app);
36883
+ const files = describeTemplateFiles(result);
36884
+ const data = { updated: result.updated, skipped: result.skipped, version: result.version, url: result.url, published: result.published };
36885
+ if (!result.published) {
36886
+ return { ok: true, summary: `"${a.app}" ya est\xE1 al d\xEDa con la plantilla; no cambi\xE9 nada.${files}`, ...data };
36887
+ }
36888
+ let p = await appPaths(cfg, a.app);
36889
+ if (before.exists) {
36890
+ await stopPreview(cfg, a.app);
36891
+ const archive = await api.download(`/v1/apps/${encodeURIComponent(a.app)}/archive`, result.version?.id);
36892
+ await replaceTree(p.root, archive);
36893
+ await fs9.rm(path9.dirname(archive), { recursive: true, force: true });
36894
+ p = await appPaths(cfg, a.app, p.owner || void 0);
36895
+ const dir = p.appDir;
36896
+ if (!await appPresent(p)) return { ok: false, summary: `Sindri actualiz\xF3 la plantilla pero lo que baj\xF3 no trae la app donde la espero. Escribe en Slack, canal #sindri.`, ...data };
36897
+ if (!await exists2(path9.join(dir, ".env.local"))) await fs9.writeFile(path9.join(dir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
36898
+ const tc = await toolchain();
36899
+ if (tc && await exists2(path9.join(p.root, "node_modules"))) {
36900
+ const db = await prepareLocalDb(tc, dir);
36901
+ if (!db.ok) process.stderr.write(`sindri: local db after update_template: ${db.detail}
36902
+ `);
36903
+ }
36904
+ await writePublished(dir, { version: result.version, tree_hash: await treeHash(dir), url: result.url });
36905
+ }
36906
+ const where = before.exists ? `en ${result.url} y en tu computadora` : `en ${result.url}`;
36907
+ return { ok: true, summary: `Listo, actualic\xE9 la plantilla de "${a.app}" ${where}.${files}`, ...data };
36908
+ });
36797
36909
  const share = guard(async (a) => {
36798
36910
  if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
36799
36911
  const r = await api.share(a.app);
36800
36912
  return { ok: true, summary: `Para Slack: ${r.text}`, text: r.text };
36801
36913
  });
36914
+ const rename_app = guard(async (a) => {
36915
+ if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
36916
+ const newName = a.new_name?.trim().toLowerCase();
36917
+ const displayName = a.display_name?.trim();
36918
+ if (!newName && !displayName) return { ok: false, summary: "Dime qu\xE9 quieres cambiar: la direcci\xF3n o el nombre que se ve.", code: "nothing_to_change" };
36919
+ if (newName !== void 0 && !validAppName(newName)) {
36920
+ return { ok: false, summary: "La direcci\xF3n nueva debe ir en min\xFAsculas, con n\xFAmeros o guiones, de 2 a 31 letras (por ejemplo: visitas-plaza)." };
36921
+ }
36922
+ if (newName === a.app) return { ok: false, summary: `"${a.app}" ya est\xE1 en esa direcci\xF3n.`, code: "same_name" };
36923
+ const parts = [];
36924
+ const data = {};
36925
+ if (displayName) {
36926
+ const p = await appPaths(cfg, a.app);
36927
+ if (!await appPresent(p)) {
36928
+ return { ok: false, summary: `No encuentro la carpeta de "${a.app}" en tu computadora y el nombre que se ve se publica desde aqu\xED. Usa rollback a su \xFAltima versi\xF3n para bajarla y vuelve a intentarlo.`, path: p.appDir };
36929
+ }
36930
+ await writeManifestValue(p.appDir, "display_name", displayName);
36931
+ const pub = await publish({ app: a.app, message: `Ahora se llama \xAB${displayName}\xBB` });
36932
+ if (!pub.ok) return { ...pub, summary: `El nombre \xAB${displayName}\xBB qued\xF3 en tu computadora pero a\xFAn no est\xE1 publicado. ${pub.summary}` };
36933
+ parts.push(`Listo, ahora se llama \xAB${displayName}\xBB y ya se ve as\xED en ${pub.url}.`);
36934
+ Object.assign(data, { display_name: displayName, version: pub.version, url: pub.url });
36935
+ }
36936
+ if (newName) {
36937
+ const before = await localState(cfg, a.app);
36938
+ if (before.exists && before.unpublished) {
36939
+ return { ok: false, summary: `Publica tus cambios antes de cambiar la direcci\xF3n de "${a.app}".`, code: "unpublished_changes", local: before };
36940
+ }
36941
+ if (await exists2(appRoot(cfg, newName))) {
36942
+ return { ok: false, summary: `Ya tienes una carpeta llamada "${newName}" en Sindri; elige otra direcci\xF3n.`, code: "folder_exists" };
36943
+ }
36944
+ const result = await api.rename(a.app, newName);
36945
+ const oldUrl = `https://${a.app}.${cfg.appsDomain}/`;
36946
+ if (before.exists) {
36947
+ const p = await appPaths(cfg, a.app);
36948
+ await stopPreview(cfg, a.app);
36949
+ const newRoot = appRoot(cfg, newName);
36950
+ await moveDir(p.root, newRoot);
36951
+ const newDir = path9.join(newRoot, "apps", p.owner, newName);
36952
+ await moveDir(path9.join(newRoot, "apps", p.owner, a.app), newDir);
36953
+ await writeManifestValue(newDir, "name", newName);
36954
+ await writePublished(newDir, { version: result.version ?? before.published_version, tree_hash: await treeHash(newDir), url: result.url });
36955
+ data.path = newDir;
36956
+ }
36957
+ parts.push(`Listo, "${a.app}" ahora vive en ${result.url}. La direcci\xF3n anterior (${oldUrl}) ya no funciona: comparte la nueva con tu equipo.`);
36958
+ Object.assign(data, { name: result.name, old_name: a.app, url: result.url, version: result.version });
36959
+ }
36960
+ return { ok: true, summary: parts.join(" "), ...data };
36961
+ });
36962
+ const delete_app = guard(async (a) => {
36963
+ if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
36964
+ if (a.confirm.trim() !== a.app) {
36965
+ return { ok: false, summary: `Para borrar "${a.app}" necesito que escribas su nombre tal cual. No la toqu\xE9.`, code: "confirm_mismatch" };
36966
+ }
36967
+ const root = appRoot(cfg, a.app);
36968
+ const present = await exists2(root);
36969
+ await stopPreview(cfg, a.app);
36970
+ let inSindri = true;
36971
+ try {
36972
+ await api.deleteApp(a.app, a.app);
36973
+ } catch (err) {
36974
+ if (err instanceof ApiError && err.status === 404 && present) inSindri = false;
36975
+ else throw err;
36976
+ }
36977
+ let trash = null;
36978
+ if (present) {
36979
+ trash = path9.join(cfg.home, ".papelera", `${a.app}-${stamp()}`);
36980
+ await moveDir(root, trash);
36981
+ }
36982
+ const kept = "Una copia de sus archivos queda en tu computadora, en la papelera de Sindri; puedes borrarla a mano cuando quieras.";
36983
+ let summary;
36984
+ if (!inSindri) summary = `"${a.app}" ya no exist\xEDa en Sindri. ${kept}`;
36985
+ else if (trash) summary = `Borr\xE9 "${a.app}" de Sindri: su direcci\xF3n y sus versiones ya no existen. ${kept}`;
36986
+ else summary = `Borr\xE9 "${a.app}" de Sindri: su direcci\xF3n y sus versiones ya no existen. En esta computadora no ten\xEDa carpeta.`;
36987
+ return { ok: true, summary, name: a.app, removed_from_sindri: inSindri, trash_path: trash };
36988
+ });
36989
+ const help = guard(async () => {
36990
+ const logged_in = await loadCredentials(cfg) !== null;
36991
+ const summary = logged_in ? HELP_SUMMARY : `${HELP_SUMMARY} Todav\xEDa no has entrado con tu cuenta de Kigo en esta computadora; cuando quieras empezar, te abro la p\xE1gina.`;
36992
+ return { ok: true, summary, space_url: SPACE_URL, logged_in, apps_dir: cfg.home };
36993
+ });
36802
36994
  const logout = guard(async () => {
36803
36995
  await clearCredentials(cfg);
36804
36996
  return { ok: true, summary: "Saliste de tu cuenta de Kigo en esta computadora." };
36805
36997
  });
36806
- return { setup, login, create_app, preview, publish, status, list_apps, versions, rollback, share, logout };
36998
+ return { setup, login, create_app, preview, publish, status, list_apps, versions, rollback, update_template, rename_app, delete_app, share, help, logout };
36999
+ }
37000
+ var HELP_SUMMARY = "Sindri te ayuda a crear apps internas peque\xF1as para tu equipo en minutos, con datos de prueba y sin escribir c\xF3digo: t\xFA me cuentas qu\xE9 necesitas y yo la construyo. Puedes pedirme crear una app, verla en tu computadora antes de publicarla, publicarla, cambiarle el nombre o la direcci\xF3n, volver a una versi\xF3n anterior, compartirla en Slack, actualizar su plantilla o borrarla. Ten en cuenta: las apps trabajan con datos sint\xE9ticos, nunca reales; funcionan de lunes a viernes de 9:05 a 24:00, hora de la Ciudad de M\xE9xico; cada app la edita solo quien la cre\xF3; y lo que deba llegar a toda la empresa se pide a desarrollo. Tus apps y sus versiones est\xE1n en https://sindri.kigo.dev.";
37001
+ async function moveDir(from, to) {
37002
+ await fs9.mkdir(path9.dirname(to), { recursive: true });
37003
+ try {
37004
+ await fs9.rename(from, to);
37005
+ } catch (err) {
37006
+ if (err.code !== "EXDEV") throw err;
37007
+ await fs9.cp(from, to, { recursive: true });
37008
+ await fs9.rm(from, { recursive: true, force: true });
37009
+ }
37010
+ }
37011
+ function stamp(d = /* @__PURE__ */ new Date()) {
37012
+ const parts = new Intl.DateTimeFormat("en-CA", { timeZone: "America/Mexico_City", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).formatToParts(d);
37013
+ const g = (t) => parts.find((p) => p.type === t)?.value ?? "";
37014
+ return `${g("year")}${g("month")}${g("day")}-${g("hour")}${g("minute")}`;
37015
+ }
37016
+ function describeTemplateFiles(r) {
37017
+ const parts = [];
37018
+ const n = r.updated.length;
37019
+ if (n > 0) parts.push(n === 1 ? "Cambi\xF3 un archivo de la plantilla." : `Cambiaron ${n} archivos de la plantilla.`);
37020
+ if (r.skipped.length > 0) {
37021
+ const list = r.skipped.length <= 5 ? r.skipped.join(", ") : `${r.skipped.slice(0, 5).join(", ")} y ${r.skipped.length - 5} m\xE1s`;
37022
+ parts.push(`Dej\xE9 como estaban ${r.skipped.length === 1 ? "uno que" : `${r.skipped.length} que`} Claude ya hab\xEDa cambiado (${list}); rev\xEDsalos si quieres el cambio ah\xED tambi\xE9n.`);
37023
+ }
37024
+ return parts.length ? ` ${parts.join(" ")}` : "";
36807
37025
  }
36808
37026
  async function prepareLocalDb(tc, dir, opts = {}) {
36809
37027
  const env = { ...tc.env, KIGO_ENV: "local", DATABASE_URL: "", CI: "1", FORCE_COLOR: "0" };
36810
- const fresh = !await exists2(path8.join(dir, ".sindri", "pglite"));
36811
- const migrate = await run(tc.pnpm, ["db:migrate"], { cwd: dir, env, timeoutMs: 18e4 });
37028
+ const fresh = !await exists2(path9.join(dir, ".sindri", "pglite"));
37029
+ const migrate = await run(tc.pnpm, [...PNPM_ARGS, "db:migrate"], { cwd: dir, env, timeoutMs: 18e4 });
36812
37030
  if (migrate.code !== 0) return { ok: false, detail: lastLine(migrate.stderr || migrate.stdout) };
36813
37031
  if (opts.seed ?? fresh) {
36814
- const seed = await run(tc.pnpm, ["db:seed"], { cwd: dir, env, timeoutMs: 18e4 });
37032
+ const seed = await run(tc.pnpm, [...PNPM_ARGS, "db:seed"], { cwd: dir, env, timeoutMs: 18e4 });
36815
37033
  if (seed.code !== 0) return { ok: false, detail: lastLine(seed.stderr || seed.stdout) };
36816
37034
  }
36817
37035
  return { ok: true };
@@ -36839,7 +37057,7 @@ function when(iso, now = /* @__PURE__ */ new Date()) {
36839
37057
  }
36840
37058
 
36841
37059
  // src/index.ts
36842
- var VERSION = "0.1.0";
37060
+ var VERSION = "0.1.3";
36843
37061
  function registerTools(server, tools) {
36844
37062
  for (const name of Object.keys(SCHEMAS)) {
36845
37063
  const meta3 = DESCRIPTIONS[name];
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "manifest_version": "0.2",
3
3
  "name": "sindri",
4
4
  "display_name": "Kigo Sindri",
5
- "version": "0.1.0",
5
+ "version": "0.1.3",
6
6
  "description": "Create, preview and publish Kigo internal apps from your Claude.",
7
7
  "long_description": "Sindri lets a Kigo product manager build small internal apps by talking to Claude. Claude edits the app on your computer; this connector installs what is needed, runs the app locally with test data, publishes it at <name>.sindri.kigo.dev and keeps versions. Sign in with your @cargomovil.com account.",
8
8
  "author": {
@@ -13,7 +13,9 @@
13
13
  "entry_point": "dist/index.js",
14
14
  "mcp_config": {
15
15
  "command": "node",
16
- "args": ["${__dirname}/dist/index.js"],
16
+ "args": [
17
+ "${__dirname}/dist/index.js"
18
+ ],
17
19
  "env": {
18
20
  "SINDRI_HOME": "${user_config.sindri_home}"
19
21
  }
@@ -29,20 +31,75 @@
29
31
  }
30
32
  },
31
33
  "tools": [
32
- { "name": "setup", "description": "Prepare this computer (Node.js and pnpm, no admin rights)" },
33
- { "name": "login", "description": "Sign in with your Kigo account" },
34
- { "name": "create_app", "description": "Create a new app from Kigo's template" },
35
- { "name": "preview", "description": "Run an app locally with test data" },
36
- { "name": "publish", "description": "Publish an app at its Sindri address" },
37
- { "name": "status", "description": "Address, last version and local changes of an app" },
38
- { "name": "list_apps", "description": "Your apps in Sindri" },
39
- { "name": "versions", "description": "Published versions of an app" },
40
- { "name": "rollback", "description": "Go back to a previous version" },
41
- { "name": "share", "description": "The line to paste in Slack" }
34
+ {
35
+ "name": "setup",
36
+ "description": "Prepare this computer (Node.js and pnpm, no admin rights)"
37
+ },
38
+ {
39
+ "name": "login",
40
+ "description": "Sign in with your Kigo account"
41
+ },
42
+ {
43
+ "name": "create_app",
44
+ "description": "Create a new app from Kigo's template"
45
+ },
46
+ {
47
+ "name": "preview",
48
+ "description": "Run an app locally with test data"
49
+ },
50
+ {
51
+ "name": "publish",
52
+ "description": "Publish an app at its Sindri address"
53
+ },
54
+ {
55
+ "name": "status",
56
+ "description": "Address, last version and local changes of an app"
57
+ },
58
+ {
59
+ "name": "list_apps",
60
+ "description": "Your apps in Sindri"
61
+ },
62
+ {
63
+ "name": "versions",
64
+ "description": "Published versions of an app"
65
+ },
66
+ {
67
+ "name": "rollback",
68
+ "description": "Go back to a previous version"
69
+ },
70
+ {
71
+ "name": "update_template",
72
+ "description": "Bring an app up to the current Kigo template"
73
+ },
74
+ {
75
+ "name": "rename_app",
76
+ "description": "Change an app's address or its visible name"
77
+ },
78
+ {
79
+ "name": "delete_app",
80
+ "description": "Delete an app after the PM types its name"
81
+ },
82
+ {
83
+ "name": "share",
84
+ "description": "The line to paste in Slack"
85
+ },
86
+ {
87
+ "name": "help",
88
+ "description": "What Sindri is and what you can ask for"
89
+ }
90
+ ],
91
+ "keywords": [
92
+ "kigo",
93
+ "sindri",
94
+ "internal apps"
42
95
  ],
43
- "keywords": ["kigo", "sindri", "internal apps"],
44
96
  "compatibility": {
45
- "platforms": ["darwin", "linux"],
46
- "runtimes": { "node": ">=20" }
97
+ "platforms": [
98
+ "darwin",
99
+ "linux"
100
+ ],
101
+ "runtimes": {
102
+ "node": ">=20"
103
+ }
47
104
  }
48
105
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kigo-dev/sindri",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Sindri connector: an MCP server that lets a Kigo PM's Claude create, preview and publish internal apps on Kigo Sindri",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  "packageManager": "pnpm@11.5.0",
36
36
  "repository": {
37
37
  "type": "git",
38
- "url": "https://github.com/Parkimovil/kigo-sindri.git",
38
+ "url": "git+https://github.com/Parkimovil/kigo-sindri.git",
39
39
  "directory": "connector"
40
40
  },
41
41
  "publishConfig": {