@dfm-fi/agent 0.2.1942 → 0.2.1958

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.
@@ -163,9 +163,10 @@ export declare class DfmApiClient {
163
163
  /**
164
164
  * `POST /agent/operating/float/withdraw` — sweep the float back to its OWNER.
165
165
  *
166
- * The destination is pinned server-side to the authenticated wallet, so this
167
- * cannot send anywhere else. Works past a stale rebalancer stamp and a user
168
- * expiry (the sweep carve-outs) — the float must never be trapped.
166
+ * The agent never names a destination: the API resolves the owner's SAVED
167
+ * withdrawal wallet first and falls back to the authenticated wallet, so
168
+ * this cannot send anywhere else. Works past a stale rebalancer stamp and a
169
+ * user expiry (the sweep carve-outs) — the float must never be trapped.
169
170
  */
170
171
  /**
171
172
  * `GET /agent/operating/assets` — every token the app wallet holds.
@@ -187,9 +188,10 @@ export declare class DfmApiClient {
187
188
  /**
188
189
  * `POST /agent/operating/assets/withdraw` — move ONE mint back to the owner.
189
190
  *
190
- * `destination` is deliberately NOT exposed here: the API defaults it to the
191
- * authenticated wallet, and an agent that could name a destination would be
192
- * a drain vector for anything that can put words in its input.
191
+ * `destination` is deliberately NOT exposed here: the API resolves the
192
+ * owner's saved withdrawal wallet first, falling back to the authenticated
193
+ * wallet — an agent that could name a destination would be a drain vector
194
+ * for anything that can put words in its input.
193
195
  */
194
196
  withdrawAppWalletAsset(mint: string, amount?: number | string): Promise<{
195
197
  signature: string;
@@ -251,6 +253,12 @@ export declare class DfmApiClient {
251
253
  * and signs it behind both cap layers plus the tx-assert simulation. Used
252
254
  * when the MCP has an API key but no keypair; see `keyless.ts` for when that
253
255
  * applies and why it never falls back to local signing.
256
+ *
257
+ * The idempotency key is PINNED to the operation itself (tool + canonical
258
+ * args, hashed) rather than a fresh UUID: when the response is lost — a 504
259
+ * the server outlived, a dropped socket — the agent's retry carries the SAME
260
+ * key, and the server returns the already-running/finished job instead of
261
+ * executing a second time (the two-vaults-two-fees bug, A7).
254
262
  */
255
263
  executeViaOperatingWallet(tool: string, args: Record<string, unknown>): Promise<{
256
264
  signature?: string;
@@ -323,6 +331,8 @@ export declare class DfmApiClient {
323
331
  assets: CreateAssetInput[];
324
332
  description?: string;
325
333
  tags?: number[];
334
+ /** Let DFM announce this fund publicly on X/Telegram/Moltbook (default false). */
335
+ announceOptIn?: boolean;
326
336
  }): Promise<CreatePrepareResponse>;
327
337
  /**
328
338
  * `POST /vaults/create/confirm` — record the vault doc after the create tx
@@ -13,13 +13,42 @@
13
13
  * NEVER sees the secret key — signing is delegated to `signing.ts` via the
14
14
  * write methods, and auth is delegated to `SiwsSession`.
15
15
  */
16
- import { randomUUID } from 'node:crypto';
16
+ import { createHash, randomUUID } from 'node:crypto';
17
+ import { dfmClientHeaders } from './client-header.js';
17
18
  const BASE58_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
18
19
  function requireBase58(addr, label) {
19
20
  if (!BASE58_RE.test(addr)) {
20
21
  throw new Error(`Invalid base58 ${label}: ${addr}`);
21
22
  }
22
23
  }
24
+ /**
25
+ * Canonical JSON for an operation id: object keys sorted recursively, so two
26
+ * calls that MEAN the same write hash identically regardless of key order.
27
+ * Money fields travel as strings server-side, so no number-precision games.
28
+ */
29
+ function stableStringify(value) {
30
+ if (value === null || typeof value !== 'object')
31
+ return JSON.stringify(value) ?? 'null';
32
+ if (Array.isArray(value))
33
+ return `[${value.map(stableStringify).join(',')}]`;
34
+ const entries = Object.keys(value)
35
+ .sort()
36
+ .map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`);
37
+ return `{${entries.join(',')}}`;
38
+ }
39
+ /**
40
+ * The deterministic operation id for ONE keyless write (A7): same tool + same
41
+ * args → same id, so a retry after a LOST response names the first attempt
42
+ * instead of executing twice. This is what "the same logical operation" means
43
+ * here — a genuinely NEW write with byte-identical args is deliberately
44
+ * treated as a retry; change an argument to make a new operation.
45
+ */
46
+ function operatingOperationKey(tool, args) {
47
+ const digest = createHash('sha256').update(`${tool}\n${stableStringify(args)}`).digest('hex');
48
+ return `opexec-${digest}`;
49
+ }
50
+ /** Launch-surface label (`launchedVia: 'mcp'` on the API side) — never a credential. */
51
+ const CLIENT_HEADERS = dfmClientHeaders();
23
52
  export class DfmApiClient {
24
53
  session;
25
54
  baseUrl;
@@ -31,7 +60,7 @@ export class DfmApiClient {
31
60
  async publicRequest(path, init) {
32
61
  const res = await fetch(`${this.baseUrl}${path}`, {
33
62
  ...init,
34
- headers: { 'Content-Type': 'application/json', ...(init?.headers ?? {}) },
63
+ headers: { 'Content-Type': 'application/json', ...CLIENT_HEADERS, ...(init?.headers ?? {}) },
35
64
  });
36
65
  if (!res.ok) {
37
66
  const body = await res.text().catch(() => '');
@@ -49,6 +78,7 @@ export class DfmApiClient {
49
78
  const idempotencyKey = mutating ? (pinnedKey ?? randomUUID()) : undefined;
50
79
  const common = (extra) => ({
51
80
  'Content-Type': 'application/json',
81
+ ...CLIENT_HEADERS,
52
82
  ...extra,
53
83
  ...(idempotencyKey ? { 'X-Idempotency-Key': idempotencyKey } : {}),
54
84
  ...(reqInit.headers ?? {}),
@@ -58,8 +88,10 @@ export class DfmApiClient {
58
88
  // resolves server-side to its owner wallet plus the credential's caps.
59
89
  // That matters because a leaked keypair IS the wallet, whereas a leaked API
60
90
  // key is revocable and capped (read-only unless explicitly widened).
61
- // There is no re-auth loop here — a key does not expire; a 401 means it was
62
- // revoked, and silently retrying a revoked credential would be wrong.
91
+ // There is no re-auth loop here — a key cannot be refreshed from the MCP
92
+ // side. A 401 means the key was revoked OR expired (the server enforces
93
+ // `expiresAt`); silently retrying a dead credential would be wrong either
94
+ // way, so the caller is told to mint a new one.
63
95
  const apiKey = sessionOnly ? undefined : process.env.DFM_API_KEY?.trim();
64
96
  if (apiKey) {
65
97
  const res = await fetch(`${this.baseUrl}${path}`, {
@@ -288,9 +320,10 @@ export class DfmApiClient {
288
320
  /**
289
321
  * `POST /agent/operating/float/withdraw` — sweep the float back to its OWNER.
290
322
  *
291
- * The destination is pinned server-side to the authenticated wallet, so this
292
- * cannot send anywhere else. Works past a stale rebalancer stamp and a user
293
- * expiry (the sweep carve-outs) — the float must never be trapped.
323
+ * The agent never names a destination: the API resolves the owner's SAVED
324
+ * withdrawal wallet first and falls back to the authenticated wallet, so
325
+ * this cannot send anywhere else. Works past a stale rebalancer stamp and a
326
+ * user expiry (the sweep carve-outs) — the float must never be trapped.
294
327
  */
295
328
  /**
296
329
  * `GET /agent/operating/assets` — every token the app wallet holds.
@@ -304,9 +337,10 @@ export class DfmApiClient {
304
337
  /**
305
338
  * `POST /agent/operating/assets/withdraw` — move ONE mint back to the owner.
306
339
  *
307
- * `destination` is deliberately NOT exposed here: the API defaults it to the
308
- * authenticated wallet, and an agent that could name a destination would be
309
- * a drain vector for anything that can put words in its input.
340
+ * `destination` is deliberately NOT exposed here: the API resolves the
341
+ * owner's saved withdrawal wallet first, falling back to the authenticated
342
+ * wallet — an agent that could name a destination would be a drain vector
343
+ * for anything that can put words in its input.
310
344
  */
311
345
  async withdrawAppWalletAsset(mint, amount) {
312
346
  return this.authRequest('/agent/operating/assets/withdraw', {
@@ -361,11 +395,18 @@ export class DfmApiClient {
361
395
  * and signs it behind both cap layers plus the tx-assert simulation. Used
362
396
  * when the MCP has an API key but no keypair; see `keyless.ts` for when that
363
397
  * applies and why it never falls back to local signing.
398
+ *
399
+ * The idempotency key is PINNED to the operation itself (tool + canonical
400
+ * args, hashed) rather than a fresh UUID: when the response is lost — a 504
401
+ * the server outlived, a dropped socket — the agent's retry carries the SAME
402
+ * key, and the server returns the already-running/finished job instead of
403
+ * executing a second time (the two-vaults-two-fees bug, A7).
364
404
  */
365
405
  async executeViaOperatingWallet(tool, args) {
366
406
  const first = await this.authRequest('/agent/operating/execute', {
367
407
  method: 'POST',
368
408
  body: JSON.stringify({ tool, args }),
409
+ idempotencyKey: operatingOperationKey(tool, args),
369
410
  });
370
411
  // Finished inside the server's inline window — the shape this method has
371
412
  // always returned.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `x-dfm-client: mcp/<version>` on every DFM API call this package makes.
3
+ *
4
+ * The API stamps it onto a launched fund as `launchedVia: 'mcp'` (marketing:
5
+ * how many funds AI agents launch through the MCP). It is a LABEL only — the
6
+ * server never grants, widens or skips anything because of it.
7
+ */
8
+ export declare const DFM_CLIENT_HEADER = "x-dfm-client";
9
+ export declare function readPackageVersion(): string;
10
+ export declare function dfmClientHeaders(version?: string): Record<string, string>;
11
+ //# sourceMappingURL=client-header.d.ts.map
@@ -0,0 +1,25 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /**
5
+ * `x-dfm-client: mcp/<version>` on every DFM API call this package makes.
6
+ *
7
+ * The API stamps it onto a launched fund as `launchedVia: 'mcp'` (marketing:
8
+ * how many funds AI agents launch through the MCP). It is a LABEL only — the
9
+ * server never grants, widens or skips anything because of it.
10
+ */
11
+ export const DFM_CLIENT_HEADER = 'x-dfm-client';
12
+ export function readPackageVersion() {
13
+ try {
14
+ // dist/client-header.js → ../package.json (same layout in the published tarball).
15
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
16
+ return JSON.parse(readFileSync(pkgPath, 'utf8')).version ?? '0.0.0';
17
+ }
18
+ catch {
19
+ return '0.0.0';
20
+ }
21
+ }
22
+ export function dfmClientHeaders(version = readPackageVersion()) {
23
+ return { [DFM_CLIENT_HEADER]: `mcp/${version}` };
24
+ }
25
+ //# sourceMappingURL=client-header.js.map
@@ -38,6 +38,8 @@ export interface LaunchVaultParams {
38
38
  /** Category enum (0-7, 255). Defaults to 255 (uncategorized/other). */
39
39
  category?: number;
40
40
  description?: string;
41
+ /** Let DFM announce this fund publicly on X/Telegram/Moltbook (default false). */
42
+ announceOptIn?: boolean;
41
43
  }
42
44
  export interface LaunchVaultResult {
43
45
  ok: boolean;
@@ -93,6 +93,7 @@ export async function runLaunchVault(config, api, params) {
93
93
  category: params.category ?? 255,
94
94
  assets: params.assets,
95
95
  description: params.description,
96
+ ...(params.announceOptIn === true && { announceOptIn: true }),
96
97
  });
97
98
  // 2. SIGN locally + 3. SUBMIT.
98
99
  const signed = signBase64Transaction(config, prepared.transaction);
@@ -889,27 +889,19 @@ async function keylessWrite(tool, args) {
889
889
  // ─── WRITE TOOL: launch_vault (create DTF) ────────────────────────────────
890
890
  server.registerTool('launch_vault', {
891
891
  title: 'Launch a fund',
892
- description: 'REAL-MONEY (gated). Create a new fund. WHO PAYS: your APP WALLET ' +
893
- '(the Privy operating wallet) signs and funds this whenever you have an ' +
894
- 'active one — that is where your money is, and it becomes the vault admin. ' +
895
- 'An app wallet is REQUIRED here: with no active one this tool refuses ' +
896
- 'instead of signing with a local keypair, because the creator is fixed ' +
897
- 'onchain forever and is what registers the fund to you — a launch signed ' +
898
- 'by any other wallet is a fund nobody can claim. Anyone can get an app ' +
899
- 'wallet (no allowlist); this is an identity rule, not a permission one. Check ' +
900
- '`app_wallet` first if a write is refused for funds. `assets` is the basket — each ' +
901
- '{ mint, symbol, allocationBps }; allocationBps MUST sum to exactly 10000, ' +
902
- '≤10 assets (mainnet cap). Live factory/fund-tier rules decide mint eligibility; ' +
903
- 'a freezable mint is accepted in production v1 but carries issuer-lock risk. Fees are factory-global ' +
904
- 'and read live by DFM — the creator cannot set them. After the vault is recorded, ' +
905
- 'the share mint automatically gets ' +
906
- 'Metaplex token metadata (name/symbol/uri) so explorers + wallets show the ' +
907
- 'Fund instead of "Unknown Token" (#47, best-effort + non-fatal); pass `metadata:false` ' +
908
- 'to skip its unrecoverable rent; the API preflight reports the current SOL requirement ' +
909
- 'and attach it later from the vault page. ' +
910
- 'Names and symbols must be unique among active DFM funds; closed funds release them. ' +
911
- 'Name matching ignores case and repeated whitespace; symbol matching ignores case. ' +
912
- 'Returns the vault address — fund the first deposit ≥ ~$12.',
892
+ description: 'REAL-MONEY (gated). Create a new fund. Your APP WALLET signs, pays and ' +
893
+ 'becomes the vault admin; with no active app wallet this tool refuses rather ' +
894
+ 'than sign with a local keypair (the creator is fixed onchain forever and is ' +
895
+ 'what registers the fund to you). Any wallet can get one — an identity rule, ' +
896
+ 'not a permission. Check `app_wallet` if a write is refused for funds. ' +
897
+ '`assets` = [{ mint, symbol, allocationBps }], allocationBps summing to exactly ' +
898
+ '10000, 2–10 assets. Live factory rules decide mint eligibility; freezable ' +
899
+ 'mints are accepted but carry issuer-lock risk. Fees are factory-global, read ' +
900
+ 'live; the creator cannot set them. The share mint gets Metaplex metadata so ' +
901
+ 'wallets show the fund (#47, non-fatal); `metadata:false` skips its ' +
902
+ 'unrecoverable rent (attach it later from the vault page). Names and symbols ' +
903
+ 'must be unique among active funds (case-insensitive). Returns the vault ' +
904
+ 'address — fund the first deposit ≥ ~$12.',
913
905
  inputSchema: {
914
906
  name: z.string().min(1).max(32),
915
907
  symbol: z
@@ -931,19 +923,27 @@ server.registerTool('launch_vault', {
931
923
  // never comes back, and the SOL floor drops from ~0.045 to ~0.03 without
932
924
  // it. The vault page can attach it later (creator-signed).
933
925
  metadata: z.boolean().optional(),
926
+ announceOptIn: z
927
+ .boolean()
928
+ .optional()
929
+ .describe('Let DFM announce this fund publicly on X/Telegram/Moltbook. Default false (no announcement).'),
934
930
  confirmPhrase: confirmPhraseField,
935
931
  },
936
932
  annotations: { title: 'DFM launch vault', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
937
933
  },
938
934
  // REVIEW: real-money path — parent must review before enabling.
939
- async ({ name, symbol, assets, category, description, metadata, confirmPhrase }) => {
935
+ async ({ name, symbol, assets, category, description, metadata, announceOptIn, confirmPhrase }) => {
940
936
  const gate = confirmGate('launch_vault', confirmPhrase);
941
937
  if (!gate.ok)
942
938
  return gate.result;
943
939
  const write = writeGate();
944
940
  if (!write.ok)
945
941
  return write.result;
946
- return keylessWrite('launch_vault', { name, symbol, assets, category, description, ...(metadata === false && { metadata }) });
942
+ return keylessWrite('launch_vault', {
943
+ name, symbol, assets, category, description,
944
+ ...(metadata === false && { metadata }),
945
+ ...(announceOptIn === true && { announceOptIn }),
946
+ });
947
947
  });
948
948
  // ─── WRITE TOOL: deposit (zap-in) ─────────────────────────────────────────
949
949
  server.registerTool('deposit', {
@@ -1530,26 +1530,18 @@ server.registerTool('init_rebalancer', {
1530
1530
  // `/execute/prepare`, just off the agent surface.)
1531
1531
  server.registerTool('rebalance_to_targets', {
1532
1532
  title: 'Rebalance a vault to its configured target weights',
1533
- description: 'THE MANAGER\'S RECURRING DUTY, not a convenience: a fund\'s config is the promise, its holdings are the ' +
1534
- 'reality, and prices pull them apart every day. A drifted fund no longer holds what its depositors bought, ' +
1535
- 'and share price settles onchain from the ACTUAL basket, so every new deposit mints against the drift. ' +
1536
- 'DFM has NO automatic rebalancing (manual by design, #35) — nothing converges by itself, so an agent that ' +
1537
- 'reads a managed vault and stays quiet about drift IS the failure. Report it with the number even when ' +
1538
- 'nobody asked, then let the ' +
1539
- 'manager decide; never fire this unasked. ' +
1540
- 'REAL-MONEY (gated). MANAGER action — only works on a vault your app wallet ' +
1541
- 'ADMINS whose rebalancer is initialized (bundled into launch_vault, or run ' +
1542
- 'init_rebalancer first). CONVERGES the vault\'s live holdings toward its ' +
1543
- 'CURRENT onchain CONFIG target weights — you supply NO inputMint/outputMint/' +
1544
- 'amount. The API derives the converging swap legs from the drift between live ' +
1545
- 'balances and the registered targets (sell-overweight → buy-underweight), ' +
1546
- 'returns them as ordered admin-signed execute_rebalance txs, and this tool ' +
1547
- 'signs + submits them IN ORDER (honoring the 5s onchain cooldown). To CHANGE ' +
1548
- 'the targets first, use update_vault_assets (reconfigure). Onchain guards are ' +
1549
- 'ORACLE-FREE: a measured min_out floor + hardcoded velocity ceilings + an ' +
1550
- 'output-mint allowlist (a value-leaking route REVERTS). If already balanced, it ' +
1551
- 'returns `converged:true` with no swaps. The vault must HOLD assets (i.e. after ' +
1552
- 'a deposit) — converging an empty vault does nothing.',
1533
+ description: 'REAL-MONEY (gated). MANAGER duty: prices pull holdings away from the config ' +
1534
+ 'every day and DFM has NO automatic rebalancing (#35), so report drift with ' +
1535
+ 'the number even unasked — but never fire this unasked; the manager decides. ' +
1536
+ 'Works only on a vault your app wallet ADMINS with an initialized rebalancer ' +
1537
+ '(bundled into launch_vault, else init_rebalancer). CONVERGES live holdings ' +
1538
+ 'toward the CURRENT onchain target weights — you supply no mints or amounts. ' +
1539
+ 'The API derives the legs from drift (sell overweight → buy underweight) as ' +
1540
+ 'ordered admin-signed execute_rebalance txs; this tool signs + submits them in ' +
1541
+ 'order (5s onchain cooldown). To CHANGE targets first, use update_vault_assets. ' +
1542
+ 'Onchain guards are oracle-free: measured min_out + velocity ceilings + an ' +
1543
+ 'output-mint allowlist (a value-leaking route reverts). Already balanced → ' +
1544
+ '`converged:true`, no swaps. An empty vault (no deposit yet) does nothing.',
1553
1545
  inputSchema: {
1554
1546
  vault: z.string().regex(BASE58, 'Invalid base58 vault address'),
1555
1547
  slippageBps: z.number().int().min(1).max(300).optional(),
package/dist/signing.js CHANGED
@@ -20,7 +20,8 @@
20
20
  * The keypair is loaded lazily (first WRITE call), so the read-only tools and
21
21
  * `npx tsx` smoke runs never touch key material.
22
22
  */
23
- import { readFileSync } from 'node:fs';
23
+ import { lstatSync, readFileSync } from 'node:fs';
24
+ import { dirname, resolve } from 'node:path';
24
25
  import { Keypair, Transaction, VersionedTransaction } from '@solana/web3.js';
25
26
  import nacl from 'tweetnacl';
26
27
  import bs58 from 'bs58';
@@ -32,6 +33,54 @@ import bs58 from 'bs58';
32
33
  */
33
34
  const PROTOCOL_WALLET = '9GjEBsZ1Cos2J6inmgwhBKv9pXGNtLSJ7xQ9xZb29bET';
34
35
  let cachedKeypair = null;
36
+ const lstatOrNull = (p) => {
37
+ try {
38
+ return lstatSync(p);
39
+ }
40
+ catch (e) {
41
+ if (e.code === 'ENOENT')
42
+ return null;
43
+ throw e;
44
+ }
45
+ };
46
+ const myUid = () => (typeof process.getuid === 'function' ? process.getuid() : undefined);
47
+ /**
48
+ * Harden the on-disk keypair path before reading it.
49
+ *
50
+ * Mirrors the owner-key checks in `app-wallet-owner.ts`: symlinks (file or
51
+ * directory), files owned by another uid, and any group/other access are
52
+ * refused outright. The env-JSON escape hatch bypasses the filesystem entirely.
53
+ */
54
+ function assertSafeKeypairPath(filePath) {
55
+ const path = resolve(filePath);
56
+ const dir = dirname(path);
57
+ const dirSt = lstatOrNull(dir);
58
+ if (!dirSt)
59
+ throw new Error(`Keypair directory ${dir} does not exist.`);
60
+ if (dirSt.isSymbolicLink())
61
+ throw new Error(`${dir} is a symlink — refusing to keep a keypair behind it`);
62
+ if (!dirSt.isDirectory())
63
+ throw new Error(`${dir} is not a directory`);
64
+ const uid = myUid();
65
+ if (uid !== undefined && dirSt.uid !== uid)
66
+ throw new Error(`${dir} is not owned by the current user`);
67
+ if ((dirSt.mode & 0o022) !== 0)
68
+ throw new Error(`${dir} is writable by other users`);
69
+ const st = lstatOrNull(path);
70
+ if (!st) {
71
+ throw new Error(`Could not read test keypair file at DFM_AGENT_KEYPAIR_PATH (ENOENT). ` +
72
+ `Set it to a Solana secret-key JSON file the operator controls locally.`);
73
+ }
74
+ if (st.isSymbolicLink())
75
+ throw new Error(`${path} is a symlink — refusing to use it as a keypair file`);
76
+ if (!st.isFile())
77
+ throw new Error(`${path} exists and is not a regular file`);
78
+ if (uid !== undefined && st.uid !== uid)
79
+ throw new Error(`${path} is not owned by the current user`);
80
+ if ((st.mode & 0o077) !== 0) {
81
+ throw new Error(`${path} is readable by other users (mode ${(st.mode & 0o777).toString(8)}); run chmod 600 on it first`);
82
+ }
83
+ }
35
84
  /** Parse a Solana 64-byte secret-key JSON array into a Keypair. */
36
85
  function keypairFromJsonArray(json, source) {
37
86
  let arr;
@@ -70,6 +119,7 @@ export function loadTestKeypair(config) {
70
119
  return cachedKeypair;
71
120
  let kp;
72
121
  if (config.keypairPath) {
122
+ assertSafeKeypairPath(config.keypairPath);
73
123
  let raw;
74
124
  try {
75
125
  raw = readFileSync(config.keypairPath, 'utf8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dfm-fi/agent",
3
- "version": "0.2.1942",
3
+ "version": "0.2.1958",
4
4
  "description": "DFM v2 MCP server — drive the full fund launch → deposit → redeem (+ manage) lifecycle from an AI agent against the live API.",
5
5
  "homepage": "https://app.dfm.finance/docs/agent",
6
6
  "mcpName": "io.github.DFM-Finance/dfm-agent",
@@ -55,10 +55,10 @@
55
55
  "lint": "eslint .",
56
56
  "example:read": "tsx src/examples/list-and-status.ts",
57
57
  "example:lifecycle": "tsx src/examples/launch-deposit-redeem.ts",
58
- "test": "npm run build && node scripts/smoke-signing.mjs && node scripts/smoke-signing-v1.mjs &&node scripts/smoke-cli.mjs && node scripts/smoke-keyless.mjs && node scripts/smoke-zap-options.mjs && tsx src/who-pays.test.ts && tsx src/wallet-identity.test.ts && tsx src/money-path.test.ts && tsx src/control-flow.test.ts && tsx src/create-flow.metadata.test.ts && tsx src/manager-flow.reconfigure.test.ts && tsx src/recovery-flow.deadleg.test.ts && tsx src/zap-v2-flow.pending.test.ts && tsx src/mcp-server.confirm-gate.test.ts && tsx src/app-wallet-owner.test.ts && tsx src/onboarding.test.ts"
58
+ "test": "npm run build && node scripts/smoke-signing.mjs && node scripts/smoke-signing-v1.mjs &&node scripts/smoke-cli.mjs && node scripts/smoke-keyless.mjs && node scripts/smoke-zap-options.mjs && tsx src/who-pays.test.ts && tsx src/wallet-identity.test.ts && tsx src/money-path.test.ts && tsx src/control-flow.test.ts && tsx src/create-flow.metadata.test.ts && tsx src/manager-flow.reconfigure.test.ts && tsx src/recovery-flow.deadleg.test.ts && tsx src/zap-v2-flow.pending.test.ts && tsx src/mcp-server.confirm-gate.test.ts && tsx src/app-wallet-owner.test.ts && tsx src/onboarding.test.ts && tsx src/client-header.test.ts"
59
59
  },
60
60
  "dependencies": {
61
- "@modelcontextprotocol/sdk": "^1.26.0",
61
+ "@modelcontextprotocol/sdk": "^1.32.1",
62
62
  "@solana/web3.js": "^1.99.0",
63
63
  "bs58": "^4.0.1",
64
64
  "tweetnacl": "^1.0.3",