@pencil-agent/nano-pencil 1.8.2 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -260,10 +260,14 @@ export class AgentSession {
260
260
  await this._extensionRunner.emit({ type: "agent_start" });
261
261
  }
262
262
  else if (event.type === "agent_end") {
263
- await this._extensionRunner.emit({
263
+ // Do not block the main turn lifecycle on extension post-processing.
264
+ // Slow hooks (e.g. memory extraction) should not delay the next prompt.
265
+ void this._extensionRunner
266
+ .emit({
264
267
  type: "agent_end",
265
268
  messages: event.messages,
266
- });
269
+ })
270
+ .catch(() => { });
267
271
  }
268
272
  else if (event.type === "turn_start") {
269
273
  const extensionEvent = {
@@ -81,7 +81,9 @@ export declare class ExtensionRunner {
81
81
  private shutdownHandler;
82
82
  private shortcutDiagnostics;
83
83
  private commandDiagnostics;
84
+ private readonly beforeAgentStartTimeoutMs;
84
85
  constructor(extensions: Extension[], runtime: ExtensionRuntime, cwd: string, sessionManager: SessionManager, modelRegistry: ModelRegistry);
86
+ private withTimeout;
85
87
  bindCore(actions: ExtensionActions, contextActions: ExtensionContextActions): void;
86
88
  bindCommandContext(actions?: ExtensionCommandContextActions): void;
87
89
  setUIContext(uiContext?: ExtensionUIContext): void;
@@ -104,6 +104,7 @@ export class ExtensionRunner {
104
104
  shutdownHandler = () => { };
105
105
  shortcutDiagnostics = [];
106
106
  commandDiagnostics = [];
107
+ beforeAgentStartTimeoutMs = 1500;
107
108
  constructor(extensions, runtime, cwd, sessionManager, modelRegistry) {
108
109
  this.extensions = extensions;
109
110
  this.runtime = runtime;
@@ -112,6 +113,20 @@ export class ExtensionRunner {
112
113
  this.sessionManager = sessionManager;
113
114
  this.modelRegistry = modelRegistry;
114
115
  }
116
+ async withTimeout(promise, timeoutMs) {
117
+ return new Promise((resolve) => {
118
+ const timer = setTimeout(() => resolve(undefined), timeoutMs);
119
+ promise
120
+ .then((value) => {
121
+ clearTimeout(timer);
122
+ resolve(value);
123
+ })
124
+ .catch(() => {
125
+ clearTimeout(timer);
126
+ resolve(undefined);
127
+ });
128
+ });
129
+ }
115
130
  bindCore(actions, contextActions) {
116
131
  // Copy actions into the shared runtime (all extension APIs reference this)
117
132
  this.runtime.sendMessage = actions.sendMessage;
@@ -540,7 +555,15 @@ export class ExtensionRunner {
540
555
  images,
541
556
  systemPrompt: currentSystemPrompt,
542
557
  };
543
- const handlerResult = await handler(event, ctx);
558
+ const handlerResult = await this.withTimeout(handler(event, ctx), this.beforeAgentStartTimeoutMs);
559
+ if (handlerResult === undefined) {
560
+ this.emitError({
561
+ extensionPath: ext.path,
562
+ event: "before_agent_start",
563
+ error: `handler timed out after ${this.beforeAgentStartTimeoutMs}ms`,
564
+ });
565
+ continue;
566
+ }
544
567
  if (handlerResult) {
545
568
  const result = handlerResult;
546
569
  if (result.message) {
@@ -8,7 +8,9 @@ import { getAgentDir } from "../config.js";
8
8
  import { existsSync } from "node:fs";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { dirname } from "node:path";
11
+ import { createRequire } from "node:module";
11
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const require = createRequire(import.meta.url);
12
14
  // Try to load from bundled packages first (dist/packages/nanosoul)
13
15
  const BUNDLED_SOUL = join(__dirname, "packages", "nanosoul");
14
16
  /**
@@ -328,6 +328,7 @@ export declare class InteractiveMode {
328
328
  stop(): void;
329
329
  private handleSoulCommand;
330
330
  private handleMemoryCommand;
331
+ private handleMcpCommand;
331
332
  private handleUpdateCommand;
332
333
  }
333
334
  //# sourceMappingURL=interactive-mode.d.ts.map
@@ -14,6 +14,7 @@ import { parseSkillBlock, } from "../../core/agent-session.js";
14
14
  import { FooterDataProvider, } from "../../core/footer-data-provider.js";
15
15
  import { KeybindingsManager } from "../../core/keybindings.js";
16
16
  import { createCompactionSummaryMessage } from "../../core/messages.js";
17
+ import { listMCPServers, setMCPServerEnabled } from "../../core/mcp/mcp-config.js";
17
18
  import { resolveModelScope } from "../../core/model-resolver.js";
18
19
  import { SessionManager, } from "../../core/session-manager.js";
19
20
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
@@ -1548,6 +1549,11 @@ export class InteractiveMode {
1548
1549
  await this.handleModelCommand(searchTerm);
1549
1550
  return;
1550
1551
  }
1552
+ if (text === "/mcp" || text.startsWith("/mcp ")) {
1553
+ await this.handleMcpCommand(text);
1554
+ this.editor.setText("");
1555
+ return;
1556
+ }
1551
1557
  if (text.startsWith("/export")) {
1552
1558
  await this.handleExportCommand(text);
1553
1559
  this.editor.setText("");
@@ -1780,10 +1786,7 @@ export class InteractiveMode {
1780
1786
  this.ui.requestRender();
1781
1787
  }
1782
1788
  else if (event.message.role === "user") {
1783
- const userText = this.getUserMessageText(event.message);
1784
- if (userText &&
1785
- this.optimisticUserMessages.length > 0 &&
1786
- this.optimisticUserMessages[0] === userText) {
1789
+ if (this.optimisticUserMessages.length > 0) {
1787
1790
  this.optimisticUserMessages.shift();
1788
1791
  this.updatePendingMessagesDisplay();
1789
1792
  this.ui.requestRender();
@@ -3991,6 +3994,43 @@ export class InteractiveMode {
3991
3994
  this.chatContainer.addChild(new Text(lines.join("\n"), 1, 0));
3992
3995
  this.ui.requestRender();
3993
3996
  }
3997
+ async handleMcpCommand(text) {
3998
+ const parts = text.trim().split(/\s+/);
3999
+ const action = (parts[1] || "list").toLowerCase();
4000
+ const target = parts[2];
4001
+ if (action === "list") {
4002
+ const servers = listMCPServers();
4003
+ this.chatContainer.addChild(new Spacer(1));
4004
+ if (servers.length === 0) {
4005
+ this.chatContainer.addChild(new Text(theme.fg("dim", "No MCP servers configured."), 1, 0));
4006
+ }
4007
+ else {
4008
+ const lines = [
4009
+ theme.bold("MCP Servers"),
4010
+ "",
4011
+ ...servers.map((s) => {
4012
+ const status = s.enabled === false ? "disabled" : "enabled";
4013
+ return `- ${s.id} (${s.name}) [${status}]`;
4014
+ }),
4015
+ "",
4016
+ theme.fg("dim", "Use: /mcp enable <id> or /mcp disable <id>"),
4017
+ ];
4018
+ this.chatContainer.addChild(new Text(lines.join("\n"), 1, 0));
4019
+ }
4020
+ this.ui.requestRender();
4021
+ return;
4022
+ }
4023
+ if ((action === "enable" || action === "disable") && target) {
4024
+ setMCPServerEnabled(target, action === "enable");
4025
+ this.chatContainer.addChild(new Spacer(1));
4026
+ this.chatContainer.addChild(new Text(`${target} ${action === "enable" ? "enabled" : "disabled"}. Run /reload to apply changes.`, 1, 0));
4027
+ this.ui.requestRender();
4028
+ return;
4029
+ }
4030
+ this.chatContainer.addChild(new Spacer(1));
4031
+ this.chatContainer.addChild(new Text("Usage: /mcp [list|enable <id>|disable <id>]", 1, 0));
4032
+ this.ui.requestRender();
4033
+ }
3994
4034
  async handleUpdateCommand() {
3995
4035
  this.chatContainer.addChild(new Spacer(1));
3996
4036
  this.chatContainer.addChild(new Text(theme.fg("accent", "🔍 Checking for updates..."), 1, 0));
@@ -12,7 +12,8 @@ import { fileURLToPath } from "node:url";
12
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
13
  const require = createRequire(import.meta.url);
14
14
  /** 内置扩展路径 */
15
- const BUNDLED_NANOMEM_EXTENSION = join(__dirname, "extensions", "nanomem", "extension.js");
15
+ const BUNDLED_NANOMEM_EXTENSION_PACKAGES = join(__dirname, "packages", "nanomem", "extension.js");
16
+ const BUNDLED_NANOMEM_EXTENSION_LEGACY = join(__dirname, "extensions", "nanomem", "extension.js");
16
17
  const BUNDLED_SIMPLIFY_EXTENSION = join(__dirname, "extensions", "simplify", "index.js");
17
18
  /** 从当前模块位置向上查找包根(含 package.json 且 name 为 nano-pencil 相关) */
18
19
  function findPackageRoot(startDir) {
@@ -40,19 +41,23 @@ function findPackageRoot(startDir) {
40
41
  export function getNanopencilDefaultExtensionPaths() {
41
42
  const paths = [];
42
43
  // === NanoMem 扩展 ===
43
- // 1) 优先使用 build 时内置的扩展(发布后开箱即用)
44
- if (existsSync(BUNDLED_NANOMEM_EXTENSION)) {
45
- paths.push(BUNDLED_NANOMEM_EXTENSION);
44
+ // 1) 优先使用 build 时打包到 dist/packages 的扩展
45
+ if (existsSync(BUNDLED_NANOMEM_EXTENSION_PACKAGES)) {
46
+ paths.push(BUNDLED_NANOMEM_EXTENSION_PACKAGES);
47
+ }
48
+ else if (existsSync(BUNDLED_NANOMEM_EXTENSION_LEGACY)) {
49
+ // 2) 兼容旧版 dist/extensions 路径
50
+ paths.push(BUNDLED_NANOMEM_EXTENSION_LEGACY);
46
51
  }
47
52
  else {
48
- // 2) require.resolve:开发/本地安装时 node_modules 中的 nanomem
53
+ // 3) require.resolve:开发/本地安装时 node_modules 中的 nanomem
49
54
  try {
50
55
  const extPath = require.resolve("nanomem/extension.js");
51
56
  if (existsSync(extPath))
52
57
  paths.push(extPath);
53
58
  }
54
59
  catch {
55
- // 3) 按包根 + node_modules/nanomem/dist/extension.js 查找
60
+ // 4) 按包根 + node_modules/nanomem/dist/extension.js 查找
56
61
  const packageRoot = findPackageRoot(__dirname);
57
62
  if (packageRoot) {
58
63
  const candidate = join(packageRoot, "node_modules", "nanomem", "dist", "extension.js");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pencil-agent/nano-pencil",
3
- "version": "1.8.2",
3
+ "version": "1.9.0",
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": {