@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 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 cst_a1 --date 2026-09-23 --sale-price 1500.00
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]="f2529e6e-d7d4-5fe2-b460-7e86ca865c25")}catch(e){}}();
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-DTsxnjRj.js");
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-DTsxnjRj.js");
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=f2529e6e-d7d4-5fe2-b460-7e86ca865c25
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]="c7f8ce05-d638-5d0f-a088-655ddade6f45")}catch(e){}}();
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 (path.split("/").includes("..")) {
1161
- writeFailure("invalid_arguments", "a path stays inside the API, so it cannot contain ..", input.isJSON);
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-CLaeAWNa.js")).send({
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
- element[key] = pair.slice(split + 1).trim();
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-DTsxnjRj.js.map
6313
- //# debugId=c7f8ce05-d638-5d0f-a088-655ddade6f45
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]="853940c3-9e72-566b-ad70-9f043e6aba8d")}catch(e){}}();
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-CLaeAWNa.js.map
4389
- //# debugId=853940c3-9e72-566b-ad70-9f043e6aba8d
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.34",
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