patchcord 0.6.42 → 0.6.44
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/.claude-plugin/marketplace.json +5 -0
- package/.claude-plugin/plugin.json +1 -1
- package/agent-plugin/.codex-plugin/plugin.json +22 -0
- package/agent-plugin/.mcp.json +9 -0
- package/agent-plugin/README.md +61 -0
- package/agent-plugin/mcp.json +9 -0
- package/agent-plugin/plugin.json +21 -0
- package/agent-plugin/skills/inbox/SKILL.md +202 -0
- package/agent-plugin/skills/subscribe/SKILL.md +154 -0
- package/agent-plugin/skills/wait/SKILL.md +31 -0
- package/bin/patchcord.mjs +199 -25
- package/harnesses.json +21 -4
- package/package.json +3 -2
- package/per-project-skills/codex/SKILL.md +1 -1
- package/per-project-skills/cursor/inbox/SKILL.md +1 -1
- package/per-project-skills/jcode/inbox/SKILL.md +34 -0
- package/per-project-skills/jcode/subscribe/SKILL.md +109 -0
- package/per-project-skills/jcode/wait/SKILL.md +26 -0
- package/scripts/build-agent-plugin.mjs +215 -0
- package/scripts/lib/hermes-home.mjs +120 -0
- package/scripts/lib/resolve-project-bearer.mjs +40 -0
- package/scripts/lib/stall-signal.mjs +104 -0
- package/scripts/subscribe.mjs +57 -1
- package/scripts/sync-plugin-version.mjs +23 -8
- package/skills/inbox/SKILL.md +1 -1
- package/skills/subscribe/SKILL.md +42 -0
package/bin/patchcord.mjs
CHANGED
|
@@ -13,6 +13,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
13
13
|
const pluginRoot = join(__dirname, "..");
|
|
14
14
|
import { resolveProjectBearer, harnessContext } from "../scripts/lib/resolve-project-bearer.mjs";
|
|
15
15
|
import { detectClaudeLocalMcpOverride } from "../scripts/lib/claude-local-mcp.mjs";
|
|
16
|
+
import { resolveHermesConfigPath, resolveHermesSkillsDest } from "../scripts/lib/hermes-home.mjs";
|
|
16
17
|
const cmd = process.argv[2];
|
|
17
18
|
|
|
18
19
|
/** Write a file then chmod 0600 (token-bearing configs must not be group/world readable). */
|
|
@@ -643,7 +644,7 @@ async function _resolveBearer(options = {}) {
|
|
|
643
644
|
() => readJsonAt(join(HOME, ".gemini", "settings.json"), ["mcpServers", "patchcord"], "gemini"),
|
|
644
645
|
() => readJsonAt(zedPath, ["context_servers", "patchcord"], "zed"),
|
|
645
646
|
() => readJsonAt(join(HOME, ".openclaw", "openclaw.json"), ["mcp", "servers", "patchcord"], "openclaw"),
|
|
646
|
-
() => readHermesShape(
|
|
647
|
+
() => readHermesShape(resolveHermesConfigPath()),
|
|
647
648
|
...clinePaths.map((p) => () => readJsonAt(p, ["mcpServers", "patchcord"], "cline")),
|
|
648
649
|
];
|
|
649
650
|
const globalCandidates = defaultGlobalCandidates;
|
|
@@ -683,6 +684,7 @@ if (cmd === "whoami") {
|
|
|
683
684
|
codex: "codex", kimi: "kimi", "kimi-code": "kimi",
|
|
684
685
|
opencode: "opencode", agy: "antigravity", antigravity: "antigravity",
|
|
685
686
|
cursor: "cursor", grok: "grok", hermes: "hermes",
|
|
687
|
+
jcode: "jcode",
|
|
686
688
|
};
|
|
687
689
|
const resolveOpts = {};
|
|
688
690
|
if (toolFlag) {
|
|
@@ -907,8 +909,28 @@ if (cmd === "upload") {
|
|
|
907
909
|
console.error(`file is empty: ${filePath}`);
|
|
908
910
|
process.exit(1);
|
|
909
911
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
+
// THIS IS NOT THE UPLOAD LIMIT AND MUST NOT BE DESCRIBED AS ONE.
|
|
913
|
+
//
|
|
914
|
+
// It used to say "max is 25MB", which was wrong in both directions. The
|
|
915
|
+
// server's limit is PATCHCORD_ATTACHMENT_MAX_BYTES, 10 MiB by default and
|
|
916
|
+
// raisable on a self-hosted install. So this client refused files a
|
|
917
|
+
// configured server would have accepted, and accepted files every default
|
|
918
|
+
// server rejects: a 15 MB file passed here, got base64'd to 20 MB, was
|
|
919
|
+
// uploaded in full, and came back 413. A guard set above the real limit does
|
|
920
|
+
// not guard; it only pays the upload first.
|
|
921
|
+
//
|
|
922
|
+
// The client must not hold a copy of a number the server owns. What is left
|
|
923
|
+
// is a memory guard for THIS process: the file, its base64 (4/3 the size),
|
|
924
|
+
// and the JSON body are all resident at once. The real limit comes back from
|
|
925
|
+
// the server, in max_bytes, and is printed below.
|
|
926
|
+
const MEMORY_GUARD_BYTES = 256 * 1024 * 1024;
|
|
927
|
+
if (stats.size > MEMORY_GUARD_BYTES) {
|
|
928
|
+
console.error(
|
|
929
|
+
`file is ${(stats.size / 1024 / 1024).toFixed(1)}MB. This client buffers the ` +
|
|
930
|
+
`file and its base64 copy in memory and refuses above ` +
|
|
931
|
+
`${MEMORY_GUARD_BYTES / 1024 / 1024}MB. This is a client memory guard, not ` +
|
|
932
|
+
`the server's size limit — the server's limit is lower.`
|
|
933
|
+
);
|
|
912
934
|
process.exit(1);
|
|
913
935
|
}
|
|
914
936
|
|
|
@@ -923,7 +945,18 @@ if (cmd === "upload") {
|
|
|
923
945
|
"POST", `${baseUrl}/api/agent/attachment/upload`, token, body
|
|
924
946
|
);
|
|
925
947
|
if (status !== "200") {
|
|
926
|
-
|
|
948
|
+
// Print max_bytes/allowed when the server sends them. Without this the user
|
|
949
|
+
// reads "attachment exceeds maximum size" and is not told the maximum, so
|
|
950
|
+
// their next move is to guess — or to ask an agent, which will guess.
|
|
951
|
+
let detail = "";
|
|
952
|
+
if (json && typeof json.max_bytes === "number") {
|
|
953
|
+
detail = ` (server limit ${(json.max_bytes / 1024 / 1024).toFixed(1)}MiB` +
|
|
954
|
+
(typeof json.actual_bytes === "number"
|
|
955
|
+
? `, this file ${(json.actual_bytes / 1024 / 1024).toFixed(1)}MiB)` : ")");
|
|
956
|
+
} else if (json && Array.isArray(json.allowed)) {
|
|
957
|
+
detail = ` (allowed: ${json.allowed.join(", ")})`;
|
|
958
|
+
}
|
|
959
|
+
console.error(`✗ HTTP ${status}: ${(json && json.error) || respBody}${detail}`);
|
|
927
960
|
process.exit(1);
|
|
928
961
|
}
|
|
929
962
|
console.log(json.path);
|
|
@@ -1500,15 +1533,95 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
|
|
|
1500
1533
|
} catch {}
|
|
1501
1534
|
return agWritten;
|
|
1502
1535
|
}
|
|
1536
|
+
if (tool === "jcode") {
|
|
1537
|
+
// jcode is the ONLY harness here that gets a STDIO entry, and the reason
|
|
1538
|
+
// is not preference: jcode has no HTTP transport. Its loader keeps an
|
|
1539
|
+
// entry only when `is_stdio()` holds and drops the rest at load time --
|
|
1540
|
+
// "MCP: Skipping non-stdio server '<name>' (http); HTTP/SSE transports
|
|
1541
|
+
// are not yet supported" (crates/jcode-base/src/mcp/protocol.rs). So the
|
|
1542
|
+
// `type: "http"` entry every other harness receives is READ, RECOGNISED
|
|
1543
|
+
// AND DISCARDED by jcode, with a log line and no error. A bridge process
|
|
1544
|
+
// is the only shape it can run today.
|
|
1545
|
+
//
|
|
1546
|
+
// NAMED patchcord-jcode, NOT patchcord, AND THAT IS LOAD-BEARING.
|
|
1547
|
+
// jcode merges .jcode/mcp.json, then .mcp.json, then .claude/mcp.json,
|
|
1548
|
+
// with later files overriding same-named servers. A guard currently saves
|
|
1549
|
+
// us -- a non-stdio entry never displaces a working stdio one (their
|
|
1550
|
+
// issue #653) -- but that guard holds only while jcode CANNOT run http.
|
|
1551
|
+
// The day it gains that transport, a shared name means .mcp.json wins,
|
|
1552
|
+
// and jcode silently starts authenticating as claude_code's agent: two
|
|
1553
|
+
// harnesses, one credential, no error anywhere. A distinct name cannot
|
|
1554
|
+
// collide in the first place. Codex already does this (patchcord-codex).
|
|
1555
|
+
//
|
|
1556
|
+
// The bridge command mirrors the OpenClaw fallback this file already
|
|
1557
|
+
// prints. NOT VERIFIED END TO END from here: no jcode on this machine,
|
|
1558
|
+
// and proving it needs a live token against the real endpoint.
|
|
1559
|
+
const jdir = join(dir, ".jcode"); mkdirSync(jdir, { recursive: true });
|
|
1560
|
+
const jcodeWritten = writeJson(join(jdir, "mcp.json"), (o) => {
|
|
1561
|
+
o.mcpServers = o.mcpServers || {};
|
|
1562
|
+
// No `type` key at all. jcode infers stdio from the presence of
|
|
1563
|
+
// `command`, and writing "type": "stdio" is one more string to get
|
|
1564
|
+
// wrong for a default that is already correct.
|
|
1565
|
+
o.mcpServers["patchcord-jcode"] = {
|
|
1566
|
+
command: "npx",
|
|
1567
|
+
args: [
|
|
1568
|
+
"mcp-remote",
|
|
1569
|
+
`${baseUrl}/mcp`,
|
|
1570
|
+
"--header",
|
|
1571
|
+
`Authorization: Bearer ${token}`,
|
|
1572
|
+
"--header",
|
|
1573
|
+
`X-Patchcord-Machine: ${hostname}`,
|
|
1574
|
+
],
|
|
1575
|
+
};
|
|
1576
|
+
});
|
|
1577
|
+
// THE SKILL IS A GUARD, NOT DOCUMENTATION, AND THIS IS THE HARNESS THAT
|
|
1578
|
+
// PROVED WHY. jcode inherits Claude Code's plugin skills today, which
|
|
1579
|
+
// say "spawn the listener under Monitor" — jcode has no Monitor, so
|
|
1580
|
+
// that instruction produces a listener that connects, receives, and
|
|
1581
|
+
// never wakes the agent. Observed in production: two messages sat
|
|
1582
|
+
// unread behind a healthy-looking listener. Without jcode's own
|
|
1583
|
+
// subscribe skill telling it to use `--stall-signal` + a bash
|
|
1584
|
+
// background task instead, every jcode agent silently inherits the
|
|
1585
|
+
// wrong instructions for its own harness.
|
|
1586
|
+
//
|
|
1587
|
+
// GLOBAL, NOT per-project (Pavel's ruling: "we dont need per project
|
|
1588
|
+
// skills, only per project MCP settings needed") — mirrors the hermes
|
|
1589
|
+
// branch above, not the antigravity one. `.jcode/mcp.json` stays
|
|
1590
|
+
// per-project (every project = one namespace = one agent identity);
|
|
1591
|
+
// skills are per machine, installed once, and MUST NOT contain
|
|
1592
|
+
// anything project-specific as a result — no namespace, no agent name,
|
|
1593
|
+
// no path baked in below.
|
|
1594
|
+
//
|
|
1595
|
+
// Destination is FLAT under ~/.jcode/skills/<name>/, not nested under
|
|
1596
|
+
// an "integrations" folder like hermes — jcode's own loader
|
|
1597
|
+
// (crates/jcode-app-core/src/tool/skill.rs) reads
|
|
1598
|
+
// ~/.jcode/skills/<skill-name>/SKILL.md directly, verified from source,
|
|
1599
|
+
// not the hermes shape.
|
|
1600
|
+
try {
|
|
1601
|
+
const jcodeSkillsSrc = join(pluginRoot, "per-project-skills", "jcode");
|
|
1602
|
+
if (existsSync(jcodeSkillsSrc)) {
|
|
1603
|
+
const jcodeSkillsDest = join(HOME, ".jcode", "skills");
|
|
1604
|
+
for (const name of ["inbox", "subscribe", "wait"]) {
|
|
1605
|
+
const from = join(jcodeSkillsSrc, name, "SKILL.md");
|
|
1606
|
+
if (!existsSync(from)) continue;
|
|
1607
|
+
const to = join(jcodeSkillsDest, name);
|
|
1608
|
+
mkdirSync(to, { recursive: true });
|
|
1609
|
+
cpSync(from, join(to, "SKILL.md"));
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
} catch {}
|
|
1613
|
+
return jcodeWritten;
|
|
1614
|
+
}
|
|
1503
1615
|
if (tool === "hermes") {
|
|
1504
|
-
// Hermes reads MCP servers ONLY from its GLOBAL
|
|
1505
|
-
//
|
|
1506
|
-
//
|
|
1507
|
-
//
|
|
1508
|
-
//
|
|
1509
|
-
// worker
|
|
1510
|
-
//
|
|
1511
|
-
|
|
1616
|
+
// Hermes reads MCP servers ONLY from its GLOBAL config.yaml (mcp_servers
|
|
1617
|
+
// key) — it ignores a project-local .mcp.json. So unlike the other tools
|
|
1618
|
+
// we cannot write into `dir`; we upsert the global config, mirroring the
|
|
1619
|
+
// `npx patchcord` installer's Hermes path. NOTE: "global" here means one
|
|
1620
|
+
// patchcord identity per Hermes HOME, so provisioning a second hermes
|
|
1621
|
+
// worker under the SAME home overwrites the first's token. Isolated by
|
|
1622
|
+
// HERMES_HOME (see resolveHermesConfigPath) or by -p profile — either
|
|
1623
|
+
// gives a separate home, and therefore a separate identity.
|
|
1624
|
+
const hermesPath = resolveHermesConfigPath();
|
|
1512
1625
|
mkdirSync(dirname(hermesPath), { recursive: true });
|
|
1513
1626
|
let existingYaml = "";
|
|
1514
1627
|
try { existingYaml = existsSync(hermesPath) ? readFileSync(hermesPath, "utf-8") : ""; } catch {}
|
|
@@ -1518,7 +1631,7 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
|
|
|
1518
1631
|
try {
|
|
1519
1632
|
const hermesSkillsSrc = join(pluginRoot, "per-project-skills", "hermes");
|
|
1520
1633
|
if (existsSync(hermesSkillsSrc)) {
|
|
1521
|
-
const hermesSkillsDest =
|
|
1634
|
+
const hermesSkillsDest = resolveHermesSkillsDest();
|
|
1522
1635
|
mkdirSync(hermesSkillsDest, { recursive: true });
|
|
1523
1636
|
cpSync(hermesSkillsSrc, hermesSkillsDest, { recursive: true });
|
|
1524
1637
|
}
|
|
@@ -1555,7 +1668,7 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
|
|
|
1555
1668
|
console.error(` .mcp.json → claude_code`);
|
|
1556
1669
|
console.error(` .cursor/mcp.json → cursor`);
|
|
1557
1670
|
console.error(` Defaulting would overwrite an agent you did not name, so there is no default.`);
|
|
1558
|
-
console.error(` One of: claude_code, codex, cursor, kimi, opencode, antigravity, grok, hermes`);
|
|
1671
|
+
console.error(` One of: claude_code, codex, cursor, kimi, opencode, antigravity, grok, hermes, jcode`);
|
|
1559
1672
|
console.error(` ${usage}`);
|
|
1560
1673
|
process.exit(1);
|
|
1561
1674
|
};
|
|
@@ -2915,11 +3028,14 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2915
3028
|
|
|
2916
3029
|
// Hermes — refresh patchcord skills to the current version on every run, so
|
|
2917
3030
|
// skill fixes land via `patchcord update` (the interactive choice 13 only
|
|
2918
|
-
// installs them once).
|
|
2919
|
-
//
|
|
3031
|
+
// installs them once). Hermes's own skills dir is not shared with any
|
|
3032
|
+
// other tool's config, so no leak risk; only refresh when the integrations
|
|
3033
|
+
// dir already exists (Hermes was set up before) — resolved through the
|
|
3034
|
+
// SAME home as config.yaml, so this refresh touches the seat's own skills
|
|
3035
|
+
// under HERMES_HOME, not the operator's global ones.
|
|
2920
3036
|
{
|
|
2921
3037
|
const hermesSkillsSrc = join(pluginRoot, "per-project-skills", "hermes");
|
|
2922
|
-
const hermesSkillsDest =
|
|
3038
|
+
const hermesSkillsDest = resolveHermesSkillsDest();
|
|
2923
3039
|
if (existsSync(hermesSkillsDest) && existsSync(hermesSkillsSrc)) {
|
|
2924
3040
|
let hermesChanged = false;
|
|
2925
3041
|
for (const name of readdirSync(hermesSkillsSrc)) {
|
|
@@ -3194,7 +3310,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
3194
3310
|
// falls through to no choice instead of silently configuring a DIFFERENT
|
|
3195
3311
|
// harness and writing someone's token to the wrong file.
|
|
3196
3312
|
"vscode": "6", "zed": "7", "opencode": "8", "openclaw": "9", "antigravity": "10",
|
|
3197
|
-
"cline": "11", "kimi": "12", "hermes": "13",
|
|
3313
|
+
"cline": "11", "kimi": "12", "hermes": "13", "jcode": "15",
|
|
3198
3314
|
};
|
|
3199
3315
|
|
|
3200
3316
|
|
|
@@ -3583,6 +3699,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
3583
3699
|
const isCline = choice === "11";
|
|
3584
3700
|
const isKimi = choice === "12";
|
|
3585
3701
|
const isHermes = choice === "13";
|
|
3702
|
+
const isJcode = choice === "15";
|
|
3586
3703
|
|
|
3587
3704
|
// MoonshotAI ships TWO CLIs that both use the `kimi` command:
|
|
3588
3705
|
// • kimi-cli (Python): supports `--mcp-config-file`, config in .kimi/mcp.json
|
|
@@ -3657,9 +3774,64 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
3657
3774
|
} catch (e) {
|
|
3658
3775
|
console.log(`\n ${yellow}⚠ Failed to write ${grokPath}: ${e.message}${r}`);
|
|
3659
3776
|
}
|
|
3777
|
+
} else if (isJcode) {
|
|
3778
|
+
// jcode: project-local .jcode/mcp.json, and a STDIO entry rather than the
|
|
3779
|
+
// http one every other harness gets. jcode has no HTTP transport -- it
|
|
3780
|
+
// recognises `type: "http"` and drops the server at load time with a log
|
|
3781
|
+
// line. See the writeWorkerConfig branch above for why the entry is named
|
|
3782
|
+
// `patchcord-jcode` and not `patchcord`; the short version is that jcode
|
|
3783
|
+
// merges .jcode/mcp.json, .mcp.json and .claude/mcp.json by server NAME,
|
|
3784
|
+
// so a shared name lets claude_code's credential silently become jcode's
|
|
3785
|
+
// the day jcode gains http support.
|
|
3786
|
+
const jcodePath = join(cwd, ".jcode", "mcp.json");
|
|
3787
|
+
try {
|
|
3788
|
+
mkdirSync(dirname(jcodePath), { recursive: true });
|
|
3789
|
+
let jobj = {};
|
|
3790
|
+
try { jobj = JSON.parse(readFileSync(jcodePath, "utf-8")); } catch {}
|
|
3791
|
+
jobj.mcpServers = jobj.mcpServers || {};
|
|
3792
|
+
jobj.mcpServers["patchcord-jcode"] = {
|
|
3793
|
+
command: "npx",
|
|
3794
|
+
args: [
|
|
3795
|
+
"mcp-remote",
|
|
3796
|
+
`${serverUrl}/mcp`,
|
|
3797
|
+
"--header",
|
|
3798
|
+
`Authorization: Bearer ${token}`,
|
|
3799
|
+
],
|
|
3800
|
+
};
|
|
3801
|
+
writeSecureFile(jcodePath, JSON.stringify(jobj, null, 2) + "\n");
|
|
3802
|
+
console.log(`\n ${green}✓${r} jcode configured: ${dim}${jcodePath}${r}`);
|
|
3803
|
+
console.log(` ${dim}Bridged over stdio — jcode does not speak HTTP MCP yet.${r}`);
|
|
3804
|
+
} catch (e) {
|
|
3805
|
+
console.log(`\n ${yellow}⚠ Failed to write ${jcodePath}: ${e.message}${r}`);
|
|
3806
|
+
}
|
|
3807
|
+
// Install jcode skills to ~/.jcode/skills/<name>/ — GLOBAL, once per
|
|
3808
|
+
// machine, not per project (Pavel's ruling: "we dont need per project
|
|
3809
|
+
// skills, only per project MCP settings needed"). See the
|
|
3810
|
+
// writeWorkerConfig branch above for the full guard-not-documentation
|
|
3811
|
+
// reasoning and why the destination is flat, not nested under
|
|
3812
|
+
// "integrations" the way hermes's is.
|
|
3813
|
+
try {
|
|
3814
|
+
const jcodeSkillsSrc = join(pluginRoot, "per-project-skills", "jcode");
|
|
3815
|
+
if (existsSync(jcodeSkillsSrc)) {
|
|
3816
|
+
const jcodeSkillsDest = join(HOME, ".jcode", "skills");
|
|
3817
|
+
for (const name of ["inbox", "subscribe", "wait"]) {
|
|
3818
|
+
const from = join(jcodeSkillsSrc, name, "SKILL.md");
|
|
3819
|
+
if (!existsSync(from)) continue;
|
|
3820
|
+
const to = join(jcodeSkillsDest, name);
|
|
3821
|
+
mkdirSync(to, { recursive: true });
|
|
3822
|
+
cpSync(from, join(to, "SKILL.md"));
|
|
3823
|
+
}
|
|
3824
|
+
console.log(` ${green}✓${r} jcode skills installed: ${dim}${jcodeSkillsDest}${r}`);
|
|
3825
|
+
}
|
|
3826
|
+
} catch {}
|
|
3660
3827
|
} else if (isHermes) {
|
|
3661
|
-
// Hermes: global only (
|
|
3662
|
-
|
|
3828
|
+
// Hermes: global-per-home only (config.yaml, YAML, mcp_servers key). Same
|
|
3829
|
+
// HERMES_HOME / `hermes config path` / ~/.hermes resolution as the
|
|
3830
|
+
// writeWorkerConfig branch above — see resolveHermesConfigPath. Both
|
|
3831
|
+
// write sites and the bearer-resolution read site must agree, or a seat
|
|
3832
|
+
// provisioned here writes to one file while `patchcord whoami` verifies
|
|
3833
|
+
// against another.
|
|
3834
|
+
const hermesPath = resolveHermesConfigPath();
|
|
3663
3835
|
mkdirSync(dirname(hermesPath), { recursive: true });
|
|
3664
3836
|
let existingYaml = "";
|
|
3665
3837
|
try { existingYaml = existsSync(hermesPath) ? readFileSync(hermesPath, "utf-8") : ""; } catch {}
|
|
@@ -3670,11 +3842,12 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
3670
3842
|
} catch (e) {
|
|
3671
3843
|
console.log(`\n ${yellow}⚠ Failed to write ${hermesPath}: ${e.message}${r}`);
|
|
3672
3844
|
}
|
|
3673
|
-
// Install Hermes skills
|
|
3845
|
+
// Install Hermes skills under the resolved home's skills/integrations/
|
|
3846
|
+
// (same home as config.yaml — see resolveHermesSkillsDest).
|
|
3674
3847
|
try {
|
|
3675
3848
|
const hermesSkillsSrc = join(pluginRoot, "per-project-skills", "hermes");
|
|
3676
3849
|
if (existsSync(hermesSkillsSrc)) {
|
|
3677
|
-
const hermesSkillsDest =
|
|
3850
|
+
const hermesSkillsDest = resolveHermesSkillsDest();
|
|
3678
3851
|
mkdirSync(hermesSkillsDest, { recursive: true });
|
|
3679
3852
|
cpSync(hermesSkillsSrc, hermesSkillsDest, { recursive: true });
|
|
3680
3853
|
console.log(` ${green}✓${r} Hermes skills installed: ${dim}${hermesSkillsDest}${r}`);
|
|
@@ -4324,10 +4497,11 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
4324
4497
|
// Hermes is global config (~/.hermes/config.yaml) — no per-project file to ignore.
|
|
4325
4498
|
if (!isWindsurf && !isGemini && !isZed && !isOpenClaw && !isCline && !isHermes) {
|
|
4326
4499
|
const gitignorePath = join(cwd, ".gitignore");
|
|
4327
|
-
const configFile = isKimiCode ? ".kimi-code/mcp.json" : isKimi ? ".kimi/mcp.json" : isCodex ? ".codex/config.toml" : isCursor ? ".cursor/mcp.json" : isGrok ? ".grok/config.toml" : isVSCode ? ".vscode/mcp.json" : isOpenCode ? "opencode.json" : isAntigravity ? ".agents/mcp_config.json" : ".mcp.json";
|
|
4500
|
+
const configFile = isJcode ? ".jcode/mcp.json" : isKimiCode ? ".kimi-code/mcp.json" : isKimi ? ".kimi/mcp.json" : isCodex ? ".codex/config.toml" : isCursor ? ".cursor/mcp.json" : isGrok ? ".grok/config.toml" : isVSCode ? ".vscode/mcp.json" : isOpenCode ? "opencode.json" : isAntigravity ? ".agents/mcp_config.json" : ".mcp.json";
|
|
4328
4501
|
// Forms that already cover this config (its file or its dir)
|
|
4329
4502
|
const patterns = [configFile];
|
|
4330
|
-
if (
|
|
4503
|
+
if (isJcode) patterns.push(".jcode/");
|
|
4504
|
+
else if (isKimiCode) patterns.push(".kimi-code/");
|
|
4331
4505
|
else if (isKimi) patterns.push(".kimi/");
|
|
4332
4506
|
else if (isCodex) patterns.push(".codex/");
|
|
4333
4507
|
else if (isCursor) patterns.push(".cursor/");
|
|
@@ -4352,7 +4526,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
4352
4526
|
}
|
|
4353
4527
|
}
|
|
4354
4528
|
|
|
4355
|
-
const toolName = isHermes ? "Hermes" : isKimiCode ? "Kimi Code" : isKimi ? "Kimi Code" : isAntigravity ? "Antigravity CLI" : isCline ? "Cline" : isOpenClaw ? "OpenClaw" : isOpenCode ? "OpenCode" : isZed ? "Zed" : isVSCode ? "VS Code" : isGemini ? "Gemini CLI" : isWindsurf ? "Windsurf" : isGrok ? "Grok CLI" : isCursor ? "Cursor" : isCodex ? "Codex" : "Claude Code";
|
|
4529
|
+
const toolName = isJcode ? "jcode" : isHermes ? "Hermes" : isKimiCode ? "Kimi Code" : isKimi ? "Kimi Code" : isAntigravity ? "Antigravity CLI" : isCline ? "Cline" : isOpenClaw ? "OpenClaw" : isOpenCode ? "OpenCode" : isZed ? "Zed" : isVSCode ? "VS Code" : isGemini ? "Gemini CLI" : isWindsurf ? "Windsurf" : isGrok ? "Grok CLI" : isCursor ? "Cursor" : isCodex ? "Codex" : "Claude Code";
|
|
4356
4530
|
|
|
4357
4531
|
if (!isWindsurf && !isGemini && !isZed && !isOpenClaw && !isCline && !isKimi && !isHermes) {
|
|
4358
4532
|
console.log(`\n ${dim}To connect a second agent:${r}`);
|
package/harnesses.json
CHANGED
|
@@ -129,10 +129,10 @@
|
|
|
129
129
|
"aliases": [],
|
|
130
130
|
"cli": "hermes",
|
|
131
131
|
"kind": "terminal",
|
|
132
|
-
"installer_scope": "
|
|
133
|
-
"installer_config": "~/.hermes/config.yaml",
|
|
134
|
-
"harness_scope": "per-profile",
|
|
135
|
-
"installer_defect": "
|
|
132
|
+
"installer_scope": "env-directed",
|
|
133
|
+
"installer_config": "$HERMES_HOME/config.yaml if set, else the output of `hermes config path` if the binary resolves, else ~/.hermes/config.yaml",
|
|
134
|
+
"harness_scope": "per-profile-or-per-home",
|
|
135
|
+
"installer_defect": "WAS: the path was hardcoded to ~/.hermes/config.yaml. The installer never called `hermes config path` and did not know profiles exist, so installing while a non-default profile was active wrote the DEFAULT profile's config — the wrong file, silently. FIXED (scripts/lib/hermes-home.mjs, resolveHermesHome/resolveHermesConfigPath/resolveHermesSkillsDest, all three deriving from the same resolved home): HERMES_HOME from the environment, else a shelled `hermes config path` (which itself honours HERMES_HOME — the child inherits the environment), else the unchanged ~/.hermes fallback. Applies to config.yaml AND to where Hermes skills install (~/.hermes/skills/integrations was hardcoded in three more places, unaware of HERMES_HOME — a seat isolated by home for identity but not for skills is not isolated: skills are a guard, not documentation). mux measured two independent isolation mechanisms this way — HERMES_HOME and -p/--profile, both real, both giving a separate mcp_servers AND a separate .env — which is why harness_scope reads per-profile-or-per-home rather than per-profile alone. installer_scope changed from 'global' to 'env-directed' for the same reason: our installer's write target is no longer a fixed machine-wide path, it is wherever HERMES_HOME points when mux sets it, falling back to global only when nothing directs it. PROFILE SUPPORT DELIBERATELY NOT BUILT: -p/--profile is a second, separate isolation axis this installer does not resolve, understand, or accept a flag for. Whether `hermes config path`'s output already reflects an ambient active profile (e.g. via an env var hermes itself reads) is unknown without reading Hermes's own source or an install to test against — neither available here. If a user runs Hermes under a non-default profile without ALSO setting HERMES_HOME to match, this installer's write may land in a config that profile-launched Hermes never reads — the same silent-wrong-file failure this whole fix exists to close, just on the axis we chose not to resolve. Not yet released or observed end to end; version not bumped.",
|
|
136
136
|
"listener": {
|
|
137
137
|
"wake": "realtime",
|
|
138
138
|
"mechanism": "webhook-bridge",
|
|
@@ -217,6 +217,23 @@
|
|
|
217
217
|
"installer_config": "cline_mcp_settings.json (VS Code globalStorage)",
|
|
218
218
|
"harness_scope": "unknown",
|
|
219
219
|
"listener": { "wake": "none", "mechanism": null, "self_arm": false, "survives_wake": null, "evidence": "declared" }
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"id": "jcode",
|
|
223
|
+
"aliases": [],
|
|
224
|
+
"cli": "jcode",
|
|
225
|
+
"kind": "terminal",
|
|
226
|
+
"installer_scope": "project",
|
|
227
|
+
"installer_config": ".jcode/mcp.json",
|
|
228
|
+
"harness_scope": "project",
|
|
229
|
+
"listener": {
|
|
230
|
+
"wake": "stall",
|
|
231
|
+
"mechanism": "subscribe-skill-stall-signal",
|
|
232
|
+
"self_arm": true,
|
|
233
|
+
"survives_wake": null,
|
|
234
|
+
"evidence": "declared",
|
|
235
|
+
"note": "STDIO ONLY FOR MCP; SKILLS ARE GLOBAL, NOT PROJECT. jcode reads .jcode/mcp.json, .mcp.json and .claude/mcp.json for MCP config (per-project, unchanged by this note), so it ALREADY finds the Claude Code entry we write - and drops it, because jcode supports stdio servers only and skips type http/sse at load time with a log line (crates/jcode-base/src/mcp/protocol.rs, retain on is_stdio). Server named patchcord-jcode, NOT patchcord, for the same reason codex uses patchcord-codex: a shared name would let a later file's entry silently override by NAME the day jcode gains http transport. jcode's own skills, separately, install ONCE GLOBALLY to ~/.jcode/skills/<name>/SKILL.md (verified from crates/jcode-app-core/src/tool/skill.rs's own loader paths) - not per project. WAKE IS \"stall\", NOT \"realtime\" OR \"none\": jcode has no Monitor, so a background task wakes it only when stdout goes quiet for stall_wake_seconds - and subscribe.mjs's only stdout writer is a real-message line, so by default the pipe is ALREADY silent while idle, making an unmodified stall wake fire on a timer regardless of whether anything arrived. `patchcord subscribe --stall-signal` (scripts/lib/stall-signal.mjs) inverts that for jcode specifically: writes a HEARTBEAT keepalive while idle so the pipe is never silently quiet by accident, and suppresses it on purpose after a real message so the stall condition elapses close to when the message actually arrived. This only works if stall_wake_seconds equals --stall-signal's own quiet window (15s by default) - a mismatch silently breaks the wake in one direction or the other, which is why the subscribe skill states the number explicitly rather than leaving it to be inferred. NOT VERIFIED END TO END: no jcode install on this machine to watch it actually fire."
|
|
236
|
+
}
|
|
220
237
|
}
|
|
221
238
|
],
|
|
222
239
|
"retired": [
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "patchcord",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.44",
|
|
4
4
|
"description": "Cross-machine agent messaging for Claude Code and Codex",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"version": "node scripts/sync-plugin-version.mjs && git add .claude-plugin/plugin.json"
|
|
6
|
+
"version": "node scripts/sync-plugin-version.mjs && git add .claude-plugin/plugin.json agent-plugin/plugin.json"
|
|
7
7
|
},
|
|
8
8
|
"author": "ppravdin",
|
|
9
9
|
"license": "MIT",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"commands/",
|
|
34
34
|
"README.md",
|
|
35
35
|
"plugins/",
|
|
36
|
+
"agent-plugin/",
|
|
36
37
|
"harnesses.json"
|
|
37
38
|
]
|
|
38
39
|
}
|
|
@@ -99,7 +99,7 @@ To message a user outside your namespace, use `@username` as the to_agent. Examp
|
|
|
99
99
|
```
|
|
100
100
|
patchcord upload /path/to/report.md --mime text/markdown
|
|
101
101
|
```
|
|
102
|
-
Prints the storage path. Pass it to `send_message`. No curl, no base64 in chat.
|
|
102
|
+
Prints the storage path. Pass it to `send_message`. No curl, no base64 in chat. The size limit belongs to the server: 10 MiB by default, raisable on a self-hosted server. Too large prints the server's own limit.
|
|
103
103
|
|
|
104
104
|
**Public URLs → `attachment(relay=true, ...)`:**
|
|
105
105
|
```
|
|
@@ -105,7 +105,7 @@ To message a user outside your namespace, use `@username` as the to_agent. Examp
|
|
|
105
105
|
```
|
|
106
106
|
patchcord upload /path/to/report.md --mime text/markdown
|
|
107
107
|
```
|
|
108
|
-
Prints the storage path. Pass that path to `send_message`. No curl, no base64 in chat, no presigned URLs.
|
|
108
|
+
Prints the storage path. Pass that path to `send_message`. No curl, no base64 in chat, no presigned URLs. The size limit is the server's, not a number to remember: it is 10 MiB by default and a self-hosted server can raise it. If a file is too large the command prints the server's own limit.
|
|
109
109
|
|
|
110
110
|
**Public URLs → `attachment(relay=true, ...)`:**
|
|
111
111
|
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: patchcord-inbox
|
|
3
|
+
description: Read Patchcord inbox and reply to messages
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Applies only when Patchcord MCP tools are loaded
|
|
7
|
+
|
|
8
|
+
If the `inbox` MCP tool is not available in this session, this skill does not
|
|
9
|
+
apply. Do nothing: do not run the Patchcord CLI, read a token from a config
|
|
10
|
+
file, or call the HTTP API. A missing project MCP config is a normal state.
|
|
11
|
+
|
|
12
|
+
Call the `inbox` MCP tool now. In its response, the first header line is YOUR
|
|
13
|
+
own identity (the recipient); the real sender of each message is on a
|
|
14
|
+
`From X` line — never confuse the two.
|
|
15
|
+
|
|
16
|
+
For each pending message, classify it and act:
|
|
17
|
+
|
|
18
|
+
- **ACK** — short signals like thanks, noted, works, great, ok, 👍 with no
|
|
19
|
+
task → close it silently: `reply(message_id, resolve=true)` with NO
|
|
20
|
+
content. Never send a text reply to an ack (it creates infinite ack
|
|
21
|
+
chains).
|
|
22
|
+
- **BLOCKED** — you cannot do the work right now (busy, missing credentials,
|
|
23
|
+
ambiguous target) → `reply(message_id, "<reason>", defer=true)` so it stays
|
|
24
|
+
in your inbox as a reminder. Never silently skip a message.
|
|
25
|
+
- **ACTIONABLE** — do the work the message asks for FIRST (edit the file,
|
|
26
|
+
run the command, write the code), THEN
|
|
27
|
+
`reply(message_id, "<concrete summary of what you did, with file paths and
|
|
28
|
+
line numbers>")`. Never reply "will do" / "understood" before doing the
|
|
29
|
+
work.
|
|
30
|
+
|
|
31
|
+
If the patchcord-subscribe listener is already running in the background,
|
|
32
|
+
you do not need to restart it after handling messages — unlike a
|
|
33
|
+
poll-and-exit script, it keeps running across wakes. Only restart it if you
|
|
34
|
+
have reason to believe it died (see the patchcord-subscribe skill).
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: patchcord-subscribe
|
|
3
|
+
description: >
|
|
4
|
+
Start the Patchcord background listener so new messages wake this agent.
|
|
5
|
+
Run this when the user asks to start Patchcord push delivery.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
jcode has no Monitor tool. A background task here wakes the agent on a
|
|
9
|
+
STALL — no new output for N seconds — not per line the way Claude Code's
|
|
10
|
+
Monitor does. Following Claude Code's own subscribe instructions verbatim on
|
|
11
|
+
jcode produces a listener that connects, receives, and never wakes you: the
|
|
12
|
+
pipe fills with messages you are never told about.
|
|
13
|
+
|
|
14
|
+
This skill exists to run correctly on jcode specifically, using
|
|
15
|
+
`patchcord subscribe --stall-signal`, a mode built for exactly this gap:
|
|
16
|
+
while idle it writes a `HEARTBEAT:` line often enough that the pipe never
|
|
17
|
+
looks silent, and on a real message it goes quiet on purpose for long enough
|
|
18
|
+
that jcode's own stall detector fires. This is this repo's global skill
|
|
19
|
+
directory (`~/.jcode/skills/`), shared by every jcode project on this
|
|
20
|
+
machine — nothing below may name a specific namespace, agent, or project
|
|
21
|
+
path.
|
|
22
|
+
|
|
23
|
+
# Start
|
|
24
|
+
|
|
25
|
+
1. **Drain the inbox first.** Call the Patchcord `inbox` MCP tool. Process
|
|
26
|
+
every pending message (see the patchcord-inbox skill) before starting the
|
|
27
|
+
listener — a backlog can accumulate while no listener was running, and
|
|
28
|
+
the listener does not replay what happened before it started.
|
|
29
|
+
|
|
30
|
+
2. **Spawn the listener with bash, in the background, with a stall wake:**
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
Bash(
|
|
34
|
+
command: "patchcord subscribe --stall-signal | grep --line-buffered '^PATCHCORD:'; exit ${PIPESTATUS[0]}",
|
|
35
|
+
run_in_background: true,
|
|
36
|
+
stall_wake_seconds: 15
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`stall_wake_seconds: 15` is not a placeholder — it MUST match
|
|
41
|
+
`--stall-signal`'s own default quiet window (15000ms). If you ever pass
|
|
42
|
+
`--stall-signal` a custom triple (`keepaliveMs:quietMs:stallMs`),
|
|
43
|
+
`stall_wake_seconds` must equal the `stallMs` you passed, or the two
|
|
44
|
+
numbers disagree and the wake either fires on nothing (too short) or
|
|
45
|
+
never fires on a real message (too long). Do not use 30 here — that
|
|
46
|
+
number predates `--stall-signal` and is shorter than nothing: it is
|
|
47
|
+
simply the wrong number for this mechanism, not a safer one.
|
|
48
|
+
|
|
49
|
+
The grep keeps only `PATCHCORD:` lines in the visible output; `HEARTBEAT:`
|
|
50
|
+
lines never match that pattern, so they never need special handling on
|
|
51
|
+
your end. `${PIPESTATUS[0]}` preserves subscribe's own exit code through
|
|
52
|
+
the pipe.
|
|
53
|
+
|
|
54
|
+
3. **Tell the user one line:** "Patchcord listener active — I'll check
|
|
55
|
+
regularly for new messages." Do not say "as messages arrive" — the
|
|
56
|
+
mechanism approximates that, it does not guarantee it, and promising more
|
|
57
|
+
than jcode can deliver is worse than being accurate.
|
|
58
|
+
|
|
59
|
+
# When the background task wakes you
|
|
60
|
+
|
|
61
|
+
A wake means the pipe went quiet for `stall_wake_seconds` — with
|
|
62
|
+
`--stall-signal` running, that quiet is now DESIGNED to correlate with a real
|
|
63
|
+
message, but two other things also produce exactly the same silence, and you
|
|
64
|
+
cannot tell which one happened from the wake alone:
|
|
65
|
+
|
|
66
|
+
1. **Check the inbox every time.** Call the Patchcord `inbox` MCP tool. If it
|
|
67
|
+
has pending messages, handle them (see the patchcord-inbox skill). If it
|
|
68
|
+
is empty, say nothing to the user and do not report the wake as an event —
|
|
69
|
+
an empty check is not news.
|
|
70
|
+
2. **If the inbox is empty, check whether the listener is still running**
|
|
71
|
+
before assuming this was a harmless false positive. A dead listener also
|
|
72
|
+
produces silence — that is a feature (it is how you notice), but only if
|
|
73
|
+
you actually look. If the background task has exited, restart it with the
|
|
74
|
+
command in Start, step 2.
|
|
75
|
+
3. **Never read the last `PATCHCORD:` line in the task output as news.** It
|
|
76
|
+
is scrollback — output already displayed. It may be the same line you
|
|
77
|
+
already handled. The inbox call in step 1 is the source of truth; a
|
|
78
|
+
`PATCHCORD:` line without a corresponding inbox check is not confirmation
|
|
79
|
+
of anything.
|
|
80
|
+
|
|
81
|
+
# Stopping
|
|
82
|
+
|
|
83
|
+
Tell the user one of:
|
|
84
|
+
|
|
85
|
+
- End this session.
|
|
86
|
+
- Kill the listener: `kill $(cat /tmp/patchcord_subscribe_<namespace>_<agent>.pid)`
|
|
87
|
+
(the exact path is printed by `patchcord subscribe` on start; do not guess
|
|
88
|
+
the namespace/agent — this is a global skill and cannot know them).
|
|
89
|
+
|
|
90
|
+
# If the background task ends on its own
|
|
91
|
+
|
|
92
|
+
Read its output. Scan for one of:
|
|
93
|
+
|
|
94
|
+
- `no patchcord config found` — not run from a project with `.jcode/mcp.json`
|
|
95
|
+
(or a parent project directory).
|
|
96
|
+
- `ticket: token rejected (HTTP 401|403)` — the bearer token is invalid or
|
|
97
|
+
expired.
|
|
98
|
+
- `already running (pid N)` (exit 2) — another listener is active for this
|
|
99
|
+
agent; report it, do not respawn.
|
|
100
|
+
- `subscribe: fatal: ...` — report the fatal line verbatim.
|
|
101
|
+
|
|
102
|
+
If none of those appear, it likely ended with the session or because its
|
|
103
|
+
output consumer closed. Restart it only when the user asks to resume Patchcord
|
|
104
|
+
push delivery.
|
|
105
|
+
|
|
106
|
+
**Forbidden on failure:** no hand-rolled polling loop in place of this
|
|
107
|
+
mechanism, no pidfile editing, no re-arm loop after every wake — the
|
|
108
|
+
background task under `run_in_background` keeps running across wakes; you
|
|
109
|
+
are only checking in, not restarting it, unless step 2 above found it dead.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: patchcord-wait
|
|
3
|
+
description: Wait for one incoming Patchcord message
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Applies only when Patchcord MCP tools are loaded
|
|
7
|
+
|
|
8
|
+
If the `wait_for_message` MCP tool is not available in this session, this
|
|
9
|
+
skill does not apply. Do nothing: do not run the Patchcord CLI, read a token
|
|
10
|
+
from a config file, or call the HTTP API. A missing project MCP config is a
|
|
11
|
+
normal state.
|
|
12
|
+
|
|
13
|
+
Call the `wait_for_message` MCP tool now to block until a message arrives or
|
|
14
|
+
~5 minutes elapse.
|
|
15
|
+
|
|
16
|
+
When a message arrives, classify it and act:
|
|
17
|
+
|
|
18
|
+
- **ACK** (thanks, noted, works, ok, 👍, no task) →
|
|
19
|
+
`reply(message_id, resolve=true)` with NO content. Never text-reply an ack.
|
|
20
|
+
- **BLOCKED** (cannot do the work right now) →
|
|
21
|
+
`reply(message_id, "<reason>", defer=true)`.
|
|
22
|
+
- **ACTIONABLE** → do the work FIRST, then
|
|
23
|
+
`reply(message_id, "<concrete summary of what you did>")`.
|
|
24
|
+
|
|
25
|
+
Use this skill for a single blocking wait, not as a substitute for the
|
|
26
|
+
persistent background listener — see the patchcord-subscribe skill for that.
|