@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.
- package/README.md +3 -2
- package/dist/agent-api.d.ts +10 -2
- package/dist/agent-api.js +10 -2
- package/dist/authoring.d.ts +12 -8
- package/dist/authoring.js +14 -7
- package/dist/cli/add-feishu.js +1 -1
- package/dist/cli/add-slack.js +1 -1
- package/dist/cli/box-login.d.ts +3 -2
- package/dist/cli/commands/add.js +2 -1
- package/dist/cli/commands/deploy.js +1 -1
- package/dist/cli/commands/env.js +5 -4
- package/dist/cli/commands/init.js +3 -3
- package/dist/cli/commands/invoke.js +2 -1
- package/dist/cli/commands/login.js +6 -4
- package/dist/cli/login-relay.d.ts +4 -3
- package/dist/cli/login-relay.js +1 -1
- package/dist/cli/shared.js +5 -3
- package/dist/content/git.d.ts +1 -1
- package/dist/content/git.js +7 -4
- package/dist/content/resolve.d.ts +1 -1
- package/dist/content/resolve.js +2 -2
- package/dist/deploy/preflight.js +1 -1
- package/dist/dev-supervisor.js +1 -1
- package/dist/env.d.ts +0 -13
- package/dist/env.js +1 -44
- package/dist/environment/mise.d.ts +8 -7
- package/dist/environment/mise.js +24 -20
- package/dist/git.d.ts +2 -2
- package/dist/git.js +5 -5
- package/dist/harnesses/models/environment.d.ts +7 -2
- package/dist/harnesses/models/environment.js +16 -10
- package/dist/harnesses/models/host.d.ts +30 -0
- package/dist/harnesses/models/host.js +97 -0
- package/dist/harnesses/models/login.d.ts +38 -73
- package/dist/harnesses/models/login.js +56 -56
- package/dist/harnesses/models/models.d.ts +4 -3
- package/dist/harnesses/models/models.js +51 -12
- package/dist/harnesses/pi-durable/agent.js +4 -0
- package/dist/harnesses/pi-durable/ids.js +2 -0
- package/dist/harnesses/pi-durable/open.d.ts +7 -0
- package/dist/harnesses/pi-durable/open.js +4 -3
- package/dist/harnesses/pi-durable/parked.d.ts +18 -0
- package/dist/harnesses/pi-durable/parked.js +27 -0
- package/dist/harnesses/pi-durable/release.d.ts +3 -2
- package/dist/harnesses/pi-durable/serve.d.ts +11 -1
- package/dist/harnesses/pi-durable/serve.js +42 -12
- package/dist/harnesses/pi-durable/storage.js +4 -2
- package/dist/harnesses/pi-durable/wake.d.ts +8 -1
- package/dist/harnesses/pi-durable/wake.js +10 -1
- package/dist/http/contract.d.ts +3 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -0
- package/dist/scaffold/add-channel.js +1 -1
- package/dist/schedule/cron.d.ts +4 -2
- package/dist/schedule/cron.js +4 -2
- package/dist/schedule/eventbridge-cron.d.ts +7 -3
- package/dist/schedule/eventbridge-cron.js +9 -8
- package/dist/sign-in-api.d.ts +147 -0
- package/dist/sign-in-api.js +9 -0
- package/dist/tunnel.js +2 -1
- package/dist/value-file.d.ts +19 -0
- package/dist/value-file.js +61 -5
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/http/contract.d.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
10
|
-
export type
|
|
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 "../
|
|
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(", ");
|
package/dist/schedule/cron.d.ts
CHANGED
|
@@ -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
|
|
22
|
-
*
|
|
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;
|
package/dist/schedule/cron.js
CHANGED
|
@@ -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
|
|
49
|
-
*
|
|
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
|
|
10
|
-
* removes (02:30 in most of the US on that day) is skipped by EventBridge and run
|
|
11
|
-
*
|
|
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
|
|
60
|
-
* removes (02:30 in most of the US on that day) is skipped by EventBridge and run
|
|
61
|
-
*
|
|
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
|
-
|
|
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 {
|
|
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";
|
package/dist/value-file.d.ts
CHANGED
|
@@ -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;
|
package/dist/value-file.js
CHANGED
|
@@ -1,13 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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 {
|
|
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.
|
|
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.
|
|
93
|
-
"@earendil-works/pi-ai": "1.
|
|
94
|
-
"@earendil-works/pi-durable": "1.
|
|
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.
|
|
97
|
+
"@larksuiteoapi/node-sdk": "^1.76.0",
|
|
98
98
|
"@smithy/signature-v4": "^5.7.4",
|
|
99
|
-
"chokidar": "^
|
|
99
|
+
"chokidar": "^6.0.0",
|
|
100
100
|
"commander": "^15.0.0",
|
|
101
|
-
"croner": "^
|
|
101
|
+
"croner": "^11.0.0",
|
|
102
102
|
"effect": "^4.0.7",
|
|
103
103
|
"giget": "^3.3.0",
|
|
104
|
-
"ignore": "^7.0.
|
|
104
|
+
"ignore": "^7.0.14",
|
|
105
105
|
"proper-lockfile": "^4.1.2",
|
|
106
106
|
"undici": "^8.6.0",
|
|
107
|
-
"yaml": "^2.9.
|
|
107
|
+
"yaml": "^2.9.1"
|
|
108
108
|
},
|
|
109
109
|
"devDependencies": {
|
|
110
|
-
"@biomejs/biome": "^2.
|
|
111
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
112
|
-
"@hono/node-server": "^2.1.
|
|
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.
|
|
116
|
-
"fastify": "^5.12.
|
|
117
|
-
"knip": "^6.
|
|
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.
|
|
119
|
+
"vitest": "^5.0.5"
|
|
120
120
|
}
|
|
121
121
|
}
|