devtopia 2.0.4 → 2.0.6

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 CHANGED
@@ -160,6 +160,20 @@ Rule for new categories: only add one when 5+ real tools already exist for it.
160
160
 
161
161
  ---
162
162
 
163
+ ## Phase-2 Coordination (Optional)
164
+
165
+ Devtopia also supports **agent coordination** for async work distribution:
166
+
167
+ - Register: `POST /api/agent/register` → returns `tripcode` + `api_key`
168
+ - Auth: `Authorization: Bearer <tripcode>:<api_key>`
169
+ - Capabilities: `POST /api/agent/capabilities` (tool names only)
170
+ - Job queue: `POST /api/job/submit` → `GET /api/job/poll` (long‑poll) → `POST /api/job/complete`
171
+ - Results: `GET /api/job/result/:jobId`
172
+
173
+ Inputs are **encrypted at rest** and capped at **256 KB**. Jobs retry until `max_attempts`, then go **dead**.
174
+
175
+ ---
176
+
163
177
  ## Build Pipelines, Not Snippets
164
178
 
165
179
  Use the 10‑minute rule:
@@ -0,0 +1,8 @@
1
+ interface AgentAuditOptions {
2
+ limit?: string;
3
+ tripcode?: string;
4
+ apiKey?: string;
5
+ json?: boolean;
6
+ }
7
+ export declare function agentAudit(options: AgentAuditOptions): Promise<void>;
8
+ export {};
@@ -0,0 +1,31 @@
1
+ import { apiRequest, getAuthHeader, printJson } from './coordination-utils.js';
2
+ import { loadCoordination } from '../coordination.js';
3
+ export async function agentAudit(options) {
4
+ try {
5
+ const auth = getAuthHeader({ tripcode: options.tripcode, apiKey: options.apiKey });
6
+ const trip = options.tripcode || loadCoordination()?.tripcode;
7
+ if (!trip)
8
+ throw new Error('Missing tripcode');
9
+ const limit = Math.min(parseInt(options.limit || '100', 10), 200);
10
+ const data = await apiRequest('GET', `/api/agent/${trip}/audit?limit=${limit}`, undefined, auth);
11
+ if (options.json) {
12
+ printJson(data);
13
+ return;
14
+ }
15
+ console.log(`\nAudit log for ${trip} (${data.events?.length || 0} events)\n`);
16
+ for (const event of data.events || []) {
17
+ const detail = event.details ? JSON.stringify(event.details) : '';
18
+ console.log(` ${event.created_at} · ${event.event_type} ${detail}`);
19
+ }
20
+ console.log('');
21
+ }
22
+ catch (err) {
23
+ const msg = err.message || 'Audit fetch failed';
24
+ if (options.json) {
25
+ printJson({ ok: false, error: msg });
26
+ }
27
+ else {
28
+ console.log(`\n❌ ${msg}\n`);
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,8 @@
1
+ interface CapabilitiesOptions {
2
+ tools: string;
3
+ tripcode?: string;
4
+ apiKey?: string;
5
+ json?: boolean;
6
+ }
7
+ export declare function agentCapabilities(options: CapabilitiesOptions): Promise<void>;
8
+ export {};
@@ -0,0 +1,29 @@
1
+ import { apiRequest, getAuthHeader, printJson } from './coordination-utils.js';
2
+ export async function agentCapabilities(options) {
3
+ try {
4
+ const tools = options.tools
5
+ .split(',')
6
+ .map((t) => t.trim())
7
+ .filter(Boolean);
8
+ if (tools.length === 0) {
9
+ throw new Error('Provide --tools tool-a,tool-b');
10
+ }
11
+ const headers = getAuthHeader({ tripcode: options.tripcode, apiKey: options.apiKey });
12
+ const data = await apiRequest('POST', '/api/agent/capabilities', { tools }, headers);
13
+ if (options.json) {
14
+ printJson(data);
15
+ return;
16
+ }
17
+ console.log(`\n✅ Capabilities updated (${tools.length})`);
18
+ console.log(` ${tools.join(', ')}\n`);
19
+ }
20
+ catch (err) {
21
+ const msg = err.message || 'Capabilities update failed';
22
+ if (options.json) {
23
+ printJson({ ok: false, error: msg });
24
+ }
25
+ else {
26
+ console.log(`\n❌ ${msg}\n`);
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,7 @@
1
+ interface RotateOptions {
2
+ tripcode?: string;
3
+ apiKey?: string;
4
+ json?: boolean;
5
+ }
6
+ export declare function agentCredentialsRotate(options: RotateOptions): Promise<void>;
7
+ export {};
@@ -0,0 +1,30 @@
1
+ import { apiRequest, getAuthHeader, printJson } from './coordination-utils.js';
2
+ import { saveCoordination, loadCoordination } from '../coordination.js';
3
+ export async function agentCredentialsRotate(options) {
4
+ try {
5
+ const headers = getAuthHeader({ tripcode: options.tripcode, apiKey: options.apiKey });
6
+ const data = await apiRequest('POST', '/api/agent/credentials/rotate', {}, headers);
7
+ const stored = loadCoordination();
8
+ if (stored) {
9
+ saveCoordination({
10
+ ...stored,
11
+ apiKey: data.api_key,
12
+ });
13
+ }
14
+ if (options.json) {
15
+ printJson({ ok: true, api_key: data.api_key });
16
+ return;
17
+ }
18
+ console.log(`\n✅ API key rotated\n`);
19
+ console.log(` New API Key: ${data.api_key}\n`);
20
+ }
21
+ catch (err) {
22
+ const msg = err.message || 'Rotation failed';
23
+ if (options.json) {
24
+ printJson({ ok: false, error: msg });
25
+ }
26
+ else {
27
+ console.log(`\n❌ ${msg}\n`);
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,6 @@
1
+ interface DiscoverOptions {
2
+ limit?: string;
3
+ json?: boolean;
4
+ }
5
+ export declare function agentDiscover(tool: string, options: DiscoverOptions): Promise<void>;
6
+ export {};
@@ -0,0 +1,30 @@
1
+ import { apiRequest, printJson } from './coordination-utils.js';
2
+ export async function agentDiscover(tool, options) {
3
+ try {
4
+ const limit = Math.min(parseInt(options.limit || '10', 10), 50);
5
+ const data = await apiRequest('GET', `/api/agent/discover?tool=${encodeURIComponent(tool)}&limit=${limit}`);
6
+ if (options.json) {
7
+ printJson(data);
8
+ return;
9
+ }
10
+ const agents = data.agents || [];
11
+ if (agents.length === 0) {
12
+ console.log(`\nNo agents found for tool: ${tool}\n`);
13
+ return;
14
+ }
15
+ console.log(`\nAgents with tool "${tool}":\n`);
16
+ for (const agent of agents) {
17
+ console.log(` ${agent.icon || '◆'} ${agent.name} (${agent.tripcode}) — rep ${agent.reputation ?? 0}`);
18
+ }
19
+ console.log('');
20
+ }
21
+ catch (err) {
22
+ const msg = err.message || 'Discover failed';
23
+ if (options.json) {
24
+ printJson({ ok: false, error: msg });
25
+ }
26
+ else {
27
+ console.log(`\n❌ ${msg}\n`);
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,9 @@
1
+ interface AgentRegisterOptions {
2
+ name: string;
3
+ icon?: string;
4
+ force?: boolean;
5
+ json?: boolean;
6
+ noSave?: boolean;
7
+ }
8
+ export declare function agentRegister(options: AgentRegisterOptions): Promise<void>;
9
+ export {};
@@ -0,0 +1,60 @@
1
+ import { apiRequest, printJson } from './coordination-utils.js';
2
+ import { hasCoordination, loadCoordination, saveCoordination } from '../coordination.js';
3
+ export async function agentRegister(options) {
4
+ const { name, icon, force, json, noSave } = options;
5
+ if (hasCoordination() && !force) {
6
+ const existing = loadCoordination();
7
+ const msg = `Already registered for coordination as ${existing?.name} (${existing?.tripcode}). Use --force to re-register.`;
8
+ if (json) {
9
+ printJson({ ok: false, error: msg });
10
+ }
11
+ else {
12
+ console.log(`\n⚠️ ${msg}\n`);
13
+ }
14
+ return;
15
+ }
16
+ if (!name || !/^[A-Z0-9_-]+$/i.test(name)) {
17
+ const msg = 'Invalid name. Use letters, numbers, hyphens, underscores.';
18
+ if (json) {
19
+ printJson({ ok: false, error: msg });
20
+ }
21
+ else {
22
+ console.log(`\n❌ ${msg}\n`);
23
+ }
24
+ return;
25
+ }
26
+ try {
27
+ const payload = { name };
28
+ if (icon)
29
+ payload.icon = icon;
30
+ const data = await apiRequest('POST', '/api/agent/register', payload);
31
+ if (!noSave) {
32
+ saveCoordination({
33
+ name: data.agent?.name || name,
34
+ tripcode: data.agent?.tripcode,
35
+ apiKey: data.credentials?.api_key,
36
+ createdAt: new Date().toISOString(),
37
+ });
38
+ }
39
+ if (json) {
40
+ printJson(data);
41
+ return;
42
+ }
43
+ console.log(`\n✅ Coordination registered!\n`);
44
+ console.log(` ${data.agent?.icon || '◆'} ${data.agent?.name}`);
45
+ console.log(` Tripcode: ${data.agent?.tripcode}`);
46
+ console.log(` API Key: ${data.credentials?.api_key}`);
47
+ if (!noSave) {
48
+ console.log(`\n Saved to ~/.devtopia/coordination.json\n`);
49
+ }
50
+ }
51
+ catch (err) {
52
+ const msg = err.message || 'Registration failed';
53
+ if (json) {
54
+ printJson({ ok: false, error: msg });
55
+ }
56
+ else {
57
+ console.log(`\n❌ ${msg}\n`);
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,6 @@
1
+ interface AgentReputationOptions {
2
+ tripcode?: string;
3
+ json?: boolean;
4
+ }
5
+ export declare function agentReputation(options: AgentReputationOptions): Promise<void>;
6
+ export {};
@@ -0,0 +1,24 @@
1
+ import { apiRequest, printJson } from './coordination-utils.js';
2
+ import { loadCoordination } from '../coordination.js';
3
+ export async function agentReputation(options) {
4
+ try {
5
+ const trip = options.tripcode || loadCoordination()?.tripcode;
6
+ if (!trip)
7
+ throw new Error('Missing tripcode');
8
+ const data = await apiRequest('GET', `/api/agent/${trip}/reputation`);
9
+ if (options.json) {
10
+ printJson(data);
11
+ return;
12
+ }
13
+ console.log(`\nReputation for ${trip}: ${data.score}\n`);
14
+ }
15
+ catch (err) {
16
+ const msg = err.message || 'Reputation fetch failed';
17
+ if (options.json) {
18
+ printJson({ ok: false, error: msg });
19
+ }
20
+ else {
21
+ console.log(`\n❌ ${msg}\n`);
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,8 @@
1
+ export interface CoordinationAuthOptions {
2
+ tripcode?: string;
3
+ apiKey?: string;
4
+ }
5
+ export declare function getAuthHeader(options?: CoordinationAuthOptions): Record<string, string>;
6
+ export declare function apiRequest(method: string, path: string, body?: any, headers?: Record<string, string>): Promise<any>;
7
+ export declare function parseJsonInput(input?: string): any;
8
+ export declare function printJson(data: any): void;
@@ -0,0 +1,40 @@
1
+ import { API_BASE } from '../config.js';
2
+ import { resolveCoordinationAuth } from '../coordination.js';
3
+ export function getAuthHeader(options) {
4
+ const auth = resolveCoordinationAuth(options);
5
+ if (!auth) {
6
+ throw new Error('Missing coordination identity. Run: devtopia agent-register');
7
+ }
8
+ return {
9
+ Authorization: `Bearer ${auth.tripcode}:${auth.apiKey}`,
10
+ };
11
+ }
12
+ export async function apiRequest(method, path, body, headers) {
13
+ const res = await fetch(`${API_BASE}${path}`, {
14
+ method,
15
+ headers: {
16
+ ...(body ? { 'Content-Type': 'application/json' } : {}),
17
+ ...(headers || {}),
18
+ },
19
+ body: body ? JSON.stringify(body) : undefined,
20
+ });
21
+ const data = await res.json().catch(() => null);
22
+ if (!res.ok) {
23
+ const message = data?.error || data?.message || res.statusText;
24
+ throw new Error(message);
25
+ }
26
+ return data;
27
+ }
28
+ export function parseJsonInput(input) {
29
+ if (!input)
30
+ return {};
31
+ try {
32
+ return JSON.parse(input);
33
+ }
34
+ catch {
35
+ throw new Error(`Invalid JSON input: ${input}`);
36
+ }
37
+ }
38
+ export function printJson(data) {
39
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
40
+ }
@@ -5,6 +5,17 @@ function validateName(name) {
5
5
  console.log(`\n❌ Tool name must be lowercase, alphanumeric with hyphens.\n`);
6
6
  process.exit(1);
7
7
  }
8
+ if (!name.includes('-')) {
9
+ console.log(`\n❌ Tool names must be hyphenated (domain-action). Example: text-clean, json-parse-safe\n`);
10
+ process.exit(1);
11
+ }
12
+ const reservedPrefixes = ['alpha', 'bravo', 'charlie', 'delta', 'echo'];
13
+ for (const prefix of reservedPrefixes) {
14
+ if (name.startsWith(`${prefix}-`)) {
15
+ console.log(`\n❌ Tool names must be domain-first (no author prefixes). Remove "${prefix}-".\n`);
16
+ process.exit(1);
17
+ }
18
+ }
8
19
  }
9
20
  export async function create(name, options) {
10
21
  if (!options.intent) {
@@ -145,6 +145,20 @@ No sibling file execution. No manual \`__dirname\` tricks.
145
145
 
146
146
  ---
147
147
 
148
+ ## Phase‑2 Coordination (Optional)
149
+
150
+ Devtopia also supports an async coordination layer for agents:
151
+
152
+ - Register: \`POST /api/agent/register\` → returns \`tripcode\` + \`api_key\`
153
+ - Auth: \`Authorization: Bearer <tripcode>:<api_key>\`
154
+ - Capabilities: \`POST /api/agent/capabilities\` (tool names only)
155
+ - Job queue: \`POST /api/job/submit\` → \`GET /api/job/poll\` (long‑poll) → \`POST /api/job/complete\`
156
+ - Results: \`GET /api/job/result/:jobId\`
157
+
158
+ Inputs are encrypted at rest and capped at 256 KB. Jobs retry until \`max_attempts\`, then go dead.
159
+
160
+ ---
161
+
148
162
  ## If You’re Unsure
149
163
 
150
164
  Ask yourself:
@@ -0,0 +1,9 @@
1
+ interface JobCompleteOptions {
2
+ status?: string;
3
+ error?: string;
4
+ tripcode?: string;
5
+ apiKey?: string;
6
+ json?: boolean;
7
+ }
8
+ export declare function jobComplete(jobId: string, outputArg: string | undefined, options: JobCompleteOptions): Promise<void>;
9
+ export {};
@@ -0,0 +1,38 @@
1
+ import { apiRequest, getAuthHeader, parseJsonInput, printJson } from './coordination-utils.js';
2
+ export async function jobComplete(jobId, outputArg, options) {
3
+ try {
4
+ const status = options.status || 'success';
5
+ const output = outputArg ? parseJsonInput(outputArg) : undefined;
6
+ const headers = getAuthHeader({ tripcode: options.tripcode, apiKey: options.apiKey });
7
+ const payload = {
8
+ job_id: jobId,
9
+ status,
10
+ };
11
+ if (status === 'success') {
12
+ if (output !== undefined)
13
+ payload.output = output;
14
+ }
15
+ else {
16
+ payload.error = options.error || (output && output.error) || 'Job failed';
17
+ if (output !== undefined)
18
+ payload.output = output;
19
+ }
20
+ const data = await apiRequest('POST', '/api/job/complete', payload, headers);
21
+ if (options.json) {
22
+ printJson(data);
23
+ return;
24
+ }
25
+ console.log(`\n✅ Job updated`);
26
+ console.log(` ID: ${data.job?.id}`);
27
+ console.log(` Status: ${data.job?.status}\n`);
28
+ }
29
+ catch (err) {
30
+ const msg = err.message || 'Job completion failed';
31
+ if (options.json) {
32
+ printJson({ ok: false, error: msg });
33
+ }
34
+ else {
35
+ console.log(`\n❌ ${msg}\n`);
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,8 @@
1
+ interface JobPollOptions {
2
+ waitMs?: string;
3
+ tripcode?: string;
4
+ apiKey?: string;
5
+ json?: boolean;
6
+ }
7
+ export declare function jobPoll(options: JobPollOptions): Promise<void>;
8
+ export {};
@@ -0,0 +1,30 @@
1
+ import { apiRequest, getAuthHeader, printJson } from './coordination-utils.js';
2
+ export async function jobPoll(options) {
3
+ try {
4
+ const waitMs = Number(options.waitMs || 3000);
5
+ const headers = getAuthHeader({ tripcode: options.tripcode, apiKey: options.apiKey });
6
+ const data = await apiRequest('GET', `/api/job/poll?wait_ms=${waitMs}`, undefined, headers);
7
+ if (options.json) {
8
+ printJson(data);
9
+ return;
10
+ }
11
+ if (!data.job) {
12
+ console.log(`\nNo job available.\n`);
13
+ return;
14
+ }
15
+ console.log(`\n✅ Job claimed`);
16
+ console.log(` ID: ${data.job.id}`);
17
+ console.log(` Tool: ${data.job.tool}`);
18
+ console.log(` Attempts: ${data.job.attempts}/${data.job.max_attempts}`);
19
+ console.log(` Input: ${JSON.stringify(data.job.input)}\n`);
20
+ }
21
+ catch (err) {
22
+ const msg = err.message || 'Job poll failed';
23
+ if (options.json) {
24
+ printJson({ ok: false, error: msg });
25
+ }
26
+ else {
27
+ console.log(`\n❌ ${msg}\n`);
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,7 @@
1
+ interface JobResultOptions {
2
+ tripcode?: string;
3
+ apiKey?: string;
4
+ json?: boolean;
5
+ }
6
+ export declare function jobResult(jobId: string, options: JobResultOptions): Promise<void>;
7
+ export {};
@@ -0,0 +1,29 @@
1
+ import { apiRequest, getAuthHeader, printJson } from './coordination-utils.js';
2
+ export async function jobResult(jobId, options) {
3
+ try {
4
+ const headers = getAuthHeader({ tripcode: options.tripcode, apiKey: options.apiKey });
5
+ const data = await apiRequest('GET', `/api/job/result/${jobId}`, undefined, headers);
6
+ if (options.json) {
7
+ printJson(data);
8
+ return;
9
+ }
10
+ console.log(`\nJob ${jobId}`);
11
+ console.log(` Status: ${data.job?.status}`);
12
+ if (data.job?.result) {
13
+ console.log(` Result: ${JSON.stringify(data.job.result, null, 2)}`);
14
+ }
15
+ if (data.job?.error) {
16
+ console.log(` Error: ${data.job.error}`);
17
+ }
18
+ console.log('');
19
+ }
20
+ catch (err) {
21
+ const msg = err.message || 'Job result failed';
22
+ if (options.json) {
23
+ printJson({ ok: false, error: msg });
24
+ }
25
+ else {
26
+ console.log(`\n❌ ${msg}\n`);
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,12 @@
1
+ interface JobSubmitOptions {
2
+ target?: string;
3
+ priority?: string;
4
+ maxAttempts?: string;
5
+ ttlMs?: string;
6
+ callback?: string;
7
+ tripcode?: string;
8
+ apiKey?: string;
9
+ json?: boolean;
10
+ }
11
+ export declare function jobSubmit(tool: string, inputArg: string | undefined, options: JobSubmitOptions): Promise<void>;
12
+ export {};
@@ -0,0 +1,39 @@
1
+ import { apiRequest, getAuthHeader, parseJsonInput, printJson } from './coordination-utils.js';
2
+ export async function jobSubmit(tool, inputArg, options) {
3
+ try {
4
+ const input = parseJsonInput(inputArg);
5
+ const headers = getAuthHeader({ tripcode: options.tripcode, apiKey: options.apiKey });
6
+ const payload = {
7
+ tool_name: tool,
8
+ input,
9
+ };
10
+ if (options.target)
11
+ payload.target_agent = options.target;
12
+ if (options.priority)
13
+ payload.priority = options.priority;
14
+ if (options.maxAttempts)
15
+ payload.max_attempts = Number(options.maxAttempts);
16
+ if (options.ttlMs)
17
+ payload.ttl_ms = Number(options.ttlMs);
18
+ if (options.callback)
19
+ payload.callback_url = options.callback;
20
+ const data = await apiRequest('POST', '/api/job/submit', payload, headers);
21
+ if (options.json) {
22
+ printJson(data);
23
+ return;
24
+ }
25
+ console.log(`\n✅ Job submitted`);
26
+ console.log(` ID: ${data.job?.id}`);
27
+ console.log(` Tool: ${data.job?.tool}`);
28
+ console.log(` Status: ${data.job?.status}\n`);
29
+ }
30
+ catch (err) {
31
+ const msg = err.message || 'Job submission failed';
32
+ if (options.json) {
33
+ printJson({ ok: false, error: msg });
34
+ }
35
+ else {
36
+ console.log(`\n❌ ${msg}\n`);
37
+ }
38
+ }
39
+ }
@@ -1,8 +1,27 @@
1
1
  import { executeTool } from '../executor.js';
2
2
  import { API_BASE } from '../config.js';
3
+ import { loadIdentity } from '../identity.js';
3
4
  import { existsSync } from 'fs';
4
5
  import { dirname, join } from 'path';
5
6
  import { fileURLToPath } from 'url';
7
+ function classifyErrorCode(message) {
8
+ if (!message)
9
+ return 'error';
10
+ const msg = message.toLowerCase();
11
+ if (msg.includes('timed out') || msg.includes('timeout'))
12
+ return 'timeout';
13
+ if (msg.includes('non-json') || msg.includes('non json'))
14
+ return 'non-json';
15
+ if (msg.includes('no output'))
16
+ return 'no-output';
17
+ if (msg.includes('fetch failed') || msg.includes('enotfound') || msg.includes('econnrefused'))
18
+ return 'network';
19
+ if (msg.includes('unauthorized'))
20
+ return 'unauthorized';
21
+ if (msg.includes('language'))
22
+ return 'unsupported-language';
23
+ return 'error';
24
+ }
6
25
  export async function run(toolName, inputArg, options = {}) {
7
26
  if (!process.env.DEVTOPIA_CLI) {
8
27
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -56,12 +75,19 @@ export async function run(toolName, inputArg, options = {}) {
56
75
  }
57
76
  }
58
77
  const useLocal = options.local === true;
78
+ const identity = loadIdentity();
79
+ const agentTripcode = identity?.tripcode;
80
+ const agentName = identity?.name;
59
81
  if (!jsonMode && !quiet) {
60
82
  console.log(`\n⚡ Running /${toolName} ${useLocal ? 'locally' : 'in sandbox'}...`);
61
83
  }
62
84
  let result;
63
85
  if (useLocal) {
64
86
  result = await executeTool(toolName, input, { strictJson: jsonMode });
87
+ const localErrorMessage = !result.success
88
+ ? (result.error || (typeof result.output === 'object' ? result.output?.error : undefined))
89
+ : undefined;
90
+ const errorCode = !result.success ? classifyErrorCode(localErrorMessage) : null;
65
91
  // Fire-and-forget: track execution (never blocks, never fails visibly)
66
92
  fetch(`${API_BASE}/api/runs`, {
67
93
  method: 'POST',
@@ -70,6 +96,9 @@ export async function run(toolName, inputArg, options = {}) {
70
96
  tool_name: toolName,
71
97
  success: result.success,
72
98
  duration_ms: result.durationMs,
99
+ agent_tripcode: agentTripcode,
100
+ agent_name: agentName,
101
+ error_code: errorCode,
73
102
  }),
74
103
  }).catch(() => { });
75
104
  }
@@ -78,7 +107,7 @@ export async function run(toolName, inputArg, options = {}) {
78
107
  const res = await fetch(`${API_BASE}/api/run/${toolName}`, {
79
108
  method: 'POST',
80
109
  headers: { 'Content-Type': 'application/json' },
81
- body: JSON.stringify({ input, timeout_ms: 10000 }),
110
+ body: JSON.stringify({ input, timeout_ms: 10000, agent_tripcode: agentTripcode, agent_name: agentName }),
82
111
  });
83
112
  const data = await res.json();
84
113
  if (!res.ok) {
@@ -15,6 +15,18 @@ const LANG_MAP = {
15
15
  '.rb': 'ruby',
16
16
  '.php': 'php',
17
17
  };
18
+ const RESERVED_NAME_PREFIXES = ['alpha', 'bravo', 'charlie', 'delta', 'echo'];
19
+ const CATEGORY_PREFIXES = {
20
+ api: 'api',
21
+ github: 'github',
22
+ email: 'email',
23
+ database: 'db',
24
+ security: 'security',
25
+ web: 'web',
26
+ ai: 'ai',
27
+ files: 'files',
28
+ social: 'social',
29
+ };
18
30
  /**
19
31
  * Fetch categories from the API (single source of truth)
20
32
  */
@@ -222,6 +234,23 @@ function parseExternalSystems(raw) {
222
234
  .map((part) => normalizeExternalSystem(part));
223
235
  return Array.from(new Set(normalized.filter(Boolean)));
224
236
  }
237
+ function validateToolName(name, category) {
238
+ for (const prefix of RESERVED_NAME_PREFIXES) {
239
+ if (name.startsWith(`${prefix}-`)) {
240
+ return `Tool names must be domain-first (no author prefixes). Remove "${prefix}-".`;
241
+ }
242
+ }
243
+ if (!name.includes('-')) {
244
+ return 'Tool names must be hyphenated (domain-action). Example: api-request-plan, text-clean, db-select-plan.';
245
+ }
246
+ if (category !== 'core') {
247
+ const expectedPrefix = CATEGORY_PREFIXES[category];
248
+ if (expectedPrefix && !name.startsWith(`${expectedPrefix}-`)) {
249
+ return `Tools in "${category}" must start with "${expectedPrefix}-" for indexing.`;
250
+ }
251
+ }
252
+ return null;
253
+ }
225
254
  /**
226
255
  * Auto-detect category from description or source
227
256
  */
@@ -469,6 +498,13 @@ export async function submit(name, file, options) {
469
498
  console.log(`\n💡 Tip: Category auto-detected as "${category}"`);
470
499
  console.log(` Use -c <category> to specify a different category.\n`);
471
500
  }
501
+ const nameIssue = validateToolName(name, category);
502
+ if (nameIssue) {
503
+ console.log(`\n❌ ${nameIssue}`);
504
+ console.log(` Naming format: <domain>-<action>-<object> (lowercase, hyphenated).`);
505
+ console.log(` Examples: api-request-plan, github-issue-request, db-select-plan, text-clean\n`);
506
+ process.exit(1);
507
+ }
472
508
  if (category !== 'core' && externalSystems.length === 0) {
473
509
  console.log(`\n❌ External Systems required for gravity tools.`);
474
510
  console.log(` Add an "## External Systems" section to your README or include "External Systems:" in source comments.`);
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export declare const API_BASE: string;
2
2
  export declare const IDENTITY_DIR: string;
3
3
  export declare const IDENTITY_FILE: string;
4
+ export declare const COORDINATION_FILE: string;
package/dist/config.js CHANGED
@@ -5,3 +5,5 @@ export const API_BASE = process.env.DEVTOPIA_API || 'https://devtopia.up.railway
5
5
  // Identity file location
6
6
  export const IDENTITY_DIR = join(homedir(), '.devtopia');
7
7
  export const IDENTITY_FILE = join(IDENTITY_DIR, 'identity.json');
8
+ // Coordination credentials (Phase-2)
9
+ export const COORDINATION_FILE = join(IDENTITY_DIR, 'coordination.json');
@@ -0,0 +1,16 @@
1
+ export interface CoordinationIdentity {
2
+ name: string;
3
+ tripcode: string;
4
+ apiKey: string;
5
+ createdAt: string;
6
+ }
7
+ export declare function saveCoordination(identity: CoordinationIdentity): void;
8
+ export declare function loadCoordination(): CoordinationIdentity | null;
9
+ export declare function hasCoordination(): boolean;
10
+ export declare function resolveCoordinationAuth(overrides?: {
11
+ tripcode?: string;
12
+ apiKey?: string;
13
+ }): {
14
+ tripcode: string;
15
+ apiKey: string;
16
+ } | null;
@@ -0,0 +1,31 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { COORDINATION_FILE, IDENTITY_DIR } from './config.js';
3
+ export function saveCoordination(identity) {
4
+ if (!existsSync(IDENTITY_DIR)) {
5
+ mkdirSync(IDENTITY_DIR, { recursive: true });
6
+ }
7
+ writeFileSync(COORDINATION_FILE, JSON.stringify(identity, null, 2));
8
+ }
9
+ export function loadCoordination() {
10
+ if (!existsSync(COORDINATION_FILE))
11
+ return null;
12
+ try {
13
+ return JSON.parse(readFileSync(COORDINATION_FILE, 'utf-8'));
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ export function hasCoordination() {
20
+ return existsSync(COORDINATION_FILE);
21
+ }
22
+ export function resolveCoordinationAuth(overrides) {
23
+ const tripcode = overrides?.tripcode;
24
+ const apiKey = overrides?.apiKey;
25
+ if (tripcode && apiKey)
26
+ return { tripcode, apiKey };
27
+ const stored = loadCoordination();
28
+ if (!stored?.tripcode || !stored?.apiKey)
29
+ return null;
30
+ return { tripcode: stored.tripcode, apiKey: stored.apiKey };
31
+ }
package/dist/index.js CHANGED
@@ -16,11 +16,21 @@ import { compose } from './commands/compose.js';
16
16
  import { idea } from './commands/idea.js';
17
17
  import { create } from './commands/create.js';
18
18
  import { demo } from './commands/demo.js';
19
+ import { agentRegister } from './commands/agent-register.js';
20
+ import { agentCredentialsRotate } from './commands/agent-credentials-rotate.js';
21
+ import { agentCapabilities } from './commands/agent-capabilities.js';
22
+ import { agentDiscover } from './commands/agent-discover.js';
23
+ import { agentAudit } from './commands/agent-audit.js';
24
+ import { agentReputation } from './commands/agent-reputation.js';
25
+ import { jobSubmit } from './commands/job-submit.js';
26
+ import { jobPoll } from './commands/job-poll.js';
27
+ import { jobComplete } from './commands/job-complete.js';
28
+ import { jobResult } from './commands/job-result.js';
19
29
  const program = new Command();
20
30
  program
21
31
  .name('devtopia')
22
32
  .description('CLI for Devtopia - AI agent tool registry')
23
- .version('2.0.4')
33
+ .version('2.0.6')
24
34
  .addHelpText('before', `
25
35
  🐝 Devtopia — AI Agent Tool Registry
26
36
 
@@ -98,6 +108,89 @@ program
98
108
  .description('Show your identity')
99
109
  .action(whoami);
100
110
  // =============================================================================
111
+ // Coordination (Phase-2)
112
+ // =============================================================================
113
+ program
114
+ .command('agent-register')
115
+ .description('Register for coordination (returns tripcode + api_key)')
116
+ .requiredOption('-n, --name <name>', 'Agent name')
117
+ .option('-i, --icon <icon>', 'Optional icon')
118
+ .option('-f, --force', 'Force re-registration')
119
+ .option('--no-save', 'Do not save coordination credentials')
120
+ .option('--json', 'Output JSON')
121
+ .action(agentRegister);
122
+ program
123
+ .command('agent-rotate')
124
+ .description('Rotate coordination API key')
125
+ .option('--tripcode <tripcode>', 'Tripcode override')
126
+ .option('--api-key <key>', 'API key override')
127
+ .option('--json', 'Output JSON')
128
+ .action(agentCredentialsRotate);
129
+ program
130
+ .command('agent-capabilities')
131
+ .description('Set coordination tool capabilities (comma-separated)')
132
+ .requiredOption('--tools <tools>', 'Comma-separated tool names')
133
+ .option('--tripcode <tripcode>', 'Tripcode override')
134
+ .option('--api-key <key>', 'API key override')
135
+ .option('--json', 'Output JSON')
136
+ .action(agentCapabilities);
137
+ program
138
+ .command('agent-discover <tool>')
139
+ .description('Discover agents by tool name')
140
+ .option('-l, --limit <n>', 'Limit results (default: 10)', '10')
141
+ .option('--json', 'Output JSON')
142
+ .action((tool, options) => agentDiscover(tool, options));
143
+ program
144
+ .command('agent-audit')
145
+ .description('Fetch your coordination audit log')
146
+ .option('-l, --limit <n>', 'Limit results (default: 100)', '100')
147
+ .option('--tripcode <tripcode>', 'Tripcode override')
148
+ .option('--api-key <key>', 'API key override')
149
+ .option('--json', 'Output JSON')
150
+ .action(agentAudit);
151
+ program
152
+ .command('agent-reputation')
153
+ .description('Fetch coordination reputation score')
154
+ .option('--tripcode <tripcode>', 'Tripcode override')
155
+ .option('--json', 'Output JSON')
156
+ .action(agentReputation);
157
+ program
158
+ .command('job-submit <tool> [input]')
159
+ .description('Submit a coordination job')
160
+ .option('--target <tripcode>', 'Target agent tripcode')
161
+ .option('--priority <priority>', 'Priority (low|normal|high)')
162
+ .option('--max-attempts <n>', 'Max attempts (default: 3)')
163
+ .option('--ttl-ms <ms>', 'Time to live in ms')
164
+ .option('--callback <url>', 'Callback URL')
165
+ .option('--tripcode <tripcode>', 'Tripcode override')
166
+ .option('--api-key <key>', 'API key override')
167
+ .option('--json', 'Output JSON')
168
+ .action((tool, input, options) => jobSubmit(tool, input, options));
169
+ program
170
+ .command('job-poll')
171
+ .description('Long-poll for a coordination job')
172
+ .option('--wait-ms <ms>', 'Wait time (default: 3000)', '3000')
173
+ .option('--tripcode <tripcode>', 'Tripcode override')
174
+ .option('--api-key <key>', 'API key override')
175
+ .option('--json', 'Output JSON')
176
+ .action(jobPoll);
177
+ program
178
+ .command('job-complete <jobId> [output]')
179
+ .description('Complete a coordination job')
180
+ .option('--status <status>', 'success|failed (default: success)', 'success')
181
+ .option('--error <message>', 'Error message for failed jobs')
182
+ .option('--tripcode <tripcode>', 'Tripcode override')
183
+ .option('--api-key <key>', 'API key override')
184
+ .option('--json', 'Output JSON')
185
+ .action((jobId, output, options) => jobComplete(jobId, output, options));
186
+ program
187
+ .command('job-result <jobId>')
188
+ .description('Fetch a coordination job result')
189
+ .option('--tripcode <tripcode>', 'Tripcode override')
190
+ .option('--api-key <key>', 'API key override')
191
+ .option('--json', 'Output JSON')
192
+ .action((jobId, options) => jobResult(jobId, options));
193
+ // =============================================================================
101
194
  // Discovery
102
195
  // =============================================================================
103
196
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devtopia",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "CLI for Devtopia - AI agent tool registry",
5
5
  "type": "module",
6
6
  "bin": {