llmond 0.1.0

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 ADDED
@@ -0,0 +1,20 @@
1
+ # llmond
2
+
3
+ Install agent skills, and report the install to [llmond.com](https://llmond.com).
4
+
5
+ ```sh
6
+ npx llmond add owner/repo --skill some-skill
7
+ npx llmond search pdf form filler # needs LLMOND_API_KEY
8
+ ```
9
+
10
+ `add` is a thin wrapper around the [skills.sh](https://skills.sh) CLI: it runs
11
+ `npx skills@latest add owner/repo` with your terminal attached, and that is what
12
+ does the installing.
13
+
14
+ On a successful install it posts exactly five fields to `api.llmond.com`:
15
+ `owner`, `repo`, `slug`, `agent`, `cli_version`. Never file contents, never
16
+ paths, never a machine or user identifier. That count is the point of the
17
+ wrapper: skills.sh reports installs only for skills listed there.
18
+
19
+ Set `LLMOND_NO_TELEMETRY=1` to install with no post at all. `LLMOND_AGENT` sets
20
+ the default `--agent`, `LLMOND_API_URL` points the CLI at another api.
package/bin/lib.mjs ADDED
@@ -0,0 +1,92 @@
1
+ // Argument parsing, output shaping and the telemetry payload for the llmond CLI. Kept out
2
+ // of the bin so all of it is unit-testable without spawning anything (../test).
3
+
4
+ import { readFileSync } from "node:fs";
5
+
6
+ export const DEFAULT_API_URL = "https://api.llmond.com";
7
+
8
+ // The version we report as cli_version comes from this package, so a published build can
9
+ // never claim a number other than its own.
10
+ export const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
11
+
12
+ export const USAGE = `llmond ${VERSION} - install agent skills with usage signals for llmond.com
13
+
14
+ Usage:
15
+ llmond add <owner/repo> [--skill <slug>] [--agent <name>] [-y]
16
+ llmond search <query...>
17
+ llmond --version
18
+ llmond --help
19
+
20
+ Env:
21
+ LLMOND_API_URL api base url (default ${DEFAULT_API_URL})
22
+ LLMOND_API_KEY api key, needed by search
23
+ LLMOND_AGENT default value for --agent
24
+ LLMOND_NO_TELEMETRY set to 1 to install without reporting anything`;
25
+
26
+ export function apiBase(env = {}) {
27
+ return (env.LLMOND_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
28
+ }
29
+
30
+ // EXACTLY the five fields the endpoint takes, and nothing else: no paths, no file
31
+ // contents, no machine or user identifier. The README promises this list, so this is the
32
+ // one place that decides what leaves the machine.
33
+ export function buildTelemetryPayload({ owner, repo, slug, agent, cliVersion }) {
34
+ const payload = { owner, repo };
35
+ if (slug) payload.slug = slug;
36
+ if (agent) payload.agent = agent;
37
+ if (cliVersion) payload.cli_version = cliVersion;
38
+ return payload;
39
+ }
40
+
41
+ export function formatHit(hit) {
42
+ const id = [hit.owner, hit.repo, hit.slug].filter(Boolean).join("/");
43
+ return `${id} ${hit.installs ?? 0} installs ${hit.stars ?? 0} stars`;
44
+ }
45
+
46
+ function parseAdd(args, env) {
47
+ let spec;
48
+ let slug;
49
+ let agent = env.LLMOND_AGENT || undefined;
50
+ let yes = false;
51
+ for (let i = 0; i < args.length; i++) {
52
+ const arg = args[i];
53
+ if (arg === "--skill" || arg === "--agent") {
54
+ const value = args[++i];
55
+ if (value === undefined || value.startsWith("-")) return { cmd: "error", message: `${arg} needs a value` };
56
+ if (arg === "--skill") slug = value;
57
+ else agent = value;
58
+ } else if (arg === "-y" || arg === "--yes") {
59
+ yes = true;
60
+ } else if (arg.startsWith("-")) {
61
+ return { cmd: "error", message: `unknown flag: ${arg}` };
62
+ } else if (spec === undefined) {
63
+ spec = arg;
64
+ } else {
65
+ return { cmd: "error", message: `unexpected argument: ${arg}` };
66
+ }
67
+ }
68
+ if (spec === undefined) return { cmd: "error", message: "add needs an owner/repo" };
69
+ const parts = spec.split("/");
70
+ // owner/repo/slug is how the catalog names a skill, so it is the likely typo here; say
71
+ // what the right form is rather than half-guessing which part was meant as the slug
72
+ if (parts.length === 3) {
73
+ const hint = `try: llmond add ${parts[0]}/${parts[1]} --skill ${parts[2]}`;
74
+ return { cmd: "error", message: `not an owner/repo: ${spec} (${hint})` };
75
+ }
76
+ if (parts.length !== 2 || !parts[0] || !parts[1]) return { cmd: "error", message: `not an owner/repo: ${spec}` };
77
+ return { cmd: "add", owner: parts[0], repo: parts[1], slug, agent, yes };
78
+ }
79
+
80
+ export function parseArgs(argv, env = {}) {
81
+ const [command, ...rest] = argv;
82
+ if (command === "--help" || command === "-h" || command === "help") return { cmd: "help" };
83
+ if (command === "--version" || command === "-v") return { cmd: "version" };
84
+ if (command === undefined) return { cmd: "error", message: "no command given" };
85
+ if (command === "add") return parseAdd(rest, env);
86
+ if (command === "search") {
87
+ const query = rest.join(" ").trim();
88
+ if (!query) return { cmd: "error", message: "search needs a query" };
89
+ return { cmd: "search", query };
90
+ }
91
+ return { cmd: "error", message: `unknown command: ${command}` };
92
+ }
package/bin/llmond.mjs ADDED
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ // llmond - install agent skills, and tell llmond.com that the install happened.
3
+ //
4
+ // The install itself is the skills.sh CLI (`npx skills@latest add ...`), spawned with
5
+ // inherited stdio: this wrapper adds no install logic of its own, and nothing about the
6
+ // installed skill reaches us beyond its name. On a successful install it posts five short
7
+ // fields to api.llmond.com, which is the only install count we own - skills.sh's number
8
+ // stays 0 forever for anything not listed there. LLMOND_NO_TELEMETRY=1 turns the post off.
9
+
10
+ import { spawnSync } from "node:child_process";
11
+ import { apiBase, buildTelemetryPayload, formatHit, parseArgs, USAGE, VERSION } from "./lib.mjs";
12
+
13
+ const TELEMETRY_TIMEOUT_MS = 2_000;
14
+ const SEARCH_TIMEOUT_MS = 10_000;
15
+ const SEARCH_LIMIT = 10;
16
+
17
+ // Fire and forget, in every sense: the response is not read, errors are swallowed, and
18
+ // the install's exit code never depends on any of it.
19
+ async function reportInstall(payload, env) {
20
+ if (env.LLMOND_NO_TELEMETRY && env.LLMOND_NO_TELEMETRY !== "0") return;
21
+ try {
22
+ await fetch(`${apiBase(env)}/v1/telemetry/install`, {
23
+ method: "POST",
24
+ headers: { "content-type": "application/json" },
25
+ body: JSON.stringify(payload),
26
+ signal: AbortSignal.timeout(TELEMETRY_TIMEOUT_MS),
27
+ });
28
+ } catch { /* the skill is already installed; our api being down is not the user's problem */ }
29
+ }
30
+
31
+ async function runAdd(cmd, env) {
32
+ // the two -y flags belong to different programs: `-y` is npx's "install it without
33
+ // asking", `--yes` is the skills CLI's own non-interactive flag, which is the one -y means
34
+ const args = ["-y", "skills@latest", "add", `${cmd.owner}/${cmd.repo}`];
35
+ if (cmd.slug) args.push("--skill", cmd.slug);
36
+ if (cmd.yes) args.push("--yes");
37
+ const run = spawnSync("npx", args, { stdio: "inherit" });
38
+ if (run.error) {
39
+ console.error(`llmond: could not run npx: ${run.error.message}`);
40
+ return 1;
41
+ }
42
+ const code = run.status ?? 1; // a signal kill leaves status null: report it as a failure
43
+ if (code === 0) await reportInstall(buildTelemetryPayload({ ...cmd, cliVersion: VERSION }), env);
44
+ return code;
45
+ }
46
+
47
+ async function runSearch(cmd, env) {
48
+ const key = env.LLMOND_API_KEY;
49
+ // /v1/search is keyed, so without one there is nothing to send: say what is missing and
50
+ // stop, rather than spending a round trip to be told 401 by the server
51
+ if (!key) {
52
+ console.error("llmond: search needs an api key - set LLMOND_API_KEY (get one at llmond.com)");
53
+ return 1;
54
+ }
55
+ const url = `${apiBase(env)}/v1/search?q=${encodeURIComponent(cmd.query)}&limit=${SEARCH_LIMIT}`;
56
+ let res;
57
+ try {
58
+ // unlike the telemetry post this one has a person waiting on it, but not forever
59
+ res = await fetch(url, { headers: { "x-api-key": key }, signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS) });
60
+ } catch (e) {
61
+ console.error(`llmond: search failed: ${e.message}`);
62
+ return 1;
63
+ }
64
+ if (!res.ok) {
65
+ console.error(`llmond: search failed: HTTP ${res.status}`);
66
+ return 1;
67
+ }
68
+ const body = await res.json();
69
+ for (const hit of body.data ?? []) console.log(formatHit(hit));
70
+ return 0;
71
+ }
72
+
73
+ const cmd = parseArgs(process.argv.slice(2), process.env);
74
+ let code = 0;
75
+ if (cmd.cmd === "help") {
76
+ console.log(USAGE);
77
+ } else if (cmd.cmd === "version") {
78
+ console.log(VERSION);
79
+ } else if (cmd.cmd === "add") {
80
+ code = await runAdd(cmd, process.env);
81
+ } else if (cmd.cmd === "search") {
82
+ code = await runSearch(cmd, process.env);
83
+ } else {
84
+ console.error(`llmond: ${cmd.message}`);
85
+ console.error(USAGE);
86
+ code = 1;
87
+ }
88
+ process.exit(code);
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "llmond",
3
+ "version": "0.1.0",
4
+ "description": "Install agent skills with usage signals for llmond.com",
5
+ "type": "module",
6
+ "bin": {
7
+ "llmond": "./bin/llmond.mjs"
8
+ },
9
+ "files": [
10
+ "bin"
11
+ ],
12
+ "engines": {
13
+ "node": ">=20"
14
+ },
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/mertbuilds/skilldb.git",
19
+ "directory": "cli"
20
+ }
21
+ }