Some checks failed
CI — Test & Build / Test & Build (push) Has been cancelled
- Implemented a status bar item for LexAI with dynamic status updates (ready, processing, notReady). - Created a suggestion panel for displaying and interacting with AI-generated suggestions. - Added functionality for accepting, regenerating, and discarding suggestions within the suggestion zone. - Introduced configuration options for writing style, prompt patterns, personas, and formats. - Integrated progress indicators for long-running tasks and improved user feedback. - Established TypeScript configuration for the vscode package.
149 lines
5.4 KiB
JavaScript
149 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* sessionStart hook — inject the facts a session should never have to ask for.
|
|
*
|
|
* 1. Are the model lanes bound, and is the picker running the model we recorded?
|
|
* 2. What did the last session leave as the next action?
|
|
* 3. Is any capped context file over its cap right now?
|
|
*
|
|
* All three are deterministic file/state checks. The point of doing them here rather
|
|
* than in a prompt is that a rule asking the model to "check the caps" is a request;
|
|
* this is an observation.
|
|
*
|
|
* Contract: stdin is the sessionStart JSON payload; stdout is
|
|
* {"additional_context": "..."}. This hook is fire-and-forget — Cursor logs the
|
|
* response but never blocks session creation on it. It fails open by design:
|
|
* any error prints an empty object and exits 0.
|
|
*/
|
|
|
|
import { readFileSync, existsSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
const readAll = () =>
|
|
new Promise((resolve) => {
|
|
let data = "";
|
|
process.stdin.setEncoding("utf8");
|
|
process.stdin.on("data", (c) => (data += c));
|
|
process.stdin.on("end", () => resolve(data));
|
|
setTimeout(() => resolve(data), 2000).unref();
|
|
});
|
|
|
|
const read = (p) => {
|
|
try {
|
|
return existsSync(p) ? readFileSync(p, "utf8") : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Real content lines: not blank, not a heading, not the file's instructional blockquote,
|
|
* not a table rule, not an HTML comment, and not one of the shipped `_None yet._` /
|
|
* `[placeholder]` template rows. Counting boilerplate would report a freshly installed
|
|
* kit as already consuming its caps.
|
|
*/
|
|
const isPlaceholder = (l) =>
|
|
/^[-|*\s]*_?(none|no )/i.test(l) ||
|
|
/^\|?\s*_?\[/.test(l) ||
|
|
/^-\s*\*\(/.test(l) ||
|
|
/^\|\s*\[/.test(l);
|
|
|
|
const contentLines = (text) =>
|
|
text
|
|
.split("\n")
|
|
.map((l) => l.trim())
|
|
.filter(
|
|
(l) =>
|
|
l &&
|
|
!l.startsWith("#") &&
|
|
!l.startsWith(">") &&
|
|
!l.startsWith("<!--") &&
|
|
!l.startsWith("*") &&
|
|
!/^\|?[\s|:-]+\|?$/.test(l) &&
|
|
!isPlaceholder(l),
|
|
);
|
|
|
|
function main(payload) {
|
|
const root = payload?.workspace_roots?.[0] ?? process.cwd();
|
|
const notes = [];
|
|
|
|
// ---- 1. model routing -------------------------------------------------
|
|
const routing = read(join(root, "docs", "MODEL_ROUTING.md"));
|
|
if (!routing) {
|
|
notes.push("Routing: docs/MODEL_ROUTING.md is missing — this kit is not fully installed.");
|
|
} else if (/\[LEAD\]|\[STRONG\]|\[MID\]|\[FAST\]/.test(routing)) {
|
|
notes.push(
|
|
"Routing: UNBOUND. Every subagent is still `model: inherit`, so each one costs what this session costs. " +
|
|
"Run /model-routing before delegating.",
|
|
);
|
|
} else {
|
|
const row = routing.match(/^\|\s*\*\*lead\*\*\s*\|\s*`?([^`|]+?)`?\s*\|/m);
|
|
// Exact match after normalising case and separators. Substring matching would treat
|
|
// composer-2.5 and composer-2.5-fast — two different lanes in this kit's own table —
|
|
// as the same model and stay silent on a real downgrade.
|
|
const norm = (s) => s.toLowerCase().replace(/[\s_()]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
const recorded = row?.[1]?.trim();
|
|
const actual = (payload?.model_id || payload?.model || "").trim();
|
|
if (recorded && actual && norm(recorded) !== norm(actual)) {
|
|
notes.push(
|
|
`Routing: lead-model drift — docs/MODEL_ROUTING.md records "${recorded}" but this session is running "${actual}". ` +
|
|
"Either switch the picker or re-run /model-routing; do not silently proceed on the wrong lane.",
|
|
);
|
|
} else if (recorded) {
|
|
notes.push(`Routing: bound (lead = ${recorded}).`);
|
|
}
|
|
}
|
|
|
|
// ---- 2. handoff -------------------------------------------------------
|
|
const handoff = read(join(root, "docs", "HANDOFF.md"));
|
|
if (handoff) {
|
|
const next = handoff.match(/Next smallest action:\*{0,2}\s*(.+)/);
|
|
if (next) notes.push(`Handoff next action: ${next[1].trim()}`);
|
|
}
|
|
|
|
// ---- 3. context caps --------------------------------------------------
|
|
const over = [];
|
|
const memory = read(join(root, "docs", "MEMORY.md"));
|
|
if (memory) {
|
|
const n = contentLines(memory).filter((l) => l.startsWith("-") || l.startsWith("|")).length;
|
|
if (n > 60) over.push(`docs/MEMORY.md ${n}/60 entry lines`);
|
|
}
|
|
if (handoff) {
|
|
const n = contentLines(handoff).length;
|
|
if (n > 25) over.push(`docs/HANDOFF.md ${n}/25 lines`);
|
|
}
|
|
const tasks = read(join(root, "docs", "TASKS.md"));
|
|
if (tasks) {
|
|
const active = tasks.split(/^##\s+/m).find((s) => /^Active\b/i.test(s));
|
|
if (active) {
|
|
const n = (active.match(/^###\s+T-/gm) || []).length;
|
|
if (n > 7) over.push(`docs/TASKS.md Active ${n}/7 contracts`);
|
|
}
|
|
}
|
|
const agents = read(join(root, "AGENTS.md"));
|
|
if (agents) {
|
|
const section = agents.split(/^##\s+Lessons\s*$/m)[1];
|
|
if (section) {
|
|
const rules = section.split(/^##\s/m)[0].split("\n").filter((l) => /^\s*-\s+\S/.test(l) && !l.includes("*(Add one-line"));
|
|
if (rules.length > 12) over.push(`AGENTS.md ## Lessons ${rules.length}/12 rules`);
|
|
}
|
|
}
|
|
if (over.length) {
|
|
notes.push(`Context caps EXCEEDED: ${over.join(" · ")}. Run /memory-sync before adding anything.`);
|
|
}
|
|
|
|
return notes.length
|
|
? { additional_context: `Project state (from .cursor/hooks/session-context.mjs):\n- ${notes.join("\n- ")}` }
|
|
: {};
|
|
}
|
|
|
|
const raw = await readAll();
|
|
let out = {};
|
|
try {
|
|
out = main(raw ? JSON.parse(raw) : {});
|
|
} catch {
|
|
out = {};
|
|
}
|
|
process.stdout.write(JSON.stringify(out));
|
|
process.exit(0);
|