@frontera-sdk/cli 1.50.47 → 1.50.48

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontera-sdk/cli",
3
- "version": "1.50.47",
3
+ "version": "1.50.48",
4
4
  "description": "The frontera CLI — scaffold, pull, save and deploy Frontera apps and automations.",
5
5
  "keywords": [
6
6
  "frontera",
@@ -39,14 +39,14 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@anthropic-ai/claude-agent-sdk": "^0.3.251",
42
- "@frontera-sdk/functions": "1.50.47",
43
- "@frontera-sdk/core": "1.50.47",
42
+ "@frontera-sdk/functions": "1.50.48",
43
+ "@frontera-sdk/core": "1.50.48",
44
44
  "ai": "^6.0.116",
45
45
  "gray-matter": "^4.0.3",
46
46
  "yaml": "^2.9.0"
47
47
  },
48
48
  "devDependencies": {
49
- "@frontera-sdk/forge-contracts": "1.50.47",
49
+ "@frontera-sdk/forge-contracts": "1.50.48",
50
50
  "@types/bun": "^1.3.14",
51
51
  "typescript": "^5.9.3"
52
52
  }
@@ -1,4 +1,4 @@
1
- import type { ForgeHostId, ForgeProject, ForgeThread, ForgeThreadStatus } from '@frontera-sdk/forge-contracts'
1
+ import type { ForgeHostId, ForgePendingApproval, ForgeProject, ForgeThread, ForgeThreadStatus } from '@frontera-sdk/forge-contracts'
2
2
  import type { UIMessageChunk } from 'ai'
3
3
 
4
4
  /**
@@ -44,6 +44,8 @@ export interface HostAdapter {
44
44
  interrupt(): Promise<void>
45
45
  /** Answer a pending approval; false when the id is unknown to this adapter. */
46
46
  respondApproval(approvalId: string, decision: ApprovalDecision): boolean
47
+ /** The prompts still waiting on this adapter, oldest first. */
48
+ pendingApprovals(): ForgePendingApproval[]
47
49
  }
48
50
 
49
51
  export type AdapterFactory = (host: ForgeHostId) => HostAdapter
@@ -54,6 +56,27 @@ export const APPROVAL_TIMEOUT_MS = 10 * 60 * 1000
54
56
  interface PendingApproval {
55
57
  resolve(decision: ApprovalDecision): void
56
58
  timer: ReturnType<typeof setTimeout>
59
+ card: ForgePendingApproval
60
+ }
61
+
62
+ /** How much of the tool's input the card shows, and how far each value runs. */
63
+ const PREVIEW_ITEMS = 6
64
+ const PREVIEW_LENGTH = 160
65
+
66
+ /**
67
+ * The tool's arguments as card rows. Unlike the platform's own builder this
68
+ * does not redact secret-looking keys: the daemon is loopback-only and the
69
+ * reader is the person whose machine is about to run the call with exactly
70
+ * these arguments, so hiding them would only hide what is being approved.
71
+ */
72
+ function approvalPreview(input: unknown): Array<{ label: string; value: string }> {
73
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return []
74
+ return Object.entries(input as Record<string, unknown>)
75
+ .slice(0, PREVIEW_ITEMS)
76
+ .map(([label, value]) => {
77
+ const text = typeof value === 'string' ? value : JSON.stringify(value) ?? String(value)
78
+ return { label, value: text.length > PREVIEW_LENGTH ? `${text.slice(0, PREVIEW_LENGTH)}...` : text }
79
+ })
57
80
  }
58
81
 
