@hams-ai/cli 1.0.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/API_PATCH.py ADDED
@@ -0,0 +1,43 @@
1
+ """
2
+ PATCH — tambahkan ini ke agent/api.py
3
+ ======================================
4
+
5
+ Tambahkan 3 hal:
6
+ 1. import os, argparse (di bagian atas file)
7
+ 2. Endpoint /health (di dalam FastAPI app)
8
+ 3. Argparse --port (di bagian if __name__ == "__main__")
9
+ """
10
+
11
+ import os
12
+ import argparse
13
+
14
+ # ── 1. Tambahkan endpoint ini ke FastAPI app kamu ────────────────────────────
15
+ @app.get("/health")
16
+ def health():
17
+ return {"status": "ok", "agent": "hams.ai", "version": "1.0.0"}
18
+
19
+
20
+ # ── 2. Ganti bagian if __name__ == "__main__" dengan ini ─────────────────────
21
+ if __name__ == "__main__":
22
+ import uvicorn
23
+
24
+ parser = argparse.ArgumentParser(description="hams.ai API Server")
25
+ parser.add_argument(
26
+ "--port",
27
+ type=int,
28
+ default=int(os.environ.get("AGENT_PORT", 8000)),
29
+ help="Port to run the server on",
30
+ )
31
+ parser.add_argument(
32
+ "--host",
33
+ type=str,
34
+ default="127.0.0.1",
35
+ )
36
+ args = parser.parse_args()
37
+
38
+ uvicorn.run(
39
+ app,
40
+ host=args.host,
41
+ port=args.port,
42
+ log_level="warning", # suppress noise saat dipakai dari CLI
43
+ )
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @hams-ai/cli
2
+
3
+ Official CLI for [hams.ai](https://hams.ai) — AI Coding Agent.
4
+
5
+ ## Install
6
+
7
+ ```powershell
8
+ npm install -g @hams-ai/cli
9
+ ```
10
+
11
+ ## Prerequisites
12
+
13
+ - **Node.js** 16+ — [nodejs.org](https://nodejs.org)
14
+ - **Python** 3.8+ — [python.org](https://python.org)
15
+ - **hams.ai** project folder (the Python agent)
16
+
17
+ ## Setup
18
+
19
+ After install, tell the CLI where your hams.ai project lives:
20
+
21
+ **PowerShell:**
22
+ ```powershell
23
+ $env:HAMS_PATH = "C:\Users\kamu\hams.ai"
24
+ ```
25
+
26
+ **Linux / Mac:**
27
+ ```bash
28
+ export HAMS_PATH="/home/kamu/hams.ai"
29
+ ```
30
+
31
+ To make it permanent, add the line above to your shell profile (`$PROFILE` on PowerShell, `~/.bashrc` or `~/.zshrc` on Linux/Mac).
32
+
33
+ ## Usage
34
+
35
+ ```powershell
36
+ # Interactive chat (default)
37
+ hams
38
+
39
+ # Run a single task
40
+ hams run "buatkan REST API dengan FastAPI untuk CRUD user"
41
+
42
+ # List available tools
43
+ hams tools
44
+
45
+ # Check if backend is running
46
+ hams status
47
+
48
+ # Show Python backend output (debug)
49
+ hams --verbose
50
+
51
+ # Use custom port
52
+ hams --port 9000
53
+ ```
54
+
55
+ ## How it works
56
+
57
+ ```
58
+ hams (CLI)
59
+ └── finds Python on your system
60
+ └── auto-installs requirements.txt (first time)
61
+ └── spawns agent/api.py --port 8000
62
+ └── waits for /health to respond
63
+ └── sends your tasks via POST /run/stream
64
+ └── streams response back to terminal
65
+ └── shuts down Python when you exit
66
+ ```
67
+
68
+ ## Development (local install)
69
+
70
+ ```powershell
71
+ # Di dalam folder cli/
72
+ npm install
73
+ npm link
74
+
75
+ # Sekarang "hams" tersedia di terminal
76
+ hams
77
+ ```
78
+
79
+ ## Publishing
80
+
81
+ ```powershell
82
+ npm login
83
+ npm publish --access public
84
+ ```
package/bin/hams.js ADDED
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * ██╗ ██╗ █████╗ ███╗ ███╗███████╗ █████╗ ██╗
6
+ * ██║ ██║██╔══██╗████╗ ████║██╔════╝ ██╔══██╗██║
7
+ * ███████║███████║██╔████╔██║███████╗ ███████║██║
8
+ * ██╔══██║██╔══██║██║╚██╔╝██║╚════██║ ██╔══██║██║
9
+ * ██║ ██║██║ ██║██║ ╚═╝ ██║███████║ ██║ ██║██║
10
+ * ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝╚═╝
11
+ *
12
+ * hams.ai CLI — @hams-ai/cli
13
+ *
14
+ * Commands:
15
+ * hams → interactive chat (default)
16
+ * hams run "task" → run single task and exit
17
+ * hams tools → list available tools
18
+ * hams status → check backend status
19
+ */
20
+
21
+ const path = require("path");
22
+ const fs = require("fs");
23
+ const { program } = require("commander");
24
+ const chalk = require("chalk");
25
+ const ora = require("ora");
26
+ const inquirer = require("inquirer");
27
+
28
+ const { findPython, findRequirements, isPythonAgentInstalled, installPythonDeps } = require("../lib/installer");
29
+ const { startServer, stopServer } = require("../lib/server");
30
+ const HamsClient = require("../lib/client");
31
+
32
+ // ─── Banner ──────────────────────────────────────────────────────────────────
33
+ const BANNER = `
34
+ ${chalk.bold.white("hams.ai")} ${chalk.dim("— AI Coding Agent")}
35
+ `;
36
+
37
+ // ─── Resolve project root ─────────────────────────────────────────────────────
38
+ function resolveProjectRoot() {
39
+ // 1. Environment variable
40
+ if (process.env.HAMS_PATH && fs.existsSync(process.env.HAMS_PATH)) {
41
+ return process.env.HAMS_PATH;
42
+ }
43
+ // 2. npm global install: .../node_modules/@hams-ai/cli/bin/hams.js → up 5 levels
44
+ const fromBin = path.resolve(__dirname, "..", "..", "..", "..", "..");
45
+ if (fs.existsSync(path.join(fromBin, "agent", "api.py"))) return fromBin;
46
+
47
+ // 3. Local dev: cli/ is sibling of agent/
48
+ const sibling = path.resolve(__dirname, "..", "..");
49
+ if (fs.existsSync(path.join(sibling, "agent", "api.py"))) return sibling;
50
+
51
+ // 4. Current working directory
52
+ if (fs.existsSync(path.join(process.cwd(), "agent", "api.py"))) return process.cwd();
53
+
54
+ return null;
55
+ }
56
+
57
+ // ─── Setup: find Python, install deps, start server ──────────────────────────
58
+ async function setup(verbose = false, port = 8000) {
59
+ console.log(BANNER);
60
+ const spinner = ora({ text: "Starting hams.ai...", color: "white" }).start();
61
+
62
+ // 1. Find Python
63
+ const pythonCmd = findPython();
64
+ if (!pythonCmd) {
65
+ spinner.fail(
66
+ chalk.red("Python not found.\n") +
67
+ chalk.dim(" Install Python 3.8+ from https://python.org and add it to PATH.")
68
+ );
69
+ process.exit(1);
70
+ }
71
+
72
+ // 2. Find project root
73
+ const projectRoot = resolveProjectRoot();
74
+ if (!projectRoot) {
75
+ spinner.fail(
76
+ chalk.red("Cannot find hams.ai project folder.\n\n") +
77
+ chalk.white(" Set the HAMS_PATH environment variable:\n") +
78
+ chalk.dim(" PowerShell: ") + chalk.cyan(`$env:HAMS_PATH = "C:\\path\\to\\hams.ai"\n`) +
79
+ chalk.dim(" Linux/Mac: ") + chalk.cyan(`export HAMS_PATH="/path/to/hams.ai"`)
80
+ );
81
+ process.exit(1);
82
+ }
83
+
84
+ spinner.text = "Checking Python dependencies...";
85
+
86
+ // 3. Auto-install deps if needed
87
+ if (!isPythonAgentInstalled(pythonCmd)) {
88
+ const reqFile = findRequirements(projectRoot);
89
+ if (reqFile) {
90
+ spinner.text = "Installing Python dependencies (first time setup)...";
91
+ try {
92
+ installPythonDeps(pythonCmd, reqFile);
93
+ } catch (err) {
94
+ spinner.warn(chalk.yellow(`Could not auto-install deps: ${err.message}`));
95
+ spinner.warn(chalk.dim(`Run manually: pip install -r ${reqFile}`));
96
+ }
97
+ }
98
+ }
99
+
100
+ // 4. Start Python backend
101
+ spinner.text = "Starting backend...";
102
+ try {
103
+ const { process: serverProc } = await startServer({ pythonCmd, projectRoot, port, verbose });
104
+ spinner.succeed(chalk.green(`hams.ai ready`) + chalk.dim(` — port ${port}`));
105
+ return { serverProc, port };
106
+ } catch (err) {
107
+ spinner.fail(chalk.red(`Backend failed to start: ${err.message}`));
108
+ if (!verbose) console.log(chalk.dim(" Tip: run with --verbose to see Python output"));
109
+ process.exit(1);
110
+ }
111
+ }
112
+
113
+ // ─── Print result ─────────────────────────────────────────────────────────────
114
+ function printResult(text) {
115
+ process.stdout.write("\n");
116
+ }
117
+
118
+ // ─── Interactive REPL ─────────────────────────────────────────────────────────
119
+ async function interactiveMode(client) {
120
+ console.log(chalk.dim(' Type your task and press Enter. Type "exit" to quit.\n'));
121
+
122
+ process.on("SIGINT", () => {
123
+ console.log(chalk.dim("\n\n Goodbye.\n"));
124
+ process.exit(0);
125
+ });
126
+
127
+ while (true) {
128
+ const { task } = await inquirer.prompt([
129
+ {
130
+ type: "input",
131
+ name: "task",
132
+ message: chalk.bold.white("›"),
133
+ validate: (v) => v.trim().length > 0 || "Please enter a task.",
134
+ },
135
+ ]);
136
+
137
+ const trimmed = task.trim();
138
+ if (["exit", "quit", "q"].includes(trimmed.toLowerCase())) {
139
+ console.log(chalk.dim("\n Goodbye.\n"));
140
+ process.exit(0);
141
+ }
142
+
143
+ const spinner = ora({ text: chalk.dim("Thinking..."), color: "white" }).start();
144
+ try {
145
+ let output = "";
146
+ let started = false;
147
+ await client.streamTask(trimmed, (chunk) => {
148
+ if (!started) {
149
+ spinner.stop();
150
+ process.stdout.write("\n");
151
+ started = true;
152
+ }
153
+ process.stdout.write(chunk);
154
+ output += chunk;
155
+ });
156
+ if (!started) {
157
+ spinner.stop();
158
+ process.stdout.write("\n" + output);
159
+ }
160
+ process.stdout.write("\n\n");
161
+ } catch (err) {
162
+ spinner.fail(chalk.red(err.message));
163
+ }
164
+ }
165
+ }
166
+
167
+ // ─── Commands ─────────────────────────────────────────────────────────────────
168
+ program
169
+ .name("hams")
170
+ .description("hams.ai — AI Coding Agent CLI")
171
+ .version("1.0.0", "-v, --version")
172
+ .option("--verbose", "Show Python backend output")
173
+ .option("--port <port>", "Backend port", "8000");
174
+
175
+ // Default: interactive chat
176
+ program
177
+ .command("chat", { isDefault: true })
178
+ .description("Start interactive chat (default)")
179
+ .action(async () => {
180
+ const opts = program.opts();
181
+ const port = parseInt(opts.port);
182
+ const { serverProc } = await setup(opts.verbose, port);
183
+ const client = new HamsClient(port);
184
+
185
+ process.on("exit", () => stopServer(serverProc));
186
+ process.on("SIGINT", () => { stopServer(serverProc); process.exit(0); });
187
+
188
+ await interactiveMode(client);
189
+ });
190
+
191
+ // One-shot run
192
+ program
193
+ .command("run <task>")
194
+ .description("Run a single task and exit")
195
+ .action(async (task) => {
196
+ const opts = program.opts();
197
+ const port = parseInt(opts.port);
198
+ const { serverProc } = await setup(opts.verbose, port);
199
+ const client = new HamsClient(port);
200
+
201
+ process.on("exit", () => stopServer(serverProc));
202
+
203
+ const spinner = ora({ text: chalk.dim("Running..."), color: "white" }).start();
204
+ try {
205
+ let started = false;
206
+ await client.streamTask(task, (chunk) => {
207
+ if (!started) { spinner.stop(); process.stdout.write("\n"); started = true; }
208
+ process.stdout.write(chunk);
209
+ });
210
+ process.stdout.write("\n\n");
211
+ stopServer(serverProc);
212
+ process.exit(0);
213
+ } catch (err) {
214
+ spinner.fail(chalk.red(err.message));
215
+ stopServer(serverProc);
216
+ process.exit(1);
217
+ }
218
+ });
219
+
220
+ // List tools
221
+ program
222
+ .command("tools")
223
+ .description("List available agent tools")
224
+ .action(async () => {
225
+ const opts = program.opts();
226
+ const port = parseInt(opts.port);
227
+ const { serverProc } = await setup(opts.verbose, port);
228
+ const client = new HamsClient(port);
229
+
230
+ try {
231
+ const tools = await client.tools();
232
+ console.log(chalk.bold("\n Tools available in hams.ai:\n"));
233
+ if (Array.isArray(tools) && tools.length) {
234
+ tools.forEach((t) => {
235
+ const name = t.name || t;
236
+ const desc = t.description || "";
237
+ console.log(` ${chalk.white("•")} ${chalk.bold(name)} ${chalk.dim(desc)}`);
238
+ });
239
+ } else {
240
+ console.log(chalk.dim(" (no tools info returned by API)"));
241
+ }
242
+ console.log();
243
+ } catch (err) {
244
+ console.error(chalk.red(`Could not fetch tools: ${err.message}`));
245
+ }
246
+ stopServer(serverProc);
247
+ process.exit(0);
248
+ });
249
+
250
+ // Status check (no server spawn — just check existing)
251
+ program
252
+ .command("status")
253
+ .description("Check if backend is already running")
254
+ .action(async () => {
255
+ const opts = program.opts();
256
+ const port = parseInt(opts.port);
257
+ const client = new HamsClient(port);
258
+ try {
259
+ const h = await client.health();
260
+ console.log(chalk.green(`\n hams.ai backend is running`) + chalk.dim(` on port ${port}`));
261
+ console.log(chalk.dim(` ${JSON.stringify(h)}\n`));
262
+ } catch (_) {
263
+ console.log(chalk.dim(`\n No backend found on port ${port}.\n`));
264
+ }
265
+ process.exit(0);
266
+ });
267
+
268
+ program.parse(process.argv);
package/lib/client.js ADDED
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+
3
+ const axios = require("axios");
4
+
5
+ class HamsClient {
6
+ constructor(port = 8000) {
7
+ this.base = `http://127.0.0.1:${port}`;
8
+ this.http = axios.create({
9
+ baseURL: this.base,
10
+ timeout: 120_000,
11
+ });
12
+ }
13
+
14
+ async runTask(task) {
15
+ const resp = await this.http.post("/run", { task });
16
+ return resp.data;
17
+ }
18
+
19
+ async streamTask(task, onChunk) {
20
+ try {
21
+ const resp = await this.http.post(
22
+ "/run/stream",
23
+ { task },
24
+ { responseType: "stream" }
25
+ );
26
+ return new Promise((resolve, reject) => {
27
+ let buffer = "";
28
+ resp.data.on("data", (chunk) => {
29
+ buffer += chunk.toString();
30
+ const lines = buffer.split("\n");
31
+ buffer = lines.pop();
32
+ for (const line of lines) {
33
+ if (line.startsWith("data: ")) {
34
+ try {
35
+ const payload = JSON.parse(line.slice(6));
36
+ onChunk(payload.text || payload.content || "");
37
+ } catch (_) {
38
+ onChunk(line.slice(6));
39
+ }
40
+ }
41
+ }
42
+ });
43
+ resp.data.on("end", resolve);
44
+ resp.data.on("error", reject);
45
+ });
46
+ } catch (err) {
47
+ const result = await this.runTask(task);
48
+ onChunk(result.result || result.output || JSON.stringify(result));
49
+ }
50
+ }
51
+
52
+ async health() {
53
+ const resp = await this.http.get("/health");
54
+ return resp.data;
55
+ }
56
+
57
+ async tools() {
58
+ try {
59
+ const resp = await this.http.get("/tools");
60
+ return resp.data;
61
+ } catch (_) {
62
+ return [];
63
+ }
64
+ }
65
+ }
66
+
67
+ module.exports = HamsClient;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+
3
+ const { spawnSync } = require("child_process");
4
+ const path = require("path");
5
+ const fs = require("fs");
6
+ const which = require("which");
7
+
8
+ function findPython() {
9
+ for (const cmd of ["python3", "python"]) {
10
+ try {
11
+ which.sync(cmd);
12
+ const result = spawnSync(cmd, ["--version"], { encoding: "utf8" });
13
+ if (result.status === 0) return cmd;
14
+ } catch (_) {}
15
+ }
16
+ return null;
17
+ }
18
+
19
+ function findRequirements(projectRoot) {
20
+ const candidates = [
21
+ path.join(projectRoot, "requirements.txt"),
22
+ path.join(projectRoot, "..", "requirements.txt"),
23
+ ];
24
+ for (const c of candidates) {
25
+ if (fs.existsSync(c)) return c;
26
+ }
27
+ return null;
28
+ }
29
+
30
+ function isPythonAgentInstalled(pythonCmd) {
31
+ const result = spawnSync(
32
+ pythonCmd,
33
+ ["-c", "import agent; print('ok')"],
34
+ { encoding: "utf8" }
35
+ );
36
+ return result.status === 0;
37
+ }
38
+
39
+ function installPythonDeps(pythonCmd, requirementsPath) {
40
+ const result = spawnSync(
41
+ pythonCmd,
42
+ ["-m", "pip", "install", "-r", requirementsPath, "--quiet"],
43
+ { encoding: "utf8", stdio: "pipe" }
44
+ );
45
+ if (result.status !== 0) {
46
+ throw new Error(`pip install failed:\n${result.stderr || result.stdout}`);
47
+ }
48
+ return true;
49
+ }
50
+
51
+ module.exports = { findPython, findRequirements, isPythonAgentInstalled, installPythonDeps };
package/lib/server.js ADDED
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+
3
+ const { spawn } = require("child_process");
4
+ const path = require("path");
5
+ const http = require("http");
6
+ const fs = require("fs");
7
+
8
+ const DEFAULT_PORT = 8000;
9
+ const STARTUP_TIMEOUT_MS = 30_000;
10
+
11
+ function waitForServer(port, timeoutMs) {
12
+ return new Promise((resolve, reject) => {
13
+ const deadline = Date.now() + timeoutMs;
14
+
15
+ function poll() {
16
+ if (Date.now() > deadline) {
17
+ return reject(new Error(`hams.ai backend did not start within ${timeoutMs / 1000}s`));
18
+ }
19
+ const req = http.get(`http://127.0.0.1:${port}/health`, (res) => {
20
+ if (res.statusCode < 500) resolve();
21
+ else setTimeout(poll, 500);
22
+ });
23
+ req.on("error", () => setTimeout(poll, 500));
24
+ req.setTimeout(1000, () => { req.destroy(); setTimeout(poll, 500); });
25
+ }
26
+
27
+ poll();
28
+ });
29
+ }
30
+
31
+ async function startServer({ pythonCmd, projectRoot, port = DEFAULT_PORT, verbose = false }) {
32
+ const apiEntry = path.join(projectRoot, "agent", "api.py");
33
+ const mainEntry = path.join(projectRoot, "agent", "main.py");
34
+ const entry = fs.existsSync(apiEntry) ? apiEntry : mainEntry;
35
+
36
+ const env = {
37
+ ...process.env,
38
+ AGENT_PORT: String(port),
39
+ PYTHONPATH: projectRoot,
40
+ };
41
+
42
+ const child = spawn(pythonCmd, [entry, "--port", String(port)], {
43
+ cwd: projectRoot,
44
+ env,
45
+ stdio: verbose ? "inherit" : "pipe",
46
+ });
47
+
48
+ if (!verbose) {
49
+ let stderr = "";
50
+ child.stderr && child.stderr.on("data", (d) => { stderr += d.toString(); });
51
+ child.on("exit", (code) => {
52
+ if (code !== 0 && code !== null) {
53
+ stderr && process.stderr.write(`\n[hams.ai] ${stderr}\n`);
54
+ }
55
+ });
56
+ }
57
+
58
+ child.on("error", (err) => {
59
+ throw new Error(`Failed to start hams.ai backend: ${err.message}`);
60
+ });
61
+
62
+ await waitForServer(port, STARTUP_TIMEOUT_MS);
63
+ return { process: child, port };
64
+ }
65
+
66
+ function stopServer(child) {
67
+ if (!child || child.killed) return;
68
+ child.kill("SIGTERM");
69
+ setTimeout(() => { if (!child.killed) child.kill("SIGKILL"); }, 5000);
70
+ }
71
+
72
+ module.exports = { startServer, stopServer };
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@hams-ai/cli",
3
+ "version": "1.0.0",
4
+ "description": "hams.ai — Official CLI for the hams.ai coding agent",
5
+ "bin": {
6
+ "hams": "./bin/hams.js"
7
+ },
8
+ "main": "./bin/hams.js",
9
+ "scripts": {
10
+ "start": "node bin/hams.js"
11
+ },
12
+ "keywords": ["hams", "hams-ai", "ai", "agent", "cli", "coding"],
13
+ "author": "hams.ai",
14
+ "license": "MIT",
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "dependencies": {
19
+ "axios": "^1.6.0",
20
+ "chalk": "^4.1.2",
21
+ "commander": "^11.1.0",
22
+ "inquirer": "^8.2.6",
23
+ "ora": "^5.4.1",
24
+ "which": "^3.0.1"
25
+ },
26
+ "engines": {
27
+ "node": ">=16.0.0"
28
+ }
29
+ }