pi-ui-extend 0.1.74 → 0.1.78
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 +5 -2
- package/dist/app/runtime.js +29 -7
- 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/external/pi-tools-suite/src/tool-descriptions.ts +2 -2
- 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SessionManager, type EventBus, type AgentSessionRuntime, type LoadExtensionsResult, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { SessionManager, type EventBus, type AgentSessionRuntime, type AgentSessionServices, type LoadExtensionsResult, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type PixConfig } from "../config.js";
|
|
3
3
|
import type { AppOptions, ThinkingLevel } from "./types.js";
|
|
4
4
|
export type PiToolsSuiteInstallAction = "installed" | "already-installed" | "existing-kept" | "missing-source";
|
|
@@ -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;
|
|
@@ -44,5 +46,6 @@ type RuntimeSessionManagerModelState = Pick<SessionManager, "getEntries" | "getB
|
|
|
44
46
|
export declare function resolvePixRuntimeModelRef(options: Pick<AppOptions, "modelRef">, sessionManager: RuntimeSessionManagerModelState, config?: PixConfig): string | undefined;
|
|
45
47
|
export declare function resolvePixRuntimeInitialThinkingLevel(options: Pick<AppOptions, "modelRef">, sessionManager: RuntimeSessionManagerModelState, config: PixConfig): ThinkingLevel | undefined;
|
|
46
48
|
export declare function resolveSessionModelRefFromTail(entries: readonly SessionEntry[]): string | undefined;
|
|
49
|
+
export declare function refreshPixModelRuntimeForStartup(modelRuntime: Pick<AgentSessionServices["modelRuntime"], "refresh">): Promise<void>;
|
|
47
50
|
export declare function createPixRuntime(options: AppOptions, runtimeOptions?: CreatePixRuntimeOptions): Promise<AgentSessionRuntime>;
|
|
48
51
|
export {};
|
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 [
|
|
@@ -218,6 +235,11 @@ export function resolveSessionModelRefFromTail(entries) {
|
|
|
218
235
|
return undefined;
|
|
219
236
|
return thinkingLevel ? `${modelRef}:${thinkingLevel}` : modelRef;
|
|
220
237
|
}
|
|
238
|
+
export async function refreshPixModelRuntimeForStartup(modelRuntime) {
|
|
239
|
+
// Startup only needs the locally configured model catalog. Remote catalog
|
|
240
|
+
// refreshes belong to explicit model-management flows and must not block boot.
|
|
241
|
+
await modelRuntime.refresh({ allowNetwork: false });
|
|
242
|
+
}
|
|
221
243
|
export async function createPixRuntime(options, runtimeOptions = {}) {
|
|
222
244
|
const agentDir = getAgentDir();
|
|
223
245
|
const reusableServices = reusableRuntimeServices(runtimeOptions.reuseServicesFrom, options.cwd, agentDir);
|
|
@@ -239,7 +261,7 @@ export async function createPixRuntime(options, runtimeOptions = {}) {
|
|
|
239
261
|
config,
|
|
240
262
|
...(runtimeOptions.eventBus === undefined ? {} : { eventBus: runtimeOptions.eventBus }),
|
|
241
263
|
});
|
|
242
|
-
await services.modelRuntime
|
|
264
|
+
await refreshPixModelRuntimeForStartup(services.modelRuntime);
|
|
243
265
|
const model = parsedModel ? services.modelRuntime.getModel(parsedModel.provider, parsedModel.modelId) : undefined;
|
|
244
266
|
if (parsedModel && !model) {
|
|
245
267
|
throw new Error(`Model not found: ${parsedModel.provider}/${parsedModel.modelId}`);
|
|
@@ -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
|
});
|
|
@@ -221,10 +221,10 @@ export const TODO_TOOL_DESCRIPTION: ToolDescription = {
|
|
|
221
221
|
"Use `todo` for complex work with 3+ steps, explicit user task lists, or new non-trivial requirements. Skip single trivial tasks and purely conversational requests.",
|
|
222
222
|
"For multi-step implementation/debugging plans, include a final user-facing report todo in the initial plan with acceptance criteria for changed files/behavior, verification results, and remaining manual actions; close it immediately before the final response, never via compression.",
|
|
223
223
|
"When create or batch_create already sets the intended status, do not issue a redundant update with the same status; continue the work instead.",
|
|
224
|
-
"Resync before continuing when user/new findings change scope, requirements, safety, feasibility, approach, dependencies, or order
|
|
224
|
+
"Resync before continuing when user/new findings change scope, requirements, safety, feasibility, approach, dependencies, or order. Before creating tasks, review pending/deferred todos; when work resumes, reactivate and reuse an equivalent deferred todo instead of duplicating it.",
|
|
225
225
|
"Update todos when starting, finishing, blocking, splitting, abandoning, or materially changing a step; before planned work mark exactly one in_progress with activeForm and complete it only after verification.",
|
|
226
226
|
"If partial, tests fail, or blocked, keep the task in_progress and add/update a blocker. Never use `clear`, `delete`, or batch deletion to hide unfinished/stale/forgotten todos; delete only on explicit request or creation mistake.",
|
|
227
|
-
"Before a final response
|
|
227
|
+
"Before a final response, list and reconcile all visible todos, including deferred ones: complete any whose outcome was achieved elsewhere, and leave deferred only genuinely unfinished or intentionally out of scope, mentioning each in the response. Do not finish with stale/duplicate deferred todos or a just-finished item in_progress.",
|
|
228
228
|
"Keep subjects short; use parentId/blockers for hierarchy/dependencies. For large explicit plans use batch_create/batch_update but keep exactly one visible in_progress unless asked otherwise; use batch_create replace:true only for superseding plans.",
|
|
229
229
|
"list hides tombstones unless includeDeleted:true; use status/blockedOnly only when needed; export/import for handoff, import replace:true only when explicitly overwriting; when all visible todos complete, state clears automatically.",
|
|
230
230
|
"Persistence: `/todos persist on|off|status` or `/todos-persist on|off|status`; on resume, ask in-scope ids and run `/todos scope <id...>` or `/todos-scope <id...>` so out-of-scope active tasks are deferred.",
|
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() {
|