@fastagent-sh/fastagent 0.20.0 → 0.21.0
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 +5 -1
- package/dist/channels/agentcore-protocol.d.ts +112 -0
- package/dist/channels/agentcore-protocol.js +22 -0
- package/dist/channels/agentcore-service.d.ts +0 -4
- package/dist/channels/agentcore-service.js +1 -6
- package/dist/channels/agentcore-state.d.ts +5 -11
- package/dist/channels/agentcore-state.js +4 -1
- package/dist/channels/agentcore.d.ts +1 -67
- package/dist/channels/agentcore.js +90 -89
- package/dist/channels/control.d.ts +1 -1
- package/dist/channels/control.js +13 -62
- package/dist/channels/discover.d.ts +0 -1
- package/dist/channels/discover.js +1 -3
- package/dist/channels/feishu/context-buffer.d.ts +6 -0
- package/dist/channels/feishu/context-buffer.js +0 -38
- package/dist/channels/feishu/crypto.d.ts +0 -2
- package/dist/channels/feishu/crypto.js +3 -8
- package/dist/channels/feishu/feishu-api.js +2 -4
- package/dist/channels/feishu/feishu.js +56 -115
- package/dist/channels/feishu/parse.d.ts +4 -3
- package/dist/channels/feishu/parse.js +5 -4
- package/dist/channels/feishu/register-webhook.d.ts +5 -5
- package/dist/channels/feishu/register-webhook.js +43 -57
- package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
- package/dist/channels/feishu/setup-mode.d.ts +30 -0
- package/dist/channels/feishu/setup-mode.js +26 -0
- package/dist/channels/feishu/shared-api.d.ts +10 -0
- package/dist/channels/feishu/shared-api.js +38 -0
- package/dist/channels/http.d.ts +0 -8
- package/dist/channels/http.js +4 -56
- package/dist/channels/kit/turn-runner.d.ts +59 -0
- package/dist/channels/kit/turn-runner.js +84 -0
- package/dist/channels/lark/scaffold/lark-send.ts +12 -67
- package/dist/channels/registration.d.ts +36 -1
- package/dist/channels/registration.js +57 -1
- package/dist/channels/secret.d.ts +1 -0
- package/dist/channels/secret.js +16 -0
- package/dist/channels/slack/config-api.d.ts +35 -3
- package/dist/channels/slack/config-api.js +51 -9
- package/dist/channels/slack/manifest.js +5 -1
- package/dist/channels/slack/onboard.d.ts +5 -5
- package/dist/channels/slack/onboard.js +41 -20
- package/dist/channels/slack/register-webhook.d.ts +3 -2
- package/dist/channels/slack/register-webhook.js +34 -14
- package/dist/channels/slack/scaffold/channel.ts +3 -10
- package/dist/channels/slack/scaffold/slack-send.ts +18 -126
- package/dist/channels/slack/shared-api.d.ts +10 -0
- package/dist/channels/slack/shared-api.js +34 -0
- package/dist/channels/slack/slack-api.d.ts +20 -2
- package/dist/channels/slack/slack-api.js +81 -22
- package/dist/channels/slack/slack.d.ts +0 -10
- package/dist/channels/slack/slack.js +60 -99
- package/dist/channels/sse.d.ts +4 -0
- package/dist/channels/sse.js +66 -0
- package/dist/channels/telegram/register-webhook.d.ts +6 -9
- package/dist/channels/telegram/register-webhook.js +44 -42
- package/dist/channels/telegram/telegram.js +47 -142
- package/dist/channels/wait-health.js +7 -4
- package/dist/cli/add-feishu.js +3 -10
- package/dist/cli/add-slack.js +7 -20
- package/dist/cli/commands/add.d.ts +0 -1
- package/dist/cli/commands/add.js +22 -31
- package/dist/cli/commands/chat.js +6 -12
- package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
- package/dist/cli/commands/deploy/agentcore.js +178 -0
- package/dist/cli/commands/deploy/docker.d.ts +2 -0
- package/dist/cli/commands/deploy/docker.js +119 -0
- package/dist/cli/commands/deploy/fly.d.ts +2 -0
- package/dist/cli/commands/deploy/fly.js +131 -0
- package/dist/cli/commands/deploy/railway.d.ts +2 -0
- package/dist/cli/commands/deploy/railway.js +71 -0
- package/dist/cli/commands/deploy/shared.d.ts +114 -0
- package/dist/cli/commands/deploy/shared.js +124 -0
- package/dist/cli/commands/deploy.d.ts +17 -49
- package/dist/cli/commands/deploy.js +39 -618
- package/dist/cli/commands/dev.js +17 -46
- package/dist/cli/commands/fire.js +6 -12
- package/dist/cli/commands/info.js +2 -1
- package/dist/cli/commands/invoke.js +4 -11
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +17 -46
- package/dist/cli/commands/tool.js +3 -8
- package/dist/cli/kernel.d.ts +0 -2
- package/dist/cli/kernel.js +0 -2
- package/dist/cli/program.js +7 -12
- package/dist/cli/serve.d.ts +36 -19
- package/dist/cli/serve.js +91 -7
- package/dist/cli/shared.d.ts +13 -18
- package/dist/cli/shared.js +20 -4
- package/dist/deploy/agentcore/forwarder.js +250 -0
- package/dist/deploy/agentcore/plan.d.ts +23 -2
- package/dist/deploy/agentcore/plan.js +29 -244
- package/dist/deploy/agentcore/run.d.ts +4 -20
- package/dist/deploy/agentcore/run.js +22 -7
- package/dist/deploy/channel-ingress.js +1 -1
- package/dist/deploy/docker/run.d.ts +17 -3
- package/dist/deploy/docker/run.js +30 -8
- package/dist/deploy/fly/plan.js +7 -0
- package/dist/deploy/fly/run.d.ts +27 -0
- package/dist/deploy/fly/run.js +102 -20
- package/dist/deploy/hosts.d.ts +5 -0
- package/dist/deploy/hosts.js +4 -0
- package/dist/deploy/preflight.js +3 -3
- package/dist/deploy/railway/plan.d.ts +5 -0
- package/dist/deploy/railway/plan.js +7 -0
- package/dist/deploy/railway/run.d.ts +2 -1
- package/dist/deploy/railway/run.js +5 -4
- package/dist/deploy/secrets.js +0 -17
- package/dist/engines/pi/agent-session-factory.d.ts +37 -17
- package/dist/engines/pi/agent-session-factory.js +109 -85
- package/dist/engines/pi/config.d.ts +1 -1
- package/dist/engines/pi/create.d.ts +22 -39
- package/dist/engines/pi/create.js +58 -73
- package/dist/engines/pi/definition.js +8 -8
- package/dist/engines/pi/invoke-session.js +13 -30
- package/dist/engines/pi/open.d.ts +2 -2
- package/dist/engines/pi/open.js +31 -41
- package/dist/engines/pi/retry-event.d.ts +6 -0
- package/dist/engines/pi/retry-event.js +15 -0
- package/dist/engines/pi/session-builder.js +26 -113
- package/dist/engines/pi/session-control.d.ts +10 -18
- package/dist/engines/pi/session-control.js +47 -81
- package/dist/engines/pi/session-settings.d.ts +1 -1
- package/dist/engines/pi/session-settings.js +1 -1
- package/dist/engines/pi/session-store.d.ts +12 -17
- package/dist/engines/pi/session-store.js +18 -40
- package/dist/engines/pi/tool-context.d.ts +3 -3
- package/dist/engines/pi/tool.d.ts +6 -9
- package/dist/engines/pi/tool.js +1 -0
- package/dist/feishu.d.ts +1 -0
- package/dist/feishu.js +1 -0
- package/dist/lark.d.ts +1 -0
- package/dist/lark.js +1 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +5 -0
- package/dist/log.d.ts +9 -17
- package/dist/log.js +25 -30
- package/dist/paths.d.ts +7 -3
- package/dist/paths.js +17 -4
- package/dist/scaffold/add-channel.d.ts +6 -1
- package/dist/scaffold/add-channel.js +48 -65
- package/dist/schedule/wake-alarm.d.ts +1 -12
- package/dist/schedule/wake-alarm.js +2 -3
- package/dist/service.d.ts +23 -22
- package/dist/service.js +7 -73
- package/dist/session-remote.d.ts +5 -5
- package/dist/session-remote.js +25 -28
- package/dist/session.d.ts +1 -1
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +1 -0
- package/dist/tunnel.d.ts +16 -6
- package/dist/tunnel.js +53 -12
- package/package.json +5 -4
- package/dist/channels/slack/bot-auth.d.ts +0 -15
- package/dist/channels/slack/bot-auth.js +0 -135
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session persistence for the `AgentSession` L0 — open-or-create a durable record by the Caller's
|
|
3
3
|
* opaque session id, on pi-coding-agent's `SessionManager` (the v3 jsonl every pi surface reads).
|
|
4
|
-
*
|
|
5
|
-
* Records written before this store existed (by the pi-agent-core `Session` the serving path used
|
|
6
|
-
* to run on) are the same v3 jsonl and are continued in place — see `legacySessionId`. That is a
|
|
7
|
-
* READ path for existing conversations, not a second engine.
|
|
8
4
|
*/
|
|
9
5
|
import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
10
6
|
import { basename, dirname, join, resolve } from "node:path";
|
|
@@ -27,10 +23,10 @@ import { copyBranchForInheritance, copyBranchInto, inheritanceCut, } from "./ses
|
|
|
27
23
|
* produce one output. `_` escapes itself for the same reason. A trailing `.` or `-` is legal
|
|
28
24
|
* mid-name but not at the end, so it escapes too.
|
|
29
25
|
*
|
|
30
|
-
* Injective within this encoding — which is only sufficient because
|
|
31
|
-
* directory.
|
|
32
|
-
* called `s42`
|
|
33
|
-
* names ambiguous no matter how
|
|
26
|
+
* Injective within this encoding — which is only sufficient because this store's records live in
|
|
27
|
+
* their own directory. A name this store did not write draws from the same character set and can
|
|
28
|
+
* still decode: a file called `s42` is also this encoding of `42`, so one directory would make some
|
|
29
|
+
* names ambiguous no matter how this side spells them.
|
|
34
30
|
*
|
|
35
31
|
* Readability is deliberate: `-1001234567890` becomes `s-1001234567890`, so an operator can still
|
|
36
32
|
* tell which room a file belongs to.
|
|
@@ -81,24 +77,19 @@ export function callerSessionId(recordId) {
|
|
|
81
77
|
* each header and would make a renamed agent directory look like an empty store (see
|
|
82
78
|
* {@link recordFiles}).
|
|
83
79
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* A PRE-EXISTING record is continued in place: looked up by the older spelling, which is injective
|
|
90
|
-
* on its own terms, and appended to where it lies. Both spellings are the same v3 jsonl, so a
|
|
91
|
-
* conversation started before this store keeps going rather than restarting empty. Nothing on disk
|
|
92
|
-
* is rewritten.
|
|
80
|
+
* Records live in a subdirectory of their own, because a name this store did not write can still
|
|
81
|
+
* decode to a Caller id: a file called `s42` beside them is also this encoding of `42`, so a shared
|
|
82
|
+
* directory would answer `42` with a record it does not own. Only this directory is scanned, which
|
|
83
|
+
* makes this encoding's own injectivity sufficient — a record written before this store existed
|
|
84
|
+
* lies outside it and is never read.
|
|
93
85
|
*
|
|
94
86
|
* SCOPE OF "open-or-create": idempotent against a store that is serialized per session, which is what
|
|
95
87
|
* the serving path provides — the single-writer lease is taken before any store call, so no two
|
|
96
88
|
* turns of one conversation reach this at once. What it does NOT do is arbitrate a FIRST open racing
|
|
97
89
|
* across processes: two instances that scan before either writes will both create, and the
|
|
98
|
-
* conversation forks into two records.
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* in-process one cannot arbitrate between them, and a file lock here would only look like it could.
|
|
90
|
+
* conversation forks into two records. A horizontally-scaled deployment that wants more owes a lease
|
|
91
|
+
* that spans its instances — an in-process one cannot arbitrate between them, and a file lock here
|
|
92
|
+
* would only look like it could.
|
|
102
93
|
*/
|
|
103
94
|
export function piSessionRecordStore(options) {
|
|
104
95
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -178,15 +169,11 @@ export function piSessionRecordStore(options) {
|
|
|
178
169
|
/** The unreadable records the last listing reported, so a polled endpoint states the condition
|
|
179
170
|
* once rather than once a second. */
|
|
180
171
|
let lastUnreadable = "";
|
|
181
|
-
/** WHERE a session's record is
|
|
182
|
-
*
|
|
183
|
-
* of the four. */
|
|
172
|
+
/** WHERE a session's record is — the one lookup every caller shares, so a fix to it (this store's
|
|
173
|
+
* own directory rather than pi's cwd-filtered listing) cannot reach three of the four. */
|
|
184
174
|
const locate = (sessionId) => {
|
|
185
175
|
const mine = recordFiles(own).find((f) => f.id === piSessionId(sessionId));
|
|
186
|
-
|
|
187
|
-
return { path: mine.path, dir: own };
|
|
188
|
-
const legacy = recordFiles(root).find((f) => f.id === legacySessionId(sessionId));
|
|
189
|
-
return legacy ? { path: legacy.path, dir: root } : undefined;
|
|
176
|
+
return mine ? { path: mine.path, dir: own } : undefined;
|
|
190
177
|
};
|
|
191
178
|
/** Open an existing record, or undefined. A closure rather than a method call, so `fork` cannot be
|
|
192
179
|
* broken by a caller that spreads this object into another one. */
|
|
@@ -217,8 +204,8 @@ export function piSessionRecordStore(options) {
|
|
|
217
204
|
const rows = [];
|
|
218
205
|
const unreadable = [];
|
|
219
206
|
for (const file of files) {
|
|
220
|
-
// A
|
|
221
|
-
//
|
|
207
|
+
// A name this store did not write cannot be decoded back to a Caller id, and a row nobody
|
|
208
|
+
// can dial is worse than a row that is missing.
|
|
222
209
|
const session = callerSessionId(file.id);
|
|
223
210
|
if (!session)
|
|
224
211
|
continue;
|
|
@@ -544,7 +531,7 @@ function reconcileInterruptedToolCalls(record) {
|
|
|
544
531
|
}
|
|
545
532
|
return record;
|
|
546
533
|
}
|
|
547
|
-
/** Where
|
|
534
|
+
/** Where the records live, under the sessions directory the store is pointed at. */
|
|
548
535
|
const OWN_RECORDS_DIR = "agent-session";
|
|
549
536
|
/**
|
|
550
537
|
* Make a NEW record exist on disk before anyone can act on it.
|
|
@@ -655,12 +642,3 @@ export function piInMemorySessionRecordStore(options = {}) {
|
|
|
655
642
|
},
|
|
656
643
|
};
|
|
657
644
|
}
|
|
658
|
-
/** The spelling used before this store existed — read-only, so older records still resolve. */
|
|
659
|
-
function legacySessionId(sessionId) {
|
|
660
|
-
return sessionId.replace(/[^A-Za-z0-9._-]/g, (c) => {
|
|
661
|
-
const code = c.charCodeAt(0);
|
|
662
|
-
return code < 0x100
|
|
663
|
-
? `%${code.toString(16).toUpperCase().padStart(2, "0")}`
|
|
664
|
-
: `%u${code.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
665
|
-
});
|
|
666
|
-
}
|
|
@@ -67,9 +67,9 @@ export interface TurnContext {
|
|
|
67
67
|
cwd?: string;
|
|
68
68
|
/** Current conversation manager. Absent outside a FastAgent-managed agent turn. */
|
|
69
69
|
sessionManager?: ReadonlySessionManager;
|
|
70
|
-
/** Tool activation for the current turn. Two producers, one consumer surface:
|
|
71
|
-
* served session;
|
|
72
|
-
* semantics). Absent only outside any turn (a bare `fastagent tool` run). */
|
|
70
|
+
/** Tool activation for the current turn. Two producers, one consumer surface: agent-session-factory.ts
|
|
71
|
+
* bridges the served session; session-builder.ts bridges the resident one (chat emulates deferral —
|
|
72
|
+
* same loader, same semantics). Absent only outside any turn (a bare `fastagent tool` run). */
|
|
73
73
|
tools?: ToolActivation;
|
|
74
74
|
}
|
|
75
75
|
export declare const turnContext: AsyncLocalStorage<TurnContext>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { type ModuleLoadFailure } from "../../loader.ts";
|
|
4
5
|
import { type ReadonlySessionManager, type ToolActivation } from "./tool-context.ts";
|
|
@@ -34,14 +35,10 @@ export interface DefineToolOptions<I extends z.ZodType> {
|
|
|
34
35
|
executionMode?: "sequential" | "parallel";
|
|
35
36
|
execute: (input: z.infer<I>, ctx: ToolContext) => unknown | Promise<unknown>;
|
|
36
37
|
}
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
* Naming the wider type is what lets `defineTool` stay context-free for authors while both families
|
|
42
|
-
* live in one array; every helper that only inspects or reorders tools is typed on THIS.
|
|
43
|
-
*/
|
|
44
|
-
export type MountedTool = AgentHarnessTool<ExecutionToolContext>;
|
|
38
|
+
/** AgentTool with Pi's optional per-call context; absent for sessionless CLI execution. */
|
|
39
|
+
export type MountedTool = Omit<AgentTool, "execute"> & {
|
|
40
|
+
execute(...args: [...Parameters<AgentTool["execute"]>, context?: ExtensionContext]): ReturnType<AgentTool["execute"]>;
|
|
41
|
+
};
|
|
45
42
|
/** An AgentTool with fastagent's deferral marker — the type for raw tools handed to fastagent
|
|
46
43
|
* (`config.tools`, L1/L2 `tools`): plain `AgentTool` has no `deferred`, so an object literal with the
|
|
47
44
|
* marker would fail excess-property checking against upstream's type. `defineTool` produces it. */
|
package/dist/engines/pi/tool.js
CHANGED
|
@@ -40,6 +40,7 @@ export function defineTool(options) {
|
|
|
40
40
|
const { $schema: _drop, ...parameters } = z.toJSONSchema(options.input);
|
|
41
41
|
const tool = {
|
|
42
42
|
name: options.name ?? "",
|
|
43
|
+
label: options.name ?? "",
|
|
43
44
|
description: options.description,
|
|
44
45
|
parameters,
|
|
45
46
|
...(options.deferred ? { deferred: true } : {}),
|
package/dist/feishu.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
/** `@fastagent-sh/fastagent/feishu` — the canonical Feishu (open.feishu.cn) bot-channel surface. */
|
|
2
2
|
export { feishuChannel, feishuWebSocketChannel, defaultFeishuRoute, feishuEnvelope, type FeishuChannelOptions, type FeishuWebSocketChannelOptions, type FeishuMessageEvent, type FeishuMessage, type FeishuRoute, type FeishuFailure, } from "./channels/feishu/feishu.ts";
|
|
3
|
+
export { feishuTransport, type FeishuTransport } from "./channels/feishu/shared-api.ts";
|
package/dist/feishu.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
/** `@fastagent-sh/fastagent/feishu` — the canonical Feishu (open.feishu.cn) bot-channel surface. */
|
|
2
2
|
export { feishuChannel, feishuWebSocketChannel, defaultFeishuRoute, feishuEnvelope, } from "./channels/feishu/feishu.js";
|
|
3
|
+
export { feishuTransport } from "./channels/feishu/shared-api.js";
|
package/dist/lark.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
/** `@fastagent-sh/fastagent/lark` — the Lark-international compatibility surface over the canonical
|
|
2
2
|
* Feishu engine. Feishu tenants use `@fastagent-sh/fastagent/feishu`. */
|
|
3
3
|
export { larkChannel, larkWebSocketChannel, defaultLarkRoute, larkEnvelope, type LarkChannelOptions, type LarkWebSocketChannelOptions, type LarkMessageEvent, type LarkMessage, type LarkRoute, type LarkFailure, } from "./channels/lark/lark.ts";
|
|
4
|
+
export { larkTransport, type FeishuTransport as LarkTransport } from "./channels/feishu/shared-api.ts";
|
package/dist/lark.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
/** `@fastagent-sh/fastagent/lark` — the Lark-international compatibility surface over the canonical
|
|
2
2
|
* Feishu engine. Feishu tenants use `@fastagent-sh/fastagent/feishu`. */
|
|
3
3
|
export { larkChannel, larkWebSocketChannel, defaultLarkRoute, larkEnvelope, } from "./channels/lark/lark.js";
|
|
4
|
+
export { larkTransport } from "./channels/feishu/shared-api.js";
|
package/dist/loader.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ export interface ModuleLoadFailure {
|
|
|
63
63
|
/** The failure message (an import error carries {@link moduleLoadHint}). */
|
|
64
64
|
message: string;
|
|
65
65
|
}
|
|
66
|
+
/** A module the loader skipped, said once, the same way for tools, channels and schedules. */
|
|
67
|
+
export declare function reportModuleLoadFailures(failures: readonly ModuleLoadFailure[]): void;
|
|
66
68
|
/**
|
|
67
69
|
* Import every module the directory declares ({@link moduleInventory}). A file that fails to IMPORT
|
|
68
70
|
* is collected into `failures` (with {@link moduleLoadHint}) rather than thrown, so the caller can
|
package/dist/loader.js
CHANGED
|
@@ -90,6 +90,11 @@ export async function moduleInventory(subDir) {
|
|
|
90
90
|
}
|
|
91
91
|
return entries;
|
|
92
92
|
}
|
|
93
|
+
/** A module the loader skipped, said once, the same way for tools, channels and schedules. */
|
|
94
|
+
export function reportModuleLoadFailures(failures) {
|
|
95
|
+
for (const f of failures)
|
|
96
|
+
log.warn(`[fastagent] ${f.label} failed to load, skipping it — ${f.message}`);
|
|
97
|
+
}
|
|
93
98
|
/**
|
|
94
99
|
* Import every module the directory declares ({@link moduleInventory}). A file that fails to IMPORT
|
|
95
100
|
* is collected into `failures` (with {@link moduleLoadHint}) rather than thrown, so the caller can
|
package/dist/log.d.ts
CHANGED
|
@@ -8,22 +8,14 @@
|
|
|
8
8
|
* This is NOT the CLI's user-facing output (help text, command results): that is the program talking to
|
|
9
9
|
* its operator and stays on plain `console`. Everything here is operational logging to stderr.
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
debug(msg: string): void;
|
|
14
|
-
info(msg: string): void;
|
|
15
|
-
warn(msg: string): void;
|
|
16
|
-
error(msg: string): void;
|
|
17
|
-
}
|
|
18
|
-
/** An embedded logger over an explicit sink — used in tests to assert level gating without the singleton. */
|
|
19
|
-
export declare function createLogger(opts: {
|
|
20
|
-
level: LogLevel;
|
|
21
|
-
sink?: (line: string) => void;
|
|
22
|
-
}): Logger;
|
|
23
|
-
/** Set the posture default. A valid `FASTAGENT_LOG_LEVEL` override, if present, wins and is not changed. */
|
|
11
|
+
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
12
|
+
/** Set the posture default. A valid `FASTAGENT_LOG_LEVEL` wins over it, whenever a line is emitted. */
|
|
24
13
|
export declare function setLogLevel(level: LogLevel): void;
|
|
25
14
|
/** The process logger. Runtime code imports this and calls `log.info(...)` etc. */
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
export declare const log: {
|
|
16
|
+
debug: (msg: string) => void;
|
|
17
|
+
info: (msg: string) => void;
|
|
18
|
+
warn: (msg: string) => void;
|
|
19
|
+
error: (msg: string) => void;
|
|
20
|
+
};
|
|
21
|
+
export {};
|
package/dist/log.js
CHANGED
|
@@ -9,47 +9,42 @@
|
|
|
9
9
|
* its operator and stays on plain `console`. Everything here is operational logging to stderr.
|
|
10
10
|
*/
|
|
11
11
|
const ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
12
|
-
const isLevel = (s) => s
|
|
12
|
+
const isLevel = (s) => Object.hasOwn(ORDER, s);
|
|
13
13
|
const format = (level, msg) => `${level.toUpperCase().padEnd(5)} ${msg}`;
|
|
14
|
-
/** An embedded logger over an explicit sink — used in tests to assert level gating without the singleton. */
|
|
15
|
-
export function createLogger(opts) {
|
|
16
|
-
const sink = opts.sink ?? ((line) => console.error(line));
|
|
17
|
-
const make = (level) => (msg) => {
|
|
18
|
-
if (ORDER[level] >= ORDER[opts.level])
|
|
19
|
-
sink(format(level, msg));
|
|
20
|
-
};
|
|
21
|
-
return { debug: make("debug"), info: make("info"), warn: make("warn"), error: make("error") };
|
|
22
|
-
}
|
|
23
14
|
/**
|
|
24
|
-
* `FASTAGENT_LOG_LEVEL`
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
15
|
+
* `FASTAGENT_LOG_LEVEL` resolved PER EMIT, not at import: this module is imported transitively by every
|
|
16
|
+
* command module, so an import-time read runs before any command reaches `loadDotEnv` and could never
|
|
17
|
+
* see the agent's `.secrets/.env` — a key that file could not carry at all.
|
|
18
|
+
*
|
|
19
|
+
* Three states: a valid value wins over the posture; an invalid value warns when it differs from the
|
|
20
|
+
* last warned value and is treated as absent, so a typo (meant to make logs louder) can never silently pin the
|
|
21
|
+
* level nor kill the posture default; absent leaves the posture. Empty is absent, matching
|
|
22
|
+
* `resolveOverridePath` — `KEY=` is how a `.env` parks a key it does not want set, not a typo to warn
|
|
23
|
+
* about. The warning is raw `console.error` because it is the logger reporting on its own gate, but
|
|
24
|
+
* reuses `format` for the same shape.
|
|
28
25
|
*/
|
|
29
|
-
|
|
26
|
+
let posture = "info";
|
|
27
|
+
let warnedFor;
|
|
28
|
+
function effectiveLevel() {
|
|
30
29
|
const raw = process.env.FASTAGENT_LOG_LEVEL;
|
|
31
|
-
if (raw
|
|
32
|
-
return
|
|
30
|
+
if (!raw)
|
|
31
|
+
return posture;
|
|
33
32
|
const value = raw.toLowerCase();
|
|
34
33
|
if (isLevel(value))
|
|
35
34
|
return value;
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
if (warnedFor !== raw) {
|
|
36
|
+
warnedFor = raw;
|
|
37
|
+
console.error(format("warn", `[fastagent] unknown FASTAGENT_LOG_LEVEL "${raw}"; using the posture default`));
|
|
38
|
+
}
|
|
39
|
+
return posture;
|
|
38
40
|
}
|
|
39
|
-
|
|
40
|
-
let currentLevel = override ?? "info";
|
|
41
|
-
/** Set the posture default. A valid `FASTAGENT_LOG_LEVEL` override, if present, wins and is not changed. */
|
|
41
|
+
/** Set the posture default. A valid `FASTAGENT_LOG_LEVEL` wins over it, whenever a line is emitted. */
|
|
42
42
|
export function setLogLevel(level) {
|
|
43
|
-
|
|
44
|
-
currentLevel = level;
|
|
43
|
+
posture = level;
|
|
45
44
|
}
|
|
46
45
|
const emit = (level) => (msg) => {
|
|
47
|
-
if (ORDER[level] >= ORDER[
|
|
46
|
+
if (ORDER[level] >= ORDER[effectiveLevel()])
|
|
48
47
|
console.error(format(level, msg));
|
|
49
48
|
};
|
|
49
|
+
/** The process logger. Runtime code imports this and calls `log.info(...)` etc. */
|
|
50
50
|
export const log = { debug: emit("debug"), info: emit("info"), warn: emit("warn"), error: emit("error") };
|
|
51
|
-
/** A module the loader skipped, said once, the same way for tools, channels and schedules. */
|
|
52
|
-
export function reportModuleLoadFailures(failures) {
|
|
53
|
-
for (const f of failures)
|
|
54
|
-
log.warn(`[fastagent] ${f.label} failed to load, skipping it — ${f.message}`);
|
|
55
|
-
}
|
package/dist/paths.d.ts
CHANGED
|
@@ -62,9 +62,9 @@ export declare function agentsAt(dir: string): string[];
|
|
|
62
62
|
* reader to a refusal whose own advice points back here.
|
|
63
63
|
*/
|
|
64
64
|
export declare function workspaceHint({ agentDir, workspace }: ResolvedPlacement, env?: NodeJS.ProcessEnv): string | undefined;
|
|
65
|
-
/** The agent dir for `dir`, or undefined when there is none — {@link findPlacement} without the pair
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
/** The agent dir for `dir`, or undefined when there is none — {@link findPlacement} without the pair
|
|
66
|
+
* or the throw. Login and standalone channel senders also operate outside configured agents, so
|
|
67
|
+
* they need absence as a value. Filesystem errors still propagate. */
|
|
68
68
|
export declare function findAgentDir(dir: string): string | undefined;
|
|
69
69
|
/**
|
|
70
70
|
* The agent whose DEFINITION contains `dir` — scaffolding there would make the new agent part of the
|
|
@@ -104,6 +104,10 @@ export declare function displayPath(cwd: string, dir: string): string | undefine
|
|
|
104
104
|
/** Does a path exist? Plain fs, no placement in it — it lives here because `paths.ts` is where the
|
|
105
105
|
* neutral path helpers are, and the scaffolder is not a utility home for the CLI and deploy. */
|
|
106
106
|
export declare function exists(p: string): Promise<boolean>;
|
|
107
|
+
/** The file's text, or undefined when there is no file. ONLY absence reads as absence: a file that
|
|
108
|
+
* exists but cannot be read (EACCES, a directory in its place) throws, because a decision made on
|
|
109
|
+
* "not there" — regenerate it, skip its gate — is the wrong one for a file that is there. */
|
|
110
|
+
export declare function readTextIfExists(p: string): Promise<string | undefined>;
|
|
107
111
|
/**
|
|
108
112
|
* Resolve a user-supplied path override (a CLI flag or an env var) to an absolute path, expanding a
|
|
109
113
|
* leading `~`/`~/` to the home dir FIRST. Path-valued config from `.env` (or any non-shell source)
|
package/dist/paths.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* modules depend on it for something the engine has no say in.
|
|
17
17
|
*/
|
|
18
18
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
19
|
-
import { access, chmod, mkdir, realpath } from "node:fs/promises";
|
|
19
|
+
import { access, chmod, mkdir, readFile, realpath } from "node:fs/promises";
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
21
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
22
22
|
/**
|
|
@@ -159,9 +159,9 @@ export function workspaceHint({ agentDir, workspace }, env = process.env) {
|
|
|
159
159
|
return undefined;
|
|
160
160
|
return `${parent} looks like a project — point fastagent at it (\`..\`) to have the agent work ON it`;
|
|
161
161
|
}
|
|
162
|
-
/** The agent dir for `dir`, or undefined when there is none — {@link findPlacement} without the pair
|
|
163
|
-
*
|
|
164
|
-
*
|
|
162
|
+
/** The agent dir for `dir`, or undefined when there is none — {@link findPlacement} without the pair
|
|
163
|
+
* or the throw. Login and standalone channel senders also operate outside configured agents, so
|
|
164
|
+
* they need absence as a value. Filesystem errors still propagate. */
|
|
165
165
|
export function findAgentDir(dir) {
|
|
166
166
|
return findPlacement(dir)?.agentDir;
|
|
167
167
|
}
|
|
@@ -267,6 +267,19 @@ export function displayPath(cwd, dir) {
|
|
|
267
267
|
export async function exists(p) {
|
|
268
268
|
return access(p).then(() => true, () => false);
|
|
269
269
|
}
|
|
270
|
+
/** The file's text, or undefined when there is no file. ONLY absence reads as absence: a file that
|
|
271
|
+
* exists but cannot be read (EACCES, a directory in its place) throws, because a decision made on
|
|
272
|
+
* "not there" — regenerate it, skip its gate — is the wrong one for a file that is there. */
|
|
273
|
+
export async function readTextIfExists(p) {
|
|
274
|
+
try {
|
|
275
|
+
return await readFile(p, "utf8");
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (error.code === "ENOENT")
|
|
279
|
+
return undefined;
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
270
283
|
/**
|
|
271
284
|
* Resolve a user-supplied path override (a CLI flag or an env var) to an absolute path, expanding a
|
|
272
285
|
* leading `~`/`~/` to the home dir FIRST. Path-valued config from `.env` (or any non-shell source)
|
|
@@ -57,8 +57,13 @@ export declare function channelExists(dir: string, kind: ChannelKind): Promise<b
|
|
|
57
57
|
*/
|
|
58
58
|
export declare function scaffoldChannel(dir: string, kind: ChannelKind, options?: {
|
|
59
59
|
ingress?: FeishuSubscriptionMode;
|
|
60
|
-
groupBehavior?: GroupBehavior;
|
|
61
60
|
}): Promise<string>;
|
|
61
|
+
/**
|
|
62
|
+
* The bundle's companion tools (every `.ts` beside `channel.ts` → `tools/<name>`). Unlike the channel
|
|
63
|
+
* file they are the package's, not authored glue, so they are written on EVERY add: re-running
|
|
64
|
+
* `add <kind>` is how an upgraded tool reaches an existing agent.
|
|
65
|
+
*/
|
|
66
|
+
export declare function scaffoldCompanionTools(dir: string, kind: ChannelKind): Promise<string[]>;
|
|
62
67
|
/**
|
|
63
68
|
* Verify the AGENT DIR is ready to host a channel: an ESM package.json that declares
|
|
64
69
|
* `@fastagent-sh/fastagent` (the channel file imports it). `add` checks and guides, never bootstraps — that
|
|
@@ -44,28 +44,12 @@ const CHANNEL_SCAFFOLDS = {
|
|
|
44
44
|
},
|
|
45
45
|
slack: {
|
|
46
46
|
env: [
|
|
47
|
-
{ name: "SLACK_BOT_TOKEN", hint: "Slack app →
|
|
48
|
-
{
|
|
49
|
-
name: "SLACK_BOT_REFRESH_TOKEN",
|
|
50
|
-
hint: "Slack OAuth bot refresh token (required when token rotation is enabled)",
|
|
51
|
-
required: false,
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
name: "SLACK_BOT_TOKEN_EXPIRES_AT",
|
|
55
|
-
hint: "Slack rotating bot access-token expiry (epoch milliseconds)",
|
|
56
|
-
required: false,
|
|
57
|
-
},
|
|
58
|
-
{ name: "SLACK_CLIENT_ID", hint: "Slack app OAuth client ID (for bot-token rotation)", required: false },
|
|
59
|
-
{
|
|
60
|
-
name: "SLACK_CLIENT_SECRET",
|
|
61
|
-
hint: "Slack app OAuth client secret (for bot-token rotation)",
|
|
62
|
-
required: false,
|
|
63
|
-
},
|
|
47
|
+
{ name: "SLACK_BOT_TOKEN", hint: "Slack app → OAuth & Permissions → Bot User OAuth Token", required: true },
|
|
64
48
|
{ name: "SLACK_SIGNING_SECRET", hint: "Slack app → Basic Information → App Credentials", required: true },
|
|
65
49
|
],
|
|
66
50
|
steps: [
|
|
67
51
|
"Slack Bot Token Scopes: app_mentions:read, assistant:write, chat:write, im:history, files:read, files:write, channels:history, groups:history, mpim:history",
|
|
68
|
-
"enable Agents (agent_view) and token rotation; subscribe app_home_opened, app_context_changed, app_mention, message.im, message.channels, message.groups, message.mpim; set Request URL to <public-url>/slack",
|
|
52
|
+
"enable Agents (agent_view) and leave token rotation OFF (it cannot be turned off again); subscribe app_home_opened, app_context_changed, app_mention, message.im, message.channels, message.groups, message.mpim; set Request URL to <public-url>/slack",
|
|
69
53
|
"reinstall the app after changing scopes, then invite it to each channel it should read",
|
|
70
54
|
"the agent can send messages or files by calling the scaffolded {tools}/slack-send.ts tool",
|
|
71
55
|
],
|
|
@@ -172,7 +156,7 @@ export function channelSetup(kind, ingress = "webhook", groupBehavior) {
|
|
|
172
156
|
env: setup.env,
|
|
173
157
|
steps: [
|
|
174
158
|
"Slack Bot Token Scopes: app_mentions:read, assistant:write, chat:write, im:history, files:read, files:write (no channel/group/mpim history scopes)",
|
|
175
|
-
"enable Agents (agent_view) and token rotation; subscribe app_home_opened, app_context_changed, app_mention, and message.im; set Request URL to <public-url>/slack",
|
|
159
|
+
"enable Agents (agent_view) and leave token rotation OFF (it cannot be turned off again); subscribe app_home_opened, app_context_changed, app_mention, and message.im; set Request URL to <public-url>/slack",
|
|
176
160
|
"group behavior: mention-only — bare thread replies and unsummoned group context remain disabled",
|
|
177
161
|
...setup.steps.slice(2),
|
|
178
162
|
],
|
|
@@ -340,55 +324,54 @@ export async function scaffoldChannel(dir, kind, options = {}) {
|
|
|
340
324
|
throw new Error(`${file} already exists — edit it, or remove it to re-scaffold`);
|
|
341
325
|
}
|
|
342
326
|
await mkdir(channelsDir, { recursive: true });
|
|
343
|
-
|
|
344
|
-
|
|
327
|
+
let content = channelTemplate(kind, "channel.ts");
|
|
328
|
+
if ((kind === "feishu" || kind === "lark") && options.ingress === "websocket") {
|
|
329
|
+
const factory = `${kind}Channel`;
|
|
330
|
+
const wsFactory = `${kind}WebSocketChannel`;
|
|
331
|
+
let configured = content
|
|
332
|
+
.replace(`import { ${factory} }`, `import { ${wsFactory} }`)
|
|
333
|
+
.replace(`export default ${factory}({`, `export default ${wsFactory}({`);
|
|
334
|
+
if (configured === content)
|
|
335
|
+
throw new Error(`${kind} channel template has no factory anchors`);
|
|
336
|
+
const prefix = kind === "feishu" ? "FEISHU" : "LARK";
|
|
337
|
+
const exportAt = configured.indexOf("export default");
|
|
338
|
+
const importEnd = configured.indexOf("\n\n");
|
|
339
|
+
if (exportAt < 0 || importEnd < 0)
|
|
340
|
+
throw new Error(`${kind} channel template header anchors are missing`);
|
|
341
|
+
const brand = kind === "feishu" ? "Feishu" : "Lark";
|
|
342
|
+
configured =
|
|
343
|
+
`${configured.slice(0, importEnd)}\n\n` +
|
|
344
|
+
`// ${brand} WebSocket long connection: the process connects OUT to the platform, so no public URL,\n` +
|
|
345
|
+
`// Verification Token, Encrypt Key, or --tunnel is needed. In Events & Callbacks choose long\n` +
|
|
346
|
+
`// connection, subscribe im.message.receive_v1, then publish the app version. Keep one process\n` +
|
|
347
|
+
`// running in production: scale-to-zero/App Sleeping would disconnect ingress.\n` +
|
|
348
|
+
configured.slice(exportAt);
|
|
349
|
+
configured = configured
|
|
350
|
+
.split("\n")
|
|
351
|
+
.filter((line) => !line.includes(`verificationToken: process.env.${prefix}_VERIFICATION_TOKEN`) &&
|
|
352
|
+
!line.includes(`encryptKey: process.env.${prefix}_ENCRYPT_KEY`))
|
|
353
|
+
.join("\n");
|
|
354
|
+
content = configured;
|
|
355
|
+
}
|
|
356
|
+
await writeFile(file, content, { flag: "wx" });
|
|
357
|
+
return file;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* The bundle's companion tools (every `.ts` beside `channel.ts` → `tools/<name>`). Unlike the channel
|
|
361
|
+
* file they are the package's, not authored glue, so they are written on EVERY add: re-running
|
|
362
|
+
* `add <kind>` is how an upgraded tool reaches an existing agent.
|
|
363
|
+
*/
|
|
364
|
+
export async function scaffoldCompanionTools(dir, kind) {
|
|
365
|
+
const written = [];
|
|
345
366
|
for (const name of channelBundleFiles(kind)) {
|
|
346
|
-
|
|
347
|
-
if (name === "channel.ts") {
|
|
348
|
-
if ((kind === "feishu" || kind === "lark") && options.ingress === "websocket") {
|
|
349
|
-
const factory = `${kind}Channel`;
|
|
350
|
-
const wsFactory = `${kind}WebSocketChannel`;
|
|
351
|
-
let configured = content
|
|
352
|
-
.replace(`import { ${factory} }`, `import { ${wsFactory} }`)
|
|
353
|
-
.replace(`export default ${factory}({`, `export default ${wsFactory}({`);
|
|
354
|
-
if (configured === content)
|
|
355
|
-
throw new Error(`${kind} channel template has no factory anchors`);
|
|
356
|
-
const prefix = kind === "feishu" ? "FEISHU" : "LARK";
|
|
357
|
-
const exportAt = configured.indexOf("export default");
|
|
358
|
-
const importEnd = configured.indexOf("\n\n");
|
|
359
|
-
if (exportAt < 0 || importEnd < 0)
|
|
360
|
-
throw new Error(`${kind} channel template header anchors are missing`);
|
|
361
|
-
const brand = kind === "feishu" ? "Feishu" : "Lark";
|
|
362
|
-
configured =
|
|
363
|
-
`${configured.slice(0, importEnd)}\n\n` +
|
|
364
|
-
`// ${brand} WebSocket long connection: the process connects OUT to the platform, so no public URL,\n` +
|
|
365
|
-
`// Verification Token, Encrypt Key, or --tunnel is needed. In Events & Callbacks choose long\n` +
|
|
366
|
-
`// connection, subscribe im.message.receive_v1, then publish the app version. Keep one process\n` +
|
|
367
|
-
`// running in production: scale-to-zero/App Sleeping would disconnect ingress.\n` +
|
|
368
|
-
configured.slice(exportAt);
|
|
369
|
-
configured = configured
|
|
370
|
-
.split("\n")
|
|
371
|
-
.filter((line) => !line.includes(`verificationToken: process.env.${prefix}_VERIFICATION_TOKEN`) &&
|
|
372
|
-
!line.includes(`encryptKey: process.env.${prefix}_ENCRYPT_KEY`))
|
|
373
|
-
.join("\n");
|
|
374
|
-
content = configured;
|
|
375
|
-
}
|
|
376
|
-
if (kind === "slack" && options.groupBehavior === "mentions") {
|
|
377
|
-
const configured = content.replace('groupBehavior: "context"', 'groupBehavior: "mentions"');
|
|
378
|
-
if (configured === content)
|
|
379
|
-
throw new Error("slack channel template has no groupBehavior anchor");
|
|
380
|
-
content = configured;
|
|
381
|
-
}
|
|
382
|
-
await writeFile(file, content, { flag: "wx" });
|
|
367
|
+
if (name === "channel.ts")
|
|
383
368
|
continue;
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
await writeFile(toolFile, content, { flag: "wx" });
|
|
389
|
-
}
|
|
369
|
+
const file = join(dir, "tools", name);
|
|
370
|
+
await mkdir(dirname(file), { recursive: true });
|
|
371
|
+
await writeFile(file, channelTemplate(kind, name));
|
|
372
|
+
written.push(file);
|
|
390
373
|
}
|
|
391
|
-
return
|
|
374
|
+
return written;
|
|
392
375
|
}
|
|
393
376
|
/**
|
|
394
377
|
* Verify the AGENT DIR is ready to host a channel: an ESM package.json that declares
|
|
@@ -1,16 +1,5 @@
|
|
|
1
|
+
import { type WakeAlarm } from "../channels/agentcore-protocol.ts";
|
|
1
2
|
import { type Wakeup } from "./wakeups.ts";
|
|
2
|
-
/** The forwarder's reserved wake-alarm path — never forwarded to channel routes. */
|
|
3
|
-
export declare const WAKE_ALARM_PATH = "/__fastagent/wake-alarm";
|
|
4
|
-
/** One desired alarm: mirror of a pending wake-up (id names the EventBridge schedule; at = fireAt). */
|
|
5
|
-
export interface WakeAlarm {
|
|
6
|
-
id: string;
|
|
7
|
-
at: string;
|
|
8
|
-
}
|
|
9
|
-
/** The wire shape the sink POSTs to {@link WAKE_ALARM_PATH} (the forwarder validates `secret`). */
|
|
10
|
-
export interface WakeAlarmRequest {
|
|
11
|
-
secret: string;
|
|
12
|
-
alarms: WakeAlarm[];
|
|
13
|
-
}
|
|
14
3
|
/**
|
|
15
4
|
* Persist the forwarder URL the adapter saw in an envelope (write-if-changed — envelopes arrive on
|
|
16
5
|
* every turn, the file should not churn). Durable under <stateRoot>/schedule/ so a freshly booted
|
|
@@ -23,12 +23,11 @@
|
|
|
23
23
|
* wasted wake-up of the box, traded for never needing list/delete choreography.
|
|
24
24
|
*/
|
|
25
25
|
import { readFileSync } from "node:fs";
|
|
26
|
+
import { RESERVED_PATHS } from "../channels/agentcore-protocol.js";
|
|
26
27
|
import { beginWork } from "../channels/busy.js";
|
|
27
28
|
import { log } from "../log.js";
|
|
28
29
|
import { scheduleFile, writeScheduleFile } from "./state.js";
|
|
29
30
|
import { listWakeups } from "./wakeups.js";
|
|
30
|
-
/** The forwarder's reserved wake-alarm path — never forwarded to channel routes. */
|
|
31
|
-
export const WAKE_ALARM_PATH = "/__fastagent/wake-alarm";
|
|
32
31
|
const URL_FILE = "wake-alarm-url";
|
|
33
32
|
/**
|
|
34
33
|
* Persist the forwarder URL the adapter saw in an envelope (write-if-changed — envelopes arrive on
|
|
@@ -106,7 +105,7 @@ export function createWakeAlarmSink(options) {
|
|
|
106
105
|
}
|
|
107
106
|
const body = { secret, alarms };
|
|
108
107
|
try {
|
|
109
|
-
const res = await fetchImpl(`${url.replace(/\/$/, "")}${
|
|
108
|
+
const res = await fetchImpl(`${url.replace(/\/$/, "")}${RESERVED_PATHS.wakeAlarm}`, {
|
|
110
109
|
method: "POST",
|
|
111
110
|
headers: { "content-type": "application/json" },
|
|
112
111
|
body: JSON.stringify(body),
|