@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,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,25 @@ 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');
|
|
9
|
+
const browserObservedData = require('./browser-observed.json');
|
|
10
|
+
const contracts = require('./contracts.json');
|
|
8
11
|
|
|
9
|
-
const catalog = Object.freeze(catalogData.entries.map((entry) =>
|
|
10
|
-
|
|
12
|
+
const catalog = Object.freeze([...catalogData.entries, ...discoveredData.entries, ...browserObservedData.entries].map((entry) =>
|
|
13
|
+
Object.freeze({ ...entry, ...(contracts[entry.key] || {}) })));
|
|
14
|
+
if (new Set(catalog.map((entry) => entry.key)).size !== catalog.length ||
|
|
15
|
+
new Set(catalog.map((entry) => entry.name)).size !== catalog.length) {
|
|
16
|
+
throw new Error('ChatGPT Web API catalog contains duplicate keys or function names');
|
|
17
|
+
}
|
|
18
|
+
const summary = Object.freeze({
|
|
19
|
+
...catalogData.summary,
|
|
20
|
+
total: catalog.length,
|
|
21
|
+
publicRequest: catalog.filter((entry) => entry.client === 'PublicRequest').length,
|
|
22
|
+
supplemental: discoveredData.entries.length,
|
|
23
|
+
browserObserved: browserObservedData.entries.length,
|
|
24
|
+
source: catalogData.source,
|
|
25
|
+
supplementSource: discoveredData.source
|
|
26
|
+
});
|
|
11
27
|
|
|
12
28
|
const readSessionHeaders = (env) => {
|
|
13
29
|
const file = paths.sessionFile(env);
|
|
@@ -103,7 +119,41 @@ exports.find = (criteria = {}) => exports.list(criteria)[0] || null;
|
|
|
103
119
|
|
|
104
120
|
exports.invoke = async (reference, options = {}) => {
|
|
105
121
|
const entry = resolveEntry(reference);
|
|
106
|
-
const
|
|
122
|
+
for (const parameter of entry.requiredQuery || []) {
|
|
123
|
+
const value = options.query?.[parameter];
|
|
124
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
125
|
+
const err = new Error(`ChatGPT API requires query parameter ${parameter}: ${entry.key}`);
|
|
126
|
+
err.code = 'API_QUERY_PARAMETER_REQUIRED';
|
|
127
|
+
err.parameter = parameter;
|
|
128
|
+
err.route = entry.route;
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const field of entry.requiredBodyFields || []) {
|
|
133
|
+
if (!options.body || typeof options.body !== 'object' || !Object.hasOwn(options.body,field) ||
|
|
134
|
+
options.body[field] === undefined || options.body[field] === null) {
|
|
135
|
+
const err = new Error(`ChatGPT API requires body field ${field}: ${entry.key}`);
|
|
136
|
+
err.code='API_BODY_PARAMETER_REQUIRED';
|
|
137
|
+
err.parameter=field;
|
|
138
|
+
err.route=entry.route;
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (entry.bodyAnyOf?.length && !entry.bodyAnyOf.some(field =>
|
|
143
|
+
typeof options.body?.[field] === 'string' && options.body[field].trim())) {
|
|
144
|
+
const err = new Error(`ChatGPT API requires one of ${entry.bodyAnyOf.join(', ')}: ${entry.key}`);
|
|
145
|
+
err.code='API_BODY_PARAMETER_REQUIRED';
|
|
146
|
+
err.parameter=entry.bodyAnyOf.join('|');
|
|
147
|
+
err.route=entry.route;
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
const rendered = renderRoute(entry.route, options);
|
|
151
|
+
// PublicRequest uses /public-api as its base. Captured wire paths already include it.
|
|
152
|
+
const endpoint = entry.client === 'PublicRequest' && !rendered.startsWith('/public-api/')
|
|
153
|
+
? '/public-api' + rendered : rendered;
|
|
154
|
+
// Captured 307 redirect: /models/ canonicalizes to /models. Keep the catalog alias callable.
|
|
155
|
+
const canonicalEndpoint = endpoint === '/backend-api/models/' ? '/backend-api/models' : endpoint;
|
|
156
|
+
const target = appendQuery(canonicalEndpoint, options.query);
|
|
107
157
|
const context = await bindContext(entry, options);
|
|
108
158
|
const responseType = options.responseType === 'text' ? 'text' : 'json';
|
|
109
159
|
const request = responseType === 'text'
|
|
@@ -111,7 +161,7 @@ exports.invoke = async (reference, options = {}) => {
|
|
|
111
161
|
: (options.requestJson || direct.requestJson);
|
|
112
162
|
const requestOptions = {
|
|
113
163
|
method: entry.method,
|
|
114
|
-
route: entry.route,
|
|
164
|
+
route: canonicalEndpoint === endpoint ? entry.route : canonicalEndpoint,
|
|
115
165
|
retry: false,
|
|
116
166
|
...(options.body === undefined ? {} : { body: options.body }),
|
|
117
167
|
...(options.headers ? { headers: options.headers } : {}),
|
|
@@ -133,7 +183,15 @@ exports.invoke = async (reference, options = {}) => {
|
|
|
133
183
|
}
|
|
134
184
|
if (options.rawResponse === true) return response;
|
|
135
185
|
if (responseType === 'text') return response.text ?? '';
|
|
136
|
-
|
|
186
|
+
if (response.status === 204 || response.status === 205) return { success: true };
|
|
187
|
+
if (response.json === null || response.json === undefined) {
|
|
188
|
+
const error = new Error(`ChatGPT API expected JSON but received no valid JSON: ${entry.method} ${entry.route}`);
|
|
189
|
+
error.code = 'API_UNEXPECTED_RESPONSE';
|
|
190
|
+
error.status = response.status;
|
|
191
|
+
error.route = entry.route;
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
return response.json;
|
|
137
195
|
};
|
|
138
196
|
|
|
139
197
|
const byKey = Object.create(null);
|
|
@@ -0,0 +1,341 @@
|
|
|
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|conversations)$|(?: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 publicOauthMetadata = new Set([
|
|
15
|
+
'authorization_url','authorization_server_base','authorization_response_iss_parameter_supported'
|
|
16
|
+
]);
|
|
17
|
+
const safeOauthMetadata = (key, entry) => {
|
|
18
|
+
if (!publicOauthMetadata.has(key)) return false;
|
|
19
|
+
if (key === 'authorization_response_iss_parameter_supported') return typeof entry === 'boolean' || entry === null;
|
|
20
|
+
if (typeof entry !== 'string') return false;
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(entry);
|
|
23
|
+
return url.protocol === 'https:' && !url.username && !url.password && !url.hash &&
|
|
24
|
+
[...url.searchParams.keys()].every(param =>
|
|
25
|
+
!/(token|code|state|nonce|secret|password|credential|key)/i.test(param));
|
|
26
|
+
} catch { return false; }
|
|
27
|
+
};
|
|
28
|
+
const redact = (value, depth = 0, seen = new WeakSet(), oauthMetadata = false) => {
|
|
29
|
+
if (depth > 30) return '[DEPTH_LIMIT]';
|
|
30
|
+
if (!value || typeof value !== 'object') return value;
|
|
31
|
+
if (seen.has(value)) return '[CIRCULAR]';
|
|
32
|
+
seen.add(value);
|
|
33
|
+
if (Array.isArray(value)) return value.map(entry => redact(entry, depth + 1, seen, oauthMetadata));
|
|
34
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => {
|
|
35
|
+
const isConfig = key === 'oauth_config' && entry && typeof entry === 'object' && !Array.isArray(entry);
|
|
36
|
+
const allowMetadata = oauthMetadata && safeOauthMetadata(key, entry);
|
|
37
|
+
return [key, secretKey.test(key) && !isConfig && !allowMetadata
|
|
38
|
+
? '[REDACTED]' : redact(entry, depth + 1, seen, oauthMetadata || !!isConfig)];
|
|
39
|
+
}));
|
|
40
|
+
};
|
|
41
|
+
const show = (stdout, value) => stdout(json(redact(value)));
|
|
42
|
+
const jsonArgument = (input, flag, kind) => {
|
|
43
|
+
let value;
|
|
44
|
+
try { value = JSON.parse(input); }
|
|
45
|
+
catch { throw error(flag + ' requires valid JSON'); }
|
|
46
|
+
if (kind === 'array' && !Array.isArray(value)) throw error(flag + ' must be a JSON array');
|
|
47
|
+
if (kind === 'object' && (value === null || typeof value !== 'object' || Array.isArray(value)))
|
|
48
|
+
throw error(flag + ' must be a JSON object');
|
|
49
|
+
return value;
|
|
50
|
+
};
|
|
51
|
+
const queryArgument = (input) => {
|
|
52
|
+
if (input.startsWith('{')) return jsonArgument(input, '--query', 'object');
|
|
53
|
+
if (!input.includes('=')) throw error('--query requires JSON or name=value parameters');
|
|
54
|
+
const query = {};
|
|
55
|
+
for (const [name,value] of new URLSearchParams(input)) {
|
|
56
|
+
if (!name || forbiddenKeys.has(name)) throw error('Invalid --query key');
|
|
57
|
+
if (Object.hasOwn(query,name)) {
|
|
58
|
+
const prior=query[name];
|
|
59
|
+
query[name]=Array.isArray(prior)?[...prior,value]:[prior,value];
|
|
60
|
+
} else query[name]=value;
|
|
61
|
+
}
|
|
62
|
+
return query;
|
|
63
|
+
};
|
|
64
|
+
const optionsFrom = (args, allowed) => {
|
|
65
|
+
const flags = {};
|
|
66
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
67
|
+
const key = args[index];
|
|
68
|
+
if (!allowed.includes(key)) throw error('Unknown option: ' + key);
|
|
69
|
+
if (Object.hasOwn(flags, key)) throw error('Duplicate option: ' + key);
|
|
70
|
+
if (key === '--allow-write' || key === '--json') { flags[key] = true; continue; }
|
|
71
|
+
if (index + 1 >= args.length || args[index + 1].startsWith('--'))
|
|
72
|
+
throw error('Missing value after ' + key);
|
|
73
|
+
flags[key] = args[++index];
|
|
74
|
+
}
|
|
75
|
+
return flags;
|
|
76
|
+
};
|
|
77
|
+
const getPublicFunction = (runtime, path) => {
|
|
78
|
+
if (typeof path !== 'string' || !/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(path)) return null;
|
|
79
|
+
const parts = path.split('.');
|
|
80
|
+
if (parts[0] === 'api' || parts.some(part => forbiddenKeys.has(part))) return null;
|
|
81
|
+
let current = runtime;
|
|
82
|
+
for (const part of parts) current = own(current, part);
|
|
83
|
+
return typeof current === 'function' ? current : null;
|
|
84
|
+
};
|
|
85
|
+
const availableFunctions = (runtime, namespace) => {
|
|
86
|
+
const found = [];
|
|
87
|
+
const visit = (obj, prefix, depth) => {
|
|
88
|
+
if (!obj || typeof obj !== 'object' || depth > 5) return;
|
|
89
|
+
for (const key of Object.keys(obj)) {
|
|
90
|
+
if (forbiddenKeys.has(key)) continue;
|
|
91
|
+
const value = own(obj, key);
|
|
92
|
+
const name = prefix ? prefix + '.' + key : key;
|
|
93
|
+
if (typeof value === 'function') found.push(name);
|
|
94
|
+
else if (value && typeof value === 'object' && depth < 5) visit(value, name, depth + 1);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
for (const owner of Object.keys(runtime)) {
|
|
98
|
+
if (owner === 'api' || forbiddenKeys.has(owner) || namespace && owner !== namespace) continue;
|
|
99
|
+
visit(own(runtime, owner), owner, 0);
|
|
100
|
+
}
|
|
101
|
+
return found.sort();
|
|
102
|
+
};
|
|
103
|
+
const writeRequired = (method) => !['GET','HEAD','OPTIONS'].includes(String(method || '').toUpperCase());
|
|
104
|
+
|
|
105
|
+
const sdkNamespace = (runtime, key) => {
|
|
106
|
+
if (typeof key !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(key) ||
|
|
107
|
+
key === 'api' || forbiddenKeys.has(key)) return null;
|
|
108
|
+
const candidate = own(runtime, key);
|
|
109
|
+
return candidate && typeof candidate === 'object' ? candidate : null;
|
|
110
|
+
};
|
|
111
|
+
const namespaceHelp = (runtime, name) => {
|
|
112
|
+
if (!sdkNamespace(runtime, name)) throw error('Unknown SDK namespace: ' + name);
|
|
113
|
+
const methods = availableFunctions(runtime, name).map(path => path.slice(name.length + 1));
|
|
114
|
+
return [
|
|
115
|
+
'zgpt ' + name + ' — SDK namespace',
|
|
116
|
+
'',
|
|
117
|
+
'Usage: zgpt ' + name + ' <method> [arguments...] [--args JSON_ARRAY] [--allow-write]',
|
|
118
|
+
'Methods (' + methods.length + '):',
|
|
119
|
+
...methods.map(method => ' zgpt ' + name + ' ' + method),
|
|
120
|
+
'',
|
|
121
|
+
'Details: zgpt help ' + name,
|
|
122
|
+
'Run state-changing methods explicitly with --allow-write.'
|
|
123
|
+
].join('\n');
|
|
124
|
+
};
|
|
125
|
+
const apiFamilies = (api) => {
|
|
126
|
+
const counts = new Map();
|
|
127
|
+
for (const item of Array.isArray(own(api, 'catalog')) ? api.catalog : []) {
|
|
128
|
+
if (typeof item.family === 'string' && item.family) {
|
|
129
|
+
counts.set(item.family, (counts.get(item.family) || 0) + 1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return [...counts].sort((a, b) => a[0].localeCompare(b[0]));
|
|
133
|
+
};
|
|
134
|
+
const apiRootHelp = (api, complete = false) => {
|
|
135
|
+
const families = apiFamilies(api);
|
|
136
|
+
const visible = complete ? families : families.slice(0, 18);
|
|
137
|
+
return [
|
|
138
|
+
'zgpt api — ChatGPT Web API catalog',
|
|
139
|
+
'',
|
|
140
|
+
'Usage:',
|
|
141
|
+
' zgpt api <family> [methodName]',
|
|
142
|
+
' zgpt api <family> list [--method GET] [--json]',
|
|
143
|
+
' zgpt api list [--family NAME] [--method GET] [--json]',
|
|
144
|
+
' zgpt api show "GET /backend-api/..."',
|
|
145
|
+
' zgpt api invoke "GET /backend-api/..."',
|
|
146
|
+
'',
|
|
147
|
+
'API families (' + families.length + '):',
|
|
148
|
+
...visible.map(([name, count]) => ' ' + name.padEnd(27) + count + ' routes'),
|
|
149
|
+
...(visible.length < families.length ? [' ... (' + (families.length - visible.length) + ' more: zgpt api families)'] : []),
|
|
150
|
+
'',
|
|
151
|
+
'View a family: zgpt api <family>',
|
|
152
|
+
'Routes are evidence-backed and not all are live-verified.'
|
|
153
|
+
].join('\n');
|
|
154
|
+
};
|
|
155
|
+
const apiFamilyHelp = (api, family) => {
|
|
156
|
+
const matching = api.catalog.filter(item => item.family === family);
|
|
157
|
+
if (!matching.length) throw error('Unknown API family: ' + family);
|
|
158
|
+
return [
|
|
159
|
+
'zgpt api ' + family + ' — ' + matching.length + ' routes',
|
|
160
|
+
'',
|
|
161
|
+
'Usage: zgpt api ' + family + ' <methodName> [--path-values JSON_ARRAY] [--query JSON_OBJECT]',
|
|
162
|
+
'',
|
|
163
|
+
...matching.map(item => ' zgpt api ' + family + ' ' + item.name + ' [' + item.method + ' ' + item.route + ']'),
|
|
164
|
+
'',
|
|
165
|
+
'Writes require --allow-write. API routes may be provisional.'
|
|
166
|
+
].join('\n');
|
|
167
|
+
};
|
|
168
|
+
exports.help = (runtime = require('../index')) => {
|
|
169
|
+
const categories = Object.keys(runtime)
|
|
170
|
+
.filter(name => name !== 'api' && sdkNamespace(runtime, name))
|
|
171
|
+
.sort()
|
|
172
|
+
.map(name => [name, availableFunctions(runtime, name).length]);
|
|
173
|
+
const api = own(runtime, 'api');
|
|
174
|
+
const total = api && Array.isArray(api.catalog) ? api.catalog.length : 0;
|
|
175
|
+
return [
|
|
176
|
+
'Zero ChatGPT CLI',
|
|
177
|
+
'',
|
|
178
|
+
'Usage:',
|
|
179
|
+
' zgpt <namespace> <method> [arguments...] [--args JSON_ARRAY]',
|
|
180
|
+
' zgpt help <namespace>',
|
|
181
|
+
' zgpt api <family> <methodName> [options]',
|
|
182
|
+
'',
|
|
183
|
+
'SDK namespaces:',
|
|
184
|
+
...categories.map(([name, count]) => ' ' + name.padEnd(17) + count + ' functions'),
|
|
185
|
+
...(api ? [' api'.padEnd(19) + total + ' catalog routes'] : []),
|
|
186
|
+
'',
|
|
187
|
+
'Examples:',
|
|
188
|
+
' zgpt profile getMe',
|
|
189
|
+
' zgpt project listProjects',
|
|
190
|
+
' zgpt help profile',
|
|
191
|
+
' zgpt api models',
|
|
192
|
+
'',
|
|
193
|
+
'Legacy aliases (still supported):',
|
|
194
|
+
' zgpt login | logout | auth-status',
|
|
195
|
+
' zgpt conversations list [--json]',
|
|
196
|
+
' zgpt get <conversation-id> [--limit N|A-B] [--debug|--json]',
|
|
197
|
+
' zgpt export <conversation-id> [--out <path>]',
|
|
198
|
+
' zgpt retry <conversation-id>',
|
|
199
|
+
' zgpt functions [--namespace NAME] [--json]',
|
|
200
|
+
' zgpt call <namespace.function> [--args JSON_ARRAY] [--allow-write]',
|
|
201
|
+
'',
|
|
202
|
+
'Change operations require --allow-write on the grouped command interface.'
|
|
203
|
+
].join('\n');
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
async function apiCommand(argv, runtime, stdout) {
|
|
207
|
+
const api = own(runtime, 'api');
|
|
208
|
+
if (!api) throw error('The loaded SDK has no API catalog');
|
|
209
|
+
const action = argv[1];
|
|
210
|
+
if (!action || action === 'help' || action === '--help') {
|
|
211
|
+
if (argv.length > 2) throw error('Usage: zgpt api help');
|
|
212
|
+
stdout(apiRootHelp(api));
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
if (action === 'families') {
|
|
216
|
+
if (argv.length > 3 || argv[2] && argv[2] !== '--json') throw error('Usage: zgpt api families [--json]');
|
|
217
|
+
const families = apiFamilies(api);
|
|
218
|
+
if (argv[2] === '--json') show(stdout, families.map(([name,count]) => ({name,count})));
|
|
219
|
+
else stdout(apiRootHelp(api, true));
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
// Any family discovered in the SDK catalog is a navigable CLI command.
|
|
223
|
+
if (!['list','show','invoke'].includes(action)) {
|
|
224
|
+
if (!Array.isArray(own(api, 'catalog'))) throw error('The loaded SDK does not expose api.catalog');
|
|
225
|
+
if (!apiFamilies(api).some(([name]) => name === action)) throw error('Unknown API family: ' + action);
|
|
226
|
+
const selected = argv[2];
|
|
227
|
+
if (!selected || selected === 'help' || selected === '--help') {
|
|
228
|
+
if (argv.length > 3) throw error('Usage: zgpt api ' + action + ' help');
|
|
229
|
+
stdout(apiFamilyHelp(api, action));
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
if (selected === 'list') {
|
|
233
|
+
const flags = optionsFrom(argv.slice(3), ['--method','--json']);
|
|
234
|
+
const rows = api.catalog.filter(item => item.family === action &&
|
|
235
|
+
(!flags['--method'] || item.method === flags['--method'].toUpperCase()));
|
|
236
|
+
if (flags['--json']) show(stdout, rows);
|
|
237
|
+
else stdout(rows.map(item => item.key).join('\\n'));
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
const entry = api.catalog.find(item => item.family === action &&
|
|
241
|
+
(item.name === selected || item.key === selected));
|
|
242
|
+
if (!entry) throw error('Unknown API method in ' + action + ': ' + selected);
|
|
243
|
+
return apiCommand(['api','invoke',entry.key,...argv.slice(3)], runtime, stdout);
|
|
244
|
+
}
|
|
245
|
+
if (action === 'list') {
|
|
246
|
+
if (typeof own(api, 'list') !== 'function') throw error('The loaded SDK does not expose api.list');
|
|
247
|
+
const opts = optionsFrom(argv.slice(2), ['--family', '--method', '--json']);
|
|
248
|
+
const criteria = {};
|
|
249
|
+
if (opts['--family']) criteria.family = opts['--family'];
|
|
250
|
+
if (opts['--method']) criteria.method = opts['--method'].toUpperCase();
|
|
251
|
+
const rows = api.list(criteria);
|
|
252
|
+
if (opts['--json']) show(stdout, rows);
|
|
253
|
+
else stdout(rows.map(item => item.key).join('\n'));
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
if (action === 'show') {
|
|
257
|
+
if (!Array.isArray(own(api, 'catalog'))) throw error('The loaded SDK does not expose api.catalog');
|
|
258
|
+
const key = argv[2];
|
|
259
|
+
if (!key || argv.length !== 3) throw error('Usage: zgpt api show "GET /backend-api/..."');
|
|
260
|
+
const entry = api.catalog.find(item => item.key === key || item.name === key);
|
|
261
|
+
if (!entry) throw error('Unknown API capability: ' + key);
|
|
262
|
+
show(stdout, entry);
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
if (action === 'invoke') {
|
|
266
|
+
if (!Array.isArray(own(api, 'catalog')) || typeof own(api, 'invoke') !== 'function')
|
|
267
|
+
throw error('The loaded SDK does not expose api.invoke/catalog');
|
|
268
|
+
const key = argv[2];
|
|
269
|
+
if (!key) throw error('API method and route key required');
|
|
270
|
+
const entry = api.catalog.find(item => item.key === key || item.name === key);
|
|
271
|
+
if (!entry) throw error('Unknown API capability: ' + key);
|
|
272
|
+
const flags = optionsFrom(argv.slice(3),
|
|
273
|
+
['--path-values', '--query', '--body', '--options', '--allow-write']);
|
|
274
|
+
const options = flags['--options'] === undefined ? {} : jsonArgument(flags['--options'], '--options', 'object');
|
|
275
|
+
for (const key of ['token','sessionHeaders','requestJson','requestText','env','sessionFile','headers','rawResponse','auth'])
|
|
276
|
+
if (Object.hasOwn(options, key)) throw error('CLI cannot override SDK authentication/transport: ' + key);
|
|
277
|
+
if (flags['--path-values']) options.pathValues = jsonArgument(flags['--path-values'], '--path-values', 'array');
|
|
278
|
+
if (flags['--query']) options.query = queryArgument(flags['--query']);
|
|
279
|
+
if (flags['--body']) options.body = jsonArgument(flags['--body'], '--body', 'object');
|
|
280
|
+
if (writeRequired(entry.method) && !flags['--allow-write']) throw error('Non-read API requires --allow-write');
|
|
281
|
+
if (!writeRequired(entry.method) && Object.hasOwn(options, 'body')) throw error('Read-only API cannot use --body');
|
|
282
|
+
show(stdout, await api.invoke(entry.key, options));
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
throw error('Usage: zgpt api list|show|invoke');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function callCommand(argv, runtime, stdout) {
|
|
289
|
+
const name = argv[1];
|
|
290
|
+
const fn = getPublicFunction(runtime, name);
|
|
291
|
+
if (!fn) throw error('Unknown SDK function: ' + (name || '(missing)'));
|
|
292
|
+
const tail = argv.slice(2);
|
|
293
|
+
const firstFlag = tail.findIndex(item => item.startsWith('--'));
|
|
294
|
+
const positionals = firstFlag === -1 ? tail : tail.slice(0, firstFlag);
|
|
295
|
+
const flags = optionsFrom(firstFlag === -1 ? [] : tail.slice(firstFlag), ['--args', '--allow-write']);
|
|
296
|
+
if (positionals.length && flags['--args'] !== undefined) throw error('Use positional arguments or --args, not both');
|
|
297
|
+
const args = flags['--args'] === undefined ? positionals : jsonArgument(flags['--args'], '--args', 'array');
|
|
298
|
+
const method = name.split('.').at(-1);
|
|
299
|
+
if (!safeReaderName.test(method) && !flags['--allow-write']) throw error('Mutating/unknown SDK function requires --allow-write');
|
|
300
|
+
show(stdout, await fn(...args));
|
|
301
|
+
return 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
exports.run = async (argv, options = {}) => {
|
|
305
|
+
const runtime = options.runtime || require('../index');
|
|
306
|
+
const stdout = options.stdout || console.log;
|
|
307
|
+
const stderr = options.stderr || console.error;
|
|
308
|
+
try {
|
|
309
|
+
if (argv[0] === 'help' || argv[0] === '--help' || argv[0] === '-h') {
|
|
310
|
+
const name = argv[1];
|
|
311
|
+
if (argv.length > 3 || argv.length === 3 && name !== 'api') throw error('Usage: zgpt help [namespace]');
|
|
312
|
+
if (!name) stdout(exports.help(runtime));
|
|
313
|
+
else if (name === 'api') stdout(argv[2] ? apiFamilyHelp(own(runtime, 'api'), argv[2]) : apiRootHelp(own(runtime, 'api')));
|
|
314
|
+
else stdout(namespaceHelp(runtime, name));
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
if (argv[0] === 'functions') {
|
|
318
|
+
const flags = optionsFrom(argv.slice(1), ['--namespace','--json']);
|
|
319
|
+
if (flags['--namespace'] && !Object.hasOwn(runtime, flags['--namespace']))
|
|
320
|
+
throw error('Unknown SDK namespace: ' + flags['--namespace']);
|
|
321
|
+
const names = availableFunctions(runtime, flags['--namespace']);
|
|
322
|
+
if (flags['--json']) show(stdout, names);
|
|
323
|
+
else stdout(names.join('\n'));
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
if (argv[0] === 'api') return await apiCommand(argv, runtime, stdout);
|
|
327
|
+
if (argv[0] === 'call') return await callCommand(argv, runtime, stdout);
|
|
328
|
+
if (sdkNamespace(runtime, argv[0])) {
|
|
329
|
+
if (!argv[1] || argv[1] === 'help' || argv[1] === '--help') {
|
|
330
|
+
if (argv.length > 2) throw error('Usage: zgpt ' + argv[0] + ' [method]');
|
|
331
|
+
stdout(namespaceHelp(runtime, argv[0]));
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
return await callCommand(['call', argv[0] + '.' + argv[1], ...argv.slice(2)], runtime, stdout);
|
|
335
|
+
}
|
|
336
|
+
throw error('Unknown SDK namespace or dispatch command: ' + (argv[0] || '(missing)'));
|
|
337
|
+
} catch (cause) {
|
|
338
|
+
stderr((cause.code === 'CLI_USAGE' ? '' : (cause.code ? cause.code + ': ' : '')) + (cause.message || 'SDK dispatch failed'));
|
|
339
|
+
return cause.exitCode || 1;
|
|
340
|
+
}
|
|
341
|
+
};
|
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;
|