@alexeiled/pi-fusion 0.2.2 → 0.2.3
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/docs/user-guide.md +4 -3
- package/package.json +1 -1
- package/src/claude-aliases.ts +255 -0
- package/src/config.ts +6 -2
package/docs/user-guide.md
CHANGED
|
@@ -110,7 +110,8 @@ Panel member:
|
|
|
110
110
|
- `id`: stable machine name
|
|
111
111
|
- `label`: human-readable report label
|
|
112
112
|
- `agent`: subagent name
|
|
113
|
-
- `model`: optional model override; often the main source of panel diversity
|
|
113
|
+
- `model`: optional model override; often the main source of panel diversity. Supports normal Pi model ids, and if `pi-claude-alias` is configured, Claude alias shorthand like `claude-work/opus-4.8`
|
|
114
|
+
- Claude alias handles must be unique across global and project alias files; duplicate handles are rejected.
|
|
114
115
|
- `thinking`: optional `off`, `minimal`, `low`, `medium`, `high`, or `xhigh`
|
|
115
116
|
- `role`: optional perspective hint layered on top of the model
|
|
116
117
|
|
|
@@ -162,7 +163,7 @@ Deliberate review:
|
|
|
162
163
|
"id": "architect",
|
|
163
164
|
"label": "Architect",
|
|
164
165
|
"agent": "pi-fusion.fusion-panelist",
|
|
165
|
-
"model": "
|
|
166
|
+
"model": "claude-work/sonnet-4.6",
|
|
166
167
|
"thinking": "high",
|
|
167
168
|
"role": "architecture and failure modes"
|
|
168
169
|
},
|
|
@@ -177,7 +178,7 @@ Deliberate review:
|
|
|
177
178
|
],
|
|
178
179
|
"judge": {
|
|
179
180
|
"agent": "pi-fusion.fusion-judge",
|
|
180
|
-
"model": "
|
|
181
|
+
"model": "claude-work/sonnet-4.6",
|
|
181
182
|
"thinking": "high"
|
|
182
183
|
},
|
|
183
184
|
"concurrency": 2,
|
package/package.json
CHANGED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { FusionConfig } from "./types.js";
|
|
5
|
+
import { FusionConfigError } from "./errors.js";
|
|
6
|
+
import { isNodeErrorCode, isNonEmptyString, isRecord } from "./utils.js";
|
|
7
|
+
import type { FusionConfigLoadContext } from "./config.js";
|
|
8
|
+
|
|
9
|
+
const CLAUDE_ALIAS_CONFIG_FILE = "claude-alias.json";
|
|
10
|
+
|
|
11
|
+
interface ClaudeAliasDefinition {
|
|
12
|
+
slug: string;
|
|
13
|
+
providerId: string;
|
|
14
|
+
handle: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface AliasFileDeps {
|
|
18
|
+
readTextFile?: (path: string) => Promise<string>;
|
|
19
|
+
agentDir?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface RawAliasConfig {
|
|
23
|
+
aliases?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RawAliasEntry {
|
|
27
|
+
slug?: unknown;
|
|
28
|
+
handle?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function applyClaudeAliasShorthand(
|
|
32
|
+
config: FusionConfig,
|
|
33
|
+
ctx: FusionConfigLoadContext,
|
|
34
|
+
deps: AliasFileDeps = {},
|
|
35
|
+
): Promise<FusionConfig> {
|
|
36
|
+
const aliases = await loadClaudeAliases(ctx, deps);
|
|
37
|
+
if (aliases.length === 0) return config;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...config,
|
|
41
|
+
profiles: Object.fromEntries(
|
|
42
|
+
Object.entries(config.profiles).map(([name, profile]) => [
|
|
43
|
+
name,
|
|
44
|
+
{
|
|
45
|
+
...profile,
|
|
46
|
+
panel: profile.panel.map((member) => ({
|
|
47
|
+
...member,
|
|
48
|
+
...(member.model
|
|
49
|
+
? { model: resolveClaudeAliasModelSpec(member.model, aliases) }
|
|
50
|
+
: {}),
|
|
51
|
+
})),
|
|
52
|
+
judge: {
|
|
53
|
+
...profile.judge,
|
|
54
|
+
...(profile.judge.model
|
|
55
|
+
? { model: resolveClaudeAliasModelSpec(profile.judge.model, aliases) }
|
|
56
|
+
: {}),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
]),
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resolveClaudeAliasModelSpec(
|
|
65
|
+
model: string,
|
|
66
|
+
aliases: readonly ClaudeAliasDefinition[],
|
|
67
|
+
): string {
|
|
68
|
+
const trimmed = model.trim();
|
|
69
|
+
const slashIndex = trimmed.indexOf("/");
|
|
70
|
+
if (slashIndex <= 0 || slashIndex === trimmed.length - 1) return trimmed;
|
|
71
|
+
|
|
72
|
+
const handle = trimmed.slice(0, slashIndex).trim().toLowerCase();
|
|
73
|
+
const modelRef = trimmed.slice(slashIndex + 1).trim();
|
|
74
|
+
const alias = aliases.find((item) => item.handle === handle);
|
|
75
|
+
if (!alias) return trimmed;
|
|
76
|
+
|
|
77
|
+
return `${alias.providerId}/${normalizeAnthropicModelRef(modelRef)}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function normalizeAnthropicModelRef(modelRef: string): string {
|
|
81
|
+
const normalized = modelRef
|
|
82
|
+
.trim()
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/[._\s]+/g, "-")
|
|
85
|
+
.replace(/-+/g, "-")
|
|
86
|
+
.replace(/^-+|-+$/g, "");
|
|
87
|
+
|
|
88
|
+
if (!normalized) return modelRef.trim();
|
|
89
|
+
return normalized.startsWith("claude-") ? normalized : `claude-${normalized}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function loadClaudeAliases(
|
|
93
|
+
ctx: FusionConfigLoadContext,
|
|
94
|
+
deps: AliasFileDeps,
|
|
95
|
+
): Promise<ClaudeAliasDefinition[]> {
|
|
96
|
+
const readTextFile = deps.readTextFile ?? readUtf8File;
|
|
97
|
+
const global = await readOptionalAliasFile(
|
|
98
|
+
getGlobalClaudeAliasConfigPath(deps.agentDir),
|
|
99
|
+
readTextFile,
|
|
100
|
+
);
|
|
101
|
+
const project = ctx.isProjectTrusted()
|
|
102
|
+
? await readOptionalAliasFile(
|
|
103
|
+
getProjectClaudeAliasConfigPath(ctx.cwd),
|
|
104
|
+
readTextFile,
|
|
105
|
+
)
|
|
106
|
+
: undefined;
|
|
107
|
+
|
|
108
|
+
const merged = new Map<string, ClaudeAliasDefinition>();
|
|
109
|
+
for (const alias of global ?? []) {
|
|
110
|
+
merged.set(alias.slug, alias);
|
|
111
|
+
}
|
|
112
|
+
for (const alias of project ?? []) {
|
|
113
|
+
merged.set(alias.slug, alias);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const aliases = [...merged.values()];
|
|
117
|
+
validateUniqueHandles(aliases);
|
|
118
|
+
return aliases;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function readOptionalAliasFile(
|
|
122
|
+
path: string,
|
|
123
|
+
readTextFile: (path: string) => Promise<string>,
|
|
124
|
+
): Promise<ClaudeAliasDefinition[] | undefined> {
|
|
125
|
+
let raw: string;
|
|
126
|
+
try {
|
|
127
|
+
raw = await readTextFile(path);
|
|
128
|
+
} catch (error: unknown) {
|
|
129
|
+
if (isNodeErrorCode(error, "ENOENT")) return undefined;
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
throw new FusionConfigError(
|
|
132
|
+
`Could not read Claude alias config at ${path}: ${message}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return parseAliasFile(raw, path);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseAliasFile(raw: string, source: string): ClaudeAliasDefinition[] {
|
|
140
|
+
let value: unknown;
|
|
141
|
+
try {
|
|
142
|
+
value = JSON.parse(raw);
|
|
143
|
+
} catch (error: unknown) {
|
|
144
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
145
|
+
throw new FusionConfigError(
|
|
146
|
+
`Invalid JSON in Claude alias config at ${source}: ${message}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!isRecord(value) || !Array.isArray((value as RawAliasConfig).aliases)) {
|
|
151
|
+
throw new FusionConfigError(
|
|
152
|
+
`Invalid Claude alias config at ${source}. Expected aliases array.`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const aliasValues = (value as { aliases: unknown[] }).aliases;
|
|
157
|
+
const aliases: ClaudeAliasDefinition[] = [];
|
|
158
|
+
for (const [index, entry] of aliasValues.entries()) {
|
|
159
|
+
const parsed = parseAliasEntry(entry);
|
|
160
|
+
if (!parsed) {
|
|
161
|
+
throw new FusionConfigError(
|
|
162
|
+
`Invalid Claude alias entry at ${source} aliases[${index}].`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
aliases.push(parsed);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return dedupeAliases(aliases, source);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseAliasEntry(value: unknown): ClaudeAliasDefinition | undefined {
|
|
172
|
+
if (!isRecord(value)) return undefined;
|
|
173
|
+
const entry = value as RawAliasEntry;
|
|
174
|
+
|
|
175
|
+
const slug = normalizeSlug(entry.slug);
|
|
176
|
+
if (!slug) return undefined;
|
|
177
|
+
|
|
178
|
+
const handle = normalizeHandle(entry.handle) ?? `claude-${slug}`;
|
|
179
|
+
if (!handle) return undefined;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
slug,
|
|
183
|
+
providerId: `anthropic-${slug}`,
|
|
184
|
+
handle,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function dedupeAliases(
|
|
189
|
+
aliases: readonly ClaudeAliasDefinition[],
|
|
190
|
+
source: string,
|
|
191
|
+
): ClaudeAliasDefinition[] {
|
|
192
|
+
const byHandle = new Set<string>();
|
|
193
|
+
const deduped: ClaudeAliasDefinition[] = [];
|
|
194
|
+
|
|
195
|
+
for (const alias of aliases) {
|
|
196
|
+
if (byHandle.has(alias.handle)) {
|
|
197
|
+
throw new FusionConfigError(
|
|
198
|
+
`Duplicate Claude alias handle "${alias.handle}" in ${source}.`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
byHandle.add(alias.handle);
|
|
202
|
+
deduped.push(alias);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return deduped;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function validateUniqueHandles(
|
|
209
|
+
aliases: readonly ClaudeAliasDefinition[],
|
|
210
|
+
): void {
|
|
211
|
+
const seen = new Map<string, string>();
|
|
212
|
+
|
|
213
|
+
for (const alias of aliases) {
|
|
214
|
+
const existingSlug = seen.get(alias.handle);
|
|
215
|
+
if (existingSlug && existingSlug !== alias.slug) {
|
|
216
|
+
throw new FusionConfigError(
|
|
217
|
+
`Duplicate Claude alias handle "${alias.handle}" across merged config. Use a unique handle for each alias.`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
seen.set(alias.handle, alias.slug);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function normalizeSlug(value: unknown): string | undefined {
|
|
225
|
+
if (!isNonEmptyString(value)) return undefined;
|
|
226
|
+
const slug = value
|
|
227
|
+
.trim()
|
|
228
|
+
.toLowerCase()
|
|
229
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
230
|
+
.replace(/^-+|-+$/g, "");
|
|
231
|
+
return slug || undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeHandle(value: unknown): string | undefined {
|
|
235
|
+
if (value === undefined) return undefined;
|
|
236
|
+
if (!isNonEmptyString(value)) return undefined;
|
|
237
|
+
const handle = value
|
|
238
|
+
.trim()
|
|
239
|
+
.toLowerCase()
|
|
240
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
241
|
+
.replace(/^-+|-+$/g, "");
|
|
242
|
+
return handle || undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getGlobalClaudeAliasConfigPath(agentDir = getAgentDir()): string {
|
|
246
|
+
return join(agentDir, CLAUDE_ALIAS_CONFIG_FILE);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function getProjectClaudeAliasConfigPath(cwd: string): string {
|
|
250
|
+
return join(cwd, CONFIG_DIR_NAME, CLAUDE_ALIAS_CONFIG_FILE);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function readUtf8File(path: string): Promise<string> {
|
|
254
|
+
return readFile(path, "utf8");
|
|
255
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { applyClaudeAliasShorthand } from "./claude-aliases.js";
|
|
4
5
|
import { FusionConfigError } from "./errors.js";
|
|
5
6
|
import {
|
|
6
7
|
THINKING_LEVELS,
|
|
@@ -104,12 +105,15 @@ export async function loadFusionConfig(
|
|
|
104
105
|
if (ctx.isProjectTrusted()) {
|
|
105
106
|
const projectPath = getProjectFusionConfigPath(ctx.cwd);
|
|
106
107
|
const projectConfig = await readOptionalConfig(projectPath, readTextFile);
|
|
107
|
-
if (projectConfig)
|
|
108
|
+
if (projectConfig) {
|
|
109
|
+
return applyClaudeAliasShorthand(projectConfig, ctx, deps);
|
|
110
|
+
}
|
|
108
111
|
}
|
|
109
112
|
|
|
110
113
|
const globalPath = getGlobalFusionConfigPath(deps.agentDir);
|
|
111
114
|
const globalConfig = await readOptionalConfig(globalPath, readTextFile);
|
|
112
|
-
|
|
115
|
+
const config = globalConfig ?? createDefaultFusionConfig();
|
|
116
|
+
return applyClaudeAliasShorthand(config, ctx, deps);
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
export function resolveProfile(
|