pi-model-profiles 0.3.3 → 0.3.4
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/CHANGELOG.md +6 -0
- package/package.json +3 -3
- package/src/agent-writer.ts +28 -6
- package/src/command-handler.ts +137 -137
- package/src/errors.ts +18 -18
- package/src/frontmatter-parser.ts +250 -249
- package/src/index.ts +36 -36
- package/src/profile-fields.ts +93 -83
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.4] - 2026-06-16
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Resolved symlinks before writing agent markdown to avoid overwriting the symlink target's parent instead of the linked file.
|
|
14
|
+
- Used `parseCompleteNumericScalar` for frontmatter number parsing to handle trailing non-numeric characters correctly.
|
|
15
|
+
|
|
10
16
|
## [0.3.3] - 2026-06-01
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-model-profiles",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Pi extension for saving, importing, and applying agent model frontmatter profiles.",
|
|
6
6
|
"type": "module",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
]
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0",
|
|
61
|
-
"@earendil-works/pi-tui": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0"
|
|
60
|
+
"@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0",
|
|
61
|
+
"@earendil-works/pi-tui": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0"
|
|
62
62
|
},
|
|
63
63
|
"publishConfig": {
|
|
64
64
|
"access": "public"
|
package/src/agent-writer.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { AGENTS_DIR } from "./constants.js";
|
|
@@ -33,6 +33,12 @@ interface SessionManagerLike {
|
|
|
33
33
|
getEntries(): readonly unknown[];
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
interface AgentDirectoryEntry {
|
|
37
|
+
name: string;
|
|
38
|
+
isFile(): boolean;
|
|
39
|
+
isSymbolicLink(): boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
type AgentScope = "user" | "project" | "both";
|
|
37
43
|
|
|
38
44
|
export interface AgentSelectionOptions {
|
|
@@ -130,6 +136,22 @@ function isDirectory(path: string): boolean {
|
|
|
130
136
|
}
|
|
131
137
|
}
|
|
132
138
|
|
|
139
|
+
function isMarkdownAgentEntry(entry: AgentDirectoryEntry): boolean {
|
|
140
|
+
return entry.name.endsWith(".md") && (entry.isFile() || entry.isSymbolicLink());
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolveAgentWritePath(filePath: string): string {
|
|
144
|
+
try {
|
|
145
|
+
return lstatSync(filePath).isSymbolicLink() ? realpathSync(filePath) : filePath;
|
|
146
|
+
} catch {
|
|
147
|
+
return filePath;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function writeAgentMarkdown(filePath: string, markdown: string): void {
|
|
152
|
+
writeFileAtomic(resolveAgentWritePath(filePath), markdown);
|
|
153
|
+
}
|
|
154
|
+
|
|
133
155
|
function findNearestProjectAgentDirs(cwd: string): string[] {
|
|
134
156
|
let currentDir = resolve(cwd);
|
|
135
157
|
|
|
@@ -187,15 +209,15 @@ export function scanAgentFiles(options: string | AgentSelectionOptions = AGENTS_
|
|
|
187
209
|
const agentsByName = new Map<string, AgentFileRecord>();
|
|
188
210
|
|
|
189
211
|
for (const agentsDir of sourceDirs) {
|
|
190
|
-
let entries:
|
|
212
|
+
let entries: AgentDirectoryEntry[];
|
|
191
213
|
try {
|
|
192
|
-
entries = readdirSync(agentsDir, { withFileTypes: true }) as
|
|
214
|
+
entries = readdirSync(agentsDir, { withFileTypes: true }) as AgentDirectoryEntry[];
|
|
193
215
|
} catch {
|
|
194
216
|
throw new ModelProfilesError(`Unable to read agents directory '${agentsDir}'.`, "AGENTS_DIR_UNAVAILABLE");
|
|
195
217
|
}
|
|
196
218
|
|
|
197
219
|
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
198
|
-
if (!
|
|
220
|
+
if (!isMarkdownAgentEntry(entry)) {
|
|
199
221
|
continue;
|
|
200
222
|
}
|
|
201
223
|
|
|
@@ -247,7 +269,7 @@ export function applyProfileToAgentRecord(agent: AgentFileRecord, fields: Profil
|
|
|
247
269
|
const normalizedFields = normalizeProfileFields(fields);
|
|
248
270
|
const markdown = readFileSync(agent.path, "utf-8");
|
|
249
271
|
const updatedMarkdown = updateMarkdownProfileFields(markdown, normalizedFields);
|
|
250
|
-
|
|
272
|
+
writeAgentMarkdown(agent.path, updatedMarkdown);
|
|
251
273
|
return {
|
|
252
274
|
updatedPath: agent.path,
|
|
253
275
|
fileName: agent.fileName,
|
|
@@ -314,7 +336,7 @@ export function applySavedProfile(profile: SavedProfile, options: string | Agent
|
|
|
314
336
|
}
|
|
315
337
|
|
|
316
338
|
for (const pending of pendingWrites) {
|
|
317
|
-
|
|
339
|
+
writeAgentMarkdown(pending.target.path, pending.updatedMarkdown);
|
|
318
340
|
}
|
|
319
341
|
|
|
320
342
|
return {
|
package/src/command-handler.ts
CHANGED
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
INITIAL_PROFILE_NAME,
|
|
5
|
-
PROFILE_NAME_SUFFIX,
|
|
6
|
-
PROFILE_STORE_PATH,
|
|
7
|
-
} from "./constants.js";
|
|
8
|
-
import { applySavedProfile, captureAgentSnapshots, detectActiveAgentName } from "./agent-writer.js";
|
|
9
|
-
import { toErrorMessage } from "./errors.js";
|
|
10
|
-
import { loadAndPrepareProfiles } from "./import-service.js";
|
|
11
|
-
import { updateProfileAndReturn } from "./profile-update-service.js";
|
|
12
|
-
import {
|
|
13
|
-
appendProfile,
|
|
14
|
-
createProfile,
|
|
15
|
-
findProfileById,
|
|
16
|
-
renameStoredProfile,
|
|
17
|
-
resolveUniqueProfileName,
|
|
18
|
-
saveProfilesFile,
|
|
19
|
-
} from "./profile-store.js";
|
|
20
|
-
import { removeProfileAndUpdate } from "./profile-removal-service.js";
|
|
21
|
-
import { openProfilesModal, type ProfileModalResult } from "./profile-modal.js";
|
|
22
|
-
import type { AppliedProfileOutcome, ProfilesFile } from "./types.js";
|
|
23
|
-
|
|
24
|
-
function buildCurrentProfileName(activeAgentName: string | null, data: ProfilesFile): string {
|
|
25
|
-
const baseName = activeAgentName ? `${activeAgentName} ${PROFILE_NAME_SUFFIX}` : INITIAL_PROFILE_NAME;
|
|
26
|
-
return resolveUniqueProfileName(baseName, data.profiles);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function notifyWarnings(ctx: ExtensionCommandContext, warnings: readonly string[]): void {
|
|
30
|
-
if (warnings.length === 0) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
ctx.ui.notify(warnings.join(" "), "warning");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function summarizeApplyOutcome(outcome: AppliedProfileOutcome): string {
|
|
37
|
-
const appliedCount = outcome.appliedAgents.length;
|
|
38
|
-
const missingCount = outcome.missingAgents.length;
|
|
39
|
-
const appliedLabel = `${appliedCount} agent file${appliedCount === 1 ? "" : "s"}`;
|
|
40
|
-
if (missingCount === 0) {
|
|
41
|
-
return appliedLabel;
|
|
42
|
-
}
|
|
43
|
-
return `${appliedLabel}; ${missingCount} missing`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function reloadAfterApply(ctx: ExtensionCommandContext, outcome: AppliedProfileOutcome): Promise<void> {
|
|
47
|
-
const summary = summarizeApplyOutcome(outcome);
|
|
48
|
-
ctx.ui.notify(`Profile '${outcome.profileName}' applied across ${summary}. Reloading…`, "info");
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
await ctx.reload();
|
|
52
|
-
} catch (error) {
|
|
53
|
-
ctx.ui.notify(
|
|
54
|
-
`Profile '${outcome.profileName}' applied across ${summary}, but automatic reload failed: ${toErrorMessage(error)}. Run /reload.`,
|
|
55
|
-
"error",
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export async function handleModelProfilesCommand(ctx: ExtensionCommandContext): Promise<void> {
|
|
61
|
-
const agentOptions = { cwd: ctx.cwd, scope: "both" as const };
|
|
62
|
-
const prepared = loadAndPrepareProfiles(PROFILE_STORE_PATH, agentOptions);
|
|
63
|
-
notifyWarnings(ctx, prepared.warnings);
|
|
64
|
-
|
|
65
|
-
let data = prepared.data;
|
|
66
|
-
const activeAgentName = detectActiveAgentName(ctx.sessionManager, ctx.getSystemPrompt());
|
|
67
|
-
|
|
68
|
-
const result: ProfileModalResult = await openProfilesModal(ctx, data, activeAgentName, {
|
|
69
|
-
renameProfile: async (profileId, nextName) => {
|
|
70
|
-
data = renameStoredProfile(data, profileId, nextName);
|
|
71
|
-
saveProfilesFile(data, PROFILE_STORE_PATH);
|
|
72
|
-
return {
|
|
73
|
-
data,
|
|
74
|
-
message: `Renamed saved snapshot to '${nextName.trim()}'.`,
|
|
75
|
-
selectedProfileId: profileId,
|
|
76
|
-
};
|
|
77
|
-
},
|
|
78
|
-
addCurrentProfile: async () => {
|
|
79
|
-
const snapshot = captureAgentSnapshots(agentOptions);
|
|
80
|
-
notifyWarnings(ctx, snapshot.warnings);
|
|
81
|
-
const profile = createProfile(buildCurrentProfileName(activeAgentName, data), snapshot.agents);
|
|
82
|
-
data = appendProfile(data, profile);
|
|
83
|
-
saveProfilesFile(data, PROFILE_STORE_PATH);
|
|
84
|
-
return {
|
|
85
|
-
data,
|
|
86
|
-
message: `Saved current agents snapshot (${snapshot.agents.length} agents).`,
|
|
87
|
-
selectedProfileId: profile.id,
|
|
88
|
-
};
|
|
89
|
-
},
|
|
90
|
-
applyProfile: async (profileId) => {
|
|
91
|
-
const profile = findProfileById(data, profileId);
|
|
92
|
-
if (!profile) {
|
|
93
|
-
throw new Error(`Saved profile '${profileId}' was not found.`);
|
|
94
|
-
}
|
|
95
|
-
const applied = applySavedProfile(profile, agentOptions);
|
|
96
|
-
notifyWarnings(ctx, applied.warnings);
|
|
97
|
-
return {
|
|
98
|
-
...applied,
|
|
99
|
-
profileName: profile.name,
|
|
100
|
-
};
|
|
101
|
-
},
|
|
102
|
-
removeProfile: async (profileId) => {
|
|
103
|
-
const profile = findProfileById(data, profileId);
|
|
104
|
-
if (!profile) {
|
|
105
|
-
throw new Error(`Saved profile '${profileId}' was not found.`);
|
|
106
|
-
}
|
|
107
|
-
const removal = removeProfileAndUpdate(data, profileId);
|
|
108
|
-
data = removal.data;
|
|
109
|
-
saveProfilesFile(data, PROFILE_STORE_PATH);
|
|
110
|
-
return {
|
|
111
|
-
data,
|
|
112
|
-
message: `Removed saved snapshot '${removal.result.removedProfileName}' (${removal.result.remainingCount} snapshots remaining).`,
|
|
113
|
-
selectedProfileId: data.profiles[0]?.id,
|
|
114
|
-
};
|
|
115
|
-
},
|
|
116
|
-
updateProfile: async (profileId) => {
|
|
117
|
-
const profile = findProfileById(data, profileId);
|
|
118
|
-
if (!profile) {
|
|
119
|
-
throw new Error(`Saved profile '${profileId}' was not found.`);
|
|
120
|
-
}
|
|
121
|
-
const update = updateProfileAndReturn(data, profileId, agentOptions);
|
|
122
|
-
notifyWarnings(ctx, update.result.warnings);
|
|
123
|
-
data = update.data;
|
|
124
|
-
saveProfilesFile(data, PROFILE_STORE_PATH);
|
|
125
|
-
return {
|
|
126
|
-
data,
|
|
127
|
-
message: `Updated '${profile.name}' with current agent state (${update.result.updatedAgents} agents).`,
|
|
128
|
-
selectedProfileId: profileId,
|
|
129
|
-
};
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
if (result.type === "applied") {
|
|
134
|
-
await reloadAfterApply(ctx, result.outcome);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
INITIAL_PROFILE_NAME,
|
|
5
|
+
PROFILE_NAME_SUFFIX,
|
|
6
|
+
PROFILE_STORE_PATH,
|
|
7
|
+
} from "./constants.js";
|
|
8
|
+
import { applySavedProfile, captureAgentSnapshots, detectActiveAgentName } from "./agent-writer.js";
|
|
9
|
+
import { toErrorMessage } from "./errors.js";
|
|
10
|
+
import { loadAndPrepareProfiles } from "./import-service.js";
|
|
11
|
+
import { updateProfileAndReturn } from "./profile-update-service.js";
|
|
12
|
+
import {
|
|
13
|
+
appendProfile,
|
|
14
|
+
createProfile,
|
|
15
|
+
findProfileById,
|
|
16
|
+
renameStoredProfile,
|
|
17
|
+
resolveUniqueProfileName,
|
|
18
|
+
saveProfilesFile,
|
|
19
|
+
} from "./profile-store.js";
|
|
20
|
+
import { removeProfileAndUpdate } from "./profile-removal-service.js";
|
|
21
|
+
import { openProfilesModal, type ProfileModalResult } from "./profile-modal.js";
|
|
22
|
+
import type { AppliedProfileOutcome, ProfilesFile } from "./types.js";
|
|
23
|
+
|
|
24
|
+
function buildCurrentProfileName(activeAgentName: string | null, data: ProfilesFile): string {
|
|
25
|
+
const baseName = activeAgentName ? `${activeAgentName} ${PROFILE_NAME_SUFFIX}` : INITIAL_PROFILE_NAME;
|
|
26
|
+
return resolveUniqueProfileName(baseName, data.profiles);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function notifyWarnings(ctx: ExtensionCommandContext, warnings: readonly string[]): void {
|
|
30
|
+
if (warnings.length === 0) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
ctx.ui.notify(warnings.join(" "), "warning");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function summarizeApplyOutcome(outcome: AppliedProfileOutcome): string {
|
|
37
|
+
const appliedCount = outcome.appliedAgents.length;
|
|
38
|
+
const missingCount = outcome.missingAgents.length;
|
|
39
|
+
const appliedLabel = `${appliedCount} agent file${appliedCount === 1 ? "" : "s"}`;
|
|
40
|
+
if (missingCount === 0) {
|
|
41
|
+
return appliedLabel;
|
|
42
|
+
}
|
|
43
|
+
return `${appliedLabel}; ${missingCount} missing`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function reloadAfterApply(ctx: ExtensionCommandContext, outcome: AppliedProfileOutcome): Promise<void> {
|
|
47
|
+
const summary = summarizeApplyOutcome(outcome);
|
|
48
|
+
ctx.ui.notify(`Profile '${outcome.profileName}' applied across ${summary}. Reloading…`, "info");
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await ctx.reload();
|
|
52
|
+
} catch (error) {
|
|
53
|
+
ctx.ui.notify(
|
|
54
|
+
`Profile '${outcome.profileName}' applied across ${summary}, but automatic reload failed: ${toErrorMessage(error)}. Run /reload.`,
|
|
55
|
+
"error",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function handleModelProfilesCommand(ctx: ExtensionCommandContext): Promise<void> {
|
|
61
|
+
const agentOptions = { cwd: ctx.cwd, scope: "both" as const };
|
|
62
|
+
const prepared = loadAndPrepareProfiles(PROFILE_STORE_PATH, agentOptions);
|
|
63
|
+
notifyWarnings(ctx, prepared.warnings);
|
|
64
|
+
|
|
65
|
+
let data = prepared.data;
|
|
66
|
+
const activeAgentName = detectActiveAgentName(ctx.sessionManager, ctx.getSystemPrompt());
|
|
67
|
+
|
|
68
|
+
const result: ProfileModalResult = await openProfilesModal(ctx, data, activeAgentName, {
|
|
69
|
+
renameProfile: async (profileId, nextName) => {
|
|
70
|
+
data = renameStoredProfile(data, profileId, nextName);
|
|
71
|
+
saveProfilesFile(data, PROFILE_STORE_PATH);
|
|
72
|
+
return {
|
|
73
|
+
data,
|
|
74
|
+
message: `Renamed saved snapshot to '${nextName.trim()}'.`,
|
|
75
|
+
selectedProfileId: profileId,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
addCurrentProfile: async () => {
|
|
79
|
+
const snapshot = captureAgentSnapshots(agentOptions);
|
|
80
|
+
notifyWarnings(ctx, snapshot.warnings);
|
|
81
|
+
const profile = createProfile(buildCurrentProfileName(activeAgentName, data), snapshot.agents);
|
|
82
|
+
data = appendProfile(data, profile);
|
|
83
|
+
saveProfilesFile(data, PROFILE_STORE_PATH);
|
|
84
|
+
return {
|
|
85
|
+
data,
|
|
86
|
+
message: `Saved current agents snapshot (${snapshot.agents.length} agents).`,
|
|
87
|
+
selectedProfileId: profile.id,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
applyProfile: async (profileId) => {
|
|
91
|
+
const profile = findProfileById(data, profileId);
|
|
92
|
+
if (!profile) {
|
|
93
|
+
throw new Error(`Saved profile '${profileId}' was not found.`);
|
|
94
|
+
}
|
|
95
|
+
const applied = applySavedProfile(profile, agentOptions);
|
|
96
|
+
notifyWarnings(ctx, applied.warnings);
|
|
97
|
+
return {
|
|
98
|
+
...applied,
|
|
99
|
+
profileName: profile.name,
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
removeProfile: async (profileId) => {
|
|
103
|
+
const profile = findProfileById(data, profileId);
|
|
104
|
+
if (!profile) {
|
|
105
|
+
throw new Error(`Saved profile '${profileId}' was not found.`);
|
|
106
|
+
}
|
|
107
|
+
const removal = removeProfileAndUpdate(data, profileId);
|
|
108
|
+
data = removal.data;
|
|
109
|
+
saveProfilesFile(data, PROFILE_STORE_PATH);
|
|
110
|
+
return {
|
|
111
|
+
data,
|
|
112
|
+
message: `Removed saved snapshot '${removal.result.removedProfileName}' (${removal.result.remainingCount} snapshots remaining).`,
|
|
113
|
+
selectedProfileId: data.profiles[0]?.id,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
updateProfile: async (profileId) => {
|
|
117
|
+
const profile = findProfileById(data, profileId);
|
|
118
|
+
if (!profile) {
|
|
119
|
+
throw new Error(`Saved profile '${profileId}' was not found.`);
|
|
120
|
+
}
|
|
121
|
+
const update = updateProfileAndReturn(data, profileId, agentOptions);
|
|
122
|
+
notifyWarnings(ctx, update.result.warnings);
|
|
123
|
+
data = update.data;
|
|
124
|
+
saveProfilesFile(data, PROFILE_STORE_PATH);
|
|
125
|
+
return {
|
|
126
|
+
data,
|
|
127
|
+
message: `Updated '${profile.name}' with current agent state (${update.result.updatedAgents} agents).`,
|
|
128
|
+
selectedProfileId: profileId,
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (result.type === "applied") {
|
|
134
|
+
await reloadAfterApply(ctx, result.outcome);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
export const COMMAND_NAME = "model-profiles";
|
|
2
|
-
|
|
3
|
-
export class ModelProfilesError extends Error {
|
|
4
|
-
readonly code: string;
|
|
5
|
-
|
|
6
|
-
constructor(message: string, code = "MODEL_PROFILES_ERROR") {
|
|
7
|
-
super(message);
|
|
8
|
-
this.name = "ModelProfilesError";
|
|
9
|
-
this.code = code;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function toErrorMessage(error: unknown): string {
|
|
14
|
-
if (error instanceof Error && error.message.trim()) {
|
|
15
|
-
return error.message;
|
|
16
|
-
}
|
|
17
|
-
return String(error);
|
|
18
|
-
}
|
|
1
|
+
export const COMMAND_NAME = "model-profiles";
|
|
2
|
+
|
|
3
|
+
export class ModelProfilesError extends Error {
|
|
4
|
+
readonly code: string;
|
|
5
|
+
|
|
6
|
+
constructor(message: string, code = "MODEL_PROFILES_ERROR") {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "ModelProfilesError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function toErrorMessage(error: unknown): string {
|
|
14
|
+
if (error instanceof Error && error.message.trim()) {
|
|
15
|
+
return error.message;
|
|
16
|
+
}
|
|
17
|
+
return String(error);
|
|
18
|
+
}
|
|
@@ -1,249 +1,250 @@
|
|
|
1
|
-
import { PROFILE_FIELD_KEYS, type ProfileFieldKey, type ProfileFields } from "./types.js";
|
|
2
|
-
import { ModelProfilesError } from "./errors.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
let
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
1
|
+
import { PROFILE_FIELD_KEYS, type ProfileFieldKey, type ProfileFields } from "./types.js";
|
|
2
|
+
import { ModelProfilesError } from "./errors.js";
|
|
3
|
+
import { parseCompleteNumericScalar } from "./profile-fields.js";
|
|
4
|
+
|
|
5
|
+
interface FrontmatterDocument {
|
|
6
|
+
frontmatter: string;
|
|
7
|
+
body: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface FrontmatterBlock {
|
|
11
|
+
key: string | null;
|
|
12
|
+
lines: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ANCHOR_KEYS = new Set(["name", "mode", "color", "description"]);
|
|
16
|
+
const SAFE_UNQUOTED_SCALAR = /^[A-Za-z0-9._/@:-]+$/;
|
|
17
|
+
const PROFILE_KEY_SET = new Set<string>(PROFILE_FIELD_KEYS);
|
|
18
|
+
|
|
19
|
+
function normalizeNewlines(value: string): string {
|
|
20
|
+
return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractTopLevelKey(line: string): string | null {
|
|
24
|
+
if (!line || line.startsWith(" ") || line.startsWith("\t")) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const match = /^([A-Za-z0-9_-]+):(?:\s*(.*))?$/.exec(line);
|
|
29
|
+
if (!match) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return match[1] ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseScalarText(value: string): string {
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
if (!trimmed) {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(trimmed) as string;
|
|
44
|
+
} catch {
|
|
45
|
+
return trimmed.slice(1, -1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
49
|
+
return trimmed.slice(1, -1).replace(/''/g, "'");
|
|
50
|
+
}
|
|
51
|
+
return trimmed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function splitFrontmatterBlocks(frontmatter: string): FrontmatterBlock[] {
|
|
55
|
+
const lines = frontmatter.split("\n");
|
|
56
|
+
const starts: Array<{ index: number; key: string }> = [];
|
|
57
|
+
|
|
58
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
59
|
+
const key = extractTopLevelKey(lines[index] ?? "");
|
|
60
|
+
if (key) {
|
|
61
|
+
starts.push({ index, key });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (starts.length === 0) {
|
|
66
|
+
return [{ key: null, lines }];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const blocks: FrontmatterBlock[] = [];
|
|
70
|
+
if ((starts[0]?.index ?? 0) > 0) {
|
|
71
|
+
blocks.push({ key: null, lines: lines.slice(0, starts[0]?.index) });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (let index = 0; index < starts.length; index += 1) {
|
|
75
|
+
const current = starts[index];
|
|
76
|
+
const next = starts[index + 1];
|
|
77
|
+
blocks.push({
|
|
78
|
+
key: current?.key ?? null,
|
|
79
|
+
lines: lines.slice(current?.index ?? 0, next ? next.index : lines.length),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return blocks.filter((block) => block.lines.length > 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function trimBlankEdges(lines: string[]): string[] {
|
|
87
|
+
let start = 0;
|
|
88
|
+
let end = lines.length;
|
|
89
|
+
while (start < end && !(lines[start] ?? "").trim()) {
|
|
90
|
+
start += 1;
|
|
91
|
+
}
|
|
92
|
+
while (end > start && !(lines[end - 1] ?? "").trim()) {
|
|
93
|
+
end -= 1;
|
|
94
|
+
}
|
|
95
|
+
return lines.slice(start, end);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function joinBlocks(blocks: FrontmatterBlock[]): string {
|
|
99
|
+
const lines = blocks.flatMap((block) => block.lines);
|
|
100
|
+
return trimBlankEdges(lines).join("\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function serializeScalar(value: string): string {
|
|
104
|
+
return SAFE_UNQUOTED_SCALAR.test(value) ? value : JSON.stringify(value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function serializeProfileLines(fields: ProfileFields): string[] {
|
|
108
|
+
const lines: string[] = [];
|
|
109
|
+
if (fields.model !== undefined) {
|
|
110
|
+
lines.push(`model: ${serializeScalar(fields.model)}`);
|
|
111
|
+
}
|
|
112
|
+
if (fields.temperature !== undefined) {
|
|
113
|
+
lines.push(`temperature: ${String(fields.temperature)}`);
|
|
114
|
+
}
|
|
115
|
+
if (fields.reasoningEffort !== undefined) {
|
|
116
|
+
lines.push(`reasoningEffort: ${serializeScalar(fields.reasoningEffort)}`);
|
|
117
|
+
}
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function findInsertIndex(blocks: FrontmatterBlock[]): number {
|
|
122
|
+
const firstProfileIndex = blocks.findIndex((block) => block.key !== null && PROFILE_KEY_SET.has(block.key));
|
|
123
|
+
if (firstProfileIndex !== -1) {
|
|
124
|
+
let keptBefore = 0;
|
|
125
|
+
for (let index = 0; index < firstProfileIndex; index += 1) {
|
|
126
|
+
if (!PROFILE_KEY_SET.has(blocks[index]?.key ?? "")) {
|
|
127
|
+
keptBefore += 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return keptBefore;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let lastAnchorIndex = -1;
|
|
134
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
135
|
+
const key = blocks[index]?.key;
|
|
136
|
+
if (key && ANCHOR_KEYS.has(key)) {
|
|
137
|
+
lastAnchorIndex = index;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (lastAnchorIndex !== -1) {
|
|
142
|
+
return lastAnchorIndex + 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return blocks[0]?.key === null ? 1 : 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function extractFrontmatterDocument(markdown: string): FrontmatterDocument {
|
|
149
|
+
const normalized = normalizeNewlines(markdown);
|
|
150
|
+
const lines = normalized.split("\n");
|
|
151
|
+
if ((lines[0] ?? "") !== "---") {
|
|
152
|
+
throw new ModelProfilesError("Agent markdown is missing opening frontmatter delimiter.", "INVALID_FRONTMATTER");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let endIndex = -1;
|
|
156
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
157
|
+
if ((lines[index] ?? "") === "---") {
|
|
158
|
+
endIndex = index;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (endIndex === -1) {
|
|
164
|
+
throw new ModelProfilesError("Agent markdown is missing closing frontmatter delimiter.", "INVALID_FRONTMATTER");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
frontmatter: lines.slice(1, endIndex).join("\n"),
|
|
169
|
+
body: lines.slice(endIndex + 1).join("\n"),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function readTopLevelScalarMap(frontmatter: string): Record<string, string> {
|
|
174
|
+
const values: Record<string, string> = {};
|
|
175
|
+
for (const line of frontmatter.split("\n")) {
|
|
176
|
+
const key = extractTopLevelKey(line);
|
|
177
|
+
if (!key) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const separatorIndex = line.indexOf(":");
|
|
181
|
+
const rawValue = line.slice(separatorIndex + 1).trim();
|
|
182
|
+
if (!rawValue) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
values[key] = parseScalarText(rawValue);
|
|
186
|
+
}
|
|
187
|
+
return values;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function readAgentNameFromMarkdown(markdown: string): string {
|
|
191
|
+
const { frontmatter } = extractFrontmatterDocument(markdown);
|
|
192
|
+
const name = readTopLevelScalarMap(frontmatter).name?.trim();
|
|
193
|
+
if (!name) {
|
|
194
|
+
throw new ModelProfilesError("Agent markdown frontmatter is missing a non-empty 'name' field.", "INVALID_AGENT_NAME");
|
|
195
|
+
}
|
|
196
|
+
return name;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function readProfileFieldsFromMarkdown(markdown: string): ProfileFields {
|
|
200
|
+
const { frontmatter } = extractFrontmatterDocument(markdown);
|
|
201
|
+
const values = readTopLevelScalarMap(frontmatter);
|
|
202
|
+
const fields: ProfileFields = {};
|
|
203
|
+
|
|
204
|
+
const model = values.model?.trim();
|
|
205
|
+
if (model) {
|
|
206
|
+
fields.model = model;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (values.temperature !== undefined) {
|
|
210
|
+
const parsedTemperature = parseCompleteNumericScalar(values.temperature);
|
|
211
|
+
if (parsedTemperature === undefined) {
|
|
212
|
+
throw new ModelProfilesError(
|
|
213
|
+
`Frontmatter field 'temperature' must be numeric, received '${values.temperature}'.`,
|
|
214
|
+
"INVALID_TEMPERATURE",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
fields.temperature = parsedTemperature;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const reasoningEffort = values.reasoningEffort?.trim();
|
|
221
|
+
if (reasoningEffort) {
|
|
222
|
+
fields.reasoningEffort = reasoningEffort;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return fields;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function updateMarkdownProfileFields(markdown: string, fields: ProfileFields): string {
|
|
229
|
+
const document = extractFrontmatterDocument(markdown);
|
|
230
|
+
const originalBlocks = splitFrontmatterBlocks(document.frontmatter);
|
|
231
|
+
const keptBlocks = originalBlocks.filter((block) => !PROFILE_KEY_SET.has(block.key ?? ""));
|
|
232
|
+
const insertIndex = findInsertIndex(originalBlocks);
|
|
233
|
+
const profileLines = serializeProfileLines(fields);
|
|
234
|
+
const nextBlocks = [...keptBlocks];
|
|
235
|
+
|
|
236
|
+
if (profileLines.length > 0) {
|
|
237
|
+
nextBlocks.splice(insertIndex, 0, { key: null, lines: profileLines });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const nextFrontmatter = joinBlocks(nextBlocks);
|
|
241
|
+
return `---\n${nextFrontmatter}\n---\n${document.body}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function listAppliedKeys(fields: ProfileFields): ProfileFieldKey[] {
|
|
245
|
+
return PROFILE_FIELD_KEYS.filter((key) => fields[key] !== undefined);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function listRemovedKeys(fields: ProfileFields): ProfileFieldKey[] {
|
|
249
|
+
return PROFILE_FIELD_KEYS.filter((key) => fields[key] === undefined);
|
|
250
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
import { COMMAND_NAME, toErrorMessage } from "./errors.js";
|
|
4
|
-
import { onResourcesDiscover, refreshModelRegistry } from "./pi-api-utils.js";
|
|
5
|
-
|
|
6
|
-
export default function modelProfilesExtension(pi: ExtensionAPI): void {
|
|
7
|
-
// Register handler to refresh model registry on /reload
|
|
8
|
-
// This works around the issue where AgentSession.reload() doesn't call ModelRegistry.refresh()
|
|
9
|
-
onResourcesDiscover(pi, (event, ctx): void => {
|
|
10
|
-
if (event.reason === "reload") {
|
|
11
|
-
refreshModelRegistry(ctx);
|
|
12
|
-
}
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
pi.registerCommand(COMMAND_NAME, {
|
|
16
|
-
description: "Open saved whole-agent model profile snapshots",
|
|
17
|
-
handler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
|
|
18
|
-
if (args.trim()) {
|
|
19
|
-
ctx.ui.notify(`Usage: /${COMMAND_NAME}`, "warning");
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (!ctx.hasUI) {
|
|
24
|
-
ctx.ui.notify(`/${COMMAND_NAME} requires interactive TUI mode.`, "warning");
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const { handleModelProfilesCommand } = await import("./command-handler.js");
|
|
30
|
-
await handleModelProfilesCommand(ctx);
|
|
31
|
-
} catch (error) {
|
|
32
|
-
ctx.ui.notify(`/${COMMAND_NAME} failed: ${toErrorMessage(error)}`, "error");
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
}
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { COMMAND_NAME, toErrorMessage } from "./errors.js";
|
|
4
|
+
import { onResourcesDiscover, refreshModelRegistry } from "./pi-api-utils.js";
|
|
5
|
+
|
|
6
|
+
export default function modelProfilesExtension(pi: ExtensionAPI): void {
|
|
7
|
+
// Register handler to refresh model registry on /reload
|
|
8
|
+
// This works around the issue where AgentSession.reload() doesn't call ModelRegistry.refresh()
|
|
9
|
+
onResourcesDiscover(pi, (event, ctx): void => {
|
|
10
|
+
if (event.reason === "reload") {
|
|
11
|
+
refreshModelRegistry(ctx);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
pi.registerCommand(COMMAND_NAME, {
|
|
16
|
+
description: "Open saved whole-agent model profile snapshots",
|
|
17
|
+
handler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
|
|
18
|
+
if (args.trim()) {
|
|
19
|
+
ctx.ui.notify(`Usage: /${COMMAND_NAME}`, "warning");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!ctx.hasUI) {
|
|
24
|
+
ctx.ui.notify(`/${COMMAND_NAME} requires interactive TUI mode.`, "warning");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const { handleModelProfilesCommand } = await import("./command-handler.js");
|
|
30
|
+
await handleModelProfilesCommand(ctx);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
ctx.ui.notify(`/${COMMAND_NAME} failed: ${toErrorMessage(error)}`, "error");
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
package/src/profile-fields.ts
CHANGED
|
@@ -1,83 +1,93 @@
|
|
|
1
|
-
import type { ProfileFieldKey, ProfileFields } from "./types.js";
|
|
2
|
-
|
|
3
|
-
function toRecord(value: unknown): Record<string, unknown> {
|
|
4
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5
|
-
return {};
|
|
6
|
-
}
|
|
7
|
-
return value as Record<string, unknown>;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function normalizeOptionalString(value: unknown): string | undefined {
|
|
11
|
-
if (typeof value !== "string") {
|
|
12
|
-
return undefined;
|
|
13
|
-
}
|
|
14
|
-
const trimmed = value.trim();
|
|
15
|
-
return trimmed ? trimmed : undefined;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
fields.
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
1
|
+
import type { ProfileFieldKey, ProfileFields } from "./types.js";
|
|
2
|
+
|
|
3
|
+
function toRecord(value: unknown): Record<string, unknown> {
|
|
4
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5
|
+
return {};
|
|
6
|
+
}
|
|
7
|
+
return value as Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeOptionalString(value: unknown): string | undefined {
|
|
11
|
+
if (typeof value !== "string") {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const trimmed = value.trim();
|
|
15
|
+
return trimmed ? trimmed : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const COMPLETE_NUMERIC_SCALAR = /^[+-]?(?:\d+\.?\d*|\.\d+)$/;
|
|
19
|
+
|
|
20
|
+
export function parseCompleteNumericScalar(value: string): number | undefined {
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
if (!COMPLETE_NUMERIC_SCALAR.test(trimmed)) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const parsed = Number.parseFloat(trimmed);
|
|
26
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeTemperatureValue(value: unknown): number | undefined {
|
|
30
|
+
if (typeof value === "number") {
|
|
31
|
+
return Number.isFinite(value) ? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === "string" && value.trim()) {
|
|
34
|
+
return parseCompleteNumericScalar(value);
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeProfileFields(value: unknown): ProfileFields {
|
|
40
|
+
const source = toRecord(value);
|
|
41
|
+
const fields: ProfileFields = {};
|
|
42
|
+
|
|
43
|
+
const model = normalizeOptionalString(source.model);
|
|
44
|
+
if (model) {
|
|
45
|
+
fields.model = model;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const temperature = normalizeTemperatureValue(source.temperature);
|
|
49
|
+
if (temperature !== undefined) {
|
|
50
|
+
fields.temperature = temperature;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const reasoningEffort = normalizeOptionalString(source.reasoningEffort);
|
|
54
|
+
if (reasoningEffort) {
|
|
55
|
+
fields.reasoningEffort = reasoningEffort;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return fields;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function hasProfileFields(fields: ProfileFields): boolean {
|
|
62
|
+
return fields.model !== undefined || fields.temperature !== undefined || fields.reasoningEffort !== undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function describeProfileFields(fields: ProfileFields): string {
|
|
66
|
+
const parts: string[] = [];
|
|
67
|
+
if (fields.model !== undefined) {
|
|
68
|
+
parts.push("model");
|
|
69
|
+
}
|
|
70
|
+
if (fields.temperature !== undefined) {
|
|
71
|
+
parts.push("temperature");
|
|
72
|
+
}
|
|
73
|
+
if (fields.reasoningEffort !== undefined) {
|
|
74
|
+
parts.push("reasoning");
|
|
75
|
+
}
|
|
76
|
+
return parts.length > 0 ? parts.join(", ") : "clears model overrides";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function formatProfileFieldValue(key: ProfileFieldKey, fields: ProfileFields): string {
|
|
80
|
+
switch (key) {
|
|
81
|
+
case "model":
|
|
82
|
+
return fields.model ?? "(absent)";
|
|
83
|
+
case "temperature":
|
|
84
|
+
return fields.temperature !== undefined ? String(fields.temperature) : "(absent)";
|
|
85
|
+
case "reasoningEffort":
|
|
86
|
+
return fields.reasoningEffort ?? "(absent)";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function sanitizeProfileName(value: string): string | null {
|
|
91
|
+
const trimmed = value.trim();
|
|
92
|
+
return trimmed ? trimmed : null;
|
|
93
|
+
}
|