pi-model-profiles 0.3.2 → 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/src/index.ts CHANGED
@@ -1,130 +1,7 @@
1
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
 
3
- import { COMMAND_NAME, INITIAL_PROFILE_NAME, PROFILE_NAME_SUFFIX, PROFILE_STORE_PATH } from "./constants.js";
4
- import { applySavedProfile, captureAgentSnapshots, detectActiveAgentName } from "./agent-writer.js";
5
- import { toErrorMessage } from "./errors.js";
6
- import { loadAndPrepareProfiles } from "./import-service.js";
7
- import { updateProfileAndReturn } from "./profile-update-service.js";
8
- import { appendProfile, createProfile, findProfileById, renameStoredProfile, resolveUniqueProfileName, saveProfilesFile } from "./profile-store.js";
9
- import { removeProfileAndUpdate } from "./profile-removal-service.js";
10
- import { openProfilesModal, type ProfileModalResult } from "./profile-modal.js";
3
+ import { COMMAND_NAME, toErrorMessage } from "./errors.js";
11
4
  import { onResourcesDiscover, refreshModelRegistry } from "./pi-api-utils.js";
12
- import type { AppliedProfileOutcome, ProfilesFile } from "./types.js";
13
-
14
- function buildCurrentProfileName(activeAgentName: string | null, data: ProfilesFile): string {
15
- const baseName = activeAgentName ? `${activeAgentName} ${PROFILE_NAME_SUFFIX}` : INITIAL_PROFILE_NAME;
16
- return resolveUniqueProfileName(baseName, data.profiles);
17
- }
18
-
19
- function notifyWarnings(ctx: ExtensionCommandContext, warnings: readonly string[]): void {
20
- if (warnings.length === 0) {
21
- return;
22
- }
23
- ctx.ui.notify(warnings.join(" "), "warning");
24
- }
25
-
26
- function summarizeApplyOutcome(outcome: AppliedProfileOutcome): string {
27
- const appliedCount = outcome.appliedAgents.length;
28
- const missingCount = outcome.missingAgents.length;
29
- const appliedLabel = `${appliedCount} agent file${appliedCount === 1 ? "" : "s"}`;
30
- if (missingCount === 0) {
31
- return appliedLabel;
32
- }
33
- return `${appliedLabel}; ${missingCount} missing`;
34
- }
35
-
36
- async function reloadAfterApply(ctx: ExtensionCommandContext, outcome: AppliedProfileOutcome): Promise<void> {
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
5
 
129
6
  export default function modelProfilesExtension(pi: ExtensionAPI): void {
130
7
  // Register handler to refresh model registry on /reload
@@ -149,6 +26,7 @@ export default function modelProfilesExtension(pi: ExtensionAPI): void {
149
26
  }
150
27
 
151
28
  try {
29
+ const { handleModelProfilesCommand } = await import("./command-handler.js");
152
30
  await handleModelProfilesCommand(ctx);
153
31
  } catch (error) {
154
32
  ctx.ui.notify(`/${COMMAND_NAME} failed: ${toErrorMessage(error)}`, "error");
@@ -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
- function normalizeTemperatureValue(value: unknown): number | undefined {
19
- if (typeof value === "number") {
20
- return Number.isFinite(value) ? value : undefined;
21
- }
22
- if (typeof value === "string" && value.trim()) {
23
- const parsed = Number.parseFloat(value);
24
- return Number.isFinite(parsed) ? parsed : undefined;
25
- }
26
- return undefined;
27
- }
28
-
29
- export function normalizeProfileFields(value: unknown): ProfileFields {
30
- const source = toRecord(value);
31
- const fields: ProfileFields = {};
32
-
33
- const model = normalizeOptionalString(source.model);
34
- if (model) {
35
- fields.model = model;
36
- }
37
-
38
- const temperature = normalizeTemperatureValue(source.temperature);
39
- if (temperature !== undefined) {
40
- fields.temperature = temperature;
41
- }
42
-
43
- const reasoningEffort = normalizeOptionalString(source.reasoningEffort);
44
- if (reasoningEffort) {
45
- fields.reasoningEffort = reasoningEffort;
46
- }
47
-
48
- return fields;
49
- }
50
-
51
- export function hasProfileFields(fields: ProfileFields): boolean {
52
- return fields.model !== undefined || fields.temperature !== undefined || fields.reasoningEffort !== undefined;
53
- }
54
-
55
- export function describeProfileFields(fields: ProfileFields): string {
56
- const parts: string[] = [];
57
- if (fields.model !== undefined) {
58
- parts.push("model");
59
- }
60
- if (fields.temperature !== undefined) {
61
- parts.push("temperature");
62
- }
63
- if (fields.reasoningEffort !== undefined) {
64
- parts.push("reasoning");
65
- }
66
- return parts.length > 0 ? parts.join(", ") : "clears model overrides";
67
- }
68
-
69
- export function formatProfileFieldValue(key: ProfileFieldKey, fields: ProfileFields): string {
70
- switch (key) {
71
- case "model":
72
- return fields.model ?? "(absent)";
73
- case "temperature":
74
- return fields.temperature !== undefined ? String(fields.temperature) : "(absent)";
75
- case "reasoningEffort":
76
- return fields.reasoningEffort ?? "(absent)";
77
- }
78
- }
79
-
80
- export function sanitizeProfileName(value: string): string | null {
81
- const trimmed = value.trim();
82
- return trimmed ? trimmed : null;
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
+ }
@@ -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
+ }