@pinet/slack-bridge 0.2.12 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/code-anchor.d.ts +10 -3
- package/dist/code-anchor.js +34 -8
- package/dist/index.js +15 -1
- package/dist/nvim-pinet-adapter.d.ts +19 -2
- package/dist/nvim-pinet-adapter.js +219 -10
- package/dist/slack-pinet-runtime-adapter.js +35 -1
- package/package.json +5 -5
package/dist/code-anchor.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export interface ContextJsonObject {
|
|
|
3
3
|
[key: string]: ContextJsonValue | undefined;
|
|
4
4
|
}
|
|
5
5
|
export type CodeAnchorSide = "old" | "new";
|
|
6
|
+
export type CodeAnchorKind = "diff" | "normal";
|
|
6
7
|
export interface CodeRevisionIdentity extends ContextJsonObject {
|
|
7
8
|
repository: string;
|
|
8
9
|
worktree: string;
|
|
@@ -10,7 +11,10 @@ export interface CodeRevisionIdentity extends ContextJsonObject {
|
|
|
10
11
|
baseOid: string | null;
|
|
11
12
|
headOid: string;
|
|
12
13
|
blobOid: string;
|
|
13
|
-
|
|
14
|
+
anchorKind: CodeAnchorKind;
|
|
15
|
+
side?: CodeAnchorSide;
|
|
16
|
+
headBlobOid?: string | null;
|
|
17
|
+
dirty?: boolean;
|
|
14
18
|
}
|
|
15
19
|
export interface CodeAnchor extends CodeRevisionIdentity {
|
|
16
20
|
startLine: number;
|
|
@@ -25,7 +29,7 @@ export interface ContextualThreadState extends ContextJsonObject {
|
|
|
25
29
|
}
|
|
26
30
|
export interface ContextualThreadMetadata extends ContextJsonObject {
|
|
27
31
|
pinetKind: "contextual_thread";
|
|
28
|
-
schemaVersion: 1;
|
|
32
|
+
schemaVersion: 1 | 2;
|
|
29
33
|
codeAnchor: CodeAnchor;
|
|
30
34
|
state: ContextualThreadState;
|
|
31
35
|
}
|
|
@@ -38,7 +42,10 @@ export declare function buildContextualThreadMetadata(input: {
|
|
|
38
42
|
baseOid?: string | null;
|
|
39
43
|
headOid: string;
|
|
40
44
|
blobOid: string;
|
|
41
|
-
|
|
45
|
+
anchorKind?: CodeAnchorKind;
|
|
46
|
+
side?: CodeAnchorSide;
|
|
47
|
+
headBlobOid?: string | null;
|
|
48
|
+
dirty?: boolean;
|
|
42
49
|
startLine: number;
|
|
43
50
|
endLine?: number;
|
|
44
51
|
selectedText?: string | null;
|
package/dist/code-anchor.js
CHANGED
|
@@ -8,9 +8,12 @@ export function buildNvimThreadId(repoSocketHash) {
|
|
|
8
8
|
export function buildContextualThreadMetadata(input) {
|
|
9
9
|
const startLine = Math.max(1, Math.floor(input.startLine));
|
|
10
10
|
const endLine = Math.max(startLine, Math.floor(input.endLine ?? startLine));
|
|
11
|
+
const anchorKind = input.anchorKind ?? "diff";
|
|
12
|
+
if (anchorKind === "diff" && !input.side)
|
|
13
|
+
throw new Error("diff code anchor side is required");
|
|
11
14
|
return {
|
|
12
15
|
pinetKind: "contextual_thread",
|
|
13
|
-
schemaVersion: 1,
|
|
16
|
+
schemaVersion: anchorKind === "normal" ? 2 : 1,
|
|
14
17
|
codeAnchor: {
|
|
15
18
|
repository: input.repository,
|
|
16
19
|
worktree: input.worktree,
|
|
@@ -18,7 +21,11 @@ export function buildContextualThreadMetadata(input) {
|
|
|
18
21
|
baseOid: input.baseOid ?? null,
|
|
19
22
|
headOid: input.headOid,
|
|
20
23
|
blobOid: input.blobOid,
|
|
21
|
-
|
|
24
|
+
anchorKind,
|
|
25
|
+
...(input.side ? { side: input.side } : {}),
|
|
26
|
+
...(anchorKind === "normal"
|
|
27
|
+
? { headBlobOid: input.headBlobOid ?? null, dirty: input.dirty === true }
|
|
28
|
+
: {}),
|
|
22
29
|
startLine,
|
|
23
30
|
endLine,
|
|
24
31
|
selectedTextSha256: input.selectedText ? sha256Text(input.selectedText) : null,
|
|
@@ -43,7 +50,9 @@ function readPositiveInteger(record, key) {
|
|
|
43
50
|
}
|
|
44
51
|
export function parseContextualThreadMetadata(value) {
|
|
45
52
|
const metadata = readRecord(value);
|
|
46
|
-
if (!metadata ||
|
|
53
|
+
if (!metadata ||
|
|
54
|
+
metadata.pinetKind !== "contextual_thread" ||
|
|
55
|
+
(metadata.schemaVersion !== 1 && metadata.schemaVersion !== 2)) {
|
|
47
56
|
return null;
|
|
48
57
|
}
|
|
49
58
|
const anchorRecord = readRecord(metadata.codeAnchor);
|
|
@@ -55,7 +64,14 @@ export function parseContextualThreadMetadata(value) {
|
|
|
55
64
|
const path = readString(anchorRecord, "path");
|
|
56
65
|
const headOid = readString(anchorRecord, "headOid");
|
|
57
66
|
const blobOid = readString(anchorRecord, "blobOid");
|
|
67
|
+
const anchorKind = metadata.schemaVersion === 2 ? anchorRecord.anchorKind : "diff";
|
|
68
|
+
if (anchorKind !== "diff" && anchorKind !== "normal")
|
|
69
|
+
return null;
|
|
58
70
|
const side = anchorRecord.side === "old" || anchorRecord.side === "new" ? anchorRecord.side : null;
|
|
71
|
+
if (anchorKind === "diff" && !side)
|
|
72
|
+
return null;
|
|
73
|
+
if (anchorKind === "normal" && typeof anchorRecord.dirty !== "boolean")
|
|
74
|
+
return null;
|
|
59
75
|
const startLine = readPositiveInteger(anchorRecord, "startLine");
|
|
60
76
|
const endLine = readPositiveInteger(anchorRecord, "endLine");
|
|
61
77
|
if (!repository ||
|
|
@@ -63,7 +79,6 @@ export function parseContextualThreadMetadata(value) {
|
|
|
63
79
|
!path ||
|
|
64
80
|
!headOid ||
|
|
65
81
|
!blobOid ||
|
|
66
|
-
!side ||
|
|
67
82
|
startLine == null ||
|
|
68
83
|
endLine == null) {
|
|
69
84
|
return null;
|
|
@@ -72,7 +87,7 @@ export function parseContextualThreadMetadata(value) {
|
|
|
72
87
|
const resolvedBy = readString(stateRecord, "resolvedBy");
|
|
73
88
|
return {
|
|
74
89
|
pinetKind: "contextual_thread",
|
|
75
|
-
schemaVersion:
|
|
90
|
+
schemaVersion: metadata.schemaVersion,
|
|
76
91
|
codeAnchor: {
|
|
77
92
|
repository,
|
|
78
93
|
worktree,
|
|
@@ -80,7 +95,14 @@ export function parseContextualThreadMetadata(value) {
|
|
|
80
95
|
baseOid: readString(anchorRecord, "baseOid"),
|
|
81
96
|
headOid,
|
|
82
97
|
blobOid,
|
|
83
|
-
|
|
98
|
+
anchorKind,
|
|
99
|
+
...(side ? { side } : {}),
|
|
100
|
+
...(anchorKind === "normal"
|
|
101
|
+
? {
|
|
102
|
+
headBlobOid: readString(anchorRecord, "headBlobOid"),
|
|
103
|
+
dirty: anchorRecord.dirty === true,
|
|
104
|
+
}
|
|
105
|
+
: {}),
|
|
84
106
|
startLine,
|
|
85
107
|
endLine,
|
|
86
108
|
selectedTextSha256: readString(anchorRecord, "selectedTextSha256"),
|
|
@@ -106,7 +128,10 @@ export function hasSameCodeRevision(anchor, candidate) {
|
|
|
106
128
|
anchor.baseOid === candidate.baseOid &&
|
|
107
129
|
anchor.headOid === candidate.headOid &&
|
|
108
130
|
anchor.blobOid === candidate.blobOid &&
|
|
109
|
-
anchor.
|
|
131
|
+
anchor.anchorKind === candidate.anchorKind &&
|
|
132
|
+
anchor.side === candidate.side &&
|
|
133
|
+
anchor.headBlobOid === candidate.headBlobOid &&
|
|
134
|
+
anchor.dirty === candidate.dirty);
|
|
110
135
|
}
|
|
111
136
|
export function formatAnchorForMessage(metadata) {
|
|
112
137
|
const anchor = metadata.codeAnchor;
|
|
@@ -114,5 +139,6 @@ export function formatAnchorForMessage(metadata) {
|
|
|
114
139
|
? `${anchor.path}:${anchor.startLine}`
|
|
115
140
|
: `${anchor.path}:${anchor.startLine}-${anchor.endLine}`;
|
|
116
141
|
const base = anchor.baseOid ? ` base=${anchor.baseOid}` : "";
|
|
117
|
-
|
|
142
|
+
const mode = anchor.anchorKind === "diff" ? `side=${anchor.side}` : `mode=normal dirty=${anchor.dirty}`;
|
|
143
|
+
return `[code-anchor ${range} ${mode} head=${anchor.headOid} blob=${anchor.blobOid}${base}]`;
|
|
118
144
|
}
|
package/dist/index.js
CHANGED
|
@@ -631,7 +631,21 @@ export default function (pi) {
|
|
|
631
631
|
broker.db.queueMessage(decision.agentId, routedMessage);
|
|
632
632
|
return;
|
|
633
633
|
}
|
|
634
|
-
if (decision.action === "
|
|
634
|
+
if (decision.action === "broadcast") {
|
|
635
|
+
const remoteAgentIds = decision.agentIds.filter((agentId) => agentId !== selfId);
|
|
636
|
+
if (broker.db.queueMessageToAgents) {
|
|
637
|
+
broker.db.queueMessageToAgents(remoteAgentIds, routedMessage);
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
for (const agentId of remoteAgentIds)
|
|
641
|
+
broker.db.queueMessage(agentId, routedMessage);
|
|
642
|
+
}
|
|
643
|
+
if (!decision.agentIds.includes(selfId))
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (decision.action === "deliver" ||
|
|
647
|
+
decision.action === "broadcast" ||
|
|
648
|
+
decision.action === "unrouted") {
|
|
635
649
|
const persisted = routedMessage.threadId
|
|
636
650
|
? persistDeliveredInboundMessage(broker.db, selfId, routedMessage)
|
|
637
651
|
: null;
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import { type ContextJsonValue } from "./code-anchor.js";
|
|
2
|
-
import type { BrokerMessage, InboundMessage, MessageAdapter, OutboundMessage, ThreadInfo } from "./broker/types.js";
|
|
1
|
+
import { type CodeRevisionIdentity, type ContextJsonValue } from "./code-anchor.js";
|
|
2
|
+
import type { BrokerMessage, InboundMessage, MessageAdapter, OutboundMessage, ThreadInfo, DocumentInfo } from "./broker/types.js";
|
|
3
3
|
export interface NvimAdapterDbPort {
|
|
4
4
|
getThread(threadId: string): ThreadInfo | null;
|
|
5
|
+
getDocument(documentId: string): DocumentInfo | null;
|
|
6
|
+
getDocumentByAlias(source: string, externalId: string): DocumentInfo | null;
|
|
7
|
+
upsertDocument(document: Omit<DocumentInfo, "createdAt" | "updatedAt">): DocumentInfo;
|
|
8
|
+
bindDocumentAlias(source: string, externalId: string, documentId: string, metadata?: Record<string, ContextJsonValue> | null): void;
|
|
9
|
+
setDocumentOwner(documentId: string, ownerAgent: string | null): DocumentInfo;
|
|
10
|
+
subscribeDocument(documentId: string, agentId: string): void;
|
|
11
|
+
unsubscribeDocument(documentId: string, agentId: string): void;
|
|
12
|
+
listDocumentSubscribers(documentId: string): string[];
|
|
13
|
+
getDocumentRecipients(documentId: string): string[];
|
|
5
14
|
createThread(thread: ThreadInfo): ThreadInfo;
|
|
6
15
|
updateThread(threadId: string, updates: Partial<ThreadInfo>): void;
|
|
7
16
|
getThreads(ownerAgent?: string): ThreadInfo[];
|
|
@@ -27,6 +36,8 @@ export interface NvimPinetAdapterOptions extends NvimRepositoryContext {
|
|
|
27
36
|
} | null;
|
|
28
37
|
}
|
|
29
38
|
export declare function resolveNvimRepositoryContext(cwd: string): NvimRepositoryContext | null;
|
|
39
|
+
export declare function buildGitFileAliasExternalId(anchor: Pick<CodeRevisionIdentity, "repository" | "worktree" | "path">): string;
|
|
40
|
+
export declare function buildGitFileDocumentId(anchor: Pick<CodeRevisionIdentity, "repository" | "worktree" | "path">): string;
|
|
30
41
|
export declare function computeNvimSocketPath(worktree: string, branch: string): string;
|
|
31
42
|
export declare class NvimPinetAdapter implements MessageAdapter {
|
|
32
43
|
private readonly options;
|
|
@@ -45,11 +56,17 @@ export declare class NvimPinetAdapter implements MessageAdapter {
|
|
|
45
56
|
private accept;
|
|
46
57
|
private handleLine;
|
|
47
58
|
private handleRequest;
|
|
59
|
+
private resolveGitDocument;
|
|
48
60
|
private createThread;
|
|
49
61
|
private replyToThread;
|
|
50
62
|
private resolveThread;
|
|
51
63
|
private listThreads;
|
|
52
64
|
private getThread;
|
|
65
|
+
private getDocument;
|
|
66
|
+
private setDocumentOwner;
|
|
67
|
+
private subscribeDocument;
|
|
68
|
+
private bindDocumentThread;
|
|
69
|
+
private emitDocumentEvent;
|
|
53
70
|
private serializeThread;
|
|
54
71
|
private emitInbound;
|
|
55
72
|
private broadcast;
|
|
@@ -31,6 +31,12 @@ export function resolveNvimRepositoryContext(cwd) {
|
|
|
31
31
|
return null;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
export function buildGitFileAliasExternalId(anchor) {
|
|
35
|
+
return `${anchor.repository}\0${anchor.worktree}\0${anchor.path}`;
|
|
36
|
+
}
|
|
37
|
+
export function buildGitFileDocumentId(anchor) {
|
|
38
|
+
return `doc:git-file:${createHash("sha256").update(buildGitFileAliasExternalId(anchor)).digest("hex")}`;
|
|
39
|
+
}
|
|
34
40
|
function computeRepoSocketHash(worktree, branch) {
|
|
35
41
|
return createHash("sha256").update(`${worktree}:${branch}`).digest("hex");
|
|
36
42
|
}
|
|
@@ -82,7 +88,16 @@ function parseAnchor(payload) {
|
|
|
82
88
|
const headOid = getString(anchor, "headOid");
|
|
83
89
|
const blobOid = getString(anchor, "blobOid");
|
|
84
90
|
const side = anchor.side === "old" || anchor.side === "new" ? anchor.side : null;
|
|
85
|
-
|
|
91
|
+
const anchorKind = anchor.anchorKind === "normal" || anchor.anchorKind === "diff"
|
|
92
|
+
? anchor.anchorKind
|
|
93
|
+
: side
|
|
94
|
+
? "diff"
|
|
95
|
+
: null;
|
|
96
|
+
if (!repository || !worktree || !anchorPath || !headOid || !blobOid || !anchorKind)
|
|
97
|
+
return null;
|
|
98
|
+
if (anchorKind === "diff" && !side)
|
|
99
|
+
return null;
|
|
100
|
+
if (anchorKind === "normal" && typeof anchor.dirty !== "boolean")
|
|
86
101
|
return null;
|
|
87
102
|
return {
|
|
88
103
|
repository,
|
|
@@ -91,7 +106,11 @@ function parseAnchor(payload) {
|
|
|
91
106
|
baseOid: getString(anchor, "baseOid"),
|
|
92
107
|
headOid,
|
|
93
108
|
blobOid,
|
|
94
|
-
|
|
109
|
+
anchorKind,
|
|
110
|
+
...(side ? { side } : {}),
|
|
111
|
+
...(anchorKind === "normal"
|
|
112
|
+
? { headBlobOid: getString(anchor, "headBlobOid"), dirty: anchor.dirty === true }
|
|
113
|
+
: {}),
|
|
95
114
|
};
|
|
96
115
|
}
|
|
97
116
|
// agent-standards-ignore prefer-inline-single-use-helper: create payload parser is a named protocol DTO boundary.
|
|
@@ -101,7 +120,7 @@ function parseCreateThreadRequest(payload) {
|
|
|
101
120
|
const anchor = parseAnchor(payload);
|
|
102
121
|
const startLine = getPositiveInteger(payload, "startLine", null);
|
|
103
122
|
const endLine = getPositiveInteger(payload, "endLine", startLine);
|
|
104
|
-
if (!
|
|
123
|
+
if (!body || !anchor || startLine == null || endLine == null)
|
|
105
124
|
return null;
|
|
106
125
|
return {
|
|
107
126
|
targetAgentId,
|
|
@@ -137,6 +156,21 @@ function parseListRequest(payload) {
|
|
|
137
156
|
limit: getPositiveInteger(payload, "limit", 100) ?? 100,
|
|
138
157
|
};
|
|
139
158
|
}
|
|
159
|
+
// agent-standards-ignore prefer-inline-single-use-helper: document payload parser is a named protocol DTO boundary.
|
|
160
|
+
function parseDocumentRequest(payload) {
|
|
161
|
+
const anchor = parseAnchor(payload);
|
|
162
|
+
return anchor ? { anchor } : null;
|
|
163
|
+
}
|
|
164
|
+
function parseBindThreadRequest(payload) {
|
|
165
|
+
const anchor = parseAnchor(payload);
|
|
166
|
+
const threadId = getString(payload, "threadId");
|
|
167
|
+
return anchor && threadId ? { anchor, threadId } : null;
|
|
168
|
+
}
|
|
169
|
+
function parseDocumentAgentRequest(payload) {
|
|
170
|
+
const anchor = parseAnchor(payload);
|
|
171
|
+
const agentId = getString(payload, "agentId");
|
|
172
|
+
return anchor && agentId ? { anchor, agentId } : null;
|
|
173
|
+
}
|
|
140
174
|
function serializeMetadata(metadata) {
|
|
141
175
|
return JSON.parse(JSON.stringify(metadata));
|
|
142
176
|
}
|
|
@@ -304,20 +338,54 @@ export class NvimPinetAdapter {
|
|
|
304
338
|
return this.listThreads(request.payload);
|
|
305
339
|
case "pinet.thread.get":
|
|
306
340
|
return this.getThread(request.payload);
|
|
341
|
+
case "pinet.document.get":
|
|
342
|
+
return this.getDocument(request.payload);
|
|
343
|
+
case "pinet.document.owner":
|
|
344
|
+
return this.setDocumentOwner(request.payload);
|
|
345
|
+
case "pinet.document.subscribe":
|
|
346
|
+
return this.subscribeDocument(request.payload, true);
|
|
347
|
+
case "pinet.document.unsubscribe":
|
|
348
|
+
return this.subscribeDocument(request.payload, false);
|
|
349
|
+
case "pinet.document.bind_thread":
|
|
350
|
+
return this.bindDocumentThread(request.payload);
|
|
307
351
|
default:
|
|
308
352
|
throw new Error(`Unknown nvim request: ${request.type}`);
|
|
309
353
|
}
|
|
310
354
|
}
|
|
355
|
+
resolveGitDocument(anchor) {
|
|
356
|
+
return (this.options.db.getDocumentByAlias("nvim", buildGitFileAliasExternalId(anchor)) ??
|
|
357
|
+
this.options.db.getDocument(buildGitFileDocumentId(anchor)));
|
|
358
|
+
}
|
|
311
359
|
createThread(payload) {
|
|
312
360
|
const parsed = parseCreateThreadRequest(payload);
|
|
313
361
|
if (!parsed)
|
|
314
|
-
throw new Error("
|
|
362
|
+
throw new Error("body, anchor, startLine, and endLine are required");
|
|
315
363
|
if (parsed.anchor.repository !== this.options.repository ||
|
|
316
364
|
parsed.anchor.worktree !== this.options.worktree) {
|
|
317
365
|
throw new Error("code anchor does not belong to this Pinet worktree");
|
|
318
366
|
}
|
|
319
|
-
|
|
320
|
-
|
|
367
|
+
let document = this.resolveGitDocument(parsed.anchor);
|
|
368
|
+
const documentId = document?.documentId ?? buildGitFileDocumentId(parsed.anchor);
|
|
369
|
+
const targetAgentId = parsed.targetAgentId ?? document?.ownerAgent ?? null;
|
|
370
|
+
if (!targetAgentId)
|
|
371
|
+
throw new Error("targetAgentId is required until this document has an owner");
|
|
372
|
+
if (!this.options.getAgentById(targetAgentId)) {
|
|
373
|
+
throw new Error(`Unknown target agent: ${targetAgentId}`);
|
|
374
|
+
}
|
|
375
|
+
if (!document) {
|
|
376
|
+
document = this.options.db.upsertDocument({
|
|
377
|
+
documentId,
|
|
378
|
+
kind: "git_file",
|
|
379
|
+
title: parsed.anchor.path,
|
|
380
|
+
ownerAgent: targetAgentId,
|
|
381
|
+
ownerBinding: "explicit",
|
|
382
|
+
metadata: {
|
|
383
|
+
repository: parsed.anchor.repository,
|
|
384
|
+
worktree: parsed.anchor.worktree,
|
|
385
|
+
path: parsed.anchor.path,
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
this.options.db.bindDocumentAlias("nvim", buildGitFileAliasExternalId(parsed.anchor), documentId);
|
|
321
389
|
}
|
|
322
390
|
const metadata = buildContextualThreadMetadata({
|
|
323
391
|
...parsed.anchor,
|
|
@@ -332,16 +400,17 @@ export class NvimPinetAdapter {
|
|
|
332
400
|
threadId,
|
|
333
401
|
source: "nvim",
|
|
334
402
|
channel: this.repoSocketHash,
|
|
335
|
-
ownerAgent:
|
|
403
|
+
ownerAgent: targetAgentId,
|
|
336
404
|
ownerBinding: "explicit",
|
|
337
|
-
metadata: serializeMetadata(metadata),
|
|
405
|
+
metadata: { ...serializeMetadata(metadata), documentId },
|
|
338
406
|
createdAt: now,
|
|
339
407
|
updatedAt: now,
|
|
340
408
|
});
|
|
341
|
-
this.emitInbound(threadId,
|
|
409
|
+
this.emitInbound(threadId, targetAgentId, `${formatAnchorForMessage(metadata)}\n\n${parsed.body}`, {
|
|
342
410
|
pinetKind: "contextual_thread_message",
|
|
343
411
|
schemaVersion: 1,
|
|
344
412
|
event: "thread.created",
|
|
413
|
+
documentId,
|
|
345
414
|
});
|
|
346
415
|
return { threadId, metadata: serializeMetadata(metadata) };
|
|
347
416
|
}
|
|
@@ -352,10 +421,12 @@ export class NvimPinetAdapter {
|
|
|
352
421
|
const thread = this.options.db.getThread(parsed.threadId);
|
|
353
422
|
if (!thread)
|
|
354
423
|
throw new Error(`Unknown thread: ${parsed.threadId}`);
|
|
424
|
+
const documentId = typeof thread.metadata?.documentId === "string" ? thread.metadata.documentId : null;
|
|
355
425
|
this.emitInbound(parsed.threadId, thread.ownerAgent ?? "", parsed.body, {
|
|
356
426
|
pinetKind: "contextual_thread_message",
|
|
357
427
|
schemaVersion: 1,
|
|
358
428
|
event: "thread.reply",
|
|
429
|
+
...(documentId ? { documentId } : {}),
|
|
359
430
|
});
|
|
360
431
|
return { threadId: parsed.threadId };
|
|
361
432
|
}
|
|
@@ -369,10 +440,12 @@ export class NvimPinetAdapter {
|
|
|
369
440
|
throw new Error(`Unknown contextual thread: ${parsed.threadId}`);
|
|
370
441
|
const updated = updateContextualThreadResolvedState(metadata, parsed.resolved, "nvim");
|
|
371
442
|
this.options.db.updateThread(parsed.threadId, { metadata: serializeMetadata(updated) });
|
|
372
|
-
|
|
443
|
+
const documentId = typeof thread.metadata?.documentId === "string" ? thread.metadata.documentId : null;
|
|
444
|
+
this.emitInbound(parsed.threadId, thread.ownerAgent ?? "", parsed.resolved ? "Resolved this thread." : "Reopened this thread.", {
|
|
373
445
|
pinetKind: "contextual_thread_message",
|
|
374
446
|
schemaVersion: 1,
|
|
375
447
|
event: parsed.resolved ? "thread.resolved" : "thread.reopened",
|
|
448
|
+
...(documentId ? { documentId } : {}),
|
|
376
449
|
});
|
|
377
450
|
this.broadcast({ type: "thread.updated", payload: { threadId: parsed.threadId } });
|
|
378
451
|
return { threadId: parsed.threadId, metadata: serializeMetadata(updated) };
|
|
@@ -403,6 +476,142 @@ export class NvimPinetAdapter {
|
|
|
403
476
|
throw new Error(`Unknown contextual thread: ${threadId}`);
|
|
404
477
|
return this.serializeThread(thread, metadata, 50);
|
|
405
478
|
}
|
|
479
|
+
getDocument(payload) {
|
|
480
|
+
const parsed = parseDocumentRequest(payload);
|
|
481
|
+
if (!parsed)
|
|
482
|
+
throw new Error("revision-aware anchor is required");
|
|
483
|
+
const document = this.resolveGitDocument(parsed.anchor);
|
|
484
|
+
const documentId = document?.documentId ?? buildGitFileDocumentId(parsed.anchor);
|
|
485
|
+
return {
|
|
486
|
+
documentId,
|
|
487
|
+
ownerAgentId: document?.ownerAgent ?? null,
|
|
488
|
+
subscribers: document ? this.options.db.listDocumentSubscribers(documentId) : [],
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
setDocumentOwner(payload) {
|
|
492
|
+
const parsed = parseDocumentAgentRequest(payload);
|
|
493
|
+
if (!parsed)
|
|
494
|
+
throw new Error("anchor and agentId are required");
|
|
495
|
+
if (!this.options.getAgentById(parsed.agentId))
|
|
496
|
+
throw new Error(`Unknown agent: ${parsed.agentId}`);
|
|
497
|
+
const existingDocument = this.resolveGitDocument(parsed.anchor);
|
|
498
|
+
const documentId = existingDocument?.documentId ?? buildGitFileDocumentId(parsed.anchor);
|
|
499
|
+
if (!existingDocument) {
|
|
500
|
+
this.options.db.upsertDocument({
|
|
501
|
+
documentId,
|
|
502
|
+
kind: "git_file",
|
|
503
|
+
title: parsed.anchor.path,
|
|
504
|
+
ownerAgent: parsed.agentId,
|
|
505
|
+
ownerBinding: "explicit",
|
|
506
|
+
metadata: {
|
|
507
|
+
repository: parsed.anchor.repository,
|
|
508
|
+
worktree: parsed.anchor.worktree,
|
|
509
|
+
path: parsed.anchor.path,
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
this.options.db.setDocumentOwner(documentId, parsed.agentId);
|
|
515
|
+
}
|
|
516
|
+
this.options.db.bindDocumentAlias("nvim", buildGitFileAliasExternalId(parsed.anchor), documentId);
|
|
517
|
+
for (const thread of this.options.db.getThreads()) {
|
|
518
|
+
if (thread.metadata?.documentId === documentId && thread.ownerAgent !== parsed.agentId) {
|
|
519
|
+
this.options.db.updateThread(thread.threadId, {
|
|
520
|
+
ownerAgent: parsed.agentId,
|
|
521
|
+
ownerBinding: "explicit",
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
this.emitDocumentEvent(documentId, parsed.agentId, `Document owner changed to ${parsed.agentId}.`, "document.owner_changed");
|
|
526
|
+
return this.getDocument({ anchor: parsed.anchor });
|
|
527
|
+
}
|
|
528
|
+
subscribeDocument(payload, subscribe) {
|
|
529
|
+
const parsed = parseDocumentAgentRequest(payload);
|
|
530
|
+
if (!parsed)
|
|
531
|
+
throw new Error("anchor and agentId are required");
|
|
532
|
+
if (!this.options.getAgentById(parsed.agentId))
|
|
533
|
+
throw new Error(`Unknown agent: ${parsed.agentId}`);
|
|
534
|
+
const document = this.resolveGitDocument(parsed.anchor);
|
|
535
|
+
const documentId = document?.documentId ?? buildGitFileDocumentId(parsed.anchor);
|
|
536
|
+
if (!document)
|
|
537
|
+
throw new Error(`Unknown document: ${documentId}`);
|
|
538
|
+
if (subscribe)
|
|
539
|
+
this.options.db.subscribeDocument(documentId, parsed.agentId);
|
|
540
|
+
else
|
|
541
|
+
this.options.db.unsubscribeDocument(documentId, parsed.agentId);
|
|
542
|
+
const updatedDocument = this.options.db.getDocument(documentId);
|
|
543
|
+
this.emitDocumentEvent(documentId, updatedDocument.ownerAgent ?? parsed.agentId, subscribe
|
|
544
|
+
? `${parsed.agentId} subscribed to this document.`
|
|
545
|
+
: `${parsed.agentId} unsubscribed from this document.`, subscribe ? "document.subscribed" : "document.unsubscribed");
|
|
546
|
+
return this.getDocument({ anchor: parsed.anchor });
|
|
547
|
+
}
|
|
548
|
+
bindDocumentThread(payload) {
|
|
549
|
+
const parsed = parseBindThreadRequest(payload);
|
|
550
|
+
if (!parsed)
|
|
551
|
+
throw new Error("anchor and threadId are required");
|
|
552
|
+
const document = this.resolveGitDocument(parsed.anchor);
|
|
553
|
+
if (!document?.ownerAgent)
|
|
554
|
+
throw new Error("Set a document owner before binding a Slack thread");
|
|
555
|
+
const thread = this.options.db.getThread(parsed.threadId);
|
|
556
|
+
if (!thread || thread.source !== "slack") {
|
|
557
|
+
throw new Error(`Unknown Slack thread: ${parsed.threadId}`);
|
|
558
|
+
}
|
|
559
|
+
const previousDocumentId = typeof thread.metadata?.documentId === "string" ? thread.metadata.documentId : null;
|
|
560
|
+
if (previousDocumentId && previousDocumentId !== document.documentId) {
|
|
561
|
+
for (const subscriber of this.options.db.listDocumentSubscribers(previousDocumentId)) {
|
|
562
|
+
this.options.db.subscribeDocument(document.documentId, subscriber);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const referenceExternalId = `${thread.channel}\0${thread.threadId}`;
|
|
566
|
+
this.options.db.bindDocumentAlias("slack-thread-ref", referenceExternalId, document.documentId, { channelId: thread.channel, threadTs: thread.threadId });
|
|
567
|
+
const scopedExternalId = typeof thread.metadata?.documentAliasExternalId === "string"
|
|
568
|
+
? thread.metadata.documentAliasExternalId
|
|
569
|
+
: null;
|
|
570
|
+
if (scopedExternalId) {
|
|
571
|
+
this.options.db.bindDocumentAlias("slack", scopedExternalId, document.documentId, {
|
|
572
|
+
channelId: thread.channel,
|
|
573
|
+
threadTs: thread.threadId,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
this.options.db.updateThread(thread.threadId, {
|
|
577
|
+
ownerAgent: document.ownerAgent,
|
|
578
|
+
ownerBinding: "explicit",
|
|
579
|
+
metadata: {
|
|
580
|
+
...(thread.metadata ?? {}),
|
|
581
|
+
documentId: document.documentId,
|
|
582
|
+
documentReferenceExternalId: referenceExternalId,
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
this.emitDocumentEvent(document.documentId, document.ownerAgent, `Bound Slack thread ${thread.channel}/${thread.threadId} to this document.`, "document.thread_bound");
|
|
586
|
+
return this.getDocument({ anchor: parsed.anchor });
|
|
587
|
+
}
|
|
588
|
+
emitDocumentEvent(documentId, ownerAgentId, body, event) {
|
|
589
|
+
const threadId = `document:${documentId}`;
|
|
590
|
+
const now = new Date().toISOString();
|
|
591
|
+
const existing = this.options.db.getThread(threadId);
|
|
592
|
+
if (!existing) {
|
|
593
|
+
this.options.db.createThread({
|
|
594
|
+
threadId,
|
|
595
|
+
source: "nvim",
|
|
596
|
+
channel: this.repoSocketHash,
|
|
597
|
+
ownerAgent: ownerAgentId,
|
|
598
|
+
ownerBinding: "explicit",
|
|
599
|
+
metadata: { pinetKind: "document_thread", documentId },
|
|
600
|
+
createdAt: now,
|
|
601
|
+
updatedAt: now,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
else if (existing.ownerAgent !== ownerAgentId) {
|
|
605
|
+
this.options.db.updateThread(threadId, { ownerAgent: ownerAgentId });
|
|
606
|
+
}
|
|
607
|
+
this.emitInbound(threadId, ownerAgentId, body, {
|
|
608
|
+
pinetKind: "document_message",
|
|
609
|
+
schemaVersion: 1,
|
|
610
|
+
event,
|
|
611
|
+
documentId,
|
|
612
|
+
});
|
|
613
|
+
this.broadcast({ type: "document.updated", payload: { documentId } });
|
|
614
|
+
}
|
|
406
615
|
serializeThread(thread, metadata, messageLimit) {
|
|
407
616
|
const messages = this.options.db.getMessagesForThread(thread.threadId, messageLimit);
|
|
408
617
|
return {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { SlackAdapter } from "./broker/adapters/slack.js";
|
|
2
3
|
export function readStoredSlackThreadContext(metadata) {
|
|
3
4
|
const value = metadata?.slackThreadContext;
|
|
@@ -34,12 +35,45 @@ function getKnownSlackThread(broker, threadTs) {
|
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
function rememberKnownSlackThread(broker, threadTs, channelId, context) {
|
|
37
|
-
const
|
|
38
|
+
const existing = broker.db.getThread(threadTs);
|
|
39
|
+
const existingMetadata = existing?.metadata ?? {};
|
|
40
|
+
const scopeKey = context ? JSON.stringify(context.scope) : "workspace";
|
|
41
|
+
const externalId = `${scopeKey}\0${channelId}\0${threadTs}`;
|
|
42
|
+
const referenceExternalId = `${channelId}\0${threadTs}`;
|
|
43
|
+
const aliasedDocument = broker.db.getDocumentByAlias("slack-thread-ref", referenceExternalId) ??
|
|
44
|
+
broker.db.getDocumentByAlias("slack", externalId);
|
|
45
|
+
const documentId = aliasedDocument?.documentId ??
|
|
46
|
+
`doc:slack-thread:${createHash("sha256").update(externalId).digest("hex")}`;
|
|
47
|
+
if (!aliasedDocument) {
|
|
48
|
+
broker.db.upsertDocument({
|
|
49
|
+
documentId,
|
|
50
|
+
kind: "slack_thread",
|
|
51
|
+
title: `Slack ${channelId}/${threadTs}`,
|
|
52
|
+
ownerAgent: existing?.ownerAgent ?? null,
|
|
53
|
+
ownerBinding: existing?.ownerBinding ?? null,
|
|
54
|
+
metadata: {
|
|
55
|
+
channelId,
|
|
56
|
+
threadTs,
|
|
57
|
+
...(context ? { slackThreadContext: context } : {}),
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
broker.db.bindDocumentAlias("slack", externalId, documentId, {
|
|
62
|
+
channelId,
|
|
63
|
+
threadTs,
|
|
64
|
+
});
|
|
65
|
+
broker.db.bindDocumentAlias("slack-thread-ref", referenceExternalId, documentId, {
|
|
66
|
+
channelId,
|
|
67
|
+
threadTs,
|
|
68
|
+
});
|
|
38
69
|
broker.db.updateThread(threadTs, {
|
|
39
70
|
source: "slack",
|
|
40
71
|
channel: channelId,
|
|
41
72
|
metadata: {
|
|
42
73
|
...existingMetadata,
|
|
74
|
+
documentId,
|
|
75
|
+
documentAliasExternalId: externalId,
|
|
76
|
+
documentReferenceExternalId: referenceExternalId,
|
|
43
77
|
...(context ? { slackThreadContext: context } : {}),
|
|
44
78
|
},
|
|
45
79
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pinet/slack-bridge",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
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 --config ../vitest.config.ts"
|
|
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.13",
|
|
53
|
+
"@pinet/imessage-bridge": "0.2.13",
|
|
54
|
+
"@pinet/pinet-core": "0.2.13",
|
|
55
|
+
"@pinet/transport-core": "0.2.13",
|
|
56
56
|
"@sinclair/typebox": "^0.34.49"
|
|
57
57
|
},
|
|
58
58
|
"types": "./dist/index.d.ts",
|