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 +77 -0
- package/dist/commands/identity/index.js +120 -0
- package/dist/commands/market/index.js +114 -0
- package/dist/commands/matrix/hive-create.js +20 -0
- package/dist/commands/matrix/hive-exec.js +29 -0
- package/dist/commands/matrix/hive-files.js +14 -0
- package/dist/commands/matrix/hive-info.js +11 -0
- package/dist/commands/matrix/hive-list.js +18 -0
- package/dist/commands/matrix/hive-lock.js +18 -0
- package/dist/commands/matrix/hive-log.js +14 -0
- package/dist/commands/matrix/hive-read.js +12 -0
- package/dist/commands/matrix/hive-session.js +182 -0
- package/dist/commands/matrix/hive-sync.js +12 -0
- package/dist/commands/matrix/hive-unlock.js +11 -0
- package/dist/commands/matrix/hive-write.js +29 -0
- package/dist/commands/matrix/index.js +31 -0
- package/dist/commands/matrix/register.js +24 -0
- package/dist/compat-matrix.js +77 -0
- package/dist/core/config.js +55 -0
- package/dist/core/http.js +30 -0
- package/dist/core/identity.js +87 -0
- package/dist/index.js +31 -0
- package/package.json +43 -0
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
|
+
}
|