@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 +43 -0
- package/README.md +84 -0
- package/bin/hams.js +268 -0
- package/lib/client.js +67 -0
- package/lib/installer.js +51 -0
- package/lib/server.js +72 -0
- package/package.json +29 -0
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;
|
package/lib/installer.js
ADDED
|
@@ -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
|
+
}
|