@forloop-cc/forloop-cli 0.2.3 → 0.2.5

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.
Files changed (3) hide show
  1. package/README.md +17 -9
  2. package/dist/forloop +133 -184
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,7 +8,6 @@ Command-line interface for the [ForLoop](https://forloop.cc) AI development plat
8
8
  - [Installation](#installation)
9
9
  - [Quick Start](#quick-start)
10
10
  - [Authentication](#authentication)
11
- - [Environments](#environments)
12
11
  - [Commands](#commands)
13
12
  - [Sprint Operations](#sprint-operations)
14
13
  - [Story Operations](#story-operations)
@@ -63,7 +62,7 @@ npm install -g @forloop-cc/forloop-cli
63
62
 
64
63
  ```bash
65
64
  forloop --version
66
- # Output: forloop 0.2.1
65
+ # Output: forloop 0.2.4
67
66
  ```
68
67
 
69
68
  ---
@@ -129,10 +128,6 @@ forloop auth logout # Remove saved token
129
128
 
130
129
  ---
131
130
 
132
- ---
133
-
134
- ## Commands
135
-
136
131
  ### Sprint Operations
137
132
 
138
133
  Work with sprints — the containers that hold your stories and track development cycles.
@@ -141,11 +136,14 @@ Work with sprints — the containers that hold your stories and track developmen
141
136
  # List all sprints you have access to
142
137
  forloop sprint list
143
138
  forloop sprint list --org-id 1
139
+ forloop sprint list --include-system-org # Include system organization sprints
140
+ forloop sprint list --output json # Structured JSON output
144
141
 
145
142
  # Get sprint details, including all stories
146
143
  forloop sprint get --id 66
147
144
  forloop sprint get --id 66 --no-stories # Exclude stories
148
145
  forloop sprint get --id 66 --no-files # Exclude files
146
+ forloop sprint get --id 66 --output json # Structured JSON output
149
147
 
150
148
  # Create a new sprint
151
149
  forloop sprint create \
@@ -193,6 +191,7 @@ forloop story create \
193
191
  # Read story details, including developer comments
194
192
  forloop story get --id 229
195
193
  forloop story get --id 229 --no-comments
194
+ forloop story get --id 229 --output json # Structured JSON output
196
195
 
197
196
  # Update a story
198
197
  forloop story update --id 229 --status in_progress
@@ -210,7 +209,11 @@ Templates define the structure of your stories. List available templates to see
210
209
 
211
210
  ```bash
212
211
  forloop template list
213
- # Output shows: Basic Task, Basic Note, Schedule Meeting, New Folder
212
+ # Built-in templates: Basic Task (basic-task), Basic Note (basic-note)
213
+ # Server templates: Schedule Meeting (schedule-meeting), New Folder (doc-folder)
214
+
215
+ # JSON output:
216
+ forloop template list --output json
214
217
  ```
215
218
 
216
219
  ### Agent Operations
@@ -220,6 +223,7 @@ Interact with ForLoop's AI agents — check their progress, trigger new work, an
220
223
  ```bash
221
224
  # See what AI agents have discussed in the past
222
225
  forloop agent history --sprint 66 --limit 20
226
+ forloop agent history --sprint 66 --output json
223
227
 
224
228
  # Check if a developer task is running, completed, or failed
225
229
  forloop agent developer-status --sprint 66
@@ -241,9 +245,11 @@ Organizations group sprints and control access for teams.
241
245
  # List organizations you belong to
242
246
  forloop org list
243
247
  forloop org list --owned-only # Only orgs you own
248
+ forloop org list --output json # Structured JSON output
244
249
 
245
250
  # View organization details
246
251
  forloop org get --id 1
252
+ forloop org get --id 1 --output json
247
253
 
248
254
  # Create an organization (requires Team or Enterprise tier)
249
255
  forloop org create \
@@ -261,14 +267,15 @@ Check your profile, limits, and usage.
261
267
  ```bash
262
268
  # View your profile
263
269
  forloop user profile
264
- # Output: User: TOSHI WORKSHOP JP, Email: ..., Tier: free
270
+ forloop user profile --output json
265
271
 
266
272
  # Check your quota limits and current usage
267
273
  forloop user quotas
268
- # Output: Stories used: 23/100, Storage used: 17KB/3GB, etc.
274
+ forloop user quotas --output json
269
275
 
270
276
  # Check quota usage for a specific organization
271
277
  forloop org quotas --org-id 1
278
+ forloop org quotas --org-id 1 --output json
272
279
  ```
273
280
 
274
281
  ### File Operations
@@ -282,6 +289,7 @@ forloop file upload --path ./mockup.png --sprint 66 --folder designs
282
289
 
283
290
  # List all files in a sprint
284
291
  forloop file list --sprint 66
292
+ forloop file list --sprint 66 --output json
285
293
 
286
294
  # Generate a download link
287
295
  forloop file download --id 84
package/dist/forloop CHANGED
@@ -912,7 +912,6 @@ function createAuthStatusCommand() {
912
912
  } else {
913
913
  console.log("Not authenticated");
914
914
  console.log("Run: forloop auth login --api-key <token>");
915
- process.exit(1);
916
915
  }
917
916
  }
918
917
  });
@@ -929,6 +928,25 @@ function createAuthLogoutCommand() {
929
928
  });
930
929
  }
931
930
 
931
+ // src/output/formatter.ts
932
+ function print(config, data, label) {
933
+ if (config.output === "json") {
934
+ console.log(JSON.stringify(data, null, 2));
935
+ } else if (typeof data === "string") {
936
+ if (label && !config.quiet) console.log(label);
937
+ console.log(data);
938
+ } else {
939
+ if (label && !config.quiet) console.log(label);
940
+ console.log(JSON.stringify(data, null, 2));
941
+ }
942
+ }
943
+ function formatError(message, format) {
944
+ if (format === "json") {
945
+ return JSON.stringify({ error: { message } }, null, 2);
946
+ }
947
+ return `Error: ${message}`;
948
+ }
949
+
932
950
  // src/commands/story.ts
933
951
  function createStoryCommands() {
934
952
  return {
@@ -945,7 +963,7 @@ async function getClient() {
945
963
  console.error("Error: No API token configured. Run: forloop auth login --api-key <token>");
946
964
  process.exit(1);
947
965
  }
948
- const client = new ForLoopAPIClient({ token, baseUrl: config.apiUrl });
966
+ const client = new ForLoopAPIClient({ token, baseUrl: config.apiUrl, timeout: 3e4 });
949
967
  return { client };
950
968
  }
951
969
  function createStoryCreateCommand() {
@@ -967,7 +985,7 @@ function createStoryCreateCommand() {
967
985
  'forloop story create --title "Meeting notes" --sprint 1 --type basic-note',
968
986
  'forloop story create --title "Design Docs" --sprint 1'
969
987
  ],
970
- async run(_config, flags) {
988
+ async run(config, flags) {
971
989
  const resolved = await getClient();
972
990
  if (!resolved) return;
973
991
  const { client } = resolved;
@@ -1007,7 +1025,7 @@ function createStoryCreateCommand() {
1007
1025
  points: flags.points
1008
1026
  })
1009
1027
  });
1010
- console.log(`Story created: #${story.id} - ${story.title} (${story.type})`);
1028
+ print(config, story, `Story created: #${story.id} - ${story.title} (${story.type})`);
1011
1029
  } else {
1012
1030
  const story = await client.createStory({
1013
1031
  title: flags.title,
@@ -1017,7 +1035,7 @@ function createStoryCreateCommand() {
1017
1035
  priority: flags.priority || "medium",
1018
1036
  status: "todo"
1019
1037
  });
1020
- console.log(`Doc folder created: #${story.id} - ${story.title}`);
1038
+ print(config, story, `Doc folder created: #${story.id} - ${story.title}`);
1021
1039
  }
1022
1040
  }
1023
1041
  });
@@ -1032,7 +1050,7 @@ function createStoryGetCommand() {
1032
1050
  { flag: "--no-comments", description: "Exclude comments" }
1033
1051
  ],
1034
1052
  examples: ["forloop story get --id 123"],
1035
- async run(_config, flags) {
1053
+ async run(config, flags) {
1036
1054
  const resolved = await getClient();
1037
1055
  if (!resolved) return;
1038
1056
  const { client } = resolved;
@@ -1042,28 +1060,7 @@ function createStoryGetCommand() {
1042
1060
  process.exit(1);
1043
1061
  }
1044
1062
  const story = await client.getStory(storyId, { includeComments: !flags.noComments });
1045
- const lines = [
1046
- `Story #${story.id}: ${story.title}`,
1047
- `Sprint: #${story.sprint?.id || "N/A"} ${story.sprint?.title || ""}`,
1048
- `Status: ${story.status}`,
1049
- `Priority: ${story.priority}`,
1050
- `Type: ${story.type}`
1051
- ];
1052
- if (story.points) lines.push(`Points: ${story.points}`);
1053
- if (story.assignee) lines.push(`Assignee: ${story.assignee.name || story.assignee.email || "Unknown"}`);
1054
- if (story.assigneeAgentKey) lines.push(`Agent: ${story.assigneeAgentKey}`);
1055
- if (story.description) {
1056
- lines.push("", "Description:", story.description);
1057
- }
1058
- if (!flags.noComments && story.comments?.length) {
1059
- lines.push("", `--- Comments (${story.comments.length}) ---`);
1060
- for (const comment of story.comments) {
1061
- const author = comment.authorAgentKey || comment.user?.name || "Unknown";
1062
- lines.push(` ${author}: ${comment.content || comment.body || ""}`);
1063
- if (comment.artifacts?.length) lines.push(` Artifacts: ${comment.artifacts.join(", ")}`);
1064
- }
1065
- }
1066
- console.log(lines.join("\n"));
1063
+ print(config, story);
1067
1064
  }
1068
1065
  });
1069
1066
  }
@@ -1081,7 +1078,7 @@ function createStoryUpdateCommand() {
1081
1078
  { flag: "--points <n>", description: "Story points (0-10)", type: "number" }
1082
1079
  ],
1083
1080
  examples: ["forloop story update --id 123 --status in_progress"],
1084
- async run(_config, flags) {
1081
+ async run(config, flags) {
1085
1082
  const resolved = await getClient();
1086
1083
  if (!resolved) return;
1087
1084
  const { client } = resolved;
@@ -1101,7 +1098,7 @@ function createStoryUpdateCommand() {
1101
1098
  process.exit(1);
1102
1099
  }
1103
1100
  const story = await client.updateStory(storyId, data);
1104
- console.log(`Story #${story.id} updated successfully.`);
1101
+ print(config, story, `Story #${story.id} updated successfully.`);
1105
1102
  }
1106
1103
  });
1107
1104
  }
@@ -1115,7 +1112,7 @@ function createStoryDeleteCommand() {
1115
1112
  { flag: "--confirm", description: "Confirm deletion" }
1116
1113
  ],
1117
1114
  examples: ["forloop story delete --id 123 --confirm"],
1118
- async run(_config, flags) {
1115
+ async run(config, flags) {
1119
1116
  if (!flags.confirm) {
1120
1117
  console.error("Error: --confirm is required to permanently delete a story.");
1121
1118
  process.exit(1);
@@ -1124,7 +1121,7 @@ function createStoryDeleteCommand() {
1124
1121
  if (!resolved) return;
1125
1122
  const { client } = resolved;
1126
1123
  await client.deleteStory(flags.id);
1127
- console.log(`Story #${flags.id} deleted successfully.`);
1124
+ print(config, { deleted: flags.id }, `Story #${flags.id} deleted successfully.`);
1128
1125
  }
1129
1126
  });
1130
1127
  }
@@ -1146,12 +1143,9 @@ async function getClient2() {
1146
1143
  console.error("Error: No API token configured. Run: forloop auth login --api-key <token>");
1147
1144
  process.exit(1);
1148
1145
  }
1149
- const client = new ForLoopAPIClient({ token, baseUrl: config.apiUrl });
1146
+ const client = new ForLoopAPIClient({ token, baseUrl: config.apiUrl, timeout: 3e4 });
1150
1147
  return { client };
1151
1148
  }
