shariq-pi-extensions 0.1.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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/THIRD_PARTY_NOTICES.md +11 -0
  4. package/docs/ARCHITECTURE.md +54 -0
  5. package/docs/DEVELOPMENT.md +88 -0
  6. package/docs/EXTENSIONS.md +89 -0
  7. package/extensions/antigravity-provider/LICENSE +21 -0
  8. package/extensions/antigravity-provider/README.md +23 -0
  9. package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
  10. package/extensions/antigravity-provider/antigravity/index.ts +67 -0
  11. package/extensions/antigravity-provider/antigravity/models.ts +184 -0
  12. package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
  13. package/extensions/antigravity-provider/index.ts +3 -0
  14. package/extensions/ask-user/README.md +21 -0
  15. package/extensions/ask-user/index.ts +417 -0
  16. package/extensions/background-terminals/README.md +43 -0
  17. package/extensions/background-terminals/index.ts +400 -0
  18. package/extensions/background-terminals/src/manager.ts +480 -0
  19. package/extensions/background-terminals/src/output-buffer.ts +110 -0
  20. package/extensions/background-terminals/src/presentation.ts +74 -0
  21. package/extensions/background-terminals/src/types.ts +47 -0
  22. package/extensions/background-terminals/src/ui.ts +478 -0
  23. package/extensions/context-usage/LICENSE +21 -0
  24. package/extensions/context-usage/README.md +20 -0
  25. package/extensions/context-usage/index.ts +2006 -0
  26. package/extensions/context-usage/lib/ansi.ts +60 -0
  27. package/extensions/context-usage/lib/boundary.ts +27 -0
  28. package/extensions/context-usage/lib/chat.ts +190 -0
  29. package/extensions/context-usage/lib/config.ts +30 -0
  30. package/extensions/context-usage/lib/fmt.ts +59 -0
  31. package/extensions/context-usage/lib/style.ts +260 -0
  32. package/extensions/copy-all/README.md +15 -0
  33. package/extensions/copy-all/index.ts +72 -0
  34. package/extensions/factory-provider/API_KEYS.md +17 -0
  35. package/extensions/factory-provider/AUTH.md +15 -0
  36. package/extensions/factory-provider/README.md +90 -0
  37. package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
  38. package/extensions/factory-provider/UPDATING.md +97 -0
  39. package/extensions/factory-provider/factory/api-keys.ts +407 -0
  40. package/extensions/factory-provider/factory/auth.ts +132 -0
  41. package/extensions/factory-provider/factory/constants.ts +15 -0
  42. package/extensions/factory-provider/factory/dashboard.ts +363 -0
  43. package/extensions/factory-provider/factory/droid.ts +84 -0
  44. package/extensions/factory-provider/factory/limits.ts +342 -0
  45. package/extensions/factory-provider/factory/models.ts +283 -0
  46. package/extensions/factory-provider/factory/responses.ts +331 -0
  47. package/extensions/factory-provider/factory/warm-theme.ts +53 -0
  48. package/extensions/factory-provider/factory/websocket.ts +130 -0
  49. package/extensions/factory-provider/index.ts +204 -0
  50. package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
  51. package/extensions/firecrawl-web/README.md +31 -0
  52. package/extensions/firecrawl-web/auth.ts +92 -0
  53. package/extensions/firecrawl-web/client.ts +238 -0
  54. package/extensions/firecrawl-web/index.ts +96 -0
  55. package/extensions/firecrawl-web/output.ts +125 -0
  56. package/extensions/git-info/README.md +25 -0
  57. package/extensions/git-info/index.ts +162 -0
  58. package/extensions/git-info/src/git.ts +206 -0
  59. package/extensions/git-info/src/ui.ts +291 -0
  60. package/extensions/goal/README.md +44 -0
  61. package/extensions/goal/constants.ts +4 -0
  62. package/extensions/goal/extension.ts +1064 -0
  63. package/extensions/goal/index.ts +2 -0
  64. package/extensions/goal/types.ts +56 -0
  65. package/extensions/goal/ui.ts +334 -0
  66. package/extensions/orchestration/README.md +58 -0
  67. package/extensions/orchestration/dashboard.ts +192 -0
  68. package/extensions/orchestration/engine.ts +706 -0
  69. package/extensions/orchestration/index.ts +243 -0
  70. package/extensions/orchestration/prompts.ts +124 -0
  71. package/extensions/orchestration/protocol.ts +116 -0
  72. package/extensions/orchestration/settings-ui.ts +41 -0
  73. package/extensions/orchestration/settings.ts +59 -0
  74. package/extensions/orchestration/storage.ts +49 -0
  75. package/extensions/orchestration/types.ts +105 -0
  76. package/extensions/performance-status/README.md +5 -0
  77. package/extensions/performance-status/index.ts +171 -0
  78. package/extensions/pi-memory/README.md +53 -0
  79. package/extensions/pi-memory/index.ts +326 -0
  80. package/extensions/pi-memory/src/config.ts +75 -0
  81. package/extensions/pi-memory/src/database.ts +617 -0
  82. package/extensions/pi-memory/src/extraction.ts +178 -0
  83. package/extensions/pi-memory/src/project.ts +80 -0
  84. package/extensions/pi-memory/src/projection.ts +59 -0
  85. package/extensions/pi-memory/src/redaction.ts +60 -0
  86. package/extensions/pi-memory/src/retrieval.ts +30 -0
  87. package/extensions/pi-memory/src/service.ts +194 -0
  88. package/extensions/pi-memory/src/session.ts +146 -0
  89. package/extensions/pi-memory/src/types.ts +82 -0
  90. package/extensions/shared/README.md +18 -0
  91. package/extensions/shared/activity-status.ts +31 -0
  92. package/extensions/shared/child-session.ts +150 -0
  93. package/extensions/shared/context-utilization.ts +47 -0
  94. package/extensions/shared/dashboard-state.ts +99 -0
  95. package/extensions/shared/tool-call-timeout.ts +104 -0
  96. package/extensions/shared/tui-dashboard.ts +139 -0
  97. package/extensions/shell-shortcuts/README.md +17 -0
  98. package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
  99. package/extensions/shell-shortcuts/index.ts +6 -0
  100. package/extensions/subagents/README.md +89 -0
  101. package/extensions/subagents/index.ts +1386 -0
  102. package/extensions/subagents/src/backend.ts +69 -0
  103. package/extensions/subagents/src/backends/pi.ts +781 -0
  104. package/extensions/subagents/src/catalog.ts +85 -0
  105. package/extensions/subagents/src/config.ts +241 -0
  106. package/extensions/subagents/src/context.ts +57 -0
  107. package/extensions/subagents/src/coordinator.ts +64 -0
  108. package/extensions/subagents/src/domain.ts +294 -0
  109. package/extensions/subagents/src/format.ts +74 -0
  110. package/extensions/subagents/src/manager.ts +813 -0
  111. package/extensions/subagents/src/prompt.ts +94 -0
  112. package/extensions/subagents/src/result-delivery.ts +20 -0
  113. package/extensions/subagents/src/runtime.ts +48 -0
  114. package/extensions/subagents/src/ui/takeover.ts +695 -0
  115. package/extensions/subagents/src/ui/transcript.ts +182 -0
  116. package/extensions/subagents/src/worktree.ts +175 -0
  117. package/extensions/web-fetch/README.md +13 -0
  118. package/extensions/web-fetch/index.ts +441 -0
  119. package/package.json +117 -0
  120. package/skills/background-terminals/SKILL.md +42 -0
  121. package/skills/orchestration/SKILL.md +33 -0
  122. package/skills/subagents/SKILL.md +89 -0
  123. package/themes/ember-warm-dark.json +85 -0
