@hardfin/cli 0.0.2-dev.10 → 0.0.2-dev.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/cli.js +188 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -185,6 +185,42 @@ environment, so `hardfin login` works out of the box.
|
|
|
185
185
|
| `clientId`, or `HARDFIN_CLIENT_ID` | the client this CLI names itself as | the seeded Hardfin CLI client |
|
|
186
186
|
| `issuerUrl`, or `HARDFIN_ISSUER_URL` | the authorization server | the API's host root |
|
|
187
187
|
|
|
188
|
+
## Serving an agent
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
hardfin mcp
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
It speaks the Model Context Protocol on standard input and output, so Claude, Codex or any
|
|
195
|
+
other client can run Hardfin commands.
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
claude mcp add hardfin -- hardfin mcp
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### One tool, and resources for the rest
|
|
202
|
+
|
|
203
|
+
Every tool a server lists sits in the agent's context for the entire session, so this server
|
|
204
|
+
offers one.
|
|
205
|
+
|
|
206
|
+
| Offered as | Holds | Costs context |
|
|
207
|
+
| --- | --- | --- |
|
|
208
|
+
| The `hardfin` tool | A list of arguments, such as `["asset", "list", "--limit", "5"]` | Always, and it is about 400 bytes |
|
|
209
|
+
| `hardfin://guide` | Every command, its flags, and the exit codes | Only when the agent reads it |
|
|
210
|
+
| `hardfin://commands` | The command tree as JSON | Only when the agent reads it |
|
|
211
|
+
|
|
212
|
+
Fifty tools would describe the same surface and crowd out the work. An agent that needs the
|
|
213
|
+
list reads a resource, or runs `["--help"]`.
|
|
214
|
+
|
|
215
|
+
A tool call runs the CLI itself, so an agent and a person get identical parsing, output and
|
|
216
|
+
exit codes.
|
|
217
|
+
|
|
218
|
+
### What it signs in as
|
|
219
|
+
|
|
220
|
+
The server uses whatever credential this machine holds, so an agent acts as the person who
|
|
221
|
+
ran `hardfin login`. Give an agent an API key through `HARDFIN_API_KEY` when it should act
|
|
222
|
+
as an integration instead.
|
|
223
|
+
|
|
188
224
|
## Diagnosing a problem
|
|
189
225
|
|
|
190
226
|
`hardfin status` prints everything a support request needs, and is the first thing to send.
|
package/dist/cli.js
CHANGED
|
@@ -5,9 +5,10 @@ import { z } from "zod";
|
|
|
5
5
|
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
7
|
import { arch, cpus, homedir, release, totalmem, type, version } from "node:os";
|
|
8
|
-
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
10
|
import { createHash, randomBytes } from "node:crypto";
|
|
11
|
+
import { createInterface } from "node:readline";
|
|
11
12
|
//#region src/command/registry.ts
|
|
12
13
|
/** ExitCode is what the process returns, and what an agent branches on. */
|
|
13
14
|
const ExitCode = {
|
|
@@ -1310,6 +1311,190 @@ async function runLogout(input) {
|
|
|
1310
1311
|
return ExitCode.OK;
|
|
1311
1312
|
}
|
|
1312
1313
|
//#endregion
|
|
1314
|
+
//#region src/mcp/server.ts
|
|
1315
|
+
const PROTOCOL_VERSION = "2025-06-18";
|
|
1316
|
+
const GUIDE_URI = "hardfin://guide";
|
|
1317
|
+
const COMMANDS_URI = "hardfin://commands";
|
|
1318
|
+
/**
|
|
1319
|
+
* One tool, because every tool a server lists sits in the agent's context for the whole
|
|
1320
|
+
* session. The commands themselves are resources, which cost nothing until one is read.
|
|
1321
|
+
*/
|
|
1322
|
+
const TOOL = {
|
|
1323
|
+
name: "hardfin",
|
|
1324
|
+
description: "Run a Hardfin CLI command against the Hardfin API. Pass the arguments as a list, such as [\"asset\", \"list\", \"--limit\", \"5\"]. Run [\"--help\"] for the commands, or read the hardfin://guide resource.",
|
|
1325
|
+
inputSchema: {
|
|
1326
|
+
type: "object",
|
|
1327
|
+
properties: { args: {
|
|
1328
|
+
type: "array",
|
|
1329
|
+
items: { type: "string" },
|
|
1330
|
+
description: "The arguments to hardfin, without the program name"
|
|
1331
|
+
} },
|
|
1332
|
+
required: ["args"]
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
/** toResponse answers one request, and answers nothing to a notification. */
|
|
1336
|
+
async function toResponse(request, commands, run) {
|
|
1337
|
+
const answer = (result) => ({
|
|
1338
|
+
jsonrpc: "2.0",
|
|
1339
|
+
id: request.id ?? null,
|
|
1340
|
+
result
|
|
1341
|
+
});
|
|
1342
|
+
switch (request.method) {
|
|
1343
|
+
case "initialize": return answer({
|
|
1344
|
+
protocolVersion: toProtocolVersion(request.params),
|
|
1345
|
+
capabilities: {
|
|
1346
|
+
tools: {},
|
|
1347
|
+
resources: {}
|
|
1348
|
+
},
|
|
1349
|
+
serverInfo: {
|
|
1350
|
+
name: "hardfin",
|
|
1351
|
+
version: version$1
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
case "tools/list": return answer({ tools: [TOOL] });
|
|
1355
|
+
case "resources/list": return answer({ resources: [{
|
|
1356
|
+
uri: GUIDE_URI,
|
|
1357
|
+
name: "Hardfin CLI guide",
|
|
1358
|
+
description: "Every command, its flags, and the exit codes",
|
|
1359
|
+
mimeType: "text/markdown"
|
|
1360
|
+
}, {
|
|
1361
|
+
uri: COMMANDS_URI,
|
|
1362
|
+
name: "Hardfin CLI commands",
|
|
1363
|
+
description: "The command tree as JSON",
|
|
1364
|
+
mimeType: "application/json"
|
|
1365
|
+
}] });
|
|
1366
|
+
case "resources/read": return answer(toResource(String(request.params?.["uri"] ?? ""), commands));
|
|
1367
|
+
case "tools/call": return answer(await toToolResult(request.params, run));
|
|
1368
|
+
case "ping": return answer({});
|
|
1369
|
+
default:
|
|
1370
|
+
if (request.id === void 0 || request.id === null) return;
|
|
1371
|
+
return {
|
|
1372
|
+
jsonrpc: "2.0",
|
|
1373
|
+
id: request.id,
|
|
1374
|
+
error: {
|
|
1375
|
+
code: -32601,
|
|
1376
|
+
message: `${request.method} is not a method this server offers`
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
function toProtocolVersion(params) {
|
|
1382
|
+
const asked = params?.["protocolVersion"];
|
|
1383
|
+
return typeof asked === "string" ? asked : PROTOCOL_VERSION;
|
|
1384
|
+
}
|
|
1385
|
+
function toResource(uri, commands) {
|
|
1386
|
+
if (uri === GUIDE_URI) return { contents: [{
|
|
1387
|
+
uri,
|
|
1388
|
+
mimeType: "text/markdown",
|
|
1389
|
+
text: toGuide(commands, version$1)
|
|
1390
|
+
}] };
|
|
1391
|
+
if (uri === COMMANDS_URI) return { contents: [{
|
|
1392
|
+
uri,
|
|
1393
|
+
mimeType: "application/json",
|
|
1394
|
+
text: JSON.stringify(toTree(commands), null, 2)
|
|
1395
|
+
}] };
|
|
1396
|
+
return {
|
|
1397
|
+
contents: [],
|
|
1398
|
+
isError: true
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
/** toTree names every command and what it takes, without the schemas a tool list would carry. */
|
|
1402
|
+
function toTree(commands) {
|
|
1403
|
+
return commands.map((command) => ({
|
|
1404
|
+
name: command.name,
|
|
1405
|
+
summary: command.summary,
|
|
1406
|
+
arguments: command.arguments.map((argument) => argument.name),
|
|
1407
|
+
flags: command.flags.map((flag) => flag.valueName ? `--${flag.name} <${flag.valueName}>` : `--${flag.name}`),
|
|
1408
|
+
subcommands: command.subcommands ? toTree(command.subcommands) : void 0
|
|
1409
|
+
}));
|
|
1410
|
+
}
|
|
1411
|
+
async function toToolResult(params, run) {
|
|
1412
|
+
const args = (params?.["arguments"])?.args;
|
|
1413
|
+
if (!Array.isArray(args) || args.some((entry) => typeof entry !== "string")) return {
|
|
1414
|
+
content: [{
|
|
1415
|
+
type: "text",
|
|
1416
|
+
text: "args must be a list of strings, such as [\"asset\", \"list\"]"
|
|
1417
|
+
}],
|
|
1418
|
+
isError: true
|
|
1419
|
+
};
|
|
1420
|
+
const outcome = await run(args);
|
|
1421
|
+
return {
|
|
1422
|
+
content: [{
|
|
1423
|
+
type: "text",
|
|
1424
|
+
text: [outcome.stdout, outcome.stderr].filter((part) => part.trim() !== "").join("\n") || `hardfin exited ${outcome.code}`
|
|
1425
|
+
}],
|
|
1426
|
+
isError: outcome.code !== 0
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
/** toCliRunner runs the CLI itself, so a tool call parses exactly as a terminal would. */
|
|
1430
|
+
function toCliRunner() {
|
|
1431
|
+
return (args) => new Promise((resolve) => {
|
|
1432
|
+
const child = spawn(process.execPath, [process.argv[1] ?? "", ...args], { env: {
|
|
1433
|
+
...process.env,
|
|
1434
|
+
HARDFIN_NO_BROWSER: "1"
|
|
1435
|
+
} });
|
|
1436
|
+
let stdout = "";
|
|
1437
|
+
let stderr = "";
|
|
1438
|
+
child.stdout.on("data", (chunk) => {
|
|
1439
|
+
stdout += chunk.toString();
|
|
1440
|
+
});
|
|
1441
|
+
child.stderr.on("data", (chunk) => {
|
|
1442
|
+
stderr += chunk.toString();
|
|
1443
|
+
});
|
|
1444
|
+
child.on("close", (code) => resolve({
|
|
1445
|
+
stdout,
|
|
1446
|
+
stderr,
|
|
1447
|
+
code: code ?? 1
|
|
1448
|
+
}));
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
/** serve answers requests on stdin until the client closes it. */
|
|
1452
|
+
async function serve(commands, run = toCliRunner()) {
|
|
1453
|
+
const lines = createInterface({ input: process.stdin });
|
|
1454
|
+
for await (const line of lines) {
|
|
1455
|
+
if (line.trim() === "") continue;
|
|
1456
|
+
let response;
|
|
1457
|
+
try {
|
|
1458
|
+
response = await toResponse(JSON.parse(line), commands, run);
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
response = {
|
|
1461
|
+
jsonrpc: "2.0",
|
|
1462
|
+
id: null,
|
|
1463
|
+
error: {
|
|
1464
|
+
code: -32700,
|
|
1465
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
if (response) process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
//#endregion
|
|
1473
|
+
//#region src/command/mcp.ts
|
|
1474
|
+
const mcpCommand = defineCommand({
|
|
1475
|
+
name: "mcp",
|
|
1476
|
+
summary: "Serve this CLI to an agent over the Model Context Protocol",
|
|
1477
|
+
description: "Speaks the Model Context Protocol on standard input and output. It offers one tool, because every tool an agent is told about occupies its context for the whole session, and serves the commands as resources the agent reads only when it needs them.",
|
|
1478
|
+
arguments: [],
|
|
1479
|
+
flags: [{
|
|
1480
|
+
name: "json",
|
|
1481
|
+
description: "Accepted for consistency, and ignored, because the protocol decides the output",
|
|
1482
|
+
schema: z.boolean()
|
|
1483
|
+
}],
|
|
1484
|
+
examples: [{
|
|
1485
|
+
description: "Register with an agent",
|
|
1486
|
+
command: "hardfin mcp"
|
|
1487
|
+
}, {
|
|
1488
|
+
description: "Add it to Claude Code",
|
|
1489
|
+
command: "claude mcp add hardfin -- hardfin mcp"
|
|
1490
|
+
}],
|
|
1491
|
+
run: runMcp
|
|
1492
|
+
});
|
|
1493
|
+
async function runMcp(input) {
|
|
1494
|
+
await serve(input.commands);
|
|
1495
|
+
return ExitCode.OK;
|
|
1496
|
+
}
|
|
1497
|
+
//#endregion
|
|
1313
1498
|
//#region src/command/operation.ts
|
|
1314
1499
|
const INPUT_FLAG = {
|
|
1315
1500
|
name: "input",
|
|
@@ -2898,7 +3083,8 @@ const commands = [
|
|
|
2898
3083
|
...surfaceCommands,
|
|
2899
3084
|
apiCommand,
|
|
2900
3085
|
configCommand,
|
|
2901
|
-
agentGuideCommand
|
|
3086
|
+
agentGuideCommand,
|
|
3087
|
+
mcpCommand
|
|
2902
3088
|
];
|
|
2903
3089
|
//#endregion
|
|
2904
3090
|
//#region src/command/validate.ts
|