@jaw.id/cli 0.0.2

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 (50) hide show
  1. package/README.md +239 -0
  2. package/bin/run.js +5 -0
  3. package/dist/base-command.js +124 -0
  4. package/dist/base-command.js.map +1 -0
  5. package/dist/commands/config/set.js +244 -0
  6. package/dist/commands/config/set.js.map +1 -0
  7. package/dist/commands/config/show.js +147 -0
  8. package/dist/commands/config/show.js.map +1 -0
  9. package/dist/commands/disconnect.js +196 -0
  10. package/dist/commands/disconnect.js.map +1 -0
  11. package/dist/commands/mcp/index.js +670 -0
  12. package/dist/commands/mcp/index.js.map +1 -0
  13. package/dist/commands/rpc/call.js +531 -0
  14. package/dist/commands/rpc/call.js.map +1 -0
  15. package/dist/index.js +460 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/lib/bridge-singleton.js +362 -0
  18. package/dist/lib/bridge-singleton.js.map +1 -0
  19. package/dist/lib/config.js +75 -0
  20. package/dist/lib/config.js.map +1 -0
  21. package/dist/lib/output.js +56 -0
  22. package/dist/lib/output.js.map +1 -0
  23. package/dist/lib/paths.js +16 -0
  24. package/dist/lib/paths.js.map +1 -0
  25. package/dist/lib/session-store.js +81 -0
  26. package/dist/lib/session-store.js.map +1 -0
  27. package/dist/lib/types.js +3 -0
  28. package/dist/lib/types.js.map +1 -0
  29. package/dist/lib/validation.js +45 -0
  30. package/dist/lib/validation.js.map +1 -0
  31. package/dist/lib/ws-bridge.js +141 -0
  32. package/dist/lib/ws-bridge.js.map +1 -0
  33. package/dist/lib/ws-daemon.js +344 -0
  34. package/dist/lib/ws-daemon.js.map +1 -0
  35. package/dist/mcp/handlers/config.js +149 -0
  36. package/dist/mcp/handlers/config.js.map +1 -0
  37. package/dist/mcp/handlers/daemon.js +185 -0
  38. package/dist/mcp/handlers/daemon.js.map +1 -0
  39. package/dist/mcp/handlers/resources.js +60 -0
  40. package/dist/mcp/handlers/resources.js.map +1 -0
  41. package/dist/mcp/handlers/rpc.js +444 -0
  42. package/dist/mcp/handlers/rpc.js.map +1 -0
  43. package/dist/mcp/helpers.js +26 -0
  44. package/dist/mcp/helpers.js.map +1 -0
  45. package/dist/mcp/server.js +658 -0
  46. package/dist/mcp/server.js.map +1 -0
  47. package/dist/mcp/tools.js +22 -0
  48. package/dist/mcp/tools.js.map +1 -0
  49. package/oclif.manifest.json +335 -0
  50. package/package.json +100 -0
