@mariozechner/pi-coding-agent 0.27.9 → 0.29.0

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.
Files changed (96) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/README.md +17 -18
  3. package/dist/cli/list-models.d.ts +2 -2
  4. package/dist/cli/list-models.d.ts.map +1 -1
  5. package/dist/cli/list-models.js +2 -7
  6. package/dist/cli/list-models.js.map +1 -1
  7. package/dist/config.d.ts +2 -2
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +3 -3
  10. package/dist/config.js.map +1 -1
  11. package/dist/core/agent-session.d.ts +6 -3
  12. package/dist/core/agent-session.d.ts.map +1 -1
  13. package/dist/core/agent-session.js +23 -25
  14. package/dist/core/agent-session.js.map +1 -1
  15. package/dist/core/auth-storage.d.ts +104 -0
  16. package/dist/core/auth-storage.d.ts.map +1 -0
  17. package/dist/core/auth-storage.js +232 -0
  18. package/dist/core/auth-storage.js.map +1 -0
  19. package/dist/core/custom-tools/types.d.ts +2 -2
  20. package/dist/core/custom-tools/types.d.ts.map +1 -1
  21. package/dist/core/custom-tools/types.js.map +1 -1
  22. package/dist/core/hooks/types.d.ts +3 -3
  23. package/dist/core/hooks/types.d.ts.map +1 -1
  24. package/dist/core/hooks/types.js.map +1 -1
  25. package/dist/core/model-registry.d.ts +50 -0
  26. package/dist/core/model-registry.d.ts.map +1 -0
  27. package/dist/core/model-registry.js +268 -0
  28. package/dist/core/model-registry.js.map +1 -0
  29. package/dist/core/model-resolver.d.ts +7 -7
  30. package/dist/core/model-resolver.d.ts.map +1 -1
  31. package/dist/core/model-resolver.js +12 -44
  32. package/dist/core/model-resolver.js.map +1 -1
  33. package/dist/core/sdk.d.ts +13 -26
  34. package/dist/core/sdk.d.ts.map +1 -1
  35. package/dist/core/sdk.js +24 -101
  36. package/dist/core/sdk.js.map +1 -1
  37. package/dist/core/settings-manager.d.ts +0 -5
  38. package/dist/core/settings-manager.d.ts.map +1 -1
  39. package/dist/core/settings-manager.js +0 -19
  40. package/dist/core/settings-manager.js.map +1 -1
  41. package/dist/core/skills.d.ts.map +1 -1
  42. package/dist/core/skills.js +15 -1
  43. package/dist/core/skills.js.map +1 -1
  44. package/dist/index.d.ts +3 -3
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +4 -8
  47. package/dist/index.js.map +1 -1
  48. package/dist/main.d.ts.map +1 -1
  49. package/dist/main.js +37 -22
  50. package/dist/main.js.map +1 -1
  51. package/dist/modes/interactive/components/footer.d.ts +3 -1
  52. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  53. package/dist/modes/interactive/components/footer.js +4 -3
  54. package/dist/modes/interactive/components/footer.js.map +1 -1
  55. package/dist/modes/interactive/components/model-selector.d.ts +3 -1
  56. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  57. package/dist/modes/interactive/components/model-selector.js +21 -14
  58. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  59. package/dist/modes/interactive/components/oauth-selector.d.ts +3 -1
  60. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  61. package/dist/modes/interactive/components/oauth-selector.js +6 -6
  62. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  63. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  64. package/dist/modes/interactive/interactive-mode.js +56 -51
  65. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  66. package/docs/custom-tools.md +3 -3
  67. package/docs/hooks.md +9 -9
  68. package/docs/sdk.md +86 -61
  69. package/examples/custom-tools/hello/index.ts +15 -15
  70. package/examples/custom-tools/question/index.ts +3 -3
  71. package/examples/custom-tools/subagent/agents.ts +1 -2
  72. package/examples/custom-tools/subagent/index.ts +332 -125
  73. package/examples/custom-tools/todo/index.ts +30 -12
  74. package/examples/hooks/confirm-destructive.ts +6 -8
  75. package/examples/hooks/custom-compaction.ts +7 -7
  76. package/examples/hooks/dirty-repo-guard.ts +7 -15
  77. package/examples/hooks/permission-gate.ts +1 -5
  78. package/examples/sdk/02-custom-model.ts +20 -7
  79. package/examples/sdk/04-skills.ts +1 -1
  80. package/examples/sdk/05-tools.ts +11 -14
  81. package/examples/sdk/06-hooks.ts +1 -1
  82. package/examples/sdk/07-context-files.ts +1 -1
  83. package/examples/sdk/08-slash-commands.ts +3 -3
  84. package/examples/sdk/09-api-keys-and-oauth.ts +36 -26
  85. package/examples/sdk/10-settings.ts +2 -2
  86. package/examples/sdk/12-full-control.ts +19 -20
  87. package/examples/sdk/README.md +26 -13
  88. package/package.json +4 -5
  89. package/dist/core/model-config.d.ts +0 -58
  90. package/dist/core/model-config.d.ts.map +0 -1
  91. package/dist/core/model-config.js +0 -384
  92. package/dist/core/model-config.js.map +0 -1
  93. package/dist/core/oauth/index.d.ts +0 -41
  94. package/dist/core/oauth/index.d.ts.map +0 -1
  95. package/dist/core/oauth/index.js +0 -84
  96. package/dist/core/oauth/index.js.map +0 -1
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Credential storage for API keys and OAuth tokens.
3
+ * Handles loading, saving, and refreshing credentials from auth.json.
4
+ */
5
+ import { getEnvApiKey, getOAuthApiKey, loginAnthropic, loginAntigravity, loginGeminiCli, loginGitHubCopilot, } from "@mariozechner/pi-ai";
6
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
7
+ import { dirname, join } from "path";
8
+ /**
9
+ * Credential storage backed by a JSON file.
10
+ */
11
+ export class AuthStorage {
12
+ authPath;
13
+ data = {};
14
+ runtimeOverrides = new Map();
15
+ fallbackResolver;
16
+ constructor(authPath) {
17
+ this.authPath = authPath;
18
+ this.reload();
19
+ }
20
+ /**
21
+ * Set a runtime API key override (not persisted to disk).
22
+ * Used for CLI --api-key flag.
23
+ */
24
+ setRuntimeApiKey(provider, apiKey) {
25
+ this.runtimeOverrides.set(provider, apiKey);
26
+ }
27
+ /**
28
+ * Remove a runtime API key override.
29
+ */
30
+ removeRuntimeApiKey(provider) {
31
+ this.runtimeOverrides.delete(provider);
32
+ }
33
+ /**
34
+ * Set a fallback resolver for API keys not found in auth.json or env vars.
35
+ * Used for custom provider keys from models.json.
36
+ */
37
+ setFallbackResolver(resolver) {
38
+ this.fallbackResolver = resolver;
39
+ }
40
+ /**
41
+ * Reload credentials from disk.
42
+ */
43
+ reload() {
44
+ if (!existsSync(this.authPath)) {
45
+ this.data = {};
46
+ return;
47
+ }
48
+ try {
49
+ this.data = JSON.parse(readFileSync(this.authPath, "utf-8"));
50
+ }
51
+ catch {
52
+ this.data = {};
53
+ }
54
+ }
55
+ /**
56
+ * Save credentials to disk.
57
+ */
58
+ save() {
59
+ const dir = dirname(this.authPath);
60
+ if (!existsSync(dir)) {
61
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
62
+ }
63
+ writeFileSync(this.authPath, JSON.stringify(this.data, null, 2), "utf-8");
64
+ chmodSync(this.authPath, 0o600);
65
+ }
66
+ /**
67
+ * Get credential for a provider.
68
+ */
69
+ get(provider) {
70
+ return this.data[provider] ?? null;
71
+ }
72
+ /**
73
+ * Set credential for a provider.
74
+ */
75
+ set(provider, credential) {
76
+ this.data[provider] = credential;
77
+ this.save();
78
+ }
79
+ /**
80
+ * Remove credential for a provider.
81
+ */
82
+ remove(provider) {
83
+ delete this.data[provider];
84
+ this.save();
85
+ }
86
+ /**
87
+ * List all providers with credentials.
88
+ */
89
+ list() {
90
+ return Object.keys(this.data);
91
+ }
92
+ /**
93
+ * Check if credentials exist for a provider.
94
+ */
95
+ has(provider) {
96
+ return provider in this.data;
97
+ }
98
+ /**
99
+ * Get all credentials (for passing to getOAuthApiKey).
100
+ */
101
+ getAll() {
102
+ return { ...this.data };
103
+ }
104
+ /**
105
+ * Login to an OAuth provider.
106
+ */
107
+ async login(provider, callbacks) {
108
+ let credentials;
109
+ switch (provider) {
110
+ case "anthropic":
111
+ credentials = await loginAnthropic((url) => callbacks.onAuth({ url }), () => callbacks.onPrompt({ message: "Paste the authorization code:" }));
112
+ break;
113
+ case "github-copilot":
114
+ credentials = await loginGitHubCopilot({
115
+ onAuth: (url, instructions) => callbacks.onAuth({ url, instructions }),
116
+ onPrompt: callbacks.onPrompt,
117
+ onProgress: callbacks.onProgress,
118
+ });
119
+ break;
120
+ case "google-gemini-cli":
121
+ credentials = await loginGeminiCli(callbacks.onAuth, callbacks.onProgress);
122
+ break;
123
+ case "google-antigravity":
124
+ credentials = await loginAntigravity(callbacks.onAuth, callbacks.onProgress);
125
+ break;
126
+ default:
127
+ throw new Error(`Unknown OAuth provider: ${provider}`);
128
+ }
129
+ this.set(provider, { type: "oauth", ...credentials });
130
+ }
131
+ /**
132
+ * Logout from a provider.
133
+ */
134
+ logout(provider) {
135
+ this.remove(provider);
136
+ }
137
+ /**
138
+ * Get API key for a provider.
139
+ * Priority:
140
+ * 1. Runtime override (CLI --api-key)
141
+ * 2. API key from auth.json
142
+ * 3. OAuth token from auth.json (auto-refreshed)
143
+ * 4. Environment variable
144
+ * 5. Fallback resolver (models.json custom providers)
145
+ */
146
+ async getApiKey(provider) {
147
+ // Runtime override takes highest priority
148
+ const runtimeKey = this.runtimeOverrides.get(provider);
149
+ if (runtimeKey) {
150
+ return runtimeKey;
151
+ }
152
+ const cred = this.data[provider];
153
+ if (cred?.type === "api_key") {
154
+ return cred.key;
155
+ }
156
+ if (cred?.type === "oauth") {
157
+ // Filter to only oauth credentials for getOAuthApiKey
158
+ const oauthCreds = {};
159
+ for (const [key, value] of Object.entries(this.data)) {
160
+ if (value.type === "oauth") {
161
+ oauthCreds[key] = value;
162
+ }
163
+ }
164
+ try {
165
+ const result = await getOAuthApiKey(provider, oauthCreds);
166
+ if (result) {
167
+ this.data[provider] = { type: "oauth", ...result.newCredentials };
168
+ this.save();
169
+ return result.apiKey;
170
+ }
171
+ }
172
+ catch {
173
+ this.remove(provider);
174
+ }
175
+ }
176
+ // Fall back to environment variable
177
+ const envKey = getEnvApiKey(provider);
178
+ if (envKey)
179
+ return envKey;
180
+ // Fall back to custom resolver (e.g., models.json custom providers)
181
+ return this.fallbackResolver?.(provider) ?? null;
182
+ }
183
+ /**
184
+ * Migrate credentials from legacy oauth.json and settings.json apiKeys to auth.json.
185
+ * Only runs if auth.json doesn't exist yet. Returns list of migrated providers.
186
+ */
187
+ static migrateLegacy(authPath, agentDir) {
188
+ const oauthPath = join(agentDir, "oauth.json");
189
+ const settingsPath = join(agentDir, "settings.json");
190
+ // Skip if auth.json already exists
191
+ if (existsSync(authPath))
192
+ return [];
193
+ const migrated = {};
194
+ const providers = [];
195
+ // Migrate oauth.json
196
+ if (existsSync(oauthPath)) {
197
+ try {
198
+ const oauth = JSON.parse(readFileSync(oauthPath, "utf-8"));
199
+ for (const [provider, cred] of Object.entries(oauth)) {
200
+ migrated[provider] = { type: "oauth", ...cred };
201
+ providers.push(provider);
202
+ }
203
+ renameSync(oauthPath, `${oauthPath}.migrated`);
204
+ }
205
+ catch { }
206
+ }
207
+ // Migrate settings.json apiKeys
208
+ if (existsSync(settingsPath)) {
209
+ try {
210
+ const content = readFileSync(settingsPath, "utf-8");
211
+ const settings = JSON.parse(content);
212
+ if (settings.apiKeys && typeof settings.apiKeys === "object") {
213
+ for (const [provider, key] of Object.entries(settings.apiKeys)) {
214
+ if (!migrated[provider] && typeof key === "string") {
215
+ migrated[provider] = { type: "api_key", key };
216
+ providers.push(provider);
217
+ }
218
+ }
219
+ delete settings.apiKeys;
220
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
221
+ }
222
+ }
223
+ catch { }
224
+ }
225
+ if (Object.keys(migrated).length > 0) {
226
+ mkdirSync(dirname(authPath), { recursive: true });
227
+ writeFileSync(authPath, JSON.stringify(migrated, null, 2), { mode: 0o600 });
228
+ }
229
+ return providers;
230
+ }
231
+ }
232
+ //# sourceMappingURL=auth-storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-storage.js","sourceRoot":"","sources":["../../src/core/auth-storage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Credential storage for API keys and OAuth tokens.\n * Handles loading, saving, and refreshing credentials from auth.json.\n */\n\nimport {\n\tgetEnvApiKey,\n\tgetOAuthApiKey,\n\tloginAnthropic,\n\tloginAntigravity,\n\tloginGeminiCli,\n\tloginGitHubCopilot,\n\ttype OAuthCredentials,\n\ttype OAuthProvider,\n} from \"@mariozechner/pi-ai\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\n\nexport type ApiKeyCredential = {\n\ttype: \"api_key\";\n\tkey: string;\n};\n\nexport type OAuthCredential = {\n\ttype: \"oauth\";\n} & OAuthCredentials;\n\nexport type AuthCredential = ApiKeyCredential | OAuthCredential;\n\nexport type AuthStorageData = Record<string, AuthCredential>;\n\n/**\n * Credential storage backed by a JSON file.\n */\nexport class AuthStorage {\n\tprivate data: AuthStorageData = {};\n\tprivate runtimeOverrides: Map<string, string> = new Map();\n\tprivate fallbackResolver?: (provider: string) => string | undefined;\n\n\tconstructor(private authPath: string) {\n\t\tthis.reload();\n\t}\n\n\t/**\n\t * Set a runtime API key override (not persisted to disk).\n\t * Used for CLI --api-key flag.\n\t */\n\tsetRuntimeApiKey(provider: string, apiKey: string): void {\n\t\tthis.runtimeOverrides.set(provider, apiKey);\n\t}\n\n\t/**\n\t * Remove a runtime API key override.\n\t */\n\tremoveRuntimeApiKey(provider: string): void {\n\t\tthis.runtimeOverrides.delete(provider);\n\t}\n\n\t/**\n\t * Set a fallback resolver for API keys not found in auth.json or env vars.\n\t * Used for custom provider keys from models.json.\n\t */\n\tsetFallbackResolver(resolver: (provider: string) => string | undefined): void {\n\t\tthis.fallbackResolver = resolver;\n\t}\n\n\t/**\n\t * Reload credentials from disk.\n\t */\n\treload(): void {\n\t\tif (!existsSync(this.authPath)) {\n\t\t\tthis.data = {};\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tthis.data = JSON.parse(readFileSync(this.authPath, \"utf-8\"));\n\t\t} catch {\n\t\t\tthis.data = {};\n\t\t}\n\t}\n\n\t/**\n\t * Save credentials to disk.\n\t */\n\tprivate save(): void {\n\t\tconst dir = dirname(this.authPath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\twriteFileSync(this.authPath, JSON.stringify(this.data, null, 2), \"utf-8\");\n\t\tchmodSync(this.authPath, 0o600);\n\t}\n\n\t/**\n\t * Get credential for a provider.\n\t */\n\tget(provider: string): AuthCredential | null {\n\t\treturn this.data[provider] ?? null;\n\t}\n\n\t/**\n\t * Set credential for a provider.\n\t */\n\tset(provider: string, credential: AuthCredential): void {\n\t\tthis.data[provider] = credential;\n\t\tthis.save();\n\t}\n\n\t/**\n\t * Remove credential for a provider.\n\t */\n\tremove(provider: string): void {\n\t\tdelete this.data[provider];\n\t\tthis.save();\n\t}\n\n\t/**\n\t * List all providers with credentials.\n\t */\n\tlist(): string[] {\n\t\treturn Object.keys(this.data);\n\t}\n\n\t/**\n\t * Check if credentials exist for a provider.\n\t */\n\thas(provider: string): boolean {\n\t\treturn provider in this.data;\n\t}\n\n\t/**\n\t * Get all credentials (for passing to getOAuthApiKey).\n\t */\n\tgetAll(): AuthStorageData {\n\t\treturn { ...this.data };\n\t}\n\n\t/**\n\t * Login to an OAuth provider.\n\t */\n\tasync login(\n\t\tprovider: OAuthProvider,\n\t\tcallbacks: {\n\t\t\tonAuth: (info: { url: string; instructions?: string }) => void;\n\t\t\tonPrompt: (prompt: { message: string; placeholder?: string }) => Promise<string>;\n\t\t\tonProgress?: (message: string) => void;\n\t\t},\n\t): Promise<void> {\n\t\tlet credentials: OAuthCredentials;\n\n\t\tswitch (provider) {\n\t\t\tcase \"anthropic\":\n\t\t\t\tcredentials = await loginAnthropic(\n\t\t\t\t\t(url) => callbacks.onAuth({ url }),\n\t\t\t\t\t() => callbacks.onPrompt({ message: \"Paste the authorization code:\" }),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"github-copilot\":\n\t\t\t\tcredentials = await loginGitHubCopilot({\n\t\t\t\t\tonAuth: (url, instructions) => callbacks.onAuth({ url, instructions }),\n\t\t\t\t\tonPrompt: callbacks.onPrompt,\n\t\t\t\t\tonProgress: callbacks.onProgress,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\tcase \"google-gemini-cli\":\n\t\t\t\tcredentials = await loginGeminiCli(callbacks.onAuth, callbacks.onProgress);\n\t\t\t\tbreak;\n\t\t\tcase \"google-antigravity\":\n\t\t\t\tcredentials = await loginAntigravity(callbacks.onAuth, callbacks.onProgress);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unknown OAuth provider: ${provider}`);\n\t\t}\n\n\t\tthis.set(provider, { type: \"oauth\", ...credentials });\n\t}\n\n\t/**\n\t * Logout from a provider.\n\t */\n\tlogout(provider: string): void {\n\t\tthis.remove(provider);\n\t}\n\n\t/**\n\t * Get API key for a provider.\n\t * Priority:\n\t * 1. Runtime override (CLI --api-key)\n\t * 2. API key from auth.json\n\t * 3. OAuth token from auth.json (auto-refreshed)\n\t * 4. Environment variable\n\t * 5. Fallback resolver (models.json custom providers)\n\t */\n\tasync getApiKey(provider: string): Promise<string | null> {\n\t\t// Runtime override takes highest priority\n\t\tconst runtimeKey = this.runtimeOverrides.get(provider);\n\t\tif (runtimeKey) {\n\t\t\treturn runtimeKey;\n\t\t}\n\n\t\tconst cred = this.data[provider];\n\n\t\tif (cred?.type === \"api_key\") {\n\t\t\treturn cred.key;\n\t\t}\n\n\t\tif (cred?.type === \"oauth\") {\n\t\t\t// Filter to only oauth credentials for getOAuthApiKey\n\t\t\tconst oauthCreds: Record<string, OAuthCredentials> = {};\n\t\t\tfor (const [key, value] of Object.entries(this.data)) {\n\t\t\t\tif (value.type === \"oauth\") {\n\t\t\t\t\toauthCreds[key] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst result = await getOAuthApiKey(provider as OAuthProvider, oauthCreds);\n\t\t\t\tif (result) {\n\t\t\t\t\tthis.data[provider] = { type: \"oauth\", ...result.newCredentials };\n\t\t\t\t\tthis.save();\n\t\t\t\t\treturn result.apiKey;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.remove(provider);\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to environment variable\n\t\tconst envKey = getEnvApiKey(provider);\n\t\tif (envKey) return envKey;\n\n\t\t// Fall back to custom resolver (e.g., models.json custom providers)\n\t\treturn this.fallbackResolver?.(provider) ?? null;\n\t}\n\n\t/**\n\t * Migrate credentials from legacy oauth.json and settings.json apiKeys to auth.json.\n\t * Only runs if auth.json doesn't exist yet. Returns list of migrated providers.\n\t */\n\tstatic migrateLegacy(authPath: string, agentDir: string): string[] {\n\t\tconst oauthPath = join(agentDir, \"oauth.json\");\n\t\tconst settingsPath = join(agentDir, \"settings.json\");\n\n\t\t// Skip if auth.json already exists\n\t\tif (existsSync(authPath)) return [];\n\n\t\tconst migrated: AuthStorageData = {};\n\t\tconst providers: string[] = [];\n\n\t\t// Migrate oauth.json\n\t\tif (existsSync(oauthPath)) {\n\t\t\ttry {\n\t\t\t\tconst oauth = JSON.parse(readFileSync(oauthPath, \"utf-8\"));\n\t\t\t\tfor (const [provider, cred] of Object.entries(oauth)) {\n\t\t\t\t\tmigrated[provider] = { type: \"oauth\", ...(cred as object) } as OAuthCredential;\n\t\t\t\t\tproviders.push(provider);\n\t\t\t\t}\n\t\t\t\trenameSync(oauthPath, `${oauthPath}.migrated`);\n\t\t\t} catch {}\n\t\t}\n\n\t\t// Migrate settings.json apiKeys\n\t\tif (existsSync(settingsPath)) {\n\t\t\ttry {\n\t\t\t\tconst content = readFileSync(settingsPath, \"utf-8\");\n\t\t\t\tconst settings = JSON.parse(content);\n\t\t\t\tif (settings.apiKeys && typeof settings.apiKeys === \"object\") {\n\t\t\t\t\tfor (const [provider, key] of Object.entries(settings.apiKeys)) {\n\t\t\t\t\t\tif (!migrated[provider] && typeof key === \"string\") {\n\t\t\t\t\t\t\tmigrated[provider] = { type: \"api_key\", key };\n\t\t\t\t\t\t\tproviders.push(provider);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdelete settings.apiKeys;\n\t\t\t\t\twriteFileSync(settingsPath, JSON.stringify(settings, null, 2));\n\t\t\t\t}\n\t\t\t} catch {}\n\t\t}\n\n\t\tif (Object.keys(migrated).length > 0) {\n\t\t\tmkdirSync(dirname(authPath), { recursive: true });\n\t\t\twriteFileSync(authPath, JSON.stringify(migrated, null, 2), { mode: 0o600 });\n\t\t}\n\n\t\treturn providers;\n\t}\n}\n"]}
@@ -44,10 +44,10 @@ export interface SessionEvent {
44
44
  entries: SessionEntry[];
45
45
  /** Current session file path, or null in --no-session mode */
46
46
  sessionFile: string | null;
47
- /** Previous session file path, or null for "start" and "clear" */
47
+ /** Previous session file path, or null for "start" and "new" */
48
48
  previousSessionFile: string | null;
49
49
  /** Reason for the session event */
50
- reason: "start" | "switch" | "branch" | "clear";
50
+ reason: "start" | "switch" | "branch" | "new";
51
51
  }
