@pencil-agent/nano-pencil 1.11.27 → 1.11.29

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.
@@ -272,6 +272,7 @@ export class ModelSelectorComponent extends Container {
272
272
  "dashscope-coding": "DashScope API key (sk-sp-...)",
273
273
  "qianfan-coding": "Qianfan API key",
274
274
  "ark-coding": "Ark API key",
275
+ openrouter: "OpenRouter API key",
275
276
  };
276
277
  const prompt = providerHints[model.provider] ?? `${model.provider} API key`;
277
278
  const key = await promptForApiKey({ prompt: `Enter ${prompt}` });
@@ -293,6 +293,10 @@ export declare class InteractiveMode {
293
293
  * Handle /apikey command - allow user to update API key for current provider
294
294
  */
295
295
  private handleApiKeyCommand;
296
+ private getStoredApiKey;
297
+ private resolveProviderId;
298
+ private promptForProviderApiKey;
299
+ private handleLoginCommand;
296
300
  private handleProviderCredentialsCommand;
297
301
  private ensureProviderConfiguredForSelection;
298
302
  private configureCustomProtocolProvider;
@@ -1760,8 +1760,8 @@ export class InteractiveMode {
1760
1760
  this.editor.setText("");
1761
1761
  return;
1762
1762
  }
1763
- if (text === "/login") {
1764
- this.showOAuthSelector("login");
1763
+ if (text === "/login" || text.startsWith("/login ")) {
1764
+ await this.handleLoginCommand(text);
1765
1765
  this.editor.setText("");
1766
1766
  return;
1767
1767
  }
@@ -3079,6 +3079,66 @@ export class InteractiveMode {
3079
3079
  async handleApiKeyCommand() {
3080
3080
  await this.handleProviderCredentialsCommand();
3081
3081
  }
3082
+ getStoredApiKey(provider) {
3083
+ const credential = this.session.modelRegistry.authStorage.get(provider);
3084
+ return credential?.type === "api_key" ? credential.key : undefined;
3085
+ }
3086
+ resolveProviderId(input) {
3087
+ const normalized = input.trim().toLowerCase();
3088
+ if (!normalized)
3089
+ return undefined;
3090
+ const providerMap = new Map();
3091
+ for (const model of this.session.modelRegistry.getAll()) {
3092
+ providerMap.set(model.provider.toLowerCase(), model.provider);
3093
+ }
3094
+ for (const provider of getOAuthProviders()) {
3095
+ providerMap.set(provider.id.toLowerCase(), provider.id);
3096
+ }
3097
+ return providerMap.get(normalized);
3098
+ }
3099
+ async promptForProviderApiKey(provider, options = {}) {
3100
+ const currentApiKey = this.getStoredApiKey(provider);
3101
+ const title = options.title ?? `Update API key for ${provider}`;
3102
+ const apiKey = await this.showExtensionInput(title, "API key", {
3103
+ initialValue: currentApiKey,
3104
+ });
3105
+ if (apiKey === undefined) {
3106
+ this.showStatus("Configuration cancelled");
3107
+ return false;
3108
+ }
3109
+ const trimmedApiKey = apiKey.trim();
3110
+ if (!trimmedApiKey) {
3111
+ this.showStatus("Configuration cancelled");
3112
+ return false;
3113
+ }
3114
+ this.session.modelRegistry.authStorage.set(provider, {
3115
+ type: "api_key",
3116
+ key: trimmedApiKey,
3117
+ });
3118
+ this.session.modelRegistry.refresh();
3119
+ this.showStatus(`Updated API key for ${provider}`);
3120
+ return true;
3121
+ }
3122
+ async handleLoginCommand(text) {
3123
+ const rawProvider = text.startsWith("/login ") ? text.slice(7).trim() : "";
3124
+ if (!rawProvider) {
3125
+ this.showOAuthSelector("login");
3126
+ return;
3127
+ }
3128
+ const providerId = this.resolveProviderId(rawProvider);
3129
+ if (!providerId) {
3130
+ this.showError(`Unknown provider: ${rawProvider}`);
3131
+ return;
3132
+ }
3133
+ const oauthProvider = getOAuthProviders().find((provider) => provider.id === providerId);
3134
+ if (oauthProvider) {
3135
+ await this.showLoginDialog(oauthProvider.id);
3136
+ return;
3137
+ }
3138
+ await this.promptForProviderApiKey(providerId, {
3139
+ title: `Set API key for ${providerId}`,
3140
+ });
3141
+ }
3082
3142
  async handleProviderCredentialsCommand() {
3083
3143
  const currentModel = this.session.model;
3084
3144
  if (!currentModel) {
@@ -3096,22 +3156,7 @@ export class InteractiveMode {
3096
3156
  }
3097
3157
  return;
3098
3158
  }
3099
- const apiKey = await this.showExtensionInput(`Update API key for ${provider}`, "API key");
3100
- if (apiKey === undefined) {
3101
- this.showStatus("Configuration cancelled");
3102
- return;
3103
- }
3104
- const trimmedApiKey = apiKey.trim();
3105
- if (!trimmedApiKey) {
3106
- this.showStatus("Configuration cancelled");
3107
- return;
3108
- }
3109
- this.session.modelRegistry.authStorage.set(provider, {
3110
- type: "api_key",
3111
- key: trimmedApiKey,
3112
- });
3113
- this.session.modelRegistry.refresh();
3114
- this.showStatus(`Updated API key for ${provider}`);
3159
+ await this.promptForProviderApiKey(provider);
3115
3160
  }
3116
3161
  catch (error) {
3117
3162
  this.showError(error instanceof Error ? error.message : String(error));
@@ -3131,6 +3176,7 @@ export class InteractiveMode {
3131
3176
  definition.defaultBaseUrl;
3132
3177
  const currentModelName = getCustomProtocolProviderModelName(modelsPath, provider) ??
3133
3178
  "custom-model";
3179
+ const currentApiKey = this.getStoredApiKey(provider) ?? "";
3134
3180
  const hasExistingApiKey = authStorage.has(provider);
3135
3181
  if (!options.force &&
3136
3182
  hasExistingApiKey &&
@@ -3149,7 +3195,7 @@ export class InteractiveMode {
3149
3195
  }
3150
3196
  const apiKeyInput = await this.showExtensionInput(`${definition.label} API key`, hasExistingApiKey && options.force
3151
3197
  ? "Leave empty to keep the current API key"
3152
- : "API key");
3198
+ : "API key", { initialValue: currentApiKey });
3153
3199
  if (apiKeyInput === undefined) {
3154
3200
  return false;
3155
3201
  }
@@ -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.27",
3
+ "version": "1.11.29",
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",