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.
Files changed (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/bin/cli.js +11 -0
  4. package/dist/cli/init.d.ts +2 -0
  5. package/dist/cli/init.js +154 -0
  6. package/dist/cli/init.js.map +1 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +122 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/lib/config.d.ts +34 -0
  11. package/dist/lib/config.js +118 -0
  12. package/dist/lib/config.js.map +1 -0
  13. package/dist/lib/embeddings.d.ts +11 -0
  14. package/dist/lib/embeddings.js +88 -0
  15. package/dist/lib/embeddings.js.map +1 -0
  16. package/dist/lib/files.d.ts +15 -0
  17. package/dist/lib/files.js +60 -0
  18. package/dist/lib/files.js.map +1 -0
  19. package/dist/lib/git-extractor.d.ts +9 -0
  20. package/dist/lib/git-extractor.js +116 -0
  21. package/dist/lib/git-extractor.js.map +1 -0
  22. package/dist/lib/git.d.ts +29 -0
  23. package/dist/lib/git.js +86 -0
  24. package/dist/lib/git.js.map +1 -0
  25. package/dist/lib/session-parser.d.ts +45 -0
  26. package/dist/lib/session-parser.js +267 -0
  27. package/dist/lib/session-parser.js.map +1 -0
  28. package/dist/lib/state.d.ts +21 -0
  29. package/dist/lib/state.js +86 -0
  30. package/dist/lib/state.js.map +1 -0
  31. package/dist/lib/timeline-db.d.ts +67 -0
  32. package/dist/lib/timeline-db.js +380 -0
  33. package/dist/lib/timeline-db.js.map +1 -0
  34. package/dist/lib/triage.d.ts +29 -0
  35. package/dist/lib/triage.js +193 -0
  36. package/dist/lib/triage.js.map +1 -0
  37. package/dist/profiles.d.ts +3 -0
  38. package/dist/profiles.js +65 -0
  39. package/dist/profiles.js.map +1 -0
  40. package/dist/tools/audit-workspace.d.ts +2 -0
  41. package/dist/tools/audit-workspace.js +86 -0
  42. package/dist/tools/audit-workspace.js.map +1 -0
  43. package/dist/tools/checkpoint.d.ts +2 -0
  44. package/dist/tools/checkpoint.js +108 -0
  45. package/dist/tools/checkpoint.js.map +1 -0
  46. package/dist/tools/clarify-intent.d.ts +2 -0
  47. package/dist/tools/clarify-intent.js +180 -0
  48. package/dist/tools/clarify-intent.js.map +1 -0
  49. package/dist/tools/enrich-agent-task.d.ts +2 -0
  50. package/dist/tools/enrich-agent-task.js +97 -0
  51. package/dist/tools/enrich-agent-task.js.map +1 -0
  52. package/dist/tools/generate-scorecard.d.ts +2 -0
  53. package/dist/tools/generate-scorecard.js +617 -0
  54. package/dist/tools/generate-scorecard.js.map +1 -0
  55. package/dist/tools/log-correction.d.ts +2 -0
  56. package/dist/tools/log-correction.js +76 -0
  57. package/dist/tools/log-correction.js.map +1 -0
  58. package/dist/tools/onboard-project.d.ts +2 -0
  59. package/dist/tools/onboard-project.js +179 -0
  60. package/dist/tools/onboard-project.js.map +1 -0
  61. package/dist/tools/preflight-check.d.ts +2 -0
  62. package/dist/tools/preflight-check.js +229 -0
  63. package/dist/tools/preflight-check.js.map +1 -0
  64. package/dist/tools/prompt-score.d.ts +2 -0
  65. package/dist/tools/prompt-score.js +132 -0
  66. package/dist/tools/prompt-score.js.map +1 -0
  67. package/dist/tools/scan-sessions.d.ts +2 -0
  68. package/dist/tools/scan-sessions.js +182 -0
  69. package/dist/tools/scan-sessions.js.map +1 -0
  70. package/dist/tools/scope-work.d.ts +2 -0
  71. package/dist/tools/scope-work.js +214 -0
  72. package/dist/tools/scope-work.js.map +1 -0
  73. package/dist/tools/search-history.d.ts +2 -0
  74. package/dist/tools/search-history.js +130 -0
  75. package/dist/tools/search-history.js.map +1 -0
  76. package/dist/tools/sequence-tasks.d.ts +2 -0
  77. package/dist/tools/sequence-tasks.js +165 -0
  78. package/dist/tools/sequence-tasks.js.map +1 -0
  79. package/dist/tools/session-handoff.d.ts +2 -0
  80. package/dist/tools/session-handoff.js +113 -0
  81. package/dist/tools/session-handoff.js.map +1 -0
  82. package/dist/tools/session-health.d.ts +2 -0
  83. package/dist/tools/session-health.js +111 -0
  84. package/dist/tools/session-health.js.map +1 -0
  85. package/dist/tools/session-stats.d.ts +2 -0
  86. package/dist/tools/session-stats.js +112 -0
  87. package/dist/tools/session-stats.js.map +1 -0
  88. package/dist/tools/sharpen-followup.d.ts +2 -0
  89. package/dist/tools/sharpen-followup.js +192 -0
  90. package/dist/tools/sharpen-followup.js.map +1 -0
  91. package/dist/tools/timeline-view.d.ts +2 -0
  92. package/dist/tools/timeline-view.js +165 -0
  93. package/dist/tools/timeline-view.js.map +1 -0
  94. package/dist/tools/token-audit.d.ts +2 -0
  95. package/dist/tools/token-audit.js +227 -0
  96. package/dist/tools/token-audit.js.map +1 -0
  97. package/dist/tools/verify-completion.d.ts +2 -0
  98. package/dist/tools/verify-completion.js +154 -0
  99. package/dist/tools/verify-completion.js.map +1 -0
  100. package/dist/tools/what-changed.d.ts +2 -0
  101. package/dist/tools/what-changed.js +40 -0
  102. package/dist/tools/what-changed.js.map +1 -0
  103. package/dist/types.d.ts +78 -0
  104. package/dist/types.js +2 -0
  105. package/dist/types.js.map +1 -0
  106. package/package.json +52 -0
  107. package/src/cli/init.ts +133 -0
  108. package/src/index.ts +135 -0
  109. package/src/lib/config.ts +157 -0
  110. package/src/lib/embeddings.ts +118 -0
  111. package/src/lib/files.ts +59 -0
  112. package/src/lib/git-extractor.ts +137 -0
  113. package/src/lib/git.ts +89 -0
  114. package/src/lib/session-parser.ts +325 -0
  115. package/src/lib/state.ts +86 -0
  116. package/src/lib/timeline-db.ts +490 -0
  117. package/src/lib/triage.ts +255 -0
  118. package/src/profiles.ts +70 -0
  119. package/src/templates/config.yml +23 -0
  120. package/src/templates/triage.yml +27 -0
  121. package/src/tools/audit-workspace.ts +97 -0
  122. package/src/tools/checkpoint.ts +119 -0
  123. package/src/tools/clarify-intent.ts +191 -0
  124. package/src/tools/enrich-agent-task.ts +108 -0
  125. package/src/tools/generate-scorecard.ts +673 -0
  126. package/src/tools/log-correction.ts +89 -0
  127. package/src/tools/onboard-project.ts +214 -0
  128. package/src/tools/preflight-check.ts +263 -0
  129. package/src/tools/prompt-score.ts +150 -0
  130. package/src/tools/scan-sessions.ts +209 -0
  131. package/src/tools/scope-work.ts +238 -0
  132. package/src/tools/search-history.ts +145 -0
  133. package/src/tools/sequence-tasks.ts +182 -0
  134. package/src/tools/session-handoff.ts +125 -0
  135. package/src/tools/session-health.ts +107 -0
  136. package/src/tools/session-stats.ts +134 -0
  137. package/src/tools/sharpen-followup.ts +200 -0
  138. package/src/tools/timeline-view.ts +181 -0
  139. package/src/tools/token-audit.ts +259 -0
  140. package/src/tools/verify-completion.ts +159 -0
  141. package/src/tools/what-changed.ts +48 -0
  142. package/src/types.ts +87 -0
@@ -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
+ }
@@ -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
+ }