knodin 0.8.6 → 0.9.0
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 +6 -0
- package/dist/bin/cli.js +459 -95
- package/dist/bin/launcher.js +0 -0
- package/dist/fixtures/update-verification/config/application.xml +4 -0
- package/dist/fixtures/update-verification/dbt/manifest.json +17 -0
- package/dist/fixtures/update-verification/dbt/models/accounts.sql +1 -0
- package/dist/fixtures/update-verification/dbt/models/contacts.sql +1 -0
- package/dist/fixtures/update-verification/fixture.json +147 -0
- package/dist/fixtures/update-verification/force-app/main/default/classes/UpdateFixture.cls +14 -0
- package/dist/fixtures/update-verification/force-app/main/default/flows/Update_Verification.flow-meta.xml +9 -0
- package/dist/fixtures/update-verification/package.json +5 -0
- package/dist/fixtures/update-verification/prisma/schema.prisma +10 -0
- package/dist/fixtures/update-verification/sql/functions.sql +16 -0
- package/dist/fixtures/update-verification/src/CsharpFixture.cs +6 -0
- package/dist/fixtures/update-verification/src/JavaFixture.java +9 -0
- package/dist/fixtures/update-verification/src/javascript.js +7 -0
- package/dist/fixtures/update-verification/src/python_fixture.py +6 -0
- package/dist/fixtures/update-verification/src/typescript.ts +7 -0
- package/dist/fixtures/update-verification/terraform/main.tf +7 -0
- package/dist/fixtures/update-verification/terraform/modules/verified-child/main.tf +3 -0
- package/dist/src/agent-integration.js +26 -3
- package/dist/src/backup-retention.js +811 -0
- package/dist/src/cli-model.js +66 -9
- package/dist/src/docs-sections.js +1 -0
- package/dist/src/doctor.js +142 -12
- package/dist/src/engine/embeddings.js +8 -1
- package/dist/src/engine/index.js +355 -292
- package/dist/src/init.js +223 -164
- package/dist/src/manager-update.js +455 -0
- package/dist/src/release-preflight.js +106 -95
- package/dist/src/repair-lease.js +77 -8
- package/dist/src/repository-management.js +219 -3
- package/dist/src/response-budget.js +5 -1
- package/dist/src/server.js +18 -5
- package/dist/src/shared-index/compatibility.js +4 -2
- package/dist/src/skill-management.js +149 -0
- package/dist/src/system-config.js +176 -16
- package/dist/src/system-management.js +508 -0
- package/dist/src/tools/knodin-tools.js +174 -22
- package/dist/src/update-ceremony.js +20 -24
- package/dist/src/update-coordination.js +158 -0
- package/dist/src/update-executor.js +336 -0
- package/dist/src/update-verifier.js +358 -0
- package/docs/BACKUP-RETENTION.md +54 -0
- package/docs/CLI.md +13 -0
- package/docs/DOCTOR-AND-UPDATES.md +44 -12
- package/docs/SIGNED-UPDATES.md +29 -22
- package/docs/SYSTEMS-AND-RELATIONSHIPS.md +27 -6
- package/docs/prompts/declare-multi-repository-system.md +18 -0
- package/docs/releases/0.8.7.md +27 -0
- package/docs/releases/0.9.0.md +43 -0
- package/package.json +11 -2
- package/roadmap/competitive-roadmap.md +8 -0
- package/skills/knodin-debug/SKILL.md +12 -0
- package/skills/knodin-debug/agents/openai.yaml +4 -0
- package/skills/knodin-explore/SKILL.md +11 -0
- package/skills/knodin-explore/agents/openai.yaml +4 -0
- package/skills/knodin-impact/SKILL.md +11 -0
- package/skills/knodin-impact/agents/openai.yaml +4 -0
- package/skills/knodin-lifecycle/SKILL.md +12 -0
- package/skills/knodin-lifecycle/agents/openai.yaml +4 -0
- package/skills/knodin-refactor/SKILL.md +12 -0
- package/skills/knodin-refactor/agents/openai.yaml +4 -0
- package/skills/knodin-review/SKILL.md +11 -0
- package/skills/knodin-review/agents/openai.yaml +4 -0
- package/skills/knodin-systems/SKILL.md +14 -0
- package/skills/knodin-systems/agents/openai.yaml +4 -0
|
@@ -901,7 +901,12 @@ function decodeCursor(cursor, hash) {
|
|
|
901
901
|
(value.repository ?? -1) < 0 ||
|
|
902
902
|
(value.offset ?? -1) < 0)
|
|
903
903
|
throw new Error("invalid cursor");
|
|
904
|
-
return {
|
|
904
|
+
return {
|
|
905
|
+
repository: value.repository ?? 0,
|
|
906
|
+
offset: value.offset ?? 0,
|
|
907
|
+
...(value.mode === "fallback" ? { mode: "fallback" } : {}),
|
|
908
|
+
...(value.mode === "symbol-only" ? { mode: "symbol-only" } : {}),
|
|
909
|
+
};
|
|
905
910
|
}
|
|
906
911
|
catch {
|
|
907
912
|
throw new Error("knodin repos search: cursor does not match this query and selection");
|
|
@@ -930,8 +935,146 @@ function searchCandidate(inventory, match, exposePaths) {
|
|
|
930
935
|
confidence: match.rrfScore || match.similarity,
|
|
931
936
|
freshness: inventory.freshness,
|
|
932
937
|
health: "healthy",
|
|
938
|
+
matchType: "symbol",
|
|
939
|
+
systemMemberships: inventory.systemMemberships,
|
|
933
940
|
};
|
|
934
941
|
}
|
|
942
|
+
const TRACKED_TEXT_CANDIDATE_LIMIT = 1000;
|
|
943
|
+
const TRACKED_TEXT_REPOSITORY_LIMIT = 200;
|
|
944
|
+
const TRACKED_TEXT_OUTPUT_LIMIT = 2 * 1024 * 1024;
|
|
945
|
+
const TRACKED_TEXT_TIMEOUT_MS = 5000;
|
|
946
|
+
function queryTokens(query) {
|
|
947
|
+
return [
|
|
948
|
+
...new Set(query
|
|
949
|
+
.toLowerCase()
|
|
950
|
+
.match(/[a-z0-9][a-z0-9._-]{2,}/g)
|
|
951
|
+
?.map((token) => token.replace(/^[._-]+|[._-]+$/g, ""))
|
|
952
|
+
.filter(Boolean) ?? []),
|
|
953
|
+
];
|
|
954
|
+
}
|
|
955
|
+
function tokenCoverage(value, tokens) {
|
|
956
|
+
const lower = value.toLowerCase();
|
|
957
|
+
return tokens.filter((token) => lower.includes(token)).length;
|
|
958
|
+
}
|
|
959
|
+
function textFallbackReason(results, query) {
|
|
960
|
+
if (results.length === 0)
|
|
961
|
+
return "no-symbol-results";
|
|
962
|
+
const tokens = queryTokens(query);
|
|
963
|
+
if (tokens.length < 3)
|
|
964
|
+
return undefined;
|
|
965
|
+
return results.every((result) => {
|
|
966
|
+
const identityCoverage = tokenCoverage(`${result.symbol} ${result.file}`, tokens);
|
|
967
|
+
const sourceCoverage = tokenCoverage(result.sourceEvidence, tokens);
|
|
968
|
+
return identityCoverage < 2 && (identityCoverage === 0 || sourceCoverage < 2);
|
|
969
|
+
})
|
|
970
|
+
? "weak-symbol-coverage"
|
|
971
|
+
: undefined;
|
|
972
|
+
}
|
|
973
|
+
function committedFile(repository, file, timeout) {
|
|
974
|
+
const result = child_process.spawnSync(gitExecutable(), ["show", `HEAD:${file}`], {
|
|
975
|
+
cwd: repository,
|
|
976
|
+
encoding: "buffer",
|
|
977
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
978
|
+
timeout,
|
|
979
|
+
maxBuffer: 1024 * 1024,
|
|
980
|
+
});
|
|
981
|
+
return result.status === 0 && !result.error ? result.stdout : undefined;
|
|
982
|
+
}
|
|
983
|
+
function trackedTextWindow(repository, file, line, timeout, cache) {
|
|
984
|
+
try {
|
|
985
|
+
let content = cache?.get(file);
|
|
986
|
+
if (!cache?.has(file)) {
|
|
987
|
+
content = committedFile(repository, file, timeout);
|
|
988
|
+
cache?.set(file, content);
|
|
989
|
+
}
|
|
990
|
+
if (!content || content.includes(0))
|
|
991
|
+
return "";
|
|
992
|
+
const lines = content.toString("utf8").split(/\r?\n/);
|
|
993
|
+
const start = Math.max(0, line - 3);
|
|
994
|
+
return lines
|
|
995
|
+
.slice(start, Math.min(lines.length, line + 2))
|
|
996
|
+
.map((value, index) => `${start + index + 1}: ${value}`)
|
|
997
|
+
.join("\n");
|
|
998
|
+
}
|
|
999
|
+
catch {
|
|
1000
|
+
return "";
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
function trackedTextMatches(inventory, query, exposePaths) {
|
|
1004
|
+
if (!inventory.path)
|
|
1005
|
+
return { matches: [], truncated: false };
|
|
1006
|
+
const tokens = queryTokens(query);
|
|
1007
|
+
if (tokens.length === 0)
|
|
1008
|
+
return { matches: [], truncated: false };
|
|
1009
|
+
const started = Date.now();
|
|
1010
|
+
const remaining = () => Math.max(1, TRACKED_TEXT_TIMEOUT_MS - (Date.now() - started));
|
|
1011
|
+
const args = ["grep", "-n", "-I", "-F", "-i"];
|
|
1012
|
+
for (const token of tokens)
|
|
1013
|
+
args.push("-e", token);
|
|
1014
|
+
args.push("HEAD");
|
|
1015
|
+
const result = child_process.spawnSync(gitExecutable(), args, {
|
|
1016
|
+
cwd: inventory.path,
|
|
1017
|
+
encoding: "utf8",
|
|
1018
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1019
|
+
timeout: remaining(),
|
|
1020
|
+
maxBuffer: TRACKED_TEXT_OUTPUT_LIMIT,
|
|
1021
|
+
});
|
|
1022
|
+
if (result.error && result.error.code !== "ENOBUFS")
|
|
1023
|
+
return { matches: [], truncated: true };
|
|
1024
|
+
const lines = String(result.stdout ?? "")
|
|
1025
|
+
.split(/\r?\n/)
|
|
1026
|
+
.filter(Boolean);
|
|
1027
|
+
const truncated = lines.length > TRACKED_TEXT_REPOSITORY_LIMIT || result.error !== undefined;
|
|
1028
|
+
const records = lines.flatMap((record) => {
|
|
1029
|
+
const match = /^HEAD:(.+?):(\d+):(.*)$/.exec(record);
|
|
1030
|
+
if (!match?.[1] || !match[2])
|
|
1031
|
+
return [];
|
|
1032
|
+
const file = match[1];
|
|
1033
|
+
const extension = path.extname(file).toLowerCase();
|
|
1034
|
+
if (![".yaml", ".yml", ".sh", ".bash", ".zsh", ""].includes(extension))
|
|
1035
|
+
return [];
|
|
1036
|
+
if (extension === "") {
|
|
1037
|
+
const content = committedFile(inventory.path ?? "", file, remaining());
|
|
1038
|
+
if (!content || !content.toString("utf8", 0, 128).startsWith("#!"))
|
|
1039
|
+
return [];
|
|
1040
|
+
}
|
|
1041
|
+
return [{ file, line: Number(match[2]), matchedSource: match[3] ?? "" }];
|
|
1042
|
+
});
|
|
1043
|
+
const phrase = query.trim().toLowerCase();
|
|
1044
|
+
const contentCache = new Map();
|
|
1045
|
+
const matches = records.slice(0, TRACKED_TEXT_REPOSITORY_LIMIT).flatMap((record) => {
|
|
1046
|
+
const { file, line, matchedSource } = record;
|
|
1047
|
+
const source = trackedTextWindow(inventory.path ?? "", file, line, remaining(), contentCache) ||
|
|
1048
|
+
matchedSource;
|
|
1049
|
+
const coverage = tokenCoverage(`${file} ${source}`, tokens);
|
|
1050
|
+
const exactPhrase = phrase.length > 0 && source.toLowerCase().includes(phrase) ? 1 : 0;
|
|
1051
|
+
const pathCoverage = tokenCoverage(file, tokens);
|
|
1052
|
+
return [
|
|
1053
|
+
{
|
|
1054
|
+
repositoryId: inventory.id,
|
|
1055
|
+
...(exposePaths === false ? {} : { repositoryPath: inventory.path }),
|
|
1056
|
+
identity: `text_${crypto.createHash("sha256").update(`${inventory.id}\0${file}\0${line}`).digest("hex").slice(0, 20)}`,
|
|
1057
|
+
symbol: path.basename(file),
|
|
1058
|
+
kind: "tracked-text",
|
|
1059
|
+
file,
|
|
1060
|
+
line,
|
|
1061
|
+
sourceEvidence: source.slice(0, 4000),
|
|
1062
|
+
sourceTruncated: source.length > 4000,
|
|
1063
|
+
confidence: coverage / tokens.length + exactPhrase * 0.01 + pathCoverage * 0.001,
|
|
1064
|
+
freshness: inventory.freshness,
|
|
1065
|
+
health: "healthy",
|
|
1066
|
+
matchType: "tracked-text",
|
|
1067
|
+
systemMemberships: inventory.systemMemberships,
|
|
1068
|
+
_score: [coverage, exactPhrase, pathCoverage],
|
|
1069
|
+
},
|
|
1070
|
+
];
|
|
1071
|
+
});
|
|
1072
|
+
matches.sort((left, right) => right._score[0] - left._score[0] ||
|
|
1073
|
+
right._score[1] - left._score[1] ||
|
|
1074
|
+
right._score[2] - left._score[2] ||
|
|
1075
|
+
compareBytes(`${left.repositoryId}:${left.file}:${left.line}`, `${right.repositoryId}:${right.file}:${right.line}`));
|
|
1076
|
+
return { matches: matches.map(({ _score: _ignored, ...match }) => match), truncated };
|
|
1077
|
+
}
|
|
935
1078
|
async function searchHealthyRepository(inventory, repositoryIndex, offset, context) {
|
|
936
1079
|
const { query, itemLimit, effectiveBytes, results, omissions, selected, options } = context;
|
|
937
1080
|
const page = await options.search(query, inventory.path ?? "", itemLimit - results.length + 1, offset);
|
|
@@ -1013,6 +1156,7 @@ export async function searchRepositories(roots, query, options) {
|
|
|
1013
1156
|
.digest("hex")
|
|
1014
1157
|
.slice(0, 20);
|
|
1015
1158
|
const start = decodeCursor(options.cursor, hash);
|
|
1159
|
+
const symbolStart = start.mode === "fallback" ? { repository: 0, offset: 0 } : start;
|
|
1016
1160
|
const itemLimit = Math.min(Math.max(options.itemBudget ?? 25, 1), 1000);
|
|
1017
1161
|
if (options.byteBudget !== undefined && options.byteBudget < 1024)
|
|
1018
1162
|
throw new Error("knodin repos search: byte budget must be at least 1024");
|
|
@@ -1021,7 +1165,58 @@ export async function searchRepositories(roots, query, options) {
|
|
|
1021
1165
|
const byteLimit = options.byteBudget ?? 65_536;
|
|
1022
1166
|
const tokenLimit = options.tokenBudget ?? 16_384;
|
|
1023
1167
|
const effectiveBytes = Math.min(byteLimit, tokenLimit * 4);
|
|
1024
|
-
const { results, omissions, next } = await collectRepositorySearchResults(selected,
|
|
1168
|
+
const { results, omissions, next } = await collectRepositorySearchResults(selected, symbolStart, query, itemLimit, effectiveBytes, options);
|
|
1169
|
+
const fallbackReason = start.mode === "symbol-only" ? undefined : textFallbackReason(results, query);
|
|
1170
|
+
let textFallbackTruncated = false;
|
|
1171
|
+
let fallbackCursor;
|
|
1172
|
+
if (fallbackReason) {
|
|
1173
|
+
const textMatches = [];
|
|
1174
|
+
const eligible = selected.filter(({ health }) => health === "healthy");
|
|
1175
|
+
const inspected = eligible.slice(0, 25);
|
|
1176
|
+
if (inspected.length < eligible.length)
|
|
1177
|
+
textFallbackTruncated = true;
|
|
1178
|
+
for (const inventory of inspected) {
|
|
1179
|
+
const text = trackedTextMatches(inventory, query, options.exposePaths);
|
|
1180
|
+
textFallbackTruncated ||= text.truncated;
|
|
1181
|
+
textMatches.push(...text.matches);
|
|
1182
|
+
if (textMatches.length >= TRACKED_TEXT_CANDIDATE_LIMIT) {
|
|
1183
|
+
textFallbackTruncated = true;
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
textMatches.sort((left, right) => right.confidence - left.confidence ||
|
|
1188
|
+
compareBytes(`${left.repositoryId}:${left.file}:${left.line ?? 0}`, `${right.repositoryId}:${right.file}:${right.line ?? 0}`));
|
|
1189
|
+
const combined = textMatches.slice(0, TRACKED_TEXT_CANDIDATE_LIMIT);
|
|
1190
|
+
const fallbackOffset = start.mode === "fallback" ? start.offset : 0;
|
|
1191
|
+
const page = [];
|
|
1192
|
+
let nextOffset = fallbackOffset;
|
|
1193
|
+
while (nextOffset < combined.length && page.length < itemLimit) {
|
|
1194
|
+
const candidate = combined[nextOffset];
|
|
1195
|
+
if (!candidate)
|
|
1196
|
+
break;
|
|
1197
|
+
const probe = JSON.stringify({
|
|
1198
|
+
results: [...page, candidate],
|
|
1199
|
+
omissions,
|
|
1200
|
+
repositories: selected,
|
|
1201
|
+
});
|
|
1202
|
+
if (Buffer.byteLength(probe) > effectiveBytes && page.length > 0)
|
|
1203
|
+
break;
|
|
1204
|
+
page.push(candidate);
|
|
1205
|
+
nextOffset += 1;
|
|
1206
|
+
if (Buffer.byteLength(probe) > effectiveBytes)
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
results.splice(0, results.length, ...page);
|
|
1210
|
+
if (nextOffset < combined.length)
|
|
1211
|
+
fallbackCursor = encodeCursor({
|
|
1212
|
+
hash,
|
|
1213
|
+
repository: 0,
|
|
1214
|
+
offset: nextOffset,
|
|
1215
|
+
mode: "fallback",
|
|
1216
|
+
});
|
|
1217
|
+
else if (results.length > 0 || next)
|
|
1218
|
+
fallbackCursor = encodeCursor({ hash, repository: 0, offset: 0, mode: "symbol-only" });
|
|
1219
|
+
}
|
|
1025
1220
|
if (omissions.length > 0 && options.allowPartial !== true) {
|
|
1026
1221
|
return finalizeRepositorySearch({
|
|
1027
1222
|
schemaVersion: 1,
|
|
@@ -1040,7 +1235,28 @@ export async function searchRepositories(roots, query, options) {
|
|
|
1040
1235
|
results,
|
|
1041
1236
|
repositories: selected,
|
|
1042
1237
|
omissions,
|
|
1043
|
-
...(
|
|
1238
|
+
...(fallbackReason
|
|
1239
|
+
? {
|
|
1240
|
+
retrieval: {
|
|
1241
|
+
textFallback: true,
|
|
1242
|
+
reason: fallbackReason,
|
|
1243
|
+
inspectedRepositories: Math.min(25, selected.filter(({ health }) => health === "healthy").length),
|
|
1244
|
+
candidateLimit: TRACKED_TEXT_CANDIDATE_LIMIT,
|
|
1245
|
+
truncated: textFallbackTruncated,
|
|
1246
|
+
},
|
|
1247
|
+
}
|
|
1248
|
+
: {}),
|
|
1249
|
+
...(fallbackCursor
|
|
1250
|
+
? { cursor: fallbackCursor }
|
|
1251
|
+
: !fallbackReason && next
|
|
1252
|
+
? {
|
|
1253
|
+
cursor: encodeCursor({
|
|
1254
|
+
hash,
|
|
1255
|
+
...next,
|
|
1256
|
+
mode: start.mode === "symbol-only" ? "symbol-only" : "symbol",
|
|
1257
|
+
}),
|
|
1258
|
+
}
|
|
1259
|
+
: {}),
|
|
1044
1260
|
};
|
|
1045
1261
|
return finalizeRepositorySearch(base, byteLimit, tokenLimit, itemLimit);
|
|
1046
1262
|
}
|
|
@@ -42,9 +42,11 @@ const PROTECTED_CONTRACT_KEYS = new Set([
|
|
|
42
42
|
"operation",
|
|
43
43
|
"kind",
|
|
44
44
|
"name",
|
|
45
|
+
"client",
|
|
45
46
|
"target",
|
|
46
47
|
"to",
|
|
47
48
|
]);
|
|
49
|
+
const PROTECTED_CONTRACT_ARRAY_PATHS = new Set(["$/agents/clients"]);
|
|
48
50
|
function isTruncatableDetail(entry) {
|
|
49
51
|
return (!entry.path.startsWith("$/sharedIndex/") &&
|
|
50
52
|
(typeof entry.key !== "string" || !PROTECTED_CONTRACT_KEYS.has(entry.key)));
|
|
@@ -105,7 +107,9 @@ function truncatePayloadToBytes(root, byteLimit) {
|
|
|
105
107
|
}
|
|
106
108
|
while (bytes(root) > byteLimit) {
|
|
107
109
|
const candidate = collectArrays(root)
|
|
108
|
-
.filter((entry) => !entry.path.startsWith("$/responseBudget") &&
|
|
110
|
+
.filter((entry) => !entry.path.startsWith("$/responseBudget") &&
|
|
111
|
+
!PROTECTED_CONTRACT_ARRAY_PATHS.has(entry.path) &&
|
|
112
|
+
entry.value.length > 0)
|
|
109
113
|
.sort((a, b) => bytes(b.value) - bytes(a.value))[0];
|
|
110
114
|
if (!candidate)
|
|
111
115
|
break;
|
package/dist/src/server.js
CHANGED
|
@@ -23,6 +23,7 @@ const RETRY_SAFE_OPERATIONS = new Set([
|
|
|
23
23
|
"explain",
|
|
24
24
|
"map",
|
|
25
25
|
"query",
|
|
26
|
+
"query_batch",
|
|
26
27
|
"review",
|
|
27
28
|
"search",
|
|
28
29
|
"status",
|
|
@@ -69,7 +70,9 @@ export function recordMcpDiagnosticFailure(args, error, defaultRepo = process.cw
|
|
|
69
70
|
});
|
|
70
71
|
const message = error instanceof Error ? error.message : String(error);
|
|
71
72
|
return diagnostic.recorded
|
|
72
|
-
? new Error(`${message} [diagnostic ${diagnostic.correlationId}]`, {
|
|
73
|
+
? new Error(`${message} [diagnostic ${diagnostic.correlationId}]`, {
|
|
74
|
+
cause: error,
|
|
75
|
+
})
|
|
73
76
|
: error;
|
|
74
77
|
}
|
|
75
78
|
export function createServer(dispatch = handleKnodinTool) {
|
|
@@ -111,7 +114,11 @@ export function createServer(dispatch = handleKnodinTool) {
|
|
|
111
114
|
void extra
|
|
112
115
|
.sendNotification({
|
|
113
116
|
method: "notifications/progress",
|
|
114
|
-
params: {
|
|
117
|
+
params: {
|
|
118
|
+
progressToken,
|
|
119
|
+
progress: sequence,
|
|
120
|
+
message: phase,
|
|
121
|
+
},
|
|
115
122
|
})
|
|
116
123
|
.catch(ignoreNotificationFailure);
|
|
117
124
|
})
|
|
@@ -121,7 +128,9 @@ export function createServer(dispatch = handleKnodinTool) {
|
|
|
121
128
|
if (progressToken === undefined)
|
|
122
129
|
return;
|
|
123
130
|
progressSequence++;
|
|
124
|
-
recordMcpLifecycle(context, "progress", {
|
|
131
|
+
recordMcpLifecycle(context, "progress", {
|
|
132
|
+
sequence: progressSequence,
|
|
133
|
+
});
|
|
125
134
|
await extra.sendNotification({
|
|
126
135
|
method: "notifications/progress",
|
|
127
136
|
params: {
|
|
@@ -143,7 +152,9 @@ export function createServer(dispatch = handleKnodinTool) {
|
|
|
143
152
|
}, { cooperativeCancellation: operation === "repair" })),
|
|
144
153
|
};
|
|
145
154
|
const result = execution.result;
|
|
146
|
-
recordMcpLifecycle(context, "success", {
|
|
155
|
+
recordMcpLifecycle(context, "success", {
|
|
156
|
+
elapsedMs: Date.now() - startedAt,
|
|
157
|
+
});
|
|
147
158
|
return {
|
|
148
159
|
_meta: {
|
|
149
160
|
requestId: context.requestId,
|
|
@@ -192,7 +203,9 @@ export function createServer(dispatch = handleKnodinTool) {
|
|
|
192
203
|
finally {
|
|
193
204
|
if (heartbeat)
|
|
194
205
|
clearInterval(heartbeat);
|
|
195
|
-
recordMcpLifecycle(context, "cleanup", {
|
|
206
|
+
recordMcpLifecycle(context, "cleanup", {
|
|
207
|
+
elapsedMs: Date.now() - startedAt,
|
|
208
|
+
});
|
|
196
209
|
}
|
|
197
210
|
});
|
|
198
211
|
return server;
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { EMBEDDING_DIMENSIONS, EMBEDDING_MODEL_ID, EMBEDDING_MODEL_REVISION, } from "../engine/embeddings.js";
|
|
5
|
+
import { EMBEDDING_DIMENSIONS, EMBEDDING_MODEL_DTYPE, EMBEDDING_MODEL_ID, EMBEDDING_MODEL_REVISION, } from "../engine/embeddings.js";
|
|
6
6
|
import { KNODIN_SCHEMA_VERSION } from "../engine/index.js";
|
|
7
7
|
import { prunePolicyDescriptor } from "../engine/prune.js";
|
|
8
8
|
import { KNODIN_VERSION } from "../version.js";
|
|
@@ -44,6 +44,7 @@ export function currentSharedIndexCompatibility() {
|
|
|
44
44
|
id: EMBEDDING_MODEL_ID,
|
|
45
45
|
revision: EMBEDDING_MODEL_REVISION,
|
|
46
46
|
dimensions: EMBEDDING_DIMENSIONS,
|
|
47
|
+
dtype: EMBEDDING_MODEL_DTYPE,
|
|
47
48
|
runtime: `@huggingface/transformers@${packageVersion("@huggingface/transformers")}`,
|
|
48
49
|
};
|
|
49
50
|
const parsers = {
|
|
@@ -61,9 +62,10 @@ export function currentSharedIndexCompatibility() {
|
|
|
61
62
|
modelDigest: digest(model),
|
|
62
63
|
parserBundleDigest: digest(parsers),
|
|
63
64
|
indexConfigDigest: digest({
|
|
64
|
-
policyVersion:
|
|
65
|
+
policyVersion: 2,
|
|
65
66
|
databaseSchemaVersion: KNODIN_SCHEMA_VERSION,
|
|
66
67
|
embeddingDimensions: EMBEDDING_DIMENSIONS,
|
|
68
|
+
embeddingDtype: EMBEDDING_MODEL_DTYPE,
|
|
67
69
|
}),
|
|
68
70
|
prunePolicyDigest: digest(prunePolicyDescriptor()),
|
|
69
71
|
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
export const KNODIN_SKILLS = [
|
|
7
|
+
"knodin-explore",
|
|
8
|
+
"knodin-debug",
|
|
9
|
+
"knodin-impact",
|
|
10
|
+
"knodin-refactor",
|
|
11
|
+
"knodin-review",
|
|
12
|
+
"knodin-systems",
|
|
13
|
+
"knodin-lifecycle",
|
|
14
|
+
];
|
|
15
|
+
const MANIFEST = ".knodin-managed.json";
|
|
16
|
+
function canonicalRoot() {
|
|
17
|
+
const moduleDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const candidates = [
|
|
19
|
+
path.resolve(moduleDirectory, "..", "skills"),
|
|
20
|
+
path.resolve(moduleDirectory, "..", "..", "skills"),
|
|
21
|
+
];
|
|
22
|
+
const root = candidates.find((candidate) => fs.existsSync(candidate));
|
|
23
|
+
if (!root)
|
|
24
|
+
throw new Error("knodin packaged skills are missing");
|
|
25
|
+
return root;
|
|
26
|
+
}
|
|
27
|
+
function destinationRoot(client, scope, repo, home) {
|
|
28
|
+
if (scope === "team") {
|
|
29
|
+
if (client === "codex")
|
|
30
|
+
return path.join(repo, ".agents", "skills");
|
|
31
|
+
if (client === "claude")
|
|
32
|
+
return path.join(repo, ".claude", "skills");
|
|
33
|
+
return path.join(repo, ".gemini", "skills");
|
|
34
|
+
}
|
|
35
|
+
if (client === "codex")
|
|
36
|
+
return path.join(home, ".agents", "skills");
|
|
37
|
+
if (client === "claude")
|
|
38
|
+
return path.join(home, ".claude", "skills");
|
|
39
|
+
return path.join(home, ".gemini", "skills");
|
|
40
|
+
}
|
|
41
|
+
function sourceDigest(source) {
|
|
42
|
+
const hash = crypto.createHash("sha256");
|
|
43
|
+
for (const relative of ["SKILL.md", path.join("agents", "openai.yaml")])
|
|
44
|
+
hash.update(fs.readFileSync(path.join(source, relative)));
|
|
45
|
+
return hash.digest("hex");
|
|
46
|
+
}
|
|
47
|
+
function managedState(target) {
|
|
48
|
+
try {
|
|
49
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(target, MANIFEST), "utf8"));
|
|
50
|
+
if (manifest.owner !== "knodin")
|
|
51
|
+
return "unmanaged";
|
|
52
|
+
return manifest.digest === sourceDigest(target) ? "managed" : "modified";
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return "unmanaged";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function installKnodinSkills(options) {
|
|
59
|
+
const result = {
|
|
60
|
+
scope: options.scope,
|
|
61
|
+
installed: [],
|
|
62
|
+
removed: [],
|
|
63
|
+
unchanged: [],
|
|
64
|
+
issues: [],
|
|
65
|
+
};
|
|
66
|
+
if (options.scope === "cli-only")
|
|
67
|
+
return result;
|
|
68
|
+
const home = options.home ?? os.homedir();
|
|
69
|
+
for (const client of options.clients) {
|
|
70
|
+
const root = destinationRoot(client, options.scope, path.resolve(options.repo), home);
|
|
71
|
+
for (const name of KNODIN_SKILLS) {
|
|
72
|
+
const source = path.join(canonicalRoot(), name);
|
|
73
|
+
const target = path.join(root, name);
|
|
74
|
+
const digest = sourceDigest(source);
|
|
75
|
+
if (fs.existsSync(target)) {
|
|
76
|
+
const state = managedState(target);
|
|
77
|
+
if (state !== "managed") {
|
|
78
|
+
result.issues.push({
|
|
79
|
+
path: target,
|
|
80
|
+
message: `${state} skill collision; left unchanged`,
|
|
81
|
+
});
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const current = JSON.parse(fs.readFileSync(path.join(target, MANIFEST), "utf8"));
|
|
87
|
+
if (current.digest === digest) {
|
|
88
|
+
result.unchanged.push(target);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Missing managed version is installed below.
|
|
94
|
+
}
|
|
95
|
+
result.installed.push(target);
|
|
96
|
+
if (options.dryRun)
|
|
97
|
+
continue;
|
|
98
|
+
fs.mkdirSync(root, { recursive: true });
|
|
99
|
+
const temporary = path.join(root, `.${name}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`);
|
|
100
|
+
fs.cpSync(source, temporary, { recursive: true, errorOnExist: true });
|
|
101
|
+
fs.writeFileSync(path.join(temporary, MANIFEST), `${JSON.stringify({ owner: "knodin", digest, client }, null, 2)}\n`);
|
|
102
|
+
if (fs.existsSync(target))
|
|
103
|
+
fs.renameSync(target, `${temporary}.old`);
|
|
104
|
+
fs.renameSync(temporary, target);
|
|
105
|
+
fs.rmSync(`${temporary}.old`, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
export function removeKnodinSkills(options) {
|
|
111
|
+
const result = {
|
|
112
|
+
scope: options.scope,
|
|
113
|
+
installed: [],
|
|
114
|
+
removed: [],
|
|
115
|
+
unchanged: [],
|
|
116
|
+
issues: [],
|
|
117
|
+
};
|
|
118
|
+
const home = options.home ?? os.homedir();
|
|
119
|
+
for (const client of options.clients) {
|
|
120
|
+
const root = destinationRoot(client, options.scope, path.resolve(options.repo), home);
|
|
121
|
+
for (const name of KNODIN_SKILLS) {
|
|
122
|
+
const target = path.join(root, name);
|
|
123
|
+
if (!fs.existsSync(target))
|
|
124
|
+
continue;
|
|
125
|
+
const state = managedState(target);
|
|
126
|
+
if (state !== "managed") {
|
|
127
|
+
result.issues.push({ path: target, message: `${state} skill collision; left unchanged` });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
result.removed.push(target);
|
|
131
|
+
if (!options.dryRun)
|
|
132
|
+
fs.rmSync(target, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
if (!options.dryRun) {
|
|
135
|
+
try {
|
|
136
|
+
fs.rmdirSync(root);
|
|
137
|
+
fs.rmdirSync(path.dirname(root));
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (!["ENOENT", "ENOTEMPTY"].includes(error.code ?? ""))
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
export function inspectKnodinSkills(options) {
|
|
148
|
+
return installKnodinSkills({ ...options, dryRun: true });
|
|
149
|
+
}
|