@minhpnq1807/contextos 0.5.45 → 0.5.49

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.
@@ -0,0 +1,207 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const PROFILE_CACHE_FILE = "project-profile.json";
6
+ const MAX_DEPENDENCIES = 80;
7
+ const MAX_SCRIPTS = 30;
8
+ const MAX_RECENT_FILES = 20;
9
+
10
+ export function projectProfile({ cwd = process.cwd(), dataDir } = {}) {
11
+ const fingerprint = projectFingerprint(cwd);
12
+ const cachePath = dataDir ? path.join(dataDir, PROFILE_CACHE_FILE) : null;
13
+ const cached = cachePath ? readCachedProfile(cachePath, fingerprint) : null;
14
+ if (cached) return cached;
15
+
16
+ const packagePaths = workspacePackagePaths(cwd);
17
+ const packageTexts = packagePaths.map((packagePath) => packageSignal(cwd, packagePath)).filter(Boolean);
18
+ const recentFiles = recentGitFiles(cwd, MAX_RECENT_FILES);
19
+ const languages = languageSignal({ cwd, packagePaths, recentFiles });
20
+ const embeddableString = [
21
+ packageTexts.length ? `[project packages: ${packageTexts.join(" | ")}]` : "",
22
+ languages.length ? `[project languages: ${languages.join(" ")}]` : "",
23
+ recentFiles.length ? `[recent files: ${recentFiles.join(", ")}]` : ""
24
+ ].filter(Boolean).join(" ");
25
+
26
+ const profile = { fingerprint, embeddableString };
27
+ if (cachePath) writeCachedProfile(cachePath, profile);
28
+ return profile;
29
+ }
30
+
31
+ export function fusedProjectQuery({ prompt = "", cwd = process.cwd(), dataDir } = {}) {
32
+ const profile = projectProfile({ cwd, dataDir });
33
+ return [String(prompt || "").trim(), profile.embeddableString].filter(Boolean).join("\n");
34
+ }
35
+
36
+ function readCachedProfile(cachePath, fingerprint) {
37
+ try {
38
+ const cached = JSON.parse(fs.readFileSync(cachePath, "utf8"));
39
+ if (cached?.fingerprint === fingerprint && typeof cached.embeddableString === "string") return cached;
40
+ } catch {
41
+ // Cache miss.
42
+ }
43
+ return null;
44
+ }
45
+
46
+ function writeCachedProfile(cachePath, profile) {
47
+ try {
48
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
49
+ fs.writeFileSync(cachePath, JSON.stringify(profile, null, 2));
50
+ } catch {
51
+ // The profile is an optimization; prompt scoring can continue without a cache write.
52
+ }
53
+ }
54
+
55
+ function projectFingerprint(cwd) {
56
+ const parts = [];
57
+ for (const packagePath of workspacePackagePaths(cwd)) {
58
+ try {
59
+ const stat = fs.statSync(packagePath);
60
+ parts.push(`${path.relative(cwd, packagePath)}:${stat.mtimeMs}:${stat.size}`);
61
+ } catch {
62
+ parts.push(`${path.relative(cwd, packagePath)}:missing`);
63
+ }
64
+ }
65
+ parts.push(`git:${gitHead(cwd)}`);
66
+ return parts.join("|");
67
+ }
68
+
69
+ function packageSignal(cwd, packagePath) {
70
+ const packageJson = readJson(packagePath);
71
+ if (!packageJson) return "";
72
+ const packageDir = path.dirname(packagePath);
73
+ const dependencies = [
74
+ ...Object.keys(packageJson.dependencies || {}),
75
+ ...Object.keys(packageJson.devDependencies || {}),
76
+ ...Object.keys(packageJson.peerDependencies || {})
77
+ ].slice(0, MAX_DEPENDENCIES);
78
+ const scripts = Object.keys(packageJson.scripts || {}).slice(0, MAX_SCRIPTS);
79
+ const configFiles = ["app.json", "app.config.js", "app.config.ts", "eas.json", "tsconfig.json", "Dockerfile"]
80
+ .filter((fileName) => fs.existsSync(path.join(packageDir, fileName)));
81
+ return [
82
+ path.relative(cwd, packagePath) || "package.json",
83
+ packageJson.name,
84
+ packageJson.description,
85
+ Array.isArray(packageJson.keywords) ? packageJson.keywords.join(" ") : "",
86
+ dependencies.join(" "),
87
+ scripts.length ? `scripts ${scripts.join(" ")}` : "",
88
+ configFiles.join(" ")
89
+ ].filter(Boolean).join(" ");
90
+ }
91
+
92
+ function languageSignal({ cwd, packagePaths, recentFiles }) {
93
+ const values = new Set();
94
+ for (const packagePath of packagePaths) {
95
+ const packageDir = path.dirname(packagePath);
96
+ const packageJson = readJson(packagePath);
97
+ const deps = normalize(Object.keys({
98
+ ...(packageJson?.dependencies || {}),
99
+ ...(packageJson?.devDependencies || {})
100
+ }).join(" "));
101
+ if (deps.includes("typescript") || fs.existsSync(path.join(packageDir, "tsconfig.json"))) values.add("TypeScript");
102
+ if (deps.includes("python")) values.add("Python");
103
+ if (deps.includes("go")) values.add("Go");
104
+ if (deps.includes("react")) values.add("React");
105
+ if (deps.includes("next")) values.add("Next.js");
106
+ if (deps.includes("expo") || deps.includes("react native")) values.add("React Native");
107
+ }
108
+ for (const file of recentFiles) {
109
+ const ext = path.extname(file);
110
+ if ([".ts", ".tsx"].includes(ext)) values.add("TypeScript");
111
+ if ([".js", ".jsx", ".mjs", ".cjs"].includes(ext)) values.add("JavaScript");
112
+ if (ext === ".py") values.add("Python");
113
+ if (ext === ".go") values.add("Go");
114
+ if (ext === ".rs") values.add("Rust");
115
+ if (ext === ".java") values.add("Java");
116
+ }
117
+ if (!values.size && fs.existsSync(path.join(cwd, "package.json"))) values.add("JavaScript");
118
+ return [...values];
119
+ }
120
+
121
+ function recentGitFiles(cwd, limit) {
122
+ try {
123
+ const output = execFileSync("git", ["log", "--name-only", "--pretty=format:", "-n", "30"], {
124
+ cwd,
125
+ encoding: "utf8",
126
+ stdio: ["ignore", "pipe", "ignore"],
127
+ timeout: 300
128
+ });
129
+ const seen = new Set();
130
+ for (const line of output.split(/\r?\n/)) {
131
+ const file = line.trim();
132
+ if (!file || seen.has(file)) continue;
133
+ seen.add(file);
134
+ if (seen.size >= limit) break;
135
+ }
136
+ return [...seen];
137
+ } catch {
138
+ return [];
139
+ }
140
+ }
141
+
142
+ function gitHead(cwd) {
143
+ try {
144
+ return execFileSync("git", ["rev-parse", "HEAD"], {
145
+ cwd,
146
+ encoding: "utf8",
147
+ stdio: ["ignore", "pipe", "ignore"],
148
+ timeout: 300
149
+ }).trim();
150
+ } catch {
151
+ return "nogit";
152
+ }
153
+ }
154
+
155
+ export function workspacePackagePaths(cwd) {
156
+ const rootPackagePath = path.join(cwd, "package.json");
157
+ const rootPackage = readJson(rootPackagePath);
158
+ const paths = new Set(fs.existsSync(rootPackagePath) ? [rootPackagePath] : []);
159
+ for (const workspace of workspacePatterns(rootPackage?.workspaces)) {
160
+ for (const packagePath of expandWorkspacePattern({ cwd, pattern: workspace })) {
161
+ paths.add(packagePath);
162
+ }
163
+ }
164
+ return [...paths];
165
+ }
166
+
167
+ function workspacePatterns(workspaces) {
168
+ if (Array.isArray(workspaces)) return workspaces.filter((item) => typeof item === "string");
169
+ if (Array.isArray(workspaces?.packages)) return workspaces.packages.filter((item) => typeof item === "string");
170
+ return [];
171
+ }
172
+
173
+ function expandWorkspacePattern({ cwd, pattern }) {
174
+ const normalizedPattern = String(pattern || "").replace(/\\/g, "/").replace(/\/+$/g, "");
175
+ if (!normalizedPattern || normalizedPattern.startsWith("..") || path.isAbsolute(normalizedPattern)) return [];
176
+ if (!normalizedPattern.includes("*")) {
177
+ const packagePath = path.join(cwd, normalizedPattern, "package.json");
178
+ return fs.existsSync(packagePath) ? [packagePath] : [];
179
+ }
180
+ const parts = normalizedPattern.split("/");
181
+ const starIndex = parts.indexOf("*");
182
+ if (starIndex < 0 || parts.includes("**")) return [];
183
+ const baseDir = path.join(cwd, ...parts.slice(0, starIndex));
184
+ const suffix = parts.slice(starIndex + 1);
185
+ let entries = [];
186
+ try {
187
+ entries = fs.readdirSync(baseDir, { withFileTypes: true });
188
+ } catch {
189
+ return [];
190
+ }
191
+ return entries
192
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
193
+ .map((entry) => path.join(baseDir, entry.name, ...suffix, "package.json"))
194
+ .filter((packagePath) => fs.existsSync(packagePath));
195
+ }
196
+
197
+ function readJson(filePath) {
198
+ try {
199
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+
205
+ function normalize(value) {
206
+ return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
207
+ }
@@ -3,15 +3,11 @@ import { appendJsonLine, writeJsonFile } from "./fs-utils.js";
3
3
  import { maybeAutoWarmWorkspace } from "./auto-warm.js";
4
4
  import { callCtxScoreContext } from "./ctx-mcp-client.js";
5
5
  import { resolveHookCwd } from "./hook-io.js";
6
- import { loadOutputConfig } from "./output-config.js";
6
+ import { loadOutputConfig, outputConfigLimits } from "./output-config.js";
7
7
  import { scoreContext as scoreContextDirect } from "./score-context.js";
8
8
  import fs from "node:fs";
9
9
  import path from "node:path";
10
10
 
11
- const PROMPT_FILE_LIMIT = 7;
12
- const PROMPT_SKILL_LIMIT = 7;
13
- const PROMPT_WORKFLOW_LIMIT = 2;
14
-
15
11
  export async function handlePromptPayload(
16
12
  payload,
17
13
  {
@@ -33,6 +29,8 @@ export async function handlePromptPayload(
33
29
  const cwd = resolvePromptTargetCwd({ cwd: hookCwd, prompt });
34
30
  const openFiles = payload.openFiles || payload.open_files || payload.files || [];
35
31
  const dataDir = dataPath ? path.dirname(dataPath) : undefined;
32
+ const effectiveOutputConfig = outputConfig || loadOutputConfig();
33
+ const promptLimits = outputConfigLimits(effectiveOutputConfig);
36
34
 
37
35
  let scored;
38
36
  try {
@@ -40,9 +38,9 @@ export async function handlePromptPayload(
40
38
  cwd,
41
39
  prompt,
42
40
  openFiles,
43
- maxFiles: PROMPT_FILE_LIMIT,
44
- maxSkills: PROMPT_SKILL_LIMIT,
45
- maxWorkflows: PROMPT_WORKFLOW_LIMIT
41
+ maxFiles: promptLimits.files,
42
+ maxSkills: promptLimits.skills,
43
+ maxWorkflows: promptLimits.workflows
46
44
  }, {
47
45
  dataDir: mcpDataDir || dataDir,
48
46
  timeoutMs: Number(process.env.CONTEXTOS_MCP_BRIDGE_TIMEOUT_MS || 2000)
@@ -53,12 +51,13 @@ export async function handlePromptPayload(
53
51
  cwd,
54
52
  prompt,
55
53
  openFiles,
56
- maxFiles: PROMPT_FILE_LIMIT,
57
- maxSkills: PROMPT_SKILL_LIMIT,
58
- maxWorkflows: PROMPT_WORKFLOW_LIMIT,
54
+ maxFiles: promptLimits.files,
55
+ maxSkills: promptLimits.skills,
56
+ maxWorkflows: promptLimits.workflows,
59
57
  dataDir: mcpDataDir || dataDir,
60
58
  embeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_EMBEDDING_TIMEOUT_MS || 500),
61
- fileEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_FILE_EMBEDDING_TIMEOUT_MS || 1000)
59
+ fileEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_FILE_EMBEDDING_TIMEOUT_MS || 1000),
60
+ skillEmbeddingTimeoutMs: Number(process.env.CONTEXTOS_HOOK_SKILL_EMBEDDING_TIMEOUT_MS || 2000)
62
61
  }), directFallbackTimeoutMs, "direct fallback scoring");
63
62
  scored.telemetry = {
64
63
  ...(scored.telemetry || {}),
@@ -76,10 +75,9 @@ export async function handlePromptPayload(
76
75
 
77
76
  if (scored.error) throw new Error(scored.error);
78
77
  const scoredRules = scored.scoredRules || [];
79
- const relevantFiles = (scored.suggestedFiles || []).slice(0, PROMPT_FILE_LIMIT);
80
- const suggestedSkills = (scored.suggestedSkills || []).slice(0, PROMPT_SKILL_LIMIT);
81
- const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0, PROMPT_WORKFLOW_LIMIT);
82
- const effectiveOutputConfig = outputConfig || loadOutputConfig();
78
+ const relevantFiles = (scored.suggestedFiles || []).slice(0, promptLimits.files);
79
+ const suggestedSkills = (scored.suggestedSkills || []).slice(0, promptLimits.skills);
80
+ const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0, promptLimits.workflows);
83
81
  const scheduled = scheduleContext({ rules: scoredRules, relevantFiles, suggestedSkills, suggestedWorkflows, outputConfig: effectiveOutputConfig });
84
82
  const contextEmptyReason = emptyContextReason({ scheduled, outputConfig: effectiveOutputConfig, injectContext });
85
83
  const autoWarm = autoWarmWorkspace({
@@ -97,7 +97,8 @@ function formatFile(file, basenameCounts) {
97
97
  }
98
98
 
99
99
  function formatSkill(skill) {
100
- return skill.name;
100
+ const name = String(skill.name || "").trim();
101
+ return name.startsWith("$") ? name : `$${name}`;
101
102
  }
102
103
 
103
104
  function formatWorkflow(workflow) {
@@ -17,7 +17,9 @@ export async function scoreContext({
17
17
  skills = null,
18
18
  workflows = null,
19
19
  embeddingTimeoutMs = 5000,
20
- fileEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_FILE_EMBEDDING_TIMEOUT_MS || 1000)
20
+ fileEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_FILE_EMBEDDING_TIMEOUT_MS || 1000),
21
+ skillEmbeddingTimeoutMs = Number(process.env.CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS || embeddingTimeoutMs),
22
+ skillSearchOptions = {}
21
23
  } = {}) {
22
24
  const started = Date.now();
23
25
  const ruleInputsPromise = Promise.resolve().then(() => {
@@ -59,7 +61,15 @@ export async function scoreContext({
59
61
  const catalog = Array.isArray(skills) ? skills : scanSkills({ cwd });
60
62
  return {
61
63
  catalog,
62
- suggestions: await suggestSkills({ cwd, prompt, skills: catalog, dataDir, limit: maxSkills })
64
+ suggestions: await suggestSkills({
65
+ cwd,
66
+ prompt,
67
+ skills: catalog,
68
+ dataDir,
69
+ limit: maxSkills,
70
+ timeoutMs: skillEmbeddingTimeoutMs,
71
+ ...skillSearchOptions
72
+ })
63
73
  };
64
74
  });
65
75
 
@@ -37,7 +37,8 @@ export function setupSummaryLines({
37
37
  agents = DEFAULT_AGENTS,
38
38
  syncRules = true,
39
39
  syncSkills = true,
40
- promptSections = null
40
+ promptSections = null,
41
+ promptLimits = null
41
42
  } = {}) {
42
43
  const lines = [
43
44
  `Installation directory: ${cwd}`,
@@ -47,5 +48,6 @@ export function setupSummaryLines({
47
48
  `skillshare skill sync: ${syncSkills ? "enabled" : "skipped"}`
48
49
  ];
49
50
  if (promptSections !== null) lines.push(`Prompt sections shown: ${promptSections}`);
51
+ if (promptLimits !== null) lines.push(`Prompt suggest limits: ${promptLimits}`);
50
52
  return lines;
51
53
  }