@hardfin/cli 0.1.0-dev.35 → 0.1.0-dev.37

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
@@ -307,6 +307,44 @@ 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
+ ## Types for code that calls the API
311
+
312
+ ```sh
313
+ hardfin types --output src/hardfin-api.d.ts
314
+ ```
315
+
316
+ `hardfin types` prints a TypeScript declaration for every record the API sends and takes,
317
+ plus `ApiEnvelope<T>` for the response wrapper. The declarations are generated from the
318
+ vendored API document by `scripts/generate-surface.mjs`, alongside the commands, so they
319
+ match the API version this CLI calls. CI fails a pull request whose generated commands or
320
+ types are stale.
321
+
322
+ ## Setting up a project for agents
323
+
324
+ ```sh
325
+ hardfin init # the working directory
326
+ hardfin init --dry-run # what it would write, writing nothing
327
+ ```
328
+
329
+ `hardfin init` gets a project ready for a coding agent to use Hardfin, the way EmDash's starter
330
+ projects come ready. Running it again updates only what it wrote.
331
+
332
+ | File | What init writes |
333
+ | ------------------------------------- | ------------------------------------------------------------------- |
334
+ | `.agents/skills/hardfin-cli/SKILL.md` | The `hardfin-cli` skill, which Codex and most agents read |
335
+ | `.claude/skills/hardfin-cli/SKILL.md` | The same skill, where Claude Code reads it |
336
+ | `AGENTS.md` | A Hardfin section between `hardfin:start` and `hardfin:end` markers |
337
+ | `.mcp.json` | The `hardfin` server, for Claude Code |
338
+ | `.cursor/mcp.json` | The `hardfin` server, for Cursor |
339
+ | `.vscode/mcp.json` | The `hardfin` server, for VS Code |
340
+
341
+ The server is the hosted one at the authorization server's host, `https://api.hardfin.com/mcp`
342
+ by default. Every other server in an MCP config, and everything in `AGENTS.md` outside the
343
+ markers, is left as it was. A config that does not hold JSON is skipped, never overwritten.
344
+
345
+ The skill lives in `skills/` and the section in `templates/AGENTS.md`. Both ship in the npm
346
+ package, so `init` writes the copy that matches the installed CLI.
347
+
310
348
  ## Serving an agent
311
349
 
