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,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URI resolver for context-aware OSC 8 hyperlinks.
|
|
3
|
+
*
|
|
4
|
+
* Routes file paths to the best URI scheme based on:
|
|
5
|
+
* - File extension (.md → mdserve, code → editor, .excalidraw → Obsidian)
|
|
6
|
+
* - Running services (mdserve port in shared state)
|
|
7
|
+
* - User config (.pi/config.json editor preference)
|
|
8
|
+
* - Obsidian vault detection (walk parents for .obsidian/)
|
|
9
|
+
*
|
|
10
|
+
* Fallback is always file://
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
import { basename, dirname, extname, join, relative } from "node:path";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Types
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export interface PiConfig {
|
|
21
|
+
editor?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ResolveUriOptions {
|
|
25
|
+
mdservePort?: number;
|
|
26
|
+
config?: PiConfig;
|
|
27
|
+
/** Override project root for mdserve relative paths. Defaults to cwd. */
|
|
28
|
+
projectRoot?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Constants
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
const CODE_EXTS = new Set([
|
|
36
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
37
|
+
".py", ".rs", ".go", ".c", ".cpp", ".cc", ".h", ".hpp",
|
|
38
|
+
".java", ".kt", ".rb", ".lua", ".sh", ".bash", ".zsh",
|
|
39
|
+
".css", ".scss", ".less", ".sql", ".swift", ".zig",
|
|
40
|
+
".vue", ".svelte", ".elm", ".hs", ".ml", ".ex", ".exs",
|
|
41
|
+
".php", ".pl", ".r", ".scala", ".dart", ".nim",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const MARKDOWN_EXTS = new Set([".md", ".markdown", ".mdx"]);
|
|
45
|
+
|
|
46
|
+
const KNOWN_EDITORS = new Set(["vscode", "cursor", "zed"]);
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Config loading
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load .pi/config.json from the given directory (or cwd).
|
|
54
|
+
* Returns empty config on missing/invalid file — never throws.
|
|
55
|
+
*/
|
|
56
|
+
export function loadConfig(root?: string): PiConfig {
|
|
57
|
+
const configPath = join(root ?? process.cwd(), ".pi", "config.json");
|
|
58
|
+
try {
|
|
59
|
+
if (!existsSync(configPath)) return {};
|
|
60
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
61
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
62
|
+
return { editor: typeof raw.editor === "string" ? raw.editor : undefined };
|
|
63
|
+
}
|
|
64
|
+
return {};
|
|
65
|
+
} catch {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Obsidian vault detection
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Walk parent directories looking for .obsidian/ folder.
|
|
76
|
+
* Returns { vaultName, vaultRoot } or undefined.
|
|
77
|
+
*/
|
|
78
|
+
export function detectObsidianVault(absPath: string): { vaultName: string; vaultRoot: string } | undefined {
|
|
79
|
+
let dir = dirname(absPath);
|
|
80
|
+
const seen = new Set<string>();
|
|
81
|
+
while (dir && !seen.has(dir)) {
|
|
82
|
+
seen.add(dir);
|
|
83
|
+
if (existsSync(join(dir, ".obsidian"))) {
|
|
84
|
+
return { vaultName: basename(dir), vaultRoot: dir };
|
|
85
|
+
}
|
|
86
|
+
const parent = dirname(dir);
|
|
87
|
+
if (parent === dir) break;
|
|
88
|
+
dir = parent;
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// URI resolution
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the best URI for a given absolute file path.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveUri(absPath: string, options?: ResolveUriOptions): string {
|
|
101
|
+
const ext = extname(absPath).toLowerCase();
|
|
102
|
+
const config = options?.config ?? loadConfig();
|
|
103
|
+
const mdservePort = options?.mdservePort;
|
|
104
|
+
const projectRoot = options?.projectRoot ?? process.cwd();
|
|
105
|
+
|
|
106
|
+
// Markdown → mdserve if running, else file://
|
|
107
|
+
if (MARKDOWN_EXTS.has(ext)) {
|
|
108
|
+
if (mdservePort) {
|
|
109
|
+
const rel = relative(projectRoot, absPath);
|
|
110
|
+
return `http://localhost:${mdservePort}/${rel}`;
|
|
111
|
+
}
|
|
112
|
+
return `file://${absPath}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Excalidraw → Obsidian if vault detected, else file://
|
|
116
|
+
if (ext === ".excalidraw") {
|
|
117
|
+
const vault = detectObsidianVault(absPath);
|
|
118
|
+
if (vault) {
|
|
119
|
+
const relPath = relative(vault.vaultRoot, absPath);
|
|
120
|
+
return `obsidian://open?vault=${encodeURIComponent(vault.vaultName)}&file=${encodeURIComponent(relPath)}`;
|
|
121
|
+
}
|
|
122
|
+
return `file://${absPath}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Code files → editor scheme if configured, else file://
|
|
126
|
+
if (CODE_EXTS.has(ext)) {
|
|
127
|
+
const editor = config.editor;
|
|
128
|
+
if (editor && KNOWN_EDITORS.has(editor)) {
|
|
129
|
+
return `${editor}://file/${absPath}`;
|
|
130
|
+
}
|
|
131
|
+
return `file://${absPath}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Everything else (images, PDFs, etc.) → file://
|
|
135
|
+
return `file://${absPath}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// OSC 8 helpers
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Wrap text in an OSC 8 hyperlink escape sequence.
|
|
144
|
+
* Terminals that don't support OSC 8 simply ignore the sequences.
|
|
145
|
+
*/
|
|
146
|
+
export function osc8Link(uri: string, text: string): string {
|
|
147
|
+
return `\x1b]8;;${uri}\x1b\\${text}\x1b]8;;\x1b\\`;
|
|
148
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// @secret BRAVE_API_KEY "Brave Search API key"
|
|
2
|
+
// @secret TAVILY_API_KEY "Tavily Search API key"
|
|
3
|
+
// @secret SERPER_API_KEY "Serper/Google Search API key"
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI } from "@cwilson613/pi-coding-agent";
|
|
6
|
+
import { Type } from "@sinclair/typebox";
|
|
7
|
+
import { StringEnum } from "../lib/typebox-helpers";
|
|
8
|
+
import { getAvailableProviders, getProvider, type SearchResult } from "./providers.ts";
|
|
9
|
+
|
|
10
|
+
function deduplicateResults(results: SearchResult[]): SearchResult[] {
|
|
11
|
+
const seen = new Map<string, SearchResult>();
|
|
12
|
+
for (const r of results) {
|
|
13
|
+
const key = r.url.replace(/\/$/, "").toLowerCase();
|
|
14
|
+
if (seen.has(key)) {
|
|
15
|
+
// Merge provider attribution
|
|
16
|
+
const existing = seen.get(key)!;
|
|
17
|
+
if (!existing.provider.includes(r.provider)) {
|
|
18
|
+
existing.provider += `, ${r.provider}`;
|
|
19
|
+
}
|
|
20
|
+
// Prefer longer snippet
|
|
21
|
+
if (r.snippet.length > existing.snippet.length) {
|
|
22
|
+
existing.snippet = r.snippet;
|
|
23
|
+
}
|
|
24
|
+
if (r.content && (!existing.content || r.content.length > existing.content.length)) {
|
|
25
|
+
existing.content = r.content;
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
seen.set(key, { ...r });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return Array.from(seen.values());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatResults(results: SearchResult[], mode: string): string {
|
|
35
|
+
if (results.length === 0) return "No results found.";
|
|
36
|
+
|
|
37
|
+
const lines: string[] = [];
|
|
38
|
+
for (const r of results) {
|
|
39
|
+
lines.push(`### ${r.title}`);
|
|
40
|
+
lines.push(`**URL:** ${r.url}`);
|
|
41
|
+
lines.push(`**Source:** ${r.provider}`);
|
|
42
|
+
lines.push(`${r.snippet}`);
|
|
43
|
+
if (r.content) {
|
|
44
|
+
lines.push(`\n<extracted_content>\n${r.content.slice(0, 2000)}\n</extracted_content>`);
|
|
45
|
+
}
|
|
46
|
+
lines.push("");
|
|
47
|
+
}
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default function (pi: ExtensionAPI) {
|
|
52
|
+
// Secrets are resolved into process.env by the 00-secrets extension
|
|
53
|
+
// before this extension loads. No .env files needed.
|
|
54
|
+
|
|
55
|
+
pi.registerTool({
|
|
56
|
+
name: "web_search",
|
|
57
|
+
label: "Web Search",
|
|
58
|
+
description: `Search the web using multiple providers. Available modes:
|
|
59
|
+
- "quick": Use a single provider (fastest)
|
|
60
|
+
- "deep": Use a single provider, more results
|
|
61
|
+
- "compare": Fan out to ALL configured providers in parallel, deduplicate results. Best for research and verification.
|
|
62
|
+
|
|
63
|
+
Available providers: brave (independent index), tavily (AI-optimized, extracts content), serper (Google results).
|
|
64
|
+
Only providers with configured API keys are available.`,
|
|
65
|
+
promptSnippet: "Search the web via multiple providers (brave, tavily, serper) with quick/deep/compare modes",
|
|
66
|
+
promptGuidelines: [
|
|
67
|
+
"Use 'compare' mode for research requiring cross-source verification",
|
|
68
|
+
],
|
|
69
|
+
parameters: Type.Object({
|
|
70
|
+
query: Type.String({ description: "Search query" }),
|
|
71
|
+
provider: Type.Optional(
|
|
72
|
+
StringEnum(["brave", "tavily", "serper"], {
|
|
73
|
+
description: "Specific provider. Omit to auto-select (quick) or fan out (compare).",
|
|
74
|
+
})
|
|
75
|
+
),
|
|
76
|
+
mode: Type.Optional(
|
|
77
|
+
StringEnum(["quick", "deep", "compare"], {
|
|
78
|
+
description: "Search mode. Default: quick",
|
|
79
|
+
})
|
|
80
|
+
),
|
|
81
|
+
max_results: Type.Optional(
|
|
82
|
+
Type.Number({ description: "Max results per provider. Default: 5", minimum: 1, maximum: 20 })
|
|
83
|
+
),
|
|
84
|
+
topic: Type.Optional(
|
|
85
|
+
StringEnum(["general", "news"], {
|
|
86
|
+
description: "Search topic. Default: general",
|
|
87
|
+
})
|
|
88
|
+
),
|
|
89
|
+
}),
|
|
90
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
91
|
+
const mode = params.mode || "quick";
|
|
92
|
+
const maxResults = params.max_results || (mode === "deep" ? 10 : 5);
|
|
93
|
+
const topic = params.topic || "general";
|
|
94
|
+
const available = getAvailableProviders();
|
|
95
|
+
|
|
96
|
+
if (available.length === 0) {
|
|
97
|
+
return {
|
|
98
|
+
content: [{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: "No search providers configured. Run `/secrets configure BRAVE_API_KEY` (or TAVILY_API_KEY, SERPER_API_KEY) to set up at least one provider.",
|
|
101
|
+
}],
|
|
102
|
+
details: { error: true },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let results: SearchResult[] = [];
|
|
107
|
+
let providersUsed: string[] = [];
|
|
108
|
+
|
|
109
|
+
if (mode === "compare") {
|
|
110
|
+
// Fan out to all available providers in parallel
|
|
111
|
+
const settled = await Promise.allSettled(
|
|
112
|
+
available.map((p) => p.search(params.query, maxResults, topic))
|
|
113
|
+
);
|
|
114
|
+
for (let i = 0; i < settled.length; i++) {
|
|
115
|
+
const outcome = settled[i];
|
|
116
|
+
if (outcome.status === "fulfilled") {
|
|
117
|
+
results.push(...outcome.value);
|
|
118
|
+
providersUsed.push(available[i].name);
|
|
119
|
+
} else {
|
|
120
|
+
providersUsed.push(`${available[i].name} (error: ${outcome.reason?.message || "unknown"})`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
results = deduplicateResults(results);
|
|
124
|
+
} else {
|
|
125
|
+
// Single provider
|
|
126
|
+
let provider;
|
|
127
|
+
if (params.provider) {
|
|
128
|
+
provider = getProvider(params.provider);
|
|
129
|
+
if (!provider) {
|
|
130
|
+
return {
|
|
131
|
+
content: [{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: `Provider "${params.provider}" not available. Configured: ${available.map((p) => p.name).join(", ")}`,
|
|
134
|
+
}],
|
|
135
|
+
details: { error: true },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
// Auto-select: prefer tavily (content extraction), then serper (google), then brave
|
|
140
|
+
provider =
|
|
141
|
+
available.find((p) => p.name === "tavily") ||
|
|
142
|
+
available.find((p) => p.name === "serper") ||
|
|
143
|
+
available[0];
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
results = await provider.search(params.query, maxResults, topic);
|
|
147
|
+
providersUsed.push(provider.name);
|
|
148
|
+
} catch (err: any) {
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: `Search error (${provider.name}): ${err.message}` }],
|
|
151
|
+
details: { error: true },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const header = `**Query:** ${params.query}\n**Mode:** ${mode} | **Providers:** ${providersUsed.join(", ")} | **Results:** ${results.length}\n\n---\n\n`;
|
|
157
|
+
const body = formatResults(results, mode);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
content: [{ type: "text", text: header + body }],
|
|
161
|
+
details: {
|
|
162
|
+
resultCount: results.length,
|
|
163
|
+
providers: providersUsed,
|
|
164
|
+
mode,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Notify on load with provider status
|
|
171
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
172
|
+
const available = getAvailableProviders();
|
|
173
|
+
if (available.length > 0) {
|
|
174
|
+
ctx.ui.notify(
|
|
175
|
+
`Web Search: ${available.map((p) => p.name).join(", ")} ready`,
|
|
176
|
+
"info"
|
|
177
|
+
);
|
|
178
|
+
} else {
|
|
179
|
+
ctx.ui.notify("Web Search: No API keys configured", "warning");
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Web search provider implementations
|
|
2
|
+
|
|
3
|
+
export interface SearchResult {
|
|
4
|
+
title: string;
|
|
5
|
+
url: string;
|
|
6
|
+
snippet: string;
|
|
7
|
+
content?: string; // Tavily returns extracted content
|
|
8
|
+
provider: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SearchProvider {
|
|
12
|
+
name: string;
|
|
13
|
+
available: boolean;
|
|
14
|
+
search(query: string, maxResults: number, topic: string): Promise<SearchResult[]>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// --- Brave Search ---
|
|
18
|
+
|
|
19
|
+
function braveProvider(): SearchProvider {
|
|
20
|
+
const apiKey = process.env.BRAVE_API_KEY;
|
|
21
|
+
return {
|
|
22
|
+
name: "brave",
|
|
23
|
+
available: !!apiKey,
|
|
24
|
+
async search(query, maxResults, topic) {
|
|
25
|
+
const params = new URLSearchParams({
|
|
26
|
+
q: query,
|
|
27
|
+
count: String(maxResults),
|
|
28
|
+
...(topic === "news" ? { freshness: "pd" } : {}),
|
|
29
|
+
});
|
|
30
|
+
const res = await fetch(
|
|
31
|
+
`https://api.search.brave.com/res/v1/web/search?${params}`,
|
|
32
|
+
{ headers: { "X-Subscription-Token": apiKey!, Accept: "application/json" } }
|
|
33
|
+
);
|
|
34
|
+
if (!res.ok) throw new Error(`Brave ${res.status}: ${await res.text()}`);
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
return (data.web?.results || []).slice(0, maxResults).map((r: any) => ({
|
|
37
|
+
title: r.title,
|
|
38
|
+
url: r.url,
|
|
39
|
+
snippet: r.description || "",
|
|
40
|
+
provider: "brave",
|
|
41
|
+
}));
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- Tavily ---
|
|
47
|
+
|
|
48
|
+
function tavilyProvider(): SearchProvider {
|
|
49
|
+
const apiKey = process.env.TAVILY_API_KEY;
|
|
50
|
+
return {
|
|
51
|
+
name: "tavily",
|
|
52
|
+
available: !!apiKey,
|
|
53
|
+
async search(query, maxResults, topic) {
|
|
54
|
+
const res = await fetch("https://api.tavily.com/search", {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "Content-Type": "application/json" },
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
api_key: apiKey,
|
|
59
|
+
query,
|
|
60
|
+
max_results: maxResults,
|
|
61
|
+
include_answer: false,
|
|
62
|
+
include_raw_content: false,
|
|
63
|
+
topic: topic === "news" ? "news" : "general",
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) throw new Error(`Tavily ${res.status}: ${await res.text()}`);
|
|
67
|
+
const data = await res.json();
|
|
68
|
+
return (data.results || []).slice(0, maxResults).map((r: any) => ({
|
|
69
|
+
title: r.title,
|
|
70
|
+
url: r.url,
|
|
71
|
+
snippet: r.content || "",
|
|
72
|
+
content: r.raw_content || undefined,
|
|
73
|
+
provider: "tavily",
|
|
74
|
+
}));
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- Serper (Google) ---
|
|
80
|
+
|
|
81
|
+
function serperProvider(): SearchProvider {
|
|
82
|
+
const apiKey = process.env.SERPER_API_KEY;
|
|
83
|
+
return {
|
|
84
|
+
name: "serper",
|
|
85
|
+
available: !!apiKey,
|
|
86
|
+
async search(query, maxResults, topic) {
|
|
87
|
+
const endpoint =
|
|
88
|
+
topic === "news"
|
|
89
|
+
? "https://google.serper.dev/news"
|
|
90
|
+
: "https://google.serper.dev/search";
|
|
91
|
+
const res = await fetch(endpoint, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "X-API-KEY": apiKey!, "Content-Type": "application/json" },
|
|
94
|
+
body: JSON.stringify({ q: query, num: maxResults }),
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok) throw new Error(`Serper ${res.status}: ${await res.text()}`);
|
|
97
|
+
const data = await res.json();
|
|
98
|
+
const results = topic === "news" ? data.news || [] : data.organic || [];
|
|
99
|
+
return results.slice(0, maxResults).map((r: any) => ({
|
|
100
|
+
title: r.title,
|
|
101
|
+
url: r.link,
|
|
102
|
+
snippet: r.snippet || r.description || "",
|
|
103
|
+
provider: "serper",
|
|
104
|
+
}));
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- Registry ---
|
|
110
|
+
|
|
111
|
+
export function getProviders(): SearchProvider[] {
|
|
112
|
+
return [braveProvider(), tavilyProvider(), serperProvider()];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getAvailableProviders(): SearchProvider[] {
|
|
116
|
+
return getProviders().filter((p) => p.available);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function getProvider(name: string): SearchProvider | undefined {
|
|
120
|
+
return getProviders().find((p) => p.name === name && p.available);
|
|
121
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import type { ExtensionAPI } from "@cwilson613/pi-coding-agent";
|
|
3
|
+
import { startWebUIServer, type WebUIServer } from "./server.ts";
|
|
4
|
+
|
|
5
|
+
let server: WebUIServer | null = null;
|
|
6
|
+
let spawnFn: typeof nodeSpawn = nodeSpawn;
|
|
7
|
+
|
|
8
|
+
export function _setSpawnFn(fn: typeof nodeSpawn): typeof nodeSpawn {
|
|
9
|
+
const prev = spawnFn;
|
|
10
|
+
spawnFn = fn;
|
|
11
|
+
return prev;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function _setServer(next: WebUIServer | null): void {
|
|
15
|
+
server = next;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Returns the current server instance. Prefer this over the live-binding export for reliable cross-Node-version semantics. */
|
|
19
|
+
export function _getServer(): WebUIServer | null {
|
|
20
|
+
return server;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function openBrowser(url: string, ctx: Parameters<typeof notify>[0]): void {
|
|
24
|
+
let cmd: string;
|
|
25
|
+
let args: string[];
|
|
26
|
+
|
|
27
|
+
if (process.platform === "darwin") {
|
|
28
|
+
cmd = "open";
|
|
29
|
+
args = [url];
|
|
30
|
+
} else if (process.platform === "win32") {
|
|
31
|
+
// cmd.exe `start` requires an empty-string window-title when the next token is a URL.
|
|
32
|
+
cmd = "cmd";
|
|
33
|
+
args = ["/c", "start", "", url];
|
|
34
|
+
} else {
|
|
35
|
+
cmd = "xdg-open";
|
|
36
|
+
args = [url];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const child = spawnFn(cmd, args, { stdio: "ignore" });
|
|
40
|
+
if (typeof child.on === "function") {
|
|
41
|
+
child.on("error", (err: Error) => {
|
|
42
|
+
notify(ctx, `Failed to open browser (${cmd}): ${err.message}`);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (typeof child.unref === "function") {
|
|
46
|
+
child.unref();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function notify(ctx: { ui?: { notify?: (msg: string, level?: "info" | "warning" | "error") => void } }, message: string): void {
|
|
51
|
+
if (typeof ctx.ui?.notify === "function") ctx.ui.notify(message, "info");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default function webUiExtension(pi: ExtensionAPI): void {
|
|
55
|
+
pi.registerCommand("web-ui", {
|
|
56
|
+
description: "Localhost-only read-only web UI dashboard (/web-ui [start|stop|status|open])",
|
|
57
|
+
handler: async (args, ctx) => {
|
|
58
|
+
const subcommand = args.trim().split(/\s+/)[0]?.toLowerCase() || "status";
|
|
59
|
+
|
|
60
|
+
switch (subcommand) {
|
|
61
|
+
case "status":
|
|
62
|
+
case "": {
|
|
63
|
+
if (!server) {
|
|
64
|
+
notify(ctx, "web-ui server is stopped. Run `/web-ui start` to start it.");
|
|
65
|
+
} else {
|
|
66
|
+
const uptimeSec = Math.round((Date.now() - server.startedAt) / 1000);
|
|
67
|
+
notify(ctx, `web-ui server is running at ${server.url} (uptime ${uptimeSec}s)`);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
case "start": {
|
|
72
|
+
if (server) {
|
|
73
|
+
notify(ctx, `web-ui server is already running at ${server.url}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
server = await startWebUIServer({ repoRoot: ctx.cwd ?? process.cwd() });
|
|
77
|
+
notify(ctx, `web-ui server started at ${server.url}. Run \`/web-ui open\` to open it in your browser.`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
case "stop": {
|
|
81
|
+
if (!server) {
|
|
82
|
+
notify(ctx, "web-ui server is not running.");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await server.stop().catch(() => {});
|
|
86
|
+
server = null;
|
|
87
|
+
notify(ctx, "web-ui server stopped.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
case "open": {
|
|
91
|
+
if (!server) {
|
|
92
|
+
notify(ctx, "web-ui server is not running. Run `/web-ui start` first.");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
openBrowser(server.url, ctx);
|
|
96
|
+
notify(ctx, `Opening ${server.url} in your default browser…`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
default:
|
|
100
|
+
notify(ctx, "Usage: /web-ui [start|stop|status|open]");
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
pi.on("session_shutdown", async () => {
|
|
106
|
+
if (!server) return;
|
|
107
|
+
await server.stop().catch(() => {});
|
|
108
|
+
server = null;
|
|
109
|
+
});
|
|
110
|
+
}
|