@pi-unipi/unipi 0.1.5 → 0.1.6

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/README.md CHANGED
@@ -18,6 +18,7 @@ pi install npm:@pi-unipi/memory
18
18
  pi install npm:@pi-unipi/info-screen
19
19
  pi install npm:@pi-unipi/subagents
20
20
  pi install npm:@pi-unipi/btw
21
+ pi install npm:@pi-unipi/web-api
21
22
  ```
22
23
 
23
24
  ## Packages
@@ -31,6 +32,7 @@ pi install npm:@pi-unipi/btw
31
32
  | `@pi-unipi/info-screen` | Dashboard and module registry overlay |
32
33
  | `@pi-unipi/subagents` | Parallel sub-agent execution with file locking |
33
34
  | `@pi-unipi/btw` | Parallel side conversations with `/btw` |
35
+ | `@pi-unipi/web-api` | Web search, read, and summarize with provider selection |
34
36
 
35
37
  ## Commands
36
38
 
@@ -94,6 +96,13 @@ pi install npm:@pi-unipi/btw
94
96
  | `/unipi:info` | Show info dashboard |
95
97
  | `/unipi:info-settings` | Configure info display |
96
98
 
99
+ ### Web API (`/unipi:web-*`)
100
+
101
+ | Command | Description |
102
+ |---------|-------------|
103
+ | `/unipi:web-settings` | Configure providers and API keys |
104
+ | `/unipi:web-cache-clear` | Clear all cached web content |
105
+
97
106
  ### Tools
98
107
 
99
108
  | Tool | Package | Description |
@@ -108,6 +117,9 @@ pi install npm:@pi-unipi/btw
108
117
  | `memory_list` | memory | List project memories |
109
118
  | `global_memory_search` | memory | Search global memories |
110
119
  | `global_memory_list` | memory | List global memories |
120
+ | `web_search` | web-api | Search the web via provider |
121
+ | `web_read` | web-api | Extract content from URL |
122
+ | `web_llm_summarize` | web-api | Summarize web content via LLM |
111
123
 
112
124
  ## How It Works
113
125
 
@@ -125,6 +137,8 @@ pi install npm:@pi-unipi/btw
125
137
 
126
138
  **Subagents** enables parallel execution with file locking, activity tracking, and custom agent types.
127
139
 
140
+ **Web API** provides web search, page reading, and LLM summarization through a ranked provider system. DuckDuckGo and Jina work out of the box; paid providers (SerpAPI, Tavily, Firecrawl, Perplexity) are configured via `/unipi:web-settings`.
141
+
128
142
  ## Module Discovery
129
143
 
130
144
  Modules announce presence via `pi.events`. When `@pi-unipi/workflow` detects `@pi-unipi/ralph`, it enables loop integration. Each module works standalone.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -128,6 +128,9 @@ web_llm_summarize(url: "https://example.com/research", prompt: "Extract key find
128
128
 
129
129
  Interactive settings dialog for managing providers and API keys.
130
130
 
131
+ - **Auto-enable on key input** — provider is automatically enabled when you add a valid API key (no extra toggle step)
132
+ - **Cursor memory** — last configured provider moves to the top of the list when you return to the menu
133
+
131
134
  ### /unipi:web-cache-clear
132
135
 
133
136
  Clear all cached web content.
@@ -145,8 +148,8 @@ Clear all cached web content.
145
148
  If you see "No search provider available":
146
149
 
147
150
  1. Run `/unipi:web-settings`
148
- 2. Enable at least one provider
149
- 3. Add API keys for paid providers
151
+ 2. Add API keys for paid providers (they auto-enable on key input)
152
+ 3. Or manually enable a free provider
150
153
 
151
154
  ### API key invalid
152
155
 
@@ -4,7 +4,7 @@
4
4
  * Registers /unipi:web-settings and /unipi:web-cache-clear commands.
5
5
  */
6
6
 
7
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
7
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
8
8
  import { UNIPI_PREFIX } from "@pi-unipi/core";
9
9
  import { showSettingsDialog } from "./tui/settings-dialog.js";
10
10
  import { webCache } from "./cache.js";
@@ -20,26 +20,24 @@ export const WEB_COMMANDS = {
20
20
  */
21
21
  export function registerWebCommands(pi: ExtensionAPI): void {
22
22
  // --- /unipi:web-settings command ---
23
- pi.registerCommand({
24
- name: `${UNIPI_PREFIX}${WEB_COMMANDS.SETTINGS}`,
23
+ pi.registerCommand(`${UNIPI_PREFIX}${WEB_COMMANDS.SETTINGS}`, {
25
24
  description: "Configure web API providers and API keys",
26
- async execute(_args, _ctx) {
27
- await showSettingsDialog(pi);
25
+ handler: async (_args, ctx) => {
26
+ await showSettingsDialog(ctx);
28
27
  },
29
28
  });
30
29
 
31
30
  // --- /unipi:web-cache-clear command ---
32
- pi.registerCommand({
33
- name: `${UNIPI_PREFIX}${WEB_COMMANDS.CACHE_CLEAR}`,
31
+ pi.registerCommand(`${UNIPI_PREFIX}${WEB_COMMANDS.CACHE_CLEAR}`, {
34
32
  description: "Clear all cached web content",
35
- async execute(_args, _ctx) {
33
+ handler: async (_args, ctx) => {
36
34
  const stats = webCache.getStats();
37
35
  const cleared = webCache.clear();
38
36
 
39
- await pi.ui.notify({
40
- message: `Cache cleared: ${cleared} entries removed (${stats.totalSizeBytes} bytes freed)`,
41
- level: "success",
42
- });
37
+ ctx.ui.notify(
38
+ `Cache cleared: ${cleared} entries removed (${stats.totalSizeBytes} bytes freed)`,
39
+ "info",
40
+ );
43
41
  },
44
42
  });
45
43
  }
@@ -36,7 +36,7 @@ function getInfoRegistry() {
36
36
 
37
37
  export default function (pi: ExtensionAPI) {
38
38
  // Register skills directory
39
- const skillsDir = new URL("./skills", import.meta.url).pathname;
39
+ const skillsDir = new URL("../skills", import.meta.url).pathname;
40
40
  pi.on("resources_discover", async (_event, _ctx) => {
41
41
  return {
42
42
  skillPaths: [skillsDir],
@@ -75,7 +75,16 @@ export default function (pi: ExtensionAPI) {
75
75
  name: "Web API",
76
76
  icon: "🌐",
77
77
  priority: 50,
78
- getData: async () => {
78
+ config: {
79
+ showByDefault: true,
80
+ stats: [
81
+ { id: "providers", label: "Enabled Providers", show: true },
82
+ { id: "cacheEntries", label: "Cache Entries", show: true },
83
+ { id: "cacheSize", label: "Cache Size", show: true },
84
+ { id: "expired", label: "Expired Entries", show: true },
85
+ ],
86
+ },
87
+ dataProvider: async () => {
79
88
  const config = loadConfig();
80
89
  const stats = webCache.getStats();
81
90
  const enabledCount = Object.values(config.providers).filter(
@@ -83,10 +92,10 @@ export default function (pi: ExtensionAPI) {
83
92
  ).length;
84
93
 
85
94
  return {
86
- "Enabled Providers": enabledCount,
87
- "Cache Entries": stats.totalEntries,
88
- "Cache Size": `${(stats.totalSizeBytes / 1024).toFixed(1)} KB`,
89
- "Expired Entries": stats.expiredEntries,
95
+ providers: { value: String(enabledCount) },
96
+ cacheEntries: { value: String(stats.totalEntries) },
97
+ cacheSize: { value: `${(stats.totalSizeBytes / 1024).toFixed(1)} KB` },
98
+ expired: { value: String(stats.expiredEntries) },
90
99
  };
91
100
  },
92
101
  });
@@ -69,9 +69,10 @@ function selectProvider(
69
69
  const allProviders = registry.getProvidersForCapability(capability);
70
70
  const providerNames = allProviders.map((p) => p.name).join(", ");
71
71
  throw new Error(
72
- `No ${capability} provider available.\n` +
73
- `Configure providers via /unipi:web-settings\n` +
74
- `Available providers: ${providerNames}`
72
+ `No ${capability} provider configured.\n` +
73
+ `→ Run /unipi:web-settings to enable providers and add API keys.\n` +
74
+ `→ Free options: DuckDuckGo (search), Jina Reader (read).\n` +
75
+ `→ Available providers: ${providerNames}`
75
76
  );
76
77
  }
77
78
 
@@ -5,7 +5,7 @@
5
5
  * Uses pi's TUI components for provider selection and key input.
6
6
  */
7
7
 
8
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
8
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
9
9
  import { registry } from "../providers/registry.js";
10
10
  import {
11
11
  getApiKey,
@@ -21,8 +21,9 @@ import { getProviderOptions, getProviderStatuses } from "./provider-selector.js"
21
21
  * Show settings dialog.
22
22
  * This is the main entry point for /unipi:web-settings command.
23
23
  */
24
- export async function showSettingsDialog(pi: ExtensionAPI): Promise<void> {
24
+ export async function showSettingsDialog(ctx: ExtensionCommandContext): Promise<void> {
25
25
  let running = true;
26
+ let lastSelected: string | undefined;
26
27
 
27
28
  while (running) {
28
29
  const options = getProviderOptions();
@@ -34,20 +35,37 @@ export async function showSettingsDialog(pi: ExtensionAPI): Promise<void> {
34
35
  description: "Exit settings",
35
36
  });
36
37
 
38
+ // Move last selected provider to top of list for quick re-entry
39
+ if (lastSelected && lastSelected !== "__exit__") {
40
+ const idx = options.findIndex(o => o.value === lastSelected);
41
+ if (idx > 0) {
42
+ const [item] = options.splice(idx, 1);
43
+ options.unshift(item);
44
+ }
45
+ }
46
+
37
47
  // Show provider list
38
- const selected = await pi.ui.select({
39
- title: "Web API Settings",
40
- message: "Select a provider to configure:",
41
- options,
48
+ const labels = options.map(o => o.value === "__exit__" ? o.label : `${o.label} — ${o.description}`);
49
+ const selected = await ctx.ui.select(
50
+ "Web API Settings",
51
+ labels,
52
+ );
53
+ // Map label back to value
54
+ const selectedOpt = options.find(o => {
55
+ const full = o.value === "__exit__" ? o.label : `${o.label} — ${o.description}`;
56
+ return full === selected;
42
57
  });
58
+ const selectedValue = selectedOpt?.value;
43
59
 
44
- if (!selected || selected === "__exit__") {
60
+ if (!selectedValue || selectedValue === "__exit__") {
45
61
  running = false;
46
62
  continue;
47
63
  }
48
64
 
65
+ lastSelected = selectedValue;
66
+
49
67
  // Show provider configuration
50
- await configureProvider(pi, selected);
68
+ await configureProvider(ctx, selectedValue);
51
69
  }
52
70
  }
53
71
 
@@ -55,15 +73,15 @@ export async function showSettingsDialog(pi: ExtensionAPI): Promise<void> {
55
73
  * Configure a specific provider.
56
74
  */
57
75
  async function configureProvider(
58
- pi: ExtensionAPI,
76
+ ctx: ExtensionCommandContext,
59
77
  providerId: string
60
78
  ): Promise<void> {
61
79
  const provider = registry.getProvider(providerId);
62
80
  if (!provider) {
63
- await pi.ui.notify({
64
- message: `Provider "${providerId}" not found`,
65
- level: "error",
66
- });
81
+ ctx.ui.notify(
82
+ `Provider "${providerId}" not found`,
83
+ "error",
84
+ );
67
85
  return;
68
86
  }
69
87
 
@@ -110,32 +128,34 @@ async function configureProvider(
110
128
  description: "Return to provider list",
111
129
  });
112
130
 
113
- const selected = await pi.ui.select({
114
- title: `Configure ${provider.name}`,
115
- message: `Capabilities: ${provider.capabilities.join(", ")}`,
116
- options,
117
- });
131
+ const labels = options.map(o => `${o.label} — ${o.description}`);
132
+ const selected = await ctx.ui.select(
133
+ `Configure ${provider.name} (${provider.capabilities.join(", ")})`,
134
+ labels,
135
+ );
136
+ const selectedOpt = options.find(o => `${o.label} — ${o.description}` === selected);
137
+ const selectedValue = selectedOpt?.value;
118
138
 
119
- switch (selected) {
139
+ switch (selectedValue) {
120
140
  case "__toggle__":
121
141
  setProviderEnabled(providerId, !enabled);
122
- await pi.ui.notify({
123
- message: `${provider.name} ${!enabled ? "enabled" : "disabled"}`,
124
- level: "success",
125
- });
142
+ ctx.ui.notify(
143
+ `${provider.name} ${!enabled ? "enabled" : "disabled"}`,
144
+ "info",
145
+ );
126
146
  break;
127
147
 
128
148
  case "__add_key__":
129
149
  case "__update_key__":
130
- await inputApiKey(pi, providerId, provider.name);
150
+ await inputApiKey(ctx, providerId, provider.name);
131
151
  break;
132
152
 
133
153
  case "__remove_key__":
134
154
  removeApiKey(providerId);
135
- await pi.ui.notify({
136
- message: `API key removed for ${provider.name}`,
137
- level: "success",
138
- });
155
+ ctx.ui.notify(
156
+ `API key removed for ${provider.name}`,
157
+ "info",
158
+ );
139
159
  break;
140
160
 
141
161
  case "__back__":
@@ -148,30 +168,25 @@ async function configureProvider(
148
168
  * Input API key for a provider.
149
169
  */
150
170
  async function inputApiKey(
151
- pi: ExtensionAPI,
171
+ ctx: ExtensionCommandContext,
152
172
  providerId: string,
153
173
  providerName: string
154
174
  ): Promise<void> {
155
- const apiKey = await pi.ui.input({
156
- title: `API Key for ${providerName}`,
157
- message: `Enter API key (env: ${registry.getProvider(providerId)?.apiKeyEnv || "N/A"}):`,
158
- placeholder: "sk-...",
159
- validate: async (value: string) => {
160
- if (!value || value.trim().length === 0) {
161
- return "API key cannot be empty";
162
- }
163
- if (!validateApiKeyFormat(providerId, value)) {
164
- return "API key format looks invalid";
165
- }
166
- return null;
167
- },
168
- });
175
+ const envHint = registry.getProvider(providerId)?.apiKeyEnv || "N/A";
176
+ const apiKey = await ctx.ui.input(
177
+ `API Key for ${providerName} (env: ${envHint})`,
178
+ "sk-...",
179
+ );
169
180
 
170
181
  if (apiKey) {
171
182
  setApiKey(providerId, apiKey);
172
- await pi.ui.notify({
173
- message: `API key saved for ${providerName}`,
174
- level: "success",
175
- });
183
+ // Auto-enable provider on successful key input
184
+ if (!isProviderEnabled(providerId)) {
185
+ setProviderEnabled(providerId, true);
186
+ }
187
+ ctx.ui.notify(
188
+ `API key saved for ${providerName} — enabled`,
189
+ "info",
190
+ );
176
191
  }
177
192
  }