smart-context-mcp 1.0.2 → 1.0.4

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,7 +5,7 @@
5
5
 
6
6
  **MCP server that reduces AI agent token usage by 90% and improves response quality.**
7
7
 
8
- Instead of reading entire files and repeating context, this MCP provides 7 smart tools that compress, rank, and maintain context efficiently.
8
+ Instead of reading entire files and repeating context, this MCP provides 8 focused tools that compress, rank, and maintain context efficiently.
9
9
 
10
10
  ## Why use this?
11
11
 
@@ -15,7 +15,7 @@ Instead of reading entire files and repeating context, this MCP provides 7 smart
15
15
 
16
16
  **Real metrics from production use:**
17
17
  - 14.5M tokens → 1.6M tokens (89.87% reduction)
18
- - 3,666 successful calls across 7 tools
18
+ - 3,666 successful calls across the original 7 core tools
19
19
  - Compression ratios: 3x to 46x depending on tool
20
20
 
21
21
  ## Quick Start (2 commands)
@@ -27,17 +27,19 @@ npx smart-context-init --target .
27
27
 
28
28
  That's it. Restart your AI client (Cursor, Codex, Claude Desktop) and the tools are available.
29
29
 
30
- **Important:** The init command automatically sets the correct `cwd` (working directory) in the generated configs, so the MCP server runs from your project root. This works for standalone projects, monorepos, and nested workspaces.
30
+ **Important:** The init command automatically sets the correct project-root env var in the generated configs, so the MCP server runs from your project root. This works for standalone projects, monorepos, and nested workspaces.
31
31
 
32
32
  ## What you get
33
33
 
34
- Seven focused tools that work automatically:
34
+ Eight focused tools that work automatically:
35
35
 
36
36
  - `smart_read`: compact file summaries instead of full file dumps (3x compression)
37
37
  - `smart_read_batch`: read multiple files in one call — reduces round-trip latency
38
38
  - `smart_search`: ripgrep-first code search with intent-aware ranking (21x compression)
39
39
  - `smart_context`: one-call context planner — search + read + graph expansion
40
40
  - `smart_summary`: maintain compressed conversation state across sessions (46x compression)
41
+ - `smart_turn`: one-call turn orchestration for start/end context recovery and checkpointing
42
+ - `smart_metrics`: inspect saved token metrics and recent usage through MCP
41
43
  - `smart_shell`: safe diagnostic shell execution with restricted commands (18x compression)
42
44
  - `build_index`: lightweight symbol index for faster lookups and smarter ranking
43
45
 
@@ -81,14 +83,18 @@ npx smart-context-init --target .
81
83
  ```
82
84
 
83
85
  This installs the MCP server and generates client configs for Cursor, Codex, Qwen, and Claude Code. Open the project with your IDE/agent and the server starts automatically.
86
+ If the target is a git repository, `smart-context-init` also installs an idempotent `pre-commit` hook that blocks commits when `.devctx/state.sqlite` is staged, tracked, or not properly ignored.
87
+ For Claude Code, `smart-context-init` also generates `.claude/settings.json` with native hooks so devctx context recovery and turn-end enforcement run automatically.
84
88
 
85
89
  ## Binaries
86
90
 
87
- The package exposes three binaries:
91
+ The package exposes five binaries:
88
92
 
93
+ - `smart-context-headless`
89
94
  - `smart-context-server`
90
95
  - `smart-context-init`
91
96
  - `smart-context-report`
97
+ - `smart-context-protect`
92
98
 
93
99
  Start the MCP server against the current project:
94
100
 
@@ -127,10 +133,12 @@ smart-context-init --target /path/to/project --command node --args '["./tools/de
127
133
  Each tool call persists token metrics to the target repo by default in:
128
134
 
129
135
  ```bash
130
- .devctx/metrics.jsonl
136
+ .devctx/state.sqlite
131
137
  ```
132
138
 
133
- This makes per-repo usage visible without digging into `node_modules`. Running `smart-context-init` also adds `.devctx/` to the target repo's `.gitignore` idempotently.
139
+ SQLite is now the primary project-local store for persisted context and metrics. Running `smart-context-init` also adds `.devctx/` to the target repo's `.gitignore` idempotently.
140
+
141
+ When an active session exists, metrics entries automatically inherit its `sessionId`, so you can inspect savings per task with `smart_metrics`.
134
142
 
135
143
  Show a quick report:
136
144
 
@@ -138,7 +146,7 @@ Show a quick report:
138
146
  smart-context-report
139
147
  ```
