@kiipu/cli 0.0.4 → 0.0.6

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/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # Kiipu CLI
2
2
 
3
- `@kiipu/cli` is the publishable Kiipu command line interface for local authentication, doctor checks, and direct post actions.
3
+ Publish to Kiipu from your terminal.
4
4
 
5
- ## What It Does
5
+ `@kiipu/cli` is the official command line interface for Kiipu. It is the best place to start if you want to authenticate locally and post directly from the command line.
6
6
 
7
- - authenticate a local machine with a Kiipu API key
8
- - publish posts directly from the terminal
9
- - delete, restore, or permanently purge posts by id
10
- - verify local setup and API reachability with `kiipu doctor`
11
- - point developers to the separate Claude Code plugin and skills packages
7
+ Use it to:
12
8
 
13
- By default, the CLI talks to the production Kiipu API at `https://api.kiipu.com`.
14
- For local development, you can explicitly override the base URL with `KIIPU_API_URL`.
9
+ - sign in on the current device
10
+ - publish posts from the command line
11
+ - delete, restore, or permanently remove posts by id
12
+ - verify local authentication and API access with `kiipu doctor`
13
+
14
+ If you want Claude Code integration on top of the CLI, use `@kiipu/claude-plugin`.
15
15
 
16
16
  ## Install
17
17
 
@@ -21,85 +21,106 @@ npm install -g @kiipu/cli
21
21
 
22
22
  ## Quick Start
23
23
 
24
- Authenticate once on the current machine:
24
+ 1. Sign in:
25
25
 
26
26
  ```bash
27
- kiipu auth login --api-key <cpk_...>
27
+ kiipu auth login
28
28
  ```
29
29
 
30
- Create a post:
30
+ 2. Publish a post:
31
31
 
32
32
  ```bash
33
- kiipu post create "Ship the beta today"
33
+ kiipu post create "Hello Kiipu"
34
34
  ```
35
35
 
36
- Check local setup:
36
+ 3. Confirm local setup:
37
37
 
38
38
  ```bash
39
39
  kiipu doctor
40
40
  ```
41
41
 
42
- ## Commands
42
+ ## Example Workflow
43
43
 
44
44
  ```bash
45
- kiipu auth login --api-key <cpk_...>
45
+ kiipu auth login
46
+ kiipu post create "Ship the beta today"
46
47
  kiipu auth status
47
- kiipu auth logout
48
-
49
- kiipu post create "Hello Kiipu"
50
- kiipu post delete --id post_123
51
- kiipu post restore --id post_123
52
- kiipu post purge --id post_123
53
-
54
- kiipu doctor
55
- kiipu skills
56
48
  ```
57
49
 
58
- ## API Base URL
50
+ ## Authentication
59
51
 
60
- The CLI uses the production Kiipu API by default:
52
+ By default, `kiipu auth login` opens your browser and connects the current device to your Kiipu account.
61
53
 
62
- ```text
63
- https://api.kiipu.com
54
+ ```bash
55
+ kiipu auth login
64
56
  ```
65
57
 
66
- To point the CLI at another environment during development, set `KIIPU_API_URL` explicitly:
58
+ Useful authentication commands:
67
59
 
68
60
  ```bash
69
- KIIPU_API_URL=http://localhost:3001 kiipu doctor
61
+ kiipu auth login --device-name "MacBook Pro"
62
+ kiipu auth login --no-browser
63
+ kiipu auth login --api-key <cpk_...>
64
+ kiipu auth status
65
+ kiipu auth logout
70
66
  ```
71
67
 
72
- The CLI no longer relies on a persisted `apiBaseUrl` in local config, which helps avoid stale non-production endpoints leaking into normal usage.
73
-
74
- ## Claude Code Packages
75
-
76
- The Claude Code integration ships separately:
68
+ ## Posting
77
69
 
78
- - plugin package: `@kiipu/claude-plugin`
79
- - skill assets package: `@kiipu/skills`
80
-
81
- In this monorepo, you can test the plugin locally with:
70
+ Create a post:
82
71
 
83
72
  ```bash
84
- claude --plugin-dir ./packages/claude-plugin
73
+ kiipu post create "Ship the beta today"
74
+ kiipu post create --content "Ship the beta today"
85
75
  ```
86
76
 
87
- ## Standalone Repo Sync
88
-
89
- Export a clean standalone CLI repository snapshot from the monorepo root:
77
+ Delete, restore, or permanently remove a post by id:
90
78
 
91
79
  ```bash
