panopticon-cli 0.4.30 → 0.4.31

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 (41) hide show
  1. package/dist/{agents-ZG4JIPO2.js → agents-GQDAKTEQ.js} +4 -3
  2. package/dist/{chunk-SIAUVHVO.js → chunk-3XAB4IXF.js} +4 -2
  3. package/dist/{chunk-SIAUVHVO.js.map → chunk-3XAB4IXF.js.map} +1 -1
  4. package/dist/chunk-ELK6Q7QI.js +545 -0
  5. package/dist/chunk-ELK6Q7QI.js.map +1 -0
  6. package/dist/{chunk-VH27COUW.js → chunk-HNEWTIR3.js} +37 -5
  7. package/dist/chunk-HNEWTIR3.js.map +1 -0
  8. package/dist/chunk-LYSBSZYV.js +1523 -0
  9. package/dist/chunk-LYSBSZYV.js.map +1 -0
  10. package/dist/{chunk-42O6XJWZ.js → chunk-TMXN7THF.js} +14 -14
  11. package/dist/{chunk-VTMXR7JF.js → chunk-VU4FLXV5.js} +47 -40
  12. package/dist/{chunk-VTMXR7JF.js.map → chunk-VU4FLXV5.js.map} +1 -1
  13. package/dist/cli/index.js +46 -28
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/{config-QWTS63TU.js → config-BOAMSKTF.js} +4 -2
  16. package/dist/dashboard/public/assets/{index-Dhwz2I7n.js → index-izWbAt7V.js} +65 -65
  17. package/dist/dashboard/public/index.html +1 -1
  18. package/dist/dashboard/server.js +94155 -23826
  19. package/dist/index.js +16 -14
  20. package/dist/index.js.map +1 -1
  21. package/dist/{remote-workspace-FNXLMNBG.js → remote-workspace-2G6V2KNP.js} +7 -5
  22. package/dist/{remote-workspace-FNXLMNBG.js.map → remote-workspace-2G6V2KNP.js.map} +1 -1
  23. package/dist/{specialist-context-QVRSHPYN.js → specialist-context-6SE5VRRC.js} +2 -2
  24. package/dist/{specialist-logs-7TQMHCUN.js → specialist-logs-EXLOQHQ2.js} +2 -2
  25. package/dist/{specialists-UUXB5KFV.js → specialists-BRUHPAXE.js} +2 -2
  26. package/dist/{traefik-7OLLXUD7.js → traefik-CUJM6K5Z.js} +3 -3
  27. package/package.json +3 -2
  28. package/scripts/record-cost-event.js +243 -79
  29. package/scripts/record-cost-event.ts +128 -68
  30. package/dist/chunk-J3J32DIR.js +0 -279
  31. package/dist/chunk-J3J32DIR.js.map +0 -1
  32. package/dist/chunk-SUMIHS2B.js +0 -1714
  33. package/dist/chunk-SUMIHS2B.js.map +0 -1
  34. package/dist/chunk-VH27COUW.js.map +0 -1
  35. /package/dist/{agents-ZG4JIPO2.js.map → agents-GQDAKTEQ.js.map} +0 -0
  36. /package/dist/{chunk-42O6XJWZ.js.map → chunk-TMXN7THF.js.map} +0 -0
  37. /package/dist/{config-QWTS63TU.js.map → config-BOAMSKTF.js.map} +0 -0
  38. /package/dist/{specialist-context-QVRSHPYN.js.map → specialist-context-6SE5VRRC.js.map} +0 -0
  39. /package/dist/{specialist-logs-7TQMHCUN.js.map → specialist-logs-EXLOQHQ2.js.map} +0 -0
  40. /package/dist/{specialists-UUXB5KFV.js.map → specialists-BRUHPAXE.js.map} +0 -0
  41. /package/dist/{traefik-7OLLXUD7.js.map → traefik-CUJM6K5Z.js.map} +0 -0
@@ -1,113 +1,173 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Record a cost event from Claude Code tool usage
4
- * Called by heartbeat-hook with JSON input on stdin
3
+ * Record cost events from Claude Code transcript data
4
+ * Called by heartbeat-hook with PostToolUse JSON on stdin
5
+ *
6
+ * Claude Code's PostToolUse events do NOT include token usage.
7
+ * Instead, we read usage from the transcript JSONL file
8
+ * (available via the transcript_path field in the event).
9
+ *
10
+ * Uses byte-offset tracking to efficiently process only new
11
+ * transcript entries on each invocation.
5
12
  */
6
13
 
7
- import { readFileSync } from 'fs';
14
+ import { readFileSync, existsSync, writeFileSync, mkdirSync, openSync, readSync, fstatSync, closeSync } from 'fs';
15
+ import { join } from 'path';
16
+ import { homedir } from 'os';
8
17
  import { calculateCost, getPricing, AIProvider } from '../src/lib/cost.js';
9
18
  import { appendCostEvent } from '../src/lib/costs/events.js';
10
19
 
11
20
  // ============== Types ==============
12
21
 
