codehere 0.4.1 → 0.5.0-alpha.2
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/README.md +64 -7
- package/dist/index.js +43 -0
- package/dist/infrastructure/hooks/claude-sdk-hook.js +18 -3
- package/dist/infrastructure/orchestration/aider-adapter.js +12 -2
- package/dist/infrastructure/orchestration/claude-code-adapter.js +9 -1
- package/dist/infrastructure/orchestration/codex-adapter.js +6 -1
- package/dist/infrastructure/orchestration/conductor-adapter.js +12 -2
- package/dist/infrastructure/orchestration/gemini-adapter.js +41 -5
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +10 -2
- package/dist/infrastructure/orchestration/memory-injector.js +337 -0
- package/dist/infrastructure/orchestration/opencode-adapter.js +34 -4
- package/dist/infrastructure/orchestration/progress-aggregator.js +119 -0
- package/dist/infrastructure/orchestration/readiness.js +2 -5
- package/dist/infrastructure/orchestration/session-persistence.js +13 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +30 -3
- package/dist/infrastructure/protocols/ui-page.js +468 -59
- package/dist/presentation/cli/commands/init-command.js +342 -24
- package/dist/presentation/cli/commands/memory-command.js +1017 -0
- package/dist/presentation/cli/commands/protocols-builder.js +254 -0
- package/dist/presentation/cli/commands/protocols-command.js +84 -0
- package/dist/presentation/cli/commands/repo-map-builder.js +247 -0
- package/package.json +4 -3
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F3 Task 7 — protocols/hook_patterns.json exporter.
|
|
3
|
+
*
|
|
4
|
+
* Reads the trust-scorer's CODE_PATTERNS bank (the in-code TypeScript
|
|
5
|
+
* source of truth for the safety hook's pattern matchers) and exports it
|
|
6
|
+
* to .codehere/protocols/hook_patterns.json so the patterns are:
|
|
7
|
+
*
|
|
8
|
+
* - inspectable (auditors, security reviewers can read the JSON)
|
|
9
|
+
* - version-controllable (the file lives in the repo)
|
|
10
|
+
* - shareable (cross-tool: e.g. PR our patterns into the
|
|
11
|
+
* agentic-stack ecosystem per ADR-006)
|
|
12
|
+
* - extensible (project-specific patterns under `patterns_project[]`
|
|
13
|
+
* survive re-exports)
|
|
14
|
+
*
|
|
15
|
+
* Schema split (regenerable / preserved):
|
|
16
|
+
* patterns_builtin[] — regenerated from CODE_PATTERNS on each export
|
|
17
|
+
* patterns_project[] — user-edited; preserved across re-exports
|
|
18
|
+
*
|
|
19
|
+
* Each pattern is identified by a stable `id` slugified from category +
|
|
20
|
+
* description. Same code → same id, so users can reference patterns
|
|
21
|
+
* deterministically (e.g. "ignore the `secrets-hardcoded-credential`
|
|
22
|
+
* pattern in this folder").
|
|
23
|
+
*
|
|
24
|
+
* Forward-compatibility: regex is serialized as a JSON-portable string
|
|
25
|
+
* (no special characters that would break JSON parsing); the flags are
|
|
26
|
+
* a separate field. This avoids the JSON-vs-RegExp impedance mismatch.
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
29
|
+
import { join, dirname } from 'path';
|
|
30
|
+
import { CODE_PATTERNS } from '../../../infrastructure/security/trust-scorer.js';
|
|
31
|
+
/**
|
|
32
|
+
* Slugify category + description into a stable id.
|
|
33
|
+
*
|
|
34
|
+
* "Secrets" + "Hardcoded credential" → "secrets-hardcoded-credential"
|
|
35
|
+
* "SQL Injection" + "String interpolation in SQL query" → "sql-injection-string-interpolation-in-sql-query"
|
|
36
|
+
*
|
|
37
|
+
* Stability matters: re-exports must produce the same id from the same
|
|
38
|
+
* source. Cite this slug in test fixtures, downstream tools, project
|
|
39
|
+
* overrides etc.
|
|
40
|
+
*/
|
|
41
|
+
function slugifyPatternId(category, description) {
|
|
42
|
+
return (category + ' ' + description)
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
45
|
+
.replace(/^-+|-+$/g, '');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Serialize a JS RegExp to a JSON-portable {regex, flags} pair.
|
|
49
|
+
* `regex` is the source string (without surrounding `/`); `flags` is the
|
|
50
|
+
* flags string ('gi' etc.).
|
|
51
|
+
*/
|
|
52
|
+
function serializeRegex(re) {
|
|
53
|
+
return { regex: re.source, flags: re.flags };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Assign stable, source-order-independent ids to a list of patterns.
|
|
57
|
+
*
|
|
58
|
+
* The sort key is (category, description, regex.source) — a 3-tier
|
|
59
|
+
* comparator. The first two tiers come from K2 Round 1 (sort by
|
|
60
|
+
* category + description so duplicates get deterministic suffix
|
|
61
|
+
* assignment). The 3rd tier (regex source) is the codex Round 2 fix:
|
|
62
|
+
* (category, description) alone leaves stable-sort fall-through to
|
|
63
|
+
* source order when two patterns share both fields, which means
|
|
64
|
+
* reordering CODE_PATTERNS would still flip which one gets the base id
|
|
65
|
+
* vs `-2`. Adding regex.source disambiguates definitively — two patterns
|
|
66
|
+
* with identical category + description + regex are the SAME pattern;
|
|
67
|
+
* they can't both exist.
|
|
68
|
+
*
|
|
69
|
+
* Returns a new sorted array with `id` attached. Pure; safe to test
|
|
70
|
+
* with arbitrary fixtures.
|
|
71
|
+
*/
|
|
72
|
+
export function assignStableIds(patterns) {
|
|
73
|
+
const sorted = [...patterns].sort((a, b) => {
|
|
74
|
+
const c = a.category.localeCompare(b.category);
|
|
75
|
+
if (c !== 0)
|
|
76
|
+
return c;
|
|
77
|
+
const d = a.description.localeCompare(b.description);
|
|
78
|
+
if (d !== 0)
|
|
79
|
+
return d;
|
|
80
|
+
return a.regex.source.localeCompare(b.regex.source);
|
|
81
|
+
});
|
|
82
|
+
const seenIds = new Map();
|
|
83
|
+
return sorted.map((p) => {
|
|
84
|
+
const baseId = slugifyPatternId(p.category, p.description);
|
|
85
|
+
const seen = seenIds.get(baseId) ?? 0;
|
|
86
|
+
seenIds.set(baseId, seen + 1);
|
|
87
|
+
const id = seen === 0 ? baseId : `${baseId}-${seen + 1}`;
|
|
88
|
+
return { ...p, id };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build the export object from CODE_PATTERNS. Pure — does not touch disk.
|
|
93
|
+
*
|
|
94
|
+
* Delegates id assignment to `assignStableIds` so the source-order-
|
|
95
|
+
* independence property has a direct unit test (see protocols-export.test.ts).
|
|
96
|
+
*/
|
|
97
|
+
export function buildHookPatternsExport() {
|
|
98
|
+
const withIds = assignStableIds(CODE_PATTERNS);
|
|
99
|
+
const builtin = withIds.map((p) => {
|
|
100
|
+
const { regex, flags } = serializeRegex(p.regex);
|
|
101
|
+
const out = {
|
|
102
|
+
id: p.id,
|
|
103
|
+
regex,
|
|
104
|
+
regex_flags: flags,
|
|
105
|
+
severity: p.severity,
|
|
106
|
+
category: p.category,
|
|
107
|
+
description: p.description,
|
|
108
|
+
};
|
|
109
|
+
if (p.cwe)
|
|
110
|
+
out.cwe = p.cwe;
|
|
111
|
+
if (p.scanRaw)
|
|
112
|
+
out.scanRaw = p.scanRaw;
|
|
113
|
+
if (p.requiresContext) {
|
|
114
|
+
const ctx = serializeRegex(p.requiresContext);
|
|
115
|
+
out.requiresContext = { regex: ctx.regex, flags: ctx.flags };
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
version: 'v0.5',
|
|
121
|
+
generatedAt: new Date().toISOString(),
|
|
122
|
+
source: 'agent/src/infrastructure/security/trust-scorer.ts',
|
|
123
|
+
_note: "Exported from codehere's trust-scorer pattern bank via 'codehere protocols export'. " +
|
|
124
|
+
"DO NOT hand-edit `patterns_builtin[]` — it's regenerated on every export. " +
|
|
125
|
+
"Add project-specific patterns to `patterns_project[]` — those are preserved across re-exports. " +
|
|
126
|
+
"Pattern ids are stable slugs of category + description; reference them deterministically.",
|
|
127
|
+
patterns_builtin: builtin,
|
|
128
|
+
patterns_project: [],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Detect whether the on-disk file is the init-time placeholder.
|
|
133
|
+
*
|
|
134
|
+
* Two placeholder shapes are recognized:
|
|
135
|
+
*
|
|
136
|
+
* 1. POST-2026-04-28 seed (current init) — split schema, empty arrays:
|
|
137
|
+
* { version, patterns_builtin: [], patterns_project: [], _note }
|
|
138
|
+
*
|
|
139
|
+
* 2. PRE-2026-04-28 seed (legacy init) — single empty `patterns` array:
|
|
140
|
+
* { version, patterns: [], _note }
|
|
141
|
+
*
|
|
142
|
+
* Either one is safe to overwrite without --force. A NON-empty legacy
|
|
143
|
+
* `patterns` array is NOT a placeholder — see `extractLegacyProjectPatterns`
|
|
144
|
+
* below which migrates user content into `patterns_project[]` rather than
|
|
145
|
+
* silently discarding it (codex Round 1 fix — the legacy seed's _note
|
|
146
|
+
* explicitly told users to add patterns to the legacy field, so users may
|
|
147
|
+
* have followed that guidance).
|
|
148
|
+
*/
|
|
149
|
+
function isInitPlaceholder(parsed) {
|
|
150
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
151
|
+
return false;
|
|
152
|
+
const obj = parsed;
|
|
153
|
+
// Current shape: split schema, both arrays empty.
|
|
154
|
+
if (Array.isArray(obj.patterns_builtin) &&
|
|
155
|
+
obj.patterns_builtin.length === 0 &&
|
|
156
|
+
Array.isArray(obj.patterns_project) &&
|
|
157
|
+
obj.patterns_project.length === 0) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
// Legacy shape: single empty `patterns` array, no _builtin/_project.
|
|
161
|
+
if (Array.isArray(obj.patterns) &&
|
|
162
|
+
obj.patterns.length === 0 &&
|
|
163
|
+
obj.patterns_builtin === undefined &&
|
|
164
|
+
obj.patterns_project === undefined) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Extract user-added patterns from the LEGACY `patterns[]` field, if any.
|
|
171
|
+
* Used to migrate non-empty legacy files into `patterns_project[]` so users
|
|
172
|
+
* who followed the legacy seed's _note advice ("add patterns to `patterns`")
|
|
173
|
+
* don't lose their work on first export (codex Round 1 fix).
|
|
174
|
+
*
|
|
175
|
+
* Returns `null` if the file isn't a legacy-shape file with non-empty
|
|
176
|
+
* patterns; callers fall through to the normal --rebuild path which reads
|
|
177
|
+
* `patterns_project[]` directly.
|
|
178
|
+
*/
|
|
179
|
+
function extractLegacyProjectPatterns(parsed) {
|
|
180
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
181
|
+
return null;
|
|
182
|
+
const obj = parsed;
|
|
183
|
+
if (Array.isArray(obj.patterns) &&
|
|
184
|
+
obj.patterns.length > 0 &&
|
|
185
|
+
obj.patterns_builtin === undefined &&
|
|
186
|
+
obj.patterns_project === undefined) {
|
|
187
|
+
return obj.patterns;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Disk-side wrapper: build + write to .codehere/protocols/hook_patterns.json.
|
|
193
|
+
*
|
|
194
|
+
* If the existing file is the init-time placeholder, overwrite without
|
|
195
|
+
* requiring --rebuild. Otherwise:
|
|
196
|
+
* - existing file present + no --rebuild → 'exists' (refuse)
|
|
197
|
+
* - existing file present + --rebuild → preserve patterns_project[],
|
|
198
|
+
* regenerate patterns_builtin[]
|
|
199
|
+
* - no existing file → write fresh
|
|
200
|
+
*/
|
|
201
|
+
export function writeHookPatternsExport(codehereDir, options = {}) {
|
|
202
|
+
const targetPath = join(codehereDir, 'protocols/hook_patterns.json');
|
|
203
|
+
let preservedProject = [];
|
|
204
|
+
if (existsSync(targetPath)) {
|
|
205
|
+
let parsed;
|
|
206
|
+
try {
|
|
207
|
+
parsed = JSON.parse(readFileSync(targetPath, 'utf8'));
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return { result: 'failed', path: targetPath, error: 'existing file is not valid JSON' };
|
|
211
|
+
}
|
|
212
|
+
// Codex Round 1 fix: migrate non-empty legacy `patterns[]` content into
|
|
213
|
+
// `patterns_project[]` BEFORE the placeholder/rebuild branches. The
|
|
214
|
+
// legacy init seed's _note told users to add patterns to the legacy
|
|
215
|
+
// field; a fresh export must preserve any content they added rather
|
|
216
|
+
// than silently discard it.
|
|
217
|
+
const legacy = extractLegacyProjectPatterns(parsed);
|
|
218
|
+
if (legacy !== null) {
|
|
219
|
+
preservedProject = legacy;
|
|
220
|
+
// Fall through to writing — we treat a non-empty-legacy file as
|
|
221
|
+
// overwriteable (the legacy patterns now live in preservedProject).
|
|
222
|
+
}
|
|
223
|
+
else if (isInitPlaceholder(parsed)) {
|
|
224
|
+
// Empty placeholder (either schema shape) — overwrite without --rebuild.
|
|
225
|
+
}
|
|
226
|
+
else if (!options.rebuild) {
|
|
227
|
+
return { result: 'exists', path: targetPath };
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
// --rebuild + non-placeholder, non-legacy existing file → preserve
|
|
231
|
+
// patterns_project[].
|
|
232
|
+
const obj = parsed;
|
|
233
|
+
if (Array.isArray(obj.patterns_project)) {
|
|
234
|
+
preservedProject = obj.patterns_project;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const built = buildHookPatternsExport();
|
|
239
|
+
built.patterns_project = preservedProject;
|
|
240
|
+
try {
|
|
241
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
242
|
+
writeFileSync(targetPath, JSON.stringify(built, null, 2) + '\n');
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
return { result: 'failed', path: targetPath, error: e instanceof Error ? e.message : String(e) };
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
result: 'exported',
|
|
249
|
+
path: targetPath,
|
|
250
|
+
builtinCount: built.patterns_builtin.length,
|
|
251
|
+
projectCount: built.patterns_project.length,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=protocols-builder.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocols Command — F3 v0.5 protocols layer operations.
|
|
3
|
+
*
|
|
4
|
+
* Today: `codehere protocols export` writes the trust-scorer's pattern
|
|
5
|
+
* bank to .codehere/protocols/hook_patterns.json. Future: `protocols
|
|
6
|
+
* import`, `protocols verify`, `protocols diff` etc.
|
|
7
|
+
*
|
|
8
|
+
* The dispatch shape mirrors `memory-command.ts` so future actions slot
|
|
9
|
+
* in without touching index.ts beyond a single .option() addition.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { colors, success, info, error, newline } from '../../../ui.js';
|
|
14
|
+
import { findCodebaseRoot } from '../../../domain/services/codebase-detector.js';
|
|
15
|
+
import { writeHookPatternsExport } from './protocols-builder.js';
|
|
16
|
+
function executeProtocolsExport(options) {
|
|
17
|
+
const codeInfo = findCodebaseRoot();
|
|
18
|
+
const root = codeInfo?.rootPath ?? process.cwd();
|
|
19
|
+
const codehereDir = join(root, '.codehere');
|
|
20
|
+
newline();
|
|
21
|
+
console.log(colors.bold('codehere protocols export'));
|
|
22
|
+
console.log(colors.dim(` exporting trust-scorer pattern bank → ${codehereDir}/protocols/hook_patterns.json`));
|
|
23
|
+
newline();
|
|
24
|
+
if (!existsSync(codehereDir)) {
|
|
25
|
+
console.log(` ${error('refused')} .codehere/ not found at this root.`);
|
|
26
|
+
console.log(` ${colors.dim('Run')} ${colors.cyan('codehere init')} ${colors.dim('first.')}`);
|
|
27
|
+
newline();
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const result = writeHookPatternsExport(codehereDir, { rebuild: !!options.rebuild });
|
|
32
|
+
switch (result.result) {
|
|
33
|
+
case 'exported': {
|
|
34
|
+
const projectNote = result.projectCount && result.projectCount > 0
|
|
35
|
+
? ` · ${result.projectCount} project pattern${result.projectCount === 1 ? '' : 's'} preserved`
|
|
36
|
+
: '';
|
|
37
|
+
console.log(` ${success('exported')} ${result.path} ${colors.dim(`(${result.builtinCount} built-in patterns${projectNote})`)}`);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case 'exists':
|
|
41
|
+
console.log(` ${info('exists ')} ${result.path} ${colors.dim('(use --rebuild to refresh built-in patterns; project patterns are preserved)')}`);
|
|
42
|
+
break;
|
|
43
|
+
case 'failed':
|
|
44
|
+
console.log(` ${error('failed ')} ${result.path} ${colors.dim(`(${result.error})`)}`);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
newline();
|
|
49
|
+
console.log(colors.bold('Next:'));
|
|
50
|
+
console.log(` - Inspect ${colors.cyan('.codehere/protocols/hook_patterns.json')} to see what the safety hook matches`);
|
|
51
|
+
console.log(` - Add project-specific patterns under ${colors.cyan('patterns_project[]')} — they survive re-exports`);
|
|
52
|
+
console.log(` ${colors.dim('(Edit patterns_builtin[] is a no-op — re-exports overwrite it from trust-scorer.ts.)')}`);
|
|
53
|
+
newline();
|
|
54
|
+
}
|
|
55
|
+
export function executeProtocolsCommand(action, options = {}) {
|
|
56
|
+
try {
|
|
57
|
+
switch (action) {
|
|
58
|
+
case 'export':
|
|
59
|
+
executeProtocolsExport(options);
|
|
60
|
+
return;
|
|
61
|
+
case undefined:
|
|
62
|
+
case 'help':
|
|
63
|
+
console.log(`
|
|
64
|
+
codehere protocols <action> — F3 protocols layer operations
|
|
65
|
+
|
|
66
|
+
export Export trust-scorer pattern bank to .codehere/protocols/hook_patterns.json
|
|
67
|
+
|
|
68
|
+
Coming later:
|
|
69
|
+
import Apply patterns from a file (cross-tool ingestion)
|
|
70
|
+
verify Check that the on-disk file matches the in-code pattern bank
|
|
71
|
+
`);
|
|
72
|
+
return;
|
|
73
|
+
default:
|
|
74
|
+
console.error(`\n ${error('failed')} unknown protocols action: ${action}\n Try: codehere protocols help\n`);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
80
|
+
console.error(`\n ${error('failed')} codehere protocols ${action ?? ''} — ${msg}\n`);
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=protocols-command.js.map
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F3 Task 4 — repo_map.json builder (v0.5 TS-native MVP).
|
|
3
|
+
*
|
|
4
|
+
* v0.5 SCOPE:
|
|
5
|
+
* - Walks `git ls-files` (respects .gitignore via git itself).
|
|
6
|
+
* - For .ts/.tsx/.js/.jsx/.py files, regex-extracts top-level public-surface
|
|
7
|
+
* symbols (export function/class/const/interface/type, def/class for Python).
|
|
8
|
+
* - Reference count via `git grep -c` per symbol name.
|
|
9
|
+
* - Writes JSON to .codehere/memory/episodic/repo_map.json.
|
|
10
|
+
*
|
|
11
|
+
* v0.6 PLAN:
|
|
12
|
+
* - Replace the regex extractor with proper tree-sitter parsing
|
|
13
|
+
* (Aider's repomap.py MIT port — Python helper + TS shell-out, OR
|
|
14
|
+
* a TS-native tree-sitter library evaluation).
|
|
15
|
+
* - Add PageRank-style symbol importance (currently raw reference count).
|
|
16
|
+
* - Incremental rebuild via the per-file `hash` field already in the schema.
|
|
17
|
+
*
|
|
18
|
+
* Schema is forward-compatible — only the extractor implementation changes.
|
|
19
|
+
*
|
|
20
|
+
* Why regex MVP for v0.5:
|
|
21
|
+
* - Zero new deps (tree-sitter would mean Python + 5 packages OR a 30+ MB
|
|
22
|
+
* TS dep). codehere is bootstrap-zero-cost; every dep is friction.
|
|
23
|
+
* - Closes the F3 episodic/repo_map.json layer enough for spawned agents
|
|
24
|
+
* to read it as a starting context block.
|
|
25
|
+
* - Honest about limitations — see schema._note in the output JSON.
|
|
26
|
+
*/
|
|
27
|
+
import { spawnSync } from 'child_process';
|
|
28
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs';
|
|
29
|
+
import { createHash } from 'crypto';
|
|
30
|
+
import { join, dirname, extname } from 'path';
|
|
31
|
+
const SUPPORTED_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.py']);
|
|
32
|
+
/**
|
|
33
|
+
* Per-language extractors. v0.5 regex-based — line-anchored to avoid
|
|
34
|
+
* matching identifiers inside string literals. Returns 1-indexed line numbers.
|
|
35
|
+
*
|
|
36
|
+
* v0.6 will swap these for AST-based extractors. The interface stays the
|
|
37
|
+
* same: (body) → RepoMapSymbol[].
|
|
38
|
+
*/
|
|
39
|
+
function extractTsLikeSymbols(body) {
|
|
40
|
+
const lines = body.split('\n');
|
|
41
|
+
const symbols = [];
|
|
42
|
+
// export function NAME( | export async function NAME(
|
|
43
|
+
// export class NAME
|
|
44
|
+
// export const NAME = | export let NAME = | export var NAME =
|
|
45
|
+
// export interface NAME { | export type NAME =
|
|
46
|
+
const PATTERNS = [
|
|
47
|
+
[/^export\s+(?:async\s+)?function\s+(\w+)\s*[<(]/, 'function'],
|
|
48
|
+
[/^export\s+(?:abstract\s+)?class\s+(\w+)/, 'class'],
|
|
49
|
+
[/^export\s+(?:const|let|var)\s+(\w+)\s*[:=]/, 'const'],
|
|
50
|
+
[/^export\s+interface\s+(\w+)/, 'interface'],
|
|
51
|
+
[/^export\s+type\s+(\w+)\s*=/, 'type'],
|
|
52
|
+
[/^export\s+enum\s+(\w+)/, 'enum'],
|
|
53
|
+
];
|
|
54
|
+
for (let i = 0; i < lines.length; i++) {
|
|
55
|
+
for (const [re, kind] of PATTERNS) {
|
|
56
|
+
const m = lines[i].match(re);
|
|
57
|
+
if (m) {
|
|
58
|
+
symbols.push({ kind, name: m[1], line: i + 1 });
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return symbols;
|
|
64
|
+
}
|
|
65
|
+
function extractPythonSymbols(body) {
|
|
66
|
+
const lines = body.split('\n');
|
|
67
|
+
const symbols = [];
|
|
68
|
+
// Top-level (column 0) def NAME( | class NAME( | class NAME:
|
|
69
|
+
const PATTERNS = [
|
|
70
|
+
[/^def\s+(\w+)\s*\(/, 'def'],
|
|
71
|
+
[/^class\s+(\w+)\s*[(:]/, 'class'],
|
|
72
|
+
];
|
|
73
|
+
for (let i = 0; i < lines.length; i++) {
|
|
74
|
+
for (const [re, kind] of PATTERNS) {
|
|
75
|
+
const m = lines[i].match(re);
|
|
76
|
+
if (m) {
|
|
77
|
+
symbols.push({ kind, name: m[1], line: i + 1 });
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return symbols;
|
|
83
|
+
}
|
|
84
|
+
function extractSymbols(filePath, body) {
|
|
85
|
+
const ext = extname(filePath);
|
|
86
|
+
if (ext === '.py')
|
|
87
|
+
return extractPythonSymbols(body);
|
|
88
|
+
if (SUPPORTED_EXTENSIONS.has(ext))
|
|
89
|
+
return extractTsLikeSymbols(body);
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Hash file content for incremental-rebuild support in v0.6.
|
|
94
|
+
* Format: `sha256:<64-hex>` so the algorithm is self-describing.
|
|
95
|
+
*/
|
|
96
|
+
function hashContent(body) {
|
|
97
|
+
return 'sha256:' + createHash('sha256').update(body).digest('hex');
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* `git ls-files` returns paths relative to the repo root. Filters out
|
|
101
|
+
* non-supported extensions in one pass. If `git` isn't a repo, returns null
|
|
102
|
+
* so the caller can refuse with a clear message.
|
|
103
|
+
*/
|
|
104
|
+
function gitListFiles(repoRoot) {
|
|
105
|
+
const r = spawnSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf-8' });
|
|
106
|
+
if (r.status !== 0)
|
|
107
|
+
return null;
|
|
108
|
+
return r.stdout
|
|
109
|
+
.split('\0')
|
|
110
|
+
.filter(p => p.length > 0)
|
|
111
|
+
.filter(p => SUPPORTED_EXTENSIONS.has(extname(p)));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Count references to a symbol name across the repo via git grep.
|
|
115
|
+
* `-c` returns "<file>:<count>" per matching file; sum the counts.
|
|
116
|
+
*
|
|
117
|
+
* Uses word-boundary regex so `popular` doesn't match `unpopular`. The grep
|
|
118
|
+
* finds DEFINITIONS too, so subtract the definition count to get pure refs.
|
|
119
|
+
*/
|
|
120
|
+
function countReferences(repoRoot, symbolName) {
|
|
121
|
+
// -F = fixed string, -w = word boundary. Combined with grep itself.
|
|
122
|
+
const r = spawnSync('git', ['grep', '-c', '-w', '-F', '--', symbolName], { cwd: repoRoot, encoding: 'utf-8' });
|
|
123
|
+
if (r.status !== 0 && r.status !== 1)
|
|
124
|
+
return 0;
|
|
125
|
+
let total = 0;
|
|
126
|
+
for (const line of r.stdout.split('\n')) {
|
|
127
|
+
const m = line.match(/:(\d+)$/);
|
|
128
|
+
if (m)
|
|
129
|
+
total += parseInt(m[1], 10);
|
|
130
|
+
}
|
|
131
|
+
// Subtract 1 for the definition line itself (best-effort — undercounts if
|
|
132
|
+
// the symbol is defined in multiple files; v0.6 with AST will be accurate).
|
|
133
|
+
return Math.max(0, total - 1);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Build the repo_map for the repo at `repoRoot`. Returns the JSON object —
|
|
137
|
+
* caller writes to disk. Pure-ish (still spawns git), no codehere-specific
|
|
138
|
+
* paths inside.
|
|
139
|
+
*
|
|
140
|
+
* The `_stats` block in the returned RepoMap persists to disk so downstream
|
|
141
|
+
* agents know whether the map is incomplete (e.g. >1MB files skipped during
|
|
142
|
+
* walk).
|
|
143
|
+
*/
|
|
144
|
+
export function buildRepoMap(repoRoot) {
|
|
145
|
+
if (!existsSync(join(repoRoot, '.git'))) {
|
|
146
|
+
return { error: 'not a git repo (run `git init` first)' };
|
|
147
|
+
}
|
|
148
|
+
const files = gitListFiles(repoRoot);
|
|
149
|
+
if (files === null) {
|
|
150
|
+
return { error: 'git ls-files failed' };
|
|
151
|
+
}
|
|
152
|
+
const fileMap = {};
|
|
153
|
+
const allSymbols = [];
|
|
154
|
+
let skippedLargeFiles = 0;
|
|
155
|
+
for (const rel of files) {
|
|
156
|
+
const abs = join(repoRoot, rel);
|
|
157
|
+
// Skip giant files (>1MB) — surfaced in the result so users see the count.
|
|
158
|
+
try {
|
|
159
|
+
if (statSync(abs).size > 1_000_000) {
|
|
160
|
+
skippedLargeFiles++;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
let body;
|
|
168
|
+
try {
|
|
169
|
+
body = readFileSync(abs, 'utf-8');
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// File deleted between ls-files and read — skip.
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const symbols = extractSymbols(rel, body);
|
|
176
|
+
fileMap[rel] = {
|
|
177
|
+
hash: hashContent(body),
|
|
178
|
+
symbols,
|
|
179
|
+
};
|
|
180
|
+
for (const s of symbols) {
|
|
181
|
+
allSymbols.push({ name: s.name, file: rel, line: s.line });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Rank by reference count.
|
|
185
|
+
//
|
|
186
|
+
// codex Round 1 fix: the previous version applied .slice(0, 200) BEFORE
|
|
187
|
+
// grepping, which meant "first 200 discovered names" not "top 200
|
|
188
|
+
// candidates." Symbols defined in files traversed late silently got
|
|
189
|
+
// references=0 and dropped out of topSymbols, regardless of how heavily
|
|
190
|
+
// they were referenced. Now we grep ALL unique names. O(N) git greps is
|
|
191
|
+
// acceptable for v0.5 — codehere's own 249-symbol repo runs in ~5s.
|
|
192
|
+
// v0.6's tree-sitter port will swap raw grep for AST-aware reference
|
|
193
|
+
// resolution + PageRank, which is faster AND more accurate.
|
|
194
|
+
const uniqueNames = Array.from(new Set(allSymbols.map(s => s.name)));
|
|
195
|
+
const refCounts = new Map();
|
|
196
|
+
for (const name of uniqueNames) {
|
|
197
|
+
refCounts.set(name, countReferences(repoRoot, name));
|
|
198
|
+
}
|
|
199
|
+
const topSymbols = allSymbols
|
|
200
|
+
.map(s => ({ ...s, references: refCounts.get(s.name) ?? 0 }))
|
|
201
|
+
// Stable sort: references DESC, then name ASC for tie-breaking.
|
|
202
|
+
.sort((a, b) => b.references - a.references || a.name.localeCompare(b.name))
|
|
203
|
+
.slice(0, 50);
|
|
204
|
+
return {
|
|
205
|
+
version: 'v0.5',
|
|
206
|
+
generatedAt: new Date().toISOString(),
|
|
207
|
+
rootPath: repoRoot,
|
|
208
|
+
_note: 'TS-native regex MVP — KNOWN MISSES (v0.5, addressed in v0.6 tree-sitter port): ' +
|
|
209
|
+
'`export default function/class` (no name on the export line), ' +
|
|
210
|
+
'`export { foo, bar }` blocks, `export { foo } from "..."` re-exports, ' +
|
|
211
|
+
'`export * from "..."` star re-exports, multi-line `export { ... }` blocks, ' +
|
|
212
|
+
'`export declare`, CommonJS `module.exports` / `exports.X` assignments, ' +
|
|
213
|
+
'symbols not at column 0 in Python. Reference counts via word-bounded ' +
|
|
214
|
+
'git grep undercount when a name is defined in multiple files. ' +
|
|
215
|
+
'Read `_stats.skippedLargeFiles` to detect files left out of the walk.',
|
|
216
|
+
_stats: { skippedLargeFiles },
|
|
217
|
+
files: fileMap,
|
|
218
|
+
topSymbols,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Disk-side wrapper: build + write to .codehere/memory/episodic/repo_map.json.
|
|
223
|
+
*
|
|
224
|
+
* `_stats` is preserved in the on-disk JSON (codex Round 2 fix) so spawned
|
|
225
|
+
* agents reading the map can tell "no symbols in this file" from "this file
|
|
226
|
+
* was skipped during the walk." The CLI also surfaces it on the return value.
|
|
227
|
+
*/
|
|
228
|
+
export function writeRepoMap(repoRoot, codehereDir, options = {}) {
|
|
229
|
+
const targetPath = join(codehereDir, 'memory/episodic/repo_map.json');
|
|
230
|
+
if (existsSync(targetPath) && !options.rebuild) {
|
|
231
|
+
return { result: 'exists', path: targetPath };
|
|
232
|
+
}
|
|
233
|
+
const built = buildRepoMap(repoRoot);
|
|
234
|
+
if ('error' in built) {
|
|
235
|
+
return { result: 'failed', path: targetPath, error: built.error };
|
|
236
|
+
}
|
|
237
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
238
|
+
writeFileSync(targetPath, JSON.stringify(built, null, 2) + '\n');
|
|
239
|
+
return {
|
|
240
|
+
result: 'written',
|
|
241
|
+
path: targetPath,
|
|
242
|
+
fileCount: Object.keys(built.files).length,
|
|
243
|
+
symbolCount: Object.values(built.files).reduce((acc, f) => acc + f.symbols.length, 0),
|
|
244
|
+
skippedLargeFiles: built._stats.skippedLargeFiles,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=repo-map-builder.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codehere",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A PM for your AI coding agents. Delegate, orchestrate, and audit Claude Code, Codex, Aider, and
|
|
3
|
+
"version": "0.5.0-alpha.2",
|
|
4
|
+
"description": "A PM for your AI coding agents. Delegate, orchestrate, and audit Claude Code, Codex, Aider, OpenCode, Gemini, and any user-configured CLI agent from one local web UI — every AI action traced, every file scored, every spawn warmed with project memory.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"lint": "eslint src/ --ext .ts",
|
|
30
30
|
"format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'",
|
|
31
31
|
"format:check": "prettier --check 'src/**/*.ts' 'test/**/*.ts'",
|
|
32
|
-
"sbom": "npm sbom --sbom-format=spdx --omit=dev"
|
|
32
|
+
"sbom": "npm sbom --sbom-format=spdx --omit=dev",
|
|
33
|
+
"pretag:check": "../scripts/pretag-npm-check.sh"
|
|
33
34
|
},
|
|
34
35
|
"keywords": [
|
|
35
36
|
"ai-coding-agents",
|