qualitative-research-pro 1.0.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/AGENTS.md +108 -0
- package/CLAUDE.md +171 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/agents/analysis-orchestrator.md +162 -0
- package/agents/audit-trail-builder.md +127 -0
- package/agents/category-developer.md +179 -0
- package/agents/citation-manager.md +83 -0
- package/agents/constant-comparator.md +135 -0
- package/agents/data-manager.md +104 -0
- package/agents/discussion-writer.md +128 -0
- package/agents/document-analyst.md +114 -0
- package/agents/ethics-reviewer.md +119 -0
- package/agents/field-note-analyst.md +124 -0
- package/agents/fit-assessor.md +192 -0
- package/agents/grounded-theorist.md +210 -0
- package/agents/literature-integrator.md +169 -0
- package/agents/literature-reviewer.md +112 -0
- package/agents/memo-writer.md +234 -0
- package/agents/methodology-critic.md +166 -0
- package/agents/methods-writer.md +109 -0
- package/agents/open-coder.md +187 -0
- package/agents/pattern-analyst.md +166 -0
- package/agents/peer-reviewer.md +129 -0
- package/agents/planner.md +122 -0
- package/agents/proposal-writer.md +108 -0
- package/agents/reflexivity-auditor.md +128 -0
- package/agents/research-designer.md +164 -0
- package/agents/research-writer.md +100 -0
- package/agents/saturation-assessor.md +159 -0
- package/agents/selective-coder.md +167 -0
- package/agents/theoretical-coder.md +260 -0
- package/agents/theoretical-sampler.md +165 -0
- package/agents/transcript-analyst.md +123 -0
- package/bin/cli.mjs +236 -0
- package/hooks/dist/agent-memory-loader.mjs +94 -0
- package/hooks/dist/agent-memory-saver.mjs +113 -0
- package/hooks/dist/bash-audit-log.mjs +71 -0
- package/hooks/dist/credential-deny.mjs +165 -0
- package/hooks/dist/forge-compile-check.mjs +92 -0
- package/hooks/dist/gas-snapshot-diff.mjs +71 -0
- package/hooks/dist/memory-awareness.mjs +276 -0
- package/hooks/dist/natspec-enforcer.mjs +67 -0
- package/hooks/dist/passive-learner.mjs +220 -0
- package/hooks/dist/pre-compact-continuity.mjs +467 -0
- package/hooks/dist/sast-on-edit.mjs +230 -0
- package/hooks/dist/session-analytics.mjs +84 -0
- package/hooks/dist/session-end-cleanup.mjs +121 -0
- package/hooks/dist/session-outcome.mjs +84 -0
- package/hooks/dist/session-register.mjs +307 -0
- package/hooks/dist/session-start-continuity.mjs +405 -0
- package/hooks/dist/slither-on-save.mjs +87 -0
- package/hooks/dist/storage-layout-check.mjs +89 -0
- package/hooks/dist/transcript-parser.mjs +214 -0
- package/install.sh +194 -0
- package/package.json +46 -0
- package/plugin.json +19 -0
- package/rules/academic-writing-style.md +42 -0
- package/rules/citation-standards.md +47 -0
- package/rules/current-methodological-state.md +40 -0
- package/rules/data-handling.md +44 -0
- package/rules/finding-output-format.md +47 -0
- package/rules/gt-coding-standards.md +40 -0
- package/rules/methodological-rigor.md +56 -0
- package/rules/quality-criteria.md +41 -0
- package/rules/reflexivity-requirements.md +40 -0
- package/rules/research-ethics-standards.md +44 -0
- package/skills/.gitkeep +2 -0
- package/skills/academic-writing/SKILL.md +73 -0
- package/skills/action-research/SKILL.md +96 -0
- package/skills/apa-formatting/SKILL.md +85 -0
- package/skills/case-study-methods/SKILL.md +96 -0
- package/skills/category-development/SKILL.md +80 -0
- package/skills/chicago-formatting/SKILL.md +81 -0
- package/skills/coding-pipeline/SKILL.md +81 -0
- package/skills/conceptual-frameworks/SKILL.md +70 -0
- package/skills/constant-comparison/SKILL.md +188 -0
- package/skills/constructivist-gt/SKILL.md +91 -0
- package/skills/data-management-protocols/SKILL.md +67 -0
- package/skills/document-analysis/SKILL.md +66 -0
- package/skills/ethnographic-methods/SKILL.md +82 -0
- package/skills/focus-group-methods/SKILL.md +66 -0
- package/skills/formal-theory/SKILL.md +159 -0
- package/skills/glaserian-grounded-theory/SKILL.md +212 -0
- package/skills/interview-design/SKILL.md +67 -0
- package/skills/literature-synthesis/SKILL.md +71 -0
- package/skills/member-checking/SKILL.md +66 -0
- package/skills/memo-writing/SKILL.md +158 -0
- package/skills/mixed-methods-design/SKILL.md +69 -0
- package/skills/narrative-inquiry/SKILL.md +101 -0
- package/skills/observation-methods/SKILL.md +67 -0
- package/skills/open-coding/SKILL.md +176 -0
- package/skills/paradigmatic-positioning/SKILL.md +72 -0
- package/skills/peer-debriefing/SKILL.md +72 -0
- package/skills/phenomenological-methods/SKILL.md +91 -0
- package/skills/qualitative-rigor/SKILL.md +78 -0
- package/skills/reflexive-practice/SKILL.md +64 -0
- package/skills/research-ethics/SKILL.md +64 -0
- package/skills/research-proposal-writing/SKILL.md +81 -0
- package/skills/research-questions/SKILL.md +66 -0
- package/skills/sampling-strategies/SKILL.md +61 -0
- package/skills/selective-coding/SKILL.md +183 -0
- package/skills/situational-analysis/SKILL.md +93 -0
- package/skills/substantive-theory/SKILL.md +169 -0
- package/skills/thematic-analysis/SKILL.md +80 -0
- package/skills/theoretical-coding/SKILL.md +213 -0
- package/skills/theoretical-sampling/SKILL.md +152 -0
- package/skills/theoretical-saturation/SKILL.md +179 -0
- package/skills/theoretical-sensitivity/SKILL.md +175 -0
- package/skills/theory-integration/SKILL.md +85 -0
- package/skills/thick-description/SKILL.md +69 -0
- package/skills/triangulation/SKILL.md +65 -0
- package/skills/visual-modeling/SKILL.md +66 -0
- package/skills/vulnerable-populations/SKILL.md +69 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// src/sast-on-edit.ts
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
var SUPPORTED_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".java", ".rb", ".php"];
|
|
4
|
+
var SECURITY_PATTERNS = [
|
|
5
|
+
// CRITICAL: Code execution
|
|
6
|
+
{
|
|
7
|
+
pattern: /\beval\s*\(/,
|
|
8
|
+
severity: "CRITICAL",
|
|
9
|
+
category: "Code Injection",
|
|
10
|
+
message: "eval() kullanimi tespit edildi - kullanici girdisi ile RCE riski"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
pattern: /\bnew\s+Function\s*\(/,
|
|
14
|
+
severity: "CRITICAL",
|
|
15
|
+
category: "Code Injection",
|
|
16
|
+
message: "new Function() kullanimi tespit edildi - dinamik kod calistirma riski"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
pattern: /\bexec\s*\(/,
|
|
20
|
+
severity: "CRITICAL",
|
|
21
|
+
category: "Command Injection",
|
|
22
|
+
message: "exec() kullanimi tespit edildi - komut enjeksiyonu riski"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
pattern: /\bexecSync\s*\(/,
|
|
26
|
+
severity: "CRITICAL",
|
|
27
|
+
category: "Command Injection",
|
|
28
|
+
message: "execSync() kullanimi tespit edildi - komut enjeksiyonu riski"
|
|
29
|
+
},
|
|
30
|
+
// CRITICAL: Command injection (Python)
|
|
31
|
+
{
|
|
32
|
+
pattern: /\bos\.system\s*\(/,
|
|
33
|
+
severity: "CRITICAL",
|
|
34
|
+
category: "Command Injection",
|
|
35
|
+
message: "os.system() kullanimi tespit edildi - komut enjeksiyonu riski"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
pattern: /\bsubprocess\.call\s*\(/,
|
|
39
|
+
severity: "CRITICAL",
|
|
40
|
+
category: "Command Injection",
|
|
41
|
+
message: "subprocess.call() kullanimi tespit edildi - shell=True ile komut enjeksiyonu riski"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
pattern: /\bsubprocess\.Popen\s*\(/,
|
|
45
|
+
severity: "CRITICAL",
|
|
46
|
+
category: "Command Injection",
|
|
47
|
+
message: "subprocess.Popen() kullanimi tespit edildi - shell parametresini kontrol et"
|
|
48
|
+
},
|
|
49
|
+
// CRITICAL: SQL injection patterns
|
|
50
|
+
{
|
|
51
|
+
pattern: /["'`]\s*\+\s*\w+.*(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE)\b/i,
|
|
52
|
+
severity: "CRITICAL",
|
|
53
|
+
category: "SQL Injection",
|
|
54
|
+
message: "SQL sorgusunda string birlestirme tespit edildi - parameterized query kullan"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
pattern: /(?:SELECT|INSERT|UPDATE|DELETE)\b.*\$\{/i,
|
|
58
|
+
severity: "CRITICAL",
|
|
59
|
+
category: "SQL Injection",
|
|
60
|
+
message: "SQL sorgusunda template literal tespit edildi - parameterized query kullan"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
pattern: /f["'](?:SELECT|INSERT|UPDATE|DELETE)\b/i,
|
|
64
|
+
severity: "CRITICAL",
|
|
65
|
+
category: "SQL Injection",
|
|
66
|
+
message: "Python f-string ile SQL sorgusu tespit edildi - parameterized query kullan"
|
|
67
|
+
},
|
|
68
|
+
// HIGH: XSS patterns
|
|
69
|
+
{
|
|
70
|
+
pattern: /\.innerHTML\s*=/,
|
|
71
|
+
severity: "HIGH",
|
|
72
|
+
category: "XSS",
|
|
73
|
+
message: "innerHTML kullanimi tespit edildi - DOMPurify ile sanitize et"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
pattern: /dangerouslySetInnerHTML/,
|
|
77
|
+
severity: "HIGH",
|
|
78
|
+
category: "XSS",
|
|
79
|
+
message: "dangerouslySetInnerHTML kullanimi tespit edildi - DOMPurify ile sanitize et"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
pattern: /document\.write\s*\(/,
|
|
83
|
+
severity: "HIGH",
|
|
84
|
+
category: "XSS",
|
|
85
|
+
message: "document.write() kullanimi tespit edildi - XSS riski"
|
|
86
|
+
},
|
|
87
|
+
// HIGH: SSRF patterns
|
|
88
|
+
{
|
|
89
|
+
pattern: /fetch\s*\(\s*(?:user|req|request|input|param|query|body)\w*/,
|
|
90
|
+
severity: "HIGH",
|
|
91
|
+
category: "SSRF",
|
|
92
|
+
message: "Kullanici girdisi ile fetch cagrisi tespit edildi - URL whitelist kullan"
|
|
93
|
+
},
|
|
94
|
+
// HIGH: Deserialization
|
|
95
|
+
{
|
|
96
|
+
pattern: /pickle\.loads?\s*\(/,
|
|
97
|
+
severity: "HIGH",
|
|
98
|
+
category: "Insecure Deserialization",
|
|
99
|
+
message: "pickle.load(s) kullanimi tespit edildi - guvenilmeyen veri ile RCE riski"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
pattern: /yaml\.load\s*\([^)]*\)\s*(?!.*Loader)/,
|
|
103
|
+
severity: "HIGH",
|
|
104
|
+
category: "Insecure Deserialization",
|
|
105
|
+
message: "yaml.load() Loader parametresi olmadan kullanilmis - yaml.safe_load() kullan"
|
|
106
|
+
},
|
|
107
|
+
// MEDIUM: Sensitive data exposure
|
|
108
|
+
{
|
|
109
|
+
pattern: /console\.log\s*\(.*(?:password|secret|token|key|credential|auth)/i,
|
|
110
|
+
severity: "MEDIUM",
|
|
111
|
+
category: "Data Exposure",
|
|
112
|
+
message: "Hassas veri console.log ile yazdiriliyor - loglardan hassas veriyi cikar"
|
|
113
|
+
},
|
|
114
|
+
// MEDIUM: Insecure crypto
|
|
115
|
+
{
|
|
116
|
+
pattern: /\bMD5\s*\(|\.md5\s*\(/i,
|
|
117
|
+
severity: "MEDIUM",
|
|
118
|
+
category: "Weak Cryptography",
|
|
119
|
+
message: "MD5 kullanimi tespit edildi - sifreleme icin SHA-256+ veya bcrypt kullan"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
pattern: /\bSHA1\s*\(|\.sha1\s*\(/i,
|
|
123
|
+
severity: "MEDIUM",
|
|
124
|
+
category: "Weak Cryptography",
|
|
125
|
+
message: "SHA1 kullanimi tespit edildi - SHA-256+ kullan"
|
|
126
|
+
}
|
|
127
|
+
];
|
|
128
|
+
function getFileExtension(filePath) {
|
|
129
|
+
const match = filePath.match(/\.[^.]+$/);
|
|
130
|
+
return match ? match[0].toLowerCase() : "";
|
|
131
|
+
}
|
|
132
|
+
function isSupportedFile(filePath) {
|
|
133
|
+
const ext = getFileExtension(filePath);
|
|
134
|
+
return SUPPORTED_EXTENSIONS.includes(ext);
|
|
135
|
+
}
|
|
136
|
+
function extractFilePath(input) {
|
|
137
|
+
if (input.tool_input?.file_path) {
|
|
138
|
+
return input.tool_input.file_path;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
function scanContent(content) {
|
|
143
|
+
const findings = [];
|
|
144
|
+
const lines = content.split("\n");
|
|
145
|
+
for (const line of lines) {
|
|
146
|
+
const trimmed = line.trim();
|
|
147
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const pattern of SECURITY_PATTERNS) {
|
|
151
|
+
if (pattern.pattern.test(line)) {
|
|
152
|
+
if (!findings.some((f) => f.category === pattern.category && f.severity === pattern.severity)) {
|
|
153
|
+
findings.push(pattern);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return findings;
|
|
159
|
+
}
|
|
160
|
+
function formatFindings(findings) {
|
|
161
|
+
if (findings.length === 0) return "";
|
|
162
|
+
const criticals = findings.filter((f) => f.severity === "CRITICAL");
|
|
163
|
+
const highs = findings.filter((f) => f.severity === "HIGH");
|
|
164
|
+
const mediums = findings.filter((f) => f.severity === "MEDIUM");
|
|
165
|
+
const parts = ["SAST UYARI: Guvenlik taramasi gerektiren pattern(ler) tespit edildi."];
|
|
166
|
+
if (criticals.length > 0) {
|
|
167
|
+
parts.push(`CRITICAL (${criticals.length}): ${criticals.map((f) => f.message).join("; ")}`);
|
|
168
|
+
}
|
|
169
|
+
if (highs.length > 0) {
|
|
170
|
+
parts.push(`HIGH (${highs.length}): ${highs.map((f) => f.message).join("; ")}`);
|
|
171
|
+
}
|
|
172
|
+
if (mediums.length > 0) {
|
|
173
|
+
parts.push(`MEDIUM (${mediums.length}): ${mediums.map((f) => f.message).join("; ")}`);
|
|
174
|
+
}
|
|
175
|
+
parts.push("sast-scanner agent veya semgrep --config auto ile detayli tarama yap.");
|
|
176
|
+
return parts.join("\n");
|
|
177
|
+
}
|
|
178
|
+
function main() {
|
|
179
|
+
let input;
|
|
180
|
+
try {
|
|
181
|
+
const stdinContent = readFileSync(0, "utf-8");
|
|
182
|
+
input = JSON.parse(stdinContent);
|
|
183
|
+
} catch {
|
|
184
|
+
console.log(JSON.stringify({ result: "continue" }));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (input.tool_name !== "Edit" && input.tool_name !== "Write") {
|
|
188
|
+
console.log(JSON.stringify({ result: "continue" }));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const filePath = extractFilePath(input);
|
|
192
|
+
if (!filePath) {
|
|
193
|
+
console.log(JSON.stringify({ result: "continue" }));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (!isSupportedFile(filePath)) {
|
|
197
|
+
console.log(JSON.stringify({ result: "continue" }));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const contentToScan = [];
|
|
201
|
+
if (input.tool_input?.new_string && typeof input.tool_input.new_string === "string") {
|
|
202
|
+
contentToScan.push(input.tool_input.new_string);
|
|
203
|
+
}
|
|
204
|
+
if (input.tool_input?.content && typeof input.tool_input.content === "string") {
|
|
205
|
+
contentToScan.push(input.tool_input.content);
|
|
206
|
+
}
|
|
207
|
+
if (input.tool_output) {
|
|
208
|
+
contentToScan.push(input.tool_output);
|
|
209
|
+
}
|
|
210
|
+
if (contentToScan.length === 0) {
|
|
211
|
+
const fileName = filePath.split("/").pop() || filePath;
|
|
212
|
+
console.log(JSON.stringify({
|
|
213
|
+
result: "continue",
|
|
214
|
+
systemMessage: `SAST: ${fileName} dosyasi edit edildi. Guvenlik acisi olabilecek pattern'ler icin sast-scanner agent'i veya semgrep kullanarak tarama yap.`
|
|
215
|
+
}));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const allContent = contentToScan.join("\n");
|
|
219
|
+
const findings = scanContent(allContent);
|
|
220
|
+
if (findings.length === 0) {
|
|
221
|
+
console.log(JSON.stringify({ result: "continue" }));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const message = formatFindings(findings);
|
|
225
|
+
console.log(JSON.stringify({
|
|
226
|
+
result: "continue",
|
|
227
|
+
systemMessage: message
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
main();
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/session-analytics.ts
|
|
2
|
+
import { readFileSync, appendFileSync, existsSync, mkdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
function readJsonl(path) {
|
|
6
|
+
if (!existsSync(path)) return [];
|
|
7
|
+
const lines = readFileSync(path, "utf-8").split("\n").filter((l) => l.trim());
|
|
8
|
+
const results = [];
|
|
9
|
+
for (const line of lines) {
|
|
10
|
+
try {
|
|
11
|
+
results.push(JSON.parse(line));
|
|
12
|
+
} catch {
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return results;
|
|
16
|
+
}
|
|
17
|
+
function main() {
|
|
18
|
+
let raw = "";
|
|
19
|
+
try {
|
|
20
|
+
raw = readFileSync(0, "utf-8");
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
let sessionId = "unknown";
|
|
24
|
+
if (raw) {
|
|
25
|
+
try {
|
|
26
|
+
const input = JSON.parse(raw);
|
|
27
|
+
sessionId = input.session_id?.slice(0, 8) || "unknown";
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const claudeDir = join(homedir(), ".claude");
|
|
32
|
+
const cacheDir = join(claudeDir, "cache");
|
|
33
|
+
if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true });
|
|
34
|
+
const eventsPath = join(claudeDir, "agent-events.jsonl");
|
|
35
|
+
const perfPath = join(cacheDir, "hook-perf.jsonl");
|
|
36
|
+
const ledgerPath = join(claudeDir, "canavar", "error-ledger.jsonl");
|
|
37
|
+
const outputPath = join(cacheDir, "session-analytics.jsonl");
|
|
38
|
+
const allEvents = readJsonl(eventsPath);
|
|
39
|
+
const sessionEvents = allEvents.filter((e) => e.session === sessionId);
|
|
40
|
+
const allPerf = readJsonl(perfPath);
|
|
41
|
+
const sessionPerf = allPerf.filter((p) => p.session === sessionId);
|
|
42
|
+
const allErrors = readJsonl(ledgerPath);
|
|
43
|
+
const sessionErrors = allErrors.filter((e) => e.session === sessionId);
|
|
44
|
+
const toolCounts = {};
|
|
45
|
+
for (const evt of sessionEvents) {
|
|
46
|
+
if (evt.tool) {
|
|
47
|
+
toolCounts[evt.tool] = (toolCounts[evt.tool] || 0) + 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const agentSpawns = sessionEvents.filter((e) => e.event === "agent_spawn" || e.tool === "Agent").length;
|
|
51
|
+
let hookTotalMs = 0;
|
|
52
|
+
let hookSlowest = null;
|
|
53
|
+
for (const p of sessionPerf) {
|
|
54
|
+
hookTotalMs += p.duration_ms;
|
|
55
|
+
if (!hookSlowest || p.duration_ms > hookSlowest.ms) {
|
|
56
|
+
hookSlowest = { name: p.hook, ms: p.duration_ms };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
let durationMs = 0;
|
|
60
|
+
if (sessionEvents.length > 0) {
|
|
61
|
+
const timestamps = sessionEvents.map((e) => new Date(e.ts).getTime()).filter((t) => !isNaN(t));
|
|
62
|
+
if (timestamps.length >= 2) {
|
|
63
|
+
durationMs = Math.max(...timestamps) - Math.min(...timestamps);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const analytics = {
|
|
67
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
68
|
+
session_id: sessionId,
|
|
69
|
+
duration_ms: durationMs,
|
|
70
|
+
tool_counts: toolCounts,
|
|
71
|
+
agent_spawns: agentSpawns,
|
|
72
|
+
errors: sessionErrors.length,
|
|
73
|
+
hook_total_ms: Math.round(hookTotalMs * 100) / 100,
|
|
74
|
+
hook_slowest: hookSlowest ? { name: hookSlowest.name, ms: Math.round(hookSlowest.ms * 100) / 100 } : null
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
appendFileSync(outputPath, JSON.stringify(analytics) + "\n");
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
console.log(JSON.stringify({
|
|
81
|
+
result: `Analytics: ${Object.values(toolCounts).reduce((a, b) => a + b, 0)} tool calls, ${agentSpawns} agents, ${sessionErrors.length} errors, ${Math.round(hookTotalMs)}ms hook overhead`
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
main();
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/session-end-cleanup.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
var EXTRACTOR_LOCK = path.join(process.env.HOME || process.env.USERPROFILE || "", ".claude", "braintrust-extractor.lock");
|
|
6
|
+
var LOCK_MAX_AGE_MS = 5 * 60 * 1e3;
|
|
7
|
+
function isExtractorRunning() {
|
|
8
|
+
if (!fs.existsSync(EXTRACTOR_LOCK)) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const lockContent = fs.readFileSync(EXTRACTOR_LOCK, "utf-8").trim();
|
|
13
|
+
const [pidStr, timestampStr] = lockContent.split(":");
|
|
14
|
+
const pid = parseInt(pidStr, 10);
|
|
15
|
+
const timestamp = parseInt(timestampStr, 10);
|
|
16
|
+
if (Date.now() - timestamp > LOCK_MAX_AGE_MS) {
|
|
17
|
+
fs.unlinkSync(EXTRACTOR_LOCK);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
process.kill(pid, 0);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
fs.unlinkSync(EXTRACTOR_LOCK);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
try {
|
|
29
|
+
fs.unlinkSync(EXTRACTOR_LOCK);
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function createExtractorLock(pid) {
|
|
36
|
+
try {
|
|
37
|
+
const lockDir = path.dirname(EXTRACTOR_LOCK);
|
|
38
|
+
if (!fs.existsSync(lockDir)) {
|
|
39
|
+
fs.mkdirSync(lockDir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
fs.writeFileSync(EXTRACTOR_LOCK, `${pid}:${Date.now()}`);
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function main() {
|
|
46
|
+
const input = JSON.parse(await readStdin());
|
|
47
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
48
|
+
try {
|
|
49
|
+
const ledgerDir = path.join(projectDir, "thoughts", "ledgers");
|
|
50
|
+
const ledgerFiles = fs.readdirSync(ledgerDir).filter((f) => f.startsWith("CONTINUITY_CLAUDE-") && f.endsWith(".md"));
|
|
51
|
+
if (ledgerFiles.length > 0) {
|
|
52
|
+
const mostRecent = ledgerFiles.sort((a, b) => {
|
|
53
|
+
const statA = fs.statSync(path.join(ledgerDir, a));
|
|
54
|
+
const statB = fs.statSync(path.join(ledgerDir, b));
|
|
55
|
+
return statB.mtime.getTime() - statA.mtime.getTime();
|
|
56
|
+
})[0];
|
|
57
|
+
const ledgerPath = path.join(ledgerDir, mostRecent);
|
|
58
|
+
let content = fs.readFileSync(ledgerPath, "utf-8");
|
|
59
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
60
|
+
content = content.replace(
|
|
61
|
+
/Updated: .*/,
|
|
62
|
+
`Updated: ${timestamp}`
|
|
63
|
+
);
|
|
64
|
+
fs.writeFileSync(ledgerPath, content);
|
|
65
|
+
}
|
|
66
|
+
const agentCacheDir = path.join(projectDir, ".claude", "cache", "agents");
|
|
67
|
+
if (fs.existsSync(agentCacheDir)) {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const maxAge = 7 * 24 * 60 * 60 * 1e3;
|
|
70
|
+
const agents = fs.readdirSync(agentCacheDir);
|
|
71
|
+
for (const agent of agents) {
|
|
72
|
+
const agentDir = path.join(agentCacheDir, agent);
|
|
73
|
+
const stat = fs.statSync(agentDir);
|
|
74
|
+
if (stat.isDirectory()) {
|
|
75
|
+
const outputFile = path.join(agentDir, "latest-output.md");
|
|
76
|
+
if (fs.existsSync(outputFile)) {
|
|
77
|
+
const fileStat = fs.statSync(outputFile);
|
|
78
|
+
if (now - fileStat.mtime.getTime() > maxAge) {
|
|
79
|
+
fs.unlinkSync(outputFile);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!process.env.BRAINTRUST_API_KEY) {
|
|
86
|
+
console.log(JSON.stringify({ result: "continue" }));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const learnScript = path.join(projectDir, "scripts", "braintrust_analyze.py");
|
|
90
|
+
const globalScript = path.join(process.env.HOME || process.env.USERPROFILE || "", ".claude", "scripts", "braintrust_analyze.py");
|
|
91
|
+
const scriptPath = fs.existsSync(learnScript) ? learnScript : globalScript;
|
|
92
|
+
if (fs.existsSync(scriptPath)) {
|
|
93
|
+
if (isExtractorRunning()) {
|
|
94
|
+
console.log(JSON.stringify({ result: "continue" }));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const isGlobalScript = scriptPath === globalScript;
|
|
98
|
+
const args = isGlobalScript ? ["run", "--with", "braintrust", "--with", "openai", "--with", "aiohttp", "python", scriptPath, "--learn", "--session-id", input.session_id] : ["run", "python", scriptPath, "--learn", "--session-id", input.session_id];
|
|
99
|
+
const child = spawn("uv", args, {
|
|
100
|
+
cwd: projectDir,
|
|
101
|
+
detached: true,
|
|
102
|
+
stdio: "ignore"
|
|
103
|
+
});
|
|
104
|
+
if (child.pid) {
|
|
105
|
+
createExtractorLock(child.pid);
|
|
106
|
+
}
|
|
107
|
+
child.unref();
|
|
108
|
+
}
|
|
109
|
+
console.log(JSON.stringify({ result: "continue" }));
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.log(JSON.stringify({ result: "continue" }));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function readStdin() {
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
let data = "";
|
|
117
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
118
|
+
process.stdin.on("end", () => resolve(data));
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/session-outcome.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
async function readStdin() {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
let data = "";
|
|
7
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
8
|
+
process.stdin.on("end", () => resolve(data));
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
async function main() {
|
|
12
|
+
const input = JSON.parse(await readStdin());
|
|
13
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
14
|
+
if (input.reason === "other") {
|
|
15
|
+
console.log(JSON.stringify({ result: "continue" }));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const dbPath = path.join(projectDir, ".claude", "cache", "artifact-index", "context.db");
|
|
19
|
+
const dbExists = fs.existsSync(dbPath);
|
|
20
|
+
if (!dbExists) {
|
|
21
|
+
console.log(JSON.stringify({ result: "continue" }));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const ledgerDir = path.join(projectDir, "thoughts", "ledgers");
|
|
25
|
+
let ledgerFiles;
|
|
26
|
+
try {
|
|
27
|
+
ledgerFiles = fs.readdirSync(ledgerDir).filter((f) => f.startsWith("CONTINUITY_CLAUDE-") && f.endsWith(".md")).sort((a, b) => {
|
|
28
|
+
const statA = fs.statSync(path.join(ledgerDir, a));
|
|
29
|
+
const statB = fs.statSync(path.join(ledgerDir, b));
|
|
30
|
+
return statB.mtime.getTime() - statA.mtime.getTime();
|
|
31
|
+
});
|
|
32
|
+
} catch {
|
|
33
|
+
console.log(JSON.stringify({ result: "continue" }));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (ledgerFiles.length === 0) {
|
|
37
|
+
console.log(JSON.stringify({ result: "continue" }));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const sessionName = ledgerFiles[0].replace("CONTINUITY_CLAUDE-", "").replace(".md", "");
|
|
41
|
+
const handoffDir = path.join(projectDir, "thoughts", "shared", "handoffs", sessionName);
|
|
42
|
+
if (!fs.existsSync(handoffDir)) {
|
|
43
|
+
console.log(JSON.stringify({ result: "continue" }));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const handoffFiles = fs.readdirSync(handoffDir).filter((f) => f.endsWith(".md") && /^\d{4}-\d{2}-\d{2}_/.test(f)).sort((a, b) => {
|
|
47
|
+
return b.localeCompare(a);
|
|
48
|
+
});
|
|
49
|
+
if (handoffFiles.length === 0) {
|
|
50
|
+
console.log(JSON.stringify({ result: "continue" }));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const latestHandoff = handoffFiles[0];
|
|
54
|
+
const handoffName = latestHandoff.replace(".md", "");
|
|
55
|
+
const output = {
|
|
56
|
+
result: "continue",
|
|
57
|
+
message: `
|
|
58
|
+
|
|
59
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
60
|
+
Session ended: ${sessionName}
|
|
61
|
+
Latest handoff: ${handoffName}
|
|
62
|
+
|
|
63
|
+
To mark outcome and improve future sessions:
|
|
64
|
+
|
|
65
|
+
cd ~/.claude && uv run python scripts/core/artifact_mark.py \\
|
|
66
|
+
--handoff <handoff-id> \\
|
|
67
|
+
--outcome SUCCEEDED|PARTIAL_PLUS|PARTIAL_MINUS|FAILED
|
|
68
|
+
|
|
69
|
+
To find handoff ID, query the database:
|
|
70
|
+
|
|
71
|
+
sqlite3 .claude/cache/artifact-index/context.db \\
|
|
72
|
+
"SELECT id, file_path FROM handoffs WHERE session_name='${sessionName}' ORDER BY indexed_at DESC LIMIT 1"
|
|
73
|
+
|
|
74
|
+
Outcome meanings:
|
|
75
|
+
SUCCEEDED - Task completed successfully
|
|
76
|
+
PARTIAL_PLUS - Mostly done, minor issues remain
|
|
77
|
+
PARTIAL_MINUS - Some progress, major issues remain
|
|
78
|
+
FAILED - Task abandoned or blocked
|
|
79
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
80
|
+
`
|
|
81
|
+
};
|
|
82
|
+
console.log(JSON.stringify(output));
|
|
83
|
+
}
|
|
84
|
+
main().catch(console.error);
|