@dtoolkit/dcontext 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -13
- package/dist/index.js +814 -3
- package/package.json +16 -6
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,33 +1,155 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="
|
|
2
|
+
<img src="../../logo.png" alt="dtoolkit" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">@dtoolkit/dcontext</h1>
|
|
6
|
-
<p align="center">
|
|
6
|
+
<p align="center">dbrain hooks for AI coding CLIs</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
9
|
<a href="https://www.npmjs.com/package/@dtoolkit/dcontext"><img src="https://img.shields.io/npm/v/@dtoolkit/dcontext.svg" alt="npm"></a>
|
|
10
10
|
<a href="../../LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Hook-based bridge between [dbrain](../dbrain/) and AI coding CLIs. Injects your identity, soul, and project facts at session start so the AI already knows everything — no need to call `recall`. Also saves key exchanges to dbrain before context compaction.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Requires a running [dbrain](../dbrain/) instance.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## Works with
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
| CLI | Hook mechanism | Status |
|
|
20
|
+
|-----|---------------|--------|
|
|
21
|
+
| **Claude Code** | Hooks in `~/.claude/settings.json` | v1 |
|
|
22
|
+
| **Codex CLI** | Hooks in `~/.codex/config.toml` | v1 |
|
|
23
|
+
| **Gemini CLI** | Hooks in `~/.gemini/settings.json` | v1 |
|
|
24
|
+
| **OpenCode** | In-process npm plugin | v1 |
|
|
23
25
|
|
|
24
|
-
##
|
|
26
|
+
## Install
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g @dtoolkit/dcontext
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
27
33
|
|
|
28
34
|
```bash
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
# 1. Connect to dbrain and map your project
|
|
36
|
+
dcontext init
|
|
37
|
+
# ? How do you want to connect to dbrain? → Connect to existing dbrain
|
|
38
|
+
# ? dbrain URL: http://localhost:7878
|
|
39
|
+
# ? dbrain token: sk-dbr_...
|
|
40
|
+
# ? Map /Users/you/my-project to entity: → my-project
|
|
41
|
+
|
|
42
|
+
# 2. Install hooks for your CLI
|
|
43
|
+
dcontext install claude
|
|
44
|
+
|
|
45
|
+
# 3. Start coding — sessions start warm
|
|
46
|
+
claude
|
|
47
|
+
# → Session Context (from dbrain) is injected automatically
|
|
48
|
+
# → Identity, soul, user profile, and project facts already loaded
|
|
49
|
+
# → No recall needed at start
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## How it works
|
|
53
|
+
|
|
54
|
+
dcontext hooks into two moments of a session's lifecycle:
|
|
55
|
+
|
|
56
|
+
| Moment | What happens |
|
|
57
|
+
|--------|-------------|
|
|
58
|
+
| **Session start** | Searches dbrain for project facts, loads identity/soul/user docs, injects as `additionalContext` |
|
|
59
|
+
| **Pre-compaction** | Reads the session transcript, extracts meaningful exchanges, saves to dbrain as a conversation |
|
|
60
|
+
|
|
61
|
+
The AI receives the context transparently via hooks — it never calls dcontext directly.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
┌─────────────────┐ SessionStart ┌─────────────┐
|
|
65
|
+
│ Claude Code │◄───────────────────── │ dcontext │
|
|
66
|
+
│ Gemini CLI │ additionalContext │ (hooks) │
|
|
67
|
+
│ OpenCode │ │ │
|
|
68
|
+
│ │ PreCompact │ │
|
|
69
|
+
│ │─────────────────────► │ │
|
|
70
|
+
└─────────────────┘ transcript └──────┬───────┘
|
|
71
|
+
│
|
|
72
|
+
search / save
|
|
73
|
+
│
|
|
74
|
+
┌──────▼───────┐
|
|
75
|
+
│ dbrain │
|
|
76
|
+
│ (memory) │
|
|
77
|
+
└──────────────┘
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### What gets injected
|
|
81
|
+
|
|
82
|
+
The session briefing includes (in order):
|
|
83
|
+
|
|
84
|
+
1. **Identity** — who the AI is (name, creation date)
|
|
85
|
+
2. **Soul** — behavioral guidelines
|
|
86
|
+
3. **User** — who you are (name, timezone)
|
|
87
|
+
4. **Project facts** — recent decisions, milestones, preferences, context (up to 15 facts, truncated to 200 chars each)
|
|
88
|
+
|
|
89
|
+
Total briefing capped at 8000 characters.
|
|
90
|
+
|
|
91
|
+
### Instruction file integration
|
|
92
|
+
|
|
93
|
+
When you run `dcontext install <target>`, it modifies the dbrain section in the client's instruction file to tell the AI **not to call `recall` at session start** — since the context is already injected. Run `dbrain connect <target>` to restore the original dbrain instructions after uninstalling.
|
|
94
|
+
|
|
95
|
+
| Target | Instruction file |
|
|
96
|
+
|--------|-----------------|
|
|
97
|
+
| `claude` | `~/.claude/CLAUDE.md` |
|
|
98
|
+
| `codex` | `~/.codex/AGENTS.md` |
|
|
99
|
+
| `gemini` | `~/.gemini/GEMINI.md` |
|
|
100
|
+
| `opencode` | `~/.config/opencode/AGENTS.md` |
|
|
101
|
+
|
|
102
|
+
## Commands
|
|
103
|
+
|
|
104
|
+
| Command | Description |
|
|
105
|
+
|---------|-------------|
|
|
106
|
+
| `dcontext init` | Interactive setup — connect to dbrain, map projects |
|
|
107
|
+
| `dcontext install <target>` | Install hooks (`claude`, `codex`, `gemini`, `opencode`) |
|
|
108
|
+
| `dcontext uninstall <target>` | Remove hooks |
|
|
109
|
+
| `dcontext status` | Show config, targets, project mappings, stats |
|
|
110
|
+
| `dcontext explore` | Preview the briefing that would be injected for the current directory |
|
|
111
|
+
| `dcontext hook <event>` | Internal — called by CLI hooks, not for direct use |
|
|
112
|
+
|
|
113
|
+
## Configuration
|
|
114
|
+
|
|
115
|
+
Config at `~/.dcontext/config.json` (created by `dcontext init`):
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"initialized": true,
|
|
120
|
+
"dbrain": {
|
|
121
|
+
"url": "http://localhost:7878",
|
|
122
|
+
"token": "sk-dbr_..."
|
|
123
|
+
},
|
|
124
|
+
"projects": {
|
|
125
|
+
"/Users/you/my-project": "my-project"
|
|
126
|
+
},
|
|
127
|
+
"briefing": {
|
|
128
|
+
"maxFacts": 15,
|
|
129
|
+
"includeIdentity": true,
|
|
130
|
+
"maxChars": 8000,
|
|
131
|
+
"maxCharsPerFact": 200
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
| Key | Default | Description |
|
|
137
|
+
|-----|---------|-------------|
|
|
138
|
+
| `dbrain.url` | — | dbrain server URL |
|
|
139
|
+
| `dbrain.token` | — | Bearer token for dbrain API |
|
|
140
|
+
| `projects` | `{}` | Maps absolute cwd path to dbrain entity name |
|
|
141
|
+
| `briefing.maxFacts` | `15` | Max facts to include in briefing |
|
|
142
|
+
| `briefing.includeIdentity` | `true` | Include identity, soul, and user docs |
|
|
143
|
+
| `briefing.maxChars` | `8000` | Max total briefing length |
|
|
144
|
+
| `briefing.maxCharsPerFact` | `200` | Max characters per individual fact |
|
|
145
|
+
|
|
146
|
+
## Data storage
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
~/.dcontext/
|
|
150
|
+
├── config.json # Configuration (created by init)
|
|
151
|
+
├── stats.json # Usage statistics (briefings served, extractions done)
|
|
152
|
+
└── error.log # Hook errors (hooks never crash — they log and exit 0)
|
|
31
153
|
```
|
|
32
154
|
|
|
33
155
|
## License
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,814 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command7 } from "commander";
|
|
5
|
+
import pc6 from "picocolors";
|
|
6
|
+
|
|
7
|
+
// src/commands/explore.ts
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import pc from "picocolors";
|
|
10
|
+
|
|
11
|
+
// src/core/briefing.ts
|
|
12
|
+
import { DBrainClient } from "@dtoolkit/sdk";
|
|
13
|
+
function cleanDocContent(content, sectionTitle) {
|
|
14
|
+
const lines = content.split("\n");
|
|
15
|
+
const cleaned = [];
|
|
16
|
+
let prevEmpty = false;
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
const trimmed = line.trim();
|
|
19
|
+
if (trimmed.startsWith("#")) {
|
|
20
|
+
const headerText = trimmed.replace(/^#+\s*/, "").trim().toLowerCase();
|
|
21
|
+
if (headerText === sectionTitle.toLowerCase()) continue;
|
|
22
|
+
}
|
|
23
|
+
if (trimmed === "") {
|
|
24
|
+
if (prevEmpty) continue;
|
|
25
|
+
prevEmpty = true;
|
|
26
|
+
} else {
|
|
27
|
+
prevEmpty = false;
|
|
28
|
+
}
|
|
29
|
+
cleaned.push(line);
|
|
30
|
+
}
|
|
31
|
+
while (cleaned.length > 0 && cleaned[0].trim() === "") cleaned.shift();
|
|
32
|
+
while (cleaned.length > 0 && cleaned[cleaned.length - 1].trim() === "") cleaned.pop();
|
|
33
|
+
return cleaned.join("\n");
|
|
34
|
+
}
|
|
35
|
+
function truncateFact(fact, max) {
|
|
36
|
+
if (fact.length <= max) return fact;
|
|
37
|
+
return fact.slice(0, max - 1) + "\u2026";
|
|
38
|
+
}
|
|
39
|
+
async function generateBriefing(projectEntity, config) {
|
|
40
|
+
const client = new DBrainClient(config.dbrain.url, config.dbrain.token);
|
|
41
|
+
const safeQuery = `"${projectEntity}"`;
|
|
42
|
+
const [results, documents] = await Promise.all([
|
|
43
|
+
client.search(safeQuery, { limit: config.briefing.maxFacts }).catch(() => []),
|
|
44
|
+
config.briefing.includeIdentity ? client.listDocuments() : Promise.resolve([])
|
|
45
|
+
]);
|
|
46
|
+
if (results.length === 0 && documents.length === 0) return null;
|
|
47
|
+
const parts = [];
|
|
48
|
+
parts.push("## Session Context (from dbrain)");
|
|
49
|
+
parts.push(
|
|
50
|
+
"> **DO NOT call `recall` or `wake_up`.** Your identity, soul, user profile, and project facts are already below. This context was injected by dcontext at session start."
|
|
51
|
+
);
|
|
52
|
+
if (config.briefing.includeIdentity && documents.length > 0) {
|
|
53
|
+
const identityDocs = documents.filter(
|
|
54
|
+
(d) => d.key === "identity" || d.key === "user" || d.key === "soul"
|
|
55
|
+
);
|
|
56
|
+
for (const doc of identityDocs) {
|
|
57
|
+
try {
|
|
58
|
+
const full = await client.getDocument(doc.key);
|
|
59
|
+
if (full.content) {
|
|
60
|
+
const title = doc.title || doc.key;
|
|
61
|
+
const cleaned = cleanDocContent(full.content, title);
|
|
62
|
+
if (cleaned) {
|
|
63
|
+
parts.push("");
|
|
64
|
+
parts.push(`### ${title}`);
|
|
65
|
+
parts.push(cleaned);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (results.length > 0) {
|
|
73
|
+
parts.push("");
|
|
74
|
+
parts.push(`### Project: ${projectEntity}`);
|
|
75
|
+
for (const r of results) {
|
|
76
|
+
const category = r.fact.category || "context";
|
|
77
|
+
parts.push(`- [${category}] ${truncateFact(r.fact.fact, config.briefing.maxCharsPerFact)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
let briefing = parts.join("\n");
|
|
81
|
+
if (briefing.length > config.briefing.maxChars) {
|
|
82
|
+
briefing = briefing.slice(0, config.briefing.maxChars - 3) + "\u2026";
|
|
83
|
+
}
|
|
84
|
+
return briefing;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/core/config.ts
|
|
88
|
+
import { appendFile, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
89
|
+
import { homedir } from "os";
|
|
90
|
+
import { join } from "path";
|
|
91
|
+
var DATA_DIR = process.env.DCONTEXT_DATA_DIR || join(homedir(), ".dcontext");
|
|
92
|
+
var DEFAULT_CONFIG = {
|
|
93
|
+
initialized: false,
|
|
94
|
+
dbrain: {
|
|
95
|
+
url: "",
|
|
96
|
+
token: ""
|
|
97
|
+
},
|
|
98
|
+
projects: {},
|
|
99
|
+
briefing: {
|
|
100
|
+
maxFacts: 15,
|
|
101
|
+
includeIdentity: true,
|
|
102
|
+
maxChars: 8e3,
|
|
103
|
+
maxCharsPerFact: 200
|
|
104
|
+
},
|
|
105
|
+
targets: {
|
|
106
|
+
claude: { installed: false },
|
|
107
|
+
codex: { installed: false },
|
|
108
|
+
gemini: { installed: false },
|
|
109
|
+
opencode: { installed: false }
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var DEFAULT_STATS = {
|
|
113
|
+
briefings: { total: 0 },
|
|
114
|
+
extractions: { total: 0, messagesSaved: 0 },
|
|
115
|
+
byTarget: {}
|
|
116
|
+
};
|
|
117
|
+
function getDataDir() {
|
|
118
|
+
return DATA_DIR;
|
|
119
|
+
}
|
|
120
|
+
async function ensureDataDir() {
|
|
121
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
function deepMerge(defaults, overrides) {
|
|
124
|
+
const result = { ...defaults };
|
|
125
|
+
for (const key in overrides) {
|
|
126
|
+
if (typeof defaults[key] === "object" && defaults[key] !== null && !Array.isArray(defaults[key]) && typeof overrides[key] === "object" && overrides[key] !== null && !Array.isArray(overrides[key])) {
|
|
127
|
+
result[key] = deepMerge(
|
|
128
|
+
defaults[key],
|
|
129
|
+
overrides[key]
|
|
130
|
+
);
|
|
131
|
+
} else {
|
|
132
|
+
result[key] = overrides[key];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
async function atomicWriteFile(filePath, data) {
|
|
138
|
+
const tmp = filePath + ".tmp";
|
|
139
|
+
await writeFile(tmp, data, "utf-8");
|
|
140
|
+
await rename(tmp, filePath);
|
|
141
|
+
}
|
|
142
|
+
async function loadConfig() {
|
|
143
|
+
try {
|
|
144
|
+
const raw = await readFile(join(DATA_DIR, "config.json"), "utf-8");
|
|
145
|
+
const parsed = JSON.parse(raw);
|
|
146
|
+
return deepMerge(
|
|
147
|
+
DEFAULT_CONFIG,
|
|
148
|
+
parsed
|
|
149
|
+
);
|
|
150
|
+
} catch {
|
|
151
|
+
return { ...DEFAULT_CONFIG };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function saveConfig(config) {
|
|
155
|
+
await ensureDataDir();
|
|
156
|
+
await atomicWriteFile(join(DATA_DIR, "config.json"), JSON.stringify(config, null, 2) + "\n");
|
|
157
|
+
}
|
|
158
|
+
async function loadStats() {
|
|
159
|
+
try {
|
|
160
|
+
const raw = await readFile(join(DATA_DIR, "stats.json"), "utf-8");
|
|
161
|
+
const parsed = JSON.parse(raw);
|
|
162
|
+
return deepMerge(
|
|
163
|
+
DEFAULT_STATS,
|
|
164
|
+
parsed
|
|
165
|
+
);
|
|
166
|
+
} catch {
|
|
167
|
+
return { ...DEFAULT_STATS };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function updateStats(fn) {
|
|
171
|
+
const stats = await loadStats();
|
|
172
|
+
fn(stats);
|
|
173
|
+
await ensureDataDir();
|
|
174
|
+
await atomicWriteFile(join(DATA_DIR, "stats.json"), JSON.stringify(stats, null, 2) + "\n");
|
|
175
|
+
}
|
|
176
|
+
async function logError(message) {
|
|
177
|
+
try {
|
|
178
|
+
await ensureDataDir();
|
|
179
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
|
|
180
|
+
`;
|
|
181
|
+
await appendFile(join(DATA_DIR, "error.log"), line, "utf-8");
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/commands/explore.ts
|
|
187
|
+
function createExploreCommand() {
|
|
188
|
+
return new Command("explore").description("Preview the context that would be injected for a directory").argument("[path]", "Directory to explore (defaults to cwd)").action(async (path) => {
|
|
189
|
+
try {
|
|
190
|
+
await runExplore(path);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error(pc.red(err.message));
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async function runExplore(path) {
|
|
198
|
+
const cwd = path || process.cwd();
|
|
199
|
+
const config = await loadConfig();
|
|
200
|
+
if (!config.dbrain.url) {
|
|
201
|
+
console.error(pc.red("dcontext not configured. Run: dcontext init"));
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
const entity = config.projects[cwd];
|
|
205
|
+
if (!entity) {
|
|
206
|
+
console.log(pc.dim(`No entity mapped for ${cwd}`));
|
|
207
|
+
console.log();
|
|
208
|
+
const projects = Object.entries(config.projects);
|
|
209
|
+
if (projects.length > 0) {
|
|
210
|
+
console.log(pc.bold("Mapped directories:"));
|
|
211
|
+
for (const [p2, e] of projects) {
|
|
212
|
+
console.log(` ${pc.dim(p2)} \u2192 ${pc.blue(e)}`);
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
console.log(`Run ${pc.cyan("dcontext init")} in a project directory to map it.`);
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
console.log(pc.bold("Context preview") + pc.dim(` \u2014 ${cwd} \u2192 ${entity}`));
|
|
220
|
+
console.log();
|
|
221
|
+
const briefing = await generateBriefing(entity, config);
|
|
222
|
+
if (!briefing) {
|
|
223
|
+
console.log(pc.dim("No context found in dbrain for this project."));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
console.log(briefing);
|
|
227
|
+
console.log();
|
|
228
|
+
console.log(pc.dim(`${briefing.length} chars \u2014 max ${config.briefing.maxChars}`));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/commands/hook.ts
|
|
232
|
+
import { Command as Command2 } from "commander";
|
|
233
|
+
|
|
234
|
+
// src/core/extraction.ts
|
|
235
|
+
import { DBrainClient as DBrainClient2 } from "@dtoolkit/sdk";
|
|
236
|
+
async function extractAndSave(sessionId, target, cwd, config) {
|
|
237
|
+
const transcriptPath = target.resolveTranscriptPath(sessionId, cwd);
|
|
238
|
+
let entries;
|
|
239
|
+
try {
|
|
240
|
+
entries = await target.parseTranscript(transcriptPath);
|
|
241
|
+
} catch (err) {
|
|
242
|
+
await logError(`Failed to parse transcript: ${err.message}`);
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const filtered = entries.filter((e) => e.content.length >= 50);
|
|
246
|
+
if (filtered.length === 0) return null;
|
|
247
|
+
const messages = filtered.map((e) => ({ role: e.role, content: e.content }));
|
|
248
|
+
try {
|
|
249
|
+
const client = new DBrainClient2(config.dbrain.url, config.dbrain.token);
|
|
250
|
+
const conv = await client.startConversation(`dcontext-${target.name}`);
|
|
251
|
+
await client.sendMessages(conv.id, messages);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
await logError(`Failed to save to dbrain: ${err.message}`);
|
|
254
|
+
}
|
|
255
|
+
await updateStats((stats) => {
|
|
256
|
+
stats.extractions.total++;
|
|
257
|
+
stats.extractions.lastAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
258
|
+
stats.extractions.messagesSaved += messages.length;
|
|
259
|
+
if (!stats.byTarget[target.name]) {
|
|
260
|
+
stats.byTarget[target.name] = { briefings: 0, extractions: 0 };
|
|
261
|
+
}
|
|
262
|
+
stats.byTarget[target.name].extractions++;
|
|
263
|
+
});
|
|
264
|
+
return `Session log saved to dbrain (${messages.length} messages from ${target.name})`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/commands/targets.ts
|
|
268
|
+
import { createClaudeTarget } from "@dtoolkit/adapter-claude";
|
|
269
|
+
import { createCodexTarget } from "@dtoolkit/adapter-codex";
|
|
270
|
+
import { createGeminiTarget } from "@dtoolkit/adapter-gemini";
|
|
271
|
+
import { createOpenCodeTarget } from "@dtoolkit/adapter-opencode";
|
|
272
|
+
var targets = {
|
|
273
|
+
claude: createClaudeTarget,
|
|
274
|
+
codex: createCodexTarget,
|
|
275
|
+
gemini: createGeminiTarget,
|
|
276
|
+
opencode: createOpenCodeTarget
|
|
277
|
+
};
|
|
278
|
+
function resolveTarget(name) {
|
|
279
|
+
const factory = targets[name];
|
|
280
|
+
return factory ? factory() : null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/commands/hook.ts
|
|
284
|
+
function createHookCommand() {
|
|
285
|
+
return new Command2("hook").description("Handle hook events (internal \u2014 called by CLI hooks)").argument("<event>", "Hook event: session-start, pre-compact").option("--target <name>", "Explicit target override (claude, codex, gemini, opencode)").action(async (event, opts) => {
|
|
286
|
+
try {
|
|
287
|
+
await runHook(event, opts.target);
|
|
288
|
+
} catch (err) {
|
|
289
|
+
await logError(`hook ${event}: ${err.message}`);
|
|
290
|
+
process.stdout.write("{}");
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function readStdinRaw() {
|
|
295
|
+
return new Promise((resolve) => {
|
|
296
|
+
if (process.stdin.isTTY) {
|
|
297
|
+
resolve("{}");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
let data = "";
|
|
301
|
+
process.stdin.setEncoding("utf-8");
|
|
302
|
+
process.stdin.on("data", (chunk) => {
|
|
303
|
+
data += chunk;
|
|
304
|
+
});
|
|
305
|
+
process.stdin.on("end", () => resolve(data || "{}"));
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function detectTarget(env) {
|
|
309
|
+
for (const name of ["claude", "codex", "gemini", "opencode"]) {
|
|
310
|
+
const target = resolveTarget(name);
|
|
311
|
+
if (target?.detectFromEnv(env)) return target;
|
|
312
|
+
}
|
|
313
|
+
return resolveTarget("claude");
|
|
314
|
+
}
|
|
315
|
+
async function runHook(event, explicitTarget) {
|
|
316
|
+
const raw = await readStdinRaw();
|
|
317
|
+
let input = {};
|
|
318
|
+
try {
|
|
319
|
+
input = JSON.parse(raw);
|
|
320
|
+
} catch {
|
|
321
|
+
}
|
|
322
|
+
const config = await loadConfig();
|
|
323
|
+
if (!config.dbrain.url) {
|
|
324
|
+
process.stdout.write("{}");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const env = process.env;
|
|
328
|
+
const target = explicitTarget ? resolveTarget(explicitTarget) : detectTarget(env);
|
|
329
|
+
if (!target) {
|
|
330
|
+
process.stdout.write("{}");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const cwd = input["cwd"] || env["GEMINI_CWD"] || env["GEMINI_PROJECT_DIR"] || process.cwd();
|
|
334
|
+
const sessionId = target.getSessionId(env, input);
|
|
335
|
+
if (event === "session-start") {
|
|
336
|
+
await handleSessionStart(target, cwd, config);
|
|
337
|
+
} else if (event === "pre-compact") {
|
|
338
|
+
await handlePreCompact(sessionId, target, cwd, config);
|
|
339
|
+
} else {
|
|
340
|
+
process.stdout.write("{}");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function formatOutput(target, event, context) {
|
|
344
|
+
if (target.name === "codex") {
|
|
345
|
+
return JSON.stringify({
|
|
346
|
+
hookSpecificOutput: {
|
|
347
|
+
hookEventName: event,
|
|
348
|
+
additionalContext: context
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return JSON.stringify({ additionalContext: context });
|
|
353
|
+
}
|
|
354
|
+
async function handleSessionStart(target, cwd, config) {
|
|
355
|
+
const projectEntity = config.projects[cwd];
|
|
356
|
+
if (!projectEntity) {
|
|
357
|
+
process.stdout.write("{}");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const briefing = await generateBriefing(projectEntity, config);
|
|
361
|
+
if (!briefing) {
|
|
362
|
+
process.stdout.write("{}");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
await updateStats((stats) => {
|
|
366
|
+
stats.briefings.total++;
|
|
367
|
+
stats.briefings.lastAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
368
|
+
});
|
|
369
|
+
process.stdout.write(formatOutput(target, "SessionStart", briefing));
|
|
370
|
+
}
|
|
371
|
+
async function handlePreCompact(sessionId, target, cwd, config) {
|
|
372
|
+
if (!sessionId) {
|
|
373
|
+
process.stdout.write("{}");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const notes = await extractAndSave(sessionId, target, cwd, config);
|
|
377
|
+
if (!notes) {
|
|
378
|
+
process.stdout.write("{}");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
process.stdout.write(formatOutput(target, "PreCompact", notes));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/commands/init.ts
|
|
385
|
+
import * as p from "@clack/prompts";
|
|
386
|
+
import { DBrainClient as DBrainClient3 } from "@dtoolkit/sdk";
|
|
387
|
+
import { Command as Command3 } from "commander";
|
|
388
|
+
import pc2 from "picocolors";
|
|
389
|
+
function createInitCommand() {
|
|
390
|
+
return new Command3("init").description("Interactive setup wizard").action(async () => {
|
|
391
|
+
try {
|
|
392
|
+
await runInit();
|
|
393
|
+
} catch (err) {
|
|
394
|
+
if (err.message?.includes("cancelled")) process.exit(0);
|
|
395
|
+
console.error(pc2.red(err.message));
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
async function runInit() {
|
|
401
|
+
p.intro(pc2.cyan("dcontext") + " \u2014 dbrain hooks for AI coding CLIs");
|
|
402
|
+
const config = await loadConfig();
|
|
403
|
+
if (config.initialized && config.dbrain.url) {
|
|
404
|
+
p.log.info(`Current dbrain: ${pc2.blue(config.dbrain.url)}`);
|
|
405
|
+
const cwd = process.cwd();
|
|
406
|
+
const entity = config.projects[cwd];
|
|
407
|
+
if (entity) {
|
|
408
|
+
p.log.info(`Current mapping: ${pc2.dim(cwd)} \u2192 ${pc2.blue(entity)}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const options = [
|
|
412
|
+
{
|
|
413
|
+
value: "connect",
|
|
414
|
+
label: "Connect to existing dbrain",
|
|
415
|
+
description: "I have a dbrain running already"
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
value: "local",
|
|
419
|
+
label: "Start a local dbrain",
|
|
420
|
+
description: "Initialize and start a new brain here"
|
|
421
|
+
}
|
|
422
|
+
];
|
|
423
|
+
if (config.initialized && config.dbrain.url) {
|
|
424
|
+
options.unshift({
|
|
425
|
+
value: "remap",
|
|
426
|
+
label: "Keep dbrain, remap this directory",
|
|
427
|
+
description: `Change entity mapping for ${process.cwd()}`
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
const mode = await p.select({
|
|
431
|
+
message: config.initialized ? "What do you want to change?" : "How do you want to connect to dbrain?",
|
|
432
|
+
options
|
|
433
|
+
});
|
|
434
|
+
if (p.isCancel(mode)) {
|
|
435
|
+
p.cancel("Init cancelled.");
|
|
436
|
+
process.exit(0);
|
|
437
|
+
}
|
|
438
|
+
if (mode === "remap") {
|
|
439
|
+
await finishInit(config);
|
|
440
|
+
} else if (mode === "local") {
|
|
441
|
+
await startLocalDbrain(config);
|
|
442
|
+
} else {
|
|
443
|
+
await connectToDbrain(config);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async function startLocalDbrain(config) {
|
|
447
|
+
const s = p.spinner();
|
|
448
|
+
s.start("Checking dbrain CLI");
|
|
449
|
+
try {
|
|
450
|
+
const { execSync } = await import("child_process");
|
|
451
|
+
execSync("dbrain --help", { stdio: "ignore" });
|
|
452
|
+
s.stop("dbrain CLI found");
|
|
453
|
+
} catch {
|
|
454
|
+
s.stop(pc2.red("dbrain CLI not found"));
|
|
455
|
+
p.log.error("Install dbrain first: " + pc2.cyan("npm i -g @dtoolkit/dbrain"));
|
|
456
|
+
p.outro(pc2.red("Then run dcontext init again."));
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
const brainPath = await p.text({
|
|
460
|
+
message: "Brain path",
|
|
461
|
+
initialValue: `${process.env.HOME}/.dbrain`,
|
|
462
|
+
validate: (v) => !v ? "Path is required" : void 0
|
|
463
|
+
});
|
|
464
|
+
if (p.isCancel(brainPath)) {
|
|
465
|
+
p.cancel("Init cancelled.");
|
|
466
|
+
process.exit(0);
|
|
467
|
+
}
|
|
468
|
+
const s2 = p.spinner();
|
|
469
|
+
s2.start("Initializing and starting dbrain");
|
|
470
|
+
try {
|
|
471
|
+
const { execSync, spawn } = await import("child_process");
|
|
472
|
+
execSync(`dbrain init "${brainPath}"`, { stdio: "ignore" });
|
|
473
|
+
const child = spawn("dbrain", ["start", brainPath], {
|
|
474
|
+
stdio: "ignore",
|
|
475
|
+
detached: true
|
|
476
|
+
});
|
|
477
|
+
child.unref();
|
|
478
|
+
let healthy = false;
|
|
479
|
+
for (let i = 0; i < 10; i++) {
|
|
480
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
481
|
+
try {
|
|
482
|
+
await new DBrainClient3("http://localhost:7878").health();
|
|
483
|
+
healthy = true;
|
|
484
|
+
break;
|
|
485
|
+
} catch {
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (!healthy) throw new Error("dbrain did not start in time");
|
|
489
|
+
let token = "";
|
|
490
|
+
try {
|
|
491
|
+
const { readFile: readFile2 } = await import("fs/promises");
|
|
492
|
+
const { join: join2 } = await import("path");
|
|
493
|
+
const dbrainConfig = JSON.parse(await readFile2(join2(brainPath, "config.json"), "utf-8"));
|
|
494
|
+
token = dbrainConfig.token || "";
|
|
495
|
+
} catch {
|
|
496
|
+
}
|
|
497
|
+
const client = new DBrainClient3("http://localhost:7878", token);
|
|
498
|
+
const health = await client.health();
|
|
499
|
+
s2.stop(
|
|
500
|
+
`dbrain running \u2014 ${pc2.green(String(health.entities))} entities, ${pc2.green(String(health.facts))} facts`
|
|
501
|
+
);
|
|
502
|
+
config.dbrain = { url: "http://localhost:7878", token };
|
|
503
|
+
await finishInit(config);
|
|
504
|
+
} catch (err) {
|
|
505
|
+
s2.stop(pc2.red("Failed to start dbrain"));
|
|
506
|
+
p.log.error(err.message);
|
|
507
|
+
p.outro(pc2.red("Try starting dbrain manually: dbrain start"));
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function connectToDbrain(config) {
|
|
512
|
+
const answers = await p.group(
|
|
513
|
+
{
|
|
514
|
+
url: () => p.text({
|
|
515
|
+
message: "dbrain URL",
|
|
516
|
+
initialValue: config.dbrain.url || "http://localhost:7878",
|
|
517
|
+
validate: (v) => !v ? "URL is required" : void 0
|
|
518
|
+
}),
|
|
519
|
+
token: () => p.text({
|
|
520
|
+
message: "dbrain token (empty if none)",
|
|
521
|
+
initialValue: config.dbrain.token || ""
|
|
522
|
+
})
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
onCancel: () => {
|
|
526
|
+
p.cancel("Init cancelled.");
|
|
527
|
+
process.exit(0);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
);
|
|
531
|
+
const s = p.spinner();
|
|
532
|
+
s.start("Connecting to dbrain");
|
|
533
|
+
try {
|
|
534
|
+
const client = new DBrainClient3(answers.url, answers.token);
|
|
535
|
+
const health = await client.health();
|
|
536
|
+
s.stop(
|
|
537
|
+
`Connected \u2014 ${pc2.green(String(health.entities))} entities, ${pc2.green(String(health.facts))} facts`
|
|
538
|
+
);
|
|
539
|
+
} catch (err) {
|
|
540
|
+
s.stop(pc2.red("Connection failed"));
|
|
541
|
+
p.log.error(err.message);
|
|
542
|
+
p.outro(pc2.red("Check dbrain URL and token, then try again."));
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
config.dbrain = { url: answers.url, token: answers.token };
|
|
546
|
+
await finishInit(config);
|
|
547
|
+
}
|
|
548
|
+
async function finishInit(config) {
|
|
549
|
+
const client = new DBrainClient3(config.dbrain.url, config.dbrain.token);
|
|
550
|
+
let entityName;
|
|
551
|
+
try {
|
|
552
|
+
const entities = await client.listEntities();
|
|
553
|
+
if (entities.length > 0) {
|
|
554
|
+
const cwd = process.cwd();
|
|
555
|
+
const selected = await p.select({
|
|
556
|
+
message: `Map ${pc2.dim(cwd)} to entity:`,
|
|
557
|
+
options: [
|
|
558
|
+
...entities.map((e) => ({
|
|
559
|
+
value: e.id,
|
|
560
|
+
label: `${e.name} (${e.type}, ${e.category})`
|
|
561
|
+
})),
|
|
562
|
+
{ value: "__skip__", label: "Skip for now" },
|
|
563
|
+
{ value: "__custom__", label: "Enter custom name..." }
|
|
564
|
+
]
|
|
565
|
+
});
|
|
566
|
+
if (p.isCancel(selected)) {
|
|
567
|
+
p.cancel("Init cancelled.");
|
|
568
|
+
process.exit(0);
|
|
569
|
+
}
|
|
570
|
+
if (selected === "__custom__") {
|
|
571
|
+
const custom = await p.text({
|
|
572
|
+
message: "Entity name for this project",
|
|
573
|
+
validate: (v) => !v ? "Name is required" : void 0
|
|
574
|
+
});
|
|
575
|
+
if (p.isCancel(custom)) {
|
|
576
|
+
p.cancel("Init cancelled.");
|
|
577
|
+
process.exit(0);
|
|
578
|
+
}
|
|
579
|
+
entityName = custom;
|
|
580
|
+
try {
|
|
581
|
+
await client.createEntity({
|
|
582
|
+
id: custom.toLowerCase().replace(/\s+/g, "-"),
|
|
583
|
+
name: custom,
|
|
584
|
+
type: "project",
|
|
585
|
+
category: "projects"
|
|
586
|
+
});
|
|
587
|
+
p.log.success(`Entity ${pc2.blue(custom)} created in dbrain`);
|
|
588
|
+
} catch {
|
|
589
|
+
}
|
|
590
|
+
} else if (selected !== "__skip__") {
|
|
591
|
+
entityName = selected;
|
|
592
|
+
}
|
|
593
|
+
if (entityName) {
|
|
594
|
+
config.projects[cwd] = entityName;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
} catch (err) {
|
|
598
|
+
p.log.warn(`Could not list entities: ${err.message}`);
|
|
599
|
+
}
|
|
600
|
+
config.initialized = true;
|
|
601
|
+
await saveConfig(config);
|
|
602
|
+
p.note(
|
|
603
|
+
[
|
|
604
|
+
`dbrain: ${pc2.green(config.dbrain.url)}`,
|
|
605
|
+
entityName ? `Project: ${pc2.green(process.cwd())} \u2192 ${pc2.green(entityName)}` : "",
|
|
606
|
+
`Config: ${pc2.green(getDataDir() + "/config.json")}`
|
|
607
|
+
].filter(Boolean).join("\n"),
|
|
608
|
+
"Configuration"
|
|
609
|
+
);
|
|
610
|
+
p.outro(`Next: ${pc2.cyan("dcontext install claude")}`);
|
|
611
|
+
}
|
|
612
|
+
async function requireInit() {
|
|
613
|
+
const config = await loadConfig();
|
|
614
|
+
if (!config.initialized) {
|
|
615
|
+
console.error(pc2.red("dcontext is not initialized. Run: dcontext init"));
|
|
616
|
+
process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/commands/install.ts
|
|
621
|
+
import { Command as Command4 } from "commander";
|
|
622
|
+
import pc3 from "picocolors";
|
|
623
|
+
function createInstallCommand() {
|
|
624
|
+
return new Command4("install").description("Install hooks for a target CLI").argument("<target>", "Target CLI: claude, gemini, opencode").action(async (targetName) => {
|
|
625
|
+
try {
|
|
626
|
+
await runInstall(targetName);
|
|
627
|
+
} catch (err) {
|
|
628
|
+
console.error(pc3.red(err.message));
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
async function runInstall(targetName) {
|
|
634
|
+
const target = resolveTarget(targetName);
|
|
635
|
+
if (!target) {
|
|
636
|
+
console.error(pc3.red(`Unknown target: ${targetName}. Use: claude, gemini, opencode`));
|
|
637
|
+
process.exit(1);
|
|
638
|
+
}
|
|
639
|
+
const already = await target.isInstalled();
|
|
640
|
+
if (already) {
|
|
641
|
+
console.log(pc3.dim(`dcontext hooks already installed for ${target.name}`));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
await target.install();
|
|
645
|
+
const config = await loadConfig();
|
|
646
|
+
config.targets[target.name] = {
|
|
647
|
+
installed: true,
|
|
648
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
649
|
+
};
|
|
650
|
+
await saveConfig(config);
|
|
651
|
+
console.log(pc3.green(`\u2713 Hooks installed for ${target.name}`));
|
|
652
|
+
if (target.name === "claude") {
|
|
653
|
+
console.log(pc3.dim(` ~/.claude/CLAUDE.md updated with dcontext instructions`));
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/commands/status.ts
|
|
658
|
+
import { DBrainClient as DBrainClient4 } from "@dtoolkit/sdk";
|
|
659
|
+
import { Command as Command5 } from "commander";
|
|
660
|
+
import pc4 from "picocolors";
|
|
661
|
+
function createStatusCommand() {
|
|
662
|
+
return new Command5("status").description("Show configuration, context, and stats").action(async () => {
|
|
663
|
+
try {
|
|
664
|
+
await runStatus();
|
|
665
|
+
} catch (err) {
|
|
666
|
+
console.error(pc4.red(err.message));
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
async function runStatus() {
|
|
672
|
+
const config = await loadConfig();
|
|
673
|
+
const stats = await loadStats();
|
|
674
|
+
console.log(pc4.bold("dcontext status"));
|
|
675
|
+
console.log();
|
|
676
|
+
console.log(pc4.bold("dbrain"));
|
|
677
|
+
const { url, token } = config.dbrain;
|
|
678
|
+
if (url) {
|
|
679
|
+
try {
|
|
680
|
+
const client = new DBrainClient4(url, token);
|
|
681
|
+
const health = await client.health();
|
|
682
|
+
console.log(` ${pc4.green(url)} \u2014 ${health.entities} entities, ${health.facts} facts`);
|
|
683
|
+
} catch {
|
|
684
|
+
console.log(` ${pc4.red(url)} (unreachable)`);
|
|
685
|
+
}
|
|
686
|
+
} else {
|
|
687
|
+
console.log(` ${pc4.red("not configured")} \u2014 run ${pc4.cyan("dcontext init")}`);
|
|
688
|
+
}
|
|
689
|
+
console.log();
|
|
690
|
+
console.log(pc4.bold("Targets"));
|
|
691
|
+
for (const name of ["claude", "gemini", "opencode"]) {
|
|
692
|
+
const target = resolveTarget(name);
|
|
693
|
+
if (!target) continue;
|
|
694
|
+
const installed = await target.isInstalled();
|
|
695
|
+
const cfg = config.targets[name];
|
|
696
|
+
if (installed) {
|
|
697
|
+
const since = cfg?.installedAt ? ` (since ${cfg.installedAt.split("T")[0]})` : "";
|
|
698
|
+
console.log(` ${name}: ${pc4.green("installed")}${pc4.dim(since)}`);
|
|
699
|
+
} else {
|
|
700
|
+
console.log(` ${name}: ${pc4.dim("not installed")}`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
console.log();
|
|
704
|
+
const cwd = process.cwd();
|
|
705
|
+
const entity = config.projects[cwd];
|
|
706
|
+
console.log(pc4.bold("Context") + pc4.dim(` (${cwd})`));
|
|
707
|
+
if (entity) {
|
|
708
|
+
console.log(` Entity: ${pc4.blue(entity)}`);
|
|
709
|
+
if (url) {
|
|
710
|
+
try {
|
|
711
|
+
const client = new DBrainClient4(url, token);
|
|
712
|
+
const results = await client.search(`"${entity}"`, { limit: 5 });
|
|
713
|
+
if (results.length > 0) {
|
|
714
|
+
console.log(` Facts: ${pc4.green(String(results.length))}+ matching`);
|
|
715
|
+
for (const r of results.slice(0, 3)) {
|
|
716
|
+
console.log(` ${pc4.dim("\xB7")} ${r.fact.fact.slice(0, 80)}`);
|
|
717
|
+
}
|
|
718
|
+
if (results.length > 3) {
|
|
719
|
+
console.log(` ${pc4.dim(`... and ${results.length - 3} more`)}`);
|
|
720
|
+
}
|
|
721
|
+
} else {
|
|
722
|
+
console.log(` Facts: ${pc4.dim("none found")}`);
|
|
723
|
+
}
|
|
724
|
+
} catch {
|
|
725
|
+
console.log(` Facts: ${pc4.dim("could not query")}`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
} else {
|
|
729
|
+
console.log(
|
|
730
|
+
` ${pc4.dim("no entity mapped")} \u2014 run ${pc4.cyan("dcontext init")} in this directory`
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
console.log();
|
|
734
|
+
const projects = Object.entries(config.projects).filter(([path]) => path !== cwd);
|
|
735
|
+
if (projects.length > 0) {
|
|
736
|
+
console.log(pc4.bold("Other projects"));
|
|
737
|
+
for (const [path, ent] of projects) {
|
|
738
|
+
console.log(` ${pc4.dim(path)} \u2192 ${pc4.blue(ent)}`);
|
|
739
|
+
}
|
|
740
|
+
console.log();
|
|
741
|
+
}
|
|
742
|
+
console.log(pc4.bold("Stats"));
|
|
743
|
+
console.log(
|
|
744
|
+
` Briefings: ${stats.briefings.total}${stats.briefings.lastAt ? pc4.dim(` (last: ${stats.briefings.lastAt.split("T")[0]})`) : ""}`
|
|
745
|
+
);
|
|
746
|
+
console.log(
|
|
747
|
+
` Extractions: ${stats.extractions.total}${stats.extractions.messagesSaved ? ` (${stats.extractions.messagesSaved} messages)` : ""}${stats.extractions.lastAt ? pc4.dim(` (last: ${stats.extractions.lastAt.split("T")[0]})`) : ""}`
|
|
748
|
+
);
|
|
749
|
+
console.log();
|
|
750
|
+
console.log(pc4.dim(`Config: ${getDataDir()}/config.json`));
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/commands/uninstall.ts
|
|
754
|
+
import { Command as Command6 } from "commander";
|
|
755
|
+
import pc5 from "picocolors";
|
|
756
|
+
function createUninstallCommand() {
|
|
757
|
+
return new Command6("uninstall").description("Remove hooks for a target CLI").argument("<target>", "Target CLI: claude, gemini, opencode").action(async (targetName) => {
|
|
758
|
+
try {
|
|
759
|
+
await runUninstall(targetName);
|
|
760
|
+
} catch (err) {
|
|
761
|
+
console.error(pc5.red(err.message));
|
|
762
|
+
process.exit(1);
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
async function runUninstall(targetName) {
|
|
767
|
+
const target = resolveTarget(targetName);
|
|
768
|
+
if (!target) {
|
|
769
|
+
console.error(pc5.red(`Unknown target: ${targetName}. Use: claude, gemini, opencode`));
|
|
770
|
+
process.exit(1);
|
|
771
|
+
}
|
|
772
|
+
const installed = await target.isInstalled();
|
|
773
|
+
if (!installed) {
|
|
774
|
+
console.log(pc5.dim(`dcontext hooks not installed for ${target.name}`));
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
await target.uninstall();
|
|
778
|
+
const config = await loadConfig();
|
|
779
|
+
config.targets[target.name] = { installed: false };
|
|
780
|
+
await saveConfig(config);
|
|
781
|
+
console.log(pc5.green(`\u2713 Hooks removed for ${target.name}`));
|
|
782
|
+
if (target.name === "claude") {
|
|
783
|
+
console.log(
|
|
784
|
+
pc5.dim(` Run ${pc5.cyan("dbrain connect claude")} to restore original dbrain instructions`)
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/index.ts
|
|
790
|
+
var program = new Command7();
|
|
791
|
+
var banner = ` _ _ _
|
|
792
|
+
| | | | | |
|
|
793
|
+
__| | ___ ___ _ __ | |_ _____ _| |_
|
|
794
|
+
/ _\` |/ __/ _ \\| '_ \\| __/ _ \\ \\/ / __|
|
|
795
|
+
| (_| | (_| (_) | | | | || __/> <| |_
|
|
796
|
+
\\__,_|\\___\\___/|_| |_|\\__\\___/_/\\_\\\\__|`;
|
|
797
|
+
var description = `${pc6.green(banner)}
|
|
798
|
+
|
|
799
|
+
${pc6.green("dbrain hooks for AI coding CLIs")}
|
|
800
|
+
${pc6.dim("Part of the dtoolkit suite")}`;
|
|
801
|
+
program.name("dcontext").description(description).version("0.1.0");
|
|
802
|
+
var guarded = (cmd) => {
|
|
803
|
+
cmd.hook("preAction", async () => {
|
|
804
|
+
await requireInit();
|
|
805
|
+
});
|
|
806
|
+
return cmd;
|
|
807
|
+
};
|
|
808
|
+
program.addCommand(createInitCommand());
|
|
809
|
+
program.addCommand(guarded(createInstallCommand()));
|
|
810
|
+
program.addCommand(guarded(createUninstallCommand()));
|
|
811
|
+
program.addCommand(guarded(createStatusCommand()));
|
|
812
|
+
program.addCommand(guarded(createExploreCommand()));
|
|
813
|
+
program.addCommand(createHookCommand());
|
|
814
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dtoolkit/dcontext",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "dbrain hooks for AI coding CLIs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Iván Campillo <ivncmp@gmail.com>",
|
|
@@ -32,19 +32,29 @@
|
|
|
32
32
|
"keywords": [
|
|
33
33
|
"ai",
|
|
34
34
|
"context",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
35
|
+
"dbrain",
|
|
36
|
+
"hooks",
|
|
37
37
|
"dtoolkit"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@
|
|
40
|
+
"@clack/prompts": "^1.3.0",
|
|
41
|
+
"commander": "^13.0.0",
|
|
42
|
+
"picocolors": "^1.1.0",
|
|
43
|
+
"@dtoolkit/adapter-gemini": "1.3.0",
|
|
44
|
+
"@dtoolkit/adapter-claude": "1.3.0",
|
|
45
|
+
"@dtoolkit/core": "0.4.0",
|
|
46
|
+
"@dtoolkit/sdk": "0.3.1",
|
|
47
|
+
"@dtoolkit/adapter-opencode": "1.3.0",
|
|
48
|
+
"@dtoolkit/adapter-codex": "1.3.0"
|
|
41
49
|
},
|
|
42
50
|
"devDependencies": {
|
|
43
51
|
"@types/node": "^22.0.0",
|
|
52
|
+
"tsup": "^8.0.0",
|
|
44
53
|
"typescript": "^5.7.0"
|
|
45
54
|
},
|
|
46
55
|
"scripts": {
|
|
47
|
-
"build": "
|
|
56
|
+
"build": "tsup",
|
|
57
|
+
"dev": "tsup --watch",
|
|
48
58
|
"lint": "eslint src/",
|
|
49
59
|
"lint:fix": "eslint src/ --fix"
|
|
50
60
|
}
|
package/dist/index.d.ts
DELETED
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,cAAc"}
|