@pencil-agent/nano-pencil 1.14.2 → 1.14.4

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.
Files changed (104) hide show
  1. package/dist/build-meta.json +3 -3
  2. package/dist/builtin-extensions.d.ts +4 -0
  3. package/dist/builtin-extensions.js +16 -16
  4. package/dist/core/config/settings-manager.d.ts +8 -1
  5. package/dist/core/config/settings-manager.js +9 -0
  6. package/dist/core/extensions/runner.d.ts +70 -4
  7. package/dist/core/extensions/runner.js +188 -8
  8. package/dist/core/extensions/types.d.ts +8 -1
  9. package/dist/core/i18n/slash-commands.d.ts +12 -0
  10. package/dist/core/i18n/slash-commands.js +16 -4
  11. package/dist/core/i18n/slash-commands.zh.d.ts +12 -0
  12. package/dist/core/i18n/slash-commands.zh.js +16 -4
  13. package/dist/core/runtime/agent-session.d.ts +5 -0
  14. package/dist/core/runtime/agent-session.js +85 -27
  15. package/dist/core/runtime/extension-core-bindings.d.ts +3 -3
  16. package/dist/core/runtime/extension-core-bindings.js +73 -20
  17. package/dist/core/runtime/retry-coordinator.d.ts +10 -1
  18. package/dist/core/runtime/retry-coordinator.js +20 -4
  19. package/dist/core/runtime/sdk.js +2 -1
  20. package/dist/core/runtime/slash-command-catalog.d.ts +2 -1
  21. package/dist/core/runtime/slash-command-catalog.js +9 -1
  22. package/dist/core/slash-commands.d.ts +12 -1
  23. package/dist/core/slash-commands.js +81 -32
  24. package/dist/core/telemetry/batching-dispatcher.d.ts +41 -0
  25. package/dist/core/telemetry/batching-dispatcher.js +89 -0
  26. package/dist/core/telemetry/build-meta.d.ts +12 -0
  27. package/dist/core/telemetry/build-meta.js +57 -0
  28. package/dist/core/telemetry/caller-context.d.ts +32 -0
  29. package/dist/core/telemetry/caller-context.js +19 -0
  30. package/dist/core/telemetry/credentials.d.ts +27 -0
  31. package/dist/core/telemetry/credentials.js +87 -0
  32. package/dist/core/telemetry/ext-events.d.ts +89 -0
  33. package/dist/core/telemetry/ext-events.js +189 -0
  34. package/dist/core/telemetry/index.d.ts +13 -0
  35. package/dist/core/telemetry/index.js +6 -0
  36. package/dist/core/telemetry/insforge-base.d.ts +37 -0
  37. package/dist/core/telemetry/insforge-base.js +160 -0
  38. package/dist/core/telemetry/types.d.ts +33 -0
  39. package/dist/core/telemetry/types.js +7 -0
  40. package/dist/extensions/defaults/browser/index.js +14 -6
  41. package/dist/extensions/defaults/btw/index.js +2 -2
  42. package/dist/extensions/defaults/debug/index.js +38 -3
  43. package/dist/extensions/defaults/diagnostics/index.js +12 -0
  44. package/dist/extensions/defaults/grub/grub-parser.d.ts +15 -1
  45. package/dist/extensions/defaults/grub/grub-parser.js +31 -1
  46. package/dist/extensions/defaults/grub/index.d.ts +1 -1
  47. package/dist/extensions/defaults/grub/index.js +4 -3
  48. package/dist/extensions/defaults/interview/index.js +2 -2
  49. package/dist/extensions/defaults/link-world/index.js +14 -6
  50. package/dist/extensions/defaults/loop/cron/cron-scheduler.js +19 -0
  51. package/dist/extensions/defaults/loop/index.js +35 -0
  52. package/dist/extensions/defaults/mcp/index.js +18 -0
  53. package/dist/extensions/defaults/plan/index.js +29 -11
  54. package/dist/extensions/defaults/presence/index.d.ts +12 -2
  55. package/dist/extensions/defaults/presence/index.js +77 -23
  56. package/dist/extensions/defaults/presence/presence-memory.d.ts +2 -1
  57. package/dist/extensions/defaults/presence/presence-memory.js +37 -1
  58. package/dist/extensions/defaults/recap/index.js +12 -0
  59. package/dist/extensions/defaults/sal/eval/insforge-sink.d.ts +6 -17
  60. package/dist/extensions/defaults/sal/eval/insforge-sink.js +40 -183
  61. package/dist/extensions/defaults/sal/index.js +31 -8
  62. package/dist/extensions/defaults/sal/sal-config.d.ts +5 -0
  63. package/dist/extensions/defaults/sal/sal-config.js +15 -82
  64. package/dist/extensions/defaults/security-audit/index.js +141 -83
  65. package/dist/extensions/defaults/subagent/index.js +29 -5
  66. package/dist/extensions/defaults/team/index.js +10 -9
  67. package/dist/extensions/defaults/team/team-parser.d.ts +18 -0
  68. package/dist/extensions/defaults/team/team-parser.js +91 -3
  69. package/dist/extensions/defaults/team/team-ui.js +4 -14
  70. package/dist/extensions/defaults/token-save/index.js +14 -1
  71. package/dist/extensions/optional/export-html/index.js +19 -5
  72. package/dist/extensions/optional/simplify/index.js +11 -5
  73. package/dist/modes/interactive/interactive-mode.d.ts +2 -1
  74. package/dist/modes/interactive/interactive-mode.js +68 -19
  75. package/dist/modes/interactive/slash-command-arguments.d.ts +16 -0
  76. package/dist/modes/interactive/slash-command-arguments.js +97 -0
  77. package/dist/modes/rpc/rpc-mode.d.ts +3 -0
  78. package/dist/modes/rpc/rpc-mode.js +40 -31
  79. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  80. package/dist/node_modules/@pencil-agent/agent-core/agent.d.ts +15 -1
  81. package/dist/node_modules/@pencil-agent/agent-core/agent.js +13 -1
  82. package/dist/node_modules/@pencil-agent/agent-core/index.d.ts +2 -1
  83. package/dist/node_modules/@pencil-agent/agent-core/index.js +2 -1
  84. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.d.ts +1 -1
  85. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.js +293 -20
  86. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-streaming-tool-executor.d.ts +33 -0
  87. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-streaming-tool-executor.js +189 -0
  88. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-tool-orchestration.d.ts +9 -0
  89. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-tool-orchestration.js +30 -5
  90. package/dist/node_modules/@pencil-agent/agent-core/types.d.ts +90 -3
  91. package/dist/node_modules/@pencil-agent/agent-core/types.js +1 -1
  92. package/dist/node_modules/@pencil-agent/ai/models.generated.d.ts +0 -17
  93. package/dist/node_modules/@pencil-agent/ai/models.generated.js +29 -46
  94. package/dist/node_modules/@pencil-agent/tui/autocomplete.d.ts +8 -1
  95. package/dist/node_modules/@pencil-agent/tui/autocomplete.js +18 -2
  96. package/dist/node_modules/@pencil-agent/tui/index.d.ts +1 -1
  97. package/dist/node_modules/@pencil-agent/tui/tui.d.ts +8 -3
  98. package/dist/node_modules/@pencil-agent/tui/tui.js +68 -33
  99. package/dist/packages/mem-core/extension.js +154 -83
  100. package/dist/packages/mem-core/full-insights-sections.d.ts +48 -0
  101. package/dist/packages/mem-core/full-insights-sections.js +231 -0
  102. package/dist/packages/mem-core/full-insights.d.ts +1 -1
  103. package/dist/packages/mem-core/full-insights.js +102 -42
  104. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.14.2",
