@ampersend_ai/ampersend-sdk 0.0.25 → 0.0.27
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.d.ts +65 -66
- package/dist/ampersend/agent.d.ts.map +1 -1
- package/dist/ampersend/agent.js +32 -22
- package/dist/ampersend/agent.js.map +1 -1
- package/dist/ampersend/approval.js.map +1 -1
- package/dist/ampersend/client.d.ts +2 -2
- package/dist/ampersend/client.d.ts.map +1 -1
- 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/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/types.d.ts +194 -318
- package/dist/ampersend/types.d.ts.map +1 -1
- package/dist/ampersend/types.js +134 -123
- 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/commands/agent.d.ts +3 -1
- package/dist/cli/commands/agent.d.ts.map +1 -1
- package/dist/cli/commands/agent.js +44 -22
- package/dist/cli/commands/agent.js.map +1 -1
- package/dist/cli/commands/card.d.ts +19 -16
- package/dist/cli/commands/card.d.ts.map +1 -1
- package/dist/cli/commands/card.js +21 -16
- package/dist/cli/commands/card.js.map +1 -1
- 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 +1 -0
- package/dist/cli/commands/fetch.d.ts.map +1 -1
- package/dist/cli/commands/fetch.js +3 -2
- package/dist/cli/commands/fetch.js.map +1 -1
- package/dist/cli/commands/marketplace.d.ts +5 -2
- package/dist/cli/commands/marketplace.d.ts.map +1 -1
- package/dist/cli/commands/marketplace.js +12 -12
- 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/config.d.ts +190 -68
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +357 -150
- package/dist/cli/config.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/cli/config.js
CHANGED
|
@@ -11,7 +11,7 @@ import { err, ok } from "./envelope.js";
|
|
|
11
11
|
* Returns an `err` envelope when the local config isn't ready, so the
|
|
12
12
|
* caller can print it and exit.
|
|
13
13
|
*/
|
|
14
|
-
export function loadCredentials() {
|
|
14
|
+
export function loadCredentials(opts = {}) {
|
|
15
15
|
try {
|
|
16
16
|
const envConfig = parseEnvConfig();
|
|
17
17
|
return {
|
|
@@ -26,19 +26,20 @@ export function loadCredentials() {
|
|
|
26
26
|
catch {
|
|
27
27
|
// Fall back to config file
|
|
28
28
|
}
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
|
|
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;
|
|
32
33
|
return {
|
|
33
34
|
ok: true,
|
|
34
35
|
credentials: {
|
|
35
|
-
agentAccount:
|
|
36
|
-
agentKey:
|
|
36
|
+
agentAccount: selected.context.agentAccount,
|
|
37
|
+
agentKey: selected.context.agentKey,
|
|
37
38
|
...(apiUrl ? { apiUrl } : {}),
|
|
38
39
|
},
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
|
-
const status =
|
|
42
|
+
const status = getConfigStatus(opts).status;
|
|
42
43
|
const code = status === "not_initialized" ? "NOT_CONFIGURED" : "SETUP_INCOMPLETE";
|
|
43
44
|
return {
|
|
44
45
|
ok: false,
|
|
@@ -49,19 +50,22 @@ export function loadCredentials() {
|
|
|
49
50
|
const CONFIG_DIR = join(homedir(), ".ampersend");
|
|
50
51
|
export const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
51
52
|
/** Current config version */
|
|
52
|
-
const CONFIG_VERSION =
|
|
53
|
+
const CONFIG_VERSION = 2;
|
|
53
54
|
/** Hard-coded approval expiration: 30 minutes */
|
|
54
55
|
const APPROVAL_EXPIRY_MS = 30 * 60 * 1000;
|
|
55
56
|
/** Default API URL (production) */
|
|
56
57
|
export const DEFAULT_API_URL = "https://api.ampersend.ai";
|
|
57
|
-
const HexString = Schema.TemplateLiteral(Schema.Literal("0x"), Schema.String);
|
|
58
|
+
const HexString = Schema.TemplateLiteral([Schema.Literal("0x"), Schema.String]);
|
|
58
59
|
/**
|
|
59
60
|
* Cached Laso Bearer token for `card details`/`list`, so a warm read costs
|
|
60
61
|
* nothing. Stamped with the identity (`agentKey`) and `apiUrl` it was minted
|
|
61
62
|
* under: `readLasoToken` treats it as absent if either no longer matches the
|
|
62
|
-
* active
|
|
63
|
+
* active context (covers env-var overrides) or it has expired. Self-correcting,
|
|
63
64
|
* so the identity/URL write paths only need to drop it, not re-thread it.
|
|
64
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.
|
|
68
|
+
*
|
|
65
69
|
* Schema is the source of truth; the `LasoToken` type is derived from it so the
|
|
66
70
|
* two can't drift (the same pattern as the schemas in `src/ampersend/`).
|
|
67
71
|
*/
|
|
@@ -71,11 +75,41 @@ const LasoTokenSchema = Schema.Struct({
|
|
|
71
75
|
agentKey: HexString,
|
|
72
76
|
apiUrl: Schema.optional(Schema.String),
|
|
73
77
|
});
|
|
74
|
-
|
|
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`.
|
|
75
86
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
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({
|
|
79
113
|
version: Schema.Literal(1),
|
|
80
114
|
agentKey: Schema.optional(HexString),
|
|
81
115
|
agentAccount: Schema.optional(HexString),
|
|
@@ -87,6 +121,51 @@ const StoredConfigSchema = Schema.Struct({
|
|
|
87
121
|
})),
|
|
88
122
|
lasoToken: Schema.optional(LasoTokenSchema),
|
|
89
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
|
+
}
|
|
90
169
|
/**
|
|
91
170
|
* Ensure config directory exists with secure permissions
|
|
92
171
|
*/
|
|
@@ -96,7 +175,7 @@ function ensureConfigDir() {
|
|
|
96
175
|
}
|
|
97
176
|
}
|
|
98
177
|
/**
|
|
99
|
-
* Read config file if it exists.
|
|
178
|
+
* Read config file if it exists, normalized to the V2 context model.
|
|
100
179
|
* Returns null if the file is missing or corrupt.
|
|
101
180
|
*/
|
|
102
181
|
export function readConfig() {
|
|
@@ -106,7 +185,8 @@ export function readConfig() {
|
|
|
106
185
|
const content = readFileSync(CONFIG_FILE, "utf-8");
|
|
107
186
|
try {
|
|
108
187
|
const parsed = JSON.parse(content);
|
|
109
|
-
|
|
188
|
+
const decoded = Schema.decodeUnknownSync(StoredConfigSchema)(parsed);
|
|
189
|
+
return decoded.version === 1 ? migrateV1toV2(decoded) : decoded;
|
|
110
190
|
}
|
|
111
191
|
catch {
|
|
112
192
|
// Corrupt or unrecognised config — treat as absent so commands can re-initialise
|
|
@@ -114,34 +194,65 @@ export function readConfig() {
|
|
|
114
194
|
}
|
|
115
195
|
}
|
|
116
196
|
/**
|
|
117
|
-
*
|
|
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.
|
|
118
214
|
*/
|
|
119
215
|
export function writeConfig(config) {
|
|
120
216
|
ensureConfigDir();
|
|
121
|
-
const
|
|
122
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(
|
|
217
|
+
const pruned = prunePendingExpired({ version: CONFIG_VERSION, ...config });
|
|
218
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(pruned, null, 2), { mode: 0o600 });
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The context name to use this invocation: `--context` flag > `AMPERSEND_CONTEXT`
|
|
222
|
+
* env > persisted `activeContext`. Returns undefined if none resolves.
|
|
223
|
+
*/
|
|
224
|
+
export function resolveContextName(opts = {}) {
|
|
225
|
+
return opts.context ?? process.env.AMPERSEND_CONTEXT ?? readConfig()?.activeContext ?? undefined;
|
|
123
226
|
}
|
|
124
227
|
/**
|
|
125
|
-
*
|
|
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.
|
|
126
230
|
*/
|
|
127
|
-
export function
|
|
128
|
-
const
|
|
129
|
-
|
|
231
|
+
export function getSelectedContext(opts = {}) {
|
|
232
|
+
const config = readConfig();
|
|
233
|
+
const name = resolveContextName(opts);
|
|
234
|
+
if (!config || !name)
|
|
130
235
|
return null;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return {
|
|
236
|
+
const context = config.contexts[name];
|
|
237
|
+
if (!context)
|
|
238
|
+
return null;
|
|
239
|
+
return { name, context };
|
|
135
240
|
}
|
|
136
241
|
/**
|
|
137
|
-
*
|
|
242
|
+
* Effective API URL for unauthenticated calls and setup flows.
|
|
243
|
+
* Precedence: AMPERSEND_API_URL (hard bypass) > selected context's apiUrl > default.
|
|
138
244
|
*/
|
|
139
|
-
export function
|
|
140
|
-
|
|
141
|
-
|
|
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)
|
|
142
254
|
return { status: "not_initialized" };
|
|
143
|
-
}
|
|
144
|
-
return { status: config.status };
|
|
255
|
+
return { status: selected.context.status === "ready" ? "ready" : "pending_agent" };
|
|
145
256
|
}
|
|
146
257
|
/**
|
|
147
258
|
* Check if a pending approval has expired locally.
|
|
@@ -155,11 +266,62 @@ export function isPendingExpired(pending) {
|
|
|
155
266
|
export function computeApprovalExpiry() {
|
|
156
267
|
return new Date(Date.now() + APPROVAL_EXPIRY_MS).toISOString();
|
|
157
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
|
+
}
|
|
158
317
|
/**
|
|
159
|
-
* Set
|
|
160
|
-
*
|
|
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.
|
|
161
323
|
*/
|
|
162
|
-
export function setConfig(secret) {
|
|
324
|
+
export function setConfig(secret, opts = {}) {
|
|
163
325
|
const parts = secret.split(":::");
|
|
164
326
|
if (parts.length !== 2) {
|
|
165
327
|
return err("INVALID_FORMAT", 'Expected format: "agentKey:::agentAccount"');
|
|
@@ -171,123 +333,148 @@ export function setConfig(secret) {
|
|
|
171
333
|
if (!isAddress(agentAccount)) {
|
|
172
334
|
return err("INVALID_ADDRESS", "Invalid Ethereum address format for agent account");
|
|
173
335
|
}
|
|
174
|
-
|
|
175
|
-
// lasoToken intentionally not carried over: changing the active identity
|
|
176
|
-
// invalidates any cached Laso token (it's stamped with the old agentKey).
|
|
177
|
-
writeConfig({
|
|
178
|
-
agentKey: agentKey,
|
|
179
|
-
agentAccount: agentAccount,
|
|
180
|
-
...(existing?.apiUrl ? { apiUrl: existing.apiUrl } : {}),
|
|
181
|
-
// Preserve pending approval if any
|
|
182
|
-
...(existing?.pendingApproval ? { pendingApproval: existing.pendingApproval } : {}),
|
|
183
|
-
});
|
|
184
|
-
const agentKeyAddress = privateKeyToAddress(agentKey);
|
|
185
|
-
return ok({
|
|
186
|
-
agentKeyAddress,
|
|
187
|
-
agentAccount,
|
|
188
|
-
status: "ready",
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Set API URL in config. Pass undefined to clear (revert to production default).
|
|
193
|
-
*/
|
|
194
|
-
export function setApiUrl(apiUrl) {
|
|
195
|
-
if (apiUrl != null) {
|
|
336
|
+
if (opts.apiUrl != null) {
|
|
196
337
|
try {
|
|
197
|
-
new URL(apiUrl);
|
|
338
|
+
new URL(opts.apiUrl);
|
|
198
339
|
}
|
|
199
340
|
catch {
|
|
200
341
|
return err("INVALID_URL", "Invalid URL format.");
|
|
201
342
|
}
|
|
202
343
|
}
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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" });
|
|
214
365
|
}
|
|
215
366
|
/**
|
|
216
|
-
*
|
|
217
|
-
*
|
|
367
|
+
* Switch the active context without re-running setup. Errors if the name is
|
|
368
|
+
* unknown.
|
|
218
369
|
*/
|
|
219
|
-
export function
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
pendingApproval: pending,
|
|
229
|
-
});
|
|
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" });
|
|
230
379
|
}
|
|
231
380
|
/**
|
|
232
|
-
*
|
|
381
|
+
* Delete a context. If it was the active one, the active selection is cleared.
|
|
382
|
+
* Errors if the name is unknown.
|
|
233
383
|
*/
|
|
234
|
-
export function
|
|
235
|
-
const
|
|
236
|
-
if (!
|
|
237
|
-
return;
|
|
238
|
-
|
|
239
|
-
|
|
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 });
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Create a `pending` context from a freshly-requested approval.
|
|
398
|
+
* Called by `setup start`. Makes it active unless `detach` is set.
|
|
399
|
+
*/
|
|
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);
|
|
240
413
|
}
|
|
241
414
|
/**
|
|
242
|
-
* Promote a pending
|
|
243
|
-
* Called by `setup finish
|
|
415
|
+
* Promote a `pending` context to `ready` (key stays, account filled in).
|
|
416
|
+
* Called by `setup finish`. Makes the context active.
|
|
244
417
|
*/
|
|
245
|
-
export function
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
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}".`);
|
|
249
423
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
// Promote: pending key becomes active, pending cleared
|
|
256
|
-
writeConfig({
|
|
257
|
-
...rest,
|
|
258
|
-
agentKey: pendingApproval.agentKey,
|
|
259
|
-
agentAccount,
|
|
260
|
-
// pendingApproval intentionally omitted — cleared on promote
|
|
261
|
-
});
|
|
262
|
-
return ok({
|
|
263
|
-
agentKeyAddress,
|
|
264
|
-
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] = {
|
|
265
429
|
status: "ready",
|
|
266
|
-
|
|
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);
|
|
267
452
|
}
|
|
268
453
|
/**
|
|
269
|
-
* Cache a Laso Bearer token, preserving the rest of the
|
|
270
|
-
* The token is stamped with the identity/URL it was minted under so
|
|
271
|
-
* `readLasoToken` can reject it after an env-var or
|
|
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.
|
|
272
457
|
*
|
|
273
|
-
* No-op when
|
|
274
|
-
* config file is the only cache, and we don't create one just to hold a token.
|
|
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.
|
|
275
460
|
*/
|
|
276
|
-
export function storeLasoToken(token) {
|
|
277
|
-
const
|
|
278
|
-
|
|
461
|
+
export function storeLasoToken(token, opts = {}) {
|
|
462
|
+
const config = readConfig();
|
|
463
|
+
const selected = getSelectedContext(opts);
|
|
464
|
+
if (!config || selected?.context.status !== "ready")
|
|
279
465
|
return;
|
|
280
|
-
|
|
281
|
-
writeConfig(
|
|
466
|
+
config.contexts[selected.name] = { ...selected.context, lasoToken: token };
|
|
467
|
+
writeConfig(config);
|
|
282
468
|
}
|
|
283
469
|
/**
|
|
284
|
-
* Read the cached Laso token, or null if there's no usable
|
|
285
|
-
* absent when: no token cached, expired, or its stamped
|
|
286
|
-
* no longer match the active credentials (covers env-var
|
|
287
|
-
*
|
|
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.
|
|
288
474
|
*/
|
|
289
|
-
export function readLasoToken(active) {
|
|
290
|
-
const
|
|
475
|
+
export function readLasoToken(active, opts = {}) {
|
|
476
|
+
const ctx = getSelectedContext(opts)?.context;
|
|
477
|
+
const stored = ctx?.status === "ready" ? ctx.lasoToken : undefined;
|
|
291
478
|
if (!stored)
|
|
292
479
|
return null;
|
|
293
480
|
if (new Date(stored.expiresAt).getTime() <= Date.now())
|
|
@@ -302,6 +489,26 @@ export function readLasoToken(active) {
|
|
|
302
489
|
return null;
|
|
303
490
|
return stored;
|
|
304
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;
|
|
511
|
+
}
|
|
305
512
|
/**
|
|
306
513
|
* Get current configuration status.
|
|
307
514
|
* Checks env vars first (takes precedence), then config file.
|
|
@@ -331,34 +538,34 @@ export function getStatus() {
|
|
|
331
538
|
// No env vars, check file
|
|
332
539
|
}
|
|
333
540
|
// Check config file
|
|
334
|
-
const config =
|
|
335
|
-
if (!config) {
|
|
541
|
+
const config = readConfig();
|
|
542
|
+
if (!config || Object.keys(config.contexts).length === 0) {
|
|
336
543
|
return ok({ status: "not_initialized", credentialSource: "none" });
|
|
337
544
|
}
|
|
338
|
-
//
|
|
339
|
-
const
|
|
340
|
-
|
|
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);
|
|
341
552
|
const result = {
|
|
342
|
-
status:
|
|
553
|
+
status: activeSummary?.status ?? "not_initialized",
|
|
343
554
|
credentialSource: "file",
|
|
344
555
|
configPath: CONFIG_FILE,
|
|
556
|
+
...(config.activeContext ? { activeContext: config.activeContext } : {}),
|
|
557
|
+
contexts,
|
|
345
558
|
};
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (config.pendingApproval) {
|
|
357
|
-
const pendingKeyAddress = privateKeyToAddress(config.pendingApproval.agentKey);
|
|
358
|
-
result.pendingApproval = {
|
|
359
|
-
agentKeyAddress: pendingKeyAddress,
|
|
360
|
-
expired: isPendingExpired(config.pendingApproval),
|
|
361
|
-
};
|
|
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
|
+
}
|
|
362
569
|
}
|
|
363
570
|
return ok(result);
|
|
364
571
|
}
|