codehere 0.6.0-alpha.1 → 0.6.1
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 +8 -8
- package/dist/index.js +41 -1
- package/dist/infrastructure/hooks/claude-sdk-hook.js +44 -2
- package/dist/infrastructure/orchestration/context-handoff.js +143 -0
- package/dist/infrastructure/orchestration/failover-config.js +77 -0
- package/dist/infrastructure/orchestration/graphify-context.js +256 -0
- package/dist/infrastructure/orchestration/opencode-adapter.js +41 -0
- package/dist/infrastructure/orchestration/session-manager.js +497 -6
- package/dist/infrastructure/protocols/cloud-sync.js +208 -0
- package/dist/infrastructure/protocols/mcp-server-impl.js +447 -311
- package/dist/infrastructure/protocols/ui-page-v2.js +505 -0
- package/dist/infrastructure/protocols/ui-page.js +303 -37
- package/dist/infrastructure/storage/project-thread-mirror.js +25 -15
- package/dist/presentation/cli/repl.js +29 -0
- package/package.json +3 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graphify-context — shared structural memory layer (P1).
|
|
3
|
+
*
|
|
4
|
+
* Wraps `graphify-ts` CLI to compile compact context packs for agent prompts.
|
|
5
|
+
* Every spawn can optionally get a graphify context pack that replaces
|
|
6
|
+
* raw file reads + unstructured JSONL scans.
|
|
7
|
+
*
|
|
8
|
+
* All functions are synchronous (execFileSync) to slot into the sync
|
|
9
|
+
* spawn() call chain without breaking the API.
|
|
10
|
+
*
|
|
11
|
+
* P1 integration points:
|
|
12
|
+
* - Session spawn: inject context pack into prompt prefix
|
|
13
|
+
* - Failover handoff (P2): replacement agent inherits the same pack
|
|
14
|
+
* - S7 compaction (future): use graph to prune stale session logs
|
|
15
|
+
* - PM-brain routing (future): community_overview for agent selection
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync } from 'child_process';
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
19
|
+
import { join } from 'path';
|
|
20
|
+
import { randomUUID } from 'crypto';
|
|
21
|
+
// ── Constants ──────────────────────────────────────────────────────────────────
|
|
22
|
+
const GRAPHIFY_OUT = 'graphify-out';
|
|
23
|
+
const GRAPH_JSON = 'graph.json';
|
|
24
|
+
const CONTEXT_PACKS_DIR = '.codehere';
|
|
25
|
+
const CONTEXT_PACKS_FILE = 'context-packs.jsonl';
|
|
26
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
27
|
+
function getGraphPath(cwd) {
|
|
28
|
+
return join(cwd, GRAPHIFY_OUT, GRAPH_JSON);
|
|
29
|
+
}
|
|
30
|
+
function getGraphStats(cwd) {
|
|
31
|
+
const graphPath = getGraphPath(cwd);
|
|
32
|
+
try {
|
|
33
|
+
if (!existsSync(graphPath)) {
|
|
34
|
+
return { nodeCount: 0, edgeCount: 0 };
|
|
35
|
+
}
|
|
36
|
+
const graphJson = JSON.parse(readFileSync(graphPath, 'utf-8'));
|
|
37
|
+
return {
|
|
38
|
+
nodeCount: graphJson.nodes?.length ?? 0,
|
|
39
|
+
edgeCount: graphJson.edges?.length ?? 0,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { nodeCount: 0, edgeCount: 0 };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// ── Graph management (sync) ────────────────────────────────────────────────────
|
|
47
|
+
/**
|
|
48
|
+
* Build or update the knowledge graph for a workspace directory.
|
|
49
|
+
* Writes to <cwd>/graphify-out/graph.json (graphify-ts default).
|
|
50
|
+
* Synchronous — runs synchronously so it can be called from spawn().
|
|
51
|
+
*/
|
|
52
|
+
export function buildGraphSync(cwd, { incremental = true } = {}) {
|
|
53
|
+
const startTime = Date.now();
|
|
54
|
+
const graphPath = getGraphPath(cwd);
|
|
55
|
+
const args = ['generate', cwd];
|
|
56
|
+
if (incremental && existsSync(graphPath)) {
|
|
57
|
+
args.push('--update');
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
execFileSync('npx', ['graphify-ts', ...args], {
|
|
61
|
+
cwd,
|
|
62
|
+
stdio: 'ignore',
|
|
63
|
+
timeout: 120_000,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Non-zero exit is fine — graphify-ts returns 1 when no files changed.
|
|
68
|
+
// Also swallow ENOENT (graphify-ts not installed) — degrade gracefully.
|
|
69
|
+
}
|
|
70
|
+
return { elapsedMs: Date.now() - startTime };
|
|
71
|
+
}
|
|
72
|
+
// ── Context pack compilation (sync) ────────────────────────────────────────────
|
|
73
|
+
/**
|
|
74
|
+
* Compile a compact context pack for a prompt + workspace.
|
|
75
|
+
* Synchronous — calls graphify-ts pack and returns the payload.
|
|
76
|
+
*/
|
|
77
|
+
export function compileContextPackSync(prompt, cwd, { task = 'explain', budget = 3000, } = {}) {
|
|
78
|
+
const startTime = Date.now();
|
|
79
|
+
const graphPath = getGraphPath(cwd);
|
|
80
|
+
// Build graph if it doesn't exist yet
|
|
81
|
+
if (!existsSync(graphPath)) {
|
|
82
|
+
buildGraphSync(cwd, { incremental: false });
|
|
83
|
+
}
|
|
84
|
+
// Degrade gracefully if graph still doesn't exist
|
|
85
|
+
if (!existsSync(graphPath)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const output = execFileSync('npx', [
|
|
90
|
+
'graphify-ts', 'pack', prompt,
|
|
91
|
+
'--graph', graphPath,
|
|
92
|
+
'--task', task,
|
|
93
|
+
'--budget', String(budget),
|
|
94
|
+
], {
|
|
95
|
+
cwd,
|
|
96
|
+
encoding: 'utf-8',
|
|
97
|
+
maxBuffer: 512 * 1024,
|
|
98
|
+
timeout: 30_000,
|
|
99
|
+
});
|
|
100
|
+
const { nodeCount, edgeCount } = getGraphStats(cwd);
|
|
101
|
+
return {
|
|
102
|
+
graphPath,
|
|
103
|
+
payload: output.trim(),
|
|
104
|
+
budget,
|
|
105
|
+
task,
|
|
106
|
+
nodeCount,
|
|
107
|
+
edgeCount,
|
|
108
|
+
elapsedMs: Date.now() - startTime,
|
|
109
|
+
packId: randomUUID(),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// graphify-ts may return non-zero or hit timeout — degrade gracefully.
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Compile a context pack from a session's final state (prompt + artifacts + steps).
|
|
119
|
+
* Used for P1 session-exit persistence and P2 handoff enrichment.
|
|
120
|
+
*/
|
|
121
|
+
export function compileSessionPackSync(cwd, { prompt, artifacts, steps, agent }) {
|
|
122
|
+
// Combine prompt + artifacts + steps into a rich prompt for the pack
|
|
123
|
+
const sessionContext = [
|
|
124
|
+
prompt,
|
|
125
|
+
agent ? `Agent: ${agent}` : undefined,
|
|
126
|
+
artifacts ? `Artifacts:\n${artifacts}` : undefined,
|
|
127
|
+
steps ? `Steps taken:\n${steps}` : undefined,
|
|
128
|
+
].filter(Boolean).join('\n\n');
|
|
129
|
+
return compileContextPackSync(sessionContext, cwd, { task: 'impact', budget: 2500 });
|
|
130
|
+
}
|
|
131
|
+
// ── Context pack persistence ───────────────────────────────────────────────────
|
|
132
|
+
/**
|
|
133
|
+
* Persist a context pack to disk for cross-session handoff (P2).
|
|
134
|
+
* Returns the path to the packs file.
|
|
135
|
+
*/
|
|
136
|
+
export function persistContextPack(pack, cwd) {
|
|
137
|
+
const packsDir = join(cwd, CONTEXT_PACKS_DIR);
|
|
138
|
+
if (!existsSync(packsDir)) {
|
|
139
|
+
mkdirSync(packsDir, { recursive: true });
|
|
140
|
+
}
|
|
141
|
+
const packsFile = join(packsDir, CONTEXT_PACKS_FILE);
|
|
142
|
+
const entry = {
|
|
143
|
+
packId: pack.packId,
|
|
144
|
+
timestamp: Date.now(),
|
|
145
|
+
task: pack.task,
|
|
146
|
+
nodeCount: pack.nodeCount,
|
|
147
|
+
edgeCount: pack.edgeCount,
|
|
148
|
+
graphPath: pack.graphPath,
|
|
149
|
+
payloadPreview: pack.payload.slice(0, 500), // truncate for log
|
|
150
|
+
payloadLength: pack.payload.length,
|
|
151
|
+
};
|
|
152
|
+
writeFileSync(packsFile, JSON.stringify(entry) + '\n', { flag: 'a' });
|
|
153
|
+
return packsFile;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Load the most recent context pack from disk (for P2 handoff).
|
|
157
|
+
*/
|
|
158
|
+
export function loadRecentContextPack(cwd) {
|
|
159
|
+
const packsFile = join(cwd, CONTEXT_PACKS_DIR, CONTEXT_PACKS_FILE);
|
|
160
|
+
if (!existsSync(packsFile)) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const lines = readFileSync(packsFile, 'utf-8').split('\n').filter(Boolean);
|
|
165
|
+
if (lines.length === 0)
|
|
166
|
+
return null;
|
|
167
|
+
const lastEntry = JSON.parse(lines[lines.length - 1]);
|
|
168
|
+
return {
|
|
169
|
+
packId: lastEntry.packId,
|
|
170
|
+
graphPath: lastEntry.graphPath,
|
|
171
|
+
payload: '', // payload is truncated in disk storage
|
|
172
|
+
budget: 3000,
|
|
173
|
+
task: lastEntry.task,
|
|
174
|
+
nodeCount: lastEntry.nodeCount,
|
|
175
|
+
edgeCount: lastEntry.edgeCount,
|
|
176
|
+
elapsedMs: 0,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// ── Prompt enrichment (sync) ───────────────────────────────────────────────────
|
|
184
|
+
/**
|
|
185
|
+
* Prepend a context pack to a prompt if graphify-ts is available.
|
|
186
|
+
*
|
|
187
|
+
* Synchronous — can be called directly from spawn().
|
|
188
|
+
* Returns the original prompt if graphify-ts fails or is unavailable.
|
|
189
|
+
*
|
|
190
|
+
* Respects CODEHERE_GRAPHIFY env var:
|
|
191
|
+
* - unset or "auto" (default): compile on spawn if graph doesn't exist
|
|
192
|
+
* - "force": always compile, even if pack exists
|
|
193
|
+
* - "off": skip entirely
|
|
194
|
+
*/
|
|
195
|
+
export function enrichPromptWithPackSync(prompt, cwd, { force = false } = {}) {
|
|
196
|
+
const mode = process.env.CODEHERE_GRAPHIFY ?? 'auto';
|
|
197
|
+
if (mode === 'off')
|
|
198
|
+
return prompt;
|
|
199
|
+
// Check for existing pack to avoid recompiling on every spawn
|
|
200
|
+
const existingPack = loadRecentContextPack(cwd);
|
|
201
|
+
const graphPath = getGraphPath(cwd);
|
|
202
|
+
const graphExists = existsSync(graphPath);
|
|
203
|
+
if (!force && existingPack && graphExists) {
|
|
204
|
+
// Reuse existing pack — just enrich the prompt with a compact reference
|
|
205
|
+
const reference = `[graphify-ts context pack: ${existingPack.nodeCount} nodes, ${existingPack.edgeCount} edges, pack=${existingPack.packId}]`;
|
|
206
|
+
return `[graphify context: ${reference} — agent should call retrieve() for full pack]\n\n${prompt}`;
|
|
207
|
+
}
|
|
208
|
+
// Compile a fresh pack
|
|
209
|
+
const pack = compileContextPackSync(prompt, cwd, { task: 'explain', budget: 2500 });
|
|
210
|
+
if (!pack) {
|
|
211
|
+
return prompt;
|
|
212
|
+
}
|
|
213
|
+
// Persist for P2 handoff
|
|
214
|
+
persistContextPack(pack, cwd);
|
|
215
|
+
// Prepend context pack as a system hint so the agent sees it first
|
|
216
|
+
return [
|
|
217
|
+
`[graphify-ts context pack — ${pack.nodeCount} nodes, ${pack.edgeCount} edges, ${pack.elapsedMs}ms]`,
|
|
218
|
+
pack.payload,
|
|
219
|
+
'---',
|
|
220
|
+
prompt,
|
|
221
|
+
].join('\n\n');
|
|
222
|
+
}
|
|
223
|
+
// ── Graph querying (sync) ──────────────────────────────────────────────────────
|
|
224
|
+
/**
|
|
225
|
+
* Query the knowledge graph for a question.
|
|
226
|
+
* Useful for PM-brain routing and structured context handoff (P2).
|
|
227
|
+
*/
|
|
228
|
+
export function queryGraphSync(question, cwd, { maxTokens = 2000 } = {}) {
|
|
229
|
+
const startTime = Date.now();
|
|
230
|
+
const graphPath = getGraphPath(cwd);
|
|
231
|
+
if (!existsSync(graphPath)) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const output = execFileSync('npx', [
|
|
236
|
+
'graphify-ts', 'query', question,
|
|
237
|
+
'--graph', graphPath,
|
|
238
|
+
'--budget', String(maxTokens),
|
|
239
|
+
], {
|
|
240
|
+
cwd,
|
|
241
|
+
encoding: 'utf-8',
|
|
242
|
+
maxBuffer: 256 * 1024,
|
|
243
|
+
timeout: 15_000,
|
|
244
|
+
});
|
|
245
|
+
return {
|
|
246
|
+
question,
|
|
247
|
+
answer: output.trim(),
|
|
248
|
+
elapsedMs: Date.now() - startTime,
|
|
249
|
+
graphPath,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=graphify-context.js.map
|
|
@@ -19,6 +19,7 @@ import { join } from 'path';
|
|
|
19
19
|
import { promisify } from 'util';
|
|
20
20
|
import { ProgressAggregator } from './progress-aggregator.js';
|
|
21
21
|
import { applyMemoryPrefix } from './memory-injector.js';
|
|
22
|
+
import { mirrorToolCallToAudit } from '../hooks/mirror-tool-call-to-audit.js';
|
|
22
23
|
import { snapshotCwd, extractArtifacts } from './artifacts-extractor.js';
|
|
23
24
|
const execFileAsync = promisify(execFile);
|
|
24
25
|
const HEADLESS_FLAGS = ['run', '--format', 'json'];
|
|
@@ -142,6 +143,46 @@ export function spawnOpenCode(prompt, options = {}) {
|
|
|
142
143
|
if (txt)
|
|
143
144
|
textAccum += txt + '\n';
|
|
144
145
|
}
|
|
146
|
+
// #114 — cross-vendor audit mirror for opencode (closes the gap left by
|
|
147
|
+
// #110 slice 4, which wired Codex/Aider/Gemini but deferred opencode).
|
|
148
|
+
// OpenCode's `run --format json` emits `tool_use` events with shape
|
|
149
|
+
// { type: 'tool_use', part: { tool: 'bash'|'read'|'write'|..., state:
|
|
150
|
+
// { input: {...}, output, status, ... } } } (verified via deepwiki +
|
|
151
|
+
// takopi cheatsheet). Same post-hoc visibility surface as Codex/Aider/
|
|
152
|
+
// Gemini — no synchronous block until upstream pre-tool hooks land.
|
|
153
|
+
if (event.type === 'tool_use') {
|
|
154
|
+
const part = event.data?.part;
|
|
155
|
+
const rawTool = String(part?.tool ?? 'tool').toLowerCase();
|
|
156
|
+
// Normalize to Claude-cased tool names so /history filter chips group
|
|
157
|
+
// cross-vendor (a bash call from claude-code and opencode show under
|
|
158
|
+
// the same "Bash" filter chip).
|
|
159
|
+
const toolNameMap = {
|
|
160
|
+
bash: 'Bash',
|
|
161
|
+
read: 'Read',
|
|
162
|
+
write: 'Write',
|
|
163
|
+
edit: 'Edit',
|
|
164
|
+
grep: 'Grep',
|
|
165
|
+
glob: 'Glob',
|
|
166
|
+
webfetch: 'WebFetch',
|
|
167
|
+
websearch: 'WebSearch',
|
|
168
|
+
task: 'Task',
|
|
169
|
+
};
|
|
170
|
+
const toolName = toolNameMap[rawTool] ?? rawTool;
|
|
171
|
+
const input = part?.state?.input ?? {};
|
|
172
|
+
const inputStr = (typeof input.command === 'string' && input.command) ||
|
|
173
|
+
(typeof input.file_path === 'string' && input.file_path) ||
|
|
174
|
+
(typeof input.path === 'string' && input.path) ||
|
|
175
|
+
(typeof input.pattern === 'string' && input.pattern) ||
|
|
176
|
+
(typeof input.query === 'string' && input.query) ||
|
|
177
|
+
JSON.stringify(input).slice(0, 500);
|
|
178
|
+
mirrorToolCallToAudit({
|
|
179
|
+
sessionId,
|
|
180
|
+
agent: 'opencode',
|
|
181
|
+
toolName,
|
|
182
|
+
toolInput: String(inputStr),
|
|
183
|
+
cwd,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
145
186
|
if (!errorEmitted) {
|
|
146
187
|
const data = event.data;
|
|
147
188
|
let text = '';
|