omegon 0.6.3 → 0.6.5
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 +12 -10
- package/bin/omegon.mjs +80 -0
- package/bin/pi.mjs +5 -26
- package/extensions/00-secrets/index.ts +146 -39
- package/extensions/01-auth/auth.ts +1 -1
- package/extensions/01-auth/index.ts +3 -3
- package/extensions/auto-compact.ts +1 -1
- package/extensions/bootstrap/deps.ts +42 -0
- package/extensions/bootstrap/index.ts +327 -110
- package/extensions/chronos/index.ts +1 -1
- package/extensions/cleave/dispatcher.ts +6 -6
- package/extensions/cleave/index.ts +6 -6
- package/extensions/cleave/planner.ts +1 -1
- package/extensions/cleave/worktree.ts +1 -1
- package/extensions/core-renderers.ts +24 -84
- package/extensions/dashboard/footer.ts +184 -40
- package/extensions/dashboard/git.ts +2 -2
- package/extensions/dashboard/index.ts +4 -4
- package/extensions/dashboard/overlay-data.ts +5 -5
- package/extensions/dashboard/overlay.ts +5 -5
- package/extensions/dashboard/render-utils.ts +1 -1
- package/extensions/dashboard/types.ts +15 -0
- package/extensions/defaults.ts +4 -12
- package/extensions/design-tree/dashboard-state.ts +6 -6
- package/extensions/design-tree/design-card.ts +3 -3
- package/extensions/design-tree/index.ts +64 -44
- package/extensions/design-tree/types.ts +4 -2
- package/extensions/distill.ts +1 -1
- package/extensions/effort/index.ts +137 -10
- package/extensions/lib/model-routing.ts +304 -32
- package/extensions/lib/operator-fallback.ts +1 -1
- package/extensions/lib/operator-profile.ts +1 -1
- package/extensions/lib/provider-env.ts +163 -0
- package/extensions/{sci-ui.ts → lib/sci-ui.ts} +119 -2
- package/extensions/{shared-state.ts → lib/shared-state.ts} +13 -9
- package/extensions/lib/slash-command-bridge.ts +1 -1
- package/extensions/{types.d.ts → lib/types.d.ts} +3 -3
- package/extensions/local-inference/index.ts +1 -1
- package/extensions/mcp-bridge/index.ts +1 -1
- package/extensions/model-budget.ts +10 -10
- package/extensions/offline-driver.ts +11 -4
- package/extensions/openspec/archive-gate.ts +1 -1
- package/extensions/openspec/branch-cleanup.ts +1 -1
- package/extensions/openspec/dashboard-state.ts +3 -3
- package/extensions/openspec/index.ts +5 -5
- package/extensions/project-memory/factstore.ts +5 -11
- package/extensions/project-memory/index.ts +48 -34
- package/extensions/project-memory/package.json +1 -1
- package/extensions/project-memory/sci-renderers.ts +1 -1
- package/extensions/render/index.ts +1 -1
- package/extensions/session-log.ts +1 -1
- package/extensions/spinner-verbs.ts +1 -1
- package/extensions/style.ts +1 -1
- package/extensions/terminal-title.ts +3 -3
- package/extensions/tool-profile/index.ts +1 -1
- package/extensions/vault/index.ts +1 -1
- package/extensions/version-check.ts +13 -9
- package/extensions/view/index.ts +4 -4
- package/extensions/web-search/index.ts +5 -2
- package/extensions/web-ui/index.ts +1 -1
- package/extensions/web-ui/state.ts +1 -1
- package/package.json +8 -7
- package/scripts/preinstall.sh +19 -3
- package/scripts/publish-pi-mono.sh +92 -0
- package/skills/pi-extensions/SKILL.md +2 -2
- package/skills/pi-tui/SKILL.md +17 -17
- package/skills/typescript/SKILL.md +1 -1
- package/themes/alpharius.json +7 -6
- /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 (`@
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
|
30
|
-
|
|
31
|
-
| **Omegon
|
|
32
|
-
| **
|
|
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:** `
|
|
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
|

|
|
41
42
|
|
|
42
|
-
Omegon extends `@
|
|
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
|
-
-
|
|
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,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Omegon entry point.
|
|
4
|
+
*
|
|
5
|
+
* Keeps mutable user state in the shared pi-compatible agent directory while
|
|
6
|
+
* injecting Omegon-packaged resources from the installed package root.
|
|
7
|
+
*
|
|
8
|
+
* Resolution order for the underlying agent core:
|
|
9
|
+
* 1. vendor/pi-mono (dev mode — git submodule present)
|
|
10
|
+
* 2. node_modules/@styrene-lab/pi-coding-agent (installed via npm)
|
|
11
|
+
*/
|
|
12
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const omegonRoot = dirname(dirname(__filename));
|
|
19
|
+
const defaultStateDir = join(homedir(), ".pi", "agent");
|
|
20
|
+
const stateDir = process.env.PI_CODING_AGENT_DIR || defaultStateDir;
|
|
21
|
+
const usingExplicitStateOverride = Boolean(process.env.PI_CODING_AGENT_DIR);
|
|
22
|
+
|
|
23
|
+
const vendorCli = join(omegonRoot, "vendor/pi-mono/packages/coding-agent/dist/cli.js");
|
|
24
|
+
const npmCli = join(omegonRoot, "node_modules/@styrene-lab/pi-coding-agent/dist/cli.js");
|
|
25
|
+
const cli = existsSync(vendorCli) ? vendorCli : npmCli;
|
|
26
|
+
const resolutionMode = cli === vendorCli ? "vendor" : "npm";
|
|
27
|
+
|
|
28
|
+
function migrateLegacyStatePath(relativePath, kind = "file") {
|
|
29
|
+
if (usingExplicitStateOverride) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const legacyPath = join(omegonRoot, relativePath);
|
|
34
|
+
const targetPath = join(stateDir, relativePath);
|
|
35
|
+
if (!existsSync(legacyPath) || existsSync(targetPath)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
40
|
+
if (kind === "directory") {
|
|
41
|
+
cpSync(legacyPath, targetPath, { recursive: true, force: false });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
copyFileSync(legacyPath, targetPath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function injectBundledResourceArgs(argv) {
|
|
48
|
+
const injected = [...argv];
|
|
49
|
+
const pushPair = (flag, value) => {
|
|
50
|
+
if (existsSync(value)) {
|
|
51
|
+
injected.push(flag, value);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
pushPair("--extension", omegonRoot);
|
|
56
|
+
pushPair("--skill", join(omegonRoot, "skills"));
|
|
57
|
+
pushPair("--prompt-template", join(omegonRoot, "prompts"));
|
|
58
|
+
pushPair("--theme", join(omegonRoot, "themes"));
|
|
59
|
+
return injected;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (process.argv.includes("--where")) {
|
|
63
|
+
process.stdout.write(JSON.stringify({
|
|
64
|
+
omegonRoot,
|
|
65
|
+
cli,
|
|
66
|
+
resolutionMode,
|
|
67
|
+
agentDir: stateDir,
|
|
68
|
+
stateDir,
|
|
69
|
+
executable: "omegon",
|
|
70
|
+
}, null, 2) + "\n");
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
process.env.PI_CODING_AGENT_DIR = stateDir;
|
|
75
|
+
migrateLegacyStatePath("auth.json");
|
|
76
|
+
migrateLegacyStatePath("settings.json");
|
|
77
|
+
migrateLegacyStatePath("sessions", "directory");
|
|
78
|
+
process.argv = injectBundledResourceArgs(process.argv);
|
|
79
|
+
|
|
80
|
+
await import(cli);
|
package/bin/pi.mjs
CHANGED
|
@@ -1,30 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Legacy compatibility shim.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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 "@
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
383
|
-
|
|
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
|
|
386
|
-
|
|
387
|
-
|
|
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
|
|
397
|
-
|
|
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
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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: ${
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 "@
|
|
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 "@
|
|
29
|
-
import { Text } from "@
|
|
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 "@
|
|
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
|
},
|