@kigo-dev/sindri 0.1.6 → 0.1.8
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 +10 -2
- package/dist/index.js +4 -3
- package/dist/launcher.js +165 -0
- package/manifest.json +3 -3
- package/package.json +2 -2
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
|
|
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@
|
|
58
|
+
claude mcp add sindri -- npx -y @kigo-dev/sindri@latest
|
|
51
59
|
```
|
|
52
60
|
|
|
53
61
|
**Manual check without an MCP client:**
|
package/dist/index.js
CHANGED
|
@@ -36913,6 +36913,7 @@ function readSchedules(manifestText) {
|
|
|
36913
36913
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
36914
36914
|
import http2 from "node:http";
|
|
36915
36915
|
var SECRET_MAX_BYTES = 8 * 1024;
|
|
36916
|
+
var REFERRER_POLICY = "same-origin";
|
|
36916
36917
|
var SecretPageBrowserUnavailable = class extends Error {
|
|
36917
36918
|
constructor() {
|
|
36918
36919
|
super("browser unavailable");
|
|
@@ -36944,7 +36945,7 @@ async function collectSecret(opts) {
|
|
|
36944
36945
|
timer = setTimeout(() => reject(new Error("expired")), opts.timeoutMs ?? 5 * 6e4);
|
|
36945
36946
|
server.on("request", (req, res) => {
|
|
36946
36947
|
const html = (status, body) => {
|
|
36947
|
-
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Referrer-Policy":
|
|
36948
|
+
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Referrer-Policy": REFERRER_POLICY, "X-Frame-Options": "DENY" });
|
|
36948
36949
|
res.end(body);
|
|
36949
36950
|
};
|
|
36950
36951
|
const u = new URL(req.url ?? "/", origin);
|
|
@@ -37041,7 +37042,7 @@ button.primary{background:var(--primary);border-color:var(--primary);color:#fff;
|
|
|
37041
37042
|
.note{font-size:13px;margin-top:1rem}
|
|
37042
37043
|
`;
|
|
37043
37044
|
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="
|
|
37045
|
+
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="${REFERRER_POLICY}"><title>${escapeHtml(title)}</title><style>${STYLE}</style></head><body><main><div class="brand"><i></i>Kigo Sindri</div>${body}</main></body></html>`;
|
|
37045
37046
|
}
|
|
37046
37047
|
function FORM_PAGE(app, name, hint, error61) {
|
|
37047
37048
|
const title = `Guardar secreto para \xAB${app}\xBB`;
|
|
@@ -37909,7 +37910,7 @@ function when(iso, now = /* @__PURE__ */ new Date()) {
|
|
|
37909
37910
|
}
|
|
37910
37911
|
|
|
37911
37912
|
// src/index.ts
|
|
37912
|
-
var VERSION = "0.1.
|
|
37913
|
+
var VERSION = "0.1.8";
|
|
37913
37914
|
function registerTools(server, tools) {
|
|
37914
37915
|
for (const name of Object.keys(SCHEMAS)) {
|
|
37915
37916
|
const meta3 = DESCRIPTIONS[name];
|
package/dist/launcher.js
ADDED
|
@@ -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,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": "0.2",
|
|
3
3
|
"name": "sindri",
|
|
4
4
|
"display_name": "Kigo Sindri",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.8",
|
|
6
6
|
"description": "Create, preview and publish Kigo internal apps from your Claude.",
|
|
7
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": {
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
},
|
|
11
11
|
"server": {
|
|
12
12
|
"type": "node",
|
|
13
|
-
"entry_point": "dist/
|
|
13
|
+
"entry_point": "dist/launcher.js",
|
|
14
14
|
"mcp_config": {
|
|
15
15
|
"command": "node",
|
|
16
16
|
"args": [
|
|
17
|
-
"${__dirname}/dist/
|
|
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.
|
|
3
|
+
"version": "0.1.8",
|
|
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",
|