1152
- function formatDate(dateString) {
1153
- return new Date(dateString).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
1154
- }
1155
1149
  function createSprintGetCommand() {
1156
1150
  return defineCommand({
1157
1151
  name: "sprint get",
@@ -1163,7 +1157,7 @@ function createSprintGetCommand() {
1163
1157
  { flag: "--no-files", description: "Exclude files from response" }
1164
1158
  ],
1165
1159
  examples: ["forloop sprint get --id 456", "forloop sprint get --id 456 --no-files"],
1166
- async run(_config, flags) {
1160
+ async run(config, flags) {
1167
1161
  const resolved = await getClient2();
1168
1162
  if (!resolved) return;
1169
1163
  const { client } = resolved;
@@ -1176,20 +1170,7 @@ function createSprintGetCommand() {
1176
1170
  includeStories: !flags.noStories,
1177
1171
  includeFiles: !flags.noFiles
1178
1172
  });
1179
- const lines = [
1180
- `Sprint #${sprint.id}: ${sprint.title}`,
1181
- `Status: ${sprint.status}`,
1182
- `Period: ${formatDate(sprint.startDate)} - ${formatDate(sprint.endDate)}`,
1183
- `Stories: ${sprint.stories?.length || 0}`
1184
- ];
1185
- if (sprint.stories?.length) {
1186
- lines.push("", "Stories:", "");
1187
- for (const story of sprint.stories) {
1188
- const icons = { todo: "[ ]", in_progress: "[>]", done: "[x]", blocked: "[!]" };
1189
- lines.push(` ${icons[story.status] || "[?]"} #${story.id} ${story.title} (${story.status})`);
1190
- }
1191
- }
1192
- console.log(lines.join("\n"));
1173
+ print(config, sprint);
1193
1174
  }
1194
1175
  });
1195
1176
  }
