habi-agent 0.1.3 → 0.1.5
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/dist/cli.d.ts +12 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +279 -184
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +8 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +127 -0
- package/dist/daemon.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +56 -1
- package/dist/server.js.map +1 -1
- package/dist/service.d.ts +17 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +267 -0
- package/dist/service.js.map +1 -0
- package/package.json +7 -6
- package/src/cli.ts +300 -195
- package/src/daemon.ts +110 -0
- package/src/server.ts +56 -1
- package/src/service.ts +223 -0
package/src/cli.ts
CHANGED
|
@@ -1,27 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* HABI Agent CLI
|
|
3
|
+
* HABI Agent CLI v0.1.5
|
|
4
|
+
*
|
|
4
5
|
* Usage:
|
|
5
|
-
* habi-agent start
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* habi-agent
|
|
6
|
+
* npx habi-agent@latest start --key <key> --name <name>
|
|
7
|
+
* Installs HABI as a persistent background service, registers the device,
|
|
8
|
+
* injects Claude Code MCP config, and exits. Terminal can be closed.
|
|
9
|
+
*
|
|
10
|
+
* habi-agent stop Stop the background service
|
|
11
|
+
* habi-agent restart Restart the background service
|
|
12
|
+
* habi-agent uninstall Remove the background service
|
|
13
|
+
* habi-agent status Show service + agent health
|
|
14
|
+
* habi-agent identity Print hardware fingerprint
|
|
15
|
+
* habi-agent logs Tail the agent log file
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import * as
|
|
17
|
-
import
|
|
18
|
-
import * as http from 'http';
|
|
19
|
-
import * as os from 'os';
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import * as http from 'http';
|
|
21
|
+
import * as os from 'os';
|
|
22
|
+
import * as readline from 'readline';
|
|
23
|
+
import { execSync, spawnSync } from 'child_process';
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
import { readHardwareIdentity } from './hardware';
|
|
26
|
+
import { registerDevice } from './cloud';
|
|
27
|
+
import {
|
|
28
|
+
saveConfig,
|
|
29
|
+
configExists,
|
|
30
|
+
installService,
|
|
31
|
+
uninstallService,
|
|
32
|
+
stopService,
|
|
33
|
+
restartService,
|
|
34
|
+
isServiceInstalled,
|
|
35
|
+
} from './service';
|
|
36
|
+
|
|
37
|
+
const AGENT_PORT = 7432;
|
|
38
|
+
const CONFIG_DIR = path.join(os.homedir(), '.habi');
|
|
39
|
+
const LOG_PATH = path.join(CONFIG_DIR, 'habi.log');
|
|
40
|
+
const HABI_VERSION = '0.1.5';
|
|
41
|
+
|
|
42
|
+
// ── Arg parsing ─────────────────────────────────────────────────────────────
|
|
43
|
+
const command = process.argv[2] || 'help';
|
|
23
44
|
|
|
24
|
-
// Parse --key and --name arguments (works on all platforms)
|
|
25
45
|
const keyArgIdx = process.argv.indexOf('--key');
|
|
26
46
|
if (keyArgIdx !== -1 && process.argv[keyArgIdx + 1]) {
|
|
27
47
|
process.env.HABI_KEY = process.argv[keyArgIdx + 1];
|
|
@@ -31,8 +51,8 @@ if (nameArgIdx !== -1 && process.argv[nameArgIdx + 1]) {
|
|
|
31
51
|
process.env.HABI_DEVICE_NAME = process.argv[nameArgIdx + 1];
|
|
32
52
|
}
|
|
33
53
|
|
|
34
|
-
// ── Utility
|
|
35
|
-
function pingAgent(p
|
|
54
|
+
// ── Utility ──────────────────────────────────────────────────────────────────
|
|
55
|
+
function pingAgent(p = '/health'): Promise<Record<string, unknown>> {
|
|
36
56
|
return new Promise((resolve, reject) => {
|
|
37
57
|
const req = http.get(
|
|
38
58
|
{ host: '127.0.0.1', port: AGENT_PORT, path: p },
|
|
@@ -41,7 +61,7 @@ function pingAgent(p: string = '/health'): Promise<Record<string, unknown>> {
|
|
|
41
61
|
res.on('data', (chunk: Buffer) => body += chunk.toString());
|
|
42
62
|
res.on('end', () => {
|
|
43
63
|
try { resolve(JSON.parse(body)); }
|
|
44
|
-
catch { reject(new Error('Invalid JSON
|
|
64
|
+
catch { reject(new Error('Invalid JSON')); }
|
|
45
65
|
});
|
|
46
66
|
}
|
|
47
67
|
);
|
|
@@ -50,225 +70,310 @@ function pingAgent(p: string = '/health'): Promise<Record<string, unknown>> {
|
|
|
50
70
|
});
|
|
51
71
|
}
|
|
52
72
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
73
|
+
function ask(question: string): Promise<string> {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
76
|
+
rl.question(question, (ans) => { rl.close(); resolve(ans.trim()); });
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function banner() {
|
|
81
|
+
console.log(`
|
|
82
|
+
┌─────────────────────────────────────────────────┐
|
|
83
|
+
│ HABI Local Identity Agent v${HABI_VERSION} │
|
|
84
|
+
│ On Spot Solutions LLC — hardware-first AI │
|
|
85
|
+
│ imhabi.com │
|
|
86
|
+
└─────────────────────────────────────────────────┘`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Claude Code detection & MCP injection ───────────────────────────────────
|
|
90
|
+
function claudeCodePaths(): string[] {
|
|
91
|
+
const home = os.homedir();
|
|
92
|
+
return [
|
|
93
|
+
path.join(home, '.claude', 'settings.json'),
|
|
94
|
+
path.join(home, 'AppData', 'Roaming', 'Claude', 'settings.json'), // Windows
|
|
95
|
+
path.join(home, 'Library', 'Application Support', 'Claude', 'settings.json'), // macOS alt
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function findClaudeSettings(): string | null {
|
|
100
|
+
return claudeCodePaths().find(p => fs.existsSync(p)) || null;
|
|
101
|
+
}
|
|
58
102
|
|
|
59
|
-
|
|
103
|
+
function isClaudeCodeInstalled(): boolean {
|
|
60
104
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
105
|
+
const r = spawnSync(os.platform() === 'win32' ? 'where' : 'which', ['claude'], { encoding: 'utf8' });
|
|
106
|
+
return r.status === 0 && r.stdout.trim().length > 0;
|
|
107
|
+
} catch { return false; }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function injectMcpConfig(habiKey: string): { injected: boolean; path: string } {
|
|
111
|
+
const settingsPath = findClaudeSettings() || claudeCodePaths()[0];
|
|
112
|
+
const settingsDir = path.dirname(settingsPath);
|
|
113
|
+
|
|
114
|
+
if (!fs.existsSync(settingsDir)) fs.mkdirSync(settingsDir, { recursive: true });
|
|
115
|
+
|
|
116
|
+
let existing: Record<string, unknown> = {};
|
|
117
|
+
if (fs.existsSync(settingsPath)) {
|
|
118
|
+
try { existing = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); }
|
|
119
|
+
catch { existing = {}; }
|
|
67
120
|
}
|
|
68
121
|
|
|
69
|
-
const
|
|
122
|
+
const mcpServers = (existing.mcpServers || {}) as Record<string, unknown>;
|
|
70
123
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
124
|
+
// Inject or update HABI entry
|
|
125
|
+
mcpServers['habi'] = {
|
|
126
|
+
type: 'url',
|
|
127
|
+
url: 'https://dokcixjitwihtbdlkbdb.supabase.co/functions/v1/habi-mcp',
|
|
128
|
+
headers: { 'x-habi-key': habiKey },
|
|
76
129
|
};
|
|
77
|
-
process.on('SIGINT', shutdown);
|
|
78
|
-
process.on('SIGTERM', shutdown);
|
|
79
130
|
|
|
131
|
+
existing.mcpServers = mcpServers;
|
|
132
|
+
fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
|
|
133
|
+
return { injected: true, path: settingsPath };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── COMMANDS ─────────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
async function cmdStart() {
|
|
139
|
+
banner();
|
|
140
|
+
|
|
141
|
+
const habiKey = process.env.HABI_KEY || '';
|
|
142
|
+
const deviceName = process.env.HABI_DEVICE_NAME || os.hostname();
|
|
143
|
+
|
|
144
|
+
if (!habiKey) {
|
|
145
|
+
console.error('\n✗ --key is required. Get your key from imhabi.com\n');
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log('\n[HABI] Reading hardware identity...');
|
|
150
|
+
const hw = readHardwareIdentity();
|
|
151
|
+
console.log(`[HABI] Hardware: ${hw.fingerprint} (${hw.type}) on ${hw.iface}`);
|
|
152
|
+
|
|
153
|
+
// ── 1. Register device ───────────────────────────────────────────────────
|
|
154
|
+
console.log(`[HABI] Registering "${deviceName}" with imhabi.com...`);
|
|
155
|
+
process.env.HABI_KEY = habiKey;
|
|
156
|
+
process.env.HABI_DEVICE_NAME = deviceName;
|
|
157
|
+
|
|
158
|
+
const cloudState = await registerDevice({
|
|
159
|
+
friendlyName: deviceName,
|
|
160
|
+
hardwareFingerprint: hw.fingerprint,
|
|
161
|
+
fingerprintType: hw.type,
|
|
162
|
+
agentVersion: HABI_VERSION,
|
|
163
|
+
osPlatform: os.platform(),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (cloudState) {
|
|
167
|
+
console.log(`[HABI] ✓ "${deviceName}" registered — visible on imhabi.com`);
|
|
168
|
+
} else {
|
|
169
|
+
console.error('[HABI] ✗ Registration failed — check your key and internet connection');
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── 2. Save config for daemon ────────────────────────────────────────────
|
|
174
|
+
saveConfig(habiKey, deviceName);
|
|
175
|
+
console.log(`[HABI] ✓ Config saved to ~/.habi/config.json`);
|
|
176
|
+
|
|
177
|
+
// ── 3. Install background service ───────────────────────────────────────
|
|
178
|
+
console.log(`[HABI] Installing background service...`);
|
|
80
179
|
try {
|
|
81
|
-
await
|
|
180
|
+
await installService();
|
|
181
|
+
console.log(`[HABI] ✓ Background service installed — runs at every login`);
|
|
182
|
+
console.log(`[HABI] ✓ You can close this terminal. HABI keeps running.`);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.error(`[HABI] ✗ Service install failed: ${err}`);
|
|
185
|
+
console.log(`[HABI] You can still run manually: npx habi-agent@latest daemon`);
|
|
186
|
+
}
|
|
82
187
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const friendlyName = process.env.HABI_DEVICE_NAME || os.hostname();
|
|
188
|
+
// ── 4. Claude Code detection ─────────────────────────────────────────────
|
|
189
|
+
console.log('\n[HABI] Checking for Claude Code...');
|
|
190
|
+
const claudeInstalled = isClaudeCodeInstalled();
|
|
87
191
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
console.log(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (cloudState) {
|
|
102
|
-
console.log(`[HABI Cloud] ✓ Device registered — ID: ${cloudState.deviceId.slice(0, 8)}...`);
|
|
103
|
-
console.log(`[HABI Cloud] ✓ Session token active until ${new Date(cloudState.sessionTokenExpiresAt).toLocaleTimeString()}\n`);
|
|
104
|
-
|
|
105
|
-
// ── Heartbeat loop every 30s ──────────────────────────────────
|
|
106
|
-
let currentToken = cloudState.sessionToken;
|
|
107
|
-
setInterval(async () => {
|
|
108
|
-
const hb = await sendHeartbeat({
|
|
109
|
-
deviceId: cloudState.deviceId,
|
|
110
|
-
sessionToken: currentToken,
|
|
111
|
-
hardwareFingerprint: hw.fingerprint,
|
|
112
|
-
});
|
|
113
|
-
if (hb) {
|
|
114
|
-
currentToken = hb.sessionToken;
|
|
115
|
-
if (hb.pendingJobs && hb.pendingJobs.length > 0) {
|
|
116
|
-
console.log(`[HABI Cloud] ${hb.pendingJobs.length} pending job(s) received from dispatch queue.`);
|
|
117
|
-
// Job execution handled in Phase 2 — log for now
|
|
118
|
-
for (const job of hb.pendingJobs) {
|
|
119
|
-
console.log(`[HABI Cloud] Job ${job.id.slice(0, 8)}: ${JSON.stringify(job.payload).slice(0, 80)}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}, 30000);
|
|
192
|
+
if (!claudeInstalled) {
|
|
193
|
+
console.log('[HABI] Claude Code not detected.');
|
|
194
|
+
const answer = await ask('[HABI] Install Claude Code now to enable MCP enforcement? (Y/n) ');
|
|
195
|
+
if (answer.toLowerCase() !== 'n') {
|
|
196
|
+
console.log('[HABI] Installing Claude Code (npm install -g @anthropic-ai/claude-code)...');
|
|
197
|
+
try {
|
|
198
|
+
execSync('npm install -g @anthropic-ai/claude-code', { stdio: 'inherit' });
|
|
199
|
+
console.log('[HABI] ✓ Claude Code installed');
|
|
200
|
+
const mcp = injectMcpConfig(habiKey);
|
|
201
|
+
console.log(`[HABI] ✓ HABI MCP server configured → ${mcp.path}`);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.error(`[HABI] ✗ Claude Code install failed: ${err}`);
|
|
204
|
+
console.log('[HABI] Install manually: npm install -g @anthropic-ai/claude-code');
|
|
124
205
|
}
|
|
206
|
+
} else {
|
|
207
|
+
console.log('[HABI] Skipping Claude Code install. MCP enforcement disabled.');
|
|
125
208
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
console.error(`\n✗ HABI agent failed to start:\n ${err}\n`);
|
|
131
|
-
process.exit(1);
|
|
209
|
+
} else {
|
|
210
|
+
console.log('[HABI] ✓ Claude Code detected');
|
|
211
|
+
const mcp = injectMcpConfig(habiKey);
|
|
212
|
+
console.log(`[HABI] ✓ HABI MCP server configured → ${mcp.path}`);
|
|
132
213
|
}
|
|
214
|
+
|
|
215
|
+
// ── 5. Done ──────────────────────────────────────────────────────────────
|
|
216
|
+
console.log(`
|
|
217
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
218
|
+
│ ✓ HABI is running as a background service │
|
|
219
|
+
│ │
|
|
220
|
+
│ Machine: ${(deviceName + ' ').padEnd(44)} │
|
|
221
|
+
│ Hardware: ${(hw.fingerprint + ' ').padEnd(44)} │
|
|
222
|
+
│ Dashboard: https://imhabi.com │
|
|
223
|
+
│ Logs: ~/.habi/habi.log │
|
|
224
|
+
│ │
|
|
225
|
+
│ Commands: │
|
|
226
|
+
│ habi-agent status — check service health │
|
|
227
|
+
│ habi-agent stop — stop the service │
|
|
228
|
+
│ habi-agent restart — restart the service │
|
|
229
|
+
│ habi-agent uninstall — remove the service │
|
|
230
|
+
└─────────────────────────────────────────────────────────────┘
|
|
231
|
+
`);
|
|
232
|
+
process.exit(0);
|
|
133
233
|
}
|
|
134
234
|
|
|
135
235
|
async function cmdStatus() {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
console.log('\n✓ HABI agent is RUNNING');
|
|
139
|
-
console.log(` Port: ${AGENT_PORT}`);
|
|
140
|
-
console.log(` Version: ${health.version || 'unknown'}`);
|
|
236
|
+
banner();
|
|
237
|
+
console.log('');
|
|
141
238
|
|
|
239
|
+
const installed = isServiceInstalled();
|
|
240
|
+
console.log(` Service: ${installed ? '✓ INSTALLED (runs at login)' : '✗ Not installed'}`);
|
|
241
|
+
|
|
242
|
+
// Check if agent is responding
|
|
243
|
+
try {
|
|
244
|
+
const health = await pingAgent('/health');
|
|
142
245
|
const identity = await pingAgent('/identity') as Record<string, unknown>;
|
|
143
246
|
const hw = identity.hardware as Record<string, unknown>;
|
|
144
|
-
|
|
247
|
+
console.log(` Agent: ✓ RUNNING on port ${AGENT_PORT}`);
|
|
248
|
+
console.log(` Version: ${health.version || HABI_VERSION}`);
|
|
145
249
|
if (hw) {
|
|
146
|
-
console.log(` Hardware:
|
|
147
|
-
console.log(` Platform:
|
|
148
|
-
}
|
|
149
|
-
if (sess) {
|
|
150
|
-
const expiresAt = new Date(sess.expiresAt as number);
|
|
151
|
-
const active = sess.active ? '✓ ACTIVE' : '✗ EXPIRED';
|
|
152
|
-
console.log(` Session: ${(sess.fingerprint as string)?.slice(0, 16)}...`);
|
|
153
|
-
console.log(` Status: ${active} (expires ${expiresAt.toLocaleTimeString()})`);
|
|
250
|
+
console.log(` Hardware: ${hw.fingerprint} (${hw.type})`);
|
|
251
|
+
console.log(` Platform: ${hw.platform} / ${hw.hostname}`);
|
|
154
252
|
}
|
|
155
|
-
console.log();
|
|
156
253
|
} catch {
|
|
157
|
-
console.log(
|
|
158
|
-
console.log(
|
|
159
|
-
process.exit(1);
|
|
254
|
+
console.log(` Agent: ✗ NOT RESPONDING on port ${AGENT_PORT}`);
|
|
255
|
+
if (installed) console.log(` (may still be starting — check ~/.habi/habi.log)`);
|
|
160
256
|
}
|
|
257
|
+
|
|
258
|
+
// Claude Code MCP check
|
|
259
|
+
const claudeSettings = findClaudeSettings();
|
|
260
|
+
if (claudeSettings) {
|
|
261
|
+
try {
|
|
262
|
+
const s = JSON.parse(fs.readFileSync(claudeSettings, 'utf8'));
|
|
263
|
+
const habiMcp = s.mcpServers?.habi;
|
|
264
|
+
console.log(` Claude MCP: ${habiMcp ? '✓ CONFIGURED' : '✗ Not configured'}`);
|
|
265
|
+
} catch {
|
|
266
|
+
console.log(` Claude MCP: ✗ Could not read settings`);
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
console.log(` Claude MCP: — Claude Code not detected`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (fs.existsSync(LOG_PATH)) {
|
|
273
|
+
console.log(`\n Log file: ${LOG_PATH}`);
|
|
274
|
+
const lines = fs.readFileSync(LOG_PATH, 'utf8').split('\n').filter(Boolean).slice(-5);
|
|
275
|
+
console.log(' Last 5 log entries:');
|
|
276
|
+
lines.forEach(l => console.log(` ${l}`));
|
|
277
|
+
}
|
|
278
|
+
console.log('');
|
|
161
279
|
}
|
|
162
280
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
281
|
+
function cmdStop() {
|
|
282
|
+
console.log('[HABI] Stopping service...');
|
|
283
|
+
stopService();
|
|
284
|
+
console.log('[HABI] ✓ Service stopped. Run: habi-agent restart to start again.');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function cmdRestart() {
|
|
288
|
+
console.log('[HABI] Restarting service...');
|
|
289
|
+
restartService();
|
|
290
|
+
console.log('[HABI] ✓ Service restarted.');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function cmdUninstall() {
|
|
294
|
+
console.log('[HABI] Uninstalling HABI background service...');
|
|
295
|
+
const answer = await ask('[HABI] This will stop HABI and remove it from startup. Continue? (y/N) ');
|
|
296
|
+
if (answer.toLowerCase() !== 'y') {
|
|
297
|
+
console.log('[HABI] Cancelled.');
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
await uninstallService();
|
|
301
|
+
|
|
302
|
+
// Also remove MCP config from Claude Code if present
|
|
303
|
+
const claudeSettings = findClaudeSettings();
|
|
304
|
+
if (claudeSettings && fs.existsSync(claudeSettings)) {
|
|
305
|
+
try {
|
|
306
|
+
const s = JSON.parse(fs.readFileSync(claudeSettings, 'utf8'));
|
|
307
|
+
if (s.mcpServers?.habi) {
|
|
308
|
+
delete s.mcpServers.habi;
|
|
309
|
+
fs.writeFileSync(claudeSettings, JSON.stringify(s, null, 2));
|
|
310
|
+
console.log('[HABI] ✓ Removed HABI from Claude Code MCP config');
|
|
311
|
+
}
|
|
312
|
+
} catch { /* ignore */ }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
console.log('[HABI] ✓ Service uninstalled');
|
|
316
|
+
console.log('[HABI] Config and logs remain at ~/.habi/ — delete manually if desired');
|
|
317
|
+
}
|
|
172
318
|
|
|
173
|
-
|
|
174
|
-
|
|
319
|
+
function cmdLogs() {
|
|
320
|
+
if (!fs.existsSync(LOG_PATH)) {
|
|
321
|
+
console.log('[HABI] No log file found at ' + LOG_PATH);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const lines = fs.readFileSync(LOG_PATH, 'utf8').split('\n').filter(Boolean);
|
|
325
|
+
const last50 = lines.slice(-50);
|
|
326
|
+
console.log(`\n── HABI Log (last ${last50.length} lines) ──────────────────────\n`);
|
|
327
|
+
last50.forEach(l => console.log(l));
|
|
328
|
+
console.log('\n── End of log ──────────────────────────────────────────────\n');
|
|
329
|
+
}
|
|
175
330
|
|
|
176
|
-
|
|
331
|
+
function cmdIdentity() {
|
|
332
|
+
try {
|
|
333
|
+
const hw = readHardwareIdentity();
|
|
334
|
+
console.log('\n┌─ HABI Hardware Identity ───────────────────────────────────┐');
|
|
177
335
|
console.log(`│ Hardware ID: ${hw.fingerprint}`);
|
|
178
336
|
console.log(`│ Type: ${hw.type}`);
|
|
179
337
|
console.log(`│ Interface: ${hw.iface}`);
|
|
180
338
|
console.log(`│ Platform: ${hw.platform}`);
|
|
181
339
|
console.log(`│ Hostname: ${hw.hostname}`);
|
|
182
|
-
console.log(`│ Context: ${ctx ? ctx.context.project : 'none loaded'}`);
|
|
183
|
-
console.log(`│ Context Hash: ${ctx ? ctx.contextHash.slice(0, 24) + '...' : 'n/a'}`);
|
|
184
|
-
console.log(`│ Session FP: ${fingerprint.slice(0, 24)}...`);
|
|
185
340
|
console.log('└────────────────────────────────────────────────────────────┘\n');
|
|
186
341
|
} catch (err) {
|
|
187
|
-
console.error(`\n✗ Could not read hardware identity
|
|
342
|
+
console.error(`\n✗ Could not read hardware identity: ${err}\n`);
|
|
188
343
|
process.exit(1);
|
|
189
344
|
}
|
|
190
345
|
}
|
|
191
346
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const contextContent = {
|
|
197
|
-
project: projectName,
|
|
198
|
-
version: '1.0.0',
|
|
199
|
-
owner: process.env.HABI_OWNER_EMAIL || process.env.USER || process.env.USERNAME || 'owner',
|
|
200
|
-
allowedMachines: [hw.fingerprint],
|
|
201
|
-
allowedOperations: {
|
|
202
|
-
paths: [`${process.cwd()}/**`],
|
|
203
|
-
cloudAccounts: [],
|
|
204
|
-
databases: [],
|
|
205
|
-
repos: [],
|
|
206
|
-
},
|
|
207
|
-
deniedOperations: [
|
|
208
|
-
'DROP TABLE',
|
|
209
|
-
'DROP DATABASE',
|
|
210
|
-
'rm -rf /',
|
|
211
|
-
'DELETE FROM audit_log',
|
|
212
|
-
'truncate audit_log',
|
|
213
|
-
],
|
|
214
|
-
};
|
|
347
|
+
function cmdHelp() {
|
|
348
|
+
console.log(`
|
|
349
|
+
HABI Agent v${HABI_VERSION} — On Spot Solutions LLC
|
|
350
|
+
Hardware-Anchored Binding Infrastructure
|
|
215
351
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
352
|
+
Usage:
|
|
353
|
+
npx habi-agent@latest start --key <key> --name <name>
|
|
354
|
+
Register device, install background service, configure Claude Code
|
|
219
355
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
fs.writeFileSync(outPath, JSON.stringify(fullContext, null, 2), 'utf8');
|
|
356
|
+
habi-agent status Show service status and agent health
|
|
357
|
+
habi-agent stop Stop the background service
|
|
358
|
+
habi-agent restart Restart the background service
|
|
359
|
+
habi-agent uninstall Remove the background service and MCP config
|
|
360
|
+
habi-agent logs Show the last 50 log entries
|
|
361
|
+
habi-agent identity Print hardware fingerprint
|
|
227
362
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
console.log(` Project: ${projectName}`);
|
|
231
|
-
console.log(` Machine: ${hw.fingerprint} (${hw.type})`);
|
|
232
|
-
console.log(` Hash: ${contextHash.slice(0, 24)}...`);
|
|
233
|
-
console.log('\n⚠ IMPORTANT: Add habi-context.json to your .gitignore');
|
|
234
|
-
console.log(' This file contains your machine identity — do not commit it.\n');
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async function cmdVerify() {
|
|
238
|
-
try {
|
|
239
|
-
const data = await pingAgent('/verify') as Record<string, unknown>;
|
|
240
|
-
if (data.verified) {
|
|
241
|
-
console.log('\n✓ Session VERIFIED');
|
|
242
|
-
console.log(` Hardware ID: ${data.hardwareId}`);
|
|
243
|
-
console.log(` Fingerprint: ${(data.fingerprint as string)?.slice(0, 24)}...`);
|
|
244
|
-
console.log(` Expires in: ${data.expiresIn}s\n`);
|
|
245
|
-
} else {
|
|
246
|
-
console.log('\n✗ Session NOT VERIFIED');
|
|
247
|
-
console.log(` Reason: ${data.reason || data.error}\n`);
|
|
248
|
-
process.exit(1);
|
|
249
|
-
}
|
|
250
|
-
} catch {
|
|
251
|
-
console.log('\n✗ HABI agent not running. Start with: npx ts-node src/cli.ts start\n');
|
|
252
|
-
process.exit(1);
|
|
253
|
-
}
|
|
363
|
+
Dashboard: https://imhabi.com
|
|
364
|
+
`);
|
|
254
365
|
}
|
|
255
366
|
|
|
256
|
-
// ── Dispatch
|
|
367
|
+
// ── Dispatch ──────────────────────────────────────────────────────────────────
|
|
257
368
|
(async () => {
|
|
258
369
|
switch (command) {
|
|
259
|
-
case 'start':
|
|
260
|
-
case '
|
|
261
|
-
case '
|
|
262
|
-
case '
|
|
263
|
-
case '
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
console.log(`\nCommands:`);
|
|
268
|
-
console.log(` start Start the local identity daemon on port 7432`);
|
|
269
|
-
console.log(` status Show agent status and hardware identity`);
|
|
270
|
-
console.log(` identity Print hardware fingerprint`);
|
|
271
|
-
console.log(` init Generate a Project Context File for this directory`);
|
|
272
|
-
console.log(` verify Verify the current session\n`);
|
|
370
|
+
case 'start': await cmdStart(); break;
|
|
371
|
+
case 'stop': cmdStop(); break;
|
|
372
|
+
case 'restart': await cmdRestart(); break;
|
|
373
|
+
case 'uninstall': await cmdUninstall(); break;
|
|
374
|
+
case 'status': await cmdStatus(); break;
|
|
375
|
+
case 'logs': cmdLogs(); break;
|
|
376
|
+
case 'identity': cmdIdentity(); break;
|
|
377
|
+
default: cmdHelp(); break;
|
|
273
378
|
}
|
|
274
379
|
})();
|