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
package/README.md
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
[](https://www.npmjs.com/package/codehere)
|
|
7
7
|
[](../LICENSE)
|
|
8
|
-
[]()
|
|
9
9
|
[]()
|
|
10
10
|
|
|
11
11
|
**Codehere is the vendor-neutral layer between you and the coding agents you already use** — Claude Code, Codex, Aider, Gemini, OpenCode. One `npm install`. Local-first, OSS, no account, no IAM, no compliance prereqs. Switch agent, keep your memory. End the day with one Final Report covering everything they did.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
All 5 adapters (Claude Code, Codex, Aider, Gemini, OpenCode) get session-tracing, Final Reports, and trust scoring. Claude Code additionally gets full pre-execution safety-hook enforcement on every tool call. The *memory*, *audit log*, *failover*, and *Final Report* surfaces ship across all 5 adapters today.
|
|
14
14
|
|
|
15
15
|
The enterprise tier is owned by [OpenAI Frontier](https://openai.com/index/introducing-openai-frontier/) (8-14 weeks of deploy prereqs, sales-team gated) and [Anthropic Cowork](https://www.anthropic.com/product/claude-cowork) (paid Claude subscription, single-vendor). Codehere fills the gap they structurally can't reach: **the individual / small-team / no-IT-department tier, vendor-neutral**.
|
|
16
16
|
|
|
@@ -29,7 +29,7 @@ One layer, every agent, your work stays portable.
|
|
|
29
29
|
|
|
30
30
|
| | | |
|
|
31
31
|
|---|---|---|
|
|
32
|
-
| 1 | **Install** | `npm install -g codehere
|
|
32
|
+
| 1 | **Install** | `npm install -g codehere` (Node 20+) — v0.6.0 is the current stable |
|
|
33
33
|
| 2 | **Detect environment** | `codehere setup` — checks API key, finds your repo |
|
|
34
34
|
| 3 | **Install the safety hook** | `codehere hook install` — every Claude Code tool call now audited |
|
|
35
35
|
| 4 | **Install at least one specialist** | Claude Code: `npm i -g @anthropic-ai/claude-code` (Codex / Aider / OpenCode / Gemini also supported · see [docs/integrations/](../docs/integrations/) for Cursor MCP setup) |
|
|
@@ -51,7 +51,7 @@ Context + End State + Constraints
|
|
|
51
51
|
|
|
52
52
|
### Folder convention (what `codehere init` writes)
|
|
53
53
|
|
|
54
|
-
The v0.
|
|
54
|
+
The v0.6 layout — F3 project memory:
|
|
55
55
|
|
|
56
56
|
| Path | Purpose | Committed? |
|
|
57
57
|
|---|---|---|
|
|
@@ -90,9 +90,9 @@ codehere report # generates a shareable markdown audit report
|
|
|
90
90
|
codehere status # quick dashboard · sessions, blocks, trust trends
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
The hook intercepts every `Bash`, `Edit`, `Write`, `MultiEdit`, and `NotebookEdit` call from any Claude Code session running on your machine, runs it through the trust-scorer pattern bank in under 5 ms, blocks anything matching the destructive / credential / injection patterns, and appends every decision to an append-only JSONL audit log at `~/.codehere/audit/`.
|
|
93
|
+
The hook intercepts every `Bash`, `Edit`, `Write`, `MultiEdit`, and `NotebookEdit` call from any Claude Code session running on your machine, runs it through the trust-scorer pattern bank in under 5 ms, blocks anything matching the destructive / credential / injection patterns, and appends every decision to an append-only JSONL audit log at `~/.codehere/audit/`. Codex, Aider, OpenCode, and Gemini adapters spawn through their native CLIs and emit to `~/.codehere/sessions.jsonl` with per-tool tracing via the cross-vendor mirror hook.
|
|
94
94
|
|
|
95
|
-
## Memory · F3 (v0.
|
|
95
|
+
## Memory · F3 (v0.6)
|
|
96
96
|
|
|
97
97
|
Once you've run `codehere init` in a project, every spawned agent — Claude Code, Codex, Aider, OpenCode, or Gemini — automatically reads the project's memory on entry. The agent starts warm with the briefing, recent lessons, and the most-referenced symbols in your codebase, instead of cold-grepping every session.
|
|
98
98
|
|
|
@@ -124,7 +124,7 @@ codehere memory show protocols # safety patterns by category
|
|
|
124
124
|
|
|
125
125
|
**Other memory commands:**
|
|
126
126
|
- `codehere memory init` — migrate the v0.4.1 layout (`CONTEXT.md` / `HANDOVER.md` / `PROMPTS.md`) into the F3 layout (one shot, leaves back-compat symlinks; `--dry-run` previews)
|
|
127
|
-
- `codehere memory repo-map [--rebuild]` — build the symbol map;
|
|
127
|
+
- `codehere memory repo-map [--rebuild]` — build the symbol map; currently TS-native MVP
|
|
128
128
|
- `codehere protocols export [--rebuild]` — write the trust-scorer pattern bank to `.codehere/protocols/hook_patterns.json` for inspection / cross-tool ingestion
|
|
129
129
|
|
|
130
130
|
## Going deeper
|
|
@@ -204,7 +204,7 @@ For depth beyond this README:
|
|
|
204
204
|
## Requirements
|
|
205
205
|
|
|
206
206
|
- **Node.js 20+** (pinned in `.nvmrc` and `engines.node`)
|
|
207
|
-
- **macOS or Linux** · Windows via WSL works for development
|
|
207
|
+
- **macOS or Linux** · Windows via WSL works for development
|
|
208
208
|
- **Optional but recommended:** at least one supported agent (Claude Code, Codex, Aider, OpenCode, or Gemini) installed before running `codehere start`
|
|
209
209
|
|
|
210
210
|
## License
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,16 @@ import { config } from 'dotenv';
|
|
|
15
15
|
import { join, dirname } from 'path';
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
17
|
import { homedir } from 'os';
|
|
18
|
+
// ── Global crash safety ───────────────────────────────────────────────────
|
|
19
|
+
// Unhandled rejections and exceptions must never silently kill the daemon.
|
|
20
|
+
process.on('unhandledRejection', (reason) => {
|
|
21
|
+
console.error('[codehere] unhandledRejection:', reason instanceof Error ? reason.stack : reason);
|
|
22
|
+
});
|
|
23
|
+
process.on('uncaughtException', (err) => {
|
|
24
|
+
console.error('[codehere] uncaughtException:', err.stack);
|
|
25
|
+
// persist what we can before dying
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
});
|
|
18
28
|
import { executeSetupCommand } from './presentation/cli/commands/setup-command.js';
|
|
19
29
|
import { executeInitCommand } from './presentation/cli/commands/init-command.js';
|
|
20
30
|
import { executeMemoryCommand } from './presentation/cli/commands/memory-command.js';
|
|
@@ -464,6 +474,7 @@ program
|
|
|
464
474
|
console.log(colors.dim('\ncodehere MCP server stopped'));
|
|
465
475
|
process.exit(0);
|
|
466
476
|
});
|
|
477
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
467
478
|
await new Promise((resolve) => {
|
|
468
479
|
server.on('close', resolve);
|
|
469
480
|
});
|
|
@@ -480,10 +491,12 @@ program
|
|
|
480
491
|
.option('-p, --port <number>', 'Port to listen on', '3773')
|
|
481
492
|
.option('--host <host>', 'Host to bind to', '127.0.0.1')
|
|
482
493
|
.option('--no-open', "Don't auto-open the browser")
|
|
494
|
+
.option('--cloud', 'Enable cloud sync — push session state to codehere.uk/app for remote monitoring')
|
|
483
495
|
.action(async (options) => {
|
|
484
496
|
const port = parseInt(options?.port ?? '3773', 10);
|
|
485
497
|
const host = options?.host ?? '127.0.0.1';
|
|
486
498
|
const shouldOpen = options?.open !== false;
|
|
499
|
+
const cloud = options?.cloud === true;
|
|
487
500
|
const url = `http://${host}:${port}/`;
|
|
488
501
|
const openBrowser = async () => {
|
|
489
502
|
if (!shouldOpen)
|
|
@@ -551,15 +564,42 @@ program
|
|
|
551
564
|
console.log(colors.bold('Tasks:') + ' ' + url);
|
|
552
565
|
console.log(colors.bold('History:') + ' ' + `http://${host}:${port}/history`);
|
|
553
566
|
console.log(colors.bold('MCP:') + ' ' + `http://${host}:${port}/mcp`);
|
|
567
|
+
if (cloud) {
|
|
568
|
+
console.log(colors.bold('Cloud:') + ' ' + 'https://codehere.uk/app');
|
|
569
|
+
}
|
|
554
570
|
newline();
|
|
555
571
|
console.log(colors.dim(' Press Ctrl+C to stop'));
|
|
556
572
|
newline();
|
|
573
|
+
// Start cloud sync if enabled
|
|
574
|
+
if (cloud) {
|
|
575
|
+
const { startCloudSync } = await import('./infrastructure/protocols/cloud-sync.js');
|
|
576
|
+
startCloudSync();
|
|
577
|
+
}
|
|
557
578
|
await openBrowser();
|
|
558
|
-
|
|
579
|
+
const shutdown = () => {
|
|
580
|
+
try {
|
|
581
|
+
const { SessionManager } = require('./infrastructure/orchestration/session-manager.js');
|
|
582
|
+
SessionManager.getInstance().shutdown();
|
|
583
|
+
}
|
|
584
|
+
catch { /* graceful degradation — daemon may not have started fully */ }
|
|
585
|
+
if (cloud) {
|
|
586
|
+
try {
|
|
587
|
+
const { stopCloudSync } = require('./infrastructure/protocols/cloud-sync.js');
|
|
588
|
+
stopCloudSync();
|
|
589
|
+
}
|
|
590
|
+
catch { /* cloud sync may not be loaded */ }
|
|
591
|
+
}
|
|
559
592
|
server.close();
|
|
593
|
+
};
|
|
594
|
+
process.on('SIGINT', () => {
|
|
595
|
+
shutdown();
|
|
560
596
|
console.log(colors.dim('\ncodehere stopped'));
|
|
561
597
|
process.exit(0);
|
|
562
598
|
});
|
|
599
|
+
process.on('SIGTERM', () => {
|
|
600
|
+
shutdown();
|
|
601
|
+
process.exit(0);
|
|
602
|
+
});
|
|
563
603
|
await new Promise((resolve) => {
|
|
564
604
|
server.on('close', resolve);
|
|
565
605
|
});
|
|
@@ -13,13 +13,20 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Docs: https://docs.anthropic.com/en/docs/claude-code/hooks
|
|
15
15
|
*/
|
|
16
|
-
import { appendFileSync, chmodSync, existsSync, mkdirSync, openSync, closeSync, readSync, statSync, readFileSync, } from 'fs';
|
|
16
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync, openSync, closeSync, readSync, statSync, readFileSync, unlinkSync, } from 'fs';
|
|
17
17
|
import { join } from 'path';
|
|
18
18
|
import { homedir } from 'os';
|
|
19
19
|
import { createHash } from 'crypto';
|
|
20
20
|
// P18 slice 2 — Tier 1 hook honors per-cwd permissions.yaml.
|
|
21
21
|
import { filterAllowed } from '../security/permissions-config.js';
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Instruction-level scan pattern bank used by `scanPayload()`. Exported so
|
|
24
|
+
* docs and the landing page can quote `PATTERNS.length` instead of hardcoding
|
|
25
|
+
* a number that silently drifts (May 2026 audit found 50 / 47 / 60+ all in
|
|
26
|
+
* the same PRD). Pair this with `CODE_PATTERNS` from `trust-scorer.ts` — the
|
|
27
|
+
* two banks are scanned in different passes and counted separately.
|
|
28
|
+
*/
|
|
29
|
+
export const PATTERNS = [
|
|
23
30
|
// Prompt Injection
|
|
24
31
|
// Allow chained modifiers: "ignore all previous instructions", "ignore all your prior rules", etc.
|
|
25
32
|
{
|
|
@@ -494,6 +501,38 @@ function readChainTip(filepath) {
|
|
|
494
501
|
return null;
|
|
495
502
|
}
|
|
496
503
|
export function appendAuditLog(entry) {
|
|
504
|
+
// Cross-process file locking. Multiple Claude Code sessions (e.g., two
|
|
505
|
+
// terminals, or IDE + terminal) can trigger PreToolUse concurrently.
|
|
506
|
+
// Without a lock, two hook processes read the same chain tip and both
|
|
507
|
+
// write seq 43 — corrupting the hash chain. We use an exclusive-create
|
|
508
|
+
// lock file (+O_EXCL) which is atomic on all POSIX systems.
|
|
509
|
+
const LOCK_FILE = AUDIT_FILE + '.lock';
|
|
510
|
+
const lockAcquired = () => {
|
|
511
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
512
|
+
try {
|
|
513
|
+
const fd = openSync(LOCK_FILE, 'wx');
|
|
514
|
+
closeSync(fd);
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
// Lock held by another process — spin-wait ~10ms
|
|
519
|
+
const deadline = Date.now() + 10;
|
|
520
|
+
while (Date.now() < deadline) { /* busy-wait — sync context */ }
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return false;
|
|
524
|
+
};
|
|
525
|
+
const lockReleased = () => {
|
|
526
|
+
try {
|
|
527
|
+
unlinkSync(LOCK_FILE);
|
|
528
|
+
}
|
|
529
|
+
catch { /* best effort */ }
|
|
530
|
+
};
|
|
531
|
+
if (!lockAcquired()) {
|
|
532
|
+
// Fall through unlocked rather than drop the entry. The entry still
|
|
533
|
+
// appends; verification may flag a chain break, but the audit data
|
|
534
|
+
// is preserved.
|
|
535
|
+
}
|
|
497
536
|
try {
|
|
498
537
|
const dir = AUDIT_FILE.substring(0, AUDIT_FILE.lastIndexOf('/'));
|
|
499
538
|
if (dir && !existsSync(dir)) {
|
|
@@ -526,6 +565,9 @@ export function appendAuditLog(entry) {
|
|
|
526
565
|
catch {
|
|
527
566
|
// Audit log failure must not block tool execution
|
|
528
567
|
}
|
|
568
|
+
finally {
|
|
569
|
+
lockReleased();
|
|
570
|
+
}
|
|
529
571
|
}
|
|
530
572
|
/**
|
|
531
573
|
* Walk the audit file and verify the hash chain. Returns:
|
|
@@ -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
|