@meistrari/tela-cli 0.1.1 → 0.2.0

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
@@ -18,12 +18,12 @@ source ~/.zshrc
18
18
 
19
19
  ### `setup`
20
20
 
21
- Configure Tela CLI, choose an Agent API target, and verify the local environment
21
+ Configure Tela CLI, sign in with device flow, choose the workspace organization, provision Forgejo access, and verify the local environment
22
22
 
23
- When to use: Run this first on a new machine, or any time your Agent API target/origin, clone root, or key changes.
23
+ When to use: Run this first on a new machine, or any time your environment target, clone root, Tela session, or SSH key changes.
24
24
 
25
25
  Examples:
26
- - `tela setup` — Choose production, staging, or a custom API origin and run post-setup checks
26
+ - `tela setup` — Choose production, staging, or a custom environment, complete browser-based sign-in, select the workspace organization, and provision Forgejo access with an SSH key
27
27
 
28
28
  ### `sync [repo]`
29
29
 
@@ -71,7 +71,7 @@ Examples:
71
71
 
72
72
  ### `status`
73
73
 
74
- Show Tela config, tool availability, repo state, and Agent API origin reachability
74
+ Show Tela config, session state, tool availability, repo state, and Agent API reachability
75
75
 
76
76
  When to use: Use this for a quick snapshot of whether the local environment is ready before running mutating commands.
77
77
 
@@ -81,7 +81,7 @@ Examples:
81
81
 
82
82
  ### `doctor`
83
83
 
84
- Diagnose config, tools, filesystem state, repo health, and Agent API origin reachability
84
+ Diagnose config, auth session, tools, filesystem state, repo health, and Agent API reachability
85
85
 
86
86
  When to use: Use this when Tela fails or behaves unexpectedly and you need exact remediation steps.
87
87
 
@@ -95,7 +95,7 @@ Examples:
95
95
 
96
96
  - Human output is structured for terminal use with grouped summaries and next actions.
97
97
  - `--json` is available on read-heavy flows and on `tela run` for the final execution envelope.
98
- - `--silent-bark` disables bark audio during interactive Ink startup while keeping the visual intro.
98
+ - `--silent-bark` disables bark audio during the interactive `tela setup` intro while keeping the visual intro.
99
99
 
100
100
  ## First run
101
101
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tela": "cli.ts"
@@ -17,6 +17,8 @@
17
17
  "typecheck": "bunx tsc --noEmit"
18
18
  },
19
19
  "dependencies": {
20
+ "@meistrari/auth-cli": "^1.0.1",
21
+ "better-auth": "^1.6.3",
20
22
  "commander": "13.1.0",
21
23
  "ink": "6.2.3",
22
24
  "react": "19"
@@ -29,4 +31,4 @@
29
31
  "patchedDependencies": {
30
32
  "ink@6.2.3": "patches/ink@6.2.3.patch"
31
33
  }
32
- }
34
+ }
package/src/agent-api.ts CHANGED
@@ -2,8 +2,12 @@ import { buildAgentApiHeaders, type FetchLike } from './auth.ts'
2
2
  import type {
3
3
  AgentFeedbackItem,
4
4
  AgentFeedbackListResponse,
5
+ ForgejoUserCreateResponse,
6
+ ForgejoUserSshKeyInput,
7
+ OrganizationListResponse,
5
8
  RepositoryLookupResponse,
6
9
  TelaCliConfig,
10
+ TelaCliForgejoOrganization,
7
11
  } from './types.ts'
8
12
 
9
13
  const DEFAULT_TIMEOUT_MS = 10_000
@@ -32,6 +36,15 @@ function isFeedbackItem(value: unknown): value is AgentFeedbackItem {
32
36
  && typeof candidate.suggestion === 'string'
33
37
  }
34
38
 
