@forloop-cc/forloop-cli 0.2.2 → 0.2.4
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 +4 -86
- package/dist/forloop +888 -240
- package/package.json +1 -1
package/dist/forloop
CHANGED
|
@@ -28,7 +28,7 @@ function buildSchema(options) {
|
|
|
28
28
|
}
|
|
29
29
|
function scanCommandPath(argv, globalOptions = []) {
|
|
30
30
|
const globalSchema = buildSchema(globalOptions);
|
|
31
|
-
const
|
|
31
|
+
const path6 = [];
|
|
32
32
|
let i = 0;
|
|
33
33
|
while (i < argv.length) {
|
|
34
34
|
const arg = argv[i];
|
|
@@ -48,10 +48,10 @@ function scanCommandPath(argv, globalOptions = []) {
|
|
|
48
48
|
i++;
|
|
49
49
|
continue;
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
path6.push(arg);
|
|
52
52
|
i++;
|
|
53
53
|
}
|
|
54
|
-
return
|
|
54
|
+
return path6;
|
|
55
55
|
}
|
|
56
56
|
function parseFlags(argv, options) {
|
|
57
57
|
const schema = buildSchema(options);
|
|
@@ -142,8 +142,727 @@ var GLOBAL_OPTIONS = [
|
|
|
142
142
|
{ flag: "--version", description: "Print version" }
|
|
143
143
|
];
|
|
144
144
|
|
|
145
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/capabilities/token-storage.ts
|
|
146
|
+
import { promises as fs } from "fs";
|
|
147
|
+
import path from "path";
|
|
148
|
+
var getTokenPath = () => {
|
|
149
|
+
return path.join(
|
|
150
|
+
process.env.HOME || process.env.USERPROFILE || ".",
|
|
151
|
+
".config",
|
|
152
|
+
"forloop",
|
|
153
|
+
"tokens.json"
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
async function loadTokens() {
|
|
157
|
+
const tokenPath = getTokenPath();
|
|
158
|
+
try {
|
|
159
|
+
const content = await fs.readFile(tokenPath, "utf-8");
|
|
160
|
+
return JSON.parse(content);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error.code === "ENOENT") {
|
|
163
|
+
return {
|
|
164
|
+
version: "1.0",
|
|
165
|
+
note: "ForLoop API tokens - do not share",
|
|
166
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function saveTokens(tokens) {
|
|
173
|
+
const tokenPath = getTokenPath();
|
|
174
|
+
const configDir = path.dirname(tokenPath);
|
|
175
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
176
|
+
await fs.writeFile(
|
|
177
|
+
tokenPath,
|
|
178
|
+
JSON.stringify(tokens, null, 2),
|
|
179
|
+
{ mode: 384 }
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
async function getToken(profile) {
|
|
183
|
+
const tokens = await loadTokens();
|
|
184
|
+
if (profile && tokens.profiles?.[profile]) {
|
|
185
|
+
return tokens.profiles[profile];
|
|
186
|
+
}
|
|
187
|
+
if (tokens.default) return tokens.default;
|
|
188
|
+
if (tokens.profiles?.["default"]) return tokens.profiles["default"];
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
async function setToken(token, profile) {
|
|
192
|
+
const tokens = await loadTokens();
|
|
193
|
+
tokens.version = "1.0";
|
|
194
|
+
tokens.note = "ForLoop API tokens - do not share";
|
|
195
|
+
if (profile) {
|
|
196
|
+
if (!tokens.profiles) {
|
|
197
|
+
tokens.profiles = {};
|
|
198
|
+
}
|
|
199
|
+
tokens.profiles[profile] = token;
|
|
200
|
+
} else {
|
|
201
|
+
tokens.default = token;
|
|
202
|
+
}
|
|
203
|
+
tokens.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
204
|
+
await saveTokens(tokens);
|
|
205
|
+
}
|
|
206
|
+
async function clearToken(profile) {
|
|
207
|
+
const tokens = await loadTokens();
|
|
208
|
+
if (profile) {
|
|
209
|
+
if (tokens.profiles) {
|
|
210
|
+
delete tokens.profiles[profile];
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
delete tokens.default;
|
|
214
|
+
}
|
|
215
|
+
tokens.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
216
|
+
await saveTokens(tokens);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/capabilities/config.ts
|
|
220
|
+
import fs2 from "fs";
|
|
221
|
+
import path2 from "path";
|
|
222
|
+
var PROD_API_URL = "https://api.forloop.cc";
|
|
223
|
+
var DEV_API_URL = "https://api.dev.forloop.cc";
|
|
224
|
+
var PROD_TOKENS_URL = "https://forloop.cc/profile?tab=api-tokens";
|
|
225
|
+
var DEV_TOKENS_URL = "https://dev.forloop.cc/profile?tab=api-tokens";
|
|
226
|
+
var readJsonFileIfExists = (filePath) => {
|
|
227
|
+
try {
|
|
228
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
229
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
230
|
+
return JSON.parse(raw);
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
var getHomeDir = () => {
|
|
236
|
+
return process.env.HOME || process.env.USERPROFILE || "";
|
|
237
|
+
};
|
|
238
|
+
var getProjectConfig = () => {
|
|
239
|
+
return readJsonFileIfExists(path2.join(process.cwd(), "opencode.json"));
|
|
240
|
+
};
|
|
241
|
+
var getGlobalConfig = () => {
|
|
242
|
+
const home = getHomeDir();
|
|
243
|
+
if (!home) return null;
|
|
244
|
+
return readJsonFileIfExists(path2.join(home, ".config", "opencode", "config.json"));
|
|
245
|
+
};
|
|
246
|
+
var getNested = (obj, keys) => {
|
|
247
|
+
let cur = obj;
|
|
248
|
+
for (const key of keys) {
|
|
249
|
+
if (!cur || typeof cur !== "object") return void 0;
|
|
250
|
+
cur = cur[key];
|
|
251
|
+
}
|
|
252
|
+
return cur;
|
|
253
|
+
};
|
|
254
|
+
var isTruthy = (value) => {
|
|
255
|
+
if (typeof value === "boolean") return value;
|
|
256
|
+
if (typeof value === "string") return value.toLowerCase() === "true" || value === "1";
|
|
257
|
+
return false;
|
|
258
|
+
};
|
|
259
|
+
var normalizeUrl = (value) => {
|
|
260
|
+
if (typeof value !== "string") return null;
|
|
261
|
+
const trimmed = value.trim();
|
|
262
|
+
if (!trimmed) return null;
|
|
263
|
+
return trimmed;
|
|
264
|
+
};
|
|
265
|
+
var isDevUrl = (apiUrl) => {
|
|
266
|
+
return apiUrl.includes("api.dev.forloop.cc") || apiUrl.includes("dev.forloop.cc");
|
|
267
|
+
};
|
|
268
|
+
var resolveAllowDev = (projectConfig, globalConfig) => {
|
|
269
|
+
return isTruthy(process.env.FORLOOP_ALLOW_DEV) || isTruthy(getNested(projectConfig, ["forloop", "allowDev"])) || isTruthy(getNested(globalConfig, ["forloop", "allowDev"]));
|
|
270
|
+
};
|
|
271
|
+
var resolveApiUrl = (projectConfig, globalConfig, allowDev) => {
|
|
272
|
+
const envApiUrl = normalizeUrl(process.env.FORLOOP_API_URL || process.env.FORLOOP_API_BASE);
|
|
273
|
+
if (envApiUrl) {
|
|
274
|
+
if (isDevUrl(envApiUrl) && !allowDev) {
|
|
275
|
+
console.warn("[ForLoop Config] Dev API URL ignored (set FORLOOP_ALLOW_DEV=true to enable)");
|
|
276
|
+
return PROD_API_URL;
|
|
277
|
+
}
|
|
278
|
+
return envApiUrl;
|
|
279
|
+
}
|
|
280
|
+
const envName = normalizeUrl(process.env.FORLOOP_ENV);
|
|
281
|
+
if (envName) {
|
|
282
|
+
if (envName === "production") return PROD_API_URL;
|
|
283
|
+
if (envName === "development") {
|
|
284
|
+
if (!allowDev) {
|
|
285
|
+
console.warn("[ForLoop Config] FORLOOP_ENV=development ignored (set FORLOOP_ALLOW_DEV=true to enable)");
|
|
286
|
+
return PROD_API_URL;
|
|
287
|
+
}
|
|
288
|
+
return DEV_API_URL;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const projectApiUrl = normalizeUrl(getNested(projectConfig, ["forloop", "apiUrl"]));
|
|
292
|
+
if (projectApiUrl) {
|
|
293
|
+
if (isDevUrl(projectApiUrl) && !allowDev) {
|
|
294
|
+
console.warn("[ForLoop Config] Project dev API URL ignored (set forloop.allowDev=true or FORLOOP_ALLOW_DEV=true)");
|
|
295
|
+
return PROD_API_URL;
|
|
296
|
+
}
|
|
297
|
+
return projectApiUrl;
|
|
298
|
+
}
|
|
299
|
+
const globalApiUrl = normalizeUrl(getNested(globalConfig, ["forloop", "apiUrl"]));
|
|
300
|
+
if (globalApiUrl) {
|
|
301
|
+
if (isDevUrl(globalApiUrl) && !allowDev) {
|
|
302
|
+
console.warn("[ForLoop Config] Global dev API URL ignored (set forloop.allowDev=true or FORLOOP_ALLOW_DEV=true)");
|
|
303
|
+
return PROD_API_URL;
|
|
304
|
+
}
|
|
305
|
+
return globalApiUrl;
|
|
306
|
+
}
|
|
307
|
+
return PROD_API_URL;
|
|
308
|
+
};
|
|
309
|
+
var detectEnvironment = (apiUrl) => {
|
|
310
|
+
if (apiUrl.includes("dev.forloop.cc")) {
|
|
311
|
+
return "development";
|
|
312
|
+
}
|
|
313
|
+
if (apiUrl.includes("api.forloop.cc")) {
|
|
314
|
+
return "production";
|
|
315
|
+
}
|
|
316
|
+
return "custom";
|
|
317
|
+
};
|
|
318
|
+
var getConfig = () => {
|
|
319
|
+
const projectConfig = getProjectConfig();
|
|
320
|
+
const globalConfig = getGlobalConfig();
|
|
321
|
+
const allowDev = resolveAllowDev(projectConfig, globalConfig);
|
|
322
|
+
const apiUrl = resolveApiUrl(projectConfig, globalConfig, allowDev);
|
|
323
|
+
return {
|
|
324
|
+
apiUrl,
|
|
325
|
+
environment: detectEnvironment(apiUrl),
|
|
326
|
+
tokenStorage: "global",
|
|
327
|
+
debug: process.env.FORLOOP_DEBUG === "true",
|
|
328
|
+
allowDev
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
var getTokensUrl = () => {
|
|
332
|
+
const config = getConfig();
|
|
333
|
+
if (config.environment === "development") return DEV_TOKENS_URL;
|
|
334
|
+
return PROD_TOKENS_URL;
|
|
335
|
+
};
|
|
336
|
+
var isLambdaExecution = () => {
|
|
337
|
+
const forced = (process.env.FORLOOP_EXECUTION_CONTEXT || "").toLowerCase();
|
|
338
|
+
if (forced === "lambda" || forced === "aws_lambda" || forced === "aws-lambda") return true;
|
|
339
|
+
return !!(process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.AWS_EXECUTION_ENV || process.env.LAMBDA_TASK_ROOT);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/capabilities/auth.ts
|
|
343
|
+
function detectTokenType(token) {
|
|
344
|
+
if (!token) return null;
|
|
345
|
+
if (token.startsWith("floop_")) {
|
|
346
|
+
return "api_key";
|
|
347
|
+
}
|
|
348
|
+
const parts = token.split(".");
|
|
349
|
+
if (parts.length === 3 && parts.every((p) => p.length > 0)) {
|
|
350
|
+
const base64urlRegex = /^[A-Za-z0-9_-]+$/;
|
|
351
|
+
if (parts.every((p) => base64urlRegex.test(p))) {
|
|
352
|
+
return "jwt";
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
async function validateToken(providedToken) {
|
|
358
|
+
const token = providedToken || await getToken();
|
|
359
|
+
if (!token) {
|
|
360
|
+
return {
|
|
361
|
+
valid: false,
|
|
362
|
+
error: "No API token configured. Please create a token at " + getTokensUrl()
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const tokenType = detectTokenType(token);
|
|
366
|
+
if (!tokenType) {
|
|
367
|
+
return {
|
|
368
|
+
valid: false,
|
|
369
|
+
error: "Unrecognized token format. Use either a ForLoop API key (floop_*) or a valid JWT token."
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (tokenType === "jwt") {
|
|
373
|
+
const isLambda = isLambdaExecution();
|
|
374
|
+
if (!isLambda) {
|
|
375
|
+
return {
|
|
376
|
+
valid: false,
|
|
377
|
+
error: "JWT tokens are only supported in Lambda execution. Please use a ForLoop API key (floop_*) for local execution."
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
valid: true,
|
|
382
|
+
token,
|
|
383
|
+
tokenType: "jwt"
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
valid: true,
|
|
388
|
+
token,
|
|
389
|
+
tokenType: "api_key"
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/capabilities/api-client.ts
|
|
394
|
+
var APIError = class extends Error {
|
|
395
|
+
constructor(code, message, status) {
|
|
396
|
+
super(message);
|
|
397
|
+
this.code = code;
|
|
398
|
+
this.status = status;
|
|
399
|
+
this.name = "APIError";
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
var ForLoopAPIClient = class {
|
|
403
|
+
config;
|
|
404
|
+
defaultHeaders;
|
|
405
|
+
tokenType;
|
|
406
|
+
jwtExchangePromise;
|
|
407
|
+
constructor(config) {
|
|
408
|
+
this.config = {
|
|
409
|
+
timeout: 3e4,
|
|
410
|
+
retryCount: 3,
|
|
411
|
+
...config
|
|
412
|
+
};
|
|
413
|
+
this.defaultHeaders = {
|
|
414
|
+
"Content-Type": "application/json"
|
|
415
|
+
};
|
|
416
|
+
this.tokenType = detectTokenType(config.token);
|
|
417
|
+
this.jwtExchangePromise = null;
|
|
418
|
+
}
|
|
419
|
+
setToken(token) {
|
|
420
|
+
this.config.token = token;
|
|
421
|
+
this.tokenType = detectTokenType(token);
|
|
422
|
+
}
|
|
423
|
+
async ensureOpencodeApiToken(endpoint) {
|
|
424
|
+
if (this.tokenType !== "jwt") return;
|
|
425
|
+
if (!endpoint.startsWith("/api/opencode/")) return;
|
|
426
|
+
if (endpoint.startsWith("/api/opencode/auth/tokens")) return;
|
|
427
|
+
if (!this.jwtExchangePromise) {
|
|
428
|
+
this.jwtExchangePromise = this.exchangeJwtForApiToken();
|
|
429
|
+
}
|
|
430
|
+
const token = await this.jwtExchangePromise;
|
|
431
|
+
this.setToken(token);
|
|
432
|
+
}
|
|
433
|
+
async exchangeJwtForApiToken() {
|
|
434
|
+
const url = `${this.config.baseUrl}/api/opencode/auth/tokens`;
|
|
435
|
+
const jwt = this.config.token;
|
|
436
|
+
const scopes = [
|
|
437
|
+
"sprint:read",
|
|
438
|
+
"sprint:write",
|
|
439
|
+
"story:read",
|
|
440
|
+
"story:write",
|
|
441
|
+
"story:delete",
|
|
442
|
+
"agent:read",
|
|
443
|
+
"agent:query",
|
|
444
|
+
"profile:read",
|
|
445
|
+
"organization:read",
|
|
446
|
+
"organization:write",
|
|
447
|
+
"organization:delete"
|
|
448
|
+
];
|
|
449
|
+
const name = `lambda-planner-${Date.now()}`;
|
|
450
|
+
const response = await fetch(url, {
|
|
451
|
+
method: "POST",
|
|
452
|
+
headers: {
|
|
453
|
+
...this.defaultHeaders,
|
|
454
|
+
Authorization: `Bearer ${jwt}`
|
|
455
|
+
},
|
|
456
|
+
body: JSON.stringify({
|
|
457
|
+
name,
|
|
458
|
+
expiresIn: "7d",
|
|
459
|
+
scopes
|
|
460
|
+
}),
|
|
461
|
+
signal: AbortSignal.timeout(this.config.timeout)
|
|
462
|
+
});
|
|
463
|
+
if (!response.ok) {
|
|
464
|
+
throw await this.parseError(response);
|
|
465
|
+
}
|
|
466
|
+
const payload = await response.json();
|
|
467
|
+
const token = typeof payload?.token === "string" ? payload.token.trim() : "";
|
|
468
|
+
if (!token) {
|
|
469
|
+
throw new APIError("invalid_token_exchange", "Token exchange returned empty token", 500);
|
|
470
|
+
}
|
|
471
|
+
await setToken(token);
|
|
472
|
+
return token;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Get auth headers based on token type
|
|
476
|
+
* - JWT tokens: Authorization: Bearer <token>
|
|
477
|
+
* - API keys: X-API-Token: <token>
|
|
478
|
+
*/
|
|
479
|
+
getAuthHeaders() {
|
|
480
|
+
const headers = {};
|
|
481
|
+
if (this.tokenType === "jwt") {
|
|
482
|
+
headers["Authorization"] = `Bearer ${this.config.token}`;
|
|
483
|
+
} else {
|
|
484
|
+
headers["X-API-Token"] = this.config.token;
|
|
485
|
+
}
|
|
486
|
+
return headers;
|
|
487
|
+
}
|
|
488
|
+
async request(endpoint, options = {}) {
|
|
489
|
+
await this.ensureOpencodeApiToken(endpoint);
|
|
490
|
+
const url = `${this.config.baseUrl}${endpoint}`;
|
|
491
|
+
let lastError = null;
|
|
492
|
+
for (let attempt = 1; attempt <= this.config.retryCount; attempt++) {
|
|
493
|
+
try {
|
|
494
|
+
const response = await fetch(url, {
|
|
495
|
+
...options,
|
|
496
|
+
headers: {
|
|
497
|
+
...this.defaultHeaders,
|
|
498
|
+
...this.getAuthHeaders(),
|
|
499
|
+
// Dynamic auth headers based on token type
|
|
500
|
+
...options.headers || {}
|
|
501
|
+
},
|
|
502
|
+
signal: AbortSignal.timeout(this.config.timeout)
|
|
503
|
+
});
|
|
504
|
+
if (!response.ok) {
|
|
505
|
+
const error = await this.parseError(response);
|
|
506
|
+
throw error;
|
|
507
|
+
}
|
|
508
|
+
return response.json();
|
|
509
|
+
} catch (error) {
|
|
510
|
+
lastError = error;
|
|
511
|
+
if (error instanceof APIError && error.status >= 400 && error.status < 500) {
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
514
|
+
if (attempt < this.config.retryCount) {
|
|
515
|
+
await this.sleep(Math.pow(2, attempt) * 100);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
throw lastError || new Error("Request failed");
|
|
520
|
+
}
|
|
521
|
+
async parseError(response) {
|
|
522
|
+
try {
|
|
523
|
+
const body = await response.json();
|
|
524
|
+
return new APIError(body.error || "UnknownError", body.message || response.statusText, response.status);
|
|
525
|
+
} catch {
|
|
526
|
+
return new APIError("UnknownError", response.statusText, response.status);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
sleep(ms) {
|
|
530
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
531
|
+
}
|
|
532
|
+
async listSprints(params) {
|
|
533
|
+
const query = new URLSearchParams(params);
|
|
534
|
+
const response = await this.request(`/api/opencode/sprints?${query}`);
|
|
535
|
+
return Array.isArray(response) ? response : response.data || [];
|
|
536
|
+
}
|
|
537
|
+
async getSprint(id, options) {
|
|
538
|
+
const query = new URLSearchParams({
|
|
539
|
+
includeStories: options?.includeStories !== false ? "true" : "false",
|
|
540
|
+
includeFiles: options?.includeFiles !== false ? "true" : "false"
|
|
541
|
+
});
|
|
542
|
+
return this.request(`/api/opencode/sprints/${id}?${query}`);
|
|
543
|
+
}
|
|
544
|
+
async createSprint(data) {
|
|
545
|
+
return this.request("/api/opencode/sprints", {
|
|
546
|
+
method: "POST",
|
|
547
|
+
body: JSON.stringify(data)
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
async updateSprint(id, data) {
|
|
551
|
+
return this.request(`/api/opencode/sprints/${id}`, {
|
|
552
|
+
method: "PUT",
|
|
553
|
+
body: JSON.stringify(data)
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
async deleteSprint(id) {
|
|
557
|
+
await this.request(`/api/opencode/sprints/${id}`, {
|
|
558
|
+
method: "DELETE"
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
async createStory(data) {
|
|
562
|
+
return this.request("/api/opencode/stories", {
|
|
563
|
+
method: "POST",
|
|
564
|
+
body: JSON.stringify(data)
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
async getStory(id, options) {
|
|
568
|
+
const params = new URLSearchParams();
|
|
569
|
+
if (options?.includeComments) params.set("includeComments", "true");
|
|
570
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
571
|
+
return this.request(`/api/opencode/stories/${id}${query}`);
|
|
572
|
+
}
|
|
573
|
+
async updateStory(id, data) {
|
|
574
|
+
return this.request(`/api/opencode/stories/${id}`, {
|
|
575
|
+
method: "PUT",
|
|
576
|
+
body: JSON.stringify(data)
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
async deleteStory(id) {
|
|
580
|
+
await this.request(`/api/opencode/stories/${id}`, {
|
|
581
|
+
method: "DELETE"
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
async listTemplates() {
|
|
585
|
+
return this.request("/api/opencode/templates");
|
|
586
|
+
}
|
|
587
|
+
async getUserProfile() {
|
|
588
|
+
return this.request("/api/opencode/user/profile");
|
|
589
|
+
}
|
|
590
|
+
async getUserQuotas() {
|
|
591
|
+
return this.request("/api/opencode/user/quotas");
|
|
592
|
+
}
|
|
593
|
+
async getOrganizationQuotas(orgId) {
|
|
594
|
+
return this.request(`/api/opencode/organization/${orgId}/quotas`);
|
|
595
|
+
}
|
|
596
|
+
async listOrganizations() {
|
|
597
|
+
return this.request("/api/opencode/organizations");
|
|
598
|
+
}
|
|
599
|
+
async getOwnedOrganizations() {
|
|
600
|
+
return this.request("/api/opencode/organizations/owned");
|
|
601
|
+
}
|
|
602
|
+
async getOrganization(id) {
|
|
603
|
+
return this.request(`/api/opencode/organizations/${id}`);
|
|
604
|
+
}
|
|
605
|
+
async createOrganization(data) {
|
|
606
|
+
return this.request("/api/opencode/organizations", {
|
|
607
|
+
method: "POST",
|
|
608
|
+
body: JSON.stringify(data)
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
async updateOrganization(id, data) {
|
|
612
|
+
return this.request(`/api/opencode/organizations/${id}`, {
|
|
613
|
+
method: "PUT",
|
|
614
|
+
body: JSON.stringify(data)
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
async deleteOrganization(id) {
|
|
618
|
+
await this.request(`/api/opencode/organizations/${id}`, {
|
|
619
|
+
method: "DELETE"
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
async chatWithAI(request) {
|
|
623
|
+
return this.request("/api/opencode/agents/chat", {
|
|
624
|
+
method: "POST",
|
|
625
|
+
body: JSON.stringify(request)
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
async getDeveloperStatus(sprintId) {
|
|
629
|
+
return this.request(`/api/opencode/sprints/${sprintId}/developer-status`);
|
|
630
|
+
}
|
|
631
|
+
async getStorySuggestions(sprintId, storyId) {
|
|
632
|
+
return this.request("/api/opencode/agents/suggestions", {
|
|
633
|
+
method: "POST",
|
|
634
|
+
body: JSON.stringify({ sprintId, storyId })
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
async getAITasks(sprintId) {
|
|
638
|
+
const query = sprintId ? new URLSearchParams({ sprintId: String(sprintId) }) : new URLSearchParams();
|
|
639
|
+
return this.request(`/api/opencode/agents/tasks?${query}`);
|
|
640
|
+
}
|
|
641
|
+
async getSprintSummaries(sprintId) {
|
|
642
|
+
return this.request(`/api/opencode/agents/summary?sprintId=${sprintId}`);
|
|
643
|
+
}
|
|
644
|
+
async getAgentSuggestions(request) {
|
|
645
|
+
return this.request("/api/agents/suggestions", {
|
|
646
|
+
method: "POST",
|
|
647
|
+
body: JSON.stringify(request)
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
async getStoryBreakdown(storyId) {
|
|
651
|
+
return this.request(`/api/agents/stories/${storyId}/breakdown`);
|
|
652
|
+
}
|
|
653
|
+
async getStoryEstimate(storyId) {
|
|
654
|
+
return this.request(`/api/agents/stories/${storyId}/estimate`);
|
|
655
|
+
}
|
|
656
|
+
async getConversationHistory(params) {
|
|
657
|
+
const query = new URLSearchParams();
|
|
658
|
+
if (params?.sprintId) query.append("sprintId", String(params.sprintId));
|
|
659
|
+
if (params?.limit) query.append("limit", String(params.limit));
|
|
660
|
+
return this.request(`/api/opencode/messages?${query}`);
|
|
661
|
+
}
|
|
662
|
+
async clearConversationHistory(params) {
|
|
663
|
+
const query = new URLSearchParams();
|
|
664
|
+
if (params?.sprintId) query.append("sprintId", String(params.sprintId));
|
|
665
|
+
query.append("confirm", "true");
|
|
666
|
+
return this.request(`/api/opencode/messages?${query}`, {
|
|
667
|
+
method: "DELETE"
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
async createPresignedUpload(data) {
|
|
671
|
+
return this.request("/api/opencode/files/presign", {
|
|
672
|
+
method: "POST",
|
|
673
|
+
body: JSON.stringify(data)
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
async completeUpload(data) {
|
|
677
|
+
return this.request("/api/opencode/files/complete", {
|
|
678
|
+
method: "POST",
|
|
679
|
+
body: JSON.stringify(data)
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
async getSprintFiles(sprintId) {
|
|
683
|
+
return this.request(`/api/opencode/files/sprint/${sprintId}`);
|
|
684
|
+
}
|
|
685
|
+
async deleteFile(fileId) {
|
|
686
|
+
await this.request(`/api/opencode/files/${fileId}`, {
|
|
687
|
+
method: "DELETE"
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
async logFileRead(fileId) {
|
|
691
|
+
await this.request(`/api/opencode/files/${fileId}/read`, {
|
|
692
|
+
method: "POST"
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
async getFileDownloadUrl(fileId) {
|
|
696
|
+
return this.request(`/api/opencode/files/download/${fileId}`);
|
|
697
|
+
}
|
|
698
|
+
async getFileOpenUrl(fileId) {
|
|
699
|
+
return this.request(`/api/opencode/files/open-url/${fileId}`);
|
|
700
|
+
}
|
|
701
|
+
async updateFilePosition(fileId, data) {
|
|
702
|
+
return this.request(`/api/opencode/files/${fileId}/position`, {
|
|
703
|
+
method: "PUT",
|
|
704
|
+
body: JSON.stringify(data)
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
async listAiAgents() {
|
|
708
|
+
const response = await this.request("/api/opencode/ai/agents");
|
|
709
|
+
return response.agents || [];
|
|
710
|
+
}
|
|
711
|
+
async updateSprintAiAgents(sprintId, enabledAgentKeys) {
|
|
712
|
+
const response = await this.request(`/api/opencode/sprints/${sprintId}/ai-agents`, {
|
|
713
|
+
method: "PUT",
|
|
714
|
+
body: JSON.stringify({ enabledAgentKeys })
|
|
715
|
+
});
|
|
716
|
+
return response.sprintAiAgents || [];
|
|
717
|
+
}
|
|
718
|
+
async updateSchedule(storyId, data) {
|
|
719
|
+
return this.updateStory(storyId, {
|
|
720
|
+
title: data.title,
|
|
721
|
+
description: data.description,
|
|
722
|
+
metadata: data
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
async recordMessage(data) {
|
|
726
|
+
return this.request("/api/opencode/messages", {
|
|
727
|
+
method: "POST",
|
|
728
|
+
body: JSON.stringify(data)
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
async removeMessage(messageId, sprintId, sessionId) {
|
|
732
|
+
await this.request(`/api/opencode/messages/${encodeURIComponent(messageId)}`, {
|
|
733
|
+
method: "DELETE",
|
|
734
|
+
body: JSON.stringify({ sprintId, sessionId })
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/tools/sprint-tools.ts
|
|
740
|
+
import { tool } from "@opencode-ai/plugin";
|
|
741
|
+
|
|
742
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/capabilities/context-resolver.ts
|
|
743
|
+
import { exec } from "child_process";
|
|
744
|
+
import { promisify } from "util";
|
|
745
|
+
import * as fs3 from "fs";
|
|
746
|
+
import * as path3 from "path";
|
|
747
|
+
var execAsync = promisify(exec);
|
|
748
|
+
async function getCurrentBranch(directory) {
|
|
749
|
+
try {
|
|
750
|
+
const result = await execAsync("git rev-parse --abbrev-ref HEAD", {
|
|
751
|
+
cwd: directory
|
|
752
|
+
});
|
|
753
|
+
return result.stdout.trim();
|
|
754
|
+
} catch (error) {
|
|
755
|
+
throw new Error("Git not available or not in a repository");
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
async function resolveSprintFromBranch(branch) {
|
|
759
|
+
const match = branch.match(/sprint-(\d+)/);
|
|
760
|
+
if (match) {
|
|
761
|
+
return parseInt(match[1], 10);
|
|
762
|
+
}
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
async function resolveSprintId(providedId, directory) {
|
|
766
|
+
if (providedId) {
|
|
767
|
+
return { sprintId: providedId, source: "provided" };
|
|
768
|
+
}
|
|
769
|
+
const envSprintId = process.env.FORLOOP_SPRINT_ID;
|
|
770
|
+
if (envSprintId) {
|
|
771
|
+
return { sprintId: parseInt(envSprintId, 10), source: "env" };
|
|
772
|
+
}
|
|
773
|
+
const manifestSprintId = getActiveSprintIdFromManifest(directory);
|
|
774
|
+
if (manifestSprintId) {
|
|
775
|
+
return { sprintId: manifestSprintId, source: "manifest" };
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
const branch = await getCurrentBranch(directory);
|
|
779
|
+
const sprintId = await resolveSprintFromBranch(branch);
|
|
780
|
+
if (sprintId) {
|
|
781
|
+
return { sprintId, branch, source: "branch" };
|
|
782
|
+
}
|
|
783
|
+
} catch (error) {
|
|
784
|
+
}
|
|
785
|
+
return { source: "config" };
|
|
786
|
+
}
|
|
787
|
+
function getActiveSprintIdFromManifest(directory) {
|
|
788
|
+
const candidates = [];
|
|
789
|
+
if (directory) {
|
|
790
|
+
candidates.push(path3.join(directory, ".forloop", "manifest.json"));
|
|
791
|
+
}
|
|
792
|
+
const home = process.env.HOME;
|
|
793
|
+
if (home) {
|
|
794
|
+
candidates.push(path3.join(home, ".forloop", "manifest.json"));
|
|
795
|
+
}
|
|
796
|
+
for (const filePath of candidates) {
|
|
797
|
+
try {
|
|
798
|
+
if (!fs3.existsSync(filePath)) continue;
|
|
799
|
+
const raw = fs3.readFileSync(filePath, "utf8");
|
|
800
|
+
const parsed = JSON.parse(raw);
|
|
801
|
+
const id = parsed?.activeSprintId;
|
|
802
|
+
if (Number.isFinite(id)) return Number(id);
|
|
803
|
+
} catch {
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
var DEFAULT_HOME = "/tmp/home";
|
|
809
|
+
function getSprintDir(sprintId) {
|
|
810
|
+
return `sprint-${sprintId}`;
|
|
811
|
+
}
|
|
812
|
+
function getForloopRoot(sprintId) {
|
|
813
|
+
const home = process.env.HOME || DEFAULT_HOME;
|
|
814
|
+
const root = path3.join(home, ".forloop");
|
|
815
|
+
if (sprintId !== void 0) {
|
|
816
|
+
return path3.join(root, getSprintDir(sprintId));
|
|
817
|
+
}
|
|
818
|
+
return root;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/tools/story-tools.ts
|
|
822
|
+
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
823
|
+
|
|
824
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/tools/template-tools.ts
|
|
825
|
+
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
826
|
+
|
|
827
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/tools/user-tools.ts
|
|
828
|
+
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
829
|
+
|
|
830
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/tools/organization-tools.ts
|
|
831
|
+
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
832
|
+
|
|
833
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/tools/file-tools.ts
|
|
834
|
+
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
835
|
+
|
|
836
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/tools/schedule-tools.ts
|
|
837
|
+
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
838
|
+
|
|
839
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/tools/folder-tools.ts
|
|
840
|
+
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
841
|
+
|
|
842
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/tools/sync-tools.ts
|
|
843
|
+
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
844
|
+
|
|
845
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/tools/agent-tools.ts
|
|
846
|
+
import { tool as tool10 } from "@opencode-ai/plugin";
|
|
847
|
+
|
|
848
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/tools/agent-suggestion-tools.ts
|
|
849
|
+
import { tool as tool11 } from "@opencode-ai/plugin";
|
|
850
|
+
|
|
851
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/tools/agent-sprint-tools.ts
|
|
852
|
+
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
853
|
+
|
|
854
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/tools/ai-trigger-tools.ts
|
|
855
|
+
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
856
|
+
|
|
857
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/tools/developer-status-tool.ts
|
|
858
|
+
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
859
|
+
|
|
860
|
+
// node_modules/@forloop-cc/forloop-opencode-plugin-planner/plugins/hooks/shell-env.ts
|
|
861
|
+
import { exec as exec2 } from "child_process";
|
|
862
|
+
import { promisify as promisify2 } from "util";
|
|
863
|
+
var execAsync2 = promisify2(exec2);
|
|
864
|
+
|
|
145
865
|
// src/commands/auth/index.ts
|
|
146
|
-
import { getToken, setToken, clearToken, validateToken } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
147
866
|
function createAuthCommands() {
|
|
148
867
|
return {
|
|
149
868
|
"auth login": createAuthLoginCommand(),
|
|
@@ -193,7 +912,6 @@ function createAuthStatusCommand() {
|
|
|
193
912
|
} else {
|
|
194
913
|
console.log("Not authenticated");
|
|
195
914
|
console.log("Run: forloop auth login --api-key <token>");
|
|
196
|
-
process.exit(1);
|
|
197
915
|
}
|
|
198
916
|
}
|
|
199
917
|
});
|
|
@@ -210,8 +928,26 @@ function createAuthLogoutCommand() {
|
|
|
210
928
|
});
|
|
211
929
|
}
|
|
212
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
|
+
|
|
213
950
|
// src/commands/story.ts
|
|
214
|
-
import { ForLoopAPIClient, getToken as getToken2, getConfig } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
215
951
|
function createStoryCommands() {
|
|
216
952
|
return {
|
|
217
953
|
"story create": createStoryCreateCommand(),
|
|
@@ -222,12 +958,12 @@ function createStoryCommands() {
|
|
|
222
958
|
}
|
|
223
959
|
async function getClient() {
|
|
224
960
|
const config = getConfig();
|
|
225
|
-
const token = await
|
|
961
|
+
const token = await getToken();
|
|
226
962
|
if (!token) {
|
|
227
963
|
console.error("Error: No API token configured. Run: forloop auth login --api-key <token>");
|
|
228
964
|
process.exit(1);
|
|
229
965
|
}
|
|
230
|
-
const client = new ForLoopAPIClient({ token, baseUrl: config.apiUrl });
|
|
966
|
+
const client = new ForLoopAPIClient({ token, baseUrl: config.apiUrl, timeout: 3e4 });
|
|
231
967
|
return { client };
|
|
232
968
|
}
|
|
233
969
|
function createStoryCreateCommand() {
|
|
@@ -249,7 +985,7 @@ function createStoryCreateCommand() {
|
|
|
249
985
|
'forloop story create --title "Meeting notes" --sprint 1 --type basic-note',
|
|
250
986
|
'forloop story create --title "Design Docs" --sprint 1'
|
|
251
987
|
],
|
|
252
|
-
async run(
|
|
988
|
+
async run(config, flags) {
|
|
253
989
|
const resolved = await getClient();
|
|
254
990
|
if (!resolved) return;
|
|
255
991
|
const { client } = resolved;
|
|
@@ -289,7 +1025,7 @@ function createStoryCreateCommand() {
|
|
|
289
1025
|
points: flags.points
|
|
290
1026
|
})
|
|
291
1027
|
});
|
|
292
|
-
|
|
1028
|
+
print(config, story, `Story created: #${story.id} - ${story.title} (${story.type})`);
|
|
293
1029
|
} else {
|
|
294
1030
|
const story = await client.createStory({
|
|
295
1031
|
title: flags.title,
|
|
@@ -299,7 +1035,7 @@ function createStoryCreateCommand() {
|
|
|
299
1035
|
priority: flags.priority || "medium",
|
|
300
1036
|
status: "todo"
|
|
301
1037
|
});
|
|
302
|
-
|
|
1038
|
+
print(config, story, `Doc folder created: #${story.id} - ${story.title}`);
|
|
303
1039
|
}
|
|
304
1040
|
}
|
|
305
1041
|
});
|
|
@@ -314,7 +1050,7 @@ function createStoryGetCommand() {
|
|
|
314
1050
|
{ flag: "--no-comments", description: "Exclude comments" }
|
|
315
1051
|
],
|
|
316
1052
|
examples: ["forloop story get --id 123"],
|
|
317
|
-
async run(
|
|
1053
|
+
async run(config, flags) {
|
|
318
1054
|
const resolved = await getClient();
|
|
319
1055
|
if (!resolved) return;
|
|
320
1056
|
const { client } = resolved;
|
|
@@ -324,28 +1060,7 @@ function createStoryGetCommand() {
|
|
|
324
1060
|
process.exit(1);
|
|
325
1061
|
}
|
|
326
1062
|
const story = await client.getStory(storyId, { includeComments: !flags.noComments });
|
|
327
|
-
|
|
328
|
-
`Story #${story.id}: ${story.title}`,
|
|
329
|
-
`Sprint: #${story.sprint?.id || "N/A"} ${story.sprint?.title || ""}`,
|
|
330
|
-
`Status: ${story.status}`,
|
|
331
|
-
`Priority: ${story.priority}`,
|
|
332
|
-
`Type: ${story.type}`
|
|
333
|
-
];
|
|
334
|
-
if (story.points) lines.push(`Points: ${story.points}`);
|
|
335
|
-
if (story.assignee) lines.push(`Assignee: ${story.assignee.name || story.assignee.email || "Unknown"}`);
|
|
336
|
-
if (story.assigneeAgentKey) lines.push(`Agent: ${story.assigneeAgentKey}`);
|
|
337
|
-
if (story.description) {
|
|
338
|
-
lines.push("", "Description:", story.description);
|
|
339
|
-
}
|
|
340
|
-
if (!flags.noComments && story.comments?.length) {
|
|
341
|
-
lines.push("", `--- Comments (${story.comments.length}) ---`);
|
|
342
|
-
for (const comment of story.comments) {
|
|
343
|
-
const author = comment.authorAgentKey || comment.user?.name || "Unknown";
|
|
344
|
-
lines.push(` ${author}: ${comment.content || comment.body || ""}`);
|
|
345
|
-
if (comment.artifacts?.length) lines.push(` Artifacts: ${comment.artifacts.join(", ")}`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
console.log(lines.join("\n"));
|
|
1063
|
+
print(config, story);
|
|
349
1064
|
}
|
|
350
1065
|
});
|
|
351
1066
|
}
|
|
@@ -363,7 +1078,7 @@ function createStoryUpdateCommand() {
|
|
|
363
1078
|
{ flag: "--points <n>", description: "Story points (0-10)", type: "number" }
|
|
364
1079
|
],
|
|
365
1080
|
examples: ["forloop story update --id 123 --status in_progress"],
|
|
366
|
-
async run(
|
|
1081
|
+
async run(config, flags) {
|
|
367
1082
|
const resolved = await getClient();
|
|
368
1083
|
if (!resolved) return;
|
|
369
1084
|
const { client } = resolved;
|
|
@@ -383,7 +1098,7 @@ function createStoryUpdateCommand() {
|
|
|
383
1098
|
process.exit(1);
|
|
384
1099
|
}
|
|
385
1100
|
const story = await client.updateStory(storyId, data);
|
|
386
|
-
|
|
1101
|
+
print(config, story, `Story #${story.id} updated successfully.`);
|
|
387
1102
|
}
|
|
388
1103
|
});
|
|
389
1104
|
}
|
|
@@ -397,7 +1112,7 @@ function createStoryDeleteCommand() {
|
|
|
397
1112
|
{ flag: "--confirm", description: "Confirm deletion" }
|
|
398
1113
|
],
|
|
399
1114
|
examples: ["forloop story delete --id 123 --confirm"],
|
|
400
|
-
async run(
|
|
1115
|
+
async run(config, flags) {
|
|
401
1116
|
if (!flags.confirm) {
|
|
402
1117
|
console.error("Error: --confirm is required to permanently delete a story.");
|
|
403
1118
|
process.exit(1);
|
|
@@ -406,13 +1121,12 @@ function createStoryDeleteCommand() {
|
|
|
406
1121
|
if (!resolved) return;
|
|
407
1122
|
const { client } = resolved;
|
|
408
1123
|
await client.deleteStory(flags.id);
|
|
409
|
-
|
|
1124
|
+
print(config, { deleted: flags.id }, `Story #${flags.id} deleted successfully.`);
|
|
410
1125
|
}
|
|
411
1126
|
});
|
|
412
1127
|
}
|
|
413
1128
|
|
|
414
1129
|
// src/commands/sprint.ts
|
|
415
|
-
import { ForLoopAPIClient as ForLoopAPIClient2, getToken as getToken3, resolveSprintId as resolveSprintId2, getConfig as getConfig2 } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
416
1130
|
function createSprintCommands() {
|
|
417
1131
|
return {
|
|
418
1132
|
"sprint get": createSprintGetCommand(),
|
|
@@ -423,18 +1137,15 @@ function createSprintCommands() {
|
|
|
423
1137
|
};
|
|
424
1138
|
}
|
|
425
1139
|
async function getClient2() {
|
|
426
|
-
const config =
|
|
427
|
-
const token = await
|
|
1140
|
+
const config = getConfig();
|
|
1141
|
+
const token = await getToken();
|
|
428
1142
|
if (!token) {
|
|
429
1143
|
console.error("Error: No API token configured. Run: forloop auth login --api-key <token>");
|
|
430
1144
|
process.exit(1);
|
|
431
1145
|
}
|
|
432
|
-
const client = new
|
|
1146
|
+
const client = new ForLoopAPIClient({ token, baseUrl: config.apiUrl, timeout: 3e4 });
|
|
433
1147
|
return { client };
|
|
434
1148
|
}
|
|
435
|
-
function formatDate(dateString) {
|
|
436
|
-
return new Date(dateString).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
437
|
-
}
|
|
438
1149
|
function createSprintGetCommand() {
|
|
439
1150
|
return defineCommand({
|
|
440
1151
|
name: "sprint get",
|
|
@@ -446,11 +1157,11 @@ function createSprintGetCommand() {
|
|
|
446
1157
|
{ flag: "--no-files", description: "Exclude files from response" }
|
|
447
1158
|
],
|
|
448
1159
|
examples: ["forloop sprint get --id 456", "forloop sprint get --id 456 --no-files"],
|
|
449
|
-
async run(
|
|
1160
|
+
async run(config, flags) {
|
|
450
1161
|
const resolved = await getClient2();
|
|
451
1162
|
if (!resolved) return;
|
|
452
1163
|
const { client } = resolved;
|
|
453
|
-
const resolution = await
|
|
1164
|
+
const resolution = await resolveSprintId(flags.id);
|
|
454
1165
|
if (!resolution.sprintId) {
|
|
455
1166
|
console.error("Error: No sprint ID resolved. Use --id, set FORLOOP_SPRINT_ID, or use a sprint-XXX branch.");
|
|
456
1167
|
process.exit(1);
|
|
@@ -459,20 +1170,7 @@ function createSprintGetCommand() {
|
|
|
459
1170
|
includeStories: !flags.noStories,
|
|
460
1171
|
includeFiles: !flags.noFiles
|
|
461
1172
|
});
|
|
462
|
-
|
|
463
|
-
`Sprint #${sprint.id}: ${sprint.title}`,
|
|
464
|
-
`Status: ${sprint.status}`,
|
|
465
|
-
`Period: ${formatDate(sprint.startDate)} - ${formatDate(sprint.endDate)}`,
|
|
466
|
-
`Stories: ${sprint.stories?.length || 0}`
|
|
467
|
-
];
|
|
468
|
-
if (sprint.stories?.length) {
|
|
469
|
-
lines.push("", "Stories:", "");
|
|
470
|
-
for (const story of sprint.stories) {
|
|
471
|
-
const icons = { todo: "[ ]", in_progress: "[>]", done: "[x]", blocked: "[!]" };
|
|
472
|
-
lines.push(` ${icons[story.status] || "[?]"} #${story.id} ${story.title} (${story.status})`);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
console.log(lines.join("\n"));
|
|
1173
|
+
print(config, sprint);
|
|
476
1174
|
}
|
|
477
1175
|
});
|
|
478
1176
|
}
|
|
@@ -486,23 +1184,15 @@ function createSprintListCommand() {
|
|
|
486
1184
|
{ flag: "--include-system-org", description: "Include system organization sprints" }
|
|
487
1185
|
],
|
|
488
1186
|
examples: ["forloop sprint list", "forloop sprint list --org-id 1"],
|
|
489
|
-
async run(
|
|
1187
|
+
async run(config, flags) {
|
|
490
1188
|
const resolved = await getClient2();
|
|
491
1189
|
if (!resolved) return;
|
|
492
1190
|
const { client } = resolved;
|
|
493
1191
|
const params = {};
|
|
494
1192
|
if (flags.orgId) params.organizationId = String(flags.orgId);
|
|
1193
|
+
if (flags.includeSystemOrg) params.includeSystemOrg = "true";
|
|
495
1194
|
const sprints = await client.listSprints(params);
|
|
496
|
-
|
|
497
|
-
console.log("No sprints found.");
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
const lines = ["Sprints:", ""];
|
|
501
|
-
for (const sprint of sprints) {
|
|
502
|
-
const orgInfo = sprint.organizationName ? ` (${sprint.organizationName})` : "";
|
|
503
|
-
lines.push(` #${sprint.id} ${sprint.title}${orgInfo} - ${sprint.status}`);
|
|
504
|
-
}
|
|
505
|
-
console.log(lines.join("\n"));
|
|
1195
|
+
print(config, sprints);
|
|
506
1196
|
}
|
|
507
1197
|
});
|
|
508
1198
|
}
|
|
@@ -520,7 +1210,7 @@ function createSprintCreateCommand() {
|
|
|
520
1210
|
{ flag: "--org-id <id>", description: "Organization ID", type: "number" }
|
|
521
1211
|
],
|
|
522
1212
|
examples: ['forloop sprint create --title "Sprint 15" --start-date 2026-06-09 --end-date 2026-06-23'],
|
|
523
|
-
async run(
|
|
1213
|
+
async run(config, flags) {
|
|
524
1214
|
const resolved = await getClient2();
|
|
525
1215
|
if (!resolved) return;
|
|
526
1216
|
const { client } = resolved;
|
|
@@ -536,8 +1226,7 @@ function createSprintCreateCommand() {
|
|
|
536
1226
|
isPrivate: flags.private === true,
|
|
537
1227
|
organizationId: flags.orgId
|
|
538
1228
|
});
|
|
539
|
-
|
|
540
|
-
console.log(`Period: ${formatDate(sprint.startDate)} - ${formatDate(sprint.endDate)}`);
|
|
1229
|
+
print(config, sprint, `Sprint created: #${sprint.id} - ${sprint.title}`);
|
|
541
1230
|
}
|
|
542
1231
|
});
|
|
543
1232
|
}
|
|
@@ -555,7 +1244,7 @@ function createSprintUpdateCommand() {
|
|
|
555
1244
|
{ flag: "--private", description: "Set visibility to private" }
|
|
556
1245
|
],
|
|
557
1246
|
examples: ['forloop sprint update --id 456 --title "Updated Sprint"'],
|
|
558
|
-
async run(
|
|
1247
|
+
async run(config, flags) {
|
|
559
1248
|
const resolved = await getClient2();
|
|
560
1249
|
if (!resolved) return;
|
|
561
1250
|
const { client } = resolved;
|
|
@@ -570,7 +1259,7 @@ function createSprintUpdateCommand() {
|
|
|
570
1259
|
process.exit(1);
|
|
571
1260
|
}
|
|
572
1261
|
const sprint = await client.updateSprint(flags.id, updateData);
|
|
573
|
-
|
|
1262
|
+
print(config, sprint, `Sprint #${sprint.id} updated successfully.`);
|
|
574
1263
|
}
|
|
575
1264
|
});
|
|
576
1265
|
}
|
|
@@ -584,7 +1273,7 @@ function createSprintDeleteCommand() {
|
|
|
584
1273
|
{ flag: "--confirm", description: "Confirm deletion" }
|
|
585
1274
|
],
|
|
586
1275
|
examples: ["forloop sprint delete --id 456 --confirm"],
|
|
587
|
-
async run(
|
|
1276
|
+
async run(config, flags) {
|
|
588
1277
|
if (!flags.confirm) {
|
|
589
1278
|
console.error("Error: --confirm is required to permanently delete a sprint.");
|
|
590
1279
|
process.exit(1);
|
|
@@ -592,14 +1281,22 @@ function createSprintDeleteCommand() {
|
|
|
592
1281
|
const resolved = await getClient2();
|
|
593
1282
|
if (!resolved) return;
|
|
594
1283
|
const { client } = resolved;
|
|
595
|
-
await client.deleteSprint(flags.id);
|
|
596
|
-
|
|
1284
|
+
const result = await client.deleteSprint(flags.id);
|
|
1285
|
+
print(config, result, `Sprint #${flags.id} deleted successfully.`);
|
|
597
1286
|
}
|
|
598
1287
|
});
|
|
599
1288
|
}
|
|
600
1289
|
|
|
601
1290
|
// src/commands/org.ts
|
|
602
|
-
|
|
1291
|
+
async function getClient3() {
|
|
1292
|
+
const config = getConfig();
|
|
1293
|
+
const token = await getToken();
|
|
1294
|
+
if (!token) {
|
|
1295
|
+
console.error("Error: No API token configured. Run: forloop auth login --api-key <token>");
|
|
1296
|
+
process.exit(1);
|
|
1297
|
+
}
|
|
1298
|
+
return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl, timeout: 3e4 }) };
|
|
1299
|
+
}
|
|
603
1300
|
function createOrgCommands() {
|
|
604
1301
|
return {
|
|
605
1302
|
"org list": createOrgListCommand(),
|
|
@@ -608,15 +1305,6 @@ function createOrgCommands() {
|
|
|
608
1305
|
"org update": createOrgUpdateCommand()
|
|
609
1306
|
};
|
|
610
1307
|
}
|
|
611
|
-
async function getClient3() {
|
|
612
|
-
const config = getConfig3();
|
|
613
|
-
const token = await getToken4();
|
|
614
|
-
if (!token) {
|
|
615
|
-
console.error("Error: No API token configured. Run: forloop auth login --api-key <token>");
|
|
616
|
-
process.exit(1);
|
|
617
|
-
}
|
|
618
|
-
return { client: new ForLoopAPIClient3({ token, baseUrl: config.apiUrl }) };
|
|
619
|
-
}
|
|
620
1308
|
function createOrgListCommand() {
|
|
621
1309
|
return defineCommand({
|
|
622
1310
|
name: "org list",
|
|
@@ -625,12 +1313,16 @@ function createOrgListCommand() {
|
|
|
625
1313
|
options: [
|
|
626
1314
|
{ flag: "--owned-only", description: "List only organizations you own" }
|
|
627
1315
|
],
|
|
628
|
-
examples: ["forloop org list", "forloop org list --owned-only"],
|
|
629
|
-
async run(
|
|
1316
|
+
examples: ["forloop org list", "forloop org list --owned-only", "forloop org list --output json"],
|
|
1317
|
+
async run(config, flags) {
|
|
630
1318
|
const resolved = await getClient3();
|
|
631
1319
|
if (!resolved) return;
|
|
632
1320
|
const { client } = resolved;
|
|
633
1321
|
const orgs = flags.ownedOnly ? await client.getOwnedOrganizations() : await client.listOrganizations();
|
|
1322
|
+
if (config.output === "json") {
|
|
1323
|
+
print(config, orgs);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
634
1326
|
if (!orgs.length) {
|
|
635
1327
|
console.log("No organizations found.");
|
|
636
1328
|
return;
|
|
@@ -652,19 +1344,16 @@ function createOrgGetCommand() {
|
|
|
652
1344
|
options: [
|
|
653
1345
|
{ flag: "--id <number>", description: "Organization ID", required: true, type: "number" }
|
|
654
1346
|
],
|
|
655
|
-
examples: ["forloop org get --id 1"],
|
|
656
|
-
async run(
|
|
1347
|
+
examples: ["forloop org get --id 1", "forloop org get --id 1 --output json"],
|
|
1348
|
+
async run(config, flags) {
|
|
657
1349
|
if (!flags.id) {
|
|
658
1350
|
console.error("Error: --id is required");
|
|
659
1351
|
process.exit(1);
|
|
660
1352
|
}
|
|
661
1353
|
const resolved = await getClient3();
|
|
662
1354
|
if (!resolved) return;
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
console.log(`Organization #${org.id}: ${org.name}`);
|
|
666
|
-
console.log(`Type: ${org.type || "user"}`);
|
|
667
|
-
if (org.description) console.log(`Description: ${org.description}`);
|
|
1355
|
+
const org = await resolved.client.getOrganization(flags.id);
|
|
1356
|
+
print(config, org);
|
|
668
1357
|
}
|
|
669
1358
|
});
|
|
670
1359
|
}
|
|
@@ -678,19 +1367,18 @@ function createOrgCreateCommand() {
|
|
|
678
1367
|
{ flag: "--description <text>", description: "Organization description" }
|
|
679
1368
|
],
|
|
680
1369
|
examples: ['forloop org create --name "My Team" --description "Engineering team"'],
|
|
681
|
-
async run(
|
|
1370
|
+
async run(config, flags) {
|
|
682
1371
|
if (!flags.name) {
|
|
683
1372
|
console.error("Error: --name is required");
|
|
684
1373
|
process.exit(1);
|
|
685
1374
|
}
|
|
686
1375
|
const resolved = await getClient3();
|
|
687
1376
|
if (!resolved) return;
|
|
688
|
-
const
|
|
689
|
-
const org = await client.createOrganization({
|
|
1377
|
+
const org = await resolved.client.createOrganization({
|
|
690
1378
|
name: flags.name,
|
|
691
1379
|
description: flags.description
|
|
692
1380
|
});
|
|
693
|
-
|
|
1381
|
+
print(config, org, `Organization created: #${org.id} - ${org.name}`);
|
|
694
1382
|
}
|
|
695
1383
|
});
|
|
696
1384
|
}
|
|
@@ -705,25 +1393,23 @@ function createOrgUpdateCommand() {
|
|
|
705
1393
|
{ flag: "--description <text>", description: "New description" }
|
|
706
1394
|
],
|
|
707
1395
|
examples: ['forloop org update --id 1 --name "New Name"'],
|
|
708
|
-
async run(
|
|
1396
|
+
async run(config, flags) {
|
|
709
1397
|
if (!flags.id) {
|
|
710
1398
|
console.error("Error: --id is required");
|
|
711
1399
|
process.exit(1);
|
|
712
1400
|
}
|
|
713
1401
|
const resolved = await getClient3();
|
|
714
1402
|
if (!resolved) return;
|
|
715
|
-
const
|
|
716
|
-
const org = await client.updateOrganization(flags.id, {
|
|
1403
|
+
const org = await resolved.client.updateOrganization(flags.id, {
|
|
717
1404
|
name: flags.name,
|
|
718
1405
|
description: flags.description
|
|
719
1406
|
});
|
|
720
|
-
|
|
1407
|
+
print(config, org, `Organization #${org.id} updated successfully.`);
|
|
721
1408
|
}
|
|
722
1409
|
});
|
|
723
1410
|
}
|
|
724
1411
|
|
|
725
1412
|
// src/commands/template.ts
|
|
726
|
-
import { ForLoopAPIClient as ForLoopAPIClient4, getToken as getToken5, getConfig as getConfig4 } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
727
1413
|
function createTemplateCommands() {
|
|
728
1414
|
return {
|
|
729
1415
|
"template list": createTemplateListCommand()
|
|
@@ -732,30 +1418,41 @@ function createTemplateCommands() {
|
|
|
732
1418
|
function createTemplateListCommand() {
|
|
733
1419
|
return defineCommand({
|
|
734
1420
|
name: "template list",
|
|
735
|
-
description: "List available story templates",
|
|
1421
|
+
description: "List available story templates (basic-task, basic-note, schedule-meeting, doc-folder)",
|
|
736
1422
|
usage: "forloop template list",
|
|
737
|
-
examples: ["forloop template list"],
|
|
738
|
-
async run(
|
|
739
|
-
const
|
|
740
|
-
const token = await
|
|
1423
|
+
examples: ["forloop template list", "forloop template list --output json"],
|
|
1424
|
+
async run(config) {
|
|
1425
|
+
const apiConfig = getConfig();
|
|
1426
|
+
const token = await getToken();
|
|
741
1427
|
if (!token) {
|
|
742
1428
|
console.error("Error: No API token configured.");
|
|
743
1429
|
process.exit(1);
|
|
744
1430
|
}
|
|
745
|
-
const client = new
|
|
1431
|
+
const client = new ForLoopAPIClient({ token, baseUrl: apiConfig.apiUrl, timeout: 3e4 });
|
|
746
1432
|
const templates = await client.listTemplates();
|
|
747
1433
|
const active = templates.filter((t) => !t.deletedAt);
|
|
1434
|
+
if (config.output === "json") {
|
|
1435
|
+
print(config, active);
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
748
1438
|
if (!active.length) {
|
|
749
1439
|
console.log("No templates found.");
|
|
750
1440
|
return;
|
|
751
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];
|
|
752
1447
|
const lines = ["Available Templates:", ""];
|
|
753
|
-
for (const t of
|
|
754
|
-
const fields = JSON.parse(t.fields);
|
|
755
|
-
const fieldNames = fields.map((f) => f.label).join(", ");
|
|
756
|
-
|
|
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}`);
|
|
757
1454
|
lines.push(` ${t.description}`);
|
|
758
|
-
lines.push(` Fields: ${fieldNames}`);
|
|
1455
|
+
if (fieldNames) lines.push(` Fields: ${fieldNames}`);
|
|
759
1456
|
lines.push("");
|
|
760
1457
|
}
|
|
761
1458
|
console.log(lines.join("\n"));
|
|
@@ -764,7 +1461,6 @@ function createTemplateListCommand() {
|
|
|
764
1461
|
}
|
|
765
1462
|
|
|
766
1463
|
// src/commands/agent.ts
|
|
767
|
-
import { ForLoopAPIClient as ForLoopAPIClient5, getToken as getToken6, resolveSprintId as resolveSprintId3, getConfig as getConfig5 } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
768
1464
|
function createAgentCommands() {
|
|
769
1465
|
return {
|
|
770
1466
|
"agent history": createAgentHistoryCommand(),
|
|
@@ -773,13 +1469,13 @@ function createAgentCommands() {
|
|
|
773
1469
|
};
|
|
774
1470
|
}
|
|
775
1471
|
async function getClient4() {
|
|
776
|
-
const config =
|
|
777
|
-
const token = await
|
|
1472
|
+
const config = getConfig();
|
|
1473
|
+
const token = await getToken();
|
|
778
1474
|
if (!token) {
|
|
779
1475
|
console.error("Error: No API token configured.");
|
|
780
1476
|
process.exit(1);
|
|
781
1477
|
}
|
|
782
|
-
return { client: new
|
|
1478
|
+
return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl, timeout: 3e4 }) };
|
|
783
1479
|
}
|
|
784
1480
|
function createAgentHistoryCommand() {
|
|
785
1481
|
return defineCommand({
|
|
@@ -791,31 +1487,17 @@ function createAgentHistoryCommand() {
|
|
|
791
1487
|
{ flag: "--limit <n>", description: "Number of messages (max 200, default 50)", type: "number" }
|
|
792
1488
|
],
|
|
793
1489
|
examples: ["forloop agent history --sprint 1 --limit 20"],
|
|
794
|
-
async run(
|
|
1490
|
+
async run(config, flags) {
|
|
795
1491
|
const resolved = await getClient4();
|
|
796
1492
|
if (!resolved) return;
|
|
797
1493
|
const { client } = resolved;
|
|
798
|
-
const resolution = await
|
|
1494
|
+
const resolution = await resolveSprintId(flags.sprint);
|
|
799
1495
|
const sprintId = resolution.sprintId || flags.sprint;
|
|
800
1496
|
const history = await client.getConversationHistory({
|
|
801
1497
|
sprintId,
|
|
802
1498
|
limit: flags.limit || 50
|
|
803
1499
|
});
|
|
804
|
-
|
|
805
|
-
if (!messages.length) {
|
|
806
|
-
console.log("No conversation history found.");
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
console.log(`Conversation History (${history.total || messages.length} total):`);
|
|
810
|
-
for (const msg of messages) {
|
|
811
|
-
const preview = (msg.userMessage || "").substring(0, 80);
|
|
812
|
-
console.log(` ${preview}${preview.length >= 80 ? "..." : ""}`);
|
|
813
|
-
if (msg.agentResponse) {
|
|
814
|
-
const resp = msg.agentResponse.substring(0, 100);
|
|
815
|
-
console.log(` -> ${resp}${msg.agentResponse.length > 100 ? "..." : ""}`);
|
|
816
|
-
}
|
|
817
|
-
console.log("");
|
|
818
|
-
}
|
|
1500
|
+
print(config, history);
|
|
819
1501
|
}
|
|
820
1502
|
});
|
|
821
1503
|
}
|
|
@@ -866,7 +1548,7 @@ function createDeveloperStatusCommand() {
|
|
|
866
1548
|
const resolved = await getClient4();
|
|
867
1549
|
if (!resolved) return;
|
|
868
1550
|
const { client } = resolved;
|
|
869
|
-
const resolution = await
|
|
1551
|
+
const resolution = await resolveSprintId(flags.sprint);
|
|
870
1552
|
if (!resolution.sprintId) {
|
|
871
1553
|
console.error("Error: No sprint ID resolved.");
|
|
872
1554
|
process.exit(1);
|
|
@@ -878,11 +1560,11 @@ function createDeveloperStatusCommand() {
|
|
|
878
1560
|
console.log(`Sprint #${resolution.sprintId}: ${sprint.storiesDone || 0}/${sprint.storiesTotal || 0} stories done.`);
|
|
879
1561
|
return;
|
|
880
1562
|
}
|
|
881
|
-
const
|
|
1563
|
+
const exec3 = status.execution || {};
|
|
882
1564
|
const task = status.task || {};
|
|
883
|
-
console.log(`Developer Task: ${
|
|
1565
|
+
console.log(`Developer Task: ${exec3.status}`);
|
|
884
1566
|
console.log(`Sprint: #${resolution.sprintId}`);
|
|
885
|
-
if (
|
|
1567
|
+
if (exec3.startedAt) console.log(`Started: ${new Date(exec3.startedAt).toLocaleString()}`);
|
|
886
1568
|
if (task.branchName) console.log(`Branch: ${task.branchName}`);
|
|
887
1569
|
if (task.errorMessage) console.log(`Error: ${task.errorMessage}`);
|
|
888
1570
|
console.log(`Progress: ${sprint.storiesDone || 0}/${sprint.storiesTotal || 0} done, ${sprint.storiesInProgress || 0} in progress.`);
|
|
@@ -891,7 +1573,15 @@ function createDeveloperStatusCommand() {
|
|
|
891
1573
|
}
|
|
892
1574
|
|
|
893
1575
|
// src/commands/user.ts
|
|
894
|
-
|
|
1576
|
+
async function getClient5() {
|
|
1577
|
+
const config = getConfig();
|
|
1578
|
+
const token = await getToken();
|
|
1579
|
+
if (!token) {
|
|
1580
|
+
console.error("Error: No API token configured.");
|
|
1581
|
+
process.exit(1);
|
|
1582
|
+
}
|
|
1583
|
+
return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl, timeout: 3e4 }) };
|
|
1584
|
+
}
|
|
895
1585
|
function createUserCommands() {
|
|
896
1586
|
return {
|
|
897
1587
|
"user profile": createUserProfileCommand(),
|
|
@@ -899,29 +1589,17 @@ function createUserCommands() {
|
|
|
899
1589
|
"org quotas": createOrgQuotasCommand()
|
|
900
1590
|
};
|
|
901
1591
|
}
|
|
902
|
-
async function getClient5() {
|
|
903
|
-
const config = getConfig6();
|
|
904
|
-
const token = await getToken7();
|
|
905
|
-
if (!token) {
|
|
906
|
-
console.error("Error: No API token configured.");
|
|
907
|
-
process.exit(1);
|
|
908
|
-
}
|
|
909
|
-
return { client: new ForLoopAPIClient6({ token, baseUrl: config.apiUrl }) };
|
|
910
|
-
}
|
|
911
1592
|
function createUserProfileCommand() {
|
|
912
1593
|
return defineCommand({
|
|
913
1594
|
name: "user profile",
|
|
914
1595
|
description: "Get current user profile",
|
|
915
1596
|
usage: "forloop user profile",
|
|
916
|
-
examples: ["forloop user profile"],
|
|
917
|
-
async run(
|
|
1597
|
+
examples: ["forloop user profile", "forloop user profile --output json"],
|
|
1598
|
+
async run(config) {
|
|
918
1599
|
const resolved = await getClient5();
|
|
919
1600
|
if (!resolved) return;
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
console.log(`User: ${profile.name || profile.email || "Unknown"}`);
|
|
923
|
-
console.log(`Email: ${profile.email}`);
|
|
924
|
-
if (profile.tier) console.log(`Tier: ${profile.tier}`);
|
|
1601
|
+
const profile = await resolved.client.getUserProfile();
|
|
1602
|
+
print(config, profile, "User Profile:");
|
|
925
1603
|
}
|
|
926
1604
|
});
|
|
927
1605
|
}
|
|
@@ -930,14 +1608,12 @@ function createUserQuotasCommand() {
|
|
|
930
1608
|
name: "user quotas",
|
|
931
1609
|
description: "Check user quota limits",
|
|
932
1610
|
usage: "forloop user quotas",
|
|
933
|
-
examples: ["forloop user quotas"],
|
|
934
|
-
async run(
|
|
1611
|
+
examples: ["forloop user quotas", "forloop user quotas --output json"],
|
|
1612
|
+
async run(config) {
|
|
935
1613
|
const resolved = await getClient5();
|
|
936
1614
|
if (!resolved) return;
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
console.log("User Quotas:");
|
|
940
|
-
console.log(JSON.stringify(quotas, null, 2));
|
|
1615
|
+
const quotas = await resolved.client.getUserQuotas();
|
|
1616
|
+
print(config, quotas, "User Quotas:");
|
|
941
1617
|
}
|
|
942
1618
|
});
|
|
943
1619
|
}
|
|
@@ -949,26 +1625,23 @@ function createOrgQuotasCommand() {
|
|
|
949
1625
|
options: [
|
|
950
1626
|
{ flag: "--org-id <id>", description: "Organization ID", required: true, type: "number" }
|
|
951
1627
|
],
|
|
952
|
-
examples: ["forloop org quotas --org-id 1"],
|
|
953
|
-
async run(
|
|
1628
|
+
examples: ["forloop org quotas --org-id 1", "forloop org quotas --org-id 1 --output json"],
|
|
1629
|
+
async run(config, flags) {
|
|
954
1630
|
if (!flags.orgId) {
|
|
955
1631
|
console.error("Error: --org-id is required");
|
|
956
1632
|
process.exit(1);
|
|
957
1633
|
}
|
|
958
1634
|
const resolved = await getClient5();
|
|
959
1635
|
if (!resolved) return;
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
console.log("Organization Quotas:");
|
|
963
|
-
console.log(JSON.stringify(quotas, null, 2));
|
|
1636
|
+
const quotas = await resolved.client.getOrganizationQuotas(flags.orgId);
|
|
1637
|
+
print(config, quotas, "Organization Quotas:");
|
|
964
1638
|
}
|
|
965
1639
|
});
|
|
966
1640
|
}
|
|
967
1641
|
|
|
968
1642
|
// src/commands/file.ts
|
|
969
|
-
import
|
|
970
|
-
import * as
|
|
971
|
-
import * as path from "path";
|
|
1643
|
+
import * as fs4 from "fs";
|
|
1644
|
+
import * as path4 from "path";
|
|
972
1645
|
function createFileCommands() {
|
|
973
1646
|
return {
|
|
974
1647
|
"file upload": createFileUploadCommand(),
|
|
@@ -978,13 +1651,13 @@ function createFileCommands() {
|
|
|
978
1651
|
};
|
|
979
1652
|
}
|
|
980
1653
|
async function getClient6() {
|
|
981
|
-
const config =
|
|
982
|
-
const token = await
|
|
1654
|
+
const config = getConfig();
|
|
1655
|
+
const token = await getToken();
|
|
983
1656
|
if (!token) {
|
|
984
1657
|
console.error("Error: No API token configured.");
|
|
985
1658
|
process.exit(1);
|
|
986
1659
|
}
|
|
987
|
-
return { client: new
|
|
1660
|
+
return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl, timeout: 3e4 }) };
|
|
988
1661
|
}
|
|
989
1662
|
var mimeTypes = {
|
|
990
1663
|
".png": "image/png",
|
|
@@ -1018,18 +1691,18 @@ function createFileUploadCommand() {
|
|
|
1018
1691
|
{ flag: "--folder <text>", description: "S3 folder within sprint" }
|
|
1019
1692
|
],
|
|
1020
1693
|
examples: ["forloop file upload --path ./report.pdf --sprint 1 --folder docs"],
|
|
1021
|
-
async run(
|
|
1694
|
+
async run(config, flags) {
|
|
1022
1695
|
const resolved = await getClient6();
|
|
1023
1696
|
if (!resolved) return;
|
|
1024
1697
|
const { client } = resolved;
|
|
1025
1698
|
const filePath = flags.path;
|
|
1026
|
-
if (!
|
|
1699
|
+
if (!fs4.existsSync(filePath)) {
|
|
1027
1700
|
console.error(`Error: File not found: ${filePath}`);
|
|
1028
1701
|
process.exit(1);
|
|
1029
1702
|
}
|
|
1030
|
-
const fileContent =
|
|
1031
|
-
const fileName =
|
|
1032
|
-
const ext =
|
|
1703
|
+
const fileContent = fs4.readFileSync(filePath);
|
|
1704
|
+
const fileName = path4.basename(filePath);
|
|
1705
|
+
const ext = path4.extname(fileName);
|
|
1033
1706
|
const contentType = mimeTypes[ext] || "application/octet-stream";
|
|
1034
1707
|
const presign = await client.createPresignedUpload({
|
|
1035
1708
|
sprintId: flags.sprint,
|
|
@@ -1054,8 +1727,7 @@ function createFileUploadCommand() {
|
|
|
1054
1727
|
size: fileContent.length,
|
|
1055
1728
|
folder: flags.folder
|
|
1056
1729
|
});
|
|
1057
|
-
|
|
1058
|
-
console.log(`URL: ${complete.url}`);
|
|
1730
|
+
print(config, { fileName, size: fileContent.length, url: complete.url, fileId: complete.id }, `File uploaded: ${fileName} (${formatSize(fileContent.length)})`);
|
|
1059
1731
|
}
|
|
1060
1732
|
});
|
|
1061
1733
|
}
|
|
@@ -1068,23 +1740,12 @@ function createFileListCommand() {
|
|
|
1068
1740
|
{ flag: "--sprint <id>", description: "Sprint ID", required: true, type: "number" }
|
|
1069
1741
|
],
|
|
1070
1742
|
examples: ["forloop file list --sprint 1"],
|
|
1071
|
-
async run(
|
|
1743
|
+
async run(config, flags) {
|
|
1072
1744
|
const resolved = await getClient6();
|
|
1073
1745
|
if (!resolved) return;
|
|
1074
1746
|
const { client } = resolved;
|
|
1075
1747
|
const files = await client.getSprintFiles(flags.sprint);
|
|
1076
|
-
|
|
1077
|
-
console.log("No files found.");
|
|
1078
|
-
return;
|
|
1079
|
-
}
|
|
1080
|
-
const lines = [`Files in Sprint #${flags.sprint}:`, ""];
|
|
1081
|
-
for (const file of files) {
|
|
1082
|
-
lines.push(` ${file.originalName || file.filename}`);
|
|
1083
|
-
lines.push(` Size: ${formatSize(file.size)} | Type: ${file.fileType}`);
|
|
1084
|
-
lines.push(` URL: ${file.url}`);
|
|
1085
|
-
lines.push("");
|
|
1086
|
-
}
|
|
1087
|
-
console.log(lines.join("\n"));
|
|
1748
|
+
print(config, files, `Files in Sprint #${flags.sprint}:`);
|
|
1088
1749
|
}
|
|
1089
1750
|
});
|
|
1090
1751
|
}
|
|
@@ -1132,7 +1793,6 @@ function createFileDownloadCommand() {
|
|
|
1132
1793
|
}
|
|
1133
1794
|
|
|
1134
1795
|
// src/commands/folder.ts
|
|
1135
|
-
import { ForLoopAPIClient as ForLoopAPIClient8, getToken as getToken9, getConfig as getConfig8 } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
1136
1796
|
function createFolderCommands() {
|
|
1137
1797
|
return {
|
|
1138
1798
|
"folder create": createFolderCreateCommand()
|
|
@@ -1151,13 +1811,13 @@ function createFolderCreateCommand() {
|
|
|
1151
1811
|
],
|
|
1152
1812
|
examples: ['forloop folder create --title "Project Docs" --sprint 1 --permissions team'],
|
|
1153
1813
|
async run(_config, flags) {
|
|
1154
|
-
const config =
|
|
1155
|
-
const token = await
|
|
1814
|
+
const config = getConfig();
|
|
1815
|
+
const token = await getToken();
|
|
1156
1816
|
if (!token) {
|
|
1157
1817
|
console.error("Error: No API token configured.");
|
|
1158
1818
|
process.exit(1);
|
|
1159
1819
|
}
|
|
1160
|
-
const client = new
|
|
1820
|
+
const client = new ForLoopAPIClient({ token, baseUrl: config.apiUrl, timeout: 3e4 });
|
|
1161
1821
|
const story = await client.createStory({
|
|
1162
1822
|
title: flags.title,
|
|
1163
1823
|
description: flags.description,
|
|
@@ -1172,9 +1832,8 @@ function createFolderCreateCommand() {
|
|
|
1172
1832
|
}
|
|
1173
1833
|
|
|
1174
1834
|
// src/commands/sync.ts
|
|
1175
|
-
import
|
|
1176
|
-
import * as
|
|
1177
|
-
import * as path2 from "path";
|
|
1835
|
+
import * as fs5 from "fs";
|
|
1836
|
+
import * as path5 from "path";
|
|
1178
1837
|
function createSyncCommands() {
|
|
1179
1838
|
return {
|
|
1180
1839
|
"sync aivy-folder": createSyncAivyFolderCommand(),
|
|
@@ -1184,13 +1843,13 @@ function createSyncCommands() {
|
|
|
1184
1843
|
};
|
|
1185
1844
|
}
|
|
1186
1845
|
async function getClient7() {
|
|
1187
|
-
const config =
|
|
1188
|
-
const token = await
|
|
1846
|
+
const config = getConfig();
|
|
1847
|
+
const token = await getToken();
|
|
1189
1848
|
if (!token) {
|
|
1190
1849
|
console.error("Error: No API token configured.");
|
|
1191
1850
|
process.exit(1);
|
|
1192
1851
|
}
|
|
1193
|
-
return { client: new
|
|
1852
|
+
return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl, timeout: 3e4 }) };
|
|
1194
1853
|
}
|
|
1195
1854
|
function createSyncAivyFolderCommand() {
|
|
1196
1855
|
return defineCommand({
|
|
@@ -1206,7 +1865,7 @@ function createSyncAivyFolderCommand() {
|
|
|
1206
1865
|
const resolved = await getClient7();
|
|
1207
1866
|
if (!resolved) return;
|
|
1208
1867
|
const { client } = resolved;
|
|
1209
|
-
const resolution = await
|
|
1868
|
+
const resolution = await resolveSprintId(flags.sprint);
|
|
1210
1869
|
if (!resolution.sprintId) {
|
|
1211
1870
|
console.error("Error: No sprint ID resolved.");
|
|
1212
1871
|
process.exit(1);
|
|
@@ -1243,7 +1902,7 @@ function createAivyDocGetCommand() {
|
|
|
1243
1902
|
const resolved = await getClient7();
|
|
1244
1903
|
if (!resolved) return;
|
|
1245
1904
|
const { client } = resolved;
|
|
1246
|
-
const resolution = await
|
|
1905
|
+
const resolution = await resolveSprintId(flags.sprint);
|
|
1247
1906
|
if (!resolution.sprintId) {
|
|
1248
1907
|
console.error("Error: No sprint ID resolved.");
|
|
1249
1908
|
process.exit(1);
|
|
@@ -1277,14 +1936,14 @@ function createS3ToLocalCommand() {
|
|
|
1277
1936
|
const resolved = await getClient7();
|
|
1278
1937
|
if (!resolved) return;
|
|
1279
1938
|
const { client } = resolved;
|
|
1280
|
-
const resolution = await
|
|
1939
|
+
const resolution = await resolveSprintId(flags.sprint);
|
|
1281
1940
|
if (!resolution.sprintId) {
|
|
1282
1941
|
console.error("Error: No sprint ID resolved.");
|
|
1283
1942
|
process.exit(1);
|
|
1284
1943
|
}
|
|
1285
1944
|
const files = await client.getSprintFiles(resolution.sprintId);
|
|
1286
1945
|
const localRoot = getForloopRoot(resolution.sprintId);
|
|
1287
|
-
|
|
1946
|
+
fs5.mkdirSync(localRoot, { recursive: true });
|
|
1288
1947
|
let downloaded = 0;
|
|
1289
1948
|
let skipped = 0;
|
|
1290
1949
|
for (const file of files) {
|
|
@@ -1299,9 +1958,9 @@ function createS3ToLocalCommand() {
|
|
|
1299
1958
|
if (res.ok) {
|
|
1300
1959
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
1301
1960
|
const kind = name.startsWith("knowledge-") ? "knowledge" : name.startsWith("plan-") ? "plan" : name.startsWith("task-") ? "task" : "other";
|
|
1302
|
-
const dir =
|
|
1303
|
-
|
|
1304
|
-
|
|
1961
|
+
const dir = path5.join(localRoot, kind);
|
|
1962
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1963
|
+
fs5.writeFileSync(path5.join(dir, name), buf);
|
|
1305
1964
|
downloaded++;
|
|
1306
1965
|
}
|
|
1307
1966
|
}
|
|
@@ -1325,18 +1984,18 @@ function createLocalToS3Command() {
|
|
|
1325
1984
|
const resolved = await getClient7();
|
|
1326
1985
|
if (!resolved) return;
|
|
1327
1986
|
const { client } = resolved;
|
|
1328
|
-
const resolution = await
|
|
1987
|
+
const resolution = await resolveSprintId(flags.sprint);
|
|
1329
1988
|
if (!resolution.sprintId) {
|
|
1330
1989
|
console.error("Error: No sprint ID resolved.");
|
|
1331
1990
|
process.exit(1);
|
|
1332
1991
|
}
|
|
1333
1992
|
const filePath = flags.path;
|
|
1334
|
-
if (!
|
|
1993
|
+
if (!fs5.existsSync(filePath)) {
|
|
1335
1994
|
console.error(`Error: File not found: ${filePath}`);
|
|
1336
1995
|
process.exit(1);
|
|
1337
1996
|
}
|
|
1338
|
-
const fileBuf =
|
|
1339
|
-
const fileName =
|
|
1997
|
+
const fileBuf = fs5.readFileSync(filePath);
|
|
1998
|
+
const fileName = path5.basename(filePath);
|
|
1340
1999
|
const contentType = "text/markdown";
|
|
1341
2000
|
const folder = flags.folder || "project";
|
|
1342
2001
|
const presign = await client.createPresignedUpload({
|
|
@@ -1367,12 +2026,12 @@ function createLocalToS3Command() {
|
|
|
1367
2026
|
var CommandRegistry = class {
|
|
1368
2027
|
root = { children: /* @__PURE__ */ new Map() };
|
|
1369
2028
|
constructor(commands) {
|
|
1370
|
-
for (const [
|
|
1371
|
-
this.register(
|
|
2029
|
+
for (const [path6, cmd] of Object.entries(commands)) {
|
|
2030
|
+
this.register(path6, cmd);
|
|
1372
2031
|
}
|
|
1373
2032
|
}
|
|
1374
|
-
register(
|
|
1375
|
-
const parts =
|
|
2033
|
+
register(path6, command) {
|
|
2034
|
+
const parts = path6.split(" ");
|
|
1376
2035
|
let node = this.root;
|
|
1377
2036
|
for (const part of parts) {
|
|
1378
2037
|
if (!node.children.has(part)) {
|
|
@@ -1622,14 +2281,6 @@ var registry = new CommandRegistry({
|
|
|
1622
2281
|
"agent sprint": agentDeveloperSprintCmd
|
|
1623
2282
|
});
|
|
1624
2283
|
|
|
1625
|
-
// src/output/formatter.ts
|
|
1626
|
-
function formatError(message, format) {
|
|
1627
|
-
if (format === "json") {
|
|
1628
|
-
return JSON.stringify({ error: { message } }, null, 2);
|
|
1629
|
-
}
|
|
1630
|
-
return `\u274C ${message}`;
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
2284
|
// src/errors/handler.ts
|
|
1634
2285
|
function handleError(error) {
|
|
1635
2286
|
const isJson = process.argv.includes("--output=json");
|
|
@@ -1652,10 +2303,7 @@ Tip: ${suggestion}
|
|
|
1652
2303
|
}
|
|
1653
2304
|
|
|
1654
2305
|
// src/main.ts
|
|
1655
|
-
|
|
1656
|
-
import { getToken as getToken11 } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
1657
|
-
import { detectTokenType } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
1658
|
-
var CLI_VERSION = "0.2.2";
|
|
2306
|
+
var CLI_VERSION = "0.2.4";
|
|
1659
2307
|
process.on("SIGINT", () => {
|
|
1660
2308
|
process.stderr.write("\nInterrupted. Exiting.\n");
|
|
1661
2309
|
process.exit(130);
|
|
@@ -1682,7 +2330,7 @@ async function main() {
|
|
|
1682
2330
|
if (commandPath.length === 0) {
|
|
1683
2331
|
registry.printHelp([], process.stderr);
|
|
1684
2332
|
try {
|
|
1685
|
-
const token2 = await
|
|
2333
|
+
const token2 = await getToken();
|
|
1686
2334
|
if (token2) {
|
|
1687
2335
|
const tokenType = detectTokenType(token2);
|
|
1688
2336
|
const masked = token2.slice(0, 8) + "..." + token2.slice(-4);
|
|
@@ -1703,8 +2351,8 @@ async function main() {
|
|
|
1703
2351
|
const { command, extra } = registry.resolve(commandPath);
|
|
1704
2352
|
const flags = parseFlags(argv, [...GLOBAL_OPTIONS, ...command.options ?? []]);
|
|
1705
2353
|
if (extra.length > 0) flags._positional = extra;
|
|
1706
|
-
const pluginConfig =
|
|
1707
|
-
const token = await
|
|
2354
|
+
const pluginConfig = getConfig();
|
|
2355
|
+
const token = await getToken();
|
|
1708
2356
|
const isNoAuth = NO_AUTH_COMMANDS.some(
|
|
1709
2357
|
(cmd) => cmd.every((c, i) => commandPath[i] === c)
|
|
1710
2358
|
);
|
|
@@ -1715,7 +2363,7 @@ async function main() {
|
|
|
1715
2363
|
process.exit(3);
|
|
1716
2364
|
}
|
|
1717
2365
|
const cliConfig = {
|
|
1718
|
-
region:
|
|
2366
|
+
region: "global",
|
|
1719
2367
|
apiUrl: pluginConfig.apiUrl,
|
|
1720
2368
|
serverLambdaUrl: "",
|
|
1721
2369
|
environment: pluginConfig.environment,
|