3
- "commitHash": "62cd264",
2
+ "version": "1.14.4",
3
+ "commitHash": "092b256",
4
4
  "branch": "main",
5
- "builtAt": "2026-05-26T18:24:58.388Z"
5
+ "builtAt": "2026-05-27T16:26:12.388Z"
6
6
  }
@@ -5,6 +5,7 @@
5
5
  * [HERE]: builtin-extensions.ts - built-in extension registry for NanoPencil
6
6
  */
7
7
  export type BuiltinExtensionRiskLevel = "passive" | "command" | "tool" | "background" | "write-capable";
8
+ export type BuiltinExtensionTestContract = "lifecycle" | "external-process" | "resource-discovery" | "write-guard";
8
9
  export interface BuiltinExtension {
9
10
  id: string;
10
11
  category: "default" | "optional" | "package";
@@ -14,6 +15,9 @@ export interface BuiltinExtension {
14
15
  startsTimers: boolean;
15
16
  writesWorkspace: boolean;
16
17
  externalProcess: boolean;
18
+ resourceDiscovery?: boolean;
19
+ testContracts?: readonly BuiltinExtensionTestContract[];
20
+ testFiles?: readonly string[];
17
21
  }
18
22
  export declare const builtInExtensions: readonly BuiltinExtension[];
19
23
  /**
@@ -33,29 +33,29 @@ const BUNDLED_RECAP_EXTENSION = join(__dirname, "extensions", "defaults", "recap
33
33
  const BUNDLED_DEBUG_EXTENSION = join(__dirname, "extensions", "defaults", "debug", "index.js");
34
34
  const BUNDLED_MCP_EXTENSION = join(__dirname, "extensions", "defaults", "mcp", "index.js");
35
35
  export const builtInExtensions = [
36
- { id: "diagnostics", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: true, writesWorkspace: false, externalProcess: false },
37
- { id: "sal", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
36
+ { id: "diagnostics", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: true, writesWorkspace: false, externalProcess: false, testContracts: ["lifecycle"], testFiles: ["test/diagnostic-buffer-throttle.test.ts", "test/diagnostics-runtime.test.ts"] },
37
+ { id: "sal", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false, testContracts: ["lifecycle"], testFiles: ["test/sal-lifecycle.test.ts"] },
38
38
  { id: "token-save", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
39
- { id: "nanomem", category: "package", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
40
- { id: "link-world", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true },
41
- { id: "browser", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true },
39
+ { id: "nanomem", category: "package", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false, testContracts: ["lifecycle"], testFiles: ["packages/mem-core/test/extension-commands.test.ts"] },
40
+ { id: "link-world", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true, resourceDiscovery: true, testContracts: ["external-process", "resource-discovery"], testFiles: ["test/link-world-extension-registration.test.ts"] },
41
+ { id: "browser", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true, resourceDiscovery: true, testContracts: ["external-process", "resource-discovery"], testFiles: ["test/browser-extension-registration.test.ts"] },
42
42
  { id: "security-audit", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
43
- { id: "soul", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
44
- { id: "presence", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: true, startsTimers: true, writesWorkspace: false, externalProcess: false },
43
+ { id: "soul", category: "default", defaultEnabled: true, riskLevel: "passive", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
44
+ { id: "presence", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: true, startsTimers: true, writesWorkspace: false, externalProcess: false, testContracts: ["lifecycle"], testFiles: ["test/presence-opening.test.ts", "test/presence-locale.test.ts"] },
45
45
  { id: "interview", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
46
- { id: "grub", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true },
47
- { id: "loop", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: true, writesWorkspace: false, externalProcess: false },
46
+ { id: "grub", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true, testContracts: ["lifecycle", "external-process"], testFiles: ["test/grub-controller.test.ts"] },
47
+ { id: "loop", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: true, writesWorkspace: false, externalProcess: false, testContracts: ["lifecycle"], testFiles: ["test/loop-lifecycle.test.ts"] },
48
48
  { id: "plan", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
49
- { id: "discipline", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
50
- { id: "subagent", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true },
51
- { id: "team", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true },
52
- { id: "idle-think", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: true, startsTimers: true, writesWorkspace: false, externalProcess: true },
49
+ { id: "discipline", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false, resourceDiscovery: true, testContracts: ["resource-discovery"], testFiles: ["test/discipline-extension.test.ts", "test/extension-smoke.test.ts"] },
50
+ { id: "subagent", category: "default", defaultEnabled: true, riskLevel: "tool", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true, testContracts: ["external-process"], testFiles: ["test/subagent-parser.test.ts", "test/worktree-manager.test.ts"] },
51
+ { id: "team", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true, testContracts: ["lifecycle", "external-process"], testFiles: ["test/team-runtime.test.ts"] },
52
+ { id: "idle-think", category: "default", defaultEnabled: true, riskLevel: "background", requiresUI: true, startsTimers: true, writesWorkspace: false, externalProcess: true, testContracts: ["lifecycle", "external-process"], testFiles: ["test/idle-think-runtime.test.ts", "test/extension-smoke.test.ts"] },
53
53
  { id: "btw", category: "default", defaultEnabled: true, riskLevel: "command", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
54
54
  { id: "recap", category: "default", defaultEnabled: true, riskLevel: "command", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
55
55
  { id: "debug", category: "default", defaultEnabled: true, riskLevel: "command", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: false },
56
- { id: "mcp", category: "default", defaultEnabled: true, riskLevel: "command", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true },
57
- { id: "simplify", category: "optional", defaultEnabled: false, riskLevel: "write-capable", requiresUI: false, startsTimers: false, writesWorkspace: true, externalProcess: true },
58
- { id: "export-html", category: "optional", defaultEnabled: false, riskLevel: "write-capable", requiresUI: false, startsTimers: false, writesWorkspace: true, externalProcess: false },
56
+ { id: "mcp", category: "default", defaultEnabled: true, riskLevel: "command", requiresUI: false, startsTimers: false, writesWorkspace: false, externalProcess: true, resourceDiscovery: true, testContracts: ["external-process", "resource-discovery"], testFiles: ["test/resource-discovery-contract.test.ts"] },
57
+ { id: "simplify", category: "optional", defaultEnabled: false, riskLevel: "write-capable", requiresUI: false, startsTimers: false, writesWorkspace: true, externalProcess: true, testContracts: ["external-process", "write-guard"], testFiles: ["test/simplify-extension.test.ts"] },
58
+ { id: "export-html", category: "optional", defaultEnabled: false, riskLevel: "write-capable", requiresUI: false, startsTimers: false, writesWorkspace: true, externalProcess: false, testContracts: ["write-guard"], testFiles: ["test/extension-smoke.test.ts", "test/export-html-branch-navigation.test.ts"] },
59
59
  ];
60
60
  /** Find package root from current module location (containing package.json with nano-pencil related name) */
61
61
  function findPackageRoot(startDir) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * [WHO]: SettingsManager class, two-tier settings (global + project-local)
2
+ * [WHO]: SettingsManager class, two-tier settings (global + project-local), agent loop defaults
3
3
  * [FROM]: Depends on ai, node:fs, proper-lockfile, config.ts
4
4
  * [TO]: Consumed by index.ts, main.ts, core/runtime/sdk.ts, cli/config-selector.ts, extensions/defaults/team/index.ts, modes/interactive/components/model-selector.ts, modes/interactive/components/config-selector.ts, and test files
5
5
  * [HERE]: core/config/settings-manager.ts - user preferences aggregation
@@ -20,6 +20,9 @@ export interface RetrySettings {
20
20
  baseDelayMs?: number;
21
21
  maxDelayMs?: number;
22
22
  }
23
+ export interface AgentLoopSettings {
24
+ maxToolResultBatchSizeChars?: number;
25
+ }
23
26
  export interface TerminalSettings {
24
27
  showImages?: boolean;
25
28
  clearOnShrink?: boolean;
@@ -65,6 +68,7 @@ export interface Settings {
65
68
  defaultModel?: string;
66
69
  defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
67
70
  agentLoopFramework?: AgentLoopFrameworkSettingInput;
71
+ agentLoop?: AgentLoopSettings;
68
72
  transport?: TransportSetting;
69
73
  steeringMode?: "all" | "one-at-a-time";
70
74
  followUpMode?: "all" | "one-at-a-time";
@@ -218,6 +222,9 @@ export declare class SettingsManager {
218
222
  getDefaultThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | undefined;
219
223
  setDefaultThinkingLevel(level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"): void;
220
224
  getAgentLoopFramework(): AgentLoopFrameworkSetting | undefined;
225
+ getAgentLoopSettings(): {
226
+ maxToolResultBatchSizeChars: number;
227
+ };
221
228
  setAgentLoopFramework(framework: AgentLoopFrameworkSettingInput | undefined): void;
222
229
  getTransport(): TransportSetting;
223
230
  setTransport(transport: TransportSetting): void;
@@ -4,6 +4,7 @@ import lockfile from "proper-lockfile";
4
4
  import { APP_NAME, CONFIG_DIR_NAME } from "../../config.js";
5
5
  import { defaultAgentDirContext } from "../agent-dir/agent-dir-context.js";
6
6
  import { readFile, writeFile, mkdir } from "node:fs/promises";
7
+ const DEFAULT_MAX_TOOL_RESULT_BATCH_SIZE_CHARS = 200_000;
7
8
  export function normalizeAgentLoopFrameworkSetting(value) {
8
9
  if (value === "high-intelligence")
9
10
  return "standard";
@@ -574,6 +575,14 @@ export class SettingsManager {
574
575
  getAgentLoopFramework() {
575
576
  return normalizeAgentLoopFrameworkSetting(this.settings.agentLoopFramework);
576
577
  }
578
+ getAgentLoopSettings() {
579
+ const configured = this.settings.agentLoop?.maxToolResultBatchSizeChars;
580
+ return {
581
+ maxToolResultBatchSizeChars: typeof configured === "number" && Number.isFinite(configured) && configured > 0
582
+ ? Math.floor(configured)
583
+ : DEFAULT_MAX_TOOL_RESULT_BATCH_SIZE_CHARS,
584
+ };
585
+ }
577
586
  setAgentLoopFramework(framework) {
578
587
  const normalized = normalizeAgentLoopFrameworkSetting(framework);
579
588
  if (normalized === undefined) {
@@ -1,8 +1,8 @@
1
1
  /**
2
- * [WHO]: ExtensionRunner class, lifecycle management, event emission
3
- * [FROM]: Depends on agent-core, ai, tui, modes/theme, session-manager, types.ts
4
- * [TO]: Consumed by core/extensions/index.ts, core/extensions/wrapper.ts
5
- * [HERE]: core/extensions/runner.ts - extension execution and lifecycle management
2
+ * [WHO]: ExtensionRunner class, lifecycle management, event emission, slash-command dispatch chokepoint (invokeCommand), telemetry sink wiring (setTelemetrySink)
3
+ * [FROM]: Depends on agent-core, ai, tui, modes/theme, session-manager, types.ts, core/telemetry (ExtensionTelemetrySink + classifyArgsSignature for the P1 ext_command_events writer)
4
+ * [TO]: Consumed by core/extensions/index.ts, core/extensions/wrapper.ts, core/runtime/agent-session.ts (delegates command dispatch via invokeCommand)
5
+ * [HERE]: core/extensions/runner.ts - extension execution and lifecycle management; owns the single try/catch around command.handler so telemetry can wrap every invocation regardless of caller mode
6
6
  */
7
7
  import type { AgentMessage } from "@pencil-agent/agent-core";
8
8
  import type { ImageContent } from "@pencil-agent/ai";
@@ -11,6 +11,7 @@ import type { ResourceDiagnostic } from "../diagnostics.js";
11
11
  import type { KeybindingsConfig } from "../keybindings.js";
12
12
  import type { ModelRegistry } from "../model-registry.js";
13
13
  import type { SessionManager } from "../session/session-manager.js";
14
+ import { type ExtensionTelemetrySink, type LlmCallEventInput } from "../telemetry/index.js";
14
15
  import type { BeforeAgentStartEvent, BeforeAgentStartEventResult, ContextEvent, Extension, ExtensionActions, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFlag, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, InputEvent, InputEventResult, InputSource, MessageRenderer, RegisteredCommand, RegisteredTool, ResourcesDiscoverEvent, SessionBeforeCompactResult, SessionBeforeForkResult, SessionBeforeSwitchResult, SessionBeforeTreeResult, ToolCallEvent, ToolCallEventResult, ToolResultEvent, ToolResultEventResult, UserBashEvent, UserBashEventResult } from "./types.js";
15
16
  /** Combined result from all before_agent_start handlers */
16
17
  interface BeforeAgentStartCombinedResult {
@@ -90,6 +91,7 @@ export declare class ExtensionRunner {
90
91
  private shutdownHandler;
91
92
  private shortcutDiagnostics;
92
93
  private commandDiagnostics;
94
+ private telemetrySink?;
93
95
  private _beforeAgentStartTimeoutMs;
94
96
  private get beforeAgentStartTimeoutMs();
95
97
  private readonly beforeAgentStartTimeoutSentinel;
@@ -127,6 +129,70 @@ export declare class ExtensionRunner {
127
129
  extensionPath: string;
128
130
  }>;
129
131
  getCommand(name: string): RegisteredCommand | undefined;
132
+ /**
133
+ * Returns the owning Extension for a given slash command name. Used by the
134
+ * telemetry middleware to stamp `extension_name` on each ext_command_events
135
+ * row. Falls back to undefined when no extension claims the command.
136
+ */
137
+ private findCommandOwner;
138
+ /**
139
+ * Derive a short, stable extension name from an Extension record. For
140
+ * built-ins (extensions/defaults/<name>) and most user extensions
141
+ * (packages/<name>) the directory basename is the right answer.
142
+ */
143
+ private deriveExtensionName;
144
+ /**
145
+ * Attach (or replace) the extension telemetry sink. The runner owns sink
146
+ * lifecycle from this point — invokeCommand() will fire-and-forget one
147
+ * `ext_command_events` row per invocation, and writeLlmCallEvent() (called
148
+ * from extension-core-bindings) writes one `ext_llm_calls` row per
149
+ * extension-initiated LLM call. Passing the noop sink (the factory's
150
+ * default when no insforge credentials exist) is the safe way to disable
151
+ * telemetry without scattering null-checks at the call sites.
152
+ */
153
+ setTelemetrySink(sink: ExtensionTelemetrySink): void;
154
+ /**
155
+ * Passthrough used by core/runtime/extension-core-bindings.ts after each
156
+ * extension-initiated LLM call. The runner owns the sink; the binding
157
+ * doesn't import it directly to keep its concerns scoped to LLM plumbing.
158
+ */
159
+ writeLlmCallEvent(input: LlmCallEventInput): void;
160
+ /**
161
+ * Wrap an extension hook handler invocation in three layers:
162
+ *
163
+ * 1. AsyncLocalStorage frame so any LLM call placed by the handler gets
164
+ * attributed to this extension + hook + isUserInitiated=false in
165
+ * ext_llm_calls.
166
+ * 2. Wall-clock timing measurement (only when the sample roll passes).
167
+ * 3. One fire-and-forget ext_hook_events row per sampled invocation,
168
+ * capturing duration_ms + ok + error_code + sample_rate.
169
+ *
170
+ * High-frequency hooks (tool_*) are sampled at 10% so the table doesn't
171
+ * drown in tool-execution rows; the sample_rate column on each row lets
172
+ * dashboards extrapolate with `count(*) * 1/sample_rate`. Sampling decision
173
+ * sits inside the AsyncLocalStorage frame so even skipped-emit hooks still
174
+ * attribute their LLM calls.
175
+ */
176
+ private invokeHookHandler;
177
+ /**
178
+ * Single chokepoint for slash command dispatch. All modes (interactive /
179
+ * print / rpc / acp) funnel through agent-session._tryExecuteExtensionCommand,
180
+ * which calls this method. The wrapper measures wall-clock duration,
181
+ * captures outcome (ok / error / cancelled), and emits one telemetry row
182
+ * per invocation. Errors are still routed through emitError() so existing
183
+ * UI surfaces (toasts, logs) keep working unchanged.
184
+ *
185
+ * Returns `{ found: false }` when no extension owns the command, letting
186
+ * the caller fall through to built-in command handling without emitting a
187
+ * telemetry row for an unknown command.
188
+ */
189
+ invokeCommand(commandName: string, args: string, ctx: ExtensionCommandContext, metadata?: {
190
+ sessionId?: string | null;
191
+ runId?: string | null;
192
+ variant?: string | null;
193
+ }): Promise<{
194
+ found: boolean;
195
+ }>;
130
196
  /**
131
197
  * Request a graceful shutdown. Called by extension tools and event handlers.
132
198
  * The actual shutdown behavior is provided by the mode via bindExtensions().
@@ -1,4 +1,5 @@
1
1
  import { theme } from "../../modes/interactive/theme/theme.js";
2
+ import { classifyArgsSignature, HOOK_SAMPLE_RATES, runWithExtCallerContext, } from "../telemetry/index.js";
2
3
  // Keybindings for these actions cannot be overridden by extensions
3
4
  const RESERVED_ACTIONS_FOR_EXTENSION_CONFLICTS = [
4
5
  "interrupt",
@@ -108,6 +109,7 @@ export class ExtensionRunner {
108
109
  shutdownHandler = () => { };
109
110
  shortcutDiagnostics = [];
110
111
  commandDiagnostics = [];
112
+ telemetrySink;
111
113
  _beforeAgentStartTimeoutMs = 1500;
112
114
  get beforeAgentStartTimeoutMs() { return this._beforeAgentStartTimeoutMs ?? 1500; }
113
115
  beforeAgentStartTimeoutSentinel = Symbol("before_agent_start_timeout");
@@ -378,6 +380,184 @@ export class ExtensionRunner {
378
380
  }
379
381
  return undefined;
380
382
  }
383
+ /**
384
+ * Returns the owning Extension for a given slash command name. Used by the
385
+ * telemetry middleware to stamp `extension_name` on each ext_command_events
386
+ * row. Falls back to undefined when no extension claims the command.
387
+ */
388
+ findCommandOwner(name) {
389
+ for (const ext of this.extensions) {
390
+ if (ext.commands.has(name))
391
+ return ext;
392
+ }
393
+ return undefined;
394
+ }
395
+ /**
396
+ * Derive a short, stable extension name from an Extension record. For
397
+ * built-ins (extensions/defaults/<name>) and most user extensions
398
+ * (packages/<name>) the directory basename is the right answer.
399
+ */
400
+ deriveExtensionName(ext) {
401
+ const path = ext.path || ext.resolvedPath || "";
402
+ const segments = path.replace(/\/+$/, "").split(/[\\/]/);
403
+ return segments[segments.length - 1] || "unknown";
404
+ }
405
+ /**
406
+ * Attach (or replace) the extension telemetry sink. The runner owns sink
407
+ * lifecycle from this point — invokeCommand() will fire-and-forget one
408
+ * `ext_command_events` row per invocation, and writeLlmCallEvent() (called
409
+ * from extension-core-bindings) writes one `ext_llm_calls` row per
410
+ * extension-initiated LLM call. Passing the noop sink (the factory's
411
+ * default when no insforge credentials exist) is the safe way to disable
412
+ * telemetry without scattering null-checks at the call sites.
413
+ */
414
+ setTelemetrySink(sink) {
415
+ this.telemetrySink = sink;
416
+ }
417
+ /**
418
+ * Passthrough used by core/runtime/extension-core-bindings.ts after each
419
+ * extension-initiated LLM call. The runner owns the sink; the binding
420
+ * doesn't import it directly to keep its concerns scoped to LLM plumbing.
421
+ */
422
+ writeLlmCallEvent(input) {
423
+ try {
424
+ this.telemetrySink?.writeLlmCallEvent(input);
425
+ }
426
+ catch {
427
+ // Telemetry must never destabilize the LLM call path.
428
+ }
429
+ }
430
+ /**
431
+ * Wrap an extension hook handler invocation in three layers:
432
+ *
433
+ * 1. AsyncLocalStorage frame so any LLM call placed by the handler gets
434
+ * attributed to this extension + hook + isUserInitiated=false in
435
+ * ext_llm_calls.
436
+ * 2. Wall-clock timing measurement (only when the sample roll passes).
437
+ * 3. One fire-and-forget ext_hook_events row per sampled invocation,
438
+ * capturing duration_ms + ok + error_code + sample_rate.
439
+ *
440
+ * High-frequency hooks (tool_*) are sampled at 10% so the table doesn't
441
+ * drown in tool-execution rows; the sample_rate column on each row lets
442
+ * dashboards extrapolate with `count(*) * 1/sample_rate`. Sampling decision
443
+ * sits inside the AsyncLocalStorage frame so even skipped-emit hooks still
444
+ * attribute their LLM calls.
445
+ */
446
+ invokeHookHandler(ext, hookName, fn) {
447
+ const extensionName = this.deriveExtensionName(ext);
448
+ const ctx = {
449
+ extensionName,
450
+ callerContext: `hook:${hookName}`,
451
+ isUserInitiated: false,
452
+ };
453
+ return runWithExtCallerContext(ctx, async () => {
454
+ const sampleRate = HOOK_SAMPLE_RATES[hookName] ?? 1.0;
455
+ if (sampleRate < 1.0 && Math.random() >= sampleRate) {
456
+ return await fn();
457
+ }
458
+ const recordedAt = new Date();
459
+ const startPerf = performance.now();
460
+ let ok = true;
461
+ let errorCode = null;
462
+ try {
463
+ return await fn();
464
+ }
465
+ catch (err) {
466
+ ok = false;
467
+ errorCode = err instanceof Error ? err.constructor.name : "unknown";
468
+ throw err;
469
+ }
470
+ finally {
471
+ try {
472
+ this.telemetrySink?.writeHookEvent({
473
+ extensionName,
474
+ hookName,
475
+ durationMs: Math.round(performance.now() - startPerf),
476
+ ok,
477
+ errorCode,
478
+ sampleRate,
479
+ recordedAt,
480
+ });
481
+ }
482
+ catch {
483
+ // Telemetry never destabilizes hook dispatch.
484
+ }
485
+ }
486
+ });
487
+ }
488
+ /**
489
+ * Single chokepoint for slash command dispatch. All modes (interactive /
490
+ * print / rpc / acp) funnel through agent-session._tryExecuteExtensionCommand,
491
+ * which calls this method. The wrapper measures wall-clock duration,
492
+ * captures outcome (ok / error / cancelled), and emits one telemetry row
493
+ * per invocation. Errors are still routed through emitError() so existing
494
+ * UI surfaces (toasts, logs) keep working unchanged.
495
+ *
496
+ * Returns `{ found: false }` when no extension owns the command, letting
497
+ * the caller fall through to built-in command handling without emitting a
498
+ * telemetry row for an unknown command.
499
+ */
500
+ async invokeCommand(commandName, args, ctx, metadata) {
501
+ const command = this.getCommand(commandName);
502
+ if (!command)
503
+ return { found: false };
504
+ const ownerExt = this.findCommandOwner(commandName);
505
+ const extensionName = ownerExt ? this.deriveExtensionName(ownerExt) : "unknown";
506
+ const startedAt = new Date();
507
+ const startPerf = performance.now();
508
+ let outcome = "ok";
509
+ let errorCode = null;
510
+ let thrown;
511
+ const argsSig = classifyArgsSignature(args);
512
+ const callerCtx = {
513
+ extensionName,
514
+ callerContext: argsSig === "no-args" ? `command:/${commandName}` : `command:/${commandName} ${argsSig}`,
515
+ isUserInitiated: true,
516
+ sessionId: metadata?.sessionId ?? null,
517
+ runId: metadata?.runId ?? null,
518
+ variant: metadata?.variant ?? null,
519
+ };
520
+ try {
521
+ await runWithExtCallerContext(callerCtx, () => command.handler(args, ctx));
522
+ }
523
+ catch (err) {
524
+ outcome = "error";
525
+ errorCode = err instanceof Error ? err.constructor.name : "unknown";
526
+ thrown = err;
527
+ this.emitError({
528
+ extensionPath: `command:${commandName}`,
529
+ event: "command",
530
+ error: err instanceof Error ? err.message : String(err),
531
+ });
532
+ }
533
+ const durationMs = Math.round(performance.now() - startPerf);
534
+ const endedAt = new Date();
535
+ try {
536
+ this.telemetrySink?.writeCommandEvent({
537
+ extensionName,
538
+ commandName,
539
+ argsSignature: argsSig,
540
+ argsLength: args.length,
541
+ outcome,
542
+ errorCode,
543
+ durationMs,
544
+ startedAt,
545
+ endedAt,
546
+ sessionId: metadata?.sessionId ?? null,
547
+ runId: metadata?.runId ?? null,
548
+ variant: metadata?.variant ?? null,
549
+ });
550
+ }
551
+ catch {
552
+ // Sink emission must never bring down command dispatch.
553
+ }
554
+ // Even though we logged the error to telemetry + emitError, callers
555
+ // expecting the thrown error (e.g. AgentSession previously re-caught it
556
+ // silently) won't see it. This matches the original behaviour: the old
557
+ // _tryExecuteExtensionCommand swallowed the error after emitError too.
558
+ void thrown;
559
+ return { found: true };
560
+ }
381
561
  /**
382
562
  * Request a graceful shutdown. Called by extension tools and event handlers.
383
563
  * The actual shutdown behavior is provided by the mode via bindExtensions().
@@ -444,7 +624,7 @@ export class ExtensionRunner {
444
624
  continue;
445
625
  for (const handler of handlers) {
446
626
  try {
447
- const handlerResult = await handler(event, ctx);
627
+ const handlerResult = await this.invokeHookHandler(ext, event.type, () => handler(event, ctx));
448
628
  if (this.isSessionBeforeEvent(event) && handlerResult) {
449
629
  result = handlerResult;
450
630
  if (result.cancel) {
@@ -476,7 +656,7 @@ export class ExtensionRunner {
476
656
  continue;
477
657
  for (const handler of handlers) {
478
658
  try {
479
- const handlerResult = (await handler(currentEvent, ctx));
659
+ const handlerResult = (await this.invokeHookHandler(ext, "tool_result", () => handler(currentEvent, ctx)));
480
660
  if (!handlerResult)
481
661
  continue;
482
662
  if (handlerResult.content !== undefined) {
@@ -521,7 +701,7 @@ export class ExtensionRunner {
521
701
  if (!handlers || handlers.length === 0)
522
702
  continue;
523
703
  for (const handler of handlers) {
524
- const handlerResult = await handler(event, ctx);
704
+ const handlerResult = await this.invokeHookHandler(ext, "tool_call", () => handler(event, ctx));
525
705
  if (handlerResult) {
526
706
  result = handlerResult;
527
707
  if (result.block) {
@@ -540,7 +720,7 @@ export class ExtensionRunner {
540
720
  continue;
541
721
  for (const handler of handlers) {
542
722
  try {
543
- const handlerResult = await handler(event, ctx);
723
+ const handlerResult = await this.invokeHookHandler(ext, "user_bash", () => handler(event, ctx));
544
724
  if (handlerResult) {
545
725
  return handlerResult;
546
726
  }
@@ -569,7 +749,7 @@ export class ExtensionRunner {
569
749
  for (const handler of handlers) {
570
750
  try {
571
751
  const event = { type: "context", messages: currentMessages };
572
- const handlerResult = await handler(event, ctx);
752
+ const handlerResult = await this.invokeHookHandler(ext, "context", () => handler(event, ctx));
573
753
  if (handlerResult && handlerResult.messages) {
574
754
  currentMessages = handlerResult.messages;
575
755
  }
@@ -605,7 +785,7 @@ export class ExtensionRunner {
605
785
  images,
606
786
  systemPrompt: currentSystemPrompt,
607
787
  };
608
- const handlerResult = await this.withTimeout(handler(event, ctx), this.beforeAgentStartTimeoutMs);
788
+ const handlerResult = await this.withTimeout(this.invokeHookHandler(ext, "before_agent_start", () => handler(event, ctx)), this.beforeAgentStartTimeoutMs);
609
789
  if (handlerResult === this.beforeAgentStartTimeoutSentinel) {
610
790
  this.reportBeforeAgentStartTimeout(ext.path);
611
791
  continue;
@@ -663,7 +843,7 @@ export class ExtensionRunner {
663
843
  for (const handler of handlers) {
664
844
  try {
665
845
  const event = { type: "resources_discover", cwd, reason };
666
- const handlerResult = await handler(event, ctx);
846
+ const handlerResult = await this.invokeHookHandler(ext, "resources_discover", () => handler(event, ctx));
667
847
  const result = handlerResult;
668
848
  if (result?.skillPaths?.length) {
669
849
  skillPaths.push(...result.skillPaths.map((path) => ({ path, extensionPath: ext.path })));
@@ -698,7 +878,7 @@ export class ExtensionRunner {
698
878
  for (const handler of ext.handlers.get("input") ?? []) {
699
879
  try {
700
880
  const event = { type: "input", text: currentText, images: currentImages, source };
701
- const result = (await handler(event, ctx));
881
+ const result = (await this.invokeHookHandler(ext, "input", () => handler(event, ctx)));
702
882
  if (result?.action === "handled")
703
883
  return result;
704
884
  if (result?.action === "transform") {
@@ -685,10 +685,17 @@ export interface MessageRenderOptions {
685
685
  expanded: boolean;
686
686
  }
687
687
  export type MessageRenderer<T = unknown> = (message: CustomMessage<T>, options: MessageRenderOptions, theme: Theme) => Component | undefined;
688
+ export interface ArgumentCompletionContext {
689
+ commandName: string;
690
+ argumentText: string;
691
+ argumentPrefix: string;
692
+ tokenIndex: number;
693
+ previousTokens: string[];
694
+ }
688
695
  export interface RegisteredCommand {
689
696
  name: string;
690
697
  description?: string;
691
- getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null;
698
+ getArgumentCompletions?: (argumentPrefix: string, context?: ArgumentCompletionContext) => AutocompleteItem[] | null;
692
699
  handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
693
700
  }
694
701
  /** Handler function type for events */
@@ -5,8 +5,19 @@
5
5
  * [HERE]: core/i18n/slash-commands.ts - English slash command translations
6
6
  */
7
7
  export declare const slashCommands: {
8
+ categories: {
9
+ core: string;
10
+ model: string;
11
+ memory: string;
12
+ session: string;
13
+ workflow: string;
14
+ agents: string;
15
+ tools: string;
16
+ admin: string;
17
+ };
8
18
  settings: string;
9
19
  model: string;
20
+ thinking: string;
10
21
  "agent-loop": string;
11
22
  "scoped-models": string;
12
23
  apikey: string;
@@ -24,6 +35,7 @@ export declare const slashCommands: {
24
35
  usage: string;
25
36
  changelog: string;
26
37
  hotkeys: string;
38
+ resources: string;
27
39
  fork: string;
28
40
  tree: string;
29
41
  login: string;
@@ -5,16 +5,27 @@
5
5
  * [HERE]: core/i18n/slash-commands.ts - English slash command translations
6
6
  */
7
7
  export const slashCommands = {
8
+ categories: {
9
+ core: "Core",
10
+ model: "Models",
11
+ memory: "Memory",
12
+ session: "Sessions",
13
+ workflow: "Workflows",
14
+ agents: "Agents",
15
+ tools: "Tools",
16
+ admin: "Admin",
17
+ },
8
18
  settings: "Open settings menu",
9
19
  model: "Select model (opens selector UI)",
10
- "agent-loop": "Set standard or weak-model-compatible agent loop for this session",
11
- "scoped-models": "Enable/disable models for Ctrl+P cycling",
20
+ thinking: "Choose reasoning depth for the current model",
21
+ "agent-loop": "Choose how the agent keeps working through a task",
22
+ "scoped-models": "Choose which models appear in quick switching",
12
23
  apikey: "Update API key for current provider",
13
24
  mcp: "Manage MCP servers (list, enable, disable)",
14
25
  soul: "Show AI personality and stats (Soul)",
15
26
  persona: "Switch AI persona/personality pack",
16
27
  memory: "Show project memory and knowledge (NanoMem)",
17
- dream: "Consolidate project memory (NanoMem)",
28
+ dream: "Refresh long-term project memory (NanoMem)",
18
29
  export: "Export session to HTML file",
19
30
  share: "Share session as a secret GitHub gist",
20
31
  copy: "Copy last agent message to clipboard",
@@ -24,6 +35,7 @@ export const slashCommands = {
24
35
  usage: "Show token usage and cost stats",
25
36
  changelog: "Show changelog entries",
26
37
  hotkeys: "Show all keyboard shortcuts",
38
+ resources: "Show loaded extensions, prompts, skills, and themes",
27
39
  fork: "Create a new fork from a previous message",
28
40
  tree: "Navigate session tree (switch branches)",
29
41
  login: "Login with OAuth provider",
@@ -34,7 +46,7 @@ export const slashCommands = {
34
46
  compact: "Manually compact the session context",
35
47
  resume: "Resume a different session",
36
48
  reload: "Reload extensions, skills, prompts, and themes",
37
- "link-world": "Install link-world for internet access (Twitter, YouTube, etc.)",
49
+ "link-world": "Set up internet access tools",
38
50
  quit: "Quit NanoPencil",
39
51
  language: "Switch language (English/Chinese)",
40
52
  };
@@ -5,8 +5,19 @@
5
5
  * [HERE]: core/i18n/slash-commands.zh.ts - Chinese slash command translations
6
6
  */
7
7
  export declare const slashCommands: {
8
+ categories: {
9
+ core: string;
10
+ model: string;
11
+ memory: string;
12
+ session: string;
13
+ workflow: string;
14
+ agents: string;
15
+ tools: string;
16
+ admin: string;
17
+ };
8
18
  settings: string;
9
19
  model: string;
20
+ thinking: string;
10
21
  "agent-loop": string;
11
22
  "scoped-models": string;
12
23
  apikey: string;
@@ -24,6 +35,7 @@ export declare const slashCommands: {
24
35
  usage: string;
25
36
  changelog: string;
26
37
  hotkeys: string;
38
+ resources: string;
27
39
  fork: string;
28
40
  tree: string;
29
41
  login: string;