@shawnstack/quickforge 1.7.5 → 1.7.7

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.
Files changed (67) hide show
  1. package/dist/assets/AgentProfilesPage-BfA_sQ3A.js +1 -0
  2. package/dist/assets/ChatPanelHost-Dul1ahKj.js +307 -0
  3. package/dist/assets/CloudAccountSettingsPage-qZh0lTUN.js +1 -0
  4. package/dist/assets/PluginsPage-CPE5tbbf.js +1 -0
  5. package/dist/assets/ScheduledTasksPage-PTxL_PPg.js +2 -0
  6. package/dist/assets/SettingsWorkspacePage-BE2T4d3P.js +1696 -0
  7. package/dist/assets/ShareLinksSettingsPage-Bl8fP62X.js +1 -0
  8. package/dist/assets/SharedConversationPage-BK9XJnEe.js +1 -0
  9. package/dist/assets/TerminalDock-CL7HU9DI.js +2 -0
  10. package/dist/assets/{WorkspaceInspector-D9kdnLDd.js → WorkspaceInspector-MNBLIKMn.js} +5 -5
  11. package/dist/assets/{dist-DLKC_odd.js → dist-yDHbi5ux.js} +1 -1
  12. package/dist/assets/{esm-Df4edDpj.js → esm-sdsK6gaB.js} +2 -2
  13. package/dist/assets/{icons-CXPP1fFT.js → icons-2D18YbSI.js} +1 -1
  14. package/dist/assets/index-BTPzyq7d.js +65 -0
  15. package/dist/assets/index-BiCTgUlY.css +3 -0
  16. package/dist/assets/{mcp-servers-dialog-BUSPvaqI.js → mcp-servers-dialog-GY7sdfIr.js} +2 -2
  17. package/dist/assets/{logger-8REuLPGK.js → model-display-label-C6-0eDxF.js} +1 -1
  18. package/dist/assets/{monaco-D6OhXgjg.js → monaco-DvXErxp2.js} +1 -1
  19. package/dist/assets/{react-vendor-1KC5PyRx.js → react-vendor-3UaA-C9W.js} +1 -1
  20. package/dist/assets/{skills-dialog-BThnIUBs.js → skills-dialog-C1fJ1Jl7.js} +3 -3
  21. package/dist/assets/{web-DoV8MSMn.js → web-B5qgZJRv.js} +1 -1
  22. package/dist/index.html +8 -8
  23. package/dist/sw.js +17 -0
  24. package/package.json +2 -1
  25. package/runtime-assets/agent/darwin-arm64/qf-agent +0 -0
  26. package/runtime-assets/agent/darwin-x64/qf-agent +0 -0
  27. package/runtime-assets/agent/linux-arm64/qf-agent +0 -0
  28. package/runtime-assets/agent/linux-x64/qf-agent +0 -0
  29. package/runtime-assets/agent/win32-x64/qf-agent.exe +0 -0
  30. package/server/access-policy.mjs +11 -0
  31. package/server/agent-manager.mjs +349 -48
  32. package/server/agent-profiles.mjs +27 -8
  33. package/server/auto-compaction.mjs +78 -9
  34. package/server/cloud/client.mjs +4 -7
  35. package/server/cloud/config.mjs +9 -16
  36. package/server/cloud/credential-store.mjs +10 -3
  37. package/server/cloud/identity.mjs +17 -60
  38. package/server/cloud/models.mjs +1 -1
  39. package/server/cloud/qf-agent-process.mjs +571 -0
  40. package/server/cloud/runtime.mjs +11 -5
  41. package/server/cloud/service-config.mjs +8 -4
  42. package/server/context-usage.mjs +11 -4
  43. package/server/index.mjs +96 -37
  44. package/server/mcp/registry.mjs +8 -7
  45. package/server/message-converters.mjs +40 -19
  46. package/server/model-catalog.mjs +11 -4
  47. package/server/opencode-acp-agent.mjs +1151 -0
  48. package/server/public-api.mjs +11 -5
  49. package/server/routes/agent-profiles.mjs +6 -3
  50. package/server/routes/agent.mjs +43 -3
  51. package/server/routes/backup.mjs +5 -4
  52. package/server/routes/cloud.mjs +54 -35
  53. package/server/routes/shares.mjs +7 -8
  54. package/server/routes/storage.mjs +9 -24
  55. package/server/routes/system.mjs +7 -4
  56. package/server/utils/process-tree.mjs +78 -0
  57. package/dist/assets/AgentProfilesPage-XNjuqJs7.js +0 -1
  58. package/dist/assets/ChatPanelHost-CuF-PZpc.js +0 -292
  59. package/dist/assets/CloudAccountSettingsPage-GJSoi6sO.js +0 -1
  60. package/dist/assets/PluginsPage-BQgp0qN-.js +0 -1
  61. package/dist/assets/ScheduledTasksPage-DdekATzq.js +0 -2
  62. package/dist/assets/SettingsWorkspacePage-Cb54Ic91.js +0 -1639
  63. package/dist/assets/ShareLinksSettingsPage-C75WJpaF.js +0 -1
  64. package/dist/assets/SharedConversationPage-CjMtSQTW.js +0 -1
  65. package/dist/assets/TerminalDock-D4zJBJc4.js +0 -2
  66. package/dist/assets/index-_ZhK6bvp.js +0 -65
  67. package/dist/assets/index-iVV2bP_f.css +0 -3
