@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.
- package/CHANGELOG.md +26 -0
- package/README.md +14 -6
- package/bin/ctx.js +72 -30
- package/package.json +1 -1
- package/plugins/ctx/.codex-plugin/plugin.json +1 -1
- package/plugins/ctx/lib/embedding-scorer.js +64 -25
- package/plugins/ctx/lib/graph-strategy.js +1 -0
- package/plugins/ctx/lib/output-config.js +37 -2
- package/plugins/ctx/lib/project-profiler.js +207 -0
- package/plugins/ctx/lib/prompt-hook.js +14 -16
- package/plugins/ctx/lib/scheduler.js +2 -1
- package/plugins/ctx/lib/score-context.js +12 -2
- package/plugins/ctx/lib/setup-wizard.js +3 -1
- package/plugins/ctx/lib/skill-discoverer.js +97 -313
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.49
|
|
4
|
+
|
|
5
|
+
- **Cold-cache MCP smoke:** `npm run test:mcp` now verifies the MCP tool contract without requiring a pre-downloaded ContextOS embedding model. When the model exists it still runs the semantic/performance smoke; otherwise it asserts cold-cache fallback behavior so publish CI does not fail before model warmup.
|
|
6
|
+
|
|
7
|
+
## 0.5.48
|
|
8
|
+
|
|
9
|
+
- **Publish-safe embedding warm:** `ctx embeddings warm` now skips remote model downloads by default in CI and returns best-effort warm results when local embedding model loading fails, preventing npm publish workflows from failing on Hugging Face 429/rate-limit errors.
|
|
10
|
+
- **CI graph embedding skip:** Code-review-graph embedding refresh is skipped when remote embeddings are disabled, avoiding extra Hugging Face calls during publish jobs.
|
|
11
|
+
|
|
12
|
+
## 0.5.47
|
|
13
|
+
|
|
14
|
+
- **Semantic-only skill discovery:** Replaced taxonomy/keyword skill ranking with fused semantic retrieval. ContextOS now embeds `prompt + project profile` and compares it against cached skill vectors, so mixed Vietnamese/English prompts can use package, language, and recent-file signals without hard-coded categories.
|
|
15
|
+
- **Project profile cache:** Added a bounded project profiler that builds an embeddable string from root/workspace `package.json` dependencies, scripts, detected languages, and recent git files. The cache invalidates when package metadata or git `HEAD` changes.
|
|
16
|
+
- **Skill dedupe and activation format:** Skill catalogs are deduplicated by normalized skill name before indexing/searching, preferring project and Codex roots. Prompt output now renders skill activations as `$skill-name`.
|
|
17
|
+
- **Refresh rebuilds skill index:** `ctx install` and `ctx refresh` now rebuild the skill embedding index by default because prompt-time skill discovery no longer falls back to taxonomy or keyword ranking for large catalogs.
|
|
18
|
+
- **Skill hook timeout split:** Prompt hooks now use a separate skill embedding timeout, so large indexed skill catalogs can still produce suggestions after direct fallback without slowing rule scoring.
|
|
19
|
+
- **Read-only embedding cache close:** Prompt-time embedding reads no longer rewrite the whole `embeddings.db` when no cache entries changed.
|
|
20
|
+
|
|
21
|
+
## 0.5.46
|
|
22
|
+
|
|
23
|
+
- **Configurable prompt suggestion limits:** `ctx --config` and interactive `ctx setup` now let users choose how many suggested files, skills, and workflows appear in prompt context. Defaults are five each, with caps of 20 files, 10 skills, and 5 workflows.
|
|
24
|
+
- **Limit-aware prompt hooks and debug:** `UserPromptSubmit` hooks, direct fallback scoring, the private `ctx-mcp` bridge request, and `ctx debug` now all honor the saved suggestion limits instead of using hard-coded counts.
|
|
25
|
+
- **Document authoring skill intent:** Prompts that create, edit, update, or maintain documents, workspace docs, README files, wiki pages, manuals, guides, specs, or ADRs now prioritize documentation skills such as `doc-coauthoring`, `documentation`, `docs-architect`, `readme`, and `wiki-page-writer`.
|
|
26
|
+
- **Safer document skill gating:** Document-processing and workspace-automation skills such as Azure Document Intelligence, DocuSign, Asana, Slack, Google Docs, and Notion no longer win generic document-writing prompts unless the provider or processing task is explicitly named.
|
|
27
|
+
- **Setup summary clarity:** The setup wizard summary now reports the saved prompt suggestion limits alongside the enabled prompt sections so users can review output volume immediately.
|
|
28
|
+
|
|
3
29
|
## 0.5.45
|
|
4
30
|
|
|
5
31
|
- **Project-aware MCP skill suggestions:** Skill ranking now reads `package.json` keywords and dependencies such as `@modelcontextprotocol/sdk`. MCP projects can recommend `mcp-builder`, `mcp-management`, `mcp-tool-developer`, and `agent-memory-mcp` for context retrieval, scorer, hook, and prompt-injection debugging tasks even when the prompt does not explicitly say `mcp`.
|
package/README.md
CHANGED
|
@@ -444,8 +444,8 @@ This warning comes from a transitive dependency in the local embedding/WASM stac
|
|
|
444
444
|
| `ctx sync --workflows --dry-run` | Previews workflow sync without writing files. | You want to inspect source workflows and target roots first. | Prints planned sync/index output and skips copying target files. |
|
|
445
445
|
| `ctx skills` | Installs community skill libraries. | You want curated skills without running the full setup wizard. | Opens the community installer, uses a portable shell on Windows/Linux/macOS, repairs unsafe skill symlinks, and syncs installed skills to selected agents. |
|
|
446
446
|
| `ctx embeddings warm -- "task"` | Prepares local semantic embedding caches. | First install, CI smoke checks, or after changing AGENTS.md/project files/skills/workflows. | Loads/downloads `Xenova/all-MiniLM-L6-v2` and writes rule, file-path, skill, and workflow vectors to `~/.ctx/contextos/embeddings.db`. |
|
|
447
|
-
| `ctx --config` | Opens an interactive
|
|
448
|
-
| `ctx refresh` | Refreshes the active Codex marketplace plugin and rebuilds local indexes. | Local development updates or
|
|
447
|
+
| `ctx --config` | Opens an interactive panel for prompt sections and suggestion limits. | You want to reduce ContextOS prompt output noise. | Toggles critical rules, suggested files, suggested skills, and suggested workflows globally under `~/.ctx/contextos/output-config.json`, then lets you set suggestion counts for files, skills, and workflows. |
|
|
448
|
+
| `ctx refresh` | Refreshes the active Codex marketplace plugin and rebuilds local indexes. | Local development updates or stale file/skill retrieval indexes. | Copies the current package to `$CODEX_HOME/marketplaces/contextos`, rebuilds file-path embeddings, skill embeddings, import adjacency, and refreshes code-review-graph embeddings when available. |
|
|
449
449
|
| `ctx ruler -- <args>` | Forwards args to the installed `ruler` CLI. | You need native Ruler commands such as `init`, `apply`, or `revert`. | Preserves Ruler stdout/stderr and exit status. |
|
|
450
450
|
| `ctx skillshare -- <args>` | Forwards args to the installed `skillshare` CLI. | You need native skillshare commands such as `status`, `target list`, `doctor`, `push`, or `pull`. | Preserves skillshare stdout/stderr and exit status. |
|
|
451
451
|
| `ctx --version` | Prints the installed ContextOS CLI version. | You want to confirm which npm version is being executed. | Prints the version from package metadata. |
|
|
@@ -511,17 +511,23 @@ This keeps the hook fast and local while still using graph semantics when availa
|
|
|
511
511
|
|
|
512
512
|
Prompt scoring does not walk the repository for file candidates or import expansion. `ctx install` and `ctx embeddings warm` rebuild the persisted file-vector index and one-hop import adjacency index by walking source paths once; prompt hooks query those indexes directly. Rules, files, skills, and workflows are scored concurrently with `Promise.all()`.
|
|
513
513
|
|
|
514
|
-
`ctx embeddings warm` automatically refreshes the active Codex marketplace payload before rebuilding indexes. Use `ctx refresh` when you want the same marketplace sync plus install-style file
|
|
514
|
+
`ctx embeddings warm` automatically refreshes the active Codex marketplace payload before rebuilding indexes. Use `ctx refresh` when you want the same marketplace sync plus install-style file, skill, import, and code-review-graph embedding refresh in one command.
|
|
515
515
|
|
|
516
516
|
If a prompt has no usable context candidates, the hook fails open without emitting an empty `hook context` block, records `emptyContextReason` in the workspace runtime file, and starts a detached `autowarm` rebuild with a cooldown. That background rebuild refreshes file vectors, skill/workflow vectors, import adjacency, and available code-review-graph node embeddings for the next prompt while keeping repository walking out of the current prompt hot path.
|
|
517
517
|
|
|
518
|
-
Use `ctx --config` to choose which prompt sections ContextOS injects. Interactive `ctx setup`
|
|
518
|
+
Use `ctx --config` to choose which prompt sections ContextOS injects and how many suggestions each section may show. Interactive `ctx setup` includes the same section picker and limit prompts, while `ctx setup --yes` keeps the current saved config for automation. The panel supports multiple selection with `Space` and persists the global choice in `~/.ctx/contextos/output-config.json`. Defaults are five suggested files, five skills, and five workflows; caps are 20 files, 10 skills, and 5 workflows. Disabling rules hides both critical and additional relevant rule sections; compliance metadata remains available for reports.
|
|
519
519
|
|
|
520
|
-
Injected prompt sections are intentionally compact: rules show only detected rule text, files show basenames without paths, skills show unique
|
|
520
|
+
Injected prompt sections are intentionally compact: rules show only detected rule text, files show a comma-separated inline list of basenames without paths, skills show unique `$skill-name` activations as a comma-separated inline list without descriptions, and workflows show names with their agent chain. Stop hooks persist reports silently; run `ctx report` or `ctx evidence` when you want the detailed compliance output.
|
|
521
521
|
|
|
522
522
|
Codex may flatten newlines in its `UserPromptSubmit hook (completed)` preview. The injected `additionalContext` payload remains multiline; this is a Codex preview display limitation.
|
|
523
523
|
|
|
524
|
-
Skill ranking
|
|
524
|
+
Skill ranking is semantic-only. ContextOS builds a fused query from the user prompt plus a cached project profile, then compares that vector with cached skill vectors:
|
|
525
|
+
|
|
526
|
+
```text
|
|
527
|
+
embed(prompt + project profile) -> cosine -> embed(skill name + description)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
The project profile is an embeddable string built from bounded root/workspace `package.json` metadata, dependencies, scripts, detected languages, and recent git files. It is cached under the ContextOS workspace data directory and invalidated when package metadata or git `HEAD` changes. ContextOS does not maintain a skill taxonomy or domain gate list for ranking; if the skill index is cold for a large catalog, prompt hooks fail open instead of falling back to arbitrary keyword matches. Skill catalogs are deduplicated by normalized skill name before indexing and rendering.
|
|
525
531
|
|
|
526
532
|
After `ctx refresh`, ContextOS invalidates the private hook bridge socket so prompts fall back to direct scoring until Codex restarts the long-running `ctx-mcp` process. Hook clients also discard a same-inode socket if an older bridge revision is detected.
|
|
527
533
|
|
|
@@ -538,6 +544,8 @@ CONTEXTOS_HOOK_DEADLINE_MS=8500 hard fail-open deadline for prompt hooks
|
|
|
538
544
|
CONTEXTOS_DIRECT_FALLBACK_TIMEOUT_MS=6000 direct scoring timeout when the bridge is unavailable
|
|
539
545
|
CONTEXTOS_HOOK_EMBEDDING_TIMEOUT_MS=500 rule embedding timeout during hook direct fallback
|
|
540
546
|
CONTEXTOS_EMBEDDING_TIMEOUT_MS=800 embedding scoring timeout inside ctx-mcp/debug
|
|
547
|
+
CONTEXTOS_HOOK_SKILL_EMBEDDING_TIMEOUT_MS=2000 skill retrieval timeout during hook direct fallback
|
|
548
|
+
CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS=2000 skill retrieval timeout inside ctx-mcp/debug
|
|
541
549
|
CONTEXTOS_FILE_EMBEDDINGS=0 disable file-path embedding retrieval
|
|
542
550
|
CONTEXTOS_HOOK_FILE_EMBEDDING_TIMEOUT_MS=500 file retrieval timeout during hook direct fallback
|
|
543
551
|
CONTEXTOS_FILE_EMBEDDING_TIMEOUT_MS=1000 file-path embedding retrieval timeout
|
package/bin/ctx.js
CHANGED
|
@@ -34,7 +34,7 @@ import { scanSkills, warmSkillEmbeddings } from "../plugins/ctx/lib/skill-discov
|
|
|
34
34
|
import { parsePassthroughArgs, runPassthrough } from "../plugins/ctx/lib/passthrough.js";
|
|
35
35
|
import { parseAgentList, parseSetupArgs, setupSummaryLines } from "../plugins/ctx/lib/setup-wizard.js";
|
|
36
36
|
import { multiSelect } from "../plugins/ctx/lib/multi-select.js";
|
|
37
|
-
import { configureOutputSections, enabledOutputSectionsLabel, loadOutputConfig } from "../plugins/ctx/lib/output-config.js";
|
|
37
|
+
import { configureOutputSections, enabledOutputSectionsLabel, loadOutputConfig, outputConfigLimits, outputConfigLimitsLabel } from "../plugins/ctx/lib/output-config.js";
|
|
38
38
|
import { syncWorkflows, warmWorkflowEmbeddings } from "../plugins/ctx/lib/workflow-discoverer.js";
|
|
39
39
|
import { checkForUpdate } from "../plugins/ctx/lib/update-notifier.js";
|
|
40
40
|
import { fetchSkillsForAgents, printSkillRecommendations, getAllLibraries, getInstallCommands } from "../plugins/ctx/lib/skill-library.js";
|
|
@@ -487,6 +487,7 @@ function graphStrategyForInstall() {
|
|
|
487
487
|
async function warmInstallEmbeddings() {
|
|
488
488
|
const dataDir = contextOSDataDir();
|
|
489
489
|
const modelReady = isModelCacheReady(dataDir);
|
|
490
|
+
const allowRemote = shouldAllowRemoteWarm(modelReady);
|
|
490
491
|
const result = await warmRuleEmbeddings({
|
|
491
492
|
rules: [
|
|
492
493
|
{ content: "Always use project rules that are semantically relevant to the user prompt." },
|
|
@@ -496,29 +497,29 @@ async function warmInstallEmbeddings() {
|
|
|
496
497
|
task: "kiểm duyệt upload moderation semantic code search",
|
|
497
498
|
dataDir,
|
|
498
499
|
sources: [],
|
|
499
|
-
allowRemote
|
|
500
|
+
allowRemote
|
|
500
501
|
});
|
|
501
502
|
const fileResult = await warmFileEmbeddings({
|
|
502
503
|
cwd: process.cwd(),
|
|
503
504
|
dataDir,
|
|
504
|
-
allowRemote
|
|
505
|
+
allowRemote
|
|
506
|
+
});
|
|
507
|
+
const skillResult = await warmSkillEmbeddings({
|
|
508
|
+
cwd: process.cwd(),
|
|
509
|
+
dataDir,
|
|
510
|
+
allowRemote
|
|
505
511
|
});
|
|
506
512
|
const warmDiscovery = process.env.CONTEXTOS_INSTALL_WARM_DISCOVERY === "1";
|
|
507
|
-
const skillResult = warmDiscovery
|
|
508
|
-
? await warmSkillEmbeddings({
|
|
509
|
-
cwd: process.cwd(),
|
|
510
|
-
dataDir,
|
|
511
|
-
allowRemote: !modelReady
|
|
512
|
-
})
|
|
513
|
-
: { count: 0 };
|
|
514
513
|
const workflowResult = warmDiscovery
|
|
515
514
|
? await warmWorkflowEmbeddings({
|
|
516
515
|
cwd: process.cwd(),
|
|
517
516
|
dataDir,
|
|
518
|
-
allowRemote
|
|
517
|
+
allowRemote
|
|
519
518
|
})
|
|
520
519
|
: { count: 0 };
|
|
521
|
-
const graphEmbedding =
|
|
520
|
+
const graphEmbedding = allowRemote
|
|
521
|
+
? embedCodeReviewGraph({ cwd: process.cwd() })
|
|
522
|
+
: { status: "skipped", reason: "remote-embedding-disabled" };
|
|
522
523
|
return { ...result, modelAlreadyCached: modelReady, fileCount: fileResult.count, skillCount: skillResult.count, workflowCount: workflowResult.count, graphEmbedding };
|
|
523
524
|
}
|
|
524
525
|
|
|
@@ -586,18 +587,20 @@ function contextOSWorkspaceDataDir(cwd = process.cwd()) {
|
|
|
586
587
|
|
|
587
588
|
async function debug(task) {
|
|
588
589
|
const cwd = process.cwd();
|
|
590
|
+
const limits = outputConfigLimits(loadOutputConfig({ dataRoot: contextOSDataDir() }));
|
|
589
591
|
const scored = await scoreContext({
|
|
590
592
|
cwd,
|
|
591
593
|
prompt: task,
|
|
592
594
|
dataDir: contextOSDataDir(),
|
|
593
|
-
maxFiles:
|
|
594
|
-
maxSkills:
|
|
595
|
+
maxFiles: limits.files,
|
|
596
|
+
maxSkills: limits.skills,
|
|
597
|
+
maxWorkflows: limits.workflows,
|
|
595
598
|
embeddingTimeoutMs: Number(process.env.CONTEXTOS_EMBEDDING_DEBUG_TIMEOUT_MS || 5000)
|
|
596
599
|
});
|
|
597
600
|
const rules = scored.scoredRules;
|
|
598
|
-
const relevantFiles = scored.suggestedFiles.slice(0,
|
|
599
|
-
const suggestedSkills = (scored.suggestedSkills || []).slice(0,
|
|
600
|
-
const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0,
|
|
601
|
+
const relevantFiles = scored.suggestedFiles.slice(0, limits.files);
|
|
602
|
+
const suggestedSkills = (scored.suggestedSkills || []).slice(0, limits.skills);
|
|
603
|
+
const suggestedWorkflows = (scored.suggestedWorkflows || []).slice(0, limits.workflows);
|
|
601
604
|
const scheduled = scheduleContext({ rules, relevantFiles, suggestedSkills, suggestedWorkflows });
|
|
602
605
|
|
|
603
606
|
console.log("ContextOS debug");
|
|
@@ -661,31 +664,36 @@ async function warmEmbeddings(task, { syncMarketplace = true, quiet = false } =
|
|
|
661
664
|
|
|
662
665
|
async function warmWorkspaceIndexes({ task = "project context" } = {}) {
|
|
663
666
|
const cwd = process.cwd();
|
|
667
|
+
const dataDir = contextOSDataDir();
|
|
668
|
+
const modelReady = isModelCacheReady(dataDir);
|
|
669
|
+
const allowRemote = shouldAllowRemoteWarm(modelReady);
|
|
664
670
|
const merged = readAgentsChain({ cwd });
|
|
665
671
|
const rules = scoreRules(filterActionableRules(parseRules(merged.content)), task, []);
|
|
666
672
|
const result = await warmRuleEmbeddings({
|
|
667
673
|
rules,
|
|
668
674
|
task,
|
|
669
|
-
dataDir
|
|
675
|
+
dataDir,
|
|
670
676
|
sources: merged.sources,
|
|
671
|
-
allowRemote
|
|
677
|
+
allowRemote
|
|
672
678
|
});
|
|
673
679
|
const fileResult = await warmFileEmbeddings({
|
|
674
680
|
cwd,
|
|
675
|
-
dataDir
|
|
676
|
-
allowRemote
|
|
681
|
+
dataDir,
|
|
682
|
+
allowRemote
|
|
677
683
|
});
|
|
678
684
|
const skillResult = await warmSkillEmbeddings({
|
|
679
685
|
cwd,
|
|
680
|
-
dataDir
|
|
681
|
-
allowRemote
|
|
686
|
+
dataDir,
|
|
687
|
+
allowRemote
|
|
682
688
|
});
|
|
683
689
|
const workflowResult = await warmWorkflowEmbeddings({
|
|
684
690
|
cwd,
|
|
685
|
-
dataDir
|
|
686
|
-
allowRemote
|
|
691
|
+
dataDir,
|
|
692
|
+
allowRemote
|
|
687
693
|
});
|
|
688
|
-
const graphEmbedding =
|
|
694
|
+
const graphEmbedding = allowRemote
|
|
695
|
+
? embedCodeReviewGraph({ cwd })
|
|
696
|
+
: { status: "skipped", reason: "remote-embedding-disabled" };
|
|
689
697
|
return {
|
|
690
698
|
ruleCount: result.count,
|
|
691
699
|
fileCount: fileResult.count,
|
|
@@ -696,12 +704,28 @@ async function warmWorkspaceIndexes({ task = "project context" } = {}) {
|
|
|
696
704
|
};
|
|
697
705
|
}
|
|
698
706
|
|
|
707
|
+
function shouldAllowRemoteWarm(modelReady) {
|
|
708
|
+
if (modelReady) return false;
|
|
709
|
+
if (process.env.CONTEXTOS_EMBEDDING_ALLOW_REMOTE !== undefined) {
|
|
710
|
+
return process.env.CONTEXTOS_EMBEDDING_ALLOW_REMOTE === "1";
|
|
711
|
+
}
|
|
712
|
+
return !isCiEnvironment();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function isCiEnvironment() {
|
|
716
|
+
return process.env.CI === "true"
|
|
717
|
+
|| process.env.GITHUB_ACTIONS === "true"
|
|
718
|
+
|| process.env.CONTINUOUS_INTEGRATION === "true"
|
|
719
|
+
|| process.env.BUILD_ID !== undefined
|
|
720
|
+
|| process.env.RUN_ID !== undefined;
|
|
721
|
+
}
|
|
722
|
+
|
|
699
723
|
async function refresh() {
|
|
700
724
|
const marketplaceSync = syncActiveCodexMarketplace();
|
|
701
725
|
const invalidatedBridge = invalidateCtxMcpSocket(contextOSDataDir());
|
|
702
726
|
const warmResult = await warmInstallEmbeddings();
|
|
703
727
|
console.log(`Marketplace: ${marketplaceSync.synced ? "synced" : "already active"} (${marketplaceSync.targetRoot})`);
|
|
704
|
-
console.log(`Indexes: ${warmResult.fileCount || 0} file paths rebuilt`);
|
|
728
|
+
console.log(`Indexes: ${warmResult.fileCount || 0} file paths rebuilt, ${warmResult.skillCount || 0} skills indexed`);
|
|
705
729
|
console.log(`Graph embeddings: ${formatCodeReviewGraphEmbedding(warmResult.graphEmbedding)}`);
|
|
706
730
|
if (invalidatedBridge) console.log("Bridge: stale private socket invalidated");
|
|
707
731
|
console.log("Restart Codex if ctx-mcp was already running.");
|
|
@@ -728,6 +752,21 @@ async function askSetupYesNo(rl, question, defaultValue = true) {
|
|
|
728
752
|
return !/^n(o)?$/i.test(answer.trim());
|
|
729
753
|
}
|
|
730
754
|
|
|
755
|
+
async function askOutputLimit({ option, currentValue }) {
|
|
756
|
+
if (!process.stdin.isTTY) return currentValue;
|
|
757
|
+
const rl = readline.createInterface({ input, output });
|
|
758
|
+
try {
|
|
759
|
+
const answer = await rl.question(`◇ ${option.label} limit (0-${option.max}, current ${currentValue}): `);
|
|
760
|
+
const trimmed = answer.trim();
|
|
761
|
+
if (!trimmed) return currentValue;
|
|
762
|
+
const value = Number(trimmed);
|
|
763
|
+
if (!Number.isFinite(value)) return currentValue;
|
|
764
|
+
return Math.max(0, Math.min(option.max, Math.trunc(value)));
|
|
765
|
+
} finally {
|
|
766
|
+
rl.close();
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
731
770
|
async function setup({ args = [], cwd = process.cwd() } = {}) {
|
|
732
771
|
const options = parseSetupArgs(args);
|
|
733
772
|
const interactive = !options.yes && process.stdin.isTTY;
|
|
@@ -777,7 +816,8 @@ async function setup({ args = [], cwd = process.cwd() } = {}) {
|
|
|
777
816
|
console.log("◇ Configure prompt output:");
|
|
778
817
|
outputConfig = await configureOutputSections({
|
|
779
818
|
dataRoot: contextOSDataDir(),
|
|
780
|
-
select: multiSelect
|
|
819
|
+
select: multiSelect,
|
|
820
|
+
askLimit: askOutputLimit
|
|
781
821
|
});
|
|
782
822
|
}
|
|
783
823
|
|
|
@@ -786,7 +826,8 @@ async function setup({ args = [], cwd = process.cwd() } = {}) {
|
|
|
786
826
|
for (const line of setupSummaryLines({
|
|
787
827
|
cwd,
|
|
788
828
|
...options,
|
|
789
|
-
promptSections: enabledOutputSectionsLabel(outputConfig)
|
|
829
|
+
promptSections: enabledOutputSectionsLabel(outputConfig),
|
|
830
|
+
promptLimits: outputConfigLimitsLabel(outputConfig)
|
|
790
831
|
})) console.log(`│ ${line}`);
|
|
791
832
|
console.log("");
|
|
792
833
|
|
|
@@ -871,7 +912,8 @@ try {
|
|
|
871
912
|
} else if (command === "--config" || command === "config") {
|
|
872
913
|
await configureOutputSections({
|
|
873
914
|
dataRoot: contextOSDataDir(),
|
|
874
|
-
select: multiSelect
|
|
915
|
+
select: multiSelect,
|
|
916
|
+
askLimit: askOutputLimit
|
|
875
917
|
});
|
|
876
918
|
} else if (command === "install") {
|
|
877
919
|
const copy = args.includes("--copy");
|
package/package.json
CHANGED
|
@@ -65,13 +65,28 @@ export async function warmRuleEmbeddings({
|
|
|
65
65
|
...rules.map((rule) => rule.content || "")
|
|
66
66
|
].filter((text) => String(text).trim()))];
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
await
|
|
68
|
+
let cache;
|
|
69
|
+
try {
|
|
70
|
+
cache = await openEmbeddingCache(dataDir);
|
|
71
|
+
const embedder = await getExtractor({ allowRemote, dataDir });
|
|
72
|
+
for (const text of texts) {
|
|
73
|
+
await getCachedEmbedding({ cache, embedder, text, sources, flush: false });
|
|
74
|
+
}
|
|
75
|
+
cache.close();
|
|
76
|
+
return { count: texts.length, cachePath: cache.path, status: "enabled" };
|
|
77
|
+
} catch (error) {
|
|
78
|
+
try {
|
|
79
|
+
cache?.close();
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore close failures while reporting a best-effort warm result.
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
count: 0,
|
|
85
|
+
cachePath: path.join(dataDir, "embeddings.db"),
|
|
86
|
+
status: "warm-failed",
|
|
87
|
+
error: error?.message || String(error)
|
|
88
|
+
};
|
|
72
89
|
}
|
|
73
|
-
cache.close();
|
|
74
|
-
return { count: texts.length, cachePath: cache.path };
|
|
75
90
|
}
|
|
76
91
|
|
|
77
92
|
export async function searchIndexedEmbeddings({
|
|
@@ -106,20 +121,35 @@ export async function warmIndexedEmbeddings({
|
|
|
106
121
|
return { count: 0, cachePath: path.join(dataDir, "embeddings.db"), status: "missing-model" };
|
|
107
122
|
}
|
|
108
123
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
124
|
+
let cache;
|
|
125
|
+
try {
|
|
126
|
+
cache = await openEmbeddingCache(dataDir);
|
|
127
|
+
const embedder = await getExtractor({ allowRemote, dataDir });
|
|
128
|
+
if (String(task || "").trim()) await getCachedEmbedding({ cache, embedder, text: task, sources });
|
|
129
|
+
|
|
130
|
+
const indexed = [];
|
|
131
|
+
for (const item of items) {
|
|
132
|
+
const text = String(item.text || "");
|
|
133
|
+
if (!item.id || !text.trim()) continue;
|
|
134
|
+
const vector = await getCachedEmbedding({ cache, embedder, text, sources, flush: false });
|
|
135
|
+
indexed.push({ id: item.id, text, vector });
|
|
136
|
+
}
|
|
137
|
+
cache.replaceIndex(kind, indexed);
|
|
138
|
+
cache.close();
|
|
139
|
+
return { count: indexed.length, cachePath: cache.path, status: "enabled" };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
try {
|
|
142
|
+
cache?.close();
|
|
143
|
+
} catch {
|
|
144
|
+
// Ignore close failures while reporting a best-effort warm result.
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
count: 0,
|
|
148
|
+
cachePath: path.join(dataDir, "embeddings.db"),
|
|
149
|
+
status: "warm-failed",
|
|
150
|
+
error: error?.message || String(error)
|
|
151
|
+
};
|
|
119
152
|
}
|
|
120
|
-
cache.replaceIndex(kind, indexed);
|
|
121
|
-
cache.close();
|
|
122
|
-
return { count: indexed.length, cachePath: cache.path };
|
|
123
153
|
}
|
|
124
154
|
|
|
125
155
|
async function enhanceRuleScores(rules, task, { dataDir, sources, allowRemote }) {
|
|
@@ -187,7 +217,10 @@ async function getExtractor({ allowRemote, dataDir }) {
|
|
|
187
217
|
return transformers.pipeline("feature-extraction", DEFAULT_MODEL, {
|
|
188
218
|
quantized: true
|
|
189
219
|
});
|
|
190
|
-
})())
|
|
220
|
+
})().catch((error) => {
|
|
221
|
+
extractorPromises.delete(key);
|
|
222
|
+
throw error;
|
|
223
|
+
}));
|
|
191
224
|
}
|
|
192
225
|
return extractorPromises.get(key);
|
|
193
226
|
}
|
|
@@ -206,7 +239,7 @@ export function isModelCacheReady(dataDir = defaultDataRoot()) {
|
|
|
206
239
|
].every((relativePath) => fs.existsSync(path.join(modelDir, relativePath)));
|
|
207
240
|
}
|
|
208
241
|
|
|
209
|
-
async function getCachedEmbedding({ cache, embedder, text, sources }) {
|
|
242
|
+
async function getCachedEmbedding({ cache, embedder, text, sources, flush = true }) {
|
|
210
243
|
const key = cacheKey(text, sources);
|
|
211
244
|
const existing = cache.get(key);
|
|
212
245
|
if (existing) return existing;
|
|
@@ -216,7 +249,7 @@ async function getCachedEmbedding({ cache, embedder, text, sources }) {
|
|
|
216
249
|
normalize: true
|
|
217
250
|
});
|
|
218
251
|
const embedding = Array.from(output.data || []);
|
|
219
|
-
cache.set(key, embedding);
|
|
252
|
+
cache.set(key, embedding, { flush });
|
|
220
253
|
return embedding;
|
|
221
254
|
}
|
|
222
255
|
|
|
@@ -225,6 +258,7 @@ export async function openEmbeddingCache(dataDir) {
|
|
|
225
258
|
const cachePath = path.join(dataDir, "embeddings.db");
|
|
226
259
|
const SQL = await getSql();
|
|
227
260
|
const db = initializeEmbeddingDatabase(SQL, cachePath);
|
|
261
|
+
let dirty = false;
|
|
228
262
|
|
|
229
263
|
return {
|
|
230
264
|
path: cachePath,
|
|
@@ -238,12 +272,16 @@ export async function openEmbeddingCache(dataDir) {
|
|
|
238
272
|
stmt.free();
|
|
239
273
|
}
|
|
240
274
|
},
|
|
241
|
-
set(key, vector) {
|
|
275
|
+
set(key, vector, { flush = true } = {}) {
|
|
242
276
|
db.run(
|
|
243
277
|
"INSERT OR REPLACE INTO embeddings (key, model, vector, updated_at) VALUES (?, ?, ?, ?)",
|
|
244
278
|
[key, DEFAULT_MODEL, JSON.stringify(vector), new Date().toISOString()]
|
|
245
279
|
);
|
|
246
|
-
|
|
280
|
+
dirty = true;
|
|
281
|
+
if (flush) {
|
|
282
|
+
writeDatabaseAtomically(cachePath, db);
|
|
283
|
+
dirty = false;
|
|
284
|
+
}
|
|
247
285
|
},
|
|
248
286
|
listIndexed(kind) {
|
|
249
287
|
const stmt = db.prepare("SELECT id, text, vector FROM embedding_index WHERE kind = ? AND model = ?");
|
|
@@ -268,9 +306,10 @@ export async function openEmbeddingCache(dataDir) {
|
|
|
268
306
|
);
|
|
269
307
|
}
|
|
270
308
|
writeDatabaseAtomically(cachePath, db);
|
|
309
|
+
dirty = false;
|
|
271
310
|
},
|
|
272
311
|
close() {
|
|
273
|
-
writeDatabaseAtomically(cachePath, db);
|
|
312
|
+
if (dirty) writeDatabaseAtomically(cachePath, db);
|
|
274
313
|
db.close();
|
|
275
314
|
}
|
|
276
315
|
};
|
|
@@ -91,6 +91,7 @@ export function formatCodeReviewGraphEmbedding(result) {
|
|
|
91
91
|
}
|
|
92
92
|
if (result.reason === "missing-graph-index") return "skipped (no .code-review-graph/graph.db)";
|
|
93
93
|
if (result.reason === "missing-code-review-graph-python") return "skipped (code-review-graph Python unavailable)";
|
|
94
|
+
if (result.reason === "remote-embedding-disabled") return "skipped (remote embedding disabled)";
|
|
94
95
|
return `skipped (${result.error || result.reason || "unavailable"})`;
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -13,9 +13,16 @@ export const OUTPUT_SECTION_OPTIONS = [
|
|
|
13
13
|
{ value: "workflows", label: "Suggested workflow for this task", hint: "Include matching workflow recommendations." }
|
|
14
14
|
];
|
|
15
15
|
|
|
16
|
+
export const OUTPUT_LIMIT_OPTIONS = [
|
|
17
|
+
{ value: "files", label: "Suggested files", defaultValue: 5, max: 20 },
|
|
18
|
+
{ value: "skills", label: "Suggested skills", defaultValue: 5, max: 10 },
|
|
19
|
+
{ value: "workflows", label: "Suggested workflows", defaultValue: 5, max: 5 }
|
|
20
|
+
];
|
|
21
|
+
|
|
16
22
|
export function defaultOutputConfig() {
|
|
17
23
|
return {
|
|
18
|
-
sections: Object.fromEntries(OUTPUT_SECTION_OPTIONS.map((option) => [option.value, true]))
|
|
24
|
+
sections: Object.fromEntries(OUTPUT_SECTION_OPTIONS.map((option) => [option.value, true])),
|
|
25
|
+
limits: Object.fromEntries(OUTPUT_LIMIT_OPTIONS.map((option) => [option.value, option.defaultValue]))
|
|
19
26
|
};
|
|
20
27
|
}
|
|
21
28
|
|
|
@@ -49,9 +56,19 @@ export function enabledOutputSectionsLabel(config = loadOutputConfig()) {
|
|
|
49
56
|
return enabled.length ? enabled.join(", ") : "(none)";
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
export function outputConfigLimits(config = loadOutputConfig()) {
|
|
60
|
+
return normalizeOutputConfig(config).limits;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function outputConfigLimitsLabel(config = loadOutputConfig()) {
|
|
64
|
+
const limits = outputConfigLimits(config);
|
|
65
|
+
return OUTPUT_LIMIT_OPTIONS.map((option) => `${option.value}: ${limits[option.value]}`).join(", ");
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
export async function configureOutputSections({
|
|
53
69
|
dataRoot = defaultDataRoot(),
|
|
54
70
|
select,
|
|
71
|
+
askLimit,
|
|
55
72
|
logger = console.log
|
|
56
73
|
} = {}) {
|
|
57
74
|
if (typeof select !== "function") throw new Error("configureOutputSections requires a multi-select function");
|
|
@@ -64,11 +81,19 @@ export async function configureOutputSections({
|
|
|
64
81
|
}))
|
|
65
82
|
});
|
|
66
83
|
const selectedSet = new Set(selected);
|
|
84
|
+
const limits = {};
|
|
85
|
+
for (const option of OUTPUT_LIMIT_OPTIONS) {
|
|
86
|
+
limits[option.value] = typeof askLimit === "function"
|
|
87
|
+
? await askLimit({ option, currentValue: current.limits[option.value] })
|
|
88
|
+
: current.limits[option.value];
|
|
89
|
+
}
|
|
67
90
|
const saved = saveOutputConfig({
|
|
68
|
-
sections: Object.fromEntries(OUTPUT_SECTION_OPTIONS.map((option) => [option.value, selectedSet.has(option.value)]))
|
|
91
|
+
sections: Object.fromEntries(OUTPUT_SECTION_OPTIONS.map((option) => [option.value, selectedSet.has(option.value)])),
|
|
92
|
+
limits
|
|
69
93
|
}, { dataRoot });
|
|
70
94
|
logger(`│ Saved ContextOS prompt section config: ${outputConfigPath(dataRoot)}`);
|
|
71
95
|
logger(`│ Enabled sections: ${enabledOutputSectionsLabel(saved)}`);
|
|
96
|
+
logger(`│ Suggest limits: ${outputConfigLimitsLabel(saved)}`);
|
|
72
97
|
return saved;
|
|
73
98
|
}
|
|
74
99
|
|
|
@@ -80,6 +105,16 @@ function normalizeOutputConfig(config = {}) {
|
|
|
80
105
|
typeof config.sections?.[option.value] === "boolean"
|
|
81
106
|
? config.sections[option.value]
|
|
82
107
|
: defaults.sections[option.value]
|
|
108
|
+
])),
|
|
109
|
+
limits: Object.fromEntries(OUTPUT_LIMIT_OPTIONS.map((option) => [
|
|
110
|
+
option.value,
|
|
111
|
+
normalizeLimit(config.limits?.[option.value], option)
|
|
83
112
|
]))
|
|
84
113
|
};
|
|
85
114
|
}
|
|
115
|
+
|
|
116
|
+
function normalizeLimit(value, option) {
|
|
117
|
+
const number = Number(value);
|
|
118
|
+
if (!Number.isFinite(number)) return option.defaultValue;
|
|
119
|
+
return Math.max(0, Math.min(option.max, Math.trunc(number)));
|
|
120
|
+
}
|