@agent-team-foundation/first-tree-hub 0.6.1 → 0.6.3
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-Dq_k_6ZD.mjs → bootstrap-DNL1cEwv.mjs} +54 -156
- package/dist/cli/index.mjs +400 -207
- package/dist/{core-Dt3yNBTm.mjs → core-B10jgThe.mjs} +4610 -3804
- package/dist/drizzle/0019_agent_configs.sql +30 -0
- package/dist/drizzle/0020_unified_user_token.sql +154 -0
- package/dist/drizzle/0021_drop_agents_profile.sql +10 -0
- package/dist/drizzle/meta/_journal.json +21 -0
- package/dist/feishu-BoMJHlOv.mjs +832 -0
- package/dist/index.mjs +4 -4
- package/dist/web/assets/index-CTl4pHIL.css +1 -0
- package/dist/web/assets/index-CnLpaSBg.js +308 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/feishu-Y4m2zFc3.mjs +0 -51
- package/dist/web/assets/index--kyp_ZHv.css +0 -1
- package/dist/web/assets/index-D7-5shxZ.js +0 -310
package/dist/cli/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as
|
|
3
|
-
import { A as
|
|
4
|
-
import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-
|
|
2
|
+
import { C as setConfigValue, S as serverConfigSchema, _ as loadAgents, b as resetConfigMeta, c as saveCredentials, f as agentConfigSchema, g as initConfig, h as getConfigValue, l as DEFAULT_CONFIG_DIR, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, r as ensureFreshAdminToken, s as saveAgentConfig, u as DEFAULT_DATA_DIR, v as readConfigFile, y as resetConfig } from "../bootstrap-DNL1cEwv.mjs";
|
|
3
|
+
import { A as SdkError, D as createOwner, E as ClientRuntime, M as cleanWorkspaces, T as stopPostgres, _ as checkServerHealth, a as formatCheckReport, b as printResults, c as onboardCreate, d as checkAgentConfigs, f as checkClientConfig, g as checkServerConfig, h as checkNodeVersion, i as promptMissingFields, j as SessionRegistry, k as FirstTreeHubSDK, l as saveOnboardState, m as checkDocker, n as isInteractive, o as loadOnboardState, p as checkDatabase, r as promptAddAgent, s as onboardCheck, t as startServer, u as runMigrations, v as checkServerReachable, y as checkWebSocket } from "../core-B10jgThe.mjs";
|
|
4
|
+
import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-BoMJHlOv.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { Command } from "commander";
|
|
7
|
-
import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { confirm, input, password, select } from "@inquirer/prompts";
|
|
10
10
|
//#region src/cli/output.ts
|
|
@@ -27,28 +27,234 @@ function fail(code, message, exitCode = 1) {
|
|
|
27
27
|
process.exit(exitCode);
|
|
28
28
|
}
|
|
29
29
|
//#endregion
|
|
30
|
+
//#region src/commands/agent-config.ts
|
|
31
|
+
async function resolveAgentRecord(serverUrl, adminToken, agentName) {
|
|
32
|
+
const res = await fetch(`${serverUrl}/api/v1/admin/agents?limit=100`, {
|
|
33
|
+
headers: { Authorization: `Bearer ${adminToken}` },
|
|
34
|
+
signal: AbortSignal.timeout(1e4)
|
|
35
|
+
});
|
|
36
|
+
if (!res.ok) fail("FETCH_ERROR", `Failed to list agents: ${res.status}`, 1);
|
|
37
|
+
const found = (await res.json()).items.find((a) => a.name === agentName || a.uuid === agentName);
|
|
38
|
+
if (!found) fail("NOT_FOUND", `Agent "${agentName}" not found`, 1);
|
|
39
|
+
return found;
|
|
40
|
+
}
|
|
41
|
+
async function adminFetch(url, init) {
|
|
42
|
+
const { adminToken, headers, ...rest } = init;
|
|
43
|
+
const res = await fetch(url, {
|
|
44
|
+
...rest,
|
|
45
|
+
headers: {
|
|
46
|
+
Authorization: `Bearer ${adminToken}`,
|
|
47
|
+
...rest.body ? { "Content-Type": "application/json" } : {},
|
|
48
|
+
...headers
|
|
49
|
+
},
|
|
50
|
+
signal: AbortSignal.timeout(15e3)
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
const text = await res.text();
|
|
54
|
+
fail(`HTTP_${res.status}`, text || res.statusText, res.status === 401 ? 3 : 1);
|
|
55
|
+
}
|
|
56
|
+
return await res.json();
|
|
57
|
+
}
|
|
58
|
+
async function getCurrent(serverUrl, adminToken, agentId) {
|
|
59
|
+
return adminFetch(`${serverUrl}/api/v1/admin/agents/${agentId}/config`, {
|
|
60
|
+
method: "GET",
|
|
61
|
+
adminToken
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function patchConfig(serverUrl, adminToken, agentId, expectedVersion, patch) {
|
|
65
|
+
return adminFetch(`${serverUrl}/api/v1/admin/agents/${agentId}/config`, {
|
|
66
|
+
method: "PATCH",
|
|
67
|
+
adminToken,
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
expectedVersion,
|
|
70
|
+
payload: patch
|
|
71
|
+
})
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function printConfig(cfg) {
|
|
75
|
+
process.stdout.write(`Agent: ${cfg.agentId}\n`);
|
|
76
|
+
process.stdout.write(`Version: ${cfg.version} (updated ${cfg.updatedAt} by ${cfg.updatedBy})\n`);
|
|
77
|
+
process.stdout.write(`\nModel: ${cfg.payload.model || "(unset)"}\n`);
|
|
78
|
+
process.stdout.write(`Prompt append: ${cfg.payload.prompt.append ? "(set)" : "(empty)"}\n`);
|
|
79
|
+
if (cfg.payload.prompt.append) process.stdout.write(` > ${cfg.payload.prompt.append.replace(/\n/g, "\n > ")}\n`);
|
|
80
|
+
process.stdout.write(`\nMCP servers (${cfg.payload.mcpServers.length}):\n`);
|
|
81
|
+
for (const s of cfg.payload.mcpServers) process.stdout.write(` - ${s.name} [${s.transport}]\n`);
|
|
82
|
+
process.stdout.write(`\nEnv (${cfg.payload.env.length}):\n`);
|
|
83
|
+
for (const e of cfg.payload.env) process.stdout.write(` - ${e.key}=${e.value} ${e.sensitive ? "(sensitive)" : ""}\n`);
|
|
84
|
+
process.stdout.write(`\nGit repos (${cfg.payload.gitRepos.length}):\n`);
|
|
85
|
+
for (const r of cfg.payload.gitRepos) {
|
|
86
|
+
const ref = r.ref ? `@${r.ref}` : "";
|
|
87
|
+
const path = r.localPath ? ` → ${r.localPath}` : "";
|
|
88
|
+
process.stdout.write(` - ${r.url}${ref}${path}\n`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function registerAgentConfigCommands(parent) {
|
|
92
|
+
const config = parent.command("config").description("Manage agent runtime configuration (Step 8)");
|
|
93
|
+
config.command("get <agent>").description("Print the current runtime config for an agent").action(async (agentName) => {
|
|
94
|
+
const serverUrl = resolveServerUrl(process.env.FIRST_TREE_HUB_SERVER_URL);
|
|
95
|
+
const adminToken = await ensureFreshAdminToken();
|
|
96
|
+
const { uuid } = await resolveAgentRecord(serverUrl, adminToken, agentName);
|
|
97
|
+
printConfig(await getCurrent(serverUrl, adminToken, uuid));
|
|
98
|
+
});
|
|
99
|
+
config.command("set-model <agent> <model>").description("Replace the model field (e.g. claude-opus-4-6)").action(async (agentName, model) => {
|
|
100
|
+
const serverUrl = resolveServerUrl(process.env.FIRST_TREE_HUB_SERVER_URL);
|
|
101
|
+
const adminToken = await ensureFreshAdminToken();
|
|
102
|
+
const { uuid } = await resolveAgentRecord(serverUrl, adminToken, agentName);
|
|
103
|
+
const updated = await patchConfig(serverUrl, adminToken, uuid, (await getCurrent(serverUrl, adminToken, uuid)).version, { model });
|
|
104
|
+
success({
|
|
105
|
+
agentId: updated.agentId,
|
|
106
|
+
version: updated.version,
|
|
107
|
+
model: updated.payload.model
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
config.command("append-prompt <agent>").description("Replace the systemPrompt append text — reads from -f file or stdin").option("-f, --file <path>", "Read prompt text from this file").action(async (agentName, opts) => {
|
|
111
|
+
const serverUrl = resolveServerUrl(process.env.FIRST_TREE_HUB_SERVER_URL);
|
|
112
|
+
const adminToken = await ensureFreshAdminToken();
|
|
113
|
+
const { uuid } = await resolveAgentRecord(serverUrl, adminToken, agentName);
|
|
114
|
+
let text;
|
|
115
|
+
if (opts.file) text = readFileSync(opts.file, "utf-8");
|
|
116
|
+
else if (!process.stdin.isTTY) text = await new Promise((resolve, reject) => {
|
|
117
|
+
const chunks = [];
|
|
118
|
+
process.stdin.on("data", (c) => chunks.push(c));
|
|
119
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
120
|
+
process.stdin.on("error", reject);
|
|
121
|
+
});
|
|
122
|
+
else fail("MISSING_INPUT", "Provide -f <file> or pipe prompt text via stdin", 2);
|
|
123
|
+
const updated = await patchConfig(serverUrl, adminToken, uuid, (await getCurrent(serverUrl, adminToken, uuid)).version, { prompt: { append: text } });
|
|
124
|
+
success({
|
|
125
|
+
agentId: updated.agentId,
|
|
126
|
+
version: updated.version,
|
|
127
|
+
append_length: text.length
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
config.command("add-mcp <agent>").description("Add or replace an MCP server (replace-by-name semantics)").requiredOption("--name <name>", "MCP server name").requiredOption("--transport <transport>", "stdio | http | sse").option("--command <command>", "stdio command").option("--args <args...>", "stdio command args").option("--url <url>", "http/sse URL").action(async (agentName, opts) => {
|
|
131
|
+
const serverUrl = resolveServerUrl(process.env.FIRST_TREE_HUB_SERVER_URL);
|
|
132
|
+
const adminToken = await ensureFreshAdminToken();
|
|
133
|
+
const { uuid } = await resolveAgentRecord(serverUrl, adminToken, agentName);
|
|
134
|
+
const current = await getCurrent(serverUrl, adminToken, uuid);
|
|
135
|
+
let server;
|
|
136
|
+
if (opts.transport === "stdio") {
|
|
137
|
+
if (!opts.command) fail("MISSING_COMMAND", "stdio transport requires --command", 2);
|
|
138
|
+
server = {
|
|
139
|
+
name: opts.name,
|
|
140
|
+
transport: "stdio",
|
|
141
|
+
command: opts.command,
|
|
142
|
+
args: opts.args
|
|
143
|
+
};
|
|
144
|
+
} else if (opts.transport === "http" || opts.transport === "sse") {
|
|
145
|
+
if (!opts.url) fail("MISSING_URL", `${opts.transport} transport requires --url`, 2);
|
|
146
|
+
server = {
|
|
147
|
+
name: opts.name,
|
|
148
|
+
transport: opts.transport,
|
|
149
|
+
url: opts.url
|
|
150
|
+
};
|
|
151
|
+
} else fail("BAD_TRANSPORT", `transport must be stdio|http|sse, got ${opts.transport}`, 2);
|
|
152
|
+
const remaining = current.payload.mcpServers.filter((s) => s.name !== opts.name);
|
|
153
|
+
const updated = await patchConfig(serverUrl, adminToken, uuid, current.version, { mcpServers: [...remaining, server] });
|
|
154
|
+
success({
|
|
155
|
+
agentId: updated.agentId,
|
|
156
|
+
version: updated.version,
|
|
157
|
+
mcpServer: opts.name
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
config.command("set-env <agent> <kv>").description("Set an env variable: KEY=VALUE. Use --sensitive for secrets.").option("--sensitive", "Mark this value as sensitive (encrypted at rest, masked in echo)").action(async (agentName, kv, opts) => {
|
|
161
|
+
const serverUrl = resolveServerUrl(process.env.FIRST_TREE_HUB_SERVER_URL);
|
|
162
|
+
const adminToken = await ensureFreshAdminToken();
|
|
163
|
+
const { uuid } = await resolveAgentRecord(serverUrl, adminToken, agentName);
|
|
164
|
+
const eqIdx = kv.indexOf("=");
|
|
165
|
+
if (eqIdx <= 0) fail("BAD_KV", "Expected KEY=VALUE", 2);
|
|
166
|
+
const key = kv.slice(0, eqIdx);
|
|
167
|
+
const value = kv.slice(eqIdx + 1);
|
|
168
|
+
const current = await getCurrent(serverUrl, adminToken, uuid);
|
|
169
|
+
const remaining = current.payload.env.filter((e) => e.key !== key);
|
|
170
|
+
const updated = await patchConfig(serverUrl, adminToken, uuid, current.version, { env: [...remaining, {
|
|
171
|
+
key,
|
|
172
|
+
value,
|
|
173
|
+
sensitive: opts.sensitive ?? false
|
|
174
|
+
}] });
|
|
175
|
+
success({
|
|
176
|
+
agentId: updated.agentId,
|
|
177
|
+
version: updated.version,
|
|
178
|
+
env: key
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
config.command("add-repo <agent> <url>").description("Add a Git repo to the agent's worktree set").option("--ref <ref>", "branch / tag / commit (defaults to repo HEAD)").option("--path <path>", "local path relative to session cwd").action(async (agentName, url, opts) => {
|
|
182
|
+
const serverUrl = resolveServerUrl(process.env.FIRST_TREE_HUB_SERVER_URL);
|
|
183
|
+
const adminToken = await ensureFreshAdminToken();
|
|
184
|
+
const { uuid } = await resolveAgentRecord(serverUrl, adminToken, agentName);
|
|
185
|
+
const current = await getCurrent(serverUrl, adminToken, uuid);
|
|
186
|
+
const remaining = current.payload.gitRepos.filter((r) => r.url !== url);
|
|
187
|
+
const updated = await patchConfig(serverUrl, adminToken, uuid, current.version, { gitRepos: [...remaining, {
|
|
188
|
+
url,
|
|
189
|
+
ref: opts.ref,
|
|
190
|
+
localPath: opts.path
|
|
191
|
+
}] });
|
|
192
|
+
success({
|
|
193
|
+
agentId: updated.agentId,
|
|
194
|
+
version: updated.version,
|
|
195
|
+
repo: url
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
config.command("dry-run <agent>").description("Validate a JSON patch and print the diff without persisting").requiredOption("-f, --file <path>", "JSON file with the partial payload to apply").action(async (agentName, opts) => {
|
|
199
|
+
const serverUrl = resolveServerUrl(process.env.FIRST_TREE_HUB_SERVER_URL);
|
|
200
|
+
const adminToken = await ensureFreshAdminToken();
|
|
201
|
+
const { uuid } = await resolveAgentRecord(serverUrl, adminToken, agentName);
|
|
202
|
+
const patch = JSON.parse(readFileSync(opts.file, "utf-8"));
|
|
203
|
+
const result = await adminFetch(`${serverUrl}/api/v1/admin/agents/${uuid}/config/dry-run`, {
|
|
204
|
+
method: "POST",
|
|
205
|
+
adminToken,
|
|
206
|
+
body: JSON.stringify({ payload: patch })
|
|
207
|
+
});
|
|
208
|
+
process.stdout.write(`Diff (${result.diff.length} change${result.diff.length === 1 ? "" : "s"}):\n`);
|
|
209
|
+
for (const d of result.diff) process.stdout.write(` ${d.op} ${d.path}\n`);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
//#endregion
|
|
30
213
|
//#region src/commands/agent.ts
|
|
31
214
|
const DEFAULT_WORKSPACE_TTL_MS = 10080 * 60 * 1e3;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Resolve the agent this CLI invocation should act on. We read the local
|
|
217
|
+
* `agents/<name>/agent.yaml` file to find the agentId, then pair it with the
|
|
218
|
+
* user's current member JWT (refreshed on demand) at call time.
|
|
219
|
+
*
|
|
220
|
+
* Only one agent is expected per command invocation — if the user has many
|
|
221
|
+
* agents configured they must pick one with `--agent <name>` (next step of
|
|
222
|
+
* CLI polish) or rely on a single entry.
|
|
223
|
+
*/
|
|
224
|
+
function resolveLocalAgent(agentName) {
|
|
225
|
+
const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
|
|
226
|
+
const agents = loadAgents({
|
|
227
|
+
schema: agentConfigSchema,
|
|
228
|
+
agentsDir
|
|
229
|
+
});
|
|
230
|
+
if (agents.size === 0) fail("MISSING_AGENT", "No agent configured. Run `first-tree-hub agent add` first.", 2);
|
|
231
|
+
let resolvedName;
|
|
232
|
+
if (agentName) resolvedName = agentName;
|
|
233
|
+
else if (agents.size === 1) {
|
|
234
|
+
const [only] = [...agents.keys()];
|
|
235
|
+
if (!only) fail("MISSING_AGENT", "No agent configured. Run `first-tree-hub agent add` first.", 2);
|
|
236
|
+
resolvedName = only;
|
|
237
|
+
} else fail("AMBIGUOUS_AGENT", `Multiple agents configured — specify --agent <name>. Available: ${[...agents.keys()].join(", ")}`, 2);
|
|
238
|
+
const cfg = agents.get(resolvedName);
|
|
239
|
+
if (!cfg) fail("UNKNOWN_AGENT", `Agent "${resolvedName}" not found in ${agentsDir}`, 2);
|
|
39
240
|
let serverUrl;
|
|
40
241
|
try {
|
|
41
242
|
serverUrl = resolveServerUrl(process.env.FIRST_TREE_HUB_SERVER_URL);
|
|
42
|
-
} catch {
|
|
43
|
-
|
|
243
|
+
} catch (error) {
|
|
244
|
+
fail("MISSING_SERVER_URL", error instanceof Error ? error.message : String(error), 2);
|
|
44
245
|
}
|
|
45
246
|
return {
|
|
46
247
|
serverUrl,
|
|
47
|
-
|
|
248
|
+
agentId: cfg.agentId
|
|
48
249
|
};
|
|
49
250
|
}
|
|
50
|
-
function createSdk() {
|
|
51
|
-
|
|
251
|
+
function createSdk(agentName) {
|
|
252
|
+
const { serverUrl, agentId } = resolveLocalAgent(agentName);
|
|
253
|
+
return new FirstTreeHubSDK({
|
|
254
|
+
serverUrl,
|
|
255
|
+
getAccessToken: () => ensureFreshAccessToken(),
|
|
256
|
+
agentId
|
|
257
|
+
});
|
|
52
258
|
}
|
|
53
259
|
function handleSdkError(error) {
|
|
54
260
|
if (error instanceof SdkError) {
|
|
@@ -82,10 +288,6 @@ function readStdin() {
|
|
|
82
288
|
process.stdin.on("error", reject);
|
|
83
289
|
});
|
|
84
290
|
}
|
|
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
291
|
async function resolveAgent(serverUrl, adminToken, agentName) {
|
|
90
292
|
const res = await fetch(`${serverUrl}/api/v1/admin/agents?limit=100`, {
|
|
91
293
|
headers: { Authorization: `Bearer ${adminToken}` },
|
|
@@ -97,22 +299,24 @@ async function resolveAgent(serverUrl, adminToken, agentName) {
|
|
|
97
299
|
return found;
|
|
98
300
|
}
|
|
99
301
|
function registerAgentCommands(program) {
|
|
100
|
-
const agent = program.command("agent").description("Agent management — config,
|
|
101
|
-
|
|
302
|
+
const agent = program.command("agent").description("Agent management — config, bindings, messaging");
|
|
303
|
+
registerAgentConfigCommands(agent);
|
|
304
|
+
agent.command("add [name]").description("Register a local alias for an existing Hub agent (stores agentId)").option("--agent-id <id>", "Agent UUID on the Hub").action(async (name, options) => {
|
|
102
305
|
try {
|
|
103
306
|
let agentName = name;
|
|
104
|
-
let
|
|
105
|
-
if (!agentName || !
|
|
307
|
+
let agentId = options?.agentId;
|
|
308
|
+
if (!agentName || !agentId) {
|
|
106
309
|
const result = await promptAddAgent();
|
|
107
310
|
agentName = agentName ?? result.name;
|
|
108
|
-
|
|
311
|
+
agentId = agentId ?? result.agentId;
|
|
109
312
|
}
|
|
313
|
+
if (!agentName || !agentId) fail("MISSING_AGENT_ARGS", "Both agent name and agent-id are required.", 2);
|
|
110
314
|
const agentDir = join(DEFAULT_CONFIG_DIR, "agents", agentName);
|
|
111
315
|
mkdirSync(agentDir, {
|
|
112
316
|
recursive: true,
|
|
113
317
|
mode: 448
|
|
114
318
|
});
|
|
115
|
-
setConfigValue(join(agentDir, "agent.yaml"), "
|
|
319
|
+
setConfigValue(join(agentDir, "agent.yaml"), "agentId", agentId);
|
|
116
320
|
process.stderr.write(` Agent "${agentName}" added.\n`);
|
|
117
321
|
process.stderr.write(` Config: ${join(agentDir, "agent.yaml")}\n`);
|
|
118
322
|
} catch (error) {
|
|
@@ -125,7 +329,7 @@ function registerAgentCommands(program) {
|
|
|
125
329
|
process.exit(1);
|
|
126
330
|
}
|
|
127
331
|
});
|
|
128
|
-
agent.command("remove <name>").description("Remove
|
|
332
|
+
agent.command("remove <name>").description("Remove a local agent alias and its runtime data").action((name) => {
|
|
129
333
|
const agentDir = join(DEFAULT_CONFIG_DIR, "agents", name);
|
|
130
334
|
if (!existsSync(agentDir)) {
|
|
131
335
|
process.stderr.write(` Agent "${name}" not found.\n`);
|
|
@@ -142,7 +346,7 @@ function registerAgentCommands(program) {
|
|
|
142
346
|
rmSync(join(DEFAULT_DATA_DIR, "sessions", `${name}.json`), { force: true });
|
|
143
347
|
process.stderr.write(` Agent "${name}" removed.\n`);
|
|
144
348
|
});
|
|
145
|
-
agent.command("list").description("List configured agents").action(() => {
|
|
349
|
+
agent.command("list").description("List locally-configured agents").action(() => {
|
|
146
350
|
const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
|
|
147
351
|
try {
|
|
148
352
|
const agents = loadAgents({
|
|
@@ -153,21 +357,22 @@ function registerAgentCommands(program) {
|
|
|
153
357
|
process.stderr.write(" No agents configured.\n");
|
|
154
358
|
return;
|
|
155
359
|
}
|
|
156
|
-
for (const [name, config] of agents) process.stderr.write(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)}
|
|
360
|
+
for (const [name, config] of agents) process.stderr.write(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)} agentId: ${config.agentId}\n`);
|
|
157
361
|
} catch {
|
|
158
362
|
process.stderr.write(" No agents configured.\n");
|
|
159
363
|
}
|
|
160
364
|
});
|
|
161
|
-
agent.command("create <name>").description("Create an agent on Hub and bind it locally
|
|
365
|
+
agent.command("create <name>").description("Create an agent on Hub and bind it locally").requiredOption("--type <type>", "Agent type (human, personal_assistant, autonomous_agent)").requiredOption("--client-id <id>", "Client (machine) that will run this agent — must be owned by you. Run `first-tree-hub client connect` on that machine first.").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
366
|
try {
|
|
163
367
|
const serverUrl = resolveServerUrl(options.server);
|
|
164
368
|
const headers = {
|
|
165
|
-
Authorization: `Bearer ${await
|
|
369
|
+
Authorization: `Bearer ${await ensureFreshAccessToken()}`,
|
|
166
370
|
"Content-Type": "application/json"
|
|
167
371
|
};
|
|
168
372
|
const createBody = {
|
|
169
373
|
name,
|
|
170
|
-
type: options.type
|
|
374
|
+
type: options.type,
|
|
375
|
+
clientId: options.clientId
|
|
171
376
|
};
|
|
172
377
|
if (options.displayName) createBody.displayName = options.displayName;
|
|
173
378
|
const createRes = await fetch(`${serverUrl}/api/v1/admin/agents`, {
|
|
@@ -177,20 +382,39 @@ function registerAgentCommands(program) {
|
|
|
177
382
|
signal: AbortSignal.timeout(1e4)
|
|
178
383
|
});
|
|
179
384
|
if (!createRes.ok) fail("CREATE_ERROR", (await createRes.json().catch(() => ({}))).error ?? `Failed to create agent (HTTP ${createRes.status})`, 1);
|
|
180
|
-
const
|
|
181
|
-
process.stderr.write(` \u2713 Agent created: ${
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
385
|
+
const created = await createRes.json();
|
|
386
|
+
process.stderr.write(` \u2713 Agent created: ${created.name ?? created.uuid}\n`);
|
|
387
|
+
const agentDir = saveAgentConfig(name, created.uuid, options.runtime);
|
|
388
|
+
process.stderr.write(` \u2713 Config saved: ${agentDir}/agent.yaml\n`);
|
|
389
|
+
process.stderr.write(" ✓ Agent ready — start the client on that machine to bind\n");
|
|
390
|
+
} catch (error) {
|
|
391
|
+
fail("CREATE_ERROR", error instanceof Error ? error.message : String(error));
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
agent.command("claim <agentName>").description("Become the manager of an agent (admin-only, or self-claim an unmanaged agent)").option("--server <url>", "Hub server URL").action(async (agentName, options) => {
|
|
395
|
+
try {
|
|
396
|
+
const serverUrl = resolveServerUrl(options.server);
|
|
397
|
+
const accessToken = await ensureFreshAccessToken();
|
|
398
|
+
const meRes = await fetch(`${serverUrl}/api/v1/me`, {
|
|
399
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
400
|
+
signal: AbortSignal.timeout(1e4)
|
|
401
|
+
});
|
|
402
|
+
if (!meRes.ok) fail("ME_ERROR", `Failed to fetch current member (HTTP ${meRes.status})`, 1);
|
|
403
|
+
const me = await meRes.json();
|
|
404
|
+
const target = await resolveAgent(serverUrl, accessToken, agentName);
|
|
405
|
+
const patchRes = await fetch(`${serverUrl}/api/v1/admin/agents/${target.uuid}`, {
|
|
406
|
+
method: "PATCH",
|
|
407
|
+
headers: {
|
|
408
|
+
Authorization: `Bearer ${accessToken}`,
|
|
409
|
+
"Content-Type": "application/json"
|
|
410
|
+
},
|
|
411
|
+
body: JSON.stringify({ managerId: me.memberId }),
|
|
186
412
|
signal: AbortSignal.timeout(1e4)
|
|
187
413
|
});
|
|
188
|
-
if (!
|
|
189
|
-
|
|
190
|
-
process.stderr.write(` \u2713 Token saved: ${agentDir}/agent.yaml\n`);
|
|
191
|
-
process.stderr.write(` \u2713 Agent ready — running client will auto-bind\n`);
|
|
414
|
+
if (!patchRes.ok) fail("CLAIM_ERROR", (await patchRes.json().catch(() => ({}))).error ?? `Claim failed (HTTP ${patchRes.status})`, 1);
|
|
415
|
+
process.stderr.write(` Claimed "${target.name ?? target.uuid}" — now managed by you.\n`);
|
|
192
416
|
} catch (error) {
|
|
193
|
-
fail("
|
|
417
|
+
fail("CLAIM_ERROR", error instanceof Error ? error.message : String(error));
|
|
194
418
|
}
|
|
195
419
|
});
|
|
196
420
|
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) => {
|
|
@@ -215,24 +439,13 @@ function registerAgentCommands(program) {
|
|
|
215
439
|
}
|
|
216
440
|
process.stderr.write(` ${totalRemoved} workspace(s) cleaned.\n`);
|
|
217
441
|
});
|
|
218
|
-
agent.command("token").description("Agent token management").command("bootstrap <agentName>").description("Bootstrap a token using GitHub identity (requires gh CLI)").option("--save-to <target>", "Save token to: \"agent\" (default) or a file path", "agent").option("--server <url>", "Hub server URL").action(async (agentName, options) => {
|
|
219
|
-
try {
|
|
220
|
-
const result = await bootstrapToken(resolveServerUrl(options.server), agentName, { saveTo: options.saveTo });
|
|
221
|
-
if (options.saveTo === "agent") process.stderr.write(`Token saved to ${DEFAULT_HOME_DIR}/config/agents/${agentName}/agent.yaml\n`);
|
|
222
|
-
else process.stderr.write(`Token saved to ${options.saveTo}\n`);
|
|
223
|
-
success({
|
|
224
|
-
agentId: result.agentId,
|
|
225
|
-
tokenSaved: true
|
|
226
|
-
});
|
|
227
|
-
} catch (error) {
|
|
228
|
-
fail("BOOTSTRAP_ERROR", error instanceof Error ? error.message : String(error));
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
442
|
const bind = agent.command("bind").description("Bind external IM accounts to agents");
|
|
232
|
-
bind.command("bot").description("Bind a Feishu bot to this agent (self-service)").requiredOption("--platform <platform>", "Platform: feishu").requiredOption("--app-id <id>", "Feishu bot App ID").requiredOption("--app-secret <secret>", "Feishu bot App Secret").option("--server <url>", "Hub server URL").action(async (options) => {
|
|
443
|
+
bind.command("bot").description("Bind a Feishu bot to this agent (self-service)").requiredOption("--platform <platform>", "Platform: feishu").requiredOption("--app-id <id>", "Feishu bot App ID").requiredOption("--app-secret <secret>", "Feishu bot App Secret").option("--agent <name>", "Local agent alias (default: first configured)").option("--server <url>", "Hub server URL").action(async (options) => {
|
|
233
444
|
try {
|
|
234
445
|
if (options.platform !== "feishu") fail("UNSUPPORTED_PLATFORM", `Platform "${options.platform}" is not supported. Use "feishu".`);
|
|
235
|
-
|
|
446
|
+
const serverUrl = resolveServerUrl(options.server);
|
|
447
|
+
const { agentId } = resolveLocalAgent(options.agent);
|
|
448
|
+
await bindFeishuBot(serverUrl, await ensureFreshAccessToken(), agentId, options.appId, options.appSecret);
|
|
236
449
|
process.stderr.write("Feishu bot bound successfully.\n");
|
|
237
450
|
success({
|
|
238
451
|
platform: "feishu",
|
|
@@ -242,10 +455,12 @@ function registerAgentCommands(program) {
|
|
|
242
455
|
fail("BIND_BOT_ERROR", error instanceof Error ? error.message : String(error));
|
|
243
456
|
}
|
|
244
457
|
});
|
|
245
|
-
bind.command("user <humanAgentId>").description("Bind a Feishu user to a human agent (via delegate_mention)").requiredOption("--platform <platform>", "Platform: feishu").requiredOption("--feishu-id <id>", "Feishu user ID (ou_xxx)").option("--server <url>", "Hub server URL").action(async (humanAgentId, options) => {
|
|
458
|
+
bind.command("user <humanAgentId>").description("Bind a Feishu user to a human agent (via delegate_mention)").requiredOption("--platform <platform>", "Platform: feishu").requiredOption("--feishu-id <id>", "Feishu user ID (ou_xxx)").option("--agent <name>", "Local agent alias (default: first configured)").option("--server <url>", "Hub server URL").action(async (humanAgentId, options) => {
|
|
246
459
|
try {
|
|
247
460
|
if (options.platform !== "feishu") fail("UNSUPPORTED_PLATFORM", `Platform "${options.platform}" is not supported. Use "feishu".`);
|
|
248
|
-
|
|
461
|
+
const serverUrl = resolveServerUrl(options.server);
|
|
462
|
+
const { agentId } = resolveLocalAgent(options.agent);
|
|
463
|
+
await bindFeishuUser(serverUrl, await ensureFreshAccessToken(), agentId, humanAgentId, options.feishuId);
|
|
249
464
|
process.stderr.write(`Feishu user ${options.feishuId} bound to ${humanAgentId}.\n`);
|
|
250
465
|
success({
|
|
251
466
|
platform: "feishu",
|
|
@@ -256,7 +471,7 @@ function registerAgentCommands(program) {
|
|
|
256
471
|
fail("BIND_USER_ERROR", error instanceof Error ? error.message : String(error));
|
|
257
472
|
}
|
|
258
473
|
});
|
|
259
|
-
agent.command("send <target> [message]").description("Send a message to an agent or chat").option("-f, --format <format>", "Message format (text|markdown|card)", "text").option("--chat", "Treat target as chat ID instead of agent ID").option("-m, --metadata <json>", "JSON metadata to attach").option("--reply-to <messageId>", "Message ID to reply to").option("--reply-to-inbox <inboxId>", "Cross-chat reply target inbox").option("--reply-to-chat <chatId>", "Cross-chat reply target chat").action(async (target, message, options) => {
|
|
474
|
+
agent.command("send <target> [message]").description("Send a message to an agent or chat").option("-f, --format <format>", "Message format (text|markdown|card)", "text").option("--chat", "Treat target as chat ID instead of agent ID").option("-m, --metadata <json>", "JSON metadata to attach").option("--reply-to <messageId>", "Message ID to reply to").option("--reply-to-inbox <inboxId>", "Cross-chat reply target inbox").option("--reply-to-chat <chatId>", "Cross-chat reply target chat").option("--agent <name>", "Local agent alias (default: first configured)").action(async (target, message, options) => {
|
|
260
475
|
try {
|
|
261
476
|
const content = message ?? await readStdin();
|
|
262
477
|
if (!content) fail("NO_MESSAGE", "No message provided. Pass as argument or pipe via stdin.", 2);
|
|
@@ -266,7 +481,7 @@ function registerAgentCommands(program) {
|
|
|
266
481
|
} catch {
|
|
267
482
|
fail("INVALID_METADATA", "Metadata must be valid JSON.", 2);
|
|
268
483
|
}
|
|
269
|
-
const sdk = createSdk();
|
|
484
|
+
const sdk = createSdk(options.agent);
|
|
270
485
|
if (options.chat) success(await sdk.sendMessage(target, {
|
|
271
486
|
format: options.format,
|
|
272
487
|
content,
|
|
@@ -286,10 +501,10 @@ function registerAgentCommands(program) {
|
|
|
286
501
|
handleSdkError(error);
|
|
287
502
|
}
|
|
288
503
|
});
|
|
289
|
-
agent.command("chats").description("List chats this agent participates in").option("-l, --limit <number>", "Maximum chats to return (1-100)", "20").option("--cursor <cursor>", "Pagination cursor from previous response").action(async (options) => {
|
|
504
|
+
agent.command("chats").description("List chats this agent participates in").option("-l, --limit <number>", "Maximum chats to return (1-100)", "20").option("--cursor <cursor>", "Pagination cursor from previous response").option("--agent <name>", "Local agent alias (default: first configured)").action(async (options) => {
|
|
290
505
|
try {
|
|
291
506
|
const limit = parseLimit(options.limit, 100);
|
|
292
|
-
success(await createSdk().listChats({
|
|
507
|
+
success(await createSdk(options.agent).listChats({
|
|
293
508
|
limit,
|
|
294
509
|
cursor: options.cursor
|
|
295
510
|
}));
|
|
@@ -297,10 +512,10 @@ function registerAgentCommands(program) {
|
|
|
297
512
|
handleSdkError(error);
|
|
298
513
|
}
|
|
299
514
|
});
|
|
300
|
-
agent.command("history <chatId>").description("View message history in a chat").option("-l, --limit <number>", "Maximum messages to return (1-100)", "20").option("--cursor <cursor>", "Pagination cursor from previous response").action(async (chatId, options) => {
|
|
515
|
+
agent.command("history <chatId>").description("View message history in a chat").option("-l, --limit <number>", "Maximum messages to return (1-100)", "20").option("--cursor <cursor>", "Pagination cursor from previous response").option("--agent <name>", "Local agent alias (default: first configured)").action(async (chatId, options) => {
|
|
301
516
|
try {
|
|
302
517
|
const limit = parseLimit(options.limit, 100);
|
|
303
|
-
success(await createSdk().listMessages(chatId, {
|
|
518
|
+
success(await createSdk(options.agent).listMessages(chatId, {
|
|
304
519
|
limit,
|
|
305
520
|
cursor: options.cursor
|
|
306
521
|
}));
|
|
@@ -312,22 +527,22 @@ function registerAgentCommands(program) {
|
|
|
312
527
|
try {
|
|
313
528
|
const serverUrl = resolveServerUrl(options?.server);
|
|
314
529
|
const response = await fetch(`${serverUrl}/api/v1/admin/agents/activity`, {
|
|
315
|
-
headers: { Authorization: `Bearer ${await
|
|
530
|
+
headers: { Authorization: `Bearer ${await ensureFreshAccessToken()}` },
|
|
316
531
|
signal: AbortSignal.timeout(1e4)
|
|
317
532
|
});
|
|
318
533
|
if (!response.ok) fail("FETCH_ERROR", `Server returned ${response.status}`, 1);
|
|
319
534
|
const data = await response.json();
|
|
320
535
|
if (name) {
|
|
321
|
-
const
|
|
322
|
-
if (!
|
|
536
|
+
const ag = data.agents.find((a) => a.agentId === name);
|
|
537
|
+
if (!ag) {
|
|
323
538
|
process.stderr.write(`\n Agent "${name}" is not running.\n\n`);
|
|
324
539
|
return;
|
|
325
540
|
}
|
|
326
|
-
process.stderr.write(`\n Agent: ${
|
|
327
|
-
process.stderr.write(` Runtime: ${
|
|
328
|
-
process.stderr.write(` State: ${
|
|
329
|
-
if (
|
|
330
|
-
if (
|
|
541
|
+
process.stderr.write(`\n Agent: ${ag.agentId}\n`);
|
|
542
|
+
process.stderr.write(` Runtime: ${ag.runtimeType ?? "—"}\n`);
|
|
543
|
+
process.stderr.write(` State: ${ag.runtimeState ?? "—"}\n`);
|
|
544
|
+
if (ag.activeSessions !== null) process.stderr.write(` Sessions: ${ag.activeSessions} active / ${ag.totalSessions ?? 0} total\n`);
|
|
545
|
+
if (ag.clientId) process.stderr.write(` Client: ${ag.clientId}\n`);
|
|
331
546
|
process.stderr.write("\n");
|
|
332
547
|
return;
|
|
333
548
|
}
|
|
@@ -346,17 +561,6 @@ function registerAgentCommands(program) {
|
|
|
346
561
|
process.stderr.write("\n");
|
|
347
562
|
}
|
|
348
563
|
} 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
564
|
fail("STATUS_ERROR", error instanceof Error ? error.message : String(error));
|
|
361
565
|
}
|
|
362
566
|
});
|
|
@@ -365,7 +569,7 @@ function registerAgentCommands(program) {
|
|
|
365
569
|
const serverUrl = resolveServerUrl(options.server);
|
|
366
570
|
const response = await fetch(`${serverUrl}/api/v1/admin/agents/activity/${name}/reset-activity`, {
|
|
367
571
|
method: "POST",
|
|
368
|
-
headers: { Authorization: `Bearer ${await
|
|
572
|
+
headers: { Authorization: `Bearer ${await ensureFreshAccessToken()}` },
|
|
369
573
|
signal: AbortSignal.timeout(1e4)
|
|
370
574
|
});
|
|
371
575
|
if (!response.ok) fail("RESET_ERROR", `Server returned ${response.status}`, 1);
|
|
@@ -377,7 +581,7 @@ function registerAgentCommands(program) {
|
|
|
377
581
|
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
582
|
try {
|
|
379
583
|
const serverUrl = resolveServerUrl(options.server);
|
|
380
|
-
const adminToken = await
|
|
584
|
+
const adminToken = await ensureFreshAccessToken();
|
|
381
585
|
const agentId = (await resolveAgent(serverUrl, adminToken, agentName)).uuid;
|
|
382
586
|
const qs = options.state ? `?state=${options.state}` : "";
|
|
383
587
|
const response = await fetch(`${serverUrl}/api/v1/admin/sessions/agents/${agentId}${qs}`, {
|
|
@@ -411,7 +615,7 @@ function registerAgentCommands(program) {
|
|
|
411
615
|
]) sessionCmd.command(`${cmd} <agent-name> <chat-id>`).description(desc).option("--server <url>", "Hub server URL").action(async (agentName, chatId, options) => {
|
|
412
616
|
try {
|
|
413
617
|
const serverUrl = resolveServerUrl(options.server);
|
|
414
|
-
const adminToken = await
|
|
618
|
+
const adminToken = await ensureFreshAccessToken();
|
|
415
619
|
const agentId = (await resolveAgent(serverUrl, adminToken, agentName)).uuid;
|
|
416
620
|
const response = await fetch(`${serverUrl}/api/v1/admin/sessions/agents/${agentId}/${chatId}/${cmd}`, {
|
|
417
621
|
method: "POST",
|
|
@@ -427,10 +631,10 @@ function registerAgentCommands(program) {
|
|
|
427
631
|
fail("SESSION_CMD_ERROR", error instanceof Error ? error.message : String(error));
|
|
428
632
|
}
|
|
429
633
|
});
|
|
430
|
-
agent.command("chat <agent-name>").description("Open an interactive chat with an agent (as
|
|
634
|
+
agent.command("chat <agent-name>").description("Open an interactive chat with an agent (as the current member's human agent)").option("--server <url>", "Hub server URL").action(async (agentName, options) => {
|
|
431
635
|
try {
|
|
432
636
|
const serverUrl = resolveServerUrl(options.server);
|
|
433
|
-
const adminToken = await
|
|
637
|
+
const adminToken = await ensureFreshAccessToken();
|
|
434
638
|
const headers = {
|
|
435
639
|
Authorization: `Bearer ${adminToken}`,
|
|
436
640
|
"Content-Type": "application/json"
|
|
@@ -516,16 +720,16 @@ function registerAgentCommands(program) {
|
|
|
516
720
|
fail("CHAT_ERROR", error instanceof Error ? error.message : String(error));
|
|
517
721
|
}
|
|
518
722
|
});
|
|
519
|
-
agent.command("register").description("Register this agent and return identity info").action(async () => {
|
|
723
|
+
agent.command("register").description("Register this agent and return identity info").option("--agent <name>", "Local agent alias (default: first configured)").action(async (options) => {
|
|
520
724
|
try {
|
|
521
|
-
success(await createSdk().register());
|
|
725
|
+
success(await createSdk(options.agent).register());
|
|
522
726
|
} catch (error) {
|
|
523
727
|
handleSdkError(error);
|
|
524
728
|
}
|
|
525
729
|
});
|
|
526
|
-
agent.command("pull").description("Pull pending messages from inbox").option("-l, --limit <number>", "Maximum entries to return", "10").option("-a, --ack", "Automatically ACK entries after pulling").action(async (options) => {
|
|
730
|
+
agent.command("pull").description("Pull pending messages from inbox").option("-l, --limit <number>", "Maximum entries to return", "10").option("-a, --ack", "Automatically ACK entries after pulling").option("--agent <name>", "Local agent alias (default: first configured)").action(async (options) => {
|
|
527
731
|
try {
|
|
528
|
-
const sdk = createSdk();
|
|
732
|
+
const sdk = createSdk(options.agent);
|
|
529
733
|
const limit = parseLimit(options.limit, 50);
|
|
530
734
|
const result = await sdk.pull(limit);
|
|
531
735
|
if (options.ack && result.entries.length > 0) await Promise.all(result.entries.map((entry) => sdk.ack(entry.id)));
|
|
@@ -536,9 +740,94 @@ function registerAgentCommands(program) {
|
|
|
536
740
|
});
|
|
537
741
|
}
|
|
538
742
|
//#endregion
|
|
743
|
+
//#region src/commands/connect.ts
|
|
744
|
+
/**
|
|
745
|
+
* Authenticate via connect token — exchange for full JWT credentials.
|
|
746
|
+
*/
|
|
747
|
+
async function authenticateWithToken(url, token) {
|
|
748
|
+
const res = await fetch(`${url}/api/v1/auth/connect-token`, {
|
|
749
|
+
method: "POST",
|
|
750
|
+
headers: { "Content-Type": "application/json" },
|
|
751
|
+
body: JSON.stringify({ token }),
|
|
752
|
+
signal: AbortSignal.timeout(1e4)
|
|
753
|
+
});
|
|
754
|
+
if (!res.ok) fail("AUTH_ERROR", (await res.json().catch(() => ({}))).error ?? `Token exchange failed (HTTP ${res.status})`, 1);
|
|
755
|
+
return await res.json();
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Authenticate via interactive username/password login.
|
|
759
|
+
*/
|
|
760
|
+
async function authenticateInteractive(url) {
|
|
761
|
+
process.stderr.write("\n Log in to Hub:\n");
|
|
762
|
+
const username = await input({ message: " Username:" });
|
|
763
|
+
const pw = await password({ message: " Password:" });
|
|
764
|
+
const loginRes = await fetch(`${url}/api/v1/auth/login`, {
|
|
765
|
+
method: "POST",
|
|
766
|
+
headers: { "Content-Type": "application/json" },
|
|
767
|
+
body: JSON.stringify({
|
|
768
|
+
username,
|
|
769
|
+
password: pw
|
|
770
|
+
}),
|
|
771
|
+
signal: AbortSignal.timeout(1e4)
|
|
772
|
+
});
|
|
773
|
+
if (!loginRes.ok) fail("AUTH_ERROR", (await loginRes.json().catch(() => ({}))).error ?? `Login failed (HTTP ${loginRes.status})`, 1);
|
|
774
|
+
return await loginRes.json();
|
|
775
|
+
}
|
|
776
|
+
function registerConnectCommand(parent) {
|
|
777
|
+
parent.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) => {
|
|
778
|
+
try {
|
|
779
|
+
const url = serverUrl.replace(/\/+$/, "");
|
|
780
|
+
setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", url);
|
|
781
|
+
process.stderr.write(`\n \u2713 Server configured: ${url}\n`);
|
|
782
|
+
saveCredentials({
|
|
783
|
+
...options.token ? await authenticateWithToken(url, options.token) : await authenticateInteractive(url),
|
|
784
|
+
serverUrl: url
|
|
785
|
+
});
|
|
786
|
+
process.stderr.write(" ✓ Authenticated\n");
|
|
787
|
+
resetConfig();
|
|
788
|
+
resetConfigMeta();
|
|
789
|
+
const config = await initConfig({
|
|
790
|
+
schema: clientConfigSchema,
|
|
791
|
+
role: "client"
|
|
792
|
+
});
|
|
793
|
+
const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
|
|
794
|
+
const agents = loadAgents({
|
|
795
|
+
schema: agentConfigSchema,
|
|
796
|
+
agentsDir
|
|
797
|
+
});
|
|
798
|
+
process.stderr.write(`\n Starting client (id: ${config.client.id})...\n`);
|
|
799
|
+
const runtime = new ClientRuntime(config.server.url, config.client.id);
|
|
800
|
+
for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
|
|
801
|
+
await runtime.start();
|
|
802
|
+
runtime.watchAgentsDir(agentsDir);
|
|
803
|
+
const shutdown = async () => {
|
|
804
|
+
process.stderr.write("\n Shutting down...\n");
|
|
805
|
+
runtime.unwatchAgentsDir();
|
|
806
|
+
await runtime.stop();
|
|
807
|
+
process.exit(0);
|
|
808
|
+
};
|
|
809
|
+
process.on("SIGINT", () => void shutdown());
|
|
810
|
+
process.on("SIGTERM", () => void shutdown());
|
|
811
|
+
await new Promise(() => {});
|
|
812
|
+
} catch (error) {
|
|
813
|
+
if (error.name === "ExitPromptError") {
|
|
814
|
+
process.stderr.write("\n Cancelled.\n");
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
818
|
+
process.stderr.write(` Error: ${msg}\n`);
|
|
819
|
+
process.exit(1);
|
|
820
|
+
} finally {
|
|
821
|
+
resetConfig();
|
|
822
|
+
resetConfigMeta();
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
//#endregion
|
|
539
827
|
//#region src/commands/client.ts
|
|
540
828
|
function registerClientCommands(program) {
|
|
541
829
|
const client = program.command("client").description("Client runtime — connect agents to the server");
|
|
830
|
+
registerConnectCommand(client);
|
|
542
831
|
client.command("start").description("Start client — connect all configured agents to the server").option("--no-interactive", "Skip interactive prompts (for Docker/CI)").action(async (options) => {
|
|
543
832
|
try {
|
|
544
833
|
await promptMissingFields({
|
|
@@ -555,8 +844,8 @@ function registerClientCommands(program) {
|
|
|
555
844
|
schema: agentConfigSchema,
|
|
556
845
|
agentsDir
|
|
557
846
|
});
|
|
558
|
-
process.stderr.write(`\n Connecting to ${config.server.url}...\n`);
|
|
559
|
-
const runtime = new ClientRuntime(config.server.url);
|
|
847
|
+
process.stderr.write(`\n Connecting to ${config.server.url} (client id: ${config.client.id})...\n`);
|
|
848
|
+
const runtime = new ClientRuntime(config.server.url, config.client.id);
|
|
560
849
|
for (const [name, agentConfig] of agents) runtime.addAgent(name, agentConfig);
|
|
561
850
|
await runtime.start();
|
|
562
851
|
runtime.watchAgentsDir(agentsDir);
|
|
@@ -585,7 +874,6 @@ function registerClientCommands(program) {
|
|
|
585
874
|
checkClientConfig(),
|
|
586
875
|
await checkServerReachable(),
|
|
587
876
|
checkAgentConfigs(),
|
|
588
|
-
await checkAgentTokens(),
|
|
589
877
|
await checkWebSocket()
|
|
590
878
|
]);
|
|
591
879
|
});
|
|
@@ -605,7 +893,7 @@ function registerClientCommands(program) {
|
|
|
605
893
|
return;
|
|
606
894
|
}
|
|
607
895
|
process.stderr.write("\n Configured agents:\n\n");
|
|
608
|
-
for (const [name, config] of agents) process.stderr.write(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)}
|
|
896
|
+
for (const [name, config] of agents) process.stderr.write(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)} agentId: ${config.agentId}\n`);
|
|
609
897
|
process.stderr.write("\n");
|
|
610
898
|
} catch {
|
|
611
899
|
process.stderr.write(" No agents directory found.\n");
|
|
@@ -614,7 +902,7 @@ function registerClientCommands(program) {
|
|
|
614
902
|
client.command("hub-list").description("List connected clients on the Hub server").option("--server <url>", "Hub server URL").action(async (options) => {
|
|
615
903
|
try {
|
|
616
904
|
const serverUrl = resolveServerUrl(options.server);
|
|
617
|
-
const token = await
|
|
905
|
+
const token = await ensureFreshAccessToken();
|
|
618
906
|
const response = await fetch(`${serverUrl}/api/v1/admin/clients`, {
|
|
619
907
|
headers: { Authorization: `Bearer ${token}` },
|
|
620
908
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -641,7 +929,7 @@ function registerClientCommands(program) {
|
|
|
641
929
|
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
930
|
try {
|
|
643
931
|
const serverUrl = resolveServerUrl(options.server);
|
|
644
|
-
const token = await
|
|
932
|
+
const token = await ensureFreshAccessToken();
|
|
645
933
|
const response = await fetch(`${serverUrl}/api/v1/admin/clients/${clientId}/disconnect`, {
|
|
646
934
|
method: "POST",
|
|
647
935
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -758,119 +1046,19 @@ function isSecretField(schema, dotPath) {
|
|
|
758
1046
|
return false;
|
|
759
1047
|
}
|
|
760
1048
|
//#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
|
|
845
1049
|
//#region src/commands/onboard.ts
|
|
846
1050
|
async function promptMissing(args) {
|
|
847
1051
|
if (!args.server) try {
|
|
848
|
-
const { resolveServerUrl } = await import("../bootstrap-
|
|
1052
|
+
const { resolveServerUrl } = await import("../bootstrap-DNL1cEwv.mjs").then((n) => n.t);
|
|
849
1053
|
resolveServerUrl();
|
|
850
1054
|
} catch {
|
|
851
1055
|
args.server = await input({ message: "Hub server URL:" });
|
|
852
1056
|
saveOnboardState(args);
|
|
853
1057
|
}
|
|
854
|
-
const {
|
|
855
|
-
|
|
856
|
-
try {
|
|
857
|
-
const res = await fetch(`${serverUrl}/api/v1/bootstrap/config`);
|
|
858
|
-
if (res.ok) {
|
|
859
|
-
if (!(await res.json()).allowedOrg) throw new Error("Server does not have FIRST_TREE_HUB_GITHUB_ALLOWED_ORG configured.\n Ask the server admin to set this before onboarding.");
|
|
860
|
-
}
|
|
861
|
-
} catch (err) {
|
|
862
|
-
if (err instanceof Error && err.message.includes("FIRST_TREE_HUB_GITHUB_ALLOWED_ORG")) throw err;
|
|
863
|
-
}
|
|
864
|
-
let ghUsername = null;
|
|
865
|
-
try {
|
|
866
|
-
const { getGitHubUsername } = await import("../bootstrap-Dq_k_6ZD.mjs").then((n) => n.n);
|
|
867
|
-
ghUsername = getGitHubUsername();
|
|
868
|
-
} catch {}
|
|
1058
|
+
const { loadCredentials } = await import("../bootstrap-DNL1cEwv.mjs").then((n) => n.t);
|
|
1059
|
+
if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
|
|
869
1060
|
if (!args.id) {
|
|
870
|
-
args.id = await input({
|
|
871
|
-
message: "Agent ID:",
|
|
872
|
-
default: ghUsername ?? void 0
|
|
873
|
-
});
|
|
1061
|
+
args.id = await input({ message: "Agent ID:" });
|
|
874
1062
|
saveOnboardState(args);
|
|
875
1063
|
}
|
|
876
1064
|
if (!args.type) {
|
|
@@ -893,6 +1081,11 @@ async function promptMissing(args) {
|
|
|
893
1081
|
});
|
|
894
1082
|
saveOnboardState(args);
|
|
895
1083
|
}
|
|
1084
|
+
if (args.type !== "human" && args.clientId === void 0) {
|
|
1085
|
+
args.clientId = await input({ message: "Client ID (Enter to leave unbound — first WS connect will claim it):" });
|
|
1086
|
+
if (!args.clientId) args.clientId = void 0;
|
|
1087
|
+
saveOnboardState(args);
|
|
1088
|
+
}
|
|
896
1089
|
if (!args.role) {
|
|
897
1090
|
const role = await input({ message: "Role (optional, Enter to skip):" });
|
|
898
1091
|
if (role) {
|
|
@@ -923,6 +1116,7 @@ async function promptMissing(args) {
|
|
|
923
1116
|
message: "Assistant ID:",
|
|
924
1117
|
default: `${args.id}-assistant`
|
|
925
1118
|
});
|
|
1119
|
+
if (args.clientId === void 0) args.clientId = await input({ message: "Client ID for the assistant (Enter to leave unbound):" }) || void 0;
|
|
926
1120
|
saveOnboardState(args);
|
|
927
1121
|
}
|
|
928
1122
|
}
|
|
@@ -938,16 +1132,16 @@ async function promptMissing(args) {
|
|
|
938
1132
|
}
|
|
939
1133
|
}
|
|
940
1134
|
function registerOnboardCommand(program) {
|
|
941
|
-
program.command("onboard").description("Onboard a new agent to First Tree Hub").option("--id <id>", "Agent ID").option("--type <type>", "Agent type: human | personal_assistant | autonomous_agent").option("--display-name <name>", "Display name (defaults to id)").option("--role <role>", "Role description").option("--domains <domains>", "Comma-separated domains").option("--
|
|
1135
|
+
program.command("onboard").description("Onboard a new agent to First Tree Hub").option("--id <id>", "Agent ID").option("--type <type>", "Agent type: human | personal_assistant | autonomous_agent").option("--client-id <id>", "Client (machine) to pin a non-human agent to").option("--display-name <name>", "Display name (defaults to id)").option("--role <role>", "Role description").option("--domains <domains>", "Comma-separated domains").option("--assistant <id>", "Also create a personal_assistant with this ID").option("--delegate-mention <id>", "Set delegate_mention field").option("--server <url>", "Hub server URL").option("--feishu-bot-app-id <id>", "Feishu bot App ID").option("--feishu-bot-app-secret <secret>", "Feishu bot App Secret").option("--check", "Dry-run: show readiness checklist without executing").action(async (options) => {
|
|
942
1136
|
try {
|
|
943
1137
|
const args = {
|
|
944
1138
|
...loadOnboardState() ?? {},
|
|
945
1139
|
...options.id && { id: options.id },
|
|
946
1140
|
...options.type && { type: options.type },
|
|
1141
|
+
...options.clientId && { clientId: options.clientId },
|
|
947
1142
|
...options.displayName && { displayName: options.displayName },
|
|
948
1143
|
...options.role && { role: options.role },
|
|
949
1144
|
...options.domains && { domains: options.domains },
|
|
950
|
-
...options.profile && { profile: options.profile },
|
|
951
1145
|
...options.assistant && { assistant: options.assistant },
|
|
952
1146
|
...options.delegateMention && { delegateMention: options.delegateMention },
|
|
953
1147
|
...options.server && { server: options.server },
|
|
@@ -1129,7 +1323,6 @@ registerClientCommands(program);
|
|
|
1129
1323
|
registerAgentCommands(program);
|
|
1130
1324
|
registerConfigCommands(program);
|
|
1131
1325
|
registerStatusCommand(program);
|
|
1132
|
-
registerConnectCommand(program);
|
|
1133
1326
|
registerOnboardCommand(program);
|
|
1134
1327
|
program.parse();
|
|
1135
1328
|
//#endregion
|