@miruzero/zgpt 0.1.3 → 0.1.4
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 +49 -1
- package/docs/architecture.md +5 -5
- package/package.json +5 -2
- package/src/api/discovered.json +165 -0
- package/src/api/index.js +19 -3
- package/src/cli/sdk-dispatch.js +310 -0
- package/src/cli.js +8 -15
- package/src/index.js +2 -3
- package/src/profile/index.js +41 -0
- package/src/session/direct-transport.js +60 -68
- package/src/settings/index.js +3 -1
- package/src/settings/web.js +38 -0
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ This package contains only ChatGPT-specific runtime code:
|
|
|
14
14
|
- direct HTTP/SSE transport
|
|
15
15
|
- ChatGPT mutable state under `M:\Zero_ChatGPT\state`
|
|
16
16
|
|
|
17
|
-
It contains no Zero Core, MCP, Agent orchestration,
|
|
17
|
+
It contains no Zero Core, MCP, Agent orchestration, or generic device/task runtime. The CLI is a thin adapter over the same public SDK exports.
|
|
18
18
|
|
|
19
19
|
## CLI
|
|
20
20
|
|
|
@@ -34,6 +34,27 @@ npx zgpt
|
|
|
34
34
|
|
|
35
35
|
The package exposes the `zgpt` executable through npm's `bin` field. A local `npm install` does not add `node_modules/.bin` to the parent shell's PATH, so use `npx zgpt` for local installs or install globally for the bare `zgpt` command.
|
|
36
36
|
|
|
37
|
+
### Hierarchical SDK → CLI automatic command discovery
|
|
38
|
+
|
|
39
|
+
The CLI discovers top-level public SDK namespaces at runtime, and groups the methods under each namespace. New namespaces and methods appear automatically without a second command registry. Use `zgpt help`, `zgpt help profile`, or `zgpt api families` to browse:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
zgpt help profile
|
|
43
|
+
zgpt profile getMe
|
|
44
|
+
zgpt profile getAuraIdentity
|
|
45
|
+
zgpt conversation get conversation-id
|
|
46
|
+
zgpt api models
|
|
47
|
+
zgpt api models getModels
|
|
48
|
+
zgpt api invoke "GET /public-api/conversation_limit"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- Preferred: `zgpt <namespace> <method> [arguments...]` dispatches to the same SDK owner function. Use `--args JSON_ARRAY` for structured parameters. Older `zgpt call namespace.function` remains compatible.
|
|
52
|
+
- `zgpt api <family> <methodName>` groups the API catalog by `family` without duplicating routes. `zgpt api invoke "METHOD /route"` remains available, and supports `--path-values`, `--query`, `--options`, and `--body` for JSON input.
|
|
53
|
+
- Explicit non-read operations need `--allow-write` and required arguments. The generic adapter does not invent request payloads or permissions.
|
|
54
|
+
- Results are JSON and credentials in recognized sensitive fields are redacted for CLI output; direct JS callers receive unmodified objects.
|
|
55
|
+
- On this host, `npm link` points to `M:\Zero_ChatGPT`, so source changes take effect in the next CLI process. For a separately installed or published npm package, its installed version must be updated to receive changes.
|
|
56
|
+
|
|
57
|
+
|
|
37
58
|
## Public API
|
|
38
59
|
|
|
39
60
|
```js
|
|
@@ -45,8 +66,35 @@ await chatgpt.conversation.get(id);
|
|
|
45
66
|
await chatgpt.conversation.send(id, message);
|
|
46
67
|
await chatgpt.project.listProjects(options);
|
|
47
68
|
await chatgpt.connector.listConnectors(options);
|
|
69
|
+
|
|
70
|
+
// Profile and Chat settings (only when entitled; writes require explicit bodies)
|
|
71
|
+
await chatgpt.profile.getMe();
|
|
72
|
+
await chatgpt.profile.getCreator();
|
|
73
|
+
await chatgpt.profile.updateCreator({ body: { display_name: 'Sky' } });
|
|
74
|
+
await chatgpt.settings.getUser();
|
|
75
|
+
await chatgpt.settings.getConsent();
|
|
76
|
+
await chatgpt.settings.getNotifications();
|
|
77
|
+
await chatgpt.profile.getAuraIdentity(); // GET /backend-api/aura/identity
|
|
78
|
+
|
|
79
|
+
// ChatGPT Web API catalog: saved Web evidence plus verified supplemental routes.
|
|
80
|
+
// Generic routes require appropriate account permissions and exact request contracts.
|
|
81
|
+
console.log(chatgpt.api.summary.total); // 1287 (includes provisional paths)
|
|
82
|
+
const route = chatgpt.api.find({ method: 'GET', route: '/api/auth/session' });
|
|
83
|
+
const authSession = await chatgpt.api.invoke(route.key);
|
|
84
|
+
const limits = await chatgpt.api.invoke('GET /public-api/conversation_limit');
|
|
85
|
+
// Never log authSession: it can contain authentication credentials.
|
|
86
|
+
|
|
87
|
+
// Parameterized route and explicit one-shot writes (only with a verified payload):
|
|
88
|
+
// await chatgpt.api.invoke('POST /backend-api/hosting/sites/{id}/access',
|
|
89
|
+
// { pathValues: [siteId], body: verifiedBody });
|
|
48
90
|
```
|
|
49
91
|
|
|
92
|
+
Discovered Web endpoints and their evidence are stored in `src/api/discovered.json`.
|
|
93
|
+
Historical endpoints remain in `src/api/catalog.json`; the runtime combines both.
|
|
94
|
+
`api.byKey`, `api.functions` and `api.groups` provide typed route-name dispatch.
|
|
95
|
+
This is an **undocumented Web endpoint catalog**, not a guarantee of API stability,
|
|
96
|
+
authorization, correct mutation payloads, or complete browser-feature parity.
|
|
97
|
+
|
|
50
98
|
## Runtime rules
|
|
51
99
|
|
|
52
100
|
- Direct ChatGPT Web transport is the only conversation transport.
|
package/docs/architecture.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
## Runtime
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
|
-
auth / session / conversation / project / connector / file / memory / settings / share
|
|
10
|
+
auth / session / conversation / project / connector / file / memory / settings / profile / share / api
|
|
11
11
|
|
|
|
12
12
|
protocol
|
|
13
13
|
sentinel / proof / conduit / payload / SSE
|
|
@@ -43,7 +43,9 @@ The state directory is the only default writer for ChatGPT runtime state.
|
|
|
43
43
|
- `connector` owns ChatGPT Chat connector discovery, app metadata, system hints, actions, refresh, and mention resolution.
|
|
44
44
|
- `file` owns Chat composer file reservation, file-entry creation, upload processing, and reservation finalization.
|
|
45
45
|
- `memory` owns Chat memory reads used by the normal Chat surface.
|
|
46
|
-
- `settings` owns normal Chat user settings and explicit
|
|
46
|
+
- `settings` owns normal Chat user settings, consent, notifications, voice options, locale and explicit model-preference mutations.
|
|
47
|
+
- `profile` owns profile reads including the Chrome extension's Aura identity GET, GPT creator profile writes and explicitly addressed account user profile updates; no implicit account mutation.
|
|
48
|
+
- `api` exposes an evidence-backed route catalog (historical and supplemental), with a shared direct transport and proper `/public-api` routing; it is not a separate independent Core/Agent or a promise that each route is an implemented or supported feature. Work-only candidate routes carry explicit scope/evidence flags.
|
|
47
49
|
- `share` owns normal Chat conversation share lifecycle.
|
|
48
50
|
- `protocol` owns ChatGPT Web payload, Sentinel, conduit, and SSE mechanics.
|
|
49
51
|
- `transport` owns HTTP, headers, retries, cooldown, and session-cookie persistence.
|
|
@@ -72,6 +74,4 @@ conversation is the only owner of stream diagnosis and repair. Callers request a
|
|
|
72
74
|
5. External ChatGPT responses are validated before state mutation.
|
|
73
75
|
6. This package owns only ChatGPT runtime state.
|
|
74
76
|
7. No Zero Core, MCP, Agent, device, task, or adapter implementation lives here.
|
|
75
|
-
8. No background polling or hidden retry loop is allowed; recovery work occurs only on the active demand or an explicit retry demand.
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
8. No background polling or hidden retry loop is allowed; recovery work occurs only on the active demand or an explicit retry demand.
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miruzero/zgpt",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"zgpt": "src/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"
|
|
10
|
+
"start": "src/cli.js",
|
|
11
|
+
"test": "node --test",
|
|
12
|
+
"api:rescan": "node scripts/collect-web-api.js",
|
|
13
|
+
"api:reconcile": "node scripts/reconcile-web-api.js"
|
|
11
14
|
},
|
|
12
15
|
"engines": {
|
|
13
16
|
"node": ">=20"
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
{
|
|
2
|
+
"source": "2026-10-11 local OSSGPT, OSSGPT1 and Chrome extension evidence; supplemental to historical catalog",
|
|
3
|
+
"entries": [
|
|
4
|
+
{
|
|
5
|
+
"key": "GET /api/auth/session",
|
|
6
|
+
"method": "GET",
|
|
7
|
+
"route": "/api/auth/session",
|
|
8
|
+
"name": "getWebAuthSession",
|
|
9
|
+
"family": "auth",
|
|
10
|
+
"client": "Request",
|
|
11
|
+
"evidence": [
|
|
12
|
+
"CAPTURE_OSSGPT1"
|
|
13
|
+
],
|
|
14
|
+
"captureCount": 2,
|
|
15
|
+
"statuses": {
|
|
16
|
+
"200": 2
|
|
17
|
+
},
|
|
18
|
+
"scope": "WEB_AUTH",
|
|
19
|
+
"verification": "CAPTURED_SUCCESS"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"key": "GET /backend-api/aura/identity",
|
|
23
|
+
"method": "GET",
|
|
24
|
+
"route": "/backend-api/aura/identity",
|
|
25
|
+
"name": "getAuraIdentity",
|
|
26
|
+
"family": "profile",
|
|
27
|
+
"client": "Request",
|
|
28
|
+
"evidence": [
|
|
29
|
+
"EXTENSION_BACKGROUND_FETCH"
|
|
30
|
+
],
|
|
31
|
+
"captureCount": 0,
|
|
32
|
+
"statuses": {},
|
|
33
|
+
"scope": "CHATGPT_BROWSER_EXTENSION",
|
|
34
|
+
"verification": "STATIC_CALLSITE"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"key": "GET /backend-api/diag",
|
|
38
|
+
"method": "GET",
|
|
39
|
+
"route": "/backend-api/diag",
|
|
40
|
+
"name": "getWebDiagnostics",
|
|
41
|
+
"family": "diag",
|
|
42
|
+
"client": "Request",
|
|
43
|
+
"evidence": [
|
|
44
|
+
"CAPTURE_OSSGPT1"
|
|
45
|
+
],
|
|
46
|
+
"captureCount": 2,
|
|
47
|
+
"statuses": {
|
|
48
|
+
"200": 2
|
|
49
|
+
},
|
|
50
|
+
"scope": "WEB_DIAGNOSTICS",
|
|
51
|
+
"verification": "CAPTURED_SUCCESS"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"key": "GET /backend-api/hosting/sites/{id}/access",
|
|
55
|
+
"method": "GET",
|
|
56
|
+
"route": "/backend-api/hosting/sites/{id}/access",
|
|
57
|
+
"name": "getHostingSiteAccess",
|
|
58
|
+
"family": "hosting",
|
|
59
|
+
"client": "Request",
|
|
60
|
+
"evidence": [
|
|
61
|
+
"STATIC_OSSGPT"
|
|
62
|
+
],
|
|
63
|
+
"captureCount": 0,
|
|
64
|
+
"statuses": {},
|
|
65
|
+
"scope": "WORK_HOSTING",
|
|
66
|
+
"verification": "STATIC_CALLSITE"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"key": "GET /backend-api/models/",
|
|
70
|
+
"method": "GET",
|
|
71
|
+
"route": "/backend-api/models/",
|
|
72
|
+
"name": "getModelsTrailingSlash",
|
|
73
|
+
"family": "models",
|
|
74
|
+
"client": "Request",
|
|
75
|
+
"evidence": [
|
|
76
|
+
"CAPTURE_OSSGPT1"
|
|
77
|
+
],
|
|
78
|
+
"captureCount": 1,
|
|
79
|
+
"statuses": {
|
|
80
|
+
"307": 1
|
|
81
|
+
},
|
|
82
|
+
"scope": "CHATGPT_WEB_REDIRECT_ALIAS",
|
|
83
|
+
"verification": "CAPTURED_REDIRECT_ONLY"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"key": "GET /public-api/conversation_limit",
|
|
87
|
+
"method": "GET",
|
|
88
|
+
"route": "/public-api/conversation_limit",
|
|
89
|
+
"name": "getPublicConversationLimitActualPath",
|
|
90
|
+
"family": "public-request",
|
|
91
|
+
"client": "PublicRequest",
|
|
92
|
+
"evidence": [
|
|
93
|
+
"CAPTURE_OSSGPT"
|
|
94
|
+
],
|
|
95
|
+
"captureCount": 1,
|
|
96
|
+
"statuses": {
|
|
97
|
+
"200": 1
|
|
98
|
+
},
|
|
99
|
+
"scope": "CHATGPT_WEB_PUBLIC",
|
|
100
|
+
"verification": "CAPTURED_SUCCESS"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"key": "GET /public-api/gizmos/{id}",
|
|
104
|
+
"method": "GET",
|
|
105
|
+
"route": "/public-api/gizmos/{id}",
|
|
106
|
+
"name": "getPublicGizmoActualPath",
|
|
107
|
+
"family": "public-request",
|
|
108
|
+
"client": "PublicRequest",
|
|
109
|
+
"evidence": [
|
|
110
|
+
"CAPTURE_OSSGPT"
|
|
111
|
+
],
|
|
112
|
+
"captureCount": 3,
|
|
113
|
+
"statuses": {
|
|
114
|
+
"403": 3
|
|
115
|
+
},
|
|
116
|
+
"scope": "CHATGPT_WEB_PUBLIC",
|
|
117
|
+
"verification": "CAPTURED_FORBIDDEN_ONLY"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"key": "PATCH /backend-api/my/image/{id}/{id}",
|
|
121
|
+
"method": "PATCH",
|
|
122
|
+
"route": "/backend-api/my/image/{id}/{id}",
|
|
123
|
+
"name": "patchMyImageByIdByIdCandidate",
|
|
124
|
+
"family": "my",
|
|
125
|
+
"client": "Request",
|
|
126
|
+
"evidence": [
|
|
127
|
+
"STATIC_OSSGPT"
|
|
128
|
+
],
|
|
129
|
+
"captureCount": 0,
|
|
130
|
+
"statuses": {},
|
|
131
|
+
"scope": "CHATGPT_WEB_DYNAMIC_PATH_UNVALIDATED",
|
|
132
|
+
"verification": "STATIC_DYNAMIC_ROUTE_CANDIDATE"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"key": "POST /backend-api/hosting/sites/{id}/access",
|
|
136
|
+
"method": "POST",
|
|
137
|
+
"route": "/backend-api/hosting/sites/{id}/access",
|
|
138
|
+
"name": "postHostingSiteAccess",
|
|
139
|
+
"family": "hosting",
|
|
140
|
+
"client": "Request",
|
|
141
|
+
"evidence": [
|
|
142
|
+
"STATIC_OSSGPT"
|
|
143
|
+
],
|
|
144
|
+
"captureCount": 0,
|
|
145
|
+
"statuses": {},
|
|
146
|
+
"scope": "WORK_HOSTING",
|
|
147
|
+
"verification": "STATIC_CALLSITE"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"key": "POST /backend-api/hosting/sites/{id}/source_repository/read_credential",
|
|
151
|
+
"method": "POST",
|
|
152
|
+
"route": "/backend-api/hosting/sites/{id}/source_repository/read_credential",
|
|
153
|
+
"name": "postHostingSiteSourceCredentialRead",
|
|
154
|
+
"family": "hosting",
|
|
155
|
+
"client": "Request",
|
|
156
|
+
"evidence": [
|
|
157
|
+
"STATIC_OSSGPT"
|
|
158
|
+
],
|
|
159
|
+
"captureCount": 0,
|
|
160
|
+
"statuses": {},
|
|
161
|
+
"scope": "WORK_HOSTING_SECURITY_SENSITIVE",
|
|
162
|
+
"verification": "STATIC_CALLSITE"
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|
package/src/api/index.js
CHANGED
|
@@ -5,9 +5,21 @@ const auth = require('../auth');
|
|
|
5
5
|
const paths = require('../config/paths');
|
|
6
6
|
const direct = require('../transport/direct/http-client');
|
|
7
7
|
const catalogData = require('./catalog.json');
|
|
8
|
+
const discoveredData = require('./discovered.json');
|
|
8
9
|
|
|
9
|
-
const catalog = Object.freeze(catalogData.entries.map((entry) => Object.freeze({ ...entry })));
|
|
10
|
-
|
|
10
|
+
const catalog = Object.freeze([...catalogData.entries, ...discoveredData.entries].map((entry) => Object.freeze({ ...entry })));
|
|
11
|
+
if (new Set(catalog.map((entry) => entry.key)).size !== catalog.length ||
|
|
12
|
+
new Set(catalog.map((entry) => entry.name)).size !== catalog.length) {
|
|
13
|
+
throw new Error('ChatGPT Web API catalog contains duplicate keys or function names');
|
|
14
|
+
}
|
|
15
|
+
const summary = Object.freeze({
|
|
16
|
+
...catalogData.summary,
|
|
17
|
+
total: catalog.length,
|
|
18
|
+
publicRequest: catalog.filter((entry) => entry.client === 'PublicRequest').length,
|
|
19
|
+
supplemental: discoveredData.entries.length,
|
|
20
|
+
source: catalogData.source,
|
|
21
|
+
supplementSource: discoveredData.source
|
|
22
|
+
});
|
|
11
23
|
|
|
12
24
|
const readSessionHeaders = (env) => {
|
|
13
25
|
const file = paths.sessionFile(env);
|
|
@@ -103,7 +115,11 @@ exports.find = (criteria = {}) => exports.list(criteria)[0] || null;
|
|
|
103
115
|
|
|
104
116
|
exports.invoke = async (reference, options = {}) => {
|
|
105
117
|
const entry = resolveEntry(reference);
|
|
106
|
-
const
|
|
118
|
+
const rendered = renderRoute(entry.route, options);
|
|
119
|
+
// PublicRequest uses /public-api as its base. Captured wire paths already include it.
|
|
120
|
+
const endpoint = entry.client === 'PublicRequest' && !rendered.startsWith('/public-api/')
|
|
121
|
+
? '/public-api' + rendered : rendered;
|
|
122
|
+
const target = appendQuery(endpoint, options.query);
|
|
107
123
|
const context = await bindContext(entry, options);
|
|
108
124
|
const responseType = options.responseType === 'text' ? 'text' : 'json';
|
|
109
125
|
const request = responseType === 'text'
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Generic CLI adapter for the public SDK. No duplicate endpoint registry or operation implementation.
|
|
4
|
+
const forbiddenKeys = new Set(['__proto__', 'prototype', 'constructor']);
|
|
5
|
+
const secretKey = /(?:access[_-]?token|refresh[_-]?token|id[_-]?token|auth(?:orization)?|cookie|password|passphrase|secret|credential|csrf|api[_-]?key|session[_-]?token|private[_-]?key)/i;
|
|
6
|
+
const safeReaderName = /^(?:(?:get|list|read|find|search|has|is|can|check|describe|inspect|status)$|(?:get|list|read|find|search|has|is|can|check|describe|inspect|status)[A-Z_])/;
|
|
7
|
+
const error = (message) => Object.assign(new Error(message), { code: 'CLI_USAGE', exitCode: 2 });
|
|
8
|
+
const own = (obj, key) => {
|
|
9
|
+
if (!obj || typeof obj !== 'object' && typeof obj !== 'function' || forbiddenKeys.has(key)) return undefined;
|
|
10
|
+
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
|
|
11
|
+
return descriptor && Object.hasOwn(descriptor, 'value') ? descriptor.value : undefined;
|
|
12
|
+
};
|
|
13
|
+
const json = (value) => JSON.stringify(value, null, 2);
|
|
14
|
+
const redact = (value, depth = 0, seen = new WeakSet()) => {
|
|
15
|
+
if (depth > 30) return '[DEPTH_LIMIT]';
|
|
16
|
+
if (!value || typeof value !== 'object') return value;
|
|
17
|
+
if (seen.has(value)) return '[CIRCULAR]';
|
|
18
|
+
seen.add(value);
|
|
19
|
+
if (Array.isArray(value)) return value.map((entry) => redact(entry, depth + 1, seen));
|
|
20
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) =>
|
|
21
|
+
[key, secretKey.test(key) ? '[REDACTED]' : redact(entry, depth + 1, seen)]));
|
|
22
|
+
};
|
|
23
|
+
const show = (stdout, value) => stdout(json(redact(value)));
|
|
24
|
+
const jsonArgument = (input, flag, kind) => {
|
|
25
|
+
let value;
|
|
26
|
+
try { value = JSON.parse(input); }
|
|
27
|
+
catch { throw error(flag + ' requires valid JSON'); }
|
|
28
|
+
if (kind === 'array' && !Array.isArray(value)) throw error(flag + ' must be a JSON array');
|
|
29
|
+
if (kind === 'object' && (value === null || typeof value !== 'object' || Array.isArray(value)))
|
|
30
|
+
throw error(flag + ' must be a JSON object');
|
|
31
|
+
return value;
|
|
32
|
+
};
|
|
33
|
+
const optionsFrom = (args, allowed) => {
|
|
34
|
+
const flags = {};
|
|
35
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
36
|
+
const key = args[index];
|
|
37
|
+
if (!allowed.includes(key)) throw error('Unknown option: ' + key);
|
|
38
|
+
if (Object.hasOwn(flags, key)) throw error('Duplicate option: ' + key);
|
|
39
|
+
if (key === '--allow-write' || key === '--json') { flags[key] = true; continue; }
|
|
40
|
+
if (index + 1 >= args.length || args[index + 1].startsWith('--'))
|
|
41
|
+
throw error('Missing value after ' + key);
|
|
42
|
+
flags[key] = args[++index];
|
|
43
|
+
}
|
|
44
|
+
return flags;
|
|
45
|
+
};
|
|
46
|
+
const getPublicFunction = (runtime, path) => {
|
|
47
|
+
if (typeof path !== 'string' || !/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(path)) return null;
|
|
48
|
+
const parts = path.split('.');
|
|
49
|
+
if (parts[0] === 'api' || parts.some(part => forbiddenKeys.has(part))) return null;
|
|
50
|
+
let current = runtime;
|
|
51
|
+
for (const part of parts) current = own(current, part);
|
|
52
|
+
return typeof current === 'function' ? current : null;
|
|
53
|
+
};
|
|
54
|
+
const availableFunctions = (runtime, namespace) => {
|
|
55
|
+
const found = [];
|
|
56
|
+
const visit = (obj, prefix, depth) => {
|
|
57
|
+
if (!obj || typeof obj !== 'object' || depth > 5) return;
|
|
58
|
+
for (const key of Object.keys(obj)) {
|
|
59
|
+
if (forbiddenKeys.has(key)) continue;
|
|
60
|
+
const value = own(obj, key);
|
|
61
|
+
const name = prefix ? prefix + '.' + key : key;
|
|
62
|
+
if (typeof value === 'function') found.push(name);
|
|
63
|
+
else if (value && typeof value === 'object' && depth < 5) visit(value, name, depth + 1);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
for (const owner of Object.keys(runtime)) {
|
|
67
|
+
if (owner === 'api' || forbiddenKeys.has(owner) || namespace && owner !== namespace) continue;
|
|
68
|
+
visit(own(runtime, owner), owner, 0);
|
|
69
|
+
}
|
|
70
|
+
return found.sort();
|
|
71
|
+
};
|
|
72
|
+
const writeRequired = (method) => !['GET','HEAD','OPTIONS'].includes(String(method || '').toUpperCase());
|
|
73
|
+
|
|
74
|
+
const sdkNamespace = (runtime, key) => {
|
|
75
|
+
if (typeof key !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(key) ||
|
|
76
|
+
key === 'api' || forbiddenKeys.has(key)) return null;
|
|
77
|
+
const candidate = own(runtime, key);
|
|
78
|
+
return candidate && typeof candidate === 'object' ? candidate : null;
|
|
79
|
+
};
|
|
80
|
+
const namespaceHelp = (runtime, name) => {
|
|
81
|
+
if (!sdkNamespace(runtime, name)) throw error('Unknown SDK namespace: ' + name);
|
|
82
|
+
const methods = availableFunctions(runtime, name).map(path => path.slice(name.length + 1));
|
|
83
|
+
return [
|
|
84
|
+
'zgpt ' + name + ' — SDK namespace',
|
|
85
|
+
'',
|
|
86
|
+
'Usage: zgpt ' + name + ' <method> [arguments...] [--args JSON_ARRAY] [--allow-write]',
|
|
87
|
+
'Methods (' + methods.length + '):',
|
|
88
|
+
...methods.map(method => ' zgpt ' + name + ' ' + method),
|
|
89
|
+
'',
|
|
90
|
+
'Details: zgpt help ' + name,
|
|
91
|
+
'Run state-changing methods explicitly with --allow-write.'
|
|
92
|
+
].join('\n');
|
|
93
|
+
};
|
|
94
|
+
const apiFamilies = (api) => {
|
|
95
|
+
const counts = new Map();
|
|
96
|
+
for (const item of Array.isArray(own(api, 'catalog')) ? api.catalog : []) {
|
|
97
|
+
if (typeof item.family === 'string' && item.family) {
|
|
98
|
+
counts.set(item.family, (counts.get(item.family) || 0) + 1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return [...counts].sort((a, b) => a[0].localeCompare(b[0]));
|
|
102
|
+
};
|
|
103
|
+
const apiRootHelp = (api, complete = false) => {
|
|
104
|
+
const families = apiFamilies(api);
|
|
105
|
+
const visible = complete ? families : families.slice(0, 18);
|
|
106
|
+
return [
|
|
107
|
+
'zgpt api — ChatGPT Web API catalog',
|
|
108
|
+
'',
|
|
109
|
+
'Usage:',
|
|
110
|
+
' zgpt api <family> [methodName]',
|
|
111
|
+
' zgpt api <family> list [--method GET] [--json]',
|
|
112
|
+
' zgpt api list [--family NAME] [--method GET] [--json]',
|
|
113
|
+
' zgpt api show "GET /backend-api/..."',
|
|
114
|
+
' zgpt api invoke "GET /backend-api/..."',
|
|
115
|
+
'',
|
|
116
|
+
'API families (' + families.length + '):',
|
|
117
|
+
...visible.map(([name, count]) => ' ' + name.padEnd(27) + count + ' routes'),
|
|
118
|
+
...(visible.length < families.length ? [' ... (' + (families.length - visible.length) + ' more: zgpt api families)'] : []),
|
|
119
|
+
'',
|
|
120
|
+
'View a family: zgpt api <family>',
|
|
121
|
+
'Routes are evidence-backed and not all are live-verified.'
|
|
122
|
+
].join('\n');
|
|
123
|
+
};
|
|
124
|
+
const apiFamilyHelp = (api, family) => {
|
|
125
|
+
const matching = api.catalog.filter(item => item.family === family);
|
|
126
|
+
if (!matching.length) throw error('Unknown API family: ' + family);
|
|
127
|
+
return [
|
|
128
|
+
'zgpt api ' + family + ' — ' + matching.length + ' routes',
|
|
129
|
+
'',
|
|
130
|
+
'Usage: zgpt api ' + family + ' <methodName> [--path-values JSON_ARRAY] [--query JSON_OBJECT]',
|
|
131
|
+
'',
|
|
132
|
+
...matching.map(item => ' zgpt api ' + family + ' ' + item.name + ' [' + item.method + ' ' + item.route + ']'),
|
|
133
|
+
'',
|
|
134
|
+
'Writes require --allow-write. API routes may be provisional.'
|
|
135
|
+
].join('\n');
|
|
136
|
+
};
|
|
137
|
+
exports.help = (runtime = require('../index')) => {
|
|
138
|
+
const categories = Object.keys(runtime)
|
|
139
|
+
.filter(name => name !== 'api' && sdkNamespace(runtime, name))
|
|
140
|
+
.sort()
|
|
141
|
+
.map(name => [name, availableFunctions(runtime, name).length]);
|
|
142
|
+
const api = own(runtime, 'api');
|
|
143
|
+
const total = api && Array.isArray(api.catalog) ? api.catalog.length : 0;
|
|
144
|
+
return [
|
|
145
|
+
'Zero ChatGPT CLI',
|
|
146
|
+
'',
|
|
147
|
+
'Usage:',
|
|
148
|
+
' zgpt <namespace> <method> [arguments...] [--args JSON_ARRAY]',
|
|
149
|
+
' zgpt help <namespace>',
|
|
150
|
+
' zgpt api <family> <methodName> [options]',
|
|
151
|
+
'',
|
|
152
|
+
'SDK namespaces:',
|
|
153
|
+
...categories.map(([name, count]) => ' ' + name.padEnd(17) + count + ' functions'),
|
|
154
|
+
...(api ? [' api'.padEnd(19) + total + ' catalog routes'] : []),
|
|
155
|
+
'',
|
|
156
|
+
'Examples:',
|
|
157
|
+
' zgpt profile getMe',
|
|
158
|
+
' zgpt project listProjects',
|
|
159
|
+
' zgpt help profile',
|
|
160
|
+
' zgpt api models',
|
|
161
|
+
'',
|
|
162
|
+
'Legacy aliases (still supported):',
|
|
163
|
+
' zgpt login | logout | auth-status',
|
|
164
|
+
' zgpt conversations list [--json]',
|
|
165
|
+
' zgpt get <conversation-id> [--limit N|A-B] [--debug|--json]',
|
|
166
|
+
' zgpt export <conversation-id> [--out <path>]',
|
|
167
|
+
' zgpt retry <conversation-id>',
|
|
168
|
+
' zgpt functions [--namespace NAME] [--json]',
|
|
169
|
+
' zgpt call <namespace.function> [--args JSON_ARRAY] [--allow-write]',
|
|
170
|
+
'',
|
|
171
|
+
'Change operations require --allow-write on the grouped command interface.'
|
|
172
|
+
].join('\n');
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
async function apiCommand(argv, runtime, stdout) {
|
|
176
|
+
const api = own(runtime, 'api');
|
|
177
|
+
if (!api) throw error('The loaded SDK has no API catalog');
|
|
178
|
+
const action = argv[1];
|
|
179
|
+
if (!action || action === 'help' || action === '--help') {
|
|
180
|
+
if (argv.length > 2) throw error('Usage: zgpt api help');
|
|
181
|
+
stdout(apiRootHelp(api));
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
if (action === 'families') {
|
|
185
|
+
if (argv.length > 3 || argv[2] && argv[2] !== '--json') throw error('Usage: zgpt api families [--json]');
|
|
186
|
+
const families = apiFamilies(api);
|
|
187
|
+
if (argv[2] === '--json') show(stdout, families.map(([name,count]) => ({name,count})));
|
|
188
|
+
else stdout(apiRootHelp(api, true));
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
// Any family discovered in the SDK catalog is a navigable CLI command.
|
|
192
|
+
if (!['list','show','invoke'].includes(action)) {
|
|
193
|
+
if (!Array.isArray(own(api, 'catalog'))) throw error('The loaded SDK does not expose api.catalog');
|
|
194
|
+
if (!apiFamilies(api).some(([name]) => name === action)) throw error('Unknown API family: ' + action);
|
|
195
|
+
const selected = argv[2];
|
|
196
|
+
if (!selected || selected === 'help' || selected === '--help') {
|
|
197
|
+
if (argv.length > 3) throw error('Usage: zgpt api ' + action + ' help');
|
|
198
|
+
stdout(apiFamilyHelp(api, action));
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
if (selected === 'list') {
|
|
202
|
+
const flags = optionsFrom(argv.slice(3), ['--method','--json']);
|
|
203
|
+
const rows = api.catalog.filter(item => item.family === action &&
|
|
204
|
+
(!flags['--method'] || item.method === flags['--method'].toUpperCase()));
|
|
205
|
+
if (flags['--json']) show(stdout, rows);
|
|
206
|
+
else stdout(rows.map(item => item.key).join('\\n'));
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
const entry = api.catalog.find(item => item.family === action &&
|
|
210
|
+
(item.name === selected || item.key === selected));
|
|
211
|
+
if (!entry) throw error('Unknown API method in ' + action + ': ' + selected);
|
|
212
|
+
return apiCommand(['api','invoke',entry.key,...argv.slice(3)], runtime, stdout);
|
|
213
|
+
}
|
|
214
|
+
if (action === 'list') {
|
|
215
|
+
if (typeof own(api, 'list') !== 'function') throw error('The loaded SDK does not expose api.list');
|
|
216
|
+
const opts = optionsFrom(argv.slice(2), ['--family', '--method', '--json']);
|
|
217
|
+
const criteria = {};
|
|
218
|
+
if (opts['--family']) criteria.family = opts['--family'];
|
|
219
|
+
if (opts['--method']) criteria.method = opts['--method'].toUpperCase();
|
|
220
|
+
const rows = api.list(criteria);
|
|
221
|
+
if (opts['--json']) show(stdout, rows);
|
|
222
|
+
else stdout(rows.map(item => item.key).join('\n'));
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
if (action === 'show') {
|
|
226
|
+
if (!Array.isArray(own(api, 'catalog'))) throw error('The loaded SDK does not expose api.catalog');
|
|
227
|
+
const key = argv[2];
|
|
228
|
+
if (!key || argv.length !== 3) throw error('Usage: zgpt api show "GET /backend-api/..."');
|
|
229
|
+
const entry = api.catalog.find(item => item.key === key || item.name === key);
|
|
230
|
+
if (!entry) throw error('Unknown API capability: ' + key);
|
|
231
|
+
show(stdout, entry);
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
if (action === 'invoke') {
|
|
235
|
+
if (!Array.isArray(own(api, 'catalog')) || typeof own(api, 'invoke') !== 'function')
|
|
236
|
+
throw error('The loaded SDK does not expose api.invoke/catalog');
|
|
237
|
+
const key = argv[2];
|
|
238
|
+
if (!key) throw error('API method and route key required');
|
|
239
|
+
const entry = api.catalog.find(item => item.key === key || item.name === key);
|
|
240
|
+
if (!entry) throw error('Unknown API capability: ' + key);
|
|
241
|
+
const flags = optionsFrom(argv.slice(3),
|
|
242
|
+
['--path-values', '--query', '--body', '--options', '--allow-write']);
|
|
243
|
+
const options = flags['--options'] === undefined ? {} : jsonArgument(flags['--options'], '--options', 'object');
|
|
244
|
+
for (const key of ['token','sessionHeaders','requestJson','requestText','env','sessionFile','headers','rawResponse','auth'])
|
|
245
|
+
if (Object.hasOwn(options, key)) throw error('CLI cannot override SDK authentication/transport: ' + key);
|
|
246
|
+
if (flags['--path-values']) options.pathValues = jsonArgument(flags['--path-values'], '--path-values', 'array');
|
|
247
|
+
if (flags['--query']) options.query = jsonArgument(flags['--query'], '--query', 'object');
|
|
248
|
+
if (flags['--body']) options.body = jsonArgument(flags['--body'], '--body', 'object');
|
|
249
|
+
if (writeRequired(entry.method) && !flags['--allow-write']) throw error('Non-read API requires --allow-write');
|
|
250
|
+
if (!writeRequired(entry.method) && Object.hasOwn(options, 'body')) throw error('Read-only API cannot use --body');
|
|
251
|
+
show(stdout, await api.invoke(entry.key, options));
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
throw error('Usage: zgpt api list|show|invoke');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function callCommand(argv, runtime, stdout) {
|
|
258
|
+
const name = argv[1];
|
|
259
|
+
const fn = getPublicFunction(runtime, name);
|
|
260
|
+
if (!fn) throw error('Unknown SDK function: ' + (name || '(missing)'));
|
|
261
|
+
const tail = argv.slice(2);
|
|
262
|
+
const firstFlag = tail.findIndex(item => item.startsWith('--'));
|
|
263
|
+
const positionals = firstFlag === -1 ? tail : tail.slice(0, firstFlag);
|
|
264
|
+
const flags = optionsFrom(firstFlag === -1 ? [] : tail.slice(firstFlag), ['--args', '--allow-write']);
|
|
265
|
+
if (positionals.length && flags['--args'] !== undefined) throw error('Use positional arguments or --args, not both');
|
|
266
|
+
const args = flags['--args'] === undefined ? positionals : jsonArgument(flags['--args'], '--args', 'array');
|
|
267
|
+
const method = name.split('.').at(-1);
|
|
268
|
+
if (!safeReaderName.test(method) && !flags['--allow-write']) throw error('Mutating/unknown SDK function requires --allow-write');
|
|
269
|
+
show(stdout, await fn(...args));
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
exports.run = async (argv, options = {}) => {
|
|
274
|
+
const runtime = options.runtime || require('../index');
|
|
275
|
+
const stdout = options.stdout || console.log;
|
|
276
|
+
const stderr = options.stderr || console.error;
|
|
277
|
+
try {
|
|
278
|
+
if (argv[0] === 'help' || argv[0] === '--help' || argv[0] === '-h') {
|
|
279
|
+
const name = argv[1];
|
|
280
|
+
if (argv.length > 3 || argv.length === 3 && name !== 'api') throw error('Usage: zgpt help [namespace]');
|
|
281
|
+
if (!name) stdout(exports.help(runtime));
|
|
282
|
+
else if (name === 'api') stdout(argv[2] ? apiFamilyHelp(own(runtime, 'api'), argv[2]) : apiRootHelp(own(runtime, 'api')));
|
|
283
|
+
else stdout(namespaceHelp(runtime, name));
|
|
284
|
+
return 0;
|
|
285
|
+
}
|
|
286
|
+
if (argv[0] === 'functions') {
|
|
287
|
+
const flags = optionsFrom(argv.slice(1), ['--namespace','--json']);
|
|
288
|
+
if (flags['--namespace'] && !Object.hasOwn(runtime, flags['--namespace']))
|
|
289
|
+
throw error('Unknown SDK namespace: ' + flags['--namespace']);
|
|
290
|
+
const names = availableFunctions(runtime, flags['--namespace']);
|
|
291
|
+
if (flags['--json']) show(stdout, names);
|
|
292
|
+
else stdout(names.join('\n'));
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
if (argv[0] === 'api') return await apiCommand(argv, runtime, stdout);
|
|
296
|
+
if (argv[0] === 'call') return await callCommand(argv, runtime, stdout);
|
|
297
|
+
if (sdkNamespace(runtime, argv[0])) {
|
|
298
|
+
if (!argv[1] || argv[1] === 'help' || argv[1] === '--help') {
|
|
299
|
+
if (argv.length > 2) throw error('Usage: zgpt ' + argv[0] + ' [method]');
|
|
300
|
+
stdout(namespaceHelp(runtime, argv[0]));
|
|
301
|
+
return 0;
|
|
302
|
+
}
|
|
303
|
+
return await callCommand(['call', argv[0] + '.' + argv[1], ...argv.slice(2)], runtime, stdout);
|
|
304
|
+
}
|
|
305
|
+
throw error('Unknown SDK namespace or dispatch command: ' + (argv[0] || '(missing)'));
|
|
306
|
+
} catch (cause) {
|
|
307
|
+
stderr((cause.code === 'CLI_USAGE' ? '' : (cause.code ? cause.code + ': ' : '')) + (cause.message || 'SDK dispatch failed'));
|
|
308
|
+
return cause.exitCode || 1;
|
|
309
|
+
}
|
|
310
|
+
};
|
package/src/cli.js
CHANGED
|
@@ -5,19 +5,7 @@ const fs = require('node:fs');
|
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const formatter = require('./cli/formatter');
|
|
7
7
|
|
|
8
|
-
const usage = () =>
|
|
9
|
-
'Zero ChatGPT CLI',
|
|
10
|
-
'',
|
|
11
|
-
'Usage:',
|
|
12
|
-
' zgpt login',
|
|
13
|
-
' zgpt logout',
|
|
14
|
-
' zgpt auth-status',
|
|
15
|
-
' zgpt conversations list [--json]',
|
|
16
|
-
' zgpt retry <conversation-id>',
|
|
17
|
-
' zgpt get <conversation-id> [--limit N|A-B] [--debug|--json]',
|
|
18
|
-
' zgpt export <conversation-id> [--limit N|A-B] [--debug|--json] [--out <path>]',
|
|
19
|
-
' zgpt help'
|
|
20
|
-
].join('\n');
|
|
8
|
+
const usage = () => require('./cli/sdk-dispatch').help();
|
|
21
9
|
|
|
22
10
|
const parseOptions = (argv) => {
|
|
23
11
|
const result = { json: false, debug: false, limit: null, out: null };
|
|
@@ -49,9 +37,14 @@ exports.run = async (argv = [], options = {}) => {
|
|
|
49
37
|
const stderr = options.stderr || console.error;
|
|
50
38
|
const command = argv[0] || 'help';
|
|
51
39
|
if (command === 'help' || command === '--help' || command === '-h') {
|
|
52
|
-
|
|
53
|
-
return 0;
|
|
40
|
+
return require('./cli/sdk-dispatch').run(argv, { ...options, stdout, stderr });
|
|
54
41
|
}
|
|
42
|
+
// SDK namespaces are routed dynamically; legacy aliases below remain compatible.
|
|
43
|
+
const runtime = options.runtime || require('./index');
|
|
44
|
+
if (command === 'functions' || command === 'call' || command === 'api' ||
|
|
45
|
+
Object.hasOwn(runtime, command) && command !== 'api' && typeof runtime[command] === 'object') {
|
|
46
|
+
return require('./cli/sdk-dispatch').run(argv, { ...options, runtime, stdout, stderr });
|
|
47
|
+
}
|
|
55
48
|
if (!['login', 'logout', 'auth-status', 'conversations', 'get', 'export', 'stream-status', 'retry', 'resume'].includes(command)) {
|
|
56
49
|
stderr(`Unknown command: ${command}\n\n${usage()}`);
|
|
57
50
|
return 2;
|
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
|
+
};
|
|
@@ -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);
|