@agent-native/core 0.136.3 → 0.136.5
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/corpus/README.md +1 -1
- package/corpus/templates/clips/.agents/skills/recording/SKILL.md +53 -0
- package/corpus/templates/clips/actions/import-loom-recording.ts +7 -0
- package/corpus/templates/clips/actions/lib/loom-import-job.ts +24 -4
- package/corpus/templates/clips/changelog/2026-08-03-restart-during-a-recording-now-immediately-starts-a-fresh-ta.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +81 -30
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +436 -158
- package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +14 -1
- package/corpus/templates/clips/server/plugins/auth.ts +9 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +13 -0
- package/dist/client/agent-page/AgentJobsTab.js +19 -4
- package/dist/client/agent-page/use-jobs.d.ts +10 -0
- package/dist/client/agent-page/use-jobs.js +21 -0
- package/dist/client/settings/SecretsSection.js +32 -8
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/jobs/actions/run-automation-now.d.ts +6 -0
- package/dist/jobs/actions/run-automation-now.js +22 -0
- package/dist/jobs/background-automation-runner.d.ts +2 -0
- package/dist/jobs/background-automation-runner.js +22 -16
- package/dist/jobs/run-history.d.ts +17 -3
- package/dist/jobs/run-history.js +73 -7
- package/dist/jobs/run-now.d.ts +20 -0
- package/dist/jobs/run-now.js +99 -0
- package/dist/jobs/scheduler.d.ts +15 -0
- package/dist/jobs/scheduler.js +76 -12
- package/dist/localization/default-messages.d.ts +8 -0
- package/dist/localization/default-messages.js +8 -0
- package/dist/notifications/routes.d.ts +4 -4
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/secrets/routes.d.ts +5 -5
- package/dist/secrets/routes.js +41 -17
- package/dist/server/action-discovery.js +4 -0
- package/dist/server/agent-chat-plugin.js +135 -68
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/email-actions.d.ts +6 -3
- package/dist/server/email-actions.js +17 -83
- package/dist/server/email-markdown.d.ts +4 -0
- package/dist/server/email-markdown.js +174 -0
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/automations/SKILL.md +1 -0
- package/dist/triggers/actions.d.ts +1 -1
- package/dist/triggers/actions.js +27 -5
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/automations/SKILL.md +1 -0
|
@@ -420,6 +420,7 @@ export function createAgentChatPlugin(options) {
|
|
|
420
420
|
...loopSettingsScripts,
|
|
421
421
|
};
|
|
422
422
|
const callAgentScript = await createCallAgentScriptEntry(options?.appId);
|
|
423
|
+
let runNowSchedulerDeps = null;
|
|
423
424
|
const browserTools = createBuilderBrowserTool({
|
|
424
425
|
getOrigin: () => getRequestRunContext()?.requestOrigin ?? "http://localhost:3000",
|
|
425
426
|
getOwner: () => getRequestRunContext()?.owner ?? getRequestUserEmail(),
|
|
@@ -741,15 +742,18 @@ export function createAgentChatPlugin(options) {
|
|
|
741
742
|
});
|
|
742
743
|
}
|
|
743
744
|
catch { }
|
|
744
|
-
// Core send-email tool
|
|
745
|
-
// SENDGRID_API_KEY is set. Keyed "core-send-email" to avoid colliding
|
|
745
|
+
// Core send-email tool. Keyed "core-send-email" to avoid colliding
|
|
746
746
|
// with the mail template's richer "send-email" action (template wins
|
|
747
747
|
// when both surfaces spread into the same object, but distinct keys
|
|
748
748
|
// keep both visible and avoid silent shadowing).
|
|
749
749
|
let coreEmailTools = {};
|
|
750
|
+
let backgroundCoreEmailTools = {};
|
|
750
751
|
try {
|
|
751
752
|
const { createCoreEmailActionEntries } = await import("./email-actions.js");
|
|
752
753
|
coreEmailTools = createCoreEmailActionEntries();
|
|
754
|
+
backgroundCoreEmailTools = createCoreEmailActionEntries({
|
|
755
|
+
unattended: true,
|
|
756
|
+
});
|
|
753
757
|
}
|
|
754
758
|
catch { }
|
|
755
759
|
// Core read-attachment tool — always registered so the agent can page
|
|
@@ -760,6 +764,29 @@ export function createAgentChatPlugin(options) {
|
|
|
760
764
|
coreAttachmentTools = createCoreAttachmentActionEntries();
|
|
761
765
|
}
|
|
762
766
|
catch { }
|
|
767
|
+
// Keep scheduler and event-trigger runs on one deliberately bounded
|
|
768
|
+
// background surface. Interactive-only tools stay out of it, while
|
|
769
|
+
// shared capabilities such as call-agent and core-send-email cannot
|
|
770
|
+
// drift independently between the two background entry points.
|
|
771
|
+
const getBackgroundActionEntries = (automation) => ({
|
|
772
|
+
...templateScripts,
|
|
773
|
+
...resourceScripts,
|
|
774
|
+
...docsScripts,
|
|
775
|
+
...(lazyContext ? frameworkContextTool : {}),
|
|
776
|
+
...urlTools,
|
|
777
|
+
...chatScripts,
|
|
778
|
+
...callAgentScript,
|
|
779
|
+
...jobTools,
|
|
780
|
+
...automationTools,
|
|
781
|
+
...notificationTools,
|
|
782
|
+
...progressTools,
|
|
783
|
+
...fetchTool,
|
|
784
|
+
...webSearchTool,
|
|
785
|
+
...toolActions,
|
|
786
|
+
...backgroundCoreEmailTools,
|
|
787
|
+
...coreAttachmentTools,
|
|
788
|
+
...getJobMcpActionEntries(automation),
|
|
789
|
+
});
|
|
763
790
|
// -----------------------------------------------------------------------
|
|
764
791
|
// Production code-execution mode resolution.
|
|
765
792
|
//
|
|
@@ -4432,6 +4459,50 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4432
4459
|
return { error: prepared.error };
|
|
4433
4460
|
}
|
|
4434
4461
|
const preparedMarker = prepared.body[AGENT_CHAT_BACKGROUND_RUN_FIELD];
|
|
4462
|
+
const automationRunId = preparedMarker && typeof preparedMarker === "object"
|
|
4463
|
+
? preparedMarker.automationRunId
|
|
4464
|
+
: undefined;
|
|
4465
|
+
if (typeof automationRunId === "string" && automationRunId) {
|
|
4466
|
+
const runAutomation = async () => {
|
|
4467
|
+
try {
|
|
4468
|
+
if (!runNowSchedulerDeps) {
|
|
4469
|
+
throw new Error("Automation runner is not initialized.");
|
|
4470
|
+
}
|
|
4471
|
+
const { runQueuedAutomation } = await import("../jobs/scheduler.js");
|
|
4472
|
+
const result = await runQueuedAutomation(automationRunId, runNowSchedulerDeps);
|
|
4473
|
+
return { ok: true, automationRunId, ...result };
|
|
4474
|
+
}
|
|
4475
|
+
catch (error) {
|
|
4476
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4477
|
+
const { finishAutomationRun } = await import("../jobs/run-history.js");
|
|
4478
|
+
await finishAutomationRun(automationRunId, "error", `Automation worker failed: ${message}. No delivery was confirmed.`).catch((finishError) => {
|
|
4479
|
+
console.warn(`[automations] could not record run-now worker failure for ${automationRunId}:`, finishError);
|
|
4480
|
+
});
|
|
4481
|
+
console.error(`[automations] run-now worker failed for ${automationRunId}:`, error);
|
|
4482
|
+
throw error;
|
|
4483
|
+
}
|
|
4484
|
+
};
|
|
4485
|
+
// Netlify background functions must keep the handler open for the
|
|
4486
|
+
// full automation. Other runtimes get an immediate receipt and
|
|
4487
|
+
// use waitUntil when the platform provides it; long-lived Node
|
|
4488
|
+
// processes can safely continue the promise after the response.
|
|
4489
|
+
if (isInBackgroundFunctionRuntime()) {
|
|
4490
|
+
try {
|
|
4491
|
+
return await runAutomation();
|
|
4492
|
+
}
|
|
4493
|
+
catch {
|
|
4494
|
+
setResponseStatus(event, 500);
|
|
4495
|
+
return { error: "Automation worker failed" };
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
const waitUntil = event.req?.waitUntil;
|
|
4499
|
+
const runPromise = runAutomation().catch(() => { });
|
|
4500
|
+
if (typeof waitUntil === "function") {
|
|
4501
|
+
waitUntil(runPromise);
|
|
4502
|
+
}
|
|
4503
|
+
setResponseStatus(event, 202);
|
|
4504
|
+
return { ok: true, accepted: true, automationRunId };
|
|
4505
|
+
}
|
|
4435
4506
|
const expectsBackgroundRuntime = backgroundRunMarkerExpectsBackgroundRuntime(preparedMarker);
|
|
4436
4507
|
const runtimeGlobals = globalThis;
|
|
4437
4508
|
const hadExpectedRuntimeMarker = Object.prototype.hasOwnProperty.call(runtimeGlobals, "__AGENT_NATIVE_BACKGROUND_RUNTIME_EXPECTED__");
|
|
@@ -4582,55 +4653,42 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4582
4653
|
// Poll every 60 seconds for due recurring jobs and execute them.
|
|
4583
4654
|
// Uses setInterval so it works in all deployment environments without
|
|
4584
4655
|
// requiring Nitro experimental tasks configuration.
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4656
|
+
try {
|
|
4657
|
+
const { processRecurringJobs } = await import("../jobs/scheduler.js");
|
|
4658
|
+
const schedulerDeps = {
|
|
4659
|
+
getActions: getBackgroundActionEntries,
|
|
4660
|
+
getSystemPrompt: async (owner) => {
|
|
4661
|
+
const resources = await loadResourcesForPrompt(owner, lazyContext, options?.appId);
|
|
4662
|
+
const schemaBlock = lazyContext
|
|
4663
|
+
? ""
|
|
4664
|
+
: await buildSchemaBlock(owner, databaseToolsMode);
|
|
4665
|
+
return basePrompt + resources + schemaBlock;
|
|
4666
|
+
},
|
|
4667
|
+
// `basePrompt` above is the same prompt the interactive chat
|
|
4668
|
+
// handler builds, so it teaches the same template actions plus
|
|
4669
|
+
// `manage-jobs` (Extended Capabilities / recurring jobs) and
|
|
4670
|
+
// `manage-progress` (SHARED_RULE_14) BY NAME — both are present in
|
|
4671
|
+
// getActions() via jobTools/progressTools. Keep the job runner's
|
|
4672
|
+
// first request on the same compact surface as interactive chat
|
|
4673
|
+
// instead of the full jobTools/automationTools/notificationTools/
|
|
4674
|
+
// fetchTool/webSearchTool/toolActions catalog every tick.
|
|
4675
|
+
getInitialToolNames: (job) => [
|
|
4676
|
+
...effectiveInitialToolNames,
|
|
4677
|
+
"manage-jobs",
|
|
4678
|
+
"manage-progress",
|
|
4679
|
+
...(job?.meta.mcpTools ?? []),
|
|
4680
|
+
],
|
|
4681
|
+
apiKey: options?.apiKey,
|
|
4682
|
+
model: options?.model,
|
|
4683
|
+
appId: options?.appId,
|
|
4684
|
+
};
|
|
4685
|
+
runNowSchedulerDeps = schedulerDeps;
|
|
4686
|
+
if (disableRecurringJobsRuntime) {
|
|
4687
|
+
if (process.env.DEBUG) {
|
|
4688
|
+
console.log("[recurring-jobs] Scheduler disabled for local development");
|
|
4689
|
+
}
|
|
4588
4690
|
}
|
|
4589
|
-
|
|
4590
|
-
else {
|
|
4591
|
-
try {
|
|
4592
|
-
const { processRecurringJobs } = await import("../jobs/scheduler.js");
|
|
4593
|
-
const schedulerDeps = {
|
|
4594
|
-
getActions: (job) => ({
|
|
4595
|
-
...templateScripts,
|
|
4596
|
-
...resourceScripts,
|
|
4597
|
-
...docsScripts,
|
|
4598
|
-
...(lazyContext ? frameworkContextTool : {}),
|
|
4599
|
-
...chatScripts,
|
|
4600
|
-
...jobTools,
|
|
4601
|
-
...automationTools,
|
|
4602
|
-
...notificationTools,
|
|
4603
|
-
...progressTools,
|
|
4604
|
-
...fetchTool,
|
|
4605
|
-
...webSearchTool,
|
|
4606
|
-
...toolActions,
|
|
4607
|
-
...getJobMcpActionEntries(job),
|
|
4608
|
-
}),
|
|
4609
|
-
getSystemPrompt: async (owner) => {
|
|
4610
|
-
const resources = await loadResourcesForPrompt(owner, lazyContext, options?.appId);
|
|
4611
|
-
const schemaBlock = lazyContext
|
|
4612
|
-
? ""
|
|
4613
|
-
: await buildSchemaBlock(owner, databaseToolsMode);
|
|
4614
|
-
return basePrompt + resources + schemaBlock;
|
|
4615
|
-
},
|
|
4616
|
-
// `basePrompt` above is the same prompt the interactive chat
|
|
4617
|
-
// handler builds, so it teaches the same template actions plus
|
|
4618
|
-
// `manage-jobs` (Extended Capabilities / recurring jobs) and
|
|
4619
|
-
// `manage-progress` (SHARED_RULE_14) BY NAME — both are present in
|
|
4620
|
-
// getActions() via jobTools/progressTools. Keep the job runner's
|
|
4621
|
-
// first request on the same compact surface as interactive chat
|
|
4622
|
-
// instead of the full jobTools/automationTools/notificationTools/
|
|
4623
|
-
// fetchTool/webSearchTool/toolActions catalog every tick.
|
|
4624
|
-
getInitialToolNames: (job) => [
|
|
4625
|
-
...effectiveInitialToolNames,
|
|
4626
|
-
"manage-jobs",
|
|
4627
|
-
"manage-progress",
|
|
4628
|
-
...(job?.meta.mcpTools ?? []),
|
|
4629
|
-
],
|
|
4630
|
-
apiKey: options?.apiKey,
|
|
4631
|
-
model: options?.model,
|
|
4632
|
-
appId: options?.appId,
|
|
4633
|
-
};
|
|
4691
|
+
else {
|
|
4634
4692
|
// Start after a 10-second delay to let the server fully initialize
|
|
4635
4693
|
setTimeout(() => {
|
|
4636
4694
|
setInterval(() => {
|
|
@@ -4642,10 +4700,33 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4642
4700
|
console.log("[recurring-jobs] Scheduler started (60s interval)");
|
|
4643
4701
|
}, 10_000);
|
|
4644
4702
|
}
|
|
4645
|
-
catch (err) {
|
|
4646
|
-
// Jobs module not available — skip silently
|
|
4647
|
-
}
|
|
4648
4703
|
}
|
|
4704
|
+
catch (error) {
|
|
4705
|
+
console.warn("[recurring-jobs] Scheduler module unavailable:", error instanceof Error ? error.message : error);
|
|
4706
|
+
}
|
|
4707
|
+
// A non-Netlify self-dispatch only waits for the request to leave the
|
|
4708
|
+
// current invocation. Keep the durable history row as a retryable queue
|
|
4709
|
+
// so a frozen serverless handoff is recovered on the next sweep.
|
|
4710
|
+
(() => {
|
|
4711
|
+
let inFlight = false;
|
|
4712
|
+
const sweep = async () => {
|
|
4713
|
+
if (inFlight)
|
|
4714
|
+
return;
|
|
4715
|
+
inFlight = true;
|
|
4716
|
+
try {
|
|
4717
|
+
const { redispatchUnclaimedAutomationRuns } = await import("../jobs/run-now.js");
|
|
4718
|
+
await redispatchUnclaimedAutomationRuns();
|
|
4719
|
+
}
|
|
4720
|
+
catch (error) {
|
|
4721
|
+
console.warn("[automations] queued-run sweep failed; retrying next tick:", error);
|
|
4722
|
+
}
|
|
4723
|
+
finally {
|
|
4724
|
+
inFlight = false;
|
|
4725
|
+
}
|
|
4726
|
+
};
|
|
4727
|
+
setTimeout(() => void sweep(), 15_000);
|
|
4728
|
+
setInterval(() => void sweep(), 30_000);
|
|
4729
|
+
})();
|
|
4649
4730
|
mcpInitializationPromise = initializeMcpManager().catch((err) => {
|
|
4650
4731
|
console.warn(`[mcp-client] deferred initialization failed: ${err?.message ?? err}`);
|
|
4651
4732
|
});
|
|
@@ -4912,21 +4993,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4912
4993
|
try {
|
|
4913
4994
|
const { initTriggerDispatcher } = await import("../triggers/dispatcher.js");
|
|
4914
4995
|
await initTriggerDispatcher({
|
|
4915
|
-
getActions:
|
|
4916
|
-
...templateScripts,
|
|
4917
|
-
...resourceScripts,
|
|
4918
|
-
...docsScripts,
|
|
4919
|
-
...(lazyContext ? frameworkContextTool : {}),
|
|
4920
|
-
...chatScripts,
|
|
4921
|
-
...jobTools,
|
|
4922
|
-
...automationTools,
|
|
4923
|
-
...notificationTools,
|
|
4924
|
-
...progressTools,
|
|
4925
|
-
...fetchTool,
|
|
4926
|
-
...webSearchTool,
|
|
4927
|
-
...toolActions,
|
|
4928
|
-
...getJobMcpActionEntries(automation),
|
|
4929
|
-
}),
|
|
4996
|
+
getActions: getBackgroundActionEntries,
|
|
4930
4997
|
getSystemPrompt: async (owner) => {
|
|
4931
4998
|
const resources = await loadResourcesForPrompt(owner, lazyContext, options?.appId);
|
|
4932
4999
|
const schemaBlock = lazyContext
|
|
@@ -27,11 +27,11 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
|
|
|
27
27
|
export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
28
28
|
error: any;
|
|
29
29
|
} | {
|
|
30
|
-
error?: undefined;
|
|
31
30
|
ok: boolean;
|
|
32
31
|
key: string;
|
|
33
32
|
baseUrlKey?: string;
|
|
34
33
|
scope: AgentEngineApiKeyScope;
|
|
34
|
+
error?: undefined;
|
|
35
35
|
}>>;
|
|
36
36
|
export {};
|
|
37
37
|
//# sourceMappingURL=agent-engine-api-key-route.d.ts.map
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* Registered as a native tool in every template. sendEmail() checks the
|
|
6
6
|
* scoped Resend/SendGrid configuration at call time.
|
|
7
7
|
*
|
|
8
|
-
* SAFETY:
|
|
9
|
-
*
|
|
8
|
+
* SAFETY: interactive runs are draft-first. Unattended automation runs are
|
|
9
|
+
* already authorized when the automation is created, so they get a separate
|
|
10
|
+
* description that permits delivery without interactive confirmation.
|
|
10
11
|
*
|
|
11
12
|
* COLLISION: the mail template registers its own richer "send-email" action.
|
|
12
13
|
* The template's registration comes after this one in the spread, so it wins
|
|
@@ -14,5 +15,7 @@
|
|
|
14
15
|
* action is keyed "core-send-email" which is distinct from the template name.
|
|
15
16
|
*/
|
|
16
17
|
import type { ActionEntry } from "../agent/production-agent.js";
|
|
17
|
-
export declare function createCoreEmailActionEntries(
|
|
18
|
+
export declare function createCoreEmailActionEntries(options?: {
|
|
19
|
+
unattended?: boolean;
|
|
20
|
+
}): Record<string, ActionEntry>;
|
|
18
21
|
//# sourceMappingURL=email-actions.d.ts.map
|
|
@@ -5,103 +5,37 @@
|
|
|
5
5
|
* Registered as a native tool in every template. sendEmail() checks the
|
|
6
6
|
* scoped Resend/SendGrid configuration at call time.
|
|
7
7
|
*
|
|
8
|
-
* SAFETY:
|
|
9
|
-
*
|
|
8
|
+
* SAFETY: interactive runs are draft-first. Unattended automation runs are
|
|
9
|
+
* already authorized when the automation is created, so they get a separate
|
|
10
|
+
* description that permits delivery without interactive confirmation.
|
|
10
11
|
*
|
|
11
12
|
* COLLISION: the mail template registers its own richer "send-email" action.
|
|
12
13
|
* The template's registration comes after this one in the spread, so it wins
|
|
13
14
|
* when both would be present under the same key. To avoid any ambiguity this
|
|
14
15
|
* action is keyed "core-send-email" which is distinct from the template name.
|
|
15
16
|
*/
|
|
17
|
+
import { markdownToHtml, markdownToText, wrapInEmailTemplate, } from "./email-markdown.js";
|
|
16
18
|
import { sendEmail } from "./email.js";
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
.replace(/!\[([^\]]*)\]\([^\s)]+\)/g, "$1")
|
|
20
|
-
.replace(/\[([^\]]+)\]\([^\s)]+\)/g, "$1 ($2)")
|
|
21
|
-
.replace(/`([^`]+)`/g, "$1")
|
|
22
|
-
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
23
|
-
.replace(/\*([^*\n]+)\*/g, "$1")
|
|
24
|
-
.replace(/^#{1,6}\s+/gm, "")
|
|
25
|
-
.trim();
|
|
26
|
-
}
|
|
27
|
-
function markdownToHtml(md) {
|
|
28
|
-
const normalized = md.replace(/\r\n/g, "\n").trim();
|
|
29
|
-
if (!normalized)
|
|
30
|
-
return "<p></p>";
|
|
31
|
-
// Escape HTML entities in a string, preserving already-encoded references.
|
|
32
|
-
const esc = (s) => s
|
|
33
|
-
.replace(/&/g, "&")
|
|
34
|
-
.replace(/</g, "<")
|
|
35
|
-
.replace(/>/g, ">")
|
|
36
|
-
.replace(/"/g, """);
|
|
37
|
-
// Inline markdown: links, bold, italic, code, bare URLs.
|
|
38
|
-
const inline = (s) => s
|
|
39
|
-
.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_m, label, url) => `<a href="${esc(url)}" target="_blank" rel="noopener noreferrer">${esc(label)}</a>`)
|
|
40
|
-
.replace(/(^|[^\w"'=])(https?:\/\/[^\s<]+)/g, (_m, pre, url) => `${pre}<a href="${esc(url)}" target="_blank" rel="noopener noreferrer">${esc(url)}</a>`)
|
|
41
|
-
.replace(/`([^`]+)`/g, (_m, code) => `<code>${esc(code)}</code>`)
|
|
42
|
-
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
43
|
-
.replace(/(^|[\s(])\*([^*\n]+)\*(?=$|[\s).,!?:;])/g, "$1<em>$2</em>");
|
|
44
|
-
const blocks = normalized.split(/\n{2,}/);
|
|
45
|
-
const html = blocks
|
|
46
|
-
.map((block) => {
|
|
47
|
-
const trimmed = block.trim();
|
|
48
|
-
// Fenced code block
|
|
49
|
-
if (trimmed.startsWith("```") && trimmed.endsWith("```")) {
|
|
50
|
-
const code = trimmed
|
|
51
|
-
.replace(/^```[^\n]*\n?/, "")
|
|
52
|
-
.replace(/\n?```$/, "");
|
|
53
|
-
return `<pre><code>${esc(code)}</code></pre>`;
|
|
54
|
-
}
|
|
55
|
-
// Heading
|
|
56
|
-
const hm = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
57
|
-
if (hm) {
|
|
58
|
-
const level = hm[1].length;
|
|
59
|
-
return `<h${level}>${inline(hm[2])}</h${level}>`;
|
|
60
|
-
}
|
|
61
|
-
// Unordered list
|
|
62
|
-
if (/^[-*+]\s+/m.test(trimmed)) {
|
|
63
|
-
const items = trimmed
|
|
64
|
-
.split("\n")
|
|
65
|
-
.filter(Boolean)
|
|
66
|
-
.map((l) => `<li>${inline(l.replace(/^[-*+]\s+/, ""))}</li>`)
|
|
67
|
-
.join("");
|
|
68
|
-
return `<ul style="margin:0 0 1em;padding-left:1.5em">${items}</ul>`;
|
|
69
|
-
}
|
|
70
|
-
// Ordered list
|
|
71
|
-
if (/^\d+\.\s+/m.test(trimmed)) {
|
|
72
|
-
const items = trimmed
|
|
73
|
-
.split("\n")
|
|
74
|
-
.filter(Boolean)
|
|
75
|
-
.map((l) => `<li>${inline(l.replace(/^\d+\.\s+/, ""))}</li>`)
|
|
76
|
-
.join("");
|
|
77
|
-
return `<ol style="margin:0 0 1em;padding-left:1.5em">${items}</ol>`;
|
|
78
|
-
}
|
|
79
|
-
// Blockquote
|
|
80
|
-
if (trimmed.startsWith("> ")) {
|
|
81
|
-
const inner = trimmed
|
|
82
|
-
.split("\n")
|
|
83
|
-
.map((l) => (l.startsWith("> ") ? l.slice(2) : l))
|
|
84
|
-
.join("\n");
|
|
85
|
-
return `<blockquote style="margin:0 0 1em 1em;border-left:3px solid #ccc;padding-left:0.75em;color:#555">${inline(inner)}</blockquote>`;
|
|
86
|
-
}
|
|
87
|
-
return `<p style="margin:0 0 1em">${inline(trimmed).replace(/\n/g, "<br />")}</p>`;
|
|
88
|
-
})
|
|
89
|
-
.join("");
|
|
90
|
-
return html;
|
|
91
|
-
}
|
|
92
|
-
export function createCoreEmailActionEntries() {
|
|
19
|
+
export function createCoreEmailActionEntries(options) {
|
|
20
|
+
const unattended = options?.unattended === true;
|
|
93
21
|
return {
|
|
94
22
|
"core-send-email": {
|
|
95
23
|
tool: {
|
|
96
24
|
description: [
|
|
97
25
|
"Send a transactional email via the app's configured email provider (Resend or SendGrid).",
|
|
98
26
|
"",
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
27
|
+
...(unattended
|
|
28
|
+
? [
|
|
29
|
+
"This is an explicitly authorized unattended automation run. Send the email without asking for an interactive confirmation.",
|
|
30
|
+
]
|
|
31
|
+
: [
|
|
32
|
+
"IMPORTANT — DRAFT-FIRST SAFETY RULE: Never call this tool until the user has explicitly",
|
|
33
|
+
"confirmed they want to send. Always compose the full email content first, show it to the",
|
|
34
|
+
"user, and wait for an explicit 'yes, send it' before invoking this action.",
|
|
35
|
+
]),
|
|
102
36
|
"",
|
|
103
37
|
"The body is written in markdown. Tables, lists, bold, italic, links, and code blocks",
|
|
104
|
-
"are all supported and will render correctly in email clients.",
|
|
38
|
+
"are all supported and will render correctly in email clients. Write the body in markdown; do not hand-write HTML.",
|
|
105
39
|
"",
|
|
106
40
|
"This sends via the framework transport (Resend/SendGrid). It is NOT the Gmail-based",
|
|
107
41
|
"send-email action in the mail template — use this for system/notification emails from",
|
|
@@ -168,7 +102,7 @@ export function createCoreEmailActionEntries() {
|
|
|
168
102
|
await sendEmail({
|
|
169
103
|
to,
|
|
170
104
|
subject,
|
|
171
|
-
html: markdownToHtml(bodyMd),
|
|
105
|
+
html: wrapInEmailTemplate(markdownToHtml(bodyMd)),
|
|
172
106
|
text: markdownToText(bodyMd),
|
|
173
107
|
...(from ? { from } : {}),
|
|
174
108
|
...(cc ? { cc } : {}),
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
const EMAIL_BACKGROUND = "#f6f7f9"; // guard:allow-raw-color - email clients need inlined colors
|
|
2
|
+
const EMAIL_TEXT = "#1f2937"; // guard:allow-raw-color - email clients need inlined colors
|
|
3
|
+
const EMAIL_MUTED = "#64748b"; // guard:allow-raw-color - email clients need inlined colors
|
|
4
|
+
const EMAIL_BORDER = "#dbe3ec"; // guard:allow-raw-color - email clients need inlined colors
|
|
5
|
+
const EMAIL_CODE = "#eef2f7"; // guard:allow-raw-color - email clients need inlined colors
|
|
6
|
+
const EMAIL_SURFACE = "#ffffff"; // guard:allow-raw-color - email clients need inlined colors
|
|
7
|
+
function escapeHtml(value) {
|
|
8
|
+
return value
|
|
9
|
+
.replace(/&/g, "&")
|
|
10
|
+
.replace(/</g, "<")
|
|
11
|
+
.replace(/>/g, ">")
|
|
12
|
+
.replace(/\"/g, """);
|
|
13
|
+
}
|
|
14
|
+
function inlineMarkdown(value) {
|
|
15
|
+
let html = escapeHtml(value);
|
|
16
|
+
const protectedLinks = [];
|
|
17
|
+
html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_match, label, url) => {
|
|
18
|
+
const index = protectedLinks.push(`<a href="${url}" style="color:${EMAIL_TEXT};text-decoration:underline;">${label}</a>`);
|
|
19
|
+
return `\u0000${index - 1}\u0000`;
|
|
20
|
+
});
|
|
21
|
+
html = html.replace(/(^|[^\w"'=])(https?:\/\/[^\s<]+)/g, (_match, prefix, rawUrl) => {
|
|
22
|
+
let url = rawUrl;
|
|
23
|
+
let trailing = "";
|
|
24
|
+
while (/[.,!?;:]$/.test(url)) {
|
|
25
|
+
trailing = url.slice(-1) + trailing;
|
|
26
|
+
url = url.slice(0, -1);
|
|
27
|
+
}
|
|
28
|
+
let label = "Open link";
|
|
29
|
+
const normalizedUrl = url.replace(/&/g, "&");
|
|
30
|
+
if (URL.canParse(normalizedUrl)) {
|
|
31
|
+
const host = new URL(normalizedUrl).hostname.replace(/^www\./, "");
|
|
32
|
+
if (host)
|
|
33
|
+
label = `Open ${host}`;
|
|
34
|
+
}
|
|
35
|
+
return `${prefix}<a href="${url}" style="color:${EMAIL_TEXT};text-decoration:underline;">${label}</a>${trailing}`;
|
|
36
|
+
});
|
|
37
|
+
html = html
|
|
38
|
+
.replace(/`([^`]+)`/g, `<code style="background:${EMAIL_CODE};padding:2px 5px;border-radius:4px;font-size:0.92em;">$1</code>`)
|
|
39
|
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
40
|
+
.replace(/__([^_]+)__/g, "<strong>$1</strong>")
|
|
41
|
+
.replace(/(^|[\s(])\*([^*\n]+)\*(?=$|[\s).,!?:;])/g, "$1<em>$2</em>")
|
|
42
|
+
.replace(/(^|[\s(])_([^_\n]+)_(?=$|[\s).,!?:;])/g, "$1<em>$2</em>");
|
|
43
|
+
html = html.replace(/\u0000(\d+)\u0000/g, (_match, index) => {
|
|
44
|
+
return protectedLinks[Number(index)] ?? "";
|
|
45
|
+
});
|
|
46
|
+
return html;
|
|
47
|
+
}
|
|
48
|
+
function renderTable(lines) {
|
|
49
|
+
if (lines.length < 2 || !lines.every((line) => line.includes("|"))) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const parseRow = (line) => line
|
|
53
|
+
.trim()
|
|
54
|
+
.replace(/^\|/, "")
|
|
55
|
+
.replace(/\|$/, "")
|
|
56
|
+
.split("|")
|
|
57
|
+
.map((cell) => cell.trim());
|
|
58
|
+
const header = parseRow(lines[0]);
|
|
59
|
+
const divider = parseRow(lines[1]);
|
|
60
|
+
if (!header.length || !divider.every((cell) => /^:?-{3,}:?$/.test(cell))) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const body = lines.slice(2).map(parseRow);
|
|
64
|
+
const headHtml = header
|
|
65
|
+
.map((cell) => `<th align="left" style="border:1px solid ${EMAIL_BORDER};padding:8px 10px;background:${EMAIL_CODE};font-weight:600;">${inlineMarkdown(cell)}</th>`)
|
|
66
|
+
.join("");
|
|
67
|
+
const bodyHtml = body
|
|
68
|
+
.map((row) => `<tr>${header
|
|
69
|
+
.map((_cell, index) => `<td style="border:1px solid ${EMAIL_BORDER};padding:8px 10px;vertical-align:top;">${inlineMarkdown(row[index] ?? "")}</td>`)
|
|
70
|
+
.join("")}</tr>`)
|
|
71
|
+
.join("");
|
|
72
|
+
return `<div style="overflow-x:auto;margin:0 0 18px;"><table role="presentation" style="border-collapse:collapse;width:100%;font-size:14px;"><thead><tr>${headHtml}</tr></thead><tbody>${bodyHtml}</tbody></table></div>`;
|
|
73
|
+
}
|
|
74
|
+
export function markdownToText(md) {
|
|
75
|
+
return md
|
|
76
|
+
.replace(/^(?:```[^\n]*\n|```$)/gm, "")
|
|
77
|
+
.replace(/!\[([^\]]*)\]\([^\s)]+\)/g, "$1")
|
|
78
|
+
.replace(/\[([^\]]+)\]\(([^\s)]+)\)/g, "$1 ($2)")
|
|
79
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
80
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
81
|
+
.replace(/__([^_]+)__/g, "$1")
|
|
82
|
+
.replace(/(^|[\s(])\*([^*\n]+)\*(?=$|[\s).,!?:;])/gm, "$1$2")
|
|
83
|
+
.replace(/(^|[\s(])_([^_\n]+)_(?=$|[\s).,!?:;])/gm, "$1$2")
|
|
84
|
+
.replace(/^#{1,6}\s+/gm, "")
|
|
85
|
+
.replace(/^\s*[-*+]\s+/gm, "")
|
|
86
|
+
.replace(/^\s*\d+\.\s+/gm, "")
|
|
87
|
+
.replace(/^>\s?/gm, "")
|
|
88
|
+
.trim();
|
|
89
|
+
}
|
|
90
|
+
export function markdownToHtml(md) {
|
|
91
|
+
const lines = md.replace(/\r\n/g, "\n").trim().split("\n");
|
|
92
|
+
if (!md.trim())
|
|
93
|
+
return '<p style="margin:0;"> </p>';
|
|
94
|
+
const blocks = [];
|
|
95
|
+
let index = 0;
|
|
96
|
+
while (index < lines.length) {
|
|
97
|
+
if (!lines[index].trim()) {
|
|
98
|
+
index += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const line = lines[index].trim();
|
|
102
|
+
if (line.startsWith("```") &&
|
|
103
|
+
lines.slice(index + 1).some((item) => item.trim() === "```")) {
|
|
104
|
+
const end = lines.findIndex((item, offset) => offset > index && item.trim() === "```");
|
|
105
|
+
const code = lines.slice(index + 1, end).join("\n");
|
|
106
|
+
blocks.push(`<pre style="margin:0 0 18px;padding:14px 16px;background:${EMAIL_CODE};border-radius:6px;overflow-x:auto;white-space:pre-wrap;"><code>${escapeHtml(code)}</code></pre>`);
|
|
107
|
+
index = end + 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
111
|
+
if (heading) {
|
|
112
|
+
const level = Math.min(heading[1].length, 3);
|
|
113
|
+
const sizes = ["24px", "19px", "16px"];
|
|
114
|
+
blocks.push(`<h${level} style="margin:0 0 10px;color:${EMAIL_TEXT};font-size:${sizes[level - 1]};line-height:1.3;">${inlineMarkdown(heading[2])}</h${level}>`);
|
|
115
|
+
index += 1;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (/^(?:-{3,}|\*{3,})$/.test(line)) {
|
|
119
|
+
blocks.push(`<hr style="border:0;border-top:1px solid ${EMAIL_BORDER};margin:4px 0 18px;" />`);
|
|
120
|
+
index += 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const listMatch = line.match(/^([-*+] |\d+\. )/);
|
|
124
|
+
if (listMatch) {
|
|
125
|
+
const ordered = /^\d/.test(listMatch[1]);
|
|
126
|
+
const items = [];
|
|
127
|
+
while (index < lines.length) {
|
|
128
|
+
const item = lines[index]
|
|
129
|
+
.trim()
|
|
130
|
+
.match(ordered ? /^\d+\.\s+(.+)$/ : /^[-*+]\s+(.+)$/);
|
|
131
|
+
if (!item)
|
|
132
|
+
break;
|
|
133
|
+
items.push(`<li style="margin:0 0 6px;">${inlineMarkdown(item[1])}</li>`);
|
|
134
|
+
index += 1;
|
|
135
|
+
}
|
|
136
|
+
const tag = ordered ? "ol" : "ul";
|
|
137
|
+
blocks.push(`<${tag} style="margin:0 0 18px;padding-left:24px;">${items.join("")}</${tag}>`);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const tableLines = [line];
|
|
141
|
+
let tableEnd = index + 1;
|
|
142
|
+
while (tableEnd < lines.length &&
|
|
143
|
+
lines[tableEnd].trim() &&
|
|
144
|
+
lines[tableEnd].includes("|")) {
|
|
145
|
+
tableLines.push(lines[tableEnd].trim());
|
|
146
|
+
tableEnd += 1;
|
|
147
|
+
}
|
|
148
|
+
const table = renderTable(tableLines);
|
|
149
|
+
if (table) {
|
|
150
|
+
blocks.push(table);
|
|
151
|
+
index = tableEnd;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const paragraph = [line];
|
|
155
|
+
index += 1;
|
|
156
|
+
while (index < lines.length && lines[index].trim()) {
|
|
157
|
+
if (/^(?:#{1,6}\s|[-*+]\s|\d+\.\s|>\s|```)/.test(lines[index].trim()))
|
|
158
|
+
break;
|
|
159
|
+
paragraph.push(lines[index].trim());
|
|
160
|
+
index += 1;
|
|
161
|
+
}
|
|
162
|
+
if (paragraph.every((item) => item.startsWith("> "))) {
|
|
163
|
+
blocks.push(`<blockquote style="margin:0 0 18px;padding:4px 0 4px 14px;border-left:3px solid ${EMAIL_BORDER};color:${EMAIL_MUTED};">${inlineMarkdown(paragraph.map((item) => item.slice(2)).join("\n")).replace(/\n/g, "<br />")}</blockquote>`);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
blocks.push(`<p style="margin:0 0 18px;line-height:1.65;">${inlineMarkdown(paragraph.join("\n")).replace(/\n/g, "<br />")}</p>`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return blocks.join("\n");
|
|
170
|
+
}
|
|
171
|
+
export function wrapInEmailTemplate(bodyHtml) {
|
|
172
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head><body style="margin:0;padding:0;background:${EMAIL_BACKGROUND};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:${EMAIL_TEXT};"><div style="padding:24px 12px;"><div style="box-sizing:border-box;max-width:640px;margin:0 auto;padding:28px 30px;background:${EMAIL_SURFACE};border:1px solid ${EMAIL_BORDER};border-radius:8px;font-size:15px;line-height:1.65;">${bodyHtml}</div></div></body></html>`;
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=email-markdown.js.map
|
|
@@ -103,6 +103,7 @@ All automation operations are accessed through a single `manage-automations` too
|
|
|
103
103
|
| `update` | Update an existing automation (enabled, condition, body) |
|
|
104
104
|
| `delete` | Delete an automation (always confirm with user first) |
|
|
105
105
|
| `fire-test` | Emit a `test.event.fired` event to validate automations |
|
|
106
|
+
| `run-now` | Run one automation immediately with its real actions and side effects |
|
|
106
107
|
|
|
107
108
|
Additional tool: `web-request` — outbound HTTP with `${keys.NAME}` substitution.
|
|
108
109
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* available in every template. The agent uses them to create, list, and
|
|
6
6
|
* manage automations from chat.
|
|
7
7
|
*
|
|
8
|
-
* All
|
|
8
|
+
* All seven operations are consolidated into a single `manage-automations` tool
|
|
9
9
|
* with an `action` discriminator to keep the tool registry compact.
|
|
10
10
|
*/
|
|
11
11
|
import type { ActionEntry } from "../agent/production-agent.js";
|