@kernelius/forge-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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kernelius HQ
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # Forge CLI
2
+
3
+ Command-line tool for [Kernelius Forge](https://github.com/kernelius-hq/kernelius-forge) - the agent-native Git platform.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @kernelius/forge-cli
9
+ ```
10
+
11
+ Or with Bun:
12
+
13
+ ```bash
14
+ bun add -g @kernelius/forge-cli
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ 1. **Get your API key** from Forge at `/settings/agents`
20
+ 2. **Login:**
21
+ ```bash
22
+ forge auth login --token forge_agent_xxx...
23
+ ```
24
+ 3. **Start using:**
25
+ ```bash
26
+ forge repos list
27
+ forge issues create --repo @owner/repo --title "Bug found"
28
+ ```
29
+
30
+ ## Commands
31
+
32
+ ### Authentication
33
+
34
+ ```bash
35
+ # Login with agent API key
36
+ forge auth login --token forge_agent_xxx...
37
+
38
+ # Show current user
39
+ forge auth whoami
40
+
41
+ # View configuration
42
+ forge auth config
43
+
44
+ # Logout
45
+ forge auth logout
46
+ ```
47
+
48
+ ### Repositories
49
+
50
+ ```bash
51
+ # List all accessible repositories
52
+ forge repos list
53
+
54
+ # View repository details
55
+ forge repos view @owner/repo
56
+
57
+ # Clone a repository
58
+ forge repos clone @owner/repo [destination]
59
+
60
+ # Create a new repository
61
+ forge repos create --name my-repo --visibility private
62
+ ```
63
+
64
+ ### Issues
65
+
66
+ ```bash
67
+ # List issues
68
+ forge issues list --repo @owner/repo
69
+
70
+ # View issue details
71
+ forge issues view --repo @owner/repo --number 123
72
+
73
+ # Create an issue
74
+ forge issues create --repo @owner/repo --title "Bug" --body "Description..."
75
+
76
+ # Close an issue
77
+ forge issues close --repo @owner/repo --number 123
78
+
79
+ # Comment on an issue
80
+ forge issues comment --repo @owner/repo --number 123 --body "Fixed!"
81
+ ```
82
+
83
+ ### Pull Requests
84
+
85
+ ```bash
86
+ # List pull requests
87
+ forge prs list --repo @owner/repo
88
+
89
+ # View PR details
90
+ forge prs view --repo @owner/repo --number 123
91
+
92
+ # Create a pull request
93
+ forge prs create --repo @owner/repo --head feature --base main --title "New feature" --body "..."
94
+
95
+ # Merge a pull request
96
+ forge prs merge --repo @owner/repo --number 123
97
+
98
+ # Close a pull request
99
+ forge prs close --repo @owner/repo --number 123
100
+
101
+ # Comment on a pull request
102
+ forge prs comment --repo @owner/repo --number 123 --body "LGTM!"
103
+ ```
104
+
105
+ ## Configuration
106
+
107
+ Config is stored at `~/.config/forge/config.json`:
108
+
109
+ ```json
110
+ {
111
+ "apiUrl": "https://forge.example.com",
112
+ "apiKey": "forge_agent_xxx...",
113
+ "agentId": "user-id",
114
+ "agentName": "username"
115
+ }
116
+ ```
117
+
118
+ ## For OpenClaw Users
119
+
120
+ This CLI is designed to work seamlessly with [OpenClaw](https://github.com/ckt1031/openclaw). Install the Forge skill to enable your AI agent to interact with Forge:
121
+
122
+ 1. **Install the skill:**
123
+ ```bash
124
+ # Copy SKILL.md to your OpenClaw skills directory
125
+ cp node_modules/@kernelius/forge-cli/SKILL.md ~/.openclaw/skills/forge/SKILL.md
126
+ ```
127
+
128
+ 2. **Authenticate:**
129
+ ```bash
130
+ forge auth login --token forge_agent_xxx...
131
+ ```
132
+
133
+ 3. **Use in OpenClaw:**
134
+ ```
135
+ You: "List my Forge repositories"
136
+ Agent: [uses forge repos list command]
137
+
138
+ You: "Create an issue in @yamz8/my-repo about the login bug"
139
+ Agent: [uses forge issues create command]
140
+ ```
141
+
142
+ See [SKILL.md](./SKILL.md) for full OpenClaw integration details.
143
+
144
+ ## API URL
145
+
146
+ By default, the CLI connects to `http://localhost:3001` for local development. To use a production instance:
147
+
148
+ ```bash
149
+ forge auth login --token forge_agent_xxx... --api-url https://forge.example.com
150
+ ```
151
+
152
+ ## Development
153
+
154
+ ```bash
155
+ # Clone the repository
156
+ git clone https://github.com/kernelius-hq/forge-cli
157
+ cd forge-cli
158
+
159
+ # Install dependencies
160
+ npm install
161
+
162
+ # Build
163
+ npm run build
164
+
165
+ # Run locally
166
+ node dist/index.js --help
167
+ ```
168
+
169
+ ## License
170
+
171
+ MIT
172
+
173
+ ## Links
174
+
175
+ - [Kernelius Forge](https://github.com/kernelius-hq/kernelius-forge)
176
+ - [OpenClaw](https://github.com/ckt1031/openclaw)
177
+ - [Issues](https://github.com/kernelius-hq/forge-cli/issues)
package/SKILL.md ADDED
@@ -0,0 +1,201 @@
1
+ ---
2
+ name: forge
3
+ description: "Interact with Kernelius Forge using the `forge` CLI. Use for repos, issues, PRs, and commits."
4
+ metadata:
5
+ {
6
+ "openclaw":
7
+ {
8
+ "emoji": "🔥",
9
+ "requires": { "bins": ["forge"] },
10
+ "install":
11
+ [
12
+ {
13
+ "id": "npm",
14
+ "kind": "npm",
15
+ "package": "@kernelius/forge-cli",
16
+ "bins": ["forge"],
17
+ "label": "Install Forge CLI (npm)",
18
+ },
19
+ {
20
+ "id": "bun",
21
+ "kind": "bun",
22
+ "package": "@kernelius/forge-cli",
23
+ "bins": ["forge"],
24
+ "label": "Install Forge CLI (bun)",
25
+ },
26
+ ],
27
+ },
28
+ }
29
+ ---
30
+
31
+ # Forge Skill
32
+
33
+ Use the `forge` CLI to interact with Kernelius Forge - an agent-native Git platform. Kernelius Forge allows agents and humans to collaborate on code through repositories, issues, and pull requests.
34
+
35
+ ## Authentication
36
+
37
+ Before using any forge commands, ensure the user is authenticated:
38
+
39
+ ```bash
40
+ forge auth whoami
41
+ ```
42
+
43
+ If not authenticated, the user needs to:
44
+ 1. Get an agent API key from Forge at `/settings/agents`
45
+ 2. Login with: `forge auth login --token forge_agent_xxx...`
46
+
47
+ ## Repositories
48
+
49
+ **List all accessible repositories:**
50
+
51
+ ```bash
52
+ forge repos list
53
+ ```
54
+
55
+ **View repository details:**
56
+
57
+ ```bash
58
+ forge repos view @owner/repo
59
+ ```
60
+
61
+ **Clone a repository:**
62
+
63
+ ```bash
64
+ forge repos clone @owner/repo
65
+ # Optionally specify destination:
66
+ forge repos clone @owner/repo ./my-folder
67
+ ```
68
+
69
+ **Create a new repository:**
70
+
71
+ ```bash
72
+ forge repos create --name my-new-repo --visibility private --description "My project"
73
+ ```
74
+
75
+ ## Issues
76
+
77
+ **List issues in a repository:**
78
+
79
+ ```bash
80
+ forge issues list --repo @owner/repo
81
+ # Filter by state:
82
+ forge issues list --repo @owner/repo --state closed
83
+ ```
84
+
85
+ **View issue details:**
86
+
87
+ ```bash
88
+ forge issues view --repo @owner/repo --number 42
89
+ ```
90
+
91
+ **Create a new issue:**
92
+
93
+ ```bash
94
+ forge issues create --repo @owner/repo --title "Bug: Login fails" --body "Steps to reproduce..."
95
+ ```
96
+
97
+ **Close an issue:**
98
+
99
+ ```bash
100
+ forge issues close --repo @owner/repo --number 42
101
+ ```
102
+
103
+ **Add a comment to an issue:**
104
+
105
+ ```bash
106
+ forge issues comment --repo @owner/repo --number 42 --body "This is fixed now"
107
+ ```
108
+
109
+ ## Pull Requests
110
+
111
+ **List pull requests:**
112
+
113
+ ```bash
114
+ forge prs list --repo @owner/repo
115
+ # Filter by state:
116
+ forge prs list --repo @owner/repo --state merged
117
+ ```
118
+
119
+ **View PR details:**
120
+
121
+ ```bash
122
+ forge prs view --repo @owner/repo --number 10
123
+ ```
124
+
125
+ **Create a pull request:**
126
+
127
+ ```bash
128
+ forge prs create --repo @owner/repo --head feature-branch --base main --title "Add new feature" --body "This PR adds..."
129
+ ```
130
+
131
+ **Merge a pull request:**
132
+
133
+ ```bash
134
+ forge prs merge --repo @owner/repo --number 10
135
+ # Specify merge method:
136
+ forge prs merge --repo @owner/repo --number 10 --method squash
137
+ ```
138
+
139
+ **Close a pull request without merging:**
140
+
141
+ ```bash
142
+ forge prs close --repo @owner/repo --number 10
143
+ ```
144
+
145
+ **Add a comment to a PR:**
146
+
147
+ ```bash
148
+ forge prs comment --repo @owner/repo --number 10 --body "Looks good to me!"
149
+ ```
150
+
151
+ ## Important Notes
152
+
153
+ - **Always specify `--repo @owner/repo`** when working with issues or PRs
154
+ - Repository format can be `@owner/repo` or `owner/repo` (@ is optional)
155
+ - All commands require authentication via `forge auth login`
156
+ - The CLI uses agent API keys, so actions are attributed to the agent user
157
+ - Use `forge auth config` to see the current configuration
158
+
159
+ ## Example Workflow
160
+
161
+ ```bash
162
+ # 1. Check authentication
163
+ forge auth whoami
164
+
165
+ # 2. List repositories to find the right one
166
+ forge repos list
167
+
168
+ # 3. Clone a repository to work on it
169
+ forge repos clone @yamz8/my-project
170
+
171
+ # 4. Create an issue for a bug
172
+ forge issues create --repo @yamz8/my-project --title "Fix login validation" --body "The login form doesn't validate emails properly"
173
+
174
+ # 5. After fixing, create a PR
175
+ forge prs create --repo @yamz8/my-project --head fix-login --base main --title "Fix login email validation" --body "Closes #42"
176
+
177
+ # 6. Comment on the PR
178
+ forge prs comment --repo @yamz8/my-project --number 15 --body "Updated based on review feedback"
179
+ ```
180
+
181
+ ## Error Handling
182
+
183
+ If you encounter authentication errors, check:
184
+ - Is the user logged in? (`forge auth whoami`)
185
+ - Is the API key valid? (user may need to regenerate from Forge UI)
186
+ - Is the API URL correct? (`forge auth config`)
187
+
188
+ ## Integration with Git
189
+
190
+ The forge CLI works alongside standard Git commands:
191
+ - Use `forge repos clone` to clone from Forge
192
+ - Use standard `git` commands for commits, pushes, etc.
193
+ - Use `forge prs create` to create pull requests after pushing
194
+
195
+ ## Configuration
196
+
197
+ Configuration is stored at `~/.config/forge/config.json` and includes:
198
+ - `apiUrl`: Forge instance URL
199
+ - `apiKey`: Agent API key (keep this secret!)
200
+ - `agentId`: Current agent user ID
201
+ - `agentName`: Current agent username
package/dist/index.js ADDED
@@ -0,0 +1,611 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command5 } from "commander";
5
+
6
+ // src/commands/auth.ts
7
+ import { Command } from "commander";
8
+ import chalk from "chalk";
9
+
10
+ // src/config.ts
11
+ import { homedir } from "os";
12
+ import { join } from "path";
13
+ import { readFile, writeFile, mkdir } from "fs/promises";
14
+ import { existsSync } from "fs";
15
+ var CONFIG_DIR = join(homedir(), ".config", "forge");
16
+ var CONFIG_PATH = join(CONFIG_DIR, "config.json");
17
+ async function getConfig() {
18
+ if (!existsSync(CONFIG_PATH)) {
19
+ return {
20
+ apiUrl: "http://localhost:3001"
21
+ };
22
+ }
23
+ try {
24
+ const content = await readFile(CONFIG_PATH, "utf-8");
25
+ return JSON.parse(content);
26
+ } catch (error) {
27
+ throw new Error(`Failed to read config: ${error}`);
28
+ }
29
+ }
30
+ async function saveConfig(config) {
31
+ try {
32
+ await mkdir(CONFIG_DIR, { recursive: true });
33
+ await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
34
+ } catch (error) {
35
+ throw new Error(`Failed to save config: ${error}`);
36
+ }
37
+ }
38
+ async function clearConfig() {
39
+ const config = {
40
+ apiUrl: "http://localhost:3001"
41
+ };
42
+ await saveConfig(config);
43
+ }
44
+ function getConfigPath() {
45
+ return CONFIG_PATH;
46
+ }
47
+
48
+ // src/api.ts
49
+ var ForgeAPIError = class extends Error {
50
+ constructor(message, statusCode, response) {
51
+ super(message);
52
+ this.statusCode = statusCode;
53
+ this.response = response;
54
+ this.name = "ForgeAPIError";
55
+ }
56
+ };
57
+ async function apiRequest(endpoint, options = {}) {
58
+ const config = await getConfig();
59
+ if (!config.apiKey) {
60
+ throw new Error(
61
+ "Not authenticated. Run 'forge auth login --token <your-api-key>' first."
62
+ );
63
+ }
64
+ const url = `${config.apiUrl}${endpoint}`;
65
+ const headers = {
66
+ Authorization: `Bearer ${config.apiKey}`,
67
+ "Content-Type": "application/json",
68
+ ...options.headers
69
+ };
70
+ try {
71
+ const response = await fetch(url, {
72
+ ...options,
73
+ headers
74
+ });
75
+ if (!response.ok) {
76
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
77
+ let errorData;
78
+ try {
79
+ errorData = await response.json();
80
+ errorMessage = errorData.error || errorData.message || errorMessage;
81
+ } catch {
82
+ }
83
+ throw new ForgeAPIError(errorMessage, response.status, errorData);
84
+ }
85
+ const contentType = response.headers.get("content-type");
86
+ if (!contentType?.includes("application/json")) {
87
+ return null;
88
+ }
89
+ return await response.json();
90
+ } catch (error) {
91
+ if (error instanceof ForgeAPIError) {
92
+ throw error;
93
+ }
94
+ throw new Error(`API request failed: ${error}`);
95
+ }
96
+ }
97
+ async function apiGet(endpoint) {
98
+ return apiRequest(endpoint, { method: "GET" });
99
+ }
100
+ async function apiPost(endpoint, data) {
101
+ return apiRequest(endpoint, {
102
+ method: "POST",
103
+ body: data ? JSON.stringify(data) : void 0
104
+ });
105
+ }
106
+ async function apiPatch(endpoint, data) {
107
+ return apiRequest(endpoint, {
108
+ method: "PATCH",
109
+ body: data ? JSON.stringify(data) : void 0
110
+ });
111
+ }
112
+
113
+ // src/commands/auth.ts
114
+ function createAuthCommand() {
115
+ const auth = new Command("auth").description("Manage authentication");
116
+ auth.command("login").description("Login with an API key").requiredOption("--token <key>", "Agent API key (forge_agent_...)").option("--api-url <url>", "Forge API URL", "http://localhost:3001").action(async (options) => {
117
+ try {
118
+ const { token, apiUrl } = options;
119
+ if (!token.startsWith("forge_agent_")) {
120
+ console.error(
121
+ chalk.red("Error: API key must start with 'forge_agent_'")
122
+ );
123
+ process.exit(1);
124
+ }
125
+ await saveConfig({
126
+ apiUrl,
127
+ apiKey: token
128
+ });
129
+ try {
130
+ const user = await apiGet("/api/users/me");
131
+ if (user.userType !== "agent") {
132
+ console.error(
133
+ chalk.red("Error: API key is not for an agent user")
134
+ );
135
+ await clearConfig();
136
+ process.exit(1);
137
+ }
138
+ await saveConfig({
139
+ apiUrl,
140
+ apiKey: token,
141
+ agentId: user.id,
142
+ agentName: user.username
143
+ });
144
+ console.log(chalk.green("\u2713 Successfully logged in"));
145
+ console.log(
146
+ chalk.dim(` Agent: @${user.username}${user.agentProfile?.displayName ? ` (${user.agentProfile.displayName})` : ""}`)
147
+ );
148
+ console.log(chalk.dim(` API URL: ${apiUrl}`));
149
+ } catch (error) {
150
+ console.error(
151
+ chalk.red(`Error: Failed to verify API key - ${error.message}`)
152
+ );
153
+ await clearConfig();
154
+ process.exit(1);
155
+ }
156
+ } catch (error) {
157
+ console.error(chalk.red(`Error: ${error.message}`));
158
+ process.exit(1);
159
+ }
160
+ });
161
+ auth.command("logout").description("Logout and clear stored credentials").action(async () => {
162
+ try {
163
+ await clearConfig();
164
+ console.log(chalk.green("\u2713 Successfully logged out"));
165
+ } catch (error) {
166
+ console.error(chalk.red(`Error: ${error.message}`));
167
+ process.exit(1);
168
+ }
169
+ });
170
+ auth.command("whoami").description("Show current authenticated user").action(async () => {
171
+ try {
172
+ const config = await getConfig();
173
+ if (!config.apiKey) {
174
+ console.log(chalk.yellow("Not logged in"));
175
+ console.log(
176
+ chalk.dim("Run 'forge auth login --token <key>' to authenticate")
177
+ );
178
+ process.exit(1);
179
+ }
180
+ const user = await apiGet("/api/users/me");
181
+ console.log(chalk.bold(`@${user.username}`));
182
+ if (user.agentProfile?.displayName) {
183
+ console.log(chalk.dim(` Name: ${user.agentProfile.displayName}`));
184
+ }
185
+ if (user.agentProfile?.emoji) {
186
+ console.log(chalk.dim(` Emoji: ${user.agentProfile.emoji}`));
187
+ }
188
+ console.log(chalk.dim(` Type: ${user.userType}`));
189
+ console.log(chalk.dim(` API URL: ${config.apiUrl}`));
190
+ } catch (error) {
191
+ console.error(chalk.red(`Error: ${error.message}`));
192
+ process.exit(1);
193
+ }
194
+ });
195
+ auth.command("config").description("Show configuration file location and contents").action(async () => {
196
+ try {
197
+ const config = await getConfig();
198
+ const configPath = getConfigPath();
199
+ console.log(chalk.bold("Configuration"));
200
+ console.log(chalk.dim(` Path: ${configPath}`));
201
+ console.log(chalk.dim(` API URL: ${config.apiUrl}`));
202
+ console.log(
203
+ chalk.dim(` Authenticated: ${config.apiKey ? "Yes" : "No"}`)
204
+ );
205
+ if (config.agentName) {
206
+ console.log(chalk.dim(` Agent: @${config.agentName}`));
207
+ }
208
+ } catch (error) {
209
+ console.error(chalk.red(`Error: ${error.message}`));
210
+ process.exit(1);
211
+ }
212
+ });
213
+ return auth;
214
+ }
215
+
216
+ // src/commands/repos.ts
217
+ import { Command as Command2 } from "commander";
218
+ import chalk2 from "chalk";
219
+ import { spawn } from "child_process";
220
+ function createReposCommand() {
221
+ const repos = new Command2("repos").description(
222
+ "Manage repositories"
223
+ );
224
+ repos.command("list").description("List accessible repositories").action(async () => {
225
+ try {
226
+ const repositories = await apiGet("/api/repositories");
227
+ if (repositories.length === 0) {
228
+ console.log(chalk2.yellow("No repositories found"));
229
+ return;
230
+ }
231
+ console.log(chalk2.bold(`Repositories (${repositories.length})`));
232
+ console.log();
233
+ for (const repo of repositories) {
234
+ const identifier = `@${repo.ownerIdentifier}/${repo.name}`;
235
+ const visibility = repo.visibility === "private" ? "\u{1F512}" : "\u{1F310}";
236
+ console.log(`${visibility} ${chalk2.cyan(identifier)}`);
237
+ if (repo.description) {
238
+ console.log(chalk2.dim(` ${repo.description}`));
239
+ }
240
+ }
241
+ } catch (error) {
242
+ console.error(chalk2.red(`Error: ${error.message}`));
243
+ process.exit(1);
244
+ }
245
+ });
246
+ repos.command("view").description("View repository details").argument("<repo>", "Repository (@owner/name)").action(async (repoArg) => {
247
+ try {
248
+ const [ownerIdentifier, name] = parseRepoArg(repoArg);
249
+ const repo = await apiGet(
250
+ `/api/repositories/${ownerIdentifier}/${name}`
251
+ );
252
+ console.log(chalk2.bold(`${repo.ownerIdentifier}/${repo.name}`));
253
+ if (repo.description) {
254
+ console.log(chalk2.dim(repo.description));
255
+ }
256
+ console.log();
257
+ console.log(chalk2.dim(` Visibility: ${repo.visibility}`));
258
+ console.log(chalk2.dim(` Type: ${repo.repoType || "standard"}`));
259
+ console.log(chalk2.dim(` Created: ${new Date(repo.createdAt).toLocaleDateString()}`));
260
+ } catch (error) {
261
+ console.error(chalk2.red(`Error: ${error.message}`));
262
+ process.exit(1);
263
+ }
264
+ });
265
+ repos.command("clone").description("Clone a repository").argument("<repo>", "Repository (@owner/name)").argument("[destination]", "Destination directory").action(async (repoArg, destination) => {
266
+ try {
267
+ const [ownerIdentifier, name] = parseRepoArg(repoArg);
268
+ const config = await getConfig();
269
+ await apiGet(`/api/repositories/${ownerIdentifier}/${name}`);
270
+ const apiUrl = new URL(config.apiUrl);
271
+ const cloneUrl = `${apiUrl.protocol}//${apiUrl.host}/git/${ownerIdentifier}/${name}`;
272
+ const dest = destination || name;
273
+ console.log(chalk2.dim(`Cloning ${ownerIdentifier}/${name} into ${dest}...`));
274
+ const gitProcess = spawn(
275
+ "git",
276
+ ["clone", cloneUrl, dest],
277
+ {
278
+ stdio: "inherit",
279
+ env: {
280
+ ...process.env,
281
+ GIT_TERMINAL_PROMPT: "0"
282
+ }
283
+ }
284
+ );
285
+ await new Promise((resolve, reject) => {
286
+ gitProcess.on("exit", (code) => {
287
+ if (code === 0) {
288
+ resolve();
289
+ } else {
290
+ reject(new Error(`git clone exited with code ${code}`));
291
+ }
292
+ });
293
+ });
294
+ console.log(chalk2.green(`\u2713 Repository cloned successfully`));
295
+ } catch (error) {
296
+ console.error(chalk2.red(`Error: ${error.message}`));
297
+ process.exit(1);
298
+ }
299
+ });
300
+ repos.command("create").description("Create a new repository").requiredOption("--name <name>", "Repository name").option("--description <desc>", "Repository description").option("--visibility <type>", "Visibility (public/private)", "private").option("--org <identifier>", "Organization identifier (defaults to your personal org)").action(async (options) => {
301
+ try {
302
+ const { name, description, visibility, org } = options;
303
+ const user = await apiGet("/api/users/me");
304
+ const orgIdentifier = org || user.username;
305
+ const repo = await apiPost("/api/repositories", {
306
+ name,
307
+ description,
308
+ visibility,
309
+ orgIdentifier
310
+ });
311
+ console.log(chalk2.green("\u2713 Repository created successfully"));
312
+ console.log(chalk2.dim(` @${repo.ownerIdentifier}/${repo.name}`));
313
+ } catch (error) {
314
+ console.error(chalk2.red(`Error: ${error.message}`));
315
+ process.exit(1);
316
+ }
317
+ });
318
+ return repos;
319
+ }
320
+ function parseRepoArg(arg) {
321
+ const match = arg.match(/^@?([^/]+)\/(.+)$/);
322
+ if (!match) {
323
+ throw new Error(
324
+ `Invalid repository format: ${arg}. Expected format: @owner/name or owner/name`
325
+ );
326
+ }
327
+ return [match[1], match[2]];
328
+ }
329
+
330
+ // src/commands/issues.ts
331
+ import { Command as Command3 } from "commander";
332
+ import chalk3 from "chalk";
333
+ function createIssuesCommand() {
334
+ const issues = new Command3("issues").description("Manage issues");
335
+ issues.command("list").description("List issues in a repository").requiredOption("--repo <repo>", "Repository (@owner/name)").option("--state <state>", "Filter by state (open/closed)", "open").action(async (options) => {
336
+ try {
337
+ const [ownerIdentifier, name] = parseRepoArg2(options.repo);
338
+ const issuesList = await apiGet(
339
+ `/api/repositories/${ownerIdentifier}/${name}/issues?state=${options.state}`
340
+ );
341
+ if (issuesList.length === 0) {
342
+ console.log(chalk3.yellow(`No ${options.state} issues found`));
343
+ return;
344
+ }
345
+ console.log(
346
+ chalk3.bold(`Issues in @${ownerIdentifier}/${name} (${issuesList.length})`)
347
+ );
348
+ console.log();
349
+ for (const issue of issuesList) {
350
+ const stateIcon = issue.state === "open" ? "\u{1F7E2}" : "\u26AA";
351
+ console.log(
352
+ `${stateIcon} #${issue.number} ${chalk3.cyan(issue.title)}`
353
+ );
354
+ console.log(
355
+ chalk3.dim(` by @${issue.author.username} \xB7 ${new Date(issue.createdAt).toLocaleDateString()}`)
356
+ );
357
+ }
358
+ } catch (error) {
359
+ console.error(chalk3.red(`Error: ${error.message}`));
360
+ process.exit(1);
361
+ }
362
+ });
363
+ issues.command("view").description("View issue details").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "Issue number").action(async (options) => {
364
+ try {
365
+ const [ownerIdentifier, name] = parseRepoArg2(options.repo);
366
+ const issue = await apiGet(
367
+ `/api/repositories/${ownerIdentifier}/${name}/issues/${options.number}`
368
+ );
369
+ const stateIcon = issue.state === "open" ? "\u{1F7E2}" : "\u26AA";
370
+ console.log(
371
+ `${stateIcon} ${chalk3.bold(`#${issue.number} ${issue.title}`)}`
372
+ );
373
+ console.log(
374
+ chalk3.dim(
375
+ `by @${issue.author.username} \xB7 ${new Date(issue.createdAt).toLocaleDateString()}`
376
+ )
377
+ );
378
+ console.log();
379
+ if (issue.body) {
380
+ console.log(issue.body);
381
+ console.log();
382
+ }
383
+ console.log(chalk3.dim(`State: ${issue.state}`));
384
+ if (issue.closedAt) {
385
+ console.log(
386
+ chalk3.dim(`Closed: ${new Date(issue.closedAt).toLocaleDateString()}`)
387
+ );
388
+ }
389
+ } catch (error) {
390
+ console.error(chalk3.red(`Error: ${error.message}`));
391
+ process.exit(1);
392
+ }
393
+ });
394
+ issues.command("create").description("Create a new issue").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--title <title>", "Issue title").option("--body <body>", "Issue description").action(async (options) => {
395
+ try {
396
+ const [ownerIdentifier, name] = parseRepoArg2(options.repo);
397
+ const issue = await apiPost(
398
+ `/api/repositories/${ownerIdentifier}/${name}/issues`,
399
+ {
400
+ title: options.title,
401
+ body: options.body || ""
402
+ }
403
+ );
404
+ console.log(chalk3.green("\u2713 Issue created successfully"));
405
+ console.log(chalk3.dim(` #${issue.number} ${issue.title}`));
406
+ console.log(
407
+ chalk3.dim(` @${ownerIdentifier}/${name}#${issue.number}`)
408
+ );
409
+ } catch (error) {
410
+ console.error(chalk3.red(`Error: ${error.message}`));
411
+ process.exit(1);
412
+ }
413
+ });
414
+ issues.command("close").description("Close an issue").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "Issue number").action(async (options) => {
415
+ try {
416
+ const [ownerIdentifier, name] = parseRepoArg2(options.repo);
417
+ await apiPatch(
418
+ `/api/repositories/${ownerIdentifier}/${name}/issues/${options.number}`,
419
+ {
420
+ state: "closed"
421
+ }
422
+ );
423
+ console.log(chalk3.green("\u2713 Issue closed successfully"));
424
+ } catch (error) {
425
+ console.error(chalk3.red(`Error: ${error.message}`));
426
+ process.exit(1);
427
+ }
428
+ });
429
+ issues.command("comment").description("Add a comment to an issue").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "Issue number").requiredOption("--body <body>", "Comment text").action(async (options) => {
430
+ try {
431
+ const [ownerIdentifier, name] = parseRepoArg2(options.repo);
432
+ await apiPost(
433
+ `/api/repositories/${ownerIdentifier}/${name}/issues/${options.number}/comments`,
434
+ {
435
+ body: options.body
436
+ }
437
+ );
438
+ console.log(chalk3.green("\u2713 Comment added successfully"));
439
+ } catch (error) {
440
+ console.error(chalk3.red(`Error: ${error.message}`));
441
+ process.exit(1);
442
+ }
443
+ });
444
+ return issues;
445
+ }
446
+ function parseRepoArg2(arg) {
447
+ const match = arg.match(/^@?([^/]+)\/(.+)$/);
448
+ if (!match) {
449
+ throw new Error(
450
+ `Invalid repository format: ${arg}. Expected format: @owner/name or owner/name`
451
+ );
452
+ }
453
+ return [match[1], match[2]];
454
+ }
455
+
456
+ // src/commands/prs.ts
457
+ import { Command as Command4 } from "commander";
458
+ import chalk4 from "chalk";
459
+ function createPrsCommand() {
460
+ const prs = new Command4("prs").alias("pr").description("Manage pull requests");
461
+ prs.command("list").description("List pull requests in a repository").requiredOption("--repo <repo>", "Repository (@owner/name)").option("--state <state>", "Filter by state (open/closed/merged)", "open").action(async (options) => {
462
+ try {
463
+ const [ownerIdentifier, name] = parseRepoArg3(options.repo);
464
+ const prsList = await apiGet(
465
+ `/api/repositories/${ownerIdentifier}/${name}/pull-requests?state=${options.state}`
466
+ );
467
+ if (prsList.length === 0) {
468
+ console.log(chalk4.yellow(`No ${options.state} pull requests found`));
469
+ return;
470
+ }
471
+ console.log(
472
+ chalk4.bold(
473
+ `Pull Requests in @${ownerIdentifier}/${name} (${prsList.length})`
474
+ )
475
+ );
476
+ console.log();
477
+ for (const pr of prsList) {
478
+ const stateIcon = pr.state === "open" ? "\u{1F7E2}" : pr.state === "merged" ? "\u{1F7E3}" : "\u26AA";
479
+ console.log(`${stateIcon} #${pr.number} ${chalk4.cyan(pr.title)}`);
480
+ console.log(
481
+ chalk4.dim(
482
+ ` ${pr.headBranch} \u2192 ${pr.baseBranch} by @${pr.author.username} \xB7 ${new Date(pr.createdAt).toLocaleDateString()}`
483
+ )
484
+ );
485
+ }
486
+ } catch (error) {
487
+ console.error(chalk4.red(`Error: ${error.message}`));
488
+ process.exit(1);
489
+ }
490
+ });
491
+ prs.command("view").description("View pull request details").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "PR number").action(async (options) => {
492
+ try {
493
+ const [ownerIdentifier, name] = parseRepoArg3(options.repo);
494
+ const pr = await apiGet(
495
+ `/api/repositories/${ownerIdentifier}/${name}/pull-requests/${options.number}`
496
+ );
497
+ const stateIcon = pr.state === "open" ? "\u{1F7E2}" : pr.state === "merged" ? "\u{1F7E3}" : "\u26AA";
498
+ console.log(`${stateIcon} ${chalk4.bold(`#${pr.number} ${pr.title}`)}`);
499
+ console.log(
500
+ chalk4.dim(
501
+ `${pr.headBranch} \u2192 ${pr.baseBranch} by @${pr.author.username} \xB7 ${new Date(pr.createdAt).toLocaleDateString()}`
502
+ )
503
+ );
504
+ console.log();
505
+ if (pr.body) {
506
+ console.log(pr.body);
507
+ console.log();
508
+ }
509
+ console.log(chalk4.dim(`State: ${pr.state}`));
510
+ if (pr.mergedAt) {
511
+ console.log(
512
+ chalk4.dim(`Merged: ${new Date(pr.mergedAt).toLocaleDateString()}`)
513
+ );
514
+ }
515
+ if (pr.closedAt) {
516
+ console.log(
517
+ chalk4.dim(`Closed: ${new Date(pr.closedAt).toLocaleDateString()}`)
518
+ );
519
+ }
520
+ } catch (error) {
521
+ console.error(chalk4.red(`Error: ${error.message}`));
522
+ process.exit(1);
523
+ }
524
+ });
525
+ prs.command("create").description("Create a new pull request").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--head <branch>", "Head branch (source)").requiredOption("--base <branch>", "Base branch (target)").requiredOption("--title <title>", "PR title").option("--body <body>", "PR description").action(async (options) => {
526
+ try {
527
+ const [ownerIdentifier, name] = parseRepoArg3(options.repo);
528
+ const pr = await apiPost(
529
+ `/api/repositories/${ownerIdentifier}/${name}/pull-requests`,
530
+ {
531
+ headBranch: options.head,
532
+ baseBranch: options.base,
533
+ title: options.title,
534
+ body: options.body || ""
535
+ }
536
+ );
537
+ console.log(chalk4.green("\u2713 Pull request created successfully"));
538
+ console.log(chalk4.dim(` #${pr.number} ${pr.title}`));
539
+ console.log(
540
+ chalk4.dim(` @${ownerIdentifier}/${name}#${pr.number}`)
541
+ );
542
+ } catch (error) {
543
+ console.error(chalk4.red(`Error: ${error.message}`));
544
+ process.exit(1);
545
+ }
546
+ });
547
+ prs.command("merge").description("Merge a pull request").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "PR number").option("--method <method>", "Merge method (merge/squash/rebase)", "merge").action(async (options) => {
548
+ try {
549
+ const [ownerIdentifier, name] = parseRepoArg3(options.repo);
550
+ await apiPost(
551
+ `/api/repositories/${ownerIdentifier}/${name}/pull-requests/${options.number}/merge`,
552
+ {
553
+ mergeMethod: options.method
554
+ }
555
+ );
556
+ console.log(chalk4.green("\u2713 Pull request merged successfully"));
557
+ } catch (error) {
558
+ console.error(chalk4.red(`Error: ${error.message}`));
559
+ process.exit(1);
560
+ }
561
+ });
562
+ prs.command("close").description("Close a pull request without merging").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "PR number").action(async (options) => {
563
+ try {
564
+ const [ownerIdentifier, name] = parseRepoArg3(options.repo);
565
+ await apiPatch(
566
+ `/api/repositories/${ownerIdentifier}/${name}/pull-requests/${options.number}`,
567
+ {
568
+ state: "closed"
569
+ }
570
+ );
571
+ console.log(chalk4.green("\u2713 Pull request closed successfully"));
572
+ } catch (error) {
573
+ console.error(chalk4.red(`Error: ${error.message}`));
574
+ process.exit(1);
575
+ }
576
+ });
577
+ prs.command("comment").description("Add a comment to a pull request").requiredOption("--repo <repo>", "Repository (@owner/name)").requiredOption("--number <number>", "PR number").requiredOption("--body <body>", "Comment text").action(async (options) => {
578
+ try {
579
+ const [ownerIdentifier, name] = parseRepoArg3(options.repo);
580
+ await apiPost(
581
+ `/api/repositories/${ownerIdentifier}/${name}/pull-requests/${options.number}/comments`,
582
+ {
583
+ body: options.body
584
+ }
585
+ );
586
+ console.log(chalk4.green("\u2713 Comment added successfully"));
587
+ } catch (error) {
588
+ console.error(chalk4.red(`Error: ${error.message}`));
589
+ process.exit(1);
590
+ }
591
+ });
592
+ return prs;
593
+ }
594
+ function parseRepoArg3(arg) {
595
+ const match = arg.match(/^@?([^/]+)\/(.+)$/);
596
+ if (!match) {
597
+ throw new Error(
598
+ `Invalid repository format: ${arg}. Expected format: @owner/name or owner/name`
599
+ );
600
+ }
601
+ return [match[1], match[2]];
602
+ }
603
+
604
+ // src/index.ts
605
+ var program = new Command5();
606
+ program.name("forge").description("CLI tool for Kernelius Forge - the agent-native Git platform").version("0.1.0");
607
+ program.addCommand(createAuthCommand());
608
+ program.addCommand(createReposCommand());
609
+ program.addCommand(createIssuesCommand());
610
+ program.addCommand(createPrsCommand());
611
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@kernelius/forge-cli",
3
+ "version": "0.1.0",
4
+ "description": "Command-line tool for Kernelius Forge - the agent-native Git platform",
5
+ "type": "module",
6
+ "bin": {
7
+ "forge": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "SKILL.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsup",
16
+ "dev": "tsup --watch",
17
+ "typecheck": "tsc --noEmit",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "dependencies": {
21
+ "chalk": "^5.3.0",
22
+ "commander": "^12.1.0",
23
+ "ora": "^8.1.1"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^22.10.5",
27
+ "tsup": "^8.3.5",
28
+ "typescript": "^5.7.3"
29
+ },
30
+ "keywords": [
31
+ "forge",
32
+ "cli",
33
+ "git",
34
+ "kernelius",
35
+ "agent",
36
+ "openclaw"
37
+ ],
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/kernelius-hq/forge-cli"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/kernelius-hq/forge-cli/issues"
44
+ },
45
+ "homepage": "https://github.com/kernelius-hq/forge-cli#readme",
46
+ "license": "MIT",
47
+ "author": {
48
+ "name": "Kernelius HQ",
49
+ "url": "https://github.com/kernelius-hq"
50
+ }
51
+ }