@@ -1203,23 +1184,15 @@ function createSprintListCommand() {
1203
1184
  { flag: "--include-system-org", description: "Include system organization sprints" }
1204
1185
  ],
1205
1186
  examples: ["forloop sprint list", "forloop sprint list --org-id 1"],
1206
- async run(_config, flags) {
1187
+ async run(config, flags) {
1207
1188
  const resolved = await getClient2();
1208
1189
  if (!resolved) return;
1209
1190
  const { client } = resolved;
1210
1191
  const params = {};
1211
1192
  if (flags.orgId) params.organizationId = String(flags.orgId);
1193
+ if (flags.includeSystemOrg) params.includeSystemOrg = "true";
1212
1194
  const sprints = await client.listSprints(params);
1213
- if (!sprints.length) {
1214
- console.log("No sprints found.");
1215
- return;
1216
- }
1217
- const lines = ["Sprints:", ""];
1218
- for (const sprint of sprints) {
1219
- const orgInfo = sprint.organizationName ? ` (${sprint.organizationName})` : "";
1220
- lines.push(` #${sprint.id} ${sprint.title}${orgInfo} - ${sprint.status}`);
1221
- }
1222
- console.log(lines.join("\n"));
1195
+ print(config, sprints);
1223
1196
  }
1224
1197
  });
