@contextstream/mcp-server 0.4.82 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +25 -0
- package/README.md +174 -290
- package/assets/contextstream-logo.png +0 -0
- package/npm/bin/contextstream-hook.mjs +9 -0
- package/npm/bin/contextstream-mcp.mjs +9 -0
- package/npm/bin/mcp-server.mjs +9 -0
- package/npm/launcher.mjs +390 -0
- package/package.json +28 -72
- package/server.json +43 -0
- package/dist/hooks/auto-rules.js +0 -3529
- package/dist/hooks/media-aware.js +0 -103
- package/dist/hooks/notification.js +0 -162
- package/dist/hooks/on-bash.js +0 -190
- package/dist/hooks/on-read.js +0 -321
- package/dist/hooks/on-save-intent.js +0 -132
- package/dist/hooks/on-task.js +0 -139
- package/dist/hooks/on-web.js +0 -155
- package/dist/hooks/permission-request.js +0 -174
- package/dist/hooks/post-compact.js +0 -172
- package/dist/hooks/post-tool-use-failure.js +0 -213
- package/dist/hooks/post-write.js +0 -1023
- package/dist/hooks/pre-compact.js +0 -317
- package/dist/hooks/pre-tool-use.js +0 -553
- package/dist/hooks/runner.js +0 -5826
- package/dist/hooks/session-end.js +0 -285
- package/dist/hooks/session-init.js +0 -1880
- package/dist/hooks/stop.js +0 -171
- package/dist/hooks/subagent-start.js +0 -195
- package/dist/hooks/subagent-stop.js +0 -301
- package/dist/hooks/task-completed.js +0 -260
- package/dist/hooks/teammate-idle.js +0 -200
- package/dist/hooks/user-prompt-submit.js +0 -866
- package/dist/index.js +0 -41985
- package/dist/test-server.js +0 -4363
- package/scripts/postinstall.js +0 -56
- package/scripts/registration-smoke.mts +0 -45
package/dist/hooks/on-read.js
DELETED
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/hooks/on-read.ts
|
|
4
|
-
import * as fs2 from "node:fs";
|
|
5
|
-
import * as path2 from "node:path";
|
|
6
|
-
import { homedir as homedir2 } from "node:os";
|
|
7
|
-
|
|
8
|
-
// src/hot-paths.ts
|
|
9
|
-
import * as fs from "node:fs";
|
|
10
|
-
import * as path from "node:path";
|
|
11
|
-
import { homedir } from "node:os";
|
|
12
|
-
var STORE_VERSION = 1;
|
|
13
|
-
var STORE_DIR = path.join(homedir(), ".contextstream");
|
|
14
|
-
var STORE_FILE = path.join(STORE_DIR, "hot-paths.json");
|
|
15
|
-
var MAX_PATHS_PER_SCOPE = 400;
|
|
16
|
-
var HALF_LIFE_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
17
|
-
function clamp(value, min, max) {
|
|
18
|
-
return Math.max(min, Math.min(max, value));
|
|
19
|
-
}
|
|
20
|
-
function normalizePathKey(input) {
|
|
21
|
-
return input.replace(/\\/g, "/").trim();
|
|
22
|
-
}
|
|
23
|
-
function toScopeKey(input) {
|
|
24
|
-
const workspace = input.workspace_id || "none";
|
|
25
|
-
const project = input.project_id || "none";
|
|
26
|
-
return `${workspace}:${project}`;
|
|
27
|
-
}
|
|
28
|
-
function signalWeight(signal) {
|
|
29
|
-
switch (signal) {
|
|
30
|
-
case "search_result":
|
|
31
|
-
return 1.8;
|
|
32
|
-
case "activity_edit":
|
|
33
|
-
return 1.4;
|
|
34
|
-
case "activity_focus":
|
|
35
|
-
return 1.2;
|
|
36
|
-
case "activity_read":
|
|
37
|
-
default:
|
|
38
|
-
return 1;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function looksBroadQuery(query) {
|
|
42
|
-
const q = query.trim().toLowerCase();
|
|
43
|
-
if (!q) return true;
|
|
44
|
-
if (q.length <= 2) return true;
|
|
45
|
-
if (/^\*+$/.test(q) || q === "all files" || q === "everything") return true;
|
|
46
|
-
const tokens = q.split(/\s+/).filter(Boolean);
|
|
47
|
-
return tokens.length > 12;
|
|
48
|
-
}
|
|
49
|
-
var HotPathStore = class {
|
|
50
|
-
constructor() {
|
|
51
|
-
this.data = { version: STORE_VERSION, scopes: {} };
|
|
52
|
-
this.load();
|
|
53
|
-
}
|
|
54
|
-
recordPaths(scope, paths, signal) {
|
|
55
|
-
if (paths.length === 0) return;
|
|
56
|
-
const now = Date.now();
|
|
57
|
-
const scopeKey = toScopeKey(scope);
|
|
58
|
-
const profile = this.ensureScope(scopeKey);
|
|
59
|
-
const weight = signalWeight(signal);
|
|
60
|
-
for (const raw of paths) {
|
|
61
|
-
const pathKey = normalizePathKey(raw);
|
|
62
|
-
if (!pathKey) continue;
|
|
63
|
-
const current = profile.paths[pathKey] || { score: 0, last_seen: now, hits: 0 };
|
|
64
|
-
const decayed = this.decayedScore(current.score, current.last_seen, now);
|
|
65
|
-
profile.paths[pathKey] = {
|
|
66
|
-
score: decayed + weight,
|
|
67
|
-
last_seen: now,
|
|
68
|
-
hits: current.hits + 1
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
profile.updated_at = now;
|
|
72
|
-
this.pruneScope(profile);
|
|
73
|
-
this.persist();
|
|
74
|
-
}
|
|
75
|
-
buildHint(input) {
|
|
76
|
-
const scopeKey = toScopeKey(input);
|
|
77
|
-
const profile = this.data.scopes[scopeKey];
|
|
78
|
-
if (!profile) return void 0;
|
|
79
|
-
const now = Date.now();
|
|
80
|
-
const baseEntries = Object.entries(profile.paths).map(([filePath, entry]) => ({
|
|
81
|
-
path: filePath,
|
|
82
|
-
score: this.decayedScore(entry.score, entry.last_seen, now)
|
|
83
|
-
})).filter((entry) => entry.score > 0.05).sort((a, b) => b.score - a.score);
|
|
84
|
-
const active = (input.active_paths || []).map(normalizePathKey).filter(Boolean);
|
|
85
|
-
const merged = /* @__PURE__ */ new Map();
|
|
86
|
-
for (const entry of baseEntries.slice(0, Math.max(12, input.limit || 8))) {
|
|
87
|
-
merged.set(entry.path, {
|
|
88
|
-
path: entry.path,
|
|
89
|
-
score: Number(entry.score.toFixed(4)),
|
|
90
|
-
source: "history"
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
for (const activePath of active) {
|
|
94
|
-
const existing = merged.get(activePath);
|
|
95
|
-
if (existing) {
|
|
96
|
-
existing.score = Number((existing.score + 0.9).toFixed(4));
|
|
97
|
-
} else {
|
|
98
|
-
merged.set(activePath, { path: activePath, score: 0.9, source: "active" });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
const limit = clamp(input.limit ?? 8, 1, 12);
|
|
102
|
-
const entries = [...merged.values()].sort((a, b) => b.score - a.score).slice(0, limit);
|
|
103
|
-
if (entries.length === 0) return void 0;
|
|
104
|
-
const scoreSum = entries.reduce((sum, item) => sum + item.score, 0);
|
|
105
|
-
const normalized = clamp(scoreSum / (limit * 2.5), 0, 1);
|
|
106
|
-
const confidencePenalty = looksBroadQuery(input.query) ? 0.55 : 1;
|
|
107
|
-
const confidence = Number((normalized * confidencePenalty).toFixed(3));
|
|
108
|
-
return {
|
|
109
|
-
entries,
|
|
110
|
-
confidence,
|
|
111
|
-
generated_at: new Date(now).toISOString(),
|
|
112
|
-
profile_version: STORE_VERSION
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
decayedScore(score, lastSeenMs, nowMs) {
|
|
116
|
-
const elapsed = Math.max(0, nowMs - lastSeenMs);
|
|
117
|
-
const decay = Math.pow(0.5, elapsed / HALF_LIFE_MS);
|
|
118
|
-
return score * decay;
|
|
119
|
-
}
|
|
120
|
-
ensureScope(scopeKey) {
|
|
121
|
-
if (!this.data.scopes[scopeKey]) {
|
|
122
|
-
this.data.scopes[scopeKey] = { paths: {}, updated_at: Date.now() };
|
|
123
|
-
}
|
|
124
|
-
return this.data.scopes[scopeKey];
|
|
125
|
-
}
|
|
126
|
-
pruneScope(profile) {
|
|
127
|
-
const entries = Object.entries(profile.paths);
|
|
128
|
-
if (entries.length <= MAX_PATHS_PER_SCOPE) return;
|
|
129
|
-
entries.sort((a, b) => b[1].score - a[1].score).slice(MAX_PATHS_PER_SCOPE).forEach(([key]) => delete profile.paths[key]);
|
|
130
|
-
}
|
|
131
|
-
load() {
|
|
132
|
-
try {
|
|
133
|
-
if (!fs.existsSync(STORE_FILE)) return;
|
|
134
|
-
const parsed = JSON.parse(fs.readFileSync(STORE_FILE, "utf-8"));
|
|
135
|
-
if (parsed?.version !== STORE_VERSION || !parsed.scopes) return;
|
|
136
|
-
this.data = parsed;
|
|
137
|
-
} catch {
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
persist() {
|
|
141
|
-
try {
|
|
142
|
-
fs.mkdirSync(STORE_DIR, { recursive: true });
|
|
143
|
-
fs.writeFileSync(STORE_FILE, JSON.stringify(this.data));
|
|
144
|
-
} catch {
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
var globalHotPathStore = new HotPathStore();
|
|
149
|
-
|
|
150
|
-
// src/hooks/on-read.ts
|
|
151
|
-
var ENABLED = process.env.CONTEXTSTREAM_READ_HOOK_ENABLED !== "false";
|
|
152
|
-
var API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
|
|
153
|
-
var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
154
|
-
var WORKSPACE_ID = null;
|
|
155
|
-
var recentCaptures = /* @__PURE__ */ new Set();
|
|
156
|
-
var CAPTURE_WINDOW_MS = 6e4;
|
|
157
|
-
function loadConfigFromMcpJson(cwd) {
|
|
158
|
-
let searchDir = path2.resolve(cwd);
|
|
159
|
-
for (let i = 0; i < 5; i++) {
|
|
160
|
-
if (!API_KEY) {
|
|
161
|
-
const mcpPath = path2.join(searchDir, ".mcp.json");
|
|
162
|
-
if (fs2.existsSync(mcpPath)) {
|
|
163
|
-
try {
|
|
164
|
-
const content = fs2.readFileSync(mcpPath, "utf-8");
|
|
165
|
-
const config = JSON.parse(content);
|
|
166
|
-
const csEnv = config.mcpServers?.contextstream?.env;
|
|
167
|
-
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
168
|
-
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
169
|
-
}
|
|
170
|
-
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
171
|
-
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
172
|
-
}
|
|
173
|
-
} catch {
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
if (!WORKSPACE_ID) {
|
|
178
|
-
const csConfigPath = path2.join(searchDir, ".contextstream", "config.json");
|
|
179
|
-
if (fs2.existsSync(csConfigPath)) {
|
|
180
|
-
try {
|
|
181
|
-
const content = fs2.readFileSync(csConfigPath, "utf-8");
|
|
182
|
-
const csConfig = JSON.parse(content);
|
|
183
|
-
if (csConfig.workspace_id) {
|
|
184
|
-
WORKSPACE_ID = csConfig.workspace_id;
|
|
185
|
-
}
|
|
186
|
-
} catch {
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
const parentDir = path2.dirname(searchDir);
|
|
191
|
-
if (parentDir === searchDir) break;
|
|
192
|
-
searchDir = parentDir;
|
|
193
|
-
}
|
|
194
|
-
if (!API_KEY) {
|
|
195
|
-
const homeMcpPath = path2.join(homedir2(), ".mcp.json");
|
|
196
|
-
if (fs2.existsSync(homeMcpPath)) {
|
|
197
|
-
try {
|
|
198
|
-
const content = fs2.readFileSync(homeMcpPath, "utf-8");
|
|
199
|
-
const config = JSON.parse(content);
|
|
200
|
-
const csEnv = config.mcpServers?.contextstream?.env;
|
|
201
|
-
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
202
|
-
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
203
|
-
}
|
|
204
|
-
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
205
|
-
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
206
|
-
}
|
|
207
|
-
} catch {
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
async function captureExploration(toolName, target, resultSummary, sessionId) {
|
|
213
|
-
if (!API_KEY) return;
|
|
214
|
-
const cacheKey = `${toolName}:${target}`;
|
|
215
|
-
if (recentCaptures.has(cacheKey)) {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
recentCaptures.add(cacheKey);
|
|
219
|
-
setTimeout(() => recentCaptures.delete(cacheKey), CAPTURE_WINDOW_MS);
|
|
220
|
-
const payload = {
|
|
221
|
-
event_type: "file_exploration",
|
|
222
|
-
title: `${toolName}: ${target.slice(0, 50)}`,
|
|
223
|
-
content: JSON.stringify({
|
|
224
|
-
tool: toolName,
|
|
225
|
-
target,
|
|
226
|
-
result_summary: resultSummary.slice(0, 500),
|
|
227
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
228
|
-
}),
|
|
229
|
-
importance: "low",
|
|
230
|
-
tags: ["exploration", toolName.toLowerCase()],
|
|
231
|
-
source_type: "hook",
|
|
232
|
-
session_id: sessionId
|
|
233
|
-
};
|
|
234
|
-
if (WORKSPACE_ID) {
|
|
235
|
-
payload.workspace_id = WORKSPACE_ID;
|
|
236
|
-
}
|
|
237
|
-
try {
|
|
238
|
-
const controller = new AbortController();
|
|
239
|
-
const timeoutId = setTimeout(() => controller.abort(), 3e3);
|
|
240
|
-
await fetch(`${API_URL}/api/v1/memory/events`, {
|
|
241
|
-
method: "POST",
|
|
242
|
-
headers: {
|
|
243
|
-
"Content-Type": "application/json",
|
|
244
|
-
"X-API-Key": API_KEY
|
|
245
|
-
},
|
|
246
|
-
body: JSON.stringify(payload),
|
|
247
|
-
signal: controller.signal
|
|
248
|
-
});
|
|
249
|
-
clearTimeout(timeoutId);
|
|
250
|
-
} catch {
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
async function runOnReadHook() {
|
|
254
|
-
if (!ENABLED) {
|
|
255
|
-
process.exit(0);
|
|
256
|
-
}
|
|
257
|
-
let inputData = "";
|
|
258
|
-
for await (const chunk of process.stdin) {
|
|
259
|
-
inputData += chunk;
|
|
260
|
-
}
|
|
261
|
-
if (!inputData.trim()) {
|
|
262
|
-
process.exit(0);
|
|
263
|
-
}
|
|
264
|
-
let input;
|
|
265
|
-
try {
|
|
266
|
-
input = JSON.parse(inputData);
|
|
267
|
-
} catch {
|
|
268
|
-
process.exit(0);
|
|
269
|
-
}
|
|
270
|
-
const toolName = input.tool_name || "";
|
|
271
|
-
if (!["Read", "Glob", "Grep"].includes(toolName)) {
|
|
272
|
-
process.exit(0);
|
|
273
|
-
}
|
|
274
|
-
const cwd = input.cwd || process.cwd();
|
|
275
|
-
loadConfigFromMcpJson(cwd);
|
|
276
|
-
const sessionId = input.session_id || "unknown";
|
|
277
|
-
let target = "";
|
|
278
|
-
let resultSummary = "";
|
|
279
|
-
switch (toolName) {
|
|
280
|
-
case "Read":
|
|
281
|
-
target = input.tool_input?.file_path || "";
|
|
282
|
-
resultSummary = `Read file: ${target}`;
|
|
283
|
-
if (target) {
|
|
284
|
-
globalHotPathStore.recordPaths(
|
|
285
|
-
{ workspace_id: WORKSPACE_ID || void 0, project_id: void 0 },
|
|
286
|
-
[target],
|
|
287
|
-
"activity_read"
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
break;
|
|
291
|
-
case "Glob":
|
|
292
|
-
target = input.tool_input?.pattern || "";
|
|
293
|
-
const globFiles = input.tool_result?.files || [];
|
|
294
|
-
resultSummary = `Found ${globFiles.length} files matching ${target}`;
|
|
295
|
-
if (globFiles.length > 0) {
|
|
296
|
-
globalHotPathStore.recordPaths(
|
|
297
|
-
{ workspace_id: WORKSPACE_ID || void 0, project_id: void 0 },
|
|
298
|
-
globFiles.slice(0, 30),
|
|
299
|
-
"activity_focus"
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
break;
|
|
303
|
-
case "Grep":
|
|
304
|
-
target = input.tool_input?.pattern || "";
|
|
305
|
-
const matches = input.tool_result?.matches || 0;
|
|
306
|
-
resultSummary = `Found ${matches} matches for "${target}"`;
|
|
307
|
-
break;
|
|
308
|
-
}
|
|
309
|
-
if (target) {
|
|
310
|
-
captureExploration(toolName, target, resultSummary, sessionId).catch(() => {
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
process.exit(0);
|
|
314
|
-
}
|
|
315
|
-
var isDirectRun = process.argv[1]?.includes("on-read") || process.argv[2] === "on-read";
|
|
316
|
-
if (isDirectRun) {
|
|
317
|
-
runOnReadHook().catch(() => process.exit(0));
|
|
318
|
-
}
|
|
319
|
-
export {
|
|
320
|
-
runOnReadHook
|
|
321
|
-
};
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/hooks/on-save-intent.ts
|
|
4
|
-
var ENABLED = process.env.CONTEXTSTREAM_SAVE_INTENT_ENABLED !== "false";
|
|
5
|
-
var SAVE_PATTERNS = [
|
|
6
|
-
// Direct save requests
|
|
7
|
-
/\b(save|store|record|capture|log|document|write down|note down|keep track)\b.*\b(this|that|it|the)\b/i,
|
|
8
|
-
/\b(save|store|record|capture|log)\b.*\b(to|in|for)\b.*\b(contextstream|memory|later|reference|future)\b/i,
|
|
9
|
-
// Document creation
|
|
10
|
-
/\b(create|make|write|draft)\b.*\b(a|the)\b.*\b(document|doc|note|summary|report|spec|design)\b/i,
|
|
11
|
-
/\b(document|summarize|write up)\b.*\b(this|that|the|our)\b.*\b(decision|discussion|conversation|meeting|finding)\b/i,
|
|
12
|
-
// Memory/reference requests
|
|
13
|
-
/\b(remember|don't forget|keep in mind|note that|important to remember)\b/i,
|
|
14
|
-
/\bfor\s+(future|later)\s+reference\b/i,
|
|
15
|
-
/\b(add|put)\s+(this|it|that)\s+(to|in)\s+(memory|notes|docs)\b/i,
|
|
16
|
-
// Decision tracking
|
|
17
|
-
/\b(we\s+)?(decided|agreed|concluded|determined)\b.*\b(to|that)\b/i,
|
|
18
|
-
/\blet('s|s)\s+document\b/i,
|
|
19
|
-
/\bsave\s+(this|the)\s+(decision|choice|approach)\b/i,
|
|
20
|
-
// Implementation/design docs
|
|
21
|
-
/\b(implementation|design|architecture|spec)\s+(doc|document|plan)\b/i,
|
|
22
|
-
/\bwrite\s+(the|a|an)\s+.*(md|markdown|readme)\b/i
|
|
23
|
-
];
|
|
24
|
-
var LOCAL_FILE_PATTERNS = [
|
|
25
|
-
/\b(save|write|create)\s+(it|this|the\s+\w+)\s+(to|in|as)\s+[./~]/i,
|
|
26
|
-
/\b(save|write)\s+to\s+.*(\.md|\.txt|\.json|docs\/|notes\/)/i,
|
|
27
|
-
/\bcreate\s+(a|the)\s+file\b/i
|
|
28
|
-
];
|
|
29
|
-
function detectsSaveIntent(text) {
|
|
30
|
-
const hasSaveIntent = SAVE_PATTERNS.some((p) => p.test(text));
|
|
31
|
-
const isLocalFile = LOCAL_FILE_PATTERNS.some((p) => p.test(text));
|
|
32
|
-
return { hasSaveIntent, isLocalFile };
|
|
33
|
-
}
|
|
34
|
-
var SAVE_GUIDANCE = `[CONTEXTSTREAM DOCUMENT STORAGE]
|
|
35
|
-
The user wants to save/store content. Use ContextStream instead of local files:
|
|
36
|
-
|
|
37
|
-
**For decisions/notes:**
|
|
38
|
-
\`\`\`
|
|
39
|
-
mcp__contextstream__session(
|
|
40
|
-
action="capture",
|
|
41
|
-
event_type="decision|note|insight",
|
|
42
|
-
title="...",
|
|
43
|
-
content="...",
|
|
44
|
-
importance="high|medium|low"
|
|
45
|
-
)
|
|
46
|
-
\`\`\`
|
|
47
|
-
|
|
48
|
-
**For documents/specs:**
|
|
49
|
-
\`\`\`
|
|
50
|
-
mcp__contextstream__docs(
|
|
51
|
-
action="create",
|
|
52
|
-
title="...",
|
|
53
|
-
content="...",
|
|
54
|
-
doc_type="implementation|design|spec|guide"
|
|
55
|
-
)
|
|
56
|
-
\`\`\`
|
|
57
|
-
|
|
58
|
-
**For plans:**
|
|
59
|
-
\`\`\`
|
|
60
|
-
mcp__contextstream__session(
|
|
61
|
-
action="capture_plan",
|
|
62
|
-
title="...",
|
|
63
|
-
steps=[...]
|
|
64
|
-
)
|
|
65
|
-
\`\`\`
|
|
66
|
-
|
|
67
|
-
**Why ContextStream?**
|
|
68
|
-
- Persists across sessions (local files don't)
|
|
69
|
-
- Searchable and retrievable
|
|
70
|
-
- Shows up in context automatically
|
|
71
|
-
- Can be shared with team
|
|
72
|
-
|
|
73
|
-
Only save to local files if user explicitly requests a specific file path.
|
|
74
|
-
[END GUIDANCE]`;
|
|
75
|
-
async function runOnSaveIntentHook() {
|
|
76
|
-
if (!ENABLED) {
|
|
77
|
-
process.exit(0);
|
|
78
|
-
}
|
|
79
|
-
let inputData = "";
|
|
80
|
-
for await (const chunk of process.stdin) {
|
|
81
|
-
inputData += chunk;
|
|
82
|
-
}
|
|
83
|
-
if (!inputData.trim()) {
|
|
84
|
-
process.exit(0);
|
|
85
|
-
}
|
|
86
|
-
let input;
|
|
87
|
-
try {
|
|
88
|
-
input = JSON.parse(inputData);
|
|
89
|
-
} catch {
|
|
90
|
-
process.exit(0);
|
|
91
|
-
}
|
|
92
|
-
let prompt = input.prompt || "";
|
|
93
|
-
if (!prompt && input.session?.messages) {
|
|
94
|
-
for (const msg of [...input.session.messages].reverse()) {
|
|
95
|
-
if (msg.role === "user") {
|
|
96
|
-
if (typeof msg.content === "string") {
|
|
97
|
-
prompt = msg.content;
|
|
98
|
-
} else if (Array.isArray(msg.content)) {
|
|
99
|
-
for (const block of msg.content) {
|
|
100
|
-
if (block.type === "text" && block.text) {
|
|
101
|
-
prompt = block.text;
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (!prompt) {
|
|
111
|
-
process.exit(0);
|
|
112
|
-
}
|
|
113
|
-
const { hasSaveIntent, isLocalFile } = detectsSaveIntent(prompt);
|
|
114
|
-
if (hasSaveIntent || isLocalFile) {
|
|
115
|
-
console.log(
|
|
116
|
-
JSON.stringify({
|
|
117
|
-
hookSpecificOutput: {
|
|
118
|
-
hookEventName: "UserPromptSubmit",
|
|
119
|
-
additionalContext: SAVE_GUIDANCE
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
process.exit(0);
|
|
125
|
-
}
|
|
126
|
-
var isDirectRun = process.argv[1]?.includes("on-save-intent") || process.argv[2] === "on-save-intent";
|
|
127
|
-
if (isDirectRun) {
|
|
128
|
-
runOnSaveIntentHook().catch(() => process.exit(0));
|
|
129
|
-
}
|
|
130
|
-
export {
|
|
131
|
-
runOnSaveIntentHook
|
|
132
|
-
};
|
package/dist/hooks/on-task.js
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/hooks/on-task.ts
|
|
4
|
-
import * as fs from "node:fs";
|
|
5
|
-
import * as path from "node:path";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
|
-
var ENABLED = process.env.CONTEXTSTREAM_TASK_HOOK_ENABLED !== "false";
|
|
8
|
-
var API_URL = process.env.CONTEXTSTREAM_API_URL || "https://api.contextstream.io";
|
|
9
|
-
var API_KEY = process.env.CONTEXTSTREAM_API_KEY || "";
|
|
10
|
-
var WORKSPACE_ID = null;
|
|
11
|
-
function loadConfigFromMcpJson(cwd) {
|
|
12
|
-
let searchDir = path.resolve(cwd);
|
|
13
|
-
for (let i = 0; i < 5; i++) {
|
|
14
|
-
if (!API_KEY) {
|
|
15
|
-
const mcpPath = path.join(searchDir, ".mcp.json");
|
|
16
|
-
if (fs.existsSync(mcpPath)) {
|
|
17
|
-
try {
|
|
18
|
-
const content = fs.readFileSync(mcpPath, "utf-8");
|
|
19
|
-
const config = JSON.parse(content);
|
|
20
|
-
const csEnv = config.mcpServers?.contextstream?.env;
|
|
21
|
-
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
22
|
-
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
23
|
-
}
|
|
24
|
-
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
25
|
-
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
26
|
-
}
|
|
27
|
-
} catch {
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
if (!WORKSPACE_ID) {
|
|
32
|
-
const csConfigPath = path.join(searchDir, ".contextstream", "config.json");
|
|
33
|
-
if (fs.existsSync(csConfigPath)) {
|
|
34
|
-
try {
|
|
35
|
-
const content = fs.readFileSync(csConfigPath, "utf-8");
|
|
36
|
-
const csConfig = JSON.parse(content);
|
|
37
|
-
if (csConfig.workspace_id) {
|
|
38
|
-
WORKSPACE_ID = csConfig.workspace_id;
|
|
39
|
-
}
|
|
40
|
-
} catch {
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
const parentDir = path.dirname(searchDir);
|
|
45
|
-
if (parentDir === searchDir) break;
|
|
46
|
-
searchDir = parentDir;
|
|
47
|
-
}
|
|
48
|
-
if (!API_KEY) {
|
|
49
|
-
const homeMcpPath = path.join(homedir(), ".mcp.json");
|
|
50
|
-
if (fs.existsSync(homeMcpPath)) {
|
|
51
|
-
try {
|
|
52
|
-
const content = fs.readFileSync(homeMcpPath, "utf-8");
|
|
53
|
-
const config = JSON.parse(content);
|
|
54
|
-
const csEnv = config.mcpServers?.contextstream?.env;
|
|
55
|
-
if (csEnv?.CONTEXTSTREAM_API_KEY) {
|
|
56
|
-
API_KEY = csEnv.CONTEXTSTREAM_API_KEY;
|
|
57
|
-
}
|
|
58
|
-
if (csEnv?.CONTEXTSTREAM_API_URL) {
|
|
59
|
-
API_URL = csEnv.CONTEXTSTREAM_API_URL;
|
|
60
|
-
}
|
|
61
|
-
} catch {
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
async function captureTaskInvocation(description, prompt, agentType, result, sessionId) {
|
|
67
|
-
if (!API_KEY) return;
|
|
68
|
-
const payload = {
|
|
69
|
-
event_type: "task_agent",
|
|
70
|
-
title: `Agent: ${agentType} - ${description}`,
|
|
71
|
-
content: JSON.stringify({
|
|
72
|
-
description,
|
|
73
|
-
prompt: prompt.slice(0, 1e3),
|
|
74
|
-
agent_type: agentType,
|
|
75
|
-
result: result.slice(0, 2e3),
|
|
76
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
77
|
-
}),
|
|
78
|
-
importance: "medium",
|
|
79
|
-
tags: ["task", "agent", agentType.toLowerCase()],
|
|
80
|
-
source_type: "hook",
|
|
81
|
-
session_id: sessionId
|
|
82
|
-
};
|
|
83
|
-
if (WORKSPACE_ID) {
|
|
84
|
-
payload.workspace_id = WORKSPACE_ID;
|
|
85
|
-
}
|
|
86
|
-
try {
|
|
87
|
-
const controller = new AbortController();
|
|
88
|
-
const timeoutId = setTimeout(() => controller.abort(), 3e3);
|
|
89
|
-
await fetch(`${API_URL}/api/v1/memory/events`, {
|
|
90
|
-
method: "POST",
|
|
91
|
-
headers: {
|
|
92
|
-
"Content-Type": "application/json",
|
|
93
|
-
"X-API-Key": API_KEY
|
|
94
|
-
},
|
|
95
|
-
body: JSON.stringify(payload),
|
|
96
|
-
signal: controller.signal
|
|
97
|
-
});
|
|
98
|
-
clearTimeout(timeoutId);
|
|
99
|
-
} catch {
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
async function runOnTaskHook() {
|
|
103
|
-
if (!ENABLED) {
|
|
104
|
-
process.exit(0);
|
|
105
|
-
}
|
|
106
|
-
let inputData = "";
|
|
107
|
-
for await (const chunk of process.stdin) {
|
|
108
|
-
inputData += chunk;
|
|
109
|
-
}
|
|
110
|
-
if (!inputData.trim()) {
|
|
111
|
-
process.exit(0);
|
|
112
|
-
}
|
|
113
|
-
let input;
|
|
114
|
-
try {
|
|
115
|
-
input = JSON.parse(inputData);
|
|
116
|
-
} catch {
|
|
117
|
-
process.exit(0);
|
|
118
|
-
}
|
|
119
|
-
if (input.tool_name !== "Task") {
|
|
120
|
-
process.exit(0);
|
|
121
|
-
}
|
|
122
|
-
const cwd = input.cwd || process.cwd();
|
|
123
|
-
loadConfigFromMcpJson(cwd);
|
|
124
|
-
const description = input.tool_input?.description || "Unknown task";
|
|
125
|
-
const prompt = input.tool_input?.prompt || "";
|
|
126
|
-
const agentType = input.tool_input?.subagent_type || "general-purpose";
|
|
127
|
-
const result = input.tool_result?.output || "";
|
|
128
|
-
const sessionId = input.session_id || "unknown";
|
|
129
|
-
captureTaskInvocation(description, prompt, agentType, result, sessionId).catch(() => {
|
|
130
|
-
});
|
|
131
|
-
process.exit(0);
|
|
132
|
-
}
|
|
133
|
-
var isDirectRun = process.argv[1]?.includes("on-task") || process.argv[2] === "on-task";
|
|
134
|
-
if (isDirectRun) {
|
|
135
|
-
runOnTaskHook().catch(() => process.exit(0));
|
|
136
|
-
}
|
|
137
|
-
export {
|
|
138
|
-
runOnTaskHook
|
|
139
|
-
};
|