140
148
 
141
- Show JSON output or a custom file:
149
+ Show JSON output or inspect a legacy/custom JSONL file explicitly:
142
150
 
143
151
  ```bash
144
152
  smart-context-report --json
@@ -150,8 +158,8 @@ Example output:
150
158
  ```text
151
159
  devctx metrics report
152
160
 
153
- File: /path/to/repo/.devctx/metrics.jsonl
154
- Source: default
161
+ File: /path/to/repo/.devctx/state.sqlite
162
+ Source: sqlite
155
163
  Entries: 148
156
164
  Raw tokens: 182,340
157
165
  Final tokens: 41,920
@@ -163,7 +171,7 @@ By tool:
163
171
  smart_search count=35 raw=33,330 final=7,800 saved=25,530 (76.59%)
164
172
  ```
165
173
 
166
- If you want to override the location entirely, set `DEVCTX_METRICS_FILE`.
174
+ If you need JSONL compatibility for external tooling, set `DEVCTX_METRICS_FILE` or pass `--file`.
167
175
 
168
176
  ## Usage per client
169
177
 
@@ -171,7 +179,7 @@ After installing and running `smart-context-init`, each client picks up the serv
171
179
 
172
180
  ### Cursor
173
181
 
174
- Open the project in Cursor. The MCP server starts automatically. Enable it in **Cursor Settings > MCP** if needed. All seven tools are available in Agent mode.
182
+ Open the project in Cursor. The MCP server starts automatically. Enable it in **Cursor Settings > MCP** if needed. All eight tools are available in Agent mode.
175
183
 
176
184
  ### Codex CLI
177
185
 
@@ -189,7 +197,20 @@ cd /path/to/your-project
189
197
  claude
190
198
  ```
191
199
 
192
- Claude Code reads `.mcp.json` from the project root.
200
+ Claude Code reads `.mcp.json` from the project root and `.claude/settings.json` for native hook automation.
201
+
202
+ ### Codex/Qwen headless fallback
203
+
204
+ When a client does not expose native per-turn hooks, use `smart-context-headless` to wrap a headless CLI run and force `smart_turn(start)` plus a closing checkpoint around that invocation.
205
+
206
+ Examples:
207
+
208
+ ```bash
209
+ smart-context-headless --client codex --prompt "Finish the runtime repo-safety docs" -- codex exec
210
+ smart-context-headless --client qwen --prompt "Review the persisted session and propose the next step" -- qwen -p
211
+ ```
212
+
213
+ This is the current automation path for non-Claude CLI agents. GUI clients without hook support still rely on generated rules plus `smart_turn`.
193
214
 
194
215
  ### Qwen Code
195
216
 
@@ -215,9 +236,9 @@ The `intent` parameter in `smart_search` and `smart_context` adjusts ranking and
215
236
 
216
237
  - **Cursor**: `.cursor/rules/devctx.mdc` (always-apply rule)
217
238
  - **Codex**: `AGENTS.md` (devctx section with sentinel markers)
218
- - **Claude Code**: `CLAUDE.md` (devctx section with sentinel markers)
239
+ - **Claude Code**: `CLAUDE.md` (devctx section with sentinel markers) and `.claude/settings.json` (native hooks)
219
240
 
220
- The rules are idempotent — running `smart-context-init` again updates the section without duplicating it. Existing content in `AGENTS.md` and `CLAUDE.md` is preserved.
241
+ The generated files are idempotent — running `smart-context-init` again updates the devctx sections and Claude hook entries without duplicating them. Existing content in `AGENTS.md`, `CLAUDE.md`, and `.claude/settings.json` is preserved.
221
242
 
222
243
  ## Use against another repo
223
244
 
@@ -233,13 +254,15 @@ or:
233
254
  DEVCTX_PROJECT_ROOT=/path/to/target-repo node ./src/mcp-server.js