1225
1198
  }
@@ -1237,7 +1210,7 @@ function createSprintCreateCommand() {
1237
1210
  { flag: "--org-id <id>", description: "Organization ID", type: "number" }
1238
1211
  ],
1239
1212
  examples: ['forloop sprint create --title "Sprint 15" --start-date 2026-06-09 --end-date 2026-06-23'],
1240
- async run(_config, flags) {
1213
+ async run(config, flags) {
1241
1214
  const resolved = await getClient2();
1242
1215
  if (!resolved) return;
1243
1216
  const { client } = resolved;
@@ -1253,8 +1226,7 @@ function createSprintCreateCommand() {
1253
1226
  isPrivate: flags.private === true,
1254
1227
  organizationId: flags.orgId
1255
1228
  });
1256
- console.log(`Sprint created: #${sprint.id} - ${sprint.title}`);
1257
- console.log(`Period: ${formatDate(sprint.startDate)} - ${formatDate(sprint.endDate)}`);
1229
+ print(config, sprint, `Sprint created: #${sprint.id} - ${sprint.title}`);
1258
1230
  }
1259
1231
  });
1260
1232
  }
@@ -1272,7 +1244,7 @@ function createSprintUpdateCommand() {
1272
1244
  { flag: "--private", description: "Set visibility to private" }
1273
1245
  ],
1274
1246
  examples: ['forloop sprint update --id 456 --title "Updated Sprint"'],
1275
- async run(_config, flags) {
1247
+ async run(config, flags) {
1276
1248
  const resolved = await getClient2();
1277
1249
  if (!resolved) return;
1278
1250
  const { client } = resolved;
@@ -1287,7 +1259,7 @@ function createSprintUpdateCommand() {
1287
1259
  process.exit(1);
1288
1260
  }
1289
1261
  const sprint = await client.updateSprint(flags.id, updateData);
1290
- console.log(`Sprint #${sprint.id} updated successfully.`);
1262
+ print(config, sprint, `Sprint #${sprint.id} updated successfully.`);
1291
1263
  }
1292
1264
  });
1293
1265
  }
@@ -1301,7 +1273,7 @@ function createSprintDeleteCommand() {
1301
1273
  { flag: "--confirm", description: "Confirm deletion" }
1302
1274
  ],
1303
1275
  examples: ["forloop sprint delete --id 456 --confirm"],
1304
- async run(_config, flags) {
1276
+ async run(config, flags) {
1305
1277
  if (!flags.confirm) {
1306
1278
  console.error("Error: --confirm is required to permanently delete a sprint.");
1307
1279
  process.exit(1);
@@ -1309,21 +1281,13 @@ function createSprintDeleteCommand() {
1309
1281
  const resolved = await getClient2();
1310
1282
  if (!resolved) return;
1311
1283
  const { client } = resolved;
1312
- await client.deleteSprint(flags.id);
1313
- console.log(`Sprint #${flags.id} deleted successfully.`);
1284
+ const result = await client.deleteSprint(flags.id);
1285
+ print(config, result, `Sprint #${flags.id} deleted successfully.`);
1314
1286
  }
1315
1287
  });
1316
1288
  }
1317
1289
 
1318
1290
  // src/commands/org.ts
1319
- function createOrgCommands() {
1320
- return {
1321
- "org list": createOrgListCommand(),
1322
- "org get": createOrgGetCommand(),
1323
- "org create": createOrgCreateCommand(),
1324
- "org update": createOrgUpdateCommand()
1325
- };
1326
- }
1327
1291
  async function getClient3() {
1328
1292
  const config = getConfig();
1329
1293
  const token = await getToken();
@@ -1331,7 +1295,15 @@ async function getClient3() {
1331
1295
  console.error("Error: No API token configured. Run: forloop auth login --api-key <token>");
1332
1296
  process.exit(1);
1333
1297
  }
1334
- return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl }) };
1298
+ return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl, timeout: 3e4 }) };
1299
+ }
1300
+ function createOrgCommands() {
1301
+ return {
1302
+ "org list": createOrgListCommand(),
1303
+ "org get": createOrgGetCommand(),
1304
+ "org create": createOrgCreateCommand(),
1305
+ "org update": createOrgUpdateCommand()
1306
+ };
1335
1307
  }
