@fancyboi999/open-tag-daemon 0.7.0 → 0.8.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/dist/agent-cli.mjs +25 -3
- package/dist/cli.mjs +507 -62
- 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
|
|
@@ -4139,6 +4140,29 @@ function summarize(tool, input) {
|
|
|
4139
4140
|
if (["Read", "Write", "Edit"].includes(tool)) return input.file_path ?? input.path ?? "";
|
|
4140
4141
|
return "";
|
|
4141
4142
|
}
|
|
4143
|
+
var CLAUDE_EFFORTS = /* @__PURE__ */ new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
4144
|
+
function buildClaudeArgs(p) {
|
|
4145
|
+
const args2 = [
|
|
4146
|
+
"-p",
|
|
4147
|
+
"--output-format",
|
|
4148
|
+
"stream-json",
|
|
4149
|
+
"--input-format",
|
|
4150
|
+
"stream-json",
|
|
4151
|
+
"--verbose",
|
|
4152
|
+
"--dangerously-skip-permissions",
|
|
4153
|
+
"--permission-mode",
|
|
4154
|
+
"bypassPermissions",
|
|
4155
|
+
"--include-partial-messages",
|
|
4156
|
+
"--disallowed-tools",
|
|
4157
|
+
"EnterPlanMode,ExitPlanMode,ScheduleWakeup,CronCreate,CronList,CronDelete,AskUserQuestion",
|
|
4158
|
+
...p.promptFileFlag
|
|
4159
|
+
];
|
|
4160
|
+
if (p.model) args2.push("--model", p.model);
|
|
4161
|
+
const effort = typeof p.reasoningEffort === "string" && CLAUDE_EFFORTS.has(p.reasoningEffort) ? p.reasoningEffort : null;
|
|
4162
|
+
if (effort) args2.push("--effort", effort);
|
|
4163
|
+
if (p.sessionId) args2.push("--resume", p.sessionId);
|
|
4164
|
+
return args2;
|
|
4165
|
+
}
|
|
4142
4166
|
var claudeRuntime = {
|
|
4143
4167
|
name: "claude",
|
|
4144
4168
|
start(opts, cb) {
|
|
@@ -4149,24 +4173,13 @@ var claudeRuntime = {
|
|
|
4149
4173
|
promptFlag = ["--append-system-prompt-file", pf];
|
|
4150
4174
|
} catch {
|
|
4151
4175
|
}
|
|
4152
|
-
const
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
"
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
"--dangerously-skip-permissions",
|
|
4160
|
-
"--permission-mode",
|
|
4161
|
-
"bypassPermissions",
|
|
4162
|
-
"--include-partial-messages",
|
|
4163
|
-
"--disallowed-tools",
|
|
4164
|
-
"EnterPlanMode,ExitPlanMode,ScheduleWakeup,CronCreate,CronList,CronDelete,AskUserQuestion",
|
|
4165
|
-
...promptFlag,
|
|
4166
|
-
"--model",
|
|
4167
|
-
opts.model ?? "sonnet"
|
|
4168
|
-
];
|
|
4169
|
-
if (opts.sessionId) args2.push("--resume", opts.sessionId);
|
|
4176
|
+
const rc = opts.runtimeConfig;
|
|
4177
|
+
const args2 = buildClaudeArgs({
|
|
4178
|
+
promptFileFlag: promptFlag,
|
|
4179
|
+
model: opts.model,
|
|
4180
|
+
reasoningEffort: rc && typeof rc.reasoningEffort === "string" ? rc.reasoningEffort : null,
|
|
4181
|
+
sessionId: opts.sessionId
|
|
4182
|
+
});
|
|
4170
4183
|
const proc = spawn("claude", args2, { cwd: opts.cwd, stdio: ["pipe", "pipe", "pipe"], env: opts.env });
|
|
4171
4184
|
let sessionId = opts.sessionId ?? null;
|
|
4172
4185
|
const writeUser = (text) => {
|
|
@@ -5364,6 +5377,302 @@ var cursorRuntime = {
|
|
|
5364
5377
|
}
|
|
5365
5378
|
};
|
|
5366
5379
|
|
|
5380
|
+
// src/daemon/hermesRuntime.ts
|
|
5381
|
+
import { spawn as spawn8 } from "node:child_process";
|
|
5382
|
+
import { existsSync } from "node:fs";
|
|
5383
|
+
import { readFile, unlink } from "node:fs/promises";
|
|
5384
|
+
import { homedir, tmpdir } from "node:os";
|
|
5385
|
+
import path10 from "node:path";
|
|
5386
|
+
var MAX8 = 4e3;
|
|
5387
|
+
var clip8 = (s) => String(s ?? "").slice(0, MAX8);
|
|
5388
|
+
var FINAL_RESPONSE_MAX = 2400;
|
|
5389
|
+
function hermesProfile(model, runtimeConfig) {
|
|
5390
|
+
const configured = runtimeConfig?.profile;
|
|
5391
|
+
if (typeof configured === "string" && configured.trim()) return configured.trim();
|
|
5392
|
+
if (model && model !== "default") return model;
|
|
5393
|
+
return "default";
|
|
5394
|
+
}
|
|
5395
|
+
function hermesProfileRoots(home = homedir(), env = process.env) {
|
|
5396
|
+
return [env.HERMES_PROFILE_DIR, path10.join(home, ".hermes", "profiles")].filter((v) => !!v);
|
|
5397
|
+
}
|
|
5398
|
+
function buildHermesPrompt(message, opts) {
|
|
5399
|
+
return [
|
|
5400
|
+
"[OpenTag runtime context]",
|
|
5401
|
+
`You are running as an OpenTag agent in this isolated workspace: ${opts.cwd}`,
|
|
5402
|
+
"Follow this OpenTag system prompt for collaboration, @mentions, and reporting:",
|
|
5403
|
+
opts.systemPrompt,
|
|
5404
|
+
"",
|
|
5405
|
+
"[OpenTag message]",
|
|
5406
|
+
message
|
|
5407
|
+
].join("\n");
|
|
5408
|
+
}
|
|
5409
|
+
function buildHermesArgs(prompt, sessionId) {
|
|
5410
|
+
const args2 = ["chat", "-q", prompt, "-Q", "--source", "open-tag"];
|
|
5411
|
+
if (sessionId) args2.push("--resume", sessionId);
|
|
5412
|
+
return args2;
|
|
5413
|
+
}
|
|
5414
|
+
function parseHermesSessionId(stderr) {
|
|
5415
|
+
const matches = [...stderr.matchAll(/^session_id:\s*(\S+)\s*$/gm)];
|
|
5416
|
+
return matches.length ? matches[matches.length - 1][1] : null;
|
|
5417
|
+
}
|
|
5418
|
+
function isMissingHermesSession(stderr) {
|
|
5419
|
+
return /Session not found:/i.test(stderr);
|
|
5420
|
+
}
|
|
5421
|
+
function parseHermesTurnEvents(jsonl) {
|
|
5422
|
+
const state = { sent: false, held: false, engaged: false, target: null };
|
|
5423
|
+
for (const line of jsonl.split("\n")) {
|
|
5424
|
+
if (!line.trim()) continue;
|
|
5425
|
+
let evt;
|
|
5426
|
+
try {
|
|
5427
|
+
evt = JSON.parse(line);
|
|
5428
|
+
} catch {
|
|
5429
|
+
continue;
|
|
5430
|
+
}
|
|
5431
|
+
if (evt.type === "send") state.sent = true;
|
|
5432
|
+
if (evt.type === "held") state.held = true;
|
|
5433
|
+
if ((evt.type === "check" || evt.type === "read") && typeof evt.target === "string" && evt.target.trim()) {
|
|
5434
|
+
state.engaged = true;
|
|
5435
|
+
state.target = evt.target.trim();
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
5438
|
+
return state;
|
|
5439
|
+
}
|
|
5440
|
+
function cleanHermesStdout(stdout) {
|
|
5441
|
+
let lines = stdout.trim().split(/\r?\n/).map((line) => line.trimEnd());
|
|
5442
|
+
while (lines.length && !lines[0].trim()) lines.shift();
|
|
5443
|
+
while (lines.length && /^(⚠|Warning:)/.test(lines[0].trim())) lines.shift();
|
|
5444
|
+
while (lines.length && !lines[0].trim()) lines.shift();
|
|
5445
|
+
const content = lines.join("\n").trim();
|
|
5446
|
+
if (!content) return { ok: false, reason: "empty-stdout" };
|
|
5447
|
+
if (/^(No new messages\.|Sent to |Freshness hold:)/.test(content)) return { ok: false, reason: "cli-output" };
|
|
5448
|
+
if (/^(Error:|Code:|Exception:|Traceback\b|Unhandled\b)/i.test(content)) return { ok: false, reason: "error-output" };
|
|
5449
|
+
if (/^(┊|diff --git\b|@@\s+-|\+\+\+ |--- )/m.test(content) || /\na\/.+\s+→\s+b\/.+/.test(content)) {
|
|
5450
|
+
return { ok: false, reason: "diff-output" };
|
|
5451
|
+
}
|
|
5452
|
+
return { ok: true, target: "", content: content.slice(0, FINAL_RESPONSE_MAX) };
|
|
5453
|
+
}
|
|
5454
|
+
function hermesBridgeDecision(stdout, state) {
|
|
5455
|
+
if (state.sent) return { ok: false, reason: "already-sent" };
|
|
5456
|
+
if (state.held) return { ok: false, reason: "already-held" };
|
|
5457
|
+
if (!state.engaged || !state.target) return { ok: false, reason: "no-open-tag-read" };
|
|
5458
|
+
if (!/^(#|dm:|thread:)/.test(state.target)) return { ok: false, reason: "invalid-target" };
|
|
5459
|
+
const cleaned = cleanHermesStdout(stdout);
|
|
5460
|
+
if (!cleaned.ok) return cleaned;
|
|
5461
|
+
return { ok: true, target: state.target, content: cleaned.content };
|
|
5462
|
+
}
|
|
5463
|
+
async function responseJson(res) {
|
|
5464
|
+
try {
|
|
5465
|
+
return await res.json();
|
|
5466
|
+
} catch {
|
|
5467
|
+
return {};
|
|
5468
|
+
}
|
|
5469
|
+
}
|
|
5470
|
+
async function postHermesBridgeMessage(fetchImpl, serverUrl2, headers, target, content) {
|
|
5471
|
+
const url = `${serverUrl2}/agent-api/message/send`;
|
|
5472
|
+
const first = await fetchImpl(url, {
|
|
5473
|
+
method: "POST",
|
|
5474
|
+
headers,
|
|
5475
|
+
body: JSON.stringify({ target, content })
|
|
5476
|
+
});
|
|
5477
|
+
const firstBody = await responseJson(first);
|
|
5478
|
+
if (!first.ok) return { ok: false, status: first.status };
|
|
5479
|
+
if (!firstBody?.held) return { ok: true };
|
|
5480
|
+
const held = { ok: false, held: true, sentDraft: false };
|
|
5481
|
+
if (typeof firstBody.text === "string") held.text = firstBody.text;
|
|
5482
|
+
return held;
|
|
5483
|
+
}
|
|
5484
|
+
function hermesProfileHome(profile, home = homedir(), roots = hermesProfileRoots(home)) {
|
|
5485
|
+
if (!profile || profile === "default") return null;
|
|
5486
|
+
for (const root of roots) {
|
|
5487
|
+
const dir = path10.join(root, profile);
|
|
5488
|
+
if (existsSync(dir)) return dir;
|
|
5489
|
+
}
|
|
5490
|
+
return null;
|
|
5491
|
+
}
|
|
5492
|
+
function hermesRuntimeEnv(baseEnv, cwd, requestedProfile, home = homedir()) {
|
|
5493
|
+
const env = { ...baseEnv, PWD: cwd };
|
|
5494
|
+
delete env.NODE_OPTIONS;
|
|
5495
|
+
delete env.HERMES_HOME;
|
|
5496
|
+
delete env.HERMES_PROFILE;
|
|
5497
|
+
const profileHome = hermesProfileHome(requestedProfile, home, hermesProfileRoots(home, env));
|
|
5498
|
+
const profile = requestedProfile === "default" || profileHome ? requestedProfile : "default";
|
|
5499
|
+
if (profileHome) {
|
|
5500
|
+
env.HERMES_HOME = profileHome;
|
|
5501
|
+
env.HERMES_PROFILE = profile;
|
|
5502
|
+
}
|
|
5503
|
+
return { env, profile };
|
|
5504
|
+
}
|
|
5505
|
+
var HermesRun = class {
|
|
5506
|
+
constructor(opts, cb) {
|
|
5507
|
+
this.opts = opts;
|
|
5508
|
+
this.cb = cb;
|
|
5509
|
+
const requestedProfile = hermesProfile(opts.model, opts.runtimeConfig);
|
|
5510
|
+
const resolved = hermesRuntimeEnv(opts.env, opts.cwd, requestedProfile);
|
|
5511
|
+
this.env = resolved.env;
|
|
5512
|
+
this.profile = resolved.profile;
|
|
5513
|
+
if (requestedProfile !== "default" && this.profile === "default") {
|
|
5514
|
+
cb.log.warn("hermes profile not found; using default profile", { profile: requestedProfile });
|
|
5515
|
+
}
|
|
5516
|
+
this.sessionId = opts.sessionId ?? null;
|
|
5517
|
+
if (this.sessionId) cb.onSession(this.sessionId);
|
|
5518
|
+
if (opts.initialPrompt.trim()) this.enqueue(opts.initialPrompt);
|
|
5519
|
+
}
|
|
5520
|
+
opts;
|
|
5521
|
+
cb;
|
|
5522
|
+
queue = [];
|
|
5523
|
+
turnBusy = false;
|
|
5524
|
+
stopped = false;
|
|
5525
|
+
proc = null;
|
|
5526
|
+
everSucceeded = false;
|
|
5527
|
+
env;
|
|
5528
|
+
profile;
|
|
5529
|
+
sessionId;
|
|
5530
|
+
enqueue(text) {
|
|
5531
|
+
if (this.stopped || !text.trim()) return;
|
|
5532
|
+
this.queue.push(text);
|
|
5533
|
+
this.pump();
|
|
5534
|
+
}
|
|
5535
|
+
pump() {
|
|
5536
|
+
if (this.stopped || this.turnBusy || this.queue.length === 0) return;
|
|
5537
|
+
this.runTurn(this.queue.shift());
|
|
5538
|
+
}
|
|
5539
|
+
runTurn(message) {
|
|
5540
|
+
this.turnBusy = true;
|
|
5541
|
+
this.cb.onActivity("working", `hermes/${this.profile}`);
|
|
5542
|
+
const prompt = buildHermesPrompt(message, this.opts);
|
|
5543
|
+
const args2 = buildHermesArgs(prompt, this.sessionId);
|
|
5544
|
+
const turnFile = path10.join(tmpdir(), `open-tag-hermes-turn-${Date.now()}-${Math.random().toString(36).slice(2)}.jsonl`);
|
|
5545
|
+
const proc = spawn8("hermes", args2, { cwd: this.opts.cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...this.env, OPEN_TAG_TURN_FILE: turnFile } });
|
|
5546
|
+
this.proc = proc;
|
|
5547
|
+
let stdout = "";
|
|
5548
|
+
const errTail = [];
|
|
5549
|
+
let errLen = 0;
|
|
5550
|
+
proc.stdout?.on("data", (c) => {
|
|
5551
|
+
if (this.stopped) return;
|
|
5552
|
+
if (stdout.length < MAX8) stdout += c.toString();
|
|
5553
|
+
});
|
|
5554
|
+
proc.stderr?.on("data", (c) => {
|
|
5555
|
+
const t = c.toString();
|
|
5556
|
+
errTail.push(t);
|
|
5557
|
+
errLen += t.length;
|
|
5558
|
+
while (errLen > 16384 && errTail.length > 1) errLen -= errTail.shift().length;
|
|
5559
|
+
});
|
|
5560
|
+
proc.on("error", (e) => {
|
|
5561
|
+
this.proc = null;
|
|
5562
|
+
this.turnBusy = false;
|
|
5563
|
+
if (this.stopped) return;
|
|
5564
|
+
this.cb.log.error("hermes spawn failed", { detail: String(e?.message ?? e) });
|
|
5565
|
+
this.cb.onActivity("offline", "hermes not found");
|
|
5566
|
+
if (!this.everSucceeded) this.cb.onExit(1);
|
|
5567
|
+
else this.pump();
|
|
5568
|
+
});
|
|
5569
|
+
proc.on("exit", async (code) => {
|
|
5570
|
+
this.proc = null;
|
|
5571
|
+
if (this.stopped) return;
|
|
5572
|
+
const out = stdout.trim();
|
|
5573
|
+
const tail = errTail.join("").trim();
|
|
5574
|
+
if (code === 0) {
|
|
5575
|
+
this.everSucceeded = true;
|
|
5576
|
+
const nextSessionId = parseHermesSessionId(tail);
|
|
5577
|
+
if (nextSessionId && nextSessionId !== this.sessionId) {
|
|
5578
|
+
this.sessionId = nextSessionId;
|
|
5579
|
+
this.cb.onSession(nextSessionId);
|
|
5580
|
+
}
|
|
5581
|
+
const bridged = await this.bridgeFinalResponse(turnFile, out);
|
|
5582
|
+
if (out) this.cb.onTrajectory([{ kind: "text", text: clip8(out) }]);
|
|
5583
|
+
if (bridged !== false) this.cb.onActivity("online", "");
|
|
5584
|
+
this.turnBusy = false;
|
|
5585
|
+
this.pump();
|
|
5586
|
+
return;
|
|
5587
|
+
}
|
|
5588
|
+
if (this.sessionId && isMissingHermesSession(tail)) {
|
|
5589
|
+
this.cb.log.warn("hermes resume session missing; retrying fresh", { sessionId: this.sessionId });
|
|
5590
|
+
this.sessionId = null;
|
|
5591
|
+
this.cb.onSession(null);
|
|
5592
|
+
this.queue.unshift(message);
|
|
5593
|
+
this.turnBusy = false;
|
|
5594
|
+
this.pump();
|
|
5595
|
+
return;
|
|
5596
|
+
}
|
|
5597
|
+
const last = tail.split("\n").filter(Boolean).pop() || `hermes exited ${code ?? "signal"}`;
|
|
5598
|
+
this.cb.onTrajectory([{ kind: "text", text: "[hermes error] " + clip8(tail || last).slice(0, 800) }]);
|
|
5599
|
+
this.cb.onActivity("error", last.slice(0, 200));
|
|
5600
|
+
this.turnBusy = false;
|
|
5601
|
+
if (!this.everSucceeded) {
|
|
5602
|
+
this.cb.onExit(code ?? 1);
|
|
5603
|
+
return;
|
|
5604
|
+
}
|
|
5605
|
+
this.pump();
|
|
5606
|
+
});
|
|
5607
|
+
}
|
|
5608
|
+
async bridgeFinalResponse(turnFile, stdout) {
|
|
5609
|
+
let state = { sent: false, held: false, engaged: false, target: null };
|
|
5610
|
+
try {
|
|
5611
|
+
state = parseHermesTurnEvents(await readFile(turnFile, "utf8"));
|
|
5612
|
+
} catch {
|
|
5613
|
+
} finally {
|
|
5614
|
+
try {
|
|
5615
|
+
await unlink(turnFile);
|
|
5616
|
+
} catch {
|
|
5617
|
+
}
|
|
5618
|
+
}
|
|
5619
|
+
const decision = hermesBridgeDecision(stdout, state);
|
|
5620
|
+
if (!decision.ok) {
|
|
5621
|
+
if (stdout.trim() && decision.reason !== "already-sent") {
|
|
5622
|
+
this.cb.log.warn("hermes final response not bridged", { reason: decision.reason });
|
|
5623
|
+
this.cb.onActivity("error", `hermes reply not sent (${decision.reason})`);
|
|
5624
|
+
return false;
|
|
5625
|
+
}
|
|
5626
|
+
return null;
|
|
5627
|
+
}
|
|
5628
|
+
try {
|
|
5629
|
+
const result = await postHermesBridgeMessage(fetch, this.env.OPEN_TAG_SERVER_URL ?? "", {
|
|
5630
|
+
authorization: `Bearer ${this.env.OPEN_TAG_AGENT_TOKEN ?? ""}`,
|
|
5631
|
+
"x-agent-id": this.env.OPEN_TAG_AGENT_ID ?? "",
|
|
5632
|
+
"content-type": "application/json"
|
|
5633
|
+
}, decision.target, decision.content);
|
|
5634
|
+
if (!result.ok) {
|
|
5635
|
+
if (result.held) {
|
|
5636
|
+
this.cb.log.warn("hermes final response freshness-held; draft saved for review", { target: decision.target });
|
|
5637
|
+
if (result.text) this.cb.onTrajectory([{ kind: "status", text: "[open-tag freshness hold]\n" + clip8(result.text) }]);
|
|
5638
|
+
this.cb.onActivity("error", "hermes reply held for freshness review");
|
|
5639
|
+
return false;
|
|
5640
|
+
}
|
|
5641
|
+
this.cb.log.warn("hermes final response bridge failed", { status: result.status, target: decision.target });
|
|
5642
|
+
this.cb.onActivity("error", "hermes bridge send failed");
|
|
5643
|
+
return false;
|
|
5644
|
+
} else {
|
|
5645
|
+
this.cb.log.info("hermes final response bridged", { target: decision.target, chars: decision.content.length, held: !!result.held });
|
|
5646
|
+
return true;
|
|
5647
|
+
}
|
|
5648
|
+
} catch (e) {
|
|
5649
|
+
this.cb.log.warn("hermes final response bridge failed", { detail: String(e?.message ?? e), target: decision.target });
|
|
5650
|
+
this.cb.onActivity("error", "hermes bridge send failed");
|
|
5651
|
+
return false;
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
5654
|
+
stop() {
|
|
5655
|
+
this.stopped = true;
|
|
5656
|
+
const p = this.proc;
|
|
5657
|
+
this.proc = null;
|
|
5658
|
+
if (p) {
|
|
5659
|
+
try {
|
|
5660
|
+
p.kill("SIGTERM");
|
|
5661
|
+
} catch {
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
};
|
|
5666
|
+
var hermesRuntime = {
|
|
5667
|
+
name: "hermes",
|
|
5668
|
+
experimental: true,
|
|
5669
|
+
oneShotWake: true,
|
|
5670
|
+
start(opts, cb) {
|
|
5671
|
+
const run = new HermesRun(opts, cb);
|
|
5672
|
+
return { deliver: (text) => run.enqueue(text), stop: () => run.stop() };
|
|
5673
|
+
}
|
|
5674
|
+
};
|
|
5675
|
+
|
|
5367
5676
|
// src/daemon/runtimes.ts
|
|
5368
5677
|
function has(tool) {
|
|
5369
5678
|
try {
|
|
@@ -5374,9 +5683,9 @@ function has(tool) {
|
|
|
5374
5683
|
}
|
|
5375
5684
|
}
|
|
5376
5685
|
function detectRuntimes() {
|
|
5377
|
-
return ["claude", "codex", "copilot", "kimi", "opencode", "pi", "cursor-agent"].filter(has).map((t) => t === "cursor-agent" ? "cursor" : t);
|
|
5686
|
+
return ["claude", "codex", "copilot", "kimi", "opencode", "pi", "cursor-agent", "hermes"].filter(has).map((t) => t === "cursor-agent" ? "cursor" : t);
|
|
5378
5687
|
}
|
|
5379
|
-
var REG = { claude: claudeRuntime, codex: codexRuntime, copilot: copilotRuntime, opencode: opencodeRuntime, kimi: kimiRuntime, pi: piRuntime, cursor: cursorRuntime };
|
|
5688
|
+
var REG = { claude: claudeRuntime, codex: codexRuntime, copilot: copilotRuntime, opencode: opencodeRuntime, kimi: kimiRuntime, pi: piRuntime, cursor: cursorRuntime, hermes: hermesRuntime };
|
|
5380
5689
|
function getRuntime(name) {
|
|
5381
5690
|
return REG[name] ?? null;
|
|
5382
5691
|
}
|
|
@@ -5385,14 +5694,28 @@ function getRuntime(name) {
|
|
|
5385
5694
|
var DATA_DIR = agentsDir();
|
|
5386
5695
|
var IDLE_MS = Number(process.env.OPEN_TAG_IDLE_MS ?? 10 * 60 * 1e3);
|
|
5387
5696
|
var DELIVER_DEBOUNCE_MS = Number(process.env.OPEN_TAG_DELIVER_DEBOUNCE_MS ?? 3e3);
|
|
5697
|
+
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);
|
|
5698
|
+
var PENDING_DELIVER_TTL_MS = Number(process.env.OPEN_TAG_PENDING_DELIVER_TTL_MS ?? 15e3);
|
|
5388
5699
|
var AgentManager = class {
|
|
5389
|
-
constructor(send) {
|
|
5700
|
+
constructor(send, opts = {}) {
|
|
5390
5701
|
this.send = send;
|
|
5391
|
-
this.binDir = ensureOpenTagBin();
|
|
5702
|
+
this.binDir = opts.binDir ?? ensureOpenTagBin();
|
|
5703
|
+
this.dataDir = opts.dataDir ?? DATA_DIR;
|
|
5704
|
+
this.deliverDebounceMs = opts.deliverDebounceMs ?? DELIVER_DEBOUNCE_MS;
|
|
5705
|
+
this.oneShotDeliverDebounceMs = opts.oneShotDeliverDebounceMs ?? ONE_SHOT_DELIVER_DEBOUNCE_MS;
|
|
5706
|
+
this.pendingDeliverTtlMs = opts.pendingDeliverTtlMs ?? PENDING_DELIVER_TTL_MS;
|
|
5707
|
+
this.runtimeResolver = opts.runtimeResolver ?? getRuntime;
|
|
5392
5708
|
}
|
|
5393
5709
|
send;
|
|
5394
5710
|
agents = /* @__PURE__ */ new Map();
|
|
5711
|
+
starting = /* @__PURE__ */ new Map();
|
|
5712
|
+
pendingDelivers = /* @__PURE__ */ new Map();
|
|
5395
5713
|
binDir;
|
|
5714
|
+
dataDir;
|
|
5715
|
+
deliverDebounceMs;
|
|
5716
|
+
oneShotDeliverDebounceMs;
|
|
5717
|
+
pendingDeliverTtlMs;
|
|
5718
|
+
runtimeResolver;
|
|
5396
5719
|
log = createLogger("daemon:agents");
|
|
5397
5720
|
running() {
|
|
5398
5721
|
return [...this.agents.keys()];
|
|
@@ -5402,6 +5725,7 @@ var AgentManager = class {
|
|
|
5402
5725
|
}
|
|
5403
5726
|
// 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.
|
|
5404
5727
|
teardown(agentId) {
|
|
5728
|
+
this.clearPendingDeliver(agentId);
|
|
5405
5729
|
const r = this.agents.get(agentId);
|
|
5406
5730
|
if (!r) return false;
|
|
5407
5731
|
if (r.idleTimer) clearTimeout(r.idleTimer);
|
|
@@ -5427,7 +5751,7 @@ var AgentManager = class {
|
|
|
5427
5751
|
async reset(agentId, wipeWorkspace = false, clearMemory = false) {
|
|
5428
5752
|
this.teardown(agentId);
|
|
5429
5753
|
this.send({ type: "agent:session", agentId, sessionId: null });
|
|
5430
|
-
const dir =
|
|
5754
|
+
const dir = path11.join(this.dataDir, agentId);
|
|
5431
5755
|
if (wipeWorkspace) {
|
|
5432
5756
|
try {
|
|
5433
5757
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -5437,7 +5761,7 @@ var AgentManager = class {
|
|
|
5437
5761
|
}
|
|
5438
5762
|
} else if (clearMemory) {
|
|
5439
5763
|
try {
|
|
5440
|
-
await writeFile(
|
|
5764
|
+
await writeFile(path11.join(dir, "MEMORY.md"), "# Memory\n\n(reset)\n");
|
|
5441
5765
|
this.log.info("memory cleared", { agentId });
|
|
5442
5766
|
} catch (e) {
|
|
5443
5767
|
this.log.warn("clearMemory failed", { agentId, detail: String(e) });
|
|
@@ -5451,10 +5775,10 @@ var AgentManager = class {
|
|
|
5451
5775
|
* title + `## Role`, preserving the agent's own sections. No-op if the workspace/file doesn't exist
|
|
5452
5776
|
* yet (a not-yet-started agent gets fresh values from the DB when start() seeds it). */
|
|
5453
5777
|
async syncProfile(agentId, displayName, description) {
|
|
5454
|
-
const mem =
|
|
5778
|
+
const mem = path11.join(this.dataDir, agentId, "MEMORY.md");
|
|
5455
5779
|
let content;
|
|
5456
5780
|
try {
|
|
5457
|
-
content = await
|
|
5781
|
+
content = await readFile2(mem, "utf8");
|
|
5458
5782
|
} catch {
|
|
5459
5783
|
this.log.debug("syncProfile: no MEMORY.md yet", { agentId });
|
|
5460
5784
|
return;
|
|
@@ -5486,16 +5810,24 @@ var AgentManager = class {
|
|
|
5486
5810
|
}
|
|
5487
5811
|
async start(agentId, config) {
|
|
5488
5812
|
if (this.agents.has(agentId)) return;
|
|
5489
|
-
const
|
|
5813
|
+
const existing = this.starting.get(agentId);
|
|
5814
|
+
if (existing) return existing;
|
|
5815
|
+
const pending = this.startNow(agentId, config).finally(() => this.starting.delete(agentId));
|
|
5816
|
+
this.starting.set(agentId, pending);
|
|
5817
|
+
return pending;
|
|
5818
|
+
}
|
|
5819
|
+
async startNow(agentId, config) {
|
|
5820
|
+
if (this.agents.has(agentId)) return;
|
|
5821
|
+
const runtime = this.runtimeResolver(config.runtime ?? "claude");
|
|
5490
5822
|
if (!runtime) {
|
|
5491
5823
|
this.log.error("no runtime", { runtime: config.runtime });
|
|
5492
5824
|
this.send({ type: "agent:activity", agentId, activity: "offline", detail: `no runtime: ${config.runtime}` });
|
|
5493
5825
|
return;
|
|
5494
5826
|
}
|
|
5495
5827
|
if (runtime.experimental) this.log.warn("experimental runtime", { runtime: runtime.name });
|
|
5496
|
-
const dir =
|
|
5497
|
-
await mkdir(
|
|
5498
|
-
const mem =
|
|
5828
|
+
const dir = path11.join(this.dataDir, agentId);
|
|
5829
|
+
await mkdir(path11.join(dir, "notes"), { recursive: true });
|
|
5830
|
+
const mem = path11.join(dir, "MEMORY.md");
|
|
5499
5831
|
try {
|
|
5500
5832
|
await access(mem);
|
|
5501
5833
|
} catch {
|
|
@@ -5542,6 +5874,8 @@ var AgentManager = class {
|
|
|
5542
5874
|
},
|
|
5543
5875
|
log: this.log
|
|
5544
5876
|
};
|
|
5877
|
+
const pendingDeliveryCount = this.pendingDelivers.get(agentId)?.items.length ?? 0;
|
|
5878
|
+
const useOneShotWakeNudge = !!runtime.oneShotWake && pendingDeliveryCount > 0;
|
|
5545
5879
|
this.agents.set(agentId, running);
|
|
5546
5880
|
running.session = runtime.start({
|
|
5547
5881
|
cwd: dir,
|
|
@@ -5550,18 +5884,55 @@ var AgentManager = class {
|
|
|
5550
5884
|
sessionId: config.sessionId,
|
|
5551
5885
|
systemPrompt,
|
|
5552
5886
|
env,
|
|
5553
|
-
initialPrompt: config.sessionId ? RESUME_NUDGE : STARTUP_NUDGE
|
|
5887
|
+
initialPrompt: useOneShotWakeNudge ? ONE_SHOT_WAKE_NUDGE : config.sessionId ? RESUME_NUDGE : STARTUP_NUDGE
|
|
5554
5888
|
}, cb);
|
|
5555
5889
|
this.send({ type: "agent:status", agentId, status: "active" });
|
|
5556
5890
|
this.send({ type: "agent:activity", agentId, activity: "working", detail: "starting" });
|
|
5557
5891
|
this.log.info("agent started", { agentId, runtime: runtime.name, model: config.model ?? "(default)", resume: !!config.sessionId, experimental: runtime.experimental ?? false });
|
|
5558
5892
|
this.resetIdle(agentId);
|
|
5893
|
+
if (useOneShotWakeNudge) {
|
|
5894
|
+
this.clearPendingDeliver(agentId);
|
|
5895
|
+
this.log.debug("pending deliver consumed by one-shot wake nudge", { agentId, runtime: runtime.name, count: pendingDeliveryCount });
|
|
5896
|
+
} else {
|
|
5897
|
+
this.flushPendingDeliver(agentId);
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5900
|
+
queuePendingDeliver(agentId, item) {
|
|
5901
|
+
let q = this.pendingDelivers.get(agentId);
|
|
5902
|
+
if (!q) {
|
|
5903
|
+
const timer = setTimeout(() => {
|
|
5904
|
+
this.pendingDelivers.delete(agentId);
|
|
5905
|
+
this.log.debug("pending deliver expired", { agentId });
|
|
5906
|
+
}, this.pendingDeliverTtlMs);
|
|
5907
|
+
q = { items: [], timer };
|
|
5908
|
+
this.pendingDelivers.set(agentId, q);
|
|
5909
|
+
}
|
|
5910
|
+
q.items.push(item);
|
|
5911
|
+
if (q.items.length > 10) q.items.shift();
|
|
5912
|
+
this.log.debug("deliver queued pending start", { agentId, count: q.items.length });
|
|
5913
|
+
}
|
|
5914
|
+
clearPendingDeliver(agentId) {
|
|
5915
|
+
const q = this.pendingDelivers.get(agentId);
|
|
5916
|
+
if (!q) return;
|
|
5917
|
+
clearTimeout(q.timer);
|
|
5918
|
+
this.pendingDelivers.delete(agentId);
|
|
5919
|
+
}
|
|
5920
|
+
flushPendingDeliver(agentId) {
|
|
5921
|
+
const q = this.pendingDelivers.get(agentId);
|
|
5922
|
+
if (!q) return;
|
|
5923
|
+
this.clearPendingDeliver(agentId);
|
|
5924
|
+
this.log.debug("pending deliver -> agent", { agentId, count: q.items.length });
|
|
5925
|
+
for (const item of q.items) this.deliver(agentId, item.from, item.target, item.mentioned, item.meta);
|
|
5926
|
+
}
|
|
5927
|
+
debounceMsFor(r) {
|
|
5928
|
+
const runtime = this.runtimeResolver(r.config.runtime ?? "claude");
|
|
5929
|
+
return runtime?.oneShotWake ? this.oneShotDeliverDebounceMs : this.deliverDebounceMs;
|
|
5559
5930
|
}
|
|
5560
|
-
/** server agent:deliver — wake a running agent with new messages;
|
|
5931
|
+
/** 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. */
|
|
5561
5932
|
deliver(agentId, from, target, mentioned = false, meta = {}) {
|
|
5562
5933
|
const r = this.agents.get(agentId);
|
|
5563
5934
|
if (!r) {
|
|
5564
|
-
this.
|
|
5935
|
+
this.queuePendingDeliver(agentId, { from, target, mentioned, meta });
|
|
5565
5936
|
return;
|
|
5566
5937
|
}
|
|
5567
5938
|
const tname = meta.targetName ?? target;
|
|
@@ -5589,29 +5960,29 @@ var AgentManager = class {
|
|
|
5589
5960
|
} catch (e) {
|
|
5590
5961
|
this.log.warn("deliver failed", { agentId, detail: String(e) });
|
|
5591
5962
|
}
|
|
5592
|
-
},
|
|
5963
|
+
}, this.debounceMsFor(r));
|
|
5593
5964
|
r.deliverBuf = buf;
|
|
5594
5965
|
}
|
|
5595
5966
|
};
|
|
5596
5967
|
|
|
5597
5968
|
// src/daemon/workspace.ts
|
|
5598
|
-
import { readdir, readFile as
|
|
5599
|
-
import
|
|
5969
|
+
import { readdir, readFile as readFile3, stat } from "node:fs/promises";
|
|
5970
|
+
import path12 from "node:path";
|
|
5600
5971
|
import os3 from "node:os";
|
|
5601
5972
|
var DATA_DIR2 = agentsDir();
|
|
5602
5973
|
var MAX_FILE = 256 * 1024;
|
|
5603
5974
|
var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
5604
5975
|
function safe(agentId, rel) {
|
|
5605
|
-
const root =
|
|
5606
|
-
const target =
|
|
5607
|
-
if (target !== root && !target.startsWith(root +
|
|
5976
|
+
const root = path12.join(DATA_DIR2, agentId);
|
|
5977
|
+
const target = path12.resolve(root, rel || ".");
|
|
5978
|
+
if (target !== root && !target.startsWith(root + path12.sep)) return null;
|
|
5608
5979
|
return target;
|
|
5609
5980
|
}
|
|
5610
5981
|
async function walk(root, rel, acc, depth) {
|
|
5611
5982
|
if (depth > 6 || acc.length > 2e3) return;
|
|
5612
5983
|
let ds;
|
|
5613
5984
|
try {
|
|
5614
|
-
ds = await readdir(
|
|
5985
|
+
ds = await readdir(path12.join(root, rel), { withFileTypes: true });
|
|
5615
5986
|
} catch {
|
|
5616
5987
|
return;
|
|
5617
5988
|
}
|
|
@@ -5621,7 +5992,7 @@ async function walk(root, rel, acc, depth) {
|
|
|
5621
5992
|
let size = 0;
|
|
5622
5993
|
let modifiedAt = null;
|
|
5623
5994
|
try {
|
|
5624
|
-
const s = await stat(
|
|
5995
|
+
const s = await stat(path12.join(root, childRel));
|
|
5625
5996
|
size = d.isFile() ? s.size : 0;
|
|
5626
5997
|
modifiedAt = s.mtime.toISOString();
|
|
5627
5998
|
} catch {
|
|
@@ -5631,13 +6002,13 @@ async function walk(root, rel, acc, depth) {
|
|
|
5631
6002
|
}
|
|
5632
6003
|
}
|
|
5633
6004
|
async function listWorkspace(agentId, _subPath = "") {
|
|
5634
|
-
const root =
|
|
6005
|
+
const root = path12.join(DATA_DIR2, agentId);
|
|
5635
6006
|
try {
|
|
5636
6007
|
const files = [];
|
|
5637
6008
|
await walk(root, "", files, 0);
|
|
5638
|
-
return { files };
|
|
6009
|
+
return { files, root };
|
|
5639
6010
|
} catch (e) {
|
|
5640
|
-
return { error: String(e?.message ?? e) };
|
|
6011
|
+
return { error: String(e?.message ?? e), root };
|
|
5641
6012
|
}
|
|
5642
6013
|
}
|
|
5643
6014
|
function fmField(fm, key) {
|
|
@@ -5669,7 +6040,7 @@ async function readSkillsDir(dir, sourcePath) {
|
|
|
5669
6040
|
if (!e.isDirectory()) continue;
|
|
5670
6041
|
let name = e.name, description = "", userInvocable = false;
|
|
5671
6042
|
try {
|
|
5672
|
-
const txt = await
|
|
6043
|
+
const txt = await readFile3(path12.join(dir, e.name, "SKILL.md"), "utf8");
|
|
5673
6044
|
const fm = /^---\n([\s\S]*?)\n---/.exec(txt);
|
|
5674
6045
|
if (fm) {
|
|
5675
6046
|
name = fmField(fm[1], "name") || e.name;
|
|
@@ -5684,21 +6055,21 @@ async function readSkillsDir(dir, sourcePath) {
|
|
|
5684
6055
|
return out;
|
|
5685
6056
|
}
|
|
5686
6057
|
var HOME = os3.homedir();
|
|
5687
|
-
var UNIVERSAL_SKILLS = { dir:
|
|
6058
|
+
var UNIVERSAL_SKILLS = { dir: path12.join(HOME, ".agents", "skills"), label: "~/.agents/skills" };
|
|
5688
6059
|
var PROVIDER_HOME_SKILLS = {
|
|
5689
|
-
claude: { dir:
|
|
5690
|
-
codex: { dir:
|
|
5691
|
-
copilot: { dir:
|
|
5692
|
-
opencode: { dir:
|
|
5693
|
-
cursor: { dir:
|
|
5694
|
-
pi: { dir:
|
|
6060
|
+
claude: { dir: path12.join(HOME, ".claude", "skills"), label: "~/.claude/skills" },
|
|
6061
|
+
codex: { dir: path12.join(process.env.CODEX_HOME || path12.join(HOME, ".codex"), "skills"), label: "~/.codex/skills" },
|
|
6062
|
+
copilot: { dir: path12.join(HOME, ".copilot", "skills"), label: "~/.copilot/skills" },
|
|
6063
|
+
opencode: { dir: path12.join(HOME, ".config", "opencode", "skills"), label: "~/.config/opencode/skills" },
|
|
6064
|
+
cursor: { dir: path12.join(HOME, ".cursor", "skills"), label: "~/.cursor/skills" },
|
|
6065
|
+
pi: { dir: path12.join(HOME, ".pi", "agent", "skills"), label: "~/.pi/agent/skills" }
|
|
5695
6066
|
};
|
|
5696
6067
|
var PROVIDER_WS_DIR = { claude: ".claude", codex: ".codex", copilot: ".copilot", opencode: ".opencode", cursor: ".cursor", pi: ".pi" };
|
|
5697
6068
|
function skillRootsFor(runtime, agentId) {
|
|
5698
6069
|
const home = PROVIDER_HOME_SKILLS[runtime];
|
|
5699
6070
|
const global = home ? [home, UNIVERSAL_SKILLS] : [UNIVERSAL_SKILLS];
|
|
5700
6071
|
const wsName = PROVIDER_WS_DIR[runtime];
|
|
5701
|
-
const workspace = wsName ? { dir:
|
|
6072
|
+
const workspace = wsName ? { dir: path12.join(DATA_DIR2, agentId, wsName, "skills"), label: `<workspace>/${wsName}/skills` } : null;
|
|
5702
6073
|
return { global, workspace };
|
|
5703
6074
|
}
|
|
5704
6075
|
async function listSkills(agentId, runtime = "claude") {
|
|
@@ -5714,7 +6085,7 @@ async function readWorkspaceFile(agentId, rel) {
|
|
|
5714
6085
|
const s = await stat(file);
|
|
5715
6086
|
if (!s.isFile()) return { error: "not a file" };
|
|
5716
6087
|
if (s.size > MAX_FILE) return { error: `file too large (${s.size} bytes, max ${MAX_FILE})` };
|
|
5717
|
-
const buf = await
|
|
6088
|
+
const buf = await readFile3(file);
|
|
5718
6089
|
if (buf.includes(0)) return { error: "binary file" };
|
|
5719
6090
|
return { path: rel, content: buf.toString("utf8") };
|
|
5720
6091
|
} catch (e) {
|
|
@@ -5723,7 +6094,10 @@ async function readWorkspaceFile(agentId, rel) {
|
|
|
5723
6094
|
}
|
|
5724
6095
|
|
|
5725
6096
|
// src/daemon/listModels.ts
|
|
5726
|
-
import { spawn as
|
|
6097
|
+
import { spawn as spawn9 } from "node:child_process";
|
|
6098
|
+
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
|
|
6099
|
+
import { homedir as homedir2 } from "node:os";
|
|
6100
|
+
import path13 from "node:path";
|
|
5727
6101
|
var titleCase = (s) => s ? s[0].toUpperCase() + s.slice(1) : s;
|
|
5728
6102
|
function isModelId(s) {
|
|
5729
6103
|
return /^[A-Za-z][A-Za-z0-9\-_./]*$/.test(s);
|
|
@@ -5830,6 +6204,71 @@ function isPiNoise(line) {
|
|
|
5830
6204
|
const l = line.toLowerCase();
|
|
5831
6205
|
return l.includes("no models match pattern") || l.startsWith("warning:") || l.startsWith("error:") || l.startsWith("info:");
|
|
5832
6206
|
}
|
|
6207
|
+
function labelFromId(id) {
|
|
6208
|
+
return id.split(/[-_]/).filter(Boolean).map(titleCase).join(" ") || id;
|
|
6209
|
+
}
|
|
6210
|
+
function firstYamlString(text, keys) {
|
|
6211
|
+
for (const key of keys) {
|
|
6212
|
+
const re = new RegExp(`^${key}:\\s*["']?([^"'\\n#]+)`, "m");
|
|
6213
|
+
const m = re.exec(text);
|
|
6214
|
+
if (m?.[1]?.trim()) return m[1].trim();
|
|
6215
|
+
}
|
|
6216
|
+
return null;
|
|
6217
|
+
}
|
|
6218
|
+
function hermesProfileLabel(dir, id) {
|
|
6219
|
+
for (const filename of ["profile.yaml", "config.yaml"]) {
|
|
6220
|
+
const file = path13.join(dir, filename);
|
|
6221
|
+
if (!existsSync2(file)) continue;
|
|
6222
|
+
try {
|
|
6223
|
+
const text = readFileSync(file, "utf8").slice(0, 4096);
|
|
6224
|
+
const label = firstYamlString(text, ["display_name", "displayName", "name", "title"]);
|
|
6225
|
+
if (label) return label;
|
|
6226
|
+
} catch {
|
|
6227
|
+
}
|
|
6228
|
+
}
|
|
6229
|
+
return labelFromId(id);
|
|
6230
|
+
}
|
|
6231
|
+
function isHermesProfileDir(dir) {
|
|
6232
|
+
return ["profile.yaml", "SOUL.md", "config.yaml"].some((name) => existsSync2(path13.join(dir, name)));
|
|
6233
|
+
}
|
|
6234
|
+
function discoverHermesProfilesFromRoots(roots) {
|
|
6235
|
+
const found = /* @__PURE__ */ new Map([
|
|
6236
|
+
["default", { id: "default", label: "Default profile", provider: "hermes", default: true }]
|
|
6237
|
+
]);
|
|
6238
|
+
for (const root of roots) {
|
|
6239
|
+
if (!root || !existsSync2(root)) continue;
|
|
6240
|
+
let entries;
|
|
6241
|
+
try {
|
|
6242
|
+
entries = readdirSync(root);
|
|
6243
|
+
} catch {
|
|
6244
|
+
continue;
|
|
6245
|
+
}
|
|
6246
|
+
for (const entry of entries) {
|
|
6247
|
+
const dir = path13.join(root, entry);
|
|
6248
|
+
try {
|
|
6249
|
+
if (!statSync(dir).isDirectory()) continue;
|
|
6250
|
+
} catch {
|
|
6251
|
+
continue;
|
|
6252
|
+
}
|
|
6253
|
+
if (!isHermesProfileDir(dir)) continue;
|
|
6254
|
+
if (!isModelId(entry)) continue;
|
|
6255
|
+
if (!found.has(entry)) found.set(entry, { id: entry, label: hermesProfileLabel(dir, entry), provider: "hermes" });
|
|
6256
|
+
}
|
|
6257
|
+
}
|
|
6258
|
+
return [...found.values()].sort((a, b) => {
|
|
6259
|
+
if (a.id === "default") return -1;
|
|
6260
|
+
if (b.id === "default") return 1;
|
|
6261
|
+
return a.id.localeCompare(b.id);
|
|
6262
|
+
});
|
|
6263
|
+
}
|
|
6264
|
+
function discoverHermesProfiles() {
|
|
6265
|
+
const home = homedir2();
|
|
6266
|
+
const roots = [
|
|
6267
|
+
process.env.HERMES_PROFILE_DIR,
|
|
6268
|
+
path13.join(home, ".hermes", "profiles")
|
|
6269
|
+
].filter((v) => !!v);
|
|
6270
|
+
return discoverHermesProfilesFromRoots(roots);
|
|
6271
|
+
}
|
|
5833
6272
|
var LIST_TIMEOUT_MS = 7e3;
|
|
5834
6273
|
var OUT_CAP = 256 * 1024;
|
|
5835
6274
|
function runList(bin, args2, timeoutMs = LIST_TIMEOUT_MS) {
|
|
@@ -5838,7 +6277,7 @@ function runList(bin, args2, timeoutMs = LIST_TIMEOUT_MS) {
|
|
|
5838
6277
|
delete env.NODE_OPTIONS;
|
|
5839
6278
|
let proc;
|
|
5840
6279
|
try {
|
|
5841
|
-
proc =
|
|
6280
|
+
proc = spawn9(bin, args2, { stdio: ["ignore", "pipe", "pipe"], env });
|
|
5842
6281
|
} catch (e) {
|
|
5843
6282
|
return resolve({ stdout: "", stderr: String(e?.message ?? e), code: 1 });
|
|
5844
6283
|
}
|
|
@@ -5901,6 +6340,10 @@ async function listModels(runtime) {
|
|
|
5901
6340
|
const models = parseCodexModels(r.stdout);
|
|
5902
6341
|
return models.length ? models : null;
|
|
5903
6342
|
}
|
|
6343
|
+
case "hermes": {
|
|
6344
|
+
const profiles = discoverHermesProfiles();
|
|
6345
|
+
return profiles.length ? profiles : null;
|
|
6346
|
+
}
|
|
5904
6347
|
default:
|
|
5905
6348
|
return null;
|
|
5906
6349
|
}
|
|
@@ -5916,8 +6359,10 @@ for (let i = 0; i < args.length; i++) {
|
|
|
5916
6359
|
if (args[i] === "--api-key" && args[i + 1]) apiKey = args[++i];
|
|
5917
6360
|
}
|
|
5918
6361
|
if (!serverUrl) serverUrl = `http://localhost:${process.env.PORT ?? 7777}`;
|
|
6362
|
+
if (!apiKey) apiKey = process.env.OPEN_TAG_DAEMON_API_KEY ?? "";
|
|
5919
6363
|
if (!apiKey) {
|
|
5920
6364
|
console.error("Usage: open-tag-daemon [--server-url <url>] --api-key <machineKey>");
|
|
6365
|
+
console.error(" or: OPEN_TAG_DAEMON_API_KEY=<machineKey> open-tag-daemon [--server-url <url>]");
|
|
5921
6366
|
process.exit(1);
|
|
5922
6367
|
}
|
|
5923
6368
|
var MID_FILE = machineIdFile();
|
|
@@ -5930,7 +6375,7 @@ var readMachineId = () => {
|
|
|
5930
6375
|
};
|
|
5931
6376
|
var saveMachineId = (id) => {
|
|
5932
6377
|
try {
|
|
5933
|
-
fs3.mkdirSync(
|
|
6378
|
+
fs3.mkdirSync(path14.dirname(MID_FILE), { recursive: true });
|
|
5934
6379
|
fs3.writeFileSync(MID_FILE, id);
|
|
5935
6380
|
} catch {
|
|
5936
6381
|
}
|
|
@@ -5991,7 +6436,7 @@ conn = new Connection(serverUrl, apiKey, (msg) => {
|
|
|
5991
6436
|
runningAgents: mgr.running(),
|
|
5992
6437
|
hostname: os4.hostname(),
|
|
5993
6438
|
os: `${os4.platform()} ${os4.arch()}`,
|
|
5994
|
-
daemonVersion: "0.
|
|
6439
|
+
daemonVersion: "0.8.0",
|
|
5995
6440
|
machineId: readMachineId()
|
|
5996
6441
|
// Stable identity: empty on first connection; server sends it back via ready:ack for persistence.
|
|
5997
6442
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fancyboi999/open-tag-daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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",
|