@hardfin/cli 0.1.1-dev.54 → 0.1.1-dev.56
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 +48 -5
- package/dist/cli.js +4 -4
- package/dist/{program-BhH7Zc0y.js → program-CI4W2agw.js} +76 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,6 +72,19 @@ A value is sent as the type the document names. `--useful-life 36` sends the num
|
|
|
72
72
|
decimal such as `--salvage-value 1500.00` stays a string, which is how the API takes money
|
|
73
73
|
without losing precision.
|
|
74
74
|
|
|
75
|
+
Inside a `key=value` flag, `=` gives a value as you typed it and `:=` gives it as JSON.
|
|
76
|
+
|
|
77
|
+
| You type | Is sent as |
|
|
78
|
+
| ----------------------- | ------------------------------------------------------------------ |
|
|
79
|
+
| `value=36` | the text "36", because a custom field may hold a serial number |
|
|
80
|
+
| `value:=36` | the number 36 |
|
|
81
|
+
| `value:=true` | the boolean true |
|
|
82
|
+
| `deliverAt:=null` | null |
|
|
83
|
+
| `order=2` or `order:=2` | the number 2, because the document types `order` as a whole number |
|
|
84
|
+
|
|
85
|
+
A value the field does not take fails locally with exit code 2, such as `order=two`, or
|
|
86
|
+
`id:=42` for a field that takes text alone.
|
|
87
|
+
|
|
75
88
|
A download is written where you name it, and a terminal is never filled with a file's bytes.
|
|
76
89
|
With no `--output`, the file takes the name it was uploaded under, in the working directory.
|
|
77
90
|
That name never chooses a folder, a hidden file, or a file that is already there. The command
|
|
@@ -411,7 +424,8 @@ projects come ready. Running it again updates only what it wrote.
|
|
|
411
424
|
| `.vscode/mcp.json` | The `hardfin` server, for VS Code |
|
|
412
425
|
|
|
413
426
|
The server is the hosted one at the authorization server's host, `https://api.hardfin.com/mcp`
|
|
414
|
-
by default.
|
|
427
|
+
by default. [Serving an agent](#serving-an-agent) says how it differs from the local server
|
|
428
|
+
this CLI runs. Every other server in an MCP config, and everything in `AGENTS.md` outside the
|
|
415
429
|
markers, is left as it was. A config that does not hold JSON is skipped, never overwritten.
|
|
416
430
|
|
|
417
431
|
`init` refuses an address that `config.local.json` or `.env` in the folder chose, because the
|
|
@@ -423,17 +437,46 @@ package, so `init` writes the copy that matches the installed CLI.
|
|
|
423
437
|
|
|
424
438
|
## Serving an agent
|
|
425
439
|
|
|
440
|
+
Hardfin has two MCP servers, and an agent needs only one of them. Hardfin hosts one. The
|
|
441
|
+
other is this CLI, run on your machine. Read this section to choose between them.
|
|
442
|
+
|
|
443
|
+
| | The hosted server | The local server |
|
|
444
|
+
| --------------------- | -------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
445
|
+
| Runs | At Hardfin, at `https://api.hardfin.com/mcp` | On your machine, started by the agent's client |
|
|
446
|
+
| Needs installed | Nothing | This CLI, and Node 20.12 or later |
|
|
447
|
+
| Set up by | `hardfin init` | `claude mcp add hardfin -- hardfin mcp` |
|
|
448
|
+
| Signs in as | Whoever signs in to Hardfin from the client | Whoever ran `hardfin login` here, or `HARDFIN_API_KEY` |
|
|
449
|
+
| Tools | `hardfin_read`, `hardfin_create`, `hardfin_update`, `hardfin_delete` | `hardfin_read`, `hardfin_write`, `hardfin_delete` |
|
|
450
|
+
| A call names | An operation from the API reference, with its parameters and body | A command, as the words typed after `hardfin` |
|
|
451
|
+
| Read-only when | The sign in was not granted `hardfin:write` | The same, or it was started with `--read-only` |
|
|
452
|
+
| Files on your machine | Out of reach | Uploaded and downloaded, inside the folder it runs in |
|
|
453
|
+
|
|
454
|
+
Choose the hosted server unless one of these holds.
|
|
455
|
+
|
|
456
|
+
- The agent has to upload a file from your machine, or download one to it
|
|
457
|
+
- The agent runs where your browser cannot sign in to Hardfin, and you have a stored sign in or an API key on the machine
|
|
458
|
+
- You want a local build of the API, which `hardfin mcp` reaches through `config.local.json`
|
|
459
|
+
|
|
460
|
+
The two servers place one command differently. Scrapping an asset goes through
|
|
461
|
+
`hardfin_delete` on the local server and through `hardfin_update` on the hosted one. Both ask
|
|
462
|
+
before it by default, because both tools carry the destructive hint.
|
|
463
|
+
|
|
464
|
+
Both set-ups name the server `hardfin`, so adding one to a project replaces the other. To run
|
|
465
|
+
both, give the local one another name:
|
|
466
|
+
|
|
426
467
|
```sh
|
|
427
|
-
hardfin mcp
|
|
468
|
+
claude mcp add hardfin-local -- hardfin mcp
|
|
428
469
|
```
|
|
429
470
|
|
|
430
|
-
|
|
431
|
-
other client can run Hardfin commands.
|
|
471
|
+
The rest of this section is about the local server.
|
|
432
472
|
|
|
433
473
|
```sh
|
|
434
|
-
|
|
474
|
+
hardfin mcp
|
|
435
475
|
```
|
|
436
476
|
|
|
477
|
+
It speaks the Model Context Protocol on standard input and output, so Claude, Codex or any
|
|
478
|
+
other client can run Hardfin commands.
|
|
479
|
+
|
|
437
480
|
### Three tools, and resources for the rest
|
|
438
481
|
|
|
439
482
|
Every tool a server lists sits in the agent's context for the entire session, so this server
|
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]="ec88336e-026f-5a04-9acd-43c5410c3229")}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-CI4W2agw.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
|
const isJSON = process.argv.includes("--json") || !process.stdout.isTTY;
|
|
35
35
|
try {
|
|
36
|
-
const { toReported, writeCrash } = await import("./program-
|
|
36
|
+
const { toReported, writeCrash } = await import("./program-CI4W2agw.js");
|
|
37
37
|
writeCrash(error, isJSON);
|
|
38
38
|
await toReported(error, isJSON, void 0, "fatal");
|
|
39
39
|
} catch {
|
|
@@ -54,4 +54,4 @@ async function toQuickAnswer(args) {
|
|
|
54
54
|
export {};
|
|
55
55
|
|
|
56
56
|
//# sourceMappingURL=cli.js.map
|
|
57
|
-
//# debugId=
|
|
57
|
+
//# debugId=ec88336e-026f-5a04-9acd-43c5410c3229
|
|
@@ -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]="cb5319cf-733c-5f06-b5d8-f8f154bf4267")}catch(e){}}();
|
|
4
4
|
import { a as defineCommand, i as GLOBAL_FLAGS, r as ExitCode, t as toInvocation } from "./scrub-BZ0egWxC.js";
|
|
5
5
|
import { n as toAnswer$1, r as toCompletion, t as PATH_VALUES } from "./completion-tree-DLC6ARLR.js";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
@@ -1477,7 +1477,8 @@ function toDescribed(flag) {
|
|
|
1477
1477
|
default: flag.defaultValue,
|
|
1478
1478
|
schema: toFlagSchema(flag),
|
|
1479
1479
|
sends: sent.queryName !== void 0 ? { query: sent.queryName } : sent.jsonPath !== void 0 ? { body: sent.jsonPath } : void 0,
|
|
1480
|
-
element: sent.element
|
|
1480
|
+
element: sent.element,
|
|
1481
|
+
elementTypes: sent.elementTypes
|
|
1481
1482
|
};
|
|
1482
1483
|
return Object.fromEntries(Object.entries(described).filter(([, value]) => value !== void 0 && value !== false));
|
|
1483
1484
|
}
|
|
@@ -3356,17 +3357,64 @@ function toElements(flag, values) {
|
|
|
3356
3357
|
for (const pair of value.split(",")) {
|
|
3357
3358
|
const split = pair.indexOf("=");
|
|
3358
3359
|
if (split < 1) return /* @__PURE__ */ new Error(`--${flag.name} takes ${flag.element?.join("=, ") ?? ""}=, such as --${flag.name} ${flag.element?.[0] ?? "field"}=value. A value cannot contain a comma`);
|
|
3359
|
-
const
|
|
3360
|
+
const isJSON = pair[split - 1] === ":";
|
|
3361
|
+
const key = pair.slice(0, isJSON ? split - 1 : split).trim();
|
|
3360
3362
|
if (flag.element && !flag.element.includes(key)) return /* @__PURE__ */ new Error(`--${flag.name} has no field ${key}. It takes ${flag.element.join(", ")}`);
|
|
3361
3363
|
if (key in element) return /* @__PURE__ */ new Error(`--${flag.name} names ${key} twice in one element, so which one it takes is not clear`);
|
|
3362
3364
|
const value = pair.slice(split + 1).trim();
|
|
3363
3365
|
if (flag.elementUuids?.includes(key) && !z.uuid().safeParse(value).success) return /* @__PURE__ */ new Error(`--${flag.name} needs a UUID for ${key}, such as 3f2a9c1e-5b7d-4e8a-9c21-7d4e5f6a8b90, and ${value || "nothing"} is not one`);
|
|
3364
|
-
|
|
3366
|
+
const types = flag.elementTypes?.[key];
|
|
3367
|
+
const typed = isJSON ? toJSONValue(value, types ?? ["string"]) : toTyped(value, types);
|
|
3368
|
+
if (typed === NOT_TAKEN) return /* @__PURE__ */ new Error(`--${flag.name} needs ${toNamedTypes(types ?? ["string"])} for ${key}, and ${value || "nothing"} is not one`);
|
|
3369
|
+
element[key] = typed;
|
|
3365
3370
|
}
|
|
3366
3371
|
elements.push(element);
|
|
3367
3372
|
}
|
|
3368
3373
|
return elements;
|
|
3369
3374
|
}
|
|
3375
|
+
/** Marks text that is none of the types a field takes. */
|
|
3376
|
+
const NOT_TAKEN = Symbol("not taken");
|
|
3377
|
+
const NUMBER = /^-?\d+(\.\d+)?$/;
|
|
3378
|
+
/**
|
|
3379
|
+
* toTyped reads the text after key= as the type the document gives the field. A field that
|
|
3380
|
+
* takes text is sent as typed, whatever it looks like, because 36 may be a serial number. A
|
|
3381
|
+
* field that takes no text has only one reading, so order=2 is sent as the number.
|
|
3382
|
+
*/
|
|
3383
|
+
function toTyped(text, types) {
|
|
3384
|
+
if (types === void 0 || types.includes("string")) return text;
|
|
3385
|
+
if (types.includes("null") && text === "null") return null;
|
|
3386
|
+
if ((types.includes("integer") || types.includes("number")) && NUMBER.test(text)) {
|
|
3387
|
+
const value = Number(text);
|
|
3388
|
+
if (types.includes("number") || Number.isInteger(value)) return value;
|
|
3389
|
+
}
|
|
3390
|
+
if (types.includes("boolean") && (text === "true" || text === "false")) return text === "true";
|
|
3391
|
+
return NOT_TAKEN;
|
|
3392
|
+
}
|
|
3393
|
+
/**
|
|
3394
|
+
* toJSONValue reads the text after key:= as JSON, and takes it when the field takes its type.
|
|
3395
|
+
* It is how a custom field is given the number 36 or true, where key= would send the text.
|
|
3396
|
+
*/
|
|
3397
|
+
function toJSONValue(text, types) {
|
|
3398
|
+
let value;
|
|
3399
|
+
try {
|
|
3400
|
+
value = JSON.parse(text);
|
|
3401
|
+
} catch {
|
|
3402
|
+
return NOT_TAKEN;
|
|
3403
|
+
}
|
|
3404
|
+
const type = value === null ? "null" : typeof value;
|
|
3405
|
+
return (types.includes(type) || type === "number" && types.includes("integer") && Number.isInteger(value)) && type !== "object" ? value : NOT_TAKEN;
|
|
3406
|
+
}
|
|
3407
|
+
/** toNamedTypes says in words what a field takes, for the message that refuses a value. */
|
|
3408
|
+
function toNamedTypes(types) {
|
|
3409
|
+
const named = {
|
|
3410
|
+
integer: "a whole number",
|
|
3411
|
+
number: "a number",
|
|
3412
|
+
boolean: "true or false",
|
|
3413
|
+
string: "text in quotes",
|
|
3414
|
+
null: "null"
|
|
3415
|
+
};
|
|
3416
|
+
return types.filter((type) => type in named).map((type) => named[type]).join(" or ");
|
|
3417
|
+
}
|
|
3370
3418
|
/** set writes a value at its path, building the objects a nested field needs. */
|
|
3371
3419
|
function set(body, path, value) {
|
|
3372
3420
|
let holder = body;
|
|
@@ -4826,6 +4874,12 @@ const commands = [
|
|
|
4826
4874
|
"destinationId",
|
|
4827
4875
|
"originId"
|
|
4828
4876
|
],
|
|
4877
|
+
elementTypes: {
|
|
4878
|
+
"deliverAt": ["string", "null"],
|
|
4879
|
+
"deliverAtTimezone": ["string", "null"],
|
|
4880
|
+
"shipAt": ["string", "null"],
|
|
4881
|
+
"shipAtTimezone": ["string", "null"]
|
|
4882
|
+
},
|
|
4829
4883
|
schema: z.array(z.string())
|
|
4830
4884
|
}]
|
|
4831
4885
|
})]
|
|
@@ -4856,6 +4910,10 @@ const commands = [
|
|
|
4856
4910
|
"shipAt"
|
|
4857
4911
|
],
|
|
4858
4912
|
elementUuids: ["assetId"],
|
|
4913
|
+
elementTypes: {
|
|
4914
|
+
"deliverAt": ["string", "null"],
|
|
4915
|
+
"shipAt": ["string", "null"]
|
|
4916
|
+
},
|
|
4859
4917
|
schema: z.array(z.string())
|
|
4860
4918
|
}]
|
|
4861
4919
|
})]
|
|
@@ -4933,6 +4991,14 @@ const commands = [
|
|
|
4933
4991
|
repeatable: true,
|
|
4934
4992
|
element: ["fieldId", "value"],
|
|
4935
4993
|
elementUuids: ["fieldId"],
|
|
4994
|
+
elementTypes: { "value": [
|
|
4995
|
+
"array",
|
|
4996
|
+
"boolean",
|
|
4997
|
+
"number",
|
|
4998
|
+
"object",
|
|
4999
|
+
"string",
|
|
5000
|
+
"null"
|
|
5001
|
+
] },
|
|
4936
5002
|
schema: z.array(z.string())
|
|
4937
5003
|
},
|
|
4938
5004
|
{
|
|
@@ -6277,6 +6343,10 @@ const commands = [
|
|
|
6277
6343
|
"section"
|
|
6278
6344
|
],
|
|
6279
6345
|
elementUuids: ["fieldId"],
|
|
6346
|
+
elementTypes: {
|
|
6347
|
+
"order": ["integer"],
|
|
6348
|
+
"section": ["integer"]
|
|
6349
|
+
},
|
|
6280
6350
|
schema: z.array(z.string())
|
|
6281
6351
|
},
|
|
6282
6352
|
{
|
|
@@ -7177,5 +7247,5 @@ async function toReported(error, isJSON, resolved, level = "error") {
|
|
|
7177
7247
|
//#endregion
|
|
7178
7248
|
export { toCli, toParseExitCode, toReported, wasReportAttempted, writeCrash };
|
|
7179
7249
|
|
|
7180
|
-
//# sourceMappingURL=program-
|
|
7181
|
-
//# debugId=
|
|
7250
|
+
//# sourceMappingURL=program-CI4W2agw.js.map
|
|
7251
|
+
//# debugId=cb5319cf-733c-5f06-b5d8-f8f154bf4267
|