mohdel 3.6.0 → 3.6.1
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 +4 -1
- package/js/chatgpt/index.js +20 -7
- package/package.json +2 -2
- package/src/cli/chatgpt.js +14 -6
- package/src/cli/index.js +7 -0
package/README.md
CHANGED
|
@@ -42,7 +42,10 @@ Use `mo chatgpt list`, `select <account-id>`, `login --new`, and
|
|
|
42
42
|
data directory (`~/.local/share/mohdel/chatgpt/` on Linux), with owner-only
|
|
43
43
|
files and serialized token refresh. They are separate from the catalog.
|
|
44
44
|
`mo chatgpt login <account-id>` reuses a saved registration after sign-out
|
|
45
|
-
or a failed code exchange.
|
|
45
|
+
or a failed code exchange. ChatGPT lists each registration by name: a new one
|
|
46
|
+
is `Mohdel (<hostname>)`, or the label from `mo chatgpt login --name <label>`
|
|
47
|
+
if you'd rather not send the hostname. The name is fixed when the registration
|
|
48
|
+
is made, and `mo chatgpt list` shows it next to the account id. If a crashed process leaves `accounts.lock`,
|
|
46
49
|
confirm its recorded PID has exited before removing that lock.
|
|
47
50
|
|
|
48
51
|
This OpenAI flow is intended for open-source and personal local tools;
|
package/js/chatgpt/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes, createHash } from 'node:crypto'
|
|
2
2
|
import { createServer } from 'node:http'
|
|
3
|
+
import { hostname } from 'node:os'
|
|
3
4
|
import { createRemoteJWKSet, jwtVerify } from 'jose'
|
|
4
5
|
import { defaultDirectory, readStore, withStore } from './store.js'
|
|
5
6
|
import { discoverModels, visibleModels } from './models.js'
|
|
@@ -12,6 +13,8 @@ const SCOPE = `openid profile email offline_access resource.invoke ${PERMISSION}
|
|
|
12
13
|
const jwks = createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks.json`))
|
|
13
14
|
const random = () => randomBytes(32).toString('base64url')
|
|
14
15
|
const REFRESH_MARGIN = 30000
|
|
16
|
+
// Every registration made before names were stored was sent this hint.
|
|
17
|
+
const UNNAMED_REGISTRATION = 'Mohdel'
|
|
15
18
|
export const usageURL = 'https://chatgpt.com/settings/usage'
|
|
16
19
|
|
|
17
20
|
async function jsonRequest (fetcher, url, options = {}) {
|
|
@@ -61,6 +64,7 @@ function credentials (tokens, previous = {}) {
|
|
|
61
64
|
|
|
62
65
|
const summary = (id, account, active) => ({
|
|
63
66
|
id,
|
|
67
|
+
name: account.name ?? UNNAMED_REGISTRATION,
|
|
64
68
|
email: account.email ?? null,
|
|
65
69
|
active: id === active,
|
|
66
70
|
connected: !!account.access_token,
|
|
@@ -116,7 +120,7 @@ export function createChatGPT (options = {}) {
|
|
|
116
120
|
return visibleModels(await discoverModels(accessToken, { fetch: fetcher }))
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
/** @param {string} [id] @returns {Promise<{revoked: boolean}>} */
|
|
123
|
+
/** @param {string} [id] @returns {Promise<{revoked: boolean, name: string}>} */
|
|
120
124
|
const logout = async (id) => withStore(directory, async (store, save) => {
|
|
121
125
|
id ??= store.active
|
|
122
126
|
const account = store.accounts[id]
|
|
@@ -141,16 +145,25 @@ export function createChatGPT (options = {}) {
|
|
|
141
145
|
for (const key of ['access_token', 'refresh_token', 'id_token', 'expires_at', 'scopes']) delete account[key]
|
|
142
146
|
if (store.active === id) store.active = null
|
|
143
147
|
await save(store)
|
|
144
|
-
return { revoked }
|
|
148
|
+
return { revoked, name: account.name ?? UNNAMED_REGISTRATION }
|
|
145
149
|
})
|
|
146
150
|
|
|
147
|
-
/**
|
|
148
|
-
|
|
151
|
+
/**
|
|
152
|
+
* `name` labels a new registration on the ChatGPT side (default `Mohdel (<hostname>)`);
|
|
153
|
+
* an existing registration keeps the name it was created with.
|
|
154
|
+
* @param {{ authorize: (url: string) => any, accountId?: string, name?: string, timeoutMs?: number }} settings
|
|
155
|
+
*/
|
|
156
|
+
const login = async ({ authorize, accountId, name, timeoutMs = 300000 }) => {
|
|
157
|
+
if (name !== undefined && (typeof name !== 'string' || !name.trim())) throw new Error('ChatGPT registration name must not be empty')
|
|
149
158
|
const initial = await withStore(directory, async (store, save) => {
|
|
150
159
|
if (accountId && !store.accounts[accountId]) throw new Error('Unknown ChatGPT account')
|
|
160
|
+
if (name !== undefined && store.accounts[accountId]) {
|
|
161
|
+
throw new Error('A ChatGPT registration keeps the name it was created with; start a new registration to use another name')
|
|
162
|
+
}
|
|
151
163
|
await save(store)
|
|
152
164
|
return { host: store.host, account: store.accounts[accountId] }
|
|
153
165
|
})
|
|
166
|
+
const label = name ?? `Mohdel (${hostname()})`
|
|
154
167
|
const state = random()
|
|
155
168
|
const nonce = random()
|
|
156
169
|
const verifier = random()
|
|
@@ -193,7 +206,7 @@ export function createChatGPT (options = {}) {
|
|
|
193
206
|
code_challenge_method: 'S256',
|
|
194
207
|
code_challenge: createHash('sha256').update(verifier).digest('base64url')
|
|
195
208
|
}).toString()
|
|
196
|
-
if (!initial.account) url.searchParams.set('agent_name_hint',
|
|
209
|
+
if (!initial.account) url.searchParams.set('agent_name_hint', label)
|
|
197
210
|
if (initial.account?.id_token) url.searchParams.set('id_token_hint', initial.account.id_token)
|
|
198
211
|
const timer = setTimeout(() => fail(new Error('ChatGPT sign-in timed out')), timeoutMs)
|
|
199
212
|
try {
|
|
@@ -205,7 +218,7 @@ export function createChatGPT (options = {}) {
|
|
|
205
218
|
}
|
|
206
219
|
// Keep the issued ID even if code exchange fails; it must not be registered again.
|
|
207
220
|
await withStore(directory, async (store, save) => {
|
|
208
|
-
store.accounts[clientId] ??= { client_id: clientId }
|
|
221
|
+
store.accounts[clientId] ??= { client_id: clientId, name: label }
|
|
209
222
|
await save(store)
|
|
210
223
|
})
|
|
211
224
|
const tokens = await tokenRequest({ grant_type: 'authorization_code', client_id: clientId, code: params.get('code'), code_verifier: verifier, redirect_uri: redirect })
|
|
@@ -213,7 +226,7 @@ export function createChatGPT (options = {}) {
|
|
|
213
226
|
return await withStore(directory, async (store, save) => {
|
|
214
227
|
const previous = store.accounts[clientId]
|
|
215
228
|
if (previous.subject && previous.subject !== identity.sub) throw new Error('ChatGPT returned a different account identity')
|
|
216
|
-
const account = { ...credentials(tokens), client_id: clientId, subject: identity.sub, email: identity.email ?? null }
|
|
229
|
+
const account = { ...credentials(tokens), client_id: clientId, name: previous.name, subject: identity.sub, email: identity.email ?? null }
|
|
217
230
|
store.accounts[clientId] = account
|
|
218
231
|
store.active = clientId
|
|
219
232
|
await save(store)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mohdel",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Christophe Le Bars",
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
|
|
145
145
|
"@opentelemetry/sdk-node": "^0.222.0",
|
|
146
146
|
"chalk": "^6.0.1",
|
|
147
|
-
"mohdel-thin-gate-linux-x64-gnu": "3.6.
|
|
147
|
+
"mohdel-thin-gate-linux-x64-gnu": "3.6.1"
|
|
148
148
|
},
|
|
149
149
|
"dependencies": {
|
|
150
150
|
"@anthropic-ai/sdk": "^0.129.0",
|
package/src/cli/chatgpt.js
CHANGED
|
@@ -3,18 +3,23 @@ import { parseJsonFlag, printAvailableFields, jsonOutput } from './json-output.j
|
|
|
3
3
|
|
|
4
4
|
export async function runChatGPT (args) {
|
|
5
5
|
const output = parseJsonFlag(args)
|
|
6
|
+
const nameAt = args.indexOf('--name')
|
|
7
|
+
const name = nameAt === -1 ? undefined : args.splice(nameAt, 2)[1]
|
|
6
8
|
const [verb, id] = args
|
|
7
9
|
const auth = createChatGPT()
|
|
8
10
|
try {
|
|
11
|
+
if (nameAt !== -1 && (verb !== 'login' || !name || name.startsWith('--'))) {
|
|
12
|
+
throw new Error('Usage: mo chatgpt login --name <label>')
|
|
13
|
+
}
|
|
9
14
|
if (verb === 'list' || verb === 'models') {
|
|
10
|
-
const fields = verb === 'list' ? ['id', 'email', 'active', 'connected', 'planUsage'] : ['id', 'model', 'label']
|
|
15
|
+
const fields = verb === 'list' ? ['id', 'name', 'email', 'active', 'connected', 'planUsage'] : ['id', 'model', 'label']
|
|
11
16
|
if (output.json && !output.fields) { printAvailableFields(fields); return }
|
|
12
17
|
const items = verb === 'list' ? await auth.accounts() : await auth.models(id)
|
|
13
18
|
if (output.json) jsonOutput(items, output.fields)
|
|
14
19
|
else {
|
|
15
20
|
for (const item of items) {
|
|
16
21
|
console.log(verb === 'list'
|
|
17
|
-
? `${item.active ? '*' : ' '} ${item.id} ${item.email ?? ''} ${item.planUsage ? 'Using ChatGPT plan' : item.connected ? 'Plan usage not enabled' : 'Signed out'}`
|
|
22
|
+
? `${item.active ? '*' : ' '} ${item.id} ${item.name} ${item.email ?? ''} ${item.planUsage ? 'Using ChatGPT plan' : item.connected ? 'Plan usage not enabled' : 'Signed out'}`
|
|
18
23
|
: `${item.id} ${item.label}`)
|
|
19
24
|
}
|
|
20
25
|
}
|
|
@@ -23,7 +28,8 @@ export async function runChatGPT (args) {
|
|
|
23
28
|
if (verb === 'login') {
|
|
24
29
|
const active = (await auth.accounts()).find(a => a.active)
|
|
25
30
|
const account = await auth.login({
|
|
26
|
-
accountId: id === '--new' ? undefined : id ?? active?.id,
|
|
31
|
+
accountId: id === '--new' || (name && !id) ? undefined : id ?? active?.id,
|
|
32
|
+
name,
|
|
27
33
|
authorize: value => {
|
|
28
34
|
// Retained ID-token hints must never be printed to a terminal or log.
|
|
29
35
|
const url = new URL(value)
|
|
@@ -31,22 +37,24 @@ export async function runChatGPT (args) {
|
|
|
31
37
|
console.log(`Continue with ChatGPT — open this URL in your browser:\n${url}`)
|
|
32
38
|
}
|
|
33
39
|
})
|
|
34
|
-
console.log(`Connected: ${account.id} ${account.email ?? ''}`)
|
|
40
|
+
console.log(`Connected: ${account.id} ${account.name} ${account.email ?? ''}`)
|
|
35
41
|
console.log(account.planUsage ? 'Using ChatGPT plan. Discover models: mo chatgpt models' : 'Plan usage not enabled. Run mo chatgpt login to grant access.')
|
|
36
42
|
console.log(`Manage usage: ${usageURL}`)
|
|
37
43
|
} else if (verb === 'select' && id) {
|
|
38
44
|
await auth.select(id)
|
|
39
45
|
console.log(`Selected ChatGPT account: ${id}`)
|
|
40
46
|
} else if (verb === 'logout') {
|
|
41
|
-
const { revoked } = await auth.logout(id)
|
|
47
|
+
const { revoked, name: registration } = await auth.logout(id)
|
|
42
48
|
console.log('Signed out locally.')
|
|
43
|
-
if (!revoked) console.log(`Remote revocation was not confirmed. Disconnect
|
|
49
|
+
if (!revoked) console.log(`Remote revocation was not confirmed. Disconnect "${registration}" in ChatGPT Settings: ${usageURL}`)
|
|
44
50
|
} else if (verb === 'usage') {
|
|
45
51
|
console.log(usageURL)
|
|
46
52
|
} else {
|
|
47
53
|
console.log(`mo chatgpt — use your eligible ChatGPT plan
|
|
48
54
|
|
|
49
55
|
login [account-id|--new] Continue with ChatGPT in your browser
|
|
56
|
+
login --name <label> Start a new registration, named as ChatGPT shows it
|
|
57
|
+
(a new registration defaults to Mohdel (<hostname>))
|
|
50
58
|
list [--json fields] Saved accounts and the active account
|
|
51
59
|
select <account-id> Choose a saved account
|
|
52
60
|
logout [account-id] Revoke and clear its local credentials
|
package/src/cli/index.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Aliases: ls → model list, rl → ratelimit
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { createRequire } from 'node:module'
|
|
12
13
|
import { detectAssistants, launchLines } from '../lib/assistants.js'
|
|
13
14
|
import { getConfig } from '../lib/common.js'
|
|
14
15
|
import { ALIASES } from './aliases.js'
|
|
@@ -34,6 +35,11 @@ if (command === 'completion') {
|
|
|
34
35
|
process.exit(0)
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
if (command === '--version') {
|
|
39
|
+
console.log(createRequire(import.meta.url)('../../package.json').version)
|
|
40
|
+
process.exit(0)
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
if (command === '-h' || command === '--help') {
|
|
38
44
|
const assistant = (await getConfig()).assistant || null
|
|
39
45
|
const { default: PROVIDER_INFO } = await import('../lib/provider-info.js')
|
|
@@ -92,6 +98,7 @@ Commands:
|
|
|
92
98
|
default [model] Set the model "mo ask" uses by default
|
|
93
99
|
doctor Check that your install is wired up
|
|
94
100
|
completion bash Shell completion — source <(mo completion bash)
|
|
101
|
+
--version Print the installed mohdel version
|
|
95
102
|
|
|
96
103
|
Catalog work with a coding agent:
|
|
97
104
|
Prices, context limits and cache rates are in no provider API — they live on
|