@miruzero/zgpt 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.
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const auth = require('../auth');
5
+ const paths = require('../config/paths');
6
+ const read = require('./read');
7
+ const write = require('./write');
8
+ const temporary = require('./temporary');
9
+
10
+ const sessionHeaders = (env) => {
11
+ const file = paths.sessionFile(env);
12
+ let state;
13
+ try { state = JSON.parse(fs.readFileSync(file, 'utf8')); }
14
+ catch {
15
+ throw Object.assign(new Error('ChatGPT session context required'), { code: 'SESSION_REQUIRED' });
16
+ }
17
+ if (!state?.headers || typeof state.headers !== 'object' || Array.isArray(state.headers)) {
18
+ throw Object.assign(new Error('ChatGPT session context required'), { code: 'SESSION_REQUIRED' });
19
+ }
20
+ return { ...state.headers };
21
+ };
22
+
23
+ const bindCanonicalContext = async (options = {}) => {
24
+ const resolved = { ...options };
25
+ const env = resolved.env || process.env;
26
+ if (!resolved.token) {
27
+ const current = await auth.ensure({ env, acquireSession: false });
28
+ resolved.token = current.accessToken;
29
+ }
30
+ if (!resolved.sessionHeaders) resolved.sessionHeaders = sessionHeaders(env);
31
+ return resolved;
32
+ };
33
+
34
+ Object.assign(exports, read, write);
35
+ exports.get = async (id, options = {}) => read.getConversation({
36
+ conversationId: id,
37
+ ...(await bindCanonicalContext(options))
38
+ });
39
+ exports.streamStatus = async (id, options = {}) => read.getConversationStreamStatus({
40
+ conversationId: id,
41
+ ...(await bindCanonicalContext(options))
42
+ });
43
+ exports.resume = async (id, options = {}) => read.resumeConversation({
44
+ conversationId: id,
45
+ ...(await bindCanonicalContext(options))
46
+ });
47
+ exports.send = async (id, message, options = {}) => write.sendConversation({
48
+ conversationId: id,
49
+ message,
50
+ ...(await bindCanonicalContext(options))
51
+ });
52
+ exports.temp = {
53
+ new: async (message, options = {}) => temporary.createConversation({
54
+ message,
55
+ ...(await bindCanonicalContext(options))
56
+ }),
57
+ send: async (id, message, options = {}) => temporary.sendConversation({
58
+ conversationId: id,
59
+ message,
60
+ ...(await bindCanonicalContext(options))
61
+ })
62
+ };
63
+
64
+ exports.attachments = require('./attachments');
65
+ exports.model = require('./model');
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ const direct = require('../transport/direct/http-client');
4
+
5
+ const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
6
+ let state = { models: [], defaultModelSlug: null, refreshedAt: 0 };
7
+
8
+ const clone = (value) => value == null ? value : structuredClone(value);
9
+ const cookieString = (headers = {}) => headers.Cookie || headers.cookie || '';
10
+ const parseCookies = (headers = {}) => {
11
+ const out = new Map();
12
+ for (const pair of String(cookieString(headers)).split(';')) {
13
+ const index = pair.indexOf('=');
14
+ if (index <= 0) continue;
15
+ out.set(pair.slice(0, index).trim(), pair.slice(index + 1).trim());
16
+ }
17
+ return out;
18
+ };
19
+ const parseJsonCookie = (cookies, name) => {
20
+ const raw = cookies.get(name);
21
+ if (!raw) return null;
22
+ try { return JSON.parse(decodeURIComponent(raw)); }
23
+ catch { return null; }
24
+ };
25
+
26
+ const sessionPreference = (headers = {}) => {
27
+ const cookies = parseCookies(headers);
28
+ const last = parseJsonCookie(cookies, 'oai-last-model-config');
29
+ if (typeof last?.model === 'string' && last.model) {
30
+ return { model: last.model, thinkingEffort: typeof last.effort === 'string' ? last.effort : null };
31
+ }
32
+ const tpp = parseJsonCookie(cookies, 'oai-tpp-model-settings');
33
+ const model = tpp?.modelSettings?.lastUsedModelSlug;
34
+ if (typeof model === 'string' && model) {
35
+ return { model, thinkingEffort: typeof tpp?.thinkingEffort === 'string' ? tpp.thinkingEffort : null };
36
+ }
37
+ return { model: null, thinkingEffort: null };
38
+ };
39
+
40
+ const normalizeModel = (item) => {
41
+ const id = typeof item?.slug === 'string' && item.slug.trim()
42
+ ? item.slug.trim()
43
+ : typeof item?.id === 'string' && item.id.trim()
44
+ ? item.id.trim()
45
+ : null;
46
+ return id ? { ...clone(item), id, slug: item.slug || id, backendModel: id } : null;
47
+ };
48
+ const allowedEfforts = (model) => (Array.isArray(model?.thinking_efforts) ? model.thinking_efforts : [])
49
+ .map((item) => typeof item === 'string' ? item : item?.thinking_effort)
50
+ .filter((item) => typeof item === 'string' && item);
51
+ const validEffort = (model, effort) => {
52
+ if (!effort) return null;
53
+ const allowed = allowedEfforts(model);
54
+ return allowed.length === 0 || allowed.includes(effort) ? effort : null;
55
+ };
56
+
57
+ exports.clearCache = () => { state = { models: [], defaultModelSlug: null, refreshedAt: 0 }; };
58
+ exports.list = () => state.models.map(clone);
59
+ exports.get = (id) => clone(state.models.find((item) => item.id === id) || null);
60
+ exports.sessionPreference = sessionPreference;
61
+
62
+ exports.refresh = async ({
63
+ token,
64
+ sessionHeaders = {},
65
+ requestJson = direct.requestJson,
66
+ cacheTtlMs = DEFAULT_CACHE_TTL_MS,
67
+ force = false
68
+ } = {}) => {
69
+ const fresh = state.models.length && (Date.now() - state.refreshedAt) < Math.max(0, cacheTtlMs);
70
+ if (!force && fresh) return { refreshed: false, cached: true, count: state.models.length, defaultModelSlug: state.defaultModelSlug };
71
+ if (!token || typeof requestJson !== 'function') throw Object.assign(new Error('ChatGPT model catalog context required'), { code: 'MODEL_CONTEXT_REQUIRED' });
72
+ const target = '/backend-api/models';
73
+ const response = await requestJson(target, token, sessionHeaders, { route: target, retry: false });
74
+ if (response?.status !== 200 || !Array.isArray(response.json?.models) || response.json.models.length === 0) {
75
+ throw Object.assign(new Error(`ChatGPT model catalog request failed (${response?.status || 'invalid'})`), { code: 'MODEL_CATALOG_UNAVAILABLE', status: response?.status || null });
76
+ }
77
+ const models = response.json.models.map(normalizeModel).filter(Boolean);
78
+ if (!models.length) throw Object.assign(new Error('ChatGPT model catalog is empty'), { code: 'MODEL_CATALOG_EMPTY' });
79
+ state = {
80
+ models,
81
+ defaultModelSlug: typeof response.json.default_model_slug === 'string' ? response.json.default_model_slug : null,
82
+ refreshedAt: Date.now()
83
+ };
84
+ return { refreshed: true, cached: false, count: models.length, defaultModelSlug: state.defaultModelSlug };
85
+ };
86
+
87
+ exports.resolve = async ({
88
+ requestedModel = 'auto',
89
+ requestedThinkingEffort = null,
90
+ initDefaultModel = null,
91
+ token,
92
+ sessionHeaders = {},
93
+ requestJson = direct.requestJson,
94
+ cacheTtlMs = DEFAULT_CACHE_TTL_MS
95
+ } = {}) => {
96
+ await exports.refresh({ token, sessionHeaders, requestJson, cacheTtlMs });
97
+ const requested = requestedModel && requestedModel !== 'auto' ? exports.get(requestedModel) : null;
98
+ const preference = sessionPreference(sessionHeaders);
99
+ const preferred = preference.model ? exports.get(preference.model) : null;
100
+ const initialized = initDefaultModel ? exports.get(initDefaultModel) : null;
101
+ const catalogDefault = state.defaultModelSlug ? exports.get(state.defaultModelSlug) : null;
102
+ const selected = requested || preferred || initialized || catalogDefault || exports.list()[0] || null;
103
+ if (!selected) throw Object.assign(new Error('No ChatGPT model is available'), { code: 'MODEL_NOT_FOUND' });
104
+ const source = requested ? 'requested' : preferred ? 'session-last-used' : initialized ? 'conversation-init' : catalogDefault ? 'catalog-default' : 'catalog-first';
105
+ const preferredEffort = source === 'session-last-used' ? preference.thinkingEffort : null;
106
+ const efforts = allowedEfforts(selected);
107
+ const thinkingEffort = validEffort(selected, requestedThinkingEffort)
108
+ || validEffort(selected, preferredEffort)
109
+ || (efforts.length ? efforts[0] : null);
110
+ return {
111
+ model: selected.id,
112
+ thinkingEffort,
113
+ source,
114
+ requestedValid: Boolean(requested),
115
+ catalogModel: selected
116
+ };
117
+ };
@@ -0,0 +1,378 @@
1
+ 'use strict';
2
+
3
+ const direct = require('../transport/direct/http-client');
4
+
5
+ const requireOk = (response, target) => {
6
+ if (!response || response.status !== 200 || !response.json) {
7
+ const error = new Error(`ChatGPT request failed (${response?.status || 'no-status'}): ${target}`);
8
+ error.status = response?.status || null;
9
+ error.target = target;
10
+ error.retryAfterMs = direct.parseRetryAfterMs(response?.headers || {});
11
+ if (error.status === 429) error.code = 'RATE_LIMITED';
12
+ throw error;
13
+ }
14
+ return response.json;
15
+ };
16
+
17
+ const requireSuccess = (response, target) => {
18
+ if (!response || response.status < 200 || response.status >= 300) {
19
+ const error = new Error(`ChatGPT request failed (${response?.status || 'no-status'}): ${target}`);
20
+ error.status = response?.status || null;
21
+ error.target = target;
22
+ error.retryAfterMs = direct.parseRetryAfterMs(response?.headers || {});
23
+ if (error.status === 429) error.code = 'RATE_LIMITED';
24
+ throw error;
25
+ }
26
+ return response.json || { success: true };
27
+ };
28
+
29
+ exports.listConversationPage = async ({
30
+ token,
31
+ sessionHeaders,
32
+ offset = 0,
33
+ limit = 50,
34
+ requestJson = direct.requestJson,
35
+ onProgress = () => {}
36
+ }) => {
37
+ const target = `/backend-api/conversations?offset=${offset}&limit=${limit}&order=updated&is_archived=false&is_starred=false`;
38
+ const page = requireOk(await requestJson(target, token, sessionHeaders), target);
39
+ const items = Array.isArray(page.items) ? page.items : [];
40
+ const totalValue = Number(page.total);
41
+ const total = Number.isFinite(totalValue) ? totalValue : items.length;
42
+ const pageLimit = Number(page.limit) || limit;
43
+ const pageOffset = Number(page.offset) || offset;
44
+ const nextOffset = items.length > 0 && pageOffset + pageLimit < total
45
+ ? pageOffset + pageLimit
46
+ : null;
47
+ onProgress({ type: 'conversation-page', count: items.length, pageCount: items.length, offset: pageOffset, total });
48
+ return { items, total, limit: pageLimit, offset: pageOffset, nextOffset };
49
+ };
50
+
51
+ exports.listStandalone = async ({
52
+ token,
53
+ sessionHeaders,
54
+ requestJson = direct.requestJson,
55
+ onProgress = () => {}
56
+ }) => {
57
+ const items = [];
58
+ let offset = 0;
59
+ const limit = 50;
60
+
61
+ for (let pageNumber = 0; pageNumber < 1000; pageNumber += 1) {
62
+ const target = `/backend-api/conversations?offset=${offset}&limit=${limit}&order=updated&is_archived=false&is_starred=false`;
63
+ const page = requireOk(await requestJson(target, token, sessionHeaders), target);
64
+ const pageItems = Array.isArray(page.items) ? page.items : [];
65
+ items.push(...pageItems);
66
+ onProgress({ type: 'conversation-page', count: items.length, pageCount: pageItems.length, offset });
67
+
68
+ const total = Number(page.total);
69
+ const pageLimit = Number(page.limit) || limit;
70
+ offset += pageLimit;
71
+ if (!Number.isFinite(total) || offset >= total || pageItems.length === 0) return items;
72
+ }
73
+ throw Object.assign(new Error('conversation pagination exceeded 1000 pages'), { code: 'PAGINATION_LIMIT' });
74
+ };
75
+ exports.searchGlobal = async ({
76
+ query,
77
+ cursor = null,
78
+ limit = 20,
79
+ source = 'conversation',
80
+ token,
81
+ sessionHeaders,
82
+ requestJson = direct.requestJson
83
+ }) => {
84
+ const normalized = String(query || '').trim();
85
+ if (!normalized) throw new Error('query is required');
86
+ const params = new URLSearchParams({ query: normalized, limit: String(limit) });
87
+ if (cursor) params.set('cursor', cursor);
88
+ if (source) params.set('sources', source);
89
+ const target = `/backend-api/global/search?${params.toString()}`;
90
+ return requireOk(await requestJson(target, token, sessionHeaders, {
91
+ route: '/backend-api/global/search'
92
+ }), target);
93
+ };
94
+ exports.getConversation = async ({
95
+ conversationId,
96
+ token,
97
+ sessionHeaders,
98
+ requestJson = direct.requestJson
99
+ }) => {
100
+ if (!conversationId) throw new Error('conversationId is required');
101
+ const target = `/backend-api/conversations/${encodeURIComponent(conversationId)}`;
102
+ return requireOk(await requestJson(target, token, sessionHeaders, {
103
+ route: '/backend-api/conversations/{conversation_id}'
104
+ }), target);
105
+ };
106
+
107
+ exports.getConversationStreamStatus = async ({
108
+ conversationId,
109
+ token,
110
+ sessionHeaders,
111
+ requestJson = direct.requestJson
112
+ }) => {
113
+ if (!conversationId) throw new Error('conversationId is required');
114
+ const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/stream_status`;
115
+ return requireOk(await requestJson(target, token, sessionHeaders, {
116
+ route: '/backend-api/conversation/{conversation_id}/stream_status'
117
+ }), target);
118
+ };
119
+
120
+ exports.resumeConversation = async ({
121
+ conversationId,
122
+ offset = 0,
123
+ conduitToken = null,
124
+ turnTraceId = null,
125
+ onChunk = null,
126
+ token,
127
+ sessionHeaders,
128
+ requestText = direct.requestText
129
+ }) => {
130
+ if (!conversationId) throw new Error('conversationId is required');
131
+ if (!Number.isSafeInteger(offset) || offset < 0) throw new Error('offset must be a non-negative integer');
132
+ const target = '/backend-api/f/conversation/resume';
133
+ const response = await requestText(target, token, sessionHeaders, {
134
+ method: 'POST',
135
+ body: { conversation_id: conversationId, offset },
136
+ route: target,
137
+ headers: {
138
+ accept: 'text/event-stream',
139
+ origin: sessionHeaders?.origin || sessionHeaders?.Origin || 'https://chatgpt.com',
140
+ 'x-openai-web-frontend': sessionHeaders?.['x-openai-web-frontend'] || 'core_web',
141
+ ...(conduitToken ? { 'x-conduit-token': conduitToken } : {}),
142
+ ...(turnTraceId ? { 'x-oai-turn-trace-id': turnTraceId } : {})
143
+ },
144
+ retry: false,
145
+ idleTimeoutMs: 60_000,
146
+ onChunk
147
+ });
148
+ if (!response || response.status < 200 || response.status >= 300) {
149
+ const error = new Error(`ChatGPT request failed (${response?.status || 'no-status'}): ${target}`);
150
+ error.status = response?.status || null;
151
+ error.target = target;
152
+ error.body = response?.text || null;
153
+ throw error;
154
+ }
155
+ return {
156
+ conversation_id: conversationId,
157
+ status: response.status,
158
+ content_type: response.contentType || null,
159
+ text: response.text || ''
160
+ };
161
+ };
162
+
163
+ exports.getConversationMessagesPage = async ({
164
+ conversationId, before, numTurns = 10, includeHasVersions = true,
165
+ token, sessionHeaders, requestJson = direct.requestJson
166
+ }) => {
167
+ if (!conversationId) throw new Error('conversationId is required');
168
+ if (!before) throw new Error('before is required');
169
+ const params = new URLSearchParams({
170
+ before: String(before),
171
+ include_has_versions: includeHasVersions ? 'true' : 'false',
172
+ num_turns: String(numTurns)
173
+ });
174
+ const target = `/backend-api/conversations/${encodeURIComponent(conversationId)}/messages?${params}`;
175
+ return requireOk(await requestJson(target, token, sessionHeaders, {
176
+ route: '/backend-api/conversations/{conversation_id}/messages'
177
+ }), target);
178
+ };
179
+ exports.findConversationMessage = async ({
180
+ conversationId, messageId, numTurns = 10, includeHasVersions = true,
181
+ token, sessionHeaders, requestJson = direct.requestJson,
182
+ getConversation = exports.getConversation, getMessagesPage = exports.getConversationMessagesPage
183
+ }) => {
184
+ if (!conversationId) throw new Error('conversationId is required');
185
+ if (!messageId) throw new Error('messageId is required');
186
+ const current = await getConversation({ conversationId, token, sessionHeaders, requestJson });
187
+ let pagesScanned = 1;
188
+ let nodesScanned = Array.isArray(current?.messages) ? current.messages.length : 0;
189
+ const currentMatch = Array.isArray(current?.messages) ? current.messages.find((item) => item?.id === messageId) : null;
190
+ if (currentMatch) return { found: true, conversation_id: conversationId, message_id: messageId, source: 'current_window', page_info: current?.page_info || null, message: currentMatch, pages_scanned: pagesScanned, nodes_scanned: nodesScanned };
191
+ let pageInfo = current?.page_info || {};
192
+ let before = pageInfo.start_cursor || null;
193
+ let hasPrevious = pageInfo.has_previous_page === true;
194
+ const seen = new Set();
195
+ while (hasPrevious && before) {
196
+ if (seen.has(before)) throw Object.assign(new Error('conversation history cursor stalled'), { code: 'PAGINATION_STALLED' });
197
+ seen.add(before);
198
+ const page = await getMessagesPage({ conversationId, before, numTurns, includeHasVersions, token, sessionHeaders, requestJson });
199
+ pagesScanned += 1;
200
+ const messages = Array.isArray(page?.messages) ? page.messages : [];
201
+ nodesScanned += messages.length;
202
+ const match = messages.find((item) => item?.id === messageId);
203
+ if (match) return { found: true, conversation_id: conversationId, message_id: messageId, source: 'historical_page', before, page_info: page?.page_info || null, message: match, pages_scanned: pagesScanned, nodes_scanned: nodesScanned };
204
+ pageInfo = page?.page_info || {};
205
+ hasPrevious = pageInfo.has_previous_page === true;
206
+ const nextBefore = pageInfo.start_cursor || null;
207
+ if (hasPrevious && (!nextBefore || nextBefore === before)) throw Object.assign(new Error('conversation history cursor stalled'), { code: 'PAGINATION_STALLED' });
208
+ before = nextBefore;
209
+ }
210
+ return { found: false, conversation_id: conversationId, message_id: messageId, source: null, page_info: pageInfo, message: null, pages_scanned: pagesScanned, nodes_scanned: nodesScanned };
211
+ };
212
+
213
+ exports.getConversationHistory = async ({
214
+ conversationId, numTurns = 10, includeHasVersions = true,
215
+ token, sessionHeaders, requestJson = direct.requestJson,
216
+ getConversation = exports.getConversation, getMessagesPage = exports.getConversationMessagesPage
217
+ }) => {
218
+ if (!conversationId) throw new Error('conversationId is required');
219
+ const current = await getConversation({ conversationId, token, sessionHeaders, requestJson });
220
+ const pages = [{ ...current, kind: 'current_window', before: null }];
221
+ let messageCount = Array.isArray(current?.messages) ? current.messages.length : 0;
222
+ let pageInfo = current?.page_info || {};
223
+ let before = pageInfo.start_cursor || null;
224
+ let hasPrevious = pageInfo.has_previous_page === true;
225
+ const seen = new Set();
226
+ while (hasPrevious && before) {
227
+ if (seen.has(before)) throw Object.assign(new Error('conversation history cursor stalled'), { code: 'PAGINATION_STALLED' });
228
+ seen.add(before);
229
+ const page = await getMessagesPage({ conversationId, before, numTurns, includeHasVersions, token, sessionHeaders, requestJson });
230
+ pages.push({ ...page, kind: 'historical_page', before });
231
+ messageCount += Array.isArray(page?.messages) ? page.messages.length : 0;
232
+ pageInfo = page?.page_info || {};
233
+ hasPrevious = pageInfo.has_previous_page === true;
234
+ const nextBefore = pageInfo.start_cursor || null;
235
+ if (hasPrevious && (!nextBefore || nextBefore === before)) throw Object.assign(new Error('conversation history cursor stalled'), { code: 'PAGINATION_STALLED' });
236
+ before = nextBefore;
237
+ }
238
+ return {
239
+ conversation_id: current?.conversation_id || current?.id || conversationId,
240
+ title: current?.title || null,
241
+ project_id: current?.gizmo_id || current?.conversation_template_id || null,
242
+ current_node: current?.current_node || null,
243
+ complete: true,
244
+ direction: 'current_to_older',
245
+ page_count: pages.length,
246
+ message_count: messageCount,
247
+ pages
248
+ };
249
+ };
250
+
251
+ exports.getConversationThread = async ({
252
+ conversationId, includeFullConversation = false,
253
+ token, sessionHeaders, requestJson = direct.requestJson
254
+ }) => {
255
+ if (!conversationId) throw new Error('conversationId is required');
256
+ const query = includeFullConversation ? '?include_full_conversation=true' : '';
257
+ const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}${query}`;
258
+ return requireOk(await requestJson(target, token, sessionHeaders, {
259
+ route: '/backend-api/conversation/{conversation_id}'
260
+ }), target);
261
+ };
262
+
263
+ exports.getConversationMessageSupplements = async ({
264
+ conversationId, token, sessionHeaders, requestJson = direct.requestJson
265
+ }) => {
266
+ if (!conversationId) throw new Error('conversationId is required');
267
+ const target = `/backend-api/conversations/${encodeURIComponent(conversationId)}/message_supplements`;
268
+ return requireOk(await requestJson(target, token, sessionHeaders, {
269
+ route: '/backend-api/conversations/{conversation_id}/message_supplements'
270
+ }), target);
271
+ };
272
+
273
+ exports.getConversationTextdocs = async ({
274
+ conversationId, token, sessionHeaders, requestJson = direct.requestJson
275
+ }) => {
276
+ if (!conversationId) throw new Error('conversationId is required');
277
+ const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/textdocs`;
278
+ return requireOk(await requestJson(target, token, sessionHeaders, {
279
+ route: '/backend-api/conversation/{conversation_id}/textdocs'
280
+ }), target);
281
+ };
282
+
283
+ exports.checkConversationUrlSafety = async ({
284
+ conversationId, url, historyAndTrainingDisabled = false,
285
+ token, sessionHeaders, requestJson = direct.requestJson
286
+ }) => {
287
+ if (!conversationId) throw new Error('conversationId is required');
288
+ if (!url) throw new Error('url is required');
289
+ const params = new URLSearchParams({
290
+ url: String(url),
291
+ history_and_training_disabled: historyAndTrainingDisabled ? 'true' : 'false'
292
+ });
293
+ const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/url_safe?${params}`;
294
+ return requireOk(await requestJson(target, token, sessionHeaders, {
295
+ route: '/backend-api/conversation/{conversation_id}/url_safe'
296
+ }), target);
297
+ };
298
+
299
+ exports.getVoiceFeedbackOptions = async ({
300
+ token, sessionHeaders, requestJson = direct.requestJson
301
+ } = {}) => {
302
+ const target = '/backend-api/conversation/voice/feedback-options';
303
+ return requireOk(await requestJson(target, token, sessionHeaders, { route: target }), target);
304
+ };
305
+
306
+ exports.listConversationFilesPage = async ({
307
+ conversationId, cursor = null, limit = 200,
308
+ token, sessionHeaders, requestJson = direct.requestJson
309
+ }) => {
310
+ if (!conversationId) throw new Error('conversationId is required');
311
+ const safeLimit = Math.max(1, Math.min(200, Number.parseInt(limit, 10) || 200));
312
+ const params = new URLSearchParams();
313
+ if (cursor) params.set('cursor', String(cursor));
314
+ params.set('limit', String(safeLimit));
315
+ const target = `/backend-api/conversations/${encodeURIComponent(conversationId)}/files?${params}`;
316
+ return requireOk(await requestJson(target, token, sessionHeaders, {
317
+ route: '/backend-api/conversations/{conversation_id}/files'
318
+ }), target);
319
+ };
320
+
321
+ exports.listConversationFiles = async ({
322
+ conversationId, limit = 200, maxPages = 1000,
323
+ token, sessionHeaders, requestJson = direct.requestJson
324
+ }) => {
325
+ if (!conversationId) throw new Error('conversationId is required');
326
+ const files = [];
327
+ const seenFiles = new Set();
328
+ const seenCursors = new Set();
329
+ let cursor = null;
330
+ const pageLimit = Math.max(1, Math.min(1000, Number.parseInt(maxPages, 10) || 1000));
331
+ for (let page = 0; page < pageLimit; page += 1) {
332
+ const result = await exports.listConversationFilesPage({ conversationId, cursor, limit, token, sessionHeaders, requestJson });
333
+ for (const file of Array.isArray(result?.items) ? result.items : []) {
334
+ const id = file?.file_id;
335
+ if (id && seenFiles.has(id)) continue;
336
+ if (id) seenFiles.add(id);
337
+ files.push(file);
338
+ }
339
+ const next = result?.cursor ?? null;
340
+ if (!next || seenCursors.has(next)) return files;
341
+ seenCursors.add(next);
342
+ cursor = next;
343
+ }
344
+ throw Object.assign(new Error(`conversation file pagination exceeded ${pageLimit} pages`), { code: 'PAGINATION_LIMIT' });
345
+ };
346
+
347
+ exports.searchConversations = async ({
348
+ query, cursor = null, token, sessionHeaders, requestJson = direct.requestJson
349
+ }) => {
350
+ const normalized = String(query || '').trim();
351
+ if (!normalized) throw new Error('query is required');
352
+ const params = new URLSearchParams({ query: normalized });
353
+ if (cursor) params.set('cursor', String(cursor));
354
+ const target = `/backend-api/conversations/search?${params}`;
355
+ return requireOk(await requestJson(target, token, sessionHeaders, {
356
+ route: '/backend-api/conversations/search'
357
+ }), target);
358
+ };
359
+
360
+ exports.requestInterpreterDownload = async ({
361
+ conversationId, messageId, sandboxPath, downloadIntent = null,
362
+ projectId = null, ownerUserId = null, requestSource = null,
363
+ token, sessionHeaders, requestJson = direct.requestJson
364
+ }) => {
365
+ if (!conversationId) throw new Error('conversationId is required');
366
+ if (!messageId) throw new Error('messageId is required');
367
+ if (!sandboxPath) throw new Error('sandboxPath is required');
368
+ const params = new URLSearchParams({ message_id: String(messageId), sandbox_path: String(sandboxPath) });
369
+ if (downloadIntent !== null && downloadIntent !== undefined) params.set('download_intent', String(downloadIntent));
370
+ const headers = {};
371
+ if (projectId) headers['chatgpt-project-id'] = String(projectId);
372
+ if (ownerUserId) headers['chatgpt-conv-owner-id'] = String(ownerUserId);
373
+ if (requestSource) headers['x-chatgpt-sandbox-download-source'] = String(requestSource);
374
+ const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/interpreter/download?${params}`;
375
+ return requireOk(await requestJson(target, token, sessionHeaders, {
376
+ route: '/backend-api/conversation/{conversation_id}/interpreter/download', headers
377
+ }), target);
378
+ };
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const paths = require('../config/paths');
6
+
7
+ const readState = (env = process.env) => {
8
+ const file = paths.tempConversationFile(env);
9
+ try {
10
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
11
+ return parsed && typeof parsed === 'object' && parsed.items && typeof parsed.items === 'object'
12
+ ? parsed
13
+ : { version: 1, items: {} };
14
+ } catch {
15
+ return { version: 1, items: {} };
16
+ }
17
+ };
18
+
19
+ const writeState = (state, env = process.env) => {
20
+ const file = paths.tempConversationFile(env);
21
+ fs.mkdirSync(path.dirname(file), { recursive: true });
22
+ const temp = `${file}.${process.pid}.tmp`;
23
+ fs.writeFileSync(temp, `${JSON.stringify(state, null, 2)}
24
+ `, { encoding: 'utf8', mode: 0o600 });
25
+ fs.renameSync(temp, file);
26
+ return file;
27
+ };
28
+
29
+ exports.get = (conversationId, env = process.env) => readState(env).items[conversationId] || null;
30
+ exports.save = ({ conversationId, currentNode, env = process.env }) => {
31
+ if (!conversationId || !currentNode) throw new TypeError('temporary conversation cursor requires conversationId and currentNode');
32
+ const state = readState(env);
33
+ state.items[conversationId] = { current_node: currentNode, updated_at: new Date().toISOString() };
34
+ writeState(state, env);
35
+ return { conversation_id: conversationId, current_node: currentNode };
36
+ };
37
+ exports.remove = (conversationId, env = process.env) => {
38
+ const state = readState(env);
39
+ const existed = Boolean(state.items[conversationId]);
40
+ delete state.items[conversationId];
41
+ writeState(state, env);
42
+ return existed;
43
+ };
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ const write = require('./write');
4
+ const tempState = require('./temp-state');
5
+
6
+ const VALID_PERSONALIZATION = new Set(['inherit', 'on', 'off']);
7
+
8
+ exports.resolvePersonalization = (mode = 'inherit', nativeValue = undefined) => {
9
+ if (!VALID_PERSONALIZATION.has(mode)) {
10
+ throw new TypeError(`invalid personalization mode: ${mode}`);
11
+ }
12
+ if (mode === 'on') return true;
13
+ if (mode === 'off') return false;
14
+ return typeof nativeValue === 'boolean' ? nativeValue : undefined;
15
+ };
16
+
17
+ exports.createConversation = async ({
18
+ message,
19
+ personalization = 'inherit',
20
+ nativePersonalization = undefined,
21
+ env = process.env,
22
+ ...options
23
+ }) => {
24
+ const result = await write.createConversation({
25
+ ...options,
26
+ message,
27
+ temporaryChat: true,
28
+ historyAndTrainingDisabled: true,
29
+ isDoNotRemember: false,
30
+ temporaryChatRequestsPersonalization: exports.resolvePersonalization(personalization, nativePersonalization)
31
+ });
32
+ tempState.save({ conversationId: result.conversation_id, currentNode: result.current_node, env });
33
+ return result;
34
+ };
35
+
36
+ exports.sendConversation = async ({
37
+ conversationId,
38
+ message,
39
+ env = process.env,
40
+ parentMessageId = null,
41
+ ...options
42
+ }) => {
43
+ const cursor = parentMessageId ? { current_node: parentMessageId } : tempState.get(conversationId, env);
44
+ if (!cursor?.current_node) {
45
+ throw Object.assign(new Error(`temporary conversation cursor not found: ${conversationId}`), { code: 'TEMP_STATE_NOT_FOUND' });
46
+ }
47
+ const result = await write.sendConversation({
48
+ ...options,
49
+ conversationId,
50
+ message,
51
+ parentMessageId: cursor.current_node,
52
+ temporaryChat: true,
53
+ historyAndTrainingDisabled: true,
54
+ isDoNotRemember: false,
55
+ temporaryChatRequestsPersonalization: null
56
+ });
57
+ tempState.save({ conversationId, currentNode: result.current_node, env });
58
+ return result;
59
+ };