13
- interface UsageData {
22
+ interface PostToolUseEvent {
23
+ session_id?: string;
24
+ transcript_path?: string;
25
+ tool_name?: string;
26
+ tool_use_id?: string;
27
+ }
28
+
29
+ interface TranscriptUsage {
14
30
  input_tokens?: number;
15
31
  output_tokens?: number;
16
32
  cache_read_input_tokens?: number;
17
33
  cache_creation_input_tokens?: number;
18
34
  }
19
35
 
20
- interface ToolInfo {
21
- model?: string;
22
- usage?: UsageData;
36
+ interface TranscriptEntry {
37
+ type?: string;
23
38
  message?: {
24
39
  model?: string;
25
- usage?: UsageData;
40
+ usage?: TranscriptUsage;
26
41
  };
42
+ requestId?: string;
27
43
  }
28
44
 
29
45
  // ============== Main ==============
30
46
 
31
- // Read tool info from stdin
32
- let toolInfo: ToolInfo;
47
+ // Read PostToolUse event from stdin
48
+ let event: PostToolUseEvent;
33
49
  try {
34
50
  const input = readFileSync(0, 'utf-8');
35
- toolInfo = JSON.parse(input) as ToolInfo;
36
- } catch (err) {
37
- // Silent failure - don't break Claude Code execution
51
+ event = JSON.parse(input) as PostToolUseEvent;
52
+ } catch {
38
53
  process.exit(0);
39
54
  }
40
55
 
41
- // Extract usage data from tool info
42
- const usage: UsageData | undefined = toolInfo?.usage || toolInfo?.message?.usage;
43
- if (!usage) {
44
- // No usage data - not a Claude API call
56
+ // Need transcript path to read usage data
57
+ const transcriptPath = event?.transcript_path;
58
+ if (!transcriptPath || !existsSync(transcriptPath)) {
45
59
  process.exit(0);
46
60
  }
47
61
 
48
- // Extract token counts
49
- const inputTokens = usage.input_tokens || 0;
50
- const outputTokens = usage.output_tokens || 0;
51
- const cacheReadTokens = usage.cache_read_input_tokens || 0;
52
- const cacheWriteTokens = usage.cache_creation_input_tokens || 0;
62
+ const sessionId = event?.session_id || 'unknown';
53
63
 
54
- // Must have at least some tokens to record
55
- if (inputTokens === 0 && outputTokens === 0 && cacheReadTokens === 0 && cacheWriteTokens === 0) {
56
- process.exit(0);
57
- }
64
+ // State tracking: byte offset per session to avoid re-processing
65
+ const stateDir = join(process.env.HOME || homedir(), '.panopticon', 'costs', 'state');
66
+ mkdirSync(stateDir, { recursive: true });
67
+ const stateFile = join(stateDir, `${sessionId}.offset`);
58
68
 
59
- // Extract model name
60
- const model: string = toolInfo?.model || toolInfo?.message?.model || 'claude-sonnet-4';
69
+ let lastOffset = 0;
70
+ if (existsSync(stateFile)) {
71
+ try {
72
+ lastOffset = parseInt(readFileSync(stateFile, 'utf-8').trim(), 10) || 0;
73
+ } catch { /* start from 0 */ }
74
+ }
61
75
 
62
- // Determine provider from model name
63
- let provider: AIProvider = 'anthropic';
64
- if (model.includes('gpt')) {
65
- provider = 'openai';
66
- } else if (model.includes('gemini')) {
67
- provider = 'google';
76
+ // Read only NEW content from the transcript (efficient for large files)
77
+ let fd: number;
78
+ try {
79
+ fd = openSync(transcriptPath, 'r');
80
+ } catch {
81
+ process.exit(0);
68
82
  }
69
83
 
70
- // Get pricing and calculate cost
71
- const pricing = getPricing(provider, model);
72
- if (!pricing) {
73
- console.warn(`No pricing found for ${provider}/${model}`);
84
+ const stat = fstatSync(fd);
85
+ if (stat.size <= lastOffset) {
86
+ closeSync(fd);
87
+ // Save current size even if no new content (handles file truncation)
88
+ writeFileSync(stateFile, String(stat.size), 'utf-8');
74
89
  process.exit(0);
75
90
  }
76
91
 
77
- const cost = calculateCost({
78
- inputTokens,
79
- outputTokens,
80
- cacheReadTokens,
81
- cacheWriteTokens,
82
- cacheTTL: '5m',
83
- }, pricing);
84
-
85
- // Get agent and issue context from environment
86
- // PANOPTICON_AGENT_ID should always be set by pan work or heartbeat-hook
87
- // If not set, use a fallback that makes it clear costs are unattributed
92
+ const bytesToRead = stat.size - lastOffset;
93
+ const buffer = Buffer.alloc(bytesToRead);
94
+ readSync(fd, buffer, 0, bytesToRead, lastOffset);
95
+ closeSync(fd);
96
+
97
+ const newContent = buffer.toString('utf-8');
98
+ const lines = newContent.split('\n');
99
+
100
+ // Get agent/issue context from environment
88
101
  const agentId: string = process.env.PANOPTICON_AGENT_ID || 'unattributed';
89
102
  const issueId: string = process.env.PANOPTICON_ISSUE_ID || 'UNKNOWN';
90
103
  const sessionType: string = process.env.PANOPTICON_SESSION_TYPE || 'implementation';
91
104
 
92
- // Record cost event
93
- try {
94
- appendCostEvent({
95
- ts: new Date().toISOString(),
96
- type: 'cost',
97
- agentId,
98
- issueId,
99
- sessionType,
100
- provider,
101
- model,
102
- input: inputTokens,
103
- output: outputTokens,
104
- cacheRead: cacheReadTokens,
105
- cacheWrite: cacheWriteTokens,
106
- cost,
107
- });
108
- } catch (err) {
109
- // Silent failure - don't break Claude Code execution
110
- console.error('Failed to record cost event:', err);
105
+ // Process new transcript lines looking for assistant messages with usage
106
+ for (const line of lines) {
107
+ if (!line.trim()) continue;
108
+
109
+ try {
110
+ const entry = JSON.parse(line) as TranscriptEntry;
111
+
112
+ // Only process assistant messages that have usage data
113
+ if (entry.type !== 'assistant' || !entry.message?.usage) {
114
+ continue;
115
+ }
116
+
117
+ const usage = entry.message.usage;
118
+ const model: string = entry.message.model || 'claude-sonnet-4';
119
+
120
+ const inputTokens = usage.input_tokens || 0;
121
+ const outputTokens = usage.output_tokens || 0;
122
+ const cacheReadTokens = usage.cache_read_input_tokens || 0;
123
+ const cacheWriteTokens = usage.cache_creation_input_tokens || 0;
124
+
125
+ // Skip entries with zero tokens
126
+ if (inputTokens === 0 && outputTokens === 0 && cacheReadTokens === 0 && cacheWriteTokens === 0) {
127
+ continue;
128
+ }
129
+
130
+ // Determine provider from model name
131
+ let provider: AIProvider = 'anthropic';
132
+ if (model.includes('gpt')) {
133
+ provider = 'openai';
134
+ } else if (model.includes('gemini')) {
135
+ provider = 'google';
136
+ }
137
+
138
+ // Get pricing and calculate cost
139
+ const pricing = getPricing(provider, model);
140
+ if (!pricing) continue;
141
+
142
+ const cost = calculateCost({
143
+ inputTokens,
144
+ outputTokens,
145
+ cacheReadTokens,
146
+ cacheWriteTokens,
147
+ cacheTTL: '5m',
148
+ }, pricing);
149
+
150
+ // Record the cost event
151
+ appendCostEvent({
152
+ ts: new Date().toISOString(),
153
+ type: 'cost',
154
+ agentId,
155
+ issueId,
156
+ sessionType,
157
+ provider,
158
+ model,
159
+ input: inputTokens,
160
+ output: outputTokens,
161
+ cacheRead: cacheReadTokens,
162
+ cacheWrite: cacheWriteTokens,
163
+ cost,
164
+ });
165
+ } catch {
166
+ // Skip malformed lines silently
167
+ }
111
168
  }
112
169
 
170
+ // Save new byte offset for next invocation
171
+ writeFileSync(stateFile, String(stat.size), 'utf-8');
172
+
113
173
  process.exit(0);
@@ -1,279 +0,0 @@
1
- import {
2
- SETTINGS_FILE,
3
- init_paths
4
- } from "./chunk-6HXKTOD7.js";
5
- import {
6
- __esm,
7
- init_esm_shims
8
- } from "./chunk-ZHC57RCV.js";
9
-
10
- // src/lib/settings.ts
11
- import { readFileSync, writeFileSync, existsSync } from "fs";
12
- function deepMerge(defaults, overrides) {
13
- const result = { ...defaults };
14
- for (const key of Object.keys(overrides)) {
15
- const defaultVal = defaults[key];
16
- const overrideVal = overrides[key];
17
- if (overrideVal === void 0) continue;
18
- if (typeof defaultVal === "object" && defaultVal !== null && !Array.isArray(defaultVal) && typeof overrideVal === "object" && overrideVal !== null && !Array.isArray(overrideVal)) {
19
- result[key] = deepMerge(defaultVal, overrideVal);
20
- } else {
21
- result[key] = overrideVal;
22
- }
23
- }
24
- return result;
25
- }
26
- function loadSettings() {
27
- let settings;
28
- if (!existsSync(SETTINGS_FILE)) {
29
- settings = getDefaultSettings();
30
- } else {
31
- try {
32
- const content = readFileSync(SETTINGS_FILE, "utf8");
33
- const parsed = JSON.parse(content);
34
- settings = deepMerge(DEFAULT_SETTINGS, parsed);
35
- } catch (error) {
36
- console.error("Warning: Failed to parse settings.json, using defaults");
37
- settings = getDefaultSettings();
38
- }
39
- }
40
- const envApiKeys = {};
41
- if (process.env.OPENAI_API_KEY) envApiKeys.openai = process.env.OPENAI_API_KEY;
42
- if (process.env.GOOGLE_API_KEY) envApiKeys.google = process.env.GOOGLE_API_KEY;
43
- if (process.env.ZAI_API_KEY) envApiKeys.zai = process.env.ZAI_API_KEY;
44
- if (process.env.KIMI_API_KEY) envApiKeys.kimi = process.env.KIMI_API_KEY;
45
- settings.api_keys = {
46
- ...envApiKeys,
47
- ...settings.api_keys
48
- };
49
- return settings;
50
- }
51
- function saveSettings(settings) {
52
- const content = JSON.stringify(settings, null, 2);
53
- writeFileSync(SETTINGS_FILE, content, "utf8");
54
- }
55
- function validateSettings(settings) {
56
- if (!settings.models) {
57
- return "Missing models configuration";
58
- }
59
- if (!settings.models.specialists) {
60
- return "Missing specialists configuration";
61
- }
62
- const specialists = settings.models.specialists;
63
- if (!specialists.review_agent || !specialists.test_agent || !specialists.merge_agent) {
64
- return "Missing specialist agent model configuration";
65
- }
66
- if (!settings.models.planning_agent) {
67
- return "Missing planning_agent configuration";
68
- }
69
- if (!settings.models.complexity) {
70
- return "Missing complexity configuration";
71
- }
72
- const complexity = settings.models.complexity;
73
- const requiredLevels = ["trivial", "simple", "medium", "complex", "expert"];
74
- for (const level of requiredLevels) {
75
- if (!complexity[level]) {
76
- return `Missing complexity level: ${level}`;
77
- }
78
- }
79
- if (!settings.api_keys) {
80
- return "Missing api_keys configuration";
81
- }
82
- return null;
83
- }
84
- function getDefaultSettings() {
85
- return JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
86
- }
87
- function getAvailableModels(settings) {
88
- const anthropicModels = [
89
- "claude-opus-4-6",
90
- "claude-sonnet-4-5",
91
- "claude-haiku-4-5"
92
- ];
93
- const openaiModels = settings.api_keys.openai ? ["gpt-5.2-codex", "o3-deep-research", "gpt-4o", "gpt-4o-mini"] : [];
94
- const googleModels = settings.api_keys.google ? ["gemini-3-pro-preview", "gemini-3-flash-preview"] : [];
95
- const zaiModels = settings.api_keys.zai ? ["glm-4.7", "glm-4.7-flash"] : [];
96
- const kimiModels = settings.api_keys.kimi ? ["kimi-k2", "kimi-k2.5"] : [];
97
- return {
98
- anthropic: anthropicModels,
99
- openai: openaiModels,
100
- google: googleModels,
101
- zai: zaiModels,
102
- kimi: kimiModels
103
- };
104
- }
105
- function isAnthropicModel(modelId) {
106
- return modelId.startsWith("claude-");
107
- }
108
- function getClaudeModelFlag(modelId) {
109
- const modelMap = {
110
- "claude-opus-4-6": "opus",
111
- "claude-sonnet-4-5": "sonnet",
112
- "claude-haiku-4-5": "haiku"
113
- };
114
- return modelMap[modelId] || "sonnet";
115
- }
116
- function getAgentCommand(modelId) {
117
- if (isAnthropicModel(modelId)) {
118
- return {
119
- command: "claude",
120
- args: ["--model", getClaudeModelFlag(modelId)]
121
- };
122
- }
123
- return {
124
- command: "claude-code-router",
125
- args: []
126
- };
127
- }
128
- var DEFAULT_SETTINGS;
129
- var init_settings = __esm({
130
- "src/lib/settings.ts"() {
131
- "use strict";
132
- init_esm_shims();
133
- init_paths();
134
- DEFAULT_SETTINGS = {
135
- models: {
136
- specialists: {
137
- review_agent: "claude-opus-4-6",
138
- test_agent: "claude-sonnet-4-5",
139
- merge_agent: "claude-sonnet-4-5"
140
- },
141
- planning_agent: "claude-opus-4-6",
142
- status_review: "claude-opus-4-6",
143
- complexity: {
144
- trivial: "claude-haiku-4-5",
145
- simple: "claude-haiku-4-5",
146
- medium: "kimi-k2.5",
147
- complex: "kimi-k2.5",
148
- expert: "claude-opus-4-6"
149
- }
150
- },
151
- api_keys: {}
152
- };
153
- }
154
- });
155
-
156
- // src/lib/providers.ts
157
- function getProviderForModel(modelId) {
158
- if (["claude-opus-4-6", "claude-sonnet-4-5", "claude-haiku-4-5"].includes(modelId)) {
159
- return PROVIDERS.anthropic;
160
- }
161
- if (["gpt-5.2-codex", "o3-deep-research", "gpt-4o", "gpt-4o-mini"].includes(modelId)) {
162
- return PROVIDERS.openai;
163
- }
164
- if (["gemini-3-pro-preview", "gemini-3-flash-preview"].includes(modelId)) {
165
- return PROVIDERS.google;
166
- }
167
- if (["glm-4.7", "glm-4.7-flash"].includes(modelId)) {
168
- return PROVIDERS.zai;
169
- }
170
- if (["kimi-k2", "kimi-k2.5"].includes(modelId)) {
171
- return PROVIDERS.kimi;
172
- }
173
- return PROVIDERS.anthropic;
174
- }
175
- function requiresRouter(provider) {
176
- return PROVIDERS[provider].compatibility === "router";
177
- }
178
- function getRouterProviders() {
179
- return Object.values(PROVIDERS).filter((p) => p.compatibility === "router");
180
- }
181
- function getDirectProviders() {
182
- return Object.values(PROVIDERS).filter((p) => p.compatibility === "direct");
183
- }
184
- function needsRouter(apiKeys) {
185
- return !!(apiKeys.openai || apiKeys.google);
186
- }
187
- function getProviderEnv(provider, apiKey) {
188
- if (provider.compatibility === "direct") {
189
- const env = {};
190
- if (provider.baseUrl) {
191
- env.ANTHROPIC_BASE_URL = provider.baseUrl;
192
- }
193
- if (provider.name !== "anthropic") {
194
- env.ANTHROPIC_AUTH_TOKEN = apiKey;
195
- }
196
- if (provider.name === "zai") {
197
- env.API_TIMEOUT_MS = "300000";
198
- }
199
- return env;
200
- } else {
201
- return {
202
- ANTHROPIC_BASE_URL: "http://localhost:3456",
203
- ANTHROPIC_AUTH_TOKEN: "router-managed"
204
- };
205
- }
206
- }
207
- var PROVIDERS;
208
- var init_providers = __esm({
209
- "src/lib/providers.ts"() {
210
- "use strict";
211
- init_esm_shims();
212
- PROVIDERS = {
213
- anthropic: {
214
- name: "anthropic",
215
- displayName: "Anthropic",
216
- compatibility: "direct",
217
- models: ["claude-opus-4-6", "claude-sonnet-4-5", "claude-haiku-4-5"],
218
- tested: true,
219
- description: "Native Claude API"
220
- },
221
- kimi: {
222
- name: "kimi",
223
- displayName: "Kimi (Moonshot AI)",
224
- compatibility: "direct",
225
- baseUrl: "https://api.kimi.com/coding/",
226
- models: [],
227
- // Kimi uses same model names as Anthropic
228
- tested: true,
229
- description: "Anthropic-compatible API, tested 2026-01-28"
230
- },
231
- zai: {
232
- name: "zai",
233
- displayName: "Z.AI (GLM)",
234
- compatibility: "direct",
235
- baseUrl: "https://api.z.ai/api/anthropic",
236
- models: ["glm-4.7", "glm-4.7-flash"],
237
- tested: true,
238
- description: "Anthropic-compatible API, tested 2026-01-28"
239
- },
240
- openai: {
241
- name: "openai",
242
- displayName: "OpenAI",
243
- compatibility: "router",
244
- models: ["gpt-5.2-codex", "o3-deep-research", "gpt-4o", "gpt-4o-mini"],
245
- tested: false,
246
- description: "Requires claude-code-router for API translation"
247
- },
248
- google: {
249
- name: "google",
250
- displayName: "Google (Gemini)",
251
- compatibility: "router",
252
- models: ["gemini-3-pro-preview", "gemini-3-flash-preview"],
253
- tested: false,
254
- description: "Requires claude-code-router for API translation"
255
- }
256
- };
257
- }
258
- });
259
-
260
- export {
261
- loadSettings,
262
- saveSettings,
263
- validateSettings,
264
- getDefaultSettings,
265
- getAvailableModels,
266
- isAnthropicModel,
267
- getClaudeModelFlag,
268
- getAgentCommand,
269
- init_settings,
270
- PROVIDERS,
271
- getProviderForModel,
272
- requiresRouter,
273
- getRouterProviders,
274
- getDirectProviders,
275
- needsRouter,
276
- getProviderEnv,
277
- init_providers
278
- };
279
- //# sourceMappingURL=chunk-J3J32DIR.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/lib/settings.ts","../src/lib/providers.ts"],"sourcesContent":["import { readFileSync, writeFileSync, existsSync } from 'fs';\nimport { SETTINGS_FILE } from './paths.js';\n\n// Model identifiers\nexport type AnthropicModel = 'claude-opus-4-6' | 'claude-sonnet-4-5' | 'claude-haiku-4-5';\nexport type OpenAIModel = 'gpt-5.2-codex' | 'o3-deep-research' | 'gpt-4o' | 'gpt-4o-mini';\nexport type GoogleModel = 'gemini-3-pro-preview' | 'gemini-3-flash-preview' | 'gemini-2.5-pro' | 'gemini-2.5-flash';\nexport type ZAIModel = 'glm-4.7' | 'glm-4.7-flash';\nexport type KimiModel = 'kimi-k2' | 'kimi-k2.5';\nexport type ModelId = AnthropicModel | OpenAIModel | GoogleModel | ZAIModel | KimiModel;\n\n// Task complexity levels\nexport type ComplexityLevel = 'trivial' | 'simple' | 'medium' | 'complex' | 'expert';\n\n// Specialist agent types\nexport interface SpecialistModels {\n review_agent: ModelId;\n test_agent: ModelId;\n merge_agent: ModelId;\n}\n\n// Complexity-based model mapping\nexport type ComplexityModels = {\n [K in ComplexityLevel]: ModelId;\n};\n\n// All model configuration\nexport interface ModelsConfig {\n specialists: SpecialistModels;\n planning_agent: ModelId;\n status_review: ModelId;\n complexity: ComplexityModels;\n}\n\n// API keys for external providers\nexport interface ApiKeysConfig {\n openai?: string;\n google?: string;\n zai?: string;\n kimi?: string;\n}\n\n// Complete settings structure\nexport interface SettingsConfig {\n models: ModelsConfig;\n api_keys: ApiKeysConfig;\n}\n\n// Default settings - match optimal defaults from settings-api.ts\nconst DEFAULT_SETTINGS: SettingsConfig = {\n models: {\n specialists: {\n review_agent: 'claude-opus-4-6',\n test_agent: 'claude-sonnet-4-5',\n merge_agent: 'claude-sonnet-4-5',\n },\n planning_agent: 'claude-opus-4-6',\n status_review: 'claude-opus-4-6',\n complexity: {\n trivial: 'claude-haiku-4-5',\n simple: 'claude-haiku-4-5',\n medium: 'kimi-k2.5',\n complex: 'kimi-k2.5',\n expert: 'claude-opus-4-6',\n },\n },\n api_keys: {},\n};\n\n/**\n * Deep merge utility that recursively merges objects.\n * - Recursively merges nested objects\n * - User values take precedence over defaults\n */\nfunction deepMerge<T extends object>(defaults: T, overrides: Partial<T>): T {\n const result = { ...defaults };\n\n for (const key of Object.keys(overrides) as (keyof T)[]) {\n const defaultVal = defaults[key];\n const overrideVal = overrides[key];\n\n // Skip undefined values in overrides\n if (overrideVal === undefined) continue;\n\n // Deep merge if both values are non-array objects\n if (\n typeof defaultVal === 'object' &&\n defaultVal !== null &&\n !Array.isArray(defaultVal) &&\n typeof overrideVal === 'object' &&\n overrideVal !== null &&\n !Array.isArray(overrideVal)\n ) {\n result[key] = deepMerge(defaultVal, overrideVal as any);\n } else {\n // For primitives or null - override wins\n result[key] = overrideVal as T[keyof T];\n }\n }\n\n return result;\n}\n\n/**\n * Load settings from ~/.panopticon/settings.json\n * Returns default settings if file doesn't exist or is invalid\n * Also loads API keys from environment variables as fallback\n */\nexport function loadSettings(): SettingsConfig {\n let settings: SettingsConfig;\n\n if (!existsSync(SETTINGS_FILE)) {\n settings = getDefaultSettings();\n } else {\n try {\n const content = readFileSync(SETTINGS_FILE, 'utf8');\n const parsed = JSON.parse(content) as Partial<SettingsConfig>;\n settings = deepMerge(DEFAULT_SETTINGS, parsed);\n } catch (error) {\n console.error('Warning: Failed to parse settings.json, using defaults');\n settings = getDefaultSettings();\n }\n }\n\n // Load API keys from environment variables as fallback\n // This allows using ~/.panopticon.env for API keys\n const envApiKeys: ApiKeysConfig = {};\n if (process.env.OPENAI_API_KEY) envApiKeys.openai = process.env.OPENAI_API_KEY;\n if (process.env.GOOGLE_API_KEY) envApiKeys.google = process.env.GOOGLE_API_KEY;\n if (process.env.ZAI_API_KEY) envApiKeys.zai = process.env.ZAI_API_KEY;\n if (process.env.KIMI_API_KEY) envApiKeys.kimi = process.env.KIMI_API_KEY;\n\n // Merge env vars as fallback (settings.json takes precedence)\n settings.api_keys = {\n ...envApiKeys,\n ...settings.api_keys,\n };\n\n return settings;\n}\n\n/**\n * Save settings to ~/.panopticon/settings.json\n * Writes with pretty formatting (2-space indent)\n */\nexport function saveSettings(settings: SettingsConfig): void {\n const content = JSON.stringify(settings, null, 2);\n writeFileSync(SETTINGS_FILE, content, 'utf8');\n}\n\n/**\n * Validate settings structure and model IDs\n * Returns error message if invalid, null if valid\n */\nexport function validateSettings(settings: SettingsConfig): string | null {\n // Validate models structure\n if (!settings.models) {\n return 'Missing models configuration';\n }\n\n // Validate specialists\n if (!settings.models.specialists) {\n return 'Missing specialists configuration';\n }\n const specialists = settings.models.specialists;\n if (!specialists.review_agent || !specialists.test_agent || !specialists.merge_agent) {\n return 'Missing specialist agent model configuration';\n }\n\n // Validate planning agent\n if (!settings.models.planning_agent) {\n return 'Missing planning_agent configuration';\n }\n\n // Validate complexity levels\n if (!settings.models.complexity) {\n return 'Missing complexity configuration';\n }\n const complexity = settings.models.complexity;\n const requiredLevels: ComplexityLevel[] = ['trivial', 'simple', 'medium', 'complex', 'expert'];\n for (const level of requiredLevels) {\n if (!complexity[level]) {\n return `Missing complexity level: ${level}`;\n }\n }\n\n // Validate api_keys structure (optional keys)\n if (!settings.api_keys) {\n return 'Missing api_keys configuration';\n }\n\n return null;\n}\n\n/**\n * Get a deep copy of the default settings\n */\nexport function getDefaultSettings(): SettingsConfig {\n return JSON.parse(JSON.stringify(DEFAULT_SETTINGS));\n}\n\n/**\n * Get available models for a provider based on configured API keys\n * Returns empty array if provider API key is not configured\n */\nexport function getAvailableModels(settings: SettingsConfig): {\n anthropic: AnthropicModel[];\n openai: OpenAIModel[];\n google: GoogleModel[];\n zai: ZAIModel[];\n kimi: KimiModel[];\n} {\n const anthropicModels: AnthropicModel[] = [\n 'claude-opus-4-6',\n 'claude-sonnet-4-5',\n 'claude-haiku-4-5',\n ];\n\n const openaiModels: OpenAIModel[] = settings.api_keys.openai\n ? ['gpt-5.2-codex', 'o3-deep-research', 'gpt-4o', 'gpt-4o-mini']\n : [];\n\n const googleModels: GoogleModel[] = settings.api_keys.google\n ? ['gemini-3-pro-preview', 'gemini-3-flash-preview']\n : [];\n\n const zaiModels: ZAIModel[] = settings.api_keys.zai\n ? ['glm-4.7', 'glm-4.7-flash']\n : [];\n\n const kimiModels: KimiModel[] = settings.api_keys.kimi\n ? ['kimi-k2', 'kimi-k2.5']\n : [];\n\n return {\n anthropic: anthropicModels,\n openai: openaiModels,\n google: googleModels,\n zai: zaiModels,\n kimi: kimiModels,\n };\n}\n\n/**\n * Check if a model ID is an Anthropic model\n * Anthropic models can be run directly with `claude` CLI\n */\nexport function isAnthropicModel(modelId: ModelId | string): boolean {\n return modelId.startsWith('claude-');\n}\n\n/**\n * Get the Claude CLI model flag for an Anthropic model\n * Maps our model IDs to Claude's expected format\n */\nexport function getClaudeModelFlag(modelId: ModelId | string): string {\n const modelMap: Record<string, string> = {\n 'claude-opus-4-6': 'opus',\n 'claude-sonnet-4-5': 'sonnet',\n 'claude-haiku-4-5': 'haiku',\n };\n return modelMap[modelId] || 'sonnet';\n}\n\n/**\n * Get the command to run an agent with a specific model\n * Returns 'claude' for Anthropic models, 'claude-code-router' for others\n */\nexport function getAgentCommand(modelId: ModelId | string): { command: string; args: string[] } {\n if (isAnthropicModel(modelId)) {\n return {\n command: 'claude',\n args: ['--model', getClaudeModelFlag(modelId)],\n };\n }\n // Non-Anthropic models require the router\n return {\n command: 'claude-code-router',\n args: [],\n };\n}\n","/**\r\n * Provider Configuration and Compatibility\r\n *\r\n * Defines which LLM providers are compatible with Claude Code's API format.\r\n * - Direct providers: Implement Anthropic-compatible API (no router needed)\r\n * - Router providers: Require claude-code-router for API translation\r\n */\r\n\r\nimport type { ModelId, AnthropicModel, OpenAIModel, GoogleModel, ZAIModel } from './settings.js';\r\n\r\nexport type ProviderName = 'anthropic' | 'kimi' | 'openai' | 'google' | 'zai';\r\n\r\n/**\r\n * Provider compatibility types\r\n * - direct: Anthropic-compatible API, use ANTHROPIC_BASE_URL directly\r\n * - router: Incompatible API, requires claude-code-router for translation\r\n */\r\nexport type ProviderCompatibility = 'direct' | 'router';\r\n\r\n/**\r\n * Provider configuration\r\n */\r\nexport interface ProviderConfig {\r\n name: ProviderName;\r\n displayName: string;\r\n compatibility: ProviderCompatibility;\r\n baseUrl?: string; // For direct providers\r\n models: ModelId[];\r\n tested: boolean; // Whether compatibility has been verified\r\n description: string;\r\n}\r\n\r\n/**\r\n * All provider configurations\r\n */\r\nexport const PROVIDERS: Record<ProviderName, ProviderConfig> = {\r\n anthropic: {\r\n name: 'anthropic',\r\n displayName: 'Anthropic',\r\n compatibility: 'direct',\r\n models: ['claude-opus-4-6', 'claude-sonnet-4-5', 'claude-haiku-4-5'],\r\n tested: true,\r\n description: 'Native Claude API',\r\n },\r\n\r\n kimi: {\r\n name: 'kimi',\r\n displayName: 'Kimi (Moonshot AI)',\r\n compatibility: 'direct',\r\n baseUrl: 'https://api.kimi.com/coding/',\r\n models: [], // Kimi uses same model names as Anthropic\r\n tested: true,\r\n description: 'Anthropic-compatible API, tested 2026-01-28',\r\n },\r\n\r\n zai: {\r\n name: 'zai',\r\n displayName: 'Z.AI (GLM)',\r\n compatibility: 'direct',\r\n baseUrl: 'https://api.z.ai/api/anthropic',\r\n models: ['glm-4.7', 'glm-4.7-flash'],\r\n tested: true,\r\n description: 'Anthropic-compatible API, tested 2026-01-28',\r\n },\r\n\r\n openai: {\r\n name: 'openai',\r\n displayName: 'OpenAI',\r\n compatibility: 'router',\r\n models: ['gpt-5.2-codex', 'o3-deep-research', 'gpt-4o', 'gpt-4o-mini'],\r\n tested: false,\r\n description: 'Requires claude-code-router for API translation',\r\n },\r\n\r\n google: {\r\n name: 'google',\r\n displayName: 'Google (Gemini)',\r\n compatibility: 'router',\r\n models: ['gemini-3-pro-preview', 'gemini-3-flash-preview'],\r\n tested: false,\r\n description: 'Requires claude-code-router for API translation',\r\n },\r\n};\r\n\r\n/**\r\n * Get provider for a given model ID\r\n */\r\nexport function getProviderForModel(modelId: ModelId): ProviderConfig {\r\n // Check Anthropic models\r\n if (['claude-opus-4-6', 'claude-sonnet-4-5', 'claude-haiku-4-5'].includes(modelId)) {\r\n return PROVIDERS.anthropic;\r\n }\r\n\r\n // Check OpenAI models\r\n if (['gpt-5.2-codex', 'o3-deep-research', 'gpt-4o', 'gpt-4o-mini'].includes(modelId)) {\r\n return PROVIDERS.openai;\r\n }\r\n\r\n // Check Google models\r\n if (['gemini-3-pro-preview', 'gemini-3-flash-preview'].includes(modelId)) {\r\n return PROVIDERS.google;\r\n }\r\n\r\n // Check Z.AI models\r\n if (['glm-4.7', 'glm-4.7-flash'].includes(modelId)) {\r\n return PROVIDERS.zai;\r\n }\r\n\r\n // Check Kimi models\r\n if (['kimi-k2', 'kimi-k2.5'].includes(modelId)) {\r\n return PROVIDERS.kimi;\r\n }\r\n\r\n // Default to Anthropic if unknown\r\n return PROVIDERS.anthropic;\r\n}\r\n\r\n/**\r\n * Check if a provider requires claude-code-router\r\n */\r\nexport function requiresRouter(provider: ProviderName): boolean {\r\n return PROVIDERS[provider].compatibility === 'router';\r\n}\r\n\r\n/**\r\n * Get all providers that require router (have router compatibility)\r\n */\r\nexport function getRouterProviders(): ProviderConfig[] {\r\n return Object.values(PROVIDERS).filter(p => p.compatibility === 'router');\r\n}\r\n\r\n/**\r\n * Get all direct-compatible providers\r\n */\r\nexport function getDirectProviders(): ProviderConfig[] {\r\n return Object.values(PROVIDERS).filter(p => p.compatibility === 'direct');\r\n}\r\n\r\n/**\r\n * Check if any configured providers require router\r\n * Used to determine if router installation is needed\r\n */\r\nexport function needsRouter(apiKeys: { openai?: string; google?: string; zai?: string }): boolean {\r\n return !!(apiKeys.openai || apiKeys.google);\r\n}\r\n\r\n/**\r\n * Get environment variables for spawning agent with specific provider\r\n */\r\nexport function getProviderEnv(\r\n provider: ProviderConfig,\r\n apiKey: string\r\n): Record<string, string> {\r\n if (provider.compatibility === 'direct') {\r\n // Direct providers use ANTHROPIC_BASE_URL\r\n const env: Record<string, string> = {};\r\n\r\n if (provider.baseUrl) {\r\n env.ANTHROPIC_BASE_URL = provider.baseUrl;\r\n }\r\n\r\n if (provider.name !== 'anthropic') {\r\n // Non-Anthropic providers need auth token\r\n env.ANTHROPIC_AUTH_TOKEN = apiKey;\r\n }\r\n\r\n // Z.AI recommends longer timeout\r\n if (provider.name === 'zai') {\r\n env.API_TIMEOUT_MS = '300000';\r\n }\r\n\r\n return env;\r\n } else {\r\n // Router providers use local router proxy\r\n return {\r\n ANTHROPIC_BASE_URL: 'http://localhost:3456',\r\n ANTHROPIC_AUTH_TOKEN: 'router-managed',\r\n };\r\n }\r\n}\r\n"],"mappings":"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","names":[]}