@agent-team-foundation/first-tree-hub 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{bootstrap-BU_7B03u.mjs → bootstrap-Dq_k_6ZD.mjs} +119 -19
- package/dist/cli/index.mjs +416 -28
- package/dist/{core-jjk1xFW_.mjs → core-Dt3yNBTm.mjs} +4264 -568
- package/dist/drizzle/0009_agent_runtime_m1.sql +31 -0
- package/dist/drizzle/0010_cloud_multi_tenancy.sql +34 -0
- package/dist/drizzle/0011_org_uuid_pk.sql +22 -0
- package/dist/drizzle/0012_session_level_state.sql +19 -0
- package/dist/drizzle/0013_hub_tasks.sql +38 -0
- package/dist/drizzle/0014_drop_task_fks.sql +9 -0
- package/dist/drizzle/0015_member_system.sql +34 -0
- package/dist/drizzle/0016_strange_havok.sql +25 -0
- package/dist/drizzle/0017_session_outputs_unique.sql +1 -0
- package/dist/drizzle/0018_agent_visibility.sql +13 -0
- package/dist/drizzle/meta/0012_snapshot.json +1451 -0
- package/dist/drizzle/meta/0013_snapshot.json +1771 -0
- package/dist/drizzle/meta/0014_snapshot.json +1717 -0
- package/dist/drizzle/meta/0016_snapshot.json +1917 -0
- package/dist/drizzle/meta/0018_snapshot.json +1938 -0
- package/dist/drizzle/meta/_journal.json +70 -0
- package/dist/index.mjs +3 -3
- package/dist/web/assets/index--kyp_ZHv.css +1 -0
- package/dist/web/assets/index-D7-5shxZ.js +310 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -2
- package/dist/web/assets/index-LFh6j4ki.js +0 -280
- package/dist/web/assets/index-vo2Sa6IQ.css +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { C as resetConfig, D as setConfigValue, E as serverConfigSchema, S as readConfigFile, _ as clientConfigSchema, b as initConfig, c as maskToken, d as saveAgentConfig, f as saveCredentials, g as agentConfigSchema, h as DEFAULT_HOME_DIR, i as ensureFreshAdminToken, l as resolveAgentToken, m as DEFAULT_DATA_DIR, p as DEFAULT_CONFIG_DIR, t as bootstrapToken, u as resolveServerUrl, w as resetConfigMeta, x as loadAgents, y as getConfigValue } from "../bootstrap-Dq_k_6ZD.mjs";
|
|
3
|
+
import { A as FirstTreeHubSDK, D as ClientRuntime, E as stopPostgres, M as SessionRegistry, N as cleanWorkspaces, O as createOwner, _ as checkServerConfig, a as formatCheckReport, b as checkWebSocket, c as onboardCreate, d as checkAgentConfigs, f as checkAgentTokens, g as checkNodeVersion, h as checkDocker, i as promptMissingFields, j as SdkError, l as saveOnboardState, m as checkDatabase, n as isInteractive, o as loadOnboardState, p as checkClientConfig, r as promptAddAgent, s as onboardCheck, t as startServer, u as runMigrations, v as checkServerHealth, x as printResults, y as checkServerReachable } from "../core-Dt3yNBTm.mjs";
|
|
4
4
|
import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-Y4m2zFc3.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import { confirm, input, select } from "@inquirer/prompts";
|
|
9
|
+
import { confirm, input, password, select } from "@inquirer/prompts";
|
|
10
10
|
//#region src/cli/output.ts
|
|
11
11
|
/** Write a success JSON envelope to stdout. */
|
|
12
12
|
function success(data) {
|
|
@@ -30,11 +30,15 @@ function fail(code, message, exitCode = 1) {
|
|
|
30
30
|
//#region src/commands/agent.ts
|
|
31
31
|
const DEFAULT_WORKSPACE_TTL_MS = 10080 * 60 * 1e3;
|
|
32
32
|
function resolveAgentConfig() {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
let token;
|
|
34
|
+
try {
|
|
35
|
+
token = resolveAgentToken();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
fail("MISSING_TOKEN", error instanceof Error ? error.message : String(error), 2);
|
|
38
|
+
}
|
|
35
39
|
let serverUrl;
|
|
36
40
|
try {
|
|
37
|
-
serverUrl = resolveServerUrl(process.env.
|
|
41
|
+
serverUrl = resolveServerUrl(process.env.FIRST_TREE_HUB_SERVER_URL);
|
|
38
42
|
} catch {
|
|
39
43
|
serverUrl = "http://localhost:8000";
|
|
40
44
|
}
|
|
@@ -78,6 +82,20 @@ function readStdin() {
|
|
|
78
82
|
process.stdin.on("error", reject);
|
|
79
83
|
});
|
|
80
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Resolve an agent name (or UUID) to its record via the admin agents API.
|
|
87
|
+
* Accepts either a name or a UUID; throws via fail() if not found.
|
|
88
|
+
*/
|
|
89
|
+
async function resolveAgent(serverUrl, adminToken, agentName) {
|
|
90
|
+
const res = await fetch(`${serverUrl}/api/v1/admin/agents?limit=100`, {
|
|
91
|
+
headers: { Authorization: `Bearer ${adminToken}` },
|
|
92
|
+
signal: AbortSignal.timeout(1e4)
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) fail("FETCH_ERROR", `Failed to list agents: ${res.status}`, 1);
|
|
95
|
+
const found = (await res.json()).items.find((a) => a.name === agentName || a.uuid === agentName);
|
|
96
|
+
if (!found) fail("NOT_FOUND", `Agent "${agentName}" not found`, 1);
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
81
99
|
function registerAgentCommands(program) {
|
|
82
100
|
const agent = program.command("agent").description("Agent management — config, tokens, bindings, messaging");
|
|
83
101
|
agent.command("add [name]").description("Add an agent instance").option("-t, --token <token>", "Agent token").action(async (name, options) => {
|
|
@@ -135,14 +153,46 @@ function registerAgentCommands(program) {
|
|
|
135
153
|
process.stderr.write(" No agents configured.\n");
|
|
136
154
|
return;
|
|
137
155
|
}
|
|
138
|
-
for (const [name, config] of agents) {
|
|
139
|
-
const masked = config.token.length > 8 ? `${config.token.slice(0, 6)}***${config.token.slice(-2)}` : "***";
|
|
140
|
-
process.stderr.write(` ${name.padEnd(20)} type: ${config.type.padEnd(14)} token: ${masked}\n`);
|
|
141
|
-
}
|
|
156
|
+
for (const [name, config] of agents) process.stderr.write(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)} token: ${maskToken(config.token)}\n`);
|
|
142
157
|
} catch {
|
|
143
158
|
process.stderr.write(" No agents configured.\n");
|
|
144
159
|
}
|
|
145
160
|
});
|
|
161
|
+
agent.command("create <name>").description("Create an agent on Hub and bind it locally (CLI-first)").requiredOption("--type <type>", "Agent type (human, personal_assistant, autonomous_agent)").option("--runtime <runtime>", "Runtime handler (default: claude-code)", "claude-code").option("--display-name <name>", "Display name").option("--server <url>", "Hub server URL").action(async (name, options) => {
|
|
162
|
+
try {
|
|
163
|
+
const serverUrl = resolveServerUrl(options.server);
|
|
164
|
+
const headers = {
|
|
165
|
+
Authorization: `Bearer ${await ensureFreshAdminToken()}`,
|
|
166
|
+
"Content-Type": "application/json"
|
|
167
|
+
};
|
|
168
|
+
const createBody = {
|
|
169
|
+
name,
|
|
170
|
+
type: options.type
|
|
171
|
+
};
|
|
172
|
+
if (options.displayName) createBody.displayName = options.displayName;
|
|
173
|
+
const createRes = await fetch(`${serverUrl}/api/v1/admin/agents`, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers,
|
|
176
|
+
body: JSON.stringify(createBody),
|
|
177
|
+
signal: AbortSignal.timeout(1e4)
|
|
178
|
+
});
|
|
179
|
+
if (!createRes.ok) fail("CREATE_ERROR", (await createRes.json().catch(() => ({}))).error ?? `Failed to create agent (HTTP ${createRes.status})`, 1);
|
|
180
|
+
const agent = await createRes.json();
|
|
181
|
+
process.stderr.write(` \u2713 Agent created: ${agent.name ?? agent.uuid}\n`);
|
|
182
|
+
const tokenRes = await fetch(`${serverUrl}/api/v1/admin/agents/${agent.uuid}/tokens`, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers,
|
|
185
|
+
body: JSON.stringify({ name: "default" }),
|
|
186
|
+
signal: AbortSignal.timeout(1e4)
|
|
187
|
+
});
|
|
188
|
+
if (!tokenRes.ok) fail("TOKEN_ERROR", `Failed to generate token (HTTP ${tokenRes.status})`, 1);
|
|
189
|
+
const agentDir = saveAgentConfig(name, (await tokenRes.json()).token, options.runtime);
|
|
190
|
+
process.stderr.write(` \u2713 Token saved: ${agentDir}/agent.yaml\n`);
|
|
191
|
+
process.stderr.write(` \u2713 Agent ready — running client will auto-bind\n`);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
fail("CREATE_ERROR", error instanceof Error ? error.message : String(error));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
146
196
|
agent.command("workspace").description("Manage agent workspaces").command("clean [agent-name]").description("Remove stale workspace directories (older than TTL with no active session)").option("--ttl <days>", "TTL in days", String(DEFAULT_WORKSPACE_TTL_MS / (1440 * 60 * 1e3))).action((agentName, options) => {
|
|
147
197
|
const defaultDays = DEFAULT_WORKSPACE_TTL_MS / (1440 * 60 * 1e3);
|
|
148
198
|
const ttlMs = Number.parseInt(options?.ttl ?? String(defaultDays), 10) * 24 * 60 * 60 * 1e3;
|
|
@@ -258,6 +308,214 @@ function registerAgentCommands(program) {
|
|
|
258
308
|
handleSdkError(error);
|
|
259
309
|
}
|
|
260
310
|
});
|
|
311
|
+
agent.command("status [name]").description("Show agent runtime status from Hub server").option("--server <url>", "Hub server URL").action(async (name, options) => {
|
|
312
|
+
try {
|
|
313
|
+
const serverUrl = resolveServerUrl(options?.server);
|
|
314
|
+
const response = await fetch(`${serverUrl}/api/v1/admin/agents/activity`, {
|
|
315
|
+
headers: { Authorization: `Bearer ${await ensureFreshAdminToken()}` },
|
|
316
|
+
signal: AbortSignal.timeout(1e4)
|
|
317
|
+
});
|
|
318
|
+
if (!response.ok) fail("FETCH_ERROR", `Server returned ${response.status}`, 1);
|
|
319
|
+
const data = await response.json();
|
|
320
|
+
if (name) {
|
|
321
|
+
const agent = data.agents.find((a) => a.agentId === name);
|
|
322
|
+
if (!agent) {
|
|
323
|
+
process.stderr.write(`\n Agent "${name}" is not running.\n\n`);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
process.stderr.write(`\n Agent: ${agent.agentId}\n`);
|
|
327
|
+
process.stderr.write(` Runtime: ${agent.runtimeType ?? "—"}\n`);
|
|
328
|
+
process.stderr.write(` State: ${agent.runtimeState ?? "—"}\n`);
|
|
329
|
+
if (agent.activeSessions !== null) process.stderr.write(` Sessions: ${agent.activeSessions} active / ${agent.totalSessions ?? 0} total\n`);
|
|
330
|
+
if (agent.clientId) process.stderr.write(` Client: ${agent.clientId}\n`);
|
|
331
|
+
process.stderr.write("\n");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
process.stderr.write(`\n Hub: ${serverUrl}\n\n`);
|
|
335
|
+
process.stderr.write(` Clients: ${data.clients} connected\n`);
|
|
336
|
+
process.stderr.write(` Agents: ${data.running} running / ${data.total} total\n`);
|
|
337
|
+
process.stderr.write(` Errors: ${data.byState.error} | Blocked: ${data.byState.blocked} | Working: ${data.byState.working} | Idle: ${data.byState.idle}\n\n`);
|
|
338
|
+
if (data.agents.length > 0) {
|
|
339
|
+
const header = ` ${"AGENT".padEnd(18)} ${"RUNTIME".padEnd(14)} ${"STATE".padEnd(10)} SESSIONS`;
|
|
340
|
+
process.stderr.write(`${header}\n`);
|
|
341
|
+
process.stderr.write(` ${"─".repeat(header.length - 2)}\n`);
|
|
342
|
+
for (const a of data.agents) {
|
|
343
|
+
const sessions = a.activeSessions !== null ? `${a.activeSessions}/${a.totalSessions ?? 0}` : "—";
|
|
344
|
+
process.stderr.write(` ${(a.agentId ?? "").padEnd(18)} ${(a.runtimeType ?? "—").padEnd(14)} ${(a.runtimeState ?? "—").padEnd(10)} ${sessions}\n`);
|
|
345
|
+
}
|
|
346
|
+
process.stderr.write("\n");
|
|
347
|
+
}
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (error instanceof Error && error.message.includes("ADMIN_TOKEN")) {
|
|
350
|
+
try {
|
|
351
|
+
const me = await createSdk().register();
|
|
352
|
+
process.stderr.write(`\n Agent: ${me.agentId} (${me.displayName ?? "no name"})\n`);
|
|
353
|
+
process.stderr.write(` Type: ${me.type}\n`);
|
|
354
|
+
process.stderr.write(` Status: ${me.status}\n\n`);
|
|
355
|
+
} catch (sdkError) {
|
|
356
|
+
handleSdkError(sdkError);
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
fail("STATUS_ERROR", error instanceof Error ? error.message : String(error));
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
agent.command("reset <name>").description("Reset agent error state to idle").option("--server <url>", "Hub server URL").action(async (name, options) => {
|
|
364
|
+
try {
|
|
365
|
+
const serverUrl = resolveServerUrl(options.server);
|
|
366
|
+
const response = await fetch(`${serverUrl}/api/v1/admin/agents/activity/${name}/reset-activity`, {
|
|
367
|
+
method: "POST",
|
|
368
|
+
headers: { Authorization: `Bearer ${await ensureFreshAdminToken()}` },
|
|
369
|
+
signal: AbortSignal.timeout(1e4)
|
|
370
|
+
});
|
|
371
|
+
if (!response.ok) fail("RESET_ERROR", `Server returned ${response.status}`, 1);
|
|
372
|
+
process.stderr.write(` Agent "${name}" reset to idle.\n`);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
fail("RESET_ERROR", error instanceof Error ? error.message : String(error));
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
agent.command("sessions <agent-name>").description("List sessions for an agent").option("--server <url>", "Hub server URL").option("--state <state>", "Filter by session state (active/suspended/evicted)").action(async (agentName, options) => {
|
|
378
|
+
try {
|
|
379
|
+
const serverUrl = resolveServerUrl(options.server);
|
|
380
|
+
const adminToken = await ensureFreshAdminToken();
|
|
381
|
+
const agentId = (await resolveAgent(serverUrl, adminToken, agentName)).uuid;
|
|
382
|
+
const qs = options.state ? `?state=${options.state}` : "";
|
|
383
|
+
const response = await fetch(`${serverUrl}/api/v1/admin/sessions/agents/${agentId}${qs}`, {
|
|
384
|
+
headers: { Authorization: `Bearer ${adminToken}` },
|
|
385
|
+
signal: AbortSignal.timeout(1e4)
|
|
386
|
+
});
|
|
387
|
+
if (!response.ok) fail("FETCH_ERROR", `Server returned ${response.status}`, 1);
|
|
388
|
+
const sessions = await response.json();
|
|
389
|
+
if (sessions.length === 0) {
|
|
390
|
+
process.stderr.write(`\n No sessions for "${agentName}".\n\n`);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
process.stderr.write(`\n Sessions for "${agentName}":\n\n`);
|
|
394
|
+
const header = ` ${"CHAT".padEnd(40)} ${"STATE".padEnd(12)} ${"RUNTIME".padEnd(10)} LAST ACTIVITY`;
|
|
395
|
+
process.stderr.write(`${header}\n`);
|
|
396
|
+
process.stderr.write(` ${"─".repeat(header.length - 2)}\n`);
|
|
397
|
+
for (const s of sessions) {
|
|
398
|
+
const chatShort = s.chatId.length > 38 ? `${s.chatId.slice(0, 35)}...` : s.chatId;
|
|
399
|
+
process.stderr.write(` ${chatShort.padEnd(40)} ${s.state.padEnd(12)} ${(s.runtimeState ?? "—").padEnd(10)} ${s.lastActivityAt}\n`);
|
|
400
|
+
}
|
|
401
|
+
process.stderr.write("\n");
|
|
402
|
+
} catch (error) {
|
|
403
|
+
fail("SESSIONS_ERROR", error instanceof Error ? error.message : String(error));
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
const sessionCmd = agent.command("session").description("Session lifecycle commands");
|
|
407
|
+
for (const [cmd, desc] of [
|
|
408
|
+
["suspend", "Suspend a session"],
|
|
409
|
+
["resume", "Resume a suspended session"],
|
|
410
|
+
["terminate", "Terminate a session"]
|
|
411
|
+
]) sessionCmd.command(`${cmd} <agent-name> <chat-id>`).description(desc).option("--server <url>", "Hub server URL").action(async (agentName, chatId, options) => {
|
|
412
|
+
try {
|
|
413
|
+
const serverUrl = resolveServerUrl(options.server);
|
|
414
|
+
const adminToken = await ensureFreshAdminToken();
|
|
415
|
+
const agentId = (await resolveAgent(serverUrl, adminToken, agentName)).uuid;
|
|
416
|
+
const response = await fetch(`${serverUrl}/api/v1/admin/sessions/agents/${agentId}/${chatId}/${cmd}`, {
|
|
417
|
+
method: "POST",
|
|
418
|
+
headers: { Authorization: `Bearer ${adminToken}` },
|
|
419
|
+
signal: AbortSignal.timeout(1e4)
|
|
420
|
+
});
|
|
421
|
+
if (!response.ok) {
|
|
422
|
+
const body = await response.text();
|
|
423
|
+
fail("SESSION_CMD_ERROR", `Server returned ${response.status}: ${body}`, 1);
|
|
424
|
+
}
|
|
425
|
+
process.stderr.write(` Session ${cmd}: ${chatId} → sent\n`);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
fail("SESSION_CMD_ERROR", error instanceof Error ? error.message : String(error));
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
agent.command("chat <agent-name>").description("Open an interactive chat with an agent (as admin human agent)").option("--server <url>", "Hub server URL").action(async (agentName, options) => {
|
|
431
|
+
try {
|
|
432
|
+
const serverUrl = resolveServerUrl(options.server);
|
|
433
|
+
const adminToken = await ensureFreshAdminToken();
|
|
434
|
+
const headers = {
|
|
435
|
+
Authorization: `Bearer ${adminToken}`,
|
|
436
|
+
"Content-Type": "application/json"
|
|
437
|
+
};
|
|
438
|
+
const targetAgent = await resolveAgent(serverUrl, adminToken, agentName);
|
|
439
|
+
const dmRes = await fetch(`${serverUrl}/api/v1/admin/agents/${targetAgent.uuid}/chats`, {
|
|
440
|
+
method: "POST",
|
|
441
|
+
headers,
|
|
442
|
+
signal: AbortSignal.timeout(1e4)
|
|
443
|
+
});
|
|
444
|
+
if (!dmRes.ok) {
|
|
445
|
+
const body = await dmRes.text();
|
|
446
|
+
fail("DM_ERROR", `Failed to create DM: ${dmRes.status} — ${body}`, 1);
|
|
447
|
+
}
|
|
448
|
+
const dm = await dmRes.json();
|
|
449
|
+
process.stderr.write(`\n Chat with ${targetAgent.displayName ?? targetAgent.name ?? targetAgent.uuid}\n`);
|
|
450
|
+
process.stderr.write(` Chat ID: ${dm.id}\n`);
|
|
451
|
+
process.stderr.write(` Type a message and press Enter. Ctrl+C to exit.\n\n`);
|
|
452
|
+
const rl = (await import("node:readline")).createInterface({
|
|
453
|
+
input: process.stdin,
|
|
454
|
+
output: process.stderr,
|
|
455
|
+
prompt: " > "
|
|
456
|
+
});
|
|
457
|
+
let lastSeenAt = null;
|
|
458
|
+
const pollMessages = async () => {
|
|
459
|
+
try {
|
|
460
|
+
const qs = lastSeenAt ? `?limit=50` : `?limit=10`;
|
|
461
|
+
const msgRes = await fetch(`${serverUrl}/api/v1/admin/chats/${dm.id}/messages${qs}`, {
|
|
462
|
+
headers,
|
|
463
|
+
signal: AbortSignal.timeout(1e4)
|
|
464
|
+
});
|
|
465
|
+
if (!msgRes.ok) return;
|
|
466
|
+
const msgData = await msgRes.json();
|
|
467
|
+
const cutoff = lastSeenAt;
|
|
468
|
+
const newMessages = cutoff ? msgData.items.filter((m) => m.createdAt > cutoff && m.senderId === targetAgent.uuid).reverse() : [];
|
|
469
|
+
for (const msg of newMessages) {
|
|
470
|
+
const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
471
|
+
const preview = content.length > 500 ? `${content.slice(0, 500)}...` : content;
|
|
472
|
+
process.stderr.write(`\r [${targetAgent.displayName ?? targetAgent.name ?? "agent"}] ${preview}\n`);
|
|
473
|
+
}
|
|
474
|
+
if (msgData.items.length > 0 && msgData.items[0]) {
|
|
475
|
+
const newest = msgData.items[0].createdAt;
|
|
476
|
+
if (!lastSeenAt || newest > lastSeenAt) lastSeenAt = newest;
|
|
477
|
+
}
|
|
478
|
+
} catch {}
|
|
479
|
+
};
|
|
480
|
+
await pollMessages();
|
|
481
|
+
const pollTimer = setInterval(() => {
|
|
482
|
+
pollMessages().then(() => rl.prompt());
|
|
483
|
+
}, 2e3);
|
|
484
|
+
rl.prompt();
|
|
485
|
+
rl.on("line", async (line) => {
|
|
486
|
+
const text = line.trim();
|
|
487
|
+
if (!text) {
|
|
488
|
+
rl.prompt();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
const sendRes = await fetch(`${serverUrl}/api/v1/admin/chats/${dm.id}/messages`, {
|
|
493
|
+
method: "POST",
|
|
494
|
+
headers,
|
|
495
|
+
body: JSON.stringify({
|
|
496
|
+
format: "text",
|
|
497
|
+
content: text
|
|
498
|
+
}),
|
|
499
|
+
signal: AbortSignal.timeout(1e4)
|
|
500
|
+
});
|
|
501
|
+
if (!sendRes.ok) {
|
|
502
|
+
const body = await sendRes.text();
|
|
503
|
+
process.stderr.write(` [error] Failed to send: ${sendRes.status} — ${body}\n`);
|
|
504
|
+
} else lastSeenAt = (await sendRes.json()).createdAt;
|
|
505
|
+
} catch (err) {
|
|
506
|
+
process.stderr.write(` [error] ${err instanceof Error ? err.message : String(err)}\n`);
|
|
507
|
+
}
|
|
508
|
+
rl.prompt();
|
|
509
|
+
});
|
|
510
|
+
rl.on("close", () => {
|
|
511
|
+
clearInterval(pollTimer);
|
|
512
|
+
process.stderr.write("\n Chat ended.\n");
|
|
513
|
+
process.exit(0);
|
|
514
|
+
});
|
|
515
|
+
} catch (error) {
|
|
516
|
+
fail("CHAT_ERROR", error instanceof Error ? error.message : String(error));
|
|
517
|
+
}
|
|
518
|
+
});
|
|
261
519
|
agent.command("register").description("Register this agent and return identity info").action(async () => {
|
|
262
520
|
try {
|
|
263
521
|
success(await createSdk().register());
|
|
@@ -292,21 +550,19 @@ function registerClientCommands(program) {
|
|
|
292
550
|
schema: clientConfigSchema,
|
|
293
551
|
role: "client"
|
|
294
552
|
});
|
|
553
|
+
const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
|
|
295
554
|
const agents = loadAgents({
|
|
296
555
|
schema: agentConfigSchema,
|
|
297
|
-
agentsDir
|
|
556
|
+
agentsDir
|
|
298
557
|
});
|
|
299
|
-
if (agents.size === 0) {
|
|
300
|
-
process.stderr.write(" No agents configured.\n");
|
|
301
|
-
process.stderr.write(" Add one with: first-tree-hub agent add <name> --token <token>\n");
|
|
302
|
-
process.exit(1);
|
|
303
|
-
}
|
|
304
558
|
process.stderr.write(`\n Connecting to ${config.server.url}...\n`);
|
|
305
559
|
const runtime = new ClientRuntime(config.server.url);
|
|
306
560
|
for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
|
|
307
561
|
await runtime.start();
|
|
562
|
+
runtime.watchAgentsDir(agentsDir);
|
|
308
563
|
const shutdown = async () => {
|
|
309
564
|
process.stderr.write("\n Shutting down...\n");
|
|
565
|
+
runtime.unwatchAgentsDir();
|
|
310
566
|
await runtime.stop();
|
|
311
567
|
process.exit(0);
|
|
312
568
|
};
|
|
@@ -349,15 +605,64 @@ function registerClientCommands(program) {
|
|
|
349
605
|
return;
|
|
350
606
|
}
|
|
351
607
|
process.stderr.write("\n Configured agents:\n\n");
|
|
352
|
-
for (const [name, config] of agents) {
|
|
353
|
-
const masked = config.token.length > 8 ? `${config.token.slice(0, 6)}***${config.token.slice(-2)}` : "***";
|
|
354
|
-
process.stderr.write(` ${name.padEnd(20)} type: ${config.type.padEnd(14)} token: ${masked}\n`);
|
|
355
|
-
}
|
|
608
|
+
for (const [name, config] of agents) process.stderr.write(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)} token: ${maskToken(config.token)}\n`);
|
|
356
609
|
process.stderr.write("\n");
|
|
357
610
|
} catch {
|
|
358
611
|
process.stderr.write(" No agents directory found.\n");
|
|
359
612
|
}
|
|
360
613
|
});
|
|
614
|
+
client.command("hub-list").description("List connected clients on the Hub server").option("--server <url>", "Hub server URL").action(async (options) => {
|
|
615
|
+
try {
|
|
616
|
+
const serverUrl = resolveServerUrl(options.server);
|
|
617
|
+
const token = await ensureFreshAdminToken();
|
|
618
|
+
const response = await fetch(`${serverUrl}/api/v1/admin/clients`, {
|
|
619
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
620
|
+
signal: AbortSignal.timeout(1e4)
|
|
621
|
+
});
|
|
622
|
+
if (!response.ok) fail("FETCH_ERROR", `Server returned ${response.status}`, 1);
|
|
623
|
+
const clients = await response.json();
|
|
624
|
+
if (clients.length === 0) {
|
|
625
|
+
process.stderr.write(" No connected clients.\n");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
process.stderr.write(`\n Connected Clients: ${clients.length}\n\n`);
|
|
629
|
+
const header = ` ${"CLIENT".padEnd(20)} ${"HOST".padEnd(25)} ${"AGENTS".padEnd(8)} CONNECTED`;
|
|
630
|
+
process.stderr.write(`${header}\n`);
|
|
631
|
+
process.stderr.write(` ${"─".repeat(header.length - 2)}\n`);
|
|
632
|
+
for (const c of clients) {
|
|
633
|
+
const since = c.connectedAt ? timeSince(c.connectedAt) : "—";
|
|
634
|
+
process.stderr.write(` ${c.id.padEnd(20)} ${(c.hostname ?? "—").padEnd(25)} ${String(c.agentCount).padEnd(8)} ${since}\n`);
|
|
635
|
+
}
|
|
636
|
+
process.stderr.write("\n");
|
|
637
|
+
} catch (error) {
|
|
638
|
+
fail("CLIENT_LIST_ERROR", error instanceof Error ? error.message : String(error));
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
client.command("hub-disconnect <clientId>").description("Force-disconnect a client from the Hub server").option("--server <url>", "Hub server URL").action(async (clientId, options) => {
|
|
642
|
+
try {
|
|
643
|
+
const serverUrl = resolveServerUrl(options.server);
|
|
644
|
+
const token = await ensureFreshAdminToken();
|
|
645
|
+
const response = await fetch(`${serverUrl}/api/v1/admin/clients/${clientId}/disconnect`, {
|
|
646
|
+
method: "POST",
|
|
647
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
648
|
+
signal: AbortSignal.timeout(1e4)
|
|
649
|
+
});
|
|
650
|
+
if (!response.ok) fail("DISCONNECT_ERROR", `Server returned ${response.status}`, 1);
|
|
651
|
+
process.stderr.write(` Client "${clientId}" disconnected.\n`);
|
|
652
|
+
} catch (error) {
|
|
653
|
+
fail("DISCONNECT_ERROR", error instanceof Error ? error.message : String(error));
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
function timeSince(isoDate) {
|
|
658
|
+
const ms = Date.now() - new Date(isoDate).getTime();
|
|
659
|
+
const seconds = Math.floor(ms / 1e3);
|
|
660
|
+
if (seconds < 60) return `${seconds}s`;
|
|
661
|
+
const minutes = Math.floor(seconds / 60);
|
|
662
|
+
if (minutes < 60) return `${minutes}m`;
|
|
663
|
+
const hours = Math.floor(minutes / 60);
|
|
664
|
+
if (hours < 24) return `${hours}h ${minutes % 60}m`;
|
|
665
|
+
return `${Math.floor(hours / 24)}d ${hours % 24}h`;
|
|
361
666
|
}
|
|
362
667
|
//#endregion
|
|
363
668
|
//#region src/commands/config.ts
|
|
@@ -453,16 +758,100 @@ function isSecretField(schema, dotPath) {
|
|
|
453
758
|
return false;
|
|
454
759
|
}
|
|
455
760
|
//#endregion
|
|
761
|
+
//#region src/commands/connect.ts
|
|
762
|
+
/**
|
|
763
|
+
* Authenticate via connect token — exchange for full JWT credentials.
|
|
764
|
+
*/
|
|
765
|
+
async function authenticateWithToken(url, token) {
|
|
766
|
+
const res = await fetch(`${url}/api/v1/auth/connect-token`, {
|
|
767
|
+
method: "POST",
|
|
768
|
+
headers: { "Content-Type": "application/json" },
|
|
769
|
+
body: JSON.stringify({ token }),
|
|
770
|
+
signal: AbortSignal.timeout(1e4)
|
|
771
|
+
});
|
|
772
|
+
if (!res.ok) fail("AUTH_ERROR", (await res.json().catch(() => ({}))).error ?? `Token exchange failed (HTTP ${res.status})`, 1);
|
|
773
|
+
return await res.json();
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Authenticate via interactive username/password login.
|
|
777
|
+
*/
|
|
778
|
+
async function authenticateInteractive(url) {
|
|
779
|
+
process.stderr.write("\n Log in to Hub:\n");
|
|
780
|
+
const username = await input({ message: " Username:" });
|
|
781
|
+
const pw = await password({ message: " Password:" });
|
|
782
|
+
const loginRes = await fetch(`${url}/api/v1/auth/login`, {
|
|
783
|
+
method: "POST",
|
|
784
|
+
headers: { "Content-Type": "application/json" },
|
|
785
|
+
body: JSON.stringify({
|
|
786
|
+
username,
|
|
787
|
+
password: pw
|
|
788
|
+
}),
|
|
789
|
+
signal: AbortSignal.timeout(1e4)
|
|
790
|
+
});
|
|
791
|
+
if (!loginRes.ok) fail("AUTH_ERROR", (await loginRes.json().catch(() => ({}))).error ?? `Login failed (HTTP ${loginRes.status})`, 1);
|
|
792
|
+
return await loginRes.json();
|
|
793
|
+
}
|
|
794
|
+
function registerConnectCommand(program) {
|
|
795
|
+
program.command("connect <server-url>").description("Connect to a Hub server — configure, authenticate, and start client").option("--token <token>", "Connect token (from Hub web console) — skips interactive login").action(async (serverUrl, options) => {
|
|
796
|
+
try {
|
|
797
|
+
const url = serverUrl.replace(/\/+$/, "");
|
|
798
|
+
setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", url);
|
|
799
|
+
process.stderr.write(`\n \u2713 Server configured: ${url}\n`);
|
|
800
|
+
saveCredentials({
|
|
801
|
+
...options.token ? await authenticateWithToken(url, options.token) : await authenticateInteractive(url),
|
|
802
|
+
serverUrl: url
|
|
803
|
+
});
|
|
804
|
+
process.stderr.write(" ✓ Authenticated\n");
|
|
805
|
+
resetConfig();
|
|
806
|
+
resetConfigMeta();
|
|
807
|
+
const config = await initConfig({
|
|
808
|
+
schema: clientConfigSchema,
|
|
809
|
+
role: "client"
|
|
810
|
+
});
|
|
811
|
+
const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
|
|
812
|
+
const agents = loadAgents({
|
|
813
|
+
schema: agentConfigSchema,
|
|
814
|
+
agentsDir
|
|
815
|
+
});
|
|
816
|
+
process.stderr.write(`\n Starting client...\n`);
|
|
817
|
+
const runtime = new ClientRuntime(config.server.url);
|
|
818
|
+
for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
|
|
819
|
+
await runtime.start();
|
|
820
|
+
runtime.watchAgentsDir(agentsDir);
|
|
821
|
+
const shutdown = async () => {
|
|
822
|
+
process.stderr.write("\n Shutting down...\n");
|
|
823
|
+
runtime.unwatchAgentsDir();
|
|
824
|
+
await runtime.stop();
|
|
825
|
+
process.exit(0);
|
|
826
|
+
};
|
|
827
|
+
process.on("SIGINT", () => void shutdown());
|
|
828
|
+
process.on("SIGTERM", () => void shutdown());
|
|
829
|
+
await new Promise(() => {});
|
|
830
|
+
} catch (error) {
|
|
831
|
+
if (error.name === "ExitPromptError") {
|
|
832
|
+
process.stderr.write("\n Cancelled.\n");
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
836
|
+
process.stderr.write(` Error: ${msg}\n`);
|
|
837
|
+
process.exit(1);
|
|
838
|
+
} finally {
|
|
839
|
+
resetConfig();
|
|
840
|
+
resetConfigMeta();
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
//#endregion
|
|
456
845
|
//#region src/commands/onboard.ts
|
|
457
846
|
async function promptMissing(args) {
|
|
458
847
|
if (!args.server) try {
|
|
459
|
-
const { resolveServerUrl } = await import("../bootstrap-
|
|
848
|
+
const { resolveServerUrl } = await import("../bootstrap-Dq_k_6ZD.mjs").then((n) => n.n);
|
|
460
849
|
resolveServerUrl();
|
|
461
850
|
} catch {
|
|
462
851
|
args.server = await input({ message: "Hub server URL:" });
|
|
463
852
|
saveOnboardState(args);
|
|
464
853
|
}
|
|
465
|
-
const { resolveServerUrl } = await import("../bootstrap-
|
|
854
|
+
const { resolveServerUrl } = await import("../bootstrap-Dq_k_6ZD.mjs").then((n) => n.n);
|
|
466
855
|
const serverUrl = resolveServerUrl(args.server).replace(/\/+$/, "");
|
|
467
856
|
try {
|
|
468
857
|
const res = await fetch(`${serverUrl}/api/v1/bootstrap/config`);
|
|
@@ -474,7 +863,7 @@ async function promptMissing(args) {
|
|
|
474
863
|
}
|
|
475
864
|
let ghUsername = null;
|
|
476
865
|
try {
|
|
477
|
-
const { getGitHubUsername } = await import("../bootstrap-
|
|
866
|
+
const { getGitHubUsername } = await import("../bootstrap-Dq_k_6ZD.mjs").then((n) => n.n);
|
|
478
867
|
ghUsername = getGitHubUsername();
|
|
479
868
|
} catch {}
|
|
480
869
|
if (!args.id) {
|
|
@@ -626,8 +1015,6 @@ function registerServerCommands(program) {
|
|
|
626
1015
|
checkDocker(),
|
|
627
1016
|
checkServerConfig(),
|
|
628
1017
|
await checkDatabase(),
|
|
629
|
-
await checkGitHubToken(),
|
|
630
|
-
await checkContextTreeRepo(),
|
|
631
1018
|
await checkServerHealth()
|
|
632
1019
|
]);
|
|
633
1020
|
});
|
|
@@ -660,12 +1047,12 @@ function registerServerCommands(program) {
|
|
|
660
1047
|
process.exit(1);
|
|
661
1048
|
}
|
|
662
1049
|
});
|
|
663
|
-
server.command("admin:create").description("Create an admin user").option("-u, --username <name>", "Admin username", "admin").option("-p, --password <pass>", "
|
|
1050
|
+
server.command("admin:create").description("Create an admin user with organization").option("-u, --username <name>", "Admin username", "admin").option("-n, --name <name>", "Display name", "Admin").option("-o, --org <org>", "Organization slug", "default").option("-p, --password <pass>", "Password (auto-generated if omitted)").action(async (options) => {
|
|
664
1051
|
try {
|
|
665
|
-
const result = await
|
|
1052
|
+
const result = await createOwner((await initConfig({
|
|
666
1053
|
schema: serverConfigSchema,
|
|
667
1054
|
role: "server"
|
|
668
|
-
})).database.url, options.username, options.password);
|
|
1055
|
+
})).database.url, options.username, options.org, options.name, options.password);
|
|
669
1056
|
process.stderr.write(` Admin user "${result.username}" created.\n`);
|
|
670
1057
|
if (!options.password) process.stderr.write(` Password: ${result.password} (save this — shown only once)\n`);
|
|
671
1058
|
} catch (error) {
|
|
@@ -742,6 +1129,7 @@ registerClientCommands(program);
|
|
|
742
1129
|
registerAgentCommands(program);
|
|
743
1130
|
registerConfigCommands(program);
|
|
744
1131
|
registerStatusCommand(program);
|
|
1132
|
+
registerConnectCommand(program);
|
|
745
1133
|
registerOnboardCommand(program);
|
|
746
1134
|
program.parse();
|
|
747
1135
|
//#endregion
|