@kage-core/kage-graph-mcp 2.5.3 → 2.5.5
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/dist/cli.js +32 -39
- package/dist/kernel.js +143 -76
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -708,6 +708,22 @@ async function main() {
|
|
|
708
708
|
}
|
|
709
709
|
return;
|
|
710
710
|
}
|
|
711
|
+
if (command === "gen-plugin-hooks") {
|
|
712
|
+
// Maintainer tool: regenerate plugin/hooks/* from the setupAgent("claude-code") templates
|
|
713
|
+
// so the plugin and the npm install path ship identical hooks (one source of truth).
|
|
714
|
+
const pluginDir = takeArg(args, "--plugin-dir") ?? (0, node_path_1.join)(process.cwd(), "plugin");
|
|
715
|
+
const result = (0, kernel_js_1.generatePluginHooks)(pluginDir);
|
|
716
|
+
if (args.includes("--json")) {
|
|
717
|
+
console.log(JSON.stringify({ plugin_dir: pluginDir, ...result }, null, 2));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
console.log(`Generated plugin hooks in ${(0, node_path_1.join)(pluginDir, "hooks")}`);
|
|
721
|
+
console.log(` scripts: ${result.scripts.join(", ")}`);
|
|
722
|
+
console.log(` events: ${result.events.join(", ")}`);
|
|
723
|
+
if (result.removed.length)
|
|
724
|
+
console.log(` removed: ${result.removed.join(", ")}`);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
711
727
|
if (command === "daemon") {
|
|
712
728
|
const action = args[1];
|
|
713
729
|
const projectDir = projectArg(args);
|
|
@@ -1396,58 +1412,35 @@ async function main() {
|
|
|
1396
1412
|
const path = takeArg(args, "--path");
|
|
1397
1413
|
if (!path)
|
|
1398
1414
|
usage();
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
if (args.includes("--json")) {
|
|
1406
|
-
console.log(JSON.stringify({ ...result, nudges: nudges.items }, null, 2));
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
const parts = [];
|
|
1410
|
-
if (result.context_block)
|
|
1411
|
-
parts.push(result.context_block);
|
|
1412
|
-
if (nudges.block)
|
|
1413
|
-
parts.push(nudges.block.trimStart());
|
|
1414
|
-
if (parts.length)
|
|
1415
|
-
console.log(parts.join("\n\n"));
|
|
1416
|
-
// No verified packets cite this file and no nudge targets it: print nothing so hooks
|
|
1417
|
-
// can gate on empty output.
|
|
1415
|
+
const result = (0, kernel_js_1.kageFileContext)(projectArg(args), path);
|
|
1416
|
+
if (args.includes("--json"))
|
|
1417
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1418
|
+
else if (result.context_block)
|
|
1419
|
+
console.log(result.context_block);
|
|
1420
|
+
// No verified packets cite this file: print nothing so hooks can gate on empty output.
|
|
1418
1421
|
return;
|
|
1419
1422
|
}
|
|
1420
1423
|
if (command === "prompt-context") {
|
|
1421
1424
|
// Top-of-task recall for an ambient UserPromptSubmit hook: recall on the user's prompt
|
|
1422
|
-
// and emit the memory
|
|
1423
|
-
// Silent when nothing relevant is found, so hooks can gate on empty output.
|
|
1425
|
+
// and emit the memory. Silent when nothing relevant is found, so hooks can gate on empty.
|
|
1424
1426
|
const query = takeArg(args, "--query") ?? firstPositional(args);
|
|
1425
1427
|
if (!query)
|
|
1426
1428
|
usage();
|
|
1427
1429
|
const project = projectArg(args);
|
|
1428
1430
|
const result = (0, kernel_js_1.recall)(project, query, 5, false, {});
|
|
1429
|
-
// Surface any nudges the kage-watcher wrote (marks them surfaced so each shows once) —
|
|
1430
|
-
// this is what makes recall/nudges felt instead of silently sitting in a file.
|
|
1431
|
-
const nudges = (0, kernel_js_1.surfacePendingNudges)(project);
|
|
1432
1431
|
if (args.includes("--json")) {
|
|
1433
|
-
console.log(JSON.stringify(
|
|
1432
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1434
1433
|
return;
|
|
1435
1434
|
}
|
|
1436
|
-
|
|
1437
|
-
if (result.results.length) {
|
|
1438
|
-
// Lead with felt behavior, not a vanity/gameable token number: tell the agent these are
|
|
1439
|
-
// verified team memories to follow. (The tokens/$ ledger lives in `kage gains`.) Keep
|
|
1440
|
-
// only the trust-relevant stale-withheld signal here.
|
|
1441
|
-
const plural = result.results.length === 1 ? "y" : "ies";
|
|
1442
|
-
const withheld = result.value_receipt?.stale_withheld ?? 0;
|
|
1443
|
-
parts.push(result.context_block);
|
|
1444
|
-
parts.push(`_${result.results.length} verified team memor${plural} above — follow them.${withheld ? ` ${withheld} stale memor${withheld === 1 ? "y" : "ies"} withheld (code changed under them).` : ""}_`);
|
|
1445
|
-
}
|
|
1446
|
-
if (nudges.block)
|
|
1447
|
-
parts.push(nudges.block.trimStart());
|
|
1448
|
-
if (!parts.length)
|
|
1435
|
+
if (!result.results.length)
|
|
1449
1436
|
return;
|
|
1450
|
-
|
|
1437
|
+
// Lead with felt behavior, not a vanity/gameable token number: tell the agent these are
|
|
1438
|
+
// verified team memories to follow. (The tokens/$ ledger lives in `kage gains`.) Keep
|
|
1439
|
+
// only the trust-relevant stale-withheld signal here.
|
|
1440
|
+
const plural = result.results.length === 1 ? "y" : "ies";
|
|
1441
|
+
const withheld = result.value_receipt?.stale_withheld ?? 0;
|
|
1442
|
+
console.log(result.context_block);
|
|
1443
|
+
console.log(`_${result.results.length} verified team memor${plural} above — follow them.${withheld ? ` ${withheld} stale memor${withheld === 1 ? "y" : "ies"} withheld (code changed under them).` : ""}_`);
|
|
1451
1444
|
return;
|
|
1452
1445
|
}
|
|
1453
1446
|
if (command === "memory-access") {
|
package/dist/kernel.js
CHANGED
|
@@ -102,7 +102,7 @@ exports.compactProject = compactProject;
|
|
|
102
102
|
exports.installAgentPolicy = installAgentPolicy;
|
|
103
103
|
exports.createDenseEmbeddingProvider = createDenseEmbeddingProvider;
|
|
104
104
|
exports.buildEmbeddingIndex = buildEmbeddingIndex;
|
|
105
|
-
exports.
|
|
105
|
+
exports.isUngroundedConversationalCapture = isUngroundedConversationalCapture;
|
|
106
106
|
exports.recall = recall;
|
|
107
107
|
exports.recallWithEmbeddings = recallWithEmbeddings;
|
|
108
108
|
exports.queryCodeGraph = queryCodeGraph;
|
|
@@ -151,6 +151,7 @@ exports.capture = capture;
|
|
|
151
151
|
exports.createPublicCandidate = createPublicCandidate;
|
|
152
152
|
exports.registryRecommendations = registryRecommendations;
|
|
153
153
|
exports.setupAgent = setupAgent;
|
|
154
|
+
exports.generatePluginHooks = generatePluginHooks;
|
|
154
155
|
exports.setupDoctor = setupDoctor;
|
|
155
156
|
exports.verifyAgentActivation = verifyAgentActivation;
|
|
156
157
|
exports.observe = observe;
|
|
@@ -1204,9 +1205,8 @@ function replayTokensSaved(packets, contextBlock) {
|
|
|
1204
1205
|
}
|
|
1205
1206
|
const FILE_CONTEXT_PACKET_CAP = 3;
|
|
1206
1207
|
// Normalize a file path to the repo-relative form used for packet citation matching:
|
|
1207
|
-
// absolute paths are made relative to the project root, backslashes/leading "./"
|
|
1208
|
-
// stripped. Returns "" for an empty/blank input.
|
|
1209
|
-
// and edit-time nudge matching so both compare paths identically.
|
|
1208
|
+
// absolute paths are made relative to the project root, backslashes/leading "./" and
|
|
1209
|
+
// trailing slashes are stripped. Returns "" for an empty/blank input.
|
|
1210
1210
|
function normalizeRepoPath(projectDir, filePath) {
|
|
1211
1211
|
let rel = (filePath ?? "").trim().replace(/\\/g, "/");
|
|
1212
1212
|
if (!rel)
|
|
@@ -2135,9 +2135,18 @@ function evaluateMemoryAdmission(projectDir, packet) {
|
|
|
2135
2135
|
score -= 10;
|
|
2136
2136
|
risks.push("too little context");
|
|
2137
2137
|
}
|
|
2138
|
+
// Ungrounded conversational chatter (a path-less, repo-reference-free user outburst) is not
|
|
2139
|
+
// durable memory regardless of its other signals — keywords like "issue"/"before" can
|
|
2140
|
+
// otherwise score it as a candidate. Hard-block admission so it can never auto-promote.
|
|
2141
|
+
if (isUngroundedConversationalCapture(packet)) {
|
|
2142
|
+
score -= 45;
|
|
2143
|
+
risks.push("ungrounded conversational utterance, not durable knowledge");
|
|
2144
|
+
}
|
|
2138
2145
|
const bounded = Math.max(0, Math.min(100, score));
|
|
2139
2146
|
return {
|
|
2140
|
-
admit: bounded >= 45
|
|
2147
|
+
admit: bounded >= 45
|
|
2148
|
+
&& risks.indexOf("session bookkeeping, not durable knowledge") === -1
|
|
2149
|
+
&& risks.indexOf("ungrounded conversational utterance, not durable knowledge") === -1,
|
|
2141
2150
|
class: bounded >= 72 ? "high_signal" : bounded >= 45 ? "candidate" : "episodic_only",
|
|
2142
2151
|
score: bounded,
|
|
2143
2152
|
reasons,
|
|
@@ -7972,63 +7981,61 @@ function isSerializedDumpBody(body) {
|
|
|
7972
7981
|
return isSerializedDumpTitle(t)
|
|
7973
7982
|
|| /<task-notification\b|<tool-use-id\b|"hookSpecificOutput"|"isImage"\s*:|"noOutputExpected"\s*:|"interrupted"\s*:\s*(true|false)/i.test(t.slice(0, 4000));
|
|
7974
7983
|
}
|
|
7975
|
-
//
|
|
7976
|
-
//
|
|
7977
|
-
//
|
|
7978
|
-
//
|
|
7979
|
-
//
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
const
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
const
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
...items.map((item) => `- ${item.file ? `${item.file}: ` : ""}${item.message}`)].join("\n");
|
|
8031
|
-
return { block, items, count: items.length };
|
|
7984
|
+
// Repo grounding signals — does the text point at something concrete in the codebase? These
|
|
7985
|
+
// three predicates are the shared vocabulary for "grounded": observationSignalScore weights
|
|
7986
|
+
// them into its durable-knowledge score, and the ungrounded-utterance guard uses their union
|
|
7987
|
+
// to decide whether a path-less capture references the repo at all. Kept as one source of
|
|
7988
|
+
// truth so the scorer and the guard can never drift on what "grounded" means.
|
|
7989
|
+
function textCitesPath(text) {
|
|
7990
|
+
return /\b[\w.-]+\/[\w./-]+\b/.test(text)
|
|
7991
|
+
|| /\b[\w-]+\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|rb|json|ya?ml|toml|md|sh|css|html|sql)\b/i.test(text);
|
|
7992
|
+
}
|
|
7993
|
+
function textHasCodeIdentifier(text) {
|
|
7994
|
+
return /\b[a-z][a-z0-9]*[A-Z]\w*\b/.test(text) // camelCase
|
|
7995
|
+
|| /\b[a-z0-9]+_[a-z0-9_]+\b/.test(text) // snake_case
|
|
7996
|
+
|| /`[^`]+`/.test(text)
|
|
7997
|
+
|| /\b\w+\(\)/.test(text);
|
|
7998
|
+
}
|
|
7999
|
+
function textHasCommand(text) {
|
|
8000
|
+
return /(^|\s)(npm|pnpm|yarn|npx|node|git|cargo|make|pytest|go|tsc|kage)\s+[\w.-]/.test(text);
|
|
8001
|
+
}
|
|
8002
|
+
function hasRepoGroundingSignal(text) {
|
|
8003
|
+
const t = text ?? "";
|
|
8004
|
+
if (!t.trim())
|
|
8005
|
+
return false;
|
|
8006
|
+
return textCitesPath(t) || textHasCodeIdentifier(t) || textHasCommand(t);
|
|
8007
|
+
}
|
|
8008
|
+
// A raw conversational user utterance: a frustrated/rhetorical/imperative chat message aimed
|
|
8009
|
+
// at the agent — e.g. "why are you asking me???!!! it's your job, don't stop before you...".
|
|
8010
|
+
// It is plain prose, so the serialized-dump guard waves it through, yet it is chatter, not
|
|
8011
|
+
// durable repo knowledge. Detection is deliberately narrow — a run of emphatic terminal
|
|
8012
|
+
// punctuation, or one of a curated set of rhetorical / second-person-at-the-assistant
|
|
8013
|
+
// phrases — so it stays clear of normal declarative learnings (which read as statements, not
|
|
8014
|
+
// outbursts addressed to "you").
|
|
8015
|
+
function looksLikeRawUserUtterance(text) {
|
|
8016
|
+
const t = (text ?? "").trim();
|
|
8017
|
+
if (!t)
|
|
8018
|
+
return false;
|
|
8019
|
+
// Emphatic or mixed terminal punctuation ("???", "!!", "?!") — common in venting chat,
|
|
8020
|
+
// essentially absent from curated memory prose.
|
|
8021
|
+
if (/[!?]{2,}/.test(t))
|
|
8022
|
+
return true;
|
|
8023
|
+
// Rhetorical questions / second-person-imperative frustration directed at the assistant.
|
|
8024
|
+
return /\b(why are you|why did you|why would you|why aren'?t you|are you (kidding|serious|even|really)|it'?s your job|that'?s your job|do your job|don'?t stop|stop asking|stop before you|keep going|hurry up|just do it|figure it out yourself|i (already )?told you)\b/i.test(t);
|
|
8025
|
+
}
|
|
8026
|
+
// Capture-noise guard for ungrounded chat. A packet trips it only when ALL hold: it cites zero
|
|
8027
|
+
// repo paths, its text carries no repo grounding signal (no path/file/identifier/command), and
|
|
8028
|
+
// it reads as a raw conversational user utterance. The conjunction is what keeps it safe — a
|
|
8029
|
+
// real ungrounded decision or convention names a symbol, file, command, or rule and so is never
|
|
8030
|
+
// caught. Such packets are routed to pending (not auto-approved) at capture time and withheld
|
|
8031
|
+
// from recall, mirroring how serialized dumps are gated.
|
|
8032
|
+
function isUngroundedConversationalCapture(packet) {
|
|
8033
|
+
if (packet.paths && packet.paths.length > 0)
|
|
8034
|
+
return false;
|
|
8035
|
+
const text = `${packet.title ?? ""}\n${packet.body ?? ""}`;
|
|
8036
|
+
if (hasRepoGroundingSignal(text))
|
|
8037
|
+
return false;
|
|
8038
|
+
return looksLikeRawUserUtterance(text);
|
|
8032
8039
|
}
|
|
8033
8040
|
// Collapse whitespace and hard-cap a value rendered inline in a context block, so one
|
|
8034
8041
|
// oversized field (e.g. a graph fact whose body is a raw transcript) can never blow up
|
|
@@ -8115,7 +8122,7 @@ function recallWithVectorScores(projectDir, query, limit = 5, explain = false, i
|
|
|
8115
8122
|
];
|
|
8116
8123
|
return { packet, score: score_breakdown.final, relevance, why_matched: unique(why).slice(0, 12), score_breakdown };
|
|
8117
8124
|
})
|
|
8118
|
-
.filter((entry) => entry.relevance > 0 && !isSerializedDumpTitle(entry.packet.title))
|
|
8125
|
+
.filter((entry) => entry.relevance > 0 && !isSerializedDumpTitle(entry.packet.title) && !isUngroundedConversationalCapture(entry.packet))
|
|
8119
8126
|
.sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title));
|
|
8120
8127
|
const scored = diversifyRecallEntries(rankedScored, limit)
|
|
8121
8128
|
.map(({ relevance, ...entry }) => entry);
|
|
@@ -8127,7 +8134,7 @@ function recallWithVectorScores(projectDir, query, limit = 5, explain = false, i
|
|
|
8127
8134
|
const { score, why } = pendingLexicalScores.get(packet.id) ?? { score: 0, why: [] };
|
|
8128
8135
|
return { packet, score, why_matched: why };
|
|
8129
8136
|
})
|
|
8130
|
-
.filter((entry) => entry.score > 0)
|
|
8137
|
+
.filter((entry) => entry.score > 0 && !isUngroundedConversationalCapture(entry.packet))
|
|
8131
8138
|
.sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title))
|
|
8132
8139
|
.filter((entry) => {
|
|
8133
8140
|
const key = `${entry.packet.type}:${entry.packet.title.toLowerCase()}:${entry.packet.paths.join(",")}`;
|
|
@@ -13771,6 +13778,20 @@ function capture(input) {
|
|
|
13771
13778
|
if (missingPaths.length) {
|
|
13772
13779
|
warnings.push(`Some referenced paths do not exist in this repo: ${missingPaths.join(", ")}`);
|
|
13773
13780
|
}
|
|
13781
|
+
// Ungrounded conversational chatter — a frustrated/rhetorical user message with no cited repo
|
|
13782
|
+
// paths — is not durable memory. Don't hard-reject (a reviewer may still salvage it); instead
|
|
13783
|
+
// deny it the auto-approve fast path so it lands in the pending inbox, tagged with why. Recall
|
|
13784
|
+
// withholds it regardless of status. Explicit pendingReview captures already route to review.
|
|
13785
|
+
const ungroundedUtterance = isUngroundedConversationalCapture({
|
|
13786
|
+
title: input.title,
|
|
13787
|
+
body: input.body,
|
|
13788
|
+
paths: groundedPaths,
|
|
13789
|
+
});
|
|
13790
|
+
const routeToPending = Boolean(input.pendingReview) || ungroundedUtterance;
|
|
13791
|
+
if (ungroundedUtterance && !input.pendingReview) {
|
|
13792
|
+
warnings.push("Routed to pending review: this reads as an ungrounded conversational message with no cited repo paths, " +
|
|
13793
|
+
"not a durable learning. Cite a repo path or restate it as a reusable insight to approve it.");
|
|
13794
|
+
}
|
|
13774
13795
|
const createdAt = nowIso();
|
|
13775
13796
|
// Agent-asserted links to code-graph nodes (PRD `graph_nodes`): the agent recording the
|
|
13776
13797
|
// memory knows which symbol/route/file the rule is about, so let it declare them instead
|
|
@@ -13789,9 +13810,9 @@ function capture(input) {
|
|
|
13789
13810
|
scope: "repo",
|
|
13790
13811
|
visibility: "team",
|
|
13791
13812
|
sensitivity: "internal",
|
|
13792
|
-
status:
|
|
13813
|
+
status: routeToPending ? "pending" : "approved",
|
|
13793
13814
|
confidence: DEFAULT_CONFIDENCE,
|
|
13794
|
-
tags: input.tags ?? [],
|
|
13815
|
+
tags: ungroundedUtterance ? unique([...(input.tags ?? []), "needs-grounding"]) : (input.tags ?? []),
|
|
13795
13816
|
paths: groundedPaths,
|
|
13796
13817
|
stack: input.stack ?? [],
|
|
13797
13818
|
source_refs: [
|
|
@@ -13853,7 +13874,7 @@ function capture(input) {
|
|
|
13853
13874
|
...evaluateMemoryQuality(input.projectDir, packet),
|
|
13854
13875
|
...(contradictions.length ? { contradicts: contradictions.map((c) => c.packet_id) } : {}),
|
|
13855
13876
|
};
|
|
13856
|
-
const path = writePacket(input.projectDir, packet,
|
|
13877
|
+
const path = writePacket(input.projectDir, packet, routeToPending ? "pending" : "packets");
|
|
13857
13878
|
recordMemoryAudit(input.projectDir, "capture", [packet], {
|
|
13858
13879
|
type: packet.type,
|
|
13859
13880
|
status: packet.status,
|
|
@@ -14414,6 +14435,58 @@ exit 0
|
|
|
14414
14435
|
setSnippet(paths[agent] || null, universal, [`Merge this MCP stdio config into ${agent}'s MCP settings.`, "Restart the agent after updating config."]);
|
|
14415
14436
|
return result;
|
|
14416
14437
|
}
|
|
14438
|
+
// Single source of truth for Claude Code hooks. The plugin's hook scripts and hooks.json are
|
|
14439
|
+
// GENERATED from the exact same setupAgent("claude-code") output the npm install path writes —
|
|
14440
|
+
// so a hook fix lands in both channels and they can't silently drift. The hook scripts call the
|
|
14441
|
+
// kage CLI and never reference their own location, so the same bodies serve both the ~/.claude
|
|
14442
|
+
// install and the plugin; only the wiring's command path is re-targeted to ${CLAUDE_PLUGIN_ROOT}.
|
|
14443
|
+
// Run via `kage gen-plugin-hooks`; a unit test asserts the committed plugin/hooks match this.
|
|
14444
|
+
function generatePluginHooks(pluginDir) {
|
|
14445
|
+
const tmpHome = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "kage-plugin-home-"));
|
|
14446
|
+
const tmpProject = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "kage-plugin-proj-"));
|
|
14447
|
+
try {
|
|
14448
|
+
setupAgent("claude-code", tmpProject, { write: true, homeDir: tmpHome });
|
|
14449
|
+
const srcHookDir = (0, node_path_1.join)(tmpHome, ".claude", "kage", "hooks");
|
|
14450
|
+
const settings = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(tmpHome, ".claude", "settings.json"), "utf8"));
|
|
14451
|
+
const hooksOut = (0, node_path_1.join)(pluginDir, "hooks");
|
|
14452
|
+
(0, node_fs_1.mkdirSync)(hooksOut, { recursive: true });
|
|
14453
|
+
// Copy the generated hook scripts verbatim (path-agnostic — they shell out to `kage`).
|
|
14454
|
+
const scripts = (0, node_fs_1.readdirSync)(srcHookDir).filter((f) => f.endsWith(".sh")).sort();
|
|
14455
|
+
for (const file of scripts) {
|
|
14456
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(hooksOut, file), (0, node_fs_1.readFileSync)((0, node_path_1.join)(srcHookDir, file), "utf8"), { mode: 0o755 });
|
|
14457
|
+
}
|
|
14458
|
+
// Re-target the event wiring from the install path's `~/.claude/kage/hooks/` to the plugin's
|
|
14459
|
+
// `${CLAUDE_PLUGIN_ROOT}/hooks/`, preserving events, matchers, and timeouts exactly.
|
|
14460
|
+
const events = Object.keys(settings.hooks ?? {});
|
|
14461
|
+
const pluginHooks = { hooks: {} };
|
|
14462
|
+
for (const event of events) {
|
|
14463
|
+
pluginHooks.hooks[event] = (settings.hooks ?? {})[event].map((entry) => ({
|
|
14464
|
+
...(entry.matcher !== undefined ? { matcher: entry.matcher } : {}),
|
|
14465
|
+
hooks: entry.hooks.map((h) => {
|
|
14466
|
+
const m = h.command.match(/^bash ~\/\.claude\/kage\/hooks\/(.+)$/);
|
|
14467
|
+
return {
|
|
14468
|
+
type: h.type,
|
|
14469
|
+
command: m ? `bash "\${CLAUDE_PLUGIN_ROOT}/hooks/${m[1]}"` : h.command,
|
|
14470
|
+
...(h.timeout !== undefined ? { timeout: h.timeout } : {}),
|
|
14471
|
+
};
|
|
14472
|
+
}),
|
|
14473
|
+
}));
|
|
14474
|
+
}
|
|
14475
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(hooksOut, "hooks.json"), `${JSON.stringify(pluginHooks, null, 2)}\n`, "utf8");
|
|
14476
|
+
// Drop committed scripts the install path no longer emits (e.g. kage-prompt-context.sh,
|
|
14477
|
+
// now subsumed by observe.sh handling UserPromptSubmit).
|
|
14478
|
+
const removed = [];
|
|
14479
|
+
for (const stale of (0, node_fs_1.readdirSync)(hooksOut).filter((f) => f.endsWith(".sh") && !scripts.includes(f))) {
|
|
14480
|
+
(0, node_fs_1.unlinkSync)((0, node_path_1.join)(hooksOut, stale));
|
|
14481
|
+
removed.push(stale);
|
|
14482
|
+
}
|
|
14483
|
+
return { scripts, removed, events };
|
|
14484
|
+
}
|
|
14485
|
+
finally {
|
|
14486
|
+
(0, node_fs_1.rmSync)(tmpHome, { recursive: true, force: true });
|
|
14487
|
+
(0, node_fs_1.rmSync)(tmpProject, { recursive: true, force: true });
|
|
14488
|
+
}
|
|
14489
|
+
}
|
|
14417
14490
|
function upsertJsonMcpServer(path, name, server) {
|
|
14418
14491
|
ensureDir((0, node_path_1.dirname)(path));
|
|
14419
14492
|
let config = {};
|
|
@@ -14816,19 +14889,13 @@ function observationSignalScore(observation) {
|
|
|
14816
14889
|
const causalHits = SIGNAL_CAUSAL_MARKERS.filter((marker) => lower.includes(marker)).length;
|
|
14817
14890
|
if (causalHits > 0)
|
|
14818
14891
|
score += Math.min(0.35, 0.25 + (causalHits - 1) * 0.05);
|
|
14819
|
-
const citesPath = Boolean(observation.path)
|
|
14820
|
-
|| /\b[\w.-]+\/[\w./-]+\b/.test(prose)
|
|
14821
|
-
|| /\b[\w-]+\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|rb|json|ya?ml|toml|md|sh|css|html|sql)\b/i.test(prose);
|
|
14892
|
+
const citesPath = Boolean(observation.path) || textCitesPath(prose);
|
|
14822
14893
|
if (citesPath)
|
|
14823
14894
|
score += 0.2;
|
|
14824
|
-
const codeIdentifier =
|
|
14825
|
-
|| /\b[a-z0-9]+_[a-z0-9_]+\b/.test(prose) // snake_case
|
|
14826
|
-
|| /`[^`]+`/.test(prose)
|
|
14827
|
-
|| /\b\w+\(\)/.test(prose);
|
|
14895
|
+
const codeIdentifier = textHasCodeIdentifier(prose);
|
|
14828
14896
|
if (codeIdentifier)
|
|
14829
14897
|
score += 0.15;
|
|
14830
|
-
const commandLine = Boolean(observation.command)
|
|
14831
|
-
|| /(^|\s)(npm|pnpm|yarn|npx|node|git|cargo|make|pytest|go|tsc|kage)\s+[\w.-]/.test(prose);
|
|
14898
|
+
const commandLine = Boolean(observation.command) || textHasCommand(prose);
|
|
14832
14899
|
if (commandLine)
|
|
14833
14900
|
score += 0.15;
|
|
14834
14901
|
const words = combined.split(/\s+/).filter(Boolean).length;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kage-core/kage-graph-mcp",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.5",
|
|
4
4
|
"description": "Team memory for coding agents: captures the decisions, runbooks, and bug fixes that get lost, verified against your code and shared via git. MCP server, zero deps, no account.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|