darkmesh-node 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/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented in this file.
4
+
5
+ ## 0.1.0 - 2026-06-01
6
+
7
+ - initial public operator package for DarkMesh prototype
8
+ - add local operator account identity + persistence
9
+ - add operator-backed node registration and binding auth
10
+ - add operator CLI for status, jobs, earnings, service control, balance, ledger, and payout claims
11
+ - add foreground/background worker controls
12
+ - add internal credits and escrow-style payout claim flow
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ ![DARKSOL Banner](./assets/darksol-banner.png)
2
+
3
+ # darkmesh-node
4
+
5
+ Built by DARKSOL 🌑
6
+
7
+ [![npm version](https://img.shields.io/npm/v/darkmesh-node.svg)](https://www.npmjs.com/package/darkmesh-node)
8
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./package.json)
9
+ [![node >= 20](https://img.shields.io/badge/node-%3E%3D20-339933.svg)](https://nodejs.org)
10
+
11
+ Operator node CLI and daemon for the DarkMesh prototype network.
12
+
13
+ `darkmesh-node` gives operators a local install surface for:
14
+ - node identity generation
15
+ - operator account key generation
16
+ - coordinator registration/connect flow
17
+ - local background service control
18
+ - status, jobs, earnings, balance, ledger, and payout-claim commands
19
+
20
+ ## Status
21
+
22
+ This package is a **working prototype operator surface**.
23
+
24
+ What is real today:
25
+ - local operator + node identity persistence
26
+ - operator-backed node registration
27
+ - outbound node connectivity to a coordinator
28
+ - routed text-job handling
29
+ - internal credits / ledger / payout-claim flow
30
+ - foreground + background service controls
31
+
32
+ What is not real yet:
33
+ - production coordinator hosting
34
+ - wallet settlement
35
+ - payout approval backend
36
+ - full operator dashboard/admin UX
37
+ - final verification/audit pipeline
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ npm install -g darkmesh-node
43
+ ```
44
+
45
+ ## Quick Start
46
+
47
+ ```bash
48
+ darkmesh init \
49
+ --coordinator-url http://127.0.0.1:8787 \
50
+ --operator-id meta \
51
+ --label my-node \
52
+ --provider memory
53
+
54
+ # register / connect the node
55
+ darkmesh connect
56
+
57
+ # inspect local/operator/node state
58
+ darkmesh whoami
59
+ darkmesh status --pretty
60
+
61
+ # run foreground worker
62
+ darkmesh run
63
+ ```
64
+
65
+ ## Core commands
66
+
67
+ ```bash
68
+ darkmesh init
69
+ darkmesh whoami
70
+ darkmesh connect
71
+ darkmesh disconnect
72
+ darkmesh status --pretty
73
+ darkmesh jobs
74
+ darkmesh earnings
75
+ darkmesh balance
76
+ darkmesh ledger
77
+ darkmesh payout --amount 1 --destination wallet:demo
78
+ darkmesh service start
79
+ darkmesh service status
80
+ darkmesh service stop
81
+ ```
82
+
83
+ ## Notes
84
+
85
+ - This package is the **operator-side install surface** only.
86
+ - The coordinator / control plane is a separate service lane.
87
+ - Current economics are **internal credits only**.
88
+
89
+ Built with teeth. 🌑
Binary file
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "darkmesh-node",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Operator node CLI and daemon for the DarkMesh prototype network.",
6
+ "license": "MIT",
7
+ "author": "DARKSOL",
8
+ "type": "module",
9
+ "main": "src/index.js",
10
+ "bin": {
11
+ "darkmesh": "src/cli.js"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "README.md",
16
+ "CHANGELOG.md",
17
+ "assets"
18
+ ],
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "keywords": [
23
+ "darkmesh",
24
+ "cli",
25
+ "operator",
26
+ "inference",
27
+ "ollama",
28
+ "ai"
29
+ ],
30
+ "scripts": {
31
+ "cli": "node src/cli.js",
32
+ "dev": "node src/index.js",
33
+ "start": "node src/index.js"
34
+ }
35
+ }
package/src/cli.js ADDED
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import { spawn } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { getDarkMeshHome } from './config.js';
6
+ import { getServiceLogPath, openServiceLogFd } from './service.js';
7
+ import { initNodeClient, whoAmI, fetchNodeStatus, connectNodeClient, fetchNodeJobs, fetchNodeEarnings, fetchNodeBalance, fetchNodeLedgerEvents, fetchNodeClaims, requestNodePayout, runNodeClient, disconnectNodeClient, getServiceStatus, getServiceLogs, startNodeService, stopNodeService, restartNodeService, settleNodeServiceStopped, waitForServiceStopRequest } from './client.js';
8
+ import { saveServiceState } from './service.js';
9
+
10
+ function parseArgs(argv) {
11
+ const args = argv.slice(2);
12
+ const command = args[0] || 'help';
13
+ const options = {};
14
+
15
+ for (let i = 1; i < args.length; i += 1) {
16
+ const token = args[i];
17
+ if (!token.startsWith('--')) continue;
18
+ const key = token.slice(2);
19
+ const next = args[i + 1];
20
+ if (!next || next.startsWith('--')) {
21
+ options[key] = true;
22
+ continue;
23
+ }
24
+ options[key] = next;
25
+ i += 1;
26
+ }
27
+
28
+ return { command, options };
29
+ }
30
+
31
+ function print(value) {
32
+ process.stdout.write(`${JSON.stringify(value)}\n`);
33
+ }
34
+
35
+ function printText(value = '') {
36
+ process.stdout.write(String(value));
37
+ }
38
+
39
+ function getHome(options) {
40
+ return getDarkMeshHome(options.home);
41
+ }
42
+
43
+ const selfPath = fileURLToPath(import.meta.url);
44
+
45
+ function spawnServiceWorker(home) {
46
+ const logFd = openServiceLogFd(home);
47
+ const child = spawn(process.execPath, [selfPath, 'run', '--home', home, '--service-mode'], {
48
+ detached: true,
49
+ stdio: ['ignore', logFd, logFd],
50
+ windowsHide: true,
51
+ cwd: process.cwd(),
52
+ env: { ...process.env, DARKMESH_SERVICE_MODE: '1' }
53
+ });
54
+ child.unref();
55
+ return child.pid;
56
+ }
57
+
58
+ function formatNodeStatusPretty(status) {
59
+ const lines = [
60
+ `DarkMesh node: ${status.label || 'unnamed-node'}`,
61
+ `operator: ${status.operatorId || 'unknown'}`,
62
+ `coordinator: ${status.coordinatorUrl}`,
63
+ `registration: ${status.registrationState}`,
64
+ `connectivity: ${status.connectivity}`,
65
+ `nodeId: ${status.nodeId || 'none'}`,
66
+ `privacy: ${status.privacyClass}`,
67
+ `models: ${(status.models || []).join(', ') || 'none'}`,
68
+ `verified models: ${(status.verifiedModels || []).join(', ') || 'none'}`,
69
+ `verification: ${status.verificationStatus || 'pending'}${status.verificationReason ? ` (${status.verificationReason})` : ''}`,
70
+ `service: ${status.runtime?.serviceStatus || 'stopped'}${status.runtime?.servicePid ? ` (pid ${status.runtime.servicePid})` : ''}`,
71
+ `credits: available ${status.availableCredits || 0} | lifetime ${status.lifetimeEarnedCredits || 0}`
72
+ ];
73
+
74
+ if (status.health) lines.push(`health: ${status.health}`);
75
+ if (status.activeJobId) lines.push(`active job: ${status.activeJobId}`);
76
+ if (status.recentJobs?.length) lines.push(`recent jobs: ${status.recentJobs.length}`);
77
+ return `${lines.join('\n')}\n`;
78
+ }
79
+
80
+ async function followServiceLogs(home, options = {}) {
81
+ const logPath = getServiceLogPath(home);
82
+ const tailLines = Math.max(1, Number(options.tailLines || 50));
83
+ const pollMs = Math.max(50, Number(options.pollMs || 250));
84
+ let offset = 0;
85
+
86
+ const initial = await getServiceLogs(home, { tailLines });
87
+ if (initial.text) {
88
+ printText(`${initial.text}${initial.text.endsWith('\n') ? '' : '\n'}`);
89
+ }
90
+
91
+ try {
92
+ const raw = await fs.readFile(logPath, 'utf8');
93
+ offset = Buffer.byteLength(raw, 'utf8');
94
+ } catch (error) {
95
+ if (error?.code !== 'ENOENT') throw error;
96
+ }
97
+
98
+ let stopping = false;
99
+ const stop = () => {
100
+ stopping = true;
101
+ };
102
+
103
+ process.on('SIGINT', stop);
104
+ process.on('SIGTERM', stop);
105
+
106
+ while (!stopping) {
107
+ try {
108
+ const raw = await fs.readFile(logPath, 'utf8');
109
+ const size = Buffer.byteLength(raw, 'utf8');
110
+ if (size < offset) {
111
+ offset = 0;
112
+ }
113
+ if (size > offset) {
114
+ const next = Buffer.from(raw, 'utf8').subarray(offset).toString('utf8');
115
+ printText(next);
116
+ offset = size;
117
+ }
118
+ } catch (error) {
119
+ if (error?.code !== 'ENOENT') throw error;
120
+ }
121
+ await new Promise(resolve => setTimeout(resolve, pollMs));
122
+ }
123
+ }
124
+
125
+ async function main() {
126
+ const { command, options } = parseArgs(process.argv);
127
+ const home = getHome(options);
128
+
129
+ if (command === 'init') {
130
+ const result = await initNodeClient(home, {
131
+ coordinatorUrl: options['coordinator-url'],
132
+ operatorId: options['operator-id'],
133
+ label: options.label,
134
+ privacyClass: options['privacy-class'],
135
+ provider: {
136
+ type: options.provider,
137
+ baseUrl: options['provider-url']
138
+ },
139
+ models: options.models ? String(options.models).split(',').map(v => v.trim()).filter(Boolean) : undefined,
140
+ privacySecret: options['privacy-secret'] || undefined
141
+ });
142
+ print({ ok: true, command, ...result });
143
+ return;
144
+ }
145
+
146
+ if (command === 'whoami') {
147
+ print({ ok: true, command, ...(await whoAmI(home)) });
148
+ return;
149
+ }
150
+
151
+ if (command === 'status') {
152
+ const result = { ok: true, command, ...(await fetchNodeStatus(home)) };
153
+ if (options.pretty || options.human) {
154
+ printText(formatNodeStatusPretty(result));
155
+ return;
156
+ }
157
+ print(result);
158
+ return;
159
+ }
160
+
161
+ if (command === 'connect') {
162
+ const { info, config } = await connectNodeClient(home);
163
+ print({
164
+ ok: true,
165
+ command,
166
+ nodeId: info.nodeId,
167
+ models: info.models,
168
+ label: config.label,
169
+ operatorId: config.operatorId,
170
+ coordinatorUrl: config.coordinatorUrl,
171
+ registered: true,
172
+ connected: false,
173
+ wsPath: info.wsPath
174
+ });
175
+ return;
176
+ }
177
+
178
+ if (command === 'jobs') {
179
+ print({ ok: true, command, ...(await fetchNodeJobs(home)) });
180
+ return;
181
+ }
182
+
183
+ if (command === 'earnings') {
184
+ print({ ok: true, command, ...(await fetchNodeEarnings(home)) });
185
+ return;
186
+ }
187
+
188
+ if (command === 'balance') {
189
+ print({ ok: true, command, ...(await fetchNodeBalance(home)) });
190
+ return;
191
+ }
192
+
193
+ if (command === 'ledger') {
194
+ print({ ok: true, command, ...(await fetchNodeLedgerEvents(home)) });
195
+ return;
196
+ }
197
+
198
+ if (command === 'claims') {
199
+ print({ ok: true, command, ...(await fetchNodeClaims(home, { status: options.status })) });
200
+ return;
201
+ }
202
+
203
+ if (command === 'payout') {
204
+ const amountCredits = Number(options.amount || options.credits);
205
+ print({
206
+ ok: true,
207
+ command,
208
+ ...(await requestNodePayout(home, {
209
+ amountCredits,
210
+ destination: options.destination,
211
+ requesterId: options['requester-id']
212
+ }))
213
+ });
214
+ return;
215
+ }
216
+
217
+ if (command === 'disconnect') {
218
+ print({ ok: true, command, ...(await disconnectNodeClient(home)) });
219
+ return;
220
+ }
221
+
222
+ if (command === 'service') {
223
+ const action = options.action || options._action || process.argv[3] || 'status';
224
+
225
+ if (action === 'status') {
226
+ print({ ok: true, command, action, service: await getServiceStatus(home) });
227
+ return;
228
+ }
229
+
230
+ if (action === 'start') {
231
+ const current = await getServiceStatus(home);
232
+ if (current.running) {
233
+ print({ ok: true, command, action, alreadyRunning: true, service: current });
234
+ return;
235
+ }
236
+
237
+ const pid = spawnServiceWorker(home);
238
+ const result = await startNodeService(home, { pid, status: 'starting' });
239
+ print({
240
+ ok: true,
241
+ command,
242
+ action,
243
+ alreadyRunning: false,
244
+ pid,
245
+ logPath: getServiceLogPath(home),
246
+ service: result.service
247
+ });
248
+ return;
249
+ }
250
+
251
+ if (action === 'restart') {
252
+ const result = await restartNodeService(home, {
253
+ status: 'starting',
254
+ spawn: () => spawnServiceWorker(home)
255
+ });
256
+ print({
257
+ ok: true,
258
+ command,
259
+ action,
260
+ restarted: true,
261
+ alreadyRunning: false,
262
+ pid: result.service.pid,
263
+ logPath: getServiceLogPath(home),
264
+ service: result.service,
265
+ previouslyRunning: result.previouslyRunning
266
+ });
267
+ return;
268
+ }
269
+
270
+ if (action === 'logs') {
271
+ const tailLines = Number(options.tail || options.lines || 50);
272
+ if (options.follow || options.f) {
273
+ await followServiceLogs(home, { tailLines, pollMs: options['poll-ms'] || 250 });
274
+ return;
275
+ }
276
+ print({ ok: true, command, action, ...(await getServiceLogs(home, { tailLines })) });
277
+ return;
278
+ }
279
+
280
+ if (action === 'stop') {
281
+ print({ ok: true, command, action, ...(await stopNodeService(home)) });
282
+ return;
283
+ }
284
+
285
+ throw new Error(`service_action_unknown_${action}`);
286
+ }
287
+
288
+ if (command === 'run') {
289
+ const { node, info, config } = await runNodeClient(home);
290
+ let shuttingDown = false;
291
+ let keepAliveResolve;
292
+ const keepAlive = new Promise(resolve => {
293
+ keepAliveResolve = resolve;
294
+ });
295
+ const shutdown = async (signal = 'shutdown') => {
296
+ if (shuttingDown) return;
297
+ shuttingDown = true;
298
+ try {
299
+ await node.close();
300
+ if (process.env.DARKMESH_SERVICE_MODE === '1' || options['service-mode']) {
301
+ await settleNodeServiceStopped(home, { signal, nodeId: info.nodeId });
302
+ }
303
+ } finally {
304
+ keepAliveResolve?.();
305
+ print({ ok: true, command, nodeId: info.nodeId, signal, stopped: true });
306
+ }
307
+ };
308
+
309
+ process.on('SIGINT', () => { shutdown('SIGINT').catch(() => process.exit(1)); });
310
+ process.on('SIGTERM', () => { shutdown('SIGTERM').catch(() => process.exit(1)); });
311
+ if (process.env.DARKMESH_SERVICE_MODE === '1' || options['service-mode']) {
312
+ waitForServiceStopRequest(home, shutdown).catch(() => process.exit(1));
313
+ }
314
+
315
+ if (process.env.DARKMESH_SERVICE_MODE === '1' || options['service-mode']) {
316
+ await saveServiceState(home, {
317
+ ...(await getServiceStatus(home)),
318
+ pid: process.pid,
319
+ status: 'running',
320
+ startedAt: new Date().toISOString(),
321
+ stoppedAt: null,
322
+ lastNodeId: info.nodeId
323
+ });
324
+ }
325
+
326
+ print({
327
+ ok: true,
328
+ command,
329
+ nodeId: info.nodeId,
330
+ models: info.models,
331
+ label: config.label,
332
+ operatorId: config.operatorId,
333
+ coordinatorUrl: config.coordinatorUrl,
334
+ connected: true,
335
+ running: true
336
+ });
337
+
338
+ await keepAlive;
339
+ return;
340
+ }
341
+
342
+ print({
343
+ ok: true,
344
+ commands: ['init', 'whoami', 'status', 'connect', 'disconnect', 'jobs', 'earnings', 'balance', 'ledger', 'claims', 'payout', 'run', 'service'],
345
+ usage: {
346
+ init: 'darkmesh init [--home <path>] [--coordinator-url <url>] [--operator-id <id>] [--label <name>] [--provider <type>] [--provider-url <url>] [--models <csv>] [--privacy-class <class>] [--privacy-secret <secret>]',
347
+ whoami: 'darkmesh whoami [--home <path>]',
348
+ status: 'darkmesh status [--home <path>] [--pretty]',
349
+ connect: 'darkmesh connect [--home <path>]',
350
+ disconnect: 'darkmesh disconnect [--home <path>]',
351
+ jobs: 'darkmesh jobs [--home <path>]',
352
+ earnings: 'darkmesh earnings [--home <path>]',
353
+ balance: 'darkmesh balance [--home <path>]',
354
+ ledger: 'darkmesh ledger [--home <path>]',
355
+ claims: 'darkmesh claims [--home <path>] [--status <pending_review|approved|rejected>]',
356
+ payout: 'darkmesh payout --amount <credits> [--destination <wallet-or-handle>] [--home <path>]',
357
+ run: 'darkmesh run [--home <path>] [--service-mode]',
358
+ service: 'darkmesh service <status|start|stop|restart|logs> [--home <path>] [--tail <lines>] [--follow]'
359
+ }
360
+ });
361
+ }
362
+
363
+ main().catch(error => {
364
+ process.stderr.write(`${JSON.stringify({ ok: false, error: error.message })}\n`);
365
+ process.exit(1);
366
+ });