ghost-status 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/package.json +33 -0
- package/src/cli.js +23 -0
- package/src/config.js +53 -0
- package/src/index.js +36 -0
- package/src/sender.js +50 -0
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ghost-status",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code status line data collector - aggregates session metrics without display",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ghost-status": "src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "node src/cli.js",
|
|
14
|
+
"test": "node --test src/**/*.test.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"claude-code",
|
|
18
|
+
"statusline",
|
|
19
|
+
"metrics",
|
|
20
|
+
"data-collector",
|
|
21
|
+
"ghost-status"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": ""
|
|
26
|
+
},
|
|
27
|
+
"author": "",
|
|
28
|
+
"license": "ISC",
|
|
29
|
+
"type": "module",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createGhostStatus } from './index.js';
|
|
3
|
+
|
|
4
|
+
const ghost = createGhostStatus();
|
|
5
|
+
|
|
6
|
+
process.stdin.setEncoding('utf-8');
|
|
7
|
+
|
|
8
|
+
let buffer = '';
|
|
9
|
+
process.stdin.on('data', (chunk) => {
|
|
10
|
+
buffer += chunk;
|
|
11
|
+
const lines = buffer.split('\n');
|
|
12
|
+
buffer = lines.pop();
|
|
13
|
+
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
if (!line.trim()) continue;
|
|
16
|
+
try {
|
|
17
|
+
const states = JSON.parse(line);
|
|
18
|
+
ghost.update(states);
|
|
19
|
+
} catch {
|
|
20
|
+
process.stderr.write(`[ghost-status] invalid JSON: ${line}\n`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
package/src/config.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = resolve(homedir(), '.claude', 'ghostStatus');
|
|
6
|
+
const CONFIG_PATH = resolve(CONFIG_DIR, 'config.env');
|
|
7
|
+
|
|
8
|
+
const TEMPLATE = `# GhostStatus Configuration
|
|
9
|
+
# Host address only (API path is /api/ghostStatus)
|
|
10
|
+
GHOST_HOST_URL=http://localhost:3000
|
|
11
|
+
GHOST_AUTH_TOKEN=
|
|
12
|
+
GHOST_AUTH_TYPE=bearer
|
|
13
|
+
GHOST_HEARTBEAT_INTERVAL=5000
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
function ensureConfig() {
|
|
17
|
+
if (existsSync(CONFIG_PATH)) return;
|
|
18
|
+
|
|
19
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
20
|
+
writeFileSync(CONFIG_PATH, TEMPLATE, 'utf-8');
|
|
21
|
+
process.stderr.write(`[ghost-status] config created at ${CONFIG_PATH}\n`);
|
|
22
|
+
process.stderr.write(`[ghost-status] please edit the config and restart.\n`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function loadConfig() {
|
|
27
|
+
ensureConfig();
|
|
28
|
+
|
|
29
|
+
const raw = readFileSync(CONFIG_PATH, 'utf-8');
|
|
30
|
+
const config = {};
|
|
31
|
+
|
|
32
|
+
for (const line of raw.split('\n')) {
|
|
33
|
+
const trimmed = line.trim();
|
|
34
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
35
|
+
const eqIndex = trimmed.indexOf('=');
|
|
36
|
+
if (eqIndex === -1) continue;
|
|
37
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
38
|
+
const value = trimmed.slice(eqIndex + 1).trim();
|
|
39
|
+
config[key] = value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hostUrl = config.GHOST_HOST_URL;
|
|
43
|
+
if (!hostUrl) {
|
|
44
|
+
throw new Error('GHOST_HOST_URL is required in ~/.claude/ghostStatus/config.env');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
hostUrl,
|
|
49
|
+
authToken: config.GHOST_AUTH_TOKEN || '',
|
|
50
|
+
authType: config.GHOST_AUTH_TYPE || 'bearer',
|
|
51
|
+
heartbeatInterval: parseInt(config.GHOST_HEARTBEAT_INTERVAL, 10) || 5000,
|
|
52
|
+
};
|
|
53
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { loadConfig } from './config.js';
|
|
2
|
+
import { createSender } from './sender.js';
|
|
3
|
+
|
|
4
|
+
export function createGhostStatus() {
|
|
5
|
+
const config = loadConfig();
|
|
6
|
+
const sender = createSender(config);
|
|
7
|
+
|
|
8
|
+
let lastStates = null;
|
|
9
|
+
|
|
10
|
+
async function sendStates(states) {
|
|
11
|
+
try {
|
|
12
|
+
const result = await sender.send(states);
|
|
13
|
+
if (result.status !== 200) {
|
|
14
|
+
const msg = `[ghost-status] server error (${result.status}): ${result.data}\n`;
|
|
15
|
+
process.stdout.write(msg);
|
|
16
|
+
process.stderr.write(msg);
|
|
17
|
+
}
|
|
18
|
+
} catch (err) {
|
|
19
|
+
const msg = `[ghost-status] send failed: ${err.message}\n`;
|
|
20
|
+
process.stdout.write(msg);
|
|
21
|
+
process.stderr.write(msg);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function update(states) {
|
|
26
|
+
const json = JSON.stringify(states);
|
|
27
|
+
const changed = json !== JSON.stringify(lastStates);
|
|
28
|
+
lastStates = states;
|
|
29
|
+
|
|
30
|
+
if (changed) {
|
|
31
|
+
sendStates(states);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { update };
|
|
36
|
+
}
|
package/src/sender.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { request } from 'node:http';
|
|
2
|
+
import { request as httpsRequest } from 'node:https';
|
|
3
|
+
|
|
4
|
+
const API_PATH = '/api/ghostStatus';
|
|
5
|
+
|
|
6
|
+
export function createSender(config) {
|
|
7
|
+
const url = new URL(config.hostUrl);
|
|
8
|
+
const isHttps = url.protocol === 'https:';
|
|
9
|
+
const doRequest = isHttps ? httpsRequest : request;
|
|
10
|
+
|
|
11
|
+
function buildHeaders(body) {
|
|
12
|
+
const headers = {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
'Content-Length': Buffer.byteLength(body),
|
|
15
|
+
};
|
|
16
|
+
if (config.authToken) {
|
|
17
|
+
if (config.authType === 'bearer') {
|
|
18
|
+
headers['Authorization'] = `Bearer ${config.authToken}`;
|
|
19
|
+
} else if (config.authType === 'apikey') {
|
|
20
|
+
headers['X-API-Key'] = config.authToken;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return headers;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function send(states) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const body = JSON.stringify(states);
|
|
29
|
+
const options = {
|
|
30
|
+
hostname: url.hostname,
|
|
31
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
32
|
+
path: API_PATH,
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: buildHeaders(body),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const req = doRequest(options, (res) => {
|
|
38
|
+
let data = '';
|
|
39
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
40
|
+
res.on('end', () => resolve({ status: res.statusCode, data }));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
req.on('error', (err) => reject(err));
|
|
44
|
+
req.write(body);
|
|
45
|
+
req.end();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { send };
|
|
50
|
+
}
|