@patimweb/pi-teams 1.7.1 → 1.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/extension/index.ts +9 -5
- package/src/graph/mappers.ts +24 -2
- package/src/graph/media.ts +5 -0
- package/src/status.ts +25 -0
- package/src/utils/attachments.ts +38 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patimweb/pi-teams",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
4
4
|
"description": "Microsoft Teams integration for pi coding agent — acts fully as the signed-in user across multiple accounts and tenants: chats, channels, meetings, presence, with configurable guardrails",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/extension/index.ts
CHANGED
|
@@ -16,7 +16,13 @@ import {
|
|
|
16
16
|
tryResolveConnection,
|
|
17
17
|
type TeamsConnection,
|
|
18
18
|
} from "../config/index.ts";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
buildConnectionCard,
|
|
21
|
+
buildConnectionLabel,
|
|
22
|
+
buildStartupNotice,
|
|
23
|
+
formatStatusText,
|
|
24
|
+
type ConnectionCard,
|
|
25
|
+
} from "../status.ts";
|
|
20
26
|
import {
|
|
21
27
|
blockReason,
|
|
22
28
|
formatMutationSummary,
|
|
@@ -416,10 +422,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
416
422
|
"warning",
|
|
417
423
|
);
|
|
418
424
|
} else {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
"info",
|
|
422
|
-
);
|
|
425
|
+
const notice = buildStartupNotice(card, connection.watch);
|
|
426
|
+
ctx.ui.notify(notice.message, notice.level);
|
|
423
427
|
}
|
|
424
428
|
|
|
425
429
|
// Survives /reload, so the transcript keeps saying who pi is acting as.
|
package/src/graph/mappers.ts
CHANGED
|
@@ -23,7 +23,7 @@ import type {
|
|
|
23
23
|
SignedInUser,
|
|
24
24
|
TeamSummary,
|
|
25
25
|
} from "../types.ts";
|
|
26
|
-
import { imageUrlsInHtml } from "../utils/attachments.ts";
|
|
26
|
+
import { collectStrings, imageUrlsInHtml } from "../utils/attachments.ts";
|
|
27
27
|
import { htmlToText } from "../utils/formatting.ts";
|
|
28
28
|
|
|
29
29
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
@@ -227,9 +227,31 @@ function mapAttachments(raw: Raw): MessageAttachment[] {
|
|
|
227
227
|
* `htmlToText` turns every `<img>` into `[image: alt]`, which is right for a
|
|
228
228
|
* reader that only wants prose and wrong for one that could fetch the image:
|
|
229
229
|
* the URL it drops points at Graph.
|
|
230
|
+
*
|
|
231
|
+
* A forwarded message keeps its images out of the outer body. The original
|
|
232
|
+
* travels as an attachment of type `forwardedMessageReference`, with the whole
|
|
233
|
+
* message in its `content` — as a JSON string, and nested again if that one had
|
|
234
|
+
* been forwarded too. Searching every string inside it finds the images without
|
|
235
|
+
* modelling that shape, which would only ever be one Graph change out of date.
|
|
230
236
|
*/
|
|
231
237
|
function mapImageUrls(raw: Raw): string[] {
|
|
232
|
-
|
|
238
|
+
const candidates: string[] = [];
|
|
239
|
+
if (typeof raw.body?.content === "string") candidates.push(raw.body.content);
|
|
240
|
+
|
|
241
|
+
for (const attachment of raw.attachments ?? []) {
|
|
242
|
+
if (typeof attachment?.content !== "string") continue;
|
|
243
|
+
let parsed: unknown;
|
|
244
|
+
try {
|
|
245
|
+
parsed = JSON.parse(attachment.content);
|
|
246
|
+
} catch {
|
|
247
|
+
// Not JSON: search it as it stands rather than dropping it.
|
|
248
|
+
candidates.push(attachment.content);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
collectStrings(parsed, candidates);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return imageUrlsInHtml(candidates.join("\n"));
|
|
233
255
|
}
|
|
234
256
|
|
|
235
257
|
function mapMentions(raw: Raw): PersonRef[] {
|
package/src/graph/media.ts
CHANGED
|
@@ -142,6 +142,11 @@ export async function saveMessageFiles(
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
if (!attachment.contentUrl) {
|
|
145
|
+
// A forwarded message is not a file being withheld: its images and files
|
|
146
|
+
// are hosted content of the message that carries it, and they are fetched
|
|
147
|
+
// above. Listing it as unsaved would put a line of noise under every
|
|
148
|
+
// forwarded message.
|
|
149
|
+
if (attachment.contentType === "forwardedMessageReference") continue;
|
|
145
150
|
skipped.push({ name: label, reason: describeMissing(attachment) });
|
|
146
151
|
continue;
|
|
147
152
|
}
|
package/src/status.ts
CHANGED
|
@@ -90,6 +90,31 @@ export function buildConnectionLabel(card: ConnectionCard | undefined): string {
|
|
|
90
90
|
return `✓ Teams · ${scope}${card.user ? ` · ${card.user}` : ""}${listening}`;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* The line a session starts with.
|
|
95
|
+
*
|
|
96
|
+
* It names listen mode on purpose. The watcher starts from the saved setting, so
|
|
97
|
+
* a session can begin polling — and answering in your name — without the
|
|
98
|
+
* transcript ever saying so, which is how a listener nobody remembers starting
|
|
99
|
+
* turns up, and how it gets to answer before anyone notices.
|
|
100
|
+
*
|
|
101
|
+
* The level is part of the answer: being listened to is the state worth a louder
|
|
102
|
+
* notice, not the ordinary one of it being off.
|
|
103
|
+
*/
|
|
104
|
+
export function buildStartupNotice(
|
|
105
|
+
card: ConnectionCard,
|
|
106
|
+
watch: { enabled: boolean; intervalSeconds: number },
|
|
107
|
+
): { message: string; level: "info" | "warning" } {
|
|
108
|
+
const identity = `@patimweb/pi-teams loaded (${card.account}${card.user ? ` as ${card.user}` : ""}, safety: ${card.safetyLevel})`;
|
|
109
|
+
|
|
110
|
+
if (!watch.enabled) return { message: `${identity} · listen mode off`, level: "info" };
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
message: `${identity} · listen mode ON — every ${watch.intervalSeconds} s, /teams-listen off to stop`,
|
|
114
|
+
level: "warning",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
93
118
|
/** Rich markdown for `/teams-status` and the expanded card. */
|
|
94
119
|
export function formatStatusText(card: ConnectionCard | undefined): string {
|
|
95
120
|
if (!card) {
|
package/src/utils/attachments.ts
CHANGED
|
@@ -11,6 +11,44 @@
|
|
|
11
11
|
* wrong and they can be tested without a Graph call.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Every string inside a value, however it is nested.
|
|
16
|
+
*
|
|
17
|
+
* Exists for the JSON that Graph hands back for a forwarded message: the
|
|
18
|
+
* original message is in there as data of an undocumented depth, and the only
|
|
19
|
+
* thing needed from it is whatever strings it contains.
|
|
20
|
+
*/
|
|
21
|
+
export function collectStrings(value: unknown, out: string[] = [], depth = 0): string[] {
|
|
22
|
+
// A guard against a pathological document, not a real limit.
|
|
23
|
+
if (depth > 8) return out;
|
|
24
|
+
|
|
25
|
+
if (typeof value === "string") {
|
|
26
|
+
out.push(value);
|
|
27
|
+
// A forwarded message inside a forwarded message is JSON stored as a string
|
|
28
|
+
// inside JSON, and its quotes stay escaped until it is parsed. Without this
|
|
29
|
+
// the images two levels down are simply not there to be found.
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
32
|
+
try {
|
|
33
|
+
collectStrings(JSON.parse(trimmed), out, depth + 1);
|
|
34
|
+
} catch {
|
|
35
|
+
/* a string that merely starts with a brace */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
for (const entry of value) collectStrings(entry, out, depth + 1);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
if (value && typeof value === "object") {
|
|
45
|
+
for (const entry of Object.values(value as Record<string, unknown>)) {
|
|
46
|
+
collectStrings(entry, out, depth + 1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
14
52
|
/** Absolute URLs of the inline images in a message body, in the order shown. */
|
|
15
53
|
export function imageUrlsInHtml(html: string): string[] {
|
|
16
54
|
const urls: string[] = [];
|