@fastagent-sh/fastagent 0.25.0 → 0.26.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 (53) 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 +50 -50
  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 +2 -0
  39. package/dist/harnesses/pi-durable/open.d.ts +7 -0
  40. package/dist/harnesses/pi-durable/open.js +4 -3
  41. package/dist/harnesses/pi-durable/serve.d.ts +11 -1
  42. package/dist/harnesses/pi-durable/serve.js +42 -12
  43. package/dist/harnesses/pi-durable/wake.d.ts +8 -1
  44. package/dist/harnesses/pi-durable/wake.js +10 -1
  45. package/dist/index.d.ts +5 -3
  46. package/dist/index.js +3 -0
  47. package/dist/scaffold/add-channel.js +1 -1
  48. package/dist/sign-in-api.d.ts +147 -0
  49. package/dist/sign-in-api.js +9 -0
  50. package/dist/tunnel.js +2 -1
  51. package/dist/value-file.d.ts +19 -0
  52. package/dist/value-file.js +61 -5
  53. package/package.json +1 -1
package/README.md CHANGED
@@ -152,15 +152,16 @@ The package exports the supported surface only, from one entry per kind of user,
152
152
  | The Agent API | `Agent`, `AgentError` ([Agent API](docs/SPEC.md)) | Stable within its version |
153
153
  | Directory → agent → service | `openAgent`, `LocalAgent`, `serve()`'s `Service` (`fetch`, `listen`) | Usable now, may tighten before 1.0 |
154
154
  | Over HTTP | `connectAgent` (`/client`), the `/v1` routes ([Agent API](docs/SPEC.md) §9) | Stable within its version |
