@kigo-dev/sindri 0.1.5 → 0.1.7

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
@@ -40,14 +40,22 @@ name, typed confirmation before `delete_app`).
40
40
 
41
41
  ## Install
42
42
 
43
- **Claude Desktop (recommended for PMs):** double-click the `sindri.mcpb` extension (built from
43
+ **Claude Desktop (recommended for builders):** double-click the `sindri.mcpb` extension (built from
44
44
  `manifest.json` with `npx @anthropic-ai/mcpb pack`), accept, done. The first conversation runs
45
45
  `setup` and `login`.
46
46
 
47
+ The extension starts `dist/launcher.js`, not the connector itself: Claude Desktop never refreshes
48
+ an installed `.mcpb`, so at every start the launcher reads npm's `latest` for `@kigo-dev/sindri`
49
+ and, when it is a different version of the same major and not older than the bundled copy,
50
+ downloads it (sha512 checked) into `~/.sindri/connector/<version>` and runs it. The cache keeps
51
+ two versions. Offline or on any error it runs the newest cached version, else the bundled one.
52
+ Releasing is therefore the `connector-vX.Y.Z` tag alone; to roll everyone back, move the tag:
53
+ `npm dist-tag add @kigo-dev/sindri@<good> latest`. `SINDRI_AUTO_UPDATE=0` turns it off.
54
+
47
55
  **Claude Code:**
48
56
 
49
57
  ```
50
- claude mcp add sindri -- npx -y @kigo-dev/sindri@0.1.5
58
+ claude mcp add sindri -- npx -y @kigo-dev/sindri@0.1.7
51
59
  ```
52
60
 
53
61
  **Manual check without an MCP client:**
package/dist/index.js CHANGED
@@ -36764,23 +36764,30 @@ async function setupToolchain(cfg, opts = {}) {
36764
36764
  await fs7.mkdir(cfg.home, { recursive: true });
36765
36765
  return { installed, node: node2, pnpm, source: tc.source };
36766
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
+ }
36767
36774
  async function installFnmLts(cfg, env) {
36768
36775
  await fs7.mkdir(cfg.fnmDir, { recursive: true });
36769
36776
  const script = await (await fetch("https://fnm.vercel.app/install")).text();
36770
- const sh = await run("bash", ["-s", "--", "--install-dir", cfg.fnmDir, "--skip-shell"], { env, input: script, timeoutMs: 3e5 });
36771
- if (sh.code !== 0) throw new Error(`fnm installer failed: ${sh.stderr.trim().slice(-300)}`);
36777
+ const sh = await run("bash", fnmInstallerArgs(cfg.fnmDir), { env, input: script, timeoutMs: 3e5 });
36778
+ if (sh.code !== 0) throw stepFailed("fnm installer", sh);
36772
36779
  const fnm = path7.join(cfg.fnmDir, "fnm");
36773
36780
  const fnmEnv = { ...env, FNM_DIR: cfg.fnmDir };
36774
36781
  const inst = await run(fnm, ["install", "--lts"], { env: fnmEnv, timeoutMs: 6e5 });
36775
- 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);
36776
36783
  const def = await run(fnm, ["default", "lts-latest"], { env: fnmEnv, timeoutMs: 6e4 });
36777
- if (def.code !== 0) throw new Error(`fnm default failed: ${def.stderr.trim().slice(-300)}`);
36784
+ if (def.code !== 0) throw stepFailed("fnm default", def);
36778
36785
  const bin = path7.join(cfg.fnmDir, "aliases", "default", "bin");
36779
36786
  const childEnv = { ...env, PATH: `${bin}${path7.delimiter}${env.PATH ?? ""}`, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", npm_config_verify_deps_before_run: "false" };
36780
36787
  const enable = await run(path7.join(bin, "corepack"), ["enable", "--install-directory", bin], { env: childEnv, timeoutMs: 6e4 });
36781
- if (enable.code !== 0) throw new Error(`corepack enable failed: ${enable.stderr.trim().slice(-300)}`);
36788
+ if (enable.code !== 0) throw stepFailed("corepack enable", enable);
36782
36789
  const prep = await run(path7.join(bin, "corepack"), ["prepare", `pnpm@${cfg.pnpmVersion}`, "--activate"], { env: childEnv, timeoutMs: 3e5 });
36783
- if (prep.code !== 0) throw new Error(`corepack prepare pnpm failed: ${prep.stderr.trim().slice(-300)}`);
36790
+ if (prep.code !== 0) throw stepFailed("corepack prepare pnpm", prep);
36784
36791
  }
36785
36792
 
36786
36793
  // src/preview.ts
@@ -37316,7 +37323,15 @@ function createTools(deps) {
37316
37323
  };
37317
37324
  }
