pi-orchestrator-config 4.3.7 → 4.3.9
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 +191 -173
- package/agents/git-expert.md +30 -1
- package/extensions/openai-compatible-discovery/index.ts +204 -0
- package/extensions/orchestrator/extended-autocomplete.ts +4 -0
- package/extensions/orchestrator/mcpc.ts +172 -0
- package/extensions/orchestrator/session-validation.ts +4 -2
- package/extensions/pidiff/pidiff-ui/dist/assets/{angular-html-mp_sY19l.js → angular-html-ghA4VuM0.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{angular-ts-BG7dz03J.js → angular-ts-BiEwt-Qs.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{apl-D0TQH1Y8.js → apl-DmYAGNOL.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{astro-cuP4968W.js → astro-Bju7L81l.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{blade-ClqnBqi9.js → blade-C_YtSRJN.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{c-VbuMUaRD.js → c-DxcP9pmN.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{chapel-Cs_P4bPZ.js → chapel-BjVM8zvB.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{cobol-B1_QfE_s.js → cobol-CyAifE-8.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{coffee-DngpUQ-g.js → coffee-BNW4Zttb.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{cpp-BJ9Ee2yy.js → cpp-De6s9lN-.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{crystal-ClUiGo15.js → crystal-JzhjW33_.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{css-DqTo82CB.js → css-DAnI1EKT.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{edge-DNO_p1iz.js → edge-DlbkxEj4.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{elixir-D9BFTDA2.js → elixir-BXz6vpDX.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{elm-DViZ7oy1.js → elm-CJcSKM6_.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{erb-BkoQ4OFP.js → erb-VHOaTR5g.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{git-rebase-7gJjIY6i.js → git-rebase-PAYwCVk-.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{glimmer-js-BsUtNNHb.js → glimmer-js-CspJJiU_.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{glimmer-ts-D-0tIMzg.js → glimmer-ts-CSMIai5o.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{glsl-B_FkRuRZ.js → glsl-Cw9yKL2B.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{graphql--F2JnwKx.js → graphql-D0OMgGsM.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{hack-oDlxgxMa.js → hack-DfiAACVA.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{haml-BZwbB22w.js → haml-DU8pAbRK.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{handlebars-D5xaEd_e.js → handlebars-CoaiK3xT.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{html-DBGhw7-O.js → html-B4C3aTRB.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{html-derivative-0H2oo-Br.js → html-derivative-C_NJUsxF.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{http-BH6WBJxg.js → http-Cr9w59E3.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{hurl-pKbsDPF3.js → hurl-BEScLvfg.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/index-CcrT73jQ.css +1 -0
- package/extensions/pidiff/pidiff-ui/dist/assets/{index-D9FMfzhy.js → index-CisAus9r.js} +10 -10
- package/extensions/pidiff/pidiff-ui/dist/assets/{java-DAIRA2DV.js → java-hb-3TTCg.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{javascript-0oTH0XMX.js → javascript-Bc0DeQtz.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{jinja-Cr2SKvYs.js → jinja-DoEIYL2y.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{jison-DkQ5WAwI.js → jison-C2OZYR2e.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{json-Dso4xRBa.js → json-DKeabECb.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{jsx-6lZH5KCN.js → jsx-Dadu-wAB.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{julia-D5nsVlA4.js → julia-CpUr0nh2.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{just-BJmCTT8q.js → just-BysHL69Z.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{latex-RNBDVFOB.js → latex-iNrlK36R.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{liquid-BNfcDoyE.js → liquid-g8x_A5Ru.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{lua-C5h0aGmI.js → lua-B4BSWsUC.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{marko-DNEK1bML.js → marko-CHSFXjEi.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{mdc-B2NVBmGf.js → mdc-js2C5rVG.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{nginx-_X9UmOgs.js → nginx-BblTQvRl.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{nim-DOS51HeY.js → nim-ChxMAPPZ.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{org-Rx4519lx.js → org-CHoj3SWZ.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{perl-Daok6kZs.js → perl-3zusLAIc.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{php-XTt5v859.js → php-DVx3dbu3.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{pug-f7ey1fQl.js → pug-DITNjLsK.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{qml-juoUuAUN.js → qml-BYmD788c.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{r-BOYZHofT.js → r-Dp_qVPOX.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{razor-BcuibiM6.js → razor-DT4CEDjF.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{regexp-BMJaNING.js → regexp-DVFGbBOe.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{rst-CJY1LueG.js → rst-njZM0E3u.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{ruby-r3j2HpTd.js → ruby-gu2hDDCx.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{sas-D60aovOU.js → sas-Bo6NwyH7.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{scss-DsQ9e_9i.js → scss-COdGMqPA.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{shellscript-C0CTDLfP.js → shellscript-DSg7qNC1.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{shellsession-CYQ4mhku.js → shellsession-CD1RmUpx.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{soy-K7-GBqQU.js → soy-C0QWCEMl.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{sql-BPU0aGlV.js → sql-VoWQdTUw.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{stata-BBQHbN5d.js → stata-BEj75h4q.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{surrealql-DaVLur3e.js → surrealql-Cj1XbW6j.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{svelte-CPKe6KLX.js → svelte-HcwCBvjC.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{templ-4FsggzCO.js → templ-D775vyzu.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{tex-Bz9V0XEc.js → tex-DNmK_71Q.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{ts-tags-BAJP-RsL.js → ts-tags-KERjFDJj.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{tsx-CaD50Q9K.js → tsx-CMT8AiHA.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{twig-Bo_rsqRW.js → twig-B5uxCA6R.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{typescript-BTuHo9Ga.js → typescript-5zwf3bJw.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{typst-BvQMhY4k.js → typst-xx02l63O.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{vue-D8h9s3oH.js → vue-BvvQRkFj.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{vue-html-UkoLjlSQ.js → vue-html-BrBDvgBy.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{vue-vine-lVry86q7.js → vue-vine-1CllPWUz.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{xml-BxyPU0Nr.js → xml-C3nsfg4x.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{xsl-BUb2EUMx.js → xsl-DDOShcMh.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/{yaml-qnjzIwGf.js → yaml-BGNXenqm.js} +1 -1
- package/extensions/pidiff/pidiff-ui/dist/index.html +2 -2
- package/extensions/pidiff/pidiff-ui/package.json +1 -1
- package/extensions/pidiff/pidiff-ui/src/App.tsx +95 -36
- package/extensions/pidiff/pidiff-ui/src/hooks/useWebSocket.ts +52 -10
- package/extensions/pidiff/pidiff-ui/src/lib/app-reconnect-watch.tsx +32 -0
- package/extensions/pidiff/pidiff-ui/src/lib/app-refresh-actions.tsx +47 -0
- package/extensions/pidiff/pidiff-ui/src/lib/create-logger.ts +37 -0
- package/extensions/pidiff/pidiff-ui/src/lib/file-cache-key.ts +34 -0
- package/extensions/pidiff/pidiff-ui/src/lib/refresh-control.tsx +43 -0
- package/extensions/pidiff/pidiff-ui/src/lib/request-diffs.ts +91 -0
- package/extensions/pidiff/pidiff-ui/src/lib/ws-send.ts +141 -0
- package/extensions/shared/openai-compatible-discovery.ts +558 -0
- package/package.json +5 -2
- package/rules/00-orchestrator-core.md +2 -2
- package/rules/15-mcpc.md +35 -0
- package/rules/20-code-review-loop.md +11 -1
- package/scripts/pidiff-git-ignore.ts +55 -0
- package/scripts/pidiff-server.ts +27 -26
- package/settings-keys.json +1 -1
- package/extensions/pidiff/pidiff-ui/dist/assets/index-CHmSwztf.css +0 -1
- package/rules/15-mcp-launchpad.md +0 -46
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
7
|
+
import { createLogger } from "./logger.js";
|
|
8
|
+
|
|
9
|
+
const log = createLogger("openai-compatible-discovery");
|
|
10
|
+
|
|
11
|
+
export const OPENAI_COMPATIBLE_DISCOVERY_TTL_MS = 5 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
export interface ResolvedOpenAiCompatibleConnection {
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
headers?: ProviderHeaders;
|
|
17
|
+
}
|
|
18
|
+
export interface OpenAiCompatibleModelsRequest {
|
|
19
|
+
/** Discovery-only /v1/models endpoint. Never use as a provider stream URL. */
|
|
20
|
+
url: string;
|
|
21
|
+
/** Resolved source endpoint for materialized provider streams. */
|
|
22
|
+
streamBaseUrl: string;
|
|
23
|
+
headers: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
export interface OpenAiCompatibleModelRecord {
|
|
26
|
+
id?: unknown;
|
|
27
|
+
name?: unknown;
|
|
28
|
+
reasoning?: unknown;
|
|
29
|
+
input?: unknown;
|
|
30
|
+
cost?: unknown;
|
|
31
|
+
contextWindow?: unknown;
|
|
32
|
+
maxTokens?: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Pi's models.json defaults for omitted static-model metadata. */
|
|
36
|
+
const PI_STATIC_MODEL_DEFAULTS = {
|
|
37
|
+
reasoning: false,
|
|
38
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
39
|
+
contextWindow: 128_000,
|
|
40
|
+
maxTokens: 16_384,
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
function finiteNumber(value: unknown, fallback: number): number {
|
|
44
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function positiveFiniteNumber(value: unknown, fallback: number): number {
|
|
48
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
49
|
+
? value
|
|
50
|
+
: fallback;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* This is a Pi compatibility declaration, not discovery filtering or a claim
|
|
55
|
+
* that the source endpoint accepts images. The /v1/models payload does not
|
|
56
|
+
* reliably advertise capabilities, so unknown models must remain selectable.
|
|
57
|
+
*/
|
|
58
|
+
function materializeInput(value: unknown): ("text" | "image")[] {
|
|
59
|
+
if (Array.isArray(value) && value.every((item) => item === "text" || item === "image"))
|
|
60
|
+
return [...value] as ("text" | "image")[];
|
|
61
|
+
return ["text", "image"];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function materializeCost(value: unknown): Model<Api>["cost"] {
|
|
65
|
+
const source = value && typeof value === "object" && !Array.isArray(value)
|
|
66
|
+
? value as Record<string, unknown>
|
|
67
|
+
: {};
|
|
68
|
+
return {
|
|
69
|
+
input: finiteNumber(source.input, PI_STATIC_MODEL_DEFAULTS.cost.input),
|
|
70
|
+
output: finiteNumber(source.output, PI_STATIC_MODEL_DEFAULTS.cost.output),
|
|
71
|
+
cacheRead: finiteNumber(source.cacheRead, PI_STATIC_MODEL_DEFAULTS.cost.cacheRead),
|
|
72
|
+
cacheWrite: finiteNumber(source.cacheWrite, PI_STATIC_MODEL_DEFAULTS.cost.cacheWrite),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export interface CachedOpenAiCompatibleDiscovery<T> {
|
|
76
|
+
models: readonly T[];
|
|
77
|
+
/** A successful refresh produced this cache entry, even if it found no models. */
|
|
78
|
+
hasSnapshot: boolean;
|
|
79
|
+
stale: boolean;
|
|
80
|
+
}
|
|
81
|
+
export interface EligibleOpenAiCompatibleProvider {
|
|
82
|
+
id: string;
|
|
83
|
+
headers?: Record<string, string>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Session transcript text for one successfully registered configured provider. */
|
|
87
|
+
export function formatOpenAiCompatibleDiscoverySummary(
|
|
88
|
+
providerId: string,
|
|
89
|
+
discoveredModelCount: number,
|
|
90
|
+
): string {
|
|
91
|
+
log.debug("formatting discovery summary", { providerId, discoveredModelCount });
|
|
92
|
+
return `Providers: ${providerId} (${discoveredModelCount})`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Match pi's getModelsPath(): PI_CODING_AGENT_DIR, then ~/.pi/agent/models.json. */
|
|
96
|
+
export function resolveOpenAiCompatibleModelsPath(
|
|
97
|
+
agentDir?: string | null,
|
|
98
|
+
): string {
|
|
99
|
+
const configuredAgentDir =
|
|
100
|
+
typeof agentDir === "string" ? agentDir.trim() : "";
|
|
101
|
+
const envAgentDir =
|
|
102
|
+
typeof process.env.PI_CODING_AGENT_DIR === "string"
|
|
103
|
+
? process.env.PI_CODING_AGENT_DIR.trim()
|
|
104
|
+
: "";
|
|
105
|
+
return join(
|
|
106
|
+
configuredAgentDir || envAgentDir || join(homedir(), ".pi", "agent"),
|
|
107
|
+
"models.json",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Pi accepts a BOM, // comments, and trailing commas in models.json. */
|
|
112
|
+
function parsePiModelsJson(content: string): unknown {
|
|
113
|
+
const withoutBom =
|
|
114
|
+
content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
|
|
115
|
+
const withoutComments = withoutBom
|
|
116
|
+
.replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*/g, (match) =>
|
|
117
|
+
match[0] === '"' ? match : "",
|
|
118
|
+
)
|
|
119
|
+
.replace(
|
|
120
|
+
/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g,
|
|
121
|
+
(match, tail) => tail ?? (match[0] === '"' ? match : ""),
|
|
122
|
+
);
|
|
123
|
+
return JSON.parse(withoutComments);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type OpenAiCompatibleModelsConfigStatus =
|
|
127
|
+
"eligible-providers" | "no-eligible-providers" | "unreadable" | "malformed";
|
|
128
|
+
|
|
129
|
+
export interface EligibleOpenAiCompatibleProviderConfigsResult {
|
|
130
|
+
status: OpenAiCompatibleModelsConfigStatus;
|
|
131
|
+
providers: EligibleOpenAiCompatibleProvider[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Read the extension-owned opt-in bit and only schema-compatible raw headers.
|
|
136
|
+
* Statuses deliberately exclude paths, configuration values, and parser errors.
|
|
137
|
+
*/
|
|
138
|
+
export function findEligibleOpenAiCompatibleProviderConfigsResult(
|
|
139
|
+
options: { modelsPath?: string; agentDir?: string | null } = {},
|
|
140
|
+
): EligibleOpenAiCompatibleProviderConfigsResult {
|
|
141
|
+
let content: string;
|
|
142
|
+
try {
|
|
143
|
+
content = readFileSync(
|
|
144
|
+
options.modelsPath ?? resolveOpenAiCompatibleModelsPath(options.agentDir),
|
|
145
|
+
"utf8",
|
|
146
|
+
);
|
|
147
|
+
} catch {
|
|
148
|
+
return { status: "unreadable", providers: [] };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let parsed: unknown;
|
|
152
|
+
try {
|
|
153
|
+
parsed = parsePiModelsJson(content);
|
|
154
|
+
} catch {
|
|
155
|
+
return { status: "malformed", providers: [] };
|
|
156
|
+
}
|
|
157
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
158
|
+
return { status: "malformed", providers: [] };
|
|
159
|
+
const providers = (parsed as { providers?: unknown }).providers;
|
|
160
|
+
if (!providers || typeof providers !== "object" || Array.isArray(providers))
|
|
161
|
+
return { status: "no-eligible-providers", providers: [] };
|
|
162
|
+
const eligible = Object.entries(providers).flatMap(([id, config]) => {
|
|
163
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
164
|
+
return [];
|
|
165
|
+
const provider = config as Record<string, unknown>;
|
|
166
|
+
if (
|
|
167
|
+
provider.api !== "openai-completions" ||
|
|
168
|
+
provider.discoverModels !== true
|
|
169
|
+
)
|
|
170
|
+
return [];
|
|
171
|
+
const headers = provider.headers;
|
|
172
|
+
if (
|
|
173
|
+
headers !== undefined &&
|
|
174
|
+
(!headers ||
|
|
175
|
+
typeof headers !== "object" ||
|
|
176
|
+
Array.isArray(headers) ||
|
|
177
|
+
Object.values(headers).some((value) => typeof value !== "string"))
|
|
178
|
+
)
|
|
179
|
+
return [];
|
|
180
|
+
return [{ id, headers: headers as Record<string, string> | undefined }];
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
status:
|
|
184
|
+
eligible.length > 0 ? "eligible-providers" : "no-eligible-providers",
|
|
185
|
+
providers: eligible,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function findEligibleOpenAiCompatibleProviderConfigs(
|
|
190
|
+
options: { modelsPath?: string; agentDir?: string | null } = {},
|
|
191
|
+
): EligibleOpenAiCompatibleProvider[] {
|
|
192
|
+
return findEligibleOpenAiCompatibleProviderConfigsResult(options).providers;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function findEligibleOpenAiCompatibleProviders(
|
|
196
|
+
options: { modelsPath?: string; agentDir?: string | null } = {},
|
|
197
|
+
): string[] {
|
|
198
|
+
return findEligibleOpenAiCompatibleProviderConfigs(options).map(
|
|
199
|
+
(provider) => provider.id,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function piBashShellConfig(shell: string): PiShellConfig {
|
|
204
|
+
const normalized = shell.replace(/\//g, "\\").toLowerCase();
|
|
205
|
+
return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(normalized)
|
|
206
|
+
? { shell, args: ["-s"], commandTransport: "stdin" }
|
|
207
|
+
: { shell, args: ["-c"] };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Mirrors Pi's getShellConfig fallback order when its public resolver is unavailable. */
|
|
211
|
+
function getPiShellConfig(): PiShellConfig {
|
|
212
|
+
if (process.platform !== "win32") {
|
|
213
|
+
if (existsSync("/bin/bash")) return piBashShellConfig("/bin/bash");
|
|
214
|
+
try {
|
|
215
|
+
const result = spawnSync("which", ["bash"], {
|
|
216
|
+
encoding: "utf8",
|
|
217
|
+
timeout: 5_000,
|
|
218
|
+
});
|
|
219
|
+
if (result.status === 0 && result.stdout) {
|
|
220
|
+
const shell = String(result.stdout).trim().split(/\r?\n/)[0];
|
|
221
|
+
if (shell) return piBashShellConfig(shell);
|
|
222
|
+
}
|
|
223
|
+
} catch {}
|
|
224
|
+
return { shell: "sh", args: ["-c"] };
|
|
225
|
+
}
|
|
226
|
+
const paths = [
|
|
227
|
+
process.env.ProgramFiles &&
|
|
228
|
+
`${process.env.ProgramFiles}\\Git\\bin\\bash.exe`,
|
|
229
|
+
process.env["ProgramFiles(x86)"] &&
|
|
230
|
+
`${process.env["ProgramFiles(x86)"]}\\Git\\bin\\bash.exe`,
|
|
231
|
+
].filter((path): path is string => Boolean(path));
|
|
232
|
+
for (const shell of paths)
|
|
233
|
+
if (existsSync(shell)) return piBashShellConfig(shell);
|
|
234
|
+
try {
|
|
235
|
+
const result = spawnSync("where", ["bash.exe"], {
|
|
236
|
+
encoding: "utf8",
|
|
237
|
+
timeout: 5_000,
|
|
238
|
+
windowsHide: true,
|
|
239
|
+
});
|
|
240
|
+
const shell = result.stdout
|
|
241
|
+
? String(result.stdout).trim().split(/\r?\n/)[0]
|
|
242
|
+
: undefined;
|
|
243
|
+
if (result.status === 0 && shell && existsSync(shell))
|
|
244
|
+
return piBashShellConfig(shell);
|
|
245
|
+
} catch {}
|
|
246
|
+
throw new Error("No bash shell found");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
type PiShellConfig = {
|
|
250
|
+
shell: string;
|
|
251
|
+
args: readonly string[];
|
|
252
|
+
commandTransport?: "argv" | "stdin";
|
|
253
|
+
};
|
|
254
|
+
export interface OpenAiCompatibleHeaderResolutionOptions {
|
|
255
|
+
platform?: NodeJS.Platform;
|
|
256
|
+
env?: Record<string, string>;
|
|
257
|
+
getShellConfig?: () => PiShellConfig;
|
|
258
|
+
spawnSync?: (
|
|
259
|
+
shell: string,
|
|
260
|
+
args: readonly string[],
|
|
261
|
+
options: Record<string, unknown>,
|
|
262
|
+
) => { error?: { code?: string }; status?: number | null; stdout?: string };
|
|
263
|
+
execSync?: (command: string, options: Record<string, unknown>) => string;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function resolvePiConfigCommand(
|
|
267
|
+
command: string,
|
|
268
|
+
options: OpenAiCompatibleHeaderResolutionOptions,
|
|
269
|
+
): Promise<string | undefined> {
|
|
270
|
+
const executeDefault = () => {
|
|
271
|
+
try {
|
|
272
|
+
return (
|
|
273
|
+
(options.execSync ?? execSync)(command, {
|
|
274
|
+
encoding: "utf8",
|
|
275
|
+
timeout: 10_000,
|
|
276
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
277
|
+
})
|
|
278
|
+
.toString()
|
|
279
|
+
.trim() || undefined
|
|
280
|
+
);
|
|
281
|
+
} catch {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
if ((options.platform ?? process.platform) !== "win32")
|
|
286
|
+
return executeDefault();
|
|
287
|
+
try {
|
|
288
|
+
const shellConfig = options.getShellConfig
|
|
289
|
+
? options.getShellConfig()
|
|
290
|
+
: await import("@earendil-works/pi-coding-agent")
|
|
291
|
+
.then(({ getShellConfig }) => getShellConfig())
|
|
292
|
+
.catch(() => getPiShellConfig());
|
|
293
|
+
const { shell, args, commandTransport } = shellConfig;
|
|
294
|
+
const commandFromStdin = commandTransport === "stdin";
|
|
295
|
+
const result = (options.spawnSync ?? spawnSync)(
|
|
296
|
+
shell,
|
|
297
|
+
commandFromStdin ? args : [...args, command],
|
|
298
|
+
{
|
|
299
|
+
encoding: "utf8",
|
|
300
|
+
input: commandFromStdin ? command : undefined,
|
|
301
|
+
timeout: 10_000,
|
|
302
|
+
stdio: [commandFromStdin ? "pipe" : "ignore", "pipe", "ignore"],
|
|
303
|
+
shell: false,
|
|
304
|
+
windowsHide: true,
|
|
305
|
+
},
|
|
306
|
+
);
|
|
307
|
+
const error = result.error;
|
|
308
|
+
if (error && "code" in error && error.code === "ENOENT")
|
|
309
|
+
return executeDefault();
|
|
310
|
+
if (error || result.status !== 0) return undefined;
|
|
311
|
+
return result.stdout
|
|
312
|
+
? String(result.stdout).trim() || undefined
|
|
313
|
+
: undefined;
|
|
314
|
+
} catch {
|
|
315
|
+
return executeDefault();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function resolvePiConfigTemplate(
|
|
320
|
+
value: string,
|
|
321
|
+
env: Record<string, string> | undefined,
|
|
322
|
+
): string | undefined {
|
|
323
|
+
return value.replace(
|
|
324
|
+
/\$\$|\$!|\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g,
|
|
325
|
+
(match, braced, bare) => {
|
|
326
|
+
if (match === "$$") return "$";
|
|
327
|
+
if (match === "$!") return "!";
|
|
328
|
+
const envValue = env?.[braced ?? bare] || process.env[braced ?? bare];
|
|
329
|
+
if (envValue === undefined)
|
|
330
|
+
throw new Error("unresolved configured header");
|
|
331
|
+
return envValue;
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Match Pi's uncached literal, environment-template, escape, and command header resolution. */
|
|
337
|
+
export async function resolveStaticOpenAiCompatibleHeaders(
|
|
338
|
+
headers: Record<string, string> | undefined,
|
|
339
|
+
options: OpenAiCompatibleHeaderResolutionOptions = {},
|
|
340
|
+
): Promise<Record<string, string> | undefined> {
|
|
341
|
+
if (!headers) return undefined;
|
|
342
|
+
const resolved: Record<string, string> = {};
|
|
343
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
344
|
+
const result = value.startsWith("!")
|
|
345
|
+
? await resolvePiConfigCommand(value.slice(1), options)
|
|
346
|
+
: resolvePiConfigTemplate(value, options.env);
|
|
347
|
+
if (result === undefined) throw new Error("unresolved configured header");
|
|
348
|
+
resolved[name] = result;
|
|
349
|
+
}
|
|
350
|
+
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function buildOpenAiCompatibleModelsRequest(
|
|
354
|
+
connection: ResolvedOpenAiCompatibleConnection,
|
|
355
|
+
): OpenAiCompatibleModelsRequest {
|
|
356
|
+
if (!connection.baseUrl)
|
|
357
|
+
throw new Error("OpenAI-compatible provider has no resolved base URL");
|
|
358
|
+
let url: URL;
|
|
359
|
+
try {
|
|
360
|
+
url = new URL(connection.baseUrl);
|
|
361
|
+
} catch {
|
|
362
|
+
throw new Error(
|
|
363
|
+
"OpenAI-compatible provider has an invalid resolved base URL",
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
url.hash = "";
|
|
367
|
+
// The source URL can carry required routing query parameters. Capture it
|
|
368
|
+
// before deriving the discovery-only endpoint so models never stream to
|
|
369
|
+
// /v1/models. URL serialization also drops the fragment safely.
|
|
370
|
+
const streamBaseUrl = url.toString();
|
|
371
|
+
const basePath = url.pathname.replace(/\/+$/, "");
|
|
372
|
+
url.pathname = /\/v1$/i.test(basePath)
|
|
373
|
+
? `${basePath}/models`
|
|
374
|
+
: `${basePath}/v1/models`;
|
|
375
|
+
const modelsUrl = url.toString();
|
|
376
|
+
const headers = Object.fromEntries(
|
|
377
|
+
Object.entries(connection.headers ?? {}).filter(
|
|
378
|
+
(entry): entry is [string, string] => entry[1] !== null,
|
|
379
|
+
),
|
|
380
|
+
);
|
|
381
|
+
// Pi passes apiKey to pi-ai's OpenAI client. That client synthesizes a Bearer
|
|
382
|
+
// header unless configured headers already provide authorization. Reproduce
|
|
383
|
+
// that request behavior for this raw GET without inspecting provider config.
|
|
384
|
+
if (
|
|
385
|
+
connection.apiKey &&
|
|
386
|
+
!Object.keys(headers).some((name) => name.toLowerCase() === "authorization")
|
|
387
|
+
) {
|
|
388
|
+
headers.Authorization = `Bearer ${connection.apiKey}`;
|
|
389
|
+
}
|
|
390
|
+
return { url: modelsUrl, streamBaseUrl, headers };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Remove resolved credentials from diagnostics before they leave the request path. */
|
|
394
|
+
export function redactOpenAiCompatibleDiagnostic(
|
|
395
|
+
message: string,
|
|
396
|
+
connection: ResolvedOpenAiCompatibleConnection,
|
|
397
|
+
): string {
|
|
398
|
+
let redacted = message;
|
|
399
|
+
const headerValues = Object.values(connection.headers ?? {}).filter(
|
|
400
|
+
(value): value is string => typeof value === "string" && value.length > 0,
|
|
401
|
+
);
|
|
402
|
+
// Transports often quote just the credential portion of an authentication
|
|
403
|
+
// header in errors (for example, "Basic <credential>"). Redact those
|
|
404
|
+
// portions as well as the complete configured header value.
|
|
405
|
+
const headerTokens = headerValues.flatMap((value) =>
|
|
406
|
+
value.split(/[\s,;=]+/).filter(Boolean),
|
|
407
|
+
);
|
|
408
|
+
const secrets = [connection.apiKey, ...headerValues, ...headerTokens]
|
|
409
|
+
.filter(
|
|
410
|
+
(value): value is string => typeof value === "string" && value.length > 0,
|
|
411
|
+
)
|
|
412
|
+
.sort((a, b) => b.length - a.length);
|
|
413
|
+
for (const secret of secrets)
|
|
414
|
+
redacted = redacted.split(secret).join("[redacted]");
|
|
415
|
+
// Fetch errors commonly echo URLs. Query values and fragments can be just as
|
|
416
|
+
// sensitive as headers, so remove both wholesale rather than guessing keys.
|
|
417
|
+
return redacted.replace(/\b[a-z][a-z\d+.-]*:\/\/[^\s'"<>]+/gi, (rawUrl) => {
|
|
418
|
+
try {
|
|
419
|
+
const url = new URL(rawUrl);
|
|
420
|
+
if (url.username || url.password) {
|
|
421
|
+
url.username = "[redacted]";
|
|
422
|
+
url.password = "";
|
|
423
|
+
}
|
|
424
|
+
if (url.search) url.search = "?[redacted]";
|
|
425
|
+
if (url.hash) url.hash = "#[redacted]";
|
|
426
|
+
return url.toString();
|
|
427
|
+
} catch {
|
|
428
|
+
return rawUrl
|
|
429
|
+
.replace(/\/\/[^/?#@\s]+@/, "//[redacted]@")
|
|
430
|
+
.replace(/\?[^#\s]*/, "?[redacted]")
|
|
431
|
+
.replace(/#[^\s]*/, "#[redacted]");
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** A stable SHA-256 key that scopes cache entries without retaining credentials. */
|
|
437
|
+
export function openAiCompatibleConnectionFingerprint(
|
|
438
|
+
connection: ResolvedOpenAiCompatibleConnection,
|
|
439
|
+
): string {
|
|
440
|
+
const headers = Object.entries(connection.headers ?? {})
|
|
441
|
+
.filter(([, value]) => value !== null)
|
|
442
|
+
.map(([name, value]) => [name.toLowerCase(), value] as const)
|
|
443
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
444
|
+
return createHash("sha256")
|
|
445
|
+
.update(
|
|
446
|
+
JSON.stringify({
|
|
447
|
+
baseUrl: connection.baseUrl ?? "",
|
|
448
|
+
apiKey: connection.apiKey ?? "",
|
|
449
|
+
headers,
|
|
450
|
+
}),
|
|
451
|
+
)
|
|
452
|
+
.digest("hex");
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Expose IDs exactly as the configured proxy reported them. A source record's
|
|
457
|
+
* string name is presentation-only; no external catalog enriches this data.
|
|
458
|
+
*/
|
|
459
|
+
export function materializeOpenAiCompatibleModels(
|
|
460
|
+
records: readonly OpenAiCompatibleModelRecord[],
|
|
461
|
+
baseUrl: string,
|
|
462
|
+
providerId: string,
|
|
463
|
+
): Model<Api>[] {
|
|
464
|
+
const models: Model<Api>[] = [];
|
|
465
|
+
const seenIds = new Set<string>();
|
|
466
|
+
for (const record of records) {
|
|
467
|
+
if (typeof record.id !== "string" || seenIds.has(record.id)) continue;
|
|
468
|
+
seenIds.add(record.id);
|
|
469
|
+
models.push({
|
|
470
|
+
id: record.id,
|
|
471
|
+
name: typeof record.name === "string" ? record.name : record.id,
|
|
472
|
+
api: "openai-completions",
|
|
473
|
+
provider: providerId,
|
|
474
|
+
baseUrl,
|
|
475
|
+
reasoning: typeof record.reasoning === "boolean"
|
|
476
|
+
? record.reasoning
|
|
477
|
+
: PI_STATIC_MODEL_DEFAULTS.reasoning,
|
|
478
|
+
input: materializeInput(record.input),
|
|
479
|
+
cost: materializeCost(record.cost),
|
|
480
|
+
contextWindow: positiveFiniteNumber(record.contextWindow, PI_STATIC_MODEL_DEFAULTS.contextWindow),
|
|
481
|
+
maxTokens: positiveFiniteNumber(record.maxTokens, PI_STATIC_MODEL_DEFAULTS.maxTokens),
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return models;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** In-memory, bounded last-known-good cache. Failed results never replace it. */
|
|
488
|
+
export class OpenAiCompatibleDiscoveryCache<T> {
|
|
489
|
+
private readonly entries = new Map<
|
|
490
|
+
string,
|
|
491
|
+
{
|
|
492
|
+
result: readonly T[];
|
|
493
|
+
hasSnapshot: boolean;
|
|
494
|
+
updatedAt: number;
|
|
495
|
+
refreshing?: Promise<CachedOpenAiCompatibleDiscovery<T>>;
|
|
496
|
+
}
|
|
497
|
+
>();
|
|
498
|
+
constructor(
|
|
499
|
+
private readonly options: {
|
|
500
|
+
ttlMs?: number;
|
|
501
|
+
maxScopes?: number;
|
|
502
|
+
} = {},
|
|
503
|
+
) {}
|
|
504
|
+
|
|
505
|
+
clear(): void {
|
|
506
|
+
this.entries.clear();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async get(
|
|
510
|
+
refresh: () => Promise<readonly T[]>,
|
|
511
|
+
options: { forceRefresh?: boolean; nowMs?: number; cacheKey?: string } = {},
|
|
512
|
+
): Promise<CachedOpenAiCompatibleDiscovery<T>> {
|
|
513
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
514
|
+
const cacheKey = options.cacheKey ?? "default";
|
|
515
|
+
const ttlMs = this.options.ttlMs ?? OPENAI_COMPATIBLE_DISCOVERY_TTL_MS;
|
|
516
|
+
let entry = this.entries.get(cacheKey);
|
|
517
|
+
if (!entry) {
|
|
518
|
+
const maxScopes = this.options.maxScopes ?? 32;
|
|
519
|
+
if (this.entries.size >= maxScopes)
|
|
520
|
+
this.entries.delete(this.entries.keys().next().value!);
|
|
521
|
+
entry = { result: [], hasSnapshot: false, updatedAt: 0 };
|
|
522
|
+
this.entries.set(cacheKey, entry);
|
|
523
|
+
}
|
|
524
|
+
if (
|
|
525
|
+
!options.forceRefresh &&
|
|
526
|
+
entry.updatedAt > 0 &&
|
|
527
|
+
nowMs - entry.updatedAt <= ttlMs
|
|
528
|
+
) {
|
|
529
|
+
return {
|
|
530
|
+
models: entry.result,
|
|
531
|
+
hasSnapshot: entry.hasSnapshot,
|
|
532
|
+
stale: false,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
if (entry.refreshing) return entry.refreshing;
|
|
536
|
+
|
|
537
|
+
entry.refreshing = (async () => {
|
|
538
|
+
try {
|
|
539
|
+
const next = await refresh();
|
|
540
|
+
// The configured provider decides the catalog. The response byte limit
|
|
541
|
+
// protects memory without dropping valid model IDs.
|
|
542
|
+
entry!.result = next;
|
|
543
|
+
entry!.hasSnapshot = true;
|
|
544
|
+
entry!.updatedAt = nowMs;
|
|
545
|
+
return { models: entry!.result, hasSnapshot: true, stale: false };
|
|
546
|
+
} catch {
|
|
547
|
+
return {
|
|
548
|
+
models: entry!.result,
|
|
549
|
+
hasSnapshot: entry!.hasSnapshot,
|
|
550
|
+
stale: entry!.hasSnapshot,
|
|
551
|
+
};
|
|
552
|
+
} finally {
|
|
553
|
+
entry!.refreshing = undefined;
|
|
554
|
+
}
|
|
555
|
+
})();
|
|
556
|
+
return entry.refreshing;
|
|
557
|
+
}
|
|
558
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-orchestrator-config",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.9",
|
|
4
4
|
"description": "Orchestrator pattern for pi: specialist subagents, enforcement, code review loop, workflow commands",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"workspaces": [
|
|
10
10
|
"packages/pi-sidecar",
|
|
11
|
-
"packages/pi-vertex-claude"
|
|
11
|
+
"packages/pi-vertex-claude",
|
|
12
|
+
"extensions/pidiff/pidiff-ui"
|
|
12
13
|
],
|
|
13
14
|
"files": [
|
|
14
15
|
"extensions/acpx-provider/",
|
|
15
16
|
"extensions/cli-provider/",
|
|
16
17
|
"extensions/providers/",
|
|
18
|
+
"extensions/openai-compatible-discovery/",
|
|
17
19
|
"extensions/shared/",
|
|
18
20
|
"extensions/orchestrator/",
|
|
19
21
|
"extensions/coms/",
|
|
@@ -38,6 +40,7 @@
|
|
|
38
40
|
"extensions/pitasks/",
|
|
39
41
|
"scripts/pidash-server.ts",
|
|
40
42
|
"scripts/pidiff-server.ts",
|
|
43
|
+
"scripts/pidiff-git-ignore.ts",
|
|
41
44
|
"scripts/daemon-shared.ts",
|
|
42
45
|
"scripts/serve-ui.ts",
|
|
43
46
|
"scripts/pidash-discord.ts",
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
The orchestrator delegates — never implements directly.
|
|
16
16
|
⚠️ Pi does not enforce these restrictions — you SHOULD NOT violate them.
|
|
17
17
|
|
|
18
|
-
**Allowed direct actions:** read files, run `
|
|
18
|
+
**Allowed direct actions:** read files, run `mcpc`, ask questions, analyze, plan,
|
|
19
19
|
route to agents via `subagent`, execute slash commands AND all their internal operations.
|
|
20
20
|
|
|
21
|
-
Outside slash commands, never use edit, write, or bash directly (except `
|
|
21
|
+
Outside slash commands, never use edit, write, or bash directly (except `mcpc`) — delegate to specialists.
|
|
22
22
|
|
|
23
23
|
**Always delegate via `subagent`:**
|
|
24
24
|
|
package/rules/15-mcpc.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# MCP client (mcpc)
|
|
2
|
+
|
|
3
|
+
Use the `mcpc` command for all MCP server interactions.
|
|
4
|
+
If a task needs a tool outside current capabilities, check `mcpc` for available tools.
|
|
5
|
+
Orchestrator discovers tools via `mcpc grep` / `mcpc @<session> tools-list` and delegates execution to agents.
|
|
6
|
+
|
|
7
|
+
Config file (user-owned): `~/.pi/pi-config/mcp.json`.
|
|
8
|
+
Pi runs `mcpc connect ~/.pi/pi-config/mcp.json --stdio` on process start when that file exists.
|
|
9
|
+
After you edit `mcp.json`, run `/mcpc connect` (or restart pi). Already-active servers stay as `mcpc` reports them.
|
|
10
|
+
|
|
11
|
+
## mcpc commands
|
|
12
|
+
|
|
13
|
+
| Command | Purpose |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `mcpc` / `mcpc --json` | Active sessions and OAuth profiles |
|
|
16
|
+
| `mcpc grep "<query>"` | Search tools across sessions |
|
|
17
|
+
| `mcpc @<session>` | Server info and tools overview |
|
|
18
|
+
| `mcpc @<session> tools-list` | List tools |
|
|
19
|
+
| `mcpc @<session> tools-get <name>` | Tool schema |
|
|
20
|
+
| `mcpc @<session> tools-call <name> …` | Call a tool (`arg:=value` or JSON) |
|
|
21
|
+
| `mcpc @<session> restart` | Restart that session bridge |
|
|
22
|
+
|
|
23
|
+
`--stdio` is required on connect. Do not omit it.
|
|
24
|
+
|
|
25
|
+
## Workflow
|
|
26
|
+
|
|
27
|
+
1. **Search** → `mcpc grep "list projects"` (or `mcpc @<session> tools-list`)
|
|
28
|
+
2. **Inspect** → `mcpc @<session> tools-get <tool>`
|
|
29
|
+
3. **Call** → `mcpc @<session> tools-call <tool> '{"param":"v"}'`
|
|
30
|
+
|
|
31
|
+
## Troubleshooting
|
|
32
|
+
|
|
33
|
+
- **Connect failed at pi start:** session still works; check `~/.pi/logs/orchestrator/` and `/mcpc connect`
|
|
34
|
+
- **Missing binary:** `npm install -g @apify/mcpc`
|
|
35
|
+
- **No config file:** create `~/.pi/pi-config/mcp.json` (standard `mcpServers` JSON)
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
# Code Review Loop
|
|
1
|
+
# Code Review Loop
|
|
2
|
+
|
|
3
|
+
{{IF:review_loop_enforcement}}
|
|
4
|
+
|
|
5
|
+
## Mandatory review loop
|
|
2
6
|
|
|
3
7
|
After ANY code change, send to ALL 6 agents (5 reviewers + test-automator) IN PARALLEL. **Never skip the first review.**
|
|
4
8
|
|
|
@@ -259,3 +263,9 @@ stop looping Stage 2.
|
|
|
259
263
|
|
|
260
264
|
Don't polish code that doesn't meet spec — it wastes work.
|
|
261
265
|
Parallel mode (all 6 agents at once) remains default for manual reviews.
|
|
266
|
+
{{/IF}}
|
|
267
|
+
|
|
268
|
+
{{IFNOT:review_loop_enforcement}}
|
|
269
|
+
Automatic review-agent and test-agent dispatch is disabled. Manual code reviews and test runs are optional.
|
|
270
|
+
No review state, cycle, or result is required before git commit.
|
|
271
|
+
{{/IFNOT}}
|