smart-context-mcp 1.0.3 → 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 +139 -26
- package/package.json +8 -3
- package/scripts/check-repo-safety.js +84 -0
- package/scripts/claude-hook.js +33 -0
- package/scripts/headless-wrapper.js +106 -0
- package/scripts/init-clients.js +137 -6
- package/scripts/report-metrics.js +22 -121
- package/src/hooks/claude-hooks.js +424 -0
- package/src/metrics.js +218 -8
- package/src/orchestration/headless-wrapper.js +314 -0
- package/src/repo-safety.js +166 -0
- package/src/server.js +83 -4
- package/src/storage/sqlite.js +1092 -0
- package/src/tools/smart-metrics.js +249 -0
- package/src/tools/smart-summary.js +1230 -324
- package/src/tools/smart-turn.js +307 -0
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
|
|
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)
|
|
@@ -31,13 +31,15 @@ That's it. Restart your AI client (Cursor, Codex, Claude Desktop) and the tools
|
|
|
31
31
|
|
|
32
32
|
## What you get
|
|
33
33
|
|
|
34
|
-
|
|
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
|
|
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/
|
|
136
|
+
.devctx/state.sqlite
|
|
131
137
|
```
|
|
132
138
|
|
|
133
|
-
|
|
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/
|
|
154
|
-
Source:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -449,16 +470,20 @@ Maintain compressed conversation state across sessions. Solves the context-loss
|
|
|
449
470
|
|
|
450
471
|
| Action | Purpose | Returns |
|
|
451
472
|
|--------|---------|---------|
|
|
452
|
-
| `get` | Retrieve current or
|
|
473
|
+
| `get` | Retrieve current, explicit, or auto-resolved session | Resume summary (≤500 tokens) + compression metadata |
|
|
453
474
|
| `update` | Create or replace session | New session with compressed state |
|
|
454
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 |
|
|
455
478
|
| `reset` | Clear session | Confirmation |
|
|
456
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 |
|
|
457
482
|
|
|
458
483
|
**Parameters:**
|
|
459
484
|
- `action` (required) — one of the actions above
|
|
460
|
-
- `sessionId` (optional) — session identifier; auto-generated from `goal` if omitted
|
|
461
|
-
- `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:
|
|
462
487
|
- `goal`: primary objective
|
|
463
488
|
- `status`: current state (`planning` | `in_progress` | `blocked` | `completed`)
|
|
464
489
|
- `pinnedContext`: critical context that should survive compression when possible
|
|
@@ -471,14 +496,27 @@ Maintain compressed conversation state across sessions. Solves the context-loss
|
|
|
471
496
|
- `nextStep`: immediate next action
|
|
472
497
|
- `touchedFiles`: array of modified files
|
|
473
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`
|
|
474
506
|
|
|
475
|
-
`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.
|
|
476
508
|
|
|
477
509
|
**Storage:**
|
|
478
|
-
-
|
|
479
|
-
-
|
|
480
|
-
-
|
|
481
|
-
-
|
|
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
|
|
482
520
|
|
|
483
521
|
**Resume summary fields:**
|
|
484
522
|
- `status` and `nextStep` are preserved with highest priority
|
|
@@ -493,6 +531,8 @@ Maintain compressed conversation state across sessions. Solves the context-loss
|
|
|
493
531
|
- `truncated`: whether the resume summary had to be compressed
|
|
494
532
|
- `compressionLevel`: `none` | `trimmed` | `reduced` | `status_only`
|
|
495
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
|
|
496
536
|
|
|
497
537
|
**Compression strategy:**
|
|
498
538
|
- Keeps the persisted session state intact and compresses only the resume summary
|
|
@@ -505,11 +545,12 @@ Maintain compressed conversation state across sessions. Solves the context-loss
|
|
|
505
545
|
```javascript
|
|
506
546
|
// Start of work session
|
|
507
547
|
smart_summary({ action: "get" })
|
|
508
|
-
// → retrieves last active session or
|
|
548
|
+
// → retrieves last active session or auto-resumes the best saved session
|
|
509
549
|
|
|
510
550
|
// After implementing auth middleware
|
|
511
551
|
smart_summary({
|
|
512
|
-
action: "
|
|
552
|
+
action: "checkpoint",
|
|
553
|
+
event: "milestone",
|
|
513
554
|
update: {
|
|
514
555
|
completed: ["auth middleware"],
|
|
515
556
|
decisions: ["JWT with 1h expiry, refresh tokens in Redis"],
|
|
@@ -525,6 +566,77 @@ smart_summary({ action: "get" })
|
|
|
525
566
|
// List all sessions
|
|
526
567
|
smart_summary({ action: "list_sessions" })
|
|
527
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
|
|
528
640
|
```
|
|
529
641
|
|
|
530
642
|
### `build_index`
|
|
@@ -575,9 +687,10 @@ Metrics include: P@5, P@10, Recall, wrong-file rate, retrieval honesty, follow-u
|
|
|
575
687
|
## Notes
|
|
576
688
|
|
|
577
689
|
- `@vscode/ripgrep` provides a bundled `rg` binary, so a system install is not required.
|
|
578
|
-
-
|
|
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.
|
|
579
692
|
- Symbol index stored in `<projectRoot>/.devctx/index.json` when `build_index` is used.
|
|
580
|
-
-
|
|
693
|
+
- Legacy session JSON files in `<projectRoot>/.devctx/sessions/` are imported idempotently when present.
|
|
581
694
|
- This package is a navigation and diagnostics layer, not a full semantic code intelligence system.
|
|
582
695
|
|
|
583
696
|
## Repository
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smart-context-mcp",
|
|
3
|
-
"version": "1.0.
|
|
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
|
+
});
|