37318
37325
  const setup = guard(async () => {
37319
- 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
+ }
37320
37335
  const what = report.installed.length ? `Instal\xE9 ${report.installed.join(" y ")}.` : "Tu computadora ya ten\xEDa lo necesario.";
37321
37336
  return {
37322
37337
  ok: true,
@@ -37894,7 +37909,7 @@ function when(iso, now = /* @__PURE__ */ new Date()) {
37894
37909
  }
37895
37910
 
37896
37911
  // src/index.ts
37897
- var VERSION = "0.1.5";
37912
+ var VERSION = "0.1.7";
37898
37913
  function registerTools(server, tools) {
37899
37914
  for (const name of Object.keys(SCHEMAS)) {
37900
37915
  const meta3 = DESCRIPTIONS[name];
@@ -0,0 +1,165 @@
1
+ // src/launcher.ts
2
+ import { createHash } from "node:crypto";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import { gunzipSync } from "node:zlib";
8
+ var PACKAGE = "@kigo-dev/sindri";
9
+ var ENTRY = path.join("package", "dist", "index.js");
10
+ var KEEP = 2;
11
+ function parseVersion(v) {
12
+ const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
13
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
14
+ }
15
+ function newer(a, b) {
16
+ if (a[0] !== b[0]) return a[0] > b[0];
17
+ if (a[1] !== b[1]) return a[1] > b[1];
18
+ return a[2] > b[2];
19
+ }
20
+ function optionsFromEnv(env = process.env) {
21
+ const stateDir = path.resolve(env.SINDRI_STATE_DIR || path.join(env.HOME || os.homedir(), ".sindri"));
22
+ return {
23
+ bundledDir: path.dirname(fileURLToPath(import.meta.url)),
24
+ cacheDir: path.join(stateDir, "connector"),
25
+ registry: (env.SINDRI_NPM_REGISTRY || "https://registry.npmjs.org").replace(/\/+$/, ""),
26
+ enabled: env.SINDRI_AUTO_UPDATE !== "0",
27
+ timeoutMs: 8e3
28
+ };
29
+ }
30
+ function bundledVersion(bundledDir) {
31
+ const pkg = JSON.parse(fs.readFileSync(path.join(bundledDir, "..", "package.json"), "utf8"));
32
+ return pkg.version;
33
+ }
34
+ function cachedVersions(cacheDir) {
35
+ let names;
36
+ try {
37
+ names = fs.readdirSync(cacheDir);
38
+ } catch {
39
+ return [];
40
+ }
41
+ const out = [];
42
+ for (const name of names) {
43
+ const parsed = parseVersion(name);
44
+ const entry = path.join(cacheDir, name, ENTRY);
45
+ if (parsed && fs.existsSync(entry)) out.push({ version: name, parsed, entry });
46
+ }
47
+ return out.sort((x, y) => newer(x.parsed, y.parsed) ? -1 : 1);
48
+ }
49
+ function bestLocal(opts, bundled, bundledV) {
50
+ const top = cachedVersions(opts.cacheDir).find((c) => c.parsed[0] === bundled[0] && newer(c.parsed, bundled));
51
+ return top ? { entry: top.entry, version: top.version, source: "cache" } : { entry: path.join(opts.bundledDir, "index.js"), version: bundledV, source: "bundled" };
52
+ }
53
+ function extractTgz(tgz, dest) {
54
+ const tar = gunzipSync(tgz);
55
+ const root = path.resolve(dest);
56
+ let off = 0;
57
+ while (off + 512 <= tar.length) {
58
+ const header = tar.subarray(off, off + 512);
59
+ if (header.every((b) => b === 0)) break;
60
+ const field = (start2, len) => header.subarray(start2, start2 + len).toString("utf8").replace(/\0.*$/s, "");
61
+ const size = Number.parseInt(field(124, 12).trim() || "0", 8);
62
+ const type = field(156, 1);
63
+ const prefix = field(345, 155);
64
+ const name = prefix ? `${prefix}/${field(0, 100)}` : field(0, 100);
65
+ const body = tar.subarray(off + 512, off + 512 + size);
66
+ off += 512 + Math.ceil(size / 512) * 512;
67
+ if (type !== "0" && type !== "") continue;
68
+ const target = path.resolve(root, name);
69
+ if (!target.startsWith(root + path.sep)) throw new Error(`tarball entry outside the package: ${name}`);
70
+ fs.mkdirSync(path.dirname(target), { recursive: true });
71
+ fs.writeFileSync(target, body);
72
+ }
73
+ }
74
+ function integrityOk(data, integrity) {
75
+ const sri = integrity.split(/\s+/).find((s) => s.startsWith("sha512-"));
76
+ return Boolean(sri) && createHash("sha512").update(data).digest("base64") === sri.slice("sha512-".length);
77
+ }
78
+ async function fetchWithin(fetchImpl, url, deadline, headers = {}) {
79
+ const res = await fetchImpl(url, { headers, signal: AbortSignal.timeout(Math.max(1, deadline - Date.now())) });
80
+ if (!res.ok) throw new Error(`${url}: HTTP ${res.status}`);
81
+ return res;
82
+ }
83
+ async function download(opts, version, tarball, integrity, deadline) {
84
+ const fetchImpl = opts.fetchImpl ?? fetch;
85
+ const data = Buffer.from(await (await fetchWithin(fetchImpl, tarball, deadline)).arrayBuffer());
86
+ if (!integrityOk(data, integrity)) throw new Error(`integrity mismatch for ${PACKAGE}@${version}`);
87
+ fs.mkdirSync(opts.cacheDir, { recursive: true });
88
+ const tmp = fs.mkdtempSync(path.join(opts.cacheDir, `.${version}-`));
89
+ try {
90
+ extractTgz(data, tmp);
91
+ if (!fs.existsSync(path.join(tmp, ENTRY))) throw new Error(`${PACKAGE}@${version} has no ${ENTRY}`);
92
+ const final = path.join(opts.cacheDir, version);
93
+ fs.rmSync(final, { recursive: true, force: true });
94
+ fs.renameSync(tmp, final);
95
+ return path.join(final, ENTRY);
96
+ } catch (err) {
97
+ fs.rmSync(tmp, { recursive: true, force: true });
98
+ throw err;
99
+ }
100
+ }
101
+ function prune(cacheDir, current) {
102
+ const others = cachedVersions(cacheDir).filter((c) => c.version !== current);
103
+ for (const c of others.slice(KEEP - 1)) fs.rmSync(path.join(cacheDir, c.version), { recursive: true, force: true });
104
+ }
105
+ async function resolveEntry(opts) {
106
+ const log = opts.log ?? (() => void 0);
107
+ const bundledV = bundledVersion(opts.bundledDir);
108
+ const bundled = parseVersion(bundledV);
109
+ if (!bundled) throw new Error(`bundled version is not x.y.z: ${bundledV}`);
110
+ const local = bestLocal(opts, bundled, bundledV);
111
+ if (!opts.enabled) return local;
112
+ const deadline = Date.now() + opts.timeoutMs;
113
+ try {
114
+ const fetchImpl = opts.fetchImpl ?? fetch;
115
+ const res = await fetchWithin(fetchImpl, `${opts.registry}/${PACKAGE.replace("/", "%2f")}`, deadline, { accept: "application/vnd.npm.install-v1+json" });
116
+ const doc = await res.json();
117
+ const latestV = doc["dist-tags"]?.latest ?? "";
118
+ const latest = parseVersion(latestV);
119
+ if (!latest || latest[0] !== bundled[0] || newer(bundled, latest) || latestV === local.version) return local;
120
+ if (latestV === bundledV) return { entry: path.join(opts.bundledDir, "index.js"), version: bundledV, source: "bundled" };
121
+ const cached = cachedVersions(opts.cacheDir).find((c) => c.version === latestV);
122
+ if (cached) return { entry: cached.entry, version: latestV, source: "cache" };
123
+ const dist = doc.versions?.[latestV]?.dist;
124
+ if (!dist?.tarball || !dist.integrity) throw new Error(`${PACKAGE}@${latestV} has no tarball or integrity`);
125
+ const entry = await download(opts, latestV, dist.tarball, dist.integrity, deadline);
126
+ prune(opts.cacheDir, latestV);
127
+ log(`sindri: actualizado a ${latestV} desde npm`);
128
+ return { entry, version: latestV, source: "npm" };
129
+ } catch (err) {
130
+ log(`sindri: sin actualizar (${err instanceof Error ? err.message : String(err)}); uso ${local.version}`);
131
+ return local;
132
+ }
133
+ }
134
+ async function start(entry) {
135
+ process.argv[1] = entry;
136
+ await import(pathToFileURL(entry).href);
137
+ }
138
+ async function main() {
139
+ const opts = optionsFromEnv();
140
+ const log = (m) => process.stderr.write(`${m}
141
+ `);
142
+ const chosen = await resolveEntry({ ...opts, log });
143
+ try {
144
+ await start(chosen.entry);
145
+ } catch (err) {
146
+ if (chosen.source === "bundled") throw err;
147
+ log(`sindri: ${chosen.version} no carg\xF3 (${err instanceof Error ? err.message : String(err)}); uso la versi\xF3n de la extensi\xF3n`);
148
+ await start(path.join(opts.bundledDir, "index.js"));
149
+ }
150
+ }
151
+ if (process.argv[1] && /(^|[\\/])launcher\.(js|ts)$/.test(process.argv[1])) {
152
+ main().catch((err) => {
153
+ process.stderr.write(`sindri: ${err instanceof Error ? err.stack ?? err.message : String(err)}
154
+ `);
155
+ process.exit(1);
156
+ });
157
+ }
158
+ export {
159
+ PACKAGE,
160
+ extractTgz,
161
+ newer,
162
+ optionsFromEnv,
163
+ parseVersion,
164
+ resolveEntry
165
+ };
package/manifest.json CHANGED
@@ -2,19 +2,19 @@
2
2
  "manifest_version": "0.2",
3
3
  "name": "sindri",
4
4
  "display_name": "Kigo Sindri",
5
- "version": "0.1.5",
5
+ "version": "0.1.7",
6
6
  "description": "Create, preview and publish Kigo internal apps from your Claude.",
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.",
7
+ "long_description": "Sindri lets anyone at Kigo 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": {
9
9
  "name": "Kigo"
10
10
  },
11
11
  "server": {
12
12
  "type": "node",
13
- "entry_point": "dist/index.js",
13
+ "entry_point": "dist/launcher.js",
14
14
  "mcp_config": {
15
15
  "command": "node",
16
16
  "args": [
17
- "${__dirname}/dist/index.js"
17
+ "${__dirname}/dist/launcher.js"
18
18
  ],
19
19
  "env": {
20
20
  "SINDRI_HOME": "${user_config.sindri_home}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kigo-dev/sindri",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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",
@@ -16,7 +16,7 @@
16
16
  "node": ">=20"
17
17
  },
18
18
  "scripts": {
19
- "build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\" && chmod +x dist/index.js",
19
+ "build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\" && esbuild src/launcher.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/launcher.js && chmod +x dist/index.js",
20
20
  "typecheck": "tsc --noEmit",
21
21
  "test": "node --import tsx --test test/*.test.ts",
22
22
  "start": "node --import tsx src/index.ts",