@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,10 +1,23 @@
1
1
  /**
2
- * [WHO]: parseGrubCommand, buildGrubHelp
2
+ * [WHO]: parseGrubCommand, buildGrubHelp, getGrubArgumentCompletions
3
3
  * [FROM]: Depends on ./grub-types
4
4
  * [TO]: Consumed by extension entry point (./index.ts)
5
5
  * [HERE]: extensions/defaults/grub/grub-parser.ts - /grub command parser with resume/status --json/--max-iter/--max-fail flags
6
6
  */
7
7
  import { grubText } from "./grub-i18n.js";
8
+ const ROOT_COMPLETIONS = [
9
+ { value: "status", label: "status", description: "Show current progress" },
10
+ { value: "resume", label: "resume", description: "Continue a saved task" },
11
+ { value: "stop", label: "stop", description: "Stop the current task" },
12
+ { value: "help", label: "help", description: "Show usage help" },
13
+ ];
14
+ const START_FLAG_COMPLETIONS = [
15
+ { value: "--max-iter", label: "--max-iter", description: "Limit total work rounds" },
16
+ { value: "--max-fail", label: "--max-fail", description: "Stop after repeated failed rounds" },
17
+ ];
18
+ const STATUS_FLAG_COMPLETIONS = [
19
+ { value: "--json", label: "--json", description: "Show full saved details" },
20
+ ];
8
21
  function scanTokens(input) {
9
22
  const tokens = [];
10
23
  let current = "";
@@ -84,6 +97,23 @@ function tokenize(input) {
84
97
  }
85
98
  return { positional, flags };
86
99
  }
