@pencil-agent/nano-pencil 1.11.28 → 1.11.30

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.
@@ -5,20 +5,26 @@
5
5
  * [COVENANT]: Change → update this header
6
6
  */
7
7
  import { Container } from "@pencil-agent/tui";
8
- import type { AuthStorage } from "../../../core/config/auth-storage.js";
8
+ export interface ProviderSelectorItem {
9
+ id: string;
10
+ name: string;
11
+ authType: "oauth" | "api_key";
12
+ loggedIn?: boolean;
13
+ }
9
14
  /**
10
- * Component that renders an OAuth provider selector
15
+ * Component that renders a provider selector
11
16
  */
12
17
  export declare class OAuthSelectorComponent extends Container {
13
18
  private listContainer;
14
19
  private allProviders;
15
20
  private selectedIndex;
16
21
  private mode;
17
- private authStorage;
18
22
  private onSelectCallback;
19
23
  private onCancelCallback;
20
- constructor(mode: "login" | "logout", authStorage: AuthStorage, onSelect: (providerId: string) => void, onCancel: () => void);
21
- private loadProviders;
24
+ private title;
25
+ constructor(mode: "login" | "logout", providers: ProviderSelectorItem[], onSelect: (providerId: string) => void, onCancel: () => void, options?: {
26
+ title?: string;
27
+ });
22
28
  private updateList;
23
29
  handleInput(keyData: string): void;
24
30
  }
@@ -4,35 +4,33 @@
4
4
  * [LOCUS]: modes/interactive/components/oauth-selector.ts -
5
5
  * [COVENANT]: Change → update this header
6
6
  */
7
- import { getOAuthProviders } from "@pencil-agent/ai";
8
7
  import { Container, getEditorKeybindings, Spacer, TruncatedText } from "@pencil-agent/tui";
9
8
  import { theme } from "../theme/theme.js";
10
9
  import { DynamicBorder } from "./dynamic-border.js";
11
10
  /**
12
- * Component that renders an OAuth provider selector
11
+ * Component that renders a provider selector
13
12
  */
