agent-factorio 0.2.0 → 0.3.0
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 +2 -2
- package/bin.js +107 -5
- package/commands/agent.mjs +272 -0
- package/commands/login.mjs +7 -7
- package/commands/org.mjs +219 -0
- package/lib/api.mjs +26 -5
- package/lib/config.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# agent-factorio
|
|
2
2
|
|
|
3
|
-
CLI for [AgentFactorio](https://github.com/
|
|
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
|
|
8
|
-
* npx agent-factorio push
|
|
9
|
-
* npx agent-factorio status
|
|
10
|
-
* npx agent-factorio whoami
|
|
11
|
-
* npx agent-factorio logout
|
|
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,6 +59,8 @@ 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")
|
|
@@ -62,4 +91,77 @@ program
|
|
|
62
91
|
.description("Poll hub and relay messages to local OpenClaw Gateway")
|
|
63
92
|
.action(connectCommand);
|
|
64
93
|
|
|
94
|
+
// --- org subcommands ---
|
|
95
|
+
|
|
96
|
+
const org = program
|
|
97
|
+
.command("org")
|
|
98
|
+
.description("Manage organizations");
|
|
99
|
+
|
|
100
|
+
org
|
|
101
|
+
.command("list")
|
|
102
|
+
.description("List all organizations you belong to")
|
|
103
|
+
.action(orgListCommand);
|
|
104
|
+
|
|
105
|
+
org
|
|
106
|
+
.command("create")
|
|
107
|
+
.description("Create a new organization")
|
|
108
|
+
.argument("[name]", "Organization name")
|
|
109
|
+
.action(orgCreateCommand);
|
|
110
|
+
|
|
111
|
+
org
|
|
112
|
+
.command("join")
|
|
113
|
+
.description("Join an organization via invite code")
|
|
114
|
+
.argument("[inviteCode]", "Invite code")
|
|
115
|
+
.action(orgJoinCommand);
|
|
116
|
+
|
|
117
|
+
org
|
|
118
|
+
.command("switch")
|
|
119
|
+
.description("Change the default organization")
|
|
120
|
+
.action(orgSwitchCommand);
|
|
121
|
+
|
|
122
|
+
org
|
|
123
|
+
.command("info")
|
|
124
|
+
.description("Show details about the current organization")
|
|
125
|
+
.action(orgInfoCommand);
|
|
126
|
+
|
|
127
|
+
// --- agent subcommands ---
|
|
128
|
+
|
|
129
|
+
const agent = program
|
|
130
|
+
.command("agent")
|
|
131
|
+
.description("Manage agents");
|
|
132
|
+
|
|
133
|
+
agent
|
|
134
|
+
.command("list")
|
|
135
|
+
.description("List all agents in the current organization")
|
|
136
|
+
.action(agentListCommand);
|
|
137
|
+
|
|
138
|
+
agent
|
|
139
|
+
.command("info")
|
|
140
|
+
.description("Show agent details")
|
|
141
|
+
.argument("[id]", "Agent ID (defaults to local project agent)")
|
|
142
|
+
.action(agentInfoCommand);
|
|
143
|
+
|
|
144
|
+
agent
|
|
145
|
+
.command("edit")
|
|
146
|
+
.description("Edit agent properties")
|
|
147
|
+
.argument("[id]", "Agent ID (defaults to local project agent)")
|
|
148
|
+
.option("--name <name>", "Agent name")
|
|
149
|
+
.option("--vendor <vendor>", "Vendor (e.g. anthropic, openai)")
|
|
150
|
+
.option("--model <model>", "Model (e.g. claude-sonnet-4-20250514)")
|
|
151
|
+
.option("--description <desc>", "Description")
|
|
152
|
+
.option("--status <status>", "Status (active, idle, error)")
|
|
153
|
+
.action(agentEditCommand);
|
|
154
|
+
|
|
155
|
+
agent
|
|
156
|
+
.command("pull")
|
|
157
|
+
.description("Sync agent config from hub to local project")
|
|
158
|
+
.argument("[id]", "Agent ID (defaults to local project agent)")
|
|
159
|
+
.action(agentPullCommand);
|
|
160
|
+
|
|
161
|
+
agent
|
|
162
|
+
.command("delete")
|
|
163
|
+
.description("Delete an agent")
|
|
164
|
+
.argument("[id]", "Agent ID (defaults to local project agent)")
|
|
165
|
+
.action(agentDeleteCommand);
|
|
166
|
+
|
|
65
167
|
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
|
+
}
|
package/commands/login.mjs
CHANGED
|
@@ -46,7 +46,7 @@ export async function loginCommand() {
|
|
|
46
46
|
const defaultUrl = existing?.organizations?.[0]?.hubUrl || "";
|
|
47
47
|
|
|
48
48
|
// 1. Hub URL
|
|
49
|
-
const hubUrl = await ask("AgentFactorio Hub URL", defaultUrl || "
|
|
49
|
+
const hubUrl = await ask("AgentFactorio Hub URL", defaultUrl || "https://agent-factorio.vercel.app");
|
|
50
50
|
if (!hubUrl) {
|
|
51
51
|
error("Hub URL is required.");
|
|
52
52
|
process.exit(1);
|
|
@@ -100,11 +100,11 @@ export async function loginCommand() {
|
|
|
100
100
|
|
|
101
101
|
// 6. Create or Join
|
|
102
102
|
const { index: actionIdx } = await choose("Create or join an organization?", [
|
|
103
|
-
"Join existing (invite code)",
|
|
104
103
|
"Create new",
|
|
104
|
+
"Join existing (invite code)",
|
|
105
105
|
]);
|
|
106
106
|
|
|
107
|
-
if (actionIdx ===
|
|
107
|
+
if (actionIdx === 0) {
|
|
108
108
|
// Create new org
|
|
109
109
|
const orgName = await ask("Organization name");
|
|
110
110
|
if (!orgName) {
|
|
@@ -121,8 +121,8 @@ export async function loginCommand() {
|
|
|
121
121
|
process.exit(1);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
const { orgId, orgName: name, inviteCode, memberId } = res.data;
|
|
125
|
-
upsertOrg({ hubUrl, orgId, orgName: name, inviteCode, memberName, email, memberId, userId });
|
|
124
|
+
const { orgId, orgName: name, inviteCode, memberId, authToken } = res.data;
|
|
125
|
+
upsertOrg({ hubUrl, orgId, orgName: name, inviteCode, memberName, email, memberId, userId, authToken });
|
|
126
126
|
|
|
127
127
|
success(`Created "${name}" (${orgId})`);
|
|
128
128
|
info(`Invite code: ${inviteCode} — share with your team!`);
|
|
@@ -143,8 +143,8 @@ export async function loginCommand() {
|
|
|
143
143
|
process.exit(1);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
const { orgId, orgName, memberId } = res.data;
|
|
147
|
-
upsertOrg({ hubUrl, orgId, orgName, inviteCode: inviteCode.toUpperCase(), memberName, email, memberId, userId });
|
|
146
|
+
const { orgId, orgName, memberId, authToken } = res.data;
|
|
147
|
+
upsertOrg({ hubUrl, orgId, orgName, inviteCode: inviteCode.toUpperCase(), memberName, email, memberId, userId, authToken });
|
|
148
148
|
|
|
149
149
|
success(`Joined "${orgName}" (${orgId})`);
|
|
150
150
|
}
|
package/commands/org.mjs
ADDED
|
@@ -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
|
|
17
|
-
|
|
18
|
-
headers
|
|
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
|
|