@hardfin/cli 0.1.0-dev.32 → 0.1.0-dev.34
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 -10
- package/dist/cli.js +4 -4
- package/dist/{program-B02z-kNH.js → program-DTsxnjRj.js} +205 -64
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -208,6 +208,21 @@ On WSL the browser is opened through Windows, by `wslview`, then PowerShell, the
|
|
|
208
208
|
The port changes every time, so Hardfin matches a loopback redirect by everything except its
|
|
209
209
|
port, which is what RFC 8252 asks of an authorization server.
|
|
210
210
|
|
|
211
|
+
### When an agent signs you in
|
|
212
|
+
|
|
213
|
+
An agent that runs `hardfin login` reads its stderr, and you only see what the agent passes
|
|
214
|
+
on. So when output is JSON, which it is whenever stdout is not a terminal, the command writes
|
|
215
|
+
one line of JSON on stderr before it starts waiting.
|
|
216
|
+
|
|
217
|
+
| `event` | Also carries | The agent should |
|
|
218
|
+
| ------------- | -------------------------------------------------------------------- | --------------------------------------------------- |
|
|
219
|
+
| `sign_in_url` | `url`, `browserOpened`, `waitSeconds` | Show you `url`, saying whether a browser tab opened |
|
|
220
|
+
| `device_code` | `userCode`, `verificationUri`, `url`, `browserOpened`, `waitSeconds` | Show you the code and where to enter it |
|
|
221
|
+
|
|
222
|
+
The command then waits up to `waitSeconds` for you to approve, so the agent has to let it run
|
|
223
|
+
that long. An agent with no way to open your browser, such as one in a cloud sandbox, signs
|
|
224
|
+
in with `--device`.
|
|
225
|
+
|
|
211
226
|
### Signing in from a machine with no browser
|
|
212
227
|
|
|
213
228
|
`hardfin login --device` prints a short code and a page to enter it on, and waits. Approve
|
|
@@ -305,22 +320,33 @@ other client can run Hardfin commands.
|
|
|
305
320
|
claude mcp add hardfin -- hardfin mcp
|
|
306
321
|
```
|
|
307
322
|
|
|
308
|
-
###
|
|
323
|
+
### Two tools, and resources for the rest
|
|
309
324
|
|
|
310
325
|
Every tool a server lists sits in the agent's context for the entire session, so this server
|
|
311
|
-
offers
|
|
326
|
+
offers two. They differ only in what they may run, which lets a client run a read without
|
|
327
|
+
asking and ask before anything changes.
|
|
312
328
|
|
|
313
|
-
| Offered as | Holds
|
|
314
|
-
| -------------------- |
|
|
315
|
-
|
|
|
316
|
-
| `
|
|
317
|
-
| `hardfin://
|
|
329
|
+
| Offered as | Holds | Costs context |
|
|
330
|
+
| -------------------- | ---------------------------------------------------------------------- | ---------------------------- |
|
|
331
|
+
| `hardfin_read` | A command that only reads, such as `["asset", "list", "--limit", "5"]` | Always, about 800 bytes |
|
|
332
|
+
| `hardfin_write` | A command that creates, changes or deletes, with an optional JSON body | Always, about 800 bytes |
|
|
333
|
+
| `hardfin://guide` | Every command, its flags, the exit codes and the error codes | Only when the agent reads it |
|
|
334
|
+
| `hardfin://commands` | The command tree as JSON | Only when the agent reads it |
|
|
318
335
|
|
|
319
|
-
|
|
320
|
-
|
|
336
|
+
A command is a read when its endpoint's method is GET, or for `hardfin api` when no other
|
|
337
|
+
`--method` is given. Anything else is a write, and `hardfin_read` refuses it by naming
|
|
338
|
+
`hardfin_write`. Both tools carry a title, the read-only or destructive hint, and a link to
|
|
339
|
+
the API reference, which Claude's connector directory requires.
|
|
321
340
|
|
|
322
341
|
A tool call runs the CLI itself, so an agent and a person get identical parsing, output and
|
|
323
|
-
exit codes.
|
|
342
|
+
exit codes. Each call has these limits:
|
|
343
|
+
|
|
344
|
+
| Limit | Value | Past it |
|
|
345
|
+
| ---------------- | --------------------------------------- | ------------------------------------------ |
|
|
346
|
+
| Time | 120 seconds | The command is stopped, and the call fails |
|
|
347
|
+
| Commands running | 4 at once | The next call waits for one to finish |
|
|
348
|
+
| One argument | 128 KiB | Refused, pointing at the `input` argument |
|
|
349
|
+
| Standard input | The call's `input` argument, or nothing | Always closed, so `--input -` never waits |
|
|
324
350
|
|
|
325
351
|
### What it signs in as
|
|
326
352
|
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="f2529e6e-d7d4-5fe2-b460-7e86ca865c25")}catch(e){}}();
|
|
4
4
|
import module from "node:module";
|
|
5
5
|
//#region src/cli.ts
|
|
6
6
|
module.enableCompileCache?.();
|
|
@@ -14,7 +14,7 @@ process.on("unhandledRejection", (error) => void toCrash(error));
|
|
|
14
14
|
try {
|
|
15
15
|
const answer = await toQuickAnswer(process.argv.slice(2));
|
|
16
16
|
if (answer === void 0) {
|
|
17
|
-
const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-
|
|
17
|
+
const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-DTsxnjRj.js");
|
|
18
18
|
try {
|
|
19
19
|
await toCli().parseAsync(process.argv);
|
|
20
20
|
} catch (error) {
|
|
@@ -33,7 +33,7 @@ async function toCrash(error) {
|
|
|
33
33
|
isCrashing = true;
|
|
34
34
|
process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
35
35
|
try {
|
|
36
|
-
const { toReported } = await import("./program-
|
|
36
|
+
const { toReported } = await import("./program-DTsxnjRj.js");
|
|
37
37
|
await toReported(error, false, void 0, "fatal");
|
|
38
38
|
} finally {
|
|
39
39
|
process.exit(1);
|
|
@@ -51,4 +51,4 @@ async function toQuickAnswer(args) {
|
|
|
51
51
|
export {};
|
|
52
52
|
|
|
53
53
|
//# sourceMappingURL=cli.js.map
|
|
54
|
-
//# debugId=
|
|
54
|
+
//# debugId=f2529e6e-d7d4-5fe2-b460-7e86ca865c25
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="c7f8ce05-d638-5d0f-a088-655ddade6f45")}catch(e){}}();
|
|
4
4
|
import { a as defineCommand, i as GLOBAL_FLAGS, r as ExitCode, t as toInvocation } from "./scrub-BN3gyBpR.js";
|
|
5
5
|
import { n as toCompletion, t as toAnswer$1 } from "./completion-tree-rR_Rmeij.js";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
@@ -987,6 +987,7 @@ function toGuide(commands, version) {
|
|
|
987
987
|
"## Before calling",
|
|
988
988
|
"",
|
|
989
989
|
"- Authenticate with `--api-key`, by setting `HARDFIN_API_KEY`, or by running `hardfin login`",
|
|
990
|
+
"- `hardfin login` writes one JSON line on stderr before it waits. Show the person its `url`, or its `userCode` and `verificationUri`, and let the command run for `waitSeconds`",
|
|
990
991
|
"- Every command prints JSON on stdout and diagnostics on stderr",
|
|
991
992
|
"- Output is JSON whenever stdout is not a terminal, so no flag is needed when calling from code",
|
|
992
993
|
"",
|
|
@@ -2154,7 +2155,13 @@ async function runLogin(input) {
|
|
|
2154
2155
|
challenge: pkce.challenge
|
|
2155
2156
|
});
|
|
2156
2157
|
const opened = input.flags["noBrowser"] !== true && !process.env["HARDFIN_NO_BROWSER"] && openBrowser(url);
|
|
2157
|
-
|
|
2158
|
+
if (input.isJSON) writeEvent({
|
|
2159
|
+
event: "sign_in_url",
|
|
2160
|
+
url,
|
|
2161
|
+
browserOpened: opened,
|
|
2162
|
+
waitSeconds: WAIT_MS / 1e3
|
|
2163
|
+
});
|
|
2164
|
+
else process.stderr.write(opened ? `Opening your browser to sign in. If it did not open:\n\n${url}\n\n` : `Open this URL to sign in:\n\n${url}\n\n`);
|
|
2158
2165
|
const prompt = toPrompt(url);
|
|
2159
2166
|
if (process.stdin.isTTY) process.stderr.write("Press c to copy the URL, or paste the code or redirect URL here: ");
|
|
2160
2167
|
let callback;
|
|
@@ -2164,7 +2171,7 @@ async function runLogin(input) {
|
|
|
2164
2171
|
listener.close();
|
|
2165
2172
|
prompt.close();
|
|
2166
2173
|
}
|
|
2167
|
-
process.stderr.write(callback.error ? "\n" : "\nApproved, finishing the sign in\n");
|
|
2174
|
+
if (!input.isJSON) process.stderr.write(callback.error ? "\n" : "\nApproved, finishing the sign in\n");
|
|
2168
2175
|
if (callback.error) {
|
|
2169
2176
|
writeFailure("sign_in_failed", `sign in was refused: ${callback.error}${callback.errorDescription ? `, ${callback.errorDescription}` : ""}`, input.isJSON);
|
|
2170
2177
|
return ExitCode.ERROR;
|
|
@@ -2200,7 +2207,15 @@ async function runDeviceLogin(input, metadata, scope) {
|
|
|
2200
2207
|
const device = await toDeviceAuthorization(metadata.device_authorization_endpoint, settings.clientId, scope);
|
|
2201
2208
|
const url = device.verificationUriComplete ?? device.verificationUri;
|
|
2202
2209
|
const opened = input.flags["noBrowser"] !== true && !process.env["HARDFIN_NO_BROWSER"] && openBrowser(url);
|
|
2203
|
-
|
|
2210
|
+
if (input.isJSON) writeEvent({
|
|
2211
|
+
event: "device_code",
|
|
2212
|
+
userCode: device.userCode,
|
|
2213
|
+
verificationUri: device.verificationUri,
|
|
2214
|
+
url,
|
|
2215
|
+
browserOpened: opened,
|
|
2216
|
+
waitSeconds: Math.max(0, Math.round((device.expiresAt - Date.now()) / 1e3))
|
|
2217
|
+
});
|
|
2218
|
+
else process.stderr.write([
|
|
2204
2219
|
"",
|
|
2205
2220
|
` Code ${device.userCode}`,
|
|
2206
2221
|
` At ${device.verificationUri}`,
|
|
@@ -2252,6 +2267,10 @@ async function toSignedIn(input, metadata, tokens) {
|
|
|
2252
2267
|
].join("\n"));
|
|
2253
2268
|
return ExitCode.OK;
|
|
2254
2269
|
}
|
|
2270
|
+
/** writeEvent tells a program what a sign in is waiting on, as one line of JSON on stderr. */
|
|
2271
|
+
function writeEvent(event) {
|
|
2272
|
+
process.stderr.write(`${JSON.stringify(event)}\n`);
|
|
2273
|
+
}
|
|
2255
2274
|
/** toAuthorizationUrl builds the URL the person approves this CLI at. */
|
|
2256
2275
|
function toAuthorizationUrl(endpoint, request) {
|
|
2257
2276
|
const url = new URL(endpoint);
|
|
@@ -2778,23 +2797,52 @@ function wasReportAttempted() {
|
|
|
2778
2797
|
const PROTOCOL_VERSION = "2025-06-18";
|
|
2779
2798
|
const GUIDE_URI = "hardfin://guide";
|
|
2780
2799
|
const COMMANDS_URI = "hardfin://commands";
|
|
2781
|
-
/**
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
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.",
|
|
2788
|
-
inputSchema: {
|
|
2789
|
-
type: "object",
|
|
2790
|
-
properties: { args: {
|
|
2800
|
+
/** The public reference for the API every command calls. */
|
|
2801
|
+
const API_REFERENCE = "https://docs.hardfin.com";
|
|
2802
|
+
const ARGUMENTS_SCHEMA = {
|
|
2803
|
+
type: "object",
|
|
2804
|
+
properties: {
|
|
2805
|
+
args: {
|
|
2791
2806
|
type: "array",
|
|
2792
2807
|
items: { type: "string" },
|
|
2793
2808
|
description: "The arguments to hardfin, without the program name"
|
|
2794
|
-
}
|
|
2795
|
-
|
|
2809
|
+
},
|
|
2810
|
+
input: {
|
|
2811
|
+
type: "string",
|
|
2812
|
+
description: "JSON sent to the command on standard input, which it reads when given --input -"
|
|
2813
|
+
}
|
|
2814
|
+
},
|
|
2815
|
+
required: ["args"]
|
|
2816
|
+
};
|
|
2817
|
+
/**
|
|
2818
|
+
* Two tools, so a client can let a read run without asking and ask before anything changes.
|
|
2819
|
+
* The commands themselves are resources, which cost nothing until one is read.
|
|
2820
|
+
*/
|
|
2821
|
+
const READ_TOOL = {
|
|
2822
|
+
name: "hardfin_read",
|
|
2823
|
+
title: "Read Hardfin data",
|
|
2824
|
+
description: `Runs a Hardfin CLI command that only reads, against the Hardfin API (${API_REFERENCE}). Takes the arguments as a list, such as ["asset", "list", "--limit", "5"]. ["--help"] lists the commands, and the hardfin://guide resource describes each one.`,
|
|
2825
|
+
inputSchema: ARGUMENTS_SCHEMA,
|
|
2826
|
+
annotations: {
|
|
2827
|
+
title: "Read Hardfin data",
|
|
2828
|
+
readOnlyHint: true,
|
|
2829
|
+
destructiveHint: false,
|
|
2830
|
+
openWorldHint: true
|
|
2831
|
+
}
|
|
2832
|
+
};
|
|
2833
|
+
const WRITE_TOOL = {
|
|
2834
|
+
name: "hardfin_write",
|
|
2835
|
+
title: "Change Hardfin data",
|
|
2836
|
+
description: `Runs a Hardfin CLI command that creates, changes or deletes records through the Hardfin API (${API_REFERENCE}). Takes the arguments as a list, such as ["customer", "create", "--name", "Acme"]. A JSON body goes in the input argument, with --input - in the list.`,
|
|
2837
|
+
inputSchema: ARGUMENTS_SCHEMA,
|
|
2838
|
+
annotations: {
|
|
2839
|
+
title: "Change Hardfin data",
|
|
2840
|
+
readOnlyHint: false,
|
|
2841
|
+
destructiveHint: true,
|
|
2842
|
+
openWorldHint: true
|
|
2796
2843
|
}
|
|
2797
2844
|
};
|
|
2845
|
+
const INSTRUCTIONS = "hardfin_read looks records up and hardfin_write changes them. Both run Hardfin CLI commands, so the arguments are what a person would type after hardfin. The hardfin://guide resource lists every command, flag and error code.";
|
|
2798
2846
|
/** ProtocolFailure is a line this server answers with a JSON-RPC error rather than a result. */
|
|
2799
2847
|
var ProtocolFailure = class extends Error {
|
|
2800
2848
|
code;
|
|
@@ -2822,10 +2870,12 @@ async function toResponse(request, commands, run) {
|
|
|
2822
2870
|
},
|
|
2823
2871
|
serverInfo: {
|
|
2824
2872
|
name: "hardfin",
|
|
2873
|
+
title: "Hardfin",
|
|
2825
2874
|
version: version$1
|
|
2826
|
-
}
|
|
2875
|
+
},
|
|
2876
|
+
instructions: INSTRUCTIONS
|
|
2827
2877
|
});
|
|
2828
|
-
case "tools/list": return answer({ tools: [
|
|
2878
|
+
case "tools/list": return answer({ tools: [READ_TOOL, WRITE_TOOL] });
|
|
2829
2879
|
case "resources/list": return answer({ resources: [{
|
|
2830
2880
|
uri: GUIDE_URI,
|
|
2831
2881
|
name: "Hardfin CLI guide",
|
|
@@ -2838,7 +2888,8 @@ async function toResponse(request, commands, run) {
|
|
|
2838
2888
|
mimeType: "application/json"
|
|
2839
2889
|
}] });
|
|
2840
2890
|
case "resources/read": {
|
|
2841
|
-
const uri =
|
|
2891
|
+
const uri = request.params?.["uri"];
|
|
2892
|
+
if (typeof uri !== "string" || uri === "") throw new ProtocolFailure(-32602, "resources/read needs a uri, such as hardfin://guide", request.id ?? null);
|
|
2842
2893
|
const resource = toResource(uri, commands);
|
|
2843
2894
|
if (!resource) return {
|
|
2844
2895
|
jsonrpc: "2.0",
|
|
@@ -2850,7 +2901,7 @@ async function toResponse(request, commands, run) {
|
|
|
2850
2901
|
};
|
|
2851
2902
|
return answer(resource);
|
|
2852
2903
|
}
|
|
2853
|
-
case "tools/call": return answer(await toToolResult(request
|
|
2904
|
+
case "tools/call": return answer(await toToolResult(request, commands, run));
|
|
2854
2905
|
case "ping": return answer({});
|
|
2855
2906
|
default:
|
|
2856
2907
|
if (request.id === void 0 || request.id === null) return;
|
|
@@ -2924,80 +2975,170 @@ function toCommandName(args) {
|
|
|
2924
2975
|
function toIsAskingForHelp(args) {
|
|
2925
2976
|
return args.includes("--help") || args.includes("-h") || args[0] === "help";
|
|
2926
2977
|
}
|
|
2927
|
-
async function toToolResult(
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
};
|
|
2935
|
-
const args = (params?.["arguments"])?.args;
|
|
2936
|
-
if (!Array.isArray(args) || args.some((entry) => typeof entry !== "string")) return {
|
|
2937
|
-
content: [{
|
|
2938
|
-
type: "text",
|
|
2939
|
-
text: "args must be a list of strings, such as [\"asset\", \"list\"]"
|
|
2940
|
-
}],
|
|
2941
|
-
isError: true
|
|
2942
|
-
};
|
|
2978
|
+
async function toToolResult(request, commands, run) {
|
|
2979
|
+
const params = request.params;
|
|
2980
|
+
const name = params?.["name"];
|
|
2981
|
+
if (name !== READ_TOOL.name && name !== WRITE_TOOL.name) throw new ProtocolFailure(-32602, `a call names its tool, and this server offers ${READ_TOOL.name} and ${WRITE_TOOL.name}`, request.id ?? null);
|
|
2982
|
+
const called = params?.["arguments"];
|
|
2983
|
+
const args = called?.args;
|
|
2984
|
+
if (!Array.isArray(args) || args.some((entry) => typeof entry !== "string")) return toError("args must be a list of strings, such as [\"asset\", \"list\"]");
|
|
2985
|
+
if (called?.input !== void 0 && typeof called.input !== "string") return toError("input must be a string holding JSON, such as {\"name\": \"Acme\"}");
|
|
2943
2986
|
const refused = toRefusedArgument(args);
|
|
2944
|
-
if (refused) return
|
|
2945
|
-
content: [{
|
|
2946
|
-
type: "text",
|
|
2947
|
-
text: refused
|
|
2948
|
-
}],
|
|
2949
|
-
isError: true
|
|
2950
|
-
};
|
|
2987
|
+
if (refused) return toError(refused);
|
|
2951
2988
|
const asked = args;
|
|
2952
2989
|
const command = toCommandName(asked) ?? "";
|
|
2953
|
-
if (REFUSED_COMMANDS.has(command) && !toIsAskingForHelp(asked)) return {
|
|
2990
|
+
if (REFUSED_COMMANDS.has(command) && !toIsAskingForHelp(asked)) return toError(`${command} is run by a person at a terminal, not through this server. Run hardfin ${command} yourself, then call this tool again`);
|
|
2991
|
+
if (name === READ_TOOL.name && toAccess(asked, commands) === "write") return toError(`this command changes data, so it runs through ${WRITE_TOOL.name}`);
|
|
2992
|
+
const outcome = await run(asked, called?.input);
|
|
2993
|
+
return {
|
|
2954
2994
|
content: [{
|
|
2955
2995
|
type: "text",
|
|
2956
|
-
text:
|
|
2996
|
+
text: [outcome.stdout, outcome.stderr].filter((part) => part.trim() !== "").join("\n") || `hardfin exited ${outcome.code}`
|
|
2957
2997
|
}],
|
|
2958
|
-
isError:
|
|
2998
|
+
isError: outcome.code !== 0
|
|
2959
2999
|
};
|
|
2960
|
-
|
|
3000
|
+
}
|
|
3001
|
+
function toError(text) {
|
|
2961
3002
|
return {
|
|
2962
3003
|
content: [{
|
|
2963
3004
|
type: "text",
|
|
2964
|
-
text
|
|
3005
|
+
text
|
|
2965
3006
|
}],
|
|
2966
|
-
isError:
|
|
3007
|
+
isError: true
|
|
2967
3008
|
};
|
|
2968
3009
|
}
|
|
2969
|
-
/**
|
|
2970
|
-
const
|
|
3010
|
+
/** Commands with no endpoint that only show something, which a read may run. */
|
|
3011
|
+
const LOCAL_READS = /* @__PURE__ */ new Set([
|
|
3012
|
+
"status",
|
|
3013
|
+
"config",
|
|
3014
|
+
"agent-guide",
|
|
3015
|
+
"help"
|
|
3016
|
+
]);
|
|
3017
|
+
/**
|
|
3018
|
+
* toAccess decides whether a call reads or changes data. A generated command answers by its
|
|
3019
|
+
* endpoint's method, hardfin api by the method it was given, and anything it cannot place is
|
|
3020
|
+
* treated as a change, so it is asked about.
|
|
3021
|
+
*/
|
|
3022
|
+
function toAccess(args, commands) {
|
|
3023
|
+
if (toIsAskingForHelp(args)) return "read";
|
|
3024
|
+
let level = commands;
|
|
3025
|
+
let found;
|
|
3026
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
3027
|
+
const word = args[index] ?? "";
|
|
3028
|
+
if (GLOBAL_OPTIONS_WITH_VALUES.has(word)) {
|
|
3029
|
+
index += 1;
|
|
3030
|
+
continue;
|
|
3031
|
+
}
|
|
3032
|
+
if (word.startsWith("-")) continue;
|
|
3033
|
+
const next = level.find((entry) => entry.name === word);
|
|
3034
|
+
if (!next) break;
|
|
3035
|
+
found = next;
|
|
3036
|
+
level = next.subcommands ?? [];
|
|
3037
|
+
if (level.length === 0) break;
|
|
3038
|
+
}
|
|
3039
|
+
if (!found) return "read";
|
|
3040
|
+
if (found.endpoint) return found.endpoint.method === "GET" ? "read" : "write";
|
|
3041
|
+
if (found.name === "api") return toApiMethod(args) === "GET" ? "read" : "write";
|
|
3042
|
+
if (LOCAL_READS.has(found.name) || (found.subcommands?.length ?? 0) > 0) return "read";
|
|
3043
|
+
return "write";
|
|
3044
|
+
}
|
|
3045
|
+
/** toApiMethod reads the method hardfin api was given, which is GET when none was. */
|
|
3046
|
+
function toApiMethod(args) {
|
|
3047
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
3048
|
+
const word = args[index] ?? "";
|
|
3049
|
+
if (word === "-X" || word === "--method") return (args[index + 1] ?? "GET").toUpperCase();
|
|
3050
|
+
if (word.startsWith("--method=")) return word.slice(9).toUpperCase();
|
|
3051
|
+
if (word.startsWith("-X") && word.length > 2) return word.slice(2).toUpperCase();
|
|
3052
|
+
}
|
|
3053
|
+
return "GET";
|
|
3054
|
+
}
|
|
3055
|
+
/** The most one argument can hold, which Linux refuses past 128 KiB. */
|
|
3056
|
+
const ARGUMENT_BYTES = 131071;
|
|
3057
|
+
/** The most a whole command line can carry, below what the operating system refuses outright. */
|
|
3058
|
+
const COMMAND_LINE_BYTES = 1e6;
|
|
2971
3059
|
/** toRefusedArgument names an argument a command line cannot carry, rather than failing later. */
|
|
2972
3060
|
function toRefusedArgument(args) {
|
|
2973
3061
|
const withNul = args.findIndex((argument) => argument.includes("\0"));
|
|
2974
3062
|
if (withNul >= 0) return `args[${withNul}] holds a NUL byte, which a command line cannot carry`;
|
|
2975
|
-
const
|
|
2976
|
-
if (
|
|
2977
|
-
|
|
3063
|
+
const oversized = args.findIndex((argument) => Buffer.byteLength(argument) > ARGUMENT_BYTES);
|
|
3064
|
+
if (oversized >= 0) return `args[${oversized}] is ${Buffer.byteLength(args[oversized] ?? "")} bytes, and one argument carries at most ${ARGUMENT_BYTES}. Send a large body in the input argument instead`;
|
|
3065
|
+
const total = args.reduce((carried, argument) => carried + Buffer.byteLength(argument), 0);
|
|
3066
|
+
if (total > COMMAND_LINE_BYTES) return `these arguments are ${total} bytes, and a command line carries at most ${COMMAND_LINE_BYTES}`;
|
|
3067
|
+
}
|
|
3068
|
+
/** How long one command may run before it is stopped, so a hung call cannot hold its client. */
|
|
3069
|
+
const CALL_TIMEOUT_MS = 12e4;
|
|
3070
|
+
/** How many commands run at once, so a burst of calls cannot start a process each. */
|
|
3071
|
+
const MAX_RUNNING = 4;
|
|
2978
3072
|
/** toCliRunner runs the CLI itself, so a tool call parses exactly as a terminal would. */
|
|
2979
|
-
function toCliRunner() {
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3073
|
+
function toCliRunner(timeoutMs = CALL_TIMEOUT_MS, entry = process.argv[1] ?? "") {
|
|
3074
|
+
const waiting = [];
|
|
3075
|
+
let running = 0;
|
|
3076
|
+
const acquire = async () => {
|
|
3077
|
+
if (running < MAX_RUNNING) {
|
|
3078
|
+
running += 1;
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
3081
|
+
await new Promise((resolve) => waiting.push(resolve));
|
|
3082
|
+
};
|
|
3083
|
+
const release = () => {
|
|
3084
|
+
const next = waiting.shift();
|
|
3085
|
+
if (next) next();
|
|
3086
|
+
else running -= 1;
|
|
3087
|
+
};
|
|
3088
|
+
return async (args, input) => {
|
|
3089
|
+
await acquire();
|
|
3090
|
+
try {
|
|
3091
|
+
return await toRun(entry, args, input, timeoutMs);
|
|
3092
|
+
} finally {
|
|
3093
|
+
release();
|
|
3094
|
+
}
|
|
3095
|
+
};
|
|
3096
|
+
}
|
|
3097
|
+
/** toRun runs one command, feeding it input on stdin and stopping it at the deadline. */
|
|
3098
|
+
function toRun(entry, args, input, timeoutMs) {
|
|
3099
|
+
return new Promise((resolve) => {
|
|
3100
|
+
let child;
|
|
3101
|
+
try {
|
|
3102
|
+
child = spawn(process.execPath, [entry, ...args], { env: {
|
|
3103
|
+
...process.env,
|
|
3104
|
+
HARDFIN_NO_BROWSER: "1",
|
|
3105
|
+
HARDFIN_MCP: "1"
|
|
3106
|
+
} });
|
|
3107
|
+
} catch (error) {
|
|
3108
|
+
resolve({
|
|
3109
|
+
stdout: "",
|
|
3110
|
+
stderr: `the command could not start: ${error instanceof Error ? error.message : String(error)}`,
|
|
3111
|
+
code: 1
|
|
3112
|
+
});
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
2986
3115
|
let stdout = "";
|
|
2987
3116
|
let stderr = "";
|
|
3117
|
+
let isTimedOut = false;
|
|
3118
|
+
const timer = setTimeout(() => {
|
|
3119
|
+
isTimedOut = true;
|
|
3120
|
+
child.kill("SIGKILL");
|
|
3121
|
+
}, timeoutMs);
|
|
2988
3122
|
child.stdout.on("data", (chunk) => {
|
|
2989
3123
|
stdout += chunk.toString();
|
|
2990
3124
|
});
|
|
2991
3125
|
child.stderr.on("data", (chunk) => {
|
|
2992
3126
|
stderr += chunk.toString();
|
|
2993
3127
|
});
|
|
3128
|
+
child.on("error", (error) => {
|
|
3129
|
+
stderr += `the command could not start: ${error.message}\n`;
|
|
3130
|
+
});
|
|
2994
3131
|
child.on("close", (code) => {
|
|
3132
|
+
clearTimeout(timer);
|
|
3133
|
+
if (isTimedOut) stderr += `this command took longer than ${timeoutMs / 1e3} seconds, so it was stopped\n`;
|
|
2995
3134
|
resolve({
|
|
2996
3135
|
stdout,
|
|
2997
3136
|
stderr,
|
|
2998
|
-
code: code ?? 1
|
|
3137
|
+
code: isTimedOut ? 1 : code ?? 1
|
|
2999
3138
|
});
|
|
3000
3139
|
});
|
|
3140
|
+
child.stdin.on("error", () => void 0);
|
|
3141
|
+
child.stdin.end(input ?? "");
|
|
3001
3142
|
});
|
|
3002
3143
|
}
|
|
3003
3144
|
/** serve answers requests on stdin until the client closes it. */
|
|
@@ -6168,5 +6309,5 @@ async function toReported(error, isJSON, resolved, level = "error") {
|
|
|
6168
6309
|
//#endregion
|
|
6169
6310
|
export { toCli, toParseExitCode, toReported, wasReportAttempted };
|
|
6170
6311
|
|
|
6171
|
-
//# sourceMappingURL=program-
|
|
6172
|
-
//# debugId=
|
|
6312
|
+
//# sourceMappingURL=program-DTsxnjRj.js.map
|
|
6313
|
+
//# debugId=c7f8ce05-d638-5d0f-a088-655ddade6f45
|