@jackpickard/hexgrid-cli 0.0.3
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 +35 -0
- package/bin/hexgrid.mjs +435 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @jackpickard/hexgrid-cli
|
|
2
|
+
|
|
3
|
+
HexGrid command line client for device login and repo session lifecycle.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @jackpickard/hexgrid-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
hexgrid login
|
|
15
|
+
hexgrid connect --runtime claude
|
|
16
|
+
hexgrid heartbeat
|
|
17
|
+
hexgrid disconnect
|
|
18
|
+
hexgrid me
|
|
19
|
+
hexgrid logout
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Login flow
|
|
23
|
+
|
|
24
|
+
`hexgrid login` uses a browser-based device flow:
|
|
25
|
+
|
|
26
|
+
1. CLI prints an approval URL and code
|
|
27
|
+
2. User approves in browser (`/device`)
|
|
28
|
+
3. CLI stores token in `~/.config/hexgrid/config.json`
|
|
29
|
+
|
|
30
|
+
## Runtime flags
|
|
31
|
+
|
|
32
|
+
- `--api-url <url>` override API base URL
|
|
33
|
+
- `--runtime <name>` set session runtime tag (`claude`, `codex`, etc.)
|
|
34
|
+
- `--name <name>` override generated session name
|
|
35
|
+
- `--description <text>` override generated session description
|
package/bin/hexgrid.mjs
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { access, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
7
|
+
import process from 'node:process'
|
|
8
|
+
|
|
9
|
+
const DEFAULT_API_URL = process.env.HEXGRID_API_URL ?? 'https://api.hexgrid.app'
|
|
10
|
+
const CONFIG_PATH = path.join(os.homedir(), '.config', 'hexgrid', 'config.json')
|
|
11
|
+
const TOOL_CANDIDATES = ['git', 'rg', 'npm', 'pnpm', 'bun', 'yarn', 'docker', 'pytest', 'go', 'cargo', 'node', 'python3']
|
|
12
|
+
|
|
13
|
+
async function fileExists(filePath) {
|
|
14
|
+
try {
|
|
15
|
+
await access(filePath)
|
|
16
|
+
return true
|
|
17
|
+
} catch {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function loadConfig() {
|
|
23
|
+
if (!(await fileExists(CONFIG_PATH))) return {}
|
|
24
|
+
const raw = await readFile(CONFIG_PATH, 'utf8')
|
|
25
|
+
return JSON.parse(raw)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function saveConfig(config) {
|
|
29
|
+
await mkdir(path.dirname(CONFIG_PATH), { recursive: true })
|
|
30
|
+
await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function usage() {
|
|
34
|
+
console.log(`HexGrid CLI
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
hexgrid login [--api-url URL] [--no-open] [--client-name NAME]
|
|
38
|
+
hexgrid connect [--runtime RUNTIME] [--name NAME] [--description TEXT]
|
|
39
|
+
hexgrid heartbeat [SESSION_ID]
|
|
40
|
+
hexgrid disconnect [SESSION_ID]
|
|
41
|
+
hexgrid me
|
|
42
|
+
hexgrid logout
|
|
43
|
+
`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseFlag(args, name, fallback = null) {
|
|
47
|
+
const idx = args.indexOf(name)
|
|
48
|
+
if (idx === -1) return fallback
|
|
49
|
+
return args[idx + 1] ?? fallback
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasFlag(args, name) {
|
|
53
|
+
return args.includes(name)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function requestJson(apiUrl, endpoint, options = {}) {
|
|
57
|
+
const { method = 'GET', body, token } = options
|
|
58
|
+
const headers = { 'Content-Type': 'application/json' }
|
|
59
|
+
if (token) headers.Authorization = `Bearer ${token}`
|
|
60
|
+
|
|
61
|
+
const response = await fetch(`${apiUrl}${endpoint}`, {
|
|
62
|
+
method,
|
|
63
|
+
headers,
|
|
64
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
let data = {}
|
|
68
|
+
try {
|
|
69
|
+
data = await response.json()
|
|
70
|
+
} catch {
|
|
71
|
+
data = {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { response, data }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sleep(ms) {
|
|
78
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function openBrowser(url) {
|
|
82
|
+
let cmd = null
|
|
83
|
+
let args = []
|
|
84
|
+
|
|
85
|
+
if (process.platform === 'darwin') {
|
|
86
|
+
cmd = 'open'
|
|
87
|
+
args = [url]
|
|
88
|
+
} else if (process.platform === 'linux') {
|
|
89
|
+
cmd = 'xdg-open'
|
|
90
|
+
args = [url]
|
|
91
|
+
} else {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const child = spawn(cmd, args, { detached: true, stdio: 'ignore' })
|
|
97
|
+
child.unref()
|
|
98
|
+
return true
|
|
99
|
+
} catch {
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function runGit(args) {
|
|
105
|
+
const result = spawnSync('git', args, { encoding: 'utf8' })
|
|
106
|
+
if (result.status !== 0) return null
|
|
107
|
+
return result.stdout.trim()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function commandExists(command) {
|
|
111
|
+
const result = spawnSync('sh', ['-lc', `command -v ${command}`], { stdio: 'ignore' })
|
|
112
|
+
return result.status === 0
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function detectRepoContext() {
|
|
116
|
+
const repoRoot = runGit(['rev-parse', '--show-toplevel']) ?? process.cwd()
|
|
117
|
+
const repoName = path.basename(repoRoot)
|
|
118
|
+
const repoUrl = runGit(['remote', 'get-url', 'origin']) ?? `local://${repoRoot}`
|
|
119
|
+
const repoType = await detectRepoType(repoRoot)
|
|
120
|
+
const tools = TOOL_CANDIDATES.filter(commandExists)
|
|
121
|
+
|
|
122
|
+
return { repoRoot, repoName, repoUrl, repoType, tools }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function detectRepoType(repoRoot) {
|
|
126
|
+
const frontendMarkers = ['next.config.js', 'next.config.ts', 'vite.config.js', 'vite.config.ts']
|
|
127
|
+
const backendMarkers = ['pyproject.toml', 'requirements.txt', 'go.mod', 'Cargo.toml', 'Dockerfile']
|
|
128
|
+
|
|
129
|
+
const hasFrontend = (await Promise.all(frontendMarkers.map(marker => fileExists(path.join(repoRoot, marker))))).some(Boolean)
|
|
130
|
+
const hasBackend = (await Promise.all(backendMarkers.map(marker => fileExists(path.join(repoRoot, marker))))).some(Boolean)
|
|
131
|
+
|
|
132
|
+
if (hasFrontend && hasBackend) return 'fullstack'
|
|
133
|
+
if (hasFrontend) return 'frontend'
|
|
134
|
+
if (hasBackend) return 'backend'
|
|
135
|
+
return 'fullstack'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function resolveApiUrl(args, config) {
|
|
139
|
+
return parseFlag(args, '--api-url', null) ?? config.api_url ?? DEFAULT_API_URL
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resolveToken(config) {
|
|
143
|
+
return process.env.HEXGRID_TOKEN ?? config.access_token ?? null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function commandLogin(args) {
|
|
147
|
+
const config = await loadConfig()
|
|
148
|
+
const apiUrl = resolveApiUrl(args, config)
|
|
149
|
+
const clientName = parseFlag(args, '--client-name', `hexgrid-cli@${os.hostname()}`)
|
|
150
|
+
const shouldOpen = !hasFlag(args, '--no-open')
|
|
151
|
+
|
|
152
|
+
const start = await requestJson(apiUrl, '/auth/device/start', {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
body: { client_name: clientName },
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
if (!start.response.ok) {
|
|
158
|
+
throw new Error(start.data.error ?? `Failed to start login (${start.response.status})`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const {
|
|
162
|
+
device_code: deviceCode,
|
|
163
|
+
user_code: userCode,
|
|
164
|
+
verification_uri: verificationUri,
|
|
165
|
+
verification_uri_complete: verificationUriComplete,
|
|
166
|
+
interval_seconds: intervalSeconds,
|
|
167
|
+
expires_in_seconds: expiresInSeconds,
|
|
168
|
+
} = start.data
|
|
169
|
+
|
|
170
|
+
console.log(`Open this URL to approve login:\n${verificationUriComplete ?? verificationUri}\n`)
|
|
171
|
+
console.log(`Device code: ${userCode}`)
|
|
172
|
+
|
|
173
|
+
if (shouldOpen) {
|
|
174
|
+
const opened = openBrowser(verificationUriComplete ?? verificationUri)
|
|
175
|
+
if (opened) console.log('Opened browser for approval.')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const startedAt = Date.now()
|
|
179
|
+
const deadline = startedAt + Number(expiresInSeconds ?? 600) * 1000
|
|
180
|
+
const intervalMs = Math.max(1000, Number(intervalSeconds ?? 3) * 1000)
|
|
181
|
+
|
|
182
|
+
while (Date.now() < deadline) {
|
|
183
|
+
await sleep(intervalMs)
|
|
184
|
+
const poll = await requestJson(apiUrl, '/auth/device/poll', {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
body: { device_code: deviceCode },
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
if (poll.response.ok && poll.data.access_token) {
|
|
190
|
+
const next = {
|
|
191
|
+
...config,
|
|
192
|
+
api_url: apiUrl,
|
|
193
|
+
access_token: poll.data.access_token,
|
|
194
|
+
access_token_expires_at: Math.floor(Date.now() / 1000) + Number(poll.data.expires_in_seconds ?? 0),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const me = await requestJson(apiUrl, '/api/cli/me', {
|
|
198
|
+
method: 'GET',
|
|
199
|
+
token: poll.data.access_token,
|
|
200
|
+
})
|
|
201
|
+
if (me.response.ok) {
|
|
202
|
+
next.user_id = me.data.user_id
|
|
203
|
+
next.email = me.data.email
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await saveConfig(next)
|
|
207
|
+
console.log(`Login successful${next.email ? ` for ${next.email}` : ''}.`)
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (poll.response.ok && poll.data.status === 'pending') {
|
|
212
|
+
process.stdout.write('.')
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
throw new Error(poll.data.error ?? `Device login failed (${poll.response.status})`)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
throw new Error('Device login timed out. Run `hexgrid login` again.')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function sessionKey(repoRoot) {
|
|
223
|
+
return path.resolve(repoRoot)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function commandConnect(args) {
|
|
227
|
+
const config = await loadConfig()
|
|
228
|
+
const apiUrl = resolveApiUrl(args, config)
|
|
229
|
+
const token = resolveToken(config)
|
|
230
|
+
if (!token) throw new Error('Not logged in. Run `hexgrid login` first.')
|
|
231
|
+
|
|
232
|
+
const runtime = parseFlag(args, '--runtime', 'cli')
|
|
233
|
+
const context = await detectRepoContext()
|
|
234
|
+
const name = parseFlag(args, '--name', `${context.repoName}-${runtime}`)
|
|
235
|
+
const description = parseFlag(
|
|
236
|
+
args,
|
|
237
|
+
'--description',
|
|
238
|
+
`${runtime} session for ${context.repoName} (${context.repoType})`,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
const capabilities = [
|
|
242
|
+
`repo:${context.repoName}`,
|
|
243
|
+
`surface:${context.repoType}`,
|
|
244
|
+
`runtime:${runtime}`,
|
|
245
|
+
...context.tools.map(tool => `tool:${tool}`),
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
const connect = await requestJson(apiUrl, '/api/cli/connect', {
|
|
249
|
+
method: 'POST',
|
|
250
|
+
token,
|
|
251
|
+
body: {
|
|
252
|
+
name,
|
|
253
|
+
repo_url: context.repoUrl,
|
|
254
|
+
description,
|
|
255
|
+
capabilities: Array.from(new Set(capabilities)),
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if (!connect.response.ok) {
|
|
260
|
+
throw new Error(connect.data.error ?? `Connect failed (${connect.response.status})`)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const sessions = config.sessions ?? {}
|
|
264
|
+
sessions[sessionKey(context.repoRoot)] = {
|
|
265
|
+
session_id: connect.data.session_id,
|
|
266
|
+
repo_root: context.repoRoot,
|
|
267
|
+
repo_url: context.repoUrl,
|
|
268
|
+
runtime,
|
|
269
|
+
name,
|
|
270
|
+
connected_at: Math.floor(Date.now() / 1000),
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
await saveConfig({
|
|
274
|
+
...config,
|
|
275
|
+
api_url: apiUrl,
|
|
276
|
+
sessions,
|
|
277
|
+
last_session_id: connect.data.session_id,
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
console.log(JSON.stringify({
|
|
281
|
+
session_id: connect.data.session_id,
|
|
282
|
+
hex_id: connect.data.hex_id,
|
|
283
|
+
active_sessions: connect.data.active_sessions?.length ?? 0,
|
|
284
|
+
repo: context.repoName,
|
|
285
|
+
runtime,
|
|
286
|
+
}, null, 2))
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function resolveSessionId(config, args) {
|
|
290
|
+
const positional = args.find(arg => !arg.startsWith('-'))
|
|
291
|
+
if (positional) return positional
|
|
292
|
+
|
|
293
|
+
const context = await detectRepoContext()
|
|
294
|
+
const byRepo = config.sessions?.[sessionKey(context.repoRoot)]?.session_id
|
|
295
|
+
return byRepo ?? config.last_session_id ?? null
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function commandHeartbeat(args) {
|
|
299
|
+
const config = await loadConfig()
|
|
300
|
+
const apiUrl = resolveApiUrl(args, config)
|
|
301
|
+
const token = resolveToken(config)
|
|
302
|
+
if (!token) throw new Error('Not logged in. Run `hexgrid login` first.')
|
|
303
|
+
|
|
304
|
+
const sessionId = await resolveSessionId(config, args)
|
|
305
|
+
if (!sessionId) throw new Error('No session_id found. Pass one explicitly or run connect in this repo.')
|
|
306
|
+
|
|
307
|
+
const heartbeat = await requestJson(apiUrl, '/api/cli/heartbeat', {
|
|
308
|
+
method: 'POST',
|
|
309
|
+
token,
|
|
310
|
+
body: { session_id: sessionId },
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
if (!heartbeat.response.ok) {
|
|
314
|
+
throw new Error(heartbeat.data.error ?? `Heartbeat failed (${heartbeat.response.status})`)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
console.log(JSON.stringify(heartbeat.data, null, 2))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function commandDisconnect(args) {
|
|
321
|
+
const config = await loadConfig()
|
|
322
|
+
const apiUrl = resolveApiUrl(args, config)
|
|
323
|
+
const token = resolveToken(config)
|
|
324
|
+
if (!token) throw new Error('Not logged in. Run `hexgrid login` first.')
|
|
325
|
+
|
|
326
|
+
const sessionId = await resolveSessionId(config, args)
|
|
327
|
+
if (!sessionId) throw new Error('No session_id found. Pass one explicitly or run connect in this repo.')
|
|
328
|
+
|
|
329
|
+
const disconnect = await requestJson(apiUrl, '/api/cli/disconnect', {
|
|
330
|
+
method: 'POST',
|
|
331
|
+
token,
|
|
332
|
+
body: { session_id: sessionId },
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
if (!disconnect.response.ok) {
|
|
336
|
+
throw new Error(disconnect.data.error ?? `Disconnect failed (${disconnect.response.status})`)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const context = await detectRepoContext()
|
|
340
|
+
const sessions = { ...(config.sessions ?? {}) }
|
|
341
|
+
const key = sessionKey(context.repoRoot)
|
|
342
|
+
if (sessions[key]?.session_id === sessionId) delete sessions[key]
|
|
343
|
+
|
|
344
|
+
await saveConfig({
|
|
345
|
+
...config,
|
|
346
|
+
sessions,
|
|
347
|
+
last_session_id: config.last_session_id === sessionId ? null : config.last_session_id,
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
console.log(JSON.stringify(disconnect.data, null, 2))
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function commandMe(args) {
|
|
354
|
+
const config = await loadConfig()
|
|
355
|
+
const apiUrl = resolveApiUrl(args, config)
|
|
356
|
+
const token = resolveToken(config)
|
|
357
|
+
if (!token) throw new Error('Not logged in. Run `hexgrid login` first.')
|
|
358
|
+
|
|
359
|
+
const me = await requestJson(apiUrl, '/api/cli/me', {
|
|
360
|
+
method: 'GET',
|
|
361
|
+
token,
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
if (!me.response.ok) {
|
|
365
|
+
throw new Error(me.data.error ?? `Failed to fetch profile (${me.response.status})`)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
console.log(JSON.stringify(me.data, null, 2))
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function commandLogout(args) {
|
|
372
|
+
const config = await loadConfig()
|
|
373
|
+
const apiUrl = resolveApiUrl(args, config)
|
|
374
|
+
const token = resolveToken(config)
|
|
375
|
+
|
|
376
|
+
if (token) {
|
|
377
|
+
await requestJson(apiUrl, '/api/cli/logout', {
|
|
378
|
+
method: 'POST',
|
|
379
|
+
token,
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const next = {
|
|
384
|
+
...config,
|
|
385
|
+
access_token: null,
|
|
386
|
+
access_token_expires_at: null,
|
|
387
|
+
user_id: null,
|
|
388
|
+
email: null,
|
|
389
|
+
}
|
|
390
|
+
await saveConfig(next)
|
|
391
|
+
console.log('Logged out.')
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function main() {
|
|
395
|
+
const [command, ...args] = process.argv.slice(2)
|
|
396
|
+
|
|
397
|
+
if (!command || command === '-h' || command === '--help' || command === 'help') {
|
|
398
|
+
usage()
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (command === 'login') {
|
|
403
|
+
await commandLogin(args)
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
if (command === 'connect') {
|
|
407
|
+
await commandConnect(args)
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
if (command === 'heartbeat') {
|
|
411
|
+
await commandHeartbeat(args)
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
if (command === 'disconnect') {
|
|
415
|
+
await commandDisconnect(args)
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
if (command === 'me') {
|
|
419
|
+
await commandMe(args)
|
|
420
|
+
return
|
|
421
|
+
}
|
|
422
|
+
if (command === 'logout') {
|
|
423
|
+
await commandLogout(args)
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
usage()
|
|
428
|
+
process.exitCode = 1
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
main().catch((err) => {
|
|
432
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
433
|
+
console.error(`Error: ${message}`)
|
|
434
|
+
process.exit(1)
|
|
435
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jackpickard/hexgrid-cli",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "HexGrid command line client for login and repo session lifecycle",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/hexgrid.mjs",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"hexgrid",
|
|
15
|
+
"cli",
|
|
16
|
+
"agents",
|
|
17
|
+
"codex",
|
|
18
|
+
"claude",
|
|
19
|
+
"mcp"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/jmjpickard/Hexgrid"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/jmjpickard/Hexgrid#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/jmjpickard/Hexgrid/issues"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"check": "node ./bin/hexgrid.mjs --help",
|
|
31
|
+
"prepublishOnly": "npm run check"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"bin": {
|
|
37
|
+
"hexgrid": "bin/hexgrid.mjs"
|
|
38
|
+
}
|
|
39
|
+
}
|