@syphin/cli 0.1.0

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 ADDED
@@ -0,0 +1,65 @@
1
+ # syphin
2
+
3
+ CLI for [Syphin](https://syphin.dev) — centralized AI agent context.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g syphin
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ ### `syphin login`
14
+
15
+ Authenticate with Syphin.
16
+
17
+ ```bash
18
+ syphin login --token <your-token>
19
+ ```
20
+
21
+ Get a token from the [Syphin dashboard](https://app.syphin.dev/settings/tokens).
22
+
23
+ ### `syphin init`
24
+
25
+ Connect the current directory to Syphin. Creates `.syphin/config.json` and adds the MCP server to `.mcp.json`.
26
+
27
+ ```bash
28
+ syphin init
29
+ syphin init --env staging
30
+ ```
31
+
32
+ ### `syphin status`
33
+
34
+ Check connection status and show the live manifest.
35
+
36
+ ```bash
37
+ syphin status
38
+ ```
39
+
40
+ ### `syphin whoami`
41
+
42
+ Show current auth context (org, plan, token type).
43
+
44
+ ### `syphin logout`
45
+
46
+ Clear stored credentials.
47
+
48
+ ### `syphin cache`
49
+
50
+ Show local cache stats (cached skills and manifests).
51
+
52
+ ### `syphin cache clear`
53
+
54
+ Clear the local cache.
55
+
56
+ ## Environment variables
57
+
58
+ | Variable | Description |
59
+ |----------|-------------|
60
+ | `SYPHIN_TOKEN` | Auth token (alternative to `syphin login`) |
61
+ | `SYPHIN_API_URL` | API base URL (default: `https://syphin.vercel.app`) |
62
+
63
+ ## License
64
+
65
+ MIT
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@syphin/cli",
3
+ "version": "0.1.0",
4
+ "description": "Syphin CLI — centralized AI agent context",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": { "syphin": "src/index.js" },
8
+ "files": ["src", "README.md"],
9
+ "scripts": {
10
+ "start": "node src/index.js",
11
+ "dev": "node --watch src/index.js"
12
+ },
13
+ "keywords": ["syphin", "cli", "ai", "agent", "context", "mcp", "claude"],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/dbigby/syphin.git",
17
+ "directory": "packages/cli"
18
+ },
19
+ "homepage": "https://syphin.dev",
20
+ "dependencies": {
21
+ "commander": "^12.0.0",
22
+ "ora": "^8.0.1",
23
+ "chalk": "^5.3.0",
24
+ "prompts": "^2.4.2",
25
+ "open": "^10.1.0"
26
+ },
27
+ "engines": { "node": ">=18.0.0" }
28
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * syphin whoami / syphin logout
3
+ */
4
+
5
+ import { loadToken, clearToken } from '../lib/config.js'
6
+ import { createApiClient } from '../lib/api.js'
7
+ import { success, fail, info, teal } from '../lib/output.js'
8
+
9
+ export function registerAuth(program) {
10
+ program
11
+ .command('whoami')
12
+ .description('Show current auth context')
13
+ .action(async () => {
14
+ const token = loadToken()
15
+ if (!token) {
16
+ fail('Not authenticated. Run `syphin login` first.')
17
+ process.exit(1)
18
+ }
19
+
20
+ try {
21
+ const api = createApiClient(token)
22
+ const result = await api.verify()
23
+ info(`Organization: ${teal(result.orgName)} (${result.orgSlug})`)
24
+ info(`Plan: ${result.orgPlan}`)
25
+ info(`Token type: ${result.tokenType}`)
26
+ } catch (err) {
27
+ fail(`Token invalid: ${err.message}`)
28
+ process.exit(1)
29
+ }
30
+ })
31
+
32
+ program
33
+ .command('logout')
34
+ .description('Clear stored credentials')
35
+ .action(() => {
36
+ clearToken()
37
+ success('Logged out.')
38
+ })
39
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * syphin cache / syphin cache clear
3
+ */
4
+
5
+ import { existsSync, readdirSync, rmSync, statSync } from 'fs'
6
+ import { join } from 'path'
7
+ import { homedir } from 'os'
8
+ import { success, info, teal } from '../lib/output.js'
9
+
10
+ const CACHE_DIR = join(homedir(), '.cache', 'syphin')
11
+
12
+ function countFiles(dir) {
13
+ if (!existsSync(dir)) return 0
14
+ return readdirSync(dir).length
15
+ }
16
+
17
+ function dirSize(dir) {
18
+ if (!existsSync(dir)) return 0
19
+ let total = 0
20
+ for (const f of readdirSync(dir)) {
21
+ const fp = join(dir, f)
22
+ try {
23
+ total += statSync(fp).size
24
+ } catch {}
25
+ }
26
+ return total
27
+ }
28
+
29
+ function formatBytes(bytes) {
30
+ if (bytes < 1024) return `${bytes} B`
31
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
32
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`
33
+ }
34
+
35
+ export function registerCache(program) {
36
+ const cache = program
37
+ .command('cache')
38
+ .description('Show cache stats')
39
+ .action(() => {
40
+ const skills = countFiles(join(CACHE_DIR, 'skills'))
41
+ const manifests = countFiles(join(CACHE_DIR, 'manifests'))
42
+ const size = dirSize(join(CACHE_DIR, 'skills')) + dirSize(join(CACHE_DIR, 'manifests'))
43
+
44
+ info(`Cache directory: ${CACHE_DIR}`)
45
+ info(`Cached skills: ${teal(skills.toString())}`)
46
+ info(`Cached manifests: ${teal(manifests.toString())}`)
47
+ info(`Total size: ${formatBytes(size)}`)
48
+ })
49
+
50
+ cache
51
+ .command('clear')
52
+ .description('Clear the local cache')
53
+ .action(() => {
54
+ if (existsSync(CACHE_DIR)) {
55
+ rmSync(CACHE_DIR, { recursive: true })
56
+ }
57
+ success('Cache cleared.')
58
+ })
59
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * syphin init [--yes] [--env <env>]
3
+ */
4
+
5
+ import ora from 'ora'
6
+ import { basename } from 'path'
7
+ import { readFileSync, writeFileSync, existsSync } from 'fs'
8
+ import { loadToken, saveProjectConfig } from '../lib/config.js'
9
+ import { createApiClient } from '../lib/api.js'
10
+ import { success, fail, info, teal } from '../lib/output.js'
11
+
12
+ function detectProjectName() {
13
+ // Try package.json
14
+ if (existsSync('package.json')) {
15
+ try {
16
+ const pkg = JSON.parse(readFileSync('package.json', 'utf-8'))
17
+ if (pkg.name) return pkg.name.replace(/^@[^/]+\//, '')
18
+ } catch {}
19
+ }
20
+ // Fall back to directory name
21
+ return basename(process.cwd())
22
+ }
23
+
24
+ function updateMcpJson() {
25
+ const mcpPath = '.mcp.json'
26
+ let mcp = {}
27
+
28
+ if (existsSync(mcpPath)) {
29
+ try {
30
+ mcp = JSON.parse(readFileSync(mcpPath, 'utf-8'))
31
+ } catch {}
32
+ }
33
+
34
+ if (!mcp.mcpServers) mcp.mcpServers = {}
35
+
36
+ mcp.mcpServers.syphin = {
37
+ command: 'npx',
38
+ args: ['@syphin/mcp'],
39
+ type: 'stdio',
40
+ }
41
+
42
+ writeFileSync(mcpPath, JSON.stringify(mcp, null, 2))
43
+ }
44
+
45
+ export function registerInit(program) {
46
+ program
47
+ .command('init')
48
+ .description('Connect current directory to Syphin')
49
+ .option('--yes', 'Skip prompts')
50
+ .option('--env <env>', 'Environment', 'dev')
51
+ .action(async (opts) => {
52
+ const token = loadToken()
53
+ if (!token) {
54
+ fail('Not authenticated. Run `syphin login` first.')
55
+ process.exit(1)
56
+ }
57
+
58
+ const projectName = detectProjectName()
59
+ const slug = projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-')
60
+ const env = opts.env
61
+
62
+ info(`Project: ${teal(projectName)} (${slug})`)
63
+ info(`Environment: ${env}`)
64
+
65
+ const spinner = ora('Connecting to Syphin...').start()
66
+
67
+ try {
68
+ const api = createApiClient(token)
69
+
70
+ // Verify auth first
71
+ await api.verify()
72
+
73
+ // Save project config
74
+ saveProjectConfig({
75
+ projectSlug: slug,
76
+ environment: env,
77
+ })
78
+
79
+ // Update .mcp.json
80
+ updateMcpJson()
81
+
82
+ spinner.succeed('Connected!')
83
+
84
+ // Try to fetch manifest as connection test
85
+ try {
86
+ const manifest = await api.getManifest(slug, env)
87
+ console.log('')
88
+ info(`Always-on skills: ${teal(manifest.alwaysOn.length.toString())}`)
89
+ info(`Available skills: ${teal(manifest.available.length.toString())}`)
90
+ info(`Total always-on tokens: ${teal('~' + manifest.totalTokens)}`)
91
+ } catch {
92
+ info('No manifest yet — the project may need skills bound to it.')
93
+ }
94
+
95
+ console.log('')
96
+ success('Project connected. Restart Claude Code to activate Syphin.')
97
+ } catch (err) {
98
+ spinner.fail('Connection failed')
99
+ fail(err.message)
100
+ process.exit(1)
101
+ }
102
+ })
103
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * syphin login [--token <t>] [--force]
3
+ */
4
+
5
+ import ora from 'ora'
6
+ import { saveToken, loadToken } from '../lib/config.js'
7
+ import { createApiClient } from '../lib/api.js'
8
+ import { success, fail, info, teal } from '../lib/output.js'
9
+
10
+ export function registerLogin(program) {
11
+ program
12
+ .command('login')
13
+ .description('Authenticate with Syphin')
14
+ .option('--token <token>', 'Direct token authentication')
15
+ .option('--force', 'Re-authenticate even if already logged in')
16
+ .action(async (opts) => {
17
+ const existing = loadToken()
18
+ if (existing && !opts.force && !opts.token) {
19
+ info('Already authenticated. Use --force to re-authenticate.')
20
+ return
21
+ }
22
+
23
+ const token = opts.token
24
+ if (!token) {
25
+ fail('Token required. Use: syphin login --token <your-token>')
26
+ info('Get a token from the Syphin dashboard at https://app.syphin.dev/settings/tokens')
27
+ process.exit(1)
28
+ }
29
+
30
+ const spinner = ora('Verifying token...').start()
31
+
32
+ try {
33
+ const api = createApiClient(token)
34
+ const result = await api.verify()
35
+
36
+ saveToken(token)
37
+ spinner.succeed('Authenticated successfully!')
38
+
39
+ console.log('')
40
+ info(`Organization: ${teal(result.orgName)} (${result.orgSlug})`)
41
+ info(`Plan: ${result.orgPlan}`)
42
+ console.log('')
43
+ } catch (err) {
44
+ spinner.fail('Authentication failed')
45
+ fail(err.message)
46
+ process.exit(1)
47
+ }
48
+ })
49
+ }
package/src/index.js ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * syphin CLI — login, init, status, cache management.
4
+ */
5
+
6
+ import { Command } from 'commander'
7
+ import { registerLogin } from './commands/login.js'
8
+ import { registerInit } from './commands/init.js'
9
+ import { registerAuth } from './commands/auth.js'
10
+ import { registerCache } from './commands/cache.js'
11
+ import { loadToken, loadProjectConfig } from './lib/config.js'
12
+ import { createApiClient } from './lib/api.js'
13
+ import { info, fail, teal, dim } from './lib/output.js'
14
+
15
+ const program = new Command()
16
+
17
+ program
18
+ .name('syphin')
19
+ .description('Syphin CLI — centralized AI agent context')
20
+ .version('0.1.0')
21
+
22
+ registerLogin(program)
23
+ registerInit(program)
24
+ registerAuth(program)
25
+ registerCache(program)
26
+
27
+ program
28
+ .command('status')
29
+ .description('Check connection status and live manifest')
30
+ .action(async () => {
31
+ const token = loadToken()
32
+ if (!token) {
33
+ fail('Not authenticated. Run `syphin login` first.')
34
+ process.exit(1)
35
+ }
36
+
37
+ const config = loadProjectConfig()
38
+ if (!config) {
39
+ fail('No project config found. Run `syphin init` in your project directory.')
40
+ process.exit(1)
41
+ }
42
+
43
+ info(`Project: ${teal(config.projectSlug)}`)
44
+ info(`Environment: ${config.environment}`)
45
+
46
+ try {
47
+ const api = createApiClient(token)
48
+ const result = await api.verify()
49
+ info(`Organization: ${teal(result.orgName)} (${result.orgSlug})`)
50
+
51
+ try {
52
+ const manifest = await api.getManifest(config.projectSlug, config.environment)
53
+ info(`Always-on: ${teal(manifest.alwaysOn.length.toString())} skills (~${manifest.totalTokens} tokens)`)
54
+ info(`Available: ${teal(manifest.available.length.toString())} skills`)
55
+
56
+ if (manifest.alwaysOn.length > 0) {
57
+ console.log('')
58
+ info(dim('Always-on skills:'))
59
+ for (const s of manifest.alwaysOn) {
60
+ info(` ${s.name} ${dim(`[${s.type}]`)} ~${s.tokenCount} tokens`)
61
+ }
62
+ }
63
+ } catch {
64
+ info('Manifest not available — project may need skills bound.')
65
+ }
66
+ } catch (err) {
67
+ fail(`Connection error: ${err.message}`)
68
+ process.exit(1)
69
+ }
70
+ })
71
+
72
+ program.parse()
package/src/lib/api.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * API client for CLI.
3
+ */
4
+
5
+ const DEFAULT_API_URL = 'https://syphin.vercel.app'
6
+
7
+ export function createApiClient(token, apiUrl) {
8
+ const baseUrl = (apiUrl || process.env.SYPHIN_API_URL || DEFAULT_API_URL).replace(/\/+$/, '')
9
+
10
+ async function request(method, path, body) {
11
+ const url = `${baseUrl}${path}`
12
+ const opts = {
13
+ method,
14
+ headers: {
15
+ 'Authorization': `Bearer ${token}`,
16
+ 'Content-Type': 'application/json',
17
+ },
18
+ }
19
+ if (body) opts.body = JSON.stringify(body)
20
+
21
+ const res = await fetch(url, opts)
22
+ const json = await res.json()
23
+
24
+ if (!res.ok) {
25
+ throw new Error(json.error || `API error ${res.status}`)
26
+ }
27
+
28
+ return json.data
29
+ }
30
+
31
+ return {
32
+ verify: () => request('POST', '/api/auth/verify'),
33
+ getManifest: (slug, env) => request('GET', `/api/projects/${slug}/manifest?env=${env}`),
34
+ createProject: (name, slug) => request('POST', '/api/projects', { name, slug }),
35
+ }
36
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Config management — credentials and project config.
3
+ */
4
+
5
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'fs'
6
+ import { join, dirname } from 'path'
7
+ import { homedir } from 'os'
8
+
9
+ const CONFIG_DIR = join(homedir(), '.config', 'syphin')
10
+ const CREDS_FILE = join(CONFIG_DIR, 'credentials.json')
11
+
12
+ function ensureDir(dir) {
13
+ if (!existsSync(dir)) {
14
+ mkdirSync(dir, { recursive: true })
15
+ }
16
+ }
17
+
18
+ export function saveToken(token) {
19
+ ensureDir(CONFIG_DIR)
20
+ writeFileSync(CREDS_FILE, JSON.stringify({ token }, null, 2))
21
+ try {
22
+ chmodSync(CREDS_FILE, 0o600)
23
+ } catch {}
24
+ }
25
+
26
+ export function loadToken() {
27
+ if (process.env.SYPHIN_TOKEN) return process.env.SYPHIN_TOKEN
28
+
29
+ if (existsSync(CREDS_FILE)) {
30
+ try {
31
+ const raw = readFileSync(CREDS_FILE, 'utf-8')
32
+ return JSON.parse(raw).token || null
33
+ } catch {
34
+ return null
35
+ }
36
+ }
37
+ return null
38
+ }
39
+
40
+ export function clearToken() {
41
+ if (existsSync(CREDS_FILE)) {
42
+ writeFileSync(CREDS_FILE, '{}')
43
+ }
44
+ }
45
+
46
+ export function loadProjectConfig() {
47
+ const configPath = join(process.cwd(), '.syphin', 'config.json')
48
+ if (!existsSync(configPath)) return null
49
+ try {
50
+ return JSON.parse(readFileSync(configPath, 'utf-8'))
51
+ } catch {
52
+ return null
53
+ }
54
+ }
55
+
56
+ export function saveProjectConfig(config) {
57
+ const dir = join(process.cwd(), '.syphin')
58
+ ensureDir(dir)
59
+ writeFileSync(join(dir, 'config.json'), JSON.stringify(config, null, 2))
60
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Styled terminal output helpers.
3
+ */
4
+
5
+ import chalk from 'chalk'
6
+
7
+ export const teal = chalk.hex('#00d4c8')
8
+ export const dim = chalk.dim
9
+ export const bold = chalk.bold
10
+ export const red = chalk.red
11
+ export const green = chalk.green
12
+ export const yellow = chalk.yellow
13
+
14
+ export function success(msg) {
15
+ console.log(green(' ✓ ') + msg)
16
+ }
17
+
18
+ export function fail(msg) {
19
+ console.log(red(' ✗ ') + msg)
20
+ }
21
+
22
+ export function info(msg) {
23
+ console.log(dim(' → ') + msg)
24
+ }
25
+
26
+ export function heading(msg) {
27
+ console.log('\n' + bold(msg))
28
+ }