@forloop-cc/forloop-opencode-plugin-planner 2.2.25 → 2.2.26

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/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.25",
4
+ "version": "2.2.26",
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",
@@ -190,7 +190,7 @@ export function createCoreServiceGetTool(client: ForLoopAPIClient) {
190
190
  export function createCoreServiceIntegrationRequestTool(client: ForLoopAPIClient) {
191
191
  return tool({
192
192
  description:
193
- 'Prepare the user-approval step for a core-service integration and return a deep link to the app’s App Connections panel plus the app-subject connection payload. Nothing is created or stored until the user clicks Connect. This never creates a connection, never touches credentials, and never constructs API URLs by hand.',
193
+ 'Start the user-approval step for a core-service integration: persist (or idempotently reuse) the pending connection request, then ask the signed-in user’s live client to open App Connections focused on it so they only need to click Approve. Falls back to the approval link when the client is offline. This never approves, never touches credentials, and never constructs API URLs by hand.',
194
194
  args: {
195
195
  agentId: tool.schema.string().describe('Target core-service agentId'),
196
196
  scopes: tool.schema
@@ -262,43 +262,86 @@ export function createCoreServiceIntegrationRequestTool(client: ForLoopAPIClient
262
262
  linkParams.set('connect', args.agentId);
263
263
  linkParams.set('scopes', requested.join(','));
264
264
  linkParams.set('subject', 'app');
265
- const connectUrl = spaceUrl ? `${spaceUrl}?${linkParams.toString()}` : null;
265
+ const fallbackConnectUrl = spaceUrl ? `${spaceUrl}?${linkParams.toString()}` : null;
266
266
 
267
- const approvalSteps = connectUrl
267
+ // Persist the pending request and nudge the live client to open the
268
+ // panel. Both steps are best-effort: an offline client (or a backend
269
+ // without the request routes) must still return an actionable link.
270
+ let requestId: string | null = null;
271
+ let delivered = false;
272
+ let connectUrl: string | null = fallbackConnectUrl;
273
+ let dropped: string[] = [];
274
+ if (callerSprintId) {
275
+ try {
276
+ const created = await client.createConnectionRequest({
277
+ sprintId: callerSprintId,
278
+ targetAgentId: args.agentId,
279
+ scopes: requested,
280
+ subjectType: 'app',
281
+ requestedBy: 'planner',
282
+ reason: args.reason,
283
+ });
284
+ const request = created?.request ?? {};
285
+ requestId = typeof request.id === 'string' ? request.id : null;
286
+ dropped = Array.isArray(created?.droppedScopes) ? created.droppedScopes : [];
287
+ connectUrl = created?.link ?? request.link ?? connectUrl;
288
+
289
+ if (requestId) {
290
+ try {
291
+ const opened = await client.openConnectionRequest(requestId);
292
+ delivered = opened?.delivered === true;
293
+ connectUrl = opened?.link ?? opened?.fallback ?? connectUrl;
294
+ } catch {
295
+ delivered = false;
296
+ }
297
+ }
298
+ } catch {
299
+ // Older backend without request persistence: fall back to the link.
300
+ }
301
+ }
302
+
303
+ const manualSteps = connectUrl
268
304
  ? [
269
305
  `1. Open ${connectUrl}`,
270
306
  ` This opens **${target.name}**'s connection panel in this app, with the scopes below and \`This app\` preselected.`,
271
307
  '2. Review the service, scopes and price shown above.',
272
- '3. Click **Connect**.',
273
- '4. Copy the **Connection ID** shown under the new connected app into the integration brief.',
308
+ '3. Click **Approve**.',
274
309
  ]
275
310
  : [
276
311
  `1. Open the app's space${spaceUrl ? `: ${spaceUrl}` : ''}.`,
277
312
  '2. Open **Aivy Actions → App Connections (Beta)** and choose the **Connect** tab.',
278
- `3. Find **${target.name}**, tick the scopes below, choose **This app**, then click **Connect**.`,
279
- '4. Copy the **Connection ID** shown under the new connected app into the integration brief.',
313
+ `3. Find **${target.name}**, tick the scopes below, choose **This app**, then click **Approve**.`,
280
314
  ];
281
315
 
282
316
  return [
283
- `## Integration approval required — nothing is created yet`,
317
+ delivered
318
+ ? '## App Connections opened — waiting for the app owner to approve'
319
+ : '## Approval needed — the app owner must approve this request',
284
320
  `- Service: **${target.name}** (\`${target.agentId}\`)`,
285
321
  `- serviceCode: ${target.serviceCode ?? 'unknown'}`,
286
322
  `- reason: ${args.reason}`,
287
323
  `- scopes: ${requested.join(', ')}`,
288
324
  `- cost: ${formatPrice(target)}`,
325
+ ...(requestId ? [`- requestId: \`${requestId}\``] : []),
326
+ ...(dropped.length
327
+ ? [`⚠️ Dropped (not declared by the target manifest): ${dropped.join(', ')}`]
328
+ : []),
289
329
  '',
290
- 'The planner cannot create or record connections. The grant exists only after the app owner clicks **Connect**; until then nothing is pending and the Permissions page stays empty.',
330
+ delivered
331
+ ? 'The App Connections panel is now open on the user’s screen, focused on this request. They only need to click **Approve** — do not paste a link or repeat manual steps.'
332
+ : 'The user’s client was not reachable. Send them this link — it opens the same focused panel so they only need to click **Approve**:',
333
+ ...(delivered ? [] : ['', ...manualSteps]),
291
334
  '',
292
- '### Approve now (creates the app-subject grant)',
293
- ...approvalSteps,
335
+ 'A pending request is not a grant. Do not write a connection id into the integration brief until `a2a_check_connections` reports an active connection.',
336
+ 'After the user approves, call `a2a_check_connections` to read the real connection id and scopes.',
294
337
  '',
295
338
  '### Connection payload (owner handoff)',
296
339
  '```json',
297
340
  JSON.stringify(connectionPayload, null, 2),
298
341
  '```',
299
342
  '',
300
- 'Only the signed-in app owner may submit this payload to `POST /api/a2a/connections` with a Clerk session. Never send credentials or create the connection from the planner.',
301
- 'If the user cannot or does not complete the approval now, write `Connection: to be created during implementation` in the integration brief and continue — do not block the plan and do not ask again.',
343
+ 'The planner can never approve. Never send credentials or create the connection from the planner.',
344
+ 'If the user cannot or does not complete the approval now, write `Connection: pending owner approval` in the integration brief and continue — do not block the plan and do not ask again.',
302
345
  ].join('\n');
303
346
  } catch (error: any) {
304
347
  return `❌ Error creating integration request: ${error.message}`;