omegon 0.6.0
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/.gitattributes +3 -0
- package/AGENTS.md +16 -0
- package/LICENSE +15 -0
- package/README.md +289 -0
- package/bin/pi.mjs +30 -0
- package/extensions/00-secrets/index.ts +1126 -0
- package/extensions/01-auth/auth.ts +401 -0
- package/extensions/01-auth/index.ts +289 -0
- package/extensions/auto-compact.ts +42 -0
- package/extensions/bootstrap/deps.ts +291 -0
- package/extensions/bootstrap/index.ts +811 -0
- package/extensions/chronos/chronos.sh +487 -0
- package/extensions/chronos/index.ts +148 -0
- package/extensions/cleave/assessment.ts +754 -0
- package/extensions/cleave/bridge.ts +31 -0
- package/extensions/cleave/conflicts.ts +250 -0
- package/extensions/cleave/dispatcher.ts +808 -0
- package/extensions/cleave/guardrails.ts +426 -0
- package/extensions/cleave/index.ts +3121 -0
- package/extensions/cleave/lifecycle-emitter.ts +20 -0
- package/extensions/cleave/openspec.ts +811 -0
- package/extensions/cleave/planner.ts +260 -0
- package/extensions/cleave/review.ts +579 -0
- package/extensions/cleave/skills.ts +355 -0
- package/extensions/cleave/types.ts +261 -0
- package/extensions/cleave/workspace.ts +861 -0
- package/extensions/cleave/worktree.ts +243 -0
- package/extensions/core-renderers.ts +253 -0
- package/extensions/dashboard/context-gauge.ts +58 -0
- package/extensions/dashboard/file-watch.ts +14 -0
- package/extensions/dashboard/footer.ts +1145 -0
- package/extensions/dashboard/git.ts +185 -0
- package/extensions/dashboard/index.ts +478 -0
- package/extensions/dashboard/memory-audit.ts +34 -0
- package/extensions/dashboard/overlay-data.ts +705 -0
- package/extensions/dashboard/overlay.ts +365 -0
- package/extensions/dashboard/render-utils.ts +54 -0
- package/extensions/dashboard/types.ts +191 -0
- package/extensions/dashboard/uri-helper.ts +45 -0
- package/extensions/debug.ts +69 -0
- package/extensions/defaults.ts +282 -0
- package/extensions/design-tree/dashboard-state.ts +161 -0
- package/extensions/design-tree/design-card.ts +362 -0
- package/extensions/design-tree/index.ts +2130 -0
- package/extensions/design-tree/lifecycle-emitter.ts +41 -0
- package/extensions/design-tree/tree.ts +1607 -0
- package/extensions/design-tree/types.ts +163 -0
- package/extensions/distill.ts +127 -0
- package/extensions/effort/index.ts +395 -0
- package/extensions/effort/tiers.ts +146 -0
- package/extensions/effort/types.ts +105 -0
- package/extensions/lib/git-state.ts +227 -0
- package/extensions/lib/local-models.ts +157 -0
- package/extensions/lib/model-preferences.ts +51 -0
- package/extensions/lib/model-routing.ts +720 -0
- package/extensions/lib/operator-fallback.ts +205 -0
- package/extensions/lib/operator-profile.ts +360 -0
- package/extensions/lib/slash-command-bridge.ts +253 -0
- package/extensions/lib/typebox-helpers.ts +16 -0
- package/extensions/local-inference/index.ts +727 -0
- package/extensions/mcp-bridge/README.md +220 -0
- package/extensions/mcp-bridge/index.ts +951 -0
- package/extensions/mcp-bridge/lib.ts +365 -0
- package/extensions/mcp-bridge/mcp.json +3 -0
- package/extensions/mcp-bridge/package.json +11 -0
- package/extensions/model-budget.ts +752 -0
- package/extensions/offline-driver.ts +403 -0
- package/extensions/openspec/archive-gate.ts +164 -0
- package/extensions/openspec/branch-cleanup.ts +64 -0
- package/extensions/openspec/dashboard-state.ts +50 -0
- package/extensions/openspec/index.ts +1917 -0
- package/extensions/openspec/lifecycle-emitter.ts +65 -0
- package/extensions/openspec/lifecycle-files.ts +70 -0
- package/extensions/openspec/lifecycle.ts +50 -0
- package/extensions/openspec/reconcile.ts +187 -0
- package/extensions/openspec/spec.ts +1385 -0
- package/extensions/openspec/types.ts +98 -0
- package/extensions/project-memory/DESIGN-global-mind.md +198 -0
- package/extensions/project-memory/README.md +202 -0
- package/extensions/project-memory/api-types.ts +382 -0
- package/extensions/project-memory/compaction-policy.ts +29 -0
- package/extensions/project-memory/core.ts +164 -0
- package/extensions/project-memory/embeddings.ts +230 -0
- package/extensions/project-memory/extraction-v2.ts +861 -0
- package/extensions/project-memory/factstore.ts +2177 -0
- package/extensions/project-memory/index.ts +3459 -0
- package/extensions/project-memory/injection-metrics.ts +91 -0
- package/extensions/project-memory/jsonl-io.ts +12 -0
- package/extensions/project-memory/lifecycle.ts +331 -0
- package/extensions/project-memory/migration.ts +293 -0
- package/extensions/project-memory/package.json +9 -0
- package/extensions/project-memory/sci-renderers.ts +7 -0
- package/extensions/project-memory/template.ts +103 -0
- package/extensions/project-memory/triggers.ts +52 -0
- package/extensions/project-memory/types.ts +102 -0
- package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
- package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
- package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
- package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
- package/extensions/render/composition/package-lock.json +534 -0
- package/extensions/render/composition/package.json +22 -0
- package/extensions/render/composition/render.mjs +246 -0
- package/extensions/render/composition/test-comp.tsx +87 -0
- package/extensions/render/composition/types.ts +24 -0
- package/extensions/render/excalidraw/UPSTREAM.md +81 -0
- package/extensions/render/excalidraw/elements.ts +764 -0
- package/extensions/render/excalidraw/index.ts +66 -0
- package/extensions/render/excalidraw/types.ts +223 -0
- package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
- package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
- package/extensions/render/excalidraw-renderer/render_template.html +59 -0
- package/extensions/render/index.ts +830 -0
- package/extensions/render/native-diagrams/index.ts +57 -0
- package/extensions/render/native-diagrams/motifs.ts +542 -0
- package/extensions/render/native-diagrams/raster.ts +8 -0
- package/extensions/render/native-diagrams/scene.ts +75 -0
- package/extensions/render/native-diagrams/spec.ts +204 -0
- package/extensions/render/native-diagrams/svg.ts +116 -0
- package/extensions/sci-ui.ts +304 -0
- package/extensions/session-log.ts +174 -0
- package/extensions/shared-state.ts +146 -0
- package/extensions/spinner-verbs.ts +91 -0
- package/extensions/style.ts +281 -0
- package/extensions/terminal-title.ts +191 -0
- package/extensions/tool-profile/index.ts +291 -0
- package/extensions/tool-profile/profiles.ts +290 -0
- package/extensions/types.d.ts +9 -0
- package/extensions/vault/index.ts +185 -0
- package/extensions/version-check.ts +90 -0
- package/extensions/view/index.ts +859 -0
- package/extensions/view/uri-resolver.ts +148 -0
- package/extensions/web-search/index.ts +182 -0
- package/extensions/web-search/providers.ts +121 -0
- package/extensions/web-ui/index.ts +110 -0
- package/extensions/web-ui/server.ts +265 -0
- package/extensions/web-ui/state.ts +462 -0
- package/extensions/web-ui/static/index.html +145 -0
- package/extensions/web-ui/types.ts +284 -0
- package/package.json +76 -0
- package/prompts/init.md +75 -0
- package/prompts/new-repo.md +54 -0
- package/prompts/oci-login.md +56 -0
- package/prompts/status.md +50 -0
- package/settings.json +4 -0
- package/skills/cleave/SKILL.md +218 -0
- package/skills/git/SKILL.md +209 -0
- package/skills/git/_reference/ci-validation.md +204 -0
- package/skills/oci/SKILL.md +338 -0
- package/skills/openspec/SKILL.md +346 -0
- package/skills/pi-extensions/SKILL.md +191 -0
- package/skills/pi-tui/SKILL.md +517 -0
- package/skills/python/SKILL.md +189 -0
- package/skills/rust/SKILL.md +268 -0
- package/skills/security/SKILL.md +206 -0
- package/skills/style/SKILL.md +264 -0
- package/skills/typescript/SKILL.md +225 -0
- package/skills/vault/SKILL.md +102 -0
- package/themes/alpharius-legacy.json +85 -0
- package/themes/alpharius.conf +59 -0
- package/themes/alpharius.json +88 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 01-auth/auth — Domain logic for authentication status checking.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.ts so tests can import without pulling in
|
|
5
|
+
* pi-tui/pi-coding-agent dependencies (which aren't resolvable under tsx).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@cwilson613/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
// ─── Types ───────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export type AuthStatus = "ok" | "expired" | "invalid" | "none" | "missing";
|
|
13
|
+
|
|
14
|
+
export interface AuthResult {
|
|
15
|
+
provider: string;
|
|
16
|
+
status: AuthStatus;
|
|
17
|
+
detail: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
refresh?: string;
|
|
20
|
+
secretHint?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AuthProvider {
|
|
24
|
+
/** Unique identifier: "github", "gitlab", "aws", etc. */
|
|
25
|
+
id: string;
|
|
26
|
+
/** Display name: "GitHub", "GitLab", "AWS", etc. */
|
|
27
|
+
name: string;
|
|
28
|
+
/** CLI binary name: "gh", "glab", "aws", etc. */
|
|
29
|
+
cli: string;
|
|
30
|
+
/** Env var that can provide a token (checked via process.env, populated by 00-secrets) */
|
|
31
|
+
tokenEnvVar?: string;
|
|
32
|
+
/** Command to refresh/login */
|
|
33
|
+
refreshCommand: string;
|
|
34
|
+
/** Check auth status. Returns structured result with diagnosis. */
|
|
35
|
+
check(pi: ExtensionAPI, signal?: AbortSignal): Promise<AuthResult>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Error Diagnosis Helpers ─────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Classify auth-specific error patterns from CLI stderr.
|
|
42
|
+
*
|
|
43
|
+
* Pattern ordering matters: expired is checked before invalid because
|
|
44
|
+
* "invalid token has expired" should classify as expired, not invalid.
|
|
45
|
+
*
|
|
46
|
+
* Only auth-specific keywords are matched. Generic terms like "denied"
|
|
47
|
+
* or "invalid" are scoped with adjacent auth context words to avoid
|
|
48
|
+
* false positives on non-auth errors like "invalid region".
|
|
49
|
+
*/
|
|
50
|
+
export function diagnoseError(stderr: string): { status: AuthStatus; reason: string } {
|
|
51
|
+
const lower = stderr.toLowerCase();
|
|
52
|
+
|
|
53
|
+
// Expired tokens — most specific, check first
|
|
54
|
+
if (lower.includes("token has expired") || lower.includes("token is expired")
|
|
55
|
+
|| lower.includes("session expired") || lower.includes("expiredtoken")
|
|
56
|
+
|| lower.includes("credentials have expired")
|
|
57
|
+
|| /\bexpired\b.*\b(?:token|session|credential|certificate)\b/.test(lower)
|
|
58
|
+
|| /\b(?:token|session|credential|certificate)\b.*\bexpired\b/.test(lower)) {
|
|
59
|
+
return { status: "expired", reason: "Token or session has expired" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Not logged in — check before invalid to avoid "not authenticated" matching "invalid"
|
|
63
|
+
if (lower.includes("not logged") || lower.includes("no token") || lower.includes("not authenticated")
|
|
64
|
+
|| lower.includes("login required") || lower.includes("no credentials")
|
|
65
|
+
|| lower.includes("no valid credentials")) {
|
|
66
|
+
return { status: "none", reason: "Not authenticated" };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Invalid/revoked credentials — scoped to auth-relevant context
|
|
70
|
+
if (lower.includes("bad credentials") || lower.includes("authentication failed")
|
|
71
|
+
|| lower.includes("revoked")
|
|
72
|
+
|| /\b401\b/.test(lower) || lower.includes("unauthorized")) {
|
|
73
|
+
return { status: "invalid", reason: extractErrorLine(stderr) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Forbidden (authenticated but insufficient permissions)
|
|
77
|
+
if (/\b403\b/.test(lower) || lower.includes("insufficient scope")
|
|
78
|
+
|| lower.includes("access denied")) {
|
|
79
|
+
return { status: "invalid", reason: `Authenticated but forbidden: ${extractErrorLine(stderr)}` };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { status: "none", reason: extractErrorLine(stderr) || "Authentication failed" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Extract the most informative error line from multi-line stderr. */
|
|
86
|
+
export function extractErrorLine(stderr: string): string {
|
|
87
|
+
const lines = stderr.trim().split("\n").filter(l => l.trim());
|
|
88
|
+
// Prefer lines with auth-relevant error keywords
|
|
89
|
+
const errorLine = lines.find(l => /error|failed|invalid|expired|denied|unauthorized|401|403/i.test(l));
|
|
90
|
+
if (errorLine) return errorLine.trim().slice(0, 200);
|
|
91
|
+
// Fall back to first non-empty line
|
|
92
|
+
return (lines[0] || "Unknown error").trim().slice(0, 200);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Providers ───────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
const gitProvider: AuthProvider = {
|
|
98
|
+
id: "git",
|
|
99
|
+
name: "Git",
|
|
100
|
+
cli: "git",
|
|
101
|
+
refreshCommand: 'git config --global user.name "Your Name" && git config --global user.email "you@example.com"',
|
|
102
|
+
|
|
103
|
+
async check(pi, signal) {
|
|
104
|
+
const nameResult = await pi.exec("git", ["config", "user.name"], { signal, timeout: 5_000 });
|
|
105
|
+
const emailResult = await pi.exec("git", ["config", "user.email"], { signal, timeout: 5_000 });
|
|
106
|
+
const name = nameResult.stdout.trim() || "";
|
|
107
|
+
const email = emailResult.stdout.trim() || "";
|
|
108
|
+
|
|
109
|
+
if (name && email) {
|
|
110
|
+
return { provider: this.id, status: "ok", detail: `${name} <${email}>` };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
provider: this.id,
|
|
114
|
+
status: "none",
|
|
115
|
+
detail: `name: ${name || "(not set)"}, email: ${email || "(not set)"}`,
|
|
116
|
+
refresh: this.refreshCommand,
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const githubProvider: AuthProvider = {
|
|
122
|
+
id: "github",
|
|
123
|
+
name: "GitHub",
|
|
124
|
+
cli: "gh",
|
|
125
|
+
tokenEnvVar: "GITHUB_TOKEN",
|
|
126
|
+
refreshCommand: "gh auth login",
|
|
127
|
+
|
|
128
|
+
async check(pi, signal) {
|
|
129
|
+
const which = await pi.exec("which", ["gh"], { signal, timeout: 3_000 });
|
|
130
|
+
if (which.code !== 0) {
|
|
131
|
+
return { provider: this.id, status: "missing", detail: "gh CLI not installed" };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result = await pi.exec("gh", ["auth", "status"], { signal, timeout: 10_000 });
|
|
135
|
+
const output = (result.stdout + "\n" + result.stderr).trim();
|
|
136
|
+
|
|
137
|
+
if (result.code === 0) {
|
|
138
|
+
const accountMatch = output.match(/Logged in to \S+ account (\S+)/);
|
|
139
|
+
const scopeMatch = output.match(/Token scopes:(.+)/);
|
|
140
|
+
let detail = accountMatch ? accountMatch[1] : "authenticated";
|
|
141
|
+
if (scopeMatch) detail += ` (scopes: ${scopeMatch[1].trim()})`;
|
|
142
|
+
return { provider: this.id, status: "ok", detail, refresh: this.refreshCommand };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const diag = diagnoseError(output);
|
|
146
|
+
return {
|
|
147
|
+
provider: this.id,
|
|
148
|
+
status: diag.status,
|
|
149
|
+
detail: diag.reason,
|
|
150
|
+
error: output.slice(0, 300),
|
|
151
|
+
refresh: this.refreshCommand,
|
|
152
|
+
secretHint: "GITHUB_TOKEN",
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const gitlabProvider: AuthProvider = {
|
|
158
|
+
id: "gitlab",
|
|
159
|
+
name: "GitLab",
|
|
160
|
+
cli: "glab",
|
|
161
|
+
tokenEnvVar: "GITLAB_TOKEN",
|
|
162
|
+
refreshCommand: "glab auth login",
|
|
163
|
+
|
|
164
|
+
async check(pi, signal) {
|
|
165
|
+
const which = await pi.exec("which", ["glab"], { signal, timeout: 3_000 });
|
|
166
|
+
if (which.code !== 0) {
|
|
167
|
+
if (process.env.GITLAB_TOKEN) {
|
|
168
|
+
return {
|
|
169
|
+
provider: this.id,
|
|
170
|
+
status: "ok",
|
|
171
|
+
detail: "GITLAB_TOKEN set (glab CLI not installed)",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return { provider: this.id, status: "missing", detail: "glab CLI not installed" };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const result = await pi.exec("glab", ["auth", "status"], { signal, timeout: 10_000 });
|
|
178
|
+
const output = (result.stdout + "\n" + result.stderr).trim();
|
|
179
|
+
|
|
180
|
+
if (result.code === 0) {
|
|
181
|
+
const accountMatch = output.match(/Logged in to \S+ (?:as|account) (\S+)/i);
|
|
182
|
+
const hostMatch = output.match(/Logged in to (\S+)/i);
|
|
183
|
+
let detail = accountMatch ? accountMatch[1] : "authenticated";
|
|
184
|
+
if (hostMatch) detail += ` @ ${hostMatch[1]}`;
|
|
185
|
+
return { provider: this.id, status: "ok", detail, refresh: this.refreshCommand };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const diag = diagnoseError(output);
|
|
189
|
+
return {
|
|
190
|
+
provider: this.id,
|
|
191
|
+
status: diag.status,
|
|
192
|
+
detail: diag.reason,
|
|
193
|
+
error: output.slice(0, 300),
|
|
194
|
+
refresh: this.refreshCommand,
|
|
195
|
+
secretHint: "GITLAB_TOKEN",
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const awsProvider: AuthProvider = {
|
|
201
|
+
id: "aws",
|
|
202
|
+
name: "AWS",
|
|
203
|
+
cli: "aws",
|
|
204
|
+
tokenEnvVar: "AWS_ACCESS_KEY_ID",
|
|
205
|
+
refreshCommand: "aws sso login --profile <profile>",
|
|
206
|
+
|
|
207
|
+
async check(pi, signal) {
|
|
208
|
+
const which = await pi.exec("which", ["aws"], { signal, timeout: 3_000 });
|
|
209
|
+
if (which.code !== 0) {
|
|
210
|
+
return { provider: this.id, status: "missing", detail: "aws CLI not installed" };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const result = await pi.exec("aws", ["sts", "get-caller-identity", "--output", "json"], { signal, timeout: 10_000 });
|
|
214
|
+
|
|
215
|
+
if (result.code === 0) {
|
|
216
|
+
try {
|
|
217
|
+
const identity = JSON.parse(result.stdout.trim());
|
|
218
|
+
return {
|
|
219
|
+
provider: this.id,
|
|
220
|
+
status: "ok",
|
|
221
|
+
detail: identity.Arn || identity.Account || "authenticated",
|
|
222
|
+
refresh: this.refreshCommand,
|
|
223
|
+
};
|
|
224
|
+
} catch {
|
|
225
|
+
return { provider: this.id, status: "ok", detail: "authenticated", refresh: this.refreshCommand };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const diag = diagnoseError(result.stderr || result.stdout);
|
|
230
|
+
return {
|
|
231
|
+
provider: this.id,
|
|
232
|
+
status: diag.status,
|
|
233
|
+
detail: diag.reason,
|
|
234
|
+
error: (result.stderr || result.stdout).slice(0, 300),
|
|
235
|
+
refresh: this.refreshCommand,
|
|
236
|
+
secretHint: "AWS_ACCESS_KEY_ID",
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const kubernetesProvider: AuthProvider = {
|
|
242
|
+
id: "kubernetes",
|
|
243
|
+
name: "Kubernetes",
|
|
244
|
+
cli: "kubectl",
|
|
245
|
+
refreshCommand: "kubectl config use-context <context>",
|
|
246
|
+
|
|
247
|
+
async check(pi, signal) {
|
|
248
|
+
const which = await pi.exec("which", ["kubectl"], { signal, timeout: 3_000 });
|
|
249
|
+
if (which.code !== 0) {
|
|
250
|
+
return { provider: this.id, status: "missing", detail: "kubectl not installed" };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const kctx = await pi.exec("kubectl", ["config", "current-context"], { signal, timeout: 5_000 });
|
|
254
|
+
if (kctx.code === 0) {
|
|
255
|
+
const context = kctx.stdout.trim();
|
|
256
|
+
const verify = await pi.exec("kubectl", ["cluster-info", "--request-timeout=5s"], { signal, timeout: 10_000 });
|
|
257
|
+
if (verify.code === 0) {
|
|
258
|
+
return {
|
|
259
|
+
provider: this.id,
|
|
260
|
+
status: "ok",
|
|
261
|
+
detail: `context: ${context}`,
|
|
262
|
+
refresh: this.refreshCommand,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const diag = diagnoseError(verify.stderr || verify.stdout);
|
|
266
|
+
return {
|
|
267
|
+
provider: this.id,
|
|
268
|
+
status: diag.status,
|
|
269
|
+
detail: `context: ${context} — ${diag.reason}`,
|
|
270
|
+
error: (verify.stderr || "").slice(0, 300),
|
|
271
|
+
refresh: this.refreshCommand,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
provider: this.id,
|
|
277
|
+
status: "none",
|
|
278
|
+
detail: "No context set",
|
|
279
|
+
refresh: this.refreshCommand,
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const ociProvider: AuthProvider = {
|
|
285
|
+
id: "oci",
|
|
286
|
+
name: "OCI Registry (ghcr.io)",
|
|
287
|
+
cli: "podman",
|
|
288
|
+
refreshCommand: "gh auth token | podman login ghcr.io -u <user> --password-stdin",
|
|
289
|
+
|
|
290
|
+
async check(pi, signal) {
|
|
291
|
+
const podmanWhich = await pi.exec("which", ["podman"], { signal, timeout: 3_000 });
|
|
292
|
+
const dockerWhich = await pi.exec("which", ["docker"], { signal, timeout: 3_000 });
|
|
293
|
+
const cmd = podmanWhich.code === 0 ? "podman" : dockerWhich.code === 0 ? "docker" : null;
|
|
294
|
+
|
|
295
|
+
if (!cmd) {
|
|
296
|
+
return { provider: this.id, status: "missing", detail: "Neither podman nor docker installed" };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const refresh = `gh auth token | ${cmd} login ghcr.io -u $(gh api user --jq .login) --password-stdin`;
|
|
300
|
+
|
|
301
|
+
const result = await pi.exec(cmd, ["login", "--get-login", "ghcr.io"], { signal, timeout: 5_000 });
|
|
302
|
+
if (result.code === 0) {
|
|
303
|
+
return {
|
|
304
|
+
provider: this.id,
|
|
305
|
+
status: "ok",
|
|
306
|
+
detail: `ghcr.io: ${result.stdout.trim()} (${cmd})`,
|
|
307
|
+
refresh,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
provider: this.id,
|
|
313
|
+
status: "none",
|
|
314
|
+
detail: `Not logged in to ghcr.io (${cmd})`,
|
|
315
|
+
refresh,
|
|
316
|
+
};
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// ─── Provider Registry ───────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
/** All providers, ordered by typical check priority. */
|
|
323
|
+
export const ALL_PROVIDERS: AuthProvider[] = [
|
|
324
|
+
gitProvider,
|
|
325
|
+
githubProvider,
|
|
326
|
+
gitlabProvider,
|
|
327
|
+
awsProvider,
|
|
328
|
+
kubernetesProvider,
|
|
329
|
+
ociProvider,
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
export function findProvider(idOrName: string): AuthProvider | undefined {
|
|
333
|
+
const lower = idOrName.toLowerCase();
|
|
334
|
+
return ALL_PROVIDERS.find(p => p.id === lower || p.name.toLowerCase() === lower);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ─── Shared check-all helper ─────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
export async function checkAllProviders(pi: ExtensionAPI, signal?: AbortSignal): Promise<AuthResult[]> {
|
|
340
|
+
const results: AuthResult[] = [];
|
|
341
|
+
for (const provider of ALL_PROVIDERS) {
|
|
342
|
+
try {
|
|
343
|
+
results.push(await provider.check(pi, signal));
|
|
344
|
+
} catch (e: any) {
|
|
345
|
+
results.push({
|
|
346
|
+
provider: provider.id,
|
|
347
|
+
status: "none",
|
|
348
|
+
detail: `Check failed: ${e.message}`,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return results;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ─── Formatting ──────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
export const STATUS_ICONS: Record<AuthStatus, string> = {
|
|
358
|
+
ok: "✓",
|
|
359
|
+
expired: "⚠",
|
|
360
|
+
invalid: "✗",
|
|
361
|
+
none: "✗",
|
|
362
|
+
missing: "·",
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
export function formatResults(results: AuthResult[]): string {
|
|
366
|
+
const lines: string[] = ["**Auth Status**", ""];
|
|
367
|
+
|
|
368
|
+
for (const r of results) {
|
|
369
|
+
const icon = STATUS_ICONS[r.status];
|
|
370
|
+
let line = ` ${icon} **${r.provider}**: ${r.detail}`;
|
|
371
|
+
if (r.error && r.status !== "ok") {
|
|
372
|
+
line += `\n Error: ${r.error.split("\n")[0].slice(0, 120)}`;
|
|
373
|
+
}
|
|
374
|
+
lines.push(line);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Actionable items
|
|
378
|
+
const fixable = results.filter(r =>
|
|
379
|
+
r.status === "expired" || r.status === "invalid" || r.status === "none"
|
|
380
|
+
);
|
|
381
|
+
if (fixable.length > 0) {
|
|
382
|
+
lines.push("", "**To fix:**");
|
|
383
|
+
for (const r of fixable) {
|
|
384
|
+
if (r.status === "expired") {
|
|
385
|
+
lines.push(` ${r.provider}: token expired → \`${r.refresh}\``);
|
|
386
|
+
} else if (r.status === "invalid") {
|
|
387
|
+
lines.push(` ${r.provider}: credentials invalid → \`${r.refresh}\``);
|
|
388
|
+
if (r.secretHint) {
|
|
389
|
+
lines.push(` Or configure via: \`/secrets configure ${r.secretHint}\``);
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
lines.push(` ${r.provider}: \`${r.refresh}\``);
|
|
393
|
+
if (r.secretHint) {
|
|
394
|
+
lines.push(` Or configure via: \`/secrets configure ${r.secretHint}\``);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return lines.join("\n");
|
|
401
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// @secret GITHUB_TOKEN "GitHub personal access token (alternative to gh auth login)"
|
|
2
|
+
// @secret GITLAB_TOKEN "GitLab personal access token (alternative to glab auth login)"
|
|
3
|
+
// @secret AWS_ACCESS_KEY_ID "AWS access key ID (alternative to aws sso login)"
|
|
4
|
+
// @secret AWS_SECRET_ACCESS_KEY "AWS secret access key"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Auth Extension — authentication status, diagnosis, and refresh across dev tools.
|
|
8
|
+
*
|
|
9
|
+
* Registers:
|
|
10
|
+
* - `whoami` tool: LLM-callable auth status check
|
|
11
|
+
* - `/auth` command: interactive auth management
|
|
12
|
+
* - `/auth` or `/auth status` — check all providers
|
|
13
|
+
* - `/auth check <provider>` — check a specific provider
|
|
14
|
+
* - `/auth refresh <provider>` — show refresh command + offer /secrets path
|
|
15
|
+
* - `/auth list` — list available providers
|
|
16
|
+
*
|
|
17
|
+
* Security model:
|
|
18
|
+
* - Auth NEVER stores, caches, or manipulates secret values directly.
|
|
19
|
+
* - All credential storage flows through 00-secrets (`/secrets configure`).
|
|
20
|
+
* - Auth reads process.env (populated by 00-secrets at init) to check
|
|
21
|
+
* whether token env vars are set.
|
|
22
|
+
* - Auth runs CLI tools (`gh`, `glab`, `aws`, etc.) to check session state
|
|
23
|
+
* and parse error output for specific failure reasons.
|
|
24
|
+
*
|
|
25
|
+
* Load order: 01-auth loads after 00-secrets, so process.env is populated.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { ExtensionAPI } from "@cwilson613/pi-coding-agent";
|
|
29
|
+
import { Text } from "@cwilson613/pi-tui";
|
|
30
|
+
import { Type } from "@sinclair/typebox";
|
|
31
|
+
import { sciCall, sciOk, sciErr, sciExpanded } from "../sci-ui.ts";
|
|
32
|
+
|
|
33
|
+
// Import domain logic from auth.ts (testable without pi-tui dependency)
|
|
34
|
+
import {
|
|
35
|
+
diagnoseError,
|
|
36
|
+
extractErrorLine,
|
|
37
|
+
ALL_PROVIDERS,
|
|
38
|
+
STATUS_ICONS,
|
|
39
|
+
formatResults,
|
|
40
|
+
findProvider,
|
|
41
|
+
checkAllProviders,
|
|
42
|
+
type AuthStatus,
|
|
43
|
+
type AuthResult,
|
|
44
|
+
type AuthProvider,
|
|
45
|
+
} from "./auth.ts";
|
|
46
|
+
|
|
47
|
+
// Re-export types for backward compatibility
|
|
48
|
+
export type { AuthStatus, AuthResult, AuthProvider };
|
|
49
|
+
|
|
50
|
+
// ─── Extension ───────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export default function authExtension(pi: ExtensionAPI) {
|
|
53
|
+
|
|
54
|
+
// ── Tool: whoami (LLM-callable) ───────────────────────────────
|
|
55
|
+
|
|
56
|
+
pi.registerTool({
|
|
57
|
+
name: "whoami",
|
|
58
|
+
label: "Auth Status",
|
|
59
|
+
description:
|
|
60
|
+
"Check authentication status across development tools " +
|
|
61
|
+
"(git, GitHub, GitLab, AWS, Kubernetes, OCI registries). " +
|
|
62
|
+
"Returns structured status with error diagnosis and refresh " +
|
|
63
|
+
"commands for expired or missing sessions.",
|
|
64
|
+
promptSnippet:
|
|
65
|
+
"Check auth status across dev tools (git, GitHub, GitLab, AWS, k8s, OCI registries)",
|
|
66
|
+
|
|
67
|
+
parameters: Type.Object({}),
|
|
68
|
+
|
|
69
|
+
async execute(_toolCallId, _params, signal, _onUpdate, _ctx) {
|
|
70
|
+
const results = await checkAllProviders(pi, signal);
|
|
71
|
+
const text = formatResults(results);
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: "text", text }],
|
|
74
|
+
details: {
|
|
75
|
+
checks: results.map(r => ({
|
|
76
|
+
provider: r.provider,
|
|
77
|
+
status: r.status,
|
|
78
|
+
detail: r.detail,
|
|
79
|
+
error: r.error,
|
|
80
|
+
})),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
renderCall(_args, theme) {
|
|
86
|
+
return sciCall("whoami", "", theme);
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
renderResult(result, { expanded }, theme) {
|
|
90
|
+
if ((result as any).isError) {
|
|
91
|
+
const first = result.content?.[0];
|
|
92
|
+
return sciErr(first && 'text' in first ? first.text : "Error", theme);
|
|
93
|
+
}
|
|
94
|
+
const checks = ((result.details as any)?.checks || []) as Array<{ provider: string; status: string; detail: string }>;
|
|
95
|
+
const summary = checks.map(c => {
|
|
96
|
+
const icon = STATUS_ICONS[c.status as AuthStatus] || "?";
|
|
97
|
+
return `${icon} ${c.provider}`;
|
|
98
|
+
}).join(" · ");
|
|
99
|
+
const hasError = checks.some(c => c.status !== "ok" && c.status !== "missing");
|
|
100
|
+
|
|
101
|
+
if (expanded) {
|
|
102
|
+
const lines = checks.map(c => {
|
|
103
|
+
const icon = STATUS_ICONS[c.status as AuthStatus] || "?";
|
|
104
|
+
const color = c.status === "ok" ? "success"
|
|
105
|
+
: c.status === "expired" ? "warning"
|
|
106
|
+
: c.status === "missing" ? "muted"
|
|
107
|
+
: "error";
|
|
108
|
+
return theme.fg(color as Parameters<typeof theme.fg>[0], `${icon} ${c.provider}`) + (c.detail ? theme.fg("dim", ` ${c.detail}`) : "");
|
|
109
|
+
});
|
|
110
|
+
return sciExpanded(lines, summary, theme);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return hasError ? sciErr(summary, theme) : sciOk(summary, theme);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ── Command: /auth ────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
pi.registerCommand("auth", {
|
|
120
|
+
description: "Auth management: status | check <provider> | refresh <provider> | list",
|
|
121
|
+
getArgumentCompletions: (prefix: string) => {
|
|
122
|
+
const parts = prefix.split(/\s+/);
|
|
123
|
+
if (parts.length <= 1) {
|
|
124
|
+
const subs = ["status", "check", "refresh", "list"];
|
|
125
|
+
const filtered = subs.filter(s => s.startsWith(parts[0] || ""));
|
|
126
|
+
return filtered.length > 0
|
|
127
|
+
? filtered.map(s => ({ value: s, label: s }))
|
|
128
|
+
: null;
|
|
129
|
+
}
|
|
130
|
+
const sub = parts[0];
|
|
131
|
+
if ((sub === "check" || sub === "refresh") && parts.length === 2) {
|
|
132
|
+
const partial = parts[1] || "";
|
|
133
|
+
return ALL_PROVIDERS
|
|
134
|
+
.filter(p => p.id.startsWith(partial) || p.name.toLowerCase().startsWith(partial))
|
|
135
|
+
.map(p => ({
|
|
136
|
+
value: `${sub} ${p.id}`,
|
|
137
|
+
label: `${p.id} — ${p.name}`,
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
handler: async (args, ctx) => {
|
|
144
|
+
const parts = (args || "status").trim().split(/\s+/);
|
|
145
|
+
const subcommand = parts[0];
|
|
146
|
+
const providerArg = parts.slice(1).join(" ");
|
|
147
|
+
|
|
148
|
+
switch (subcommand) {
|
|
149
|
+
case "status":
|
|
150
|
+
case "": {
|
|
151
|
+
const results = await checkAllProviders(pi);
|
|
152
|
+
const text = formatResults(results);
|
|
153
|
+
pi.sendMessage({ customType: "view", content: text, display: true });
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case "check": {
|
|
158
|
+
if (!providerArg) {
|
|
159
|
+
ctx.ui.notify("Usage: /auth check <provider> (try /auth list)", "error");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const provider = findProvider(providerArg);
|
|
163
|
+
if (!provider) {
|
|
164
|
+
ctx.ui.notify(
|
|
165
|
+
`Unknown provider: ${providerArg}\nAvailable: ${ALL_PROVIDERS.map(p => p.id).join(", ")}`,
|
|
166
|
+
"error"
|
|
167
|
+
);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const result = await provider.check(pi);
|
|
172
|
+
const text = formatResults([result]);
|
|
173
|
+
pi.sendMessage({ customType: "view", content: text, display: true });
|
|
174
|
+
} catch (e: any) {
|
|
175
|
+
ctx.ui.notify(`Check failed: ${e.message}`, "error");
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case "refresh": {
|
|
181
|
+
if (!providerArg) {
|
|
182
|
+
ctx.ui.notify("Usage: /auth refresh <provider> (try /auth list)", "error");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const provider = findProvider(providerArg);
|
|
186
|
+
if (!provider) {
|
|
187
|
+
ctx.ui.notify(
|
|
188
|
+
`Unknown provider: ${providerArg}\nAvailable: ${ALL_PROVIDERS.map(p => p.id).join(", ")}`,
|
|
189
|
+
"error"
|
|
190
|
+
);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check current state first
|
|
195
|
+
let current: AuthResult;
|
|
196
|
+
try {
|
|
197
|
+
current = await provider.check(pi);
|
|
198
|
+
} catch (e: any) {
|
|
199
|
+
ctx.ui.notify(`Check failed: ${e.message}`, "error");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (current.status === "ok") {
|
|
204
|
+
ctx.ui.notify(`${provider.name} is already authenticated: ${current.detail}`, "info");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (current.status === "missing") {
|
|
209
|
+
ctx.ui.notify(
|
|
210
|
+
`${provider.name}: ${provider.cli} CLI is not installed.\n` +
|
|
211
|
+
(provider.tokenEnvVar
|
|
212
|
+
? `You can set ${provider.tokenEnvVar} instead: /secrets configure ${provider.tokenEnvVar}`
|
|
213
|
+
: `Install ${provider.cli} first.`),
|
|
214
|
+
"warning"
|
|
215
|
+
);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Show refresh instructions — don't execute interactive commands
|
|
220
|
+
// because pi.exec runs without a TTY. CLI login commands (gh auth login,
|
|
221
|
+
// glab auth login, aws sso login) require browser interaction.
|
|
222
|
+
const statusLabel = current.status === "expired"
|
|
223
|
+
? "expired"
|
|
224
|
+
: current.status === "invalid"
|
|
225
|
+
? "invalid"
|
|
226
|
+
: "not authenticated";
|
|
227
|
+
|
|
228
|
+
const lines = [
|
|
229
|
+
`**${provider.name}** — ${statusLabel}`,
|
|
230
|
+
];
|
|
231
|
+
if (current.error) {
|
|
232
|
+
lines.push(`Error: ${current.error.split("\n")[0].slice(0, 120)}`);
|
|
233
|
+
}
|
|
234
|
+
lines.push("", "**Options:**");
|
|
235
|
+
lines.push(` 1. Run in your terminal: \`${provider.refreshCommand}\``);
|
|
236
|
+
if (provider.tokenEnvVar) {
|
|
237
|
+
lines.push(` 2. Configure token: \`/secrets configure ${provider.tokenEnvVar}\``);
|
|
238
|
+
}
|
|
239
|
+
lines.push("", "After authenticating, run `/auth check " + provider.id + "` to verify.");
|
|
240
|
+
|
|
241
|
+
pi.sendMessage({ customType: "view", content: lines.join("\n"), display: true });
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
case "list": {
|
|
246
|
+
const lines = ALL_PROVIDERS.map(p => {
|
|
247
|
+
let line = ` ${p.id} — ${p.name} (${p.cli})`;
|
|
248
|
+
if (p.tokenEnvVar) line += ` [env: ${p.tokenEnvVar}]`;
|
|
249
|
+
return line;
|
|
250
|
+
});
|
|
251
|
+
ctx.ui.notify("Available auth providers:\n\n" + lines.join("\n"), "info");
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
default:
|
|
256
|
+
ctx.ui.notify(
|
|
257
|
+
"Usage: /auth <status|check|refresh|list> [provider]\n\n" +
|
|
258
|
+
" /auth — check all providers\n" +
|
|
259
|
+
" /auth check github — check a specific provider\n" +
|
|
260
|
+
" /auth refresh aws — show refresh instructions\n" +
|
|
261
|
+
" /auth list — list available providers",
|
|
262
|
+
"info"
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ── Backward compat: /whoami command ──────────────────────────
|
|
269
|
+
|
|
270
|
+
pi.registerCommand("whoami", {
|
|
271
|
+
description: "Alias for /auth status — check authentication across dev tools",
|
|
272
|
+
handler: async (_args, _ctx) => {
|
|
273
|
+
const results = await checkAllProviders(pi);
|
|
274
|
+
const text = formatResults(results);
|
|
275
|
+
pi.sendMessage({ customType: "view", content: text, display: true });
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Domain logic re-exported from auth.ts for backward compatibility
|
|
281
|
+
export {
|
|
282
|
+
diagnoseError,
|
|
283
|
+
extractErrorLine,
|
|
284
|
+
ALL_PROVIDERS,
|
|
285
|
+
STATUS_ICONS,
|
|
286
|
+
formatResults,
|
|
287
|
+
findProvider,
|
|
288
|
+
checkAllProviders,
|
|
289
|
+
} from "./auth.ts";
|