@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.
@@ -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": "anthropic/claude-sonnet-4",
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": "anthropic/claude-sonnet-4",
181
+ "model": "claude-work/sonnet-4.6",
181
182
  "thinking": "high"
182
183
  },
183
184
  "concurrency": 2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexeiled/pi-fusion",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Stronger answers for hard Pi questions via a parallel model panel + judge, built on pi-subagents",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -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) return 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
- return globalConfig ?? createDefaultFusionConfig();
115
+ const config = globalConfig ?? createDefaultFusionConfig();
116
+ return applyClaudeAliasShorthand(config, ctx, deps);
113
117
  }
114
118
 
115
119
  export function resolveProfile(