@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,508 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const direct = require('../transport/direct/http-client');
5
+ const sentinel = require('../protocol/sentinel-client');
6
+ const prepare = require('../protocol/conversation-payload');
7
+ const conduit = require('../protocol/conduit');
8
+ const sse = require('../protocol/sse');
9
+ const read = require('./read');
10
+ const connector = require('../connector');
11
+
12
+ const requestConversationInit = async ({
13
+ conversationId = null,
14
+ projectId = null,
15
+ model = null,
16
+ conversationOrigin = null,
17
+ systemHints = [],
18
+ token,
19
+ sessionHeaders,
20
+ requestJson = direct.requestJson
21
+ }) => {
22
+ const target = '/backend-api/conversation/init';
23
+ const body = {
24
+ gizmo_id: projectId || null,
25
+ requested_default_model: model || null,
26
+ conversation_id: conversationId || null,
27
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || null,
28
+ timezone_offset_min: new Date().getTimezoneOffset(),
29
+ conversation_origin: conversationOrigin,
30
+ ...(Array.isArray(systemHints) && systemHints.length ? { system_hints: [...systemHints] } : {})
31
+ };
32
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
33
+ method: 'POST', body, route: target
34
+ }), target);
35
+ };
36
+
37
+ exports.initConversation = async (input) => {
38
+ if (!input?.conversationId) throw new Error('conversationId is required');
39
+ return requestConversationInit(input);
40
+ };
41
+
42
+ const initNewConversation = (input) => requestConversationInit({
43
+ ...input,
44
+ conversationId: null
45
+ });
46
+
47
+ const requireSuccess = (response, target) => {
48
+ if (!response || response.status < 200 || response.status >= 300) {
49
+ const error = new Error(`ChatGPT request failed (${response?.status || 'no-status'}): ${target}`);
50
+ error.status = response?.status || null;
51
+ error.target = target;
52
+ error.retryAfterMs = direct.parseRetryAfterMs(response?.headers || {});
53
+ if (error.status === 429) error.code = 'RATE_LIMITED';
54
+ throw error;
55
+ }
56
+ return response.json || { success: true };
57
+ };
58
+
59
+ const mutateConversation = async ({ conversationId, body, token, sessionHeaders, requestJson }) => {
60
+ if (!conversationId) throw new Error('conversationId is required');
61
+ const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}`;
62
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
63
+ method: 'PATCH', body, route: '/backend-api/conversation/{conversation_id}'
64
+ }), target);
65
+ };
66
+
67
+ exports.renameConversation = ({ conversationId, title, token, sessionHeaders, requestJson = direct.requestJson }) =>
68
+ mutateConversation({ conversationId, body: { title }, token, sessionHeaders, requestJson });
69
+
70
+ exports.deleteConversation = ({ conversationId, token, sessionHeaders, requestJson = direct.requestJson }) =>
71
+ mutateConversation({ conversationId, body: { is_visible: false }, token, sessionHeaders, requestJson });
72
+
73
+ exports.moveConversation = ({ conversationId, projectId, token, sessionHeaders, requestJson = direct.requestJson }) =>
74
+ mutateConversation({ conversationId, body: { gizmo_id: projectId || null }, token, sessionHeaders, requestJson });
75
+
76
+ exports.setConversationAsyncStatus = async ({ conversationId, status, token, sessionHeaders, requestJson = direct.requestJson }) => {
77
+ if (!conversationId) throw new Error('conversationId is required');
78
+ const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/async-status`;
79
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
80
+ method: 'POST', body: { status }, route: '/backend-api/conversation/{conversation_id}/async-status'
81
+ }), target);
82
+ };
83
+
84
+ exports.saveTemporaryConversation = async ({ conversationId, selectedLeafId, token, sessionHeaders, requestJson = direct.requestJson }) => {
85
+ if (!conversationId) throw new Error('conversationId is required');
86
+ if (!selectedLeafId) throw new Error('selectedLeafId is required');
87
+ const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/save`;
88
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
89
+ method: 'POST', body: { selected_leaf_id: selectedLeafId }, route: '/backend-api/conversation/{conversation_id}/save'
90
+ }), target);
91
+ };
92
+
93
+ exports.setMessageReaction = async ({ conversationId, messageId, reaction, token, sessionHeaders, requestJson = direct.requestJson }) => {
94
+ if (!conversationId) throw new Error('conversationId is required');
95
+ if (!messageId) throw new Error('messageId is required');
96
+ if (reaction == null) throw new Error('reaction is required');
97
+ const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}/reactions`;
98
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
99
+ method: 'POST', body: { reaction, entrypoint: 'message_action_button' }, route: '/backend-api/conversation/{conversation_id}/messages/{message_id}/reactions'
100
+ }), target);
101
+ };
102
+
103
+ exports.removeMessageReaction = async ({ conversationId, messageId, token, sessionHeaders, requestJson = direct.requestJson }) => {
104
+ if (!conversationId) throw new Error('conversationId is required');
105
+ if (!messageId) throw new Error('messageId is required');
106
+ const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}/reactions`;
107
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
108
+ method: 'DELETE', route: '/backend-api/conversation/{conversation_id}/messages/{message_id}/reactions'
109
+ }), target);
110
+ };
111
+
112
+ exports.rateConversation = async ({ conversationId, messageId, rating, shownAtAssistantTurn, prompt, popupUiMessage, token, sessionHeaders, requestJson = direct.requestJson }) => {
113
+ if (!conversationId) throw new Error('conversationId is required');
114
+ if (!messageId) throw new Error('messageId is required');
115
+ const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/rating`;
116
+ const body = { message_id: messageId, rating, shown_at_assistant_turn: shownAtAssistantTurn, prompt, popup_ui_message: popupUiMessage };
117
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
118
+ method: 'POST', body, route: '/backend-api/conversation/{conversation_id}/rating'
119
+ }), target);
120
+ };
121
+
122
+ exports.generateConversationTitle = async ({ conversationId, messageId, token, sessionHeaders, requestJson = direct.requestJson }) => {
123
+ if (!conversationId) throw new Error('conversationId is required');
124
+ if (!messageId) throw new Error('messageId is required');
125
+ const target = `/backend-api/conversation/gen_title/${encodeURIComponent(conversationId)}`;
126
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
127
+ method: 'POST', body: { message_id: messageId }, route: '/backend-api/conversation/gen_title/{conversation_id}'
128
+ }), target);
129
+ };
130
+
131
+ const sendWithRecovery = async ({
132
+ target, options, conversationId = null, conduitToken, turnTraceId,
133
+ token, sessionHeaders, requestJson, requestText
134
+ }) => {
135
+ const progress = sse.createParser();
136
+ let sawChunk = false;
137
+ let response = null;
138
+ let upstreamError = null;
139
+ try {
140
+ response = await requestText(target, token, sessionHeaders, {
141
+ ...options, retry: false, idleTimeoutMs: 60_000,
142
+ onChunk: (chunk) => { sawChunk = true; progress.push(chunk); options.onChunk?.(chunk); }
143
+ });
144
+ } catch (error) { upstreamError = error; }
145
+ if (response && (response.status < 200 || response.status >= 300)) requireSuccess(response, target);
146
+ if (response?.text && !sawChunk) progress.push(response.text);
147
+ const state = progress.finish();
148
+ const observedId = state.conversationId || conversationId;
149
+ if (state.completed) return { response: response || { status: 200, text: '' }, conversationId: observedId, currentNode: state.currentNode };
150
+ if (!observedId) { if (upstreamError) throw upstreamError; throw Object.assign(new Error('conversation stream ended before conversation_id was available'), { code: 'STREAM_INCOMPLETE' }); }
151
+ const streamState = await read.getConversationStreamStatus({
152
+ conversationId: observedId, token, sessionHeaders, requestJson
153
+ });
154
+ if (streamState?.status !== 'IS_STREAMING') {
155
+ return { response: response || { status: 200, text: '' }, conversationId: observedId, currentNode: state.currentNode };
156
+ }
157
+ const resumedProgress = sse.createParser();
158
+ let resumedSawChunk = false;
159
+ const resumed = await read.resumeConversation({
160
+ conversationId: observedId, offset: 0, conduitToken: state.resumeToken || conduitToken, turnTraceId,
161
+ token, sessionHeaders, requestJson, requestText,
162
+ onChunk: (chunk) => { resumedSawChunk = true; resumedProgress.push(chunk); options.onChunk?.(chunk); }
163
+ });
164
+ if (resumed.text && !resumedSawChunk) resumedProgress.push(resumed.text);
165
+ const resumedState = resumedProgress.finish();
166
+ if (!resumedState.completed) throw Object.assign(new Error('resumed conversation stream ended before [DONE]'), { code: 'STREAM_INCOMPLETE' });
167
+ return { response: response || { status: 200, text: '' }, conversationId: observedId, currentNode: resumedState.currentNode || state.currentNode };
168
+ };
169
+ exports.createConversation = async ({
170
+ message,
171
+ projectId = null,
172
+ model = prepare.DEFAULT_MODEL,
173
+ thinkingEffort = prepare.DEFAULT_THINKING_EFFORT,
174
+ systemHints = null,
175
+ systemHintMentions = null,
176
+ targetedReply = null,
177
+ hiddenSystemMessages = [],
178
+ hideUserMessage = false,
179
+ localFunctionNames = null,
180
+ timezone = null,
181
+ timezoneOffsetMin = null,
182
+ modelResponseContracts = null,
183
+ clientContextualInfo = null,
184
+ temporaryChat = false,
185
+ historyAndTrainingDisabled = null,
186
+ isDoNotRemember = null,
187
+ temporaryChatRequestsPersonalization = null,
188
+ browserContext = null,
189
+ attachments = [],
190
+ enableMessageFollowups = true,
191
+ forceParallelSwitch = 'auto',
192
+ paragenCotSummaryDisplayOverride = 'allow',
193
+ onChunk = null,
194
+ recoverStream = false,
195
+ token,
196
+ sessionHeaders,
197
+ resolveSystemHintSelections = connector.resolveSystemHintSelections,
198
+ requestJson = direct.requestJson,
199
+ requestText = direct.requestText
200
+ }) => {
201
+ if (!message) throw new Error('message is required');
202
+ if (!Array.isArray(systemHints) && !Array.isArray(systemHintMentions)) ({ systemHints, systemHintMentions } = await resolveSystemHintSelections({ message, token, sessionHeaders }));
203
+ systemHints = Array.isArray(systemHints) ? systemHints : [];
204
+ systemHintMentions = Array.isArray(systemHintMentions) ? systemHintMentions : [];
205
+
206
+ let preInit = null;
207
+ let preInitError = null;
208
+ if (model === 'auto') {
209
+ try {
210
+ preInit = await initNewConversation({
211
+ projectId,
212
+ model: null,
213
+ systemHints,
214
+ token,
215
+ sessionHeaders,
216
+ requestJson
217
+ });
218
+ model = preInit?.intended_default_model_slug || preInit?.default_model_slug || prepare.DEFAULT_MODEL;
219
+ } catch (error) {
220
+ preInitError = error.message || String(error);
221
+ model = prepare.DEFAULT_MODEL;
222
+ }
223
+ }
224
+
225
+ const traceId = crypto.randomUUID();
226
+ const prepareTarget = '/backend-api/f/conversation/prepare';
227
+ const prepareResponse = await requestJson(prepareTarget, token, sessionHeaders, {
228
+ method: 'POST',
229
+ body: prepare.buildPrepareBody({ projectId, model, thinkingEffort, systemHints, localFunctionNames, timezone, timezoneOffsetMin, modelResponseContracts, clientContextualInfo, temporaryChat, attachments }),
230
+ route: prepareTarget,
231
+ headers: { 'x-oai-turn-trace-id': traceId }
232
+ });
233
+ if (!prepareResponse || prepareResponse.status < 200 || prepareResponse.status >= 300) {
234
+ requireSuccess(prepareResponse, prepareTarget);
235
+ }
236
+ const conduitToken = prepareResponse?.json?.conduit_token
237
+ || prepareResponse?.headers?.['x-conduit-token'];
238
+ if (!conduitToken) throw new Error('conversation prepare response missing conduit token');
239
+ let sentinelHeaders = conduit.takeSentinelHeaders(sessionHeaders || {});
240
+ try {
241
+ const fresh = await sentinel.fetchChatRequirements({ token, sessionHeaders: sessionHeaders || {}, requestJson });
242
+ if (fresh?.token) sentinelHeaders = { ...sentinelHeaders, 'openai-sentinel-chat-requirements-token': fresh.token };
243
+ if (fresh?.proofToken) sentinelHeaders = { ...sentinelHeaders, 'openai-sentinel-proof-token': fresh.proofToken };
244
+ } catch {
245
+ // fallback: ใช้ sentinel headers จาก session เดิมถ้า fetch สดไม่สำเร็จ
246
+ }
247
+ const sendTarget = '/backend-api/f/conversation';
248
+ const sendOptions = {
249
+ method: 'POST',
250
+ body: prepare.buildSendBody({ message, projectId, model, thinkingEffort, systemHints, systemHintMentions, targetedReply, hiddenSystemMessages, hideUserMessage, timezone, timezoneOffsetMin, modelResponseContracts, clientContextualInfo, localFunctionNames, temporaryChat, historyAndTrainingDisabled, isDoNotRemember, temporaryChatRequestsPersonalization, browserContext, attachments, enableMessageFollowups, forceParallelSwitch, paragenCotSummaryDisplayOverride }),
251
+ route: sendTarget,
252
+ headers: {
253
+ ...sentinelHeaders,
254
+ 'x-conduit-token': conduitToken,
255
+ 'x-oai-turn-trace-id': traceId,
256
+ accept: 'text/event-stream'
257
+ },
258
+ onChunk
259
+ };
260
+ const sent = recoverStream
261
+ ? await sendWithRecovery({
262
+ target: sendTarget, options: sendOptions, conduitToken, turnTraceId: traceId,
263
+ token, sessionHeaders, requestJson, requestText
264
+ })
265
+ : { response: await requestText(sendTarget, token, sessionHeaders, sendOptions) };
266
+ const sendResponse = sent.response;
267
+ if (!sendResponse || sendResponse.status < 200 || sendResponse.status >= 300) {
268
+ requireSuccess(sendResponse, sendTarget);
269
+ }
270
+ const parsedStream = sse.parse(sendResponse.text);
271
+ const conversationId = sent.conversationId || parsedStream.conversationId;
272
+ if (!conversationId) throw new Error('conversation response missing conversation_id');
273
+ if (temporaryChat) {
274
+ const currentNode = sent.currentNode || parsedStream.currentNode;
275
+ if (!currentNode) throw Object.assign(new Error('temporary conversation response missing current node'), { code: 'TEMP_CURRENT_NODE_MISSING' });
276
+ return { conversation_id: conversationId, current_node: currentNode, temporary_chat: true, messages: [] };
277
+ }
278
+ const conversation = await read.getConversation({ conversationId, token, sessionHeaders, requestJson });
279
+ if (preInit) {
280
+ conversation.init = preInit;
281
+ } else {
282
+ try {
283
+ conversation.init = await exports.initConversation({ conversationId, projectId, model, token, sessionHeaders, requestJson });
284
+ } catch (initError) {
285
+ conversation.init_error = initError.message || String(initError);
286
+ }
287
+ }
288
+ if (preInitError) conversation.init_preflight_error = preInitError;
289
+ return conversation;
290
+ };
291
+
292
+ const sendConversationDirect = async ({
293
+ conversationId,
294
+ message,
295
+ parentMessageId = null,
296
+ projectId = null,
297
+ model = prepare.DEFAULT_MODEL,
298
+ thinkingEffort = prepare.DEFAULT_THINKING_EFFORT,
299
+ systemHints = null,
300
+ systemHintMentions = null,
301
+ targetedReply = null,
302
+ hiddenSystemMessages = [],
303
+ hideUserMessage = false,
304
+ localFunctionNames = null,
305
+ timezone = null,
306
+ timezoneOffsetMin = null,
307
+ modelResponseContracts = null,
308
+ clientContextualInfo = null,
309
+ temporaryChat = false,
310
+ historyAndTrainingDisabled = null,
311
+ isDoNotRemember = null,
312
+ temporaryChatRequestsPersonalization = null,
313
+ browserContext = null,
314
+ attachments = [],
315
+ enableMessageFollowups = true,
316
+ forceParallelSwitch = 'auto',
317
+ paragenCotSummaryDisplayOverride = 'allow',
318
+ onChunk = null,
319
+ recoverStream = false,
320
+ token,
321
+ sessionHeaders,
322
+ resolveSystemHintSelections = connector.resolveSystemHintSelections,
323
+ requestJson = direct.requestJson,
324
+ requestText = direct.requestText
325
+ }) => {
326
+ if (!conversationId) throw new Error('conversationId is required');
327
+ if (!message) throw new Error('message is required');
328
+ if (!Array.isArray(systemHints) && !Array.isArray(systemHintMentions)) ({ systemHints, systemHintMentions } = await resolveSystemHintSelections({ message, token, sessionHeaders }));
329
+ systemHints = Array.isArray(systemHints) ? systemHints : [];
330
+ systemHintMentions = Array.isArray(systemHintMentions) ? systemHintMentions : [];
331
+ let resolvedParentMessageId = parentMessageId;
332
+ let resolvedProjectId = projectId;
333
+ if (!temporaryChat) {
334
+ const before = await read.getConversation({ conversationId, token, sessionHeaders, requestJson });
335
+ resolvedParentMessageId = before.current_node;
336
+ resolvedProjectId = before.gizmo_id || null;
337
+ }
338
+ if (!resolvedParentMessageId) throw new Error('conversation current_node is required');
339
+ let sentinelHeaders = conduit.takeSentinelHeaders(sessionHeaders);
340
+ try {
341
+ const fresh = await sentinel.fetchChatRequirements({ token, sessionHeaders, requestJson });
342
+ if (fresh?.token) sentinelHeaders = { ...sentinelHeaders, 'openai-sentinel-chat-requirements-token': fresh.token };
343
+ if (fresh?.proofToken) sentinelHeaders = { ...sentinelHeaders, 'openai-sentinel-proof-token': fresh.proofToken };
344
+ } catch {
345
+ // fallback: ใช้ sentinel headers จาก session เดิมถ้า fetch สดไม่สำเร็จ
346
+ }
347
+ const turnHeaders = { ...sessionHeaders };
348
+ for (const name of conduit.SENTINEL_HEADER_NAMES) delete turnHeaders[name];
349
+ const traceId = crypto.randomUUID();
350
+ const prepareTarget = '/backend-api/f/conversation/prepare';
351
+ const prepareBody = {
352
+ ...prepare.buildPrepareBody({ projectId: resolvedProjectId, model, thinkingEffort, systemHints, localFunctionNames, timezone, timezoneOffsetMin, modelResponseContracts, clientContextualInfo, temporaryChat, attachments, includeForkFromSharedPost: false }),
353
+ conversation_id: conversationId,
354
+ parent_message_id: resolvedParentMessageId,
355
+ client_prepare_dispatch: 'immediate',
356
+ client_prepare_source: 'context_change'
357
+ };
358
+ const prepareResponse = await requestJson(prepareTarget, token, turnHeaders, {
359
+ method: 'POST', body: prepareBody, route: prepareTarget,
360
+ headers: { 'x-oai-turn-trace-id': traceId }
361
+ });
362
+ if (!prepareResponse || prepareResponse.status < 200 || prepareResponse.status >= 300) requireSuccess(prepareResponse, prepareTarget);
363
+ conduit.applyIntegrityUpdate(turnHeaders, prepareResponse.headers, 'r');
364
+ let conduitToken = prepareResponse?.json?.conduit_token || prepareResponse?.headers?.['x-conduit-token'];
365
+ if (!conduitToken) throw new Error('conversation prepare response missing conduit token');
366
+ const preparedBody = {
367
+ ...prepareBody,
368
+ client_prepare_state: 'success',
369
+ client_prepare_dispatch: 'debounced',
370
+ client_prepare_source: 'composer_editor_state'
371
+ };
372
+ const preparedResponse = await requestJson(prepareTarget, token, turnHeaders, {
373
+ method: 'POST', body: preparedBody, route: prepareTarget,
374
+ headers: {
375
+ 'x-conduit-token': conduitToken,
376
+ 'x-oai-turn-trace-id': traceId,
377
+ 'x-oai-is-client-observation': turnHeaders['x-oai-is-client-observation']
378
+ }
379
+ });
380
+ if (!preparedResponse || preparedResponse.status < 200 || preparedResponse.status >= 300) requireSuccess(preparedResponse, prepareTarget);
381
+ conduit.applyIntegrityUpdate(turnHeaders, preparedResponse.headers, 'r');
382
+ conduitToken = preparedResponse?.headers?.['x-conduit-token'] || conduitToken;
383
+ const sendTarget = '/backend-api/f/conversation';
384
+ const sendBody = {
385
+ ...prepare.buildSendBody({ message, projectId: resolvedProjectId, model, thinkingEffort, systemHints, systemHintMentions, targetedReply, hiddenSystemMessages, hideUserMessage, timezone, timezoneOffsetMin, modelResponseContracts, clientContextualInfo, localFunctionNames, temporaryChat, historyAndTrainingDisabled, isDoNotRemember, temporaryChatRequestsPersonalization, browserContext, attachments, enableMessageFollowups, forceParallelSwitch, paragenCotSummaryDisplayOverride }),
386
+ conversation_id: conversationId,
387
+ parent_message_id: resolvedParentMessageId
388
+ };
389
+ const sendOptions = {
390
+ method: 'POST', body: sendBody, route: sendTarget,
391
+ headers: {
392
+ ...sentinelHeaders,
393
+ 'x-conduit-token': conduitToken,
394
+ 'x-oai-turn-trace-id': traceId,
395
+ 'x-oai-is-client-observation': conduit.streamObservation(turnHeaders['x-oai-is-client-observation']),
396
+ accept: 'text/event-stream'
397
+ },
398
+ onChunk
399
+ };
400
+ const sent = recoverStream
401
+ ? await sendWithRecovery({
402
+ target: sendTarget, options: sendOptions, conversationId, conduitToken, turnTraceId: traceId,
403
+ token, sessionHeaders: turnHeaders, requestJson, requestText
404
+ })
405
+ : { response: await requestText(sendTarget, token, turnHeaders, sendOptions) };
406
+ const sendResponse = sent.response;
407
+ if (!sendResponse || sendResponse.status < 200 || sendResponse.status >= 300) requireSuccess(sendResponse, sendTarget);
408
+ if (temporaryChat) {
409
+ const parsedStream = sse.parse(sendResponse.text);
410
+ const currentNode = sent.currentNode || parsedStream.currentNode;
411
+ if (!currentNode) throw Object.assign(new Error('temporary conversation response missing current node'), { code: 'TEMP_CURRENT_NODE_MISSING' });
412
+ return {
413
+ conversation_id: conversationId,
414
+ current_node: currentNode,
415
+ previous_node: resolvedParentMessageId,
416
+ temporary_chat: true,
417
+ messages: []
418
+ };
419
+ }
420
+ const after = await read.getConversation({ conversationId, token, sessionHeaders, requestJson });
421
+ if (after && typeof after === 'object') after.previous_node = resolvedParentMessageId;
422
+ return after;
423
+ };
424
+ exports.sendConversation = async ({ transport = 'direct', ...input }) => {
425
+ if (transport !== 'direct') {
426
+ const error = new Error('Unsupported ChatGPT transport: ' + transport);
427
+ error.code = 'UNSUPPORTED_TRANSPORT';
428
+ throw error;
429
+ }
430
+ return sendConversationDirect(input);
431
+ };
432
+
433
+ exports.setConversationAsyncStatus = async ({
434
+ conversationId, status = null, token, sessionHeaders, requestJson = direct.requestJson
435
+ }) => {
436
+ if (!conversationId) throw new Error('conversationId is required');
437
+ const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/async-status`;
438
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
439
+ method: 'POST', body: { status }, route: '/backend-api/conversation/{conversation_id}/async-status', retry: false
440
+ }), target);
441
+ };
442
+
443
+ exports.setMessageReaction = async ({
444
+ conversationId, messageId, reaction, entrypoint = 'message_action_button',
445
+ token, sessionHeaders, requestJson = direct.requestJson
446
+ }) => {
447
+ if (!conversationId) throw new Error('conversationId is required');
448
+ if (!messageId) throw new Error('messageId is required');
449
+ if (!reaction) throw new Error('reaction is required');
450
+ const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}/reactions`;
451
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
452
+ method: 'POST', body: { reaction, entrypoint },
453
+ route: '/backend-api/conversation/{conversation_id}/messages/{message_id}/reactions', retry: false
454
+ }), target);
455
+ };
456
+
457
+ exports.removeMessageReaction = async ({
458
+ conversationId, messageId, token, sessionHeaders, requestJson = direct.requestJson
459
+ }) => {
460
+ if (!conversationId) throw new Error('conversationId is required');
461
+ if (!messageId) throw new Error('messageId is required');
462
+ const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}/reactions`;
463
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
464
+ method: 'DELETE', route: '/backend-api/conversation/{conversation_id}/messages/{message_id}/reactions', retry: false
465
+ }), target);
466
+ };
467
+
468
+ exports.rateConversation = async ({
469
+ conversationId, messageId, rating, shownAtAssistantTurn = -1,
470
+ prompt = null, popupUiMessage = null,
471
+ token, sessionHeaders, requestJson = direct.requestJson
472
+ }) => {
473
+ if (!conversationId) throw new Error('conversationId is required');
474
+ if (!messageId) throw new Error('messageId is required');
475
+ const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/rating`;
476
+ const body = {
477
+ message_id: messageId, rating,
478
+ shown_at_assistant_turn: shownAtAssistantTurn,
479
+ prompt, popup_ui_message: popupUiMessage
480
+ };
481
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
482
+ method: 'POST', body, route: '/backend-api/conversation/{conversation_id}/rating', retry: false
483
+ }), target);
484
+ };
485
+
486
+ exports.generateConversationTitle = async ({
487
+ conversationId, messageId, token, sessionHeaders, requestJson = direct.requestJson
488
+ }) => {
489
+ if (!conversationId) throw new Error('conversationId is required');
490
+ if (!messageId) throw new Error('messageId is required');
491
+ const target = `/backend-api/conversation/gen_title/${encodeURIComponent(conversationId)}`;
492
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
493
+ method: 'POST', body: { message_id: messageId },
494
+ route: '/backend-api/conversation/gen_title/{conversation_id}', retry: false
495
+ }), target);
496
+ };
497
+
498
+ exports.saveTemporaryConversation = async ({
499
+ conversationId, selectedLeafId, token, sessionHeaders, requestJson = direct.requestJson
500
+ }) => {
501
+ if (!conversationId) throw new Error('conversationId is required');
502
+ if (!selectedLeafId) throw new Error('selectedLeafId is required');
503
+ const target = `/backend-api/conversation/${encodeURIComponent(conversationId)}/save`;
504
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
505
+ method: 'POST', body: { selected_leaf_id: selectedLeafId },
506
+ route: '/backend-api/conversation/{conversation_id}/save', retry: false
507
+ }), target);
508
+ };
package/src/index.js ADDED
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ exports.auth = require('./auth');
4
+ exports.session = require('./session');
5
+ exports.conversation = require('./conversation');
6
+ exports.project = require('./project');
7
+ exports.connector = require('./connector');
8
+ exports.api = require('./api');
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ const read = require('./read');
4
+ const write = require('./write');
5
+
6
+ Object.assign(exports, read, write);
@@ -0,0 +1,91 @@
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
+ exports.listProjects = async ({ token, sessionHeaders, requestJson = direct.requestJson }) => {
18
+ const projects = [];
19
+ let cursor = null;
20
+ for (let pageNumber = 0; pageNumber < 1000; pageNumber += 1) {
21
+ const cursorPart = cursor ? `&cursor=${encodeURIComponent(cursor)}` : '';
22
+ const target = `/backend-api/gizmos/snorlax/sidebar?owned_only=true&conversations_per_gizmo=5&limit=20${cursorPart}`;
23
+ let page;
24
+ try {
25
+ page = requireOk(await requestJson(target, token, sessionHeaders), target);
26
+ } catch (error) {
27
+ if (error.status === 401 || error.status === 403 || error.status === 404) return projects;
28
+ throw error;
29
+ }
30
+ for (const item of Array.isArray(page.items) ? page.items : []) {
31
+ const gizmo = item?.gizmo?.gizmo;
32
+ if (!gizmo?.id) continue;
33
+ projects.push({ id: gizmo.id, name: gizmo.display?.name || gizmo.short_url || gizmo.id });
34
+ }
35
+ if (!page.cursor || page.cursor === cursor) return projects;
36
+ cursor = page.cursor;
37
+ }
38
+ throw Object.assign(new Error('project pagination exceeded 1000 pages'), { code: 'PAGINATION_LIMIT' });
39
+ };
40
+
41
+ exports.getProject = async ({ projectId, token, sessionHeaders, requestJson = direct.requestJson }) => {
42
+ if (!projectId) throw new Error('projectId is required');
43
+ const target = `/backend-api/gizmos/${encodeURIComponent(projectId)}`;
44
+ return requireOk(await requestJson(target, token, sessionHeaders, { route: '/backend-api/gizmos/{gizmo_id}' }), target);
45
+ };
46
+
47
+ exports.listProjectConversationPage = async ({ projectId, token, sessionHeaders, cursor = '0', limit = 50, requestJson = direct.requestJson }) => {
48
+ if (!projectId) throw new Error('projectId is required');
49
+ const target = `/backend-api/gizmos/${encodeURIComponent(projectId)}/conversations?cursor=${encodeURIComponent(cursor)}&limit=${limit}&owned_only=true`;
50
+ const page = requireOk(await requestJson(target, token, sessionHeaders, { route: '/backend-api/gizmos/{gizmo_id}/conversations' }), target);
51
+ return { items: Array.isArray(page.items) ? page.items : [], cursor, nextCursor: page.cursor && page.cursor !== cursor ? page.cursor : null };
52
+ };
53
+
54
+ exports.listProjectConversations = async ({ projectId, token, sessionHeaders, requestJson = direct.requestJson }) => {
55
+ const items = [];
56
+ let cursor = '0';
57
+ for (let pageNumber = 0; pageNumber < 1000; pageNumber += 1) {
58
+ const page = await exports.listProjectConversationPage({ projectId, token, sessionHeaders, cursor, limit: 50, requestJson });
59
+ items.push(...page.items);
60
+ if (!page.nextCursor) return items;
61
+ cursor = page.nextCursor;
62
+ }
63
+ throw Object.assign(new Error('project conversation pagination exceeded 1000 pages'), { code: 'PAGINATION_LIMIT' });
64
+ };
65
+
66
+ exports.listAll = async ({ token, sessionHeaders, requestJson = direct.requestJson, onProgress = () => {} }) => {
67
+ const conversation = require('../conversation/read');
68
+ const conversationPage = await conversation.listConversationPage({ token, sessionHeaders, requestJson, onProgress });
69
+ onProgress({ type: 'standalone-loaded', count: conversationPage.items.length, total: conversationPage.total });
70
+ const projectList = await exports.listProjects({ token, sessionHeaders, requestJson });
71
+ onProgress({ type: 'projects-loaded', count: projectList.length });
72
+ const projects = projectList.map((project) => ({ ...project, conversations: [] }));
73
+ const projectsById = new Map(projects.map((project) => [project.id, project]));
74
+ const outside = [];
75
+ for (const item of conversationPage.items) {
76
+ const project = item.gizmo_id ? projectsById.get(item.gizmo_id) : null;
77
+ if (project) project.conversations.push(item);
78
+ else outside.push(item);
79
+ }
80
+ const result = {
81
+ outside,
82
+ projects,
83
+ returned: conversationPage.items.length,
84
+ total: conversationPage.total,
85
+ offset: conversationPage.offset,
86
+ limit: conversationPage.limit,
87
+ nextOffset: conversationPage.nextOffset
88
+ };
89
+ onProgress({ type: 'done', returned: result.returned, total: result.total });
90
+ return result;
91
+ };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ const direct = require('../transport/direct/http-client');
4
+ const read = require('./read');
5
+
6
+ const requireSuccess = (response, target) => {
7
+ if (!response || response.status < 200 || response.status >= 300) {
8
+ const error = new Error(`ChatGPT request failed (${response?.status || 'no-status'}): ${target}`);
9
+ error.status = response?.status || null;
10
+ error.target = target;
11
+ error.retryAfterMs = direct.parseRetryAfterMs(response?.headers || {});
12
+ if (error.status === 429) error.code = 'RATE_LIMITED';
13
+ throw error;
14
+ }
15
+ return response.json || { success: true };
16
+ };
17
+
18
+ exports.createProject = async ({ name, token, sessionHeaders, requestJson = direct.requestJson }) => {
19
+ const target = '/backend-api/projects';
20
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
21
+ method: 'POST', body: { name, instructions: '', memory_scope: 'project_v2' }, route: target
22
+ }), target);
23
+ };
24
+
25
+ exports.renameProject = async ({ projectId, name, token, sessionHeaders, requestJson = direct.requestJson }) => {
26
+ const raw = await read.getProject({ projectId, token, sessionHeaders, requestJson });
27
+ const g = raw?.gizmo?.gizmo || raw?.gizmo || raw || {};
28
+ const display = g.display || {};
29
+ const target = `/backend-api/projects/${encodeURIComponent(projectId)}`;
30
+ const body = { name, instructions: g.instructions || '', emoji: display.emoji ?? null, theme: display.theme ?? null };
31
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
32
+ method: 'PATCH', body, route: '/backend-api/projects/{project_id}'
33
+ }), target);
34
+ };
35
+
36
+ exports.saveProjectMessage = async ({ projectId, conversationId, messageId, token, sessionHeaders, requestJson = direct.requestJson }) => {
37
+ if (!projectId) throw new Error('projectId is required');
38
+ if (!conversationId) throw new Error('conversationId is required');
39
+ if (!messageId) throw new Error('messageId is required');
40
+ const target = `/backend-api/projects/${encodeURIComponent(projectId)}/saves`;
41
+ return requireSuccess(await requestJson(target, token, sessionHeaders, {
42
+ method: 'POST', body: { conversation_id: conversationId, message_id: messageId }, route: '/backend-api/projects/{project_id}/saves', retry: false
43
+ }), target);
44
+ };
45
+
46
+ exports.deleteProject = async ({ projectId, token, sessionHeaders, requestJson = direct.requestJson }) => {
47
+ if (!projectId) throw new Error('projectId is required');
48
+ const target = `/backend-api/gizmos/${encodeURIComponent(projectId)}`;
49
+ return requireSuccess(await requestJson(target, token, sessionHeaders, { method: 'DELETE', route: '/backend-api/gizmos/{gizmo_id}' }), target);
50
+ };