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 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. If a crashed process leaves `accounts.lock`,
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;
@@ -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
- /** @param {{ authorize: (url: string) => any, accountId?: string, timeoutMs?: number }} settings */
148
- const login = async ({ authorize, accountId, timeoutMs = 300000 }) => {
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', 'Mohdel')
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.0",
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.0"
147
+ "mohdel-thin-gate-linux-x64-gnu": "3.6.1"
148
148
  },
149
149
  "dependencies": {
150
150
  "@anthropic-ai/sdk": "^0.129.0",
@@ -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 Mohdel in ChatGPT Settings: ${usageURL}`)
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