1336
1308
  function createOrgListCommand() {
1337
1309
  return defineCommand({
@@ -1341,12 +1313,16 @@ function createOrgListCommand() {
1341
1313
  options: [
1342
1314
  { flag: "--owned-only", description: "List only organizations you own" }
1343
1315
  ],
1344
- examples: ["forloop org list", "forloop org list --owned-only"],
1345
- async run(_config, flags) {
1316
+ examples: ["forloop org list", "forloop org list --owned-only", "forloop org list --output json"],
1317
+ async run(config, flags) {
1346
1318
  const resolved = await getClient3();
1347
1319
  if (!resolved) return;
1348
1320
  const { client } = resolved;
1349
1321
  const orgs = flags.ownedOnly ? await client.getOwnedOrganizations() : await client.listOrganizations();
1322
+ if (config.output === "json") {
1323
+ print(config, orgs);
1324
+ return;
1325
+ }
1350
1326
  if (!orgs.length) {
1351
1327
  console.log("No organizations found.");
1352
1328
  return;
@@ -1368,19 +1344,16 @@ function createOrgGetCommand() {
1368
1344
  options: [
1369
1345
  { flag: "--id <number>", description: "Organization ID", required: true, type: "number" }
1370
1346
  ],
1371
- examples: ["forloop org get --id 1"],
1372
- async run(_config, flags) {
1347
+ examples: ["forloop org get --id 1", "forloop org get --id 1 --output json"],
1348
+ async run(config, flags) {
1373
1349
  if (!flags.id) {
1374
1350
  console.error("Error: --id is required");
1375
1351
  process.exit(1);
1376
1352
  }
1377
1353
  const resolved = await getClient3();
1378
1354
  if (!resolved) return;
1379
- const { client } = resolved;
1380
- const org = await client.getOrganization(flags.id);
1381
- console.log(`Organization #${org.id}: ${org.name}`);
1382
- console.log(`Type: ${org.type || "user"}`);
1383
- if (org.description) console.log(`Description: ${org.description}`);
1355
+ const org = await resolved.client.getOrganization(flags.id);
1356
+ print(config, org);
1384
1357
  }
1385
1358
  });
1386
1359
  }
@@ -1394,19 +1367,18 @@ function createOrgCreateCommand() {
1394
1367
  { flag: "--description <text>", description: "Organization description" }
1395
1368
  ],
1396
1369
  examples: ['forloop org create --name "My Team" --description "Engineering team"'],
1397
- async run(_config, flags) {
1370
+ async run(config, flags) {
1398
1371
  if (!flags.name) {
1399
1372
  console.error("Error: --name is required");
1400
1373
  process.exit(1);
1401
1374
  }
1402
1375
  const resolved = await getClient3();
1403
1376
  if (!resolved) return;
1404
- const { client } = resolved;
1405
- const org = await client.createOrganization({
1377
+ const org = await resolved.client.createOrganization({
1406
1378
  name: flags.name,
1407
1379
  description: flags.description
1408
1380
  });
1409
- console.log(`Organization created: #${org.id} - ${org.name}`);
1381
+ print(config, org, `Organization created: #${org.id} - ${org.name}`);
1410
1382
  }
1411
1383
  });
1412
1384
  }
@@ -1421,19 +1393,18 @@ function createOrgUpdateCommand() {
1421
1393
  { flag: "--description <text>", description: "New description" }
1422
1394
  ],
1423
1395
  examples: ['forloop org update --id 1 --name "New Name"'],
1424
- async run(_config, flags) {
1396
+ async run(config, flags) {
1425
1397
  if (!flags.id) {
1426
1398
  console.error("Error: --id is required");
1427
1399
  process.exit(1);
1428
1400
  }
1429
1401
  const resolved = await getClient3();
1430
1402
  if (!resolved) return;
1431
- const { client } = resolved;
1432
- const org = await client.updateOrganization(flags.id, {
1403
+ const org = await resolved.client.updateOrganization(flags.id, {
1433
1404
  name: flags.name,
1434
1405
  description: flags.description
1435
1406
  });
1436
- console.log(`Organization #${org.id} updated successfully.`);
1407
+ print(config, org, `Organization #${org.id} updated successfully.`);
1437
1408
  }
1438
1409
  });
1439
1410
  }
@@ -1447,30 +1418,41 @@ function createTemplateCommands() {
1447
1418
  function createTemplateListCommand() {
1448
1419
  return defineCommand({
1449
1420
  name: "template list",
1450
- description: "List available story templates",
1421
+ description: "List available story templates (basic-task, basic-note, schedule-meeting, doc-folder)",
1451
1422
  usage: "forloop template list",
1452
- examples: ["forloop template list"],
1453
- async run(_config) {
1454
- const config = getConfig();
1423
+ examples: ["forloop template list", "forloop template list --output json"],
1424
+ async run(config) {
1425
+ const apiConfig = getConfig();
1455
1426
  const token = await getToken();
1456
1427
  if (!token) {
1457
1428
  console.error("Error: No API token configured.");
1458
1429
  process.exit(1);
1459
1430
  }
1460
- const client = new ForLoopAPIClient({ token, baseUrl: config.apiUrl });
1431
+ const client = new ForLoopAPIClient({ token, baseUrl: apiConfig.apiUrl, timeout: 3e4 });
1461
1432
  const templates = await client.listTemplates();
1462
1433
  const active = templates.filter((t) => !t.deletedAt);
1434
+ if (config.output === "json") {
1435
+ print(config, active);
1436
+ return;
1437
+ }
1463
1438
  if (!active.length) {
1464
1439
  console.log("No templates found.");
1465
1440
  return;
1466
1441
  }
1442
+ const builtIn = [
1443
+ { name: "Basic Task", slug: "basic-task", description: "Standard task with title, description, priority, and story points", fields: [] },
1444
+ { name: "Basic Note", slug: "basic-note", description: "Simple note with title and description", fields: [] }
1445
+ ];
1446
+ const allTemplates = [...builtIn, ...active];
1467
1447
  const lines = ["Available Templates:", ""];
1468
- for (const t of active) {
1469
- const fields = JSON.parse(t.fields);
1470
- const fieldNames = fields.map((f) => f.label).join(", ");
1471
- lines.push(` ${t.name} (slug: ${t.slug})`);
1448
+ for (const t of allTemplates) {
1449
+ const fields = t.fields && typeof t.fields === "string" ? JSON.parse(t.fields) : Array.isArray(t.fields) ? t.fields : [];
1450
+ const fieldNames = Array.isArray(fields) ? fields.map((f) => f.label || f).join(", ") : "";
1451
+ const isBuiltIn = builtIn.some((b) => b.slug === t.slug);
1452
+ const badge = isBuiltIn ? " (built-in)" : "";
1453
+ lines.push(` ${t.name} (slug: ${t.slug})${badge}`);
1472
1454
  lines.push(` ${t.description}`);
1473
- lines.push(` Fields: ${fieldNames}`);
1455
+ if (fieldNames) lines.push(` Fields: ${fieldNames}`);
1474
1456
  lines.push("");
1475
1457
  }
1476
1458
  console.log(lines.join("\n"));
@@ -1493,7 +1475,7 @@ async function getClient4() {
1493
1475
  console.error("Error: No API token configured.");
1494
1476
  process.exit(1);
1495
1477
  }
1496
- return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl }) };
1478
+ return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl, timeout: 3e4 }) };
1497
1479
  }
1498
1480
  function createAgentHistoryCommand() {
1499
1481
  return defineCommand({
@@ -1505,7 +1487,7 @@ function createAgentHistoryCommand() {
1505
1487
  { flag: "--limit <n>", description: "Number of messages (max 200, default 50)", type: "number" }
1506
1488
  ],
1507
1489
  examples: ["forloop agent history --sprint 1 --limit 20"],
1508
- async run(_config, flags) {
1490
+ async run(config, flags) {
1509
1491
  const resolved = await getClient4();
1510
1492
  if (!resolved) return;
1511
1493
  const { client } = resolved;
@@ -1515,21 +1497,7 @@ function createAgentHistoryCommand() {
1515
1497
  sprintId,
1516
1498
  limit: flags.limit || 50
1517
1499
  });
1518
- const messages = history.messages || [];
1519
- if (!messages.length) {
1520
- console.log("No conversation history found.");
1521
- return;
1522
- }
1523
- console.log(`Conversation History (${history.total || messages.length} total):`);
1524
- for (const msg of messages) {
1525
- const preview = (msg.userMessage || "").substring(0, 80);
1526
- console.log(` ${preview}${preview.length >= 80 ? "..." : ""}`);
1527
- if (msg.agentResponse) {
1528
- const resp = msg.agentResponse.substring(0, 100);
1529
- console.log(` -> ${resp}${msg.agentResponse.length > 100 ? "..." : ""}`);
1530
- }
1531
- console.log("");
1532
- }
1500
+ print(config, history);
1533
1501
  }
1534
1502
  });
1535
1503
  }
@@ -1605,13 +1573,6 @@ function createDeveloperStatusCommand() {
1605
1573
  }
1606
1574
 
1607
1575
  // src/commands/user.ts
1608
- function createUserCommands() {
1609
- return {
1610
- "user profile": createUserProfileCommand(),
1611
- "user quotas": createUserQuotasCommand(),
1612
- "org quotas": createOrgQuotasCommand()
1613
- };
1614
- }
1615
1576
  async function getClient5() {
1616
1577
  const config = getConfig();
1617
1578
  const token = await getToken();
@@ -1619,22 +1580,26 @@ async function getClient5() {
1619
1580
  console.error("Error: No API token configured.");
1620
1581
  process.exit(1);
1621
1582
  }
1622
- return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl }) };
1583
+ return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl, timeout: 3e4 }) };
1584
+ }
1585
+ function createUserCommands() {
1586
+ return {
1587
+ "user profile": createUserProfileCommand(),
1588
+ "user quotas": createUserQuotasCommand(),
1589
+ "org quotas": createOrgQuotasCommand()
1590
+ };
1623
1591
  }