92
- pnpm export:cli:repo
80
+ kiipu post delete --id post_123
81
+ kiipu post restore --id post_123
82
+ kiipu post purge --id post_123
93
83
  ```
94
84
 
95
- Push that exported snapshot to a dedicated GitHub repository:
85
+ ## Core Commands
96
86
 
97
87
  ```bash
98
- pnpm sync:cli:repo -- git@github.com:kiipu/cli.git
88
+ kiipu auth login
89
+ kiipu auth status
90
+ kiipu auth logout
91
+
92
+ kiipu post create "Hello Kiipu"
93
+ kiipu post delete --id post_123
94
+ kiipu post restore --id post_123
95
+ kiipu post purge --id post_123
96
+
97
+ kiipu doctor
98
+ kiipu --help
99
99
  ```
100
100
 
101
- If your SSH remote is the default `kiipu/cli` repository, you can use:
101
+ ## Troubleshooting
102
+
103
+ If browser login does not finish:
104
+
105
+ - Complete sign-in in the browser tab opened by `kiipu auth login`.
106
+ - If the browser does not open automatically, run `kiipu auth login --no-browser` and open the printed URL yourself.
107
+
108
+ If a command fails with an authentication error:
109
+
110
+ - Run `kiipu auth status` to confirm the current device is still signed in.
111
+ - Re-run `kiipu auth login` if you need to refresh local credentials.
112
+
113
+ If `kiipu doctor` reports a problem:
114
+
115
+ - Re-run `kiipu auth login`.
116
+ - Confirm you can reach Kiipu from the same machine in your browser.
117
+
118
+ ## Help
119
+
120
+ See the full command reference in the terminal:
102
121
 
103
122
  ```bash
