@fastagent-sh/fastagent 0.19.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 (212) hide show
  1. package/README.md +5 -1
  2. package/dist/atomic-write.d.ts +11 -7
  3. package/dist/atomic-write.js +16 -11
  4. package/dist/channels/agentcore-limits.d.ts +9 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-protocol.d.ts +112 -0
  7. package/dist/channels/agentcore-protocol.js +22 -0
  8. package/dist/channels/agentcore-service.d.ts +12 -15
  9. package/dist/channels/agentcore-service.js +15 -24
  10. package/dist/channels/agentcore-state.d.ts +5 -11
  11. package/dist/channels/agentcore-state.js +4 -1
  12. package/dist/channels/agentcore.d.ts +9 -83
  13. package/dist/channels/agentcore.js +101 -93
  14. package/dist/channels/control.d.ts +50 -12
  15. package/dist/channels/control.js +251 -157
  16. package/dist/channels/discover.d.ts +26 -5
  17. package/dist/channels/discover.js +13 -26
  18. package/dist/channels/feishu/context-buffer.d.ts +6 -0
  19. package/dist/channels/feishu/context-buffer.js +0 -38
  20. package/dist/channels/feishu/crypto.d.ts +0 -2
  21. package/dist/channels/feishu/crypto.js +3 -8
  22. package/dist/channels/feishu/feishu-api.js +7 -12
  23. package/dist/channels/feishu/feishu.js +86 -128
  24. package/dist/channels/feishu/parse.d.ts +4 -3
  25. package/dist/channels/feishu/parse.js +5 -4
  26. package/dist/channels/feishu/register-webhook.d.ts +5 -5
  27. package/dist/channels/feishu/register-webhook.js +43 -57
  28. package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
  29. package/dist/channels/feishu/setup-mode.d.ts +30 -0
  30. package/dist/channels/feishu/setup-mode.js +26 -0
  31. package/dist/channels/feishu/shared-api.d.ts +10 -0
  32. package/dist/channels/feishu/shared-api.js +38 -0
  33. package/dist/channels/http.d.ts +0 -8
  34. package/dist/channels/http.js +4 -56
  35. package/dist/channels/kit/attachment-path.d.ts +12 -0
  36. package/dist/channels/kit/attachment-path.js +43 -0
  37. package/dist/channels/kit/context-buffer.d.ts +9 -0
  38. package/dist/channels/kit/context-buffer.js +11 -0
  39. package/dist/channels/kit/signature.d.ts +12 -0
  40. package/dist/channels/kit/signature.js +17 -0
  41. package/dist/channels/kit/stop-command.js +3 -3
  42. package/dist/channels/kit/tasks.d.ts +1 -1
  43. package/dist/channels/kit/tasks.js +10 -4
  44. package/dist/channels/kit/turn-runner.d.ts +59 -0
  45. package/dist/channels/kit/turn-runner.js +84 -0
  46. package/dist/channels/kit/turn-store.d.ts +19 -3
  47. package/dist/channels/kit/turn-store.js +29 -2
  48. package/dist/channels/lark/scaffold/lark-send.ts +12 -67
  49. package/dist/channels/registration.d.ts +36 -1
  50. package/dist/channels/registration.js +57 -1
  51. package/dist/channels/secret.d.ts +1 -0
  52. package/dist/channels/secret.js +16 -0
  53. package/dist/channels/slack/config-api.d.ts +35 -3
  54. package/dist/channels/slack/config-api.js +51 -9
  55. package/dist/channels/slack/invoke-turn.d.ts +1 -1
  56. package/dist/channels/slack/invoke-turn.js +5 -1
  57. package/dist/channels/slack/manifest.js +5 -1
  58. package/dist/channels/slack/onboard.d.ts +5 -5
  59. package/dist/channels/slack/onboard.js +44 -23
  60. package/dist/channels/slack/onboarding-state.d.ts +7 -3
  61. package/dist/channels/slack/onboarding-state.js +13 -20
  62. package/dist/channels/slack/register-webhook.d.ts +3 -2
  63. package/dist/channels/slack/register-webhook.js +35 -15
  64. package/dist/channels/slack/scaffold/channel.ts +3 -10
  65. package/dist/channels/slack/scaffold/slack-send.ts +18 -141
  66. package/dist/channels/slack/shared-api.d.ts +10 -0
  67. package/dist/channels/slack/shared-api.js +34 -0
  68. package/dist/channels/slack/slack-api.d.ts +20 -2
  69. package/dist/channels/slack/slack-api.js +100 -73
  70. package/dist/channels/slack/slack.d.ts +0 -10
  71. package/dist/channels/slack/slack.js +68 -107
  72. package/dist/channels/sse.d.ts +4 -0
  73. package/dist/channels/sse.js +66 -0
  74. package/dist/channels/telegram/parse.d.ts +21 -1
  75. package/dist/channels/telegram/parse.js +65 -11
  76. package/dist/channels/telegram/register-webhook.d.ts +6 -9
  77. package/dist/channels/telegram/register-webhook.js +44 -42
  78. package/dist/channels/telegram/scaffold/channel.ts +7 -3
  79. package/dist/channels/telegram/telegram-api.js +5 -6
  80. package/dist/channels/telegram/telegram.d.ts +2 -2
  81. package/dist/channels/telegram/telegram.js +97 -204
  82. package/dist/channels/wait-health.js +7 -4
  83. package/dist/cli/add-feishu.js +3 -10
  84. package/dist/cli/add-slack.js +13 -25
  85. package/dist/cli/commands/add.d.ts +0 -1
  86. package/dist/cli/commands/add.js +22 -35
  87. package/dist/cli/commands/attach.d.ts +2 -4
  88. package/dist/cli/commands/attach.js +17 -15
  89. package/dist/cli/commands/chat.js +6 -12
  90. package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
  91. package/dist/cli/commands/deploy/agentcore.js +178 -0
  92. package/dist/cli/commands/deploy/docker.d.ts +2 -0
  93. package/dist/cli/commands/deploy/docker.js +119 -0
  94. package/dist/cli/commands/deploy/fly.d.ts +2 -0
  95. package/dist/cli/commands/deploy/fly.js +131 -0
  96. package/dist/cli/commands/deploy/railway.d.ts +2 -0
  97. package/dist/cli/commands/deploy/railway.js +71 -0
  98. package/dist/cli/commands/deploy/shared.d.ts +114 -0
  99. package/dist/cli/commands/deploy/shared.js +124 -0
  100. package/dist/cli/commands/deploy.d.ts +38 -25
  101. package/dist/cli/commands/deploy.js +101 -631
  102. package/dist/cli/commands/dev.js +20 -76
  103. package/dist/cli/commands/fire.js +6 -12
  104. package/dist/cli/commands/info.js +1 -1
  105. package/dist/cli/commands/init.js +1 -2
  106. package/dist/cli/commands/invoke.js +4 -11
  107. package/dist/cli/commands/schedule.js +1 -1
  108. package/dist/cli/commands/start.js +41 -80
  109. package/dist/cli/commands/tool.js +3 -8
  110. package/dist/cli/kernel.d.ts +0 -2
  111. package/dist/cli/kernel.js +0 -2
  112. package/dist/cli/program.js +7 -12
  113. package/dist/cli/serve.d.ts +42 -34
  114. package/dist/cli/serve.js +107 -28
  115. package/dist/cli/shared.d.ts +52 -27
  116. package/dist/cli/shared.js +69 -6
  117. package/dist/deploy/agentcore/forwarder.js +250 -0
  118. package/dist/deploy/agentcore/logs.d.ts +2 -2
  119. package/dist/deploy/agentcore/logs.js +2 -2
  120. package/dist/deploy/agentcore/plan.d.ts +34 -11
  121. package/dist/deploy/agentcore/plan.js +52 -259
  122. package/dist/deploy/agentcore/run.d.ts +9 -24
  123. package/dist/deploy/agentcore/run.js +35 -43
  124. package/dist/deploy/channel-ingress.d.ts +73 -0
  125. package/dist/deploy/channel-ingress.js +101 -0
  126. package/dist/deploy/docker/plan.d.ts +3 -7
  127. package/dist/deploy/docker/plan.js +4 -15
  128. package/dist/deploy/docker/run.d.ts +35 -4
  129. package/dist/deploy/docker/run.js +40 -8
  130. package/dist/deploy/fly/plan.d.ts +4 -5
  131. package/dist/deploy/fly/plan.js +14 -22
  132. package/dist/deploy/fly/run.d.ts +36 -23
  133. package/dist/deploy/fly/run.js +129 -83
  134. package/dist/deploy/hosts.d.ts +5 -0
  135. package/dist/deploy/hosts.js +4 -0
  136. package/dist/deploy/preflight.d.ts +6 -7
  137. package/dist/deploy/preflight.js +27 -23
  138. package/dist/deploy/railway/plan.d.ts +9 -5
  139. package/dist/deploy/railway/plan.js +18 -26
  140. package/dist/deploy/railway/run.d.ts +11 -10
  141. package/dist/deploy/railway/run.js +44 -43
  142. package/dist/deploy/runner.js +22 -2
  143. package/dist/deploy/secrets.d.ts +18 -9
  144. package/dist/deploy/secrets.js +63 -28
  145. package/dist/dev-supervisor.js +3 -4
  146. package/dist/engines/pi/agent-session-factory.d.ts +37 -17
  147. package/dist/engines/pi/agent-session-factory.js +111 -106
  148. package/dist/engines/pi/auth.js +43 -43
  149. package/dist/engines/pi/config.d.ts +2 -2
  150. package/dist/engines/pi/create.d.ts +22 -27
  151. package/dist/engines/pi/create.js +59 -74
  152. package/dist/engines/pi/definition.d.ts +1 -1
  153. package/dist/engines/pi/definition.js +11 -12
  154. package/dist/engines/pi/invoke-session.js +13 -30
  155. package/dist/engines/pi/login.js +32 -16
  156. package/dist/engines/pi/open.d.ts +3 -4
  157. package/dist/engines/pi/open.js +32 -42
  158. package/dist/engines/pi/retry-event.d.ts +6 -0
  159. package/dist/engines/pi/retry-event.js +15 -0
  160. package/dist/engines/pi/search-tools.js +1 -1
  161. package/dist/engines/pi/service.d.ts +1 -1
  162. package/dist/engines/pi/service.js +8 -0
  163. package/dist/engines/pi/session-builder.js +26 -140
  164. package/dist/engines/pi/session-control.d.ts +27 -22
  165. package/dist/engines/pi/session-control.js +551 -490
  166. package/dist/engines/pi/session-inheritance.d.ts +8 -22
  167. package/dist/engines/pi/session-inheritance.js +95 -76
  168. package/dist/engines/pi/session-markers.d.ts +48 -0
  169. package/dist/engines/pi/session-markers.js +59 -0
  170. package/dist/engines/pi/session-settings.d.ts +5 -5
  171. package/dist/engines/pi/session-settings.js +8 -5
  172. package/dist/engines/pi/session-store.d.ts +91 -26
  173. package/dist/engines/pi/session-store.js +413 -82
  174. package/dist/engines/pi/tool-context.d.ts +24 -11
  175. package/dist/engines/pi/tool-context.js +29 -4
  176. package/dist/engines/pi/tool.d.ts +6 -9
  177. package/dist/engines/pi/tool.js +3 -2
  178. package/dist/env.js +1 -2
  179. package/dist/feishu.d.ts +1 -0
  180. package/dist/feishu.js +1 -0
  181. package/dist/lark.d.ts +1 -0
  182. package/dist/lark.js +1 -0
  183. package/dist/loader.d.ts +51 -7
  184. package/dist/loader.js +84 -18
  185. package/dist/log.d.ts +9 -17
  186. package/dist/log.js +25 -30
  187. package/dist/paths.d.ts +26 -3
  188. package/dist/paths.js +43 -5
  189. package/dist/scaffold/add-channel.d.ts +6 -1
  190. package/dist/scaffold/add-channel.js +50 -67
  191. package/dist/scaffold/init.js +7 -2
  192. package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
  193. package/dist/schedule/discover.js +3 -15
  194. package/dist/schedule/wake-alarm.d.ts +14 -19
  195. package/dist/schedule/wake-alarm.js +89 -48
  196. package/dist/schedule/wakeups.d.ts +1 -1
  197. package/dist/schedule/wakeups.js +10 -7
  198. package/dist/service.d.ts +24 -24
  199. package/dist/service.js +33 -77
  200. package/dist/session-remote.d.ts +12 -7
  201. package/dist/session-remote.js +185 -118
  202. package/dist/session.d.ts +227 -93
  203. package/dist/session.js +61 -23
  204. package/dist/slack.d.ts +2 -0
  205. package/dist/slack.js +1 -0
  206. package/dist/telegram.d.ts +1 -1
  207. package/dist/telegram.js +1 -1
  208. package/dist/tunnel.d.ts +34 -9
  209. package/dist/tunnel.js +83 -50
  210. package/package.json +5 -4
  211. package/dist/channels/slack/bot-auth.d.ts +0 -15
  212. package/dist/channels/slack/bot-auth.js +0 -135
