loopengine 0.1.94 → 0.1.97

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.
Files changed (68) hide show
  1. package/README.md +21 -2
  2. package/dist/bin/ability-manager.d.ts +4 -0
  3. package/dist/bin/ability-manager.js +34 -2
  4. package/dist/bin/cli.d.ts +1 -1
  5. package/dist/bin/cli.js +3 -0
  6. package/dist/core/ability-details.d.ts +31 -0
  7. package/dist/core/ability-details.js +140 -0
  8. package/dist/core/ability-sources.d.ts +59 -0
  9. package/dist/core/ability-sources.js +312 -0
  10. package/dist/core/agent-config.d.ts +21 -1
  11. package/dist/core/discover-agents.js +11 -1
  12. package/dist/core/http-notifier.d.ts +7 -1
  13. package/dist/core/http-notifier.js +68 -9
  14. package/dist/core/http-notify-triggers/email.d.ts +2 -2
  15. package/dist/core/http-notify-triggers/email.js +2 -4
  16. package/dist/core/http-notify-triggers/lark.d.ts +2 -2
  17. package/dist/core/http-notify-triggers/lark.js +2 -4
  18. package/dist/core/http-notify-triggers/slack.d.ts +2 -2
  19. package/dist/core/http-notify-triggers/slack.js +2 -4
  20. package/dist/core/http-notify-triggers/telegram.d.ts +28 -0
  21. package/dist/core/http-notify-triggers/telegram.js +71 -0
  22. package/dist/core/http-notify-triggers/webhook.d.ts +2 -2
  23. package/dist/core/http-notify-triggers/webhook.js +2 -4
  24. package/dist/core/model-calls/meta-model-call.d.ts +15 -0
  25. package/dist/core/model-calls/meta-model-call.js +48 -0
  26. package/dist/core/model-calls/openai-model-call.d.ts +5 -0
  27. package/dist/core/model-calls/openai-model-call.js +47 -1
  28. package/dist/core/model-calls/xai-model-call.d.ts +15 -0
  29. package/dist/core/model-calls/xai-model-call.js +45 -0
  30. package/dist/core/notifier-file.d.ts +40 -6
  31. package/dist/core/notifier-file.js +119 -25
  32. package/dist/core/system-agents/workflow-planner.d.ts +100 -0
  33. package/dist/core/system-agents/workflow-planner.js +278 -0
  34. package/dist/core/workflows.d.ts +26 -0
  35. package/dist/core/workflows.js +39 -0
  36. package/dist/core/workspace-abilities.d.ts +31 -0
  37. package/dist/core/workspace-abilities.js +110 -0
  38. package/dist/index.d.ts +18 -4
  39. package/dist/index.js +17 -3
  40. package/dist/web/ability-details-admin.d.ts +17 -0
  41. package/dist/web/ability-details-admin.js +45 -0
  42. package/dist/web/ability-sources-admin.d.ts +33 -0
  43. package/dist/web/ability-sources-admin.js +65 -0
  44. package/dist/web/agent-file-admin.d.ts +1 -1
  45. package/dist/web/agent-file-admin.js +3 -0
  46. package/dist/web/agents-config-page.js +608 -224
  47. package/dist/web/agents-list-page.js +49 -20
  48. package/dist/web/console-shell.js +248 -7
  49. package/dist/web/dev-ui-styles.js +13 -1
  50. package/dist/web/global-config-page.js +403 -81
  51. package/dist/web/global-config.d.ts +44 -1
  52. package/dist/web/global-config.js +162 -2
  53. package/dist/web/interview-admin.js +2 -2
  54. package/dist/web/messaging-icons.d.ts +2 -0
  55. package/dist/web/messaging-icons.js +7 -0
  56. package/dist/web/notifier-admin.d.ts +33 -15
  57. package/dist/web/notifier-admin.js +109 -65
  58. package/dist/web/provider-icons.d.ts +2 -0
  59. package/dist/web/provider-icons.js +10 -0
  60. package/dist/web/templates-admin.d.ts +3 -0
  61. package/dist/web/templates-admin.js +5 -1
  62. package/dist/web/workflow-admin.d.ts +124 -0
  63. package/dist/web/workflow-admin.js +308 -0
  64. package/dist/web/workflow-page.d.ts +1 -0
  65. package/dist/web/workflow-page.js +226 -0
  66. package/dist/web/workspace-abilities-admin.d.ts +24 -0
  67. package/dist/web/workspace-abilities-admin.js +118 -0
  68. package/package.json +13 -1
