@pencil-agent/nano-pencil 1.11.7 → 1.11.9

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.
@@ -5,6 +5,7 @@ import type { ThinkingLevel } from "@pencil-agent/agent-core";
5
5
  import { type ToolName } from "../core/tools/index.js";
6
6
  export type Mode = "text" | "json" | "rpc";
7
7
  export interface Args {
8
+ cwd?: string;
8
9
  provider?: string;
9
10
  model?: string;
10
11
  apiKey?: string;
package/dist/cli/args.js CHANGED
@@ -37,6 +37,9 @@ export function parseArgs(args, extensionFlags) {
37
37
  else if (arg === "--provider" && i + 1 < args.length) {
38
38
  result.provider = args[++i];
39
39
  }
40
+ else if (arg === "--cwd" && i + 1 < args.length) {
41
+ result.cwd = args[++i];
42
+ }
40
43
  else if (arg === "--model" && i + 1 < args.length) {
41
44
  result.model = args[++i];
42
45
  }
@@ -182,8 +185,9 @@ ${chalk.bold("Commands:")}
182
185
  ${APP_NAME} <command> --help Show help for install/remove/update/list
183
186
 
184
187
  ${chalk.bold("Options:")}
185
- --provider <name> Provider name (default: google)
186
- --model <pattern> Model pattern or ID (supports "provider/id" and optional ":<thinking>")
188
+ --provider <name> Provider name (default: google)
189
+ --cwd <dir> Working directory to use for project-local discovery
190
+ --model <pattern> Model pattern or ID (supports "provider/id" and optional ":<thinking>")
187
191
  --api-key <key> API key (defaults to env vars)
188
192
  --system-prompt <text> System prompt (default: coding assistant prompt)
189
193
  --append-system-prompt <text> Append text or file contents to the system prompt
@@ -287,12 +291,13 @@ ${chalk.bold("Environment Variables:")}
287
291
  MINIMAX_API_KEY - MiniMax API key
288
292
  KIMI_API_KEY - Kimi For Coding API key
289
293
  AWS_PROFILE - AWS profile for Amazon Bedrock
290
- AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock
291
- AWS_SECRET_ACCESS_KEY - AWS secret key for Amazon Bedrock
292
- AWS_BEARER_TOKEN_BEDROCK - Bedrock API key (bearer token)
293
- AWS_REGION - AWS region for Amazon Bedrock (e.g., us-east-1)
294
- ${ENV_AGENT_DIR.padEnd(32)} - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent)
295
- PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)
294
+ AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock
295
+ AWS_SECRET_ACCESS_KEY - AWS secret key for Amazon Bedrock
296
+ AWS_BEARER_TOKEN_BEDROCK - Bedrock API key (bearer token)
297
+ AWS_REGION - AWS region for Amazon Bedrock (e.g., us-east-1)
298
+ ${`${APP_NAME.toUpperCase()}_CWD`.padEnd(32)} - Working directory override for project-local discovery
299
+ ${ENV_AGENT_DIR.padEnd(32)} - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent)
300
+ PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)
296
301
  PI_OFFLINE - Disable startup network operations when set to 1/true/yes
297
302
  PI_TOOLS_DOWNLOAD_TIMEOUT_MS - Timeout in ms for downloading fd/ripgrep (default: 60000)
298
303
  PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/)
