pi-model-profiles 0.3.1 → 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 CHANGED
@@ -1,48 +1,59 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ## [0.3.1] - 2026-05-22
11
-
12
- ### Changed
13
- - Reworked debug logging to redact sensitive values and use asynchronous buffered file writes with safe shutdown.
14
- - Updated Pi peer dependencies and runtime imports to the `@earendil-works` scope.
15
-
16
- ### Fixed
17
- - Improved debug log writer lifecycle handling so buffered events flush reliably without opening logs when debug is disabled.
18
-
19
- ## [0.3.0] - 2026-04-30
20
-
21
- ### Changed
22
- - Refined the model profiles modal layout with wider sizing, a single bordered grid, and clearer model table columns.
23
- - Updated the public README screenshot and usage details.
24
- - Bumped Pi peer dependency ranges to `^0.70.6`.
25
-
26
- ## [0.2.0] - 2026-04-26
27
-
28
- ### Added
29
- - Phase 6: Git & publishing preparation.
30
- - NPM package metadata, README, CHANGELOG, LICENSE, and package ignore rules.
31
- - Profile update, removal, persisted sorting, configuration, and file-gated debug logging.
32
-
33
- ### Fixed
34
- - Confirmation prompts now accept typed input before update or removal actions run.
35
- - Sort menu keyboard handling now works consistently and closes without exiting the modal.
36
- - Profile update and removal command handlers now avoid duplicate scans and duplicate removal events.
37
-
38
- ## [0.1.0] - 2026-04-25
39
-
40
- ### Added
41
- - Initial extension structure
42
- - Core profile management functionality
43
- - Frontmatter parser implementation
44
- - Profile store with atomic writes
45
- - Import service for external profiles
46
- - Agent writer for profile application
47
- - Type definitions and error handling
48
- - Test suite for core functionality
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
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
+
16
+ ## [0.3.2] - 2026-05-26
17
+
18
+ ### Changed
19
+ - Widened peer dependency ranges to `^0.74.0 || ^0.75.0`.
20
+
21
+ ## [0.3.1] - 2026-05-22
22
+
23
+ ### Changed
24
+ - Reworked debug logging to redact sensitive values and use asynchronous buffered file writes with safe shutdown.
25
+ - Updated Pi peer dependencies and runtime imports to the `@earendil-works` scope.
26
+
27
+ ### Fixed
28
+ - Improved debug log writer lifecycle handling so buffered events flush reliably without opening logs when debug is disabled.
29
+
30
+ ## [0.3.0] - 2026-04-30
31
+
32
+ ### Changed
33
+ - Refined the model profiles modal layout with wider sizing, a single bordered grid, and clearer model table columns.
34
+ - Updated the public README screenshot and usage details.
35
+ - Bumped Pi peer dependency ranges to `^0.70.6`.
36
+
37
+ ## [0.2.0] - 2026-04-26
38
+
39
+ ### Added
40
+ - Phase 6: Git & publishing preparation.
41
+ - NPM package metadata, README, CHANGELOG, LICENSE, and package ignore rules.
42
+ - Profile update, removal, persisted sorting, configuration, and file-gated debug logging.
43
+
44
+ ### Fixed
45
+ - Confirmation prompts now accept typed input before update or removal actions run.
46
+ - Sort menu keyboard handling now works consistently and closes without exiting the modal.
47
+ - Profile update and removal command handlers now avoid duplicate scans and duplicate removal events.
48
+
49
+ ## [0.1.0] - 2026-04-25
50
+
51
+ ### Added
52
+ - Initial extension structure
53
+ - Core profile management functionality
54
+ - Frontmatter parser implementation
55
+ - Profile store with atomic writes
56
+ - Import service for external profiles
57
+ - Agent writer for profile application
58
+ - Type definitions and error handling
59
+ - Test suite for core functionality
package/package.json CHANGED
@@ -1,66 +1,66 @@
1
- {
2
- "name": "pi-model-profiles",
3
- "version": "0.3.1",
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.75.4",
61
- "@earendil-works/pi-tui": "^0.75.4"
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
@@ -1,3 +1,5 @@
1
+ export const COMMAND_NAME = "model-profiles";
2
+
1
3
  export class ModelProfilesError extends Error {
2
4
  readonly code: string;
3
5
 
@@ -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
+ }