@pinet/slack-bridge 0.2.1 → 0.2.4
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/README.md +302 -448
- package/dist/broker/adapters/slack.d.ts +11 -0
- package/dist/broker/adapters/slack.js +90 -2
- package/dist/broker/client.d.ts +3 -1
- package/dist/broker/client.js +11 -0
- package/dist/broker/socket-server.d.ts +2 -0
- package/dist/broker/socket-server.js +89 -2
- package/dist/helpers.d.ts +17 -0
- package/dist/helpers.js +12 -1
- package/dist/index.js +3 -1
- package/dist/pinet-mesh-ops.d.ts +6 -1
- package/dist/pinet-mesh-ops.js +20 -0
- package/dist/pinet-session-formatting.d.ts +13 -0
- package/dist/pinet-session-formatting.js +80 -0
- package/dist/pinet-tools.d.ts +4 -1
- package/dist/pinet-tools.js +148 -3
- package/dist/slack-markdown.d.ts +1 -0
- package/dist/slack-markdown.js +128 -0
- package/dist/slack-pinet-runtime-adapter.js +7 -0
- package/dist/slack-tools.d.ts +8 -0
- package/dist/slack-tools.js +26 -49
- package/dist/subtree-broker-runtime.d.ts +3 -0
- package/dist/subtree-broker-runtime.js +3 -0
- package/package.json +5 -5
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
function stableIdDigest(stableId) {
|
|
4
|
+
return crypto.createHash("sha256").update(stableId).digest("hex").slice(0, 12);
|
|
5
|
+
}
|
|
6
|
+
export function parsePinetStableId(stableId) {
|
|
7
|
+
const value = stableId?.trim();
|
|
8
|
+
if (!value)
|
|
9
|
+
return null;
|
|
10
|
+
const firstColon = value.indexOf(":");
|
|
11
|
+
const secondColon = firstColon >= 0 ? value.indexOf(":", firstColon + 1) : -1;
|
|
12
|
+
if (firstColon < 0 || secondColon < 0) {
|
|
13
|
+
return { host: null, kind: "unknown", locator: value, hasPath: false };
|
|
14
|
+
}
|
|
15
|
+
const host = value.slice(0, firstColon) || null;
|
|
16
|
+
const rawKind = value.slice(firstColon + 1, secondColon);
|
|
17
|
+
const locator = value.slice(secondColon + 1);
|
|
18
|
+
const kind = rawKind === "session" || rawKind === "leaf" || rawKind === "cwd" || rawKind === "broker"
|
|
19
|
+
? rawKind
|
|
20
|
+
: "unknown";
|
|
21
|
+
return {
|
|
22
|
+
host,
|
|
23
|
+
kind,
|
|
24
|
+
locator,
|
|
25
|
+
hasPath: locator.startsWith("/") && (kind === "session" || kind === "cwd" || kind === "broker"),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function summarizePinetStableId(stableId) {
|
|
29
|
+
const value = stableId?.trim();
|
|
30
|
+
if (!value)
|
|
31
|
+
return null;
|
|
32
|
+
const parsed = parsePinetStableId(value);
|
|
33
|
+
const kind = parsed?.kind ?? "unknown";
|
|
34
|
+
return {
|
|
35
|
+
kind,
|
|
36
|
+
ref: `${kind}:${stableIdDigest(value)}`,
|
|
37
|
+
host: parsed?.host ?? null,
|
|
38
|
+
hasPath: parsed?.hasPath ?? false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function getPinetSessionPath(stableId) {
|
|
42
|
+
const parsed = parsePinetStableId(stableId);
|
|
43
|
+
if (!parsed?.hasPath)
|
|
44
|
+
return null;
|
|
45
|
+
return parsed.locator;
|
|
46
|
+
}
|
|
47
|
+
export function getPinetSessionFilename(stableId) {
|
|
48
|
+
const sessionPath = getPinetSessionPath(stableId);
|
|
49
|
+
return sessionPath ? path.basename(sessionPath) : null;
|
|
50
|
+
}
|
|
51
|
+
export function buildPinetSessionFullDetails(session) {
|
|
52
|
+
const summary = summarizePinetStableId(session.stableId);
|
|
53
|
+
const sessionPath = getPinetSessionPath(session.stableId);
|
|
54
|
+
return {
|
|
55
|
+
...session,
|
|
56
|
+
session: summary,
|
|
57
|
+
...(sessionPath ? { jsonlPath: sessionPath } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function buildPinetSessionCompactDetails(session) {
|
|
61
|
+
const summary = summarizePinetStableId(session.stableId);
|
|
62
|
+
return {
|
|
63
|
+
agentId: session.agentId,
|
|
64
|
+
agentName: session.agentName,
|
|
65
|
+
emoji: session.emoji,
|
|
66
|
+
pid: session.pid,
|
|
67
|
+
status: session.status,
|
|
68
|
+
health: session.disconnectedAt ? "disconnected" : "live",
|
|
69
|
+
session: summary?.ref ?? null,
|
|
70
|
+
sessionKind: summary?.kind ?? null,
|
|
71
|
+
host: summary?.host ?? null,
|
|
72
|
+
repo: session.repo,
|
|
73
|
+
branch: session.branch,
|
|
74
|
+
tmuxSession: session.tmuxSession,
|
|
75
|
+
lastSeen: session.lastSeen,
|
|
76
|
+
disconnectedAt: session.disconnectedAt,
|
|
77
|
+
relatedThreadIds: session.relatedThreadIds.slice(0, 5),
|
|
78
|
+
matchedBy: session.matchedBy,
|
|
79
|
+
};
|
|
80
|
+
}
|
package/dist/pinet-tools.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { type PinetReadOptions, type PinetReadResult } from "@pinet/pinet-core/pinet-read-formatting";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { RalphSnoozeStatus } from "./ralph-loop.js";
|
|
4
|
-
import type { PortLeaseAcquireInput, PortLeaseInfo, PortLeaseListOptions, PortLeaseReleaseInput, PortLeaseRenewInput, PinetLaneInfo, PinetLaneListOptions, PinetLaneParticipantInfo, PinetLaneParticipantUpsertInput, PinetLaneUpsertInput } from "./broker/types.js";
|
|
4
|
+
import type { AgentSessionSearchInfo, AgentSessionSearchOptions, AgentSessionSummary, PortLeaseAcquireInput, PortLeaseInfo, PortLeaseListOptions, PortLeaseReleaseInput, PortLeaseRenewInput, PinetLaneInfo, PinetLaneListOptions, PinetLaneParticipantInfo, PinetLaneParticipantUpsertInput, PinetLaneUpsertInput } from "./broker/types.js";
|
|
5
5
|
export interface PinetToolsAgentRecord {
|
|
6
6
|
emoji: string;
|
|
7
7
|
name: string;
|
|
8
8
|
id: string;
|
|
9
9
|
pid?: number;
|
|
10
|
+
stableId?: string | null;
|
|
11
|
+
session?: AgentSessionSummary | null;
|
|
10
12
|
status: "working" | "idle";
|
|
11
13
|
metadata: Record<string, unknown> | null;
|
|
12
14
|
lastHeartbeat: string;
|
|
@@ -76,6 +78,7 @@ export interface RegisterPinetToolsDeps {
|
|
|
76
78
|
readPinetInbox: (options: PinetReadOptions) => Promise<PinetReadResult>;
|
|
77
79
|
listBrokerAgents: () => PinetToolsAgentRecord[];
|
|
78
80
|
listFollowerAgents: (includeGhosts: boolean) => Promise<PinetToolsAgentRecord[]>;
|
|
81
|
+
searchPinetSessions: (options: AgentSessionSearchOptions) => Promise<AgentSessionSearchInfo[]>;
|
|
79
82
|
listSubtreeAgents?: (includeGhosts: boolean) => PinetToolsAgentRecord[] | null;
|
|
80
83
|
getSubtreeSelfAgentId?: () => string | null;
|
|
81
84
|
spawnSubtreeWorker?: (input: PinetSubtreeSpawnInput) => Promise<PinetSubtreeSpawnResult>;
|
package/dist/pinet-tools.js
CHANGED
|
@@ -8,6 +8,7 @@ import { buildAgentDisplayInfo, filterAgentsForMeshVisibility, formatAgentList,
|
|
|
8
8
|
import { isBroadcastChannelTarget } from "./broker/agent-messaging.js";
|
|
9
9
|
import { DEFAULT_HEARTBEAT_TIMEOUT_MS } from "./broker/socket-server.js";
|
|
10
10
|
import { HEARTBEAT_INTERVAL_MS } from "./broker/client.js";
|
|
11
|
+
import { buildPinetSessionCompactDetails, buildPinetSessionFullDetails, getPinetSessionFilename, getPinetSessionPath, summarizePinetStableId, } from "./pinet-session-formatting.js";
|
|
11
12
|
const PINET_DISPATCHER_EXAMPLES = {
|
|
12
13
|
send: [{ action: "send", args: { to: "@worker", message: "Please review PR #123" } }],
|
|
13
14
|
read: [
|
|
@@ -29,6 +30,10 @@ const PINET_DISPATCHER_EXAMPLES = {
|
|
|
29
30
|
],
|
|
30
31
|
schedule: [{ action: "schedule", args: { delay: "30m", message: "Check queue state" } }],
|
|
31
32
|
agents: [{ action: "agents", args: { repo: "<repo>", role: "worker" } }],
|
|
33
|
+
sessions: [
|
|
34
|
+
{ action: "sessions", args: { agent_name: "Frozen Hazel Whale" } },
|
|
35
|
+
{ action: "sessions", args: { thread_id: "a2a:<broker>:<worker>", full: true } },
|
|
36
|
+
],
|
|
32
37
|
spawn: [
|
|
33
38
|
{
|
|
34
39
|
action: "spawn",
|
|
@@ -89,6 +94,7 @@ function normalizeDispatcherAction(value) {
|
|
|
89
94
|
"snooze",
|
|
90
95
|
"schedule",
|
|
91
96
|
"agents",
|
|
97
|
+
"sessions",
|
|
92
98
|
"lanes",
|
|
93
99
|
"ports",
|
|
94
100
|
"spawn",
|
|
@@ -137,7 +143,8 @@ function classifyPinetError(message) {
|
|
|
137
143
|
message.includes("ttl_ms") ||
|
|
138
144
|
message.includes("purpose") ||
|
|
139
145
|
message.includes("port") ||
|
|
140
|
-
message.includes("spawn")
|
|
146
|
+
message.includes("spawn") ||
|
|
147
|
+
message.includes("sessions op")) {
|
|
141
148
|
return {
|
|
142
149
|
class: "input",
|
|
143
150
|
message,
|
|
@@ -803,6 +810,8 @@ function buildCompactAgentDetails(agents, hint) {
|
|
|
803
810
|
repo: getAgentRepo(agent) ?? null,
|
|
804
811
|
branch: getAgentBranch(agent) ?? null,
|
|
805
812
|
role: getAgentRole(agent) ?? null,
|
|
813
|
+
session: agent.session?.ref ?? null,
|
|
814
|
+
sessionKind: agent.session?.kind ?? null,
|
|
806
815
|
brokerManaged: agent.metadata?.brokerManaged === true,
|
|
807
816
|
parentAgentId: agent.metadata?.parentAgentId ?? null,
|
|
808
817
|
treeDepth: agent.metadata?.treeDepth ?? 0,
|
|
@@ -1363,6 +1372,123 @@ function runPinetAgentsAction(params, deps, toolName, output) {
|
|
|
1363
1372
|
};
|
|
1364
1373
|
})();
|
|
1365
1374
|
}
|
|
1375
|
+
function buildPinetSessionSearchOptions(params) {
|
|
1376
|
+
return {
|
|
1377
|
+
...((getMaybeString(params, "agent_name") ?? getMaybeString(params, "name"))
|
|
1378
|
+
? { agentName: getMaybeString(params, "agent_name") ?? getMaybeString(params, "name") }
|
|
1379
|
+
: {}),
|
|
1380
|
+
...(getMaybeString(params, "agent_id") ? { agentId: getMaybeString(params, "agent_id") } : {}),
|
|
1381
|
+
...(getMaybeString(params, "thread_id")
|
|
1382
|
+
? { threadId: getMaybeString(params, "thread_id") }
|
|
1383
|
+
: {}),
|
|
1384
|
+
...(getMaybeString(params, "repo") ? { repo: getMaybeString(params, "repo") } : {}),
|
|
1385
|
+
...(getMaybeString(params, "worktree_path")
|
|
1386
|
+
? { worktreePath: getMaybeString(params, "worktree_path") }
|
|
1387
|
+
: {}),
|
|
1388
|
+
...(getMaybeString(params, "tmux_session")
|
|
1389
|
+
? { tmuxSession: getMaybeString(params, "tmux_session") }
|
|
1390
|
+
: {}),
|
|
1391
|
+
...(getMaybeString(params, "since") ? { since: getMaybeString(params, "since") } : {}),
|
|
1392
|
+
...(getMaybeString(params, "until") ? { until: getMaybeString(params, "until") } : {}),
|
|
1393
|
+
...(getMaybeNumber(params, "limit") ? { limit: getMaybeNumber(params, "limit") } : {}),
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
function formatPinetSessionSearchHeader(sessions, options) {
|
|
1397
|
+
const filters = [
|
|
1398
|
+
options.agentName ? `agent_name=${options.agentName}` : null,
|
|
1399
|
+
options.agentId ? `agent_id=${options.agentId}` : null,
|
|
1400
|
+
options.threadId ? `thread_id=${options.threadId}` : null,
|
|
1401
|
+
options.repo ? `repo=${options.repo}` : null,
|
|
1402
|
+
options.worktreePath ? `worktree=${options.worktreePath}` : null,
|
|
1403
|
+
options.tmuxSession ? `tmux=${options.tmuxSession}` : null,
|
|
1404
|
+
options.since ? `since=${options.since}` : null,
|
|
1405
|
+
options.until ? `until=${options.until}` : null,
|
|
1406
|
+
].filter((item) => Boolean(item));
|
|
1407
|
+
const filterText = filters.length > 0 ? ` for ${filters.join(" · ")}` : "";
|
|
1408
|
+
return `Pinet sessions: ${sessions.length} match${sessions.length === 1 ? "" : "es"}${filterText}.`;
|
|
1409
|
+
}
|
|
1410
|
+
function formatPinetSessionLine(session, full) {
|
|
1411
|
+
const summary = summarizePinetStableId(session.stableId);
|
|
1412
|
+
const health = session.disconnectedAt ? "disconnected" : "live";
|
|
1413
|
+
const where = [
|
|
1414
|
+
session.repo ?? null,
|
|
1415
|
+
session.branch ? `branch=${session.branch}` : null,
|
|
1416
|
+
session.tmuxSession ? `tmux=${session.tmuxSession}` : null,
|
|
1417
|
+
].filter((item) => Boolean(item));
|
|
1418
|
+
const threads = session.relatedThreadIds.slice(0, full ? 10 : 3).join(", ");
|
|
1419
|
+
const suffix = session.relatedThreadIds.length > (full ? 10 : 3) ? " …" : "";
|
|
1420
|
+
const base = [
|
|
1421
|
+
`- ${session.emoji} ${session.agentName} (${session.agentId})`,
|
|
1422
|
+
`${health}`,
|
|
1423
|
+
`pid:${session.pid}`,
|
|
1424
|
+
summary?.ref ? `session:${summary.ref}` : "session:none",
|
|
1425
|
+
`lastSeen:${session.lastSeen}`,
|
|
1426
|
+
where.length > 0 ? where.join(" · ") : null,
|
|
1427
|
+
].filter((item) => Boolean(item));
|
|
1428
|
+
const lines = [base.join(" — ")];
|
|
1429
|
+
if (threads) {
|
|
1430
|
+
lines.push(` threads: ${threads}${suffix}`);
|
|
1431
|
+
}
|
|
1432
|
+
if (full) {
|
|
1433
|
+
const sessionPath = getPinetSessionPath(session.stableId);
|
|
1434
|
+
const sessionFilename = getPinetSessionFilename(session.stableId);
|
|
1435
|
+
if (session.stableId)
|
|
1436
|
+
lines.push(` stableId: ${session.stableId}`);
|
|
1437
|
+
if (sessionPath)
|
|
1438
|
+
lines.push(` jsonl: ${sessionPath}`);
|
|
1439
|
+
if (sessionFilename)
|
|
1440
|
+
lines.push(` jsonlFile: ${sessionFilename}`);
|
|
1441
|
+
if (session.cwd)
|
|
1442
|
+
lines.push(` cwd: ${session.cwd}`);
|
|
1443
|
+
if (session.repoRoot)
|
|
1444
|
+
lines.push(` repoRoot: ${session.repoRoot}`);
|
|
1445
|
+
if (session.worktreePath)
|
|
1446
|
+
lines.push(` worktreePath: ${session.worktreePath}`);
|
|
1447
|
+
if (session.brokerManaged) {
|
|
1448
|
+
lines.push(` managed: source=${session.launchSource ?? "broker"}${session.brokerManagedBy ? ` by=${session.brokerManagedBy}` : ""}`);
|
|
1449
|
+
}
|
|
1450
|
+
lines.push(` matchedBy: ${session.matchedBy.join(", ")}`);
|
|
1451
|
+
}
|
|
1452
|
+
return lines.join("\n");
|
|
1453
|
+
}
|
|
1454
|
+
function formatPinetSessionSearch(sessions, options, full) {
|
|
1455
|
+
const header = formatPinetSessionSearchHeader(sessions, options);
|
|
1456
|
+
if (sessions.length === 0)
|
|
1457
|
+
return `${header} Try a broader agent_name, thread_id, or repo filter.`;
|
|
1458
|
+
const body = sessions.map((session) => formatPinetSessionLine(session, full)).join("\n");
|
|
1459
|
+
const fullHint = full ? "" : "\nUse args.full=true for exact stableId/jsonl paths.";
|
|
1460
|
+
return `${header}\n${body}${fullHint}`;
|
|
1461
|
+
}
|
|
1462
|
+
function runPinetSessionsAction(params, deps, toolName, output) {
|
|
1463
|
+
return (async () => {
|
|
1464
|
+
const op = getMaybeString(params, "op") ?? "search";
|
|
1465
|
+
if (op !== "search") {
|
|
1466
|
+
throw new Error("sessions op must be search");
|
|
1467
|
+
}
|
|
1468
|
+
const options = buildPinetSessionSearchOptions(params);
|
|
1469
|
+
deps.requireToolPolicy(toolName, undefined, `agent_name=${options.agentName ?? ""} | agent_id=${options.agentId ?? ""} | thread_id=${options.threadId ?? ""} | repo=${options.repo ?? ""} | worktree_path=${options.worktreePath ?? ""} | tmux_session=${options.tmuxSession ?? ""} | since=${options.since ?? ""} | until=${options.until ?? ""} | limit=${options.limit ?? ""} | format=${output.format} | full=${output.full}`);
|
|
1470
|
+
if (!deps.pinetEnabled()) {
|
|
1471
|
+
throw new Error("Pinet is not running. Use /pinet start or /pinet follow first.");
|
|
1472
|
+
}
|
|
1473
|
+
const sessions = await deps.searchPinetSessions(options);
|
|
1474
|
+
const text = formatPinetSessionSearch(sessions, options, output.full);
|
|
1475
|
+
return {
|
|
1476
|
+
content: [{ type: "text", text }],
|
|
1477
|
+
details: { count: sessions.length, sessions, options },
|
|
1478
|
+
compactDetails: {
|
|
1479
|
+
count: sessions.length,
|
|
1480
|
+
options,
|
|
1481
|
+
sessions: sessions.map(buildPinetSessionCompactDetails),
|
|
1482
|
+
},
|
|
1483
|
+
fullDetails: {
|
|
1484
|
+
count: sessions.length,
|
|
1485
|
+
options,
|
|
1486
|
+
sessions: sessions.map(buildPinetSessionFullDetails),
|
|
1487
|
+
},
|
|
1488
|
+
expandedText: text,
|
|
1489
|
+
};
|
|
1490
|
+
})();
|
|
1491
|
+
}
|
|
1366
1492
|
export function registerPinetTools(pi, deps) {
|
|
1367
1493
|
const actionDefinitions = new Map();
|
|
1368
1494
|
function registerAction(definition) {
|
|
@@ -1478,6 +1604,25 @@ export function registerPinetTools(pi, deps) {
|
|
|
1478
1604
|
}),
|
|
1479
1605
|
execute: (_id, params, output) => runPinetAgentsAction(params, deps, "pinet:agents", output),
|
|
1480
1606
|
});
|
|
1607
|
+
registerAction({
|
|
1608
|
+
name: "sessions",
|
|
1609
|
+
description: "Search live and historical Pinet worker Pi sessions by display name, agent id, thread, repo/worktree, tmux session, or time range.",
|
|
1610
|
+
parameters: Type.Object({
|
|
1611
|
+
op: Type.Optional(Type.String({ description: "Operation: search (default)" })),
|
|
1612
|
+
agent_name: Type.Optional(Type.String({ description: "Worker display name, e.g. Frozen Hazel Whale" })),
|
|
1613
|
+
name: Type.Optional(Type.String({ description: "Alias for agent_name" })),
|
|
1614
|
+
agent_id: Type.Optional(Type.String({ description: "Pinet agent id or id prefix" })),
|
|
1615
|
+
thread_id: Type.Optional(Type.String({ description: "Related Pinet/Slack/A2A thread id" })),
|
|
1616
|
+
repo: Type.Optional(Type.String({ description: "Repo name or path fragment" })),
|
|
1617
|
+
worktree_path: Type.Optional(Type.String({ description: "Worktree/cwd path fragment" })),
|
|
1618
|
+
tmux_session: Type.Optional(Type.String({ description: "Broker-managed tmux session name" })),
|
|
1619
|
+
since: Type.Optional(Type.String({ description: "Only sessions active after this ISO time" })),
|
|
1620
|
+
until: Type.Optional(Type.String({ description: "Only sessions started before this ISO time" })),
|
|
1621
|
+
limit: Type.Optional(Type.Number({ description: "Maximum matches (default 20, max 100)" })),
|
|
1622
|
+
...PINET_OUTPUT_OPTION_PARAMETERS,
|
|
1623
|
+
}),
|
|
1624
|
+
execute: (_id, params, output) => runPinetSessionsAction(params, deps, "pinet:sessions", output),
|
|
1625
|
+
});
|
|
1481
1626
|
registerAction({
|
|
1482
1627
|
name: "ports",
|
|
1483
1628
|
description: "Acquire, renew, release, inspect, list, or expire durable Pinet local port leases.",
|
|
@@ -1549,10 +1694,10 @@ export function registerPinetTools(pi, deps) {
|
|
|
1549
1694
|
name: "pinet",
|
|
1550
1695
|
label: "Pinet Dispatcher",
|
|
1551
1696
|
description: "Dispatch Pinet operations by action with compact help and schema discovery.",
|
|
1552
|
-
promptSnippet: 'Use this compact dispatcher for Pinet actions: send, read, free, snooze, schedule, agents, lanes, ports, reload, exit, spawn, and help. Use /pinet start, /pinet follow, /pinet unfollow, and /pinet subtree start for TUI lifecycle changes. Defaults to terse CLI text; pass args.format="json" for the compact envelope or args.full=true for verbose/debug detail.',
|
|
1697
|
+
promptSnippet: 'Use this compact dispatcher for Pinet actions: send, read, free, snooze, schedule, agents, sessions, lanes, ports, reload, exit, spawn, and help. Use /pinet start, /pinet follow, /pinet unfollow, and /pinet subtree start for TUI lifecycle changes. Defaults to terse CLI text; pass args.format="json" for the compact envelope or args.full=true for verbose/debug detail.',
|
|
1553
1698
|
parameters: Type.Object({
|
|
1554
1699
|
action: Type.String({
|
|
1555
|
-
description: "Action name: help, send, read, free, snooze, schedule, agents, lanes, ports, reload, or exit. Also supports spawn for launching worker-owned subtree children.",
|
|
1700
|
+
description: "Action name: help, send, read, free, snooze, schedule, agents, sessions, lanes, ports, reload, or exit. Also supports spawn for launching worker-owned subtree children.",
|
|
1556
1701
|
}),
|
|
1557
1702
|
args: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
|
|
1558
1703
|
description: 'Action arguments. Add format="cli"|"json" (or f/"-f") for presentation, and full=true (or "--full": true) only for verbose/debug details. Default cli and non-full json keep data.details compact.',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function renderMarkdownForSlackMrkdwn(text: string): string;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export function renderMarkdownForSlackMrkdwn(text) {
|
|
2
|
+
if (!text.includes("**")) {
|
|
3
|
+
return text;
|
|
4
|
+
}
|
|
5
|
+
return transformOutsideBacktickCode(text, convertMarkdownStrongToSlackBold);
|
|
6
|
+
}
|
|
7
|
+
function transformOutsideBacktickCode(text, transform) {
|
|
8
|
+
let output = "";
|
|
9
|
+
let segmentStart = 0;
|
|
10
|
+
let index = 0;
|
|
11
|
+
while (index < text.length) {
|
|
12
|
+
if (text[index] !== "`") {
|
|
13
|
+
index += 1;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const runLength = countRepeated(text, index, "`");
|
|
17
|
+
const closingIndex = findClosingBacktickRun(text, index + runLength, runLength);
|
|
18
|
+
if (closingIndex === -1) {
|
|
19
|
+
index += runLength;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
output += transform(text.slice(segmentStart, index));
|
|
23
|
+
output += text.slice(index, closingIndex + runLength);
|
|
24
|
+
index = closingIndex + runLength;
|
|
25
|
+
segmentStart = index;
|
|
26
|
+
}
|
|
27
|
+
output += transform(text.slice(segmentStart));
|
|
28
|
+
return output;
|
|
29
|
+
}
|
|
30
|
+
function convertMarkdownStrongToSlackBold(segment) {
|
|
31
|
+
let output = "";
|
|
32
|
+
let index = 0;
|
|
33
|
+
while (index < segment.length) {
|
|
34
|
+
if (segment.startsWith("**", index) && isStrongOpeningDelimiter(segment, index)) {
|
|
35
|
+
const closingIndex = findStrongClosingDelimiter(segment, index + 2);
|
|
36
|
+
if (closingIndex !== -1) {
|
|
37
|
+
output += `*${segment.slice(index + 2, closingIndex)}*`;
|
|
38
|
+
index = closingIndex + 2;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
output += segment[index];
|
|
43
|
+
index += 1;
|
|
44
|
+
}
|
|
45
|
+
return output;
|
|
46
|
+
}
|
|
47
|
+
function findStrongClosingDelimiter(segment, fromIndex) {
|
|
48
|
+
let index = fromIndex;
|
|
49
|
+
while (index < segment.length) {
|
|
50
|
+
const next = segment.indexOf("**", index);
|
|
51
|
+
if (next === -1) {
|
|
52
|
+
return -1;
|
|
53
|
+
}
|
|
54
|
+
if (isStrongClosingDelimiter(segment, next)) {
|
|
55
|
+
return next;
|
|
56
|
+
}
|
|
57
|
+
index = next + 2;
|
|
58
|
+
}
|
|
59
|
+
return -1;
|
|
60
|
+
}
|
|
61
|
+
function isStrongOpeningDelimiter(text, index) {
|
|
62
|
+
if (isEscaped(text, index)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const previous = text[index - 1];
|
|
66
|
+
const next = text[index + 2];
|
|
67
|
+
return isSafeOpeningBoundary(previous, next);
|
|
68
|
+
}
|
|
69
|
+
function isStrongClosingDelimiter(text, index) {
|
|
70
|
+
if (isEscaped(text, index)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const previous = text[index - 1];
|
|
74
|
+
const next = text[index + 2];
|
|
75
|
+
return isSafeClosingBoundary(previous, next);
|
|
76
|
+
}
|
|
77
|
+
function isSafeOpeningBoundary(previous, next) {
|
|
78
|
+
if (!next || isWhitespace(next) || next === "*" || next === "/") {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (previous === "*" || previous === "/") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
function isSafeClosingBoundary(previous, next) {
|
|
87
|
+
if (!previous || isWhitespace(previous) || previous === "*" || previous === "/") {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (next === "*" || next === "/") {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
function isWhitespace(value) {
|
|
96
|
+
return /\s/u.test(value);
|
|
97
|
+
}
|
|
98
|
+
function countRepeated(text, index, char) {
|
|
99
|
+
let count = 0;
|
|
100
|
+
while (text[index + count] === char) {
|
|
101
|
+
count += 1;
|
|
102
|
+
}
|
|
103
|
+
return count;
|
|
104
|
+
}
|
|
105
|
+
function findClosingBacktickRun(text, fromIndex, runLength) {
|
|
106
|
+
let index = fromIndex;
|
|
107
|
+
while (index < text.length) {
|
|
108
|
+
const next = text.indexOf("`", index);
|
|
109
|
+
if (next === -1) {
|
|
110
|
+
return -1;
|
|
111
|
+
}
|
|
112
|
+
const nextRunLength = countRepeated(text, next, "`");
|
|
113
|
+
if (nextRunLength === runLength) {
|
|
114
|
+
return next;
|
|
115
|
+
}
|
|
116
|
+
index = next + nextRunLength;
|
|
117
|
+
}
|
|
118
|
+
return -1;
|
|
119
|
+
}
|
|
120
|
+
function isEscaped(text, index) {
|
|
121
|
+
let slashCount = 0;
|
|
122
|
+
let cursor = index - 1;
|
|
123
|
+
while (cursor >= 0 && text[cursor] === "\\") {
|
|
124
|
+
slashCount += 1;
|
|
125
|
+
cursor -= 1;
|
|
126
|
+
}
|
|
127
|
+
return slashCount % 2 === 1;
|
|
128
|
+
}
|
|
@@ -61,6 +61,7 @@ export function createSlackPinetRuntimeAdapterFactory(deps) {
|
|
|
61
61
|
appToken: deps.getAppToken(),
|
|
62
62
|
allowedUsers: allowedUsers ? [...allowedUsers] : undefined,
|
|
63
63
|
allowAllWorkspaceUsers: deps.shouldAllowAllWorkspaceUsers(),
|
|
64
|
+
ingressGuard: settings.ingressGuard,
|
|
64
65
|
suggestedPrompts: settings.suggestedPrompts,
|
|
65
66
|
reactionCommands: settings.reactionCommands,
|
|
66
67
|
isKnownThread: (threadTs) => shouldRouteKnownSlackThread(broker.db.getThread(threadTs)),
|
|
@@ -69,6 +70,12 @@ export function createSlackPinetRuntimeAdapterFactory(deps) {
|
|
|
69
70
|
rememberKnownSlackThread(broker, threadTs, channelId, context);
|
|
70
71
|
},
|
|
71
72
|
isReactionThreadAuthorized: (threadTs, channelId) => isAuthorizedReactionThread(broker, threadTs, channelId),
|
|
73
|
+
isPinetOwnedThread: (threadTs, channelId) => {
|
|
74
|
+
const thread = broker.db.getThread(threadTs);
|
|
75
|
+
if (!thread || thread.source !== "slack" || thread.channel !== channelId)
|
|
76
|
+
return false;
|
|
77
|
+
return !!thread.ownerAgent || readStoredSlackThreadContext(thread.metadata) !== null;
|
|
78
|
+
},
|
|
72
79
|
onAppHomeOpened: async ({ userId }) => {
|
|
73
80
|
await deps.onAppHomeOpened(userId, ctx);
|
|
74
81
|
},
|
package/dist/slack-tools.d.ts
CHANGED
|
@@ -26,6 +26,14 @@ export interface SlackPinetDeliveryResult {
|
|
|
26
26
|
source: string;
|
|
27
27
|
}
|
|
28
28
|
export interface SlackPinetDeliveryPort {
|
|
29
|
+
/**
|
|
30
|
+
* True when Pinet is configured/enabled for this session, regardless of
|
|
31
|
+
* live broker connectivity. When true, threaded Slack replies MUST route
|
|
32
|
+
* through the broker; direct Slack fallback is refused even if the broker
|
|
33
|
+
* is momentarily unavailable, to preserve broker-enforced thread
|
|
34
|
+
* ownership (see gugu91/extensions#855).
|
|
35
|
+
*/
|
|
36
|
+
isEnabled: () => boolean;
|
|
29
37
|
isAvailable: () => boolean;
|
|
30
38
|
sendSlackMessage: (input: SlackPinetDeliveryInput) => Promise<SlackPinetDeliveryResult>;
|
|
31
39
|
}
|
package/dist/slack-tools.js
CHANGED
|
@@ -180,25 +180,6 @@ function isAbortError(error) {
|
|
|
180
180
|
function getErrorMessage(error) {
|
|
181
181
|
return error instanceof Error ? error.message : String(error);
|
|
182
182
|
}
|
|
183
|
-
function isPinetDeliveryFallbackError(error) {
|
|
184
|
-
const lower = getErrorMessage(error).toLowerCase();
|
|
185
|
-
if (lower.includes("already owned"))
|
|
186
|
-
return false;
|
|
187
|
-
return (lower.includes("not running") ||
|
|
188
|
-
lower.includes("unexpected state") ||
|
|
189
|
-
lower.includes("unavailable") ||
|
|
190
|
-
lower.includes("not connected") ||
|
|
191
|
-
lower.includes("disconnected") ||
|
|
192
|
-
lower.includes("timeout") ||
|
|
193
|
-
lower.includes("timed out") ||
|
|
194
|
-
lower.includes("econn") ||
|
|
195
|
-
lower.includes("socket") ||
|
|
196
|
-
lower.includes("no transport source") ||
|
|
197
|
-
lower.includes("no transport channel") ||
|
|
198
|
-
lower.includes("only allows local file paths") ||
|
|
199
|
-
lower.includes("no adapter") ||
|
|
200
|
-
lower.includes("identity is unavailable"));
|
|
201
|
-
}
|
|
202
183
|
function classifySlackDispatcherError(error) {
|
|
203
184
|
const message = getErrorMessage(error);
|
|
204
185
|
const lower = message.toLowerCase();
|
|
@@ -423,6 +404,7 @@ function buildSlackSendPromptGuidelines() {
|
|
|
423
404
|
return [
|
|
424
405
|
"Use slack_send for replies in the current Slack assistant thread; always reply where the task came from.",
|
|
425
406
|
"For rich Block Kit JSON examples or modal/canvas patterns, load the slack-bridge skill instead of relying on tool schemas.",
|
|
407
|
+
"If slack_send fails with 'broker is unavailable' or 'already owned by another agent', do NOT retry via post_channel or a different tool \u2014 report the blocker in the same thread (or to the broker) and wait for ownership to be transferred; direct posting would bypass thread ownership (#855).",
|
|
426
408
|
];
|
|
427
409
|
}
|
|
428
410
|
function getSlackCanvasSummary(markdown) {
|
|
@@ -624,32 +606,31 @@ export function registerSlackTools(pi, deps) {
|
|
|
624
606
|
}
|
|
625
607
|
async function deliverSlackMessage(input) {
|
|
626
608
|
const blocks = input.blocks ? normalizeSlackBlocksInput(input.blocks) : undefined;
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
609
|
+
// Threaded Slack replies routed through Pinet MUST NOT fall back to direct
|
|
610
|
+
// Slack when the broker is unavailable or the delivery errors: direct
|
|
611
|
+
// Slack posting bypasses broker thread-ownership enforcement and lets any
|
|
612
|
+
// worker with a valid bot token take over a Slack thread it does not own.
|
|
613
|
+
// See gugu91/extensions#855. Single-player mode (isEnabled === false) and
|
|
614
|
+
// top-level channel posts (no threadTs) remain unaffected.
|
|
615
|
+
if (input.threadTs && pinetDelivery?.isEnabled()) {
|
|
616
|
+
if (!pinetDelivery.isAvailable()) {
|
|
617
|
+
throw new Error("Cannot post to Slack thread: Pinet broker is unavailable and direct Slack fallback would bypass thread-ownership enforcement. Wait for the broker to reconnect, or ask the broker to transfer this thread.");
|
|
618
|
+
}
|
|
619
|
+
const result = await pinetDelivery.sendSlackMessage({
|
|
620
|
+
threadId: input.threadTs,
|
|
621
|
+
channel: input.channel,
|
|
622
|
+
text: input.text,
|
|
623
|
+
...(blocks ? { blocks } : {}),
|
|
624
|
+
...(input.files ? { files: input.files } : {}),
|
|
625
|
+
});
|
|
626
|
+
return {
|
|
627
|
+
threadTs: input.threadTs,
|
|
628
|
+
channel: result.channel,
|
|
629
|
+
blocksCount: blocks?.length ?? 0,
|
|
630
|
+
delivery: "pinet",
|
|
631
|
+
adapter: result.adapter,
|
|
632
|
+
messageId: result.messageId,
|
|
633
|
+
};
|
|
653
634
|
}
|
|
654
635
|
if (input.files && input.files.length > 0) {
|
|
655
636
|
if (blocks && blocks.length > 0) {
|
|
@@ -676,7 +657,6 @@ export function registerSlackTools(pi, deps) {
|
|
|
676
657
|
channel: input.channel,
|
|
677
658
|
blocksCount: blocks?.length ?? 0,
|
|
678
659
|
delivery: "slack",
|
|
679
|
-
...(fallbackReason ? { fallbackReason } : {}),
|
|
680
660
|
};
|
|
681
661
|
}
|
|
682
662
|
const body = {
|
|
@@ -700,7 +680,6 @@ export function registerSlackTools(pi, deps) {
|
|
|
700
680
|
channel: input.channel,
|
|
701
681
|
blocksCount: blocks?.length ?? 0,
|
|
702
682
|
delivery: "slack",
|
|
703
|
-
...(fallbackReason ? { fallbackReason } : {}),
|
|
704
683
|
};
|
|
705
684
|
}
|
|
706
685
|
const slackActionRegistry = new Map();
|
|
@@ -1479,7 +1458,6 @@ export function registerSlackTools(pi, deps) {
|
|
|
1479
1458
|
filesCount: Array.isArray(params.files) ? params.files.length : 0,
|
|
1480
1459
|
...(delivery.adapter ? { adapter: delivery.adapter } : {}),
|
|
1481
1460
|
...(delivery.messageId ? { messageId: delivery.messageId } : {}),
|
|
1482
|
-
...(delivery.fallbackReason ? { fallbackReason: delivery.fallbackReason } : {}),
|
|
1483
1461
|
},
|
|
1484
1462
|
};
|
|
1485
1463
|
},
|
|
@@ -2194,7 +2172,6 @@ export function registerSlackTools(pi, deps) {
|
|
|
2194
2172
|
filesCount: Array.isArray(params.files) ? params.files.length : 0,
|
|
2195
2173
|
...(delivery.adapter ? { adapter: delivery.adapter } : {}),
|
|
2196
2174
|
...(delivery.messageId ? { messageId: delivery.messageId } : {}),
|
|
2197
|
-
...(delivery.fallbackReason ? { fallbackReason: delivery.fallbackReason } : {}),
|
|
2198
2175
|
},
|
|
2199
2176
|
};
|
|
2200
2177
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AgentSessionSummary } from "./broker/types.js";
|
|
2
3
|
import type { PinetReadOptions, PinetReadResult } from "@pinet/pinet-core/pinet-read-formatting";
|
|
3
4
|
import { type InboxMessage, type PinetControlCommand, type PinetRemoteControlRequestResult, type SlackBridgeSettings } from "./helpers.js";
|
|
4
5
|
export interface SubtreeBrokerPaths {
|
|
@@ -32,6 +33,8 @@ export interface SubtreeAgentRecord {
|
|
|
32
33
|
name: string;
|
|
33
34
|
id: string;
|
|
34
35
|
pid?: number;
|
|
36
|
+
stableId?: string | null;
|
|
37
|
+
session?: AgentSessionSummary | null;
|
|
35
38
|
status: "working" | "idle";
|
|
36
39
|
metadata: Record<string, unknown> | null;
|
|
37
40
|
lastHeartbeat: string;
|
|
@@ -4,6 +4,7 @@ import * as os from "node:os";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
+
import { summarizePinetStableId } from "./pinet-session-formatting.js";
|
|
7
8
|
import { dispatchDirectAgentMessage, resolveDirectAgentTarget } from "./broker/agent-messaging.js";
|
|
8
9
|
import { startBroker } from "./broker/index.js";
|
|
9
10
|
import { HEARTBEAT_INTERVAL_MS } from "./broker/client.js";
|
|
@@ -174,6 +175,8 @@ function toSubtreeAgentRecord(db, agent) {
|
|
|
174
175
|
name: agent.name,
|
|
175
176
|
id: agent.id,
|
|
176
177
|
pid: agent.pid,
|
|
178
|
+
stableId: agent.stableId ?? null,
|
|
179
|
+
session: summarizePinetStableId(agent.stableId),
|
|
177
180
|
status: agent.status,
|
|
178
181
|
metadata: agent.metadata,
|
|
179
182
|
lastHeartbeat: agent.lastHeartbeat,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pinet/slack-bridge",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi package for Pinet Slack assistant integration — multi-agent broker, thread routing, and inbox tools",
|
|
6
6
|
"author": "Will Porcellini <5994936+gugu91@users.noreply.github.com>",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"test": "vitest run"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@pinet/broker-core": "0.2.
|
|
53
|
-
"@pinet/imessage-bridge": "0.2.
|
|
54
|
-
"@pinet/pinet-core": "0.2.
|
|
55
|
-
"@pinet/transport-core": "0.2.
|
|
52
|
+
"@pinet/broker-core": "0.2.4",
|
|
53
|
+
"@pinet/imessage-bridge": "0.2.4",
|
|
54
|
+
"@pinet/pinet-core": "0.2.4",
|
|
55
|
+
"@pinet/transport-core": "0.2.4",
|
|
56
56
|
"@sinclair/typebox": "^0.34.49"
|
|
57
57
|
},
|
|
58
58
|
"types": "./dist/index.d.ts",
|