52
52
  /** Rendering options passed to renderResult */
53
53
  export interface RenderResultOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/custom-tools/types.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom tool types.\n *\n * Custom tools are TypeScript modules that define additional tools for the agent.\n * They can provide custom rendering for tool calls and results in the TUI.\n */\n\nimport type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from \"@mariozechner/pi-ai\";\nimport type { Component } from \"@mariozechner/pi-tui\";\nimport type { Static, TSchema } from \"@sinclair/typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { SessionEntry } from \"../session-manager.js\";\n\n/** Alias for clarity */\nexport type ToolUIContext = HookUIContext;\n\n/** Re-export for custom tools to use in execute signature */\nexport type { AgentToolUpdateCallback };\n\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\t/** True if the process was killed due to signal or timeout */\n\tkilled?: boolean;\n}\n\nexport interface ExecOptions {\n\t/** AbortSignal to cancel the process */\n\tsignal?: AbortSignal;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n}\n\n/** API passed to custom tool factory (stable across session changes) */\nexport interface ToolAPI {\n\t/** Current working directory */\n\tcwd: string;\n\t/** Execute a command */\n\texec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;\n\t/** UI methods for user interaction (select, confirm, input, notify) */\n\tui: ToolUIContext;\n\t/** Whether UI is available (false in print/RPC mode) */\n\thasUI: boolean;\n}\n\n/** Session event passed to onSession callback */\nexport interface SessionEvent {\n\t/** All session entries (including pre-compaction history) */\n\tentries: SessionEntry[];\n\t/** Current session file path, or null in --no-session mode */\n\tsessionFile: string | null;\n\t/** Previous session file path, or null for \"start\" and \"clear\" */\n\tpreviousSessionFile: string | null;\n\t/** Reason for the session event */\n\treason: \"start\" | \"switch\" | \"branch\" | \"clear\";\n}\n\n/** Rendering options passed to renderResult */\nexport interface RenderResultOptions {\n\t/** Whether the result view is expanded */\n\texpanded: boolean;\n\t/** Whether this is a partial/streaming result */\n\tisPartial: boolean;\n}\n\n/**\n * Custom tool with optional lifecycle and rendering methods.\n *\n * The execute signature inherited from AgentTool includes an optional onUpdate callback\n * for streaming progress updates during long-running operations:\n * - The callback emits partial results to subscribers (e.g. TUI/RPC), not to the LLM.\n * - Partial updates should use the same TDetails type as the final result (use a union if needed).\n *\n * @example\n * ```typescript\n * type Details =\n * | { status: \"running\"; step: number; total: number }\n * | { status: \"done\"; count: number };\n *\n * async execute(toolCallId, params, signal, onUpdate) {\n * const items = params.items || [];\n * for (let i = 0; i < items.length; i++) {\n * onUpdate?.({\n * content: [{ type: \"text\", text: `Step ${i + 1}/${items.length}...` }],\n * details: { status: \"running\", step: i + 1, total: items.length },\n * });\n * await processItem(items[i], signal);\n * }\n * return { content: [{ type: \"text\", text: \"Done\" }], details: { status: \"done\", count: items.length } };\n * }\n * ```\n *\n * Progress updates are rendered via renderResult with isPartial: true.\n */\nexport interface CustomAgentTool<TParams extends TSchema = TSchema, TDetails = any>\n\textends AgentTool<TParams, TDetails> {\n\t/** Called on session start/switch/branch/clear - use to reconstruct state from entries */\n\tonSession?: (event: SessionEvent) => void | Promise<void>;\n\t/** Custom rendering for tool call display - return a Component */\n\trenderCall?: (args: Static<TParams>, theme: Theme) => Component;\n\t/** Custom rendering for tool result display - return a Component */\n\trenderResult?: (result: AgentToolResult<TDetails>, options: RenderResultOptions, theme: Theme) => Component;\n\t/** Called when session ends - cleanup resources */\n\tdispose?: () => Promise<void> | void;\n}\n\n/** Factory function that creates a custom tool or array of tools */\nexport type CustomToolFactory = (\n\tpi: ToolAPI,\n) => CustomAgentTool<any> | CustomAgentTool[] | Promise<CustomAgentTool | CustomAgentTool[]>;\n\n/** Loaded custom tool with metadata */\nexport interface LoadedCustomTool {\n\t/** Original path (as specified) */\n\tpath: string;\n\t/** Resolved absolute path */\n\tresolvedPath: string;\n\t/** The tool instance */\n\ttool: CustomAgentTool;\n}\n\n/** Result from loading custom tools */\nexport interface CustomToolsLoadResult {\n\ttools: LoadedCustomTool[];\n\terrors: Array<{ path: string; error: string }>;\n\t/** Update the UI context for all loaded tools. Call when mode initializes. */\n\tsetUIContext(uiContext: ToolUIContext, hasUI: boolean): void;\n}\n"]}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/custom-tools/types.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Custom tool types.\n *\n * Custom tools are TypeScript modules that define additional tools for the agent.\n * They can provide custom rendering for tool calls and results in the TUI.\n */\n\nimport type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from \"@mariozechner/pi-ai\";\nimport type { Component } from \"@mariozechner/pi-tui\";\nimport type { Static, TSchema } from \"@sinclair/typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { SessionEntry } from \"../session-manager.js\";\n\n/** Alias for clarity */\nexport type ToolUIContext = HookUIContext;\n\n/** Re-export for custom tools to use in execute signature */\nexport type { AgentToolUpdateCallback };\n\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\t/** True if the process was killed due to signal or timeout */\n\tkilled?: boolean;\n}\n\nexport interface ExecOptions {\n\t/** AbortSignal to cancel the process */\n\tsignal?: AbortSignal;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n}\n\n/** API passed to custom tool factory (stable across session changes) */\nexport interface ToolAPI {\n\t/** Current working directory */\n\tcwd: string;\n\t/** Execute a command */\n\texec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;\n\t/** UI methods for user interaction (select, confirm, input, notify) */\n\tui: ToolUIContext;\n\t/** Whether UI is available (false in print/RPC mode) */\n\thasUI: boolean;\n}\n\n/** Session event passed to onSession callback */\nexport interface SessionEvent {\n\t/** All session entries (including pre-compaction history) */\n\tentries: SessionEntry[];\n\t/** Current session file path, or null in --no-session mode */\n\tsessionFile: string | null;\n\t/** Previous session file path, or null for \"start\" and \"new\" */\n\tpreviousSessionFile: string | null;\n\t/** Reason for the session event */\n\treason: \"start\" | \"switch\" | \"branch\" | \"new\";\n}\n\n/** Rendering options passed to renderResult */\nexport interface RenderResultOptions {\n\t/** Whether the result view is expanded */\n\texpanded: boolean;\n\t/** Whether this is a partial/streaming result */\n\tisPartial: boolean;\n}\n\n/**\n * Custom tool with optional lifecycle and rendering methods.\n *\n * The execute signature inherited from AgentTool includes an optional onUpdate callback\n * for streaming progress updates during long-running operations:\n * - The callback emits partial results to subscribers (e.g. TUI/RPC), not to the LLM.\n * - Partial updates should use the same TDetails type as the final result (use a union if needed).\n *\n * @example\n * ```typescript\n * type Details =\n * | { status: \"running\"; step: number; total: number }\n * | { status: \"done\"; count: number };\n *\n * async execute(toolCallId, params, signal, onUpdate) {\n * const items = params.items || [];\n * for (let i = 0; i < items.length; i++) {\n * onUpdate?.({\n * content: [{ type: \"text\", text: `Step ${i + 1}/${items.length}...` }],\n * details: { status: \"running\", step: i + 1, total: items.length },\n * });\n * await processItem(items[i], signal);\n * }\n * return { content: [{ type: \"text\", text: \"Done\" }], details: { status: \"done\", count: items.length } };\n * }\n * ```\n *\n * Progress updates are rendered via renderResult with isPartial: true.\n */\nexport interface CustomAgentTool<TParams extends TSchema = TSchema, TDetails = any>\n\textends AgentTool<TParams, TDetails> {\n\t/** Called on session start/switch/branch/clear - use to reconstruct state from entries */\n\tonSession?: (event: SessionEvent) => void | Promise<void>;\n\t/** Custom rendering for tool call display - return a Component */\n\trenderCall?: (args: Static<TParams>, theme: Theme) => Component;\n\t/** Custom rendering for tool result display - return a Component */\n\trenderResult?: (result: AgentToolResult<TDetails>, options: RenderResultOptions, theme: Theme) => Component;\n\t/** Called when session ends - cleanup resources */\n\tdispose?: () => Promise<void> | void;\n}\n\n/** Factory function that creates a custom tool or array of tools */\nexport type CustomToolFactory = (\n\tpi: ToolAPI,\n) => CustomAgentTool<any> | CustomAgentTool[] | Promise<CustomAgentTool | CustomAgentTool[]>;\n\n/** Loaded custom tool with metadata */\nexport interface LoadedCustomTool {\n\t/** Original path (as specified) */\n\tpath: string;\n\t/** Resolved absolute path */\n\tresolvedPath: string;\n\t/** The tool instance */\n\ttool: CustomAgentTool;\n}\n\n/** Result from loading custom tools */\nexport interface CustomToolsLoadResult {\n\ttools: LoadedCustomTool[];\n\terrors: Array<{ path: string; error: string }>;\n\t/** Update the UI context for all loaded tools. Call when mode initializes. */\n\tsetUIContext(uiContext: ToolUIContext, hasUI: boolean): void;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/custom-tools/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * Custom tool types.\n *\n * Custom tools are TypeScript modules that define additional tools for the agent.\n * They can provide custom rendering for tool calls and results in the TUI.\n */\n\nimport type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from \"@mariozechner/pi-ai\";\nimport type { Component } from \"@mariozechner/pi-tui\";\nimport type { Static, TSchema } from \"@sinclair/typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { SessionEntry } from \"../session-manager.js\";\n\n/** Alias for clarity */\nexport type ToolUIContext = HookUIContext;\n\n/** Re-export for custom tools to use in execute signature */\nexport type { AgentToolUpdateCallback };\n\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\t/** True if the process was killed due to signal or timeout */\n\tkilled?: boolean;\n}\n\nexport interface ExecOptions {\n\t/** AbortSignal to cancel the process */\n\tsignal?: AbortSignal;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n}\n\n/** API passed to custom tool factory (stable across session changes) */\nexport interface ToolAPI {\n\t/** Current working directory */\n\tcwd: string;\n\t/** Execute a command */\n\texec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;\n\t/** UI methods for user interaction (select, confirm, input, notify) */\n\tui: ToolUIContext;\n\t/** Whether UI is available (false in print/RPC mode) */\n\thasUI: boolean;\n}\n\n/** Session event passed to onSession callback */\nexport interface SessionEvent {\n\t/** All session entries (including pre-compaction history) */\n\tentries: SessionEntry[];\n\t/** Current session file path, or null in --no-session mode */\n\tsessionFile: string | null;\n\t/** Previous session file path, or null for \"start\" and \"clear\" */\n\tpreviousSessionFile: string | null;\n\t/** Reason for the session event */\n\treason: \"start\" | \"switch\" | \"branch\" | \"clear\";\n}\n\n/** Rendering options passed to renderResult */\nexport interface RenderResultOptions {\n\t/** Whether the result view is expanded */\n\texpanded: boolean;\n\t/** Whether this is a partial/streaming result */\n\tisPartial: boolean;\n}\n\n/**\n * Custom tool with optional lifecycle and rendering methods.\n *\n * The execute signature inherited from AgentTool includes an optional onUpdate callback\n * for streaming progress updates during long-running operations:\n * - The callback emits partial results to subscribers (e.g. TUI/RPC), not to the LLM.\n * - Partial updates should use the same TDetails type as the final result (use a union if needed).\n *\n * @example\n * ```typescript\n * type Details =\n * | { status: \"running\"; step: number; total: number }\n * | { status: \"done\"; count: number };\n *\n * async execute(toolCallId, params, signal, onUpdate) {\n * const items = params.items || [];\n * for (let i = 0; i < items.length; i++) {\n * onUpdate?.({\n * content: [{ type: \"text\", text: `Step ${i + 1}/${items.length}...` }],\n * details: { status: \"running\", step: i + 1, total: items.length },\n * });\n * await processItem(items[i], signal);\n * }\n * return { content: [{ type: \"text\", text: \"Done\" }], details: { status: \"done\", count: items.length } };\n * }\n * ```\n *\n * Progress updates are rendered via renderResult with isPartial: true.\n */\nexport interface CustomAgentTool<TParams extends TSchema = TSchema, TDetails = any>\n\textends AgentTool<TParams, TDetails> {\n\t/** Called on session start/switch/branch/clear - use to reconstruct state from entries */\n\tonSession?: (event: SessionEvent) => void | Promise<void>;\n\t/** Custom rendering for tool call display - return a Component */\n\trenderCall?: (args: Static<TParams>, theme: Theme) => Component;\n\t/** Custom rendering for tool result display - return a Component */\n\trenderResult?: (result: AgentToolResult<TDetails>, options: RenderResultOptions, theme: Theme) => Component;\n\t/** Called when session ends - cleanup resources */\n\tdispose?: () => Promise<void> | void;\n}\n\n/** Factory function that creates a custom tool or array of tools */\nexport type CustomToolFactory = (\n\tpi: ToolAPI,\n) => CustomAgentTool<any> | CustomAgentTool[] | Promise<CustomAgentTool | CustomAgentTool[]>;\n\n/** Loaded custom tool with metadata */\nexport interface LoadedCustomTool {\n\t/** Original path (as specified) */\n\tpath: string;\n\t/** Resolved absolute path */\n\tresolvedPath: string;\n\t/** The tool instance */\n\ttool: CustomAgentTool;\n}\n\n/** Result from loading custom tools */\nexport interface CustomToolsLoadResult {\n\ttools: LoadedCustomTool[];\n\terrors: Array<{ path: string; error: string }>;\n\t/** Update the UI context for all loaded tools. Call when mode initializes. */\n\tsetUIContext(uiContext: ToolUIContext, hasUI: boolean): void;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/custom-tools/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * Custom tool types.\n *\n * Custom tools are TypeScript modules that define additional tools for the agent.\n * They can provide custom rendering for tool calls and results in the TUI.\n */\n\nimport type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from \"@mariozechner/pi-ai\";\nimport type { Component } from \"@mariozechner/pi-tui\";\nimport type { Static, TSchema } from \"@sinclair/typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { HookUIContext } from \"../hooks/types.js\";\nimport type { SessionEntry } from \"../session-manager.js\";\n\n/** Alias for clarity */\nexport type ToolUIContext = HookUIContext;\n\n/** Re-export for custom tools to use in execute signature */\nexport type { AgentToolUpdateCallback };\n\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\t/** True if the process was killed due to signal or timeout */\n\tkilled?: boolean;\n}\n\nexport interface ExecOptions {\n\t/** AbortSignal to cancel the process */\n\tsignal?: AbortSignal;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n}\n\n/** API passed to custom tool factory (stable across session changes) */\nexport interface ToolAPI {\n\t/** Current working directory */\n\tcwd: string;\n\t/** Execute a command */\n\texec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;\n\t/** UI methods for user interaction (select, confirm, input, notify) */\n\tui: ToolUIContext;\n\t/** Whether UI is available (false in print/RPC mode) */\n\thasUI: boolean;\n}\n\n/** Session event passed to onSession callback */\nexport interface SessionEvent {\n\t/** All session entries (including pre-compaction history) */\n\tentries: SessionEntry[];\n\t/** Current session file path, or null in --no-session mode */\n\tsessionFile: string | null;\n\t/** Previous session file path, or null for \"start\" and \"new\" */\n\tpreviousSessionFile: string | null;\n\t/** Reason for the session event */\n\treason: \"start\" | \"switch\" | \"branch\" | \"new\";\n}\n\n/** Rendering options passed to renderResult */\nexport interface RenderResultOptions {\n\t/** Whether the result view is expanded */\n\texpanded: boolean;\n\t/** Whether this is a partial/streaming result */\n\tisPartial: boolean;\n}\n\n/**\n * Custom tool with optional lifecycle and rendering methods.\n *\n * The execute signature inherited from AgentTool includes an optional onUpdate callback\n * for streaming progress updates during long-running operations:\n * - The callback emits partial results to subscribers (e.g. TUI/RPC), not to the LLM.\n * - Partial updates should use the same TDetails type as the final result (use a union if needed).\n *\n * @example\n * ```typescript\n * type Details =\n * | { status: \"running\"; step: number; total: number }\n * | { status: \"done\"; count: number };\n *\n * async execute(toolCallId, params, signal, onUpdate) {\n * const items = params.items || [];\n * for (let i = 0; i < items.length; i++) {\n * onUpdate?.({\n * content: [{ type: \"text\", text: `Step ${i + 1}/${items.length}...` }],\n * details: { status: \"running\", step: i + 1, total: items.length },\n * });\n * await processItem(items[i], signal);\n * }\n * return { content: [{ type: \"text\", text: \"Done\" }], details: { status: \"done\", count: items.length } };\n * }\n * ```\n *\n * Progress updates are rendered via renderResult with isPartial: true.\n */\nexport interface CustomAgentTool<TParams extends TSchema = TSchema, TDetails = any>\n\textends AgentTool<TParams, TDetails> {\n\t/** Called on session start/switch/branch/clear - use to reconstruct state from entries */\n\tonSession?: (event: SessionEvent) => void | Promise<void>;\n\t/** Custom rendering for tool call display - return a Component */\n\trenderCall?: (args: Static<TParams>, theme: Theme) => Component;\n\t/** Custom rendering for tool result display - return a Component */\n\trenderResult?: (result: AgentToolResult<TDetails>, options: RenderResultOptions, theme: Theme) => Component;\n\t/** Called when session ends - cleanup resources */\n\tdispose?: () => Promise<void> | void;\n}\n\n/** Factory function that creates a custom tool or array of tools */\nexport type CustomToolFactory = (\n\tpi: ToolAPI,\n) => CustomAgentTool<any> | CustomAgentTool[] | Promise<CustomAgentTool | CustomAgentTool[]>;\n\n/** Loaded custom tool with metadata */\nexport interface LoadedCustomTool {\n\t/** Original path (as specified) */\n\tpath: string;\n\t/** Resolved absolute path */\n\tresolvedPath: string;\n\t/** The tool instance */\n\ttool: CustomAgentTool;\n}\n\n/** Result from loading custom tools */\nexport interface CustomToolsLoadResult {\n\ttools: LoadedCustomTool[];\n\terrors: Array<{ path: string; error: string }>;\n\t/** Update the UI context for all loaded tools. Call when mode initializes. */\n\tsetUIContext(uiContext: ToolUIContext, hasUI: boolean): void;\n}\n"]}
