@forloop-cc/forloop-opencode-plugin-planner 2.2.4 → 2.2.6

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 CHANGED
@@ -7,6 +7,8 @@
7
7
 
8
8
  **AI-powered sprint planning, story management, and task automation for opencode**
9
9
 
10
+ [English](README.md) · [中文](README_zh.md) · [日本語](README_ja.md)
11
+
10
12
  [Quick Install](#quick-install) · [How It Works](#how-it-works) · [Capabilities](#capabilities) · [FAQ](#faq)
11
13
 
12
14
  </div>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@forloop-cc/forloop-opencode-plugin-planner",
4
- "version": "2.2.4",
4
+ "version": "2.2.6",
5
5
  "description": "ForLoop integration plugin for opencode — Sprint, story, and agent tools for the ForLoop platform",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -24,6 +24,10 @@
24
24
  ".": {
25
25
  "import": "./plugins/forloop-plugin.ts",
26
26
  "types": "./plugins/forloop-plugin.ts"
27
+ },
28
+ "./exports": {
29
+ "import": "./plugins/exports.ts",
30
+ "types": "./plugins/exports.ts"
27
31
  }
28
32
  },
29
33
  "files": [
@@ -0,0 +1,14 @@
1
+ // Re-exports for @forloop-cc/forloop-cli consumption.
2
+ // These were moved out of forloop-plugin.ts because Bun's TypeScript
3
+ // transpiler in opencode-ai@1.15.6 fails when a file has both
4
+ // `export default async` and additional `export { }` / `export type { }` lines.
5
+ export { ForLoopAPIClient } from './capabilities/api-client';
6
+ export type { APIError, CreateStoryRequest, CreateSprintRequest, ListSprintsParams, SprintOptions } from './capabilities/api-client';
7
+ export { getConfig, isLambdaExecution, PROD_API_URL, DEV_API_URL } from './capabilities/config';
8
+ export type { ForLoopConfig } from './capabilities/config';
9
+ export { getToken, setToken, clearToken } from './capabilities/token-storage';
10
+ export type { TokenProfile } from './capabilities/token-storage';
11
+ export { validateToken, detectTokenType } from './capabilities/auth';
12
+ export type { TokenValidationResult } from './capabilities/auth';
13
+ export { resolveSprintId, getCurrentBranch, resolveSprintFromBranch, getForloopRoot, getManifestPath } from './capabilities/context-resolver';
14
+ export type { ContextResolution } from './capabilities/context-resolver';
@@ -35,6 +35,7 @@ import {
35
35
  createFileListTool,
36
36
  createFileDeleteTool,
37
37
  createFileDownloadUrlTool,
38
+ createFileDownloadTool,
38
39
  } from './tools/file-tools';
39
40
  import {
40
41
  createScheduleTool,
@@ -123,7 +124,8 @@ export default async (ctx: any) => {
123
124
  'forloopFileUpload': createFileUploadTool(client),
124
125
  'forloopFileList': createFileListTool(client),
125
126
  'forloopFileDelete': createFileDeleteTool(client),
126
- 'forloopFileDownload': createFileDownloadUrlTool(client),
127
+ 'forloopFileDownloadUrl': createFileDownloadUrlTool(client),
128
+ 'forloopFileDownload': createFileDownloadTool(client),
127
129
 
128
130
  // Schedule Meetings
129
131
  'forloopScheduleCreate': createScheduleTool(client),
@@ -174,13 +176,3 @@ export default async (ctx: any) => {
174
176
  };
175
177
  };
176
178
 
177
- export { ForLoopAPIClient } from './capabilities/api-client';
178
- export type { APIError, CreateStoryRequest, CreateSprintRequest, ListSprintsParams, SprintOptions } from './capabilities/api-client';
179
- export { getConfig, isLambdaExecution, PROD_API_URL, DEV_API_URL } from './capabilities/config';
180
- export type { ForLoopConfig } from './capabilities/config';
181
- export { getToken, setToken, clearToken } from './capabilities/token-storage';
182
- export type { TokenProfile } from './capabilities/token-storage';
183
- export { validateToken, detectTokenType } from './capabilities/auth';
184
- export type { TokenValidationResult } from './capabilities/auth';
185
- export { resolveSprintId, getCurrentBranch, resolveSprintFromBranch, getForloopRoot, getManifestPath } from './capabilities/context-resolver';
186
- export type { ContextResolution } from './capabilities/context-resolver';
@@ -176,10 +176,10 @@ export function createFileDeleteTool(client: ForLoopAPIClient) {
176
176
 
177
177
  export function createFileDownloadUrlTool(client: ForLoopAPIClient) {
178
178
  return tool({
179
- description: 'Get a download URL for a file',
179
+ description: 'Get a presigned download URL for a file (does not download the file itself)',
180
180
  args: {
181
181
  fileId: tool.schema.number()
182
- .describe('File ID to download'),
182
+ .describe('File ID to get download URL for'),
183
183
  },
184
184
  async execute(args, _context) {
185
185
  const tokenResult = await validateToken();
@@ -189,10 +189,9 @@ export function createFileDownloadUrlTool(client: ForLoopAPIClient) {
189
189
  client.setToken?.(tokenResult.token);
190
190
 
191
191
  try {
192
- // Log file read (tracks access)
193
- await client.logFileRead(args.fileId);
192
+ // Fire-and-forget audit log — don't block download on audit failure
193
+ client.logFileRead(args.fileId).catch(() => {});
194
194
 
195
- // Get download URL
196
195
  const download = await client.getFileDownloadUrl(args.fileId);
197
196
 
198
197
  return [
@@ -209,3 +208,59 @@ export function createFileDownloadUrlTool(client: ForLoopAPIClient) {
209
208
  },
210
209
  });
211
210
  }
211
+
212
+ export function createFileDownloadTool(client: ForLoopAPIClient) {
213
+ return tool({
214
+ description: 'Download a sprint file by ID to the local sandbox',
215
+ args: {
216
+ fileId: tool.schema.number()
217
+ .describe('File ID to download (from forloopFileList)'),
218
+ destPath: tool.schema.string()
219
+ .optional()
220
+ .describe('Destination path relative to sandbox downloads dir (default: <filename>)'),
221
+ },
222
+ async execute(args, _context) {
223
+ const tokenResult = await validateToken();
224
+ if (!tokenResult.valid) {
225
+ return tokenResult.error ?? '❌ No API token configured.';
226
+ }
227
+ client.setToken?.(tokenResult.token);
228
+
229
+ try {
230
+ const open = await client.getFileOpenUrl(Number(args.fileId));
231
+ const url = String(open?.openUrl || open?.url || '');
232
+ if (!url) {
233
+ return `❌ Could not get download URL for file #${args.fileId}.`;
234
+ }
235
+
236
+ const response = await fetch(url);
237
+ if (!response.ok) {
238
+ return `❌ Download failed: HTTP ${response.status}`;
239
+ }
240
+
241
+ const buf = Buffer.from(await response.arrayBuffer());
242
+ const fileName = open?.originalName || open?.filename || `file-${args.fileId}`;
243
+ const downloadDir = path.join(process.env.HOME || '/tmp', 'downloads');
244
+ if (!fs.existsSync(downloadDir)) {
245
+ fs.mkdirSync(downloadDir, { recursive: true });
246
+ }
247
+
248
+ const destFile = args.destPath
249
+ ? path.join(downloadDir, args.destPath)
250
+ : path.join(downloadDir, fileName);
251
+ const destDir = path.dirname(destFile);
252
+ if (!fs.existsSync(destDir)) {
253
+ fs.mkdirSync(destDir, { recursive: true });
254
+ }
255
+ fs.writeFileSync(destFile, buf);
256
+
257
+ // Log file read (fire-and-forget audit)
258
+ client.logFileRead(args.fileId).catch(() => {});
259
+
260
+ return `✅ Downloaded file #${args.fileId} → ${destFile} (${buf.length} bytes)`;
261
+ } catch (error: any) {
262
+ return `❌ File download failed: ${error.message}`;
263
+ }
264
+ },
265
+ });
266
+ }