CoDEVX / lib /command-parser.ts
Tiger's Macbook Air
Build agentic PM demo app
3f76ff4
import type { CommandMode, ParsedCommand, WorkPackage } from "./work-package-types";
export type CommandParseResult = {
parsed: ParsedCommand;
matchedWorkPackageId?: string;
matchedWorkPackageTitle?: string;
suggestions?: { id: string; title: string; shortName: string }[];
error?: string;
};
const MODES: CommandMode[] = ["ask", "plan", "change", "execute"];
function normalize(s: string): string {
return s.trim().replace(/\s+/g, " ").toLowerCase();
}
function matchWorkPackageByName(
workPackages: WorkPackage[],
name: string,
): { id: string; title: string; shortName: string } | undefined {
const n = normalize(name);
if (!n) return;
return workPackages.find((wp) => {
return normalize(wp.title) === n || normalize(wp.shortName) === n;
});
}
function getSuggestions(workPackages: WorkPackage[], name: string) {
const n = normalize(name);
if (!n) return [];
// Cheap “contains” suggestion without pulling in a fuzzy-match dep.
return workPackages
.map((wp) => ({
id: wp.id,
title: wp.title,
shortName: wp.shortName,
score:
Number(normalize(wp.title).includes(n)) +
Number(normalize(wp.shortName).includes(n)),
}))
.filter((x) => x.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.map(({ id, title, shortName }) => ({ id, title, shortName }));
}
export function parseCommand(
text: string,
workPackages: WorkPackage[],
): CommandParseResult {
const raw = text.trim();
if (!raw.startsWith("@")) {
return { parsed: { instruction: raw } };
}
const afterAt = raw.slice(1).trim();
if (!afterAt) {
return { parsed: { instruction: "" }, error: "Missing work package name." };
}
const tokens = afterAt.split(/\s+/g);
// Find explicit mode token (if present).
let modeIdx = -1;
for (let i = 0; i < tokens.length; i++) {
if (MODES.includes(tokens[i].toLowerCase() as CommandMode)) {
modeIdx = i;
break;
}
}
// If mode is missing, infer the longest package-name prefix that matches.
if (modeIdx === -1) {
let bestMatchLen = 0;
let bestMatch:
| { id: string; title: string; shortName: string; matchedName: string }
| undefined;
for (let len = Math.min(tokens.length, 10); len >= 1; len--) {
const candidate = tokens.slice(0, len).join(" ");
const m = matchWorkPackageByName(workPackages, candidate);
if (m) {
bestMatchLen = len;
bestMatch = { ...m, matchedName: candidate };
break;
}
}
if (!bestMatch) {
const guessName = tokens[0];
return {
parsed: { referencedPackageName: guessName, mode: "ask", instruction: tokens.slice(1).join(" ") },
suggestions: getSuggestions(workPackages, guessName),
error: `Work package not found: "${guessName}".`,
};
}
const instruction = tokens.slice(bestMatchLen).join(" ").trim();
return {
parsed: {
referencedPackageName: bestMatch.matchedName,
mode: "ask",
instruction,
},
matchedWorkPackageId: bestMatch.id,
matchedWorkPackageTitle: bestMatch.title,
};
}
const packageName = tokens.slice(0, modeIdx).join(" ").trim();
const mode = tokens[modeIdx].toLowerCase() as CommandMode;
const instruction = tokens.slice(modeIdx + 1).join(" ").trim();
const m = matchWorkPackageByName(workPackages, packageName);
if (!m) {
return {
parsed: { referencedPackageName: packageName, mode, instruction },
suggestions: getSuggestions(workPackages, packageName),
error: `Work package not found: "${packageName}".`,
};
}
return {
parsed: { referencedPackageName: m.title, mode, instruction },
matchedWorkPackageId: m.id,
matchedWorkPackageTitle: m.title,
};
}