@ekkos/cli 0.2.17 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/dist/capture/eviction-client.d.ts +139 -0
  3. package/dist/capture/eviction-client.js +454 -0
  4. package/dist/capture/index.d.ts +2 -0
  5. package/dist/capture/index.js +2 -0
  6. package/dist/capture/jsonl-rewriter.d.ts +96 -0
  7. package/dist/capture/jsonl-rewriter.js +1369 -0
  8. package/dist/capture/transcript-repair.d.ts +50 -0
  9. package/dist/capture/transcript-repair.js +308 -0
  10. package/dist/commands/doctor.js +23 -1
  11. package/dist/commands/run.d.ts +2 -0
  12. package/dist/commands/run.js +1229 -293
  13. package/dist/commands/setup.js +62 -16
  14. package/dist/commands/usage.d.ts +7 -0
  15. package/dist/commands/usage.js +214 -0
  16. package/dist/cron/index.d.ts +7 -0
  17. package/dist/cron/index.js +13 -0
  18. package/dist/cron/promoter.d.ts +70 -0
  19. package/dist/cron/promoter.js +403 -0
  20. package/dist/index.js +24 -3
  21. package/dist/lib/usage-monitor.d.ts +47 -0
  22. package/dist/lib/usage-monitor.js +124 -0
  23. package/dist/lib/usage-parser.d.ts +72 -0
  24. package/dist/lib/usage-parser.js +238 -0
  25. package/dist/restore/RestoreOrchestrator.d.ts +4 -0
  26. package/dist/restore/RestoreOrchestrator.js +118 -30
  27. package/package.json +12 -12
  28. package/templates/cursor-hooks/after-agent-response.sh +0 -0
  29. package/templates/cursor-hooks/before-submit-prompt.sh +0 -0
  30. package/templates/cursor-hooks/stop.sh +0 -0
  31. package/templates/ekkos-manifest.json +2 -2
  32. package/templates/hooks/assistant-response.sh +0 -0
  33. package/templates/hooks/session-start.sh +0 -0
  34. package/templates/hooks/user-prompt-submit.sh +6 -0
  35. package/templates/plan-template.md +0 -0
  36. package/templates/spec-template.md +0 -0
  37. package/templates/agents/README.md +0 -182
  38. package/templates/agents/code-reviewer.md +0 -166
  39. package/templates/agents/debug-detective.md +0 -169
  40. package/templates/agents/ekkOS_Vercel.md +0 -99
  41. package/templates/agents/extension-manager.md +0 -229
  42. package/templates/agents/git-companion.md +0 -185
  43. package/templates/agents/github-test-agent.md +0 -321
  44. package/templates/agents/railway-manager.md +0 -215