312
350
  ```sh
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]="e0ebce65-52b6-5732-b4b2-1becfe72bdef")}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]="b1580db0-6822-5a25-b000-03705726eaf6")}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-BdRDIa3u.js");
17
+ const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-CjKwnPeY.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-BdRDIa3u.js");
36
+ const { toReported } = await import("./program-CjKwnPeY.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=e0ebce65-52b6-5732-b4b2-1becfe72bdef
54
+ //# debugId=b1580db0-6822-5a25-b000-03705726eaf6
@@ -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",
@@ -2553,6 +2572,24 @@
2553
2572
  ],
2554
2573
  "subcommands": []
2555
2574
  },
2575
+ {
2576
+ "name": "types",
2577
+ "summary": "Print TypeScript types for the Hardfin API",
2578
+ "runs": true,
2579
+ "flags": [
2580
+ {
2581
+ "name": "output",
2582
+ "description": "Where to write the declarations, instead of standard output",
2583
+ "valueName": "path"
2584
+ },
2585
+ {
2586
+ "name": "json",
2587
+ "description": "Print machine-readable output, which is the default when stdout is not a terminal"
2588
+ }
2589
+ ],
2590
+ "arguments": [],
2591
+ "subcommands": []
2592
+ },
2556
2593
  {
2557
2594
  "name": "config",
2558
2595
  "summary": "Print what this invocation talks to, and where each value came from",
@@ -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]="6057a656-73c2-55b1-9f09-873e3b83b368")}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]="5a591d32-0d31-5357-b447-e33420753351")}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
@@ -1486,7 +1486,7 @@ const uninstallCommand = defineCommand({
1486
1486
  * directory by itself, and every other shell is told to load the script from its startup
1487
1487
  * file, because not every bash has bash-completion to load it lazily.
1488
1488
  */
1489
- function toPlan(shell, place) {
1489
+ function toPlan$1(shell, place) {
1490
1490
  const content = `${toScript(shell)}\n`;
1491
1491
  if (shell === "fish") {
1492
1492
  const config = place.env["XDG_CONFIG_HOME"] || join(place.home, ".config");
@@ -1569,7 +1569,7 @@ function toShell(place) {
1569
1569
  }
1570
1570
  /** isInstalled answers whether install has written a script for any shell. */
1571
1571
  function isInstalled(place = toPlace()) {
1572
- return SHELLS.some((shell) => existsSync(toPlan(shell, {
1572
+ return SHELLS.some((shell) => existsSync(toPlan$1(shell, {
1573
1573
  ...place,
1574
1574
  toProfile: () => void 0
1575
1575
  }).script.path));
@@ -1602,7 +1602,7 @@ async function runChange(input, change) {
1602
1602
  writeFailure("invalid_arguments", `name the shell to ${change} for: ${SHELLS.join(", ")}`, input.isJSON);
1603
1603
  return ExitCode.USAGE;
1604
1604
  }
1605
- const plan = toPlan(shell, place);
1605
+ const plan = toPlan$1(shell, place);
1606
1606
  if (change === "install" && shell === "powershell" && !plan.startup) {
1607
1607
  writeFailure("shell_unavailable", "PowerShell did not report its profile, so there is nowhere to load the script from", input.isJSON);
1608
1608
  return ExitCode.ERROR;
@@ -1841,6 +1841,208 @@ const crashCommand = defineCommand({
1841
1841
  run: () => Promise.reject(/* @__PURE__ */ new TypeError("hardfin __crash failed on purpose, to check that defects are reported"))
1842
1842
  });
1843
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
2002
+ //#region src/command/types.generated.ts
2003
+ /** API_TYPES is the TypeScript hardfin types prints. */
2004
+ const API_TYPES = "// Types for the Hardfin API, version 2026-09-17, generated from its published document.\n// Regenerate them with hardfin types after upgrading the CLI.\n\n/** ApiError is one entry in a response's errors. */\nexport interface ApiError {\n error: string;\n statusCode: number;\n}\n\n/** ApiEnvelope is the shape every response takes, with the record or records in data. */\nexport interface ApiEnvelope<T> {\n data: T;\n metadata: {\n errors?: ApiError[] | null;\n requestId?: string;\n requestTime?: string;\n statusCode?: number;\n pagination?: unknown;\n };\n}\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type ArchivedState = \"all\" | \"false\" | \"true\";\n\nexport type Asset = SparseAsset & AssetLocationData & {\n /** The asset's cost basis, depreciation, and accounting history. */\n accounting?: AssetAccountingDetails;\n /** The asset's recent events, such as shipments and deliveries. */\n activity?: AssetEvent[];\n /** A free-form description of the asset. */\n description?: string | null;\n /** The number of files attached to the asset. */\n fileCount?: number;\n /** The day the asset entered inventory. */\n inInventoryDate?: string;\n /** The asset's value for each custom field its item defines. */\n metadata?: AssetMetadataFieldWrapper[];\n /** Who owns the asset now. */\n ownership?: AssetOwnership;\n /** The project the asset is assigned to, or null when it is unassigned. */\n project?: SparseProject | null;\n /** How and when the asset was scrapped, or null when it has not been. */\n scrappedContext?: AssetScrappedContext | null;\n /** The number of tickets about the asset. */\n ticketCount?: number;\n /** The number of links attached to the asset. */\n urlLinkCount?: number;\n};\n\nexport interface AssetAccountingDetails {\n /** The asset's inventory cost basis, before deployment costs, which is what inventory is relieved by when the asset leaves it. */\n acquisitionCogs?: string | null;\n /** The asset's share of overhead. */\n allocatedIndirect?: string | null;\n /** The problems that keep the asset's accounting from being computed, such as a missing cost basis. */\n assetExceptions?: AssetExceptionType[];\n /** The parts cost. */\n billOfMaterials?: string | null;\n /** The cost basis on the books now, the acquisition basis in inventory and the deployed basis once the asset is a fixed asset. */\n capitalizedCosts?: string | null;\n /** The capitalizations and impairments made to the asset's cost after it was acquired. */\n costAdjustments?: AssetCostAdjustment[];\n /** The method the asset is depreciated by. */\n depreciationModel?: Depreciation;\n /** The asset's book value month by month, from its inventory month, or null while it has an accounting exception. */\n depreciationPeriods?: AssetDepreciationChartPeriod[] | null;\n /** The labor cost to build the asset. */\n directLabor?: string | null;\n /** The shipping cost to receive the asset. */\n freightInbound?: string | null;\n /** The shipping cost to deploy the asset, a deployment cost. */\n freightOutbound?: string | null;\n /** The day the asset was put into service and began depreciating. */\n inServiceDate?: string | null;\n /** The cost to install the asset, a deployment cost. */\n installation?: string | null;\n /** The financing cost of the asset. */\n interest?: string | null;\n /** Whether Hardfin sets the in-service date itself rather than keeping one a person entered. */\n isInServiceDateManagedAutomatically?: boolean;\n /** The value the asset keeps at the end of its useful life. */\n salvageValue?: string | null;\n /** A single cost entered in place of the cost components. */\n simpleCostBasis?: string | null;\n /** The import duty paid on the asset. */\n tariffs?: string | null;\n /** The tax paid on the asset. */\n tax?: string | null;\n /** The deployed cost basis, including deployment costs, which is what is capitalized once the asset is put into service. */\n totalPotentialCOGS?: string | null;\n /** The number of months the asset is depreciated over. */\n usefulLife?: number | null;\n /** The changes made to the asset's useful life. */\n usefulLifeRevisions?: AssetUsefulLifeRevision[];\n}\n\nexport interface AssetCostAdjustment {\n /** CAPITALIZATION to add to the cost basis, or IMPAIRMENT to write it down. */\n adjustmentType?: CostAdjustmentType;\n /** The size of the adjustment. */\n amount?: string;\n /** When the adjustment was recorded. */\n createdAt?: string;\n /** The person who recorded the adjustment. */\n createdBy?: SparsePerson;\n /** The day the adjustment takes effect. */\n effectiveDate?: string;\n /** The cost adjustment's key. */\n key?: string;\n /** Free-form detail about the adjustment. */\n notes?: string | null;\n /** Why the cost changed. */\n reason?: CostAdjustmentReason;\n}\n\nexport interface AssetDepreciationChartPeriod {\n /** The depreciation charged from the in-service date through the end of the period. */\n accumulated?: string;\n /** The asset's value on the books at the end of the period. */\n bookValue?: string;\n /** The last day of the month the period covers. */\n date?: string;\n /** The depreciation charged in the period. */\n depreciation?: string;\n}\n\nexport interface AssetEvent {\n /** When the event was recorded. */\n createdAt?: string;\n /** The IANA time zone the event's times were entered in. */\n eventTimezone?: string;\n /** When the event is expected to happen, or null when no one has said. */\n expectedAt?: string | null;\n /** The event's unique identifier. */\n id?: string;\n /** Whether the event has to happen for its group to complete. */\n isRequired?: boolean;\n /** When the event happened, or null while it has not. */\n occurredAt?: string | null;\n /** The day the event was promised to a customer, or null when none was. */\n promiseDate?: string | null;\n /** The event's values a person set, such as carrier tracking. */\n properties?: AssetEventProperty[];\n /** The event's position within its group. */\n sequence?: number;\n /** When someone marked the event as happened in Hardfin, or null while it has not happened. */\n triggeredAt?: string | null;\n /** What kind of event this is. */\n type?: AssetEventType;\n}\n\nexport interface AssetEventConfiguration {\n /** The properties an event of the type carries. */\n properties?: AssetEventProperty[];\n /** The kind of event the configuration describes. */\n type?: AssetEventType;\n}\n\nexport interface AssetEventGroup {\n /** The kind of group this is, and the events it carries. */\n configuration?: AssetEventGroupConfiguration;\n /** Where the movement ends, or null when the group does not move the asset. */\n destination?: SparseLocationWithParent | null;\n /** The events the group has recorded so far. */\n events?: AssetEvent[];\n /** The event group's unique identifier. */\n id?: string;\n /** The event group's key, which the event group endpoints take as eventGroupKey. */\n key?: string;\n /** Where the movement started, or null when the group does not move the asset. */\n origin?: SparseLocationWithParent | null;\n /** The shipment the movement travels on, or null when it travels on none. */\n shipment?: SparseShipment | null;\n}\n\nexport interface AssetEventGroupConfiguration {\n /** The events a group of the kind carries, in the order they happen. */\n events?: AssetEventConfiguration[];\n /** The configuration's unique identifier. */\n id?: string;\n /** The configuration's key. */\n key?: string;\n /** What the configuration is called, such as ship and deliver. */\n name?: string;\n}\n\nexport interface AssetEventProperty {\n /** The event property's unique identifier. */\n id?: string;\n /** Whether Hardfin sets the property itself rather than a person, which responses leave out. */\n isSystemProperty?: boolean;\n /** The property's display name. */\n label?: string;\n /** The kind of value the property holds, such as CARRIER_TRACKING or CACHED_LOCATION. */\n type?: string;\n /** The property's value, whose JSON shape depends on the property's type. */\n value?: unknown[] | boolean | number | Record<string, unknown> | string | null;\n}\n\nexport interface AssetEventType {\n /** The event type's unique identifier. */\n id?: string;\n /** The event type's name, such as Ship or Deliver. */\n name?: string;\n /** Whether an event of this type only happens as part of a group, such as a shipment. */\n requiresEventGroup?: boolean;\n}\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type AssetExceptionType = \"DEPLOYMENT_WITHOUT_ACQUISITION\" | \"DISPOSAL_DATE_BEFORE_INVENTORY\" | \"FIRST_ASSIGNMENT_BEFORE_INVENTORY\" | \"FIRST_MOVEMENT_BEFORE_INVENTORY\" | \"FIRST_SALE_DATE_BEFORE_INVENTORY\" | \"IN_SERVICE_DATE_BEFORE_INVENTORY\" | \"MISSING_COST_BASIS\" | \"MISSING_USEFUL_LIFE\" | \"SALVAGE_EXCEEDS_50_PERCENT\" | \"UNKNOWN\";\n\nexport interface AssetFile {\n /** The file's unique identifier. */\n id?: string;\n /** Whether the file is readable with any organization's API key. */\n isPublic?: boolean;\n /** The file's key, which the file endpoints take as fileKey. */\n key?: string;\n /** The file's media type, or an empty string when the upload named none. */\n mimeType?: string;\n /** The name the file had when it was uploaded. */\n name?: string;\n /** The file's size in bytes. */\n size?: number;\n /** The file type the file was uploaded as. */\n type?: string;\n /** When the file was uploaded. */\n uploadedAt?: string;\n}\n\n/** Whether an asset works, is broken, awaits review, or has been scrapped. Accepted in any case. The listed spelling is the one the API sends back. */\nexport type AssetFunctionalStatus = \"FUNCTIONAL\" | \"NEEDS_REVIEW\" | \"NON-FUNCTIONAL\" | \"SCRAPPED\";\n\nexport interface AssetFunctionalStatusHistoryItem {\n /** When the asset left the status, or null while it still holds it. */\n end?: string | null;\n /** When the asset took the status. */\n start?: string;\n /** Whether the asset worked, was broken, awaited review, or had been scrapped through the period. */\n status?: AssetFunctionalStatus;\n}\n\nexport interface AssetListing {\n /** The page of records. */\n results?: Asset[];\n /** The number of records that match the filters, across every page. */\n totalCount?: number;\n}\n\nexport interface AssetLocationData {\n /** Where the asset is, or null while it is in transit. */\n currentLocation?: SparseLocationWithParent | null;\n /** Whether the asset has shipped and not yet been delivered. */\n inTransit?: boolean;\n /** Where a planned or in-progress move takes the asset, or null when none is planned. */\n nextLocation?: SparseLocationWithParent | null;\n /** The location the asset was at before its current one, or null when it has not moved. */\n previousLocation?: SparseLocationWithParent | null;\n}\n\nexport interface AssetMetadataField {\n /** The item field's unique identifier. */\n id?: string;\n /** The item field's key. */\n key?: string;\n /** The field's display name. */\n label?: string;\n /** The field's position within its section, starting at 0. */\n order?: number;\n /** The group the field is shown in, starting at 0. */\n section?: number;\n /** The kind of value the field holds, which decides the JSON type of its value. */\n type?: ItemFieldType;\n}\n\nexport type AssetMetadataFieldWrapper = {\n field?: {\n type?: \"BOOLEAN\";\n };\n value?: boolean | null;\n} | {\n field?: {\n type?: \"DATE\";\n };\n value?: string | null;\n} | {\n field?: {\n type?: \"DATE_TIME\";\n };\n value?: string | null;\n} | {\n field?: {\n type?: \"INTEGER\";\n };\n value?: number | null;\n} | {\n field?: {\n type?: \"MULTILINE_TEXT\";\n };\n value?: string | null;\n} | {\n field?: {\n type?: \"NUMBER\";\n };\n value?: number | null;\n} | {\n field?: {\n type?: \"TEXT\";\n };\n value?: string | null;\n} | {\n field?: {\n type?: \"TIME\";\n };\n value?: string | null;\n};\n\nexport interface AssetMoveToPlan {\n /** The ID of the asset to move. */\n assetId?: string;\n /** When the asset reaches its destination, or null when the move only ships. */\n deliverAt?: string | null;\n /** The client's own name for the move, which the plan echoes back beside its answer. */\n id?: string;\n /** When the asset leaves its origin, or null when the move only delivers. */\n shipAt?: string | null;\n}\n\nexport interface AssetOwnership {\n /** The customer that owns the asset, or null when your organization owns it. */\n customer?: SparseCustomer | null;\n /** The day the asset's current owner took ownership, or null when its ownership has never changed. */\n ownershipChangeDate?: string | null;\n}\n\nexport interface AssetOwnershipHistoryItem {\n /** When the segment was recorded. */\n createdAt?: string;\n /** The customer that owned the asset during the segment, or null when your organization did. */\n customer?: SparseCustomer | null;\n /** The day the segment's owner took ownership. */\n date?: string;\n /** The ownership segment's unique identifier. */\n id?: string;\n /** The ownership segment's key, which the segment endpoints take as segmentKey. */\n key?: string;\n /** When the segment was last changed. */\n lastModifiedAt?: string;\n /** What the owner paid for the asset, or null when no price was recorded. */\n salePrice?: string | null;\n}\n\nexport interface AssetPlannedIntersection {\n /** The ID of the event group the move would collide with, or null when none names one. */\n aegId?: string | null;\n /** The key of the event group the move would collide with, or null when none names one. */\n aegKey?: string | null;\n /** When the colliding movement ends, or null while it has no end. */\n end?: string | null;\n /** When the colliding movement starts. */\n start?: string;\n}\n\nexport interface AssetPlannedMove {\n /** The client's own name for the move, echoed from the plan it asked about. */\n id?: string;\n /** The movement the move would collide with, or null when it collides with none. */\n intersection?: AssetPlannedIntersection | null;\n /** Whether the move can be carried out as asked. */\n isValid?: boolean;\n}\n\nexport interface AssetProposedMultiMove {\n /** The ID of the asset to move. */\n assetId?: string;\n /** When the asset reaches its destination, which a move sends unless it ships. */\n deliverAt?: string | null;\n /** The timezone the delivery time was entered in, or null for the organization's. */\n deliverAtTimezone?: string | null;\n /** The ID of the location the asset arrives at. */\n destinationId?: string;\n /** The ID of the location the asset leaves. */\n originId?: string;\n /** When the asset leaves its origin, which a move sends unless it delivers. */\n shipAt?: string | null;\n /** The timezone the ship time was entered in, or null for the organization's. */\n shipAtTimezone?: string | null;\n}\n\nexport interface AssetScrappedContext {\n /** The day the asset was scrapped. */\n disposalDate?: string;\n /** What the scrapped asset was sold for, or null when it was not sold. */\n disposalPrice?: string | null;\n /** Why the asset was scrapped. */\n disposalReason?: string | null;\n}\n\nexport interface AssetUpdateMetadataItem {\n /** The ID of the item field to set on the asset. */\n fieldId?: string;\n /** The field's new value, in the JSON type the field's type calls for, or null to clear it. */\n value?: unknown[] | boolean | number | Record<string, unknown> | string | null;\n}\n\nexport interface AssetUsefulLifeRevision {\n /** When the revision was recorded. */\n createdAt?: string;\n /** The person who recorded the revision. */\n createdBy?: SparsePerson;\n /** The day the new useful life takes effect. */\n effectiveDate?: string;\n /** The revision's key. */\n key?: string;\n /** Free-form detail about the revision. */\n notes?: string | null;\n /** Why the useful life changed. */\n reason?: UsefulLifeRevisionReason;\n /** The asset's new useful life in months. */\n usefulLifeMonths?: number;\n}\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type BillingFrequency = \"ANNUALLY\" | \"BIENNIALLY\" | \"BIWEEKLY\" | \"DAILY\" | \"MONTHLY\" | \"ONCE\" | \"QUADRENNIALLY\" | \"QUARTERLY\" | \"QUINQUENNIALLY\" | \"SEMIANNUALLY\" | \"TRIENNIALLY\" | \"WEEKLY\";\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type BillingSchedule = \"MILESTONE\" | \"PERIODIC\";\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type BillingTiming = \"ADVANCE\" | \"ARREARS\";\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type CostAdjustmentReason = \"ADDITION\" | \"BETTERMENT\" | \"DAMAGE\" | \"INSTALLATION\" | \"LIFE_EXTENSION\" | \"MARKET_DECLINE\" | \"OBSOLESCENCE\" | \"OTHER\" | \"REGULATORY\";\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type CostAdjustmentType = \"CAPITALIZATION\" | \"IMPAIRMENT\";\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type CostBasisMode = \"ADVANCED\" | \"SIMPLE\" | null;\n\n/** The kind of credential a request was made with. Accepted in any case. The listed spelling is the one the API sends back. | Value | Description | | --- | --- | | `API_KEY` | An API key, for an integration that runs unattended | | `OAUTH` | An OAuth access token, issued to an application a person connected | | `SESSION` | A person signed in to Hardfin in a browser | */\nexport type CredentialType = \"API_KEY\" | \"OAUTH\" | \"SESSION\";\n\nexport interface Currency {\n /** The value as a decimal string with two decimal places. */\n amount?: string;\n /** The value's ISO 4217 currency code. */\n isoCode?: string;\n}\n\nexport type Customer = SparseCustomer & {\n /** The number of the customer's active projects. */\n activeProjectCt?: number;\n /** The number of assets the customer owns. */\n assetCount?: number;\n /** The number of assets at the customer's locations. */\n assetsAtLocation?: number;\n /** The address invoices are sent to. */\n billingAddress?: string | null;\n /** The person who receives the customer's invoices. */\n billingContact?: CustomerBillingContact | null;\n /** A free-form note about the customer. */\n comment?: string | null;\n /** The customer's web domain, used to look up its logo. */\n domain?: string | null;\n /** The customer's identifier in another system. */\n externalId?: string | null;\n /** The number of the customer's incomplete projects. */\n incompleteProjectCt?: number;\n /** The customer's links to integrated systems. */\n integrations?: SharedIntegration[];\n /** The number of the customer's locations. */\n locationCount?: number;\n /** The customer's locations. */\n locations?: SparseLocationWithParent[];\n /** The storage key of the customer's logo file. */\n logoFileKey?: string | null;\n /** The number of the customer's past projects. */\n pastProjectCt?: number;\n /** The number of the customer's upcoming projects. */\n upcomingProjectCt?: number;\n};\n\nexport interface CustomerBillingContact {\n /** The billing contact's email address. */\n email?: string | null;\n /** The billing contact's name. */\n name?: string | null;\n /** The billing contact's phone number. */\n phone?: string | null;\n}\n\nexport interface CustomerListing {\n /** The page of records. */\n results?: Customer[];\n /** The number of records that match the filters, across every page. */\n totalCount?: number;\n}\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type Depreciation = \"DOUBLE_DECLINING\" | \"STRAIGHT_LINE\" | \"SUM_YEAR\" | \"UNIT_OF_PRODUCTION\" | null;\n\nexport interface Error {\n error: string;\n statusCode: number;\n}\n\nexport interface FileNewUpload {\n /** The uploaded file's unique identifier. */\n fileId?: string;\n /** The uploaded file's key, which the file endpoints take as fileKey. */\n fileKey?: string;\n}\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type FrontendItemType = \"BULK\" | \"DEVICE\" | \"SERVICE\";\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type IntegrationCategory = \"CRM\" | \"ERP\";\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type IntegrationID = \"HUBSPOT\" | \"NETSUITE\" | \"NONE\" | \"QBO\" | \"SFDC\";\n\nexport interface Item {\n /** Whether a BULK item records serial numbers on its units, which is false for SERVICE and DEVICE items. */\n acceptsBulkSerials?: boolean;\n /** The number of assets of a DEVICE item, and null for other types. */\n assetCount?: number | null;\n /** A free-form description of the item. */\n description?: string | null;\n /** The item's unique identifier. */\n id?: string;\n /** The item's links to integrated systems. */\n integrations?: SharedIntegration[];\n /** Whether the item is archived. */\n isArchived?: boolean;\n /** The item's key, which the item endpoints take as itemKey. */\n key?: string;\n /** When the item was last changed. */\n lastUpdatedAt?: string | null;\n /** The person who last changed the item. */\n lastUpdatedBy?: SparsePerson;\n /** The item's display name. */\n name?: string;\n /** Whether the item's units carry serial numbers, which is true for a DEVICE item and for a BULK item that accepts serials. */\n shouldRenderSerialFields?: boolean;\n /** The item's stock keeping unit, unique within your organization. */\n sku?: string;\n /** SERVICE for a non-physical item, DEVICE for a physical item tracked by serial number, or BULK for a part tracked by quantity. */\n type?: FrontendItemType;\n}\n\nexport interface ItemAccounting {\n /** The default share of overhead for one unit. */\n allocatedIndirect?: string | null;\n /** The default parts cost for one unit. */\n billOfMaterials?: string | null;\n /** The default method one unit is depreciated by. */\n depreciationModel?: Depreciation;\n /** The default labor cost to build one unit. */\n directLabor?: string | null;\n /** The default shipping cost to receive one unit. */\n freightInbound?: string | null;\n /** The default shipping cost to deliver one unit. */\n freightOutbound?: string | null;\n /** The default cost to install one unit. */\n installation?: string | null;\n /** The default financing cost for one unit. */\n interest?: string | null;\n /** The default value one unit keeps at the end of its useful life. */\n salvageValue?: string | null;\n /** A single default cost for one unit, used instead of the cost components. */\n simpleCostBasis?: string | null;\n /** The default import duty paid on one unit. */\n tariffs?: string | null;\n /** The default tax paid on one unit. */\n tax?: string | null;\n /** The default cost of goods sold for one unit, which is the simple cost basis when set and otherwise the sum of the cost components. */\n totalCOGS?: string | null;\n /** The default number of months one unit is depreciated over. */\n usefulLife?: number | null;\n}\n\nexport interface ItemBOMItem {\n /** The BULK part the bill of materials uses. */\n item?: SparseItem;\n /** The number of the part one unit of the item uses. */\n quantity?: number;\n}\n\nexport interface ItemDetail {\n /** Whether a BULK item records serial numbers on its units, which is false for SERVICE and DEVICE items. */\n acceptsBulkSerials?: boolean;\n /** The default costs and depreciation for a unit of a DEVICE or BULK item. */\n accounting?: ItemAccounting;\n /** The number of assets of a DEVICE item, and null for other types. */\n assetCount?: number | null;\n /** The bill of materials, the BULK parts one unit of the item is built from, or null when it has none. */\n bom?: ItemBOMItem[] | null;\n /** The pricing a project line item's billing plan starts from when it sells the item, or null when the item has none. */\n defaultPricing?: ItemPricing | null;\n /** A free-form description of the item. */\n description?: string | null;\n /** The custom fields each asset of a DEVICE item can hold. */\n fields?: ItemField[];\n /** Whether each unit of the item is tracked as its own asset, which is true for a DEVICE item. */\n hasAssets?: boolean;\n /** The item's unique identifier. */\n id?: string;\n /** The item's links to integrated systems. */\n integrations?: SharedIntegration[];\n /** The number of inventory movements of a BULK item, and null for other types. */\n inventoryMovementCount?: number | null;\n /** Whether the item is archived. */\n isArchived?: boolean;\n /** Whether the item is physical, which is true for DEVICE and BULK items. */\n isPhysical?: boolean;\n /** The item's key, which the item endpoints take as itemKey. */\n key?: string;\n /** When the item was last changed. */\n lastUpdatedAt?: string | null;\n /** The person who last changed the item. */\n lastUpdatedBy?: SparsePerson;\n /** The item's display name. */\n name?: string;\n /** Whether the item's units carry serial numbers, which is true for a DEVICE item and for a BULK item that accepts serials. */\n shouldRenderSerialFields?: boolean;\n /** The item's stock keeping unit, unique within your organization. */\n sku?: string;\n /** SERVICE for a non-physical item, DEVICE for a physical item tracked by serial number, or BULK for a part tracked by quantity. */\n type?: FrontendItemType;\n}\n\nexport interface ItemField {\n /** The field's unique identifier. */\n id?: string;\n /** The field's key, which the item field endpoints take as fieldKey. */\n key?: string;\n /** The field's display name. */\n label?: string;\n /** The number of assets that hold a value for the field. */\n metadataCount?: number;\n /** The field's position within its section, starting at 0. */\n order?: number;\n /** The group the field is shown in, starting at 0. */\n section?: number;\n /** The kind of value the field holds. */\n type?: ItemFieldType;\n}\n\nexport interface ItemFieldOrderingUpdateItem {\n /** The ID of the item field to move. */\n fieldId?: string;\n /** The field's position within its section, starting at 0. */\n order?: number;\n /** The group the field is shown in, starting at 0. */\n section?: number;\n}\n\n/** The kind of value an item's custom field holds. Accepted in any case. The listed spelling is the one the API sends back. */\nexport type ItemFieldType = \"BOOLEAN\" | \"DATE\" | \"DATE_TIME\" | \"INTEGER\" | \"MULTILINE_TEXT\" | \"NUMBER\" | \"TEXT\" | \"TIME\";\n\nexport interface ItemListing {\n /** The page of records. */\n results?: Item[];\n /** The number of records that match the filters, across every page. */\n totalCount?: number;\n}\n\nexport interface ItemPricing {\n /** How often a PERIODIC charge recurs. */\n chargeFrequency?: BillingFrequency | null;\n /** PERIODIC to charge on a recurring cadence, or MILESTONE to charge when a milestone is reached. */\n chargeSchedule?: BillingSchedule | null;\n /** The default price for the whole line, used when pricingMode is FIXED. */\n fixedPrice?: Currency | null;\n /** How often the charges are invoiced. */\n invoiceFrequency?: BillingFrequency | null;\n /** ADVANCE to invoice a period before it starts, or ARREARS to invoice it after it ends. */\n invoiceMode?: BillingTiming | null;\n /** PER_UNIT to charge the unit price for each unit, or FIXED to charge the fixed price for the line whatever its quantity. */\n pricingMode?: PricingMode | null;\n /** The default price for each unit, used when pricingMode is PER_UNIT. */\n unitPrice?: Currency | null;\n}\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type ItemType = \"marked_bulk\" | \"service\" | \"tracked_device\" | \"unmarked_bulk\";\n\nexport type Location = SparseLocationWithIntegration & {\n /** The number of active projects at the location. */\n activeProjectCount?: number;\n /** The number of assets currently at the location. */\n assetCount?: number;\n /** The number of incomplete projects at the location. */\n incompleteProjectCount?: number;\n /** The number of past projects at the location. */\n pastProjectCount?: number;\n /** The number of upcoming projects at the location. */\n upcomingProjectCount?: number;\n};\n\nexport interface LocationListing {\n /** The page of records. */\n results?: Location[];\n /** The number of records that match the filters, across every page. */\n totalCount?: number;\n}\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type LocationType = \"SITE\" | \"UNKNOWN\" | \"ZONE\" | null;\n\nexport interface Metadata {\n errors: Error[] | null;\n /** The identifier to quote when asking Hardfin about this request. */\n requestId: string;\n /** How long the server took to answer. */\n requestTime: string;\n statusCode: number;\n}\n\nexport interface MetadataLocationComponents {\n /** The first line of the street address. */\n addressLine1?: string | null;\n /** The second line of the street address, such as a suite. */\n addressLine2?: string | null;\n /** The city. */\n city?: string | null;\n /** The country. */\n country?: string | null;\n /** The whole address on one line. */\n formattedAddress?: string | null;\n /** The postal or ZIP code. */\n postalCode?: string | null;\n /** The state or region. */\n state?: string | null;\n}\n\nexport interface MetadataLocationSite {\n /** The location's structured address, which a zone reads from its parent site. */\n address?: MetadataLocationComponents | null;\n /** Google's place ID for the address, when it was found through Google. */\n googlePlaceId?: string | null;\n /** The location's latitude, sent as a decimal string. */\n latitude?: string | null;\n /** The location's longitude, sent as a decimal string. */\n longitude?: string | null;\n /** The person or business that receives shipments at the location. */\n shippingName?: string | null;\n}\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type PricingMode = \"FIXED\" | \"PER_UNIT\";\n\n/** The item type a request asks an item to become. Accepted in any case. The listed spelling is the one the API sends back. */\nexport type RequestedItemType = \"BULK\" | \"DEVICE\" | \"SERVICE\";\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type ScrappedState = \"all\" | \"false\" | \"true\";\n\nexport interface SharedBool {\n /** Whether the operation completed. */\n success?: boolean;\n}\n\nexport interface SharedIntegration {\n /** The Hardfin error code of the failed sync, or null when the sync did not fail. */\n errorCode?: number | null;\n /** The integrated system, such as SFDC or HUBSPOT. */\n id?: IntegrationID;\n /** Whether the last sync with the external system failed. */\n inError?: boolean;\n /** Whether a sync with the external system is pending. */\n isSyncing?: boolean;\n /** The system's display name. */\n label?: string;\n /** A URL to the linked record in the external system. */\n link?: string | null;\n /** Whether the record differs from the external system and needs a person to review it. */\n needsReview?: boolean;\n /** The kind of system, CRM or ERP. */\n type?: IntegrationCategory;\n}\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type SortOrder = \"ASC\" | \"DESC\";\n\nexport interface SparseAsset {\n /** Whether the asset works, is broken, awaits review, or has been scrapped. */\n functionalStatus?: AssetFunctionalStatus;\n /** The asset's unique identifier. */\n id?: string;\n /** The day the asset entered inventory. */\n inInventoryDate?: string;\n /** Whether the asset is archived. */\n isArchived?: boolean;\n /** Whether the asset has shipped and not yet been delivered. */\n isInTransit?: boolean;\n /** The catalog item the asset is a unit of. */\n item?: SparseItem;\n /** The asset's key, which the asset endpoints take as assetKey. */\n key?: string;\n /** The asset's serial number, unique within its item. */\n serial?: string;\n}\n\nexport interface SparseCustomer {\n /** The customer's unique identifier. */\n id?: string;\n /** Whether the customer is archived. */\n isArchived?: boolean;\n /** Whether the company is a customer. */\n isCustomer?: boolean;\n /** Whether the company is a supplier. */\n isSupplier?: boolean;\n /** The customer's key, which the customer endpoints take as customerKey. */\n key?: string;\n /** The customer's display name. */\n name?: string;\n}\n\nexport interface SparseItem {\n /** Whether each unit of the item is tracked as its own asset, which is true for a DEVICE item. */\n hasAssets?: boolean;\n /** The item's unique identifier. */\n id?: string;\n /** Whether the item is archived. */\n isArchived?: boolean;\n /** Whether the item is physical, which is true for DEVICE and BULK items. */\n isPhysical?: boolean;\n /** The item's key, which the item endpoints take as itemKey. */\n key?: string;\n /** The item's display name. */\n name?: string;\n /** Whether the item's units carry serial numbers, which is true for a DEVICE item and for a BULK item that accepts serials. */\n shouldRenderSerialFields?: boolean;\n /** The item's stock keeping unit, unique within your organization. */\n sku?: string;\n /** SERVICE for a non-physical item, DEVICE for a physical item tracked by serial number, or BULK for a part tracked by quantity. */\n type?: FrontendItemType;\n /** The unit the item's quantities are counted in. */\n unitOfMeasure?: UnitOfMeasure;\n}\n\nexport type SparseLocation = MetadataLocationSite & {\n /** The number of archived zones at a site. */\n archivedZoneCount?: number;\n /** The customer a zone is designated for, such as for reservations, provisioning or a 3PL. */\n consignee?: SparseCustomer | null;\n /** The customer the location is assigned to, which a zone reads from its parent site, or null for your organization's own locations. */\n customer?: SparseCustomer | null;\n /** A free-form description of a zone. */\n description?: string | null;\n /** The location's unique identifier. */\n id?: string;\n /** Whether the location is archived. */\n isArchived?: boolean;\n /** Whether assets at the location count as inventory for reporting. */\n isInventory?: boolean;\n /** Whether a zone sets its own isInventory rather than inheriting its parent site's. */\n isInventoryOverride?: boolean;\n /** Whether a third-party logistics (3PL) provider fulfills orders from the location. */\n isThirdPartyEnabled?: boolean;\n /** Whether assets make only occasional or temporary stops at the location, which hides it from location lists by default. */\n isTransient?: boolean;\n /** The location's key, which the location endpoints take as locationKey. */\n key?: string;\n /** The location's display name. */\n name?: string;\n /** The 3PL provider's identifier for the location. */\n thirdPartyIdentifier?: string | null;\n /** SITE for a site, or ZONE for a zone within a site. */\n type?: LocationType;\n /** The number of zones at a site. */\n zoneCount?: number;\n /** The site's zones. */\n zones?: SparseZone[];\n};\n\nexport type SparseLocationWithIntegration = SparseLocationWithParent & {\n /** The location's links to integrated systems. */\n integrations?: SharedIntegration[];\n};\n\nexport type SparseLocationWithParent = SparseLocation & {\n /** The site a zone belongs to, or null for a site. */\n parentLocation?: SparseLocation | null;\n};\n\nexport interface SparseOrganization {\n /** The organization's ID */\n id?: string;\n /** The public identifier that can be used in URLs */\n key?: string;\n /** The name of the organization */\n name?: string;\n}\n\nexport interface SparsePerson {\n /** The person's first name. */\n firstName?: string | null;\n /** The person's unique identifier. */\n id?: string;\n /** The person's key. */\n key?: string;\n /** The person's last name. */\n lastName?: string | null;\n}\n\nexport interface SparseProject {\n /** The customer the project is for. */\n customer?: SparseCustomer | null;\n /** The day the project ends. */\n end?: string | null;\n /** The project's unique identifier. */\n id?: string;\n /** The project's links to integrated systems. */\n integrations?: SharedIntegration[];\n /** Whether the project is archived. */\n isArchived?: boolean;\n /** The project's key. */\n key?: string;\n /** The project's display name. */\n name?: string;\n /** The name of the project's payment term. */\n paymentTermLabel?: string | null;\n /** The customer's purchase order number, which new invoices for the project carry. */\n purchaseOrderNumber?: string | null;\n /** The day the project starts. */\n start?: string | null;\n /** Whether the project is upcoming, active, or past, judged from its dates. */\n status?: string;\n}\n\nexport interface SparseShipment {\n /** When the shipment arrived, or null while it has not. */\n deliveredAt?: string | null;\n /** The shipment's unique identifier. */\n id?: string;\n /** The shipment's key. */\n key?: string;\n /** The number the shipment is known by. */\n shipmentNumber?: string;\n /** What kind of movement the shipment carries. */\n shipmentType?: string;\n /** When the shipment left, or null while it has not. */\n shippedAt?: string | null;\n}\n\nexport interface SparseZone {\n /** The number of assets in the zone. */\n assetCount?: number;\n /** The customer the zone is designated for, such as for reservations, provisioning or a 3PL. */\n consignee?: SparseCustomer | null;\n /** A free-form description of the zone. */\n description?: string | null;\n /** The zone's unique identifier. */\n id?: string;\n /** Whether the zone is archived. */\n isArchived?: boolean;\n /** Whether assets in the zone count as inventory for reporting. */\n isInventory?: boolean;\n /** The zone's key. */\n key?: string;\n /** The zone's display name, unique within its site. */\n name?: string;\n}\n\nexport interface Token {\n /** When the API key was created, the application was connected, or the person signed in. */\n createdAt?: string;\n /** When the access token or session expires, or null for an API key, which does not. */\n expiresAt?: string | null;\n /** The API key's public identifier or the application's client_id, or null for a session. */\n identifier?: string | null;\n /** The API key's label or the application's name, or null when there is none. */\n label?: string | null;\n /** When the API key was last regenerated or the access token was issued, or null for a session. */\n lastRegeneratedAt?: string | null;\n /** The organization the credential acts in. */\n organization?: SparseOrganization;\n /** The person the credential acts for. */\n person?: SparsePerson;\n scopes?: string[];\n /** The kind of credential. */\n type?: CredentialType;\n}\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type TransientState = \"all\" | \"false\" | \"true\";\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type TransitStatus = \"IN_TRANSIT\" | \"IN_TRANSIT_TO_FIELD\" | \"IN_TRANSIT_TO_INVENTORY\" | \"NOT_IN_TRANSIT\";\n\nexport interface URLLink {\n /** When the link was created. */\n createdAt?: string;\n /** The URL link's unique identifier. */\n id?: string;\n /** The URL link's key, which the URL link endpoints take as linkKey. */\n key?: string;\n /** The link's display name, or null when the link shows its address instead. */\n name?: string | null;\n /** When the link was last changed. */\n updatedAt?: string;\n /** The address the link points to. */\n url?: string;\n}\n\n/** The unit a quantity is counted in, sent as its UNECE Recommendation 20 code. Accepted in any case. The listed spelling is the one the API sends back. | Value | Description | | --- | --- | | `BG` | Bag | | `BO` | Bottle | | `BX` | Box | | `C62` | Unit | | `CMK` | Square centimeter (cm2) | | `CMT` | Centimeter (cm) | | `CR` | Crate | | `CS` | Case | | `CT` | Carton | | `DMQ` | Cubic decimeter (dm3) | | `DR` | Drum | | `DZN` | Dozen (doz) | | `EA` | Each (ea) | | `EN` | Envelope | | `FOT` | Foot (ft) | | `FTK` | Square foot (ft2) | | `FTQ` | Cubic foot (ft3) | | `GLL` | Gallon (gal) | | `GRM` | Gram (g) | | `GRO` | Gross | | `H87` | Piece (pc) | | `INH` | Inch (in) | | `INK` | Square inch (in2) | | `INQ` | Cubic inch (in3) | | `KG` | Keg | | `KGM` | Kilogram (kg) | | `KMT` | Kilometer (km) | | `KT` | Kit | | `LBR` | Pound (lb) | | `LO` | Lot | | `LTR` | Liter (L) | | `MGM` | Milligram (mg) | | `MLT` | Milliliter (mL) | | `MMK` | Square millimeter (mm2) | | `MMT` | Millimeter (mm) | | `MTK` | Square meter (m2) | | `MTQ` | Cubic meter (m3) | | `MTR` | Meter (m) | | `ONZ` | Ounce (oz) | | `OZA` | Fluid ounce (fl oz) | | `PK` | Pack | | `PR` | Pair (pr) | | `PTI` | Pint (pt) | | `PX` | Pallet | | `QTI` | Quart (qt) | | `RL` | Reel | | `RO` | Roll | | `SET` | Set | | `SMI` | Mile (mi) | | `ST` | Sheet | | `STN` | Ton US (NT) | | `SV` | Skid | | `TNE` | Tonne metric (t) | | `TU` | Tube | | `YDK` | Square yard (yd2) | | `YDQ` | Cubic yard (yd3) | | `YRD` | Yard (yd) | */\nexport type UnitOfMeasure = \"BG\" | \"BO\" | \"BX\" | \"C62\" | \"CMK\" | \"CMT\" | \"CR\" | \"CS\" | \"CT\" | \"DMQ\" | \"DR\" | \"DZN\" | \"EA\" | \"EN\" | \"FOT\" | \"FTK\" | \"FTQ\" | \"GLL\" | \"GRM\" | \"GRO\" | \"H87\" | \"INH\" | \"INK\" | \"INQ\" | \"KG\" | \"KGM\" | \"KMT\" | \"KT\" | \"LBR\" | \"LO\" | \"LTR\" | \"MGM\" | \"MLT\" | \"MMK\" | \"MMT\" | \"MTK\" | \"MTQ\" | \"MTR\" | \"ONZ\" | \"OZA\" | \"PK\" | \"PR\" | \"PTI\" | \"PX\" | \"QTI\" | \"RL\" | \"RO\" | \"SET\" | \"SMI\" | \"ST\" | \"STN\" | \"SV\" | \"TNE\" | \"TU\" | \"YDK\" | \"YDQ\" | \"YRD\";\n\n/** Accepted in any case. The listed spelling is the one the API sends back. */\nexport type UsefulLifeRevisionReason = \"CHANGE_IN_USE\" | \"DAMAGE\" | \"OBSOLESCENCE\" | \"OTHER\" | \"REASSESSMENT\" | \"REFURBISHMENT\" | \"REGULATORY\";\n";
2005
+ //#endregion
2006
+ //#region src/command/types.ts
2007
+ const typesCommand = defineCommand({
2008
+ name: "types",
2009
+ summary: "Print TypeScript types for the Hardfin API",
2010
+ description: `Writes a declaration for every record the API sends and takes, for API version ${API_VERSION}, which is the version this CLI calls. Regenerate them after upgrading the CLI.`,
2011
+ arguments: [],
2012
+ flags: [{
2013
+ name: "output",
2014
+ description: "Where to write the declarations, instead of standard output",
2015
+ valueName: "path",
2016
+ schema: z.string()
2017
+ }, {
2018
+ name: "json",
2019
+ description: "Print machine-readable output, which is the default when stdout is not a terminal",
2020
+ schema: z.boolean()
2021
+ }],
2022
+ examples: [{
2023
+ description: "Write them into a project",
2024
+ command: "hardfin types --output src/hardfin-api.d.ts"
2025
+ }, {
2026
+ description: "Read them",
2027
+ command: "hardfin types | less"
2028
+ }],
2029
+ run: runTypes
2030
+ });
2031
+ async function runTypes(input) {
2032
+ const output = input.flags["output"];
2033
+ if (typeof output !== "string") {
2034
+ process.stdout.write(API_TYPES);
2035
+ return ExitCode.OK;
2036
+ }
2037
+ writeFileSync(output, API_TYPES);
2038
+ writeData(input.isJSON ? {
2039
+ written: output,
2040
+ apiVersion: API_VERSION,
2041
+ bytes: Buffer.byteLength(API_TYPES)
2042
+ } : `Wrote ${output}`);
2043
+ return ExitCode.OK;
2044
+ }
2045
+ //#endregion
1844
2046
  //#region src/auth/device.ts
1845
2047
  const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
1846
2048
  /** The interval a server gives is in seconds, and five more are added on a slow_down. */
@@ -2742,7 +2944,7 @@ async function report(error, occasion, resolved) {
2742
2944
  const settings = resolved ?? toResolved();
2743
2945
  const status = settings ? await toStatus(settings) : void 0;
2744
2946
  const secrets = [settings?.settings.apiKey, settings ? toCredential(settings.settings.issuerUrl)?.refreshToken : void 0];
2745
- return await (await import("./sentry-CLaeAWNa.js")).send({
2947
+ return await (await import("./sentry-BM9Rvhhy.js")).send({
2746
2948
  error,
2747
2949
  occasion,
2748
2950
  telemetry,
@@ -3528,6 +3730,7 @@ const commands = [
3528
3730
  loginCommand,
3529
3731
  logoutCommand,
3530
3732
  statusCommand,
3733
+ initCommand,
3531
3734
  ...[
3532
3735
  {
3533
3736
  name: "asset",
@@ -6192,6 +6395,7 @@ const commands = [
6192
6395
  }
6193
6396
  ],
6194
6397
  apiCommand,
6398
+ typesCommand,
6195
6399
  configCommand,
6196
6400
  agentGuideCommand,
6197
6401
  completionCommand,
@@ -6330,5 +6534,5 @@ async function toReported(error, isJSON, resolved, level = "error") {
6330
6534
  //#endregion
6331
6535
  export { toCli, toParseExitCode, toReported, wasReportAttempted };
6332
6536
 
6333
- //# sourceMappingURL=program-BdRDIa3u.js.map
6334
- //# debugId=6057a656-73c2-55b1-9f09-873e3b83b368
6537
+ //# sourceMappingURL=program-CjKwnPeY.js.map
6538
+ //# debugId=5a591d32-0d31-5357-b447-e33420753351
@@ -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.35",
3
+ "version": "0.1.0-dev.37",
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