14
13
  export class OAuthSelectorComponent extends Container {
15
14
  listContainer;
16
15
  allProviders = [];
17
16
  selectedIndex = 0;
18
17
  mode;
19
- authStorage;
20
18
  onSelectCallback;
21
19
  onCancelCallback;
22
- constructor(mode, authStorage, onSelect, onCancel) {
20
+ title;
21
+ constructor(mode, providers, onSelect, onCancel, options) {
23
22
  super();
24
23
  this.mode = mode;
25
- this.authStorage = authStorage;
24
+ this.allProviders = providers;
26
25
  this.onSelectCallback = onSelect;
27
26
  this.onCancelCallback = onCancel;
28
- // Load all OAuth providers
29
- this.loadProviders();
27
+ this.title =
28
+ options?.title ?? (mode === "login" ? "Select provider to login:" : "Select provider to logout:");
30
29
  // Add top border
31
30
  this.addChild(new DynamicBorder());
32
31
  this.addChild(new Spacer(1));
33
32
  // Add title
34
- const title = mode === "login" ? "Select provider to login:" : "Select provider to logout:";
35
- this.addChild(new TruncatedText(theme.bold(title)));
33
+ this.addChild(new TruncatedText(theme.bold(this.title)));
36
34
  this.addChild(new Spacer(1));
37
35
  // Create list container
38
36
  this.listContainer = new Container();
@@ -43,9 +41,6 @@ export class OAuthSelectorComponent extends Container {
43
41
  // Initial render
44
42
  this.updateList();
45
43
  }
46
- loadProviders() {
47
- this.allProviders = getOAuthProviders();
48
- }
49
44
  updateList() {
50
45
  this.listContainer.clear();
51
46
  for (let i = 0; i < this.allProviders.length; i++) {
@@ -53,10 +48,7 @@ export class OAuthSelectorComponent extends Container {
53
48
  if (!provider)
54
49
  continue;
55
50
  const isSelected = i === this.selectedIndex;
56
- // Check if user is logged in for this provider
57
- const credentials = this.authStorage.get(provider.id);
58
- const isLoggedIn = credentials?.type === "oauth";
59
- const statusIndicator = isLoggedIn ? theme.fg("success", " ✓ logged in") : "";
51
+ const statusIndicator = provider.loggedIn ? theme.fg("success", " configured") : "";
60
52
  let line = "";
61
53
  if (isSelected) {
62
54
  const prefix = theme.fg("accent", "→ ");
@@ -71,7 +63,7 @@ export class OAuthSelectorComponent extends Container {
71
63
  }
72
64
  // Show "no providers" if empty
73
65
  if (this.allProviders.length === 0) {
74
- const message = this.mode === "login" ? "No OAuth providers available" : "No OAuth providers logged in. Use /login first.";
66
+ const message = this.mode === "login" ? "No providers available" : "No providers logged in. Use /login first.";
75
67
  this.listContainer.addChild(new TruncatedText(theme.fg("muted", ` ${message}`), 0, 0));
76
68
  }
77
69
  }
@@ -315,6 +315,7 @@ export declare class InteractiveMode {
315
315
  private showSessionSelector;
316
316
  private handleResumeSession;
317
317
  private showOAuthSelector;
318
+ private getLoginSelectorProviders;
318
319
  private showLoginDialog;
319
320
  private handleReloadCommand;
320
321
  private handleExportCommand;
@@ -48,7 +48,7 @@ import { FooterComponent } from "./components/footer.js";
48
48
  import { appKey, appKeyHint, editorKey, keyHint, rawKeyHint, } from "./components/keybinding-hints.js";
49
49
  import { LoginDialogComponent } from "./components/login-dialog.js";
50
50
  import { ModelSelectorComponent } from "./components/model-selector.js";
51
- import { OAuthSelectorComponent } from "./components/oauth-selector.js";
51
+ import { OAuthSelectorComponent, } from "./components/oauth-selector.js";
52
52
  import { formatSoulStats } from "./components/soul-stats.js";
53
53
  import { ProviderSelectorComponent } from "./components/provider-selector.js";
54
54
  import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
@@ -3647,19 +3647,26 @@ export class InteractiveMode {
3647
3647
  this.showStatus("Resumed session");
3648
3648
  }
3649
3649
  async showOAuthSelector(mode) {
3650
- if (mode === "logout") {
3651
- const providers = this.session.modelRegistry.authStorage.list();
3652
- const loggedInProviders = providers.filter((p) => this.session.modelRegistry.authStorage.get(p)?.type === "oauth");
3653
- if (loggedInProviders.length === 0) {
3654
- this.showStatus("No OAuth providers logged in. Use /login first.");
3655
- return;
3656
- }
3650
+ const providers = this.getLoginSelectorProviders(mode);
3651
+ if (providers.length === 0) {
3652
+ this.showStatus(mode === "login"
3653
+ ? "No providers available."
3654
+ : "No providers logged in. Use /login first.");
3655
+ return;
3657
3656
  }
3658
3657
  this.showSelector((done) => {
3659
- const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, async (providerId) => {
3658
+ const selector = new OAuthSelectorComponent(mode, providers, async (providerId) => {
3660
3659
  done();
3661
3660
  if (mode === "login") {
3662
- await this.showLoginDialog(providerId);
3661
+ const oauthProvider = getOAuthProviders().find((p) => p.id === providerId);
3662
+ if (oauthProvider) {
3663
+ await this.showLoginDialog(providerId);
3664
+ }
3665
+ else {
3666
+ await this.promptForProviderApiKey(providerId, {
3667
+ title: `Set API key for ${providerId}`,
3668
+ });
3669
+ }
3663
3670
  }
3664
3671
  else {
3665
3672
  // Logout flow
@@ -3678,10 +3685,47 @@ export class InteractiveMode {
3678
3685
  }, () => {
3679
3686
  done();
3680
3687
  this.ui.requestRender();
3688
+ }, {
3689
+ title: mode === "login"
3690
+ ? "Select provider to login or configure:"
3691
+ : "Select provider to logout:",
3681
3692
  });
3682
3693
  return { component: selector, focus: selector };
3683
3694
  });
3684
3695
  }
3696
+ getLoginSelectorProviders(mode) {
3697
+ const oauthProviders = getOAuthProviders().map((provider) => ({
3698
+ id: provider.id,
3699
+ name: provider.name,
3700
+ authType: "oauth",
3701
+ loggedIn: this.session.modelRegistry.authStorage.get(provider.id)?.type ===
3702
+ "oauth",
3703
+ }));
3704
+ if (mode === "logout") {
3705
+ return oauthProviders.filter((provider) => provider.loggedIn);
3706
+ }
3707
+ const items = [...oauthProviders];
3708
+ const providerIds = new Set(items.map((provider) => provider.id));
3709
+ const apiKeyProviders = [
3710
+ { id: "openrouter", name: "OpenRouter" },
3711
+ ];
3712
+ for (const provider of apiKeyProviders) {
3713
+ if (providerIds.has(provider.id))
3714
+ continue;
3715
+ const hasModels = this.session.modelRegistry
3716
+ .getAll()
3717
+ .some((model) => model.provider === provider.id);
3718
+ if (!hasModels)
3719
+ continue;
3720
+ items.push({
3721
+ id: provider.id,
3722
+ name: provider.name,
3723
+ authType: "api_key",
3724
+ loggedIn: !!this.getStoredApiKey(provider.id),
3725
+ });
3726
+ }
3727
+ return items;
3728
+ }
3685
3729
  async showLoginDialog(providerId) {
3686
3730
  const providerInfo = getOAuthProviders().find((p) => p.id === providerId);
3687
3731
  const providerName = providerInfo?.name || providerId;
@@ -8,7 +8,7 @@
8
8
  * For non-NanoPencil hosts, import from the package root (index.ts) instead.
9
9
  */
10
10
  import { writeFileSync } from "node:fs";
11
- import { basename, join } from "node:path";
11
+ import { basename, join, resolve } from "node:path";
12
12
  import { Type } from "@sinclair/typebox";
13
13
  import { SessionManager } from "@pencil-agent/nano-pencil";
14
14
  import { NanoMemEngine } from "./engine.js";
@@ -555,9 +555,12 @@ export default function nanomemExtension(pi) {
555
555
  ctx.ui.notify(`NanoMem: ${s.totalSessions} sessions | ${s.knowledge} knowledge | ${s.lessons} lessons | ${s.events} events | ${s.preferences} prefs | ${s.work} work | ${s.episodes} episodes`, "info");
556
556
  },
557
557
  });
558
- pi.registerCommand("mem-insights", {
559
- description: "Generate NanoMem full insights HTML report (uses LLM when available)",
560
- handler: async (args, ctx) => {
558
+ const runMemInsights = async (args, ctx) => {
559
+ ctx.ui.setStatus("nanomem", "Generating insights...");
560
+ const requestedPath = args?.trim() || "./nanomem-insights.html";
561
+ const outputPath = resolve(process.cwd(), requestedPath);
562
+ ctx.ui.notify(`NanoMem: generating insights report -> ${outputPath}`, "info");
563
+ try {
561
564
  const llmCtx = ctx;
562
565
  if (llmCtx.completeSimple) {
563
566
  engine.setLlmFn(async (systemPrompt, userMessage) => {
@@ -565,9 +568,18 @@ export default function nanomemExtension(pi) {
565
568
  return out ?? "";
566
569
  });
567
570
  }
568
- const outputPath = args?.trim() || "./nanomem-insights.html";
569
- ctx.ui.notify("NanoMem: Generating full insights report...", "info");
570
- const enhanced = await engine.generateEnhancedInsights();
571
+ let enhanced;
572
+ try {
573
+ enhanced = await engine.generateEnhancedInsights();
574
+ }
575
+ catch {
576
+ enhanced = {
577
+ report: await engine.generateFullInsights(),
578
+ persona: undefined,
579
+ humanInsights: [],
580
+ rootCauses: [],
581
+ };
582
+ }
571
583
  const html = renderFullInsightsHtml({
572
584
  ...enhanced.report,
573
585
  persona: enhanced.persona,
@@ -575,8 +587,20 @@ export default function nanomemExtension(pi) {
575
587
  rootCauses: enhanced.rootCauses,
576
588
  }, engine.cfg.locale);
577
589
  writeFileSync(outputPath, html, "utf-8");
578
- ctx.ui.notify(`NanoMem: Insights report written to ${outputPath}`, "info");
579
- },
590
+ ctx.ui.notify(`NanoMem: insights report written to ${outputPath}`, "info");
591
+ }
592
+ catch (error) {
593
+ const message = error instanceof Error ? error.message : String(error);
594
+ ctx.ui.notify(`NanoMem: failed to generate insights report: ${message}`, "error");
595
+ throw error;
596
+ }
597
+ finally {
598
+ ctx.ui.setStatus("nanomem", "");
599
+ }
600
+ };
601
+ pi.registerCommand("mem-insights", {
602
+ description: "Generate NanoMem full insights HTML report (uses LLM when available)",
603
+ handler: runMemInsights,
580
604
  });
581
605
  pi.registerCommand("mem-align", {
582
606
  description: "Show which stable memories and current states are shaping the agent",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pencil-agent/nano-pencil",
3
- "version": "1.11.28",
3
+ "version": "1.11.30",
4
4
  "description": "CLI writing agent with read, bash, edit, write tools and session management. Based on pi; supports DashScope Coding Plan. Soul enabled by default for AI personality evolution.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,6 +28,7 @@
28
28
  "copy:extensions": "node -e \"\nconst fs=require('fs');\nconst p=require('path');\nfunction c(s,d){\n if(fs.existsSync(d))fs.rmSync(d,{recursive:true,force:true});\n if(!fs.existsSync(d))fs.mkdirSync(d,{recursive:true});\n fs.readdirSync(s).forEach(f=>{\n const sp=p.join(s,f),dp=p.join(d,f);\n fs.statSync(sp).isDirectory()?c(sp,dp):fs.copyFileSync(sp,dp)\n })\n}\n// Copy defaults extensions\n\nc('extensions/defaults/security-audit','dist/extensions/defaults/security-audit');\nc('extensions/defaults/soul','dist/extensions/defaults/soul');\nc('extensions/defaults/presence','dist/extensions/defaults/presence');\nc('extensions/defaults/interview','dist/extensions/defaults/interview');\nc('extensions/defaults/loop','dist/extensions/defaults/loop');\nc('extensions/defaults/team','dist/extensions/defaults/team');\nc('extensions/defaults/mcp','dist/extensions/defaults/mcp');\n// Copy optional extensions\nc('extensions/optional/simplify','dist/extensions/optional/simplify');\nc('extensions/optional/export-html','dist/extensions/optional/export-html');\nconsole.log('Extensions copied to dist/');\n\"",
29
29
  "bundle:packages": "node scripts/bundle-deps.js",
30
30
  "watch": "tsc --watch",
31
+ "test:presence": "node --test --import tsx test/presence-opening.test.ts",
31
32
  "start": "npx cross-env NODE_ENV=production node --no-deprecation dist/cli.js",
32
33
  "prepublishOnly": "npm run build",
33
34
  "changelog": "node scripts/generate-changelog.js",