@fastagent-sh/fastagent 0.25.0 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +3 -2
  2. package/dist/agent-api.d.ts +10 -2
  3. package/dist/agent-api.js +10 -2
  4. package/dist/authoring.d.ts +12 -8
  5. package/dist/authoring.js +14 -7
  6. package/dist/cli/add-feishu.js +1 -1
  7. package/dist/cli/add-slack.js +1 -1
  8. package/dist/cli/box-login.d.ts +3 -2
  9. package/dist/cli/commands/add.js +2 -1
  10. package/dist/cli/commands/deploy.js +1 -1
  11. package/dist/cli/commands/env.js +5 -4
  12. package/dist/cli/commands/init.js +3 -3
  13. package/dist/cli/commands/invoke.js +2 -1
  14. package/dist/cli/commands/login.js +6 -4
  15. package/dist/cli/login-relay.d.ts +4 -3
  16. package/dist/cli/login-relay.js +1 -1
  17. package/dist/cli/shared.js +5 -3
  18. package/dist/content/git.d.ts +1 -1
  19. package/dist/content/git.js +7 -4
  20. package/dist/content/resolve.d.ts +1 -1
  21. package/dist/content/resolve.js +2 -2
  22. package/dist/deploy/preflight.js +1 -1
  23. package/dist/dev-supervisor.js +1 -1
  24. package/dist/env.d.ts +0 -13
  25. package/dist/env.js +1 -44
  26. package/dist/environment/mise.d.ts +8 -7
  27. package/dist/environment/mise.js +24 -20
  28. package/dist/git.d.ts +2 -2
  29. package/dist/git.js +5 -5
  30. package/dist/harnesses/models/environment.d.ts +7 -2
  31. package/dist/harnesses/models/environment.js +16 -10
  32. package/dist/harnesses/models/host.d.ts +30 -0
  33. package/dist/harnesses/models/host.js +97 -0
  34. package/dist/harnesses/models/login.d.ts +38 -73
  35. package/dist/harnesses/models/login.js +56 -56
  36. package/dist/harnesses/models/models.d.ts +4 -3
  37. package/dist/harnesses/models/models.js +51 -12
  38. package/dist/harnesses/pi-durable/agent.js +4 -0
  39. package/dist/harnesses/pi-durable/ids.js +2 -0
  40. package/dist/harnesses/pi-durable/open.d.ts +7 -0
  41. package/dist/harnesses/pi-durable/open.js +4 -3
  42. package/dist/harnesses/pi-durable/parked.d.ts +18 -0
  43. package/dist/harnesses/pi-durable/parked.js +27 -0
  44. package/dist/harnesses/pi-durable/release.d.ts +3 -2
  45. package/dist/harnesses/pi-durable/serve.d.ts +11 -1
  46. package/dist/harnesses/pi-durable/serve.js +42 -12
  47. package/dist/harnesses/pi-durable/storage.js +4 -2
  48. package/dist/harnesses/pi-durable/wake.d.ts +8 -1
  49. package/dist/harnesses/pi-durable/wake.js +10 -1
  50. package/dist/http/contract.d.ts +3 -3
  51. package/dist/index.d.ts +5 -3
  52. package/dist/index.js +3 -0
  53. package/dist/scaffold/add-channel.js +1 -1
  54. package/dist/schedule/cron.d.ts +4 -2
  55. package/dist/schedule/cron.js +4 -2
  56. package/dist/schedule/eventbridge-cron.d.ts +7 -3
  57. package/dist/schedule/eventbridge-cron.js +9 -8
  58. package/dist/sign-in-api.d.ts +147 -0
  59. package/dist/sign-in-api.js +9 -0
  60. package/dist/tunnel.js +2 -1
  61. package/dist/value-file.d.ts +19 -0
  62. package/dist/value-file.js +61 -5
  63. package/package.json +16 -16
@@ -33,8 +33,10 @@ export async function openLockedStorage(path, locking, holders) {
33
33
  return await SqliteStorage.open(adapter);
34
34
  }
