kern0-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.
@@ -0,0 +1,257 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+
5
+ export interface Auth {
6
+ device_token: string;
7
+ expires_at: number;
8
+ token_type: "device" | "pat";
9
+ }
10
+
11
+ export interface User {
12
+ id: string;
13
+ email: string;
14
+ }
15
+
16
+ export interface Organization {
17
+ id: string;
18
+ name: string;
19
+ }
20
+
21
+ export interface Config {
22
+ api_url: string;
23
+ current_org_id?: string;
24
+ current_customer_id?: string;
25
+ current_env_id?: string;
26
+ user?: User;
27
+ auth?: Auth;
28
+ organizations?: Organization[];
29
+ }
30
+
31
+ const CONFIG_DIR = path.join(os.homedir(), ".kern0");
32
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
33
+ const CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials");
34
+
35
+ export function ensureConfigDir(): void {
36
+ if (!fs.existsSync(CONFIG_DIR)) {
37
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
38
+ }
39
+ }
40
+
41
+ export function loadConfig(): Config {
42
+ ensureConfigDir();
43
+
44
+ if (!fs.existsSync(CONFIG_FILE)) {
45
+ return { api_url: process.env.KERN0_API_URL || "http://localhost:3000" };
46
+ }
47
+
48
+ try {
49
+ const content = fs.readFileSync(CONFIG_FILE, "utf-8");
50
+ return JSON.parse(content);
51
+ } catch (error) {
52
+ console.error("Failed to load config:", error);
53
+ return { api_url: process.env.KERN0_API_URL || "http://localhost:3000" };
54
+ }
55
+ }
56
+
57
+ export function saveConfig(config: Config): void {
58
+ ensureConfigDir();
59
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
60
+ mode: 0o600,
61
+ });
62
+ }
63
+
64
+ export function loadToken(): string | null {
65
+ ensureConfigDir();
66
+
67
+ if (!fs.existsSync(CREDENTIALS_FILE)) {
68
+ return null;
69
+ }
70
+
71
+ try {
72
+ const token = fs.readFileSync(CREDENTIALS_FILE, "utf-8").trim();
73
+ return token || null;
74
+ } catch (error) {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ export function saveToken(token: string): void {
80
+ ensureConfigDir();
81
+ fs.writeFileSync(CREDENTIALS_FILE, token, { mode: 0o600 });
82
+ }
83
+
84
+ export function deleteToken(): void {
85
+ ensureConfigDir();
86
+ if (fs.existsSync(CREDENTIALS_FILE)) {
87
+ fs.unlinkSync(CREDENTIALS_FILE);
88
+ }
89
+ }
90
+
91
+ export function getCurrentOrg(): string | null {
92
+ const config = loadConfig();
93
+ return config.current_org_id || null;
94
+ }
95
+
96
+ export function setCurrentOrg(orgId: string): void {
97
+ const config = loadConfig();
98
+ config.current_org_id = orgId;
99
+ config.current_customer_id = undefined; // Clear customer when switching org
100
+ saveConfig(config);
101
+ }
102
+
103
+ export function getCurrentCustomer(): string | null {
104
+ const config = loadConfig();
105
+ return config.current_customer_id || null;
106
+ }
107
+
108
+ export function setCurrentCustomer(customerId: string): void {
109
+ const config = loadConfig();
110
+ config.current_customer_id = customerId;
111
+ saveConfig(config);
112
+ }
113
+
114
+ export function getCurrentEnv(): string | null {
115
+ const config = loadConfig();
116
+ return config.current_env_id || null;
117
+ }
118
+
119
+ export function setCurrentEnv(envId: string): void {
120
+ const config = loadConfig();
121
+ config.current_env_id = envId;
122
+ saveConfig(config);
123
+ }
124
+
125
+ export function getUser(): User | null {
126
+ const config = loadConfig();
127
+ return config.user || null;
128
+ }
129
+
130
+ export function setUser(user: User): void {
131
+ const config = loadConfig();
132
+ config.user = user;
133
+ saveConfig(config);
134
+ }
135
+
136
+ export function getOrganizations(): Organization[] {
137
+ const config = loadConfig();
138
+ return config.organizations || [];
139
+ }
140
+
141
+ export function setOrganizations(orgs: Organization[]): void {
142
+ const config = loadConfig();
143
+ config.organizations = orgs;
144
+ saveConfig(config);
145
+ }
146
+
147
+ export function addOrganization(org: Organization): void {
148
+ const config = loadConfig();
149
+ const orgs = config.organizations || [];
150
+
151
+ // Don't add duplicate
152
+ if (!orgs.find(o => o.id === org.id)) {
153
+ orgs.push(org);
154
+ config.organizations = orgs;
155
+ saveConfig(config);
156
+ }
157
+ }
158
+
159
+ export function getAuth(): Auth | null {
160
+ const config = loadConfig();
161
+ return config.auth || null;
162
+ }
163
+
164
+ export function setAuth(auth: Auth): void {
165
+ const config = loadConfig();
166
+ config.auth = auth;
167
+ saveConfig(config);
168
+ }
169
+
170
+ export function isAuthenticated(): boolean {
171
+ const token = loadToken();
172
+ const auth = getAuth();
173
+
174
+ if (!token || !auth) {
175
+ return false;
176
+ }
177
+
178
+ // Check if token expired
179
+ if (auth.expires_at && auth.expires_at < Date.now()) {
180
+ return false;
181
+ }
182
+
183
+ return true;
184
+ }
185
+
186
+ export function requireAuthentication(): string {
187
+ if (!isAuthenticated()) {
188
+ throw new Error(
189
+ "Not authenticated. Run `kern0 login` to authenticate."
190
+ );
191
+ }
192
+
193
+ const token = loadToken();
194
+ if (!token) {
195
+ throw new Error("Token not found. Run `kern0 login` to authenticate.");
196
+ }
197
+
198
+ return token;
199
+ }
200
+
201
+ export function requireOrganization(): string {
202
+ const orgId = getCurrentOrg();
203
+ if (!orgId) {
204
+ throw new Error(
205
+ "No organization selected. Run `kern0 org select <orgId>` to select one."
206
+ );
207
+ }
208
+ return orgId;
209
+ }
210
+
211
+ export function requireCustomer(): string {
212
+ const customerId = getCurrentCustomer();
213
+ if (!customerId) {
214
+ throw new Error(
215
+ "No customer selected. Run `kern0 switch-customer <id>` to select one."
216
+ );
217
+ }
218
+ return customerId;
219
+ }
220
+
221
+ export function printContext(): void {
222
+ const config = loadConfig();
223
+ const auth = getAuth();
224
+ const user = getUser();
225
+ const org = config.current_org_id
226
+ ? getOrganizations().find(o => o.id === config.current_org_id)
227
+ : null;
228
+
229
+ console.log("\n📋 Current Context:");
230
+
231
+ if (user) {
232
+ console.log(` User: ${user.email} (${user.id})`);
233
+ } else {
234
+ console.log(` User: Not authenticated`);
235
+ }
236
+
237
+ if (org) {
238
+ console.log(` Organization: ${org.name} (${org.id})`);
239
+ } else {
240
+ console.log(` Organization: None selected`);
241
+ }
242
+
243
+ if (config.current_customer_id) {
244
+ console.log(` Customer: ${config.current_customer_id}`);
245
+ }
246
+
247
+ if (config.current_env_id) {
248
+ console.log(` Environment: ${config.current_env_id}`);
249
+ }
250
+
251
+ if (auth?.expires_at) {
252
+ const expiresIn = Math.floor((auth.expires_at - Date.now()) / 1000 / 60 / 60);
253
+ console.log(` Token expires: ${expiresIn} hours from now`);
254
+ }
255
+
256
+ console.log("");
257
+ }
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { login, logout } from "./commands/auth";
4
+ import {
5
+ listOrgs,
6
+ selectOrg,
7
+ getOrgInfo,
8
+ createOrg,
9
+ } from "./commands/org";
10
+ import {
11
+ listTokensCmd,
12
+ createTokenCmd,
13
+ revokeTokenCmd,
14
+ } from "./commands/tokens";
15
+ import {
16
+ listCustomersCmd,
17
+ switchCustomerCmd,
18
+ } from "./commands/customers";
19
+ import {
20
+ listEnvironments,
21
+ getEnvironmentInfo,
22
+ } from "./commands/environments";
23
+ import { printContext } from "./config";
24
+
25
+ const args = process.argv.slice(2);
26
+ const command = args[0];
27
+ const subcommand = args[1];
28
+ const arg1 = args[2];
29
+ const arg2 = args[3];
30
+
31
+ async function main() {
32
+ try {
33
+ switch (command) {
34
+ // Auth commands
35
+ case "login":
36
+ await login();
37
+ break;
38
+
39
+ case "logout":
40
+ await logout();
41
+ break;
42
+
43
+ // Organization commands
44
+ case "org":
45
+ switch (subcommand) {
46
+ case "list":
47
+ await listOrgs();
48
+ break;
49
+ case "select":
50
+ if (!arg1) {
51
+ console.error("Usage: kern0 org select <orgId>");
52
+ process.exit(1);
53
+ }
54
+ await selectOrg(arg1);
55
+ break;
56
+ case "info":
57
+ await getOrgInfo();
58
+ break;
59
+ case "create":
60
+ if (!arg1) {
61
+ console.error("Usage: kern0 org create <name> [type]");
62
+ process.exit(1);
63
+ }
64
+ const type = (arg2 as "direct" | "msp") || "direct";
65
+ await createOrg(arg1, type);
66
+ break;
67
+ default:
68
+ console.error("Usage: kern0 org [list|select|info|create]");
69
+ process.exit(1);
70
+ }
71
+ break;
72
+
73
+ // Customer commands
74
+ case "customers":
75
+ switch (subcommand) {
76
+ case "list":
77
+ await listCustomersCmd();
78
+ break;
79
+ default:
80
+ console.error("Usage: kern0 customers list");
81
+ process.exit(1);
82
+ }
83
+ break;
84
+
85
+ case "switch-customer":
86
+ if (!arg1) {
87
+ console.error("Usage: kern0 switch-customer <customerId>");
88
+ process.exit(1);
89
+ }
90
+ await switchCustomerCmd(arg1);
91
+ break;
92
+
93
+ // Token commands
94
+ case "tokens":
95
+ switch (subcommand) {
96
+ case "list":
97
+ await listTokensCmd();
98
+ break;
99
+ case "create":
100
+ if (!arg1) {
101
+ console.error("Usage: kern0 tokens create <name>");
102
+ process.exit(1);
103
+ }
104
+ await createTokenCmd(arg1);
105
+ break;
106
+ case "delete":
107
+ case "revoke":
108
+ if (!arg1) {
109
+ console.error("Usage: kern0 tokens revoke <tokenId>");
110
+ process.exit(1);
111
+ }
112
+ await revokeTokenCmd(arg1);
113
+ break;
114
+ default:
115
+ console.error("Usage: kern0 tokens [list|create|revoke]");
116
+ process.exit(1);
117
+ }
118
+ break;
119
+
120
+ // Environment commands
121
+ case "environments":
122
+ case "envs":
123
+ case "env":
124
+ switch (subcommand) {
125
+ case "list":
126
+ await listEnvironments();
127
+ break;
128
+ case "info":
129
+ if (!arg1) {
130
+ console.error("Usage: kern0 environments info <envId>");
131
+ process.exit(1);
132
+ }
133
+ await getEnvironmentInfo(arg1);
134
+ break;
135
+ default:
136
+ console.error("Usage: kern0 environments [list|info <envId>]");
137
+ process.exit(1);
138
+ }
139
+ break;
140
+
141
+ // Status commands
142
+ case "whoami":
143
+ printContext();
144
+ break;
145
+
146
+ case "--version":
147
+ case "-v":
148
+ console.log("Kern0 CLI v0.1.0");
149
+ break;
150
+
151
+ case "--help":
152
+ case "-h":
153
+ case undefined:
154
+ showHelp();
155
+ break;
156
+
157
+ default:
158
+ console.error(`Unknown command: ${command}`);
159
+ console.error("Run 'kern0 --help' for usage information");
160
+ process.exit(1);
161
+ }
162
+ } catch (error) {
163
+ if (error instanceof Error) {
164
+ console.error(`\n❌ Error: ${error.message}\n`);
165
+ }
166
+ process.exit(1);
167
+ }
168
+ }
169
+
170
+ function showHelp() {
171
+ console.log(`
172
+ Kern0 CLI - Recovery Readiness Assessment Tool
173
+
174
+ USAGE:
175
+ kern0 <command> [options]
176
+
177
+ AUTHENTICATION:
178
+ login Authenticate with OAuth device flow
179
+ logout Clear credentials and log out
180
+ whoami Show current user, organization, and customer
181
+
182
+ ORGANIZATIONS:
183
+ org list List your organizations
184
+ org select <id> Switch to an organization
185
+ org info Show current organization details
186
+ org create <name> Create a new organization
187
+
188
+ CUSTOMERS:
189
+ customers list List customers in current organization
190
+ switch-customer <id> Switch to a customer
191
+
192
+ TOKENS:
193
+ tokens list List API tokens
194
+ tokens create <name> Create a new API token
195
+ tokens revoke <id> Revoke an API token
196
+
197
+ CONFIG:
198
+ config get <key> Get a configuration value
199
+ config set <key> <val> Set a configuration value
200
+
201
+ OPTIONS:
202
+ -h, --help Show this help message
203
+ -v, --version Show version
204
+
205
+ EXAMPLES:
206
+ kern0 login
207
+ kern0 org list
208
+ kern0 org select acme-corp
209
+ kern0 customers list
210
+ kern0 switch-customer acme-prod
211
+ kern0 tokens create "GitHub Actions"
212
+ kern0 whoami
213
+
214
+ For more information, visit: https://kern0.dev/docs/cli
215
+ `);
216
+ }
217
+
218
+ // Run CLI
219
+ main().catch(error => {
220
+ console.error("Fatal error:", error);
221
+ process.exit(1);
222
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * API client for MCP server to communicate with KernZero backend
3
+ */
4
+
5
+ export async function fetchAPI(
6
+ endpoint: string,
7
+ token: string,
8
+ apiUrl: string
9
+ ): Promise<any> {
10
+ const url = new URL(endpoint, apiUrl).href;
11
+
12
+ const response = await fetch(url, {
13
+ method: "GET",
14
+ headers: {
15
+ "Authorization": `Bearer ${token}`,
16
+ "Content-Type": "application/json",
17
+ },
18
+ });
19
+
20
+ if (!response.ok) {
21
+ const errorText = await response.text();
22
+ throw new Error(
23
+ `API request failed: ${response.status} ${response.statusText}\n${errorText}`
24
+ );
25
+ }
26
+
27
+ const data = await response.json();
28
+
29
+ // Handle wrapped API responses
30
+ if (data.success && data.data) {
31
+ return data.data;
32
+ }
33
+
34
+ // Handle direct responses (auth/me endpoint)
35
+ if (data.authenticated !== undefined) {
36
+ return data;
37
+ }
38
+
39
+ // Return as-is for other responses
40
+ return data;
41
+ }
42
+
43
+ export async function callAPI(
44
+ method: "GET" | "POST" | "PUT" | "DELETE",
45
+ endpoint: string,
46
+ token: string,
47
+ apiUrl: string,
48
+ body?: any
49
+ ): Promise<any> {
50
+ const url = new URL(endpoint, apiUrl).href;
51
+
52
+ const response = await fetch(url, {
53
+ method,
54
+ headers: {
55
+ "Authorization": `Bearer ${token}`,
56
+ "Content-Type": "application/json",
57
+ },
58
+ body: body ? JSON.stringify(body) : undefined,
59
+ });
60
+
61
+ if (!response.ok) {
62
+ const errorText = await response.text();
63
+ throw new Error(
64
+ `API request failed: ${response.status} ${response.statusText}\n${errorText}`
65
+ );
66
+ }
67
+
68
+ const data = await response.json();
69
+
70
+ // Handle wrapped API responses
71
+ if (data.success && data.data) {
72
+ return data.data;
73
+ }
74
+
75
+ return data;
76
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * MCP resource definitions for KernZero
3
+ * Resources represent data that Claude can read and analyze
4
+ */
5
+
6
+ export interface Resource {
7
+ uri: string;
8
+ name: string;
9
+ description: string;
10
+ mimeType: string;
11
+ }
12
+
13
+ export function getOrganizationResources(org: any): Resource[] {
14
+ return [
15
+ {
16
+ uri: `kernzero://organization/${org.id}`,
17
+ name: `Organization: ${org.name}`,
18
+ description: `Details for ${org.name} (${org.type} type) - Created ${new Date(org.createdAt).toLocaleDateString()}`,
19
+ mimeType: "application/json",
20
+ },
21
+ ];
22
+ }
23
+
24
+ export function getEnvironmentResources(orgId: string): Resource[] {
25
+ return [
26
+ {
27
+ uri: `kernzero://environments/${orgId}`,
28
+ name: `Environments in ${orgId}`,
29
+ description: `All monitored environments and forest DNS for organization ${orgId}`,
30
+ mimeType: "application/json",
31
+ },
32
+ ];
33
+ }
34
+
35
+ export function getTokenResources(orgId: string): Resource[] {
36
+ return [
37
+ {
38
+ uri: `kernzero://tokens/${orgId}`,
39
+ name: `API Tokens in ${orgId}`,
40
+ description: `Personal Access Tokens and their scopes for organization ${orgId}`,
41
+ mimeType: "application/json",
42
+ },
43
+ ];
44
+ }
45
+
46
+ export function getAuditLogResources(orgId: string): Resource[] {
47
+ return [
48
+ {
49
+ uri: `kernzero://audit-logs/${orgId}`,
50
+ name: `Audit Logs in ${orgId}`,
51
+ description: `Action history and who did what when in organization ${orgId}`,
52
+ mimeType: "application/json",
53
+ },
54
+ ];
55
+ }