Fix Hermes adapter release package metadata

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-26 13:53:05 +00:00
1 parent 133c080b69
commit fb11ed07fb
7 files changed
+329 -6

No files matched your search

+1
View File
@@ -31,6 +31,7 @@ COPY packages/adapters/cursor-cloud/package.json packages/adapters/cursor-cloud/
COPY packages/adapters/cursor-local/package.json packages/adapters/cursor-local/
COPY packages/adapters/gemini-local/package.json packages/adapters/gemini-local/
COPY packages/adapters/grok-local/package.json packages/adapters/grok-local/
COPY packages/adapters/hermes-local/package.json packages/adapters/hermes-local/
COPY packages/adapters/openclaw-gateway/package.json packages/adapters/openclaw-gateway/
COPY packages/adapters/opencode-local/package.json packages/adapters/opencode-local/
COPY packages/adapters/pi-local/package.json packages/adapters/pi-local/
+4 -4
View File
@@ -210,10 +210,10 @@ Paperclip UI can display both managed and native skills in one view.
## Development
```bash
git clone https://github.com/paperclipai/hermes-paperclip-adapter
cd hermes-paperclip-adapter
npm install
npm run build
git clone https://github.com/paperclipai/paperclip
cd paperclip/packages/adapters/hermes-local
pnpm install
pnpm build
```
## License
+10 -2
View File
@@ -26,7 +26,11 @@
".": "./src/index.ts",
"./server": "./src/server/index.ts",
"./ui": "./src/ui/index.ts",
"./cli": "./src/cli/index.ts"
"./cli": "./src/cli/index.ts",
"./ui-parser": "./ui-parser.cjs"
},
"paperclip": {
"adapterUiParser": "1.0.0"
},
"publishConfig": {
"access": "public",
@@ -46,7 +50,8 @@
"./cli": {
"types": "./dist/cli/index.d.ts",
"import": "./dist/cli/index.js"
}
},
"./ui-parser": "./ui-parser.cjs"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -54,6 +59,7 @@
},
"files": [
"dist",
"ui-parser.cjs",
"README.md",
"LICENSE"
],
@@ -64,6 +70,8 @@
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:command-resolution": "vitest run src/server/command-resolution.test.ts",
"prepack": "node scripts/prepare-publish-package.mjs",
"postpack": "node scripts/restore-dev-package.mjs",
"clean": "rm -rf dist"
},
"dependencies": {
@@ -0,0 +1,106 @@
#!/usr/bin/env node
import { existsSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const packageDir = resolve(scriptDir, "..");
const packageJsonPath = join(packageDir, "package.json");
const devPackageJsonPath = join(packageDir, "package.dev.json");
function findRepoRoot(startDir) {
let current = startDir;
while (current !== dirname(current)) {
if (existsSync(join(current, "pnpm-workspace.yaml"))) {
return current;
}
current = dirname(current);
}
return null;
}
function readJson(filePath) {
return JSON.parse(readFileSync(filePath, "utf8"));
}
function collectWorkspaceVersions(repoRoot) {
const roots = ["packages", "server", "ui", "cli"];
const versions = new Map();
function walk(relDir) {
const absDir = join(repoRoot, relDir);
if (!existsSync(absDir)) return;
const pkgPath = join(absDir, "package.json");
if (existsSync(pkgPath)) {
const pkg = readJson(pkgPath);
if (pkg.name && pkg.version) {
versions.set(pkg.name, pkg.version);
}
return;
}
for (const entry of readdirSync(absDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") continue;
walk(join(relDir, entry.name));
}
}
for (const root of roots) {
walk(root);
}
return versions;
}
function rewriteWorkspaceDeps(deps, workspaceVersions) {
if (!deps) return deps;
return Object.fromEntries(
Object.entries(deps).map(([name, version]) => {
if (typeof version !== "string" || !version.startsWith("workspace:")) {
return [name, version];
}
const resolvedVersion = workspaceVersions.get(name);
if (!resolvedVersion) {
throw new Error(`Cannot resolve workspace dependency ${name} for publish package`);
}
return [name, resolvedVersion];
}),
);
}
const pkg = readJson(packageJsonPath);
const publishConfig = pkg.publishConfig ?? {};
if (existsSync(devPackageJsonPath)) {
throw new Error(`Refusing to overwrite existing ${devPackageJsonPath}`);
}
if (!publishConfig.exports) {
throw new Error(`${pkg.name} is missing publishConfig.exports`);
}
const repoRoot = findRepoRoot(packageDir);
const workspaceVersions = repoRoot ? collectWorkspaceVersions(repoRoot) : new Map();
renameSync(packageJsonPath, devPackageJsonPath);
const nextPublishConfig = { ...publishConfig };
delete nextPublishConfig.exports;
delete nextPublishConfig.main;
delete nextPublishConfig.types;
const publishPkg = {
...pkg,
exports: publishConfig.exports,
main: publishConfig.main,
types: publishConfig.types,
publishConfig: nextPublishConfig,
dependencies: rewriteWorkspaceDeps(pkg.dependencies, workspaceVersions),
optionalDependencies: rewriteWorkspaceDeps(pkg.optionalDependencies, workspaceVersions),
peerDependencies: rewriteWorkspaceDeps(pkg.peerDependencies, workspaceVersions),
};
writeFileSync(packageJsonPath, `${JSON.stringify(publishPkg, null, 2)}\n`);
@@ -0,0 +1,14 @@
#!/usr/bin/env node
import { existsSync, renameSync, rmSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const packageDir = resolve(scriptDir, "..");
const packageJsonPath = join(packageDir, "package.json");
const devPackageJsonPath = join(packageDir, "package.dev.json");
if (existsSync(devPackageJsonPath)) {
rmSync(packageJsonPath, { force: true });
renameSync(devPackageJsonPath, packageJsonPath);
}
@@ -72,6 +72,7 @@ function parseToolCompletionLine(
let verbAndDetail = durationMatch
? cleaned.slice(0, cleaned.lastIndexOf(durationMatch[0])).trim()
: cleaned;
verbAndDetail = verbAndDetail.replace(/^\p{Emoji_Presentation}\s*/u, "");
// Check for error suffixes
const hasError = /\[(?:exit \d+|error|full)\]/.test(verbAndDetail) ||
@@ -0,0 +1,193 @@
"use strict";
const TOOL_OUTPUT_PREFIX = "\u250a";
function stripKaomoji(text) {
return text.replace(/[(][^()]{2,20}[)]\s*/gu, "").trim();
}
function isAssistantToolLine(stripped) {
return /^\u250a\s*\u{1f4ac}/u.test(stripped);
}
function extractAssistantText(line) {
return line.replace(/^[\s\u250a]*\u{1f4ac}\s*/u, "").trim();
}
function parseToolCompletionLine(line) {
let cleaned = line.trim().replace(/^\[done\]\s*/, "");
if (!cleaned.startsWith(TOOL_OUTPUT_PREFIX)) return null;
cleaned = cleaned.slice(TOOL_OUTPUT_PREFIX.length);
cleaned = stripKaomoji(cleaned).trim();
const durationMatch = cleaned.match(/([\d.]+s)\s*(?:\([\d.]+s\))?\s*$/);
const duration = durationMatch ? durationMatch[1] : "";
const verbAndDetail = durationMatch
? cleaned.slice(0, cleaned.lastIndexOf(durationMatch[0])).trim()
: cleaned;
const detailWithoutEmoji = verbAndDetail.replace(/^\p{Emoji_Presentation}\s*/u, "");
const hasError = /\[(?:exit \d+|error|full)\]/.test(detailWithoutEmoji) ||
/\[error\]\s*$/.test(cleaned);
const parts = detailWithoutEmoji.match(/^(\S+)\s+(.*)/);
if (!parts) {
return { name: "tool", detail: detailWithoutEmoji, duration, hasError };
}
const verb = parts[1];
const detail = parts[2].trim();
const nameMap = {
"$": "shell",
exec: "shell",
terminal: "shell",
search: "search",
fetch: "fetch",
crawl: "crawl",
navigate: "browser",
snapshot: "browser",
click: "browser",
type: "browser",
scroll: "browser",
back: "browser",
press: "browser",
close: "browser",
images: "browser",
vision: "browser",
read: "read",
write: "write",
patch: "patch",
grep: "search",
find: "search",
plan: "plan",
recall: "recall",
proc: "process",
delegate: "delegate",
todo: "todo",
memory: "memory",
clarify: "clarify",
session_search: "recall",
code: "execute",
execute: "execute",
web_search: "search",
web_extract: "fetch",
browser_navigate: "browser",
browser_click: "browser",
browser_type: "browser",
browser_snapshot: "browser",
browser_vision: "browser",
browser_scroll: "browser",
browser_press: "browser",
browser_back: "browser",
browser_close: "browser",
browser_get_images: "browser",
read_file: "read",
write_file: "write_file",
search_files: "search",
patch_file: "patch",
execute_code: "execute",
};
return {
name: nameMap[verb.toLowerCase()] || verb,
detail,
duration,
hasError,
};
}
let toolCallCounter = 0;
function syntheticToolUseId() {
toolCallCounter += 1;
return `hermes-tool-${toolCallCounter}`;
}
function isThinkingLine(line) {
return (
line.includes("\u{1f4ad}") ||
line.startsWith("<thinking>") ||
line.startsWith("</thinking>") ||
line.startsWith("Thinking:")
);
}
function parseStdoutLine(line, ts) {
const trimmed = line.trim();
if (!trimmed) return [];
if (trimmed.startsWith("[hermes]") || trimmed.startsWith("[paperclip]")) {
return [{ kind: "system", ts, text: trimmed }];
}
if (trimmed.startsWith("[tool]")) {
return [];
}
if (/^\[\d{4}-\d{2}-\d{2}T/.test(trimmed)) {
return [{ kind: "stderr", ts, text: trimmed }];
}
if (/^\p{Emoji_Presentation}\s*(Completed|Running|Error)?\s*$/u.test(trimmed)) {
return [];
}
if (trimmed.startsWith("session_id:")) {
return [{ kind: "system", ts, text: trimmed }];
}
if (trimmed.includes(TOOL_OUTPUT_PREFIX)) {
if (isAssistantToolLine(trimmed)) {
return [{ kind: "assistant", ts, text: extractAssistantText(trimmed) }];
}
const toolInfo = parseToolCompletionLine(trimmed);
if (toolInfo) {
const id = syntheticToolUseId();
const detailText = toolInfo.duration
? `${toolInfo.detail} ${toolInfo.duration}`
: toolInfo.detail;
return [
{
kind: "tool_call",
ts,
name: toolInfo.name,
input: { detail: toolInfo.detail },
toolUseId: id,
},
{
kind: "tool_result",
ts,
toolUseId: id,
content: detailText,
isError: toolInfo.hasError,
},
];
}
const escapedPrefix = TOOL_OUTPUT_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const stripped = trimmed
.replace(/^\[done\]\s*/, "")
.replace(new RegExp(`^${escapedPrefix}\\s*`), "")
.trim();
return [{ kind: "stdout", ts, text: stripped }];
}
if (isThinkingLine(trimmed)) {
return [{ kind: "thinking", ts, text: trimmed.replace(/^\u{1f4ad}\s*/u, "") }];
}
if (
trimmed.startsWith("Error:") ||
trimmed.startsWith("ERROR:") ||
trimmed.startsWith("Traceback")
) {
return [{ kind: "stderr", ts, text: trimmed }];
}
return [{ kind: "assistant", ts, text: trimmed }];
}
module.exports = { parseStdoutLine };