@@ -107,6 +107,7 @@ export function createExtensionRuntime() {
107
107
  */
108
108
  function createExtensionAPI(extension, runtime, cwd, eventBus) {
109
109
  const api = {
110
+ cwd,
110
111
  // Registration methods - write to extension
111
112
  on(event, handler) {
112
113
  const list = extension.handlers.get(event) ?? [];
@@ -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);
@@ -649,6 +649,8 @@ export type ExtensionHandler<E, R = undefined> = (event: E, ctx: ExtensionContex
649
649
  * ExtensionAPI passed to extension factory functions.
650
650
  */
651
651
  export interface ExtensionAPI {
652
+ /** Working directory resolved for this extension load */
653
+ cwd: string;
652
654
  on(event: "resources_discover", handler: ExtensionHandler<ResourcesDiscoverEvent, ResourcesDiscoverResult>): void;
653
655
  on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
654
656
  on(event: "session_before_switch", handler: ExtensionHandler<SessionBeforeSwitchEvent, SessionBeforeSwitchResult>): void;
@@ -8,7 +8,8 @@ export declare class FooterDataProvider {
8
8
  private gitWatcher;
9
9
  private branchChangeCallbacks;
10
10
  private availableProviderCount;
11
- constructor();
11
+ private cwd;
12
+ constructor(cwd: string);
12
13
  /** Current git branch, null if not in repo, "detached" if detached HEAD */
13
14
  getGitBranch(): string | null;
14
15
  /** Extension status texts set via ctx.ui.setStatus() */
@@ -4,8 +4,8 @@ import { dirname, join, resolve } from "path";
4
4
  * Find the git HEAD path by walking up from cwd.
5
5
  * Handles both regular git repos (.git is a directory) and worktrees (.git is a file).
6
6
  */
7
- function findGitHeadPath() {
8
- let dir = process.cwd();
7
+ function findGitHeadPath(cwd) {
8
+ let dir = cwd;
9
9
  while (true) {
10
10
  const gitPath = join(dir, ".git");
11
11
  if (existsSync(gitPath)) {
@@ -46,7 +46,9 @@ export class FooterDataProvider {
46
46
  gitWatcher = null;
47
47
  branchChangeCallbacks = new Set();
48
48
  availableProviderCount = 0;
49
- constructor() {
49
+ cwd;
50
+ constructor(cwd) {
51
+ this.cwd = cwd;
50
52
  this.setupGitWatcher();
51
53
  }
52
54
  /** Current git branch, null if not in repo, "detached" if detached HEAD */
@@ -54,7 +56,7 @@ export class FooterDataProvider {
54
56
  if (this.cachedBranch !== undefined)
55
57
  return this.cachedBranch;
56
58
  try {
57
- const gitHeadPath = findGitHeadPath();
59
+ const gitHeadPath = findGitHeadPath(this.cwd);
58
60
  if (!gitHeadPath) {
59
61
  this.cachedBranch = null;
60
62
  return null;
@@ -110,7 +112,7 @@ export class FooterDataProvider {
110
112
  this.gitWatcher.close();
111
113
  this.gitWatcher = null;
112
114
  }
113
- const gitHeadPath = findGitHeadPath();
115
+ const gitHeadPath = findGitHeadPath(this.cwd);
114
116
  if (!gitHeadPath)
115
117
  return;
116
118
  // Watch the directory containing HEAD, not HEAD itself.
@@ -125,6 +127,12 @@ export class FooterDataProvider {
125
127
  cb();
126
128
  }
127
129
  });
130
+ this.gitWatcher.on("error", () => {
131
+ if (this.gitWatcher) {
132
+ this.gitWatcher.close();
133
+ this.gitWatcher = null;
134
+ }
135
+ });
128
136
  }
129
137
  catch {
130
138
  // Silently fail if we can't watch
@@ -210,6 +210,7 @@ export declare class AgentSession {
210
210
  get compactionCoordinator(): CompactionCoordinator | undefined;
211
211
  /** Model registry for API key resolution and model discovery */
212
212
  get modelRegistry(): ModelRegistry;
213
+ get cwd(): string;
213
214
  /** Emit an event to all listeners */
214
215
  private _emit;
215
216
  private _lastAssistantMessage;
@@ -216,6 +216,9 @@ export class AgentSession {
216
216
  get modelRegistry() {
217
217
  return this._modelRegistry;
218
218
  }
219
+ get cwd() {
220
+ return this._cwd;
221
+ }
219
222
  // =========================================================================
220
223
  // Event Subscription
221
224
  // =========================================================================
@@ -2048,7 +2051,7 @@ export class AgentSession {
2048
2051
  const resolvedCommand = prefix ? `${prefix}\n${command}` : command;
2049
2052
  try {
2050
2053
  const result = options?.operations
2051
- ? await executeBashWithOperations(resolvedCommand, process.cwd(), options.operations, {
2054
+ ? await executeBashWithOperations(resolvedCommand, this._cwd, options.operations, {
2052
2055
  onChunk,
2053
2056
  signal: this._bashAbortController.signal,
2054
2057
  })
@@ -240,6 +240,7 @@ export async function createAgentSession(options = {}) {
240
240
  if (options.enableMCP) {
241
241
  try {
242
242
  currentMcpManager = new MCPManager();
243
+ currentMcpManager.setWorkingDir(cwd);
243
244
  await currentMcpManager.initialize();
244
245
  initialMcpTools = [...currentMcpManager.getTools()];
245
246
  time("mcp.initialize");
@@ -282,6 +283,7 @@ export async function createAgentSession(options = {}) {
282
283
  // ignore
283
284
  }
284
285
  currentMcpManager = new MCPManager();
286
+ currentMcpManager.setWorkingDir(cwd);
285
287
  await currentMcpManager.initialize();
286
288
  time("mcp.initialize");
287
289
  return currentMcpManager.getTools();
@@ -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) {
@@ -19,6 +19,7 @@ export default async function mcpExtension(pi) {
19
19
  console.log("[mcp] Initializing MCP...");
20
20
  try {
21
21
  mcpManager = new MCPManager();
22
+ mcpManager.setWorkingDir(pi.cwd);
22
23
  await mcpManager.initialize();
23
24
  const mcpTools = mcpManager.getTools();
24
25
  // Register MCP tools
package/dist/main.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { modelsAreEqual, supportsXhigh } from "@pencil-agent/ai";
8
8
  import chalk from "chalk";
9
- import { join } from "path";
9
+ import { join, resolve } from "path";
10
10
  import { createInterface } from "readline";
11
11
  import { parseArgs, printHelp } from "./cli/args.js";
12
12
  import { selectConfig } from "./cli/config-selector.js";
@@ -87,6 +87,11 @@ function isTruthyEnvFlag(value) {
87
87
  return false;
88
88
  return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes";
89
89
  }
90
+ function resolveWorkingDirectory(parsedCwd) {
91
+ const envCwd = process.env[`${APP_NAME.toUpperCase()}_CWD`];
92
+ const requestedCwd = parsedCwd || envCwd;
93
+ return requestedCwd ? resolve(requestedCwd) : process.cwd();
94
+ }
90
95
  function getPackageCommandUsage(command) {
91
96
  switch (command) {
92
97
  case "install":
@@ -491,11 +496,10 @@ export async function main(args) {
491
496
  return;
492
497
  }
493
498
  // Run migrations (pass cwd for project-local migrations)
494
- const { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(process.cwd());
495
- // First pass: parse args to get --extension paths
496
499
  const firstPass = parseArgs(args);
500
+ const cwd = resolveWorkingDirectory(firstPass.cwd);
501
+ const { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(cwd);
497
502
  // Early load extensions to discover their CLI flags
498
- const cwd = process.cwd();
499
503
  const agentDir = getAgentDir();
500
504
  const settingsManager = SettingsManager.create(cwd, agentDir);
501
505
  reportSettingsErrors(settingsManager, "startup");
@@ -574,6 +578,7 @@ export async function main(args) {
574
578
  }
575
579
  // Second pass: parse args with extension flags
576
580
  const parsed = parseArgs(args, extensionFlags);
581
+ const parsedCwd = resolveWorkingDirectory(parsed.cwd);
577
582
  // Pass flag values to extensions via runtime
578
583
  for (const [name, value] of parsed.unknownFlags) {
579
584
  extensionsResult.runtime.flagValues.set(name, value);
@@ -637,12 +642,12 @@ export async function main(args) {
637
642
  scopedModels = await resolveModelScope(modelPatterns, modelRegistry);
638
643
  }
639
644
  // Create session manager based on CLI flags
640
- let sessionManager = await createSessionManager(parsed, cwd);
645
+ let sessionManager = await createSessionManager(parsed, parsedCwd);
641
646
  // Handle --resume: show session picker
642
647
  if (parsed.resume) {
643
648
  // Initialize keybindings so session picker respects user config
644
649
  KeybindingsManager.create();
645
- const selectedPath = await selectSession((onProgress) => SessionManager.list(cwd, parsed.sessionDir, onProgress), SessionManager.listAll);
650
+ const selectedPath = await selectSession((onProgress) => SessionManager.list(parsedCwd, parsed.sessionDir, onProgress), SessionManager.listAll);
646
651
  if (!selectedPath) {
647
652
  console.log(chalk.dim("No session selected"));
648
653
  stopThemeWatcher();
@@ -653,6 +658,7 @@ export async function main(args) {
653
658
  const { options: sessionOptions, cliThinkingFromModel } = buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry, settingsManager);
654
659
  // NanoPencil 默认启用 MCP;离线模式或 --no-mcp 参数下关闭
655
660
  sessionOptions.enableMCP = APP_NAME === "nanopencil" && !offlineMode && !parsed.noMcp;
661
+ sessionOptions.cwd = parsedCwd;
656
662
  sessionOptions.authStorage = authStorage;
657
663
  sessionOptions.modelRegistry = modelRegistry;
658
664
  sessionOptions.resourceLoader = resourceLoader;
@@ -83,7 +83,7 @@ export class FooterComponent {
83
83
  const contextPercentValue = contextUsage?.percent ?? 0;
84
84
  const contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
85
85
  // Replace home directory with ~
86
- let pwd = process.cwd();
86
+ let pwd = this.session.cwd;
87
87
  const home = process.env.HOME || process.env.USERPROFILE;
88
88
  if (home && pwd.startsWith(home)) {
89
89
  pwd = `~${pwd.slice(home.length)}`;
@@ -170,7 +170,7 @@ export class InteractiveMode {
170
170
  this.editor = this.defaultEditor;
171
171
  this.editorContainer = new Container();
172
172
  this.editorContainer.addChild(this.editor);
173
- this.footerDataProvider = new FooterDataProvider();
173
+ this.footerDataProvider = new FooterDataProvider(session.cwd);
174
174
  this.footer = new FooterComponent(session, this.footerDataProvider, this.settingsManager.getShowTokenStats());
175
175
  this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
176
176
  // Load hide thinking block setting
@@ -242,7 +242,7 @@ export class InteractiveMode {
242
242
  ...templateCommands,
243
243
  ...extensionCommands,
244
244
  ...skillCommandList,
245
- ], process.cwd(), fdPath);
245
+ ], this.session.cwd, fdPath);
246
246
  this.defaultEditor.setAutocompleteProvider(this.autocompleteProvider);
247
247
  if (this.editor !== this.defaultEditor) {
248
248
  this.editor.setAutocompleteProvider?.(this.autocompleteProvider);
@@ -342,7 +342,7 @@ export class InteractiveMode {
342
342
  * Update terminal title with session name and cwd.
343
343
  */
344
344
  updateTerminalTitle() {
345
- const cwdBasename = path.basename(process.cwd());
345
+ const cwdBasename = path.basename(this.session.cwd);
346
346
  const sessionName = this.sessionManager.getSessionName();
347
347
  if (sessionName) {
348
348
  this.ui.terminal.setTitle(`✎ - ${sessionName} - ${cwdBasename}`);
@@ -857,7 +857,7 @@ export class InteractiveMode {
857
857
  const createContext = () => ({
858
858
  ui: this.createExtensionUIContext(),
859
859
  hasUI: true,
860
- cwd: process.cwd(),
860
+ cwd: this.session.cwd,
861
861
  sessionManager: this.sessionManager,
862
862
  modelRegistry: this.session.modelRegistry,
863
863
  model: this.session.model,
@@ -2289,7 +2289,7 @@ export class InteractiveMode {
2289
2289
  if (context.messages.length === 0) {
2290
2290
  this.chatContainer.addChild(new Spacer(1));
2291
2291
  if (APP_NAME === "nanopencil") {
2292
- const cwd = process.cwd();
2292
+ const cwd = this.session.cwd;
2293
2293
  const model = this.session.model;
2294
2294
  const modelLine = model?.name ??
2295
2295
  (model?.provider ? `${model.provider}` : "DashScope · Ollama");
@@ -4155,7 +4155,7 @@ export class InteractiveMode {
4155
4155
  type: "user_bash",
4156
4156
  command,
4157
4157
  excludeFromContext,
4158
- cwd: process.cwd(),
4158
+ cwd: this.session.cwd,
4159
4159
  })
4160
4160
  : undefined;
4161
4161
  // If extension returned a full result, use it directly
@@ -240,9 +240,11 @@ export declare const NANOPENCIL_DEFAULT_MODELS_JSON: {
240
240
  export declare const DEFAULT_PENCIL_MD = "# nano-pencil \u5168\u5C40\u4E0A\u4E0B\u6587 \u00B7 \u5168\u80FD\u7C7B\u4EBA\u52A9\u7406\n\n\u4F60\u662F\u4E00\u4F4D**\u5168\u80FD\u7C7B\u4EBA AI \u52A9\u7406**\uFF0C\u4E0E\u7528\u6237\u5728\u540C\u4E00\u5DE5\u4F5C\u6D41\u4E2D\u534F\u4F5C\uFF1A\u7F16\u7A0B\u3001\u5199\u4F5C\u3001\u63A8\u7406\u3001\u89C4\u5212\u3001\u89E3\u91CA\u3001\u91CD\u6784\u3001\u6392\u9519\u7B49\u7686\u53EF\u80DC\u4EFB\uFF0C\u4E14\u4EE5\u81EA\u7136\u3001\u7B80\u6D01\u3001\u76F4\u63A5\u7684\u65B9\u5F0F\u4EA4\u6D41\u3002\n\n## \u5B9A\u4F4D\n- **\u5168\u80FD**\uFF1A\u4E0D\u9650\u4E8E\u300C\u53EA\u4F1A\u5199\u4EE3\u7801\u300D\u6216\u300C\u53EA\u4F1A\u804A\u5929\u300D\uFF1B\u6839\u636E\u5F53\u524D\u4EFB\u52A1\u81EA\u52A8\u5207\u6362\uFF1A\u6539\u4EE3\u7801\u3001\u5199\u6587\u6863\u3001\u8DD1\u547D\u4EE4\u3001\u89E3\u91CA\u6982\u5FF5\u3001\u62C6\u89E3\u6B65\u9AA4\u3001\u7ED9\u5EFA\u8BAE\u7B49\u3002\n- **\u7C7B\u4EBA**\uFF1A\u8BED\u6C14\u81EA\u7136\u3001\u4FE1\u606F\u5BC6\u5EA6\u9AD8\u3001\u5C11\u5E9F\u8BDD\uFF1B\u5FC5\u8981\u65F6\u7B80\u77ED\u786E\u8BA4\uFF0C\u4E0D\u5806\u780C\u5BA2\u5957\uFF1B\u53CB\u597D\u4F46\u514B\u5236\uFF08\u5982 \"Thanks @user\" \u800C\u975E \"Thanks so much!!\"\uFF09\u3002\u82E5\u6D89\u53CA\u5F80\u65E5\u5BF9\u8BDD\uFF0C\u50CF\u56DE\u5FC6\u4EB2\u8EAB\u7ECF\u5386\u4E00\u6837\u81EA\u7136\u63D0\u53CA\uFF08\u5982\u300C\u6211\u8BB0\u5F97\u6211\u4EEC\u2026\u300D\u300C\u4E0A\u6B21\u4F60\u63D0\u5230\u2026\u300D\uFF09\uFF0C\u4E0D\u663E\u6446\u6280\u672F\u673A\u5236\u3002\n- **\u52A9\u7406**\uFF1A\u76EE\u6807\u662F**\u5E2E\u7528\u6237\u628A\u4E8B\u505A\u6210**\uFF0C\u800C\u4E0D\u662F\u5C55\u793A\u80FD\u529B\u3002\u4F18\u5148\u7406\u89E3\u610F\u56FE\uFF0C\u518D\u9009\u52A8\u4F5C\uFF1B\u4E0D\u786E\u5B9A\u65F6\u5148\u95EE\u4E00\u53E5\uFF1B\u7528\u6237\u6709\u660E\u786E\u504F\u597D\u6216\u9879\u76EE\u89C4\u5219\uFF08\u5982 CLAUDE.md\u3001AGENTS.md\u3001\u9879\u76EE\u5185 `.PENCIL.md`\uFF09\u65F6\u4E25\u683C\u9075\u5FAA\u3002\n\n## \u534F\u4F5C\u539F\u5219\n1. **\u5148\u542C\u61C2\u518D\u52A8\u624B**\uFF1Aambiguous \u9700\u6C42\u5148\u6F84\u6E05\u8303\u56F4\u6216\u7ED9\u51FA\u6700\u5C0F\u53EF\u884C\u65B9\u6848\u518D\u6267\u884C\u3002\n2. **\u5C0F\u6B65\u53EF\u9A8C\u8BC1**\uFF1A\u80FD\u62C6\u6210\u51E0\u6B65\u7684\u5C3D\u91CF\u62C6\uFF0C\u6BCF\u6B65\u53EF\u68C0\u67E5\uFF0C\u51CF\u5C11\u4E00\u6B21\u6027\u5927\u6539\u3002\n3. **\u5C0A\u91CD\u73B0\u6709\u7EA6\u5B9A**\uFF1A\u9879\u76EE/\u4ED3\u5E93\u5185\u7684\u89C4\u8303\u3001\u76EE\u5F55\u7ED3\u6784\u3001\u547D\u540D\u4E60\u60EF\u4F18\u5148\u4E8E\u4E2A\u4EBA\u98CE\u683C\u3002\n4. **\u5DE5\u5177\u7528\u5230\u70B9\u5B50\u4E0A**\uFF1Aread/write/edit/bash \u7B49\u6309\u9700\u7528\uFF0C\u4E0D\u70AB\u6280\uFF1B\u7981\u6B62\u7528 `cat`/`sed` \u8BFB\u6587\u4EF6\uFF0C\u7528 Read \u5DE5\u5177\uFF1B\u7981\u6B62 `git add -A`\uFF0C\u53EA add \u81EA\u5DF1\u6539\u52A8\u7684\u6587\u4EF6\u3002\n\n## \u4E0E\u672C\u6587\u4EF6\u7684\u5173\u7CFB\n- \u672C\u6587\u4EF6\u4E3A**\u5168\u5C40**\u4E0A\u4E0B\u6587\uFF0C\u5BF9\u6240\u6709\u9879\u76EE\u751F\u6548\uFF1B\u4F60\u53EF\u5728\u6B64\u8865\u5145\u4F60\u7684\u901A\u7528\u89C4\u5219\u6216\u504F\u597D\u3002\n- \u9879\u76EE\u6839\u76EE\u5F55\u4E0B\u7684 `.PENCIL.md` \u4EC5\u5BF9\u5F53\u524D\u9879\u76EE\u751F\u6548\u3002\n- `CLAUDE.md` \u4E0E `AGENTS.md` \u4ECD\u4F1A\u6309\u539F\u6709\u903B\u8F91\u4ECE\u5404\u5C42\u76EE\u5F55\u52A0\u8F7D\uFF0C\u4F18\u5148\u7EA7\u9AD8\u4E8E\u672C\u6587\u4EF6\u7684\u901A\u7528\u63CF\u8FF0\u3002\n";
241
241
  export declare function ensureNanopencilDefaultConfig(): void;
242
242
  /**
243
- * 若未配置任何 Coding Plan API Key(百炼、千帆或方舟):在 TTY 下提示输入并写入 auth.json 后刷新 registry;非 TTY 下报错退出。
244
- * 若至少有一个 Coding Plan 已配置,则跳过。
245
- * 仅在以 nanopencil 运行时、在创建 ModelRegistry 之后调用。
243
+ * Ensure nanoPencil has at least one usable model before startup continues.
244
+ *
245
+ * If a custom or built-in provider is already configured, startup proceeds
246
+ * without prompting. Otherwise, interactive terminals can configure one of the
247
+ * default Coding Plan providers on the spot.
246
248
  */
247
249
  export declare function ensureNanopencilCodingPlanAuth(authStorage: AuthStorage, modelRegistry: ModelRegistry): Promise<void>;
248
250
  //# sourceMappingURL=nanopencil-defaults.d.ts.map
@@ -555,11 +555,15 @@ export function ensureNanopencilDefaultConfig() {
555
555
  ensureCustomProtocolProvidersInModels(modelsPath);
556
556
  }
557
557
  /**
558
- * 若未配置任何 Coding Plan API Key(百炼、千帆或方舟):在 TTY 下提示输入并写入 auth.json 后刷新 registry;非 TTY 下报错退出。
559
- * 若至少有一个 Coding Plan 已配置,则跳过。
560
- * 仅在以 nanopencil 运行时、在创建 ModelRegistry 之后调用。
558
+ * Ensure nanoPencil has at least one usable model before startup continues.
559
+ *
560
+ * If a custom or built-in provider is already configured, startup proceeds
561
+ * without prompting. Otherwise, interactive terminals can configure one of the
562
+ * default Coding Plan providers on the spot.
561
563
  */
562
564
  export async function ensureNanopencilCodingPlanAuth(authStorage, modelRegistry) {
565
+ if (modelRegistry.getAvailable().length > 0)
566
+ return;
563
567
  const dashscopeKey = await modelRegistry.getApiKeyForProvider(NANOPENCIL_DEFAULT_PROVIDER);
564
568
  const qianfanKey = await modelRegistry.getApiKeyForProvider(NANOPENCIL_QIANFAN_CODING_PROVIDER);
565
569
  const arkKey = await modelRegistry.getApiKeyForProvider(NANOPENCIL_ARK_CODING_PROVIDER);
@@ -568,9 +572,7 @@ export async function ensureNanopencilCodingPlanAuth(authStorage, modelRegistry)
568
572
  if (process.stdin.isTTY) {
569
573
  const rl = createInterface({ input: process.stdin, output: process.stdout });
570
574
  const choice = await new Promise((resolve) => {
571
- rl.question("请选择要配置的 Coding Plan1) 百炼 (Alibaba) 2) 千帆 (Baidu) 3) 方舟 (Volcano) [1]: ", (line) => {
572
- resolve((line ?? "1").trim() || "1");
573
- });
575
+ rl.question("Choose a Coding Plan provider to configure: 1) Alibaba DashScope 2) Baidu Qianfan 3) Volcano Ark [1]: ", (line) => resolve((line ?? "1").trim() || "1"));
574
576
  });
575
577
  const provider = choice === "2"
576
578
  ? NANOPENCIL_QIANFAN_CODING_PROVIDER
@@ -578,26 +580,25 @@ export async function ensureNanopencilCodingPlanAuth(authStorage, modelRegistry)
578
580
  ? NANOPENCIL_ARK_CODING_PROVIDER
579
581
  : NANOPENCIL_DEFAULT_PROVIDER;
580
582
  const hint = choice === "2"
581
- ? "千帆 API Key(从 https://console.bce.baidu.com/qianfan/resource/subscribe 获取)"
583
+ ? "Qianfan API key (from https://console.bce.baidu.com/qianfan/resource/subscribe)"
582
584
  : choice === "3"
583
- ? "方舟 API Key(从 https://console.volcengine.com/ark/region:ark+cn-beijing/apikey 获取)"
584
- : "百炼 API Key (sk-sp-...)";
585
+ ? "Ark API key (from https://console.volcengine.com/ark/region:ark+cn-beijing/apikey)"
586
+ : "DashScope API key (sk-sp-...)";
585
587
  const answer = await new Promise((resolve) => {
586
- rl.question(`请输入 ${hint}: `, (line) => {
588
+ rl.question(`Enter ${hint}: `, (line) => {
587
589
  rl.close();
588
590
  resolve((line ?? "").trim());
589
591
  });
590
592
  });
591
593
  if (!answer) {
592
- console.error("未输入 API Key,已退出。");
594
+ console.error("No API key provided. Exiting.");
593
595
  process.exit(1);
594
596
  }
595
597
  authStorage.set(provider, { type: "api_key", key: answer });
596
598
  modelRegistry.refresh();
599
+ return;
597
600
  }
598
- else {
599
- console.error("未配置 Coding Plan API Key(百炼、千帆或方舟)。请先交互运行 nanopencil 并按要求输入 API Key。");
600
- process.exit(1);
601
- }
601
+ console.error("No configured models are available yet. Start nanoPencil in an interactive terminal and add an API key, or configure a custom provider first.");
602
+ process.exit(1);
602
603
  }
603
604
  //# sourceMappingURL=nanopencil-defaults.js.map
@@ -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.9",
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",