@kiipu/cli 0.0.1

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 ADDED
@@ -0,0 +1,38 @@
1
+ # Kiipu CLI
2
+
3
+ `@kiipu/cli` is the publishable npm package for Kiipu local tooling.
4
+
5
+ It ships:
6
+
7
+ - the `kiipu` CLI
8
+ - `kiipu skills` guidance for the Claude Code plugin package
9
+ - `kiipu doctor`
10
+ - direct post create/delete/restore/purge commands
11
+
12
+ The Kiipu CLI is the stable local control-plane entrypoint for:
13
+
14
+ - authentication
15
+ - direct publishing actions
16
+ - install and doctor checks
17
+
18
+ Claude Code plugin assets now publish separately from `@kiipu/claude-plugin`.
19
+
20
+ ## Standalone Repo Sync
21
+
22
+ Export a clean standalone CLI repository snapshot from the monorepo root:
23
+
24
+ ```bash
25
+ pnpm export:cli:repo
26
+ ```
27
+
28
+ Push that exported snapshot to a dedicated GitHub repository:
29
+
30
+ ```bash
31
+ pnpm sync:cli:repo -- git@github.com:kiipu/cli.git
32
+ ```
33
+
34
+ If your SSH remote is the default `kiipu/cli` repository, you can use:
35
+
36
+ ```bash
37
+ pnpm sync:cli:ssh
38
+ ```
@@ -0,0 +1,70 @@
1
+ import { KiipuUserApiClient } from '../lib/kiipu-user-client.js';
2
+ import { saveKiipuConfig } from '../config/config.js';
3
+ export async function runAuthCommand(config, input) {
4
+ if (input.action === 'logout') {
5
+ delete config.apiKey;
6
+ delete config.keyPrefix;
7
+ delete config.authUserId;
8
+ delete config.authUsername;
9
+ await saveKiipuConfig(config);
10
+ return {
11
+ ok: true,
12
+ message: 'Kiipu API key authentication was cleared from the local config.',
13
+ };
14
+ }
15
+ if (input.action === 'status') {
16
+ if (!config.apiKey) {
17
+ return {
18
+ ok: true,
19
+ message: 'Kiipu CLI is not authenticated yet. Run `kiipu auth login --api-key <cpk_...>` first.',
20
+ };
21
+ }
22
+ const client = new KiipuUserApiClient({
23
+ apiBaseUrl: config.apiBaseUrl,
24
+ apiKey: config.apiKey,
25
+ });
26
+ const response = await client.getApiKeyMe();
27
+ if (!response.ok) {
28
+ return {
29
+ ok: false,
30
+ message: response.error.message,
31
+ };
32
+ }
33
+ config.keyPrefix = response.data.keyPrefix ?? config.keyPrefix;
34
+ config.authUserId = response.data.userId;
35
+ config.authUsername = response.data.username;
36
+ await saveKiipuConfig(config);
37
+ return {
38
+ ok: true,
39
+ message: `Authenticated as ${response.data.username} (${response.data.displayName}) with key ${response.data.keyPrefix ?? 'unknown'}.`,
40
+ data: response.data,
41
+ };
42
+ }
43
+ if (!input.apiKey) {
44
+ return {
45
+ ok: false,
46
+ message: 'Usage: kiipu auth login --api-key <cpk_...>',
47
+ };
48
+ }
49
+ const client = new KiipuUserApiClient({
50
+ apiBaseUrl: config.apiBaseUrl,
51
+ apiKey: input.apiKey,
52
+ });
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
+ }
@@ -0,0 +1,47 @@
1
+ import { KiipuUserApiClient } from '../lib/kiipu-user-client.js';
2
+ import { access } from 'node:fs/promises';
3
+ import { getDefaultConfigPath } from '../config/config.js';
4
+ import { logCliEvent } from '../logger/cli-logger.js';
5
+ export async function runDoctorCommand(config) {
6
+ logCliEvent('doctor_start');
7
+ const configPath = getDefaultConfigPath();
8
+ if (!config) {
9
+ return {
10
+ ok: false,
11
+ message: `Missing config at ${configPath}. Run \`kiipu auth login --api-key <cpk_...>\` first.`,
12
+ };
13
+ }
14
+ const checks = [];
15
+ checks.push(config.apiKey ? `OK API key: ${config.keyPrefix ?? 'configured'}` : 'Missing API key. Run `kiipu auth login --api-key <cpk_...>`.');
16
+ checks.push(config.apiKey || process.env.KIIPU_API_KEY ? 'OK post API auth: configured' : 'Missing API key for post requests.');
17
+ let apiStatus = 'API unreachable';
18
+ try {
19
+ const response = await fetch(`${config.apiBaseUrl}/health`);
20
+ apiStatus = response.ok ? 'API reachable' : `API returned ${response.status}`;
21
+ }
22
+ catch {
23
+ apiStatus = `API unreachable at ${config.apiBaseUrl}`;
24
+ }
25
+ checks.push(apiStatus);
26
+ if (config.apiKey) {
27
+ const authStatus = await new KiipuUserApiClient({
28
+ apiBaseUrl: config.apiBaseUrl,
29
+ apiKey: config.apiKey,
30
+ }).getApiKeyMe();
31
+ checks.push(authStatus.ok ? `OK API key auth: ${authStatus.data.username}` : `API key auth failed: ${authStatus.error.message}`);
32
+ }
33
+ try {
34
+ await access(configPath);
35
+ checks.push(`OK config: ${configPath}`);
36
+ }
37
+ catch {
38
+ checks.push(`Missing config file: ${configPath}`);
39
+ }
40
+ logCliEvent('doctor_complete', {
41
+ ok: checks.every((line) => line.startsWith('OK') || line.includes('reachable')),
42
+ });
43
+ return {
44
+ ok: true,
45
+ message: checks.join('\n'),
46
+ };
47
+ }
@@ -0,0 +1,131 @@
1
+ function block(lines) {
2
+ return lines.join('\n');
3
+ }
4
+ export function getHelpResult(command) {
5
+ if (command === 'post') {
6
+ return {
7
+ ok: true,
8
+ message: block([
9
+ 'Kiipu CLI',
10
+ '',
11
+ 'Usage:',
12
+ ' kiipu post create --content "<text>"',
13
+ ' kiipu post create "<text>"',
14
+ ' kiipu post delete --id <postId>',
15
+ ' kiipu post restore --id <postId>',
16
+ ' kiipu post purge --id <postId>',
17
+ '',
18
+ 'Description:',
19
+ ' Run direct Kiipu post actions from the local CLI.',
20
+ '',
21
+ 'Actions:',
22
+ ' create Create a new post',
23
+ ' delete Soft-delete an existing post by explicit id',
24
+ ' restore Restore a deleted post by explicit id',
25
+ ' purge Permanently delete a post by explicit id',
26
+ '',
27
+ 'Options:',
28
+ ' --content "<text>" Post content for create',
29
+ ' --id <postId> Required for delete, restore, and purge',
30
+ '',
31
+ 'Examples:',
32
+ ' kiipu post create "Hello Kiipu"',
33
+ ' kiipu post create --content "Hello Kiipu"',
34
+ ' kiipu post delete --id 123',
35
+ ' kiipu post restore --id 123',
36
+ ]),
37
+ };
38
+ }
39
+ if (command === 'skills') {
40
+ return {
41
+ ok: true,
42
+ message: block([
43
+ 'Kiipu CLI',
44
+ '',
45
+ 'Skills:',
46
+ ' Claude Code plugin support is published from the separate `@kiipu/claude-plugin` package.',
47
+ '',
48
+ 'Examples:',
49
+ ' kiipu skills',
50
+ ' npm view @kiipu/claude-plugin version',
51
+ ' claude --plugin-dir ./apps/claude-plugin',
52
+ ]),
53
+ };
54
+ }
55
+ if (command === 'auth') {
56
+ return {
57
+ ok: true,
58
+ message: block([
59
+ 'Kiipu CLI',
60
+ '',
61
+ 'Usage:',
62
+ ' kiipu auth login --api-key <cpk_...>',
63
+ ' kiipu auth status',
64
+ ' kiipu auth logout',
65
+ '',
66
+ 'Description:',
67
+ ' Manage local Kiipu API key authentication.',
68
+ '',
69
+ 'Options:',
70
+ ' --api-key <cpk_...> API key created on /connect. Required for login.',
71
+ '',
72
+ 'Examples:',
73
+ ' kiipu auth login --api-key cpk_abc123...',
74
+ ' kiipu auth status',
75
+ ' kiipu auth logout',
76
+ ]),
77
+ };
78
+ }
79
+ if (command === 'doctor') {
80
+ return {
81
+ ok: true,
82
+ message: block([
83
+ 'Kiipu CLI',
84
+ '',
85
+ 'Usage:',
86
+ ' kiipu doctor',
87
+ '',
88
+ 'Description:',
89
+ ' Inspect local setup and API reachability for the local CLI.',
90
+ '',
91
+ 'Checks:',
92
+ ' - local Kiipu config exists',
93
+ ' - API health and API key auth are reachable',
94
+ ]),
95
+ };
96
+ }
97
+ return {
98
+ ok: true,
99
+ message: block([
100
+ 'Kiipu CLI',
101
+ '',
102
+ 'Usage:',
103
+ ' kiipu <command> [options]',
104
+ '',
105
+ 'Core commands:',
106
+ ' post Create, delete, restore, or purge posts with direct CLI arguments',
107
+ ' auth Manage local API key authentication',
108
+ ' doctor Check local setup, API access, and wrapper readiness',
109
+ ' skills Show where the Claude Code plugin package lives',
110
+ '',
111
+ 'Core examples:',
112
+ ' kiipu post create "Hello Kiipu"',
113
+ ' kiipu post delete --id 123',
114
+ ' kiipu auth login --api-key <cpk_...>',
115
+ ' kiipu doctor',
116
+ ' claude --plugin-dir ./apps/claude-plugin',
117
+ '',
118
+ 'Claude Code plugin:',
119
+ ' kiipu skills',
120
+ '',
121
+ 'Global options:',
122
+ ' --json Print structured JSON instead of plain text',
123
+ ' -h, --help Show help for the root CLI or a subcommand',
124
+ '',
125
+ 'Help:',
126
+ ' kiipu --help',
127
+ ' kiipu -h',
128
+ ' kiipu <command> --help',
129
+ ]),
130
+ };
131
+ }
@@ -0,0 +1,27 @@
1
+ import { executePostAction } from '../lib/post-actions.js';
2
+ export async function runPostCommand(config, input) {
3
+ if (input.action === 'create') {
4
+ const content = input.content?.trim();
5
+ if (!content) {
6
+ return {
7
+ ok: false,
8
+ message: 'Usage: kiipu post create --content "<text>"\n or: kiipu post create "<text>"',
9
+ };
10
+ }
11
+ return executePostAction(config, {
12
+ action: 'create',
13
+ content,
14
+ });
15
+ }
16
+ const postId = input.postId?.trim();
17
+ if (!postId) {
18
+ return {
19
+ ok: false,
20
+ message: `Usage: kiipu post ${input.action} --id <postId>`,
21
+ };
22
+ }
23
+ return executePostAction(config, {
24
+ action: input.action,
25
+ postId,
26
+ });
27
+ }
@@ -0,0 +1,18 @@
1
+ export async function runSkillsCommand() {
2
+ return {
3
+ ok: true,
4
+ message: [
5
+ 'Kiipu skills',
6
+ 'Claude Code plugin package: @kiipu/claude-plugin',
7
+ 'Local plugin directory: ./apps/claude-plugin',
8
+ '',
9
+ 'Next actions:',
10
+ ' claude --plugin-dir ./apps/claude-plugin',
11
+ ' npm view @kiipu/claude-plugin version',
12
+ ].join('\n'),
13
+ data: {
14
+ packageName: '@kiipu/claude-plugin',
15
+ pluginDir: './apps/claude-plugin',
16
+ },
17
+ };
18
+ }
@@ -0,0 +1,46 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ export function getKiipuConfigHome() {
6
+ if (process.env.KIIPU_CONFIG_HOME) {
7
+ return process.env.KIIPU_CONFIG_HOME;
8
+ }
9
+ return path.join(os.homedir(), '.config', 'kiipu');
10
+ }
11
+ export function getDefaultConfigPath() {
12
+ return path.join(getKiipuConfigHome(), 'config.json');
13
+ }
14
+ export function createDefaultConfig() {
15
+ return {
16
+ apiBaseUrl: process.env.KIIPU_API_URL ?? 'http://localhost:4000',
17
+ };
18
+ }
19
+ export async function loadKiipuConfig(configPath = getDefaultConfigPath()) {
20
+ if (!existsSync(configPath)) {
21
+ return null;
22
+ }
23
+ const content = await readFile(configPath, 'utf8');
24
+ const raw = JSON.parse(content);
25
+ const nextConfig = createDefaultConfig();
26
+ if (typeof raw.apiBaseUrl === 'string') {
27
+ nextConfig.apiBaseUrl = raw.apiBaseUrl;
28
+ }
29
+ if (typeof raw.apiKey === 'string') {
30
+ nextConfig.apiKey = raw.apiKey;
31
+ }
32
+ if (typeof raw.keyPrefix === 'string') {
33
+ nextConfig.keyPrefix = raw.keyPrefix;
34
+ }
35
+ if (typeof raw.authUserId === 'string') {
36
+ nextConfig.authUserId = raw.authUserId;
37
+ }
38
+ if (typeof raw.authUsername === 'string') {
39
+ nextConfig.authUsername = raw.authUsername;
40
+ }
41
+ return nextConfig;
42
+ }
43
+ export async function saveKiipuConfig(config, configPath = getDefaultConfigPath()) {
44
+ await mkdir(path.dirname(configPath), { recursive: true });
45
+ await writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
46
+ }
@@ -0,0 +1,27 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ function parseEnvFile(content) {
3
+ for (const rawLine of content.split(/\r?\n/)) {
4
+ const line = rawLine.trim();
5
+ if (!line || line.startsWith('#')) {
6
+ continue;
7
+ }
8
+ const separatorIndex = line.indexOf('=');
9
+ if (separatorIndex === -1) {
10
+ continue;
11
+ }
12
+ const key = line.slice(0, separatorIndex).trim();
13
+ if (!key || process.env[key] !== undefined) {
14
+ continue;
15
+ }
16
+ const value = line.slice(separatorIndex + 1).trim().replace(/^['"]|['"]$/g, '');
17
+ process.env[key] = value;
18
+ }
19
+ }
20
+ function loadLocalEnv() {
21
+ const filePath = `${process.cwd()}/.env`;
22
+ if (!existsSync(filePath)) {
23
+ return;
24
+ }
25
+ parseEnvFile(readFileSync(filePath, 'utf8'));
26
+ }
27
+ loadLocalEnv();
package/dist/index.js ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ import './config/load-env.js';
3
+ import { createDefaultConfig, loadKiipuConfig } from './config/config.js';
4
+ import { runAuthCommand } from './commands/auth.js';
5
+ import { runDoctorCommand } from './commands/doctor.js';
6
+ import { getHelpResult } from './commands/help.js';
7
+ import { runPostCommand } from './commands/post.js';
8
+ import { runSkillsCommand } from './commands/skills.js';
9
+ import { readFlag, hasFlag } from './utils/args.js';
10
+ function printResult(result, asJson) {
11
+ if (asJson) {
12
+ console.log(JSON.stringify(result, null, 2));
13
+ }
14
+ else {
15
+ console.log(result.message);
16
+ }
17
+ if (!result.ok) {
18
+ process.exitCode = 1;
19
+ }
20
+ }
21
+ async function main() {
22
+ const args = process.argv.slice(2);
23
+ const normalizedArgs = args[0] === '--' ? args.slice(1) : args;
24
+ const asJson = hasFlag(normalizedArgs, '--json');
25
+ const wantsHelp = hasFlag(normalizedArgs, '--help') || hasFlag(normalizedArgs, '-h');
26
+ const positionalArgs = normalizedArgs.filter((arg, index, all) => {
27
+ if (arg === '--json' || arg === '--help' || arg === '-h')
28
+ return false;
29
+ const prev = all[index - 1];
30
+ return (prev !== '--scheduled-at' &&
31
+ prev !== '--package-root' &&
32
+ prev !== '--skills-dir' &&
33
+ prev !== '--text' &&
34
+ prev !== '--content' &&
35
+ prev !== '--id' &&
36
+ prev !== '--api-key' &&
37
+ prev !== '--config-path' &&
38
+ prev !== '--wrapper-path' &&
39
+ prev !== '--conversation-id' &&
40
+ prev !== '--device-name');
41
+ });
42
+ const [command, subcommand, subject] = positionalArgs;
43
+ if (wantsHelp || command === 'help') {
44
+ return printResult(getHelpResult(command === 'help' ? subcommand : command), asJson);
45
+ }
46
+ const config = (await loadKiipuConfig()) ?? createDefaultConfig();
47
+ let result;
48
+ if (command === 'skills') {
49
+ if (wantsHelp || !subcommand) {
50
+ result = getHelpResult('skills');
51
+ return printResult(result, asJson);
52
+ }
53
+ result = await runSkillsCommand();
54
+ return printResult(result, asJson);
55
+ }
56
+ if (command === 'doctor') {
57
+ result = await runDoctorCommand(await loadKiipuConfig());
58
+ return printResult(result, asJson);
59
+ }
60
+ if (command === 'post') {
61
+ const action = subcommand;
62
+ if (!action || !['create', 'delete', 'restore', 'purge'].includes(action)) {
63
+ result = getHelpResult('post');
64
+ return printResult(result, asJson);
65
+ }
66
+ result = await runPostCommand(config, {
67
+ action,
68
+ content: readFlag(normalizedArgs, '--content') ?? subject,
69
+ postId: readFlag(normalizedArgs, '--id'),
70
+ });
71
+ if (!result.ok) {
72
+ return printResult(action === 'create' && !(readFlag(normalizedArgs, '--content') ?? subject) ? getHelpResult('post') : result, asJson);
73
+ }
74
+ return printResult(result, asJson);
75
+ }
76
+ if (command === 'auth') {
77
+ const action = subcommand;
78
+ if (!action || !['login', 'status', 'logout'].includes(action)) {
79
+ result = getHelpResult('auth');
80
+ return printResult(result, asJson);
81
+ }
82
+ result = await runAuthCommand(config, {
83
+ action,
84
+ apiKey: readFlag(normalizedArgs, '--api-key'),
85
+ });
86
+ return printResult(result, asJson);
87
+ }
88
+ result = getHelpResult();
89
+ printResult(result, asJson);
90
+ }
91
+ void main();
@@ -0,0 +1,104 @@
1
+ function buildError(requestId, message, code = 'request_failed') {
2
+ return {
3
+ ok: false,
4
+ error: {
5
+ code,
6
+ message,
7
+ requestId,
8
+ },
9
+ };
10
+ }
11
+ function parseCreatePostRequest(input) {
12
+ const requestId = typeof input.requestId === 'string' ? input.requestId : crypto.randomUUID();
13
+ const rawText = typeof input.rawText === 'string' ? input.rawText.trim() : '';
14
+ if (!rawText) {
15
+ throw new Error('rawText is required.');
16
+ }
17
+ const visibility = input.visibility;
18
+ const tags = Array.isArray(input.tags) ? input.tags.filter((tag) => typeof tag === 'string') : [];
19
+ return {
20
+ requestId,
21
+ requestedAt: typeof input.requestedAt === 'string' ? input.requestedAt : undefined,
22
+ traceId: typeof input.traceId === 'string' ? input.traceId : undefined,
23
+ rawText,
24
+ sourceType: input.sourceType === 'manual' || input.sourceType === 'imported' || input.sourceType === 'skill_command'
25
+ ? input.sourceType
26
+ : 'skill_command',
27
+ finalText: typeof input.finalText === 'string' ? input.finalText : undefined,
28
+ visibility: visibility === 'unlisted' || visibility === 'private' || visibility === 'public' ? visibility : 'public',
29
+ sourceMessageId: typeof input.sourceMessageId === 'string' ? input.sourceMessageId : undefined,
30
+ title: typeof input.title === 'string' ? input.title : input.title === null ? null : undefined,
31
+ tags,
32
+ };
33
+ }
34
+ function parsePostMutationRequest(input) {
35
+ const requestId = typeof input.requestId === 'string' ? input.requestId : crypto.randomUUID();
36
+ const postId = typeof input.postId === 'string' ? input.postId.trim() : '';
37
+ if (!postId) {
38
+ throw new Error('postId is required.');
39
+ }
40
+ return {
41
+ requestId,
42
+ requestedAt: typeof input.requestedAt === 'string' ? input.requestedAt : undefined,
43
+ traceId: typeof input.traceId === 'string' ? input.traceId : undefined,
44
+ postId,
45
+ };
46
+ }
47
+ export class KiipuSkillApiClient {
48
+ config;
49
+ constructor(config) {
50
+ this.config = config;
51
+ }
52
+ async request(path, method, body) {
53
+ let response;
54
+ try {
55
+ response = await fetch(`${this.config.apiBaseUrl}${path}`, {
56
+ method,
57
+ headers: {
58
+ 'Content-Type': 'application/json',
59
+ Authorization: `Bearer ${this.config.apiKey}`,
60
+ },
61
+ body: JSON.stringify(body),
62
+ });
63
+ }
64
+ catch {
65
+ return buildError(body.requestId, `Kiipu API is unreachable at ${this.config.apiBaseUrl}.`, 'api_unreachable');
66
+ }
67
+ const payload = (await response.json());
68
+ if (response.ok) {
69
+ return {
70
+ ok: true,
71
+ requestId: typeof payload.requestId === 'string' ? payload.requestId : body.requestId,
72
+ data: payload.data ?? {},
73
+ };
74
+ }
75
+ return buildError(body.requestId, typeof payload.message === 'string'
76
+ ? payload.message
77
+ : typeof payload.message === 'object' && typeof payload.message?.message === 'string'
78
+ ? payload.message.message
79
+ : typeof payload.error?.message === 'string'
80
+ ? payload.error.message
81
+ : 'CLI request failed.', typeof payload.message === 'object' && typeof payload.message?.code === 'string'
82
+ ? payload.message.code
83
+ : typeof payload.error?.code === 'string'
84
+ ? payload.error.code
85
+ : typeof payload.code === 'string'
86
+ ? payload.code
87
+ : 'request_failed');
88
+ }
89
+ createPost(input) {
90
+ return this.request('/skill/posts', 'POST', parseCreatePostRequest(input));
91
+ }
92
+ deletePost(input) {
93
+ const body = parsePostMutationRequest(input);
94
+ return this.request(`/skill/posts/${body.postId}/delete`, 'POST', body);
95
+ }
96
+ restorePost(input) {
97
+ const body = parsePostMutationRequest(input);
98
+ return this.request(`/skill/posts/${body.postId}/restore`, 'POST', body);
99
+ }
100
+ permanentDeletePost(input) {
101
+ const body = parsePostMutationRequest(input);
102
+ return this.request(`/skill/posts/${body.postId}/permanent-delete`, 'POST', body);
103
+ }
104
+ }
@@ -0,0 +1,50 @@
1
+ function buildError(message, code = 'request_failed') {
2
+ return {
3
+ ok: false,
4
+ error: {
5
+ code,
6
+ message,
7
+ },
8
+ };
9
+ }
10
+ export class KiipuUserApiClient {
11
+ config;
12
+ constructor(config) {
13
+ this.config = config;
14
+ }
15
+ async request(path, init) {
16
+ let response;
17
+ try {
18
+ response = await fetch(`${this.config.apiBaseUrl}${path}`, {
19
+ ...init,
20
+ headers: {
21
+ 'Content-Type': 'application/json',
22
+ Authorization: `Bearer ${this.config.apiKey}`,
23
+ ...(init?.headers ?? {}),
24
+ },
25
+ });
26
+ }
27
+ catch {
28
+ return buildError(`Kiipu API is unreachable at ${this.config.apiBaseUrl}.`, 'api_unreachable');
29
+ }
30
+ const payload = (await response.json());
31
+ if (response.ok) {
32
+ return {
33
+ ok: true,
34
+ data: (payload.data ?? payload),
35
+ };
36
+ }
37
+ return buildError(typeof payload.message === 'string'
38
+ ? payload.message
39
+ : typeof payload.message?.message === 'string'
40
+ ? payload.message.message
41
+ : `Request failed with ${response.status}.`, typeof payload.code === 'string'
42
+ ? payload.code
43
+ : typeof payload.message === 'object' && typeof payload.message?.code === 'string'
44
+ ? payload.message.code
45
+ : 'request_failed');
46
+ }
47
+ getApiKeyMe() {
48
+ return this.request('/auth/api-key/me');
49
+ }
50
+ }
@@ -0,0 +1,113 @@
1
+ import { KiipuSkillApiClient } from './kiipu-skill-client.js';
2
+ function formatRequestFailed(message, code) {
3
+ return `Request failed: ${message} (${code}).`;
4
+ }
5
+ function getPostApiClient(config) {
6
+ const apiKey = config.apiKey ?? process.env.KIIPU_API_KEY ?? '';
7
+ if (!apiKey) {
8
+ return {
9
+ error: {
10
+ ok: false,
11
+ message: 'Kiipu API key is missing. Run `kiipu auth login --api-key <cpk_...>` first.',
12
+ },
13
+ };
14
+ }
15
+ return {
16
+ client: new KiipuSkillApiClient({
17
+ apiBaseUrl: config.apiBaseUrl,
18
+ apiKey,
19
+ }),
20
+ };
21
+ }
22
+ export async function executePostAction(config, input) {
23
+ const { client, error } = getPostApiClient(config);
24
+ if (!client) {
25
+ return error;
26
+ }
27
+ if (input.action === 'create') {
28
+ const response = await client.createPost({
29
+ requestId: crypto.randomUUID(),
30
+ requestedAt: new Date().toISOString(),
31
+ traceId: `${input.traceIdPrefix ?? 'post'}-${Date.now()}`,
32
+ rawText: input.content,
33
+ sourceType: 'skill_command',
34
+ sourceMessageId: input.sourceMessageId ?? `local-${Date.now()}`,
35
+ visibility: 'public',
36
+ tags: ['kiipu'],
37
+ });
38
+ if (!response.ok) {
39
+ return {
40
+ ok: false,
41
+ message: formatRequestFailed(response.error.message, response.error.code),
42
+ };
43
+ }
44
+ return {
45
+ ok: true,
46
+ message: `Post published. Post id ${String(response.data.id)} is now visible in the feed.`,
47
+ data: response.data,
48
+ requestId: response.requestId,
49
+ postId: String(response.data.id),
50
+ };
51
+ }
52
+ if (input.action === 'delete') {
53
+ const response = await client.deletePost({
54
+ requestId: crypto.randomUUID(),
55
+ requestedAt: new Date().toISOString(),
56
+ traceId: `${input.traceIdPrefix ?? 'delete'}-${Date.now()}`,
57
+ postId: input.postId,
58
+ });
59
+ if (!response.ok) {
60
+ return {
61
+ ok: false,
62
+ message: formatRequestFailed(response.error.message, response.error.code),
63
+ };
64
+ }
65
+ return {
66
+ ok: true,
67
+ message: 'Post deleted. The current post is no longer visible in the feed.',
68
+ data: response.data,
69
+ requestId: response.requestId,
70
+ postId: null,
71
+ };
72
+ }
73
+ if (input.action === 'restore') {
74
+ const response = await client.restorePost({
75
+ requestId: crypto.randomUUID(),
76
+ requestedAt: new Date().toISOString(),
77
+ traceId: `${input.traceIdPrefix ?? 'restore'}-${Date.now()}`,
78
+ postId: input.postId,
79
+ });
80
+ if (!response.ok) {
81
+ return {
82
+ ok: false,
83
+ message: formatRequestFailed(response.error.message, response.error.code),
84
+ };
85
+ }
86
+ return {
87
+ ok: true,
88
+ message: `Post restored. Post id ${input.postId} is now back in the feed.`,
89
+ data: response.data,
90
+ requestId: response.requestId,
91
+ postId: input.postId,
92
+ };
93
+ }
94
+ const response = await client.permanentDeletePost({
95
+ requestId: crypto.randomUUID(),
96
+ requestedAt: new Date().toISOString(),
97
+ traceId: `${input.traceIdPrefix ?? 'purge'}-${Date.now()}`,
98
+ postId: input.postId,
99
+ });
100
+ if (!response.ok) {
101
+ return {
102
+ ok: false,
103
+ message: formatRequestFailed(response.error.message, response.error.code),
104
+ };
105
+ }
106
+ return {
107
+ ok: true,
108
+ message: `Post permanently deleted. Post id ${input.postId} has been removed from the database.`,
109
+ data: response.data,
110
+ requestId: response.requestId,
111
+ postId: null,
112
+ };
113
+ }
@@ -0,0 +1,9 @@
1
+ export function logCliEvent(event, details = {}) {
2
+ const payload = {
3
+ scope: 'kiipu-cli',
4
+ event,
5
+ timestamp: new Date().toISOString(),
6
+ ...details,
7
+ };
8
+ console.error(`[kiipu-cli] ${JSON.stringify(payload)}`);
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ export function hasFlag(args, flag) {
2
+ return args.includes(flag);
3
+ }
4
+ export function readFlag(args, flag) {
5
+ const index = args.indexOf(flag);
6
+ if (index === -1) {
7
+ return undefined;
8
+ }
9
+ return args[index + 1]?.trim();
10
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@kiipu/cli",
3
+ "version": "0.0.1",
4
+ "description": "Kiipu CLI for local authentication, doctor checks, and direct post actions.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/kiipu/kiipu.git"
10
+ },
11
+ "keywords": [
12
+ "kiipu",
13
+ "cli",
14
+ "skill"
15
+ ],
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "bin": {
20
+ "kiipu": "dist/index.js"
21
+ },
22
+ "exports": {
23
+ ".": "./dist/index.js",
24
+ "./package.json": "./package.json"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "README.md"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "dev": "node --import tsx src/index.ts",
35
+ "build": "node scripts/clean-build.mjs && tsc -p tsconfig.json",
36
+ "lint": "ESLINT_USE_FLAT_CONFIG=false eslint src test --ext .ts",
37
+ "test": "node --import tsx --test test/**/*.test.ts",
38
+ "release:patch": "npm version patch --no-git-tag-version",
39
+ "release:minor": "npm version minor --no-git-tag-version",
40
+ "release:major": "npm version major --no-git-tag-version",
41
+ "pack:local": "node scripts/pack-local.mjs",
42
+ "verify:package": "node scripts/verify-package.mjs",
43
+ "publish:dry-run": "npm publish --dry-run"
44
+ },
45
+ "dependencies": {},
46
+ "devDependencies": {
47
+ "@types/node": "^22.8.1",
48
+ "@typescript-eslint/eslint-plugin": "^8.12.2",
49
+ "@typescript-eslint/parser": "^8.12.2",
50
+ "eslint": "^9.12.0",
51
+ "eslint-config-prettier": "^9.1.0",
52
+ "tsx": "^4.19.1",
53
+ "typescript": "^5.6.3"
54
+ }
55
+ }