pairai 0.5.1 → 0.5.2
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/package.json +1 -1
- package/pairai.ts +96 -11
package/package.json
CHANGED
package/pairai.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* PAIRAI_KEY_FILE — path to RSA private key .pem
|
|
15
15
|
* PAIRAI_POLL_MS — poll interval in ms (default: 5000)
|
|
16
16
|
* PAIRAI_LOCK_DIR — lock file directory (default: ~/.pairai/locks)
|
|
17
|
+
* PAIRAI_CHANNEL_NOTIFICATIONS — "1" = poll loop acks server cursor (for Claude with --channel)
|
|
17
18
|
* PAIRAI_DEBUG — verbose log: "1" for ~/.pairai/debug.log, or a file path
|
|
18
19
|
* Legacy: PAIRAI_URL, PAIRAI_API_KEY, PAIRAI_PRIVATE_KEY_PATH
|
|
19
20
|
*/
|
|
@@ -96,6 +97,7 @@ if (command === "help" || args.includes("--help") || args.includes("-h")) {
|
|
|
96
97
|
console.log(" PAIRAI_AGENT_CRED Agent API key");
|
|
97
98
|
console.log(" PAIRAI_KEY_FILE Path to RSA private key .pem");
|
|
98
99
|
console.log(" PAIRAI_POLL_MS Poll interval in ms (default: 5000)");
|
|
100
|
+
console.log(" PAIRAI_CHANNEL_NOTIFICATIONS=1 Poll acks cursor (Claude --channel)");
|
|
99
101
|
console.log(" PAIRAI_DEBUG=1 Verbose log to ~/.pairai/debug.log");
|
|
100
102
|
console.log("\nExamples:");
|
|
101
103
|
console.log(' npx pairai setup "My Assistant"');
|
|
@@ -515,6 +517,7 @@ if (command !== "serve") {
|
|
|
515
517
|
console.error(" PAIRAI_KEY_FILE Path to RSA private key .pem file");
|
|
516
518
|
console.error(" PAIRAI_POLL_MS Poll interval in ms (default: 5000)");
|
|
517
519
|
console.error(" PAIRAI_LOCK_DIR Lock file directory (default: ~/.pairai/locks)");
|
|
520
|
+
console.error(" PAIRAI_CHANNEL_NOTIFICATIONS=1 Poll acks cursor (Claude --channel)");
|
|
518
521
|
console.error(" PAIRAI_DEBUG=1 Verbose log to ~/.pairai/debug.log");
|
|
519
522
|
console.error(" PAIRAI_DEBUG=<path> Verbose log to custom file");
|
|
520
523
|
process.exit(1);
|
|
@@ -1128,9 +1131,14 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1128
1131
|
parts.push(`**Unread messages:**\n${enriched.join("\n")}`);
|
|
1129
1132
|
}
|
|
1130
1133
|
|
|
1131
|
-
// Ack
|
|
1134
|
+
// Ack server-side cursor. For channel-capable clients the poll loop also acks,
|
|
1135
|
+
// but for non-channel clients this is the only place the server cursor advances.
|
|
1132
1136
|
if (updates.cursor > 0) {
|
|
1133
1137
|
await hubPost("/events/ack", { cursor: updates.cursor });
|
|
1138
|
+
// Sync local poll cursor so we don't re-notify for these events
|
|
1139
|
+
if (!channelCapable && updates.cursor > lastNotifiedEventId) {
|
|
1140
|
+
lastNotifiedEventId = updates.cursor;
|
|
1141
|
+
}
|
|
1134
1142
|
}
|
|
1135
1143
|
|
|
1136
1144
|
return { content: [{ type: "text" as const, text: parts.join("\n\n") }] };
|
|
@@ -1896,12 +1904,81 @@ async function deliverEventNotification(event: {
|
|
|
1896
1904
|
}
|
|
1897
1905
|
}
|
|
1898
1906
|
|
|
1907
|
+
// Detect whether the MCP client reliably surfaces channel notifications.
|
|
1908
|
+
// Walk the process tree to find Claude Code with --dangerously-load-development-channels server:pairai-channel.
|
|
1909
|
+
// Falls back to PAIRAI_CHANNEL_NOTIFICATIONS=1 env var (non-Linux or custom setups).
|
|
1910
|
+
const CHANNEL_FLAG = "--dangerously-load-development-channels";
|
|
1911
|
+
const CHANNEL_VALUE = "server:pairai-channel";
|
|
1912
|
+
|
|
1913
|
+
function detectChannelCapable(): boolean {
|
|
1914
|
+
if (process.env.PAIRAI_CHANNEL_NOTIFICATIONS === "1") {
|
|
1915
|
+
debugLog("detect-channel: PAIRAI_CHANNEL_NOTIFICATIONS=1 (env override)");
|
|
1916
|
+
return true;
|
|
1917
|
+
}
|
|
1918
|
+
if (process.platform !== "linux") {
|
|
1919
|
+
debugLog(`detect-channel: platform=${process.platform} (not linux, skipping /proc walk)`);
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1922
|
+
try {
|
|
1923
|
+
let pid = String(process.ppid);
|
|
1924
|
+
debugLog(`detect-channel: starting walk from ppid=${pid}`);
|
|
1925
|
+
for (let i = 0; i < 10; i++) {
|
|
1926
|
+
const cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf-8").split("\0").filter(Boolean);
|
|
1927
|
+
const bin = cmdline[0] ?? "";
|
|
1928
|
+
debugLog(`detect-channel: pid=${pid} bin=${bin} args=${JSON.stringify(cmdline.slice(1))}`);
|
|
1929
|
+
if (bin === "claude" || bin.endsWith("/claude")) {
|
|
1930
|
+
// Check for our specific channel: --flag server:pairai-channel or --flag=server:pairai-channel
|
|
1931
|
+
let found = false;
|
|
1932
|
+
const channelArgs: string[] = [];
|
|
1933
|
+
for (let j = 1; j < cmdline.length; j++) {
|
|
1934
|
+
const arg = cmdline[j]!;
|
|
1935
|
+
if (arg === CHANNEL_FLAG && cmdline[j + 1]) {
|
|
1936
|
+
channelArgs.push(cmdline[j + 1]!);
|
|
1937
|
+
if (cmdline[j + 1] === CHANNEL_VALUE) found = true;
|
|
1938
|
+
j++; // skip value
|
|
1939
|
+
} else if (arg.startsWith(`${CHANNEL_FLAG}=`)) {
|
|
1940
|
+
const val = arg.slice(CHANNEL_FLAG.length + 1);
|
|
1941
|
+
channelArgs.push(val);
|
|
1942
|
+
if (val === CHANNEL_VALUE) found = true;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
debugLog(`detect-channel: found claude binary at pid=${pid}, channels=${JSON.stringify(channelArgs)}, looking for="${CHANNEL_VALUE}", match=${found}`);
|
|
1946
|
+
return found;
|
|
1947
|
+
}
|
|
1948
|
+
// Walk up: read ppid from /proc/<pid>/stat
|
|
1949
|
+
// Format: "pid (comm) state ppid ..." — comm can contain spaces/parens,
|
|
1950
|
+
// so find the LAST ")" to skip past it, then parse fields after it.
|
|
1951
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
1952
|
+
const afterComm = stat.slice(stat.lastIndexOf(")") + 2);
|
|
1953
|
+
const nextPid = afterComm.split(" ")[1]; // fields: state ppid ...
|
|
1954
|
+
debugLog(`detect-channel: pid=${pid} → ppid=${nextPid}`);
|
|
1955
|
+
if (!nextPid || nextPid === "1" || nextPid === "0") {
|
|
1956
|
+
debugLog(`detect-channel: reached process tree root (pid=${nextPid}), stopping`);
|
|
1957
|
+
break;
|
|
1958
|
+
}
|
|
1959
|
+
pid = nextPid;
|
|
1960
|
+
}
|
|
1961
|
+
debugLog("detect-channel: exhausted process tree without finding claude binary");
|
|
1962
|
+
} catch (err) {
|
|
1963
|
+
debugLog(`detect-channel: error walking /proc: ${(err as Error).message}`);
|
|
1964
|
+
}
|
|
1965
|
+
return false;
|
|
1966
|
+
}
|
|
1967
|
+
const channelCapable = detectChannelCapable();
|
|
1968
|
+
|
|
1969
|
+
// For non-channel clients: track the highest event ID we've notified for locally,
|
|
1970
|
+
// so we don't re-deliver on each poll cycle. Not used when channelCapable=true.
|
|
1971
|
+
let lastNotifiedEventId = 0;
|
|
1972
|
+
|
|
1899
1973
|
async function poll() {
|
|
1900
1974
|
try {
|
|
1901
1975
|
// Refresh public keys to pick up new connections
|
|
1902
1976
|
await loadPublicKeys();
|
|
1903
1977
|
|
|
1904
|
-
|
|
1978
|
+
// Non-channel clients: use local cursor to avoid re-notifying, but don't touch server cursor.
|
|
1979
|
+
// Channel clients: use server cursor (default behavior — omit after=).
|
|
1980
|
+
const afterQs = !channelCapable && lastNotifiedEventId > 0 ? `?after=${lastNotifiedEventId}` : "";
|
|
1981
|
+
const updates = (await hubGet(`/events${afterQs}`)) as {
|
|
1905
1982
|
events: Array<{
|
|
1906
1983
|
id: number;
|
|
1907
1984
|
type: string;
|
|
@@ -1914,7 +1991,7 @@ async function poll() {
|
|
|
1914
1991
|
hasMore: boolean;
|
|
1915
1992
|
};
|
|
1916
1993
|
|
|
1917
|
-
debugLog(`poll: ${updates.events.length} events, cursor=${updates.cursor}, hasMore=${updates.hasMore}`);
|
|
1994
|
+
debugLog(`poll: ${updates.events.length} events, cursor=${updates.cursor}, hasMore=${updates.hasMore}${channelCapable ? "" : `, localCursor=${lastNotifiedEventId}`}`);
|
|
1918
1995
|
|
|
1919
1996
|
if (updates.events.length === 0) return;
|
|
1920
1997
|
|
|
@@ -1926,14 +2003,20 @@ async function poll() {
|
|
|
1926
2003
|
}
|
|
1927
2004
|
}
|
|
1928
2005
|
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2006
|
+
if (channelCapable) {
|
|
2007
|
+
// Channel clients: ack server-side — notifications are reliably delivered
|
|
2008
|
+
if (updates.cursor > 0) {
|
|
2009
|
+
try {
|
|
2010
|
+
await hubPost("/events/ack", { cursor: updates.cursor });
|
|
2011
|
+
debugLog(`poll: acked cursor=${updates.cursor}`);
|
|
2012
|
+
} catch (err) {
|
|
2013
|
+
debugLog(`poll: ack failed (will retry next cycle): ${(err as Error).message}`);
|
|
2014
|
+
}
|
|
1936
2015
|
}
|
|
2016
|
+
} else {
|
|
2017
|
+
// Non-channel clients: advance local cursor only, leave server cursor for check_updates
|
|
2018
|
+
lastNotifiedEventId = updates.cursor;
|
|
2019
|
+
debugLog(`poll: local cursor advanced to ${lastNotifiedEventId}`);
|
|
1937
2020
|
}
|
|
1938
2021
|
|
|
1939
2022
|
if (updates.hasMore) {
|
|
@@ -1948,7 +2031,9 @@ async function poll() {
|
|
|
1948
2031
|
// ── Start ────────────────────────────────────────────────────────────────────
|
|
1949
2032
|
|
|
1950
2033
|
await mcp.connect(new StdioServerTransport());
|
|
1951
|
-
console.error(`[pairai] connected. provider=${serveProvider}
|
|
2034
|
+
console.error(`[pairai] connected. provider=${serveProvider} channelNotifications=${channelCapable} agent=${myAgentId || "(loading)"}`);
|
|
2035
|
+
debugLog(`startup: provider=${serveProvider} channelCapable=${channelCapable} (${channelCapable ? "poll acks server cursor" : "poll uses local cursor, check_updates acks"})`);
|
|
2036
|
+
|
|
1952
2037
|
await loadAgentInfo();
|
|
1953
2038
|
if (!myAgentId) {
|
|
1954
2039
|
console.error("[pairai] failed to load agent info from hub. Cannot start polling.");
|