@ampersend_ai/ampersend-sdk 0.0.22 → 0.0.26
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/dist/ampersend/agent-client.d.ts +81 -0
- package/dist/ampersend/agent-client.d.ts.map +1 -0
- package/dist/ampersend/agent-client.js +104 -0
- package/dist/ampersend/agent-client.js.map +1 -0
- package/dist/ampersend/agent.d.ts +118 -0
- package/dist/ampersend/agent.d.ts.map +1 -0
- package/dist/ampersend/agent.js +109 -0
- package/dist/ampersend/agent.js.map +1 -0
- package/dist/ampersend/approval.js.map +1 -1
- package/dist/ampersend/client.d.ts +32 -8
- package/dist/ampersend/client.d.ts.map +1 -1
- package/dist/ampersend/client.js +81 -3
- package/dist/ampersend/client.js.map +1 -1
- package/dist/ampersend/curated-agent.d.ts +94 -94
- package/dist/ampersend/curated-agent.d.ts.map +1 -1
- package/dist/ampersend/curated-agent.js +41 -41
- package/dist/ampersend/curated-agent.js.map +1 -1
- package/dist/ampersend/index.d.ts +2 -0
- package/dist/ampersend/index.d.ts.map +1 -1
- package/dist/ampersend/index.js +4 -0
- package/dist/ampersend/index.js.map +1 -1
- package/dist/ampersend/management.d.ts +23 -59
- package/dist/ampersend/management.d.ts.map +1 -1
- package/dist/ampersend/management.js +6 -8
- package/dist/ampersend/management.js.map +1 -1
- package/dist/ampersend/marketplace.d.ts +20 -14
- package/dist/ampersend/marketplace.d.ts.map +1 -1
- package/dist/ampersend/marketplace.js +23 -49
- package/dist/ampersend/marketplace.js.map +1 -1
- package/dist/ampersend/treasurer.d.ts +4 -0
- package/dist/ampersend/treasurer.d.ts.map +1 -1
- package/dist/ampersend/treasurer.js +2 -2
- package/dist/ampersend/treasurer.js.map +1 -1
- package/dist/ampersend/types.d.ts +215 -310
- package/dist/ampersend/types.d.ts.map +1 -1
- package/dist/ampersend/types.js +146 -119
- package/dist/ampersend/types.js.map +1 -1
- package/dist/ampersend/zod-bridge.d.ts +4 -5
- package/dist/ampersend/zod-bridge.d.ts.map +1 -1
- package/dist/ampersend/zod-bridge.js +35 -19
- package/dist/ampersend/zod-bridge.js.map +1 -1
- package/dist/cli/ampersend.js +45 -2
- package/dist/cli/ampersend.js.map +1 -1
- package/dist/cli/commands/agent.d.ts +24 -0
- package/dist/cli/commands/agent.d.ts.map +1 -0
- package/dist/cli/commands/agent.js +166 -0
- package/dist/cli/commands/agent.js.map +1 -0
- package/dist/cli/commands/card.d.ts +128 -0
- package/dist/cli/commands/card.d.ts.map +1 -0
- package/dist/cli/commands/card.js +404 -0
- package/dist/cli/commands/card.js.map +1 -0
- package/dist/cli/commands/config.d.ts +1 -1
- package/dist/cli/commands/config.d.ts.map +1 -1
- package/dist/cli/commands/config.js +31 -42
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/fetch.d.ts +77 -2
- package/dist/cli/commands/fetch.d.ts.map +1 -1
- package/dist/cli/commands/fetch.js +210 -129
- package/dist/cli/commands/fetch.js.map +1 -1
- package/dist/cli/commands/fund.d.ts +3 -0
- package/dist/cli/commands/fund.d.ts.map +1 -0
- package/dist/cli/commands/fund.js +22 -0
- package/dist/cli/commands/fund.js.map +1 -0
- package/dist/cli/commands/marketplace.d.ts +6 -0
- package/dist/cli/commands/marketplace.d.ts.map +1 -1
- package/dist/cli/commands/marketplace.js +27 -8
- package/dist/cli/commands/marketplace.js.map +1 -1
- package/dist/cli/commands/setup.d.ts +4 -1
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +66 -41
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/version.d.ts +3 -0
- package/dist/cli/commands/version.d.ts.map +1 -0
- package/dist/cli/commands/version.js +12 -0
- package/dist/cli/commands/version.js.map +1 -0
- package/dist/cli/config.d.ts +225 -45
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +426 -115
- package/dist/cli/config.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/proxy/cli.js +1 -0
- package/dist/mcp/proxy/cli.js.map +1 -1
- package/dist/mcp/proxy/factory.d.ts +2 -0
- package/dist/mcp/proxy/factory.d.ts.map +1 -1
- package/dist/mcp/proxy/factory.js +10 -1
- package/dist/mcp/proxy/factory.js.map +1 -1
- package/dist/mcp/proxy/server/init.js +1 -1
- package/dist/mcp/proxy/server/init.js.map +1 -1
- package/dist/mcp/proxy/server/server.d.ts +3 -1
- package/dist/mcp/proxy/server/server.d.ts.map +1 -1
- package/dist/mcp/proxy/server/server.js +5 -2
- package/dist/mcp/proxy/server/server.js.map +1 -1
- package/dist/mcp/proxy/types.d.ts +7 -0
- package/dist/mcp/proxy/types.d.ts.map +1 -1
- package/dist/mcp/proxy/types.js.map +1 -1
- package/dist/version.d.ts +3 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +3 -1
- package/dist/version.js.map +1 -1
- package/dist/x402/http/factory.d.ts +2 -0
- package/dist/x402/http/factory.d.ts.map +1 -1
- package/dist/x402/http/factory.js +1 -0
- package/dist/x402/http/factory.js.map +1 -1
- package/dist/x402/index.d.ts +2 -0
- package/dist/x402/index.d.ts.map +1 -1
- package/dist/x402/index.js +2 -0
- package/dist/x402/index.js.map +1 -1
- package/dist/x402/siwx.d.ts +55 -0
- package/dist/x402/siwx.d.ts.map +1 -0
- package/dist/x402/siwx.js +122 -0
- package/dist/x402/siwx.js.map +1 -0
- package/dist/x402/wallets/smart-account/cosigned.d.ts +9 -2
- package/dist/x402/wallets/smart-account/cosigned.d.ts.map +1 -1
- package/dist/x402/wallets/smart-account/cosigned.js +13 -4
- package/dist/x402/wallets/smart-account/cosigned.js.map +1 -1
- package/package.json +3 -2
package/dist/cli/config.js
CHANGED
|
@@ -6,21 +6,110 @@ import { isAddress } from "viem";
|
|
|
6
6
|
import { privateKeyToAddress } from "viem/accounts";
|
|
7
7
|
import { parseEnvConfig } from "../ampersend/env.js";
|
|
8
8
|
import { err, ok } from "./envelope.js";
|
|
9
|
+
/**
|
|
10
|
+
* Shared by every CLI command that needs to talk to the API as an agent.
|
|
11
|
+
* Returns an `err` envelope when the local config isn't ready, so the
|
|
12
|
+
* caller can print it and exit.
|
|
13
|
+
*/
|
|
14
|
+
export function loadCredentials(opts = {}) {
|
|
15
|
+
try {
|
|
16
|
+
const envConfig = parseEnvConfig();
|
|
17
|
+
return {
|
|
18
|
+
ok: true,
|
|
19
|
+
credentials: {
|
|
20
|
+
agentAccount: envConfig.AGENT_ACCOUNT,
|
|
21
|
+
agentKey: envConfig.AGENT_KEY,
|
|
22
|
+
...(envConfig.API_URL ? { apiUrl: envConfig.API_URL } : {}),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Fall back to config file
|
|
28
|
+
}
|
|
29
|
+
const selected = getSelectedContext(opts);
|
|
30
|
+
if (selected?.context.status === "ready") {
|
|
31
|
+
// AMPERSEND_API_URL is a hard bypass: if set, it always wins.
|
|
32
|
+
const apiUrl = process.env.AMPERSEND_API_URL ?? selected.context.apiUrl;
|
|
33
|
+
return {
|
|
34
|
+
ok: true,
|
|
35
|
+
credentials: {
|
|
36
|
+
agentAccount: selected.context.agentAccount,
|
|
37
|
+
agentKey: selected.context.agentKey,
|
|
38
|
+
...(apiUrl ? { apiUrl } : {}),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const status = getConfigStatus(opts).status;
|
|
43
|
+
const code = status === "not_initialized" ? "NOT_CONFIGURED" : "SETUP_INCOMPLETE";
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
error: err(code, 'Run "ampersend setup start" or "ampersend config set" to configure', { status }),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
9
49
|
/** Config directory and file paths */
|
|
10
50
|
const CONFIG_DIR = join(homedir(), ".ampersend");
|
|
11
51
|
export const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
12
52
|
/** Current config version */
|
|
13
|
-
const CONFIG_VERSION =
|
|
53
|
+
const CONFIG_VERSION = 2;
|
|
14
54
|
/** Hard-coded approval expiration: 30 minutes */
|
|
15
55
|
const APPROVAL_EXPIRY_MS = 30 * 60 * 1000;
|
|
16
56
|
/** Default API URL (production) */
|
|
17
57
|
export const DEFAULT_API_URL = "https://api.ampersend.ai";
|
|
18
|
-
const HexString = Schema.TemplateLiteral(Schema.Literal("0x"), Schema.String);
|
|
19
|
-
/**
|
|
58
|
+
const HexString = Schema.TemplateLiteral([Schema.Literal("0x"), Schema.String]);
|
|
59
|
+
/**
|
|
60
|
+
* Cached Laso Bearer token for `card details`/`list`, so a warm read costs
|
|
61
|
+
* nothing. Stamped with the identity (`agentKey`) and `apiUrl` it was minted
|
|
62
|
+
* under: `readLasoToken` treats it as absent if either no longer matches the
|
|
63
|
+
* active context (covers env-var overrides) or it has expired. Self-correcting,
|
|
64
|
+
* so the identity/URL write paths only need to drop it, not re-thread it.
|
|
65
|
+
*
|
|
66
|
+
* Lives inside the context it was minted under, so switching the active context
|
|
67
|
+
* (`config use`) keeps each context's token intact without any explicit drop.
|
|
20
68
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
69
|
+
* Schema is the source of truth; the `LasoToken` type is derived from it so the
|
|
70
|
+
* two can't drift (the same pattern as the schemas in `src/ampersend/`).
|
|
71
|
+
*/
|
|
72
|
+
const LasoTokenSchema = Schema.Struct({
|
|
73
|
+
idToken: Schema.String,
|
|
74
|
+
expiresAt: Schema.String, // ISO timestamp
|
|
75
|
+
agentKey: HexString,
|
|
76
|
+
apiUrl: Schema.optional(Schema.String),
|
|
77
|
+
});
|
|
78
|
+
// ─── Context model (V2) ──────────────────────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* A named identity in the config. A context is either:
|
|
81
|
+
* - `ready`: resolved, carrying an active key + on-chain account (+ optional
|
|
82
|
+
* per-context apiUrl and cached lasoToken).
|
|
83
|
+
* - `pending`: an in-flight `setup start` — a generated key plus the approval
|
|
84
|
+
* token and its local expiry, but no account yet. `setup finish`
|
|
85
|
+
* promotes it to `ready`.
|
|
86
|
+
*
|
|
87
|
+
* Each context carries its own `apiUrl`, so e.g. a sandbox and a prod context
|
|
88
|
+
* can coexist pointed at different environments.
|
|
89
|
+
*/
|
|
90
|
+
const ReadyContextSchema = Schema.Struct({
|
|
91
|
+
status: Schema.Literal("ready"),
|
|
92
|
+
agentKey: HexString,
|
|
93
|
+
agentAccount: HexString,
|
|
94
|
+
createdAt: Schema.String, // ISO timestamp the context was first created
|
|
95
|
+
apiUrl: Schema.optional(Schema.String),
|
|
96
|
+
lasoToken: Schema.optional(LasoTokenSchema),
|
|
97
|
+
});
|
|
98
|
+
const PendingContextSchema = Schema.Struct({
|
|
99
|
+
status: Schema.Literal("pending"),
|
|
100
|
+
agentKey: HexString,
|
|
101
|
+
token: Schema.String,
|
|
102
|
+
expiresAt: Schema.String, // ISO timestamp — informational; `setup finish` lets the API decide
|
|
103
|
+
createdAt: Schema.String, // ISO timestamp the context was first created
|
|
104
|
+
apiUrl: Schema.optional(Schema.String),
|
|
105
|
+
});
|
|
106
|
+
const ContextSchema = Schema.Union([ReadyContextSchema, PendingContextSchema]);
|
|
107
|
+
const StoredConfigV2Schema = Schema.Struct({
|
|
108
|
+
version: Schema.Literal(2),
|
|
109
|
+
activeContext: Schema.optional(Schema.String),
|
|
110
|
+
contexts: Schema.Record(Schema.String, ContextSchema),
|
|
111
|
+
});
|
|
112
|
+
const StoredConfigV1Schema = Schema.Struct({
|
|
24
113
|
version: Schema.Literal(1),
|
|
25
114
|
agentKey: Schema.optional(HexString),
|
|
26
115
|
agentAccount: Schema.optional(HexString),
|
|
@@ -30,7 +119,53 @@ const StoredConfigSchema = Schema.Struct({
|
|
|
30
119
|
agentKey: HexString,
|
|
31
120
|
expiresAt: Schema.String,
|
|
32
121
|
})),
|
|
122
|
+
lasoToken: Schema.optional(LasoTokenSchema),
|
|
33
123
|
});
|
|
124
|
+
/** Decodes either version; V1 is normalized to V2 by `readConfig`. */
|
|
125
|
+
const StoredConfigSchema = Schema.Union([StoredConfigV2Schema, StoredConfigV1Schema]);
|
|
126
|
+
/**
|
|
127
|
+
* Migrate a legacy single-account V1 config to the V2 context model. There is
|
|
128
|
+
* no `default` context in V2 — both migrated contexts are auto-named from their
|
|
129
|
+
* key (the same scheme `setup`/`config set` use), and `createdAt` is stamped to
|
|
130
|
+
* the migration time since a V1 file carries no creation timestamp.
|
|
131
|
+
*
|
|
132
|
+
* - A complete identity (key + account) becomes a `ready` context carrying the
|
|
133
|
+
* old top-level apiUrl/lasoToken; it becomes active.
|
|
134
|
+
* - A standalone pending approval becomes a `pending` context. If there was no
|
|
135
|
+
* active identity, the pending context becomes active.
|
|
136
|
+
* - A key-only V1 file (no account, no pending) was already non-functional for
|
|
137
|
+
* reads, so we drop the orphan key and migrate to an empty (not_initialized)
|
|
138
|
+
* config.
|
|
139
|
+
*/
|
|
140
|
+
function migrateV1toV2(v1) {
|
|
141
|
+
const config = { version: 2, contexts: {} };
|
|
142
|
+
const createdAt = new Date().toISOString();
|
|
143
|
+
if (v1.agentKey && v1.agentAccount) {
|
|
144
|
+
const name = uniqueContextName(config, v1.apiUrl, privateKeyToAddress(v1.agentKey));
|
|
145
|
+
config.contexts[name] = {
|
|
146
|
+
status: "ready",
|
|
147
|
+
agentKey: v1.agentKey,
|
|
148
|
+
agentAccount: v1.agentAccount,
|
|
149
|
+
createdAt,
|
|
150
|
+
...(v1.apiUrl ? { apiUrl: v1.apiUrl } : {}),
|
|
151
|
+
...(v1.lasoToken ? { lasoToken: v1.lasoToken } : {}),
|
|
152
|
+
};
|
|
153
|
+
config.activeContext = name;
|
|
154
|
+
}
|
|
155
|
+
if (v1.pendingApproval) {
|
|
156
|
+
const name = uniqueContextName(config, v1.apiUrl, privateKeyToAddress(v1.pendingApproval.agentKey));
|
|
157
|
+
config.contexts[name] = {
|
|
158
|
+
status: "pending",
|
|
159
|
+
agentKey: v1.pendingApproval.agentKey,
|
|
160
|
+
token: v1.pendingApproval.token,
|
|
161
|
+
expiresAt: v1.pendingApproval.expiresAt,
|
|
162
|
+
createdAt,
|
|
163
|
+
...(v1.apiUrl ? { apiUrl: v1.apiUrl } : {}),
|
|
164
|
+
};
|
|
165
|
+
config.activeContext ??= name;
|
|
166
|
+
}
|
|
167
|
+
return config;
|
|
168
|
+
}
|
|
34
169
|
/**
|
|
35
170
|
* Ensure config directory exists with secure permissions
|
|
36
171
|
*/
|
|
@@ -40,7 +175,7 @@ function ensureConfigDir() {
|
|
|
40
175
|
}
|
|
41
176
|
}
|
|
42
177
|
/**
|
|
43
|
-
* Read config file if it exists.
|
|
178
|
+
* Read config file if it exists, normalized to the V2 context model.
|
|
44
179
|
* Returns null if the file is missing or corrupt.
|
|
45
180
|
*/
|
|
46
181
|
export function readConfig() {
|
|
@@ -50,7 +185,8 @@ export function readConfig() {
|
|
|
50
185
|
const content = readFileSync(CONFIG_FILE, "utf-8");
|
|
51
186
|
try {
|
|
52
187
|
const parsed = JSON.parse(content);
|
|
53
|
-
|
|
188
|
+
const decoded = Schema.decodeUnknownSync(StoredConfigSchema)(parsed);
|
|
189
|
+
return decoded.version === 1 ? migrateV1toV2(decoded) : decoded;
|
|
54
190
|
}
|
|
55
191
|
catch {
|
|
56
192
|
// Corrupt or unrecognised config — treat as absent so commands can re-initialise
|
|
@@ -58,34 +194,65 @@ export function readConfig() {
|
|
|
58
194
|
}
|
|
59
195
|
}
|
|
60
196
|
/**
|
|
61
|
-
*
|
|
197
|
+
* Drop expired *pending* contexts so the map can't grow without bound. Ready
|
|
198
|
+
* contexts are never auto-pruned (only `config rm` removes them). If the active
|
|
199
|
+
* context is pruned, `activeContext` is cleared.
|
|
200
|
+
*/
|
|
201
|
+
function prunePendingExpired(config) {
|
|
202
|
+
const contexts = {};
|
|
203
|
+
for (const [name, ctx] of Object.entries(config.contexts)) {
|
|
204
|
+
if (ctx.status === "pending" && isPendingExpired(ctx))
|
|
205
|
+
continue;
|
|
206
|
+
contexts[name] = ctx;
|
|
207
|
+
}
|
|
208
|
+
const activeContext = config.activeContext && contexts[config.activeContext] ? config.activeContext : undefined;
|
|
209
|
+
return { version: 2, ...(activeContext ? { activeContext } : {}), contexts };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Write config file with secure permissions. Always writes V2 and prunes
|
|
213
|
+
* expired pending contexts on the way out.
|
|
62
214
|
*/
|
|
63
215
|
export function writeConfig(config) {
|
|
64
216
|
ensureConfigDir();
|
|
65
|
-
const
|
|
66
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(
|
|
217
|
+
const pruned = prunePendingExpired({ version: CONFIG_VERSION, ...config });
|
|
218
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(pruned, null, 2), { mode: 0o600 });
|
|
67
219
|
}
|
|
68
220
|
/**
|
|
69
|
-
*
|
|
221
|
+
* The context name to use this invocation: `--context` flag > `AMPERSEND_CONTEXT`
|
|
222
|
+
* env > persisted `activeContext`. Returns undefined if none resolves.
|
|
70
223
|
*/
|
|
71
|
-
export function
|
|
72
|
-
|
|
73
|
-
|
|
224
|
+
export function resolveContextName(opts = {}) {
|
|
225
|
+
return opts.context ?? process.env.AMPERSEND_CONTEXT ?? readConfig()?.activeContext ?? undefined;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The selected context (name + value) after applying flag/env/active precedence,
|
|
229
|
+
* or null if no config file exists or the resolved name has no context.
|
|
230
|
+
*/
|
|
231
|
+
export function getSelectedContext(opts = {}) {
|
|
232
|
+
const config = readConfig();
|
|
233
|
+
const name = resolveContextName(opts);
|
|
234
|
+
if (!config || !name)
|
|
74
235
|
return null;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return {
|
|
236
|
+
const context = config.contexts[name];
|
|
237
|
+
if (!context)
|
|
238
|
+
return null;
|
|
239
|
+
return { name, context };
|
|
79
240
|
}
|
|
80
241
|
/**
|
|
81
|
-
*
|
|
242
|
+
* Effective API URL for unauthenticated calls and setup flows.
|
|
243
|
+
* Precedence: AMPERSEND_API_URL (hard bypass) > selected context's apiUrl > default.
|
|
82
244
|
*/
|
|
83
|
-
export function
|
|
84
|
-
|
|
85
|
-
|
|
245
|
+
export function getActiveApiUrl(opts = {}) {
|
|
246
|
+
return process.env.AMPERSEND_API_URL ?? getSelectedContext(opts)?.context.apiUrl ?? DEFAULT_API_URL;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Per-context status for the selected context, for error messages and `status`.
|
|
250
|
+
*/
|
|
251
|
+
export function getConfigStatus(opts = {}) {
|
|
252
|
+
const selected = getSelectedContext(opts);
|
|
253
|
+
if (!selected)
|
|
86
254
|
return { status: "not_initialized" };
|
|
87
|
-
}
|
|
88
|
-
return { status: config.status };
|
|
255
|
+
return { status: selected.context.status === "ready" ? "ready" : "pending_agent" };
|
|
89
256
|
}
|
|
90
257
|
/**
|
|
91
258
|
* Check if a pending approval has expired locally.
|
|
@@ -99,11 +266,62 @@ export function isPendingExpired(pending) {
|
|
|
99
266
|
export function computeApprovalExpiry() {
|
|
100
267
|
return new Date(Date.now() + APPROVAL_EXPIRY_MS).toISOString();
|
|
101
268
|
}
|
|
269
|
+
/** Empty V2 config used when starting from scratch. */
|
|
270
|
+
function emptyConfig() {
|
|
271
|
+
return { version: 2, contexts: {} };
|
|
272
|
+
}
|
|
273
|
+
/** Return a copy of `record` without `key` (avoids dynamic `delete`). */
|
|
274
|
+
function omitKey(record, key) {
|
|
275
|
+
const { [key]: _omitted, ...rest } = record;
|
|
276
|
+
return rest;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Production = the default URL, or no URL at all. Auto-derived context names get
|
|
280
|
+
* the URL host prepended only for non-production environments.
|
|
281
|
+
*/
|
|
282
|
+
export function isProductionUrl(apiUrl) {
|
|
283
|
+
return !apiUrl || apiUrl === DEFAULT_API_URL;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Auto-derive a context name from the agent key when `--context` is omitted.
|
|
287
|
+
* The base is `ctx-<4 hex of key address>`; non-production URLs prepend the host
|
|
288
|
+
* so contexts targeting different environments stay distinct (e.g.
|
|
289
|
+
* `api.sandbox.ampersend.ai-ctx-1a2b`). Not guaranteed unique on its own —
|
|
290
|
+
* callers go through `uniqueContextName` to disambiguate.
|
|
291
|
+
*/
|
|
292
|
+
export function autoContextName(apiUrl, keyAddress) {
|
|
293
|
+
const base = `ctx-${keyAddress.slice(2, 6).toLowerCase()}`;
|
|
294
|
+
if (isProductionUrl(apiUrl))
|
|
295
|
+
return base;
|
|
296
|
+
try {
|
|
297
|
+
return `${new URL(apiUrl).host}-${base}`;
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return base;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* An auto-derived context name guaranteed free in `config`. Appends `-2`, `-3`,
|
|
305
|
+
* … if the base name (or a prior counter) is already taken.
|
|
306
|
+
*/
|
|
307
|
+
export function uniqueContextName(config, apiUrl, keyAddress) {
|
|
308
|
+
const base = autoContextName(apiUrl, keyAddress);
|
|
309
|
+
if (!config.contexts[base])
|
|
310
|
+
return base;
|
|
311
|
+
for (let n = 2;; n++) {
|
|
312
|
+
const candidate = `${base}-${n}`;
|
|
313
|
+
if (!config.contexts[candidate])
|
|
314
|
+
return candidate;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
102
317
|
/**
|
|
103
|
-
* Set
|
|
104
|
-
*
|
|
318
|
+
* Set a context's identity directly using "agentKey:::agentAccount" format and
|
|
319
|
+
* make it active. With `--context <name>` the identity is written to that named
|
|
320
|
+
* context (creating or overwriting it); without one, a fresh auto-named context
|
|
321
|
+
* is minted. A context's `apiUrl` is fixed at creation — `setConfig` only sets
|
|
322
|
+
* it on a brand-new context, never edits an existing one's URL.
|
|
105
323
|
*/
|
|
106
|
-
export function setConfig(secret) {
|
|
324
|
+
export function setConfig(secret, opts = {}) {
|
|
107
325
|
const parts = secret.split(":::");
|
|
108
326
|
if (parts.length !== 2) {
|
|
109
327
|
return err("INVALID_FORMAT", 'Expected format: "agentKey:::agentAccount"');
|
|
@@ -115,88 +333,181 @@ export function setConfig(secret) {
|
|
|
115
333
|
if (!isAddress(agentAccount)) {
|
|
116
334
|
return err("INVALID_ADDRESS", "Invalid Ethereum address format for agent account");
|
|
117
335
|
}
|
|
118
|
-
|
|
119
|
-
writeConfig({
|
|
120
|
-
agentKey: agentKey,
|
|
121
|
-
agentAccount: agentAccount,
|
|
122
|
-
...(existing?.apiUrl ? { apiUrl: existing.apiUrl } : {}),
|
|
123
|
-
// Preserve pending approval if any
|
|
124
|
-
...(existing?.pendingApproval ? { pendingApproval: existing.pendingApproval } : {}),
|
|
125
|
-
});
|
|
126
|
-
const agentKeyAddress = privateKeyToAddress(agentKey);
|
|
127
|
-
return ok({
|
|
128
|
-
agentKeyAddress,
|
|
129
|
-
agentAccount,
|
|
130
|
-
status: "ready",
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Set API URL in config. Pass undefined to clear (revert to production default).
|
|
135
|
-
*/
|
|
136
|
-
export function setApiUrl(apiUrl) {
|
|
137
|
-
if (apiUrl != null) {
|
|
336
|
+
if (opts.apiUrl != null) {
|
|
138
337
|
try {
|
|
139
|
-
new URL(apiUrl);
|
|
338
|
+
new URL(opts.apiUrl);
|
|
140
339
|
}
|
|
141
340
|
catch {
|
|
142
341
|
return err("INVALID_URL", "Invalid URL format.");
|
|
143
342
|
}
|
|
144
343
|
}
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
344
|
+
const agentKeyAddress = privateKeyToAddress(agentKey);
|
|
345
|
+
const config = readConfig() ?? emptyConfig();
|
|
346
|
+
// Explicit --context names the target (create or overwrite); otherwise mint a
|
|
347
|
+
// fresh auto-named context.
|
|
348
|
+
const name = opts.name ?? uniqueContextName(config, opts.apiUrl, agentKeyAddress);
|
|
349
|
+
const existing = config.contexts[name];
|
|
350
|
+
// The URL is set only on a new context; an explicit opt on creation wins,
|
|
351
|
+
// otherwise inherit the URL an overwritten context already had. lasoToken is
|
|
352
|
+
// intentionally dropped: changing the identity invalidates a token stamped
|
|
353
|
+
// with the old key. createdAt is preserved when overwriting a named context.
|
|
354
|
+
const apiUrl = opts.apiUrl ?? existing?.apiUrl;
|
|
355
|
+
config.contexts[name] = {
|
|
356
|
+
status: "ready",
|
|
357
|
+
agentKey: agentKey,
|
|
358
|
+
agentAccount: agentAccount,
|
|
359
|
+
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
360
|
+
...(apiUrl ? { apiUrl } : {}),
|
|
361
|
+
};
|
|
362
|
+
config.activeContext = name;
|
|
363
|
+
writeConfig(config);
|
|
364
|
+
return ok({ agentKeyAddress, agentAccount, context: name, status: "ready" });
|
|
153
365
|
}
|
|
154
366
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
367
|
+
* Switch the active context without re-running setup. Errors if the name is
|
|
368
|
+
* unknown.
|
|
157
369
|
*/
|
|
158
|
-
export function
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
370
|
+
export function useContext(name) {
|
|
371
|
+
const config = readConfig();
|
|
372
|
+
const context = config?.contexts[name];
|
|
373
|
+
if (!config || !context) {
|
|
374
|
+
return err("UNKNOWN_CONTEXT", `No context named "${name}". Run "ampersend config status" to list contexts.`);
|
|
375
|
+
}
|
|
376
|
+
config.activeContext = name;
|
|
377
|
+
writeConfig(config);
|
|
378
|
+
return ok({ context: name, status: context.status === "ready" ? "ready" : "pending_agent" });
|
|
166
379
|
}
|
|
167
380
|
/**
|
|
168
|
-
*
|
|
381
|
+
* Delete a context. If it was the active one, the active selection is cleared.
|
|
382
|
+
* Errors if the name is unknown.
|
|
169
383
|
*/
|
|
170
|
-
export function
|
|
171
|
-
const
|
|
172
|
-
if (!
|
|
173
|
-
return;
|
|
174
|
-
|
|
175
|
-
|
|
384
|
+
export function removeContext(name) {
|
|
385
|
+
const config = readConfig();
|
|
386
|
+
if (!config || !config.contexts[name]) {
|
|
387
|
+
return err("UNKNOWN_CONTEXT", `No context named "${name}". Run "ampersend config status" to list contexts.`);
|
|
388
|
+
}
|
|
389
|
+
const wasActive = config.activeContext === name;
|
|
390
|
+
config.contexts = omitKey(config.contexts, name);
|
|
391
|
+
if (wasActive)
|
|
392
|
+
delete config.activeContext;
|
|
393
|
+
writeConfig(config);
|
|
394
|
+
return ok({ context: name, wasActive });
|
|
176
395
|
}
|
|
177
396
|
/**
|
|
178
|
-
*
|
|
179
|
-
* Called by `setup
|
|
397
|
+
* Create a `pending` context from a freshly-requested approval.
|
|
398
|
+
* Called by `setup start`. Makes it active unless `detach` is set.
|
|
180
399
|
*/
|
|
181
|
-
export function
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
400
|
+
export function startContext(name, pending, opts = {}) {
|
|
401
|
+
const config = readConfig() ?? emptyConfig();
|
|
402
|
+
config.contexts[name] = {
|
|
403
|
+
status: "pending",
|
|
404
|
+
agentKey: pending.agentKey,
|
|
405
|
+
token: pending.token,
|
|
406
|
+
expiresAt: pending.expiresAt,
|
|
407
|
+
createdAt: config.contexts[name]?.createdAt ?? new Date().toISOString(),
|
|
408
|
+
...(opts.apiUrl && !isProductionUrl(opts.apiUrl) ? { apiUrl: opts.apiUrl } : {}),
|
|
409
|
+
};
|
|
410
|
+
if (!opts.detach)
|
|
411
|
+
config.activeContext = name;
|
|
412
|
+
writeConfig(config);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Promote a `pending` context to `ready` (key stays, account filled in).
|
|
416
|
+
* Called by `setup finish`. Makes the context active.
|
|
417
|
+
*/
|
|
418
|
+
export function finishContext(name, agentAccount) {
|
|
419
|
+
const config = readConfig();
|
|
420
|
+
const context = config?.contexts[name];
|
|
421
|
+
if (!config || !context) {
|
|
422
|
+
return err("UNKNOWN_CONTEXT", `No context named "${name}".`);
|
|
185
423
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
agentKey: pendingApproval.agentKey,
|
|
192
|
-
agentAccount,
|
|
193
|
-
// pendingApproval intentionally omitted — cleared on promote
|
|
194
|
-
});
|
|
195
|
-
return ok({
|
|
196
|
-
agentKeyAddress,
|
|
197
|
-
agentAccount,
|
|
424
|
+
if (context.status !== "pending") {
|
|
425
|
+
return err("NOT_PENDING", `Context "${name}" is already ready. Use "ampersend config use ${name}" to select it.`);
|
|
426
|
+
}
|
|
427
|
+
const agentKeyAddress = privateKeyToAddress(context.agentKey);
|
|
428
|
+
config.contexts[name] = {
|
|
198
429
|
status: "ready",
|
|
199
|
-
|
|
430
|
+
agentKey: context.agentKey,
|
|
431
|
+
agentAccount,
|
|
432
|
+
createdAt: context.createdAt,
|
|
433
|
+
...(context.apiUrl ? { apiUrl: context.apiUrl } : {}),
|
|
434
|
+
};
|
|
435
|
+
config.activeContext = name;
|
|
436
|
+
writeConfig(config);
|
|
437
|
+
return ok({ agentKeyAddress, agentAccount, context: name, status: "ready" });
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Remove a pending context (e.g. when an approval is rejected). No-op if the
|
|
441
|
+
* context is missing or no longer pending.
|
|
442
|
+
*/
|
|
443
|
+
export function clearPendingContext(name) {
|
|
444
|
+
const config = readConfig();
|
|
445
|
+
const context = config?.contexts[name];
|
|
446
|
+
if (!config || !context || context.status !== "pending")
|
|
447
|
+
return;
|
|
448
|
+
config.contexts = omitKey(config.contexts, name);
|
|
449
|
+
if (config.activeContext === name)
|
|
450
|
+
delete config.activeContext;
|
|
451
|
+
writeConfig(config);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Cache a Laso Bearer token on the selected context, preserving the rest of the
|
|
455
|
+
* config. The token is stamped with the identity/URL it was minted under so
|
|
456
|
+
* `readLasoToken` can reject it after an env-var or context change.
|
|
457
|
+
*
|
|
458
|
+
* No-op when the selected context isn't a ready one (e.g. env-only credentials):
|
|
459
|
+
* the config file is the only cache, and we don't create one just to hold a token.
|
|
460
|
+
*/
|
|
461
|
+
export function storeLasoToken(token, opts = {}) {
|
|
462
|
+
const config = readConfig();
|
|
463
|
+
const selected = getSelectedContext(opts);
|
|
464
|
+
if (!config || selected?.context.status !== "ready")
|
|
465
|
+
return;
|
|
466
|
+
config.contexts[selected.name] = { ...selected.context, lasoToken: token };
|
|
467
|
+
writeConfig(config);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Read the selected context's cached Laso token, or null if there's no usable
|
|
471
|
+
* one. Treated as absent when: no token cached, expired, or its stamped
|
|
472
|
+
* `agentKey`/`apiUrl` no longer match the active credentials (covers env-var
|
|
473
|
+
* overrides). Self-correcting by construction.
|
|
474
|
+
*/
|
|
475
|
+
export function readLasoToken(active, opts = {}) {
|
|
476
|
+
const ctx = getSelectedContext(opts)?.context;
|
|
477
|
+
const stored = ctx?.status === "ready" ? ctx.lasoToken : undefined;
|
|
478
|
+
if (!stored)
|
|
479
|
+
return null;
|
|
480
|
+
if (new Date(stored.expiresAt).getTime() <= Date.now())
|
|
481
|
+
return null;
|
|
482
|
+
if (stored.agentKey !== active.agentKey)
|
|
483
|
+
return null;
|
|
484
|
+
// apiUrl absent on either side means production default; normalize so an
|
|
485
|
+
// explicit prod URL and an implicit one are treated as the same context.
|
|
486
|
+
const storedUrl = stored.apiUrl ?? DEFAULT_API_URL;
|
|
487
|
+
const activeUrl = active.apiUrl ?? DEFAULT_API_URL;
|
|
488
|
+
if (storedUrl !== activeUrl)
|
|
489
|
+
return null;
|
|
490
|
+
return stored;
|
|
491
|
+
}
|
|
492
|
+
/** Build a status summary for one context. */
|
|
493
|
+
function summarizeContext(name, context, activeName) {
|
|
494
|
+
const summary = {
|
|
495
|
+
name,
|
|
496
|
+
status: context.status === "ready" ? "ready" : "pending_agent",
|
|
497
|
+
active: name === activeName,
|
|
498
|
+
createdAt: context.createdAt,
|
|
499
|
+
agentKeyAddress: privateKeyToAddress(context.agentKey),
|
|
500
|
+
};
|
|
501
|
+
if (context.status === "ready") {
|
|
502
|
+
summary.agentAccount = context.agentAccount;
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
summary.pendingExpired = isPendingExpired(context);
|
|
506
|
+
}
|
|
507
|
+
if (context.apiUrl && context.apiUrl !== DEFAULT_API_URL) {
|
|
508
|
+
summary.apiUrl = context.apiUrl;
|
|
509
|
+
}
|
|
510
|
+
return summary;
|
|
200
511
|
}
|
|
201
512
|
/**
|
|
202
513
|
* Get current configuration status.
|
|
@@ -227,34 +538,34 @@ export function getStatus() {
|
|
|
227
538
|
// No env vars, check file
|
|
228
539
|
}
|
|
229
540
|
// Check config file
|
|
230
|
-
const config =
|
|
231
|
-
if (!config) {
|
|
541
|
+
const config = readConfig();
|
|
542
|
+
if (!config || Object.keys(config.contexts).length === 0) {
|
|
232
543
|
return ok({ status: "not_initialized", credentialSource: "none" });
|
|
233
544
|
}
|
|
234
|
-
//
|
|
235
|
-
const
|
|
236
|
-
|
|
545
|
+
// Oldest-first so `config status` lists contexts in a stable creation order.
|
|
546
|
+
const contexts = Object.entries(config.contexts)
|
|
547
|
+
.map(([name, ctx]) => summarizeContext(name, ctx, config.activeContext))
|
|
548
|
+
.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
549
|
+
// Hoist the active context's fields to the top level, reusing its summary
|
|
550
|
+
// (built above) rather than re-deriving the key address and account.
|
|
551
|
+
const activeSummary = contexts.find((c) => c.active);
|
|
237
552
|
const result = {
|
|
238
|
-
status:
|
|
553
|
+
status: activeSummary?.status ?? "not_initialized",
|
|
239
554
|
credentialSource: "file",
|
|
240
555
|
configPath: CONFIG_FILE,
|
|
556
|
+
...(config.activeContext ? { activeContext: config.activeContext } : {}),
|
|
557
|
+
contexts,
|
|
241
558
|
};
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (config.pendingApproval) {
|
|
253
|
-
const pendingKeyAddress = privateKeyToAddress(config.pendingApproval.agentKey);
|
|
254
|
-
result.pendingApproval = {
|
|
255
|
-
agentKeyAddress: pendingKeyAddress,
|
|
256
|
-
expired: isPendingExpired(config.pendingApproval),
|
|
257
|
-
};
|
|
559
|
+
if (activeSummary) {
|
|
560
|
+
if (activeSummary.agentKeyAddress)
|
|
561
|
+
result.agentKeyAddress = activeSummary.agentKeyAddress;
|
|
562
|
+
if (activeSummary.agentAccount)
|
|
563
|
+
result.agentAccount = activeSummary.agentAccount;
|
|
564
|
+
// Effective API URL for the active context (env var takes precedence).
|
|
565
|
+
const effectiveApiUrl = process.env.AMPERSEND_API_URL ?? activeSummary.apiUrl;
|
|
566
|
+
if (effectiveApiUrl && effectiveApiUrl !== DEFAULT_API_URL) {
|
|
567
|
+
result.apiUrl = effectiveApiUrl;
|
|
568
|
+
}
|
|
258
569
|
}
|
|
259
570
|
return ok(result);
|
|
260
571
|
}
|