@@ -0,0 +1,226 @@
1
+ // The console's "Describe a workflow" page (/workflows/new): a person
2
+ // describes a workflow they already have in mind, sees the plan the
3
+ // planner makes of it (web/workflow-admin.ts behind /workflows/*): each
4
+ // step matched to a tool, the checkpoints where it asks them, the
5
+ // permissions, the abilities it needs and the agent it goes into, and
6
+ // creates it with one click. Same shell and styles as the other console
7
+ // pages; like them, its inline script avoids backslashes and template
8
+ // placeholders, since it lives inside a template literal.
9
+ import { devUiCss } from './dev-ui-styles.js';
10
+ import { consoleShellScript, consoleSidebarHtml } from './console-shell.js';
11
+ export const workflowPageHtml = `<!doctype html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="utf-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1">
16
+ <title>Describe a workflow</title>
17
+ <style>${devUiCss}
18
+ .wf { max-width: 860px; display: grid; gap: 18px; }
19
+ .wf textarea { width: 100%; min-height: 130px; box-sizing: border-box; font: inherit; padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); color: var(--ink); }
20
+ .wf-examples { display: flex; flex-wrap: wrap; gap: 6px; }
21
+ .wf-examples button { font-size: 12.5px; }
22
+ .wf-card { border: 1px solid var(--line); border-radius: var(--radius); padding: 14px 16px; display: grid; gap: 10px; background: var(--bg); }
23
+ .wf-card h2 { margin: 0; font-size: 18px; }
24
+ .wf-card h3 { margin: 6px 0 0; font-size: 14px; }
25
+ .wf-steps { width: 100%; border-collapse: collapse; }
26
+ .wf-steps td, .wf-steps th { text-align: left; padding: 8px 10px; border-top: 1px solid var(--line); vertical-align: top; font-size: 13px; }
27
+ .wf-steps th { font-size: 12px; color: var(--ink-muted); font-weight: 600; border-top: 0; }
28
+ .wf-num { width: 24px; color: var(--ink-muted); }
29
+ .wf-target { display: grid; gap: 8px; }
30
+ .wf-target label { display: flex; gap: 8px; align-items: center; font-size: 13.5px; }
31
+ .wf-gaps { border: 1px solid var(--warn); background: var(--warn-soft); border-radius: var(--radius); padding: 10px 14px; }
32
+ .wf-gaps ul, .wf-list { margin: 4px 0 0; padding-left: 18px; }
33
+ .wf-gaps li, .wf-list li { margin: 3px 0; font-size: 13px; }
34
+ .wf-skill { white-space: pre-wrap; font-family: var(--font-mono); font-size: 12px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 10px 12px; max-height: 280px; overflow: auto; }
35
+ .wf-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
36
+ .wf-btn { display: inline-block; padding: 7px 14px; border-radius: 6px; text-decoration: none; font-size: 13px; font-weight: 600; background: var(--accent); color: var(--on-accent); border: 0; cursor: pointer; }
37
+ .wf-btn.plain { background: none; color: var(--ink); border: 1px solid var(--line); font-weight: 500; }
38
+ .wf-btn:disabled { opacity: 0.6; cursor: progress; }
39
+ .hint { font-size: 12px; color: var(--ink-muted); }
40
+ .error { font-size: 13px; color: var(--bad); }
41
+ </style>
42
+ </head>
43
+ <body data-le-page="workflows">
44
+ <div class="le-app">
45
+ ${consoleSidebarHtml}
46
+ <main class="le-main">
47
+ <div class="le-head plain">
48
+ <div class="le-head-row">
49
+ <div class="le-title"><button type="button" class="le-menu-btn" data-le-menu aria-label="Open navigation">&#9776;</button>
50
+ <div><h1>Describe a workflow</h1><p>Say what you want done, step by step, and where you want to decide. You get a plan to check before anything is created.</p></div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ <div class="le-scroll"><div class="le-page"><div class="wf" id="wfPage"><p class="hint">Loading&hellip;</p></div></div></div>
55
+ </main>
56
+ </div>
57
+ ${consoleShellScript}
58
+ <script>
59
+ (function () {
60
+ var page = document.getElementById('wfPage');
61
+ var ELLIPSIS = String.fromCharCode(8230);
62
+ var DASH = String.fromCharCode(8212);
63
+ var LABELS = { allow: 'Allow', ask: 'Ask me', deny: 'Never' };
64
+ var EXAMPLES = [
65
+ ['Blog post', 'Every Monday morning, browse the web for timely blog post ideas about our product area. Ask me on Slack to pick one of the ideas. Then draft the post and let me review it before it is created.'],
66
+ ['Weekly digest', 'Every Friday afternoon, search the web for news about our competitors and summarise the important changes. Send me the summary for review before it goes to the team channel.'],
67
+ ['Refund requests', 'When asked, look up the customer' + String.fromCharCode(8217) + 's order and check our refund policy. If a refund fits the policy, prepare it and ask me to approve it before it is issued.']
68
+ ];
69
+ var state = { description: '', planned: null, agents: [], ready: false, canInstall: false };
70
+
71
+ function esc(s) {
72
+ return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
73
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
74
+ });
75
+ }
76
+ function post(path, body) {
77
+ return fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
78
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); });
79
+ }
80
+ function pill(decision) {
81
+ return '<span class="le-pill ' + esc(decision) + '">' + esc(LABELS[decision] || decision) + '</span>';
82
+ }
83
+
84
+ // ---- 1. Describe it ----
85
+ function renderDescribe(error) {
86
+ page.innerHTML =
87
+ (state.ready ? '' : '<p class="error">Planning needs a model: set one on an agent (Overview ' + String.fromCharCode(8594) + ' Model) and add its key under <a href="/config?section=models">Models</a>.</p>') +
88
+ '<div class="wf-card"><label for="wfText"><strong>Your workflow</strong> <span class="hint">what starts it, the steps in order, and where you want to choose or approve</span></label>' +
89
+ '<textarea id="wfText" placeholder="Every Monday, find blog ideas on the web, ask me on Slack to pick one, draft it, and let me review it before it goes up.">' + esc(state.description) + '</textarea>' +
90
+ '<div class="wf-examples"><span class="hint">Examples:</span>' + EXAMPLES.map(function (e, i) { return '<button type="button" class="wf-btn plain" data-example="' + i + '">' + esc(e[0]) + '</button>'; }).join('') + '</div>' +
91
+ '<div class="wf-actions"><button type="button" class="wf-btn" id="wfPlan"' + (state.ready ? '' : ' disabled') + '>Plan it</button>' +
92
+ '<span class="hint" id="wfStatus">It looks at your agents and abilities first; this can take a few minutes.</span></div>' +
93
+ (error ? '<p class="error">' + esc(error) + '</p>' : '') +
94
+ '</div>' +
95
+ '<p class="hint">Rather start from something ready-made? <a href="/templates">Start from a template</a>, or <a href="/interview">find your first agent</a>.</p>';
96
+ var text = document.getElementById('wfText');
97
+ var examples = page.querySelectorAll('[data-example]');
98
+ for (var i = 0; i < examples.length; i++) {
99
+ examples[i].addEventListener('click', function (ev) { text.value = EXAMPLES[Number(ev.currentTarget.dataset.example)][1]; text.focus(); });
100
+ }
101
+ document.getElementById('wfPlan').addEventListener('click', function () {
102
+ state.description = text.value.trim();
103
+ var btn = document.getElementById('wfPlan');
104
+ btn.disabled = true;
105
+ document.getElementById('wfStatus').textContent = 'Planning' + ELLIPSIS + ' looking at your agents and abilities. This can take a few minutes; keep this page open.';
106
+ var started = Date.now();
107
+ post('/workflows/plan', { description: state.description }).then(function (r) {
108
+ if (!r.ok) { renderDescribe(r.body.error || 'Could not plan it.'); return; }
109
+ waitForPlan(r.body.id, started);
110
+ }).catch(function (err) { renderDescribe(err.message); });
111
+ });
112
+ }
113
+
114
+ // Planning runs on the server as a job; check on it every few seconds.
115
+ function waitForPlan(id, started) {
116
+ setTimeout(function () {
117
+ fetch('/workflows/plan/' + encodeURIComponent(id), { cache: 'no-store' })
118
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
119
+ .then(function (r) {
120
+ if (!r.ok) { renderDescribe(r.body.error || 'Could not plan it.'); return; }
121
+ if (r.body.status === 'planning') {
122
+ var status = document.getElementById('wfStatus');
123
+ if (status) status.textContent = 'Planning' + ELLIPSIS + ' ' + Math.round((Date.now() - started) / 1000) + 's so far. It looks at your agents and abilities first; keep this page open.';
124
+ waitForPlan(id, started);
125
+ return;
126
+ }
127
+ if (r.body.status === 'failed') { renderDescribe(r.body.error || 'Could not plan it.'); return; }
128
+ state.planned = r.body.result;
129
+ renderPlan();
130
+ })
131
+ // A dropped check (a restart, the network): try again.
132
+ .catch(function () { waitForPlan(id, started); });
133
+ }, 3000);
134
+ }
135
+
136
+ // ---- 2. Check the plan ----
137
+ function stepRow(step, i) {
138
+ var becomes = step.tool
139
+ ? '<code>' + esc(step.tool) + '</code>' + (step.ability ? ' <span class="hint">from ' + esc(step.ability) + '</span>' : step.connection ? ' <span class="hint">connected service: ' + esc(step.connection) + '</span>' : (step.tool === 'system_ask_user' ? ' <span class="hint">built in</span>' : ''))
140
+ : '<span class="hint">the agent' + String.fromCharCode(8217) + 's own work</span>';
141
+ var badge = step.missing
142
+ ? '<span class="le-pill bad">Missing</span> <span class="hint">' + esc(step.missing) + '</span>'
143
+ : step.kind === 'ask'
144
+ ? '<span class="le-pill accent">Asks you</span>'
145
+ : step.kind === 'approval'
146
+ ? '<span class="le-pill ask">You approve</span>'
147
+ : '';
148
+ return '<tr><td class="wf-num">' + (i + 1) + '</td><td><strong>' + esc(step.title) + '</strong>' + (step.detail ? '<div class="hint">' + esc(step.detail) + '</div>' : '') + '</td><td>' + becomes + '</td><td>' + badge + '</td></tr>';
149
+ }
150
+
151
+ function renderPlan(error) {
152
+ var p = state.planned.plan;
153
+ var suggested = p.target.kind === 'existing' && p.target.agent ? p.target.agent : '';
154
+ var others = state.agents.filter(function (a) { return a !== suggested; });
155
+ page.innerHTML =
156
+ '<div class="wf-card">' +
157
+ '<h2>' + esc(p.name) + '</h2><p style="margin:0">' + esc(p.summary) + '</p>' +
158
+ '<p class="hint" style="margin:0"><strong>Starts:</strong> ' + esc(p.trigger.description) + (p.trigger.kind === 'schedule' ? ' (created paused; you turn it on)' : '') + '</p>' +
159
+ '<h3>Steps</h3>' +
160
+ '<div class="le-table-wrap"><table class="wf-steps"><thead><tr><th></th><th>Step</th><th>Becomes</th><th>Checkpoint</th></tr></thead><tbody>' + p.steps.map(stepRow).join('') + '</tbody></table></div>' +
161
+ (p.rules.length ? '<h3>Permissions it sets</h3><ul class="wf-list">' + p.rules.map(function (r) { return '<li><code>' + esc(r.tool) + '</code> ' + pill(r.decision) + '</li>'; }).join('') + '</ul>' : '') +
162
+ (p.abilities.length ? '<h3>Abilities it uses</h3><ul class="wf-list">' + p.abilities.map(function (a) { return '<li><code>' + esc(a) + '</code></li>'; }).join('') + '</ul>' + (state.canInstall ? '' : '<p class="hint">Installing abilities needs LOOPENGINE_ADMIN_AUTH set; otherwise they head the checklist.</p>') : '') +
163
+ (p.keys.length ? '<h3>Keys it needs</h3><p class="hint" style="margin:0">' + p.keys.map(esc).join(', ') + ' ' + DASH + ' you add them after, from the checklist.</p>' : '') +
164
+ (p.needsApprovals ? '<p class="hint" style="margin:0">It asks you through the agent' + String.fromCharCode(8217) + 's primary messaging app (Messaging tab) when nobody is in Chat.</p>' : '') +
165
+ '</div>' +
166
+ (p.gaps.length ? '<div class="wf-gaps"><strong>Worth knowing</strong><ul>' + p.gaps.map(function (g) { return '<li>' + esc(g) + '</li>'; }).join('') + '</ul></div>' : '') +
167
+ '<div class="wf-card"><h3 style="margin:0">Where it goes</h3>' +
168
+ '<div class="wf-target">' +
169
+ '<label><input type="radio" name="wfTarget" value="new"' + (suggested ? '' : ' checked') + '> A new agent, <code>' + esc(p.newAgent.name) + '</code></label>' +
170
+ (suggested ? '<label><input type="radio" name="wfTarget" value="existing" data-agent="' + esc(suggested) + '" checked> Add to <code>' + esc(suggested) + '</code></label>' : '') +
171
+ (others.length ? '<label><input type="radio" name="wfTarget" value="other"> Add to <select id="wfOther">' + others.map(function (a) { return '<option>' + esc(a) + '</option>'; }).join('') + '</select></label>' : '') +
172
+ '</div>' +
173
+ (p.target.reason ? '<p class="hint" style="margin:0">' + esc(p.target.reason) + '</p>' : '') +
174
+ (state.planned.note ? '<p class="hint" style="margin:0"><strong>Note:</strong> ' + esc(state.planned.note) + '</p>' : '') +
175
+ '<p class="hint" style="margin:0">Added to an existing agent, it keeps every decision that agent already has; any it would change are listed afterwards.</p>' +
176
+ '</div>' +
177
+ '<details class="wf-card"><summary><strong>The steps the agent follows</strong> <span class="hint">(saved as its skill <code>' + esc(p.id) + '</code>)</span></summary><div class="wf-skill">' + esc(p.skill.body) + '</div></details>' +
178
+ '<div class="wf-actions"><button type="button" class="wf-btn" id="wfCreate">Create it</button><button type="button" class="wf-btn plain" id="wfBack">Change the description</button>' +
179
+ '<span class="hint" id="wfStatus"></span></div>' +
180
+ (error ? '<p class="error">' + esc(error) + '</p>' : '');
181
+ document.getElementById('wfBack').addEventListener('click', function () { renderDescribe(); });
182
+ document.getElementById('wfCreate').addEventListener('click', function () {
183
+ var chosen = page.querySelector('input[name="wfTarget"]:checked');
184
+ var target = chosen.value === 'new' ? { kind: 'new' } : { kind: 'existing', agent: chosen.value === 'other' ? document.getElementById('wfOther').value : chosen.dataset.agent };
185
+ var btn = document.getElementById('wfCreate');
186
+ btn.disabled = true;
187
+ document.getElementById('wfStatus').textContent = 'Creating' + ELLIPSIS + ' installing abilities can take a minute.';
188
+ post('/workflows/create', { plan: p, target: target, description: state.description }).then(function (r) {
189
+ if (!r.ok) { renderPlan(r.body.error || 'Could not create it.'); return; }
190
+ renderDone(r.body);
191
+ }).catch(function (err) { renderPlan(err.message); });
192
+ });
193
+ }
194
+
195
+ // ---- 3. Done ----
196
+ function renderDone(c) {
197
+ var agentHref = '/agents/config?agent=' + encodeURIComponent(c.agent);
198
+ var items = c.checklist || [];
199
+ page.innerHTML = '<div class="wf-card">' +
200
+ '<h2>' + esc(c.workflow) + ' is ready</h2>' +
201
+ '<p style="margin:0">' + (c.newAgent ? 'Created as a new agent, <code>' + esc(c.agent) + '</code>.' : 'Added to <code>' + esc(c.agent) + '</code>.') + '</p>' +
202
+ (c.schedule ? '<p class="hint" style="margin:0">Its schedule (' + esc(c.schedule.description) + ') is paused: turn it on in the agent' + String.fromCharCode(8217) + 's Schedules tab when you' + String.fromCharCode(8217) + 're ready.</p>' : '') +
203
+ (c.scheduleError ? '<p class="error">The schedule could not be created: ' + esc(c.scheduleError) + '</p>' : '') +
204
+ (c.loadError ? '<p class="error">The agent did not load: ' + esc(c.loadError) + '</p>' : '') +
205
+ (c.abilities.failed.length ? '<p class="error">Not installed: ' + c.abilities.failed.map(function (f) { return esc(f.name) + ' (' + esc(f.error) + ')'; }).join('; ') + '</p>' : '') +
206
+ (c.keptDecisions.length ? '<p class="hint" style="margin:0"><strong>Kept as they were:</strong> ' + c.keptDecisions.map(function (k) { return '<code>' + esc(k.tool) + '</code> stays ' + esc(LABELS[k.has]) + ' (the workflow wanted ' + esc(LABELS[k.wants]) + ')'; }).join('; ') + '. Change them in Permissions if you want.</p>' : '') +
207
+ (items.length ? '<h3>Left to do</h3><ul class="wf-list">' + items.map(function (it) { return '<li>' + (it.href ? '<a href="' + esc(it.href) + '">' + esc(it.label) + '</a>' : esc(it.label)) + '</li>'; }).join('') + '</ul>' : '<p class="hint" style="margin:0">Nothing left to set up.</p>') +
208
+ '<div class="wf-actions"><a class="wf-btn" href="' + agentHref + '">Open ' + esc(c.agent) + '</a><a class="wf-btn plain" href="/playground?agent=' + encodeURIComponent(c.agent) + '">Try it in Chat</a>' +
209
+ '<button type="button" class="wf-btn plain" id="wfAnother">Describe another</button></div>' +
210
+ '</div>';
211
+ document.getElementById('wfAnother').addEventListener('click', function () { state.description = ''; state.planned = null; renderDescribe(); });
212
+ }
213
+
214
+ fetch('/workflows', { cache: 'no-store' })
215
+ .then(function (r) { return r.json(); })
216
+ .then(function (d) {
217
+ state.ready = Boolean(d.ready);
218
+ state.canInstall = Boolean(d.canInstallAbilities);
219
+ state.agents = d.agents || [];
220
+ renderDescribe();
221
+ })
222
+ .catch(function (err) { page.innerHTML = '<p class="error">Could not load: ' + esc(err.message) + '</p>'; });
223
+ })();
224
+ </script>
225
+ </body>
226
+ </html>`;
@@ -0,0 +1,24 @@
1
+ import { type WorkspaceAbility } from '../core/workspace-abilities.js';
2
+ /** Fetches `spec` like an agent install would (a private source's token
3
+ * included) and stores it as the workspace's copy, replacing an earlier
4
+ * one. */
5
+ export declare function installWorkspaceAbility(spec: string, options?: {
6
+ fetchDir?: (spec: string) => Promise<string>;
7
+ }): Promise<WorkspaceAbility>;
8
+ /** Largest upload accepted, archive or folder (base64 aside). */
9
+ export declare const MAX_UPLOAD_BYTES: number;
10
+ export type AbilityUpload = {
11
+ kind: 'archive';
12
+ fileName: string;
13
+ data: string;
14
+ } | {
15
+ kind: 'files';
16
+ folderName?: string;
17
+ files: Array<{
18
+ path: string;
19
+ data: string;
20
+ }>;
21
+ };
22
+ /** Stores an uploaded ability (a .zip or .tgz, or a folder's files) as
23
+ * the workspace's copy, recorded as upload:<name>. */
24
+ export declare function installUploadedAbility(upload: AbilityUpload): Promise<WorkspaceAbility>;
@@ -0,0 +1,118 @@
1
+ // Backs the Abilities page's private Install (an ability into the
2
+ // workspace) and each agent's Private abilities list: see
3
+ // core/workspace-abilities.ts.
4
+ import { execFile } from 'node:child_process';
5
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import { basename, dirname, join } from 'node:path';
8
+ import { promisify } from 'node:util';
9
+ import { fetchAbilityDirAsync } from '../bin/ability-manager.js';
10
+ import { WorkspaceAbilityError, addWorkspaceAbility } from '../core/workspace-abilities.js';
11
+ /** Fetches `spec` like an agent install would (a private source's token
12
+ * included) and stores it as the workspace's copy, replacing an earlier
13
+ * one. */
14
+ export async function installWorkspaceAbility(spec, options = {}) {
15
+ const dir = await (options.fetchDir ?? fetchAbilityDirAsync)(spec);
16
+ try {
17
+ return addWorkspaceAbility(dir, spec);
18
+ }
19
+ finally {
20
+ rmSync(dirname(dir), { recursive: true, force: true });
21
+ }
22
+ }
23
+ // ---- Uploading an ability from your own computer ----
24
+ /** Largest upload accepted, archive or folder (base64 aside). */
25
+ export const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
26
+ /** A relative path inside the upload, never one that could land outside
27
+ * it: no absolute paths, drive letters or ".." parts. */
28
+ function safeRelativePath(path) {
29
+ const parts = path.replace(/\\/g, '/').split('/').filter((p) => p && p !== '.');
30
+ if (!parts.length || /^[a-zA-Z]:$/.test(parts[0]) || path.startsWith('/') || parts.some((p) => p === '..')) {
31
+ throw new WorkspaceAbilityError(`The upload has a file outside its folder (${path}), so it was refused`);
32
+ }
33
+ return parts.join('/');
34
+ }
35
+ /** The folder holding loopengine.ability.json: the upload's top level, or
36
+ * the one folder it's wrapped in (package/ from npm pack, or the folder's
37
+ * own name). */
38
+ function findAbilityRoot(dir) {
39
+ if (existsSync(join(dir, 'loopengine.ability.json')))
40
+ return dir;
41
+ const entries = readdirSync(dir, { withFileTypes: true }).filter((e) => !e.name.startsWith('.') && e.name !== '__MACOSX');
42
+ if (entries.length === 1 && entries[0].isDirectory() && existsSync(join(dir, entries[0].name, 'loopengine.ability.json')))
43
+ return join(dir, entries[0].name);
44
+ throw new WorkspaceAbilityError('No loopengine.ability.json at the top of the upload (or in the one folder it holds), so it isn\'t a loopengine ability');
45
+ }
46
+ /** Refuses symlinks anywhere in an extracted upload: one could point
47
+ * outside it. */
48
+ function refuseSymlinks(dir) {
49
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
50
+ const path = join(dir, entry.name);
51
+ if (lstatSync(path).isSymbolicLink())
52
+ throw new WorkspaceAbilityError(`The upload has a link (${entry.name}), so it was refused`);
53
+ if (entry.isDirectory())
54
+ refuseSymlinks(path);
55
+ }
56
+ }
57
+ /** Stores an uploaded ability (a .zip or .tgz, or a folder's files) as
58
+ * the workspace's copy, recorded as upload:<name>. */
59
+ export async function installUploadedAbility(upload) {
60
+ const work = mkdtempSync(join(tmpdir(), 'loopengine-upload-'));
61
+ try {
62
+ const out = join(work, 'out');
63
+ mkdirSync(out);
64
+ let label;
65
+ if (upload.kind === 'archive') {
66
+ const fileName = basename(upload.fileName || 'ability');
67
+ const data = Buffer.from(upload.data, 'base64');
68
+ if (!data.length)
69
+ throw new WorkspaceAbilityError('The file is empty');
70
+ if (data.length > MAX_UPLOAD_BYTES)
71
+ throw new WorkspaceAbilityError('The file is larger than 25 MB');
72
+ const archive = join(work, 'upload');
73
+ writeFileSync(archive, data);
74
+ const run = promisify(execFile);
75
+ if (/\.zip$/i.test(fileName)) {
76
+ const { stdout } = await run('unzip', ['-Z1', archive]).catch(() => run('unzip', ['-l', archive]));
77
+ // `unzip -l` (busybox) lists sizes and dates first; the name is last.
78
+ for (const line of stdout.split('\n')) {
79
+ const name = line.trim().split(/\s+/).slice(/^\d/.test(line.trim()) ? 3 : 0).join(' ');
80
+ if (name && !/^-+$|^Name$|^Archive:|files?$/.test(name))
81
+ safeRelativePath(name);
82
+ }
83
+ await run('unzip', ['-q', archive, '-d', out]);
84
+ }
85
+ else if (/\.(tgz|tar\.gz)$/i.test(fileName)) {
86
+ const { stdout } = await run('tar', ['-tzf', archive]);
87
+ for (const name of stdout.split('\n'))
88
+ if (name.trim())
89
+ safeRelativePath(name.trim());
90
+ await run('tar', ['-xzf', archive, '-C', out]);
91
+ }
92
+ else {
93
+ throw new WorkspaceAbilityError('Upload a .zip or .tgz, or choose a folder');
94
+ }
95
+ label = fileName;
96
+ }
97
+ else {
98
+ let total = 0;
99
+ for (const file of upload.files) {
100
+ const data = Buffer.from(file.data, 'base64');
101
+ total += data.length;
102
+ if (total > MAX_UPLOAD_BYTES)
103
+ throw new WorkspaceAbilityError('The folder is larger than 25 MB');
104
+ const path = join(out, safeRelativePath(file.path));
105
+ mkdirSync(dirname(path), { recursive: true });
106
+ writeFileSync(path, data);
107
+ }
108
+ if (!upload.files.length)
109
+ throw new WorkspaceAbilityError('The folder is empty');
110
+ label = basename(upload.folderName || 'folder');
111
+ }
112
+ refuseSymlinks(out);
113
+ return addWorkspaceAbility(findAbilityRoot(out), `upload:${label}`);
114
+ }
115
+ finally {
116
+ rmSync(work, { recursive: true, force: true });
117
+ }
118
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.1.94",
3
+ "version": "0.1.97",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",
@@ -58,6 +58,12 @@
58
58
  "dist/core/scheduler.js",
59
59
  "dist/core/scheduler.d.ts",
60
60
  "dist/core/time-zone.js",
61
+ "dist/core/ability-sources.js",
62
+ "dist/core/ability-sources.d.ts",
63
+ "dist/core/ability-details.js",
64
+ "dist/core/ability-details.d.ts",
65
+ "dist/core/workspace-abilities.js",
66
+ "dist/core/workspace-abilities.d.ts",
61
67
  "dist/core/time-zone.d.ts",
62
68
  "dist/core/session-store.js",
63
69
  "dist/core/session-store.d.ts",
@@ -75,6 +81,10 @@
75
81
  "dist/core/model-calls/glm-model-call.d.ts",
76
82
  "dist/core/model-calls/gemini-model-call.js",
77
83
  "dist/core/model-calls/gemini-model-call.d.ts",
84
+ "dist/core/model-calls/xai-model-call.js",
85
+ "dist/core/model-calls/xai-model-call.d.ts",
86
+ "dist/core/model-calls/meta-model-call.js",
87
+ "dist/core/model-calls/meta-model-call.d.ts",
78
88
  "dist/core/discover-agents.js",
79
89
  "dist/core/discover-agents.d.ts",
80
90
  "dist/core/agent-as-tool.js",
@@ -104,6 +114,8 @@
104
114
  "dist/core/skillgarden",
105
115
  "dist/core/durable-approvals.js",
106
116
  "dist/core/durable-approvals.d.ts",
117
+ "dist/core/workflows.js",
118
+ "dist/core/workflows.d.ts",
107
119
  "dist/web",
108
120
  "protocol/loop-event.schema.json",
109
121
  "PROTOCOL.md",