@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.
@@ -110,7 +110,25 @@ export function classifyDominantRole(memberSessions) {
110
110
  return "tool-heavy";
111
111
  return "user-driven";
112
112
  }
113
- export function buildTopicNodes(clusterMembers, sessions, tfidf, toolIdf) {
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: top 2-3 keywords + project
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 label = `${labelKeywords} ${projectSuffix}`;
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, } from "./knowledge-graph/index.js";
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" });
@@ -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
- const parts = [topicInfo, prompts, toolSig, toolPatterns, graphPosition, connectedTopicsSection, reference, instruction].filter(Boolean);
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
  }
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@chigichan24/crune",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
5
8
  "bin": {
6
9
  "crune": "./bin/crune.js"
7
10
  },