@pencil-agent/nano-pencil 1.11.7 → 1.11.8

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.
@@ -115,10 +115,10 @@ export class ExtensionRunner {
115
115
  this.sessionManager = sessionManager;
116
116
  this.modelRegistry = modelRegistry;
117
117
  }
118
- async withTimeout(promise, timeoutMs) {
118
+ async withTimeout(valueOrPromise, timeoutMs) {
119
119
  return new Promise((resolve) => {
120
120
  const timer = setTimeout(() => resolve(this.beforeAgentStartTimeoutSentinel), timeoutMs);
121
- promise
121
+ Promise.resolve(valueOrPromise)
122
122
  .then((value) => {
123
123
  clearTimeout(timer);
124
124
  resolve(value);
@@ -125,6 +125,12 @@ export class FooterDataProvider {
125
125
  cb();
126
126
  }
127
127
  });
128
+ this.gitWatcher.on("error", () => {
129
+ if (this.gitWatcher) {
130
+ this.gitWatcher.close();
131
+ this.gitWatcher = null;
132
+ }
133
+ });
128
134
  }
129
135
  catch {
130
136
  // Silently fail if we can't watch
@@ -175,7 +175,49 @@ function extractLoopDecision(text) {
175
175
  };
176
176
  }
177
177
  catch {
178
- return undefined;
178
+ const lines = payload
179
+ .split(/\r?\n/)
180
+ .map((line) => line.trim())
181
+ .filter(Boolean);
182
+ if (lines.length === 0) {
183
+ return undefined;
184
+ }
185
+ const getValue = (...prefixes) => {
186
+ const line = lines.find((entry) => prefixes.some((prefix) => entry.toLowerCase().startsWith(prefix.toLowerCase())));
187
+ if (!line) {
188
+ return undefined;
189
+ }
190
+ const separatorIndex = line.indexOf(":");
191
+ if (separatorIndex === -1) {
192
+ return undefined;
193
+ }
194
+ return line.slice(separatorIndex + 1).trim();
195
+ };
196
+ const rawStatus = getValue("status:", "状态:");
197
+ const normalizedStatus = rawStatus === "complete" || rawStatus === "完成"
198
+ ? "complete"
199
+ : rawStatus === "continue" || rawStatus === "继续" || rawStatus === "in_progress"
200
+ ? "continue"
201
+ : rawStatus === "blocked" || rawStatus === "阻塞"
202
+ ? "blocked"
203
+ : undefined;
204
+ if (!normalizedStatus) {
205
+ return undefined;
206
+ }
207
+ const summary = getValue("summary:", "摘要:", "已完成工作:", "completed work:") ??
208
+ lines.filter((line) => !line.startsWith("-")).slice(1).join(" ").trim();
209
+ if (!summary) {
210
+ return undefined;
211
+ }
212
+ const nextStep = getValue("next step:", "下一步:");
213
+ if (normalizedStatus === "continue" && !nextStep) {
214
+ return undefined;
215
+ }
216
+ return {
217
+ status: normalizedStatus,
218
+ summary,
219
+ nextStep,
220
+ };
179
221
  }
180
222
  }
181
223
  function describeDecision(decision) {
@@ -94,8 +94,13 @@ Usage:
94
94
  writeFileSync(outputPath, html, "utf-8");
95
95
  }
96
96
  else {
97
- const report = await engine.generateFullInsights();
98
- const html = renderFullInsightsHtml(report, engine.cfg.locale);
97
+ const enhanced = await engine.generateEnhancedInsights();
98
+ const html = renderFullInsightsHtml({
99
+ ...enhanced.report,
100
+ persona: enhanced.persona,
101
+ humanInsights: enhanced.humanInsights,
102
+ rootCauses: enhanced.rootCauses,
103
+ }, engine.cfg.locale);
99
104
  writeFileSync(outputPath, html, "utf-8");
100
105
  }
101
106
  console.log(`Insights report written to: ${outputPath}`);
@@ -7,7 +7,7 @@
7
7
  * No dependency on any specific AI framework — LLM is pluggable.
8
8
  */
9
9
  import { type NanomemConfig } from "./config.js";
10
- import type { Episode, ExtractedItem, FullInsightsReport, HumanInsight, InsightsReport, LlmFn, MemoryEntry, MemoryScope, Meta, RootCauseInsight, WorkEntry } from "./types.js";
10
+ import type { DeveloperPersona, Episode, ExtractedItem, FullInsightsReport, HumanInsight, InsightsReport, LlmFn, MemoryEntry, MemoryScope, Meta, RootCauseInsight, WorkEntry } from "./types.js";
11
11
  export declare class NanoMemEngine {
12
12
  readonly cfg: NanomemConfig;
13
13
  private llmFn?;
@@ -74,6 +74,7 @@ export declare class NanoMemEngine {
74
74
  */
75
75
  generateEnhancedInsights(): Promise<{
76
76
  report: FullInsightsReport;
77
+ persona?: DeveloperPersona;
77
78
  humanInsights: HumanInsight[];
78
79
  rootCauses: RootCauseInsight[];
79
80
  }>;
@@ -424,6 +424,7 @@ export class NanoMemEngine {
424
424
  ]);
425
425
  return {
426
426
  report: baseReport,
427
+ persona: humanData.persona,
427
428
  humanInsights: humanData.humanInsights,
428
429
  rootCauses: humanData.rootCauses,
429
430
  };
@@ -255,8 +255,13 @@ export default function nanomemExtension(pi) {
255
255
  }
256
256
  const outputPath = args?.trim() || "./nanomem-insights.html";
257
257
  ctx.ui.notify("NanoMem: Generating full insights report...", "info");
258
- const report = await engine.generateFullInsights();
259
- const html = renderFullInsightsHtml(report, engine.cfg.locale);
258
+ const enhanced = await engine.generateEnhancedInsights();
259
+ const html = renderFullInsightsHtml({
260
+ ...enhanced.report,
261
+ persona: enhanced.persona,
262
+ humanInsights: enhanced.humanInsights,
263
+ rootCauses: enhanced.rootCauses,
264
+ }, engine.cfg.locale);
260
265
  writeFileSync(outputPath, html, "utf-8");
261
266
  ctx.ui.notify(`NanoMem: Insights report written to ${outputPath}`, "info");
262
267
  },
@@ -39,12 +39,28 @@ function renderBarRows(chart) {
39
39
  export function renderFullInsightsHtml(report, locale) {
40
40
  const p = PROMPTS[locale] ?? PROMPTS.en;
41
41
  const lang = locale === "zh" ? "zh-CN" : "en";
42
+ const enhancedReport = report;
42
43
  const sections = [];
43
44
  // TOC links (only for sections we might render)
44
45
  const tocLinks = [
45
46
  '<a href="#section-glance"><i class="ri-dashboard-line"></i> ' + escapeHtml(p.fullInsightsAtAGlance) + "</a>",
46
47
  '<a href="#section-work"><i class="ri-briefcase-4-line"></i> ' + escapeHtml(p.fullInsightsWorkOn) + "</a>",
47
48
  ];
49
+ if (enhancedReport.persona) {
50
+ tocLinks.push('<a href="#section-persona"><i class="ri-user-star-line"></i> ' +
51
+ escapeHtml(p.humanInsightsSectionPersona) +
52
+ "</a>");
53
+ }
54
+ if (enhancedReport.humanInsights?.length) {
55
+ tocLinks.push('<a href="#section-human-insights"><i class="ri-robot-2-line"></i> ' +
56
+ escapeHtml(p.humanInsightsSectionInsights) +
57
+ "</a>");
58
+ }
59
+ if (enhancedReport.rootCauses?.length) {
60
+ tocLinks.push('<a href="#section-root-causes"><i class="ri-stethoscope-line"></i> ' +
61
+ escapeHtml(p.humanInsightsSectionRootCauses) +
62
+ "</a>");
63
+ }
48
64
  if (report.charts.length)
49
65
  tocLinks.push('<a href="#section-charts"><i class="ri-bar-chart-box-line"></i> Charts</a>');
50
66
  if (report.wins.length)
@@ -70,15 +86,78 @@ export function renderFullInsightsHtml(report, locale) {
70
86
  ${statItems.map((s) => `<div class="stat"><i class="${s.icon} stat-icon"></i><div class="stat-value">${s.value}</div><div class="stat-label">${escapeHtml(s.label)}</div></div>`).join("\n")}
71
87
  </section>`;
72
88
  // At a Glance
73
- const glanceHtml = `<section id="section-glance" class="at-a-glance">
74
- <h2 class="glance-title"><i class="ri-dashboard-line"></i> ${escapeHtml(p.fullInsightsAtAGlance)}</h2>
75
- <div class="glance-grid">
89
+ const glanceHtml = `<section id="section-glance" class="at-a-glance">
90
+ <h2 class="glance-title"><i class="ri-dashboard-line"></i> ${escapeHtml(p.fullInsightsAtAGlance)}</h2>
91
+ <div class="glance-grid">
76
92
  <article class="glance-card"><h3><i class="ri-checkbox-circle-line"></i> What's working</h3><p>${escapeHtml(report.atAGlance.working)}</p></article>
77
93
  <article class="glance-card warn"><h3><i class="ri-error-warning-line"></i> What's hindering</h3><p>${escapeHtml(report.atAGlance.hindering)}</p></article>
78
94
  <article class="glance-card"><h3><i class="ri-lightbulb-line"></i> Quick wins</h3><p>${escapeHtml(report.atAGlance.quickWins)}</p></article>
79
95
  <article class="glance-card"><h3><i class="ri-rocket-line"></i> Ambitious</h3><p>${escapeHtml(report.atAGlance.ambitious)}</p></article>
80
- </div>
96
+ </div>
97
+ </section>`;
98
+ let personaHtml = "";
99
+ if (enhancedReport.persona) {
100
+ const persona = enhancedReport.persona;
101
+ personaHtml = `<section id="section-persona" class="section">
102
+ <h2><i class="ri-user-star-line"></i> ${escapeHtml(p.humanInsightsSectionPersona)}</h2>
103
+ <div class="persona-grid">
104
+ <article class="persona-card persona-lead">
105
+ <div class="persona-kicker">${escapeHtml(persona.summary)}</div>
106
+ <div class="persona-text">${escapeHtml(persona.whatTheyDo)}</div>
107
+ <div class="persona-text">${escapeHtml(persona.workStyle)}</div>
108
+ <div class="persona-meta">${escapeHtml(persona.experienceLevel)}</div>
109
+ </article>
110
+ <article class="persona-card">
111
+ <div class="persona-card-title">Strengths</div>
112
+ <ul class="persona-list">${persona.superpowers.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>
113
+ </article>
114
+ <article class="persona-card">
115
+ <div class="persona-card-title">Watchouts</div>
116
+ <ul class="persona-list">${persona.painPoints.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>
117
+ </article>
118
+ </div>
119
+ </section>`;
120
+ }
121
+ let humanInsightsHtml = "";
122
+ if (enhancedReport.humanInsights?.length) {
123
+ humanInsightsHtml = `<section id="section-human-insights" class="section">
124
+ <h2><i class="ri-robot-2-line"></i> ${escapeHtml(p.humanInsightsSectionInsights)}</h2>
125
+ <div class="insight-review-list">
126
+ ${enhancedReport.humanInsights
127
+ .map((insight) => ` <article class="insight-review-card priority-${escapeHtml(insight.utility)}">
128
+ <div class="insight-review-header">
129
+ <div class="insight-review-icon">${escapeHtml(insight.icon)}</div>
130
+ <div>
131
+ <div class="insight-review-title">${escapeHtml(insight.title)}</div>
132
+ <div class="insight-review-priority">${escapeHtml(insight.utility.toUpperCase())}</div>
133
+ </div>
134
+ </div>
135
+ <div class="insight-review-content">${escapeHtml(insight.content)}</div>
136
+ ${insight.tags.length ? `<div class="insight-tags">${insight.tags.map((tag) => `<span>${escapeHtml(tag)}</span>`).join("")}</div>` : ""}
137
+ </article>`)
138
+ .join("\n")}
139
+ </div>
140
+ </section>`;
141
+ }
142
+ let rootCausesHtml = "";
143
+ if (enhancedReport.rootCauses?.length) {
144
+ rootCausesHtml = `<section id="section-root-causes" class="section">
145
+ <h2><i class="ri-stethoscope-line"></i> ${escapeHtml(p.humanInsightsSectionRootCauses)}</h2>
146
+ <div class="root-cause-list">
147
+ ${enhancedReport.rootCauses
148
+ .map((item) => ` <article class="root-cause-card">
149
+ <div class="root-cause-label">Recurring symptom</div>
150
+ <div class="root-cause-title">${escapeHtml(item.symptom)}</div>
151
+ <div class="root-cause-label">Likely cause</div>
152
+ <div class="root-cause-body">${escapeHtml(item.rootCause)}</div>
153
+ ${item.evidence.length ? `<div class="root-cause-label">Evidence</div><ul class="root-cause-evidence">${item.evidence.map((fact) => `<li>${escapeHtml(fact)}</li>`).join("")}</ul>` : ""}
154
+ <div class="root-cause-label">Recommended fix</div>
155
+ <div class="root-cause-body">${escapeHtml(item.suggestion)}</div>
156
+ </article>`)
157
+ .join("\n")}
158
+ </div>
81
159
  </section>`;
160
+ }
82
161
  // What You Work On
83
162
  let workHtml = "";
84
163
  if (report.projectAreas.length) {
@@ -201,10 +280,37 @@ h2 .ri{vertical-align:middle;margin-right:6px}
201
280
  .subtitle{color:#64748b;font-size:15px;margin-bottom:24px}
202
281
  .nav-toc{display:flex;flex-wrap:wrap;gap:8px;margin:24px 0 32px;padding:16px;background:#fff;border-radius:8px;border:1px solid #e2e8f0}
203
282
  .nav-toc a{font-size:12px;color:#64748b;text-decoration:none;padding:6px 12px;border-radius:6px;background:#f1f5f9;transition:all .15s}
204
- .nav-toc a:hover{background:#e2e8f0;color:#334155}
205
- .nav-toc .ri{margin-right:4px;vertical-align:middle}
206
- .stats-row{display:flex;gap:24px;margin-bottom:32px;padding:20px 0;border-top:1px solid #e2e8f0;border-bottom:1px solid #e2e8f0;flex-wrap:wrap}
207
- .stat{text-align:center}
283
+ .nav-toc a:hover{background:#e2e8f0;color:#334155}
284
+ .nav-toc .ri{margin-right:4px;vertical-align:middle}
285
+ .stats-row{display:flex;gap:24px;margin-bottom:32px;padding:20px 0;border-top:1px solid #e2e8f0;border-bottom:1px solid #e2e8f0;flex-wrap:wrap}
286
+ .persona-grid{display:grid;grid-template-columns:2fr 1fr 1fr;gap:16px}
287
+ .persona-card{background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:16px}
288
+ .persona-lead{background:linear-gradient(135deg,#fff7ed 0%,#ffedd5 100%);border-color:#fdba74}
289
+ .persona-kicker{font-size:18px;font-weight:700;color:#9a3412;margin-bottom:10px}
290
+ .persona-text{font-size:14px;color:#334155;line-height:1.6;margin-bottom:8px}
291
+ .persona-meta{font-size:12px;color:#7c2d12;text-transform:uppercase;letter-spacing:.04em}
292
+ .persona-card-title{font-size:12px;font-weight:700;color:#64748b;text-transform:uppercase;margin-bottom:10px}
293
+ .persona-list{margin:0;padding-left:18px}
294
+ .persona-list li{margin-bottom:8px;font-size:14px;color:#334155}
295
+ .insight-review-list,.root-cause-list{display:flex;flex-direction:column;gap:16px}
296
+ .insight-review-card{border-radius:10px;padding:18px;border:1px solid #dbeafe;background:#f8fbff}
297
+ .insight-review-card.priority-high{border-color:#93c5fd;background:#eff6ff}
298
+ .insight-review-card.priority-medium{border-color:#cbd5e1;background:#f8fafc}
299
+ .insight-review-card.priority-low{border-color:#d1fae5;background:#f0fdf4}
300
+ .insight-review-header{display:flex;align-items:center;gap:12px;margin-bottom:12px}
301
+ .insight-review-icon{font-size:24px;line-height:1}
302
+ .insight-review-title{font-size:16px;font-weight:700;color:#0f172a}
303
+ .insight-review-priority{font-size:11px;color:#475569;text-transform:uppercase;letter-spacing:.08em}
304
+ .insight-review-content{font-size:14px;color:#334155;line-height:1.7}
305
+ .insight-tags{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
306
+ .insight-tags span{font-size:11px;color:#475569;background:#e2e8f0;border-radius:999px;padding:4px 8px}
307
+ .root-cause-card{border-radius:10px;padding:18px;border:1px solid #fecaca;background:#fff7f7}
308
+ .root-cause-label{font-size:11px;font-weight:700;color:#991b1b;text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px}
309
+ .root-cause-title{font-size:16px;font-weight:700;color:#7f1d1d;margin-bottom:10px}
310
+ .root-cause-body{font-size:14px;color:#334155;line-height:1.7;margin-bottom:12px}
311
+ .root-cause-evidence{margin:0 0 12px 18px}
312
+ .root-cause-evidence li{margin-bottom:6px;font-size:13px;color:#475569}
313
+ .stat{text-align:center}
208
314
  .stat-icon{font-size:20px;color:#64748b;display:block;margin-bottom:4px}
209
315
  .stat-value{font-size:24px;font-weight:700;color:#0f172a}
210
316
  .stat-label{font-size:11px;color:#64748b;text-transform:uppercase}
@@ -254,8 +360,9 @@ h2 .ri{vertical-align:middle;margin-right:6px}
254
360
  .copy-btn:hover{background:#cbd5e1}
255
361
  .pattern-list{margin:0;padding-left:20px}
256
362
  .pattern-list li{margin-bottom:6px;font-size:14px;color:#334155}
257
- footer{margin-top:32px;text-align:center;font-size:12px;color:#94a3b8}
258
- @media (max-width:640px){.charts-row{grid-template-columns:1fr}.stats-row{justify-content:center}}
363
+ footer{margin-top:32px;text-align:center;font-size:12px;color:#94a3b8}
364
+ @media (max-width:640px){.charts-row{grid-template-columns:1fr}.stats-row{justify-content:center}}
365
+ @media (max-width:900px){.persona-grid{grid-template-columns:1fr}}
259
366
  `;
260
367
  const copyScript = `
261
368
  document.querySelectorAll('.copy-btn').forEach(function(btn){
@@ -291,11 +398,14 @@ document.querySelectorAll('.copy-btn').forEach(function(btn){
291
398
  ${tocLinks.map((link) => " " + link).join("\n")}
292
399
  </nav>
293
400
 
294
- ${statsHtml}
295
- ${glanceHtml}
296
- ${workHtml}
297
- ${chartsHtml}
298
- ${winsHtml}
401
+ ${statsHtml}
402
+ ${glanceHtml}
403
+ ${workHtml}
404
+ ${personaHtml}
405
+ ${humanInsightsHtml}
406
+ ${rootCausesHtml}
407
+ ${chartsHtml}
408
+ ${winsHtml}
299
409
  ${frictionsHtml}
300
410
  ${recHtml}
301
411
  ${featuresHtml}
@@ -1,19 +1,27 @@
1
1
  /**
2
2
  * [INPUT]: ExportAllResult, LlmFn, locale
3
- * [OUTPUT]: 开发者画像 + 人话版洞察 + 根因分析
4
- * [POS]: LLM-powered human-readable insights generation
5
- */
6
- import type { DeveloperPersona, EnhancedInsightsReport, ExportAllResult, HumanInsight, LlmFn, RootCauseInsight } from "./types.js";
7
- /**
8
- * 生成大白话版洞察报告
3
+ * [OUTPUT]: developer persona, evidence-backed insights, and root-cause analysis
4
+ * [POS]: LLM-powered usage review generation
9
5
  */
6
+ import type { DeveloperPersona, EnhancedInsightsReport, HumanInsight, LlmFn, MemoryEntry, Episode, RootCauseInsight, WorkEntry } from "./types.js";
7
+ interface ExportAllResult {
8
+ knowledge: MemoryEntry[];
9
+ lessons: MemoryEntry[];
10
+ preferences: MemoryEntry[];
11
+ facets: MemoryEntry[];
12
+ work: WorkEntry[];
13
+ episodes: Episode[];
14
+ meta: {
15
+ totalSessions: number;
16
+ lastConsolidation?: string;
17
+ version: number;
18
+ };
19
+ }
10
20
  export declare function generateHumanInsights(all: ExportAllResult, llmFn: LlmFn | undefined, locale: string): Promise<{
11
21
  persona?: DeveloperPersona;
12
22
  humanInsights: HumanInsight[];
13
23
  rootCauses: RootCauseInsight[];
14
24
  }>;
15
- /**
16
- * 将人类可读洞察合并到 FullInsightsReport 生成流程中
17
- */
18
25
  export declare function buildEnhancedInsightsReport(all: ExportAllResult, llmFn: LlmFn | undefined, locale: string): Promise<EnhancedInsightsReport>;
26
+ export {};
19
27
  //# sourceMappingURL=human-insights.d.ts.map
@@ -1,118 +1,203 @@
1
1
  /**
2
2
  * [INPUT]: ExportAllResult, LlmFn, locale
3
- * [OUTPUT]: 开发者画像 + 人话版洞察 + 根因分析
4
- * [POS]: LLM-powered human-readable insights generation
3
+ * [OUTPUT]: developer persona, evidence-backed insights, and root-cause analysis
4
+ * [POS]: LLM-powered usage review generation
5
5
  */
6
- import { PROMPTS } from "./i18n.js";
6
+ const HUMAN_INSIGHTS_SYSTEM_PROMPT = `You are an elite AI product analyst and developer workflow coach.
7
+
8
+ You are reviewing one specific user's real usage history over time.
9
+ Write like a warm, observant expert who deeply understands how experienced AI users actually work.
10
+
11
+ Goals:
12
+ - Sound human, perceptive, and respectful rather than robotic or generic
13
+ - Give clear corrections when the user's habits are inefficient
14
+ - Back every major conclusion with concrete evidence from the supplied data
15
+ - Prefer precise language, plain English, and practical recommendations
16
+ - Explain what the user is doing well, where they are losing time, and what they should change next
17
+
18
+ Output requirements:
19
+ - Output ONLY valid JSON
20
+ - Do not use markdown or code fences
21
+ - Use the supplied data only
22
+ - Be specific, not motivational fluff
23
+ - Each insight should feel like part of a thoughtful performance review
24
+ - Recommendations should be direct, concrete, and easy to act on
25
+ - Evidence should reference counts, repeated behaviors, or recurring issues when possible
26
+ - If locale is "zh", write the JSON string values in Simplified Chinese; otherwise write in English
27
+
28
+ Return JSON matching this schema:
29
+ {
30
+ "persona": {
31
+ "whatTheyDo": "1-2 sentences",
32
+ "experienceLevel": "1 sentence",
33
+ "superpowers": ["...", "..."],
34
+ "painPoints": ["...", "..."],
35
+ "workStyle": "1-2 sentences",
36
+ "summary": "1 sentence"
37
+ },
38
+ "insights": [
39
+ {
40
+ "title": "short title",
41
+ "content": "3-5 sentences combining observation, evidence, correction, and advice",
42
+ "icon": "emoji",
43
+ "utility": "high|medium|low",
44
+ "tags": ["tag1", "tag2"]
45
+ }
46
+ ],
47
+ "rootCauses": [
48
+ {
49
+ "symptom": "what keeps happening",
50
+ "rootCause": "why it likely happens",
51
+ "evidence": ["fact 1", "fact 2"],
52
+ "suggestion": "what to change next"
53
+ }
54
+ ]
55
+ }`.trim();
56
+ function summarizeCounts(rows, formatter) {
57
+ return rows.map(([label, value]) => formatter(label, value));
58
+ }
7
59
  function buildHumanInsightsData(all) {
8
- // 工具使用
9
- const tools = all.episodes.length > 0
10
- ? Object.entries(all.episodes.reduce((acc, ep) => {
11
- for (const [tool, count] of Object.entries(ep.toolsUsed || {})) {
12
- acc[tool] = (acc[tool] || 0) + count;
13
- }
14
- return acc;
15
- }, {}))
16
- .sort((a, b) => b[1] - a[1])
17
- .slice(0, 10)
18
- .map(([t, c]) => `${t} (${c}次)`)
19
- .join(", ")
20
- : "暂无数据";
21
- // 语言统计
22
- const langCounts = {};
23
- for (const ep of all.episodes) {
24
- for (const f of ep.filesModified || []) {
25
- const ext = f.includes(".") ? f.split(".").pop()?.toLowerCase() ?? "other" : "other";
26
- langCounts[ext] = (langCounts[ext] || 0) + 1;
60
+ const totalToolUses = all.episodes.reduce((total, episode) => total +
61
+ Object.values(episode.toolsUsed ?? {}).reduce((sum, count) => sum + count, 0), 0);
62
+ const toolCounts = all.episodes.reduce((acc, episode) => {
63
+ for (const [tool, count] of Object.entries(episode.toolsUsed ?? {})) {
64
+ acc[tool] = (acc[tool] ?? 0) + count;
27
65
  }
28
- }
29
- const languages = Object.keys(langCounts).length > 0
30
- ? Object.entries(langCounts)
31
- .sort((a, b) => b[1] - a[1])
32
- .slice(0, 8)
33
- .map(([l, c]) => `${l} (${c}个文件)`)
34
- .join(", ")
35
- : "暂无数据";
36
- // 已解决的问题 (wins)
37
- const wins = all.facets
38
- .filter((f) => f.type === "struggle" && f.facetData?.kind === "struggle" && f.facetData.solution)
39
- .slice(0, 8)
40
- .map((f) => f.summary || f.facetData?.kind === "struggle" && f.facetData.problem)
41
- .filter(Boolean)
42
- .join("; ") || "暂无记录";
43
- // 未解决的问题 (struggles)
44
- const struggles = all.facets
45
- .filter((f) => f.type === "struggle" && (!f.facetData || (f.facetData.kind === "struggle" && !f.facetData.solution)))
46
- .slice(0, 8)
47
- .map((f) => f.facetData?.kind === "struggle" ? f.facetData.problem : (f.summary || f.detail || ""))
48
- .filter(Boolean)
49
- .join("; ") || "暂无记录";
50
- // 经验教训
51
- const lessons = all.lessons
52
- .slice(0, 10)
53
- .map((l) => l.summary || l.detail || l.content || "")
54
- .filter(Boolean)
55
- .join("; ") || "暂无记录";
56
- // 错误统计
57
- const errorCounts = {};
58
- for (const ep of all.episodes) {
59
- for (const err of ep.errors || []) {
60
- const key = err.slice(0, 50).trim();
61
- errorCounts[key] = (errorCounts[key] || 0) + 1;
66
+ return acc;
67
+ }, {});
68
+ const languageCounts = all.episodes.reduce((acc, episode) => {
69
+ for (const file of episode.filesModified ?? []) {
70
+ const ext = file.includes(".") ? file.split(".").pop()?.toLowerCase() ?? "other" : "other";
71
+ acc[ext] = (acc[ext] ?? 0) + 1;
62
72
  }
63
- }
64
- const errors = Object.keys(errorCounts).length > 0
65
- ? Object.entries(errorCounts)
73
+ return acc;
74
+ }, {});
75
+ const errorCounts = all.episodes.reduce((acc, episode) => {
76
+ for (const error of episode.errors ?? []) {
77
+ const key = error.replace(/\s+/g, " ").trim().slice(0, 120);
78
+ if (!key)
79
+ continue;
80
+ acc[key] = (acc[key] ?? 0) + 1;
81
+ }
82
+ return acc;
83
+ }, {});
84
+ const resolvedStruggles = all.facets.filter((entry) => entry.type === "struggle" && entry.facetData?.kind === "struggle" && !!entry.facetData.solution);
85
+ const unresolvedStruggles = all.facets.filter((entry) => entry.type === "struggle" && entry.facetData?.kind === "struggle" && !entry.facetData.solution);
86
+ const patternEntries = all.facets.filter((entry) => entry.type === "pattern" && entry.facetData?.kind === "pattern");
87
+ const topTools = Object.entries(toolCounts)
88
+ .sort((a, b) => b[1] - a[1])
89
+ .slice(0, 8);
90
+ const topLanguages = Object.entries(languageCounts)
91
+ .sort((a, b) => b[1] - a[1])
92
+ .slice(0, 8);
93
+ const topErrors = Object.entries(errorCounts)
94
+ .sort((a, b) => b[1] - a[1])
95
+ .slice(0, 8);
96
+ const topPatterns = patternEntries
97
+ .slice()
98
+ .sort((a, b) => (b.accessCount + 1) * b.importance - (a.accessCount + 1) * a.importance)
99
+ .slice(0, 6)
100
+ .map((entry) => ({
101
+ trigger: entry.facetData?.kind === "pattern" ? entry.facetData.trigger : "",
102
+ behavior: entry.facetData?.kind === "pattern" ? entry.facetData.behavior : "",
103
+ importance: entry.importance,
104
+ accessCount: entry.accessCount,
105
+ }));
106
+ const notableWins = resolvedStruggles.slice(0, 6).map((entry) => ({
107
+ problem: entry.facetData?.kind === "struggle" ? entry.facetData.problem : entry.summary || "",
108
+ solution: entry.facetData?.kind === "struggle" ? entry.facetData.solution : "",
109
+ importance: entry.importance,
110
+ }));
111
+ const notableFrictions = unresolvedStruggles.slice(0, 6).map((entry) => ({
112
+ problem: entry.facetData?.kind === "struggle" ? entry.facetData.problem : entry.summary || "",
113
+ attempts: entry.facetData?.kind === "struggle" ? entry.facetData.attempts : [],
114
+ importance: entry.importance,
115
+ }));
116
+ const topLessons = all.lessons
117
+ .slice()
118
+ .sort((a, b) => (b.accessCount + 1) * b.importance - (a.accessCount + 1) * a.importance)
119
+ .slice(0, 8)
120
+ .map((entry) => entry.summary || entry.detail || entry.content || "")
121
+ .filter(Boolean);
122
+ const projectCounts = all.episodes.reduce((acc, episode) => {
123
+ const key = episode.project || "default";
124
+ acc[key] = (acc[key] ?? 0) + 1;
125
+ return acc;
126
+ }, {});
127
+ return {
128
+ overview: {
129
+ totalSessions: all.meta.totalSessions,
130
+ episodes: all.episodes.length,
131
+ workEntries: all.work.length,
132
+ knowledgeEntries: all.knowledge.length,
133
+ lessonEntries: all.lessons.length,
134
+ preferenceEntries: all.preferences.length,
135
+ facetEntries: all.facets.length,
136
+ totalToolUses,
137
+ resolvedStruggleCount: resolvedStruggles.length,
138
+ unresolvedStruggleCount: unresolvedStruggles.length,
139
+ },
140
+ topTools: topTools.map(([tool, count]) => ({
141
+ tool,
142
+ count,
143
+ share: totalToolUses > 0 ? Number(((count / totalToolUses) * 100).toFixed(1)) : 0,
144
+ })),
145
+ topLanguages: topLanguages.map(([language, fileCount]) => ({ language, fileCount })),
146
+ topErrors: topErrors.map(([error, count]) => ({ error, count })),
147
+ topPatterns,
148
+ notableWins,
149
+ notableFrictions,
150
+ topLessons,
151
+ projectDistribution: Object.entries(projectCounts)
66
152
  .sort((a, b) => b[1] - a[1])
67
- .slice(0, 10)
68
- .map(([e, c]) => `${e} (${c})`)
69
- .join("; ")
70
- : "暂无错误记录";
71
- return { tools, languages, wins, struggles, lessons, errors };
153
+ .slice(0, 6)
154
+ .map(([project, sessions]) => ({ project, sessions })),
155
+ evidenceDigest: {
156
+ tools: summarizeCounts(topTools, (tool, count) => `${tool}: ${count} uses`),
157
+ languages: summarizeCounts(topLanguages, (language, count) => `${language}: ${count} files`),
158
+ errors: summarizeCounts(topErrors, (error, count) => `${error}: ${count} times`),
159
+ },
160
+ };
72
161
  }
73
162
  function parseHumanInsightsResponse(raw) {
74
163
  try {
75
164
  const cleaned = raw.replace(/```json?\n?/g, "").replace(/```/g, "").trim();
76
165
  const parsed = JSON.parse(cleaned);
77
- // Validate structure
78
- if (typeof parsed !== "object" || parsed === null)
166
+ if (typeof parsed !== "object" || parsed === null) {
79
167
  return null;
168
+ }
80
169
  const persona = parsed.persona
81
170
  ? {
82
171
  whatTheyDo: String(parsed.persona.whatTheyDo || ""),
83
172
  experienceLevel: String(parsed.persona.experienceLevel || ""),
84
- superpowers: Array.isArray(parsed.persona.superpowers)
85
- ? parsed.persona.superpowers.map(String)
86
- : [],
87
- painPoints: Array.isArray(parsed.persona.painPoints)
88
- ? parsed.persona.painPoints.map(String)
89
- : [],
173
+ superpowers: Array.isArray(parsed.persona.superpowers) ? parsed.persona.superpowers.map(String) : [],
174
+ painPoints: Array.isArray(parsed.persona.painPoints) ? parsed.persona.painPoints.map(String) : [],
90
175
  workStyle: String(parsed.persona.workStyle || ""),
91
176
  summary: String(parsed.persona.summary || ""),
92
177
  }
93
178
  : undefined;
94
179
  const insights = Array.isArray(parsed.insights)
95
- ? parsed.insights.map((i) => ({
96
- title: String(i.title || ""),
97
- content: String(i.content || ""),
98
- icon: String(i.icon || "💡"),
99
- utility: ["high", "medium", "low"].includes(String(i.utility))
100
- ? String(i.utility)
180
+ ? parsed.insights
181
+ .map((item) => ({
182
+ title: String(item.title || "").trim(),
183
+ content: String(item.content || "").trim(),
184
+ icon: String(item.icon || "Insight").trim(),
185
+ utility: ["high", "medium", "low"].includes(String(item.utility))
186
+ ? String(item.utility)
101
187
  : "medium",
102
- tags: Array.isArray(i.tags)
103
- ? i.tags.map(String)
104
- : [],
188
+ tags: Array.isArray(item.tags) ? item.tags.map(String) : [],
105
189
  }))
190
+ .filter((item) => item.title && item.content)
106
191
  : [];
107
192
  const rootCauses = Array.isArray(parsed.rootCauses)
108
- ? parsed.rootCauses.map((r) => ({
109
- symptom: String(r.symptom || ""),
110
- rootCause: String(r.rootCause || ""),
111
- evidence: Array.isArray(r.evidence)
112
- ? r.evidence.map(String)
113
- : [],
114
- suggestion: String(r.suggestion || ""),
193
+ ? parsed.rootCauses
194
+ .map((item) => ({
195
+ symptom: String(item.symptom || "").trim(),
196
+ rootCause: String(item.rootCause || "").trim(),
197
+ evidence: Array.isArray(item.evidence) ? item.evidence.map(String) : [],
198
+ suggestion: String(item.suggestion || "").trim(),
115
199
  }))
200
+ .filter((item) => item.symptom && item.rootCause)
116
201
  : [];
117
202
  return { persona, insights, rootCauses };
118
203
  }
@@ -120,26 +205,14 @@ function parseHumanInsightsResponse(raw) {
120
205
  return null;
121
206
  }
122
207
  }
123
- /**
124
- * 生成大白话版洞察报告
125
- */
126
208
  export async function generateHumanInsights(all, llmFn, locale) {
127
- // 如果没有 LLM,返回空结果
128
209
  if (!llmFn) {
129
210
  return { humanInsights: [], rootCauses: [] };
130
211
  }
131
- const p = PROMPTS[locale] || PROMPTS.en;
132
212
  const data = buildHumanInsightsData(all);
133
- // 构建用户 prompt,替换模板变量
134
- let userPrompt = p.humanInsightsUserTemplate;
135
- userPrompt = userPrompt.replace("{{tools}}", data.tools);
136
- userPrompt = userPrompt.replace("{{languages}}", data.languages);
137
- userPrompt = userPrompt.replace("{{wins}}", data.wins);
138
- userPrompt = userPrompt.replace("{{struggles}}", data.struggles);
139
- userPrompt = userPrompt.replace("{{lessons}}", data.lessons);
140
- userPrompt = userPrompt.replace("{{errors}}", data.errors);
213
+ const userPrompt = JSON.stringify({ locale, reviewData: data });
141
214
  try {
142
- const raw = await llmFn(p.humanInsightsSystemPrompt, userPrompt);
215
+ const raw = await llmFn(HUMAN_INSIGHTS_SYSTEM_PROMPT, userPrompt);
143
216
  const parsed = parseHumanInsightsResponse(raw);
144
217
  if (parsed) {
145
218
  return {
@@ -150,18 +223,12 @@ export async function generateHumanInsights(all, llmFn, locale) {
150
223
  }
151
224
  }
152
225
  catch {
153
- // Fallback to empty
226
+ // Fall back to empty enhanced insights when the LLM path is unavailable.
154
227
  }
155
228
  return { humanInsights: [], rootCauses: [] };
156
229
  }
157
- /**
158
- * 将人类可读洞察合并到 FullInsightsReport 生成流程中
159
- */
160
230
  export async function buildEnhancedInsightsReport(all, llmFn, locale) {
161
- // 这个函数会在 engine.ts 中被调用来生成完整报告
162
- // 目前 placeholder - 实际逻辑在对应的调用处
163
231
  const humanData = await generateHumanInsights(all, llmFn, locale);
164
- // 返回一个基础结构,实际的完整报告会在调用处构建
165
232
  return {
166
233
  stats: {
167
234
  knowledge: all.knowledge.length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pencil-agent/nano-pencil",
3
- "version": "1.11.7",
3
+ "version": "1.11.8",
4
4
  "description": "CLI writing agent with read, bash, edit, write tools and session management. Based on pi; supports DashScope Coding Plan. Soul enabled by default for AI personality evolution.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -65,11 +65,11 @@
65
65
  "hosted-git-info": "^9.0.2",
66
66
  "ignore": "^7.0.5",
67
67
  "marked": "^15.0.12",
68
- "minimatch": "^10.1.1",
69
- "proper-lockfile": "^4.1.2",
70
- "yaml": "^2.8.2",
71
- "zod": "^4.3.6"
72
- },
68
+ "minimatch": "^10.1.1",
69
+ "proper-lockfile": "^4.1.2",
70
+ "yaml": "^2.8.2",
71
+ "zod": "^4.3.6"
72
+ },
73
73
  "bundledDependencies": [
74
74
  "@pencil-agent/agent-core",
75
75
  "@pencil-agent/ai",