@@ -76,7 +76,7 @@ interface SessionEventBase {
76
76
  entries: SessionEntry[];
77
77
  /** Current session file path, or null in --no-session mode */
78
78
  sessionFile: string | null;
79
- /** Previous session file path, or null for "start" and "clear" */
79
+ /** Previous session file path, or null for "start" and "new" */
80
80
  previousSessionFile: string | null;
81
81
  }
82
82
  /**
@@ -86,7 +86,7 @@ interface SessionEventBase {
86
86
  * Lifecycle:
87
87
  * - start: Initial session load
88
88
  * - before_switch / switch: Session switch (e.g., /resume command)
89
- * - before_clear / clear: Session clear (e.g., /clear command)
89
+ * - before_new / new: New session (e.g., /new command)
90
90
  * - before_branch / branch: Session branch (e.g., /branch command)
91
91
  * - before_compact / compact: Before/after context compaction
92
92
  * - shutdown: Process exit (SIGINT/SIGTERM)
@@ -95,7 +95,7 @@ interface SessionEventBase {
95
95
  * Other events fire after the action completes.
96
96
  */
97
97
  export type SessionEvent = (SessionEventBase & {
98
- reason: "start" | "switch" | "clear" | "before_switch" | "before_clear" | "shutdown";
98
+ reason: "start" | "switch" | "new" | "before_switch" | "before_new" | "shutdown";
99
99
  }) | (SessionEventBase & {
100
100
  reason: "branch" | "before_branch";
101
101
  /** Index of the turn to branch from */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook system types.\n *\n * Hooks are TypeScript modules that can subscribe to agent lifecycle events\n * and interact with the user via UI primitives.\n */\n\nimport type { AppMessage, Attachment } from \"@mariozechner/pi-agent-core\";\nimport type { ImageContent, Model, TextContent, ToolResultMessage } from \"@mariozechner/pi-ai\";\nimport type { CutPointResult } from \"../compaction.js\";\nimport type { CompactionEntry, SessionEntry } from \"../session-manager.js\";\nimport type {\n\tBashToolDetails,\n\tFindToolDetails,\n\tGrepToolDetails,\n\tLsToolDetails,\n\tReadToolDetails,\n} from \"../tools/index.js\";\n\n// ============================================================================\n// Execution Context\n// ============================================================================\n\n/**\n * Result of executing a command via ctx.exec()\n */\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\t/** True if the process was killed due to signal or timeout */\n\tkilled?: boolean;\n}\n\nexport interface ExecOptions {\n\t/** AbortSignal to cancel the process */\n\tsignal?: AbortSignal;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n}\n\n/**\n * UI context for hooks to request interactive UI from the harness.\n * Each mode (interactive, RPC, print) provides its own implementation.\n */\nexport interface HookUIContext {\n\t/**\n\t * Show a selector and return the user's choice.\n\t * @param title - Title to display\n\t * @param options - Array of string options\n\t * @returns Selected option string, or null if cancelled\n\t */\n\tselect(title: string, options: string[]): Promise<string | null>;\n\n\t/**\n\t * Show a confirmation dialog.\n\t * @returns true if confirmed, false if cancelled\n\t */\n\tconfirm(title: string, message: string): Promise<boolean>;\n\n\t/**\n\t * Show a text input dialog.\n\t * @returns User input, or null if cancelled\n\t */\n\tinput(title: string, placeholder?: string): Promise<string | null>;\n\n\t/**\n\t * Show a notification to the user.\n\t */\n\tnotify(message: string, type?: \"info\" | \"warning\" | \"error\"): void;\n}\n\n/**\n * Context passed to hook event handlers.\n */\nexport interface HookEventContext {\n\t/** Execute a command and return stdout/stderr/code */\n\texec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;\n\t/** UI methods for user interaction */\n\tui: HookUIContext;\n\t/** Whether UI is available (false in print mode) */\n\thasUI: boolean;\n\t/** Current working directory */\n\tcwd: string;\n\t/** Path to session file, or null if --no-session */\n\tsessionFile: string | null;\n}\n\n// ============================================================================\n// Events\n// ============================================================================\n\n/**\n * Base fields shared by all session events.\n */\ninterface SessionEventBase {\n\ttype: \"session\";\n\t/** All session entries (including pre-compaction history) */\n\tentries: SessionEntry[];\n\t/** Current session file path, or null in --no-session mode */\n\tsessionFile: string | null;\n\t/** Previous session file path, or null for \"start\" and \"clear\" */\n\tpreviousSessionFile: string | null;\n}\n\n/**\n * Event data for session events.\n * Discriminated union based on reason.\n *\n * Lifecycle:\n * - start: Initial session load\n * - before_switch / switch: Session switch (e.g., /resume command)\n * - before_clear / clear: Session clear (e.g., /clear command)\n * - before_branch / branch: Session branch (e.g., /branch command)\n * - before_compact / compact: Before/after context compaction\n * - shutdown: Process exit (SIGINT/SIGTERM)\n *\n * \"before_*\" events fire before the action and can be cancelled via SessionEventResult.\n * Other events fire after the action completes.\n */\nexport type SessionEvent =\n\t| (SessionEventBase & {\n\t\t\treason: \"start\" | \"switch\" | \"clear\" | \"before_switch\" | \"before_clear\" | \"shutdown\";\n\t })\n\t| (SessionEventBase & {\n\t\t\treason: \"branch\" | \"before_branch\";\n\t\t\t/** Index of the turn to branch from */\n\t\t\ttargetTurnIndex: number;\n\t })\n\t| (SessionEventBase & {\n\t\t\treason: \"before_compact\";\n\t\t\tcutPoint: CutPointResult;\n\t\t\t/** Summary from previous compaction, if any. Include this in your summary to preserve context. */\n\t\t\tpreviousSummary?: string;\n\t\t\t/** Messages that will be summarized and discarded */\n\t\t\tmessagesToSummarize: AppMessage[];\n\t\t\t/** Messages that will be kept after the summary (recent turns) */\n\t\t\tmessagesToKeep: AppMessage[];\n\t\t\ttokensBefore: number;\n\t\t\tcustomInstructions?: string;\n\t\t\tmodel: Model<any>;\n\t\t\t/** Resolve API key for any model (checks settings, OAuth, env vars) */\n\t\t\tresolveApiKey: (model: Model<any>) => Promise<string | undefined>;\n\t\t\t/** Abort signal - hooks should pass this to LLM calls and check it periodically */\n\t\t\tsignal: AbortSignal;\n\t })\n\t| (SessionEventBase & {\n\t\t\treason: \"compact\";\n\t\t\tcompactionEntry: CompactionEntry;\n\t\t\ttokensBefore: number;\n\t\t\t/** Whether the compaction entry was provided by a hook */\n\t\t\tfromHook: boolean;\n\t });\n\n/**\n * Event data for agent_start event.\n * Fired when an agent loop starts (once per user prompt).\n */\nexport interface AgentStartEvent {\n\ttype: \"agent_start\";\n}\n\n/**\n * Event data for agent_end event.\n */\nexport interface AgentEndEvent {\n\ttype: \"agent_end\";\n\tmessages: AppMessage[];\n}\n\n/**\n * Event data for turn_start event.\n */\nexport interface TurnStartEvent {\n\ttype: \"turn_start\";\n\tturnIndex: number;\n\ttimestamp: number;\n}\n\n/**\n * Event data for turn_end event.\n */\nexport interface TurnEndEvent {\n\ttype: \"turn_end\";\n\tturnIndex: number;\n\tmessage: AppMessage;\n\ttoolResults: ToolResultMessage[];\n}\n\n/**\n * Event data for tool_call event.\n * Fired before a tool is executed. Hooks can block execution.\n */\nexport interface ToolCallEvent {\n\ttype: \"tool_call\";\n\t/** Tool name (e.g., \"bash\", \"edit\", \"write\") */\n\ttoolName: string;\n\t/** Tool call ID */\n\ttoolCallId: string;\n\t/** Tool input parameters */\n\tinput: Record<string, unknown>;\n}\n\n/**\n * Base interface for tool_result events.\n */\ninterface ToolResultEventBase {\n\ttype: \"tool_result\";\n\t/** Tool call ID */\n\ttoolCallId: string;\n\t/** Tool input parameters */\n\tinput: Record<string, unknown>;\n\t/** Full content array (text and images) */\n\tcontent: (TextContent | ImageContent)[];\n\t/** Whether the tool execution was an error */\n\tisError: boolean;\n}\n\n/** Tool result event for bash tool */\nexport interface BashToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"bash\";\n\tdetails: BashToolDetails | undefined;\n}\n\n/** Tool result event for read tool */\nexport interface ReadToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"read\";\n\tdetails: ReadToolDetails | undefined;\n}\n\n/** Tool result event for edit tool */\nexport interface EditToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"edit\";\n\tdetails: undefined;\n}\n\n/** Tool result event for write tool */\nexport interface WriteToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"write\";\n\tdetails: undefined;\n}\n\n/** Tool result event for grep tool */\nexport interface GrepToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"grep\";\n\tdetails: GrepToolDetails | undefined;\n}\n\n/** Tool result event for find tool */\nexport interface FindToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"find\";\n\tdetails: FindToolDetails | undefined;\n}\n\n/** Tool result event for ls tool */\nexport interface LsToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"ls\";\n\tdetails: LsToolDetails | undefined;\n}\n\n/** Tool result event for custom/unknown tools */\nexport interface CustomToolResultEvent extends ToolResultEventBase {\n\ttoolName: string;\n\tdetails: unknown;\n}\n\n/**\n * Event data for tool_result event.\n * Fired after a tool is executed. Hooks can modify the result.\n * Use toolName to discriminate and get typed details.\n */\nexport type ToolResultEvent =\n\t| BashToolResultEvent\n\t| ReadToolResultEvent\n\t| EditToolResultEvent\n\t| WriteToolResultEvent\n\t| GrepToolResultEvent\n\t| FindToolResultEvent\n\t| LsToolResultEvent\n\t| CustomToolResultEvent;\n\n// Type guards for narrowing ToolResultEvent to specific tool types\nexport function isBashToolResult(e: ToolResultEvent): e is BashToolResultEvent {\n\treturn e.toolName === \"bash\";\n}\nexport function isReadToolResult(e: ToolResultEvent): e is ReadToolResultEvent {\n\treturn e.toolName === \"read\";\n}\nexport function isEditToolResult(e: ToolResultEvent): e is EditToolResultEvent {\n\treturn e.toolName === \"edit\";\n}\nexport function isWriteToolResult(e: ToolResultEvent): e is WriteToolResultEvent {\n\treturn e.toolName === \"write\";\n}\nexport function isGrepToolResult(e: ToolResultEvent): e is GrepToolResultEvent {\n\treturn e.toolName === \"grep\";\n}\nexport function isFindToolResult(e: ToolResultEvent): e is FindToolResultEvent {\n\treturn e.toolName === \"find\";\n}\nexport function isLsToolResult(e: ToolResultEvent): e is LsToolResultEvent {\n\treturn e.toolName === \"ls\";\n}\n\n/**\n * Union of all hook event types.\n */\nexport type HookEvent =\n\t| SessionEvent\n\t| AgentStartEvent\n\t| AgentEndEvent\n\t| TurnStartEvent\n\t| TurnEndEvent\n\t| ToolCallEvent\n\t| ToolResultEvent;\n\n// ============================================================================\n// Event Results\n// ============================================================================\n\n/**\n * Return type for tool_call event handlers.\n * Allows hooks to block tool execution.\n */\nexport interface ToolCallEventResult {\n\t/** If true, block the tool from executing */\n\tblock?: boolean;\n\t/** Reason for blocking (returned to LLM as error) */\n\treason?: string;\n}\n\n/**\n * Return type for tool_result event handlers.\n * Allows hooks to modify tool results.\n */\nexport interface ToolResultEventResult {\n\t/** Replacement content array (text and images) */\n\tcontent?: (TextContent | ImageContent)[];\n\t/** Replacement details */\n\tdetails?: unknown;\n\t/** Override isError flag */\n\tisError?: boolean;\n}\n\n/**\n * Return type for session event handlers.\n * Allows hooks to cancel \"before_*\" actions.\n */\nexport interface SessionEventResult {\n\t/** If true, cancel the pending action (switch, clear, or branch) */\n\tcancel?: boolean;\n\t/** If true (for before_branch only), skip restoring conversation to branch point while still creating the branched session file */\n\tskipConversationRestore?: boolean;\n\t/** Custom compaction entry (for before_compact event) */\n\tcompactionEntry?: CompactionEntry;\n}\n\n// ============================================================================\n// Hook API\n// ============================================================================\n\n/**\n * Handler function type for each event.\n */\nexport type HookHandler<E, R = void> = (event: E, ctx: HookEventContext) => Promise<R>;\n\n/**\n * HookAPI passed to hook factory functions.\n * Hooks use pi.on() to subscribe to events and pi.send() to inject messages.\n */\nexport interface HookAPI {\n\t// biome-ignore lint/suspicious/noConfusingVoidType: void allows handlers to not return anything\n\ton(event: \"session\", handler: HookHandler<SessionEvent, SessionEventResult | void>): void;\n\ton(event: \"agent_start\", handler: HookHandler<AgentStartEvent>): void;\n\ton(event: \"agent_end\", handler: HookHandler<AgentEndEvent>): void;\n\ton(event: \"turn_start\", handler: HookHandler<TurnStartEvent>): void;\n\ton(event: \"turn_end\", handler: HookHandler<TurnEndEvent>): void;\n\ton(event: \"tool_call\", handler: HookHandler<ToolCallEvent, ToolCallEventResult | undefined>): void;\n\ton(event: \"tool_result\", handler: HookHandler<ToolResultEvent, ToolResultEventResult | undefined>): void;\n\n\t/**\n\t * Send a message to the agent.\n\t * If the agent is streaming, the message is queued.\n\t * If the agent is idle, a new agent loop is started.\n\t */\n\tsend(text: string, attachments?: Attachment[]): void;\n}\n\n/**\n * Hook factory function type.\n * Hooks export a default function that receives the HookAPI.\n */\nexport type HookFactory = (pi: HookAPI) => void;\n\n// ============================================================================\n// Errors\n// ============================================================================\n\n/**\n * Error emitted when a hook fails.\n */\nexport interface HookError {\n\thookPath: string;\n\tevent: string;\n\terror: string;\n}\n"]}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC/F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC3E,OAAO,KAAK,EACX,eAAe,EACf,eAAe,EACf,eAAe,EACf,aAAa,EACb,eAAe,EACf,MAAM,mBAAmB,CAAC;AAM3B;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC3B,wCAAwC;IACxC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC7B;;;;;OAKG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAEjE;;;OAGG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE1D;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAEnE;;OAEG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,IAAI,CAAC;CACnE;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,sDAAsD;IACtD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAClF,sCAAsC;IACtC,EAAE,EAAE,aAAa,CAAC;IAClB,oDAAoD;IACpD,KAAK,EAAE,OAAO,CAAC;IACf,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,oDAAoD;IACpD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAMD;;GAEG;AACH,UAAU,gBAAgB;IACzB,IAAI,EAAE,SAAS,CAAC;IAChB,6DAA6D;IAC7D,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,8DAA8D;IAC9D,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,gEAAgE;IAChE,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,YAAY,GACrB,CAAC,gBAAgB,GAAG;IACpB,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,GAAG,eAAe,GAAG,YAAY,GAAG,UAAU,CAAC;CAChF,CAAC,GACF,CAAC,gBAAgB,GAAG;IACpB,MAAM,EAAE,QAAQ,GAAG,eAAe,CAAC;IACnC,uCAAuC;IACvC,eAAe,EAAE,MAAM,CAAC;CACvB,CAAC,GACF,CAAC,gBAAgB,GAAG;IACpB,MAAM,EAAE,gBAAgB,CAAC;IACzB,QAAQ,EAAE,cAAc,CAAC;IACzB,kGAAkG;IAClG,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qDAAqD;IACrD,mBAAmB,EAAE,UAAU,EAAE,CAAC;IAClC,kEAAkE;IAClE,cAAc,EAAE,UAAU,EAAE,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,uEAAuE;IACvE,aAAa,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAClE,mFAAmF;IACnF,MAAM,EAAE,WAAW,CAAC;CACnB,CAAC,GACF,CAAC,gBAAgB,GAAG;IACpB,MAAM,EAAE,SAAS,CAAC;IAClB,eAAe,EAAE,eAAe,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,0DAA0D;IAC1D,QAAQ,EAAE,OAAO,CAAC;CACjB,CAAC,CAAC;AAEN;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,aAAa,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,UAAU,EAAE,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,YAAY,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,UAAU,CAAC;IACpB,WAAW,EAAE,iBAAiB,EAAE,CAAC;CACjC;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,WAAW,CAAC;IAClB,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED;;GAEG;AACH,UAAU,mBAAmB;IAC5B,IAAI,EAAE,aAAa,CAAC;IACpB,mBAAmB;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,2CAA2C;IAC3C,OAAO,EAAE,CAAC,WAAW,GAAG,YAAY,CAAC,EAAE,CAAC;IACxC,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;CACjB;AAED,sCAAsC;AACtC,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,eAAe,GAAG,SAAS,CAAC;CACrC;AAED,sCAAsC;AACtC,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,eAAe,GAAG,SAAS,CAAC;CACrC;AAED,sCAAsC;AACtC,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,SAAS,CAAC;CACnB;AAED,uCAAuC;AACvC,MAAM,WAAW,oBAAqB,SAAQ,mBAAmB;IAChE,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,SAAS,CAAC;CACnB;AAED,sCAAsC;AACtC,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,eAAe,GAAG,SAAS,CAAC;CACrC;AAED,sCAAsC;AACtC,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,eAAe,GAAG,SAAS,CAAC;CACrC;AAED,oCAAoC;AACpC,MAAM,WAAW,iBAAkB,SAAQ,mBAAmB;IAC7D,QAAQ,EAAE,IAAI,CAAC;IACf,OAAO,EAAE,aAAa,GAAG,SAAS,CAAC;CACnC;AAED,iDAAiD;AACjD,MAAM,WAAW,qBAAsB,SAAQ,mBAAmB;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CACjB;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GACxB,mBAAmB,GACnB,mBAAmB,GACnB,mBAAmB,GACnB,oBAAoB,GACpB,mBAAmB,GACnB,mBAAmB,GACnB,iBAAiB,GACjB,qBAAqB,CAAC;AAGzB,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,eAAe,GAAG,CAAC,IAAI,mBAAmB,CAE7E;AACD,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,eAAe,GAAG,CAAC,IAAI,mBAAmB,CAE7E;AACD,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,eAAe,GAAG,CAAC,IAAI,mBAAmB,CAE7E;AACD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,eAAe,GAAG,CAAC,IAAI,oBAAoB,CAE/E;AACD,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,eAAe,GAAG,CAAC,IAAI,mBAAmB,CAE7E;AACD,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,eAAe,GAAG,CAAC,IAAI,mBAAmB,CAE7E;AACD,wBAAgB,cAAc,CAAC,CAAC,EAAE,eAAe,GAAG,CAAC,IAAI,iBAAiB,CAEzE;AAED;;GAEG;AACH,MAAM,MAAM,SAAS,GAClB,YAAY,GACZ,eAAe,GACf,aAAa,GACb,cAAc,GACd,YAAY,GACZ,aAAa,GACb,eAAe,CAAC;AAMnB;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IACnC,6CAA6C;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACrC,kDAAkD;IAClD,OAAO,CAAC,EAAE,CAAC,WAAW,GAAG,YAAY,CAAC,EAAE,CAAC;IACzC,0BAA0B;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IAClC,oEAAoE;IACpE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,mIAAmI;IACnI,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,yDAAyD;IACzD,eAAe,CAAC,EAAE,eAAe,CAAC;CAClC;AAMD;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;AAEvF;;;GAGG;AACH,MAAM,WAAW,OAAO;IAEvB,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,CAAC,YAAY,EAAE,kBAAkB,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1F,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW,CAAC,eAAe,CAAC,GAAG,IAAI,CAAC;IACtE,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC;IAClE,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,CAAC,cAAc,CAAC,GAAG,IAAI,CAAC;IACpE,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC;IAChE,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,CAAC,aAAa,EAAE,mBAAmB,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;IACnG,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW,CAAC,eAAe,EAAE,qBAAqB,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;IAEzG;;;;OAIG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;CACrD;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,EAAE,EAAE,OAAO,KAAK,IAAI,CAAC;AAMhD;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACd","sourcesContent":["/**\n * Hook system types.\n *\n * Hooks are TypeScript modules that can subscribe to agent lifecycle events\n * and interact with the user via UI primitives.\n */\n\nimport type { AppMessage, Attachment } from \"@mariozechner/pi-agent-core\";\nimport type { ImageContent, Model, TextContent, ToolResultMessage } from \"@mariozechner/pi-ai\";\nimport type { CutPointResult } from \"../compaction.js\";\nimport type { CompactionEntry, SessionEntry } from \"../session-manager.js\";\nimport type {\n\tBashToolDetails,\n\tFindToolDetails,\n\tGrepToolDetails,\n\tLsToolDetails,\n\tReadToolDetails,\n} from \"../tools/index.js\";\n\n// ============================================================================\n// Execution Context\n// ============================================================================\n\n/**\n * Result of executing a command via ctx.exec()\n */\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\t/** True if the process was killed due to signal or timeout */\n\tkilled?: boolean;\n}\n\nexport interface ExecOptions {\n\t/** AbortSignal to cancel the process */\n\tsignal?: AbortSignal;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n}\n\n/**\n * UI context for hooks to request interactive UI from the harness.\n * Each mode (interactive, RPC, print) provides its own implementation.\n */\nexport interface HookUIContext {\n\t/**\n\t * Show a selector and return the user's choice.\n\t * @param title - Title to display\n\t * @param options - Array of string options\n\t * @returns Selected option string, or null if cancelled\n\t */\n\tselect(title: string, options: string[]): Promise<string | null>;\n\n\t/**\n\t * Show a confirmation dialog.\n\t * @returns true if confirmed, false if cancelled\n\t */\n\tconfirm(title: string, message: string): Promise<boolean>;\n\n\t/**\n\t * Show a text input dialog.\n\t * @returns User input, or null if cancelled\n\t */\n\tinput(title: string, placeholder?: string): Promise<string | null>;\n\n\t/**\n\t * Show a notification to the user.\n\t */\n\tnotify(message: string, type?: \"info\" | \"warning\" | \"error\"): void;\n}\n\n/**\n * Context passed to hook event handlers.\n */\nexport interface HookEventContext {\n\t/** Execute a command and return stdout/stderr/code */\n\texec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;\n\t/** UI methods for user interaction */\n\tui: HookUIContext;\n\t/** Whether UI is available (false in print mode) */\n\thasUI: boolean;\n\t/** Current working directory */\n\tcwd: string;\n\t/** Path to session file, or null if --no-session */\n\tsessionFile: string | null;\n}\n\n// ============================================================================\n// Events\n// ============================================================================\n\n/**\n * Base fields shared by all session events.\n */\ninterface SessionEventBase {\n\ttype: \"session\";\n\t/** All session entries (including pre-compaction history) */\n\tentries: SessionEntry[];\n\t/** Current session file path, or null in --no-session mode */\n\tsessionFile: string | null;\n\t/** Previous session file path, or null for \"start\" and \"new\" */\n\tpreviousSessionFile: string | null;\n}\n\n/**\n * Event data for session events.\n * Discriminated union based on reason.\n *\n * Lifecycle:\n * - start: Initial session load\n * - before_switch / switch: Session switch (e.g., /resume command)\n * - before_new / new: New session (e.g., /new command)\n * - before_branch / branch: Session branch (e.g., /branch command)\n * - before_compact / compact: Before/after context compaction\n * - shutdown: Process exit (SIGINT/SIGTERM)\n *\n * \"before_*\" events fire before the action and can be cancelled via SessionEventResult.\n * Other events fire after the action completes.\n */\nexport type SessionEvent =\n\t| (SessionEventBase & {\n\t\t\treason: \"start\" | \"switch\" | \"new\" | \"before_switch\" | \"before_new\" | \"shutdown\";\n\t })\n\t| (SessionEventBase & {\n\t\t\treason: \"branch\" | \"before_branch\";\n\t\t\t/** Index of the turn to branch from */\n\t\t\ttargetTurnIndex: number;\n\t })\n\t| (SessionEventBase & {\n\t\t\treason: \"before_compact\";\n\t\t\tcutPoint: CutPointResult;\n\t\t\t/** Summary from previous compaction, if any. Include this in your summary to preserve context. */\n\t\t\tpreviousSummary?: string;\n\t\t\t/** Messages that will be summarized and discarded */\n\t\t\tmessagesToSummarize: AppMessage[];\n\t\t\t/** Messages that will be kept after the summary (recent turns) */\n\t\t\tmessagesToKeep: AppMessage[];\n\t\t\ttokensBefore: number;\n\t\t\tcustomInstructions?: string;\n\t\t\tmodel: Model<any>;\n\t\t\t/** Resolve API key for any model (checks settings, OAuth, env vars) */\n\t\t\tresolveApiKey: (model: Model<any>) => Promise<string | undefined>;\n\t\t\t/** Abort signal - hooks should pass this to LLM calls and check it periodically */\n\t\t\tsignal: AbortSignal;\n\t })\n\t| (SessionEventBase & {\n\t\t\treason: \"compact\";\n\t\t\tcompactionEntry: CompactionEntry;\n\t\t\ttokensBefore: number;\n\t\t\t/** Whether the compaction entry was provided by a hook */\n\t\t\tfromHook: boolean;\n\t });\n\n/**\n * Event data for agent_start event.\n * Fired when an agent loop starts (once per user prompt).\n */\nexport interface AgentStartEvent {\n\ttype: \"agent_start\";\n}\n\n/**\n * Event data for agent_end event.\n */\nexport interface AgentEndEvent {\n\ttype: \"agent_end\";\n\tmessages: AppMessage[];\n}\n\n/**\n * Event data for turn_start event.\n */\nexport interface TurnStartEvent {\n\ttype: \"turn_start\";\n\tturnIndex: number;\n\ttimestamp: number;\n}\n\n/**\n * Event data for turn_end event.\n */\nexport interface TurnEndEvent {\n\ttype: \"turn_end\";\n\tturnIndex: number;\n\tmessage: AppMessage;\n\ttoolResults: ToolResultMessage[];\n}\n\n/**\n * Event data for tool_call event.\n * Fired before a tool is executed. Hooks can block execution.\n */\nexport interface ToolCallEvent {\n\ttype: \"tool_call\";\n\t/** Tool name (e.g., \"bash\", \"edit\", \"write\") */\n\ttoolName: string;\n\t/** Tool call ID */\n\ttoolCallId: string;\n\t/** Tool input parameters */\n\tinput: Record<string, unknown>;\n}\n\n/**\n * Base interface for tool_result events.\n */\ninterface ToolResultEventBase {\n\ttype: \"tool_result\";\n\t/** Tool call ID */\n\ttoolCallId: string;\n\t/** Tool input parameters */\n\tinput: Record<string, unknown>;\n\t/** Full content array (text and images) */\n\tcontent: (TextContent | ImageContent)[];\n\t/** Whether the tool execution was an error */\n\tisError: boolean;\n}\n\n/** Tool result event for bash tool */\nexport interface BashToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"bash\";\n\tdetails: BashToolDetails | undefined;\n}\n\n/** Tool result event for read tool */\nexport interface ReadToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"read\";\n\tdetails: ReadToolDetails | undefined;\n}\n\n/** Tool result event for edit tool */\nexport interface EditToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"edit\";\n\tdetails: undefined;\n}\n\n/** Tool result event for write tool */\nexport interface WriteToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"write\";\n\tdetails: undefined;\n}\n\n/** Tool result event for grep tool */\nexport interface GrepToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"grep\";\n\tdetails: GrepToolDetails | undefined;\n}\n\n/** Tool result event for find tool */\nexport interface FindToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"find\";\n\tdetails: FindToolDetails | undefined;\n}\n\n/** Tool result event for ls tool */\nexport interface LsToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"ls\";\n\tdetails: LsToolDetails | undefined;\n}\n\n/** Tool result event for custom/unknown tools */\nexport interface CustomToolResultEvent extends ToolResultEventBase {\n\ttoolName: string;\n\tdetails: unknown;\n}\n\n/**\n * Event data for tool_result event.\n * Fired after a tool is executed. Hooks can modify the result.\n * Use toolName to discriminate and get typed details.\n */\nexport type ToolResultEvent =\n\t| BashToolResultEvent\n\t| ReadToolResultEvent\n\t| EditToolResultEvent\n\t| WriteToolResultEvent\n\t| GrepToolResultEvent\n\t| FindToolResultEvent\n\t| LsToolResultEvent\n\t| CustomToolResultEvent;\n\n// Type guards for narrowing ToolResultEvent to specific tool types\nexport function isBashToolResult(e: ToolResultEvent): e is BashToolResultEvent {\n\treturn e.toolName === \"bash\";\n}\nexport function isReadToolResult(e: ToolResultEvent): e is ReadToolResultEvent {\n\treturn e.toolName === \"read\";\n}\nexport function isEditToolResult(e: ToolResultEvent): e is EditToolResultEvent {\n\treturn e.toolName === \"edit\";\n}\nexport function isWriteToolResult(e: ToolResultEvent): e is WriteToolResultEvent {\n\treturn e.toolName === \"write\";\n}\nexport function isGrepToolResult(e: ToolResultEvent): e is GrepToolResultEvent {\n\treturn e.toolName === \"grep\";\n}\nexport function isFindToolResult(e: ToolResultEvent): e is FindToolResultEvent {\n\treturn e.toolName === \"find\";\n}\nexport function isLsToolResult(e: ToolResultEvent): e is LsToolResultEvent {\n\treturn e.toolName === \"ls\";\n}\n\n/**\n * Union of all hook event types.\n */\nexport type HookEvent =\n\t| SessionEvent\n\t| AgentStartEvent\n\t| AgentEndEvent\n\t| TurnStartEvent\n\t| TurnEndEvent\n\t| ToolCallEvent\n\t| ToolResultEvent;\n\n// ============================================================================\n// Event Results\n// ============================================================================\n\n/**\n * Return type for tool_call event handlers.\n * Allows hooks to block tool execution.\n */\nexport interface ToolCallEventResult {\n\t/** If true, block the tool from executing */\n\tblock?: boolean;\n\t/** Reason for blocking (returned to LLM as error) */\n\treason?: string;\n}\n\n/**\n * Return type for tool_result event handlers.\n * Allows hooks to modify tool results.\n */\nexport interface ToolResultEventResult {\n\t/** Replacement content array (text and images) */\n\tcontent?: (TextContent | ImageContent)[];\n\t/** Replacement details */\n\tdetails?: unknown;\n\t/** Override isError flag */\n\tisError?: boolean;\n}\n\n/**\n * Return type for session event handlers.\n * Allows hooks to cancel \"before_*\" actions.\n */\nexport interface SessionEventResult {\n\t/** If true, cancel the pending action (switch, clear, or branch) */\n\tcancel?: boolean;\n\t/** If true (for before_branch only), skip restoring conversation to branch point while still creating the branched session file */\n\tskipConversationRestore?: boolean;\n\t/** Custom compaction entry (for before_compact event) */\n\tcompactionEntry?: CompactionEntry;\n}\n\n// ============================================================================\n// Hook API\n// ============================================================================\n\n/**\n * Handler function type for each event.\n */\nexport type HookHandler<E, R = void> = (event: E, ctx: HookEventContext) => Promise<R>;\n\n/**\n * HookAPI passed to hook factory functions.\n * Hooks use pi.on() to subscribe to events and pi.send() to inject messages.\n */\nexport interface HookAPI {\n\t// biome-ignore lint/suspicious/noConfusingVoidType: void allows handlers to not return anything\n\ton(event: \"session\", handler: HookHandler<SessionEvent, SessionEventResult | void>): void;\n\ton(event: \"agent_start\", handler: HookHandler<AgentStartEvent>): void;\n\ton(event: \"agent_end\", handler: HookHandler<AgentEndEvent>): void;\n\ton(event: \"turn_start\", handler: HookHandler<TurnStartEvent>): void;\n\ton(event: \"turn_end\", handler: HookHandler<TurnEndEvent>): void;\n\ton(event: \"tool_call\", handler: HookHandler<ToolCallEvent, ToolCallEventResult | undefined>): void;\n\ton(event: \"tool_result\", handler: HookHandler<ToolResultEvent, ToolResultEventResult | undefined>): void;\n\n\t/**\n\t * Send a message to the agent.\n\t * If the agent is streaming, the message is queued.\n\t * If the agent is idle, a new agent loop is started.\n\t */\n\tsend(text: string, attachments?: Attachment[]): void;\n}\n\n/**\n * Hook factory function type.\n * Hooks export a default function that receives the HookAPI.\n */\nexport type HookFactory = (pi: HookAPI) => void;\n\n// ============================================================================\n// Errors\n// ============================================================================\n\n/**\n * Error emitted when a hook fails.\n */\nexport interface HookError {\n\thookPath: string;\n\tevent: string;\n\terror: string;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoRH,mEAAmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,iBAAiB,CAAC,CAAkB,EAA6B;IAChF,OAAO,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC;AAAA,CAC9B;AACD,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,cAAc,CAAC,CAAkB,EAA0B;IAC1E,OAAO,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC;AAAA,CAC3B","sourcesContent":["/**\n * Hook system types.\n *\n * Hooks are TypeScript modules that can subscribe to agent lifecycle events\n * and interact with the user via UI primitives.\n */\n\nimport type { AppMessage, Attachment } from \"@mariozechner/pi-agent-core\";\nimport type { ImageContent, Model, TextContent, ToolResultMessage } from \"@mariozechner/pi-ai\";\nimport type { CutPointResult } from \"../compaction.js\";\nimport type { CompactionEntry, SessionEntry } from \"../session-manager.js\";\nimport type {\n\tBashToolDetails,\n\tFindToolDetails,\n\tGrepToolDetails,\n\tLsToolDetails,\n\tReadToolDetails,\n} from \"../tools/index.js\";\n\n// ============================================================================\n// Execution Context\n// ============================================================================\n\n/**\n * Result of executing a command via ctx.exec()\n */\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\t/** True if the process was killed due to signal or timeout */\n\tkilled?: boolean;\n}\n\nexport interface ExecOptions {\n\t/** AbortSignal to cancel the process */\n\tsignal?: AbortSignal;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n}\n\n/**\n * UI context for hooks to request interactive UI from the harness.\n * Each mode (interactive, RPC, print) provides its own implementation.\n */\nexport interface HookUIContext {\n\t/**\n\t * Show a selector and return the user's choice.\n\t * @param title - Title to display\n\t * @param options - Array of string options\n\t * @returns Selected option string, or null if cancelled\n\t */\n\tselect(title: string, options: string[]): Promise<string | null>;\n\n\t/**\n\t * Show a confirmation dialog.\n\t * @returns true if confirmed, false if cancelled\n\t */\n\tconfirm(title: string, message: string): Promise<boolean>;\n\n\t/**\n\t * Show a text input dialog.\n\t * @returns User input, or null if cancelled\n\t */\n\tinput(title: string, placeholder?: string): Promise<string | null>;\n\n\t/**\n\t * Show a notification to the user.\n\t */\n\tnotify(message: string, type?: \"info\" | \"warning\" | \"error\"): void;\n}\n\n/**\n * Context passed to hook event handlers.\n */\nexport interface HookEventContext {\n\t/** Execute a command and return stdout/stderr/code */\n\texec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;\n\t/** UI methods for user interaction */\n\tui: HookUIContext;\n\t/** Whether UI is available (false in print mode) */\n\thasUI: boolean;\n\t/** Current working directory */\n\tcwd: string;\n\t/** Path to session file, or null if --no-session */\n\tsessionFile: string | null;\n}\n\n// ============================================================================\n// Events\n// ============================================================================\n\n/**\n * Base fields shared by all session events.\n */\ninterface SessionEventBase {\n\ttype: \"session\";\n\t/** All session entries (including pre-compaction history) */\n\tentries: SessionEntry[];\n\t/** Current session file path, or null in --no-session mode */\n\tsessionFile: string | null;\n\t/** Previous session file path, or null for \"start\" and \"clear\" */\n\tpreviousSessionFile: string | null;\n}\n\n/**\n * Event data for session events.\n * Discriminated union based on reason.\n *\n * Lifecycle:\n * - start: Initial session load\n * - before_switch / switch: Session switch (e.g., /resume command)\n * - before_clear / clear: Session clear (e.g., /clear command)\n * - before_branch / branch: Session branch (e.g., /branch command)\n * - before_compact / compact: Before/after context compaction\n * - shutdown: Process exit (SIGINT/SIGTERM)\n *\n * \"before_*\" events fire before the action and can be cancelled via SessionEventResult.\n * Other events fire after the action completes.\n */\nexport type SessionEvent =\n\t| (SessionEventBase & {\n\t\t\treason: \"start\" | \"switch\" | \"clear\" | \"before_switch\" | \"before_clear\" | \"shutdown\";\n\t })\n\t| (SessionEventBase & {\n\t\t\treason: \"branch\" | \"before_branch\";\n\t\t\t/** Index of the turn to branch from */\n\t\t\ttargetTurnIndex: number;\n\t })\n\t| (SessionEventBase & {\n\t\t\treason: \"before_compact\";\n\t\t\tcutPoint: CutPointResult;\n\t\t\t/** Summary from previous compaction, if any. Include this in your summary to preserve context. */\n\t\t\tpreviousSummary?: string;\n\t\t\t/** Messages that will be summarized and discarded */\n\t\t\tmessagesToSummarize: AppMessage[];\n\t\t\t/** Messages that will be kept after the summary (recent turns) */\n\t\t\tmessagesToKeep: AppMessage[];\n\t\t\ttokensBefore: number;\n\t\t\tcustomInstructions?: string;\n\t\t\tmodel: Model<any>;\n\t\t\t/** Resolve API key for any model (checks settings, OAuth, env vars) */\n\t\t\tresolveApiKey: (model: Model<any>) => Promise<string | undefined>;\n\t\t\t/** Abort signal - hooks should pass this to LLM calls and check it periodically */\n\t\t\tsignal: AbortSignal;\n\t })\n\t| (SessionEventBase & {\n\t\t\treason: \"compact\";\n\t\t\tcompactionEntry: CompactionEntry;\n\t\t\ttokensBefore: number;\n\t\t\t/** Whether the compaction entry was provided by a hook */\n\t\t\tfromHook: boolean;\n\t });\n\n/**\n * Event data for agent_start event.\n * Fired when an agent loop starts (once per user prompt).\n */\nexport interface AgentStartEvent {\n\ttype: \"agent_start\";\n}\n\n/**\n * Event data for agent_end event.\n */\nexport interface AgentEndEvent {\n\ttype: \"agent_end\";\n\tmessages: AppMessage[];\n}\n\n/**\n * Event data for turn_start event.\n */\nexport interface TurnStartEvent {\n\ttype: \"turn_start\";\n\tturnIndex: number;\n\ttimestamp: number;\n}\n\n/**\n * Event data for turn_end event.\n */\nexport interface TurnEndEvent {\n\ttype: \"turn_end\";\n\tturnIndex: number;\n\tmessage: AppMessage;\n\ttoolResults: ToolResultMessage[];\n}\n\n/**\n * Event data for tool_call event.\n * Fired before a tool is executed. Hooks can block execution.\n */\nexport interface ToolCallEvent {\n\ttype: \"tool_call\";\n\t/** Tool name (e.g., \"bash\", \"edit\", \"write\") */\n\ttoolName: string;\n\t/** Tool call ID */\n\ttoolCallId: string;\n\t/** Tool input parameters */\n\tinput: Record<string, unknown>;\n}\n\n/**\n * Base interface for tool_result events.\n */\ninterface ToolResultEventBase {\n\ttype: \"tool_result\";\n\t/** Tool call ID */\n\ttoolCallId: string;\n\t/** Tool input parameters */\n\tinput: Record<string, unknown>;\n\t/** Full content array (text and images) */\n\tcontent: (TextContent | ImageContent)[];\n\t/** Whether the tool execution was an error */\n\tisError: boolean;\n}\n\n/** Tool result event for bash tool */\nexport interface BashToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"bash\";\n\tdetails: BashToolDetails | undefined;\n}\n\n/** Tool result event for read tool */\nexport interface ReadToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"read\";\n\tdetails: ReadToolDetails | undefined;\n}\n\n/** Tool result event for edit tool */\nexport interface EditToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"edit\";\n\tdetails: undefined;\n}\n\n/** Tool result event for write tool */\nexport interface WriteToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"write\";\n\tdetails: undefined;\n}\n\n/** Tool result event for grep tool */\nexport interface GrepToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"grep\";\n\tdetails: GrepToolDetails | undefined;\n}\n\n/** Tool result event for find tool */\nexport interface FindToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"find\";\n\tdetails: FindToolDetails | undefined;\n}\n\n/** Tool result event for ls tool */\nexport interface LsToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"ls\";\n\tdetails: LsToolDetails | undefined;\n}\n\n/** Tool result event for custom/unknown tools */\nexport interface CustomToolResultEvent extends ToolResultEventBase {\n\ttoolName: string;\n\tdetails: unknown;\n}\n\n/**\n * Event data for tool_result event.\n * Fired after a tool is executed. Hooks can modify the result.\n * Use toolName to discriminate and get typed details.\n */\nexport type ToolResultEvent =\n\t| BashToolResultEvent\n\t| ReadToolResultEvent\n\t| EditToolResultEvent\n\t| WriteToolResultEvent\n\t| GrepToolResultEvent\n\t| FindToolResultEvent\n\t| LsToolResultEvent\n\t| CustomToolResultEvent;\n\n// Type guards for narrowing ToolResultEvent to specific tool types\nexport function isBashToolResult(e: ToolResultEvent): e is BashToolResultEvent {\n\treturn e.toolName === \"bash\";\n}\nexport function isReadToolResult(e: ToolResultEvent): e is ReadToolResultEvent {\n\treturn e.toolName === \"read\";\n}\nexport function isEditToolResult(e: ToolResultEvent): e is EditToolResultEvent {\n\treturn e.toolName === \"edit\";\n}\nexport function isWriteToolResult(e: ToolResultEvent): e is WriteToolResultEvent {\n\treturn e.toolName === \"write\";\n}\nexport function isGrepToolResult(e: ToolResultEvent): e is GrepToolResultEvent {\n\treturn e.toolName === \"grep\";\n}\nexport function isFindToolResult(e: ToolResultEvent): e is FindToolResultEvent {\n\treturn e.toolName === \"find\";\n}\nexport function isLsToolResult(e: ToolResultEvent): e is LsToolResultEvent {\n\treturn e.toolName === \"ls\";\n}\n\n/**\n * Union of all hook event types.\n */\nexport type HookEvent =\n\t| SessionEvent\n\t| AgentStartEvent\n\t| AgentEndEvent\n\t| TurnStartEvent\n\t| TurnEndEvent\n\t| ToolCallEvent\n\t| ToolResultEvent;\n\n// ============================================================================\n// Event Results\n// ============================================================================\n\n/**\n * Return type for tool_call event handlers.\n * Allows hooks to block tool execution.\n */\nexport interface ToolCallEventResult {\n\t/** If true, block the tool from executing */\n\tblock?: boolean;\n\t/** Reason for blocking (returned to LLM as error) */\n\treason?: string;\n}\n\n/**\n * Return type for tool_result event handlers.\n * Allows hooks to modify tool results.\n */\nexport interface ToolResultEventResult {\n\t/** Replacement content array (text and images) */\n\tcontent?: (TextContent | ImageContent)[];\n\t/** Replacement details */\n\tdetails?: unknown;\n\t/** Override isError flag */\n\tisError?: boolean;\n}\n\n/**\n * Return type for session event handlers.\n * Allows hooks to cancel \"before_*\" actions.\n */\nexport interface SessionEventResult {\n\t/** If true, cancel the pending action (switch, clear, or branch) */\n\tcancel?: boolean;\n\t/** If true (for before_branch only), skip restoring conversation to branch point while still creating the branched session file */\n\tskipConversationRestore?: boolean;\n\t/** Custom compaction entry (for before_compact event) */\n\tcompactionEntry?: CompactionEntry;\n}\n\n// ============================================================================\n// Hook API\n// ============================================================================\n\n/**\n * Handler function type for each event.\n */\nexport type HookHandler<E, R = void> = (event: E, ctx: HookEventContext) => Promise<R>;\n\n/**\n * HookAPI passed to hook factory functions.\n * Hooks use pi.on() to subscribe to events and pi.send() to inject messages.\n */\nexport interface HookAPI {\n\t// biome-ignore lint/suspicious/noConfusingVoidType: void allows handlers to not return anything\n\ton(event: \"session\", handler: HookHandler<SessionEvent, SessionEventResult | void>): void;\n\ton(event: \"agent_start\", handler: HookHandler<AgentStartEvent>): void;\n\ton(event: \"agent_end\", handler: HookHandler<AgentEndEvent>): void;\n\ton(event: \"turn_start\", handler: HookHandler<TurnStartEvent>): void;\n\ton(event: \"turn_end\", handler: HookHandler<TurnEndEvent>): void;\n\ton(event: \"tool_call\", handler: HookHandler<ToolCallEvent, ToolCallEventResult | undefined>): void;\n\ton(event: \"tool_result\", handler: HookHandler<ToolResultEvent, ToolResultEventResult | undefined>): void;\n\n\t/**\n\t * Send a message to the agent.\n\t * If the agent is streaming, the message is queued.\n\t * If the agent is idle, a new agent loop is started.\n\t */\n\tsend(text: string, attachments?: Attachment[]): void;\n}\n\n/**\n * Hook factory function type.\n * Hooks export a default function that receives the HookAPI.\n */\nexport type HookFactory = (pi: HookAPI) => void;\n\n// ============================================================================\n// Errors\n// ============================================================================\n\n/**\n * Error emitted when a hook fails.\n */\nexport interface HookError {\n\thookPath: string;\n\tevent: string;\n\terror: string;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoRH,mEAAmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,iBAAiB,CAAC,CAAkB,EAA6B;IAChF,OAAO,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC;AAAA,CAC9B;AACD,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,gBAAgB,CAAC,CAAkB,EAA4B;IAC9E,OAAO,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC;AAAA,CAC7B;AACD,MAAM,UAAU,cAAc,CAAC,CAAkB,EAA0B;IAC1E,OAAO,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC;AAAA,CAC3B","sourcesContent":["/**\n * Hook system types.\n *\n * Hooks are TypeScript modules that can subscribe to agent lifecycle events\n * and interact with the user via UI primitives.\n */\n\nimport type { AppMessage, Attachment } from \"@mariozechner/pi-agent-core\";\nimport type { ImageContent, Model, TextContent, ToolResultMessage } from \"@mariozechner/pi-ai\";\nimport type { CutPointResult } from \"../compaction.js\";\nimport type { CompactionEntry, SessionEntry } from \"../session-manager.js\";\nimport type {\n\tBashToolDetails,\n\tFindToolDetails,\n\tGrepToolDetails,\n\tLsToolDetails,\n\tReadToolDetails,\n} from \"../tools/index.js\";\n\n// ============================================================================\n// Execution Context\n// ============================================================================\n\n/**\n * Result of executing a command via ctx.exec()\n */\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\t/** True if the process was killed due to signal or timeout */\n\tkilled?: boolean;\n}\n\nexport interface ExecOptions {\n\t/** AbortSignal to cancel the process */\n\tsignal?: AbortSignal;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n}\n\n/**\n * UI context for hooks to request interactive UI from the harness.\n * Each mode (interactive, RPC, print) provides its own implementation.\n */\nexport interface HookUIContext {\n\t/**\n\t * Show a selector and return the user's choice.\n\t * @param title - Title to display\n\t * @param options - Array of string options\n\t * @returns Selected option string, or null if cancelled\n\t */\n\tselect(title: string, options: string[]): Promise<string | null>;\n\n\t/**\n\t * Show a confirmation dialog.\n\t * @returns true if confirmed, false if cancelled\n\t */\n\tconfirm(title: string, message: string): Promise<boolean>;\n\n\t/**\n\t * Show a text input dialog.\n\t * @returns User input, or null if cancelled\n\t */\n\tinput(title: string, placeholder?: string): Promise<string | null>;\n\n\t/**\n\t * Show a notification to the user.\n\t */\n\tnotify(message: string, type?: \"info\" | \"warning\" | \"error\"): void;\n}\n\n/**\n * Context passed to hook event handlers.\n */\nexport interface HookEventContext {\n\t/** Execute a command and return stdout/stderr/code */\n\texec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;\n\t/** UI methods for user interaction */\n\tui: HookUIContext;\n\t/** Whether UI is available (false in print mode) */\n\thasUI: boolean;\n\t/** Current working directory */\n\tcwd: string;\n\t/** Path to session file, or null if --no-session */\n\tsessionFile: string | null;\n}\n\n// ============================================================================\n// Events\n// ============================================================================\n\n/**\n * Base fields shared by all session events.\n */\ninterface SessionEventBase {\n\ttype: \"session\";\n\t/** All session entries (including pre-compaction history) */\n\tentries: SessionEntry[];\n\t/** Current session file path, or null in --no-session mode */\n\tsessionFile: string | null;\n\t/** Previous session file path, or null for \"start\" and \"new\" */\n\tpreviousSessionFile: string | null;\n}\n\n/**\n * Event data for session events.\n * Discriminated union based on reason.\n *\n * Lifecycle:\n * - start: Initial session load\n * - before_switch / switch: Session switch (e.g., /resume command)\n * - before_new / new: New session (e.g., /new command)\n * - before_branch / branch: Session branch (e.g., /branch command)\n * - before_compact / compact: Before/after context compaction\n * - shutdown: Process exit (SIGINT/SIGTERM)\n *\n * \"before_*\" events fire before the action and can be cancelled via SessionEventResult.\n * Other events fire after the action completes.\n */\nexport type SessionEvent =\n\t| (SessionEventBase & {\n\t\t\treason: \"start\" | \"switch\" | \"new\" | \"before_switch\" | \"before_new\" | \"shutdown\";\n\t })\n\t| (SessionEventBase & {\n\t\t\treason: \"branch\" | \"before_branch\";\n\t\t\t/** Index of the turn to branch from */\n\t\t\ttargetTurnIndex: number;\n\t })\n\t| (SessionEventBase & {\n\t\t\treason: \"before_compact\";\n\t\t\tcutPoint: CutPointResult;\n\t\t\t/** Summary from previous compaction, if any. Include this in your summary to preserve context. */\n\t\t\tpreviousSummary?: string;\n\t\t\t/** Messages that will be summarized and discarded */\n\t\t\tmessagesToSummarize: AppMessage[];\n\t\t\t/** Messages that will be kept after the summary (recent turns) */\n\t\t\tmessagesToKeep: AppMessage[];\n\t\t\ttokensBefore: number;\n\t\t\tcustomInstructions?: string;\n\t\t\tmodel: Model<any>;\n\t\t\t/** Resolve API key for any model (checks settings, OAuth, env vars) */\n\t\t\tresolveApiKey: (model: Model<any>) => Promise<string | undefined>;\n\t\t\t/** Abort signal - hooks should pass this to LLM calls and check it periodically */\n\t\t\tsignal: AbortSignal;\n\t })\n\t| (SessionEventBase & {\n\t\t\treason: \"compact\";\n\t\t\tcompactionEntry: CompactionEntry;\n\t\t\ttokensBefore: number;\n\t\t\t/** Whether the compaction entry was provided by a hook */\n\t\t\tfromHook: boolean;\n\t });\n\n/**\n * Event data for agent_start event.\n * Fired when an agent loop starts (once per user prompt).\n */\nexport interface AgentStartEvent {\n\ttype: \"agent_start\";\n}\n\n/**\n * Event data for agent_end event.\n */\nexport interface AgentEndEvent {\n\ttype: \"agent_end\";\n\tmessages: AppMessage[];\n}\n\n/**\n * Event data for turn_start event.\n */\nexport interface TurnStartEvent {\n\ttype: \"turn_start\";\n\tturnIndex: number;\n\ttimestamp: number;\n}\n\n/**\n * Event data for turn_end event.\n */\nexport interface TurnEndEvent {\n\ttype: \"turn_end\";\n\tturnIndex: number;\n\tmessage: AppMessage;\n\ttoolResults: ToolResultMessage[];\n}\n\n/**\n * Event data for tool_call event.\n * Fired before a tool is executed. Hooks can block execution.\n */\nexport interface ToolCallEvent {\n\ttype: \"tool_call\";\n\t/** Tool name (e.g., \"bash\", \"edit\", \"write\") */\n\ttoolName: string;\n\t/** Tool call ID */\n\ttoolCallId: string;\n\t/** Tool input parameters */\n\tinput: Record<string, unknown>;\n}\n\n/**\n * Base interface for tool_result events.\n */\ninterface ToolResultEventBase {\n\ttype: \"tool_result\";\n\t/** Tool call ID */\n\ttoolCallId: string;\n\t/** Tool input parameters */\n\tinput: Record<string, unknown>;\n\t/** Full content array (text and images) */\n\tcontent: (TextContent | ImageContent)[];\n\t/** Whether the tool execution was an error */\n\tisError: boolean;\n}\n\n/** Tool result event for bash tool */\nexport interface BashToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"bash\";\n\tdetails: BashToolDetails | undefined;\n}\n\n/** Tool result event for read tool */\nexport interface ReadToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"read\";\n\tdetails: ReadToolDetails | undefined;\n}\n\n/** Tool result event for edit tool */\nexport interface EditToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"edit\";\n\tdetails: undefined;\n}\n\n/** Tool result event for write tool */\nexport interface WriteToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"write\";\n\tdetails: undefined;\n}\n\n/** Tool result event for grep tool */\nexport interface GrepToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"grep\";\n\tdetails: GrepToolDetails | undefined;\n}\n\n/** Tool result event for find tool */\nexport interface FindToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"find\";\n\tdetails: FindToolDetails | undefined;\n}\n\n/** Tool result event for ls tool */\nexport interface LsToolResultEvent extends ToolResultEventBase {\n\ttoolName: \"ls\";\n\tdetails: LsToolDetails | undefined;\n}\n\n/** Tool result event for custom/unknown tools */\nexport interface CustomToolResultEvent extends ToolResultEventBase {\n\ttoolName: string;\n\tdetails: unknown;\n}\n\n/**\n * Event data for tool_result event.\n * Fired after a tool is executed. Hooks can modify the result.\n * Use toolName to discriminate and get typed details.\n */\nexport type ToolResultEvent =\n\t| BashToolResultEvent\n\t| ReadToolResultEvent\n\t| EditToolResultEvent\n\t| WriteToolResultEvent\n\t| GrepToolResultEvent\n\t| FindToolResultEvent\n\t| LsToolResultEvent\n\t| CustomToolResultEvent;\n\n// Type guards for narrowing ToolResultEvent to specific tool types\nexport function isBashToolResult(e: ToolResultEvent): e is BashToolResultEvent {\n\treturn e.toolName === \"bash\";\n}\nexport function isReadToolResult(e: ToolResultEvent): e is ReadToolResultEvent {\n\treturn e.toolName === \"read\";\n}\nexport function isEditToolResult(e: ToolResultEvent): e is EditToolResultEvent {\n\treturn e.toolName === \"edit\";\n}\nexport function isWriteToolResult(e: ToolResultEvent): e is WriteToolResultEvent {\n\treturn e.toolName === \"write\";\n}\nexport function isGrepToolResult(e: ToolResultEvent): e is GrepToolResultEvent {\n\treturn e.toolName === \"grep\";\n}\nexport function isFindToolResult(e: ToolResultEvent): e is FindToolResultEvent {\n\treturn e.toolName === \"find\";\n}\nexport function isLsToolResult(e: ToolResultEvent): e is LsToolResultEvent {\n\treturn e.toolName === \"ls\";\n}\n\n/**\n * Union of all hook event types.\n */\nexport type HookEvent =\n\t| SessionEvent\n\t| AgentStartEvent\n\t| AgentEndEvent\n\t| TurnStartEvent\n\t| TurnEndEvent\n\t| ToolCallEvent\n\t| ToolResultEvent;\n\n// ============================================================================\n// Event Results\n// ============================================================================\n\n/**\n * Return type for tool_call event handlers.\n * Allows hooks to block tool execution.\n */\nexport interface ToolCallEventResult {\n\t/** If true, block the tool from executing */\n\tblock?: boolean;\n\t/** Reason for blocking (returned to LLM as error) */\n\treason?: string;\n}\n\n/**\n * Return type for tool_result event handlers.\n * Allows hooks to modify tool results.\n */\nexport interface ToolResultEventResult {\n\t/** Replacement content array (text and images) */\n\tcontent?: (TextContent | ImageContent)[];\n\t/** Replacement details */\n\tdetails?: unknown;\n\t/** Override isError flag */\n\tisError?: boolean;\n}\n\n/**\n * Return type for session event handlers.\n * Allows hooks to cancel \"before_*\" actions.\n */\nexport interface SessionEventResult {\n\t/** If true, cancel the pending action (switch, clear, or branch) */\n\tcancel?: boolean;\n\t/** If true (for before_branch only), skip restoring conversation to branch point while still creating the branched session file */\n\tskipConversationRestore?: boolean;\n\t/** Custom compaction entry (for before_compact event) */\n\tcompactionEntry?: CompactionEntry;\n}\n\n// ============================================================================\n// Hook API\n// ============================================================================\n\n/**\n * Handler function type for each event.\n */\nexport type HookHandler<E, R = void> = (event: E, ctx: HookEventContext) => Promise<R>;\n\n/**\n * HookAPI passed to hook factory functions.\n * Hooks use pi.on() to subscribe to events and pi.send() to inject messages.\n */\nexport interface HookAPI {\n\t// biome-ignore lint/suspicious/noConfusingVoidType: void allows handlers to not return anything\n\ton(event: \"session\", handler: HookHandler<SessionEvent, SessionEventResult | void>): void;\n\ton(event: \"agent_start\", handler: HookHandler<AgentStartEvent>): void;\n\ton(event: \"agent_end\", handler: HookHandler<AgentEndEvent>): void;\n\ton(event: \"turn_start\", handler: HookHandler<TurnStartEvent>): void;\n\ton(event: \"turn_end\", handler: HookHandler<TurnEndEvent>): void;\n\ton(event: \"tool_call\", handler: HookHandler<ToolCallEvent, ToolCallEventResult | undefined>): void;\n\ton(event: \"tool_result\", handler: HookHandler<ToolResultEvent, ToolResultEventResult | undefined>): void;\n\n\t/**\n\t * Send a message to the agent.\n\t * If the agent is streaming, the message is queued.\n\t * If the agent is idle, a new agent loop is started.\n\t */\n\tsend(text: string, attachments?: Attachment[]): void;\n}\n\n/**\n * Hook factory function type.\n * Hooks export a default function that receives the HookAPI.\n */\nexport type HookFactory = (pi: HookAPI) => void;\n\n// ============================================================================\n// Errors\n// ============================================================================\n\n/**\n * Error emitted when a hook fails.\n */\nexport interface HookError {\n\thookPath: string;\n\tevent: string;\n\terror: string;\n}\n"]}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Model registry - manages built-in and custom models, provides API key resolution.
