@forloop-cc/forloop-cli 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1 -1
- package/dist/{forloop.mjs → forloop} +74 -781
- package/package.json +8 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ForLoop
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
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 path3 = [];
|
|
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
|
+
path3.push(arg);
|
|
52
52
|
i++;
|
|
53
53
|
}
|
|
54
|
-
return
|
|
54
|
+
return path3;
|
|
55
55
|
}
|
|
56
56
|
function parseFlags(argv, options) {
|
|
57
57
|
const schema = buildSchema(options);
|
|
@@ -142,727 +142,8 @@ var GLOBAL_OPTIONS = [
|
|
|
142
142
|
{ flag: "--version", description: "Print version" }
|
|
143
143
|
];
|
|
144
144
|
|
|
145
|
-
// ../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
|
-
// ../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
|
-
// ../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
|
-
// ../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
|
-
// ../forloop-opencode-plugin-planner/plugins/tools/sprint-tools.ts
|
|
740
|
-
import { tool } from "@opencode-ai/plugin";
|
|
741
|
-
|
|
742
|
-
// ../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
|
-
// ../forloop-opencode-plugin-planner/plugins/tools/story-tools.ts
|
|
822
|
-
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
823
|
-
|
|
824
|
-
// ../forloop-opencode-plugin-planner/plugins/tools/template-tools.ts
|
|
825
|
-
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
826
|
-
|
|
827
|
-
// ../forloop-opencode-plugin-planner/plugins/tools/user-tools.ts
|
|
828
|
-
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
829
|
-
|
|
830
|
-
// ../forloop-opencode-plugin-planner/plugins/tools/organization-tools.ts
|
|
831
|
-
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
832
|
-
|
|
833
|
-
// ../forloop-opencode-plugin-planner/plugins/tools/file-tools.ts
|
|
834
|
-
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
835
|
-
|
|
836
|
-
// ../forloop-opencode-plugin-planner/plugins/tools/schedule-tools.ts
|
|
837
|
-
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
838
|
-
|
|
839
|
-
// ../forloop-opencode-plugin-planner/plugins/tools/folder-tools.ts
|
|
840
|
-
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
841
|
-
|
|
842
|
-
// ../forloop-opencode-plugin-planner/plugins/tools/sync-tools.ts
|
|
843
|
-
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
844
|
-
|
|
845
|
-
// ../forloop-opencode-plugin-planner/plugins/tools/agent-tools.ts
|
|
846
|
-
import { tool as tool10 } from "@opencode-ai/plugin";
|
|
847
|
-
|
|
848
|
-
// ../forloop-opencode-plugin-planner/plugins/tools/agent-suggestion-tools.ts
|
|
849
|
-
import { tool as tool11 } from "@opencode-ai/plugin";
|
|
850
|
-
|
|
851
|
-
// ../forloop-opencode-plugin-planner/plugins/tools/agent-sprint-tools.ts
|
|
852
|
-
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
853
|
-
|
|
854
|
-
// ../forloop-opencode-plugin-planner/plugins/tools/ai-trigger-tools.ts
|
|
855
|
-
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
856
|
-
|
|
857
|
-
// ../forloop-opencode-plugin-planner/plugins/tools/developer-status-tool.ts
|
|
858
|
-
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
859
|
-
|
|
860
|
-
// ../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
|
-
|
|
865
145
|
// src/commands/auth/index.ts
|
|
146
|
+
import { getToken, setToken, clearToken, validateToken } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
866
147
|
function createAuthCommands() {
|
|
867
148
|
return {
|
|
868
149
|
"auth login": createAuthLoginCommand(),
|
|
@@ -930,6 +211,7 @@ function createAuthLogoutCommand() {
|
|
|
930
211
|
}
|
|
931
212
|
|
|
932
213
|
// src/commands/story.ts
|
|
214
|
+
import { ForLoopAPIClient, getToken as getToken2, getConfig } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
933
215
|
function createStoryCommands() {
|
|
934
216
|
return {
|
|
935
217
|
"story create": createStoryCreateCommand(),
|
|
@@ -940,7 +222,7 @@ function createStoryCommands() {
|
|
|
940
222
|
}
|
|
941
223
|
async function getClient() {
|
|
942
224
|
const config = getConfig();
|
|
943
|
-
const token = await
|
|
225
|
+
const token = await getToken2();
|
|
944
226
|
if (!token) {
|
|
945
227
|
console.error("Error: No API token configured. Run: forloop auth login --api-key <token>");
|
|
946
228
|
process.exit(1);
|
|
@@ -1130,6 +412,7 @@ function createStoryDeleteCommand() {
|
|
|
1130
412
|
}
|
|
1131
413
|
|
|
1132
414
|
// 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";
|
|
1133
416
|
function createSprintCommands() {
|
|
1134
417
|
return {
|
|
1135
418
|
"sprint get": createSprintGetCommand(),
|
|
@@ -1140,13 +423,13 @@ function createSprintCommands() {
|
|
|
1140
423
|
};
|
|
1141
424
|
}
|
|
1142
425
|
async function getClient2() {
|
|
1143
|
-
const config =
|
|
1144
|
-
const token = await
|
|
426
|
+
const config = getConfig2();
|
|
427
|
+
const token = await getToken3();
|
|
1145
428
|
if (!token) {
|
|
1146
429
|
console.error("Error: No API token configured. Run: forloop auth login --api-key <token>");
|
|
1147
430
|
process.exit(1);
|
|
1148
431
|
}
|
|
1149
|
-
const client = new
|
|
432
|
+
const client = new ForLoopAPIClient2({ token, baseUrl: config.apiUrl });
|
|
1150
433
|
return { client };
|
|
1151
434
|
}
|
|
1152
435
|
function formatDate(dateString) {
|
|
@@ -1167,7 +450,7 @@ function createSprintGetCommand() {
|
|
|
1167
450
|
const resolved = await getClient2();
|
|
1168
451
|
if (!resolved) return;
|
|
1169
452
|
const { client } = resolved;
|
|
1170
|
-
const resolution = await
|
|
453
|
+
const resolution = await resolveSprintId2(flags.id);
|
|
1171
454
|
if (!resolution.sprintId) {
|
|
1172
455
|
console.error("Error: No sprint ID resolved. Use --id, set FORLOOP_SPRINT_ID, or use a sprint-XXX branch.");
|
|
1173
456
|
process.exit(1);
|
|
@@ -1316,6 +599,7 @@ function createSprintDeleteCommand() {
|
|
|
1316
599
|
}
|
|
1317
600
|
|
|
1318
601
|
// src/commands/org.ts
|
|
602
|
+
import { ForLoopAPIClient as ForLoopAPIClient3, getToken as getToken4, getConfig as getConfig3 } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
1319
603
|
function createOrgCommands() {
|
|
1320
604
|
return {
|
|
1321
605
|
"org list": createOrgListCommand(),
|
|
@@ -1325,13 +609,13 @@ function createOrgCommands() {
|
|
|
1325
609
|
};
|
|
1326
610
|
}
|
|
1327
611
|
async function getClient3() {
|
|
1328
|
-
const config =
|
|
1329
|
-
const token = await
|
|
612
|
+
const config = getConfig3();
|
|
613
|
+
const token = await getToken4();
|
|
1330
614
|
if (!token) {
|
|
1331
615
|
console.error("Error: No API token configured. Run: forloop auth login --api-key <token>");
|
|
1332
616
|
process.exit(1);
|
|
1333
617
|
}
|
|
1334
|
-
return { client: new
|
|
618
|
+
return { client: new ForLoopAPIClient3({ token, baseUrl: config.apiUrl }) };
|
|
1335
619
|
}
|
|
1336
620
|
function createOrgListCommand() {
|
|
1337
621
|
return defineCommand({
|
|
@@ -1439,6 +723,7 @@ function createOrgUpdateCommand() {
|
|
|
1439
723
|
}
|
|
1440
724
|
|
|
1441
725
|
// src/commands/template.ts
|
|
726
|
+
import { ForLoopAPIClient as ForLoopAPIClient4, getToken as getToken5, getConfig as getConfig4 } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
1442
727
|
function createTemplateCommands() {
|
|
1443
728
|
return {
|
|
1444
729
|
"template list": createTemplateListCommand()
|
|
@@ -1451,13 +736,13 @@ function createTemplateListCommand() {
|
|
|
1451
736
|
usage: "forloop template list",
|
|
1452
737
|
examples: ["forloop template list"],
|
|
1453
738
|
async run(_config) {
|
|
1454
|
-
const config =
|
|
1455
|
-
const token = await
|
|
739
|
+
const config = getConfig4();
|
|
740
|
+
const token = await getToken5();
|
|
1456
741
|
if (!token) {
|
|
1457
742
|
console.error("Error: No API token configured.");
|
|
1458
743
|
process.exit(1);
|
|
1459
744
|
}
|
|
1460
|
-
const client = new
|
|
745
|
+
const client = new ForLoopAPIClient4({ token, baseUrl: config.apiUrl });
|
|
1461
746
|
const templates = await client.listTemplates();
|
|
1462
747
|
const active = templates.filter((t) => !t.deletedAt);
|
|
1463
748
|
if (!active.length) {
|
|
@@ -1479,6 +764,7 @@ function createTemplateListCommand() {
|
|
|
1479
764
|
}
|
|
1480
765
|
|
|
1481
766
|
// 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";
|
|
1482
768
|
function createAgentCommands() {
|
|
1483
769
|
return {
|
|
1484
770
|
"agent history": createAgentHistoryCommand(),
|
|
@@ -1487,13 +773,13 @@ function createAgentCommands() {
|
|
|
1487
773
|
};
|
|
1488
774
|
}
|
|
1489
775
|
async function getClient4() {
|
|
1490
|
-
const config =
|
|
1491
|
-
const token = await
|
|
776
|
+
const config = getConfig5();
|
|
777
|
+
const token = await getToken6();
|
|
1492
778
|
if (!token) {
|
|
1493
779
|
console.error("Error: No API token configured.");
|
|
1494
780
|
process.exit(1);
|
|
1495
781
|
}
|
|
1496
|
-
return { client: new
|
|
782
|
+
return { client: new ForLoopAPIClient5({ token, baseUrl: config.apiUrl }) };
|
|
1497
783
|
}
|
|
1498
784
|
function createAgentHistoryCommand() {
|
|
1499
785
|
return defineCommand({
|
|
@@ -1509,7 +795,7 @@ function createAgentHistoryCommand() {
|
|
|
1509
795
|
const resolved = await getClient4();
|
|
1510
796
|
if (!resolved) return;
|
|
1511
797
|
const { client } = resolved;
|
|
1512
|
-
const resolution = await
|
|
798
|
+
const resolution = await resolveSprintId3(flags.sprint);
|
|
1513
799
|
const sprintId = resolution.sprintId || flags.sprint;
|
|
1514
800
|
const history = await client.getConversationHistory({
|
|
1515
801
|
sprintId,
|
|
@@ -1580,7 +866,7 @@ function createDeveloperStatusCommand() {
|
|
|
1580
866
|
const resolved = await getClient4();
|
|
1581
867
|
if (!resolved) return;
|
|
1582
868
|
const { client } = resolved;
|
|
1583
|
-
const resolution = await
|
|
869
|
+
const resolution = await resolveSprintId3(flags.sprint);
|
|
1584
870
|
if (!resolution.sprintId) {
|
|
1585
871
|
console.error("Error: No sprint ID resolved.");
|
|
1586
872
|
process.exit(1);
|
|
@@ -1592,11 +878,11 @@ function createDeveloperStatusCommand() {
|
|
|
1592
878
|
console.log(`Sprint #${resolution.sprintId}: ${sprint.storiesDone || 0}/${sprint.storiesTotal || 0} stories done.`);
|
|
1593
879
|
return;
|
|
1594
880
|
}
|
|
1595
|
-
const
|
|
881
|
+
const exec = status.execution || {};
|
|
1596
882
|
const task = status.task || {};
|
|
1597
|
-
console.log(`Developer Task: ${
|
|
883
|
+
console.log(`Developer Task: ${exec.status}`);
|
|
1598
884
|
console.log(`Sprint: #${resolution.sprintId}`);
|
|
1599
|
-
if (
|
|
885
|
+
if (exec.startedAt) console.log(`Started: ${new Date(exec.startedAt).toLocaleString()}`);
|
|
1600
886
|
if (task.branchName) console.log(`Branch: ${task.branchName}`);
|
|
1601
887
|
if (task.errorMessage) console.log(`Error: ${task.errorMessage}`);
|
|
1602
888
|
console.log(`Progress: ${sprint.storiesDone || 0}/${sprint.storiesTotal || 0} done, ${sprint.storiesInProgress || 0} in progress.`);
|
|
@@ -1605,6 +891,7 @@ function createDeveloperStatusCommand() {
|
|
|
1605
891
|
}
|
|
1606
892
|
|
|
1607
893
|
// src/commands/user.ts
|
|
894
|
+
import { ForLoopAPIClient as ForLoopAPIClient6, getToken as getToken7, getConfig as getConfig6 } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
1608
895
|
function createUserCommands() {
|
|
1609
896
|
return {
|
|
1610
897
|
"user profile": createUserProfileCommand(),
|
|
@@ -1613,13 +900,13 @@ function createUserCommands() {
|
|
|
1613
900
|
};
|
|
1614
901
|
}
|
|
1615
902
|
async function getClient5() {
|
|
1616
|
-
const config =
|
|
1617
|
-
const token = await
|
|
903
|
+
const config = getConfig6();
|
|
904
|
+
const token = await getToken7();
|
|
1618
905
|
if (!token) {
|
|
1619
906
|
console.error("Error: No API token configured.");
|
|
1620
907
|
process.exit(1);
|
|
1621
908
|
}
|
|
1622
|
-
return { client: new
|
|
909
|
+
return { client: new ForLoopAPIClient6({ token, baseUrl: config.apiUrl }) };
|
|
1623
910
|
}
|
|
1624
911
|
function createUserProfileCommand() {
|
|
1625
912
|
return defineCommand({
|
|
@@ -1679,8 +966,9 @@ function createOrgQuotasCommand() {
|
|
|
1679
966
|
}
|
|
1680
967
|
|
|
1681
968
|
// src/commands/file.ts
|
|
1682
|
-
import
|
|
1683
|
-
import * as
|
|
969
|
+
import { ForLoopAPIClient as ForLoopAPIClient7, getToken as getToken8, getConfig as getConfig7 } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
970
|
+
import * as fs from "fs";
|
|
971
|
+
import * as path from "path";
|
|
1684
972
|
function createFileCommands() {
|
|
1685
973
|
return {
|
|
1686
974
|
"file upload": createFileUploadCommand(),
|
|
@@ -1690,13 +978,13 @@ function createFileCommands() {
|
|
|
1690
978
|
};
|
|
1691
979
|
}
|
|
1692
980
|
async function getClient6() {
|
|
1693
|
-
const config =
|
|
1694
|
-
const token = await
|
|
981
|
+
const config = getConfig7();
|
|
982
|
+
const token = await getToken8();
|
|
1695
983
|
if (!token) {
|
|
1696
984
|
console.error("Error: No API token configured.");
|
|
1697
985
|
process.exit(1);
|
|
1698
986
|
}
|
|
1699
|
-
return { client: new
|
|
987
|
+
return { client: new ForLoopAPIClient7({ token, baseUrl: config.apiUrl }) };
|
|
1700
988
|
}
|
|
1701
989
|
var mimeTypes = {
|
|
1702
990
|
".png": "image/png",
|
|
@@ -1735,13 +1023,13 @@ function createFileUploadCommand() {
|
|
|
1735
1023
|
if (!resolved) return;
|
|
1736
1024
|
const { client } = resolved;
|
|
1737
1025
|
const filePath = flags.path;
|
|
1738
|
-
if (!
|
|
1026
|
+
if (!fs.existsSync(filePath)) {
|
|
1739
1027
|
console.error(`Error: File not found: ${filePath}`);
|
|
1740
1028
|
process.exit(1);
|
|
1741
1029
|
}
|
|
1742
|
-
const fileContent =
|
|
1743
|
-
const fileName =
|
|
1744
|
-
const ext =
|
|
1030
|
+
const fileContent = fs.readFileSync(filePath);
|
|
1031
|
+
const fileName = path.basename(filePath);
|
|
1032
|
+
const ext = path.extname(fileName);
|
|
1745
1033
|
const contentType = mimeTypes[ext] || "application/octet-stream";
|
|
1746
1034
|
const presign = await client.createPresignedUpload({
|
|
1747
1035
|
sprintId: flags.sprint,
|
|
@@ -1844,6 +1132,7 @@ function createFileDownloadCommand() {
|
|
|
1844
1132
|
}
|
|
1845
1133
|
|
|
1846
1134
|
// src/commands/folder.ts
|
|
1135
|
+
import { ForLoopAPIClient as ForLoopAPIClient8, getToken as getToken9, getConfig as getConfig8 } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
1847
1136
|
function createFolderCommands() {
|
|
1848
1137
|
return {
|
|
1849
1138
|
"folder create": createFolderCreateCommand()
|
|
@@ -1862,13 +1151,13 @@ function createFolderCreateCommand() {
|
|
|
1862
1151
|
],
|
|
1863
1152
|
examples: ['forloop folder create --title "Project Docs" --sprint 1 --permissions team'],
|
|
1864
1153
|
async run(_config, flags) {
|
|
1865
|
-
const config =
|
|
1866
|
-
const token = await
|
|
1154
|
+
const config = getConfig8();
|
|
1155
|
+
const token = await getToken9();
|
|
1867
1156
|
if (!token) {
|
|
1868
1157
|
console.error("Error: No API token configured.");
|
|
1869
1158
|
process.exit(1);
|
|
1870
1159
|
}
|
|
1871
|
-
const client = new
|
|
1160
|
+
const client = new ForLoopAPIClient8({ token, baseUrl: config.apiUrl });
|
|
1872
1161
|
const story = await client.createStory({
|
|
1873
1162
|
title: flags.title,
|
|
1874
1163
|
description: flags.description,
|
|
@@ -1883,8 +1172,9 @@ function createFolderCreateCommand() {
|
|
|
1883
1172
|
}
|
|
1884
1173
|
|
|
1885
1174
|
// src/commands/sync.ts
|
|
1886
|
-
import
|
|
1887
|
-
import * as
|
|
1175
|
+
import { ForLoopAPIClient as ForLoopAPIClient9, getToken as getToken10, resolveSprintId as resolveSprintId4, getConfig as getConfig9, getForloopRoot } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
1176
|
+
import * as fs2 from "fs";
|
|
1177
|
+
import * as path2 from "path";
|
|
1888
1178
|
function createSyncCommands() {
|
|
1889
1179
|
return {
|
|
1890
1180
|
"sync aivy-folder": createSyncAivyFolderCommand(),
|
|
@@ -1894,13 +1184,13 @@ function createSyncCommands() {
|
|
|
1894
1184
|
};
|
|
1895
1185
|
}
|
|
1896
1186
|
async function getClient7() {
|
|
1897
|
-
const config =
|
|
1898
|
-
const token = await
|
|
1187
|
+
const config = getConfig9();
|
|
1188
|
+
const token = await getToken10();
|
|
1899
1189
|
if (!token) {
|
|
1900
1190
|
console.error("Error: No API token configured.");
|
|
1901
1191
|
process.exit(1);
|
|
1902
1192
|
}
|
|
1903
|
-
return { client: new
|
|
1193
|
+
return { client: new ForLoopAPIClient9({ token, baseUrl: config.apiUrl }) };
|
|
1904
1194
|
}
|
|
1905
1195
|
function createSyncAivyFolderCommand() {
|
|
1906
1196
|
return defineCommand({
|
|
@@ -1916,7 +1206,7 @@ function createSyncAivyFolderCommand() {
|
|
|
1916
1206
|
const resolved = await getClient7();
|
|
1917
1207
|
if (!resolved) return;
|
|
1918
1208
|
const { client } = resolved;
|
|
1919
|
-
const resolution = await
|
|
1209
|
+
const resolution = await resolveSprintId4(flags.sprint);
|
|
1920
1210
|
if (!resolution.sprintId) {
|
|
1921
1211
|
console.error("Error: No sprint ID resolved.");
|
|
1922
1212
|
process.exit(1);
|
|
@@ -1953,7 +1243,7 @@ function createAivyDocGetCommand() {
|
|
|
1953
1243
|
const resolved = await getClient7();
|
|
1954
1244
|
if (!resolved) return;
|
|
1955
1245
|
const { client } = resolved;
|
|
1956
|
-
const resolution = await
|
|
1246
|
+
const resolution = await resolveSprintId4(flags.sprint);
|
|
1957
1247
|
if (!resolution.sprintId) {
|
|
1958
1248
|
console.error("Error: No sprint ID resolved.");
|
|
1959
1249
|
process.exit(1);
|
|
@@ -1987,14 +1277,14 @@ function createS3ToLocalCommand() {
|
|
|
1987
1277
|
const resolved = await getClient7();
|
|
1988
1278
|
if (!resolved) return;
|
|
1989
1279
|
const { client } = resolved;
|
|
1990
|
-
const resolution = await
|
|
1280
|
+
const resolution = await resolveSprintId4(flags.sprint);
|
|
1991
1281
|
if (!resolution.sprintId) {
|
|
1992
1282
|
console.error("Error: No sprint ID resolved.");
|
|
1993
1283
|
process.exit(1);
|
|
1994
1284
|
}
|
|
1995
1285
|
const files = await client.getSprintFiles(resolution.sprintId);
|
|
1996
1286
|
const localRoot = getForloopRoot(resolution.sprintId);
|
|
1997
|
-
|
|
1287
|
+
fs2.mkdirSync(localRoot, { recursive: true });
|
|
1998
1288
|
let downloaded = 0;
|
|
1999
1289
|
let skipped = 0;
|
|
2000
1290
|
for (const file of files) {
|
|
@@ -2009,9 +1299,9 @@ function createS3ToLocalCommand() {
|
|
|
2009
1299
|
if (res.ok) {
|
|
2010
1300
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
2011
1301
|
const kind = name.startsWith("knowledge-") ? "knowledge" : name.startsWith("plan-") ? "plan" : name.startsWith("task-") ? "task" : "other";
|
|
2012
|
-
const dir =
|
|
2013
|
-
|
|
2014
|
-
|
|
1302
|
+
const dir = path2.join(localRoot, kind);
|
|
1303
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
1304
|
+
fs2.writeFileSync(path2.join(dir, name), buf);
|
|
2015
1305
|
downloaded++;
|
|
2016
1306
|
}
|
|
2017
1307
|
}
|
|
@@ -2035,18 +1325,18 @@ function createLocalToS3Command() {
|
|
|
2035
1325
|
const resolved = await getClient7();
|
|
2036
1326
|
if (!resolved) return;
|
|
2037
1327
|
const { client } = resolved;
|
|
2038
|
-
const resolution = await
|
|
1328
|
+
const resolution = await resolveSprintId4(flags.sprint);
|
|
2039
1329
|
if (!resolution.sprintId) {
|
|
2040
1330
|
console.error("Error: No sprint ID resolved.");
|
|
2041
1331
|
process.exit(1);
|
|
2042
1332
|
}
|
|
2043
1333
|
const filePath = flags.path;
|
|
2044
|
-
if (!
|
|
1334
|
+
if (!fs2.existsSync(filePath)) {
|
|
2045
1335
|
console.error(`Error: File not found: ${filePath}`);
|
|
2046
1336
|
process.exit(1);
|
|
2047
1337
|
}
|
|
2048
|
-
const fileBuf =
|
|
2049
|
-
const fileName =
|
|
1338
|
+
const fileBuf = fs2.readFileSync(filePath);
|
|
1339
|
+
const fileName = path2.basename(filePath);
|
|
2050
1340
|
const contentType = "text/markdown";
|
|
2051
1341
|
const folder = flags.folder || "project";
|
|
2052
1342
|
const presign = await client.createPresignedUpload({
|
|
@@ -2077,12 +1367,12 @@ function createLocalToS3Command() {
|
|
|
2077
1367
|
var CommandRegistry = class {
|
|
2078
1368
|
root = { children: /* @__PURE__ */ new Map() };
|
|
2079
1369
|
constructor(commands) {
|
|
2080
|
-
for (const [
|
|
2081
|
-
this.register(
|
|
1370
|
+
for (const [path3, cmd] of Object.entries(commands)) {
|
|
1371
|
+
this.register(path3, cmd);
|
|
2082
1372
|
}
|
|
2083
1373
|
}
|
|
2084
|
-
register(
|
|
2085
|
-
const parts =
|
|
1374
|
+
register(path3, command) {
|
|
1375
|
+
const parts = path3.split(" ");
|
|
2086
1376
|
let node = this.root;
|
|
2087
1377
|
for (const part of parts) {
|
|
2088
1378
|
if (!node.children.has(part)) {
|
|
@@ -2362,7 +1652,10 @@ Tip: ${suggestion}
|
|
|
2362
1652
|
}
|
|
2363
1653
|
|
|
2364
1654
|
// src/main.ts
|
|
2365
|
-
|
|
1655
|
+
import { getConfig as getConfig10 } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
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";
|
|
2366
1659
|
process.on("SIGINT", () => {
|
|
2367
1660
|
process.stderr.write("\nInterrupted. Exiting.\n");
|
|
2368
1661
|
process.exit(130);
|
|
@@ -2389,7 +1682,7 @@ async function main() {
|
|
|
2389
1682
|
if (commandPath.length === 0) {
|
|
2390
1683
|
registry.printHelp([], process.stderr);
|
|
2391
1684
|
try {
|
|
2392
|
-
const token2 = await
|
|
1685
|
+
const token2 = await getToken11();
|
|
2393
1686
|
if (token2) {
|
|
2394
1687
|
const tokenType = detectTokenType(token2);
|
|
2395
1688
|
const masked = token2.slice(0, 8) + "..." + token2.slice(-4);
|
|
@@ -2410,8 +1703,8 @@ async function main() {
|
|
|
2410
1703
|
const { command, extra } = registry.resolve(commandPath);
|
|
2411
1704
|
const flags = parseFlags(argv, [...GLOBAL_OPTIONS, ...command.options ?? []]);
|
|
2412
1705
|
if (extra.length > 0) flags._positional = extra;
|
|
2413
|
-
const pluginConfig =
|
|
2414
|
-
const token = await
|
|
1706
|
+
const pluginConfig = getConfig10();
|
|
1707
|
+
const token = await getToken11();
|
|
2415
1708
|
const isNoAuth = NO_AUTH_COMMANDS.some(
|
|
2416
1709
|
(cmd) => cmd.every((c, i) => commandPath[i] === c)
|
|
2417
1710
|
);
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forloop-cc/forloop-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Command-line interface for the ForLoop AI development platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/forloop-cc/forloop-cli#readme",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/forloop-cc/forloop-cli/issues"
|
|
10
|
+
},
|
|
7
11
|
"keywords": [
|
|
8
12
|
"forloop",
|
|
9
13
|
"cli",
|
|
@@ -13,10 +17,10 @@
|
|
|
13
17
|
],
|
|
14
18
|
"repository": {
|
|
15
19
|
"type": "git",
|
|
16
|
-
"url": "https://github.com/forloop-cc/forloop-cli"
|
|
20
|
+
"url": "git+https://github.com/forloop-cc/forloop-cli.git"
|
|
17
21
|
},
|
|
18
22
|
"bin": {
|
|
19
|
-
"forloop": "
|
|
23
|
+
"forloop": "dist/forloop"
|
|
20
24
|
},
|
|
21
25
|
"files": [
|
|
22
26
|
"dist/",
|
|
@@ -36,7 +40,7 @@
|
|
|
36
40
|
"prepublishOnly": "npm run build"
|
|
37
41
|
},
|
|
38
42
|
"dependencies": {
|
|
39
|
-
"@forloop/opencode-plugin": "^2.2.0",
|
|
43
|
+
"@forloop-cc/forloop-opencode-plugin-planner": "^2.2.0",
|
|
40
44
|
"@opencode-ai/plugin": "^1.3.0"
|
|
41
45
|
},
|
|
42
46
|
"devDependencies": {
|