agent-factorio 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # agent-factorio
2
2
 
3
- CLI for [AgentFactorio](https://github.com/your-org/agent-factorio) — AI Agent Fleet Management hub.
3
+ CLI for [AgentFactorio](https://github.com/gmuffiness/agent-factorio) — All your team's agents, one place.
4
4
 
5
- Register and manage your AI agents from any project.
5
+ Register and manage your AI agents (Claude Code, Cursor, Codex, etc.) from any project.
6
6
 
7
7
  ## Install
8
8
 
package/bin.js CHANGED
@@ -4,11 +4,24 @@
4
4
  * AgentFactorio CLI — register and manage agents from any project
5
5
  *
6
6
  * Usage:
7
- * npx agent-factorio login # Connect to hub + join organization
8
- * npx agent-factorio push # Push agent config to hub
9
- * npx agent-factorio status # Show registration status
10
- * npx agent-factorio whoami # Show login info
11
- * npx agent-factorio logout # Remove global config
7
+ * npx agent-factorio login # Connect to hub + join organization
8
+ * npx agent-factorio push # Push agent config to hub
9
+ * npx agent-factorio status # Show registration status
10
+ * npx agent-factorio whoami # Show login info
11
+ * npx agent-factorio logout # Remove global config
12
+ * npx agent-factorio connect # Poll hub and relay messages
13
+ *
14
+ * npx agent-factorio org list # List organizations
15
+ * npx agent-factorio org create # Create a new organization
16
+ * npx agent-factorio org join # Join via invite code
17
+ * npx agent-factorio org switch # Change default organization
18
+ * npx agent-factorio org info # Show current org details
19
+ *
20
+ * npx agent-factorio agent list # List agents in current org
21
+ * npx agent-factorio agent info # Show agent details
22
+ * npx agent-factorio agent edit # Edit agent properties
23
+ * npx agent-factorio agent pull # Sync hub config to local
24
+ * npx agent-factorio agent delete # Delete an agent
12
25
  */
13
26
 
14
27
  import { readFileSync } from "fs";
@@ -21,6 +34,20 @@ import { statusCommand } from "./commands/status.mjs";
21
34
  import { whoamiCommand } from "./commands/whoami.mjs";
22
35
  import { logoutCommand } from "./commands/logout.mjs";
23
36
  import { connectCommand } from "./commands/connect.mjs";
37
+ import {
38
+ orgListCommand,
39
+ orgCreateCommand,
40
+ orgJoinCommand,
41
+ orgSwitchCommand,
42
+ orgInfoCommand,
43
+ } from "./commands/org.mjs";
44
+ import {
45
+ agentListCommand,
46
+ agentInfoCommand,
47
+ agentEditCommand,
48
+ agentPullCommand,
49
+ agentDeleteCommand,
50
+ } from "./commands/agent.mjs";
24
51
 
25
52
  const __dirname = dirname(fileURLToPath(import.meta.url));
26
53
  const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8"));
@@ -32,9 +59,12 @@ program
32
59
  .description("AgentFactorio CLI — AI Agent Fleet Management")
33
60
  .version(pkg.version);
34
61
 
62
+ // --- Existing top-level commands ---
63
+
35
64
  program
36
65
  .command("login")
37
66
  .description("Connect to an AgentFactorio hub and join an organization")
67
+ .option("--hub-url <url>", "Hub URL (default: https://agent-factorio.vercel.app)")
38
68
  .action(loginCommand);
39
69
 
40
70
  program
@@ -62,4 +92,77 @@ program
62
92
  .description("Poll hub and relay messages to local OpenClaw Gateway")
63
93
  .action(connectCommand);
64
94
 
95
+ // --- org subcommands ---
96
+
97
+ const org = program
98
+ .command("org")
99
+ .description("Manage organizations");
100
+
101
+ org
102
+ .command("list")
103
+ .description("List all organizations you belong to")
104
+ .action(orgListCommand);
105
+
106
+ org
107
+ .command("create")
108
+ .description("Create a new organization")
109
+ .argument("[name]", "Organization name")
110
+ .action(orgCreateCommand);
111
+
112
+ org
113
+ .command("join")
114
+ .description("Join an organization via invite code")
115
+ .argument("[inviteCode]", "Invite code")
116
+ .action(orgJoinCommand);
117
+
118
+ org
119
+ .command("switch")
120
+ .description("Change the default organization")
121
+ .action(orgSwitchCommand);
122
+
123
+ org
124
+ .command("info")
125
+ .description("Show details about the current organization")
126
+ .action(orgInfoCommand);
127
+
128
+ // --- agent subcommands ---
129
+
130
+ const agent = program
131
+ .command("agent")
132
+ .description("Manage agents");
133
+
134
+ agent
135
+ .command("list")
136
+ .description("List all agents in the current organization")
137
+ .action(agentListCommand);
138
+
139
+ agent
140
+ .command("info")
141
+ .description("Show agent details")
142
+ .argument("[id]", "Agent ID (defaults to local project agent)")
143
+ .action(agentInfoCommand);
144
+
145
+ agent
146
+ .command("edit")
147
+ .description("Edit agent properties")
148
+ .argument("[id]", "Agent ID (defaults to local project agent)")
149
+ .option("--name <name>", "Agent name")
150
+ .option("--vendor <vendor>", "Vendor (e.g. anthropic, openai)")
151
+ .option("--model <model>", "Model (e.g. claude-sonnet-4-20250514)")
152
+ .option("--description <desc>", "Description")
153
+ .option("--status <status>", "Status (active, idle, error)")
154
+ .action(agentEditCommand);
155
+
156
+ agent
157
+ .command("pull")
158
+ .description("Sync agent config from hub to local project")
159
+ .argument("[id]", "Agent ID (defaults to local project agent)")
160
+ .action(agentPullCommand);
161
+
162
+ agent
163
+ .command("delete")
164
+ .description("Delete an agent")
165
+ .argument("[id]", "Agent ID (defaults to local project agent)")
166
+ .action(agentDeleteCommand);
167
+
65
168
  program.parse();
@@ -0,0 +1,272 @@
1
+ /**
2
+ * agent-factorio agent — Agent management subcommands
3
+ */
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import { ask, confirm } from "../lib/prompt.mjs";
7
+ import { getDefaultOrg, readLocalConfig, writeLocalConfig, findProjectRoot } from "../lib/config.mjs";
8
+ import { authApiCall } from "../lib/api.mjs";
9
+ import { success, error, info, label, heading, dim } from "../lib/log.mjs";
10
+
11
+ /**
12
+ * Resolve agent ID from argument or local config
13
+ * @param {string} [idArg]
14
+ * @returns {string}
15
+ */
16
+ function resolveAgentId(idArg) {
17
+ if (idArg) return idArg;
18
+
19
+ const local = readLocalConfig();
20
+ if (local?.agentId) return local.agentId;
21
+
22
+ error("No agent ID specified and no local .agent-factorio/config.json found.");
23
+ info("Run this command with an agent ID, or run it from a project with a registered agent.");
24
+ process.exit(1);
25
+ }
26
+
27
+ /**
28
+ * agent list — List all agents in the current organization
29
+ */
30
+ export async function agentListCommand() {
31
+ const org = getDefaultOrg();
32
+ if (!org) {
33
+ error("Not logged in. Run `agent-factorio login` first.");
34
+ process.exit(1);
35
+ }
36
+
37
+ try {
38
+ const res = await authApiCall(`/api/cli/agents?orgId=${org.orgId}`);
39
+ if (!res.ok) {
40
+ error(res.data?.error || "Failed to fetch agents");
41
+ process.exit(1);
42
+ }
43
+
44
+ const { agents } = res.data;
45
+ if (!agents.length) {
46
+ info("No agents in this organization.");
47
+ info("Run `agent-factorio push` from a project to register an agent.");
48
+ return;
49
+ }
50
+
51
+ heading(`Agents in "${org.orgName}"`);
52
+ console.log("");
53
+
54
+ // Find max name length for alignment
55
+ const maxName = Math.max(...agents.map((a) => a.name.length), 4);
56
+
57
+ // Header
58
+ const header = ` ${"NAME".padEnd(maxName + 2)}${"VENDOR".padEnd(12)}${"MODEL".padEnd(20)}${"STATUS".padEnd(10)}DEPARTMENT`;
59
+ dim(header);
60
+
61
+ for (const agent of agents) {
62
+ const status = agent.status === "active" ? "\x1b[32mactive\x1b[0m" : agent.status;
63
+ console.log(
64
+ ` ${agent.name.padEnd(maxName + 2)}${(agent.vendor || "-").padEnd(12)}${(agent.model || "-").padEnd(20)}${(agent.status || "-").padEnd(10)}${agent.departmentName || "-"}`
65
+ );
66
+ }
67
+
68
+ console.log("");
69
+ dim(` ${agents.length} agent(s) total`);
70
+ } catch (err) {
71
+ error(err.message);
72
+ process.exit(1);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * agent info [id] — Show agent details
78
+ */
79
+ export async function agentInfoCommand(id) {
80
+ const agentId = resolveAgentId(id);
81
+
82
+ try {
83
+ const res = await authApiCall(`/api/cli/agents/${agentId}`);
84
+ if (!res.ok) {
85
+ error(res.data?.error || "Failed to fetch agent");
86
+ process.exit(1);
87
+ }
88
+
89
+ const a = res.data;
90
+
91
+ heading(`Agent: ${a.name}`);
92
+ console.log("");
93
+ label(" ID", a.id);
94
+ label(" Vendor", a.vendor);
95
+ label(" Model", a.model);
96
+ label(" Status", a.status);
97
+ label(" Description", a.description || "-");
98
+ label(" Department", a.departmentName);
99
+ label(" Runtime", a.runtimeType || "api");
100
+ label(" Last Active", a.lastActive || "-");
101
+ label(" Created", a.createdAt || "-");
102
+ label(" Monthly Cost", `$${a.monthlyCost ?? 0}`);
103
+ label(" Tokens Used", String(a.tokensUsed ?? 0));
104
+
105
+ if (a.skills?.length) {
106
+ console.log("");
107
+ heading(" Skills");
108
+ for (const s of a.skills) {
109
+ console.log(` - ${s.name} (${s.category})`);
110
+ }
111
+ }
112
+
113
+ if (a.mcpTools?.length) {
114
+ console.log("");
115
+ heading(" MCP Tools");
116
+ for (const t of a.mcpTools) {
117
+ const server = t.server ? ` [${t.server}]` : "";
118
+ console.log(` - ${t.name}${server}`);
119
+ }
120
+ }
121
+
122
+ if (a.resources?.length) {
123
+ console.log("");
124
+ heading(" Resources");
125
+ for (const r of a.resources) {
126
+ console.log(` - ${r.type}: ${r.name} (${r.url || "-"})`);
127
+ }
128
+ }
129
+
130
+ if (a.context?.length) {
131
+ console.log("");
132
+ heading(" Context");
133
+ for (const c of a.context) {
134
+ const source = c.sourceFile ? ` (${c.sourceFile})` : "";
135
+ const preview = c.content.length > 80 ? c.content.slice(0, 80) + "..." : c.content;
136
+ console.log(` - [${c.type}]${source} ${preview}`);
137
+ }
138
+ }
139
+ } catch (err) {
140
+ error(err.message);
141
+ process.exit(1);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * agent edit [id] — Edit agent properties
147
+ */
148
+ export async function agentEditCommand(id, options) {
149
+ const agentId = resolveAgentId(id);
150
+
151
+ const updates = {};
152
+ if (options.name) updates.name = options.name;
153
+ if (options.vendor) updates.vendor = options.vendor;
154
+ if (options.model) updates.model = options.model;
155
+ if (options.description) updates.description = options.description;
156
+ if (options.status) updates.status = options.status;
157
+
158
+ if (Object.keys(updates).length === 0) {
159
+ error("No fields to update. Use --name, --vendor, --model, --description, or --status.");
160
+ process.exit(1);
161
+ }
162
+
163
+ try {
164
+ const res = await authApiCall(`/api/cli/agents/${agentId}`, {
165
+ method: "PATCH",
166
+ body: updates,
167
+ });
168
+
169
+ if (!res.ok) {
170
+ error(res.data?.error || "Failed to update agent");
171
+ process.exit(1);
172
+ }
173
+
174
+ success(res.data.message || "Agent updated successfully");
175
+
176
+ // Update local config if this is the local agent
177
+ const local = readLocalConfig();
178
+ if (local?.agentId === agentId) {
179
+ if (updates.name) local.agentName = updates.name;
180
+ if (updates.vendor) local.vendor = updates.vendor;
181
+ if (updates.model) local.model = updates.model;
182
+ writeLocalConfig(local);
183
+ dim(" Local config updated.");
184
+ }
185
+ } catch (err) {
186
+ error(err.message);
187
+ process.exit(1);
188
+ }
189
+ }
190
+
191
+ /**
192
+ * agent pull [id] — Pull agent config from hub to local config
193
+ */
194
+ export async function agentPullCommand(id) {
195
+ const agentId = resolveAgentId(id);
196
+
197
+ try {
198
+ const res = await authApiCall(`/api/cli/agents/${agentId}`);
199
+ if (!res.ok) {
200
+ error(res.data?.error || "Failed to fetch agent");
201
+ process.exit(1);
202
+ }
203
+
204
+ const a = res.data;
205
+ const org = getDefaultOrg();
206
+
207
+ writeLocalConfig({
208
+ hubUrl: org.hubUrl,
209
+ orgId: a.orgId,
210
+ agentId: a.id,
211
+ agentName: a.name,
212
+ vendor: a.vendor,
213
+ model: a.model,
214
+ pushedAt: a.lastActive || new Date().toISOString(),
215
+ });
216
+
217
+ success(`Synced agent "${a.name}" to local config.`);
218
+ label(" Agent", a.name);
219
+ label(" Vendor", a.vendor);
220
+ label(" Model", a.model);
221
+ label(" Status", a.status);
222
+ } catch (err) {
223
+ error(err.message);
224
+ process.exit(1);
225
+ }
226
+ }
227
+
228
+ /**
229
+ * agent delete [id] — Delete an agent
230
+ */
231
+ export async function agentDeleteCommand(id) {
232
+ const agentId = resolveAgentId(id);
233
+
234
+ // Fetch agent name for confirmation
235
+ try {
236
+ const infoRes = await authApiCall(`/api/cli/agents/${agentId}`);
237
+ const agentName = infoRes.ok ? infoRes.data.name : agentId;
238
+
239
+ const confirmed = await confirm(`Delete agent "${agentName}"? This cannot be undone`, false);
240
+ if (!confirmed) {
241
+ info("Cancelled.");
242
+ return;
243
+ }
244
+
245
+ const res = await authApiCall(`/api/cli/agents/${agentId}`, {
246
+ method: "DELETE",
247
+ });
248
+
249
+ if (!res.ok) {
250
+ error(res.data?.error || "Failed to delete agent");
251
+ process.exit(1);
252
+ }
253
+
254
+ success(res.data.message || "Agent deleted successfully");
255
+
256
+ // Remove local config if this is the local agent
257
+ const local = readLocalConfig();
258
+ if (local?.agentId === agentId) {
259
+ const root = findProjectRoot();
260
+ const configPath = path.join(root, ".agent-factorio", "config.json");
261
+ try {
262
+ fs.unlinkSync(configPath);
263
+ dim(" Local config removed.");
264
+ } catch {
265
+ // ignore
266
+ }
267
+ }
268
+ } catch (err) {
269
+ error(err.message);
270
+ process.exit(1);
271
+ }
272
+ }
@@ -41,16 +41,11 @@ async function waitForVerification(hubUrl, loginToken) {
41
41
  throw new Error("Verification timed out. Please try again.");
42
42
  }
43
43
 
44
- export async function loginCommand() {
45
- const existing = readGlobalConfig();
46
- const defaultUrl = existing?.organizations?.[0]?.hubUrl || "";
44
+ const DEFAULT_HUB_URL = "https://agent-factorio.vercel.app";
47
45
 
48
- // 1. Hub URL
49
- const hubUrl = await ask("AgentFactorio Hub URL", defaultUrl || "http://localhost:3000");
50
- if (!hubUrl) {
51
- error("Hub URL is required.");
52
- process.exit(1);
53
- }
46
+ export async function loginCommand(options = {}) {
47
+ const existing = readGlobalConfig();
48
+ const hubUrl = options.hubUrl || existing?.organizations?.[0]?.hubUrl || DEFAULT_HUB_URL;
54
49
 
55
50
  // Check connectivity
56
51
  const reachable = await checkHub(hubUrl);
@@ -58,7 +53,6 @@ export async function loginCommand() {
58
53
  error(`Cannot connect to ${hubUrl}. Is the hub running?`);
59
54
  process.exit(1);
60
55
  }
61
- success("Hub connected.");
62
56
 
63
57
  // 2. Email input
64
58
  const email = await ask("Your email (used as your identifier)");
@@ -100,11 +94,11 @@ export async function loginCommand() {
100
94
 
101
95
  // 6. Create or Join
102
96
  const { index: actionIdx } = await choose("Create or join an organization?", [
103
- "Join existing (invite code)",
104
97
  "Create new",
98
+ "Join existing (invite code)",
105
99
  ]);
106
100
 
107
- if (actionIdx === 1) {
101
+ if (actionIdx === 0) {
108
102
  // Create new org
109
103
  const orgName = await ask("Organization name");
110
104
  if (!orgName) {
@@ -121,8 +115,8 @@ export async function loginCommand() {
121
115
  process.exit(1);
122
116
  }
123
117
 
124
- const { orgId, orgName: name, inviteCode, memberId } = res.data;
125
- upsertOrg({ hubUrl, orgId, orgName: name, inviteCode, memberName, email, memberId, userId });
118
+ const { orgId, orgName: name, inviteCode, memberId, authToken } = res.data;
119
+ upsertOrg({ hubUrl, orgId, orgName: name, inviteCode, memberName, email, memberId, userId, authToken });
126
120
 
127
121
  success(`Created "${name}" (${orgId})`);
128
122
  info(`Invite code: ${inviteCode} — share with your team!`);
@@ -143,8 +137,8 @@ export async function loginCommand() {
143
137
  process.exit(1);
144
138
  }
145
139
 
146
- const { orgId, orgName, memberId } = res.data;
147
- upsertOrg({ hubUrl, orgId, orgName, inviteCode: inviteCode.toUpperCase(), memberName, email, memberId, userId });
140
+ const { orgId, orgName, memberId, authToken } = res.data;
141
+ upsertOrg({ hubUrl, orgId, orgName, inviteCode: inviteCode.toUpperCase(), memberName, email, memberId, userId, authToken });
148
142
 
149
143
  success(`Joined "${orgName}" (${orgId})`);
150
144
  }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * agent-factorio org — Organization management subcommands
3
+ */
4
+ import { ask, choose } from "../lib/prompt.mjs";
5
+ import { readGlobalConfig, writeGlobalConfig, getDefaultOrg, upsertOrg } from "../lib/config.mjs";
6
+ import { apiCall, authApiCall } from "../lib/api.mjs";
7
+ import { success, error, info, label, heading, dim } from "../lib/log.mjs";
8
+
9
+ /**
10
+ * org list — List all organizations the user belongs to
11
+ */
12
+ export async function orgListCommand() {
13
+ try {
14
+ const res = await authApiCall("/api/cli/orgs");
15
+ if (!res.ok) {
16
+ error(res.data?.error || "Failed to fetch organizations");
17
+ process.exit(1);
18
+ }
19
+
20
+ const { organizations } = res.data;
21
+ if (!organizations.length) {
22
+ info("You are not a member of any organizations.");
23
+ info("Run `agent-factorio org create` or `agent-factorio org join` to get started.");
24
+ return;
25
+ }
26
+
27
+ const config = readGlobalConfig();
28
+ const defaultOrgId = config?.defaultOrg;
29
+
30
+ heading("Organizations");
31
+ console.log("");
32
+
33
+ for (const org of organizations) {
34
+ const isDefault = org.orgId === defaultOrgId;
35
+ const marker = isDefault ? " (default)" : "";
36
+ console.log(` ${org.orgName}${marker}`);
37
+ label(" ID", org.orgId);
38
+ label(" Role", org.role);
39
+ label(" Invite Code", org.inviteCode);
40
+ label(" Members", String(org.memberCount));
41
+ label(" Agents", String(org.agentCount));
42
+ console.log("");
43
+ }
44
+ } catch (err) {
45
+ error(err.message);
46
+ process.exit(1);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * org create <name> — Create a new organization
52
+ */
53
+ export async function orgCreateCommand(name) {
54
+ const org = getDefaultOrg();
55
+ if (!org) {
56
+ error("Not logged in. Run `agent-factorio login` first.");
57
+ process.exit(1);
58
+ }
59
+
60
+ const orgName = name || await ask("Organization name");
61
+ if (!orgName) {
62
+ error("Organization name is required.");
63
+ process.exit(1);
64
+ }
65
+
66
+ const res = await apiCall(org.hubUrl, "/api/cli/login", {
67
+ body: {
68
+ action: "create",
69
+ orgName,
70
+ memberName: org.memberName || "CLI User",
71
+ email: org.email,
72
+ userId: org.userId,
73
+ },
74
+ });
75
+
76
+ if (!res.ok) {
77
+ error(`Failed to create organization: ${res.data?.error || "Unknown error"}`);
78
+ process.exit(1);
79
+ }
80
+
81
+ const { orgId, orgName: createdName, inviteCode, memberId, authToken } = res.data;
82
+ upsertOrg({
83
+ hubUrl: org.hubUrl,
84
+ orgId,
85
+ orgName: createdName,
86
+ inviteCode,
87
+ memberName: org.memberName,
88
+ email: org.email,
89
+ memberId,
90
+ userId: org.userId,
91
+ authToken,
92
+ });
93
+
94
+ success(`Created "${createdName}" (${orgId})`);
95
+ info(`Invite code: ${inviteCode} — share with your team!`);
96
+ }
97
+
98
+ /**
99
+ * org join <inviteCode> — Join an organization via invite code
100
+ */
101
+ export async function orgJoinCommand(code) {
102
+ const org = getDefaultOrg();
103
+ if (!org) {
104
+ error("Not logged in. Run `agent-factorio login` first.");
105
+ process.exit(1);
106
+ }
107
+
108
+ const inviteCode = code || await ask("Invite code");
109
+ if (!inviteCode) {
110
+ error("Invite code is required.");
111
+ process.exit(1);
112
+ }
113
+
114
+ const res = await apiCall(org.hubUrl, "/api/cli/login", {
115
+ body: {
116
+ action: "join",
117
+ inviteCode,
118
+ memberName: org.memberName || "CLI User",
119
+ email: org.email,
120
+ userId: org.userId,
121
+ },
122
+ });
123
+
124
+ if (!res.ok) {
125
+ error(`Failed to join: ${res.data?.error || "Invalid invite code"}`);
126
+ process.exit(1);
127
+ }
128
+
129
+ const { orgId, orgName, memberId, authToken } = res.data;
130
+ upsertOrg({
131
+ hubUrl: org.hubUrl,
132
+ orgId,
133
+ orgName,
134
+ inviteCode: inviteCode.toUpperCase(),
135
+ memberName: org.memberName,
136
+ email: org.email,
137
+ memberId,
138
+ userId: org.userId,
139
+ authToken,
140
+ });
141
+
142
+ success(`Joined "${orgName}" (${orgId})`);
143
+ }
144
+
145
+ /**
146
+ * org switch — Interactively select the default organization
147
+ */
148
+ export async function orgSwitchCommand() {
149
+ const config = readGlobalConfig();
150
+ if (!config?.organizations?.length) {
151
+ error("No organizations found. Run `agent-factorio login` first.");
152
+ process.exit(1);
153
+ }
154
+
155
+ if (config.organizations.length === 1) {
156
+ info(`Only one organization: "${config.organizations[0].orgName}". Already set as default.`);
157
+ return;
158
+ }
159
+
160
+ const options = config.organizations.map((o) => {
161
+ const marker = o.orgId === config.defaultOrg ? " (current)" : "";
162
+ return `${o.orgName}${marker}`;
163
+ });
164
+
165
+ const { index } = await choose("Select default organization", options);
166
+ const selected = config.organizations[index];
167
+
168
+ config.defaultOrg = selected.orgId;
169
+ writeGlobalConfig(config);
170
+
171
+ success(`Default organization set to "${selected.orgName}" (${selected.orgId})`);
172
+ }
173
+
174
+ /**
175
+ * org info — Show details about the current default organization
176
+ */
177
+ export async function orgInfoCommand() {
178
+ const org = getDefaultOrg();
179
+ if (!org) {
180
+ error("Not logged in. Run `agent-factorio login` first.");
181
+ process.exit(1);
182
+ }
183
+
184
+ try {
185
+ const res = await authApiCall("/api/cli/orgs");
186
+ if (!res.ok) {
187
+ // Fallback to local config info
188
+ heading("Organization (local config)");
189
+ console.log("");
190
+ label(" Name", org.orgName);
191
+ label(" ID", org.orgId);
192
+ label(" Invite Code", org.inviteCode);
193
+ label(" Hub", org.hubUrl);
194
+ return;
195
+ }
196
+
197
+ const match = res.data.organizations.find((o) => o.orgId === org.orgId);
198
+ if (!match) {
199
+ info("Organization not found on hub. Showing local config.");
200
+ label(" Name", org.orgName);
201
+ label(" ID", org.orgId);
202
+ label(" Hub", org.hubUrl);
203
+ return;
204
+ }
205
+
206
+ heading("Organization");
207
+ console.log("");
208
+ label(" Name", match.orgName);
209
+ label(" ID", match.orgId);
210
+ label(" Role", match.role);
211
+ label(" Invite Code", match.inviteCode);
212
+ label(" Members", String(match.memberCount));
213
+ label(" Agents", String(match.agentCount));
214
+ label(" Hub", org.hubUrl);
215
+ } catch (err) {
216
+ error(err.message);
217
+ process.exit(1);
218
+ }
219
+ }
package/lib/api.mjs CHANGED
@@ -2,21 +2,25 @@
2
2
  * Hub API call helper
3
3
  */
4
4
 
5
+ import { getDefaultOrg } from "./config.mjs";
6
+
5
7
  /**
6
8
  * Make an API request to the AgentFactorio hub
7
9
  * @param {string} hubUrl - Base URL of the hub
8
10
  * @param {string} path - API path (e.g. "/api/cli/login")
9
- * @param {{ method?: string, body?: unknown }} [options]
11
+ * @param {{ method?: string, body?: unknown, authToken?: string }} [options]
10
12
  * @returns {Promise<{ ok: boolean, status: number, data: unknown }>}
11
13
  */
12
14
  export async function apiCall(hubUrl, path, options = {}) {
13
15
  const url = `${hubUrl.replace(/\/$/, "")}${path}`;
14
16
  const method = options.method || (options.body ? "POST" : "GET");
15
17
 
16
- const fetchOptions = {
17
- method,
18
- headers: { "Content-Type": "application/json" },
19
- };
18
+ const headers = { "Content-Type": "application/json" };
19
+ if (options.authToken) {
20
+ headers["Authorization"] = `Bearer ${options.authToken}`;
21
+ }
22
+
23
+ const fetchOptions = { method, headers };
20
24
 
21
25
  if (options.body) {
22
26
  fetchOptions.body = JSON.stringify(options.body);
@@ -33,6 +37,23 @@ export async function apiCall(hubUrl, path, options = {}) {
33
37
  return { ok: res.ok, status: res.status, data };
34
38
  }
35
39
 
40
+ /**
41
+ * Make an authenticated API call using the default org's hubUrl and authToken.
42
+ * @param {string} path - API path
43
+ * @param {{ method?: string, body?: unknown }} [options]
44
+ * @returns {Promise<{ ok: boolean, status: number, data: unknown }>}
45
+ */
46
+ export async function authApiCall(path, options = {}) {
47
+ const org = getDefaultOrg();
48
+ if (!org) {
49
+ throw new Error("Not logged in. Run `agent-factorio login` first.");
50
+ }
51
+ if (!org.authToken) {
52
+ throw new Error("Auth token missing. Run `agent-factorio login` again to get a token.");
53
+ }
54
+ return apiCall(org.hubUrl, path, { ...options, authToken: org.authToken });
55
+ }
56
+
36
57
  /**
37
58
  * Check if hub is reachable
38
59
  * @param {string} hubUrl
package/lib/config.mjs CHANGED
@@ -13,7 +13,7 @@ const LOCAL_CONFIG_NAME = "config.json";
13
13
  // --- Global config ---
14
14
 
15
15
  /**
16
- * @typedef {{ hubUrl: string, orgId: string, orgName: string, inviteCode: string, memberName?: string, email?: string, memberId?: string, userId?: string }} OrgEntry
16
+ * @typedef {{ hubUrl: string, orgId: string, orgName: string, inviteCode: string, memberName?: string, email?: string, memberId?: string, userId?: string, authToken?: string }} OrgEntry
17
17
  * @typedef {{ organizations: OrgEntry[], defaultOrg?: string }} GlobalConfig
18
18
  */
19
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-factorio",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "CLI for AgentFactorio — AI Agent Fleet Management hub",
5
5
  "type": "module",
6
6
  "bin": {