@hardfin/cli 0.1.0-dev.34 → 0.1.0-dev.36
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 +27 -1
- package/dist/cli.js +6 -6
- package/dist/completion-tree.json +19 -0
- package/dist/{program-DTsxnjRj.js → program-DtqGeehp.js} +193 -13
- package/dist/{sentry-CLaeAWNa.js → sentry-BM9Rvhhy.js} +6 -6
- package/package.json +4 -2
- package/skills/hardfin-cli/SKILL.md +133 -0
- package/templates/AGENTS.md +25 -0
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ Everything an endpoint takes becomes a flag or an argument. No command asks for
|
|
|
60
60
|
|
|
61
61
|
```sh
|
|
62
62
|
hardfin asset ownership create ast_4f9xk2mq7plr8stz \
|
|
63
|
-
--customer-id
|
|
63
|
+
--customer-id 0192f7d4-3b6a-7c1e-9f2d-5a8b4c6e7d73 --date 2026-09-23 --sale-price 1500.00
|
|
64
64
|
|
|
65
65
|
hardfin asset move execute create \
|
|
66
66
|
--move assetId=0192f7d4-3b6a-7c1e-9f2d-5a8b4c6e7d70,originId=0192f7d4-3b6a-7c1e-9f2d-5a8b4c6e7d71,destinationId=0192f7d4-3b6a-7c1e-9f2d-5a8b4c6e7d72,shipAt=2026-09-24T10:00:00Z
|
|
@@ -307,6 +307,32 @@ show a description beside each.
|
|
|
307
307
|
The script calls `hardfin` by name, so it needs `hardfin` on your `PATH`. Running
|
|
308
308
|
`node dist/cli.js` from a checkout leaves nothing for it to call, and `npm link` fixes that.
|
|
309
309
|
|
|
310
|
+
## Setting up a project for agents
|
|
311
|
+
|
|
312
|
+
```sh
|
|
313
|
+
hardfin init # the working directory
|
|
314
|
+
hardfin init --dry-run # what it would write, writing nothing
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
`hardfin init` gets a project ready for a coding agent to use Hardfin, the way EmDash's starter
|
|
318
|
+
projects come ready. Running it again updates only what it wrote.
|
|
319
|
+
|
|
320
|
+
| File | What init writes |
|
|
321
|
+
| ------------------------------------- | ------------------------------------------------------------------- |
|
|
322
|
+
| `.agents/skills/hardfin-cli/SKILL.md` | The `hardfin-cli` skill, which Codex and most agents read |
|
|
323
|
+
| `.claude/skills/hardfin-cli/SKILL.md` | The same skill, where Claude Code reads it |
|
|
324
|
+
| `AGENTS.md` | A Hardfin section between `hardfin:start` and `hardfin:end` markers |
|
|
325
|
+
| `.mcp.json` | The `hardfin` server, for Claude Code |
|
|
326
|
+
| `.cursor/mcp.json` | The `hardfin` server, for Cursor |
|
|
327
|
+
| `.vscode/mcp.json` | The `hardfin` server, for VS Code |
|
|
328
|
+
|
|
329
|
+
The server is the hosted one at the authorization server's host, `https://api.hardfin.com/mcp`
|
|
330
|
+
by default. Every other server in an MCP config, and everything in `AGENTS.md` outside the
|
|
331
|
+
markers, is left as it was. A config that does not hold JSON is skipped, never overwritten.
|
|
332
|
+
|
|
333
|
+
The skill lives in `skills/` and the section in `templates/AGENTS.md`. Both ship in the npm
|
|
334
|
+
package, so `init` writes the copy that matches the installed CLI.
|
|
335
|
+
|
|
310
336
|
## Serving an agent
|
|
311
337
|
|
|
312
338
|
```sh
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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]="fa6bfe29-9ee3-5a91-aaa1-4b75576d7943")}catch(e){}}();
|
|
4
4
|
import module from "node:module";
|
|
5
5
|
//#region src/cli.ts
|
|
6
6
|
module.enableCompileCache?.();
|
|
7
|
-
process.stdout.on("error", (error) => {
|
|
8
|
-
if (error.code === "EPIPE") process.exit(0);
|
|
7
|
+
for (const stream of [process.stdout, process.stderr]) stream.on("error", (error) => {
|
|
8
|
+
if (error.code === "EPIPE") process.exit(process.exitCode ?? 0);
|
|
9
9
|
throw error;
|
|
10
10
|
});
|
|
11
11
|
let isCrashing = false;
|
|
@@ -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-DtqGeehp.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-DtqGeehp.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=fa6bfe29-9ee3-5a91-aaa1-4b75576d7943
|
|
@@ -73,6 +73,25 @@
|
|
|
73
73
|
"arguments": [],
|
|
74
74
|
"subcommands": []
|
|
75
75
|
},
|
|
76
|
+
{
|
|
77
|
+
"name": "init",
|
|
78
|
+
"summary": "Set up a project so coding agents can use Hardfin",
|
|
79
|
+
"runs": true,
|
|
80
|
+
"flags": [
|
|
81
|
+
{
|
|
82
|
+
"name": "dry-run",
|
|
83
|
+
"description": "Say what would be written, and write nothing"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "json",
|
|
87
|
+
"description": "Print machine-readable output, which is the default when stdout is not a terminal"
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"arguments": [
|
|
91
|
+
{}
|
|
92
|
+
],
|
|
93
|
+
"subcommands": []
|
|
94
|
+
},
|
|
76
95
|
{
|
|
77
96
|
"name": "asset",
|
|
78
97
|
"summary": "Asset commands",
|
|
@@ -1,20 +1,20 @@
|
|
|
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]="03b49867-f2d5-5a64-af87-e45e530711a1")}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";
|
|
7
7
|
import { Command, CommanderError, Option } from "commander";
|
|
8
8
|
import { z } from "zod";
|
|
9
|
-
import { accessSync, chmodSync, closeSync, constants, existsSync, mkdirSync, openAsBlob, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
10
|
-
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
9
|
+
import { accessSync, chmodSync, closeSync, constants, existsSync, mkdirSync, openAsBlob, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
10
|
+
import { basename, delimiter, dirname, join, relative, resolve } from "node:path";
|
|
11
11
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
12
12
|
import { arch, cpus, homedir, release, totalmem, type, version } from "node:os";
|
|
13
13
|
import { spawn, spawnSync } from "node:child_process";
|
|
14
14
|
import { createInterface } from "node:readline/promises";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
15
16
|
import { createServer } from "node:http";
|
|
16
17
|
import { createInterface as createInterface$1 } from "node:readline";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
18
|
//#region src/report/expected.ts
|
|
19
19
|
/**
|
|
20
20
|
* ExpectedFailure is a failure the CLI anticipated: the person's input, their network, their
|
|
@@ -1157,8 +1157,8 @@ async function runApi(input) {
|
|
|
1157
1157
|
writeFailure("invalid_arguments", "a path is required, such as /customer", input.isJSON);
|
|
1158
1158
|
return ExitCode.USAGE;
|
|
1159
1159
|
}
|
|
1160
|
-
if (
|
|
1161
|
-
writeFailure("invalid_arguments", "a path stays inside the API, so it cannot
|
|
1160
|
+
if (!isInsideApi(input.resolved.settings.apiUrl, path)) {
|
|
1161
|
+
writeFailure("invalid_arguments", "a path stays inside the API, so it cannot climb out of it with ..", input.isJSON);
|
|
1162
1162
|
return ExitCode.USAGE;
|
|
1163
1163
|
}
|
|
1164
1164
|
const query = toQuery$1(input.flags["field"]);
|
|
@@ -1196,6 +1196,17 @@ async function runApi(input) {
|
|
|
1196
1196
|
throw error;
|
|
1197
1197
|
}
|
|
1198
1198
|
}
|
|
1199
|
+
/** isInsideApi reports whether a path, once parsed, still lands under the API's own path. */
|
|
1200
|
+
function isInsideApi(apiUrl, path) {
|
|
1201
|
+
try {
|
|
1202
|
+
const base = new URL(apiUrl);
|
|
1203
|
+
const target = new URL(`${apiUrl}${path.startsWith("/") ? "" : "/"}${path}`);
|
|
1204
|
+
const root = base.pathname.replace(/\/+$/, "");
|
|
1205
|
+
return target.origin === base.origin && (target.pathname === root || target.pathname.startsWith(`${root}/`));
|
|
1206
|
+
} catch {
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1199
1210
|
/** toQuery folds the repeated --field flags into query parameters. */
|
|
1200
1211
|
function toQuery$1(fields) {
|
|
1201
1212
|
const query = new URLSearchParams();
|
|
@@ -1475,7 +1486,7 @@ const uninstallCommand = defineCommand({
|
|
|
1475
1486
|
* directory by itself, and every other shell is told to load the script from its startup
|
|
1476
1487
|
* file, because not every bash has bash-completion to load it lazily.
|
|
1477
1488
|
*/
|
|
1478
|
-
function toPlan(shell, place) {
|
|
1489
|
+
function toPlan$1(shell, place) {
|
|
1479
1490
|
const content = `${toScript(shell)}\n`;
|
|
1480
1491
|
if (shell === "fish") {
|
|
1481
1492
|
const config = place.env["XDG_CONFIG_HOME"] || join(place.home, ".config");
|
|
@@ -1558,7 +1569,7 @@ function toShell(place) {
|
|
|
1558
1569
|
}
|
|
1559
1570
|
/** isInstalled answers whether install has written a script for any shell. */
|
|
1560
1571
|
function isInstalled(place = toPlace()) {
|
|
1561
|
-
return SHELLS.some((shell) => existsSync(toPlan(shell, {
|
|
1572
|
+
return SHELLS.some((shell) => existsSync(toPlan$1(shell, {
|
|
1562
1573
|
...place,
|
|
1563
1574
|
toProfile: () => void 0
|
|
1564
1575
|
}).script.path));
|
|
@@ -1591,7 +1602,7 @@ async function runChange(input, change) {
|
|
|
1591
1602
|
writeFailure("invalid_arguments", `name the shell to ${change} for: ${SHELLS.join(", ")}`, input.isJSON);
|
|
1592
1603
|
return ExitCode.USAGE;
|
|
1593
1604
|
}
|
|
1594
|
-
const plan = toPlan(shell, place);
|
|
1605
|
+
const plan = toPlan$1(shell, place);
|
|
1595
1606
|
if (change === "install" && shell === "powershell" && !plan.startup) {
|
|
1596
1607
|
writeFailure("shell_unavailable", "PowerShell did not report its profile, so there is nowhere to load the script from", input.isJSON);
|
|
1597
1608
|
return ExitCode.ERROR;
|
|
@@ -1830,6 +1841,164 @@ const crashCommand = defineCommand({
|
|
|
1830
1841
|
run: () => Promise.reject(/* @__PURE__ */ new TypeError("hardfin __crash failed on purpose, to check that defects are reported"))
|
|
1831
1842
|
});
|
|
1832
1843
|
//#endregion
|
|
1844
|
+
//#region src/command/init.ts
|
|
1845
|
+
/** The markers around the section init owns in AGENTS.md, so a second run replaces only that. */
|
|
1846
|
+
const SECTION_START = "<!-- hardfin:start -->";
|
|
1847
|
+
const SECTION_END = "<!-- hardfin:end -->";
|
|
1848
|
+
/** Where agents look for skills: Codex and most others, then Claude Code. */
|
|
1849
|
+
const SKILL_DIRECTORIES = [".agents/skills", ".claude/skills"];
|
|
1850
|
+
const initCommand = defineCommand({
|
|
1851
|
+
name: "init",
|
|
1852
|
+
summary: "Set up a project so coding agents can use Hardfin",
|
|
1853
|
+
description: "Writes the hardfin-cli skill, a Hardfin section in AGENTS.md, and the Hardfin MCP server for Claude Code, Cursor and VS Code. Running it again updates what it wrote and leaves everything else alone.",
|
|
1854
|
+
arguments: [{
|
|
1855
|
+
name: "directory",
|
|
1856
|
+
description: "The project to set up, which is the working directory when omitted",
|
|
1857
|
+
required: false
|
|
1858
|
+
}],
|
|
1859
|
+
flags: [{
|
|
1860
|
+
name: "dry-run",
|
|
1861
|
+
description: "Say what would be written, and write nothing",
|
|
1862
|
+
schema: z.boolean()
|
|
1863
|
+
}, {
|
|
1864
|
+
name: "json",
|
|
1865
|
+
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
1866
|
+
schema: z.boolean()
|
|
1867
|
+
}],
|
|
1868
|
+
examples: [{
|
|
1869
|
+
description: "Set up the project you are in",
|
|
1870
|
+
command: "hardfin init"
|
|
1871
|
+
}, {
|
|
1872
|
+
description: "See what it would write first",
|
|
1873
|
+
command: "hardfin init --dry-run"
|
|
1874
|
+
}],
|
|
1875
|
+
run: runInit
|
|
1876
|
+
});
|
|
1877
|
+
async function runInit(input) {
|
|
1878
|
+
const target = resolve(input.args[0] ?? ".");
|
|
1879
|
+
if (!existsSync(target) || !statSync(target).isDirectory()) {
|
|
1880
|
+
writeFailure("invalid_arguments", `${target} is not a folder, so there is nothing to set up`, input.isJSON);
|
|
1881
|
+
return ExitCode.USAGE;
|
|
1882
|
+
}
|
|
1883
|
+
const mcpUrl = `${input.resolved.settings.issuerUrl.replace(/\/+$/, "")}/mcp`;
|
|
1884
|
+
const planned = toPlan(target, toPackageDirectory(), mcpUrl);
|
|
1885
|
+
const isDryRun = input.flags["dryRun"] === true;
|
|
1886
|
+
if (!isDryRun) for (const entry of planned) entry.write?.();
|
|
1887
|
+
const changes = planned.map((entry) => entry.change);
|
|
1888
|
+
if (input.isJSON) {
|
|
1889
|
+
writeData({
|
|
1890
|
+
directory: target,
|
|
1891
|
+
dryRun: isDryRun,
|
|
1892
|
+
mcpUrl,
|
|
1893
|
+
changes
|
|
1894
|
+
});
|
|
1895
|
+
return ExitCode.OK;
|
|
1896
|
+
}
|
|
1897
|
+
const lines = changes.map((change) => `${change.action.padEnd(10)} ${change.path}${change.reason ? ` (${change.reason})` : ""}`);
|
|
1898
|
+
const closing = isDryRun ? "Nothing was written, because this was a dry run." : "Start an agent here and ask it to use Hardfin.";
|
|
1899
|
+
writeData([
|
|
1900
|
+
...lines,
|
|
1901
|
+
"",
|
|
1902
|
+
closing
|
|
1903
|
+
].join("\n"));
|
|
1904
|
+
return ExitCode.OK;
|
|
1905
|
+
}
|
|
1906
|
+
/** toPlan works out every file init writes, without writing any of them. */
|
|
1907
|
+
function toPlan(target, packageDirectory, mcpUrl) {
|
|
1908
|
+
const planned = [];
|
|
1909
|
+
const skills = join(packageDirectory, "skills");
|
|
1910
|
+
for (const skill of readdirSync(skills)) for (const file of toFiles(join(skills, skill))) {
|
|
1911
|
+
const content = readFileSync(file, "utf8");
|
|
1912
|
+
for (const place of SKILL_DIRECTORIES) planned.push(toFilePlan(target, join(place, skill, relative(join(skills, skill), file)), content));
|
|
1913
|
+
}
|
|
1914
|
+
const section = readFileSync(join(packageDirectory, "templates", "AGENTS.md"), "utf8").replaceAll("{{MCP_URL}}", mcpUrl);
|
|
1915
|
+
planned.push(toFilePlan(target, "AGENTS.md", toAgentsFile(join(target, "AGENTS.md"), section)));
|
|
1916
|
+
planned.push(toJSONPlan(target, ".mcp.json", (config) => toWithServer(config, "mcpServers", {
|
|
1917
|
+
type: "http",
|
|
1918
|
+
url: mcpUrl
|
|
1919
|
+
})), toJSONPlan(target, ".cursor/mcp.json", (config) => toWithServer(config, "mcpServers", { url: mcpUrl })), toJSONPlan(target, ".vscode/mcp.json", (config) => toWithServer(config, "servers", {
|
|
1920
|
+
type: "http",
|
|
1921
|
+
url: mcpUrl
|
|
1922
|
+
})));
|
|
1923
|
+
return planned;
|
|
1924
|
+
}
|
|
1925
|
+
/** toAgentsFile puts the Hardfin section into AGENTS.md, replacing the one an earlier run wrote. */
|
|
1926
|
+
function toAgentsFile(path, section) {
|
|
1927
|
+
const block = `${SECTION_START}\n${section.trimEnd()}\n${SECTION_END}\n`;
|
|
1928
|
+
if (!existsSync(path)) return `# Agent instructions\n\n${block}`;
|
|
1929
|
+
const current = readFileSync(path, "utf8");
|
|
1930
|
+
const start = current.indexOf(SECTION_START);
|
|
1931
|
+
const end = current.indexOf(SECTION_END);
|
|
1932
|
+
if (start >= 0 && end > start) return `${current.slice(0, start)}${block}${current.slice(end + 20).replace(/^\n/, "")}`;
|
|
1933
|
+
return `${current.trimEnd()}\n\n${block}`;
|
|
1934
|
+
}
|
|
1935
|
+
/** toWithServer adds the hardfin server to an MCP config, leaving every other server as it was. */
|
|
1936
|
+
function toWithServer(config, key, server) {
|
|
1937
|
+
const servers = typeof config[key] === "object" && config[key] !== null ? config[key] : {};
|
|
1938
|
+
return {
|
|
1939
|
+
...config,
|
|
1940
|
+
[key]: {
|
|
1941
|
+
...servers,
|
|
1942
|
+
hardfin: server
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
/** toFilePlan compares what a file holds with what init would write there. */
|
|
1947
|
+
function toFilePlan(target, path, content) {
|
|
1948
|
+
const full = join(target, path);
|
|
1949
|
+
const current = existsSync(full) ? readFileSync(full, "utf8") : void 0;
|
|
1950
|
+
if (current === content) return { change: {
|
|
1951
|
+
path,
|
|
1952
|
+
action: "unchanged"
|
|
1953
|
+
} };
|
|
1954
|
+
return {
|
|
1955
|
+
change: {
|
|
1956
|
+
path,
|
|
1957
|
+
action: current === void 0 ? "created" : "updated"
|
|
1958
|
+
},
|
|
1959
|
+
write: () => {
|
|
1960
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
1961
|
+
writeFileSync(full, content);
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
/** toJSONPlan changes a JSON config, and leaves alone one it cannot read rather than overwrite it. */
|
|
1966
|
+
function toJSONPlan(target, path, change) {
|
|
1967
|
+
const full = join(target, path);
|
|
1968
|
+
let config = {};
|
|
1969
|
+
if (existsSync(full)) try {
|
|
1970
|
+
const parsed = JSON.parse(readFileSync(full, "utf8"));
|
|
1971
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return { change: {
|
|
1972
|
+
path,
|
|
1973
|
+
action: "skipped",
|
|
1974
|
+
reason: "it does not hold a JSON object, so it was left alone"
|
|
1975
|
+
} };
|
|
1976
|
+
config = parsed;
|
|
1977
|
+
} catch {
|
|
1978
|
+
return { change: {
|
|
1979
|
+
path,
|
|
1980
|
+
action: "skipped",
|
|
1981
|
+
reason: "it does not hold JSON, so it was left alone"
|
|
1982
|
+
} };
|
|
1983
|
+
}
|
|
1984
|
+
return toFilePlan(target, path, `${JSON.stringify(change(config), null, 2)}\n`);
|
|
1985
|
+
}
|
|
1986
|
+
/** toFiles lists every file under a folder. */
|
|
1987
|
+
function toFiles(directory) {
|
|
1988
|
+
return readdirSync(directory).flatMap((name) => {
|
|
1989
|
+
const path = join(directory, name);
|
|
1990
|
+
return statSync(path).isDirectory() ? toFiles(path) : [path];
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
/** toPackageDirectory finds this package's own folder, which holds the skills and templates it ships. */
|
|
1994
|
+
function toPackageDirectory(from = dirname(fileURLToPath(import.meta.url))) {
|
|
1995
|
+
for (let directory = from;; directory = dirname(directory)) {
|
|
1996
|
+
const manifest = join(directory, "package.json");
|
|
1997
|
+
if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === "@hardfin/cli") return directory;
|
|
1998
|
+
if (dirname(directory) === directory) throw new Error("the @hardfin/cli package folder could not be found, so its skills cannot be copied");
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
//#endregion
|
|
1833
2002
|
//#region src/auth/device.ts
|
|
1834
2003
|
const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
1835
2004
|
/** The interval a server gives is in seconds, and five more are added on a slow_down. */
|
|
@@ -2731,7 +2900,7 @@ async function report(error, occasion, resolved) {
|
|
|
2731
2900
|
const settings = resolved ?? toResolved();
|
|
2732
2901
|
const status = settings ? await toStatus(settings) : void 0;
|
|
2733
2902
|
const secrets = [settings?.settings.apiKey, settings ? toCredential(settings.settings.issuerUrl)?.refreshToken : void 0];
|
|
2734
|
-
return await (await import("./sentry-
|
|
2903
|
+
return await (await import("./sentry-BM9Rvhhy.js")).send({
|
|
2735
2904
|
error,
|
|
2736
2905
|
occasion,
|
|
2737
2906
|
telemetry,
|
|
@@ -3493,7 +3662,9 @@ function toElements(flag, values) {
|
|
|
3493
3662
|
const key = pair.slice(0, split).trim();
|
|
3494
3663
|
if (flag.element && !flag.element.includes(key)) return /* @__PURE__ */ new Error(`--${flag.name} has no field ${key}. It takes ${flag.element.join(", ")}`);
|
|
3495
3664
|
if (key in element) return /* @__PURE__ */ new Error(`--${flag.name} names ${key} twice in one element, so which one it takes is not clear`);
|
|
3496
|
-
|
|
3665
|
+
const value = pair.slice(split + 1).trim();
|
|
3666
|
+
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`);
|
|
3667
|
+
element[key] = value;
|
|
3497
3668
|
}
|
|
3498
3669
|
elements.push(element);
|
|
3499
3670
|
}
|
|
@@ -3515,6 +3686,7 @@ const commands = [
|
|
|
3515
3686
|
loginCommand,
|
|
3516
3687
|
logoutCommand,
|
|
3517
3688
|
statusCommand,
|
|
3689
|
+
initCommand,
|
|
3518
3690
|
...[
|
|
3519
3691
|
{
|
|
3520
3692
|
name: "asset",
|
|
@@ -3925,6 +4097,11 @@ const commands = [
|
|
|
3925
4097
|
"shipAt",
|
|
3926
4098
|
"shipAtTimezone"
|
|
3927
4099
|
],
|
|
4100
|
+
elementUuids: [
|
|
4101
|
+
"assetId",
|
|
4102
|
+
"destinationId",
|
|
4103
|
+
"originId"
|
|
4104
|
+
],
|
|
3928
4105
|
schema: z.array(z.string())
|
|
3929
4106
|
}]
|
|
3930
4107
|
})]
|
|
@@ -3954,6 +4131,7 @@ const commands = [
|
|
|
3954
4131
|
"id",
|
|
3955
4132
|
"shipAt"
|
|
3956
4133
|
],
|
|
4134
|
+
elementUuids: ["assetId"],
|
|
3957
4135
|
schema: z.array(z.string())
|
|
3958
4136
|
}]
|
|
3959
4137
|
})]
|
|
@@ -4030,6 +4208,7 @@ const commands = [
|
|
|
4030
4208
|
valueName: "fieldId=,value=",
|
|
4031
4209
|
repeatable: true,
|
|
4032
4210
|
element: ["fieldId", "value"],
|
|
4211
|
+
elementUuids: ["fieldId"],
|
|
4033
4212
|
schema: z.array(z.string())
|
|
4034
4213
|
},
|
|
4035
4214
|
{
|
|
@@ -5373,6 +5552,7 @@ const commands = [
|
|
|
5373
5552
|
"order",
|
|
5374
5553
|
"section"
|
|
5375
5554
|
],
|
|
5555
|
+
elementUuids: ["fieldId"],
|
|
5376
5556
|
schema: z.array(z.string())
|
|
5377
5557
|
},
|
|
5378
5558
|
{
|
|
@@ -6309,5 +6489,5 @@ async function toReported(error, isJSON, resolved, level = "error") {
|
|
|
6309
6489
|
//#endregion
|
|
6310
6490
|
export { toCli, toParseExitCode, toReported, wasReportAttempted };
|
|
6311
6491
|
|
|
6312
|
-
//# sourceMappingURL=program-
|
|
6313
|
-
//# debugId=
|
|
6492
|
+
//# sourceMappingURL=program-DtqGeehp.js.map
|
|
6493
|
+
//# debugId=03b49867-f2d5-5a64-af87-e45e530711a1
|
|
@@ -1,10 +1,10 @@
|
|
|
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]="35c80804-dcd8-5662-9f00-9c8ff04f286a")}catch(e){}}();
|
|
4
4
|
import { n as toScrubbed } from "./scrub-BN3gyBpR.js";
|
|
5
5
|
import { dirname } from "node:path";
|
|
6
|
-
import { request } from "node:http";
|
|
7
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { request } from "node:http";
|
|
8
8
|
import { request as request$1 } from "node:https";
|
|
9
9
|
//#region node_modules/@sentry/core/build/esm/debug-build.js
|
|
10
10
|
const DEBUG_BUILD = typeof __SENTRY_DEBUG__ === "undefined" || __SENTRY_DEBUG__;
|
|
@@ -3925,7 +3925,7 @@ function trim(arr) {
|
|
|
3925
3925
|
if (start > end) return [];
|
|
3926
3926
|
return arr.slice(start, end - start + 1);
|
|
3927
3927
|
}
|
|
3928
|
-
function relative(from, to) {
|
|
3928
|
+
function relative$1(from, to) {
|
|
3929
3929
|
from = resolve$1(from).slice(1);
|
|
3930
3930
|
to = resolve$1(to).slice(1);
|
|
3931
3931
|
const fromParts = trim(from.split("/"));
|
|
@@ -4001,7 +4001,7 @@ function generateIteratee({ isBrowser, root, prefix }) {
|
|
|
4001
4001
|
}
|
|
4002
4002
|
} else if (isWindowsFrame || startsWithSlash) {
|
|
4003
4003
|
const filename = isWindowsFrame ? frame.filename.replace(/^[a-zA-Z]:/, "").replace(/\\/g, "/") : frame.filename;
|
|
4004
|
-
frame.filename = `${prefix}${root ? relative(root, filename) : basename$1(filename)}`;
|
|
4004
|
+
frame.filename = `${prefix}${root ? relative$1(root, filename) : basename$1(filename)}`;
|
|
4005
4005
|
}
|
|
4006
4006
|
return frame;
|
|
4007
4007
|
};
|
|
@@ -4385,5 +4385,5 @@ function toTags(delivery) {
|
|
|
4385
4385
|
//#endregion
|
|
4386
4386
|
export { send };
|
|
4387
4387
|
|
|
4388
|
-
//# sourceMappingURL=sentry-
|
|
4389
|
-
//# debugId=
|
|
4388
|
+
//# sourceMappingURL=sentry-BM9Rvhhy.js.map
|
|
4389
|
+
//# debugId=35c80804-dcd8-5662-9f00-9c8ff04f286a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hardfin/cli",
|
|
3
|
-
"version": "0.1.0-dev.
|
|
3
|
+
"version": "0.1.0-dev.36",
|
|
4
4
|
"description": "Command line interface for the Hardfin API",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Hardfin, Inc.",
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
|
-
"!dist/**/*.map"
|
|
27
|
+
"!dist/**/*.map",
|
|
28
|
+
"skills",
|
|
29
|
+
"templates"
|
|
28
30
|
],
|
|
29
31
|
"engines": {
|
|
30
32
|
"node": ">=20"
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hardfin-cli
|
|
3
|
+
description: Read and change records in a Hardfin organization with the Hardfin CLI (`hardfin`) - assets, customers, items, locations, files, ownership and moves. Use when the person asks to look up, import, create, update, move, scrap or archive Hardfin data, to sign in to Hardfin, or to check what the CLI is connected to. Not for writing application code that calls the Hardfin API over HTTP; read https://docs.hardfin.com for that.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Hardfin CLI
|
|
7
|
+
|
|
8
|
+
The `hardfin` command reads and changes the records in one Hardfin organization through the
|
|
9
|
+
Hardfin API. Every API operation is a command, named `hardfin <noun> <verb>`.
|
|
10
|
+
|
|
11
|
+
This skill gives the patterns. The installed CLI is the authority on exact commands and
|
|
12
|
+
flags, so check `--help` before relying on a flag this skill does not show.
|
|
13
|
+
|
|
14
|
+
## 1. Make sure the CLI is installed
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
hardfin --version || npm install -g @hardfin/cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
It needs Node 20 or later. If `npm install -g` is refused, run commands as
|
|
21
|
+
`npx -y @hardfin/cli <command>` instead.
|
|
22
|
+
|
|
23
|
+
## 2. Make sure it is signed in
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
hardfin status --offline --json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`credential.kind` is `none` when nobody is signed in. Then sign in one of three ways.
|
|
30
|
+
|
|
31
|
+
| Situation | Do this |
|
|
32
|
+
| ---------------------------------------------------------- | ------------------------------------------------ |
|
|
33
|
+
| You run on the person's own machine | Run `hardfin login` in the foreground |
|
|
34
|
+
| You run in a cloud sandbox, away from the person's browser | Run `hardfin login --device` |
|
|
35
|
+
| The person gave you an API key | Pass `--api-key <key>`, or set `HARDFIN_API_KEY` |
|
|
36
|
+
|
|
37
|
+
`hardfin login` writes one JSON line on stderr before it waits:
|
|
38
|
+
|
|
39
|
+
- `{"event": "sign_in_url", "url": …, "browserOpened": …}`: show the person `url`, and say
|
|
40
|
+
whether a browser tab opened
|
|
41
|
+
- `{"event": "device_code", "userCode": …, "verificationUri": …}`: show the person the code
|
|
42
|
+
and where to enter it
|
|
43
|
+
|
|
44
|
+
It then waits up to `waitSeconds` (300) for the person to approve. Give the command that
|
|
45
|
+
long to finish. Do not run it in the background and move on.
|
|
46
|
+
|
|
47
|
+
Never write an API key into a file in the repository, and never echo it back.
|
|
48
|
+
|
|
49
|
+
## 3. Find the command
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
hardfin --help # the nouns
|
|
53
|
+
hardfin asset --help # what you can do with assets
|
|
54
|
+
hardfin asset create --help # every flag that command takes
|
|
55
|
+
hardfin agent-guide # every command, flag, exit code and error code, as Markdown
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Noun | Holds |
|
|
59
|
+
| ------------------ | ---------------------------------------------------------------------------------- |
|
|
60
|
+
| `asset` | Physical assets, with their accounting, events, files, moves, ownership and status |
|
|
61
|
+
| `customer` | The customers assets are sold or leased to |
|
|
62
|
+
| `item` | The catalogue of item types an asset is an instance of |
|
|
63
|
+
| `location` | Warehouses, sites and their zones |
|
|
64
|
+
| `file`, `url-link` | Attachments, and links to records elsewhere |
|
|
65
|
+
| `token` | What the current credential is |
|
|
66
|
+
| `api` | Any endpoint directly, when no command fits |
|
|
67
|
+
|
|
68
|
+
## 4. Keys and IDs
|
|
69
|
+
|
|
70
|
+
A record has both, and they are not interchangeable.
|
|
71
|
+
|
|
72
|
+
| | Looks like | Used for |
|
|
73
|
+
| --- | ----------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
74
|
+
| Key | `ast_4f9xk2mq7plr8stz`, `item_V1StGXR8Z5jdHi6B` | The positional argument of `get`, `update` and nested commands |
|
|
75
|
+
| ID | `0192f7d4-3b6a-7c1e-9f2d-5a8b4c6e7d70` | A field that points at another record, such as `--customer-id` or `--move assetId=…` |
|
|
76
|
+
|
|
77
|
+
A list or get answers with both. Take the one the next command asks for.
|
|
78
|
+
|
|
79
|
+
## 5. Read
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
hardfin asset list --limit 20 --search-query "RX-200"
|
|
83
|
+
hardfin asset get ast_4f9xk2mq7plr8stz
|
|
84
|
+
hardfin customer list --page 2 --limit 100
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Output is JSON on stdout whenever stdout is not a terminal, so pipe it to `jq` or parse it.
|
|
88
|
+
A list holds at most 100 records a page. Fetch every page before saying how many there are.
|
|
89
|
+
|
|
90
|
+
## 6. Change
|
|
91
|
+
|
|
92
|
+
Before any change:
|
|
93
|
+
|
|
94
|
+
1. Read the record you are about to change, and work from what it says now
|
|
95
|
+
2. For more than a few records, or anything that deletes, scraps or archives, tell the person
|
|
96
|
+
what will change and wait for them to agree
|
|
97
|
+
3. Make the change
|
|
98
|
+
4. Read the record back to confirm it
|
|
99
|
+
|
|
100
|
+
Set fields with one flag each, or send the whole body as JSON with `--input`:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
hardfin customer create --name "Acme"
|
|
104
|
+
echo '{"name": "Dock 4, north side"}' | hardfin location create --input -
|
|
105
|
+
hardfin asset accounting update ast_4f9xk2mq7plr8stz --unset useful-life
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
A flag given with `--input` replaces that field of the body. A value with a comma in it
|
|
109
|
+
must go through `--input`, because a `key=value` flag cannot carry one.
|
|
110
|
+
|
|
111
|
+
## 7. When a command fails
|
|
112
|
+
|
|
113
|
+
The exit code says what kind of failure it was.
|
|
114
|
+
|
|
115
|
+
| Exit code | Means | Do this |
|
|
116
|
+
| --------- | --------------------------------- | ---------------------------------------------- |
|
|
117
|
+
| 0 | It worked | |
|
|
118
|
+
| 1 | The API or the network refused it | Read the error's `code` |
|
|
119
|
+
| 2 | The command was called wrongly | Run the command with `--help` and fix the call |
|
|
120
|
+
| 4 | No usable credential | Sign in, as in step 2 |
|
|
121
|
+
|
|
122
|
+
The error is JSON on stderr. Branch on its `code`, never on the message.
|
|
123
|
+
|
|
124
|
+
| `code` | Do this |
|
|
125
|
+
| --------------------------------------------------- | ---------------------------------------------------------------- |
|
|
126
|
+
| `not_signed_in`, `unauthorized` | Sign in again |
|
|
127
|
+
| `invalid_flag`, `missing_flag`, `invalid_arguments` | Read `--help` for that command |
|
|
128
|
+
| `not_found` | Check you passed a key, not an ID, where the command wants a key |
|
|
129
|
+
| `bad_request`, `invalid_request`, `conflict` | Read `errors`, which names the field |
|
|
130
|
+
| `rate_limited` | Wait a minute, then retry once |
|
|
131
|
+
| `unreachable`, `server_error` | Tell the person, and include `requestId` if there is one |
|
|
132
|
+
|
|
133
|
+
`hardfin agent-guide` lists every code.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## Hardfin
|
|
2
|
+
|
|
3
|
+
This project works with Hardfin data through the Hardfin CLI, `hardfin`.
|
|
4
|
+
|
|
5
|
+
| Task | Command |
|
|
6
|
+
| ---------------------------------- | ---------------------------------------------------------------------------- |
|
|
7
|
+
| Install | `npm install -g @hardfin/cli`, with Node 20 or later |
|
|
8
|
+
| Sign in | `hardfin login`, or `--device` away from a browser, or set `HARDFIN_API_KEY` |
|
|
9
|
+
| Check what it is connected to | `hardfin status` |
|
|
10
|
+
| Find a command | `hardfin --help`, then `hardfin <noun> --help` |
|
|
11
|
+
| Every command, flag and error code | `hardfin agent-guide` |
|
|
12
|
+
| The API reference | https://docs.hardfin.com |
|
|
13
|
+
|
|
14
|
+
Start with the `hardfin-cli` skill in `.agents/skills/hardfin-cli/SKILL.md`.
|
|
15
|
+
|
|
16
|
+
The Hardfin MCP server, `{{MCP_URL}}`, is configured for Claude Code in `.mcp.json`, for
|
|
17
|
+
Cursor in `.cursor/mcp.json`, and for VS Code in `.vscode/mcp.json`. For Codex, add
|
|
18
|
+
`[mcp_servers.hardfin]` with `url = "{{MCP_URL}}"` to `~/.codex/config.toml`.
|
|
19
|
+
|
|
20
|
+
Rules:
|
|
21
|
+
|
|
22
|
+
- Read a record before changing it, and read it back afterwards
|
|
23
|
+
- Ask the person before a bulk change, or anything that deletes, scraps or archives
|
|
24
|
+
- Never write an API key into a file in this repository
|
|
25
|
+
- Branch on an error's `code`, never on its message
|