@@ -0,0 +1,147 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+
6
+ // src/base-command.ts
7
+ var JAW_DIR = path.join(os.homedir(), ".jaw");
8
+ var PATHS = {
9
+ config: path.join(JAW_DIR, "config.json"),
10
+ session: path.join(JAW_DIR, "session.json"),
11
+ bridge: path.join(JAW_DIR, "bridge.json"),
12
+ daemonLog: path.join(JAW_DIR, "daemon.log")
13
+ };
14
+
15
+ // src/lib/config.ts
16
+ function loadConfig() {
17
+ if (!fs.existsSync(PATHS.config)) {
18
+ return {};
19
+ }
20
+ const raw = fs.readFileSync(PATHS.config, "utf-8");
21
+ try {
22
+ return JSON.parse(raw);
23
+ } catch {
24
+ throw new Error(
25
+ `Config file at ${PATHS.config} is not valid JSON. Run \`jaw config set apiKey=<key>\` to reset it.`
26
+ );
27
+ }
28
+ }
29
+ function redactConfig(config) {
30
+ return {
31
+ ...config,
32
+ apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...` : void 0
33
+ };
34
+ }
35
+
36
+ // src/lib/output.ts
37
+ function formatOutput(data, format) {
38
+ if (format === "json") {
39
+ return JSON.stringify(data, replaceBigInt, 2);
40
+ }
41
+ return formatHuman(data);
42
+ }
43
+ function replaceBigInt(_key, value) {
44
+ if (typeof value === "bigint") {
45
+ return value.toString();
46
+ }
47
+ return value;
48
+ }
49
+ function formatHuman(data, indent = 0) {
50
+ if (data === null || data === void 0) {
51
+ return "null";
52
+ }
53
+ if (typeof data === "string" || typeof data === "number" || typeof data === "boolean" || typeof data === "bigint") {
54
+ return String(data);
55
+ }
56
+ if (Array.isArray(data)) {
57
+ if (data.length === 0) return "(empty)";
58
+ return data.map((item, i) => `${i + 1}. ${formatHuman(item, indent + 2)}`).join("\n");
59
+ }
60
+ if (typeof data === "object") {
61
+ const entries = Object.entries(data);
62
+ if (entries.length === 0) return "(empty)";
63
+ const pad = " ".repeat(indent);
64
+ const maxKeyLen = Math.max(...entries.map(([k]) => k.length));
65
+ return entries.map(([key, val]) => {
66
+ const paddedKey = key.padEnd(maxKeyLen);
67
+ const valStr = typeof val === "object" && val !== null ? "\n" + formatHuman(val, indent + 2) : String(val);
68
+ return `${pad}${paddedKey} ${valStr}`;
69
+ }).join("\n");
70
+ }
71
+ return String(data);
72
+ }
73
+
74
+ // src/base-command.ts
75
+ var BaseCommand = class extends Command {
76
+ static baseFlags = {
77
+ output: Flags.string({
78
+ char: "o",
79
+ description: "Output format",
80
+ options: ["json", "human"],
81
+ default: "human",
82
+ env: "JAW_OUTPUT"
83
+ }),
84
+ chain: Flags.integer({
85
+ char: "c",
86
+ description: "Chain ID",
87
+ env: "JAW_CHAIN_ID"
88
+ }),
89
+ "api-key": Flags.string({
90
+ description: "JAW API key",
91
+ env: "JAW_API_KEY"
92
+ }),
93
+ yes: Flags.boolean({
94
+ char: "y",
95
+ description: "Skip confirmations (for AI agents)",
96
+ default: false
97
+ }),
98
+ quiet: Flags.boolean({
99
+ char: "q",
100
+ description: "Suppress non-essential output",
101
+ default: false
102
+ })
103
+ };
104
+ resolveApiKey(flags) {
105
+ const apiKey = flags["api-key"] ?? loadConfig().apiKey;
106
+ if (!apiKey) {
107
+ this.error(
108
+ "API key required. Set via --api-key, JAW_API_KEY env, or `jaw config set apiKey <key>`"
109
+ );
110
+ }
111
+ return apiKey;
112
+ }
113
+ resolveChainId(flags) {
114
+ const chainId = flags.chain ?? loadConfig().defaultChain;
115
+ if (!chainId) {
116
+ this.error(
117
+ "Chain ID required. Set via --chain, JAW_CHAIN_ID env, or `jaw config set defaultChain <id>`"
118
+ );
119
+ }
120
+ return chainId;
121
+ }
122
+ outputResult(data, format) {
123
+ const output = formatOutput(data, format);
124
+ this.log(output);
125
+ }
126
+ };
127
+
128
+ // src/commands/config/show.ts
129
+ var ConfigShow = class _ConfigShow extends BaseCommand {
130
+ static description = "Show current CLI configuration";
131
+ static examples = [
132
+ "<%= config.bin %> config show",
133
+ "<%= config.bin %> config show --output json"
134
+ ];
135
+ static flags = {
136
+ ...BaseCommand.baseFlags
137
+ };
138
+ async run() {
139
+ const { flags } = await this.parse(_ConfigShow);
140
+ const config = loadConfig();
141
+ this.outputResult(redactConfig(config), flags.output);
142
+ }
143
+ };
144
+
145
+ export { ConfigShow as default };
146
+ //# sourceMappingURL=show.js.map
147
+ //# sourceMappingURL=show.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/paths.ts","../../../src/lib/config.ts","../../../src/lib/output.ts","../../../src/base-command.ts","../../../src/commands/config/show.ts"],"names":[],"mappings":"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","file":"show.js","sourcesContent":["import * as path from \"node:path\";\nimport * as os from \"node:os\";\n\nconst JAW_DIR = path.join(os.homedir(), \".jaw\");\n\nexport const PATHS = {\n root: JAW_DIR,\n config: path.join(JAW_DIR, \"config.json\"),\n session: path.join(JAW_DIR, \"session.json\"),\n bridge: path.join(JAW_DIR, \"bridge.json\"),\n daemonLog: path.join(JAW_DIR, \"daemon.log\"),\n} as const;\n","import * as fs from \"node:fs\";\nimport { PATHS } from \"./paths.js\";\nimport type { JawConfig } from \"./types.js\";\nimport { isValidKeysUrl } from \"./validation.js\";\n\nexport function ensureDir(dir: string): void {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n fs.chmodSync(dir, 0o700);\n}\n\nexport function loadConfig(): JawConfig {\n if (!fs.existsSync(PATHS.config)) {\n return {};\n }\n const raw = fs.readFileSync(PATHS.config, \"utf-8\");\n try {\n return JSON.parse(raw) as JawConfig;\n } catch {\n throw new Error(\n `Config file at ${PATHS.config} is not valid JSON. Run \\`jaw config set apiKey=<key>\\` to reset it.`,\n );\n }\n}\n\nexport function saveConfig(config: JawConfig): void {\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n}\n\nexport function redactConfig(config: JawConfig): Record<string, unknown> {\n return {\n ...config,\n apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...` : undefined,\n };\n}\n\nexport function getConfigValue(\n key: keyof JawConfig,\n): string | number | undefined {\n const config = loadConfig();\n return config[key];\n}\n\nexport function setConfigValue(\n key: keyof JawConfig,\n value: string | number,\n): void {\n if (\n key === \"keysUrl\" &&\n typeof value === \"string\" &&\n !isValidKeysUrl(value)\n ) {\n throw new Error(\n `Untrusted keysUrl: ${value}. Must be a *.jaw.id domain (HTTPS) or localhost.`,\n );\n }\n const config = loadConfig();\n const updated = { ...config, [key]: value };\n saveConfig(updated);\n}\n","import type { OutputFormat } from './types.js';\n\nexport function formatOutput(data: unknown, format: OutputFormat): string {\n if (format === 'json') {\n return JSON.stringify(data, replaceBigInt, 2);\n }\n return formatHuman(data);\n}\n\nfunction replaceBigInt(_key: string, value: unknown): unknown {\n if (typeof value === 'bigint') {\n return value.toString();\n }\n return value;\n}\n\nfunction formatHuman(data: unknown, indent = 0): string {\n if (data === null || data === undefined) {\n return 'null';\n }\n\n if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean' || typeof data === 'bigint') {\n return String(data);\n }\n\n if (Array.isArray(data)) {\n if (data.length === 0) return '(empty)';\n return data.map((item, i) => `${i + 1}. ${formatHuman(item, indent + 2)}`).join('\\n');\n }\n\n if (typeof data === 'object') {\n const entries = Object.entries(data as Record<string, unknown>);\n if (entries.length === 0) return '(empty)';\n const pad = ' '.repeat(indent);\n const maxKeyLen = Math.max(...entries.map(([k]) => k.length));\n return entries\n .map(([key, val]) => {\n const paddedKey = key.padEnd(maxKeyLen);\n const valStr = typeof val === 'object' && val !== null\n ? '\\n' + formatHuman(val, indent + 2)\n : String(val);\n return `${pad}${paddedKey} ${valStr}`;\n })\n .join('\\n');\n }\n\n return String(data);\n}\n\nexport function printTable(rows: Record<string, unknown>[]): string {\n if (rows.length === 0) return '(no results)';\n\n const keys = Object.keys(rows[0]);\n const widths = keys.map(k =>\n Math.max(k.length, ...rows.map(r => String(r[k] ?? '').length))\n );\n\n const header = keys.map((k, i) => k.padEnd(widths[i])).join(' ');\n const separator = widths.map(w => '-'.repeat(w)).join(' ');\n const body = rows.map(r =>\n keys.map((k, i) => String(r[k] ?? '').padEnd(widths[i])).join(' ')\n ).join('\\n');\n\n return `${header}\\n${separator}\\n${body}`;\n}\n","import { Command, Flags } from \"@oclif/core\";\nimport type { OutputFormat } from \"./lib/types.js\";\nimport { loadConfig } from \"./lib/config.js\";\nimport { formatOutput } from \"./lib/output.js\";\n\nexport abstract class BaseCommand extends Command {\n static override baseFlags = {\n output: Flags.string({\n char: \"o\",\n description: \"Output format\",\n options: [\"json\", \"human\"],\n default: \"human\",\n env: \"JAW_OUTPUT\",\n }),\n chain: Flags.integer({\n char: \"c\",\n description: \"Chain ID\",\n env: \"JAW_CHAIN_ID\",\n }),\n \"api-key\": Flags.string({\n description: \"JAW API key\",\n env: \"JAW_API_KEY\",\n }),\n yes: Flags.boolean({\n char: \"y\",\n description: \"Skip confirmations (for AI agents)\",\n default: false,\n }),\n quiet: Flags.boolean({\n char: \"q\",\n description: \"Suppress non-essential output\",\n default: false,\n }),\n };\n\n protected resolveApiKey(flags: { \"api-key\"?: string }): string {\n const apiKey = flags[\"api-key\"] ?? loadConfig().apiKey;\n if (!apiKey) {\n this.error(\n \"API key required. Set via --api-key, JAW_API_KEY env, or `jaw config set apiKey <key>`\",\n );\n }\n return apiKey;\n }\n\n protected resolveChainId(flags: { chain?: number }): number {\n const chainId = flags.chain ?? loadConfig().defaultChain;\n if (!chainId) {\n this.error(\n \"Chain ID required. Set via --chain, JAW_CHAIN_ID env, or `jaw config set defaultChain <id>`\",\n );\n }\n return chainId;\n }\n\n protected outputResult(data: unknown, format: OutputFormat): void {\n const output = formatOutput(data, format);\n this.log(output);\n }\n}\n","import { BaseCommand } from \"../../base-command.js\";\nimport { loadConfig, redactConfig } from \"../../lib/config.js\";\n\nexport default class ConfigShow extends BaseCommand {\n static override description = \"Show current CLI configuration\";\n\n static override examples = [\n \"<%= config.bin %> config show\",\n \"<%= config.bin %> config show --output json\",\n ];\n\n static override flags = {\n ...BaseCommand.baseFlags,\n };\n\n async run(): Promise<void> {\n const { flags } = await this.parse(ConfigShow);\n const config = loadConfig();\n this.outputResult(redactConfig(config), flags.output as \"json\" | \"human\");\n }\n}\n"]}
@@ -0,0 +1,196 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import * as fs2 from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import 'url';
6
+ import { execSync } from 'child_process';
7
+ import 'crypto';
8
+ import 'ws';
9
+
10
+ // src/base-command.ts
11
+ var JAW_DIR = path.join(os.homedir(), ".jaw");
12
+ var PATHS = {
13
+ root: JAW_DIR,
14
+ config: path.join(JAW_DIR, "config.json"),
15
+ session: path.join(JAW_DIR, "session.json"),
16
+ bridge: path.join(JAW_DIR, "bridge.json"),
17
+ daemonLog: path.join(JAW_DIR, "daemon.log")
18
+ };
19
+
20
+ // src/lib/config.ts
21
+ function loadConfig() {
22
+ if (!fs2.existsSync(PATHS.config)) {
23
+ return {};
24
+ }
25
+ const raw = fs2.readFileSync(PATHS.config, "utf-8");
26
+ try {
27
+ return JSON.parse(raw);
28
+ } catch {
29
+ throw new Error(
30
+ `Config file at ${PATHS.config} is not valid JSON. Run \`jaw config set apiKey=<key>\` to reset it.`
31
+ );
32
+ }
33
+ }
34
+
35
+ // src/lib/output.ts
36
+ function formatOutput(data, format) {
37
+ if (format === "json") {
38
+ return JSON.stringify(data, replaceBigInt, 2);
39
+ }
40
+ return formatHuman(data);
41
+ }
42
+ function replaceBigInt(_key, value) {
43
+ if (typeof value === "bigint") {
44
+ return value.toString();
45
+ }
46
+ return value;
47
+ }
48
+ function formatHuman(data, indent = 0) {
49
+ if (data === null || data === void 0) {
50
+ return "null";
51
+ }
52
+ if (typeof data === "string" || typeof data === "number" || typeof data === "boolean" || typeof data === "bigint") {
53
+ return String(data);
54
+ }
55
+ if (Array.isArray(data)) {
56
+ if (data.length === 0) return "(empty)";
57
+ return data.map((item, i) => `${i + 1}. ${formatHuman(item, indent + 2)}`).join("\n");
58
+ }
59
+ if (typeof data === "object") {
60
+ const entries = Object.entries(data);
61
+ if (entries.length === 0) return "(empty)";
62
+ const pad = " ".repeat(indent);
63
+ const maxKeyLen = Math.max(...entries.map(([k]) => k.length));
64
+ return entries.map(([key, val]) => {
65
+ const paddedKey = key.padEnd(maxKeyLen);
66
+ const valStr = typeof val === "object" && val !== null ? "\n" + formatHuman(val, indent + 2) : String(val);
67
+ return `${pad}${paddedKey} ${valStr}`;
68
+ }).join("\n");
69
+ }
70
+ return String(data);
71
+ }
72
+
73
+ // src/base-command.ts
74
+ var BaseCommand = class extends Command {
75
+ static baseFlags = {
76
+ output: Flags.string({
77
+ char: "o",
78
+ description: "Output format",
79
+ options: ["json", "human"],
80
+ default: "human",
81
+ env: "JAW_OUTPUT"
82
+ }),
83
+ chain: Flags.integer({
84
+ char: "c",
85
+ description: "Chain ID",
86
+ env: "JAW_CHAIN_ID"
87
+ }),
88
+ "api-key": Flags.string({
89
+ description: "JAW API key",
90
+ env: "JAW_API_KEY"
91
+ }),
92
+ yes: Flags.boolean({
93
+ char: "y",
94
+ description: "Skip confirmations (for AI agents)",
95
+ default: false
96
+ }),
97
+ quiet: Flags.boolean({
98
+ char: "q",
99
+ description: "Suppress non-essential output",
100
+ default: false
101
+ })
102
+ };
103
+ resolveApiKey(flags) {
104
+ const apiKey = flags["api-key"] ?? loadConfig().apiKey;
105
+ if (!apiKey) {
106
+ this.error(
107
+ "API key required. Set via --api-key, JAW_API_KEY env, or `jaw config set apiKey <key>`"
108
+ );
109
+ }
110
+ return apiKey;
111
+ }
112
+ resolveChainId(flags) {
113
+ const chainId = flags.chain ?? loadConfig().defaultChain;
114
+ if (!chainId) {
115
+ this.error(
116
+ "Chain ID required. Set via --chain, JAW_CHAIN_ID env, or `jaw config set defaultChain <id>`"
117
+ );
118
+ }
119
+ return chainId;
120
+ }
121
+ outputResult(data, format) {
122
+ const output = formatOutput(data, format);
123
+ this.log(output);
124
+ }
125
+ };
126
+
127
+ // src/lib/bridge-singleton.ts
128
+ path.join(PATHS.root, "daemon.lock");
129
+ function isDaemonProcess(pid) {
130
+ if (!Number.isInteger(pid) || pid <= 0 || pid > 4194304) return false;
131
+ try {
132
+ process.kill(pid, 0);
133
+ } catch {
134
+ return false;
135
+ }
136
+ try {
137
+ const cmd = execSync(`ps -p ${String(pid)} -o command=`, {
138
+ encoding: "utf-8",
139
+ timeout: 3e3
140
+ }).trim();
141
+ return cmd.includes("ws-daemon");
142
+ } catch {
143
+ return false;
144
+ }
145
+ }
146
+ function loadBridgeInfo() {
147
+ try {
148
+ if (!fs2.existsSync(PATHS.bridge)) return null;
149
+ const raw = fs2.readFileSync(PATHS.bridge, "utf-8");
150
+ const info = JSON.parse(raw);
151
+ if (!isDaemonProcess(info.pid)) {
152
+ try {
153
+ fs2.unlinkSync(PATHS.bridge);
154
+ } catch {
155
+ }
156
+ return null;
157
+ }
158
+ return info;
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
163
+ async function shutdownDaemon() {
164
+ const info = loadBridgeInfo();
165
+ if (!info) return;
166
+ try {
167
+ process.kill(info.pid, "SIGTERM");
168
+ } catch {
169
+ }
170
+ try {
171
+ if (fs2.existsSync(PATHS.bridge)) fs2.unlinkSync(PATHS.bridge);
172
+ } catch {
173
+ }
174
+ }
175
+
176
+ // src/commands/disconnect.ts
177
+ var Disconnect = class _Disconnect extends BaseCommand {
178
+ static description = "Stop the background bridge daemon and close the browser session.";
179
+ static examples = ["<%= config.bin %> disconnect"];
180
+ static flags = {
181
+ ...BaseCommand.baseFlags
182
+ };
183
+ async run() {
184
+ const { flags } = await this.parse(_Disconnect);
185
+ await shutdownDaemon();
186
+ if (flags.output === "json") {
187
+ this.outputResult({ success: true }, "json");
188
+ } else {
189
+ this.log("Bridge daemon stopped.");
190
+ }
191
+ }
192
+ };
193
+
194
+ export { Disconnect as default };
195
+ //# sourceMappingURL=disconnect.js.map
196
+ //# sourceMappingURL=disconnect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/paths.ts","../../src/lib/config.ts","../../src/lib/output.ts","../../src/base-command.ts","../../src/lib/bridge-singleton.ts","../../src/commands/disconnect.ts"],"names":["fs","path2"],"mappings":"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","file":"disconnect.js","sourcesContent":["import * as path from \"node:path\";\nimport * as os from \"node:os\";\n\nconst JAW_DIR = path.join(os.homedir(), \".jaw\");\n\nexport const PATHS = {\n root: JAW_DIR,\n config: path.join(JAW_DIR, \"config.json\"),\n session: path.join(JAW_DIR, \"session.json\"),\n bridge: path.join(JAW_DIR, \"bridge.json\"),\n daemonLog: path.join(JAW_DIR, \"daemon.log\"),\n} as const;\n","import * as fs from \"node:fs\";\nimport { PATHS } from \"./paths.js\";\nimport type { JawConfig } from \"./types.js\";\nimport { isValidKeysUrl } from \"./validation.js\";\n\nexport function ensureDir(dir: string): void {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n fs.chmodSync(dir, 0o700);\n}\n\nexport function loadConfig(): JawConfig {\n if (!fs.existsSync(PATHS.config)) {\n return {};\n }\n const raw = fs.readFileSync(PATHS.config, \"utf-8\");\n try {\n return JSON.parse(raw) as JawConfig;\n } catch {\n throw new Error(\n `Config file at ${PATHS.config} is not valid JSON. Run \\`jaw config set apiKey=<key>\\` to reset it.`,\n );\n }\n}\n\nexport function saveConfig(config: JawConfig): void {\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n}\n\nexport function redactConfig(config: JawConfig): Record<string, unknown> {\n return {\n ...config,\n apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...` : undefined,\n };\n}\n\nexport function getConfigValue(\n key: keyof JawConfig,\n): string | number | undefined {\n const config = loadConfig();\n return config[key];\n}\n\nexport function setConfigValue(\n key: keyof JawConfig,\n value: string | number,\n): void {\n if (\n key === \"keysUrl\" &&\n typeof value === \"string\" &&\n !isValidKeysUrl(value)\n ) {\n throw new Error(\n `Untrusted keysUrl: ${value}. Must be a *.jaw.id domain (HTTPS) or localhost.`,\n );\n }\n const config = loadConfig();\n const updated = { ...config, [key]: value };\n saveConfig(updated);\n}\n","import type { OutputFormat } from './types.js';\n\nexport function formatOutput(data: unknown, format: OutputFormat): string {\n if (format === 'json') {\n return JSON.stringify(data, replaceBigInt, 2);\n }\n return formatHuman(data);\n}\n\nfunction replaceBigInt(_key: string, value: unknown): unknown {\n if (typeof value === 'bigint') {\n return value.toString();\n }\n return value;\n}\n\nfunction formatHuman(data: unknown, indent = 0): string {\n if (data === null || data === undefined) {\n return 'null';\n }\n\n if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean' || typeof data === 'bigint') {\n return String(data);\n }\n\n if (Array.isArray(data)) {\n if (data.length === 0) return '(empty)';\n return data.map((item, i) => `${i + 1}. ${formatHuman(item, indent + 2)}`).join('\\n');\n }\n\n if (typeof data === 'object') {\n const entries = Object.entries(data as Record<string, unknown>);\n if (entries.length === 0) return '(empty)';\n const pad = ' '.repeat(indent);\n const maxKeyLen = Math.max(...entries.map(([k]) => k.length));\n return entries\n .map(([key, val]) => {\n const paddedKey = key.padEnd(maxKeyLen);\n const valStr = typeof val === 'object' && val !== null\n ? '\\n' + formatHuman(val, indent + 2)\n : String(val);\n return `${pad}${paddedKey} ${valStr}`;\n })\n .join('\\n');\n }\n\n return String(data);\n}\n\nexport function printTable(rows: Record<string, unknown>[]): string {\n if (rows.length === 0) return '(no results)';\n\n const keys = Object.keys(rows[0]);\n const widths = keys.map(k =>\n Math.max(k.length, ...rows.map(r => String(r[k] ?? '').length))\n );\n\n const header = keys.map((k, i) => k.padEnd(widths[i])).join(' ');\n const separator = widths.map(w => '-'.repeat(w)).join(' ');\n const body = rows.map(r =>\n keys.map((k, i) => String(r[k] ?? '').padEnd(widths[i])).join(' ')\n ).join('\\n');\n\n return `${header}\\n${separator}\\n${body}`;\n}\n","import { Command, Flags } from \"@oclif/core\";\nimport type { OutputFormat } from \"./lib/types.js\";\nimport { loadConfig } from \"./lib/config.js\";\nimport { formatOutput } from \"./lib/output.js\";\n\nexport abstract class BaseCommand extends Command {\n static override baseFlags = {\n output: Flags.string({\n char: \"o\",\n description: \"Output format\",\n options: [\"json\", \"human\"],\n default: \"human\",\n env: \"JAW_OUTPUT\",\n }),\n chain: Flags.integer({\n char: \"c\",\n description: \"Chain ID\",\n env: \"JAW_CHAIN_ID\",\n }),\n \"api-key\": Flags.string({\n description: \"JAW API key\",\n env: \"JAW_API_KEY\",\n }),\n yes: Flags.boolean({\n char: \"y\",\n description: \"Skip confirmations (for AI agents)\",\n default: false,\n }),\n quiet: Flags.boolean({\n char: \"q\",\n description: \"Suppress non-essential output\",\n default: false,\n }),\n };\n\n protected resolveApiKey(flags: { \"api-key\"?: string }): string {\n const apiKey = flags[\"api-key\"] ?? loadConfig().apiKey;\n if (!apiKey) {\n this.error(\n \"API key required. Set via --api-key, JAW_API_KEY env, or `jaw config set apiKey <key>`\",\n );\n }\n return apiKey;\n }\n\n protected resolveChainId(flags: { chain?: number }): number {\n const chainId = flags.chain ?? loadConfig().defaultChain;\n if (!chainId) {\n this.error(\n \"Chain ID required. Set via --chain, JAW_CHAIN_ID env, or `jaw config set defaultChain <id>`\",\n );\n }\n return chainId;\n }\n\n protected outputResult(data: unknown, format: OutputFormat): void {\n const output = formatOutput(data, format);\n this.log(output);\n }\n}\n","/**\n * Bridge lifecycle management.\n *\n * - Spawns a background daemon if one isn't running\n * - Creates a WSBridge client connected to the daemon\n * - Provides shutdown to kill the daemon\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { spawn, execSync } from \"node:child_process\";\nimport { PATHS } from \"./paths.js\";\nimport { ensureDir } from \"./config.js\";\nimport { WSBridge } from \"./ws-bridge.js\";\nimport { loadConfig } from \"./config.js\";\nimport { isValidKeysUrl } from \"./validation.js\";\n\n/**\n * Find the dist directory root by walking up from the current file.\n * Needed because tsup inlines this code into consuming entry points,\n * so import.meta.url may point to e.g. dist/commands/rpc/call.js\n * rather than dist/lib/bridge-singleton.js.\n */\nfunction findDistDir(): string {\n let dir = path.dirname(fileURLToPath(import.meta.url));\n // Walk up until we find a directory that contains lib/ws-daemon.js\n for (let i = 0; i < 10; i++) {\n const candidate = path.join(dir, \"lib\", \"ws-daemon.js\");\n if (fs.existsSync(candidate)) return dir;\n dir = path.dirname(dir);\n }\n throw new Error(\"Cannot find ws-daemon.js in dist tree\");\n}\n\nconst JAW_KEYS_URL = \"https://keys.jaw.id\";\nconst LOCK_PATH = path.join(PATHS.root, \"daemon.lock\");\n\ninterface BridgeInfo {\n port: number;\n token: string;\n pid: number;\n startedAt: string;\n}\n\nexport interface BridgeOptions {\n keysUrl?: string;\n apiKey: string;\n chainId?: number;\n ens?: string;\n paymasterUrl?: string;\n timeout?: number;\n}\n\n/**\n * Verify that the process at `pid` is actually our ws-daemon, not an\n * impersonator that killed the real daemon and took over the port.\n */\nfunction isDaemonProcess(pid: number): boolean {\n // Validate PID is a positive integer to prevent command injection via bridge.json\n if (!Number.isInteger(pid) || pid <= 0 || pid > 4_194_304) return false;\n\n try {\n // Signal 0 checks if process exists\n process.kill(pid, 0);\n } catch {\n return false;\n }\n\n try {\n // Verify the process command line contains ws-daemon\n const cmd = execSync(`ps -p ${String(pid)} -o command=`, {\n encoding: \"utf-8\",\n timeout: 3000,\n }).trim();\n return cmd.includes(\"ws-daemon\");\n } catch {\n // If ps fails (process just died), treat as invalid\n return false;\n }\n}\n\nfunction loadBridgeInfo(): BridgeInfo | null {\n try {\n if (!fs.existsSync(PATHS.bridge)) return null;\n const raw = fs.readFileSync(PATHS.bridge, \"utf-8\");\n const info = JSON.parse(raw) as BridgeInfo;\n if (!isDaemonProcess(info.pid)) {\n // Process is dead or not our daemon — clean up stale file\n try {\n fs.unlinkSync(PATHS.bridge);\n } catch {\n /* ignore */\n }\n return null;\n }\n return info;\n } catch {\n return null;\n }\n}\n\n/**\n * Ensure a daemon is running and return a connected WSBridge client.\n */\nexport async function getBridge(options: BridgeOptions): Promise<WSBridge> {\n let info = loadBridgeInfo();\n\n if (!info) {\n info = await spawnDaemon(options);\n }\n\n const bridge = new WSBridge({\n port: info.port,\n token: info.token,\n timeout: options.timeout,\n });\n\n await bridge.connect();\n return bridge;\n}\n\n/**\n * Shutdown the running daemon.\n */\nexport async function shutdownDaemon(): Promise<void> {\n const info = loadBridgeInfo();\n if (!info) return;\n\n // Kill the daemon process directly\n try {\n process.kill(info.pid, \"SIGTERM\");\n } catch {\n // Process already dead\n }\n\n // Clean up bridge file\n try {\n if (fs.existsSync(PATHS.bridge)) fs.unlinkSync(PATHS.bridge);\n } catch {\n // ignore\n }\n}\n\n/**\n * Acquire an exclusive lockfile to prevent concurrent daemon spawning.\n * Returns the file descriptor on success, or null if another process holds the lock.\n */\nfunction acquireLock(): number | null {\n ensureDir(PATHS.root);\n try {\n // O_CREAT | O_EXCL — fails atomically if file already exists\n const fd = fs.openSync(LOCK_PATH, \"wx\");\n fs.writeFileSync(LOCK_PATH, String(process.pid), { mode: 0o600 });\n return fd;\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code === \"EEXIST\") {\n // Check if the lock holder is still alive (stale lock recovery)\n try {\n const lockPid = parseInt(fs.readFileSync(LOCK_PATH, \"utf-8\").trim(), 10);\n if (Number.isInteger(lockPid) && lockPid > 0) {\n try {\n process.kill(lockPid, 0);\n return null; // Process alive — lock is held\n } catch {\n // Process dead — stale lock, remove and retry once\n try { fs.unlinkSync(LOCK_PATH); } catch { /* ignore */ }\n return acquireLock();\n }\n }\n } catch {\n // Can't read lock — try removing stale file\n try { fs.unlinkSync(LOCK_PATH); } catch { /* ignore */ }\n }\n return null;\n }\n throw err;\n }\n}\n\nfunction releaseLock(fd: number): void {\n try { fs.closeSync(fd); } catch { /* ignore */ }\n try { fs.unlinkSync(LOCK_PATH); } catch { /* ignore */ }\n}\n\nasync function spawnDaemon(options: BridgeOptions): Promise<BridgeInfo> {\n ensureDir(PATHS.root);\n\n // Acquire lock to prevent concurrent daemon spawning (TOCTOU guard)\n const lockFd = acquireLock();\n if (lockFd === null) {\n // Another process is spawning — wait for bridge.json to appear\n const deadline = Date.now() + 15_000;\n while (Date.now() < deadline) {\n await new Promise((r) => setTimeout(r, 300));\n const info = loadBridgeInfo();\n if (info) return info;\n }\n throw new Error(\n \"Another process is starting the daemon. Timed out waiting for it.\",\n );\n }\n\n try {\n // Re-check after acquiring lock — another process may have just finished\n const existing = loadBridgeInfo();\n if (existing) return existing;\n\n const config = loadConfig();\n\n const keysUrl = options.keysUrl ?? config.keysUrl ?? JAW_KEYS_URL;\n if (!isValidKeysUrl(keysUrl)) {\n throw new Error(\n `Untrusted keysUrl: ${keysUrl}. Must be a *.jaw.id domain (HTTPS) or localhost.`,\n );\n }\n\n const daemonArgs = {\n keysUrl,\n chainId: options.chainId ?? config.defaultChain ?? 1,\n ens: options.ens ?? config.ens,\n paymasterUrl: options.paymasterUrl ?? config.paymasterUrl,\n timeout: options.timeout ?? 120_000,\n };\n\n const daemonScript = path.join(findDistDir(), \"lib\", \"ws-daemon.js\");\n\n // Remove stale bridge file before spawning\n try {\n if (fs.existsSync(PATHS.bridge)) fs.unlinkSync(PATHS.bridge);\n } catch {\n // ignore\n }\n\n // Truncate log to prevent unbounded growth from previous sessions\n const logFd = fs.openSync(PATHS.daemonLog, \"w\", 0o600);\n const child = spawn(\n process.execPath,\n [daemonScript, JSON.stringify(daemonArgs)],\n {\n detached: true,\n stdio: [\"ignore\", logFd, logFd],\n // Pass API key via env var instead of process args to avoid ps aux exposure\n env: { ...process.env, JAW_DAEMON_API_KEY: options.apiKey },\n },\n );\n child.unref();\n fs.closeSync(logFd);\n\n // Poll for bridge.json to appear (daemon writes it once WS server is ready)\n const deadline = Date.now() + 15_000;\n while (Date.now() < deadline) {\n await new Promise((r) => setTimeout(r, 200));\n const info = loadBridgeInfo();\n if (info) return info;\n }\n\n throw new Error(\n `Daemon failed to start within 15s. Check ${PATHS.daemonLog} for details.`,\n );\n } finally {\n releaseLock(lockFd);\n }\n}\n","import { BaseCommand } from \"../base-command.js\";\nimport { shutdownDaemon } from \"../lib/bridge-singleton.js\";\n\nexport default class Disconnect extends BaseCommand {\n static override description =\n \"Stop the background bridge daemon and close the browser session.\";\n\n static override examples = [\"<%= config.bin %> disconnect\"];\n\n static override flags = {\n ...BaseCommand.baseFlags,\n };\n\n async run(): Promise<void> {\n const { flags } = await this.parse(Disconnect);\n\n await shutdownDaemon();\n\n if (flags.output === \"json\") {\n this.outputResult({ success: true }, \"json\");\n } else {\n this.log(\"Bridge daemon stopped.\");\n }\n }\n}\n"]}