loopengine 0.1.86 → 0.1.87
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 +6 -3
- package/dist/core/messaging/engine.d.ts +5 -0
- package/dist/core/messaging/engine.js +58 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/web/agents-config-page.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -185,9 +185,12 @@ only their own message to tell whether it's a decision, and anything
|
|
|
185
185
|
unclear is treated as a question. An approver can also change a request
|
|
186
186
|
first ("make it $40"): the change is checked against the tool's schema
|
|
187
187
|
and the permission rules and shown with **Approve as changed / Keep
|
|
188
|
-
original / Deny**, and nothing runs until they approve it. A
|
|
189
|
-
|
|
190
|
-
a
|
|
188
|
+
original / Deny**, and nothing runs until they approve it. A plain "yes"
|
|
189
|
+
("approve", "go ahead", "好的", "はい", "sí", 👍…: the whole message,
|
|
190
|
+
matched against a fixed list of phrases in common languages, not by a
|
|
191
|
+
model) runs it; any other reply read as approval ("sure, send
|
|
192
|
+
it", "yes but check the amount") shows exactly what will run with a
|
|
193
|
+
**Confirm** button, and only that click runs it. Arguments are shown
|
|
191
194
|
as labelled fields, with long text such as an email body quoted, not as
|
|
192
195
|
JSON. Once approvers are listed, only their button clicks count too. The same tab lists every decision
|
|
193
196
|
still waiting with Approve/Deny buttons (its count shows on the tab), and
|
|
@@ -55,6 +55,11 @@ export interface DecisionOutcome {
|
|
|
55
55
|
/** Handles one person's message: a reply in a request's thread gets an
|
|
56
56
|
* answer; anything else is left alone. */
|
|
57
57
|
export declare function handleInboundMessage(message: InboundMessage, deps: MessagingDeps): Promise<void>;
|
|
58
|
+
/** Whether a reply is a plain approval: lower-cased, a leading @mention,
|
|
59
|
+
* emoji skin tones and "please" dropped, punctuation and spaces
|
|
60
|
+
* (full-width ones too) ignored, then compared with the fixed list. A
|
|
61
|
+
* trailing question mark keeps it from matching ("yes?"). */
|
|
62
|
+
export declare function isPlainApproval(text: string): boolean;
|
|
58
63
|
/** A click on one of the engine's own buttons (Approve as changed, Keep
|
|
59
64
|
* original, Confirm, Cancel) — the caller has already checked the
|
|
60
65
|
* person may decide. Returns what the clicked message should now say. */
|
|
@@ -94,6 +94,12 @@ export async function handleInboundMessage(message, deps) {
|
|
|
94
94
|
}
|
|
95
95
|
if (verdict.decision === 'approve') {
|
|
96
96
|
const args = link.proposedArgs ?? item.args;
|
|
97
|
+
// A plain "yes" runs it; anything less clear that still reads as
|
|
98
|
+
// approval asks for a Confirm click first.
|
|
99
|
+
if (isPlainApproval(message.text)) {
|
|
100
|
+
await runApproved(deps, link, item.tool, args, who);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
97
103
|
await connector.postChoice(message.conversation, threadTs, `Confirm to run \`${item.tool}\` with:\n${formatArgs(args, connector.escape, await deps.toolSchema(item.tool))}`, [
|
|
98
104
|
{ action: 'confirm', label: 'Confirm', style: 'primary', pendingId: link.pendingId },
|
|
99
105
|
{ action: 'cancel_confirm', label: 'Cancel', pendingId: link.pendingId },
|
|
@@ -133,6 +139,58 @@ export async function handleInboundMessage(message, deps) {
|
|
|
133
139
|
await say(reply);
|
|
134
140
|
});
|
|
135
141
|
}
|
|
142
|
+
// Whole messages that approve and nothing else, in the languages people
|
|
143
|
+
// most often reply in. Matched exactly after the clean-up in
|
|
144
|
+
// isPlainApproval, so a condition or a second thought ("yes but…",
|
|
145
|
+
// "好的,但是…") never counts as plain; a phrase that isn't here just gets
|
|
146
|
+
// a Confirm click. Words that can also mean "hmm", "understood" or "no
|
|
147
|
+
// thanks" (嗯, 了解, いいです) are left out on purpose.
|
|
148
|
+
const PLAIN_APPROVAL_PHRASES = [
|
|
149
|
+
// English
|
|
150
|
+
'yes', 'y', 'yep', 'yeah', 'yup', 'sure', 'yes go ahead', 'go ahead', 'approve', 'approved', 'i approve', 'do it', 'yes do it',
|
|
151
|
+
'ok', 'okay', 'ok go ahead', 'lgtm', 'confirm', 'confirmed', 'send it', 'run it',
|
|
152
|
+
// Chinese (simplified and traditional)
|
|
153
|
+
'是', '是的', '好', '好的', '可以', '行', '同意', '批准', '确认', '確認', '没问题', '沒問題', '发吧', '發吧', '执行', '執行', '执行吧', '執行吧',
|
|
154
|
+
'请执行', '請執行', '好的发吧', '好的發吧', '可以执行', '可以執行', '同意执行', '同意執行',
|
|
155
|
+
// Japanese
|
|
156
|
+
'はい', '承認', '承認します', 'お願いします', 'はいお願いします', 'okです', '進めて', '進めてください', '実行してください',
|
|
157
|
+
// Korean
|
|
158
|
+
'네', '예', '승인', '승인합니다', '좋아요', '진행해주세요', '네진행해주세요',
|
|
159
|
+
// Spanish, Portuguese, Italian
|
|
160
|
+
'sí', 'si', 'aprobado', 'apruebo', 'adelante', 'sí adelante', 'dale', 'de acuerdo',
|
|
161
|
+
'sim', 'aprovado', 'aprovo', 'pode', 'pode mandar', 'manda', 'pode seguir',
|
|
162
|
+
'sì', 'approvato', 'approvo', 'vai', 'procedi', 'va bene',
|
|
163
|
+
// French, German
|
|
164
|
+
'oui', 'approuvé', "j'approuve", 'vas-y', 'allez-y', "d'accord", 'ok vas-y',
|
|
165
|
+
'ja', 'genehmigt', 'freigegeben', 'mach es', 'mach das', 'einverstanden', 'passt',
|
|
166
|
+
// Russian
|
|
167
|
+
'да', 'одобряю', 'одобрено', 'подтверждаю', 'давай',
|
|
168
|
+
// Emoji, typed or as Slack shortcodes
|
|
169
|
+
'👍', '✅', '👌', ':+1:', ':thumbsup:', ':white_check_mark:', ':heavy_check_mark:', ':ok_hand:',
|
|
170
|
+
];
|
|
171
|
+
// "Please" at either end of a reply doesn't change it.
|
|
172
|
+
const PLEASE = /^(please|pls|por favor|bitte|svp|s'il vous plaît|per favore)\s+|\s+(please|pls|por favor|bitte|svp|s'il vous plaît|per favore)$/g;
|
|
173
|
+
function approvalKey(text) {
|
|
174
|
+
return text
|
|
175
|
+
.toLowerCase()
|
|
176
|
+
.replace(/^\s*<@[a-z0-9]+>\s*/, '')
|
|
177
|
+
.replace(/:skin-tone-\d:/g, '')
|
|
178
|
+
.replace(/[\u{1F3FB}-\u{1F3FF}\u{FE0F}]/gu, '')
|
|
179
|
+
.replace(/[\u2018\u2019]/g, "'")
|
|
180
|
+
.replace(/[,,、;;]+/g, ' ')
|
|
181
|
+
.replace(/[.!?。!~~]+$/, (m) => (m.includes('?') ? '?' : ''))
|
|
182
|
+
.trim()
|
|
183
|
+
.replace(PLEASE, '')
|
|
184
|
+
.replace(/\s+/g, '');
|
|
185
|
+
}
|
|
186
|
+
const PLAIN_APPROVALS = new Set(PLAIN_APPROVAL_PHRASES.map(approvalKey));
|
|
187
|
+
/** Whether a reply is a plain approval: lower-cased, a leading @mention,
|
|
188
|
+
* emoji skin tones and "please" dropped, punctuation and spaces
|
|
189
|
+
* (full-width ones too) ignored, then compared with the fixed list. A
|
|
190
|
+
* trailing question mark keeps it from matching ("yes?"). */
|
|
191
|
+
export function isPlainApproval(text) {
|
|
192
|
+
return PLAIN_APPROVALS.has(approvalKey(text));
|
|
193
|
+
}
|
|
136
194
|
/** Says what will run, runs it, reports the outcome and updates the
|
|
137
195
|
* request message. `args` differing from the request's own means it was
|
|
138
196
|
* approved as changed. */
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { listDeliveries, recordDelivery, type NotifierDelivery } from '#core/not
|
|
|
12
12
|
export { describeNotifier } from '#core/http-notifier.js';
|
|
13
13
|
export { Scheduler, ScheduleError, FileSchedulerStore, getSchedulerStore, setSchedulerStore, createScheduledTask, listScheduledTasks, getScheduledTask, cancelScheduledTask, pauseScheduledTask, resumeScheduledTask, runScheduledTaskNow, deleteScheduledTask, nextScheduledWakeAt, importLegacyScheduledTasks, parseEvery, describeSchedule, type ScheduledTask, type ScheduledTaskRun, type ScheduledTaskStatus, type Schedule, type SchedulerStore, type FireTask, type CreateScheduledTaskInput } from '#core/scheduler.js';
|
|
14
14
|
export { NOTIFIER_SLACK_SIGNING_SECRET_VAR } from '#core/notifier-file.js';
|
|
15
|
-
export { firstSighting, handleInboundMessage, handleMessagingAction, type MessagingDeps, type MessagingDecision, type DecisionOutcome } from '#core/messaging/engine.js';
|
|
15
|
+
export { firstSighting, handleInboundMessage, handleMessagingAction, isPlainApproval, type MessagingDeps, type MessagingDecision, type DecisionOutcome } from '#core/messaging/engine.js';
|
|
16
16
|
export { formatArgs, formatChange, labelFor } from '#core/messaging/format-args.js';
|
|
17
17
|
export { decideReply, proposeEdit, describeChange, type ReplyDecision, type DecisionRequest } from '#core/messaging/decide.js';
|
|
18
18
|
export { recordThread, findThread, findThreadByPending, type ThreadLink } from '#core/messaging/threads.js';
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { listDeliveries, recordDelivery } from '#core/notifier-deliveries.js';
|
|
|
14
14
|
export { describeNotifier } from '#core/http-notifier.js';
|
|
15
15
|
export { Scheduler, ScheduleError, FileSchedulerStore, getSchedulerStore, setSchedulerStore, createScheduledTask, listScheduledTasks, getScheduledTask, cancelScheduledTask, pauseScheduledTask, resumeScheduledTask, runScheduledTaskNow, deleteScheduledTask, nextScheduledWakeAt, importLegacyScheduledTasks, parseEvery, describeSchedule } from '#core/scheduler.js';
|
|
16
16
|
export { NOTIFIER_SLACK_SIGNING_SECRET_VAR } from '#core/notifier-file.js';
|
|
17
|
-
export { firstSighting, handleInboundMessage, handleMessagingAction } from '#core/messaging/engine.js';
|
|
17
|
+
export { firstSighting, handleInboundMessage, handleMessagingAction, isPlainApproval } from '#core/messaging/engine.js';
|
|
18
18
|
export { formatArgs, formatChange, labelFor } from '#core/messaging/format-args.js';
|
|
19
19
|
export { decideReply, proposeEdit, describeChange } from '#core/messaging/decide.js';
|
|
20
20
|
export { recordThread, findThread, findThreadByPending } from '#core/messaging/threads.js';
|
|
@@ -1194,7 +1194,7 @@ ${consoleShellScript}
|
|
|
1194
1194
|
'<td><button type="button" class="delete-btn approver-remove" data-user="' + escapeHtml(a.user) + '"' + (n.canSave === false ? ' disabled' : '') + '>Remove</button></td></tr>';
|
|
1195
1195
|
}).join('');
|
|
1196
1196
|
return '<section id="approversSection"><h3>Approvers</h3>' +
|
|
1197
|
-
'<p class="muted" style="margin:0 0 10px">People who can approve, deny, change or answer a request by replying in its thread, like “yes, go ahead” or “make it $40”.
|
|
1197
|
+
'<p class="muted" style="margin:0 0 10px">People who can approve, deny, change or answer a request by replying in its thread, like “yes, go ahead” or “make it $40”. A plain “yes” runs it; a reply that's less clear (“sure, send it”, “yes but check the amount”) shows exactly what will run with a Confirm button first, and a change is shown to approve first. Once anyone is listed, only approvers' button clicks count too. Anyone in the channel can still ask about a request.</p>' +
|
|
1198
1198
|
(approvers.length
|
|
1199
1199
|
? '<div class="le-table-wrap" style="max-width:560px"><table><thead><tr><th>Name</th><th>Slack ID</th><th></th></tr></thead><tbody>' + rows + '</tbody></table></div>'
|
|
1200
1200
|
: '<p class="hint">No approvers yet. Until you add someone, anyone in the channel can use the buttons, and deciding by reply is off.</p>') +
|