@@ -0,0 +1,72 @@
1
+ export interface TurnMetrics {
2
+ turn_number: number;
3
+ timestamp: string;
4
+ input_tokens: number;
5
+ output_tokens: number;
6
+ cache_read_tokens: number;
7
+ cache_creation_tokens: number;
8
+ total_tokens: number;
9
+ context_percentage: number;
10
+ model: string;
11
+ }
12
+ export interface SessionUsage {
13
+ session_id: string;
14
+ instance_id: string;
15
+ session_name: string;
16
+ turn_count: number;
17
+ total_input_tokens: number;
18
+ total_output_tokens: number;
19
+ total_cache_read_tokens: number;
20
+ total_cache_creation_tokens: number;
21
+ total_tokens: number;
22
+ total_cost: number;
23
+ avg_context_percentage: number;
24
+ max_context_percentage: number;
25
+ started_at: string;
26
+ last_activity: string;
27
+ models_used: string[];
28
+ turns: TurnMetrics[];
29
+ patterns_retrieved?: number;
30
+ patterns_applied?: number;
31
+ patterns_learned?: number;
32
+ confidence_gain?: number;
33
+ }
34
+ export interface CcusageSession {
35
+ sessionId: string;
36
+ inputTokens: number;
37
+ outputTokens: number;
38
+ cacheCreationTokens: number;
39
+ cacheReadTokens: number;
40
+ totalTokens: number;
41
+ totalCost: number;
42
+ lastActivity: string;
43
+ modelsUsed: string[];
44
+ modelBreakdowns: {
45
+ modelName: string;
46
+ inputTokens: number;
47
+ outputTokens: number;
48
+ cacheCreationTokens: number;
49
+ cacheReadTokens: number;
50
+ cost: number;
51
+ }[];
52
+ projectPath: string;
53
+ }
54
+ export interface CcusageOutput {
55
+ sessions: CcusageSession[];
56
+ }
57
+ /**
58
+ * Get all sessions using ccusage
59
+ */
60
+ export declare function getAllSessions(instanceId?: string): Promise<SessionUsage[]>;
61
+ /**
62
+ * Get specific session usage using ccusage
63
+ */
64
+ export declare function getSessionUsage(sessionId: string, instanceId?: string): Promise<SessionUsage | null>;
65
+ /**
66
+ * Get human-readable session name
67
+ */
68
+ export declare function getSessionName(sessionId: string): string;
69
+ /**
70
+ * Get current session ID from active Claude Code process
71
+ */
72
+ export declare function getCurrentSessionId(): string | null;
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getAllSessions = getAllSessions;
37
+ exports.getSessionUsage = getSessionUsage;
38
+ exports.getSessionName = getSessionName;
39
+ exports.getCurrentSessionId = getCurrentSessionId;
40
+ const child_process_1 = require("child_process");
41
+ const fs = __importStar(require("fs"));
42
+ const os = __importStar(require("os"));
43
+ const path = __importStar(require("path"));
44
+ /**
45
+ * Get all sessions using ccusage
46
+ */
47
+ async function getAllSessions(instanceId) {
48
+ try {
49
+ // Run ccusage to get all sessions
50
+ const result = (0, child_process_1.execSync)('npx -y ccusage@latest session --json --breakdown', {
51
+ encoding: 'utf-8',
52
+ stdio: ['pipe', 'pipe', 'ignore'], // Suppress npm warnings
53
+ });
54
+ const data = JSON.parse(result);
55
+ // Convert ccusage format to our format
56
+ const sessions = await Promise.all(data.sessions.map(async (session) => convertCcusageSession(session, instanceId)));
57
+ return sessions.filter((s) => s !== null);
58
+ }
59
+ catch (error) {
60
+ console.error('Failed to fetch sessions from ccusage:', error);
61
+ return [];
62
+ }
63
+ }
64
+ /**
65
+ * Get specific session usage using ccusage
66
+ */
67
+ async function getSessionUsage(sessionId, instanceId) {
68
+ try {
69
+ // Run ccusage to get all sessions (--id flag doesn't work for IDs with dashes)
70
+ const result = (0, child_process_1.execSync)('npx -y ccusage@latest session --json --breakdown', {
71
+ encoding: 'utf-8',
72
+ stdio: ['pipe', 'pipe', 'ignore'], // Suppress npm warnings
73
+ });
74
+ const data = JSON.parse(result);
75
+ // Find the specific session
76
+ const session = data.sessions.find(s => s.sessionId === sessionId);
77
+ if (!session) {
78
+ return null;
79
+ }
80
+ return convertCcusageSession(session, instanceId);
81
+ }
82
+ catch (error) {
83
+ console.error('Failed to fetch session from ccusage:', error);
84
+ return null;
85
+ }
86
+ }
87
+ /**
88
+ * Convert ccusage session to our SessionUsage format
89
+ */
90
+ async function convertCcusageSession(session, instanceId) {
91
+ // Try to get turn-by-turn data from Claude Code's JSONL files
92
+ const turns = await getTurnMetrics(session.sessionId, instanceId);
93
+ // Calculate context percentages if we have turn data
94
+ let avgContextPercentage = 0;
95
+ let maxContextPercentage = 0;
96
+ if (turns.length > 0) {
97
+ avgContextPercentage =
98
+ turns.reduce((sum, t) => sum + t.context_percentage, 0) / turns.length;
99
+ maxContextPercentage = Math.max(...turns.map((t) => t.context_percentage));
100
+ }
101
+ // Try to fetch ekkOS pattern metrics from memory API
102
+ let patternMetrics;
103
+ try {
104
+ patternMetrics = await fetchPatternMetrics(session.sessionId);
105
+ }
106
+ catch {
107
+ // Fail silently if memory API not available
108
+ }
109
+ return {
110
+ session_id: session.sessionId,
111
+ instance_id: instanceId || 'unknown',
112
+ session_name: getSessionName(session.sessionId),
113
+ turn_count: turns.length || 0,
114
+ total_input_tokens: session.inputTokens,
115
+ total_output_tokens: session.outputTokens,
116
+ total_cache_read_tokens: session.cacheReadTokens,
117
+ total_cache_creation_tokens: session.cacheCreationTokens,
118
+ total_tokens: session.totalTokens,
119
+ total_cost: session.totalCost,
120
+ avg_context_percentage: avgContextPercentage,
121
+ max_context_percentage: maxContextPercentage,
122
+ started_at: turns[0]?.timestamp || 'unknown',
123
+ last_activity: session.lastActivity,
124
+ models_used: session.modelsUsed,
125
+ turns,
126
+ ...patternMetrics,
127
+ };
128
+ }
129
+ /**
130
+ * Get turn-by-turn metrics from Claude Code JSONL files
131
+ */
132
+ async function getTurnMetrics(sessionId, instanceId) {
133
+ // Try multiple possible locations for Claude Code JSONL files
134
+ const possiblePaths = [
135
+ path.join(os.homedir(), '.claude', 'projects', instanceId || '', `${sessionId}.jsonl`),
136
+ path.join(os.homedir(), '.config', 'claude', 'projects', instanceId || '', `${sessionId}.jsonl`),
137
+ path.join(os.homedir(), '.codex', instanceId || '', `${sessionId}.jsonl`),
138
+ ];
139
+ for (const filePath of possiblePaths) {
140
+ if (fs.existsSync(filePath)) {
141
+ try {
142
+ const content = fs.readFileSync(filePath, 'utf-8');
143
+ const lines = content.trim().split('\n');
144
+ const turns = [];
145
+ for (const line of lines) {
146
+ try {
147
+ const entry = JSON.parse(line);
148
+ // Look for usage events
149
+ if (entry.type === 'usage' || entry.usage) {
150
+ const usage = entry.usage || entry;
151
+ turns.push({
152
+ turn_number: turns.length,
153
+ timestamp: entry.timestamp || new Date().toISOString(),
154
+ input_tokens: usage.input_tokens || 0,
155
+ output_tokens: usage.output_tokens || 0,
156
+ cache_read_tokens: usage.cache_read_input_tokens || 0,
157
+ cache_creation_tokens: usage.cache_creation_input_tokens || 0,
158
+ total_tokens: (usage.input_tokens || 0) +
159
+ (usage.output_tokens || 0) +
160
+ (usage.cache_read_input_tokens || 0) +
161
+ (usage.cache_creation_input_tokens || 0),
162
+ context_percentage: calculateContextPercentage(usage),
163
+ model: entry.model || 'unknown',
164
+ });
165
+ }
166
+ }
167
+ catch {
168
+ // Skip invalid JSON lines
169
+ }
170
+ }
171
+ return turns;
172
+ }
173
+ catch {
174
+ // Continue to next path
175
+ }
176
+ }
177
+ }
178
+ return [];
179
+ }
180
+ /**
181
+ * Calculate context percentage from usage data
182
+ */
183
+ function calculateContextPercentage(usage) {
184
+ const totalTokens = (usage.input_tokens || 0) +
185
+ (usage.cache_read_input_tokens || 0) +
186
+ (usage.cache_creation_input_tokens || 0);
187
+ // Claude Code context window is typically 200k tokens
188
+ const contextWindow = 200000;
189
+ return (totalTokens / contextWindow) * 100;
190
+ }
191
+ /**
192
+ * Fetch pattern metrics from ekkOS memory API
193
+ */
194
+ async function fetchPatternMetrics(sessionId) {
195
+ try {
196
+ const response = await fetch(`http://localhost:3001/api/v1/session/stats?session_id=${sessionId}`, {
197
+ method: 'GET',
198
+ headers: {
199
+ 'Content-Type': 'application/json',
200
+ },
201
+ });
202
+ if (!response.ok) {
203
+ return null;
204
+ }
205
+ const data = await response.json();
206
+ return {
207
+ patterns_retrieved: data.patterns_retrieved || 0,
208
+ patterns_applied: data.patterns_applied || 0,
209
+ patterns_learned: data.patterns_learned || 0,
210
+ confidence_gain: data.confidence_gain || 0,
211
+ };
212
+ }
213
+ catch {
214
+ return null;
215
+ }
216
+ }
217
+ /**
218
+ * Get human-readable session name
219
+ */
220
+ function getSessionName(sessionId) {
221
+ // Extract last part of path-based session IDs
222
+ const parts = sessionId.split('/');
223
+ const lastPart = parts[parts.length - 1];
224
+ // If it's a UUID-like string, try to map to session name
225
+ // For now, just return a shortened version
226
+ if (lastPart.length > 20) {
227
+ return lastPart.substring(0, 8) + '...' + lastPart.substring(lastPart.length - 8);
228
+ }
229
+ return lastPart;
230
+ }
231
+ /**
232
+ * Get current session ID from active Claude Code process
233
+ */
234
+ function getCurrentSessionId() {
235
+ // TODO: Extract from process environment or Claude Code state
236
+ // For now, require user to pass session ID
237
+ return null;
238
+ }
@@ -30,6 +30,10 @@ export declare class RestoreOrchestrator {
30
30
  * Main restore function - attempts tiers in order
31
31
  */
32
32
  restore(options?: RestoreOptions): Promise<CacheResult<RestorePayload>>;
33
+ /**
34
+ * Scan payload for [ekkOS:page-out:...] stubs and rehydrate from Proxy
35
+ */
36
+ private rehydratePayload;
33
37
  /**
34
38
  * Tier -1: Restore from stream log (has mid-turn content)
35
39
  * This is checked FIRST because stream logs have the most recent data,
@@ -58,8 +58,10 @@ const types_js_1 = require("../cache/types.js");
58
58
  const stream_tailer_js_1 = require("../capture/stream-tailer.js");
59
59
  const paths_js_1 = require("../utils/paths.js");
60
60
  // API configuration
61
- const MEMORY_API_URL = process.env.EKKOS_API_URL || 'https://api.ekkos.dev';
61
+ const MEMORY_API_URL = process.env.EKKOS_API_URL || 'https://mcp.ekkos.dev';
62
62
  const CONFIG_PATH = path.join(os.homedir(), '.ekkos', 'config.json');
63
+ // Rehydration patterns
64
+ const STUB_REGEX = /\[ekkOS:page-out:r2:\/\/([^/]+)\/([^/]+)\/([^/]+)\/([^\]?]+)(?:\?range=(\d+)-(\d+))?\]/g;
63
65
  /**
64
66
  * Load auth token from config
65
67
  */
@@ -122,45 +124,131 @@ class RestoreOrchestrator {
122
124
  latency_ms: Date.now() - startTime,
123
125
  };
124
126
  }