1624
1592
  function createUserProfileCommand() {
1625
1593
  return defineCommand({
1626
1594
  name: "user profile",
1627
1595
  description: "Get current user profile",
1628
1596
  usage: "forloop user profile",
1629
- examples: ["forloop user profile"],
1630
- async run(_config) {
1597
+ examples: ["forloop user profile", "forloop user profile --output json"],
1598
+ async run(config) {
1631
1599
  const resolved = await getClient5();
1632
1600
  if (!resolved) return;
1633
- const { client } = resolved;
1634
- const profile = await client.getUserProfile();
1635
- console.log(`User: ${profile.name || profile.email || "Unknown"}`);
1636
- console.log(`Email: ${profile.email}`);
1637
- if (profile.tier) console.log(`Tier: ${profile.tier}`);
1601
+ const profile = await resolved.client.getUserProfile();
1602
+ print(config, profile, "User Profile:");
1638
1603
  }
1639
1604
  });
1640
1605
  }
@@ -1643,14 +1608,12 @@ function createUserQuotasCommand() {
1643
1608
  name: "user quotas",
1644
1609
  description: "Check user quota limits",
1645
1610
  usage: "forloop user quotas",
1646
- examples: ["forloop user quotas"],
1647
- async run(_config) {
1611
+ examples: ["forloop user quotas", "forloop user quotas --output json"],
1612
+ async run(config) {
1648
1613
  const resolved = await getClient5();
1649
1614
  if (!resolved) return;
1650
- const { client } = resolved;
1651
- const quotas = await client.getUserQuotas();
1652
- console.log("User Quotas:");
1653
- console.log(JSON.stringify(quotas, null, 2));
1615
+ const quotas = await resolved.client.getUserQuotas();
1616
+ print(config, quotas, "User Quotas:");
1654
1617
  }
1655
1618
  });
1656
1619
  }
