@saleso.innovations/bridge 0.1.16 → 0.1.17

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.
@@ -0,0 +1,105 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ const VERSION_TIMEOUT_MS = 10_000;
5
+ const UPDATE_CHECK_TIMEOUT_MS = 30_000;
6
+ const SEMVER_PATTERN = /\b(\d+\.\d+\.\d+(?:[-+][\w.-]+)?)\b/;
7
+ const COMMIT_PATTERN = /\b([0-9a-f]{7,40})\b/i;
8
+ function combineOutput(stdout, stderr) {
9
+ return [stdout, stderr].filter((chunk) => chunk.trim().length > 0).join("\n").trim();
10
+ }
11
+ function parseSemver(text) {
12
+ const match = text.match(SEMVER_PATTERN);
13
+ return match?.[1] ?? null;
14
+ }
15
+ function parseLatestFromCheckOutput(output, installed) {
16
+ const lines = output.split("\n").map((line) => line.trim()).filter(Boolean);
17
+ const tokens = lines.flatMap((line) => {
18
+ const semverMatches = [...line.matchAll(new RegExp(SEMVER_PATTERN.source, "g"))].map((match) => match[1]);
19
+ const commitMatches = [...line.matchAll(new RegExp(COMMIT_PATTERN.source, "gi"))].map((match) => match[1]);
20
+ return [...semverMatches, ...commitMatches];
21
+ });
22
+ if (tokens.length === 0) {
23
+ return null;
24
+ }
25
+ if (installed) {
26
+ const different = tokens.find((token) => token !== installed);
27
+ if (different) {
28
+ return different;
29
+ }
30
+ }
31
+ return tokens[tokens.length - 1] ?? null;
32
+ }
33
+ export async function runHermesVersion() {
34
+ try {
35
+ const { stdout, stderr } = await execFileAsync("hermes", ["--version"], {
36
+ timeout: VERSION_TIMEOUT_MS,
37
+ env: process.env,
38
+ });
39
+ return parseSemver(combineOutput(stdout, stderr));
40
+ }
41
+ catch (error) {
42
+ if (error && typeof error === "object" && "stdout" in error && "stderr" in error) {
43
+ const stdout = String(error.stdout ?? "");
44
+ const stderr = String(error.stderr ?? "");
45
+ const parsed = parseSemver(combineOutput(stdout, stderr));
46
+ if (parsed) {
47
+ return parsed;
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+ }
53
+ export async function checkHermesUpdate(installed) {
54
+ try {
55
+ const { stdout, stderr } = await execFileAsync("hermes", ["update", "--check"], {
56
+ timeout: UPDATE_CHECK_TIMEOUT_MS,
57
+ env: process.env,
58
+ });
59
+ const output = combineOutput(stdout, stderr);
60
+ const latest = parseLatestFromCheckOutput(output, installed);
61
+ return {
62
+ latest: latest ?? installed,
63
+ updateAvailable: false,
64
+ };
65
+ }
66
+ catch (error) {
67
+ const execError = error;
68
+ const output = combineOutput(String(execError.stdout ?? ""), String(execError.stderr ?? ""));
69
+ const exitCode = typeof execError.code === "number" ? execError.code : null;
70
+ if (exitCode === 1) {
71
+ const latest = parseLatestFromCheckOutput(output, installed);
72
+ return {
73
+ latest,
74
+ updateAvailable: true,
75
+ };
76
+ }
77
+ if (output.trim().length > 0) {
78
+ const latest = parseLatestFromCheckOutput(output, installed);
79
+ if (latest) {
80
+ return {
81
+ latest,
82
+ updateAvailable: installed ? latest !== installed : null,
83
+ };
84
+ }
85
+ }
86
+ const message = execError instanceof Error && execError.message.trim().length > 0
87
+ ? execError.message.trim()
88
+ : "Hermes update check failed.";
89
+ return {
90
+ latest: null,
91
+ updateAvailable: null,
92
+ checkError: message,
93
+ };
94
+ }
95
+ }
96
+ export async function fetchHermesRuntimeVersion() {
97
+ const installed = await runHermesVersion();
98
+ const check = await checkHermesUpdate(installed);
99
+ return {
100
+ installed,
101
+ latest: check.latest,
102
+ updateAvailable: check.updateAvailable,
103
+ ...(check.checkError ? { checkError: check.checkError } : {}),
104
+ };
105
+ }
@@ -0,0 +1,9 @@
1
+ export type HermesSkillEntry = {
2
+ name: string;
3
+ description: string;
4
+ category?: string;
5
+ };
6
+ export declare function listHermesSkills(): Promise<{
7
+ skills: HermesSkillEntry[];
8
+ }>;
9
+ //# sourceMappingURL=skillsList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skillsList.d.ts","sourceRoot":"","sources":["../src/skillsList.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AA4LF,wBAAsB,gBAAgB,IAAI,OAAO,CAAC;IAAE,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAAC,CAOhF"}
@@ -0,0 +1,187 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join, relative } from "node:path";
5
+ import { promisify } from "node:util";
6
+ const execFileAsync = promisify(execFile);
7
+ const HERMES_SKILLS_DIR = join(homedir(), ".hermes", "skills");
8
+ const LIST_TIMEOUT_MS = 30_000;
9
+ const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
10
+ const SKILL_FILE_NAME = "SKILL.md";
11
+ const HIDDEN_DIR_NAMES = new Set([".hub", ".git"]);
12
+ function normalizeSkillsPayload(value) {
13
+ if (Array.isArray(value)) {
14
+ return { skills: value.flatMap((item) => parseSkillRecord(item)) };
15
+ }
16
+ if (value && typeof value === "object") {
17
+ const record = value;
18
+ for (const key of ["skills", "items", "data", "results"]) {
19
+ if (Array.isArray(record[key])) {
20
+ return { skills: record[key].flatMap((item) => parseSkillRecord(item)) };
21
+ }
22
+ }
23
+ }
24
+ return { skills: [] };
25
+ }
26
+ function parseSkillRecord(value) {
27
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
28
+ return [];
29
+ }
30
+ const record = value;
31
+ const name = pickString(record, ["name", "id", "skill", "skill_name"]);
32
+ if (!name) {
33
+ return [];
34
+ }
35
+ const description = pickString(record, ["description", "desc", "summary"]) ?? "";
36
+ const category = pickString(record, ["category", "path", "group"]);
37
+ const entry = { name, description };
38
+ if (category) {
39
+ entry.category = category;
40
+ }
41
+ return [entry];
42
+ }
43
+ function pickString(record, keys) {
44
+ for (const key of keys) {
45
+ const value = record[key];
46
+ if (typeof value === "string") {
47
+ const trimmed = value.trim();
48
+ if (trimmed.length > 0) {
49
+ return trimmed;
50
+ }
51
+ }
52
+ }
53
+ return undefined;
54
+ }
55
+ function parseFrontmatter(raw) {
56
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
57
+ if (!match) {
58
+ return {};
59
+ }
60
+ const frontmatter = {};
61
+ const body = match[1];
62
+ if (!body) {
63
+ return {};
64
+ }
65
+ for (const line of body.split(/\r?\n/)) {
66
+ const separator = line.indexOf(":");
67
+ if (separator <= 0) {
68
+ continue;
69
+ }
70
+ const key = line.slice(0, separator).trim();
71
+ let value = line.slice(separator + 1).trim();
72
+ if ((value.startsWith('"') && value.endsWith('"')) ||
73
+ (value.startsWith("'") && value.endsWith("'"))) {
74
+ value = value.slice(1, -1);
75
+ }
76
+ if (key.length > 0) {
77
+ frontmatter[key] = value;
78
+ }
79
+ }
80
+ return frontmatter;
81
+ }
82
+ function deriveCategory(skillsRoot, skillFilePath) {
83
+ const skillDir = join(skillFilePath, "..");
84
+ const relativePath = relative(skillsRoot, skillDir);
85
+ if (!relativePath || relativePath === ".") {
86
+ return undefined;
87
+ }
88
+ const segments = relativePath.split(/[/\\]/).filter((segment) => segment.length > 0);
89
+ if (segments.length <= 1) {
90
+ return undefined;
91
+ }
92
+ return segments[0];
93
+ }
94
+ function readSkillsFromDirectory() {
95
+ if (!existsSync(HERMES_SKILLS_DIR)) {
96
+ return { skills: [] };
97
+ }
98
+ const skills = [];
99
+ function walk(currentDir) {
100
+ let names;
101
+ try {
102
+ names = readdirSync(currentDir);
103
+ }
104
+ catch {
105
+ return;
106
+ }
107
+ for (const entryName of names) {
108
+ if (entryName.startsWith(".") && HIDDEN_DIR_NAMES.has(entryName)) {
109
+ continue;
110
+ }
111
+ const fullPath = join(currentDir, entryName);
112
+ let isDirectory = false;
113
+ let isFile = false;
114
+ try {
115
+ const stats = statSync(fullPath);
116
+ isDirectory = stats.isDirectory();
117
+ isFile = stats.isFile();
118
+ }
119
+ catch {
120
+ continue;
121
+ }
122
+ if (isDirectory) {
123
+ walk(fullPath);
124
+ continue;
125
+ }
126
+ if (!isFile || entryName !== SKILL_FILE_NAME) {
127
+ continue;
128
+ }
129
+ let raw = "";
130
+ try {
131
+ raw = readFileSync(fullPath, "utf8");
132
+ }
133
+ catch {
134
+ continue;
135
+ }
136
+ const frontmatter = parseFrontmatter(raw);
137
+ const relativePath = relative(HERMES_SKILLS_DIR, join(fullPath, ".."));
138
+ const fallbackName = relativePath.split(/[/\\]/).filter(Boolean).pop() ?? relativePath;
139
+ const name = frontmatter.name?.trim() || fallbackName;
140
+ if (!name) {
141
+ continue;
142
+ }
143
+ const description = frontmatter.description?.trim() ?? "";
144
+ const category = deriveCategory(HERMES_SKILLS_DIR, fullPath);
145
+ const skill = { name, description };
146
+ if (category) {
147
+ skill.category = category;
148
+ }
149
+ skills.push(skill);
150
+ }
151
+ }
152
+ walk(HERMES_SKILLS_DIR);
153
+ skills.sort((left, right) => {
154
+ const categoryCompare = (left.category ?? "").localeCompare(right.category ?? "", undefined, {
155
+ sensitivity: "base",
156
+ });
157
+ if (categoryCompare !== 0) {
158
+ return categoryCompare;
159
+ }
160
+ return left.name.localeCompare(right.name, undefined, { sensitivity: "base" });
161
+ });
162
+ return { skills };
163
+ }
164
+ async function runHermesSkillsListJson() {
165
+ try {
166
+ const { stdout } = await execFileAsync("hermes", ["skills", "list", "--json"], {
167
+ timeout: LIST_TIMEOUT_MS,
168
+ maxBuffer: MAX_OUTPUT_BYTES,
169
+ env: process.env,
170
+ });
171
+ const trimmed = stdout.trim();
172
+ if (!trimmed) {
173
+ return null;
174
+ }
175
+ return normalizeSkillsPayload(JSON.parse(trimmed));
176
+ }
177
+ catch {
178
+ return null;
179
+ }
180
+ }
181
+ export async function listHermesSkills() {
182
+ const jsonResult = await runHermesSkillsListJson();
183
+ if (jsonResult != null) {
184
+ return jsonResult;
185
+ }
186
+ return readSkillsFromDirectory();
187
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saleso.innovations/bridge",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Connect your Hermes agent to the Cleos iOS app via pairing code.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,14 +9,23 @@
9
9
  "url": "git+https://github.com/RemiRonning/cleos.git",
10
10
  "directory": "packages/cleos-hermes-connect"
11
11
  },
12
- "keywords": ["cleos", "hermes", "agent", "relay"],
12
+ "keywords": [
13
+ "cleos",
14
+ "hermes",
15
+ "agent",
16
+ "relay"
17
+ ],
13
18
  "exports": {
14
19
  ".": {
15
20
  "types": "./dist/index.d.ts",
16
21
  "import": "./dist/index.js"
17
22
  }
18
23
  },
19
- "files": ["dist", "README.md", "INTEGRATION.md"],
24
+ "files": [
25
+ "dist",
26
+ "README.md",
27
+ "INTEGRATION.md"
28
+ ],
20
29
  "bin": {
21
30
  "cleos-bridge": "./dist/cli.js",
22
31
  "hermes-relay": "./dist/cli.js"
@@ -30,11 +39,13 @@
30
39
  "test": "echo 'No @saleso.innovations/bridge tests configured'"
31
40
  },
32
41
  "dependencies": {
42
+ "better-sqlite3": "^11.10.0",
33
43
  "ws": "^8.18.3"
34
44
  },
35
45
  "devDependencies": {
36
46
  "@repo/eslint-config": "*",
37
47
  "@repo/typescript-config": "*",
48
+ "@types/better-sqlite3": "^7.6.13",
38
49
  "@types/node": "^25.4.0",
39
50
  "@types/ws": "^8.18.1",
40
51
  "eslint": "^9.39.1",