35
35
  catch (error) {
36
- // The connection only: the adapter's close checkpoints first, which a refused opener cannot do.
37
- database.close();
36
+ // The connection only: the adapter's close checkpoints first, which a refused opener cannot do. Already closed
37
+ // when the storage refused the database itself (a schema it does not know), and closing it again would hide why.
38
+ if (database.isOpen)
39
+ database.close();
38
40
  if (error.errcode !== SQLITE_BUSY)
39
41
  throw error;
40
42
  const who = holders === undefined ? "" : `: ${holders} is running`;
@@ -1,4 +1,11 @@
1
1
  import { type Extension } from "@earendil-works/pi-durable";
2
2
  export declare const WAKE_EXTENSION = "fastagent.wake";
3
3
  export declare const WAKE_TOOLS: string[];
4
- export declare function wakeExtension(stateRoot: string, now?: () => Date): Extension;
4
+ /**
5
+ * `ticking` says whether a clock fires the wake-ups kept in `stateRoot` now; a wake-up nothing would fire is refused.
6
+ * Without it, one always does: `dev`, `start` and AgentCore serve whatever they open.
7
+ */
8
+ export declare function wakeExtension(stateRoot: string, options?: {
9
+ now?: () => Date;
10
+ ticking?: () => boolean;
11
+ }): Extension;
@@ -12,7 +12,14 @@ export const WAKE_TOOLS = ["wake", "unwake"];
12
12
  const said = (text) => ({ output: [{ type: "text", text }] });
13
13
  const refused = (text) => ({ output: [{ type: "text", text }], isError: true });
14
14
  const NOT_A_SESSION = "wake-ups belong to a session, and this conversation is not one (a subagent's): nothing would wake it.";
15
- export function wakeExtension(stateRoot, now = () => new Date()) {
15
+ const NO_CLOCK = "nothing fires wake-ups for this agent now: the app that opened it does not serve it, so no clock runs. Do the " +
16
+ "work now, or say it cannot be scheduled.";
17
+ /**
18
+ * `ticking` says whether a clock fires the wake-ups kept in `stateRoot` now; a wake-up nothing would fire is refused.
19
+ * Without it, one always does: `dev`, `start` and AgentCore serve whatever they open.
20
+ */
21
+ export function wakeExtension(stateRoot, options = {}) {
22
+ const now = options.now ?? (() => new Date());
16
23
  return defineExtension({
17
24
  name: WAKE_EXTENSION,
18
25
  tools: [
@@ -37,6 +44,8 @@ export function wakeExtension(stateRoot, now = () => new Date()) {
37
44
  }),
38
45
  }),
39
46
  execute: async (args, api, context) => {
47
+ if (options.ticking?.() === false)
48
+ return refused(NO_CLOCK);
40
49
  if ((args.in === undefined) === (args.cron === undefined)) {
41
50
  return refused("pass exactly one of `in` (one-shot) or `cron` (recurring).");
42
51
  }
@@ -464,6 +464,9 @@ declare const AgentHttpApi_base: HttpApi.HttpApi<"fastagent", HttpApiGroup.HttpA
464
464
  readonly event: Schema.String;
465
465
  readonly data: Schema.String;
466
466
  }>, never, never>, Schema.Never, {
467
+ readonly type: "message";
468
+ readonly message: Message;
469
+ } | {
467
470
  readonly type: "entry";
468
471
  readonly entry: Entry;
469
472
  } | {
@@ -499,9 +502,6 @@ declare const AgentHttpApi_base: HttpApi.HttpApi<"fastagent", HttpApiGroup.HttpA
499
502
  readonly retryable: boolean;
500
503
  readonly details?: Schema.Json | undefined;
501
504
  };
502
- } | {
503
- readonly type: "message";
504
- readonly message: Message;
505
505
  } | {
506
506
  readonly type: string;
507
507
  }> | Schema.toCodecJson<typeof Message>, Schema.toCodecJson<Schema.decodeTo<Schema.declare<AgentError, AgentError>, Schema.Struct<{
package/dist/index.d.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * The package's one entry (SPEC.md §8), besides each first-party channel's: the Agent API, opening an agent
3
- * directory and serving it, and what an author writes.
3
+ * directory and serving it, what a host embedding agents needs beside them (creating one, model credentials and
4
+ * sign-in), and what an author writes.
4
5
  */
5
6
  export type { Agent, AgentErrorData, AgentInfo, CompactOptions, ContentPart, CreateSessionOptions, EntriesOptions, Entry, ErrorCode, Json, ListOptions, Message, MessageKey, MessageStatus, MessageStreamEvent, ModelRef, Page, SendRequest, Session, SessionChanges, SessionEvent, SessionSettings, SessionStatus, ThinkingLevel, Tokens, Usage, WaitOptions, } from "./agent-api.ts";
6
7
  export { AgentError, LIMITS, hasEnded } from "./agent-api.ts";
7
8
  export { openAgent, type LocalAgent, type OpenAgentOptions, type ServeAgentOptions, } from "./harnesses/pi-durable/serve.ts";
8
9
  export type { Service } from "./service.ts";
9
- /** What `openAgent`'s `credentials` takes: where provider credentials are kept, pi-ai's. */
10
- export type { CredentialStore } from "@earendil-works/pi-ai";
10
+ export { createAgent, type CreatedAgent } from "./authoring.ts";
11
+ export { SignInCancelled, type Credential, type CredentialStore, type ProviderAuth, type SignInEvent, type SignInInteraction, type SignInMethod, type SignInOption, type SignInPrompt, type SignInRequest, type SignInResult, } from "./sign-in-api.ts";
12
+ export { credentialFile, providerAuth, signIn, signInOptions } from "./harnesses/models/host.ts";
11
13
  export { defineAgent, defineSchedule, needs, type AgentConfig, type Needs, type SecretSpec, type SecretValues, } from "./harnesses/pi-durable/define.ts";
12
14
  export type { ScheduleConfig } from "./schedule/schedule.ts";
13
15
  export { defineChannel, type ChannelContext, type ChannelHandler, type ChannelModule, type ForkSource, type LongConnection, type LongConnectionChannelModule, type Routes, } from "./channel.ts";
package/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  export { AgentError, LIMITS, hasEnded } from "./agent-api.js";
2
2
  export { openAgent, } from "./harnesses/pi-durable/serve.js";
3
+ export { createAgent } from "./authoring.js";
4
+ export { SignInCancelled, } from "./sign-in-api.js";
5
+ export { credentialFile, providerAuth, signIn, signInOptions } from "./harnesses/models/host.js";
3
6
  export { defineAgent, defineSchedule, needs, } from "./harnesses/pi-durable/define.js";
4
7
  export { defineChannel, } from "./channel.js";
5
8
  // What a channel reads a request with, answers with, and compares its platform's secret with (constant time).
@@ -8,7 +8,7 @@ import { detectRuntime } from "../runtime.js";
8
8
  import { assertInsideAgentDir, exists } from "../paths.js";
9
9
  import { channelBundleFiles, channelTemplate } from "./templates.js";
10
10
  import { writeValues } from "../value-file.js";
11
- import { envExamplePath } from "../env.js";
11
+ import { envExamplePath } from "../value-file.js";
12
12
  import { FEISHU_AGENT_SCOPES } from "../channels/feishu/setup-mode.js";
13
13
  import { slackBotScopes } from "../channels/slack/manifest.js";
14
14
  const AGENT_SCOPE_LIST = FEISHU_AGENT_SCOPES.map((entry) => entry.request).join(", ");
@@ -18,7 +18,9 @@ export declare function cronError(cron: string, tz: string | undefined): string
18
18
  * A function of the expression alone, so a file's verdict never flips with the time it is read. Two consecutive
19
19
  * instants under 10 minutes apart are on one day or across one midnight, and the time-of-day fields repeat every day,
20
20
  * so two days of the time-of-day form (the day fields made `*`, in UTC) hold every such gap. That is conservative:
21
- * `0,55 0,23 * * 1` is refused for the 23:55 → 00:00 pair, though Tuesday never fires. A DST change only lengthens a
22
- * gap (croner fires a repeated hour once). A year field is refused, since what repeats every year does not need one.
21
+ * `0,55 0,23 * * 1` is refused for the 23:55 → 00:00 pair, though Tuesday never fires. A DST change can bring two
22
+ * instants closer once a year, which this does not refuse: an expression with several times runs both passes of the
23
+ * hour the autumn change repeats (`0,55 1 * * *` at 01:55 and, 5 minutes on, the second 01:00), and a schedule whose
24
+ * session is busy skips a fire anyway. A year field is refused, since what repeats every year does not need one.
23
25
  */
24
26
  export declare function recurringCronError(cron: string, tz: string | undefined): string | undefined;
@@ -45,8 +45,10 @@ const verdicts = new Map();
45
45
  * A function of the expression alone, so a file's verdict never flips with the time it is read. Two consecutive
46
46
  * instants under 10 minutes apart are on one day or across one midnight, and the time-of-day fields repeat every day,
47
47
  * so two days of the time-of-day form (the day fields made `*`, in UTC) hold every such gap. That is conservative:
48
- * `0,55 0,23 * * 1` is refused for the 23:55 → 00:00 pair, though Tuesday never fires. A DST change only lengthens a
49
- * gap (croner fires a repeated hour once). A year field is refused, since what repeats every year does not need one.
48
+ * `0,55 0,23 * * 1` is refused for the 23:55 → 00:00 pair, though Tuesday never fires. A DST change can bring two
49
+ * instants closer once a year, which this does not refuse: an expression with several times runs both passes of the
50
+ * hour the autumn change repeats (`0,55 1 * * *` at 01:55 and, 5 minutes on, the second 01:00), and a schedule whose
51
+ * session is busy skips a fire anyway. A year field is refused, since what repeats every year does not need one.
50
52
  */
51
53
  export function recurringCronError(cron, tz) {
52
54
  const key = `${cron}\u0000${tz ?? ""}`;
@@ -6,9 +6,13 @@
6
6
  */
7
7
  /**
8
8
  * Translate a 5-field cron into EventBridge Scheduler's `cron(m h dom mon dow *)`, or say why it can't be. The two
9
- * fire on the same instants in the same zone, with one exception at a DST change: a local time the spring change
10
- * removes (02:30 in most of the US on that day) is skipped by EventBridge and run an hour later by croner, so such a
11
- * schedule runs once more a year on a resident host. A time the autumn change repeats runs once in both.
9
+ * fire on the same instants in the same zone, with two exceptions at a DST change, both on a resident host (croner):
10
+ * a local time the spring change removes (02:30 in most of the US on that day) is skipped by EventBridge and run by
11
+ * croner at the first valid time after the gap, and a schedule with several minutes or hours (every 15 minutes) runs
12
+ * both passes of the hour the autumn change repeats. EventBridge's documentation says a schedule "runs only once" when
13
+ * the clock goes back, which is read here as once per time: once for each of the four times of an every-15-minutes
14
+ * schedule. That reading is not tested against a live schedule. A single time in the repeated hour runs once in both.
15
+ * `?` is `*`, as croner reads it and as EventBridge means it ("any").
12
16
  */
13
17
  export declare function toEventBridgeCron(cron: string): {
14
18
  expression: string;
@@ -56,23 +56,24 @@ function mapDowField(dow) {
56
56
  }
57
57
  /**
58
58
  * Translate a 5-field cron into EventBridge Scheduler's `cron(m h dom mon dow *)`, or say why it can't be. The two
59
- * fire on the same instants in the same zone, with one exception at a DST change: a local time the spring change
60
- * removes (02:30 in most of the US on that day) is skipped by EventBridge and run an hour later by croner, so such a
61
- * schedule runs once more a year on a resident host. A time the autumn change repeats runs once in both.
59
+ * fire on the same instants in the same zone, with two exceptions at a DST change, both on a resident host (croner):
60
+ * a local time the spring change removes (02:30 in most of the US on that day) is skipped by EventBridge and run by
61
+ * croner at the first valid time after the gap, and a schedule with several minutes or hours (every 15 minutes) runs
62
+ * both passes of the hour the autumn change repeats. EventBridge's documentation says a schedule "runs only once" when
63
+ * the clock goes back, which is read here as once per time: once for each of the four times of an every-15-minutes
64
+ * schedule. That reading is not tested against a live schedule. A single time in the repeated hour runs once in both.
65
+ * `?` is `*`, as croner reads it and as EventBridge means it ("any").
62
66
  */
63
67
  export function toEventBridgeCron(cron) {
64
68
  const fields = cron.trim().split(/\s+/);
65
69
  if (fields.length !== 5) {
66
70
  return { error: `EventBridge supports 5-field cron only (got ${fields.length} fields)` };
67
71
  }
68
- const [min, hour, dom, mon, dow] = fields;
72
+ // EventBridge takes `?` in the day fields only, and exactly one of them; croner reads it as `*` anywhere.
73
+ const [min, hour, dom, mon, dow] = fields.map((field) => (field === "?" ? "*" : field));
69
74
  if (/[L#]/i.test(dow) || /[L#]/i.test(dom)) {
70
75
  return { error: "L/# day forms don't translate to EventBridge numbering — set this schedule up manually" };
71
76
  }
72
- // `?` FIRST, and not as a synonym for `*`.
73
- if (dom === "?" || dow === "?") {
74
- return { expression: `cron(${min} ${hour} * ${mon} ? *)` };
75
- }
76
77
  if (dom !== "*" && dow !== "*") {
77
78
  return {
78
79
  error: "restricting BOTH day-of-month and day-of-week (cron OR semantics) is not expressible in EventBridge",
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Model credentials and provider sign-in as an embedding host sees them: FastAgent's own types, so a host keeps the
3
+ * credentials where it chooses and runs a sign-in in its own UI without knowing the harness. Imports nothing; the
4
+ * functions over these types are in harnesses/models/host.ts.
5
+ */
6
+ /** One provider's credential, kept by a {@link CredentialStore} whole, as it is given. */
7
+ export type Credential = {
8
+ type: "api_key";
9
+ key?: string;
10
+ env?: Record<string, string>;
11
+ } | {
12
+ type: "oauth";
13
+ access: string;
14
+ refresh: string;
15
+ expires: number;
16
+ [field: string]: unknown;
17
+ };
18
+ /**
19
+ * Where model credentials are kept, one per provider. `modify` is the only write, and it runs one at a time per
20
+ * provider (across processes, where the store can): an OAuth refresh happens inside it, and a rotated refresh token
21
+ * must never be used twice. A store it cannot read rejects; answering it as empty would let the next write replace
22
+ * every provider's credential.
23
+ */
24
+ export interface CredentialStore {
25
+ /** The stored credential, possibly expired; undefined when the provider has none. */
26
+ read(provider: string): Promise<Credential | undefined>;
27
+ /** Which providers have a credential, and of what kind; never the secrets. */
28
+ list(): Promise<readonly {
29
+ provider: string;
30
+ type: Credential["type"];
31
+ }[]>;
32
+ /** `change` returns the new credential, or undefined to leave it as it is. Resolves with what is stored after. */
33
+ modify(provider: string, change: (current: Credential | undefined) => Promise<Credential | undefined>): Promise<Credential | undefined>;
34
+ delete(provider: string): Promise<void>;
35
+ }
36
+ export type SignInMethod = Credential["type"];
37
+ /** One way to sign in to one provider, with what authenticates the provider now. */
38
+ export interface SignInOption {
39
+ provider: string;
40
+ /** The provider's name: "OpenAI", "OpenAI Codex (legacy)". */
41
+ name: string;
42
+ method: SignInMethod;
43
+ /** What signing in this way is called: the provider's own label ("Sign in with ChatGPT"), else a generic one. */
44
+ label: string;
45
+ /** An account sign-in backed by a provider subscription. */
46
+ subscription: boolean;
47
+ /** Its key comes from the environment only: there is no sign-in to run. */
48
+ external: boolean;
49
+ /**
50
+ * What authenticates the provider now: a stored credential (`OAuth`, `stored credential`), else the environment
51
+ * (a variable such as `OPENAI_API_KEY`). One credential is stored per provider, so signing in with the other method
52
+ * replaces a stored one.
53
+ */
54
+ status?: {
55
+ type: SignInMethod;
56
+ source: string;
57
+ };
58
+ /** What in the environment authenticates the provider with nothing stored: what still answers after a sign-out. */
59
+ environment?: string;
60
+ /**
61
+ * Why checking the environment for the provider failed: `environment` is then unknown, and so is `status` when
62
+ * nothing is stored.
63
+ */
64
+ problem?: string;
65
+ }
66
+ /** A question a sign-in asks. `manual_code`: the address the browser ended on after signing in, or the code its page showed. */
67
+ export type SignInPrompt = {
68
+ signal?: AbortSignal;
69
+ } & ({
70
+ type: "text" | "secret" | "manual_code";
71
+ message: string;
72
+ placeholder?: string;
73
+ } | {
74
+ type: "select";
75
+ message: string;
76
+ options: readonly {
77
+ id: string;
78
+ label: string;
79
+ description?: string;
80
+ }[];
81
+ });
82
+ /** What a sign-in tells the person: a page to open, a code to enter there, progress. */
83
+ export type SignInEvent = {
84
+ type: "info";
85
+ message: string;
86
+ links?: readonly {
87
+ url: string;
88
+ label?: string;
89
+ }[];
90
+ } | {
91
+ type: "progress";
92
+ message: string;
93
+ } | {
94
+ type: "auth_url";
95
+ url: string;
96
+ instructions?: string;
97
+ } | {
98
+ type: "device_code";
99
+ userCode: string;
100
+ verificationUri: string;
101
+ intervalSeconds?: number;
102
+ expiresInSeconds?: number;
103
+ };
104
+ /**
105
+ * How a sign-in reaches the person. `prompt` resolves with the answer (a select's option `id`); it rejects with
106
+ * {@link SignInCancelled} when the person backs out, which ends the sign-in with that error, and with any other error
107
+ * when asking failed, which fails the sign-in with it. A prompt's `signal` aborts when the sign-in no longer needs the
108
+ * answer, such as a `manual_code` question a browser callback answered first.
109
+ */
110
+ export interface SignInInteraction {
111
+ signal?: AbortSignal;
112
+ prompt(prompt: SignInPrompt): Promise<string>;
113
+ notify(event: SignInEvent): void;
114
+ }
115
+ export interface SignInRequest {
116
+ credentials: CredentialStore;
117
+ provider: string;
118
+ method: SignInMethod;
119
+ interaction: SignInInteraction;
120
+ /**
121
+ * A stable id of this installation, which Sign in with ChatGPT requires. `credentialFile` keeps one beside its
122
+ * file; a store of the host's own passes it here.
123
+ */
124
+ deviceId?: string;
125
+ }
126
+ export interface SignInResult {
127
+ provider: string;
128
+ method: SignInMethod;
129
+ /** An API key is checked with one request before it is stored: accepted, or not decidable (stored). */
130
+ verified: "ok" | "unknown" | "n/a";
131
+ }
132
+ /** What a request to a provider authenticates with. */
133
+ export interface ProviderAuth {
134
+ type: SignInMethod;
135
+ /** Where it comes from: `OAuth`, `stored credential`, an environment variable's name. */
136
+ source?: string;
137
+ /** The key or access token a request sends. */
138
+ apiKey?: string;
139
+ /** Headers a request sends, where the provider authenticates by them (an `Authorization` of its own). */
140
+ headers?: Record<string, string>;
141
+ /** Where the request goes, where the credential decides it (an enterprise endpoint). */
142
+ baseUrl?: string;
143
+ }
144
+ /** The person backed out of a sign-in, or its signal aborted: a decision, not a failure. Nothing was stored. */
145
+ export declare class SignInCancelled extends Error {
146
+ name: string;
147
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Model credentials and provider sign-in as an embedding host sees them: FastAgent's own types, so a host keeps the
3
+ * credentials where it chooses and runs a sign-in in its own UI without knowing the harness. Imports nothing; the
4
+ * functions over these types are in harnesses/models/host.ts.
5
+ */
6
+ /** The person backed out of a sign-in, or its signal aborted: a decision, not a failure. Nothing was stored. */
7
+ export class SignInCancelled extends Error {
8
+ name = "SignInCancelled";
9
+ }
package/dist/tunnel.js CHANGED
@@ -8,7 +8,8 @@ import { registerFeishuWebhook } from "./channels/feishu/register-webhook.js";
8
8
  import { registerSlackWebhook } from "./channels/slack/register-webhook.js";
9
9
  import { registerTelegramWebhook } from "./channels/telegram/register-webhook.js";
10
10
  import { pointChannelsAt, webhookKinds } from "./deploy/channel-ingress.js";
11
- import { dotEnvPath, loadDotEnv } from "./env.js";
11
+ import { loadDotEnv } from "./env.js";
12
+ import { dotEnvPath } from "./value-file.js";
12
13
  import { installProxyFetch } from "./proxy.js";
13
14
  import { AGENT_ENVIRONMENT_ENV, resolveStateRoot } from "./paths.js";
14
15
  import { log } from "./log.js";
@@ -1,3 +1,22 @@
1
+ /** Parse .env content into key → value (the dialect above; last occurrence of a key wins). */
2
+ export declare function parseEnvContent(content: string): Map<string, string>;
3
+ /**
4
+ * READ a value file without entering it: the values as data, never `process.env`. This is how code reasons about an
5
+ * environment OTHER than its own — `deploy` resolves configuration in the environment being deployed, which this
6
+ * file declares, and merging it into the running process would put the two environments in one bag. A missing file
7
+ * is no values, which is normal.
8
+ */
9
+ export declare function loadEnvValues(file: string): Map<string, string>;
10
+ /** The agent's `.env` file: `<resolved secrets dir>/.env`. */
11
+ export declare function dotEnvPath(agentDir: string, env?: NodeJS.ProcessEnv): string;
12
+ /** The committable template: `<agentDir>/.secrets/.env.example`. */
13
+ export declare function envExamplePath(agentDir: string): string;
14
+ /**
15
+ * What the agent's value file ({@link dotEnvPath}) adds to this process's environment, read and not entered: each
16
+ * variable the process does not hold, as entering the file would set it. An agent opened in a process that runs
17
+ * others too reads its own values through this; one that a command entered (`dev`, `start`) adds nothing here.
18
+ */
19
+ export declare function agentValues(agentDir: string): Record<string, string>;
1
20
  /** One variable to write: with a `value`, an active line; without one, a placeholder under its `hint`. */
2
21
  export interface ValueEntry {
3
22
  name: string;
@@ -1,13 +1,69 @@
1
1
  /**
2
- * Writing the selected environment's value file (`.secrets/.env`, or `.secrets/production/.env` for a deployment):
3
- * what a command generates (a channel's secret, the Agent API's key) goes in as an active `NAME=value` line, a value
4
- * already set stays, and the secrets directory fastagent made carries its own `.gitignore`.
2
+ * The selected environment's value file (`.secrets/.env`, or `.secrets/production/.env` for a deployment), read and
3
+ * written; entering it into `process.env` is env.ts's. Read as data: what it declares, and what it adds to an agent's
4
+ * environment. Written: what a command generates (a channel's secret, the Agent API's key) goes in as an active
5
+ * `NAME=value` line, a value already set stays, and the secrets directory fastagent made carries its own `.gitignore`.
5
6
  */
7
+ import { readFileSync } from "node:fs";
6
8
  import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
7
9
  import { dirname, join } from "node:path";
8
- import { dotEnvPath, parseEnvContent } from "./env.js";
9
- import { SECRETS_DIRNAME, SECRET_FILE_MODE, exists } from "./paths.js";
10
+ import { SECRETS_DIRNAME, SECRET_FILE_MODE, exists, resolveSecretsDir } from "./paths.js";
10
11
  import { baseTemplate } from "./scaffold/templates.js";
12
+ /** Parse .env content into key → value (the dialect above; last occurrence of a key wins). */
13
+ export function parseEnvContent(content) {
14
+ const parsed = new Map();
15
+ for (const raw of content.split("\n")) {
16
+ const line = raw.trim();
17
+ if (!line || line.startsWith("#"))
18
+ continue;
19
+ const eq = line.indexOf("=");
20
+ if (eq < 1)
21
+ continue; // no `=`, or an empty key
22
+ const key = line.slice(0, eq).trim();
23
+ let value = line.slice(eq + 1).trim();
24
+ const quote = value[0];
25
+ if ((quote === '"' || quote === "'") && value.at(-1) === quote)
26
+ value = value.slice(1, -1);
27
+ parsed.set(key, value); // in-file: last occurrence wins (Map overwrite)
28
+ }
29
+ return parsed;
30
+ }
31
+ /**
32
+ * READ a value file without entering it: the values as data, never `process.env`. This is how code reasons about an
33
+ * environment OTHER than its own — `deploy` resolves configuration in the environment being deployed, which this
34
+ * file declares, and merging it into the running process would put the two environments in one bag. A missing file
35
+ * is no values, which is normal.
36
+ */
37
+ export function loadEnvValues(file) {
38
+ try {
39
+ return parseEnvContent(readFileSync(file, "utf8"));
40
+ }
41
+ catch (error) {
42
+ if (error.code === "ENOENT")
43
+ return new Map();
44
+ throw error;
45
+ }
46
+ }
47
+ /** The agent's `.env` file: `<resolved secrets dir>/.env`. */
48
+ export function dotEnvPath(agentDir, env = process.env) {
49
+ return join(resolveSecretsDir(agentDir, env), ".env");
50
+ }
51
+ /** The committable template: `<agentDir>/.secrets/.env.example`. */
52
+ export function envExamplePath(agentDir) {
53
+ return join(agentDir, SECRETS_DIRNAME, ".env.example");
54
+ }
55
+ /**
56
+ * What the agent's value file ({@link dotEnvPath}) adds to this process's environment, read and not entered: each
57
+ * variable the process does not hold, as entering the file would set it. An agent opened in a process that runs
58
+ * others too reads its own values through this; one that a command entered (`dev`, `start`) adds nothing here.
59
+ */
60
+ export function agentValues(agentDir) {
61
+ const values = {};
62
+ for (const [key, value] of loadEnvValues(dotEnvPath(agentDir)))
63
+ if (!(key in process.env))
64
+ values[key] = value;
65
+ return values;
66
+ }
11
67
  /** What a command says when it wrote a secret into a secrets directory fastagent does not own and git may track. */
12
68
  export function unprotectedSecretsNote(dir) {
13
69
  return (`${dir} (FASTAGENT_SECRETS_DIR) now holds a secret and has no .gitignore — that directory is yours, so ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastagent-sh/fastagent",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
4
4
  "description": "Vibe first. Then FastAgent: turn a local agent directory into a live service in your app, on Telegram, Slack, Feishu, or behind any channel.",
5
5
  "keywords": [
6
6
  "agent",
@@ -89,33 +89,33 @@
89
89
  },
90
90
  "dependencies": {
91
91
  "@clack/prompts": "^1.6.0",
92
- "@earendil-works/chord": "1.3.0",
93
- "@earendil-works/pi-ai": "1.3.0",
94
- "@earendil-works/pi-durable": "1.3.0",
92
+ "@earendil-works/chord": "1.4.0",
93
+ "@earendil-works/pi-ai": "1.4.0",
94
+ "@earendil-works/pi-durable": "1.4.0",
95
95
  "@effect/platform-bun": "^4.0.7",
96
96
  "@effect/platform-node": "^4.0.7",
97
- "@larksuiteoapi/node-sdk": "^1.71.1",
97
+ "@larksuiteoapi/node-sdk": "^1.76.0",
98
98
  "@smithy/signature-v4": "^5.7.4",
99
- "chokidar": "^5.0.0",
99
+ "chokidar": "^6.0.0",
100
100
  "commander": "^15.0.0",
101
- "croner": "^10.0.1",
101
+ "croner": "^11.0.0",
102
102
  "effect": "^4.0.7",
103
103
  "giget": "^3.3.0",
104
- "ignore": "^7.0.12",
104
+ "ignore": "^7.0.14",
105
105
  "proper-lockfile": "^4.1.2",
106
106
  "undici": "^8.6.0",
107
- "yaml": "^2.9.0"
107
+ "yaml": "^2.9.1"
108
108
  },
109
109
  "devDependencies": {
110
- "@biomejs/biome": "^2.5.15",
111
- "@earendil-works/pi-coding-agent": "1.3.0",
112
- "@hono/node-server": "^2.1.4",
110
+ "@biomejs/biome": "^2.6.4",
111
+ "@earendil-works/pi-coding-agent": "1.4.0",
112
+ "@hono/node-server": "^2.1.5",
113
113
  "@types/express": "^5.0.6",
114
114
  "@types/proper-lockfile": "^4.1.4",
115
- "express": "^5.2.1",
116
- "fastify": "^5.12.5",
117
- "knip": "^6.41.0",
115
+ "express": "^5.3.0",
116
+ "fastify": "^5.12.7",
117
+ "knip": "^6.44.0",
118
118
  "typescript": "^7.0.2",
119
- "vitest": "^5.0.3"
119
+ "vitest": "^5.0.5"
120
120
  }
121
121
  }