loopengine 0.3.8 → 0.3.10
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/bin/ability-manager.js +5 -0
- package/dist/core/ability-sources.d.ts +3 -1
- package/dist/core/ability-sources.js +1 -1
- package/dist/core/mcp-directory.d.ts +16 -0
- package/dist/core/mcp-directory.js +96 -3
- package/dist/core/messaging/connectors/telegram/index.d.ts +4 -0
- package/dist/core/messaging/connectors/telegram/index.js +10 -1
- package/dist/core/model-calls/openai-model-call.d.ts +4 -0
- package/dist/core/model-calls/openai-model-call.js +12 -2
- package/dist/core/official-abilities.d.ts +14 -0
- package/dist/core/official-abilities.js +58 -0
- package/dist/core/system-agents/templates.d.ts +9 -0
- package/dist/core/system-agents/templates.js +127 -61
- package/dist/core/system-agents/workflow-planner.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/web/abilities-admin.js +18 -2
- package/dist/web/actauth-admin.d.ts +33 -1
- package/dist/web/actauth-admin.js +39 -2
- package/dist/web/agents-config-page.js +41 -36
- package/dist/web/console-shell.js +6 -5
- package/dist/web/global-config-page.js +82 -6
- package/dist/web/notifier-admin.d.ts +2 -0
- package/dist/web/notifier-admin.js +3 -1
- package/dist/web/playground.js +1 -1
- package/dist/web/templates-admin.js +25 -16
- package/dist/web/templates-page.js +6 -5
- package/dist/web/workflow-admin.js +8 -15
- package/dist/web/workflow-page.js +1 -1
- package/package.json +3 -1
|
@@ -2,9 +2,8 @@ import { nextTimeOfDay, parseTimeOfDay, workspaceTimeZone } from '../time-zone.j
|
|
|
2
2
|
const GMAIL_KEYS = ['GMAIL_CLIENT_ID', 'GMAIL_CLIENT_SECRET', 'GMAIL_REFRESH_TOKEN'];
|
|
3
3
|
const SHOPIFY_KEYS = ['SHOPIFY_STORE_DOMAIN', 'SHOPIFY_ADMIN_TOKEN'];
|
|
4
4
|
const GITHUB_APP = 'io.github.github/github-mcp-server';
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const DOCS_NOTE = 'Put your policies and help articles in the project\'s docs folder, as Markdown or text files (or point DOCS_SEARCH_PATH at another folder)';
|
|
5
|
+
const FIRECRAWL_APP = 'io.github.firecrawl/firecrawl-mcp-server';
|
|
6
|
+
const policiesNote = (what, skill) => `Write ${what} into its ${skill} skill (Advanced → Skills): the agent answers from it and nothing else`;
|
|
8
7
|
export const STARTER_TEMPLATES = [
|
|
9
8
|
{
|
|
10
9
|
id: 'customer-email-replies',
|
|
@@ -18,9 +17,9 @@ export const STARTER_TEMPLATES = [
|
|
|
18
17
|
risk: 'medium',
|
|
19
18
|
asksFirst: ['sending a reply'],
|
|
20
19
|
needs: [
|
|
21
|
-
{ kind: 'ability', name: '
|
|
22
|
-
{ kind: 'ability', name: '
|
|
23
|
-
{ kind: '
|
|
20
|
+
{ kind: 'ability', name: '@loopengine-co/gmail', description: 'your support inbox' },
|
|
21
|
+
{ kind: 'ability', name: '@loopengine-co/shopify', description: 'your store\'s orders' },
|
|
22
|
+
{ kind: 'other', name: 'Your policies', description: 'shipping, returns and the rest, in its support-policies skill' },
|
|
24
23
|
],
|
|
25
24
|
agent: {
|
|
26
25
|
name: 'customer-email-replies',
|
|
@@ -30,7 +29,7 @@ When asked to handle new emails:
|
|
|
30
29
|
1. Find them with gmail_search, query "is:unread in:inbox". Skip newsletters, receipts, automatic notifications and "no-reply" senders: mark those read and move on.
|
|
31
30
|
2. For each customer email, read the whole conversation with gmail_get_thread first.
|
|
32
31
|
3. Work out what they want: order status, a product question, a return or refund, a complaint, or something else.
|
|
33
|
-
4. Look up what you need before answering: the order with shopify_find_orders (by the order number they give, or their email) and shopify_get_order, and our policies with
|
|
32
|
+
4. Look up what you need before answering: the order with shopify_find_orders (by the order number they give, or their email) and shopify_get_order, and our policies in the support-policies skill (open it with Skill). Never guess an order detail, a date or a policy.
|
|
34
33
|
5. Reply with gmail_send, passing reply_to_message_id so it stays in the same thread: short, friendly, in the customer's language, using their name, answering the question first and saying what happens next.
|
|
35
34
|
6. Mark the email read with gmail_modify_labels (remove "UNREAD").
|
|
36
35
|
|
|
@@ -44,9 +43,31 @@ Rules:
|
|
|
44
43
|
},
|
|
45
44
|
},
|
|
46
45
|
setup: {
|
|
47
|
-
abilities: ['
|
|
46
|
+
abilities: ['@loopengine-co/gmail', '@loopengine-co/shopify'],
|
|
48
47
|
keys: [...GMAIL_KEYS, ...SHOPIFY_KEYS],
|
|
49
|
-
|
|
48
|
+
skills: [{
|
|
49
|
+
id: 'support-policies',
|
|
50
|
+
description: 'Our shipping, returns, refunds and other customer policies. Read it before telling a customer anything about a policy.',
|
|
51
|
+
body: `# Our customer policies
|
|
52
|
+
|
|
53
|
+
Edit this: replace each [bracket] with your own policy. The agent answers customers from this and nothing else.
|
|
54
|
+
|
|
55
|
+
## Shipping
|
|
56
|
+
- Where we ship: [countries]
|
|
57
|
+
- How long it takes: [e.g. 3–5 business days]
|
|
58
|
+
- Tracking: [when and how customers get it]
|
|
59
|
+
|
|
60
|
+
## Returns and refunds
|
|
61
|
+
- Return window: [e.g. 30 days from delivery]
|
|
62
|
+
- What can't be returned: [e.g. sale items, opened cosmetics]
|
|
63
|
+
- How refunds are paid: [e.g. to the original payment method, in 5–10 business days]
|
|
64
|
+
|
|
65
|
+
## Other questions
|
|
66
|
+
- [Order changes and cancellations]
|
|
67
|
+
- [Warranty]
|
|
68
|
+
- [Anything customers often ask]
|
|
69
|
+
`,
|
|
70
|
+
}],
|
|
50
71
|
rules: [
|
|
51
72
|
{ name: 'template-read-mail-allowed', tool: 'gmail_search', decision: 'allow' },
|
|
52
73
|
{ name: 'template-read-thread-allowed', tool: 'gmail_get_thread', decision: 'allow' },
|
|
@@ -57,7 +78,7 @@ Rules:
|
|
|
57
78
|
],
|
|
58
79
|
schedule: { every: '10m', name: 'Check the support inbox', message: 'Handle new customer emails in the support inbox.' },
|
|
59
80
|
needsApprovals: true,
|
|
60
|
-
notes: [
|
|
81
|
+
notes: [policiesNote('your policies', 'support-policies')],
|
|
61
82
|
},
|
|
62
83
|
},
|
|
63
84
|
{
|
|
@@ -72,9 +93,9 @@ Rules:
|
|
|
72
93
|
risk: 'high',
|
|
73
94
|
asksFirst: ['issuing a refund', 'sending the reply'],
|
|
74
95
|
needs: [
|
|
75
|
-
{ kind: 'ability', name: '
|
|
76
|
-
{ kind: 'ability', name: '
|
|
77
|
-
{ kind: '
|
|
96
|
+
{ kind: 'ability', name: '@loopengine-co/gmail', description: 'your support inbox' },
|
|
97
|
+
{ kind: 'ability', name: '@loopengine-co/shopify', description: 'your store\'s orders and refunds' },
|
|
98
|
+
{ kind: 'other', name: 'Your return policy', description: 'in its return-policy skill' },
|
|
78
99
|
],
|
|
79
100
|
agent: {
|
|
80
101
|
name: 'refund-approvals',
|
|
@@ -83,7 +104,7 @@ Rules:
|
|
|
83
104
|
When asked to handle new refund requests:
|
|
84
105
|
1. Find them with gmail_search, query "is:unread in:inbox (refund OR return OR damaged OR broken OR "money back")". Read each conversation with gmail_get_thread. If an email isn't really a refund or return request, leave it unread for the support team.
|
|
85
106
|
2. Find the order with shopify_find_orders (the order number they give, or their email) and read it with shopify_get_order: what was bought, when, how much, what's already been refunded, whether it was delivered.
|
|
86
|
-
3. Check the request against our return policy with
|
|
107
|
+
3. Check the request against our return policy in the return-policy skill (open it with Skill): the return window, whether the item is eligible, and whether the reason is covered.
|
|
87
108
|
4. Decide what to propose: a full refund, a partial refund, refunding shipping, or no refund (and which part of the policy says so).
|
|
88
109
|
5. Work out the exact amount with shopify_preview_refund. Never state an amount you haven't previewed.
|
|
89
110
|
6. Issue it with shopify_create_refund, with a note giving the policy rule and the facts from the order. A person approves it first.
|
|
@@ -99,9 +120,24 @@ Rules:
|
|
|
99
120
|
},
|
|
100
121
|
},
|
|
101
122
|
setup: {
|
|
102
|
-
abilities: ['
|
|
123
|
+
abilities: ['@loopengine-co/gmail', '@loopengine-co/shopify'],
|
|
103
124
|
keys: [...GMAIL_KEYS, ...SHOPIFY_KEYS],
|
|
104
|
-
|
|
125
|
+
skills: [{
|
|
126
|
+
id: 'return-policy',
|
|
127
|
+
description: 'Our return and refund policy: the window, what qualifies, and how much is refunded. Read it before proposing any refund.',
|
|
128
|
+
body: `# Our return policy
|
|
129
|
+
|
|
130
|
+
Edit this: replace each [bracket] with your own policy. The agent proposes refunds from this and nothing else.
|
|
131
|
+
|
|
132
|
+
- Return window: [e.g. 30 days from delivery]
|
|
133
|
+
- Eligible items: [e.g. unused, in the original packaging]
|
|
134
|
+
- Not eligible: [e.g. sale items, gift cards, personalised items]
|
|
135
|
+
- Damaged or wrong items: [e.g. full refund including shipping, no return needed]
|
|
136
|
+
- Change of mind: [e.g. refund of the item price, customer pays return shipping]
|
|
137
|
+
- Shipping costs: [when they're refunded]
|
|
138
|
+
- Partial refunds: [when you offer one, and how much]
|
|
139
|
+
`,
|
|
140
|
+
}],
|
|
105
141
|
rules: [
|
|
106
142
|
{ name: 'template-read-mail-allowed', tool: 'gmail_search', decision: 'allow' },
|
|
107
143
|
{ name: 'template-read-thread-allowed', tool: 'gmail_get_thread', decision: 'allow' },
|
|
@@ -113,7 +149,7 @@ Rules:
|
|
|
113
149
|
],
|
|
114
150
|
schedule: { every: '10m', name: 'Check for refund requests', message: 'Handle new refund and return requests in the support inbox.' },
|
|
115
151
|
needsApprovals: true,
|
|
116
|
-
notes: [
|
|
152
|
+
notes: [policiesNote('your return policy', 'return-policy')],
|
|
117
153
|
},
|
|
118
154
|
},
|
|
119
155
|
{
|
|
@@ -121,15 +157,15 @@ Rules:
|
|
|
121
157
|
category: 'Operations',
|
|
122
158
|
candidate: {
|
|
123
159
|
title: 'Triage incidents',
|
|
124
|
-
summary: 'Takes an alert or a bug report, works out what broke and how bad it is, finds the deploys and changes that likely caused it, and
|
|
125
|
-
trigger: { kind: '
|
|
160
|
+
summary: 'Takes an alert or a bug report, works out what broke and how bad it is, finds the deploys and changes that likely caused it, and sends a summary to Slack. Opening or commenting on issues waits for approval.',
|
|
161
|
+
trigger: { kind: 'event', description: 'when your alerting tool calls its webhook, or someone passes it an alert in Chat or Slack' },
|
|
126
162
|
hoursPerWeek: 3,
|
|
127
163
|
fit: 6,
|
|
128
164
|
risk: 'medium',
|
|
129
165
|
asksFirst: ['opening or commenting on issues'],
|
|
130
166
|
needs: [
|
|
131
167
|
{ kind: 'app', name: 'GitHub', description: 'your repository\'s commits, deploys and issues' },
|
|
132
|
-
{ kind: '
|
|
168
|
+
{ kind: 'messaging', name: 'Slack', description: 'where each summary is sent (its Messaging tab)' },
|
|
133
169
|
],
|
|
134
170
|
agent: {
|
|
135
171
|
name: 'incident-triage',
|
|
@@ -137,32 +173,38 @@ Rules:
|
|
|
137
173
|
|
|
138
174
|
Our repository on GitHub: [owner/name] (edit this). Pass its owner and repo to every GitHub tool.
|
|
139
175
|
|
|
140
|
-
When you get an alert, error or bug report
|
|
141
|
-
1. Summarise it in two sentences: what is failing and who is affected.
|
|
142
|
-
2. Rate the severity: SEV1 (customers can't use the product), SEV2 (a major feature is broken), SEV3 (degraded or a workaround exists), SEV4 (minor).
|
|
143
|
-
3. Find what changed shortly before it started: github_actions_list (method list_workflow_runs) for recent deploys and failed builds, github_list_pull_requests (state closed) for recently merged changes, and github_list_commits (narrowed with since and path) for detail. Match times carefully.
|
|
144
|
-
4. Check whether it's already known with github_search_issues; read a match with github_issue_read (method get).
|
|
145
|
-
5. Post a short summary to Slack with post_to_slack: what's broken, severity, the most likely cause with links, and one next step (who should look, what to check, or whether to roll back).
|
|
146
|
-
6. If an issue already tracks it, add your findings with github_add_issue_comment; otherwise, for SEV1 or SEV2, open one with github_issue_write (method create). Both ask a person first.
|
|
176
|
+
When you get an alert, error or bug report, follow your triage-incident workflow.
|
|
147
177
|
|
|
148
178
|
Rules:
|
|
149
179
|
- Only read on your own; never roll anything back yourself.
|
|
150
180
|
- Say how sure you are. If the evidence is thin, say what would confirm it instead of guessing.
|
|
151
|
-
- Keep the
|
|
181
|
+
- Keep the summary short enough to read on a phone.`,
|
|
152
182
|
exampleMessage: 'Alert: checkout-api error rate 18% (normally under 1%) since 14:05 UTC. Errors: "timeout connecting to payments".',
|
|
153
183
|
},
|
|
154
184
|
},
|
|
155
185
|
setup: {
|
|
156
|
-
abilities: [
|
|
186
|
+
abilities: [],
|
|
157
187
|
apps: [{ id: GITHUB_APP, title: 'GitHub', tools: ['list_commits', 'list_pull_requests', 'actions_list', 'search_issues', 'issue_read', 'issue_write', 'add_issue_comment'] }],
|
|
158
|
-
keys: [
|
|
188
|
+
keys: [],
|
|
189
|
+
skills: [{
|
|
190
|
+
id: 'triage-incident',
|
|
191
|
+
description: 'Triage an alert, error or bug report: what broke, how bad it is, and the change that likely caused it.',
|
|
192
|
+
workflow: { title: 'Triage an incident', parameters: { alert: { description: 'The alert, error or bug report', required: true } }, webhook: true },
|
|
193
|
+
body: `Triage the alert you were given.
|
|
194
|
+
|
|
195
|
+
1. Summarise it in two sentences: what is failing and who is affected.
|
|
196
|
+
2. Rate the severity: SEV1 (customers can't use the product), SEV2 (a major feature is broken), SEV3 (degraded or a workaround exists), SEV4 (minor).
|
|
197
|
+
3. Find what changed shortly before it started: github_actions_list (method list_workflow_runs) for recent deploys and failed builds, github_list_pull_requests (state closed) for recently merged changes, and github_list_commits (narrowed with since and path) for detail. Match times carefully.
|
|
198
|
+
4. Check whether it's already known with github_search_issues; read a match with github_issue_read (method get).
|
|
199
|
+
5. If an issue already tracks it, add your findings with github_add_issue_comment; otherwise, for SEV1 or SEV2, open one with github_issue_write (method create). Both ask a person first.
|
|
200
|
+
6. End with the summary, which is sent to Slack: what's broken, severity, the most likely cause with links, and one next step (who should look, what to check, or whether to roll back).`,
|
|
201
|
+
}],
|
|
159
202
|
rules: [
|
|
160
|
-
{ name: 'template-slack-summary-allowed', tool: 'post_to_slack', decision: 'allow' },
|
|
161
203
|
{ name: 'template-new-issues-need-approval', tool: 'github_issue_write', decision: 'ask' },
|
|
162
204
|
{ name: 'template-comments-need-approval', tool: 'github_add_issue_comment', decision: 'ask' },
|
|
163
205
|
],
|
|
164
206
|
needsApprovals: true,
|
|
165
|
-
notes: ['Send it alerts: paste one into Chat,
|
|
207
|
+
notes: ['Send it alerts: paste one into Chat, mention it in Slack, or have your alerting tool call its webhook'],
|
|
166
208
|
},
|
|
167
209
|
},
|
|
168
210
|
{
|
|
@@ -177,8 +219,8 @@ Rules:
|
|
|
177
219
|
risk: 'high',
|
|
178
220
|
asksFirst: ['replying to the claimant'],
|
|
179
221
|
needs: [
|
|
180
|
-
{ kind: 'ability', name: '
|
|
181
|
-
{ kind: '
|
|
222
|
+
{ kind: 'ability', name: '@loopengine-co/gmail', description: 'the inbox where claims arrive' },
|
|
223
|
+
{ kind: 'other', name: 'Your expense policy', description: 'in its expense-policy skill' },
|
|
182
224
|
],
|
|
183
225
|
agent: {
|
|
184
226
|
name: 'expense-approvals',
|
|
@@ -187,7 +229,7 @@ Rules:
|
|
|
187
229
|
When asked to check new claims:
|
|
188
230
|
1. Find them with gmail_search, query "is:unread in:inbox". Read each with gmail_get_thread.
|
|
189
231
|
2. Read the amount, currency, date, merchant, category and who submitted it, from the email and its receipt (the attachment names show whether a receipt is there).
|
|
190
|
-
3. Check it against
|
|
232
|
+
3. Check it against our expense policy in the expense-policy skill (open it with Skill): the category is allowed, it's under the limit, the receipt matches the amount and date, and it isn't a duplicate of an earlier claim (search the inbox for the same merchant and amount).
|
|
191
233
|
4. Write a one-line verdict: "Within policy" or "Outside policy: <reason>", then the details you checked.
|
|
192
234
|
5. Email the verdict to the approver at the address in the EXPENSE_APPROVER_EMAIL setting with gmail_send, then mark the claim read with gmail_modify_labels.
|
|
193
235
|
|
|
@@ -200,9 +242,30 @@ Rules:
|
|
|
200
242
|
},
|
|
201
243
|
},
|
|
202
244
|
setup: {
|
|
203
|
-
abilities: ['
|
|
245
|
+
abilities: ['@loopengine-co/gmail'],
|
|
204
246
|
keys: [...GMAIL_KEYS],
|
|
205
|
-
|
|
247
|
+
skills: [{
|
|
248
|
+
id: 'expense-policy',
|
|
249
|
+
description: 'Our expense policy: what can be claimed, the limits, and what a claim needs. Read it before giving any verdict.',
|
|
250
|
+
body: `# Our expense policy
|
|
251
|
+
|
|
252
|
+
Edit this: replace each [bracket] with your own policy. The agent checks claims against this and nothing else.
|
|
253
|
+
|
|
254
|
+
## What can be claimed, and the limits
|
|
255
|
+
- Travel: [e.g. economy flights and trains; taxis when there's no public transport]
|
|
256
|
+
- Hotels: [e.g. up to 150 per night]
|
|
257
|
+
- Meals: [e.g. up to 50 per day when travelling]
|
|
258
|
+
- Equipment and software: [e.g. up to 200 without pre-approval]
|
|
259
|
+
|
|
260
|
+
## What can't be claimed
|
|
261
|
+
- [e.g. alcohol, fines, personal entertainment]
|
|
262
|
+
|
|
263
|
+
## What every claim needs
|
|
264
|
+
- An itemised receipt showing the amount, date and merchant
|
|
265
|
+
- [Pre-approval for anything over a limit]
|
|
266
|
+
- Submitted within [e.g. 60 days]
|
|
267
|
+
`,
|
|
268
|
+
}],
|
|
206
269
|
rules: [
|
|
207
270
|
{ name: 'template-read-mail-allowed', tool: 'gmail_search', decision: 'allow' },
|
|
208
271
|
{ name: 'template-read-thread-allowed', tool: 'gmail_get_thread', decision: 'allow' },
|
|
@@ -212,7 +275,7 @@ Rules:
|
|
|
212
275
|
],
|
|
213
276
|
schedule: { every: '15m', name: 'Check for new claims', message: 'Check the new expense claims in the inbox.' },
|
|
214
277
|
needsApprovals: true,
|
|
215
|
-
notes: [
|
|
278
|
+
notes: [policiesNote('your expense policy', 'expense-policy'), 'Set EXPENSE_APPROVER_EMAIL (Environment tab) to the approver\'s address'],
|
|
216
279
|
},
|
|
217
280
|
},
|
|
218
281
|
{
|
|
@@ -227,9 +290,9 @@ Rules:
|
|
|
227
290
|
risk: 'medium',
|
|
228
291
|
asksFirst: ['sending the first email', 'updating the lead'],
|
|
229
292
|
needs: [
|
|
230
|
-
{ kind: 'ability', name: '
|
|
231
|
-
{ kind: '
|
|
232
|
-
{ kind: 'ability', name: '
|
|
293
|
+
{ kind: 'ability', name: '@loopengine-co/hubspot', description: 'your leads in HubSpot' },
|
|
294
|
+
{ kind: 'app', name: 'Firecrawl', description: 'researching each lead on the web' },
|
|
295
|
+
{ kind: 'ability', name: '@loopengine-co/gmail', description: 'the inbox to send from' },
|
|
233
296
|
],
|
|
234
297
|
agent: {
|
|
235
298
|
name: 'lead-follow-up',
|
|
@@ -243,7 +306,7 @@ What we sell (edit this):
|
|
|
243
306
|
When asked to follow up with new leads:
|
|
244
307
|
1. Find them with hubspot_find_contacts: lead_status "NEW", created_in_last_days 2.
|
|
245
308
|
2. For each, read hubspot_get_contact. Skip anyone with an owner, recent notes, or a last-contacted date: someone is already on it.
|
|
246
|
-
3. Research them with
|
|
309
|
+
3. Research them with firecrawl_search and firecrawl_scrape: what their company does, its size, and anything recent that matters (funding, hiring, a launch). Note your sources.
|
|
247
310
|
4. Work out what they most likely need from us, in one sentence.
|
|
248
311
|
5. Send a short first email (under 120 words) with gmail_send: a specific opening based on your research, one sentence on how we help with that need, and one easy question to reply to. A person approves it first.
|
|
249
312
|
6. Log what you sent and found with hubspot_add_note, and set the lead to "ATTEMPTED_TO_CONTACT" with hubspot_update_contact.
|
|
@@ -257,9 +320,12 @@ Rules:
|
|
|
257
320
|
},
|
|
258
321
|
},
|
|
259
322
|
setup: {
|
|
260
|
-
abilities: ['
|
|
261
|
-
|
|
323
|
+
abilities: ['@loopengine-co/hubspot', '@loopengine-co/gmail'],
|
|
324
|
+
apps: [{ id: FIRECRAWL_APP, title: 'Firecrawl', tools: ['firecrawl_search', 'firecrawl_scrape'] }],
|
|
325
|
+
keys: ['HUBSPOT_ACCESS_TOKEN', ...GMAIL_KEYS],
|
|
262
326
|
rules: [
|
|
327
|
+
{ name: 'template-search-allowed', tool: 'firecrawl_search', decision: 'allow' },
|
|
328
|
+
{ name: 'template-read-pages-allowed', tool: 'firecrawl_scrape', decision: 'allow' },
|
|
263
329
|
{ name: 'template-notes-allowed', tool: 'hubspot_add_note', decision: 'allow' },
|
|
264
330
|
{ name: 'template-lead-updates-need-approval', tool: 'hubspot_update_contact', decision: 'ask' },
|
|
265
331
|
{ name: 'template-no-new-contacts', tool: 'hubspot_create_contact', decision: 'deny' },
|
|
@@ -282,7 +348,7 @@ Rules:
|
|
|
282
348
|
risk: 'low',
|
|
283
349
|
asksFirst: ['replying to the sender'],
|
|
284
350
|
needs: [
|
|
285
|
-
{ kind: 'ability', name: '
|
|
351
|
+
{ kind: 'ability', name: '@loopengine-co/gmail', description: 'the shared inbox' },
|
|
286
352
|
{ kind: 'app', name: 'GitHub', description: 'tasks, as GitHub issues' },
|
|
287
353
|
],
|
|
288
354
|
agent: {
|
|
@@ -312,7 +378,7 @@ Rules:
|
|
|
312
378
|
},
|
|
313
379
|
},
|
|
314
380
|
setup: {
|
|
315
|
-
abilities: ['
|
|
381
|
+
abilities: ['@loopengine-co/gmail'],
|
|
316
382
|
apps: [{ id: GITHUB_APP, title: 'GitHub', tools: ['search_issues', 'issue_write', 'add_issue_comment'] }],
|
|
317
383
|
keys: [...GMAIL_KEYS],
|
|
318
384
|
rules: [
|
|
@@ -333,19 +399,19 @@ Rules:
|
|
|
333
399
|
category: 'Research',
|
|
334
400
|
candidate: {
|
|
335
401
|
title: 'Weekly research digest',
|
|
336
|
-
summary: 'Every Monday morning, searches the web for news on the topics you choose and
|
|
402
|
+
summary: 'Every Monday morning, searches the web for news on the topics you choose and sends a short digest with links to Slack.',
|
|
337
403
|
trigger: { kind: 'schedule', description: 'Mondays at 9:00', every: '7d' },
|
|
338
404
|
hoursPerWeek: 2,
|
|
339
405
|
fit: 6,
|
|
340
406
|
risk: 'low',
|
|
341
407
|
asksFirst: [],
|
|
342
408
|
needs: [
|
|
343
|
-
{ kind: '
|
|
344
|
-
{ kind: '
|
|
409
|
+
{ kind: 'app', name: 'Firecrawl', description: 'searching the web and reading pages' },
|
|
410
|
+
{ kind: 'messaging', name: 'Slack', description: 'where each digest is sent (its Messaging tab)' },
|
|
345
411
|
],
|
|
346
412
|
agent: {
|
|
347
413
|
name: 'weekly-research-digest',
|
|
348
|
-
systemPrompt: `You write a weekly research digest
|
|
414
|
+
systemPrompt: `You write a weekly research digest. Your answer is the digest: it's sent to Slack as it is.
|
|
349
415
|
|
|
350
416
|
Topics to follow (edit this list):
|
|
351
417
|
- [Your industry or market]
|
|
@@ -353,29 +419,29 @@ Topics to follow (edit this list):
|
|
|
353
419
|
- [Technologies or regulations you care about]
|
|
354
420
|
|
|
355
421
|
Each week:
|
|
356
|
-
1. Search the web with
|
|
357
|
-
2. Open the most relevant pages with
|
|
422
|
+
1. Search the web with firecrawl_search for what happened in the last 7 days on each topic.
|
|
423
|
+
2. Open the most relevant pages with firecrawl_scrape and read them; don't rely on search snippets.
|
|
358
424
|
3. Write the digest: for each topic, up to three items, each with a one-sentence summary, why it matters to us, and the link.
|
|
359
425
|
4. End with "Worth a closer look": the single most important item, and why.
|
|
360
|
-
5.
|
|
426
|
+
5. Answer with the digest alone, nothing before or after it.
|
|
361
427
|
|
|
362
428
|
Rules:
|
|
363
429
|
- Only include things published in the last 7 days, and give each item's date.
|
|
364
430
|
- Prefer original sources over aggregators. Never invent a link.
|
|
365
431
|
- If a topic had no real news, say so in one line.`,
|
|
366
|
-
scheduleMessage: 'Write
|
|
367
|
-
exampleMessage: 'Write
|
|
432
|
+
scheduleMessage: 'Write this week\'s research digest.',
|
|
433
|
+
exampleMessage: 'Write this week\'s research digest.',
|
|
368
434
|
},
|
|
369
435
|
},
|
|
370
436
|
setup: {
|
|
371
|
-
abilities: [
|
|
372
|
-
|
|
437
|
+
abilities: [],
|
|
438
|
+
apps: [{ id: FIRECRAWL_APP, title: 'Firecrawl', tools: ['firecrawl_search', 'firecrawl_scrape'] }],
|
|
439
|
+
keys: [],
|
|
373
440
|
rules: [
|
|
374
|
-
{ name: 'template-search-allowed', tool: '
|
|
375
|
-
{ name: 'template-
|
|
376
|
-
{ name: 'template-digest-post-allowed', tool: 'post_to_slack', decision: 'allow' },
|
|
441
|
+
{ name: 'template-search-allowed', tool: 'firecrawl_search', decision: 'allow' },
|
|
442
|
+
{ name: 'template-read-pages-allowed', tool: 'firecrawl_scrape', decision: 'allow' },
|
|
377
443
|
],
|
|
378
|
-
schedule: { every: '7d', at: '09:00', weekday: 1, name: 'Weekly research digest', message: 'Write
|
|
444
|
+
schedule: { every: '7d', at: '09:00', weekday: 1, name: 'Weekly research digest', message: 'Write this week\'s research digest.', notify: true },
|
|
379
445
|
needsApprovals: false,
|
|
380
446
|
notes: ['Edit "Topics to follow" at the top of its instructions (Overview tab)'],
|
|
381
447
|
},
|
|
@@ -411,7 +411,8 @@ export function workflowTemplate(plan) {
|
|
|
411
411
|
...(plan.apps?.length ? { apps: plan.apps } : {}),
|
|
412
412
|
keys: plan.keys,
|
|
413
413
|
skills: [{ id: plan.id, description: plan.skill.description, body: plan.skill.body }],
|
|
414
|
-
|
|
414
|
+
// Named after the tool: readable, and made unique if taken.
|
|
415
|
+
rules: plan.rules.map((r) => ({ name: r.tool, tool: r.tool, decision: r.decision })),
|
|
415
416
|
...(plan.trigger.kind === 'schedule' && plan.trigger.every
|
|
416
417
|
? { schedule: { every: plan.trigger.every, ...(plan.trigger.at ? { at: plan.trigger.at } : {}), ...(plan.trigger.weekday !== undefined ? { weekday: plan.trigger.weekday } : {}), name: plan.name, message: plan.startMessage, workflowId: plan.id, ...(plan.notify?.on ? { notify: true } : {}) } }
|
|
417
418
|
: {}),
|
package/dist/index.d.ts
CHANGED
|
@@ -108,7 +108,7 @@ export { AbilitySourceError, abilityOrigin, readAbilitySources, saveAbilitySourc
|
|
|
108
108
|
export { describeAbilitySources, listPrivateAbilities, saveAbilitySourceFromConsole, removeAbilitySourceFromConsole } from './web/ability-sources-admin.js';
|
|
109
109
|
export { readAbilityDetails, readToolDefinitions, type AbilityDetails } from './core/ability-details.js';
|
|
110
110
|
export { loadMcpAbilityTools, mcpAbilityLoadError, closeMcpConnections, resolveMcpManifest, listMcpServerTools, isMcpAuthError, type McpServerManifest, type McpAuth, type InstalledMcpRecord, type McpToolInfo } from './core/mcp-abilities.js';
|
|
111
|
-
export { searchMcpServers, getMcpServer, savedMcpServers, type McpDirectoryServer } from './core/mcp-directory.js';
|
|
111
|
+
export { searchMcpServers, getMcpServer, savedMcpServers, ownMcpServer, saveMcpServer, removeSavedMcpServer, McpServerInputError, type McpDirectoryServer } from './core/mcp-directory.js';
|
|
112
112
|
export { generateMcpAbility, mcpAbilityNames, McpAbilityGenerationError, type GeneratedMcpAbility, type McpAbilityRequest } from './core/mcp-ability-generator.js';
|
|
113
113
|
export { abilityDetails } from './web/ability-details-admin.js';
|
|
114
114
|
export { WorkspaceAbilityError, listWorkspaceAbilities, addWorkspaceAbility, removeWorkspaceAbility, workspaceAbilitiesDir, type WorkspaceAbility } from './core/workspace-abilities.js';
|
package/dist/index.js
CHANGED
|
@@ -98,7 +98,7 @@ export { AbilitySourceError, abilityOrigin, readAbilitySources, saveAbilitySourc
|
|
|
98
98
|
export { describeAbilitySources, listPrivateAbilities, saveAbilitySourceFromConsole, removeAbilitySourceFromConsole } from './web/ability-sources-admin.js';
|
|
99
99
|
export { readAbilityDetails, readToolDefinitions } from './core/ability-details.js';
|
|
100
100
|
export { loadMcpAbilityTools, mcpAbilityLoadError, closeMcpConnections, resolveMcpManifest, listMcpServerTools, isMcpAuthError } from './core/mcp-abilities.js';
|
|
101
|
-
export { searchMcpServers, getMcpServer, savedMcpServers } from './core/mcp-directory.js';
|
|
101
|
+
export { searchMcpServers, getMcpServer, savedMcpServers, ownMcpServer, saveMcpServer, removeSavedMcpServer, McpServerInputError } from './core/mcp-directory.js';
|
|
102
102
|
export { generateMcpAbility, mcpAbilityNames, McpAbilityGenerationError } from './core/mcp-ability-generator.js';
|
|
103
103
|
export { abilityDetails } from './web/ability-details-admin.js';
|
|
104
104
|
export { WorkspaceAbilityError, listWorkspaceAbilities, addWorkspaceAbility, removeWorkspaceAbility, workspaceAbilitiesDir } from './core/workspace-abilities.js';
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
// Backs the Admin UI's official abilities search box: the npm packages
|
|
2
|
+
// under LoopEngine's @loopengine-co scope, tagged with the
|
|
3
|
+
// loopengine-ability keyword (core/official-abilities.ts). A fresh call on
|
|
4
|
+
// every search, no caching: this is an on-demand lookup the operator
|
|
5
|
+
// explicitly asked for, not data the tab needs hot the way
|
|
6
|
+
// gateway-tools.ts's own live-reconnect data is — same "only pay the
|
|
7
|
+
// live cost when actually asked for" split, just with nothing worth
|
|
8
|
+
// remembering afterward here.
|
|
9
|
+
import { isOfficialScope, unofficialAbilitiesAllowed } from '../core/official-abilities.js';
|
|
1
10
|
const NPM_SEARCH_TIMEOUT_MS = 10000;
|
|
2
|
-
|
|
11
|
+
// npm's most per page: the official ones are filtered out of everything
|
|
12
|
+
// tagged with the keyword, so ask for all of it.
|
|
13
|
+
const NPM_SEARCH_SIZE = 250;
|
|
3
14
|
export async function searchPublicAbilities(query) {
|
|
4
15
|
const text = query ? `keywords:loopengine-ability ${query}` : 'keywords:loopengine-ability';
|
|
5
16
|
const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(text)}&size=${NPM_SEARCH_SIZE}`;
|
|
@@ -18,7 +29,12 @@ export async function searchPublicAbilities(query) {
|
|
|
18
29
|
if (!res.ok)
|
|
19
30
|
throw new Error(`npm registry search failed (HTTP ${res.status})`);
|
|
20
31
|
const data = (await res.json());
|
|
21
|
-
|
|
32
|
+
// Only LoopEngine's own scope, unless unofficial abilities are turned
|
|
33
|
+
// on: anyone can publish with the keyword. (npm's scope: qualifier
|
|
34
|
+
// only ranks, it doesn't filter.)
|
|
35
|
+
const all = unofficialAbilitiesAllowed();
|
|
36
|
+
const official = (data.objects ?? []).filter((o) => all || isOfficialScope(o.package.name));
|
|
37
|
+
const results = official.map((o) => ({
|
|
22
38
|
name: o.package.name,
|
|
23
39
|
description: o.package.description ?? '',
|
|
24
40
|
version: o.package.version,
|
|
@@ -23,7 +23,38 @@ export declare function readActauthConfig(agentName: string): ActauthConfigView;
|
|
|
23
23
|
/** Appends a new rule. `rule.name` must be unique among this agent's
|
|
24
24
|
* existing rules — it's the only handle update/removeActauthRule below
|
|
25
25
|
* have to find it again, so a duplicate would make those ambiguous. */
|
|
26
|
-
|
|
26
|
+
/** Adds a rule at the end, or, with `before`, just above that named rule
|
|
27
|
+
* (the first rule that matches a call decides it, so above is what makes
|
|
28
|
+
* it count). */
|
|
29
|
+
export declare function addActauthRule(agentName: string, rule: ActauthRuleInput, options?: {
|
|
30
|
+
before?: string;
|
|
31
|
+
}): void;
|
|
32
|
+
type RuleDecision = 'allow' | 'ask' | 'deny';
|
|
33
|
+
export interface PlannedRule {
|
|
34
|
+
tool: string;
|
|
35
|
+
decision: RuleDecision;
|
|
36
|
+
/** Its name in actauth.yml; the tool's name (made unique) by default. */
|
|
37
|
+
name?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface PlannedRulesResult {
|
|
40
|
+
added: string[];
|
|
41
|
+
/** Tools that already had a rule with the same decision. */
|
|
42
|
+
unchanged: string[];
|
|
43
|
+
/** Tools whose first rule is stricter or looser than planned and was
|
|
44
|
+
* kept as it is: the plan never loosens a decision the agent has. */
|
|
45
|
+
kept: {
|
|
46
|
+
tool: string;
|
|
47
|
+
has: RuleDecision;
|
|
48
|
+
wants: RuleDecision;
|
|
49
|
+
}[];
|
|
50
|
+
}
|
|
51
|
+
/** Gives the planned tools their decisions, once the agent's abilities and
|
|
52
|
+
* apps are installed (their own rules in place), so nothing is added twice:
|
|
53
|
+
* a tool with no rule gets one; one whose first matching rule already says
|
|
54
|
+
* the same is left alone; one a plan wants stricter (ask where an ability
|
|
55
|
+
* allows) gets its rule just above, so it counts; one a plan wants looser
|
|
56
|
+
* keeps what it has, and is reported. */
|
|
57
|
+
export declare function applyPlannedRules(agentName: string, planned: PlannedRule[]): PlannedRulesResult;
|
|
27
58
|
/** Mutates an existing rule's scope/tool/decision in place (via the
|
|
28
59
|
* node's own `.set()`, not remove-then-re-add), so any comment attached
|
|
29
60
|
* to that specific rule in the source YAML survives the edit. */
|
|
@@ -34,3 +65,4 @@ export declare function updateActauthRule(agentName: string, ruleName: string, u
|
|
|
34
65
|
}): void;
|
|
35
66
|
export declare function removeActauthRule(agentName: string, ruleName: string): void;
|
|
36
67
|
export declare function setDefaultDecision(agentName: string, decision: Decision): void;
|
|
68
|
+
export {};
|
|
@@ -62,16 +62,53 @@ function findRuleIndex(rules, ruleName) {
|
|
|
62
62
|
/** Appends a new rule. `rule.name` must be unique among this agent's
|
|
63
63
|
* existing rules — it's the only handle update/removeActauthRule below
|
|
64
64
|
* have to find it again, so a duplicate would make those ambiguous. */
|
|
65
|
-
|
|
65
|
+
/** Adds a rule at the end, or, with `before`, just above that named rule
|
|
66
|
+
* (the first rule that matches a call decides it, so above is what makes
|
|
67
|
+
* it count). */
|
|
68
|
+
export function addActauthRule(agentName, rule, options = {}) {
|
|
66
69
|
const dir = agentDir(agentName);
|
|
67
70
|
const { path, doc } = loadDoc(dir);
|
|
68
71
|
const rules = doc.get('rules');
|
|
69
72
|
if (findRuleIndex(rules, rule.name) !== -1) {
|
|
70
73
|
throw new ActauthRuleExistsError(`A rule named '${rule.name}' already exists for '${agentName}' — pick a different name or remove it first.`);
|
|
71
74
|
}
|
|
72
|
-
|
|
75
|
+
const node = doc.createNode({ name: rule.name, scope: rule.scope, tool: rule.tool, decision: rule.decision });
|
|
76
|
+
const at = options.before ? findRuleIndex(rules, options.before) : -1;
|
|
77
|
+
if (at === -1)
|
|
78
|
+
rules.add(node);
|
|
79
|
+
else
|
|
80
|
+
rules.items.splice(at, 0, node);
|
|
73
81
|
saveDoc(dir, path, doc);
|
|
74
82
|
}
|
|
83
|
+
const STRICTNESS = { allow: 0, ask: 1, deny: 2 };
|
|
84
|
+
/** Gives the planned tools their decisions, once the agent's abilities and
|
|
85
|
+
* apps are installed (their own rules in place), so nothing is added twice:
|
|
86
|
+
* a tool with no rule gets one; one whose first matching rule already says
|
|
87
|
+
* the same is left alone; one a plan wants stricter (ask where an ability
|
|
88
|
+
* allows) gets its rule just above, so it counts; one a plan wants looser
|
|
89
|
+
* keeps what it has, and is reported. */
|
|
90
|
+
export function applyPlannedRules(agentName, planned) {
|
|
91
|
+
const result = { added: [], unchanged: [], kept: [] };
|
|
92
|
+
for (const rule of planned) {
|
|
93
|
+
const current = readActauthConfig(agentName).rules;
|
|
94
|
+
const first = current.find((r) => r.tool === rule.tool);
|
|
95
|
+
if (first && first.decision === rule.decision) {
|
|
96
|
+
result.unchanged.push(rule.tool);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (first && STRICTNESS[rule.decision] < STRICTNESS[first.decision]) {
|
|
100
|
+
result.kept.push({ tool: rule.tool, has: first.decision, wants: rule.decision });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const base = rule.name ?? rule.tool;
|
|
104
|
+
let name = base;
|
|
105
|
+
for (let i = 2; current.some((r) => r.name === name); i++)
|
|
106
|
+
name = `${base}-${i}`;
|
|
107
|
+
addActauthRule(agentName, { name, scope: '*/*', tool: rule.tool, decision: rule.decision }, first ? { before: first.name } : {});
|
|
108
|
+
result.added.push(name);
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
75
112
|
/** Mutates an existing rule's scope/tool/decision in place (via the
|
|
76
113
|
* node's own `.set()`, not remove-then-re-add), so any comment attached
|
|
77
114
|
* to that specific rule in the source YAML survives the edit. */
|