omegon 0.6.3 → 0.6.4

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.
Files changed (69) hide show
  1. package/README.md +12 -10
  2. package/bin/omegon.mjs +40 -0
  3. package/bin/pi.mjs +5 -26
  4. package/extensions/00-secrets/index.ts +146 -39
  5. package/extensions/01-auth/auth.ts +1 -1
  6. package/extensions/01-auth/index.ts +3 -3
  7. package/extensions/auto-compact.ts +1 -1
  8. package/extensions/bootstrap/deps.ts +42 -0
  9. package/extensions/bootstrap/index.ts +326 -110
  10. package/extensions/chronos/index.ts +1 -1
  11. package/extensions/cleave/dispatcher.ts +6 -6
  12. package/extensions/cleave/index.ts +6 -6
  13. package/extensions/cleave/planner.ts +1 -1
  14. package/extensions/cleave/worktree.ts +1 -1
  15. package/extensions/core-renderers.ts +24 -84
  16. package/extensions/dashboard/footer.ts +184 -40
  17. package/extensions/dashboard/git.ts +2 -2
  18. package/extensions/dashboard/index.ts +4 -4
  19. package/extensions/dashboard/overlay-data.ts +5 -5
  20. package/extensions/dashboard/overlay.ts +5 -5
  21. package/extensions/dashboard/render-utils.ts +1 -1
  22. package/extensions/dashboard/types.ts +15 -0
  23. package/extensions/defaults.ts +4 -12
  24. package/extensions/design-tree/dashboard-state.ts +6 -6
  25. package/extensions/design-tree/design-card.ts +3 -3
  26. package/extensions/design-tree/index.ts +64 -44
  27. package/extensions/design-tree/types.ts +4 -2
  28. package/extensions/distill.ts +1 -1
  29. package/extensions/effort/index.ts +137 -10
  30. package/extensions/lib/model-routing.ts +304 -32
  31. package/extensions/lib/operator-fallback.ts +1 -1
  32. package/extensions/lib/operator-profile.ts +1 -1
  33. package/extensions/lib/provider-env.ts +163 -0
  34. package/extensions/{sci-ui.ts → lib/sci-ui.ts} +119 -2
  35. package/extensions/{shared-state.ts → lib/shared-state.ts} +13 -9
  36. package/extensions/lib/slash-command-bridge.ts +1 -1
  37. package/extensions/{types.d.ts → lib/types.d.ts} +3 -3
  38. package/extensions/local-inference/index.ts +1 -1
  39. package/extensions/mcp-bridge/index.ts +1 -1
  40. package/extensions/model-budget.ts +10 -10
  41. package/extensions/offline-driver.ts +11 -4
  42. package/extensions/openspec/archive-gate.ts +1 -1
  43. package/extensions/openspec/branch-cleanup.ts +1 -1
  44. package/extensions/openspec/dashboard-state.ts +3 -3
  45. package/extensions/openspec/index.ts +5 -5
  46. package/extensions/project-memory/factstore.ts +5 -11
  47. package/extensions/project-memory/index.ts +48 -34
  48. package/extensions/project-memory/package.json +1 -1
  49. package/extensions/project-memory/sci-renderers.ts +1 -1
  50. package/extensions/render/index.ts +1 -1
  51. package/extensions/session-log.ts +1 -1
  52. package/extensions/spinner-verbs.ts +1 -1
  53. package/extensions/style.ts +1 -1
  54. package/extensions/terminal-title.ts +3 -3
  55. package/extensions/tool-profile/index.ts +1 -1
  56. package/extensions/vault/index.ts +1 -1
  57. package/extensions/version-check.ts +13 -9
  58. package/extensions/view/index.ts +4 -4
  59. package/extensions/web-search/index.ts +5 -2
  60. package/extensions/web-ui/index.ts +1 -1
  61. package/extensions/web-ui/state.ts +1 -1
  62. package/package.json +8 -7
  63. package/scripts/preinstall.sh +19 -3
  64. package/scripts/publish-pi-mono.sh +92 -0
  65. package/skills/pi-extensions/SKILL.md +2 -2
  66. package/skills/pi-tui/SKILL.md +17 -17
  67. package/skills/typescript/SKILL.md +1 -1
  68. package/themes/alpharius.json +7 -6
  69. /package/extensions/{debug.ts → lib/debug.ts} +0 -0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  An opinionated distribution of [**pi**](https://github.com/badlogic/pi) — the coding agent by [Mario Zechner](https://github.com/badlogic). Omegon bundles pi core with extensions for persistent project memory, spec-driven development, local LLM inference, image generation, web search, parallel task decomposition, a live dashboard, and quality-of-life tools.
4
4
 
5
- > **Relationship to pi:** Omegon is not a fork or replacement. It packages pi as a dependency and layers extensions on top. All credit for the pi coding agent goes to Mario Zechner and the pi contributors. The core pi packages (`@cwilson613/pi-coding-agent`) track [upstream `badlogic/pi-mono`](https://github.com/badlogic/pi-mono) daily, adding targeted fixes for OAuth login reliability and bracketed-paste input handling. If you want standalone pi without Omegon's extensions, install `@mariozechner/pi-coding-agent` directly.
5
+ > **Relationship to pi:** Omegon is not a fork or replacement. It packages pi as a dependency and layers extensions on top. All credit for the pi coding agent goes to Mario Zechner and the pi contributors. The core pi packages are migrating to the styrene-lab-owned npm scope (`@styrene-lab/pi-coding-agent` and related packages) so package ownership matches the `styrene-lab/omegon` product boundary. Older `@cwilson613/*` package names are compatibility debt during the transition, not the long-term release boundary. If you want standalone pi without Omegon's extensions, install `@mariozechner/pi-coding-agent` directly.
6
6
 
7
7
  ## Installation
8
8
 
@@ -10,7 +10,7 @@ An opinionated distribution of [**pi**](https://github.com/badlogic/pi) — the
10
10
  npm install -g omegon
11
11
  ```
12
12
 
13
- This installs the `pi` command globally. If a standalone pi package is already installed, omegon will transparently replace it (the same `pi` command, with extensions included). To switch back to standalone pi at any time:
13
+ This installs the canonical `omegon` command globally. A legacy `pi` alias may remain available for compatibility, but the supported lifecycle entrypoint is `omegon`. If a standalone pi package is already installed, omegon transparently takes ownership of the lifecycle boundary so startup, update, verification, and restart all stay inside Omegon control. To switch back to standalone pi at any time:
14
14
 
15
15
  ```bash
16
16
  npm uninstall -g omegon
@@ -20,26 +20,27 @@ npm install -g @mariozechner/pi-coding-agent
20
20
  **First-time setup:**
21
21
 
22
22
  ```bash
23
- pi # start pi in any project directory
23
+ omegon # start Omegon in any project directory
24
24
  /bootstrap # check deps, install missing tools, configure preferences
25
25
  ```
26
26
 
27
27
  ### Keeping up to date
28
28
 
29
- | What to update | How |
30
- |----------------|-----|
31
- | **Omegon extensions** | `/update` or `pi update` (tracks `main` branch) |
32
- | **pi binary** | `/update-pi` checks npm for latest `@cwilson613/pi-coding-agent`, prompts before installing |
29
+ | Context | How |
30
+ |--------|-----|
31
+ | **Installed Omegon (`npm install -g omegon`)** | Run `/update` from inside Omegon. Omegon installs the latest package, verifies the active `omegon` command still resolves to Omegon, clears caches, then asks you to restart Omegon. |
32
+ | **Dev checkout / contributor workflow** | Run `/update` or `./scripts/install-pi.sh`. Both follow the same lifecycle contract: pull/sync, build, refresh dependencies, `npm link --force`, verify the active `omegon` target, then stop at an explicit restart handoff. |
33
+ | **Lightweight cache refresh only** | Run `/refresh`. This clears transient caches and reloads extensions, but it is not equivalent to package/runtime replacement. |
33
34
 
34
35
  > The patched fork syncs from upstream daily via GitHub Actions. Bug fixes and new AI provider support land automatically. If a sync PR has conflicts, they are surfaced for manual review before merging — upstream changes are never silently dropped.
35
36
 
36
- > **Note:** `pi install` and `pi update` track the `main` branch. The version-check extension notifies you when a new Omegon release is available.
37
+ > **Note:** `/update` is the authoritative Omegon update path. It intentionally ends at a verified restart boundary rather than hot-swapping the running process after package/runtime mutation.
37
38
 
38
39
  ## Architecture
39
40
 
40
41
  ![Omegon Architecture](docs/img/architecture.png)
41
42
 
42
- Omegon extends `@cwilson613/pi-coding-agent` with **27 extensions**, **12 skills**, and **4 prompt templates** — loaded automatically on session start.
43
+ Omegon extends `@styrene-lab/pi-coding-agent` with **27 extensions**, **12 skills**, and **4 prompt templates** — loaded automatically on session start.
43
44
 
44
45
  ### Development Methodology
45
46
 
@@ -263,7 +264,8 @@ Pre-built prompts for common workflows:
263
264
  ## Requirements
264
265
 
265
266
  **Required:**
266
- - `@cwilson613/pi-coding-agent` ≥ 0.57 patched fork of [badlogic/pi-mono](https://github.com/badlogic/pi-mono). Install via `npm install -g @cwilson613/pi-coding-agent`. Fork source: [cwilson613/pi-mono](https://github.com/cwilson613/pi-mono)
267
+ - `omegon`install via `npm install -g omegon`; launch via `omegon`
268
+ - `@styrene-lab/pi-coding-agent` ≥ 0.57 underpins Omegon's bundled agent core and tracks a patched fork of [badlogic/pi-mono](https://github.com/badlogic/pi-mono). Fork source: [cwilson613/pi-mono](https://github.com/cwilson613/pi-mono)
267
269
 
268
270
  **Optional (installed by `/bootstrap`):**
269
271
  - [Ollama](https://ollama.ai) — local inference, offline mode, semantic memory search
package/bin/omegon.mjs ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Omegon entry point.
4
+ *
5
+ * Sets PI_CODING_AGENT_DIR to the omegon package root so the bundled agent
6
+ * core loads all agent configuration (extensions, themes, skills, AGENTS.md)
7
+ * from omegon rather than from ~/.pi/agent/.
8
+ *
9
+ * Resolution order for the underlying agent core:
10
+ * 1. vendor/pi-mono (dev mode — git submodule present)
11
+ * 2. node_modules/@styrene-lab/pi-coding-agent (installed via npm)
12
+ */
13
+ import { dirname, join } from "node:path";
14
+ import { existsSync } from "node:fs";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const omegonRoot = dirname(dirname(__filename));
19
+
20
+ const vendorCli = join(omegonRoot, "vendor/pi-mono/packages/coding-agent/dist/cli.js");
21
+ const npmCli = join(omegonRoot, "node_modules/@styrene-lab/pi-coding-agent/dist/cli.js");
22
+ const cli = existsSync(vendorCli) ? vendorCli : npmCli;
23
+ const resolutionMode = cli === vendorCli ? "vendor" : "npm";
24
+
25
+ if (process.argv.includes("--where")) {
26
+ process.stdout.write(JSON.stringify({
27
+ omegonRoot,
28
+ cli,
29
+ resolutionMode,
30
+ agentDir: process.env.PI_CODING_AGENT_DIR ?? omegonRoot,
31
+ executable: "omegon",
32
+ }, null, 2) + "\n");
33
+ process.exit(0);
34
+ }
35
+
36
+ if (!process.env.PI_CODING_AGENT_DIR) {
37
+ process.env.PI_CODING_AGENT_DIR = omegonRoot;
38
+ }
39
+
40
+ await import(cli);
package/bin/pi.mjs CHANGED
@@ -1,30 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Omegon pi entry point.
3
+ * Legacy compatibility shim.
4
4
  *
5
- * Sets PI_CODING_AGENT_DIR to the omegon package root so pi loads all agent
6
- * configuration (extensions, themes, skills, AGENTS.md) from omegon rather
7
- * than from ~/.pi/agent/.
8
- *
9
- * Resolution order for pi core:
10
- * 1. vendor/pi-mono (dev mode — git submodule present)
11
- * 2. node_modules/@cwilson613/pi-coding-agent (installed via npm)
5
+ * `pi` remains available temporarily so existing installs are not stranded,
6
+ * but it immediately re-enters the same Omegon-owned executable boundary as
7
+ * the canonical `omegon` command.
12
8
  */
13
- import { dirname, join } from "node:path";
14
- import { existsSync } from "node:fs";
15
- import { fileURLToPath } from "node:url";
16
-
17
- const __filename = fileURLToPath(import.meta.url);
18
- const omegonRoot = dirname(dirname(__filename));
19
-
20
- // Only set if not already overridden
21
- if (!process.env.PI_CODING_AGENT_DIR) {
22
- process.env.PI_CODING_AGENT_DIR = omegonRoot;
23
- }
24
-
25
- // Resolve pi core: prefer vendor/ (dev), fall back to node_modules/ (installed)
26
- const vendorCli = join(omegonRoot, "vendor/pi-mono/packages/coding-agent/dist/cli.js");
27
- const npmCli = join(omegonRoot, "node_modules/@cwilson613/pi-coding-agent/dist/cli.js");
28
-
29
- const cli = existsSync(vendorCli) ? vendorCli : npmCli;
30
- await import(cli);
9
+ await import("./omegon.mjs");
@@ -14,9 +14,10 @@
14
14
  * Commands: /secrets list, /secrets configure <name>, /secrets rm <name>, /secrets test <name>
15
15
  */
16
16
 
17
- import type { ExtensionAPI } from "@cwilson613/pi-coding-agent";
18
- import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, readdirSync } from "fs";
19
- import { join, resolve } from "path";
17
+ import type { ExtensionAPI } from "@styrene-lab/pi-coding-agent";
18
+ import { existsSync, readFileSync, realpathSync, writeFileSync, appendFileSync, mkdirSync, readdirSync } from "fs";
19
+ import { dirname, join, resolve } from "path";
20
+ import { fileURLToPath } from "url";
20
21
  import { homedir } from "os";
21
22
  import { execSync, execFileSync } from "child_process";
22
23
 
@@ -51,17 +52,33 @@ function scanAnnotations(): {
51
52
  const secretPattern = /^\/\/\s*@secret\s+([A-Z_][A-Z0-9_]*)\s+"([^"]+)"/;
52
53
  const configPattern = /^\/\/\s*@config\s+([A-Z_][A-Z0-9_]*)\s+"([^"]+)"(?:\s+\[default:\s*([^\]]*)\])?/;
53
54
 
54
- // Extension directories to scan
55
- const extensionDirs = [
56
- join(homedir(), ".pi", "agent", "extensions"),
57
- join(homedir(), ".pi", "agent", "git"), // Omegon and other git packages
58
- ];
55
+ // Extension directories to scan (deduplicated by realpath to avoid
56
+ // double-walking when dev checkout overlaps with git-installed package)
57
+ const seen = new Set<string>();
58
+ const extensionDirs: string[] = [];
59
+ function addDir(dir: string) {
60
+ try {
61
+ const real = realpathSync(dir);
62
+ if (!seen.has(real)) { seen.add(real); extensionDirs.push(dir); }
63
+ } catch {
64
+ // realpathSync fails if dir doesn't exist — skip silently
65
+ }
66
+ }
67
+
68
+ addDir(join(homedir(), ".pi", "agent", "extensions"));
69
+ addDir(join(homedir(), ".pi", "agent", "git")); // Omegon and other git packages
70
+
71
+ // Scan the package's own extensions/ directory (where this file lives).
72
+ // Covers both dev (repo checkout) and npm-installed modes.
73
+ try {
74
+ const thisDir = dirname(fileURLToPath(import.meta.url));
75
+ addDir(resolve(thisDir, "..")); // 00-secrets/ → extensions/
76
+ } catch {}
59
77
 
60
78
  // Also scan project-local extensions
61
79
  try {
62
80
  const cwd = process.cwd();
63
- const projectDir = join(cwd, ".pi", "extensions");
64
- if (existsSync(projectDir)) extensionDirs.push(projectDir);
81
+ addDir(join(cwd, ".pi", "extensions"));
65
82
  } catch {}
66
83
 
67
84
  function scanFile(filePath: string) {
@@ -367,11 +384,17 @@ const SECRET_ACCESS_PATTERNS = [
367
384
  /\bvault\s+(read|kv\s+get)\b/i,
368
385
 
369
386
  // ── Environment variable dumping ──
370
- // Targeted env access with secret-adjacent keywords
371
- /\benv\b.*\b(key|token|secret|password|credential)/i,
372
- /\bprintenv\b.*\b(key|token|secret|password|credential)/i,
373
- // Echo/printf of known secret env vars
374
- /\b(echo|printf)\s+.*\$[A-Z_]*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)/i,
387
+ // Targeted env/printenv commands with secret-adjacent keywords.
388
+ // Match only the standalone `env` or `printenv` command, not the substring
389
+ // "env" in filenames like "env-api-keys.ts" or words like "environment".
390
+ /(?:^|\||\;|&&|\|\|)\s*env\s+.*\b(key|token|secret|password|credential)/i,
391
+ /(?:^|\||\;|&&|\|\|)\s*printenv\s+.*\b(key|token|secret|password|credential)/i,
392
+ // Bare `env` piped to grep/filter for secrets (full dump → filter pattern).
393
+ // Use looser keyword matching (no leading \b) since env var names use
394
+ // underscores: API_KEY, _TOKEN, etc. where \b won't match mid-identifier.
395
+ /(?:^|\||\;|&&|\|\|)\s*env\s*\|.*(key|token|secret|password|credential)/i,
396
+ // Echo/printf of known secret env vars (literal $VAR references, not prose)
397
+ /\b(echo|printf)\s+.*\$[A-Z_]*(API_KEY|_TOKEN|_SECRET|_PASSWORD|_CREDENTIAL)\b/i,
375
398
  // Full env dumps (these can leak all injected secrets)
376
399
  /\bnode\s+-e\s+.*process\.env/i,
377
400
  /\bpython[23]?\s+-c\s+.*os\.environ/i,
@@ -379,12 +402,18 @@ const SECRET_ACCESS_PATTERNS = [
379
402
  /\bperl\s+-e\s+.*%ENV/i,
380
403
 
381
404
  // ── File readers on sensitive paths ──
382
- // cat/less/more/head/tail/bat on secret-adjacent files
383
- /\b(cat|less|more|head|tail|bat|batcat)\b.*(secrets?\.json|\bcredentials?\b|\.env\b)/i,
405
+ // cat/less/more/head/tail/bat on actual secret/credential files.
406
+ // Match files like "secrets.json", "credentials", ".env" — but not source code
407
+ // files that happen to contain the word "credential" (e.g. provider-env.ts).
408
+ // The path must look like a config/data file, not a .ts/.js/.py source file.
409
+ /\b(cat|less|more|head|tail|bat|batcat)\s+\S*secrets?\.json\b/i,
410
+ /\b(cat|less|more|head|tail|bat|batcat)\s+\S*credentials\s*$/im,
411
+ /\b(cat|less|more|head|tail|bat|batcat)\s+\S*\.env(\.[a-z]+)?\s*$/im,
384
412
  // jq on secret files
385
- /\bjq\b.*\b(secrets?\.json|credentials?)\b/i,
386
- // sed/awk/grep reading secret files
387
- /\b(sed|awk|grep)\b.*\b(secrets?\.json|credentials?)\b/i,
413
+ /\bjq\b.*\b(secrets?\.json)\b/i,
414
+ /\bjq\b\s+\S+\s+\S*credentials\s*$/im,
415
+ // sed/awk/grep on actual secret data files (not source code containing the word)
416
+ /\b(sed|awk|grep)\b.*\bsecrets?\.json\b/i,
388
417
  // Our own secrets file — match the specific path
389
418
  /\.pi\/agent\/secrets\.json/i,
390
419
  // Writing to secrets file (via tee, redirect, etc.)
@@ -393,8 +422,9 @@ const SECRET_ACCESS_PATTERNS = [
393
422
  /\b(cat|less|more|head|tail)\b.*\.(aws|gcloud)\/(credentials|config)/i,
394
423
 
395
424
  // ── Command wrapping (shell indirection) ──
396
- // sh/bash/zsh -c wrapping with secret-adjacent content
397
- /\b(sh|bash|zsh)\s+-c\s+.*\b(security|op\s+read|pass\s+show|vault\s+read|keychain|credential|secret)/i,
425
+ // sh/bash/zsh -c wrapping with actual secret store tool invocations.
426
+ // Narrow: only match specific tool commands, not prose containing "secret".
427
+ /\b(sh|bash|zsh)\s+-c\s+.*\b(security\s+find|op\s+(read|get|item)|pass\s+show|vault\s+(read|kv\s+get)|keychain)/i,
398
428
  // Python/Ruby/Node/Perl subprocess wrappers accessing secret stores
399
429
  /\b(python[23]?|ruby|node|perl)\b.*\b(security\s+find|op\s+read|find-generic-password|secrets?\.json)/i,
400
430
  // Base64 decode piped to shell (obfuscation technique)
@@ -580,6 +610,39 @@ export default function (pi: ExtensionAPI) {
580
610
  "error"
581
611
  );
582
612
  }
613
+
614
+ // Warn about secrets resolved from bare env vars (no recipe — set in
615
+ // .bashrc/.zshrc or shell profile). These are insecure: visible in
616
+ // /proc/*/environ, inherited by every child process, persisted in
617
+ // dotfile repos and shell history. A keychain-backed recipe resolves
618
+ // at runtime with biometric/password auth and doesn't leak on disk.
619
+ //
620
+ // Skip in CI environments where env vars are the expected mechanism.
621
+ // Exempt tokens managed by their own CLI credential stores (e.g.
622
+ // GH_TOKEN set by `gh auth login`, GITHUB_TOKEN from gh, COPILOT_GITHUB_TOKEN
623
+ // from the Copilot extension) — these are already secured by the tool.
624
+ const isCI = !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI);
625
+ if (!isCI) {
626
+ // Tokens managed by CLI tools that handle their own credential storage
627
+ const CLI_MANAGED_TOKENS = new Set([
628
+ "GH_TOKEN", "GITHUB_TOKEN", "COPILOT_GITHUB_TOKEN", // gh auth login
629
+ "GITLAB_TOKEN", // glab auth login
630
+ "AWS_PROFILE", // aws configure / SSO profile name (not a secret)
631
+ ]);
632
+ const bareEnvSecrets = Object.keys(KNOWN_SECRETS).filter(name =>
633
+ resolvedCache.has(name) && !recipes[name] && !CLI_MANAGED_TOKENS.has(name)
634
+ );
635
+ if (bareEnvSecrets.length > 0) {
636
+ // Show at most 3 names to avoid wall-of-text, never show values
637
+ const examples = bareEnvSecrets.slice(0, 3).join(", ");
638
+ const more = bareEnvSecrets.length > 3 ? ` (+${bareEnvSecrets.length - 3} more)` : "";
639
+ ctx.ui.notify(
640
+ `🔓 ${bareEnvSecrets.length} secret${bareEnvSecrets.length !== 1 ? "s" : ""} loaded from plain env vars: ${examples}${more}\n` +
641
+ `Run \`/secrets configure <name>\` to migrate to a secure backend.`,
642
+ "warning"
643
+ );
644
+ }
645
+ }
583
646
  });