39
+ function isForgejoOrganization(value: unknown): value is TelaCliForgejoOrganization {
40
+ if (!value || typeof value !== 'object') {
41
+ return false
42
+ }
43
+
44
+ const candidate = value as Partial<TelaCliForgejoOrganization>
45
+ return typeof candidate.id === 'string' && typeof candidate.name === 'string'
46
+ }
47
+
35
48
  export class AgentApiError extends Error {
36
49
  constructor(
37
50
  message: string,
@@ -47,11 +60,12 @@ export async function fetchRepositoryLookup(
47
60
  config: TelaCliConfig,
48
61
  fetchFn: FetchLike = fetch,
49
62
  ): Promise<Required<Pick<RepositoryLookupResponse, 'organization' | 'agentRepo' | 'forgejoRepository'>> & { success: true }> {
63
+ const headers = await buildAgentApiHeaders(config, { fetchFn })
50
64
  const response = await fetchFn(
51
65
  joinUrl(config.agentApiUrl, `/v3/agents/repositories/${encodeURIComponent(repository)}`),
52
66
  {
53
67
  method: 'GET',
54
- headers: buildAgentApiHeaders(config),
68
+ headers,
55
69
  signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
56
70
  },
57
71
  )
@@ -86,6 +100,7 @@ export async function fetchCommitFeedback(
86
100
  config: TelaCliConfig,
87
101
  fetchFn: FetchLike = fetch,
88
102
  ): Promise<AgentFeedbackListResponse> {
103
+ const headers = await buildAgentApiHeaders(config, { fetchFn })
89
104
  const response = await fetchFn(
90
105
  joinUrl(
91
106
  config.agentApiUrl,
@@ -93,7 +108,7 @@ export async function fetchCommitFeedback(
93
108
  ),
94
109
  {
95
110
  method: 'GET',
96
- headers: buildAgentApiHeaders(config),
111
+ headers,
97
112
  signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
98
113
  },
99
114
  )
@@ -122,3 +137,88 @@ export async function fetchCommitFeedback(
122
137
  feedbacks: payload.feedbacks,
123
138
  }
124
139
  }
140
+
141
+ export async function listOrganizations(
142
+ config: TelaCliConfig,
143
+ fetchFn: FetchLike = fetch,
144
+ ): Promise<TelaCliForgejoOrganization[]> {
145
+ const headers = await buildAgentApiHeaders(config, { fetchFn })
146
+ const response = await fetchFn(
147
+ joinUrl(config.agentApiUrl, '/v3/organizations'),
148
+ {
149
+ method: 'GET',
150
+ headers,
151
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
152
+ },
153
+ )
154
+
155
+ const responseText = await response.text()
156
+ const payload = parseJsonBody(responseText) as OrganizationListResponse | null
157
+
158
+ if (!response.ok) {
159
+ throw new AgentApiError(
160
+ payload?.error ?? `Agent API organization lookup failed with status ${response.status}`,
161
+ response.status,
162
+ responseText,
163
+ )
164
+ }
165
+
166
+ if (!payload
167
+ || payload.success !== true
168
+ || !Array.isArray(payload.organizations)
169
+ || !payload.organizations.every(isForgejoOrganization)) {
170
+ throw new AgentApiError('Agent API organization lookup returned an invalid payload', response.status, responseText)
171
+ }
172
+
173
+ return payload.organizations
174
+ }
175
+
176
+ export async function provisionForgejoUser(
177
+ input: {
178
+ organizationName: string
179
+ sshKey: ForgejoUserSshKeyInput
180
+ },
181
+ config: TelaCliConfig,
182
+ fetchFn: FetchLike = fetch,
183
+ ): Promise<ForgejoUserCreateResponse> {
184
+ const headers = await buildAgentApiHeaders(config, { fetchFn })
185
+ const response = await fetchFn(
186
+ joinUrl(config.agentApiUrl, '/v3/forgejo/users'),
187
+ {
188
+ method: 'POST',
189
+ headers: {
190
+ ...headers,
191
+ 'Content-Type': 'application/json',
192
+ },
193
+ body: JSON.stringify({
194
+ organizationName: input.organizationName,
195
+ sshKey: input.sshKey,
196
+ }),
197
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
198
+ },
199
+ )
200
+
201
+ const responseText = await response.text()
202
+ const payload = parseJsonBody(responseText) as ForgejoUserCreateResponse | null
203
+
204
+ if (!response.ok) {
205
+ throw new AgentApiError(
206
+ payload?.error ?? `Agent API Forgejo user provisioning failed with status ${response.status}`,
207
+ response.status,
208
+ responseText,
209
+ )
210
+ }
211
+
212
+ if (!payload
213
+ || payload.success !== true
214
+ || typeof payload.username !== 'string'
215
+ || typeof payload.organizationName !== 'string'
216
+ || typeof payload.teamName !== 'string'
217
+ || typeof payload.userCreated !== 'boolean'
218
+ || typeof payload.teamCreated !== 'boolean'
219
+ || typeof payload.sshKeyCreated !== 'boolean') {
220
+ throw new AgentApiError('Agent API Forgejo user provisioning returned an invalid payload', response.status, responseText)
221
+ }
222
+
223
+ return payload
224
+ }
@@ -0,0 +1,4 @@
1
+ declare module '*.mp3' {
2
+ const assetPath: string
3
+ export default assetPath
4
+ }
package/src/auth.ts CHANGED
@@ -1,16 +1,432 @@
1
- import type { TelaCliConfig } from './types.ts'
1
+ import {
2
+ DeviceAccessDeniedError,
3
+ DeviceCodeExpiredError,
4
+ DeviceFlow,
5
+ DeviceFlowAbortedError,
6
+ RefreshTokenExpiredError,
7
+ } from '@meistrari/auth-cli'
8
+ import {
9
+ clearSavedAuth,
10
+ isPlaceholderApplicationId,
11
+ saveConfig,
12
+ } from './config.ts'
13
+ import { createUserFacingError } from './errors.ts'
14
+ import { runCommand } from './process.ts'
15
+ import type {
16
+ TelaCliAuthState,
17
+ TelaCliConfig,
18
+ TelaCliUser,
19
+ TelaCliWorkspace,
20
+ } from './types.ts'
2
21
 
3
22
  export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>
4
23
 
5
- export function buildAuthorizationHeader(config: Pick<TelaCliConfig, 'apiKey'>): string {
6
- return `Bearer ${config.apiKey.trim()}`
24
+ type StoredTokensLike = {
25
+ accessToken: string
26
+ refreshToken: string
27
+ expiresAt: number
28
+ user: {
29
+ id: string
30
+ email: string
31
+ name?: string | null
32
+ }
33
+ organization: {
34
+ id: string
35
+ name?: string | null
36
+ title?: string | null
37
+ slug?: string | null
38
+ }
39
+ }
40
+
41
+ export interface DeviceFlowLike {
42
+ on(
43
+ eventName: 'userCode',
44
+ listener: (payload: {
45
+ userCode: string
46
+ verificationUri: string
47
+ verificationUriComplete: string
48
+ expiresIn: number
49
+ interval: number
50
+ }) => void | Promise<void>,
51
+ ): unknown
52
+ on(
53
+ eventName: 'pending',
54
+ listener: (payload: { attempt: number, elapsedMs: number }) => void | Promise<void>,
55
+ ): unknown
56
+ on(
57
+ eventName: 'slowDown',
58
+ listener: (payload: { previousInterval: number, newInterval: number }) => void | Promise<void>,
59
+ ): unknown
60
+ on(
61
+ eventName: 'transientError',
62
+ listener: (payload: { attempt: number }) => void | Promise<void>,
63
+ ): unknown
64
+ authenticate(): Promise<StoredTokensLike>
65
+ refresh(refreshToken?: string): Promise<StoredTokensLike>
66
+ }
67
+
68
+ export type DeviceFlowFactory = (input: {
69
+ apiUrl: string
70
+ requesterApplicationId: string
71
+ targetApplicationId: string
72
+ signal?: AbortSignal
73
+ }) => DeviceFlowLike
74
+
75
+ export interface SetupAuthEvent {
76
+ type: 'message' | 'user_code' | 'browser_opened' | 'browser_open_failed' | 'success'
77
+ message: string
78
+ title?: string
79
+ verificationUrl?: string
80
+ userCode?: string
81
+ expiresInSeconds?: number
82
+ }
83
+
84
+ export interface AuthenticateWithDeviceFlowOptions {
85
+ createDeviceFlow?: DeviceFlowFactory
86
+ openBrowser?: (url: string) => Promise<boolean>
87
+ timeoutMs?: number
88
+ onEvent?: (event: SetupAuthEvent) => void
89
+ }
90
+
91
+ export interface BuildAgentApiHeadersOptions {
92
+ clearAuth?: (config: TelaCliConfig) => TelaCliConfig
93
+ createDeviceFlow?: DeviceFlowFactory
94
+ fetchFn?: FetchLike
95
+ nowMs?: number
96
+ persistConfig?: (config: TelaCliConfig) => void
97
+ }
98
+
99
+ function defaultDeviceFlowFactory(input: ConstructorParameters<typeof DeviceFlow>[0]): DeviceFlowLike {
100
+ return new DeviceFlow(input)
101
+ }
102
+
103
+ function createAuthRequiredError(summary: string): ReturnType<typeof createUserFacingError> {
104
+ return createUserFacingError({
105
+ code: 'auth_required',
106
+ summary,
107
+ fixes: ['Run `tela setup` in an interactive terminal to sign in again.'],
108
+ })
109
+ }
110
+
111
+ function mapUser(user: StoredTokensLike['user']): TelaCliUser {
112
+ return {
113
+ id: user.id,
114
+ email: user.email,
115
+ name: user.name?.trim() || user.email,
116
+ }
117
+ }
118
+
119
+ function mapWorkspace(organization: StoredTokensLike['organization']): TelaCliWorkspace {
120
+ return {
121
+ id: organization.id,
122
+ title: organization.name?.trim()
123
+ || organization.title?.trim()
124
+ || organization.slug?.trim()
125
+ || organization.id,
126
+ }
127
+ }
128
+
129
+ function mapTokens(tokens: StoredTokensLike): TelaCliAuthState {
130
+ return {
131
+ user: mapUser(tokens.user),
132
+ workspace: mapWorkspace(tokens.organization),
133
+ session: {
134
+ accessToken: tokens.accessToken,
135
+ refreshToken: tokens.refreshToken,
136
+ expiresAt: tokens.expiresAt,
137
+ },
138
+ }
139
+ }
140
+
141
+ function buildDashboardVerificationUrl(dashboardUrl: string, userCode: string, fallbackUrl: string): string {
142
+ try {
143
+ const url = new URL('/applications/device', dashboardUrl)
144
+ url.searchParams.set('user_code', userCode)
145
+ return url.toString()
146
+ }
147
+ catch {
148
+ return fallbackUrl
149
+ }
150
+ }
151
+
152
+ async function defaultOpenBrowser(url: string): Promise<boolean> {
153
+ const command = (() => {
154
+ switch (process.platform) {
155
+ case 'darwin':
156
+ return ['open', url]
157
+ case 'win32':
158
+ return ['cmd', '/c', 'start', '', url]
159
+ default:
160
+ return ['xdg-open', url]
161
+ }
162
+ })()
163
+
164
+ try {
165
+ const result = await runCommand(command, { allowFailure: true })
166
+ return result.exitCode === 0
167
+ }
168
+ catch {
169
+ return false
170
+ }
7
171
  }
8
172
 
9
- export function buildAgentApiHeaders(
10
- config: Pick<TelaCliConfig, 'apiKey'>,
11
- ): Record<string, string> {
173
+ function ensurePresetApplicationIds(input: {
174
+ environment: TelaCliConfig['environment']
175
+ requesterApplicationId: string
176
+ targetApplicationId: string
177
+ }): void {
178
+ if (input.environment === 'custom') {
179
+ return
180
+ }
181
+
182
+ if (!isPlaceholderApplicationId(input.requesterApplicationId) && !isPlaceholderApplicationId(input.targetApplicationId)) {
183
+ return
184
+ }
185
+
186
+ throw createUserFacingError({
187
+ code: 'setup_preset_incomplete',
188
+ summary: `The ${input.environment} auth preset is missing the real Tela CLI application IDs.`,
189
+ fixes: [
190
+ 'Update the production/staging requester and target application IDs in `src/config.ts`, or use the custom setup target.',
191
+ ],
192
+ })
193
+ }
194
+
195
+ export async function authenticateWithDeviceFlow(
196
+ input: Pick<TelaCliConfig, 'environment' | 'authApiUrl' | 'authDashboardUrl' | 'requesterApplicationId' | 'targetApplicationId'>,
197
+ options: AuthenticateWithDeviceFlowOptions = {},
198
+ ): Promise<TelaCliAuthState> {
199
+ ensurePresetApplicationIds(input)
200
+
201
+ const timeoutMs = options.timeoutMs ?? (15 * 60 * 1000)
202
+ const signal = AbortSignal.timeout(timeoutMs)
203
+ const createDeviceFlow = options.createDeviceFlow ?? defaultDeviceFlowFactory
204
+ const openBrowser = options.openBrowser ?? defaultOpenBrowser
205
+ const emit = (event: SetupAuthEvent) => {
206
+ options.onEvent?.(event)
207
+ }
208
+
209
+ const flow = createDeviceFlow({
210
+ apiUrl: input.authApiUrl,
211
+ requesterApplicationId: input.requesterApplicationId,
212
+ targetApplicationId: input.targetApplicationId,
213
+ signal,
214
+ })
215
+
216
+ flow.on('userCode', async ({ userCode, verificationUriComplete, expiresIn }) => {
217
+ const verificationUrl = buildDashboardVerificationUrl(
218
+ input.authDashboardUrl,
219
+ userCode,
220
+ verificationUriComplete,
221
+ )
222
+
223
+ emit({
224
+ type: 'user_code',
225
+ title: 'Authentication',
226
+ message: `Open ${verificationUrl}\nUser code: ${userCode}\nCode expires in ${expiresIn}s`,
227
+ verificationUrl,
228
+ userCode,
229
+ expiresInSeconds: expiresIn,
230
+ })
231
+
232
+ const opened = await openBrowser(verificationUrl)
233
+ emit(opened
234
+ ? {
235
+ type: 'browser_opened',
236
+ title: 'Authentication',
237
+ message: 'Opened the verification URL in your browser.',
238
+ verificationUrl,
239
+ }
240
+ : {
241
+ type: 'browser_open_failed',
242
+ title: 'Authentication',
243
+ message: 'Could not open the browser automatically. Open the verification URL manually.',
244
+ verificationUrl,
245
+ })
246
+ })
247
+
248
+ flow.on('pending', ({ attempt }) => {
249
+ emit({
250
+ type: 'message',
251
+ title: 'Authentication',
252
+ message: `[poll ${attempt}] Waiting for approval...`,
253
+ })
254
+ })
255
+
256
+ flow.on('slowDown', ({ newInterval }) => {
257
+ emit({
258
+ type: 'message',
259
+ title: 'Authentication',
260
+ message: `Server requested a slower polling interval (${newInterval}s).`,
261
+ })
262
+ })
263
+
264
+ flow.on('transientError', ({ attempt }) => {
265
+ emit({
266
+ type: 'message',
267
+ title: 'Authentication',
268
+ message: `[poll ${attempt}] Temporary auth server error. Retrying...`,
269
+ })
270
+ })
271
+
272
+ try {
273
+ const tokens = await flow.authenticate()
274
+ const auth = mapTokens(tokens)
275
+ emit({
276
+ type: 'success',
277
+ title: 'Authentication',
278
+ message: `Signed in as ${auth.user.email} for workspace ${auth.workspace.title}.`,
279
+ })
280
+ return auth
281
+ }
282
+ catch (error) {
283
+ if (error instanceof DeviceAccessDeniedError) {
284
+ throw createUserFacingError({
285
+ code: 'device_access_denied',
286
+ summary: 'The device authorization request was denied.',
287
+ fixes: ['Run `tela setup` again and approve the request in the browser.'],
288
+ })
289
+ }
290
+
291
+ if (error instanceof DeviceCodeExpiredError) {
292
+ throw createUserFacingError({
293
+ code: 'device_code_expired',
294
+ summary: 'The device authorization expired before approval completed.',
295
+ fixes: ['Run `tela setup` again and approve the request before the code expires.'],
296
+ })
297
+ }
298
+
299
+ if (error instanceof DeviceFlowAbortedError) {
300
+ throw createUserFacingError({
301
+ code: 'device_flow_timeout',
302
+ summary: `Timed out waiting for device authorization after ${Math.round(timeoutMs / 60_000)} minutes.`,
303
+ fixes: ['Run `tela setup` again and complete the browser approval before the timeout.'],
304
+ })
305
+ }
306
+
307
+ throw error
308
+ }
309
+ }
310
+
311
+ async function refreshAuthState(
312
+ config: TelaCliConfig,
313
+ options: BuildAgentApiHeadersOptions,
314
+ ): Promise<TelaCliAuthState> {
315
+ if (!config.auth?.session.refreshToken) {
316
+ throw createAuthRequiredError('Tela CLI is not authenticated.')
317
+ }
318
+
319
+ const createDeviceFlow = options.createDeviceFlow ?? defaultDeviceFlowFactory
320
+ const flow = createDeviceFlow({
321
+ apiUrl: config.authApiUrl,
322
+ requesterApplicationId: config.requesterApplicationId,
323
+ targetApplicationId: config.targetApplicationId,
324
+ signal: AbortSignal.timeout(10_000),
325
+ })
326
+
327
+ try {
328
+ const refreshed = await flow.refresh(config.auth.session.refreshToken)
329
+ const nextAuth = mapTokens(refreshed)
330
+ const nextConfig: TelaCliConfig = {
331
+ ...config,
332
+ auth: nextAuth,
333
+ }
334
+ ;(options.persistConfig ?? saveConfig)(nextConfig)
335
+ config.auth = nextAuth
336
+ return nextAuth
337
+ }
338
+ catch (error) {
339
+ if (error instanceof RefreshTokenExpiredError) {
340
+ ;(options.clearAuth ?? clearSavedAuth)(config)
341
+ config.auth = null
342
+ throw createAuthRequiredError('The saved Tela session expired and must be reauthenticated.')
343
+ }
344
+
345
+ throw error
346
+ }
347
+ }
348
+
349
+ async function getValidAccessToken(
350
+ config: TelaCliConfig,
351
+ options: BuildAgentApiHeadersOptions = {},
352
+ ): Promise<string> {
353
+ if (!config.auth?.session.accessToken) {
354
+ throw createAuthRequiredError('Tela CLI is not authenticated.')
355
+ }
356
+
357
+ const nowMs = options.nowMs ?? Date.now()
358
+ if (config.auth.session.expiresAt > nowMs + 30_000) {
359
+ return config.auth.session.accessToken
360
+ }
361
+
362
+ const nextAuth = await refreshAuthState(config, options)
363
+ return nextAuth.session.accessToken
364
+ }
365
+
366
+ type DataTokenResponse = {
367
+ dataToken?: string
368
+ error?: string
369
+ }
370
+
371
+ async function exchangeDataToken(
372
+ config: TelaCliConfig,
373
+ accessToken: string,
374
+ fetchFn: FetchLike,
375
+ ): Promise<string> {
376
+ const dataTokenUrl = new URL('/data-token', config.authApiUrl)
377
+ dataTokenUrl.searchParams.set('expiresAt', '1d')
378
+
379
+ const response = await fetchFn(dataTokenUrl.toString(), {
380
+ method: 'GET',
381
+ headers: {
382
+ Authorization: `Bearer ${accessToken}`,
383
+ Accept: 'application/json',
384
+ },
385
+ signal: AbortSignal.timeout(10_000),
386
+ })
387
+
388
+ const responseText = await response.text()
389
+ let payload: DataTokenResponse | null = null
390
+ try {
391
+ payload = responseText ? JSON.parse(responseText) as DataTokenResponse : null
392
+ }
393
+ catch {
394
+ payload = null
395
+ }
396
+
397
+ if (!response.ok) {
398
+ throw createUserFacingError({
399
+ code: 'data_token_exchange_failed',
400
+ summary: payload?.error ?? `Tela data-token exchange failed with status ${response.status}.`,
401
+ fixes: ['Run `tela setup` again if the saved session is stale.'],
402
+ evidence: responseText ? [responseText] : [],
403
+ })
404
+ }
405
+
406
+ const headerToken = response.headers.get('x-data-token')?.trim()
407
+ const dataToken = headerToken || payload?.dataToken?.trim()
408
+ if (!dataToken) {
409
+ throw createUserFacingError({
410
+ code: 'data_token_exchange_invalid',
411
+ summary: 'Tela data-token exchange returned an invalid response.',
412
+ fixes: ['Run `tela setup` again if the saved session is stale.'],
413
+ evidence: responseText ? [responseText] : [],
414
+ })
415
+ }
416
+
417
+ return dataToken
418
+ }
419
+
420
+ export async function buildAgentApiHeaders(
421
+ config: TelaCliConfig,
422
+ options: BuildAgentApiHeadersOptions = {},
423
+ ): Promise<Record<string, string>> {
424
+ const fetchFn = options.fetchFn ?? fetch
425
+ const accessToken = await getValidAccessToken(config, options)
426
+ const dataToken = await exchangeDataToken(config, accessToken, fetchFn)
427
+
12
428
  return {
13
- Authorization: buildAuthorizationHeader(config),
429
+ 'x-data-token': dataToken,
14
430
  Accept: 'application/json',
15
431
  }
16
432
  }
@@ -540,7 +540,7 @@ export async function runFeedbackCommand(options: RunFeedbackCommandOptions): Pr
540
540
  const runInteractiveCommandFn = options.runInteractiveCommand ?? runInteractiveCommand
541
541
  const selectionMode = getFeedbackSelectionMode(options)
542
542
 
543
- const config: TelaCliConfig = await ensureConfig({ prompts })
543
+ const config: TelaCliConfig = await ensureConfig()
544
544
  const data = await loadFeedbackData({
545
545
  cwd: repoRoot,
546
546
  config,