59
82
  /**
@@ -68,9 +91,28 @@ export class ApprovalBroker {
68
91
  ctx: Pick<TurnContext, 'setThreadStatus' | 'signal'>,
69
92
  emit: Emit,
70
93
  toolCallId: string,
94
+ toolName: string,
95
+ input: unknown,
71
96
  timeoutMs = APPROVAL_TIMEOUT_MS,
72
97
  ): Promise<ApprovalDecision> {
73
98
  const approvalId = crypto.randomUUID()
99
+ const preview = approvalPreview(input)
100
+ const card: ForgePendingApproval = {
101
+ id: approvalId,
102
+ status: 'pending',
103
+ pauseReason: 'needs_approval',
104
+ installId: null,
105
+ toolCallId,
106
+ toolName,
107
+ toolSource: 'forge',
108
+ risk: null,
109
+ actionSummary: {
110
+ title: toolName.replaceAll('__', ' / ').replaceAll('_', ' '),
111
+ ...(preview.length ? { preview } : {}),
112
+ },
113
+ policyReason: null,
114
+ expiresAt: new Date(Date.now() + timeoutMs).toISOString(),
115
+ }
74
116
  return new Promise<ApprovalDecision>((resolve) => {
75
117
  const settle = (decision: ApprovalDecision) => {
76
118
  const entry = this.pending.get(approvalId)
@@ -83,7 +125,7 @@ export class ApprovalBroker {
83
125
  }
84
126
  const onAbort = () => settle('deny')
85
127
  const timer = setTimeout(() => settle('deny'), timeoutMs)
86
- this.pending.set(approvalId, { resolve: settle, timer })
128
+ this.pending.set(approvalId, { resolve: settle, timer, card })
87
129
  ctx.signal.addEventListener('abort', onAbort, { once: true })
88
130
  ctx.setThreadStatus('awaiting_approval')
89
131
  emit({ type: 'tool-approval-request', approvalId, toolCallId } as UIMessageChunk)
@@ -98,6 +140,10 @@ export class ApprovalBroker {
98
140
  return true
99
141
  }
100
142
 
143
+ cards(): ForgePendingApproval[] {
144
+ return [...this.pending.values()].map((entry) => entry.card)
145
+ }
146
+
101
147
  denyAll(): void {
102
148
  for (const id of [...this.pending.keys()]) this.respond(id, 'deny')
103
149
  }
@@ -1,4 +1,5 @@
1
1
  import type { CanUseTool, Options, PermissionMode, Query } from '@anthropic-ai/claude-agent-sdk'
2
+ import type { ForgePendingApproval } from '@frontera-sdk/forge-contracts'
2
3
 
3
4
  import { resolveCredential } from '../config'
4
5
  import { ApprovalBroker, type ApprovalDecision, type Emit, type HostAdapter, type TurnContext, type TurnOutcome } from './adapter'
@@ -97,7 +98,7 @@ export class ClaudeAdapter implements HostAdapter {
97
98
  if (autoAllows(mode, toolName)) return { behavior: 'allow', updatedInput: input }
98
99
  const toolCallId = options.toolUseID ?? crypto.randomUUID()
99
100
  flush(translator.ensureToolCall(toolCallId, toolName, input))
100
- const decision = await this.approvals.request(ctx, emit, toolCallId)
101
+ const decision = await this.approvals.request(ctx, emit, toolCallId, toolName, input)
101
102
  if (decision === 'approve') return { behavior: 'allow', updatedInput: input }
102
103
  return { behavior: 'deny', message: DENIED_MESSAGE }
103
104
  }
@@ -175,6 +176,10 @@ export class ClaudeAdapter implements HostAdapter {
175
176
  return this.approvals.respond(approvalId, decision)
176
177
  }
177
178
 
179
+ pendingApprovals(): ForgePendingApproval[] {
180
+ return this.approvals.cards()
181
+ }
182
+
178
183
  /**
179
184
  * The platform's MCP server, with the key the project directory selects.
180
185
  * No profile bound, or a profile for another deployment: the host still
@@ -1,3 +1,4 @@
1
+ import type { ForgePendingApproval } from '@frontera-sdk/forge-contracts'
1
2
  import type { UIMessageChunk } from 'ai'
2
3
 
3
4
  import type { ApprovalDecision, Emit, HostAdapter, TurnContext, TurnOutcome } from './adapter'
@@ -25,4 +26,8 @@ export class CodexAdapter implements HostAdapter {
25
26
  respondApproval(_approvalId: string, _decision: ApprovalDecision): boolean {
26
27
  return false
27
28
  }
29
+
30
+ pendingApprovals(): ForgePendingApproval[] {
31
+ return []
32
+ }
28
33
  }
package/src/forge/runs.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ForgeProject } from '@frontera-sdk/forge-contracts'
1
+ import type { ForgePendingApproval, ForgeProject } from '@frontera-sdk/forge-contracts'
2
2
 
3
3
  import { FORGE_RUN_ID_PREFIX } from './protocol'
4
4
  import { readUIMessageStream, type UIMessage, type UIMessageChunk } from 'ai'
@@ -141,6 +141,16 @@ export class RunManager {
141
141
  return true
142
142
  }
143
143
 
144
+ /** Everything this thread's live run is parked on, for the approval rail. */
145
+ pendingApprovals(threadId: string): ForgePendingApproval[] {
146
+ const cards: ForgePendingApproval[] = []
147
+ for (const run of this.live.values()) {
148
+ if (run.threadId !== threadId || run.finished) continue
149
+ cards.push(...run.adapter.pendingApprovals())
150
+ }
151
+ return cards
152
+ }
153
+
144
154
  respondApproval(threadId: string, approvalId: string, decision: ApprovalDecision): boolean {
145
155
  for (const run of this.live.values()) {
146
156
  if (run.threadId !== threadId || run.finished) continue
@@ -513,7 +513,7 @@ export function startForgeServer(opts: ForgeServerOptions): ForgeServer {
513
513
  return sseResponse(stream, {
514
514
  [FORGE_HEADERS.conversationId]: thread.id,
515
515
  [FORGE_HEADERS.runId]: runId,
516
- [FORGE_HEADERS.agentRuntime]: 'wdk',
516
+ [FORGE_HEADERS.agentRuntime]: 'pi',
517
517
  })
518
518
  })
519
519
 
@@ -531,7 +531,7 @@ export function startForgeServer(opts: ForgeServerOptions): ForgeServer {
531
531
  return sseResponse(stream, {
532
532
  ...(run ? { [FORGE_HEADERS.conversationId]: run.threadId } : {}),
533
533
  [FORGE_HEADERS.runId]: runId,
534
- [FORGE_HEADERS.agentRuntime]: 'wdk',
534
+ [FORGE_HEADERS.agentRuntime]: 'pi',
535
535
  })
536
536
  })
537
537
 
@@ -584,7 +584,7 @@ export function startForgeServer(opts: ForgeServerOptions): ForgeServer {
584
584
 
585
585
  route('GET', '/v1/sessions/:id/pending', (_req, params) => {
586
586
  requireThread(params.id!)
587
- return ok([])
587
+ return ok(runs.pendingApprovals(params.id!))
588
588
  })
589
589
 
590
590
  route('POST', '/v1/sessions/:id/approvals/:approvalId', async (req, params) => {