@fibukiapp/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/bin/fibuki.mjs ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * FiBuKI CLI
5
+ *
6
+ * Usage:
7
+ * npx @fibukiapp/cli auth Save API key to ~/.fibuki/config.json
8
+ * npx @fibukiapp/cli auth --format mcp Print Claude Desktop MCP config
9
+ * npx @fibukiapp/cli auth --format env Print FIBUKI_API_KEY=fk_xxx
10
+ */
11
+
12
+ import { deviceAuth } from "../lib/auth.mjs";
13
+
14
+ const args = process.argv.slice(2);
15
+ const command = args[0];
16
+
17
+ if (!command || command === "help" || command === "--help" || command === "-h") {
18
+ console.log(`
19
+ FiBuKI CLI — authenticate and configure AI integrations
20
+
21
+ Usage:
22
+ fibuki auth Authenticate via browser and save API key
23
+ fibuki auth --format mcp Print Claude Desktop MCP config snippet
24
+ fibuki auth --format env Print FIBUKI_API_KEY export
25
+
26
+ Options:
27
+ --format <type> Output format: json (default), mcp, env
28
+ --base-url <url> Override API base URL (for development)
29
+ --help Show this help message
30
+ `);
31
+ process.exit(0);
32
+ }
33
+
34
+ if (command === "auth") {
35
+ const formatIdx = args.indexOf("--format");
36
+ const format = formatIdx !== -1 ? args[formatIdx + 1] : "json";
37
+
38
+ const baseUrlIdx = args.indexOf("--base-url");
39
+ const baseUrl = baseUrlIdx !== -1 ? args[baseUrlIdx + 1] : "https://fibuki.com";
40
+
41
+ if (!["json", "mcp", "env"].includes(format)) {
42
+ console.error(`Unknown format: ${format}. Use json, mcp, or env.`);
43
+ process.exit(1);
44
+ }
45
+
46
+ deviceAuth({ format, baseUrl }).catch((err) => {
47
+ console.error("\nError:", err.message);
48
+ process.exit(1);
49
+ });
50
+ } else {
51
+ console.error(`Unknown command: ${command}. Run 'fibuki --help' for usage.`);
52
+ process.exit(1);
53
+ }
package/lib/auth.mjs ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Device Authorization Flow
3
+ *
4
+ * 1. POST /api/auth/device/code → get device_code + user_code
5
+ * 2. Open browser to /auth/device?code=XXXX-XXXX
6
+ * 3. Poll /api/auth/device/token until approved
7
+ * 4. Save or print API key
8
+ *
9
+ * Zero dependencies — uses only Node 18+ built-ins.
10
+ */
11
+
12
+ import { writeFileSync, mkdirSync, existsSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { homedir } from "node:os";
15
+ import { exec } from "node:child_process";
16
+
17
+ /**
18
+ * Open a URL in the default browser (platform-aware).
19
+ */
20
+ function openBrowser(url) {
21
+ const platform = process.platform;
22
+ const cmd =
23
+ platform === "darwin" ? "open" :
24
+ platform === "win32" ? "start" :
25
+ "xdg-open";
26
+
27
+ exec(`${cmd} "${url}"`, (err) => {
28
+ if (err) {
29
+ // Silently fail — user can open the URL manually
30
+ }
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Sleep for ms milliseconds.
36
+ */
37
+ function sleep(ms) {
38
+ return new Promise((resolve) => setTimeout(resolve, ms));
39
+ }
40
+
41
+ /**
42
+ * Run the device auth flow.
43
+ */
44
+ export async function deviceAuth({ format = "json", baseUrl = "https://fibuki.com" }) {
45
+ console.log("\n FiBuKI — Device Authorization\n");
46
+
47
+ // Step 1: Request device code
48
+ const codeRes = await fetch(`${baseUrl}/api/auth/device/code`, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ });
52
+
53
+ if (!codeRes.ok) {
54
+ const err = await codeRes.json().catch(() => ({}));
55
+ throw new Error(err.error || `Failed to start auth flow (HTTP ${codeRes.status})`);
56
+ }
57
+
58
+ const { device_code, user_code, verification_uri, expires_in, interval } = await codeRes.json();
59
+
60
+ // Step 2: Show instructions and open browser
61
+ const browserUrl = `${verification_uri}?code=${encodeURIComponent(user_code)}`;
62
+
63
+ console.log(` Your code: ${user_code}\n`);
64
+ console.log(` Opening browser to: ${browserUrl}`);
65
+ console.log(` (If the browser doesn't open, visit the URL above manually)\n`);
66
+ console.log(` Waiting for approval...`);
67
+
68
+ openBrowser(browserUrl);
69
+
70
+ // Step 3: Poll for approval
71
+ let pollInterval = (interval || 5) * 1000;
72
+ const deadline = Date.now() + expires_in * 1000;
73
+
74
+ while (Date.now() < deadline) {
75
+ await sleep(pollInterval);
76
+
77
+ const tokenRes = await fetch(`${baseUrl}/api/auth/device/token`, {
78
+ method: "POST",
79
+ headers: { "Content-Type": "application/json" },
80
+ body: JSON.stringify({
81
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
82
+ device_code,
83
+ }),
84
+ });
85
+
86
+ const tokenData = await tokenRes.json();
87
+
88
+ if (tokenRes.ok && tokenData.access_token) {
89
+ // Success!
90
+ console.log(`\n Authorized! Key "${tokenData.key_name}" created.\n`);
91
+ outputResult({ format, apiKey: tokenData.access_token, keyName: tokenData.key_name, baseUrl });
92
+ return;
93
+ }
94
+
95
+ if (tokenData.error === "authorization_pending") {
96
+ process.stdout.write(".");
97
+ continue;
98
+ }
99
+
100
+ if (tokenData.error === "slow_down") {
101
+ pollInterval += 2000;
102
+ continue;
103
+ }
104
+
105
+ if (tokenData.error === "expired_token") {
106
+ throw new Error("Code expired. Please run the command again.");
107
+ }
108
+
109
+ throw new Error(tokenData.error_description || tokenData.error || "Unknown error");
110
+ }
111
+
112
+ throw new Error("Timed out waiting for approval. Please run the command again.");
113
+ }
114
+
115
+ /**
116
+ * Output the API key in the requested format.
117
+ */
118
+ function outputResult({ format, apiKey, keyName, baseUrl }) {
119
+ if (format === "env") {
120
+ console.log(` FIBUKI_API_KEY=${apiKey}\n`);
121
+ return;
122
+ }
123
+
124
+ if (format === "mcp") {
125
+ const mcpUrl = baseUrl.endsWith("/") ? `${baseUrl}api/mcp/sse` : `${baseUrl}/api/mcp/sse`;
126
+ const config = {
127
+ mcpServers: {
128
+ fibuki: {
129
+ url: mcpUrl,
130
+ headers: {
131
+ Authorization: `Bearer ${apiKey}`,
132
+ },
133
+ },
134
+ },
135
+ };
136
+ console.log(" Add this to your Claude Desktop config:\n");
137
+ console.log(JSON.stringify(config, null, 2));
138
+ console.log("");
139
+ return;
140
+ }
141
+
142
+ // Default: save to ~/.fibuki/config.json
143
+ const configDir = join(homedir(), ".fibuki");
144
+ const configPath = join(configDir, "config.json");
145
+
146
+ if (!existsSync(configDir)) {
147
+ mkdirSync(configDir, { recursive: true });
148
+ }
149
+
150
+ const config = { apiKey, keyName };
151
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
152
+
153
+ console.log(` Saved to ${configPath}\n`);
154
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@fibukiapp/cli",
3
+ "version": "0.1.0",
4
+ "description": "FiBuKI CLI — authenticate and configure AI integrations",
5
+ "bin": {
6
+ "fibuki": "./bin/fibuki.mjs"
7
+ },
8
+ "type": "module",
9
+ "files": [
10
+ "bin/",
11
+ "lib/"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/fibukiapp/fibuki"
19
+ },
20
+ "license": "MIT",
21
+ "keywords": [
22
+ "fibuki",
23
+ "tax",
24
+ "bookkeeping",
25
+ "mcp",
26
+ "cli"
27
+ ]
28
+ }