104
- pnpm sync:cli:ssh
123
+ kiipu --help
124
+ kiipu auth --help
125
+ kiipu post --help
105
126
  ```
@@ -1,5 +1,110 @@
1
- import { KiipuUserApiClient } from '../lib/kiipu-user-client.js';
2
1
  import { saveKiipuConfig } from '../config/config.js';
2
+ import { createAuthState, createLoopbackServer, createPkcePair, getDefaultDeviceName, openBrowser, waitForEnterBeforeOpeningBrowser } from '../lib/browser-auth.js';
3
+ import { KiipuUserApiClient } from '../lib/kiipu-user-client.js';
4
+ import { logCliEvent } from '../logger/cli-logger.js';
5
+ async function storeAuthenticatedConfig(config, payload) {
6
+ config.apiKey = payload.apiKey;
7
+ config.keyPrefix = payload.keyPrefix ?? undefined;
8
+ config.authUserId = payload.userId;
9
+ config.authUsername = payload.username;
10
+ await saveKiipuConfig(config);
11
+ }
12
+ async function loginWithApiKey(config, apiKey) {
13
+ const client = new KiipuUserApiClient({
14
+ apiBaseUrl: config.apiBaseUrl,
15
+ apiKey,
16
+ });
17
+ const response = await client.getApiKeyMe();
18
+ if (!response.ok) {
19
+ return {
20
+ ok: false,
21
+ message: response.error.message,
22
+ };
23
+ }
24
+ await storeAuthenticatedConfig(config, {
25
+ apiKey,
26
+ keyPrefix: response.data.keyPrefix,
27
+ userId: response.data.userId,
28
+ username: response.data.username,
29
+ });
30
+ return {
31
+ ok: true,
32
+ message: `Authenticated as ${response.data.username} (${response.data.displayName}). Key ${response.data.keyPrefix ?? 'unknown'} is now stored locally.`,
33
+ data: response.data,
34
+ };
35
+ }
36
+ async function loginWithBrowser(config, input) {
37
+ const deviceName = input.deviceName?.trim() || getDefaultDeviceName();
38
+ const state = createAuthState();
39
+ const { verifier, challenge } = createPkcePair();
40
+ const server = await createLoopbackServer(state);
41
+ const client = new KiipuUserApiClient({
42
+ apiBaseUrl: config.apiBaseUrl,
43
+ });
44
+ try {
45
+ const session = await client.createCliAuthSession({
46
+ deviceName,
47
+ redirectUri: server.redirectUri,
48
+ state,
49
+ codeChallenge: challenge,
50
+ });
51
+ if (!session.ok) {
52
+ return {
53
+ ok: false,
54
+ message: session.error.message,
55
+ };
56
+ }
57
+ console.log(`Kiipu CLI will connect this device as "${deviceName}".`);
58
+ console.log(`Waiting for browser login at ${session.data.authorizeUrl}`);
59
+ if (input.noBrowser) {
60
+ console.log('Open the URL above in your browser to continue.');
61
+ }
62
+ else {
63
+ await waitForEnterBeforeOpeningBrowser();
64
+ const opened = openBrowser(session.data.authorizeUrl);
65
+ if (!opened) {
66
+ console.log('Could not open the browser automatically. Open this URL manually:');
67
+ console.log(session.data.authorizeUrl);
68
+ }
69
+ }
70
+ const callback = await server.waitForCallback(new Date(session.data.expiresAt).getTime() - Date.now());
71
+ const exchange = await client.exchangeCliAuthSession({
72
+ sessionId: session.data.sessionId,
73
+ authorizationCode: callback.code,
74
+ codeVerifier: verifier,
75
+ });
76
+ if (!exchange.ok) {
77
+ return {
78
+ ok: false,
79
+ message: exchange.error.message,
80
+ };
81
+ }
82
+ await storeAuthenticatedConfig(config, {
83
+ apiKey: exchange.data.apiKey,
84
+ keyPrefix: exchange.data.keyPrefix,
85
+ userId: exchange.data.userId,
86
+ username: exchange.data.username,
87
+ });
88
+ logCliEvent('auth_browser_login_complete', {
89
+ username: exchange.data.username,
90
+ deviceName,
91
+ });
92
+ return {
93
+ ok: true,
94
+ message: `Authenticated as ${exchange.data.username} (${exchange.data.displayName}). This device is now connected to Kiipu and stored locally.`,
95
+ data: exchange.data,
96
+ };
97
+ }
98
+ catch (error) {
99
+ return {
100
+ ok: false,
101
+ message: error instanceof Error ? error.message : 'Kiipu browser login failed.',
102
+ };
103
+ }
104
+ finally {
105
+ await server.close().catch(() => undefined);
106
+ }
107
+ }
3
108
  export async function runAuthCommand(config, input) {
4
109
  if (input.action === 'logout') {
5
110
  delete config.apiKey;
@@ -9,14 +114,14 @@ export async function runAuthCommand(config, input) {
9
114
  await saveKiipuConfig(config);
10
115
  return {
11
116
  ok: true,
12
- message: 'Kiipu API key authentication was cleared from the local config.',
117
+ message: 'Kiipu authentication was cleared from the local config. Connected devices stay revocable from the web settings page.',
13
118
  };
14
119
  }
15
120
  if (input.action === 'status') {
16
121
  if (!config.apiKey) {
17
122
  return {
18
123
  ok: true,
19
- message: 'Kiipu CLI is not authenticated yet. Run `kiipu auth login --api-key <cpk_...>` first.',
124
+ message: 'Kiipu CLI is not authenticated yet. Run `kiipu auth login` first.',
20
125
  };
21
126
  }
22
127
  const client = new KiipuUserApiClient({
@@ -30,41 +135,23 @@ export async function runAuthCommand(config, input) {
30
135
  message: response.error.message,
31
136
  };
32
137
  }
33
- config.keyPrefix = response.data.keyPrefix ?? config.keyPrefix;
34
- config.authUserId = response.data.userId;
35
- config.authUsername = response.data.username;
36
- await saveKiipuConfig(config);
138
+ await storeAuthenticatedConfig(config, {
139
+ apiKey: config.apiKey,
140
+ keyPrefix: response.data.keyPrefix,
141
+ userId: response.data.userId,
142
+ username: response.data.username,
143
+ });
37
144
  return {
38
145
  ok: true,
39
146
  message: `Authenticated as ${response.data.username} (${response.data.displayName}) with key ${response.data.keyPrefix ?? 'unknown'}.`,
40
147
  data: response.data,
41
148
  };
42
149
  }
43
- if (!input.apiKey) {
44
- return {
45
- ok: false,
46
- message: 'Usage: kiipu auth login --api-key <cpk_...>',
47
- };
150
+ if (input.apiKey) {
151
+ return loginWithApiKey(config, input.apiKey);
48
152
  }
49
- const client = new KiipuUserApiClient({
50
- apiBaseUrl: config.apiBaseUrl,
51
- apiKey: input.apiKey,
153
+ return loginWithBrowser(config, {
154
+ deviceName: input.deviceName,
155
+ noBrowser: input.noBrowser,
52
156
  });
53
- const response = await client.getApiKeyMe();
54
- if (!response.ok) {
55
- return {
56
- ok: false,
57
- message: response.error.message,
58
- };
59
- }
60
- config.apiKey = input.apiKey;
61
- config.keyPrefix = response.data.keyPrefix ?? undefined;
62
- config.authUserId = response.data.userId;
63
- config.authUsername = response.data.username;
64
- await saveKiipuConfig(config);
65
- return {
66
- ok: true,
67
- message: `Authenticated as ${response.data.username} (${response.data.displayName}). Key ${response.data.keyPrefix ?? 'unknown'} is now stored locally.`,
68
- data: response.data,
69
- };
70
157
  }
@@ -1,6 +1,6 @@
1
1
  import { KiipuUserApiClient } from '../lib/kiipu-user-client.js';
2
2
  import { access } from 'node:fs/promises';
3
- import { DEFAULT_KIIPU_API_BASE_URL, getConfiguredApiBaseUrl, getDefaultConfigPath } from '../config/config.js';
3
+ import { getConfiguredApiBaseUrl, getDefaultConfigPath } from '../config/config.js';
4
4
  import { logCliEvent } from '../logger/cli-logger.js';
5
5
  export async function runDoctorCommand(config) {
6
6
  logCliEvent('doctor_start');
@@ -8,13 +8,13 @@ export async function runDoctorCommand(config) {
8
8
  if (!config) {
9
9
  return {
10
10
  ok: false,
11
- message: `Missing config at ${configPath}. Run \`kiipu auth login --api-key <cpk_...>\` first.`,
11
+ message: `Missing config at ${configPath}. Run \`kiipu auth login\` first.`,
12
12
  };
