@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.
- package/dist/build-meta.json +3 -3
- package/dist/builtin-extensions.d.ts +4 -0
- package/dist/builtin-extensions.js +16 -16
- package/dist/core/config/settings-manager.d.ts +8 -1
- package/dist/core/config/settings-manager.js +9 -0
- package/dist/core/extensions/runner.d.ts +70 -4
- package/dist/core/extensions/runner.js +188 -8
- package/dist/core/extensions/types.d.ts +8 -1
- package/dist/core/i18n/slash-commands.d.ts +12 -0
- package/dist/core/i18n/slash-commands.js +16 -4
- package/dist/core/i18n/slash-commands.zh.d.ts +12 -0
- package/dist/core/i18n/slash-commands.zh.js +16 -4
- package/dist/core/runtime/agent-session.d.ts +5 -0
- package/dist/core/runtime/agent-session.js +85 -27
- package/dist/core/runtime/extension-core-bindings.d.ts +3 -3
- package/dist/core/runtime/extension-core-bindings.js +73 -20
- package/dist/core/runtime/retry-coordinator.d.ts +10 -1
- package/dist/core/runtime/retry-coordinator.js +20 -4
- package/dist/core/runtime/sdk.js +2 -1
- package/dist/core/runtime/slash-command-catalog.d.ts +2 -1
- package/dist/core/runtime/slash-command-catalog.js +9 -1
- package/dist/core/slash-commands.d.ts +12 -1
- package/dist/core/slash-commands.js +81 -32
- package/dist/core/telemetry/batching-dispatcher.d.ts +41 -0
- package/dist/core/telemetry/batching-dispatcher.js +89 -0
- package/dist/core/telemetry/build-meta.d.ts +12 -0
- package/dist/core/telemetry/build-meta.js +57 -0
- package/dist/core/telemetry/caller-context.d.ts +32 -0
- package/dist/core/telemetry/caller-context.js +19 -0
- package/dist/core/telemetry/credentials.d.ts +27 -0
- package/dist/core/telemetry/credentials.js +87 -0
- package/dist/core/telemetry/ext-events.d.ts +89 -0
- package/dist/core/telemetry/ext-events.js +189 -0
- package/dist/core/telemetry/index.d.ts +13 -0
- package/dist/core/telemetry/index.js +6 -0
- package/dist/core/telemetry/insforge-base.d.ts +37 -0
- package/dist/core/telemetry/insforge-base.js +160 -0
- package/dist/core/telemetry/types.d.ts +33 -0
- package/dist/core/telemetry/types.js +7 -0
- package/dist/extensions/defaults/browser/index.js +14 -6
- package/dist/extensions/defaults/btw/index.js +2 -2
- package/dist/extensions/defaults/debug/index.js +38 -3
- package/dist/extensions/defaults/diagnostics/index.js +12 -0
- package/dist/extensions/defaults/grub/grub-parser.d.ts +15 -1
- package/dist/extensions/defaults/grub/grub-parser.js +31 -1
- package/dist/extensions/defaults/grub/index.d.ts +1 -1
- package/dist/extensions/defaults/grub/index.js +4 -3
- package/dist/extensions/defaults/interview/index.js +2 -2
- package/dist/extensions/defaults/link-world/index.js +14 -6
- package/dist/extensions/defaults/loop/cron/cron-scheduler.js +19 -0
- package/dist/extensions/defaults/loop/index.js +35 -0
- package/dist/extensions/defaults/mcp/index.js +18 -0
- package/dist/extensions/defaults/plan/index.js +29 -11
- package/dist/extensions/defaults/presence/index.d.ts +12 -2
- package/dist/extensions/defaults/presence/index.js +77 -23
- package/dist/extensions/defaults/presence/presence-memory.d.ts +2 -1
- package/dist/extensions/defaults/presence/presence-memory.js +37 -1
- package/dist/extensions/defaults/recap/index.js +12 -0
- package/dist/extensions/defaults/sal/eval/insforge-sink.d.ts +6 -17
- package/dist/extensions/defaults/sal/eval/insforge-sink.js +40 -183
- package/dist/extensions/defaults/sal/index.js +31 -8
- package/dist/extensions/defaults/sal/sal-config.d.ts +5 -0
- package/dist/extensions/defaults/sal/sal-config.js +15 -82
- package/dist/extensions/defaults/security-audit/index.js +141 -83
- package/dist/extensions/defaults/subagent/index.js +29 -5
- package/dist/extensions/defaults/team/index.js +10 -9
- package/dist/extensions/defaults/team/team-parser.d.ts +18 -0
- package/dist/extensions/defaults/team/team-parser.js +91 -3
- package/dist/extensions/defaults/team/team-ui.js +4 -14
- package/dist/extensions/defaults/token-save/index.js +14 -1
- package/dist/extensions/optional/export-html/index.js +19 -5
- package/dist/extensions/optional/simplify/index.js +11 -5
- package/dist/modes/interactive/interactive-mode.d.ts +2 -1
- package/dist/modes/interactive/interactive-mode.js +68 -19
- package/dist/modes/interactive/slash-command-arguments.d.ts +16 -0
- package/dist/modes/interactive/slash-command-arguments.js +97 -0
- package/dist/modes/rpc/rpc-mode.d.ts +3 -0
- package/dist/modes/rpc/rpc-mode.js +40 -31
- package/dist/modes/rpc/rpc-types.d.ts +3 -0
- package/dist/node_modules/@pencil-agent/agent-core/agent.d.ts +15 -1
- package/dist/node_modules/@pencil-agent/agent-core/agent.js +13 -1
- package/dist/node_modules/@pencil-agent/agent-core/index.d.ts +2 -1
- package/dist/node_modules/@pencil-agent/agent-core/index.js +2 -1
- package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.d.ts +1 -1
- package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.js +293 -20
- package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-streaming-tool-executor.d.ts +33 -0
- package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-streaming-tool-executor.js +189 -0
- package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-tool-orchestration.d.ts +9 -0
- package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-tool-orchestration.js +30 -5
- package/dist/node_modules/@pencil-agent/agent-core/types.d.ts +90 -3
- package/dist/node_modules/@pencil-agent/agent-core/types.js +1 -1
- package/dist/node_modules/@pencil-agent/ai/models.generated.d.ts +0 -17
- package/dist/node_modules/@pencil-agent/ai/models.generated.js +29 -46
- package/dist/node_modules/@pencil-agent/tui/autocomplete.d.ts +8 -1
- package/dist/node_modules/@pencil-agent/tui/autocomplete.js +18 -2
- package/dist/node_modules/@pencil-agent/tui/index.d.ts +1 -1
- package/dist/node_modules/@pencil-agent/tui/tui.d.ts +8 -3
- package/dist/node_modules/@pencil-agent/tui/tui.js +68 -33
- package/dist/packages/mem-core/extension.js +154 -83
- package/dist/packages/mem-core/full-insights-sections.d.ts +48 -0
- package/dist/packages/mem-core/full-insights-sections.js +231 -0
- package/dist/packages/mem-core/full-insights.d.ts +1 -1
- package/dist/packages/mem-core/full-insights.js +102 -42
- 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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
329
|
-
getArgumentCompletions: (argumentPrefix) => {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
"-
|
|
318
|
-
"-
|
|
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
|
-
"-
|
|
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
|
|
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
|
}
|