@ekkos/cli 0.3.3 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/agent/daemon.d.ts +27 -0
- package/dist/agent/daemon.js +254 -29
- package/dist/agent/health-check.d.ts +35 -0
- package/dist/agent/health-check.js +243 -0
- package/dist/agent/pty-runner.d.ts +1 -0
- package/dist/agent/pty-runner.js +6 -1
- package/dist/capture/transcript-repair.d.ts +1 -0
- package/dist/capture/transcript-repair.js +12 -1
- package/dist/commands/agent.d.ts +6 -0
- package/dist/commands/agent.js +244 -0
- package/dist/commands/dashboard.d.ts +25 -0
- package/dist/commands/dashboard.js +1175 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +503 -350
- package/dist/commands/setup-remote.js +146 -37
- package/dist/commands/swarm-dashboard.d.ts +20 -0
- package/dist/commands/swarm-dashboard.js +735 -0
- package/dist/commands/swarm-setup.d.ts +10 -0
- package/dist/commands/swarm-setup.js +956 -0
- package/dist/commands/swarm.d.ts +46 -0
- package/dist/commands/swarm.js +441 -0
- package/dist/commands/test-claude.d.ts +16 -0
- package/dist/commands/test-claude.js +156 -0
- package/dist/commands/usage/blocks.d.ts +8 -0
- package/dist/commands/usage/blocks.js +60 -0
- package/dist/commands/usage/daily.d.ts +9 -0
- package/dist/commands/usage/daily.js +96 -0
- package/dist/commands/usage/dashboard.d.ts +8 -0
- package/dist/commands/usage/dashboard.js +104 -0
- package/dist/commands/usage/formatters.d.ts +41 -0
- package/dist/commands/usage/formatters.js +147 -0
- package/dist/commands/usage/index.d.ts +13 -0
- package/dist/commands/usage/index.js +87 -0
- package/dist/commands/usage/monthly.d.ts +8 -0
- package/dist/commands/usage/monthly.js +66 -0
- package/dist/commands/usage/session.d.ts +11 -0
- package/dist/commands/usage/session.js +193 -0
- package/dist/commands/usage/weekly.d.ts +9 -0
- package/dist/commands/usage/weekly.js +61 -0
- package/dist/deploy/instructions.d.ts +5 -2
- package/dist/deploy/instructions.js +11 -8
- package/dist/index.js +256 -20
- package/dist/lib/tmux-scrollbar.d.ts +14 -0
- package/dist/lib/tmux-scrollbar.js +296 -0
- package/dist/lib/usage-parser.d.ts +95 -5
- package/dist/lib/usage-parser.js +416 -71
- package/dist/utils/log-rotate.d.ts +18 -0
- package/dist/utils/log-rotate.js +74 -0
- package/dist/utils/platform.d.ts +2 -0
- package/dist/utils/platform.js +3 -1
- package/dist/utils/session-binding.d.ts +5 -0
- package/dist/utils/session-binding.js +46 -0
- package/dist/utils/state.js +4 -0
- package/dist/utils/verify-remote-terminal.d.ts +10 -0
- package/dist/utils/verify-remote-terminal.js +415 -0
- package/package.json +16 -11
- package/templates/CLAUDE.md +135 -23
- package/templates/cursor-hooks/after-agent-response.sh +0 -0
- package/templates/cursor-hooks/before-submit-prompt.sh +0 -0
- package/templates/cursor-hooks/stop.sh +0 -0
- package/templates/ekkos-manifest.json +5 -5
- package/templates/hooks/assistant-response.sh +0 -0
- package/templates/hooks/lib/contract.sh +43 -31
- package/templates/hooks/lib/count-tokens.cjs +86 -0
- package/templates/hooks/lib/ekkos-reminders.sh +98 -0
- package/templates/hooks/lib/state.sh +53 -1
- package/templates/hooks/session-start.sh +0 -0
- package/templates/hooks/stop.sh +150 -388
- package/templates/hooks/user-prompt-submit.sh +353 -443
- package/templates/plan-template.md +0 -0
- package/templates/spec-template.md +0 -0
- package/templates/windsurf-hooks/README.md +212 -0
- package/templates/windsurf-hooks/hooks.json +9 -2
- package/templates/windsurf-hooks/install.sh +148 -0
- package/templates/windsurf-hooks/lib/contract.sh +2 -0
- package/templates/windsurf-hooks/post-cascade-response.sh +251 -0
- package/templates/windsurf-hooks/pre-user-prompt.sh +435 -0
- package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
- package/LICENSE +0 -21
- package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.sessionCommand = sessionCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const usage_parser_js_1 = require("../../lib/usage-parser.js");
|
|
9
|
+
const formatters_js_1 = require("./formatters.js");
|
|
10
|
+
/**
|
|
11
|
+
* ekkos usage session [id] [--list] [--instance id] [--json]
|
|
12
|
+
*
|
|
13
|
+
* Accepts ekkOS 3-word names (lit-lex-zip) or ccusage project paths.
|
|
14
|
+
*/
|
|
15
|
+
async function sessionCommand(options) {
|
|
16
|
+
const instanceId = options.instance;
|
|
17
|
+
if (options.list) {
|
|
18
|
+
await listSessions(instanceId, options.json);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (!options.sessionId) {
|
|
22
|
+
await listSessions(instanceId, options.json);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
let usage = null;
|
|
27
|
+
// Detect ekkOS 3-word session names and resolve via active-sessions.json
|
|
28
|
+
if ((0, usage_parser_js_1.isEkkosSessionName)(options.sessionId)) {
|
|
29
|
+
usage = await (0, usage_parser_js_1.getSessionUsageByName)(options.sessionId);
|
|
30
|
+
if (!usage) {
|
|
31
|
+
if (options.json) {
|
|
32
|
+
console.log(JSON.stringify({ error: `ekkOS session "${options.sessionId}" not found` }));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.log(chalk_1.default.yellow(` ekkOS session "${options.sessionId}" not found.`));
|
|
36
|
+
console.log(chalk_1.default.gray(' Check available sessions with: ekkos usage session -l'));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Fallback to ccusage project-path lookup
|
|
42
|
+
usage = await (0, usage_parser_js_1.getSessionUsage)(options.sessionId, instanceId);
|
|
43
|
+
}
|
|
44
|
+
if (!usage) {
|
|
45
|
+
if (options.json) {
|
|
46
|
+
console.log(JSON.stringify({ error: 'Session not found' }));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
console.log(chalk_1.default.yellow(` No usage data found for session: ${options.sessionId}`));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (options.json) {
|
|
53
|
+
console.log(JSON.stringify(usage, null, 2));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
displaySessionUsage(usage);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.log(chalk_1.default.red(` Error: ${err.message}`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Display session usage in formatted table */
|
|
63
|
+
function displaySessionUsage(usage) {
|
|
64
|
+
(0, formatters_js_1.titleBar)(`Session: ${usage.session_name}`, usage.session_id);
|
|
65
|
+
(0, formatters_js_1.sectionHeader)('Summary');
|
|
66
|
+
console.log(` ${chalk_1.default.bold('Session ID:')} ${usage.session_id}`);
|
|
67
|
+
console.log(` ${chalk_1.default.bold('Turn Count:')} ${chalk_1.default.green(usage.turn_count.toString())}`);
|
|
68
|
+
console.log(` ${chalk_1.default.bold('Total Tokens:')} ${(0, formatters_js_1.formatNumber)(usage.total_tokens)}`);
|
|
69
|
+
console.log(` ${chalk_1.default.bold('Avg Context:')} ${(0, formatters_js_1.formatPercentage)(usage.avg_context_percentage)}`);
|
|
70
|
+
console.log(` ${chalk_1.default.bold('Max Context:')} ${(0, formatters_js_1.formatPercentage)(usage.max_context_percentage)}`);
|
|
71
|
+
console.log(` ${chalk_1.default.bold('Started:')} ${(0, formatters_js_1.formatTimestamp)(usage.started_at)}`);
|
|
72
|
+
console.log(` ${chalk_1.default.bold('Last Activity:')} ${(0, formatters_js_1.formatTimestamp)(usage.last_activity)}`);
|
|
73
|
+
console.log();
|
|
74
|
+
(0, formatters_js_1.sectionHeader)('Token Breakdown');
|
|
75
|
+
console.log(` ${chalk_1.default.bold('Input Tokens:')} ${(0, formatters_js_1.formatNumber)(usage.total_input_tokens)}`);
|
|
76
|
+
console.log(` ${chalk_1.default.bold('Output Tokens:')} ${(0, formatters_js_1.formatNumber)(usage.total_output_tokens)}`);
|
|
77
|
+
console.log(` ${chalk_1.default.bold('Cache Read:')} ${(0, formatters_js_1.formatNumber)(usage.total_cache_read_tokens)} ${chalk_1.default.gray('(90% discount)')}`);
|
|
78
|
+
console.log(` ${chalk_1.default.bold('Cache Creation:')} ${(0, formatters_js_1.formatNumber)(usage.total_cache_creation_tokens)} ${chalk_1.default.gray('(25% premium)')}`);
|
|
79
|
+
console.log();
|
|
80
|
+
(0, formatters_js_1.sectionHeader)('Cost');
|
|
81
|
+
console.log(` ${chalk_1.default.bold('Total Cost:')} ${chalk_1.default.green((0, formatters_js_1.formatCost)(usage.total_cost))}`);
|
|
82
|
+
console.log(` ${chalk_1.default.bold('Models Used:')} ${chalk_1.default.cyan(usage.models_used.join(', '))}`);
|
|
83
|
+
console.log();
|
|
84
|
+
// ekkOS pattern metrics
|
|
85
|
+
if (usage.patterns_retrieved !== undefined) {
|
|
86
|
+
(0, formatters_js_1.sectionHeader)('ekkOS Pattern Metrics');
|
|
87
|
+
console.log(` ${chalk_1.default.bold('Patterns Retrieved:')} ${chalk_1.default.cyan(usage.patterns_retrieved.toString())}`);
|
|
88
|
+
console.log(` ${chalk_1.default.bold('Patterns Applied:')} ${chalk_1.default.green(usage.patterns_applied?.toString() || '0')}`);
|
|
89
|
+
console.log(` ${chalk_1.default.bold('Patterns Learned:')} ${chalk_1.default.yellow(usage.patterns_learned?.toString() || '0')}`);
|
|
90
|
+
console.log(` ${chalk_1.default.bold('Confidence Gain:')} ${chalk_1.default.magenta(`+${((usage.confidence_gain || 0) * 100).toFixed(1)}%`)}`);
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
93
|
+
// Turn-by-turn breakdown
|
|
94
|
+
if (usage.turns.length > 0) {
|
|
95
|
+
(0, formatters_js_1.sectionHeader)('Turn-by-Turn Breakdown');
|
|
96
|
+
console.log(chalk_1.default.gray(' Turn │ Context % │ Input │ Output │ Cache Read │ Total'));
|
|
97
|
+
console.log(chalk_1.default.gray('─'.repeat(80)));
|
|
98
|
+
for (const turn of usage.turns) {
|
|
99
|
+
const turnStr = turn.turn_number.toString().padStart(4);
|
|
100
|
+
const contextStr = (0, formatters_js_1.formatPercentage)(turn.context_percentage).padStart(10);
|
|
101
|
+
const inputStr = (0, formatters_js_1.formatNumber)(turn.input_tokens).padStart(7);
|
|
102
|
+
const outputStr = (0, formatters_js_1.formatNumber)(turn.output_tokens).padStart(7);
|
|
103
|
+
const cacheStr = (0, formatters_js_1.formatNumber)(turn.cache_read_tokens).padStart(11);
|
|
104
|
+
const totalStr = (0, formatters_js_1.formatNumber)(turn.total_tokens).padStart(8);
|
|
105
|
+
const contextColor = (0, formatters_js_1.getContextColor)(turn.context_percentage);
|
|
106
|
+
console.log(` ${turnStr} │ ${contextColor(contextStr)} │ ${inputStr} │ ${outputStr} │ ${cacheStr} │ ${totalStr}`);
|
|
107
|
+
}
|
|
108
|
+
console.log();
|
|
109
|
+
}
|
|
110
|
+
// VM validation
|
|
111
|
+
if (usage.turn_count >= 5) {
|
|
112
|
+
const contextValues = usage.turns.map(t => t.context_percentage);
|
|
113
|
+
const range = Math.max(...contextValues) - Math.min(...contextValues);
|
|
114
|
+
if (range < 15) {
|
|
115
|
+
console.log(chalk_1.default.green.bold(' VM Working: Context staying in constant band'));
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
console.log(chalk_1.default.yellow.bold(' VM Warning: Context growing linearly (check proxy eviction)'));
|
|
119
|
+
}
|
|
120
|
+
console.log();
|
|
121
|
+
}
|
|
122
|
+
console.log(chalk_1.default.bold.cyan('═'.repeat(80)));
|
|
123
|
+
console.log();
|
|
124
|
+
}
|
|
125
|
+
/** List available sessions - shows ekkOS sessions by default, falls back to ccusage */
|
|
126
|
+
async function listSessions(instanceId, json) {
|
|
127
|
+
// Try ekkOS sessions first (individual sessions with 3-word names)
|
|
128
|
+
const ekkosSessions = await (0, usage_parser_js_1.listEkkosSessions)(30);
|
|
129
|
+
if (ekkosSessions.length > 0) {
|
|
130
|
+
if (json) {
|
|
131
|
+
console.log(JSON.stringify(ekkosSessions, null, 2));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
(0, formatters_js_1.titleBar)('ekkOS Sessions', `${ekkosSessions.length} recent`);
|
|
135
|
+
// Find max cost for bar rendering
|
|
136
|
+
const maxCost = Math.max(...ekkosSessions.map(s => s.cost), 0.01);
|
|
137
|
+
console.log(chalk_1.default.gray(' Session Name │ Turns │ Cost │ Tokens │ Models │ Started'));
|
|
138
|
+
console.log(chalk_1.default.gray(' ' + '─'.repeat(100)));
|
|
139
|
+
for (const s of ekkosSessions) {
|
|
140
|
+
const name = chalk_1.default.bold.cyan(s.name.padEnd(20));
|
|
141
|
+
const turns = s.turnCount.toString().padStart(5);
|
|
142
|
+
const cost = chalk_1.default.green((0, formatters_js_1.formatCost)(s.cost).padStart(7));
|
|
143
|
+
const tokens = (0, formatters_js_1.formatCompact)(s.tokens).padStart(8);
|
|
144
|
+
const models = chalk_1.default.gray(s.models
|
|
145
|
+
.filter(m => !m.includes('synthetic'))
|
|
146
|
+
.map(m => {
|
|
147
|
+
if (m.includes('opus-4-6'))
|
|
148
|
+
return 'opus-4.6';
|
|
149
|
+
if (m.includes('opus-4-5'))
|
|
150
|
+
return 'opus-4.5';
|
|
151
|
+
if (m.includes('sonnet-4-5'))
|
|
152
|
+
return 'sonnet-4.5';
|
|
153
|
+
if (m.includes('haiku-4-5'))
|
|
154
|
+
return 'haiku-4.5';
|
|
155
|
+
return m;
|
|
156
|
+
}).join(', ').padEnd(24));
|
|
157
|
+
const started = chalk_1.default.gray((0, formatters_js_1.formatTimestamp)(s.startedAt));
|
|
158
|
+
const bar = (0, formatters_js_1.renderBar)(s.cost, maxCost, 8);
|
|
159
|
+
console.log(` ${name}│ ${turns} │ ${cost} │ ${tokens} │ ${models}│ ${started} ${bar}`);
|
|
160
|
+
}
|
|
161
|
+
console.log();
|
|
162
|
+
console.log(chalk_1.default.gray(' Use: ekkos usage session <name> for detailed breakdown'));
|
|
163
|
+
console.log(chalk_1.default.bold.cyan('═'.repeat(80)));
|
|
164
|
+
console.log();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Fallback to ccusage project-level sessions
|
|
168
|
+
const sessions = await (0, usage_parser_js_1.getAllSessions)(instanceId);
|
|
169
|
+
if (sessions.length === 0) {
|
|
170
|
+
if (json) {
|
|
171
|
+
console.log(JSON.stringify([]));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
(0, formatters_js_1.noData)('session');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (json) {
|
|
178
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
(0, formatters_js_1.titleBar)('Sessions', `${sessions.length} found`);
|
|
182
|
+
const sorted = [...sessions].sort((a, b) => (b.last_activity || '').localeCompare(a.last_activity || ''));
|
|
183
|
+
for (const session of sorted) {
|
|
184
|
+
console.log(` ${chalk_1.default.bold(session.session_id)}`);
|
|
185
|
+
console.log(` ${chalk_1.default.gray('Cost:')} ${chalk_1.default.green((0, formatters_js_1.formatCost)(session.total_cost))}` +
|
|
186
|
+
` ${chalk_1.default.gray('Tokens:')} ${(0, formatters_js_1.formatCompact)(session.total_tokens)}` +
|
|
187
|
+
` ${chalk_1.default.gray('Last:')} ${session.last_activity}` +
|
|
188
|
+
` ${chalk_1.default.gray('Models:')} ${chalk_1.default.cyan(session.models_used.join(', '))}`);
|
|
189
|
+
console.log();
|
|
190
|
+
}
|
|
191
|
+
console.log(chalk_1.default.bold.cyan('═'.repeat(80)));
|
|
192
|
+
console.log();
|
|
193
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.weeklyCommand = weeklyCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const usage_parser_js_1 = require("../../lib/usage-parser.js");
|
|
9
|
+
const formatters_js_1 = require("./formatters.js");
|
|
10
|
+
/**
|
|
11
|
+
* ekkos usage weekly [--weeks N] [--json]
|
|
12
|
+
*
|
|
13
|
+
* Show weekly usage breakdown
|
|
14
|
+
*/
|
|
15
|
+
async function weeklyCommand(options) {
|
|
16
|
+
const weeks = options.weeks || 8;
|
|
17
|
+
const data = await (0, usage_parser_js_1.getWeeklyUsage)();
|
|
18
|
+
if (data.length === 0) {
|
|
19
|
+
if (options.json) {
|
|
20
|
+
console.log(JSON.stringify([]));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
(0, formatters_js_1.noData)('weekly usage');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Sort by week date descending
|
|
27
|
+
const sorted = [...data].sort((a, b) => (b.week || '').localeCompare(a.week || ''));
|
|
28
|
+
const recent = sorted.slice(0, weeks);
|
|
29
|
+
if (options.json) {
|
|
30
|
+
console.log(JSON.stringify(recent, null, 2));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
(0, formatters_js_1.titleBar)('Weekly Usage Report', `Last ${Math.min(weeks, recent.length)} weeks`);
|
|
34
|
+
const maxCost = Math.max(...recent.map(w => w.totalCost || 0));
|
|
35
|
+
let totalCost = 0;
|
|
36
|
+
console.log(chalk_1.default.gray(' Week of'.padEnd(22)) +
|
|
37
|
+
chalk_1.default.gray('│ ') +
|
|
38
|
+
chalk_1.default.gray('Cost'.padStart(10)) +
|
|
39
|
+
chalk_1.default.gray(' │ ') +
|
|
40
|
+
chalk_1.default.gray('Input'.padStart(8)) +
|
|
41
|
+
chalk_1.default.gray(' │ ') +
|
|
42
|
+
chalk_1.default.gray('Output'.padStart(8)) +
|
|
43
|
+
chalk_1.default.gray(' │ ') +
|
|
44
|
+
chalk_1.default.gray('Cache'.padStart(8)) +
|
|
45
|
+
chalk_1.default.gray(' │ ') +
|
|
46
|
+
chalk_1.default.gray('Chart'));
|
|
47
|
+
console.log(chalk_1.default.gray('─'.repeat(80)));
|
|
48
|
+
for (const week of recent) {
|
|
49
|
+
const cost = week.totalCost || 0;
|
|
50
|
+
totalCost += cost;
|
|
51
|
+
const weekDate = (0, formatters_js_1.formatDate)(week.week || '');
|
|
52
|
+
const bar = (0, formatters_js_1.renderBar)(cost, maxCost, 14);
|
|
53
|
+
console.log(` ${chalk_1.default.white(weekDate.padEnd(20))} │ ${chalk_1.default.green((0, formatters_js_1.formatCost)(cost).padStart(10))} │ ${(0, formatters_js_1.formatCompact)(week.inputTokens || 0).padStart(8)} │ ${(0, formatters_js_1.formatCompact)(week.outputTokens || 0).padStart(8)} │ ${(0, formatters_js_1.formatCompact)(week.cacheReadTokens || 0).padStart(8)} │ ${bar}`);
|
|
54
|
+
}
|
|
55
|
+
console.log(chalk_1.default.gray('─'.repeat(80)));
|
|
56
|
+
console.log(chalk_1.default.bold(' TOTAL'.padEnd(22)) +
|
|
57
|
+
`│ ${chalk_1.default.green.bold((0, formatters_js_1.formatCost)(totalCost).padStart(10))} │`);
|
|
58
|
+
console.log();
|
|
59
|
+
console.log(chalk_1.default.bold.cyan('═'.repeat(80)));
|
|
60
|
+
console.log();
|
|
61
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Deploy
|
|
2
|
+
* Deploy ekkOS instructions to ~/.claude/rules/ekkos.md
|
|
3
|
+
*
|
|
4
|
+
* Uses Claude Code's user-level rules directory — auto-loaded for all projects
|
|
5
|
+
* without touching the user's existing CLAUDE.md.
|
|
3
6
|
*/
|
|
4
7
|
export declare function deployInstructions(): void;
|
|
5
8
|
/**
|
|
6
|
-
* Check if
|
|
9
|
+
* Check if ekkOS instructions are deployed
|
|
7
10
|
*/
|
|
8
11
|
export declare function isInstructionsDeployed(): boolean;
|
|
9
12
|
/**
|
|
@@ -7,21 +7,24 @@ const fs_1 = require("fs");
|
|
|
7
7
|
const platform_1 = require("../utils/platform");
|
|
8
8
|
const templates_1 = require("../utils/templates");
|
|
9
9
|
/**
|
|
10
|
-
* Deploy
|
|
10
|
+
* Deploy ekkOS instructions to ~/.claude/rules/ekkos.md
|
|
11
|
+
*
|
|
12
|
+
* Uses Claude Code's user-level rules directory — auto-loaded for all projects
|
|
13
|
+
* without touching the user's existing CLAUDE.md.
|
|
11
14
|
*/
|
|
12
15
|
function deployInstructions() {
|
|
13
|
-
// Ensure
|
|
14
|
-
if (!(0, fs_1.existsSync)(platform_1.
|
|
15
|
-
(0, fs_1.mkdirSync)(platform_1.
|
|
16
|
+
// Ensure ~/.claude/rules/ exists
|
|
17
|
+
if (!(0, fs_1.existsSync)(platform_1.CLAUDE_RULES_DIR)) {
|
|
18
|
+
(0, fs_1.mkdirSync)(platform_1.CLAUDE_RULES_DIR, { recursive: true });
|
|
16
19
|
}
|
|
17
|
-
//
|
|
18
|
-
(0, templates_1.copyTemplateFile)('CLAUDE.md', platform_1.
|
|
20
|
+
// Deploy to rules/ekkos.md (safe to overwrite — this is our file)
|
|
21
|
+
(0, templates_1.copyTemplateFile)('CLAUDE.md', platform_1.CLAUDE_EKKOS_RULES);
|
|
19
22
|
}
|
|
20
23
|
/**
|
|
21
|
-
* Check if
|
|
24
|
+
* Check if ekkOS instructions are deployed
|
|
22
25
|
*/
|
|
23
26
|
function isInstructionsDeployed() {
|
|
24
|
-
return (0, fs_1.existsSync)(platform_1.
|
|
27
|
+
return (0, fs_1.existsSync)(platform_1.CLAUDE_EKKOS_RULES);
|
|
25
28
|
}
|
|
26
29
|
/**
|
|
27
30
|
* Get the CLAUDE.md content (for preview)
|
package/dist/index.js
CHANGED
|
@@ -42,13 +42,18 @@ const init_1 = require("./commands/init");
|
|
|
42
42
|
const test_1 = require("./commands/test");
|
|
43
43
|
const status_1 = require("./commands/status");
|
|
44
44
|
const run_1 = require("./commands/run");
|
|
45
|
+
const test_claude_1 = require("./commands/test-claude");
|
|
45
46
|
const doctor_1 = require("./commands/doctor");
|
|
46
47
|
const stream_1 = require("./commands/stream");
|
|
47
48
|
const hooks_1 = require("./commands/hooks");
|
|
48
49
|
const setup_remote_1 = require("./commands/setup-remote");
|
|
49
50
|
const agent_1 = require("./commands/agent");
|
|
50
51
|
const state_1 = require("./utils/state");
|
|
51
|
-
const
|
|
52
|
+
const index_1 = require("./commands/usage/index");
|
|
53
|
+
const dashboard_1 = require("./commands/dashboard");
|
|
54
|
+
const swarm_1 = require("./commands/swarm");
|
|
55
|
+
const swarm_dashboard_1 = require("./commands/swarm-dashboard");
|
|
56
|
+
const swarm_setup_1 = require("./commands/swarm-setup");
|
|
52
57
|
const chalk_1 = __importDefault(require("chalk"));
|
|
53
58
|
const fs = __importStar(require("fs"));
|
|
54
59
|
const path = __importStar(require("path"));
|
|
@@ -59,7 +64,158 @@ commander_1.program
|
|
|
59
64
|
.name('ekkos')
|
|
60
65
|
.description('ekkOS memory CLI for AI coding assistants')
|
|
61
66
|
.version(pkg.version)
|
|
62
|
-
.addHelpText('
|
|
67
|
+
.addHelpText('before', [
|
|
68
|
+
'',
|
|
69
|
+
chalk_1.default.cyan.bold(' ekkOS_') + chalk_1.default.gray(` v${pkg.version}`) + chalk_1.default.cyan(' — Memory for AI coding assistants'),
|
|
70
|
+
chalk_1.default.gray(' https://docs.ekkos.dev'),
|
|
71
|
+
'',
|
|
72
|
+
].join('\n'))
|
|
73
|
+
.addHelpText('after', [
|
|
74
|
+
'',
|
|
75
|
+
chalk_1.default.cyan.bold('Examples:'),
|
|
76
|
+
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos')} ${chalk_1.default.gray('Start Claude Code with ekkOS memory (default: run)')}`,
|
|
77
|
+
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos init')} ${chalk_1.default.gray('First-time setup — authenticate + configure your IDE')}`,
|
|
78
|
+
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos run --dashboard')} ${chalk_1.default.gray('Launch with live usage dashboard (tmux split)')}`,
|
|
79
|
+
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos run -b')} ${chalk_1.default.gray('Launch with bypass permissions mode')}`,
|
|
80
|
+
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos doctor --fix')} ${chalk_1.default.gray('Check and auto-fix system prerequisites')}`,
|
|
81
|
+
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos usage daily')} ${chalk_1.default.gray("View today's token usage and costs")}`,
|
|
82
|
+
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos swarm launch -t "build X"')} ${chalk_1.default.gray('Launch parallel workers on a task')}`,
|
|
83
|
+
'',
|
|
84
|
+
chalk_1.default.gray(' Run ') + chalk_1.default.white('ekkos <command> --help') + chalk_1.default.gray(' for detailed options on any command.'),
|
|
85
|
+
'',
|
|
86
|
+
].join('\n'))
|
|
87
|
+
.configureHelp({
|
|
88
|
+
formatHelp: (cmd, helper) => {
|
|
89
|
+
// Only customize root help — subcommands use default formatting
|
|
90
|
+
if (cmd.parent) {
|
|
91
|
+
// Fall back to default formatting for subcommands
|
|
92
|
+
const termWidth = helper.padWidth(cmd, helper);
|
|
93
|
+
const helpWidth = helper.helpWidth || 80;
|
|
94
|
+
const itemIndentWidth = 2;
|
|
95
|
+
const itemSeparatorWidth = 2;
|
|
96
|
+
let output = '';
|
|
97
|
+
// Description
|
|
98
|
+
const desc = helper.commandDescription(cmd);
|
|
99
|
+
if (desc) {
|
|
100
|
+
output += desc + '\n\n';
|
|
101
|
+
}
|
|
102
|
+
// Usage
|
|
103
|
+
output += chalk_1.default.cyan.bold('Usage:') + ' ' + helper.commandUsage(cmd) + '\n';
|
|
104
|
+
// Arguments
|
|
105
|
+
const args = helper.visibleArguments(cmd);
|
|
106
|
+
if (args.length > 0) {
|
|
107
|
+
output += '\n' + chalk_1.default.cyan.bold('Arguments:') + '\n';
|
|
108
|
+
for (const arg of args) {
|
|
109
|
+
const term = helper.argumentTerm(arg);
|
|
110
|
+
const desc = helper.argumentDescription(arg);
|
|
111
|
+
output += ` ${chalk_1.default.green(term.padEnd(termWidth + itemSeparatorWidth))}${desc}\n`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Options
|
|
115
|
+
const opts = helper.visibleOptions(cmd);
|
|
116
|
+
if (opts.length > 0) {
|
|
117
|
+
output += '\n' + chalk_1.default.cyan.bold('Options:') + '\n';
|
|
118
|
+
for (const opt of opts) {
|
|
119
|
+
const term = helper.optionTerm(opt);
|
|
120
|
+
const desc = helper.optionDescription(opt);
|
|
121
|
+
output += ` ${chalk_1.default.green(term.padEnd(termWidth + itemSeparatorWidth))}${desc}\n`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Subcommands
|
|
125
|
+
const cmds = helper.visibleCommands(cmd);
|
|
126
|
+
if (cmds.length > 0) {
|
|
127
|
+
output += '\n' + chalk_1.default.cyan.bold('Commands:') + '\n';
|
|
128
|
+
for (const sub of cmds) {
|
|
129
|
+
const term = helper.subcommandTerm(sub);
|
|
130
|
+
const desc = helper.subcommandDescription(sub);
|
|
131
|
+
output += ` ${chalk_1.default.green(term.padEnd(termWidth + itemSeparatorWidth))}${desc}\n`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return output;
|
|
135
|
+
}
|
|
136
|
+
// ── Root help: grouped command listing ──
|
|
137
|
+
const commands = helper.visibleCommands(cmd);
|
|
138
|
+
const cmdMap = new Map();
|
|
139
|
+
for (const sub of commands) {
|
|
140
|
+
cmdMap.set(sub.name(), {
|
|
141
|
+
term: helper.subcommandTerm(sub),
|
|
142
|
+
desc: helper.subcommandDescription(sub),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const groups = [
|
|
146
|
+
{
|
|
147
|
+
title: 'Getting Started',
|
|
148
|
+
icon: '▸',
|
|
149
|
+
commands: [
|
|
150
|
+
{ name: 'init', desc: 'Authenticate and configure your IDE (Claude, Cursor, Windsurf)' },
|
|
151
|
+
{ name: 'status', desc: 'Show memory status and installation info' },
|
|
152
|
+
{ name: 'test', desc: 'Test connection to ekkOS memory API' },
|
|
153
|
+
{ name: 'doctor', desc: 'Check system prerequisites (Node, PTY, Claude, MCP)' },
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
title: 'Running',
|
|
158
|
+
icon: '▸',
|
|
159
|
+
commands: [
|
|
160
|
+
{ name: 'run', desc: 'Launch Claude Code with ekkOS memory + auto-continue', note: 'default' },
|
|
161
|
+
{ name: 'test-claude', desc: 'Launch Claude with proxy only (no ccDNA/PTY) for debugging' },
|
|
162
|
+
{ name: 'sessions', desc: 'List active Claude Code sessions' },
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
title: 'Monitoring & Usage',
|
|
167
|
+
icon: '▸',
|
|
168
|
+
commands: [
|
|
169
|
+
{ name: 'usage', desc: 'Token usage and cost tracking (daily, weekly, monthly, session)' },
|
|
170
|
+
{ name: 'dashboard', desc: 'Live TUI dashboard for session monitoring' },
|
|
171
|
+
{ name: 'stream', desc: 'Stream capture status and management' },
|
|
172
|
+
{ name: 'hooks', desc: 'Install, verify, and manage ekkOS hooks' },
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
title: 'Remote Terminal',
|
|
177
|
+
icon: '▸',
|
|
178
|
+
commands: [
|
|
179
|
+
{ name: 'setup-remote', desc: 'Set up remote access (run Claude on your PC from anywhere)' },
|
|
180
|
+
{ name: 'agent', desc: 'Manage the remote terminal agent (start, stop, status, logs)' },
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
title: 'Swarm (Multi-Agent)',
|
|
185
|
+
icon: '▸',
|
|
186
|
+
commands: [
|
|
187
|
+
{ name: 'swarm', desc: 'Parallel workers, Q-learning routing, swarm dashboard' },
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
];
|
|
191
|
+
const padCmd = 18;
|
|
192
|
+
let output = '';
|
|
193
|
+
output += chalk_1.default.cyan.bold('Usage:') + ` ekkos ${chalk_1.default.gray('[command] [options]')}\n`;
|
|
194
|
+
for (const group of groups) {
|
|
195
|
+
output += '\n' + chalk_1.default.cyan.bold(`${group.icon} ${group.title}`) + '\n';
|
|
196
|
+
for (const c of group.commands) {
|
|
197
|
+
const nameStr = c.note
|
|
198
|
+
? chalk_1.default.green(c.name) + chalk_1.default.gray(` (${c.note})`)
|
|
199
|
+
: chalk_1.default.green(c.name);
|
|
200
|
+
// Compute visible length for padding (strip ANSI)
|
|
201
|
+
const visLen = c.note ? c.name.length + ` (${c.note})`.length : c.name.length;
|
|
202
|
+
const pad = Math.max(1, padCmd - visLen);
|
|
203
|
+
output += ` ${nameStr}${' '.repeat(pad)}${chalk_1.default.gray(c.desc)}\n`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Options
|
|
207
|
+
const opts = helper.visibleOptions(cmd);
|
|
208
|
+
if (opts.length > 0) {
|
|
209
|
+
output += '\n' + chalk_1.default.cyan.bold('Options:') + '\n';
|
|
210
|
+
for (const opt of opts) {
|
|
211
|
+
const term = helper.optionTerm(opt);
|
|
212
|
+
const desc = helper.optionDescription(opt);
|
|
213
|
+
output += ` ${chalk_1.default.green(term.padEnd(22))}${desc}\n`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return output;
|
|
217
|
+
},
|
|
218
|
+
});
|
|
63
219
|
// Main init command (combined auth + setup)
|
|
64
220
|
commander_1.program
|
|
65
221
|
.command('init')
|
|
@@ -92,6 +248,9 @@ commander_1.program
|
|
|
92
248
|
.option('--skip-inject', 'Monitor-only mode (detect context wall but print instructions instead of auto-inject)')
|
|
93
249
|
.option('--skip-dna', 'Skip ccDNA injection (bypass Claude Code patching)')
|
|
94
250
|
.option('--skip-proxy', 'Skip API proxy (use direct Anthropic API, disables seamless context eviction)')
|
|
251
|
+
.option('--dashboard', 'Launch with live usage dashboard in an isolated 60/40 tmux split (requires tmux)')
|
|
252
|
+
.option('--kickstart', 'Auto-send "test" on load to create session immediately (used internally by --dashboard)')
|
|
253
|
+
.option('--add-dir <dirs...>', 'Additional directories Claude Code can access (outside working directory)')
|
|
95
254
|
.action((options) => {
|
|
96
255
|
(0, run_1.run)({
|
|
97
256
|
session: options.session,
|
|
@@ -101,7 +260,24 @@ commander_1.program
|
|
|
101
260
|
noInject: options.skipInject,
|
|
102
261
|
research: options.research,
|
|
103
262
|
noDna: options.skipDna,
|
|
104
|
-
noProxy: options.skipProxy
|
|
263
|
+
noProxy: options.skipProxy,
|
|
264
|
+
dashboard: options.dashboard,
|
|
265
|
+
kickstart: options.kickstart,
|
|
266
|
+
addDirs: options.addDir,
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
// Test Claude — bare proxy test (no CLI wrapper)
|
|
270
|
+
commander_1.program
|
|
271
|
+
.command('test-claude')
|
|
272
|
+
.description('Launch Claude Code with proxy only (no ccDNA, no PTY, no injections) for debugging')
|
|
273
|
+
.option('--no-proxy', 'Skip proxy too (completely vanilla Claude)')
|
|
274
|
+
.option('--no-hooks', 'Temporarily disable all hooks during test')
|
|
275
|
+
.option('-v, --verbose', 'Show debug output')
|
|
276
|
+
.action((options) => {
|
|
277
|
+
(0, test_claude_1.testClaude)({
|
|
278
|
+
noProxy: options.proxy === false,
|
|
279
|
+
noHooks: options.hooks === false,
|
|
280
|
+
verbose: options.verbose,
|
|
105
281
|
});
|
|
106
282
|
});
|
|
107
283
|
// Doctor command - check system prerequisites
|
|
@@ -174,22 +350,10 @@ hooksCmd
|
|
|
174
350
|
.action((options) => {
|
|
175
351
|
(0, hooks_1.hooksStatus)({ verbose: options.verbose });
|
|
176
352
|
});
|
|
177
|
-
// Usage command - track
|
|
178
|
-
commander_1.program
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
.option('--instance <id>', 'Instance ID (project path identifier)')
|
|
182
|
-
.option('--list', 'List available sessions')
|
|
183
|
-
.action((sessionId, options) => {
|
|
184
|
-
const args = [];
|
|
185
|
-
if (sessionId)
|
|
186
|
-
args.push(sessionId);
|
|
187
|
-
if (options.instance)
|
|
188
|
-
args.push('--instance', options.instance);
|
|
189
|
-
if (options.list)
|
|
190
|
-
args.push('--list');
|
|
191
|
-
(0, usage_1.usageCommand)(args);
|
|
192
|
-
});
|
|
353
|
+
// Usage command - track Claude Code token usage and costs (powered by ccusage)
|
|
354
|
+
(0, index_1.registerUsageCommand)(commander_1.program);
|
|
355
|
+
// Dashboard command - live TUI for monitoring session usage
|
|
356
|
+
commander_1.program.addCommand(dashboard_1.dashboardCommand);
|
|
193
357
|
// Sessions command - list active Claude Code sessions (swarm support)
|
|
194
358
|
commander_1.program
|
|
195
359
|
.command('sessions')
|
|
@@ -222,7 +386,7 @@ commander_1.program
|
|
|
222
386
|
}
|
|
223
387
|
}
|
|
224
388
|
});
|
|
225
|
-
// Deprecated setup command (redirects to init)
|
|
389
|
+
// Deprecated setup command (redirects to init) — hidden from root help by custom formatHelp
|
|
226
390
|
commander_1.program
|
|
227
391
|
.command('setup')
|
|
228
392
|
.description('[DEPRECATED] Use "ekkos init" instead')
|
|
@@ -311,4 +475,76 @@ agentCmd
|
|
|
311
475
|
.action((options) => {
|
|
312
476
|
(0, agent_1.agentLogs)({ follow: options.follow });
|
|
313
477
|
});
|
|
478
|
+
agentCmd
|
|
479
|
+
.command('health')
|
|
480
|
+
.description('Check agent daemon health and diagnose connection issues')
|
|
481
|
+
.option('-j, --json', 'Output machine-readable JSON')
|
|
482
|
+
.action((options) => {
|
|
483
|
+
(0, agent_1.agentHealth)({ json: options.json });
|
|
484
|
+
});
|
|
485
|
+
// Swarm command - manage Q-learning routing
|
|
486
|
+
const swarmCmd = commander_1.program
|
|
487
|
+
.command('swarm')
|
|
488
|
+
.description('Manage Swarm Q-learning model routing');
|
|
489
|
+
swarmCmd
|
|
490
|
+
.command('status')
|
|
491
|
+
.description('Show Q-table stats (states, visits, epsilon, top actions)')
|
|
492
|
+
.action(swarm_1.swarmStatus);
|
|
493
|
+
swarmCmd
|
|
494
|
+
.command('reset')
|
|
495
|
+
.description('Clear Q-table from Redis (routing reverts to static rules)')
|
|
496
|
+
.action(swarm_1.swarmReset);
|
|
497
|
+
swarmCmd
|
|
498
|
+
.command('export')
|
|
499
|
+
.description('Export Q-table to .swarm/q-learning-model.json')
|
|
500
|
+
.action(swarm_1.swarmExport);
|
|
501
|
+
swarmCmd
|
|
502
|
+
.command('import')
|
|
503
|
+
.description('Import Q-table from .swarm/q-learning-model.json into Redis')
|
|
504
|
+
.action(swarm_1.swarmImport);
|
|
505
|
+
swarmCmd
|
|
506
|
+
.command('launch')
|
|
507
|
+
.description('Launch parallel workers on a decomposed task (opens wizard if --task is omitted)')
|
|
508
|
+
.option('-w, --workers <count>', 'Number of parallel workers (2-8)', parseInt)
|
|
509
|
+
.option('-t, --task <task>', 'Task description to decompose and execute')
|
|
510
|
+
.option('--no-bypass', 'Disable bypass permissions mode')
|
|
511
|
+
.option('--no-decompose', 'Skip AI decomposition (send same task to all workers)')
|
|
512
|
+
.option('--no-queen', 'Skip launching the Python Queen coordinator')
|
|
513
|
+
.option('--queen-strategy <strategy>', 'Queen strategy (adaptive-default, hierarchical-cascade, mesh-consensus)')
|
|
514
|
+
.option('-v, --verbose', 'Show debug output')
|
|
515
|
+
.action((options) => {
|
|
516
|
+
// Auto-open wizard when --task is missing
|
|
517
|
+
if (!options.task) {
|
|
518
|
+
(0, swarm_setup_1.swarmSetup)();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
(0, swarm_1.swarmLaunch)({
|
|
522
|
+
workers: options.workers || 4,
|
|
523
|
+
task: options.task,
|
|
524
|
+
bypass: options.bypass !== false,
|
|
525
|
+
noDecompose: options.decompose === false,
|
|
526
|
+
noQueen: options.queen === false,
|
|
527
|
+
queenStrategy: options.queenStrategy,
|
|
528
|
+
verbose: options.verbose,
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
swarmCmd
|
|
532
|
+
.command('setup')
|
|
533
|
+
.description('Interactive TUI wizard for configuring and launching a swarm')
|
|
534
|
+
.action(() => {
|
|
535
|
+
(0, swarm_setup_1.swarmSetup)();
|
|
536
|
+
});
|
|
537
|
+
swarmCmd.addCommand(swarm_dashboard_1.swarmDashboardCommand);
|
|
538
|
+
// Handle `-help` (single dash) — rewrite to `--help` for Commander compatibility
|
|
539
|
+
const helpIdx = process.argv.indexOf('-help');
|
|
540
|
+
if (helpIdx !== -1) {
|
|
541
|
+
process.argv[helpIdx] = '--help';
|
|
542
|
+
}
|
|
543
|
+
// Default to `run` if no command specified (e.g. `ekkos`, `ekkos -b --dashboard`)
|
|
544
|
+
const knownCommands = commander_1.program.commands.map(c => c.name());
|
|
545
|
+
const userArgs = process.argv.slice(2);
|
|
546
|
+
const hasCommand = userArgs.some(a => knownCommands.includes(a) || a === 'help' || a === '--help' || a === '-h' || a === '--version' || a === '-V');
|
|
547
|
+
if (!hasCommand) {
|
|
548
|
+
process.argv.splice(2, 0, 'run');
|
|
549
|
+
}
|
|
314
550
|
commander_1.program.parse();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* tmux-scrollbar.ts
|
|
4
|
+
*
|
|
5
|
+
* A thin visual scrollbar (2 columns wide) that runs in its own narrow tmux pane.
|
|
6
|
+
* It tracks the scroll position of a target pane (Claude Code) and renders a
|
|
7
|
+
* proportional scrollbar thumb. Supports click-to-jump and drag-to-scroll.
|
|
8
|
+
*
|
|
9
|
+
* Usage: node tmux-scrollbar.js <target-pane>
|
|
10
|
+
* Example: node tmux-scrollbar.js :.0
|
|
11
|
+
*
|
|
12
|
+
* Layout: [Claude Code (68%)] [Scrollbar (2col)] [Dashboard (30%)]
|
|
13
|
+
*/
|
|
14
|
+
export {};
|