@majeanson/lac 3.0.0 → 3.0.2
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/index.mjs +4210 -13
- package/dist/index.mjs.map +1 -1
- package/dist/mcp.mjs +555 -24
- package/package.json +3 -2
package/dist/mcp.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import process from "node:process";
|
|
3
|
+
import process$1 from "node:process";
|
|
4
4
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
-
import
|
|
7
|
+
import readline from "node:readline";
|
|
8
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
9
|
+
import { execSync } from "node:child_process";
|
|
8
10
|
|
|
9
11
|
//#region ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/core/core.js
|
|
10
12
|
/** A special constant with type `never` */
|
|
@@ -2606,7 +2608,7 @@ function initializeContext(params) {
|
|
|
2606
2608
|
external: params?.external ?? void 0
|
|
2607
2609
|
};
|
|
2608
2610
|
}
|
|
2609
|
-
function process$
|
|
2611
|
+
function process$2(schema, ctx, _params = {
|
|
2610
2612
|
path: [],
|
|
2611
2613
|
schemaPath: []
|
|
2612
2614
|
}) {
|
|
@@ -2643,7 +2645,7 @@ function process$1(schema, ctx, _params = {
|
|
|
2643
2645
|
const parent = schema._zod.parent;
|
|
2644
2646
|
if (parent) {
|
|
2645
2647
|
if (!result.ref) result.ref = parent;
|
|
2646
|
-
process$
|
|
2648
|
+
process$2(parent, ctx, params);
|
|
2647
2649
|
ctx.seen.get(parent).isParent = true;
|
|
2648
2650
|
}
|
|
2649
2651
|
}
|
|
@@ -2855,7 +2857,7 @@ const createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
|
|
|
2855
2857
|
...params,
|
|
2856
2858
|
processors
|
|
2857
2859
|
});
|
|
2858
|
-
process$
|
|
2860
|
+
process$2(schema, ctx);
|
|
2859
2861
|
extractDefs(ctx, schema);
|
|
2860
2862
|
return finalize(ctx, schema);
|
|
2861
2863
|
};
|
|
@@ -2867,7 +2869,7 @@ const createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params)
|
|
|
2867
2869
|
io,
|
|
2868
2870
|
processors
|
|
2869
2871
|
});
|
|
2870
|
-
process$
|
|
2872
|
+
process$2(schema, ctx);
|
|
2871
2873
|
extractDefs(ctx, schema);
|
|
2872
2874
|
return finalize(ctx, schema);
|
|
2873
2875
|
};
|
|
@@ -2951,7 +2953,7 @@ const arrayProcessor = (schema, ctx, _json, params) => {
|
|
|
2951
2953
|
if (typeof minimum === "number") json.minItems = minimum;
|
|
2952
2954
|
if (typeof maximum === "number") json.maxItems = maximum;
|
|
2953
2955
|
json.type = "array";
|
|
2954
|
-
json.items = process$
|
|
2956
|
+
json.items = process$2(def.element, ctx, {
|
|
2955
2957
|
...params,
|
|
2956
2958
|
path: [...params.path, "items"]
|
|
2957
2959
|
});
|
|
@@ -2962,7 +2964,7 @@ const objectProcessor = (schema, ctx, _json, params) => {
|
|
|
2962
2964
|
json.type = "object";
|
|
2963
2965
|
json.properties = {};
|
|
2964
2966
|
const shape = def.shape;
|
|
2965
|
-
for (const key in shape) json.properties[key] = process$
|
|
2967
|
+
for (const key in shape) json.properties[key] = process$2(shape[key], ctx, {
|
|
2966
2968
|
...params,
|
|
2967
2969
|
path: [
|
|
2968
2970
|
...params.path,
|
|
@@ -2980,7 +2982,7 @@ const objectProcessor = (schema, ctx, _json, params) => {
|
|
|
2980
2982
|
if (def.catchall?._zod.def.type === "never") json.additionalProperties = false;
|
|
2981
2983
|
else if (!def.catchall) {
|
|
2982
2984
|
if (ctx.io === "output") json.additionalProperties = false;
|
|
2983
|
-
} else if (def.catchall) json.additionalProperties = process$
|
|
2985
|
+
} else if (def.catchall) json.additionalProperties = process$2(def.catchall, ctx, {
|
|
2984
2986
|
...params,
|
|
2985
2987
|
path: [...params.path, "additionalProperties"]
|
|
2986
2988
|
});
|
|
@@ -2988,7 +2990,7 @@ const objectProcessor = (schema, ctx, _json, params) => {
|
|
|
2988
2990
|
const unionProcessor = (schema, ctx, json, params) => {
|
|
2989
2991
|
const def = schema._zod.def;
|
|
2990
2992
|
const isExclusive = def.inclusive === false;
|
|
2991
|
-
const options = def.options.map((x, i) => process$
|
|
2993
|
+
const options = def.options.map((x, i) => process$2(x, ctx, {
|
|
2992
2994
|
...params,
|
|
2993
2995
|
path: [
|
|
2994
2996
|
...params.path,
|
|
@@ -3001,7 +3003,7 @@ const unionProcessor = (schema, ctx, json, params) => {
|
|
|
3001
3003
|
};
|
|
3002
3004
|
const intersectionProcessor = (schema, ctx, json, params) => {
|
|
3003
3005
|
const def = schema._zod.def;
|
|
3004
|
-
const a = process$
|
|
3006
|
+
const a = process$2(def.left, ctx, {
|
|
3005
3007
|
...params,
|
|
3006
3008
|
path: [
|
|
3007
3009
|
...params.path,
|
|
@@ -3009,7 +3011,7 @@ const intersectionProcessor = (schema, ctx, json, params) => {
|
|
|
3009
3011
|
0
|
|
3010
3012
|
]
|
|
3011
3013
|
});
|
|
3012
|
-
const b = process$
|
|
3014
|
+
const b = process$2(def.right, ctx, {
|
|
3013
3015
|
...params,
|
|
3014
3016
|
path: [
|
|
3015
3017
|
...params.path,
|
|
@@ -3022,7 +3024,7 @@ const intersectionProcessor = (schema, ctx, json, params) => {
|
|
|
3022
3024
|
};
|
|
3023
3025
|
const nullableProcessor = (schema, ctx, json, params) => {
|
|
3024
3026
|
const def = schema._zod.def;
|
|
3025
|
-
const inner = process$
|
|
3027
|
+
const inner = process$2(def.innerType, ctx, params);
|
|
3026
3028
|
const seen = ctx.seen.get(schema);
|
|
3027
3029
|
if (ctx.target === "openapi-3.0") {
|
|
3028
3030
|
seen.ref = def.innerType;
|
|
@@ -3031,27 +3033,27 @@ const nullableProcessor = (schema, ctx, json, params) => {
|
|
|
3031
3033
|
};
|
|
3032
3034
|
const nonoptionalProcessor = (schema, ctx, _json, params) => {
|
|
3033
3035
|
const def = schema._zod.def;
|
|
3034
|
-
process$
|
|
3036
|
+
process$2(def.innerType, ctx, params);
|
|
3035
3037
|
const seen = ctx.seen.get(schema);
|
|
3036
3038
|
seen.ref = def.innerType;
|
|
3037
3039
|
};
|
|
3038
3040
|
const defaultProcessor = (schema, ctx, json, params) => {
|
|
3039
3041
|
const def = schema._zod.def;
|
|
3040
|
-
process$
|
|
3042
|
+
process$2(def.innerType, ctx, params);
|
|
3041
3043
|
const seen = ctx.seen.get(schema);
|
|
3042
3044
|
seen.ref = def.innerType;
|
|
3043
3045
|
json.default = JSON.parse(JSON.stringify(def.defaultValue));
|
|
3044
3046
|
};
|
|
3045
3047
|
const prefaultProcessor = (schema, ctx, json, params) => {
|
|
3046
3048
|
const def = schema._zod.def;
|
|
3047
|
-
process$
|
|
3049
|
+
process$2(def.innerType, ctx, params);
|
|
3048
3050
|
const seen = ctx.seen.get(schema);
|
|
3049
3051
|
seen.ref = def.innerType;
|
|
3050
3052
|
if (ctx.io === "input") json._prefault = JSON.parse(JSON.stringify(def.defaultValue));
|
|
3051
3053
|
};
|
|
3052
3054
|
const catchProcessor = (schema, ctx, json, params) => {
|
|
3053
3055
|
const def = schema._zod.def;
|
|
3054
|
-
process$
|
|
3056
|
+
process$2(def.innerType, ctx, params);
|
|
3055
3057
|
const seen = ctx.seen.get(schema);
|
|
3056
3058
|
seen.ref = def.innerType;
|
|
3057
3059
|
let catchValue;
|
|
@@ -3065,20 +3067,20 @@ const catchProcessor = (schema, ctx, json, params) => {
|
|
|
3065
3067
|
const pipeProcessor = (schema, ctx, _json, params) => {
|
|
3066
3068
|
const def = schema._zod.def;
|
|
3067
3069
|
const innerType = ctx.io === "input" ? def.in._zod.def.type === "transform" ? def.out : def.in : def.out;
|
|
3068
|
-
process$
|
|
3070
|
+
process$2(innerType, ctx, params);
|
|
3069
3071
|
const seen = ctx.seen.get(schema);
|
|
3070
3072
|
seen.ref = innerType;
|
|
3071
3073
|
};
|
|
3072
3074
|
const readonlyProcessor = (schema, ctx, json, params) => {
|
|
3073
3075
|
const def = schema._zod.def;
|
|
3074
|
-
process$
|
|
3076
|
+
process$2(def.innerType, ctx, params);
|
|
3075
3077
|
const seen = ctx.seen.get(schema);
|
|
3076
3078
|
seen.ref = def.innerType;
|
|
3077
3079
|
json.readOnly = true;
|
|
3078
3080
|
};
|
|
3079
3081
|
const optionalProcessor = (schema, ctx, _json, params) => {
|
|
3080
3082
|
const def = schema._zod.def;
|
|
3081
|
-
process$
|
|
3083
|
+
process$2(def.innerType, ctx, params);
|
|
3082
3084
|
const seen = ctx.seen.get(schema);
|
|
3083
3085
|
seen.ref = def.innerType;
|
|
3084
3086
|
};
|
|
@@ -3773,9 +3775,437 @@ function validateFeature(data) {
|
|
|
3773
3775
|
};
|
|
3774
3776
|
}
|
|
3775
3777
|
|
|
3778
|
+
//#endregion
|
|
3779
|
+
//#region ../lac-claude/dist/index.mjs
|
|
3780
|
+
function createClient() {
|
|
3781
|
+
let apiKey = process$1.env.ANTHROPIC_API_KEY;
|
|
3782
|
+
if (!apiKey) {
|
|
3783
|
+
const configPath = findLacConfig();
|
|
3784
|
+
if (configPath) try {
|
|
3785
|
+
apiKey = JSON.parse(fs.readFileSync(configPath, "utf-8"))?.ai?.apiKey;
|
|
3786
|
+
} catch {}
|
|
3787
|
+
}
|
|
3788
|
+
if (!apiKey) throw new Error("ANTHROPIC_API_KEY not set.\nSet it via:\n export ANTHROPIC_API_KEY=sk-ant-...\nOr add it to .lac/config.json:\n { \"ai\": { \"apiKey\": \"sk-ant-...\" } }\nGet a key at https://console.anthropic.com/settings/keys");
|
|
3789
|
+
return new Anthropic({ apiKey });
|
|
3790
|
+
}
|
|
3791
|
+
function findLacConfig() {
|
|
3792
|
+
let current = process$1.cwd();
|
|
3793
|
+
while (true) {
|
|
3794
|
+
const candidate = path.join(current, ".lac", "config.json");
|
|
3795
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
3796
|
+
const parent = path.dirname(current);
|
|
3797
|
+
if (parent === current) return null;
|
|
3798
|
+
current = parent;
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
async function generateText(client, systemPrompt, userMessage, model = "claude-sonnet-4-6") {
|
|
3802
|
+
const content = (await client.messages.create({
|
|
3803
|
+
model,
|
|
3804
|
+
max_tokens: 4096,
|
|
3805
|
+
system: systemPrompt,
|
|
3806
|
+
messages: [{
|
|
3807
|
+
role: "user",
|
|
3808
|
+
content: userMessage
|
|
3809
|
+
}]
|
|
3810
|
+
})).content[0];
|
|
3811
|
+
if (!content || content.type !== "text") throw new Error("Unexpected response type from Claude API");
|
|
3812
|
+
return content.text;
|
|
3813
|
+
}
|
|
3814
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
3815
|
+
".ts",
|
|
3816
|
+
".tsx",
|
|
3817
|
+
".js",
|
|
3818
|
+
".jsx",
|
|
3819
|
+
".py",
|
|
3820
|
+
".go",
|
|
3821
|
+
".rs",
|
|
3822
|
+
".java",
|
|
3823
|
+
".cs",
|
|
3824
|
+
".rb",
|
|
3825
|
+
".php",
|
|
3826
|
+
".vue",
|
|
3827
|
+
".svelte",
|
|
3828
|
+
".sql"
|
|
3829
|
+
]);
|
|
3830
|
+
const MAX_FILE_CHARS = 8e3;
|
|
3831
|
+
const MAX_TOTAL_CHARS = 32e4;
|
|
3832
|
+
function buildContext(featureDir, feature) {
|
|
3833
|
+
return {
|
|
3834
|
+
feature,
|
|
3835
|
+
featurePath: path.join(featureDir, "feature.json"),
|
|
3836
|
+
sourceFiles: gatherSourceFiles(featureDir),
|
|
3837
|
+
gitLog: getGitLog(featureDir)
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3840
|
+
function gatherSourceFiles(dir) {
|
|
3841
|
+
const files = [];
|
|
3842
|
+
let totalChars = 0;
|
|
3843
|
+
const priorityNames = [
|
|
3844
|
+
"package.json",
|
|
3845
|
+
"README.md",
|
|
3846
|
+
"tsconfig.json",
|
|
3847
|
+
".env.example"
|
|
3848
|
+
];
|
|
3849
|
+
for (const name of priorityNames) {
|
|
3850
|
+
const p = path.join(dir, name);
|
|
3851
|
+
if (fs.existsSync(p)) try {
|
|
3852
|
+
const content = truncate(fs.readFileSync(p, "utf-8"), 4e3);
|
|
3853
|
+
files.push({
|
|
3854
|
+
relativePath: name,
|
|
3855
|
+
content
|
|
3856
|
+
});
|
|
3857
|
+
totalChars += content.length;
|
|
3858
|
+
} catch {}
|
|
3859
|
+
}
|
|
3860
|
+
const allSource = walkDir(dir).filter((f) => SOURCE_EXTENSIONS.has(path.extname(f)) && !f.includes("node_modules") && !f.includes(".turbo") && !f.includes("dist/"));
|
|
3861
|
+
allSource.sort((a, b) => {
|
|
3862
|
+
const aTest = /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(a);
|
|
3863
|
+
return aTest === /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(b) ? 0 : aTest ? 1 : -1;
|
|
3864
|
+
});
|
|
3865
|
+
for (const filePath of allSource) {
|
|
3866
|
+
if (totalChars >= MAX_TOTAL_CHARS) break;
|
|
3867
|
+
if (priorityNames.includes(path.basename(filePath))) continue;
|
|
3868
|
+
try {
|
|
3869
|
+
const content = truncate(fs.readFileSync(filePath, "utf-8"), MAX_FILE_CHARS);
|
|
3870
|
+
const relativePath = path.relative(dir, filePath);
|
|
3871
|
+
files.push({
|
|
3872
|
+
relativePath,
|
|
3873
|
+
content
|
|
3874
|
+
});
|
|
3875
|
+
totalChars += content.length;
|
|
3876
|
+
} catch {}
|
|
3877
|
+
}
|
|
3878
|
+
return files;
|
|
3879
|
+
}
|
|
3880
|
+
function walkDir(dir) {
|
|
3881
|
+
const results = [];
|
|
3882
|
+
try {
|
|
3883
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
3884
|
+
for (const entry of entries) {
|
|
3885
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") continue;
|
|
3886
|
+
const full = path.join(dir, entry.name);
|
|
3887
|
+
if (entry.isDirectory()) results.push(...walkDir(full));
|
|
3888
|
+
else results.push(full);
|
|
3889
|
+
}
|
|
3890
|
+
} catch {}
|
|
3891
|
+
return results;
|
|
3892
|
+
}
|
|
3893
|
+
function truncate(content, maxChars) {
|
|
3894
|
+
if (content.length <= maxChars) return content;
|
|
3895
|
+
return content.slice(0, maxChars) + "\n... [truncated]";
|
|
3896
|
+
}
|
|
3897
|
+
function getGitLog(dir) {
|
|
3898
|
+
try {
|
|
3899
|
+
return execSync("git log --oneline --follow -20 -- .", {
|
|
3900
|
+
cwd: dir,
|
|
3901
|
+
encoding: "utf-8",
|
|
3902
|
+
stdio: [
|
|
3903
|
+
"pipe",
|
|
3904
|
+
"pipe",
|
|
3905
|
+
"pipe"
|
|
3906
|
+
]
|
|
3907
|
+
}).trim();
|
|
3908
|
+
} catch {
|
|
3909
|
+
return "";
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
function contextToString(ctx) {
|
|
3913
|
+
const parts = [];
|
|
3914
|
+
parts.push("=== feature.json ===");
|
|
3915
|
+
parts.push(JSON.stringify(ctx.feature, null, 2));
|
|
3916
|
+
if (ctx.gitLog) {
|
|
3917
|
+
parts.push("\n=== git log (last 20 commits) ===");
|
|
3918
|
+
parts.push(ctx.gitLog);
|
|
3919
|
+
}
|
|
3920
|
+
for (const file of ctx.sourceFiles) {
|
|
3921
|
+
parts.push(`\n=== ${file.relativePath} ===`);
|
|
3922
|
+
parts.push(file.content);
|
|
3923
|
+
}
|
|
3924
|
+
return parts.join("\n");
|
|
3925
|
+
}
|
|
3926
|
+
const RESET = "\x1B[0m";
|
|
3927
|
+
const BOLD = "\x1B[1m";
|
|
3928
|
+
const GREEN = "\x1B[32m";
|
|
3929
|
+
const CYAN = "\x1B[36m";
|
|
3930
|
+
const DIM = "\x1B[2m";
|
|
3931
|
+
function formatValue(value) {
|
|
3932
|
+
if (typeof value === "string") return value.length > 300 ? value.slice(0, 300) + "…" : value;
|
|
3933
|
+
return JSON.stringify(value, null, 2);
|
|
3934
|
+
}
|
|
3935
|
+
function printDiff(diffs) {
|
|
3936
|
+
const separator = "━".repeat(52);
|
|
3937
|
+
for (const diff of diffs) {
|
|
3938
|
+
const label = diff.wasEmpty ? "empty → generated" : "updated";
|
|
3939
|
+
process.stdout.write(`\n${BOLD}${CYAN}${separator}${RESET}\n`);
|
|
3940
|
+
process.stdout.write(`${BOLD} ${diff.field}${RESET} ${DIM}(${label})${RESET}\n`);
|
|
3941
|
+
process.stdout.write(`${CYAN}${separator}${RESET}\n`);
|
|
3942
|
+
const lines = formatValue(diff.proposed).split("\n");
|
|
3943
|
+
for (const line of lines) process.stdout.write(`${GREEN} ${line}${RESET}\n`);
|
|
3944
|
+
}
|
|
3945
|
+
process.stdout.write("\n");
|
|
3946
|
+
}
|
|
3947
|
+
const FILL_PROMPTS = {
|
|
3948
|
+
analysis: {
|
|
3949
|
+
system: `You are a software engineering analyst. Given a feature.json and the feature's source code, write a clear analysis section. Cover: what the code does architecturally, key patterns used, and why they were likely chosen. Be specific — name actual functions, modules, and techniques visible in the code. Write in first-person technical prose, 150-300 words. Return only the analysis text, no JSON wrapper, no markdown heading.`,
|
|
3950
|
+
userSuffix: "Write the analysis field for this feature."
|
|
3951
|
+
},
|
|
3952
|
+
decisions: {
|
|
3953
|
+
system: `You are a software engineering analyst. Given a feature.json and source code, extract 2-4 key technical decisions evident from the code. For each: what was decided (concrete), why (rationale from code evidence), what alternatives were likely considered.
|
|
3954
|
+
|
|
3955
|
+
Return ONLY a valid JSON array — no other text, no markdown fences:
|
|
3956
|
+
[
|
|
3957
|
+
{
|
|
3958
|
+
"decision": "string",
|
|
3959
|
+
"rationale": "string",
|
|
3960
|
+
"alternativesConsidered": ["string"],
|
|
3961
|
+
"date": null
|
|
3962
|
+
}
|
|
3963
|
+
]`,
|
|
3964
|
+
userSuffix: "Extract the key technical decisions from this feature."
|
|
3965
|
+
},
|
|
3966
|
+
implementation: {
|
|
3967
|
+
system: `You are a software engineering analyst. Given a feature.json and source code, write concise implementation notes. Cover: the main components and their roles, how data flows through the feature, and any non-obvious patterns or constraints. 100-200 words. Return only the text, no JSON wrapper, no heading.`,
|
|
3968
|
+
userSuffix: "Write the implementation field for this feature."
|
|
3969
|
+
},
|
|
3970
|
+
knownLimitations: {
|
|
3971
|
+
system: `You are a software engineering analyst. Identify 2-4 known limitations, trade-offs, or tech-debt items visible in this code. Look for TODOs, FIXMEs, missing error handling, overly complex patterns, or performance gaps.
|
|
3972
|
+
|
|
3973
|
+
Return ONLY a valid JSON array of strings — no other text:
|
|
3974
|
+
["limitation 1", "limitation 2"]`,
|
|
3975
|
+
userSuffix: "List the known limitations visible in this feature."
|
|
3976
|
+
},
|
|
3977
|
+
tags: {
|
|
3978
|
+
system: `You are a software engineering analyst. Generate 3-6 tags from the domain language in this code. Lowercase, single words or hyphenated. Reflect the actual domain, not generic terms like "code" or "feature".
|
|
3979
|
+
|
|
3980
|
+
Return ONLY a valid JSON array of strings — no other text:
|
|
3981
|
+
["tag1", "tag2", "tag3"]`,
|
|
3982
|
+
userSuffix: "Generate tags for this feature."
|
|
3983
|
+
},
|
|
3984
|
+
annotations: {
|
|
3985
|
+
system: `You are a software engineering analyst. Identify 1-3 significant annotations worth capturing — warnings, lessons, tech debt, or breaking-change risks visible in the code.
|
|
3986
|
+
|
|
3987
|
+
Return ONLY a valid JSON array — no other text:
|
|
3988
|
+
[
|
|
3989
|
+
{
|
|
3990
|
+
"id": "auto-1",
|
|
3991
|
+
"author": "lac fill",
|
|
3992
|
+
"date": "${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}",
|
|
3993
|
+
"type": "tech-debt",
|
|
3994
|
+
"body": "string"
|
|
3995
|
+
}
|
|
3996
|
+
]`,
|
|
3997
|
+
userSuffix: "Generate annotations for this feature."
|
|
3998
|
+
},
|
|
3999
|
+
successCriteria: {
|
|
4000
|
+
system: `You are a software engineering analyst. Write a plain-language success criteria statement for this feature — "how do we know it's done and working?" Be specific and testable. 1-3 sentences. Return only the text, no JSON wrapper, no heading.`,
|
|
4001
|
+
userSuffix: "Write the success criteria for this feature."
|
|
4002
|
+
},
|
|
4003
|
+
domain: {
|
|
4004
|
+
system: `You are a software engineering analyst. Identify the primary technical domain for this feature from its code and problem statement. Return a single lowercase word or short hyphenated phrase (e.g. "auth", "payments", "notifications", "data-pipeline"). Return only the domain value — nothing else.`,
|
|
4005
|
+
userSuffix: "Identify the domain for this feature."
|
|
4006
|
+
}
|
|
4007
|
+
};
|
|
4008
|
+
const JSON_FIELDS = new Set([
|
|
4009
|
+
"decisions",
|
|
4010
|
+
"knownLimitations",
|
|
4011
|
+
"tags",
|
|
4012
|
+
"annotations"
|
|
4013
|
+
]);
|
|
4014
|
+
const ALL_FILLABLE_FIELDS = [
|
|
4015
|
+
"analysis",
|
|
4016
|
+
"decisions",
|
|
4017
|
+
"implementation",
|
|
4018
|
+
"knownLimitations",
|
|
4019
|
+
"tags",
|
|
4020
|
+
"successCriteria",
|
|
4021
|
+
"domain"
|
|
4022
|
+
];
|
|
4023
|
+
function getMissingFields(feature) {
|
|
4024
|
+
return ALL_FILLABLE_FIELDS.filter((field) => {
|
|
4025
|
+
const val = feature[field];
|
|
4026
|
+
if (val === void 0 || val === null) return true;
|
|
4027
|
+
if (typeof val === "string") return val.trim().length === 0;
|
|
4028
|
+
if (Array.isArray(val)) return val.length === 0;
|
|
4029
|
+
return false;
|
|
4030
|
+
});
|
|
4031
|
+
}
|
|
4032
|
+
const GEN_PROMPTS = {
|
|
4033
|
+
component: {
|
|
4034
|
+
system: `You are an expert React/TypeScript developer. You will be given a feature.json describing a feature. Generate a production-quality React component implementing the core UI for this feature. Include TypeScript types, sensible props, and clear comments. Make it maintainable — any developer unfamiliar with this feature should understand it. Return only the component code, no explanation.`,
|
|
4035
|
+
userSuffix: "Generate a React TypeScript component for this feature."
|
|
4036
|
+
},
|
|
4037
|
+
test: {
|
|
4038
|
+
system: `You are an expert software testing engineer. You will be given a feature.json. Generate a comprehensive test suite using Vitest. Use the successCriteria to derive happy-path tests and the knownLimitations to derive edge-case tests. Return only the test code, no explanation.`,
|
|
4039
|
+
userSuffix: "Generate a Vitest test suite for this feature."
|
|
4040
|
+
},
|
|
4041
|
+
migration: {
|
|
4042
|
+
system: `You are an expert database engineer. You will be given a feature.json. Generate a database migration scaffold for the data model this feature implies. Use SQL with clear comments. Include both up (CREATE) and down (DROP) sections. Return only the SQL, no explanation.`,
|
|
4043
|
+
userSuffix: "Generate a database migration for this feature."
|
|
4044
|
+
},
|
|
4045
|
+
docs: {
|
|
4046
|
+
system: `You are a technical writer. You will be given a feature.json. Generate user-facing documentation for this feature. Write it clearly enough that any end user can understand it (not developer-focused). Cover: what it does, how to use it, and known limitations. Use Markdown. Return only the documentation, no explanation.`,
|
|
4047
|
+
userSuffix: "Generate user-facing documentation for this feature."
|
|
4048
|
+
}
|
|
4049
|
+
};
|
|
4050
|
+
async function fillFeature(options) {
|
|
4051
|
+
const { featureDir, dryRun = false, skipConfirm = false, model = "claude-sonnet-4-6" } = options;
|
|
4052
|
+
const featurePath = path.join(featureDir, "feature.json");
|
|
4053
|
+
let raw;
|
|
4054
|
+
try {
|
|
4055
|
+
raw = fs.readFileSync(featurePath, "utf-8");
|
|
4056
|
+
} catch {
|
|
4057
|
+
throw new Error(`No feature.json found at "${featurePath}"`);
|
|
4058
|
+
}
|
|
4059
|
+
let parsed;
|
|
4060
|
+
try {
|
|
4061
|
+
parsed = JSON.parse(raw);
|
|
4062
|
+
} catch {
|
|
4063
|
+
throw new Error(`Invalid JSON in "${featurePath}"`);
|
|
4064
|
+
}
|
|
4065
|
+
const result = validateFeature(parsed);
|
|
4066
|
+
if (!result.success) throw new Error(`Invalid feature.json: ${result.errors.join(", ")}`);
|
|
4067
|
+
const feature = result.data;
|
|
4068
|
+
const client = createClient();
|
|
4069
|
+
const fieldsToFill = options.fields ? options.fields : getMissingFields(feature);
|
|
4070
|
+
if (fieldsToFill.length === 0) {
|
|
4071
|
+
process$1.stdout.write(` All fields already filled for ${feature.featureKey}.\n`);
|
|
4072
|
+
return {
|
|
4073
|
+
applied: false,
|
|
4074
|
+
fields: [],
|
|
4075
|
+
patch: {}
|
|
4076
|
+
};
|
|
4077
|
+
}
|
|
4078
|
+
process$1.stdout.write(`\nAnalyzing ${feature.featureKey} (${feature.title})...\n`);
|
|
4079
|
+
const ctx = buildContext(featureDir, feature);
|
|
4080
|
+
const contextStr = contextToString(ctx);
|
|
4081
|
+
process$1.stdout.write(`Reading ${ctx.sourceFiles.length} source file(s)...\n`);
|
|
4082
|
+
process$1.stdout.write(`Generating with ${model}...\n`);
|
|
4083
|
+
const patch = {};
|
|
4084
|
+
const diffs = [];
|
|
4085
|
+
for (const field of fieldsToFill) {
|
|
4086
|
+
const prompt = FILL_PROMPTS[field];
|
|
4087
|
+
if (!prompt) continue;
|
|
4088
|
+
process$1.stdout.write(` → ${field}...`);
|
|
4089
|
+
try {
|
|
4090
|
+
const rawValue = await generateText(client, prompt.system, `${contextStr}\n\n${prompt.userSuffix}`, model);
|
|
4091
|
+
let value = rawValue.trim();
|
|
4092
|
+
if (JSON_FIELDS.has(field)) try {
|
|
4093
|
+
const jsonStr = rawValue.match(/```(?:json)?\s*([\s\S]*?)```/)?.[1] ?? rawValue;
|
|
4094
|
+
value = JSON.parse(jsonStr.trim());
|
|
4095
|
+
} catch {
|
|
4096
|
+
process$1.stderr.write(`\n Warning: could not parse JSON for "${field}", storing as string\n`);
|
|
4097
|
+
}
|
|
4098
|
+
patch[field] = value;
|
|
4099
|
+
const existing = feature[field];
|
|
4100
|
+
const wasEmpty = existing === void 0 || existing === null || typeof existing === "string" && existing.trim().length === 0 || Array.isArray(existing) && existing.length === 0;
|
|
4101
|
+
diffs.push({
|
|
4102
|
+
field,
|
|
4103
|
+
wasEmpty,
|
|
4104
|
+
proposed: value
|
|
4105
|
+
});
|
|
4106
|
+
process$1.stdout.write(" done\n");
|
|
4107
|
+
} catch (err) {
|
|
4108
|
+
process$1.stdout.write(" failed\n");
|
|
4109
|
+
process$1.stderr.write(` Error generating "${field}": ${err instanceof Error ? err.message : String(err)}\n`);
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
if (diffs.length === 0) return {
|
|
4113
|
+
applied: false,
|
|
4114
|
+
fields: [],
|
|
4115
|
+
patch: {}
|
|
4116
|
+
};
|
|
4117
|
+
printDiff(diffs);
|
|
4118
|
+
if (dryRun) {
|
|
4119
|
+
process$1.stdout.write(" [dry-run] No changes written.\n\n");
|
|
4120
|
+
return {
|
|
4121
|
+
applied: false,
|
|
4122
|
+
fields: Object.keys(patch),
|
|
4123
|
+
patch
|
|
4124
|
+
};
|
|
4125
|
+
}
|
|
4126
|
+
if (!skipConfirm) {
|
|
4127
|
+
const answer = await askUser("Apply? [Y]es / [n]o / [f]ield-by-field: ");
|
|
4128
|
+
if (answer.toLowerCase() === "n") {
|
|
4129
|
+
process$1.stdout.write(" Cancelled.\n");
|
|
4130
|
+
return {
|
|
4131
|
+
applied: false,
|
|
4132
|
+
fields: Object.keys(patch),
|
|
4133
|
+
patch
|
|
4134
|
+
};
|
|
4135
|
+
}
|
|
4136
|
+
if (answer.toLowerCase() === "f") {
|
|
4137
|
+
const approved = {};
|
|
4138
|
+
for (const [field, value] of Object.entries(patch)) if ((await askUser(` Apply "${field}"? [Y/n]: `)).toLowerCase() !== "n") approved[field] = value;
|
|
4139
|
+
for (const key of Object.keys(patch)) if (!(key in approved)) delete patch[key];
|
|
4140
|
+
Object.assign(patch, approved);
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
const updated = {
|
|
4144
|
+
...parsed,
|
|
4145
|
+
...patch
|
|
4146
|
+
};
|
|
4147
|
+
fs.writeFileSync(featurePath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
|
|
4148
|
+
const count = Object.keys(patch).length;
|
|
4149
|
+
process$1.stdout.write(`\n ✓ Updated ${feature.featureKey} — ${count} field${count === 1 ? "" : "s"} written.\n\n`);
|
|
4150
|
+
return {
|
|
4151
|
+
applied: true,
|
|
4152
|
+
fields: Object.keys(patch),
|
|
4153
|
+
patch
|
|
4154
|
+
};
|
|
4155
|
+
}
|
|
4156
|
+
async function genFromFeature(options) {
|
|
4157
|
+
const { featureDir, type, dryRun = false, model = "claude-sonnet-4-6" } = options;
|
|
4158
|
+
const featurePath = path.join(featureDir, "feature.json");
|
|
4159
|
+
let raw;
|
|
4160
|
+
try {
|
|
4161
|
+
raw = fs.readFileSync(featurePath, "utf-8");
|
|
4162
|
+
} catch {
|
|
4163
|
+
throw new Error(`No feature.json found at "${featurePath}"`);
|
|
4164
|
+
}
|
|
4165
|
+
const result = validateFeature(JSON.parse(raw));
|
|
4166
|
+
if (!result.success) throw new Error(`Invalid feature.json: ${result.errors.join(", ")}`);
|
|
4167
|
+
const feature = result.data;
|
|
4168
|
+
const promptConfig = GEN_PROMPTS[type];
|
|
4169
|
+
if (!promptConfig) throw new Error(`Unknown generation type: "${type}". Available: component, test, migration, docs`);
|
|
4170
|
+
const client = createClient();
|
|
4171
|
+
process$1.stdout.write(`\nGenerating ${type} for ${feature.featureKey} (${feature.title})...\n`);
|
|
4172
|
+
process$1.stdout.write(`Model: ${model}\n\n`);
|
|
4173
|
+
const contextStr = contextToString(buildContext(featureDir, feature));
|
|
4174
|
+
const generated = await generateText(client, promptConfig.system, `${contextStr}\n\n${promptConfig.userSuffix}`, model);
|
|
4175
|
+
if (dryRun) {
|
|
4176
|
+
process$1.stdout.write(generated);
|
|
4177
|
+
process$1.stdout.write("\n\n [dry-run] No file written.\n");
|
|
4178
|
+
return generated;
|
|
4179
|
+
}
|
|
4180
|
+
const outFile = options.outFile ?? path.join(featureDir, `${feature.featureKey}${typeToExt(type)}`);
|
|
4181
|
+
fs.writeFileSync(outFile, generated, "utf-8");
|
|
4182
|
+
process$1.stdout.write(` ✓ Written to ${outFile}\n\n`);
|
|
4183
|
+
return generated;
|
|
4184
|
+
}
|
|
4185
|
+
function typeToExt(type) {
|
|
4186
|
+
return {
|
|
4187
|
+
component: ".tsx",
|
|
4188
|
+
test: ".test.ts",
|
|
4189
|
+
migration: ".sql",
|
|
4190
|
+
docs: ".md"
|
|
4191
|
+
}[type] ?? ".txt";
|
|
4192
|
+
}
|
|
4193
|
+
function askUser(question) {
|
|
4194
|
+
return new Promise((resolve) => {
|
|
4195
|
+
const rl = readline.createInterface({
|
|
4196
|
+
input: process$1.stdin,
|
|
4197
|
+
output: process$1.stdout
|
|
4198
|
+
});
|
|
4199
|
+
rl.question(question, (answer) => {
|
|
4200
|
+
rl.close();
|
|
4201
|
+
resolve(answer.trim() || "y");
|
|
4202
|
+
});
|
|
4203
|
+
});
|
|
4204
|
+
}
|
|
4205
|
+
|
|
3776
4206
|
//#endregion
|
|
3777
4207
|
//#region ../lac-mcp/src/index.ts
|
|
3778
|
-
const workspaceRoot = process.argv[2] ?? process.env.LAC_WORKSPACE ?? process.cwd();
|
|
4208
|
+
const workspaceRoot = process$1.argv[2] ?? process$1.env.LAC_WORKSPACE ?? process$1.cwd();
|
|
3779
4209
|
const server = new Server({
|
|
3780
4210
|
name: "lac",
|
|
3781
4211
|
version: "1.0.0"
|
|
@@ -3939,6 +4369,36 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [
|
|
|
3939
4369
|
description: "Directory to scan (default: workspace root)"
|
|
3940
4370
|
} }
|
|
3941
4371
|
}
|
|
4372
|
+
},
|
|
4373
|
+
{
|
|
4374
|
+
name: "read_feature_context",
|
|
4375
|
+
description: "Read a feature.json and all surrounding source files. Returns the full context needed to fill missing fields — use this when the user asks you to fill or analyse a feature WITHOUT calling an external AI API (you ARE the AI). After reading, generate the missing fields yourself and call write_feature_fields to save.",
|
|
4376
|
+
inputSchema: {
|
|
4377
|
+
type: "object",
|
|
4378
|
+
properties: { path: {
|
|
4379
|
+
type: "string",
|
|
4380
|
+
description: "Absolute or relative path to the feature folder (contains feature.json)"
|
|
4381
|
+
} },
|
|
4382
|
+
required: ["path"]
|
|
4383
|
+
}
|
|
4384
|
+
},
|
|
4385
|
+
{
|
|
4386
|
+
name: "write_feature_fields",
|
|
4387
|
+
description: "Patch a feature.json with new field values. Use this after read_feature_context — write the fields you generated back to disk.",
|
|
4388
|
+
inputSchema: {
|
|
4389
|
+
type: "object",
|
|
4390
|
+
properties: {
|
|
4391
|
+
path: {
|
|
4392
|
+
type: "string",
|
|
4393
|
+
description: "Absolute or relative path to the feature folder (contains feature.json)"
|
|
4394
|
+
},
|
|
4395
|
+
fields: {
|
|
4396
|
+
type: "object",
|
|
4397
|
+
description: "Key-value pairs to merge into feature.json. Values may be strings, arrays, or objects depending on the field."
|
|
4398
|
+
}
|
|
4399
|
+
},
|
|
4400
|
+
required: ["path", "fields"]
|
|
4401
|
+
}
|
|
3942
4402
|
}
|
|
3943
4403
|
] }));
|
|
3944
4404
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -4061,6 +4521,77 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4061
4521
|
text: `${passes.length} passed, ${failures.length} failed — ${results.length} features checked\n\n${lines.join("\n")}`
|
|
4062
4522
|
}] };
|
|
4063
4523
|
}
|
|
4524
|
+
case "read_feature_context": {
|
|
4525
|
+
const featureDir = resolvePath(String(a.path));
|
|
4526
|
+
const featurePath = path.join(featureDir, "feature.json");
|
|
4527
|
+
let raw;
|
|
4528
|
+
try {
|
|
4529
|
+
raw = fs.readFileSync(featurePath, "utf-8");
|
|
4530
|
+
} catch {
|
|
4531
|
+
return {
|
|
4532
|
+
content: [{
|
|
4533
|
+
type: "text",
|
|
4534
|
+
text: `No feature.json found at "${featurePath}"`
|
|
4535
|
+
}],
|
|
4536
|
+
isError: true
|
|
4537
|
+
};
|
|
4538
|
+
}
|
|
4539
|
+
const result = validateFeature(JSON.parse(raw));
|
|
4540
|
+
if (!result.success) return {
|
|
4541
|
+
content: [{
|
|
4542
|
+
type: "text",
|
|
4543
|
+
text: `Invalid feature.json: ${result.errors.join(", ")}`
|
|
4544
|
+
}],
|
|
4545
|
+
isError: true
|
|
4546
|
+
};
|
|
4547
|
+
const feature = result.data;
|
|
4548
|
+
const contextStr = contextToString(buildContext(featureDir, feature));
|
|
4549
|
+
const missingFields = getMissingFields(feature);
|
|
4550
|
+
const fieldInstructions = missingFields.map((field) => {
|
|
4551
|
+
const prompt = FILL_PROMPTS[field];
|
|
4552
|
+
const isJson = JSON_FIELDS.has(field);
|
|
4553
|
+
return `### ${field}\n${prompt.system}\n${prompt.userSuffix}\n${isJson ? "(Return valid JSON for this field)" : "(Return plain text for this field)"}`;
|
|
4554
|
+
}).join("\n\n");
|
|
4555
|
+
return { content: [{
|
|
4556
|
+
type: "text",
|
|
4557
|
+
text: `${missingFields.length === 0 ? "All fillable fields are already populated. No generation needed." : `## Missing fields to fill (${missingFields.join(", ")})\n\nFor each field below, generate the value described, then call write_feature_fields with all generated values.\n\n${fieldInstructions}`}\n\n## Context\n\n${contextStr}`
|
|
4558
|
+
}] };
|
|
4559
|
+
}
|
|
4560
|
+
case "write_feature_fields": {
|
|
4561
|
+
const featureDir = resolvePath(String(a.path));
|
|
4562
|
+
const featurePath = path.join(featureDir, "feature.json");
|
|
4563
|
+
let raw;
|
|
4564
|
+
try {
|
|
4565
|
+
raw = fs.readFileSync(featurePath, "utf-8");
|
|
4566
|
+
} catch {
|
|
4567
|
+
return {
|
|
4568
|
+
content: [{
|
|
4569
|
+
type: "text",
|
|
4570
|
+
text: `No feature.json found at "${featurePath}"`
|
|
4571
|
+
}],
|
|
4572
|
+
isError: true
|
|
4573
|
+
};
|
|
4574
|
+
}
|
|
4575
|
+
const existing = JSON.parse(raw);
|
|
4576
|
+
const fields = a.fields;
|
|
4577
|
+
if (!fields || typeof fields !== "object" || Array.isArray(fields)) return {
|
|
4578
|
+
content: [{
|
|
4579
|
+
type: "text",
|
|
4580
|
+
text: "fields must be a JSON object"
|
|
4581
|
+
}],
|
|
4582
|
+
isError: true
|
|
4583
|
+
};
|
|
4584
|
+
const updated = {
|
|
4585
|
+
...existing,
|
|
4586
|
+
...fields
|
|
4587
|
+
};
|
|
4588
|
+
fs.writeFileSync(featurePath, JSON.stringify(updated, null, 2) + "\n", "utf-8");
|
|
4589
|
+
const writtenKeys = Object.keys(fields);
|
|
4590
|
+
return { content: [{
|
|
4591
|
+
type: "text",
|
|
4592
|
+
text: `✓ Wrote ${writtenKeys.length} field(s) to ${featurePath}: ${writtenKeys.join(", ")}`
|
|
4593
|
+
}] };
|
|
4594
|
+
}
|
|
4064
4595
|
default: return { content: [{
|
|
4065
4596
|
type: "text",
|
|
4066
4597
|
text: `Unknown tool: ${name}`
|
|
@@ -4146,11 +4677,11 @@ function statusIcon(status) {
|
|
|
4146
4677
|
async function main() {
|
|
4147
4678
|
const transport = new StdioServerTransport();
|
|
4148
4679
|
await server.connect(transport);
|
|
4149
|
-
process.stderr.write(`lac MCP server running (workspace: ${workspaceRoot})\n`);
|
|
4680
|
+
process$1.stderr.write(`lac MCP server running (workspace: ${workspaceRoot})\n`);
|
|
4150
4681
|
}
|
|
4151
4682
|
main().catch((err) => {
|
|
4152
|
-
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
4153
|
-
process.exit(1);
|
|
4683
|
+
process$1.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
4684
|
+
process$1.exit(1);
|
|
4154
4685
|
});
|
|
4155
4686
|
|
|
4156
4687
|
//#endregion
|