127
+ let result;
125
128
  // Try Tier -1: Stream log (has mid-turn content, most recent data)
126
129
  const streamResult = await this.restoreFromStreamLog(instanceId, sessionId, sessionName || '', lastN);
127
130
  if (streamResult.success && streamResult.data) {
128
- return {
129
- ...streamResult,
130
- latency_ms: Date.now() - startTime,
131
- };
131
+ result = streamResult;
132
132
  }
133
- // Try Tier 0: Local cache
134
- const localResult = await this.restoreFromLocal(instanceId, sessionId, sessionName || '', lastN);
135
- if (localResult.success && localResult.data) {
136
- return {
137
- ...localResult,
138
- latency_ms: Date.now() - startTime,
139
- };
140
- }
141
- // Try Tier 1: Redis
142
- const redisResult = await this.restoreFromRedis(sessionName || sessionId, lastN, instanceId);
143
- if (redisResult.success && redisResult.data) {
144
- return {
145
- ...redisResult,
146
- latency_ms: Date.now() - startTime,
147
- };
133
+ else {
134
+ // Try Tier 0: Local cache
135
+ const localResult = await this.restoreFromLocal(instanceId, sessionId, sessionName || '', lastN);
136
+ if (localResult.success && localResult.data) {
137
+ result = localResult;
138
+ }
139
+ else {
140
+ // Try Tier 1: Redis
141
+ const redisResult = await this.restoreFromRedis(sessionName || sessionId, lastN, instanceId);
142
+ if (redisResult.success && redisResult.data) {
143
+ result = redisResult;
144
+ }
145
+ else {
146
+ // Try Tier 2: Supabase
147
+ const supabaseResult = await this.restoreFromSupabase(sessionId, lastN, instanceId);
148
+ if (supabaseResult.success && supabaseResult.data) {
149
+ result = supabaseResult;
150
+ }
151
+ else {
152
+ // All tiers failed
153
+ return {
154
+ success: false,
155
+ error: `All restore tiers failed. Local: ${localResult.error}, Redis: ${redisResult.error}, Supabase: ${supabaseResult.error}`,
156
+ latency_ms: Date.now() - startTime,
157
+ };
158
+ }
159
+ }
160
+ }
148
161
  }
149
- // Try Tier 2: Supabase
150
- const supabaseResult = await this.restoreFromSupabase(sessionId, lastN, instanceId);
151
- if (supabaseResult.success && supabaseResult.data) {
152
- return {
153
- ...supabaseResult,
154
- latency_ms: Date.now() - startTime,
155
- };
162
+ // Rehydrate stubs if any found in restored payload
163
+ if (result.success && result.data) {
164
+ await this.rehydratePayload(result.data);
156
165
  }
157
- // All tiers failed
158
166
  return {
159
- success: false,
160
- error: `All restore tiers failed. Local: ${localResult.error}, Redis: ${redisResult.error}, Supabase: ${supabaseResult.error}`,
167
+ ...result,
161
168
  latency_ms: Date.now() - startTime,
162
169
  };
163
170
  }
171
+ /**
172
+ * Scan payload for [ekkOS:page-out:...] stubs and rehydrate from Proxy
173
+ */
174
+ async rehydratePayload(payload) {
175
+ const stubs = new Set();
176
+ // Scan all restored turns (both user queries and assistant responses)
177
+ // Stubs are injected as user messages, so check user_query too
178
+ for (const turn of payload.restored_turns) {
179
+ for (const text of [turn.user_query, turn.assistant_response]) {
180
+ if (!text)
181
+ continue;
182
+ STUB_REGEX.lastIndex = 0;
183
+ const matches = text.match(STUB_REGEX);
184
+ if (matches) {
185
+ for (const match of matches)
186
+ stubs.add(match);
187
+ }
188
+ }
189
+ }
190
+ // Also scan latest (both user and assistant)
191
+ for (const text of [payload.latest.user_query, payload.latest.assistant_response]) {
192
+ if (!text)
193
+ continue;
194
+ STUB_REGEX.lastIndex = 0;
195
+ const matches = text.match(STUB_REGEX);
196
+ if (matches) {
197
+ for (const match of matches)
198
+ stubs.add(match);
199
+ }
200
+ }
201
+ if (stubs.size === 0)
202
+ return;
203
+ console.log(`[Restore] Detected ${stubs.size} stubs, rehydrating...`);
204
+ try {
205
+ const url = `${MEMORY_API_URL}/api/v1/context/rehydrate`;
206
+ const response = await fetch(url, {
207
+ method: 'POST',
208
+ headers: {
209
+ 'Authorization': `Bearer ${this.authToken}`,
210
+ 'Content-Type': 'application/json',
211
+ },
212
+ body: JSON.stringify({ stubs: Array.from(stubs) }),
213
+ signal: AbortSignal.timeout(10000),
214
+ });
215
+ if (!response.ok) {
216
+ console.warn(`[Restore] Rehydration API failed: ${response.status}`);
217
+ return;
218
+ }
219
+ const { rehydrated } = await response.json();
220
+ if (!rehydrated || !Array.isArray(rehydrated))
221
+ return;
222
+ // Create mapping of stub -> content
223
+ const stubMap = new Map();
224
+ for (const entry of rehydrated) {
225
+ const content = entry.messages.map((m) => {
226
+ const role = m.role === 'user' ? 'U' : 'A';
227
+ const text = typeof m.content === 'string'
228
+ ? m.content
229
+ : Array.isArray(m.content)
230
+ ? m.content.map((b) => b.text || '').join('\n')
231
+ : '';
232
+ return `${role}: ${text}`;
233
+ }).join('\n\n');
234
+ stubMap.set(entry.stub, content);
235
+ }
236
+ // Replace stubs in turns
237
+ for (const turn of payload.restored_turns) {
238
+ for (const [stub, content] of stubMap.entries()) {
239
+ turn.assistant_response = turn.assistant_response.replace(stub, `[REHYDRATED CONTENT]\n${content}`);
240
+ }
241
+ }
242
+ // Replace stubs in latest
243
+ for (const [stub, content] of stubMap.entries()) {
244
+ payload.latest.assistant_response = payload.latest.assistant_response.replace(stub, `[REHYDRATED CONTENT]\n${content}`);
245
+ }
246
+ console.log(`[Restore] Successfully rehydrated ${stubMap.size} stubs`);
247
+ }
248
+ catch (err) {
249
+ console.warn(`[Restore] Rehydration error: ${err instanceof Error ? err.message : String(err)}`);
250
+ }
251
+ }
164
252
  /**
165
253
  * Tier -1: Restore from stream log (has mid-turn content)
166
254
  * This is checked FIRST because stream logs have the most recent data,
package/package.json CHANGED
@@ -1,19 +1,13 @@
1
1
  {
2
2
  "name": "@ekkos/cli",
3
- "version": "0.2.17",
3
+ "version": "0.3.3",
4
4
  "description": "Setup ekkOS memory for AI coding assistants (Claude Code, Cursor, Windsurf)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "ekkos": "dist/index.js",
8
8
  "cli": "dist/index.js",
9
- "ekkos-capture": "dist/cache/capture.js"
10
- },
11
- "scripts": {
12
- "build": "tsc",
13
- "dev": "ts-node src/index.ts",
14
- "prepack": "node scripts/build-templates.js prepack",
15
- "postpack": "node scripts/build-templates.js postpack",
16
- "prepublishOnly": "npm run build"
9
+ "ekkos-capture": "dist/cache/capture.js",
10
+ "ekkos-promote": "dist/cron/promoter.js"
17
11
  },
18
12
  "keywords": [
19
13
  "ekkos",
@@ -28,13 +22,15 @@
28
22
  "author": "ekkOS",
29
23
  "license": "MIT",
30
24
  "dependencies": {
25
+ "@supabase/supabase-js": "^2.39.8",
31
26
  "chalk": "^5.3.0",
32
27
  "commander": "^12.1.0",
33
28
  "inquirer": "^9.2.23",
34
29
  "node-pty": "1.2.0-beta.7",
35
30
  "open": "^10.0.0",
36
31
  "ora": "^8.0.1",
37
- "ws": "^8.19.0"
32
+ "ws": "^8.19.0",
33
+ "@ekkos/prometheus": "0.1.0"
38
34
  },
39
35
  "devDependencies": {
40
36
  "@types/node": "^20.11.0",
@@ -46,5 +42,9 @@
46
42
  "files": [
47
43
  "dist",
48
44
  "templates"
49
- ]
50
- }
45
+ ],
46
+ "scripts": {
47
+ "build": "tsc",
48
+ "dev": "ts-node src/index.ts"
49
+ }
50
+ }
File without changes
File without changes
File without changes
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://ekkos.dev/schemas/manifest-v1.json",
3
3
  "manifestVersion": "1.0.0",
4
- "generatedAt": "2026-01-22T06:19:22.744Z",
4
+ "generatedAt": "2026-02-04T04:30:49.260Z",
5
5
  "platforms": {
6
6
  "darwin": {
7
7
  "configDir": "~/.ekkos",
@@ -68,7 +68,7 @@
68
68
  "source": "hooks/user-prompt-submit.sh",
69
69
  "destination": "user-prompt-submit.sh",
70
70
  "description": "User prompt submit hook (Unix)",
71
- "checksum": "0e006eb7becba874f34fc622c9ee83b3c96a6e00daf95db840369894af94abf3",
71
+ "checksum": "38aa190d08e2c24129e7f653732a860693fbd2a7015e59226aec57e8b3659114",
72
72
  "executable": true
73
73
  },
74
74
  {
File without changes
File without changes
@@ -159,6 +159,12 @@ if [ -z "$RAW_SESSION_ID" ] || [ "$RAW_SESSION_ID" = "unknown" ] || [ "$RAW_SESS
159
159
  if [ -f "$STATE_FILE" ] && [ -f "$JSON_PARSE_HELPER" ]; then
160
160
  RAW_SESSION_ID=$(node "$JSON_PARSE_HELPER" "$STATE_FILE" '.session_id' 2>/dev/null || echo "unknown")
161
161
  fi
162
+
163
+ # VSCode extension fallback: Extract session ID from transcript path
164
+ # Path format: ~/.claude/projects/<project>/<session-uuid>.jsonl
165
+ if [ "$RAW_SESSION_ID" = "unknown" ] && [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
166
+ RAW_SESSION_ID=$(basename "$TRANSCRIPT_PATH" .jsonl)
167
+ fi
162
168
  fi
163
169
 
164
170
  # ═══════════════════════════════════════════════════════════════════════════
File without changes
File without changes