@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1sat/cli",
3
- "version": "0.0.11",
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.53",
20
- "@1sat/client": "0.0.16",
21
- "@1sat/types": "0.0.13",
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.4",
23
+ "@bsv/sdk": "^2.0.6",
24
24
  "chalk": "^5.0.0",
25
25
  "@clack/prompts": "^0.8.0",
26
- "bitcoin-backup": "^0.0.8",
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
@@ -28,7 +28,7 @@ import {
28
28
  const SETTABLE_KEYS: Array<keyof OneSatCliConfig> = [
29
29
  'chain',
30
30
  'dataDir',
31
- 'remoteStorageUrl',
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
- remoteStorageUrl: config.remoteStorageUrl ?? '(not set)',
72
+ activeRemote: config.activeRemote ?? '(not set)',
73
73
  storageIdentityKey: config.storageIdentityKey ?? '(not set)',
74
74
  })
75
75
  console.log()
@@ -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
- outro(formatSuccess(`Wallet configured! Address: ${address}`))
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 backup */
20
- remoteStorageUrl?: string
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
- remoteStorageUrl: config.remoteStorageUrl,
47
+ activeRemote: config.activeRemote,
48
48
  })
49
49
 
50
- walletResult.monitor.startTasks()
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.9'
13
+ return pkg.version || '0.0.11'
14
14
  } catch {
15
- return '0.0.9'
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. ~/.1sat/keys.bep encrypted file
7
- * 3. Fail with "Run 1sat init"
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
- * Load the private key from env or encrypted file.
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
- * @param password - Required if loading from encrypted file
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
- if (!password) {
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 set ONESAT_PASSWORD.',
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 encrypted = readFileSync(keysPath, 'utf8')
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
  */