@@ -26,11 +26,36 @@ export function agentSessionManager(session, sessionId) {
26
26
  },
27
27
  };
28
28
  }
29
+ /**
30
+ * The activation bridge over a live pi session — the ONE implementation, for both consumers.
31
+ *
32
+ * Serving (`agent-session-factory.ts`) and chat (`session-builder.ts`) had a copy each, identical
33
+ * but for the persistence line; the neighbouring `definitionResourceLoaderOptions` exists because
34
+ * that exact duplication drifted once before. The difference is a PARAMETER now: `onActivated` is
35
+ * what a served session uses to record the delta that carries the discovery into its next turn,
36
+ * and chat has nowhere to put one (pi's SessionContext has no active-tool set).
37
+ *
38
+ * Bind it to the SESSION, never to a tool call: the next call has to see what this one activated.
39
+ */
40
+ export function sessionToolActivation(session, onActivated) {
41
+ return {
42
+ active: () => session.getActiveToolNames(),
43
+ registered: () => session.getAllTools().map((t) => ({ name: t.name, description: t.description ?? "" })),
44
+ activate(names) {
45
+ const current = session.getActiveToolNames();
46
+ const added = additiveActivation(session.getAllTools().map((t) => t.name), current, names);
47
+ if (added.length === 0)
48
+ return added;
49
+ session.setActiveToolsByName([...current, ...added]);
50
+ onActivated?.(added);
51
+ return added;
52
+ },
53
+ };
54
+ }
29
55
  export const turnContext = new AsyncLocalStorage();
