@kigo-dev/sindri 0.1.0 → 0.1.4
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 +30 -4
- package/dist/index.js +734 -161
- package/manifest.json +76 -15
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3262,8 +3262,8 @@ var require_utils = __commonJS({
|
|
|
3262
3262
|
}
|
|
3263
3263
|
return ind;
|
|
3264
3264
|
}
|
|
3265
|
-
function removeDotSegments(
|
|
3266
|
-
let input2 =
|
|
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
|
|
3676
|
-
wsComponent.path =
|
|
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,
|
|
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,
|
|
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:
|
|
7576
|
-
const fullPath = [...
|
|
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,
|
|
7691
|
+
constructor(parent, value, path10, key) {
|
|
7692
7692
|
this._cachedPath = [];
|
|
7693
7693
|
this.parent = parent;
|
|
7694
7694
|
this.data = value;
|
|
7695
|
-
this._path =
|
|
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,
|
|
11584
|
-
if (!
|
|
11583
|
+
function getElementAtPath(obj, path10) {
|
|
11584
|
+
if (!path10)
|
|
11585
11585
|
return obj;
|
|
11586
|
-
return
|
|
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(
|
|
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(
|
|
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,
|
|
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 }, [...
|
|
12438
|
+
issue2.errors.map((issues) => processError({ issues }, [...path10, ...issue2.path]));
|
|
12439
12439
|
} else if (issue2.code === "invalid_key") {
|
|
12440
|
-
processError({ issues: issue2.issues }, [...
|
|
12440
|
+
processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
|
|
12441
12441
|
} else if (issue2.code === "invalid_element") {
|
|
12442
|
-
processError({ issues: issue2.issues }, [...
|
|
12442
|
+
processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
|
|
12443
12443
|
} else {
|
|
12444
|
-
const fullpath = [...
|
|
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,
|
|
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 }, [...
|
|
12487
|
+
issue2.errors.map((issues) => processError({ issues }, [...path10, ...issue2.path]));
|
|
12488
12488
|
} else if (issue2.code === "invalid_key") {
|
|
12489
|
-
processError({ issues: issue2.issues }, [...
|
|
12489
|
+
processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
|
|
12490
12490
|
} else if (issue2.code === "invalid_element") {
|
|
12491
|
-
processError({ issues: issue2.issues }, [...
|
|
12491
|
+
processError({ issues: issue2.issues }, [...path10, ...issue2.path]);
|
|
12492
12492
|
} else {
|
|
12493
|
-
const fullpath = [...
|
|
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
|
|
12533
|
-
for (const seg of
|
|
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(
|
|
27515
|
-
if (
|
|
27514
|
+
function getDotPath(path10) {
|
|
27515
|
+
if (path10.length === 0) {
|
|
27516
27516
|
return "object root";
|
|
27517
27517
|
}
|
|
27518
|
-
return
|
|
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
|
|
29682
|
-
if (
|
|
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 (
|
|
29687
|
-
const key =
|
|
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
|
}
|
|
@@ -34702,6 +34702,228 @@ var McpZodTypeKind;
|
|
|
34702
34702
|
McpZodTypeKind2["Completable"] = "McpCompletable";
|
|
34703
34703
|
})(McpZodTypeKind || (McpZodTypeKind = {}));
|
|
34704
34704
|
|
|
34705
|
+
// node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@4.5.4/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/uriTemplate.js
|
|
34706
|
+
var MAX_TEMPLATE_LENGTH = 1e6;
|
|
34707
|
+
var MAX_VARIABLE_LENGTH = 1e6;
|
|
34708
|
+
var MAX_TEMPLATE_EXPRESSIONS = 1e4;
|
|
34709
|
+
var MAX_REGEX_LENGTH = 1e6;
|
|
34710
|
+
var UriTemplate = class _UriTemplate {
|
|
34711
|
+
/**
|
|
34712
|
+
* Returns true if the given string contains any URI template expressions.
|
|
34713
|
+
* A template expression is a sequence of characters enclosed in curly braces,
|
|
34714
|
+
* like {foo} or {?bar}.
|
|
34715
|
+
*/
|
|
34716
|
+
static isTemplate(str) {
|
|
34717
|
+
return /\{[^}\s]+\}/.test(str);
|
|
34718
|
+
}
|
|
34719
|
+
static validateLength(str, max, context) {
|
|
34720
|
+
if (str.length > max) {
|
|
34721
|
+
throw new Error(`${context} exceeds maximum length of ${max} characters (got ${str.length})`);
|
|
34722
|
+
}
|
|
34723
|
+
}
|
|
34724
|
+
get variableNames() {
|
|
34725
|
+
return this.parts.flatMap((part) => typeof part === "string" ? [] : part.names);
|
|
34726
|
+
}
|
|
34727
|
+
constructor(template) {
|
|
34728
|
+
_UriTemplate.validateLength(template, MAX_TEMPLATE_LENGTH, "Template");
|
|
34729
|
+
this.template = template;
|
|
34730
|
+
this.parts = this.parse(template);
|
|
34731
|
+
}
|
|
34732
|
+
toString() {
|
|
34733
|
+
return this.template;
|
|
34734
|
+
}
|
|
34735
|
+
parse(template) {
|
|
34736
|
+
const parts = [];
|
|
34737
|
+
let currentText = "";
|
|
34738
|
+
let i = 0;
|
|
34739
|
+
let expressionCount = 0;
|
|
34740
|
+
while (i < template.length) {
|
|
34741
|
+
if (template[i] === "{") {
|
|
34742
|
+
if (currentText) {
|
|
34743
|
+
parts.push(currentText);
|
|
34744
|
+
currentText = "";
|
|
34745
|
+
}
|
|
34746
|
+
const end = template.indexOf("}", i);
|
|
34747
|
+
if (end === -1)
|
|
34748
|
+
throw new Error("Unclosed template expression");
|
|
34749
|
+
expressionCount++;
|
|
34750
|
+
if (expressionCount > MAX_TEMPLATE_EXPRESSIONS) {
|
|
34751
|
+
throw new Error(`Template contains too many expressions (max ${MAX_TEMPLATE_EXPRESSIONS})`);
|
|
34752
|
+
}
|
|
34753
|
+
const expr = template.slice(i + 1, end);
|
|
34754
|
+
const operator = this.getOperator(expr);
|
|
34755
|
+
const exploded = expr.includes("*");
|
|
34756
|
+
const names = this.getNames(expr);
|
|
34757
|
+
const name = names[0];
|
|
34758
|
+
for (const name2 of names) {
|
|
34759
|
+
_UriTemplate.validateLength(name2, MAX_VARIABLE_LENGTH, "Variable name");
|
|
34760
|
+
}
|
|
34761
|
+
parts.push({ name, operator, names, exploded });
|
|
34762
|
+
i = end + 1;
|
|
34763
|
+
} else {
|
|
34764
|
+
currentText += template[i];
|
|
34765
|
+
i++;
|
|
34766
|
+
}
|
|
34767
|
+
}
|
|
34768
|
+
if (currentText) {
|
|
34769
|
+
parts.push(currentText);
|
|
34770
|
+
}
|
|
34771
|
+
return parts;
|
|
34772
|
+
}
|
|
34773
|
+
getOperator(expr) {
|
|
34774
|
+
const operators = ["+", "#", ".", "/", "?", "&"];
|
|
34775
|
+
return operators.find((op) => expr.startsWith(op)) || "";
|
|
34776
|
+
}
|
|
34777
|
+
getNames(expr) {
|
|
34778
|
+
const operator = this.getOperator(expr);
|
|
34779
|
+
return expr.slice(operator.length).split(",").map((name) => name.replace("*", "").trim()).filter((name) => name.length > 0);
|
|
34780
|
+
}
|
|
34781
|
+
encodeValue(value, operator) {
|
|
34782
|
+
_UriTemplate.validateLength(value, MAX_VARIABLE_LENGTH, "Variable value");
|
|
34783
|
+
if (operator === "+" || operator === "#") {
|
|
34784
|
+
return encodeURI(value);
|
|
34785
|
+
}
|
|
34786
|
+
return encodeURIComponent(value);
|
|
34787
|
+
}
|
|
34788
|
+
expandPart(part, variables) {
|
|
34789
|
+
if (part.operator === "?" || part.operator === "&") {
|
|
34790
|
+
const pairs = part.names.map((name) => {
|
|
34791
|
+
const value2 = variables[name];
|
|
34792
|
+
if (value2 === void 0)
|
|
34793
|
+
return "";
|
|
34794
|
+
const encoded2 = Array.isArray(value2) ? value2.map((v) => this.encodeValue(v, part.operator)).join(",") : this.encodeValue(value2.toString(), part.operator);
|
|
34795
|
+
return `${name}=${encoded2}`;
|
|
34796
|
+
}).filter((pair) => pair.length > 0);
|
|
34797
|
+
if (pairs.length === 0)
|
|
34798
|
+
return "";
|
|
34799
|
+
const separator = part.operator === "?" ? "?" : "&";
|
|
34800
|
+
return separator + pairs.join("&");
|
|
34801
|
+
}
|
|
34802
|
+
if (part.names.length > 1) {
|
|
34803
|
+
const values2 = part.names.map((name) => variables[name]).filter((v) => v !== void 0);
|
|
34804
|
+
if (values2.length === 0)
|
|
34805
|
+
return "";
|
|
34806
|
+
return values2.map((v) => Array.isArray(v) ? v[0] : v).join(",");
|
|
34807
|
+
}
|
|
34808
|
+
const value = variables[part.name];
|
|
34809
|
+
if (value === void 0)
|
|
34810
|
+
return "";
|
|
34811
|
+
const values = Array.isArray(value) ? value : [value];
|
|
34812
|
+
const encoded = values.map((v) => this.encodeValue(v, part.operator));
|
|
34813
|
+
switch (part.operator) {
|
|
34814
|
+
case "":
|
|
34815
|
+
return encoded.join(",");
|
|
34816
|
+
case "+":
|
|
34817
|
+
return encoded.join(",");
|
|
34818
|
+
case "#":
|
|
34819
|
+
return "#" + encoded.join(",");
|
|
34820
|
+
case ".":
|
|
34821
|
+
return "." + encoded.join(".");
|
|
34822
|
+
case "/":
|
|
34823
|
+
return "/" + encoded.join("/");
|
|
34824
|
+
default:
|
|
34825
|
+
return encoded.join(",");
|
|
34826
|
+
}
|
|
34827
|
+
}
|
|
34828
|
+
expand(variables) {
|
|
34829
|
+
let result = "";
|
|
34830
|
+
let hasQueryParam = false;
|
|
34831
|
+
for (const part of this.parts) {
|
|
34832
|
+
if (typeof part === "string") {
|
|
34833
|
+
result += part;
|
|
34834
|
+
continue;
|
|
34835
|
+
}
|
|
34836
|
+
const expanded = this.expandPart(part, variables);
|
|
34837
|
+
if (!expanded)
|
|
34838
|
+
continue;
|
|
34839
|
+
if ((part.operator === "?" || part.operator === "&") && hasQueryParam) {
|
|
34840
|
+
result += expanded.replace("?", "&");
|
|
34841
|
+
} else {
|
|
34842
|
+
result += expanded;
|
|
34843
|
+
}
|
|
34844
|
+
if (part.operator === "?" || part.operator === "&") {
|
|
34845
|
+
hasQueryParam = true;
|
|
34846
|
+
}
|
|
34847
|
+
}
|
|
34848
|
+
return result;
|
|
34849
|
+
}
|
|
34850
|
+
escapeRegExp(str) {
|
|
34851
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
34852
|
+
}
|
|
34853
|
+
partToRegExp(part) {
|
|
34854
|
+
const patterns = [];
|
|
34855
|
+
for (const name2 of part.names) {
|
|
34856
|
+
_UriTemplate.validateLength(name2, MAX_VARIABLE_LENGTH, "Variable name");
|
|
34857
|
+
}
|
|
34858
|
+
if (part.operator === "?" || part.operator === "&") {
|
|
34859
|
+
for (let i = 0; i < part.names.length; i++) {
|
|
34860
|
+
const name2 = part.names[i];
|
|
34861
|
+
const prefix = i === 0 ? "\\" + part.operator : "&";
|
|
34862
|
+
patterns.push({
|
|
34863
|
+
pattern: prefix + this.escapeRegExp(name2) + "=([^&]+)",
|
|
34864
|
+
name: name2
|
|
34865
|
+
});
|
|
34866
|
+
}
|
|
34867
|
+
return patterns;
|
|
34868
|
+
}
|
|
34869
|
+
let pattern;
|
|
34870
|
+
const name = part.name;
|
|
34871
|
+
switch (part.operator) {
|
|
34872
|
+
case "":
|
|
34873
|
+
pattern = part.exploded ? "([^/,]+(?:,[^/,]+)*)" : "([^/,]+)";
|
|
34874
|
+
break;
|
|
34875
|
+
case "+":
|
|
34876
|
+
case "#":
|
|
34877
|
+
pattern = "(.+)";
|
|
34878
|
+
break;
|
|
34879
|
+
case ".":
|
|
34880
|
+
pattern = "\\.([^/,]+)";
|
|
34881
|
+
break;
|
|
34882
|
+
case "/":
|
|
34883
|
+
pattern = "/" + (part.exploded ? "([^/,]+(?:,[^/,]+)*)" : "([^/,]+)");
|
|
34884
|
+
break;
|
|
34885
|
+
default:
|
|
34886
|
+
pattern = "([^/]+)";
|
|
34887
|
+
}
|
|
34888
|
+
patterns.push({ pattern, name });
|
|
34889
|
+
return patterns;
|
|
34890
|
+
}
|
|
34891
|
+
match(uri) {
|
|
34892
|
+
_UriTemplate.validateLength(uri, MAX_TEMPLATE_LENGTH, "URI");
|
|
34893
|
+
let pattern = "^";
|
|
34894
|
+
const names = [];
|
|
34895
|
+
for (const part of this.parts) {
|
|
34896
|
+
if (typeof part === "string") {
|
|
34897
|
+
pattern += this.escapeRegExp(part);
|
|
34898
|
+
} else {
|
|
34899
|
+
const patterns = this.partToRegExp(part);
|
|
34900
|
+
for (const { pattern: partPattern, name } of patterns) {
|
|
34901
|
+
pattern += partPattern;
|
|
34902
|
+
names.push({ name, exploded: part.exploded });
|
|
34903
|
+
}
|
|
34904
|
+
}
|
|
34905
|
+
}
|
|
34906
|
+
pattern += "$";
|
|
34907
|
+
_UriTemplate.validateLength(pattern, MAX_REGEX_LENGTH, "Generated regex pattern");
|
|
34908
|
+
const regex = new RegExp(pattern);
|
|
34909
|
+
const match = uri.match(regex);
|
|
34910
|
+
if (!match)
|
|
34911
|
+
return null;
|
|
34912
|
+
const result = {};
|
|
34913
|
+
for (let i = 0; i < names.length; i++) {
|
|
34914
|
+
const { name, exploded } = names[i];
|
|
34915
|
+
const value = match[i + 1];
|
|
34916
|
+
const cleanName = name.replace("*", "");
|
|
34917
|
+
if (exploded && value.includes(",")) {
|
|
34918
|
+
result[cleanName] = value.split(",");
|
|
34919
|
+
} else {
|
|
34920
|
+
result[cleanName] = value;
|
|
34921
|
+
}
|
|
34922
|
+
}
|
|
34923
|
+
return result;
|
|
34924
|
+
}
|
|
34925
|
+
};
|
|
34926
|
+
|
|
34705
34927
|
// node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@4.5.4/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/toolNameValidation.js
|
|
34706
34928
|
var TOOL_NAME_REGEX = /^[A-Za-z0-9._-]{1,128}$/;
|
|
34707
34929
|
function validateToolName(name) {
|
|
@@ -35491,6 +35713,30 @@ var McpServer = class {
|
|
|
35491
35713
|
}
|
|
35492
35714
|
}
|
|
35493
35715
|
};
|
|
35716
|
+
var ResourceTemplate = class {
|
|
35717
|
+
constructor(uriTemplate, _callbacks) {
|
|
35718
|
+
this._callbacks = _callbacks;
|
|
35719
|
+
this._uriTemplate = typeof uriTemplate === "string" ? new UriTemplate(uriTemplate) : uriTemplate;
|
|
35720
|
+
}
|
|
35721
|
+
/**
|
|
35722
|
+
* Gets the URI template pattern.
|
|
35723
|
+
*/
|
|
35724
|
+
get uriTemplate() {
|
|
35725
|
+
return this._uriTemplate;
|
|
35726
|
+
}
|
|
35727
|
+
/**
|
|
35728
|
+
* Gets the list callback, if one was provided.
|
|
35729
|
+
*/
|
|
35730
|
+
get listCallback() {
|
|
35731
|
+
return this._callbacks.list;
|
|
35732
|
+
}
|
|
35733
|
+
/**
|
|
35734
|
+
* Gets the callback for completing a specific URI template variable, if one was provided.
|
|
35735
|
+
*/
|
|
35736
|
+
completeCallback(variable) {
|
|
35737
|
+
return this._callbacks.complete?.[variable];
|
|
35738
|
+
}
|
|
35739
|
+
};
|
|
35494
35740
|
var EMPTY_OBJECT_JSON_SCHEMA = {
|
|
35495
35741
|
type: "object",
|
|
35496
35742
|
properties: {}
|
|
@@ -35677,6 +35923,7 @@ var StdioServerTransport = class {
|
|
|
35677
35923
|
// src/config.ts
|
|
35678
35924
|
import os from "node:os";
|
|
35679
35925
|
import path from "node:path";
|
|
35926
|
+
var SPACE_URL = "https://sindri.kigo.dev";
|
|
35680
35927
|
var DEFAULTS = {
|
|
35681
35928
|
apiUrl: "https://api.sindri.kigo.dev",
|
|
35682
35929
|
issuer: "https://id.kigo.dev",
|
|
@@ -35719,18 +35966,25 @@ var INSTRUCTIONS = `You are helping a Kigo product manager (PM) build small inte
|
|
|
35719
35966
|
|
|
35720
35967
|
Language and tone: always Spanish, "t\xFA", short sentences. Say "app" (feminine: "la app", "tu app"), never "herramienta", "proyecto", "repo", "deploy" or "build". Never paste file paths, commands, logs or stack traces; summarize in one line what happened and what you will do. The \`summary\` field of every tool result is written for the PM: repeat it as is or paraphrase it.
|
|
35721
35968
|
|
|
35969
|
+
First contact: the first time the PM speaks in a conversation, or whenever they ask what Sindri is or can do, call \`help\` and relay its \`summary\` (that is the introduction: what Sindri is, what they can ask for, the limits, the link to Mi espacio), then ask what they want to build. Do not repeat it later in the same conversation. When the PM asks where to see their apps or their versions, give them the Mi espacio link (\`space_url\`, https://sindri.kigo.dev).
|
|
35970
|
+
|
|
35722
35971
|
Flow:
|
|
35723
35972
|
1. First time on a machine: run \`setup\` (installs Node and pnpm without admin rights) and \`login\` (opens the browser on Kigo's sign-in page; the PM enters with their @cargomovil.com Google account and the browser comes back on its own with "Listo, ya entraste a Sindri"; tell them to return to Claude). \`login\` waits up to five minutes. If any tool answers \`needs_setup\` or \`needs_login\`, run that tool and retry once.
|
|
35724
35973
|
2. New app: ask at most one question if the request is vague (for whom, what it must show), then \`create_app\` with a short lowercase name (it becomes the address https://<name>.sindri.kigo.dev) and a one-sentence description in Spanish. Then edit the files in the folder the tool returns to build what the PM asked. The template is Next.js + Drizzle; read its CLAUDE.md first; keep the UI in Spanish, "t\xFA", with the Kigo components it ships.
|
|
35725
35974
|
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.
|
|
35975
|
+
4. Ship: \`publish\` with a message in the PM's words ("Filtro por fecha"). Give the URL and say the team enters with their Google de Kigo. If publish fails, the published version did not change: say what broke in one line, fix it, publish again. The published app has its own database: publish runs the migrations and, when this version changed drizzle/ or src/db/seed.ts, loads the test data again; rows the PM typed into the local preview never travel. Keep test data in the seed, not in the UI.
|
|
35727
35976
|
5. Later: \`status\` before working on an existing app (tells you whether the local folder is behind or has unpublished edits); \`versions\` and \`rollback\` when the PM wants "como estaba ayer"; \`share\` for the Slack line.
|
|
35977
|
+
6. Template updates: when the PM says "actualiza la plantilla" or Sindri announces a template fix, run \`update_template\`. It replaces only the files the app never customised, publishes \xABPlantilla actualizada\xBB and brings the local folder to it; the files you had changed are listed in \`skipped\` so you can apply the same change there by hand if it matters. It refuses while there are unpublished local edits: publish them first (or tell the PM what would be lost) and retry.
|
|
35978
|
+
7. Renaming: an app has an address (https://<name>.sindri.kigo.dev) and a visible name (the title people see). When the PM says "c\xE1mbiale el nombre" and it is not obvious which one they mean, ask: "\xBFla direcci\xF3n o el nombre que se ve?". \`rename_app\` with \`new_name\` changes the address (Sindri renames it, the local folder follows, the old link stops working: tell the PM to share the new one); it refuses while there are unpublished edits, so publish first. \`rename_app\` with \`display_name\` changes only the visible name and publishes it as \xABAhora se llama \xAB<nombre>\xBB\xBB. Both in one call is fine.
|
|
35979
|
+
8. Deleting: \`delete_app\` is final in Sindri (address, versions, database). Before calling it, ask the PM to type the app's name to confirm and pass exactly what they wrote as \`confirm\`; never fill it in yourself, never guess it, never call it because you inferred they want it. The local files are moved to the Sindri papelera on their computer, not deleted; say so.
|
|
35980
|
+
|
|
35981
|
+
Design guide: before creating or changing any page, component, layout, style or copy, call \`design_guide\` for the app, first without \`topic\` (the overview and the list of topics), then with the topic that matches what you are building (admin, landing, mini-app, mobile; tokens, writing and checklist when relevant). Follow it; it is for you, never paste its content to the PM. If it answers \`no_design_guide\`, run \`update_template\` once and call \`design_guide\` again.
|
|
35728
35982
|
|
|
35729
35983
|
Rules: synthetic data only (never paste real emails, plates or phone numbers into seeds); one author per app (the PM); never touch files outside the app folder; do not edit middleware.ts, next.config.ts or package.json scripts (publish will refuse them); apps work Monday to Friday 9:05 am to midnight Mexico City time and the link says so outside those hours. When something fails, explain in one line what to do next; never blame the PM.`;
|
|
35730
35984
|
|
|
35731
35985
|
// src/tools.ts
|
|
35732
|
-
import
|
|
35733
|
-
import
|
|
35986
|
+
import fs9 from "node:fs/promises";
|
|
35987
|
+
import path9 from "node:path";
|
|
35734
35988
|
|
|
35735
35989
|
// src/api.ts
|
|
35736
35990
|
import fs3 from "node:fs/promises";
|
|
@@ -35814,8 +36068,7 @@ var KEYCHAIN_SERVICE = "sindri";
|
|
|
35814
36068
|
async function loadCredentials(cfg) {
|
|
35815
36069
|
if (cfg.credentialStore === "keychain") {
|
|
35816
36070
|
const r = await run("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", cfg.issuer, "-w"], { timeoutMs: 1e4 });
|
|
35817
|
-
if (r.code
|
|
35818
|
-
return parse3(r.stdout.trim());
|
|
36071
|
+
if (r.code === 0) return parse3(r.stdout.trim());
|
|
35819
36072
|
}
|
|
35820
36073
|
try {
|
|
35821
36074
|
return parse3(await fs2.readFile(credentialsFile(cfg), "utf8"));
|
|
@@ -35861,7 +36114,7 @@ async function deviceStart(cfg, fetchImpl = fetch) {
|
|
|
35861
36114
|
if (!discovery.device_authorization_endpoint) throw new Error("Identity does not offer the device flow");
|
|
35862
36115
|
const body = new URLSearchParams({
|
|
35863
36116
|
client_id: cfg.clientId,
|
|
35864
|
-
scope:
|
|
36117
|
+
scope: SCOPES,
|
|
35865
36118
|
audience: cfg.audience
|
|
35866
36119
|
});
|
|
35867
36120
|
const res = await fetchImpl(discovery.device_authorization_endpoint, {
|
|
@@ -35903,12 +36156,16 @@ async function devicePoll(cfg, discovery, start, opts = {}) {
|
|
|
35903
36156
|
}
|
|
35904
36157
|
throw new Error("expired");
|
|
35905
36158
|
}
|
|
36159
|
+
var SCOPES = "openid profile email offline_access";
|
|
35906
36160
|
async function refresh(cfg, creds, fetchImpl = fetch) {
|
|
35907
36161
|
if (!creds.refresh_token) throw new NeedsLogin();
|
|
35908
36162
|
const discovery = await discover(cfg, fetchImpl);
|
|
35909
36163
|
const res = await fetchImpl(discovery.token_endpoint, {
|
|
35910
36164
|
method: "POST",
|
|
35911
36165
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
36166
|
+
// No `scope` here: Identity rejects it on refresh (invalid_scope). It also issues the refreshed
|
|
36167
|
+
// token without scopes (bug reported); the API copes by remembering the email per subject and
|
|
36168
|
+
// `no_email` is treated as needs_login (api.ts).
|
|
35912
36169
|
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: creds.refresh_token, client_id: cfg.clientId })
|
|
35913
36170
|
});
|
|
35914
36171
|
if (!res.ok) throw new NeedsLogin();
|
|
@@ -35977,7 +36234,7 @@ async function authCodeLogin(cfg, opts) {
|
|
|
35977
36234
|
authorize.search = new URLSearchParams({
|
|
35978
36235
|
client_id: cfg.clientId,
|
|
35979
36236
|
response_type: "code",
|
|
35980
|
-
scope:
|
|
36237
|
+
scope: SCOPES,
|
|
35981
36238
|
redirect_uri: redirectUri,
|
|
35982
36239
|
state,
|
|
35983
36240
|
code_challenge: challenge,
|
|
@@ -36095,6 +36352,18 @@ var ApiClient = class {
|
|
|
36095
36352
|
rollback(name, version2) {
|
|
36096
36353
|
return this.json("POST", `/v1/apps/${enc(name)}/rollback`, { version: version2 }, 12e4);
|
|
36097
36354
|
}
|
|
36355
|
+
/** Brings the app's never-customised files up to the current template; `skipped` are the ones Claude changed. */
|
|
36356
|
+
updateTemplate(name) {
|
|
36357
|
+
return this.json("POST", `/v1/apps/${enc(name)}/template`, {}, 32e4);
|
|
36358
|
+
}
|
|
36359
|
+
/** Changes the app's address; the builder renames branch and folder, the old address stops resolving. */
|
|
36360
|
+
rename(name, newName) {
|
|
36361
|
+
return this.json("POST", `/v1/apps/${enc(name)}/rename`, { new_name: newName }, 15e4);
|
|
36362
|
+
}
|
|
36363
|
+
/** Removes the app from Sindri. `confirm` must equal the name; the API refuses otherwise. */
|
|
36364
|
+
deleteApp(name, confirm) {
|
|
36365
|
+
return this.json("DELETE", `/v1/apps/${enc(name)}`, { confirm }, 12e4);
|
|
36366
|
+
}
|
|
36098
36367
|
share(name) {
|
|
36099
36368
|
return this.json("GET", `/v1/apps/${enc(name)}/share`);
|
|
36100
36369
|
}
|
|
@@ -36143,6 +36412,7 @@ var ApiClient = class {
|
|
|
36143
36412
|
const raw = await res.text();
|
|
36144
36413
|
try {
|
|
36145
36414
|
const j = JSON.parse(raw);
|
|
36415
|
+
if (res.status === 403 && j.code === "no_email") return new NeedsLogin();
|
|
36146
36416
|
if (j.error) return new ApiError(res.status, j.error, j.code);
|
|
36147
36417
|
} catch {
|
|
36148
36418
|
}
|
|
@@ -36203,99 +36473,87 @@ async function appPresent(p) {
|
|
|
36203
36473
|
return false;
|
|
36204
36474
|
}
|
|
36205
36475
|
}
|
|
36206
|
-
|
|
36207
|
-
|
|
36208
|
-
|
|
36209
|
-
|
|
36210
|
-
import path5 from "node:path";
|
|
36211
|
-
function registryFile(cfg) {
|
|
36212
|
-
return path5.join(cfg.stateDir, "previews.json");
|
|
36213
|
-
}
|
|
36214
|
-
async function readRegistry(cfg) {
|
|
36476
|
+
var DESIGN_GUIDE_REL = path4.join(".claude", "skills", "kigo-sindri-design");
|
|
36477
|
+
async function readDesignGuide(p, topic) {
|
|
36478
|
+
const dir = path4.join(p.appDir, DESIGN_GUIDE_REL);
|
|
36479
|
+
let skill;
|
|
36215
36480
|
try {
|
|
36216
|
-
|
|
36481
|
+
skill = await fs4.readFile(path4.join(dir, "SKILL.md"), "utf8");
|
|
36217
36482
|
} catch {
|
|
36218
|
-
return
|
|
36483
|
+
return null;
|
|
36219
36484
|
}
|
|
36485
|
+
const topics = await designTopics(dir);
|
|
36486
|
+
if (topic === void 0) return { content: skill, topics };
|
|
36487
|
+
if (!topics.includes(topic)) return { unknown: true, topics };
|
|
36488
|
+
return { content: await fs4.readFile(path4.join(dir, "references", `${topic}.md`), "utf8"), topics };
|
|
36220
36489
|
}
|
|
36221
|
-
async function
|
|
36222
|
-
|
|
36223
|
-
|
|
36224
|
-
|
|
36225
|
-
|
|
36226
|
-
|
|
36227
|
-
if (await portFree(p)) return p;
|
|
36490
|
+
async function designTopics(dir) {
|
|
36491
|
+
try {
|
|
36492
|
+
const entries = await fs4.readdir(path4.join(dir, "references"), { withFileTypes: true });
|
|
36493
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".md") && !e.name.startsWith(".")).map((e) => e.name.slice(0, -".md".length)).sort();
|
|
36494
|
+
} catch {
|
|
36495
|
+
return [];
|
|
36228
36496
|
}
|
|
36229
|
-
throw new Error("no free port between 3000 and 3099");
|
|
36230
36497
|
}
|
|
36231
|
-
function
|
|
36232
|
-
|
|
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) {
|
|
36498
|
+
async function linkDesignGuide(p) {
|
|
36499
|
+
const target = path4.join(p.appDir, DESIGN_GUIDE_REL);
|
|
36239
36500
|
try {
|
|
36240
|
-
|
|
36241
|
-
return res.ok;
|
|
36501
|
+
if (!(await fs4.stat(target)).isDirectory()) return false;
|
|
36242
36502
|
} catch {
|
|
36243
36503
|
return false;
|
|
36244
36504
|
}
|
|
36505
|
+
const link = path4.join(p.root, DESIGN_GUIDE_REL);
|
|
36506
|
+
const rel = path4.relative(path4.dirname(link), target);
|
|
36507
|
+
for (const parent of [path4.join(p.root, ".claude"), path4.dirname(link)]) {
|
|
36508
|
+
const st = await fs4.lstat(parent).catch(() => null);
|
|
36509
|
+
if (st && !st.isDirectory()) return false;
|
|
36510
|
+
}
|
|
36511
|
+
const existing = await fs4.lstat(link).catch(() => null);
|
|
36512
|
+
if (existing) {
|
|
36513
|
+
if (!existing.isSymbolicLink()) return false;
|
|
36514
|
+
if (await fs4.readlink(link) === rel) return true;
|
|
36515
|
+
await fs4.unlink(link);
|
|
36516
|
+
}
|
|
36517
|
+
await fs4.mkdir(path4.dirname(link), { recursive: true });
|
|
36518
|
+
await fs4.symlink(rel, link, "dir");
|
|
36519
|
+
return true;
|
|
36245
36520
|
}
|
|
36246
|
-
|
|
36247
|
-
|
|
36248
|
-
|
|
36249
|
-
|
|
36250
|
-
|
|
36251
|
-
|
|
36252
|
-
|
|
36253
|
-
|
|
36254
|
-
|
|
36255
|
-
|
|
36256
|
-
|
|
36257
|
-
|
|
36258
|
-
|
|
36259
|
-
|
|
36260
|
-
|
|
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);
|
|
36521
|
+
|
|
36522
|
+
// src/manifest.ts
|
|
36523
|
+
import fs5 from "node:fs/promises";
|
|
36524
|
+
import path5 from "node:path";
|
|
36525
|
+
var MANIFEST_FILE = "kigo-app.yaml";
|
|
36526
|
+
var KEY_RE = /^([a-z_]+):\s*(.*)$/;
|
|
36527
|
+
function setManifestValue(text, key, value) {
|
|
36528
|
+
const line = `${key}: ${JSON.stringify(value)}`;
|
|
36529
|
+
const lines = text.split("\n");
|
|
36530
|
+
const idx = lines.findIndex((l) => KEY_RE.exec(l)?.[1] === key);
|
|
36531
|
+
if (idx >= 0) {
|
|
36532
|
+
lines[idx] = line;
|
|
36533
|
+
} else {
|
|
36534
|
+
if (lines.at(-1) === "") lines.splice(lines.length - 1, 0, line);
|
|
36535
|
+
else lines.push(line, "");
|
|
36274
36536
|
}
|
|
36275
|
-
|
|
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") };
|
|
36537
|
+
return lines.join("\n");
|
|
36280
36538
|
}
|
|
36281
|
-
async function
|
|
36282
|
-
|
|
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;
|
|
36539
|
+
async function readManifest(appDir) {
|
|
36540
|
+
return fs5.readFile(path5.join(appDir, MANIFEST_FILE), "utf8").catch(() => "");
|
|
36289
36541
|
}
|
|
36290
|
-
async function
|
|
36291
|
-
|
|
36292
|
-
await
|
|
36542
|
+
async function writeManifestValue(appDir, key, value) {
|
|
36543
|
+
const text = await readManifest(appDir);
|
|
36544
|
+
await fs5.writeFile(path5.join(appDir, MANIFEST_FILE), setManifestValue(text, key, value));
|
|
36293
36545
|
}
|
|
36294
36546
|
|
|
36547
|
+
// src/preview.ts
|
|
36548
|
+
import fs7 from "node:fs/promises";
|
|
36549
|
+
import net from "node:net";
|
|
36550
|
+
import path7 from "node:path";
|
|
36551
|
+
|
|
36295
36552
|
// src/toolchain.ts
|
|
36296
36553
|
import fs6 from "node:fs/promises";
|
|
36297
36554
|
import path6 from "node:path";
|
|
36298
36555
|
var MIN_NODE = 20;
|
|
36556
|
+
var PNPM_ARGS = ["--config.verify-deps-before-run=false"];
|
|
36299
36557
|
async function resolveToolchain(cfg, env = process.env) {
|
|
36300
36558
|
const fnmBin = await fnmDefaultBin(cfg);
|
|
36301
36559
|
if (fnmBin) {
|
|
@@ -36387,11 +36645,96 @@ async function installFnmLts(cfg, env) {
|
|
|
36387
36645
|
if (prep.code !== 0) throw new Error(`corepack prepare pnpm failed: ${prep.stderr.trim().slice(-300)}`);
|
|
36388
36646
|
}
|
|
36389
36647
|
|
|
36648
|
+
// src/preview.ts
|
|
36649
|
+
function registryFile(cfg) {
|
|
36650
|
+
return path7.join(cfg.stateDir, "previews.json");
|
|
36651
|
+
}
|
|
36652
|
+
async function readRegistry(cfg) {
|
|
36653
|
+
try {
|
|
36654
|
+
return JSON.parse(await fs7.readFile(registryFile(cfg), "utf8"));
|
|
36655
|
+
} catch {
|
|
36656
|
+
return {};
|
|
36657
|
+
}
|
|
36658
|
+
}
|
|
36659
|
+
async function writeRegistry(cfg, reg) {
|
|
36660
|
+
await fs7.mkdir(cfg.stateDir, { recursive: true, mode: 448 });
|
|
36661
|
+
await fs7.writeFile(registryFile(cfg), JSON.stringify(reg, null, 2) + "\n");
|
|
36662
|
+
}
|
|
36663
|
+
async function freePort(from = 3e3, to = 3099) {
|
|
36664
|
+
for (let p = from; p <= to; p++) {
|
|
36665
|
+
if (await portFree(p)) return p;
|
|
36666
|
+
}
|
|
36667
|
+
throw new Error("no free port between 3000 and 3099");
|
|
36668
|
+
}
|
|
36669
|
+
function portFree(port) {
|
|
36670
|
+
return new Promise((resolve) => {
|
|
36671
|
+
const srv = net.createServer();
|
|
36672
|
+
srv.once("error", () => resolve(false));
|
|
36673
|
+
srv.listen(port, "127.0.0.1", () => srv.close(() => resolve(true)));
|
|
36674
|
+
});
|
|
36675
|
+
}
|
|
36676
|
+
async function healthy(port, fetchImpl = fetch) {
|
|
36677
|
+
try {
|
|
36678
|
+
const res = await fetchImpl(`http://127.0.0.1:${port}/healthz`, { signal: AbortSignal.timeout(3e3) });
|
|
36679
|
+
return res.ok;
|
|
36680
|
+
} catch {
|
|
36681
|
+
return false;
|
|
36682
|
+
}
|
|
36683
|
+
}
|
|
36684
|
+
async function runningPreview(cfg, app, fetchImpl = fetch) {
|
|
36685
|
+
const reg = await readRegistry(cfg);
|
|
36686
|
+
const e = reg[app];
|
|
36687
|
+
if (!e) return null;
|
|
36688
|
+
if (alive(e.pid) && await healthy(e.port, fetchImpl)) return e;
|
|
36689
|
+
delete reg[app];
|
|
36690
|
+
await writeRegistry(cfg, reg);
|
|
36691
|
+
return null;
|
|
36692
|
+
}
|
|
36693
|
+
async function startPreview(cfg, tc, app, dir, opts = {}) {
|
|
36694
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
36695
|
+
const existing = await runningPreview(cfg, app, fetchImpl);
|
|
36696
|
+
if (existing) return { port: existing.port, url: `http://127.0.0.1:${existing.port}`, reused: true };
|
|
36697
|
+
const port = await freePort();
|
|
36698
|
+
await fs7.mkdir(path7.join(dir, ".sindri"), { recursive: true });
|
|
36699
|
+
const logFile = path7.join(dir, ".sindri", "preview.log");
|
|
36700
|
+
await fs7.writeFile(logFile, "");
|
|
36701
|
+
const env = { ...tc.env, KIGO_ENV: "local", DATABASE_URL: "", PORT: String(port), CI: "1", FORCE_COLOR: "0", BROWSER: "none" };
|
|
36702
|
+
const pid = spawnDetached(tc.pnpm, [...PNPM_ARGS, "dev", "--port", String(port)], { cwd: dir, env, logFile });
|
|
36703
|
+
const reg = await readRegistry(cfg);
|
|
36704
|
+
reg[app] = { pid, port, dir, started_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
36705
|
+
await writeRegistry(cfg, reg);
|
|
36706
|
+
const deadline = Date.now() + (opts.waitMs ?? 6e4);
|
|
36707
|
+
const sleepImpl = opts.sleepImpl ?? sleep;
|
|
36708
|
+
while (Date.now() < deadline) {
|
|
36709
|
+
if (await healthy(port, fetchImpl)) return { port, url: `http://127.0.0.1:${port}`, reused: false };
|
|
36710
|
+
if (!alive(pid)) break;
|
|
36711
|
+
await sleepImpl(1e3);
|
|
36712
|
+
}
|
|
36713
|
+
killTree(pid);
|
|
36714
|
+
delete reg[app];
|
|
36715
|
+
await writeRegistry(cfg, reg);
|
|
36716
|
+
const log = await fs7.readFile(logFile, "utf8").catch(() => "");
|
|
36717
|
+
return { port, url: `http://127.0.0.1:${port}`, reused: false, log: log.split("\n").slice(-15).join("\n") };
|
|
36718
|
+
}
|
|
36719
|
+
async function stopPreview(cfg, app) {
|
|
36720
|
+
const reg = await readRegistry(cfg);
|
|
36721
|
+
const e = reg[app];
|
|
36722
|
+
if (!e) return false;
|
|
36723
|
+
killTree(e.pid);
|
|
36724
|
+
delete reg[app];
|
|
36725
|
+
await writeRegistry(cfg, reg);
|
|
36726
|
+
return true;
|
|
36727
|
+
}
|
|
36728
|
+
async function openInBrowser(cfg, url2) {
|
|
36729
|
+
if (!cfg.opener) return;
|
|
36730
|
+
await run(cfg.opener, [url2], { timeoutMs: 1e4 });
|
|
36731
|
+
}
|
|
36732
|
+
|
|
36390
36733
|
// src/tree.ts
|
|
36391
36734
|
import { createHash as createHash2 } from "node:crypto";
|
|
36392
|
-
import
|
|
36735
|
+
import fs8 from "node:fs/promises";
|
|
36393
36736
|
import os3 from "node:os";
|
|
36394
|
-
import
|
|
36737
|
+
import path8 from "node:path";
|
|
36395
36738
|
var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".sindri", "dist", ".turbo", ".git"]);
|
|
36396
36739
|
function excluded(name) {
|
|
36397
36740
|
if (EXCLUDED_DIRS.has(name)) return true;
|
|
@@ -36402,11 +36745,11 @@ function excluded(name) {
|
|
|
36402
36745
|
async function listFiles(root) {
|
|
36403
36746
|
const out = [];
|
|
36404
36747
|
async function walk(dir, rel) {
|
|
36405
|
-
const entries = await
|
|
36748
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
36406
36749
|
for (const e of entries) {
|
|
36407
36750
|
if (excluded(e.name)) continue;
|
|
36408
36751
|
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
36409
|
-
if (e.isDirectory()) await walk(
|
|
36752
|
+
if (e.isDirectory()) await walk(path8.join(dir, e.name), r);
|
|
36410
36753
|
else if (e.isFile()) out.push(r);
|
|
36411
36754
|
}
|
|
36412
36755
|
}
|
|
@@ -36417,7 +36760,7 @@ async function treeHash(root) {
|
|
|
36417
36760
|
const files = await listFiles(root);
|
|
36418
36761
|
const h = createHash2("sha256");
|
|
36419
36762
|
for (const rel of files) {
|
|
36420
|
-
const fh = createHash2("sha256").update(await
|
|
36763
|
+
const fh = createHash2("sha256").update(await fs8.readFile(path8.join(root, rel))).digest("hex");
|
|
36421
36764
|
h.update(`${rel}\0${fh}
|
|
36422
36765
|
`);
|
|
36423
36766
|
}
|
|
@@ -36426,49 +36769,49 @@ async function treeHash(root) {
|
|
|
36426
36769
|
async function packTree(root) {
|
|
36427
36770
|
const files = await listFiles(root);
|
|
36428
36771
|
if (files.length === 0) throw new Error("empty tree");
|
|
36429
|
-
const listFile =
|
|
36430
|
-
await
|
|
36431
|
-
const out =
|
|
36772
|
+
const listFile = path8.join(await fs8.mkdtemp(path8.join(os3.tmpdir(), "sindri-pack-")), "files.txt");
|
|
36773
|
+
await fs8.writeFile(listFile, files.join("\n") + "\n");
|
|
36774
|
+
const out = path8.join(path8.dirname(listFile), "app.tar.gz");
|
|
36432
36775
|
const r = await run("tar", ["-czf", out, "-C", root, "-T", listFile], { timeoutMs: 12e4 });
|
|
36433
36776
|
if (r.code !== 0) throw new Error(`tar failed: ${r.stderr.trim()}`);
|
|
36434
|
-
const st = await
|
|
36777
|
+
const st = await fs8.stat(out);
|
|
36435
36778
|
return { file: out, files: files.length, bytes: st.size };
|
|
36436
36779
|
}
|
|
36437
36780
|
async function replaceTree(root, archive) {
|
|
36438
|
-
const tmp = await
|
|
36781
|
+
const tmp = await fs8.mkdtemp(path8.join(os3.tmpdir(), "sindri-unpack-"));
|
|
36439
36782
|
const r = await run("tar", ["-xzf", archive, "-C", tmp], { timeoutMs: 12e4 });
|
|
36440
36783
|
if (r.code !== 0) throw new Error(`tar failed: ${r.stderr.trim()}`);
|
|
36441
36784
|
const src = await singleRoot(tmp);
|
|
36442
|
-
await
|
|
36785
|
+
await fs8.mkdir(root, { recursive: true });
|
|
36443
36786
|
await prune(root, 0);
|
|
36444
|
-
await
|
|
36445
|
-
await
|
|
36787
|
+
await fs8.cp(src, root, { recursive: true, force: true, filter: (s) => path8.basename(s) !== "node_modules" && path8.basename(s) !== ".DS_Store" });
|
|
36788
|
+
await fs8.rm(tmp, { recursive: true, force: true });
|
|
36446
36789
|
}
|
|
36447
36790
|
async function prune(dir, depth) {
|
|
36448
|
-
const entries = await
|
|
36791
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
36449
36792
|
for (const e of entries) {
|
|
36450
|
-
const p =
|
|
36793
|
+
const p = path8.join(dir, e.name);
|
|
36451
36794
|
if (e.name === "node_modules") continue;
|
|
36452
36795
|
if (depth === 3 && (e.name === ".sindri" || /^\.env(\..*)?$/.test(e.name))) continue;
|
|
36453
36796
|
if (e.isDirectory()) {
|
|
36454
36797
|
await prune(p, depth + 1);
|
|
36455
|
-
const left = await
|
|
36456
|
-
if (left.length === 0) await
|
|
36798
|
+
const left = await fs8.readdir(p);
|
|
36799
|
+
if (left.length === 0) await fs8.rmdir(p);
|
|
36457
36800
|
} else {
|
|
36458
|
-
await
|
|
36801
|
+
await fs8.rm(p, { force: true });
|
|
36459
36802
|
}
|
|
36460
36803
|
}
|
|
36461
36804
|
}
|
|
36462
36805
|
async function singleRoot(dir) {
|
|
36463
|
-
const entries = (await
|
|
36464
|
-
if (entries.length === 1 && entries[0]?.isDirectory() && !await exists2(
|
|
36465
|
-
return
|
|
36806
|
+
const entries = (await fs8.readdir(dir, { withFileTypes: true })).filter((e) => e.name !== ".DS_Store");
|
|
36807
|
+
if (entries.length === 1 && entries[0]?.isDirectory() && !await exists2(path8.join(dir, "package.json"))) {
|
|
36808
|
+
return path8.join(dir, entries[0].name);
|
|
36466
36809
|
}
|
|
36467
36810
|
return dir;
|
|
36468
36811
|
}
|
|
36469
36812
|
async function exists2(p) {
|
|
36470
36813
|
try {
|
|
36471
|
-
await
|
|
36814
|
+
await fs8.access(p);
|
|
36472
36815
|
return true;
|
|
36473
36816
|
} catch {
|
|
36474
36817
|
return false;
|
|
@@ -36476,14 +36819,14 @@ async function exists2(p) {
|
|
|
36476
36819
|
}
|
|
36477
36820
|
async function readPublished(root) {
|
|
36478
36821
|
try {
|
|
36479
|
-
return JSON.parse(await
|
|
36822
|
+
return JSON.parse(await fs8.readFile(path8.join(root, ".sindri", "last-published.json"), "utf8"));
|
|
36480
36823
|
} catch {
|
|
36481
36824
|
return null;
|
|
36482
36825
|
}
|
|
36483
36826
|
}
|
|
36484
36827
|
async function writePublished(root, p) {
|
|
36485
|
-
await
|
|
36486
|
-
await
|
|
36828
|
+
await fs8.mkdir(path8.join(root, ".sindri"), { recursive: true });
|
|
36829
|
+
await fs8.writeFile(path8.join(root, ".sindri", "last-published.json"), JSON.stringify(p, null, 2) + "\n");
|
|
36487
36830
|
}
|
|
36488
36831
|
|
|
36489
36832
|
// src/tools.ts
|
|
@@ -36522,7 +36865,22 @@ var SCHEMAS = {
|
|
|
36522
36865
|
list_apps: {},
|
|
36523
36866
|
versions: { app: appName },
|
|
36524
36867
|
rollback: { app: appName, version: external_exports.string().min(1).describe("Version id from `versions`.") },
|
|
36525
|
-
|
|
36868
|
+
update_template: { app: appName },
|
|
36869
|
+
rename_app: {
|
|
36870
|
+
app: appName,
|
|
36871
|
+
new_name: appName.optional().describe("New address: lowercase letters, digits and hyphens (2 to 31 chars). Becomes https://<new_name>.sindri.kigo.dev; the old address stops working."),
|
|
36872
|
+
display_name: external_exports.string().min(2).max(60).optional().describe("New visible name (the title people see), in Spanish, as the PM wrote it. The address does not change.")
|
|
36873
|
+
},
|
|
36874
|
+
delete_app: {
|
|
36875
|
+
app: appName,
|
|
36876
|
+
confirm: external_exports.string().describe("The app name exactly as the PM typed it to confirm. Must equal `app`; never fill it in for them.")
|
|
36877
|
+
},
|
|
36878
|
+
share: { app: appName },
|
|
36879
|
+
help: {},
|
|
36880
|
+
design_guide: {
|
|
36881
|
+
app: appName,
|
|
36882
|
+
topic: external_exports.string().min(1).max(60).optional().describe("A reference topic from `topics` (admin, landing, mini-app, mobile, tokens, writing, checklist). Omit for the overview (SKILL.md) and the list of topics.")
|
|
36883
|
+
}
|
|
36526
36884
|
};
|
|
36527
36885
|
var DESCRIPTIONS = {
|
|
36528
36886
|
setup: {
|
|
@@ -36555,7 +36913,27 @@ var DESCRIPTIONS = {
|
|
|
36555
36913
|
title: "Go back to a version",
|
|
36556
36914
|
description: "Publishes a previous version and brings the local folder to it. Nothing is deleted: the newer versions stay in the history."
|
|
36557
36915
|
},
|
|
36558
|
-
|
|
36916
|
+
update_template: {
|
|
36917
|
+
title: "Update the template",
|
|
36918
|
+
description: "Brings the app up to the current Kigo template: files the app never customised are replaced, files you changed are left alone and listed. Publishes a version \xABPlantilla actualizada\xBB and brings the local folder to it. Use it when the PM asks to update the template or Sindri announces a template fix. Refuses while there are unpublished local edits: publish (or discard) them first."
|
|
36919
|
+
},
|
|
36920
|
+
rename_app: {
|
|
36921
|
+
title: "Rename an app",
|
|
36922
|
+
description: 'Two different things, pass at least one. `new_name` changes the address (https://<new_name>.sindri.kigo.dev): Sindri renames it, the local folder is moved to match and the old address stops working; refuses while the folder has unpublished edits (publish first). `display_name` changes only the visible name: it is written to kigo-app.yaml and published as \xABAhora se llama \xAB<display_name>\xBB\xBB. When the PM says "c\xE1mbiale el nombre" and it is unclear which one they mean, ask: \xBFla direcci\xF3n o el nombre que se ve? Both together are allowed (visible name first, then the address).'
|
|
36923
|
+
},
|
|
36924
|
+
delete_app: {
|
|
36925
|
+
title: "Delete an app",
|
|
36926
|
+
description: "Removes the app from Sindri (address, published versions and its database) after the PM typed its name: `confirm` must equal `app`, ask them to write it, never guess it. Stops the preview and moves the local folder to ~/Sindri/.papelera/<app>-<date> instead of deleting it, so the PM can still recover the files by hand."
|
|
36927
|
+
},
|
|
36928
|
+
share: { title: "Share", description: "The line to paste in Slack: address, test-data notice and how the team enters." },
|
|
36929
|
+
help: {
|
|
36930
|
+
title: "What Sindri is",
|
|
36931
|
+
description: "No network. The Spanish introduction to give the PM the first time they speak in a conversation or whenever they ask what Sindri can do: what it is, what they can ask for, the limits and the link to Mi espacio. Also says whether they are already signed in on this computer (`logged_in`) and where their apps live (`apps_dir`)."
|
|
36932
|
+
},
|
|
36933
|
+
design_guide: {
|
|
36934
|
+
title: "Design guide",
|
|
36935
|
+
description: "No network. Kigo's design guide for Sindri apps, read from the app folder (the kigo-sindri-design skill the template ships). Call it before creating or changing any page, component, layout, style or copy: first without `topic` (the overview plus the list of `topics`), then with the topic that matches what you are building (admin, landing, mini-app, mobile). `content` is markdown for you to follow, not to paste to the PM. If it answers `no_design_guide`, the app predates the guide: run update_template once and retry."
|
|
36936
|
+
}
|
|
36559
36937
|
};
|
|
36560
36938
|
function createTools(deps) {
|
|
36561
36939
|
const { cfg } = deps;
|
|
@@ -36594,9 +36972,11 @@ function createTools(deps) {
|
|
|
36594
36972
|
const login = guard(async () => {
|
|
36595
36973
|
try {
|
|
36596
36974
|
const creds2 = await accessToken(cfg, fetchImpl);
|
|
36975
|
+
await api.me();
|
|
36597
36976
|
return { ok: true, summary: `Ya est\xE1s dentro como ${creds2.email ?? "tu cuenta de Kigo"}.`, email: creds2.email ?? null, already: true };
|
|
36598
36977
|
} catch (err) {
|
|
36599
36978
|
if (!(err instanceof NeedsLogin)) throw err;
|
|
36979
|
+
await clearCredentials(cfg).catch(() => void 0);
|
|
36600
36980
|
}
|
|
36601
36981
|
const openUrl = deps.openUrl ?? defaultOpenUrl(cfg);
|
|
36602
36982
|
let creds;
|
|
@@ -36647,15 +37027,15 @@ function createTools(deps) {
|
|
|
36647
37027
|
}
|
|
36648
37028
|
const created = await api.createApp(a.name, a.description.trim());
|
|
36649
37029
|
const archive = await api.download(created.archive_url);
|
|
36650
|
-
await
|
|
37030
|
+
await fs9.mkdir(p.root, { recursive: true });
|
|
36651
37031
|
await replaceTree(p.root, archive);
|
|
36652
|
-
await
|
|
37032
|
+
await fs9.rm(path9.dirname(archive), { recursive: true, force: true });
|
|
36653
37033
|
p = await appPaths(cfg, a.name, me.owner);
|
|
36654
37034
|
if (!await appPresent(p)) {
|
|
36655
37035
|
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
37036
|
}
|
|
36657
|
-
await
|
|
36658
|
-
const install = await run(tc.pnpm, ["install", "--no-frozen-lockfile"], { cwd: p.root, env: { ...tc.env, CI: "1", FORCE_COLOR: "0" }, timeoutMs: 6e5 });
|
|
37037
|
+
await fs9.writeFile(path9.join(p.appDir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
|
|
37038
|
+
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
37039
|
if (install.code !== 0) {
|
|
36660
37040
|
const why = lastLine(install.stderr || install.stdout);
|
|
36661
37041
|
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 };
|
|
@@ -36666,6 +37046,7 @@ function createTools(deps) {
|
|
|
36666
37046
|
}
|
|
36667
37047
|
const app = await api.app(a.name).catch(() => null);
|
|
36668
37048
|
await writePublished(p.appDir, { version: app?.last_version ?? null, tree_hash: await treeHash(p.appDir), url: created.url });
|
|
37049
|
+
await linkDesignGuide(p);
|
|
36669
37050
|
return {
|
|
36670
37051
|
ok: true,
|
|
36671
37052
|
summary: `Cre\xE9 la app "${a.name}". Su direcci\xF3n ser\xE1 ${created.url} cuando la publiques. Ahora edita los archivos de su carpeta para construirla; cuando quieras verla, usa preview.`,
|
|
@@ -36683,8 +37064,8 @@ function createTools(deps) {
|
|
|
36683
37064
|
}
|
|
36684
37065
|
const tc = await toolchain();
|
|
36685
37066
|
if (!tc) return NEEDS_SETUP;
|
|
36686
|
-
if (!await exists2(
|
|
36687
|
-
const install = await run(tc.pnpm, ["install", "--no-frozen-lockfile"], { cwd: p.root, env: { ...tc.env, CI: "1", FORCE_COLOR: "0" }, timeoutMs: 6e5 });
|
|
37067
|
+
if (!await exists2(path9.join(p.root, "node_modules"))) {
|
|
37068
|
+
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
37069
|
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
37070
|
}
|
|
36690
37071
|
if (!await runningPreview(cfg, a.app, fetchImpl)) {
|
|
@@ -36712,7 +37093,7 @@ function createTools(deps) {
|
|
|
36712
37093
|
const tc = await toolchain();
|
|
36713
37094
|
if (!tc) return NEEDS_SETUP;
|
|
36714
37095
|
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 });
|
|
37096
|
+
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
37097
|
if (tsc.code !== 0) {
|
|
36717
37098
|
const first = (tsc.stdout + tsc.stderr).split("\n").find((l) => /error TS\d+/.test(l))?.trim().slice(0, 200);
|
|
36718
37099
|
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 +37105,14 @@ function createTools(deps) {
|
|
|
36724
37105
|
}
|
|
36725
37106
|
const packed = await packTree(dir);
|
|
36726
37107
|
if (packed.bytes > 20 * 1024 * 1024) {
|
|
36727
|
-
await
|
|
37108
|
+
await fs9.rm(path9.dirname(packed.file), { recursive: true, force: true });
|
|
36728
37109
|
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
37110
|
}
|
|
36730
37111
|
let result;
|
|
36731
37112
|
try {
|
|
36732
37113
|
result = await api.publish(a.app, packed.file, message);
|
|
36733
37114
|
} finally {
|
|
36734
|
-
await
|
|
37115
|
+
await fs9.rm(path9.dirname(packed.file), { recursive: true, force: true });
|
|
36735
37116
|
}
|
|
36736
37117
|
await writePublished(dir, { version: result.version, tree_hash: await treeHash(dir), url: result.url });
|
|
36737
37118
|
return {
|
|
@@ -36754,11 +37135,11 @@ function createTools(deps) {
|
|
|
36754
37135
|
});
|
|
36755
37136
|
const list_apps = guard(async () => {
|
|
36756
37137
|
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: [] };
|
|
37138
|
+
if (apps.length === 0) return { ok: true, summary: "Todav\xEDa no tienes apps. Cuando quieras, crea la primera.", apps: [], space_url: SPACE_URL };
|
|
36758
37139
|
const rows = [];
|
|
36759
37140
|
for (const app of apps) rows.push({ ...app, local: await localState(cfg, app.name) });
|
|
36760
37141
|
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 };
|
|
37142
|
+
return { ok: true, summary: `Tienes ${rows.length === 1 ? "una app" : `${rows.length} apps`}: ${names}.`, apps: rows, space_url: SPACE_URL };
|
|
36762
37143
|
});
|
|
36763
37144
|
const versions = guard(async (a) => {
|
|
36764
37145
|
if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
|
|
@@ -36773,15 +37154,16 @@ function createTools(deps) {
|
|
|
36773
37154
|
let p = await appPaths(cfg, a.app);
|
|
36774
37155
|
await stopPreview(cfg, a.app);
|
|
36775
37156
|
const archive = await api.download(`/v1/apps/${encodeURIComponent(a.app)}/archive`, result.version.id);
|
|
36776
|
-
await
|
|
37157
|
+
await fs9.mkdir(p.root, { recursive: true });
|
|
36777
37158
|
await replaceTree(p.root, archive);
|
|
36778
|
-
await
|
|
37159
|
+
await fs9.rm(path9.dirname(archive), { recursive: true, force: true });
|
|
36779
37160
|
p = await appPaths(cfg, a.app, p.owner || void 0);
|
|
36780
37161
|
const dir = p.appDir;
|
|
36781
37162
|
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(
|
|
37163
|
+
if (!await exists2(path9.join(dir, ".env.local"))) await fs9.writeFile(path9.join(dir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
|
|
37164
|
+
await linkDesignGuide(p);
|
|
36783
37165
|
const tc = await toolchain();
|
|
36784
|
-
if (tc && await exists2(
|
|
37166
|
+
if (tc && await exists2(path9.join(p.root, "node_modules"))) {
|
|
36785
37167
|
const db = await prepareLocalDb(tc, dir);
|
|
36786
37168
|
if (!db.ok) process.stderr.write(`sindri: local db after rollback: ${db.detail}
|
|
36787
37169
|
`);
|
|
@@ -36794,24 +37176,193 @@ function createTools(deps) {
|
|
|
36794
37176
|
url: result.url
|
|
36795
37177
|
};
|
|
36796
37178
|
});
|
|
37179
|
+
const update_template = guard(async (a) => {
|
|
37180
|
+
if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
|
|
37181
|
+
const before = await localState(cfg, a.app);
|
|
37182
|
+
if (before.exists && before.unpublished) {
|
|
37183
|
+
return { ok: false, summary: "Publica o descarta tus cambios antes de actualizar la plantilla.", code: "unpublished_changes", local: before };
|
|
37184
|
+
}
|
|
37185
|
+
const result = await api.updateTemplate(a.app);
|
|
37186
|
+
const files = describeTemplateFiles(result);
|
|
37187
|
+
const data = { updated: result.updated, skipped: result.skipped, version: result.version, url: result.url, published: result.published };
|
|
37188
|
+
if (!result.published) {
|
|
37189
|
+
return { ok: true, summary: `"${a.app}" ya est\xE1 al d\xEDa con la plantilla; no cambi\xE9 nada.${files}`, ...data };
|
|
37190
|
+
}
|
|
37191
|
+
let p = await appPaths(cfg, a.app);
|
|
37192
|
+
if (before.exists) {
|
|
37193
|
+
await stopPreview(cfg, a.app);
|
|
37194
|
+
const archive = await api.download(`/v1/apps/${encodeURIComponent(a.app)}/archive`, result.version?.id);
|
|
37195
|
+
await replaceTree(p.root, archive);
|
|
37196
|
+
await fs9.rm(path9.dirname(archive), { recursive: true, force: true });
|
|
37197
|
+
p = await appPaths(cfg, a.app, p.owner || void 0);
|
|
37198
|
+
const dir = p.appDir;
|
|
37199
|
+
if (!await appPresent(p)) return { ok: false, summary: `Sindri actualiz\xF3 la plantilla pero lo que baj\xF3 no trae la app donde la espero. Escribe en Slack, canal #sindri.`, ...data };
|
|
37200
|
+
if (!await exists2(path9.join(dir, ".env.local"))) await fs9.writeFile(path9.join(dir, ".env.local"), "KIGO_ENV=local\n", { mode: 384 });
|
|
37201
|
+
await linkDesignGuide(p);
|
|
37202
|
+
const tc = await toolchain();
|
|
37203
|
+
if (tc && await exists2(path9.join(p.root, "node_modules"))) {
|
|
37204
|
+
const db = await prepareLocalDb(tc, dir);
|
|
37205
|
+
if (!db.ok) process.stderr.write(`sindri: local db after update_template: ${db.detail}
|
|
37206
|
+
`);
|
|
37207
|
+
}
|
|
37208
|
+
await writePublished(dir, { version: result.version, tree_hash: await treeHash(dir), url: result.url });
|
|
37209
|
+
}
|
|
37210
|
+
const where = before.exists ? `en ${result.url} y en tu computadora` : `en ${result.url}`;
|
|
37211
|
+
return { ok: true, summary: `Listo, actualic\xE9 la plantilla de "${a.app}" ${where}.${files}`, ...data };
|
|
37212
|
+
});
|
|
36797
37213
|
const share = guard(async (a) => {
|
|
36798
37214
|
if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
|
|
36799
37215
|
const r = await api.share(a.app);
|
|
36800
37216
|
return { ok: true, summary: `Para Slack: ${r.text}`, text: r.text };
|
|
36801
37217
|
});
|
|
37218
|
+
const rename_app = guard(async (a) => {
|
|
37219
|
+
if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
|
|
37220
|
+
const newName = a.new_name?.trim().toLowerCase();
|
|
37221
|
+
const displayName = a.display_name?.trim();
|
|
37222
|
+
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" };
|
|
37223
|
+
if (newName !== void 0 && !validAppName(newName)) {
|
|
37224
|
+
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)." };
|
|
37225
|
+
}
|
|
37226
|
+
if (newName === a.app) return { ok: false, summary: `"${a.app}" ya est\xE1 en esa direcci\xF3n.`, code: "same_name" };
|
|
37227
|
+
const parts = [];
|
|
37228
|
+
const data = {};
|
|
37229
|
+
if (displayName) {
|
|
37230
|
+
const p = await appPaths(cfg, a.app);
|
|
37231
|
+
if (!await appPresent(p)) {
|
|
37232
|
+
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 };
|
|
37233
|
+
}
|
|
37234
|
+
await writeManifestValue(p.appDir, "display_name", displayName);
|
|
37235
|
+
const pub = await publish({ app: a.app, message: `Ahora se llama \xAB${displayName}\xBB` });
|
|
37236
|
+
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}` };
|
|
37237
|
+
parts.push(`Listo, ahora se llama \xAB${displayName}\xBB y ya se ve as\xED en ${pub.url}.`);
|
|
37238
|
+
Object.assign(data, { display_name: displayName, version: pub.version, url: pub.url });
|
|
37239
|
+
}
|
|
37240
|
+
if (newName) {
|
|
37241
|
+
const before = await localState(cfg, a.app);
|
|
37242
|
+
if (before.exists && before.unpublished) {
|
|
37243
|
+
return { ok: false, summary: `Publica tus cambios antes de cambiar la direcci\xF3n de "${a.app}".`, code: "unpublished_changes", local: before };
|
|
37244
|
+
}
|
|
37245
|
+
if (await exists2(appRoot(cfg, newName))) {
|
|
37246
|
+
return { ok: false, summary: `Ya tienes una carpeta llamada "${newName}" en Sindri; elige otra direcci\xF3n.`, code: "folder_exists" };
|
|
37247
|
+
}
|
|
37248
|
+
const result = await api.rename(a.app, newName);
|
|
37249
|
+
const oldUrl = `https://${a.app}.${cfg.appsDomain}/`;
|
|
37250
|
+
if (before.exists) {
|
|
37251
|
+
const p = await appPaths(cfg, a.app);
|
|
37252
|
+
await stopPreview(cfg, a.app);
|
|
37253
|
+
const newRoot = appRoot(cfg, newName);
|
|
37254
|
+
await moveDir(p.root, newRoot);
|
|
37255
|
+
const newDir = path9.join(newRoot, "apps", p.owner, newName);
|
|
37256
|
+
await moveDir(path9.join(newRoot, "apps", p.owner, a.app), newDir);
|
|
37257
|
+
await writeManifestValue(newDir, "name", newName);
|
|
37258
|
+
await writePublished(newDir, { version: result.version ?? before.published_version, tree_hash: await treeHash(newDir), url: result.url });
|
|
37259
|
+
await linkDesignGuide({ root: newRoot, appDir: newDir, owner: p.owner });
|
|
37260
|
+
data.path = newDir;
|
|
37261
|
+
}
|
|
37262
|
+
parts.push(`Listo, "${a.app}" ahora vive en ${result.url}. La direcci\xF3n anterior (${oldUrl}) ya no funciona: comparte la nueva con tu equipo.`);
|
|
37263
|
+
Object.assign(data, { name: result.name, old_name: a.app, url: result.url, version: result.version });
|
|
37264
|
+
}
|
|
37265
|
+
return { ok: true, summary: parts.join(" "), ...data };
|
|
37266
|
+
});
|
|
37267
|
+
const delete_app = guard(async (a) => {
|
|
37268
|
+
if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
|
|
37269
|
+
if (a.confirm.trim() !== a.app) {
|
|
37270
|
+
return { ok: false, summary: `Para borrar "${a.app}" necesito que escribas su nombre tal cual. No la toqu\xE9.`, code: "confirm_mismatch" };
|
|
37271
|
+
}
|
|
37272
|
+
const root = appRoot(cfg, a.app);
|
|
37273
|
+
const present = await exists2(root);
|
|
37274
|
+
await stopPreview(cfg, a.app);
|
|
37275
|
+
let inSindri = true;
|
|
37276
|
+
try {
|
|
37277
|
+
await api.deleteApp(a.app, a.app);
|
|
37278
|
+
} catch (err) {
|
|
37279
|
+
if (err instanceof ApiError && err.status === 404 && present) inSindri = false;
|
|
37280
|
+
else throw err;
|
|
37281
|
+
}
|
|
37282
|
+
let trash = null;
|
|
37283
|
+
if (present) {
|
|
37284
|
+
trash = path9.join(cfg.home, ".papelera", `${a.app}-${stamp()}`);
|
|
37285
|
+
await moveDir(root, trash);
|
|
37286
|
+
}
|
|
37287
|
+
const kept = "Una copia de sus archivos queda en tu computadora, en la papelera de Sindri; puedes borrarla a mano cuando quieras.";
|
|
37288
|
+
let summary;
|
|
37289
|
+
if (!inSindri) summary = `"${a.app}" ya no exist\xEDa en Sindri. ${kept}`;
|
|
37290
|
+
else if (trash) summary = `Borr\xE9 "${a.app}" de Sindri: su direcci\xF3n y sus versiones ya no existen. ${kept}`;
|
|
37291
|
+
else summary = `Borr\xE9 "${a.app}" de Sindri: su direcci\xF3n y sus versiones ya no existen. En esta computadora no ten\xEDa carpeta.`;
|
|
37292
|
+
return { ok: true, summary, name: a.app, removed_from_sindri: inSindri, trash_path: trash };
|
|
37293
|
+
});
|
|
37294
|
+
const help = guard(async () => {
|
|
37295
|
+
const logged_in = await loadCredentials(cfg) !== null;
|
|
37296
|
+
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.`;
|
|
37297
|
+
return { ok: true, summary, space_url: SPACE_URL, logged_in, apps_dir: cfg.home };
|
|
37298
|
+
});
|
|
37299
|
+
const design_guide = guard(async (a) => {
|
|
37300
|
+
if (!validAppName(a.app)) return { ok: false, summary: "Ese nombre de app no es v\xE1lido." };
|
|
37301
|
+
const p = await appPaths(cfg, a.app);
|
|
37302
|
+
if (!await appPresent(p)) {
|
|
37303
|
+
return { ok: false, summary: `No encuentro la carpeta de "${a.app}" en tu computadora. Si la app existe en Sindri, usa rollback a su \xFAltima versi\xF3n para bajarla.`, code: "no_app", path: p.appDir };
|
|
37304
|
+
}
|
|
37305
|
+
const topic = a.topic?.trim().toLowerCase().replace(/\.md$/, "");
|
|
37306
|
+
const guide = await readDesignGuide(p, topic || void 0);
|
|
37307
|
+
if (guide === null) {
|
|
37308
|
+
return {
|
|
37309
|
+
ok: false,
|
|
37310
|
+
summary: `"${a.app}" todav\xEDa no trae la gu\xEDa de dise\xF1o de Sindri. Corre update_template una vez para traerla y vuelve a llamar a design_guide.`,
|
|
37311
|
+
code: "no_design_guide",
|
|
37312
|
+
topics: []
|
|
37313
|
+
};
|
|
37314
|
+
}
|
|
37315
|
+
if ("unknown" in guide) {
|
|
37316
|
+
return { ok: false, summary: `No hay un tema "${topic}" en la gu\xEDa de dise\xF1o. Los temas disponibles son: ${guide.topics.join(", ")}.`, code: "unknown_topic", topics: guide.topics };
|
|
37317
|
+
}
|
|
37318
|
+
await linkDesignGuide(p);
|
|
37319
|
+
return {
|
|
37320
|
+
ok: true,
|
|
37321
|
+
summary: topic ? `Gu\xEDa de dise\xF1o de Sindri (tema: ${topic}).` : `Gu\xEDa de dise\xF1o de Sindri (temas: ${guide.topics.join(", ")}).`,
|
|
37322
|
+
topic: topic ?? null,
|
|
37323
|
+
topics: guide.topics,
|
|
37324
|
+
content: guide.content
|
|
37325
|
+
};
|
|
37326
|
+
});
|
|
36802
37327
|
const logout = guard(async () => {
|
|
36803
37328
|
await clearCredentials(cfg);
|
|
36804
37329
|
return { ok: true, summary: "Saliste de tu cuenta de Kigo en esta computadora." };
|
|
36805
37330
|
});
|
|
36806
|
-
return { setup, login, create_app, preview, publish, status, list_apps, versions, rollback, share, logout };
|
|
37331
|
+
return { setup, login, create_app, preview, publish, status, list_apps, versions, rollback, update_template, rename_app, delete_app, share, help, design_guide, logout };
|
|
37332
|
+
}
|
|
37333
|
+
var HELP_SUMMARY = "Sindri te ayuda a crear apps internas peque\xF1as para tu equipo en minutos, con datos de prueba y sin escribir c\xF3digo: t\xFA me cuentas qu\xE9 necesitas y yo la construyo. Puedes pedirme crear una app, verla en tu computadora antes de publicarla, publicarla, cambiarle el nombre o la direcci\xF3n, volver a una versi\xF3n anterior, compartirla en Slack, actualizar su plantilla o borrarla. Ten en cuenta: las apps trabajan con datos sint\xE9ticos, nunca reales; funcionan de lunes a viernes de 9:05 a 24:00, hora de la Ciudad de M\xE9xico; cada app la edita solo quien la cre\xF3; y lo que deba llegar a toda la empresa se pide a desarrollo. Tus apps y sus versiones est\xE1n en https://sindri.kigo.dev.";
|
|
37334
|
+
async function moveDir(from, to) {
|
|
37335
|
+
await fs9.mkdir(path9.dirname(to), { recursive: true });
|
|
37336
|
+
try {
|
|
37337
|
+
await fs9.rename(from, to);
|
|
37338
|
+
} catch (err) {
|
|
37339
|
+
if (err.code !== "EXDEV") throw err;
|
|
37340
|
+
await fs9.cp(from, to, { recursive: true });
|
|
37341
|
+
await fs9.rm(from, { recursive: true, force: true });
|
|
37342
|
+
}
|
|
37343
|
+
}
|
|
37344
|
+
function stamp(d = /* @__PURE__ */ new Date()) {
|
|
37345
|
+
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);
|
|
37346
|
+
const g = (t) => parts.find((p) => p.type === t)?.value ?? "";
|
|
37347
|
+
return `${g("year")}${g("month")}${g("day")}-${g("hour")}${g("minute")}`;
|
|
37348
|
+
}
|
|
37349
|
+
function describeTemplateFiles(r) {
|
|
37350
|
+
const parts = [];
|
|
37351
|
+
const n = r.updated.length;
|
|
37352
|
+
if (n > 0) parts.push(n === 1 ? "Cambi\xF3 un archivo de la plantilla." : `Cambiaron ${n} archivos de la plantilla.`);
|
|
37353
|
+
if (r.skipped.length > 0) {
|
|
37354
|
+
const list = r.skipped.length <= 5 ? r.skipped.join(", ") : `${r.skipped.slice(0, 5).join(", ")} y ${r.skipped.length - 5} m\xE1s`;
|
|
37355
|
+
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.`);
|
|
37356
|
+
}
|
|
37357
|
+
return parts.length ? ` ${parts.join(" ")}` : "";
|
|
36807
37358
|
}
|
|
36808
37359
|
async function prepareLocalDb(tc, dir, opts = {}) {
|
|
36809
37360
|
const env = { ...tc.env, KIGO_ENV: "local", DATABASE_URL: "", CI: "1", FORCE_COLOR: "0" };
|
|
36810
|
-
const fresh = !await exists2(
|
|
36811
|
-
const migrate = await run(tc.pnpm, ["db:migrate"], { cwd: dir, env, timeoutMs: 18e4 });
|
|
37361
|
+
const fresh = !await exists2(path9.join(dir, ".sindri", "pglite"));
|
|
37362
|
+
const migrate = await run(tc.pnpm, [...PNPM_ARGS, "db:migrate"], { cwd: dir, env, timeoutMs: 18e4 });
|
|
36812
37363
|
if (migrate.code !== 0) return { ok: false, detail: lastLine(migrate.stderr || migrate.stdout) };
|
|
36813
37364
|
if (opts.seed ?? fresh) {
|
|
36814
|
-
const seed = await run(tc.pnpm, ["db:seed"], { cwd: dir, env, timeoutMs: 18e4 });
|
|
37365
|
+
const seed = await run(tc.pnpm, [...PNPM_ARGS, "db:seed"], { cwd: dir, env, timeoutMs: 18e4 });
|
|
36815
37366
|
if (seed.code !== 0) return { ok: false, detail: lastLine(seed.stderr || seed.stdout) };
|
|
36816
37367
|
}
|
|
36817
37368
|
return { ok: true };
|
|
@@ -36839,7 +37390,7 @@ function when(iso, now = /* @__PURE__ */ new Date()) {
|
|
|
36839
37390
|
}
|
|
36840
37391
|
|
|
36841
37392
|
// src/index.ts
|
|
36842
|
-
var VERSION = "0.1.
|
|
37393
|
+
var VERSION = "0.1.4";
|
|
36843
37394
|
function registerTools(server, tools) {
|
|
36844
37395
|
for (const name of Object.keys(SCHEMAS)) {
|
|
36845
37396
|
const meta3 = DESCRIPTIONS[name];
|
|
@@ -36854,9 +37405,30 @@ function registerTools(server, tools) {
|
|
|
36854
37405
|
});
|
|
36855
37406
|
}
|
|
36856
37407
|
}
|
|
37408
|
+
function registerResources(server, tools) {
|
|
37409
|
+
const read = async (uri, app, topic) => {
|
|
37410
|
+
const r = await tools.design_guide({ app, ...topic ? { topic } : {} });
|
|
37411
|
+
if (!r.ok) throw new Error(r.summary);
|
|
37412
|
+
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: r.content }] };
|
|
37413
|
+
};
|
|
37414
|
+
const one = (v) => Array.isArray(v) ? v[0] ?? "" : v ?? "";
|
|
37415
|
+
server.registerResource(
|
|
37416
|
+
"design-guide",
|
|
37417
|
+
new ResourceTemplate("sindri://design-guide/{app}", { list: void 0 }),
|
|
37418
|
+
{ title: "Sindri design guide", description: "Kigo's design guide for an app (overview and topics), read from the app folder.", mimeType: "text/markdown" },
|
|
37419
|
+
(uri, vars) => read(uri, one(vars.app))
|
|
37420
|
+
);
|
|
37421
|
+
server.registerResource(
|
|
37422
|
+
"design-guide-topic",
|
|
37423
|
+
new ResourceTemplate("sindri://design-guide/{app}/{topic}", { list: void 0 }),
|
|
37424
|
+
{ title: "Sindri design guide topic", description: "One topic of Kigo's design guide (admin, landing, mini-app, mobile, tokens, writing, checklist).", mimeType: "text/markdown" },
|
|
37425
|
+
(uri, vars) => read(uri, one(vars.app), one(vars.topic))
|
|
37426
|
+
);
|
|
37427
|
+
}
|
|
36857
37428
|
function createServer(tools) {
|
|
36858
37429
|
const server = new McpServer({ name: "sindri", version: VERSION }, { instructions: INSTRUCTIONS });
|
|
36859
37430
|
registerTools(server, tools);
|
|
37431
|
+
registerResources(server, tools);
|
|
36860
37432
|
return server;
|
|
36861
37433
|
}
|
|
36862
37434
|
async function callTool(tools, name, rawArgs) {
|
|
@@ -36895,5 +37467,6 @@ export {
|
|
|
36895
37467
|
VERSION,
|
|
36896
37468
|
callTool,
|
|
36897
37469
|
createServer,
|
|
37470
|
+
registerResources,
|
|
36898
37471
|
registerTools
|
|
36899
37472
|
};
|