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.
Files changed (160) hide show
  1. package/.gitattributes +3 -0
  2. package/AGENTS.md +16 -0
  3. package/LICENSE +15 -0
  4. package/README.md +289 -0
  5. package/bin/pi.mjs +30 -0
  6. package/extensions/00-secrets/index.ts +1126 -0
  7. package/extensions/01-auth/auth.ts +401 -0
  8. package/extensions/01-auth/index.ts +289 -0
  9. package/extensions/auto-compact.ts +42 -0
  10. package/extensions/bootstrap/deps.ts +291 -0
  11. package/extensions/bootstrap/index.ts +811 -0
  12. package/extensions/chronos/chronos.sh +487 -0
  13. package/extensions/chronos/index.ts +148 -0
  14. package/extensions/cleave/assessment.ts +754 -0
  15. package/extensions/cleave/bridge.ts +31 -0
  16. package/extensions/cleave/conflicts.ts +250 -0
  17. package/extensions/cleave/dispatcher.ts +808 -0
  18. package/extensions/cleave/guardrails.ts +426 -0
  19. package/extensions/cleave/index.ts +3121 -0
  20. package/extensions/cleave/lifecycle-emitter.ts +20 -0
  21. package/extensions/cleave/openspec.ts +811 -0
  22. package/extensions/cleave/planner.ts +260 -0
  23. package/extensions/cleave/review.ts +579 -0
  24. package/extensions/cleave/skills.ts +355 -0
  25. package/extensions/cleave/types.ts +261 -0
  26. package/extensions/cleave/workspace.ts +861 -0
  27. package/extensions/cleave/worktree.ts +243 -0
  28. package/extensions/core-renderers.ts +253 -0
  29. package/extensions/dashboard/context-gauge.ts +58 -0
  30. package/extensions/dashboard/file-watch.ts +14 -0
  31. package/extensions/dashboard/footer.ts +1145 -0
  32. package/extensions/dashboard/git.ts +185 -0
  33. package/extensions/dashboard/index.ts +478 -0
  34. package/extensions/dashboard/memory-audit.ts +34 -0
  35. package/extensions/dashboard/overlay-data.ts +705 -0
  36. package/extensions/dashboard/overlay.ts +365 -0
  37. package/extensions/dashboard/render-utils.ts +54 -0
  38. package/extensions/dashboard/types.ts +191 -0
  39. package/extensions/dashboard/uri-helper.ts +45 -0
  40. package/extensions/debug.ts +69 -0
  41. package/extensions/defaults.ts +282 -0
  42. package/extensions/design-tree/dashboard-state.ts +161 -0
  43. package/extensions/design-tree/design-card.ts +362 -0
  44. package/extensions/design-tree/index.ts +2130 -0
  45. package/extensions/design-tree/lifecycle-emitter.ts +41 -0
  46. package/extensions/design-tree/tree.ts +1607 -0
  47. package/extensions/design-tree/types.ts +163 -0
  48. package/extensions/distill.ts +127 -0
  49. package/extensions/effort/index.ts +395 -0
  50. package/extensions/effort/tiers.ts +146 -0
  51. package/extensions/effort/types.ts +105 -0
  52. package/extensions/lib/git-state.ts +227 -0
  53. package/extensions/lib/local-models.ts +157 -0
  54. package/extensions/lib/model-preferences.ts +51 -0
  55. package/extensions/lib/model-routing.ts +720 -0
  56. package/extensions/lib/operator-fallback.ts +205 -0
  57. package/extensions/lib/operator-profile.ts +360 -0
  58. package/extensions/lib/slash-command-bridge.ts +253 -0
  59. package/extensions/lib/typebox-helpers.ts +16 -0
  60. package/extensions/local-inference/index.ts +727 -0
  61. package/extensions/mcp-bridge/README.md +220 -0
  62. package/extensions/mcp-bridge/index.ts +951 -0
  63. package/extensions/mcp-bridge/lib.ts +365 -0
  64. package/extensions/mcp-bridge/mcp.json +3 -0
  65. package/extensions/mcp-bridge/package.json +11 -0
  66. package/extensions/model-budget.ts +752 -0
  67. package/extensions/offline-driver.ts +403 -0
  68. package/extensions/openspec/archive-gate.ts +164 -0
  69. package/extensions/openspec/branch-cleanup.ts +64 -0
  70. package/extensions/openspec/dashboard-state.ts +50 -0
  71. package/extensions/openspec/index.ts +1917 -0
  72. package/extensions/openspec/lifecycle-emitter.ts +65 -0
  73. package/extensions/openspec/lifecycle-files.ts +70 -0
  74. package/extensions/openspec/lifecycle.ts +50 -0
  75. package/extensions/openspec/reconcile.ts +187 -0
  76. package/extensions/openspec/spec.ts +1385 -0
  77. package/extensions/openspec/types.ts +98 -0
  78. package/extensions/project-memory/DESIGN-global-mind.md +198 -0
  79. package/extensions/project-memory/README.md +202 -0
  80. package/extensions/project-memory/api-types.ts +382 -0
  81. package/extensions/project-memory/compaction-policy.ts +29 -0
  82. package/extensions/project-memory/core.ts +164 -0
  83. package/extensions/project-memory/embeddings.ts +230 -0
  84. package/extensions/project-memory/extraction-v2.ts +861 -0
  85. package/extensions/project-memory/factstore.ts +2177 -0
  86. package/extensions/project-memory/index.ts +3459 -0
  87. package/extensions/project-memory/injection-metrics.ts +91 -0
  88. package/extensions/project-memory/jsonl-io.ts +12 -0
  89. package/extensions/project-memory/lifecycle.ts +331 -0
  90. package/extensions/project-memory/migration.ts +293 -0
  91. package/extensions/project-memory/package.json +9 -0
  92. package/extensions/project-memory/sci-renderers.ts +7 -0
  93. package/extensions/project-memory/template.ts +103 -0
  94. package/extensions/project-memory/triggers.ts +52 -0
  95. package/extensions/project-memory/types.ts +102 -0
  96. package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
  97. package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
  98. package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
  99. package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
  100. package/extensions/render/composition/package-lock.json +534 -0
  101. package/extensions/render/composition/package.json +22 -0
  102. package/extensions/render/composition/render.mjs +246 -0
  103. package/extensions/render/composition/test-comp.tsx +87 -0
  104. package/extensions/render/composition/types.ts +24 -0
  105. package/extensions/render/excalidraw/UPSTREAM.md +81 -0
  106. package/extensions/render/excalidraw/elements.ts +764 -0
  107. package/extensions/render/excalidraw/index.ts +66 -0
  108. package/extensions/render/excalidraw/types.ts +223 -0
  109. package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
  110. package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
  111. package/extensions/render/excalidraw-renderer/render_template.html +59 -0
  112. package/extensions/render/index.ts +830 -0
  113. package/extensions/render/native-diagrams/index.ts +57 -0
  114. package/extensions/render/native-diagrams/motifs.ts +542 -0
  115. package/extensions/render/native-diagrams/raster.ts +8 -0
  116. package/extensions/render/native-diagrams/scene.ts +75 -0
  117. package/extensions/render/native-diagrams/spec.ts +204 -0
  118. package/extensions/render/native-diagrams/svg.ts +116 -0
  119. package/extensions/sci-ui.ts +304 -0
  120. package/extensions/session-log.ts +174 -0
  121. package/extensions/shared-state.ts +146 -0
  122. package/extensions/spinner-verbs.ts +91 -0
  123. package/extensions/style.ts +281 -0
  124. package/extensions/terminal-title.ts +191 -0
  125. package/extensions/tool-profile/index.ts +291 -0
  126. package/extensions/tool-profile/profiles.ts +290 -0
  127. package/extensions/types.d.ts +9 -0
  128. package/extensions/vault/index.ts +185 -0
  129. package/extensions/version-check.ts +90 -0
  130. package/extensions/view/index.ts +859 -0
  131. package/extensions/view/uri-resolver.ts +148 -0
  132. package/extensions/web-search/index.ts +182 -0
  133. package/extensions/web-search/providers.ts +121 -0
  134. package/extensions/web-ui/index.ts +110 -0
  135. package/extensions/web-ui/server.ts +265 -0
  136. package/extensions/web-ui/state.ts +462 -0
  137. package/extensions/web-ui/static/index.html +145 -0
  138. package/extensions/web-ui/types.ts +284 -0
  139. package/package.json +76 -0
  140. package/prompts/init.md +75 -0
  141. package/prompts/new-repo.md +54 -0
  142. package/prompts/oci-login.md +56 -0
  143. package/prompts/status.md +50 -0
  144. package/settings.json +4 -0
  145. package/skills/cleave/SKILL.md +218 -0
  146. package/skills/git/SKILL.md +209 -0
  147. package/skills/git/_reference/ci-validation.md +204 -0
  148. package/skills/oci/SKILL.md +338 -0
  149. package/skills/openspec/SKILL.md +346 -0
  150. package/skills/pi-extensions/SKILL.md +191 -0
  151. package/skills/pi-tui/SKILL.md +517 -0
  152. package/skills/python/SKILL.md +189 -0
  153. package/skills/rust/SKILL.md +268 -0
  154. package/skills/security/SKILL.md +206 -0
  155. package/skills/style/SKILL.md +264 -0
  156. package/skills/typescript/SKILL.md +225 -0
  157. package/skills/vault/SKILL.md +102 -0
  158. package/themes/alpharius-legacy.json +85 -0
  159. package/themes/alpharius.conf +59 -0
  160. package/themes/alpharius.json +88 -0
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Profile definitions and project detection logic.
3
+ *
4
+ * A profile is a named group of tools that should be active together.
5
+ * Detection functions scan the cwd for project signals to determine
6
+ * which profiles apply.
7
+ */
8
+
9
+ import { execSync } from "node:child_process";
10
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+
14
+ // ── Profile Definitions ─────────────────────────────────────────
15
+
16
+ export interface Profile {
17
+ /** Profile identifier */
18
+ id: string;
19
+ /** Human-readable label */
20
+ label: string;
21
+ /** Description shown in /profile list */
22
+ description: string;
23
+ /** Tool name patterns to include. Exact match or glob-like prefix (e.g. "mcp_scribe_*") */
24
+ tools: string[];
25
+ /** Detection function: returns true if this profile should be auto-activated */
26
+ detect: (cwd: string) => boolean;
27
+ /** If true, always included regardless of detection */
28
+ alwaysOn?: boolean;
29
+ }
30
+
31
+ /** Check if a command exists on PATH */
32
+ function hasCmd(name: string): boolean {
33
+ try {
34
+ execSync(`command -v ${name}`, { stdio: "ignore" });
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ function fileExists(cwd: string, ...paths: string[]): boolean {
42
+ return paths.some((p) => existsSync(join(cwd, p)));
43
+ }
44
+
45
+ /** Check if any file with the given extension exists in common content directories (shallow). */
46
+ function dirHasExt(cwd: string, ext: string): boolean {
47
+ // Only check top-level and common content dirs — never walk node_modules/.git/etc.
48
+ const SCAN_DIRS = [".", "docs", "assets", "images", "diagrams", "design", "src", "lib"];
49
+ for (const dir of SCAN_DIRS) {
50
+ try {
51
+ const fullDir = join(cwd, dir);
52
+ const entries = readdirSync(fullDir);
53
+ if (entries.some((e) => e.endsWith(ext))) return true;
54
+ } catch {
55
+ // Directory doesn't exist, skip
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+
61
+ function readJsonField(cwd: string, file: string, field: string): unknown {
62
+ try {
63
+ const raw = readFileSync(join(cwd, file), "utf8");
64
+ const json = JSON.parse(raw);
65
+ return json[field];
66
+ } catch {
67
+ return undefined;
68
+ }
69
+ }
70
+
71
+ export const PROFILES: Profile[] = [
72
+ {
73
+ id: "core",
74
+ label: "Core",
75
+ description: "Essential tools: built-in file/shell ops, memory, chronos, auth, model control",
76
+ alwaysOn: true,
77
+ tools: [
78
+ // pi built-in tools (lowercase — these are pi's native tools, not Claude Code's
79
+ // PascalCase variants). Must be included or setActiveTools() deactivates them.
80
+ "read", "write", "edit", "bash",
81
+ // Memory
82
+ "memory_query", "memory_recall", "memory_episodes", "memory_focus",
83
+ "memory_release", "memory_store", "memory_supersede", "memory_search_archive",
84
+ "memory_connect", "memory_archive", "memory_compact",
85
+ // Utilities
86
+ "chronos", "whoami",
87
+ // Model control
88
+ "set_model_tier", "set_thinking_level", "switch_to_offline_driver",
89
+ // Tool management
90
+ "manage_tools",
91
+ ],
92
+ detect: () => true,
93
+ },
94
+ {
95
+ id: "coding",
96
+ label: "Coding",
97
+ description: "Cleave decomposition, OpenSpec, design tree",
98
+ tools: [
99
+ "cleave_assess", "cleave_run",
100
+ "openspec_manage",
101
+ "design_tree", "design_tree_update",
102
+ ],
103
+ detect: (cwd) => fileExists(cwd, ".git"),
104
+ },
105
+ {
106
+ id: "visual",
107
+ label: "Visual",
108
+ description: "Image generation, D2 diagrams, Excalidraw rendering",
109
+ tools: [
110
+ "generate_image_local", "render_diagram", "render_excalidraw",
111
+ ],
112
+ detect: (cwd) =>
113
+ fileExists(cwd, "images") ||
114
+ dirHasExt(cwd, ".excalidraw") ||
115
+ dirHasExt(cwd, ".d2"),
116
+ },
117
+ {
118
+ id: "local-ai",
119
+ label: "Local AI",
120
+ description: "Ollama local inference (ask_local_model, manage_ollama)",
121
+ tools: [
122
+ "ask_local_model", "list_local_models", "manage_ollama",
123
+ ],
124
+ detect: () => hasCmd("ollama"),
125
+ },
126
+ {
127
+ id: "web",
128
+ label: "Web & View",
129
+ description: "Web search and file viewing",
130
+ alwaysOn: true,
131
+ tools: ["web_search", "view"],
132
+ detect: () => true,
133
+ },
134
+ {
135
+ id: "scribe",
136
+ label: "Scribe",
137
+ description: "Partnership tracking via MCP bridge (mcp_scribe_*)",
138
+ tools: ["mcp_scribe_*"],
139
+ detect: (cwd) => {
140
+ // Detect if scribe MCP is configured — check for .pi/mcp.json or similar
141
+ return fileExists(cwd, ".pi/mcp.json") ||
142
+ existsSync(join(homedir(), ".pi", "mcp.json"));
143
+ },
144
+ },
145
+ {
146
+ id: "pi-dev",
147
+ label: "Pi Development",
148
+ description: "All tools enabled — for working on Omegon itself",
149
+ tools: ["*"],
150
+ detect: (cwd) => {
151
+ const piExts = readJsonField(cwd, "package.json", "pi") as { extensions?: string[] } | undefined;
152
+ return !!piExts?.extensions;
153
+ },
154
+ },
155
+ ];
156
+
157
+ // ── Profile Config (persisted) ──────────────────────────────────
158
+
159
+ export interface ProfileConfig {
160
+ /** Profiles to force-include regardless of detection */
161
+ include?: string[];
162
+ /** Profiles to force-exclude regardless of detection */
163
+ exclude?: string[];
164
+ /** Individual tool overrides */
165
+ tools?: {
166
+ enable?: string[];
167
+ disable?: string[];
168
+ };
169
+ }
170
+
171
+ export function loadProfileConfig(cwd: string): ProfileConfig {
172
+ const configPath = join(cwd, ".pi", "profile.json");
173
+ try {
174
+ return JSON.parse(readFileSync(configPath, "utf8")) as ProfileConfig;
175
+ } catch {
176
+ return {};
177
+ }
178
+ }
179
+
180
+ // ── Detection & Merge ───────────────────────────────────────────
181
+
182
+ /** Detect which profiles should be active for the given cwd */
183
+ export function detectProfiles(cwd: string): string[] {
184
+ return PROFILES
185
+ .filter((p) => p.alwaysOn || p.detect(cwd))
186
+ .map((p) => p.id);
187
+ }
188
+
189
+ /** Match a tool name against a pattern (exact or wildcard suffix) */
190
+ export function matchTool(toolName: string, pattern: string): boolean {
191
+ if (pattern === "*") return true;
192
+ if (pattern.endsWith("*")) {
193
+ return toolName.startsWith(pattern.slice(0, -1));
194
+ }
195
+ return toolName === pattern;
196
+ }
197
+
198
+ /** Given detected profiles + config overrides, compute the active tool set */
199
+ export function resolveActiveTools(
200
+ allToolNames: string[],
201
+ detectedProfileIds: string[],
202
+ config: ProfileConfig,
203
+ ): string[] {
204
+ // Apply include/exclude overrides to detected profiles
205
+ let activeProfileIds = new Set(detectedProfileIds);
206
+
207
+ for (const id of config.include ?? []) {
208
+ activeProfileIds.add(id);
209
+ }
210
+ for (const id of config.exclude ?? []) {
211
+ activeProfileIds.delete(id);
212
+ }
213
+
214
+ // Collect tool patterns from active profiles
215
+ const activeProfiles = PROFILES.filter((p) => activeProfileIds.has(p.id));
216
+ const patterns = activeProfiles.flatMap((p) => p.tools);
217
+
218
+ // Check for wildcard — if any profile has "*", enable all
219
+ if (patterns.includes("*")) {
220
+ let tools = new Set(allToolNames);
221
+ for (const name of config.tools?.disable ?? []) {
222
+ tools.delete(name);
223
+ }
224
+ return [...tools];
225
+ }
226
+
227
+ // Match tools against patterns
228
+ let enabledTools = new Set<string>();
229
+ for (const toolName of allToolNames) {
230
+ if (patterns.some((p) => matchTool(toolName, p))) {
231
+ enabledTools.add(toolName);
232
+ }
233
+ }
234
+
235
+ // Apply individual tool overrides
236
+ for (const name of config.tools?.enable ?? []) {
237
+ if (allToolNames.includes(name)) {
238
+ enabledTools.add(name);
239
+ }
240
+ }
241
+ for (const name of config.tools?.disable ?? []) {
242
+ enabledTools.delete(name);
243
+ }
244
+
245
+ return [...enabledTools];
246
+ }
247
+
248
+ /** Format a summary of active/inactive profiles for display */
249
+ export function formatProfileSummary(
250
+ detectedIds: string[],
251
+ config: ProfileConfig,
252
+ allToolNames: string[],
253
+ ): string {
254
+ const activeTools = resolveActiveTools(allToolNames, detectedIds, config);
255
+ const lines: string[] = [];
256
+
257
+ lines.push("## Tool Profiles\n");
258
+
259
+ for (const profile of PROFILES) {
260
+ const detected = detectedIds.includes(profile.id);
261
+ const included = config.include?.includes(profile.id);
262
+ const excluded = config.exclude?.includes(profile.id);
263
+
264
+ let status: string;
265
+ if (excluded) {
266
+ status = "⊘ excluded";
267
+ } else if (detected || included) {
268
+ status = included && !detected ? "✓ forced" : "✓ active";
269
+ } else {
270
+ status = "○ inactive";
271
+ }
272
+
273
+ const toolCount = profile.tools.includes("*")
274
+ ? "all"
275
+ : `${profile.tools.length} tools`;
276
+
277
+ lines.push(`- **${profile.label}** (${profile.id}): ${status} — ${toolCount}`);
278
+ }
279
+
280
+ lines.push("");
281
+ lines.push(`**Active:** ${activeTools.length}/${allToolNames.length} tools`);
282
+
283
+ if (config.tools?.enable?.length || config.tools?.disable?.length) {
284
+ lines.push("\n**Overrides:**");
285
+ for (const t of config.tools?.enable ?? []) lines.push(` + ${t}`);
286
+ for (const t of config.tools?.disable ?? []) lines.push(` - ${t}`);
287
+ }
288
+
289
+ return lines.join("\n");
290
+ }
@@ -0,0 +1,9 @@
1
+ import "@cwilson613/pi-coding-agent";
2
+ import type { SlashCommandBridgeMetadata, SlashCommandBridgeResult, SlashCommandExecutionContext } from "./lib/slash-command-bridge.js";
3
+
4
+ declare module "@cwilson613/pi-coding-agent" {
5
+ interface RegisteredCommand {
6
+ bridge?: SlashCommandBridgeMetadata;
7
+ structuredExecutor?: (args: string, ctx: SlashCommandExecutionContext) => Promise<SlashCommandBridgeResult>;
8
+ }
9
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * vault — Markdown viewport extension
3
+ *
4
+ * Spawns mdserve to render interlinked project markdown as a navigable
5
+ * web UI with wikilink resolution, graph view, and live reload.
6
+ *
7
+ * Auto-starts mdserve on session_start if the binary is on $PATH.
8
+ * Stores port in sharedState for URI resolver consumption.
9
+ *
10
+ * Commands:
11
+ * /vault — Show status (running/stopped, port, PID)
12
+ * /vault [path] — Start mdserve on a specific directory
13
+ * /vault stop — Stop the running mdserve instance
14
+ * /vault graph — Open the graph view in the browser
15
+ */
16
+
17
+ import { execSync, spawn, type ChildProcess } from "node:child_process";
18
+ import type { ExtensionAPI } from "@cwilson613/pi-coding-agent";
19
+
20
+ const DEFAULT_PORT = 3333;
21
+ const BINARY_NAME = "mdserve";
22
+
23
+ let mdserveProcess: ChildProcess | null = null;
24
+ let mdservePort: number | null = null;
25
+ let mdserveDir: string | null = null;
26
+
27
+ /** Get the current mdserve port, or null if not running. Used by uri-resolver. */
28
+ export function getMdservePort(): number | null {
29
+ return mdservePort;
30
+ }
31
+
32
+ function hasBinary(): boolean {
33
+ try {
34
+ execSync(`which ${BINARY_NAME}`, { stdio: "ignore" });
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ function openBrowser(url: string): void {
42
+ try {
43
+ const cmd = process.platform === "darwin" ? "open" : "xdg-open";
44
+ spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
45
+ } catch { /* user can open manually */ }
46
+ }
47
+
48
+ // No shared state update needed — getMdservePort() is the public API
49
+
50
+ function stopMdserve(): string {
51
+ if (mdserveProcess) {
52
+ const pid = mdserveProcess.pid;
53
+ mdserveProcess.kill("SIGTERM");
54
+ mdserveProcess = null;
55
+ const msg = `Stopped mdserve (PID ${pid}, was serving ${mdserveDir} on port ${mdservePort})`;
56
+ mdservePort = null;
57
+ mdserveDir = null;
58
+
59
+ return msg;
60
+ }
61
+ return "mdserve is not running.";
62
+ }
63
+
64
+ function spawnMdserve(dir: string, port: number, options?: { silent?: boolean }): string {
65
+ if (mdserveProcess) {
66
+ if (mdserveDir === dir) {
67
+ return `mdserve already running at http://127.0.0.1:${mdservePort} (PID ${mdserveProcess.pid})\n` +
68
+ `Serving: ${mdserveDir}\n` +
69
+ `Use \`/vault stop\` to stop, or \`/vault graph\` to open graph view.`;
70
+ }
71
+ stopMdserve();
72
+ }
73
+
74
+ const child = spawn(BINARY_NAME, [dir, "--port", String(port)], {
75
+ stdio: ["ignore", "pipe", "pipe"],
76
+ detached: false,
77
+ });
78
+
79
+ mdserveProcess = child;
80
+ mdservePort = port;
81
+ mdserveDir = dir;
82
+
83
+
84
+ child.stdout?.on("data", (data: Buffer) => {
85
+ const match = data.toString().match(/using (\d+) instead/);
86
+ if (match) {
87
+ mdservePort = parseInt(match[1], 10);
88
+
89
+ }
90
+ });
91
+
92
+ child.on("exit", () => {
93
+ if (mdserveProcess === child) {
94
+ mdserveProcess = null;
95
+ mdservePort = null;
96
+ mdserveDir = null;
97
+
98
+ }
99
+ });
100
+
101
+ if (!options?.silent) {
102
+ openBrowser(`http://127.0.0.1:${port}`);
103
+ }
104
+
105
+ const prefix = options?.silent ? "Auto-started" : "Started";
106
+ return `${prefix} mdserve at http://127.0.0.1:${port} (PID ${child.pid})\n` +
107
+ `Serving: ${dir}\n` +
108
+ `Graph view: http://127.0.0.1:${port}/graph\n` +
109
+ `Use \`/vault stop\` to stop.`;
110
+ }
111
+
112
+ const NOT_INSTALLED = "`mdserve` is not installed. Run `/bootstrap` to set up Omegon dependencies.";
113
+
114
+ export default function (pi: ExtensionAPI) {
115
+
116
+
117
+ // Auto-start mdserve on session start if binary is available
118
+ pi.on("session_start", () => {
119
+ if (hasBinary()) {
120
+ spawnMdserve(process.cwd(), DEFAULT_PORT, { silent: true });
121
+ }
122
+ });
123
+
124
+ pi.on("session_shutdown", () => {
125
+ if (mdserveProcess) {
126
+ mdserveProcess.kill("SIGTERM");
127
+ mdserveProcess = null;
128
+ mdservePort = null;
129
+ mdserveDir = null;
130
+
131
+ }
132
+ });
133
+
134
+ pi.registerCommand("vault", {
135
+ description: "Markdown viewport — serve project docs with wikilinks and graph view",
136
+ handler: async (args, ctx) => {
137
+ const subcommand = args.trim().split(/\s+/)[0]?.toLowerCase() || "";
138
+
139
+ switch (subcommand) {
140
+ case "stop":
141
+ ctx.ui.notify(stopMdserve(), "info");
142
+ return;
143
+
144
+ case "status":
145
+ case "": {
146
+ // Default: show status
147
+ if (mdserveProcess) {
148
+ ctx.ui.notify(
149
+ `mdserve is running (PID ${mdserveProcess.pid})\n` +
150
+ `URL: http://127.0.0.1:${mdservePort}\n` +
151
+ `Serving: ${mdserveDir}`,
152
+ "info",
153
+ );
154
+ } else if (!hasBinary()) {
155
+ ctx.ui.notify(NOT_INSTALLED, "warning");
156
+ } else {
157
+ ctx.ui.notify("mdserve is not running. Use `/vault [path]` to start.", "info");
158
+ }
159
+ return;
160
+ }
161
+
162
+ case "graph":
163
+ if (!hasBinary()) { ctx.ui.notify(NOT_INSTALLED, "warning"); return; }
164
+ if (mdserveProcess && mdservePort) {
165
+ openBrowser(`http://127.0.0.1:${mdservePort}/graph`);
166
+ ctx.ui.notify(`Opened graph view at http://127.0.0.1:${mdservePort}/graph`, "info");
167
+ } else {
168
+ const dir = process.cwd();
169
+ ctx.ui.notify(spawnMdserve(dir, DEFAULT_PORT), "info");
170
+ setTimeout(() => {
171
+ if (mdservePort) openBrowser(`http://127.0.0.1:${mdservePort}/graph`);
172
+ }, 1000);
173
+ }
174
+ return;
175
+
176
+ default: {
177
+ if (!hasBinary()) { ctx.ui.notify(NOT_INSTALLED, "warning"); return; }
178
+ const dir = subcommand;
179
+ ctx.ui.notify(spawnMdserve(dir, DEFAULT_PORT), "info");
180
+ return;
181
+ }
182
+ }
183
+ },
184
+ });
185
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * version-check — Polls GitHub for new Omegon releases and notifies the operator.
3
+ *
4
+ * Checks on session start, then hourly. Compares the installed version
5
+ * (from package.json) against the latest GitHub release tag. If a newer
6
+ * version is found, sends a notification suggesting `pi update`.
7
+ *
8
+ * Respects PI_SKIP_VERSION_CHECK and PI_OFFLINE environment variables.
9
+ */
10
+
11
+ import type { ExtensionAPI } from "@cwilson613/pi-coding-agent";
12
+ import { readFileSync } from "node:fs";
13
+ import { join, dirname } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const REPO_OWNER = "cwilson613";
17
+ const REPO_NAME = "omegon";
18
+ const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
19
+ const FETCH_TIMEOUT_MS = 10_000;
20
+
21
+ /** Read installed version from package.json */
22
+ function getInstalledVersion(): string {
23
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
24
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
25
+ return pkg.version;
26
+ }
27
+
28
+ /** Fetch the latest release tag from GitHub. Returns version string or null. */
29
+ async function fetchLatestRelease(): Promise<string | null> {
30
+ if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE) return null;
31
+
32
+ try {
33
+ const response = await fetch(
34
+ `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`,
35
+ {
36
+ headers: { Accept: "application/vnd.github+json" },
37
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
38
+ },
39
+ );
40
+ if (!response.ok) return null;
41
+ const data = (await response.json()) as { tag_name?: string };
42
+ return data.tag_name?.replace(/^v/, "") ?? null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /** Simple semver comparison. Returns true if latest > current. */
49
+ function isNewer(latest: string, current: string): boolean {
50
+ const parse = (v: string) => v.split(".").map((n) => parseInt(n, 10) || 0);
51
+ const l = parse(latest);
52
+ const c = parse(current);
53
+ for (let i = 0; i < 3; i++) {
54
+ if ((l[i] ?? 0) > (c[i] ?? 0)) return true;
55
+ if ((l[i] ?? 0) < (c[i] ?? 0)) return false;
56
+ }
57
+ return false;
58
+ }
59
+
60
+ export default function versionCheck(pi: ExtensionAPI) {
61
+ let timer: ReturnType<typeof setInterval> | null = null;
62
+ let notifiedVersion: string | null = null;
63
+
64
+ async function check() {
65
+ const installed = getInstalledVersion();
66
+ const latest = await fetchLatestRelease();
67
+ if (!latest || !isNewer(latest, installed)) return;
68
+ if (latest === notifiedVersion) return; // don't spam
69
+
70
+ notifiedVersion = latest;
71
+ pi.sendMessage({
72
+ customType: "view",
73
+ content: `**Omegon update available:** v${installed} → v${latest}\n\nRun \`pi update\` to upgrade.`,
74
+ display: true,
75
+ });
76
+ }
77
+
78
+ pi.on("session_start", async () => {
79
+ // Fire-and-forget — don't block session start
80
+ check();
81
+ timer = setInterval(check, CHECK_INTERVAL_MS);
82
+ });
83
+
84
+ pi.on("session_shutdown", async () => {
85
+ if (timer) {
86
+ clearInterval(timer);
87
+ timer = null;
88
+ }
89
+ });
90
+ }