@@ -0,0 +1,407 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
5
+ import { FACTORY_API_BASE_URL, FACTORY_STATE_DIR } from "./constants.ts";
6
+ import { factoryApiForModel } from "./models.ts";
7
+ import { streamSimpleFactoryResponses } from "./responses.ts";
8
+ import { createFactoryResponsesWebSocketFetch } from "./websocket.ts";
9
+ import {
10
+ cachedLimitDecision,
11
+ factoryCredentialId,
12
+ refreshFactoryLimits,
13
+ } from "./limits.ts";
14
+ import { droidVersion } from "./droid.ts";
15
+
16
+ export const FACTORY_API_KEY_FILE_SENTINEL = "pi-factory-api-key-file";
17
+ export const FACTORY_API_KEYS_PATH = join(FACTORY_STATE_DIR, "api-keys.json");
18
+ const DEFAULT_COOLDOWN_MS = 6 * 60 * 60 * 1000;
19
+ const AUTH_COOLDOWN_MS = 24 * 60 * 60 * 1000;
20
+ const RATE_COOLDOWN_MS = 30 * 60 * 1000;
21
+
22
+ export type FactoryApiKeyEntry = {
23
+ label: string;
24
+ key: string;
25
+ disabled?: boolean;
26
+ };
27
+
28
+ type FactoryApiKeyConfig = {
29
+ keys?: Array<string | Partial<FactoryApiKeyEntry>>;
30
+ };
31
+
32
+ type KeyRuntimeState = {
33
+ cooldownUntil?: number;
34
+ cooldownKind?: "auth" | "rate" | "quota";
35
+ lastError?: string;
36
+ lastUsedAt?: number;
37
+ };
38
+
39
+ const state = new Map<string, KeyRuntimeState>();
40
+ let lastConfigurationWarning: string | undefined;
41
+
42
+ function keyId(entry: FactoryApiKeyEntry) {
43
+ return factoryCredentialId(entry.key);
44
+ }
45
+
46
+ export function maskKey(key: string) {
47
+ const trimmed = key.trim();
48
+ if (trimmed.length <= 12) return "****";
49
+ return `${trimmed.slice(0, 5)}…${trimmed.slice(-4)}`;
50
+ }
51
+
52
+ function normalizeEntry(raw: string | Partial<FactoryApiKeyEntry>, index: number): FactoryApiKeyEntry | null {
53
+ if (typeof raw === "string") {
54
+ const key = raw.trim();
55
+ return key ? { label: `key-${index + 1}`, key } : null;
56
+ }
57
+ const key = raw.key?.trim();
58
+ if (!key) return null;
59
+ return { label: raw.label?.trim() || `key-${index + 1}`, key, disabled: Boolean(raw.disabled) };
60
+ }
61
+
62
+ export function parseFactoryApiKeyFile(raw: string): { entries: FactoryApiKeyEntry[]; warning?: string } {
63
+ try {
64
+ const parsed = JSON.parse(raw) as FactoryApiKeyConfig | string[];
65
+ const keys = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.keys) ? parsed.keys : [];
66
+ return { entries: keys.map(normalizeEntry).filter((entry): entry is FactoryApiKeyEntry => Boolean(entry)) };
67
+ } catch {
68
+ return { entries: [], warning: "Factory rotating-key configuration contains invalid JSON." };
69
+ }
70
+ }
71
+
72
+ export function loadFactoryApiKeys(): FactoryApiKeyEntry[] {
73
+ const entries: FactoryApiKeyEntry[] = [];
74
+ const envKeys = (process.env.FACTORY_API_KEYS || process.env.FACTORY_API_KEY || "")
75
+ .split(/[\n,]+/)
76
+ .map((key) => key.trim())
77
+ .filter(Boolean);
78
+ envKeys.forEach((key, index) => entries.push({ label: `env-${index + 1}`, key }));
79
+
80
+ lastConfigurationWarning = undefined;
81
+ if (existsSync(FACTORY_API_KEYS_PATH)) {
82
+ try {
83
+ const parsed = parseFactoryApiKeyFile(readFileSync(FACTORY_API_KEYS_PATH, "utf8"));
84
+ entries.push(...parsed.entries);
85
+ lastConfigurationWarning = parsed.warning;
86
+ } catch {
87
+ lastConfigurationWarning = "Factory rotating-key configuration could not be read.";
88
+ }
89
+ }
90
+
91
+ const seen = new Set<string>();
92
+ return entries.filter((entry) => {
93
+ if (entry.disabled) return false;
94
+ if (seen.has(entry.key)) return false;
95
+ seen.add(entry.key);
96
+ return true;
97
+ });
98
+ }
99
+
100
+ export function sortFactoryApiKeysByLastUsed(
101
+ entries: FactoryApiKeyEntry[],
102
+ lastUsed = (entry: FactoryApiKeyEntry) => state.get(keyId(entry))?.lastUsedAt,
103
+ ) {
104
+ return entries
105
+ .map((entry, index) => ({ entry, index, lastUsedAt: lastUsed(entry) ?? 0 }))
106
+ .sort((left, right) =>
107
+ left.lastUsedAt - right.lastUsedAt || left.index - right.index,
108
+ )
109
+ .map(({ entry }) => entry);
110
+ }
111
+
112
+ function activeKeyEntries(modelId?: string) {
113
+ const now = Date.now();
114
+ const active = loadFactoryApiKeys().filter((entry) => {
115
+ const id = keyId(entry);
116
+ const runtime = state.get(id);
117
+ if (!runtime?.cooldownUntil || runtime.cooldownUntil <= now) return true;
118
+ if (runtime.cooldownKind !== "quota" || !modelId) return false;
119
+ const decision = cachedLimitDecision(entry.key, modelId, now);
120
+ if (decision?.available === true) {
121
+ state.set(id, { ...runtime, cooldownUntil: undefined, cooldownKind: undefined });
122
+ return true;
123
+ }
124
+ if (decision?.available === false && decision.resetAt) {
125
+ state.set(id, { ...runtime, cooldownUntil: decision.resetAt });
126
+ }
127
+ return false;
128
+ });
129
+ // Least-recently-used ordering distributes successful traffic instead of
130
+ // draining the first configured key before considering the rest.
131
+ return sortFactoryApiKeysByLastUsed(active);
132
+ }
133
+
134
+ export function classifyFactoryKeyCooldown(message: string): { ms: number; kind: "auth" | "rate" | "quota" } | null {
135
+ const lower = message.toLowerCase();
136
+ // A bare 401/403 usually means our Factory transport or headers are wrong.
137
+ // Disable a credential only when Factory explicitly identifies the key itself.
138
+ if (lower.includes("invalid api key") || lower.includes("api key revoked") || lower.includes("api key expired")) return { ms: AUTH_COOLDOWN_MS, kind: "auth" };
139
+ if (/\b429\b/.test(lower) || lower.includes("rate limit")) return { ms: RATE_COOLDOWN_MS, kind: "rate" };
140
+ if (lower.includes("quota") || lower.includes("billing") || lower.includes("credit") || lower.includes("usage limit") || lower.includes("exhaust")) return { ms: DEFAULT_COOLDOWN_MS, kind: "quota" };
141
+ return null;
142
+ }
143
+
144
+ function markKeyFailure(
145
+ entry: FactoryApiKeyEntry,
146
+ error: string,
147
+ modelId: string,
148
+ ) {
149
+ const cooldown = classifyFactoryKeyCooldown(error);
150
+ if (!cooldown) return false;
151
+ const now = Date.now();
152
+ const cached = cachedLimitDecision(entry.key, modelId, now);
153
+ state.set(keyId(entry), {
154
+ cooldownUntil:
155
+ cached?.available === false && cached.resetAt
156
+ ? cached.resetAt
157
+ : now + cooldown.ms,
158
+ cooldownKind: cooldown.kind,
159
+ lastError: error,
160
+ lastUsedAt: now,
161
+ });
162
+
163
+ // Authorization failures cannot query billing. For rate/quota failures,
164
+ // refresh Factory's authoritative windows and replace the fallback cooldown
165
+ // with the exact monthly/weekly/5-hour reset when available.
166
+ if (cooldown.kind !== "auth") {
167
+ void refreshFactoryLimits(
168
+ [{ label: entry.label, secret: entry.key }],
169
+ { force: true, version: droidVersion() },
170
+ ).then(() => {
171
+ const decision = cachedLimitDecision(entry.key, modelId);
172
+ if (decision?.available !== false || !decision.resetAt) return;
173
+ const runtime = state.get(keyId(entry)) ?? {};
174
+ state.set(keyId(entry), {
175
+ ...runtime,
176
+ cooldownUntil: decision.resetAt,
177
+ });
178
+ }).catch(() => {
179
+ // Retain the conservative error-class fallback cooldown.
180
+ });
181
+ }
182
+ return true;
183
+ }
184
+
185
+ function markKeyUsed(entry: FactoryApiKeyEntry) {
186
+ const runtime = state.get(keyId(entry)) || {};
187
+ state.set(keyId(entry), { ...runtime, lastUsedAt: Date.now() });
188
+ }
189
+
190
+ function errorText(event: any): string | null {
191
+ if (!event || event.type !== "error") return null;
192
+ return event.error?.errorMessage || event.error?.message || event.reason || JSON.stringify(event).slice(0, 1000);
193
+ }
194
+
195
+ function errorEvent(model: any, message: string): any {
196
+ return {
197
+ type: "error",
198
+ reason: "error",
199
+ error: {
200
+ role: "assistant",
201
+ content: [],
202
+ api: model.api,
203
+ provider: model.provider,
204
+ model: model.id,
205
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
206
+ stopReason: "error",
207
+ errorMessage: message,
208
+ timestamp: Date.now(),
209
+ },
210
+ };
211
+ }
212
+
213
+ const organizationIdCache = new Map<string, Promise<string | undefined>>();
214
+
215
+ async function factoryApiKeyOrganizationId(key: FactoryApiKeyEntry, version: string) {
216
+ const id = keyId(key);
217
+ let pending = organizationIdCache.get(id);
218
+ if (!pending) {
219
+ pending = fetch(`${FACTORY_API_BASE_URL}/api/cli/whoami`, {
220
+ headers: {
221
+ Authorization: `Bearer ${key.key}`,
222
+ "User-Agent": `factory-cli/${version}`,
223
+ "X-Client-Version": version,
224
+ "X-Factory-Client": "cli",
225
+ },
226
+ }).then(async (response) => {
227
+ if (!response.ok) return undefined;
228
+ const body = await response.json() as { organization_id?: string; organizationId?: string };
229
+ return body.organization_id || body.organizationId;
230
+ }).catch(() => undefined);
231
+ organizationIdCache.set(id, pending);
232
+ }
233
+ return pending;
234
+ }
235
+
236
+ export function factoryProviderRoutingSource(apiProvider: string | undefined) {
237
+ return apiProvider === "xai" || apiProvider === "google" ? "session_lock" : "configured_order";
238
+ }
239
+
240
+ export function factoryResponsesUsesWebSocket(api: string, headers: Record<string, string>) {
241
+ const apiProvider = Object.entries(headers).find(([name]) => name.toLowerCase() === "x-api-provider")?.[1];
242
+ return api === "openai-responses" && apiProvider === "openai";
243
+ }
244
+
245
+ async function streamAttempt(model: any, context: any, options: any, key: FactoryApiKeyEntry) {
246
+ const api = factoryApiForModel(model.id);
247
+ const version = droidVersion();
248
+ const sessionId = options?.sessionId || randomUUID();
249
+ const assistantMessageId = randomUUID();
250
+ const organizationId = await factoryApiKeyOrganizationId(key, version);
251
+ const modelApiProvider = Object.entries(model.headers || {}).find(([name]) => name.toLowerCase() === "x-api-provider")?.[1] as string | undefined;
252
+ const headers: Record<string, string> = {
253
+ ...(model.headers || {}),
254
+ ...(options?.headers || {}),
255
+ "User-Agent": `factory-cli/${version}`,
256
+ "X-Client-Version": version,
257
+ "X-Factory-Client": "cli",
258
+ "X-Provider-Routing-Source": factoryProviderRoutingSource(modelApiProvider),
259
+ "X-Session-Id": sessionId,
260
+ "X-Assistant-Message-Id": assistantMessageId,
261
+ ...(organizationId ? { "X-Factory-Org-Id": organizationId } : {}),
262
+ };
263
+ delete headers.Authorization;
264
+ delete headers.authorization;
265
+ headers.Authorization = `Bearer ${key.key}`;
266
+ if (api === "anthropic-messages") headers["X-Api-Key"] = "placeholder";
267
+
268
+ const useResponsesWebSocket = factoryResponsesUsesWebSocket(api, headers);
269
+ return streamSimpleFactoryResponses(model, context, {
270
+ ...options,
271
+ sessionId,
272
+ headers,
273
+ ...(useResponsesWebSocket
274
+ ? { fetch: createFactoryResponsesWebSocketFetch({ apiKey: key.key, assistantMessageId, headers }) }
275
+ : {}),
276
+ });
277
+ }
278
+
279
+ export function selectFactoryApiKeysByLimits(
280
+ entries: FactoryApiKeyEntry[],
281
+ modelId: string,
282
+ decide = cachedLimitDecision,
283
+ ) {
284
+ const decisions = entries.map((key) => ({
285
+ key,
286
+ decision: decide(key.key, modelId),
287
+ }));
288
+ return {
289
+ keys: decisions
290
+ .filter(({ decision }) => decision?.available !== false)
291
+ .map(({ key }) => key),
292
+ exhausted: decisions
293
+ .filter(({ decision }) => decision?.available === false)
294
+ .map(({ key, decision }) =>
295
+ `${key.label}: ${decision!.pool} ${decision!.exhausted} exhausted`,
296
+ ),
297
+ };
298
+ }
299
+
300
+ export function streamSimpleFactoryApiKeyResponses(model: any, context: any, options?: any) {
301
+ const stream = createAssistantMessageEventStream();
302
+ (async () => {
303
+ const selected = selectFactoryApiKeysByLimits(activeKeyEntries(model.id), model.id);
304
+ const keys = selected.keys;
305
+ if (!keys.length) {
306
+ stream.push(errorEvent(
307
+ model,
308
+ selected.exhausted.length
309
+ ? `No configured Factory API key has available ${model.id} usage (${selected.exhausted.join("; ")}). Refresh with /factory.`
310
+ : `No active Factory API keys configured. Add keys to ${FACTORY_API_KEYS_PATH} or FACTORY_API_KEYS.`,
311
+ ));
312
+ stream.end();
313
+ return;
314
+ }
315
+
316
+ let lastError = "";
317
+ for (const key of keys) {
318
+ markKeyUsed(key);
319
+ const inner = await streamAttempt(model, context, options, key);
320
+ const buffered: any[] = [];
321
+ let hasStarted = false;
322
+ let retriedBeforeStart = false;
323
+
324
+ for await (const event of inner as any) {
325
+ const error = errorText(event);
326
+ if (error) {
327
+ lastError = error;
328
+ const retryNext = !hasStarted && markKeyFailure(key, error, model.id);
329
+ if (retryNext) {
330
+ retriedBeforeStart = true;
331
+ break;
332
+ }
333
+ for (const bufferedEvent of buffered) stream.push(bufferedEvent);
334
+ stream.push(event);
335
+ stream.end();
336
+ return;
337
+ }
338
+
339
+ if (!hasStarted) {
340
+ hasStarted = true;
341
+ for (const bufferedEvent of buffered) stream.push(bufferedEvent);
342
+ buffered.length = 0;
343
+ }
344
+ stream.push(event);
345
+ }
346
+
347
+ if (retriedBeforeStart) continue;
348
+ stream.end();
349
+ return;
350
+ }
351
+
352
+ stream.push(errorEvent(model, `All configured Factory API keys are cooling down or failed. Last error: ${lastError}`));
353
+ stream.end();
354
+ })().catch((error) => {
355
+ stream.push(errorEvent(model, error?.message || String(error)));
356
+ stream.end();
357
+ });
358
+ return stream;
359
+ }
360
+
361
+ function headerValue(headers: Record<string, string> | undefined, name: string) {
362
+ const match = Object.entries(headers || {}).find(([key]) => key.toLowerCase() === name.toLowerCase());
363
+ return match?.[1];
364
+ }
365
+
366
+ function withoutHeader(headers: Record<string, string> | undefined, name: string) {
367
+ return Object.fromEntries(Object.entries(headers || {}).filter(([key]) => key.toLowerCase() !== name.toLowerCase()));
368
+ }
369
+
370
+ export function factoryAuthModeFromHeaders(headers?: Record<string, string>) {
371
+ const authorization = headerValue(headers, "authorization") || "";
372
+ const token = authorization.replace(/^Bearer\s+/i, "").trim();
373
+ if (token === FACTORY_API_KEY_FILE_SENTINEL) return "api-key-file" as const;
374
+ if (token.split(".").length === 3) return "oauth" as const;
375
+ return token ? "api-key" as const : "missing" as const;
376
+ }
377
+
378
+ export function streamSimpleUnifiedFactoryResponses(model: any, context: any, options?: any) {
379
+ const mode = factoryAuthModeFromHeaders(options?.headers);
380
+ const headers = mode === "oauth" ? options?.headers : withoutHeader(options?.headers, "X-Factory-Org-Id");
381
+ if (mode === "api-key-file") {
382
+ return streamSimpleFactoryApiKeyResponses(model, context, { ...options, headers });
383
+ }
384
+ return streamSimpleFactoryResponses(model, context, { ...options, headers });
385
+ }
386
+
387
+ export function factoryApiKeyStatus() {
388
+ const keys = loadFactoryApiKeys();
389
+ const now = Date.now();
390
+ return {
391
+ configured: keys.length,
392
+ active: activeKeyEntries().length,
393
+ path: FACTORY_API_KEYS_PATH,
394
+ warning: lastConfigurationWarning,
395
+ keys: keys.map((entry) => {
396
+ const runtime = state.get(keyId(entry));
397
+ return {
398
+ id: keyId(entry),
399
+ label: entry.label,
400
+ key: maskKey(entry.key),
401
+ cooldownSeconds: runtime?.cooldownUntil && runtime.cooldownUntil > now ? Math.ceil((runtime.cooldownUntil - now) / 1000) : 0,
402
+ lastError: runtime?.lastError ? runtime.lastError.slice(0, 200) : undefined,
403
+ lastUsedAt: runtime?.lastUsedAt ? new Date(runtime.lastUsedAt).toISOString() : undefined,
404
+ };
405
+ }),
406
+ };
407
+ }
@@ -0,0 +1,132 @@
1
+ import type { OAuthCredentials } from "@earendil-works/pi-ai";
2
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
3
+ import { existsSync, readFileSync, writeFileSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { join } from "path";
6
+ import { REFRESH_SKEW_MS, WORKOS_BASE_URL, WORKOS_CLIENT_ID } from "./constants.ts";
7
+
8
+ export type DroidAuth = { access_token: string; refresh_token: string; active_organization_id?: string | null };
9
+ export type FactoryOAuthCredentials = OAuthCredentials & { activeOrganizationId?: string | null };
10
+ type WorkOSTokens = { access_token: string; refresh_token?: string; expires_in?: number };
11
+
12
+ function jwtPayload(token: string): any | undefined {
13
+ try {
14
+ const parts = token.split(".");
15
+ if (parts.length !== 3) return undefined;
16
+ return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
17
+ } catch {
18
+ return undefined;
19
+ }
20
+ }
21
+
22
+ function jwtExpiryMs(token: string): number | undefined {
23
+ const payload = jwtPayload(token);
24
+ return typeof payload?.exp === "number" ? payload.exp * 1000 : undefined;
25
+ }
26
+
27
+ function jwtOrgId(token: string): string | undefined {
28
+ const payload = jwtPayload(token);
29
+ return typeof payload?.external_org_id === "string" ? payload.external_org_id : typeof payload?.org_id === "string" ? payload.org_id : undefined;
30
+ }
31
+
32
+ function credentialsFromTokens(tokens: WorkOSTokens, fallbackRefresh?: string, activeOrganizationId?: string | null): FactoryOAuthCredentials {
33
+ const expires = jwtExpiryMs(tokens.access_token) || (tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : Date.now() + 60 * 60 * 1000);
34
+ return {
35
+ access: tokens.access_token,
36
+ refresh: tokens.refresh_token || fallbackRefresh || "",
37
+ expires: expires - REFRESH_SKEW_MS,
38
+ activeOrganizationId: activeOrganizationId || jwtOrgId(tokens.access_token) || null,
39
+ };
40
+ }
41
+
42
+ export function factoryOrgHeader(credentials: OAuthCredentials): Record<string, string> {
43
+ const orgId = (credentials as FactoryOAuthCredentials).activeOrganizationId || jwtOrgId(credentials.access);
44
+ return orgId ? { "X-Factory-Org-Id": orgId } : {};
45
+ }
46
+
47
+ export async function refreshFactoryToken(refreshToken: string, activeOrganizationId?: string | null): Promise<FactoryOAuthCredentials> {
48
+ const response = await fetch(`${WORKOS_BASE_URL}/authenticate`, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
51
+ body: new URLSearchParams({
52
+ grant_type: "refresh_token",
53
+ refresh_token: refreshToken,
54
+ client_id: WORKOS_CLIENT_ID,
55
+ ...(activeOrganizationId ? { organization_id: activeOrganizationId } : {}),
56
+ }),
57
+ });
58
+ if (!response.ok) throw new Error(`Factory WorkOS refresh failed: ${response.status} ${await response.text()}`);
59
+ return credentialsFromTokens((await response.json()) as WorkOSTokens, refreshToken, activeOrganizationId);
60
+ }
61
+
62
+ function factoryDir() { return join(homedir(), ".factory"); }
63
+
64
+ export function decryptFactoryAuthFile(): DroidAuth | null {
65
+ const keyPath = join(factoryDir(), "auth.v2.key");
66
+ const filePath = join(factoryDir(), "auth.v2.file");
67
+ if (!existsSync(keyPath) || !existsSync(filePath)) return null;
68
+ const key = Buffer.from(readFileSync(keyPath, "utf8").trim(), "base64");
69
+ const parts = readFileSync(filePath, "utf8").trim().split(":");
70
+ if (key.length !== 32 || parts.length !== 3) return null;
71
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(parts[0], "base64"));
72
+ decipher.setAuthTag(Buffer.from(parts[1], "base64"));
73
+ const plaintext = Buffer.concat([decipher.update(Buffer.from(parts[2], "base64")), decipher.final()]).toString("utf8");
74
+ const data = JSON.parse(plaintext) as DroidAuth;
75
+ return data?.access_token && data?.refresh_token ? data : null;
76
+ }
77
+
78
+ export function saveFactoryAuthFile(tokens: OAuthCredentials, activeOrganizationId?: string | null) {
79
+ const keyPath = join(factoryDir(), "auth.v2.key");
80
+ const filePath = join(factoryDir(), "auth.v2.file");
81
+ if (!existsSync(keyPath) || !existsSync(filePath)) return;
82
+ const key = Buffer.from(readFileSync(keyPath, "utf8").trim(), "base64");
83
+ if (key.length !== 32) return;
84
+ const body = JSON.stringify({ access_token: tokens.access, refresh_token: tokens.refresh, active_organization_id: activeOrganizationId || null });
85
+ const iv = randomBytes(16);
86
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
87
+ const encrypted = Buffer.concat([cipher.update(body, "utf8"), cipher.final()]);
88
+ writeFileSync(filePath, `${iv.toString("base64")}:${cipher.getAuthTag().toString("base64")}:${encrypted.toString("base64")}`, { mode: 0o600 });
89
+ }
90
+
91
+ export async function importDroidCredentials(): Promise<FactoryOAuthCredentials | null> {
92
+ const auth = decryptFactoryAuthFile();
93
+ if (!auth) return null;
94
+ const existing = credentialsFromTokens({ access_token: auth.access_token, refresh_token: auth.refresh_token }, undefined, auth.active_organization_id);
95
+ if (existing.expires > Date.now()) return existing;
96
+ const refreshed = await refreshFactoryToken(auth.refresh_token, auth.active_organization_id);
97
+ saveFactoryAuthFile(refreshed, auth.active_organization_id);
98
+ return refreshed;
99
+ }
100
+
101
+ export async function requestDeviceCode() {
102
+ const response = await fetch(`${WORKOS_BASE_URL}/authorize/device`, {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
105
+ body: new URLSearchParams({ client_id: WORKOS_CLIENT_ID }),
106
+ });
107
+ if (!response.ok) throw new Error(`Factory device authorization failed: ${response.status} ${await response.text()}`);
108
+ return (await response.json()) as { device_code: string; user_code: string; verification_uri: string; verification_uri_complete: string; expires_in: number; interval: number };
109
+ }
110
+
111
+ export async function pollDeviceCode(deviceCode: string, expiresInSeconds: number, intervalSeconds: number): Promise<FactoryOAuthCredentials> {
112
+ const deadline = Date.now() + expiresInSeconds * 1000;
113
+ let interval = Math.max(1, intervalSeconds || 5);
114
+ while (Date.now() < deadline) {
115
+ const response = await fetch(`${WORKOS_BASE_URL}/authenticate`, {
116
+ method: "POST",
117
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
118
+ body: new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: deviceCode, client_id: WORKOS_CLIENT_ID }),
119
+ });
120
+ const text = await response.text();
121
+ if (response.ok) return credentialsFromTokens(JSON.parse(text) as WorkOSTokens);
122
+ let error = "unknown";
123
+ try { error = JSON.parse(text).error || error; } catch { /* ignore */ }
124
+ if (error === "authorization_pending" || error === "slow_down") {
125
+ if (error === "slow_down") interval += 1;
126
+ await new Promise((resolve) => setTimeout(resolve, interval * 1000));
127
+ continue;
128
+ }
129
+ throw new Error(`Factory device authorization failed: ${error}`);
130
+ }
131
+ throw new Error("Factory device authorization expired");
132
+ }
@@ -0,0 +1,15 @@
1
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
+ import { join } from "node:path";
3
+
4
+ export const PROVIDER_ID = "factory";
5
+ export const FACTORY_API_BASE_URL = process.env.FACTORY_API_BASE_URL?.trim() || "https://api.factory.ai";
6
+ export const FACTORY_RESPONSES_BASE_URL = `${FACTORY_API_BASE_URL}/api/llm/o/v1`;
7
+ export const WORKOS_BASE_URL = "https://api.workos.com/user_management";
8
+ export const WORKOS_CLIENT_ID = "client_01HNM792M5G5G1A2THWPXKFMXB";
9
+ export const FACTORY_CLIENT_PROTOCOL = "cli";
10
+ export const FALLBACK_DROID_VERSION = "0.200.0";
11
+ export const REFRESH_SKEW_MS = 2 * 60 * 1000;
12
+
13
+ export const DEFAULT_DROID_BINARY = "droid";
14
+ export const FACTORY_STATE_DIR = join(getAgentDir(), "factory");
15
+ export const DROID_CACHE_PATH = join(FACTORY_STATE_DIR, "droid.json");