pi-ui-extend 0.1.72 → 0.1.77

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.
@@ -72,15 +72,18 @@ export function formatOpencodeImportResult(result: OpencodeImportResult): string
72
72
  if (antigravity) providerLines.push(`- ${antigravity}`);
73
73
 
74
74
  if (providerLines.length === 0) {
75
- return `No opencode credentials were imported. Checked ${result.sourcePath}${result.antigravitySourcePath ? ` and ${result.antigravitySourcePath}` : ""}.`;
75
+ return `No OpenCode credentials were imported. Checked ${result.sourcePath}${result.antigravitySourcePath ? ` and ${result.antigravitySourcePath}` : ""}.`;
76
76
  }
77
77
 
78
78
  const suffix = missingCount > 0 ? `\nMissing ${missingCount} known opencode provider entr${missingCount === 1 ? "y" : "ies"}.` : "";
79
- return `Opencode import wrote to ${result.authPath}:\n${providerLines.join("\n")}${suffix}`;
79
+ const heading = result.wroteAuth ? `Imported OpenCode credentials into ${result.authPath}:` : `OpenCode credential check for ${result.authPath}:`;
80
+ return `${heading}\n${providerLines.join("\n")}${suffix}`;
80
81
  }
81
82
 
82
- export function notificationLevel(result: OpencodeImportResult): "info" | "warn" | "error" {
83
+ export function notificationLevel(result: OpencodeImportResult): "info" | "warning" | "error" {
83
84
  if (result.wroteAuth) return "info";
84
- if (result.providers.some((provider) => provider.status === "auth-exists-use-force") || result.antigravity?.reason === "auth-exists-use-force") return "warn";
85
- return "error";
85
+ if (result.providers.some((provider) => provider.status === "auth-exists-use-force") || result.antigravity?.reason === "auth-exists-use-force") return "warning";
86
+ if (result.providers.some((provider) => provider.status === "invalid-source") || result.antigravity?.reason === "matching-account-not-found") return "error";
87
+ if (result.providers.some((provider) => provider.status === "already-imported") || result.antigravity?.reason === "already-imported") return "info";
88
+ return "warning";
86
89
  }
@@ -55,16 +55,16 @@ export type OpencodeImportOptions = {
55
55
  type Mapping = {
56
56
  label: string;
57
57
  sourceProvider: string;
58
- targetProvider: string;
58
+ targetProvider: string | ((credential: OpencodeAuthCredential | undefined) => string);
59
59
  transform: (credential: OpencodeAuthCredential) => PiAuthCredential | undefined;
60
60
  };
61
61
 
62
62
  const AUTH_JSON_MAPPINGS: Mapping[] = [
63
63
  {
64
- label: "OpenAI Codex",
64
+ label: "OpenAI",
65
65
  sourceProvider: "openai",
66
- targetProvider: "openai-codex",
67
- transform: transformOAuthCredential,
66
+ targetProvider: (credential) => (isOAuthCredential(credential) ? "openai-codex" : "openai"),
67
+ transform: transformOpenAiCredential,
68
68
  },
69
69
  {
70
70
  label: "GitHub Copilot",
@@ -92,24 +92,45 @@ export function getDefaultOpencodeAuthPath(): string {
92
92
  }
93
93
 
94
94
  function transformOAuthCredential(credential: OpencodeAuthCredential): PiAuthCredential | undefined {
95
- if (!credential.access && !credential.refresh) return undefined;
95
+ if (
96
+ typeof credential.access !== "string" ||
97
+ !credential.access ||
98
+ typeof credential.refresh !== "string" ||
99
+ !credential.refresh ||
100
+ typeof credential.expires !== "number" ||
101
+ !Number.isFinite(credential.expires)
102
+ ) {
103
+ return undefined;
104
+ }
96
105
  return { ...credential, type: "oauth" };
97
106
  }
98
107
 
99
108
  function transformApiKeyCredential(credential: OpencodeAuthCredential): PiAuthCredential | undefined {
100
- if (!credential.key) return undefined;
109
+ if (typeof credential.key !== "string" || !credential.key) return undefined;
101
110
  return { ...credential, type: "api_key", key: credential.key };
102
111
  }
103
112
 
113
+ function isOAuthCredential(credential: OpencodeAuthCredential | undefined): boolean {
114
+ return credential?.type === "oauth" || typeof credential?.access === "string" || typeof credential?.refresh === "string";
115
+ }
116
+
117
+ function transformOpenAiCredential(credential: OpencodeAuthCredential): PiAuthCredential | undefined {
118
+ return isOAuthCredential(credential) ? transformOAuthCredential(credential) : transformApiKeyCredential(credential);
119
+ }
120
+
104
121
  function sameCredential(a: PiAuthCredential | undefined, b: PiAuthCredential | undefined): boolean {
105
122
  return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
106
123
  }
107
124
 
108
- function providerResult(mapping: Mapping, status: OpencodeProviderImportStatus): OpencodeProviderImportResult {
125
+ function targetProvider(mapping: Mapping, credential: OpencodeAuthCredential | undefined): string {
126
+ return typeof mapping.targetProvider === "function" ? mapping.targetProvider(credential) : mapping.targetProvider;
127
+ }
128
+
129
+ function providerResult(mapping: Mapping, status: OpencodeProviderImportStatus, credential?: OpencodeAuthCredential): OpencodeProviderImportResult {
109
130
  return {
110
131
  label: mapping.label,
111
132
  sourceProvider: mapping.sourceProvider,
112
- targetProvider: mapping.targetProvider,
133
+ targetProvider: targetProvider(mapping, credential),
113
134
  status,
114
135
  };
115
136
  }
@@ -123,8 +144,15 @@ async function pathExists(path: string): Promise<boolean> {
123
144
  }
124
145
  }
125
146
 
147
+ async function readOpencodeAuth(sourcePath: string, environmentSource: string | undefined): Promise<OpencodeAuthData> {
148
+ if (environmentSource !== undefined) return parseOpencodeAuthContent(environmentSource);
149
+ if (!(await pathExists(sourcePath))) return {};
150
+ return readJsonFile<OpencodeAuthData>(sourcePath, {});
151
+ }
152
+
126
153
  export async function importOpencodeAccounts(options: OpencodeImportOptions = {}): Promise<OpencodeImportResult> {
127
- const sourcePath = options.sourcePath ?? getDefaultOpencodeAuthPath();
154
+ const environmentSource = options.sourcePath === undefined ? process.env.OPENCODE_AUTH_CONTENT : undefined;
155
+ const sourcePath = options.sourcePath ?? (environmentSource !== undefined ? "OPENCODE_AUTH_CONTENT" : getDefaultOpencodeAuthPath());
128
156
  const authPath = options.authPath ?? getPiAuthPath();
129
157
  const result: OpencodeImportResult = {
130
158
  sourcePath,
@@ -138,8 +166,7 @@ export async function importOpencodeAccounts(options: OpencodeImportOptions = {}
138
166
  const changedTargets = new Set<string>();
139
167
 
140
168
  if (!options.skipAuthJson) {
141
- const sourceExists = await pathExists(sourcePath);
142
- const opencodeAuth = sourceExists ? await readJsonFile<OpencodeAuthData>(sourcePath, {}) : {};
169
+ const opencodeAuth = await readOpencodeAuth(sourcePath, environmentSource);
143
170
 
144
171
  for (const mapping of AUTH_JSON_MAPPINGS) {
145
172
  const sourceCredential = opencodeAuth[mapping.sourceProvider];
@@ -147,34 +174,35 @@ export async function importOpencodeAccounts(options: OpencodeImportOptions = {}
147
174
  result.providers.push(providerResult(mapping, "source-missing"));
148
175
  continue;
149
176
  }
177
+ const resolvedTargetProvider = targetProvider(mapping, sourceCredential);
150
178
 
151
- if (changedTargets.has(mapping.targetProvider)) {
152
- result.providers.push(providerResult(mapping, "target-set-from-other-source"));
179
+ if (changedTargets.has(resolvedTargetProvider)) {
180
+ result.providers.push(providerResult(mapping, "target-set-from-other-source", sourceCredential));
153
181
  continue;
154
182
  }
155
183
 
156
184
  const nextCredential = mapping.transform(sourceCredential);
157
185
  if (!nextCredential) {
158
- result.providers.push(providerResult(mapping, "invalid-source"));
186
+ result.providers.push(providerResult(mapping, "invalid-source", sourceCredential));
159
187
  continue;
160
188
  }
161
189
 
162
- const existingCredential = piAuth[mapping.targetProvider];
190
+ const existingCredential = piAuth[resolvedTargetProvider];
163
191
  if (sameCredential(existingCredential, nextCredential)) {
164
- result.providers.push(providerResult(mapping, "already-imported"));
165
- changedTargets.add(mapping.targetProvider);
192
+ result.providers.push(providerResult(mapping, "already-imported", sourceCredential));
193
+ changedTargets.add(resolvedTargetProvider);
166
194
  continue;
167
195
  }
168
196
 
169
197
  if (existingCredential && !options.overwrite) {
170
- result.providers.push(providerResult(mapping, "auth-exists-use-force"));
171
- changedTargets.add(mapping.targetProvider);
198
+ result.providers.push(providerResult(mapping, "auth-exists-use-force", sourceCredential));
199
+ changedTargets.add(resolvedTargetProvider);
172
200
  continue;
173
201
  }
174
202
 
175
- piAuth = { ...piAuth, [mapping.targetProvider]: nextCredential };
176
- changedTargets.add(mapping.targetProvider);
177
- result.providers.push(providerResult(mapping, "imported"));
203
+ piAuth = { ...piAuth, [resolvedTargetProvider]: nextCredential };
204
+ changedTargets.add(resolvedTargetProvider);
205
+ result.providers.push(providerResult(mapping, "imported", sourceCredential));
178
206
  }
179
207
 
180
208
  if (result.providers.some((provider) => provider.status === "imported")) {
@@ -206,3 +234,16 @@ export async function importOpencodeAccounts(options: OpencodeImportOptions = {}
206
234
 
207
235
  return result;
208
236
  }
237
+
238
+ function parseOpencodeAuthContent(content: string): OpencodeAuthData {
239
+ let parsed: unknown;
240
+ try {
241
+ parsed = JSON.parse(content);
242
+ } catch {
243
+ throw new Error("OPENCODE_AUTH_CONTENT must contain a valid JSON object.");
244
+ }
245
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
246
+ throw new Error("OPENCODE_AUTH_CONTENT must contain a JSON object.");
247
+ }
248
+ return parsed as OpencodeAuthData;
249
+ }
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { formatOpencodeImportResult, notificationLevel, parseOpencodeImportCommandArgs } from "./commands";
3
3
  import { importOpencodeAccounts } from "./importer";
4
4
 
@@ -8,10 +8,12 @@ export type { OpencodeImportOptions, OpencodeImportResult } from "./importer";
8
8
 
9
9
  export default function opencodeImport(pi: ExtensionAPI): void {
10
10
  pi.registerCommand("opencode-import", {
11
- description: "Import opencode auth.json credentials and Antigravity accounts into Pi/Pix auth.json",
12
- handler: async (args: string, ctx: any) => {
11
+ description: "Import supported OpenCode credentials and Antigravity accounts into Pi/Pix auth.json",
12
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
13
+ let wroteAuth = false;
13
14
  try {
14
15
  const result = await importOpencodeAccounts(parseOpencodeImportCommandArgs(args));
16
+ wroteAuth = result.wroteAuth;
15
17
  const message = formatOpencodeImportResult(result);
16
18
  if (ctx.ui?.notify) ctx.ui.notify(message, notificationLevel(result));
17
19
  else console.log(message);
@@ -19,6 +21,15 @@ export default function opencodeImport(pi: ExtensionAPI): void {
19
21
  const message = error instanceof Error ? error.message : String(error);
20
22
  if (ctx.ui?.notify) ctx.ui.notify(message, "error");
21
23
  else console.error(message);
24
+ return;
25
+ }
26
+
27
+ if (!wroteAuth) return;
28
+ try {
29
+ await ctx.reload();
30
+ } catch (error) {
31
+ const message = error instanceof Error ? error.message : String(error);
32
+ console.error(`OpenCode credentials were imported, but resource reload failed: ${message}`);
22
33
  }
23
34
  },
24
35
  });
@@ -246,8 +246,8 @@ export const WEB_SEARCH_TOOL_DESCRIPTIONS = {
246
246
  name: "web_search",
247
247
  label: "web-search",
248
248
  description:
249
- "Search the web for real-time information using your local Ollama instance's web_search API. Requires Ollama running locally with web search enabled; supports per-call timeout_ms and PI_WEB_SEARCH_TIMEOUT_MS.",
250
- promptSnippet: "Search the web for current or real-time information through the local Ollama web_search API.",
249
+ "Search the web for real-time information through Ollama, with automatic configured Tavily fallback. Credentials come from /web-credentials or environment variables. Supports per-call timeout_ms and PI_WEB_SEARCH_TIMEOUT_MS.",
250
+ promptSnippet: "Search the web for current or real-time information through Ollama, with Tavily fallback when configured.",
251
251
  promptGuidelines: [
252
252
  "Use web_search only for current public web information; keep queries focused and set max_results only when useful.",
253
253
  "Never include secrets, tokens, or private repository data; do not use web_search for repo-local discovery—use repo_* or file/search tools.",
@@ -257,8 +257,8 @@ export const WEB_SEARCH_TOOL_DESCRIPTIONS = {
257
257
  name: "web_fetch",
258
258
  label: "web-fetch",
259
259
  description:
260
- "Fetch and extract text content from a web page URL using your local Ollama instance's web_fetch API. Requires Ollama running locally with web fetch enabled; supports per-call timeout_ms and PI_WEB_SEARCH_TIMEOUT_MS.",
261
- promptSnippet: "Fetch and extract text from a specific URL through the local Ollama web_fetch API.",
260
+ "Fetch and extract text content from a web page URL through Ollama, with automatic configured Tavily Extract fallback. Credentials come from /web-credentials or environment variables. Supports per-call timeout_ms and PI_WEB_SEARCH_TIMEOUT_MS.",
261
+ promptSnippet: "Fetch and extract text from a specific URL through Ollama, with Tavily Extract fallback when configured.",
262
262
  promptGuidelines: [
263
263
  "Use web_fetch for user-provided URLs or web_search results needing deeper reading; never pass secret/private-credential URLs, and use read for local files.",
264
264
  ],