@chigichan24/crune 0.1.4 → 0.1.6
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 +22 -5
- package/dist-cli/__tests__/facets-reader.test.js +180 -0
- package/dist-cli/__tests__/merge-narrow-clusters.test.js +171 -0
- package/dist-cli/__tests__/reusability-scoring.test.js +175 -0
- package/dist-cli/__tests__/tokenizer.test.js +17 -0
- package/dist-cli/analyze-sessions.js +57 -13
- package/dist-cli/cli.js +2 -2
- package/dist-cli/knowledge-graph/clustering.js +108 -0
- package/dist-cli/knowledge-graph/facets-reader.js +188 -0
- package/dist-cli/knowledge-graph/index.js +22 -7
- package/dist-cli/knowledge-graph/reusability.js +42 -6
- package/dist-cli/knowledge-graph/tokenizer.js +11 -3
- package/dist-cli/knowledge-graph/topic-nodes.js +27 -4
- package/dist-cli/knowledge-graph-builder.js +4 -2
- package/dist-cli/session-parser.js +19 -0
- package/dist-cli/skill-server.js +2 -2
- package/dist-cli/skill-synthesizer.js +38 -3
- package/package.json +4 -1
|
@@ -110,7 +110,25 @@ export function classifyDominantRole(memberSessions) {
|
|
|
110
110
|
return "tool-heavy";
|
|
111
111
|
return "user-driven";
|
|
112
112
|
}
|
|
113
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Build a concise label from facets underlying_goal strings.
|
|
115
|
+
* Picks the shortest goal (most concise), truncates at 80 chars.
|
|
116
|
+
*/
|
|
117
|
+
function buildFacetsLabel(memberSessions, facetsMap) {
|
|
118
|
+
const goals = [];
|
|
119
|
+
for (const s of memberSessions) {
|
|
120
|
+
const f = facetsMap.get(s.sessionId);
|
|
121
|
+
if (f?.underlyingGoal)
|
|
122
|
+
goals.push(f.underlyingGoal);
|
|
123
|
+
}
|
|
124
|
+
if (goals.length === 0)
|
|
125
|
+
return undefined;
|
|
126
|
+
// Pick the shortest goal as the most concise summary
|
|
127
|
+
goals.sort((a, b) => a.length - b.length);
|
|
128
|
+
const best = goals[0];
|
|
129
|
+
return best.length > 80 ? best.slice(0, 77) + "..." : best;
|
|
130
|
+
}
|
|
131
|
+
export function buildTopicNodes(clusterMembers, sessions, tfidf, toolIdf, facetsMap) {
|
|
114
132
|
const topics = [];
|
|
115
133
|
for (let ci = 0; ci < clusterMembers.length; ci++) {
|
|
116
134
|
const members = clusterMembers[ci];
|
|
@@ -141,12 +159,17 @@ export function buildTopicNodes(clusterMembers, sessions, tfidf, toolIdf) {
|
|
|
141
159
|
const sortedProjects = [...projectCounts.entries()].sort((a, b) => b[1] - a[1]);
|
|
142
160
|
const dominantProject = sortedProjects[0]?.[0] ?? "";
|
|
143
161
|
const allProjects = [...new Set(memberSessions.map((s) => s.projectDisplayName))];
|
|
144
|
-
// Label:
|
|
145
|
-
const labelKeywords = keywords.slice(0, 3).join(", ");
|
|
162
|
+
// Label: prefer facets goal, fall back to TF-IDF keywords
|
|
146
163
|
const projectSuffix = allProjects.length > 1
|
|
147
164
|
? `(${allProjects.length} projects)`
|
|
148
165
|
: `(${dominantProject.split("/").pop() || dominantProject})`;
|
|
149
|
-
const
|
|
166
|
+
const facetsLabel = facetsMap && facetsMap.size > 0
|
|
167
|
+
? buildFacetsLabel(memberSessions, facetsMap)
|
|
168
|
+
: undefined;
|
|
169
|
+
const labelKeywords = keywords.slice(0, 3).join(", ");
|
|
170
|
+
const label = facetsLabel
|
|
171
|
+
? `${facetsLabel} ${projectSuffix}`
|
|
172
|
+
: `${labelKeywords} ${projectSuffix}`;
|
|
150
173
|
// Aggregate metadata
|
|
151
174
|
const sessionIds = memberSessions.map((s) => s.sessionId);
|
|
152
175
|
const totalDuration = memberSessions.reduce((sum, s) => sum + s.meta.durationMinutes, 0);
|
|
@@ -18,10 +18,12 @@ buildCombinedMatrix, truncatedSvd, interpretLatentDimensions,
|
|
|
18
18
|
// Similarity
|
|
19
19
|
cosineSimilarity, cosineDistance,
|
|
20
20
|
// Clustering
|
|
21
|
-
agglomerativeClusteringFromDistMatrix, findElbowThreshold, clusterWithThresholdFromDistMatrix, splitOversizedClusters,
|
|
21
|
+
agglomerativeClusteringFromDistMatrix, findElbowThreshold, clusterWithThresholdFromDistMatrix, splitOversizedClusters, mergeNarrowClusters,
|
|
22
22
|
// Topic nodes
|
|
23
23
|
extractDominantAction, selectRepresentativePrompts, generateSuggestedPrompt, computeToolSignature, classifyDominantRole, buildTopicNodes,
|
|
24
24
|
// Edges
|
|
25
25
|
buildTopicEdges, classifyEdge, findSharedKeywords, findCommonPathPrefix,
|
|
26
26
|
// Community
|
|
27
|
-
louvainDetection, brandesBetweenness,
|
|
27
|
+
louvainDetection, brandesBetweenness,
|
|
28
|
+
// Reusability
|
|
29
|
+
computeReusabilityScores, readFacetsDir, normalizeGoalCategory, helpfulnessToScore, aggregateFacetsForTopic, } from "./knowledge-graph/index.js";
|
|
@@ -70,6 +70,25 @@ export function inferProjectName(dirName) {
|
|
|
70
70
|
return dirName;
|
|
71
71
|
}
|
|
72
72
|
// ─── JSONL Parser + Turn Builder ────────────────────────────────────────────
|
|
73
|
+
/**
|
|
74
|
+
* Check if a JSONL file is a non-interactive session created by `claude -p`.
|
|
75
|
+
* These sessions contain `queue-operation` entries and should be excluded
|
|
76
|
+
* from the dashboard to prevent synthesis prompts from appearing as sessions.
|
|
77
|
+
*/
|
|
78
|
+
export function isNonInteractiveSession(filePath) {
|
|
79
|
+
try {
|
|
80
|
+
// Read only the first 4KB to check for queue-operation (always near the top)
|
|
81
|
+
const fd = fs.openSync(filePath, "r");
|
|
82
|
+
const buf = Buffer.alloc(4096);
|
|
83
|
+
const bytesRead = fs.readSync(fd, buf, 0, 4096, 0);
|
|
84
|
+
fs.closeSync(fd);
|
|
85
|
+
const head = buf.toString("utf-8", 0, bytesRead);
|
|
86
|
+
return head.includes('"type":"queue-operation"');
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
73
92
|
export async function parseJsonlFile(filePath) {
|
|
74
93
|
const lines = [];
|
|
75
94
|
const stream = fs.createReadStream(filePath, { encoding: "utf-8" });
|
package/dist-cli/skill-server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
-
import { buildSynthesisPrompt, synthesizeWithClaude } from "./skill-synthesizer.js";
|
|
2
|
+
import { buildSynthesisPrompt, synthesizeWithClaude, stripSynthesisPreamble } from "./skill-synthesizer.js";
|
|
3
3
|
// ---------- Helpers ----------
|
|
4
4
|
function readBody(req) {
|
|
5
5
|
return new Promise((resolve, reject) => {
|
|
@@ -34,7 +34,7 @@ async function handleSynthesize(req, res) {
|
|
|
34
34
|
sendJson(res, 500, { success: false, error: result.error });
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
|
-
sendJson(res, 200, { success: true, synthesizedMarkdown: result.stdout });
|
|
37
|
+
sendJson(res, 200, { success: true, synthesizedMarkdown: stripSynthesisPreamble(result.stdout) });
|
|
38
38
|
}
|
|
39
39
|
// ---------- Server ----------
|
|
40
40
|
const isDirectRun = process.argv[1]?.endsWith("skill-server.ts") || process.argv[1]?.endsWith("skill-server.js");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
// ---------- Prompt Builder ----------
|
|
3
3
|
export function buildSynthesisPrompt(body) {
|
|
4
|
-
const { skillCandidate, topicNode, enrichedSequences, graphContext } = body;
|
|
4
|
+
const { skillCandidate, topicNode, enrichedSequences, graphContext, facetsInsights } = body;
|
|
5
5
|
const topicInfo = [
|
|
6
6
|
`## Topic Information`,
|
|
7
7
|
`- **Label:** ${topicNode.label}`,
|
|
@@ -132,13 +132,48 @@ export function buildSynthesisPrompt(body) {
|
|
|
132
132
|
instructionLines.push(`8. If workflow-continuation connections exist, include \`requires\` and/or \`next\` fields in the YAML frontmatter listing the connected topic labels.`);
|
|
133
133
|
}
|
|
134
134
|
const instruction = instructionLines.join("\n");
|
|
135
|
-
|
|
135
|
+
// --- Facets insights section ---
|
|
136
|
+
let facetsSection = "";
|
|
137
|
+
if (facetsInsights) {
|
|
138
|
+
const lines = [`## Session Insights (from /insights analysis)`];
|
|
139
|
+
if (facetsInsights.aggregatedGoals.length > 0) {
|
|
140
|
+
lines.push(`- **Underlying Goals:** ${facetsInsights.aggregatedGoals.join("; ")}`);
|
|
141
|
+
}
|
|
142
|
+
if (facetsInsights.normalizedCategories.length > 0) {
|
|
143
|
+
lines.push(`- **Goal Categories:** ${facetsInsights.normalizedCategories.join(", ")}`);
|
|
144
|
+
}
|
|
145
|
+
lines.push(`- **Success Rate:** ${(facetsInsights.successRate * 100).toFixed(0)}% of sessions achieved their goal`);
|
|
146
|
+
lines.push(`- **Helpfulness Score:** ${(facetsInsights.helpfulnessScore * 100).toFixed(0)}%`);
|
|
147
|
+
if (facetsInsights.commonFrictions.length > 0) {
|
|
148
|
+
lines.push(`- **Common Frictions:** ${facetsInsights.commonFrictions.join(", ")}`);
|
|
149
|
+
for (const detail of facetsInsights.frictionDetails.slice(0, 2)) {
|
|
150
|
+
lines.push(` - ${detail}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
facetsSection = lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
const parts = [topicInfo, prompts, toolSig, toolPatterns, graphPosition, connectedTopicsSection, facetsSection, reference, instruction].filter(Boolean);
|
|
136
156
|
return parts.join("\n\n");
|
|
137
157
|
}
|
|
158
|
+
// ---------- Post-process synthesis output ----------
|
|
159
|
+
/**
|
|
160
|
+
* Strip preamble text that the model sometimes emits before the actual
|
|
161
|
+
* YAML-frontmatter skill markdown (e.g. "Now I have a thorough understanding…").
|
|
162
|
+
* Valid skill output always starts with "---".
|
|
163
|
+
*/
|
|
164
|
+
export function stripSynthesisPreamble(raw) {
|
|
165
|
+
const trimmed = raw.trimStart();
|
|
166
|
+
if (trimmed.startsWith("---"))
|
|
167
|
+
return trimmed;
|
|
168
|
+
const idx = trimmed.indexOf("\n---");
|
|
169
|
+
if (idx !== -1)
|
|
170
|
+
return trimmed.slice(idx + 1);
|
|
171
|
+
return trimmed;
|
|
172
|
+
}
|
|
138
173
|
export function synthesizeWithClaude(prompt, options = {}) {
|
|
139
174
|
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
140
175
|
return new Promise((resolve) => {
|
|
141
|
-
const args = ["-p", "--output-format", "text", "--permission-mode", "acceptEdits"];
|
|
176
|
+
const args = ["-p", "--output-format", "text", "--permission-mode", "acceptEdits", "--no-session-persistence"];
|
|
142
177
|
if (options.model) {
|
|
143
178
|
args.push("--model", options.model);
|
|
144
179
|
}
|