sigmap 8.24.0 → 8.26.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/CHANGELOG.md +24 -0
- package/README.md +4 -4
- package/gen-context.js +494 -2
- package/llms-full.txt +7 -3
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/config/tune.js +210 -0
- package/src/mcp/server.js +1 -1
- package/src/skills/skills.js +182 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,30 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [8.26.0] — 2026-08-18
|
|
14
|
+
|
|
15
|
+
Minor release — **"Agent Economy III" (v8.26, F3+F4)**: the optimal SigMap usage loop ships as installable agent skills in every client's native format — completing the Agent Economy pillar (F1 budget → F2 tune → F3+F4 skills).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`sigmap skills` (#517, PR #518):** new `src/skills/skills.js` — two canonical, deterministic skill documents: **sigmap-usage-maximizer** (F3 — the spend-minimizing loop: `ask` before any file read · `get_lines` for anchored ranges instead of whole files · `verify_suggestion` before trusting generated code · `squeeze` any big log/trace/JSON · checkpoint via `create_checkpoint`/`note` · check `get_budget` and, near budget, summarize-then-drop; token accounting reads the F1 ledger — no LLM calls) and **sigmap-config-optimizer** (the F2 playbook: `tune` → review the evidence-naming reasons → `tune --apply` → `validate`). Multi-client installer mirroring the `mcp/install.js` CLIENTS pattern: Claude Code (`.claude/skills/<skill>/SKILL.md`), Cursor (`.cursor/rules/*.mdc`), Windsurf (`.windsurf/rules/*.md`), GitHub Copilot (`.github/instructions/*.instructions.md`), and Codex — a marker-delimited block injected into `AGENTS.md` **above** the `## Auto-generated signatures` marker, so the codex adapter's regeneration preserves it (proven by an end-to-end regeneration test). Human content is never touched; every install is idempotent (`installed`/`updated`/`already`). CLI: `sigmap skills list` and `sigmap skills install [--client <name> | --all] [--json]` — plain `install` wires only clients whose parent artifact exists (the `--setup` only-touch-existing precedent); `--client`/`--all` create. Zero new dependencies.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- 8 new integration tests (132 test files); bundle rebuilt (149 modules).
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## [8.25.0] — 2026-08-18
|
|
26
|
+
|
|
27
|
+
Minor release — **"Agent Economy II" (v8.25, F2)**: the discovery stack becomes a config optimizer — one command that recommends (and can apply) the config a repo actually needs.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- **`sigmap tune` (#514, PR #515):** new `src/config/tune.js` — `buildTuneProposal(cwd)` packages the existing discovery stack (`resolveSourceRoots`, workspace markers, client-artifact probes) into a deterministic recommended-config diff with one evidence-naming reason per change. Five rules, explicit user choices never overridden: **srcDirs pin** (resolver roots, confidence-gated, never proposed against user-pinned dirs — pinned srcDirs are stable across runs and protected from token-budget drops), **monorepo** (fires when a workspace marker exists and the mode is off; reason names the marker — `pnpm-workspace.yaml`/`turbo.json`/`nx.json`/`lerna.json`/package.json `workspaces`), **adapters** (additive from client artifacts present: `CLAUDE.md`→claude, `.cursorrules`/`.cursor/`→cursor, `.windsurfrules`→windsurf, `AGENTS.md`→codex), **exclude** (curated vendored/generated dirs present at root — `third_party`, `external(s)`, `generated`, `testdata`, `tmp`, `.cache` … — appended with defaults preserved), **autoMaxTokens** (fires only when a pinned budget is below a labeled ~25-tokens/file heuristic estimate). `applyTuneProposal` merges into `gen-context.config.json` preserving every user key; re-proposal after apply is empty (idempotent). CLI: `sigmap tune` is read-only by default (`--dry-run` alias), `--apply` writes and points to `sigmap validate`, `--json` for agents. Zero new dependencies.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- 10 new integration tests (131 test files); bundle rebuilt (148 modules).
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
13
37
|
## [8.24.0] — 2026-07-28
|
|
14
38
|
|
|
15
39
|
Minor release — **"Trust Quick Wins I" (v8.24, G3a)**: the secret-redaction engine that already protects signatures, `get_lines`, and evidence packs becomes a standalone command for arbitrary text.
|
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ That map is exactly what agentic grep is worst at: reproducible, auditable conte
|
|
|
59
59
|
|
|
60
60
|
**Proof it pays off** (full benchmark below):
|
|
61
61
|
<!--SM:whyMetrics-->
|
|
62
|
-
- **82.2% hit@5** — right file in top 5 results (vs 44.
|
|
62
|
+
- **82.2% hit@5** — right file in top 5 results (vs 44.0% single-shot grep baseline — 1.87× lift)
|
|
63
63
|
- **96.8% token reduction** — average across 21 real repos
|
|
64
64
|
- **64.8% task-success proxy** — modeled from retrieval tiers, not measured LLM sessions
|
|
65
65
|
- **1.53 prompts per task** — down from 2.84 (46.1% fewer retries, modeled)
|
|
@@ -122,10 +122,10 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
122
122
|
|
|
123
123
|
<!--SM:benchmarkBlock-->
|
|
124
124
|
```
|
|
125
|
-
Benchmark : sigmap-v8.
|
|
126
|
-
Date : 2026-
|
|
125
|
+
Benchmark : sigmap-v8.26-main (21 repositories, including R language)
|
|
126
|
+
Date : 2026-08-17
|
|
127
127
|
|
|
128
|
-
Hit@5 : 82.2% (grep-agent baseline 44.
|
|
128
|
+
Hit@5 : 82.2% (grep-agent baseline 44.0% — 1.87× lift)
|
|
129
129
|
Token reduction: 96.8% (across 21 repos)
|
|
130
130
|
Prompt reduction : 46.1% (2.84 → 1.53 prompts per task, modeled)
|
|
131
131
|
Task success : 64.8% (proxy — modeled from retrieval tiers)
|
package/gen-context.js
CHANGED
|
@@ -1877,6 +1877,220 @@ __factories["./src/config/loader"] = function(module, exports) {
|
|
|
1877
1877
|
|
|
1878
1878
|
};
|
|
1879
1879
|
|
|
1880
|
+
// ── ./src/config/tune ──
|
|
1881
|
+
__factories["./src/config/tune"] = function(module, exports) {
|
|
1882
|
+
|
|
1883
|
+
/**
|
|
1884
|
+
* sigmap tune — deterministic config optimizer (F2, #514).
|
|
1885
|
+
*
|
|
1886
|
+
* Packages the existing discovery stack (source-root-resolver, monorepo
|
|
1887
|
+
* markers, client-artifact probes) into a recommended config diff with a
|
|
1888
|
+
* one-line reason per change. Read-only by default; `applyTuneProposal`
|
|
1889
|
+
* merges accepted changes into gen-context.config.json, preserving every
|
|
1890
|
+
* user key. Explicit user choices are never proposed against.
|
|
1891
|
+
*/
|
|
1892
|
+
|
|
1893
|
+
const fs = require('fs');
|
|
1894
|
+
const path = require('path');
|
|
1895
|
+
const { loadConfig } = __require('./src/config/loader');
|
|
1896
|
+
const { resolveSourceRoots } = __require('./src/discovery/source-root-resolver');
|
|
1897
|
+
|
|
1898
|
+
// Workspace markers, in probe order (reason names the first one found).
|
|
1899
|
+
const MONOREPO_MARKERS = ['pnpm-workspace.yaml', 'turbo.json', 'nx.json', 'lerna.json'];
|
|
1900
|
+
|
|
1901
|
+
// Client artifacts → adapter names (additive only).
|
|
1902
|
+
const ADAPTER_MARKERS = [
|
|
1903
|
+
{ adapter: 'claude', files: ['CLAUDE.md'] },
|
|
1904
|
+
{ adapter: 'cursor', files: ['.cursorrules', '.cursor'] },
|
|
1905
|
+
{ adapter: 'windsurf', files: ['.windsurfrules', '.windsurf'] },
|
|
1906
|
+
{ adapter: 'codex', files: ['AGENTS.md'] },
|
|
1907
|
+
];
|
|
1908
|
+
|
|
1909
|
+
// Root-level dirs that are typically vendored/generated when present.
|
|
1910
|
+
const JUNK_DIRS = [
|
|
1911
|
+
'third_party', 'thirdparty', 'external', 'externals',
|
|
1912
|
+
'generated', 'testdata', 'snapshots', 'tmp', 'temp', '.cache',
|
|
1913
|
+
];
|
|
1914
|
+
|
|
1915
|
+
// Rough signature cost per source file (chars/4 world) for the budget check.
|
|
1916
|
+
const TOKENS_PER_FILE = 25;
|
|
1917
|
+
|
|
1918
|
+
const SOURCE_EXTS = new Set([
|
|
1919
|
+
'.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.py', '.rb', '.go', '.rs',
|
|
1920
|
+
'.java', '.kt', '.cs', '.cpp', '.c', '.h', '.hpp', '.swift', '.dart',
|
|
1921
|
+
'.scala', '.php', '.gd', '.r', '.R',
|
|
1922
|
+
]);
|
|
1923
|
+
|
|
1924
|
+
/** Raw user config file content, or null when absent/unparsable. */
|
|
1925
|
+
function _readUserConfig(cwd) {
|
|
1926
|
+
try {
|
|
1927
|
+
return JSON.parse(fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8'));
|
|
1928
|
+
} catch (_) {
|
|
1929
|
+
return null;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
/** Count source files under `roots` (relative to cwd), depth-capped, deterministic. */
|
|
1934
|
+
function _countSourceFiles(cwd, roots, exclude, depth = 5) {
|
|
1935
|
+
const excSet = new Set(exclude || []);
|
|
1936
|
+
let count = 0;
|
|
1937
|
+
const walk = (dir, d) => {
|
|
1938
|
+
if (d <= 0 || count > 20000) return;
|
|
1939
|
+
let entries;
|
|
1940
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
1941
|
+
for (const e of entries) {
|
|
1942
|
+
if (excSet.has(e.name)) continue;
|
|
1943
|
+
if (e.isDirectory()) walk(path.join(dir, e.name), d - 1);
|
|
1944
|
+
else if (e.isFile() && SOURCE_EXTS.has(path.extname(e.name))) count++;
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
1947
|
+
for (const r of roots) {
|
|
1948
|
+
const full = path.join(cwd, r);
|
|
1949
|
+
if (fs.existsSync(full)) walk(full, depth);
|
|
1950
|
+
}
|
|
1951
|
+
return count;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
/** The workspace marker present at cwd, or null. */
|
|
1955
|
+
function _monorepoMarker(cwd) {
|
|
1956
|
+
for (const m of MONOREPO_MARKERS) {
|
|
1957
|
+
if (fs.existsSync(path.join(cwd, m))) return m;
|
|
1958
|
+
}
|
|
1959
|
+
try {
|
|
1960
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
1961
|
+
if (pkg.workspaces) return 'package.json workspaces';
|
|
1962
|
+
} catch (_) {}
|
|
1963
|
+
return null;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
/**
|
|
1967
|
+
* Build the recommended config diff for a repo.
|
|
1968
|
+
*
|
|
1969
|
+
* @param {string} cwd
|
|
1970
|
+
* @returns {{ changes: Array<{key:string, current:*, recommended:*, reason:string}>,
|
|
1971
|
+
* detection: { roots:string[], confidence:string, isMonorepo:boolean },
|
|
1972
|
+
* configExists: boolean }}
|
|
1973
|
+
*/
|
|
1974
|
+
function buildTuneProposal(cwd) {
|
|
1975
|
+
const userConfig = _readUserConfig(cwd);
|
|
1976
|
+
const config = loadConfig(cwd);
|
|
1977
|
+
const detection = resolveSourceRoots(cwd, { exclude: config.exclude });
|
|
1978
|
+
const changes = [];
|
|
1979
|
+
|
|
1980
|
+
// 1. srcDirs — recommend pinning the detected roots when the user hasn't.
|
|
1981
|
+
// Pinned srcDirs make generation explicit/stable and are protected from
|
|
1982
|
+
// token-budget drops; user-pinned srcDirs are never proposed against.
|
|
1983
|
+
const userPinnedSrcDirs = !!(userConfig && Array.isArray(userConfig.srcDirs));
|
|
1984
|
+
if (!userPinnedSrcDirs && detection.roots.length > 0 && detection.confidence !== 'low') {
|
|
1985
|
+
changes.push({
|
|
1986
|
+
key: 'srcDirs',
|
|
1987
|
+
current: null,
|
|
1988
|
+
recommended: detection.roots,
|
|
1989
|
+
reason: `pin the ${detection.roots.length} detected source root(s) [confidence ${detection.confidence}] — explicit srcDirs are stable across runs and protected from budget drops`,
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// 2. monorepo — a workspace marker exists but the mode is off.
|
|
1994
|
+
const marker = _monorepoMarker(cwd);
|
|
1995
|
+
if (marker && config.monorepo !== true) {
|
|
1996
|
+
changes.push({
|
|
1997
|
+
key: 'monorepo',
|
|
1998
|
+
current: config.monorepo,
|
|
1999
|
+
recommended: true,
|
|
2000
|
+
reason: `workspace marker found: ${marker}`,
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// 3. adapters — client artifacts present that the adapter list doesn't cover.
|
|
2005
|
+
const currentAdapters = Array.isArray(config.adapters) ? config.adapters
|
|
2006
|
+
: (Array.isArray(config.outputs) ? config.outputs : ['copilot']);
|
|
2007
|
+
const found = [];
|
|
2008
|
+
for (const { adapter, files } of ADAPTER_MARKERS) {
|
|
2009
|
+
if (currentAdapters.includes(adapter)) continue;
|
|
2010
|
+
const hit = files.find((f) => fs.existsSync(path.join(cwd, f)));
|
|
2011
|
+
if (hit) found.push({ adapter, hit });
|
|
2012
|
+
}
|
|
2013
|
+
if (found.length > 0) {
|
|
2014
|
+
changes.push({
|
|
2015
|
+
key: 'adapters',
|
|
2016
|
+
current: currentAdapters,
|
|
2017
|
+
recommended: [...currentAdapters, ...found.map((f) => f.adapter)],
|
|
2018
|
+
reason: `client files present: ${found.map((f) => f.hit).join(', ')}`,
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// 4. exclude — root-level vendored/generated dirs not excluded yet.
|
|
2023
|
+
const junkFound = JUNK_DIRS.filter((d) => {
|
|
2024
|
+
if ((config.exclude || []).includes(d)) return false;
|
|
2025
|
+
try { return fs.statSync(path.join(cwd, d)).isDirectory(); } catch (_) { return false; }
|
|
2026
|
+
});
|
|
2027
|
+
if (junkFound.length > 0) {
|
|
2028
|
+
changes.push({
|
|
2029
|
+
key: 'exclude',
|
|
2030
|
+
current: userConfig && userConfig.exclude ? userConfig.exclude : null,
|
|
2031
|
+
recommended: [...((userConfig && userConfig.exclude) || config.exclude), ...junkFound],
|
|
2032
|
+
reason: `present at root and typically vendored/generated: ${junkFound.join(', ')}`,
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// 5. autoMaxTokens — a pinned budget that the repo's size will overflow.
|
|
2037
|
+
if (config.autoMaxTokens === false) {
|
|
2038
|
+
const roots = detection.roots.length > 0 ? detection.roots : config.srcDirs;
|
|
2039
|
+
const files = _countSourceFiles(cwd, roots, config.exclude);
|
|
2040
|
+
const estTokens = files * TOKENS_PER_FILE;
|
|
2041
|
+
if (estTokens > config.maxTokens) {
|
|
2042
|
+
changes.push({
|
|
2043
|
+
key: 'autoMaxTokens',
|
|
2044
|
+
current: false,
|
|
2045
|
+
recommended: true,
|
|
2046
|
+
reason: `~${files} source files need ~${estTokens} tokens (heuristic) but maxTokens is pinned at ${config.maxTokens} — auto-scaling targets ${Math.round((config.coverageTarget || 0.8) * 100)}% coverage`,
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
return {
|
|
2052
|
+
changes,
|
|
2053
|
+
detection: { roots: detection.roots, confidence: detection.confidence, isMonorepo: detection.isMonorepo },
|
|
2054
|
+
configExists: userConfig !== null,
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
/**
|
|
2059
|
+
* Merge a proposal's changes into gen-context.config.json (create if absent).
|
|
2060
|
+
* Preserves every existing user key; only the proposed keys are written.
|
|
2061
|
+
*
|
|
2062
|
+
* @returns {{ path: string, applied: string[] }}
|
|
2063
|
+
*/
|
|
2064
|
+
function applyTuneProposal(cwd, proposal) {
|
|
2065
|
+
const cfgPath = path.join(cwd, 'gen-context.config.json');
|
|
2066
|
+
const existing = _readUserConfig(cwd) || {};
|
|
2067
|
+
for (const c of proposal.changes) existing[c.key] = c.recommended;
|
|
2068
|
+
fs.writeFileSync(cfgPath, JSON.stringify(existing, null, 2) + '\n');
|
|
2069
|
+
return { path: cfgPath, applied: proposal.changes.map((c) => c.key) };
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
/** Human rendering of a proposal (one block per change, reason indented). */
|
|
2073
|
+
function formatTuneProposal(proposal) {
|
|
2074
|
+
const lines = [];
|
|
2075
|
+
if (proposal.changes.length === 0) {
|
|
2076
|
+
lines.push('[sigmap] tune: config already matches detection — no changes recommended');
|
|
2077
|
+
} else {
|
|
2078
|
+
lines.push(`[sigmap] tune: ${proposal.changes.length} recommended change(s)`);
|
|
2079
|
+
for (const c of proposal.changes) {
|
|
2080
|
+
lines.push(` ${c.key.padEnd(14)} ${JSON.stringify(c.current)} → ${JSON.stringify(c.recommended)}`);
|
|
2081
|
+
lines.push(` ${''.padEnd(14)} reason: ${c.reason}`);
|
|
2082
|
+
}
|
|
2083
|
+
lines.push('');
|
|
2084
|
+
lines.push(' apply with: sigmap tune --apply (then: sigmap validate)');
|
|
2085
|
+
}
|
|
2086
|
+
lines.push(` detection: roots [${proposal.detection.roots.join(', ')}] · confidence ${proposal.detection.confidence} · monorepo ${proposal.detection.isMonorepo ? 'yes' : 'no'}`);
|
|
2087
|
+
return lines.join('\n');
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
module.exports = { buildTuneProposal, applyTuneProposal, formatTuneProposal, JUNK_DIRS, TOKENS_PER_FILE };
|
|
2091
|
+
|
|
2092
|
+
};
|
|
2093
|
+
|
|
1880
2094
|
// ── ./src/conventions/ci ──
|
|
1881
2095
|
__factories["./src/conventions/ci"] = function(module, exports) {
|
|
1882
2096
|
|
|
@@ -14981,7 +15195,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
14981
15195
|
|
|
14982
15196
|
const SERVER_INFO = {
|
|
14983
15197
|
name: 'sigmap',
|
|
14984
|
-
version: '8.
|
|
15198
|
+
version: '8.26.0',
|
|
14985
15199
|
description: 'SigMap MCP server — code signatures on demand',
|
|
14986
15200
|
};
|
|
14987
15201
|
|
|
@@ -17875,6 +18089,192 @@ __factories["./src/session/notes"] = function(module, exports) {
|
|
|
17875
18089
|
|
|
17876
18090
|
};
|
|
17877
18091
|
|
|
18092
|
+
// ── ./src/skills/skills ──
|
|
18093
|
+
__factories["./src/skills/skills"] = function(module, exports) {
|
|
18094
|
+
|
|
18095
|
+
/**
|
|
18096
|
+
* sigmap skills — canonical agent playbooks + multi-client installer (F3+F4, #517).
|
|
18097
|
+
*
|
|
18098
|
+
* Two deterministic skill documents (the spend-minimizing usage loop and the
|
|
18099
|
+
* config-optimizer playbook) emitted in each client's native skill/rules
|
|
18100
|
+
* format — the `mcp/install.js` CLIENTS pattern applied to skills. File-based
|
|
18101
|
+
* clients get sigmap-namespaced files we own outright; AGENTS.md (codex) gets
|
|
18102
|
+
* a marker-delimited block inserted ABOVE the `## Auto-generated signatures`
|
|
18103
|
+
* marker, since the codex adapter preserves content above it and replaces
|
|
18104
|
+
* everything below. Idempotent; human content is never touched.
|
|
18105
|
+
*/
|
|
18106
|
+
|
|
18107
|
+
const fs = require('fs');
|
|
18108
|
+
const path = require('path');
|
|
18109
|
+
|
|
18110
|
+
const START = '<!-- sigmap-skills:start -->';
|
|
18111
|
+
const END = '<!-- sigmap-skills:end -->';
|
|
18112
|
+
const SIGNATURES_MARKER = '## Auto-generated signatures';
|
|
18113
|
+
|
|
18114
|
+
const SKILLS = {
|
|
18115
|
+
'sigmap-usage-maximizer': {
|
|
18116
|
+
title: 'SigMap usage maximizer',
|
|
18117
|
+
description: 'Spend-minimizing loop for agents in a SigMap-indexed repo: ask before reading, read ranges, verify before trusting, squeeze big pastes, checkpoint, watch the budget.',
|
|
18118
|
+
body: [
|
|
18119
|
+
'Follow this loop before any file exploration in a repo with SigMap installed.',
|
|
18120
|
+
'',
|
|
18121
|
+
'1. **Ask before reading.** `sigmap ask "<task>"` (or the `query_context` MCP tool) ranks the relevant files as ~hundreds of tokens of signatures instead of thousands of raw-file tokens. Never open files to "look around".',
|
|
18122
|
+
'2. **Read ranges, not files.** Use the `get_lines` MCP tool with the `:start-end` line anchors carried on every signature to pull only the lines you need.',
|
|
18123
|
+
'3. **Ground before trusting.** Run the `verify_suggestion` MCP tool (or `sigmap verify-ai-output`) on generated code before applying it — it flags fabricated files, imports, symbols, and npm scripts against the live index.',
|
|
18124
|
+
'4. **Squeeze big pastes.** Any stack trace, CI/build log, or JSON blob goes through `sigmap squeeze` (or the `squeeze_output` MCP tool) before it enters context — the signal survives, the noise does not.',
|
|
18125
|
+
'5. **Checkpoint progress.** Use the `create_checkpoint` MCP tool or `sigmap note "<decision>"` so a follow-up session resumes without re-deriving state.',
|
|
18126
|
+
'6. **Watch the budget.** Check the `get_budget` MCP tool or `sigmap budget` (estimates from SigMap\'s local ledger — no LLM calls). Near the budget: summarize-then-drop older context instead of accumulating, and prefer terse output.',
|
|
18127
|
+
].join('\n'),
|
|
18128
|
+
},
|
|
18129
|
+
'sigmap-config-optimizer': {
|
|
18130
|
+
title: 'SigMap config optimizer',
|
|
18131
|
+
description: 'Playbook for getting a correct SigMap config on any repo: detect with sigmap tune, review the per-change reasons, apply, validate.',
|
|
18132
|
+
body: [
|
|
18133
|
+
'Playbook for configuring SigMap on a new or misconfigured repo.',
|
|
18134
|
+
'',
|
|
18135
|
+
'1. **Detect.** `sigmap tune` prints a recommended config diff — srcDirs pin, monorepo mode, adapters, excludes, budget — with one evidence-naming reason per change. Read-only.',
|
|
18136
|
+
'2. **Review.** Every reason names its evidence (workspace marker, client artifact, vendored dir, file-count estimate). Explicit user config is never proposed against.',
|
|
18137
|
+
'3. **Apply.** `sigmap tune --apply` merges the recommendations into `gen-context.config.json`, preserving every existing key. Idempotent — a second `tune` proposes nothing.',
|
|
18138
|
+
'4. **Validate.** `sigmap validate` confirms the generated context matches the config; `sigmap doctor` diagnoses anything left.',
|
|
18139
|
+
].join('\n'),
|
|
18140
|
+
},
|
|
18141
|
+
};
|
|
18142
|
+
|
|
18143
|
+
// Client registry — parent = artifact whose presence means "user uses this
|
|
18144
|
+
// client" (the `--setup` only-touch-existing rule for plain `skills install`).
|
|
18145
|
+
const SKILL_CLIENTS = {
|
|
18146
|
+
claude: { label: 'Claude Code', parent: ['.claude'],
|
|
18147
|
+
target: (cwd, skill) => path.join(cwd, '.claude', 'skills', skill, 'SKILL.md') },
|
|
18148
|
+
cursor: { label: 'Cursor', parent: ['.cursor'],
|
|
18149
|
+
target: (cwd, skill) => path.join(cwd, '.cursor', 'rules', `${skill}.mdc`) },
|
|
18150
|
+
windsurf: { label: 'Windsurf', parent: ['.windsurf'],
|
|
18151
|
+
target: (cwd, skill) => path.join(cwd, '.windsurf', 'rules', `${skill}.md`) },
|
|
18152
|
+
copilot: { label: 'GitHub Copilot', parent: ['.github'],
|
|
18153
|
+
target: (cwd, skill) => path.join(cwd, '.github', 'instructions', `${skill}.instructions.md`) },
|
|
18154
|
+
codex: { label: 'Codex CLI (AGENTS.md)', parent: ['AGENTS.md'],
|
|
18155
|
+
target: (cwd) => path.join(cwd, 'AGENTS.md'), inject: true },
|
|
18156
|
+
};
|
|
18157
|
+
|
|
18158
|
+
function _footer(version) {
|
|
18159
|
+
const ver = version ? ` v${version}` : '';
|
|
18160
|
+
return `<sub>Generated by SigMap${ver} · run \`sigmap skills install\` to refresh.</sub>`;
|
|
18161
|
+
}
|
|
18162
|
+
|
|
18163
|
+
/** Render one skill's client-specific file content. */
|
|
18164
|
+
function renderSkill(client, skillName, version) {
|
|
18165
|
+
const skill = SKILLS[skillName];
|
|
18166
|
+
const body = `# ${skill.title}\n\n${skill.body}\n\n${_footer(version)}\n`;
|
|
18167
|
+
if (client === 'claude') {
|
|
18168
|
+
return `---\nname: ${skillName}\ndescription: ${skill.description}\n---\n\n${body}`;
|
|
18169
|
+
}
|
|
18170
|
+
if (client === 'cursor') {
|
|
18171
|
+
return `---\ndescription: ${skill.description}\nalwaysApply: false\n---\n\n${body}`;
|
|
18172
|
+
}
|
|
18173
|
+
if (client === 'copilot') {
|
|
18174
|
+
return `---\napplyTo: "**"\n---\n\n${body}`;
|
|
18175
|
+
}
|
|
18176
|
+
return body; // windsurf: plain markdown
|
|
18177
|
+
}
|
|
18178
|
+
|
|
18179
|
+
/** Render the combined AGENTS.md block (both skills, marker-delimited). */
|
|
18180
|
+
function renderAgentsBlock(version) {
|
|
18181
|
+
const parts = [START, '## SigMap agent skills', ''];
|
|
18182
|
+
for (const name of Object.keys(SKILLS)) {
|
|
18183
|
+
parts.push(`### ${SKILLS[name].title}`, '', SKILLS[name].body, '');
|
|
18184
|
+
}
|
|
18185
|
+
parts.push(_footer(version), END);
|
|
18186
|
+
return parts.join('\n');
|
|
18187
|
+
}
|
|
18188
|
+
|
|
18189
|
+
/**
|
|
18190
|
+
* Inject (or replace) the skills block in AGENTS.md content.
|
|
18191
|
+
* A new block is inserted ABOVE the `## Auto-generated signatures` marker —
|
|
18192
|
+
* the codex adapter's write() preserves content above that marker and
|
|
18193
|
+
* replaces everything below it. Never touches content outside the markers.
|
|
18194
|
+
*/
|
|
18195
|
+
function injectSkillsBlock(existing, block) {
|
|
18196
|
+
const src = String(existing || '');
|
|
18197
|
+
const startIdx = src.indexOf(START);
|
|
18198
|
+
if (startIdx !== -1) {
|
|
18199
|
+
const endIdx = src.indexOf(END, startIdx);
|
|
18200
|
+
if (endIdx !== -1) {
|
|
18201
|
+
return src.slice(0, startIdx) + block + src.slice(endIdx + END.length);
|
|
18202
|
+
}
|
|
18203
|
+
}
|
|
18204
|
+
const sigIdx = src.indexOf(SIGNATURES_MARKER);
|
|
18205
|
+
if (sigIdx !== -1) {
|
|
18206
|
+
return src.slice(0, sigIdx) + block + '\n\n' + src.slice(sigIdx);
|
|
18207
|
+
}
|
|
18208
|
+
if (src.trim() === '') return block + '\n';
|
|
18209
|
+
return src + (src.endsWith('\n') ? '\n' : '\n\n') + block + '\n';
|
|
18210
|
+
}
|
|
18211
|
+
|
|
18212
|
+
function _writeIfChanged(filePath, content) {
|
|
18213
|
+
if (fs.existsSync(filePath)) {
|
|
18214
|
+
const existing = fs.readFileSync(filePath, 'utf8');
|
|
18215
|
+
if (existing === content) return 'already';
|
|
18216
|
+
fs.writeFileSync(filePath, content);
|
|
18217
|
+
return 'updated';
|
|
18218
|
+
}
|
|
18219
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
18220
|
+
fs.writeFileSync(filePath, content);
|
|
18221
|
+
return 'installed';
|
|
18222
|
+
}
|
|
18223
|
+
|
|
18224
|
+
/**
|
|
18225
|
+
* Install both skills for one client.
|
|
18226
|
+
* @returns {{ client, label, results: Array<{skill, path, status}> }}
|
|
18227
|
+
* status: 'installed' | 'updated' | 'already'; or { status:'unknown', valid } for a bad client.
|
|
18228
|
+
*/
|
|
18229
|
+
function installSkills(client, opts = {}) {
|
|
18230
|
+
const spec = SKILL_CLIENTS[client];
|
|
18231
|
+
if (!spec) return { client, status: 'unknown', valid: Object.keys(SKILL_CLIENTS) };
|
|
18232
|
+
const cwd = opts.cwd || process.cwd();
|
|
18233
|
+
const version = opts.version || null;
|
|
18234
|
+
const results = [];
|
|
18235
|
+
|
|
18236
|
+
if (spec.inject) {
|
|
18237
|
+
const filePath = spec.target(cwd);
|
|
18238
|
+
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
18239
|
+
const next = injectSkillsBlock(existing, renderAgentsBlock(version));
|
|
18240
|
+
let status;
|
|
18241
|
+
if (next === existing) status = 'already';
|
|
18242
|
+
else { fs.writeFileSync(filePath, next); status = existing ? 'updated' : 'installed'; }
|
|
18243
|
+
results.push({ skill: Object.keys(SKILLS).join(' + '), path: filePath, status });
|
|
18244
|
+
} else {
|
|
18245
|
+
for (const skillName of Object.keys(SKILLS)) {
|
|
18246
|
+
const filePath = spec.target(cwd, skillName);
|
|
18247
|
+
results.push({ skill: skillName, path: filePath, status: _writeIfChanged(filePath, renderSkill(client, skillName, version)) });
|
|
18248
|
+
}
|
|
18249
|
+
}
|
|
18250
|
+
return { client, label: spec.label, results };
|
|
18251
|
+
}
|
|
18252
|
+
|
|
18253
|
+
/** True when the client's parent artifact exists (plain-install eligibility). */
|
|
18254
|
+
function clientPresent(client, cwd) {
|
|
18255
|
+
const spec = SKILL_CLIENTS[client];
|
|
18256
|
+
return !!spec && fs.existsSync(path.join(cwd, ...spec.parent));
|
|
18257
|
+
}
|
|
18258
|
+
|
|
18259
|
+
/** List clients with target paths, presence, and installed state. */
|
|
18260
|
+
function listSkillClients(opts = {}) {
|
|
18261
|
+
const cwd = opts.cwd || process.cwd();
|
|
18262
|
+
return Object.keys(SKILL_CLIENTS).map((client) => {
|
|
18263
|
+
const spec = SKILL_CLIENTS[client];
|
|
18264
|
+
const targets = spec.inject
|
|
18265
|
+
? [spec.target(cwd)]
|
|
18266
|
+
: Object.keys(SKILLS).map((s) => spec.target(cwd, s));
|
|
18267
|
+
const installed = spec.inject
|
|
18268
|
+
? (fs.existsSync(targets[0]) && fs.readFileSync(targets[0], 'utf8').includes(START))
|
|
18269
|
+
: targets.every((t) => fs.existsSync(t));
|
|
18270
|
+
return { client, label: spec.label, present: clientPresent(client, cwd), installed, targets };
|
|
18271
|
+
});
|
|
18272
|
+
}
|
|
18273
|
+
|
|
18274
|
+
module.exports = { SKILLS, SKILL_CLIENTS, renderSkill, renderAgentsBlock, injectSkillsBlock, installSkills, listSkillClients, clientPresent, START, END };
|
|
18275
|
+
|
|
18276
|
+
};
|
|
18277
|
+
|
|
17878
18278
|
// ── ./src/squeeze/cilog ──
|
|
17879
18279
|
__factories["./src/squeeze/cilog"] = function(module, exports) {
|
|
17880
18280
|
|
|
@@ -20407,7 +20807,7 @@ function __tryGit(args, opts = {}) {
|
|
|
20407
20807
|
catch (_) { return ''; }
|
|
20408
20808
|
}
|
|
20409
20809
|
|
|
20410
|
-
const VERSION = '8.
|
|
20810
|
+
const VERSION = '8.26.0';
|
|
20411
20811
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
20412
20812
|
|
|
20413
20813
|
function requireSourceOrBundled(key) {
|
|
@@ -22295,6 +22695,10 @@ Usage:
|
|
|
22295
22695
|
${cmd} budget Session spend ledger — estimated SigMap-emitted tokens, budget, context age (--json)
|
|
22296
22696
|
${cmd} budget --budget <tokens> One-off budget override (config: sessionBudgetTokens, contextTtlDays)
|
|
22297
22697
|
${cmd} redact [file] Mask secrets in a file or stdin (10-pattern bank); redacted text to stdout (--json)
|
|
22698
|
+
${cmd} tune Recommend config from repo detection — srcDirs, monorepo, adapters, exclude, budget (--json)
|
|
22699
|
+
${cmd} tune --apply Write the recommendations into gen-context.config.json (merges; your keys preserved)
|
|
22700
|
+
${cmd} skills list List skill clients (Claude/Cursor/Windsurf/Copilot/AGENTS.md) and install state (--json)
|
|
22701
|
+
${cmd} skills install Install the SigMap agent playbooks for detected clients (--client <name> | --all)
|
|
22298
22702
|
${cmd} note "<text>" Append a note to the cross-session decision log
|
|
22299
22703
|
${cmd} note List recent notes (also: note --list <N>)
|
|
22300
22704
|
${cmd} status Show repo state — branch, dirty files, index freshness, notes
|
|
@@ -23694,6 +24098,94 @@ function main() {
|
|
|
23694
24098
|
process.exit(0);
|
|
23695
24099
|
}
|
|
23696
24100
|
|
|
24101
|
+
// `sigmap tune [--apply|--dry-run] [--json]` — deterministic config optimizer:
|
|
24102
|
+
// recommend srcDirs/monorepo/adapters/exclude/autoMaxTokens from the existing
|
|
24103
|
+
// discovery stack, one reason per change. Read-only unless --apply.
|
|
24104
|
+
if (args[0] === 'tune') {
|
|
24105
|
+
const { buildTuneProposal, applyTuneProposal, formatTuneProposal } = requireSourceOrBundled('./src/config/tune');
|
|
24106
|
+
let proposal;
|
|
24107
|
+
try {
|
|
24108
|
+
proposal = buildTuneProposal(cwd);
|
|
24109
|
+
} catch (err) {
|
|
24110
|
+
console.error(`[sigmap] tune failed: ${err.message}`);
|
|
24111
|
+
process.exit(1);
|
|
24112
|
+
}
|
|
24113
|
+
if (args.includes('--apply') && proposal.changes.length > 0) {
|
|
24114
|
+
const res = applyTuneProposal(cwd, proposal);
|
|
24115
|
+
if (args.includes('--json')) {
|
|
24116
|
+
process.stdout.write(JSON.stringify({ ...proposal, applied: res.applied, path: res.path }) + '\n');
|
|
24117
|
+
} else {
|
|
24118
|
+
console.log(formatTuneProposal(proposal));
|
|
24119
|
+
console.log(`[sigmap] tune: wrote ${res.applied.join(', ')} → ${path.relative(cwd, res.path)} — run: sigmap validate`);
|
|
24120
|
+
}
|
|
24121
|
+
process.exit(0);
|
|
24122
|
+
}
|
|
24123
|
+
if (args.includes('--json')) {
|
|
24124
|
+
process.stdout.write(JSON.stringify(proposal) + '\n');
|
|
24125
|
+
} else {
|
|
24126
|
+
console.log(formatTuneProposal(proposal));
|
|
24127
|
+
}
|
|
24128
|
+
process.exit(0);
|
|
24129
|
+
}
|
|
24130
|
+
|
|
24131
|
+
// `sigmap skills list|install [--client <name>|--all] [--json]` — install the
|
|
24132
|
+
// agent playbooks (usage-maximizer, config-optimizer) in each client's native
|
|
24133
|
+
// skill/rules format. Plain `install` touches only clients whose parent
|
|
24134
|
+
// artifact exists; --client/--all create. Idempotent.
|
|
24135
|
+
if (args[0] === 'skills') {
|
|
24136
|
+
const { installSkills, listSkillClients, SKILL_CLIENTS } = requireSourceOrBundled('./src/skills/skills');
|
|
24137
|
+
const sub = args[1];
|
|
24138
|
+
const jsonOut = args.includes('--json');
|
|
24139
|
+
|
|
24140
|
+
if (sub === 'list' || sub === undefined) {
|
|
24141
|
+
const clients = listSkillClients({ cwd });
|
|
24142
|
+
if (jsonOut) {
|
|
24143
|
+
process.stdout.write(JSON.stringify(clients, null, 2) + '\n');
|
|
24144
|
+
process.exit(0);
|
|
24145
|
+
}
|
|
24146
|
+
console.log('Supported skill clients:\n');
|
|
24147
|
+
for (const c of clients) {
|
|
24148
|
+
const state = c.installed ? 'installed' : (c.present ? 'client present, skills not installed' : 'client not detected');
|
|
24149
|
+
console.log(` ${c.client.padEnd(10)} ${c.label.padEnd(24)} ${state}`);
|
|
24150
|
+
for (const t of c.targets) console.log(` ${path.relative(cwd, t)}`);
|
|
24151
|
+
}
|
|
24152
|
+
console.log('\nInstall with: sigmap skills install [--client <name> | --all]');
|
|
24153
|
+
process.exit(0);
|
|
24154
|
+
}
|
|
24155
|
+
|
|
24156
|
+
if (sub === 'install') {
|
|
24157
|
+
const cIdx = args.indexOf('--client');
|
|
24158
|
+
const one = cIdx !== -1 ? args[cIdx + 1] : null;
|
|
24159
|
+
let targets;
|
|
24160
|
+
if (one) targets = [one];
|
|
24161
|
+
else if (args.includes('--all')) targets = Object.keys(SKILL_CLIENTS);
|
|
24162
|
+
else targets = listSkillClients({ cwd }).filter((c) => c.present).map((c) => c.client);
|
|
24163
|
+
if (targets.length === 0) {
|
|
24164
|
+
console.error('[sigmap] skills: no clients detected — use --client <name> or --all (see: sigmap skills list)');
|
|
24165
|
+
process.exit(1);
|
|
24166
|
+
}
|
|
24167
|
+
const out = targets.map((c) => installSkills(c, { cwd, version: VERSION }));
|
|
24168
|
+
const bad = out.find((r) => r.status === 'unknown');
|
|
24169
|
+
if (bad) {
|
|
24170
|
+
console.error(`[sigmap] skills: unknown client "${bad.client}" — valid: ${bad.valid.join(', ')}`);
|
|
24171
|
+
process.exit(1);
|
|
24172
|
+
}
|
|
24173
|
+
if (jsonOut) {
|
|
24174
|
+
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
|
|
24175
|
+
process.exit(0);
|
|
24176
|
+
}
|
|
24177
|
+
for (const r of out) {
|
|
24178
|
+
for (const res of r.results) {
|
|
24179
|
+
console.log(` ${r.client.padEnd(10)} ${res.status.padEnd(10)} ${path.relative(cwd, res.path)}`);
|
|
24180
|
+
}
|
|
24181
|
+
}
|
|
24182
|
+
process.exit(0);
|
|
24183
|
+
}
|
|
24184
|
+
|
|
24185
|
+
console.error('[sigmap] usage: sigmap skills [list | install [--client <name> | --all]] [--json]');
|
|
24186
|
+
process.exit(1);
|
|
24187
|
+
}
|
|
24188
|
+
|
|
23697
24189
|
// `sigmap note "<text>"` — append to the cross-session decision log.
|
|
23698
24190
|
// With no text, lists recent notes (also `note --list [N]`).
|
|
23699
24191
|
// `sigmap memory` — one view over the cross-session stores in .context/.
|
package/llms-full.txt
CHANGED
|
@@ -11,17 +11,17 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
|
|
|
11
11
|
effect), with no LLM calls, embeddings, or vector database. Works with Claude,
|
|
12
12
|
Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
13
13
|
|
|
14
|
-
# Version: 8.
|
|
14
|
+
# Version: 8.26.0 | Benchmark: sigmap-v8.26-main (2026-08-17)
|
|
15
15
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
16
16
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
## Core metrics (benchmark: sigmap-v8.
|
|
20
|
+
## Core metrics (benchmark: sigmap-v8.26-main, 2026-08-17)
|
|
21
21
|
|
|
22
22
|
| Metric | Without SigMap | With SigMap |
|
|
23
23
|
|--------|----------------|-------------|
|
|
24
|
-
| Retrieval hit@5 | 44.
|
|
24
|
+
| Retrieval hit@5 | 44.0% (single-shot grep) | 82.2% (1.87× lift) |
|
|
25
25
|
| Token reduction | — | 96.8% average |
|
|
26
26
|
| Task-success proxy (modeled) | — | 64.8% |
|
|
27
27
|
| Prompts per task | 2.84 | 1.53 (46.1% fewer) |
|
|
@@ -128,6 +128,10 @@ sigmap memory --clear <store> Clear one store: session|notes|weights|
|
|
|
128
128
|
sigmap budget Session spend ledger — estimated SigMap-emitted tokens, budget, context age (--json)
|
|
129
129
|
sigmap budget --budget <tokens> One-off budget override (config: sessionBudgetTokens, contextTtlDays)
|
|
130
130
|
sigmap redact [file] Mask secrets in a file or stdin (10-pattern bank); redacted text to stdout (--json)
|
|
131
|
+
sigmap tune Recommend config from repo detection — srcDirs, monorepo, adapters, exclude, budget (--json)
|
|
132
|
+
sigmap tune --apply Write the recommendations into gen-context.config.json (merges; your keys preserved)
|
|
133
|
+
sigmap skills list List skill clients (Claude/Cursor/Windsurf/Copilot/AGENTS.md) and install state (--json)
|
|
134
|
+
sigmap skills install Install the SigMap agent playbooks for detected clients (--client <name> | --all)
|
|
131
135
|
sigmap note "<text>" Append a note to the cross-session decision log
|
|
132
136
|
sigmap note List recent notes (also: note --list <N>)
|
|
133
137
|
sigmap status Show repo state — branch, dirty files, index freshness, notes
|
package/llms.txt
CHANGED
|
@@ -11,7 +11,7 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
|
|
|
11
11
|
effect), with no LLM calls, embeddings, or vector database. Works with Claude,
|
|
12
12
|
Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
13
13
|
|
|
14
|
-
# Version: 8.
|
|
14
|
+
# Version: 8.26.0 | Benchmark: sigmap-v8.26-main (2026-08-17)
|
|
15
15
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
16
16
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
17
17
|
|
|
@@ -23,9 +23,9 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
|
23
23
|
- No blast-radius awareness before editing a hub file — `--impact` shows every file a change touches.
|
|
24
24
|
- Pasted stack traces, CI logs, and JSON bloat the prompt — `squeeze` minimizes them and enriches the top frame from the symbol index.
|
|
25
25
|
|
|
26
|
-
## Core metrics (benchmark: sigmap-v8.
|
|
26
|
+
## Core metrics (benchmark: sigmap-v8.26-main, 2026-08-17)
|
|
27
27
|
|
|
28
|
-
- hit@5 retrieval: 82.2% vs 44.
|
|
28
|
+
- hit@5 retrieval: 82.2% vs 44.0% single-shot grep baseline (1.87× lift)
|
|
29
29
|
- Token reduction: 96.8% average across benchmark repos
|
|
30
30
|
- Task-success proxy: 64.8% (modeled from retrieval tiers, not measured LLM sessions)
|
|
31
31
|
- Prompts per task: 1.53 vs 2.84 baseline (46.1% fewer, modeled)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.26.0",
|
|
4
4
|
"description": "The deterministic, verifiable grounding layer for AI code work — a zero-dependency signature-and-evidence map that grounds Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP agents against your real code (repo + installed libraries) so they stop hallucinating files, imports & APIs. Runs offline via npx; byte-stable output; ~97% token reduction as proof.",
|
|
5
5
|
"main": "packages/core/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sigmap tune — deterministic config optimizer (F2, #514).
|
|
5
|
+
*
|
|
6
|
+
* Packages the existing discovery stack (source-root-resolver, monorepo
|
|
7
|
+
* markers, client-artifact probes) into a recommended config diff with a
|
|
8
|
+
* one-line reason per change. Read-only by default; `applyTuneProposal`
|
|
9
|
+
* merges accepted changes into gen-context.config.json, preserving every
|
|
10
|
+
* user key. Explicit user choices are never proposed against.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { loadConfig } = require('./loader');
|
|
16
|
+
const { resolveSourceRoots } = require('../discovery/source-root-resolver');
|
|
17
|
+
|
|
18
|
+
// Workspace markers, in probe order (reason names the first one found).
|
|
19
|
+
const MONOREPO_MARKERS = ['pnpm-workspace.yaml', 'turbo.json', 'nx.json', 'lerna.json'];
|
|
20
|
+
|
|
21
|
+
// Client artifacts → adapter names (additive only).
|
|
22
|
+
const ADAPTER_MARKERS = [
|
|
23
|
+
{ adapter: 'claude', files: ['CLAUDE.md'] },
|
|
24
|
+
{ adapter: 'cursor', files: ['.cursorrules', '.cursor'] },
|
|
25
|
+
{ adapter: 'windsurf', files: ['.windsurfrules', '.windsurf'] },
|
|
26
|
+
{ adapter: 'codex', files: ['AGENTS.md'] },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Root-level dirs that are typically vendored/generated when present.
|
|
30
|
+
const JUNK_DIRS = [
|
|
31
|
+
'third_party', 'thirdparty', 'external', 'externals',
|
|
32
|
+
'generated', 'testdata', 'snapshots', 'tmp', 'temp', '.cache',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Rough signature cost per source file (chars/4 world) for the budget check.
|
|
36
|
+
const TOKENS_PER_FILE = 25;
|
|
37
|
+
|
|
38
|
+
const SOURCE_EXTS = new Set([
|
|
39
|
+
'.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.py', '.rb', '.go', '.rs',
|
|
40
|
+
'.java', '.kt', '.cs', '.cpp', '.c', '.h', '.hpp', '.swift', '.dart',
|
|
41
|
+
'.scala', '.php', '.gd', '.r', '.R',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/** Raw user config file content, or null when absent/unparsable. */
|
|
45
|
+
function _readUserConfig(cwd) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8'));
|
|
48
|
+
} catch (_) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Count source files under `roots` (relative to cwd), depth-capped, deterministic. */
|
|
54
|
+
function _countSourceFiles(cwd, roots, exclude, depth = 5) {
|
|
55
|
+
const excSet = new Set(exclude || []);
|
|
56
|
+
let count = 0;
|
|
57
|
+
const walk = (dir, d) => {
|
|
58
|
+
if (d <= 0 || count > 20000) return;
|
|
59
|
+
let entries;
|
|
60
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
61
|
+
for (const e of entries) {
|
|
62
|
+
if (excSet.has(e.name)) continue;
|
|
63
|
+
if (e.isDirectory()) walk(path.join(dir, e.name), d - 1);
|
|
64
|
+
else if (e.isFile() && SOURCE_EXTS.has(path.extname(e.name))) count++;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
for (const r of roots) {
|
|
68
|
+
const full = path.join(cwd, r);
|
|
69
|
+
if (fs.existsSync(full)) walk(full, depth);
|
|
70
|
+
}
|
|
71
|
+
return count;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The workspace marker present at cwd, or null. */
|
|
75
|
+
function _monorepoMarker(cwd) {
|
|
76
|
+
for (const m of MONOREPO_MARKERS) {
|
|
77
|
+
if (fs.existsSync(path.join(cwd, m))) return m;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
81
|
+
if (pkg.workspaces) return 'package.json workspaces';
|
|
82
|
+
} catch (_) {}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build the recommended config diff for a repo.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} cwd
|
|
90
|
+
* @returns {{ changes: Array<{key:string, current:*, recommended:*, reason:string}>,
|
|
91
|
+
* detection: { roots:string[], confidence:string, isMonorepo:boolean },
|
|
92
|
+
* configExists: boolean }}
|
|
93
|
+
*/
|
|
94
|
+
function buildTuneProposal(cwd) {
|
|
95
|
+
const userConfig = _readUserConfig(cwd);
|
|
96
|
+
const config = loadConfig(cwd);
|
|
97
|
+
const detection = resolveSourceRoots(cwd, { exclude: config.exclude });
|
|
98
|
+
const changes = [];
|
|
99
|
+
|
|
100
|
+
// 1. srcDirs — recommend pinning the detected roots when the user hasn't.
|
|
101
|
+
// Pinned srcDirs make generation explicit/stable and are protected from
|
|
102
|
+
// token-budget drops; user-pinned srcDirs are never proposed against.
|
|
103
|
+
const userPinnedSrcDirs = !!(userConfig && Array.isArray(userConfig.srcDirs));
|
|
104
|
+
if (!userPinnedSrcDirs && detection.roots.length > 0 && detection.confidence !== 'low') {
|
|
105
|
+
changes.push({
|
|
106
|
+
key: 'srcDirs',
|
|
107
|
+
current: null,
|
|
108
|
+
recommended: detection.roots,
|
|
109
|
+
reason: `pin the ${detection.roots.length} detected source root(s) [confidence ${detection.confidence}] — explicit srcDirs are stable across runs and protected from budget drops`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 2. monorepo — a workspace marker exists but the mode is off.
|
|
114
|
+
const marker = _monorepoMarker(cwd);
|
|
115
|
+
if (marker && config.monorepo !== true) {
|
|
116
|
+
changes.push({
|
|
117
|
+
key: 'monorepo',
|
|
118
|
+
current: config.monorepo,
|
|
119
|
+
recommended: true,
|
|
120
|
+
reason: `workspace marker found: ${marker}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 3. adapters — client artifacts present that the adapter list doesn't cover.
|
|
125
|
+
const currentAdapters = Array.isArray(config.adapters) ? config.adapters
|
|
126
|
+
: (Array.isArray(config.outputs) ? config.outputs : ['copilot']);
|
|
127
|
+
const found = [];
|
|
128
|
+
for (const { adapter, files } of ADAPTER_MARKERS) {
|
|
129
|
+
if (currentAdapters.includes(adapter)) continue;
|
|
130
|
+
const hit = files.find((f) => fs.existsSync(path.join(cwd, f)));
|
|
131
|
+
if (hit) found.push({ adapter, hit });
|
|
132
|
+
}
|
|
133
|
+
if (found.length > 0) {
|
|
134
|
+
changes.push({
|
|
135
|
+
key: 'adapters',
|
|
136
|
+
current: currentAdapters,
|
|
137
|
+
recommended: [...currentAdapters, ...found.map((f) => f.adapter)],
|
|
138
|
+
reason: `client files present: ${found.map((f) => f.hit).join(', ')}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 4. exclude — root-level vendored/generated dirs not excluded yet.
|
|
143
|
+
const junkFound = JUNK_DIRS.filter((d) => {
|
|
144
|
+
if ((config.exclude || []).includes(d)) return false;
|
|
145
|
+
try { return fs.statSync(path.join(cwd, d)).isDirectory(); } catch (_) { return false; }
|
|
146
|
+
});
|
|
147
|
+
if (junkFound.length > 0) {
|
|
148
|
+
changes.push({
|
|
149
|
+
key: 'exclude',
|
|
150
|
+
current: userConfig && userConfig.exclude ? userConfig.exclude : null,
|
|
151
|
+
recommended: [...((userConfig && userConfig.exclude) || config.exclude), ...junkFound],
|
|
152
|
+
reason: `present at root and typically vendored/generated: ${junkFound.join(', ')}`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 5. autoMaxTokens — a pinned budget that the repo's size will overflow.
|
|
157
|
+
if (config.autoMaxTokens === false) {
|
|
158
|
+
const roots = detection.roots.length > 0 ? detection.roots : config.srcDirs;
|
|
159
|
+
const files = _countSourceFiles(cwd, roots, config.exclude);
|
|
160
|
+
const estTokens = files * TOKENS_PER_FILE;
|
|
161
|
+
if (estTokens > config.maxTokens) {
|
|
162
|
+
changes.push({
|
|
163
|
+
key: 'autoMaxTokens',
|
|
164
|
+
current: false,
|
|
165
|
+
recommended: true,
|
|
166
|
+
reason: `~${files} source files need ~${estTokens} tokens (heuristic) but maxTokens is pinned at ${config.maxTokens} — auto-scaling targets ${Math.round((config.coverageTarget || 0.8) * 100)}% coverage`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
changes,
|
|
173
|
+
detection: { roots: detection.roots, confidence: detection.confidence, isMonorepo: detection.isMonorepo },
|
|
174
|
+
configExists: userConfig !== null,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Merge a proposal's changes into gen-context.config.json (create if absent).
|
|
180
|
+
* Preserves every existing user key; only the proposed keys are written.
|
|
181
|
+
*
|
|
182
|
+
* @returns {{ path: string, applied: string[] }}
|
|
183
|
+
*/
|
|
184
|
+
function applyTuneProposal(cwd, proposal) {
|
|
185
|
+
const cfgPath = path.join(cwd, 'gen-context.config.json');
|
|
186
|
+
const existing = _readUserConfig(cwd) || {};
|
|
187
|
+
for (const c of proposal.changes) existing[c.key] = c.recommended;
|
|
188
|
+
fs.writeFileSync(cfgPath, JSON.stringify(existing, null, 2) + '\n');
|
|
189
|
+
return { path: cfgPath, applied: proposal.changes.map((c) => c.key) };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Human rendering of a proposal (one block per change, reason indented). */
|
|
193
|
+
function formatTuneProposal(proposal) {
|
|
194
|
+
const lines = [];
|
|
195
|
+
if (proposal.changes.length === 0) {
|
|
196
|
+
lines.push('[sigmap] tune: config already matches detection — no changes recommended');
|
|
197
|
+
} else {
|
|
198
|
+
lines.push(`[sigmap] tune: ${proposal.changes.length} recommended change(s)`);
|
|
199
|
+
for (const c of proposal.changes) {
|
|
200
|
+
lines.push(` ${c.key.padEnd(14)} ${JSON.stringify(c.current)} → ${JSON.stringify(c.recommended)}`);
|
|
201
|
+
lines.push(` ${''.padEnd(14)} reason: ${c.reason}`);
|
|
202
|
+
}
|
|
203
|
+
lines.push('');
|
|
204
|
+
lines.push(' apply with: sigmap tune --apply (then: sigmap validate)');
|
|
205
|
+
}
|
|
206
|
+
lines.push(` detection: roots [${proposal.detection.roots.join(', ')}] · confidence ${proposal.detection.confidence} · monorepo ${proposal.detection.isMonorepo ? 'yes' : 'no'}`);
|
|
207
|
+
return lines.join('\n');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = { buildTuneProposal, applyTuneProposal, formatTuneProposal, JUNK_DIRS, TOKENS_PER_FILE };
|
package/src/mcp/server.js
CHANGED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sigmap skills — canonical agent playbooks + multi-client installer (F3+F4, #517).
|
|
5
|
+
*
|
|
6
|
+
* Two deterministic skill documents (the spend-minimizing usage loop and the
|
|
7
|
+
* config-optimizer playbook) emitted in each client's native skill/rules
|
|
8
|
+
* format — the `mcp/install.js` CLIENTS pattern applied to skills. File-based
|
|
9
|
+
* clients get sigmap-namespaced files we own outright; AGENTS.md (codex) gets
|
|
10
|
+
* a marker-delimited block inserted ABOVE the `## Auto-generated signatures`
|
|
11
|
+
* marker, since the codex adapter preserves content above it and replaces
|
|
12
|
+
* everything below. Idempotent; human content is never touched.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const START = '<!-- sigmap-skills:start -->';
|
|
19
|
+
const END = '<!-- sigmap-skills:end -->';
|
|
20
|
+
const SIGNATURES_MARKER = '## Auto-generated signatures';
|
|
21
|
+
|
|
22
|
+
const SKILLS = {
|
|
23
|
+
'sigmap-usage-maximizer': {
|
|
24
|
+
title: 'SigMap usage maximizer',
|
|
25
|
+
description: 'Spend-minimizing loop for agents in a SigMap-indexed repo: ask before reading, read ranges, verify before trusting, squeeze big pastes, checkpoint, watch the budget.',
|
|
26
|
+
body: [
|
|
27
|
+
'Follow this loop before any file exploration in a repo with SigMap installed.',
|
|
28
|
+
'',
|
|
29
|
+
'1. **Ask before reading.** `sigmap ask "<task>"` (or the `query_context` MCP tool) ranks the relevant files as ~hundreds of tokens of signatures instead of thousands of raw-file tokens. Never open files to "look around".',
|
|
30
|
+
'2. **Read ranges, not files.** Use the `get_lines` MCP tool with the `:start-end` line anchors carried on every signature to pull only the lines you need.',
|
|
31
|
+
'3. **Ground before trusting.** Run the `verify_suggestion` MCP tool (or `sigmap verify-ai-output`) on generated code before applying it — it flags fabricated files, imports, symbols, and npm scripts against the live index.',
|
|
32
|
+
'4. **Squeeze big pastes.** Any stack trace, CI/build log, or JSON blob goes through `sigmap squeeze` (or the `squeeze_output` MCP tool) before it enters context — the signal survives, the noise does not.',
|
|
33
|
+
'5. **Checkpoint progress.** Use the `create_checkpoint` MCP tool or `sigmap note "<decision>"` so a follow-up session resumes without re-deriving state.',
|
|
34
|
+
'6. **Watch the budget.** Check the `get_budget` MCP tool or `sigmap budget` (estimates from SigMap\'s local ledger — no LLM calls). Near the budget: summarize-then-drop older context instead of accumulating, and prefer terse output.',
|
|
35
|
+
].join('\n'),
|
|
36
|
+
},
|
|
37
|
+
'sigmap-config-optimizer': {
|
|
38
|
+
title: 'SigMap config optimizer',
|
|
39
|
+
description: 'Playbook for getting a correct SigMap config on any repo: detect with sigmap tune, review the per-change reasons, apply, validate.',
|
|
40
|
+
body: [
|
|
41
|
+
'Playbook for configuring SigMap on a new or misconfigured repo.',
|
|
42
|
+
'',
|
|
43
|
+
'1. **Detect.** `sigmap tune` prints a recommended config diff — srcDirs pin, monorepo mode, adapters, excludes, budget — with one evidence-naming reason per change. Read-only.',
|
|
44
|
+
'2. **Review.** Every reason names its evidence (workspace marker, client artifact, vendored dir, file-count estimate). Explicit user config is never proposed against.',
|
|
45
|
+
'3. **Apply.** `sigmap tune --apply` merges the recommendations into `gen-context.config.json`, preserving every existing key. Idempotent — a second `tune` proposes nothing.',
|
|
46
|
+
'4. **Validate.** `sigmap validate` confirms the generated context matches the config; `sigmap doctor` diagnoses anything left.',
|
|
47
|
+
].join('\n'),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Client registry — parent = artifact whose presence means "user uses this
|
|
52
|
+
// client" (the `--setup` only-touch-existing rule for plain `skills install`).
|
|
53
|
+
const SKILL_CLIENTS = {
|
|
54
|
+
claude: { label: 'Claude Code', parent: ['.claude'],
|
|
55
|
+
target: (cwd, skill) => path.join(cwd, '.claude', 'skills', skill, 'SKILL.md') },
|
|
56
|
+
cursor: { label: 'Cursor', parent: ['.cursor'],
|
|
57
|
+
target: (cwd, skill) => path.join(cwd, '.cursor', 'rules', `${skill}.mdc`) },
|
|
58
|
+
windsurf: { label: 'Windsurf', parent: ['.windsurf'],
|
|
59
|
+
target: (cwd, skill) => path.join(cwd, '.windsurf', 'rules', `${skill}.md`) },
|
|
60
|
+
copilot: { label: 'GitHub Copilot', parent: ['.github'],
|
|
61
|
+
target: (cwd, skill) => path.join(cwd, '.github', 'instructions', `${skill}.instructions.md`) },
|
|
62
|
+
codex: { label: 'Codex CLI (AGENTS.md)', parent: ['AGENTS.md'],
|
|
63
|
+
target: (cwd) => path.join(cwd, 'AGENTS.md'), inject: true },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function _footer(version) {
|
|
67
|
+
const ver = version ? ` v${version}` : '';
|
|
68
|
+
return `<sub>Generated by SigMap${ver} · run \`sigmap skills install\` to refresh.</sub>`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Render one skill's client-specific file content. */
|
|
72
|
+
function renderSkill(client, skillName, version) {
|
|
73
|
+
const skill = SKILLS[skillName];
|
|
74
|
+
const body = `# ${skill.title}\n\n${skill.body}\n\n${_footer(version)}\n`;
|
|
75
|
+
if (client === 'claude') {
|
|
76
|
+
return `---\nname: ${skillName}\ndescription: ${skill.description}\n---\n\n${body}`;
|
|
77
|
+
}
|
|
78
|
+
if (client === 'cursor') {
|
|
79
|
+
return `---\ndescription: ${skill.description}\nalwaysApply: false\n---\n\n${body}`;
|
|
80
|
+
}
|
|
81
|
+
if (client === 'copilot') {
|
|
82
|
+
return `---\napplyTo: "**"\n---\n\n${body}`;
|
|
83
|
+
}
|
|
84
|
+
return body; // windsurf: plain markdown
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Render the combined AGENTS.md block (both skills, marker-delimited). */
|
|
88
|
+
function renderAgentsBlock(version) {
|
|
89
|
+
const parts = [START, '## SigMap agent skills', ''];
|
|
90
|
+
for (const name of Object.keys(SKILLS)) {
|
|
91
|
+
parts.push(`### ${SKILLS[name].title}`, '', SKILLS[name].body, '');
|
|
92
|
+
}
|
|
93
|
+
parts.push(_footer(version), END);
|
|
94
|
+
return parts.join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Inject (or replace) the skills block in AGENTS.md content.
|
|
99
|
+
* A new block is inserted ABOVE the `## Auto-generated signatures` marker —
|
|
100
|
+
* the codex adapter's write() preserves content above that marker and
|
|
101
|
+
* replaces everything below it. Never touches content outside the markers.
|
|
102
|
+
*/
|
|
103
|
+
function injectSkillsBlock(existing, block) {
|
|
104
|
+
const src = String(existing || '');
|
|
105
|
+
const startIdx = src.indexOf(START);
|
|
106
|
+
if (startIdx !== -1) {
|
|
107
|
+
const endIdx = src.indexOf(END, startIdx);
|
|
108
|
+
if (endIdx !== -1) {
|
|
109
|
+
return src.slice(0, startIdx) + block + src.slice(endIdx + END.length);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const sigIdx = src.indexOf(SIGNATURES_MARKER);
|
|
113
|
+
if (sigIdx !== -1) {
|
|
114
|
+
return src.slice(0, sigIdx) + block + '\n\n' + src.slice(sigIdx);
|
|
115
|
+
}
|
|
116
|
+
if (src.trim() === '') return block + '\n';
|
|
117
|
+
return src + (src.endsWith('\n') ? '\n' : '\n\n') + block + '\n';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function _writeIfChanged(filePath, content) {
|
|
121
|
+
if (fs.existsSync(filePath)) {
|
|
122
|
+
const existing = fs.readFileSync(filePath, 'utf8');
|
|
123
|
+
if (existing === content) return 'already';
|
|
124
|
+
fs.writeFileSync(filePath, content);
|
|
125
|
+
return 'updated';
|
|
126
|
+
}
|
|
127
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
128
|
+
fs.writeFileSync(filePath, content);
|
|
129
|
+
return 'installed';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Install both skills for one client.
|
|
134
|
+
* @returns {{ client, label, results: Array<{skill, path, status}> }}
|
|
135
|
+
* status: 'installed' | 'updated' | 'already'; or { status:'unknown', valid } for a bad client.
|
|
136
|
+
*/
|
|
137
|
+
function installSkills(client, opts = {}) {
|
|
138
|
+
const spec = SKILL_CLIENTS[client];
|
|
139
|
+
if (!spec) return { client, status: 'unknown', valid: Object.keys(SKILL_CLIENTS) };
|
|
140
|
+
const cwd = opts.cwd || process.cwd();
|
|
141
|
+
const version = opts.version || null;
|
|
142
|
+
const results = [];
|
|
143
|
+
|
|
144
|
+
if (spec.inject) {
|
|
145
|
+
const filePath = spec.target(cwd);
|
|
146
|
+
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
147
|
+
const next = injectSkillsBlock(existing, renderAgentsBlock(version));
|
|
148
|
+
let status;
|
|
149
|
+
if (next === existing) status = 'already';
|
|
150
|
+
else { fs.writeFileSync(filePath, next); status = existing ? 'updated' : 'installed'; }
|
|
151
|
+
results.push({ skill: Object.keys(SKILLS).join(' + '), path: filePath, status });
|
|
152
|
+
} else {
|
|
153
|
+
for (const skillName of Object.keys(SKILLS)) {
|
|
154
|
+
const filePath = spec.target(cwd, skillName);
|
|
155
|
+
results.push({ skill: skillName, path: filePath, status: _writeIfChanged(filePath, renderSkill(client, skillName, version)) });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { client, label: spec.label, results };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** True when the client's parent artifact exists (plain-install eligibility). */
|
|
162
|
+
function clientPresent(client, cwd) {
|
|
163
|
+
const spec = SKILL_CLIENTS[client];
|
|
164
|
+
return !!spec && fs.existsSync(path.join(cwd, ...spec.parent));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** List clients with target paths, presence, and installed state. */
|
|
168
|
+
function listSkillClients(opts = {}) {
|
|
169
|
+
const cwd = opts.cwd || process.cwd();
|
|
170
|
+
return Object.keys(SKILL_CLIENTS).map((client) => {
|
|
171
|
+
const spec = SKILL_CLIENTS[client];
|
|
172
|
+
const targets = spec.inject
|
|
173
|
+
? [spec.target(cwd)]
|
|
174
|
+
: Object.keys(SKILLS).map((s) => spec.target(cwd, s));
|
|
175
|
+
const installed = spec.inject
|
|
176
|
+
? (fs.existsSync(targets[0]) && fs.readFileSync(targets[0], 'utf8').includes(START))
|
|
177
|
+
: targets.every((t) => fs.existsSync(t));
|
|
178
|
+
return { client, label: spec.label, present: clientPresent(client, cwd), installed, targets };
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = { SKILLS, SKILL_CLIENTS, renderSkill, renderAgentsBlock, injectSkillsBlock, installSkills, listSkillClients, clientPresent, START, END };
|