@neverquits/nq 0.1.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/bin/nq.mjs +113 -0
- package/lib/api.mjs +28 -0
- package/lib/commands/login.mjs +60 -0
- package/lib/commands/pull.mjs +98 -0
- package/lib/commands/push.mjs +33 -0
- package/lib/config.mjs +52 -0
- package/package.json +20 -0
package/bin/nq.mjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const args = process.argv.slice(2)
|
|
4
|
+
const command = args[0]
|
|
5
|
+
|
|
6
|
+
function parseFlags(args) {
|
|
7
|
+
const flags = {}
|
|
8
|
+
const positional = []
|
|
9
|
+
for (let i = 0; i < args.length; i++) {
|
|
10
|
+
if (args[i].startsWith('--')) {
|
|
11
|
+
const key = args[i].slice(2)
|
|
12
|
+
const next = args[i + 1]
|
|
13
|
+
if (next && !next.startsWith('--')) {
|
|
14
|
+
flags[key] = next
|
|
15
|
+
i++
|
|
16
|
+
} else {
|
|
17
|
+
flags[key] = true
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
positional.push(args[i])
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { flags, positional }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function printHelp() {
|
|
27
|
+
console.log(`
|
|
28
|
+
nq — NeverQuits CLI
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
nq login Log in with your access token
|
|
32
|
+
nq pull <agent> --org <orgId> Clone agent workspace
|
|
33
|
+
nq push [-m "message"] Commit and push changes
|
|
34
|
+
nq logout Clear stored credentials
|
|
35
|
+
nq status Show current config
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
nq pull <agent> --org <id> --session <id> Also write .mcp.json
|
|
39
|
+
nq pull <agent> --org <id> --dir <path> Custom clone directory
|
|
40
|
+
nq push --message "my changes" Custom commit message
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
nq login
|
|
44
|
+
nq pull alex --org acme123 --session sess_abc
|
|
45
|
+
nq push -m "updated research notes"
|
|
46
|
+
`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function main() {
|
|
50
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
51
|
+
printHelp()
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (command === 'login') {
|
|
56
|
+
const { login } = await import('../lib/commands/login.mjs')
|
|
57
|
+
await login()
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (command === 'logout') {
|
|
62
|
+
const { clearCredentials } = await import('../lib/config.mjs')
|
|
63
|
+
clearCredentials()
|
|
64
|
+
console.log(' Logged out. Credentials cleared.')
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (command === 'status') {
|
|
69
|
+
const { getCredentials, getConfig } = await import('../lib/config.mjs')
|
|
70
|
+
const creds = getCredentials()
|
|
71
|
+
const config = getConfig()
|
|
72
|
+
console.log()
|
|
73
|
+
console.log(` Logged in: ${creds?.token ? 'yes' : 'no'}`)
|
|
74
|
+
if (creds?.token) {
|
|
75
|
+
console.log(` Token: ${creds.token.slice(0, 12)}...`)
|
|
76
|
+
}
|
|
77
|
+
console.log(` API: ${config.apiUrl ?? 'https://dev.neverquits.work'}`)
|
|
78
|
+
console.log()
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (command === 'pull') {
|
|
83
|
+
const { flags, positional } = parseFlags(args.slice(1))
|
|
84
|
+
const agentKey = positional[0]
|
|
85
|
+
if (!agentKey) {
|
|
86
|
+
console.error('Usage: nq pull <agent> --org <orgId>')
|
|
87
|
+
process.exit(1)
|
|
88
|
+
}
|
|
89
|
+
const { pull } = await import('../lib/commands/pull.mjs')
|
|
90
|
+
await pull(agentKey, {
|
|
91
|
+
org: flags.org,
|
|
92
|
+
session: flags.session,
|
|
93
|
+
dir: flags.dir,
|
|
94
|
+
})
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (command === 'push') {
|
|
99
|
+
const { flags } = parseFlags(args.slice(1))
|
|
100
|
+
const { push } = await import('../lib/commands/push.mjs')
|
|
101
|
+
await push({ message: flags.message ?? flags.m })
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.error(` Unknown command: ${command}`)
|
|
106
|
+
console.error(' Run "nq help" for usage.')
|
|
107
|
+
process.exit(1)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main().catch((err) => {
|
|
111
|
+
console.error(err.message ?? err)
|
|
112
|
+
process.exit(1)
|
|
113
|
+
})
|
package/lib/api.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getCredentials, getApiUrl } from './config.mjs'
|
|
2
|
+
|
|
3
|
+
export function getToken() {
|
|
4
|
+
const creds = getCredentials()
|
|
5
|
+
if (!creds?.token) {
|
|
6
|
+
console.error('Not logged in. Run: nq login')
|
|
7
|
+
process.exit(1)
|
|
8
|
+
}
|
|
9
|
+
return creds.token
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function apiFetch(path, options = {}) {
|
|
13
|
+
const token = getToken()
|
|
14
|
+
const url = `${getApiUrl()}${path}`
|
|
15
|
+
const res = await fetch(url, {
|
|
16
|
+
...options,
|
|
17
|
+
headers: {
|
|
18
|
+
'Authorization': `Bearer ${token}`,
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
...options.headers,
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
if (res.status === 401) {
|
|
24
|
+
console.error('Token expired or invalid. Run: nq login')
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
return res
|
|
28
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline'
|
|
2
|
+
import { saveCredentials, getApiUrl } from '../config.mjs'
|
|
3
|
+
|
|
4
|
+
export async function login() {
|
|
5
|
+
const apiUrl = getApiUrl()
|
|
6
|
+
const tokenUrl = `${apiUrl}/agentops/settings/tokens`
|
|
7
|
+
|
|
8
|
+
console.log()
|
|
9
|
+
console.log(' 1. Open this URL in your browser:')
|
|
10
|
+
console.log()
|
|
11
|
+
console.log(` ${tokenUrl}`)
|
|
12
|
+
console.log()
|
|
13
|
+
console.log(' 2. Create a new access token and copy it.')
|
|
14
|
+
console.log()
|
|
15
|
+
|
|
16
|
+
// Try to open browser automatically
|
|
17
|
+
try {
|
|
18
|
+
const { exec } = await import('node:child_process')
|
|
19
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'
|
|
20
|
+
exec(`${cmd} ${tokenUrl}`)
|
|
21
|
+
} catch {
|
|
22
|
+
// silent — manual paste is fine
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
26
|
+
const token = await new Promise((resolve) => {
|
|
27
|
+
rl.question(' Paste your token: ', (answer) => {
|
|
28
|
+
rl.close()
|
|
29
|
+
resolve(answer.trim())
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
if (!token) {
|
|
34
|
+
console.error('No token provided.')
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!token.startsWith('nq_aop_')) {
|
|
39
|
+
console.error('Invalid token format. Tokens start with nq_aop_')
|
|
40
|
+
process.exit(1)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Verify the token works
|
|
44
|
+
const res = await fetch(`${apiUrl}/api/agentops/me`, {
|
|
45
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
console.error('Token verification failed. Check the token and try again.')
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const me = await res.json()
|
|
54
|
+
saveCredentials({ token })
|
|
55
|
+
|
|
56
|
+
console.log()
|
|
57
|
+
console.log(` Logged in as ${me.email ?? me.name ?? 'operator'}.`)
|
|
58
|
+
console.log(' Credentials saved to ~/.nq/credentials.json')
|
|
59
|
+
console.log()
|
|
60
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import { apiFetch, getToken } from '../api.mjs'
|
|
5
|
+
import { getApiUrl } from '../config.mjs'
|
|
6
|
+
|
|
7
|
+
export async function pull(agentKey, options) {
|
|
8
|
+
const orgId = options.org
|
|
9
|
+
if (!orgId) {
|
|
10
|
+
console.error('Missing --org flag. Usage: nq pull <agent> --org <orgId>')
|
|
11
|
+
process.exit(1)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const sessionId = options.session
|
|
15
|
+
const dir = options.dir ?? `${orgId}-${agentKey}`
|
|
16
|
+
const dest = resolve(dir)
|
|
17
|
+
|
|
18
|
+
console.log()
|
|
19
|
+
console.log(` Pulling workspace: ${orgId}/${agentKey}`)
|
|
20
|
+
|
|
21
|
+
// 1. Check if workspace repo exists
|
|
22
|
+
const res = await apiFetch(`/api/workspace/repos?orgId=${orgId}&agentKey=${agentKey}`)
|
|
23
|
+
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
console.error(` Failed to fetch workspace info: ${res.status}`)
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const repo = await res.json()
|
|
30
|
+
|
|
31
|
+
if (!repo?.repoName) {
|
|
32
|
+
console.error(' No workspace repo found. Has onboarding been completed for this org?')
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. Clone or pull
|
|
37
|
+
const gitDir = join(dest, '.git')
|
|
38
|
+
const org = repo.githubOrg ?? process.env.GITHUB_WORKSPACES_ORG
|
|
39
|
+
|
|
40
|
+
if (existsSync(gitDir)) {
|
|
41
|
+
console.log(' Pulling latest changes...')
|
|
42
|
+
try {
|
|
43
|
+
execSync('git pull --ff-only', { cwd: dest, stdio: 'inherit', timeout: 30_000 })
|
|
44
|
+
} catch {
|
|
45
|
+
console.error(' Pull failed. Resolve conflicts manually.')
|
|
46
|
+
process.exit(1)
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
console.log(` Cloning ${org}/${repo.repoName}...`)
|
|
50
|
+
try {
|
|
51
|
+
execSync(`git clone https://github.com/${org}/${repo.repoName}.git ${dest}`, {
|
|
52
|
+
stdio: 'inherit',
|
|
53
|
+
timeout: 60_000,
|
|
54
|
+
})
|
|
55
|
+
} catch {
|
|
56
|
+
console.error(' Clone failed. Make sure you have git access to the repo.')
|
|
57
|
+
console.error(` Try: gh auth login && gh repo clone ${org}/${repo.repoName}`)
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 3. Write .mcp.json if we have a session
|
|
63
|
+
if (sessionId) {
|
|
64
|
+
writeMcpJson(dest, orgId, agentKey, sessionId)
|
|
65
|
+
console.log(' Wrote .mcp.json')
|
|
66
|
+
} else {
|
|
67
|
+
console.log(' Skipped .mcp.json (no --session). Create a session first, then run:')
|
|
68
|
+
console.log(` nq pull ${agentKey} --org ${orgId} --session <sessionId>`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log()
|
|
72
|
+
console.log(` Done. Workspace at: ${dest}`)
|
|
73
|
+
console.log()
|
|
74
|
+
|
|
75
|
+
if (sessionId) {
|
|
76
|
+
console.log(' Open this folder in Claude Code to start using the agent tools.')
|
|
77
|
+
console.log()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function writeMcpJson(dest, orgId, agentKey, sessionId) {
|
|
82
|
+
const apiUrl = getApiUrl()
|
|
83
|
+
const token = getToken()
|
|
84
|
+
const mcpUrl = `${apiUrl}/api/mcp/${orgId}/${agentKey}?sessionId=${sessionId}`
|
|
85
|
+
|
|
86
|
+
const config = {
|
|
87
|
+
mcpServers: {
|
|
88
|
+
[`nq-${agentKey}`]: {
|
|
89
|
+
url: mcpUrl,
|
|
90
|
+
headers: {
|
|
91
|
+
Authorization: `Bearer ${token}`,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
writeFileSync(join(dest, '.mcp.json'), JSON.stringify(config, null, 2) + '\n')
|
|
98
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
export async function push(options) {
|
|
4
|
+
const message = options.message ?? 'workspace update'
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' })
|
|
8
|
+
} catch {
|
|
9
|
+
console.error('Not inside a git repo. Run this from your workspace directory.')
|
|
10
|
+
process.exit(1)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
// Check for changes
|
|
15
|
+
const status = execSync('git status --porcelain', { encoding: 'utf-8' }).trim()
|
|
16
|
+
if (!status) {
|
|
17
|
+
console.log(' Nothing to push — working tree clean.')
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log()
|
|
22
|
+
console.log(' Staging and pushing changes...')
|
|
23
|
+
execSync('git add -A', { stdio: 'inherit' })
|
|
24
|
+
execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { stdio: 'inherit' })
|
|
25
|
+
execSync('git push', { stdio: 'inherit', timeout: 30_000 })
|
|
26
|
+
console.log()
|
|
27
|
+
console.log(' Pushed.')
|
|
28
|
+
console.log()
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error(' Push failed:', err.message)
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
}
|
package/lib/config.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = join(homedir(), '.nq')
|
|
6
|
+
const CREDENTIALS_FILE = join(CONFIG_DIR, 'credentials.json')
|
|
7
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
|
|
8
|
+
|
|
9
|
+
function ensureDir() {
|
|
10
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
11
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 })
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getCredentials() {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(CREDENTIALS_FILE, 'utf-8'))
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function saveCredentials(creds) {
|
|
24
|
+
ensureDir()
|
|
25
|
+
writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function clearCredentials() {
|
|
29
|
+
try {
|
|
30
|
+
writeFileSync(CREDENTIALS_FILE, '{}', { mode: 0o600 })
|
|
31
|
+
} catch {
|
|
32
|
+
// ignore
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getConfig() {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'))
|
|
39
|
+
} catch {
|
|
40
|
+
return {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function saveConfig(config) {
|
|
45
|
+
ensureDir()
|
|
46
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getApiUrl() {
|
|
50
|
+
const config = getConfig()
|
|
51
|
+
return config.apiUrl ?? 'https://dev.neverquits.work'
|
|
52
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neverquits/nq",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "NeverQuits CLI — workspace setup for AI agent operators",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nq": "./bin/nq.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"lib/"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "node bin/nq.mjs"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT"
|
|
20
|
+
}
|