3
+ */
4
+ import { type Api, type Model } from "@mariozechner/pi-ai";
5
+ import type { AuthStorage } from "./auth-storage.js";
6
+ /**
7
+ * Model registry - loads and manages models, resolves API keys via AuthStorage.
8
+ */
9
+ export declare class ModelRegistry {
10
+ readonly authStorage: AuthStorage;
11
+ private modelsJsonPath;
12
+ private models;
13
+ private customProviderApiKeys;
14
+ private loadError;
15
+ constructor(authStorage: AuthStorage, modelsJsonPath?: string | null);
16
+ /**
17
+ * Reload models from disk (built-in + custom from models.json).
18
+ */
19
+ refresh(): void;
20
+ /**
21
+ * Get any error from loading models.json (null if no error).
22
+ */
23
+ getError(): string | null;
24
+ private loadModels;
25
+ private loadCustomModels;
26
+ private validateConfig;
27
+ private parseModels;
28
+ /**
29
+ * Get all models (built-in + custom).
30
+ * If models.json had errors, returns only built-in models.
31
+ */
32
+ getAll(): Model<Api>[];
33
+ /**
34
+ * Get only models that have valid API keys available.
35
+ */
36
+ getAvailable(): Promise<Model<Api>[]>;
37
+ /**
38
+ * Find a model by provider and ID.
39
+ */
40
+ find(provider: string, modelId: string): Model<Api> | null;
41
+ /**
42
+ * Get API key for a model.
43
+ */
44
+ getApiKey(model: Model<Api>): Promise<string | null>;
45
+ /**
46
+ * Check if a model is using OAuth credentials (subscription).
47
+ */
48
+ isUsingOAuth(model: Model<Api>): boolean;
49
+ }
50
+ //# sourceMappingURL=model-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-registry.d.ts","sourceRoot":"","sources":["../../src/core/model-registry.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACN,KAAK,GAAG,EAKR,KAAK,KAAK,EAEV,MAAM,qBAAqB,CAAC;AAI7B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAsErD;;GAEG;AACH,qBAAa,aAAa;IAMxB,QAAQ,CAAC,WAAW,EAAE,WAAW;IACjC,OAAO,CAAC,cAAc;IANvB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,qBAAqB,CAAkC;IAC/D,OAAO,CAAC,SAAS,CAAuB;IAExC,YACU,WAAW,EAAE,WAAW,EACzB,cAAc,GAAE,MAAM,GAAG,IAAW,EAa5C;IAED;;OAEG;IACH,OAAO,IAAI,IAAI,CAId;IAED;;OAEG;IACH,QAAQ,IAAI,MAAM,GAAG,IAAI,CAExB;IAED,OAAO,CAAC,UAAU;IAmClB,OAAO,CAAC,gBAAgB;IAyCxB,OAAO,CAAC,cAAc;IAuBtB,OAAO,CAAC,WAAW;IA6CnB;;;OAGG;IACH,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAErB;IAED;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAS1C;IAED;;OAEG;IACH,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAEzD;IAED;;OAEG;IACG,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEzD;IAED;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAGvC;CACD","sourcesContent":["/**\n * Model registry - manages built-in and custom models, provides API key resolution.\n */\n\nimport {\n\ttype Api,\n\tgetGitHubCopilotBaseUrl,\n\tgetModels,\n\tgetProviders,\n\ttype KnownProvider,\n\ttype Model,\n\tnormalizeDomain,\n} from \"@mariozechner/pi-ai\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport AjvModule from \"ajv\";\nimport { existsSync, readFileSync } from \"fs\";\nimport type { AuthStorage } from \"./auth-storage.js\";\n\nconst Ajv = (AjvModule as any).default || AjvModule;\n\n// Schema for OpenAI compatibility settings\nconst OpenAICompatSchema = Type.Object({\n\tsupportsStore: Type.Optional(Type.Boolean()),\n\tsupportsDeveloperRole: Type.Optional(Type.Boolean()),\n\tsupportsReasoningEffort: Type.Optional(Type.Boolean()),\n\tmaxTokensField: Type.Optional(Type.Union([Type.Literal(\"max_completion_tokens\"), Type.Literal(\"max_tokens\")])),\n});\n\n// Schema for custom model definition\nconst ModelDefinitionSchema = Type.Object({\n\tid: Type.String({ minLength: 1 }),\n\tname: Type.String({ minLength: 1 }),\n\tapi: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai-completions\"),\n\t\t\tType.Literal(\"openai-responses\"),\n\t\t\tType.Literal(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t]),\n\t),\n\treasoning: Type.Boolean(),\n\tinput: Type.Array(Type.Union([Type.Literal(\"text\"), Type.Literal(\"image\")])),\n\tcost: Type.Object({\n\t\tinput: Type.Number(),\n\t\toutput: Type.Number(),\n\t\tcacheRead: Type.Number(),\n\t\tcacheWrite: Type.Number(),\n\t}),\n\tcontextWindow: Type.Number(),\n\tmaxTokens: Type.Number(),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tcompat: Type.Optional(OpenAICompatSchema),\n});\n\nconst ProviderConfigSchema = Type.Object({\n\tbaseUrl: Type.String({ minLength: 1 }),\n\tapiKey: Type.String({ minLength: 1 }),\n\tapi: Type.Optional(\n\t\tType.Union([\n\t\t\tType.Literal(\"openai-completions\"),\n\t\t\tType.Literal(\"openai-responses\"),\n\t\t\tType.Literal(\"anthropic-messages\"),\n\t\t\tType.Literal(\"google-generative-ai\"),\n\t\t]),\n\t),\n\theaders: Type.Optional(Type.Record(Type.String(), Type.String())),\n\tauthHeader: Type.Optional(Type.Boolean()),\n\tmodels: Type.Array(ModelDefinitionSchema),\n});\n\nconst ModelsConfigSchema = Type.Object({\n\tproviders: Type.Record(Type.String(), ProviderConfigSchema),\n});\n\ntype ModelsConfig = Static<typeof ModelsConfigSchema>;\n\n/**\n * Resolve an API key config value to an actual key.\n * Checks environment variable first, then treats as literal.\n */\nfunction resolveApiKeyConfig(keyConfig: string): string | undefined {\n\tconst envValue = process.env[keyConfig];\n\tif (envValue) return envValue;\n\treturn keyConfig;\n}\n\n/**\n * Model registry - loads and manages models, resolves API keys via AuthStorage.\n */\nexport class ModelRegistry {\n\tprivate models: Model<Api>[] = [];\n\tprivate customProviderApiKeys: Map<string, string> = new Map();\n\tprivate loadError: string | null = null;\n\n\tconstructor(\n\t\treadonly authStorage: AuthStorage,\n\t\tprivate modelsJsonPath: string | null = null,\n\t) {\n\t\t// Set up fallback resolver for custom provider API keys\n\t\tthis.authStorage.setFallbackResolver((provider) => {\n\t\t\tconst keyConfig = this.customProviderApiKeys.get(provider);\n\t\t\tif (keyConfig) {\n\t\t\t\treturn resolveApiKeyConfig(keyConfig);\n\t\t\t}\n\t\t\treturn undefined;\n\t\t});\n\n\t\t// Load models\n\t\tthis.loadModels();\n\t}\n\n\t/**\n\t * Reload models from disk (built-in + custom from models.json).\n\t */\n\trefresh(): void {\n\t\tthis.customProviderApiKeys.clear();\n\t\tthis.loadError = null;\n\t\tthis.loadModels();\n\t}\n\n\t/**\n\t * Get any error from loading models.json (null if no error).\n\t */\n\tgetError(): string | null {\n\t\treturn this.loadError;\n\t}\n\n\tprivate loadModels(): void {\n\t\t// Load built-in models\n\t\tconst builtInModels: Model<Api>[] = [];\n\t\tfor (const provider of getProviders()) {\n\t\t\tconst providerModels = getModels(provider as KnownProvider);\n\t\t\tbuiltInModels.push(...(providerModels as Model<Api>[]));\n\t\t}\n\n\t\t// Load custom models from models.json (if path provided)\n\t\tlet customModels: Model<Api>[] = [];\n\t\tif (this.modelsJsonPath) {\n\t\t\tconst result = this.loadCustomModels(this.modelsJsonPath);\n\t\t\tif (result.error) {\n\t\t\t\tthis.loadError = result.error;\n\t\t\t\t// Keep built-in models even if custom models failed to load\n\t\t\t} else {\n\t\t\t\tcustomModels = result.models;\n\t\t\t}\n\t\t}\n\n\t\tconst combined = [...builtInModels, ...customModels];\n\n\t\t// Update github-copilot base URL based on OAuth credentials\n\t\tconst copilotCred = this.authStorage.get(\"github-copilot\");\n\t\tif (copilotCred?.type === \"oauth\") {\n\t\t\tconst domain = copilotCred.enterpriseUrl\n\t\t\t\t? (normalizeDomain(copilotCred.enterpriseUrl) ?? undefined)\n\t\t\t\t: undefined;\n\t\t\tconst baseUrl = getGitHubCopilotBaseUrl(copilotCred.access, domain);\n\t\t\tthis.models = combined.map((m) => (m.provider === \"github-copilot\" ? { ...m, baseUrl } : m));\n\t\t} else {\n\t\t\tthis.models = combined;\n\t\t}\n\t}\n\n\tprivate loadCustomModels(modelsJsonPath: string): { models: Model<Api>[]; error: string | null } {\n\t\tif (!existsSync(modelsJsonPath)) {\n\t\t\treturn { models: [], error: null };\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(modelsJsonPath, \"utf-8\");\n\t\t\tconst config: ModelsConfig = JSON.parse(content);\n\n\t\t\t// Validate schema\n\t\t\tconst ajv = new Ajv();\n\t\t\tconst validate = ajv.compile(ModelsConfigSchema);\n\t\t\tif (!validate(config)) {\n\t\t\t\tconst errors =\n\t\t\t\t\tvalidate.errors?.map((e: any) => ` - ${e.instancePath || \"root\"}: ${e.message}`).join(\"\\n\") ||\n\t\t\t\t\t\"Unknown schema error\";\n\t\t\t\treturn {\n\t\t\t\t\tmodels: [],\n\t\t\t\t\terror: `Invalid models.json schema:\\n${errors}\\n\\nFile: ${modelsJsonPath}`,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Additional validation\n\t\t\tthis.validateConfig(config);\n\n\t\t\t// Parse models\n\t\t\treturn { models: this.parseModels(config), error: null };\n\t\t} catch (error) {\n\t\t\tif (error instanceof SyntaxError) {\n\t\t\t\treturn {\n\t\t\t\t\tmodels: [],\n\t\t\t\t\terror: `Failed to parse models.json: ${error.message}\\n\\nFile: ${modelsJsonPath}`,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tmodels: [],\n\t\t\t\terror: `Failed to load models.json: ${error instanceof Error ? error.message : error}\\n\\nFile: ${modelsJsonPath}`,\n\t\t\t};\n\t\t}\n\t}\n\n\tprivate validateConfig(config: ModelsConfig): void {\n\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\tconst hasProviderApi = !!providerConfig.api;\n\n\t\t\tfor (const modelDef of providerConfig.models) {\n\t\t\t\tconst hasModelApi = !!modelDef.api;\n\n\t\t\t\tif (!hasProviderApi && !hasModelApi) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Provider ${providerName}, model ${modelDef.id}: no \"api\" specified. Set at provider or model level.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (!modelDef.id) throw new Error(`Provider ${providerName}: model missing \"id\"`);\n\t\t\t\tif (!modelDef.name) throw new Error(`Provider ${providerName}: model missing \"name\"`);\n\t\t\t\tif (modelDef.contextWindow <= 0)\n\t\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`);\n\t\t\t\tif (modelDef.maxTokens <= 0)\n\t\t\t\t\tthrow new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate parseModels(config: ModelsConfig): Model<Api>[] {\n\t\tconst models: Model<Api>[] = [];\n\n\t\tfor (const [providerName, providerConfig] of Object.entries(config.providers)) {\n\t\t\t// Store API key config for fallback resolver\n\t\t\tthis.customProviderApiKeys.set(providerName, providerConfig.apiKey);\n\n\t\t\tfor (const modelDef of providerConfig.models) {\n\t\t\t\tconst api = modelDef.api || providerConfig.api;\n\t\t\t\tif (!api) continue;\n\n\t\t\t\t// Merge headers: provider headers are base, model headers override\n\t\t\t\tlet headers =\n\t\t\t\t\tproviderConfig.headers || modelDef.headers\n\t\t\t\t\t\t? { ...providerConfig.headers, ...modelDef.headers }\n\t\t\t\t\t\t: undefined;\n\n\t\t\t\t// If authHeader is true, add Authorization header with resolved API key\n\t\t\t\tif (providerConfig.authHeader) {\n\t\t\t\t\tconst resolvedKey = resolveApiKeyConfig(providerConfig.apiKey);\n\t\t\t\t\tif (resolvedKey) {\n\t\t\t\t\t\theaders = { ...headers, Authorization: `Bearer ${resolvedKey}` };\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tmodels.push({\n\t\t\t\t\tid: modelDef.id,\n\t\t\t\t\tname: modelDef.name,\n\t\t\t\t\tapi: api as Api,\n\t\t\t\t\tprovider: providerName,\n\t\t\t\t\tbaseUrl: providerConfig.baseUrl,\n\t\t\t\t\treasoning: modelDef.reasoning,\n\t\t\t\t\tinput: modelDef.input as (\"text\" | \"image\")[],\n\t\t\t\t\tcost: modelDef.cost,\n\t\t\t\t\tcontextWindow: modelDef.contextWindow,\n\t\t\t\t\tmaxTokens: modelDef.maxTokens,\n\t\t\t\t\theaders,\n\t\t\t\t\tcompat: modelDef.compat,\n\t\t\t\t} as Model<Api>);\n\t\t\t}\n\t\t}\n\n\t\treturn models;\n\t}\n\n\t/**\n\t * Get all models (built-in + custom).\n\t * If models.json had errors, returns only built-in models.\n\t */\n\tgetAll(): Model<Api>[] {\n\t\treturn this.models;\n\t}\n\n\t/**\n\t * Get only models that have valid API keys available.\n\t */\n\tasync getAvailable(): Promise<Model<Api>[]> {\n\t\tconst available: Model<Api>[] = [];\n\t\tfor (const model of this.models) {\n\t\t\tconst apiKey = await this.authStorage.getApiKey(model.provider);\n\t\t\tif (apiKey) {\n\t\t\t\tavailable.push(model);\n\t\t\t}\n\t\t}\n\t\treturn available;\n\t}\n\n\t/**\n\t * Find a model by provider and ID.\n\t */\n\tfind(provider: string, modelId: string): Model<Api> | null {\n\t\treturn this.models.find((m) => m.provider === provider && m.id === modelId) ?? null;\n\t}\n\n\t/**\n\t * Get API key for a model.\n\t */\n\tasync getApiKey(model: Model<Api>): Promise<string | null> {\n\t\treturn this.authStorage.getApiKey(model.provider);\n\t}\n\n\t/**\n\t * Check if a model is using OAuth credentials (subscription).\n\t */\n\tisUsingOAuth(model: Model<Api>): boolean {\n\t\tconst cred = this.authStorage.get(model.provider);\n\t\treturn cred?.type === \"oauth\";\n\t}\n}\n"]}