30
- /** The additive-activation contract, in ONE place for both bridges (the served session,
31
- * chat.ts over pi's AgentSession): dedupe keep registered names only (pi's setters THROW on
32
- * unknown) exclude already-active → the names to actually add (empty = nothing to set). */
33
- export function additiveActivation(registered, current, names) {
56
+ /** dedupe keep registered names only (pi's setters THROW on unknown) → exclude already-active →
57
+ * the names to actually add (empty = nothing to set). */
58
+ function additiveActivation(registered, current, names) {
34
59
  const known = new Set(registered);
35
60
  const active = new Set(current);
36
61
  return [...new Set(names)].filter((name) => known.has(name) && !active.has(name));
@@ -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 } : {}),
@@ -62,9 +63,9 @@ export function defineTool(options) {
62
63
  const tools = store?.tools
63
64
  ? {
64
65
  ...store.tools,
65
- activate: async (names) => {
66
+ activate: (names) => {
66
67
  // biome-ignore lint/style/noNonNullAssertion: guarded by the ternary above
67
- const activated = await store.tools.activate(names);
68
+ const activated = store.tools.activate(names);
68
69
  added.push(...activated);
69
70
  return activated;
70
71
  },
package/dist/env.js CHANGED
@@ -1,6 +1,5 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readFileSync, existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { existsSync } from "node:fs";
4
3
  import { log } from "./log.js";
5
4
  import { SECRETS_DIRNAME, resolveSecretsDir } from "./paths.js";
6
5
  /**
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
@@ -1,5 +1,48 @@
1
- /** Whether `name` is an importable agent module (a discovery candidate, not a type declaration). */
2
- export declare function isModuleFile(name: string): boolean;
1
+ /** One module file a directory declares. `name` is what the domain knows it by; nothing is imported
2
+ * to produce this. */
3
+ export interface InventoryEntry {
4
+ /** Basename without extension — the authoritative name for tools/channels/schedules. */
5
+ name: string;
6
+ /** "tools/foo.ts"-style label for errors and collisions. */
7
+ label: string;
8
+ file: string;
9
+ }
10
+ /**
11
+ * WHAT A CODE-INPUT DIRECTORY DECLARES — the single answer to "which files here are modules",
12
+ * without importing any of them.
13
+ *
14
+ * It exists because four consumers need that answer and only one of them may import: the loader
15
+ * below, `fastagent info`'s channel listing, `--tunnel`'s webhook registration, and the deploy
16
+ * pre-flight's schedule probe. When they each read the directory themselves they disagreed — on
17
+ * what counts as a module file, on how to strip the extension, on which errno means "no such
18
+ * directory" — and a fix had to be applied in four places, which is why the fourth kept being
19
+ * missed. There is one reading now; what to DO with a failure stays with the caller, because that
20
+ * genuinely differs (the loader throws, the tunnel cannot).
21
+ *
22
+ * A missing directory is an empty inventory. Everything else throws, ENOTDIR included: `channels`
23
+ * and `schedules` are named directories, so the path existing as a FILE is a mistake in the agent
24
+ * rather than an agent without one (`service.test.ts` pins that for `schedules`). `paths.ts` folds
25
+ * ENOTDIR into its empty scan for the opposite reason — it asks which children HAPPEN to be agent
26
+ * dirs, where a file simply is not one. `not_found` is a non-Node runtime's ENOENT.
27
+ *
28
+ * SYMLINKS ARE SKIPPED, and that is a boundary rather than an oversight: `assertInsideAgentDir`
29
+ * guards the code-input DIRECTORY against escaping the definition, and nothing guards the entries
30
+ * inside it, so following a link would import from anywhere on the box past the very check meant to
31
+ * prevent it. Do not "fix" this by following them — report them, which this does.
32
+ *
33
+ * The skip is WARNED HERE, not handed back, because "this file is not loadable" holds for all four
34
+ * consumers and only one of them imports — a listing that reports the name it cannot load reads as
35
+ * "I never created it". What to do about a load FAILURE does differ per caller, so that travels as
36
+ * data. One warning per READ, so a command that both lists and loads the same directory (`deploy
37
+ * agentcore` does) says it twice — both readings are true, and remembering what was already said
38
+ * would mean a restart stops mentioning a skip that is still there.
39
+ *
40
+ * A skip is therefore a warning ONLY, unlike a {@link ModuleLoadFailure}: it is absent from `info
41
+ * --json` and `deploy` does not gate on it. That is the deliberate cost of one report for four
42
+ * consumers, three of which list names and have nowhere to put data. To give a machine consumer the
43
+ * skips, return them beside the entries — do not reconstruct them from stderr.
44
+ */
45
+ export declare function moduleInventory(subDir: string): Promise<InventoryEntry[]>;
3
46
  export interface DiscoveredModule {
4
47
  /** Basename without extension — the authoritative name for tools/channels. */
5
48
  name: string;
@@ -20,12 +63,13 @@ export interface ModuleLoadFailure {
20
63
  /** The failure message (an import error carries {@link moduleLoadHint}). */
21
64
  message: string;
22
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;
23
68
  /**
24
- * Import every module file in `subDir`, sorted by name. Missing dir returns none. A file that fails to
25
- * IMPORT is collected into `failures` (with {@link moduleLoadHint}) rather than thrown, so the caller can
26
- * report every bad file and apply domain policy; `loadTools`/`loadChannels` add validation failures the
27
- * same way. (A missing DIRECTORY still returns empty; an unreadable directory still throws
28
- * — that's not a per-file problem.)
69
+ * Import every module the directory declares ({@link moduleInventory}). A file that fails to IMPORT
70
+ * is collected into `failures` (with {@link moduleLoadHint}) rather than thrown, so the caller can
71
+ * report every bad file and apply domain policy; `loadTools`/`loadChannels` add validation failures
72
+ * the same way. Entries the inventory SKIPPED are already reported by it, for every consumer.
29
73
  */
30
74
  export declare function loadModuleDir(subDir: string): Promise<{
31
75
  modules: DiscoveredModule[];
package/dist/loader.js CHANGED
@@ -9,40 +9,106 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
9
9
  import { readdir } from "node:fs/promises";
10
10
  import { basename, extname, join } from "node:path";
11
11
  import { pathToFileURL } from "node:url";
12
+ import { log } from "./log.js";
12
13
  const MODULE_EXTS = new Set([".ts", ".js", ".mjs"]);
13
14
  /** Whether `name` is an importable agent module (a discovery candidate, not a type declaration). */
14
- export function isModuleFile(name) {
15
+ function isModuleFile(name) {
15
16
  return MODULE_EXTS.has(extname(name)) && !name.endsWith(".d.ts");
16
17
  }
18
+ /** The name a module is known by: its basename without the extension. */
19
+ function moduleName(fileName) {
20
+ return basename(fileName, extname(fileName));
21
+ }
17
22
  /**
18
- * Import every module file in `subDir`, sorted by name. Missing dir returns none. A file that fails to
19
- * IMPORT is collected into `failures` (with {@link moduleLoadHint}) rather than thrown, so the caller can
20
- * report every bad file and apply domain policy; `loadTools`/`loadChannels` add validation failures the
21
- * same way. (A missing DIRECTORY still returns empty; an unreadable directory still throws
22
- * that's not a per-file problem.)
23
+ * WHAT A CODE-INPUT DIRECTORY DECLARES the single answer to "which files here are modules",
24
+ * without importing any of them.
25
+ *
26
+ * It exists because four consumers need that answer and only one of them may import: the loader
27
+ * below, `fastagent info`'s channel listing, `--tunnel`'s webhook registration, and the deploy
28
+ * pre-flight's schedule probe. When they each read the directory themselves they disagreed — on
29
+ * what counts as a module file, on how to strip the extension, on which errno means "no such
30
+ * directory" — and a fix had to be applied in four places, which is why the fourth kept being
31
+ * missed. There is one reading now; what to DO with a failure stays with the caller, because that
32
+ * genuinely differs (the loader throws, the tunnel cannot).
33
+ *
34
+ * A missing directory is an empty inventory. Everything else throws, ENOTDIR included: `channels`
35
+ * and `schedules` are named directories, so the path existing as a FILE is a mistake in the agent
36
+ * rather than an agent without one (`service.test.ts` pins that for `schedules`). `paths.ts` folds
37
+ * ENOTDIR into its empty scan for the opposite reason — it asks which children HAPPEN to be agent
38
+ * dirs, where a file simply is not one. `not_found` is a non-Node runtime's ENOENT.
39
+ *
40
+ * SYMLINKS ARE SKIPPED, and that is a boundary rather than an oversight: `assertInsideAgentDir`
41
+ * guards the code-input DIRECTORY against escaping the definition, and nothing guards the entries
42
+ * inside it, so following a link would import from anywhere on the box past the very check meant to
43
+ * prevent it. Do not "fix" this by following them — report them, which this does.
44
+ *
45
+ * The skip is WARNED HERE, not handed back, because "this file is not loadable" holds for all four
46
+ * consumers and only one of them imports — a listing that reports the name it cannot load reads as
47
+ * "I never created it". What to do about a load FAILURE does differ per caller, so that travels as
48
+ * data. One warning per READ, so a command that both lists and loads the same directory (`deploy
49
+ * agentcore` does) says it twice — both readings are true, and remembering what was already said
50
+ * would mean a restart stops mentioning a skip that is still there.
51
+ *
52
+ * A skip is therefore a warning ONLY, unlike a {@link ModuleLoadFailure}: it is absent from `info
53
+ * --json` and `deploy` does not gate on it. That is the deliberate cost of one report for four
54
+ * consumers, three of which list names and have nowhere to put data. To give a machine consumer the
55
+ * skips, return them beside the entries — do not reconstruct them from stderr.
23
56
  */
24
- export async function loadModuleDir(subDir) {
25
- let entries;
57
+ export async function moduleInventory(subDir) {
58
+ let dirents;
26
59
  try {
27
- entries = await readdir(subDir, { withFileTypes: true });
60
+ dirents = await readdir(subDir, { withFileTypes: true });
28
61
  }
29
62
  catch (error) {
30
- const e = error;
31
- if (e.code === "ENOENT" || e.code === "not_found")
32
- return { modules: [], failures: [] };
63
+ const code = error.code;
64
+ if (code === "ENOENT" || code === "not_found")
65
+ return [];
33
66
  throw new Error(`cannot read ${subDir}: ${error.message}`);
34
67
  }
35
68
  const sub = basename(subDir);
69
+ const entries = [];
70
+ // Sorted by the NAME a consumer sees, so none of them re-sorts and none can disagree about order.
71
+ // Filename breaks a tie: `foo.js` and `foo.ts` both read as `foo`, and the domain loaders document
72
+ // that the FIRST wins — deciding that here keeps it from depending on readdir's order.
73
+ const byName = (a, b) => moduleName(a.name).localeCompare(moduleName(b.name)) || a.name.localeCompare(b.name);
74
+ for (const dirent of dirents.sort(byName)) {
75
+ if (!isModuleFile(dirent.name))
76
+ continue;
77
+ const label = `${sub}/${dirent.name}`;
78
+ if (dirent.isFile()) {
79
+ entries.push({ name: moduleName(dirent.name), label, file: join(subDir, dirent.name) });
80
+ }
81
+ else if (dirent.isSymbolicLink()) {
82
+ log.warn(`[fastagent] ${label} is a symlink — code inputs must be real files inside the agent dir — not loaded`);
83
+ }
84
+ else if (dirent.isDirectory()) {
85
+ log.warn(`[fastagent] ${label} is a directory, not a file — not loaded`);
86
+ }
87
+ else {
88
+ log.warn(`[fastagent] ${label} is not a regular file — not loaded`);
89
+ }
90
+ }
91
+ return entries;
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
+ }
98
+ /**
99
+ * Import every module the directory declares ({@link moduleInventory}). A file that fails to IMPORT
100
+ * is collected into `failures` (with {@link moduleLoadHint}) rather than thrown, so the caller can
101
+ * report every bad file and apply domain policy; `loadTools`/`loadChannels` add validation failures
102
+ * the same way. Entries the inventory SKIPPED are already reported by it, for every consumer.
103
+ */
104
+ export async function loadModuleDir(subDir) {
105
+ const entries = await moduleInventory(subDir);
36
106
  const modules = [];
37
107
  const failures = [];
38
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
39
- if (!entry.isFile() || !isModuleFile(entry.name))
40
- continue;
41
- const file = join(subDir, entry.name);
42
- const label = `${sub}/${entry.name}`;
108
+ for (const { name, label, file } of entries) {
43
109
  try {
44
110
  const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(file).href)));
45
- modules.push({ name: basename(entry.name, extname(entry.name)), label, file, mod });
111
+ modules.push({ name, label, file, mod });
46
112
  }
47
113
  catch (error) {
48
114
  failures.push({
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)
@@ -136,6 +140,25 @@ export declare function resolveStateRoot(dir: string, env?: NodeJS.ProcessEnv):
136
140
  * auth.json but cannot move the file it is read from (env.ts dotEnvPath).
137
141
  */
138
142
  export declare function resolveSecretsDir(dir: string, env?: NodeJS.ProcessEnv): string;
143
+ /** What a file under {@link resolveSecretsDir} is written with (auth.json, .env). */
144
+ export declare const SECRET_FILE_MODE = 384;
145
+ /**
146
+ * Create the secrets directory with the mode its contents require — and REPAIR it when it already
147
+ * exists, which is the case that matters.
148
+ *
149
+ * The DIRECTORY is the boundary that actually protects a credential: without its `x` bit nothing
150
+ * below it is reachable, whatever a file's own mode says. And it is decided ONCE, by whichever
151
+ * writer gets there first — `mkdir`'s `mode` is ignored for a directory that already exists, so a
152
+ * later, more careful caller silently inherits the first one's answer. Four callers create this
153
+ * directory (`init`, `add <channel>`, the credential store, the deploy seed) and the ordinary order
154
+ * is init → add → login, so the careful one is LAST: the rule has to live where all of them can
155
+ * reach it, and it has to chmod rather than trust the create.
156
+ *
157
+ * A chmod the caller never asked for owes them its reason: the raw `EPERM ... chmod '/shared/creds'`
158
+ * reads as a bug in whatever they WERE doing (storing a credential), not as fastagent tightening a
159
+ * directory they pointed it at, and says nothing about the way out.
160
+ */
161
+ export declare function ensureSecretsDir(dir: string): Promise<void>;
139
162
  /**
140
163
  * Guard that `<agentDir>/<name>` resolves INSIDE the agent dir — a symlink that escapes (or an
141
164
  * absolute target) is rejected, so discovery/scaffolding never reaches out of the definition directory.
package/dist/paths.js CHANGED
@@ -16,8 +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 } from "node:fs/promises";
20
- import { realpath } from "node:fs/promises";
19
+ import { access, chmod, mkdir, readFile, realpath } from "node:fs/promises";
21
20
  import { homedir } from "node:os";
22
21
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
23
22
  /**
@@ -160,9 +159,9 @@ export function workspaceHint({ agentDir, workspace }, env = process.env) {
160
159
  return undefined;
161
160
  return `${parent} looks like a project — point fastagent at it (\`..\`) to have the agent work ON it`;
162
161
  }
163
- /** The agent dir for `dir`, or undefined when there is none — {@link findPlacement} without the pair,
164
- * and without the throw. `login` is the caller: it is the one command allowed to run outside an agent,
165
- * 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. */
166
165
  export function findAgentDir(dir) {
167
166
  return findPlacement(dir)?.agentDir;
168
167
  }
@@ -268,6 +267,19 @@ export function displayPath(cwd, dir) {
268
267
  export async function exists(p) {
269
268
  return access(p).then(() => true, () => false);
270
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
+ }
271
283
  /**
272
284
  * Resolve a user-supplied path override (a CLI flag or an env var) to an absolute path, expanding a
273
285
  * leading `~`/`~/` to the home dir FIRST. Path-valued config from `.env` (or any non-shell source)
@@ -309,6 +321,32 @@ export function resolveStateRoot(dir, env = process.env) {
309
321
  export function resolveSecretsDir(dir, env = process.env) {
310
322
  return resolveOverridePath(env.FASTAGENT_SECRETS_DIR) ?? join(resolve(dir), SECRETS_DIRNAME);
311
323
  }
324
+ /** What a file under {@link resolveSecretsDir} is written with (auth.json, .env). */
325
+ export const SECRET_FILE_MODE = 0o600;
326
+ /** What the secrets directory itself is created with. */
327
+ const SECRETS_DIR_MODE = 0o700;
328
+ /**
329
+ * Create the secrets directory with the mode its contents require — and REPAIR it when it already
330
+ * exists, which is the case that matters.
331
+ *
332
+ * The DIRECTORY is the boundary that actually protects a credential: without its `x` bit nothing
333
+ * below it is reachable, whatever a file's own mode says. And it is decided ONCE, by whichever
334
+ * writer gets there first — `mkdir`'s `mode` is ignored for a directory that already exists, so a
335
+ * later, more careful caller silently inherits the first one's answer. Four callers create this
336
+ * directory (`init`, `add <channel>`, the credential store, the deploy seed) and the ordinary order
337
+ * is init → add → login, so the careful one is LAST: the rule has to live where all of them can
338
+ * reach it, and it has to chmod rather than trust the create.
339
+ *
340
+ * A chmod the caller never asked for owes them its reason: the raw `EPERM ... chmod '/shared/creds'`
341
+ * reads as a bug in whatever they WERE doing (storing a credential), not as fastagent tightening a
342
+ * directory they pointed it at, and says nothing about the way out.
343
+ */
344
+ export async function ensureSecretsDir(dir) {
345
+ await mkdir(dir, { recursive: true, mode: SECRETS_DIR_MODE });
346
+ await chmod(dir, SECRETS_DIR_MODE).catch((e) => {
347
+ throw new Error(`cannot secure secrets dir ${dir} (fastagent keeps it 0700): ${e.message} — point --auth-path/FASTAGENT_SECRETS_DIR at a directory this process owns`, { cause: e });
348
+ });
349
+ }
312
350
  /**
313
351
  * Guard that `<agentDir>/<name>` resolves INSIDE the agent dir — a symlink that escapes (or an
314
352
  * absolute target) is rejected, so discovery/scaffolding never reaches out of the definition directory.
@@ -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