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,331 @@
1
+ import { clampThinkingLevel } from "@earendil-works/pi-ai";
2
+ import {
3
+ createAssistantMessageEventStream,
4
+ stream,
5
+ streamSimple,
6
+ } from "@earendil-works/pi-ai/compat";
7
+ import { factoryApiForModel } from "./models.ts";
8
+
9
+ export const FACTORY_SYSTEM_MARKER = "You are Droid, an AI software engineering agent built by Factory.";
10
+
11
+ const BLOCKED_PI_PROMPT_PHRASES: Array<[RegExp, string]> = [
12
+ [
13
+ /You are an expert coding assistant operating inside pi, a coding agent harness\./g,
14
+ "You are an expert coding assistant running in Pi.",
15
+ ],
16
+ ];
17
+
18
+ function sanitizeText(text: string) {
19
+ let sanitized = text;
20
+ for (const [pattern, replacement] of BLOCKED_PI_PROMPT_PHRASES) sanitized = sanitized.replace(pattern, replacement);
21
+ return sanitized;
22
+ }
23
+
24
+ function sanitizeContent(content: any): any {
25
+ if (typeof content === "string") return sanitizeText(content);
26
+ if (Array.isArray(content)) return content.map(sanitizeContent);
27
+ if (content && typeof content === "object") {
28
+ if (typeof content.text === "string") return { ...content, text: sanitizeText(content.text) };
29
+ return content;
30
+ }
31
+ return content;
32
+ }
33
+
34
+ export function sanitizeFactoryContext(context: any) {
35
+ const messages = Array.isArray(context?.messages)
36
+ ? context.messages.map((message: any) => {
37
+ if (message?.role !== "system" && message?.role !== "developer") return message;
38
+ return { ...message, content: sanitizeContent(message.content) };
39
+ })
40
+ : context?.messages;
41
+ const sanitizedPrompt = typeof context?.systemPrompt === "string" ? sanitizeText(context.systemPrompt) : "";
42
+ const systemPrompt = sanitizedPrompt.startsWith(FACTORY_SYSTEM_MARKER)
43
+ ? sanitizedPrompt
44
+ : `${FACTORY_SYSTEM_MARKER}${sanitizedPrompt ? `\n\n${sanitizedPrompt}` : ""}`;
45
+ return { ...context, systemPrompt, messages };
46
+ }
47
+
48
+ export function resolvedFactoryReasoning(model: any, reasoning: string | undefined) {
49
+ if (!reasoning) return undefined;
50
+ const clamped = clampThinkingLevel(model, reasoning as any);
51
+ const mapped = model.thinkingLevelMap?.[clamped];
52
+ return typeof mapped === "string" ? mapped : clamped;
53
+ }
54
+
55
+ function optionsWithDirectReasoningEffort(options: any, model: any) {
56
+ const reasoningEffort = resolvedFactoryReasoning(model, options?.reasoning);
57
+ return {
58
+ ...options,
59
+ reasoningEffort: reasoningEffort && reasoningEffort !== "off" && reasoningEffort !== "none" ? reasoningEffort : undefined,
60
+ };
61
+ }
62
+
63
+ function optionsWithFactoryResponsesPayload(options: any, model: any) {
64
+ const apiProvider = Object.entries(model.headers || {}).find(([name]) => name.toLowerCase() === "x-api-provider")?.[1];
65
+ return {
66
+ ...options,
67
+ onPayload: async (payload: any, payloadModel: any) => {
68
+ const input = Array.isArray(payload?.input) ? [...payload.input] : [];
69
+ const first = input[0];
70
+ if (
71
+ first &&
72
+ (first.role === "system" || first.role === "developer") &&
73
+ typeof first.content === "string" &&
74
+ first.content.includes(FACTORY_SYSTEM_MARKER)
75
+ ) {
76
+ payload.instructions = first.content;
77
+ input.shift();
78
+ payload.input = input;
79
+ }
80
+ payload.parallel_tool_calls ??= true;
81
+ if (apiProvider === "xai" && payload.reasoning && typeof payload.reasoning === "object") {
82
+ delete payload.reasoning.summary;
83
+ }
84
+ return options?.onPayload ? options.onPayload(payload, payloadModel) : payload;
85
+ },
86
+ };
87
+ }
88
+
89
+ function messageText(content: any): string {
90
+ if (typeof content === "string") return content;
91
+ if (!Array.isArray(content)) return "";
92
+ return content.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
93
+ }
94
+
95
+ function convertFactoryGeminiMessages(context: any): any[] {
96
+ const contents: any[] = [];
97
+ for (const message of context.messages ?? []) {
98
+ if (message.role === "system" || message.role === "developer") continue;
99
+ if (message.role === "user") {
100
+ const blocks = typeof message.content === "string" ? [{ type: "text", text: message.content }] : message.content ?? [];
101
+ const parts = blocks.flatMap((part: any) => {
102
+ if (part?.type === "text" && typeof part.text === "string") return [{ text: part.text }];
103
+ if (part?.type === "image" && typeof part.data === "string") {
104
+ return [{ inlineData: { mimeType: part.mimeType ?? "image/png", data: part.data } }];
105
+ }
106
+ return [];
107
+ });
108
+ if (parts.length > 0) contents.push({ role: "user", parts });
109
+ continue;
110
+ }
111
+ if (message.role === "assistant") {
112
+ const parts = (message.content ?? []).flatMap((part: any) => {
113
+ if (part?.type === "text" && typeof part.text === "string") return [{ text: part.text }];
114
+ if (part?.type === "thinking" && typeof part.thinking === "string") return [{ text: part.thinking, thought: true }];
115
+ if (part?.type === "toolCall") {
116
+ return [{ functionCall: { id: part.id, name: part.name, args: part.arguments ?? {} }, ...(part.thoughtSignature ? { thoughtSignature: part.thoughtSignature } : {}) }];
117
+ }
118
+ return [];
119
+ });
120
+ if (parts.length > 0) contents.push({ role: "model", parts });
121
+ continue;
122
+ }
123
+ if (message.role === "toolResult") {
124
+ const responseText = messageText(message.content);
125
+ const part = {
126
+ functionResponse: {
127
+ id: message.toolCallId,
128
+ name: message.toolName,
129
+ response: message.isError ? { error: responseText } : { output: responseText },
130
+ },
131
+ };
132
+ const previous = contents.at(-1);
133
+ if (previous?.role === "user" && previous.parts?.some((item: any) => item.functionResponse)) previous.parts.push(part);
134
+ else contents.push({ role: "user", parts: [part] });
135
+ }
136
+ }
137
+ return contents;
138
+ }
139
+
140
+ function factoryGeminiTools(tools: any[] | undefined) {
141
+ if (!tools?.length) return undefined;
142
+ return [{
143
+ functionDeclarations: tools.map((tool) => ({
144
+ name: tool.name,
145
+ description: tool.description,
146
+ parametersJsonSchema: tool.parameters,
147
+ })),
148
+ }];
149
+ }
150
+
151
+ async function forEachFactoryGeminiEvent(response: Response, visit: (event: any) => void) {
152
+ const contentType = response.headers.get("content-type") ?? "";
153
+ if (!response.body || !contentType.includes("text/event-stream")) {
154
+ const parsed = JSON.parse(await response.text());
155
+ for (const event of Array.isArray(parsed) ? parsed : [parsed]) visit(event);
156
+ return;
157
+ }
158
+ const reader = response.body.getReader();
159
+ const decoder = new TextDecoder();
160
+ let pending = "";
161
+ while (true) {
162
+ const { done, value } = await reader.read();
163
+ pending += decoder.decode(value, { stream: !done });
164
+ const lines = pending.split(/\r?\n/);
165
+ pending = done ? "" : lines.pop() ?? "";
166
+ for (const line of lines) {
167
+ const trimmed = line.trim();
168
+ if (!trimmed.startsWith("data:")) continue;
169
+ const data = trimmed.slice(5).trim();
170
+ if (data && data !== "[DONE]") visit(JSON.parse(data));
171
+ }
172
+ if (done) break;
173
+ }
174
+ const trailing = pending.trim();
175
+ if (trailing.startsWith("data:")) {
176
+ const data = trailing.slice(5).trim();
177
+ if (data && data !== "[DONE]") visit(JSON.parse(data));
178
+ }
179
+ }
180
+
181
+ let factoryGeminiToolCallCounter = 0;
182
+
183
+ export function streamFactoryGemini(model: any, context: any, options?: any) {
184
+ const eventStream = createAssistantMessageEventStream();
185
+ (async () => {
186
+ const output: any = {
187
+ role: "assistant",
188
+ content: [],
189
+ api: model.api,
190
+ provider: model.provider,
191
+ model: model.id,
192
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
193
+ stopReason: "pending",
194
+ timestamp: Date.now(),
195
+ };
196
+ let currentBlock: any;
197
+ const finishBlock = () => {
198
+ if (!currentBlock) return;
199
+ const contentIndex = output.content.indexOf(currentBlock);
200
+ if (currentBlock.type === "thinking") {
201
+ eventStream.push({ type: "thinking_end", contentIndex, content: currentBlock.thinking, partial: output });
202
+ } else {
203
+ eventStream.push({ type: "text_end", contentIndex, content: currentBlock.text, partial: output });
204
+ }
205
+ currentBlock = undefined;
206
+ };
207
+ try {
208
+ const reasoningEffort = resolvedFactoryReasoning(model, options?.reasoning);
209
+ let payload: any = {
210
+ model: model.id,
211
+ contents: convertFactoryGeminiMessages(context),
212
+ ...(context.systemPrompt ? { systemInstruction: { parts: [{ text: context.systemPrompt }] } } : {}),
213
+ ...(context.tools?.length ? { tools: factoryGeminiTools(context.tools) } : {}),
214
+ generationConfig: {
215
+ temperature: options?.temperature ?? 1,
216
+ topP: 0.95,
217
+ topK: 64,
218
+ ...(reasoningEffort ? { thinkingConfig: { includeThoughts: true, thinkingLevel: reasoningEffort.toUpperCase() } } : {}),
219
+ ...(options?.maxTokens !== undefined ? { maxOutputTokens: options.maxTokens } : {}),
220
+ },
221
+ };
222
+ const transformed = await options?.onPayload?.(payload, model);
223
+ if (transformed !== undefined) payload = transformed;
224
+ const baseUrl = String(model.baseUrl || "https://api.factory.ai/api/llm/g/v1").replace(/\/$/, "");
225
+ const response = await fetch(`${baseUrl}/generate`, {
226
+ method: "POST",
227
+ headers: { "Content-Type": "application/json", ...(options?.headers || {}) },
228
+ body: JSON.stringify(payload),
229
+ signal: options?.signal,
230
+ });
231
+ if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
232
+ eventStream.push({ type: "start", partial: output });
233
+ await forEachFactoryGeminiEvent(response, (chunk) => {
234
+ const candidate = chunk?.candidates?.[0];
235
+ for (const part of candidate?.content?.parts ?? []) {
236
+ if (typeof part.text === "string") {
237
+ const blockType = part.thought === true ? "thinking" : "text";
238
+ if (!currentBlock || currentBlock.type !== blockType) {
239
+ finishBlock();
240
+ currentBlock = blockType === "thinking"
241
+ ? { type: "thinking", thinking: "", thinkingSignature: part.thoughtSignature }
242
+ : { type: "text", text: "", textSignature: part.thoughtSignature };
243
+ output.content.push(currentBlock);
244
+ eventStream.push({ type: `${blockType}_start`, contentIndex: output.content.length - 1, partial: output });
245
+ }
246
+ if (blockType === "thinking") {
247
+ currentBlock.thinking += part.text;
248
+ currentBlock.thinkingSignature ||= part.thoughtSignature;
249
+ eventStream.push({ type: "thinking_delta", contentIndex: output.content.length - 1, delta: part.text, partial: output });
250
+ } else {
251
+ currentBlock.text += part.text;
252
+ currentBlock.textSignature ||= part.thoughtSignature;
253
+ eventStream.push({ type: "text_delta", contentIndex: output.content.length - 1, delta: part.text, partial: output });
254
+ }
255
+ }
256
+ if (part.functionCall) {
257
+ finishBlock();
258
+ const toolCall = {
259
+ type: "toolCall" as const,
260
+ id: part.functionCall.id || `${part.functionCall.name}_${Date.now()}_${++factoryGeminiToolCallCounter}`,
261
+ name: part.functionCall.name || "",
262
+ arguments: part.functionCall.args ?? {},
263
+ ...(part.thoughtSignature ? { thoughtSignature: part.thoughtSignature } : {}),
264
+ };
265
+ output.content.push(toolCall);
266
+ const contentIndex = output.content.length - 1;
267
+ eventStream.push({ type: "toolcall_start", contentIndex, partial: output });
268
+ eventStream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(toolCall.arguments), partial: output });
269
+ eventStream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
270
+ }
271
+ }
272
+ if (candidate?.finishReason) {
273
+ output.rawStopReason = candidate.finishReason;
274
+ output.stopReason = candidate.finishReason === "MAX_TOKENS" ? "length" : candidate.finishReason === "STOP" ? "stop" : "error";
275
+ }
276
+ if (chunk?.usageMetadata) {
277
+ const usage = chunk.usageMetadata;
278
+ output.usage = {
279
+ input: (usage.promptTokenCount ?? 0) - (usage.cachedContentTokenCount ?? 0),
280
+ output: (usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0),
281
+ cacheRead: usage.cachedContentTokenCount ?? 0,
282
+ cacheWrite: 0,
283
+ reasoning: usage.thoughtsTokenCount ?? 0,
284
+ totalTokens: usage.totalTokenCount ?? 0,
285
+ cost: output.usage.cost,
286
+ };
287
+ }
288
+ });
289
+ finishBlock();
290
+ if (options?.signal?.aborted) throw new Error("Request was aborted");
291
+ if (output.content.some((part: any) => part.type === "toolCall") && output.stopReason === "stop") output.stopReason = "toolUse";
292
+ if (output.stopReason === "pending") throw new Error("Factory Gemini stream ended without a finish reason");
293
+ if (output.stopReason === "error") throw new Error(`Provider stopped with: ${output.rawStopReason ?? "unknown"}`);
294
+ eventStream.push({ type: "done", reason: output.stopReason, message: output });
295
+ eventStream.end();
296
+ } catch (error: any) {
297
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
298
+ output.errorMessage = error?.message || String(error);
299
+ eventStream.push({ type: "error", reason: output.stopReason, error: output });
300
+ eventStream.end();
301
+ }
302
+ })();
303
+ return eventStream;
304
+ }
305
+
306
+ export function streamSimpleFactoryResponses(model: any, context: any, options?: any) {
307
+ const api = factoryApiForModel(model.id);
308
+ const routedModel = { ...model, api };
309
+ const sanitizedContext = sanitizeFactoryContext(context);
310
+ switch (api) {
311
+ case "anthropic-messages": {
312
+ const reasoningDisabled = options?.reasoning === "off" || options?.reasoning === "none";
313
+ if (!reasoningDisabled) return streamSimple(routedModel, sanitizedContext, options);
314
+ return streamSimple(routedModel, sanitizedContext, {
315
+ ...options,
316
+ reasoning: undefined,
317
+ onPayload: async (payload: any, payloadModel: any) => {
318
+ if (payload?.thinking?.type === "disabled") delete payload.thinking;
319
+ return options?.onPayload ? options.onPayload(payload, payloadModel) : payload;
320
+ },
321
+ });
322
+ }
323
+ case "google-generative-ai":
324
+ return streamFactoryGemini(routedModel, sanitizedContext, options);
325
+ case "openai-completions":
326
+ return stream(routedModel, sanitizedContext, optionsWithDirectReasoningEffort(options, routedModel));
327
+ case "openai-responses":
328
+ default:
329
+ return streamSimple(routedModel, sanitizedContext, optionsWithFactoryResponsesPayload(options, routedModel));
330
+ }
331
+ }
@@ -0,0 +1,53 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+
3
+ type Rgb = readonly [number, number, number];
4
+
5
+ /**
6
+ * Warm, high-contrast Factory-only experiment.
7
+ *
8
+ * The palette uses distinct page, surface, and focus levels while replacing
9
+ * blue/cyan with amber, cream, terracotta, olive, and warm charcoal. It never
10
+ * changes Pi's global theme.
11
+ */
12
+ export const FACTORY_WARM_PALETTE = {
13
+ accent: [242, 169, 86],
14
+ text: [248, 234, 211],
15
+ muted: [207, 177, 145],
16
+ dim: [158, 128, 103],
17
+ success: [216, 194, 122],
18
+ warning: [239, 145, 87],
19
+ error: [239, 112, 96],
20
+ border: [139, 96, 68],
21
+ borderMuted: [82, 59, 48],
22
+ borderAccent: [220, 132, 64],
23
+ selectedBg: [72, 46, 35],
24
+ } satisfies Record<string, Rgb>;
25
+
26
+ function foreground(color: Rgb, text: string) {
27
+ return `\x1b[38;2;${color[0]};${color[1]};${color[2]}m${text}\x1b[39m`;
28
+ }
29
+
30
+ function background(color: Rgb, text: string) {
31
+ return `\x1b[48;2;${color[0]};${color[1]};${color[2]}m${text}\x1b[49m`;
32
+ }
33
+
34
+ export function factoryWarmTheme(theme: Theme): Theme {
35
+ return new Proxy(theme, {
36
+ get(target, property, receiver) {
37
+ if (property === "fg") {
38
+ return (color: Parameters<Theme["fg"]>[0], text: string) => {
39
+ const rgb = FACTORY_WARM_PALETTE[color as keyof typeof FACTORY_WARM_PALETTE];
40
+ return rgb ? foreground(rgb, text) : target.fg(color, text);
41
+ };
42
+ }
43
+ if (property === "bg") {
44
+ return (color: Parameters<Theme["bg"]>[0], text: string) => {
45
+ const rgb = FACTORY_WARM_PALETTE[color as keyof typeof FACTORY_WARM_PALETTE];
46
+ return rgb ? background(rgb, text) : target.bg(color, text);
47
+ };
48
+ }
49
+ const value = Reflect.get(target, property, receiver);
50
+ return typeof value === "function" ? value.bind(target) : value;
51
+ },
52
+ });
53
+ }
@@ -0,0 +1,130 @@
1
+ import WebSocket, { type RawData } from "ws";
2
+
3
+ const FACTORY_DROID_MARKER = "You are Droid, an AI software engineering agent built by Factory.";
4
+
5
+ export interface FactoryWebSocketFetchOptions {
6
+ apiKey: string;
7
+ assistantMessageId: string;
8
+ headers: Record<string, string>;
9
+ }
10
+
11
+ export function factoryResponsesWebSocketUrl(httpUrl: string): string {
12
+ const url = new URL(httpUrl);
13
+ url.protocol = url.protocol === "http:" ? "ws:" : "wss:";
14
+ url.pathname = `${url.pathname.replace(/\/$/, "")}/ws`;
15
+ return url.toString();
16
+ }
17
+
18
+ export function prepareFactoryResponsesWebSocketPayload(body: unknown): Record<string, unknown> {
19
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
20
+ throw new Error("Factory Responses request body must be a JSON object");
21
+ }
22
+
23
+ const payload = { ...(body as Record<string, unknown>) };
24
+ const input = Array.isArray(payload.input) ? [...payload.input] : [];
25
+ const first = input[0];
26
+ if (first && typeof first === "object" && !Array.isArray(first)) {
27
+ const message = first as Record<string, unknown>;
28
+ if (
29
+ (message.role === "developer" || message.role === "system") &&
30
+ typeof message.content === "string" &&
31
+ message.content.includes(FACTORY_DROID_MARKER)
32
+ ) {
33
+ payload.instructions = message.content;
34
+ input.shift();
35
+ payload.input = input;
36
+ }
37
+ }
38
+
39
+ payload.parallel_tool_calls ??= true;
40
+ payload.tool_choice ??= "auto";
41
+ return { type: "response.create", ...payload };
42
+ }
43
+
44
+ /** Adapt Factory's Responses WebSocket transport to the Fetch/SSE interface used by pi-ai. */
45
+ export function createFactoryResponsesWebSocketFetch(options: FactoryWebSocketFetchOptions): typeof fetch {
46
+ return (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
47
+ const requestUrl = typeof input === "string" || input instanceof URL ? input.toString() : input.url;
48
+ const bodyText = typeof init?.body === "string" ? init.body : await new Request(input, init).text();
49
+ const payload = prepareFactoryResponsesWebSocketPayload(JSON.parse(bodyText));
50
+ payload._factory = { assistantMessageId: options.assistantMessageId };
51
+ const responseStream = new TransformStream<Uint8Array, Uint8Array>();
52
+ const writer = responseStream.writable.getWriter();
53
+ const encoder = new TextEncoder();
54
+
55
+ return await new Promise<Response>((resolve, reject) => {
56
+ let opened = false;
57
+ let settled = false;
58
+ const socket = new WebSocket(factoryResponsesWebSocketUrl(requestUrl), {
59
+ headers: {
60
+ ...options.headers,
61
+ Authorization: `Bearer ${options.apiKey}`,
62
+ },
63
+ });
64
+
65
+ const abort = () => {
66
+ socket.close();
67
+ void writer.abort(new Error("Factory request was aborted"));
68
+ if (!settled) {
69
+ settled = true;
70
+ reject(new Error("Factory request was aborted"));
71
+ }
72
+ };
73
+ init?.signal?.addEventListener("abort", abort, { once: true });
74
+
75
+ socket.once("open", () => {
76
+ opened = true;
77
+ settled = true;
78
+ socket.send(JSON.stringify(payload));
79
+ resolve(
80
+ new Response(responseStream.readable, {
81
+ status: 200,
82
+ headers: { "content-type": "text/event-stream" },
83
+ }),
84
+ );
85
+ });
86
+
87
+ socket.on("message", (data: RawData) => {
88
+ const text = data.toString();
89
+ void writer.write(encoder.encode(`data: ${text}\n\n`));
90
+ try {
91
+ const event = JSON.parse(text) as { type?: string };
92
+ if (event.type === "response.completed" || event.type === "response.failed" || event.type === "error") {
93
+ void writer.close();
94
+ socket.close();
95
+ }
96
+ } catch {
97
+ // Let pi-ai report malformed provider events through its normal parser.
98
+ }
99
+ });
100
+
101
+ socket.once("unexpected-response", (_request, response) => {
102
+ const chunks: Buffer[] = [];
103
+ response.on("data", (chunk: Buffer) => chunks.push(chunk));
104
+ response.on("end", () => {
105
+ settled = true;
106
+ resolve(
107
+ new Response(Buffer.concat(chunks), {
108
+ status: response.statusCode ?? 500,
109
+ statusText: response.statusMessage,
110
+ headers: response.headers as HeadersInit,
111
+ }),
112
+ );
113
+ });
114
+ });
115
+
116
+ socket.once("error", (error) => {
117
+ if (!settled) {
118
+ settled = true;
119
+ reject(error);
120
+ } else if (opened) {
121
+ void writer.abort(error);
122
+ }
123
+ });
124
+ socket.once("close", () => {
125
+ init?.signal?.removeEventListener("abort", abort);
126
+ if (opened) void writer.close().catch(() => undefined);
127
+ });
128
+ });
129
+ }) as typeof fetch;
130
+ }