584
647
 
585
648
  // ──────────────────────────────────────────────────────────────
@@ -824,36 +887,60 @@ export default function (pi: ExtensionAPI) {
824
887
  case "list": {
825
888
  const lines: string[] = ["Secret recipes (~/.pi/agent/secrets.json):", ""];
826
889
 
890
+ function describeSource(name: string, recipe: string | undefined, resolved: boolean): string {
891
+ if (recipe) {
892
+ if (recipe.startsWith("!")) return `command: ${recipe.slice(1, 40)}${recipe.length > 41 ? "..." : ""}`;
893
+ if (recipe.startsWith("literal:")) return "⚠️ literal value (insecure — run /secrets configure to migrate)";
894
+ return `env: ${recipe}`;
895
+ }
896
+ if (resolved) return "🔓 plain env var (run /secrets configure to use a secure backend)";
897
+ return "not configured";
898
+ }
899
+
900
+ // Split into resolved and unresolved for cleaner display
901
+ const resolvedEntries: Array<[string, string]> = [];
902
+ const unresolvedEntries: Array<[string, string]> = [];
827
903
  for (const [name, desc] of Object.entries(KNOWN_SECRETS)) {
828
- const recipe = recipes[name];
829
904
  const resolved = resolvedCache.has(name);
830
- const source = recipe
831
- ? recipe.startsWith("!")
832
- ? `command: ${recipe.slice(1, 40)}${recipe.length > 41 ? "..." : ""}`
833
- : recipe.startsWith("literal:")
834
- ? "⚠️ literal value (insecure — run /secrets configure to migrate)"
835
- : `env: ${recipe}`
836
- : resolved
837
- ? "env (auto-detected)"
838
- : "not configured";
905
+ if (resolved || recipes[name]) {
906
+ resolvedEntries.push([name, desc]);
907
+ } else {
908
+ unresolvedEntries.push([name, desc]);
909
+ }
910
+ }
839
911
 
912
+ // Show resolved/configured secrets first
913
+ for (const [name, desc] of resolvedEntries) {
914
+ const recipe = recipes[name];
915
+ const resolved = resolvedCache.has(name);
840
916
  const status = resolved ? "✅" : "❌";
841
917
  lines.push(` ${status} ${name}`);
842
918
  lines.push(` ${desc}`);
843
- lines.push(` Source: ${source}`);
919
+ lines.push(` Source: ${describeSource(name, recipe, resolved)}`);
844
920
  lines.push("");
845
921
  }
846
922
 
847
- // Show any non-known secrets
923
+ // Show any non-known custom secrets
848
924
  for (const name of Object.keys(recipes)) {
849
925
  if (name in KNOWN_SECRETS) continue;
850
926
  const recipe = recipes[name];
851
927
  const resolved = resolvedCache.has(name);
852
928
  const status = resolved ? "✅" : "❌";
853
929
  lines.push(` ${status} ${name} (custom)`);
854
- lines.push(
855
- ` Source: ${recipe.startsWith("!") ? `command: ${recipe.slice(1, 40)}` : recipe.startsWith("literal:") ? "⚠️ literal (insecure)" : `env: ${recipe}`}`
856
- );
930
+ lines.push(` Source: ${describeSource(name, recipe, resolved)}`);
931
+ lines.push("");
932
+ }
933
+
934
+ // Unconfigured secrets: collapsed summary instead of a wall
935
+ if (unresolvedEntries.length > 0) {
936
+ const names = unresolvedEntries.map(([n]) => n);
937
+ if (names.length <= 5) {
938
+ lines.push(` Not configured: ${names.join(", ")}`);
939
+ } else {
940
+ const shown = names.slice(0, 5).join(", ");
941
+ lines.push(` Not configured: ${shown} (+${names.length - 5} more)`);
942
+ }
943
+ lines.push(` Run /secrets configure <name> to set up any of these.`);
857
944
  lines.push("");
858
945
  }
859
946
 
@@ -1032,14 +1119,34 @@ export default function (pi: ExtensionAPI) {
1032
1119
  saveRecipes(recipes);
1033
1120
 
1034
1121
  // Verify it actually resolves — this is the moment of truth
1122
+ // Note: resolveSecret checks process.env FIRST (for CI compat), so if the
1123
+ // env var is still set from the user's shell profile, it shadows the recipe.
1124
+ // We detect this and warn the user to remove the export.
1035
1125
  resolvedCache.delete(secretName);
1036
- const value = resolveSecret(secretName);
1126
+ const envShadowed = !!process.env[secretName];
1127
+ // Temporarily clear env to test the recipe in isolation
1128
+ const savedEnv = process.env[secretName];
1129
+ if (envShadowed) delete process.env[secretName];
1130
+ const recipeValue = resolveSecret(secretName);
1131
+ // Restore env (it'll be the active source until user removes it)
1132
+ if (savedEnv !== undefined) {
1133
+ process.env[secretName] = savedEnv;
1134
+ resolvedCache.delete(secretName);
1135
+ resolvedCache.set(secretName, savedEnv);
1136
+ }
1137
+ const value = recipeValue || savedEnv;
1037
1138
  if (value) {
1038
1139
  process.env[secretName] = value;
1039
1140
  const masked = value.length > 8
1040
1141
  ? value.slice(0, 4) + "•".repeat(Math.min(value.length - 4, 16)) + ` (${value.length} chars)`
1041
1142
  : "•".repeat(value.length) + ` (${value.length} chars)`;
1042
- ctx.ui.notify(`✅ ${secretName} configured and verified: ${masked}`, "info");
1143
+ let msg = `✅ ${secretName} configured and verified: ${masked}`;
1144
+ if (envShadowed && recipeValue) {
1145
+ msg += `\n\n⚠️ Note: \$${secretName} is also set in your shell environment.` +
1146
+ `\nRemove the \`export ${secretName}=...\` from your shell profile` +
1147
+ `\nso the secure backend is used instead of the plain env var.`;
1148
+ }
1149
+ ctx.ui.notify(msg, "info");
1043
1150
  } else {
1044
1151
  // Don't just warn — this is a failure. Remove the broken recipe.
1045
1152
  delete recipes[secretName];
@@ -5,7 +5,7 @@
5
5
  * pi-tui/pi-coding-agent dependencies (which aren't resolvable under tsx).
6
6
  */
7
7
 
8
- import type { ExtensionAPI } from "@cwilson613/pi-coding-agent";
8
+ import type { ExtensionAPI } from "@styrene-lab/pi-coding-agent";
9
9
 
10
10
  // ─── Types ───────────────────────────────────────────────────────
11
11
 
@@ -25,10 +25,10 @@
25
25
  * Load order: 01-auth loads after 00-secrets, so process.env is populated.
26
26
  */
27
27
 
28
- import type { ExtensionAPI } from "@cwilson613/pi-coding-agent";
29
- import { Text } from "@cwilson613/pi-tui";
28
+ import type { ExtensionAPI } from "@styrene-lab/pi-coding-agent";
29
+ import { Text } from "@styrene-lab/pi-tui";
30
30
  import { Type } from "@sinclair/typebox";
31
- import { sciCall, sciOk, sciErr, sciExpanded } from "../sci-ui.ts";
31
+ import { sciCall, sciOk, sciErr, sciExpanded } from "../lib/sci-ui.ts";
32
32
 
33
33
  // Import domain logic from auth.ts (testable without pi-tui dependency)
34
34
  import {
@@ -12,7 +12,7 @@
12
12
  * AUTO_COMPACT_COOLDOWN — minimum seconds between compactions (default: 60)
13
13
  */
14
14
 
15
- import type { ExtensionAPI, ExtensionContext } from "@cwilson613/pi-coding-agent";
15
+ import type { ExtensionAPI, ExtensionContext } from "@styrene-lab/pi-coding-agent";
16
16
 
17
17
  const COMPACT_PERCENT = Number(process.env.AUTO_COMPACT_PERCENT) || 70;
18
18
  const COOLDOWN_MS = (Number(process.env.AUTO_COMPACT_COOLDOWN) || 60) * 1000;
@@ -46,9 +46,46 @@ function hasCmd(cmd: string): boolean {
46
46
  }
47
47
  }
48
48
 
49
+ /**
50
+ * Detect immutable/atomic Linux distros (Bazzite, Silverblue, Kinoite, etc.)
51
+ * where dnf/apt are unavailable or aliased to guides. These distros typically
52
+ * use Homebrew (Linuxbrew) or Flatpak for user-space packages.
53
+ */
54
+ function isImmutableLinux(): boolean {
55
+ if (process.platform !== "linux") return false;
56
+ try {
57
+ const osRelease = execSync("cat /etc/os-release 2>/dev/null", { encoding: "utf-8" });
58
+ // Bazzite, Silverblue, Kinoite, Aurora, Bluefin — all Fedora Atomic variants
59
+ return /VARIANT_ID=.*(silverblue|kinoite|bazzite|aurora|bluefin|atomic)/i.test(osRelease)
60
+ || /ostree/i.test(osRelease);
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ /** Cached immutable Linux detection */
67
+ const _isImmutable = isImmutableLinux();
68
+
49
69
  /** Get the best install command for the current platform */
50
70
  export function bestInstallCmd(dep: Dep): string | undefined {
51
71
  const plat = process.platform === "darwin" ? "darwin" : "linux";
72
+
73
+ // On immutable Linux (Bazzite, Silverblue, etc.), dnf/apt are unavailable
74
+ // or aliased to documentation guides. Prefer brew commands.
75
+ // On regular Linux, prefer non-brew (apt/dnf) unless brew is the only option.
76
+ const hasBrew = hasCmd("brew");
77
+ if (plat === "linux" && (_isImmutable || !hasBrew)) {
78
+ // Immutable: must use brew (skip apt/dnf). Regular without brew: skip brew commands.
79
+ const candidates = dep.install.filter((o) => o.platform === plat || o.platform === "any");
80
+ if (_isImmutable && hasBrew) {
81
+ const brewCmd = candidates.find((o) => o.cmd.startsWith("brew "));
82
+ if (brewCmd) return brewCmd.cmd;
83
+ } else if (!_isImmutable) {
84
+ const nonBrew = candidates.find((o) => !o.cmd.startsWith("brew "));
85
+ if (nonBrew) return nonBrew.cmd;
86
+ }
87
+ }
88
+
52
89
  return (
53
90
  dep.install.find((o) => o.platform === plat)?.cmd ??
54
91
  dep.install.find((o) => o.platform === "any")?.cmd ??
@@ -108,6 +145,7 @@ export const DEPS: Dep[] = [
108
145
  check: () => hasCmd("gh"),
109
146
  install: [
110
147
  { platform: "darwin", cmd: "brew install gh" },
148
+ { platform: "linux", cmd: "brew install gh" },
111
149
  { platform: "linux", cmd: "sudo apt install gh || sudo dnf install gh" },
112
150
  ],
113
151
  url: "https://cli.github.com",
@@ -163,6 +201,7 @@ export const DEPS: Dep[] = [
163
201
  check: () => hasCmd("rsvg-convert"),
164
202
  install: [
165
203
  { platform: "darwin", cmd: "brew install librsvg" },
204
+ { platform: "linux", cmd: "brew install librsvg" },
166
205
  { platform: "linux", cmd: "sudo apt install librsvg2-bin" },
167
206
  ],
168
207
  },
@@ -175,6 +214,7 @@ export const DEPS: Dep[] = [
175
214
  check: () => hasCmd("pdftoppm"),
176
215
  install: [
177
216
  { platform: "darwin", cmd: "brew install poppler" },
217
+ { platform: "linux", cmd: "brew install poppler" },
178
218
  { platform: "linux", cmd: "sudo apt install poppler-utils" },
179
219
  ],
180
220
  },
@@ -200,6 +240,7 @@ export const DEPS: Dep[] = [
200
240
  check: () => hasCmd("aws"),
201
241
  install: [
202
242
  { platform: "darwin", cmd: "brew install awscli" },
243
+ { platform: "linux", cmd: "brew install awscli" },
203
244
  { platform: "linux", cmd: "sudo apt install awscli" },
204
245
  ],
205
246
  },
@@ -212,6 +253,7 @@ export const DEPS: Dep[] = [
212
253
  check: () => hasCmd("kubectl"),
213
254
  install: [
214
255
  { platform: "darwin", cmd: "brew install kubectl" },
256
+ { platform: "linux", cmd: "brew install kubectl" },
215
257
  { platform: "linux", cmd: "sudo apt install kubectl" },
216
258
  ],
217
259
  },