@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.
Files changed (155) hide show
  1. package/README.md +5 -1
  2. package/dist/channels/agentcore-protocol.d.ts +112 -0
  3. package/dist/channels/agentcore-protocol.js +22 -0
  4. package/dist/channels/agentcore-service.d.ts +0 -4
  5. package/dist/channels/agentcore-service.js +1 -6
  6. package/dist/channels/agentcore-state.d.ts +5 -11
  7. package/dist/channels/agentcore-state.js +4 -1
  8. package/dist/channels/agentcore.d.ts +1 -67
  9. package/dist/channels/agentcore.js +90 -89
  10. package/dist/channels/control.d.ts +1 -1
  11. package/dist/channels/control.js +13 -62
  12. package/dist/channels/discover.d.ts +0 -1
  13. package/dist/channels/discover.js +1 -3
  14. package/dist/channels/feishu/context-buffer.d.ts +6 -0
  15. package/dist/channels/feishu/context-buffer.js +0 -38
  16. package/dist/channels/feishu/crypto.d.ts +0 -2
  17. package/dist/channels/feishu/crypto.js +3 -8
  18. package/dist/channels/feishu/feishu-api.js +2 -4
  19. package/dist/channels/feishu/feishu.js +56 -115
  20. package/dist/channels/feishu/parse.d.ts +4 -3
  21. package/dist/channels/feishu/parse.js +5 -4
  22. package/dist/channels/feishu/register-webhook.d.ts +5 -5
  23. package/dist/channels/feishu/register-webhook.js +43 -57
  24. package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
  25. package/dist/channels/feishu/setup-mode.d.ts +30 -0
  26. package/dist/channels/feishu/setup-mode.js +26 -0
  27. package/dist/channels/feishu/shared-api.d.ts +10 -0
  28. package/dist/channels/feishu/shared-api.js +38 -0
  29. package/dist/channels/http.d.ts +0 -8
  30. package/dist/channels/http.js +4 -56
  31. package/dist/channels/kit/turn-runner.d.ts +59 -0
  32. package/dist/channels/kit/turn-runner.js +84 -0
  33. package/dist/channels/lark/scaffold/lark-send.ts +12 -67
  34. package/dist/channels/registration.d.ts +36 -1
  35. package/dist/channels/registration.js +57 -1
  36. package/dist/channels/secret.d.ts +1 -0
  37. package/dist/channels/secret.js +16 -0
  38. package/dist/channels/slack/config-api.d.ts +35 -3
  39. package/dist/channels/slack/config-api.js +51 -9
  40. package/dist/channels/slack/manifest.js +5 -1
  41. package/dist/channels/slack/onboard.d.ts +5 -5
  42. package/dist/channels/slack/onboard.js +41 -20
  43. package/dist/channels/slack/register-webhook.d.ts +3 -2
  44. package/dist/channels/slack/register-webhook.js +34 -14
  45. package/dist/channels/slack/scaffold/channel.ts +3 -10
  46. package/dist/channels/slack/scaffold/slack-send.ts +18 -126
  47. package/dist/channels/slack/shared-api.d.ts +10 -0
  48. package/dist/channels/slack/shared-api.js +34 -0
  49. package/dist/channels/slack/slack-api.d.ts +20 -2
  50. package/dist/channels/slack/slack-api.js +81 -22
  51. package/dist/channels/slack/slack.d.ts +0 -10
  52. package/dist/channels/slack/slack.js +60 -99
  53. package/dist/channels/sse.d.ts +4 -0
  54. package/dist/channels/sse.js +66 -0
  55. package/dist/channels/telegram/register-webhook.d.ts +6 -9
  56. package/dist/channels/telegram/register-webhook.js +44 -42
  57. package/dist/channels/telegram/telegram.js +47 -142
  58. package/dist/channels/wait-health.js +7 -4
  59. package/dist/cli/add-feishu.js +3 -10
  60. package/dist/cli/add-slack.js +7 -20
  61. package/dist/cli/commands/add.d.ts +0 -1
  62. package/dist/cli/commands/add.js +22 -31
  63. package/dist/cli/commands/chat.js +6 -12
  64. package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
  65. package/dist/cli/commands/deploy/agentcore.js +178 -0
  66. package/dist/cli/commands/deploy/docker.d.ts +2 -0
  67. package/dist/cli/commands/deploy/docker.js +119 -0
  68. package/dist/cli/commands/deploy/fly.d.ts +2 -0
  69. package/dist/cli/commands/deploy/fly.js +131 -0
  70. package/dist/cli/commands/deploy/railway.d.ts +2 -0
  71. package/dist/cli/commands/deploy/railway.js +71 -0
  72. package/dist/cli/commands/deploy/shared.d.ts +114 -0
  73. package/dist/cli/commands/deploy/shared.js +124 -0
  74. package/dist/cli/commands/deploy.d.ts +17 -49
  75. package/dist/cli/commands/deploy.js +39 -618
  76. package/dist/cli/commands/dev.js +17 -46
  77. package/dist/cli/commands/fire.js +6 -12
  78. package/dist/cli/commands/info.js +2 -1
  79. package/dist/cli/commands/invoke.js +4 -11
  80. package/dist/cli/commands/schedule.js +1 -1
  81. package/dist/cli/commands/start.js +17 -46
  82. package/dist/cli/commands/tool.js +3 -8
  83. package/dist/cli/kernel.d.ts +0 -2
  84. package/dist/cli/kernel.js +0 -2
  85. package/dist/cli/program.js +7 -12
  86. package/dist/cli/serve.d.ts +36 -19
  87. package/dist/cli/serve.js +91 -7
  88. package/dist/cli/shared.d.ts +13 -18
  89. package/dist/cli/shared.js +20 -4
  90. package/dist/deploy/agentcore/forwarder.js +250 -0
  91. package/dist/deploy/agentcore/plan.d.ts +23 -2
  92. package/dist/deploy/agentcore/plan.js +29 -244
  93. package/dist/deploy/agentcore/run.d.ts +4 -20
  94. package/dist/deploy/agentcore/run.js +22 -7
  95. package/dist/deploy/channel-ingress.js +1 -1
  96. package/dist/deploy/docker/run.d.ts +17 -3
  97. package/dist/deploy/docker/run.js +30 -8
  98. package/dist/deploy/fly/plan.js +7 -0
  99. package/dist/deploy/fly/run.d.ts +27 -0
  100. package/dist/deploy/fly/run.js +102 -20
  101. package/dist/deploy/hosts.d.ts +5 -0
  102. package/dist/deploy/hosts.js +4 -0
  103. package/dist/deploy/preflight.js +3 -3
  104. package/dist/deploy/railway/plan.d.ts +5 -0
  105. package/dist/deploy/railway/plan.js +7 -0
  106. package/dist/deploy/railway/run.d.ts +2 -1
  107. package/dist/deploy/railway/run.js +5 -4
  108. package/dist/deploy/secrets.js +0 -17
  109. package/dist/engines/pi/agent-session-factory.d.ts +37 -17
  110. package/dist/engines/pi/agent-session-factory.js +109 -85
  111. package/dist/engines/pi/config.d.ts +1 -1
  112. package/dist/engines/pi/create.d.ts +22 -39
  113. package/dist/engines/pi/create.js +58 -73
  114. package/dist/engines/pi/definition.js +8 -8
  115. package/dist/engines/pi/invoke-session.js +13 -30
  116. package/dist/engines/pi/open.d.ts +2 -2
  117. package/dist/engines/pi/open.js +31 -41
  118. package/dist/engines/pi/retry-event.d.ts +6 -0
  119. package/dist/engines/pi/retry-event.js +15 -0
  120. package/dist/engines/pi/session-builder.js +26 -113
  121. package/dist/engines/pi/session-control.d.ts +10 -18
  122. package/dist/engines/pi/session-control.js +47 -81
  123. package/dist/engines/pi/session-settings.d.ts +1 -1
  124. package/dist/engines/pi/session-settings.js +1 -1
  125. package/dist/engines/pi/session-store.d.ts +12 -17
  126. package/dist/engines/pi/session-store.js +18 -40
  127. package/dist/engines/pi/tool-context.d.ts +3 -3
  128. package/dist/engines/pi/tool.d.ts +6 -9
  129. package/dist/engines/pi/tool.js +1 -0
  130. package/dist/feishu.d.ts +1 -0
  131. package/dist/feishu.js +1 -0
  132. package/dist/lark.d.ts +1 -0
  133. package/dist/lark.js +1 -0
  134. package/dist/loader.d.ts +2 -0
  135. package/dist/loader.js +5 -0
  136. package/dist/log.d.ts +9 -17
  137. package/dist/log.js +25 -30
  138. package/dist/paths.d.ts +7 -3
  139. package/dist/paths.js +17 -4
  140. package/dist/scaffold/add-channel.d.ts +6 -1
  141. package/dist/scaffold/add-channel.js +48 -65
  142. package/dist/schedule/wake-alarm.d.ts +1 -12
  143. package/dist/schedule/wake-alarm.js +2 -3
  144. package/dist/service.d.ts +23 -22
  145. package/dist/service.js +7 -73
  146. package/dist/session-remote.d.ts +5 -5
  147. package/dist/session-remote.js +25 -28
  148. package/dist/session.d.ts +1 -1
  149. package/dist/slack.d.ts +2 -0
  150. package/dist/slack.js +1 -0
  151. package/dist/tunnel.d.ts +16 -6
  152. package/dist/tunnel.js +53 -12
  153. package/package.json +5 -4
  154. package/dist/channels/slack/bot-auth.d.ts +0 -15
  155. 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 new records live in their own
