@pencil-agent/nano-pencil 1.14.2 → 1.14.3
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/i18n/slash-commands.d.ts +11 -0
- package/dist/core/i18n/slash-commands.js +15 -4
- package/dist/core/i18n/slash-commands.zh.d.ts +11 -0
- package/dist/core/i18n/slash-commands.zh.js +15 -4
- 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 +9 -1
- package/dist/core/slash-commands.js +77 -32
- package/dist/extensions/defaults/browser/index.js +1 -1
- package/dist/extensions/defaults/debug/index.js +28 -3
- package/dist/extensions/defaults/link-world/index.js +1 -1
- package/dist/extensions/defaults/loop/cron/cron-scheduler.js +19 -0
- package/dist/extensions/defaults/security-audit/index.js +4 -4
- package/dist/extensions/defaults/team/team-ui.js +4 -14
- package/dist/extensions/defaults/token-save/index.js +8 -1
- package/dist/modes/interactive/interactive-mode.js +5 -15
- package/dist/node_modules/@pencil-agent/ai/models.generated.js +14 -14
- package/dist/node_modules/@pencil-agent/tui/autocomplete.js +7 -2
- package/dist/node_modules/@pencil-agent/tui/tui.d.ts +8 -3
- package/dist/node_modules/@pencil-agent/tui/tui.js +63 -31
- package/dist/packages/mem-core/extension.js +150 -83
- package/package.json +2 -2
|
@@ -10,11 +10,11 @@ import { Type } from "@sinclair/typebox";
|
|
|
10
10
|
import { SessionManager } from "@pencil-agent/nano-pencil";
|
|
11
11
|
import { NanoMemEngine } from "./engine.js";
|
|
12
12
|
import { reportDiagnostic } from "./diagnostics.js";
|
|
13
|
-
import { readDreamLockMtimeMs, rollbackDreamLock,
|
|
13
|
+
import { readDreamLockMtimeMs, rollbackDreamLock, tryAcquireDreamLock } from "./dream-lock.js";
|
|
14
14
|
import { renderFullInsightsHtml } from "./full-insights-html.js";
|
|
15
15
|
import { hasParseableLlmJson } from "./llm-json.js";
|
|
16
16
|
import { extractTags } from "./scoring.js";
|
|
17
|
-
import {
|
|
17
|
+
import { loadMeta } from "./store.js";
|
|
18
18
|
const extractedItemSchema = Type.Object({
|
|
19
19
|
type: Type.Union([
|
|
20
20
|
Type.Literal("preference"),
|
|
@@ -66,6 +66,9 @@ const memoryJsonContracts = {
|
|
|
66
66
|
schema: Type.Object({ recommendations: Type.Array(Type.String()) }),
|
|
67
67
|
},
|
|
68
68
|
};
|
|
69
|
+
const dreamCommands = ["run", "status", "stop"];
|
|
70
|
+
const memoryEditFields = ["summary", "detail", "content", "salience", "ttl", "retention"];
|
|
71
|
+
const memoryResolveActions = ["merge", "demote", "forget", "mark-situational"];
|
|
69
72
|
function withTimeout(promise, timeoutMs) {
|
|
70
73
|
return new Promise((resolve) => {
|
|
71
74
|
const timer = setTimeout(() => resolve(undefined), timeoutMs);
|
|
@@ -314,25 +317,51 @@ async function readMetaLastConsolidationMs(memoryDir) {
|
|
|
314
317
|
return 0;
|
|
315
318
|
}
|
|
316
319
|
}
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return
|
|
320
|
+
function formatElapsed(ms) {
|
|
321
|
+
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
322
|
+
if (seconds < 60)
|
|
323
|
+
return `${seconds}s`;
|
|
324
|
+
const minutes = Math.floor(seconds / 60);
|
|
325
|
+
const remainder = seconds % 60;
|
|
326
|
+
return remainder > 0 ? `${minutes}m ${remainder}s` : `${minutes}m`;
|
|
327
|
+
}
|
|
328
|
+
function describeDreamSource(source) {
|
|
329
|
+
return source === "auto" ? "automatic" : source === "manual" ? "manual" : "not started";
|
|
324
330
|
}
|
|
325
|
-
function
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
331
|
+
function formatDreamStatus(task, memoryDir) {
|
|
332
|
+
const lines = [`Memory refresh: ${task.status}`];
|
|
333
|
+
lines.push(`Mode: ${describeDreamSource(task.source)}`);
|
|
334
|
+
if (task.startedAtMs) {
|
|
335
|
+
const endedAt = task.endedAtMs ?? Date.now();
|
|
336
|
+
lines.push(`${task.status === "running" ? "Running for" : "Duration"}: ${formatElapsed(endedAt - task.startedAtMs)}`);
|
|
329
337
|
}
|
|
330
|
-
|
|
331
|
-
`
|
|
332
|
-
|
|
333
|
-
`
|
|
334
|
-
`
|
|
335
|
-
|
|
338
|
+
if (task.sessionsReviewing)
|
|
339
|
+
lines.push(`Reviewing: ${task.sessionsReviewing} past sessions`);
|
|
340
|
+
if (task.result) {
|
|
341
|
+
lines.push(`Reviewed: ${task.result.episodesConsidered} past sessions`);
|
|
342
|
+
lines.push(`Memories: ${task.result.added} added, ${task.result.updated} updated, ${task.result.skipped} skipped`);
|
|
343
|
+
}
|
|
344
|
+
if (task.status === "running")
|
|
345
|
+
lines.push("Stop: /dream stop");
|
|
346
|
+
if (task.lastError)
|
|
347
|
+
lines.push(`Needs attention: ${task.lastError}`);
|
|
348
|
+
lines.push(`Saved in: ${memoryDir}`);
|
|
349
|
+
return lines.join("\n");
|
|
350
|
+
}
|
|
351
|
+
function formatDreamResult(result, memoryDir) {
|
|
352
|
+
const sample = result.entries
|
|
353
|
+
.slice(0, 5)
|
|
354
|
+
.map((entry) => `- [${entry.type}] ${entry.name || entry.summary || entry.id}`)
|
|
355
|
+
.join("\n");
|
|
356
|
+
const lines = [
|
|
357
|
+
"Memory refreshed.",
|
|
358
|
+
`Reviewed: ${result.stats.episodesConsidered} past sessions`,
|
|
359
|
+
`Memories: ${result.stats.added} added, ${result.stats.updated} updated, ${result.stats.skipped} skipped`,
|
|
360
|
+
`Saved in: ${memoryDir}`,
|
|
361
|
+
];
|
|
362
|
+
if (sample)
|
|
363
|
+
lines.push("", "Examples:", sample);
|
|
364
|
+
return lines.join("\n");
|
|
336
365
|
}
|
|
337
366
|
export default function nanomemExtension(api) {
|
|
338
367
|
const project = getProject();
|
|
@@ -440,67 +469,100 @@ export default function nanomemExtension(api) {
|
|
|
440
469
|
console.error("[nanomem] startup maintenance failed:", error);
|
|
441
470
|
}
|
|
442
471
|
});
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
return;
|
|
449
|
-
const lastAtMs = Math.max(await readMetaLastConsolidationMs(memoryDir), await readDreamLockMtimeMs(lockPath));
|
|
450
|
-
const hoursSince = (Date.now() - lastAtMs) / 3_600_000;
|
|
451
|
-
if (lastAtMs > 0 && hoursSince < cfg.minHours)
|
|
452
|
-
return;
|
|
453
|
-
const scanIntervalMs = cfg.scanIntervalMinutes * 60_000;
|
|
454
|
-
if (Date.now() - lastSessionScanAtMs < scanIntervalMs)
|
|
455
|
-
return;
|
|
456
|
-
lastSessionScanAtMs = Date.now();
|
|
457
|
-
const touchedCount = await SessionManager.countTouchedSince(ctx.cwd, lastAtMs, {
|
|
458
|
-
excludeBasename: sessionId,
|
|
459
|
-
});
|
|
460
|
-
if (touchedCount < cfg.minSessions)
|
|
472
|
+
const runDreamTask = async (ctx, source, options = {}) => {
|
|
473
|
+
if (dreamTask.status === "running") {
|
|
474
|
+
if (options.notifyWhenDone) {
|
|
475
|
+
ctx.ui.notify("Memory refresh is already running. Use /dream status to check progress.", "info");
|
|
476
|
+
}
|
|
461
477
|
return;
|
|
478
|
+
}
|
|
479
|
+
const cfg = getDreamConfig(ctx);
|
|
462
480
|
const prior = await tryAcquireDreamLock(lockPath, cfg.holderStaleMinutes * 60_000);
|
|
463
|
-
if (prior === null)
|
|
481
|
+
if (prior === null) {
|
|
482
|
+
if (options.notifyWhenDone) {
|
|
483
|
+
ctx.ui.notify("Memory refresh is already running in another session. Try /dream status later.", "info");
|
|
484
|
+
}
|
|
464
485
|
return;
|
|
486
|
+
}
|
|
465
487
|
const abort = new AbortController();
|
|
466
488
|
dreamTask = {
|
|
467
489
|
status: "running",
|
|
490
|
+
source,
|
|
468
491
|
startedAtMs: Date.now(),
|
|
469
|
-
sessionsReviewing:
|
|
492
|
+
sessionsReviewing: options.sessionsReviewing,
|
|
470
493
|
priorLockMtimeMs: prior,
|
|
471
494
|
abort,
|
|
472
495
|
};
|
|
473
|
-
ctx.ui.setStatus("nanomem", `
|
|
496
|
+
ctx.ui.setStatus("nanomem", options.sessionsReviewing ? `Memory refresh: running (${options.sessionsReviewing} sessions)` : "Memory refresh: running");
|
|
497
|
+
bindLlm(ctx);
|
|
474
498
|
try {
|
|
475
499
|
const result = await engine.consolidateDetailed({ signal: abort.signal });
|
|
476
500
|
dreamTask = {
|
|
477
501
|
status: "completed",
|
|
502
|
+
source,
|
|
478
503
|
startedAtMs: dreamTask.startedAtMs,
|
|
479
504
|
endedAtMs: Date.now(),
|
|
480
|
-
sessionsReviewing:
|
|
505
|
+
sessionsReviewing: options.sessionsReviewing,
|
|
506
|
+
result: result.stats,
|
|
481
507
|
};
|
|
482
|
-
ctx.ui.setStatus("nanomem", `
|
|
483
|
-
if (result.stats.added + result.stats.updated > 0
|
|
484
|
-
ctx.ui.notify(
|
|
508
|
+
ctx.ui.setStatus("nanomem", `Memory refresh: done (+${result.stats.added})`);
|
|
509
|
+
if (options.notifyWhenDone || (ctx.hasUI && result.stats.added + result.stats.updated > 0)) {
|
|
510
|
+
ctx.ui.notify(formatDreamResult(result, memoryDir), "info");
|
|
485
511
|
}
|
|
486
512
|
}
|
|
487
|
-
catch (
|
|
488
|
-
if (abort.signal.aborted || (
|
|
489
|
-
|
|
513
|
+
catch (error) {
|
|
514
|
+
if (abort.signal.aborted || (error instanceof Error && error.message === "AbortError")) {
|
|
515
|
+
if (dreamTask.status !== "killed") {
|
|
516
|
+
dreamTask = {
|
|
517
|
+
status: "killed",
|
|
518
|
+
source,
|
|
519
|
+
startedAtMs: dreamTask.startedAtMs,
|
|
520
|
+
endedAtMs: Date.now(),
|
|
521
|
+
sessionsReviewing: options.sessionsReviewing,
|
|
522
|
+
};
|
|
523
|
+
await rollbackDreamLock(lockPath, prior);
|
|
524
|
+
ctx.ui.setStatus("nanomem", "Memory refresh: stopped");
|
|
525
|
+
}
|
|
490
526
|
return;
|
|
491
527
|
}
|
|
528
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
492
529
|
dreamTask = {
|
|
493
530
|
status: "failed",
|
|
531
|
+
source,
|
|
494
532
|
startedAtMs: dreamTask.startedAtMs,
|
|
495
533
|
endedAtMs: Date.now(),
|
|
496
|
-
sessionsReviewing:
|
|
497
|
-
lastError:
|
|
534
|
+
sessionsReviewing: options.sessionsReviewing,
|
|
535
|
+
lastError: message,
|
|
498
536
|
priorLockMtimeMs: prior,
|
|
499
537
|
};
|
|
500
|
-
ctx.ui.setStatus("nanomem", "
|
|
538
|
+
ctx.ui.setStatus("nanomem", "Memory refresh: failed");
|
|
501
539
|
await rollbackDreamLock(lockPath, prior);
|
|
540
|
+
if (options.notifyWhenDone || ctx.hasUI) {
|
|
541
|
+
ctx.ui.notify(`Memory refresh failed.\nNeeds attention: ${message}`, "error");
|
|
542
|
+
}
|
|
502
543
|
}
|
|
503
544
|
};
|
|
545
|
+
const maybeRunAutoDream = async (ctx) => {
|
|
546
|
+
const cfg = getDreamConfig(ctx);
|
|
547
|
+
if (!cfg.enabled)
|
|
548
|
+
return;
|
|
549
|
+
if (dreamTask.status === "running")
|
|
550
|
+
return;
|
|
551
|
+
const lastAtMs = Math.max(await readMetaLastConsolidationMs(memoryDir), await readDreamLockMtimeMs(lockPath));
|
|
552
|
+
const hoursSince = (Date.now() - lastAtMs) / 3_600_000;
|
|
553
|
+
if (lastAtMs > 0 && hoursSince < cfg.minHours)
|
|
554
|
+
return;
|
|
555
|
+
const scanIntervalMs = cfg.scanIntervalMinutes * 60_000;
|
|
556
|
+
if (Date.now() - lastSessionScanAtMs < scanIntervalMs)
|
|
557
|
+
return;
|
|
558
|
+
lastSessionScanAtMs = Date.now();
|
|
559
|
+
const touchedCount = await SessionManager.countTouchedSince(ctx.cwd, lastAtMs, {
|
|
560
|
+
excludeBasename: sessionId,
|
|
561
|
+
});
|
|
562
|
+
if (touchedCount < cfg.minSessions)
|
|
563
|
+
return;
|
|
564
|
+
await runDreamTask(ctx, "auto", { sessionsReviewing: touchedCount });
|
|
565
|
+
};
|
|
504
566
|
api.on("turn_end", async (_event, ctx) => {
|
|
505
567
|
// Never block turn lifecycle
|
|
506
568
|
void maybeRunAutoDream(ctx).catch(() => { });
|
|
@@ -591,50 +653,41 @@ export default function nanomemExtension(api) {
|
|
|
591
653
|
}
|
|
592
654
|
});
|
|
593
655
|
api.registerCommand("dream", {
|
|
594
|
-
description: "
|
|
656
|
+
description: "Refresh long-term NanoMem memories. Usage: /dream [run|status|stop]",
|
|
657
|
+
getArgumentCompletions: (argumentPrefix) => {
|
|
658
|
+
const prefix = argumentPrefix.trim().toLowerCase();
|
|
659
|
+
const matches = dreamCommands.filter((command) => command.startsWith(prefix));
|
|
660
|
+
if (matches.length === 0)
|
|
661
|
+
return null;
|
|
662
|
+
return matches.map((command) => ({ value: command, label: command }));
|
|
663
|
+
},
|
|
595
664
|
handler: async (args, ctx) => {
|
|
596
665
|
const cmd = (args || "").trim().toLowerCase();
|
|
597
666
|
if (cmd === "stop" || cmd === "kill" || cmd === "cancel") {
|
|
598
667
|
if (dreamTask.status !== "running" || !dreamTask.abort) {
|
|
599
|
-
ctx.ui.notify("
|
|
668
|
+
ctx.ui.notify("No memory refresh is running.", "info");
|
|
600
669
|
return;
|
|
601
670
|
}
|
|
671
|
+
const prior = dreamTask.priorLockMtimeMs;
|
|
602
672
|
dreamTask.abort.abort();
|
|
603
673
|
dreamTask.status = "killed";
|
|
604
674
|
dreamTask.endedAtMs = Date.now();
|
|
605
|
-
ctx.ui.setStatus("nanomem", "
|
|
606
|
-
if (typeof
|
|
607
|
-
await rollbackDreamLock(lockPath,
|
|
675
|
+
ctx.ui.setStatus("nanomem", "Memory refresh: stopped");
|
|
676
|
+
if (typeof prior === "number") {
|
|
677
|
+
await rollbackDreamLock(lockPath, prior);
|
|
608
678
|
}
|
|
609
|
-
ctx.ui.notify("
|
|
679
|
+
ctx.ui.notify("Memory refresh stopped. You can run /dream again when ready.", "info");
|
|
610
680
|
return;
|
|
611
681
|
}
|
|
612
682
|
if (cmd === "status") {
|
|
613
|
-
|
|
614
|
-
const sessions = dreamTask.sessionsReviewing ?? 0;
|
|
615
|
-
const err = dreamTask.lastError ? `\nError: ${dreamTask.lastError}` : "";
|
|
616
|
-
ctx.ui.notify(`Dream status: ${status}${sessions ? ` (${sessions} sessions)` : ""}${err}`, "info");
|
|
683
|
+
ctx.ui.notify(formatDreamStatus(dreamTask, memoryDir), "info");
|
|
617
684
|
return;
|
|
618
685
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
.slice(0, 5)
|
|
625
|
-
.map((e) => `- [${e.type}] ${e.name || e.summary || e.id}`)
|
|
626
|
-
.join("\n");
|
|
627
|
-
const summary = [
|
|
628
|
-
`Dream: consolidation completed.`,
|
|
629
|
-
`Episodes considered: ${result.stats.episodesConsidered}`,
|
|
630
|
-
`Added: ${result.stats.added} | Updated: ${result.stats.updated} | Skipped: ${result.stats.skipped}`,
|
|
631
|
-
`Memory dir: ${memoryDir}`,
|
|
632
|
-
sample ? "" : "",
|
|
633
|
-
sample ? `\nExamples:\n${sample}` : "",
|
|
634
|
-
]
|
|
635
|
-
.filter((x) => x !== "")
|
|
636
|
-
.join("\n");
|
|
637
|
-
ctx.ui.notify(summary, "info");
|
|
686
|
+
if (cmd && cmd !== "run") {
|
|
687
|
+
ctx.ui.notify("Usage: /dream [run|status|stop]", "warning");
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
await runDreamTask(ctx, "manual", { notifyWhenDone: true });
|
|
638
691
|
},
|
|
639
692
|
});
|
|
640
693
|
api.registerCommand("mem-search", {
|
|
@@ -694,11 +747,11 @@ export default function nanomemExtension(api) {
|
|
|
694
747
|
}
|
|
695
748
|
};
|
|
696
749
|
api.registerCommand("mem-insights", {
|
|
697
|
-
description: "
|
|
750
|
+
description: "Create a readable NanoMem insights HTML report",
|
|
698
751
|
handler: runMemInsights,
|
|
699
752
|
});
|
|
700
753
|
api.registerCommand("mem-align", {
|
|
701
|
-
description: "Show which
|
|
754
|
+
description: "Show which memories are shaping the current agent behavior",
|
|
702
755
|
handler: async (_args, ctx) => {
|
|
703
756
|
const snapshot = await engine.getAlignmentSnapshot();
|
|
704
757
|
const topIdentity = snapshot.identityCore
|
|
@@ -718,7 +771,7 @@ export default function nanomemExtension(api) {
|
|
|
718
771
|
},
|
|
719
772
|
});
|
|
720
773
|
api.registerCommand("mem-review", {
|
|
721
|
-
description: "Review
|
|
774
|
+
description: "Review memory conflicts and suggested actions",
|
|
722
775
|
handler: async (_args, ctx) => {
|
|
723
776
|
const snapshot = await engine.getAlignmentSnapshot();
|
|
724
777
|
const topConflicts = snapshot.conflicts.slice(0, 3);
|
|
@@ -733,7 +786,14 @@ export default function nanomemExtension(api) {
|
|
|
733
786
|
},
|
|
734
787
|
});
|
|
735
788
|
api.registerCommand("mem-edit", {
|
|
736
|
-
description: "Edit
|
|
789
|
+
description: "Edit one memory by ID. Usage: /mem-edit <id> <field> <value>",
|
|
790
|
+
getArgumentCompletions: (argumentPrefix) => {
|
|
791
|
+
const prefix = argumentPrefix.trim().toLowerCase();
|
|
792
|
+
const values = memoryEditFields
|
|
793
|
+
.filter((value) => value.startsWith(prefix))
|
|
794
|
+
.map((value) => ({ value, label: value }));
|
|
795
|
+
return values.length > 0 ? values : null;
|
|
796
|
+
},
|
|
737
797
|
handler: async (args, ctx) => {
|
|
738
798
|
const parts = (args || "").trim().split(/\s+/);
|
|
739
799
|
const [id, field, ...rest] = parts;
|
|
@@ -751,6 +811,13 @@ export default function nanomemExtension(api) {
|
|
|
751
811
|
});
|
|
752
812
|
api.registerCommand("mem-resolve", {
|
|
753
813
|
description: "Resolve a memory conflict. Usage: /mem-resolve <aId> <bId> [merge|demote|forget|mark-situational]",
|
|
814
|
+
getArgumentCompletions: (argumentPrefix) => {
|
|
815
|
+
const prefix = argumentPrefix.trim().toLowerCase();
|
|
816
|
+
const values = memoryResolveActions
|
|
817
|
+
.filter((value) => value.startsWith(prefix))
|
|
818
|
+
.map((value) => ({ value, label: value }));
|
|
819
|
+
return values.length > 0 ? values : null;
|
|
820
|
+
},
|
|
754
821
|
handler: async (args, ctx) => {
|
|
755
822
|
const [aId, bId, action] = (args || "").trim().split(/\s+/);
|
|
756
823
|
if (!aId || !bId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pencil-agent/nano-pencil",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.3",
|
|
4
4
|
"description": "CLI writing agent with read, bash, edit, write tools and session management. Supports DashScope and Ali Token Plan. Soul enabled by default for AI personality evolution.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"test:team": "node --test --import tsx test/team-parser.test.ts test/team-runtime.test.ts",
|
|
50
50
|
"test:plan": "node --test --import tsx test/plan-mode.test.ts",
|
|
51
51
|
"test:utils": "node --test --import tsx test/logger.test.ts",
|
|
52
|
-
"test:commands": "node --test --import tsx test/slash-command-catalog.test.ts",
|
|
52
|
+
"test:commands": "node --test --import tsx test/slash-command-catalog.test.ts test/extension-command-completions.test.ts",
|
|
53
53
|
"test:security": "node --test --import tsx test/security-audit.test.ts",
|
|
54
54
|
"test:tools": "node --test --import tsx test/workspace-write-guard.test.ts test/read-tool.test.ts test/tool-window-validation.test.ts test/bash-sandbox.test.ts",
|
|
55
55
|
"test:presence": "node --test --import tsx test/presence-opening.test.ts",
|