155
+ | A host embedding agents | `createAgent`, `CredentialStore`, `credentialFile`, `signIn`, `signInOptions`, `providerAuth` ([embedding](docs/embedding.md#6-a-desktop-host)) | Usable now, may tighten before 1.0 |
155
156
  | Authoring | `defineAgent`, `defineChannel`, `defineSchedule`, `needs`, `sessionIdOf`, `readBodyCapped`, `text`, `secretEquals` | Usable now, may tighten before 1.0 |
156
- | Not exported | The assembly's parts (`router`, `createScheduler`), content, models and sign-in (the CLI's), harness internals | No compatibility promise |
157
+ | Not exported | The assembly's parts (`router`, `createScheduler`), content editing, the model environment, harness internals | No compatibility promise |
157
158
 
158
159
  A tool or an extension is pi-durable's own (`defineTool`, `defineExtension` from `@earendil-works/pi-durable`), and a
159
160
  custom provider is pi-ai's (`createProvider` from `@earendil-works/pi-ai`): `init` adds both packages to the agent.
160
161
 
161
162
  Entry points (`./package.json` is also exported, for tools that read the version):
162
163
 
163
- - `@fastagent-sh/fastagent` — the Agent API, `openAgent`, and what an author writes;
164
+ - `@fastagent-sh/fastagent` — the Agent API, `openAgent`, what a host embedding agents needs, and what an author writes;
164
165
  - `@fastagent-sh/fastagent/client` — `connectAgent`, the Agent API over HTTP, for a program anywhere `fetch` runs;
165
166
  - `@fastagent-sh/fastagent/telegram` — Telegram bot channel;
166
167
  - `@fastagent-sh/fastagent/slack` — Slack Events API bot channel;
@@ -50,11 +50,15 @@ declare const AgentInfo_base: Schema.Opaque<AgentInfo, Schema.Struct<{
50
50
  readonly name: Schema.String;
51
51
  readonly description: Schema.optionalKey<Schema.String>;
52
52
  }>>;
53
- /** The models a session may name: those whose provider has a credential here. */
53
+ /**
54
+ * The models a session may name: those whose provider has a credential here. `contextWindow`, in tokens, is the
55
+ * window the agent runs the model with, absent when the model declares none.
56
+ */
54
57
  readonly models: Schema.$Array<Schema.Struct<{
55
58
  readonly provider: Schema.String;
56
59
  readonly modelId: Schema.String;
57
60
  readonly name: Schema.optionalKey<Schema.String>;
61
+ readonly contextWindow: Schema.optionalKey<Schema.Number>;
58
62
  }>>;
59
63
  readonly defaultModel: Schema.optionalKey<Schema.Struct<{
60
64
  readonly provider: Schema.String;
@@ -73,11 +77,15 @@ declare const AgentInfo_base: Schema.Opaque<AgentInfo, Schema.Struct<{
73
77
  readonly name: Schema.String;
74
78
  readonly description: Schema.optionalKey<Schema.String>;
75
79
  }>>;
76
- /** The models a session may name: those whose provider has a credential here. */
80
+ /**
81
+ * The models a session may name: those whose provider has a credential here. `contextWindow`, in tokens, is the
82
+ * window the agent runs the model with, absent when the model declares none.
83
+ */
77
84
  readonly models: Schema.$Array<Schema.Struct<{
78
85
  readonly provider: Schema.String;
79
86
  readonly modelId: Schema.String;
80
87
  readonly name: Schema.optionalKey<Schema.String>;
88
+ readonly contextWindow: Schema.optionalKey<Schema.Number>;
81
89
  }>>;
82
90
  readonly defaultModel: Schema.optionalKey<Schema.Struct<{
83
91
  readonly provider: Schema.String;
package/dist/agent-api.js CHANGED
@@ -28,8 +28,16 @@ export class AgentInfo extends Schema.Opaque()(Schema.Struct({
28
28
  commands: Schema.Array(Named),
29
29
  /** The tools a session may enable. */
30
30
  tools: Schema.Array(Named),
31
- /** The models a session may name: those whose provider has a credential here. */
32
- models: Schema.Array(Schema.Struct({ provider: Schema.String, modelId: Schema.String, name: Schema.optionalKey(Schema.String) })),
31
+ /**
32
+ * The models a session may name: those whose provider has a credential here. `contextWindow`, in tokens, is the
33
+ * window the agent runs the model with, absent when the model declares none.
34
+ */
35
+ models: Schema.Array(Schema.Struct({
36
+ provider: Schema.String,
37
+ modelId: Schema.String,
38
+ name: Schema.optionalKey(Schema.String),
39
+ contextWindow: Schema.optionalKey(Schema.Number),
40
+ })),
33
41
  defaultModel: Schema.optionalKey(ModelRef),
34
42
  })) {
35
43
  }
@@ -9,7 +9,7 @@ import type { ContentAddition } from "./content/source.ts";
9
9
  export declare class ContentNameError extends Error {
10
10
  name: string;
11
11
  }
12
- export interface CreateAgentOptions {
12
+ export interface InitAgentOptions {
13
13
  /** What the agent works on and knows: repositories into `context.json`, this machine's directories into
14
14
  * `context.local.json`. */
15
15
  content?: ContentAddition[];
@@ -26,8 +26,6 @@ export interface CreatedAgent {
26
26
  dir: string;
27
27
  /** The files written, relative to `dir`. */
28
28
  created: string[];
29
- /** The content, resolved for this instance. */
30
- content: ResolvedContent[];
31
29
  /**
32
30
  * What became of the agent's git repository, as a sentence to show: created with the scaffold as its first commit,
33
31
  * or why not (inside a repository that tracks it, git missing, no commit identity).
@@ -35,12 +33,18 @@ export interface CreatedAgent {
35
33
  repository: string;
36
34
  }
37
35
  /**
38
- * Create an agent in `dir`, which must be new or empty. Every content entry is checked before anything is written, and
39
- * the scaffold is removed again when writing the content fails. The agent is then a git repository whose first commit
40
- * is what was written:
41
- * it changes itself, and version control is how its author goes back to a version that worked.
36
+ * Create an agent in `dir` as `fastagent init` does, which must be new or empty: the whole scaffold, and a git
37
+ * repository whose first commit is what was written, since the agent changes itself and version control is how its
38
+ * author goes back to a version that worked. It installs no dependencies; the agent opens without them.
39
+ */
40
+ export declare function createAgent(dir: string): Promise<CreatedAgent>;
41
+ /**
42
+ * {@link createAgent} with what `fastagent init` adds: content, and the install. Every content entry is checked before
43
+ * anything is written, and the scaffold is removed again when writing the content fails.
42
44
  */
43
- export declare function createAgent(dir: string, options?: CreateAgentOptions): Promise<CreatedAgent>;
45
+ export declare function initAgent(dir: string, options?: InitAgentOptions): Promise<CreatedAgent & {
46
+ content: ResolvedContent[];
47
+ }>;
44
48
  /**
45
49
  * The agent's content, resolved for this instance (what `fastagent content list` shows), and the directories this
46
50
  * machine binds that are not here: the agent is not told of those.
package/dist/authoring.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Creating an agent and editing its content: `fastagent init` and `fastagent content` are thin wrappers over these (and
3
3
  * `fastagent env` writes its declaration through {@link writeEnvironment}), so the rules live here, not in a command.
4
- * Nothing here prints or exits; every refusal is thrown with the message the CLI shows. Not exported from the package
5
- * (#847 decides how a client reaches them).
4
+ * Nothing here prints or exits; every refusal is thrown with the message the CLI shows. The package exports
5
+ * {@link createAgent}; the content and environment edits wait for context packages, whose shape they follow.
6
6
  */
7
7
  import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
8
8
  import { rm } from "node:fs/promises";
@@ -25,12 +25,19 @@ export class ContentNameError extends Error {
25
25
  name = "ContentNameError";
26
26
  }
27
27
  /**
28
- * Create an agent in `dir`, which must be new or empty. Every content entry is checked before anything is written, and
29
- * the scaffold is removed again when writing the content fails. The agent is then a git repository whose first commit
30
- * is what was written:
31
- * it changes itself, and version control is how its author goes back to a version that worked.
28
+ * Create an agent in `dir` as `fastagent init` does, which must be new or empty: the whole scaffold, and a git
29
+ * repository whose first commit is what was written, since the agent changes itself and version control is how its
30
+ * author goes back to a version that worked. It installs no dependencies; the agent opens without them.
32
31
  */
33
- export async function createAgent(dir, options = {}) {
32
+ export async function createAgent(dir) {
33
+ const { content: _, ...created } = await initAgent(dir);
34
+ return created;
35
+ }
36
+ /**
37
+ * {@link createAgent} with what `fastagent init` adds: content, and the install. Every content entry is checked before
38
+ * anything is written, and the scaffold is removed again when writing the content fails.
39
+ */
40
+ export async function initAgent(dir, options = {}) {
34
41
  const agentDir = resolve(dir);
35
42
  const additions = options.content ?? [];
36
43
  let maps = { content: {}, bindings: {} };
@@ -10,7 +10,7 @@ import { cloudFor } from "../channels/feishu/cloud.js";
10
10
  import { createFeishuApi, isFeishuConfigApiMissing, isTransientFeishuRegistrationError, } from "../channels/feishu/feishu-api.js";
11
11
  import { registerFeishuApp } from "../channels/feishu/register-app.js";
12
12
  import { LARK_CONSOLE_URL, onboardLarkApp } from "../channels/lark/onboard.js";
13
- import { dotEnvPath, loadEnvValues, parseEnvContent } from "../env.js";
13
+ import { dotEnvPath, loadEnvValues, parseEnvContent } from "../value-file.js";
14
14
  import { openExternalUrl } from "../open-url.js";
15
15
  import { terminalPerson } from "./person.js";
16
16
  import { AGENT_ENVIRONMENT_ENV } from "../paths.js";
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { basename } from "node:path";
3
3
  import { isCancel, log as clackLog, password, select, text as clackText } from "@clack/prompts";
4
- import { dotEnvPath, parseEnvContent } from "../env.js";
4
+ import { dotEnvPath, parseEnvContent } from "../value-file.js";
5
5
  import { openExternalUrl } from "../open-url.js";
6
6
  import { appendChannelDotEnv } from "../scaffold/add-channel.js";
7
7
  import { newSlackOnboardingState, onboardSlackApp } from "../channels/slack/onboard.js";
@@ -1,6 +1,7 @@
1
1
  import { type BoxShell } from "../deploy/box-shell.ts";
2
2
  import type { DeployHost } from "../deploy/hosts.ts";
3
- import type { LoginIO, LoginMethod } from "../harnesses/models/login.ts";
3
+ import type { LoginIO } from "../harnesses/models/login.ts";
4
+ import type { SignInMethod } from "../sign-in-api.ts";
4
5
  import type { TicketStep } from "./ticket.ts";
5
6
  export interface BoxLoginRequest {
6
7
  host: DeployHost;
@@ -8,7 +9,7 @@ export interface BoxLoginRequest {
8
9
  /** Skip the provider menu. */
9
10
  provider?: string;
10
11
  /** Skip the method menu. */
11
- method?: LoginMethod;
12
+ method?: SignInMethod;
12
13
  /** Keep a credential the box already holds for `provider` (a redeploy), instead of replacing it. */
13
14
  ifMissing?: boolean;
14
15
  /** false: never ask; a box without the credential is a failure naming the command to run in a terminal. */
@@ -13,7 +13,8 @@ import { feishuIngressFor } from "../../channels/feishu/setup-mode.js";
13
13
  import { markDevServe, unmarkDevServe } from "../../serving-command.js";
14
14
  import { channelIngress } from "../../channels/discover.js";
15
15
  import { buildDefinitionShape, prepareDefinition } from "../../harnesses/pi-durable/open.js";
16
- import { dotEnvPath, enterAgentEnv, loadEnvValues } from "../../env.js";
16
+ import { enterAgentEnv } from "../../env.js";
17
+ import { dotEnvPath, loadEnvValues } from "../../value-file.js";
17
18
  import { selectAgentEnvironment, resolveStateRoot, SECRETS_DIRNAME, isUnderDir, displayPath } from "../../paths.js";
18
19
  import { detectRuntime, readPackageJson } from "../../runtime.js";
19
20
  import { appendChannelDotEnv, appendChannelEnv, assertChannelReady, channelExists, channelSetup, scaffoldChannel, scaffoldCompanionTools, } from "../../scaffold/add-channel.js";
@@ -6,7 +6,7 @@ import { relative } from "node:path";
6
6
  import { cloudFor } from "../../channels/feishu/cloud.js";
7
7
  import { FEISHU_INGRESS_SETTINGS } from "../../channels/feishu/setup-mode.js";
8
8
  import { assembleSecrets, divergentSettingsGate, missingValuesGate } from "../../deploy/secrets.js";
9
- import { dotEnvPath, loadEnvValues } from "../../env.js";
9
+ import { dotEnvPath, loadEnvValues } from "../../value-file.js";
10
10
  import { unmarkDevServe } from "../../serving-command.js";
11
11
  import { assertIndependentFeishuApps, feishuAppSecretNames, larkCredentialsNeeded, prepareFeishuApps, unpreparedFeishuApps, } from "../add-feishu.js";
12
12
  import { displayPath, selectAgentEnvironment } from "../../paths.js";
@@ -7,11 +7,12 @@ import { resolve } from "node:path";
7
7
  import { loadEnvironment, readContextFile } from "../../content/file.js";
8
8
  import { aptAdvice, missingAptPackages } from "../../environment/apt.js";
9
9
  import { declareEnvironment, declaredVariables, parsePackageArgument, runtimeAdvice, } from "../../environment/declare.js";
10
- import { checkPackages, ensureMise, enterEnvironment, installPackages, miseBinary } from "../../environment/mise.js";
10
+ import { checkPackages, ensureMise, installPackages, miseBinary, prepareEnvironment } from "../../environment/mise.js";
11
11
  import { writeEnvironment } from "../../authoring.js";
12
12
  import { log } from "../../log.js";
13
13
  import { agentDirOrExit, failStartup } from "../fail.js";
14
- import { dotEnvPath, enterAgentEnv } from "../../env.js";
14
+ import { enterAgentEnv } from "../../env.js";
15
+ import { dotEnvPath } from "../../value-file.js";
15
16
  import { missingSecrets } from "../../declared-secrets.js";
16
17
  const NONE = { packages: {}, aptPackages: [], variables: {} };
17
18
  /**
@@ -147,10 +148,10 @@ export async function runEnvInstall() {
147
148
  }
148
149
  export async function runEnvExec(command) {
149
150
  const agentDir = enterAgentHere();
150
- await enterEnvironment(agentDir).catch(failStartup);
151
+ const variables = await prepareEnvironment(agentDir, process.env).catch(failStartup);
151
152
  const [program, ...args] = command;
152
153
  const code = await new Promise((done, fail) => {
153
- const child = spawn(program, args, { stdio: "inherit" });
154
+ const child = spawn(program, args, { stdio: "inherit", env: { ...process.env, ...variables } });
154
155
  child.on("error", fail);
155
156
  child.on("close", (exit, signal) => done(exit ?? (signal ? 1 : 0)));
156
157
  }).catch((error) => failStartup(error.code === "ENOENT" ? new Error(`${program}: command not found in the environment`) : error));
@@ -2,19 +2,19 @@
2
2
  import { spawn } from "node:child_process";
3
3
  import { resolve } from "node:path";
4
4
  import { displayPath } from "../../paths.js";
5
- import { createAgent } from "../../authoring.js";
5
+ import { initAgent } from "../../authoring.js";
6
6
  import { readContentSource } from "../../content/source.js";
7
7
  import { failEdit } from "./content.js";
8
8
  import { contentLines } from "../content-view.js";
9
9
  import { failStartup } from "../fail.js";
10
10
  export async function runInit(dirArg, opts) {
11
11
  const dir = resolve(dirArg);
12
- // Every source is read, and every entry checked (by createAgent), BEFORE anything is created.
12
+ // Every source is read, and every entry checked (by initAgent), BEFORE anything is created.
13
13
  const read = await Promise.resolve()
14
14
  .then(() => opts.content.map((source) => readContentSource(source, process.cwd())))
15
15
  .catch(failStartup);
16
16
  let installFailed = false;
17
- const { created, content, repository } = await createAgent(dir, {
17
+ const { created, content, repository } = await initAgent(dir, {
18
18
  content: read.map((source) => source.addition),
19
19
  ...(opts.install
20
20
  ? {
@@ -5,7 +5,8 @@
5
5
  */
6
6
  import { join, relative, resolve } from "node:path";
7
7
  import { AgentError } from "../../agent-api.js";
8
- import { dotEnvPath, enterAgentEnv, loadEnvValues } from "../../env.js";
8
+ import { enterAgentEnv } from "../../env.js";
9
+ import { dotEnvPath, loadEnvValues } from "../../value-file.js";
9
10
  import { installProxyFetch } from "../../proxy.js";
10
11
  import { modelRefOf } from "../../harnesses/pi-durable/open.js";
11
12
  import { openServing } from "../../harnesses/pi-durable/serve.js";
@@ -1,9 +1,11 @@
1
- import { enterAgentEnv, dotEnvPath } from "../../env.js";
1
+ import { enterAgentEnv } from "../../env.js";
2
+ import { dotEnvPath } from "../../value-file.js";
2
3
  import { GLOBAL_AUTH_PATH, resolveAuthPath, } from "../../harnesses/models/credentials.js";
3
4
  import { modelEnvironment } from "../../harnesses/models/environment.js";
4
5
  import { DEPLOY_HOSTS } from "../../deploy/hosts.js";
5
6
  import { findAgentDir, globalHome, selectAgentEnvironment } from "../../paths.js";
6
- import { LoginCancelled, LoginRefused, PI_DEFAULT_MODELS, chooseLogin, loginFlow, loginOptionsOver, loginOver, loginStatusText, } from "../../harnesses/models/login.js";
7
+ import { SignInCancelled } from "../../sign-in-api.js";
8
+ import { LoginRefused, PI_DEFAULT_MODELS, chooseLogin, loginFlow, loginOptionsOver, loginOver, loginStatusText, } from "../../harnesses/models/login.js";
7
9
  import { environmentAuth, keyVariables, loginProviders } from "../../harnesses/models/models.js";
8
10
  import { definitionProviders, loadConfig, modelSpecFor } from "../../harnesses/pi-durable/open.js";
9
11
  import { loginOnBox, ticketLoginIO } from "../box-login.js";
@@ -95,7 +97,7 @@ export async function runLogin(provider, opts) {
95
97
  }).catch((error) => {
96
98
  if (timeout?.aborted)
97
99
  failStartup(new Error(`login timed out after ${givenTimeout}s (--timeout)`));
98
- if (error instanceof LoginCancelled) {
100
+ if (error instanceof SignInCancelled) {
99
101
  // A decision, not a failure — neutral wording; non-zero exit because no credential was stored.
100
102
  console.error(`[fastagent] login cancelled`);
101
103
  process.exit(1);
@@ -470,7 +472,7 @@ async function requestDeploymentLogin(agentDir, host, given, opts, timeoutSecond
470
472
  */
471
473
  async function runStdioLogin(provider, opts) {
472
474
  const wire = stdioLoginIO(process.stdin, process.stdout);
473
- const result = await stdioLogin(wire.io, provider, opts).catch((error) => error instanceof LoginCancelled
475
+ const result = await stdioLogin(wire.io, provider, opts).catch((error) => error instanceof SignInCancelled
474
476
  ? { ok: false, reason: "cancelled", message: "cancelled" }
475
477
  : { ok: false, reason: "failed", message: error.message });
476
478
  wire.result(result);
@@ -1,12 +1,13 @@
1
1
  import type { Readable, Writable } from "node:stream";
2
- import type { LoginIO, LoginMethod } from "../harnesses/models/login.ts";
2
+ import type { LoginIO } from "../harnesses/models/login.ts";
3
+ import type { SignInMethod } from "../sign-in-api.ts";
3
4
  /** How a login on the box ended, as the box reports it. */
4
5
  export type RelayResult =
5
6
  /** Logged in now: `method` is how, `path` where the box saved it. */
6
7
  {
7
8
  ok: true;
8
9
  provider: string;
9
- method: LoginMethod;
10
+ method: SignInMethod;
10
11
  path: string;
11
12
  }
12
13
  /**
@@ -24,7 +25,7 @@ export type RelayResult =
24
25
  };
25
26
  /**
26
27
  * The box half: a {@link LoginIO} over `input`/`output`. Once `input` ends (the session dropped), every pending and
27
- * later question reads as backed out, so the flow ends in `LoginCancelled` instead of waiting forever.
28
+ * later question reads as backed out, so the flow ends in `SignInCancelled` instead of waiting forever.
28
29
  */
29
30
  export declare function stdioLoginIO(input: Readable, output: Writable): {
30
31
  io: LoginIO;
@@ -12,7 +12,7 @@ import { BOX_CLI_PREDATES } from "../deploy/box-shell.js";
12
12
  const send = (out, message) => void out.write(`${JSON.stringify(message)}\n`);
13
13
  /**
14
14
  * The box half: a {@link LoginIO} over `input`/`output`. Once `input` ends (the session dropped), every pending and
15
- * later question reads as backed out, so the flow ends in `LoginCancelled` instead of waiting forever.
15
+ * later question reads as backed out, so the flow ends in `SignInCancelled` instead of waiting forever.
16
16
  */
17
17
  export function stdioLoginIO(input, output) {
18
18
  const pending = new Map();
@@ -5,7 +5,8 @@
5
5
  import { resolve } from "node:path";
6
6
  import { autocomplete, isCancel, log as clackLog, password, select, text as clackText } from "@clack/prompts";
7
7
  import { buildModelPickerOptions } from "./models-view.js";
8
- import { LoginCancelled, loginFlow } from "../harnesses/models/login.js";
8
+ import { loginFlow } from "../harnesses/models/login.js";
9
+ import { SignInCancelled } from "../sign-in-api.js";
9
10
  import { modelEnvironment } from "../harnesses/models/environment.js";
10
11
  import { listModels, providerAuthStatuses, providerOf } from "../harnesses/models/models.js";
11
12
  import { definitionProviders, loadConfig, modelSpecFor } from "../harnesses/pi-durable/open.js";
@@ -13,7 +14,8 @@ import { formatAuthReport } from "./auth-view.js";
13
14
  import { contentLines } from "./content-view.js";
14
15
  import { deployedHost, isDeployedWorkspace } from "../paths.js";
15
16
  import { log } from "../log.js";
16
- import { dotEnvPath, enterAgentEnv } from "../env.js";
17
+ import { enterAgentEnv } from "../env.js";
18
+ import { dotEnvPath } from "../value-file.js";
17
19
  import { openExternalUrl } from "../open-url.js";
18
20
  import { bindAddress, isBindAddress } from "../bind.js";
19
21
  import { agentDirOrExit, failStartup, failUsage } from "./fail.js";
@@ -180,7 +182,7 @@ async function pickWithCredentials(models, authPath, providers) {
180
182
  console.error(`[fastagent] logged in to ${provider} — saved to ${authPath}`);
181
183
  }
182
184
  catch (error) {
183
- if (error instanceof LoginCancelled)
185
+ if (error instanceof SignInCancelled)
184
186
  return undefined; // user backed out — discard the choice, like a picker cancel
185
187
  // A FAILED login keeps the choice: the pick persists, the startup auth report names the remedy, and a later
186
188
  // `fastagent login` fixes auth without re-picking the model.
@@ -38,4 +38,4 @@ export type CloneOutcome = {
38
38
  * - None yet: cloned beside it and renamed into place. A first clone that fails stops the start.
39
39
  * - A clone of another repository there (the entry was renamed or redeclared): refused, naming it.
40
40
  */
41
- export declare function refreshClone(repo: string, ref: string | undefined, dir: string): Promise<CloneOutcome>;
41
+ export declare function refreshClone(repo: string, ref: string | undefined, dir: string, env?: NodeJS.ProcessEnv): Promise<CloneOutcome>;
@@ -6,7 +6,8 @@
6
6
  import { existsSync, mkdirSync, realpathSync, renameSync, rmSync, statSync } from "node:fs";
7
7
  import { basename, dirname, join } from "node:path";
8
8
  import { gitFor } from "../git.js";
9
- const { answer: gitAnswer, run: runGit } = gitFor("github content needs it on this machine");
9
+ const WHY = "github content needs it on this machine";
10
+ const { answer: gitAnswer } = gitFor(WHY);
10
11
  /** The `owner/repo` a GitHub remote URL names, in any of its spellings (https, ssh, scp-like), or undefined. */
11
12
  export function githubRepoOf(url) {
12
13
  const match = /^(?:(?:https?|git|ssh):\/\/(?:[^@/]+@)?github\.com\/|(?:[^@/:]+@)?github\.com:)([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/i.exec(url.trim());
@@ -94,14 +95,16 @@ const HELPER_KEY = "credential.https://github.com.helper";
94
95
  * - None yet: cloned beside it and renamed into place. A first clone that fails stops the start.
95
96
  * - A clone of another repository there (the entry was renamed or redeclared): refused, naming it.
96
97
  */
97
- export async function refreshClone(repo, ref, dir) {
98
+ export async function refreshClone(repo, ref, dir, env = process.env) {
99
+ // The agent's environment, whose GITHUB_TOKEN the clone's credential helper reads.
100
+ const { run: runGit } = gitFor(WHY, env);
98
101
  // A library caller can hand this anything; git reads a leading "-" as an option, and no repository or ref has one
99
102
  // (a declaration with one is refused at load, declare.ts).
100
103
  if (repo.startsWith("-") || ref?.startsWith("-")) {
101
104
  throw new Error(`github ${repo}${ref !== undefined ? ` at ${ref}` : ""} would reach git as an option`);
102
105
  }
103
106
  if (!existsSync(dir))
104
- return cloneInto(repo, ref, dir);
107
+ return cloneInto(repo, ref, dir, runGit);
105
108
  const origin = gitAnswer(["config", "--get", "remote.origin.url"], dir);
106
109
  if (origin === undefined || githubRepoOf(origin)?.toLowerCase() !== repo.toLowerCase()) {
107
110
  throw new Error(`${dir} is a clone of ${origin ?? "no repository"}, not of github ${repo}: move it away (push what should last ` +
@@ -162,7 +165,7 @@ function gitReason(error) {
162
165
  * process reads no clone or a whole one. When another process put its own there first, that one stands and this one
163
166
  * is discarded. Shallow: a starting point to work from; a later fetch brings the history an update needs.
164
167
  */
165
- async function cloneInto(repo, ref, dir) {
168
+ async function cloneInto(repo, ref, dir, runGit) {
166
169
  const parent = dirname(dir);
167
170
  mkdirSync(parent, { recursive: true });
168
171
  const next = join(parent, `.${basename(dir)}.next-${process.pid}-${Date.now()}`);
@@ -49,4 +49,4 @@ export declare function checkBinding(agentDir: string, name: string, target: str
49
49
  */
50
50
  export declare function cloneContent(entry: Extract<ResolvedContent, {
51
51
  kind: "github";
52
- }>): Promise<CloneOutcome>;
52
+ }>, env?: NodeJS.ProcessEnv): Promise<CloneOutcome>;
@@ -103,11 +103,11 @@ export function checkBinding(agentDir, name, target, repository) {
103
103
  * agent did in it (git.ts `refreshClone`): what git refuses, or a remote that cannot be reached, keeps it as it is,
104
104
  * with the reason. Only a process that runs the agent calls this. A checkout this machine binds is never cloned over.
105
105
  */
106
- export async function cloneContent(entry) {
106
+ export async function cloneContent(entry, env = process.env) {
107
107
  if (!entry.clone)
108
108
  throw new Error(`content "${entry.name}" is bound to the checkout ${entry.location}, not a clone`);
109
109
  ensureContentDir(dirname(entry.location));
110
- return refreshClone(entry.repo, entry.ref, entry.location).catch((error) => {
110
+ return refreshClone(entry.repo, entry.ref, entry.location, env).catch((error) => {
111
111
  throw new Error(`content "${entry.name}": ${error.message}`);
112
112
  });
113
113
  }
@@ -19,7 +19,7 @@ import { RUNTIME_FLOOR, belowFloor, detectRuntime, listsFastagent, readPackageJs
19
19
  import { fastagentVersion } from "../version.js";
20
20
  import { isGeneratedDockerfile } from "./container.js";
21
21
  import { buildContextPaths, checkKeptIgnoreFiles } from "./build-context.js";
22
- import { dotEnvPath, loadEnvValues } from "../env.js";
22
+ import { dotEnvPath, loadEnvValues } from "../value-file.js";
23
23
  import { deploymentSecrets, isEnvKey } from "./secrets.js";
24
24
  import { DEFAULT_HTTP_PORT } from "../service.js";
25
25
  import { declaredVariables, declaresTool, runtimeAdvice } from "../environment/declare.js";
@@ -6,7 +6,7 @@ import { spawn } from "node:child_process";
6
6
  import { relative, sep } from "node:path";
7
7
  import { watch as watchTree } from "chokidar";
8
8
  import { AGENT_CONFIG_FILE, CONTEXT_FILE, LOCAL_CONTEXT_FILE, resolveStateRoot, isUnderDir } from "./paths.js";
9
- import { dotEnvPath } from "./env.js";
9
+ import { dotEnvPath } from "./value-file.js";
10
10
  import { log } from "./log.js";
11
11
  import { openExternalUrl } from "./open-url.js";
12
12
  import { declaredChannels } from "./channels/discover.js";
package/dist/env.d.ts CHANGED
@@ -4,19 +4,6 @@
4
4
  * occurrence wins ({@link parseEnvContent}).
5
5
  */
6
6
  export declare function applyEnvValues(values: ReadonlyMap<string, string>): void;
7
- /** Parse .env content into key → value (the dialect above; last occurrence of a key wins). */
8
- export declare function parseEnvContent(content: string): Map<string, string>;
9
- /**
10
- * READ a value file without entering it: the values as data, never `process.env`. This is how code reasons about an
11
- * environment OTHER than its own — `deploy` resolves configuration in the environment being deployed, which this
12
- * file declares, and merging it into the running process would put the two environments in one bag. A missing file
13
- * is no values, which is normal.
14
- */
15
- export declare function loadEnvValues(file: string): Map<string, string>;
16
- /** The agent's `.env` file: `<resolved secrets dir>/.env`. */
17
- export declare function dotEnvPath(agentDir: string, env?: NodeJS.ProcessEnv): string;
18
- /** The committable template: `<agentDir>/.secrets/.env.example`. */
19
- export declare function envExamplePath(agentDir: string): string;
20
7
  /**
21
8
  * ENTER an agent directory's runtime environment: its `.env` becomes `process.env`, and this process's outbound fetch
22
9
  * follows whatever proxy that (or the ambient environment) declares.
package/dist/env.js CHANGED
@@ -2,7 +2,7 @@ import { readFileSync, existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { log } from "./log.js";
4
4
  import { installProxyFetch } from "./proxy.js";
5
- import { SECRETS_DIRNAME, resolveSecretsDir } from "./paths.js";
5
+ import { dotEnvPath, loadEnvValues, parseEnvContent } from "./value-file.js";
6
6
  /**
7
7
  * Write parsed values into `process.env`, matching Node's `--env-file` / `process.loadEnvFile` precedence on BOTH
8
8
  * axes (verified against Node in test/env.test.ts): a real env var wins over the file, and within the file the last
@@ -13,49 +13,6 @@ export function applyEnvValues(values) {
13
13
  if (!(key in process.env))
14
14
  process.env[key] = value;
15
15
  }
16
- /** Parse .env content into key → value (the dialect above; last occurrence of a key wins). */
17
- export function parseEnvContent(content) {
18
- const parsed = new Map();
19
- for (const raw of content.split("\n")) {
20
- const line = raw.trim();
21
- if (!line || line.startsWith("#"))
22
- continue;
23
- const eq = line.indexOf("=");
24
- if (eq < 1)
25
- continue; // no `=`, or an empty key
26
- const key = line.slice(0, eq).trim();
27
- let value = line.slice(eq + 1).trim();
28
- const quote = value[0];
29
- if ((quote === '"' || quote === "'") && value.at(-1) === quote)
30
- value = value.slice(1, -1);
31
- parsed.set(key, value); // in-file: last occurrence wins (Map overwrite)
32
- }
33
- return parsed;
34
- }
35
- /**
36
- * READ a value file without entering it: the values as data, never `process.env`. This is how code reasons about an
37
- * environment OTHER than its own — `deploy` resolves configuration in the environment being deployed, which this
38
- * file declares, and merging it into the running process would put the two environments in one bag. A missing file
39
- * is no values, which is normal.
40
- */
41
- export function loadEnvValues(file) {
42
- try {
43
- return parseEnvContent(readFileSync(file, "utf8"));
44
- }
45
- catch (error) {
46
- if (error.code === "ENOENT")
47
- return new Map();
48
- throw error;
49
- }
50
- }
51
- /** The agent's `.env` file: `<resolved secrets dir>/.env`. */
52
- export function dotEnvPath(agentDir, env = process.env) {
53
- return join(resolveSecretsDir(agentDir, env), ".env");
54
- }
55
- /** The committable template: `<agentDir>/.secrets/.env.example`. */
56
- export function envExamplePath(agentDir) {
57
- return join(agentDir, SECRETS_DIRNAME, ".env.example");
58
- }
59
16
  /**
60
17
  * ENTER an agent directory's runtime environment: its `.env` becomes `process.env`, and this process's outbound fetch
61
18
  * follows whatever proxy that (or the ambient environment) declares.
@@ -1,4 +1,4 @@
1
- import { type DeclaredEnvironment, type MissingRuntime } from "./declare.ts";
1
+ import { type MissingRuntime } from "./declare.ts";
2
2
  /** Every platform's package: an agent lists them all, so its lockfile has the image's too. */
3
3
  export declare const MISE_PACKAGE_NAMES: readonly string[];
4
4
  /**
@@ -40,11 +40,12 @@ export declare function checkPackages(agentDir: string, bin: string, packages: R
40
40
  * so a start whose packages are installed needs no network and keeps the versions it has. An install that matches may
41
41
  * be another project's: mise's data directory is the machine's.
42
42
  */
43
- export declare function installPackages(agentDir: string, bin: string, packages: Record<string, string>, newest?: readonly string[] | "all"): Promise<void>;
43
+ export declare function installPackages(agentDir: string, bin: string, packages: Record<string, string>, newest?: readonly string[] | "all", env?: NodeJS.ProcessEnv): Promise<void>;
44
44
  /**
45
- * Enter the agent's environment, for a process that runs it: install the declared packages that have no install here
46
- * yet, and put them first on this process's PATH, which every command the agent runs inherits. mise itself is not put
47
- * on the PATH. An agent without packages borrows the machine's commands. Apt packages are the image's to install; one
48
- * missing here is said, with how.
45
+ * The agent's declared environment, for a process that runs it: install the declared packages that have no install
46
+ * here yet, and answer the variables that put them first on the PATH of the commands the agent runs. mise runs in
47
+ * `env`, the agent's environment so far, and nothing is written into this process's: a process may run several agents.
48
+ * mise itself is not put on the PATH. An agent without packages borrows the machine's commands (no variables). Apt
49
+ * packages are the image's to install; one missing here is said, with how.
49
50
  */
50
- export declare function enterEnvironment(agentDir: string): Promise<DeclaredEnvironment | undefined>;
51
+ export declare function prepareEnvironment(agentDir: string, env: NodeJS.ProcessEnv): Promise<Record<string, string>>;