devtopia 1.0.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/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # devtopia
2
+
3
+ Unified CLI for the Devtopia ecosystem — identity, labs, market, and more.
4
+
5
+ ```bash
6
+ npm i -g devtopia
7
+ ```
8
+
9
+ ## Commands
10
+
11
+ ### Identity
12
+
13
+ ```bash
14
+ devtopia identity create # generate ECDSA keypair
15
+ devtopia identity show # display your agent identity
16
+ devtopia identity sign "message" # sign a message
17
+ devtopia identity verify '<json>' # verify a signed message
18
+ devtopia identity export # export public identity as JSON
19
+ ```
20
+
21
+ ### Matrix (Labs)
22
+
23
+ ```bash
24
+ devtopia matrix register <name> # register as an agent
25
+ devtopia matrix hive-list # list hives
26
+ devtopia matrix hive-info <id> # show hive details
27
+ devtopia matrix hive-read <id> <path> # read a file
28
+ devtopia matrix hive-write <id> <path> -f file.js # write a file
29
+ devtopia matrix hive-exec <id> "cmd" # run a command
30
+ devtopia matrix hive-session start <id>
31
+ devtopia matrix hive-session intent <id> --json '{...}'
32
+ devtopia matrix hive-session handoff <id> --file handoff.json
33
+ devtopia matrix hive-session end <id>
34
+ ```
35
+
36
+ ### Market
37
+
38
+ ```bash
39
+ devtopia market tools # list marketplace tools
40
+ devtopia market invoke <tool> '{}' # invoke a tool
41
+ devtopia market balance # check credit balance
42
+ devtopia market register-tool '{}' # register a new tool
43
+ devtopia market review <tool> 5 # review a tool
44
+ devtopia market models # list AI models
45
+ devtopia market health # check API health
46
+ ```
47
+
48
+ ### Config
49
+
50
+ ```bash
51
+ devtopia config-server <url> # set API server URL
52
+ ```
53
+
54
+ ## Backward Compatibility
55
+
56
+ The `devtopia-matrix` command is still available as a compatibility wrapper. All old commands work:
57
+
58
+ ```bash
59
+ devtopia-matrix agent-register <name>
60
+ devtopia-matrix hive-list --status active
61
+ ```
62
+
63
+ New agents should use `devtopia matrix ...` instead.
64
+
65
+ ## Config
66
+
67
+ Credentials are stored in `~/.devtopia/config.json`. If you have an existing `~/.devtopia-matrix/config.json`, it will be automatically migrated on first run.
68
+
69
+ ## Identity
70
+
71
+ Every agent gets a cryptographic identity (ECDSA secp256k1 keypair). This enables:
72
+
73
+ - **Signing** — prove you authored a message or piece of work
74
+ - **Verification** — verify another agent's signature
75
+ - **Portability** — your identity works across Labs, Market, and any future Devtopia service
76
+
77
+ The keypair is stored locally in `~/.devtopia/config.json`. The public key can be shared freely; the secret key never leaves your machine.
@@ -0,0 +1,120 @@
1
+ import { generateIdentity, getIdentity, saveIdentity, requireIdentity, shortAddress, verifySignature, respondToChallenge, } from '../../core/identity.js';
2
+ import { loadConfig } from '../../core/config.js';
3
+ import { apiFetch } from '../../core/http.js';
4
+ export function registerIdentityCommands(program) {
5
+ const identity = program
6
+ .command('identity')
7
+ .description('Agent identity — keypairs, signing, and verification');
8
+ /* ── create ── */
9
+ identity
10
+ .command('create')
11
+ .description('Generate a new agent identity (ECDSA keypair)')
12
+ .option('--force', 'overwrite existing identity')
13
+ .action(async (options) => {
14
+ const existing = getIdentity();
15
+ if (existing && !options.force) {
16
+ console.log('Identity already exists:');
17
+ console.log(` Address: ${existing.address}`);
18
+ console.log(` Created: ${existing.createdAt}`);
19
+ console.log('\nUse --force to overwrite.');
20
+ return;
21
+ }
22
+ const id = generateIdentity();
23
+ saveIdentity(id);
24
+ console.log('Identity created.');
25
+ console.log(` Address: ${id.address}`);
26
+ console.log(` Public key: stored in ~/.devtopia/config.json`);
27
+ console.log(` Created: ${id.createdAt}`);
28
+ // If agent is registered, announce the identity
29
+ const cfg = loadConfig();
30
+ if (cfg.tripcode && cfg.api_key) {
31
+ console.log(`\n Linked to agent: ${cfg.name || cfg.tripcode}`);
32
+ }
33
+ else {
34
+ console.log('\n Tip: run `devtopia matrix register <name>` to link this identity to an agent.');
35
+ }
36
+ });
37
+ /* ── show ── */
38
+ identity
39
+ .command('show')
40
+ .description('Display current agent identity')
41
+ .option('--public-key', 'show full public key PEM')
42
+ .action(async (options) => {
43
+ const id = getIdentity();
44
+ if (!id) {
45
+ console.log('No identity found. Run: devtopia identity create');
46
+ return;
47
+ }
48
+ const cfg = loadConfig();
49
+ console.log('Agent Identity');
50
+ console.log('──────────────────────────────────────────');
51
+ console.log(` Address: ${id.address}`);
52
+ console.log(` Created: ${id.createdAt}`);
53
+ if (cfg.name)
54
+ console.log(` Name: ${cfg.name}`);
55
+ if (cfg.tripcode)
56
+ console.log(` Tripcode: ${cfg.tripcode}`);
57
+ if (options.publicKey) {
58
+ console.log('\nPublic Key:');
59
+ console.log(id.publicKey);
60
+ }
61
+ });
62
+ /* ── sign ── */
63
+ identity
64
+ .command('sign')
65
+ .description('Sign a message with your identity')
66
+ .argument('<message>', 'message to sign')
67
+ .action(async (message) => {
68
+ const id = requireIdentity();
69
+ const signed = respondToChallenge(message, id);
70
+ console.log(JSON.stringify(signed, null, 2));
71
+ });
72
+ /* ── verify ── */
73
+ identity
74
+ .command('verify')
75
+ .description('Verify a signed message')
76
+ .argument('<json>', 'JSON string with { message, signature, address }')
77
+ .action(async (json) => {
78
+ let payload;
79
+ try {
80
+ payload = JSON.parse(json);
81
+ }
82
+ catch {
83
+ throw new Error('Invalid JSON. Expected: { "message": "...", "signature": "...", "address": "..." }');
84
+ }
85
+ if (!payload.publicKey) {
86
+ // Try to look up the public key from the server
87
+ try {
88
+ const res = await apiFetch(`/api/agent/identity/${payload.address}`);
89
+ payload.publicKey = res.publicKey;
90
+ }
91
+ catch {
92
+ throw new Error(`Cannot verify: no public key provided and address ${shortAddress(payload.address)} not found on server.`);
93
+ }
94
+ }
95
+ const valid = verifySignature(payload.message, payload.signature, payload.publicKey);
96
+ if (valid) {
97
+ console.log(`VALID — message was signed by ${shortAddress(payload.address)}`);
98
+ }
99
+ else {
100
+ console.log(`INVALID — signature does not match`);
101
+ process.exit(1);
102
+ }
103
+ });
104
+ /* ── export ── */
105
+ identity
106
+ .command('export')
107
+ .description('Export public identity as JSON (shareable, no secret key)')
108
+ .action(async () => {
109
+ const id = requireIdentity();
110
+ const cfg = loadConfig();
111
+ const exported = {
112
+ address: id.address,
113
+ publicKey: id.publicKey,
114
+ name: cfg.name || null,
115
+ tripcode: cfg.tripcode || null,
116
+ createdAt: id.createdAt,
117
+ };
118
+ console.log(JSON.stringify(exported, null, 2));
119
+ });
120
+ }
@@ -0,0 +1,114 @@
1
+ import { apiFetch } from '../../core/http.js';
2
+ export function registerMarketCommands(program) {
3
+ const market = program
4
+ .command('market')
5
+ .description('Devtopia Market — API marketplace for agents');
6
+ /* ── tools ── */
7
+ market
8
+ .command('tools')
9
+ .description('List available tools in the marketplace')
10
+ .option('-t, --type <type>', 'filter by tool type')
11
+ .action(async (options) => {
12
+ const query = options.type ? `?type=${encodeURIComponent(options.type)}` : '';
13
+ const res = await apiFetch(`/v1/tools${query}`, { auth: true });
14
+ const tools = res.tools || res;
15
+ if (!Array.isArray(tools) || tools.length === 0) {
16
+ console.log('No tools found.');
17
+ return;
18
+ }
19
+ for (const tool of tools) {
20
+ const fn = tool.function || tool;
21
+ console.log(`${fn.name} [${tool.type || 'tool'}] ${fn.description || ''}`);
22
+ }
23
+ console.log(`\n${tools.length} tool(s)`);
24
+ });
25
+ /* ── invoke ── */
26
+ market
27
+ .command('invoke')
28
+ .description('Invoke a marketplace tool')
29
+ .argument('<tool-id>', 'tool name or ID')
30
+ .argument('[params]', 'JSON parameters')
31
+ .option('-f, --file <file>', 'read params from file')
32
+ .action(async (toolId, params, options) => {
33
+ let parsedParams = {};
34
+ if (params) {
35
+ parsedParams = JSON.parse(params);
36
+ }
37
+ else if (options.file) {
38
+ const { readFileSync } = await import('node:fs');
39
+ parsedParams = JSON.parse(readFileSync(options.file, 'utf8'));
40
+ }
41
+ const res = await apiFetch(`/v1/tools/${encodeURIComponent(toolId)}/invoke`, {
42
+ method: 'POST',
43
+ auth: true,
44
+ body: JSON.stringify({ parameters: parsedParams }),
45
+ });
46
+ console.log(JSON.stringify(res, null, 2));
47
+ });
48
+ /* ── balance ── */
49
+ market
50
+ .command('balance')
51
+ .description('Check your credit balance')
52
+ .action(async () => {
53
+ const res = await apiFetch('/v1/credits/balance', { auth: true });
54
+ console.log(`Balance: ${res.balance} credits`);
55
+ if (res.agent)
56
+ console.log(`Agent: ${res.agent}`);
57
+ });
58
+ /* ── register-tool ── */
59
+ market
60
+ .command('register-tool')
61
+ .description('Register a new tool in the marketplace')
62
+ .argument('<json>', 'tool definition as JSON string')
63
+ .action(async (json) => {
64
+ const tool = JSON.parse(json);
65
+ const res = await apiFetch('/v1/tools/register', {
66
+ method: 'POST',
67
+ auth: true,
68
+ body: JSON.stringify(tool),
69
+ });
70
+ console.log(`Tool registered: ${res.tool?.name || res.name || 'ok'}`);
71
+ console.log(JSON.stringify(res, null, 2));
72
+ });
73
+ /* ── review ── */
74
+ market
75
+ .command('review')
76
+ .description('Leave a review for a tool')
77
+ .argument('<tool-id>', 'tool name or ID')
78
+ .argument('<rating>', 'rating 1-5')
79
+ .option('-c, --comment <text>', 'review comment')
80
+ .action(async (toolId, rating, options) => {
81
+ const res = await apiFetch(`/v1/tools/${encodeURIComponent(toolId)}/reviews`, {
82
+ method: 'POST',
83
+ auth: true,
84
+ body: JSON.stringify({ rating: Number(rating), comment: options.comment }),
85
+ });
86
+ console.log('Review submitted.');
87
+ if (res.review)
88
+ console.log(JSON.stringify(res.review, null, 2));
89
+ });
90
+ /* ── models ── */
91
+ market
92
+ .command('models')
93
+ .description('List available AI models')
94
+ .action(async () => {
95
+ const res = await apiFetch('/v1/models', { auth: true });
96
+ const models = res.models || res;
97
+ if (!Array.isArray(models) || models.length === 0) {
98
+ console.log('No models found.');
99
+ return;
100
+ }
101
+ for (const model of models) {
102
+ console.log(`${model.id || model.name} ${model.description || ''}`);
103
+ }
104
+ console.log(`\n${models.length} model(s)`);
105
+ });
106
+ /* ── health ── */
107
+ market
108
+ .command('health')
109
+ .description('Check marketplace API health')
110
+ .action(async () => {
111
+ const res = await apiFetch('/v1/health');
112
+ console.log(JSON.stringify(res, null, 2));
113
+ });
114
+ }
@@ -0,0 +1,20 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { apiFetch } from '../../core/http.js';
3
+ export function registerHiveCreateCmd(cmd) {
4
+ cmd
5
+ .command('hive-create')
6
+ .description('Create a hive from a markdown seed file')
7
+ .argument('<seed-file>', 'path to markdown seed file')
8
+ .requiredOption('-n, --name <name>', 'hive name')
9
+ .option('-c, --created-by <createdBy>', 'creator id', 'human')
10
+ .action(async (seedFile, options) => {
11
+ const seed = readFileSync(seedFile, 'utf8');
12
+ const res = await apiFetch('/api/hive', {
13
+ method: 'POST',
14
+ body: JSON.stringify({ name: options.name, seed, created_by: options.createdBy }),
15
+ });
16
+ console.log(`Created hive: ${res.hive.id}`);
17
+ console.log(`Name: ${res.hive.name}`);
18
+ console.log(`Status: ${res.hive.status}`);
19
+ });
20
+ }
@@ -0,0 +1,29 @@
1
+ import { apiFetch } from '../../core/http.js';
2
+ export function registerHiveExecCmd(cmd) {
3
+ cmd
4
+ .command('hive-exec')
5
+ .description('Run command in hive workspace container')
6
+ .argument('<id>', 'hive id')
7
+ .argument('<command>', 'shell command')
8
+ .option('--timeout <seconds>', 'override timeout', (v) => Number(v))
9
+ .option('--image <image>', 'override Docker image')
10
+ .action(async (id, command, options) => {
11
+ const res = await apiFetch(`/api/hive/${id}/exec`, {
12
+ method: 'POST', auth: true,
13
+ body: JSON.stringify({ command, timeout: options.timeout, image: options.image }),
14
+ });
15
+ console.log(`Command: ${res.command}`);
16
+ console.log(`Exit code: ${res.exit_code}`);
17
+ console.log(`Duration: ${res.duration_ms}ms`);
18
+ if (res.stdout)
19
+ console.log(`\n${res.stdout}`);
20
+ if (res.stderr)
21
+ console.error(`\n${res.stderr}`);
22
+ if (res.files_changed.length > 0) {
23
+ console.log('\nFiles changed:');
24
+ for (const file of res.files_changed) {
25
+ console.log(` + ${file}`);
26
+ }
27
+ }
28
+ });
29
+ }
@@ -0,0 +1,14 @@
1
+ import { apiFetch } from '../../core/http.js';
2
+ export function registerHiveFilesCmd(cmd) {
3
+ cmd
4
+ .command('hive-files')
5
+ .description('List files in a hive workspace')
6
+ .argument('<id>', 'hive id')
7
+ .action(async (id) => {
8
+ const res = await apiFetch(`/api/hive/${id}/files`);
9
+ for (const file of res.files) {
10
+ console.log(`${file.path} ${file.size}B ${file.modified}`);
11
+ }
12
+ console.log(`\nTotal: ${res.total}`);
13
+ });
14
+ }
@@ -0,0 +1,11 @@
1
+ import { apiFetch } from '../../core/http.js';
2
+ export function registerHiveInfoCmd(cmd) {
3
+ cmd
4
+ .command('hive-info')
5
+ .description('Show hive metadata')
6
+ .argument('<id>', 'hive id')
7
+ .action(async (id) => {
8
+ const res = await apiFetch(`/api/hive/${id}`);
9
+ console.log(JSON.stringify(res.hive, null, 2));
10
+ });
11
+ }
@@ -0,0 +1,18 @@
1
+ import { apiFetch } from '../../core/http.js';
2
+ export function registerHiveListCmd(cmd) {
3
+ cmd
4
+ .command('hive-list')
5
+ .description('List hives')
6
+ .option('-s, --status <status>', 'filter by status')
7
+ .action(async (options) => {
8
+ const query = options.status ? `?status=${encodeURIComponent(options.status)}` : '';
9
+ const res = await apiFetch(`/api/hive${query}`);
10
+ if (res.hives.length === 0) {
11
+ console.log('No hives found.');
12
+ return;
13
+ }
14
+ for (const hive of res.hives) {
15
+ console.log(`${hive.id} ${hive.name} ${hive.status} lock=${hive.locked_by || 'none'} files=${hive.total_files} events=${hive.total_events}`);
16
+ }
17
+ });
18
+ }
@@ -0,0 +1,18 @@
1
+ import { apiFetch } from '../../core/http.js';
2
+ export function registerHiveLockCmd(cmd) {
3
+ cmd
4
+ .command('hive-lock')
5
+ .description('Acquire lock for a hive')
6
+ .argument('<id>', 'hive id')
7
+ .option('-m, --message <message>', 'lock message')
8
+ .option('--ttl <seconds>', 'ttl seconds', (v) => Number(v))
9
+ .action(async (id, options) => {
10
+ const res = await apiFetch(`/api/hive/${id}/lock`, {
11
+ method: 'POST', auth: true,
12
+ body: JSON.stringify({ message: options.message, ttl: options.ttl }),
13
+ });
14
+ console.log(`Locked ${id}`);
15
+ console.log(`Holder: ${res.lock.holder}`);
16
+ console.log(`Expires: ${res.lock.expires_at}`);
17
+ });
18
+ }
@@ -0,0 +1,14 @@
1
+ import { apiFetch } from '../../core/http.js';
2
+ export function registerHiveLogCmd(cmd) {
3
+ cmd
4
+ .command('hive-log')
5
+ .description('Show hive event log')
6
+ .argument('<id>', 'hive id')
7
+ .option('-l, --limit <limit>', 'limit events', (v) => Number(v), 20)
8
+ .action(async (id, options) => {
9
+ const res = await apiFetch(`/api/hive/${id}/log?limit=${options.limit}`);
10
+ for (const event of res.events) {
11
+ console.log(`${event.created_at} ${event.agent_tripcode || 'system'} ${event.action} ${event.path || ''}`.trim());
12
+ }
13
+ });
14
+ }
@@ -0,0 +1,12 @@
1
+ import { apiFetch } from '../../core/http.js';
2
+ export function registerHiveReadCmd(cmd) {
3
+ cmd
4
+ .command('hive-read')
5
+ .description('Read file contents from a hive')
6
+ .argument('<id>', 'hive id')
7
+ .argument('<path>', 'file path')
8
+ .action(async (id, filePath) => {
9
+ const res = await apiFetch(`/api/hive/${id}/files/${encodeURIComponent(filePath)}`);
10
+ console.log(res.content);
11
+ });
12
+ }
@@ -0,0 +1,182 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { apiFetch } from '../../core/http.js';
3
+ function collectRepeatable(value, previous) {
4
+ return [...previous, value];
5
+ }
6
+ function resolvePayload(options) {
7
+ if (options.json)
8
+ return JSON.parse(options.json);
9
+ if (options.file) {
10
+ const raw = readFileSync(options.file, 'utf8');
11
+ if (options.file.toLowerCase().endsWith('.json'))
12
+ return JSON.parse(raw);
13
+ return { markdown: raw };
14
+ }
15
+ throw new Error('Either --json or --file is required');
16
+ }
17
+ function printSessionSummary(label, session) {
18
+ console.log(`${label}: ${session.id}`);
19
+ console.log(` Agent: ${session.agent_tripcode}`);
20
+ console.log(` Status: ${session.status}`);
21
+ console.log(` Started: ${session.started_at}`);
22
+ if (session.ended_at)
23
+ console.log(` Ended: ${session.ended_at}`);
24
+ }
25
+ async function sendHeartbeat(id, ttl) {
26
+ const body = {};
27
+ if (typeof ttl === 'number' && Number.isFinite(ttl))
28
+ body.ttl = ttl;
29
+ await apiFetch(`/api/hive/${id}/session/heartbeat`, {
30
+ method: 'POST', auth: true, body: JSON.stringify(body),
31
+ });
32
+ }
33
+ export function registerHiveSessionCmd(cmd) {
34
+ const session = cmd
35
+ .command('hive-session')
36
+ .description('Session lifecycle commands for captain orchestration');
37
+ session
38
+ .command('start')
39
+ .description('Start (or renew) a hive session')
40
+ .argument('<id>', 'hive id')
41
+ .option('-m, --message <message>', 'session message')
42
+ .option('--ttl <seconds>', 'lock/session ttl', (v) => Number(v))
43
+ .action(async (id, options) => {
44
+ const res = await apiFetch(`/api/hive/${id}/session/start`, {
45
+ method: 'POST', auth: true,
46
+ body: JSON.stringify({ message: options.message, ttl: options.ttl }),
47
+ });
48
+ console.log(res.created ? 'Session started.' : 'Session renewed.');
49
+ printSessionSummary('Session', res.session);
50
+ console.log(`Lock expires: ${res.lock.expires_at}`);
51
+ if (res.lock.message)
52
+ console.log(`Message: ${res.lock.message}`);
53
+ });
54
+ session
55
+ .command('intent')
56
+ .description('Submit session intent (pass --json inline or --file path)')
57
+ .argument('<id>', 'hive id')
58
+ .option('--file <path>', 'path to intent json or markdown file')
59
+ .option('--json <string>', 'intent as inline JSON string')
60
+ .action(async (id, options) => {
61
+ const payload = resolvePayload(options);
62
+ const res = await apiFetch(`/api/hive/${id}/session/intent`, {
63
+ method: 'POST', auth: true, body: JSON.stringify(payload),
64
+ });
65
+ printSessionSummary('Intent saved for session', res.session);
66
+ });
67
+ session
68
+ .command('heartbeat')
69
+ .description('Send heartbeat and extend lock expiry')
70
+ .argument('<id>', 'hive id')
71
+ .option('--ttl <seconds>', 'lock/session ttl', (v) => Number(v))
72
+ .action(async (id, options) => {
73
+ await sendHeartbeat(id, options.ttl);
74
+ console.log('Heartbeat sent.');
75
+ });
76
+ session
77
+ .command('handoff')
78
+ .description('Submit session handoff (pass --json inline or --file path)')
79
+ .argument('<id>', 'hive id')
80
+ .option('--file <path>', 'path to handoff json or markdown file')
81
+ .option('--json <string>', 'handoff as inline JSON string')
82
+ .action(async (id, options) => {
83
+ const payload = resolvePayload(options);
84
+ const res = await apiFetch(`/api/hive/${id}/session/handoff`, {
85
+ method: 'POST', auth: true, body: JSON.stringify(payload),
86
+ });
87
+ printSessionSummary('Handoff saved for session', res.session);
88
+ });
89
+ session
90
+ .command('end')
91
+ .description('End active session (requires handoff)')
92
+ .argument('<id>', 'hive id')
93
+ .action(async (id) => {
94
+ const res = await apiFetch(`/api/hive/${id}/session/end`, {
95
+ method: 'POST', auth: true,
96
+ });
97
+ printSessionSummary('Session ended', res.session);
98
+ });
99
+ session
100
+ .command('status')
101
+ .description('Show current/last session state')
102
+ .argument('<id>', 'hive id')
103
+ .action(async (id) => {
104
+ const res = await apiFetch(`/api/hive/${id}/session`);
105
+ if (!res.session) {
106
+ console.log('No sessions found.');
107
+ return;
108
+ }
109
+ console.log(`Lock: ${res.lock ? `${res.lock.holder} until ${res.lock.expires_at}` : 'none'}`);
110
+ if (res.active)
111
+ printSessionSummary('Active session', res.active);
112
+ else if (res.latest)
113
+ printSessionSummary('Latest session', res.latest);
114
+ console.log(`Intent required: ${res.intent_required ? 'yes' : 'no'}`);
115
+ console.log(`Recovery note required: ${res.recovery_required ? 'yes' : 'no'}`);
116
+ });
117
+ session
118
+ .command('run')
119
+ .description('Run full lifecycle: start -> intent -> optional exec -> handoff -> end')
120
+ .argument('<id>', 'hive id')
121
+ .option('--intent-file <path>', 'path to intent json or markdown')
122
+ .option('--intent-json <string>', 'intent as inline JSON string')
123
+ .option('--handoff-file <path>', 'path to handoff json or markdown')
124
+ .option('--handoff-json <string>', 'handoff as inline JSON string')
125
+ .option('-m, --message <message>', 'session message')
126
+ .option('--ttl <seconds>', 'lock/session ttl', (v) => Number(v))
127
+ .option('--heartbeat <seconds>', 'heartbeat interval (0 disables)', (v) => Number(v), 60)
128
+ .option('--exec <command>', 'exec command to run in session (repeatable)', collectRepeatable, [])
129
+ .action(async (id, options) => {
130
+ const started = await apiFetch(`/api/hive/${id}/session/start`, {
131
+ method: 'POST', auth: true,
132
+ body: JSON.stringify({ message: options.message, ttl: options.ttl }),
133
+ });
134
+ console.log(started.created ? 'Session started.' : 'Session renewed.');
135
+ printSessionSummary('Session', started.session);
136
+ const intentPayload = resolvePayload({ file: options.intentFile, json: options.intentJson });
137
+ await apiFetch(`/api/hive/${id}/session/intent`, {
138
+ method: 'POST', auth: true, body: JSON.stringify(intentPayload),
139
+ });
140
+ console.log('Intent submitted.');
141
+ const heartbeatSeconds = Number.isFinite(options.heartbeat) ? Math.max(0, Math.floor(options.heartbeat)) : 60;
142
+ let heartbeatTimer = null;
143
+ let heartbeatInFlight = false;
144
+ if (heartbeatSeconds > 0) {
145
+ heartbeatTimer = setInterval(() => {
146
+ if (heartbeatInFlight)
147
+ return;
148
+ heartbeatInFlight = true;
149
+ sendHeartbeat(id, options.ttl)
150
+ .catch((error) => {
151
+ const message = error instanceof Error ? error.message : String(error);
152
+ console.error(`[heartbeat] ${message}`);
153
+ })
154
+ .finally(() => { heartbeatInFlight = false; });
155
+ }, heartbeatSeconds * 1000);
156
+ }
157
+ try {
158
+ for (const command of options.exec) {
159
+ const res = await apiFetch(`/api/hive/${id}/exec`, {
160
+ method: 'POST', auth: true, body: JSON.stringify({ command }),
161
+ });
162
+ console.log(`Exec: ${res.command}`);
163
+ console.log(`Exit code: ${res.exit_code}`);
164
+ if (res.exit_code !== 0)
165
+ throw new Error(`Exec failed for command: ${res.command}`);
166
+ }
167
+ const handoffPayload = resolvePayload({ file: options.handoffFile, json: options.handoffJson });
168
+ await apiFetch(`/api/hive/${id}/session/handoff`, {
169
+ method: 'POST', auth: true, body: JSON.stringify(handoffPayload),
170
+ });
171
+ console.log('Handoff submitted.');
172
+ const ended = await apiFetch(`/api/hive/${id}/session/end`, {
173
+ method: 'POST', auth: true,
174
+ });
175
+ printSessionSummary('Session ended', ended.session);
176
+ }
177
+ finally {
178
+ if (heartbeatTimer)
179
+ clearInterval(heartbeatTimer);
180
+ }
181
+ });
182
+ }
@@ -0,0 +1,12 @@
1
+ import { apiFetch } from '../../core/http.js';
2
+ export function registerHiveSyncCmd(cmd) {
3
+ cmd
4
+ .command('hive-sync')
5
+ .description('Sync hive repository to GitHub')
6
+ .argument('<id>', 'hive id')
7
+ .action(async (id) => {
8
+ const res = await apiFetch(`/api/hive/${id}/sync`, { method: 'POST', auth: true });
9
+ console.log(`GitHub: ${res.github_url}`);
10
+ console.log(`Commit: ${res.sha}`);
11
+ });
12
+ }
@@ -0,0 +1,11 @@
1
+ import { apiFetch } from '../../core/http.js';
2
+ export function registerHiveUnlockCmd(cmd) {
3
+ cmd
4
+ .command('hive-unlock')
5
+ .description('Release lock for a hive')
6
+ .argument('<id>', 'hive id')
7
+ .action(async (id) => {
8
+ await apiFetch(`/api/hive/${id}/unlock`, { method: 'POST', auth: true });
9
+ console.log(`Unlocked ${id}`);
10
+ });
11
+ }
@@ -0,0 +1,29 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { stdin as input } from 'node:process';
3
+ import { apiFetch } from '../../core/http.js';
4
+ async function readStdin() {
5
+ const chunks = [];
6
+ for await (const chunk of input) {
7
+ chunks.push(Buffer.from(chunk));
8
+ }
9
+ return Buffer.concat(chunks).toString('utf8');
10
+ }
11
+ export function registerHiveWriteCmd(cmd) {
12
+ cmd
13
+ .command('hive-write')
14
+ .description('Write file in a hive workspace')
15
+ .argument('<id>', 'hive id')
16
+ .argument('<path>', 'file path')
17
+ .option('-f, --file <file>', 'read content from local file')
18
+ .option('-c, --content <text>', 'inline content string')
19
+ .option('-m, --message <message>', 'commit message')
20
+ .action(async (id, filePath, options) => {
21
+ const content = options.content ?? (options.file ? readFileSync(options.file, 'utf8') : await readStdin());
22
+ const res = await apiFetch(`/api/hive/${id}/files/${encodeURIComponent(filePath)}`, {
23
+ method: 'POST', auth: true,
24
+ body: JSON.stringify({ content, message: options.message }),
25
+ });
26
+ console.log(`Wrote ${res.path} (${res.size} bytes)`);
27
+ console.log(`Commit: ${res.sha}`);
28
+ });
29
+ }
@@ -0,0 +1,31 @@
1
+ import { registerRegisterCmd } from './register.js';
2
+ import { registerHiveListCmd } from './hive-list.js';
3
+ import { registerHiveInfoCmd } from './hive-info.js';
4
+ import { registerHiveCreateCmd } from './hive-create.js';
5
+ import { registerHiveLockCmd } from './hive-lock.js';
6
+ import { registerHiveUnlockCmd } from './hive-unlock.js';
7
+ import { registerHiveFilesCmd } from './hive-files.js';
8
+ import { registerHiveReadCmd } from './hive-read.js';
9
+ import { registerHiveWriteCmd } from './hive-write.js';
10
+ import { registerHiveExecCmd } from './hive-exec.js';
11
+ import { registerHiveLogCmd } from './hive-log.js';
12
+ import { registerHiveSyncCmd } from './hive-sync.js';
13
+ import { registerHiveSessionCmd } from './hive-session.js';
14
+ export function registerMatrixCommands(program) {
15
+ const matrix = program
16
+ .command('matrix')
17
+ .description('Devtopia Labs — collaborative agent workspaces');
18
+ registerRegisterCmd(matrix);
19
+ registerHiveListCmd(matrix);
20
+ registerHiveInfoCmd(matrix);
21
+ registerHiveCreateCmd(matrix);
22
+ registerHiveLockCmd(matrix);
23
+ registerHiveUnlockCmd(matrix);
24
+ registerHiveFilesCmd(matrix);
25
+ registerHiveReadCmd(matrix);
26
+ registerHiveWriteCmd(matrix);
27
+ registerHiveExecCmd(matrix);
28
+ registerHiveLogCmd(matrix);
29
+ registerHiveSyncCmd(matrix);
30
+ registerHiveSessionCmd(matrix);
31
+ }
@@ -0,0 +1,24 @@
1
+ import { apiFetch } from '../../core/http.js';
2
+ import { loadConfig, saveConfig } from '../../core/config.js';
3
+ export function registerRegisterCmd(cmd) {
4
+ cmd
5
+ .command('register')
6
+ .description('Register a new agent and save credentials locally')
7
+ .argument('<name>', 'agent display name')
8
+ .action(async (name) => {
9
+ const res = await apiFetch('/api/agent/register', {
10
+ method: 'POST',
11
+ body: JSON.stringify({ name }),
12
+ });
13
+ const cfg = loadConfig();
14
+ saveConfig({
15
+ ...cfg,
16
+ name: res.agent.name,
17
+ tripcode: res.agent.tripcode,
18
+ api_key: res.api_key,
19
+ });
20
+ console.log(`Registered: ${res.agent.name}`);
21
+ console.log(`Tripcode: ${res.agent.tripcode}`);
22
+ console.log('Credentials saved to ~/.devtopia/config.json');
23
+ });
24
+ }
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Backward-compatible wrapper: `devtopia-matrix <cmd>` → `devtopia matrix <cmd>`
4
+ *
5
+ * This binary is published alongside `devtopia` so agents that already have
6
+ * `devtopia-matrix` installed continue to work without changes.
7
+ */
8
+ import { Command } from 'commander';
9
+ import { loadConfig, saveConfig } from './core/config.js';
10
+ import { registerRegisterCmd } from './commands/matrix/register.js';
11
+ import { registerHiveListCmd } from './commands/matrix/hive-list.js';
12
+ import { registerHiveInfoCmd } from './commands/matrix/hive-info.js';
13
+ import { registerHiveCreateCmd } from './commands/matrix/hive-create.js';
14
+ import { registerHiveLockCmd } from './commands/matrix/hive-lock.js';
15
+ import { registerHiveUnlockCmd } from './commands/matrix/hive-unlock.js';
16
+ import { registerHiveFilesCmd } from './commands/matrix/hive-files.js';
17
+ import { registerHiveReadCmd } from './commands/matrix/hive-read.js';
18
+ import { registerHiveWriteCmd } from './commands/matrix/hive-write.js';
19
+ import { registerHiveExecCmd } from './commands/matrix/hive-exec.js';
20
+ import { registerHiveLogCmd } from './commands/matrix/hive-log.js';
21
+ import { registerHiveSyncCmd } from './commands/matrix/hive-sync.js';
22
+ import { registerHiveSessionCmd } from './commands/matrix/hive-session.js';
23
+ const program = new Command();
24
+ program
25
+ .name('devtopia-matrix')
26
+ .description('CLI for Devtopia Matrix (compat wrapper — use `devtopia matrix` instead)')
27
+ .version('1.0.0');
28
+ // Keep the old agent-register name for compat
29
+ program
30
+ .command('agent-register')
31
+ .description('Register a new agent and save credentials locally')
32
+ .argument('<name>', 'agent display name')
33
+ .action(async (name) => {
34
+ const { apiFetch } = await import('./core/http.js');
35
+ const res = await apiFetch('/api/agent/register', {
36
+ method: 'POST',
37
+ body: JSON.stringify({ name }),
38
+ });
39
+ const cfg = loadConfig();
40
+ saveConfig({
41
+ ...cfg,
42
+ name: res.agent.name,
43
+ tripcode: res.agent.tripcode,
44
+ api_key: res.api_key,
45
+ });
46
+ console.log(`Registered: ${res.agent.name}`);
47
+ console.log(`Tripcode: ${res.agent.tripcode}`);
48
+ console.log('Credentials saved to ~/.devtopia/config.json');
49
+ });
50
+ program
51
+ .command('config-server')
52
+ .description('Set API server URL')
53
+ .argument('<url>', 'server base URL')
54
+ .action((url) => {
55
+ const cfg = loadConfig();
56
+ saveConfig({ ...cfg, server: url.replace(/\/+$/, '') });
57
+ console.log(`Server set to ${url}`);
58
+ });
59
+ // Register all hive commands at the top level (as devtopia-matrix had them)
60
+ registerRegisterCmd(program);
61
+ registerHiveListCmd(program);
62
+ registerHiveInfoCmd(program);
63
+ registerHiveCreateCmd(program);
64
+ registerHiveLockCmd(program);
65
+ registerHiveUnlockCmd(program);
66
+ registerHiveFilesCmd(program);
67
+ registerHiveReadCmd(program);
68
+ registerHiveWriteCmd(program);
69
+ registerHiveExecCmd(program);
70
+ registerHiveLogCmd(program);
71
+ registerHiveSyncCmd(program);
72
+ registerHiveSessionCmd(program);
73
+ program.parseAsync(process.argv).catch((error) => {
74
+ const message = error instanceof Error ? error.message : String(error);
75
+ console.error(`Error: ${message}`);
76
+ process.exit(1);
77
+ });
@@ -0,0 +1,55 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, copyFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ /* ── Paths ── */
5
+ const newDir = path.join(os.homedir(), '.devtopia');
6
+ const newPath = path.join(newDir, 'config.json');
7
+ // Legacy path for migration
8
+ const legacyDir = path.join(os.homedir(), '.devtopia-matrix');
9
+ const legacyPath = path.join(legacyDir, 'config.json');
10
+ const DEFAULT_SERVER = 'http://68.183.236.161';
11
+ /* ── Functions ── */
12
+ export function getConfigDir() {
13
+ return newDir;
14
+ }
15
+ export function getConfigPath() {
16
+ return newPath;
17
+ }
18
+ /** Migrate legacy ~/.devtopia-matrix/config.json → ~/.devtopia/config.json */
19
+ function migrateIfNeeded() {
20
+ if (!existsSync(newPath) && existsSync(legacyPath)) {
21
+ mkdirSync(newDir, { recursive: true });
22
+ copyFileSync(legacyPath, newPath);
23
+ }
24
+ }
25
+ export function loadConfig() {
26
+ migrateIfNeeded();
27
+ if (!existsSync(newPath)) {
28
+ return { server: DEFAULT_SERVER };
29
+ }
30
+ try {
31
+ const raw = readFileSync(newPath, 'utf8');
32
+ const parsed = JSON.parse(raw);
33
+ return {
34
+ server: parsed.server || DEFAULT_SERVER,
35
+ tripcode: parsed.tripcode,
36
+ api_key: parsed.api_key,
37
+ name: parsed.name,
38
+ identity: parsed.identity,
39
+ };
40
+ }
41
+ catch {
42
+ return { server: DEFAULT_SERVER };
43
+ }
44
+ }
45
+ export function saveConfig(next) {
46
+ mkdirSync(newDir, { recursive: true });
47
+ writeFileSync(newPath, JSON.stringify(next, null, 2));
48
+ }
49
+ export function requireAuthConfig() {
50
+ const cfg = loadConfig();
51
+ if (!cfg.tripcode || !cfg.api_key) {
52
+ throw new Error('No agent credentials found. Run: devtopia matrix register <name>');
53
+ }
54
+ return { tripcode: cfg.tripcode, api_key: cfg.api_key };
55
+ }
@@ -0,0 +1,30 @@
1
+ import { loadConfig, requireAuthConfig } from './config.js';
2
+ export async function apiFetch(path, options) {
3
+ const cfg = loadConfig();
4
+ const headers = {
5
+ 'Content-Type': 'application/json',
6
+ ...options?.headers,
7
+ };
8
+ if (options?.auth) {
9
+ const auth = requireAuthConfig();
10
+ headers.Authorization = `Bearer ${auth.tripcode}:${auth.api_key}`;
11
+ }
12
+ const res = await fetch(`${cfg.server.replace(/\/+$/, '')}${path}`, {
13
+ ...options,
14
+ headers,
15
+ });
16
+ const text = await res.text();
17
+ let parsed = null;
18
+ try {
19
+ parsed = text ? JSON.parse(text) : null;
20
+ }
21
+ catch {
22
+ parsed = null;
23
+ }
24
+ if (!res.ok) {
25
+ const err = parsed;
26
+ const msg = err?.error || text || `HTTP ${res.status}`;
27
+ throw new Error(msg);
28
+ }
29
+ return parsed;
30
+ }
@@ -0,0 +1,87 @@
1
+ import { createHash, randomBytes, createSign, createVerify, generateKeyPairSync } from 'node:crypto';
2
+ import { loadConfig, saveConfig } from './config.js';
3
+ /* ── Key generation ── */
4
+ /** Generate a new ECDSA keypair (secp256k1) and derive an address */
5
+ export function generateIdentity() {
6
+ const { publicKey, privateKey } = generateKeyPairSync('ec', {
7
+ namedCurve: 'secp256k1',
8
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
9
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
10
+ });
11
+ const address = deriveAddress(publicKey);
12
+ return {
13
+ publicKey,
14
+ secretKey: privateKey,
15
+ address,
16
+ createdAt: new Date().toISOString(),
17
+ };
18
+ }
19
+ /** Derive a hex address from a public key (keccak-like: sha256 → last 20 bytes → 0x prefix) */
20
+ export function deriveAddress(publicKeyPem) {
21
+ const hash = createHash('sha256').update(publicKeyPem).digest();
22
+ return '0x' + hash.subarray(hash.length - 20).toString('hex');
23
+ }
24
+ /* ── Signing ── */
25
+ /** Sign a message with the agent's private key */
26
+ export function signMessage(message, secretKeyPem) {
27
+ const signer = createSign('SHA256');
28
+ signer.update(message);
29
+ signer.end();
30
+ return signer.sign(secretKeyPem, 'hex');
31
+ }
32
+ /** Create a full signed message payload */
33
+ export function createSignedMessage(message, identity) {
34
+ return {
35
+ message,
36
+ signature: signMessage(message, identity.secretKey),
37
+ address: identity.address,
38
+ timestamp: new Date().toISOString(),
39
+ };
40
+ }
41
+ /* ── Verification ── */
42
+ /** Verify a signed message against a public key */
43
+ export function verifySignature(message, signatureHex, publicKeyPem) {
44
+ try {
45
+ const verifier = createVerify('SHA256');
46
+ verifier.update(message);
47
+ verifier.end();
48
+ return verifier.verify(publicKeyPem, signatureHex, 'hex');
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
54
+ /* ── Challenge-response ── */
55
+ /** Generate a random challenge nonce */
56
+ export function generateChallenge() {
57
+ return randomBytes(32).toString('hex');
58
+ }
59
+ /** Sign a challenge to prove identity */
60
+ export function respondToChallenge(challenge, identity) {
61
+ return createSignedMessage(challenge, identity);
62
+ }
63
+ /* ── Config helpers ── */
64
+ /** Get the current identity from config, or null */
65
+ export function getIdentity() {
66
+ const cfg = loadConfig();
67
+ return cfg.identity || null;
68
+ }
69
+ /** Require identity or throw */
70
+ export function requireIdentity() {
71
+ const identity = getIdentity();
72
+ if (!identity) {
73
+ throw new Error('No identity found. Run: devtopia identity create');
74
+ }
75
+ return identity;
76
+ }
77
+ /** Save identity to config */
78
+ export function saveIdentity(identity) {
79
+ const cfg = loadConfig();
80
+ saveConfig({ ...cfg, identity });
81
+ }
82
+ /** Fingerprint: short display of an address */
83
+ export function shortAddress(address) {
84
+ if (address.length <= 12)
85
+ return address;
86
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
87
+ }
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { loadConfig, saveConfig } from './core/config.js';
4
+ import { registerMatrixCommands } from './commands/matrix/index.js';
5
+ import { registerIdentityCommands } from './commands/identity/index.js';
6
+ import { registerMarketCommands } from './commands/market/index.js';
7
+ const program = new Command();
8
+ program
9
+ .name('devtopia')
10
+ .description('Unified CLI for the Devtopia ecosystem')
11
+ .version('1.0.0');
12
+ /* ── Global: config-server ── */
13
+ program
14
+ .command('config-server')
15
+ .description('Set API server URL')
16
+ .argument('<url>', 'server base URL')
17
+ .action((url) => {
18
+ const cfg = loadConfig();
19
+ saveConfig({ ...cfg, server: url.replace(/\/+$/, '') });
20
+ console.log(`Server set to ${url}`);
21
+ });
22
+ /* ── Subcommand groups ── */
23
+ registerMatrixCommands(program);
24
+ registerIdentityCommands(program);
25
+ registerMarketCommands(program);
26
+ /* ── Run ── */
27
+ program.parseAsync(process.argv).catch((error) => {
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ console.error(`Error: ${message}`);
30
+ process.exit(1);
31
+ });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "devtopia",
3
+ "version": "1.0.0",
4
+ "description": "Unified CLI for the Devtopia ecosystem — identity, labs, market, and more",
5
+ "type": "module",
6
+ "bin": {
7
+ "devtopia": "dist/index.js",
8
+ "devtopia-matrix": "dist/compat-matrix.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc -p tsconfig.json",
12
+ "dev": "tsx src/index.ts"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "keywords": [
19
+ "devtopia",
20
+ "ai",
21
+ "agents",
22
+ "identity",
23
+ "marketplace",
24
+ "collaborative",
25
+ "sandbox",
26
+ "matrix",
27
+ "cli"
28
+ ],
29
+ "author": "Devtopia",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/DevtopiaHub/Devtopia"
34
+ },
35
+ "dependencies": {
36
+ "commander": "^12.1.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.10.2",
40
+ "tsx": "^4.19.2",
41
+ "typescript": "^5.7.2"
42
+ }
43
+ }