@every-env/spiral-cli 0.2.0 → 1.0.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 +35 -6
- package/package.json +5 -14
- package/src/api.ts +82 -474
- package/src/auth.ts +49 -214
- package/src/cli.ts +264 -948
- package/src/config.ts +29 -45
- package/src/output.ts +162 -0
- package/src/types.ts +87 -117
- package/src/attachments/index.ts +0 -174
- package/src/drafts/editor.ts +0 -105
- package/src/drafts/index.ts +0 -208
- package/src/notes/index.ts +0 -130
- package/src/styles/index.ts +0 -45
- package/src/suggestions/diff.ts +0 -33
- package/src/suggestions/index.ts +0 -205
- package/src/suggestions/parser.ts +0 -83
- package/src/tools/renderer.ts +0 -104
- package/src/workspaces/index.ts +0 -55
package/src/api.ts
CHANGED
|
@@ -1,520 +1,128 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getAuthToken, sanitizeError } from "./auth";
|
|
1
|
+
import { getAuthToken, sanitizeError } from "./auth.js";
|
|
3
2
|
import {
|
|
4
3
|
ApiError,
|
|
5
|
-
|
|
6
|
-
type Draft,
|
|
7
|
-
type DraftVersion,
|
|
8
|
-
type Message,
|
|
4
|
+
AuthenticationError,
|
|
9
5
|
NetworkError,
|
|
10
|
-
type
|
|
11
|
-
type
|
|
12
|
-
type
|
|
6
|
+
type GenerateRequest,
|
|
7
|
+
type GenerateResponse,
|
|
8
|
+
type SessionQuotaResponse,
|
|
13
9
|
type WritingStyle,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
(process.env.NODE_ENV === "development"
|
|
20
|
-
? "http://localhost:8000"
|
|
21
|
-
: "https://spiral-next-backend-production.up.railway.app");
|
|
10
|
+
type Workspace,
|
|
11
|
+
type Conversation,
|
|
12
|
+
type Draft,
|
|
13
|
+
type UserProfile,
|
|
14
|
+
} from "./types.js";
|
|
22
15
|
|
|
23
|
-
const
|
|
24
|
-
|
|
16
|
+
const BASE_URL =
|
|
17
|
+
process.env["SPIRAL_API_URL"] || "https://api.writewithspiral.com";
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
onToolCall: (tool: ToolCall) => void;
|
|
30
|
-
onSessionName: (name: string) => void;
|
|
31
|
-
onRetry: (info: { attempt: number; max: number; message: string }) => void;
|
|
32
|
-
onModelDowngrade: (from: string, to: string) => void;
|
|
33
|
-
onComplete: (sessionId: string) => void;
|
|
34
|
-
onError: (error: Error) => void;
|
|
19
|
+
interface FetchOptions extends RequestInit {
|
|
20
|
+
/** Skip auth header (for public endpoints like /prime). */
|
|
21
|
+
public?: boolean;
|
|
35
22
|
}
|
|
36
23
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
24
|
+
async function apiFetch<T>(
|
|
25
|
+
path: string,
|
|
26
|
+
options: FetchOptions = {},
|
|
27
|
+
): Promise<T> {
|
|
28
|
+
const { public: isPublic, ...fetchOptions } = options;
|
|
29
|
+
const url = `${BASE_URL}${path}`;
|
|
44
30
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
31
|
+
const headers: Record<string, string> = {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
...((fetchOptions.headers as Record<string, string>) ?? {}),
|
|
34
|
+
};
|
|
49
35
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
*/
|
|
54
|
-
export async function streamChat(
|
|
55
|
-
message: string,
|
|
56
|
-
sessionId: string | null,
|
|
57
|
-
callbacks: StreamCallbacks,
|
|
58
|
-
signal?: AbortSignal,
|
|
59
|
-
options?: StreamChatOptions,
|
|
60
|
-
): Promise<void> {
|
|
61
|
-
// Security: Validate inputs
|
|
62
|
-
if (message.length > MAX_MESSAGE_LENGTH) {
|
|
63
|
-
throw new ApiError(`Message too long (max ${MAX_MESSAGE_LENGTH} chars)`, 400);
|
|
64
|
-
}
|
|
65
|
-
if (sessionId && !isValidSessionId(sessionId)) {
|
|
66
|
-
throw new ApiError("Invalid session ID format", 400);
|
|
36
|
+
if (!isPublic) {
|
|
37
|
+
const token = getAuthToken();
|
|
38
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
67
39
|
}
|
|
68
40
|
|
|
69
|
-
|
|
41
|
+
let res: Response;
|
|
42
|
+
try {
|
|
43
|
+
res = await fetch(url, { ...fetchOptions, headers });
|
|
44
|
+
} catch (err) {
|
|
45
|
+
throw new NetworkError(
|
|
46
|
+
`Failed to connect to Spiral API: ${sanitizeError(err)}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
70
49
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
50
|
+
if (res.status === 401 || res.status === 403) {
|
|
51
|
+
throw new AuthenticationError(
|
|
52
|
+
"Invalid or expired API key. Run `spiral auth login` to re-authenticate.",
|
|
53
|
+
);
|
|
75
54
|
}
|
|
76
55
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
? Buffer.from(attachment.content, "base64")
|
|
83
|
-
: attachment.content;
|
|
84
|
-
const blob = new Blob([content], {
|
|
85
|
-
type: attachment.type === "text" ? "text/plain" : "application/octet-stream",
|
|
86
|
-
});
|
|
87
|
-
formData.append("files", blob, attachment.name);
|
|
88
|
-
}
|
|
56
|
+
if (res.status === 402) {
|
|
57
|
+
throw new ApiError(
|
|
58
|
+
"Session quota exceeded. Upgrade at app.writewithspiral.com/settings.",
|
|
59
|
+
402,
|
|
60
|
+
);
|
|
89
61
|
}
|
|
90
62
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
formData.append("notes", JSON.stringify(options.notes));
|
|
63
|
+
if (res.status === 429) {
|
|
64
|
+
throw new ApiError("Rate limited. Please wait and try again.", 429);
|
|
94
65
|
}
|
|
95
66
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
response = await fetch(`${API_BASE}/api/v1/team/stream`, {
|
|
99
|
-
method: "POST",
|
|
100
|
-
headers: {
|
|
101
|
-
Authorization: `Bearer ${token}`,
|
|
102
|
-
Accept: "text/event-stream",
|
|
103
|
-
},
|
|
104
|
-
body: formData,
|
|
105
|
-
signal,
|
|
106
|
-
});
|
|
107
|
-
} catch (error) {
|
|
108
|
-
if (signal?.aborted) return;
|
|
109
|
-
throw new NetworkError(`Connection failed: ${(error as Error).message}`);
|
|
67
|
+
if (res.status === 504) {
|
|
68
|
+
throw new ApiError("Request timed out. Try again or use --instant.", 504);
|
|
110
69
|
}
|
|
111
70
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const errorText = await response.text().catch(() => "");
|
|
115
|
-
let errorMessage = `API error: ${response.status}`;
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
let detail = "";
|
|
116
73
|
try {
|
|
117
|
-
const
|
|
118
|
-
|
|
74
|
+
const body = await res.json();
|
|
75
|
+
detail = body.detail || JSON.stringify(body);
|
|
119
76
|
} catch {
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
if (process.env.DEBUG) {
|
|
123
|
-
console.debug("API error response:", {
|
|
124
|
-
status: response.status,
|
|
125
|
-
url: `${API_BASE}/api/v1/team/stream`,
|
|
126
|
-
errorText: errorText.substring(0, 200),
|
|
127
|
-
});
|
|
77
|
+
detail = await res.text().catch(() => "Unknown error");
|
|
128
78
|
}
|
|
129
|
-
throw new ApiError(
|
|
79
|
+
throw new ApiError(`API error (${res.status}): ${detail}`, res.status);
|
|
130
80
|
}
|
|
131
81
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// Heartbeat timeout detection
|
|
137
|
-
const heartbeatCheck = setInterval(() => {
|
|
138
|
-
if (Date.now() - lastHeartbeat > HEARTBEAT_TIMEOUT_MS) {
|
|
139
|
-
clearInterval(heartbeatCheck);
|
|
140
|
-
callbacks.onError(new NetworkError("Stream timeout: no heartbeat received"));
|
|
141
|
-
}
|
|
142
|
-
}, 5000);
|
|
143
|
-
|
|
144
|
-
const parser = createParser({
|
|
145
|
-
onEvent(event: EventSourceMessage) {
|
|
146
|
-
lastHeartbeat = Date.now(); // Reset heartbeat on any event
|
|
147
|
-
|
|
148
|
-
if (event.data === "[DONE]") {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
const data = JSON.parse(event.data);
|
|
154
|
-
|
|
155
|
-
// Handle ALL event types from the SSE spec
|
|
156
|
-
switch (event.event) {
|
|
157
|
-
// Content events
|
|
158
|
-
case "RunResponseContent":
|
|
159
|
-
case "RunResponse":
|
|
160
|
-
if (data.content) callbacks.onChunk(data.content);
|
|
161
|
-
if (data.thinking) callbacks.onThinking(data.thinking);
|
|
162
|
-
if (data.reasoning_content) callbacks.onThinking(data.reasoning_content);
|
|
163
|
-
break;
|
|
164
|
-
|
|
165
|
-
// Session events
|
|
166
|
-
case "RunStarted":
|
|
167
|
-
currentSessionId = data.run_id || data.session_id || currentSessionId;
|
|
168
|
-
break;
|
|
169
|
-
case "SessionName":
|
|
170
|
-
callbacks.onSessionName(data.content || data.name || data.session_name);
|
|
171
|
-
break;
|
|
172
|
-
|
|
173
|
-
// Tool events
|
|
174
|
-
case "ToolCallStarted":
|
|
175
|
-
if (data.tools) {
|
|
176
|
-
for (const tool of data.tools) {
|
|
177
|
-
callbacks.onToolCall({ ...tool, status: "started" });
|
|
178
|
-
}
|
|
179
|
-
} else if (data.tool) {
|
|
180
|
-
const toolName = typeof data.tool === "string" ? data.tool : data.tool.tool_name;
|
|
181
|
-
callbacks.onToolCall({ tool_name: toolName, status: "started" });
|
|
182
|
-
}
|
|
183
|
-
break;
|
|
184
|
-
case "ToolCallArgumentsDelta":
|
|
185
|
-
callbacks.onToolCall({
|
|
186
|
-
tool_name: data.tool_name || "unknown",
|
|
187
|
-
tool_call_id: data.tool_call_id,
|
|
188
|
-
status: "arguments_delta",
|
|
189
|
-
});
|
|
190
|
-
break;
|
|
191
|
-
case "ToolCallArgumentsDone":
|
|
192
|
-
callbacks.onToolCall({
|
|
193
|
-
tool_name: data.tool_name || "unknown",
|
|
194
|
-
tool_call_id: data.tool_call_id,
|
|
195
|
-
status: "arguments_done",
|
|
196
|
-
});
|
|
197
|
-
break;
|
|
198
|
-
case "ToolCallCompleted":
|
|
199
|
-
if (data.tools) {
|
|
200
|
-
for (const tool of data.tools) {
|
|
201
|
-
callbacks.onToolCall({ ...tool, status: "completed" });
|
|
202
|
-
}
|
|
203
|
-
} else if (data.tool) {
|
|
204
|
-
const toolName = typeof data.tool === "string" ? data.tool : data.tool.tool_name;
|
|
205
|
-
callbacks.onToolCall({ tool_name: toolName, status: "completed" });
|
|
206
|
-
}
|
|
207
|
-
break;
|
|
208
|
-
|
|
209
|
-
// Completion
|
|
210
|
-
case "RunCompleted":
|
|
211
|
-
callbacks.onComplete(currentSessionId);
|
|
212
|
-
break;
|
|
213
|
-
|
|
214
|
-
// Handoff notification (agent switching)
|
|
215
|
-
case "HandoffNotification":
|
|
216
|
-
if (process.env.DEBUG) {
|
|
217
|
-
console.debug(`Agent handoff: ${data.from_agent} → ${data.to_agent}`);
|
|
218
|
-
}
|
|
219
|
-
break;
|
|
220
|
-
|
|
221
|
-
// Error handling events
|
|
222
|
-
case "retry_started":
|
|
223
|
-
case "retry_attempt":
|
|
224
|
-
try {
|
|
225
|
-
const retryData = typeof data.content === "string" ? JSON.parse(data.content) : data;
|
|
226
|
-
callbacks.onRetry({
|
|
227
|
-
attempt: retryData.retry_attempt || 1,
|
|
228
|
-
max: retryData.max_retries || 3,
|
|
229
|
-
message: retryData.message || "Retrying...",
|
|
230
|
-
});
|
|
231
|
-
} catch {
|
|
232
|
-
callbacks.onRetry({ attempt: 1, max: 3, message: "Retrying..." });
|
|
233
|
-
}
|
|
234
|
-
break;
|
|
235
|
-
case "retry_failed":
|
|
236
|
-
callbacks.onError(new ApiError(data.message || data.content || "Retry failed", 500));
|
|
237
|
-
break;
|
|
238
|
-
case "model_downgrade":
|
|
239
|
-
try {
|
|
240
|
-
const downgradeData =
|
|
241
|
-
typeof data.content === "string" ? JSON.parse(data.content) : data;
|
|
242
|
-
callbacks.onModelDowngrade(
|
|
243
|
-
downgradeData.from_model || "unknown",
|
|
244
|
-
downgradeData.to_model || "unknown",
|
|
245
|
-
);
|
|
246
|
-
} catch {
|
|
247
|
-
callbacks.onModelDowngrade("unknown", "unknown");
|
|
248
|
-
}
|
|
249
|
-
break;
|
|
250
|
-
case "Error":
|
|
251
|
-
callbacks.onError(
|
|
252
|
-
new ApiError(data.content || "Unknown error", data.status_code || 500),
|
|
253
|
-
);
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
} catch (_parseError) {
|
|
257
|
-
// Log parse errors in debug mode (don't silently swallow)
|
|
258
|
-
if (process.env.DEBUG) {
|
|
259
|
-
console.debug("Skipping non-JSON SSE event:", event.data?.substring(0, 100));
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
},
|
|
263
|
-
onComment() {
|
|
264
|
-
lastHeartbeat = Date.now(); // Pings appear as comments
|
|
265
|
-
},
|
|
266
|
-
onError(error) {
|
|
267
|
-
if (process.env.DEBUG) {
|
|
268
|
-
console.debug("SSE parse error:", error);
|
|
269
|
-
}
|
|
270
|
-
},
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
const reader = response.body?.getReader();
|
|
274
|
-
if (!reader) throw new NetworkError("No response body");
|
|
275
|
-
|
|
276
|
-
const decoder = new TextDecoder();
|
|
277
|
-
try {
|
|
278
|
-
while (true) {
|
|
279
|
-
const { done, value } = await reader.read();
|
|
280
|
-
if (done) break;
|
|
281
|
-
if (signal?.aborted) break;
|
|
282
|
-
parser.feed(decoder.decode(value, { stream: true }));
|
|
283
|
-
}
|
|
284
|
-
} finally {
|
|
285
|
-
clearInterval(heartbeatCheck);
|
|
286
|
-
reader.releaseLock();
|
|
287
|
-
parser.reset(); // Clear internal buffers (per performance review)
|
|
82
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
83
|
+
if (contentType.includes("application/json")) {
|
|
84
|
+
return (await res.json()) as T;
|
|
288
85
|
}
|
|
86
|
+
return (await res.text()) as unknown as T;
|
|
289
87
|
}
|
|
290
88
|
|
|
291
|
-
|
|
292
|
-
* Fetch conversations list
|
|
293
|
-
* @note Uses /conversations not /sessions (architecture review fix)
|
|
294
|
-
*/
|
|
295
|
-
export async function fetchConversations(): Promise<Conversation[]> {
|
|
296
|
-
const token = await getAuthToken();
|
|
89
|
+
// ── Endpoints ──
|
|
297
90
|
|
|
298
|
-
|
|
299
|
-
|
|
91
|
+
export async function generate(
|
|
92
|
+
req: GenerateRequest,
|
|
93
|
+
): Promise<GenerateResponse> {
|
|
94
|
+
return apiFetch<GenerateResponse>("/api/v1/generate", {
|
|
95
|
+
method: "POST",
|
|
96
|
+
body: JSON.stringify(req),
|
|
300
97
|
});
|
|
301
|
-
|
|
302
|
-
if (!response.ok) {
|
|
303
|
-
throw new ApiError(`Failed to fetch conversations: ${response.status}`, response.status);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const data = await response.json();
|
|
307
|
-
return data.sessions || [];
|
|
308
98
|
}
|
|
309
99
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
*/
|
|
313
|
-
export async function fetchMessages(conversationId: string): Promise<Message[]> {
|
|
314
|
-
if (!isValidSessionId(conversationId)) {
|
|
315
|
-
throw new ApiError("Invalid conversation ID format", 400);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const token = await getAuthToken();
|
|
319
|
-
|
|
320
|
-
const response = await fetch(
|
|
321
|
-
`${API_BASE}/api/v1/conversations/${encodeURIComponent(conversationId)}/messages`,
|
|
322
|
-
{ headers: { Authorization: `Bearer ${token}` } },
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
if (!response.ok) {
|
|
326
|
-
throw new ApiError(`Failed to fetch messages: ${response.status}`, response.status);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const data = await response.json();
|
|
330
|
-
return data.messages || [];
|
|
100
|
+
export async function getMe(): Promise<UserProfile> {
|
|
101
|
+
return apiFetch<UserProfile>("/api/v1/users/me");
|
|
331
102
|
}
|
|
332
103
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
*/
|
|
336
|
-
export function getApiBaseUrl(): string {
|
|
337
|
-
return API_BASE;
|
|
104
|
+
export async function getQuota(): Promise<SessionQuotaResponse> {
|
|
105
|
+
return apiFetch<SessionQuotaResponse>("/api/v1/billing/session-quota");
|
|
338
106
|
}
|
|
339
107
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
// ============================================================================
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Fetch all drafts for a session
|
|
346
|
-
*/
|
|
347
|
-
export async function fetchSessionDrafts(sessionId: string): Promise<Draft[]> {
|
|
348
|
-
if (!isValidSessionId(sessionId)) {
|
|
349
|
-
throw new ApiError("Invalid session ID format", 400);
|
|
350
|
-
}
|
|
351
|
-
const token = await getAuthToken();
|
|
352
|
-
const response = await fetch(`${API_BASE}/api/v1/sessions/${sessionId}/drafts`, {
|
|
353
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
354
|
-
});
|
|
355
|
-
if (!response.ok) {
|
|
356
|
-
throw new ApiError(`Failed to fetch drafts: ${response.status}`, response.status);
|
|
357
|
-
}
|
|
358
|
-
const data = await response.json();
|
|
359
|
-
// Backend returns array directly, not wrapped in {drafts: ...}
|
|
360
|
-
return Array.isArray(data) ? data : data.drafts || [];
|
|
108
|
+
export async function getStyles(): Promise<WritingStyle[]> {
|
|
109
|
+
return apiFetch<WritingStyle[]>("/api/v1/writing-styles/");
|
|
361
110
|
}
|
|
362
111
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
*/
|
|
366
|
-
export async function fetchDraft(sessionId: string, draftId: string): Promise<Draft> {
|
|
367
|
-
if (!isValidSessionId(sessionId)) {
|
|
368
|
-
throw new ApiError("Invalid session ID format", 400);
|
|
369
|
-
}
|
|
370
|
-
const token = await getAuthToken();
|
|
371
|
-
const response = await fetch(
|
|
372
|
-
`${API_BASE}/api/v1/sessions/${sessionId}/drafts/${draftId}/nodes?mode=full`,
|
|
373
|
-
{ headers: { Authorization: `Bearer ${token}` } },
|
|
374
|
-
);
|
|
375
|
-
if (!response.ok) {
|
|
376
|
-
throw new ApiError(`Failed to fetch draft: ${response.status}`, response.status);
|
|
377
|
-
}
|
|
378
|
-
return response.json();
|
|
112
|
+
export async function getWorkspaces(): Promise<Workspace[]> {
|
|
113
|
+
return apiFetch<Workspace[]>("/api/v1/workspaces/");
|
|
379
114
|
}
|
|
380
115
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
sessionId: string,
|
|
386
|
-
draftId: string,
|
|
387
|
-
content: string,
|
|
388
|
-
title?: string,
|
|
389
|
-
): Promise<Draft> {
|
|
390
|
-
if (!isValidSessionId(sessionId)) {
|
|
391
|
-
throw new ApiError("Invalid session ID format", 400);
|
|
392
|
-
}
|
|
393
|
-
const token = await getAuthToken();
|
|
394
|
-
const response = await fetch(`${API_BASE}/api/v1/sessions/${sessionId}/drafts/${draftId}`, {
|
|
395
|
-
method: "PUT",
|
|
396
|
-
headers: {
|
|
397
|
-
Authorization: `Bearer ${token}`,
|
|
398
|
-
"Content-Type": "application/json",
|
|
399
|
-
},
|
|
400
|
-
body: JSON.stringify({ content, title }),
|
|
401
|
-
});
|
|
402
|
-
if (!response.ok) {
|
|
403
|
-
throw new ApiError(`Failed to update draft: ${response.status}`, response.status);
|
|
404
|
-
}
|
|
405
|
-
return response.json();
|
|
116
|
+
export async function getConversations(): Promise<{
|
|
117
|
+
sessions: Conversation[];
|
|
118
|
+
}> {
|
|
119
|
+
return apiFetch<{ sessions: Conversation[] }>("/api/v1/conversations");
|
|
406
120
|
}
|
|
407
121
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
*/
|
|
411
|
-
export async function fetchDraftVersions(
|
|
412
|
-
sessionId: string,
|
|
413
|
-
draftId: string,
|
|
414
|
-
): Promise<DraftVersion[]> {
|
|
415
|
-
if (!isValidSessionId(sessionId)) {
|
|
416
|
-
throw new ApiError("Invalid session ID format", 400);
|
|
417
|
-
}
|
|
418
|
-
const token = await getAuthToken();
|
|
419
|
-
const response = await fetch(
|
|
420
|
-
`${API_BASE}/api/v1/sessions/${sessionId}/drafts/${draftId}/versions`,
|
|
421
|
-
{ headers: { Authorization: `Bearer ${token}` } },
|
|
422
|
-
);
|
|
423
|
-
if (!response.ok) {
|
|
424
|
-
throw new ApiError(`Failed to fetch versions: ${response.status}`, response.status);
|
|
425
|
-
}
|
|
426
|
-
const data = await response.json();
|
|
427
|
-
return data.versions || [];
|
|
122
|
+
export async function getSessionDrafts(sessionId: string): Promise<Draft[]> {
|
|
123
|
+
return apiFetch<Draft[]>(`/api/v1/sessions/${sessionId}/drafts`);
|
|
428
124
|
}
|
|
429
125
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
*/
|
|
433
|
-
export async function restoreDraftVersion(
|
|
434
|
-
sessionId: string,
|
|
435
|
-
draftId: string,
|
|
436
|
-
versionId: string,
|
|
437
|
-
): Promise<Draft> {
|
|
438
|
-
if (!isValidSessionId(sessionId)) {
|
|
439
|
-
throw new ApiError("Invalid session ID format", 400);
|
|
440
|
-
}
|
|
441
|
-
const token = await getAuthToken();
|
|
442
|
-
const response = await fetch(
|
|
443
|
-
`${API_BASE}/api/v1/sessions/${sessionId}/drafts/${draftId}/restore/${versionId}`,
|
|
444
|
-
{
|
|
445
|
-
method: "POST",
|
|
446
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
447
|
-
},
|
|
448
|
-
);
|
|
449
|
-
if (!response.ok) {
|
|
450
|
-
throw new ApiError(`Failed to restore version: ${response.status}`, response.status);
|
|
451
|
-
}
|
|
452
|
-
return response.json();
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Apply patch operations to a draft
|
|
457
|
-
*/
|
|
458
|
-
export async function applyDraftPatch(
|
|
459
|
-
sessionId: string,
|
|
460
|
-
draftId: string,
|
|
461
|
-
operations: PatchOperation[],
|
|
462
|
-
): Promise<Draft> {
|
|
463
|
-
if (!isValidSessionId(sessionId)) {
|
|
464
|
-
throw new ApiError("Invalid session ID format", 400);
|
|
465
|
-
}
|
|
466
|
-
const token = await getAuthToken();
|
|
467
|
-
const response = await fetch(
|
|
468
|
-
`${API_BASE}/api/v1/sessions/${sessionId}/drafts/${draftId}/apply-patch`,
|
|
469
|
-
{
|
|
470
|
-
method: "POST",
|
|
471
|
-
headers: {
|
|
472
|
-
Authorization: `Bearer ${token}`,
|
|
473
|
-
"Content-Type": "application/json",
|
|
474
|
-
},
|
|
475
|
-
body: JSON.stringify({ operations }),
|
|
476
|
-
},
|
|
477
|
-
);
|
|
478
|
-
if (!response.ok) {
|
|
479
|
-
throw new ApiError(`Failed to apply patch: ${response.status}`, response.status);
|
|
480
|
-
}
|
|
481
|
-
return response.json();
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// ============================================================================
|
|
485
|
-
// Writing Styles API
|
|
486
|
-
// ============================================================================
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Fetch all writing styles
|
|
490
|
-
*/
|
|
491
|
-
export async function fetchWritingStyles(): Promise<WritingStyle[]> {
|
|
492
|
-
const token = await getAuthToken();
|
|
493
|
-
const response = await fetch(`${API_BASE}/api/v1/writing-styles/`, {
|
|
494
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
495
|
-
});
|
|
496
|
-
if (!response.ok) {
|
|
497
|
-
throw new ApiError(`Failed to fetch writing styles: ${response.status}`, response.status);
|
|
498
|
-
}
|
|
499
|
-
const data = await response.json();
|
|
500
|
-
return data.styles || data || [];
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// ============================================================================
|
|
504
|
-
// Workspaces API
|
|
505
|
-
// ============================================================================
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Fetch all workspaces
|
|
509
|
-
*/
|
|
510
|
-
export async function fetchWorkspaces(): Promise<Workspace[]> {
|
|
511
|
-
const token = await getAuthToken();
|
|
512
|
-
const response = await fetch(`${API_BASE}/api/v1/workspaces/`, {
|
|
513
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
514
|
-
});
|
|
515
|
-
if (!response.ok) {
|
|
516
|
-
throw new ApiError(`Failed to fetch workspaces: ${response.status}`, response.status);
|
|
517
|
-
}
|
|
518
|
-
const data = await response.json();
|
|
519
|
-
return data.workspaces || data || [];
|
|
126
|
+
export async function getPrime(): Promise<string> {
|
|
127
|
+
return apiFetch<string>("/api/v1/prime", { public: true });
|
|
520
128
|
}
|