loopengine 0.1.98 → 0.1.99
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.d.ts +35 -2
- package/dist/bin/ability-manager.js +168 -35
- package/dist/core/ability-details.js +11 -0
- package/dist/core/mcp-abilities.d.ts +91 -0
- package/dist/core/mcp-abilities.js +400 -0
- package/dist/core/mcp-ability-generator.d.ts +59 -0
- package/dist/core/mcp-ability-generator.js +193 -0
- package/dist/core/mcp-apps.d.ts +95 -0
- package/dist/core/mcp-apps.js +254 -0
- package/dist/core/mcp-directory.d.ts +32 -0
- package/dist/core/mcp-directory.js +191 -0
- package/dist/core/run-agent.js +5 -2
- package/dist/core/system-agents/interviewer.d.ts +1 -1
- package/dist/core/system-agents/interviewer.js +1 -1
- package/dist/core/system-agents/templates.d.ts +11 -0
- package/dist/core/system-agents/templates.js +22 -16
- package/dist/core/system-agents/workflow-planner.d.ts +36 -3
- package/dist/core/system-agents/workflow-planner.js +23 -6
- package/dist/core/workspace-abilities.js +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/web/abilities-admin.d.ts +1 -1
- package/dist/web/abilities-admin.js +1 -1
- package/dist/web/agents-config-page.js +187 -11
- package/dist/web/console-shell.js +7 -5
- package/dist/web/dev-ui-styles.js +1 -1
- package/dist/web/global-config-page.js +293 -13
- package/dist/web/interview-admin.js +2 -0
- package/dist/web/templates-admin.d.ts +10 -2
- package/dist/web/templates-admin.js +36 -3
- package/dist/web/templates-page.js +1 -0
- package/dist/web/workflow-admin.d.ts +46 -0
- package/dist/web/workflow-admin.js +195 -3
- package/dist/web/workflow-page.js +33 -3
- package/package.json +10 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { nextTimeOfDay, parseTimeOfDay, workspaceTimeZone } from '../time-zone.js';
|
|
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
|
-
const
|
|
4
|
+
const GITHUB_APP = 'io.github.github/github-mcp-server';
|
|
5
5
|
const SLACK_KEYS = ['SLACK_BOT_TOKEN', 'SLACK_DEFAULT_CHANNEL'];
|
|
6
6
|
const WEB_SEARCH_KEYS = [['TAVILY_API_KEY', 'BRAVE_API_KEY', 'FIRECRAWL_API_KEY']];
|
|
7
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)';
|
|
@@ -128,20 +128,22 @@ Rules:
|
|
|
128
128
|
risk: 'medium',
|
|
129
129
|
asksFirst: ['opening or commenting on issues'],
|
|
130
130
|
needs: [
|
|
131
|
-
{ kind: '
|
|
131
|
+
{ kind: 'app', name: 'GitHub', description: 'your repository\'s commits, deploys and issues' },
|
|
132
132
|
{ kind: 'ability', name: 'lp-slack-notifier', description: 'posting the summary to Slack' },
|
|
133
133
|
],
|
|
134
134
|
agent: {
|
|
135
135
|
name: 'incident-triage',
|
|
136
136
|
systemPrompt: `You help the on-call engineer triage incidents.
|
|
137
137
|
|
|
138
|
+
Our repository on GitHub: [owner/name] (edit this). Pass its owner and repo to every GitHub tool.
|
|
139
|
+
|
|
138
140
|
When you get an alert, error or bug report:
|
|
139
141
|
1. Summarise it in two sentences: what is failing and who is affected.
|
|
140
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).
|
|
141
|
-
3. Find what changed shortly before it started:
|
|
142
|
-
4. Check whether it's already known with github_search_issues; read a match with
|
|
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).
|
|
143
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).
|
|
144
|
-
6. If an issue already tracks it, add your findings with
|
|
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.
|
|
145
147
|
|
|
146
148
|
Rules:
|
|
147
149
|
- Only read on your own; never roll anything back yourself.
|
|
@@ -151,12 +153,13 @@ Rules:
|
|
|
151
153
|
},
|
|
152
154
|
},
|
|
153
155
|
setup: {
|
|
154
|
-
abilities: ['lp-
|
|
155
|
-
|
|
156
|
+
abilities: ['lp-slack-notifier'],
|
|
157
|
+
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],
|
|
156
159
|
rules: [
|
|
157
160
|
{ name: 'template-slack-summary-allowed', tool: 'post_to_slack', decision: 'allow' },
|
|
158
|
-
{ name: 'template-new-issues-need-approval', tool: '
|
|
159
|
-
{ name: 'template-comments-need-approval', tool: '
|
|
161
|
+
{ name: 'template-new-issues-need-approval', tool: 'github_issue_write', decision: 'ask' },
|
|
162
|
+
{ name: 'template-comments-need-approval', tool: 'github_add_issue_comment', decision: 'ask' },
|
|
160
163
|
],
|
|
161
164
|
needsApprovals: true,
|
|
162
165
|
notes: ['Send it alerts: paste one into Chat, have your alerting tool call it over HTTP, or reply to it in Slack'],
|
|
@@ -280,12 +283,14 @@ Rules:
|
|
|
280
283
|
asksFirst: ['replying to the sender'],
|
|
281
284
|
needs: [
|
|
282
285
|
{ kind: 'ability', name: 'lp-gmail', description: 'the shared inbox' },
|
|
283
|
-
{ kind: '
|
|
286
|
+
{ kind: 'app', name: 'GitHub', description: 'tasks, as GitHub issues' },
|
|
284
287
|
],
|
|
285
288
|
agent: {
|
|
286
289
|
name: 'inbox-to-tasks',
|
|
287
290
|
systemPrompt: `You turn requests in our shared inbox into tasks, kept as GitHub issues.
|
|
288
291
|
|
|
292
|
+
Tasks live in this GitHub repository: [owner/name] (edit this). Pass its owner and repo to every GitHub tool.
|
|
293
|
+
|
|
289
294
|
Who handles what (edit this; GitHub usernames):
|
|
290
295
|
- Billing and invoices: [username]
|
|
291
296
|
- Technical problems: [username]
|
|
@@ -294,8 +299,8 @@ Who handles what (edit this; GitHub usernames):
|
|
|
294
299
|
When asked to handle new emails:
|
|
295
300
|
1. Find them with gmail_search, query "is:unread in:inbox". Read each with gmail_get_thread.
|
|
296
301
|
2. Decide whether it's a request someone needs to act on. Newsletters, receipts, out-of-office replies and spam are not: mark them read and move on.
|
|
297
|
-
3. Check whether a task already exists with github_search_issues (the sender and subject). If so, add the new details with
|
|
298
|
-
4. Otherwise open one with
|
|
302
|
+
3. Check whether a task already exists with github_search_issues (the sender and subject). If so, add the new details with github_add_issue_comment instead.
|
|
303
|
+
4. Otherwise open one with github_issue_write (method create): a short title starting with a verb, a two-line summary, the sender, the due date (the date they asked for, or 2 working days if they didn't say), and the owner from the list above as the assignee.
|
|
299
304
|
5. Reply to the sender with gmail_send (reply_to_message_id set): who is handling it and by when. A person approves it first.
|
|
300
305
|
6. Mark the email read with gmail_modify_labels.
|
|
301
306
|
|
|
@@ -307,14 +312,15 @@ Rules:
|
|
|
307
312
|
},
|
|
308
313
|
},
|
|
309
314
|
setup: {
|
|
310
|
-
abilities: ['lp-gmail'
|
|
311
|
-
|
|
315
|
+
abilities: ['lp-gmail'],
|
|
316
|
+
apps: [{ id: GITHUB_APP, title: 'GitHub', tools: ['search_issues', 'issue_write', 'add_issue_comment'] }],
|
|
317
|
+
keys: [...GMAIL_KEYS],
|
|
312
318
|
rules: [
|
|
313
319
|
{ name: 'template-read-mail-allowed', tool: 'gmail_search', decision: 'allow' },
|
|
314
320
|
{ name: 'template-read-thread-allowed', tool: 'gmail_get_thread', decision: 'allow' },
|
|
315
321
|
{ name: 'template-labels-allowed', tool: 'gmail_modify_labels', decision: 'allow' },
|
|
316
|
-
{ name: 'template-tasks-allowed', tool: '
|
|
317
|
-
{ name: 'template-task-updates-allowed', tool: '
|
|
322
|
+
{ name: 'template-tasks-allowed', tool: 'github_issue_write', decision: 'allow' },
|
|
323
|
+
{ name: 'template-task-updates-allowed', tool: 'github_add_issue_comment', decision: 'allow' },
|
|
318
324
|
{ name: 'template-replies-need-approval', tool: 'gmail_send', decision: 'ask' },
|
|
319
325
|
],
|
|
320
326
|
schedule: { every: '10m', name: 'Check the shared inbox', message: 'Turn the new emails in the shared inbox into tasks.' },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ToolSchema } from '#core/agent-config.js';
|
|
2
2
|
import type { ModelCall } from '#core/run-agent.js';
|
|
3
|
-
import type { StarterTemplate } from './templates.js';
|
|
3
|
+
import type { StarterTemplate, TemplateApp } from './templates.js';
|
|
4
4
|
export type Decision = 'allow' | 'ask' | 'deny';
|
|
5
5
|
export interface WorkflowStep {
|
|
6
6
|
title: string;
|
|
@@ -18,6 +18,30 @@ export interface WorkflowStep {
|
|
|
18
18
|
* an ability): the source's name. It's copied to the agent that runs the
|
|
19
19
|
* workflow, not installed. */
|
|
20
20
|
connection?: string;
|
|
21
|
+
/** For a step done by an MCP server's tool, when no ability has it: the
|
|
22
|
+
* server's id (search_mcp_servers). `tool` is then the server's own
|
|
23
|
+
* tool name, if the planner could see them. On Create the server
|
|
24
|
+
* becomes a private ability (core/mcp-ability-generator.ts) and the
|
|
25
|
+
* step points at its tool instead. */
|
|
26
|
+
mcp?: string;
|
|
27
|
+
}
|
|
28
|
+
/** An MCP server a plan would set up as a private ability. */
|
|
29
|
+
export interface PlannedMcpServer {
|
|
30
|
+
id: string;
|
|
31
|
+
title: string;
|
|
32
|
+
url: string;
|
|
33
|
+
description: string;
|
|
34
|
+
/** Saved by the workspace, or on its publisher's verified domain. */
|
|
35
|
+
verified: boolean;
|
|
36
|
+
source: 'saved' | 'registry';
|
|
37
|
+
/** The private ability it becomes, e.g. mcp-linear. */
|
|
38
|
+
ability: string;
|
|
39
|
+
/** The setting its key is saved as, when it takes one. */
|
|
40
|
+
keyEnv: string;
|
|
41
|
+
/** Whether that setting already has a value. */
|
|
42
|
+
keySet: boolean;
|
|
43
|
+
/** The workspace already has an ability made from this server. */
|
|
44
|
+
existing?: string;
|
|
21
45
|
}
|
|
22
46
|
export interface WorkflowPlan {
|
|
23
47
|
name: string;
|
|
@@ -64,6 +88,11 @@ export interface WorkflowPlan {
|
|
|
64
88
|
};
|
|
65
89
|
/** What the workspace can't do yet, in plain words. */
|
|
66
90
|
gaps: string[];
|
|
91
|
+
/** MCP servers its steps use, set up on Create (filled in after planning). */
|
|
92
|
+
mcpServers?: PlannedMcpServer[];
|
|
93
|
+
/** Connected apps it adds to the agent, with just the tools its steps
|
|
94
|
+
* use (filled in on Create, from mcpServers). */
|
|
95
|
+
apps?: TemplateApp[];
|
|
67
96
|
}
|
|
68
97
|
export interface WorkflowPlannerTools {
|
|
69
98
|
/** Every agent: its tools, installed abilities, rules and messaging. */
|
|
@@ -81,9 +110,13 @@ export interface WorkflowPlannerTools {
|
|
|
81
110
|
/** An ability's tools (with the decision each ships with) and settings. */
|
|
82
111
|
abilityDetails(spec: string): Promise<unknown>;
|
|
83
112
|
listConnections(): Promise<Record<string, unknown>>;
|
|
113
|
+
/** MCP servers matching a query (core/mcp-directory.ts). Optional: without it the planner only plans with abilities. */
|
|
114
|
+
searchMcpServers?(query: string): Promise<unknown>;
|
|
115
|
+
/** A server's tools, when it can be listed without a key (or with one already set). */
|
|
116
|
+
mcpServerTools?(id: string): Promise<unknown>;
|
|
84
117
|
}
|
|
85
|
-
export declare const WORKFLOW_PLANNER_PROMPT = "You turn a workflow a person describes into a plan for a LoopEngine agent.\n\nLoopEngine agents call tools. Tools come from abilities (installable packages: public ones on npm, or the workspace's own private ones) or are built in. Each tool has a permission: allow (runs on its own), ask (waits for a person to approve, edit or deny it) or deny. When nobody is in Chat, questions and approvals go to the agent's messaging app (Slack, Telegram, Lark or email) and the run waits, for days if needed, then carries on.\n\nHow to plan:\n1. Look before you plan: list_agents (what exists, and what each agent's tools and rules are), list_workspace_abilities, search_public_abilities for each kind of step, ability_details for any ability you might use (its exact tool names and the settings it needs), and list_connections.\n2. Split the workflow into steps, in order. For each step:\n - something to fetch or do: kind \"tool\", with the exact tool name and where it comes from. Pick the source in this order:\n a. A tool the target agent already has (from list_agents): use it, nothing to install.\n b. An ability: a workspace ability (list_workspace_abilities) or a public one (search_public_abilities, e.g. \"firecrawl\" or \"web search\" for searching and reading the web). Set \"ability\" to its install spec (the npm name, or a workspace ability's spec) and use the exact tool name from ability_details.\n - the person chooses or answers (\"ask me which\", \"let me pick\"): kind \"ask\", tool \"system_ask_user\" (built in, no ability). Say what options are offered. It reaches them through the agent's messaging app by itself, so don't add a separate step to post to Slack or another chat first, and don't install a chat ability just to ask.\n - the person reviews or approves before something happens (\"let me review it\", \"before it's sent\"): kind \"approval\", on the tool that does that thing; its rule becomes \"ask\".\n - the agent's own work (writing, summarising, choosing): kind \"other\".\n - nothing found can do it: set \"missing\" to what's needed, and add it to gaps.\n3. Rules: every tool the workflow uses gets one. Reading and searching: allow. Anything a person should see first (sending, publishing, paying, creating something others see): ask. Never set deny unless asked.\n4. Skill: the steps in order, as numbered instructions the agent follows, naming the tools and saying exactly when to stop and ask. Write it so the agent keeps going after each answer or decision.\n5. Trigger: a schedule (\"every Monday at 9\" is every \"7d\", at \"09:00\", weekday 1) or on_request (started from Chat or a message). startMessage is what the agent receives when it starts.\n6. Target: an existing agent only when it already has (or can install) the tools, none of the workflow's rules would change a decision it already has, and the same people would approve. Otherwise a new agent, with a short lowercase-hyphenated name and a system prompt saying who it is and what it's for. Give the reason either way.\n7. keys: the setting names its abilities need that list_connections or the agents don't show as set (from ability_details settings).\n\nThen call save_plan once, with the whole plan. Don't ask the person questions: if something is unclear, plan the most reasonable version and note the assumption in gaps.";
|
|
86
|
-
export declare function workflowPlannerTools(): ToolSchema[];
|
|
118
|
+
export declare const WORKFLOW_PLANNER_PROMPT = "You turn a workflow a person describes into a plan for a LoopEngine agent.\n\nLoopEngine agents call tools. Tools come from abilities (installable packages: public ones on npm, or the workspace's own private ones) or are built in. Each tool has a permission: allow (runs on its own), ask (waits for a person to approve, edit or deny it) or deny. When nobody is in Chat, questions and approvals go to the agent's messaging app (Slack, Telegram, Lark or email) and the run waits, for days if needed, then carries on.\n\nHow to plan:\n1. Look before you plan: list_agents (what exists, and what each agent's tools and rules are), list_workspace_abilities, search_public_abilities for each kind of step, ability_details for any ability you might use (its exact tool names and the settings it needs), and list_connections.\n2. Split the workflow into steps, in order. For each step:\n - something to fetch or do: kind \"tool\", with the exact tool name and where it comes from. Pick the source in this order:\n a. A tool the target agent already has (from list_agents): use it, nothing to install.\n b. An ability: a workspace ability (list_workspace_abilities) or a public one (search_public_abilities, e.g. \"firecrawl\" or \"web search\" for searching and reading the web). Set \"ability\" to its install spec (the npm name, or a workspace ability's spec) and use the exact tool name from ability_details.\n c. Only when no ability can do it, and search_mcp_servers is available: an app's MCP server. Search for the app (e.g. \"linear\", \"stripe\"), pick the app's own server (official), else a saved or verified one, then call mcp_server_tools. Set \"mcp\" to the server's id, and \"tool\" to its exact tool name when mcp_server_tools lists them (leave \"tool\" empty when it needs a key first, and say in \"detail\" what the step does). It's set up as a private ability when the workflow is created, which picks its tools and their permissions, so add no rules for MCP tools.\n - the person chooses or answers (\"ask me which\", \"let me pick\"): kind \"ask\", tool \"system_ask_user\" (built in, no ability). Say what options are offered. It reaches them through the agent's messaging app by itself, so don't add a separate step to post to Slack or another chat first, and don't install a chat ability just to ask.\n - the person reviews or approves before something happens (\"let me review it\", \"before it's sent\"): kind \"approval\", on the tool that does that thing; its rule becomes \"ask\".\n - the agent's own work (writing, summarising, choosing): kind \"other\".\n - nothing found can do it: set \"missing\" to what's needed, and add it to gaps.\n3. Rules: every tool the workflow uses gets one. Reading and searching: allow. Anything a person should see first (sending, publishing, paying, creating something others see): ask. Never set deny unless asked.\n4. Skill: the steps in order, as numbered instructions the agent follows, naming the tools and saying exactly when to stop and ask. Write it so the agent keeps going after each answer or decision.\n5. Trigger: a schedule (\"every Monday at 9\" is every \"7d\", at \"09:00\", weekday 1) or on_request (started from Chat or a message). startMessage is what the agent receives when it starts.\n6. Target: an existing agent only when it already has (or can install) the tools, none of the workflow's rules would change a decision it already has, and the same people would approve. Otherwise a new agent, with a short lowercase-hyphenated name and a system prompt saying who it is and what it's for. Give the reason either way.\n7. keys: the setting names its abilities need that list_connections or the agents don't show as set (from ability_details settings). Not for MCP servers: their keys are asked for on the plan.\n\nThen call save_plan once, with the whole plan. Don't ask the person questions: if something is unclear, plan the most reasonable version and note the assumption in gaps.";
|
|
119
|
+
export declare function workflowPlannerTools(withMcp?: boolean): ToolSchema[];
|
|
87
120
|
/** lowercase-hyphenated, 2–40 characters. */
|
|
88
121
|
export declare function slugify(value: string, fallback?: string): string;
|
|
89
122
|
/** A plan from the model's save_plan input, or undefined when it lacks a
|
|
@@ -9,6 +9,7 @@ How to plan:
|
|
|
9
9
|
- something to fetch or do: kind "tool", with the exact tool name and where it comes from. Pick the source in this order:
|
|
10
10
|
a. A tool the target agent already has (from list_agents): use it, nothing to install.
|
|
11
11
|
b. An ability: a workspace ability (list_workspace_abilities) or a public one (search_public_abilities, e.g. "firecrawl" or "web search" for searching and reading the web). Set "ability" to its install spec (the npm name, or a workspace ability's spec) and use the exact tool name from ability_details.
|
|
12
|
+
c. Only when no ability can do it, and search_mcp_servers is available: an app's MCP server. Search for the app (e.g. "linear", "stripe"), pick the app's own server (official), else a saved or verified one, then call mcp_server_tools. Set "mcp" to the server's id, and "tool" to its exact tool name when mcp_server_tools lists them (leave "tool" empty when it needs a key first, and say in "detail" what the step does). It's set up as a private ability when the workflow is created, which picks its tools and their permissions, so add no rules for MCP tools.
|
|
12
13
|
- the person chooses or answers ("ask me which", "let me pick"): kind "ask", tool "system_ask_user" (built in, no ability). Say what options are offered. It reaches them through the agent's messaging app by itself, so don't add a separate step to post to Slack or another chat first, and don't install a chat ability just to ask.
|
|
13
14
|
- the person reviews or approves before something happens ("let me review it", "before it's sent"): kind "approval", on the tool that does that thing; its rule becomes "ask".
|
|
14
15
|
- the agent's own work (writing, summarising, choosing): kind "other".
|
|
@@ -17,10 +18,10 @@ How to plan:
|
|
|
17
18
|
4. Skill: the steps in order, as numbered instructions the agent follows, naming the tools and saying exactly when to stop and ask. Write it so the agent keeps going after each answer or decision.
|
|
18
19
|
5. Trigger: a schedule ("every Monday at 9" is every "7d", at "09:00", weekday 1) or on_request (started from Chat or a message). startMessage is what the agent receives when it starts.
|
|
19
20
|
6. Target: an existing agent only when it already has (or can install) the tools, none of the workflow's rules would change a decision it already has, and the same people would approve. Otherwise a new agent, with a short lowercase-hyphenated name and a system prompt saying who it is and what it's for. Give the reason either way.
|
|
20
|
-
7. keys: the setting names its abilities need that list_connections or the agents don't show as set (from ability_details settings).
|
|
21
|
+
7. keys: the setting names its abilities need that list_connections or the agents don't show as set (from ability_details settings). Not for MCP servers: their keys are asked for on the plan.
|
|
21
22
|
|
|
22
23
|
Then call save_plan once, with the whole plan. Don't ask the person questions: if something is unclear, plan the most reasonable version and note the assumption in gaps.`;
|
|
23
|
-
export function workflowPlannerTools() {
|
|
24
|
+
export function workflowPlannerTools(withMcp = false) {
|
|
24
25
|
const step = {
|
|
25
26
|
type: 'object',
|
|
26
27
|
properties: {
|
|
@@ -28,6 +29,7 @@ export function workflowPlannerTools() {
|
|
|
28
29
|
kind: { type: 'string', enum: ['tool', 'ask', 'approval', 'other'] },
|
|
29
30
|
tool: { type: 'string' },
|
|
30
31
|
ability: { type: 'string' },
|
|
32
|
+
mcp: { type: 'string', description: "an MCP server's id, for a step no ability can do" },
|
|
31
33
|
detail: { type: 'string' },
|
|
32
34
|
missing: { type: 'string' },
|
|
33
35
|
},
|
|
@@ -39,6 +41,10 @@ export function workflowPlannerTools() {
|
|
|
39
41
|
{ name: 'search_public_abilities', description: 'Search public LoopEngine abilities on npm, e.g. "web search", "wordpress", "slack".', input_schema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] } },
|
|
40
42
|
{ name: 'ability_details', description: "An ability's exact tool names, what each does, the permission each ships with, and the settings it needs. spec: its npm name or workspace spec.", input_schema: { type: 'object', properties: { spec: { type: 'string' } }, required: ['spec'] } },
|
|
41
43
|
{ name: 'list_connections', description: 'What is set up: model providers with keys, messaging apps per agent.', input_schema: { type: 'object', properties: {} } },
|
|
44
|
+
...(withMcp ? [
|
|
45
|
+
{ name: 'search_mcp_servers', description: "Search apps' MCP servers (the workspace's saved ones, then the public MCP Registry), for a step no ability can do. query: the app's name, e.g. \"linear\".", input_schema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] } },
|
|
46
|
+
{ name: 'mcp_server_tools', description: "An MCP server's tools (name, description, read-only or not), or a note that it needs a key before they can be seen. id: from search_mcp_servers.", input_schema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
|
|
47
|
+
] : []),
|
|
42
48
|
{
|
|
43
49
|
name: 'save_plan',
|
|
44
50
|
description: 'Save the workflow plan. Call once, at the end.',
|
|
@@ -110,6 +116,7 @@ export function validateWorkflowPlan(raw) {
|
|
|
110
116
|
...(str(step.detail, 600) ? { detail: str(step.detail, 600) } : {}),
|
|
111
117
|
...(str(step.missing, 300) ? { missing: str(step.missing, 300) } : {}),
|
|
112
118
|
...(str(step.connection, 120) ? { connection: str(step.connection, 120) } : {}),
|
|
119
|
+
...(str(step.mcp, 200) && !str(step.ability, 300) ? { mcp: str(step.mcp, 200) } : {}),
|
|
113
120
|
}];
|
|
114
121
|
});
|
|
115
122
|
if (!steps.length)
|
|
@@ -125,14 +132,17 @@ export function validateWorkflowPlan(raw) {
|
|
|
125
132
|
...(typeof triggerRaw.weekday === 'number' && triggerRaw.weekday >= 0 && triggerRaw.weekday <= 7 ? { weekday: triggerRaw.weekday } : {}),
|
|
126
133
|
}
|
|
127
134
|
: { kind: 'on_request', description: str(triggerRaw.description, 200) ?? 'when asked' };
|
|
135
|
+
// An MCP step's permissions are set when its server becomes an ability.
|
|
136
|
+
const mcpTools = new Set(steps.flatMap((s) => (s.mcp && s.tool ? [s.tool] : [])));
|
|
128
137
|
const rules = (Array.isArray(r.rules) ? r.rules : []).flatMap((x) => {
|
|
129
138
|
const rule = (x ?? {});
|
|
130
139
|
const tool = str(rule.tool, 120);
|
|
131
140
|
return tool && isDecision(rule.decision) && tool !== 'system_ask_user' ? [{ tool, decision: rule.decision }] : [];
|
|
132
|
-
});
|
|
133
|
-
// An approval step's tool always asks first
|
|
141
|
+
}).filter((rule) => !mcpTools.has(rule.tool));
|
|
142
|
+
// An approval step's tool always asks first (an MCP step's tool gets
|
|
143
|
+
// that when its ability is made).
|
|
134
144
|
for (const step of steps) {
|
|
135
|
-
if (step.kind !== 'approval' || !step.tool)
|
|
145
|
+
if (step.kind !== 'approval' || !step.tool || step.mcp)
|
|
136
146
|
continue;
|
|
137
147
|
const rule = rules.find((x) => x.tool === step.tool);
|
|
138
148
|
if (rule)
|
|
@@ -175,7 +185,7 @@ export async function planWorkflowWithModel(modelCall, description, tools) {
|
|
|
175
185
|
for (let round = 0; round <= MAX_TOOL_ROUNDS; round++) {
|
|
176
186
|
let response;
|
|
177
187
|
try {
|
|
178
|
-
response = await modelCall(messages, WORKFLOW_PLANNER_PROMPT, workflowPlannerTools());
|
|
188
|
+
response = await modelCall(messages, WORKFLOW_PLANNER_PROMPT, workflowPlannerTools(Boolean(tools.searchMcpServers)));
|
|
179
189
|
}
|
|
180
190
|
catch (err) {
|
|
181
191
|
// A reply cut off part-way (its tool arguments end mid-JSON) usually
|
|
@@ -226,6 +236,12 @@ export async function planWorkflowWithModel(modelCall, description, tools) {
|
|
|
226
236
|
case 'list_connections':
|
|
227
237
|
output = await tools.listConnections();
|
|
228
238
|
break;
|
|
239
|
+
case 'search_mcp_servers':
|
|
240
|
+
output = tools.searchMcpServers ? await tools.searchMcpServers(String(input.query ?? '')) : 'not available';
|
|
241
|
+
break;
|
|
242
|
+
case 'mcp_server_tools':
|
|
243
|
+
output = tools.mcpServerTools ? await tools.mcpServerTools(String(input.id ?? '')) : 'not available';
|
|
244
|
+
break;
|
|
229
245
|
case 'save_plan':
|
|
230
246
|
plan = validateWorkflowPlan(input);
|
|
231
247
|
output = plan ? 'saved' : 'that plan needs a name, summary, at least one step, a skill body and newAgent.systemPrompt; fix and call again';
|
|
@@ -263,6 +279,7 @@ export function workflowTemplate(plan) {
|
|
|
263
279
|
},
|
|
264
280
|
setup: {
|
|
265
281
|
abilities: plan.abilities,
|
|
282
|
+
...(plan.apps?.length ? { apps: plan.apps } : {}),
|
|
266
283
|
keys: plan.keys,
|
|
267
284
|
skills: [{ id: plan.id, description: plan.skill.description, body: plan.skill.body }],
|
|
268
285
|
rules: plan.rules.map((r) => ({ name: slugify(`${plan.id}-${r.tool}`, `${plan.id}-rule`), tool: r.tool, decision: r.decision })),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The workspace's own copies of private abilities: installed once from a
|
|
2
2
|
// private source (core/ability-sources.ts) on the console's Abilities
|
|
3
|
-
// page, then added to any agent from its
|
|
3
|
+
// page, then added to any agent from its Apps tab. Each copy is a
|
|
4
4
|
// folder under abilities/ in the project root (or the folder
|
|
5
5
|
// LOOPENGINE_ABILITIES_DIR names), and abilities/workspace.json says
|
|
6
6
|
// where each came from:
|
package/dist/index.d.ts
CHANGED
|
@@ -99,6 +99,9 @@ export type { PublicAbilitySearchResult } from './web/abilities-admin.js';
|
|
|
99
99
|
export { AbilitySourceError, abilityOrigin, readAbilitySources, saveAbilitySource, removeAbilitySource, listSourceAbilities, sourceTokenVar, type AbilitySource, type SourceAbility, type AbilityOrigin } from './core/ability-sources.js';
|
|
100
100
|
export { describeAbilitySources, listPrivateAbilities, saveAbilitySourceFromConsole, removeAbilitySourceFromConsole } from './web/ability-sources-admin.js';
|
|
101
101
|
export { readAbilityDetails, readToolDefinitions, type AbilityDetails } from './core/ability-details.js';
|
|
102
|
+
export { loadMcpAbilityTools, mcpAbilityLoadError, closeMcpConnections, resolveMcpManifest, listMcpServerTools, isMcpAuthError, type McpServerManifest, type McpAuth, type InstalledMcpRecord, type McpToolInfo } from './core/mcp-abilities.js';
|
|
103
|
+
export { searchMcpServers, getMcpServer, savedMcpServers, type McpDirectoryServer } from './core/mcp-directory.js';
|
|
104
|
+
export { generateMcpAbility, mcpAbilityNames, McpAbilityGenerationError, type GeneratedMcpAbility, type McpAbilityRequest } from './core/mcp-ability-generator.js';
|
|
102
105
|
export { abilityDetails } from './web/ability-details-admin.js';
|
|
103
106
|
export { WorkspaceAbilityError, listWorkspaceAbilities, addWorkspaceAbility, removeWorkspaceAbility, workspaceAbilitiesDir, type WorkspaceAbility } from './core/workspace-abilities.js';
|
|
104
107
|
export { installWorkspaceAbility, installUploadedAbility, MAX_UPLOAD_BYTES, type AbilityUpload } from './web/workspace-abilities-admin.js';
|
package/dist/index.js
CHANGED
|
@@ -89,6 +89,9 @@ export { searchPublicAbilities, fetchLatestAbilityVersion } from './web/abilitie
|
|
|
89
89
|
export { AbilitySourceError, abilityOrigin, readAbilitySources, saveAbilitySource, removeAbilitySource, listSourceAbilities, sourceTokenVar } from './core/ability-sources.js';
|
|
90
90
|
export { describeAbilitySources, listPrivateAbilities, saveAbilitySourceFromConsole, removeAbilitySourceFromConsole } from './web/ability-sources-admin.js';
|
|
91
91
|
export { readAbilityDetails, readToolDefinitions } from './core/ability-details.js';
|
|
92
|
+
export { loadMcpAbilityTools, mcpAbilityLoadError, closeMcpConnections, resolveMcpManifest, listMcpServerTools, isMcpAuthError } from './core/mcp-abilities.js';
|
|
93
|
+
export { searchMcpServers, getMcpServer, savedMcpServers } from './core/mcp-directory.js';
|
|
94
|
+
export { generateMcpAbility, mcpAbilityNames, McpAbilityGenerationError } from './core/mcp-ability-generator.js';
|
|
92
95
|
export { abilityDetails } from './web/ability-details-admin.js';
|
|
93
96
|
export { WorkspaceAbilityError, listWorkspaceAbilities, addWorkspaceAbility, removeWorkspaceAbility, workspaceAbilitiesDir } from './core/workspace-abilities.js';
|
|
94
97
|
export { installWorkspaceAbility, installUploadedAbility, MAX_UPLOAD_BYTES } from './web/workspace-abilities-admin.js';
|
|
@@ -8,7 +8,7 @@ export declare function searchPublicAbilities(query?: string): Promise<PublicAbi
|
|
|
8
8
|
* `packageName`, or null if it can't be determined — not published
|
|
9
9
|
* there at all (a private/file:/git-installed ability, the common case
|
|
10
10
|
* during local development), a network hiccup, or a timeout. Best-effort
|
|
11
|
-
* only: the
|
|
11
|
+
* only: the Apps tab uses this to decide whether an installed
|
|
12
12
|
* ability's own "Upgrade" button is worth showing, not to block
|
|
13
13
|
* anything, so a failure here surfaces as "can't tell, show the button
|
|
14
14
|
* anyway" rather than an error the operator has to deal with. */
|
|
@@ -43,7 +43,7 @@ const NPM_LATEST_VERSION_TIMEOUT_MS = 5000;
|
|
|
43
43
|
* `packageName`, or null if it can't be determined — not published
|
|
44
44
|
* there at all (a private/file:/git-installed ability, the common case
|
|
45
45
|
* during local development), a network hiccup, or a timeout. Best-effort
|
|
46
|
-
* only: the
|
|
46
|
+
* only: the Apps tab uses this to decide whether an installed
|
|
47
47
|
* ability's own "Upgrade" button is worth showing, not to block
|
|
48
48
|
* anything, so a failure here surfaces as "can't tell, show the button
|
|
49
49
|
* anyway" rather than an error the operator has to deal with. */
|
|
@@ -335,6 +335,16 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
335
335
|
#httpToolAddFieldBtn, #httpToolAddHeaderBtn {
|
|
336
336
|
padding: 6px 12px;
|
|
337
337
|
}
|
|
338
|
+
.app-pick { border: 1px solid var(--line); border-radius: var(--radius); padding: 10px 12px; margin: 6px 0 12px; display: grid; gap: 8px; background: var(--surface); }
|
|
339
|
+
.app-pick-bar { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
|
340
|
+
.app-pick-bar input[type=search] { font: inherit; font-size: 12px; padding: 5px 7px; max-width: 240px; }
|
|
341
|
+
.app-pick-list { display: grid; gap: 2px; max-height: 380px; overflow: auto; border: 1px solid var(--line); border-radius: var(--radius); padding: 4px; background: var(--bg); }
|
|
342
|
+
.app-pick-tool { display: flex; gap: 8px; align-items: flex-start; padding: 5px 6px; border-radius: 6px; font-size: 12.5px; }
|
|
343
|
+
.app-pick-tool:hover { background: var(--surface); }
|
|
344
|
+
.app-pick-tool input { margin-top: 3px; }
|
|
345
|
+
.app-pick-tool code { overflow-wrap: anywhere; }
|
|
346
|
+
.app-pick-tool select { font: inherit; font-size: 11.5px; padding: 1px 4px; }
|
|
347
|
+
.app-pick-desc { display: block; }
|
|
338
348
|
</style>
|
|
339
349
|
</head>
|
|
340
350
|
<body data-le-page="agent-config">
|
|
@@ -361,7 +371,7 @@ ${consoleShellScript}
|
|
|
361
371
|
var actauthLoadedFor = null;
|
|
362
372
|
// Same idea again, for the Environment tab's own GET .../env.
|
|
363
373
|
var envLoadedFor = null;
|
|
364
|
-
// Same idea again, for the
|
|
374
|
+
// Same idea again, for the Apps tab's own GET .../abilities.
|
|
365
375
|
var abilitiesLoadedFor = null;
|
|
366
376
|
// The full /agents/:name/config response the currently-open agent was
|
|
367
377
|
// last rendered from — kept around so refreshActauthDependentPanels/
|
|
@@ -637,8 +647,8 @@ ${consoleShellScript}
|
|
|
637
647
|
return '<div class="le-cards" style="margin-bottom:28px">' +
|
|
638
648
|
card(null, 'Model', escapeHtml(modelValue), escapeHtml(modelDetail)) +
|
|
639
649
|
(cfg.abilityCount != null
|
|
640
|
-
? card('abilities', '
|
|
641
|
-
: card('abilities', '
|
|
650
|
+
? card('abilities', 'Apps', String(cfg.abilityCount), cfg.abilityCount ? 'Apps and abilities added to this agent' : 'Add apps and abilities, with the tools you choose')
|
|
651
|
+
: card('abilities', 'Apps', 'Browse', 'Add apps and abilities, with the tools you choose')) +
|
|
642
652
|
card('tools', 'Tools', String(cfg.tools.length), escapeHtml(toolNames.slice(0, 3).join(', ') + (toolNames.length > 3 ? ', \u2026' : '')) || 'None yet') +
|
|
643
653
|
card('skills', 'Skills', String(cfg.skills.length), escapeHtml(cfg.skills.map(function (k) { return k.name; }).join(', ')) || 'None yet') +
|
|
644
654
|
card('actauth', 'Permissions', rules.length + (rules.length === 1 ? ' rule' : ' rules'),
|
|
@@ -2308,7 +2318,7 @@ ${consoleShellScript}
|
|
|
2308
2318
|
});
|
|
2309
2319
|
}
|
|
2310
2320
|
|
|
2311
|
-
// ----
|
|
2321
|
+
// ---- Apps tab: what's already installed for this agent, a
|
|
2312
2322
|
// search box over public (npm, tagged loopengine-ability) packages,
|
|
2313
2323
|
// and a plain spec field for a private ability (a git URL, a local
|
|
2314
2324
|
// file:../path, or a private registry name) — both installs go
|
|
@@ -2349,7 +2359,7 @@ ${consoleShellScript}
|
|
|
2349
2359
|
'<td><code>' + escapeHtml(a.name) + '</code></td>' +
|
|
2350
2360
|
'<td>' + escapeHtml(a.version) + '</td>' +
|
|
2351
2361
|
'<td>' + leShell.abilityOriginBadge(a.origin) + '</td>' +
|
|
2352
|
-
'<td class="hint">' + escapeHtml((a.tools || []).join(', ')) + '</td>' +
|
|
2362
|
+
'<td class="hint">' + escapeHtml((a.tools || []).concat(a.mcp ? a.mcp.tools.map(function (t) { return t.name; }) : []).join(', ')) + '</td>' +
|
|
2353
2363
|
'<td style="white-space:nowrap">' + detailsHtml + ' ' + actionHtml + '</td>' +
|
|
2354
2364
|
'</tr>';
|
|
2355
2365
|
}
|
|
@@ -2374,11 +2384,15 @@ ${consoleShellScript}
|
|
|
2374
2384
|
var installedHtml = installed.length
|
|
2375
2385
|
? '<table><thead><tr><th>Name</th><th>Version</th><th>Source</th><th>Tools</th><th></th></tr></thead><tbody>' + installed.map(renderInstalledAbilityRow).join('') + '</tbody></table>' +
|
|
2376
2386
|
'<p id="abilityUpgradeStatus" class="hint"></p>'
|
|
2377
|
-
: '<p class="hint">
|
|
2387
|
+
: '<p class="hint">Nothing added to this agent yet.</p>';
|
|
2378
2388
|
|
|
2379
|
-
|
|
2389
|
+
// What this agent has first; then the workspace's connected apps (each
|
|
2390
|
+
// with the tools this agent gets), then public and private abilities.
|
|
2391
|
+
return '<h3 style="justify-content:flex-start;align-items:baseline;gap:8px">Installed <span class="hint" style="font-weight:400">the apps and abilities this agent has</span></h3>' + installedHtml +
|
|
2380
2392
|
// Installing by name or spec is on the console's Abilities page.
|
|
2381
2393
|
'<p id="abilityInstallStatus" class="hint"></p>' +
|
|
2394
|
+
'<h3 style="margin-top:24px;justify-content:flex-start;align-items:baseline;gap:8px">Apps <span class="hint" style="font-weight:400">connected in this workspace; choose which of their tools this agent gets, and each one' + String.fromCharCode(8217) + 's permission</span></h3>' +
|
|
2395
|
+
'<div id="agentApps" style="margin-bottom:32px"><p class="hint">Loading…</p></div>' +
|
|
2382
2396
|
'<h3 style="margin-top:24px;justify-content:flex-start;align-items:baseline;gap:8px">Public abilities <span class="hint" style="font-weight:400">npm packages tagged loopengine-ability</span></h3>' +
|
|
2383
2397
|
'<form id="abilitySearchForm" style="display:flex;gap:6px;margin:0 0 10px;max-width:560px">' +
|
|
2384
2398
|
'<input type="search" name="q" placeholder="Search abilities, e.g. gmail" aria-label="Search public abilities" style="flex:1">' +
|
|
@@ -2456,7 +2470,8 @@ ${consoleShellScript}
|
|
|
2456
2470
|
.then(function (result) {
|
|
2457
2471
|
if (!result.ok) throw new Error(result.body.error || 'request failed');
|
|
2458
2472
|
var files = result.body.files || [];
|
|
2459
|
-
|
|
2473
|
+
// added/removed: new rules, and tool files a version dropped (its tools now come from the app's MCP server).
|
|
2474
|
+
var updated = files.filter(function (f) { return f.status === 'updated' || f.status === 'added' || f.status === 'removed'; });
|
|
2460
2475
|
var conflicts = files.filter(function (f) { return f.status === 'conflict'; });
|
|
2461
2476
|
var message;
|
|
2462
2477
|
if (!files.length || (!updated.length && !conflicts.length)) {
|
|
@@ -2565,7 +2580,8 @@ ${consoleShellScript}
|
|
|
2565
2580
|
.then(function (r) { return r.json(); })
|
|
2566
2581
|
.then(function (d) {
|
|
2567
2582
|
if (currentName !== name) return;
|
|
2568
|
-
|
|
2583
|
+
// Connected apps are listed under Apps, above.
|
|
2584
|
+
var abilities = (d.abilities || []).filter(function (a) { return !a.app; });
|
|
2569
2585
|
if (!abilities.length) {
|
|
2570
2586
|
el.innerHTML = '<p><a href="/config?section=abilities#private-abilities">Install private abilities</a></p>';
|
|
2571
2587
|
return;
|
|
@@ -2665,6 +2681,165 @@ ${consoleShellScript}
|
|
|
2665
2681
|
runAbilitySearch(name, '', content, resultsEl, installedNames);
|
|
2666
2682
|
}
|
|
2667
2683
|
loadPrivateAbilitiesFor(name, content, installedNames);
|
|
2684
|
+
loadAgentApps(name, content);
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
// ---- Apps: a connected app's tools for this agent ----
|
|
2688
|
+
// The workspace's connected apps (the console's Apps page). Adding one,
|
|
2689
|
+
// or Choose tools on one already added, opens a picker: each tool with a
|
|
2690
|
+
// checkbox and this agent's permission for it; Save installs the app
|
|
2691
|
+
// with just those tools, or changes the agent's choice.
|
|
2692
|
+
|
|
2693
|
+
var agentApps = { name: null, connected: [], pickers: {} };
|
|
2694
|
+
var DECISION_NAMES = { allow: 'Allow', ask: 'Ask me', deny: 'Never' };
|
|
2695
|
+
|
|
2696
|
+
function loadAgentApps(name, content) {
|
|
2697
|
+
var el = content.querySelector('#agentApps');
|
|
2698
|
+
if (!el) return;
|
|
2699
|
+
if (agentApps.name !== name) agentApps = { name: name, connected: [], pickers: {} };
|
|
2700
|
+
fetch('/config/apps', { cache: 'no-store' })
|
|
2701
|
+
.then(function (r) { return r.json(); })
|
|
2702
|
+
.then(function (d) {
|
|
2703
|
+
if (currentName !== name) return;
|
|
2704
|
+
agentApps.connected = d.connected || [];
|
|
2705
|
+
agentApps.canChange = Boolean(d.canConnect);
|
|
2706
|
+
renderAgentApps();
|
|
2707
|
+
})
|
|
2708
|
+
.catch(function (err) { el.innerHTML = '<p class="error">Could not list apps: ' + escapeHtml(err.message) + '</p>'; });
|
|
2709
|
+
if (!content.dataset.appsWired) {
|
|
2710
|
+
content.dataset.appsWired = '1';
|
|
2711
|
+
content.addEventListener('click', onAgentAppsClick);
|
|
2712
|
+
content.addEventListener('change', onAgentAppsChange);
|
|
2713
|
+
content.addEventListener('input', onAgentAppsInput);
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
function renderAgentApps() {
|
|
2718
|
+
var content = abilitiesContentEl();
|
|
2719
|
+
var el = content && content.querySelector('#agentApps');
|
|
2720
|
+
if (!el) return;
|
|
2721
|
+
var name = agentApps.name;
|
|
2722
|
+
if (!agentApps.connected.length) {
|
|
2723
|
+
el.innerHTML = '<p class="hint">No apps connected yet. <a href="/config?section=apps">Connect one on the Apps page</a>, then add it here.</p>';
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
el.innerHTML = '<table><thead><tr><th>App</th><th>For this agent</th><th></th></tr></thead><tbody>' + agentApps.connected.map(function (c) {
|
|
2727
|
+
var added = c.agents.indexOf(name) !== -1;
|
|
2728
|
+
var open = agentApps.pickers[c.ability];
|
|
2729
|
+
return '<tr><td><strong>' + escapeHtml(c.title) + '</strong> <span class="hint">' + escapeHtml(c.ability) + '</span></td>' +
|
|
2730
|
+
'<td class="hint">' + (added ? 'Added' : 'Not added') + '</td>' +
|
|
2731
|
+
'<td style="white-space:nowrap"><button type="button" class="app-pick-open" data-ability="' + escapeHtml(c.ability) + '">' + (open ? 'Close' : added ? 'Choose tools' : 'Add') + '</button></td></tr>' +
|
|
2732
|
+
(open ? '<tr><td colspan="3">' + pickerHtml(c.ability) + '</td></tr>' : '');
|
|
2733
|
+
}).join('') + '</tbody></table>' +
|
|
2734
|
+
'<p class="hint" style="margin:6px 0 0"><a href="/config?section=apps">Connect more apps</a></p>';
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
function pickerHtml(ability) {
|
|
2738
|
+
var p = agentApps.pickers[ability];
|
|
2739
|
+
if (p.error) return '<div class="app-pick"><p class="error" style="margin:0">' + escapeHtml(p.error) + '</p></div>';
|
|
2740
|
+
if (!p.tools) return '<div class="app-pick"><p class="hint" style="margin:0">Asking the app for its tools' + String.fromCharCode(8230) + '</p></div>';
|
|
2741
|
+
var chosen = p.tools.filter(function (t) { return t.selected; }).length;
|
|
2742
|
+
var f = (p.filter || '').toLowerCase();
|
|
2743
|
+
var shown = p.tools.filter(function (t) { return !f || (t.remote + ' ' + t.description).toLowerCase().indexOf(f) !== -1; });
|
|
2744
|
+
var disabled = agentApps.canChange && !p.busy ? '' : ' disabled';
|
|
2745
|
+
return '<div class="app-pick">' +
|
|
2746
|
+
'<p class="hint" style="margin:0">Choose the tools this agent can use, and what each may do on its own: Allow runs it, Ask me waits for your OK, Never blocks it. Each starts at the app' + String.fromCharCode(8217) + 's default.</p>' +
|
|
2747
|
+
'<div class="app-pick-bar">' +
|
|
2748
|
+
(p.tools.length > 12 ? '<input type="search" class="app-pick-filter" data-ability="' + escapeHtml(ability) + '" placeholder="Filter tools" value="' + escapeHtml(p.filter || '') + '" aria-label="Filter tools">' : '') +
|
|
2749
|
+
'<button type="button" class="app-pick-readonly" data-ability="' + escapeHtml(ability) + '"' + disabled + '>Select read-only tools</button>' +
|
|
2750
|
+
'<button type="button" class="app-pick-none" data-ability="' + escapeHtml(ability) + '"' + disabled + '>Clear</button>' +
|
|
2751
|
+
'<span class="hint">' + chosen + ' of ' + p.tools.length + ' selected' + (chosen > 20 ? '. That' + String.fromCharCode(8217) + 's a lot: more tools make an agent slower to choose and likelier to pick the wrong one.' : '') + '</span>' +
|
|
2752
|
+
'</div>' +
|
|
2753
|
+
'<div class="app-pick-list">' + shown.map(function (t) {
|
|
2754
|
+
return '<label class="app-pick-tool"><input type="checkbox" class="app-pick-check" data-ability="' + escapeHtml(ability) + '" data-tool="' + escapeHtml(t.remote) + '"' + (t.selected ? ' checked' : '') + disabled + '>' +
|
|
2755
|
+
'<span><code>' + escapeHtml(t.name) + '</code> ' +
|
|
2756
|
+
'<select class="app-pick-decision" data-ability="' + escapeHtml(ability) + '" data-tool="' + escapeHtml(t.remote) + '" aria-label="Permission for ' + escapeHtml(t.name) + '"' + disabled + '>' +
|
|
2757
|
+
['allow', 'ask', 'deny'].map(function (d) { return '<option value="' + d + '"' + (d === t.decision ? ' selected' : '') + '>' + DECISION_NAMES[d] + '</option>'; }).join('') + '</select>' +
|
|
2758
|
+
(t.destructive ? ' <span class="hint">destructive</span>' : t.readOnly ? ' <span class="hint">read-only</span>' : '') +
|
|
2759
|
+
(t.description ? '<span class="hint app-pick-desc">' + escapeHtml(t.description) + '</span>' : '') +
|
|
2760
|
+
'</span></label>';
|
|
2761
|
+
}).join('') + '</div>' +
|
|
2762
|
+
'<div class="app-pick-bar"><button type="button" class="app-pick-save" data-ability="' + escapeHtml(ability) + '"' + (chosen && !p.busy && agentApps.canChange ? '' : ' disabled') + '>' + (p.installed ? 'Save' : 'Add with ' + chosen + ' tool' + (chosen === 1 ? '' : 's')) + '</button>' +
|
|
2763
|
+
(agentApps.canChange ? '' : '<span class="error">Changing an agent' + String.fromCharCode(8217) + 's tools needs LOOPENGINE_ADMIN_AUTH set.</span>') +
|
|
2764
|
+
(p.message ? '<span class="' + (p.messageBad ? 'error' : 'hint') + '">' + escapeHtml(p.message) + '</span>' : '') + '</div>' +
|
|
2765
|
+
'</div>';
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
function pickerTool(ability, remote) {
|
|
2769
|
+
var tools = agentApps.pickers[ability].tools;
|
|
2770
|
+
for (var i = 0; i < tools.length; i++) if (tools[i].remote === remote) return tools[i];
|
|
2771
|
+
return null;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
function openPicker(ability) {
|
|
2775
|
+
var name = agentApps.name;
|
|
2776
|
+
agentApps.pickers[ability] = { tools: null, filter: '' };
|
|
2777
|
+
renderAgentApps();
|
|
2778
|
+
fetch('/agents/' + encodeURIComponent(name) + '/apps/' + encodeURIComponent(ability), { cache: 'no-store' })
|
|
2779
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
2780
|
+
.then(function (r) {
|
|
2781
|
+
var p = agentApps.pickers[ability];
|
|
2782
|
+
if (!p || agentApps.name !== name) return;
|
|
2783
|
+
if (!r.ok) p.error = r.body.error || 'Could not list its tools.';
|
|
2784
|
+
else { p.tools = r.body.tools; p.installed = r.body.installed; }
|
|
2785
|
+
renderAgentApps();
|
|
2786
|
+
})
|
|
2787
|
+
.catch(function (err) { var p = agentApps.pickers[ability]; if (p) { p.error = err.message; renderAgentApps(); } });
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
function savePicker(ability) {
|
|
2791
|
+
var name = agentApps.name;
|
|
2792
|
+
var p = agentApps.pickers[ability];
|
|
2793
|
+
var tools = p.tools.filter(function (t) { return t.selected; }).map(function (t) { return { remote: t.remote, decision: t.decision }; });
|
|
2794
|
+
p.busy = true;
|
|
2795
|
+
p.message = 'Saving' + String.fromCharCode(8230);
|
|
2796
|
+
p.messageBad = false;
|
|
2797
|
+
renderAgentApps();
|
|
2798
|
+
fetch('/agents/' + encodeURIComponent(name) + '/apps/' + encodeURIComponent(ability), { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ tools: tools }) })
|
|
2799
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
2800
|
+
.then(function (r) {
|
|
2801
|
+
p.busy = false;
|
|
2802
|
+
if (!r.ok) { p.message = r.body.error || 'Could not save.'; p.messageBad = true; renderAgentApps(); return; }
|
|
2803
|
+
delete agentApps.pickers[ability];
|
|
2804
|
+
// The Installed table and the counts change too.
|
|
2805
|
+
loadAbilitiesTab(name);
|
|
2806
|
+
})
|
|
2807
|
+
.catch(function (err) { p.busy = false; p.message = err.message; p.messageBad = true; renderAgentApps(); });
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
function onAgentAppsClick(ev) {
|
|
2811
|
+
var t = ev.target;
|
|
2812
|
+
if (!t.dataset || !t.dataset.ability) return;
|
|
2813
|
+
var ability = t.dataset.ability;
|
|
2814
|
+
if (t.classList.contains('app-pick-open')) {
|
|
2815
|
+
if (agentApps.pickers[ability]) { delete agentApps.pickers[ability]; renderAgentApps(); }
|
|
2816
|
+
else openPicker(ability);
|
|
2817
|
+
} else if (t.classList.contains('app-pick-readonly') || t.classList.contains('app-pick-none')) {
|
|
2818
|
+
var readOnly = t.classList.contains('app-pick-readonly');
|
|
2819
|
+
agentApps.pickers[ability].tools.forEach(function (tool) { tool.selected = readOnly ? tool.default === 'allow' : false; });
|
|
2820
|
+
renderAgentApps();
|
|
2821
|
+
} else if (t.classList.contains('app-pick-save')) {
|
|
2822
|
+
savePicker(ability);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
function onAgentAppsChange(ev) {
|
|
2827
|
+
var t = ev.target;
|
|
2828
|
+
if (!t.dataset || !t.dataset.tool || !agentApps.pickers[t.dataset.ability]) return;
|
|
2829
|
+
var tool = pickerTool(t.dataset.ability, t.dataset.tool);
|
|
2830
|
+
if (!tool) return;
|
|
2831
|
+
if (t.classList.contains('app-pick-check')) tool.selected = t.checked;
|
|
2832
|
+
if (t.classList.contains('app-pick-decision')) { tool.decision = t.value; tool.selected = true; }
|
|
2833
|
+
renderAgentApps();
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
function onAgentAppsInput(ev) {
|
|
2837
|
+
var t = ev.target;
|
|
2838
|
+
if (!t.classList.contains('app-pick-filter')) return;
|
|
2839
|
+
agentApps.pickers[t.dataset.ability].filter = t.value;
|
|
2840
|
+
renderAgentApps();
|
|
2841
|
+
var again = abilitiesContentEl().querySelector('.app-pick-filter[data-ability="' + t.dataset.ability + '"]');
|
|
2842
|
+
if (again) { again.focus(); again.setSelectionRange(again.value.length, again.value.length); }
|
|
2668
2843
|
}
|
|
2669
2844
|
|
|
2670
2845
|
function applyAbilitiesConfig(name, data) {
|
|
@@ -3470,7 +3645,8 @@ ${consoleShellScript}
|
|
|
3470
3645
|
var CONFIG_TABS = ['overview', 'abilities', 'actauth', 'messaging', 'schedules', 'env', 'advanced'];
|
|
3471
3646
|
// The old Tools and Skills tabs: now parts of Advanced. A link to one
|
|
3472
3647
|
// opens Advanced at that part.
|
|
3473
|
-
|
|
3648
|
+
// The Apps tab keeps the id 'abilities'; ?tab=apps opens it too.
|
|
3649
|
+
var TAB_ALIASES = { tools: 'advanced', skills: 'advanced', apps: 'abilities' };
|
|
3474
3650
|
|
|
3475
3651
|
function resolveTab(tab) {
|
|
3476
3652
|
tab = TAB_ALIASES[tab] || tab;
|
|
@@ -3607,7 +3783,7 @@ ${consoleShellScript}
|
|
|
3607
3783
|
// own skills. Each part keeps its old data-tab-panel name, which the
|
|
3608
3784
|
// code that redraws it looks for; only .tab-panel is shown or hidden.
|
|
3609
3785
|
'<div class="tab-panel" data-tab-panel="advanced">' +
|
|
3610
|
-
'<p class="muted" style="margin:0 0 20px">For building your own: tools and skills that don't come from an ability, Composio tools, subagents and permission rules in full. What each ability adds is in its Details on the
|
|
3786
|
+
'<p class="muted" style="margin:0 0 20px">For building your own: tools and skills that don't come from an ability, Composio tools, subagents and permission rules in full. What each ability adds is in its Details on the Apps tab.</p>' +
|
|
3611
3787
|
'<h2 class="advanced-part" id="advanced-tools">Tools</h2>' +
|
|
3612
3788
|
'<div data-tab-panel="tools">' + renderToolsTabHtml(cfg) + '</div>' +
|
|
3613
3789
|
'<h2 class="advanced-part" id="advanced-skills">Skills</h2>' +
|