@miruzero/zgpt 0.1.3 → 0.1.5
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 +112 -11
- package/docs/architecture.md +5 -5
- package/docs/custom-mcp-management-api.md +132 -0
- package/package.json +10 -3
- package/src/api/browser-observed.json +82 -0
- package/src/api/contracts.json +101 -0
- package/src/api/discovered.json +165 -0
- package/src/api/index.js +63 -5
- package/src/cli/sdk-dispatch.js +341 -0
- package/src/cli.js +8 -15
- package/src/connector/index.js +2 -1
- package/src/connector/mcp.js +154 -0
- package/src/connector/write.js +2 -1
- package/src/conversation/index.js +29 -0
- package/src/conversation/read.js +41 -7
- package/src/index.js +2 -3
- package/src/profile/index.js +41 -0
- package/src/project/index.js +35 -1
- package/src/session/direct-transport.js +60 -68
- package/src/settings/index.js +3 -1
- package/src/settings/web.js +38 -0
- package/src/transport/direct/http-client.js +40 -12
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Browser-derived MCP connector operations. The single SDK API catalog owns routes,
|
|
4
|
+
// authentication, transport, encoding and response handling. Never auto-create a link.
|
|
5
|
+
const api = require('../api');
|
|
6
|
+
|
|
7
|
+
const bad = (message) => Object.assign(new Error(message), { code: 'BAD_ARGS' });
|
|
8
|
+
const nonempty = (value, field) => {
|
|
9
|
+
if (typeof value !== 'string' || !value.trim()) throw bad(field + ' is required');
|
|
10
|
+
return value.trim();
|
|
11
|
+
};
|
|
12
|
+
const safeHttpUrl = (value, field) => {
|
|
13
|
+
const text = nonempty(value, field);
|
|
14
|
+
let url;
|
|
15
|
+
try { url = new URL(text); } catch { throw bad(field + ' must be a valid HTTPS URL'); }
|
|
16
|
+
if (url.protocol !== 'https:' || url.username || url.password)
|
|
17
|
+
throw bad(field + ' must be a valid HTTPS URL without embedded credentials');
|
|
18
|
+
return text;
|
|
19
|
+
};
|
|
20
|
+
const logoValue = (value) => {
|
|
21
|
+
const input=nonempty(value,'logoUrl');
|
|
22
|
+
if (input.startsWith('data:')) {
|
|
23
|
+
const match=/^data:image\/png;base64,([A-Za-z0-9+/]+={0,2})$/.exec(input);
|
|
24
|
+
if (!match) throw bad('logoUrl must be a PNG data URL');
|
|
25
|
+
if (Buffer.from(match[1],'base64').length > 10 * 1024)
|
|
26
|
+
throw bad('logoUrl PNG exceeds the 10 KB maximum');
|
|
27
|
+
return input;
|
|
28
|
+
}
|
|
29
|
+
return safeHttpUrl(input,'logoUrl');
|
|
30
|
+
};
|
|
31
|
+
const makeTarget = ({mcpUrl, tunnelId}) => {
|
|
32
|
+
if (mcpUrl != null && tunnelId != null) throw bad('Choose either mcpUrl or tunnelId, not both');
|
|
33
|
+
if (mcpUrl != null) return {mcp_url:safeHttpUrl(mcpUrl,'mcpUrl')};
|
|
34
|
+
if (tunnelId != null) {
|
|
35
|
+
const id=nonempty(tunnelId,'tunnelId');
|
|
36
|
+
if (!/^tunnel_[a-z0-9]{32}$/.test(id)) throw bad('tunnelId must match tunnel_<32 lowercase letters or digits>');
|
|
37
|
+
return {tunnel_id:id};
|
|
38
|
+
}
|
|
39
|
+
throw bad('mcpUrl or tunnelId is required');
|
|
40
|
+
};
|
|
41
|
+
const request = (key, options, patch = {}) =>
|
|
42
|
+
api.invoke(key, {
|
|
43
|
+
...options,...patch,
|
|
44
|
+
headers:{...options.headers,...patch.headers,'OAI-Product-Sku':'CONNECTOR_SETTING'}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const authRequestFromDiscovery = (discovery) => {
|
|
48
|
+
const config=discovery?.oauth_config;
|
|
49
|
+
const invalid=() => Object.assign(new Error('MCP OAuth discovery returned incomplete or unsupported metadata'), {
|
|
50
|
+
code:'MCP_OAUTH_CONFIG_INVALID'
|
|
51
|
+
});
|
|
52
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) throw invalid();
|
|
53
|
+
if (config.type === 'NONE') return {supported_auth:[],oauth_client_params:null};
|
|
54
|
+
if (config.type !== 'OAUTH' ||
|
|
55
|
+
typeof config.authorization_url !== 'string' || !config.authorization_url ||
|
|
56
|
+
typeof config.token_url !== 'string' || !config.token_url) throw invalid();
|
|
57
|
+
// ChatGPT Web puts the discovered OAuth config inside supported_auth.
|
|
58
|
+
// Registration, PKCE, scopes and resource stay exactly as advertised by the server.
|
|
59
|
+
return {supported_auth:[config],oauth_client_params:null};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
exports.createCustomMcpServer = async (options = {}) => {
|
|
63
|
+
const name = nonempty(options.name,'name');
|
|
64
|
+
const target=makeTarget(options);
|
|
65
|
+
let authRequest;
|
|
66
|
+
if (options.authRequest !== undefined) {
|
|
67
|
+
if (options.authentication) throw bad('Use authentication or authRequest, not both');
|
|
68
|
+
if (!options.authRequest || typeof options.authRequest !== 'object' ||
|
|
69
|
+
Array.isArray(options.authRequest) || !Array.isArray(options.authRequest.supported_auth))
|
|
70
|
+
throw bad('authRequest must be a web-compatible object with supported_auth array');
|
|
71
|
+
authRequest=options.authRequest;
|
|
72
|
+
} else if (options.authentication === 'NONE') {
|
|
73
|
+
authRequest={supported_auth:[],oauth_client_params:null};
|
|
74
|
+
} else if (options.authentication === undefined || options.authentication === 'AUTO') {
|
|
75
|
+
// Discovery is safe to repeat and does not create a connector or a link.
|
|
76
|
+
// Never silently downgrade OAuth to NONE on a discovery error.
|
|
77
|
+
const discovery=await exports.getCustomMcpOAuthConfig(options);
|
|
78
|
+
authRequest=authRequestFromDiscovery(discovery);
|
|
79
|
+
} else throw bad('Unknown authentication type; use AUTO, NONE or a configured authRequest');
|
|
80
|
+
if (options.description !== undefined && typeof options.description !== 'string') throw bad('description must be a string');
|
|
81
|
+
if (options.customHeaders !== undefined && !Array.isArray(options.customHeaders)) throw bad('customHeaders must be an array');
|
|
82
|
+
const body = {
|
|
83
|
+
name, ...target,
|
|
84
|
+
description:(options.description || '').trim(),
|
|
85
|
+
logo_url:options.logoUrl == null ? null : logoValue(options.logoUrl),
|
|
86
|
+
auth_request:authRequest
|
|
87
|
+
};
|
|
88
|
+
if (options.customHeaders?.length) body.custom_headers=options.customHeaders;
|
|
89
|
+
return request('POST /backend-api/aip/connectors/mcp',options,{body});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
exports.renameCustomMcpServer = async (options = {}) =>
|
|
93
|
+
request('PATCH /backend-api/aip/connectors/{id}/name',options,{
|
|
94
|
+
pathValues:[nonempty(options.connectorId,'connectorId')],
|
|
95
|
+
body:{name:nonempty(options.name,'name')}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
exports.describeCustomMcpServer = async (options = {}) => {
|
|
99
|
+
if (typeof options.description !== 'string') throw bad('description must be a string');
|
|
100
|
+
return request('PATCH /backend-api/aip/connectors/{id}/description',options,{
|
|
101
|
+
pathValues:[nonempty(options.connectorId,'connectorId')],
|
|
102
|
+
body:{description:options.description}
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
exports.setCustomMcpServerLogo = async (options = {}) =>
|
|
107
|
+
request('PATCH /backend-api/aip/connectors/{id}/logo',options,{
|
|
108
|
+
pathValues:[nonempty(options.connectorId,'connectorId')],
|
|
109
|
+
body:{logo_url:logoValue(options.logoUrl)}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
exports.deleteCustomMcpServer = async (options = {}) =>
|
|
113
|
+
request('DELETE /backend-api/aip/connectors/{id}',options,{
|
|
114
|
+
pathValues:[nonempty(options.connectorId,'connectorId')]
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
exports.getCustomMcpServer = async (options = {}) =>
|
|
118
|
+
request('GET /backend-api/aip/connectors/{id}',options,{
|
|
119
|
+
pathValues:[nonempty(options.connectorId,'connectorId')]
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
exports.listCustomMcpTunnels = async (options = {}) =>
|
|
123
|
+
request('GET /backend-api/aip/connectors/mcp/tunnels',options);
|
|
124
|
+
|
|
125
|
+
exports.getCustomMcpOAuthConfig = async (options = {}, injectedOptions = {}) => {
|
|
126
|
+
// A single URL argument makes `zgpt connector getCustomMcpOAuthConfig https://...` usable on PowerShell.
|
|
127
|
+
if (typeof options === 'string') options={...injectedOptions,mcpUrl:options};
|
|
128
|
+
if (options.customHeaders !== undefined && !Array.isArray(options.customHeaders))
|
|
129
|
+
throw bad('customHeaders must be an array');
|
|
130
|
+
return request('POST /backend-api/aip/connectors/mcp/oauth_config',options,{
|
|
131
|
+
body:{...makeTarget(options),custom_headers:options.customHeaders || []}
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Unlink an account connection without deleting the MCP connector definition.
|
|
136
|
+
// The user must explicitly invoke this destructive operation; never auto-unlink
|
|
137
|
+
// after an auth error, reconnect or action refresh.
|
|
138
|
+
exports.disconnectConnectorLink = async (options = {}, injectedOptions = {}) => {
|
|
139
|
+
if (typeof options === 'string') options = { ...injectedOptions, linkId: options };
|
|
140
|
+
const linkId = nonempty(options.linkId, 'linkId');
|
|
141
|
+
if (!/^link_[A-Za-z0-9_-]+$/.test(linkId))
|
|
142
|
+
throw bad('linkId must be a connector link identifier (link_...)');
|
|
143
|
+
return request('DELETE /backend-api/aip/connectors/links/{id}', options, {
|
|
144
|
+
pathValues: [linkId]
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Refresh uses the linked connection ID, not the connector ID.
|
|
149
|
+
exports.reloadCustomMcpServerActions = async (options = {}, injectedOptions = {}) => {
|
|
150
|
+
// Accept the linkId directly for `zgpt connector reloadCustomMcpServerActions <linkId>`.
|
|
151
|
+
// The legacy options-object form and the SDK's single refresh implementation stay intact.
|
|
152
|
+
if (typeof options === 'string') options = { ...injectedOptions, linkId: options };
|
|
153
|
+
return require('./write').refreshConnectorActions(options);
|
|
154
|
+
};
|
package/src/connector/write.js
CHANGED
|
@@ -21,6 +21,7 @@ exports.refreshConnectorActions = async (options = {}) => {
|
|
|
21
21
|
const target = '/backend-api/aip/connectors/mcp/refresh_actions';
|
|
22
22
|
const requestJson = options.requestJson || direct.requestJson;
|
|
23
23
|
return requireSuccess(await requestJson(target, options.token, options.sessionHeaders, {
|
|
24
|
-
method: 'POST', body: { link_id: options.linkId }, route: target, retry: false
|
|
24
|
+
method: 'POST', body: { link_id: options.linkId }, route: target, retry: false,
|
|
25
|
+
headers:{...options.headers,'OAI-Product-Sku':'CONNECTOR_SETTING'}
|
|
25
26
|
}), target);
|
|
26
27
|
};
|
|
@@ -55,6 +55,35 @@ exports.send = async (id, message, options = {}) => write.sendConversation({
|
|
|
55
55
|
message,
|
|
56
56
|
...(await bindCanonicalContext(options))
|
|
57
57
|
});
|
|
58
|
+
// CLI-friendly management façade matching the verified Zero_LLM command tree.
|
|
59
|
+
// Mutations stay owned by ./write; no copied HTTP routes or parallel auth stack.
|
|
60
|
+
const required = (value, name) => {
|
|
61
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
62
|
+
throw Object.assign(new Error(name + ' is required'), { code: 'BAD_ARGS' });
|
|
63
|
+
}
|
|
64
|
+
return value.trim();
|
|
65
|
+
};
|
|
66
|
+
exports.create = async (message, options = {}) => write.createConversation({
|
|
67
|
+
...(await bindCanonicalContext(options)),
|
|
68
|
+
message: required(message, 'message')
|
|
69
|
+
});
|
|
70
|
+
exports.rename = async (id, title, options = {}) => write.renameConversation({
|
|
71
|
+
...(await bindCanonicalContext(options)),
|
|
72
|
+
conversationId: required(id, 'conversationId'),
|
|
73
|
+
title: required(title, 'title')
|
|
74
|
+
});
|
|
75
|
+
exports.delete = async (id, options = {}) => write.deleteConversation({
|
|
76
|
+
...(await bindCanonicalContext(options)),
|
|
77
|
+
conversationId: required(id, 'conversationId')
|
|
78
|
+
});
|
|
79
|
+
exports.move = async (id, projectId = null, options = {}) => write.moveConversation({
|
|
80
|
+
...(await bindCanonicalContext(options)),
|
|
81
|
+
conversationId: required(id, 'conversationId'),
|
|
82
|
+
projectId: projectId === 'exit' || projectId == null ? null : required(projectId, 'projectId')
|
|
83
|
+
});
|
|
84
|
+
// Compatibility with the Zero_LLM CLI: new/del map to the same owners.
|
|
85
|
+
exports.new = exports.create;
|
|
86
|
+
exports.del = exports.delete;
|
|
58
87
|
exports.temp = {
|
|
59
88
|
new: async (message, options = {}) => temporary.createConversation({
|
|
60
89
|
message,
|
package/src/conversation/read.js
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
const direct = require('../transport/direct/http-client');
|
|
4
4
|
|
|
5
|
+
const pageBudget = (maxPages = 1000) => {
|
|
6
|
+
if (!Number.isSafeInteger(maxPages) || maxPages < 1) {
|
|
7
|
+
throw Object.assign(new Error('maxPages must be a positive safe integer'), { code: 'PAGINATION_BUDGET_INVALID' });
|
|
8
|
+
}
|
|
9
|
+
return maxPages;
|
|
10
|
+
};
|
|
11
|
+
const paginationError = (code, message) => Object.assign(new Error(message), { code });
|
|
12
|
+
|
|
5
13
|
const requireOk = (response, target) => {
|
|
6
14
|
if (!response || response.status !== 200 || !response.json) {
|
|
7
15
|
const error = new Error(`ChatGPT request failed (${response?.status || 'no-status'}): ${target}`);
|
|
@@ -52,25 +60,45 @@ exports.listStandalone = async ({
|
|
|
52
60
|
token,
|
|
53
61
|
sessionHeaders,
|
|
54
62
|
requestJson = direct.requestJson,
|
|
55
|
-
onProgress = () => {}
|
|
63
|
+
onProgress = () => {},
|
|
64
|
+
maxPages = 1000
|
|
56
65
|
}) => {
|
|
66
|
+
const budget = pageBudget(maxPages);
|
|
57
67
|
const items = [];
|
|
68
|
+
const seenItems = new Set();
|
|
58
69
|
let offset = 0;
|
|
59
70
|
const limit = 50;
|
|
60
71
|
|
|
61
|
-
for (let pageNumber = 0; pageNumber <
|
|
72
|
+
for (let pageNumber = 0; pageNumber < budget; pageNumber += 1) {
|
|
62
73
|
const target = `/backend-api/conversations?offset=${offset}&limit=${limit}&order=updated&is_archived=false&is_starred=false`;
|
|
63
74
|
const page = requireOk(await requestJson(target, token, sessionHeaders), target);
|
|
64
75
|
const pageItems = Array.isArray(page.items) ? page.items : [];
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
if (page.offset !== undefined && page.offset !== null && Number(page.offset) !== offset) {
|
|
77
|
+
throw paginationError('PAGINATION_STALLED', 'conversation pagination server offset did not advance');
|
|
78
|
+
}
|
|
79
|
+
let added = 0;
|
|
80
|
+
for (const item of pageItems) {
|
|
81
|
+
const id = item?.id || item?.conversation_id;
|
|
82
|
+
const key = id ? `id:${id}` : `row:${JSON.stringify(item)}`;
|
|
83
|
+
if (seenItems.has(key)) continue;
|
|
84
|
+
seenItems.add(key);
|
|
85
|
+
items.push(item);
|
|
86
|
+
added++;
|
|
87
|
+
}
|
|
88
|
+
if (pageItems.length > 0 && added === 0 && pageNumber > 0) {
|
|
89
|
+
throw paginationError('PAGINATION_STALLED', 'conversation pagination returned no new items');
|
|
90
|
+
}
|
|
91
|
+
onProgress({ type: 'conversation-page', count: items.length, pageCount: added, offset });
|
|
67
92
|
|
|
68
93
|
const total = Number(page.total);
|
|
69
94
|
const pageLimit = Number(page.limit) || limit;
|
|
95
|
+
if (!Number.isSafeInteger(pageLimit) || pageLimit <= 0) {
|
|
96
|
+
throw paginationError('PAGINATION_STALLED', 'conversation pagination returned an invalid page limit');
|
|
97
|
+
}
|
|
70
98
|
offset += pageLimit;
|
|
71
99
|
if (!Number.isFinite(total) || offset >= total || pageItems.length === 0) return items;
|
|
72
100
|
}
|
|
73
|
-
throw
|
|
101
|
+
throw paginationError('PAGINATION_LIMIT', `conversation pagination exceeded ${budget} pages`);
|
|
74
102
|
};
|
|
75
103
|
exports.searchGlobal = async ({
|
|
76
104
|
query,
|
|
@@ -186,10 +214,12 @@ exports.getConversationMessagesPage = async ({
|
|
|
186
214
|
exports.findConversationMessage = async ({
|
|
187
215
|
conversationId, messageId, numTurns = 10, includeHasVersions = true,
|
|
188
216
|
token, sessionHeaders, requestJson = direct.requestJson,
|
|
189
|
-
getConversation = exports.getConversation, getMessagesPage = exports.getConversationMessagesPage
|
|
217
|
+
getConversation = exports.getConversation, getMessagesPage = exports.getConversationMessagesPage,
|
|
218
|
+
maxPages = 1000
|
|
190
219
|
}) => {
|
|
191
220
|
if (!conversationId) throw new Error('conversationId is required');
|
|
192
221
|
if (!messageId) throw new Error('messageId is required');
|
|
222
|
+
const budget = pageBudget(maxPages);
|
|
193
223
|
const current = await getConversation({ conversationId, token, sessionHeaders, requestJson });
|
|
194
224
|
let pagesScanned = 1;
|
|
195
225
|
let nodesScanned = Array.isArray(current?.messages) ? current.messages.length : 0;
|
|
@@ -200,6 +230,7 @@ exports.findConversationMessage = async ({
|
|
|
200
230
|
let hasPrevious = pageInfo.has_previous_page === true;
|
|
201
231
|
const seen = new Set();
|
|
202
232
|
while (hasPrevious && before) {
|
|
233
|
+
if (pagesScanned >= budget) throw paginationError('PAGINATION_LIMIT', `conversation message search exceeded ${budget} pages`);
|
|
203
234
|
if (seen.has(before)) throw Object.assign(new Error('conversation history cursor stalled'), { code: 'PAGINATION_STALLED' });
|
|
204
235
|
seen.add(before);
|
|
205
236
|
const page = await getMessagesPage({ conversationId, before, numTurns, includeHasVersions, token, sessionHeaders, requestJson });
|
|
@@ -220,9 +251,11 @@ exports.findConversationMessage = async ({
|
|
|
220
251
|
exports.getConversationHistory = async ({
|
|
221
252
|
conversationId, numTurns = 10, includeHasVersions = true,
|
|
222
253
|
token, sessionHeaders, requestJson = direct.requestJson,
|
|
223
|
-
getConversation = exports.getConversation, getMessagesPage = exports.getConversationMessagesPage
|
|
254
|
+
getConversation = exports.getConversation, getMessagesPage = exports.getConversationMessagesPage,
|
|
255
|
+
maxPages = 1000
|
|
224
256
|
}) => {
|
|
225
257
|
if (!conversationId) throw new Error('conversationId is required');
|
|
258
|
+
const budget = pageBudget(maxPages);
|
|
226
259
|
const current = await getConversation({ conversationId, token, sessionHeaders, requestJson });
|
|
227
260
|
const pages = [{ ...current, kind: 'current_window', before: null }];
|
|
228
261
|
let messageCount = Array.isArray(current?.messages) ? current.messages.length : 0;
|
|
@@ -231,6 +264,7 @@ exports.getConversationHistory = async ({
|
|
|
231
264
|
let hasPrevious = pageInfo.has_previous_page === true;
|
|
232
265
|
const seen = new Set();
|
|
233
266
|
while (hasPrevious && before) {
|
|
267
|
+
if (pages.length >= budget) throw paginationError('PAGINATION_LIMIT', `conversation history exceeded ${budget} pages`);
|
|
234
268
|
if (seen.has(before)) throw Object.assign(new Error('conversation history cursor stalled'), { code: 'PAGINATION_STALLED' });
|
|
235
269
|
seen.add(before);
|
|
236
270
|
const page = await getMessagesPage({ conversationId, before, numTurns, includeHasVersions, token, sessionHeaders, requestJson });
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const context = require('../runtime-context');
|
|
3
|
+
const direct = require('../transport/direct/http-client');
|
|
4
|
+
|
|
5
|
+
const bodyRequired = (value) => {
|
|
6
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length === 0)
|
|
7
|
+
throw Object.assign(new Error('body must be a nonempty object'), { code: 'BAD_ARGS' });
|
|
8
|
+
return value;
|
|
9
|
+
};
|
|
10
|
+
const idRequired = (value, name) => {
|
|
11
|
+
if (typeof value !== 'string' || value.trim().length === 0)
|
|
12
|
+
throw Object.assign(new Error(name + ' is required'), { code: 'BAD_ARGS' });
|
|
13
|
+
return encodeURIComponent(value);
|
|
14
|
+
};
|
|
15
|
+
const call = async (target, method, options) => {
|
|
16
|
+
const resolved = await context.bind(options);
|
|
17
|
+
const route = method === 'PATCH' ? '/backend-api/accounts/{id}/users/{id}' : target;
|
|
18
|
+
const args = { method, route, retry: false };
|
|
19
|
+
if (method !== 'GET') args.body = bodyRequired(resolved.body);
|
|
20
|
+
const response = await (resolved.requestJson || direct.requestJson)(target, resolved.token, resolved.sessionHeaders, args);
|
|
21
|
+
if (!response || response.status < 200 || response.status >= 300)
|
|
22
|
+
throw Object.assign(new Error('ChatGPT profile request failed (' + (response?.status || 'no-status') + '): ' + target), {
|
|
23
|
+
status: response?.status || null, code: response?.status === 429 ? 'RATE_LIMITED' : 'PROFILE_REQUEST_FAILED', target
|
|
24
|
+
});
|
|
25
|
+
if (method === 'GET' && (response.json === undefined || response.json === null))
|
|
26
|
+
throw Object.assign(new Error('Profile response missing JSON'), { code: 'PROFILE_BAD_RESPONSE' });
|
|
27
|
+
return response.json || { success: true };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// These are captured/static Web Chat routes. Callers must be entitled to the requested surface.
|
|
31
|
+
exports.getMe = (options = {}) => call('/backend-api/profiles/me', 'GET', options);
|
|
32
|
+
exports.getIdentity = (options = {}) => call('/backend-anon/me', 'GET', options);
|
|
33
|
+
exports.getAuraIdentity = (options = {}) => call('/backend-api/aura/identity', 'GET', options);
|
|
34
|
+
exports.getCreator = (options = {}) => call('/backend-api/gizmo_creator_profile', 'GET', options);
|
|
35
|
+
exports.updateCreator = (options = {}) => call('/backend-api/gizmo_creator_profile', 'POST', options);
|
|
36
|
+
exports.updateWorkspaceUser = async (options = {}) => {
|
|
37
|
+
const a = idRequired(options.accountId, 'accountId');
|
|
38
|
+
const u = idRequired(options.userId, 'userId');
|
|
39
|
+
bodyRequired(options.body);
|
|
40
|
+
return call('/backend-api/accounts/' + a + '/users/' + u, 'PATCH', options);
|
|
41
|
+
};
|
package/src/project/index.js
CHANGED
|
@@ -3,4 +3,38 @@
|
|
|
3
3
|
const read = require('./read');
|
|
4
4
|
const write = require('./write');
|
|
5
5
|
|
|
6
|
-
Object.assign(exports, read, write);
|
|
6
|
+
Object.assign(exports, read, write);
|
|
7
|
+
|
|
8
|
+
// Mirror the proven Zero_LLM Project CLI without copying HTTP implementations.
|
|
9
|
+
// All actions use the same canonical Zero SDK session and project owner methods.
|
|
10
|
+
const bindContext = require('../runtime-context').bind;
|
|
11
|
+
const required = (value, name) => {
|
|
12
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
13
|
+
throw Object.assign(new Error(name + ' is required'), { code: 'BAD_ARGS' });
|
|
14
|
+
}
|
|
15
|
+
return value.trim();
|
|
16
|
+
};
|
|
17
|
+
exports.list = async (options = {}) => read.listProjects(await bindContext(options));
|
|
18
|
+
exports.get = async (id, options = {}) => read.getProject({
|
|
19
|
+
...(await bindContext(options)), projectId: required(id, 'projectId')
|
|
20
|
+
});
|
|
21
|
+
exports.conversations = async (id, options = {}) => read.listProjectConversations({
|
|
22
|
+
...(await bindContext(options)), projectId: required(id, 'projectId')
|
|
23
|
+
});
|
|
24
|
+
exports.create = async (name, options = {}) => write.createProject({
|
|
25
|
+
...(await bindContext(options)), name: required(name, 'name')
|
|
26
|
+
});
|
|
27
|
+
exports.rename = async (id, name, options = {}) => write.renameProject({
|
|
28
|
+
...(await bindContext(options)), projectId: required(id, 'projectId'), name: required(name, 'name')
|
|
29
|
+
});
|
|
30
|
+
exports.delete = async (id, options = {}) => write.deleteProject({
|
|
31
|
+
...(await bindContext(options)), projectId: required(id, 'projectId')
|
|
32
|
+
});
|
|
33
|
+
exports.save = async (projectId, conversationId, messageId, options = {}) => write.saveProjectMessage({
|
|
34
|
+
...(await bindContext(options)), projectId: required(projectId, 'projectId'),
|
|
35
|
+
conversationId: required(conversationId, 'conversationId'),
|
|
36
|
+
messageId: required(messageId, 'messageId')
|
|
37
|
+
});
|
|
38
|
+
// Keep the legacy LAB's natural command names without duplicating logic.
|
|
39
|
+
exports.new = exports.create;
|
|
40
|
+
exports.del = exports.delete;
|
|
@@ -1,68 +1,60 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const paths = require('../config/paths');
|
|
5
|
-
const direct = require('../transport/direct/http-client');
|
|
6
|
-
|
|
7
|
-
const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } };
|
|
8
|
-
const mergeSetCookies = (cookieString, setCookie) => {
|
|
9
|
-
const jar = new Map();
|
|
10
|
-
const add = (entry) => {
|
|
11
|
-
const first = String(entry || '').split(';')[0];
|
|
12
|
-
const index = first.indexOf('=');
|
|
13
|
-
if (index > 0) jar.set(first.slice(0, index).trim(), first.slice(index + 1).trim());
|
|
14
|
-
};
|
|
15
|
-
for (const pair of String(cookieString || '').split(';')) add(pair);
|
|
16
|
-
for (const entry of Array.isArray(setCookie) ? setCookie : [setCookie]) add(entry);
|
|
17
|
-
return [...jar].map(([name, value]) => `${name}=${value}`).join('; ');
|
|
18
|
-
};
|
|
19
|
-
const bootstrapHeaders = (env) => {
|
|
20
|
-
const state = readJson(paths.sessionFile(env)) || {};
|
|
21
|
-
const saved = state.headers && typeof state.headers === 'object' && !Array.isArray(state.headers) ? state.headers : {};
|
|
22
|
-
const { Cookie: _cookie, cookie: _cookieLower, authorization: _authorization, ...safe } = saved;
|
|
23
|
-
return {
|
|
24
|
-
...safe,
|
|
25
|
-
origin: safe.origin || 'https://chatgpt.com',
|
|
26
|
-
referer: safe.referer || 'https://chatgpt.com/',
|
|
27
|
-
accept: safe.accept || 'application/json'
|
|
28
|
-
};
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
exports.createTransport = ({ requestJson = direct.requestJson } = {}) => async ({ sessionMarker, workspaceAccountId = null, env = process.env } = {}) => {
|
|
32
|
-
if (!sessionMarker || typeof sessionMarker !== 'string') {
|
|
33
|
-
throw Object.assign(new Error('Missing local ChatGPT session marker'), { code: 'CHATGPT_SESSION_MARKER_MISSING' });
|
|
34
|
-
}
|
|
35
|
-
const sessionFile = paths.sessionFile(env);
|
|
36
|
-
const sessionHeaders = bootstrapHeaders(env);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
sessionHeaders.Cookie = cookie;
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
persistSession: false
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (switchedCookie) sessionHeaders.Cookie = switchedCookie;
|
|
62
|
-
const second = await requestJson('/api/auth/session', null, sessionHeaders, {
|
|
63
|
-
route: '/api/auth/session', env, sessionFile, persistSession: false
|
|
64
|
-
});
|
|
65
|
-
return { status: Number(second?.status || 0), body: second?.json ?? null, sessionHeaders };
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
exports.transport = exports.createTransport();
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const paths = require('../config/paths');
|
|
5
|
+
const direct = require('../transport/direct/http-client');
|
|
6
|
+
|
|
7
|
+
const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } };
|
|
8
|
+
const mergeSetCookies = (cookieString, setCookie) => {
|
|
9
|
+
const jar = new Map();
|
|
10
|
+
const add = (entry) => {
|
|
11
|
+
const first = String(entry || '').split(';')[0];
|
|
12
|
+
const index = first.indexOf('=');
|
|
13
|
+
if (index > 0) jar.set(first.slice(0, index).trim(), first.slice(index + 1).trim());
|
|
14
|
+
};
|
|
15
|
+
for (const pair of String(cookieString || '').split(';')) add(pair);
|
|
16
|
+
for (const entry of Array.isArray(setCookie) ? setCookie : [setCookie]) add(entry);
|
|
17
|
+
return [...jar].map(([name, value]) => `${name}=${value}`).join('; ');
|
|
18
|
+
};
|
|
19
|
+
const bootstrapHeaders = (env) => {
|
|
20
|
+
const state = readJson(paths.sessionFile(env)) || {};
|
|
21
|
+
const saved = state.headers && typeof state.headers === 'object' && !Array.isArray(state.headers) ? state.headers : {};
|
|
22
|
+
const { Cookie: _cookie, cookie: _cookieLower, authorization: _authorization, ...safe } = saved;
|
|
23
|
+
return {
|
|
24
|
+
...safe,
|
|
25
|
+
origin: safe.origin || 'https://chatgpt.com',
|
|
26
|
+
referer: safe.referer || 'https://chatgpt.com/',
|
|
27
|
+
accept: safe.accept || 'application/json'
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
exports.createTransport = ({ requestJson = direct.requestJson } = {}) => async ({ sessionMarker, workspaceAccountId = null, env = process.env } = {}) => {
|
|
32
|
+
if (!sessionMarker || typeof sessionMarker !== 'string') {
|
|
33
|
+
throw Object.assign(new Error('Missing local ChatGPT session marker'), { code: 'CHATGPT_SESSION_MARKER_MISSING' });
|
|
34
|
+
}
|
|
35
|
+
const sessionFile = paths.sessionFile(env);
|
|
36
|
+
const sessionHeaders = bootstrapHeaders(env);
|
|
37
|
+
sessionHeaders.Cookie = mergeSetCookies(sessionHeaders.Cookie || sessionHeaders.cookie || '', `__Secure-next-auth.session-token=${sessionMarker}`);
|
|
38
|
+
delete sessionHeaders.cookie;
|
|
39
|
+
|
|
40
|
+
const first = await requestJson('/api/auth/session', null, sessionHeaders, {
|
|
41
|
+
route: '/api/auth/session', env, sessionFile, persistSession: false
|
|
42
|
+
});
|
|
43
|
+
if (![200, 403].includes(Number(first?.status))) {
|
|
44
|
+
throw Object.assign(new Error('ChatGPT session bootstrap round one failed'), { code: 'CHATGPT_SESSION_BOOTSTRAP_FAILED', status: first?.status || 0 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
sessionHeaders.Cookie = mergeSetCookies(sessionHeaders.Cookie, first?.headers?.['set-cookie']);
|
|
48
|
+
const firstBody = first?.json;
|
|
49
|
+
const firstAuthenticated = Boolean(firstBody && typeof firstBody === 'object' && !Array.isArray(firstBody) && (firstBody.user || firstBody.account || firstBody.accessToken || firstBody.sessionToken));
|
|
50
|
+
if (Number(first?.status) === 200 && firstAuthenticated) {
|
|
51
|
+
return { status: 200, body: firstBody, sessionHeaders };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const second = await requestJson('/api/auth/session', null, sessionHeaders, {
|
|
55
|
+
route: '/api/auth/session', env, sessionFile, persistSession: false
|
|
56
|
+
});
|
|
57
|
+
return { status: Number(second?.status || 0), body: second?.json ?? null, sessionHeaders };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
exports.transport = exports.createTransport();
|
package/src/settings/index.js
CHANGED
|
@@ -15,4 +15,6 @@ exports.updateAccountUserSetting = async (options = {}) => {
|
|
|
15
15
|
if (!options.body || typeof options.body !== 'object' || Array.isArray(options.body)) throw Object.assign(new Error('body is required'), { code: 'BAD_ARGS' });
|
|
16
16
|
const target = '/backend-api/settings/account_user_setting';
|
|
17
17
|
return requireOk(await (options.requestJson || direct.requestJson)(target, options.token, options.sessionHeaders, { method: 'PATCH', retry: false, route: target, body: options.body }), target);
|
|
18
|
-
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
Object.assign(exports, require('./web'));
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const context = require('../runtime-context');
|
|
3
|
+
const direct = require('../transport/direct/http-client');
|
|
4
|
+
|
|
5
|
+
const invoke = async (route, method, options = {}) => {
|
|
6
|
+
const requestOptions = { route, method, retry: false };
|
|
7
|
+
if (method !== 'GET') {
|
|
8
|
+
if (!options.body || typeof options.body !== 'object' || Array.isArray(options.body) || Object.keys(options.body).length === 0)
|
|
9
|
+
throw Object.assign(new Error('body must be a nonempty object'), { code: 'BAD_ARGS' });
|
|
10
|
+
requestOptions.body = options.body;
|
|
11
|
+
}
|
|
12
|
+
const bound = await context.bind(options);
|
|
13
|
+
const response = await (bound.requestJson || direct.requestJson)(route, bound.token, bound.sessionHeaders, requestOptions);
|
|
14
|
+
if (!response || response.status < 200 || response.status >= 300) {
|
|
15
|
+
const error = new Error('ChatGPT settings request failed (' + (response?.status || 'no-status') + '): ' + route);
|
|
16
|
+
error.status = response?.status || null;
|
|
17
|
+
error.target = route;
|
|
18
|
+
error.code = response?.status === 429 ? 'RATE_LIMITED' : 'SETTINGS_REQUEST_FAILED';
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
if (method === 'GET' && (response.json === undefined || response.json === null))
|
|
22
|
+
throw Object.assign(new Error('Settings response missing JSON'), { code: 'SETTINGS_BAD_RESPONSE' });
|
|
23
|
+
return response.json || { success: true };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
exports.getVoices = (options = {}) => invoke('/backend-api/settings/voices', 'GET', options);
|
|
27
|
+
exports.getTrainingDataUsage = (options = {}) => invoke('/backend-api/accounts/data_usage_for_training', 'GET', options);
|
|
28
|
+
exports.getAgeVerificationEnabled = (options = {}) => invoke('/backend-api/settings/age_verification_enabled', 'GET', options);
|
|
29
|
+
exports.getAdultStatus = (options = {}) => invoke('/backend-api/settings/is_adult', 'GET', options);
|
|
30
|
+
exports.getConsent = (options = {}) => invoke('/backend-api/user_granular_consent', 'GET', options);
|
|
31
|
+
exports.updateConsent = (options = {}) => invoke('/backend-api/user_granular_consent', 'POST', options);
|
|
32
|
+
exports.getNotifications = (options = {}) => invoke('/backend-api/notifications/settings', 'GET', options);
|
|
33
|
+
exports.updateNotifications = (options = {}) => invoke('/backend-api/notifications/settings', 'PATCH', options);
|
|
34
|
+
exports.setDefaultModel = (options = {}) => invoke('/backend-api/settings/user_default_model_config', 'PATCH', options);
|
|
35
|
+
exports.setLastUsedModel = (options = {}) => invoke('/backend-api/settings/user_last_used_model_config', 'PATCH', options);
|
|
36
|
+
exports.setTppDefaultModel = (options = {}) => invoke('/backend-api/settings/user_tpp_default_model_config', 'PATCH', options);
|
|
37
|
+
exports.setTppLastUsedModel = (options = {}) => invoke('/backend-api/settings/user_tpp_last_used_model_config', 'PATCH', options);
|
|
38
|
+
exports.setLocale = (options = {}) => invoke('/backend-api/accounts/users/locale', 'PATCH', options);
|