@fancyboi999/open-tag-daemon 0.7.1 → 0.8.1
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/dist/agent-cli.mjs +25 -3
- package/dist/cli.mjs +495 -45
- package/package.json +1 -1
package/dist/agent-cli.mjs
CHANGED
|
@@ -3047,7 +3047,7 @@ var {
|
|
|
3047
3047
|
} = import_index.default;
|
|
3048
3048
|
|
|
3049
3049
|
// src/cli/index.ts
|
|
3050
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
3050
|
+
import { readFile, writeFile, mkdir, appendFile } from "node:fs/promises";
|
|
3051
3051
|
import { basename, join } from "node:path";
|
|
3052
3052
|
|
|
3053
3053
|
// src/log.ts
|
|
@@ -3110,6 +3110,7 @@ var log = createLogger("cli");
|
|
|
3110
3110
|
var BASE = process.env.OPEN_TAG_SERVER_URL ?? "http://localhost:7777";
|
|
3111
3111
|
var KEY = process.env.OPEN_TAG_AGENT_TOKEN ?? process.env.OPEN_TAG_MACHINE_KEY ?? process.env.OPEN_TAG_API_KEY ?? "poc-secret-key";
|
|
3112
3112
|
var AGENT = process.env.OPEN_TAG_AGENT_ID ?? "";
|
|
3113
|
+
var TURN_FILE = process.env.OPEN_TAG_TURN_FILE ?? "";
|
|
3113
3114
|
function headers() {
|
|
3114
3115
|
return { authorization: `Bearer ${KEY}`, "x-agent-id": AGENT, "content-type": "application/json" };
|
|
3115
3116
|
}
|
|
@@ -3147,12 +3148,28 @@ function readStdin() {
|
|
|
3147
3148
|
process.stdin.on("end", () => resolve(d));
|
|
3148
3149
|
});
|
|
3149
3150
|
}
|
|
3151
|
+
function targetFromText(text) {
|
|
3152
|
+
const m = /^\[target=([^\s\]]+)/.exec(text);
|
|
3153
|
+
return m?.[1] ?? null;
|
|
3154
|
+
}
|
|
3155
|
+
async function recordTurnEvent(event) {
|
|
3156
|
+
if (!TURN_FILE) return;
|
|
3157
|
+
try {
|
|
3158
|
+
await appendFile(TURN_FILE, JSON.stringify({ ...event, at: Date.now() }) + "\n");
|
|
3159
|
+
} catch {
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3150
3162
|
var program2 = new Command();
|
|
3151
3163
|
program2.name("open-tag").description("open-tag agent CLI").version("0.1.0");
|
|
3152
3164
|
var message = program2.command("message").description("message send/receive");
|
|
3153
3165
|
message.command("check").description("non-blocking check for new messages").action(async () => {
|
|
3154
3166
|
const d = await api("GET", "/agent-api/message/check");
|
|
3155
|
-
if (!d.messages?.length)
|
|
3167
|
+
if (!d.messages?.length) {
|
|
3168
|
+
await recordTurnEvent({ type: "check", count: 0 });
|
|
3169
|
+
return console.log("No new messages.");
|
|
3170
|
+
}
|
|
3171
|
+
const targets = Array.from(new Set((d.messages ?? []).map((m) => targetFromText(String(m.text ?? ""))).filter(Boolean)));
|
|
3172
|
+
await recordTurnEvent({ type: "check", count: d.messages.length, target: targets[targets.length - 1] ?? null, targets });
|
|
3156
3173
|
for (const m of d.messages) console.log(m.text);
|
|
3157
3174
|
console.log("No more new messages.");
|
|
3158
3175
|
});
|
|
@@ -3166,7 +3183,11 @@ message.command("send").description("send a message (body read from stdin); if n
|
|
|
3166
3183
|
process.exit(1);
|
|
3167
3184
|
}
|
|
3168
3185
|
const d = await api("POST", "/agent-api/message/send", { target: opts.target, content, attachmentIds, sendDraft });
|
|
3169
|
-
if (d.held)
|
|
3186
|
+
if (d.held) {
|
|
3187
|
+
await recordTurnEvent({ type: "held", target: opts.target });
|
|
3188
|
+
return console.log(d.text);
|
|
3189
|
+
}
|
|
3190
|
+
await recordTurnEvent({ type: "send", target: opts.target, id: d.id, seq: d.seq });
|
|
3170
3191
|
console.log(`Sent to ${opts.target} (msg ${String(d.id).slice(0, 8)}, seq ${d.seq})`);
|
|
3171
3192
|
});
|
|
3172
3193
|
message.command("read").description("read channel history (supports anchor flags --before/--after/--around: message short/full id or seq, jumps to the specified context)").requiredOption("--channel <channel>").option("--limit <n>", "number of messages", "50").option("--around <idOrSeq>", "fetch context centered on this message").option("--before <idOrSeq>", "fetch messages before this one").option("--after <idOrSeq>", "fetch messages after this one").action(async (opts) => {
|
|
@@ -3175,6 +3196,7 @@ message.command("read").description("read channel history (supports anchor flags
|
|
|
3175
3196
|
else if (opts.before) q.set("before", opts.before);
|
|
3176
3197
|
else if (opts.after) q.set("after", opts.after);
|
|
3177
3198
|
const d = await api("GET", `/agent-api/message/read?${q}`);
|
|
3199
|
+
await recordTurnEvent({ type: "read", target: opts.channel, count: d.messages?.length ?? 0 });
|
|
3178
3200
|
for (const m of d.messages ?? []) console.log(m.text);
|
|
3179
3201
|
});
|
|
3180
3202
|
message.command("react").description("add or remove a reaction emoji on a message (lightweight feedback)").requiredOption("--message-id <id>").requiredOption("--emoji <emoji>", "e.g. \u{1F44D} \u2705").option("--remove", "remove the reaction instead of adding it").action(async (opts) => {
|
package/dist/cli.mjs
CHANGED
|
@@ -3725,7 +3725,7 @@ try {
|
|
|
3725
3725
|
// src/daemon/index.ts
|
|
3726
3726
|
import os4 from "node:os";
|
|
3727
3727
|
import fs3 from "node:fs";
|
|
3728
|
-
import
|
|
3728
|
+
import path14 from "node:path";
|
|
3729
3729
|
|
|
3730
3730
|
// node_modules/ws/wrapper.mjs
|
|
3731
3731
|
var import_stream = __toESM(require_stream(), 1);
|
|
@@ -3913,8 +3913,8 @@ var Connection = class {
|
|
|
3913
3913
|
};
|
|
3914
3914
|
|
|
3915
3915
|
// src/daemon/agentManager.ts
|
|
3916
|
-
import { mkdir, writeFile, readFile, access, rm } from "node:fs/promises";
|
|
3917
|
-
import
|
|
3916
|
+
import { mkdir, writeFile, readFile as readFile2, access, rm } from "node:fs/promises";
|
|
3917
|
+
import path11 from "node:path";
|
|
3918
3918
|
import os2 from "node:os";
|
|
3919
3919
|
|
|
3920
3920
|
// src/daemon/prompt.ts
|
|
@@ -4033,6 +4033,7 @@ ${c.description}. This may evolve.` : ""}`;
|
|
|
4033
4033
|
}
|
|
4034
4034
|
var STARTUP_NUDGE = "You just started \u2014 someone messaged you. FIRST run `open-tag message check` to read the message(s) waiting for you, handle them fully, reply with `open-tag message send`, then stop.";
|
|
4035
4035
|
var RESUME_NUDGE = "You were woken because new messages may be waiting. Run `open-tag message check` to read them, handle them, reply with `open-tag message send`, then stop.";
|
|
4036
|
+
var ONE_SHOT_WAKE_NUDGE = "You were woken by a new open-tag delivery. FIRST run `open-tag message check` now, handle the pending message(s), and send exactly one reply with `open-tag message send`. Do not end this turn with stdout only; only `open-tag message send` reaches the human.";
|
|
4036
4037
|
function inboxNotice(o) {
|
|
4037
4038
|
const plural = (n) => n === 1 ? "" : "s";
|
|
4038
4039
|
const changed = o.changedTargets ?? 1;
|
|
@@ -4043,7 +4044,7 @@ function inboxNotice(o) {
|
|
|
4043
4044
|
Inbox update: ${o.count} unread message${plural(o.count)} total; ${changed} changed target${plural(changed)}
|
|
4044
4045
|
${o.targetName} pending: ${o.count} message${plural(o.count)}${first} \xB7 latest @${o.from}${latest}${suffix}
|
|
4045
4046
|
]
|
|
4046
|
-
Content-free signal \u2014 message bodies are withheld, not absent. Finish your current step, then run \`open-tag message check\` to read and handle. Never conclude "no work" from this notice alone.`;
|
|
4047
|
+
Content-free signal \u2014 message bodies are withheld, not absent. Finish your current step, then run \`open-tag message check\` to read and handle. If this notice is the only thing in your current turn, check now and reply with \`open-tag message send\`; do not finish with stdout only. Never conclude "no work" from this notice alone.`;
|
|
4047
4048
|
}
|
|
4048
4049
|
|
|
4049
4050
|
// src/daemon/memory.ts
|
|
@@ -4418,8 +4419,15 @@ var codexRuntime = {
|
|
|
4418
4419
|
const proc = spawn2("codex", ["app-server", "--listen", "stdio://"], { cwd: opts.cwd, stdio: ["pipe", "pipe", "pipe"], env: opts.env });
|
|
4419
4420
|
const client = new CodexClient(proc, cb);
|
|
4420
4421
|
let ready = false;
|
|
4422
|
+
let spawnFailed = false;
|
|
4423
|
+
let reportedExit = false;
|
|
4421
4424
|
const queue = [];
|
|
4422
4425
|
let turnBusy = false;
|
|
4426
|
+
function reportExit(code) {
|
|
4427
|
+
if (reportedExit) return;
|
|
4428
|
+
reportedExit = true;
|
|
4429
|
+
cb.onExit(code);
|
|
4430
|
+
}
|
|
4423
4431
|
client.onTurnDone = () => {
|
|
4424
4432
|
turnBusy = false;
|
|
4425
4433
|
pump();
|
|
@@ -4430,6 +4438,7 @@ var codexRuntime = {
|
|
|
4430
4438
|
turnBusy = true;
|
|
4431
4439
|
cb.onActivity("working", "turn");
|
|
4432
4440
|
client.request("turn/start", turnParams(opts, client.threadId, text)).catch((e) => {
|
|
4441
|
+
if (spawnFailed) return;
|
|
4433
4442
|
cb.log.warn("codex turn/start failed", { detail: String(e?.message ?? e) });
|
|
4434
4443
|
turnBusy = false;
|
|
4435
4444
|
pump();
|
|
@@ -4465,6 +4474,7 @@ var codexRuntime = {
|
|
|
4465
4474
|
queue.push(opts.initialPrompt);
|
|
4466
4475
|
pump();
|
|
4467
4476
|
} catch (e) {
|
|
4477
|
+
if (spawnFailed) return;
|
|
4468
4478
|
cb.log.error("codex init failed", { detail: String(e?.message ?? e) });
|
|
4469
4479
|
cb.onActivity("offline", "codex init failed");
|
|
4470
4480
|
}
|
|
@@ -4473,9 +4483,17 @@ var codexRuntime = {
|
|
|
4473
4483
|
const t = c.toString().trim();
|
|
4474
4484
|
if (t) cb.log.debug("codex stderr", { t: t.slice(0, 300) });
|
|
4475
4485
|
});
|
|
4486
|
+
proc.on("error", (e) => {
|
|
4487
|
+
spawnFailed = true;
|
|
4488
|
+
const detail = e.code === "ENOENT" ? "codex not found" : "codex spawn failed";
|
|
4489
|
+
client.closeAllPending(new Error(detail));
|
|
4490
|
+
cb.log.error("codex spawn failed", { detail: String(e?.message ?? e), code: e.code ?? "" });
|
|
4491
|
+
cb.onActivity("offline", detail);
|
|
4492
|
+
reportExit(1);
|
|
4493
|
+
});
|
|
4476
4494
|
proc.on("exit", (code) => {
|
|
4477
4495
|
client.closeAllPending(new Error("codex exited"));
|
|
4478
|
-
|
|
4496
|
+
reportExit(code);
|
|
4479
4497
|
});
|
|
4480
4498
|
return { deliver: (text) => {
|
|
4481
4499
|
queue.push(text);
|
|
@@ -5376,6 +5394,302 @@ var cursorRuntime = {
|
|
|
5376
5394
|
}
|
|
5377
5395
|
};
|
|
5378
5396
|
|
|
5397
|
+
// src/daemon/hermesRuntime.ts
|
|
5398
|
+
import { spawn as spawn8 } from "node:child_process";
|
|
5399
|
+
import { existsSync } from "node:fs";
|
|
5400
|
+
import { readFile, unlink } from "node:fs/promises";
|
|
5401
|
+
import { homedir, tmpdir } from "node:os";
|
|
5402
|
+
import path10 from "node:path";
|
|
5403
|
+
var MAX8 = 4e3;
|
|
5404
|
+
var clip8 = (s) => String(s ?? "").slice(0, MAX8);
|
|
5405
|
+
var FINAL_RESPONSE_MAX = 2400;
|
|
5406
|
+
function hermesProfile(model, runtimeConfig) {
|
|
5407
|
+
const configured = runtimeConfig?.profile;
|
|
5408
|
+
if (typeof configured === "string" && configured.trim()) return configured.trim();
|
|
5409
|
+
if (model && model !== "default") return model;
|
|
5410
|
+
return "default";
|
|
5411
|
+
}
|
|
5412
|
+
function hermesProfileRoots(home = homedir(), env = process.env) {
|
|
5413
|
+
return [env.HERMES_PROFILE_DIR, path10.join(home, ".hermes", "profiles")].filter((v) => !!v);
|
|
5414
|
+
}
|
|
5415
|
+
function buildHermesPrompt(message, opts) {
|
|
5416
|
+
return [
|
|
5417
|
+
"[OpenTag runtime context]",
|
|
5418
|
+
`You are running as an OpenTag agent in this isolated workspace: ${opts.cwd}`,
|
|
5419
|
+
"Follow this OpenTag system prompt for collaboration, @mentions, and reporting:",
|
|
5420
|
+
opts.systemPrompt,
|
|
5421
|
+
"",
|
|
5422
|
+
"[OpenTag message]",
|
|
5423
|
+
message
|
|
5424
|
+
].join("\n");
|
|
5425
|
+
}
|
|
5426
|
+
function buildHermesArgs(prompt, sessionId) {
|
|
5427
|
+
const args2 = ["chat", "-q", prompt, "-Q", "--source", "open-tag"];
|
|
5428
|
+
if (sessionId) args2.push("--resume", sessionId);
|
|
5429
|
+
return args2;
|
|
5430
|
+
}
|
|
5431
|
+
function parseHermesSessionId(stderr) {
|
|
5432
|
+
const matches = [...stderr.matchAll(/^session_id:\s*(\S+)\s*$/gm)];
|
|
5433
|
+
return matches.length ? matches[matches.length - 1][1] : null;
|
|
5434
|
+
}
|
|
5435
|
+
function isMissingHermesSession(stderr) {
|
|
5436
|
+
return /Session not found:/i.test(stderr);
|
|
5437
|
+
}
|
|
5438
|
+
function parseHermesTurnEvents(jsonl) {
|
|
5439
|
+
const state = { sent: false, held: false, engaged: false, target: null };
|
|
5440
|
+
for (const line of jsonl.split("\n")) {
|
|
5441
|
+
if (!line.trim()) continue;
|
|
5442
|
+
let evt;
|
|
5443
|
+
try {
|
|
5444
|
+
evt = JSON.parse(line);
|
|
5445
|
+
} catch {
|
|
5446
|
+
continue;
|
|
5447
|
+
}
|
|
5448
|
+
if (evt.type === "send") state.sent = true;
|
|
5449
|
+
if (evt.type === "held") state.held = true;
|
|
5450
|
+
if ((evt.type === "check" || evt.type === "read") && typeof evt.target === "string" && evt.target.trim()) {
|
|
5451
|
+
state.engaged = true;
|
|
5452
|
+
state.target = evt.target.trim();
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
return state;
|
|
5456
|
+
}
|
|
5457
|
+
function cleanHermesStdout(stdout) {
|
|
5458
|
+
let lines = stdout.trim().split(/\r?\n/).map((line) => line.trimEnd());
|
|
5459
|
+
while (lines.length && !lines[0].trim()) lines.shift();
|
|
5460
|
+
while (lines.length && /^(⚠|Warning:)/.test(lines[0].trim())) lines.shift();
|
|
5461
|
+
while (lines.length && !lines[0].trim()) lines.shift();
|
|
5462
|
+
const content = lines.join("\n").trim();
|
|
5463
|
+
if (!content) return { ok: false, reason: "empty-stdout" };
|
|
5464
|
+
if (/^(No new messages\.|Sent to |Freshness hold:)/.test(content)) return { ok: false, reason: "cli-output" };
|
|
5465
|
+
if (/^(Error:|Code:|Exception:|Traceback\b|Unhandled\b)/i.test(content)) return { ok: false, reason: "error-output" };
|
|
5466
|
+
if (/^(┊|diff --git\b|@@\s+-|\+\+\+ |--- )/m.test(content) || /\na\/.+\s+→\s+b\/.+/.test(content)) {
|
|
5467
|
+
return { ok: false, reason: "diff-output" };
|
|
5468
|
+
}
|
|
5469
|
+
return { ok: true, target: "", content: content.slice(0, FINAL_RESPONSE_MAX) };
|
|
5470
|
+
}
|
|
5471
|
+
function hermesBridgeDecision(stdout, state) {
|
|
5472
|
+
if (state.sent) return { ok: false, reason: "already-sent" };
|
|
5473
|
+
if (state.held) return { ok: false, reason: "already-held" };
|
|
5474
|
+
if (!state.engaged || !state.target) return { ok: false, reason: "no-open-tag-read" };
|
|
5475
|
+
if (!/^(#|dm:|thread:)/.test(state.target)) return { ok: false, reason: "invalid-target" };
|
|
5476
|
+
const cleaned = cleanHermesStdout(stdout);
|
|
5477
|
+
if (!cleaned.ok) return cleaned;
|
|
5478
|
+
return { ok: true, target: state.target, content: cleaned.content };
|
|
5479
|
+
}
|
|
5480
|
+
async function responseJson(res) {
|
|
5481
|
+
try {
|
|
5482
|
+
return await res.json();
|
|
5483
|
+
} catch {
|
|
5484
|
+
return {};
|
|
5485
|
+
}
|
|
5486
|
+
}
|
|
5487
|
+
async function postHermesBridgeMessage(fetchImpl, serverUrl2, headers, target, content) {
|
|
5488
|
+
const url = `${serverUrl2}/agent-api/message/send`;
|
|
5489
|
+
const first = await fetchImpl(url, {
|
|
5490
|
+
method: "POST",
|
|
5491
|
+
headers,
|
|
5492
|
+
body: JSON.stringify({ target, content })
|
|
5493
|
+
});
|
|
5494
|
+
const firstBody = await responseJson(first);
|
|
5495
|
+
if (!first.ok) return { ok: false, status: first.status };
|
|
5496
|
+
if (!firstBody?.held) return { ok: true };
|
|
5497
|
+
const held = { ok: false, held: true, sentDraft: false };
|
|
5498
|
+
if (typeof firstBody.text === "string") held.text = firstBody.text;
|
|
5499
|
+
return held;
|
|
5500
|
+
}
|
|
5501
|
+
function hermesProfileHome(profile, home = homedir(), roots = hermesProfileRoots(home)) {
|
|
5502
|
+
if (!profile || profile === "default") return null;
|
|
5503
|
+
for (const root of roots) {
|
|
5504
|
+
const dir = path10.join(root, profile);
|
|
5505
|
+
if (existsSync(dir)) return dir;
|
|
5506
|
+
}
|
|
5507
|
+
return null;
|
|
5508
|
+
}
|
|
5509
|
+
function hermesRuntimeEnv(baseEnv, cwd, requestedProfile, home = homedir()) {
|
|
5510
|
+
const env = { ...baseEnv, PWD: cwd };
|
|
5511
|
+
delete env.NODE_OPTIONS;
|
|
5512
|
+
delete env.HERMES_HOME;
|
|
5513
|
+
delete env.HERMES_PROFILE;
|
|
5514
|
+
const profileHome = hermesProfileHome(requestedProfile, home, hermesProfileRoots(home, env));
|
|
5515
|
+
const profile = requestedProfile === "default" || profileHome ? requestedProfile : "default";
|
|
5516
|
+
if (profileHome) {
|
|
5517
|
+
env.HERMES_HOME = profileHome;
|
|
5518
|
+
env.HERMES_PROFILE = profile;
|
|
5519
|
+
}
|
|
5520
|
+
return { env, profile };
|
|
5521
|
+
}
|
|
5522
|
+
var HermesRun = class {
|
|
5523
|
+
constructor(opts, cb) {
|
|
5524
|
+
this.opts = opts;
|
|
5525
|
+
this.cb = cb;
|
|
5526
|
+
const requestedProfile = hermesProfile(opts.model, opts.runtimeConfig);
|
|
5527
|
+
const resolved = hermesRuntimeEnv(opts.env, opts.cwd, requestedProfile);
|
|
5528
|
+
this.env = resolved.env;
|
|
5529
|
+
this.profile = resolved.profile;
|
|
5530
|
+
if (requestedProfile !== "default" && this.profile === "default") {
|
|
5531
|
+
cb.log.warn("hermes profile not found; using default profile", { profile: requestedProfile });
|
|
5532
|
+
}
|
|
5533
|
+
this.sessionId = opts.sessionId ?? null;
|
|
5534
|
+
if (this.sessionId) cb.onSession(this.sessionId);
|
|
5535
|
+
if (opts.initialPrompt.trim()) this.enqueue(opts.initialPrompt);
|
|
5536
|
+
}
|
|
5537
|
+
opts;
|
|
5538
|
+
cb;
|
|
5539
|
+
queue = [];
|
|
5540
|
+
turnBusy = false;
|
|
5541
|
+
stopped = false;
|
|
5542
|
+
proc = null;
|
|
5543
|
+
everSucceeded = false;
|
|
5544
|
+
env;
|
|
5545
|
+
profile;
|
|
5546
|
+
sessionId;
|
|
5547
|
+
enqueue(text) {
|
|
5548
|
+
if (this.stopped || !text.trim()) return;
|
|
5549
|
+
this.queue.push(text);
|
|
5550
|
+
this.pump();
|
|
5551
|
+
}
|
|
5552
|
+
pump() {
|
|
5553
|
+
if (this.stopped || this.turnBusy || this.queue.length === 0) return;
|
|
5554
|
+
this.runTurn(this.queue.shift());
|
|
5555
|
+
}
|
|
5556
|
+
runTurn(message) {
|
|
5557
|
+
this.turnBusy = true;
|
|
5558
|
+
this.cb.onActivity("working", `hermes/${this.profile}`);
|
|
5559
|
+
const prompt = buildHermesPrompt(message, this.opts);
|
|
5560
|
+
const args2 = buildHermesArgs(prompt, this.sessionId);
|
|
5561
|
+
const turnFile = path10.join(tmpdir(), `open-tag-hermes-turn-${Date.now()}-${Math.random().toString(36).slice(2)}.jsonl`);
|
|
5562
|
+
const proc = spawn8("hermes", args2, { cwd: this.opts.cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...this.env, OPEN_TAG_TURN_FILE: turnFile } });
|
|
5563
|
+
this.proc = proc;
|
|
5564
|
+
let stdout = "";
|
|
5565
|
+
const errTail = [];
|
|
5566
|
+
let errLen = 0;
|
|
5567
|
+
proc.stdout?.on("data", (c) => {
|
|
5568
|
+
if (this.stopped) return;
|
|
5569
|
+
if (stdout.length < MAX8) stdout += c.toString();
|
|
5570
|
+
});
|
|
5571
|
+
proc.stderr?.on("data", (c) => {
|
|
5572
|
+
const t = c.toString();
|
|
5573
|
+
errTail.push(t);
|
|
5574
|
+
errLen += t.length;
|
|
5575
|
+
while (errLen > 16384 && errTail.length > 1) errLen -= errTail.shift().length;
|
|
5576
|
+
});
|
|
5577
|
+
proc.on("error", (e) => {
|
|
5578
|
+
this.proc = null;
|
|
5579
|
+
this.turnBusy = false;
|
|
5580
|
+
if (this.stopped) return;
|
|
5581
|
+
this.cb.log.error("hermes spawn failed", { detail: String(e?.message ?? e) });
|
|
5582
|
+
this.cb.onActivity("offline", "hermes not found");
|
|
5583
|
+
if (!this.everSucceeded) this.cb.onExit(1);
|
|
5584
|
+
else this.pump();
|
|
5585
|
+
});
|
|
5586
|
+
proc.on("exit", async (code) => {
|
|
5587
|
+
this.proc = null;
|
|
5588
|
+
if (this.stopped) return;
|
|
5589
|
+
const out = stdout.trim();
|
|
5590
|
+
const tail = errTail.join("").trim();
|
|
5591
|
+
if (code === 0) {
|
|
5592
|
+
this.everSucceeded = true;
|
|
5593
|
+
const nextSessionId = parseHermesSessionId(tail);
|
|
5594
|
+
if (nextSessionId && nextSessionId !== this.sessionId) {
|
|
5595
|
+
this.sessionId = nextSessionId;
|
|
5596
|
+
this.cb.onSession(nextSessionId);
|
|
5597
|
+
}
|
|
5598
|
+
const bridged = await this.bridgeFinalResponse(turnFile, out);
|
|
5599
|
+
if (out) this.cb.onTrajectory([{ kind: "text", text: clip8(out) }]);
|
|
5600
|
+
if (bridged !== false) this.cb.onActivity("online", "");
|
|
5601
|
+
this.turnBusy = false;
|
|
5602
|
+
this.pump();
|
|
5603
|
+
return;
|
|
5604
|
+
}
|
|
5605
|
+
if (this.sessionId && isMissingHermesSession(tail)) {
|
|
5606
|
+
this.cb.log.warn("hermes resume session missing; retrying fresh", { sessionId: this.sessionId });
|
|
5607
|
+
this.sessionId = null;
|
|
5608
|
+
this.cb.onSession(null);
|
|
5609
|
+
this.queue.unshift(message);
|
|
5610
|
+
this.turnBusy = false;
|
|
5611
|
+
this.pump();
|
|
5612
|
+
return;
|
|
5613
|
+
}
|
|
5614
|
+
const last = tail.split("\n").filter(Boolean).pop() || `hermes exited ${code ?? "signal"}`;
|
|
5615
|
+
this.cb.onTrajectory([{ kind: "text", text: "[hermes error] " + clip8(tail || last).slice(0, 800) }]);
|
|
5616
|
+
this.cb.onActivity("error", last.slice(0, 200));
|
|
5617
|
+
this.turnBusy = false;
|
|
5618
|
+
if (!this.everSucceeded) {
|
|
5619
|
+
this.cb.onExit(code ?? 1);
|
|
5620
|
+
return;
|
|
5621
|
+
}
|
|
5622
|
+
this.pump();
|
|
5623
|
+
});
|
|
5624
|
+
}
|
|
5625
|
+
async bridgeFinalResponse(turnFile, stdout) {
|
|
5626
|
+
let state = { sent: false, held: false, engaged: false, target: null };
|
|
5627
|
+
try {
|
|
5628
|
+
state = parseHermesTurnEvents(await readFile(turnFile, "utf8"));
|
|
5629
|
+
} catch {
|
|
5630
|
+
} finally {
|
|
5631
|
+
try {
|
|
5632
|
+
await unlink(turnFile);
|
|
5633
|
+
} catch {
|
|
5634
|
+
}
|
|
5635
|
+
}
|
|
5636
|
+
const decision = hermesBridgeDecision(stdout, state);
|
|
5637
|
+
if (!decision.ok) {
|
|
5638
|
+
if (stdout.trim() && decision.reason !== "already-sent") {
|
|
5639
|
+
this.cb.log.warn("hermes final response not bridged", { reason: decision.reason });
|
|
5640
|
+
this.cb.onActivity("error", `hermes reply not sent (${decision.reason})`);
|
|
5641
|
+
return false;
|
|
5642
|
+
}
|
|
5643
|
+
return null;
|
|
5644
|
+
}
|
|
5645
|
+
try {
|
|
5646
|
+
const result = await postHermesBridgeMessage(fetch, this.env.OPEN_TAG_SERVER_URL ?? "", {
|
|
5647
|
+
authorization: `Bearer ${this.env.OPEN_TAG_AGENT_TOKEN ?? ""}`,
|
|
5648
|
+
"x-agent-id": this.env.OPEN_TAG_AGENT_ID ?? "",
|
|
5649
|
+
"content-type": "application/json"
|
|
5650
|
+
}, decision.target, decision.content);
|
|
5651
|
+
if (!result.ok) {
|
|
5652
|
+
if (result.held) {
|
|
5653
|
+
this.cb.log.warn("hermes final response freshness-held; draft saved for review", { target: decision.target });
|
|
5654
|
+
if (result.text) this.cb.onTrajectory([{ kind: "status", text: "[open-tag freshness hold]\n" + clip8(result.text) }]);
|
|
5655
|
+
this.cb.onActivity("error", "hermes reply held for freshness review");
|
|
5656
|
+
return false;
|
|
5657
|
+
}
|
|
5658
|
+
this.cb.log.warn("hermes final response bridge failed", { status: result.status, target: decision.target });
|
|
5659
|
+
this.cb.onActivity("error", "hermes bridge send failed");
|
|
5660
|
+
return false;
|
|
5661
|
+
} else {
|
|
5662
|
+
this.cb.log.info("hermes final response bridged", { target: decision.target, chars: decision.content.length, held: !!result.held });
|
|
5663
|
+
return true;
|
|
5664
|
+
}
|
|
5665
|
+
} catch (e) {
|
|
5666
|
+
this.cb.log.warn("hermes final response bridge failed", { detail: String(e?.message ?? e), target: decision.target });
|
|
5667
|
+
this.cb.onActivity("error", "hermes bridge send failed");
|
|
5668
|
+
return false;
|
|
5669
|
+
}
|
|
5670
|
+
}
|
|
5671
|
+
stop() {
|
|
5672
|
+
this.stopped = true;
|
|
5673
|
+
const p = this.proc;
|
|
5674
|
+
this.proc = null;
|
|
5675
|
+
if (p) {
|
|
5676
|
+
try {
|
|
5677
|
+
p.kill("SIGTERM");
|
|
5678
|
+
} catch {
|
|
5679
|
+
}
|
|
5680
|
+
}
|
|
5681
|
+
}
|
|
5682
|
+
};
|
|
5683
|
+
var hermesRuntime = {
|
|
5684
|
+
name: "hermes",
|
|
5685
|
+
experimental: true,
|
|
5686
|
+
oneShotWake: true,
|
|
5687
|
+
start(opts, cb) {
|
|
5688
|
+
const run = new HermesRun(opts, cb);
|
|
5689
|
+
return { deliver: (text) => run.enqueue(text), stop: () => run.stop() };
|
|
5690
|
+
}
|
|
5691
|
+
};
|
|
5692
|
+
|
|
5379
5693
|
// src/daemon/runtimes.ts
|
|
5380
5694
|
function has(tool) {
|
|
5381
5695
|
try {
|
|
@@ -5386,9 +5700,9 @@ function has(tool) {
|
|
|
5386
5700
|
}
|
|
5387
5701
|
}
|
|
5388
5702
|
function detectRuntimes() {
|
|
5389
|
-
return ["claude", "codex", "copilot", "kimi", "opencode", "pi", "cursor-agent"].filter(has).map((t) => t === "cursor-agent" ? "cursor" : t);
|
|
5703
|
+
return ["claude", "codex", "copilot", "kimi", "opencode", "pi", "cursor-agent", "hermes"].filter(has).map((t) => t === "cursor-agent" ? "cursor" : t);
|
|
5390
5704
|
}
|
|
5391
|
-
var REG = { claude: claudeRuntime, codex: codexRuntime, copilot: copilotRuntime, opencode: opencodeRuntime, kimi: kimiRuntime, pi: piRuntime, cursor: cursorRuntime };
|
|
5705
|
+
var REG = { claude: claudeRuntime, codex: codexRuntime, copilot: copilotRuntime, opencode: opencodeRuntime, kimi: kimiRuntime, pi: piRuntime, cursor: cursorRuntime, hermes: hermesRuntime };
|
|
5392
5706
|
function getRuntime(name) {
|
|
5393
5707
|
return REG[name] ?? null;
|
|
5394
5708
|
}
|
|
@@ -5397,14 +5711,28 @@ function getRuntime(name) {
|
|
|
5397
5711
|
var DATA_DIR = agentsDir();
|
|
5398
5712
|
var IDLE_MS = Number(process.env.OPEN_TAG_IDLE_MS ?? 10 * 60 * 1e3);
|
|
5399
5713
|
var DELIVER_DEBOUNCE_MS = Number(process.env.OPEN_TAG_DELIVER_DEBOUNCE_MS ?? 3e3);
|
|
5714
|
+
var ONE_SHOT_DELIVER_DEBOUNCE_MS = Number(process.env.OPEN_TAG_ONE_SHOT_DELIVER_DEBOUNCE_MS ?? process.env.OPEN_TAG_HERMES_DELIVER_DEBOUNCE_MS ?? 500);
|
|
5715
|
+
var PENDING_DELIVER_TTL_MS = Number(process.env.OPEN_TAG_PENDING_DELIVER_TTL_MS ?? 15e3);
|
|
5400
5716
|
var AgentManager = class {
|
|
5401
|
-
constructor(send) {
|
|
5717
|
+
constructor(send, opts = {}) {
|
|
5402
5718
|
this.send = send;
|
|
5403
|
-
this.binDir = ensureOpenTagBin();
|
|
5719
|
+
this.binDir = opts.binDir ?? ensureOpenTagBin();
|
|
5720
|
+
this.dataDir = opts.dataDir ?? DATA_DIR;
|
|
5721
|
+
this.deliverDebounceMs = opts.deliverDebounceMs ?? DELIVER_DEBOUNCE_MS;
|
|
5722
|
+
this.oneShotDeliverDebounceMs = opts.oneShotDeliverDebounceMs ?? ONE_SHOT_DELIVER_DEBOUNCE_MS;
|
|
5723
|
+
this.pendingDeliverTtlMs = opts.pendingDeliverTtlMs ?? PENDING_DELIVER_TTL_MS;
|
|
5724
|
+
this.runtimeResolver = opts.runtimeResolver ?? getRuntime;
|
|
5404
5725
|
}
|
|
5405
5726
|
send;
|
|
5406
5727
|
agents = /* @__PURE__ */ new Map();
|
|
5728
|
+
starting = /* @__PURE__ */ new Map();
|
|
5729
|
+
pendingDelivers = /* @__PURE__ */ new Map();
|
|
5407
5730
|
binDir;
|
|
5731
|
+
dataDir;
|
|
5732
|
+
deliverDebounceMs;
|
|
5733
|
+
oneShotDeliverDebounceMs;
|
|
5734
|
+
pendingDeliverTtlMs;
|
|
5735
|
+
runtimeResolver;
|
|
5408
5736
|
log = createLogger("daemon:agents");
|
|
5409
5737
|
running() {
|
|
5410
5738
|
return [...this.agents.keys()];
|
|
@@ -5414,6 +5742,7 @@ var AgentManager = class {
|
|
|
5414
5742
|
}
|
|
5415
5743
|
// Tear down process: clear timers + remove from map first (critical: deletion before session.stop() lets the onExit has() guard recognize this as an intentional stop, suppressing unexpected sleeping status) + stop runtime. Returns whether the agent was found.
|
|
5416
5744
|
teardown(agentId) {
|
|
5745
|
+
this.clearPendingDeliver(agentId);
|
|
5417
5746
|
const r = this.agents.get(agentId);
|
|
5418
5747
|
if (!r) return false;
|
|
5419
5748
|
if (r.idleTimer) clearTimeout(r.idleTimer);
|
|
@@ -5439,7 +5768,7 @@ var AgentManager = class {
|
|
|
5439
5768
|
async reset(agentId, wipeWorkspace = false, clearMemory = false) {
|
|
5440
5769
|
this.teardown(agentId);
|
|
5441
5770
|
this.send({ type: "agent:session", agentId, sessionId: null });
|
|
5442
|
-
const dir =
|
|
5771
|
+
const dir = path11.join(this.dataDir, agentId);
|
|
5443
5772
|
if (wipeWorkspace) {
|
|
5444
5773
|
try {
|
|
5445
5774
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -5449,7 +5778,7 @@ var AgentManager = class {
|
|
|
5449
5778
|
}
|
|
5450
5779
|
} else if (clearMemory) {
|
|
5451
5780
|
try {
|
|
5452
|
-
await writeFile(
|
|
5781
|
+
await writeFile(path11.join(dir, "MEMORY.md"), "# Memory\n\n(reset)\n");
|
|
5453
5782
|
this.log.info("memory cleared", { agentId });
|
|
5454
5783
|
} catch (e) {
|
|
5455
5784
|
this.log.warn("clearMemory failed", { agentId, detail: String(e) });
|
|
@@ -5463,10 +5792,10 @@ var AgentManager = class {
|
|
|
5463
5792
|
* title + `## Role`, preserving the agent's own sections. No-op if the workspace/file doesn't exist
|
|
5464
5793
|
* yet (a not-yet-started agent gets fresh values from the DB when start() seeds it). */
|
|
5465
5794
|
async syncProfile(agentId, displayName, description) {
|
|
5466
|
-
const mem =
|
|
5795
|
+
const mem = path11.join(this.dataDir, agentId, "MEMORY.md");
|
|
5467
5796
|
let content;
|
|
5468
5797
|
try {
|
|
5469
|
-
content = await
|
|
5798
|
+
content = await readFile2(mem, "utf8");
|
|
5470
5799
|
} catch {
|
|
5471
5800
|
this.log.debug("syncProfile: no MEMORY.md yet", { agentId });
|
|
5472
5801
|
return;
|
|
@@ -5498,16 +5827,24 @@ var AgentManager = class {
|
|
|
5498
5827
|
}
|
|
5499
5828
|
async start(agentId, config) {
|
|
5500
5829
|
if (this.agents.has(agentId)) return;
|
|
5501
|
-
const
|
|
5830
|
+
const existing = this.starting.get(agentId);
|
|
5831
|
+
if (existing) return existing;
|
|
5832
|
+
const pending = this.startNow(agentId, config).finally(() => this.starting.delete(agentId));
|
|
5833
|
+
this.starting.set(agentId, pending);
|
|
5834
|
+
return pending;
|
|
5835
|
+
}
|
|
5836
|
+
async startNow(agentId, config) {
|
|
5837
|
+
if (this.agents.has(agentId)) return;
|
|
5838
|
+
const runtime = this.runtimeResolver(config.runtime ?? "claude");
|
|
5502
5839
|
if (!runtime) {
|
|
5503
5840
|
this.log.error("no runtime", { runtime: config.runtime });
|
|
5504
5841
|
this.send({ type: "agent:activity", agentId, activity: "offline", detail: `no runtime: ${config.runtime}` });
|
|
5505
5842
|
return;
|
|
5506
5843
|
}
|
|
5507
5844
|
if (runtime.experimental) this.log.warn("experimental runtime", { runtime: runtime.name });
|
|
5508
|
-
const dir =
|
|
5509
|
-
await mkdir(
|
|
5510
|
-
const mem =
|
|
5845
|
+
const dir = path11.join(this.dataDir, agentId);
|
|
5846
|
+
await mkdir(path11.join(dir, "notes"), { recursive: true });
|
|
5847
|
+
const mem = path11.join(dir, "MEMORY.md");
|
|
5511
5848
|
try {
|
|
5512
5849
|
await access(mem);
|
|
5513
5850
|
} catch {
|
|
@@ -5554,6 +5891,8 @@ var AgentManager = class {
|
|
|
5554
5891
|
},
|
|
5555
5892
|
log: this.log
|
|
5556
5893
|
};
|
|
5894
|
+
const pendingDeliveryCount = this.pendingDelivers.get(agentId)?.items.length ?? 0;
|
|
5895
|
+
const useOneShotWakeNudge = !!runtime.oneShotWake && pendingDeliveryCount > 0;
|
|
5557
5896
|
this.agents.set(agentId, running);
|
|
5558
5897
|
running.session = runtime.start({
|
|
5559
5898
|
cwd: dir,
|
|
@@ -5562,18 +5901,55 @@ var AgentManager = class {
|
|
|
5562
5901
|
sessionId: config.sessionId,
|
|
5563
5902
|
systemPrompt,
|
|
5564
5903
|
env,
|
|
5565
|
-
initialPrompt: config.sessionId ? RESUME_NUDGE : STARTUP_NUDGE
|
|
5904
|
+
initialPrompt: useOneShotWakeNudge ? ONE_SHOT_WAKE_NUDGE : config.sessionId ? RESUME_NUDGE : STARTUP_NUDGE
|
|
5566
5905
|
}, cb);
|
|
5567
5906
|
this.send({ type: "agent:status", agentId, status: "active" });
|
|
5568
5907
|
this.send({ type: "agent:activity", agentId, activity: "working", detail: "starting" });
|
|
5569
5908
|
this.log.info("agent started", { agentId, runtime: runtime.name, model: config.model ?? "(default)", resume: !!config.sessionId, experimental: runtime.experimental ?? false });
|
|
5570
5909
|
this.resetIdle(agentId);
|
|
5910
|
+
if (useOneShotWakeNudge) {
|
|
5911
|
+
this.clearPendingDeliver(agentId);
|
|
5912
|
+
this.log.debug("pending deliver consumed by one-shot wake nudge", { agentId, runtime: runtime.name, count: pendingDeliveryCount });
|
|
5913
|
+
} else {
|
|
5914
|
+
this.flushPendingDeliver(agentId);
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5917
|
+
queuePendingDeliver(agentId, item) {
|
|
5918
|
+
let q = this.pendingDelivers.get(agentId);
|
|
5919
|
+
if (!q) {
|
|
5920
|
+
const timer = setTimeout(() => {
|
|
5921
|
+
this.pendingDelivers.delete(agentId);
|
|
5922
|
+
this.log.debug("pending deliver expired", { agentId });
|
|
5923
|
+
}, this.pendingDeliverTtlMs);
|
|
5924
|
+
q = { items: [], timer };
|
|
5925
|
+
this.pendingDelivers.set(agentId, q);
|
|
5926
|
+
}
|
|
5927
|
+
q.items.push(item);
|
|
5928
|
+
if (q.items.length > 10) q.items.shift();
|
|
5929
|
+
this.log.debug("deliver queued pending start", { agentId, count: q.items.length });
|
|
5930
|
+
}
|
|
5931
|
+
clearPendingDeliver(agentId) {
|
|
5932
|
+
const q = this.pendingDelivers.get(agentId);
|
|
5933
|
+
if (!q) return;
|
|
5934
|
+
clearTimeout(q.timer);
|
|
5935
|
+
this.pendingDelivers.delete(agentId);
|
|
5936
|
+
}
|
|
5937
|
+
flushPendingDeliver(agentId) {
|
|
5938
|
+
const q = this.pendingDelivers.get(agentId);
|
|
5939
|
+
if (!q) return;
|
|
5940
|
+
this.clearPendingDeliver(agentId);
|
|
5941
|
+
this.log.debug("pending deliver -> agent", { agentId, count: q.items.length });
|
|
5942
|
+
for (const item of q.items) this.deliver(agentId, item.from, item.target, item.mentioned, item.meta);
|
|
5943
|
+
}
|
|
5944
|
+
debounceMsFor(r) {
|
|
5945
|
+
const runtime = this.runtimeResolver(r.config.runtime ?? "claude");
|
|
5946
|
+
return runtime?.oneShotWake ? this.oneShotDeliverDebounceMs : this.deliverDebounceMs;
|
|
5571
5947
|
}
|
|
5572
|
-
/** server agent:deliver — wake a running agent with new messages;
|
|
5948
|
+
/** server agent:deliver — wake a running agent with new messages; if start is still preparing the workspace, briefly queue and flush once the runtime exists. */
|
|
5573
5949
|
deliver(agentId, from, target, mentioned = false, meta = {}) {
|
|
5574
5950
|
const r = this.agents.get(agentId);
|
|
5575
5951
|
if (!r) {
|
|
5576
|
-
this.
|
|
5952
|
+
this.queuePendingDeliver(agentId, { from, target, mentioned, meta });
|
|
5577
5953
|
return;
|
|
5578
5954
|
}
|
|
5579
5955
|
const tname = meta.targetName ?? target;
|
|
@@ -5601,29 +5977,29 @@ var AgentManager = class {
|
|
|
5601
5977
|
} catch (e) {
|
|
5602
5978
|
this.log.warn("deliver failed", { agentId, detail: String(e) });
|
|
5603
5979
|
}
|
|
5604
|
-
},
|
|
5980
|
+
}, this.debounceMsFor(r));
|
|
5605
5981
|
r.deliverBuf = buf;
|
|
5606
5982
|
}
|
|
5607
5983
|
};
|
|
5608
5984
|
|
|
5609
5985
|
// src/daemon/workspace.ts
|
|
5610
|
-
import { readdir, readFile as
|
|
5611
|
-
import
|
|
5986
|
+
import { readdir, readFile as readFile3, stat } from "node:fs/promises";
|
|
5987
|
+
import path12 from "node:path";
|
|
5612
5988
|
import os3 from "node:os";
|
|
5613
5989
|
var DATA_DIR2 = agentsDir();
|
|
5614
5990
|
var MAX_FILE = 256 * 1024;
|
|
5615
5991
|
var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
5616
5992
|
function safe(agentId, rel) {
|
|
5617
|
-
const root =
|
|
5618
|
-
const target =
|
|
5619
|
-
if (target !== root && !target.startsWith(root +
|
|
5993
|
+
const root = path12.join(DATA_DIR2, agentId);
|
|
5994
|
+
const target = path12.resolve(root, rel || ".");
|
|
5995
|
+
if (target !== root && !target.startsWith(root + path12.sep)) return null;
|
|
5620
5996
|
return target;
|
|
5621
5997
|
}
|
|
5622
5998
|
async function walk(root, rel, acc, depth) {
|
|
5623
5999
|
if (depth > 6 || acc.length > 2e3) return;
|
|
5624
6000
|
let ds;
|
|
5625
6001
|
try {
|
|
5626
|
-
ds = await readdir(
|
|
6002
|
+
ds = await readdir(path12.join(root, rel), { withFileTypes: true });
|
|
5627
6003
|
} catch {
|
|
5628
6004
|
return;
|
|
5629
6005
|
}
|
|
@@ -5633,7 +6009,7 @@ async function walk(root, rel, acc, depth) {
|
|
|
5633
6009
|
let size = 0;
|
|
5634
6010
|
let modifiedAt = null;
|
|
5635
6011
|
try {
|
|
5636
|
-
const s = await stat(
|
|
6012
|
+
const s = await stat(path12.join(root, childRel));
|
|
5637
6013
|
size = d.isFile() ? s.size : 0;
|
|
5638
6014
|
modifiedAt = s.mtime.toISOString();
|
|
5639
6015
|
} catch {
|
|
@@ -5643,13 +6019,13 @@ async function walk(root, rel, acc, depth) {
|
|
|
5643
6019
|
}
|
|
5644
6020
|
}
|
|
5645
6021
|
async function listWorkspace(agentId, _subPath = "") {
|
|
5646
|
-
const root =
|
|
6022
|
+
const root = path12.join(DATA_DIR2, agentId);
|
|
5647
6023
|
try {
|
|
5648
6024
|
const files = [];
|
|
5649
6025
|
await walk(root, "", files, 0);
|
|
5650
|
-
return { files };
|
|
6026
|
+
return { files, root };
|
|
5651
6027
|
} catch (e) {
|
|
5652
|
-
return { error: String(e?.message ?? e) };
|
|
6028
|
+
return { error: String(e?.message ?? e), root };
|
|
5653
6029
|
}
|
|
5654
6030
|
}
|
|
5655
6031
|
function fmField(fm, key) {
|
|
@@ -5681,7 +6057,7 @@ async function readSkillsDir(dir, sourcePath) {
|
|
|
5681
6057
|
if (!e.isDirectory()) continue;
|
|
5682
6058
|
let name = e.name, description = "", userInvocable = false;
|
|
5683
6059
|
try {
|
|
5684
|
-
const txt = await
|
|
6060
|
+
const txt = await readFile3(path12.join(dir, e.name, "SKILL.md"), "utf8");
|
|
5685
6061
|
const fm = /^---\n([\s\S]*?)\n---/.exec(txt);
|
|
5686
6062
|
if (fm) {
|
|
5687
6063
|
name = fmField(fm[1], "name") || e.name;
|
|
@@ -5696,21 +6072,21 @@ async function readSkillsDir(dir, sourcePath) {
|
|
|
5696
6072
|
return out;
|
|
5697
6073
|
}
|
|
5698
6074
|
var HOME = os3.homedir();
|
|
5699
|
-
var UNIVERSAL_SKILLS = { dir:
|
|
6075
|
+
var UNIVERSAL_SKILLS = { dir: path12.join(HOME, ".agents", "skills"), label: "~/.agents/skills" };
|
|
5700
6076
|
var PROVIDER_HOME_SKILLS = {
|
|
5701
|
-
claude: { dir:
|
|
5702
|
-
codex: { dir:
|
|
5703
|
-
copilot: { dir:
|
|
5704
|
-
opencode: { dir:
|
|
5705
|
-
cursor: { dir:
|
|
5706
|
-
pi: { dir:
|
|
6077
|
+
claude: { dir: path12.join(HOME, ".claude", "skills"), label: "~/.claude/skills" },
|
|
6078
|
+
codex: { dir: path12.join(process.env.CODEX_HOME || path12.join(HOME, ".codex"), "skills"), label: "~/.codex/skills" },
|
|
6079
|
+
copilot: { dir: path12.join(HOME, ".copilot", "skills"), label: "~/.copilot/skills" },
|
|
6080
|
+
opencode: { dir: path12.join(HOME, ".config", "opencode", "skills"), label: "~/.config/opencode/skills" },
|
|
6081
|
+
cursor: { dir: path12.join(HOME, ".cursor", "skills"), label: "~/.cursor/skills" },
|
|
6082
|
+
pi: { dir: path12.join(HOME, ".pi", "agent", "skills"), label: "~/.pi/agent/skills" }
|
|
5707
6083
|
};
|
|
5708
6084
|
var PROVIDER_WS_DIR = { claude: ".claude", codex: ".codex", copilot: ".copilot", opencode: ".opencode", cursor: ".cursor", pi: ".pi" };
|
|
5709
6085
|
function skillRootsFor(runtime, agentId) {
|
|
5710
6086
|
const home = PROVIDER_HOME_SKILLS[runtime];
|
|
5711
6087
|
const global = home ? [home, UNIVERSAL_SKILLS] : [UNIVERSAL_SKILLS];
|
|
5712
6088
|
const wsName = PROVIDER_WS_DIR[runtime];
|
|
5713
|
-
const workspace = wsName ? { dir:
|
|
6089
|
+
const workspace = wsName ? { dir: path12.join(DATA_DIR2, agentId, wsName, "skills"), label: `<workspace>/${wsName}/skills` } : null;
|
|
5714
6090
|
return { global, workspace };
|
|
5715
6091
|
}
|
|
5716
6092
|
async function listSkills(agentId, runtime = "claude") {
|
|
@@ -5726,7 +6102,7 @@ async function readWorkspaceFile(agentId, rel) {
|
|
|
5726
6102
|
const s = await stat(file);
|
|
5727
6103
|
if (!s.isFile()) return { error: "not a file" };
|
|
5728
6104
|
if (s.size > MAX_FILE) return { error: `file too large (${s.size} bytes, max ${MAX_FILE})` };
|
|
5729
|
-
const buf = await
|
|
6105
|
+
const buf = await readFile3(file);
|
|
5730
6106
|
if (buf.includes(0)) return { error: "binary file" };
|
|
5731
6107
|
return { path: rel, content: buf.toString("utf8") };
|
|
5732
6108
|
} catch (e) {
|
|
@@ -5735,7 +6111,10 @@ async function readWorkspaceFile(agentId, rel) {
|
|
|
5735
6111
|
}
|
|
5736
6112
|
|
|
5737
6113
|
// src/daemon/listModels.ts
|
|
5738
|
-
import { spawn as
|
|
6114
|
+
import { spawn as spawn9 } from "node:child_process";
|
|
6115
|
+
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
|
|
6116
|
+
import { homedir as homedir2 } from "node:os";
|
|
6117
|
+
import path13 from "node:path";
|
|
5739
6118
|
var titleCase = (s) => s ? s[0].toUpperCase() + s.slice(1) : s;
|
|
5740
6119
|
function isModelId(s) {
|
|
5741
6120
|
return /^[A-Za-z][A-Za-z0-9\-_./]*$/.test(s);
|
|
@@ -5842,6 +6221,71 @@ function isPiNoise(line) {
|
|
|
5842
6221
|
const l = line.toLowerCase();
|
|
5843
6222
|
return l.includes("no models match pattern") || l.startsWith("warning:") || l.startsWith("error:") || l.startsWith("info:");
|
|
5844
6223
|
}
|
|
6224
|
+
function labelFromId(id) {
|
|
6225
|
+
return id.split(/[-_]/).filter(Boolean).map(titleCase).join(" ") || id;
|
|
6226
|
+
}
|
|
6227
|
+
function firstYamlString(text, keys) {
|
|
6228
|
+
for (const key of keys) {
|
|
6229
|
+
const re = new RegExp(`^${key}:\\s*["']?([^"'\\n#]+)`, "m");
|
|
6230
|
+
const m = re.exec(text);
|
|
6231
|
+
if (m?.[1]?.trim()) return m[1].trim();
|
|
6232
|
+
}
|
|
6233
|
+
return null;
|
|
6234
|
+
}
|
|
6235
|
+
function hermesProfileLabel(dir, id) {
|
|
6236
|
+
for (const filename of ["profile.yaml", "config.yaml"]) {
|
|
6237
|
+
const file = path13.join(dir, filename);
|
|
6238
|
+
if (!existsSync2(file)) continue;
|
|
6239
|
+
try {
|
|
6240
|
+
const text = readFileSync(file, "utf8").slice(0, 4096);
|
|
6241
|
+
const label = firstYamlString(text, ["display_name", "displayName", "name", "title"]);
|
|
6242
|
+
if (label) return label;
|
|
6243
|
+
} catch {
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6246
|
+
return labelFromId(id);
|
|
6247
|
+
}
|
|
6248
|
+
function isHermesProfileDir(dir) {
|
|
6249
|
+
return ["profile.yaml", "SOUL.md", "config.yaml"].some((name) => existsSync2(path13.join(dir, name)));
|
|
6250
|
+
}
|
|
6251
|
+
function discoverHermesProfilesFromRoots(roots) {
|
|
6252
|
+
const found = /* @__PURE__ */ new Map([
|
|
6253
|
+
["default", { id: "default", label: "Default profile", provider: "hermes", default: true }]
|
|
6254
|
+
]);
|
|
6255
|
+
for (const root of roots) {
|
|
6256
|
+
if (!root || !existsSync2(root)) continue;
|
|
6257
|
+
let entries;
|
|
6258
|
+
try {
|
|
6259
|
+
entries = readdirSync(root);
|
|
6260
|
+
} catch {
|
|
6261
|
+
continue;
|
|
6262
|
+
}
|
|
6263
|
+
for (const entry of entries) {
|
|
6264
|
+
const dir = path13.join(root, entry);
|
|
6265
|
+
try {
|
|
6266
|
+
if (!statSync(dir).isDirectory()) continue;
|
|
6267
|
+
} catch {
|
|
6268
|
+
continue;
|
|
6269
|
+
}
|
|
6270
|
+
if (!isHermesProfileDir(dir)) continue;
|
|
6271
|
+
if (!isModelId(entry)) continue;
|
|
6272
|
+
if (!found.has(entry)) found.set(entry, { id: entry, label: hermesProfileLabel(dir, entry), provider: "hermes" });
|
|
6273
|
+
}
|
|
6274
|
+
}
|
|
6275
|
+
return [...found.values()].sort((a, b) => {
|
|
6276
|
+
if (a.id === "default") return -1;
|
|
6277
|
+
if (b.id === "default") return 1;
|
|
6278
|
+
return a.id.localeCompare(b.id);
|
|
6279
|
+
});
|
|
6280
|
+
}
|
|
6281
|
+
function discoverHermesProfiles() {
|
|
6282
|
+
const home = homedir2();
|
|
6283
|
+
const roots = [
|
|
6284
|
+
process.env.HERMES_PROFILE_DIR,
|
|
6285
|
+
path13.join(home, ".hermes", "profiles")
|
|
6286
|
+
].filter((v) => !!v);
|
|
6287
|
+
return discoverHermesProfilesFromRoots(roots);
|
|
6288
|
+
}
|
|
5845
6289
|
var LIST_TIMEOUT_MS = 7e3;
|
|
5846
6290
|
var OUT_CAP = 256 * 1024;
|
|
5847
6291
|
function runList(bin, args2, timeoutMs = LIST_TIMEOUT_MS) {
|
|
@@ -5850,7 +6294,7 @@ function runList(bin, args2, timeoutMs = LIST_TIMEOUT_MS) {
|
|
|
5850
6294
|
delete env.NODE_OPTIONS;
|
|
5851
6295
|
let proc;
|
|
5852
6296
|
try {
|
|
5853
|
-
proc =
|
|
6297
|
+
proc = spawn9(bin, args2, { stdio: ["ignore", "pipe", "pipe"], env });
|
|
5854
6298
|
} catch (e) {
|
|
5855
6299
|
return resolve({ stdout: "", stderr: String(e?.message ?? e), code: 1 });
|
|
5856
6300
|
}
|
|
@@ -5913,6 +6357,10 @@ async function listModels(runtime) {
|
|
|
5913
6357
|
const models = parseCodexModels(r.stdout);
|
|
5914
6358
|
return models.length ? models : null;
|
|
5915
6359
|
}
|
|
6360
|
+
case "hermes": {
|
|
6361
|
+
const profiles = discoverHermesProfiles();
|
|
6362
|
+
return profiles.length ? profiles : null;
|
|
6363
|
+
}
|
|
5916
6364
|
default:
|
|
5917
6365
|
return null;
|
|
5918
6366
|
}
|
|
@@ -5928,8 +6376,10 @@ for (let i = 0; i < args.length; i++) {
|
|
|
5928
6376
|
if (args[i] === "--api-key" && args[i + 1]) apiKey = args[++i];
|
|
5929
6377
|
}
|
|
5930
6378
|
if (!serverUrl) serverUrl = `http://localhost:${process.env.PORT ?? 7777}`;
|
|
6379
|
+
if (!apiKey) apiKey = process.env.OPEN_TAG_DAEMON_API_KEY ?? "";
|
|
5931
6380
|
if (!apiKey) {
|
|
5932
6381
|
console.error("Usage: open-tag-daemon [--server-url <url>] --api-key <machineKey>");
|
|
6382
|
+
console.error(" or: OPEN_TAG_DAEMON_API_KEY=<machineKey> open-tag-daemon [--server-url <url>]");
|
|
5933
6383
|
process.exit(1);
|
|
5934
6384
|
}
|
|
5935
6385
|
var MID_FILE = machineIdFile();
|
|
@@ -5942,7 +6392,7 @@ var readMachineId = () => {
|
|
|
5942
6392
|
};
|
|
5943
6393
|
var saveMachineId = (id) => {
|
|
5944
6394
|
try {
|
|
5945
|
-
fs3.mkdirSync(
|
|
6395
|
+
fs3.mkdirSync(path14.dirname(MID_FILE), { recursive: true });
|
|
5946
6396
|
fs3.writeFileSync(MID_FILE, id);
|
|
5947
6397
|
} catch {
|
|
5948
6398
|
}
|
|
@@ -6003,7 +6453,7 @@ conn = new Connection(serverUrl, apiKey, (msg) => {
|
|
|
6003
6453
|
runningAgents: mgr.running(),
|
|
6004
6454
|
hostname: os4.hostname(),
|
|
6005
6455
|
os: `${os4.platform()} ${os4.arch()}`,
|
|
6006
|
-
daemonVersion: "0.
|
|
6456
|
+
daemonVersion: "0.8.1",
|
|
6007
6457
|
machineId: readMachineId()
|
|
6008
6458
|
// Stable identity: empty on first connection; server sends it back via ready:ack for persistence.
|
|
6009
6459
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fancyboi999/open-tag-daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "open-tag compute-plane daemon — connect any machine to an open-tag server so its agents run there. No repo clone needed.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|