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 +20 -0
- package/bin/lib.mjs +92 -0
- package/bin/llmond.mjs +88 -0
- package/package.json +21 -0
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
|
+
}
|