@@ -75,7 +75,7 @@ function builtinProfileFromSubagent(definition, override = null) {
75
75
  allowedTools: [...definition.allowedTools],
76
76
  capabilityPolicy: definition.capabilityPolicy || inferCapabilityPolicy(definition.allowedTools),
77
77
  model: modelReferenceSnapshot(override?.model || definition.model),
78
- thinkingLevel: 'inherit',
78
+ thinkingLevel: normalizeAgentProfileThinkingLevel(override?.thinkingLevel || 'inherit'),
79
79
  lifecycle: 'builtin',
80
80
  managed: true,
81
81
  maxRuntimeMs: definition.maxRuntimeMs || DEFAULT_MAX_RUNTIME_MS,
@@ -308,17 +308,36 @@ export async function updateCustomAgentProfile(id, patch) {
308
308
  return { ...next, filePath: file, relativePath: `~/.quickforge/agents/${next.name}.md` }
309
309
  }
310
310
 
311
- export async function updateBuiltinAgentModelOverride(id, modelInput) {
311
+ export async function updateBuiltinAgentOverrides(id, patch = {}) {
312
312
  const current = await getAgentProfile(id)
313
313
  if (!current) throw requestError('Agent not found', 404)
314
- if (!current.builtin) throw requestError('Only built-in agents support model overrides', 400)
315
- const model = validateModelReference(normalizeModelReference(modelInput))
314
+ if (!current.builtin) throw requestError('Only built-in agents support overrides', 400)
316
315
  const overrides = await readStore(BUILTIN_OVERRIDES_STORE).catch(() => ({}))
317
- const next = { ...(overrides && typeof overrides === 'object' ? overrides : {}) }
318
- if (model.mode === 'fixed') next[current.name] = { model }
319
- else delete next[current.name]
316
+ const base = overrides && typeof overrides === 'object' ? overrides : {}
317
+ const next = { ...base }
318
+ const entry = { ...(base[current.name] && typeof base[current.name] === 'object' ? base[current.name] : {}) }
319
+
320
+ if (Object.prototype.hasOwnProperty.call(patch, 'model')) {
321
+ const model = validateModelReference(normalizeModelReference(patch.model))
322
+ if (model.mode === 'fixed') entry.model = model
323
+ else delete entry.model
324
+ }
325
+ if (Object.prototype.hasOwnProperty.call(patch, 'thinkingLevel')) {
326
+ const thinkingLevel = normalizeAgentProfileThinkingLevel(patch.thinkingLevel)
327
+ if (thinkingLevel === 'inherit') delete entry.thinkingLevel
328
+ else entry.thinkingLevel = thinkingLevel
329
+ }
330
+
331
+ if (Object.keys(entry).length === 0) delete next[current.name]
332
+ else next[current.name] = entry
320
333
  await writeStore(BUILTIN_OVERRIDES_STORE, next)
321
- return { ...current, model: modelReferenceSnapshot(model) }
334
+
335
+ const definition = subagentDefinitions.find((item) => item.name === current.name)
336
+ return builtinProfileFromSubagent(definition, next[current.name] || null)
337
+ }
338
+
339
+ export async function updateBuiltinAgentModelOverride(id, modelInput) {
340
+ return updateBuiltinAgentOverrides(id, { model: modelInput })
322
341
  }
323
342
 
324
343
  export async function deleteCustomAgentProfile(id) {
@@ -18,6 +18,14 @@ export const DEFAULT_AUTO_COMPACT_SETTINGS = {
18
18
  requireConfirmation: true,
19
19
  }
20
20
 
21
+ /**
22
+ * 保留尾部(最近回合)的字符预算上限。当会话以工具消息为主、user 回合
23
+ * 稀疏时,按 keepRecentTurns 从尾部回溯会跨越大量工具消息,导致压缩
24
+ * 范围过小甚至为空(tailStart 与 compactedUpToIndex 重合)。达到该预算
25
+ * 时按当前用户轮次起点截断,将更早的完整工具链历史并入压缩范围。
26
+ */
27
+ export const DEFAULT_MAX_TAIL_CHARS = 40000
28
+
21
29
  const AUTO_COMPACT_MIN_INTERVAL_MS = 30_000
22
30
  const AUTO_COMPACT_REJECTION_SUPPRESS_MS = 10 * 60_000
23
31
 
@@ -64,11 +72,13 @@ function contentToText(content) {
64
72
  }).filter(Boolean).join('\n')
65
73
  }
66
74
 
75
+ function messageChars(message) {
76
+ if (!message || typeof message !== 'object') return 0
77
+ return [message.role || '', contentToText(message.content), safeJson(message.attachments)].join('\n').length
78
+ }
79
+
67
80
  function estimateMessagesChars(messages) {
68
- return (Array.isArray(messages) ? messages : []).reduce((total, message) => {
69
- if (!message || typeof message !== 'object') return total
70
- return total + [message.role || '', contentToText(message.content), safeJson(message.attachments)].join('\n').length
71
- }, 0)
81
+ return (Array.isArray(messages) ? messages : []).reduce((total, message) => total + messageChars(message), 0)
72
82
  }
73
83
 
74
84
  function isUserMessage(message) {
@@ -76,13 +86,26 @@ function isUserMessage(message) {
76
86
  && !isCompactSummaryMessage(message)
77
87
  }
78
88
 
79
- function tailStartForRecentTurns(messages, keepRecentTurns) {
89
+ function isToolResultMessage(message) {
90
+ return message?.role === 'toolResult'
91
+ }
92
+
93
+ export function tailStartForRecentTurns(messages, keepRecentTurns, maxTailChars = DEFAULT_MAX_TAIL_CHARS) {
80
94
  const source = Array.isArray(messages) ? messages : []
81
95
  let seenUserTurns = 0
96
+ let tailChars = 0
82
97
  for (let index = source.length - 1; index >= 0; index--) {
83
- if (!isUserMessage(source[index])) continue
84
- seenUserTurns += 1
98
+ if (isUserMessage(source[index])) seenUserTurns += 1
99
+ tailChars += messageChars(source[index])
85
100
  if (seenUserTurns >= keepRecentTurns) return index
101
+ // 保留的尾部超过预算:仍按完整用户轮次切分,避免把当前运行中的
102
+ // assistant/toolResult 链从中间截断,导致压缩线落在一轮对话内部。
103
+ if (tailChars >= maxTailChars) {
104
+ for (let turnStart = index; turnStart >= 0; turnStart--) {
105
+ if (isUserMessage(source[turnStart])) return turnStart
106
+ }
107
+ return 0
108
+ }
86
109
  }
87
110
  return 0
88
111
  }
@@ -145,7 +168,51 @@ export function buildAutoCompactLoopMessages(session, messages) {
145
168
  if (!summaryMessage) return messages
146
169
  const source = Array.isArray(messages) ? messages : []
147
170
  const compactedUpToIndex = Math.min(source.length, Math.max(0, Number(session.contextCompaction?.compactedUpToIndex) || 0))
148
- return [summaryMessage, ...source.slice(compactedUpToIndex)]
171
+ // 防御旧持久化数据:compactedUpToIndex 可能指向 toolResult,跳过开头的孤儿,
172
+ // 避免压缩后的消息序列以 tool 消息开头(LLM API 会拒绝孤立 tool 消息)
173
+ let tailStart = compactedUpToIndex
174
+ while (tailStart < source.length && isToolResultMessage(source[tailStart])) tailStart += 1
175
+ return [summaryMessage, ...source.slice(tailStart)]
176
+ }
177
+
178
+ function messageTimestampMs(message) {
179
+ const value = message?.timestamp
180
+ if (typeof value === 'number' && Number.isFinite(value)) return value
181
+ if (typeof value === 'string') {
182
+ const parsed = Date.parse(value)
183
+ if (Number.isFinite(parsed)) return parsed
184
+ }
185
+ return 0
186
+ }
187
+
188
+ function minimumProviderUsageIndex(session, messages) {
189
+ if (!session?.contextCompaction?.summaryMessage) return 0
190
+ const source = Array.isArray(messages) ? messages : []
191
+ const compactedUpToIndex = Math.min(source.length, Math.max(0, Number(session.contextCompaction.compactedUpToIndex) || 0))
192
+ let tailStart = compactedUpToIndex
193
+ while (tailStart < source.length && isToolResultMessage(source[tailStart])) tailStart += 1
194
+
195
+ const compactedAt = Date.parse(session.contextCompaction.compactedAt || '')
196
+ if (Number.isFinite(compactedAt)) {
197
+ const firstNewMessageIndex = source.findIndex((message) => messageTimestampMs(message) > compactedAt)
198
+ if (firstNewMessageIndex >= 0) return Math.max(1, firstNewMessageIndex - tailStart + 1)
199
+ }
200
+
201
+ const sourceMessageCount = Number(session.contextCompaction.sourceMessageCount)
202
+ if (Number.isFinite(sourceMessageCount) && sourceMessageCount > 0 && source.length >= sourceMessageCount) {
203
+ return Math.max(1, sourceMessageCount - tailStart + 1)
204
+ }
205
+ return source.length - tailStart + 1
206
+ }
207
+
208
+ export function hasNewMessagesSinceCompaction(session, messages) {
209
+ if (!session?.contextCompaction?.summaryMessage) return true
210
+ const source = Array.isArray(messages) ? messages : []
211
+ const sourceMessageCount = Number(session.contextCompaction.sourceMessageCount)
212
+ if (Number.isFinite(sourceMessageCount) && sourceMessageCount > 0 && source.length > sourceMessageCount) return true
213
+
214
+ const compactedAt = Date.parse(session.contextCompaction.compactedAt || '')
215
+ return Number.isFinite(compactedAt) && source.some((message) => messageTimestampMs(message) > compactedAt)
149
216
  }
150
217
 
151
218
  export async function compactSessionInPlace({
@@ -281,6 +348,7 @@ export function estimateSessionContextUsage(session, messages = session?.agent?.
281
348
  messages: loopMessages,
282
349
  tools: session.agent.state.tools,
283
350
  model: session.model,
351
+ minimumProviderUsageIndex: minimumProviderUsageIndex(session, sourceMessages),
284
352
  })
285
353
  value.isCompacted = loopMessages !== sourceMessages
286
354
  value.originalMessageCount = sourceMessages.length
@@ -305,6 +373,7 @@ export async function maybeAutoCompactSession({ session, messages, signal, emitS
305
373
  messages: loopMessages,
306
374
  tools: session.agent.state.tools,
307
375
  model: session.model,
376
+ minimumProviderUsageIndex: minimumProviderUsageIndex(session, messages),
308
377
  })
309
378
  if (!usage.contextWindow) return { compacted: false, usage, reason: 'missing_context_window' }
310
379
  if (!shouldCompactContextByPercent(usage, settings.thresholdPercent)) return { compacted: false, usage, reason: 'below_threshold' }
@@ -314,7 +383,7 @@ export async function maybeAutoCompactSession({ session, messages, signal, emitS
314
383
  if (session.lastAutoCompactAt && now - session.lastAutoCompactAt < AUTO_COMPACT_MIN_INTERVAL_MS) {
315
384
  return { compacted: false, usage, reason: 'recently_compacted' }
316
385
  }
317
- if (session.contextCompaction?.sourceMessageCount && messages.length <= session.contextCompaction.sourceMessageCount + 2) {
386
+ if (!hasNewMessagesSinceCompaction(session, messages)) {
318
387
  return { compacted: false, usage, reason: 'not_enough_new_messages' }
319
388
  }
320
389
 
@@ -60,19 +60,16 @@ export class CloudClient {
60
60
 
61
61
  health(signal) { return this.request('healthz', { signal }) }
62
62
  ready(signal) { return this.request('readyz', { signal }) }
63
- registerGuest(input, idempotencyKey, signal) {
64
- return this.request('v1/guest/installations', {
65
- method: 'POST', body: input, headers: { 'Idempotency-Key': idempotencyKey }, signal,
66
- })
67
- }
68
63
  refresh(refreshToken, signal) {
69
64
  return this.request('oauth/token', {
70
65
  method: 'POST', body: { grantType: 'refresh_token', refreshToken }, signal,
71
66
  })
72
67
  }
73
- authorizeDevice(token, installationId, clientId = 'quickforge-desktop', signal) {
68
+ authorizeDevice({ installationId, clientId = 'quickforge-desktop', publicKey, installationName, platform, clientVersion, signal } = {}) {
74
69
  return this.request('oauth/device_authorization', {
75
- method: 'POST', token, body: { installationId, clientId }, signal,
70
+ method: 'POST',
71
+ body: { installationId, clientId, publicKey, installationName, platform, clientVersion },
72
+ signal,
76
73
  })
77
74
  }
78
75
  exchangeDeviceCode(deviceCode, clientId = 'quickforge-desktop', signal) {
@@ -1,14 +1,9 @@
1
- import net from 'node:net'
2
-
3
1
  const DEFAULT_TIMEOUT_MS = 10_000
4
2
 
5
- function isLoopbackHost(hostname) {
6
- const host = String(hostname || '').toLowerCase()
7
- if (host === 'localhost' || host.endsWith('.localhost')) return true
8
- const ip = net.isIP(host)
9
- if (ip === 4) return host.startsWith('127.')
10
- if (ip === 6) return host === '::1' || host === '0:0:0:0:0:0:0:1'
11
- return false
3
+ function invalidUrl(message) {
4
+ const error = new Error(message)
5
+ error.statusCode = 400
6
+ return error
12
7
  }
13
8
 
14
9
  export function parseCloudBaseUrl(value, _options = {}) {
@@ -19,18 +14,16 @@ export function parseCloudBaseUrl(value, _options = {}) {
19
14
  try {
20
15
  url = new URL(raw)
21
16
  } catch {
22
- throw new Error('QUICKFORGE_CLOUD_URL must be a valid URL.')
17
+ throw invalidUrl('QuickForge Cloud URL must be a valid URL.')
23
18
  }
24
19
 
25
20
  if (url.username || url.password || url.search || url.hash) {
26
- throw new Error('QUICKFORGE_CLOUD_URL must not contain credentials, query parameters, or fragments.')
21
+ throw invalidUrl('QuickForge Cloud URL must not contain credentials, query parameters, or fragments.')
27
22
  }
28
- if (url.protocol !== 'https:') {
29
- if (!(url.protocol === 'http:' && isLoopbackHost(url.hostname))) {
30
- throw new Error('QUICKFORGE_CLOUD_URL must use HTTPS. HTTP is allowed only for loopback addresses.')
31
- }
23
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') {
24
+ throw invalidUrl('QuickForge Cloud URL must use http or https.')
32
25
  }
33
- if (url.pathname.includes('..')) throw new Error('QUICKFORGE_CLOUD_URL contains an unsafe path.')
26
+ if (url.pathname.includes('..')) throw invalidUrl('QuickForge Cloud URL contains an unsafe path.')
34
27
  url.pathname = `${url.pathname.replace(/\/+$/, '')}/`
35
28
  return url
36
29
  }
@@ -1,4 +1,4 @@
1
- import { generateKeyPair } from 'node:crypto'
1
+ import { generateKeyPair, randomUUID } from 'node:crypto'
2
2
  import { promises as fs } from 'node:fs'
3
3
  import os from 'node:os'
4
4
  import path from 'node:path'
@@ -145,8 +145,15 @@ export function createCloudCredentialStore({
145
145
 
146
146
  async function ensureInstallation() {
147
147
  const current = await read()
148
- if (current.publicKey && current.privateKeyPkcs8) return current
149
- return update(async (record) => ({ ...record, ...(await generateInstallationKeyPair()) }))
148
+ if (current.publicKey && current.privateKeyPkcs8 && current.installationId) return current
149
+ return update(async (record) => {
150
+ const keyPair = (record.publicKey && record.privateKeyPkcs8) ? {} : await generateInstallationKeyPair()
151
+ return {
152
+ ...record,
153
+ ...keyPair,
154
+ installationId: record.installationId || randomUUID(),
155
+ }
156
+ })
150
157
  }
151
158
 
152
159
  async function rotateInstallation() {
@@ -1,5 +1,4 @@
1
- import { createHash, randomUUID } from 'node:crypto'
2
- import { CloudApiError } from './client.mjs'
1
+ import { CloudApiError } from './client.mjs'
3
2
 
4
3
  const SESSION_INVALID_CODES = new Set(['refresh_token_reused', 'installation_revoked', 'invalid_refresh_token'])
5
4
  const DEVICE_PENDING_CODES = new Set(['authorization_pending', 'pending'])
@@ -11,7 +10,6 @@ const DEVICE_NETWORK_CAUSE_CODES = new Set(['ECONNREFUSED', 'ECONNRESET', 'ENETD
11
10
  const DEVICE_CLIENT_ID = 'quickforge-desktop'
12
11
  const MODEL_CACHE_TTL_MS = 60_000
13
12
  const MODEL_CACHE_HARD_TTL_MS = 5 * 60_000
14
- const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
15
13
 
16
14
  function tokenExpiry(expiresIn) {
17
15
  const seconds = Number(expiresIn)
@@ -38,15 +36,6 @@ function isDeviceFlowNetworkError(error) {
38
36
  return error instanceof TypeError || DEVICE_NETWORK_CAUSE_CODES.has(causeCode)
39
37
  }
40
38
 
41
- function registrationHash(record, serviceUrl) {
42
- return createHash('sha256').update(JSON.stringify({
43
- installationName: record.installationName,
44
- platform: record.platform,
45
- clientVersion: record.clientVersion,
46
- publicKey: record.publicKey,
47
- serviceUrl,
48
- })).digest('base64url')
49
- }
50
39
 
51
40
  export class CloudIdentityManager {
52
41
  constructor({ client, store, serviceUrl } = {}) {
@@ -92,45 +81,6 @@ export class CloudIdentityManager {
92
81
  }
93
82
  }
94
83
 
95
- async startGuest({ signal } = {}) {
96
- let record = await this.store.read()
97
- this.assertSessionService(record)
98
- if (record.rotateInstallationBeforeRegistration) {
99
- record = await this.store.rotateInstallation()
100
- } else {
101
- record = await this.store.ensureInstallation()
102
- }
103
- const requestHash = registrationHash(record, this.serviceUrl)
104
- let idempotencyKey = record.pendingRegistrationKey
105
- if (!UUID_PATTERN.test(idempotencyKey || '') || record.pendingRegistrationHash !== requestHash) {
106
- idempotencyKey = randomUUID()
107
- await this.store.update((current) => ({
108
- ...current,
109
- pendingRegistrationKey: idempotencyKey,
110
- pendingRegistrationHash: requestHash,
111
- }))
112
- }
113
-
114
- const tokens = await this.client.registerGuest({
115
- installationName: record.installationName,
116
- platform: record.platform,
117
- clientVersion: record.clientVersion,
118
- publicKey: record.publicKey,
119
- }, idempotencyKey, signal)
120
-
121
- this.setAccess(tokens)
122
- await this.store.update((current) => ({
123
- ...current,
124
- mode: tokens.identityMode || 'guest',
125
- installationId: tokens.installationId,
126
- refreshToken: tokens.refreshToken,
127
- sessionCloudUrl: this.serviceUrl,
128
- pendingRegistrationKey: undefined,
129
- pendingRegistrationHash: undefined,
130
- pendingDeviceFlow: undefined,
131
- }))
132
- return this.getStatusWithRemoteSummary(signal)
133
- }
134
84
 
135
85
  async startDeviceFlow({ signal } = {}) {
136
86
  let record = await this.store.read()
@@ -138,16 +88,23 @@ export class CloudIdentityManager {
138
88
  if (record.mode === 'account' && record.refreshToken) {
139
89
  throw new CloudApiError('QuickForge Cloud is already connected to an account.', { status: 409, code: 'cloud_account_already_connected' })
140
90
  }
141
- if (record.mode === 'local') {
142
- await this.startGuest({ signal })
143
- record = await this.store.read()
91
+ if (record.rotateInstallationBeforeRegistration) {
92
+ record = await this.store.rotateInstallation()
144
93
  }
145
- if (!record.refreshToken || !record.installationId) {
146
- throw new CloudApiError('QuickForge Cloud guest session is required before account sign-in.', { status: 409, code: 'cloud_not_connected' })
94
+ record = await this.store.ensureInstallation()
95
+ if (!record.installationId || !record.publicKey) {
96
+ throw new CloudApiError('QuickForge Cloud installation identity is incomplete.', { status: 409, code: 'cloud_installation_missing' })
147
97
  }
148
98
  this.assertSessionService(record)
149
- const accessToken = await this.access({ signal })
150
- const authorization = await this.client.authorizeDevice(accessToken, record.installationId, DEVICE_CLIENT_ID, signal)
99
+ const authorization = await this.client.authorizeDevice({
100
+ installationId: record.installationId,
101
+ clientId: DEVICE_CLIENT_ID,
102
+ publicKey: record.publicKey,
103
+ installationName: record.installationName,
104
+ platform: record.platform,
105
+ clientVersion: record.clientVersion,
106
+ signal,
107
+ })
151
108
  const expiresIn = Math.max(1, Number(authorization?.expiresIn) || 600)
152
109
  const interval = Math.max(1, Number(authorization?.interval) || 5)
153
110
  const pendingDeviceFlow = {
@@ -270,7 +227,7 @@ export class CloudIdentityManager {
270
227
  this.setAccess(tokens)
271
228
  await this.store.update((current) => ({
272
229
  ...current,
273
- mode: tokens.identityMode || current.mode || 'guest',
230
+ mode: tokens.identityMode || current.mode || 'account',
274
231
  installationId: tokens.installationId || current.installationId,
275
232
  refreshToken: tokens.refreshToken,
276
233
  sessionCloudUrl: this.serviceUrl,
@@ -338,7 +295,7 @@ export class CloudIdentityManager {
338
295
  if (this.modelsPromise) return this.modelsPromise
339
296
  this.modelsPromise = this.withAccessToken((token) => this.client.models(token, signal), { signal })
340
297
  .then((response) => {
341
- this.modelsCache = Array.isArray(response?.items) ? response.items : []
298
+ this.modelsCache = Array.isArray(response?.data) ? response.data : (Array.isArray(response?.items) ? response.items : [])
342
299
  this.modelsCachedAt = Date.now()
343
300
  return this.modelsCache
344
301
  })
@@ -16,7 +16,7 @@ export function toPublicCloudModel(model) {
16
16
  reasoning: Boolean(capabilities.reasoning),
17
17
  input: capabilities.vision ? ['text', 'image'] : ['text'],
18
18
  contextWindow: Number(model.contextWindow || 128_000),
19
- maxTokens: Number(model.maxTokens || 8_192),
19
+ maxTokens: Number(model.maxTokens || 32_768),
20
20
  quickforgeModelSource: CLOUD_SOURCE,
21
21
  quickforgeCatalogId: id,
22
22
  quickforgeCapabilities: {