@@ -1662,18 +1625,16 @@ function createOrgQuotasCommand() {
1662
1625
  options: [
1663
1626
  { flag: "--org-id <id>", description: "Organization ID", required: true, type: "number" }
1664
1627
  ],
1665
- examples: ["forloop org quotas --org-id 1"],
1666
- async run(_config, flags) {
1628
+ examples: ["forloop org quotas --org-id 1", "forloop org quotas --org-id 1 --output json"],
1629
+ async run(config, flags) {
1667
1630
  if (!flags.orgId) {
1668
1631
  console.error("Error: --org-id is required");
1669
1632
  process.exit(1);
1670
1633
  }
1671
1634
  const resolved = await getClient5();
1672
1635
  if (!resolved) return;
1673
- const { client } = resolved;
1674
- const quotas = await client.getOrganizationQuotas(flags.orgId);
1675
- console.log("Organization Quotas:");
1676
- console.log(JSON.stringify(quotas, null, 2));
1636
+ const quotas = await resolved.client.getOrganizationQuotas(flags.orgId);
1637
+ print(config, quotas, "Organization Quotas:");
1677
1638
  }
1678
1639
  });
1679
1640
  }
@@ -1696,7 +1657,7 @@ async function getClient6() {
1696
1657
  console.error("Error: No API token configured.");
1697
1658
  process.exit(1);
1698
1659
  }
1699
- return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl }) };
1660
+ return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl, timeout: 3e4 }) };
1700
1661
  }
1701
1662
  var mimeTypes = {
1702
1663
  ".png": "image/png",
@@ -1727,10 +1688,11 @@ function createFileUploadCommand() {
1727
1688
  { flag: "--path <path>", description: "Local file path", required: true },
1728
1689
  { flag: "--sprint <id>", description: "Target sprint ID", required: true, type: "number" },
1729
1690
  { flag: "--description <text>", description: "File description" },
1730
- { flag: "--folder <text>", description: "S3 folder within sprint" }
1691
+ { flag: "--folder <text>", description: "S3 folder within sprint" },
1692
+ { flag: "--story-id <id>", description: "Link file to a story (e.g., doc_folder story)", type: "number" }
1731
1693
  ],
1732
1694
  examples: ["forloop file upload --path ./report.pdf --sprint 1 --folder docs"],
1733
- async run(_config, flags) {
1695
+ async run(config, flags) {
1734
1696
  const resolved = await getClient6();
1735
1697
  if (!resolved) return;
1736
1698
  const { client } = resolved;
@@ -1764,10 +1726,12 @@ function createFileUploadCommand() {
1764
1726
  originalName: fileName,
1765
1727
  fileType: contentType,
1766
1728
  size: fileContent.length,
1767
- folder: flags.folder
1729
+ folder: flags.folder,
1730
+ x: 0,
1731
+ y: 0,
1732
+ storyId: flags.storyId
1768
1733
  });
1769
- console.log(`File uploaded: ${fileName} (${formatSize(fileContent.length)})`);
1770
- console.log(`URL: ${complete.url}`);
1734
+ print(config, { fileName, size: fileContent.length, url: complete.url, fileId: complete.id }, `File uploaded: ${fileName} (${formatSize(fileContent.length)})`);
1771
1735
  }
1772
1736
  });
1773
1737
  }
@@ -1780,23 +1744,12 @@ function createFileListCommand() {
1780
1744
  { flag: "--sprint <id>", description: "Sprint ID", required: true, type: "number" }
1781
1745
  ],
1782
1746
  examples: ["forloop file list --sprint 1"],
1783
- async run(_config, flags) {
1747
+ async run(config, flags) {
1784
1748
  const resolved = await getClient6();
1785
1749
  if (!resolved) return;
1786
1750
  const { client } = resolved;
1787
1751
  const files = await client.getSprintFiles(flags.sprint);
1788
- if (!files.length) {
1789
- console.log("No files found.");
1790
- return;
1791
- }
1792
- const lines = [`Files in Sprint #${flags.sprint}:`, ""];
1793
- for (const file of files) {
1794
- lines.push(` ${file.originalName || file.filename}`);
1795
- lines.push(` Size: ${formatSize(file.size)} | Type: ${file.fileType}`);
1796
- lines.push(` URL: ${file.url}`);
1797
- lines.push("");
1798
- }
1799
- console.log(lines.join("\n"));
1752
+ print(config, files, `Files in Sprint #${flags.sprint}:`);
1800
1753
  }
1801
1754
  });
