11/* ──────────────────────────────────────────────
22 * Extractor – Grok (grok.com)
33 *
4- * Endpoint: /rest/app-chat/conversations
4+ * Three-step fetch per conversation:
5+ * 1. LIST GET /rest/app-chat/conversations?pageSize=60[&cursor=…]
6+ * 2. IDs GET /rest/app-chat/conversations/{id}/response-node?includeThreads=true
7+ * 3. BODY POST /rest/app-chat/conversations/{id}/load-responses
8+ * body: { responseIds: string[] }
9+ *
510 * Auth: cookie-based (sso + sso-rw). No CSRF header required.
6- * Pagination: pageSize + cursor (X/Twitter API style ).
11+ * Pagination: cursor-based (nextCursor field in response wrapper ).
712 * ────────────────────────────────────────────── */
813
914import type { Extractor } from "./base" ;
10- import type { Message , Thread } from "../types/schema" ;
15+ import type { Message , Thread , Role } from "../types/schema" ;
1116import { uuid , jitteredDelay } from "../utils/helpers" ;
1217
13- const BASE = "" ;
1418const PAGE_SIZE = 60 ;
1519
1620async function apiFetch < T > ( path : string ) : Promise < T > {
17- const res = await fetch ( ` ${ BASE } ${ path } ` , {
21+ const res = await fetch ( path , {
1822 credentials : "include" ,
19- headers : {
20- "Content-Type" : "application/json" ,
21- } ,
23+ headers : { "Content-Type" : "application/json" } ,
2224 } ) ;
2325 if ( ! res . ok ) {
24- throw new Error (
25- `Grok API ${ path } returned ${ res . status } : ${ res . statusText } `
26- ) ;
26+ throw new Error ( `Grok API GET ${ path } → ${ res . status } : ${ res . statusText } ` ) ;
27+ }
28+ return res . json ( ) as Promise < T > ;
29+ }
30+
31+ async function apiPost < T > ( path : string , body : unknown ) : Promise < T > {
32+ const res = await fetch ( path , {
33+ method : "POST" ,
34+ credentials : "include" ,
35+ headers : { "Content-Type" : "application/json" } ,
36+ body : JSON . stringify ( body ) ,
37+ } ) ;
38+ if ( ! res . ok ) {
39+ throw new Error ( `Grok API POST ${ path } → ${ res . status } : ${ res . statusText } ` ) ;
2740 }
2841 return res . json ( ) as Promise < T > ;
2942}
@@ -46,28 +59,57 @@ type RawConvListResponse =
4659 | RawConvListItem [ ]
4760 | { conversations : RawConvListItem [ ] ; nextCursor ?: string ; cursor ?: string } ;
4861
49- interface RawGrokMessage {
62+ /**
63+ * Shape returned by GET .../response-node?includeThreads=true
64+ *
65+ * Grok returns a flat list of nodes under `responseNodes`.
66+ * Each node has a `responseId` and optional `parentResponseId`.
67+ * Older API versions may return a tree via `children`/`nodes`.
68+ */
69+ interface RawResponseNode {
70+ responseId ?: string ;
5071 id ?: string ;
72+ sender ?: string ;
73+ parentResponseId ?: string ;
74+ children ?: RawResponseNode [ ] ;
75+ nodes ?: RawResponseNode [ ] ;
76+ responses ?: RawResponseNode [ ] ;
77+ }
78+
79+ interface RawResponseNodeResult {
80+ responseNodes ?: RawResponseNode [ ] ;
81+ // legacy / alternate shapes
82+ nodes ?: RawResponseNode [ ] ;
83+ }
84+
85+ /** Shape of each item returned by POST .../load-responses */
86+ interface RawLoadedMessage {
87+ responseId ?: string ;
5188 messageId ?: string ;
89+ id ?: string ;
5290 sender ?: "human" | "assistant" ;
5391 role ?: "user" | "assistant" ;
5492 message ?: string ;
5593 text ?: string ;
94+ query ?: string ; // user turn (some versions)
95+ response ?: string ; // assistant turn (some versions)
5696 createTime ?: string ;
5797 created_at ?: string ;
5898 model ?: string ;
5999}
60100
61- interface RawConversation {
101+ type RawLoadResponsesResult =
102+ | RawLoadedMessage [ ]
103+ | { responses : RawLoadedMessage [ ] } ;
104+
105+ interface RawConvMeta {
62106 conversationId ?: string ;
63107 id ?: string ;
64108 title ?: string ;
65109 createTime ?: string ;
66110 modifyTime ?: string ;
67111 created_at ?: string ;
68112 updated_at ?: string ;
69- responses ?: RawGrokMessage [ ] ;
70- messages ?: RawGrokMessage [ ] ;
71113}
72114
73115/* ── helpers ──────────────────────────────────────────────── */
@@ -81,18 +123,38 @@ function normaliseItem(item: RawConvListItem): { providerId: string; title: stri
81123 } ;
82124}
83125
84- function mapMessages ( raw : RawGrokMessage [ ] ) : Message [ ] {
85- return raw . map ( ( m ) => {
86- const role : "user" | "assistant" =
126+ /** Recursively collect all responseId values from a node tree. */
127+ function collectResponseIds ( node : RawResponseNode ) : string [ ] {
128+ const ids : string [ ] = [ ] ;
129+ const id = node . responseId ?? node . id ;
130+ if ( id ) ids . push ( id ) ;
131+ const children = node . children ?? node . nodes ?? node . responses ?? [ ] ;
132+ for ( const child of children ) {
133+ ids . push ( ...collectResponseIds ( child ) ) ;
134+ }
135+ return ids ;
136+ }
137+
138+ function mapMessages ( raw : RawLoadedMessage [ ] ) : Message [ ] {
139+ const out : Message [ ] = [ ] ;
140+ for ( const m of raw ) {
141+ const role : Role =
87142 m . role === "user" || m . sender === "human" ? "user" : "assistant" ;
88- return {
89- id : m . messageId ?? m . id ?? uuid ( ) ,
143+ const content =
144+ m . message ??
145+ m . text ??
146+ ( role === "user" ? m . query : m . response ) ??
147+ "" ;
148+ if ( ! content ) continue ;
149+ out . push ( {
150+ id : m . responseId ?? m . messageId ?? m . id ?? uuid ( ) ,
90151 role,
91- content : m . message ?? m . text ?? "" ,
152+ content,
92153 createdAt : m . createTime ?? m . created_at ?? "" ,
93154 model : m . model ,
94- } ;
95- } ) ;
155+ } ) ;
156+ }
157+ return out ;
96158}
97159
98160/* ── Extractor implementation ─────────────────────────────── */
@@ -102,7 +164,6 @@ export const grokExtractor: Extractor = {
102164 const all : { providerId : string ; title : string ; createdAt : string ; updatedAt : string } [ ] = [ ] ;
103165 let cursor : string | undefined ;
104166
105- // eslint-disable-next-line no-constant-condition
106167 while ( true ) {
107168 const qs = cursor
108169 ? `pageSize=${ PAGE_SIZE } &cursor=${ encodeURIComponent ( cursor ) } `
@@ -131,10 +192,7 @@ export const grokExtractor: Extractor = {
131192 if ( nextCursor ) {
132193 cursor = nextCursor ;
133194 await jitteredDelay ( 500 , 1200 ) ;
134- } else if ( items . length < PAGE_SIZE ) {
135- break ;
136195 } else {
137- // No cursor returned but page was full — stop to avoid infinite loop.
138196 break ;
139197 }
140198 }
@@ -143,19 +201,56 @@ export const grokExtractor: Extractor = {
143201 } ,
144202
145203 async fetchThread ( conversationId : string ) : Promise < Thread > {
146- const raw = await apiFetch < RawConversation > (
204+ // Step 1: fetch conversation metadata (title, timestamps)
205+ const meta = await apiFetch < RawConvMeta > (
147206 `/rest/app-chat/conversations/${ conversationId } `
148207 ) ;
149208
150- const messages = mapMessages ( raw . responses ?? raw . messages ?? [ ] ) ;
209+ // Step 2: fetch the response-node list to collect all response IDs
210+ const nodeResult = await apiFetch < RawResponseNodeResult | RawResponseNode [ ] > (
211+ `/rest/app-chat/conversations/${ conversationId } /response-node?includeThreads=true`
212+ ) ;
213+
214+ let responseIds : string [ ] ;
215+ if ( Array . isArray ( nodeResult ) ) {
216+ // legacy: root is a plain array
217+ responseIds = nodeResult . flatMap ( collectResponseIds ) ;
218+ } else {
219+ // current API: { responseNodes: [...] }
220+ const flatNodes =
221+ ( nodeResult as RawResponseNodeResult ) . responseNodes ??
222+ ( nodeResult as RawResponseNodeResult ) . nodes ??
223+ [ ] ;
224+ responseIds = flatNodes . flatMap ( collectResponseIds ) ;
225+ }
226+
227+ // Step 3: load full message bodies
228+ let messages : Message [ ] = [ ] ;
229+ if ( responseIds . length > 0 ) {
230+ const loaded = await apiPost < RawLoadResponsesResult > (
231+ `/rest/app-chat/conversations/${ conversationId } /load-responses` ,
232+ { responseIds }
233+ ) ;
234+
235+ const rawMessages = Array . isArray ( loaded )
236+ ? loaded
237+ : ( loaded . responses ?? [ ] ) ;
238+
239+ messages = mapMessages ( rawMessages ) ;
240+ }
151241
152242 return {
153243 id : uuid ( ) ,
154244 providerId : conversationId ,
155245 provider : "grok" ,
156- title : raw . title || "Untitled" ,
157- createdAt : raw . createTime ?? raw . created_at ?? "" ,
158- updatedAt : raw . modifyTime ?? raw . updated_at ?? raw . createTime ?? raw . created_at ?? "" ,
246+ title : meta . title || "Untitled" ,
247+ createdAt : meta . createTime ?? meta . created_at ?? "" ,
248+ updatedAt :
249+ meta . modifyTime ??
250+ meta . updated_at ??
251+ meta . createTime ??
252+ meta . created_at ??
253+ "" ,
159254 messages,
160255 tags : [ ] ,
161256 } ;
0 commit comments