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 CHANGED
@@ -5,12 +5,12 @@
5
5
 
6
6
  [![npm](https://img.shields.io/npm/v/codehere)](https://www.npmjs.com/package/codehere)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow)](../LICENSE)
8
- [![Tests](https://img.shields.io/badge/tests-668%2F668-brightgreen)]()
8
+ [![Tests](https://img.shields.io/badge/tests-1196%2F1196-brightgreen)]()
9
9
  [![Vulnerabilities](https://img.shields.io/badge/vulnerabilities-0-brightgreen)]()
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
- Today, Claude Code gets full pre-execution safety-hook enforcement on every tool call; the other adapters are session-traced and reported, with per-tool hook parity in progress for v0.6. The vendor-neutral *memory*, *audit log*, and *Final Report* surfaces ship across all 5 adapters today.
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@alpha` (Node 20+) — current alpha track is v0.5; `@latest` still on v0.4 |
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.5 layout — F3 project memory:
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/`. (Hook coverage today is Claude Code-only — Codex / Aider / OpenCode adapters spawn through their native CLIs and emit to `~/.codehere/sessions.jsonl`; full hook parity across all adapters is v0.4 work.)
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.5)
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; v0.5 ships a TS-native MVP, v0.6 swaps in tree-sitter
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; native Windows is on the v0.4 roadmap
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
- process.on('SIGINT', () => {
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
- const PATTERNS = [
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