@patimweb/pi-teams 1.7.2 → 1.8.0
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 +13 -4
- package/package.json +1 -1
- package/src/config/index.ts +11 -0
- package/src/extension/index.ts +32 -10
- package/src/status.ts +28 -0
- package/src/tools/teams-watch.ts +5 -2
package/README.md
CHANGED
|
@@ -51,9 +51,16 @@ into a prompt it answers — as you, in the chat it arrived in.
|
|
|
51
51
|
/teams-listen off
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
Listen mode never starts on its own. It belongs to the session it was switched
|
|
55
|
+
on in and ends with it: quitting pi closes the watcher, and `/new` or `/resume`
|
|
56
|
+
stops it too. A stored `enabled: true` is a setting, not an instruction — set
|
|
57
|
+
`watch.autoStart` if a session is meant to start listening by itself, which is
|
|
58
|
+
only sensible for an unattended one.
|
|
59
|
+
|
|
54
60
|
```json
|
|
55
61
|
"watch": {
|
|
56
|
-
"enabled":
|
|
62
|
+
"enabled": false,
|
|
63
|
+
"autoStart": false,
|
|
57
64
|
"intervalSeconds": 60,
|
|
58
65
|
"chats": ["Anna*", "Vertrieb*"],
|
|
59
66
|
"from": ["anna.schmidt@contoso.com"],
|
|
@@ -65,7 +72,8 @@ into a prompt it answers — as you, in the chat it arrived in.
|
|
|
65
72
|
|
|
66
73
|
| Key | Default | Meaning |
|
|
67
74
|
|-----|---------|---------|
|
|
68
|
-
| `enabled` | `false` | whether
|
|
75
|
+
| `enabled` | `false` | whether listen mode is switched on |
|
|
76
|
+
| `autoStart` | `false` | whether a pi session may start the watcher without being asked |
|
|
69
77
|
| `intervalSeconds` | `60` | seconds between polls (minimum 15) |
|
|
70
78
|
| `chats` | `[]` | **where** pi listens: glob patterns matched against topic, label, chat ID and participant names. Empty means every recent chat. |
|
|
71
79
|
| `from` | `[]` | **who** pi listens to: glob patterns matched against display name, UPN and e-mail. Empty means any sender. |
|
|
@@ -603,8 +611,9 @@ teams_send_channel_message:
|
|
|
603
611
|
|
|
604
612
|
Both the config and the token cache are written through a private temp file and
|
|
605
613
|
an atomic rename, so an interrupted write cannot truncate them. The watch
|
|
606
|
-
cursor is what
|
|
607
|
-
one re-examination of the chats in the list — it is not
|
|
614
|
+
cursor is what keeps listen mode from re-answering what it has already seen, and
|
|
615
|
+
deleting it only costs one re-examination of the chats in the list — it is not
|
|
616
|
+
silently re-answering
|
|
608
617
|
anything, because the decision also asks Teams whether the message is still
|
|
609
618
|
unread. To revoke pi's access entirely, run `teams_logout` with `all: true` and
|
|
610
619
|
remove the app's consent in Entra ID.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patimweb/pi-teams",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
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/config/index.ts
CHANGED
|
@@ -99,6 +99,14 @@ export interface TenantConfig {
|
|
|
99
99
|
export interface WatchConfig {
|
|
100
100
|
/** Whether the watcher runs at all (default: false) */
|
|
101
101
|
enabled?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Whether a pi session may start the watcher by itself (default: false).
|
|
104
|
+
*
|
|
105
|
+
* Off means listen mode only ever starts when it is switched on in that
|
|
106
|
+
* session. A stored setting is not a decision: an agent that answers in your
|
|
107
|
+
* name should not begin doing so because a file says it may.
|
|
108
|
+
*/
|
|
109
|
+
autoStart?: boolean;
|
|
102
110
|
/** Seconds between polls (default: 60, minimum: 15) */
|
|
103
111
|
intervalSeconds?: number;
|
|
104
112
|
/**
|
|
@@ -126,6 +134,7 @@ export interface WatchConfig {
|
|
|
126
134
|
/** A watch config after every default and clamp has been applied. */
|
|
127
135
|
export interface ResolvedWatchConfig {
|
|
128
136
|
enabled: boolean;
|
|
137
|
+
autoStart: boolean;
|
|
129
138
|
intervalSeconds: number;
|
|
130
139
|
chats: string[];
|
|
131
140
|
from: string[];
|
|
@@ -309,6 +318,7 @@ const DEFAULTS = {
|
|
|
309
318
|
*/
|
|
310
319
|
export const WATCH_DEFAULTS: ResolvedWatchConfig = {
|
|
311
320
|
enabled: false,
|
|
321
|
+
autoStart: false,
|
|
312
322
|
intervalSeconds: 60,
|
|
313
323
|
chats: [],
|
|
314
324
|
from: [],
|
|
@@ -721,6 +731,7 @@ export function resolveWatchConfig(
|
|
|
721
731
|
|
|
722
732
|
return {
|
|
723
733
|
enabled: pick("enabled") ?? WATCH_DEFAULTS.enabled,
|
|
734
|
+
autoStart: pick("autoStart") ?? WATCH_DEFAULTS.autoStart,
|
|
724
735
|
intervalSeconds: clamp(pick("intervalSeconds"), WATCH_BOUNDS.intervalSeconds, WATCH_DEFAULTS.intervalSeconds),
|
|
725
736
|
chats: normalizePatterns(pick("chats")),
|
|
726
737
|
from: normalizePatterns(pick("from")),
|
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,
|
|
@@ -167,12 +173,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
167
173
|
};
|
|
168
174
|
|
|
169
175
|
/**
|
|
170
|
-
* Start the watcher for the session's account
|
|
176
|
+
* Start the watcher for the session's account — but only when it was asked for.
|
|
177
|
+
*
|
|
178
|
+
* `explicit` is the whole safety property. Switching listen mode on is a
|
|
179
|
+
* decision, so only the two places the user makes it — `/teams-listen on` and
|
|
180
|
+
* `teams_watch action: enable` — pass `explicit: true`. Everything else may
|
|
181
|
+
* only stop the loop. Without that split, "re-read the config" would be a way
|
|
182
|
+
* for a stored setting to switch itself back on, which is precisely how a
|
|
183
|
+
* listener nobody started turns up, polling and answering in your name.
|
|
171
184
|
*
|
|
172
185
|
* Restarting is the same call: the old loop is stopped first, so a config
|
|
173
186
|
* change cannot leave two pollers running.
|
|
174
187
|
*/
|
|
175
|
-
const startWatch = (ctx: any) => {
|
|
188
|
+
const startWatch = (ctx: any, options: { explicit?: boolean } = {}) => {
|
|
176
189
|
const conn = refresh();
|
|
177
190
|
|
|
178
191
|
// Already watching this exact configuration: leave the loop alone, so its
|
|
@@ -183,6 +196,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
183
196
|
stopWatch();
|
|
184
197
|
|
|
185
198
|
if (!conn?.watch.enabled) return;
|
|
199
|
+
if (!options.explicit) return;
|
|
186
200
|
|
|
187
201
|
reportedWatchError = undefined;
|
|
188
202
|
const loop = startWatchLoop({
|
|
@@ -317,7 +331,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
317
331
|
// Written to the account, not globally: the watcher listens as one
|
|
318
332
|
// identity, and "which account" must not be a guess.
|
|
319
333
|
const watch = setWatchConfig({ enabled: action === "on" }, conn.account);
|
|
320
|
-
startWatch(ctx);
|
|
334
|
+
startWatch(ctx, { explicit: true });
|
|
321
335
|
paintStatus(ctx);
|
|
322
336
|
ctx.ui.notify(
|
|
323
337
|
watch.enabled
|
|
@@ -407,8 +421,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
407
421
|
const card = buildConnectionCard(connection);
|
|
408
422
|
if (!card) return;
|
|
409
423
|
|
|
424
|
+
// A stored setting is not a decision. A session starts the watcher only when
|
|
425
|
+
// the configuration opts into it; otherwise listen mode waits until it is
|
|
426
|
+
// switched on in this session with `/teams-listen on`.
|
|
427
|
+
if (connection.watch.autoStart) startWatch(ctx, { explicit: true });
|
|
428
|
+
|
|
410
429
|
paintStatus(ctx);
|
|
411
|
-
startWatch(ctx);
|
|
412
430
|
|
|
413
431
|
if (!card.signedIn) {
|
|
414
432
|
ctx.ui.notify(
|
|
@@ -416,10 +434,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
416
434
|
"warning",
|
|
417
435
|
);
|
|
418
436
|
} else {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
);
|
|
437
|
+
const notice = buildStartupNotice(card, {
|
|
438
|
+
listening: !!watchLoop?.status().running,
|
|
439
|
+
intervalSeconds: connection.watch.intervalSeconds,
|
|
440
|
+
});
|
|
441
|
+
ctx.ui.notify(notice.message, notice.level);
|
|
423
442
|
}
|
|
424
443
|
|
|
425
444
|
// Survives /reload, so the transcript keeps saying who pi is acting as.
|
|
@@ -535,7 +554,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
535
554
|
// errors; leaving it running would be noise, not service.
|
|
536
555
|
stopWatch();
|
|
537
556
|
} else if (WATCH_CONFIG_TOOLS.has(event.toolName)) {
|
|
538
|
-
|
|
557
|
+
// Only `enable` is a decision. A status call, or a re-read after
|
|
558
|
+
// teams_setup, may stop the loop but never start it.
|
|
559
|
+
const action = (event.input as { action?: string } | undefined)?.action;
|
|
560
|
+
startWatch(ctx, { explicit: action === "enable" });
|
|
539
561
|
}
|
|
540
562
|
|
|
541
563
|
paintStatus(ctx);
|
package/src/status.ts
CHANGED
|
@@ -90,6 +90,34 @@ 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. A session can be listening — polling, and
|
|
97
|
+
* answering in your name — so the transcript should never be silent about it.
|
|
98
|
+
*
|
|
99
|
+
* The level is part of the answer: being listened to is the state worth a louder
|
|
100
|
+
* notice, not the ordinary one of it being off.
|
|
101
|
+
*/
|
|
102
|
+
export function buildStartupNotice(
|
|
103
|
+
card: ConnectionCard,
|
|
104
|
+
watch: { listening: boolean; intervalSeconds: number },
|
|
105
|
+
): { message: string; level: "info" | "warning" } {
|
|
106
|
+
const identity = `@patimweb/pi-teams loaded (${card.account}${card.user ? ` as ${card.user}` : ""}, safety: ${card.safetyLevel})`;
|
|
107
|
+
|
|
108
|
+
// Reported from what is actually running, not from the stored setting: those
|
|
109
|
+
// two are no longer the same thing, and a notice that reported the file
|
|
110
|
+
// instead of the loop would be the very confusion it exists to prevent.
|
|
111
|
+
if (!watch.listening) {
|
|
112
|
+
return { message: `${identity} · listen mode off — /teams-listen on to start`, level: "info" };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
message: `${identity} · listen mode ON — every ${watch.intervalSeconds} s, /teams-listen off to stop`,
|
|
117
|
+
level: "warning",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
93
121
|
/** Rich markdown for `/teams-status` and the expanded card. */
|
|
94
122
|
export function formatStatusText(card: ConnectionCard | undefined): string {
|
|
95
123
|
if (!card) {
|
package/src/tools/teams-watch.ts
CHANGED
|
@@ -53,6 +53,7 @@ const CURSOR_NOTES = [
|
|
|
53
53
|
/** Renders the effective settings the same way the config file would. */
|
|
54
54
|
function describe(watch: {
|
|
55
55
|
enabled: boolean;
|
|
56
|
+
autoStart: boolean;
|
|
56
57
|
intervalSeconds: number;
|
|
57
58
|
chats: string[];
|
|
58
59
|
from: string[];
|
|
@@ -62,6 +63,7 @@ function describe(watch: {
|
|
|
62
63
|
}): string {
|
|
63
64
|
return [
|
|
64
65
|
`- enabled: ${watch.enabled}`,
|
|
66
|
+
`- starts with a session: ${watch.autoStart}`,
|
|
65
67
|
`- poll every: ${watch.intervalSeconds} s`,
|
|
66
68
|
`- chats: ${watch.chats.length > 0 ? watch.chats.map((c) => `\`${c}\``).join(", ") : "every chat with recent activity"}`,
|
|
67
69
|
`- people: ${watch.from.length > 0 ? watch.from.map((p) => `\`${p}\``).join(", ") : "any sender"}`,
|
|
@@ -82,8 +84,9 @@ function runtimeLines(runtime: WatchRuntimeStatus | undefined, conn: TeamsConnec
|
|
|
82
84
|
return [
|
|
83
85
|
"Nothing is polling in this session.",
|
|
84
86
|
"",
|
|
85
|
-
"Listen mode
|
|
86
|
-
+ "
|
|
87
|
+
"Listen mode starts only when it is switched on here — `/teams-listen on`, or "
|
|
88
|
+
+ "`teams_watch action: enable`. A stored `enabled: true` does not start it by itself: "
|
|
89
|
+
+ "set `watch.autoStart` if a session is meant to start listening on its own.",
|
|
87
90
|
];
|
|
88
91
|
}
|
|
89
92
|
|