@bahulam/code 0.1.2 → 0.1.4

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.
Files changed (45) hide show
  1. package/package.json +5 -8
  2. package/pulse/lib/tool-categories.ts +13 -0
  3. package/src/commands/device.mjs +121 -0
  4. package/src/commands/pair.mjs +190 -0
  5. package/src/commands/remote.mjs +110 -0
  6. package/src/core/event-log.mjs +393 -0
  7. package/src/core/headless.mjs +198 -0
  8. package/src/core/loop.mjs +276 -0
  9. package/src/core/memory-disk.mjs +210 -0
  10. package/src/core/paths.mjs +36 -0
  11. package/src/core/stream-client.mjs +28 -9
  12. package/src/core/tool-executor.mjs +56 -16
  13. package/src/daemon/approval-store.mjs +253 -0
  14. package/src/daemon/attach-client.mjs +361 -0
  15. package/src/daemon/daemonize.mjs +151 -0
  16. package/src/daemon/event-tap.mjs +197 -0
  17. package/src/daemon/input-lock.mjs +191 -0
  18. package/src/daemon/relay-client.mjs +258 -0
  19. package/src/daemon/session-core.mjs +179 -0
  20. package/src/daemon/session-list.mjs +26 -0
  21. package/src/daemon/session-publisher.mjs +78 -0
  22. package/src/daemon/socket-server.mjs +329 -0
  23. package/src/daemon/stop-daemon.mjs +18 -0
  24. package/src/permissions/checker.mjs +6 -6
  25. package/src/permissions/prompt.mjs +8 -7
  26. package/src/terminal/ansi.mjs +20 -3
  27. package/src/terminal/main.mjs +97 -3
  28. package/src/terminal/repl-render.mjs +21 -8
  29. package/src/terminal/repl.mjs +201 -2
  30. package/src/tools/analyze-code.mjs +39 -0
  31. package/src/tools/bash.mjs +1 -1
  32. package/src/tools/edit.mjs +18 -18
  33. package/src/tools/git-diff.mjs +34 -0
  34. package/src/tools/git-status.mjs +30 -0
  35. package/src/tools/glob.mjs +5 -2
  36. package/src/tools/grep.mjs +1 -1
  37. package/src/tools/meta-tools.mjs +85 -0
  38. package/src/tools/read-files.mjs +37 -0
  39. package/src/tools/read.mjs +20 -10
  40. package/src/tools/registry.mjs +20 -0
  41. package/src/tools/remember.mjs +147 -0
  42. package/src/tools/search-files.mjs +41 -0
  43. package/src/tools/write-project.mjs +62 -0
  44. package/src/tools/write.mjs +1 -1
  45. package/src/ui/sub-agent.mjs +8 -2
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@bahulam/code",
3
- "version": "0.1.2",
4
- "description": "Bahulam Code \u2014 abundance, in your terminal. CLI-first, reliability-first, sub-agents, 65.6% SWE-bench Verified.",
3
+ "version": "0.1.4",
4
+ "description": "Bahulam Code abundance, in your terminal. CLI-first, reliability-first, sub-agents, 65.6% SWE-bench Verified.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "bahulam": "src/terminal/main.mjs",
8
- "bahulam-code": "src/terminal/main.mjs"
8
+ "bahulam-code": "src/terminal/main.mjs",
9
+ "bahulamd": "src/terminal/main.mjs"
9
10
  },
10
11
  "files": [
11
12
  "src/",
@@ -50,9 +51,5 @@
50
51
  "tree-sitter-wasms": "^0.1.13",
51
52
  "web-tree-sitter": "^0.26.9"
52
53
  },
53
- "optionalDependencies": {
54
- "@bahulam/runtime-darwin-arm64": "0.1.2",
55
- "@bahulam/runtime-linux-x64": "0.1.2",
56
- "@bahulam/runtime-win32-x64": "0.1.2"
57
- }
54
+ "optionalDependencies": {}
58
55
  }
@@ -11,13 +11,20 @@ export type ToolCategory =
11
11
 
