loopengine 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/core/run-agent.js +11 -2
- package/dist/core/run-callbacks.d.ts +39 -0
- package/dist/core/run-callbacks.js +125 -0
- package/dist/core/scheduler.d.ts +29 -1
- package/dist/core/scheduler.js +28 -4
- package/dist/core/skillgarden/loader.d.ts +5 -1
- package/dist/core/skillgarden/loader.js +10 -3
- package/dist/core/skillgarden/substitute.d.ts +15 -1
- package/dist/core/skillgarden/substitute.js +28 -6
- package/dist/core/system-agents/templates.d.ts +5 -0
- package/dist/core/system-agents/workflow-planner.d.ts +26 -1
- package/dist/core/system-agents/workflow-planner.js +57 -5
- package/dist/core/system-tools/schedule.d.ts +5 -1
- package/dist/core/system-tools/schedule.js +45 -10
- package/dist/core/workflow-runs.d.ts +131 -0
- package/dist/core/workflow-runs.js +229 -0
- package/dist/core/workflow-triggers.d.ts +130 -0
- package/dist/core/workflow-triggers.js +361 -0
- package/dist/core/workflows.d.ts +52 -23
- package/dist/core/workflows.js +218 -27
- package/dist/index.d.ts +7 -4
- package/dist/index.js +7 -4
- package/dist/web/agents-config-page.js +394 -22
- package/dist/web/skills-admin.d.ts +4 -8
- package/dist/web/skills-admin.js +8 -12
- package/dist/web/templates-admin.js +7 -2
- package/dist/web/workflow-admin.d.ts +43 -18
- package/dist/web/workflow-admin.js +117 -50
- package/dist/web/workflow-page.js +72 -0
- package/package.json +7 -1
|
@@ -49,8 +49,20 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
49
49
|
form.add-source .schedule-when input[type="number"] { width: 80px; }
|
|
50
50
|
form.add-source .schedule-when select, form.add-source .schedule-when input[type="datetime-local"] { width: auto; }
|
|
51
51
|
.workflow-card { margin-bottom: 10px; }
|
|
52
|
-
.workflow-steps {
|
|
53
|
-
.workflow-
|
|
52
|
+
.workflow-steps-text { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13px; margin: 6px 0 0; padding: 8px 10px; border-radius: 6px; background: var(--le-surface, #f6f7f9); }
|
|
53
|
+
.workflow-params { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
|
|
54
|
+
.workflow-params[hidden] { display: none; }
|
|
55
|
+
.workflow-editor[hidden] { display: none; }
|
|
56
|
+
.workflow-hooks[hidden] { display: none; }
|
|
57
|
+
.hook-custom[hidden] { display: none; }
|
|
58
|
+
.workflow-hook { border-top: 1px solid var(--le-line, #e5e7eb); padding: 8px 0; }
|
|
59
|
+
.hook-url { display: flex; flex-direction: column; gap: 4px; padding: 8px 10px; margin: 6px 0; border-radius: 6px; background: var(--le-surface, #f6f7f9); font-size: 13px; }
|
|
60
|
+
.hook-url input { font-family: var(--le-mono, monospace); font-size: 12.5px; }
|
|
61
|
+
.workflow-param-rows { display: flex; flex-direction: column; gap: 6px; margin: 6px 0; }
|
|
62
|
+
.workflow-param-row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
|
63
|
+
.workflow-param-row .wp-name { width: 160px; font-family: var(--le-mono, monospace); }
|
|
64
|
+
.workflow-param-row .wp-desc { flex: 1; min-width: 200px; }
|
|
65
|
+
.workflow-param { display: flex; flex-direction: column; gap: 3px; font-size: 12px; min-width: 180px; }
|
|
54
66
|
.advanced-part { font-size: 17px; font-weight: 600; margin: 28px 0 12px; padding-top: 16px; border-top: 1px solid var(--line); }
|
|
55
67
|
.advanced-part#advanced-tools { margin-top: 0; padding-top: 0; border-top: 0; }
|
|
56
68
|
.approvals-nudge { border: 1px solid var(--warn); background: var(--warn-soft); color: var(--ink); border-radius: var(--radius); padding: 12px 14px; margin-bottom: 24px; font-size: 13px; }
|
|
@@ -192,6 +204,9 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
192
204
|
max-width: 480px;
|
|
193
205
|
}
|
|
194
206
|
form.add-source label { font-size: 12px; display: flex; flex-direction: column; gap: 4px; }
|
|
207
|
+
form.add-source label[hidden] { display: none; }
|
|
208
|
+
.schedule-notify { display: flex; align-items: center; gap: 6px; margin-top: 4px; font-size: 12px; cursor: pointer; }
|
|
209
|
+
.schedule-notify input { width: auto; margin: 0; }
|
|
195
210
|
form.add-source input, form.add-source select, form.add-source textarea { width: 100%; }
|
|
196
211
|
form.add-source textarea { font-family: ui-monospace, monospace; min-height: 60px; }
|
|
197
212
|
/* Overrides form.add-source's shared max-width/textarea defaults for
|
|
@@ -678,7 +693,7 @@ ${consoleShellScript}
|
|
|
678
693
|
return workflows.map(function (w) {
|
|
679
694
|
var s = w.schedule;
|
|
680
695
|
var state = !s
|
|
681
|
-
?
|
|
696
|
+
? '<span class="hint">' + escapeHtml(w.trigger) + '</span>'
|
|
682
697
|
: s.waiting
|
|
683
698
|
? '<span class="le-pill ask">Waiting for a decision</span>'
|
|
684
699
|
: s.status === 'paused'
|
|
@@ -686,53 +701,337 @@ ${consoleShellScript}
|
|
|
686
701
|
: s.status === 'running'
|
|
687
702
|
? '<span class="le-pill accent">Running</span>'
|
|
688
703
|
: '<span class="le-pill good">On</span>' + (s.nextRunAt ? ' <span class="hint">next ' + escapeHtml(new Date(s.nextRunAt).toLocaleString()) + '</span>' : '');
|
|
689
|
-
var
|
|
690
|
-
|
|
691
|
-
|
|
704
|
+
var paramNames = Object.keys(w.parameters || {});
|
|
705
|
+
var params = paramNames.map(function (p) {
|
|
706
|
+
var spec = w.parameters[p];
|
|
707
|
+
return '<label class="workflow-param">' + escapeHtml(p) + (spec.required ? '' : ' <span class="hint">(optional)</span>') +
|
|
708
|
+
'<input name="' + escapeHtml(p) + '"' + (spec.required ? ' required' : '') + ' placeholder="' + escapeHtml(spec.description || '') + '"></label>';
|
|
709
|
+
}).join('');
|
|
710
|
+
var scheduleCount = (w.schedules || []).filter(function (x) { return x.repeating; }).length;
|
|
711
|
+
var runs = (w.recentRuns || []).map(function (r) {
|
|
712
|
+
var pill = r.status === 'done' ? '<span class="le-pill good">Done</span>'
|
|
713
|
+
: r.status === 'error' ? '<span class="le-pill bad">Failed</span>'
|
|
714
|
+
: r.status === 'awaiting_approval' ? '<span class="le-pill ask">Waiting for a decision</span>'
|
|
715
|
+
: '<span class="le-pill accent">Running</span>';
|
|
716
|
+
var values = r.params ? Object.keys(r.params).map(function (k) { return escapeHtml(k) + ': ' + escapeHtml(r.params[k]); }).join(' ' + String.fromCharCode(183) + ' ') : '';
|
|
717
|
+
var outcome = r.status === 'error' ? (r.error || '') : (r.result || '');
|
|
718
|
+
return '<li>' + pill + ' <span class="hint">' + escapeHtml(new Date(r.started_at).toLocaleString()) + (r.source === 'api' ? ' ' + String.fromCharCode(183) + ' from an app' : r.source === 'schedule' ? ' ' + String.fromCharCode(183) + ' scheduled' + (r.task_name ? ' (' + escapeHtml(r.task_name) + ')' : '') : '') + '</span>' +
|
|
719
|
+
(values ? '<div class="hint">' + values + '</div>' : '') +
|
|
720
|
+
(outcome ? '<div class="schedule-result">' + escapeHtml(outcome) + '</div>' : '') + '</li>';
|
|
692
721
|
}).join('');
|
|
693
722
|
return '<div class="source workflow-card" data-workflow="' + escapeHtml(w.id) + '">' +
|
|
694
723
|
'<div style="display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;align-items:baseline">' +
|
|
695
|
-
'<strong>' + escapeHtml(w.
|
|
696
|
-
'<p class="hint" style="margin:4px 0 6px">' + escapeHtml(w.
|
|
697
|
-
'<
|
|
724
|
+
'<strong>' + escapeHtml(w.title) + '</strong><span>' + state + '</span></div>' +
|
|
725
|
+
'<p class="hint" style="margin:4px 0 6px">' + escapeHtml(w.description) + (s ? ' ' + String.fromCharCode(183) + ' ' + escapeHtml(w.trigger) : '') + '</p>' +
|
|
726
|
+
'<details><summary class="hint">Steps <span>(the ' + escapeHtml(w.id) + ' skill)</span></summary><div class="workflow-steps-text">' + escapeHtml(w.steps) + '</div></details>' +
|
|
727
|
+
(params ? '<div class="workflow-params">' + params + '</div>' : '') +
|
|
728
|
+
(runs ? '<details class="workflow-runs"' + ((w.recentRuns || []).some(function (r) { return r.status === 'running' || r.status === 'awaiting_approval'; }) ? ' open' : '') + '><summary class="hint">Recent runs (' + w.recentRuns.length + ')</summary><ul class="schedule-history">' + runs + '</ul></details>' : '') +
|
|
698
729
|
'<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">' +
|
|
699
730
|
'<button type="button" class="workflow-run">Run now</button>' +
|
|
700
|
-
(
|
|
731
|
+
(scheduleCount ? '<a href="?tab=schedules" data-tab="schedules" class="hint" style="align-self:center">' + (scheduleCount > 1 ? 'Schedules (' + scheduleCount + ')' : 'Schedule') + '</a>' : '') +
|
|
732
|
+
'<button type="button" class="workflow-edit">Edit</button>' +
|
|
733
|
+
'<button type="button" class="workflow-hooks-btn">Webhooks</button>' +
|
|
734
|
+
'<a href="?tab=skills" data-tab="skills" class="hint" style="align-self:center">Edit steps</a>' +
|
|
701
735
|
'<button type="button" class="delete-btn workflow-forget">Forget</button>' +
|
|
702
|
-
'</div><div class="hint workflow-result"></div></div>';
|
|
736
|
+
'</div><div class="hint workflow-result"></div><div class="workflow-editor" hidden></div><div class="workflow-hooks" hidden></div></div>';
|
|
703
737
|
}).join('') + '<p class="hint" style="margin:6px 0 0"><a href="/workflows/new">Describe another workflow</a></p>';
|
|
704
738
|
}
|
|
705
739
|
|
|
740
|
+
var workflowsRefreshTimer = null;
|
|
741
|
+
var workflowsById = {};
|
|
742
|
+
// Set while a workflow's editor is open, so the running-run refresh
|
|
743
|
+
// doesn't redraw the card under someone typing.
|
|
744
|
+
var workflowEditing = false;
|
|
745
|
+
|
|
706
746
|
function loadWorkflowsSection(name) {
|
|
707
747
|
var el = detail.querySelector('#workflowsContent');
|
|
708
748
|
if (!el) return;
|
|
749
|
+
if (workflowsRefreshTimer) { clearTimeout(workflowsRefreshTimer); workflowsRefreshTimer = null; }
|
|
709
750
|
fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/workflows').then(function (r) {
|
|
710
751
|
if (currentName !== name) return;
|
|
711
752
|
if (!r.ok) { el.innerHTML = '<p class="error">Could not load workflows: ' + escapeHtml(r.body.error || 'unknown error') + '</p>'; return; }
|
|
712
|
-
|
|
753
|
+
var workflows = r.body.workflows || [];
|
|
754
|
+
workflowsById = {};
|
|
755
|
+
workflows.forEach(function (w) { workflowsById[w.id] = w; });
|
|
756
|
+
el.innerHTML = renderWorkflowsHtml(name, workflows);
|
|
713
757
|
var cards = el.querySelectorAll('.workflow-card');
|
|
714
758
|
for (var i = 0; i < cards.length; i++) wireWorkflowCard(name, cards[i]);
|
|
759
|
+
// Keep a running run's status current while the Overview is open.
|
|
760
|
+
var running = workflows.some(function (w) { return (w.recentRuns || []).some(function (x) { return x.status === 'running'; }); });
|
|
761
|
+
if (running && currentTab === 'overview') workflowsRefreshTimer = setTimeout(function () { if (!workflowEditing) loadWorkflowsSection(name); }, 3000);
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ---- The workflow editor: how a workflow's runs start. Its steps are
|
|
766
|
+
// the skill body, edited on the Skills tab.
|
|
767
|
+
function workflowParamRowHtml(pname, spec) {
|
|
768
|
+
return '<div class="workflow-param-row">' +
|
|
769
|
+
'<input class="wp-name" aria-label="Parameter name" placeholder="order_number" value="' + escapeHtml(pname || '') + '">' +
|
|
770
|
+
'<input class="wp-desc" aria-label="What it is" placeholder="What it is, e.g. The order to check" value="' + escapeHtml((spec && spec.description) || '') + '">' +
|
|
771
|
+
'<label class="http-tool-inline-label"><input type="checkbox" class="wp-required"' + (spec && spec.required ? ' checked' : '') + '> Required</label>' +
|
|
772
|
+
'<button type="button" class="delete-btn wp-remove">Remove</button></div>';
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function workflowEditorHtml(w) {
|
|
776
|
+
var rows = Object.keys(w.parameters || {}).map(function (p) { return workflowParamRowHtml(p, w.parameters[p]); }).join('');
|
|
777
|
+
return '<form class="add-source workflow-editor-form" style="margin-top:10px">' +
|
|
778
|
+
'<label>Title<input name="title" maxlength="120" value="' + escapeHtml(w.title) + '"></label>' +
|
|
779
|
+
'<label>Start message <span class="hint">(what each run begins with; $name puts in a parameter's value)</span>' +
|
|
780
|
+
'<textarea name="start_message" maxlength="4000" required>' + escapeHtml(w.startMessage) + '</textarea></label>' +
|
|
781
|
+
'<label>When it runs <span class="hint">(in words, for people; the timing itself is its schedule)</span><input name="trigger" maxlength="200" value="' + escapeHtml(w.trigger) + '"></label>' +
|
|
782
|
+
'<div class="body-field">Parameters <span class="hint">(values a run is given; apps send them as params)</span>' +
|
|
783
|
+
'<div class="workflow-param-rows">' + rows + '</div>' +
|
|
784
|
+
'<button type="button" class="wp-add" style="align-self:flex-start">Add parameter</button></div>' +
|
|
785
|
+
'<p class="hint workflow-editor-check" style="margin:0"></p>' +
|
|
786
|
+
'<div style="display:flex;gap:8px"><button type="submit" class="primary">Save</button><button type="button" class="wp-cancel">Cancel</button></div>' +
|
|
787
|
+
'<div class="hint workflow-editor-result"></div></form>';
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Placeholders used in the start message or steps that no parameter
|
|
791
|
+
// declares, and declared parameters nothing uses.
|
|
792
|
+
function workflowPlaceholderCheck(w, form) {
|
|
793
|
+
var used = {};
|
|
794
|
+
var pattern = new RegExp('[$]([a-z_][a-z0-9_]*)', 'g');
|
|
795
|
+
var text = form.querySelector('[name="start_message"]').value + ' ' + (w.steps || '');
|
|
796
|
+
var m;
|
|
797
|
+
while ((m = pattern.exec(text))) used[m[1]] = true;
|
|
798
|
+
var declared = {};
|
|
799
|
+
var names = form.querySelectorAll('.wp-name');
|
|
800
|
+
for (var i = 0; i < names.length; i++) if (names[i].value.trim()) declared[names[i].value.trim()] = true;
|
|
801
|
+
var notes = [];
|
|
802
|
+
var missing = Object.keys(used).filter(function (n) { return !declared[n]; });
|
|
803
|
+
var unused = Object.keys(declared).filter(function (n) { return !used[n]; });
|
|
804
|
+
if (missing.length) notes.push('Uses ' + missing.map(function (n) { return '$' + n; }).join(', ') + ', which ' + (missing.length > 1 ? 'aren' : 'isn') + String.fromCharCode(8217) + 't a parameter: add ' + (missing.length > 1 ? 'them' : 'it') + ', or the text stays as written.');
|
|
805
|
+
if (unused.length) notes.push(unused.join(', ') + ' ' + (unused.length > 1 ? 'aren' : 'isn') + String.fromCharCode(8217) + 't used in the start message or steps: its value is passed as an input.');
|
|
806
|
+
form.querySelector('.workflow-editor-check').textContent = notes.join(' ');
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function openWorkflowEditor(name, card) {
|
|
810
|
+
var w = workflowsById[card.dataset.workflow];
|
|
811
|
+
var box = card.querySelector('.workflow-editor');
|
|
812
|
+
if (!w || !box) return;
|
|
813
|
+
workflowEditing = true;
|
|
814
|
+
box.innerHTML = workflowEditorHtml(w);
|
|
815
|
+
box.hidden = false;
|
|
816
|
+
var form = box.querySelector('form');
|
|
817
|
+
var rows = form.querySelector('.workflow-param-rows');
|
|
818
|
+
var result = form.querySelector('.workflow-editor-result');
|
|
819
|
+
function check() { workflowPlaceholderCheck(w, form); }
|
|
820
|
+
function close() { workflowEditing = false; box.hidden = true; box.innerHTML = ''; }
|
|
821
|
+
rows.addEventListener('click', function (ev) {
|
|
822
|
+
if (ev.target.classList.contains('wp-remove')) { ev.target.closest('.workflow-param-row').remove(); check(); }
|
|
823
|
+
});
|
|
824
|
+
form.addEventListener('input', check);
|
|
825
|
+
form.querySelector('.wp-add').addEventListener('click', function () {
|
|
826
|
+
rows.insertAdjacentHTML('beforeend', workflowParamRowHtml('', null));
|
|
827
|
+
rows.lastElementChild.querySelector('.wp-name').focus();
|
|
828
|
+
});
|
|
829
|
+
form.querySelector('.wp-cancel').addEventListener('click', close);
|
|
830
|
+
form.addEventListener('submit', function (ev) {
|
|
831
|
+
ev.preventDefault();
|
|
832
|
+
var parameters = {};
|
|
833
|
+
var problem = '';
|
|
834
|
+
var list = rows.querySelectorAll('.workflow-param-row');
|
|
835
|
+
for (var i = 0; i < list.length; i++) {
|
|
836
|
+
var pname = list[i].querySelector('.wp-name').value.trim();
|
|
837
|
+
if (!pname) continue;
|
|
838
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(pname)) { problem = 'Parameter "' + pname + '": use lowercase letters, digits and _, starting with a letter, e.g. order_number.'; break; }
|
|
839
|
+
if (parameters[pname]) { problem = 'Parameter "' + pname + '" is listed twice.'; break; }
|
|
840
|
+
parameters[pname] = { description: list[i].querySelector('.wp-desc').value.trim(), required: list[i].querySelector('.wp-required').checked };
|
|
841
|
+
}
|
|
842
|
+
if (problem) { result.textContent = problem; return; }
|
|
843
|
+
result.textContent = 'Saving' + String.fromCharCode(8230);
|
|
844
|
+
fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(w.id), {
|
|
845
|
+
method: 'PUT', headers: { 'content-type': 'application/json' },
|
|
846
|
+
body: JSON.stringify({ title: form.title.value, start_message: form.start_message.value, trigger: form.trigger.value, parameters: parameters }),
|
|
847
|
+
}).then(function (r) {
|
|
848
|
+
if (!r.ok) { result.textContent = r.body.error || 'Could not save'; return; }
|
|
849
|
+
workflowEditing = false;
|
|
850
|
+
loadWorkflowsSection(name);
|
|
851
|
+
});
|
|
715
852
|
});
|
|
853
|
+
check();
|
|
854
|
+
form.title.focus();
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// ---- A workflow's webhook triggers (core/workflow-triggers.ts): secret
|
|
858
|
+
// URLs an outside service posts its events to, to start a run.
|
|
859
|
+
var HOOK_OUTCOMES = {
|
|
860
|
+
started: ['good', 'Started a run'], filtered: ['', 'Skipped by the filter'], duplicate: ['', 'A retry, skipped'],
|
|
861
|
+
busy: ['', 'Already running'], rejected: ['bad', 'Signature check failed'], invalid: ['bad', 'Couldn' + String.fromCharCode(8217) + 't start'], error: ['bad', 'Failed'],
|
|
862
|
+
};
|
|
863
|
+
var HOOK_SENDERS = { stripe: 'Stripe', github: 'GitHub', shopify: 'Shopify', standard: 'Standard Webhooks' };
|
|
864
|
+
|
|
865
|
+
function hookRowHtml(t) {
|
|
866
|
+
var last = (t.deliveries || []).slice(0, 3).map(function (d) {
|
|
867
|
+
var o = HOOK_OUTCOMES[d.outcome] || ['', d.outcome];
|
|
868
|
+
return '<li><span class="le-pill ' + o[0] + '">' + escapeHtml(o[1]) + '</span> <span class="hint">' + escapeHtml(new Date(d.at).toLocaleString()) + '</span>' +
|
|
869
|
+
(d.detail ? '<div class="schedule-result">' + escapeHtml(d.detail) + '</div>' : '') + '</li>';
|
|
870
|
+
}).join('');
|
|
871
|
+
var rules = [];
|
|
872
|
+
if (t.verify) rules.push(t.verify.sender === 'custom'
|
|
873
|
+
? 'checks the signature in ' + escapeHtml(t.verify.header) + ' (' + escapeHtml(t.verify.algorithm || 'sha256') + ', ' + escapeHtml(t.verify.encoding || 'hex') + ')'
|
|
874
|
+
: 'checks the ' + escapeHtml(HOOK_SENDERS[t.verify.sender] || t.verify.sender) + ' signature');
|
|
875
|
+
if (t.filter) rules.push('only when ' + escapeHtml(t.filter.path) + ' is ' + t.filter.values.map(escapeHtml).join(' or '));
|
|
876
|
+
if (t.mapping) rules.push(Object.keys(t.mapping).map(function (p) { return escapeHtml(p) + ' ' + String.fromCharCode(8592) + ' ' + escapeHtml(t.mapping[p]); }).join(', '));
|
|
877
|
+
if (t.payload_param) rules.push('the whole payload goes to ' + escapeHtml(t.payload_param));
|
|
878
|
+
if (t.notify) rules.push('sends each result to you');
|
|
879
|
+
if (t.callback_url) rules.push('reports to ' + escapeHtml(t.callback_url));
|
|
880
|
+
return '<div class="workflow-hook" data-trigger="' + escapeHtml(t.trigger_id) + '">' +
|
|
881
|
+
'<div style="display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap;align-items:baseline"><strong>' + escapeHtml(t.name) + '</strong>' +
|
|
882
|
+
'<code class="hint">/hooks/workflows/' + escapeHtml(t.token_hint) + '</code>' + (t.enabled ? '' : ' <span class="le-pill">Off</span>') + '</div>' +
|
|
883
|
+
(rules.length ? '<p class="hint" style="margin:2px 0">' + rules.join(' ' + String.fromCharCode(183) + ' ') + '</p>' : '') +
|
|
884
|
+
(last ? '<ul class="schedule-history">' + last + '</ul>' : '<p class="hint" style="margin:2px 0">No deliveries yet.</p>') +
|
|
885
|
+
'<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:4px">' +
|
|
886
|
+
'<button type="button" class="hook-toggle">' + (t.enabled ? 'Turn off' : 'Turn on') + '</button>' +
|
|
887
|
+
'<button type="button" class="hook-rotate">New URL</button>' +
|
|
888
|
+
'<button type="button" class="delete-btn hook-delete">Delete</button></div></div>';
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function hookFormHtml(w) {
|
|
892
|
+
var params = Object.keys(w.parameters || {});
|
|
893
|
+
return '<form class="add-source hook-form" style="margin-top:10px">' +
|
|
894
|
+
'<h4 style="margin:0">Add a webhook</h4>' +
|
|
895
|
+
'<label>Name<input name="name" maxlength="80" placeholder="Stripe disputes"></label>' +
|
|
896
|
+
'<label>Check the sender' + String.fromCharCode(8217) + 's signature <span class="hint">(the URL itself is secret; this also proves who sent it)</span>' +
|
|
897
|
+
'<select name="sender"><option value="">No, the URL is enough</option><option value="stripe">Stripe</option><option value="github">GitHub</option><option value="shopify">Shopify</option>' +
|
|
898
|
+
'<option value="standard">Standard Webhooks (Clerk, Resend, OpenAI, Svix)</option><option value="custom">Another service: set it up myself</option></select></label>' +
|
|
899
|
+
'<div class="hook-custom body-field" hidden>How it signs <span class="hint">(from its webhook docs: an HMAC of the request body, in a header)</span>' +
|
|
900
|
+
'<div class="workflow-param-row">' +
|
|
901
|
+
'<input name="sig_header" placeholder="Header, e.g. Linear-Signature" style="flex:1">' +
|
|
902
|
+
'<select name="sig_algorithm" aria-label="Hash"><option value="sha256">SHA-256</option><option value="sha1">SHA-1</option><option value="sha512">SHA-512</option></select>' +
|
|
903
|
+
'<select name="sig_encoding" aria-label="Written as"><option value="hex">hex</option><option value="base64">base64</option></select>' +
|
|
904
|
+
'<input name="sig_prefix" placeholder="Prefix, e.g. sha256= (if any)" style="width:180px">' +
|
|
905
|
+
'</div></div>' +
|
|
906
|
+
'<label class="hook-secret" hidden>Signing secret <span class="hint">(from the sender' + String.fromCharCode(8217) + 's webhook settings)</span><input name="secret" autocomplete="off"></label>' +
|
|
907
|
+
'<label>Run only when <span class="hint">(optional: a field in the payload, and the values that start a run)</span>' +
|
|
908
|
+
'<div class="workflow-param-row"><input name="filter_path" placeholder="type"><input name="filter_values" placeholder="charge.dispute.created, charge.dispute.updated" style="flex:1"></div></label>' +
|
|
909
|
+
(params.length
|
|
910
|
+
? '<div class="body-field">Parameters from the payload <span class="hint">(a dot path into its JSON, e.g. data.object.id)</span>' +
|
|
911
|
+
params.map(function (p) { return '<div class="workflow-param-row"><code class="wp-name">' + escapeHtml(p) + '</code><input name="map:' + escapeHtml(p) + '" placeholder="data.object.id" style="flex:1"></div>'; }).join('') + '</div>' +
|
|
912
|
+
'<label>Or give the whole payload to <select name="payload_param"><option value="">(none)</option>' + params.map(function (p) { return '<option>' + escapeHtml(p) + '</option>'; }).join('') + '</select></label>'
|
|
913
|
+
: '<p class="hint" style="margin:0">This workflow takes no parameters, so the payload isn' + String.fromCharCode(8217) + 't passed on. Add one in Edit to use it.</p>') +
|
|
914
|
+
'<label class="http-tool-inline-label"><input type="checkbox" name="notify"> Send each run' + String.fromCharCode(8217) + 's result to you through the agent' + String.fromCharCode(8217) + 's messaging app</label>' +
|
|
915
|
+
'<label>Post each run' + String.fromCharCode(8217) + 's result to <span class="hint">(optional, an https URL)</span><input name="callback_url" placeholder="https://example.com/hooks/loopengine"></label>' +
|
|
916
|
+
'<div style="display:flex;gap:8px"><button type="submit" class="primary">Make the URL</button></div>' +
|
|
917
|
+
'<div class="hint hook-form-result"></div></form>';
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function hookUrlHtml(url) {
|
|
921
|
+
return '<div class="hook-url"><strong>Copy this URL now</strong> <span class="hint">(it' + String.fromCharCode(8217) + 's shown only once; make a new one if it' + String.fromCharCode(8217) + 's lost)</span>' +
|
|
922
|
+
'<input readonly value="' + escapeHtml(url) + '" onclick="this.select()"></div>';
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function openWorkflowHooks(name, card) {
|
|
926
|
+
var w = workflowsById[card.dataset.workflow];
|
|
927
|
+
var box = card.querySelector('.workflow-hooks');
|
|
928
|
+
if (!w || !box) return;
|
|
929
|
+
if (!box.hidden) { box.hidden = true; box.innerHTML = ''; workflowEditing = false; return; }
|
|
930
|
+
workflowEditing = true;
|
|
931
|
+
box.hidden = false;
|
|
932
|
+
var base = '/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(w.id) + '/triggers';
|
|
933
|
+
function render(shownUrl) {
|
|
934
|
+
box.innerHTML = '<p class="hint" style="margin:0">Loading' + String.fromCharCode(8230) + '</p>';
|
|
935
|
+
fetchJsonRetryOnce(base).then(function (r) {
|
|
936
|
+
if (!r.ok) { box.innerHTML = '<p class="error">' + escapeHtml(r.body.error || 'Could not load webhooks') + '</p>'; return; }
|
|
937
|
+
var list = r.body.triggers || [];
|
|
938
|
+
box.innerHTML = '<h4 style="margin:10px 0 4px">Webhooks</h4>' +
|
|
939
|
+
'<p class="hint" style="margin:0 0 6px">A secret URL an outside service (Stripe, GitHub, a form, monitoring) posts its events to; each one starts a run.</p>' +
|
|
940
|
+
(shownUrl ? hookUrlHtml(shownUrl) : '') +
|
|
941
|
+
(list.length ? list.map(hookRowHtml).join('') : '') + hookFormHtml(w);
|
|
942
|
+
wire();
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
function act(id, path, opts) {
|
|
946
|
+
return fetchJsonRetryOnce(base + '/' + encodeURIComponent(id) + path, opts).then(function (r) {
|
|
947
|
+
if (!r.ok) { alert(r.body.error || 'That didn' + String.fromCharCode(8217) + 't work'); return; }
|
|
948
|
+
render(r.body.url);
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
function wire() {
|
|
952
|
+
var rows = box.querySelectorAll('.workflow-hook');
|
|
953
|
+
for (var i = 0; i < rows.length; i++) {
|
|
954
|
+
(function (row) {
|
|
955
|
+
var id = row.dataset.trigger;
|
|
956
|
+
row.querySelector('.hook-toggle').addEventListener('click', function (ev) {
|
|
957
|
+
act(id, '', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ enabled: ev.currentTarget.textContent === 'Turn on' }) });
|
|
958
|
+
});
|
|
959
|
+
row.querySelector('.hook-rotate').addEventListener('click', function (ev) {
|
|
960
|
+
var btn = ev.currentTarget;
|
|
961
|
+
if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again: the old URL stops working'; return; }
|
|
962
|
+
act(id, '/rotate', { method: 'POST' });
|
|
963
|
+
});
|
|
964
|
+
row.querySelector('.hook-delete').addEventListener('click', function (ev) {
|
|
965
|
+
var btn = ev.currentTarget;
|
|
966
|
+
if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again to delete'; return; }
|
|
967
|
+
act(id, '', { method: 'DELETE' });
|
|
968
|
+
});
|
|
969
|
+
})(rows[i]);
|
|
970
|
+
}
|
|
971
|
+
var form = box.querySelector('.hook-form');
|
|
972
|
+
var secretField = form.querySelector('.hook-secret');
|
|
973
|
+
var customField = form.querySelector('.hook-custom');
|
|
974
|
+
form.sender.addEventListener('change', function () {
|
|
975
|
+
secretField.hidden = !form.sender.value;
|
|
976
|
+
customField.hidden = form.sender.value !== 'custom';
|
|
977
|
+
});
|
|
978
|
+
form.addEventListener('submit', function (ev) {
|
|
979
|
+
ev.preventDefault();
|
|
980
|
+
var result = form.querySelector('.hook-form-result');
|
|
981
|
+
var body = { name: form.name.value };
|
|
982
|
+
if (form.sender.value) body.verify = { sender: form.sender.value, secret: form.secret.value };
|
|
983
|
+
if (form.sender.value === 'custom') {
|
|
984
|
+
body.verify.header = form.sig_header.value.trim();
|
|
985
|
+
body.verify.algorithm = form.sig_algorithm.value;
|
|
986
|
+
body.verify.encoding = form.sig_encoding.value;
|
|
987
|
+
if (form.sig_prefix.value.trim()) body.verify.prefix = form.sig_prefix.value.trim();
|
|
988
|
+
}
|
|
989
|
+
var values = form.filter_values.value.split(',').map(function (v) { return v.trim(); }).filter(Boolean);
|
|
990
|
+
if (form.filter_path.value.trim() || values.length) body.filter = { path: form.filter_path.value.trim(), values: values };
|
|
991
|
+
var mapping = {};
|
|
992
|
+
var inputs = form.querySelectorAll('input[name^="map:"]');
|
|
993
|
+
for (var j = 0; j < inputs.length; j++) if (inputs[j].value.trim()) mapping[inputs[j].name.slice(4)] = inputs[j].value.trim();
|
|
994
|
+
if (Object.keys(mapping).length) body.mapping = mapping;
|
|
995
|
+
if (form.payload_param && form.payload_param.value) body.payload_param = form.payload_param.value;
|
|
996
|
+
if (form.notify.checked) body.notify = true;
|
|
997
|
+
if (form.callback_url.value.trim()) body.callback_url = form.callback_url.value.trim();
|
|
998
|
+
result.textContent = 'Making it' + String.fromCharCode(8230);
|
|
999
|
+
fetchJsonRetryOnce(base, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }).then(function (r) {
|
|
1000
|
+
if (!r.ok) { result.textContent = r.body.error || 'Could not make it'; return; }
|
|
1001
|
+
render(r.body.url);
|
|
1002
|
+
});
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
render(null);
|
|
716
1006
|
}
|
|
717
1007
|
|
|
718
1008
|
function wireWorkflowCard(name, card) {
|
|
719
1009
|
var id = card.dataset.workflow;
|
|
720
1010
|
var result = card.querySelector('.workflow-result');
|
|
721
1011
|
var base = '/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(id);
|
|
1012
|
+
card.querySelector('.workflow-edit').addEventListener('click', function () { openWorkflowEditor(name, card); });
|
|
1013
|
+
card.querySelector('.workflow-hooks-btn').addEventListener('click', function () { openWorkflowHooks(name, card); });
|
|
722
1014
|
card.querySelector('.workflow-run').addEventListener('click', function (ev) {
|
|
723
1015
|
var btn = ev.currentTarget;
|
|
1016
|
+
var params = {};
|
|
1017
|
+
var inputs = card.querySelectorAll('.workflow-params input');
|
|
1018
|
+
for (var i = 0; i < inputs.length; i++) {
|
|
1019
|
+
var value = inputs[i].value.trim();
|
|
1020
|
+
if (!value && inputs[i].required) { result.textContent = 'Fill in ' + inputs[i].name + ' first.'; inputs[i].focus(); return; }
|
|
1021
|
+
if (value) params[inputs[i].name] = value;
|
|
1022
|
+
}
|
|
724
1023
|
btn.disabled = true;
|
|
725
1024
|
result.textContent = 'Starting' + String.fromCharCode(8230);
|
|
726
|
-
fetchJsonRetryOnce(base + '/run', { method: 'POST' }).then(function (r) {
|
|
1025
|
+
fetchJsonRetryOnce(base + '/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ params: params, source: 'console' }) }).then(function (r) {
|
|
727
1026
|
btn.disabled = false;
|
|
728
|
-
result.textContent = r.
|
|
729
|
-
|
|
730
|
-
|
|
1027
|
+
if (!r.ok) { result.textContent = r.body.error || 'Could not start it.'; return; }
|
|
1028
|
+
result.textContent = 'Started. Questions and approvals go to this agent' + String.fromCharCode(8217) + 's messaging app; the result shows under Recent runs.';
|
|
1029
|
+
setTimeout(function () { loadWorkflowsSection(name); }, 800);
|
|
731
1030
|
});
|
|
732
1031
|
});
|
|
733
1032
|
card.querySelector('.workflow-forget').addEventListener('click', function (ev) {
|
|
734
1033
|
var btn = ev.currentTarget;
|
|
735
|
-
if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again (its
|
|
1034
|
+
if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again (its schedules are cancelled; the skill stays)'; return; }
|
|
736
1035
|
fetchJsonRetryOnce(base, { method: 'DELETE' }).then(function () { loadWorkflowsSection(name); });
|
|
737
1036
|
});
|
|
738
1037
|
}
|
|
@@ -1775,6 +2074,9 @@ ${consoleShellScript}
|
|
|
1775
2074
|
// The workspace's time zone, from GET /agents/:name/schedules (an
|
|
1776
2075
|
// older project adapter doesn't send it: then the browser's own).
|
|
1777
2076
|
var schedulesTimeZone = null;
|
|
2077
|
+
// The agent's workflows, from the same response: a new schedule can run
|
|
2078
|
+
// one of them instead of a message.
|
|
2079
|
+
var schedulesWorkflows = [];
|
|
1778
2080
|
|
|
1779
2081
|
function fmtScheduleTime(iso) {
|
|
1780
2082
|
var d = new Date(iso);
|
|
@@ -1807,8 +2109,16 @@ ${consoleShellScript}
|
|
|
1807
2109
|
'<div class="schedule-result">' + escapeHtml(h.status === 'ok' ? (h.result || '') : (h.error || '')) + '</div></li>';
|
|
1808
2110
|
}).join('');
|
|
1809
2111
|
return '<tr>' +
|
|
1810
|
-
'<td style="min-width:180px"><strong>' + escapeHtml(t.task_name || 'Untitled') + '</strong>' +
|
|
1811
|
-
|
|
2112
|
+
'<td style="min-width:180px"><strong>' + escapeHtml(t.task_name || 'Untitled') + '</strong>' +
|
|
2113
|
+
(t.workflow
|
|
2114
|
+
? '<div><span class="le-pill accent">Workflow</span> ' + escapeHtml(t.workflow.title) + '</div><div class="schedule-result hint">' + escapeHtml(t.workflow.start_message) +
|
|
2115
|
+
(t.params ? '<br>' + Object.keys(t.params).map(function (k) { return escapeHtml(k) + ': ' + escapeHtml(t.params[k]); }).join(' ' + String.fromCharCode(183) + ' ') : '') + '</div>'
|
|
2116
|
+
: t.workflow_missing
|
|
2117
|
+
? '<div><span class="le-pill bad">Workflow removed</span> <span class="hint">' + escapeHtml(t.workflow_id) + '</span></div>'
|
|
2118
|
+
: '<div class="schedule-result hint">' + escapeHtml(t.message) + '</div>') +
|
|
2119
|
+
(t.status !== 'cancelled' && t.status !== 'done'
|
|
2120
|
+
? '<label class="schedule-notify hint" title="Send each run's result through this agent's messaging connection (Messaging tab)"><input type="checkbox" class="schedule-notify-toggle" data-id="' + escapeHtml(t.task_id) + '"' + (t.notify ? ' checked' : '') + '> Send each result to you</label>'
|
|
2121
|
+
: (t.notify ? '<div class="hint">Sent each result to you</div>' : '')) + '</td>' +
|
|
1812
2122
|
'<td>' + scheduleText(t.schedule) + (t.next_run_at ? '<div class="hint">next ' + escapeHtml(fmtScheduleTime(t.next_run_at)) + '</div>' : '') + '</td>' +
|
|
1813
2123
|
'<td><span class="le-pill ' + (SCHEDULE_PILLS[t.status] || 'neutral') + '">' + escapeHtml(SCHEDULE_LABELS[t.status] || t.status) + '</span>' +
|
|
1814
2124
|
(t.status === 'awaiting_approval' ? '<div class="hint"><a href="#" class="schedule-to-approvals">Decide in Messaging</a></div>' : '') + '</td>' +
|
|
@@ -1823,11 +2133,22 @@ ${consoleShellScript}
|
|
|
1823
2133
|
var list = tasks.length
|
|
1824
2134
|
? '<div class="le-table-wrap"><table><thead><tr><th>Task</th><th>When</th><th>Status</th><th>Last run</th><th></th></tr></thead><tbody>' + tasks.map(renderScheduleRow).join('') + '</tbody></table></div>'
|
|
1825
2135
|
: '<p class="hint">Nothing scheduled yet.</p>';
|
|
1826
|
-
return '<p class="muted" style="margin:0 0 20px">
|
|
2136
|
+
return '<p class="muted" style="margin:0 0 20px">Workflows and messages this agent runs on a timer, like a cron job. A workflow schedule starts with the workflow's start message as it is at each run. Each run works like an app calling it over HTTP: an approval goes wherever the Messaging tab sends it, and with nowhere set, a call that needs approval is denied. The agent can also schedule its own tasks when asked in Chat (“remind me tomorrow at 9”); they appear here too.</p>' +
|
|
1827
2137
|
'<section><h3>Scheduled (' + active.length + ' active)</h3>' + list + '</section>' +
|
|
1828
2138
|
'<section><h3>New schedule</h3>' +
|
|
1829
2139
|
'<form class="add-source" id="scheduleForm" style="max-width:560px">' +
|
|
1830
|
-
|
|
2140
|
+
(schedulesWorkflows.length
|
|
2141
|
+
? '<div class="body-field">Runs' +
|
|
2142
|
+
'<div class="schedule-when"><input type="radio" name="runs" value="workflow" id="runsWorkflow" checked><label for="runsWorkflow">A workflow</label>' +
|
|
2143
|
+
'<select name="workflow_id" aria-label="Workflow">' + schedulesWorkflows.map(function (w) {
|
|
2144
|
+
return '<option value="' + escapeHtml(w.id) + '">' + escapeHtml(w.title) + '</option>';
|
|
2145
|
+
}).join('') + '</select></div>' +
|
|
2146
|
+
'<div class="workflow-params" id="scheduleParams" style="margin:0 0 4px 22px"></div>' +
|
|
2147
|
+
'<p class="hint" style="margin:0 0 4px 22px">Each run starts with the workflow's start message as it is then, so changing the workflow changes what runs.</p>' +
|
|
2148
|
+
'<div class="schedule-when"><input type="radio" name="runs" value="message" id="runsMessage"><label for="runsMessage">A message</label></div>' +
|
|
2149
|
+
'</div>'
|
|
2150
|
+
: '') +
|
|
2151
|
+
'<label id="scheduleMessageField"' + (schedulesWorkflows.length ? ' hidden' : '') + '>Message <span class="hint">(what the agent receives each time, as if someone typed it)</span><textarea name="message" placeholder="Summarize yesterday's support tickets and post the summary."></textarea></label>' +
|
|
1831
2152
|
'<div class="body-field">When' +
|
|
1832
2153
|
'<div class="schedule-when"><input type="radio" name="when" value="every" id="whenEvery" checked><label for="whenEvery">Every</label>' +
|
|
1833
2154
|
'<input type="number" name="everyN" value="1" min="1" aria-label="How many">' +
|
|
@@ -1845,6 +2166,15 @@ ${consoleShellScript}
|
|
|
1845
2166
|
'</form></section>';
|
|
1846
2167
|
}
|
|
1847
2168
|
|
|
2169
|
+
// The New schedule form's parameter fields ("param:<name>"), filled ones only.
|
|
2170
|
+
function scheduleParams(data) {
|
|
2171
|
+
var params = {};
|
|
2172
|
+
data.forEach(function (value, key) {
|
|
2173
|
+
if (key.indexOf('param:') === 0 && String(value).trim()) params[key.slice(6)] = String(value).trim();
|
|
2174
|
+
});
|
|
2175
|
+
return params;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
1848
2178
|
// A date field's value ("2026-11-02T09:00") goes as it is, for the
|
|
1849
2179
|
// server to read in the workspace's time zone; with no workspace time
|
|
1850
2180
|
// zone known, it's the browser's.
|
|
@@ -1872,15 +2202,53 @@ ${consoleShellScript}
|
|
|
1872
2202
|
request.then(function () { setTimeout(function () { loadSchedulesTab(name); }, action === 'run' ? 800 : 0); });
|
|
1873
2203
|
});
|
|
1874
2204
|
}
|
|
2205
|
+
var notifyToggles = content.querySelectorAll('.schedule-notify-toggle');
|
|
2206
|
+
for (var n = 0; n < notifyToggles.length; n++) {
|
|
2207
|
+
notifyToggles[n].addEventListener('change', function (ev) {
|
|
2208
|
+
var box = ev.currentTarget;
|
|
2209
|
+
box.disabled = true;
|
|
2210
|
+
fetchJsonRetryOnce('/schedules/' + encodeURIComponent(box.dataset.id) + '/notify', {
|
|
2211
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ notify: box.checked }),
|
|
2212
|
+
}).then(function (r) {
|
|
2213
|
+
box.disabled = false;
|
|
2214
|
+
if (!r.ok) { box.checked = !box.checked; alert('Could not change it: ' + (r.body.error || 'unknown error')); }
|
|
2215
|
+
});
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
1875
2218
|
var toApprovals = content.querySelectorAll('.schedule-to-approvals');
|
|
1876
2219
|
for (var j = 0; j < toApprovals.length; j++) {
|
|
1877
2220
|
toApprovals[j].addEventListener('click', function (ev) { ev.preventDefault(); switchTab('messaging'); });
|
|
1878
2221
|
}
|
|
1879
2222
|
var form = content.querySelector('#scheduleForm');
|
|
1880
2223
|
var result = content.querySelector('#scheduleResult');
|
|
2224
|
+
var messageField = content.querySelector('#scheduleMessageField');
|
|
2225
|
+
var runsRadios = content.querySelectorAll('input[name="runs"]');
|
|
2226
|
+
var paramsBox = content.querySelector('#scheduleParams');
|
|
2227
|
+
var workflowSelect = form.querySelector('select[name="workflow_id"]');
|
|
2228
|
+
// One field per parameter of the chosen workflow.
|
|
2229
|
+
function renderScheduleParams() {
|
|
2230
|
+
if (!paramsBox || !workflowSelect) return;
|
|
2231
|
+
var w = schedulesWorkflows.filter(function (x) { return x.id === workflowSelect.value; })[0];
|
|
2232
|
+
var parameters = (w && w.parameters) || {};
|
|
2233
|
+
paramsBox.innerHTML = Object.keys(parameters).map(function (p) {
|
|
2234
|
+
return '<label class="workflow-param">' + escapeHtml(p) + (parameters[p].required ? '' : ' <span class="hint">(optional)</span>') +
|
|
2235
|
+
'<input name="param:' + escapeHtml(p) + '" placeholder="' + escapeHtml(parameters[p].description || '') + '"></label>';
|
|
2236
|
+
}).join('');
|
|
2237
|
+
}
|
|
2238
|
+
renderScheduleParams();
|
|
2239
|
+
if (workflowSelect) workflowSelect.addEventListener('change', renderScheduleParams);
|
|
2240
|
+
for (var k = 0; k < runsRadios.length; k++) {
|
|
2241
|
+
runsRadios[k].addEventListener('change', function () {
|
|
2242
|
+
var byMessage = form.querySelector('input[name="runs"]:checked').value === 'message';
|
|
2243
|
+
messageField.hidden = !byMessage;
|
|
2244
|
+
if (paramsBox) paramsBox.hidden = byMessage;
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
1881
2247
|
form.addEventListener('submit', function (ev) {
|
|
1882
2248
|
ev.preventDefault();
|
|
1883
2249
|
var data = new FormData(form);
|
|
2250
|
+
var runsWorkflow = data.get('runs') === 'workflow';
|
|
2251
|
+
if (!runsWorkflow && !String(data.get('message') || '').trim()) { result.textContent = 'Write the message the agent receives.'; return; }
|
|
1884
2252
|
var schedule;
|
|
1885
2253
|
if (data.get('when') === 'once') {
|
|
1886
2254
|
if (!data.get('runAt')) { result.textContent = 'Pick a date and time.'; return; }
|
|
@@ -1895,7 +2263,10 @@ ${consoleShellScript}
|
|
|
1895
2263
|
result.textContent = 'Scheduling' + String.fromCharCode(8230);
|
|
1896
2264
|
fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/schedules', {
|
|
1897
2265
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1898
|
-
body: JSON.stringify(
|
|
2266
|
+
body: JSON.stringify(Object.assign(
|
|
2267
|
+
runsWorkflow ? { workflow_id: data.get('workflow_id'), params: scheduleParams(data) } : { message: data.get('message') },
|
|
2268
|
+
{ schedule: schedule, task_name: data.get('task_name'), notify: data.get('notify') === 'on' }
|
|
2269
|
+
)),
|
|
1899
2270
|
}).then(function (r) {
|
|
1900
2271
|
if (!r.ok) { result.textContent = r.body.error || 'Could not schedule'; return; }
|
|
1901
2272
|
loadSchedulesTab(name);
|
|
@@ -1912,6 +2283,7 @@ ${consoleShellScript}
|
|
|
1912
2283
|
if (!r.ok) throw new Error(r.body.error || 'request failed');
|
|
1913
2284
|
var tasks = r.body.tasks || [];
|
|
1914
2285
|
schedulesTimeZone = r.body.time_zone || null;
|
|
2286
|
+
schedulesWorkflows = r.body.workflows || [];
|
|
1915
2287
|
content.innerHTML = renderSchedulesHtml(tasks);
|
|
1916
2288
|
wireSchedulesHandlers(name);
|
|
1917
2289
|
leShell.setCount('schedules', tasks.filter(function (t) { return t.status !== 'cancelled' && t.status !== 'done'; }).length);
|
|
@@ -8,14 +8,10 @@ export interface SkillContent {
|
|
|
8
8
|
body: string;
|
|
9
9
|
}
|
|
10
10
|
export declare function readSkill(agentName: string, skillId: string): SkillContent;
|
|
11
|
-
/** Creates the skill if `skillId` doesn't exist yet, or
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* might have (e.g. `paths` — see SkillFrontmatter's own doc comment) are
|
|
16
|
-
* intentionally not preserved here, since this admin UI has no field for
|
|
17
|
-
* them. A skill that needs those is still editable by hand — this just
|
|
18
|
-
* isn't the tool for it. */
|
|
11
|
+
/** Creates the skill if `skillId` doesn't exist yet, or replaces its
|
|
12
|
+
* description and body if it does. Its other front matter is kept: a
|
|
13
|
+
* workflow's start message, trigger and parameters (core/workflows.ts),
|
|
14
|
+
* `paths` and anything hand-written, none of which the Skills tab edits. */
|
|
19
15
|
export declare function writeSkill(agentName: string, skillId: string, content: {
|
|
20
16
|
description: string;
|
|
21
17
|
body: string;
|
package/dist/web/skills-admin.js
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
// this repo's own example agents actually uses. Read-only display of a
|
|
10
10
|
// nested skill (via describeAgent's own skills list) still works fine —
|
|
11
11
|
// only adding/editing/removing one through this UI is out of scope.
|
|
12
|
-
import { existsSync,
|
|
12
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
13
13
|
import { dirname, join } from 'node:path';
|
|
14
14
|
import { parseSkillFile } from '../core/skillgarden/index.js';
|
|
15
15
|
import { agentDir } from '../core/gateway-tools.js';
|
|
16
|
+
import { writeSkillFile } from '../core/workflows.js';
|
|
16
17
|
export class SkillInvalidIdError extends Error {
|
|
17
18
|
}
|
|
18
19
|
export class SkillNotFoundError extends Error {
|
|
@@ -43,20 +44,15 @@ export function readSkill(agentName, skillId) {
|
|
|
43
44
|
const { frontmatter, body } = parseSkillFile(readFileSync(path, 'utf8'), skillId);
|
|
44
45
|
return { id: skillId, description: frontmatter.description ?? '', body };
|
|
45
46
|
}
|
|
46
|
-
/** Creates the skill if `skillId` doesn't exist yet, or
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* might have (e.g. `paths` — see SkillFrontmatter's own doc comment) are
|
|
51
|
-
* intentionally not preserved here, since this admin UI has no field for
|
|
52
|
-
* them. A skill that needs those is still editable by hand — this just
|
|
53
|
-
* isn't the tool for it. */
|
|
47
|
+
/** Creates the skill if `skillId` doesn't exist yet, or replaces its
|
|
48
|
+
* description and body if it does. Its other front matter is kept: a
|
|
49
|
+
* workflow's start message, trigger and parameters (core/workflows.ts),
|
|
50
|
+
* `paths` and anything hand-written, none of which the Skills tab edits. */
|
|
54
51
|
export function writeSkill(agentName, skillId, content) {
|
|
55
52
|
const path = skillFilePath(agentName, skillId);
|
|
56
53
|
const description = content.description.replace(/\n/g, ' ').trim();
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
writeFileSync(path, frontmatter + content.body.trim() + '\n');
|
|
54
|
+
const existing = existsSync(path) ? parseSkillFile(readFileSync(path, 'utf8'), skillId).frontmatter : {};
|
|
55
|
+
writeSkillFile(path, { ...existing, name: skillId, description }, content.body);
|
|
60
56
|
}
|
|
61
57
|
export function deleteSkill(agentName, skillId) {
|
|
62
58
|
const path = skillFilePath(agentName, skillId);
|