codehere 0.6.0-alpha.1 → 0.6.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/dist/index.js +14 -0
- 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/session-manager.js +420 -6
- package/dist/infrastructure/protocols/cloud-sync.js +208 -0
- package/dist/infrastructure/protocols/mcp-server-impl.js +122 -10
- 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 +2 -1
package/dist/index.js
CHANGED
|
@@ -480,10 +480,12 @@ program
|
|
|
480
480
|
.option('-p, --port <number>', 'Port to listen on', '3773')
|
|
481
481
|
.option('--host <host>', 'Host to bind to', '127.0.0.1')
|
|
482
482
|
.option('--no-open', "Don't auto-open the browser")
|
|
483
|
+
.option('--cloud', 'Enable cloud sync — push session state to codehere.uk/app for remote monitoring')
|
|
483
484
|
.action(async (options) => {
|
|
484
485
|
const port = parseInt(options?.port ?? '3773', 10);
|
|
485
486
|
const host = options?.host ?? '127.0.0.1';
|
|
486
487
|
const shouldOpen = options?.open !== false;
|
|
488
|
+
const cloud = options?.cloud === true;
|
|
487
489
|
const url = `http://${host}:${port}/`;
|
|
488
490
|
const openBrowser = async () => {
|
|
489
491
|
if (!shouldOpen)
|
|
@@ -551,11 +553,23 @@ program
|
|
|
551
553
|
console.log(colors.bold('Tasks:') + ' ' + url);
|
|
552
554
|
console.log(colors.bold('History:') + ' ' + `http://${host}:${port}/history`);
|
|
553
555
|
console.log(colors.bold('MCP:') + ' ' + `http://${host}:${port}/mcp`);
|
|
556
|
+
if (cloud) {
|
|
557
|
+
console.log(colors.bold('Cloud:') + ' ' + 'https://codehere.uk/app');
|
|
558
|
+
}
|
|
554
559
|
newline();
|
|
555
560
|
console.log(colors.dim(' Press Ctrl+C to stop'));
|
|
556
561
|
newline();
|
|
562
|
+
// Start cloud sync if enabled
|
|
563
|
+
if (cloud) {
|
|
564
|
+
const { startCloudSync } = await import('./infrastructure/protocols/cloud-sync.js');
|
|
565
|
+
startCloudSync();
|
|
566
|
+
}
|
|
557
567
|
await openBrowser();
|
|
558
568
|
process.on('SIGINT', () => {
|
|
569
|
+
if (cloud) {
|
|
570
|
+
const { stopCloudSync } = require('./infrastructure/protocols/cloud-sync.js');
|
|
571
|
+
stopCloudSync();
|
|
572
|
+
}
|
|
559
573
|
server.close();
|
|
560
574
|
console.log(colors.dim('\ncodehere stopped'));
|
|
561
575
|
process.exit(0);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-handoff — structured cross-session context handoff protocol (P2).
|
|
3
|
+
*
|
|
4
|
+
* Bridges P0's auto-failover trigger with P1's graphify-ts context packs.
|
|
5
|
+
* When a session fails over (or a new session spawns), the handoff loader
|
|
6
|
+
* finds the most relevant context pack from recent session logs and injects
|
|
7
|
+
* it as a system hint so the replacement agent inherits continuity.
|
|
8
|
+
*
|
|
9
|
+
* Protocol:
|
|
10
|
+
* 1. loadHandoffContext(cwd, { fromSessionId?, sessionId?, maxAgeMs? })
|
|
11
|
+
* — scans .codehere/context-packs.jsonl for the most relevant pack
|
|
12
|
+
* 2. enrichPromptWithHandoff(prompt, cwd, handoff)
|
|
13
|
+
* — merges the handoff context into a prompt string
|
|
14
|
+
* 3. session handoff is automatic during failover (wired in session-manager)
|
|
15
|
+
*
|
|
16
|
+
* Handoff format (injected as system hint):
|
|
17
|
+
* [handoff: previous agent=<agent>, completed=<status>, pack=<packId>]
|
|
18
|
+
* Previous work summary: <pack.payloadPreview or compiled summary>
|
|
19
|
+
* ---
|
|
20
|
+
* <original prompt>
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, readFileSync } from 'fs';
|
|
23
|
+
import { join } from 'path';
|
|
24
|
+
// ── Constants ──────────────────────────────────────────────────────────────────
|
|
25
|
+
const CONTEXT_PACKS_DIR = '.codehere';
|
|
26
|
+
const CONTEXT_PACKS_FILE = 'context-packs.jsonl';
|
|
27
|
+
const DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
28
|
+
const DEFAULT_MIN_NODES = 5;
|
|
29
|
+
// ── Session log scanning ───────────────────────────────────────────────────────
|
|
30
|
+
/**
|
|
31
|
+
* Load all recent context packs from disk, sorted by timestamp (newest first).
|
|
32
|
+
*/
|
|
33
|
+
function loadAllPacks(cwd) {
|
|
34
|
+
const packsFile = join(cwd, CONTEXT_PACKS_DIR, CONTEXT_PACKS_FILE);
|
|
35
|
+
if (!existsSync(packsFile)) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const content = readFileSync(packsFile, 'utf-8');
|
|
40
|
+
const lines = content.split('\n').filter(Boolean);
|
|
41
|
+
return lines.map((line) => JSON.parse(line)).reverse();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Find the most relevant handoff context from previous sessions.
|
|
49
|
+
*
|
|
50
|
+
* Priority:
|
|
51
|
+
* 1. Same workspace, same prompt keywords (most recent pack)
|
|
52
|
+
* 2. Same workspace, any pack (most recent)
|
|
53
|
+
* 3. Fallback: no handoff
|
|
54
|
+
*/
|
|
55
|
+
export function loadHandoffContext(cwd, { fromSessionId, maxAgeMs = DEFAULT_MAX_AGE_MS, minNodes = DEFAULT_MIN_NODES } = {}) {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
const packs = loadAllPacks(cwd);
|
|
58
|
+
// Filter by age
|
|
59
|
+
const recent = packs.filter((p) => {
|
|
60
|
+
const ts = p.timestamp;
|
|
61
|
+
return ts && (now - ts) < maxAgeMs;
|
|
62
|
+
});
|
|
63
|
+
// Prefer packs with enough graph structure to be useful
|
|
64
|
+
const structured = recent.filter((p) => {
|
|
65
|
+
const nodes = p.nodeCount ?? 0;
|
|
66
|
+
return nodes >= minNodes;
|
|
67
|
+
});
|
|
68
|
+
if (structured.length === 0)
|
|
69
|
+
return null;
|
|
70
|
+
const last = structured[0];
|
|
71
|
+
return {
|
|
72
|
+
packId: String(last.packId ?? ''),
|
|
73
|
+
agent: String(last.agent ?? 'unknown'),
|
|
74
|
+
prompt: String(last.prompt ?? ''),
|
|
75
|
+
status: String(last.status ?? 'unknown'),
|
|
76
|
+
timestamp: last.timestamp,
|
|
77
|
+
nodeCount: last.nodeCount,
|
|
78
|
+
edgeCount: last.edgeCount,
|
|
79
|
+
task: String(last.task ?? 'explain'),
|
|
80
|
+
payloadPreview: String(last.payloadPreview ?? ''),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// ── Prompt enrichment ──────────────────────────────────────────────────────────
|
|
84
|
+
/**
|
|
85
|
+
* Inject handoff context into a prompt.
|
|
86
|
+
*
|
|
87
|
+
* Returns the enriched prompt string with the handoff as a system hint
|
|
88
|
+
* prepended before the original prompt. Returns the original prompt
|
|
89
|
+
* if no handoff is available.
|
|
90
|
+
*/
|
|
91
|
+
export function enrichPromptWithHandoff(prompt, handoff) {
|
|
92
|
+
if (!handoff)
|
|
93
|
+
return prompt;
|
|
94
|
+
const handoffLine = `[handoff: agent=${handoff.agent}, pack=${handoff.packId}, ${handoff.nodeCount} nodes, ${handoff.edgeCount} edges]`;
|
|
95
|
+
const preview = handoff.payloadPreview ? ` ${handoff.payloadPreview.slice(0, 800)}` : '';
|
|
96
|
+
return [
|
|
97
|
+
`[Previous session context — ${handoffLine}]`,
|
|
98
|
+
`This workspace has recent structural context from a prior session.`,
|
|
99
|
+
`For details, refer to the knowledge graph at the workspace root.`,
|
|
100
|
+
`---`,
|
|
101
|
+
prompt,
|
|
102
|
+
].join('\n');
|
|
103
|
+
}
|
|
104
|
+
// ── Failover handoff (P2 integration point) ────────────────────────────────────
|
|
105
|
+
/**
|
|
106
|
+
* Build a handoff context for a failover target.
|
|
107
|
+
*
|
|
108
|
+
* When session A fails and session B auto-spawns as replacement,
|
|
109
|
+
* this loads the most recent pack from the same workspace and
|
|
110
|
+
* merges it into the prompt so B has continuity with A's work.
|
|
111
|
+
*/
|
|
112
|
+
export function buildFailoverHandoff(cwd, { failedAgent, failedPrompt }) {
|
|
113
|
+
// Load most recent pack from workspace
|
|
114
|
+
const pack = loadHandoffContext(cwd, { maxAgeMs: DEFAULT_MAX_AGE_MS, minNodes: 3 });
|
|
115
|
+
if (!pack)
|
|
116
|
+
return null;
|
|
117
|
+
// Enrich the prompt with the handoff
|
|
118
|
+
const enrichedPrompt = enrichPromptWithHandoff(failedPrompt, {
|
|
119
|
+
...pack,
|
|
120
|
+
agent: failedAgent,
|
|
121
|
+
status: 'failed',
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
...pack,
|
|
125
|
+
agent: failedAgent,
|
|
126
|
+
prompt: enrichedPrompt,
|
|
127
|
+
status: 'failed',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// ── Handoff summary (for UI / API) ─────────────────────────────────────────────
|
|
131
|
+
/**
|
|
132
|
+
* Generate a human-readable handoff summary for the dashboard.
|
|
133
|
+
*/
|
|
134
|
+
export function formatHandoffSummary(handoff) {
|
|
135
|
+
const ageMs = Date.now() - handoff.timestamp;
|
|
136
|
+
const ageStr = ageMs < 60_000
|
|
137
|
+
? `${Math.round(ageMs / 1000)}s ago`
|
|
138
|
+
: ageMs < 3600_000
|
|
139
|
+
? `${Math.round(ageMs / 60_000)}m ago`
|
|
140
|
+
: `${Math.round(ageMs / 3600_000)}h ago`;
|
|
141
|
+
return `Previous session (${handoff.agent}) completed ${ageStr}. ${handoff.nodeCount} nodes, ${handoff.edgeCount} edges in workspace graph.`;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=context-handoff.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Failover config loader — v0.6.x rate-limit fail-over wedge (Slice C).
|
|
3
|
+
*
|
|
4
|
+
* Reads `failoverChain` from `~/.codehere/config.json` and validates it
|
|
5
|
+
* against the known agent ids. Returns the validated chain or undefined
|
|
6
|
+
* if the user has not configured one (caller falls back to the default).
|
|
7
|
+
*
|
|
8
|
+
* Config shape (~/.codehere/config.json):
|
|
9
|
+
* {
|
|
10
|
+
* "version": "0.6.0",
|
|
11
|
+
* "failover": {
|
|
12
|
+
* "chain": ["codex", "claude-code", "aider"]
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* If `failover.chain` is missing, empty, or invalid, returns undefined
|
|
17
|
+
* so the caller uses DEFAULT_FAILOVER_CHAIN from failover-policy.ts.
|
|
18
|
+
*
|
|
19
|
+
* Slice C scope: config read + validation. Slice D wires it into
|
|
20
|
+
* SessionManager so the resolved chain flows into the resolver.
|
|
21
|
+
*/
|
|
22
|
+
import { readFileSync, existsSync } from 'fs';
|
|
23
|
+
import { homedir } from 'os';
|
|
24
|
+
import { join } from 'path';
|
|
25
|
+
import { validateFailoverChain } from './failover-policy.js';
|
|
26
|
+
const GLOBAL_CONFIG_PATH = join(homedir(), '.codehere', 'config.json');
|
|
27
|
+
/**
|
|
28
|
+
* Load failover configuration from ~/.codehere/config.json.
|
|
29
|
+
* Returns undefined if the config file does not exist or has no failover section.
|
|
30
|
+
*
|
|
31
|
+
* Safe to call at any time — no side effects, no caching, no I/O errors
|
|
32
|
+
* propagated (returns undefined on any read failure).
|
|
33
|
+
*/
|
|
34
|
+
export function loadFailoverConfig() {
|
|
35
|
+
if (!existsSync(GLOBAL_CONFIG_PATH))
|
|
36
|
+
return undefined;
|
|
37
|
+
try {
|
|
38
|
+
const raw = JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, 'utf-8'));
|
|
39
|
+
if (!raw || typeof raw !== 'object')
|
|
40
|
+
return undefined;
|
|
41
|
+
const failover = raw.failover;
|
|
42
|
+
if (!failover || typeof failover !== 'object')
|
|
43
|
+
return undefined;
|
|
44
|
+
const config = {};
|
|
45
|
+
// Validate chain if present
|
|
46
|
+
if (Array.isArray(failover.chain)) {
|
|
47
|
+
try {
|
|
48
|
+
config.chain = validateFailoverChain(failover.chain);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Invalid chain — skip it, caller uses default
|
|
52
|
+
config.chain = undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// autoFailover — boolean, defaults to false
|
|
56
|
+
if (typeof failover.autoFailover === 'boolean') {
|
|
57
|
+
config.autoFailover = failover.autoFailover;
|
|
58
|
+
}
|
|
59
|
+
return config;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Config file exists but is malformed — return undefined so caller
|
|
63
|
+
// falls back to defaults without crashing.
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get the resolved failover chain: user-configured if present, else default.
|
|
69
|
+
* Pure function — no I/O. Caller loads config and passes it in.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveChain(userChain) {
|
|
72
|
+
if (userChain && userChain.length > 0)
|
|
73
|
+
return userChain;
|
|
74
|
+
// Re-export the default from failover-policy
|
|
75
|
+
return ['claude-code', 'codex', 'aider', 'opencode', 'gemini'];
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=failover-config.js.map
|
|
@@ -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
|