@proofofprotocol/inscribe-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/package.json +58 -0
- package/src/cli/commands/balance.js +151 -0
- package/src/cli/commands/config.js +87 -0
- package/src/cli/commands/init.js +186 -0
- package/src/cli/commands/log.js +193 -0
- package/src/cli/commands/show.js +249 -0
- package/src/cli/index.js +44 -0
- package/src/cli/lib/config.js +126 -0
- package/src/cli/lib/exit-codes.js +19 -0
- package/src/lib/did.js +64 -0
- package/src/lib/hedera.js +221 -0
- package/src/lib/logger.js +200 -0
- package/src/server-sse.js +239 -0
- package/src/server.js +102 -0
- package/src/test.js +107 -0
- package/src/tools/layer1/history.js +174 -0
- package/src/tools/layer1/identity.js +120 -0
- package/src/tools/layer1/inscribe.js +132 -0
- package/src/tools/layer1/inscribe_url.js +193 -0
- package/src/tools/layer1/verify.js +177 -0
- package/src/tools/layer2/account.js +155 -0
- package/src/tools/layer2/hcs.js +163 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init command
|
|
3
|
+
*
|
|
4
|
+
* Initialize inscribe-mcp configuration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { createInterface } from 'readline';
|
|
9
|
+
import { writeConfig, configExists, getConfigPath } from '../lib/config.js';
|
|
10
|
+
import { EXIT_CODES } from '../lib/exit-codes.js';
|
|
11
|
+
|
|
12
|
+
// Simple prompt helper (no external dependency)
|
|
13
|
+
function prompt(question) {
|
|
14
|
+
const rl = createInterface({
|
|
15
|
+
input: process.stdin,
|
|
16
|
+
output: process.stdout
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
rl.question(question, (answer) => {
|
|
21
|
+
rl.close();
|
|
22
|
+
resolve(answer.trim());
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ANSI colors
|
|
28
|
+
const colors = {
|
|
29
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
30
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
31
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
32
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
33
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
34
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const initCommand = new Command('init')
|
|
38
|
+
.description('Initialize inscribe-mcp configuration')
|
|
39
|
+
.option('-f, --force', 'Overwrite existing configuration')
|
|
40
|
+
.action(async (options) => {
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log(colors.bold('inscribe-mcp Setup'));
|
|
43
|
+
console.log('─'.repeat(40));
|
|
44
|
+
console.log('');
|
|
45
|
+
|
|
46
|
+
// Check existing config
|
|
47
|
+
if (configExists() && !options.force) {
|
|
48
|
+
console.log(colors.yellow('Config already exists: ') + getConfigPath());
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log('Use --force to overwrite.');
|
|
51
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Network selection
|
|
55
|
+
console.log(colors.bold('1. Select Network'));
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(' 1) testnet ' + colors.dim('(recommended for testing)'));
|
|
58
|
+
console.log(' 2) mainnet ' + colors.dim('(real HBAR costs)'));
|
|
59
|
+
console.log('');
|
|
60
|
+
|
|
61
|
+
let network;
|
|
62
|
+
while (!network) {
|
|
63
|
+
const choice = await prompt('Select (1 or 2): ');
|
|
64
|
+
if (choice === '1') network = 'testnet';
|
|
65
|
+
else if (choice === '2') network = 'mainnet';
|
|
66
|
+
else console.log(colors.red('Invalid selection. Enter 1 or 2.'));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log('');
|
|
70
|
+
console.log(colors.green('✓') + ` Network: ${network}`);
|
|
71
|
+
console.log('');
|
|
72
|
+
|
|
73
|
+
// Mainnet warning
|
|
74
|
+
if (network === 'mainnet') {
|
|
75
|
+
console.log(colors.yellow('⚠ Mainnet Warning'));
|
|
76
|
+
console.log('─'.repeat(40));
|
|
77
|
+
console.log('• Real HBAR will be consumed');
|
|
78
|
+
console.log('• Transactions are irreversible');
|
|
79
|
+
console.log('• Secure your private key');
|
|
80
|
+
console.log('');
|
|
81
|
+
const confirm = await prompt('Continue? (yes/no): ');
|
|
82
|
+
if (confirm.toLowerCase() !== 'yes') {
|
|
83
|
+
console.log('Aborted.');
|
|
84
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
85
|
+
}
|
|
86
|
+
console.log('');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Account setup
|
|
90
|
+
console.log(colors.bold('2. Hedera Account'));
|
|
91
|
+
console.log('');
|
|
92
|
+
|
|
93
|
+
if (network === 'testnet') {
|
|
94
|
+
console.log(colors.cyan('Testnet Faucet:'));
|
|
95
|
+
console.log(' https://portal.hedera.com/faucet');
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log('Create an account and get free HBAR for testing.');
|
|
98
|
+
console.log('');
|
|
99
|
+
} else {
|
|
100
|
+
console.log(colors.cyan('Create Mainnet Account:'));
|
|
101
|
+
console.log(' HashPack: https://www.hashpack.app/');
|
|
102
|
+
console.log(' Blade: https://www.bladewallet.io/');
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log('Transfer a small amount of HBAR (1-5 HBAR recommended).');
|
|
105
|
+
console.log('');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Account ID input
|
|
109
|
+
let accountId;
|
|
110
|
+
while (!accountId) {
|
|
111
|
+
const input = await prompt('Account ID (e.g., 0.0.123456): ');
|
|
112
|
+
if (/^0\.0\.\d+$/.test(input)) {
|
|
113
|
+
accountId = input;
|
|
114
|
+
} else {
|
|
115
|
+
console.log(colors.red('Invalid format. Use 0.0.XXXXXX'));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log(colors.green('✓') + ` Account ID: ${accountId}`);
|
|
120
|
+
console.log('');
|
|
121
|
+
|
|
122
|
+
// Private key input
|
|
123
|
+
console.log(colors.bold('3. Private Key'));
|
|
124
|
+
console.log('');
|
|
125
|
+
console.log(colors.dim('Your private key is stored locally and never transmitted.'));
|
|
126
|
+
console.log('');
|
|
127
|
+
|
|
128
|
+
let privateKey;
|
|
129
|
+
while (!privateKey) {
|
|
130
|
+
const input = await prompt('Private Key (hex or DER format): ');
|
|
131
|
+
if (input.length >= 32) {
|
|
132
|
+
privateKey = input;
|
|
133
|
+
} else {
|
|
134
|
+
console.log(colors.red('Private key seems too short.'));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(colors.green('✓') + ' Private key received');
|
|
139
|
+
console.log('');
|
|
140
|
+
|
|
141
|
+
// Topic ID (optional)
|
|
142
|
+
console.log(colors.bold('4. Topic ID (optional)'));
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log(colors.dim('Leave empty to auto-create on first inscribe.'));
|
|
145
|
+
console.log('');
|
|
146
|
+
|
|
147
|
+
const topicInput = await prompt('Topic ID (e.g., 0.0.123456) or Enter to skip: ');
|
|
148
|
+
let topicId = null;
|
|
149
|
+
if (topicInput && /^0\.0\.\d+$/.test(topicInput)) {
|
|
150
|
+
topicId = topicInput;
|
|
151
|
+
console.log(colors.green('✓') + ` Topic ID: ${topicId}`);
|
|
152
|
+
} else if (topicInput) {
|
|
153
|
+
console.log(colors.yellow('Invalid format, skipping.'));
|
|
154
|
+
} else {
|
|
155
|
+
console.log(colors.dim('Topic will be created on first inscribe.'));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log('');
|
|
159
|
+
|
|
160
|
+
// Build and save config
|
|
161
|
+
const config = {
|
|
162
|
+
network,
|
|
163
|
+
operatorAccountId: accountId,
|
|
164
|
+
operatorPrivateKey: privateKey
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (topicId) {
|
|
168
|
+
config.defaultTopicId = topicId;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
writeConfig(config);
|
|
172
|
+
|
|
173
|
+
// Summary
|
|
174
|
+
console.log('─'.repeat(40));
|
|
175
|
+
console.log(colors.green(colors.bold('✓ Setup Complete!')));
|
|
176
|
+
console.log('');
|
|
177
|
+
console.log('Config saved to:');
|
|
178
|
+
console.log(` ${getConfigPath()}`);
|
|
179
|
+
console.log('');
|
|
180
|
+
console.log('Next steps:');
|
|
181
|
+
console.log(' • Run ' + colors.cyan('inscribe-mcp config') + ' to verify');
|
|
182
|
+
console.log(' • Run ' + colors.cyan('inscribe-mcp balance') + ' to check balance');
|
|
183
|
+
console.log('');
|
|
184
|
+
|
|
185
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
186
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* log command
|
|
3
|
+
*
|
|
4
|
+
* Display MCP execution logs (Read-Only).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { getLogsDir } from '../../lib/logger.js';
|
|
11
|
+
import { EXIT_CODES } from '../lib/exit-codes.js';
|
|
12
|
+
|
|
13
|
+
// ANSI colors
|
|
14
|
+
const colors = {
|
|
15
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
16
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
17
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
18
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
19
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
20
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse time duration string (e.g., "1h", "30m", "7d")
|
|
25
|
+
* @returns {number} milliseconds
|
|
26
|
+
*/
|
|
27
|
+
function parseDuration(str) {
|
|
28
|
+
const match = str.match(/^(\d+)([smhd])$/);
|
|
29
|
+
if (!match) return null;
|
|
30
|
+
|
|
31
|
+
const value = parseInt(match[1], 10);
|
|
32
|
+
const unit = match[2];
|
|
33
|
+
|
|
34
|
+
const multipliers = {
|
|
35
|
+
s: 1000,
|
|
36
|
+
m: 60 * 1000,
|
|
37
|
+
h: 60 * 60 * 1000,
|
|
38
|
+
d: 24 * 60 * 60 * 1000
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return value * multipliers[unit];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Format timestamp for display
|
|
46
|
+
*/
|
|
47
|
+
function formatTime(isoString) {
|
|
48
|
+
const date = new Date(isoString);
|
|
49
|
+
return date.toLocaleString('ja-JP', {
|
|
50
|
+
month: '2-digit',
|
|
51
|
+
day: '2-digit',
|
|
52
|
+
hour: '2-digit',
|
|
53
|
+
minute: '2-digit',
|
|
54
|
+
second: '2-digit'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format log entry for display
|
|
60
|
+
*/
|
|
61
|
+
function formatLogEntry(entry) {
|
|
62
|
+
const time = formatTime(entry.timestamp);
|
|
63
|
+
const cmd = (entry.command || 'unknown').toUpperCase().padEnd(12);
|
|
64
|
+
const topic = entry.topicId ? `topic=${entry.topicId}` : '';
|
|
65
|
+
const latency = entry.latency ? `${entry.latency}ms` : '—';
|
|
66
|
+
|
|
67
|
+
let status;
|
|
68
|
+
if (entry.status === 'success') {
|
|
69
|
+
status = colors.green('OK');
|
|
70
|
+
} else {
|
|
71
|
+
status = colors.red('FAILED');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let line = `[${time}] ${cmd} ${topic.padEnd(20)} latency=${latency.padEnd(8)} ${status}`;
|
|
75
|
+
|
|
76
|
+
if (entry.error) {
|
|
77
|
+
line += ` (${entry.error})`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return line;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Read all log entries from log files
|
|
85
|
+
*/
|
|
86
|
+
function readLogs(logsDir, options = {}) {
|
|
87
|
+
const { since, errorOnly } = options;
|
|
88
|
+
const entries = [];
|
|
89
|
+
|
|
90
|
+
if (!existsSync(logsDir)) {
|
|
91
|
+
return entries;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Get log files sorted by date (newest first)
|
|
95
|
+
const files = readdirSync(logsDir)
|
|
96
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
97
|
+
.sort()
|
|
98
|
+
.reverse();
|
|
99
|
+
|
|
100
|
+
const sinceMs = since ? parseDuration(since) : null;
|
|
101
|
+
const cutoff = sinceMs ? Date.now() - sinceMs : null;
|
|
102
|
+
|
|
103
|
+
for (const file of files) {
|
|
104
|
+
const filePath = join(logsDir, file);
|
|
105
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
106
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
107
|
+
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
try {
|
|
110
|
+
const entry = JSON.parse(line);
|
|
111
|
+
|
|
112
|
+
// Skip debug entries
|
|
113
|
+
if (entry.level === 'debug') continue;
|
|
114
|
+
|
|
115
|
+
// Apply time filter
|
|
116
|
+
if (cutoff) {
|
|
117
|
+
const entryTime = new Date(entry.timestamp).getTime();
|
|
118
|
+
if (entryTime < cutoff) continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Apply error filter
|
|
122
|
+
if (errorOnly && entry.status !== 'error') continue;
|
|
123
|
+
|
|
124
|
+
entries.push(entry);
|
|
125
|
+
} catch {
|
|
126
|
+
// Skip invalid lines
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Sort by timestamp descending
|
|
132
|
+
entries.sort((a, b) =>
|
|
133
|
+
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return entries;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const logCommand = new Command('log')
|
|
140
|
+
.description('Display MCP execution logs')
|
|
141
|
+
.option('-n, --tail <n>', 'Show last N entries', '20')
|
|
142
|
+
.option('-s, --since <duration>', 'Show entries since (e.g., 1h, 30m, 7d)')
|
|
143
|
+
.option('-e, --error', 'Show only errors')
|
|
144
|
+
.option('--json', 'Output as JSON')
|
|
145
|
+
.action(async (options) => {
|
|
146
|
+
const logsDir = getLogsDir();
|
|
147
|
+
|
|
148
|
+
if (!existsSync(logsDir)) {
|
|
149
|
+
console.log('');
|
|
150
|
+
console.log(colors.yellow('No logs found.'));
|
|
151
|
+
console.log('');
|
|
152
|
+
console.log('Logs are created when MCP tools are executed.');
|
|
153
|
+
console.log('');
|
|
154
|
+
process.exit(EXIT_CODES.LOG_NOT_FOUND);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const entries = readLogs(logsDir, {
|
|
158
|
+
since: options.since,
|
|
159
|
+
errorOnly: options.error
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (entries.length === 0) {
|
|
163
|
+
console.log('');
|
|
164
|
+
console.log(colors.yellow('No matching log entries.'));
|
|
165
|
+
console.log('');
|
|
166
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Apply tail limit
|
|
170
|
+
const limit = parseInt(options.tail, 10) || 20;
|
|
171
|
+
const displayEntries = entries.slice(0, limit);
|
|
172
|
+
|
|
173
|
+
if (options.json) {
|
|
174
|
+
console.log(JSON.stringify(displayEntries, null, 2));
|
|
175
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Display logs
|
|
179
|
+
console.log('');
|
|
180
|
+
console.log(colors.bold('inscribe-mcp Logs'));
|
|
181
|
+
console.log('─'.repeat(80));
|
|
182
|
+
|
|
183
|
+
// Reverse for chronological display
|
|
184
|
+
displayEntries.reverse().forEach(entry => {
|
|
185
|
+
console.log(formatLogEntry(entry));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
console.log('─'.repeat(80));
|
|
189
|
+
console.log(colors.dim(`Showing ${displayEntries.length} of ${entries.length} entries`));
|
|
190
|
+
console.log('');
|
|
191
|
+
|
|
192
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
193
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* show command
|
|
3
|
+
*
|
|
4
|
+
* Display nvidia-smi style dashboard for inscribe-mcp status.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { getLogsDir } from '../../lib/logger.js';
|
|
11
|
+
import { configExists, readConfig, maskPrivateKey } from '../lib/config.js';
|
|
12
|
+
import { EXIT_CODES } from '../lib/exit-codes.js';
|
|
13
|
+
|
|
14
|
+
// ANSI colors
|
|
15
|
+
const colors = {
|
|
16
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
17
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
18
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
19
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
20
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
21
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get Mirror Node URL
|
|
26
|
+
*/
|
|
27
|
+
function getMirrorUrl(network) {
|
|
28
|
+
return network === 'mainnet'
|
|
29
|
+
? 'https://mainnet.mirrornode.hedera.com'
|
|
30
|
+
: 'https://testnet.mirrornode.hedera.com';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format relative time
|
|
35
|
+
*/
|
|
36
|
+
function formatRelativeTime(isoString) {
|
|
37
|
+
if (!isoString) return 'never';
|
|
38
|
+
|
|
39
|
+
const diff = Date.now() - new Date(isoString).getTime();
|
|
40
|
+
const minutes = Math.floor(diff / 60000);
|
|
41
|
+
const hours = Math.floor(diff / 3600000);
|
|
42
|
+
const days = Math.floor(diff / 86400000);
|
|
43
|
+
|
|
44
|
+
if (days > 0) return `${days}d ago`;
|
|
45
|
+
if (hours > 0) return `${hours}h ago`;
|
|
46
|
+
if (minutes > 0) return `${minutes}m ago`;
|
|
47
|
+
return 'just now';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read all log entries and compute stats
|
|
52
|
+
*/
|
|
53
|
+
function computeStats(logsDir) {
|
|
54
|
+
const stats = {
|
|
55
|
+
total: 0,
|
|
56
|
+
success: 0,
|
|
57
|
+
error: 0,
|
|
58
|
+
byCommand: {},
|
|
59
|
+
totalLatency: 0,
|
|
60
|
+
latencyCount: 0,
|
|
61
|
+
lastInscribe: null,
|
|
62
|
+
lastVerify: null,
|
|
63
|
+
lastError: null,
|
|
64
|
+
lastErrorMessage: null
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (!existsSync(logsDir)) {
|
|
68
|
+
return stats;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const files = readdirSync(logsDir).filter(f => f.endsWith('.jsonl'));
|
|
72
|
+
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
const filePath = join(logsDir, file);
|
|
75
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
76
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
77
|
+
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
try {
|
|
80
|
+
const entry = JSON.parse(line);
|
|
81
|
+
|
|
82
|
+
// Skip debug entries
|
|
83
|
+
if (entry.level === 'debug') continue;
|
|
84
|
+
|
|
85
|
+
stats.total++;
|
|
86
|
+
|
|
87
|
+
if (entry.status === 'success') {
|
|
88
|
+
stats.success++;
|
|
89
|
+
} else {
|
|
90
|
+
stats.error++;
|
|
91
|
+
if (!stats.lastError || entry.timestamp > stats.lastError) {
|
|
92
|
+
stats.lastError = entry.timestamp;
|
|
93
|
+
stats.lastErrorMessage = entry.error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Track by command
|
|
98
|
+
const cmd = entry.command || 'unknown';
|
|
99
|
+
if (!stats.byCommand[cmd]) {
|
|
100
|
+
stats.byCommand[cmd] = { success: 0, error: 0 };
|
|
101
|
+
}
|
|
102
|
+
if (entry.status === 'success') {
|
|
103
|
+
stats.byCommand[cmd].success++;
|
|
104
|
+
} else {
|
|
105
|
+
stats.byCommand[cmd].error++;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Track latency
|
|
109
|
+
if (entry.latency) {
|
|
110
|
+
stats.totalLatency += entry.latency;
|
|
111
|
+
stats.latencyCount++;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Track last operations
|
|
115
|
+
if (entry.command === 'inscribe' && entry.status === 'success') {
|
|
116
|
+
if (!stats.lastInscribe || entry.timestamp > stats.lastInscribe) {
|
|
117
|
+
stats.lastInscribe = entry.timestamp;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (entry.command === 'verify' && entry.status === 'success') {
|
|
121
|
+
if (!stats.lastVerify || entry.timestamp > stats.lastVerify) {
|
|
122
|
+
stats.lastVerify = entry.timestamp;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
// Skip invalid lines
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
stats.avgLatency = stats.latencyCount > 0
|
|
132
|
+
? Math.round(stats.totalLatency / stats.latencyCount)
|
|
133
|
+
: 0;
|
|
134
|
+
|
|
135
|
+
return stats;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const showCommand = new Command('show')
|
|
139
|
+
.description('Display inscribe-mcp status dashboard')
|
|
140
|
+
.option('--json', 'Output as JSON')
|
|
141
|
+
.action(async (options) => {
|
|
142
|
+
// Check config
|
|
143
|
+
if (!configExists()) {
|
|
144
|
+
console.log('');
|
|
145
|
+
console.log(colors.red('Config not found.'));
|
|
146
|
+
console.log('');
|
|
147
|
+
console.log('Run ' + colors.cyan('inscribe-mcp init') + ' to configure.');
|
|
148
|
+
console.log('');
|
|
149
|
+
process.exit(EXIT_CODES.CONFIG_ERROR);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const config = readConfig();
|
|
153
|
+
const { network, operatorAccountId, defaultTopicId } = config;
|
|
154
|
+
|
|
155
|
+
// Fetch balance
|
|
156
|
+
let balance = '—';
|
|
157
|
+
try {
|
|
158
|
+
const mirrorUrl = getMirrorUrl(network);
|
|
159
|
+
const response = await fetch(`${mirrorUrl}/api/v1/accounts/${operatorAccountId}`);
|
|
160
|
+
if (response.ok) {
|
|
161
|
+
const data = await response.json();
|
|
162
|
+
const hbar = (data.balance?.balance || 0) / 100_000_000;
|
|
163
|
+
balance = hbar.toFixed(2) + ' HBAR';
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
balance = colors.red('fetch error');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Compute stats from logs
|
|
170
|
+
const logsDir = getLogsDir();
|
|
171
|
+
const stats = computeStats(logsDir);
|
|
172
|
+
|
|
173
|
+
if (options.json) {
|
|
174
|
+
console.log(JSON.stringify({
|
|
175
|
+
config: {
|
|
176
|
+
network,
|
|
177
|
+
operatorAccountId,
|
|
178
|
+
defaultTopicId,
|
|
179
|
+
operatorPrivateKey: maskPrivateKey(config.operatorPrivateKey)
|
|
180
|
+
},
|
|
181
|
+
balance,
|
|
182
|
+
stats
|
|
183
|
+
}, null, 2));
|
|
184
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Dashboard display
|
|
188
|
+
const width = 60;
|
|
189
|
+
const line = '─'.repeat(width);
|
|
190
|
+
const doubleLine = '═'.repeat(width);
|
|
191
|
+
|
|
192
|
+
console.log('');
|
|
193
|
+
console.log(colors.bold(`inscribe-mcp status (network: ${colors.cyan(network)})`));
|
|
194
|
+
console.log(doubleLine);
|
|
195
|
+
|
|
196
|
+
// Account info
|
|
197
|
+
console.log(`Operator: ${operatorAccountId}`);
|
|
198
|
+
console.log(`Balance: ${colors.green(balance)}`);
|
|
199
|
+
if (defaultTopicId) {
|
|
200
|
+
console.log(`Topic: ${defaultTopicId}`);
|
|
201
|
+
} else {
|
|
202
|
+
console.log(`Topic: ${colors.dim('(not configured)')}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log('');
|
|
206
|
+
console.log(colors.bold('MCP Activity (total)'));
|
|
207
|
+
console.log(line);
|
|
208
|
+
|
|
209
|
+
// Command stats
|
|
210
|
+
const inscribeStats = stats.byCommand['inscribe'] || { success: 0, error: 0 };
|
|
211
|
+
const verifyStats = stats.byCommand['verify'] || { success: 0, error: 0 };
|
|
212
|
+
const historyStats = stats.byCommand['history'] || { success: 0, error: 0 };
|
|
213
|
+
|
|
214
|
+
const formatStat = (s) => `${s.success + s.error} (success ${colors.green(s.success)} / fail ${s.error > 0 ? colors.red(s.error) : s.error})`;
|
|
215
|
+
|
|
216
|
+
console.log(`Inscribe calls: ${formatStat(inscribeStats)}`);
|
|
217
|
+
console.log(`Verify calls: ${formatStat(verifyStats)}`);
|
|
218
|
+
console.log(`History calls: ${formatStat(historyStats)}`);
|
|
219
|
+
console.log(`Avg latency: ${stats.avgLatency || 0} ms`);
|
|
220
|
+
|
|
221
|
+
console.log('');
|
|
222
|
+
console.log(colors.bold('Timeline'));
|
|
223
|
+
console.log(line);
|
|
224
|
+
|
|
225
|
+
console.log(`Last inscribe: ${stats.lastInscribe ? formatRelativeTime(stats.lastInscribe) : colors.dim('never')}`);
|
|
226
|
+
console.log(`Last verify: ${stats.lastVerify ? formatRelativeTime(stats.lastVerify) : colors.dim('never')}`);
|
|
227
|
+
if (stats.lastError) {
|
|
228
|
+
console.log(`Last error: ${colors.red(stats.lastErrorMessage || 'unknown')} (${formatRelativeTime(stats.lastError)})`);
|
|
229
|
+
} else {
|
|
230
|
+
console.log(`Last error: ${colors.dim('none')}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log('');
|
|
234
|
+
console.log(colors.bold('Links'));
|
|
235
|
+
console.log(line);
|
|
236
|
+
|
|
237
|
+
const hashscanBase = network === 'mainnet'
|
|
238
|
+
? 'https://hashscan.io/mainnet'
|
|
239
|
+
: 'https://hashscan.io/testnet';
|
|
240
|
+
|
|
241
|
+
console.log(`Account: ${hashscanBase}/account/${operatorAccountId}`);
|
|
242
|
+
if (defaultTopicId) {
|
|
243
|
+
console.log(`Topic: ${hashscanBase}/topic/${defaultTopicId}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
console.log('');
|
|
247
|
+
|
|
248
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
249
|
+
});
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* inscribe-mcp CLI
|
|
5
|
+
*
|
|
6
|
+
* Observation and configuration layer for inscribe-mcp.
|
|
7
|
+
* Read-Only: This CLI does not send transactions to Hedera.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import { readFileSync } from 'fs';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { dirname, join } from 'path';
|
|
14
|
+
|
|
15
|
+
import { initCommand } from './commands/init.js';
|
|
16
|
+
import { configCommand } from './commands/config.js';
|
|
17
|
+
import { balanceCommand } from './commands/balance.js';
|
|
18
|
+
import { logCommand } from './commands/log.js';
|
|
19
|
+
import { showCommand } from './commands/show.js';
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
|
|
24
|
+
// Read version from package.json
|
|
25
|
+
const packageJson = JSON.parse(
|
|
26
|
+
readFileSync(join(__dirname, '../../package.json'), 'utf-8')
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const program = new Command();
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.name('inscribe-mcp')
|
|
33
|
+
.description('Observation and configuration CLI for inscribe-mcp')
|
|
34
|
+
.version(packageJson.version);
|
|
35
|
+
|
|
36
|
+
// Register commands
|
|
37
|
+
program.addCommand(initCommand);
|
|
38
|
+
program.addCommand(configCommand);
|
|
39
|
+
program.addCommand(balanceCommand);
|
|
40
|
+
program.addCommand(logCommand);
|
|
41
|
+
program.addCommand(showCommand);
|
|
42
|
+
|
|
43
|
+
// Parse and execute
|
|
44
|
+
program.parse();
|