1802
1755
  }
@@ -1868,7 +1821,7 @@ function createFolderCreateCommand() {
1868
1821
  console.error("Error: No API token configured.");
1869
1822
  process.exit(1);
1870
1823
  }
1871
- const client = new ForLoopAPIClient({ token, baseUrl: config.apiUrl });
1824
+ const client = new ForLoopAPIClient({ token, baseUrl: config.apiUrl, timeout: 3e4 });
1872
1825
  const story = await client.createStory({
1873
1826
  title: flags.title,
1874
1827
  description: flags.description,
@@ -1900,7 +1853,7 @@ async function getClient7() {
1900
1853
  console.error("Error: No API token configured.");
1901
1854
  process.exit(1);
1902
1855
  }
1903
- return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl }) };
1856
+ return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl, timeout: 3e4 }) };
1904
1857
  }
1905
1858
  function createSyncAivyFolderCommand() {
1906
1859
  return defineCommand({
@@ -1949,7 +1902,7 @@ function createAivyDocGetCommand() {
1949
1902
  { flag: "--title <text>", description: 'Doc folder title (default: "forloop Aivy doc")' }
1950
1903
  ],
1951
1904
  examples: ["forloop sync aivy-doc-get --sprint 1"],
1952
- async run(_config, flags) {
1905
+ async run(config, flags) {
1953
1906
  const resolved = await getClient7();
1954
1907
  if (!resolved) return;
1955
1908
  const { client } = resolved;
@@ -1963,9 +1916,9 @@ function createAivyDocGetCommand() {
1963
1916
  const stories = Array.isArray(sprint?.stories) ? sprint.stories : [];
1964
1917
  const existing = stories.find((s) => s.type === "doc_folder" && s.title?.toLowerCase() === title.toLowerCase());
1965
1918
  if (existing?.id) {
1966
- console.log(`Doc folder: #${existing.id} "${existing.title}" (Sprint #${resolution.sprintId})`);
1919
+ print(config, { docFolderId: existing.id, exists: true, title: existing.title, sprintId: resolution.sprintId }, `Doc folder: #${existing.id} "${existing.title}" (Sprint #${resolution.sprintId})`);
1967
1920
  } else {
1968
- console.log(`Doc folder not found. Run: forloop sync aivy-folder --sprint ${resolution.sprintId}`);
1921
+ print(config, { exists: false, sprintId: resolution.sprintId }, `Doc folder not found. Run: forloop sync aivy-folder --sprint ${resolution.sprintId}`);
1969
1922
  }
1970
1923
  }
1971
1924
  });
@@ -2028,6 +1981,7 @@ function createLocalToS3Command() {
2028
1981
  { flag: "--path <path>", description: "Local file path", required: true },
2029
1982
  { flag: "--sprint <id>", description: "Target sprint ID", type: "number" },
2030
1983
  { flag: "--folder <text>", description: "Remote folder (e.g., project/plans)" },
1984
+ { flag: "--story-id <id>", description: "Link file to a story (e.g., doc_folder story)", type: "number" },
2031
1985
  { flag: "--delete", description: "Delete remote file instead of uploading" }
2032
1986
  ],
2033
1987
  examples: ["forloop sync local-to-s3 --path ~/.forloop/sprint-1/plan/plan.md --sprint 1"],
@@ -2066,7 +2020,10 @@ function createLocalToS3Command() {
2066
2020
  originalName: fileName,
2067
2021
  fileType: contentType,
2068
2022
  size: fileBuf.length,
2069
- folder
2023
+ folder,
2024
+ x: 0,
2025
+ y: 0,
2026
+ storyId: flags.storyId
2070
2027
  });
2071
2028
  console.log(`Synced: ${fileName} -> Sprint #${resolution.sprintId}`);
2072
2029
  }
@@ -2332,14 +2289,6 @@ var registry = new CommandRegistry({
2332
2289
  "agent sprint": agentDeveloperSprintCmd
2333
2290
  });
2334
2291
 
2335
- // src/output/formatter.ts
2336
- function formatError(message, format) {
2337
- if (format === "json") {
2338
- return JSON.stringify({ error: { message } }, null, 2);
2339
- }
2340
- return `\u274C ${message}`;
2341
- }
2342
-
2343
2292
  // src/errors/handler.ts
2344
2293
  function handleError(error) {
2345
2294
  const isJson = process.argv.includes("--output=json");
@@ -2362,7 +2311,7 @@ Tip: ${suggestion}
2362
2311
  }
2363
2312
 
2364
2313
  // src/main.ts
2365
- var CLI_VERSION = "0.2.3";
2314
+ var CLI_VERSION = "0.2.4";
2366
2315
  process.on("SIGINT", () => {
2367
2316
  process.stderr.write("\nInterrupted. Exiting.\n");
2368
2317
  process.exit(130);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forloop-cc/forloop-cli",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Command-line interface for the ForLoop AI development platform",
5
5
  "type": "module",
6
6
  "license": "MIT",