12
12
  export const TOOL_CATEGORIES: Record<string, ToolCategory> = {
13
13
  Read: 'file-io',
14
+ read_file: 'file-io',
14
15
  Write: 'file-io',
16
+ write_file: 'file-io',
15
17
  Edit: 'file-io',
18
+ edit_file: 'file-io',
16
19
  Glob: 'file-io',
20
+ list_files: 'file-io',
17
21
  Grep: 'file-io',
22
+ search_code: 'file-io',
23
+ search_files: 'file-io',
18
24
  NotebookEdit: 'file-io',
19
25
 
20
26
  Bash: 'shell',
27
+ shell: 'shell',
21
28
 
22
29
  Task: 'agent',
23
30
  TaskCreate: 'agent',
@@ -58,10 +65,16 @@ export const CATEGORY_COLORS: Record<ToolCategory, string> = {
58
65
  /** Per-tool bar colors so Read / Write / Edit / … stay distinct on project cards */
59
66
  const TOOL_BAR_OVERRIDES: Record<string, string> = {
60
67
  Read: 'var(--viz-tool-read)',
68
+ read_file: 'var(--viz-tool-read)',
61
69
  Write: 'var(--viz-tool-write)',
70
+ write_file: 'var(--viz-tool-write)',
62
71
  Edit: 'var(--viz-tool-edit)',
72
+ edit_file: 'var(--viz-tool-edit)',
63
73
  Grep: 'var(--viz-tool-grep)',
74
+ search_code: 'var(--viz-tool-grep)',
75
+ search_files: 'var(--viz-tool-grep)',
64
76
  Glob: 'var(--viz-tool-glob)',
77
+ list_files: 'var(--viz-tool-glob)',
65
78
  NotebookEdit: 'var(--viz-tool-edit)',
66
79
  }
67
80
 
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Device inventory and revocation.
3
+ *
4
+ * bahulam device list List all paired devices for this user.
5
+ * bahulam device revoke <id> Revoke a device (kill switch).
6
+ * Revoked devices are dropped within * revoking itself remotely.
7
+ *
8
+ * bahulam device whoami Print this device's registered id +
9
+ * pubkey (for cross-checking with
10
+ * `bahulam device list`).
11
+ */
12
+
13
+ import { TarangAuth } from '../auth/tarang-auth.mjs';
14
+
15
+ const GATEWAY = (process.env.BAHULAM_GATEWAY_URL || 'https://gateway.bahulam.ai').replace(/\/+$/, '').replace(/\/v1$/, '');
16
+ const RESET = '\x1b[0m';
17
+ const BOLD = '\x1b[1m';
18
+ const DIM = '\x1b[2m';
19
+ const RED = '\x1b[31m';
20
+ const GREEN = '\x1b[32m';
21
+ const YELLOW = '\x1b[33m';
22
+
23
+ export async function runDeviceCommand(args = []) {
24
+ const sub = (args[0] || '').toLowerCase();
25
+ const auth = new TarangAuth();
26
+ const creds = auth.loadCredentials();
27
+ if (!creds.token) {
28
+ process.stderr.write(`${RED}Not logged in. Run ${BOLD}bahulam login${RESET}${RED} first.${RESET}\n`);
29
+ return 1;
30
+ }
31
+
32
+ if (sub === 'list') return await _list(creds.token, auth);
33
+ if (sub === 'revoke') return await _revoke(creds.token, args[1], auth);
34
+ if (sub === 'whoami') return _whoami(auth);
35
+
36
+ process.stderr.write(`${BOLD}bahulam device${RESET} ${DIM}— list and revoke paired devices${RESET}\n\n`);
37
+ process.stderr.write(` bahulam device list List paired devices\n`);
38
+ process.stderr.write(` bahulam device revoke <id> Revoke a device (kill switch)\n`);
39
+ process.stderr.write(` bahulam device whoami Show this device's registered id\n`);
40
+ return 1;
41
+ }
42
+
43
+ async function _list(token, auth) {
44
+ try {
45
+ const res = await fetch(`${GATEWAY}/v1/devices`, {
46
+ headers: { Authorization: `Bearer ${token}` },
47
+ });
48
+ if (!res.ok) {
49
+ const body = await res.text().catch(() => '');
50
+ process.stderr.write(`${RED}Failed to list devices (${res.status}): ${body}${RESET}\n`);
51
+ return 1;
52
+ }
53
+ const { devices } = await res.json();
54
+ if (!devices || devices.length === 0) {
55
+ process.stderr.write(`${DIM}No paired devices.${RESET}\n`);
56
+ return 0;
57
+ }
58
+ const self = auth.getRawConfig()?.pairing?.device_id || '';
59
+ process.stderr.write(`\n${BOLD}${devices.length} device(s)${RESET}\n`);
60
+ for (const d of devices) {
61
+ const badge = d.device_id === self ? ` ${GREEN}(this)${RESET}` : '';
62
+ const status = d.status === 'active' ? GREEN + 'active' + RESET : RED + 'revoked' + RESET;
63
+ const seen = d.last_seen_at ? `last seen ${d.last_seen_at}` : `paired ${d.created_at}`;
64
+ process.stderr.write(` ${d.device_id}${badge} ${status} ${BOLD}${d.name}${RESET} ${DIM}${seen}${RESET}\n`);
65
+ }
66
+ process.stderr.write('\n');
67
+ return 0;
68
+ } catch (err) {
69
+ process.stderr.write(`${RED}Network error: ${err.message}${RESET}\n`);
70
+ return 1;
71
+ }
72
+ }
73
+
74
+ async function _revoke(token, deviceId, auth) {
75
+ if (!deviceId) {
76
+ process.stderr.write(`Usage: ${BOLD}bahulam device revoke <device_id>${RESET}\n`);
77
+ process.stderr.write(`${DIM}Run ${BOLD}bahulam device list${RESET}${DIM} to see ids.${RESET}\n`);
78
+ return 1;
79
+ }
80
+ try {
81
+ const res = await fetch(`${GATEWAY}/v1/devices/revoke`, {
82
+ method: 'POST',
83
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
84
+ body: JSON.stringify({ device_id: deviceId }),
85
+ });
86
+ if (!res.ok) {
87
+ const body = await res.text().catch(() => '');
88
+ process.stderr.write(`${RED}Revoke failed (${res.status}): ${body}${RESET}\n`);
89
+ return 1;
90
+ }
91
+ process.stderr.write(`${GREEN}✓ revoked${RESET} ${deviceId}\n`);
92
+ // If revoking self, clear local pairing so the daemon doesn't keep
93
+ // trying to reconnect with a dead key.
94
+ const self = auth.getRawConfig()?.pairing?.device_id;
95
+ if (self === deviceId) {
96
+ auth.saveCredentials({
97
+ pairing: null,
98
+ remote: { ...(auth.getRawConfig()?.remote || {}), enabled: false },
99
+ });
100
+ process.stderr.write(`${YELLOW}⚠ this was THIS device; local pairing + remote both cleared.${RESET}\n`);
101
+ }
102
+ process.stderr.write(`${DIM}Relay drops the revoked connection within one heartbeat.${RESET}\n\n`);
103
+ return 0;
104
+ } catch (err) {
105
+ process.stderr.write(`${RED}Network error: ${err.message}${RESET}\n`);
106
+ return 1;
107
+ }
108
+ }
109
+
110
+ function _whoami(auth) {
111
+ const p = auth.getRawConfig()?.pairing;
112
+ if (!p?.device_id) {
113
+ process.stderr.write(`${DIM}not paired.${RESET} Run ${BOLD}bahulam pair get-code${RESET}${DIM} on this device.${RESET}\n`);
114
+ return 0;
115
+ }
116
+ process.stderr.write(` ${DIM}device_id:${RESET} ${p.device_id}\n`);
117
+ process.stderr.write(` ${DIM}name:${RESET} ${p.device_name}\n`);
118
+ process.stderr.write(` ${DIM}pubkey:${RESET} ${(p.pubkey_base64 || '').slice(0, 32)}…\n`);
119
+ process.stderr.write(` ${DIM}paired_at:${RESET} ${p.paired_at}\n`);
120
+ return 0;
121
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Device pairing — pair this laptop with a phone or another device
3
+ * for remote session monitoring.
4
+ *
5
+ * Two directions:
6
+ *
7
+ * bahulam pair get-code [name] Generate a fresh 6-digit code. *
8
+ * Two directions:
9
+ *
10
+ * bahulam pair get-code [name] Ask the gateway for a fresh 6-digit
11
+ * code + expiry, print it. Give this
12
+ * to a NEW device (phone, other laptop)
13
+ * that will run `bahulam pair` with it.
14
+ *
15
+ * bahulam pair <code> Register THIS device by claiming the
16
+ * code. Generates a per-device Ed25519
17
+ * keypair, POSTs to /v1/devices/pair,
18
+ * stashes the returned device_id +
19
+ * peer pubkeys in ~/.bahulam/config.json.
20
+ *
21
+ * Storage (config.json.pairing):
22
+ * { device_id, device_name, private_key, public_key, paired_at,
23
+ * peer_pubkeys: { device_id: pubkey_base64, ... } }
24
+ *
25
+ * The private key never leaves the device. Key format is PEM (Node's
26
+ * default for generateKeyPair) so we don't need any conversion library.
27
+ */
28
+
29
+ import * as fs from 'node:fs';
30
+ import * as os from 'node:os';
31
+ import { generateKeyPairSync, createPrivateKey, createPublicKey } from 'node:crypto';
32
+ import { promisify } from 'node:util';
33
+ import { TarangAuth } from '../auth/tarang-auth.mjs';
34
+
35
+ const GATEWAY = (process.env.BAHULAM_GATEWAY_URL || 'https://gateway.bahulam.ai').replace(/\/+$/, '').replace(/\/v1$/, '');
36
+ const RESET = '\x1b[0m';
37
+ const BOLD = '\x1b[1m';
38
+ const DIM = '\x1b[2m';
39
+ const RED = '\x1b[31m';
40
+ const GREEN = '\x1b[32m';
41
+ const YELLOW = '\x1b[33m';
42
+ const CYAN = '\x1b[36m';
43
+
44
+ export async function runPairCommand(args = []) {
45
+ const auth = new TarangAuth();
46
+ const creds = auth.loadCredentials();
47
+ if (!creds.token) {
48
+ process.stderr.write(`${RED}Not logged in. Run ${BOLD}bahulam login${RESET}${RED} first.${RESET}\n`);
49
+ return 1;
50
+ }
51
+
52
+ const sub = (args[0] || '').toLowerCase();
53
+ if (sub === 'get-code') {
54
+ return await _getCode(creds.token, args[1]);
55
+ }
56
+ if (sub === 'status') {
57
+ return _status(auth);
58
+ }
59
+ if (/^\d{6}$/.test(sub)) {
60
+ return await _claimCode(creds.token, sub, args[1] || _defaultDeviceName(), auth);
61
+ }
62
+ _usage();
63
+ return 1;
64
+ }
65
+
66
+ function _usage() {
67
+ process.stderr.write(`${BOLD}bahulam pair${RESET} ${DIM}— pair a device for remote monitoring${RESET}\n\n`);
68
+ process.stderr.write(` bahulam pair get-code [name] Get a 6-digit code (share with new device)\n`);
69
+ process.stderr.write(` bahulam pair <code> [name] Register THIS device using a 6-digit code\n`);
70
+ process.stderr.write(` bahulam pair status Show current pairing state\n`);
71
+ }
72
+
73
+ async function _getCode(token, deviceName) {
74
+ const name = deviceName || _defaultDeviceName();
75
+ try {
76
+ const res = await fetch(`${GATEWAY}/v1/devices/code`, {
77
+ method: 'POST',
78
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
79
+ body: JSON.stringify({ device_name: name }),
80
+ });
81
+ if (!res.ok) {
82
+ const body = await res.text().catch(() => '');
83
+ process.stderr.write(`${RED}Failed to get code (${res.status}): ${body}${RESET}\n`);
84
+ return 1;
85
+ }
86
+ const { code, expires_at } = await res.json();
87
+ process.stderr.write(`\n ${CYAN}${BOLD}${code}${RESET}\n\n`);
88
+ process.stderr.write(` ${DIM}Enter this code on the other device with:${RESET}\n`);
89
+ process.stderr.write(` ${BOLD}bahulam pair ${code}${RESET}\n`);
90
+ process.stderr.write(` ${DIM}Or at ${GATEWAY.replace('gateway.', 'pair.')} (until ${expires_at})${RESET}\n\n`);
91
+ return 0;
92
+ } catch (err) {
93
+ process.stderr.write(`${RED}Network error: ${err.message}${RESET}\n`);
94
+ return 1;
95
+ }
96
+ }
97
+
98
+ async function _claimCode(token, code, name, auth) {
99
+ // Generate a per-device Ed25519 keypair. Node's built-in — no npm dep.
100
+ const { publicKey, privateKey } = generateKeyPairSync('ed25519', {
101
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
102
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
103
+ });
104
+ const pubkeyBase64 = _pemPubkeyToBase64Raw(publicKey);
105
+
106
+ try {
107
+ const res = await fetch(`${GATEWAY}/v1/devices/pair`, {
108
+ method: 'POST',
109
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({
111
+ code,
112
+ device_pubkey: pubkeyBase64,
113
+ device_name: name,
114
+ }),
115
+ });
116
+ if (!res.ok) {
117
+ const body = await res.text().catch(() => '');
118
+ process.stderr.write(`${RED}Pairing failed (${res.status}): ${body}${RESET}\n`);
119
+ process.stderr.write(`${DIM}The code may be invalid, expired, or already claimed.${RESET}\n`);
120
+ return 1;
121
+ }
122
+ const { device_id, peer_pubkeys } = await res.json();
123
+
124
+ auth.saveCredentials({
125
+ pairing: {
126
+ device_id,
127
+ device_name: name,
128
+ private_key: privateKey,
129
+ public_key: publicKey,
130
+ pubkey_base64: pubkeyBase64,
131
+ paired_at: new Date().toISOString(),
132
+ peer_pubkeys: peer_pubkeys || {},
133
+ },
134
+ });
135
+
136
+ process.stderr.write(`\n${GREEN}✓ device paired${RESET}\n`);
137
+ process.stderr.write(` ${DIM}id:${RESET} ${device_id}\n`);
138
+ process.stderr.write(` ${DIM}name:${RESET} ${name}\n`);
139
+ const peers = Object.keys(peer_pubkeys || {});
140
+ process.stderr.write(` ${DIM}peers:${RESET} ${peers.length}${peers.length ? ` (${peers.join(', ')})` : ''}\n`);
141
+ process.stderr.write(`\n ${DIM}Next: ${BOLD}bahulam remote enable${RESET}${DIM} to connect this device to the relay.${RESET}\n\n`);
142
+ return 0;
143
+ } catch (err) {
144
+ process.stderr.write(`${RED}Network error: ${err.message}${RESET}\n`);
145
+ return 1;
146
+ }
147
+ }
148
+
149
+ function _status(auth) {
150
+ const config = auth.getRawConfig();
151
+ const p = config.pairing;
152
+ if (!p || !p.device_id) {
153
+ process.stderr.write(`${DIM}not paired. Run ${BOLD}bahulam pair get-code${RESET}${DIM} on this device or ${BOLD}bahulam pair <code>${RESET}${DIM} with a code from another.${RESET}\n`);
154
+ return 0;
155
+ }
156
+ process.stderr.write(`\n${BOLD}pairing${RESET}\n`);
157
+ process.stderr.write(` ${DIM}id:${RESET} ${p.device_id}\n`);
158
+ process.stderr.write(` ${DIM}name:${RESET} ${p.device_name}\n`);
159
+ process.stderr.write(` ${DIM}since:${RESET} ${p.paired_at}\n`);
160
+ const peers = Object.keys(p.peer_pubkeys || {});
161
+ process.stderr.write(` ${DIM}peers:${RESET} ${peers.length}${peers.length ? ` (${peers.join(', ')})` : ''}\n\n`);
162
+ return 0;
163
+ }
164
+
165
+ // ── helpers ──────────────────────────────────────────────────────────
166
+
167
+ function _defaultDeviceName() {
168
+ const user = process.env.USER || process.env.USERNAME || 'user';
169
+ const host = (process.env.HOSTNAME || os.hostname() || 'host').split('.')[0];
170
+ return `${user}@${host}`;
171
+ }
172
+
173
+ // Convert a Node-exported Ed25519 PEM pubkey to the raw 32-byte base64
174
+ // form the gateway stores. Node ships it wrapped in an SPKI DER; we
175
+ // strip the SPKI header prefix to get the raw pubkey bytes.
176
+ // SPKI Ed25519 header: 30 2a 30 05 06 03 2b 65 70 03 21 00 (12 bytes),
177
+ // followed by the 32-byte raw pubkey.
178
+ function _pemPubkeyToBase64Raw(pemStr) {
179
+ const derBase64 = pemStr
180
+ .replace(/-----BEGIN PUBLIC KEY-----/, '')
181
+ .replace(/-----END PUBLIC KEY-----/, '')
182
+ .replace(/\s+/g, '');
183
+ const der = Buffer.from(derBase64, 'base64');
184
+ // Strip the 12-byte SPKI Ed25519 header. If length isn't 44 (12+32),
185
+ // fall back to sending the full DER base64 — the gateway just stores
186
+ // it opaquely — the raw-vs-SPKI decision is a future
187
+ // (session key wrap) concern anyway.
188
+ if (der.length === 44) return der.slice(12).toString('base64');
189
+ return derBase64;
190
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Remote relay control.
3
+ *
4
+ * bahulam remote enable Turn on the relay connection.
5
+ * Flips config to enable outbound relay dial.
6
+ *
7
+ * bahulam remote disable Turn off the relay connection (kill switch).
8
+ * Disabling drops the relay connection within
9
+ * one heartbeat interval.
10
+ *
11
+ * IMPORTANT: this command only flips a flag. The actual relay dial and
12
+ * disconnect lives in the daemon. Enabling does not guarantee a
13
+ * connection — the daemon must be running. * daemon needs a device_id + keypair to authenticate to the relay.
14
+ */
15
+
16
+ import { TarangAuth } from '../auth/tarang-auth.mjs';
17
+
18
+ const RESET = '\x1b[0m';
19
+ const BOLD = '\x1b[1m';
20
+ const DIM = '\x1b[2m';
21
+ const RED = '\x1b[31m';
22
+ const GREEN = '\x1b[32m';
23
+ const YELLOW = '\x1b[33m';
24
+
25
+ const DEFAULT_RELAY = (process.env.BAHULAM_RELAY_URL || 'wss://relay.bahulam.ai').replace(/\/+$/, '');
26
+
27
+ export async function runRemoteCommand(args = []) {
28
+ const sub = (args[0] || '').toLowerCase();
29
+ const auth = new TarangAuth();
30
+ auth.loadCredentials();
31
+
32
+ if (sub === 'enable') return _enable(auth);
33
+ if (sub === 'disable') return _disable(auth);
34
+ if (sub === 'status') return _status(auth);
35
+
36
+ process.stderr.write(`${BOLD}bahulam remote${RESET} ${DIM}— control the remote relay connection${RESET}\n\n`);
37
+ process.stderr.write(` bahulam remote enable Connect the daemon to the relay for remote monitoring/control\n`);
38
+ process.stderr.write(` bahulam remote disable Kill switch — drops relay connection within one heartbeat\n`);
39
+ process.stderr.write(` bahulam remote status Show current remote-connection state\n`);
40
+ return 1;
41
+ }
42
+
43
+ function _enable(auth) {
44
+ const config = auth.getRawConfig();
45
+ if (!config.pairing?.device_id) {
46
+ process.stderr.write(`${YELLOW}⚠ this device is not paired yet.${RESET}\n`);
47
+ process.stderr.write(` ${DIM}Run ${BOLD}bahulam pair get-code${RESET}${DIM} (this device) or ${BOLD}bahulam pair <code>${RESET}${DIM} (with a code).${RESET}\n`);
48
+ process.stderr.write(` ${DIM}Enable will still write the flag, but the daemon can't connect without a device.${RESET}\n\n`);
49
+ }
50
+ auth.saveCredentials({
51
+ remote: {
52
+ enabled: true,
53
+ relay_url: config.remote?.relay_url || DEFAULT_RELAY,
54
+ enabled_at: new Date().toISOString(),
55
+ },
56
+ });
57
+ process.stderr.write(`${GREEN}✓ remote enabled${RESET}\n`);
58
+ process.stderr.write(` ${DIM}relay:${RESET} ${config.remote?.relay_url || DEFAULT_RELAY}\n`);
59
+ process.stderr.write(` ${DIM}A running daemon will pick this up on next session start.${RESET}\n\n`);
60
+ return 0;
61
+ }
62
+
63
+ function _disable(auth) {
64
+ const config = auth.getRawConfig();
65
+ auth.saveCredentials({
66
+ remote: {
67
+ ...(config.remote || {}),
68
+ enabled: false,
69
+ disabled_at: new Date().toISOString(),
70
+ },
71
+ });
72
+ process.stderr.write(`${GREEN}✓ remote disabled${RESET} ${DIM}(kill switch)${RESET}\n`);
73
+ process.stderr.write(` ${DIM}Any running daemon drops its relay connection within one heartbeat.${RESET}\n\n`);
74
+ return 0;
75
+ }
76
+
77
+ function _status(auth) {
78
+ const config = auth.getRawConfig();
79
+ const r = config.remote || {};
80
+ const p = config.pairing || {};
81
+ process.stderr.write(`\n${BOLD}remote${RESET}\n`);
82
+ process.stderr.write(` ${DIM}enabled:${RESET} ${r.enabled ? GREEN + 'yes' + RESET : DIM + 'no' + RESET}\n`);
83
+ process.stderr.write(` ${DIM}relay:${RESET} ${r.relay_url || DEFAULT_RELAY}\n`);
84
+ if (r.enabled_at) process.stderr.write(` ${DIM}since:${RESET} ${r.enabled_at}\n`);
85
+ if (r.disabled_at && !r.enabled) process.stderr.write(` ${DIM}stopped:${RESET} ${r.disabled_at}\n`);
86
+ process.stderr.write(` ${DIM}device:${RESET} ${p.device_id ? `${p.device_id} (${p.device_name})` : DIM + 'not paired' + RESET}\n\n`);
87
+ return 0;
88
+ }
89
+
90
+ /**
91
+ * Read-only helper for the daemon. Returns the effective
92
+ * remote config: { enabled, relay_url, device_id, private_key,
93
+ * public_key, peer_pubkeys } — or null if we shouldn't dial.
94
+ */
95
+ export function loadRemoteConfig() {
96
+ const auth = new TarangAuth();
97
+ const config = auth.getRawConfig() || auth.loadCredentials() && auth.getRawConfig();
98
+ if (!config?.remote?.enabled) return null;
99
+ if (!config?.pairing?.device_id) return null;
100
+ return {
101
+ enabled: true,
102
+ relay_url: config.remote.relay_url || DEFAULT_RELAY,
103
+ device_id: config.pairing.device_id,
104
+ private_key: config.pairing.private_key,
105
+ public_key: config.pairing.public_key,
106
+ pubkey_base64: config.pairing.pubkey_base64,
107
+ peer_pubkeys: config.pairing.peer_pubkeys || {},
108
+ token: auth.loadCredentials().token,
109
+ };
110
+ }