pi-model-profiles 0.3.2 → 0.3.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/CHANGELOG.md +6 -0
- package/package.json +66 -66
- package/src/command-handler.ts +137 -0
- package/src/errors.ts +2 -0
- package/src/import-service.ts +60 -60
- package/src/index.ts +36 -158
- package/src/profile-removal-service.ts +106 -106
- package/src/profile-sort-service.ts +105 -105
- package/src/profile-update-service.ts +134 -134
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.3] - 2026-06-01
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Lazy-loaded the model profiles command handler by extracting it into `src/command-handler.ts`, reducing startup work.
|
|
14
|
+
- Widened Pi peer dependency compatibility to include Pi 0.77.x and 0.78.x.
|
|
15
|
+
|
|
10
16
|
## [0.3.2] - 2026-05-26
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-model-profiles",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"private": false,
|
|
5
|
-
"description": "Pi extension for saving, importing, and applying agent model frontmatter profiles.",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"main": "./index.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": "./index.ts"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"index.ts",
|
|
13
|
-
"src",
|
|
14
|
-
"README.md",
|
|
15
|
-
"CHANGELOG.md",
|
|
16
|
-
"config/config.example.json",
|
|
17
|
-
"LICENSE"
|
|
18
|
-
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "npx --yes -p typescript@5.9.2 tsc -p tsconfig.json",
|
|
21
|
-
"lint": "npm run build",
|
|
22
|
-
"test:clean": "node -e \"require('node:fs').rmSync('.test-dist', { recursive: true, force: true })\"",
|
|
23
|
-
"prepublishOnly": "npm run test",
|
|
24
|
-
"pretest": "npm run test:clean",
|
|
25
|
-
"test": "npx --yes -p typescript@5.9.2 tsc --strict --skipLibCheck --module nodenext --moduleResolution nodenext --target ES2022 --outDir .test-dist src/types-shims.d.ts src/errors.ts src/types.ts src/constants.ts src/atomic-write.ts src/profile-fields.ts src/frontmatter-parser.ts src/profile-store.ts src/agent-writer.ts src/import-service.ts src/config.ts src/debug-logger.ts test/frontmatter-parser.test.ts test/import-service.test.ts test/agent-writer.test.ts test/profile-store.test.ts test/debug-logger.test.ts && node --test .test-dist/test/frontmatter-parser.test.js .test-dist/test/import-service.test.js .test-dist/test/agent-writer.test.js .test-dist/test/profile-store.test.js .test-dist/test/debug-logger.test.js",
|
|
26
|
-
"posttest": "npm run test:clean",
|
|
27
|
-
"check": "npm run build && npm run test",
|
|
28
|
-
"package:dry-run": "npm pack --dry-run"
|
|
29
|
-
},
|
|
30
|
-
"keywords": [
|
|
31
|
-
"pi-package",
|
|
32
|
-
"pi",
|
|
33
|
-
"pi-extension",
|
|
34
|
-
"pi-coding-agent",
|
|
35
|
-
"coding-agent",
|
|
36
|
-
"model-profiles",
|
|
37
|
-
"frontmatter",
|
|
38
|
-
"agent-configuration",
|
|
39
|
-
"profiles"
|
|
40
|
-
],
|
|
41
|
-
"author": "MasuRii",
|
|
42
|
-
"license": "MIT",
|
|
43
|
-
"homepage": "https://github.com/MasuRii/pi-model-profiles#readme",
|
|
44
|
-
"repository": {
|
|
45
|
-
"type": "git",
|
|
46
|
-
"url": "git+https://github.com/MasuRii/pi-model-profiles.git"
|
|
47
|
-
},
|
|
48
|
-
"bugs": {
|
|
49
|
-
"url": "https://github.com/MasuRii/pi-model-profiles/issues"
|
|
50
|
-
},
|
|
51
|
-
"engines": {
|
|
52
|
-
"node": ">=20"
|
|
53
|
-
},
|
|
54
|
-
"pi": {
|
|
55
|
-
"extensions": [
|
|
56
|
-
"./index.ts"
|
|
57
|
-
]
|
|
58
|
-
},
|
|
59
|
-
"peerDependencies": {
|
|
60
|
-
"@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0",
|
|
61
|
-
"@earendil-works/pi-tui": "^0.74.0 || ^0.75.0"
|
|
62
|
-
},
|
|
63
|
-
"publishConfig": {
|
|
64
|
-
"access": "public"
|
|
65
|
-
}
|
|
66
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-model-profiles",
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Pi extension for saving, importing, and applying agent model frontmatter profiles.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.ts"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.ts",
|
|
13
|
+
"src",
|
|
14
|
+
"README.md",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"config/config.example.json",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "npx --yes -p typescript@5.9.2 tsc -p tsconfig.json",
|
|
21
|
+
"lint": "npm run build",
|
|
22
|
+
"test:clean": "node -e \"require('node:fs').rmSync('.test-dist', { recursive: true, force: true })\"",
|
|
23
|
+
"prepublishOnly": "npm run test",
|
|
24
|
+
"pretest": "npm run test:clean",
|
|
25
|
+
"test": "npx --yes -p typescript@5.9.2 tsc --strict --skipLibCheck --module nodenext --moduleResolution nodenext --target ES2022 --outDir .test-dist src/types-shims.d.ts src/errors.ts src/types.ts src/constants.ts src/atomic-write.ts src/profile-fields.ts src/frontmatter-parser.ts src/profile-store.ts src/agent-writer.ts src/import-service.ts src/config.ts src/debug-logger.ts test/frontmatter-parser.test.ts test/import-service.test.ts test/agent-writer.test.ts test/profile-store.test.ts test/debug-logger.test.ts && node --test .test-dist/test/frontmatter-parser.test.js .test-dist/test/import-service.test.js .test-dist/test/agent-writer.test.js .test-dist/test/profile-store.test.js .test-dist/test/debug-logger.test.js",
|
|
26
|
+
"posttest": "npm run test:clean",
|
|
27
|
+
"check": "npm run build && npm run test",
|
|
28
|
+
"package:dry-run": "npm pack --dry-run"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"pi-package",
|
|
32
|
+
"pi",
|
|
33
|
+
"pi-extension",
|
|
34
|
+
"pi-coding-agent",
|
|
35
|
+
"coding-agent",
|
|
36
|
+
"model-profiles",
|
|
37
|
+
"frontmatter",
|
|
38
|
+
"agent-configuration",
|
|
39
|
+
"profiles"
|
|
40
|
+
],
|
|
41
|
+
"author": "MasuRii",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"homepage": "https://github.com/MasuRii/pi-model-profiles#readme",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/MasuRii/pi-model-profiles.git"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/MasuRii/pi-model-profiles/issues"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=20"
|
|
53
|
+
},
|
|
54
|
+
"pi": {
|
|
55
|
+
"extensions": [
|
|
56
|
+
"./index.ts"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
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"
|
|
62
|
+
},
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
package/src/errors.ts
CHANGED
package/src/import-service.ts
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
import { AGENTS_DIR, INITIAL_PROFILE_NAME, PROFILE_STORE_PATH } from "./constants.js";
|
|
2
|
-
import { captureAgentSnapshots, type AgentSelectionOptions } from "./agent-writer.js";
|
|
3
|
-
import { appendProfile, createProfile, loadProfilesFile, resolveUniqueProfileName, saveProfilesFile } from "./profile-store.js";
|
|
4
|
-
import { loadMultiProfilesConfig } from "./config.js";
|
|
5
|
-
import { multiProfilesDebugLogger } from "./debug-logger.js";
|
|
6
|
-
import type { ImportProfilesResult, ProfilesFile } from "./types.js";
|
|
7
|
-
|
|
8
|
-
export function ensureProfilesImported(
|
|
9
|
-
data: ProfilesFile,
|
|
10
|
-
agentOptions: string | AgentSelectionOptions = AGENTS_DIR,
|
|
11
|
-
): ImportProfilesResult {
|
|
12
|
-
if (data.importedAt) {
|
|
13
|
-
return {
|
|
14
|
-
data,
|
|
15
|
-
imported: false,
|
|
16
|
-
importedCount: 0,
|
|
17
|
-
warnings: [],
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const snapshot = captureAgentSnapshots(agentOptions);
|
|
22
|
-
const timestamp = new Date().toISOString();
|
|
23
|
-
const profile = createProfile(resolveUniqueProfileName(INITIAL_PROFILE_NAME, data.profiles), snapshot.agents, { timestamp });
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
data: {
|
|
27
|
-
...appendProfile(data, profile),
|
|
28
|
-
importedAt: timestamp,
|
|
29
|
-
},
|
|
30
|
-
imported: true,
|
|
31
|
-
importedCount: 1,
|
|
32
|
-
warnings: snapshot.warnings,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function loadAndPrepareProfiles(
|
|
37
|
-
storePath = PROFILE_STORE_PATH,
|
|
38
|
-
agentOptions: string | AgentSelectionOptions = AGENTS_DIR,
|
|
39
|
-
): ImportProfilesResult {
|
|
40
|
-
const configLoad = loadMultiProfilesConfig();
|
|
41
|
-
|
|
42
|
-
multiProfilesDebugLogger.log("extension.initialized", {
|
|
43
|
-
configCreated: configLoad.created,
|
|
44
|
-
timestamp: new Date().toISOString(),
|
|
45
|
-
profilesVersion: 2,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const loaded = loadProfilesFile(storePath, agentOptions);
|
|
49
|
-
const prepared = ensureProfilesImported(loaded.data, agentOptions);
|
|
50
|
-
|
|
51
|
-
if (loaded.needsSave || prepared.imported) {
|
|
52
|
-
saveProfilesFile(prepared.data, storePath);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const warnings = [configLoad.warning, loaded.warning, ...prepared.warnings].filter((message): message is string => Boolean(message));
|
|
56
|
-
return {
|
|
57
|
-
...prepared,
|
|
58
|
-
warnings,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
1
|
+
import { AGENTS_DIR, INITIAL_PROFILE_NAME, PROFILE_STORE_PATH } from "./constants.js";
|
|
2
|
+
import { captureAgentSnapshots, type AgentSelectionOptions } from "./agent-writer.js";
|
|
3
|
+
import { appendProfile, createProfile, loadProfilesFile, resolveUniqueProfileName, saveProfilesFile } from "./profile-store.js";
|
|
4
|
+
import { loadMultiProfilesConfig } from "./config.js";
|
|
5
|
+
import { multiProfilesDebugLogger } from "./debug-logger.js";
|
|
6
|
+
import type { ImportProfilesResult, ProfilesFile } from "./types.js";
|
|
7
|
+
|
|
8
|
+
export function ensureProfilesImported(
|
|
9
|
+
data: ProfilesFile,
|
|
10
|
+
agentOptions: string | AgentSelectionOptions = AGENTS_DIR,
|
|
11
|
+
): ImportProfilesResult {
|
|
12
|
+
if (data.importedAt) {
|
|
13
|
+
return {
|
|
14
|
+
data,
|
|
15
|
+
imported: false,
|
|
16
|
+
importedCount: 0,
|
|
17
|
+
warnings: [],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const snapshot = captureAgentSnapshots(agentOptions);
|
|
22
|
+
const timestamp = new Date().toISOString();
|
|
23
|
+
const profile = createProfile(resolveUniqueProfileName(INITIAL_PROFILE_NAME, data.profiles), snapshot.agents, { timestamp });
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
data: {
|
|
27
|
+
...appendProfile(data, profile),
|
|
28
|
+
importedAt: timestamp,
|
|
29
|
+
},
|
|
30
|
+
imported: true,
|
|
31
|
+
importedCount: 1,
|
|
32
|
+
warnings: snapshot.warnings,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function loadAndPrepareProfiles(
|
|
37
|
+
storePath = PROFILE_STORE_PATH,
|
|
38
|
+
agentOptions: string | AgentSelectionOptions = AGENTS_DIR,
|
|
39
|
+
): ImportProfilesResult {
|
|
40
|
+
const configLoad = loadMultiProfilesConfig();
|
|
41
|
+
|
|
42
|
+
multiProfilesDebugLogger.log("extension.initialized", {
|
|
43
|
+
configCreated: configLoad.created,
|
|
44
|
+
timestamp: new Date().toISOString(),
|
|
45
|
+
profilesVersion: 2,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const loaded = loadProfilesFile(storePath, agentOptions);
|
|
49
|
+
const prepared = ensureProfilesImported(loaded.data, agentOptions);
|
|
50
|
+
|
|
51
|
+
if (loaded.needsSave || prepared.imported) {
|
|
52
|
+
saveProfilesFile(prepared.data, storePath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const warnings = [configLoad.warning, loaded.warning, ...prepared.warnings].filter((message): message is string => Boolean(message));
|
|
56
|
+
return {
|
|
57
|
+
...prepared,
|
|
58
|
+
warnings,
|
|
59
|
+
};
|
|
60
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,158 +1,36 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionCommandContext
|
|
2
|
-
|
|
3
|
-
import { COMMAND_NAME,
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const summary = summarizeApplyOutcome(outcome);
|
|
38
|
-
ctx.ui.notify(`Profile '${outcome.profileName}' applied across ${summary}. Reloading…`, "info");
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
await ctx.reload();
|
|
42
|
-
} catch (error) {
|
|
43
|
-
ctx.ui.notify(
|
|
44
|
-
`Profile '${outcome.profileName}' applied across ${summary}, but automatic reload failed: ${toErrorMessage(error)}. Run /reload.`,
|
|
45
|
-
"error",
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function handleModelProfilesCommand(ctx: ExtensionCommandContext): Promise<void> {
|
|
51
|
-
const agentOptions = { cwd: ctx.cwd, scope: "both" as const };
|
|
52
|
-
const prepared = loadAndPrepareProfiles(PROFILE_STORE_PATH, agentOptions);
|
|
53
|
-
notifyWarnings(ctx, prepared.warnings);
|
|
54
|
-
|
|
55
|
-
let data = prepared.data;
|
|
56
|
-
const activeAgentName = detectActiveAgentName(ctx.sessionManager, ctx.getSystemPrompt());
|
|
57
|
-
|
|
58
|
-
const result: ProfileModalResult = await openProfilesModal(ctx, data, activeAgentName, {
|
|
59
|
-
renameProfile: async (profileId, nextName) => {
|
|
60
|
-
data = renameStoredProfile(data, profileId, nextName);
|
|
61
|
-
saveProfilesFile(data, PROFILE_STORE_PATH);
|
|
62
|
-
return {
|
|
63
|
-
data,
|
|
64
|
-
message: `Renamed saved snapshot to '${nextName.trim()}'.`,
|
|
65
|
-
selectedProfileId: profileId,
|
|
66
|
-
};
|
|
67
|
-
},
|
|
68
|
-
addCurrentProfile: async () => {
|
|
69
|
-
const snapshot = captureAgentSnapshots(agentOptions);
|
|
70
|
-
notifyWarnings(ctx, snapshot.warnings);
|
|
71
|
-
const profile = createProfile(buildCurrentProfileName(activeAgentName, data), snapshot.agents);
|
|
72
|
-
data = appendProfile(data, profile);
|
|
73
|
-
saveProfilesFile(data, PROFILE_STORE_PATH);
|
|
74
|
-
return {
|
|
75
|
-
data,
|
|
76
|
-
message: `Saved current agents snapshot (${snapshot.agents.length} agents).`,
|
|
77
|
-
selectedProfileId: profile.id,
|
|
78
|
-
};
|
|
79
|
-
},
|
|
80
|
-
applyProfile: async (profileId) => {
|
|
81
|
-
const profile = findProfileById(data, profileId);
|
|
82
|
-
if (!profile) {
|
|
83
|
-
throw new Error(`Saved profile '${profileId}' was not found.`);
|
|
84
|
-
}
|
|
85
|
-
const applied = applySavedProfile(profile, agentOptions);
|
|
86
|
-
notifyWarnings(ctx, applied.warnings);
|
|
87
|
-
return {
|
|
88
|
-
...applied,
|
|
89
|
-
profileName: profile.name,
|
|
90
|
-
};
|
|
91
|
-
},
|
|
92
|
-
removeProfile: async (profileId) => {
|
|
93
|
-
const profile = findProfileById(data, profileId);
|
|
94
|
-
if (!profile) {
|
|
95
|
-
throw new Error(`Saved profile '${profileId}' was not found.`);
|
|
96
|
-
}
|
|
97
|
-
const removal = removeProfileAndUpdate(data, profileId);
|
|
98
|
-
data = removal.data;
|
|
99
|
-
saveProfilesFile(data, PROFILE_STORE_PATH);
|
|
100
|
-
return {
|
|
101
|
-
data,
|
|
102
|
-
message: `Removed saved snapshot '${removal.result.removedProfileName}' (${removal.result.remainingCount} snapshots remaining).`,
|
|
103
|
-
selectedProfileId: data.profiles[0]?.id,
|
|
104
|
-
};
|
|
105
|
-
},
|
|
106
|
-
updateProfile: async (profileId) => {
|
|
107
|
-
const profile = findProfileById(data, profileId);
|
|
108
|
-
if (!profile) {
|
|
109
|
-
throw new Error(`Saved profile '${profileId}' was not found.`);
|
|
110
|
-
}
|
|
111
|
-
const update = updateProfileAndReturn(data, profileId, agentOptions);
|
|
112
|
-
notifyWarnings(ctx, update.result.warnings);
|
|
113
|
-
data = update.data;
|
|
114
|
-
saveProfilesFile(data, PROFILE_STORE_PATH);
|
|
115
|
-
return {
|
|
116
|
-
data,
|
|
117
|
-
message: `Updated '${profile.name}' with current agent state (${update.result.updatedAgents} agents).`,
|
|
118
|
-
selectedProfileId: profileId,
|
|
119
|
-
};
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
if (result.type === "applied") {
|
|
124
|
-
await reloadAfterApply(ctx, result.outcome);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export default function modelProfilesExtension(pi: ExtensionAPI): void {
|
|
130
|
-
// Register handler to refresh model registry on /reload
|
|
131
|
-
// This works around the issue where AgentSession.reload() doesn't call ModelRegistry.refresh()
|
|
132
|
-
onResourcesDiscover(pi, (event, ctx): void => {
|
|
133
|
-
if (event.reason === "reload") {
|
|
134
|
-
refreshModelRegistry(ctx);
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
pi.registerCommand(COMMAND_NAME, {
|
|
139
|
-
description: "Open saved whole-agent model profile snapshots",
|
|
140
|
-
handler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
|
|
141
|
-
if (args.trim()) {
|
|
142
|
-
ctx.ui.notify(`Usage: /${COMMAND_NAME}`, "warning");
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (!ctx.hasUI) {
|
|
147
|
-
ctx.ui.notify(`/${COMMAND_NAME} requires interactive TUI mode.`, "warning");
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
await handleModelProfilesCommand(ctx);
|
|
153
|
-
} catch (error) {
|
|
154
|
-
ctx.ui.notify(`/${COMMAND_NAME} failed: ${toErrorMessage(error)}`, "error");
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
}
|
|
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,106 +1,106 @@
|
|
|
1
|
-
import { multiProfilesDebugLogger } from "./debug-logger.js";
|
|
2
|
-
import { ModelProfilesError } from "./errors.js";
|
|
3
|
-
import type { ProfilesFile, ProfileRemovalResult, SavedProfile } from "./types.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Error code for profile not found during removal.
|
|
7
|
-
*/
|
|
8
|
-
export const PROFILE_NOT_FOUND_CODE = "PROFILE_NOT_FOUND";
|
|
9
|
-
|
|
10
|
-
export interface ProfileRemovalUpdateResult {
|
|
11
|
-
data: ProfilesFile;
|
|
12
|
-
result: ProfileRemovalResult;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface ProfileRemovalPlan {
|
|
16
|
-
profile: SavedProfile;
|
|
17
|
-
remainingProfiles: SavedProfile[];
|
|
18
|
-
result: ProfileRemovalResult;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function createProfileNotFoundError(profileId: string): ModelProfilesError {
|
|
22
|
-
return new ModelProfilesError(
|
|
23
|
-
`Profile '${profileId}' not found. It may have been removed already.`,
|
|
24
|
-
PROFILE_NOT_FOUND_CODE,
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function buildRemovalPlan(data: ProfilesFile, profileId: string): ProfileRemovalPlan {
|
|
29
|
-
const profileIndex = data.profiles.findIndex((profile) => profile.id === profileId);
|
|
30
|
-
const profile = data.profiles[profileIndex];
|
|
31
|
-
|
|
32
|
-
if (profileIndex === -1 || !profile) {
|
|
33
|
-
multiProfilesDebugLogger.log("profile-removal", {
|
|
34
|
-
event: "removal_failed",
|
|
35
|
-
profileId,
|
|
36
|
-
reason: "profile_not_found",
|
|
37
|
-
profileCount: data.profiles.length,
|
|
38
|
-
});
|
|
39
|
-
throw createProfileNotFoundError(profileId);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const remainingProfiles = [
|
|
43
|
-
...data.profiles.slice(0, profileIndex),
|
|
44
|
-
...data.profiles.slice(profileIndex + 1),
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
profile,
|
|
49
|
-
remainingProfiles,
|
|
50
|
-
result: {
|
|
51
|
-
removedProfileId: profile.id,
|
|
52
|
-
removedProfileName: profile.name,
|
|
53
|
-
remainingCount: remainingProfiles.length,
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function logProfileRemoved(plan: ProfileRemovalPlan): void {
|
|
59
|
-
multiProfilesDebugLogger.log("profile-removal", {
|
|
60
|
-
event: "profile_removed",
|
|
61
|
-
profileId: plan.profile.id,
|
|
62
|
-
profileName: plan.profile.name,
|
|
63
|
-
agentCount: plan.profile.agents.length,
|
|
64
|
-
remainingCount: plan.result.remainingCount,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Remove a profile from the profiles file.
|
|
70
|
-
*
|
|
71
|
-
* Validates that the profile exists before removal.
|
|
72
|
-
* Preserves other profiles unchanged (immutable pattern).
|
|
73
|
-
* Logs removal event via debug logger.
|
|
74
|
-
*
|
|
75
|
-
* @param data - The profiles file data
|
|
76
|
-
* @param profileId - The ID of the profile to remove
|
|
77
|
-
* @returns ProfileRemovalResult with removed profile info and remaining count
|
|
78
|
-
* @throws ModelProfilesError with code PROFILE_NOT_FOUND if profile doesn't exist
|
|
79
|
-
*/
|
|
80
|
-
export function removeProfile(data: ProfilesFile, profileId: string): ProfileRemovalResult {
|
|
81
|
-
const plan = buildRemovalPlan(data, profileId);
|
|
82
|
-
logProfileRemoved(plan);
|
|
83
|
-
return plan.result;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Remove a profile and return the updated profiles file with removal metadata.
|
|
88
|
-
*
|
|
89
|
-
* @param data - The profiles file data
|
|
90
|
-
* @param profileId - The ID of the profile to remove
|
|
91
|
-
* @returns New ProfilesFile with the profile removed and removal metadata
|
|
92
|
-
* @throws ModelProfilesError with code PROFILE_NOT_FOUND if profile doesn't exist
|
|
93
|
-
*/
|
|
94
|
-
export function removeProfileAndUpdate(data: ProfilesFile, profileId: string): ProfileRemovalUpdateResult {
|
|
95
|
-
const plan = buildRemovalPlan(data, profileId);
|
|
96
|
-
logProfileRemoved(plan);
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
data: {
|
|
100
|
-
version: data.version,
|
|
101
|
-
importedAt: data.importedAt,
|
|
102
|
-
profiles: plan.remainingProfiles,
|
|
103
|
-
},
|
|
104
|
-
result: plan.result,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
1
|
+
import { multiProfilesDebugLogger } from "./debug-logger.js";
|
|
2
|
+
import { ModelProfilesError } from "./errors.js";
|
|
3
|
+
import type { ProfilesFile, ProfileRemovalResult, SavedProfile } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Error code for profile not found during removal.
|
|
7
|
+
*/
|
|
8
|
+
export const PROFILE_NOT_FOUND_CODE = "PROFILE_NOT_FOUND";
|
|
9
|
+
|
|
10
|
+
export interface ProfileRemovalUpdateResult {
|
|
11
|
+
data: ProfilesFile;
|
|
12
|
+
result: ProfileRemovalResult;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ProfileRemovalPlan {
|
|
16
|
+
profile: SavedProfile;
|
|
17
|
+
remainingProfiles: SavedProfile[];
|
|
18
|
+
result: ProfileRemovalResult;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createProfileNotFoundError(profileId: string): ModelProfilesError {
|
|
22
|
+
return new ModelProfilesError(
|
|
23
|
+
`Profile '${profileId}' not found. It may have been removed already.`,
|
|
24
|
+
PROFILE_NOT_FOUND_CODE,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildRemovalPlan(data: ProfilesFile, profileId: string): ProfileRemovalPlan {
|
|
29
|
+
const profileIndex = data.profiles.findIndex((profile) => profile.id === profileId);
|
|
30
|
+
const profile = data.profiles[profileIndex];
|
|
31
|
+
|
|
32
|
+
if (profileIndex === -1 || !profile) {
|
|
33
|
+
multiProfilesDebugLogger.log("profile-removal", {
|
|
34
|
+
event: "removal_failed",
|
|
35
|
+
profileId,
|
|
36
|
+
reason: "profile_not_found",
|
|
37
|
+
profileCount: data.profiles.length,
|
|
38
|
+
});
|
|
39
|
+
throw createProfileNotFoundError(profileId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const remainingProfiles = [
|
|
43
|
+
...data.profiles.slice(0, profileIndex),
|
|
44
|
+
...data.profiles.slice(profileIndex + 1),
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
profile,
|
|
49
|
+
remainingProfiles,
|
|
50
|
+
result: {
|
|
51
|
+
removedProfileId: profile.id,
|
|
52
|
+
removedProfileName: profile.name,
|
|
53
|
+
remainingCount: remainingProfiles.length,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function logProfileRemoved(plan: ProfileRemovalPlan): void {
|
|
59
|
+
multiProfilesDebugLogger.log("profile-removal", {
|
|
60
|
+
event: "profile_removed",
|
|
61
|
+
profileId: plan.profile.id,
|
|
62
|
+
profileName: plan.profile.name,
|
|
63
|
+
agentCount: plan.profile.agents.length,
|
|
64
|
+
remainingCount: plan.result.remainingCount,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Remove a profile from the profiles file.
|
|
70
|
+
*
|
|
71
|
+
* Validates that the profile exists before removal.
|
|
72
|
+
* Preserves other profiles unchanged (immutable pattern).
|
|
73
|
+
* Logs removal event via debug logger.
|
|
74
|
+
*
|
|
75
|
+
* @param data - The profiles file data
|
|
76
|
+
* @param profileId - The ID of the profile to remove
|
|
77
|
+
* @returns ProfileRemovalResult with removed profile info and remaining count
|
|
78
|
+
* @throws ModelProfilesError with code PROFILE_NOT_FOUND if profile doesn't exist
|
|
79
|
+
*/
|
|
80
|
+
export function removeProfile(data: ProfilesFile, profileId: string): ProfileRemovalResult {
|
|
81
|
+
const plan = buildRemovalPlan(data, profileId);
|
|
82
|
+
logProfileRemoved(plan);
|
|
83
|
+
return plan.result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Remove a profile and return the updated profiles file with removal metadata.
|
|
88
|
+
*
|
|
89
|
+
* @param data - The profiles file data
|
|
90
|
+
* @param profileId - The ID of the profile to remove
|
|
91
|
+
* @returns New ProfilesFile with the profile removed and removal metadata
|
|
92
|
+
* @throws ModelProfilesError with code PROFILE_NOT_FOUND if profile doesn't exist
|
|
93
|
+
*/
|
|
94
|
+
export function removeProfileAndUpdate(data: ProfilesFile, profileId: string): ProfileRemovalUpdateResult {
|
|
95
|
+
const plan = buildRemovalPlan(data, profileId);
|
|
96
|
+
logProfileRemoved(plan);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
data: {
|
|
100
|
+
version: data.version,
|
|
101
|
+
importedAt: data.importedAt,
|
|
102
|
+
profiles: plan.remainingProfiles,
|
|
103
|
+
},
|
|
104
|
+
result: plan.result,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
import { multiProfilesDebugLogger } from "./debug-logger.js";
|
|
2
|
-
import { loadMultiProfilesConfig, saveMultiProfilesConfig } from "./config.js";
|
|
3
|
-
import type { ProfileSortResult, ProfilesFile, SavedProfile } from "./types.js";
|
|
4
|
-
import type { ProfileSortOrder } from "./types.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Compare two profile names using locale-aware string comparison.
|
|
8
|
-
*/
|
|
9
|
-
function compareByName(left: SavedProfile, right: SavedProfile): number {
|
|
10
|
-
return left.name.localeCompare(right.name);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Compare two profiles by their createdAt timestamp.
|
|
15
|
-
*/
|
|
16
|
-
function compareByDate(left: SavedProfile, right: SavedProfile): number {
|
|
17
|
-
const leftDate = new Date(left.createdAt).getTime();
|
|
18
|
-
const rightDate = new Date(right.createdAt).getTime();
|
|
19
|
-
return leftDate - rightDate;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Sort profiles based on the specified order.
|
|
24
|
-
* Returns a new array (immutable pattern), preserving the original data.profiles.
|
|
25
|
-
*
|
|
26
|
-
* @param data - The profiles file containing the profiles array
|
|
27
|
-
* @param order - The sort order to apply
|
|
28
|
-
* @returns ProfileSortResult with sorted profiles and applied sort order
|
|
29
|
-
*/
|
|
30
|
-
export function sortProfiles(data: ProfilesFile, order: ProfileSortOrder): ProfileSortResult {
|
|
31
|
-
const comparator = getSortComparator(order);
|
|
32
|
-
const sortedProfiles = [...data.profiles].sort(comparator);
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
sortedProfiles,
|
|
36
|
-
sortOrder: order,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Get the comparator function for a given sort order.
|
|
42
|
-
*/
|
|
43
|
-
function getSortComparator(order: ProfileSortOrder): (left: SavedProfile, right: SavedProfile) => number {
|
|
44
|
-
switch (order) {
|
|
45
|
-
case "name-asc":
|
|
46
|
-
return compareByName;
|
|
47
|
-
case "name-desc":
|
|
48
|
-
return (left, right) => compareByName(right, left);
|
|
49
|
-
case "date-asc":
|
|
50
|
-
return compareByDate;
|
|
51
|
-
case "date-desc":
|
|
52
|
-
return (left, right) => compareByDate(right, left);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Get the current sort order from config.
|
|
58
|
-
*/
|
|
59
|
-
export function getCurrentSortOrder(): ProfileSortOrder {
|
|
60
|
-
const result = loadMultiProfilesConfig();
|
|
61
|
-
return result.config.sorting.defaultSort;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Persist the sort order to config.
|
|
66
|
-
*/
|
|
67
|
-
export function persistSortOrder(order: ProfileSortOrder): void {
|
|
68
|
-
const result = loadMultiProfilesConfig();
|
|
69
|
-
const config = result.config;
|
|
70
|
-
config.sorting.defaultSort = order;
|
|
71
|
-
saveMultiProfilesConfig(config);
|
|
72
|
-
|
|
73
|
-
multiProfilesDebugLogger.log("config", {
|
|
74
|
-
event: "sort_order_persisted",
|
|
75
|
-
order,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Get display label for a sort order.
|
|
81
|
-
*/
|
|
82
|
-
export function getSortOrderLabel(order: ProfileSortOrder): string {
|
|
83
|
-
switch (order) {
|
|
84
|
-
case "name-asc":
|
|
85
|
-
return "Name (A-Z)";
|
|
86
|
-
case "name-desc":
|
|
87
|
-
return "Name (Z-A)";
|
|
88
|
-
case "date-asc":
|
|
89
|
-
return "Date (Oldest)";
|
|
90
|
-
case "date-desc":
|
|
91
|
-
return "Date (Newest)";
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Get all available sort orders with their labels.
|
|
97
|
-
*/
|
|
98
|
-
export function getAvailableSortOrders(): Array<{ order: ProfileSortOrder; label: string }> {
|
|
99
|
-
return [
|
|
100
|
-
{ order: "name-asc", label: "Name (A-Z)" },
|
|
101
|
-
{ order: "name-desc", label: "Name (Z-A)" },
|
|
102
|
-
{ order: "date-asc", label: "Date (Oldest)" },
|
|
103
|
-
{ order: "date-desc", label: "Date (Newest)" },
|
|
104
|
-
];
|
|
105
|
-
}
|
|
1
|
+
import { multiProfilesDebugLogger } from "./debug-logger.js";
|
|
2
|
+
import { loadMultiProfilesConfig, saveMultiProfilesConfig } from "./config.js";
|
|
3
|
+
import type { ProfileSortResult, ProfilesFile, SavedProfile } from "./types.js";
|
|
4
|
+
import type { ProfileSortOrder } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Compare two profile names using locale-aware string comparison.
|
|
8
|
+
*/
|
|
9
|
+
function compareByName(left: SavedProfile, right: SavedProfile): number {
|
|
10
|
+
return left.name.localeCompare(right.name);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compare two profiles by their createdAt timestamp.
|
|
15
|
+
*/
|
|
16
|
+
function compareByDate(left: SavedProfile, right: SavedProfile): number {
|
|
17
|
+
const leftDate = new Date(left.createdAt).getTime();
|
|
18
|
+
const rightDate = new Date(right.createdAt).getTime();
|
|
19
|
+
return leftDate - rightDate;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Sort profiles based on the specified order.
|
|
24
|
+
* Returns a new array (immutable pattern), preserving the original data.profiles.
|
|
25
|
+
*
|
|
26
|
+
* @param data - The profiles file containing the profiles array
|
|
27
|
+
* @param order - The sort order to apply
|
|
28
|
+
* @returns ProfileSortResult with sorted profiles and applied sort order
|
|
29
|
+
*/
|
|
30
|
+
export function sortProfiles(data: ProfilesFile, order: ProfileSortOrder): ProfileSortResult {
|
|
31
|
+
const comparator = getSortComparator(order);
|
|
32
|
+
const sortedProfiles = [...data.profiles].sort(comparator);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
sortedProfiles,
|
|
36
|
+
sortOrder: order,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the comparator function for a given sort order.
|
|
42
|
+
*/
|
|
43
|
+
function getSortComparator(order: ProfileSortOrder): (left: SavedProfile, right: SavedProfile) => number {
|
|
44
|
+
switch (order) {
|
|
45
|
+
case "name-asc":
|
|
46
|
+
return compareByName;
|
|
47
|
+
case "name-desc":
|
|
48
|
+
return (left, right) => compareByName(right, left);
|
|
49
|
+
case "date-asc":
|
|
50
|
+
return compareByDate;
|
|
51
|
+
case "date-desc":
|
|
52
|
+
return (left, right) => compareByDate(right, left);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get the current sort order from config.
|
|
58
|
+
*/
|
|
59
|
+
export function getCurrentSortOrder(): ProfileSortOrder {
|
|
60
|
+
const result = loadMultiProfilesConfig();
|
|
61
|
+
return result.config.sorting.defaultSort;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Persist the sort order to config.
|
|
66
|
+
*/
|
|
67
|
+
export function persistSortOrder(order: ProfileSortOrder): void {
|
|
68
|
+
const result = loadMultiProfilesConfig();
|
|
69
|
+
const config = result.config;
|
|
70
|
+
config.sorting.defaultSort = order;
|
|
71
|
+
saveMultiProfilesConfig(config);
|
|
72
|
+
|
|
73
|
+
multiProfilesDebugLogger.log("config", {
|
|
74
|
+
event: "sort_order_persisted",
|
|
75
|
+
order,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get display label for a sort order.
|
|
81
|
+
*/
|
|
82
|
+
export function getSortOrderLabel(order: ProfileSortOrder): string {
|
|
83
|
+
switch (order) {
|
|
84
|
+
case "name-asc":
|
|
85
|
+
return "Name (A-Z)";
|
|
86
|
+
case "name-desc":
|
|
87
|
+
return "Name (Z-A)";
|
|
88
|
+
case "date-asc":
|
|
89
|
+
return "Date (Oldest)";
|
|
90
|
+
case "date-desc":
|
|
91
|
+
return "Date (Newest)";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get all available sort orders with their labels.
|
|
97
|
+
*/
|
|
98
|
+
export function getAvailableSortOrders(): Array<{ order: ProfileSortOrder; label: string }> {
|
|
99
|
+
return [
|
|
100
|
+
{ order: "name-asc", label: "Name (A-Z)" },
|
|
101
|
+
{ order: "name-desc", label: "Name (Z-A)" },
|
|
102
|
+
{ order: "date-asc", label: "Date (Oldest)" },
|
|
103
|
+
{ order: "date-desc", label: "Date (Newest)" },
|
|
104
|
+
];
|
|
105
|
+
}
|
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
import { multiProfilesDebugLogger } from "./debug-logger.js";
|
|
2
|
-
import { ModelProfilesError } from "./errors.js";
|
|
3
|
-
import { captureAgentSnapshots, type AgentSelectionOptions } from "./agent-writer.js";
|
|
4
|
-
import type { ProfilesFile, ProfileUpdateResult, SavedProfile } from "./types.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Error code for profile not found during update.
|
|
8
|
-
*/
|
|
9
|
-
export const PROFILE_NOT_FOUND_CODE = "PROFILE_NOT_FOUND";
|
|
10
|
-
|
|
11
|
-
export interface ProfileUpdateDataResult {
|
|
12
|
-
data: ProfilesFile;
|
|
13
|
-
result: ProfileUpdateResult;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface ProfileUpdatePlan {
|
|
17
|
-
updatedData: ProfilesFile;
|
|
18
|
-
updatedProfile: SavedProfile;
|
|
19
|
-
previousProfile: SavedProfile;
|
|
20
|
-
result: ProfileUpdateResult;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function createProfileNotFoundError(profileId: string): ModelProfilesError {
|
|
24
|
-
return new ModelProfilesError(
|
|
25
|
-
`Profile '${profileId}' not found. It may have been removed already.`,
|
|
26
|
-
PROFILE_NOT_FOUND_CODE,
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function buildUpdatePlan(
|
|
31
|
-
data: ProfilesFile,
|
|
32
|
-
profileId: string,
|
|
33
|
-
agentOptions: AgentSelectionOptions,
|
|
34
|
-
): ProfileUpdatePlan {
|
|
35
|
-
const profileIndex = data.profiles.findIndex((profile) => profile.id === profileId);
|
|
36
|
-
const profile = data.profiles[profileIndex];
|
|
37
|
-
|
|
38
|
-
if (profileIndex === -1 || !profile) {
|
|
39
|
-
multiProfilesDebugLogger.log("profile-update", {
|
|
40
|
-
event: "update_failed",
|
|
41
|
-
profileId,
|
|
42
|
-
reason: "profile_not_found",
|
|
43
|
-
profileCount: data.profiles.length,
|
|
44
|
-
});
|
|
45
|
-
throw createProfileNotFoundError(profileId);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const snapshot = captureAgentSnapshots(agentOptions);
|
|
49
|
-
const updatedProfile: SavedProfile = {
|
|
50
|
-
id: profile.id,
|
|
51
|
-
name: profile.name,
|
|
52
|
-
agents: snapshot.agents,
|
|
53
|
-
createdAt: profile.createdAt,
|
|
54
|
-
updatedAt: new Date().toISOString(),
|
|
55
|
-
};
|
|
56
|
-
const updatedProfiles = [
|
|
57
|
-
...data.profiles.slice(0, profileIndex),
|
|
58
|
-
updatedProfile,
|
|
59
|
-
...data.profiles.slice(profileIndex + 1),
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
updatedData: {
|
|
64
|
-
version: data.version,
|
|
65
|
-
importedAt: data.importedAt,
|
|
66
|
-
profiles: updatedProfiles,
|
|
67
|
-
},
|
|
68
|
-
updatedProfile,
|
|
69
|
-
previousProfile: profile,
|
|
70
|
-
result: {
|
|
71
|
-
updatedProfileId: profile.id,
|
|
72
|
-
updatedProfileName: profile.name,
|
|
73
|
-
updatedAgents: snapshot.agents.length,
|
|
74
|
-
warnings: snapshot.warnings,
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function logProfileUpdated(plan: ProfileUpdatePlan): void {
|
|
80
|
-
multiProfilesDebugLogger.log("profile-update", {
|
|
81
|
-
event: "profile_updated",
|
|
82
|
-
profileId: plan.updatedProfile.id,
|
|
83
|
-
profileName: plan.updatedProfile.name,
|
|
84
|
-
beforeAgentCount: plan.previousProfile.agents.length,
|
|
85
|
-
afterAgentCount: plan.updatedProfile.agents.length,
|
|
86
|
-
warnings: plan.result.warnings,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Update a profile with the current agent state.
|
|
92
|
-
*
|
|
93
|
-
* Captures the current agent snapshots and replaces the profile's agents array.
|
|
94
|
-
* Preserves profile.id, profile.name, and profile.createdAt.
|
|
95
|
-
* Updates profile.updatedAt timestamp to current time.
|
|
96
|
-
* Logs update event with before/after agent counts via debug logger.
|
|
97
|
-
*
|
|
98
|
-
* @param data - The profiles file data
|
|
99
|
-
* @param profileId - The ID of the profile to update
|
|
100
|
-
* @param agentOptions - Options for capturing agent snapshots
|
|
101
|
-
* @returns ProfileUpdateResult with updated profile info and agent counts
|
|
102
|
-
* @throws ModelProfilesError with code PROFILE_NOT_FOUND if profile doesn't exist
|
|
103
|
-
*/
|
|
104
|
-
export function updateProfile(
|
|
105
|
-
data: ProfilesFile,
|
|
106
|
-
profileId: string,
|
|
107
|
-
agentOptions: AgentSelectionOptions,
|
|
108
|
-
): ProfileUpdateResult {
|
|
109
|
-
const plan = buildUpdatePlan(data, profileId, agentOptions);
|
|
110
|
-
logProfileUpdated(plan);
|
|
111
|
-
return plan.result;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Update a profile and return the updated profiles file with update metadata.
|
|
116
|
-
*
|
|
117
|
-
* @param data - The profiles file data
|
|
118
|
-
* @param profileId - The ID of the profile to update
|
|
119
|
-
* @param agentOptions - Options for capturing agent snapshots
|
|
120
|
-
* @returns New ProfilesFile with the profile updated and update metadata
|
|
121
|
-
* @throws ModelProfilesError with code PROFILE_NOT_FOUND if profile doesn't exist
|
|
122
|
-
*/
|
|
123
|
-
export function updateProfileAndReturn(
|
|
124
|
-
data: ProfilesFile,
|
|
125
|
-
profileId: string,
|
|
126
|
-
agentOptions: AgentSelectionOptions,
|
|
127
|
-
): ProfileUpdateDataResult {
|
|
128
|
-
const plan = buildUpdatePlan(data, profileId, agentOptions);
|
|
129
|
-
logProfileUpdated(plan);
|
|
130
|
-
return {
|
|
131
|
-
data: plan.updatedData,
|
|
132
|
-
result: plan.result,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
1
|
+
import { multiProfilesDebugLogger } from "./debug-logger.js";
|
|
2
|
+
import { ModelProfilesError } from "./errors.js";
|
|
3
|
+
import { captureAgentSnapshots, type AgentSelectionOptions } from "./agent-writer.js";
|
|
4
|
+
import type { ProfilesFile, ProfileUpdateResult, SavedProfile } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Error code for profile not found during update.
|
|
8
|
+
*/
|
|
9
|
+
export const PROFILE_NOT_FOUND_CODE = "PROFILE_NOT_FOUND";
|
|
10
|
+
|
|
11
|
+
export interface ProfileUpdateDataResult {
|
|
12
|
+
data: ProfilesFile;
|
|
13
|
+
result: ProfileUpdateResult;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ProfileUpdatePlan {
|
|
17
|
+
updatedData: ProfilesFile;
|
|
18
|
+
updatedProfile: SavedProfile;
|
|
19
|
+
previousProfile: SavedProfile;
|
|
20
|
+
result: ProfileUpdateResult;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createProfileNotFoundError(profileId: string): ModelProfilesError {
|
|
24
|
+
return new ModelProfilesError(
|
|
25
|
+
`Profile '${profileId}' not found. It may have been removed already.`,
|
|
26
|
+
PROFILE_NOT_FOUND_CODE,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildUpdatePlan(
|
|
31
|
+
data: ProfilesFile,
|
|
32
|
+
profileId: string,
|
|
33
|
+
agentOptions: AgentSelectionOptions,
|
|
34
|
+
): ProfileUpdatePlan {
|
|
35
|
+
const profileIndex = data.profiles.findIndex((profile) => profile.id === profileId);
|
|
36
|
+
const profile = data.profiles[profileIndex];
|
|
37
|
+
|
|
38
|
+
if (profileIndex === -1 || !profile) {
|
|
39
|
+
multiProfilesDebugLogger.log("profile-update", {
|
|
40
|
+
event: "update_failed",
|
|
41
|
+
profileId,
|
|
42
|
+
reason: "profile_not_found",
|
|
43
|
+
profileCount: data.profiles.length,
|
|
44
|
+
});
|
|
45
|
+
throw createProfileNotFoundError(profileId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const snapshot = captureAgentSnapshots(agentOptions);
|
|
49
|
+
const updatedProfile: SavedProfile = {
|
|
50
|
+
id: profile.id,
|
|
51
|
+
name: profile.name,
|
|
52
|
+
agents: snapshot.agents,
|
|
53
|
+
createdAt: profile.createdAt,
|
|
54
|
+
updatedAt: new Date().toISOString(),
|
|
55
|
+
};
|
|
56
|
+
const updatedProfiles = [
|
|
57
|
+
...data.profiles.slice(0, profileIndex),
|
|
58
|
+
updatedProfile,
|
|
59
|
+
...data.profiles.slice(profileIndex + 1),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
updatedData: {
|
|
64
|
+
version: data.version,
|
|
65
|
+
importedAt: data.importedAt,
|
|
66
|
+
profiles: updatedProfiles,
|
|
67
|
+
},
|
|
68
|
+
updatedProfile,
|
|
69
|
+
previousProfile: profile,
|
|
70
|
+
result: {
|
|
71
|
+
updatedProfileId: profile.id,
|
|
72
|
+
updatedProfileName: profile.name,
|
|
73
|
+
updatedAgents: snapshot.agents.length,
|
|
74
|
+
warnings: snapshot.warnings,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function logProfileUpdated(plan: ProfileUpdatePlan): void {
|
|
80
|
+
multiProfilesDebugLogger.log("profile-update", {
|
|
81
|
+
event: "profile_updated",
|
|
82
|
+
profileId: plan.updatedProfile.id,
|
|
83
|
+
profileName: plan.updatedProfile.name,
|
|
84
|
+
beforeAgentCount: plan.previousProfile.agents.length,
|
|
85
|
+
afterAgentCount: plan.updatedProfile.agents.length,
|
|
86
|
+
warnings: plan.result.warnings,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Update a profile with the current agent state.
|
|
92
|
+
*
|
|
93
|
+
* Captures the current agent snapshots and replaces the profile's agents array.
|
|
94
|
+
* Preserves profile.id, profile.name, and profile.createdAt.
|
|
95
|
+
* Updates profile.updatedAt timestamp to current time.
|
|
96
|
+
* Logs update event with before/after agent counts via debug logger.
|
|
97
|
+
*
|
|
98
|
+
* @param data - The profiles file data
|
|
99
|
+
* @param profileId - The ID of the profile to update
|
|
100
|
+
* @param agentOptions - Options for capturing agent snapshots
|
|
101
|
+
* @returns ProfileUpdateResult with updated profile info and agent counts
|
|
102
|
+
* @throws ModelProfilesError with code PROFILE_NOT_FOUND if profile doesn't exist
|
|
103
|
+
*/
|
|
104
|
+
export function updateProfile(
|
|
105
|
+
data: ProfilesFile,
|
|
106
|
+
profileId: string,
|
|
107
|
+
agentOptions: AgentSelectionOptions,
|
|
108
|
+
): ProfileUpdateResult {
|
|
109
|
+
const plan = buildUpdatePlan(data, profileId, agentOptions);
|
|
110
|
+
logProfileUpdated(plan);
|
|
111
|
+
return plan.result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Update a profile and return the updated profiles file with update metadata.
|
|
116
|
+
*
|
|
117
|
+
* @param data - The profiles file data
|
|
118
|
+
* @param profileId - The ID of the profile to update
|
|
119
|
+
* @param agentOptions - Options for capturing agent snapshots
|
|
120
|
+
* @returns New ProfilesFile with the profile updated and update metadata
|
|
121
|
+
* @throws ModelProfilesError with code PROFILE_NOT_FOUND if profile doesn't exist
|
|
122
|
+
*/
|
|
123
|
+
export function updateProfileAndReturn(
|
|
124
|
+
data: ProfilesFile,
|
|
125
|
+
profileId: string,
|
|
126
|
+
agentOptions: AgentSelectionOptions,
|
|
127
|
+
): ProfileUpdateDataResult {
|
|
128
|
+
const plan = buildUpdatePlan(data, profileId, agentOptions);
|
|
129
|
+
logProfileUpdated(plan);
|
|
130
|
+
return {
|
|
131
|
+
data: plan.updatedData,
|
|
132
|
+
result: plan.result,
|
|
133
|
+
};
|
|
134
|
+
}
|