31
- * directory. The older spelling draws names from the same character set (it stored a room literally
32
- * called `s42` as `s42`, which is also this encoding of `42`), so one directory would make some
33
- * names ambiguous no matter how either side spells them.
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
- * NEW records live in a subdirectory of their own, because the two engines cannot share a namespace:
85
- * both spell ids into `[A-Za-z0-9._-]`, so neither can claim a prefix the other cannot produce, and
86
- * a directory holding both would have names that belong to two conversations at once in whichever
87
- * direction it is read. Separate directories make each side's own injectivity sufficient.
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. sessions.ts states the same boundary for the same reason
99
- * ("the serving path serializes it with the single-writer lease before reaching any store"), and a
100
- * horizontally-scaled deployment that wants more owes a lease that spans its instances — an
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, under either spelling — the one lookup every caller shares, so a
182
- * fix to it (this store's own directory rather than pi's cwd-filtered listing) cannot reach three
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
- if (mine)
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 record this store did not write (the older spelling) cannot be decoded back to a Caller
221
- // id, and a row nobody can dial is worse than a row that is missing. It stays openable BY id.
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 this engine's own records live, under the sessions directory both engines are pointed at. */
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: invoke.ts bridges the
71
- * served session; chat.ts bridges the resident one (chat emulates deferral — same loader, same
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 { AgentHarnessTool, ExecutionToolContext, AgentTool } from "@earendil-works/pi-agent-core";
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
- * A tool as MOUNTED: what the engine actually runs. Wider than the authored {@link AgentTool} on
39
- * purpose a lower-level mounted tool may read an `ExecutionEnv` from the fifth `execute` parameter,
40
- * while coding-agent's cwd-bound tools ignore it and fastagent's authored tools take four arguments.
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. */
@@ -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
- export type LogLevel = "debug" | "info" | "warn" | "error";
12
- export interface Logger {
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
- import type { ModuleLoadFailure } from "./loader.ts";
27
- export declare const log: Logger;
28
- /** A module the loader skipped, said once, the same way for tools, channels and schedules. */
29
- export declare function reportModuleLoadFailures(failures: readonly ModuleLoadFailure[]): void;
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 in ORDER;
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` parsed to three states: a valid value locks the level (overrides posture); a
25
- * present-but-invalid value warns and is treated as absent, so a typo (meant to make logs louder) can
26
- * never silently pin the level to info nor kill the posture default; absent returns undefined. The
27
- * warning is raw — the singleton below is not built yet — but reuses `format` for the same shape.
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
- function parseEnvOverride() {
26
+ let posture = "info";
27
+ let warnedFor;
28
+ function effectiveLevel() {
30
29
  const raw = process.env.FASTAGENT_LOG_LEVEL;
31
- if (raw === undefined)
32
- return undefined;
30
+ if (!raw)
31
+ return posture;
33
32
  const value = raw.toLowerCase();
34
33
  if (isLevel(value))
35
34
  return value;
36
- console.error(format("warn", `[fastagent] unknown FASTAGENT_LOG_LEVEL "${raw}"; using the posture default`));
37
- return undefined;
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
- const override = parseEnvOverride();
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
- if (override === undefined)
44
- currentLevel = level;
43
+ posture = level;
45
44
  }
46
45
  const emit = (level) => (msg) => {
47
- if (ORDER[level] >= ORDER[currentLevel])
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
- * and without the throw. `login` is the caller: it is the one command allowed to run outside an agent,
67
- * so it needs the answer as a value rather than as a refusal. */
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
- * and without the throw. `login` is the caller: it is the one command allowed to run outside an agent,
164
- * so it needs the answer as a value rather than as a refusal. */
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 → rotating Bot User OAuth access token", required: true },
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
- // `channel.ts` is THE adapter ( channels/<kind>.ts); any other .ts in the bundle is a companion tool
344
- // ( tools/<name>, never clobbering an authored one).
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
- let content = channelTemplate(kind, name);
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
- const toolFile = join(dir, "tools", name);
386
- if (!(await exists(toolFile))) {
387
- await mkdir(join(dir, "tools"), { recursive: true });
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 file;
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(/\/$/, "")}${WAKE_ALARM_PATH}`, {
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),