@frontera-sdk/cli 1.50.47 → 1.50.49
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 +4 -4
- package/src/forge/adapter.ts +48 -2
- package/src/forge/claude-adapter.ts +6 -1
- package/src/forge/codex-adapter.ts +5 -0
- package/src/forge/runs.ts +11 -1
- package/src/forge/server.ts +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontera-sdk/cli",
|
|
3
|
-
"version": "1.50.
|
|
3
|
+
"version": "1.50.49",
|
|
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.
|
|
43
|
-
"@frontera-sdk/core": "1.50.
|
|
42
|
+
"@frontera-sdk/functions": "1.50.49",
|
|
43
|
+
"@frontera-sdk/core": "1.50.49",
|
|
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.
|
|
49
|
+
"@frontera-sdk/forge-contracts": "1.50.49",
|
|
50
50
|
"@types/bun": "^1.3.14",
|
|
51
51
|
"typescript": "^5.9.3"
|
|
52
52
|
}
|
package/src/forge/adapter.ts
CHANGED
|
@@ -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
|
package/src/forge/server.ts
CHANGED
|
@@ -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]: '
|
|
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]: '
|
|
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) => {
|