@1sat/cli 0.0.11 → 0.0.12
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/package.json +7 -8
- package/src/cli.ts +5 -0
- package/src/commands/config.ts +2 -2
- package/src/commands/init.ts +35 -2
- package/src/commands/mcp-proxy.ts +115 -0
- package/src/config.ts +2 -2
- package/src/context.ts +2 -2
- package/src/help.ts +2 -2
- package/src/keys.ts +52 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1sat/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "CLI for 1Sat Ordinals SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/cli.ts",
|
|
@@ -16,16 +16,15 @@
|
|
|
16
16
|
"keywords": ["1sat", "bsv", "ordinals", "cli"],
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@1sat/actions": "0.0.
|
|
20
|
-
"@1sat/client": "0.0.
|
|
21
|
-
"@1sat/types": "0.0.
|
|
19
|
+
"@1sat/actions": "0.0.62",
|
|
20
|
+
"@1sat/client": "0.0.17",
|
|
21
|
+
"@1sat/types": "0.0.14",
|
|
22
22
|
"@1sat/wallet-node": ">=0.0.13",
|
|
23
|
-
"@bsv/sdk": "^2.0.
|
|
23
|
+
"@bsv/sdk": "^2.0.6",
|
|
24
24
|
"chalk": "^5.0.0",
|
|
25
25
|
"@clack/prompts": "^0.8.0",
|
|
26
|
-
"bitcoin-backup": "^0.0.
|
|
27
|
-
"dotenv": "^17.0.0"
|
|
28
|
-
"sigma-protocol": "^0.1.9"
|
|
26
|
+
"bitcoin-backup": "^0.0.11",
|
|
27
|
+
"dotenv": "^17.0.0"
|
|
29
28
|
},
|
|
30
29
|
"devDependencies": {
|
|
31
30
|
"@types/bun": "^1.3.9",
|
package/src/cli.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { parseGlobalFlags } from './args'
|
|
10
10
|
import { handleActionCommand } from './commands/action'
|
|
11
|
+
import { handleMcpProxyCommand } from './commands/mcp-proxy'
|
|
11
12
|
import { handleConfigCommand } from './commands/config'
|
|
12
13
|
import { handleIdentityCommand } from './commands/identity'
|
|
13
14
|
import { handleInitCommand } from './commands/init'
|
|
@@ -88,6 +89,10 @@ async function main(): Promise<void> {
|
|
|
88
89
|
await handleTxCommand(rest, flags)
|
|
89
90
|
break
|
|
90
91
|
|
|
92
|
+
case 'mcp-proxy':
|
|
93
|
+
await handleMcpProxyCommand()
|
|
94
|
+
break
|
|
95
|
+
|
|
91
96
|
case 'help':
|
|
92
97
|
printHelp()
|
|
93
98
|
break
|
package/src/commands/config.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
const SETTABLE_KEYS: Array<keyof OneSatCliConfig> = [
|
|
29
29
|
'chain',
|
|
30
30
|
'dataDir',
|
|
31
|
-
'
|
|
31
|
+
'activeRemote',
|
|
32
32
|
'storageIdentityKey',
|
|
33
33
|
]
|
|
34
34
|
|
|
@@ -69,7 +69,7 @@ function configShow(opts: GlobalFlags): void {
|
|
|
69
69
|
printKeyValue({
|
|
70
70
|
chain: config.chain,
|
|
71
71
|
dataDir: config.dataDir,
|
|
72
|
-
|
|
72
|
+
activeRemote: config.activeRemote ?? '(not set)',
|
|
73
73
|
storageIdentityKey: config.storageIdentityKey ?? '(not set)',
|
|
74
74
|
})
|
|
75
75
|
console.log()
|
package/src/commands/init.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from '@clack/prompts'
|
|
22
22
|
import type { GlobalFlags } from '../args'
|
|
23
23
|
import { ensureConfigDir, loadConfig, saveConfig } from '../config'
|
|
24
|
-
import { hasKey, saveKey } from '../keys'
|
|
24
|
+
import { cacheKeyPassword, hasKey, saveKey } from '../keys'
|
|
25
25
|
import { fatal, formatSuccess, formatValue, formatWarning } from '../output'
|
|
26
26
|
|
|
27
27
|
export async function handleInitCommand(
|
|
@@ -143,6 +143,25 @@ export async function handleInitCommand(
|
|
|
143
143
|
fatal('Passwords do not match.')
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// 3.5. Touch ID protection (macOS arm64)
|
|
147
|
+
let useTouchID = false
|
|
148
|
+
try {
|
|
149
|
+
const { isTouchIDAvailable } = await import('bitcoin-backup')
|
|
150
|
+
if (isTouchIDAvailable()) {
|
|
151
|
+
const enableTouchID = await confirm({
|
|
152
|
+
message:
|
|
153
|
+
'Enable Touch ID? (unlock your wallet without typing a password)',
|
|
154
|
+
})
|
|
155
|
+
if (isCancel(enableTouchID)) {
|
|
156
|
+
cancel('Setup cancelled.')
|
|
157
|
+
process.exit(0)
|
|
158
|
+
}
|
|
159
|
+
useTouchID = enableTouchID as boolean
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// bitcoin-backup Touch ID not available — skip silently
|
|
163
|
+
}
|
|
164
|
+
|
|
146
165
|
// 4. Optional: storage identity key
|
|
147
166
|
const storageId = await text({
|
|
148
167
|
message: 'Storage identity key (for wallet persistence):',
|
|
@@ -159,6 +178,19 @@ export async function handleInitCommand(
|
|
|
159
178
|
|
|
160
179
|
await saveKey(wif, pw as string)
|
|
161
180
|
|
|
181
|
+
// Cache password with Touch ID if user opted in
|
|
182
|
+
if (useTouchID) {
|
|
183
|
+
try {
|
|
184
|
+
await cacheKeyPassword(pw as string)
|
|
185
|
+
} catch {
|
|
186
|
+
console.log(
|
|
187
|
+
formatWarning(
|
|
188
|
+
' Touch ID caching failed. You can enable it later with "1sat touchid enable".',
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
162
194
|
saveConfig({
|
|
163
195
|
...loadConfig(),
|
|
164
196
|
chain: chain as 'main' | 'test',
|
|
@@ -168,5 +200,6 @@ export async function handleInitCommand(
|
|
|
168
200
|
const pk = PrivateKey.fromWif(wif)
|
|
169
201
|
const address = pk.toPublicKey().toAddress()
|
|
170
202
|
|
|
171
|
-
|
|
203
|
+
const touchIdNote = useTouchID ? ' (Touch ID enabled)' : ''
|
|
204
|
+
outro(formatSuccess(`Wallet configured${touchIdNote}! Address: ${address}`))
|
|
172
205
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1sat mcp-proxy — stdio-to-HTTP bridge for the wallet-desktop MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Authenticates with BRC-103/104 via AuthFetch + ProtoWallet and proxies
|
|
5
|
+
* JSON-RPC newline-delimited messages from stdin to the local MCP server
|
|
6
|
+
* at :3322, writing responses to stdout.
|
|
7
|
+
*
|
|
8
|
+
* Used by Claude Code's .mcp.json to connect agents to the running
|
|
9
|
+
* 1Sat desktop wallet.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { AuthFetch, PrivateKey, ProtoWallet } from '@bsv/sdk'
|
|
13
|
+
|
|
14
|
+
const MCP_URL = process.env.ONESAT_MCP_URL ?? 'http://127.0.0.1:3322'
|
|
15
|
+
const KEY_DIR = `${process.env.HOME}/.1sat-wallet`
|
|
16
|
+
const CLIENT_KEY_PATH = `${KEY_DIR}/mcp-agent.key`
|
|
17
|
+
|
|
18
|
+
function log(msg: string): void {
|
|
19
|
+
process.stderr.write(`[1sat mcp-proxy] ${msg}\n`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getClientKey(): PrivateKey {
|
|
23
|
+
mkdirSync(KEY_DIR, { recursive: true })
|
|
24
|
+
|
|
25
|
+
if (existsSync(CLIENT_KEY_PATH)) {
|
|
26
|
+
return PrivateKey.fromWif(readFileSync(CLIENT_KEY_PATH, 'utf-8').trim())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const key = PrivateKey.fromRandom()
|
|
30
|
+
writeFileSync(CLIENT_KEY_PATH, key.toWif(), { mode: 0o600 })
|
|
31
|
+
log('Generated new agent identity key')
|
|
32
|
+
return key
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function handleMcpProxyCommand(): Promise<void> {
|
|
36
|
+
// Health check before doing anything else
|
|
37
|
+
try {
|
|
38
|
+
await fetch(MCP_URL, { signal: AbortSignal.timeout(2000) })
|
|
39
|
+
} catch {
|
|
40
|
+
log(`Server not reachable at ${MCP_URL} — is 1Sat wallet running?`)
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const key = getClientKey()
|
|
45
|
+
const wallet = new ProtoWallet(key)
|
|
46
|
+
const authFetch = new AuthFetch(wallet)
|
|
47
|
+
|
|
48
|
+
log(`Connecting to ${MCP_URL}/mcp`)
|
|
49
|
+
|
|
50
|
+
let mcpSessionId: string | null = null
|
|
51
|
+
|
|
52
|
+
const decoder = new TextDecoder()
|
|
53
|
+
const reader = Bun.stdin.stream().getReader()
|
|
54
|
+
let buffer = ''
|
|
55
|
+
|
|
56
|
+
while (true) {
|
|
57
|
+
const { done, value } = await reader.read()
|
|
58
|
+
if (done) break
|
|
59
|
+
|
|
60
|
+
buffer += decoder.decode(value, { stream: true })
|
|
61
|
+
|
|
62
|
+
let newlineIdx: number
|
|
63
|
+
while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
|
|
64
|
+
const line = buffer.slice(0, newlineIdx).trim()
|
|
65
|
+
buffer = buffer.slice(newlineIdx + 1)
|
|
66
|
+
|
|
67
|
+
if (!line) continue
|
|
68
|
+
|
|
69
|
+
const headers: Record<string, string> = {
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
Accept: 'application/json, text/event-stream',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (mcpSessionId) {
|
|
75
|
+
headers['mcp-session-id'] = mcpSessionId
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const res = await authFetch.fetch(`${MCP_URL}/mcp`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
body: line,
|
|
82
|
+
headers,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const sessionHeader = res.headers.get('mcp-session-id')
|
|
86
|
+
if (sessionHeader) mcpSessionId = sessionHeader
|
|
87
|
+
|
|
88
|
+
const contentType = res.headers.get('content-type') ?? ''
|
|
89
|
+
|
|
90
|
+
if (contentType.includes('text/event-stream')) {
|
|
91
|
+
const text = await res.text()
|
|
92
|
+
for (const eventLine of text.split('\n')) {
|
|
93
|
+
if (eventLine.startsWith('data: ')) {
|
|
94
|
+
const data = eventLine.slice(6).trim()
|
|
95
|
+
if (data) process.stdout.write(`${data}\n`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
const body = await res.text()
|
|
100
|
+
if (body.trim()) process.stdout.write(`${body.trim()}\n`)
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
104
|
+
log(`Request failed: ${msg}`)
|
|
105
|
+
process.stdout.write(
|
|
106
|
+
`${JSON.stringify({
|
|
107
|
+
jsonrpc: '2.0',
|
|
108
|
+
error: { code: -32000, message: `MCP proxy error: ${msg}` },
|
|
109
|
+
id: null,
|
|
110
|
+
})}\n`,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -16,8 +16,8 @@ export interface OneSatCliConfig {
|
|
|
16
16
|
chain: 'main' | 'test'
|
|
17
17
|
/** Data directory for wallet databases */
|
|
18
18
|
dataDir: string
|
|
19
|
-
/** Remote storage URL for wallet
|
|
20
|
-
|
|
19
|
+
/** Remote storage URL for wallet sync */
|
|
20
|
+
activeRemote?: string
|
|
21
21
|
/** Storage identity key for wallet persistence */
|
|
22
22
|
storageIdentityKey?: string
|
|
23
23
|
}
|
package/src/context.ts
CHANGED
|
@@ -44,10 +44,10 @@ export async function loadContext(
|
|
|
44
44
|
},
|
|
45
45
|
useNullAsDefault: true,
|
|
46
46
|
},
|
|
47
|
-
|
|
47
|
+
activeRemote: config.activeRemote,
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
-
walletResult.monitor
|
|
50
|
+
walletResult.monitor?.startTasks()
|
|
51
51
|
|
|
52
52
|
const ctx = createContext(walletResult.wallet, {
|
|
53
53
|
services: walletResult.services,
|
package/src/help.ts
CHANGED
|
@@ -10,9 +10,9 @@ const VERSION = (() => {
|
|
|
10
10
|
const pkg = JSON.parse(
|
|
11
11
|
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
12
12
|
)
|
|
13
|
-
return pkg.version || '0.0.
|
|
13
|
+
return pkg.version || '0.0.11'
|
|
14
14
|
} catch {
|
|
15
|
-
return '0.0.
|
|
15
|
+
return '0.0.11'
|
|
16
16
|
}
|
|
17
17
|
})()
|
|
18
18
|
|
package/src/keys.ts
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* Encrypted key management for the 1sat CLI.
|
|
3
3
|
*
|
|
4
4
|
* Key resolution priority:
|
|
5
|
-
* 1. PRIVATE_KEY_WIF env var
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
5
|
+
* 1. PRIVATE_KEY_WIF env var (headless/CI)
|
|
6
|
+
* 2. Touch ID cached password → decrypt keys.bep (macOS arm64)
|
|
7
|
+
* 3. Explicit password → decrypt keys.bep
|
|
8
|
+
* 4. Fail with guidance
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { arch, platform } from 'node:os'
|
|
11
13
|
import { join } from 'node:path'
|
|
12
14
|
import { PrivateKey } from '@bsv/sdk'
|
|
13
15
|
import { type WifBackup, decryptBackup, encryptBackup } from 'bitcoin-backup'
|
|
@@ -28,9 +30,19 @@ export function hasKey(): boolean {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
|
-
*
|
|
33
|
+
* Check if Touch ID is available for password caching.
|
|
34
|
+
*/
|
|
35
|
+
export function isTouchIDAvailable(): boolean {
|
|
36
|
+
return platform() === 'darwin' && arch() === 'arm64'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load the private key from env, Touch ID cache, or password.
|
|
32
41
|
*
|
|
33
|
-
*
|
|
42
|
+
* Resolution order:
|
|
43
|
+
* 1. PRIVATE_KEY_WIF env var
|
|
44
|
+
* 2. Touch ID cached password (if available)
|
|
45
|
+
* 3. Explicit password parameter
|
|
34
46
|
*/
|
|
35
47
|
export async function loadKey(password?: string): Promise<PrivateKey> {
|
|
36
48
|
// Priority 1: Environment variable
|
|
@@ -47,14 +59,29 @@ export async function loadKey(password?: string): Promise<PrivateKey> {
|
|
|
47
59
|
)
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
|
|
62
|
+
const encrypted = readFileSync(keysPath, 'utf8')
|
|
63
|
+
|
|
64
|
+
// Priority 2a: Try Touch ID cached password
|
|
65
|
+
let resolvedPassword = password
|
|
66
|
+
if (!resolvedPassword) {
|
|
67
|
+
try {
|
|
68
|
+
const { getCachedPassword } = await import('bitcoin-backup')
|
|
69
|
+
const cached = await getCachedPassword(keysPath)
|
|
70
|
+
if (cached) {
|
|
71
|
+
resolvedPassword = cached
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Touch ID not available or no cached password — fall through
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!resolvedPassword) {
|
|
51
79
|
throw new Error(
|
|
52
|
-
'Password required to decrypt key file. Pass --password or
|
|
80
|
+
'Password required to decrypt key file. Pass --password, set ONESAT_PASSWORD, or run "1sat init --touchid" to enable Touch ID.',
|
|
53
81
|
)
|
|
54
82
|
}
|
|
55
83
|
|
|
56
|
-
const
|
|
57
|
-
const backup = await decryptBackup(encrypted, password)
|
|
84
|
+
const backup = await decryptBackup(encrypted, resolvedPassword)
|
|
58
85
|
if (!('wif' in backup) || typeof backup.wif !== 'string') {
|
|
59
86
|
throw new Error('Key file does not contain a WIF key.')
|
|
60
87
|
}
|
|
@@ -80,6 +107,22 @@ export async function saveKey(wif: string, password: string): Promise<void> {
|
|
|
80
107
|
writeFileSync(keysPath, encrypted, { mode: 0o600 })
|
|
81
108
|
}
|
|
82
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Cache the password for keys.bep using Touch ID.
|
|
112
|
+
*/
|
|
113
|
+
export async function cacheKeyPassword(password: string): Promise<void> {
|
|
114
|
+
const { cachePassword } = await import('bitcoin-backup')
|
|
115
|
+
await cachePassword(getKeysPath(), password)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Remove the cached password for keys.bep.
|
|
120
|
+
*/
|
|
121
|
+
export async function forgetKeyPassword(): Promise<void> {
|
|
122
|
+
const { forgetPassword } = await import('bitcoin-backup')
|
|
123
|
+
await forgetPassword(getKeysPath())
|
|
124
|
+
}
|
|
125
|
+
|
|
83
126
|
/**
|
|
84
127
|
* Resolve a password from flag or environment variable.
|
|
85
128
|
*/
|