11/* ──────────────────────────────────────────────
2- * Extractor – Grok (grok.com / x.com/i/grok )
2+ * Extractor – Grok (grok.com)
33 *
4- * Uses X/Twitter infrastructure.
5- * Requires x-csrf-token from ct0 cookie.
4+ * Endpoint: /rest/app-chat/conversations
5+ * Auth: cookie-based (sso + sso-rw). No CSRF header required.
6+ * Pagination: pageSize + cursor (X/Twitter API style).
67 * ────────────────────────────────────────────── */
78
89import type { Extractor } from "./base" ;
910import type { Message , Thread } from "../types/schema" ;
1011import { uuid , jitteredDelay } from "../utils/helpers" ;
1112
12- const BASE = "" ; // Rely on relative path since we inject into either grok.com or x.com
13-
14- // In browser context content-scripts, getting cookies dynamically is harder via chrome.cookies unless it's sent from BG.
15- // However, since we are doing a same-origin fetch from the content-script,
16- // the browser handles `credentials` and cookies automatically for most cases.
17- // We still need the CSRF/ct0 header if required.
18- function getCsrfFromDocument ( ) : string {
19- const match = document . cookie . match ( / (?: ^ | ; \s * ) c t 0 = ( [ ^ ; ] * ) / ) ;
20- if ( ! match ) throw new Error ( "Grok: ct0 CSRF cookie not found in document.cookie" ) ;
21- return match [ 1 ] ;
22- }
13+ const BASE = "" ;
14+ const PAGE_SIZE = 60 ;
2315
2416async function apiFetch < T > ( path : string ) : Promise < T > {
25- const csrf = getCsrfFromDocument ( ) ;
2617 const res = await fetch ( `${ BASE } ${ path } ` , {
18+ credentials : "include" ,
2719 headers : {
2820 "Content-Type" : "application/json" ,
29- "x-csrf-token" : csrf ,
3021 } ,
3122 } ) ;
3223 if ( ! res . ok ) {
@@ -40,85 +31,132 @@ async function apiFetch<T>(path: string): Promise<T> {
4031/* ── raw response shapes ──────────────────────────────────── */
4132
4233interface RawConvListItem {
43- id : string ;
34+ conversationId : string ;
4435 title ?: string ;
45- created_at : string ;
46- updated_at : string ;
36+ createTime ?: string ;
37+ modifyTime ?: string ;
38+ // fallback snake_case (older API versions)
39+ id ?: string ;
40+ created_at ?: string ;
41+ updated_at ?: string ;
4742}
4843
44+ /** The list endpoint returns either a plain array or a wrapped object. */
45+ type RawConvListResponse =
46+ | RawConvListItem [ ]
47+ | { conversations : RawConvListItem [ ] ; nextCursor ?: string ; cursor ?: string } ;
48+
4949interface RawGrokMessage {
50- id : string ;
51- role : "user" | "assistant" ;
52- text : string ;
53- created_at : string ;
50+ id ?: string ;
51+ messageId ?: string ;
52+ sender ?: "human" | "assistant" ;
53+ role ?: "user" | "assistant" ;
54+ message ?: string ;
55+ text ?: string ;
56+ createTime ?: string ;
57+ created_at ?: string ;
5458 model ?: string ;
5559}
5660
5761interface RawConversation {
58- id : string ;
62+ conversationId ?: string ;
63+ id ?: string ;
5964 title ?: string ;
60- created_at : string ;
61- updated_at : string ;
62- messages : RawGrokMessage [ ] ;
65+ createTime ?: string ;
66+ modifyTime ?: string ;
67+ created_at ?: string ;
68+ updated_at ?: string ;
69+ responses ?: RawGrokMessage [ ] ;
70+ messages ?: RawGrokMessage [ ] ;
6371}
6472
6573/* ── helpers ──────────────────────────────────────────────── */
6674
75+ function normaliseItem ( item : RawConvListItem ) : { providerId : string ; title : string ; createdAt : string ; updatedAt : string } {
76+ return {
77+ providerId : item . conversationId ?? item . id ?? "" ,
78+ title : item . title || "Untitled" ,
79+ createdAt : item . createTime ?? item . created_at ?? "" ,
80+ updatedAt : item . modifyTime ?? item . updated_at ?? item . createTime ?? item . created_at ?? "" ,
81+ } ;
82+ }
83+
6784function mapMessages ( raw : RawGrokMessage [ ] ) : Message [ ] {
68- return raw . map ( ( m ) => ( {
69- id : m . id ,
70- role : m . role ,
71- content : m . text ,
72- createdAt : m . created_at ,
73- model : m . model ,
74- } ) ) ;
85+ return raw . map ( ( m ) => {
86+ const role : "user" | "assistant" =
87+ m . role === "user" || m . sender === "human" ? "user" : "assistant" ;
88+ return {
89+ id : m . messageId ?? m . id ?? uuid ( ) ,
90+ role,
91+ content : m . message ?? m . text ?? "" ,
92+ createdAt : m . createTime ?? m . created_at ?? "" ,
93+ model : m . model ,
94+ } ;
95+ } ) ;
7596}
7697
7798/* ── Extractor implementation ─────────────────────────────── */
7899
79100export const grokExtractor : Extractor = {
80101 async listConversations ( ) {
81102 const all : { providerId : string ; title : string ; createdAt : string ; updatedAt : string } [ ] = [ ] ;
82- let offset = 0 ;
83- const limit = 50 ;
103+ let cursor : string | undefined ;
84104
85105 // eslint-disable-next-line no-constant-condition
86106 while ( true ) {
87- const list = await apiFetch < RawConvListItem [ ] > (
88- `/rest/api/conversations?limit=${ limit } &offset=${ offset } `
107+ const qs = cursor
108+ ? `pageSize=${ PAGE_SIZE } &cursor=${ encodeURIComponent ( cursor ) } `
109+ : `pageSize=${ PAGE_SIZE } ` ;
110+
111+ const raw = await apiFetch < RawConvListResponse > (
112+ `/rest/app-chat/conversations?${ qs } `
89113 ) ;
90- if ( ! list . length ) break ;
91-
92- for ( const item of list ) {
93- all . push ( {
94- providerId : item . id ,
95- title : item . title || "Untitled" ,
96- createdAt : item . created_at ,
97- updatedAt : item . updated_at ,
98- } ) ;
114+
115+ let items : RawConvListItem [ ] ;
116+ let nextCursor : string | undefined ;
117+
118+ if ( Array . isArray ( raw ) ) {
119+ items = raw ;
120+ } else {
121+ items = raw . conversations ?? [ ] ;
122+ nextCursor = raw . nextCursor ?? raw . cursor ;
123+ }
124+
125+ if ( ! items . length ) break ;
126+
127+ for ( const item of items ) {
128+ all . push ( normaliseItem ( item ) ) ;
99129 }
100130
101- offset += limit ;
102- if ( list . length < limit ) break ;
103- await jitteredDelay ( 500 , 1200 ) ;
131+ if ( nextCursor ) {
132+ cursor = nextCursor ;
133+ await jitteredDelay ( 500 , 1200 ) ;
134+ } else if ( items . length < PAGE_SIZE ) {
135+ break ;
136+ } else {
137+ // No cursor returned but page was full — stop to avoid infinite loop.
138+ break ;
139+ }
104140 }
105141
106142 return all ;
107143 } ,
108144
109145 async fetchThread ( conversationId : string ) : Promise < Thread > {
110146 const raw = await apiFetch < RawConversation > (
111- `/rest/api /conversations/${ conversationId } `
147+ `/rest/app-chat /conversations/${ conversationId } `
112148 ) ;
113149
150+ const messages = mapMessages ( raw . responses ?? raw . messages ?? [ ] ) ;
151+
114152 return {
115153 id : uuid ( ) ,
116154 providerId : conversationId ,
117155 provider : "grok" ,
118156 title : raw . title || "Untitled" ,
119- createdAt : raw . created_at ,
120- updatedAt : raw . updated_at ,
121- messages : mapMessages ( raw . messages ?? [ ] ) ,
157+ createdAt : raw . createTime ?? raw . created_at ?? "" ,
158+ updatedAt : raw . modifyTime ?? raw . updated_at ?? raw . createTime ?? raw . created_at ?? "" ,
159+ messages,
122160 tags : [ ] ,
123161 } ;
124162 } ,
0 commit comments