@agent-team-foundation/first-tree-hub 0.6.0 → 0.6.2
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-BnlTKa0H.mjs → bootstrap-DW7aIpmE.mjs} +50 -133
- package/dist/cli/index.mjs +319 -117
- package/dist/{core-B9bH7EjM.mjs → core-RXUUKkCO.mjs} +5008 -3951
- package/dist/drizzle/0018_agent_visibility.sql +13 -0
- package/dist/drizzle/0019_agent_configs.sql +30 -0
- package/dist/drizzle/0020_unified_user_token.sql +148 -0
- package/dist/drizzle/0021_drop_agents_profile.sql +10 -0
- package/dist/drizzle/meta/0018_snapshot.json +1938 -0
- package/dist/drizzle/meta/_journal.json +28 -0
- package/dist/feishu-BZ8pnMrQ.mjs +832 -0
- package/dist/index.mjs +4 -4
- package/dist/web/assets/index-BMOr9-X2.js +308 -0
- package/dist/web/assets/index-CTl4pHIL.css +1 -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-C_FKYVro.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-DW7aIpmE.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-RXUUKkCO.mjs";
|
|
4
|
+
import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-BZ8pnMrQ.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,24 +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
|
-
|
|
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);
|
|
35
240
|
let serverUrl;
|
|
36
241
|
try {
|
|
37
|
-
serverUrl = resolveServerUrl(process.env.
|
|
38
|
-
} catch {
|
|
39
|
-
|
|
242
|
+
serverUrl = resolveServerUrl(process.env.FIRST_TREE_HUB_SERVER_URL);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
fail("MISSING_SERVER_URL", error instanceof Error ? error.message : String(error), 2);
|
|
40
245
|
}
|
|
41
246
|
return {
|
|
42
247
|
serverUrl,
|
|
43
|
-
|
|
248
|
+
agentId: cfg.agentId
|
|
44
249
|
};
|
|
45
250
|
}
|
|
46
|
-
function createSdk() {
|
|
47
|
-
|
|
251
|
+
function createSdk(agentName) {
|
|
252
|
+
const { serverUrl, agentId } = resolveLocalAgent(agentName);
|
|
253
|
+
return new FirstTreeHubSDK({
|
|
254
|
+
serverUrl,
|
|
255
|
+
getAccessToken: () => ensureFreshAccessToken(),
|
|
256
|
+
agentId
|
|
257
|
+
});
|
|
48
258
|
}
|
|
49
259
|
function handleSdkError(error) {
|
|
50
260
|
if (error instanceof SdkError) {
|
|
@@ -78,10 +288,6 @@ function readStdin() {
|
|
|
78
288
|
process.stdin.on("error", reject);
|
|
79
289
|
});
|
|
80
290
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Resolve an agent name (or UUID) to its record via the admin agents API.
|
|
83
|
-
* Accepts either a name or a UUID; throws via fail() if not found.
|
|
84
|
-
*/
|
|
85
291
|
async function resolveAgent(serverUrl, adminToken, agentName) {
|
|
86
292
|
const res = await fetch(`${serverUrl}/api/v1/admin/agents?limit=100`, {
|
|
87
293
|
headers: { Authorization: `Bearer ${adminToken}` },
|
|
@@ -93,22 +299,24 @@ async function resolveAgent(serverUrl, adminToken, agentName) {
|
|
|
93
299
|
return found;
|
|
94
300
|
}
|
|
95
301
|
function registerAgentCommands(program) {
|
|
96
|
-
const agent = program.command("agent").description("Agent management — config,
|
|
97
|
-
|
|
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) => {
|
|
98
305
|
try {
|
|
99
306
|
let agentName = name;
|
|
100
|
-
let
|
|
101
|
-
if (!agentName || !
|
|
307
|
+
let agentId = options?.agentId;
|
|
308
|
+
if (!agentName || !agentId) {
|
|
102
309
|
const result = await promptAddAgent();
|
|
103
310
|
agentName = agentName ?? result.name;
|
|
104
|
-
|
|
311
|
+
agentId = agentId ?? result.agentId;
|
|
105
312
|
}
|
|
313
|
+
if (!agentName || !agentId) fail("MISSING_AGENT_ARGS", "Both agent name and agent-id are required.", 2);
|
|
106
314
|
const agentDir = join(DEFAULT_CONFIG_DIR, "agents", agentName);
|
|
107
315
|
mkdirSync(agentDir, {
|
|
108
316
|
recursive: true,
|
|
109
317
|
mode: 448
|
|
110
318
|
});
|
|
111
|
-
setConfigValue(join(agentDir, "agent.yaml"), "
|
|
319
|
+
setConfigValue(join(agentDir, "agent.yaml"), "agentId", agentId);
|
|
112
320
|
process.stderr.write(` Agent "${agentName}" added.\n`);
|
|
113
321
|
process.stderr.write(` Config: ${join(agentDir, "agent.yaml")}\n`);
|
|
114
322
|
} catch (error) {
|
|
@@ -121,7 +329,7 @@ function registerAgentCommands(program) {
|
|
|
121
329
|
process.exit(1);
|
|
122
330
|
}
|
|
123
331
|
});
|
|
124
|
-
agent.command("remove <name>").description("Remove
|
|
332
|
+
agent.command("remove <name>").description("Remove a local agent alias and its runtime data").action((name) => {
|
|
125
333
|
const agentDir = join(DEFAULT_CONFIG_DIR, "agents", name);
|
|
126
334
|
if (!existsSync(agentDir)) {
|
|
127
335
|
process.stderr.write(` Agent "${name}" not found.\n`);
|
|
@@ -138,7 +346,7 @@ function registerAgentCommands(program) {
|
|
|
138
346
|
rmSync(join(DEFAULT_DATA_DIR, "sessions", `${name}.json`), { force: true });
|
|
139
347
|
process.stderr.write(` Agent "${name}" removed.\n`);
|
|
140
348
|
});
|
|
141
|
-
agent.command("list").description("List configured agents").action(() => {
|
|
349
|
+
agent.command("list").description("List locally-configured agents").action(() => {
|
|
142
350
|
const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
|
|
143
351
|
try {
|
|
144
352
|
const agents = loadAgents({
|
|
@@ -149,21 +357,22 @@ function registerAgentCommands(program) {
|
|
|
149
357
|
process.stderr.write(" No agents configured.\n");
|
|
150
358
|
return;
|
|
151
359
|
}
|
|
152
|
-
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`);
|
|
153
361
|
} catch {
|
|
154
362
|
process.stderr.write(" No agents configured.\n");
|
|
155
363
|
}
|
|
156
364
|
});
|
|
157
|
-
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 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) => {
|
|
158
366
|
try {
|
|
159
367
|
const serverUrl = resolveServerUrl(options.server);
|
|
160
368
|
const headers = {
|
|
161
|
-
Authorization: `Bearer ${await
|
|
369
|
+
Authorization: `Bearer ${await ensureFreshAccessToken()}`,
|
|
162
370
|
"Content-Type": "application/json"
|
|
163
371
|
};
|
|
164
372
|
const createBody = {
|
|
165
373
|
name,
|
|
166
|
-
type: options.type
|
|
374
|
+
type: options.type,
|
|
375
|
+
clientId: options.clientId
|
|
167
376
|
};
|
|
168
377
|
if (options.displayName) createBody.displayName = options.displayName;
|
|
169
378
|
const createRes = await fetch(`${serverUrl}/api/v1/admin/agents`, {
|
|
@@ -173,20 +382,39 @@ function registerAgentCommands(program) {
|
|
|
173
382
|
signal: AbortSignal.timeout(1e4)
|
|
174
383
|
});
|
|
175
384
|
if (!createRes.ok) fail("CREATE_ERROR", (await createRes.json().catch(() => ({}))).error ?? `Failed to create agent (HTTP ${createRes.status})`, 1);
|
|
176
|
-
const
|
|
177
|
-
process.stderr.write(` \u2713 Agent created: ${
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 }),
|
|
182
412
|
signal: AbortSignal.timeout(1e4)
|
|
183
413
|
});
|
|
184
|
-
if (!
|
|
185
|
-
|
|
186
|
-
process.stderr.write(` \u2713 Token saved: ${agentDir}/agent.yaml\n`);
|
|
187
|
-
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`);
|
|
188
416
|
} catch (error) {
|
|
189
|
-
fail("
|
|
417
|
+
fail("CLAIM_ERROR", error instanceof Error ? error.message : String(error));
|
|
190
418
|
}
|
|
191
419
|
});
|
|
192
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) => {
|
|
@@ -211,24 +439,13 @@ function registerAgentCommands(program) {
|
|
|
211
439
|
}
|
|
212
440
|
process.stderr.write(` ${totalRemoved} workspace(s) cleaned.\n`);
|
|
213
441
|
});
|
|
214
|
-
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) => {
|
|
215
|
-
try {
|
|
216
|
-
const result = await bootstrapToken(resolveServerUrl(options.server), agentName, { saveTo: options.saveTo });
|
|
217
|
-
if (options.saveTo === "agent") process.stderr.write(`Token saved to ${DEFAULT_HOME_DIR}/config/agents/${agentName}/agent.yaml\n`);
|
|
218
|
-
else process.stderr.write(`Token saved to ${options.saveTo}\n`);
|
|
219
|
-
success({
|
|
220
|
-
agentId: result.agentId,
|
|
221
|
-
tokenSaved: true
|
|
222
|
-
});
|
|
223
|
-
} catch (error) {
|
|
224
|
-
fail("BOOTSTRAP_ERROR", error instanceof Error ? error.message : String(error));
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
442
|
const bind = agent.command("bind").description("Bind external IM accounts to agents");
|
|
228
|
-
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) => {
|
|
229
444
|
try {
|
|
230
445
|
if (options.platform !== "feishu") fail("UNSUPPORTED_PLATFORM", `Platform "${options.platform}" is not supported. Use "feishu".`);
|
|
231
|
-
|
|
446
|
+
const serverUrl = resolveServerUrl(options.server);
|
|
447
|
+
const { agentId } = resolveLocalAgent(options.agent);
|
|
448
|
+
await bindFeishuBot(serverUrl, await ensureFreshAccessToken(), agentId, options.appId, options.appSecret);
|
|
232
449
|
process.stderr.write("Feishu bot bound successfully.\n");
|
|
233
450
|
success({
|
|
234
451
|
platform: "feishu",
|
|
@@ -238,10 +455,12 @@ function registerAgentCommands(program) {
|
|
|
238
455
|
fail("BIND_BOT_ERROR", error instanceof Error ? error.message : String(error));
|
|
239
456
|
}
|
|
240
457
|
});
|
|
241
|
-
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) => {
|
|
242
459
|
try {
|
|
243
460
|
if (options.platform !== "feishu") fail("UNSUPPORTED_PLATFORM", `Platform "${options.platform}" is not supported. Use "feishu".`);
|
|
244
|
-
|
|
461
|
+
const serverUrl = resolveServerUrl(options.server);
|
|
462
|
+
const { agentId } = resolveLocalAgent(options.agent);
|
|
463
|
+
await bindFeishuUser(serverUrl, await ensureFreshAccessToken(), agentId, humanAgentId, options.feishuId);
|
|
245
464
|
process.stderr.write(`Feishu user ${options.feishuId} bound to ${humanAgentId}.\n`);
|
|
246
465
|
success({
|
|
247
466
|
platform: "feishu",
|
|
@@ -252,7 +471,7 @@ function registerAgentCommands(program) {
|
|
|
252
471
|
fail("BIND_USER_ERROR", error instanceof Error ? error.message : String(error));
|
|
253
472
|
}
|
|
254
473
|
});
|
|
255
|
-
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) => {
|
|
256
475
|
try {
|
|
257
476
|
const content = message ?? await readStdin();
|
|
258
477
|
if (!content) fail("NO_MESSAGE", "No message provided. Pass as argument or pipe via stdin.", 2);
|
|
@@ -262,7 +481,7 @@ function registerAgentCommands(program) {
|
|
|
262
481
|
} catch {
|
|
263
482
|
fail("INVALID_METADATA", "Metadata must be valid JSON.", 2);
|
|
264
483
|
}
|
|
265
|
-
const sdk = createSdk();
|
|
484
|
+
const sdk = createSdk(options.agent);
|
|
266
485
|
if (options.chat) success(await sdk.sendMessage(target, {
|
|
267
486
|
format: options.format,
|
|
268
487
|
content,
|
|
@@ -282,10 +501,10 @@ function registerAgentCommands(program) {
|
|
|
282
501
|
handleSdkError(error);
|
|
283
502
|
}
|
|
284
503
|
});
|
|
285
|
-
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) => {
|
|
286
505
|
try {
|
|
287
506
|
const limit = parseLimit(options.limit, 100);
|
|
288
|
-
success(await createSdk().listChats({
|
|
507
|
+
success(await createSdk(options.agent).listChats({
|
|
289
508
|
limit,
|
|
290
509
|
cursor: options.cursor
|
|
291
510
|
}));
|
|
@@ -293,10 +512,10 @@ function registerAgentCommands(program) {
|
|
|
293
512
|
handleSdkError(error);
|
|
294
513
|
}
|
|
295
514
|
});
|
|
296
|
-
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) => {
|
|
297
516
|
try {
|
|
298
517
|
const limit = parseLimit(options.limit, 100);
|
|
299
|
-
success(await createSdk().listMessages(chatId, {
|
|
518
|
+
success(await createSdk(options.agent).listMessages(chatId, {
|
|
300
519
|
limit,
|
|
301
520
|
cursor: options.cursor
|
|
302
521
|
}));
|
|
@@ -308,22 +527,22 @@ function registerAgentCommands(program) {
|
|
|
308
527
|
try {
|
|
309
528
|
const serverUrl = resolveServerUrl(options?.server);
|
|
310
529
|
const response = await fetch(`${serverUrl}/api/v1/admin/agents/activity`, {
|
|
311
|
-
headers: { Authorization: `Bearer ${await
|
|
530
|
+
headers: { Authorization: `Bearer ${await ensureFreshAccessToken()}` },
|
|
312
531
|
signal: AbortSignal.timeout(1e4)
|
|
313
532
|
});
|
|
314
533
|
if (!response.ok) fail("FETCH_ERROR", `Server returned ${response.status}`, 1);
|
|
315
534
|
const data = await response.json();
|
|
316
535
|
if (name) {
|
|
317
|
-
const
|
|
318
|
-
if (!
|
|
536
|
+
const ag = data.agents.find((a) => a.agentId === name);
|
|
537
|
+
if (!ag) {
|
|
319
538
|
process.stderr.write(`\n Agent "${name}" is not running.\n\n`);
|
|
320
539
|
return;
|
|
321
540
|
}
|
|
322
|
-
process.stderr.write(`\n Agent: ${
|
|
323
|
-
process.stderr.write(` Runtime: ${
|
|
324
|
-
process.stderr.write(` State: ${
|
|
325
|
-
if (
|
|
326
|
-
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`);
|
|
327
546
|
process.stderr.write("\n");
|
|
328
547
|
return;
|
|
329
548
|
}
|
|
@@ -342,17 +561,6 @@ function registerAgentCommands(program) {
|
|
|
342
561
|
process.stderr.write("\n");
|
|
343
562
|
}
|
|
344
563
|
} catch (error) {
|
|
345
|
-
if (error instanceof Error && error.message.includes("ADMIN_TOKEN")) {
|
|
346
|
-
try {
|
|
347
|
-
const me = await createSdk().register();
|
|
348
|
-
process.stderr.write(`\n Agent: ${me.agentId} (${me.displayName ?? "no name"})\n`);
|
|
349
|
-
process.stderr.write(` Type: ${me.type}\n`);
|
|
350
|
-
process.stderr.write(` Status: ${me.status}\n\n`);
|
|
351
|
-
} catch (sdkError) {
|
|
352
|
-
handleSdkError(sdkError);
|
|
353
|
-
}
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
564
|
fail("STATUS_ERROR", error instanceof Error ? error.message : String(error));
|
|
357
565
|
}
|
|
358
566
|
});
|
|
@@ -361,7 +569,7 @@ function registerAgentCommands(program) {
|
|
|
361
569
|
const serverUrl = resolveServerUrl(options.server);
|
|
362
570
|
const response = await fetch(`${serverUrl}/api/v1/admin/agents/activity/${name}/reset-activity`, {
|
|
363
571
|
method: "POST",
|
|
364
|
-
headers: { Authorization: `Bearer ${await
|
|
572
|
+
headers: { Authorization: `Bearer ${await ensureFreshAccessToken()}` },
|
|
365
573
|
signal: AbortSignal.timeout(1e4)
|
|
366
574
|
});
|
|
367
575
|
if (!response.ok) fail("RESET_ERROR", `Server returned ${response.status}`, 1);
|
|
@@ -373,7 +581,7 @@ function registerAgentCommands(program) {
|
|
|
373
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) => {
|
|
374
582
|
try {
|
|
375
583
|
const serverUrl = resolveServerUrl(options.server);
|
|
376
|
-
const adminToken = await
|
|
584
|
+
const adminToken = await ensureFreshAccessToken();
|
|
377
585
|
const agentId = (await resolveAgent(serverUrl, adminToken, agentName)).uuid;
|
|
378
586
|
const qs = options.state ? `?state=${options.state}` : "";
|
|
379
587
|
const response = await fetch(`${serverUrl}/api/v1/admin/sessions/agents/${agentId}${qs}`, {
|
|
@@ -407,7 +615,7 @@ function registerAgentCommands(program) {
|
|
|
407
615
|
]) sessionCmd.command(`${cmd} <agent-name> <chat-id>`).description(desc).option("--server <url>", "Hub server URL").action(async (agentName, chatId, options) => {
|
|
408
616
|
try {
|
|
409
617
|
const serverUrl = resolveServerUrl(options.server);
|
|
410
|
-
const adminToken = await
|
|
618
|
+
const adminToken = await ensureFreshAccessToken();
|
|
411
619
|
const agentId = (await resolveAgent(serverUrl, adminToken, agentName)).uuid;
|
|
412
620
|
const response = await fetch(`${serverUrl}/api/v1/admin/sessions/agents/${agentId}/${chatId}/${cmd}`, {
|
|
413
621
|
method: "POST",
|
|
@@ -423,10 +631,10 @@ function registerAgentCommands(program) {
|
|
|
423
631
|
fail("SESSION_CMD_ERROR", error instanceof Error ? error.message : String(error));
|
|
424
632
|
}
|
|
425
633
|
});
|
|
426
|
-
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) => {
|
|
427
635
|
try {
|
|
428
636
|
const serverUrl = resolveServerUrl(options.server);
|
|
429
|
-
const adminToken = await
|
|
637
|
+
const adminToken = await ensureFreshAccessToken();
|
|
430
638
|
const headers = {
|
|
431
639
|
Authorization: `Bearer ${adminToken}`,
|
|
432
640
|
"Content-Type": "application/json"
|
|
@@ -512,16 +720,16 @@ function registerAgentCommands(program) {
|
|
|
512
720
|
fail("CHAT_ERROR", error instanceof Error ? error.message : String(error));
|
|
513
721
|
}
|
|
514
722
|
});
|
|
515
|
-
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) => {
|
|
516
724
|
try {
|
|
517
|
-
success(await createSdk().register());
|
|
725
|
+
success(await createSdk(options.agent).register());
|
|
518
726
|
} catch (error) {
|
|
519
727
|
handleSdkError(error);
|
|
520
728
|
}
|
|
521
729
|
});
|
|
522
|
-
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) => {
|
|
523
731
|
try {
|
|
524
|
-
const sdk = createSdk();
|
|
732
|
+
const sdk = createSdk(options.agent);
|
|
525
733
|
const limit = parseLimit(options.limit, 50);
|
|
526
734
|
const result = await sdk.pull(limit);
|
|
527
735
|
if (options.ack && result.entries.length > 0) await Promise.all(result.entries.map((entry) => sdk.ack(entry.id)));
|
|
@@ -581,7 +789,6 @@ function registerClientCommands(program) {
|
|
|
581
789
|
checkClientConfig(),
|
|
582
790
|
await checkServerReachable(),
|
|
583
791
|
checkAgentConfigs(),
|
|
584
|
-
await checkAgentTokens(),
|
|
585
792
|
await checkWebSocket()
|
|
586
793
|
]);
|
|
587
794
|
});
|
|
@@ -601,7 +808,7 @@ function registerClientCommands(program) {
|
|
|
601
808
|
return;
|
|
602
809
|
}
|
|
603
810
|
process.stderr.write("\n Configured agents:\n\n");
|
|
604
|
-
for (const [name, config] of agents) process.stderr.write(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)}
|
|
811
|
+
for (const [name, config] of agents) process.stderr.write(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)} agentId: ${config.agentId}\n`);
|
|
605
812
|
process.stderr.write("\n");
|
|
606
813
|
} catch {
|
|
607
814
|
process.stderr.write(" No agents directory found.\n");
|
|
@@ -610,7 +817,7 @@ function registerClientCommands(program) {
|
|
|
610
817
|
client.command("hub-list").description("List connected clients on the Hub server").option("--server <url>", "Hub server URL").action(async (options) => {
|
|
611
818
|
try {
|
|
612
819
|
const serverUrl = resolveServerUrl(options.server);
|
|
613
|
-
const token = await
|
|
820
|
+
const token = await ensureFreshAccessToken();
|
|
614
821
|
const response = await fetch(`${serverUrl}/api/v1/admin/clients`, {
|
|
615
822
|
headers: { Authorization: `Bearer ${token}` },
|
|
616
823
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -637,7 +844,7 @@ function registerClientCommands(program) {
|
|
|
637
844
|
client.command("hub-disconnect <clientId>").description("Force-disconnect a client from the Hub server").option("--server <url>", "Hub server URL").action(async (clientId, options) => {
|
|
638
845
|
try {
|
|
639
846
|
const serverUrl = resolveServerUrl(options.server);
|
|
640
|
-
const token = await
|
|
847
|
+
const token = await ensureFreshAccessToken();
|
|
641
848
|
const response = await fetch(`${serverUrl}/api/v1/admin/clients/${clientId}/disconnect`, {
|
|
642
849
|
method: "POST",
|
|
643
850
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -841,32 +1048,16 @@ function registerConnectCommand(program) {
|
|
|
841
1048
|
//#region src/commands/onboard.ts
|
|
842
1049
|
async function promptMissing(args) {
|
|
843
1050
|
if (!args.server) try {
|
|
844
|
-
const { resolveServerUrl } = await import("../bootstrap-
|
|
1051
|
+
const { resolveServerUrl } = await import("../bootstrap-DW7aIpmE.mjs").then((n) => n.t);
|
|
845
1052
|
resolveServerUrl();
|
|
846
1053
|
} catch {
|
|
847
1054
|
args.server = await input({ message: "Hub server URL:" });
|
|
848
1055
|
saveOnboardState(args);
|
|
849
1056
|
}
|
|
850
|
-
const {
|
|
851
|
-
|
|
852
|
-
try {
|
|
853
|
-
const res = await fetch(`${serverUrl}/api/v1/bootstrap/config`);
|
|
854
|
-
if (res.ok) {
|
|
855
|
-
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.");
|
|
856
|
-
}
|
|
857
|
-
} catch (err) {
|
|
858
|
-
if (err instanceof Error && err.message.includes("FIRST_TREE_HUB_GITHUB_ALLOWED_ORG")) throw err;
|
|
859
|
-
}
|
|
860
|
-
let ghUsername = null;
|
|
861
|
-
try {
|
|
862
|
-
const { getGitHubUsername } = await import("../bootstrap-BnlTKa0H.mjs").then((n) => n.n);
|
|
863
|
-
ghUsername = getGitHubUsername();
|
|
864
|
-
} catch {}
|
|
1057
|
+
const { loadCredentials } = await import("../bootstrap-DW7aIpmE.mjs").then((n) => n.t);
|
|
1058
|
+
if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub connect <server-url>` before onboarding.");
|
|
865
1059
|
if (!args.id) {
|
|
866
|
-
args.id = await input({
|
|
867
|
-
message: "Agent ID:",
|
|
868
|
-
default: ghUsername ?? void 0
|
|
869
|
-
});
|
|
1060
|
+
args.id = await input({ message: "Agent ID:" });
|
|
870
1061
|
saveOnboardState(args);
|
|
871
1062
|
}
|
|
872
1063
|
if (!args.type) {
|
|
@@ -889,6 +1080,13 @@ async function promptMissing(args) {
|
|
|
889
1080
|
});
|
|
890
1081
|
saveOnboardState(args);
|
|
891
1082
|
}
|
|
1083
|
+
if (args.type !== "human" && !args.clientId) {
|
|
1084
|
+
args.clientId = await input({
|
|
1085
|
+
message: "Client ID (machine that will run this agent — must be owned by you):",
|
|
1086
|
+
validate: (v) => v.length > 0 ? true : "clientId is required for non-human agents"
|
|
1087
|
+
});
|
|
1088
|
+
saveOnboardState(args);
|
|
1089
|
+
}
|
|
892
1090
|
if (!args.role) {
|
|
893
1091
|
const role = await input({ message: "Role (optional, Enter to skip):" });
|
|
894
1092
|
if (role) {
|
|
@@ -919,6 +1117,10 @@ async function promptMissing(args) {
|
|
|
919
1117
|
message: "Assistant ID:",
|
|
920
1118
|
default: `${args.id}-assistant`
|
|
921
1119
|
});
|
|
1120
|
+
if (!args.clientId) args.clientId = await input({
|
|
1121
|
+
message: "Client ID for the assistant (must be owned by you):",
|
|
1122
|
+
validate: (v) => v.length > 0 ? true : "clientId is required"
|
|
1123
|
+
});
|
|
922
1124
|
saveOnboardState(args);
|
|
923
1125
|
}
|
|
924
1126
|
}
|
|
@@ -934,16 +1136,16 @@ async function promptMissing(args) {
|
|
|
934
1136
|
}
|
|
935
1137
|
}
|
|
936
1138
|
function registerOnboardCommand(program) {
|
|
937
|
-
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("--
|
|
1139
|
+
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) => {
|
|
938
1140
|
try {
|
|
939
1141
|
const args = {
|
|
940
1142
|
...loadOnboardState() ?? {},
|
|
941
1143
|
...options.id && { id: options.id },
|
|
942
1144
|
...options.type && { type: options.type },
|
|
1145
|
+
...options.clientId && { clientId: options.clientId },
|
|
943
1146
|
...options.displayName && { displayName: options.displayName },
|
|
944
1147
|
...options.role && { role: options.role },
|
|
945
1148
|
...options.domains && { domains: options.domains },
|
|
946
|
-
...options.profile && { profile: options.profile },
|
|
947
1149
|
...options.assistant && { assistant: options.assistant },
|
|
948
1150
|
...options.delegateMention && { delegateMention: options.delegateMention },
|
|
949
1151
|
...options.server && { server: options.server },
|