234
255
  ```
235
256
 
236
- or (recommended for MCP clients):
257
+ or (recommended for MCP clients and generated configs):
237
258
 
238
259
  ```bash
239
- MCP_PROJECT_ROOT=/path/to/target-repo node ./src/mcp-server.js
260
+ DEVCTX_PROJECT_ROOT=/path/to/target-repo node ./src/mcp-server.js
240
261
  ```
241
262
 
242
- `smart-context-init` automatically sets `MCP_PROJECT_ROOT` in the generated client configs (`.cursor/mcp.json`, `.codex/config.toml`, `.mcp.json`, `.qwen/settings.json`), so the MCP server always launches from the correct project context, even in monorepos or when installed globally.
263
+ Legacy configs that still set `MCP_PROJECT_ROOT` remain supported for backward compatibility.
264
+
265
+ `smart-context-init` automatically sets `DEVCTX_PROJECT_ROOT` in the generated client configs (`.cursor/mcp.json`, `.codex/config.toml`, `.mcp.json`, `.qwen/settings.json`), so the MCP server always launches from the correct project context, even in monorepos or when installed globally.
243
266
 
244
267
  ## What it is good at
245
268
 
@@ -447,16 +470,20 @@ Maintain compressed conversation state across sessions. Solves the context-loss
447
470
 
448
471
  | Action | Purpose | Returns |
449
472
  |--------|---------|---------|
450
- | `get` | Retrieve current or specified session | Resume summary (≤500 tokens) + compression metadata |
473
+ | `get` | Retrieve current, explicit, or auto-resolved session | Resume summary (≤500 tokens) + compression metadata |
451
474
  | `update` | Create or replace session | New session with compressed state |
452
475
  | `append` | Add to existing session | Merged session state |
476
+ | `auto_append` | Add only when something meaningful changed | Merged session state or skipped no-op result |
477
+ | `checkpoint` | Event-driven orchestration for persistence decisions | Persisted update or skipped event with decision metadata |
453
478
  | `reset` | Clear session | Confirmation |
454
479
  | `list_sessions` | Show all available sessions | Array of sessions with metadata |
480
+ | `compact` | Apply retention/compaction to SQLite state | Counts for pruned sessions, events, and metrics |
481
+ | `cleanup_legacy` | Inspect or remove imported JSON/JSONL artifacts | Dry-run or deletion report |
455
482
 
456
483
  **Parameters:**
457
484
  - `action` (required) — one of the actions above
458
- - `sessionId` (optional) — session identifier; auto-generated from `goal` if omitted
459
- - `update` (required for update/append) — object with:
485
+ - `sessionId` (optional) — session identifier; auto-generated from `goal` if omitted. Pass `"auto"` to accept the recommended recent session when multiple candidates exist.
486
+ - `update` (required for update/append/auto_append/checkpoint) — object with:
460
487
  - `goal`: primary objective
461
488
  - `status`: current state (`planning` | `in_progress` | `blocked` | `completed`)
462
489
  - `pinnedContext`: critical context that should survive compression when possible
@@ -469,14 +496,27 @@ Maintain compressed conversation state across sessions. Solves the context-loss
469
496
  - `nextStep`: immediate next action
470
497
  - `touchedFiles`: array of modified files
471
498
  - `maxTokens` (optional, default 500) — hard cap on summary size
499
+ - `event` (optional for `checkpoint`) — one of `manual`, `milestone`, `decision`, `blocker`, `status_change`, `file_change`, `task_switch`, `task_complete`, `session_end`, `read_only`, `heartbeat`
500
+ - `force` (optional, default false) — override a suppressed checkpoint event
501
+ - `retentionDays` (optional, default 30) — used by `compact`
502
+ - `keepLatestEventsPerSession` (optional, default 20) — used by `compact`
503
+ - `keepLatestMetrics` (optional, default 1000) — used by `compact`
504
+ - `vacuum` (optional, default false) — run SQLite `VACUUM` after deletions during `compact`
505
+ - `apply` (optional, default false) — required to actually delete files during `cleanup_legacy`
472
506
 
473
- `update` replaces the stored session state for that `sessionId`, so omitted fields are cleared. Use `append` when you want to keep existing state and add progress incrementally.
507
+ `update` replaces the stored session state for that `sessionId`, so omitted fields are cleared. Use `append` when you want to keep existing state and add progress incrementally. Use `auto_append` when the caller may fire checkpoint saves often and you want the tool to skip no-op updates automatically. Use `checkpoint` when the caller has a meaningful event and wants the tool to decide whether that event deserves persistence.
474
508
 
475
509
  **Storage:**
476
- - Sessions persist in `.devctx/sessions/<sessionId>.json`
477
- - Active session tracked in `.devctx/sessions/active.json`
478
- - 30-day retention for inactive sessions
479
- - No expiration for active sessions
510
+ - Session state, session events, summary cache, and metrics persist in `.devctx/state.sqlite`
511
+ - Legacy `.devctx/sessions/*.json`, `.devctx/sessions/active.json`, and `.devctx/metrics.jsonl` are imported idempotently when present
512
+ - `compact` enforces retention without deleting the active session
513
+ - `cleanup_legacy` is dry-run by default and only deletes imported legacy artifacts when `apply: true`
514
+
515
+ **Auto-resume behavior:**
516
+ - `get` returns the active session immediately when `active.json` exists
517
+ - If there is no active session, `get` auto-resumes the best saved session when there is a single clear candidate
518
+ - If multiple recent sessions are plausible, `get` returns ordered `candidates` plus `recommendedSessionId`
519
+ - Passing `sessionId: "auto"` accepts that recommendation and restores it as the active session
480
520
 
481
521
  **Resume summary fields:**
482
522
  - `status` and `nextStep` are preserved with highest priority
@@ -491,6 +531,8 @@ Maintain compressed conversation state across sessions. Solves the context-loss
491
531
  - `truncated`: whether the resume summary had to be compressed
492
532
  - `compressionLevel`: `none` | `trimmed` | `reduced` | `status_only`
493
533
  - `omitted`: fields dropped from the resume summary to fit the token budget
534
+ - `repoSafety`: git hygiene signal for `.devctx/state.sqlite` (`isIgnored`, `isTracked`, `isStaged`, warnings, recommended actions)
535
+ - mutating actions (`update`, `append`, `auto_append`, `checkpoint`, `reset`, `compact`) are blocked at runtime when `.devctx/state.sqlite` is tracked or staged
494
536
 
495
537
  **Compression strategy:**
496
538
  - Keeps the persisted session state intact and compresses only the resume summary
@@ -503,11 +545,12 @@ Maintain compressed conversation state across sessions. Solves the context-loss
503
545
  ```javascript
504
546
  // Start of work session
505
547
  smart_summary({ action: "get" })
506
- // → retrieves last active session or returns "not found"
548
+ // → retrieves last active session or auto-resumes the best saved session
507
549
 
508
550
  // After implementing auth middleware
509
551
  smart_summary({
510
- action: "append",
552
+ action: "checkpoint",
553
+ event: "milestone",
511
554
  update: {
512
555
  completed: ["auth middleware"],
513
556
  decisions: ["JWT with 1h expiry, refresh tokens in Redis"],
@@ -523,6 +566,77 @@ smart_summary({ action: "get" })
523
566
  // List all sessions
524
567
  smart_summary({ action: "list_sessions" })
525
568
  // → see all available sessions, pick one to resume
569
+
570
+ // Inspect git safety for project-local state from any smart_summary response
571
+ smart_summary({ action: "get" })
572
+ // → repoSafety warns if .devctx/state.sqlite is tracked or not ignored
573
+
574
+ // Suppress noisy read-only exploration checkpoints
575
+ smart_summary({
576
+ action: "checkpoint",
577
+ event: "read_only",
578
+ update: { currentFocus: "inspect auth flow" }
579
+ })
580
+ // → skipped=true, no event persisted
581
+
582
+ // Compact old SQLite events while keeping recent history
583
+ smart_summary({ action: "compact", retentionDays: 30, keepLatestEventsPerSession: 20, keepLatestMetrics: 1000 })
584
+
585
+ // Inspect what legacy files are safe to remove
586
+ smart_summary({ action: "cleanup_legacy" })
587
+
588
+ // Remove imported legacy JSON/JSONL artifacts explicitly
589
+ smart_summary({ action: "cleanup_legacy", apply: true })
590
+ ```
591
+
592
+ ### `smart_metrics`
593
+
594
+ Inspect token metrics recorded in project-local SQLite storage without leaving MCP.
595
+
596
+ - Returns aggregated totals, savings percentage, and per-tool breakdowns
597
+ - Supports `window`: `24h` | `7d` | `30d` | `all`
598
+ - Supports filtering by `tool`
599
+ - Supports filtering by `sessionId`, including `sessionId: "active"`
600
+ - Includes `latestEntries` so an agent can explain recent savings without parsing storage manually
601
+ - Includes `overheadTokens` and `overheadTools` so hook/wrapper context cost stays measurable against the savings
602
+ - When `.devctx/state.sqlite` is tracked or staged, metric writes are skipped and reads fall back to a temporary read-only snapshot with `sideEffectsSuppressed: true`
603
+
604
+ **Example workflow:**
605
+
606
+ ```javascript
607
+ smart_metrics({ window: "7d", sessionId: "active" })
608
+ // → totals and recent entries for the current task/session
609
+ ```
610
+
611
+ ### `smart_turn`
612
+
613
+ Orchestrate the start or end of a meaningful agent turn with one MCP call.
614
+
615
+ - `phase: "start"` rehydrates context, classifies whether the current prompt aligns with persisted work, and can auto-create a planning session for a substantial new task
616
+ - `phase: "end"` writes a checkpoint through `smart_summary` and can optionally include compact metrics
617
+ - Designed to make context usage almost mandatory without forcing the agent to chain `smart_summary(get)` and `smart_summary(checkpoint)` manually on every turn
618
+ - Claude Code can invoke this automatically through generated native hooks on `SessionStart`, `UserPromptSubmit`, `PostToolUse`, and `Stop`
619
+ - Non-Claude CLI clients can approximate the same flow with `smart-context-headless`, which wraps one headless agent invocation around `smart_turn(start)` and `smart_turn(end)`
620
+
621
+ **Example workflow:**
622
+
623
+ ```javascript
624
+ smart_turn({
625
+ phase: "start",
626
+ prompt: "Finish runtime repo-safety enforcement for smart metrics",
627
+ ensureSession: true
628
+ })
629
+ // → summary + continuity classification + repoSafety
630
+
631
+ smart_turn({
632
+ phase: "end",
633
+ event: "milestone",
634
+ update: {
635
+ completed: ["Finished smart metrics repo-safety enforcement"],
636
+ nextStep: "Update docs and run the full suite"
637
+ }
638
+ })
639
+ // → checkpoint result + optional compact metrics
526
640
  ```
527
641
 
528
642
  ### `build_index`
@@ -573,9 +687,10 @@ Metrics include: P@5, P@10, Recall, wrong-file rate, retrieval honesty, follow-u
573
687
  ## Notes
574
688
 
575
689
  - `@vscode/ripgrep` provides a bundled `rg` binary, so a system install is not required.
576
- - Metrics are written to `<projectRoot>/.devctx/metrics.jsonl` (override with `DEVCTX_METRICS_FILE` env var).
690
+ - Persistent context and metrics live in `<projectRoot>/.devctx/state.sqlite`.
691
+ - `DEVCTX_METRICS_FILE` is now an explicit compatibility override for JSONL-based workflows and reports.
577
692
  - Symbol index stored in `<projectRoot>/.devctx/index.json` when `build_index` is used.
578
- - Conversation sessions stored in `<projectRoot>/.devctx/sessions/` when `smart_summary` is used.
693
+ - Legacy session JSON files in `<projectRoot>/.devctx/sessions/` are imported idempotently when present.
579
694
  - This package is a navigation and diagnostics layer, not a full semantic code intelligence system.
580
695
 
581
696
  ## Repository
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-context-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "MCP server that reduces agent token usage and improves response quality with compact file summaries, ranked code search, and curated context.",
5
5
  "author": "Francisco Caballero Portero <fcp1978@hotmail.com>",
6
6
  "type": "module",
@@ -13,9 +13,11 @@
13
13
  "url": "https://github.com/Arrayo/devctx-mcp-mvp/issues"
14
14
  },
15
15
  "bin": {
16
+ "smart-context-headless": "scripts/headless-wrapper.js",
16
17
  "smart-context-server": "scripts/devctx-server.js",
17
18
  "smart-context-init": "scripts/init-clients.js",
18
- "smart-context-report": "scripts/report-metrics.js"
19
+ "smart-context-report": "scripts/report-metrics.js",
20
+ "smart-context-protect": "scripts/check-repo-safety.js"
19
21
  },
20
22
  "exports": {
21
23
  ".": "./src/server.js",
@@ -23,7 +25,10 @@
23
25
  },
24
26
  "files": [
25
27
  "src/",
28
+ "scripts/claude-hook.js",
29
+ "scripts/check-repo-safety.js",
26
30
  "scripts/devctx-server.js",
31
+ "scripts/headless-wrapper.js",
27
32
  "scripts/init-clients.js",
28
33
  "scripts/report-metrics.js"
29
34
  ],
@@ -45,7 +50,7 @@
45
50
  "smoke:json": "node ./scripts/smoke-test.js --json",
46
51
  "init:clients": "node ./scripts/init-clients.js",
47
52
  "smoke:formats": "node ./scripts/format-smoke.js",
48
- "test": "node --test ./tests/*.test.js",
53
+ "test": "node --test --test-concurrency=1 ./tests/*.test.js",
49
54
  "eval": "node ./evals/harness.js",
50
55
  "eval:context": "node ./evals/harness.js --tool=context",
51
56
  "eval:both": "node ./evals/harness.js --tool=both",
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { enforceRepoSafety } from '../src/repo-safety.js';
5
+ import { setProjectRoot } from '../src/utils/runtime-config.js';
6
+
7
+ const writeStdout = (text) => {
8
+ fs.writeSync(process.stdout.fd, text);
9
+ };
10
+
11
+ const writeStderr = (text) => {
12
+ fs.writeSync(process.stderr.fd, text);
13
+ };
14
+
15
+ const parseArgs = (argv) => {
16
+ const options = {
17
+ projectRoot: process.cwd(),
18
+ json: false,
19
+ };
20
+
21
+ for (let index = 0; index < argv.length; index += 1) {
22
+ const token = argv[index];
23
+
24
+ if (token === '--project-root') {
25
+ const next = argv[index + 1];
26
+ if (!next || next.startsWith('--')) {
27
+ throw new Error('Missing value for --project-root');
28
+ }
29
+ options.projectRoot = path.resolve(next);
30
+ index += 1;
31
+ continue;
32
+ }
33
+
34
+ if (token === '--json') {
35
+ options.json = true;
36
+ continue;
37
+ }
38
+
39
+ throw new Error(`Unknown argument: ${token}`);
40
+ }
41
+
42
+ return options;
43
+ };
44
+
45
+ const printHuman = (result) => {
46
+ if (result.ok) {
47
+ writeStdout('devctx repo safety: ok\n');
48
+ return;
49
+ }
50
+
51
+ writeStderr('devctx repo safety: failed\n');
52
+ for (const violation of result.violations) {
53
+ writeStderr(`- ${violation}\n`);
54
+ }
55
+ for (const action of result.recommendedActions) {
56
+ writeStderr(`- ${action}\n`);
57
+ }
58
+ };
59
+
60
+ const main = () => {
61
+ const options = parseArgs(process.argv.slice(2));
62
+ setProjectRoot(options.projectRoot);
63
+
64
+ const result = enforceRepoSafety({ root: options.projectRoot });
65
+ if (options.json) {
66
+ const output = `${JSON.stringify(result, null, 2)}\n`;
67
+ if (result.ok) {
68
+ writeStdout(output);
69
+ } else {
70
+ writeStderr(output);
71
+ }
72
+ } else {
73
+ printHuman(result);
74
+ }
75
+
76
+ process.exitCode = result.ok ? 0 : 1;
77
+ };
78
+
79
+ try {
80
+ main();
81
+ } catch (error) {
82
+ writeStderr(`${error.message}\n`);
83
+ process.exitCode = 1;
84
+ }
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { handleClaudeHookEvent } from '../src/hooks/claude-hooks.js';
3
+
4
+ const readStdin = async () => {
5
+ let buffer = '';
6
+ for await (const chunk of process.stdin) {
7
+ buffer += chunk;
8
+ }
9
+ return buffer.trim();
10
+ };
11
+
12
+ const main = async () => {
13
+ const raw = await readStdin();
14
+ if (!raw) {
15
+ return;
16
+ }
17
+
18
+ const input = JSON.parse(raw);
19
+ const result = await handleClaudeHookEvent(input);
20
+
21
+ if (!result) {
22
+ return;
23
+ }
24
+
25
+ process.stdout.write(`${JSON.stringify(result)}\n`);
26
+ };
27
+
28
+ main().catch((error) => {
29
+ if (process.env.DEVCTX_DEBUG === '1') {
30
+ process.stderr.write(`devctx claude hook error: ${error.message}\n`);
31
+ }
32
+ process.exit(0);
33
+ });
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ import { runHeadlessWrapper } from '../src/orchestration/headless-wrapper.js';
3
+
4
+ const requireValue = (argv, index, flag) => {
5
+ const value = argv[index + 1];
6
+ if (!value || value === '--') {
7
+ throw new Error(`Missing value for ${flag}`);
8
+ }
9
+ return value;
10
+ };
11
+
12
+ const parseArgs = (argv) => {
13
+ const options = {
14
+ client: 'generic',
15
+ prompt: '',
16
+ sessionId: undefined,
17
+ event: undefined,
18
+ stdinPrompt: false,
19
+ dryRun: false,
20
+ json: false,
21
+ streamOutput: true,
22
+ command: '',
23
+ args: [],
24
+ };
25
+
26
+ let commandIndex = argv.indexOf('--');
27
+ const head = commandIndex === -1 ? argv : argv.slice(0, commandIndex);
28
+
29
+ for (let index = 0; index < head.length; index += 1) {
30
+ const token = head[index];
31
+
32
+ if (token === '--client') {
33
+ options.client = requireValue(head, index, '--client');
34
+ index += 1;
35
+ continue;
36
+ }
37
+
38
+ if (token === '--prompt') {
39
+ options.prompt = requireValue(head, index, '--prompt');
40
+ index += 1;
41
+ continue;
42
+ }
43
+
44
+ if (token === '--session-id') {
45
+ options.sessionId = requireValue(head, index, '--session-id');
46
+ index += 1;
47
+ continue;
48
+ }
49
+
50
+ if (token === '--event') {
51
+ options.event = requireValue(head, index, '--event');
52
+ index += 1;
53
+ continue;
54
+ }
55
+
56
+ if (token === '--stdin-prompt') {
57
+ options.stdinPrompt = true;
58
+ continue;
59
+ }
60
+
61
+ if (token === '--dry-run') {
62
+ options.dryRun = true;
63
+ continue;
64
+ }
65
+
66
+ if (token === '--json') {
67
+ options.json = true;
68
+ continue;
69
+ }
70
+
71
+ if (token === '--quiet') {
72
+ options.streamOutput = false;
73
+ continue;
74
+ }
75
+
76
+ throw new Error(`Unknown argument: ${token}`);
77
+ }
78
+
79
+ if (commandIndex !== -1) {
80
+ const commandParts = argv.slice(commandIndex + 1);
81
+ if (commandParts.length > 0) {
82
+ [options.command, ...options.args] = commandParts;
83
+ }
84
+ }
85
+
86
+ return options;
87
+ };
88
+
89
+ const main = async () => {
90
+ const options = parseArgs(process.argv.slice(2));
91
+ if (options.json) {
92
+ options.streamOutput = false;
93
+ }
94
+ const result = await runHeadlessWrapper(options);
95
+ if (options.dryRun || options.json) {
96
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
97
+ }
98
+ if (!options.dryRun && result.exitCode !== 0) {
99
+ process.exitCode = result.exitCode;
100
+ }
101
+ };
102
+
103
+ main().catch((error) => {
104
+ console.error(error.message);
105
+ process.exit(1);
106
+ });