@forloop-cc/forloop-cli 0.2.2 → 0.2.3
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 +782 -75
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,10 +99,7 @@ That's it. You're connected.
|
|
|
99
99
|
|
|
100
100
|
Visit the ForLoop settings page to generate a token:
|
|
101
101
|
|
|
102
|
-
| Environment | URL |
|
|
103
|
-
|-------------|-----|
|
|
104
102
|
| Production | https://forloop.cc/profile?tab=api-tokens |
|
|
105
|
-
| Development | https://dev.forloop.cc/profile?tab=api-tokens |
|
|
106
103
|
|
|
107
104
|
Tokens start with `floop_`. Create a token with at least these scopes:
|
|
108
105
|
- `sprint:read`, `sprint:write`
|
|
@@ -132,69 +129,6 @@ forloop auth logout # Remove saved token
|
|
|
132
129
|
|
|
133
130
|
---
|
|
134
131
|
|
|
135
|
-
## Environments
|
|
136
|
-
|
|
137
|
-
ForLoop has two environments — **Production** and **Development**. By default, the CLI connects to production. Use the development environment when you want to test features or work on staging data without affecting production.
|
|
138
|
-
|
|
139
|
-
### Production (default)
|
|
140
|
-
|
|
141
|
-
No extra setup needed. The CLI connects to `https://api.forloop.cc` automatically.
|
|
142
|
-
|
|
143
|
-
```bash
|
|
144
|
-
forloop sprint list
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### Development
|
|
148
|
-
|
|
149
|
-
The development environment connects to `https://api.dev.forloop.cc`. You need two things:
|
|
150
|
-
|
|
151
|
-
1. **A dev API token** — create one at https://dev.forloop.cc/profile?tab=api-tokens
|
|
152
|
-
2. **Environment variables** — the CLI requires explicit opt-in for security:
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
export FORLOOP_ENV=development
|
|
156
|
-
export FORLOOP_ALLOW_DEV=true
|
|
157
|
-
forloop auth login --api-key floop_xxxxx
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
> **Security note:** `FORLOOP_ALLOW_DEV=true` is required because dev API tokens have fewer restrictions. This flag prevents accidentally using a dev token against production data. Set it only when you intend to use the dev environment.
|
|
161
|
-
|
|
162
|
-
### One-Liner for Dev
|
|
163
|
-
|
|
164
|
-
```bash
|
|
165
|
-
FORLOOP_ENV=development FORLOOP_ALLOW_DEV=true forloop sprint list
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### Persistent Dev Setup
|
|
169
|
-
|
|
170
|
-
To always use dev for a specific project, add an `opencode.json` in your project root:
|
|
171
|
-
|
|
172
|
-
```json
|
|
173
|
-
{
|
|
174
|
-
"forloop": {
|
|
175
|
-
"apiUrl": "https://api.dev.forloop.cc",
|
|
176
|
-
"allowDev": true
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
Or set it globally in `~/.config/opencode/config.json`:
|
|
182
|
-
|
|
183
|
-
```json
|
|
184
|
-
{
|
|
185
|
-
"forloop": {
|
|
186
|
-
"apiUrl": "https://api.dev.forloop.cc",
|
|
187
|
-
"allowDev": true
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
| Question | Answer |
|
|
193
|
-
|----------|--------|
|
|
194
|
-
| What happens if I don't set `FORLOOP_ALLOW_DEV=true`? | The CLI will ignore your dev URL and connect to production instead, with a warning message |
|
|
195
|
-
| Can I switch between prod and dev per-command? | Yes, the `--api-url` flag or `FORLOOP_ENV` env var overrides the config |
|
|
196
|
-
| Do I need separate tokens for prod and dev? | Yes — create one token at each environment's settings page |
|
|
197
|
-
|
|
198
132
|
---
|
|
199
133
|
|
|
200
134
|
## Commands
|
|
@@ -419,9 +353,7 @@ These flags work with every command:
|
|
|
419
353
|
| Variable | Purpose |
|
|
420
354
|
|----------|---------|
|
|
421
355
|
| `FORLOOP_API_KEY` | API token (alternative to `forloop auth login`) |
|
|
422
|
-
| `FORLOOP_API_URL` | Override the API base URL |
|
|
423
|
-
| `FORLOOP_ENV` | Set to `production` or `development` |
|
|
424
|
-
| `FORLOOP_ALLOW_DEV` | Set to `true` to enable dev environment URLs |
|
|
356
|
+
| `FORLOOP_API_URL` | Override the API base URL (e.g., `https://api.dev.forloop.cc`) |
|
|
425
357
|
| `FORLOOP_SPRINT_ID` | Default sprint ID for commands that auto-detect it |
|
|
426
358
|
|
|
427
359
|
---
|
|
@@ -561,8 +493,7 @@ The CLI resolves configuration from these sources, in priority order:
|
|
|
561
493
|
```json
|
|
562
494
|
{
|
|
563
495
|
"forloop": {
|
|
564
|
-
"apiUrl": "https://api.
|
|
565
|
-
"allowDev": true
|
|
496
|
+
"apiUrl": "https://api.forloop.cc"
|
|
566
497
|
}
|
|
567
498
|
}
|
|
568
499
|
```
|
|
@@ -572,8 +503,7 @@ The CLI resolves configuration from these sources, in priority order:
|
|
|
572
503
|
```json
|
|
573
504
|
{
|
|
574
505
|
"forloop": {
|
|
575
|
-
"apiUrl": "https://api.forloop.cc"
|
|
576
|
-
"allowDev": false
|
|
506
|
+
"apiUrl": "https://api.forloop.cc"
|
|
577
507
|
}
|
|
578
508
|
}
|
|
579
509
|
```
|
|
@@ -600,23 +530,11 @@ export FORLOOP_API_KEY=floop_xxxxx
|
|
|
600
530
|
|
|
601
531
|
Tokens must start with `floop_`. Double-check you copied the entire token from https://forloop.cc/profile?tab=api-tokens.
|
|
602
532
|
|
|
603
|
-
### "Dev API URL ignored"
|
|
604
|
-
|
|
605
|
-
The CLI refuses dev URLs unless you explicitly opt in:
|
|
606
|
-
|
|
607
|
-
```bash
|
|
608
|
-
export FORLOOP_ALLOW_DEV=true
|
|
609
|
-
export FORLOOP_ENV=development
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
This is a security measure — dev tokens should never accidentally hit production APIs.
|
|
613
|
-
|
|
614
533
|
### "Forbidden" or "Not Found" on Specific Resources
|
|
615
534
|
|
|
616
535
|
Some resources (organizations, stories, files) return 403/404 when you don't have access or they don't exist in your current environment. Try:
|
|
617
|
-
- Checking you're in the right environment (prod vs dev)
|
|
618
536
|
- Verifying the resource ID exists with `forloop sprint get --id <id>`
|
|
619
|
-
- Checking
|
|
537
|
+
- Checking your token has the required scopes
|
|
620
538
|
|
|
621
539
|
### Verbose Debugging
|
|
622
540
|
|
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(),
|
|
@@ -211,7 +930,6 @@ function createAuthLogoutCommand() {
|
|
|
211
930
|
}
|
|
212
931
|
|
|
213
932
|
// src/commands/story.ts
|
|
214
|
-
import { ForLoopAPIClient, getToken as getToken2, getConfig } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
215
933
|
function createStoryCommands() {
|
|
216
934
|
return {
|
|
217
935
|
"story create": createStoryCreateCommand(),
|
|
@@ -222,7 +940,7 @@ function createStoryCommands() {
|
|
|
222
940
|
}
|
|
223
941
|
async function getClient() {
|
|
224
942
|
const config = getConfig();
|
|
225
|
-
const token = await
|
|
943
|
+
const token = await getToken();
|
|
226
944
|
if (!token) {
|
|
227
945
|
console.error("Error: No API token configured. Run: forloop auth login --api-key <token>");
|
|
228
946
|
process.exit(1);
|
|
@@ -412,7 +1130,6 @@ function createStoryDeleteCommand() {
|
|
|
412
1130
|
}
|
|
413
1131
|
|
|
414
1132
|
// 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
1133
|
function createSprintCommands() {
|
|
417
1134
|
return {
|
|
418
1135
|
"sprint get": createSprintGetCommand(),
|
|
@@ -423,13 +1140,13 @@ function createSprintCommands() {
|
|
|
423
1140
|
};
|
|
424
1141
|
}
|
|
425
1142
|
async function getClient2() {
|
|
426
|
-
const config =
|
|
427
|
-
const token = await
|
|
1143
|
+
const config = getConfig();
|
|
1144
|
+
const token = await getToken();
|
|
428
1145
|
if (!token) {
|
|
429
1146
|
console.error("Error: No API token configured. Run: forloop auth login --api-key <token>");
|
|
430
1147
|
process.exit(1);
|
|
431
1148
|
}
|
|
432
|
-
const client = new
|
|
1149
|
+
const client = new ForLoopAPIClient({ token, baseUrl: config.apiUrl });
|
|
433
1150
|
return { client };
|
|
434
1151
|
}
|
|
435
1152
|
function formatDate(dateString) {
|
|
@@ -450,7 +1167,7 @@ function createSprintGetCommand() {
|
|
|
450
1167
|
const resolved = await getClient2();
|
|
451
1168
|
if (!resolved) return;
|
|
452
1169
|
const { client } = resolved;
|
|
453
|
-
const resolution = await
|
|
1170
|
+
const resolution = await resolveSprintId(flags.id);
|
|
454
1171
|
if (!resolution.sprintId) {
|
|
455
1172
|
console.error("Error: No sprint ID resolved. Use --id, set FORLOOP_SPRINT_ID, or use a sprint-XXX branch.");
|
|
456
1173
|
process.exit(1);
|
|
@@ -599,7 +1316,6 @@ function createSprintDeleteCommand() {
|
|
|
599
1316
|
}
|
|
600
1317
|
|
|
601
1318
|
// src/commands/org.ts
|
|
602
|
-
import { ForLoopAPIClient as ForLoopAPIClient3, getToken as getToken4, getConfig as getConfig3 } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
603
1319
|
function createOrgCommands() {
|
|
604
1320
|
return {
|
|
605
1321
|
"org list": createOrgListCommand(),
|
|
@@ -609,13 +1325,13 @@ function createOrgCommands() {
|
|
|
609
1325
|
};
|
|
610
1326
|
}
|
|
611
1327
|
async function getClient3() {
|
|
612
|
-
const config =
|
|
613
|
-
const token = await
|
|
1328
|
+
const config = getConfig();
|
|
1329
|
+
const token = await getToken();
|
|
614
1330
|
if (!token) {
|
|
615
1331
|
console.error("Error: No API token configured. Run: forloop auth login --api-key <token>");
|
|
616
1332
|
process.exit(1);
|
|
617
1333
|
}
|
|
618
|
-
return { client: new
|
|
1334
|
+
return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl }) };
|
|
619
1335
|
}
|
|
620
1336
|
function createOrgListCommand() {
|
|
621
1337
|
return defineCommand({
|
|
@@ -723,7 +1439,6 @@ function createOrgUpdateCommand() {
|
|
|
723
1439
|
}
|
|
724
1440
|
|
|
725
1441
|
// src/commands/template.ts
|
|
726
|
-
import { ForLoopAPIClient as ForLoopAPIClient4, getToken as getToken5, getConfig as getConfig4 } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
727
1442
|
function createTemplateCommands() {
|
|
728
1443
|
return {
|
|
729
1444
|
"template list": createTemplateListCommand()
|
|
@@ -736,13 +1451,13 @@ function createTemplateListCommand() {
|
|
|
736
1451
|
usage: "forloop template list",
|
|
737
1452
|
examples: ["forloop template list"],
|
|
738
1453
|
async run(_config) {
|
|
739
|
-
const config =
|
|
740
|
-
const token = await
|
|
1454
|
+
const config = getConfig();
|
|
1455
|
+
const token = await getToken();
|
|
741
1456
|
if (!token) {
|
|
742
1457
|
console.error("Error: No API token configured.");
|
|
743
1458
|
process.exit(1);
|
|
744
1459
|
}
|
|
745
|
-
const client = new
|
|
1460
|
+
const client = new ForLoopAPIClient({ token, baseUrl: config.apiUrl });
|
|
746
1461
|
const templates = await client.listTemplates();
|
|
747
1462
|
const active = templates.filter((t) => !t.deletedAt);
|
|
748
1463
|
if (!active.length) {
|
|
@@ -764,7 +1479,6 @@ function createTemplateListCommand() {
|
|
|
764
1479
|
}
|
|
765
1480
|
|
|
766
1481
|
// 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
1482
|
function createAgentCommands() {
|
|
769
1483
|
return {
|
|
770
1484
|
"agent history": createAgentHistoryCommand(),
|
|
@@ -773,13 +1487,13 @@ function createAgentCommands() {
|
|
|
773
1487
|
};
|
|
774
1488
|
}
|
|
775
1489
|
async function getClient4() {
|
|
776
|
-
const config =
|
|
777
|
-
const token = await
|
|
1490
|
+
const config = getConfig();
|
|
1491
|
+
const token = await getToken();
|
|
778
1492
|
if (!token) {
|
|
779
1493
|
console.error("Error: No API token configured.");
|
|
780
1494
|
process.exit(1);
|
|
781
1495
|
}
|
|
782
|
-
return { client: new
|
|
1496
|
+
return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl }) };
|
|
783
1497
|
}
|
|
784
1498
|
function createAgentHistoryCommand() {
|
|
785
1499
|
return defineCommand({
|
|
@@ -795,7 +1509,7 @@ function createAgentHistoryCommand() {
|
|
|
795
1509
|
const resolved = await getClient4();
|
|
796
1510
|
if (!resolved) return;
|
|
797
1511
|
const { client } = resolved;
|
|
798
|
-
const resolution = await
|
|
1512
|
+
const resolution = await resolveSprintId(flags.sprint);
|
|
799
1513
|
const sprintId = resolution.sprintId || flags.sprint;
|
|
800
1514
|
const history = await client.getConversationHistory({
|
|
801
1515
|
sprintId,
|
|
@@ -866,7 +1580,7 @@ function createDeveloperStatusCommand() {
|
|
|
866
1580
|
const resolved = await getClient4();
|
|
867
1581
|
if (!resolved) return;
|
|
868
1582
|
const { client } = resolved;
|
|
869
|
-
const resolution = await
|
|
1583
|
+
const resolution = await resolveSprintId(flags.sprint);
|
|
870
1584
|
if (!resolution.sprintId) {
|
|
871
1585
|
console.error("Error: No sprint ID resolved.");
|
|
872
1586
|
process.exit(1);
|
|
@@ -878,11 +1592,11 @@ function createDeveloperStatusCommand() {
|
|
|
878
1592
|
console.log(`Sprint #${resolution.sprintId}: ${sprint.storiesDone || 0}/${sprint.storiesTotal || 0} stories done.`);
|
|
879
1593
|
return;
|
|
880
1594
|
}
|
|
881
|
-
const
|
|
1595
|
+
const exec3 = status.execution || {};
|
|
882
1596
|
const task = status.task || {};
|
|
883
|
-
console.log(`Developer Task: ${
|
|
1597
|
+
console.log(`Developer Task: ${exec3.status}`);
|
|
884
1598
|
console.log(`Sprint: #${resolution.sprintId}`);
|
|
885
|
-
if (
|
|
1599
|
+
if (exec3.startedAt) console.log(`Started: ${new Date(exec3.startedAt).toLocaleString()}`);
|
|
886
1600
|
if (task.branchName) console.log(`Branch: ${task.branchName}`);
|
|
887
1601
|
if (task.errorMessage) console.log(`Error: ${task.errorMessage}`);
|
|
888
1602
|
console.log(`Progress: ${sprint.storiesDone || 0}/${sprint.storiesTotal || 0} done, ${sprint.storiesInProgress || 0} in progress.`);
|
|
@@ -891,7 +1605,6 @@ function createDeveloperStatusCommand() {
|
|
|
891
1605
|
}
|
|
892
1606
|
|
|
893
1607
|
// src/commands/user.ts
|
|
894
|
-
import { ForLoopAPIClient as ForLoopAPIClient6, getToken as getToken7, getConfig as getConfig6 } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
895
1608
|
function createUserCommands() {
|
|
896
1609
|
return {
|
|
897
1610
|
"user profile": createUserProfileCommand(),
|
|
@@ -900,13 +1613,13 @@ function createUserCommands() {
|
|
|
900
1613
|
};
|
|
901
1614
|
}
|
|
902
1615
|
async function getClient5() {
|
|
903
|
-
const config =
|
|
904
|
-
const token = await
|
|
1616
|
+
const config = getConfig();
|
|
1617
|
+
const token = await getToken();
|
|
905
1618
|
if (!token) {
|
|
906
1619
|
console.error("Error: No API token configured.");
|
|
907
1620
|
process.exit(1);
|
|
908
1621
|
}
|
|
909
|
-
return { client: new
|
|
1622
|
+
return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl }) };
|
|
910
1623
|
}
|
|
911
1624
|
function createUserProfileCommand() {
|
|
912
1625
|
return defineCommand({
|
|
@@ -966,9 +1679,8 @@ function createOrgQuotasCommand() {
|
|
|
966
1679
|
}
|
|
967
1680
|
|
|
968
1681
|
// src/commands/file.ts
|
|
969
|
-
import
|
|
970
|
-
import * as
|
|
971
|
-
import * as path from "path";
|
|
1682
|
+
import * as fs4 from "fs";
|
|
1683
|
+
import * as path4 from "path";
|
|
972
1684
|
function createFileCommands() {
|
|
973
1685
|
return {
|
|
974
1686
|
"file upload": createFileUploadCommand(),
|
|
@@ -978,13 +1690,13 @@ function createFileCommands() {
|
|
|
978
1690
|
};
|
|
979
1691
|
}
|
|
980
1692
|
async function getClient6() {
|
|
981
|
-
const config =
|
|
982
|
-
const token = await
|
|
1693
|
+
const config = getConfig();
|
|
1694
|
+
const token = await getToken();
|
|
983
1695
|
if (!token) {
|
|
984
1696
|
console.error("Error: No API token configured.");
|
|
985
1697
|
process.exit(1);
|
|
986
1698
|
}
|
|
987
|
-
return { client: new
|
|
1699
|
+
return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl }) };
|
|
988
1700
|
}
|
|
989
1701
|
var mimeTypes = {
|
|
990
1702
|
".png": "image/png",
|
|
@@ -1023,13 +1735,13 @@ function createFileUploadCommand() {
|
|
|
1023
1735
|
if (!resolved) return;
|
|
1024
1736
|
const { client } = resolved;
|
|
1025
1737
|
const filePath = flags.path;
|
|
1026
|
-
if (!
|
|
1738
|
+
if (!fs4.existsSync(filePath)) {
|
|
1027
1739
|
console.error(`Error: File not found: ${filePath}`);
|
|
1028
1740
|
process.exit(1);
|
|
1029
1741
|
}
|
|
1030
|
-
const fileContent =
|
|
1031
|
-
const fileName =
|
|
1032
|
-
const ext =
|
|
1742
|
+
const fileContent = fs4.readFileSync(filePath);
|
|
1743
|
+
const fileName = path4.basename(filePath);
|
|
1744
|
+
const ext = path4.extname(fileName);
|
|
1033
1745
|
const contentType = mimeTypes[ext] || "application/octet-stream";
|
|
1034
1746
|
const presign = await client.createPresignedUpload({
|
|
1035
1747
|
sprintId: flags.sprint,
|
|
@@ -1132,7 +1844,6 @@ function createFileDownloadCommand() {
|
|
|
1132
1844
|
}
|
|
1133
1845
|
|
|
1134
1846
|
// src/commands/folder.ts
|
|
1135
|
-
import { ForLoopAPIClient as ForLoopAPIClient8, getToken as getToken9, getConfig as getConfig8 } from "@forloop-cc/forloop-opencode-plugin-planner";
|
|
1136
1847
|
function createFolderCommands() {
|
|
1137
1848
|
return {
|
|
1138
1849
|
"folder create": createFolderCreateCommand()
|
|
@@ -1151,13 +1862,13 @@ function createFolderCreateCommand() {
|
|
|
1151
1862
|
],
|
|
1152
1863
|
examples: ['forloop folder create --title "Project Docs" --sprint 1 --permissions team'],
|
|
1153
1864
|
async run(_config, flags) {
|
|
1154
|
-
const config =
|
|
1155
|
-
const token = await
|
|
1865
|
+
const config = getConfig();
|
|
1866
|
+
const token = await getToken();
|
|
1156
1867
|
if (!token) {
|
|
1157
1868
|
console.error("Error: No API token configured.");
|
|
1158
1869
|
process.exit(1);
|
|
1159
1870
|
}
|
|
1160
|
-
const client = new
|
|
1871
|
+
const client = new ForLoopAPIClient({ token, baseUrl: config.apiUrl });
|
|
1161
1872
|
const story = await client.createStory({
|
|
1162
1873
|
title: flags.title,
|
|
1163
1874
|
description: flags.description,
|
|
@@ -1172,9 +1883,8 @@ function createFolderCreateCommand() {
|
|
|
1172
1883
|
}
|
|
1173
1884
|
|
|
1174
1885
|
// src/commands/sync.ts
|
|
1175
|
-
import
|
|
1176
|
-
import * as
|
|
1177
|
-
import * as path2 from "path";
|
|
1886
|
+
import * as fs5 from "fs";
|
|
1887
|
+
import * as path5 from "path";
|
|
1178
1888
|
function createSyncCommands() {
|
|
1179
1889
|
return {
|
|
1180
1890
|
"sync aivy-folder": createSyncAivyFolderCommand(),
|
|
@@ -1184,13 +1894,13 @@ function createSyncCommands() {
|
|
|
1184
1894
|
};
|
|
1185
1895
|
}
|
|
1186
1896
|
async function getClient7() {
|
|
1187
|
-
const config =
|
|
1188
|
-
const token = await
|
|
1897
|
+
const config = getConfig();
|
|
1898
|
+
const token = await getToken();
|
|
1189
1899
|
if (!token) {
|
|
1190
1900
|
console.error("Error: No API token configured.");
|
|
1191
1901
|
process.exit(1);
|
|
1192
1902
|
}
|
|
1193
|
-
return { client: new
|
|
1903
|
+
return { client: new ForLoopAPIClient({ token, baseUrl: config.apiUrl }) };
|
|
1194
1904
|
}
|
|
1195
1905
|
function createSyncAivyFolderCommand() {
|
|
1196
1906
|
return defineCommand({
|
|
@@ -1206,7 +1916,7 @@ function createSyncAivyFolderCommand() {
|
|
|
1206
1916
|
const resolved = await getClient7();
|
|
1207
1917
|
if (!resolved) return;
|
|
1208
1918
|
const { client } = resolved;
|
|
1209
|
-
const resolution = await
|
|
1919
|
+
const resolution = await resolveSprintId(flags.sprint);
|
|
1210
1920
|
if (!resolution.sprintId) {
|
|
1211
1921
|
console.error("Error: No sprint ID resolved.");
|
|
1212
1922
|
process.exit(1);
|
|
@@ -1243,7 +1953,7 @@ function createAivyDocGetCommand() {
|
|
|
1243
1953
|
const resolved = await getClient7();
|
|
1244
1954
|
if (!resolved) return;
|
|
1245
1955
|
const { client } = resolved;
|
|
1246
|
-
const resolution = await
|
|
1956
|
+
const resolution = await resolveSprintId(flags.sprint);
|
|
1247
1957
|
if (!resolution.sprintId) {
|
|
1248
1958
|
console.error("Error: No sprint ID resolved.");
|
|
1249
1959
|
process.exit(1);
|
|
@@ -1277,14 +1987,14 @@ function createS3ToLocalCommand() {
|
|
|
1277
1987
|
const resolved = await getClient7();
|
|
1278
1988
|
if (!resolved) return;
|
|
1279
1989
|
const { client } = resolved;
|
|
1280
|
-
const resolution = await
|
|
1990
|
+
const resolution = await resolveSprintId(flags.sprint);
|
|
1281
1991
|
if (!resolution.sprintId) {
|
|
1282
1992
|
console.error("Error: No sprint ID resolved.");
|
|
1283
1993
|
process.exit(1);
|
|
1284
1994
|
}
|
|
1285
1995
|
const files = await client.getSprintFiles(resolution.sprintId);
|
|
1286
1996
|
const localRoot = getForloopRoot(resolution.sprintId);
|
|
1287
|
-
|
|
1997
|
+
fs5.mkdirSync(localRoot, { recursive: true });
|
|
1288
1998
|
let downloaded = 0;
|
|
1289
1999
|
let skipped = 0;
|
|
1290
2000
|
for (const file of files) {
|
|
@@ -1299,9 +2009,9 @@ function createS3ToLocalCommand() {
|
|
|
1299
2009
|
if (res.ok) {
|
|
1300
2010
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
1301
2011
|
const kind = name.startsWith("knowledge-") ? "knowledge" : name.startsWith("plan-") ? "plan" : name.startsWith("task-") ? "task" : "other";
|
|
1302
|
-
const dir =
|
|
1303
|
-
|
|
1304
|
-
|
|
2012
|
+
const dir = path5.join(localRoot, kind);
|
|
2013
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
2014
|
+
fs5.writeFileSync(path5.join(dir, name), buf);
|
|
1305
2015
|
downloaded++;
|
|
1306
2016
|
}
|
|
1307
2017
|
}
|
|
@@ -1325,18 +2035,18 @@ function createLocalToS3Command() {
|
|
|
1325
2035
|
const resolved = await getClient7();
|
|
1326
2036
|
if (!resolved) return;
|
|
1327
2037
|
const { client } = resolved;
|
|
1328
|
-
const resolution = await
|
|
2038
|
+
const resolution = await resolveSprintId(flags.sprint);
|
|
1329
2039
|
if (!resolution.sprintId) {
|
|
1330
2040
|
console.error("Error: No sprint ID resolved.");
|
|
1331
2041
|
process.exit(1);
|
|
1332
2042
|
}
|
|
1333
2043
|
const filePath = flags.path;
|
|
1334
|
-
if (!
|
|
2044
|
+
if (!fs5.existsSync(filePath)) {
|
|
1335
2045
|
console.error(`Error: File not found: ${filePath}`);
|
|
1336
2046
|
process.exit(1);
|
|
1337
2047
|
}
|
|
1338
|
-
const fileBuf =
|
|
1339
|
-
const fileName =
|
|
2048
|
+
const fileBuf = fs5.readFileSync(filePath);
|
|
2049
|
+
const fileName = path5.basename(filePath);
|
|
1340
2050
|
const contentType = "text/markdown";
|
|
1341
2051
|
const folder = flags.folder || "project";
|
|
1342
2052
|
const presign = await client.createPresignedUpload({
|
|
@@ -1367,12 +2077,12 @@ function createLocalToS3Command() {
|
|
|
1367
2077
|
var CommandRegistry = class {
|
|
1368
2078
|
root = { children: /* @__PURE__ */ new Map() };
|
|
1369
2079
|
constructor(commands) {
|
|
1370
|
-
for (const [
|
|
1371
|
-
this.register(
|
|
2080
|
+
for (const [path6, cmd] of Object.entries(commands)) {
|
|
2081
|
+
this.register(path6, cmd);
|
|
1372
2082
|
}
|
|
1373
2083
|
}
|
|
1374
|
-
register(
|
|
1375
|
-
const parts =
|
|
2084
|
+
register(path6, command) {
|
|
2085
|
+
const parts = path6.split(" ");
|
|
1376
2086
|
let node = this.root;
|
|
1377
2087
|
for (const part of parts) {
|
|
1378
2088
|
if (!node.children.has(part)) {
|
|
@@ -1652,10 +2362,7 @@ Tip: ${suggestion}
|
|
|
1652
2362
|
}
|
|
1653
2363
|
|
|
1654
2364
|
// 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";
|
|
2365
|
+
var CLI_VERSION = "0.2.3";
|
|
1659
2366
|
process.on("SIGINT", () => {
|
|
1660
2367
|
process.stderr.write("\nInterrupted. Exiting.\n");
|
|
1661
2368
|
process.exit(130);
|
|
@@ -1682,7 +2389,7 @@ async function main() {
|
|
|
1682
2389
|
if (commandPath.length === 0) {
|
|
1683
2390
|
registry.printHelp([], process.stderr);
|
|
1684
2391
|
try {
|
|
1685
|
-
const token2 = await
|
|
2392
|
+
const token2 = await getToken();
|
|
1686
2393
|
if (token2) {
|
|
1687
2394
|
const tokenType = detectTokenType(token2);
|
|
1688
2395
|
const masked = token2.slice(0, 8) + "..." + token2.slice(-4);
|
|
@@ -1703,8 +2410,8 @@ async function main() {
|
|
|
1703
2410
|
const { command, extra } = registry.resolve(commandPath);
|
|
1704
2411
|
const flags = parseFlags(argv, [...GLOBAL_OPTIONS, ...command.options ?? []]);
|
|
1705
2412
|
if (extra.length > 0) flags._positional = extra;
|
|
1706
|
-
const pluginConfig =
|
|
1707
|
-
const token = await
|
|
2413
|
+
const pluginConfig = getConfig();
|
|
2414
|
+
const token = await getToken();
|
|
1708
2415
|
const isNoAuth = NO_AUTH_COMMANDS.some(
|
|
1709
2416
|
(cmd) => cmd.every((c, i) => commandPath[i] === c)
|
|
1710
2417
|
);
|
|
@@ -1715,7 +2422,7 @@ async function main() {
|
|
|
1715
2422
|
process.exit(3);
|
|
1716
2423
|
}
|
|
1717
2424
|
const cliConfig = {
|
|
1718
|
-
region:
|
|
2425
|
+
region: "global",
|
|
1719
2426
|
apiUrl: pluginConfig.apiUrl,
|
|
1720
2427
|
serverLambdaUrl: "",
|
|
1721
2428
|
environment: pluginConfig.environment,
|