@jameslovespancakes/pi-plus 1.0.0 → 1.0.1
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/LICENSE +21 -21
- package/README.md +190 -190
- package/config/pi-plus.example.json +60 -60
- package/config/skills/model-routing/SKILL.md +86 -86
- package/images/pi-plus.svg +10 -10
- package/package.json +67 -67
- package/server/board-server.mjs +641 -641
- package/server/package.json +17 -17
- package/src/core/accounts/registry.ts +93 -93
- package/src/core/anthropic/client-identity.ts +241 -241
- package/src/core/catalog/quality.ts +314 -314
- package/src/core/config.ts +169 -169
- package/src/core/env.ts +58 -58
- package/src/core/exec/process.ts +146 -146
- package/src/core/exec/ssh-config.ts +157 -157
- package/src/core/policy/policy.ts +183 -183
- package/src/core/quota/pool.ts +64 -64
- package/src/core/quota/usage-source.ts +289 -289
- package/src/core/store.ts +43 -43
- package/src/domains/agents/board-setup.ts +409 -409
- package/src/domains/agents/index.ts +462 -462
- package/src/domains/models/catalog-tool.ts +361 -361
- package/src/domains/models/index.ts +14 -14
- package/src/domains/models/policy-gate.ts +169 -169
- package/src/domains/models/provider-picker.ts +207 -207
- package/src/domains/remote/config-path.ts +41 -41
- package/src/domains/remote/index.ts +866 -866
- package/src/domains/remote/setup.ts +425 -425
- package/src/domains/setup/index.ts +220 -220
- package/src/domains/subscriptions/accounts.ts +242 -242
- package/src/domains/subscriptions/footer.ts +182 -182
- package/src/domains/subscriptions/index.ts +42 -42
- package/src/domains/subscriptions/provider.ts +219 -219
- package/src/domains/subscriptions/providers/anthropic.ts +149 -149
- package/src/domains/subscriptions/providers/codex.ts +148 -148
- package/src/domains/subscriptions/routing.ts +72 -72
- package/src/services/usage-service.ts +186 -186
- package/src/ui/format.ts +73 -73
- package/src/ui/usage-bars.ts +154 -154
- package/src/vendor/anthropic.ts +109 -109
|
@@ -1,220 +1,220 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { env } from "../../core/env.ts";
|
|
4
|
-
import { agentPath, readJson } from "../../core/store.ts";
|
|
5
|
-
import { configPath, readConfig } from "../../core/config.ts";
|
|
6
|
-
import { accountProviders } from "../../core/accounts/registry.ts";
|
|
7
|
-
import { readSshHosts } from "../../core/exec/ssh-config.ts";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* `/pi-plus` is the status modal for the pack; `/pi-plus help` explains it.
|
|
11
|
-
*
|
|
12
|
-
* The modal lists every feature with its live state. Selecting an unconfigured
|
|
13
|
-
* one runs its setup command; selecting a ready one opens its hub. `help` hands
|
|
14
|
-
* the detected state to the model so the explanation is specific to this
|
|
15
|
-
* machine rather than a static README dump.
|
|
16
|
-
*
|
|
17
|
-
* Only `core/` is read, so this stays inside the layering rules: it never
|
|
18
|
-
* reaches into another domain.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
interface Feature {
|
|
22
|
-
name: string;
|
|
23
|
-
ready: boolean;
|
|
24
|
-
detail: string;
|
|
25
|
-
commands: string[];
|
|
26
|
-
/** Command that configures this feature, when it is not ready. */
|
|
27
|
-
setup?: string;
|
|
28
|
-
/** Command that opens this feature once it is ready. */
|
|
29
|
-
open: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function inspect(ctx: any): Promise<Feature[]> {
|
|
33
|
-
const config = readConfig();
|
|
34
|
-
const features: Feature[] = [];
|
|
35
|
-
|
|
36
|
-
/* subscriptions */
|
|
37
|
-
const providers = accountProviders();
|
|
38
|
-
const accountSummary: string[] = [];
|
|
39
|
-
let anySignedIn = false;
|
|
40
|
-
for (const provider of providers) {
|
|
41
|
-
// pi holds the primary credential itself; the adapter only knows about the
|
|
42
|
-
// extra pooled accounts. Being signed in at all is what makes this usable.
|
|
43
|
-
let primary = false;
|
|
44
|
-
try {
|
|
45
|
-
primary = !!(await ctx.modelRegistry.getProviderAuth(provider.id))?.auth?.apiKey;
|
|
46
|
-
} catch { /* provider not configured */ }
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const accounts = await provider.list();
|
|
50
|
-
const routing = provider.routing ? await provider.routing.get() : "n/a";
|
|
51
|
-
const pooled = accounts.length + (primary ? 1 : 0);
|
|
52
|
-
anySignedIn ||= primary || accounts.length > 0;
|
|
53
|
-
accountSummary.push(
|
|
54
|
-
primary || accounts.length > 0
|
|
55
|
-
? `${provider.id}: ${pooled} account(s), routing=${routing}`
|
|
56
|
-
: `${provider.id}: not signed in`,
|
|
57
|
-
);
|
|
58
|
-
} catch {
|
|
59
|
-
accountSummary.push(`${provider.id}: unreadable`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
features.push({
|
|
63
|
-
name: "Subscriptions",
|
|
64
|
-
ready: anySignedIn,
|
|
65
|
-
detail: accountSummary.join("; ") || "no account providers registered",
|
|
66
|
-
commands: ["/account", "/account <provider> add", "/routing standard|optimal", "/usage"],
|
|
67
|
-
setup: anySignedIn ? undefined : "/account anthropic add",
|
|
68
|
-
open: "/account",
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
/* benchmarks */
|
|
72
|
-
const hasKey = !!env("ARTIFICIAL_ANALYSIS_API_KEY");
|
|
73
|
-
const cache = readJson<{ records?: Record<string, unknown>; checkedAt?: number; fetchedAt?: number }>(
|
|
74
|
-
agentPath("model-quality.json"),
|
|
75
|
-
{},
|
|
76
|
-
);
|
|
77
|
-
const records = Object.keys(cache.records ?? {}).length;
|
|
78
|
-
// Caches written before the store rewrite carry `fetchedAt`.
|
|
79
|
-
const refreshedAt = cache.checkedAt ?? cache.fetchedAt;
|
|
80
|
-
const ageHours = refreshedAt ? Math.round((Date.now() - refreshedAt) / 3.6e6) : undefined;
|
|
81
|
-
features.push({
|
|
82
|
-
name: "Model Information",
|
|
83
|
-
ready: hasKey && records > 0,
|
|
84
|
-
detail: hasKey
|
|
85
|
-
? `${records} models cached${ageHours !== undefined ? `, refreshed ${ageHours}h ago` : ""}`
|
|
86
|
-
: "no Artificial Analysis API key",
|
|
87
|
-
commands: ["/models", "/model-info <id>", "/model-info refresh", "list_models (tool)"],
|
|
88
|
-
setup: hasKey ? undefined : "/model-info setup",
|
|
89
|
-
open: "/models",
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
/* spend policy */
|
|
93
|
-
features.push({
|
|
94
|
-
name: "Providers",
|
|
95
|
-
ready: config.policy.requireApproval.length > 0,
|
|
96
|
-
detail: `${config.policy.requireApproval.length} gated pattern(s), ${config.policy.autoApprove.length} auto-approved`,
|
|
97
|
-
commands: ["/provider", "/provider list", "/provider approve <name>"],
|
|
98
|
-
open: "/provider",
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
/* agent board */
|
|
102
|
-
const boardUrl = env("AGENT_BOARD_URL");
|
|
103
|
-
const boardReady = !!boardUrl && !!env("AGENT_BOARD_TOKEN");
|
|
104
|
-
// A board configured before /board setup existed has no recorded mode; it is
|
|
105
|
-
// externally managed by definition.
|
|
106
|
-
const mode = env("AGENT_BOARD_MODE") ?? (boardReady ? "external" : "none");
|
|
107
|
-
features.push({
|
|
108
|
-
name: "Agent Board",
|
|
109
|
-
ready: boardReady,
|
|
110
|
-
detail: boardUrl ? `${mode} at ${boardUrl}` : "no board configured",
|
|
111
|
-
commands: ["/board", "/board setup", "/board restart", "/board clear", "agent_board (tool)"],
|
|
112
|
-
setup: boardReady ? undefined : "/board setup",
|
|
113
|
-
open: "/board status",
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
/* remote workers */
|
|
117
|
-
const workers = config.remote.workers ?? [];
|
|
118
|
-
const enabled = workers.filter((worker) => (worker as { enabled?: boolean }).enabled !== false);
|
|
119
|
-
let sshHosts = 0;
|
|
120
|
-
try {
|
|
121
|
-
sshHosts = readSshHosts().length;
|
|
122
|
-
} catch { /* no ssh config */ }
|
|
123
|
-
features.push({
|
|
124
|
-
name: "Remote Workers",
|
|
125
|
-
ready: enabled.length > 0,
|
|
126
|
-
detail: enabled.length > 0
|
|
127
|
-
? `${enabled.length} enabled of ${workers.length} configured`
|
|
128
|
-
: `none enabled${sshHosts > 0 ? ` (${sshHosts} host(s) available in ~/.ssh/config)` : ""}`,
|
|
129
|
-
commands: ["/remote setup", "/remote add", "remote_test (tool)", "remote_status (tool)"],
|
|
130
|
-
setup: enabled.length > 0 ? undefined : "/remote setup",
|
|
131
|
-
open: "/remote setup",
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
return features;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Unconfigured features sort first: the work to do is what you see first. */
|
|
138
|
-
function ordered(features: Feature[]): Feature[] {
|
|
139
|
-
return [...features].sort((a, b) => Number(a.ready) - Number(b.ready));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function renderRow(feature: Feature, width: number): string {
|
|
143
|
-
return `[${feature.ready ? "✓" : " "}] ${feature.name.padEnd(width)} ${feature.detail}`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function buildBrief(features: Feature[]): string {
|
|
147
|
-
const lines = features.map((feature) => {
|
|
148
|
-
const parts = [
|
|
149
|
-
`- ${feature.name}: ${feature.ready ? "READY" : "NOT SET UP"}`,
|
|
150
|
-
` state: ${feature.detail}`,
|
|
151
|
-
` commands: ${feature.commands.join(", ")}`,
|
|
152
|
-
];
|
|
153
|
-
if (feature.setup) parts.push(` to enable: ${feature.setup}`);
|
|
154
|
-
return parts.join("\n");
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const pending = features.filter((feature) => !feature.ready);
|
|
158
|
-
|
|
159
|
-
return [
|
|
160
|
-
"The user just ran /pi-plus. Give them a short, friendly orientation to the pi-plus extension pack.",
|
|
161
|
-
"",
|
|
162
|
-
"Detected state on this machine:",
|
|
163
|
-
"",
|
|
164
|
-
...lines,
|
|
165
|
-
"",
|
|
166
|
-
`Config file: ${configPath()}${existsSync(configPath()) ? "" : " (not created yet)"}`,
|
|
167
|
-
"",
|
|
168
|
-
"Write the reply yourself, in chat. Requirements:",
|
|
169
|
-
"1. One short sentence on what pi-plus is: four capabilities in one package.",
|
|
170
|
-
"2. A compact list of the capabilities, each with one line on what it does and the command to try. Mark which are already working.",
|
|
171
|
-
pending.length > 0
|
|
172
|
-
? `3. Then a short 'Set these up next' section covering ONLY the ones marked NOT SET UP (${pending.map((f) => f.name).join(", ")}), each with the single command to run and one line on what it will ask for.`
|
|
173
|
-
: "3. Note that everything is already configured, and suggest one or two commands worth trying.",
|
|
174
|
-
"4. Keep it under ~250 words. No preamble, no headings deeper than one level, no invented features.",
|
|
175
|
-
"5. Do not call any tools. Just answer.",
|
|
176
|
-
].join("\n");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export default function setupGuide(pi: ExtensionAPI) {
|
|
180
|
-
pi.registerCommand("pi-plus", {
|
|
181
|
-
description: "Status for every pi-plus feature, or `help` for an explanation",
|
|
182
|
-
getArgumentCompletions: (prefix) =>
|
|
183
|
-
"help".startsWith(prefix) ? [{ value: "help", label: "help: explain the extension and what is missing" }] : [],
|
|
184
|
-
handler: async (args, ctx) => {
|
|
185
|
-
const features = await inspect(ctx);
|
|
186
|
-
|
|
187
|
-
// `/pi-plus help` asks the model to explain the pack and what is missing.
|
|
188
|
-
if (args.trim().toLowerCase() === "help") {
|
|
189
|
-
await pi.sendUserMessage(buildBrief(features));
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Everything else is the status modal. ui.select needs a TTY, so headless
|
|
194
|
-
// sessions fall back to the same information as plain text.
|
|
195
|
-
const rows = ordered(features);
|
|
196
|
-
if (!ctx.hasUI) {
|
|
197
|
-
ctx.ui.notify(
|
|
198
|
-
rows
|
|
199
|
-
.map((feature) => `${feature.ready ? "[ready]" : "[setup]"} ${feature.name}\n ${feature.detail}`)
|
|
200
|
-
.join("\n"),
|
|
201
|
-
"info",
|
|
202
|
-
);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const width = Math.max(...rows.map((feature) => feature.name.length));
|
|
207
|
-
const labels = rows.map((feature) => renderRow(feature, width));
|
|
208
|
-
const choice = await ctx.ui.select("pi-plus", labels);
|
|
209
|
-
if (!choice) return;
|
|
210
|
-
|
|
211
|
-
const picked = rows[labels.indexOf(choice)];
|
|
212
|
-
if (!picked) return;
|
|
213
|
-
|
|
214
|
-
// Dispatch through the command pipeline rather than importing another
|
|
215
|
-
// domain, which would break the no-cross-domain-imports rule.
|
|
216
|
-
const command = picked.ready ? picked.open : (picked.setup ?? picked.open);
|
|
217
|
-
await pi.sendUserMessage(command, { expandPromptTemplates: true });
|
|
218
|
-
},
|
|
219
|
-
});
|
|
220
|
-
}
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { env } from "../../core/env.ts";
|
|
4
|
+
import { agentPath, readJson } from "../../core/store.ts";
|
|
5
|
+
import { configPath, readConfig } from "../../core/config.ts";
|
|
6
|
+
import { accountProviders } from "../../core/accounts/registry.ts";
|
|
7
|
+
import { readSshHosts } from "../../core/exec/ssh-config.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `/pi-plus` is the status modal for the pack; `/pi-plus help` explains it.
|
|
11
|
+
*
|
|
12
|
+
* The modal lists every feature with its live state. Selecting an unconfigured
|
|
13
|
+
* one runs its setup command; selecting a ready one opens its hub. `help` hands
|
|
14
|
+
* the detected state to the model so the explanation is specific to this
|
|
15
|
+
* machine rather than a static README dump.
|
|
16
|
+
*
|
|
17
|
+
* Only `core/` is read, so this stays inside the layering rules: it never
|
|
18
|
+
* reaches into another domain.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
interface Feature {
|
|
22
|
+
name: string;
|
|
23
|
+
ready: boolean;
|
|
24
|
+
detail: string;
|
|
25
|
+
commands: string[];
|
|
26
|
+
/** Command that configures this feature, when it is not ready. */
|
|
27
|
+
setup?: string;
|
|
28
|
+
/** Command that opens this feature once it is ready. */
|
|
29
|
+
open: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function inspect(ctx: any): Promise<Feature[]> {
|
|
33
|
+
const config = readConfig();
|
|
34
|
+
const features: Feature[] = [];
|
|
35
|
+
|
|
36
|
+
/* subscriptions */
|
|
37
|
+
const providers = accountProviders();
|
|
38
|
+
const accountSummary: string[] = [];
|
|
39
|
+
let anySignedIn = false;
|
|
40
|
+
for (const provider of providers) {
|
|
41
|
+
// pi holds the primary credential itself; the adapter only knows about the
|
|
42
|
+
// extra pooled accounts. Being signed in at all is what makes this usable.
|
|
43
|
+
let primary = false;
|
|
44
|
+
try {
|
|
45
|
+
primary = !!(await ctx.modelRegistry.getProviderAuth(provider.id))?.auth?.apiKey;
|
|
46
|
+
} catch { /* provider not configured */ }
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const accounts = await provider.list();
|
|
50
|
+
const routing = provider.routing ? await provider.routing.get() : "n/a";
|
|
51
|
+
const pooled = accounts.length + (primary ? 1 : 0);
|
|
52
|
+
anySignedIn ||= primary || accounts.length > 0;
|
|
53
|
+
accountSummary.push(
|
|
54
|
+
primary || accounts.length > 0
|
|
55
|
+
? `${provider.id}: ${pooled} account(s), routing=${routing}`
|
|
56
|
+
: `${provider.id}: not signed in`,
|
|
57
|
+
);
|
|
58
|
+
} catch {
|
|
59
|
+
accountSummary.push(`${provider.id}: unreadable`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
features.push({
|
|
63
|
+
name: "Subscriptions",
|
|
64
|
+
ready: anySignedIn,
|
|
65
|
+
detail: accountSummary.join("; ") || "no account providers registered",
|
|
66
|
+
commands: ["/account", "/account <provider> add", "/routing standard|optimal", "/usage"],
|
|
67
|
+
setup: anySignedIn ? undefined : "/account anthropic add",
|
|
68
|
+
open: "/account",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/* benchmarks */
|
|
72
|
+
const hasKey = !!env("ARTIFICIAL_ANALYSIS_API_KEY");
|
|
73
|
+
const cache = readJson<{ records?: Record<string, unknown>; checkedAt?: number; fetchedAt?: number }>(
|
|
74
|
+
agentPath("model-quality.json"),
|
|
75
|
+
{},
|
|
76
|
+
);
|
|
77
|
+
const records = Object.keys(cache.records ?? {}).length;
|
|
78
|
+
// Caches written before the store rewrite carry `fetchedAt`.
|
|
79
|
+
const refreshedAt = cache.checkedAt ?? cache.fetchedAt;
|
|
80
|
+
const ageHours = refreshedAt ? Math.round((Date.now() - refreshedAt) / 3.6e6) : undefined;
|
|
81
|
+
features.push({
|
|
82
|
+
name: "Model Information",
|
|
83
|
+
ready: hasKey && records > 0,
|
|
84
|
+
detail: hasKey
|
|
85
|
+
? `${records} models cached${ageHours !== undefined ? `, refreshed ${ageHours}h ago` : ""}`
|
|
86
|
+
: "no Artificial Analysis API key",
|
|
87
|
+
commands: ["/models", "/model-info <id>", "/model-info refresh", "list_models (tool)"],
|
|
88
|
+
setup: hasKey ? undefined : "/model-info setup",
|
|
89
|
+
open: "/models",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/* spend policy */
|
|
93
|
+
features.push({
|
|
94
|
+
name: "Providers",
|
|
95
|
+
ready: config.policy.requireApproval.length > 0,
|
|
96
|
+
detail: `${config.policy.requireApproval.length} gated pattern(s), ${config.policy.autoApprove.length} auto-approved`,
|
|
97
|
+
commands: ["/provider", "/provider list", "/provider approve <name>"],
|
|
98
|
+
open: "/provider",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/* agent board */
|
|
102
|
+
const boardUrl = env("AGENT_BOARD_URL");
|
|
103
|
+
const boardReady = !!boardUrl && !!env("AGENT_BOARD_TOKEN");
|
|
104
|
+
// A board configured before /board setup existed has no recorded mode; it is
|
|
105
|
+
// externally managed by definition.
|
|
106
|
+
const mode = env("AGENT_BOARD_MODE") ?? (boardReady ? "external" : "none");
|
|
107
|
+
features.push({
|
|
108
|
+
name: "Agent Board",
|
|
109
|
+
ready: boardReady,
|
|
110
|
+
detail: boardUrl ? `${mode} at ${boardUrl}` : "no board configured",
|
|
111
|
+
commands: ["/board", "/board setup", "/board restart", "/board clear", "agent_board (tool)"],
|
|
112
|
+
setup: boardReady ? undefined : "/board setup",
|
|
113
|
+
open: "/board status",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/* remote workers */
|
|
117
|
+
const workers = config.remote.workers ?? [];
|
|
118
|
+
const enabled = workers.filter((worker) => (worker as { enabled?: boolean }).enabled !== false);
|
|
119
|
+
let sshHosts = 0;
|
|
120
|
+
try {
|
|
121
|
+
sshHosts = readSshHosts().length;
|
|
122
|
+
} catch { /* no ssh config */ }
|
|
123
|
+
features.push({
|
|
124
|
+
name: "Remote Workers",
|
|
125
|
+
ready: enabled.length > 0,
|
|
126
|
+
detail: enabled.length > 0
|
|
127
|
+
? `${enabled.length} enabled of ${workers.length} configured`
|
|
128
|
+
: `none enabled${sshHosts > 0 ? ` (${sshHosts} host(s) available in ~/.ssh/config)` : ""}`,
|
|
129
|
+
commands: ["/remote setup", "/remote add", "remote_test (tool)", "remote_status (tool)"],
|
|
130
|
+
setup: enabled.length > 0 ? undefined : "/remote setup",
|
|
131
|
+
open: "/remote setup",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return features;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Unconfigured features sort first: the work to do is what you see first. */
|
|
138
|
+
function ordered(features: Feature[]): Feature[] {
|
|
139
|
+
return [...features].sort((a, b) => Number(a.ready) - Number(b.ready));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderRow(feature: Feature, width: number): string {
|
|
143
|
+
return `[${feature.ready ? "✓" : " "}] ${feature.name.padEnd(width)} ${feature.detail}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function buildBrief(features: Feature[]): string {
|
|
147
|
+
const lines = features.map((feature) => {
|
|
148
|
+
const parts = [
|
|
149
|
+
`- ${feature.name}: ${feature.ready ? "READY" : "NOT SET UP"}`,
|
|
150
|
+
` state: ${feature.detail}`,
|
|
151
|
+
` commands: ${feature.commands.join(", ")}`,
|
|
152
|
+
];
|
|
153
|
+
if (feature.setup) parts.push(` to enable: ${feature.setup}`);
|
|
154
|
+
return parts.join("\n");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const pending = features.filter((feature) => !feature.ready);
|
|
158
|
+
|
|
159
|
+
return [
|
|
160
|
+
"The user just ran /pi-plus. Give them a short, friendly orientation to the pi-plus extension pack.",
|
|
161
|
+
"",
|
|
162
|
+
"Detected state on this machine:",
|
|
163
|
+
"",
|
|
164
|
+
...lines,
|
|
165
|
+
"",
|
|
166
|
+
`Config file: ${configPath()}${existsSync(configPath()) ? "" : " (not created yet)"}`,
|
|
167
|
+
"",
|
|
168
|
+
"Write the reply yourself, in chat. Requirements:",
|
|
169
|
+
"1. One short sentence on what pi-plus is: four capabilities in one package.",
|
|
170
|
+
"2. A compact list of the capabilities, each with one line on what it does and the command to try. Mark which are already working.",
|
|
171
|
+
pending.length > 0
|
|
172
|
+
? `3. Then a short 'Set these up next' section covering ONLY the ones marked NOT SET UP (${pending.map((f) => f.name).join(", ")}), each with the single command to run and one line on what it will ask for.`
|
|
173
|
+
: "3. Note that everything is already configured, and suggest one or two commands worth trying.",
|
|
174
|
+
"4. Keep it under ~250 words. No preamble, no headings deeper than one level, no invented features.",
|
|
175
|
+
"5. Do not call any tools. Just answer.",
|
|
176
|
+
].join("\n");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export default function setupGuide(pi: ExtensionAPI) {
|
|
180
|
+
pi.registerCommand("pi-plus", {
|
|
181
|
+
description: "Status for every pi-plus feature, or `help` for an explanation",
|
|
182
|
+
getArgumentCompletions: (prefix) =>
|
|
183
|
+
"help".startsWith(prefix) ? [{ value: "help", label: "help: explain the extension and what is missing" }] : [],
|
|
184
|
+
handler: async (args, ctx) => {
|
|
185
|
+
const features = await inspect(ctx);
|
|
186
|
+
|
|
187
|
+
// `/pi-plus help` asks the model to explain the pack and what is missing.
|
|
188
|
+
if (args.trim().toLowerCase() === "help") {
|
|
189
|
+
await pi.sendUserMessage(buildBrief(features));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Everything else is the status modal. ui.select needs a TTY, so headless
|
|
194
|
+
// sessions fall back to the same information as plain text.
|
|
195
|
+
const rows = ordered(features);
|
|
196
|
+
if (!ctx.hasUI) {
|
|
197
|
+
ctx.ui.notify(
|
|
198
|
+
rows
|
|
199
|
+
.map((feature) => `${feature.ready ? "[ready]" : "[setup]"} ${feature.name}\n ${feature.detail}`)
|
|
200
|
+
.join("\n"),
|
|
201
|
+
"info",
|
|
202
|
+
);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const width = Math.max(...rows.map((feature) => feature.name.length));
|
|
207
|
+
const labels = rows.map((feature) => renderRow(feature, width));
|
|
208
|
+
const choice = await ctx.ui.select("pi-plus", labels);
|
|
209
|
+
if (!choice) return;
|
|
210
|
+
|
|
211
|
+
const picked = rows[labels.indexOf(choice)];
|
|
212
|
+
if (!picked) return;
|
|
213
|
+
|
|
214
|
+
// Dispatch through the command pipeline rather than importing another
|
|
215
|
+
// domain, which would break the no-cross-domain-imports rule.
|
|
216
|
+
const command = picked.ready ? picked.open : (picked.setup ?? picked.open);
|
|
217
|
+
await pi.sendUserMessage(command, { expandPromptTemplates: true });
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|