pi-ui-extend 0.1.74 → 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.
- package/README.md +45 -3
- package/dist/app/cli/install.d.ts +20 -2
- package/dist/app/cli/install.js +128 -13
- package/dist/app/cli/startup-checks.d.ts +1 -0
- package/dist/app/cli/startup-checks.js +16 -0
- package/dist/app/runtime.d.ts +3 -1
- package/dist/app/runtime.js +23 -6
- package/external/pi-tools-suite/README.md +8 -1
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +20 -6
- package/external/pi-tools-suite/src/opencode-import/commands.ts +8 -5
- package/external/pi-tools-suite/src/opencode-import/importer.ts +63 -22
- package/external/pi-tools-suite/src/opencode-import/index.ts +14 -3
- package/package.json +1 -1
- package/skills/context7/SKILL.md +10 -0
- package/skills/context7/scripts/context7.sh +3 -2
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ npx pi-ui-extend install
|
|
|
22
22
|
npx pi-ui-extend --cwd .
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
The setup command checks
|
|
25
|
+
The setup command checks runtime helpers, creates non-secret Pix and tools configuration templates when missing, and prints a credential-aware checklist. It never imports credentials. The second command opens the current project in Pix without requiring a global Pix install.
|
|
26
26
|
|
|
27
27
|
> Already installed globally? Run `pix --cwd .`.
|
|
28
28
|
|
|
@@ -152,6 +152,19 @@ npx pi-ui-extend install
|
|
|
152
152
|
npx pi-ui-extend --cwd /path/to/project
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
+
### First-run checklist
|
|
156
|
+
|
|
157
|
+
1. Run `npx pi-ui-extend install`. It preserves existing files and creates these non-secret templates when missing:
|
|
158
|
+
- `~/.config/pi/pix.jsonc`
|
|
159
|
+
- `~/.config/pi/pi-tools-suite.jsonc`
|
|
160
|
+
2. Configure at least one model provider:
|
|
161
|
+
- With an existing OpenCode setup, start Pix and run `/opencode-import`; review [OpenCode migration](#opencode-migration) before using `--force`.
|
|
162
|
+
- Otherwise run `npx @earendil-works/pi-coding-agent`, use `/login` in the stock Pi TUI, then exit. Environment-based provider API keys also work.
|
|
163
|
+
3. Start Pix, use `/model` to select an available model, and follow any startup authentication diagnostic.
|
|
164
|
+
4. Configure only the optional integrations you need; none are required for normal model conversations.
|
|
165
|
+
|
|
166
|
+
The installer installs JetBrainsMono Nerd Font when it is missing. Pix also checks it on startup and starts a background installation when needed so the icon UI remains readable even when setup was skipped. Installation failures are reported while Pix keeps its fallback icons. Use `--check` to inspect the machine without creating files, installing helpers, or changing credentials.
|
|
167
|
+
|
|
155
168
|
### Global install
|
|
156
169
|
|
|
157
170
|
```bash
|
|
@@ -190,8 +203,37 @@ pix --cwd . --no-session
|
|
|
190
203
|
|
|
191
204
|
Pix uses Pi's model and authentication stores. Environment-based API keys and credentials already configured for Pi are available to Pix.
|
|
192
205
|
|
|
193
|
-
|
|
194
|
-
|
|
206
|
+
| Credential or integration | Required? | Setup | Notes |
|
|
207
|
+
| --- | --- | --- | --- |
|
|
208
|
+
| Model provider | **Yes**, unless the selected model needs no credentials | Run the stock Pi TUI with `npx @earendil-works/pi-coding-agent`, use `/login`, then `/reload` in Pix; or set a provider-supported environment key | Pix does not yet implement Pi's interactive `/login` and `/logout` dialogs. |
|
|
209
|
+
| Existing OpenCode accounts | No; migration shortcut | Run `/opencode-import` in Pix | Imports only the credential types listed below and preserves existing Pi entries by default. |
|
|
210
|
+
| Model-backed helpers | No | Review `promptEnhancer`, `autocomplete`, and `sessionTitle` in `pix.jsonc` | Each configured helper model needs credentials for its own provider; it need not use the main session provider. |
|
|
211
|
+
| Ollama/Tavily web access | No | Use local Ollama without a cloud key, set `OLLAMA_API_KEY`/`TAVILY_API_KEY`, or run `/web-credentials` | Stored keys live in `~/.config/pi/pi-tools-suite-credentials.json` with mode `0600`. |
|
|
212
|
+
| Context7 documentation skill | No | Export `CONTEXT7_API_KEY` | The skill fails before making a network request when the variable is absent. |
|
|
213
|
+
| Telegram terminal bell | No | Configure `terminalBell.telegram` or `PI_TERMINAL_BELL_TELEGRAM_BOT_TOKEN` plus `PI_TERMINAL_BELL_TELEGRAM_CHAT_ID` | Used only for the optional terminal notification integration. |
|
|
214
|
+
|
|
215
|
+
`pix install` reports whether these sources appear configured, but never displays, copies, or overwrites token values.
|
|
216
|
+
|
|
217
|
+
### OpenCode migration
|
|
218
|
+
|
|
219
|
+
Run this inside Pix after the bundled tools suite loads:
|
|
220
|
+
|
|
221
|
+
```text
|
|
222
|
+
/opencode-import
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Supported mappings are deliberately narrow:
|
|
226
|
+
|
|
227
|
+
- OpenAI OAuth → Pi's `openai-codex` provider
|
|
228
|
+
- OpenAI API key → Pi's `openai` provider
|
|
229
|
+
- GitHub Copilot OAuth → `github-copilot`
|
|
230
|
+
- Z.ai/Zhipu-compatible credentials → `zai`
|
|
231
|
+
- an OpenCode Antigravity account → Pi's Antigravity provider
|
|
232
|
+
|
|
233
|
+
By default the command reads `$OPENCODE_AUTH_CONTENT`, `$OPENCODE_DATA_DIR/auth.json`, or the XDG/default OpenCode data path, plus the corresponding OpenCode Antigravity account file. It writes to Pi's active agent directory (`$PI_CODING_AGENT_DIR` when set), preserves existing provider entries, and reloads Pix only after a successful write. Use `/opencode-import --force` only when you intentionally want supported existing entries replaced. Explicit source and destination paths are available through `/opencode-import --path <file>`, `--auth-path <file>`, and `--antigravity-path <file>`.
|
|
234
|
+
|
|
235
|
+
OpenCode model/provider definitions, default-model choices, MCP servers, plugins, instructions, and tool settings are **not** migrated because their formats and security assumptions do not map safely. Recreate those settings in Pix/Pi and `pi-tools-suite.jsonc` as needed. No secret is imported during `pix install` itself.
|
|
236
|
+
|
|
195
237
|
- Run `/antigravity-add-account` to add an Antigravity OAuth account, then use `/antigravity-account` and `/antigravity-status` to manage it.
|
|
196
238
|
- Use `/model`, `/scoped-models`, and `/thinking` to control the active runtime.
|
|
197
239
|
- Use `/default-model`, `/default-thinking`, and `/autocomplete` to change defaults for new sessions.
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { installJetBrainsNerdFont, isJetBrainsNerdFontInstalled } from "../terminal/nerd-font-controller.js";
|
|
4
4
|
import { clipboardInstallHint, clipboardSupportAvailable } from "../screen/clipboard.js";
|
|
5
5
|
type PixInstallTestDeps = {
|
|
6
6
|
existsSync: typeof existsSync;
|
|
7
|
+
readFileSync: typeof readFileSync;
|
|
8
|
+
mkdirSync: typeof mkdirSync;
|
|
9
|
+
writeFileSync: typeof writeFileSync;
|
|
7
10
|
spawn: typeof spawn;
|
|
8
11
|
isJetBrainsNerdFontInstalled: typeof isJetBrainsNerdFontInstalled;
|
|
9
12
|
installJetBrainsNerdFont: typeof installJetBrainsNerdFont;
|
|
@@ -19,7 +22,22 @@ export type PixInstallCliContext = {
|
|
|
19
22
|
env?: NodeJS.ProcessEnv;
|
|
20
23
|
homeDir?: string;
|
|
21
24
|
};
|
|
22
|
-
export
|
|
25
|
+
export type PixOnboardingState = {
|
|
26
|
+
pixConfigExists: boolean;
|
|
27
|
+
toolsConfigExists: boolean;
|
|
28
|
+
providerAuthConfigured: boolean;
|
|
29
|
+
opencodeAuthExists: boolean;
|
|
30
|
+
opencodeAntigravityExists: boolean;
|
|
31
|
+
webCredentialsConfigured: boolean;
|
|
32
|
+
context7Configured: boolean;
|
|
33
|
+
telegramConfigured: boolean;
|
|
34
|
+
};
|
|
35
|
+
export declare function inspectPixOnboarding(homeDir?: string, env?: NodeJS.ProcessEnv): PixOnboardingState;
|
|
36
|
+
export declare function ensurePixSetupFiles(homeDir?: string): {
|
|
37
|
+
created: string[];
|
|
38
|
+
existing: string[];
|
|
39
|
+
};
|
|
40
|
+
export declare function formatPixInstallNextSteps(homeDir?: string, state?: PixOnboardingState): string;
|
|
23
41
|
export declare function pixInstallUsage(): string;
|
|
24
42
|
export declare function parsePixInstallArgs(argv: readonly string[]): PixInstallCliOptions;
|
|
25
43
|
export declare function runPixInstallCli(argv?: readonly string[], context?: PixInstallCliContext): Promise<number>;
|
package/dist/app/cli/install.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
5
|
import { FONT_FAMILY_NAME, installJetBrainsNerdFont, isJetBrainsNerdFontInstalled, } from "../terminal/nerd-font-controller.js";
|
|
6
6
|
import { clipboardInstallHint, clipboardSupportAvailable } from "../screen/clipboard.js";
|
|
7
7
|
import { getPixConfigPath } from "../../config.js";
|
|
8
|
+
import { DEFAULT_PIX_CONFIG_JSONC } from "../../default-pix-config.js";
|
|
8
9
|
const defaultPixInstallDeps = {
|
|
9
10
|
existsSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
writeFileSync,
|
|
10
14
|
spawn,
|
|
11
15
|
isJetBrainsNerdFontInstalled,
|
|
12
16
|
installJetBrainsNerdFont,
|
|
@@ -17,24 +21,114 @@ let pixInstallDeps = defaultPixInstallDeps;
|
|
|
17
21
|
export function setPixInstallTestDeps(overrides) {
|
|
18
22
|
pixInstallDeps = overrides ? { ...defaultPixInstallDeps, ...overrides } : defaultPixInstallDeps;
|
|
19
23
|
}
|
|
20
|
-
|
|
24
|
+
const PROVIDER_API_KEY_ENV_NAMES = [
|
|
25
|
+
"ANTHROPIC_API_KEY",
|
|
26
|
+
"AZURE_OPENAI_API_KEY",
|
|
27
|
+
"GEMINI_API_KEY",
|
|
28
|
+
"GOOGLE_API_KEY",
|
|
29
|
+
"GROQ_API_KEY",
|
|
30
|
+
"MISTRAL_API_KEY",
|
|
31
|
+
"OPENAI_API_KEY",
|
|
32
|
+
"OPENROUTER_API_KEY",
|
|
33
|
+
"XAI_API_KEY",
|
|
34
|
+
"ZAI_API_KEY",
|
|
35
|
+
];
|
|
36
|
+
const DEFAULT_TOOLS_CONFIG_JSONC = `{
|
|
37
|
+
"$schema": "https://unpkg.com/pi-ui-extend/schemas/pi-tools-suite.json",
|
|
38
|
+
"todoThinking": true,
|
|
39
|
+
"disabledModules": []
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
export function inspectPixOnboarding(homeDir = homedir(), env = process.env) {
|
|
43
|
+
const pixConfigPath = getPixConfigPath(homeDir);
|
|
44
|
+
const toolsConfigPath = join(homeDir, ".config", "pi", "pi-tools-suite.jsonc");
|
|
45
|
+
const agentDir = env.PI_CODING_AGENT_DIR?.trim() || join(homeDir, ".pi", "agent");
|
|
46
|
+
const piAuthPath = join(agentDir, "auth.json");
|
|
47
|
+
const opencodeDataDir = env.OPENCODE_DATA_DIR?.trim()
|
|
48
|
+
|| join(env.XDG_DATA_HOME?.trim() || join(homeDir, ".local", "share"), "opencode");
|
|
49
|
+
const opencodeConfigDir = env.OPENCODE_CONFIG_DIR?.trim()
|
|
50
|
+
|| join(env.XDG_CONFIG_HOME?.trim() || join(homeDir, ".config"), "opencode");
|
|
51
|
+
const webCredentialPath = env.PI_TOOLS_SUITE_WEB_CREDENTIALS_PATH?.trim()
|
|
52
|
+
|| join(homeDir, ".config", "pi", "pi-tools-suite-credentials.json");
|
|
53
|
+
const providerEnvConfigured = PROVIDER_API_KEY_ENV_NAMES.some((name) => Boolean(env[name]?.trim()));
|
|
54
|
+
return {
|
|
55
|
+
pixConfigExists: pixInstallDeps.existsSync(pixConfigPath),
|
|
56
|
+
toolsConfigExists: pixInstallDeps.existsSync(toolsConfigPath),
|
|
57
|
+
providerAuthConfigured: providerEnvConfigured || jsonObjectHasEntries(piAuthPath),
|
|
58
|
+
opencodeAuthExists: Boolean(env.OPENCODE_AUTH_CONTENT?.trim()) || pixInstallDeps.existsSync(join(opencodeDataDir, "auth.json")),
|
|
59
|
+
opencodeAntigravityExists: pixInstallDeps.existsSync(join(opencodeConfigDir, "antigravity-accounts.json")),
|
|
60
|
+
webCredentialsConfigured: Boolean(env.OLLAMA_API_KEY?.trim() || env.TAVILY_API_KEY?.trim()) || jsonObjectHasEntries(webCredentialPath),
|
|
61
|
+
context7Configured: Boolean(env.CONTEXT7_API_KEY?.trim()),
|
|
62
|
+
telegramConfigured: Boolean(env.PI_TERMINAL_BELL_TELEGRAM_BOT_TOKEN?.trim() && env.PI_TERMINAL_BELL_TELEGRAM_CHAT_ID?.trim()),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function ensurePixSetupFiles(homeDir = homedir()) {
|
|
66
|
+
const files = [
|
|
67
|
+
{ path: getPixConfigPath(homeDir), content: DEFAULT_PIX_CONFIG_JSONC },
|
|
68
|
+
{ path: join(homeDir, ".config", "pi", "pi-tools-suite.jsonc"), content: DEFAULT_TOOLS_CONFIG_JSONC },
|
|
69
|
+
];
|
|
70
|
+
const created = [];
|
|
71
|
+
const existing = [];
|
|
72
|
+
for (const file of files) {
|
|
73
|
+
if (pixInstallDeps.existsSync(file.path)) {
|
|
74
|
+
existing.push(file.path);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
pixInstallDeps.mkdirSync(dirname(file.path), { recursive: true });
|
|
78
|
+
try {
|
|
79
|
+
pixInstallDeps.writeFileSync(file.path, file.content, { encoding: "utf8", flag: "wx" });
|
|
80
|
+
created.push(file.path);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error.code !== "EEXIST")
|
|
84
|
+
throw error;
|
|
85
|
+
existing.push(file.path);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { created, existing };
|
|
89
|
+
}
|
|
90
|
+
export function formatPixInstallNextSteps(homeDir = homedir(), state = inspectPixOnboarding(homeDir)) {
|
|
21
91
|
const pixConfigPath = getPixConfigPath(homeDir);
|
|
22
92
|
const toolsConfigPath = join(homeDir, ".config", "pi", "pi-tools-suite.jsonc");
|
|
93
|
+
const opencodeDetected = state.opencodeAuthExists || state.opencodeAntigravityExists;
|
|
94
|
+
const providerStep = formatProviderSetupStep(state.providerAuthConfigured, opencodeDetected);
|
|
23
95
|
return [
|
|
24
96
|
"",
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
97
|
+
"First-run setup:",
|
|
98
|
+
providerStep,
|
|
99
|
+
...(opencodeDetected ? [
|
|
100
|
+
" /opencode-import safely preserves existing Pi credentials unless --force is used.",
|
|
101
|
+
" Supported migration: OpenAI/Codex, GitHub Copilot, Z.ai, and Antigravity credentials; OpenCode models, MCP, plugins, and tool settings are reported as manual work.",
|
|
102
|
+
] : []),
|
|
103
|
+
` ${state.pixConfigExists ? "✓" : "→"} Pix config: ${pixConfigPath}`,
|
|
104
|
+
` ${state.toolsConfigExists ? "✓" : "→"} Tools config: ${toolsConfigPath}`,
|
|
105
|
+
"",
|
|
106
|
+
"Optional integrations:",
|
|
107
|
+
` ${state.webCredentialsConfigured ? "✓ Web credentials detected." : "○ Web search: use local Ollama without a key, or run /web-credentials for Ollama Cloud/Tavily."}`,
|
|
108
|
+
` ${state.context7Configured ? "✓ Context7 API key detected." : "○ Context7 docs skill: export CONTEXT7_API_KEY."}`,
|
|
109
|
+
` ${state.telegramConfigured ? "✓ Telegram terminal-bell credentials detected in the environment." : "○ Telegram bell: set terminalBell.telegram in Pix config or PI_TERMINAL_BELL_TELEGRAM_* env vars."}`,
|
|
110
|
+
" ○ Model-backed helpers: review promptEnhancer, autocomplete, and sessionTitle model refs; each provider needs its own credentials.",
|
|
111
|
+
" ○ Voice: set dictation.language in Pix config. LSP: configure and trust servers under lsp.servers in tools config.",
|
|
112
|
+
"",
|
|
113
|
+
"Pix never imports or overwrites credentials during install. Optional tokens remain opt-in.",
|
|
30
114
|
].join("\n");
|
|
31
115
|
}
|
|
116
|
+
function formatProviderSetupStep(providerAuthConfigured, opencodeDetected) {
|
|
117
|
+
if (providerAuthConfigured)
|
|
118
|
+
return " ✓ Model provider credentials detected. Start Pix and use /model to choose a model.";
|
|
119
|
+
if (opencodeDetected)
|
|
120
|
+
return " → OpenCode credentials detected. Start Pix, run /opencode-import, then /model.";
|
|
121
|
+
return " → Configure a model provider first: run `npx @earendil-works/pi-coding-agent`, use /login, then start Pix and use /model.";
|
|
122
|
+
}
|
|
32
123
|
export function pixInstallUsage() {
|
|
33
124
|
return `Usage: pix install [--check]
|
|
34
125
|
pix setup [--check]
|
|
35
126
|
|
|
36
127
|
Check and install Pix runtime helpers for this user.
|
|
37
128
|
|
|
129
|
+
It also creates non-secret Pix and pi-tools-suite config templates and prints a
|
|
130
|
+
credential-aware first-run checklist. It never imports credentials automatically.
|
|
131
|
+
|
|
38
132
|
What it checks:
|
|
39
133
|
- ${FONT_FAMILY_NAME} icon font for Pix glyphs
|
|
40
134
|
- pi CLI availability, including Pix's bundled Pi dependency
|
|
@@ -81,7 +175,6 @@ export async function runPixInstallCli(argv = process.argv.slice(2), context = {
|
|
|
81
175
|
}
|
|
82
176
|
else if (options.checkOnly) {
|
|
83
177
|
console.log(`! ${FONT_FAMILY_NAME} is missing`);
|
|
84
|
-
failures += 1;
|
|
85
178
|
}
|
|
86
179
|
else {
|
|
87
180
|
try {
|
|
@@ -89,8 +182,7 @@ export async function runPixInstallCli(argv = process.argv.slice(2), context = {
|
|
|
89
182
|
console.log(`✓ Installed ${FONT_FAMILY_NAME}`);
|
|
90
183
|
}
|
|
91
184
|
catch (error) {
|
|
92
|
-
console.
|
|
93
|
-
failures += 1;
|
|
185
|
+
console.warn(`! Failed to install optional ${FONT_FAMILY_NAME}: ${errorMessage(error)}`);
|
|
94
186
|
}
|
|
95
187
|
}
|
|
96
188
|
const piCli = await resolvePiCliStatus(env);
|
|
@@ -117,12 +209,35 @@ export async function runPixInstallCli(argv = process.argv.slice(2), context = {
|
|
|
117
209
|
}
|
|
118
210
|
else {
|
|
119
211
|
console.log(`! Clipboard support is missing. ${pixInstallDeps.clipboardInstallHint()}`);
|
|
120
|
-
|
|
212
|
+
}
|
|
213
|
+
const homeDir = context.homeDir ?? homedir();
|
|
214
|
+
if (!options.checkOnly) {
|
|
215
|
+
try {
|
|
216
|
+
const setupFiles = ensurePixSetupFiles(homeDir);
|
|
217
|
+
for (const path of setupFiles.created)
|
|
218
|
+
console.log(`✓ Created ${path}`);
|
|
219
|
+
for (const path of setupFiles.existing)
|
|
220
|
+
console.log(`✓ Kept existing ${path}`);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
console.error(`✗ Failed to create setup config: ${errorMessage(error)}`);
|
|
121
224
|
failures += 1;
|
|
225
|
+
}
|
|
122
226
|
}
|
|
123
|
-
console.log(formatPixInstallNextSteps(
|
|
227
|
+
console.log(formatPixInstallNextSteps(homeDir, inspectPixOnboarding(homeDir, env)));
|
|
124
228
|
return failures === 0 ? 0 : 1;
|
|
125
229
|
}
|
|
230
|
+
function jsonObjectHasEntries(path) {
|
|
231
|
+
if (!pixInstallDeps.existsSync(path))
|
|
232
|
+
return false;
|
|
233
|
+
try {
|
|
234
|
+
const parsed = JSON.parse(pixInstallDeps.readFileSync(path, "utf8"));
|
|
235
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && Object.keys(parsed).length > 0;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
126
241
|
async function resolvePiCliStatus(env) {
|
|
127
242
|
const bundledBin = env.PIX_BUNDLED_PI_BIN;
|
|
128
243
|
if (bundledBin && (pixInstallDeps.existsSync(join(bundledBin, process.platform === "win32" ? "pi.cmd" : "pi")) || pixInstallDeps.existsSync(join(bundledBin, "pi")))) {
|
|
@@ -4,5 +4,6 @@ export type StartupAvailabilityIssue = {
|
|
|
4
4
|
message: string;
|
|
5
5
|
};
|
|
6
6
|
export declare function collectStartupAvailabilityIssues(runtime: AgentSessionRuntime): Promise<StartupAvailabilityIssue[]>;
|
|
7
|
+
export declare function checkSelectedModelAuthAvailability(runtime: AgentSessionRuntime): StartupAvailabilityIssue[];
|
|
7
8
|
export declare function checkPiCliAvailability(pathValue?: string): Promise<StartupAvailabilityIssue[]>;
|
|
8
9
|
export declare function checkPiToolsSuiteExtensionAvailability(extensionsResult: LoadExtensionsResult): StartupAvailabilityIssue[];
|
|
@@ -7,8 +7,24 @@ export async function collectStartupAvailabilityIssues(runtime) {
|
|
|
7
7
|
return [
|
|
8
8
|
...(await checkPiCliAvailability()),
|
|
9
9
|
...checkPiToolsSuiteExtensionAvailability(runtime.services.resourceLoader.getExtensions()),
|
|
10
|
+
...checkSelectedModelAuthAvailability(runtime),
|
|
10
11
|
];
|
|
11
12
|
}
|
|
13
|
+
export function checkSelectedModelAuthAvailability(runtime) {
|
|
14
|
+
const model = runtime.session.model;
|
|
15
|
+
if (!model) {
|
|
16
|
+
return [{
|
|
17
|
+
kind: "error",
|
|
18
|
+
message: "No model is selected. Configure a provider with `npx @earendil-works/pi-coding-agent` and /login (or run /opencode-import), then choose /model in Pix.",
|
|
19
|
+
}];
|
|
20
|
+
}
|
|
21
|
+
if (runtime.services.modelRuntime.getProviderAuthStatus(model.provider).configured)
|
|
22
|
+
return [];
|
|
23
|
+
return [{
|
|
24
|
+
kind: "error",
|
|
25
|
+
message: `Selected model ${model.provider}/${model.id} has no configured credentials. Use \`npx @earendil-works/pi-coding-agent\` and /login, or run /opencode-import for supported OpenCode accounts, then choose /model.`,
|
|
26
|
+
}];
|
|
27
|
+
}
|
|
12
28
|
export async function checkPiCliAvailability(pathValue = process.env.PATH ?? "") {
|
|
13
29
|
if (await executableExistsOnPath(PI_CLI_COMMAND, pathValue))
|
|
14
30
|
return [];
|
package/dist/app/runtime.d.ts
CHANGED
|
@@ -12,11 +12,13 @@ export type PiToolsSuiteInstallOptions = {
|
|
|
12
12
|
sourcePath?: string;
|
|
13
13
|
targetPath?: string;
|
|
14
14
|
};
|
|
15
|
-
export type BundledSkillsInstallAction = "installed" | "already-installed" | "missing-source";
|
|
15
|
+
export type BundledSkillsInstallAction = "installed" | "already-installed" | "existing-kept" | "missing-source";
|
|
16
16
|
export type BundledSkillsInstallResult = {
|
|
17
17
|
action: BundledSkillsInstallAction;
|
|
18
18
|
sourcePath: string;
|
|
19
19
|
targetPath: string;
|
|
20
|
+
installedSkills: string[];
|
|
21
|
+
preservedSkills: string[];
|
|
20
22
|
};
|
|
21
23
|
export type BundledSkillsInstallOptions = {
|
|
22
24
|
homeDir?: string;
|
package/dist/app/runtime.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { access, cp, lstat, mkdir, readlink, realpath, rm, symlink } from "node:fs/promises";
|
|
2
|
+
import { access, cp, lstat, mkdir, readdir, readlink, realpath, rm, symlink } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -68,14 +68,31 @@ export async function ensureBundledSkillsInstalled(options = {}) {
|
|
|
68
68
|
const targetPath = resolve(options.targetPath ?? bundledSkillsInstallPath(options.homeDir));
|
|
69
69
|
const sourceStat = await lstat(sourcePath).catch(() => undefined);
|
|
70
70
|
if (!sourceStat?.isDirectory()) {
|
|
71
|
-
return { action: "missing-source", sourcePath, targetPath };
|
|
71
|
+
return { action: "missing-source", sourcePath, targetPath, installedSkills: [], preservedSkills: [] };
|
|
72
72
|
}
|
|
73
73
|
if (await pathsReferToSameEntry(sourcePath, targetPath)) {
|
|
74
|
-
return { action: "already-installed", sourcePath, targetPath };
|
|
74
|
+
return { action: "already-installed", sourcePath, targetPath, installedSkills: [], preservedSkills: [] };
|
|
75
75
|
}
|
|
76
|
-
await mkdir(
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
await mkdir(targetPath, { recursive: true });
|
|
77
|
+
const installedSkills = [];
|
|
78
|
+
const preservedSkills = [];
|
|
79
|
+
for (const entry of await readdir(sourcePath, { withFileTypes: true })) {
|
|
80
|
+
if (!entry.isDirectory())
|
|
81
|
+
continue;
|
|
82
|
+
const skillTarget = join(targetPath, entry.name);
|
|
83
|
+
if (await lstat(skillTarget).catch(() => undefined)) {
|
|
84
|
+
preservedSkills.push(entry.name);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
await cp(join(sourcePath, entry.name), skillTarget, { recursive: true, errorOnExist: true, force: false });
|
|
88
|
+
installedSkills.push(entry.name);
|
|
89
|
+
}
|
|
90
|
+
let action = "already-installed";
|
|
91
|
+
if (installedSkills.length > 0)
|
|
92
|
+
action = "installed";
|
|
93
|
+
else if (preservedSkills.length > 0)
|
|
94
|
+
action = "existing-kept";
|
|
95
|
+
return { action, sourcePath, targetPath, installedSkills, preservedSkills };
|
|
79
96
|
}
|
|
80
97
|
export function getBundledExtensionPaths() {
|
|
81
98
|
return [
|
|
@@ -12,6 +12,7 @@ This package keeps shared Pi tools as ordinary source folders under `src/` and r
|
|
|
12
12
|
- `src/session-name` — `session_name` tool for reading or setting the current session title directly from tool calls, without relying on slash-command parsing
|
|
13
13
|
- `src/repo-discovery` — `/idx-init`, `/idx-update`, and indexed-only `repo_architecture` / `repo_structure` / `repo_ast` / `repo_search` / `repo_explain` / `repo_deps`; tools register only when the launch project has `.indexer-cli`
|
|
14
14
|
- `src/antigravity-auth` — `antigravity` custom provider with Google Antigravity OAuth login, startup account list, auth.json-only runtime account loading, `/antigravity-add-account` OAuth append into rotation, `/antigravity-account` status display, account rotation/failover, Antigravity plus Gemini CLI model registration, and streaming through the Cloud Code Assist unified gateway
|
|
15
|
+
- `src/opencode-import` — `/opencode-import` for bounded migration of supported OpenCode OpenAI/Codex, GitHub Copilot, Z.ai, and Antigravity credentials into Pi; existing entries are preserved unless `--force` is passed
|
|
15
16
|
- `src/todo` — `todo` tool, `/todos`, `/todos-persist`, and `/todos-scope`; supports parent/subtask hierarchy, blockers, ready-task filtering, deferred out-of-scope items, batch operations, JSON/Markdown import/export, automatic clearing when all visible todos are completed, and optional project persistence via `/todos persist on` or `/todos-persist on`; localization/i18n has been removed
|
|
16
17
|
- `src/model-tools` — model-specific tool aliases such as Claude/GLM-style `Read` / `Edit` / `Write` / `Bash` / `Grep` / `Glob` / `LS`, GPT/Codex-style `shell`, and model-gated `apply_patch`
|
|
17
18
|
- `src/usage` — `/usage` command and startup hint for read-only AI quota checks across OpenAI, Zhipu AI, Z.ai, and Google Antigravity, including Antigravity quota by model
|
|
@@ -22,7 +23,7 @@ This package keeps shared Pi tools as ordinary source folders under `src/` and r
|
|
|
22
23
|
|
|
23
24
|
`index.ts` is intentionally only a thin auto-discovery shim that re-exports `src/index.ts`. There is no `pi.extensions` manifest here, so local Pi auto-discovery loads the suite once via `~/.pi/agent/extensions/pi-tools-suite/index.ts` and does not double-register tools.
|
|
24
25
|
|
|
25
|
-
Registration order is preserved in `src/index.ts`: coding-discipline, ast-grep, async-subagents, lsp, comment-checker, session-name, repo-discovery command/tool gate, antigravity-auth provider, todo, model-tools, usage, web-search, dcp, prompt-commands, then skill-installer. Tool metadata and active model-specific tool sets have two modes: standard and repo-aware. When `.indexer-cli` enables `repo_*`, those tools stay active ahead of overlapping lower-level aliases so the indexed discovery surface has priority.
|
|
26
|
+
Registration order is preserved in `src/index.ts`: coding-discipline, ast-grep, async-subagents, lsp, comment-checker, session-name, repo-discovery command/tool gate, antigravity-auth provider, OpenCode import, todo, model-tools, usage, web-search, dcp, prompt-commands, then skill-installer. Tool metadata and active model-specific tool sets have two modes: standard and repo-aware. When `.indexer-cli` enables `repo_*`, those tools stay active ahead of overlapping lower-level aliases so the indexed discovery surface has priority.
|
|
26
27
|
|
|
27
28
|
## Disabling modules
|
|
28
29
|
|
|
@@ -303,6 +304,12 @@ Runtime logs are minimized by default: successful agents do not keep `events.jso
|
|
|
303
304
|
|
|
304
305
|
`asyncSubagents` config also supports `maxConcurrent` (default 5, project-wide; `0` means unlimited), global/per-type `retry` with exponential backoff, global/per-type `maxResultBytes` for bounding `result.json.resultText` while keeping raw `result.md` intact, and global/per-type/preset `timeoutMs` for wall-clock agent watchdogs. Spawn calls and individual task objects can pass `timeoutSeconds` to shorten the watchdog for synthetic tests or bounded probes. Stop requests mark running, queued planned, and retry-pending agents as `stopped` so queued work is not launched later. Completed agents write `result.json` with status/duration/model/retry metadata plus best-effort `summary`, `findings`, `files`, `risks`, `nextActions`, and `confidence` fields for parent-agent chaining.
|
|
305
306
|
|
|
307
|
+
## OpenCode credential import
|
|
308
|
+
|
|
309
|
+
`/opencode-import` migrates only credential formats with an explicit Pi mapping: OpenAI OAuth to `openai-codex`, OpenAI API keys to `openai`, GitHub Copilot OAuth, Z.ai aliases, and one matching OpenCode Antigravity account. It does not migrate OpenCode model definitions, defaults, MCP servers, plugins, instructions, or tool settings.
|
|
310
|
+
|
|
311
|
+
The default source is `OPENCODE_AUTH_CONTENT`, `OPENCODE_DATA_DIR/auth.json`, or the XDG/default OpenCode data directory. The destination is the active Pi agent directory, including `PI_CODING_AGENT_DIR`. Existing Pi credentials are preserved; pass `--force` only to replace supported entries intentionally. Successful writes are atomic, use restrictive file permissions, and trigger a runtime reload. The command also accepts `--path`, `--auth-path`, `--antigravity-path`, `--skip-auth-json`, and `--skip-antigravity` for controlled migrations.
|
|
312
|
+
|
|
306
313
|
## Web search
|
|
307
314
|
|
|
308
315
|
`src/web-search` registers two Ollama-first tools and the `/web-credentials` setup command:
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { promises as fs } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
6
|
import { DEFAULT_PROJECT_ID, PROVIDER_ID } from "./constants";
|
|
5
7
|
import type { GoogleOAuthClientCredentials, OpencodeAntigravityAccount, OpencodeAntigravityImportResult, OpencodeAntigravityStorage, PiAuthCredential, PiAuthData } from "./types";
|
|
6
8
|
|
|
7
|
-
export const PI_AUTH_PATH = join(
|
|
9
|
+
export const PI_AUTH_PATH = join(getAgentDir(), "auth.json");
|
|
8
10
|
|
|
9
11
|
function testPiAuthPath(): string | undefined {
|
|
10
12
|
return process.env.NODE_ENV === "test" ? process.env.PI_TOOLS_SUITE_TEST_AUTH_PATH : undefined;
|
|
@@ -48,7 +50,7 @@ export async function importDefaultOpencodeAntigravityAccount(options: { overwri
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export function getPiAuthPath(): string {
|
|
51
|
-
return testPiAuthPath() ??
|
|
53
|
+
return testPiAuthPath() ?? join(getAgentDir(), "auth.json");
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export async function readJsonFile<T>(path: string, fallback: T): Promise<T> {
|
|
@@ -61,9 +63,21 @@ export async function readJsonFile<T>(path: string, fallback: T): Promise<T> {
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
export async function writeJsonFileSecure(path: string, data: unknown): Promise<void> {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
await fs.
|
|
66
|
+
const directory = dirname(path);
|
|
67
|
+
const temporaryPath = join(directory, `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
|
|
68
|
+
await fs.mkdir(directory, { recursive: true });
|
|
69
|
+
try {
|
|
70
|
+
await fs.writeFile(temporaryPath, `${JSON.stringify(data, null, 2)}\n`, {
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
flag: "wx",
|
|
73
|
+
mode: 0o600,
|
|
74
|
+
});
|
|
75
|
+
await fs.rename(temporaryPath, path);
|
|
76
|
+
await fs.chmod(path, 0o600).catch(() => undefined);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
export function getStoredAccounts(credential?: PiAuthCredential): OpencodeAntigravityAccount[] {
|
|
@@ -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
|
|
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
|
-
|
|
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" | "
|
|
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 "
|
|
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
|
|
64
|
+
label: "OpenAI",
|
|
65
65
|
sourceProvider: "openai",
|
|
66
|
-
targetProvider: "openai-codex",
|
|
67
|
-
transform:
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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[
|
|
190
|
+
const existingCredential = piAuth[resolvedTargetProvider];
|
|
163
191
|
if (sameCredential(existingCredential, nextCredential)) {
|
|
164
|
-
result.providers.push(providerResult(mapping, "already-imported"));
|
|
165
|
-
changedTargets.add(
|
|
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(
|
|
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, [
|
|
176
|
-
changedTargets.add(
|
|
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
|
|
12
|
-
handler: async (args: string, ctx:
|
|
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
|
});
|
package/package.json
CHANGED
package/skills/context7/SKILL.md
CHANGED
|
@@ -9,6 +9,16 @@ This skill fetches current documentation and code examples for any programming l
|
|
|
9
9
|
|
|
10
10
|
Context7 indexes documentation from official sources and provides version-specific, relevant excerpts — much more reliable than relying on potentially outdated training data.
|
|
11
11
|
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
Create a Context7 API key in the Context7 dashboard and export it before using the skill:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
export CONTEXT7_API_KEY="..."
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The script does not contain a fallback key and exits before making a network request when the variable is missing. It also requires `curl` and `jq` on `PATH`.
|
|
21
|
+
|
|
12
22
|
## Two-step workflow
|
|
13
23
|
|
|
14
24
|
### Step 1: Resolve the library ID
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
# 1. resolve — find the Context7 library ID for a package
|
|
11
11
|
# 2. docs — query documentation for a resolved library ID
|
|
12
12
|
#
|
|
13
|
-
# Requires CONTEXT7_API_KEY env var
|
|
13
|
+
# Requires CONTEXT7_API_KEY env var.
|
|
14
14
|
|
|
15
15
|
set -euo pipefail
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
: "${CONTEXT7_API_KEY:?Context7 requires CONTEXT7_API_KEY. Create a key at https://context7.com/dashboard and export it before using this skill.}"
|
|
18
|
+
API_KEY="$CONTEXT7_API_KEY"
|
|
18
19
|
BASE_URL="https://mcp.context7.com/mcp"
|
|
19
20
|
|
|
20
21
|
call_mcp() {
|