@pinet/slack-bridge 0.2.0 → 0.2.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/README.md +43 -29
- 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 +1 -0
- package/dist/broker/socket-server.js +36 -2
- package/dist/helpers.d.ts +17 -0
- package/dist/helpers.js +12 -1
- package/dist/index.js +2 -1
- package/dist/pinet-mesh-ops.d.ts +6 -1
- package/dist/pinet-mesh-ops.js +19 -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 +158 -5
- 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/subtree-broker-runtime.d.ts +3 -0
- package/dist/subtree-broker-runtime.js +3 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -160,6 +160,15 @@ Behavior and precedence:
|
|
|
160
160
|
"appToken": "xapp-...",
|
|
161
161
|
"runtimeMode": "single",
|
|
162
162
|
"allowedUsers": ["U_EXAMPLE_MEMBER_ID"],
|
|
163
|
+
"ingressGuard": {
|
|
164
|
+
"requireMention": {
|
|
165
|
+
"channels": ["C_EXTERNAL_CHANNEL_ID"],
|
|
166
|
+
"mixedParticipantThreads": {
|
|
167
|
+
"enabled": true,
|
|
168
|
+
"trustedUsers": ["U_EXAMPLE_MEMBER_ID"]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
163
172
|
"defaultChannel": "C_EXAMPLE_CHANNEL_ID",
|
|
164
173
|
"logChannel": "#pinet-logs",
|
|
165
174
|
"logLevel": "actions",
|
|
@@ -183,30 +192,33 @@ Slack access is now **default-deny** unless you configure one of these explicitl
|
|
|
183
192
|
- `allowedUsers` / `SLACK_ALLOWED_USERS` — allow only specific Slack user IDs
|
|
184
193
|
- `allowAllWorkspaceUsers: true` / `SLACK_ALLOW_ALL_WORKSPACE_USERS=true` — explicit workspace-wide opt-in
|
|
185
194
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
|
189
|
-
|
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
208
|
-
| `
|
|
209
|
-
| `
|
|
195
|
+
Optional explicit invocation guard: `ingressGuard.requireMention` is off by default. Set `channels` to Slack channel IDs where otherwise-actionable messages must mention the bot (`@pinet` / `<@bot>`), and set `mixedParticipantThreads.enabled: true` with `trustedUsers` to require a mention in Pinet-owned Slack threads once anyone outside `trustedUsers` plus the bot has participated. This guard is orthogonal to `allowedUsers`: sender authorization still decides who may invoke Pinet; the guard only decides when an explicit mention is required.
|
|
196
|
+
|
|
197
|
+
| Key | Required | Description |
|
|
198
|
+
| ------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
199
|
+
| `botToken` | **yes** | Bot User OAuth Token (`xoxb-...`) |
|
|
200
|
+
| `appToken` | **yes** | App-Level Token for Socket Mode (`xapp-...`) |
|
|
201
|
+
| `allowedUsers` | no | Slack user IDs that can interact; when unset, access is denied unless `allowAllWorkspaceUsers` is true |
|
|
202
|
+
| `allowAllWorkspaceUsers` | no | Explicit opt-in for workspace-wide Slack access when you do not want a user allowlist |
|
|
203
|
+
| `ingressGuard.requireMention` | no | Optional Slack ingress guard requiring `@pinet` in configured channel IDs and/or mixed-participant Pinet-owned threads |
|
|
204
|
+
| `defaultChannel` | no | Default channel for the `slack` dispatcher `post_channel` action |
|
|
205
|
+
| `logChannel` | no | Channel for broker activity logs |
|
|
206
|
+
| `logLevel` | no | `"errors"`, `"actions"` (default), or `"verbose"` |
|
|
207
|
+
| `runtimeMode` | no | Explicit startup mode: `"off"`, `"single"`, `"broker"`, or `"follower"` |
|
|
208
|
+
| `autoConnect` | no | Legacy compatibility alias for `runtimeMode: "single"` |
|
|
209
|
+
| `autoFollow` | no | Legacy compatibility alias for follower startup when a broker socket exists |
|
|
210
|
+
| `ralphLoopIntervalMs` | no | Broker RALPH maintenance cadence in milliseconds; defaults to `300000` (5 minutes), valid range `1000`-`2147483647` |
|
|
211
|
+
| `ralphSnoozeAfterEmptyCycles` | no | Broker RALPH auto-snooze trigger after N empty cycles; defaults to `0` (disabled), valid range `0`-`100` |
|
|
212
|
+
| `ralphSnoozeDurationMs` | no | Broker RALPH auto-snooze duration in milliseconds; defaults to `1800000` (30 minutes), valid range `60000`-`86400000` |
|
|
213
|
+
| `skinTheme` | no | Pinet presentation skin selected at broker startup/reload (`default`, `foundation`, `cosmere`, or free-form) |
|
|
214
|
+
| `slackCommandName` | no | Slack web app slash command name for `agents list`; defaults to `/pinet`, or `/oathgate` for Oathgate/Cosmere skins |
|
|
215
|
+
| `slackCommandNames` | no | Optional list of accepted/deployed Slack slash command aliases when one app needs multiple command names |
|
|
216
|
+
| `meshSecret` | no | Optional inline Pinet shared secret; overrides `meshSecretPath` and env fallbacks |
|
|
217
|
+
| `meshSecretPath` | no | Optional path to a shared-secret file; broker creates it if missing, followers require an existing file |
|
|
218
|
+
| `suggestedPrompts` | no | Prompts shown when a user opens a new conversation |
|
|
219
|
+
| `security.readOnly` | no | Runtime-block write-capable tools for Slack-triggered turns, including core tools like `bash`, `edit`, and `write` |
|
|
220
|
+
| `security.requireConfirmation` | no | Runtime-require Slack approval before matching tools execute; core tools need a specific Slack thread context |
|
|
221
|
+
| `security.blockedTools` | no | Runtime-block matching tools for Slack-triggered turns, including core tools |
|
|
210
222
|
|
|
211
223
|
## Scope carrier model (compatibility-first)
|
|
212
224
|
|
|
@@ -516,17 +528,19 @@ Only broker prompt content is replaceable. Broker runtime/tool restrictions rema
|
|
|
516
528
|
|
|
517
529
|
### Multi-agent tools
|
|
518
530
|
|
|
519
|
-
| Tool | Description
|
|
520
|
-
| ------- |
|
|
521
|
-
| `pinet` | Pinet dispatcher with token-efficient `action`-based routing (`help`, `send`, `read`, `free`, `snooze`, `schedule`, `agents`, `lanes`, `ports`, `spawn`, `reload`, `exit`) |
|
|
531
|
+
| Tool | Description |
|
|
532
|
+
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
533
|
+
| `pinet` | Pinet dispatcher with token-efficient `action`-based routing (`help`, `send`, `read`, `free`, `snooze`, `schedule`, `agents`, `sessions`, `lanes`, `ports`, `spawn`, `reload`, `exit`) |
|
|
522
534
|
|
|
523
|
-
Use the dispatcher for Pinet tool actions: `pinet action=send`, `pinet action=read`, `pinet action=free`, `pinet action=snooze`, `pinet action=schedule`, `pinet action=agents`, `pinet action=lanes`, `pinet action=ports`, `pinet action=spawn`, `pinet action=reload`, and `pinet action=exit`. Use slash commands for UI lifecycle transitions: `/pinet start`, `/pinet follow`, `/pinet unfollow`, and `/pinet subtree start`. Dedicated direct Pinet tools (`pinet_message`, `pinet_read`, `pinet_agents`, `pinet_free`, `pinet_schedule`) are no longer registered. Legacy `pinet_*` guardrail patterns still match dispatcher action names, and legacy send policies such as `pinet_send` or `pinet_message` also cover `pinet action=send`, so existing security configs fail closed during migration.
|
|
535
|
+
Use the dispatcher for Pinet tool actions: `pinet action=send`, `pinet action=read`, `pinet action=free`, `pinet action=snooze`, `pinet action=schedule`, `pinet action=agents`, `pinet action=sessions`, `pinet action=lanes`, `pinet action=ports`, `pinet action=spawn`, `pinet action=reload`, and `pinet action=exit`. Use slash commands for UI lifecycle transitions: `/pinet start`, `/pinet follow`, `/pinet unfollow`, and `/pinet subtree start`. Dedicated direct Pinet tools (`pinet_message`, `pinet_read`, `pinet_agents`, `pinet_free`, `pinet_schedule`) are no longer registered. Legacy `pinet_*` guardrail patterns still match dispatcher action names, and legacy send policies such as `pinet_send` or `pinet_message` also cover `pinet action=send`, so existing security configs fail closed during migration.
|
|
524
536
|
|
|
525
537
|
Worker-owned subtree brokers let a follower worker supervise its own child mesh without registering those children in the central broker. Run `/pinet subtree start` (alias: `/pinet subbroker start`) from a follower worker. The worker remains connected to the central broker as a normal worker, and it also starts a separate broker socket/database under `~/.pi/pinet-subtrees/<worker>/`. Child workers launched by this worker receive `PINET_SOCKET_PATH`, `PINET_PARENT_AGENT_ID`, `PINET_ROOT_AGENT_ID`, `PINET_LAUNCH_ID`, `PINET_SUBTREE_ROLE`, and related metadata, so they follow the worker's subtree broker instead of the central Pinet broker.
|
|
526
538
|
|
|
527
539
|
Use `pinet action=spawn args.repo=<repo> args.task=<task> [args.role=<role>] [args.lane_id=<lane>]` or `/pinet subtree spawn repo=<repo> [role=<role>] [lane=<lane>] <task>` to launch a tmux-backed child worker, wait for it to register in the subtree broker, and deliver the task over private Pinet A2A. Use `pinet action=agents args.scope=subtree args.full=true` from the supervising worker to list subtree children, `pinet action=send args.to=<child> args.message=<message>` to reply/control them, `pinet action=read` to read child reports, and `pinet action=exit args.target=<child>` or `/pinet subtree stop` to clean them up. The central broker sees only the supervising worker; the subtree DB contains the child roster and messages.
|
|
528
540
|
|
|
529
|
-
Dispatcher content defaults to terse CLI-style confirmations/summaries for noisy reads, sends, and
|
|
541
|
+
Dispatcher content defaults to terse CLI-style confirmations/summaries for noisy reads, sends, agent lists, and session lookups. Bulky read/agent/session payloads are compacted in `data.details` by default, including when `args.format="json"` (or `args.f` / `args["-f"]`) renders the dispatcher envelope in content. Use `args.full=true` / `args["--full"]=true` only when you need verbose text and full structured debug details such as exact message bodies, agent metadata, stable session IDs, or local session JSONL paths.
|
|
542
|
+
|
|
543
|
+
`pinet action=agents` shows a broker-safe session reference (`session:<digest>`) alongside pid in verbose roster output without exposing raw local paths by default. Use `pinet action=sessions args.agent_name="Frozen Hazel Whale"` to search live and historical worker sessions by display name; the search also accepts `agent_id`, `thread_id`, `repo`, `worktree_path`, `tmux_session`, `since`, `until`, and `limit`. Default output redacts path-bearing stable IDs; add `args.full=true` (or JSON plus `full`) only in local/debug contexts when the broker needs the exact stable ID or Pi session JSONL path for inspection/resume.
|
|
530
544
|
|
|
531
545
|
Durable Pinet inbox notifications are classified as `steering`, `fwup`, or `maintenance/context` from explicit metadata or message cues. Follower prompts receive compact pointers such as `pinet action=read args.thread_id=...` instead of the full durable message body; agents use `pinet action=read` to retrieve the actual context. Delivery, read/ack state, and mail classification remain separate.
|
|
532
546
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ParsedAppHomeOpened, type ParsedSlashCommand, type ParsedThreadStarted } from "../../slack-access.js";
|
|
2
|
+
import { type SlackIngressGuardSettings } from "../../helpers.js";
|
|
2
3
|
import { type ReactionCommandSettings } from "../../reaction-triggers.js";
|
|
3
4
|
import type { AdapterCapabilityRequest, AdapterCapabilityResult, InboundMessage, OutboundMessage, MessageAdapter } from "./types.js";
|
|
4
5
|
export { classifyMessage, extractAppHomeOpened, extractThreadStarted, parseMemberJoinedChannel, parseSocketFrame, RECONNECT_DELAY_MS, } from "../../slack-access.js";
|
|
@@ -7,6 +8,7 @@ export interface SlackAdapterConfig {
|
|
|
7
8
|
appToken: string;
|
|
8
9
|
allowedUsers?: string[];
|
|
9
10
|
allowAllWorkspaceUsers?: boolean;
|
|
11
|
+
ingressGuard?: SlackIngressGuardSettings;
|
|
10
12
|
suggestedPrompts?: {
|
|
11
13
|
title: string;
|
|
12
14
|
message: string;
|
|
@@ -33,6 +35,8 @@ export interface SlackAdapterConfig {
|
|
|
33
35
|
* Slack messages (#812).
|
|
34
36
|
*/
|
|
35
37
|
isReactionThreadAuthorized?: (threadTs: string, channelId: string) => boolean;
|
|
38
|
+
/** Check whether a known Slack thread is Pinet-owned for mixed-participant mention gating. */
|
|
39
|
+
isPinetOwnedThread?: (threadTs: string, channelId: string) => boolean;
|
|
36
40
|
/** Best-effort callback for Home tab opens. */
|
|
37
41
|
onAppHomeOpened?: (event: ParsedAppHomeOpened) => Promise<void> | void;
|
|
38
42
|
/** Best-effort callback for Slack slash commands handled by the broker process. */
|
|
@@ -43,6 +47,7 @@ export declare const SLACK_THREAD_CACHE_TTL_MS: number;
|
|
|
43
47
|
export declare const SLACK_PENDING_ATTENTION_MAX_THREADS = 1000;
|
|
44
48
|
export declare const SLACK_PENDING_ATTENTION_TTL_MS: number;
|
|
45
49
|
export declare const SLACK_PENDING_ATTENTION_MAX_MESSAGES_PER_THREAD = 50;
|
|
50
|
+
export declare const SLACK_INGRESS_GUARD_THREAD_PARTICIPANT_LIMIT = 200;
|
|
46
51
|
export declare class SlackAdapter implements MessageAdapter {
|
|
47
52
|
readonly name = "slack";
|
|
48
53
|
private readonly config;
|
|
@@ -79,6 +84,12 @@ export declare class SlackAdapter implements MessageAdapter {
|
|
|
79
84
|
private onReactionAdded;
|
|
80
85
|
private getCachedThread;
|
|
81
86
|
private isReactionThreadAuthorized;
|
|
87
|
+
private messageMentionsBot;
|
|
88
|
+
private requiresMentionInChannel;
|
|
89
|
+
private shouldRequireMentionForMixedParticipantThread;
|
|
90
|
+
private isPinetOwnedThreadForIngressGuard;
|
|
91
|
+
private fetchThreadParticipantUserIds;
|
|
92
|
+
private shouldRequireExplicitMention;
|
|
82
93
|
private onMessage;
|
|
83
94
|
private emitInteractiveInbound;
|
|
84
95
|
private onMemberJoined;
|
|
@@ -5,6 +5,7 @@ import { buildReactionTriggerMessage, normalizeReactionName, resolveReactionComm
|
|
|
5
5
|
import { TtlCache, TtlSet } from "../../ttl-cache.js";
|
|
6
6
|
import { SLACK_SOCKET_DELIVERY_DEDUP_MAX_SIZE, SLACK_SOCKET_DELIVERY_DEDUP_TTL_MS, } from "../../slack-access.js";
|
|
7
7
|
import { DEFAULT_SLACK_THREAD_STATUS, SlackThreadStatusManager, } from "../../slack-thread-status.js";
|
|
8
|
+
import { renderMarkdownForSlackMrkdwn } from "../../slack-markdown.js";
|
|
8
9
|
import { performSlackUploads, prepareSlackUpload } from "../../slack-upload.js";
|
|
9
10
|
export { classifyMessage, extractAppHomeOpened, extractThreadStarted, parseMemberJoinedChannel, parseSocketFrame, RECONNECT_DELAY_MS, } from "../../slack-access.js";
|
|
10
11
|
export const SLACK_THREAD_CACHE_MAX_SIZE = 5000;
|
|
@@ -12,6 +13,7 @@ export const SLACK_THREAD_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
|
12
13
|
export const SLACK_PENDING_ATTENTION_MAX_THREADS = 1000;
|
|
13
14
|
export const SLACK_PENDING_ATTENTION_TTL_MS = 2 * 60 * 60 * 1000;
|
|
14
15
|
export const SLACK_PENDING_ATTENTION_MAX_MESSAGES_PER_THREAD = 50;
|
|
16
|
+
export const SLACK_INGRESS_GUARD_THREAD_PARTICIPANT_LIMIT = 200;
|
|
15
17
|
export class SlackAdapter {
|
|
16
18
|
name = "slack";
|
|
17
19
|
config;
|
|
@@ -124,9 +126,10 @@ export class SlackAdapter {
|
|
|
124
126
|
: msg.blocks && msg.blocks.length > 0
|
|
125
127
|
? msg.blocks
|
|
126
128
|
: undefined;
|
|
129
|
+
const renderedText = renderMarkdownForSlackMrkdwn(msg.content?.markdown ?? msg.content?.text ?? msg.text);
|
|
127
130
|
const body = {
|
|
128
131
|
channel: msg.channel,
|
|
129
|
-
text:
|
|
132
|
+
text: renderedText,
|
|
130
133
|
thread_ts: msg.threadId,
|
|
131
134
|
...(slackBlocks ? { blocks: slackBlocks } : {}),
|
|
132
135
|
};
|
|
@@ -157,7 +160,7 @@ export class SlackAdapter {
|
|
|
157
160
|
uploads,
|
|
158
161
|
channelId: msg.channel,
|
|
159
162
|
threadTs: msg.threadId,
|
|
160
|
-
initialComment:
|
|
163
|
+
initialComment: renderedText,
|
|
161
164
|
slack: this.callSlack.bind(this),
|
|
162
165
|
token: this.config.botToken,
|
|
163
166
|
});
|
|
@@ -464,6 +467,87 @@ export class SlackAdapter {
|
|
|
464
467
|
return false;
|
|
465
468
|
}
|
|
466
469
|
}
|
|
470
|
+
messageMentionsBot(evt) {
|
|
471
|
+
if (!this.botUserId)
|
|
472
|
+
return false;
|
|
473
|
+
const text = typeof evt.text === "string" ? evt.text : "";
|
|
474
|
+
return text.includes(`<@${this.botUserId}>`) || text.includes(`<@${this.botUserId}|`);
|
|
475
|
+
}
|
|
476
|
+
requiresMentionInChannel(channelId) {
|
|
477
|
+
const channels = this.config.ingressGuard?.requireMention?.channels ?? [];
|
|
478
|
+
return channels.some((channel) => channel.trim() === channelId);
|
|
479
|
+
}
|
|
480
|
+
async shouldRequireMentionForMixedParticipantThread(input) {
|
|
481
|
+
const config = this.config.ingressGuard?.requireMention?.mixedParticipantThreads;
|
|
482
|
+
if (!config?.enabled || input.isDM || typeof input.evt.thread_ts !== "string") {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
if (!this.isPinetOwnedThreadForIngressGuard(input.threadTs, input.channel)) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
const participants = await this.fetchThreadParticipantUserIds(input.channel, input.threadTs, input.userId);
|
|
489
|
+
if (!participants) {
|
|
490
|
+
// Fail closed for this guard: if we cannot inspect the thread makeup,
|
|
491
|
+
// require an explicit mention before Pinet acts in a configured mixed-thread posture.
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
const trustedUsers = new Set((config.trustedUsers ?? []).map((user) => user.trim()).filter(Boolean));
|
|
495
|
+
for (const participant of participants) {
|
|
496
|
+
if (this.botUserId && participant === this.botUserId)
|
|
497
|
+
continue;
|
|
498
|
+
if (trustedUsers.has(participant))
|
|
499
|
+
continue;
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
isPinetOwnedThreadForIngressGuard(threadTs, channelId) {
|
|
505
|
+
const isOwned = this.config.isPinetOwnedThread;
|
|
506
|
+
if (isOwned) {
|
|
507
|
+
try {
|
|
508
|
+
return isOwned(threadTs, channelId);
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
console.error(`[slack-adapter] Pinet-owned thread check failed: ${errorMsg(error)}`);
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const thread = this.getThread(threadTs);
|
|
516
|
+
return !!thread && thread.channelId === channelId;
|
|
517
|
+
}
|
|
518
|
+
async fetchThreadParticipantUserIds(channelId, threadTs, currentUserId) {
|
|
519
|
+
const participants = new Set();
|
|
520
|
+
if (currentUserId)
|
|
521
|
+
participants.add(currentUserId);
|
|
522
|
+
try {
|
|
523
|
+
const response = await this.callSlack("conversations.replies", this.config.botToken, {
|
|
524
|
+
channel: channelId,
|
|
525
|
+
ts: threadTs,
|
|
526
|
+
limit: SLACK_INGRESS_GUARD_THREAD_PARTICIPANT_LIMIT,
|
|
527
|
+
});
|
|
528
|
+
const messages = Array.isArray(response.messages) ? response.messages : [];
|
|
529
|
+
for (const message of messages) {
|
|
530
|
+
if (!message || typeof message !== "object" || Array.isArray(message))
|
|
531
|
+
continue;
|
|
532
|
+
const userId = message.user;
|
|
533
|
+
if (typeof userId === "string" && userId.length > 0) {
|
|
534
|
+
participants.add(userId);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return participants;
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
console.error(`[slack-adapter] failed to inspect Slack thread participants: ${errorMsg(error)}`);
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async shouldRequireExplicitMention(input) {
|
|
545
|
+
if (!this.config.ingressGuard?.requireMention)
|
|
546
|
+
return false;
|
|
547
|
+
if (this.requiresMentionInChannel(input.channel))
|
|
548
|
+
return true;
|
|
549
|
+
return this.shouldRequireMentionForMixedParticipantThread(input);
|
|
550
|
+
}
|
|
467
551
|
async onMessage(evt) {
|
|
468
552
|
if (this.shuttingDown)
|
|
469
553
|
return;
|
|
@@ -480,6 +564,10 @@ export class SlackAdapter {
|
|
|
480
564
|
// traffic can never mint known-thread/affinity side effects (#812).
|
|
481
565
|
if (!isSlackUserAllowed(this.allowlist, userId))
|
|
482
566
|
return;
|
|
567
|
+
if (!this.messageMentionsBot(evt) &&
|
|
568
|
+
(await this.shouldRequireExplicitMention({ evt, channel, threadTs, userId, isDM }))) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
483
571
|
if (!this.getThread(threadTs)) {
|
|
484
572
|
this.threads.set(threadTs, {
|
|
485
573
|
channelId: channel,
|
package/dist/broker/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PinetReadOptions, PinetReadResult } from "@pinet/pinet-core/pinet-read-formatting";
|
|
2
|
-
import type { ClientAgentInfo, NormalizedMessageContent, OutboundAttachmentFile, PortLeaseAcquireInput, PortLeaseInfo, PortLeaseListOptions, PortLeaseReleaseInput, PortLeaseRenewInput, PinetLaneInfo, PinetLaneListOptions, PinetLaneParticipantInfo, PinetLaneParticipantUpsertInput, PinetLaneUpsertInput } from "./types.js";
|
|
2
|
+
import type { AgentSessionSearchInfo, AgentSessionSearchOptions, ClientAgentInfo, NormalizedMessageContent, OutboundAttachmentFile, PortLeaseAcquireInput, PortLeaseInfo, PortLeaseListOptions, PortLeaseReleaseInput, PortLeaseRenewInput, PinetLaneInfo, PinetLaneListOptions, PinetLaneParticipantInfo, PinetLaneParticipantUpsertInput, PinetLaneUpsertInput } from "./types.js";
|
|
3
3
|
export interface InboxItem {
|
|
4
4
|
inboxId: number;
|
|
5
5
|
message: {
|
|
@@ -25,6 +25,7 @@ export interface ThreadInfo {
|
|
|
25
25
|
updatedAt: string;
|
|
26
26
|
}
|
|
27
27
|
export type AgentInfo = ClientAgentInfo;
|
|
28
|
+
export type { AgentSessionSearchInfo, AgentSessionSearchOptions };
|
|
28
29
|
export interface ScheduledWakeupInfo {
|
|
29
30
|
id: number;
|
|
30
31
|
threadId: string;
|
|
@@ -136,6 +137,7 @@ export declare class BrokerClient {
|
|
|
136
137
|
expirePortLeases(): Promise<PortLeaseInfo[]>;
|
|
137
138
|
listThreads(): Promise<ThreadInfo[]>;
|
|
138
139
|
listAgents(includeDisconnected?: boolean): Promise<AgentInfo[]>;
|
|
140
|
+
searchAgentSessions(options?: AgentSessionSearchOptions): Promise<AgentSessionSearchInfo[]>;
|
|
139
141
|
invokeAdapterCapability(adapter: string, capability: string, params?: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
140
142
|
/**
|
|
141
143
|
* Compatibility wrapper for callers that still need direct Slack API access.
|
package/dist/broker/client.js
CHANGED
|
@@ -366,6 +366,17 @@ export class BrokerClient {
|
|
|
366
366
|
const result = (await this.request("agents.list", includeDisconnected ? { includeDisconnected: true } : undefined));
|
|
367
367
|
return result;
|
|
368
368
|
}
|
|
369
|
+
async searchAgentSessions(options = {}) {
|
|
370
|
+
try {
|
|
371
|
+
return (await this.request("agent.sessions.search", options));
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
if (isRpcMethodNotFoundError(err, "agent.sessions.search")) {
|
|
375
|
+
throw new Error("Broker does not support Pinet session search (`agent.sessions.search`). Upgrade the broker before using pinet action=sessions.");
|
|
376
|
+
}
|
|
377
|
+
throw err;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
369
380
|
// ─── Adapter capabilities ───────────────────────────
|
|
370
381
|
async invokeAdapterCapability(adapter, capability, params = {}) {
|
|
371
382
|
const result = (await this.request("adapter.capability", {
|
|
@@ -98,6 +98,7 @@ export declare class BrokerSocketServer {
|
|
|
98
98
|
private handleMessageSend;
|
|
99
99
|
private handleThreadsList;
|
|
100
100
|
private handleAgentsList;
|
|
101
|
+
private handleAgentSessionsSearch;
|
|
101
102
|
private handleThreadClaim;
|
|
102
103
|
private handleResolveThread;
|
|
103
104
|
private handleAgentMessage;
|
|
@@ -7,14 +7,17 @@ import { MessageRouter } from "./router.js";
|
|
|
7
7
|
import { dispatchDirectAgentMessage } from "./agent-messaging.js";
|
|
8
8
|
import { sendBrokerMessage } from "./message-send.js";
|
|
9
9
|
import { assertLoopbackTcpHost } from "./raw-tcp-loopback.js";
|
|
10
|
+
import { summarizePinetStableId } from "../pinet-session-formatting.js";
|
|
10
11
|
import { RPC_PARSE_ERROR, RPC_INVALID_REQUEST, RPC_METHOD_NOT_FOUND, RPC_INVALID_PARAMS, RPC_INTERNAL_ERROR, RPC_AUTH_REQUIRED, RPC_AGENT_NAME_CONFLICT, RPC_AGENT_STABLE_ID_CONFLICT, } from "./types.js";
|
|
11
12
|
export const DEFAULT_HEARTBEAT_TIMEOUT_MS = 15_000;
|
|
12
13
|
export const DEFAULT_PRUNE_INTERVAL_MS = 5_000;
|
|
13
14
|
export const DEFAULT_AUTH_TIMEOUT_MS = 2_000;
|
|
14
15
|
function toClientAgentInfo(agent) {
|
|
15
16
|
const { stableId, ...clientAgent } = agent;
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
return {
|
|
18
|
+
...clientAgent,
|
|
19
|
+
session: summarizePinetStableId(stableId),
|
|
20
|
+
};
|
|
18
21
|
}
|
|
19
22
|
// ─── RPC helpers ─────────────────────────────────────────
|
|
20
23
|
function rpcOk(id, result) {
|
|
@@ -351,6 +354,8 @@ export class BrokerSocketServer {
|
|
|
351
354
|
return this.handleThreadsList(req, state);
|
|
352
355
|
case "agents.list":
|
|
353
356
|
return this.handleAgentsList(req);
|
|
357
|
+
case "agent.sessions.search":
|
|
358
|
+
return this.handleAgentSessionsSearch(req, state);
|
|
354
359
|
case "thread.claim":
|
|
355
360
|
return this.handleThreadClaim(req, state);
|
|
356
361
|
case "resolveThread":
|
|
@@ -686,6 +691,35 @@ export class BrokerSocketServer {
|
|
|
686
691
|
}));
|
|
687
692
|
return rpcOk(req.id, agents);
|
|
688
693
|
}
|
|
694
|
+
handleAgentSessionsSearch(req, state) {
|
|
695
|
+
if (!state.agentId) {
|
|
696
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
697
|
+
}
|
|
698
|
+
const caller = this.db.getAgentById(state.agentId);
|
|
699
|
+
const metadataRole = typeof caller?.metadata?.role === "string" ? caller.metadata.role.trim().toLowerCase() : "";
|
|
700
|
+
const capabilities = caller?.metadata?.capabilities &&
|
|
701
|
+
typeof caller.metadata.capabilities === "object" &&
|
|
702
|
+
!Array.isArray(caller.metadata.capabilities)
|
|
703
|
+
? caller.metadata.capabilities
|
|
704
|
+
: null;
|
|
705
|
+
const capabilityRole = typeof capabilities?.role === "string" ? capabilities.role.trim().toLowerCase() : "";
|
|
706
|
+
if (metadataRole !== "broker" && capabilityRole !== "broker") {
|
|
707
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "agent.sessions.search requires a broker agent");
|
|
708
|
+
}
|
|
709
|
+
const params = req.params ?? {};
|
|
710
|
+
const options = {
|
|
711
|
+
...(typeof params.agentName === "string" ? { agentName: params.agentName } : {}),
|
|
712
|
+
...(typeof params.agentId === "string" ? { agentId: params.agentId } : {}),
|
|
713
|
+
...(typeof params.threadId === "string" ? { threadId: params.threadId } : {}),
|
|
714
|
+
...(typeof params.repo === "string" ? { repo: params.repo } : {}),
|
|
715
|
+
...(typeof params.worktreePath === "string" ? { worktreePath: params.worktreePath } : {}),
|
|
716
|
+
...(typeof params.tmuxSession === "string" ? { tmuxSession: params.tmuxSession } : {}),
|
|
717
|
+
...(typeof params.since === "string" ? { since: params.since } : {}),
|
|
718
|
+
...(typeof params.until === "string" ? { until: params.until } : {}),
|
|
719
|
+
...(typeof params.limit === "number" ? { limit: params.limit } : {}),
|
|
720
|
+
};
|
|
721
|
+
return rpcOk(req.id, this.db.searchAgentSessions(options));
|
|
722
|
+
}
|
|
689
723
|
// ─── Thread claim handler ─────────────────────────────
|
|
690
724
|
handleThreadClaim(req, state) {
|
|
691
725
|
if (!state.agentId) {
|
package/dist/helpers.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { type RuntimeScopeCarrier } from "@pinet/transport-core";
|
|
2
2
|
import type { ReactionCommandSettings } from "./reaction-triggers.js";
|
|
3
|
+
import type { AgentSessionSummary } from "./broker/types.js";
|
|
4
|
+
export interface SlackIngressGuardSettings {
|
|
5
|
+
requireMention?: {
|
|
6
|
+
/** Slack channel IDs where every actionable message must explicitly mention the bot. */
|
|
7
|
+
channels?: string[];
|
|
8
|
+
/** Require mention in Pinet-owned threads once anyone outside trustedUsers + the bot participates. */
|
|
9
|
+
mixedParticipantThreads?: {
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
trustedUsers?: string[];
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
}
|
|
3
15
|
export interface SlackBridgeSettings {
|
|
4
16
|
botToken?: string;
|
|
5
17
|
appToken?: string;
|
|
@@ -7,6 +19,7 @@ export interface SlackBridgeSettings {
|
|
|
7
19
|
appConfigToken?: string;
|
|
8
20
|
allowedUsers?: string[];
|
|
9
21
|
allowAllWorkspaceUsers?: boolean;
|
|
22
|
+
ingressGuard?: SlackIngressGuardSettings;
|
|
10
23
|
defaultChannel?: string;
|
|
11
24
|
logChannel?: string;
|
|
12
25
|
logLevel?: "errors" | "actions" | "verbose";
|
|
@@ -157,6 +170,8 @@ export interface AgentDisplayInfo {
|
|
|
157
170
|
name: string;
|
|
158
171
|
id: string;
|
|
159
172
|
pid?: number;
|
|
173
|
+
stableId?: string | null;
|
|
174
|
+
session?: AgentSessionSummary | null;
|
|
160
175
|
status: "working" | "idle";
|
|
161
176
|
metadata?: {
|
|
162
177
|
cwd?: string;
|
|
@@ -210,6 +225,8 @@ export interface AgentVisibilityInput {
|
|
|
210
225
|
name: string;
|
|
211
226
|
id: string;
|
|
212
227
|
pid?: number;
|
|
228
|
+
stableId?: string | null;
|
|
229
|
+
session?: AgentSessionSummary | null;
|
|
213
230
|
status: "working" | "idle";
|
|
214
231
|
metadata?: Record<string, unknown> | null;
|
|
215
232
|
lastHeartbeat?: string;
|
package/dist/helpers.js
CHANGED
|
@@ -5,6 +5,7 @@ import { classifyPinetMail, formatPinetMailClassLabel, } from "@pinet/broker-cor
|
|
|
5
5
|
import { buildCompatibilityInstanceScope, buildCompatibilityWorkspaceScope, buildRuntimeScopeCarrier, } from "@pinet/transport-core";
|
|
6
6
|
import { buildPinetReadPointer } from "./broker-inbound-persistence.js";
|
|
7
7
|
import { matchesToolPattern } from "./guardrails.js";
|
|
8
|
+
import { getPinetSessionPath, summarizePinetStableId } from "./pinet-session-formatting.js";
|
|
8
9
|
function normalizeOptionalSetting(value) {
|
|
9
10
|
const trimmed = value?.trim();
|
|
10
11
|
return trimmed && trimmed.length > 0 ? trimmed : null;
|
|
@@ -855,6 +856,8 @@ export function buildAgentDisplayInfo(agent, options = {}) {
|
|
|
855
856
|
name: agent.name,
|
|
856
857
|
id: agent.id,
|
|
857
858
|
...(agent.pid != null ? { pid: agent.pid } : {}),
|
|
859
|
+
...(agent.stableId !== undefined ? { stableId: agent.stableId } : {}),
|
|
860
|
+
session: agent.session ?? summarizePinetStableId(agent.stableId),
|
|
858
861
|
status: agent.status,
|
|
859
862
|
metadata: metadata || hasHierarchyMetadata
|
|
860
863
|
? {
|
|
@@ -1733,7 +1736,15 @@ export function formatAgentList(agents, homedir) {
|
|
|
1733
1736
|
: "";
|
|
1734
1737
|
const stuckTag = a.stuck ? " [stuck]" : "";
|
|
1735
1738
|
const pid = a.pid != null ? ` pid:${a.pid}` : "";
|
|
1736
|
-
|
|
1739
|
+
const sessionRef = a.session?.ref ? ` session:${a.session.ref}` : "";
|
|
1740
|
+
let line = `${a.emoji} ${a.name} (${a.id}) \u2014 ${a.status}${statusFlavor}${health}${stuckTag}${pid}${sessionRef}`;
|
|
1741
|
+
if (a.stableId) {
|
|
1742
|
+
const sessionPath = getPinetSessionPath(a.stableId);
|
|
1743
|
+
line += `\n stable: ${a.stableId}`;
|
|
1744
|
+
if (sessionPath) {
|
|
1745
|
+
line += `\n jsonl: ${sessionPath}`;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1737
1748
|
const meta = a.metadata;
|
|
1738
1749
|
if (meta && (meta.cwd || meta.branch || meta.host || meta.gitProbeFailed)) {
|
|
1739
1750
|
const cwd = meta.cwd ? shortenPath(meta.cwd, homedir) : "";
|
package/dist/index.js
CHANGED
|
@@ -733,7 +733,7 @@ export default function (pi) {
|
|
|
733
733
|
brokerRuntime.logActivity(entry);
|
|
734
734
|
},
|
|
735
735
|
});
|
|
736
|
-
const { sendPinetAgentMessage, sendPinetBroadcastMessage, scheduleBrokerWakeup, scheduleFollowerWakeup, listBrokerAgents, listFollowerAgents, } = pinetMeshOps;
|
|
736
|
+
const { sendPinetAgentMessage, sendPinetBroadcastMessage, scheduleBrokerWakeup, scheduleFollowerWakeup, listBrokerAgents, listFollowerAgents, searchPinetSessions, } = pinetMeshOps;
|
|
737
737
|
async function listPinetLanes(options) {
|
|
738
738
|
if (brokerRole === "broker") {
|
|
739
739
|
const db = getActiveBrokerDb();
|
|
@@ -1072,6 +1072,7 @@ export default function (pi) {
|
|
|
1072
1072
|
readPinetInbox,
|
|
1073
1073
|
listBrokerAgents,
|
|
1074
1074
|
listFollowerAgents,
|
|
1075
|
+
searchPinetSessions,
|
|
1075
1076
|
listSubtreeAgents: (includeGhosts) => subtreeBrokerRuntime.listAgents(includeGhosts),
|
|
1076
1077
|
getSubtreeSelfAgentId: () => subtreeBrokerRuntime.getStatus().selfAgentId,
|
|
1077
1078
|
spawnSubtreeWorker: async (input) => {
|
package/dist/pinet-mesh-ops.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { type AgentMessageStorage } from "./broker/agent-messaging.js";
|
|
2
|
-
import type { AgentInfo, TaskAssignmentInfo, TaskAssignmentKind } from "./broker/types.js";
|
|
2
|
+
import type { AgentInfo, AgentSessionSearchInfo, AgentSessionSearchOptions, AgentSessionSummary, TaskAssignmentInfo, TaskAssignmentKind } from "./broker/types.js";
|
|
3
3
|
import type { ActivityLogEntry } from "./activity-log.js";
|
|
4
4
|
export interface PinetMeshOpsAgentRecord {
|
|
5
5
|
emoji: string;
|
|
6
6
|
name: string;
|
|
7
7
|
id: string;
|
|
8
8
|
pid?: number;
|
|
9
|
+
stableId?: string | null;
|
|
10
|
+
session?: AgentSessionSummary | null;
|
|
9
11
|
status: "working" | "idle";
|
|
10
12
|
metadata: Record<string, unknown> | null;
|
|
11
13
|
lastHeartbeat: string;
|
|
@@ -32,6 +34,7 @@ export interface PinetMeshOpsTransferableThread {
|
|
|
32
34
|
}
|
|
33
35
|
export interface PinetMeshOpsBrokerDbPort extends AgentMessageStorage {
|
|
34
36
|
getAllAgents: () => AgentInfo[];
|
|
37
|
+
searchAgentSessions: (options?: AgentSessionSearchOptions) => AgentSessionSearchInfo[];
|
|
35
38
|
getPendingInboxCount: (agentId: string) => number;
|
|
36
39
|
getThread: (threadId: string) => PinetMeshOpsTransferableThread | null;
|
|
37
40
|
transferThreadOwnership: (threadId: string, ownerAgent: string) => {
|
|
@@ -62,6 +65,7 @@ export interface PinetMeshOpsFollowerClientPort {
|
|
|
62
65
|
fireAt: string;
|
|
63
66
|
}>;
|
|
64
67
|
listAgents: (includeGhosts: boolean) => Promise<PinetMeshOpsFollowerAgentRecord[]>;
|
|
68
|
+
searchAgentSessions: (options: AgentSessionSearchOptions) => Promise<AgentSessionSearchInfo[]>;
|
|
65
69
|
}
|
|
66
70
|
export interface PinetMeshOpsDeps {
|
|
67
71
|
getPinetEnabled: () => boolean;
|
|
@@ -100,6 +104,7 @@ export interface PinetMeshOps {
|
|
|
100
104
|
}>;
|
|
101
105
|
listBrokerAgents: () => PinetMeshOpsAgentRecord[];
|
|
102
106
|
listFollowerAgents: (includeGhosts: boolean) => Promise<PinetMeshOpsAgentRecord[]>;
|
|
107
|
+
searchPinetSessions: (options: AgentSessionSearchOptions) => Promise<AgentSessionSearchInfo[]>;
|
|
103
108
|
}
|
|
104
109
|
export declare function parseGitHubRemoteRepo(remoteUrl: string): {
|
|
105
110
|
repoOwner: string;
|
package/dist/pinet-mesh-ops.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { normalizeOutgoingPinetControlMessage } from "./helpers.js";
|
|
4
4
|
import { dispatchBroadcastAgentMessage, dispatchDirectAgentMessage, isBroadcastChannelTarget, } from "./broker/agent-messaging.js";
|
|
5
|
+
import { summarizePinetStableId } from "./pinet-session-formatting.js";
|
|
5
6
|
import { extractTaskAssignmentsFromMessage } from "./task-assignments.js";
|
|
6
7
|
const execFileAsync = promisify(execFile);
|
|
7
8
|
function prepareOutgoingPinetAgentMessage(body, metadata) {
|
|
@@ -230,6 +231,8 @@ export function createPinetMeshOps(deps) {
|
|
|
230
231
|
name: agent.name,
|
|
231
232
|
id: agent.id,
|
|
232
233
|
pid: agent.pid,
|
|
234
|
+
stableId: agent.stableId ?? null,
|
|
235
|
+
session: summarizePinetStableId(agent.stableId),
|
|
233
236
|
status: agent.status,
|
|
234
237
|
metadata: agent.metadata,
|
|
235
238
|
lastHeartbeat: agent.lastHeartbeat,
|
|
@@ -256,6 +259,7 @@ export function createPinetMeshOps(deps) {
|
|
|
256
259
|
name: agent.name,
|
|
257
260
|
id: agent.id,
|
|
258
261
|
pid: agent.pid,
|
|
262
|
+
session: agent.session ?? null,
|
|
259
263
|
status: agent.status ?? "idle",
|
|
260
264
|
metadata: agent.metadata,
|
|
261
265
|
lastHeartbeat: agent.lastHeartbeat,
|
|
@@ -272,6 +276,20 @@ export function createPinetMeshOps(deps) {
|
|
|
272
276
|
laneId: agent.laneId,
|
|
273
277
|
}));
|
|
274
278
|
}
|
|
279
|
+
async function searchPinetSessions(options) {
|
|
280
|
+
if (deps.getBrokerRole() === "broker") {
|
|
281
|
+
const db = deps.getActiveBrokerDb();
|
|
282
|
+
if (!db) {
|
|
283
|
+
throw new Error("Broker agent identity is unavailable.");
|
|
284
|
+
}
|
|
285
|
+
return db.searchAgentSessions(options);
|
|
286
|
+
}
|
|
287
|
+
const client = deps.getFollowerClient();
|
|
288
|
+
if (!client) {
|
|
289
|
+
throw new Error("Pinet is in an unexpected state.");
|
|
290
|
+
}
|
|
291
|
+
return await client.searchAgentSessions(options);
|
|
292
|
+
}
|
|
275
293
|
return {
|
|
276
294
|
sendPinetAgentMessage,
|
|
277
295
|
sendPinetBroadcastMessage,
|
|
@@ -279,5 +297,6 @@ export function createPinetMeshOps(deps) {
|
|
|
279
297
|
scheduleFollowerWakeup,
|
|
280
298
|
listBrokerAgents,
|
|
281
299
|
listFollowerAgents,
|
|
300
|
+
searchPinetSessions,
|
|
282
301
|
};
|
|
283
302
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AgentSessionKind, AgentSessionSearchInfo, AgentSessionSummary } from "./broker/types.js";
|
|
2
|
+
export interface ParsedPinetStableId {
|
|
3
|
+
host: string | null;
|
|
4
|
+
kind: AgentSessionKind;
|
|
5
|
+
locator: string;
|
|
6
|
+
hasPath: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function parsePinetStableId(stableId: string | null | undefined): ParsedPinetStableId | null;
|
|
9
|
+
export declare function summarizePinetStableId(stableId: string | null | undefined): AgentSessionSummary | null;
|
|
10
|
+
export declare function getPinetSessionPath(stableId: string | null | undefined): string | null;
|
|
11
|
+
export declare function getPinetSessionFilename(stableId: string | null | undefined): string | null;
|
|
12
|
+
export declare function buildPinetSessionFullDetails(session: AgentSessionSearchInfo): Record<string, unknown>;
|
|
13
|
+
export declare function buildPinetSessionCompactDetails(session: AgentSessionSearchInfo): Record<string, unknown>;
|
|
@@ -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,11 +8,20 @@ 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: [
|
|
15
|
+
// Routine inbox drain: defaults are `unread_only: true` + `mark_read: true`,
|
|
16
|
+
// so this returns and consumes only this agent's unread rows. The compact
|
|
17
|
+
// output already includes capped per-message previews; no `full=true` needed.
|
|
18
|
+
{ action: "read", args: { limit: 20 } },
|
|
19
|
+
// Same defaults, scoped to one thread.
|
|
14
20
|
{ action: "read", args: { thread_id: "a2a:<broker>:<worker>", limit: 20 } },
|
|
15
|
-
|
|
21
|
+
// Non-destructive latest-history peek (does NOT mark anything read or
|
|
22
|
+
// change unread state). Use only when you need to revisit recently-read
|
|
23
|
+
// context, not as a routine inbox read.
|
|
24
|
+
{ action: "read", args: { unread_only: false, mark_read: false, limit: 5 } },
|
|
16
25
|
],
|
|
17
26
|
free: [{ action: "free", args: { note: "Wrapped up <issue>" } }],
|
|
18
27
|
snooze: [
|
|
@@ -21,6 +30,10 @@ const PINET_DISPATCHER_EXAMPLES = {
|
|
|
21
30
|
],
|
|
22
31
|
schedule: [{ action: "schedule", args: { delay: "30m", message: "Check queue state" } }],
|
|
23
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
|
+
],
|
|
24
37
|
spawn: [
|
|
25
38
|
{
|
|
26
39
|
action: "spawn",
|
|
@@ -81,6 +94,7 @@ function normalizeDispatcherAction(value) {
|
|
|
81
94
|
"snooze",
|
|
82
95
|
"schedule",
|
|
83
96
|
"agents",
|
|
97
|
+
"sessions",
|
|
84
98
|
"lanes",
|
|
85
99
|
"ports",
|
|
86
100
|
"spawn",
|
|
@@ -129,7 +143,8 @@ function classifyPinetError(message) {
|
|
|
129
143
|
message.includes("ttl_ms") ||
|
|
130
144
|
message.includes("purpose") ||
|
|
131
145
|
message.includes("port") ||
|
|
132
|
-
message.includes("spawn")
|
|
146
|
+
message.includes("spawn") ||
|
|
147
|
+
message.includes("sessions op")) {
|
|
133
148
|
return {
|
|
134
149
|
class: "input",
|
|
135
150
|
message,
|
|
@@ -795,6 +810,8 @@ function buildCompactAgentDetails(agents, hint) {
|
|
|
795
810
|
repo: getAgentRepo(agent) ?? null,
|
|
796
811
|
branch: getAgentBranch(agent) ?? null,
|
|
797
812
|
role: getAgentRole(agent) ?? null,
|
|
813
|
+
session: agent.session?.ref ?? null,
|
|
814
|
+
sessionKind: agent.session?.kind ?? null,
|
|
798
815
|
brokerManaged: agent.metadata?.brokerManaged === true,
|
|
799
816
|
parentAgentId: agent.metadata?.parentAgentId ?? null,
|
|
800
817
|
treeDepth: agent.metadata?.treeDepth ?? 0,
|
|
@@ -1355,6 +1372,123 @@ function runPinetAgentsAction(params, deps, toolName, output) {
|
|
|
1355
1372
|
};
|
|
1356
1373
|
})();
|
|
1357
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
|
+
}
|
|
1358
1492
|
export function registerPinetTools(pi, deps) {
|
|
1359
1493
|
const actionDefinitions = new Map();
|
|
1360
1494
|
function registerAction(definition) {
|
|
@@ -1377,7 +1511,7 @@ export function registerPinetTools(pi, deps) {
|
|
|
1377
1511
|
});
|
|
1378
1512
|
registerAction({
|
|
1379
1513
|
name: "read",
|
|
1380
|
-
description: "Read this agent's durable SQLite-backed Pinet inbox context with unread/read semantics. Ordinary workers only see rows addressed to their own agent identity; broker coordination visibility is limited to broker-addressed inbox rows.",
|
|
1514
|
+
description: "Read this agent's durable SQLite-backed Pinet inbox context with unread/read semantics. Defaults to draining unread rows (`unread_only: true`, `mark_read: true`) with compact per-message previews — no `full=true` required for routine reads. Override `unread_only`/`mark_read` only for a non-destructive latest-history peek. Ordinary workers only see rows addressed to their own agent identity; broker coordination visibility is limited to broker-addressed inbox rows.",
|
|
1381
1515
|
parameters: Type.Object({
|
|
1382
1516
|
thread_id: Type.Optional(Type.String({
|
|
1383
1517
|
description: "Optional Pinet/Slack broker thread ID to filter this agent's own inbox rows; it does not grant cross-agent thread access.",
|
|
@@ -1470,6 +1604,25 @@ export function registerPinetTools(pi, deps) {
|
|
|
1470
1604
|
}),
|
|
1471
1605
|
execute: (_id, params, output) => runPinetAgentsAction(params, deps, "pinet:agents", output),
|
|
1472
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
|
+
});
|
|
1473
1626
|
registerAction({
|
|
1474
1627
|
name: "ports",
|
|
1475
1628
|
description: "Acquire, renew, release, inspect, list, or expire durable Pinet local port leases.",
|
|
@@ -1541,10 +1694,10 @@ export function registerPinetTools(pi, deps) {
|
|
|
1541
1694
|
name: "pinet",
|
|
1542
1695
|
label: "Pinet Dispatcher",
|
|
1543
1696
|
description: "Dispatch Pinet operations by action with compact help and schema discovery.",
|
|
1544
|
-
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.',
|
|
1545
1698
|
parameters: Type.Object({
|
|
1546
1699
|
action: Type.String({
|
|
1547
|
-
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.",
|
|
1548
1701
|
}),
|
|
1549
1702
|
args: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
|
|
1550
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
|
},
|
|
@@ -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.2",
|
|
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.2",
|
|
53
|
+
"@pinet/imessage-bridge": "0.2.2",
|
|
54
|
+
"@pinet/pinet-core": "0.2.2",
|
|
55
|
+
"@pinet/transport-core": "0.2.2",
|
|
56
56
|
"@sinclair/typebox": "^0.34.49"
|
|
57
57
|
},
|
|
58
58
|
"types": "./dist/index.d.ts",
|