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.
@@ -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 SLACK_KEYS = ['SLACK_BOT_TOKEN', 'SLACK_DEFAULT_CHANNEL'];
6
- const WEB_SEARCH_KEYS = [['TAVILY_API_KEY', 'BRAVE_API_KEY', 'FIRECRAWL_API_KEY']];
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: 'lp-gmail', description: 'your support inbox' },
22
- { kind: 'ability', name: 'lp-shopify', description: 'your store\'s orders' },
23
- { kind: 'ability', name: 'lp-docs-search', description: 'your help articles and policies' },
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 search_docs. Never guess an order detail, a date or a policy.
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: ['lp-gmail', 'lp-shopify', 'lp-docs-search'],
46
+ abilities: ['@loopengine-co/gmail', '@loopengine-co/shopify'],
48
47
  keys: [...GMAIL_KEYS, ...SHOPIFY_KEYS],
49
- env: { DOCS_SEARCH_PATH: 'docs' },
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: [DOCS_NOTE],
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: 'lp-gmail', description: 'your support inbox' },
76
- { kind: 'ability', name: 'lp-shopify', description: 'your store\'s orders and refunds' },
77
- { kind: 'ability', name: 'lp-docs-search', description: 'your return policy' },
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 search_docs: the return window, whether the item is eligible, and whether the reason is covered.
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: ['lp-gmail', 'lp-shopify', 'lp-docs-search'],
123
+ abilities: ['@loopengine-co/gmail', '@loopengine-co/shopify'],
103
124
  keys: [...GMAIL_KEYS, ...SHOPIFY_KEYS],
104
- env: { DOCS_SEARCH_PATH: 'docs' },
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: [DOCS_NOTE + ' — especially your return policy'],
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 posts a summary to Slack. Opening or commenting on issues waits for approval.',
125
- trigger: { kind: 'on_request', description: 'when an alert or report is passed to it in Chat, Slack or over HTTP' },
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: 'ability', name: 'lp-slack-notifier', description: 'posting the summary to Slack' },
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 Slack summary short enough to read on a phone.`,
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: ['lp-slack-notifier'],
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: [...SLACK_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, have your alerting tool call it over HTTP, or reply to it in Slack'],
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: 'lp-gmail', description: 'the inbox where claims arrive' },
181
- { kind: 'ability', name: 'lp-docs-search', description: 'your expense policy' },
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 the expense policy with search_docs: 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).
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: ['lp-gmail', 'lp-docs-search'],
245
+ abilities: ['@loopengine-co/gmail'],
204
246
  keys: [...GMAIL_KEYS],
205
- env: { DOCS_SEARCH_PATH: 'docs' },
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: [DOCS_NOTE + ' — especially your expense policy', 'Set EXPENSE_APPROVER_EMAIL (Environment tab) to the approver\'s address'],
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: 'lp-hubspot', description: 'your leads in HubSpot' },
231
- { kind: 'ability', name: 'lp-web-search', description: 'researching each lead' },
232
- { kind: 'ability', name: 'lp-gmail', description: 'the inbox to send from' },
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 web_search and web_fetch: what their company does, its size, and anything recent that matters (funding, hiring, a launch). Note your sources.
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: ['lp-hubspot', 'lp-web-search', 'lp-gmail'],
261
- keys: ['HUBSPOT_ACCESS_TOKEN', ...WEB_SEARCH_KEYS, ...GMAIL_KEYS],
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: 'lp-gmail', description: 'the shared inbox' },
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: ['lp-gmail'],
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 posts a short digest with links to Slack.',
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: 'ability', name: 'lp-web-search', description: 'web search' },
344
- { kind: 'ability', name: 'lp-slack-notifier', description: 'posting the digest to Slack' },
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 and post it to Slack.
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 web_search for what happened in the last 7 days on each topic.
357
- 2. Open the most relevant pages with web_fetch and read them; don't rely on search snippets.
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. Post it with post_to_slack.
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 and post this week\'s research digest.',
367
- exampleMessage: 'Write and post this week\'s research digest.',
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: ['lp-web-search', 'lp-slack-notifier'],
372
- keys: [...WEB_SEARCH_KEYS, ...SLACK_KEYS],
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: 'web_search', decision: 'allow' },
375
- { name: 'template-fetch-allowed', tool: 'web_fetch', decision: 'allow' },
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 and post this week\'s research digest.' },
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
- rules: plan.rules.map((r) => ({ name: slugify(`${plan.id}-${r.tool}`, `${plan.id}-rule`), tool: r.tool, decision: r.decision })),
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
- const NPM_SEARCH_SIZE = 20;
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
- const results = (data.objects ?? []).map((o) => ({
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
- export declare function addActauthRule(agentName: string, rule: ActauthRuleInput): void;
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
- export function addActauthRule(agentName, rule) {
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
- rules.add(doc.createNode({ name: rule.name, scope: rule.scope, tool: rule.tool, decision: rule.decision }));
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. */