@wirecat/cli-core 0.363.0 → 0.365.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/CHANGELOG.md +22 -0
- package/dist/cli/messenger/context.d.ts +2 -2
- package/dist/cli/messenger/context.js +6 -2
- package/dist/cli/messenger/flooded.d.ts +2 -1
- package/dist/cli/messenger/flooded.js +32 -12
- package/dist/cli/messenger/replies-command.js +7 -5
- package/dist/cli/messenger/replies-edit-command.js +3 -2
- package/dist/cli/messenger/serve-command.js +3 -2
- package/dist/cli/messenger/serve-relay.d.ts +1 -1
- package/dist/cli/messenger/stored.js +5 -1
- package/dist/replies/rendering.d.ts +2 -1
- package/dist/replies/rendering.js +2 -2
- package/dist/replies/rules.d.ts +6 -1
- package/dist/replies/rules.js +12 -4
- package/dist/replies/serve.d.ts +2 -1
- package/dist/replies/serve.js +2 -2
- package/dist/replies/template.d.ts +3 -0
- package/dist/replies/template.js +14 -2
- package/dist/sends/guard.d.ts +3 -2
- package/dist/sends/guard.js +5 -1
- package/dist/store/sqlite/evidence-findings.js +2 -1
- package/dist/store/sqlite/meeting-findings.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.365.0 — 19.10.2026
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **A findings batch whose evidence changed can still be rejected.** Review rechecked the cited sources even when
|
|
15
|
+
it accepted nothing, so a stale meeting or evidence findings batch could never be closed and stayed open in
|
|
16
|
+
every list. Accepting still requires current evidence.
|
|
17
|
+
|
|
18
|
+
## 0.364.0 — 19.10.2026
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **More warnings in `serve`'s log now carry a code.** A flood wait (`flood_wait`, with `counts.seconds`),
|
|
23
|
+
a long wait for the profile's pace (`pace_wait`, with `counts.seconds`), a held account (`account_frozen`,
|
|
24
|
+
`account_limited`, `account_banned`), a wait that could not be remembered (`wait_not_remembered`), a send missing
|
|
25
|
+
from the send journal (`send_not_journaled`), a recipient list that could not be re-keyed or read
|
|
26
|
+
(`recipients_not_rekeyed`, `recipients_not_read`), the reply audience warnings (`audience_person_on_both_lists`,
|
|
27
|
+
`audience_chat_on_both_lists`, `audience_allow_unused`, `audience_allow_empty`) and the reply template warnings
|
|
28
|
+
(`reply_legacy_placeholders`, `reply_legacy_may_reword`, `reply_template_warning`) each log their code and
|
|
29
|
+
operation instead of a bare `serve.warning`. The record never holds the sentence, a chat id, a name or an error
|
|
30
|
+
message; the sentence at a terminal is unchanged.
|
|
31
|
+
|
|
10
32
|
## 0.363.0 — 19.10.2026
|
|
11
33
|
|
|
12
34
|
### Added
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import type { AdminRight, GroupSettingKey, Id, Message, PollVote, PrivacyKey, Provider } from "../../domain/models.js";
|
|
3
3
|
import type { MessengerAdapter } from "../../ports/messenger.js";
|
|
4
|
-
import type { EventSink } from "../../runs/events.js";
|
|
4
|
+
import type { EventSink, Warn } from "../../runs/events.js";
|
|
5
5
|
import { type Asker, type SendGuard } from "../../sends/guard.js";
|
|
6
6
|
import { type PaceRate } from "../../sends/pace.js";
|
|
7
7
|
import { type PermissionKey } from "../../sends/permissions.js";
|
|
@@ -186,7 +186,7 @@ export interface Messenger extends ServiceMessenger {
|
|
|
186
186
|
* `ask`, when given, answers a write at level `ask` instead of the messenger's own prompt: the MCP
|
|
187
187
|
* server has no terminal to ask at.
|
|
188
188
|
*/
|
|
189
|
-
guard?: (command: Command, settings: Settings, warn:
|
|
189
|
+
guard?: (command: Command, settings: Settings, warn: Warn, ask?: Asker) => SendGuard;
|
|
190
190
|
/** The permission key of one of this CLI's own commands, when `keyForCommand` does not know it. */
|
|
191
191
|
permissionKey?: PermissionKeyOf;
|
|
192
192
|
/** The CLI's SKILL.md, which the MCP server also serves as `<command>://skill`. */
|
|
@@ -169,7 +169,11 @@ const rekeyRecipients = async (app, profile, env, warn, from, to) => {
|
|
|
169
169
|
await new RecipientList(recipientsPathFor(app, profile, env), app.command, REKEY_LOCK_WAIT_MS).rekey(from, to);
|
|
170
170
|
}
|
|
171
171
|
catch (error) {
|
|
172
|
-
warn(`the recipient list still names ${from}: ${error instanceof Error ? error.message : String(error)}
|
|
172
|
+
warn(`the recipient list still names ${from}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
173
|
+
event: "warning",
|
|
174
|
+
code: "recipients_not_rekeyed",
|
|
175
|
+
operation: "recipients.rekey",
|
|
176
|
+
});
|
|
173
177
|
}
|
|
174
178
|
};
|
|
175
179
|
const waitsOf = (command) => {
|
|
@@ -344,7 +348,7 @@ export const messengerContext = (command, messenger, resolving = {}) => {
|
|
|
344
348
|
base.renderer.warn(`permissions.${key} names no command and does nothing — \`config unset permissions.${key}\``);
|
|
345
349
|
}
|
|
346
350
|
// Read when warning, not now: serve swaps the renderer's methods for log records after this.
|
|
347
|
-
const warn = (text) => base.renderer.warn(text);
|
|
351
|
+
const warn = (text, warning) => base.renderer.warn(text, warning);
|
|
348
352
|
const accountFor = () => {
|
|
349
353
|
const account = messenger.accountOf?.(command) ?? recalledAccount(app, provider, profile, base.env);
|
|
350
354
|
if (!account)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type MessengerAdapter } from "../../ports/messenger.js";
|
|
2
|
+
import { type Warn } from "../../runs/events.js";
|
|
2
3
|
import { type FloodMemory } from "../../sends/flood.js";
|
|
3
4
|
import type { Pacer } from "../../sends/pace.js";
|
|
4
5
|
/**
|
|
@@ -9,6 +10,6 @@ import type { Pacer } from "../../sends/pace.js";
|
|
|
9
10
|
*/
|
|
10
11
|
export declare const flooded: (messenger: MessengerAdapter, memory: FloodMemory, { name, warn, pacer }: {
|
|
11
12
|
name: string;
|
|
12
|
-
warn:
|
|
13
|
+
warn: Warn;
|
|
13
14
|
pacer?: Pacer;
|
|
14
15
|
}) => MessengerAdapter;
|
|
@@ -19,12 +19,16 @@ const chatOf = (args) => typeof args[0] === "string" && /^-?\d+$/.test(args[0])
|
|
|
19
19
|
* waits for its turn in the profile's pace, and a wait the messenger asks for holds the whole profile.
|
|
20
20
|
*/
|
|
21
21
|
export const flooded = (messenger, memory, { name, warn, pacer }) => {
|
|
22
|
-
const kept = async (what) => {
|
|
22
|
+
const kept = async (operation, what) => {
|
|
23
23
|
try {
|
|
24
24
|
await what();
|
|
25
25
|
}
|
|
26
26
|
catch (error) {
|
|
27
|
-
warn(`could not remember ${name}'s wait (${error instanceof Error ? error.message : String(error)})
|
|
27
|
+
warn(`could not remember ${name}'s wait (${error instanceof Error ? error.message : String(error)})`, {
|
|
28
|
+
event: "warning",
|
|
29
|
+
code: "wait_not_remembered",
|
|
30
|
+
operation,
|
|
31
|
+
});
|
|
28
32
|
}
|
|
29
33
|
};
|
|
30
34
|
const noted = async (operation, chatId, error) => {
|
|
@@ -33,13 +37,17 @@ export const flooded = (messenger, memory, { name, warn, pacer }) => {
|
|
|
33
37
|
const details = error.details ?? {};
|
|
34
38
|
const standing = details.standing;
|
|
35
39
|
if (standing && BLOCKING.has(standing.state)) {
|
|
36
|
-
await kept(() => {
|
|
40
|
+
await kept(operation, () => {
|
|
37
41
|
const block = memory.block({
|
|
38
42
|
state: standing.state,
|
|
39
43
|
hint: standing.hint ?? error.message,
|
|
40
44
|
...(standing.until ? { until: standing.until } : {}),
|
|
41
45
|
});
|
|
42
|
-
warn(`writes from this profile are held ${holdEnd(block)}: ${block.hint}
|
|
46
|
+
warn(`writes from this profile are held ${holdEnd(block)}: ${block.hint}`, {
|
|
47
|
+
event: "warning",
|
|
48
|
+
code: `account_${block.state}`,
|
|
49
|
+
operation,
|
|
50
|
+
});
|
|
43
51
|
});
|
|
44
52
|
return;
|
|
45
53
|
}
|
|
@@ -47,23 +55,28 @@ export const flooded = (messenger, memory, { name, warn, pacer }) => {
|
|
|
47
55
|
if (error.code !== "rate_limited" || details.remembered === true || !Number.isFinite(waitMs) || waitMs <= 0)
|
|
48
56
|
return;
|
|
49
57
|
const providerError = providerErrorKey(details.provider_error);
|
|
50
|
-
await kept(() => {
|
|
58
|
+
await kept(operation, () => {
|
|
51
59
|
const deadline = memory.remember({
|
|
52
60
|
operation,
|
|
53
61
|
...(chatId ? { chatId } : {}),
|
|
54
62
|
...(providerError ? { providerError } : {}),
|
|
55
63
|
waitMs,
|
|
56
64
|
});
|
|
57
|
-
warn(`${name} asked to wait before ${operation} again — until ${deadline.until}
|
|
65
|
+
warn(`${name} asked to wait before ${operation} again — until ${deadline.until}`, {
|
|
66
|
+
event: "warning",
|
|
67
|
+
code: "flood_wait",
|
|
68
|
+
operation,
|
|
69
|
+
counts: { seconds: Math.ceil(waitMs / 1000) },
|
|
70
|
+
});
|
|
58
71
|
});
|
|
59
72
|
if (pacer)
|
|
60
|
-
await kept(() => pacer.holdFor(waitMs));
|
|
73
|
+
await kept(operation, () => pacer.holdFor(waitMs));
|
|
61
74
|
};
|
|
62
|
-
const turn = async () => {
|
|
75
|
+
const turn = async (operation) => {
|
|
63
76
|
if (!pacer)
|
|
64
77
|
return;
|
|
65
78
|
let at = Date.now();
|
|
66
|
-
await kept(async () => {
|
|
79
|
+
await kept(operation, async () => {
|
|
67
80
|
at = await pacer.reserve();
|
|
68
81
|
});
|
|
69
82
|
const waitMs = at - Date.now();
|
|
@@ -72,8 +85,15 @@ export const flooded = (messenger, memory, { name, warn, pacer }) => {
|
|
|
72
85
|
if (waitMs > LONGEST_PACE_WAIT_MS) {
|
|
73
86
|
throw new CliError("rate_limited", `${name} asked this profile to wait, ${Math.ceil(waitMs / 1000)} s are left — nothing was sent to ${name}`, { retry_after_ms: waitMs, retry_at: new Date(at).toISOString(), remembered: true });
|
|
74
87
|
}
|
|
75
|
-
if (waitMs >= ANNOUNCE_WAIT_MS)
|
|
76
|
-
|
|
88
|
+
if (waitMs >= ANNOUNCE_WAIT_MS) {
|
|
89
|
+
const seconds = Math.ceil(waitMs / 1000);
|
|
90
|
+
warn(`waiting ${seconds} s to keep this profile's pace with ${name}`, {
|
|
91
|
+
event: "warning",
|
|
92
|
+
code: "pace_wait",
|
|
93
|
+
operation,
|
|
94
|
+
counts: { seconds },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
77
97
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
78
98
|
};
|
|
79
99
|
return throughWrapper(messenger, {}, (operation, call) => {
|
|
@@ -101,7 +121,7 @@ export const flooded = (messenger, memory, { name, warn, pacer }) => {
|
|
|
101
121
|
...(owed.providerError ? { provider_error: owed.providerError } : {}),
|
|
102
122
|
});
|
|
103
123
|
}
|
|
104
|
-
await turn();
|
|
124
|
+
await turn(operation);
|
|
105
125
|
try {
|
|
106
126
|
return await call(...args);
|
|
107
127
|
}
|
|
@@ -43,8 +43,9 @@ export const repliesCommand = (messenger) => {
|
|
|
43
43
|
if (rules.length === 0) {
|
|
44
44
|
renderer.note(`no reply rules yet — they live in ${path}`);
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const warn = renderer.warn;
|
|
47
|
+
for (const { text, warning } of audienceWarnings(audience))
|
|
48
|
+
warn(text, warning);
|
|
48
49
|
const found = await context.withStore((store, account) => dryRun(store, account, rules, {
|
|
49
50
|
since,
|
|
50
51
|
audience,
|
|
@@ -103,8 +104,9 @@ export const repliesCommand = (messenger) => {
|
|
|
103
104
|
const { settings, renderer, env } = context;
|
|
104
105
|
const { rules, audience } = readReplies(repliesPathFor(messenger.app, settings.profile, env), messenger.provider);
|
|
105
106
|
const warnings = audienceWarnings(audience);
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
const warn = renderer.warn;
|
|
108
|
+
for (const { text, warning } of warnings)
|
|
109
|
+
warn(text, warning);
|
|
108
110
|
const state = readRepliesState(repliesStatePathFor(messenger.app, settings.profile, env));
|
|
109
111
|
const level = levelFor(settings.permissions ?? {}, "replies.send").level;
|
|
110
112
|
if (level !== "allow")
|
|
@@ -117,7 +119,7 @@ export const repliesCommand = (messenger) => {
|
|
|
117
119
|
allow: audience.allow.people.length + audience.allow.chats.length,
|
|
118
120
|
deny: audience.deny.people.length + audience.deny.chats.length,
|
|
119
121
|
},
|
|
120
|
-
warnings,
|
|
122
|
+
warnings: warnings.map(({ text }) => text),
|
|
121
123
|
rules: rules.map(({ id, on }) => ({ id, on })),
|
|
122
124
|
}));
|
|
123
125
|
});
|
|
@@ -78,8 +78,9 @@ export const addReplyEditors = (replies, messenger) => {
|
|
|
78
78
|
const context = messengerContext(this, messenger);
|
|
79
79
|
const { settings, renderer, env } = context;
|
|
80
80
|
const answer = editAudience(repliesPathFor(messenger.app, settings.profile, env), messenger.provider, this.opts());
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
const warn = renderer.warn;
|
|
82
|
+
for (const { text, warning } of audienceWarnings(answer))
|
|
83
|
+
warn(text, warning);
|
|
83
84
|
renderItem(this, context, answer);
|
|
84
85
|
});
|
|
85
86
|
replies.addCommand(audience);
|
|
@@ -162,9 +162,10 @@ const replying = (context, messenger, since) => {
|
|
|
162
162
|
// A file that does not read is said on each message instead; it must not stop serve.
|
|
163
163
|
try {
|
|
164
164
|
const { rules, audience } = readReplies(repliesPathFor(app, profile, env), provider);
|
|
165
|
+
const warn = renderer.warn;
|
|
165
166
|
if (rules.length > 0)
|
|
166
|
-
for (const warning of audienceWarnings(audience))
|
|
167
|
-
|
|
167
|
+
for (const { text, warning } of audienceWarnings(audience))
|
|
168
|
+
warn(text, warning);
|
|
168
169
|
}
|
|
169
170
|
catch { }
|
|
170
171
|
const handle = async (message) => {
|
|
@@ -85,7 +85,7 @@ export declare const serveRelay: (command: Command, messenger: Messenger) => {
|
|
|
85
85
|
officialStats?: boolean;
|
|
86
86
|
tracksMembers?: boolean;
|
|
87
87
|
speechModels?: readonly string[];
|
|
88
|
-
guard?: (command: Command, settings: import("../settings.js").Settings, warn: (
|
|
88
|
+
guard?: (command: Command, settings: import("../settings.js").Settings, warn: import("../../runs/events.js").Warn, ask?: import("../../sends/guard.js").Asker) => import("../../sends/guard.js").SendGuard;
|
|
89
89
|
permissionKey?: import("../permission-keys.js").PermissionKeyOf;
|
|
90
90
|
skill?: URL;
|
|
91
91
|
diagnose?: (command: Command, context: import("../context.js").BaseContext) => Promise<Record<string, unknown>>;
|
|
@@ -461,7 +461,11 @@ const announceStoredChats = async (messenger, store, account, events, warn, reci
|
|
|
461
461
|
listed = recipientIds?.() ?? [];
|
|
462
462
|
}
|
|
463
463
|
catch (error) {
|
|
464
|
-
warn(`the messenger was not told the recipient list: ${error instanceof Error ? error.message : String(error)}
|
|
464
|
+
warn(`the messenger was not told the recipient list: ${error instanceof Error ? error.message : String(error)}`, {
|
|
465
|
+
event: "warning",
|
|
466
|
+
code: "recipients_not_read",
|
|
467
|
+
operation: "history.stored_chats",
|
|
468
|
+
});
|
|
465
469
|
}
|
|
466
470
|
try {
|
|
467
471
|
const opened = await store();
|
|
@@ -2,6 +2,7 @@ import { type KeyringStore } from "@wirecat/cli-base";
|
|
|
2
2
|
import type { Chat, Message } from "../domain/models.js";
|
|
3
3
|
import { type ModelSettings } from "../models/index.js";
|
|
4
4
|
import type { AppIdentity } from "../ports/app.js";
|
|
5
|
+
import type { Warn } from "../runs/events.js";
|
|
5
6
|
import type { ReplyRule } from "./rules.js";
|
|
6
7
|
import { type TemplateResult } from "./template.js";
|
|
7
8
|
export type ReplyChat = Pick<Chat, "id" | "kind"> & {
|
|
@@ -10,7 +11,7 @@ export type ReplyChat = Pick<Chat, "id" | "kind"> & {
|
|
|
10
11
|
export type ReplyRender = (rule: ReplyRule, message: Message, chat: ReplyChat, now: number) => Promise<TemplateResult>;
|
|
11
12
|
export declare const replyRenderer: (app: AppIdentity, profile: string, settings: () => {
|
|
12
13
|
models?: ModelSettings;
|
|
13
|
-
}, env: NodeJS.ProcessEnv, warn:
|
|
14
|
+
}, env: NodeJS.ProcessEnv, warn: Warn, options?: {
|
|
14
15
|
ai?: boolean;
|
|
15
16
|
preview?: boolean;
|
|
16
17
|
signal?: AbortSignal;
|
|
@@ -2,7 +2,7 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
2
2
|
import { embeddingKeys, endpointKeyName } from "../embeddings/keys.js";
|
|
3
3
|
import { modelGateway, modelTarget } from "../models/index.js";
|
|
4
4
|
import { mayModelReply, replyConsentPathFor, replyModelIdentity } from "./consents.js";
|
|
5
|
-
import { renderReplyTemplate } from "./template.js";
|
|
5
|
+
import { renderReplyTemplate, templateWarning } from "./template.js";
|
|
6
6
|
export const replyRenderer = (app, profile, settings, env, warn, options = {
|
|
7
7
|
ai: true,
|
|
8
8
|
}) => async (rule, message, chat, now) => {
|
|
@@ -48,6 +48,6 @@ export const replyRenderer = (app, profile, settings, env, warn, options = {
|
|
|
48
48
|
},
|
|
49
49
|
});
|
|
50
50
|
for (const warning of result.warnings)
|
|
51
|
-
warn(warning);
|
|
51
|
+
warn(warning, templateWarning(warning));
|
|
52
52
|
return result;
|
|
53
53
|
};
|
package/dist/replies/rules.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
2
|
import type { AppIdentity } from "../ports/app.js";
|
|
3
|
+
import type { WarningEvent } from "../runs/events.js";
|
|
3
4
|
export declare const REPLY_ACTIONS: readonly ["reply", "task"];
|
|
4
5
|
export declare const REPLY_MODELS: readonly ["fill-only", "may-reword"];
|
|
5
6
|
export type ReplyAction = (typeof REPLY_ACTIONS)[number];
|
|
@@ -199,8 +200,12 @@ export interface Replies {
|
|
|
199
200
|
export declare const EVERYONE: Audience;
|
|
200
201
|
/** Why nobody may be answered here, or `null` when the audience lets the reply go. */
|
|
201
202
|
export declare const outsideAudience: (audience: Audience, person: string | null, chat: string) => string | null;
|
|
203
|
+
export interface AudienceWarning {
|
|
204
|
+
text: string;
|
|
205
|
+
warning: WarningEvent;
|
|
206
|
+
}
|
|
202
207
|
/** What in the audience does not do what it seems to say — said, never refused. */
|
|
203
|
-
export declare const audienceWarnings: ({ reply, allow, deny }: Audience) =>
|
|
208
|
+
export declare const audienceWarnings: ({ reply, allow, deny }: Audience) => AudienceWarning[];
|
|
204
209
|
/** The owner's rules, in file order. No file is no rules; a file that does not check out refuses, naming the field. */
|
|
205
210
|
export declare const readReplyRules: (path: string) => ReplyRule[];
|
|
206
211
|
/** `provider`: the messenger reading the file, which an older file's `testers` may have named. */
|
package/dist/replies/rules.js
CHANGED
|
@@ -112,19 +112,27 @@ export const outsideAudience = (audience, person, chat) => {
|
|
|
112
112
|
const listed = (person !== null && audience.allow.people.includes(person)) || audience.allow.chats.includes(chat);
|
|
113
113
|
return listed ? null : "not on the allow list";
|
|
114
114
|
};
|
|
115
|
+
const audienceWarning = (code, text) => ({
|
|
116
|
+
text,
|
|
117
|
+
warning: { event: "warning", code, operation: "replies.audience" },
|
|
118
|
+
});
|
|
115
119
|
/** What in the audience does not do what it seems to say — said, never refused. */
|
|
116
120
|
export const audienceWarnings = ({ reply, allow, deny }) => [
|
|
117
121
|
...allow.people
|
|
118
122
|
.filter((id) => deny.people.includes(id))
|
|
119
|
-
.map((id) => `person ${id} is on both lists: deny wins, nobody answers them`),
|
|
123
|
+
.map((id) => audienceWarning("audience_person_on_both_lists", `person ${id} is on both lists: deny wins, nobody answers them`)),
|
|
120
124
|
...allow.chats
|
|
121
125
|
.filter((id) => deny.chats.includes(id))
|
|
122
|
-
.map((id) => `chat ${id} is on both lists: deny wins, nothing is answered there`),
|
|
126
|
+
.map((id) => audienceWarning("audience_chat_on_both_lists", `chat ${id} is on both lists: deny wins, nothing is answered there`)),
|
|
123
127
|
...(reply === "all" && allow.people.length + allow.chats.length > 0
|
|
124
|
-
? [
|
|
128
|
+
? [
|
|
129
|
+
audienceWarning("audience_allow_unused", 'the allow list does nothing while audience.reply is "all" — set it to "listed" to answer only those'),
|
|
130
|
+
]
|
|
125
131
|
: []),
|
|
126
132
|
...(reply === "listed" && allow.people.length + allow.chats.length === 0
|
|
127
|
-
? [
|
|
133
|
+
? [
|
|
134
|
+
audienceWarning("audience_allow_empty", 'audience.reply is "listed" and the allow list is empty: nobody is answered'),
|
|
135
|
+
]
|
|
128
136
|
: []),
|
|
129
137
|
];
|
|
130
138
|
/** The owner's rules, in file order. No file is no rules; a file that does not check out refuses, naming the field. */
|
package/dist/replies/serve.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Id, Message } from "../domain/models.js";
|
|
2
|
+
import type { Warn } from "../runs/events.js";
|
|
2
3
|
import type { ReplyChat, ReplyRender } from "./rendering.js";
|
|
3
4
|
export declare const NOT_ALLOWED = "replies.send is not allow";
|
|
4
5
|
export declare const NO_RULES = "no rules";
|
|
@@ -27,7 +28,7 @@ export interface Replier {
|
|
|
27
28
|
allowed: () => boolean;
|
|
28
29
|
chatOf: (chat: Id) => Promise<ReplyChat>;
|
|
29
30
|
render?: ReplyRender;
|
|
30
|
-
warn?:
|
|
31
|
+
warn?: Warn;
|
|
31
32
|
senderOf: (person: Id) => Promise<{
|
|
32
33
|
isBot: boolean;
|
|
33
34
|
isContact: boolean;
|
package/dist/replies/serve.js
CHANGED
|
@@ -2,7 +2,7 @@ import { codeOf } from "../sends/guarded.js";
|
|
|
2
2
|
import { decide } from "./decide.js";
|
|
3
3
|
import { outsideAudience, readReplies } from "./rules.js";
|
|
4
4
|
import { readRepliesState, recordReply, writeRepliesState } from "./state.js";
|
|
5
|
-
import { renderReplyTemplate } from "./template.js";
|
|
5
|
+
import { renderReplyTemplate, templateWarning } from "./template.js";
|
|
6
6
|
export const NOT_ALLOWED = "replies.send is not allow";
|
|
7
7
|
export const NO_RULES = "no rules";
|
|
8
8
|
export const ONLY_TASK = "opened a task, sent nothing";
|
|
@@ -69,7 +69,7 @@ export const replyTo = async (deps, message) => {
|
|
|
69
69
|
data: message.text,
|
|
70
70
|
});
|
|
71
71
|
for (const warning of rendered.warnings)
|
|
72
|
-
deps.warn?.(warning);
|
|
72
|
+
deps.warn?.(warning, templateWarning(warning));
|
|
73
73
|
if (rendered.text === null)
|
|
74
74
|
return { skip: rendered.reason ?? "reply template produced no reply", ...opened };
|
|
75
75
|
// Pause and consent can be revoked while an async model request is in flight.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WarningEvent } from "../runs/events.js";
|
|
1
2
|
import type { ReplyRule } from "./rules.js";
|
|
2
3
|
export declare const TEMPLATE_LIMITS: {
|
|
3
4
|
parse: number;
|
|
@@ -34,4 +35,6 @@ export interface TemplateInput {
|
|
|
34
35
|
}>;
|
|
35
36
|
limits?: Partial<typeof TEMPLATE_LIMITS>;
|
|
36
37
|
}
|
|
38
|
+
/** A rendering warning as a log keeps it: a renderer a CLI hands in may warn in words this module does not know. */
|
|
39
|
+
export declare const templateWarning: (text: string) => WarningEvent;
|
|
37
40
|
export declare const renderReplyTemplate: (reply: ReplyRule["reply"], input: TemplateInput) => Promise<TemplateResult>;
|
package/dist/replies/template.js
CHANGED
|
@@ -9,6 +9,18 @@ export const TEMPLATE_LIMITS = {
|
|
|
9
9
|
aiOutput: 1_024,
|
|
10
10
|
calls: 4,
|
|
11
11
|
};
|
|
12
|
+
const LEGACY_PLACEHOLDERS = "legacy reply placeholders: use {{ sender.firstName }} and {{ sender.name }}";
|
|
13
|
+
const LEGACY_REWORD = "legacy may-reword: use an ai block with an explicit fallback";
|
|
14
|
+
const WARNING_CODES = {
|
|
15
|
+
[LEGACY_PLACEHOLDERS]: "reply_legacy_placeholders",
|
|
16
|
+
[LEGACY_REWORD]: "reply_legacy_may_reword",
|
|
17
|
+
};
|
|
18
|
+
/** A rendering warning as a log keeps it: a renderer a CLI hands in may warn in words this module does not know. */
|
|
19
|
+
export const templateWarning = (text) => ({
|
|
20
|
+
event: "warning",
|
|
21
|
+
code: WARNING_CODES[text] ?? "reply_template_warning",
|
|
22
|
+
operation: "replies.render",
|
|
23
|
+
});
|
|
12
24
|
export const renderReplyTemplate = async (reply, input) => {
|
|
13
25
|
const limits = { ...TEMPLATE_LIMITS, ...input.limits };
|
|
14
26
|
const blocks = [];
|
|
@@ -16,11 +28,11 @@ export const renderReplyTemplate = async (reply, input) => {
|
|
|
16
28
|
let source = reply.template;
|
|
17
29
|
const legacy = /(?<!\{)\{(firstName|name)\}(?!\})/g;
|
|
18
30
|
if (legacy.test(source)) {
|
|
19
|
-
warnings.push(
|
|
31
|
+
warnings.push(LEGACY_PLACEHOLDERS);
|
|
20
32
|
source = source.replace(legacy, (_, name) => `{{ sender.${name} }}`);
|
|
21
33
|
}
|
|
22
34
|
if (reply.model === "may-reword") {
|
|
23
|
-
warnings.push(
|
|
35
|
+
warnings.push(LEGACY_REWORD);
|
|
24
36
|
if (!/\{%[-]?\s*ai\b/.test(source))
|
|
25
37
|
source = `{% ai %}${source}{% else %}${source}{% endai %}`;
|
|
26
38
|
}
|
package/dist/sends/guard.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import type { Id } from "../domain/models.js";
|
|
3
3
|
import { type AppIdentity } from "../ports/app.js";
|
|
4
|
+
import type { Warn } from "../runs/events.js";
|
|
4
5
|
import { FloodMemory } from "./flood.js";
|
|
5
6
|
import { type AccountAction, type ChatAction, type SendEntry, SendJournal, type SendKind } from "./journal.js";
|
|
6
7
|
import { type Level, type PermissionKey } from "./permissions.js";
|
|
@@ -66,7 +67,7 @@ export interface SendGuardOptions {
|
|
|
66
67
|
recipients: RecipientList;
|
|
67
68
|
/** Where a messenger's "this account may not write" is remembered; without it nothing is held. */
|
|
68
69
|
flood?: FloodMemory;
|
|
69
|
-
warn:
|
|
70
|
+
warn: Warn;
|
|
70
71
|
now?: () => Date;
|
|
71
72
|
}
|
|
72
73
|
/** A write the profile's level refuses, worded alike by the guard and by the check before connecting. */
|
|
@@ -96,7 +97,7 @@ export interface GuardSettings {
|
|
|
96
97
|
* The guard a profile's configuration asks for — the command's, and a background server's for every
|
|
97
98
|
* write it forwards. Built per request there, so `config set permissions.messages readonly` needs no restart.
|
|
98
99
|
*/
|
|
99
|
-
export declare const guardFor: (app: AppIdentity, settings: GuardSettings, warn:
|
|
100
|
+
export declare const guardFor: (app: AppIdentity, settings: GuardSettings, warn: Warn, env?: NodeJS.ProcessEnv, ask?: Asker) => SendGuard;
|
|
100
101
|
/**
|
|
101
102
|
* Over a background server, the server journals what it forwards, with the outcome it saw; the command
|
|
102
103
|
* writes only the refusals of its own check, which never reached the server.
|
package/dist/sends/guard.js
CHANGED
|
@@ -227,7 +227,11 @@ export const sendGuard = ({ profile, recipients, command = recipients.command, r
|
|
|
227
227
|
journal.append({ at: now().toISOString(), profile, ...kept, ...(settles ? { reservation: settles } : {}) });
|
|
228
228
|
}
|
|
229
229
|
catch (error) {
|
|
230
|
-
warn(`this send is not in the send journal: ${journalFailure(error)}
|
|
230
|
+
warn(`this send is not in the send journal: ${journalFailure(error)}`, {
|
|
231
|
+
event: "warning",
|
|
232
|
+
code: "send_not_journaled",
|
|
233
|
+
operation: "sends.journal",
|
|
234
|
+
});
|
|
231
235
|
}
|
|
232
236
|
},
|
|
233
237
|
};
|
|
@@ -152,7 +152,8 @@ export const evidenceFindingsStoreOver = (context) => {
|
|
|
152
152
|
return batch;
|
|
153
153
|
}
|
|
154
154
|
const input = proposal.payload;
|
|
155
|
-
|
|
155
|
+
// Rejecting creates nothing, so a batch whose evidence changed can still be closed.
|
|
156
|
+
if (accepted.length && snapshot(batch.input) !== input.snapshotHash)
|
|
156
157
|
return stale();
|
|
157
158
|
if (proposal.proposedBy.type !== "bot" || !proposal.proposedBy.name)
|
|
158
159
|
return invalid("a findings author must be an agent");
|
|
@@ -182,7 +182,8 @@ export const meetingFindingsStoreOver = (context) => {
|
|
|
182
182
|
return batch;
|
|
183
183
|
}
|
|
184
184
|
const input = proposal.payload;
|
|
185
|
-
|
|
185
|
+
// Rejecting creates nothing, so a batch whose evidence changed can still be closed.
|
|
186
|
+
if (accepted.length && snapshot(account, batch.input) !== input.snapshotHash)
|
|
186
187
|
return stale();
|
|
187
188
|
if (proposal.proposedBy.type !== "bot" || !proposal.proposedBy.name)
|
|
188
189
|
return invalid("a findings author must be an agent");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.365.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|