preflight-dev 3.1.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/LICENSE +21 -0
- package/README.md +172 -0
- package/bin/cli.js +11 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.js +154 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +122 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config.d.ts +34 -0
- package/dist/lib/config.js +118 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/embeddings.d.ts +11 -0
- package/dist/lib/embeddings.js +88 -0
- package/dist/lib/embeddings.js.map +1 -0
- package/dist/lib/files.d.ts +15 -0
- package/dist/lib/files.js +60 -0
- package/dist/lib/files.js.map +1 -0
- package/dist/lib/git-extractor.d.ts +9 -0
- package/dist/lib/git-extractor.js +116 -0
- package/dist/lib/git-extractor.js.map +1 -0
- package/dist/lib/git.d.ts +29 -0
- package/dist/lib/git.js +86 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/session-parser.d.ts +45 -0
- package/dist/lib/session-parser.js +267 -0
- package/dist/lib/session-parser.js.map +1 -0
- package/dist/lib/state.d.ts +21 -0
- package/dist/lib/state.js +86 -0
- package/dist/lib/state.js.map +1 -0
- package/dist/lib/timeline-db.d.ts +67 -0
- package/dist/lib/timeline-db.js +380 -0
- package/dist/lib/timeline-db.js.map +1 -0
- package/dist/lib/triage.d.ts +29 -0
- package/dist/lib/triage.js +193 -0
- package/dist/lib/triage.js.map +1 -0
- package/dist/profiles.d.ts +3 -0
- package/dist/profiles.js +65 -0
- package/dist/profiles.js.map +1 -0
- package/dist/tools/audit-workspace.d.ts +2 -0
- package/dist/tools/audit-workspace.js +86 -0
- package/dist/tools/audit-workspace.js.map +1 -0
- package/dist/tools/checkpoint.d.ts +2 -0
- package/dist/tools/checkpoint.js +108 -0
- package/dist/tools/checkpoint.js.map +1 -0
- package/dist/tools/clarify-intent.d.ts +2 -0
- package/dist/tools/clarify-intent.js +180 -0
- package/dist/tools/clarify-intent.js.map +1 -0
- package/dist/tools/enrich-agent-task.d.ts +2 -0
- package/dist/tools/enrich-agent-task.js +97 -0
- package/dist/tools/enrich-agent-task.js.map +1 -0
- package/dist/tools/generate-scorecard.d.ts +2 -0
- package/dist/tools/generate-scorecard.js +617 -0
- package/dist/tools/generate-scorecard.js.map +1 -0
- package/dist/tools/log-correction.d.ts +2 -0
- package/dist/tools/log-correction.js +76 -0
- package/dist/tools/log-correction.js.map +1 -0
- package/dist/tools/onboard-project.d.ts +2 -0
- package/dist/tools/onboard-project.js +179 -0
- package/dist/tools/onboard-project.js.map +1 -0
- package/dist/tools/preflight-check.d.ts +2 -0
- package/dist/tools/preflight-check.js +229 -0
- package/dist/tools/preflight-check.js.map +1 -0
- package/dist/tools/prompt-score.d.ts +2 -0
- package/dist/tools/prompt-score.js +132 -0
- package/dist/tools/prompt-score.js.map +1 -0
- package/dist/tools/scan-sessions.d.ts +2 -0
- package/dist/tools/scan-sessions.js +182 -0
- package/dist/tools/scan-sessions.js.map +1 -0
- package/dist/tools/scope-work.d.ts +2 -0
- package/dist/tools/scope-work.js +214 -0
- package/dist/tools/scope-work.js.map +1 -0
- package/dist/tools/search-history.d.ts +2 -0
- package/dist/tools/search-history.js +130 -0
- package/dist/tools/search-history.js.map +1 -0
- package/dist/tools/sequence-tasks.d.ts +2 -0
- package/dist/tools/sequence-tasks.js +165 -0
- package/dist/tools/sequence-tasks.js.map +1 -0
- package/dist/tools/session-handoff.d.ts +2 -0
- package/dist/tools/session-handoff.js +113 -0
- package/dist/tools/session-handoff.js.map +1 -0
- package/dist/tools/session-health.d.ts +2 -0
- package/dist/tools/session-health.js +111 -0
- package/dist/tools/session-health.js.map +1 -0
- package/dist/tools/session-stats.d.ts +2 -0
- package/dist/tools/session-stats.js +112 -0
- package/dist/tools/session-stats.js.map +1 -0
- package/dist/tools/sharpen-followup.d.ts +2 -0
- package/dist/tools/sharpen-followup.js +192 -0
- package/dist/tools/sharpen-followup.js.map +1 -0
- package/dist/tools/timeline-view.d.ts +2 -0
- package/dist/tools/timeline-view.js +165 -0
- package/dist/tools/timeline-view.js.map +1 -0
- package/dist/tools/token-audit.d.ts +2 -0
- package/dist/tools/token-audit.js +227 -0
- package/dist/tools/token-audit.js.map +1 -0
- package/dist/tools/verify-completion.d.ts +2 -0
- package/dist/tools/verify-completion.js +154 -0
- package/dist/tools/verify-completion.js.map +1 -0
- package/dist/tools/what-changed.d.ts +2 -0
- package/dist/tools/what-changed.js +40 -0
- package/dist/tools/what-changed.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
- package/src/cli/init.ts +133 -0
- package/src/index.ts +135 -0
- package/src/lib/config.ts +157 -0
- package/src/lib/embeddings.ts +118 -0
- package/src/lib/files.ts +59 -0
- package/src/lib/git-extractor.ts +137 -0
- package/src/lib/git.ts +89 -0
- package/src/lib/session-parser.ts +325 -0
- package/src/lib/state.ts +86 -0
- package/src/lib/timeline-db.ts +490 -0
- package/src/lib/triage.ts +255 -0
- package/src/profiles.ts +70 -0
- package/src/templates/config.yml +23 -0
- package/src/templates/triage.yml +27 -0
- package/src/tools/audit-workspace.ts +97 -0
- package/src/tools/checkpoint.ts +119 -0
- package/src/tools/clarify-intent.ts +191 -0
- package/src/tools/enrich-agent-task.ts +108 -0
- package/src/tools/generate-scorecard.ts +673 -0
- package/src/tools/log-correction.ts +89 -0
- package/src/tools/onboard-project.ts +214 -0
- package/src/tools/preflight-check.ts +263 -0
- package/src/tools/prompt-score.ts +150 -0
- package/src/tools/scan-sessions.ts +209 -0
- package/src/tools/scope-work.ts +238 -0
- package/src/tools/search-history.ts +145 -0
- package/src/tools/sequence-tasks.ts +182 -0
- package/src/tools/session-handoff.ts +125 -0
- package/src/tools/session-health.ts +107 -0
- package/src/tools/session-stats.ts +134 -0
- package/src/tools/sharpen-followup.ts +200 -0
- package/src/tools/timeline-view.ts +181 -0
- package/src/tools/token-audit.ts +259 -0
- package/src/tools/verify-completion.ts +159 -0
- package/src/tools/what-changed.ts +48 -0
- package/src/types.ts +87 -0
package/src/cli/init.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// preflight init — Zero-config MCP server setup for Claude Code
|
|
4
|
+
// =============================================================================
|
|
5
|
+
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
import { readFile, writeFile, mkdir, copyFile } from "node:fs/promises";
|
|
8
|
+
import { join, dirname } from "node:path";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
|
|
14
|
+
function ask(question: string): Promise<string> {
|
|
15
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Create .preflight/ directory with template files */
|
|
19
|
+
async function createPreflightConfig(): Promise<void> {
|
|
20
|
+
const preflightDir = join(process.cwd(), ".preflight");
|
|
21
|
+
|
|
22
|
+
if (existsSync(preflightDir)) {
|
|
23
|
+
console.log("⚠️ .preflight/ directory already exists, skipping...\n");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await mkdir(preflightDir, { recursive: true });
|
|
29
|
+
|
|
30
|
+
// Get the current module's directory to find templates
|
|
31
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
32
|
+
const srcDir = dirname(dirname(currentFile)); // Go up from cli/ to src/
|
|
33
|
+
const templatesDir = join(srcDir, "templates");
|
|
34
|
+
|
|
35
|
+
// Copy template files
|
|
36
|
+
await copyFile(join(templatesDir, "config.yml"), join(preflightDir, "config.yml"));
|
|
37
|
+
await copyFile(join(templatesDir, "triage.yml"), join(preflightDir, "triage.yml"));
|
|
38
|
+
|
|
39
|
+
console.log("✅ Created .preflight/ directory with template config files");
|
|
40
|
+
console.log(" Edit .preflight/config.yml to configure your project settings");
|
|
41
|
+
console.log(" Edit .preflight/triage.yml to customize prompt triage rules\n");
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`❌ Failed to create .preflight/ directory: ${error}\n`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface McpConfig {
|
|
48
|
+
mcpServers: Record<string, {
|
|
49
|
+
command: string;
|
|
50
|
+
args: string[];
|
|
51
|
+
env?: Record<string, string>;
|
|
52
|
+
}>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function main(): Promise<void> {
|
|
56
|
+
console.log("\n✈️ preflight — MCP server setup\n");
|
|
57
|
+
|
|
58
|
+
const mcpPath = join(process.cwd(), ".mcp.json");
|
|
59
|
+
let config: McpConfig;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const existing = await readFile(mcpPath, "utf-8");
|
|
63
|
+
config = JSON.parse(existing);
|
|
64
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
65
|
+
console.log("Found existing .mcp.json\n");
|
|
66
|
+
} catch {
|
|
67
|
+
config = { mcpServers: {} };
|
|
68
|
+
console.log("Creating new .mcp.json\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log("Choose a profile:\n");
|
|
72
|
+
console.log(" 1) minimal — 4 tools: clarify_intent, check_session_health, session_stats, prompt_score");
|
|
73
|
+
console.log(" 2) standard — 16 tools: all prompt discipline + session_stats + prompt_score");
|
|
74
|
+
console.log(" 3) full — 20 tools: everything + timeline/vector search (needs LanceDB)\n");
|
|
75
|
+
|
|
76
|
+
const choice = await ask("Profile [1/2/3] (default: 2): ");
|
|
77
|
+
const profileMap: Record<string, string> = { "1": "minimal", "2": "standard", "3": "full" };
|
|
78
|
+
const profile = profileMap[choice.trim()] || "standard";
|
|
79
|
+
|
|
80
|
+
// Ask about creating .preflight/ directory
|
|
81
|
+
console.log("\nPreflight can use either environment variables or a .preflight/ directory for configuration.");
|
|
82
|
+
console.log("The .preflight/ directory allows you to configure related projects, custom triage rules, and thresholds.\n");
|
|
83
|
+
|
|
84
|
+
const createConfig = await ask("Create .preflight/ directory with template config? [y/N]: ");
|
|
85
|
+
if (createConfig.trim().toLowerCase() === "y" || createConfig.trim().toLowerCase() === "yes") {
|
|
86
|
+
await createPreflightConfig();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const env: Record<string, string> = {
|
|
90
|
+
PROMPT_DISCIPLINE_PROFILE: profile,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (profile === "full") {
|
|
94
|
+
console.log("\nFull profile uses embeddings for vector search.");
|
|
95
|
+
const provider = await ask("Embedding provider [local/openai] (default: local): ");
|
|
96
|
+
if (provider.trim().toLowerCase() === "openai") {
|
|
97
|
+
const key = await ask("OpenAI API key (or set OPENAI_API_KEY later): ");
|
|
98
|
+
if (key.trim()) {
|
|
99
|
+
env.OPENAI_API_KEY = key.trim();
|
|
100
|
+
}
|
|
101
|
+
env.EMBEDDING_PROVIDER = "openai";
|
|
102
|
+
} else {
|
|
103
|
+
env.EMBEDDING_PROVIDER = "local";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
config.mcpServers["preflight"] = {
|
|
108
|
+
command: "npx",
|
|
109
|
+
args: ["-y", "preflight-dev@latest"],
|
|
110
|
+
env,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// For the actual server entry point, we need to point to index.ts via tsx
|
|
114
|
+
// But npx will resolve the bin entry which is the init script
|
|
115
|
+
// So use a different approach: command runs the server
|
|
116
|
+
config.mcpServers["preflight"] = {
|
|
117
|
+
command: "npx",
|
|
118
|
+
args: ["-y", "tsx", "node_modules/preflight/src/index.ts"],
|
|
119
|
+
env,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
await writeFile(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
123
|
+
|
|
124
|
+
console.log(`\n✅ preflight added! (profile: ${profile})`);
|
|
125
|
+
console.log("Restart Claude Code to connect.\n");
|
|
126
|
+
|
|
127
|
+
rl.close();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
main().catch((err) => {
|
|
131
|
+
console.error("Error:", err);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// Preflight-Dev MCP Server — v3.0
|
|
4
|
+
// =============================================================================
|
|
5
|
+
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { isToolEnabled, getProfile } from "./profiles.js";
|
|
9
|
+
import { getConfig, hasPreflightConfig } from "./lib/config.js";
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
|
|
12
|
+
// Main entry point
|
|
13
|
+
import { registerPreflightCheck } from "./tools/preflight-check.js";
|
|
14
|
+
// Category 1: Plans
|
|
15
|
+
import { registerScopeWork } from "./tools/scope-work.js";
|
|
16
|
+
// Category 2: Clarification
|
|
17
|
+
import { registerClarifyIntent } from "./tools/clarify-intent.js";
|
|
18
|
+
// Category 3: Delegation
|
|
19
|
+
import { registerEnrichAgentTask } from "./tools/enrich-agent-task.js";
|
|
20
|
+
// Category 4: Follow-up Specificity
|
|
21
|
+
import { registerSharpenFollowup } from "./tools/sharpen-followup.js";
|
|
22
|
+
// Category 5: Token Efficiency
|
|
23
|
+
import { registerTokenAudit } from "./tools/token-audit.js";
|
|
24
|
+
// Category 6: Sequencing
|
|
25
|
+
import { registerSequenceTasks } from "./tools/sequence-tasks.js";
|
|
26
|
+
// Category 7: Compaction Management
|
|
27
|
+
import { registerCheckpoint } from "./tools/checkpoint.js";
|
|
28
|
+
// Category 8: Session Lifecycle
|
|
29
|
+
import { registerSessionHealth } from "./tools/session-health.js";
|
|
30
|
+
// Category 9: Error Recovery
|
|
31
|
+
import { registerLogCorrection } from "./tools/log-correction.js";
|
|
32
|
+
// Category 10: Workspace Hygiene
|
|
33
|
+
import { registerAuditWorkspace } from "./tools/audit-workspace.js";
|
|
34
|
+
// Category 11: Cross-Session Continuity
|
|
35
|
+
import { registerSessionHandoff } from "./tools/session-handoff.js";
|
|
36
|
+
import { registerWhatChanged } from "./tools/what-changed.js";
|
|
37
|
+
// Category 12: Verification
|
|
38
|
+
import { registerVerifyCompletion } from "./tools/verify-completion.js";
|
|
39
|
+
// New lightweight tools
|
|
40
|
+
import { registerSessionStats } from "./tools/session-stats.js";
|
|
41
|
+
import { registerPromptScore } from "./tools/prompt-score.js";
|
|
42
|
+
// Timeline: Project Intelligence
|
|
43
|
+
import { registerOnboardProject } from "./tools/onboard-project.js";
|
|
44
|
+
import { registerSearchHistory } from "./tools/search-history.js";
|
|
45
|
+
import { registerTimeline } from "./tools/timeline-view.js";
|
|
46
|
+
import { registerScanSessions } from "./tools/scan-sessions.js";
|
|
47
|
+
import { registerGenerateScorecard } from "./tools/generate-scorecard.js";
|
|
48
|
+
|
|
49
|
+
// Validate related projects from config
|
|
50
|
+
function validateRelatedProjects(): void {
|
|
51
|
+
const config = getConfig();
|
|
52
|
+
const projects = config.related_projects;
|
|
53
|
+
|
|
54
|
+
if (projects.length === 0) return;
|
|
55
|
+
|
|
56
|
+
const invalid: string[] = [];
|
|
57
|
+
|
|
58
|
+
for (const project of projects) {
|
|
59
|
+
if (!existsSync(project.path)) {
|
|
60
|
+
invalid.push(`${project.alias} (${project.path})`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (invalid.length > 0) {
|
|
65
|
+
process.stderr.write(`preflight-dev: warning - related projects contain invalid paths: ${invalid.join(", ")}\n`);
|
|
66
|
+
} else {
|
|
67
|
+
process.stderr.write(`preflight-dev: related projects: ${projects.length} configured\n`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Load config and validate related projects on startup
|
|
72
|
+
const config = getConfig();
|
|
73
|
+
validateRelatedProjects();
|
|
74
|
+
|
|
75
|
+
const profile = getProfile();
|
|
76
|
+
const server = new McpServer({
|
|
77
|
+
name: "preflight",
|
|
78
|
+
version: "3.0.0",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Register tools based on profile
|
|
82
|
+
type RegisterFn = (server: McpServer) => void;
|
|
83
|
+
|
|
84
|
+
const toolRegistry: Array<[string, RegisterFn]> = [
|
|
85
|
+
["preflight_check", registerPreflightCheck],
|
|
86
|
+
["scope_work", registerScopeWork],
|
|
87
|
+
["clarify_intent", registerClarifyIntent],
|
|
88
|
+
["enrich_agent_task", registerEnrichAgentTask],
|
|
89
|
+
["sharpen_followup", registerSharpenFollowup],
|
|
90
|
+
["token_audit", registerTokenAudit],
|
|
91
|
+
["sequence_tasks", registerSequenceTasks],
|
|
92
|
+
["checkpoint", registerCheckpoint],
|
|
93
|
+
["check_session_health", registerSessionHealth],
|
|
94
|
+
["log_correction", registerLogCorrection],
|
|
95
|
+
["audit_workspace", registerAuditWorkspace],
|
|
96
|
+
["session_handoff", registerSessionHandoff],
|
|
97
|
+
["what_changed", registerWhatChanged],
|
|
98
|
+
["verify_completion", registerVerifyCompletion],
|
|
99
|
+
["session_stats", registerSessionStats],
|
|
100
|
+
["prompt_score", registerPromptScore],
|
|
101
|
+
["onboard_project", registerOnboardProject],
|
|
102
|
+
["search_history", registerSearchHistory],
|
|
103
|
+
["timeline_view", registerTimeline],
|
|
104
|
+
["scan_sessions", registerScanSessions],
|
|
105
|
+
["generate_scorecard", registerGenerateScorecard],
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
let registered = 0;
|
|
109
|
+
for (const [name, register] of toolRegistry) {
|
|
110
|
+
if (isToolEnabled(name)) {
|
|
111
|
+
register(server);
|
|
112
|
+
registered++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const configSource = hasPreflightConfig() ? ".preflight/" : "env vars";
|
|
117
|
+
process.stderr.write(`preflight: profile=${profile}, tools=${registered}, config=${configSource}\n`);
|
|
118
|
+
|
|
119
|
+
// Graceful shutdown
|
|
120
|
+
function shutdown() {
|
|
121
|
+
process.stderr.write("preflight: shutting down\n");
|
|
122
|
+
process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
process.on("SIGINT", shutdown);
|
|
125
|
+
process.on("SIGTERM", shutdown);
|
|
126
|
+
|
|
127
|
+
// Connect transport
|
|
128
|
+
try {
|
|
129
|
+
const transport = new StdioServerTransport();
|
|
130
|
+
await server.connect(transport);
|
|
131
|
+
process.stderr.write("preflight: server started\n");
|
|
132
|
+
} catch (err) {
|
|
133
|
+
process.stderr.write(`preflight: failed to start — ${err}\n`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Preflight Configuration System
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Loads configuration from .preflight/ directory if present,
|
|
5
|
+
// falls back to environment variables.
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { load as yamlLoad } from "js-yaml";
|
|
11
|
+
import { PROJECT_DIR } from "./files.js";
|
|
12
|
+
|
|
13
|
+
export type Profile = "minimal" | "standard" | "full";
|
|
14
|
+
export type EmbeddingProvider = "local" | "openai";
|
|
15
|
+
export type TriageStrictness = "relaxed" | "standard" | "strict";
|
|
16
|
+
|
|
17
|
+
export interface RelatedProject {
|
|
18
|
+
path: string;
|
|
19
|
+
alias: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PreflightConfig {
|
|
23
|
+
profile: Profile;
|
|
24
|
+
related_projects: RelatedProject[];
|
|
25
|
+
thresholds: {
|
|
26
|
+
session_stale_minutes: number;
|
|
27
|
+
max_tool_calls_before_checkpoint: number;
|
|
28
|
+
correction_pattern_threshold: number;
|
|
29
|
+
};
|
|
30
|
+
embeddings: {
|
|
31
|
+
provider: EmbeddingProvider;
|
|
32
|
+
openai_api_key?: string;
|
|
33
|
+
};
|
|
34
|
+
triage: {
|
|
35
|
+
rules: {
|
|
36
|
+
always_check: string[];
|
|
37
|
+
skip: string[];
|
|
38
|
+
cross_service_keywords: string[];
|
|
39
|
+
};
|
|
40
|
+
strictness: TriageStrictness;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Default configuration (env var fallback)
|
|
45
|
+
const DEFAULT_CONFIG: PreflightConfig = {
|
|
46
|
+
profile: "standard",
|
|
47
|
+
related_projects: [],
|
|
48
|
+
thresholds: {
|
|
49
|
+
session_stale_minutes: 30,
|
|
50
|
+
max_tool_calls_before_checkpoint: 100,
|
|
51
|
+
correction_pattern_threshold: 3,
|
|
52
|
+
},
|
|
53
|
+
embeddings: {
|
|
54
|
+
provider: "local",
|
|
55
|
+
},
|
|
56
|
+
triage: {
|
|
57
|
+
rules: {
|
|
58
|
+
always_check: ["rewards", "permissions", "migration", "schema"],
|
|
59
|
+
skip: ["commit", "format", "lint"],
|
|
60
|
+
cross_service_keywords: ["auth", "notification", "event", "webhook"],
|
|
61
|
+
},
|
|
62
|
+
strictness: "standard",
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
let _config: PreflightConfig | null = null;
|
|
67
|
+
|
|
68
|
+
/** Load config from .preflight/ directory or fall back to env vars */
|
|
69
|
+
function loadConfig(): PreflightConfig {
|
|
70
|
+
const preflightDir = join(PROJECT_DIR, ".preflight");
|
|
71
|
+
const configPath = join(preflightDir, "config.yml");
|
|
72
|
+
const triagePath = join(preflightDir, "triage.yml");
|
|
73
|
+
|
|
74
|
+
// Start with defaults
|
|
75
|
+
let config: PreflightConfig = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
76
|
+
|
|
77
|
+
// Load .preflight/config.yml if it exists
|
|
78
|
+
if (existsSync(configPath)) {
|
|
79
|
+
try {
|
|
80
|
+
const configYaml = readFileSync(configPath, "utf-8");
|
|
81
|
+
const configData = yamlLoad(configYaml) as any;
|
|
82
|
+
|
|
83
|
+
if (configData) {
|
|
84
|
+
// Merge config data with defaults
|
|
85
|
+
if (configData.profile) config.profile = configData.profile;
|
|
86
|
+
if (configData.related_projects) config.related_projects = configData.related_projects;
|
|
87
|
+
if (configData.thresholds) config.thresholds = { ...config.thresholds, ...configData.thresholds };
|
|
88
|
+
if (configData.embeddings) config.embeddings = { ...config.embeddings, ...configData.embeddings };
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.warn(`preflight: warning - failed to parse .preflight/config.yml: ${error}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Load .preflight/triage.yml if it exists
|
|
96
|
+
if (existsSync(triagePath)) {
|
|
97
|
+
try {
|
|
98
|
+
const triageYaml = readFileSync(triagePath, "utf-8");
|
|
99
|
+
const triageData = yamlLoad(triageYaml) as any;
|
|
100
|
+
|
|
101
|
+
if (triageData) {
|
|
102
|
+
if (triageData.rules) config.triage.rules = { ...config.triage.rules, ...triageData.rules };
|
|
103
|
+
if (triageData.strictness) config.triage.strictness = triageData.strictness;
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.warn(`preflight: warning - failed to parse .preflight/triage.yml: ${error}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Apply environment variable overrides (env vars are fallback, .preflight/ takes precedence)
|
|
111
|
+
// Only use env vars if .preflight/ directory doesn't exist
|
|
112
|
+
if (!existsSync(preflightDir)) {
|
|
113
|
+
// Profile
|
|
114
|
+
const envProfile = process.env.PROMPT_DISCIPLINE_PROFILE?.toLowerCase();
|
|
115
|
+
if (envProfile === "minimal" || envProfile === "standard" || envProfile === "full") {
|
|
116
|
+
config.profile = envProfile;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Related projects
|
|
120
|
+
const envRelated = process.env.PREFLIGHT_RELATED;
|
|
121
|
+
if (envRelated) {
|
|
122
|
+
const projects = envRelated.split(",").map(p => p.trim()).filter(Boolean);
|
|
123
|
+
config.related_projects = projects.map(path => ({ path, alias: path.split("/").pop() || path }));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Embedding provider
|
|
127
|
+
const envProvider = process.env.EMBEDDING_PROVIDER?.toLowerCase();
|
|
128
|
+
if (envProvider === "local" || envProvider === "openai") {
|
|
129
|
+
config.embeddings.provider = envProvider;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// OpenAI API key
|
|
133
|
+
if (process.env.OPENAI_API_KEY) {
|
|
134
|
+
config.embeddings.openai_api_key = process.env.OPENAI_API_KEY;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return config;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Get the singleton configuration object */
|
|
142
|
+
export function getConfig(): PreflightConfig {
|
|
143
|
+
if (_config === null) {
|
|
144
|
+
_config = loadConfig();
|
|
145
|
+
}
|
|
146
|
+
return _config;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Get related projects as simple path array (backward compatibility) */
|
|
150
|
+
export function getRelatedProjects(): string[] {
|
|
151
|
+
return getConfig().related_projects.map(p => p.path);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Check if .preflight/ directory exists */
|
|
155
|
+
export function hasPreflightConfig(): boolean {
|
|
156
|
+
return existsSync(join(PROJECT_DIR, ".preflight"));
|
|
157
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { pipeline } from "@xenova/transformers";
|
|
2
|
+
|
|
3
|
+
// --- Types ---
|
|
4
|
+
|
|
5
|
+
export interface EmbeddingProvider {
|
|
6
|
+
embed(text: string): Promise<number[]>;
|
|
7
|
+
embedBatch(texts: string[]): Promise<number[][]>;
|
|
8
|
+
dimensions: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// --- Text Preprocessing ---
|
|
12
|
+
|
|
13
|
+
export function preprocessText(text: string): string {
|
|
14
|
+
let t = text;
|
|
15
|
+
// Strip markdown formatting
|
|
16
|
+
t = t.replace(/```[\s\S]*?```/g, " "); // code blocks
|
|
17
|
+
t = t.replace(/`[^`]+`/g, " "); // inline code
|
|
18
|
+
t = t.replace(/[#*_~>\[\]()!]/g, ""); // markdown chars
|
|
19
|
+
t = t.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); // links
|
|
20
|
+
// Normalize whitespace
|
|
21
|
+
t = t.replace(/\s+/g, " ").trim();
|
|
22
|
+
// Truncate to ~512 tokens (~2048 chars as rough approximation)
|
|
23
|
+
if (t.length > 2048) t = t.slice(0, 2048);
|
|
24
|
+
return t;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- Local Provider (Xenova/transformers) ---
|
|
28
|
+
|
|
29
|
+
let extractor: any = null;
|
|
30
|
+
|
|
31
|
+
async function getExtractor(): Promise<any> {
|
|
32
|
+
if (!extractor) {
|
|
33
|
+
extractor = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
|
|
34
|
+
}
|
|
35
|
+
return extractor;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class LocalEmbeddingProvider implements EmbeddingProvider {
|
|
39
|
+
dimensions = 384;
|
|
40
|
+
|
|
41
|
+
async embed(text: string): Promise<number[]> {
|
|
42
|
+
const ext = await getExtractor();
|
|
43
|
+
const processed = preprocessText(text);
|
|
44
|
+
const output = await ext(processed, { pooling: "mean", normalize: true });
|
|
45
|
+
return Array.from(output.data as Float32Array);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async embedBatch(texts: string[]): Promise<number[][]> {
|
|
49
|
+
const results: number[][] = [];
|
|
50
|
+
for (const text of texts) {
|
|
51
|
+
results.push(await this.embed(text));
|
|
52
|
+
}
|
|
53
|
+
return results;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- OpenAI Provider ---
|
|
58
|
+
|
|
59
|
+
class OpenAIEmbeddingProvider implements EmbeddingProvider {
|
|
60
|
+
dimensions = 1536;
|
|
61
|
+
private apiKey: string;
|
|
62
|
+
|
|
63
|
+
constructor(apiKey: string) {
|
|
64
|
+
this.apiKey = apiKey;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async embed(text: string): Promise<number[]> {
|
|
68
|
+
const [result] = await this.embedBatch([text]);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async embedBatch(texts: string[]): Promise<number[][]> {
|
|
73
|
+
const results: number[][] = [];
|
|
74
|
+
const processed = texts.map(preprocessText);
|
|
75
|
+
|
|
76
|
+
// Batch up to 100 at a time
|
|
77
|
+
for (let i = 0; i < processed.length; i += 100) {
|
|
78
|
+
const batch = processed.slice(i, i + 100);
|
|
79
|
+
const resp = await fetch("https://api.openai.com/v1/embeddings", {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: {
|
|
82
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify({ model: "text-embedding-3-small", input: batch }),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!resp.ok) {
|
|
89
|
+
const err = await resp.text();
|
|
90
|
+
throw new Error(`OpenAI embeddings API error ${resp.status}: ${err}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const data = await resp.json();
|
|
94
|
+
// Sort by index to preserve order
|
|
95
|
+
const sorted = data.data.sort((a: any, b: any) => a.index - b.index);
|
|
96
|
+
for (const item of sorted) {
|
|
97
|
+
results.push(item.embedding);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- Factory ---
|
|
106
|
+
|
|
107
|
+
export interface EmbeddingConfig {
|
|
108
|
+
provider: "local" | "openai";
|
|
109
|
+
apiKey?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function createEmbeddingProvider(config: EmbeddingConfig): EmbeddingProvider {
|
|
113
|
+
if (config.provider === "openai") {
|
|
114
|
+
if (!config.apiKey) throw new Error("OpenAI API key required for openai embedding provider");
|
|
115
|
+
return new OpenAIEmbeddingProvider(config.apiKey);
|
|
116
|
+
}
|
|
117
|
+
return new LocalEmbeddingProvider();
|
|
118
|
+
}
|
package/src/lib/files.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import type { DocInfo, DocMeta } from "../types.js";
|
|
4
|
+
|
|
5
|
+
/** Single source of truth for the project directory. */
|
|
6
|
+
export const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
7
|
+
|
|
8
|
+
const MAX_SCAN_DEPTH = 10;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Read a file relative to PROJECT_DIR, returning at most `maxLines` lines.
|
|
12
|
+
* Returns null if the file doesn't exist or can't be read as UTF-8.
|
|
13
|
+
*/
|
|
14
|
+
export function readIfExists(relPath: string, maxLines = 50): string | null {
|
|
15
|
+
const full = join(PROJECT_DIR, relPath);
|
|
16
|
+
if (!existsSync(full)) return null;
|
|
17
|
+
try {
|
|
18
|
+
const buf = readFileSync(full);
|
|
19
|
+
// Reject likely-binary files: check for null bytes in first 8KB
|
|
20
|
+
if (buf.subarray(0, 8192).includes(0)) return null;
|
|
21
|
+
const lines = buf.toString("utf-8").split("\n");
|
|
22
|
+
return lines.slice(0, maxLines).join("\n");
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Scan .claude/ for markdown docs. Returns content + metadata by default.
|
|
30
|
+
* Pass `metadataOnly: true` to skip reading file content.
|
|
31
|
+
*/
|
|
32
|
+
export function findWorkspaceDocs(opts?: { metadataOnly?: boolean }): Record<string, DocInfo> {
|
|
33
|
+
const docs: Record<string, DocInfo> = {};
|
|
34
|
+
const claudeDir = join(PROJECT_DIR, ".claude");
|
|
35
|
+
if (!existsSync(claudeDir)) return docs;
|
|
36
|
+
|
|
37
|
+
const scanDir = (dir: string, prefix = "", depth = 0): void => {
|
|
38
|
+
if (depth > MAX_SCAN_DEPTH) return;
|
|
39
|
+
try {
|
|
40
|
+
for (const entry of readdirSync(dir)) {
|
|
41
|
+
const full = join(dir, entry);
|
|
42
|
+
const rel = prefix ? `${prefix}/${entry}` : entry;
|
|
43
|
+
const stat = statSync(full);
|
|
44
|
+
if (stat.isDirectory() && !entry.startsWith(".") && !entry.includes("node_modules") && entry !== "preflight-state") {
|
|
45
|
+
scanDir(full, rel, depth + 1);
|
|
46
|
+
} else if (entry.endsWith(".md") && stat.size < 50000) {
|
|
47
|
+
docs[rel] = {
|
|
48
|
+
content: opts?.metadataOnly ? "" : readFileSync(full, "utf-8").split("\n").slice(0, 40).join("\n"),
|
|
49
|
+
mtime: stat.mtime,
|
|
50
|
+
size: stat.size,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch { /* permission errors, etc. */ }
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
scanDir(claudeDir);
|
|
58
|
+
return docs;
|
|
59
|
+
}
|