@hardfin/cli 0.1.0-dev.35 → 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 +26 -0
- package/dist/cli.js +4 -4
- package/dist/completion-tree.json +19 -0
- package/dist/{program-BdRDIa3u.js → program-DtqGeehp.js} +169 -10
- package/dist/{sentry-CLaeAWNa.js → sentry-BM9Rvhhy.js} +6 -6
- package/package.json +4 -2
- package/skills/hardfin-cli/SKILL.md +133 -0
- package/templates/AGENTS.md +25 -0
package/README.md
CHANGED
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="fa6bfe29-9ee3-5a91-aaa1-4b75576d7943")}catch(e){}}();
|
|
4
4
|
import module from "node:module";
|
|
5
5
|
//#region src/cli.ts
|
|
6
6
|
module.enableCompileCache?.();
|
|
@@ -14,7 +14,7 @@ process.on("unhandledRejection", (error) => void toCrash(error));
|
|
|
14
14
|
try {
|
|
15
15
|
const answer = await toQuickAnswer(process.argv.slice(2));
|
|
16
16
|
if (answer === void 0) {
|
|
17
|
-
const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-
|
|
17
|
+
const { toCli, toParseExitCode, wasReportAttempted } = await import("./program-DtqGeehp.js");
|
|
18
18
|
try {
|
|
19
19
|
await toCli().parseAsync(process.argv);
|
|
20
20
|
} catch (error) {
|
|
@@ -33,7 +33,7 @@ async function toCrash(error) {
|
|
|
33
33
|
isCrashing = true;
|
|
34
34
|
process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
35
35
|
try {
|
|
36
|
-
const { toReported } = await import("./program-
|
|
36
|
+
const { toReported } = await import("./program-DtqGeehp.js");
|
|
37
37
|
await toReported(error, false, void 0, "fatal");
|
|
38
38
|
} finally {
|
|
39
39
|
process.exit(1);
|
|
@@ -51,4 +51,4 @@ async function toQuickAnswer(args) {
|
|
|
51
51
|
export {};
|
|
52
52
|
|
|
53
53
|
//# sourceMappingURL=cli.js.map
|
|
54
|
-
//# debugId=
|
|
54
|
+
//# debugId=fa6bfe29-9ee3-5a91-aaa1-4b75576d7943
|
|
@@ -73,6 +73,25 @@
|
|
|
73
73
|
"arguments": [],
|
|
74
74
|
"subcommands": []
|
|
75
75
|
},
|
|
76
|
+
{
|
|
77
|
+
"name": "init",
|
|
78
|
+
"summary": "Set up a project so coding agents can use Hardfin",
|
|
79
|
+
"runs": true,
|
|
80
|
+
"flags": [
|
|
81
|
+
{
|
|
82
|
+
"name": "dry-run",
|
|
83
|
+
"description": "Say what would be written, and write nothing"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "json",
|
|
87
|
+
"description": "Print machine-readable output, which is the default when stdout is not a terminal"
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"arguments": [
|
|
91
|
+
{}
|
|
92
|
+
],
|
|
93
|
+
"subcommands": []
|
|
94
|
+
},
|
|
76
95
|
{
|
|
77
96
|
"name": "asset",
|
|
78
97
|
"summary": "Asset commands",
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="03b49867-f2d5-5a64-af87-e45e530711a1")}catch(e){}}();
|
|
4
4
|
import { a as defineCommand, i as GLOBAL_FLAGS, r as ExitCode, t as toInvocation } from "./scrub-BN3gyBpR.js";
|
|
5
5
|
import { n as toCompletion, t as toAnswer$1 } from "./completion-tree-rR_Rmeij.js";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { Command, CommanderError, Option } from "commander";
|
|
8
8
|
import { z } from "zod";
|
|
9
|
-
import { accessSync, chmodSync, closeSync, constants, existsSync, mkdirSync, openAsBlob, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
10
|
-
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
9
|
+
import { accessSync, chmodSync, closeSync, constants, existsSync, mkdirSync, openAsBlob, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
10
|
+
import { basename, delimiter, dirname, join, relative, resolve } from "node:path";
|
|
11
11
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
12
12
|
import { arch, cpus, homedir, release, totalmem, type, version } from "node:os";
|
|
13
13
|
import { spawn, spawnSync } from "node:child_process";
|
|
14
14
|
import { createInterface } from "node:readline/promises";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
15
16
|
import { createServer } from "node:http";
|
|
16
17
|
import { createInterface as createInterface$1 } from "node:readline";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
18
|
//#region src/report/expected.ts
|
|
19
19
|
/**
|
|
20
20
|
* ExpectedFailure is a failure the CLI anticipated: the person's input, their network, their
|
|
@@ -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,164 @@ 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
|
|
1844
2002
|
//#region src/auth/device.ts
|
|
1845
2003
|
const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
1846
2004
|
/** The interval a server gives is in seconds, and five more are added on a slow_down. */
|
|
@@ -2742,7 +2900,7 @@ async function report(error, occasion, resolved) {
|
|
|
2742
2900
|
const settings = resolved ?? toResolved();
|
|
2743
2901
|
const status = settings ? await toStatus(settings) : void 0;
|
|
2744
2902
|
const secrets = [settings?.settings.apiKey, settings ? toCredential(settings.settings.issuerUrl)?.refreshToken : void 0];
|
|
2745
|
-
return await (await import("./sentry-
|
|
2903
|
+
return await (await import("./sentry-BM9Rvhhy.js")).send({
|
|
2746
2904
|
error,
|
|
2747
2905
|
occasion,
|
|
2748
2906
|
telemetry,
|
|
@@ -3528,6 +3686,7 @@ const commands = [
|
|
|
3528
3686
|
loginCommand,
|
|
3529
3687
|
logoutCommand,
|
|
3530
3688
|
statusCommand,
|
|
3689
|
+
initCommand,
|
|
3531
3690
|
...[
|
|
3532
3691
|
{
|
|
3533
3692
|
name: "asset",
|
|
@@ -6330,5 +6489,5 @@ async function toReported(error, isJSON, resolved, level = "error") {
|
|
|
6330
6489
|
//#endregion
|
|
6331
6490
|
export { toCli, toParseExitCode, toReported, wasReportAttempted };
|
|
6332
6491
|
|
|
6333
|
-
//# sourceMappingURL=program-
|
|
6334
|
-
//# debugId=
|
|
6492
|
+
//# sourceMappingURL=program-DtqGeehp.js.map
|
|
6493
|
+
//# debugId=03b49867-f2d5-5a64-af87-e45e530711a1
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="35c80804-dcd8-5662-9f00-9c8ff04f286a")}catch(e){}}();
|
|
4
4
|
import { n as toScrubbed } from "./scrub-BN3gyBpR.js";
|
|
5
5
|
import { dirname } from "node:path";
|
|
6
|
-
import { request } from "node:http";
|
|
7
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { request } from "node:http";
|
|
8
8
|
import { request as request$1 } from "node:https";
|
|
9
9
|
//#region node_modules/@sentry/core/build/esm/debug-build.js
|
|
10
10
|
const DEBUG_BUILD = typeof __SENTRY_DEBUG__ === "undefined" || __SENTRY_DEBUG__;
|
|
@@ -3925,7 +3925,7 @@ function trim(arr) {
|
|
|
3925
3925
|
if (start > end) return [];
|
|
3926
3926
|
return arr.slice(start, end - start + 1);
|
|
3927
3927
|
}
|
|
3928
|
-
function relative(from, to) {
|
|
3928
|
+
function relative$1(from, to) {
|
|
3929
3929
|
from = resolve$1(from).slice(1);
|
|
3930
3930
|
to = resolve$1(to).slice(1);
|
|
3931
3931
|
const fromParts = trim(from.split("/"));
|
|
@@ -4001,7 +4001,7 @@ function generateIteratee({ isBrowser, root, prefix }) {
|
|
|
4001
4001
|
}
|
|
4002
4002
|
} else if (isWindowsFrame || startsWithSlash) {
|
|
4003
4003
|
const filename = isWindowsFrame ? frame.filename.replace(/^[a-zA-Z]:/, "").replace(/\\/g, "/") : frame.filename;
|
|
4004
|
-
frame.filename = `${prefix}${root ? relative(root, filename) : basename$1(filename)}`;
|
|
4004
|
+
frame.filename = `${prefix}${root ? relative$1(root, filename) : basename$1(filename)}`;
|
|
4005
4005
|
}
|
|
4006
4006
|
return frame;
|
|
4007
4007
|
};
|
|
@@ -4385,5 +4385,5 @@ function toTags(delivery) {
|
|
|
4385
4385
|
//#endregion
|
|
4386
4386
|
export { send };
|
|
4387
4387
|
|
|
4388
|
-
//# sourceMappingURL=sentry-
|
|
4389
|
-
//# debugId=
|
|
4388
|
+
//# sourceMappingURL=sentry-BM9Rvhhy.js.map
|
|
4389
|
+
//# debugId=35c80804-dcd8-5662-9f00-9c8ff04f286a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hardfin/cli",
|
|
3
|
-
"version": "0.1.0-dev.
|
|
3
|
+
"version": "0.1.0-dev.36",
|
|
4
4
|
"description": "Command line interface for the Hardfin API",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Hardfin, Inc.",
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
|
-
"!dist/**/*.map"
|
|
27
|
+
"!dist/**/*.map",
|
|
28
|
+
"skills",
|
|
29
|
+
"templates"
|
|
28
30
|
],
|
|
29
31
|
"engines": {
|
|
30
32
|
"node": ">=20"
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hardfin-cli
|
|
3
|
+
description: Read and change records in a Hardfin organization with the Hardfin CLI (`hardfin`) - assets, customers, items, locations, files, ownership and moves. Use when the person asks to look up, import, create, update, move, scrap or archive Hardfin data, to sign in to Hardfin, or to check what the CLI is connected to. Not for writing application code that calls the Hardfin API over HTTP; read https://docs.hardfin.com for that.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Hardfin CLI
|
|
7
|
+
|
|
8
|
+
The `hardfin` command reads and changes the records in one Hardfin organization through the
|
|
9
|
+
Hardfin API. Every API operation is a command, named `hardfin <noun> <verb>`.
|
|
10
|
+
|
|
11
|
+
This skill gives the patterns. The installed CLI is the authority on exact commands and
|
|
12
|
+
flags, so check `--help` before relying on a flag this skill does not show.
|
|
13
|
+
|
|
14
|
+
## 1. Make sure the CLI is installed
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
hardfin --version || npm install -g @hardfin/cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
It needs Node 20 or later. If `npm install -g` is refused, run commands as
|
|
21
|
+
`npx -y @hardfin/cli <command>` instead.
|
|
22
|
+
|
|
23
|
+
## 2. Make sure it is signed in
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
hardfin status --offline --json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`credential.kind` is `none` when nobody is signed in. Then sign in one of three ways.
|
|
30
|
+
|
|
31
|
+
| Situation | Do this |
|
|
32
|
+
| ---------------------------------------------------------- | ------------------------------------------------ |
|
|
33
|
+
| You run on the person's own machine | Run `hardfin login` in the foreground |
|
|
34
|
+
| You run in a cloud sandbox, away from the person's browser | Run `hardfin login --device` |
|
|
35
|
+
| The person gave you an API key | Pass `--api-key <key>`, or set `HARDFIN_API_KEY` |
|
|
36
|
+
|
|
37
|
+
`hardfin login` writes one JSON line on stderr before it waits:
|
|
38
|
+
|
|
39
|
+
- `{"event": "sign_in_url", "url": …, "browserOpened": …}`: show the person `url`, and say
|
|
40
|
+
whether a browser tab opened
|
|
41
|
+
- `{"event": "device_code", "userCode": …, "verificationUri": …}`: show the person the code
|
|
42
|
+
and where to enter it
|
|
43
|
+
|
|
44
|
+
It then waits up to `waitSeconds` (300) for the person to approve. Give the command that
|
|
45
|
+
long to finish. Do not run it in the background and move on.
|
|
46
|
+
|
|
47
|
+
Never write an API key into a file in the repository, and never echo it back.
|
|
48
|
+
|
|
49
|
+
## 3. Find the command
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
hardfin --help # the nouns
|
|
53
|
+
hardfin asset --help # what you can do with assets
|
|
54
|
+
hardfin asset create --help # every flag that command takes
|
|
55
|
+
hardfin agent-guide # every command, flag, exit code and error code, as Markdown
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Noun | Holds |
|
|
59
|
+
| ------------------ | ---------------------------------------------------------------------------------- |
|
|
60
|
+
| `asset` | Physical assets, with their accounting, events, files, moves, ownership and status |
|
|
61
|
+
| `customer` | The customers assets are sold or leased to |
|
|
62
|
+
| `item` | The catalogue of item types an asset is an instance of |
|
|
63
|
+
| `location` | Warehouses, sites and their zones |
|
|
64
|
+
| `file`, `url-link` | Attachments, and links to records elsewhere |
|
|
65
|
+
| `token` | What the current credential is |
|
|
66
|
+
| `api` | Any endpoint directly, when no command fits |
|
|
67
|
+
|
|
68
|
+
## 4. Keys and IDs
|
|
69
|
+
|
|
70
|
+
A record has both, and they are not interchangeable.
|
|
71
|
+
|
|
72
|
+
| | Looks like | Used for |
|
|
73
|
+
| --- | ----------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
74
|
+
| Key | `ast_4f9xk2mq7plr8stz`, `item_V1StGXR8Z5jdHi6B` | The positional argument of `get`, `update` and nested commands |
|
|
75
|
+
| ID | `0192f7d4-3b6a-7c1e-9f2d-5a8b4c6e7d70` | A field that points at another record, such as `--customer-id` or `--move assetId=…` |
|
|
76
|
+
|
|
77
|
+
A list or get answers with both. Take the one the next command asks for.
|
|
78
|
+
|
|
79
|
+
## 5. Read
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
hardfin asset list --limit 20 --search-query "RX-200"
|
|
83
|
+
hardfin asset get ast_4f9xk2mq7plr8stz
|
|
84
|
+
hardfin customer list --page 2 --limit 100
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Output is JSON on stdout whenever stdout is not a terminal, so pipe it to `jq` or parse it.
|
|
88
|
+
A list holds at most 100 records a page. Fetch every page before saying how many there are.
|
|
89
|
+
|
|
90
|
+
## 6. Change
|
|
91
|
+
|
|
92
|
+
Before any change:
|
|
93
|
+
|
|
94
|
+
1. Read the record you are about to change, and work from what it says now
|
|
95
|
+
2. For more than a few records, or anything that deletes, scraps or archives, tell the person
|
|
96
|
+
what will change and wait for them to agree
|
|
97
|
+
3. Make the change
|
|
98
|
+
4. Read the record back to confirm it
|
|
99
|
+
|
|
100
|
+
Set fields with one flag each, or send the whole body as JSON with `--input`:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
hardfin customer create --name "Acme"
|
|
104
|
+
echo '{"name": "Dock 4, north side"}' | hardfin location create --input -
|
|
105
|
+
hardfin asset accounting update ast_4f9xk2mq7plr8stz --unset useful-life
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
A flag given with `--input` replaces that field of the body. A value with a comma in it
|
|
109
|
+
must go through `--input`, because a `key=value` flag cannot carry one.
|
|
110
|
+
|
|
111
|
+
## 7. When a command fails
|
|
112
|
+
|
|
113
|
+
The exit code says what kind of failure it was.
|
|
114
|
+
|
|
115
|
+
| Exit code | Means | Do this |
|
|
116
|
+
| --------- | --------------------------------- | ---------------------------------------------- |
|
|
117
|
+
| 0 | It worked | |
|
|
118
|
+
| 1 | The API or the network refused it | Read the error's `code` |
|
|
119
|
+
| 2 | The command was called wrongly | Run the command with `--help` and fix the call |
|
|
120
|
+
| 4 | No usable credential | Sign in, as in step 2 |
|
|
121
|
+
|
|
122
|
+
The error is JSON on stderr. Branch on its `code`, never on the message.
|
|
123
|
+
|
|
124
|
+
| `code` | Do this |
|
|
125
|
+
| --------------------------------------------------- | ---------------------------------------------------------------- |
|
|
126
|
+
| `not_signed_in`, `unauthorized` | Sign in again |
|
|
127
|
+
| `invalid_flag`, `missing_flag`, `invalid_arguments` | Read `--help` for that command |
|
|
128
|
+
| `not_found` | Check you passed a key, not an ID, where the command wants a key |
|
|
129
|
+
| `bad_request`, `invalid_request`, `conflict` | Read `errors`, which names the field |
|
|
130
|
+
| `rate_limited` | Wait a minute, then retry once |
|
|
131
|
+
| `unreachable`, `server_error` | Tell the person, and include `requestId` if there is one |
|
|
132
|
+
|
|
133
|
+
`hardfin agent-guide` lists every code.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## Hardfin
|
|
2
|
+
|
|
3
|
+
This project works with Hardfin data through the Hardfin CLI, `hardfin`.
|
|
4
|
+
|
|
5
|
+
| Task | Command |
|
|
6
|
+
| ---------------------------------- | ---------------------------------------------------------------------------- |
|
|
7
|
+
| Install | `npm install -g @hardfin/cli`, with Node 20 or later |
|
|
8
|
+
| Sign in | `hardfin login`, or `--device` away from a browser, or set `HARDFIN_API_KEY` |
|
|
9
|
+
| Check what it is connected to | `hardfin status` |
|
|
10
|
+
| Find a command | `hardfin --help`, then `hardfin <noun> --help` |
|
|
11
|
+
| Every command, flag and error code | `hardfin agent-guide` |
|
|
12
|
+
| The API reference | https://docs.hardfin.com |
|
|
13
|
+
|
|
14
|
+
Start with the `hardfin-cli` skill in `.agents/skills/hardfin-cli/SKILL.md`.
|
|
15
|
+
|
|
16
|
+
The Hardfin MCP server, `{{MCP_URL}}`, is configured for Claude Code in `.mcp.json`, for
|
|
17
|
+
Cursor in `.cursor/mcp.json`, and for VS Code in `.vscode/mcp.json`. For Codex, add
|
|
18
|
+
`[mcp_servers.hardfin]` with `url = "{{MCP_URL}}"` to `~/.codex/config.toml`.
|
|
19
|
+
|
|
20
|
+
Rules:
|
|
21
|
+
|
|
22
|
+
- Read a record before changing it, and read it back afterwards
|
|
23
|
+
- Ask the person before a bulk change, or anything that deletes, scraps or archives
|
|
24
|
+
- Never write an API key into a file in this repository
|
|
25
|
+
- Branch on an error's `code`, never on its message
|