pi-orchestrator-config 4.1.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 +808 -0
- package/agents/api-documenter.md +32 -0
- package/agents/bash-expert.md +53 -0
- package/agents/code-reviewer-docs.md +145 -0
- package/agents/code-reviewer-guidelines.md +97 -0
- package/agents/code-reviewer-quality.md +142 -0
- package/agents/code-reviewer-security.md +107 -0
- package/agents/code-reviewer-spec.md +164 -0
- package/agents/debugger.md +63 -0
- package/agents/docker-expert.md +40 -0
- package/agents/docs-fetcher.md +48 -0
- package/agents/git-expert.md +63 -0
- package/agents/github-expert.md +83 -0
- package/agents/go-expert.md +62 -0
- package/agents/issue-reviewer-feasibility.md +60 -0
- package/agents/issue-reviewer-scope.md +63 -0
- package/agents/issue-reviewer-spec.md +77 -0
- package/agents/java-expert.md +31 -0
- package/agents/jenkins-expert.md +38 -0
- package/agents/kubernetes-expert.md +40 -0
- package/agents/planner.md +42 -0
- package/agents/python-expert.md +61 -0
- package/agents/reviewer.md +26 -0
- package/agents/scout.md +40 -0
- package/agents/security-auditor.md +254 -0
- package/agents/technical-documentation-writer.md +37 -0
- package/agents/test-automator.md +55 -0
- package/agents/test-runner.md +47 -0
- package/agents/ts-expert.md +23 -0
- package/agents/worker.md +30 -0
- package/extensions/acpx-provider/index.ts +104 -0
- package/extensions/acpx-provider/load-runtime.ts +99 -0
- package/extensions/acpx-provider/runtime-models.ts +40 -0
- package/extensions/cli-provider/agents/claude.ts +128 -0
- package/extensions/cli-provider/agents/cursor.ts +84 -0
- package/extensions/cli-provider/agents/gemini.ts +100 -0
- package/extensions/cli-provider/discover.ts +77 -0
- package/extensions/cli-provider/index.ts +57 -0
- package/extensions/cli-provider/parsers.ts +304 -0
- package/extensions/cli-provider/providers.ts +58 -0
- package/extensions/cli-provider/runner.ts +225 -0
- package/extensions/cli-provider/runtime-models.ts +32 -0
- package/extensions/cli-provider/session-reaper.ts +122 -0
- package/extensions/cli-provider/sessions.ts +533 -0
- package/extensions/cli-provider/shared/discover-cache.ts +235 -0
- package/extensions/cli-provider/types.ts +48 -0
- package/extensions/coms/coms-net-server.ts +1642 -0
- package/extensions/coms/coms-net-wrapper.ts +521 -0
- package/extensions/coms/coms-net.ts +1589 -0
- package/extensions/coms/coms-p2p.ts +1879 -0
- package/extensions/coms/coms-shared.ts +510 -0
- package/extensions/coms/coms-wrapper.ts +138 -0
- package/extensions/coms/index.ts +20 -0
- package/extensions/coms/themeMap.ts +147 -0
- package/extensions/image-gen/image-gen.ts +332 -0
- package/extensions/image-gen/index.ts +14 -0
- package/extensions/orchestrator/agents.ts +134 -0
- package/extensions/orchestrator/ask-user.ts +293 -0
- package/extensions/orchestrator/async-agents.ts +1037 -0
- package/extensions/orchestrator/async-capability.ts +129 -0
- package/extensions/orchestrator/async-runner.ts +324 -0
- package/extensions/orchestrator/async-status-parse.ts +31 -0
- package/extensions/orchestrator/async-status-ui.ts +201 -0
- package/extensions/orchestrator/async-wait.ts +36 -0
- package/extensions/orchestrator/btw.ts +210 -0
- package/extensions/orchestrator/cron-status-format.ts +126 -0
- package/extensions/orchestrator/cron-status-ui.ts +132 -0
- package/extensions/orchestrator/cron.ts +730 -0
- package/extensions/orchestrator/dreaming.ts +355 -0
- package/extensions/orchestrator/enforcement-helpers.ts +530 -0
- package/extensions/orchestrator/enforcement-rules.ts +329 -0
- package/extensions/orchestrator/enforcement.ts +828 -0
- package/extensions/orchestrator/extended-autocomplete.ts +593 -0
- package/extensions/orchestrator/git-helpers.ts +420 -0
- package/extensions/orchestrator/github-autocomplete.ts +250 -0
- package/extensions/orchestrator/icons.ts +17 -0
- package/extensions/orchestrator/index.ts +100 -0
- package/extensions/orchestrator/memory-embeddings.ts +281 -0
- package/extensions/orchestrator/memory-promotion.ts +449 -0
- package/extensions/orchestrator/memory-provenance.ts +80 -0
- package/extensions/orchestrator/memory-query-class.ts +90 -0
- package/extensions/orchestrator/memory-scoring.ts +434 -0
- package/extensions/orchestrator/memory-tools.ts +883 -0
- package/extensions/orchestrator/memory-tree.ts +212 -0
- package/extensions/orchestrator/nvim.ts +169 -0
- package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
- package/extensions/orchestrator/overlay-dashboard.ts +512 -0
- package/extensions/orchestrator/pi-config-review-state.ts +303 -0
- package/extensions/orchestrator/preference-extractor.ts +106 -0
- package/extensions/orchestrator/project-settings.ts +468 -0
- package/extensions/orchestrator/promotion-queue.ts +311 -0
- package/extensions/orchestrator/resolve-agent-model.ts +32 -0
- package/extensions/orchestrator/review-ui.ts +204 -0
- package/extensions/orchestrator/rule-placeholders.ts +12 -0
- package/extensions/orchestrator/rules.ts +481 -0
- package/extensions/orchestrator/session-search.ts +243 -0
- package/extensions/orchestrator/session-validation.ts +374 -0
- package/extensions/orchestrator/situation-report.ts +370 -0
- package/extensions/orchestrator/status-bar.ts +31 -0
- package/extensions/orchestrator/status-line.ts +192 -0
- package/extensions/orchestrator/status.ts +110 -0
- package/extensions/orchestrator/subagent-tool.ts +1748 -0
- package/extensions/orchestrator/task-lifecycle.ts +88 -0
- package/extensions/orchestrator/utils.ts +227 -0
- package/extensions/pidash/index.ts +15 -0
- package/extensions/pidash/pidash.ts +941 -0
- package/extensions/pidiff/index.ts +13 -0
- package/extensions/pidiff/pidiff.ts +369 -0
- package/extensions/providers/acpx-driver.ts +479 -0
- package/extensions/providers/built-in-drivers.ts +52 -0
- package/extensions/providers/claude-driver.ts +319 -0
- package/extensions/providers/cursor-acpx-driver.ts +398 -0
- package/extensions/providers/cursor-cli-driver.ts +484 -0
- package/extensions/providers/gemini-driver.ts +302 -0
- package/extensions/providers/index.ts +513 -0
- package/extensions/shared/build-system-prompt.ts +57 -0
- package/extensions/shared/create-runtime-provider.ts +190 -0
- package/extensions/shared/daemon-manager.ts +248 -0
- package/extensions/shared/file-logger.ts +115 -0
- package/extensions/shared/managed-refresh.ts +211 -0
- package/extensions/shared/provider-driver.ts +295 -0
- package/extensions/shared/provider-errors.ts +305 -0
- package/extensions/shared/provider-registry.ts +388 -0
- package/extensions/shared/stream-builder.ts +275 -0
- package/extensions/shared/types.ts +20 -0
- package/extensions/shared/ui/README.md +10 -0
- package/extensions/shared/ui/badge.tsx +52 -0
- package/extensions/shared/ui/button.tsx +58 -0
- package/extensions/shared/ui/collapsible.tsx +21 -0
- package/extensions/shared/ui/dropdown-menu.tsx +266 -0
- package/extensions/shared/ui/input.tsx +20 -0
- package/extensions/shared/ui/popover.tsx +114 -0
- package/extensions/shared/ui/resizable.tsx +48 -0
- package/extensions/shared/ui/scroll-area.tsx +53 -0
- package/extensions/shared/ui/select.tsx +201 -0
- package/extensions/shared/ui/separator.tsx +25 -0
- package/extensions/shared/ui/tooltip.tsx +64 -0
- package/extensions/shared/ws-client.ts +74 -0
- package/package.json +59 -0
- package/prompts/coderabbit-rate-limit.md +95 -0
- package/prompts/create-coms-feature-manager.md +186 -0
- package/prompts/create-skill.md +128 -0
- package/prompts/domain-model.md +109 -0
- package/prompts/external-ai.md +665 -0
- package/prompts/implement-and-review.md +25 -0
- package/prompts/implement.md +26 -0
- package/prompts/issue-review.md +256 -0
- package/prompts/pr-review.md +753 -0
- package/prompts/query-db.md +154 -0
- package/prompts/refine-review.md +208 -0
- package/prompts/release.md +235 -0
- package/prompts/remember.md +33 -0
- package/prompts/review-handler-status.md +27 -0
- package/prompts/review-handler.md +677 -0
- package/prompts/review-local.md +159 -0
- package/prompts/scout-and-plan.md +21 -0
- package/rules/00-orchestrator-core.md +42 -0
- package/rules/05-issue-first-workflow.md +119 -0
- package/rules/10-agent-routing.md +58 -0
- package/rules/15-mcp-launchpad.md +46 -0
- package/rules/20-code-review-loop.md +234 -0
- package/rules/25-documentation-updates.md +15 -0
- package/rules/30-prompt-templates.md +19 -0
- package/rules/35-memory.md +240 -0
- package/rules/40-critical-rules.md +124 -0
- package/rules/45-file-preview.md +29 -0
- package/rules/50-agent-bug-reporting.md +90 -0
- package/rules/55-coms-protocol.md +63 -0
- package/rules/60-task-tracking.md +66 -0
- package/skills/conflict-resolver/SKILL.md +213 -0
- package/skills/skill-creator/SKILL.md +85 -0
- package/skills/systematic-debugging/SKILL.md +353 -0
- package/skills/test-driven-development/SKILL.md +335 -0
- package/skills/update-agents-md/SKILL.md +282 -0
- package/templates/coms-feature-manager-prompt.md +313 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Forked from https://github.com/disler/pi-vs-claude-code (commit b93c3f1)
|
|
2
|
+
// We own this code — check upstream periodically for relevant changes.
|
|
3
|
+
/**
|
|
4
|
+
* themeMap.ts — Per-extension default theme assignments
|
|
5
|
+
*
|
|
6
|
+
* Themes live in .pi/themes/ and are mapped by extension filename (no extension).
|
|
7
|
+
* Each extension calls applyExtensionTheme(import.meta.url, ctx) in its session_start
|
|
8
|
+
* hook to automatically load its designated theme on boot.
|
|
9
|
+
*
|
|
10
|
+
* Available themes (.pi/themes/):
|
|
11
|
+
* catppuccin-mocha · cyberpunk · dracula · everforest · gruvbox
|
|
12
|
+
* midnight-ocean · nord · ocean-breeze · rose-pine
|
|
13
|
+
* synthwave · tokyo-night
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { basename } from "path";
|
|
18
|
+
import { fileURLToPath } from "url";
|
|
19
|
+
|
|
20
|
+
// ── Theme assignments ──────────────────────────────────────────────────────
|
|
21
|
+
//
|
|
22
|
+
// Key = extension filename without extension (matches extensions/<key>.ts)
|
|
23
|
+
// Value = theme name from .pi/themes/<value>.json
|
|
24
|
+
//
|
|
25
|
+
export const THEME_MAP: Record<string, string> = {
|
|
26
|
+
"agent-chain": "midnight-ocean", // deep sequential pipeline
|
|
27
|
+
"agent-team": "dracula", // rich orchestration palette
|
|
28
|
+
"coms": "ocean-breeze", // peer-to-peer messaging, cross-boundary
|
|
29
|
+
"coms-net": "ocean-breeze", // peer-to-peer messaging, cross-boundary
|
|
30
|
+
"cross-agent": "ocean-breeze", // cross-boundary, connecting
|
|
31
|
+
"damage-control": "gruvbox", // grounded, earthy safety
|
|
32
|
+
"minimal": "synthwave", // synthwave by default now!
|
|
33
|
+
"pi-pi": "rose-pine", // warm creative meta-agent
|
|
34
|
+
"pure-focus": "everforest", // calm, distraction-free
|
|
35
|
+
"purpose-gate": "tokyo-night", // intentional, sharp focus
|
|
36
|
+
"session-replay": "catppuccin-mocha", // soft, reflective history
|
|
37
|
+
"subagent-widget": "cyberpunk", // multi-agent futuristic
|
|
38
|
+
"system-select": "catppuccin-mocha", // soft selection UI
|
|
39
|
+
"theme-cycler": "synthwave", // neon, it's a theme tool
|
|
40
|
+
"tilldone": "everforest", // task-focused calm
|
|
41
|
+
"tool-counter": "synthwave", // techy metrics
|
|
42
|
+
"tool-counter-widget":"synthwave", // same family
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/** Derive the extension name (e.g. "minimal") from its import.meta.url. */
|
|
48
|
+
function extensionName(fileUrl: string): string {
|
|
49
|
+
const filePath = fileUrl.startsWith("file://") ? fileURLToPath(fileUrl) : fileUrl;
|
|
50
|
+
return basename(filePath).replace(/\.[^.]+$/, "");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Theme ──────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Apply the mapped theme for an extension on session boot.
|
|
57
|
+
*
|
|
58
|
+
* @param fileUrl Pass `import.meta.url` from the calling extension file.
|
|
59
|
+
* @param ctx The ExtensionContext from the session_start handler.
|
|
60
|
+
* @returns true if the theme was applied successfully, false otherwise.
|
|
61
|
+
*/
|
|
62
|
+
export function applyExtensionTheme(fileUrl: string, ctx: ExtensionContext): boolean {
|
|
63
|
+
if (!ctx.hasUI) return false;
|
|
64
|
+
|
|
65
|
+
const name = extensionName(fileUrl);
|
|
66
|
+
|
|
67
|
+
// If there are multiple extensions stacked in 'ipi', they each fire session_start
|
|
68
|
+
// and try to apply their own mapped theme. The LAST one to fire wins.
|
|
69
|
+
// Since system-select is last in the ipi alias array, it was setting 'catppuccin-mocha'.
|
|
70
|
+
|
|
71
|
+
// We want to skip theme application for all secondary extensions if they are stacked,
|
|
72
|
+
// so the primary extension (first in the array) dictates the theme.
|
|
73
|
+
const primaryExt = primaryExtensionName();
|
|
74
|
+
if (primaryExt && primaryExt !== name) {
|
|
75
|
+
return true; // Pretend we succeeded, but don't overwrite the primary theme
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let themeName = THEME_MAP[name];
|
|
79
|
+
|
|
80
|
+
if (!themeName) {
|
|
81
|
+
themeName = "synthwave";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = ctx.ui.setTheme(themeName);
|
|
85
|
+
|
|
86
|
+
if (!result.success && themeName !== "synthwave") {
|
|
87
|
+
return ctx.ui.setTheme("synthwave").success;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result.success;
|
|
91
|
+
}
|
|
92
|
+
// ── Title ──────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Read process.argv to find the first -e / --extension flag value.
|
|
96
|
+
*
|
|
97
|
+
* When Pi is launched as:
|
|
98
|
+
* pi -e extensions/subagent-widget.ts -e extensions/pure-focus.ts
|
|
99
|
+
*
|
|
100
|
+
* process.argv contains those paths verbatim. Every stacked extension calls
|
|
101
|
+
* this and gets the same answer ("subagent-widget"), so all setTitle calls
|
|
102
|
+
* are idempotent — no shared state or deduplication needed.
|
|
103
|
+
*
|
|
104
|
+
* Returns null if no -e flag is present (e.g. plain `pi` with no extensions).
|
|
105
|
+
*/
|
|
106
|
+
function primaryExtensionName(): string | null {
|
|
107
|
+
const argv = process.argv;
|
|
108
|
+
for (let i = 0; i < argv.length - 1; i++) {
|
|
109
|
+
if (argv[i] === "-e" || argv[i] === "--extension") {
|
|
110
|
+
return basename(argv[i + 1]).replace(/\.[^.]+$/, "");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Set the terminal title to "π - <first-extension-name>" on session boot.
|
|
118
|
+
* Reads the title from process.argv so all stacked extensions agree on the
|
|
119
|
+
* same value — no coordination or shared state required.
|
|
120
|
+
*
|
|
121
|
+
* Deferred 150 ms to fire after Pi's own startup title-set.
|
|
122
|
+
*/
|
|
123
|
+
function applyExtensionTitle(ctx: ExtensionContext): void {
|
|
124
|
+
if (!ctx.hasUI) return;
|
|
125
|
+
const name = primaryExtensionName();
|
|
126
|
+
if (!name) return;
|
|
127
|
+
setTimeout(() => ctx.ui.setTitle(`π - ${name}`), 150);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Combined default ───────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Apply both the mapped theme AND the terminal title for an extension.
|
|
134
|
+
* Drop-in replacement for applyExtensionTheme — call this in every session_start.
|
|
135
|
+
*
|
|
136
|
+
* Usage:
|
|
137
|
+
* import { applyExtensionDefaults } from "./themeMap.js";
|
|
138
|
+
*
|
|
139
|
+
* pi.on("session_start", async (_event, ctx) => {
|
|
140
|
+
* applyExtensionDefaults(import.meta.url, ctx);
|
|
141
|
+
* // ... rest of handler
|
|
142
|
+
* });
|
|
143
|
+
*/
|
|
144
|
+
export function applyExtensionDefaults(fileUrl: string, ctx: ExtensionContext): void {
|
|
145
|
+
applyExtensionTheme(fileUrl, ctx);
|
|
146
|
+
applyExtensionTitle(ctx);
|
|
147
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* image-gen.ts — Image generation tool via Gemini API
|
|
3
|
+
*
|
|
4
|
+
* Generates images from structured parameters using Google's Gemini API.
|
|
5
|
+
* Model via pi-config-settings.json (`image_model`) or PI_IMAGE_MODEL env.
|
|
6
|
+
* API key via GEMINI_API_KEY or GOOGLE_API_KEY (env only — secrets stay out of settings).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Type } from "typebox";
|
|
10
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { getSetting } from "../orchestrator/project-settings.js";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const ASPECT_RATIOS = ["1:1", "3:4", "4:3", "9:16", "16:9"] as const;
|
|
18
|
+
const IMAGE_TIMEOUT = 3 * 60 * 1000; // 3 minutes
|
|
19
|
+
|
|
20
|
+
const imageGenSchema = Type.Object({
|
|
21
|
+
subject: Type.String({ description: "Main subject of the image" }),
|
|
22
|
+
action: Type.Optional(Type.String({ description: "What the subject is doing" })),
|
|
23
|
+
scene: Type.Optional(Type.String({ description: "Location or environment" })),
|
|
24
|
+
composition: Type.Optional(Type.String({ description: "Camera angle and framing" })),
|
|
25
|
+
lighting: Type.Optional(Type.String({ description: "Lighting setup" })),
|
|
26
|
+
style: Type.Optional(Type.String({ description: "Artistic style (e.g., photorealistic, watercolor, pixel art)" })),
|
|
27
|
+
text: Type.Optional(Type.String({ description: "Text to render in the image" })),
|
|
28
|
+
aspect_ratio: Type.Optional(Type.Enum(
|
|
29
|
+
Object.fromEntries(ASPECT_RATIOS.map(r => [r, r])) as Record<string, string>,
|
|
30
|
+
{ description: "Aspect ratio: 1:1, 3:4, 4:3, 9:16, 16:9" }
|
|
31
|
+
)),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
interface GeminiPart {
|
|
35
|
+
text?: string;
|
|
36
|
+
inlineData?: { mimeType: string; data: string };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface GeminiCandidate {
|
|
40
|
+
content?: { parts?: GeminiPart[] };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface GeminiResponse {
|
|
44
|
+
candidates?: GeminiCandidate[];
|
|
45
|
+
promptFeedback?: { blockReason?: string };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getApiKey(): string | null {
|
|
49
|
+
return process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getModel(cwd: string): string | null {
|
|
53
|
+
const model = getSetting(cwd, "image_model");
|
|
54
|
+
return model || null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function assemblePrompt(params: {
|
|
58
|
+
subject: string;
|
|
59
|
+
action?: string;
|
|
60
|
+
scene?: string;
|
|
61
|
+
composition?: string;
|
|
62
|
+
lighting?: string;
|
|
63
|
+
style?: string;
|
|
64
|
+
text?: string;
|
|
65
|
+
}): string {
|
|
66
|
+
const parts: string[] = [];
|
|
67
|
+
|
|
68
|
+
const subjectParts = [params.subject];
|
|
69
|
+
if (params.action) subjectParts.push(params.action);
|
|
70
|
+
if (params.scene) subjectParts.push(params.scene);
|
|
71
|
+
parts.push(subjectParts.join(", "));
|
|
72
|
+
|
|
73
|
+
if (params.composition) parts.push(params.composition);
|
|
74
|
+
if (params.lighting) parts.push(params.lighting);
|
|
75
|
+
if (params.style) parts.push(params.style);
|
|
76
|
+
|
|
77
|
+
let prompt = `${parts.map(p => p.replace(/[.!,;:]+$/, "")).join(". ")}.`;
|
|
78
|
+
|
|
79
|
+
if (params.text) {
|
|
80
|
+
prompt += `\n\nText: ${params.text}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return prompt;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getExtensionForMime(mimeType: string): string {
|
|
87
|
+
const map: Record<string, string> = {
|
|
88
|
+
"image/png": "png",
|
|
89
|
+
"image/jpeg": "jpg",
|
|
90
|
+
"image/gif": "gif",
|
|
91
|
+
"image/webp": "webp",
|
|
92
|
+
};
|
|
93
|
+
return map[mimeType] ?? "png";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Same logic as getProjectTmpDir in extensions/orchestrator/utils.ts
|
|
97
|
+
function getTempDir(cwd: string): string {
|
|
98
|
+
const dir = path.join(cwd, ".pi", "tmp");
|
|
99
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
100
|
+
return dir;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getHttpdPath(): string {
|
|
104
|
+
// Resolve relative to this extension's location → ../../scripts/httpd.py
|
|
105
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
106
|
+
return path.resolve(thisDir, "..", "..", "scripts", "httpd.py");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function serveImage(imagePath: string): { url: string; port: string; dir: string } | null {
|
|
110
|
+
// Only serve via HTTP in containers (where user can't access local files)
|
|
111
|
+
// Use filesystem checks matching orchestrator's isRunningInContainer()
|
|
112
|
+
const inContainer = fs.existsSync("/.dockerenv") ||
|
|
113
|
+
fs.existsSync("/run/.containerenv") ||
|
|
114
|
+
(() => { try { return fs.readFileSync("/proc/1/cgroup", "utf-8").includes("docker") || fs.readFileSync("/proc/1/cgroup", "utf-8").includes("containerd"); } catch { return false; } })();
|
|
115
|
+
if (!inContainer) return null;
|
|
116
|
+
|
|
117
|
+
const httpd = getHttpdPath();
|
|
118
|
+
if (!fs.existsSync(httpd)) return null;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
// Find a free port
|
|
122
|
+
const port = execFileSync("uv", ["run", "python3", httpd, "--find-port"], {
|
|
123
|
+
encoding: "utf-8",
|
|
124
|
+
timeout: 5000,
|
|
125
|
+
}).trim();
|
|
126
|
+
|
|
127
|
+
const dir = path.dirname(imagePath);
|
|
128
|
+
const filename = path.basename(imagePath);
|
|
129
|
+
const logFile = path.join(dir, `httpd-${port}.log`);
|
|
130
|
+
|
|
131
|
+
// Launch server in background — close FDs after spawn
|
|
132
|
+
const logFd = fs.openSync(logFile, "a");
|
|
133
|
+
const child = spawn("uv", ["run", "python3", httpd, "--port", port, "--dir", dir], {
|
|
134
|
+
detached: true,
|
|
135
|
+
stdio: ["ignore", logFd, logFd],
|
|
136
|
+
});
|
|
137
|
+
child.unref();
|
|
138
|
+
fs.closeSync(logFd);
|
|
139
|
+
|
|
140
|
+
return { url: `http://localhost:${port}/${filename}`, port, dir };
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createImageGenTool(): ToolDefinition {
|
|
147
|
+
return {
|
|
148
|
+
name: "generate_image",
|
|
149
|
+
label: "GenerateImage",
|
|
150
|
+
description: [
|
|
151
|
+
"Generate an image from a structured description using Gemini.",
|
|
152
|
+
"Provide a subject (required) and optional parameters for action, scene, composition, lighting, style, and text.",
|
|
153
|
+
"Returns the file path of the generated image.",
|
|
154
|
+
"Requires image_model in pi-config-settings.json (or PI_IMAGE_MODEL) and GEMINI_API_KEY/GOOGLE_API_KEY.",
|
|
155
|
+
].join("\n"),
|
|
156
|
+
promptSnippet: "generate_image: Generate images from structured descriptions via Gemini API",
|
|
157
|
+
parameters: imageGenSchema,
|
|
158
|
+
|
|
159
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
160
|
+
const model = getModel(ctx.cwd);
|
|
161
|
+
if (!model) {
|
|
162
|
+
return {
|
|
163
|
+
content: [{
|
|
164
|
+
type: "text" as const,
|
|
165
|
+
text: "Error: image model is not set. Add image_model to pi-config-settings.json (or PI_IMAGE_MODEL env) and reload pi.",
|
|
166
|
+
}],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const apiKey = getApiKey();
|
|
171
|
+
if (!apiKey) {
|
|
172
|
+
return {
|
|
173
|
+
content: [{
|
|
174
|
+
type: "text" as const,
|
|
175
|
+
text: "Error: No API key found. Set GEMINI_API_KEY or GOOGLE_API_KEY environment variable and restart pi.",
|
|
176
|
+
}],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const promptText = assemblePrompt(params);
|
|
181
|
+
|
|
182
|
+
const generationConfig: Record<string, unknown> = {
|
|
183
|
+
responseModalities: ["IMAGE"],
|
|
184
|
+
};
|
|
185
|
+
if (params.aspect_ratio) {
|
|
186
|
+
generationConfig.imageConfig = { aspectRatio: params.aspect_ratio };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const requestBody = {
|
|
190
|
+
contents: [{ role: "user" as const, parts: [{ text: promptText }] }],
|
|
191
|
+
generationConfig,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const abortSignal = signal ?? AbortSignal.timeout(IMAGE_TIMEOUT);
|
|
195
|
+
|
|
196
|
+
let response: Response;
|
|
197
|
+
try {
|
|
198
|
+
response = await fetch(
|
|
199
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
|
|
200
|
+
{
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: {
|
|
203
|
+
"Content-Type": "application/json",
|
|
204
|
+
"x-goog-api-key": apiKey,
|
|
205
|
+
},
|
|
206
|
+
body: JSON.stringify(requestBody),
|
|
207
|
+
signal: abortSignal,
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
} catch (err: any) {
|
|
211
|
+
return {
|
|
212
|
+
content: [{
|
|
213
|
+
type: "text" as const,
|
|
214
|
+
text: `Error: Gemini API request failed: ${err?.message ?? String(err)}`,
|
|
215
|
+
}],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const rawText = await response.text();
|
|
220
|
+
if (!response.ok) {
|
|
221
|
+
let message = rawText;
|
|
222
|
+
try {
|
|
223
|
+
const parsed = JSON.parse(rawText) as { error?: { message?: string } };
|
|
224
|
+
message = parsed.error?.message ?? message;
|
|
225
|
+
} catch {
|
|
226
|
+
// Keep raw text
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
content: [{
|
|
230
|
+
type: "text" as const,
|
|
231
|
+
text: `Error: Gemini image request failed (${response.status}): ${message}`,
|
|
232
|
+
}],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let data: GeminiResponse;
|
|
237
|
+
try {
|
|
238
|
+
data = JSON.parse(rawText) as GeminiResponse;
|
|
239
|
+
} catch {
|
|
240
|
+
return {
|
|
241
|
+
content: [{
|
|
242
|
+
type: "text" as const,
|
|
243
|
+
text: `Error: Failed to parse Gemini response: ${rawText.slice(0, 200)}`,
|
|
244
|
+
}],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (data.promptFeedback?.blockReason) {
|
|
249
|
+
return {
|
|
250
|
+
content: [{
|
|
251
|
+
type: "text" as const,
|
|
252
|
+
text: `Error: Image generation blocked by safety filter: ${data.promptFeedback.blockReason}`,
|
|
253
|
+
}],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const images: { data: string; mimeType: string }[] = [];
|
|
258
|
+
let responseText = "";
|
|
259
|
+
|
|
260
|
+
for (const candidate of data.candidates ?? []) {
|
|
261
|
+
for (const part of candidate.content?.parts ?? []) {
|
|
262
|
+
if (part.inlineData?.data && part.inlineData?.mimeType) {
|
|
263
|
+
images.push({
|
|
264
|
+
data: part.inlineData.data,
|
|
265
|
+
mimeType: part.inlineData.mimeType,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (part.text) {
|
|
269
|
+
responseText += part.text;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (images.length === 0) {
|
|
275
|
+
const extra = responseText ? `\n\n${responseText}` : "";
|
|
276
|
+
return {
|
|
277
|
+
content: [{
|
|
278
|
+
type: "text" as const,
|
|
279
|
+
text: `No image data returned from Gemini.${extra}`,
|
|
280
|
+
}],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
285
|
+
const tempDir = getTempDir(cwd);
|
|
286
|
+
const savedPaths: string[] = [];
|
|
287
|
+
|
|
288
|
+
for (const image of images) {
|
|
289
|
+
const ext = getExtensionForMime(image.mimeType);
|
|
290
|
+
const filename = `pi-image-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
|
|
291
|
+
const filepath = path.join(tempDir, filename);
|
|
292
|
+
try {
|
|
293
|
+
const buffer = Buffer.from(image.data, "base64");
|
|
294
|
+
if (buffer.length === 0) continue; // Skip empty/malformed data
|
|
295
|
+
fs.writeFileSync(filepath, buffer);
|
|
296
|
+
savedPaths.push(filepath);
|
|
297
|
+
} catch (err: any) {
|
|
298
|
+
// Skip corrupted image data — log but don't crash
|
|
299
|
+
console.debug(`[image-gen] Failed to decode/save image: ${err?.message?.slice(0, 100)}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Start a single preview server for all images (container only)
|
|
304
|
+
let previewServer: { url: string; port: string; dir: string } | null = null;
|
|
305
|
+
if (savedPaths.length > 0) {
|
|
306
|
+
previewServer = serveImage(savedPaths[0]);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const lines = [
|
|
310
|
+
`Model: ${model}`,
|
|
311
|
+
`Generated ${savedPaths.length} image(s):`,
|
|
312
|
+
];
|
|
313
|
+
for (const p of savedPaths) {
|
|
314
|
+
lines.push(` ${p}`);
|
|
315
|
+
if (previewServer) {
|
|
316
|
+
const filename = path.basename(p);
|
|
317
|
+
lines.push(` Preview: http://localhost:${previewServer.port}/${filename}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (responseText.trim()) {
|
|
321
|
+
lines.push("", responseText.trim());
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
content: [{
|
|
326
|
+
type: "text" as const,
|
|
327
|
+
text: lines.join("\n"),
|
|
328
|
+
}],
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Generation Extension for pi
|
|
3
|
+
*
|
|
4
|
+
* Registers a `generate_image` tool that generates images via Gemini API.
|
|
5
|
+
* Requires image_model in pi-config-settings.json (or PI_IMAGE_MODEL) and
|
|
6
|
+
* GEMINI_API_KEY/GOOGLE_API_KEY environment variables.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { createImageGenTool } from "./image-gen.js";
|
|
11
|
+
|
|
12
|
+
export default function (pi: ExtensionAPI) {
|
|
13
|
+
pi.registerTool(createImageGenTool());
|
|
14
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent discovery — extended from pi's subagent example.
|
|
3
|
+
*
|
|
4
|
+
* Discovery sources (in priority order, later overrides earlier):
|
|
5
|
+
* 1. Package-bundled agents (this package's agents/ directory)
|
|
6
|
+
* 2. User agents (~/.pi/agent/agents/)
|
|
7
|
+
* 3. Project agents (.pi/agents/ in cwd or parent dirs)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
|
|
14
|
+
export type AgentScope = "user" | "project" | "both";
|
|
15
|
+
|
|
16
|
+
export interface AgentConfig {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
tools?: string[];
|
|
20
|
+
model?: string;
|
|
21
|
+
provider?: string;
|
|
22
|
+
systemPrompt: string;
|
|
23
|
+
source: "user" | "project" | "package";
|
|
24
|
+
filePath: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AgentDiscoveryResult {
|
|
28
|
+
agents: AgentConfig[];
|
|
29
|
+
projectAgentsDir: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function loadAgentsFromDir(dir: string, source: "user" | "project" | "package"): AgentConfig[] {
|
|
33
|
+
const agents: AgentConfig[] = [];
|
|
34
|
+
|
|
35
|
+
if (!fs.existsSync(dir)) return agents;
|
|
36
|
+
|
|
37
|
+
let entries: fs.Dirent[];
|
|
38
|
+
try {
|
|
39
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
40
|
+
} catch {
|
|
41
|
+
return agents;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
46
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
47
|
+
|
|
48
|
+
const filePath = path.join(dir, entry.name);
|
|
49
|
+
let content: string;
|
|
50
|
+
try {
|
|
51
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
52
|
+
} catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
|
|
57
|
+
|
|
58
|
+
if (!frontmatter.name || !frontmatter.description) continue;
|
|
59
|
+
|
|
60
|
+
const tools = frontmatter.tools
|
|
61
|
+
?.split(",")
|
|
62
|
+
.map((t: string) => t.trim())
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
|
|
65
|
+
agents.push({
|
|
66
|
+
name: frontmatter.name,
|
|
67
|
+
description: frontmatter.description,
|
|
68
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
69
|
+
model: typeof frontmatter.model === "string" ? frontmatter.model.trim() || undefined : undefined,
|
|
70
|
+
provider: typeof frontmatter.provider === "string" ? frontmatter.provider.trim() || undefined : undefined,
|
|
71
|
+
systemPrompt: body,
|
|
72
|
+
source,
|
|
73
|
+
filePath,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return agents;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isDirectory(p: string): boolean {
|
|
81
|
+
try {
|
|
82
|
+
return fs.statSync(p).isDirectory();
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
89
|
+
let currentDir = cwd;
|
|
90
|
+
while (true) {
|
|
91
|
+
const candidate = path.join(currentDir, ".pi", "agents");
|
|
92
|
+
if (isDirectory(candidate)) return candidate;
|
|
93
|
+
|
|
94
|
+
const parentDir = path.dirname(currentDir);
|
|
95
|
+
if (parentDir === currentDir) return null;
|
|
96
|
+
currentDir = parentDir;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get the package's bundled agents directory.
|
|
102
|
+
* This file lives in extensions/orchestrator/agents.ts,
|
|
103
|
+
* and the agents are in ../../agents/ relative to this file.
|
|
104
|
+
*/
|
|
105
|
+
function getPackageAgentsDir(): string {
|
|
106
|
+
return path.resolve(__dirname, "..", "..", "agents");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
110
|
+
const packageDir = getPackageAgentsDir();
|
|
111
|
+
const userDir = path.join(getAgentDir(), "agents");
|
|
112
|
+
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
113
|
+
|
|
114
|
+
// Load from all sources
|
|
115
|
+
const packageAgents = loadAgentsFromDir(packageDir, "package");
|
|
116
|
+
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
117
|
+
const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
118
|
+
|
|
119
|
+
// Merge: package < user < project (later overrides earlier by name)
|
|
120
|
+
const agentMap = new Map<string, AgentConfig>();
|
|
121
|
+
|
|
122
|
+
// Package agents are always loaded as the base
|
|
123
|
+
for (const agent of packageAgents) agentMap.set(agent.name, agent);
|
|
124
|
+
|
|
125
|
+
if (scope !== "project") {
|
|
126
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (scope !== "user" && projectAgentsDir) {
|
|
130
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
134
|
+
}
|