100
+ function filterCompletions(items, argumentPrefix) {
101
+ const prefix = argumentPrefix.trim().toLowerCase();
102
+ const matches = items.filter((item) => item.value.startsWith(prefix));
103
+ return matches.length > 0 ? [...matches] : null;
104
+ }
105
+ export function getGrubArgumentCompletions(argumentPrefix, context) {
106
+ if (argumentPrefix.trim().startsWith("--")) {
107
+ const firstToken = context?.previousTokens[0]?.toLowerCase();
108
+ return filterCompletions(firstToken === "status" || firstToken === "list"
109
+ ? STATUS_FLAG_COMPLETIONS
110
+ : START_FLAG_COMPLETIONS, argumentPrefix);
111
+ }
112
+ if (!context || context.tokenIndex === 0) {
113
+ return filterCompletions(ROOT_COMPLETIONS, argumentPrefix);
114
+ }
115
+ return null;
116
+ }
87
117
  function parsePositiveInt(value) {
88
118
  if (typeof value !== "string")
89
119
  return undefined;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * [WHO]: grubExtension default export - registers /grub command, dual-phase prompts, resume support, feature-list validation, and grub renderer
2
+ * [WHO]: grubExtension default export - registers /grub command, completions, dual-phase prompts, resume support, feature-list validation, and grub renderer
3
3
  * [FROM]: Depends on @pencil-agent/agent-core, @pencil-agent/tui, core/extensions/types, core/runtime/event-bus, ./grub-controller, ./grub-format, ./grub-parser, ./grub-types, ./grub-harness, ./grub-prompts, ./grub-persistence, ./grub-turn
4
4
  * [TO]: Auto-loaded by builtin-extensions.ts as a default extension
5
5
  * [HERE]: extensions/defaults/grub/index.ts - autonomous iterative task runner with Anthropic-style long-running harness (feature-list.json + durable state + phase-specialized prompts)
@@ -1,5 +1,5 @@
1
1
  /**
2
- * [WHO]: grubExtension default export - registers /grub command, dual-phase prompts, resume support, feature-list validation, and grub renderer
2
+ * [WHO]: grubExtension default export - registers /grub command, completions, dual-phase prompts, resume support, feature-list validation, and grub renderer
3
3
  * [FROM]: Depends on @pencil-agent/agent-core, @pencil-agent/tui, core/extensions/types, core/runtime/event-bus, ./grub-controller, ./grub-format, ./grub-parser, ./grub-types, ./grub-harness, ./grub-prompts, ./grub-persistence, ./grub-turn
4
4
  * [TO]: Auto-loaded by builtin-extensions.ts as a default extension
5
5
  * [HERE]: extensions/defaults/grub/index.ts - autonomous iterative task runner with Anthropic-style long-running harness (feature-list.json + durable state + phase-specialized prompts)
@@ -10,7 +10,7 @@ import { GrubController } from "./grub-controller.js";
10
10
  import { describeTaskState, formatSnapshot, formatTaskState, } from "./grub-format.js";
11
11
  import { ensureHarnessArtifacts } from "./grub-harness.js";
12
12
  import { detectGrubLocale, grubText, languageName } from "./grub-i18n.js";
13
- import { buildGrubHelp, parseGrubCommand } from "./grub-parser.js";
13
+ import { buildGrubHelp, getGrubArgumentCompletions, parseGrubCommand } from "./grub-parser.js";
14
14
  import { discoverActiveTasks, pruneStale } from "./grub-persistence.js";
15
15
  import { buildGrubCodingPrompt, buildGrubInitializerPrompt } from "./grub-prompts.js";
16
16
  import { resolveGrubTurn } from "./grub-turn.js";
@@ -288,7 +288,8 @@ export default async function grubExtension(api) {
288
288
  }
289
289
  };
290
290
  api.registerCommand("grub", {
291
- description: "Dig through one autonomous task until it is complete, blocked, stopped, or fails.",
291
+ description: "Keep working on one task until it is done, stopped, or needs your help.",
292
+ getArgumentCompletions: getGrubArgumentCompletions,
292
293
  handler: handleGrubCommand,
293
294
  });
294
295
  }
@@ -183,11 +183,11 @@ export default async function interviewExtension(api) {
183
183
  api.registerMessageRenderer(GRILL_CUSTOM_TYPE, (message, _options, theme) => renderSafeCustomMessage(message, theme));
184
184
  // Register /interview command for manual clarification
185
185
  api.registerCommand("interview", {
186
- description: "Clarify an ambiguous request and inject a refined intent.",
186
+ description: "Turn a rough request into clear next steps.",
187
187
  handler: async (args, ctx) => runInterviewCommand(api, args, ctx, "clarify"),
188
188
  });
189
189
  api.registerCommand("grill-me", {
190
- description: "Stress-test a plan or unclear request with one-question-at-a-time follow-ups and recommended answers.",
190
+ description: "Challenge a plan with focused follow-up questions.",
191
191
  handler: async (args, ctx) => runInterviewCommand(api, args, ctx, "grill"),
192
192
  });
193
193
  // Register interview tool for Agent to call when needed
@@ -19,6 +19,14 @@ const NETWORK_ROUTING_SKILL_PATH = join(__dirname, "network-routing.md");
19
19
  const WORKSPACE_TEMPLATE_PATH = join(__dirname, "agent-workspace");
20
20
  const LINK_WORLD_CUSTOM_TYPE = "link-world-install";
21
21
  const DEFAULT_TIMEOUT_SECONDS = 120;
22
+ const LINK_WORLD_COMMAND_COMPLETIONS = [
23
+ { value: "status", label: "status", description: "Show installed internet-access capabilities" },
24
+ { value: "doctor", label: "doctor", description: "Run agent-reach doctor diagnostics" },
25
+ { value: "version", label: "version", description: "Show installed agent-reach version" },
26
+ { value: "install", label: "install", description: "Show bundled installation guidance" },
27
+ { value: "workspace", label: "workspace", description: "Show the link-world workspace path" },
28
+ { value: "help", label: "help", description: "Show link-world commands" },
29
+ ];
22
30
  const LinkWorldAdminInputSchema = Type.Object({
23
31
  action: Type.Union([
24
32
  Type.Literal("status"),
@@ -325,12 +333,12 @@ export default function linkWorldExtension(api) {
325
333
  ensureLinkWorldWorkspace();
326
334
  });
327
335
  api.registerCommand("link-world", {
328
- description: "Inspect, install, or troubleshoot the built-in internet access integration",
329
- getArgumentCompletions: (argumentPrefix) => {
330
- const prefix = argumentPrefix.trim();
331
- const values = ["status", "doctor", "version", "install", "workspace", "help"]
332
- .filter((value) => value.startsWith(prefix))
333
- .map((value) => ({ value, label: value }));
336
+ description: "Set up or inspect internet access tools",
337
+ getArgumentCompletions: (argumentPrefix, context) => {
338
+ if (context && context.tokenIndex > 0)
339
+ return null;
340
+ const prefix = argumentPrefix.trim().toLowerCase();
341
+ const values = LINK_WORLD_COMMAND_COMPLETIONS.filter((item) => item.value.startsWith(prefix));
334
342
  return values.length > 0 ? values : null;
335
343
  },
336
344
  handler: async (args, _ctx) => {
@@ -233,12 +233,30 @@ export function createCronScheduler(options) {
233
233
  }, WATCH_INTERVAL_MS);
234
234
  }
235
235
  async function enable() {
236
+ if (isKilled())
237
+ return;
236
238
  enabled = true;
237
239
  if (dir) {
238
240
  await acquireLock();
241
+ if (isKilled()) {
242
+ await releaseLock();
243
+ return;
244
+ }
239
245
  await loadTasks();
246
+ if (isKilled()) {
247
+ await releaseLock();
248
+ return;
249
+ }
240
250
  startWatching();
241
251
  }
252
+ if (isKilled()) {
253
+ if (watchIntervalId) {
254
+ clearInterval(watchIntervalId);
255
+ watchIntervalId = null;
256
+ }
257
+ await releaseLock();
258
+ return;
259
+ }
242
260
  intervalId = setInterval(check, TICK_MS);
243
261
  console.log("[Cron-Scheduler] Scheduler enabled and ticking");
244
262
  }
@@ -266,6 +284,7 @@ export function createCronScheduler(options) {
266
284
  },
267
285
  stop() {
268
286
  killed = true;
287
+ enabled = false;
269
288
  if (intervalId) {
270
289
  clearInterval(intervalId);
271
290
  intervalId = null;
@@ -10,6 +10,40 @@ import { createCronScheduler, addCronTask, deleteCronTask, listCronTasks, getCro
10
10
  import { buildSchedulerHelp, parseSchedulerCommand } from "./scheduler-parser.js";
11
11
  const LOOP_CUSTOM_TYPE = "loop";
12
12
  const SCHEDULER_TICK_MS = 1000;
13
+ const LOOP_ACTION_COMPLETIONS = [
14
+ { value: "list", label: "list", description: "Show scheduled tasks" },
15
+ { value: "status", label: "status", description: "Show scheduled tasks" },
16
+ { value: "pause", label: "pause", description: "Pause a scheduled task" },
17
+ { value: "resume", label: "resume", description: "Resume a paused task" },
18
+ { value: "run", label: "run", description: "Run a scheduled task now" },
19
+ { value: "cancel", label: "cancel", description: "Cancel a scheduled task" },
20
+ { value: "clear", label: "clear", description: "Remove session-only tasks" },
21
+ { value: "every", label: "every", description: "Schedule a repeated prompt or command" },
22
+ { value: "hourly", label: "hourly", description: "Run once per hour" },
23
+ { value: "daily", label: "daily", description: "Run once per day" },
24
+ ];
25
+ const LOOP_FLAG_COMPLETIONS = [
26
+ { value: "--name", label: "--name", description: "Give the schedule a name" },
27
+ { value: "--max", label: "--max", description: "Stop after a number of runs" },
28
+ { value: "--quiet", label: "--quiet", description: "Hide per-run messages" },
29
+ { value: "--durable", label: "--durable", description: "Keep the schedule across sessions" },
30
+ { value: "-q", label: "-q", description: "Hide per-run messages" },
31
+ { value: "-d", label: "-d", description: "Keep the schedule across sessions" },
32
+ ];
33
+ function matchLoopCompletions(completions, prefix) {
34
+ const matches = completions.filter((completion) => completion.value.startsWith(prefix));
35
+ return matches.length > 0 ? matches.map((completion) => ({ ...completion })) : null;
36
+ }
37
+ function getLoopArgumentCompletions(argumentPrefix, context) {
38
+ const prefix = argumentPrefix.trim().toLowerCase();
39
+ const isFirstToken = !context || context.tokenIndex === 0;
40
+ if (!isFirstToken && !prefix.startsWith("-"))
41
+ return null;
42
+ const completions = prefix.startsWith("-")
43
+ ? LOOP_FLAG_COMPLETIONS
44
+ : [...LOOP_ACTION_COMPLETIONS, ...LOOP_FLAG_COMPLETIONS];
45
+ return matchLoopCompletions(completions, prefix);
46
+ }
13
47
  // Single unified scheduler per session
14
48
  const cronSchedulerByBus = new WeakMap();
15
49
  const apiByBus = new WeakMap();
@@ -420,6 +454,7 @@ export default async function loopExtension(api) {
420
454
  };
421
455
  api.registerCommand("loop", {
422
456
  description: "Schedule a recurring prompt or slash command for this session.",
457
+ getArgumentCompletions: getLoopArgumentCompletions,
423
458
  handler: handleLoopCommand,
424
459
  });
425
460
  }
@@ -22,9 +22,26 @@ const MCP_SKILL_PATH = join(__dirname, "mcp-management.md");
22
22
  const FIGMA_SKILL_PATH = join(__dirname, "figma-design.md");
23
23
  const FIGMA_DESKTOP_URL = "http://127.0.0.1:3845/mcp";
24
24
  const FIGMA_REMOTE_URL = "https://mcp.figma.com/mcp";
25
+ const FIGMA_COMMAND_COMPLETIONS = [
26
+ { value: "help", label: "help", description: "Show setup steps" },
27
+ { value: "auth", label: "auth", description: "Sign in to Figma" },
28
+ { value: "remote", label: "remote", description: "Enable the remote MCP path" },
29
+ { value: "setup", label: "setup", description: "Probe and enable the best Figma path" },
30
+ { value: "status", label: "status", description: "Inspect the current connection" },
31
+ { value: "logout", label: "logout", description: "Remove saved Figma credentials" },
32
+ { value: "enable", label: "enable", description: "Enable the desktop MCP preset" },
33
+ { value: "disable", label: "disable", description: "Disable the desktop MCP preset" },
34
+ ];
25
35
  function getFigmaAuthStorage() {
26
36
  return AuthStorage.create(getAuthPath());
27
37
  }
38
+ function getFigmaArgumentCompletions(argumentPrefix, context) {
39
+ if (context && context.tokenIndex > 0)
40
+ return null;
41
+ const prefix = argumentPrefix.trim().toLowerCase();
42
+ const values = FIGMA_COMMAND_COMPLETIONS.filter((item) => item.value.startsWith(prefix));
43
+ return values.length > 0 ? values.map((item) => ({ ...item })) : null;
44
+ }
28
45
  async function getFigmaAccessToken() {
29
46
  return getFigmaAuthStorage().getApiKey("figma");
30
47
  }
@@ -123,6 +140,7 @@ async function probeFigmaRemoteEndpoint(token) {
123
140
  export default async function mcpExtension(api) {
124
141
  api.registerCommand("figma", {
125
142
  description: "Connect NanoPencil to Figma for generative design",
143
+ getArgumentCompletions: getFigmaArgumentCompletions,
126
144
  handler: async (args, ctx) => {
127
145
  const [rawAction, ...rest] = args.trim().split(/\s+/).filter(Boolean);
128
146
  const action = (rawAction || "help").toLowerCase();
@@ -17,6 +17,19 @@ const PLAN_ATTACHMENT_CONFIG = {
17
17
  TURNS_BETWEEN_ATTACHMENTS: 3,
18
18
  FULL_REMINDER_EVERY_N: 3,
19
19
  };
20
+ const PLAN_ROOT_COMPLETIONS = [
21
+ { value: "open", label: "open", description: "Edit the current plan" },
22
+ { value: "exit", label: "exit", description: "Ask to leave plan mode" },
23
+ { value: "validate", label: "validate", description: "Check the current plan" },
24
+ { value: "approve", label: "approve", description: "Approve a teammate plan" },
25
+ ];
26
+ function getPlanArgumentCompletions(argumentPrefix, context) {
27
+ if (context && context.tokenIndex > 0)
28
+ return null;
29
+ const prefix = argumentPrefix.trim().toLowerCase();
30
+ const values = PLAN_ROOT_COMPLETIONS.filter((value) => value.value.startsWith(prefix));
31
+ return values.length > 0 ? values.map((value) => ({ ...value })) : null;
32
+ }
20
33
  function countHumanTurns(ctx) {
21
34
  return ctx.sessionManager.getBranch().filter((entry) => {
22
35
  if (entry.type !== "message")
@@ -169,17 +182,6 @@ export default async function planExtension(api) {
169
182
  // Display plan content
170
183
  displayPlan(api, ctx);
171
184
  };
172
- api.registerCommand("plan", {
173
- description: "Enable plan mode or view the current session plan",
174
- getArgumentCompletions: (argumentPrefix) => {
175
- const prefix = argumentPrefix.trim();
176
- const values = ["open", "exit"]
177
- .filter((value) => value.startsWith(prefix))
178
- .map((value) => ({ value, label: value }));
179
- return values.length > 0 ? values : null;
180
- },
181
- handler: handlePlanCommand,
182
- });
183
185
  // =========================================================================
184
186
  // /plan:validate command - validate current plan structure
185
187
  // =========================================================================
@@ -259,6 +261,22 @@ export default async function planExtension(api) {
259
261
  description: "Approve a teammate's plan (for team leaders)",
260
262
  handler: handlePlanApproveCommand,
261
263
  });
264
+ api.registerCommand("plan", {
265
+ description: "Plan, edit, validate, approve, or exit the current session plan",
266
+ getArgumentCompletions: getPlanArgumentCompletions,
267
+ handler: async (args, ctx) => {
268
+ const [action, ...rest] = args.trim().split(/\s+/);
269
+ if (action === "validate") {
270
+ await handlePlanValidateCommand("", ctx);
271
+ return;
272
+ }
273
+ if (action === "approve") {
274
+ await handlePlanApproveCommand(rest.join(" "), ctx);
275
+ return;
276
+ }
277
+ await handlePlanCommand(args, ctx);
278
+ },
279
+ });
262
280
  // =========================================================================
263
281
  // Permission gating: block write tools in plan mode
264
282
  // =========================================================================
@@ -2,15 +2,22 @@
2
2
  * [WHO]: Extension interface, AI-driven personalized greetings and idle cues
3
3
  * [FROM]: Depends on @pencil-agent/tui, core/extensions/types.js, core/i18n, node:path, node:url, node:fs
4
4
  * [TO]: Loaded by core/extensions/loader.ts as extension entry point
5
- * [HERE]: extensions/defaults/presence/index.ts - AI-generated opening + idle presence lines from memory (episodes/preferences/lessons) + git snapshot (branch/last commit/changed files) + soul personality traits, injects last MAX_RECENT_PRESENCE lines into agent systemPrompt per turn, configurable via settings.presence.enabled, canSendOpening guards against agent-is-busy race
5
+ * [HERE]: extensions/defaults/presence/index.ts - AI-generated opening + idle presence lines from memory (episodes/preferences/lessons) + git snapshot (branch/last commit/changed files) + soul personality traits and identity/style preferences, injects last MAX_RECENT_PRESENCE lines into agent systemPrompt per turn, configurable via settings.presence.enabled, canSendOpening guards against agent-is-busy race
6
6
  */
7
7
  import type { ExtensionAPI } from "../../../core/extensions/types.js";
8
- import { detectLanguageFromMemory } from "./presence-memory.js";
8
+ import { collectIdentityPreferenceHighlights, detectLanguageFromMemory } from "./presence-memory.js";
9
9
  declare function getFallbackOpeningLines(locale?: "en" | "zh"): string[];
10
10
  declare function getFallbackIdleLines(locale?: "en" | "zh"): string[];
11
11
  declare function resolveBundledPackageEntry(packageName: "mem-core" | "soul-core"): string | undefined;
12
12
  declare function getOpeningDelayMs(): number;
13
13
  declare function importRuntimeModule<T>(moduleNames: string[], bundledPackageName?: "mem-core" | "soul-core"): Promise<T | undefined>;
14
+ type PresenceSoulHints = {
15
+ traits: string[];
16
+ tone?: string;
17
+ identityPreferences: string[];
18
+ };
19
+ declare function collectSoulHints(soulManager: unknown): PresenceSoulHints;
20
+ declare function buildPresenceSystemPrompt(locale: "en" | "zh", soulHints: PresenceSoulHints, kind: "opening" | "idle"): string;
14
21
  export default function presenceExtension(api: ExtensionAPI): Promise<void>;
15
22
  export declare const __testUtils: {
16
23
  getFallbackOpeningLines: typeof getFallbackOpeningLines;
@@ -18,6 +25,9 @@ export declare const __testUtils: {
18
25
  resolveBundledPackageEntry: typeof resolveBundledPackageEntry;
19
26
  importRuntimeModule: typeof importRuntimeModule;
20
27
  detectLanguageFromMemory: typeof detectLanguageFromMemory;
28
+ collectIdentityPreferenceHighlights: typeof collectIdentityPreferenceHighlights;
21
29
  getOpeningDelayMs: typeof getOpeningDelayMs;
30
+ collectSoulHints: typeof collectSoulHints;
31
+ buildPresenceSystemPrompt: typeof buildPresenceSystemPrompt;
22
32
  };
23
33
  export {};
@@ -2,7 +2,7 @@
2
2
  * [WHO]: Extension interface, AI-driven personalized greetings and idle cues
3
3
  * [FROM]: Depends on @pencil-agent/tui, core/extensions/types.js, core/i18n, node:path, node:url, node:fs
4
4
  * [TO]: Loaded by core/extensions/loader.ts as extension entry point
5
- * [HERE]: extensions/defaults/presence/index.ts - AI-generated opening + idle presence lines from memory (episodes/preferences/lessons) + git snapshot (branch/last commit/changed files) + soul personality traits, injects last MAX_RECENT_PRESENCE lines into agent systemPrompt per turn, configurable via settings.presence.enabled, canSendOpening guards against agent-is-busy race
5
+ * [HERE]: extensions/defaults/presence/index.ts - AI-generated opening + idle presence lines from memory (episodes/preferences/lessons) + git snapshot (branch/last commit/changed files) + soul personality traits and identity/style preferences, injects last MAX_RECENT_PRESENCE lines into agent systemPrompt per turn, configurable via settings.presence.enabled, canSendOpening guards against agent-is-busy race
6
6
  */
7
7
  import { Box, Container, Spacer, Text } from "@pencil-agent/tui";
8
8
  import { getLocale, tValue } from "../../../core/i18n/index.js";
@@ -11,7 +11,7 @@ import { execFile } from "node:child_process";
11
11
  import { promisify } from "node:util";
12
12
  import { existsSync, realpathSync } from "node:fs";
13
13
  import { fileURLToPath, pathToFileURL } from "node:url";
14
- import { collectMemoryHighlights, detectLanguageFromMemory, getMemoryDir, getProject, } from "./presence-memory.js";
14
+ import { collectIdentityPreferenceHighlights, collectMemoryHighlights, detectLanguageFromMemory, getMemoryDir, getProject, } from "./presence-memory.js";
15
15
  const execFileAsync = promisify(execFile);
16
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
17
  const PRESENCE_MESSAGE_TYPE = "presence";
@@ -125,8 +125,14 @@ async function importRuntimeModule(moduleNames, bundledPackageName) {
125
125
  }
126
126
  return undefined;
127
127
  }
128
+ function normalizePreferenceText(value) {
129
+ if (typeof value !== "string")
130
+ return undefined;
131
+ const text = value.trim().replace(/\s+/g, " ");
132
+ return text ? text.slice(0, 160) : undefined;
133
+ }
128
134
  function collectSoulHints(soulManager) {
129
- const out = { traits: [] };
135
+ const out = { traits: [], identityPreferences: [] };
130
136
  if (!soulManager || typeof soulManager !== "object")
131
137
  return out;
132
138
  try {
@@ -143,12 +149,38 @@ function collectSoulHints(soulManager) {
143
149
  const mood = profile?.emotionalState?.mood;
144
150
  if (typeof mood === "string")
145
151
  out.tone = mood;
152
+ const knownPreferences = profile?.userRelationship?.knownPreferences;
153
+ if (Array.isArray(knownPreferences)) {
154
+ out.identityPreferences = knownPreferences
155
+ .map(normalizePreferenceText)
156
+ .filter((text) => Boolean(text))
157
+ .slice(0, 5);
158
+ }
146
159
  }
147
160
  catch {
148
161
  /* fail-soft */
149
162
  }
150
163
  return out;
151
164
  }
165
+ function mergeIdentityPreferences(...groups) {
166
+ const seen = new Set();
167
+ const out = [];
168
+ for (const group of groups) {
169
+ for (const value of group) {
170
+ const text = normalizePreferenceText(value);
171
+ if (!text)
172
+ continue;
173
+ const key = text.toLowerCase();
174
+ if (seen.has(key))
175
+ continue;
176
+ seen.add(key);
177
+ out.push(text);
178
+ if (out.length >= 5)
179
+ return out;
180
+ }
181
+ }
182
+ return out;
183
+ }
152
184
  function touch(state) {
153
185
  state.lastActivityAt = Date.now();
154
186
  state.idleReminderSent = false;
@@ -257,12 +289,12 @@ async function buildGreetingPrompt(state, detectedLocale, soulHints, kind = "ope
257
289
  : "用户安静了几分钟。轻轻问候一下,别打扰他。一句话就够。",
258
290
  "",
259
291
  "要求:",
260
- "- 像老朋友一样自然,不要太正式",
261
- "- 简短随意",
292
+ "- 简短自然,不要太正式",
293
+ "- 如果有身份、语气、称呼或角色偏好,必须遵守",
262
294
  kind === "idle" ? "- 不要重复你之前说过的开场白" : "- 如果有上下文,可以自然提一句",
263
295
  "- 不要为了有话说而硬找话题",
264
296
  "- 如果他只是在做很琐碎的事情,不需要特别提起,简单打个招呼就好",
265
- "- 偏好和经验只是背景参考,不需要每次都引用,只在真正自然的时候带一句",
297
+ "- 普通偏好和经验只是背景参考;身份、语气、称呼和角色偏好不是背景,必须优先遵守",
266
298
  "- 不要反复提同样的记忆或概念,要换着花样",
267
299
  "",
268
300
  "项目状态:",
@@ -283,6 +315,11 @@ async function buildGreetingPrompt(state, detectedLocale, soulHints, kind = "ope
283
315
  for (const p of highlights.preferences)
284
316
  lines.push(`- ${p}`);
285
317
  }
318
+ if (soulHints.identityPreferences.length > 0) {
319
+ lines.push("", "高优先级身份/语气/称呼约束:");
320
+ for (const p of soulHints.identityPreferences)
321
+ lines.push(`- ${p}`);
322
+ }
286
323
  if (highlights.lessons.length > 0) {
287
324
  lines.push("", "记下的经验:");
288
325
  for (const l of highlights.lessons)
@@ -314,12 +351,12 @@ async function buildGreetingPrompt(state, detectedLocale, soulHints, kind = "ope
314
351
  : "The user has been quiet for a few minutes. Drop a soft, non-pushy check-in. One short sentence.",
315
352
  "",
316
353
  "Requirements:",
317
- "- Sound like a friend, not formal",
318
- "- Short and casual",
354
+ "- Keep it short and natural, not formal",
355
+ "- If identity, tone, address, or role preferences are present, follow them",
319
356
  kind === "idle" ? "- Do NOT repeat your earlier opening greeting" : "- If there's recent context, mention it naturally",
320
357
  "- Don't force a topic just to have something to say",
321
358
  "- If they're only doing trivial things (like creating an empty folder), don't mention it - just say hi",
322
- "- Preferences and lessons are background context only - reference at most 1-2, only when it naturally fits",
359
+ "- Ordinary preferences and lessons are background context; identity, tone, address, and role preferences are high-priority constraints",
323
360
  "- Don't keep bringing up the same memories or concepts repeatedly",
324
361
  "",
325
362
  "Project state:",
@@ -340,6 +377,11 @@ async function buildGreetingPrompt(state, detectedLocale, soulHints, kind = "ope
340
377
  for (const p of highlights.preferences)
341
378
  lines.push(`- ${p}`);
342
379
  }
380
+ if (soulHints.identityPreferences.length > 0) {
381
+ lines.push("", "High-priority identity/tone/address constraints:");
382
+ for (const p of soulHints.identityPreferences)
383
+ lines.push(`- ${p}`);
384
+ }
343
385
  if (highlights.lessons.length > 0) {
344
386
  lines.push("", "Lessons remembered:");
345
387
  for (const l of highlights.lessons)
@@ -369,6 +411,22 @@ async function buildGreetingPrompt(state, detectedLocale, soulHints, kind = "ope
369
411
  return undefined;
370
412
  }
371
413
  }
414
+ function buildPresenceSystemPrompt(locale, soulHints, kind) {
415
+ const traitsHint = soulHints.traits.length > 0
416
+ ? ` ${locale === "zh" ? "人格倾向" : "Personality tilt"}: ${soulHints.traits.map((t) => t.split(":")[0]).join(", ")}.`
417
+ : "";
418
+ const identityHint = soulHints.identityPreferences.length > 0
419
+ ? ` ${locale === "zh" ? "必须遵守这些身份/语气/称呼约束" : "Follow these identity/tone/address constraints"}: ${soulHints.identityPreferences.join(" | ")}.`
420
+ : "";
421
+ if (locale === "zh") {
422
+ return kind === "opening"
423
+ ? `生成一句简短自然的开场问候。不要覆盖已知身份设定。${identityHint}${traitsHint}`
424
+ : `生成一句简短、轻声、不打扰的问候。不要覆盖已知身份设定。${identityHint}${traitsHint}`;
425
+ }
426
+ return kind === "opening"
427
+ ? `Generate one brief, natural opening greeting. Do not override known identity settings.${identityHint}${traitsHint}`
428
+ : `Generate one brief, quiet, non-pushy check-in. Do not override known identity settings.${identityHint}${traitsHint}`;
429
+ }
372
430
  function getLastUserMessage(ctx) {
373
431
  const entries = ctx.sessionManager.getEntries();
374
432
  for (let i = entries.length - 1; i >= 0; i -= 1) {
@@ -401,22 +459,15 @@ async function generatePresenceLine(api, ctx, state, kind) {
401
459
  return fallback();
402
460
  const lastUser = kind === "idle" ? getLastUserMessage(ctx) : undefined;
403
461
  const soulHints = collectSoulHints(ctx.getSoulManager());
404
- const prompt = await buildGreetingPrompt(state, locale, soulHints, kind, lastUser);
462
+ const memoryIdentityPreferences = await collectIdentityPreferenceHighlights(state);
463
+ const presenceHints = {
464
+ ...soulHints,
465
+ identityPreferences: mergeIdentityPreferences(soulHints.identityPreferences, memoryIdentityPreferences),
466
+ };
467
+ const prompt = await buildGreetingPrompt(state, locale, presenceHints, kind, lastUser);
405
468
  if (!prompt)
406
469
  return fallback();
407
- const systemPrompt = (() => {
408
- const traitsHint = soulHints.traits.length > 0
409
- ? ` Your key traits: ${soulHints.traits.map((t) => t.split(":")[0]).join(", ")}.`
410
- : "";
411
- if (locale === "zh") {
412
- return kind === "opening"
413
- ? `你是个程序员的好朋友,现在来打个招呼。说得随意自然点。${traitsHint}`
414
- : `你是个程序员的好朋友。轻声问候,不打扰。一句话。${traitsHint}`;
415
- }
416
- return kind === "opening"
417
- ? `You're a developer's coding buddy saying hi. Keep it casual and human.${traitsHint}`
418
- : `You're a developer's coding buddy doing a quiet check-in. One short, non-pushy line.${traitsHint}`;
419
- })();
470
+ const systemPrompt = buildPresenceSystemPrompt(locale, presenceHints, kind);
420
471
  try {
421
472
  const line = await ctx.completeSimple(systemPrompt, prompt);
422
473
  if (line && line.trim().length > 0 && line.trim().length < 200) {
@@ -650,5 +701,8 @@ export const __testUtils = {
650
701
  resolveBundledPackageEntry,
651
702
  importRuntimeModule,
652
703
  detectLanguageFromMemory,
704
+ collectIdentityPreferenceHighlights,
653
705
  getOpeningDelayMs,
706
+ collectSoulHints,
707
+ buildPresenceSystemPrompt,
654
708
  };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * [WHO]: PresenceMemoryEngine type, getMemoryDir(), getProject(), detectLanguageFromMemory(), collectMemoryHighlights()
2
+ * [WHO]: PresenceMemoryEngine type, getMemoryDir(), getProject(), detectLanguageFromMemory(), collectMemoryHighlights(), collectIdentityPreferenceHighlights()
3
3
  * [FROM]: Depends on node:os, node:path, node:fs for memory path discovery
4
4
  * [TO]: Consumed by extensions/defaults/presence/index.ts and presence tests
5
5
  * [HERE]: extensions/defaults/presence/presence-memory.ts - memory-derived locale and highlight selection for presence prompts
@@ -43,4 +43,5 @@ export declare function getMemoryDir(): string;
43
43
  export declare function getProject(): string;
44
44
  export declare function detectLanguageFromMemory(state: PresenceMemoryState): Promise<"en" | "zh" | undefined>;
45
45
  export declare function collectMemoryHighlights(state: PresenceMemoryState): Promise<MemoryHighlights>;
46
+ export declare function collectIdentityPreferenceHighlights(state: PresenceMemoryState): Promise<string[]>;
46
47
  export {};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * [WHO]: PresenceMemoryEngine type, getMemoryDir(), getProject(), detectLanguageFromMemory(), collectMemoryHighlights()
2
+ * [WHO]: PresenceMemoryEngine type, getMemoryDir(), getProject(), detectLanguageFromMemory(), collectMemoryHighlights(), collectIdentityPreferenceHighlights()
3
3
  * [FROM]: Depends on node:os, node:path, node:fs for memory path discovery
4
4
  * [TO]: Consumed by extensions/defaults/presence/index.ts and presence tests
5
5
  * [HERE]: extensions/defaults/presence/presence-memory.ts - memory-derived locale and highlight selection for presence prompts
@@ -7,6 +7,7 @@
7
7
  import { existsSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
9
  import { join } from "node:path";
10
+ const IDENTITY_PREFERENCE_PATTERN = /(tone|style|speaking|speak|call(?:s|ed)?\s+(?:me|user|them)?|address|persona|role|identity|character|扮演|角色|人设|身份|语气|口吻|说话方式|称呼|叫我|雷姆|rem-like|rem\b)/i;
10
11
  export function getMemoryDir() {
11
12
  // Use the same memory directory as the main app.
12
13
  // Priority: env var > nanopencil default > legacy nanomem path.
@@ -152,6 +153,41 @@ export async function collectMemoryHighlights(state) {
152
153
  }
153
154
  return out;
154
155
  }
156
+ export async function collectIdentityPreferenceHighlights(state) {
157
+ if (!state.memEngine)
158
+ return [];
159
+ try {
160
+ const entries = await state.memEngine.getAllEntries();
161
+ const prefPool = [
162
+ ...(entries.preferences ?? []),
163
+ ...entries.knowledge.filter((entry) => entry.type === "preference" || entry.tags.includes("preference")),
164
+ ...entries.lessons.filter((entry) => entry.type === "preference" || entry.tags.includes("preference")),
165
+ ].filter((entry) => entry.type === "preference" || entry.tags.includes("preference"));
166
+ const searchResults = await state.memEngine.searchEntries("tone style speaking call address persona role identity 称呼 语气 角色 扮演");
167
+ const candidates = [...prefPool, ...searchResults];
168
+ const seen = new Set();
169
+ const out = [];
170
+ for (const entry of candidates) {
171
+ const text = (entry.summary || entry.detail || entry.content || "").toString().trim().replace(/\s+/g, " ");
172
+ const label = (entry.name || "preference").toString().trim();
173
+ const searchable = `${label} ${text} ${(entry.tags || []).join(" ")}`;
174
+ if (!text || !IDENTITY_PREFERENCE_PATTERN.test(searchable))
175
+ continue;
176
+ const line = `${label || "preference"}: ${text.slice(0, 160)}`;
177
+ const key = line.toLowerCase();
178
+ if (seen.has(key))
179
+ continue;
180
+ seen.add(key);
181
+ out.push(line);
182
+ if (out.length >= 5)
183
+ break;
184
+ }
185
+ return out;
186
+ }
187
+ catch {
188
+ return [];
189
+ }
190
+ }
155
191
  /** Randomly pick `count` items from array without replacement. */
156
192
  function shufflePick(arr, count) {
157
193
  if (count <= 0 || arr.length === 0)
@@ -3,6 +3,17 @@ import { createRecapRenderer } from "./recap-renderer.js";
3
3
  import { hasMeaningfulActivity, synthesizeSmartRecap } from "./recap-synthesizer.js";
4
4
  import { RECAP_DEFAULTS, RECAP_MESSAGE_TYPE } from "./recap-types.js";
5
5
  const RECAP_TIMEOUT_MS = 30_000;
6
+ const RECAP_MODE_COMPLETIONS = [
7
+ { value: "--free", label: "--free", description: "Use the instant zero-token recap" },
8
+ { value: "--smart", label: "--smart", description: "Use an LLM-polished recap" },
9
+ ];
10
+ function getRecapArgumentCompletions(argumentPrefix, context) {
11
+ if (context && context.tokenIndex > 0)
12
+ return null;
13
+ const prefix = argumentPrefix.trim().toLowerCase();
14
+ const values = RECAP_MODE_COMPLETIONS.filter((item) => item.value.startsWith(prefix));
15
+ return values.length > 0 ? values.map((item) => ({ ...item })) : null;
16
+ }
6
17
  function emitFreeRecap(ctx, api) {
7
18
  const entries = ctx.sessionManager.getEntries();
8
19
  const recap = extractFreeRecap(entries);
@@ -99,6 +110,7 @@ export default async function recapExtension(api) {
99
110
  api.registerMessageRenderer(RECAP_MESSAGE_TYPE, createRecapRenderer());
100
111
  api.registerCommand("recap", {
101
112
  description: "Show a brief situational recap of the current task (goal, key facts, next decision). Free by default; add --smart for LLM-polished synthesis (costs tokens).",
113
+ getArgumentCompletions: getRecapArgumentCompletions,
102
114
  handler: (args, ctx) => handleRecapCommand(args, ctx, api),
103
115
  });
104
116
  }