13
13
  }
14
14
  const checks = [];
15
15
  let ok = true;
16
16
  const apiBaseUrlConfig = getConfiguredApiBaseUrl();
17
- checks.push(config.apiKey ? `OK API key: ${config.keyPrefix ?? 'configured'}` : 'Missing API key. Run `kiipu auth login --api-key <cpk_...>`.');
17
+ checks.push(config.apiKey ? `OK API key: ${config.keyPrefix ?? 'configured'}` : 'Missing API key. Run `kiipu auth login`.');
18
18
  if (!(config.apiKey || process.env.KIIPU_API_KEY)) {
19
19
  ok = false;
20
20
  }
@@ -22,10 +22,6 @@ export async function runDoctorCommand(config) {
22
22
  checks.push(apiBaseUrlConfig.source === 'env'
23
23
  ? `WARN API base URL override: ${config.apiBaseUrl}`
24
24
  : `OK API base URL: ${config.apiBaseUrl}`);
25
- if (apiBaseUrlConfig.source === 'default' && config.apiBaseUrl !== DEFAULT_KIIPU_API_BASE_URL) {
26
- ok = false;
27
- checks.push(`Config mismatch: expected ${DEFAULT_KIIPU_API_BASE_URL} but loaded ${config.apiBaseUrl}. Re-run auth to refresh local config.`);
28
- }
29
25
  let apiStatus = `API unreachable at ${config.apiBaseUrl}`;
30
26
  try {
31
27
  const response = await fetch(`${config.apiBaseUrl}/health`);
@@ -59,17 +59,25 @@ export function getHelpResult(command) {
59
59
  'Kiipu CLI',
60
60
  '',
61
61
  'Usage:',
62
+ ' kiipu auth login',
63
+ ' kiipu auth login --device-name "<name>"',
64
+ ' kiipu auth login --no-browser',
62
65
  ' kiipu auth login --api-key <cpk_...>',
63
66
  ' kiipu auth status',
64
67
  ' kiipu auth logout',
65
68
  '',
66
69
  'Description:',
67
- ' Manage local Kiipu API key authentication.',
70
+ ' Manage local Kiipu authentication. Browser login is the default flow.',
68
71
  '',
69
72
  'Options:',
70
- ' --api-key <cpk_...> API key created on /connect. Required for login.',
73
+ ' --device-name "<name>" Name shown under Connected Devices for browser login.',
74
+ ' --no-browser Print the browser login URL instead of opening it automatically.',
75
+ ' --api-key <cpk_...> Manually store an existing API key for compatibility or CI.',
71
76
  '',
72
77
  'Examples:',
78
+ ' kiipu auth login',
79
+ ' kiipu auth login --device-name "MacBook Pro"',
80
+ ' kiipu auth login --no-browser',
73
81
  ' kiipu auth login --api-key cpk_abc123...',
74
82
  ' kiipu auth status',
75
83
  ' kiipu auth logout',
@@ -112,7 +120,7 @@ export function getHelpResult(command) {
112
120
  'Core examples:',
113
121
  ' kiipu post create "Hello Kiipu"',
114
122
  ' kiipu post delete --id 123',
115
- ' kiipu auth login --api-key <cpk_...>',
123
+ ' kiipu auth login',
116
124
  ' kiipu doctor',
117
125
  ' claude --plugin-dir ./packages/claude-plugin',
118
126
  ' KIIPU_API_URL=http://localhost:3001 kiipu doctor',
@@ -25,6 +25,16 @@ export function getConfiguredApiBaseUrl() {
25
25
  source: 'default',
26
26
  };
27
27
  }
28
+ function getFileConfiguredApiBaseUrl(raw) {
29
+ const value = typeof raw.apiBaseUrl === 'string' ? raw.apiBaseUrl.trim() : '';
30
+ if (!value) {
31
+ return null;
32
+ }
33
+ return {
34
+ value,
35
+ source: 'file',
36
+ };
37
+ }
28
38
  export function createDefaultConfig() {
29
39
  return {
30
40
  apiBaseUrl: getConfiguredApiBaseUrl().value,
@@ -37,6 +47,11 @@ export async function loadKiipuConfig(configPath = getDefaultConfigPath()) {
37
47
  const content = await readFile(configPath, 'utf8');
38
48
  const raw = JSON.parse(content);
39
49
  const nextConfig = createDefaultConfig();
50
+ const envConfiguredApiBaseUrl = process.env.KIIPU_API_URL?.trim();
51
+ const fileConfiguredApiBaseUrl = getFileConfiguredApiBaseUrl(raw);
52
+ if (!envConfiguredApiBaseUrl && fileConfiguredApiBaseUrl) {
53
+ nextConfig.apiBaseUrl = fileConfiguredApiBaseUrl.value;
54
+ }
40
55
  if (typeof raw.apiKey === 'string') {
41
56
  nextConfig.apiKey = raw.apiKey;
42
57
  }
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ async function main() {
26
26
  const wantsHelp = hasFlag(normalizedArgs, '--help') || hasFlag(normalizedArgs, '-h');
27
27
  const wantsVersion = hasFlag(normalizedArgs, '--version') || hasFlag(normalizedArgs, '-v');
28
28
  const positionalArgs = normalizedArgs.filter((arg, index, all) => {
29
- if (arg === '--json' || arg === '--help' || arg === '-h' || arg === '--version' || arg === '-v')
29
+ if (arg === '--json' || arg === '--help' || arg === '-h' || arg === '--version' || arg === '-v' || arg === '--no-browser')
30
30
  return false;
31
31
  const prev = all[index - 1];
32
32
  return (prev !== '--scheduled-at' &&
@@ -36,6 +36,7 @@ async function main() {
36
36
  prev !== '--content' &&
37
37
  prev !== '--id' &&
38
38
  prev !== '--api-key' &&
39
+ prev !== '--device-name' &&
39
40
  prev !== '--config-path' &&
40
41
  prev !== '--wrapper-path' &&
41
42
  prev !== '--conversation-id' &&
@@ -88,6 +89,8 @@ async function main() {
88
89
  result = await runAuthCommand(config, {
89
90
  action,
90
91
  apiKey: readFlag(normalizedArgs, '--api-key'),
92
+ deviceName: readFlag(normalizedArgs, '--device-name'),
93
+ noBrowser: hasFlag(normalizedArgs, '--no-browser'),
91
94
  });
92
95
  return printResult(result, asJson);
93
96
  }
@@ -0,0 +1,319 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { createServer } from 'node:http';
3
+ import os from 'node:os';
4
+ import { spawn } from 'node:child_process';
5
+ import readline from 'node:readline/promises';
6
+ import { logCliEvent } from '../logger/cli-logger.js';
7
+ function toBase64Url(buffer) {
8
+ return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
9
+ }
10
+ function createRandomToken(size = 32) {
11
+ return toBase64Url(randomBytes(size));
12
+ }
13
+ export function createPkcePair() {
14
+ const verifier = createRandomToken(48);
15
+ const challenge = toBase64Url(createHash('sha256').update(verifier).digest());
16
+ return {
17
+ verifier,
18
+ challenge,
19
+ };
20
+ }
21
+ export function createAuthState() {
22
+ return createRandomToken(24);
23
+ }
24
+ export function getDefaultDeviceName() {
25
+ return os.hostname();
26
+ }
27
+ export async function createLoopbackServer(expectedState) {
28
+ return new Promise((resolve, reject) => {
29
+ let timeout = null;
30
+ let callbackResolver = null;
31
+ let callbackRejecter = null;
32
+ let pendingValue = null;
33
+ let pendingError = null;
34
+ function renderPage(input) {
35
+ const tone = input.tone ?? 'success';
36
+ const accent = tone === 'success' ? '#d56c47' : '#c2410c';
37
+ const accentSoft = tone === 'success' ? 'rgba(213, 108, 71, 0.16)' : 'rgba(194, 65, 12, 0.14)';
38
+ const badgeText = tone === 'success' ? 'CLI Connected' : 'Connection Error';
39
+ return `<!doctype html>
40
+ <html lang="en">
41
+ <head>
42
+ <meta charset="utf-8" />
43
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
44
+ <title>${input.title}</title>
45
+ <style>
46
+ :root {
47
+ color-scheme: light;
48
+ --bg: #f7f1ec;
49
+ --card: rgba(255, 255, 255, 0.88);
50
+ --text: #201714;
51
+ --muted: rgba(32, 23, 20, 0.64);
52
+ --border: rgba(32, 23, 20, 0.08);
53
+ --accent: ${accent};
54
+ --accent-soft: ${accentSoft};
55
+ }
56
+
57
+ * {
58
+ box-sizing: border-box;
59
+ }
60
+
61
+ body {
62
+ margin: 0;
63
+ min-height: 100vh;
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ padding: 24px;
68
+ font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
69
+ background:
70
+ radial-gradient(circle at top left, rgba(213, 108, 71, 0.18), transparent 30%),
71
+ linear-gradient(135deg, #fcf8f5 0%, #f5ece5 100%);
72
+ color: var(--text);
73
+ }
74
+
75
+ .shell {
76
+ width: min(100%, 560px);
77
+ border: 1px solid var(--border);
78
+ border-radius: 28px;
79
+ background: var(--card);
80
+ backdrop-filter: blur(14px);
81
+ box-shadow:
82
+ 0 20px 60px rgba(61, 33, 20, 0.10),
83
+ inset 0 1px 0 rgba(255, 255, 255, 0.55);
84
+ overflow: hidden;
85
+ }
86
+
87
+ .hero {
88
+ padding: 28px 28px 18px;
89
+ background:
90
+ radial-gradient(circle at top left, var(--accent-soft), transparent 42%),
91
+ linear-gradient(180deg, rgba(255,255,255,0.75), rgba(255,255,255,0.4));
92
+ }
93
+
94
+ .badge {
95
+ display: inline-flex;
96
+ align-items: center;
97
+ gap: 8px;
98
+ padding: 8px 12px;
99
+ border-radius: 999px;
100
+ background: var(--accent-soft);
101
+ color: var(--accent);
102
+ font-size: 12px;
103
+ font-weight: 700;
104
+ letter-spacing: 0.08em;
105
+ text-transform: uppercase;
106
+ }
107
+
108
+ .dot {
109
+ width: 8px;
110
+ height: 8px;
111
+ border-radius: 999px;
112
+ background: currentColor;
113
+ box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.55);
114
+ }
115
+
116
+ .content {
117
+ padding: 8px 28px 28px;
118
+ }
119
+
120
+ h1 {
121
+ margin: 18px 0 10px;
122
+ font-size: clamp(28px, 5vw, 38px);
123
+ line-height: 1.04;
124
+ letter-spacing: -0.04em;
125
+ }
126
+
127
+ p {
128
+ margin: 0;
129
+ color: var(--muted);
130
+ font-size: 15px;
131
+ line-height: 1.7;
132
+ }
133
+
134
+ .panel {
135
+ margin-top: 22px;
136
+ display: grid;
137
+ gap: 12px;
138
+ border: 1px solid var(--border);
139
+ border-radius: 22px;
140
+ background: rgba(255, 255, 255, 0.72);
141
+ padding: 16px 18px;
142
+ }
143
+
144
+ .panel strong {
145
+ display: block;
146
+ font-size: 14px;
147
+ color: var(--text);
148
+ margin-bottom: 4px;
149
+ }
150
+
151
+ .footer {
152
+ margin-top: 16px;
153
+ font-size: 13px;
154
+ color: rgba(32, 23, 20, 0.48);
155
+ }
156
+ </style>
157
+ </head>
158
+ <body>
159
+ <main class="shell">
160
+ <section class="hero">
161
+ <div class="badge"><span class="dot"></span>${badgeText}</div>
162
+ <h1>${input.title}</h1>
163
+ </section>
164
+ <section class="content">
165
+ <p>${input.body}</p>
166
+ <div class="panel">
167
+ <div>
168
+ <strong>What happens next</strong>
169
+ Return to your terminal and Kiipu will finish connecting this device automatically.
170
+ </div>
171
+ </div>
172
+ <p class="footer">This tab is only used to complete the local Kiipu CLI sign-in flow.</p>
173
+ </section>
174
+ </main>
175
+ </body>
176
+ </html>`;
177
+ }
178
+ function respond(response, status, body) {
179
+ response.writeHead(status, {
180
+ 'Content-Type': 'text/html; charset=utf-8',
181
+ });
182
+ response.end(body);
183
+ }
184
+ function done(error, value) {
185
+ if (timeout) {
186
+ clearTimeout(timeout);
187
+ timeout = null;
188
+ }
189
+ if (error) {
190
+ if (callbackRejecter) {
191
+ callbackRejecter(error);
192
+ }
193
+ else {
194
+ pendingError = error;
195
+ }
196
+ }
197
+ else if (value) {
198
+ if (callbackResolver) {
199
+ callbackResolver(value);
200
+ }
201
+ else {
202
+ pendingValue = value;
203
+ }
204
+ }
205
+ }
206
+ const server = createServer((request, response) => {
207
+ const url = new URL(request.url ?? '/', 'http://127.0.0.1');
208
+ if (url.pathname !== '/callback') {
209
+ respond(response, 404, '<h1>Not found</h1>');
210
+ return;
211
+ }
212
+ const code = url.searchParams.get('code')?.trim() ?? '';
213
+ const state = url.searchParams.get('state')?.trim() ?? '';
214
+ if (!code || !state) {
215
+ respond(response, 400, renderPage({
216
+ title: 'Missing login parameters',
217
+ body: 'Kiipu could not complete this local sign-in because the callback was missing required details. Close this tab and run `kiipu auth login` again.',
218
+ tone: 'error',
219
+ }));
220
+ return;
221
+ }
222
+ if (state !== expectedState) {
223
+ respond(response, 400, renderPage({
224
+ title: 'This login link is no longer valid',
225
+ body: 'The browser callback did not match the active Kiipu CLI session. Start a fresh `kiipu auth login` command and try again.',
226
+ tone: 'error',
227
+ }));
228
+ done(new Error('CLI login callback state did not match the expected request.'));
229
+ return;
230
+ }
231
+ respond(response, 200, renderPage({
232
+ title: 'Kiipu CLI connected',
233
+ body: 'This browser step is complete. You can close this tab and return to your terminal.',
234
+ tone: 'success',
235
+ }));
236
+ done(undefined, { code, state });
237
+ });
238
+ server.once('error', reject);
239
+ server.listen(0, '127.0.0.1', () => {
240
+ const address = server.address();
241
+ if (!address || typeof address === 'string') {
242
+ reject(new Error('Failed to determine the local CLI callback port.'));
243
+ return;
244
+ }
245
+ resolve({
246
+ redirectUri: `http://127.0.0.1:${address.port}/callback`,
247
+ waitForCallback(timeoutMs) {
248
+ return new Promise((innerResolve, innerReject) => {
249
+ if (pendingError) {
250
+ const error = pendingError;
251
+ pendingError = null;
252
+ innerReject(error);
253
+ return;
254
+ }
255
+ if (pendingValue) {
256
+ const value = pendingValue;
257
+ pendingValue = null;
258
+ innerResolve(value);
259
+ return;
260
+ }
261
+ callbackResolver = innerResolve;
262
+ callbackRejecter = innerReject;
263
+ timeout = setTimeout(() => {
264
+ innerReject(new Error('Timed out waiting for browser login to complete.'));
265
+ }, timeoutMs);
266
+ });
267
+ },
268
+ close() {
269
+ return new Promise((innerResolve, innerReject) => {
270
+ server.close((error) => {
271
+ if (error) {
272
+ innerReject(error);
273
+ return;
274
+ }
275
+ innerResolve();
276
+ });
277
+ });
278
+ },
279
+ });
280
+ });
281
+ });
282
+ }
283
+ export async function waitForEnterBeforeOpeningBrowser() {
284
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
285
+ return;
286
+ }
287
+ const rl = readline.createInterface({
288
+ input: process.stdin,
289
+ output: process.stdout,
290
+ });
291
+ try {
292
+ await rl.question('Press Enter to open the browser and continue login...');
293
+ }
294
+ finally {
295
+ rl.close();
296
+ }
297
+ }
298
+ export function openBrowser(url) {
299
+ const platform = process.platform;
300
+ const command = platform === 'darwin'
301
+ ? { file: 'open', args: [url] }
302
+ : platform === 'win32'
303
+ ? { file: 'cmd', args: ['/c', 'start', '', url] }
304
+ : { file: 'xdg-open', args: [url] };
305
+ try {
306
+ const child = spawn(command.file, command.args, {
307
+ detached: true,
308
+ stdio: 'ignore',
309
+ });
310
+ child.unref();
311
+ logCliEvent('auth_browser_opened', {
312
+ platform,
313
+ });
314
+ return true;
315
+ }
316
+ catch {
317
+ return false;
318
+ }
319
+ }
@@ -45,7 +45,7 @@ function parsePostMutationRequest(input) {
45
45
  postId,
46
46
  };
47
47
  }
48
- export class KiipuSkillApiClient {
48
+ export class KiipuIntegrationApiClient {
49
49
  config;
50
50
  constructor(config) {
51
51
  this.config = config;
@@ -88,18 +88,18 @@ export class KiipuSkillApiClient {
88
88
  : 'request_failed');
89
89
  }
90
90
  createPost(input) {
91
- return this.request('/skill/posts', 'POST', parseCreatePostRequest(input));
91
+ return this.request('/integrations/posts', 'POST', parseCreatePostRequest(input));
92
92
  }
93
93
  deletePost(input) {
94
94
  const body = parsePostMutationRequest(input);
95
- return this.request(`/skill/posts/${body.postId}/delete`, 'POST', body);
95
+ return this.request(`/integrations/posts/${body.postId}/delete`, 'POST', body);
96
96
  }
97
97
  restorePost(input) {
98
98
  const body = parsePostMutationRequest(input);
99
- return this.request(`/skill/posts/${body.postId}/restore`, 'POST', body);
99
+ return this.request(`/integrations/posts/${body.postId}/restore`, 'POST', body);
100
100
  }
101
101
  permanentDeletePost(input) {
102
102
  const body = parsePostMutationRequest(input);
103
- return this.request(`/skill/posts/${body.postId}/permanent-delete`, 'POST', body);
103
+ return this.request(`/integrations/posts/${body.postId}/permanent-delete`, 'POST', body);
104
104
  }
105
105
  }
@@ -19,7 +19,7 @@ export class KiipuUserApiClient {
19
19
  ...init,
20
20
  headers: {
21
21
  'Content-Type': 'application/json',
22
- Authorization: `Bearer ${this.config.apiKey}`,
22
+ ...(this.config.apiKey ? { Authorization: `Bearer ${this.config.apiKey}` } : {}),
23
23
  ...(init?.headers ?? {}),
24
24
  },
25
25
  });
@@ -47,4 +47,16 @@ export class KiipuUserApiClient {
47
47
  getApiKeyMe() {
48
48
  return this.request('/auth/api-key/me');
49
49
  }
50
+ createCliAuthSession(input) {
51
+ return this.request('/auth/cli/sessions', {
52
+ method: 'POST',
53
+ body: JSON.stringify(input),
54
+ });
55
+ }
56
+ exchangeCliAuthSession(input) {
57
+ return this.request('/auth/cli/exchange', {
58
+ method: 'POST',
59
+ body: JSON.stringify(input),
60
+ });
61
+ }
50
62
  }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { KiipuSkillApiClient } from './kiipu-skill-client.js';
2
+ import { KiipuIntegrationApiClient } from './kiipu-integration-client.js';
3
3
  function formatRequestFailed(message, code) {
4
4
  return `Request failed: ${message} (${code}).`;
5
5
  }
@@ -9,12 +9,12 @@ function getPostApiClient(config) {
9
9
  return {
10
10
  error: {
11
11
  ok: false,
12
- message: 'Kiipu API key is missing. Run `kiipu auth login --api-key <cpk_...>` first.',
12
+ message: 'Kiipu API key is missing. Run `kiipu auth login` first.',
13
13
  },
14
14
  };
15
15
  }
16
16
  return {
17
- client: new KiipuSkillApiClient({
17
+ client: new KiipuIntegrationApiClient({
18
18
  apiBaseUrl: config.apiBaseUrl,
19
19
  apiKey,
20
20
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kiipu/cli",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Kiipu CLI for local authentication, doctor checks, and direct post actions.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -40,7 +40,7 @@
40
40
  "release:major": "npm version major --no-git-tag-version",
41
41
  "pack:local": "node scripts/pack-local.mjs",
42
42
  "verify:package": "node scripts/verify-package.mjs",
43
- "publish:dry-run": "npm publish --dry-run"
43
+ "publish:dry-run": "node ../../infra/scripts/prepare-cli-release.mjs && npm publish ../../.release/cli-package --dry-run"
44
44
  },
45
45
  "dependencies": {},
46
46
  "devDependencies": {