@connormartin/seed-network-agent 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -91,6 +91,14 @@ After publishing, users can update their installed CLI with:
91
91
  npm install -g @connormartin/seed-network-agent@latest
92
92
  ```
93
93
 
94
+ If npm returns `ETARGET` immediately after publishing, the registry metadata and tarball may not have fully propagated yet. Wait a minute, verify the package, then retry with fresh registry metadata:
95
+
96
+ ```bash
97
+ npm view @connormartin/seed-network-agent version versions
98
+ npm install -g @connormartin/seed-network-agent@latest --prefer-online --registry=https://registry.npmjs.org/
99
+ hash -r
100
+ ```
101
+
94
102
  Or run the latest version directly:
95
103
 
96
104
  ```bash
@@ -130,5 +138,18 @@ The wrapper can connect to the Seed Network web app as an Agent Auth client:
130
138
  /seed disconnect
131
139
  ```
132
140
 
133
- By default the provider URL is `SEED_NETWORK_AGENT_AUTH_PROVIDER` or `https://seed-network-v2.vercel.app/`.
141
+ By default, the published npm CLI (`seed`) connects to `https://seed-network-v2.vercel.app/`, while the local source command (`pnpm agent`) connects to `http://localhost:3000` for development. Override either with `SEED_NETWORK_AGENT_AUTH_PROVIDER`.
134
142
  Agent Auth identity and grants are stored locally under `~/.seed-network-agent/auth/`.
143
+
144
+ If `/submit-deal` fails with `fetch failed` during PDF upload, check the upload URL in the error. If it mentions `localhost:3000`, remove stale local Agent Auth state and reconnect to production:
145
+
146
+ ```bash
147
+ /seed disconnect
148
+ /connect
149
+ ```
150
+
151
+ You can also force the provider for one run:
152
+
153
+ ```bash
154
+ SEED_NETWORK_AGENT_AUTH_PROVIDER=https://seed-network-v2.vercel.app/ seed
155
+ ```
@@ -46,7 +46,7 @@ async function handleSeedCommand(args, ctx) {
46
46
  const grants = connection.capabilityGrants
47
47
  .map((grant) => `${grant.capability}:${grant.status}`)
48
48
  .join(", ");
49
- return `${connection.providerName} ${connection.agentId} (${connection.mode}) — ${grants || "no grants"}`;
49
+ return `${connection.providerName} ${connection.agentId} (${connection.mode}, ${connection.issuer}) — ${grants || "no grants"}`;
50
50
  });
51
51
  ctx.ui.notify(lines.join("\n"), "info");
52
52
  return;
@@ -41,7 +41,7 @@ export async function preparePdfDealSubmission(submission, connection, ui, optio
41
41
  ui?.notify?.(`Deal interpretation warnings:\n${interpreted.warnings.join("\n")}`, "warning");
42
42
  }
43
43
  ui?.notify?.("Uploading PDF deck to Seed Network storage…", "info");
44
- const deck = await uploadDealDeck(validated, connection);
44
+ const deck = await uploadDealDeck(validated, connection, ui);
45
45
  const sanitizedAnswers = omitAnswer(submission.answers, "deck_file_path");
46
46
  const explicitName = answerValue(submission.answers.name);
47
47
  return {
@@ -1,5 +1,5 @@
1
- import { createSeedAgentAuthClient, DEFAULT_PROVIDER_URL, SEED_SUBMIT_DEAL_CAPABILITY } from "../../seed-agent-auth.js";
2
- export async function uploadDealDeck(validated, connection) {
1
+ import { createSeedAgentAuthClient, DEFAULT_PROVIDER_URL, isLocalhostUrl, SEED_SUBMIT_DEAL_CAPABILITY, } from "../../seed-agent-auth.js";
2
+ export async function uploadDealDeck(validated, connection, ui) {
3
3
  const client = createSeedAgentAuthClient();
4
4
  try {
5
5
  const { token } = await client.signJwt({
@@ -10,17 +10,25 @@ export async function uploadDealDeck(validated, connection) {
10
10
  form.set("file", new Blob([validated.buffer], { type: "application/pdf" }), validated.fileName || "deck.pdf");
11
11
  form.set("sha256", validated.sha256);
12
12
  form.set("size", String(validated.size));
13
- const response = await fetch(agentUploadUrl(connection), {
14
- method: "POST",
15
- headers: {
16
- Authorization: `Bearer ${token}`,
17
- },
18
- body: form,
19
- });
13
+ const uploadUrl = agentUploadUrl(connection);
14
+ ui?.notify?.(`Uploading PDF deck to ${urlOrigin(uploadUrl) ?? uploadUrl}…`, "info");
15
+ let response;
16
+ try {
17
+ response = await fetch(uploadUrl, {
18
+ method: "POST",
19
+ headers: {
20
+ Authorization: `Bearer ${token}`,
21
+ },
22
+ body: form,
23
+ });
24
+ }
25
+ catch (error) {
26
+ throw new Error(`PDF deck upload could not reach ${uploadUrl} (Agent Auth issuer: ${connection.issuer}). ${formatFetchError(error)}${uploadHint(uploadUrl)}`, { cause: error });
27
+ }
20
28
  const data = await response.json().catch(() => null);
21
29
  if (!response.ok) {
22
30
  const message = isRecord(data) && typeof data.error === "string" ? data.error : `HTTP ${response.status}`;
23
- throw new Error(`PDF deck upload failed: ${message}`);
31
+ throw new Error(`PDF deck upload failed at ${uploadUrl}: ${message}`);
24
32
  }
25
33
  if (!isRecord(data) || data.ok !== true || !isRecord(data.deck)) {
26
34
  throw new Error("PDF deck upload returned an invalid response.");
@@ -52,6 +60,36 @@ function agentUploadUrl(connection) {
52
60
  return new URL("/api/agent/deal-deck-upload", DEFAULT_PROVIDER_URL).toString();
53
61
  }
54
62
  }
63
+ function uploadHint(uploadUrl) {
64
+ if (isLocalhostUrl(uploadUrl)) {
65
+ return " This looks like a stale localhost Agent Auth connection. Run /seed disconnect, then /connect, then retry /submit-deal.";
66
+ }
67
+ return " Check your network connection or set SEED_NETWORK_AGENT_DECK_UPLOAD_URL to override the upload endpoint.";
68
+ }
69
+ function formatFetchError(error) {
70
+ if (!(error instanceof Error))
71
+ return `Fetch error: ${String(error)}.`;
72
+ const cause = error.cause;
73
+ if (cause && typeof cause === "object") {
74
+ const code = "code" in cause && typeof cause.code === "string" ? cause.code : undefined;
75
+ const message = "message" in cause && typeof cause.message === "string" ? cause.message : undefined;
76
+ if (code && message)
77
+ return `Fetch error: ${error.message} (${code}: ${message}).`;
78
+ if (code)
79
+ return `Fetch error: ${error.message} (${code}).`;
80
+ if (message)
81
+ return `Fetch error: ${error.message} (${message}).`;
82
+ }
83
+ return `Fetch error: ${error.message}.`;
84
+ }
85
+ function urlOrigin(value) {
86
+ try {
87
+ return new URL(value).origin;
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
55
93
  function stringProperty(record, field) {
56
94
  const value = record[field];
57
95
  if (typeof value !== "string" || value.length === 0)
package/dist/pi.js CHANGED
@@ -34,6 +34,7 @@ export async function runPi(userArgs = []) {
34
34
  env: {
35
35
  ...process.env,
36
36
  SEED_NETWORK_AGENT_ROOT: agentRoot,
37
+ SEED_NETWORK_AGENT_RUNTIME: process.env.SEED_NETWORK_AGENT_RUNTIME ?? agentRuntime(),
37
38
  },
38
39
  });
39
40
  await new Promise((resolve, reject) => {
@@ -54,6 +55,9 @@ function resolveExtensionPath() {
54
55
  return compiledExtensionPath;
55
56
  return path.join(__dirname, "extension", "index.ts");
56
57
  }
58
+ function agentRuntime() {
59
+ return path.basename(__dirname) === "src" ? "source" : "package";
60
+ }
57
61
  function shouldCheckForUpdates(userArgs) {
58
62
  const skipFlags = new Set(["--help", "-h", "--version", "-v", "--offline"]);
59
63
  return !userArgs.some((arg) => skipFlags.has(arg));
@@ -3,8 +3,13 @@ import os from "node:os";
3
3
  import { AgentAuthClient } from "@auth/agent";
4
4
  import { SeedAgentAuthStorage } from "./agent-auth-storage.js";
5
5
  export const SEED_SUBMIT_DEAL_CAPABILITY = "submit_deal";
6
- export const DEFAULT_PROVIDER_URL = process.env.SEED_NETWORK_AGENT_AUTH_PROVIDER ?? "https://seed-network-v2.vercel.app/";
6
+ export const LOCAL_PROVIDER_URL = "http://localhost:3000";
7
+ export const PROD_PROVIDER_URL = "https://seed-network-v2.vercel.app/";
8
+ export const DEFAULT_PROVIDER_URL = process.env.SEED_NETWORK_AGENT_AUTH_PROVIDER ?? defaultProviderUrl();
7
9
  const DEFAULT_SEED_CAPABILITIES = [SEED_SUBMIT_DEAL_CAPABILITY];
10
+ function defaultProviderUrl() {
11
+ return process.env.SEED_NETWORK_AGENT_RUNTIME === "source" ? LOCAL_PROVIDER_URL : PROD_PROVIDER_URL;
12
+ }
8
13
  export function createSeedAgentAuthClient(ui) {
9
14
  return new AgentAuthClient({
10
15
  storage: new SeedAgentAuthStorage(),
@@ -45,8 +50,8 @@ export async function listSeedAgentConnections() {
45
50
  }
46
51
  export async function getPreferredConnection() {
47
52
  const connections = await listSeedAgentConnections();
48
- return connections.find((connection) => hasActiveCapability(connection, SEED_SUBMIT_DEAL_CAPABILITY))
49
- ?? connections[0];
53
+ return preferCurrentProvider(connections.filter((connection) => hasActiveCapability(connection, SEED_SUBMIT_DEAL_CAPABILITY)))
54
+ ?? preferCurrentProvider(connections);
50
55
  }
51
56
  export async function ensureActiveSeedCapability(client, capability, ui, approval) {
52
57
  const storage = new SeedAgentAuthStorage();
@@ -138,11 +143,41 @@ async function waitForActiveCapability(client, agentId, capability, timeoutMs =
138
143
  }
139
144
  async function getConnectionWithCapability(capability) {
140
145
  const connections = await listSeedAgentConnections();
141
- return connections.find((connection) => hasActiveCapability(connection, capability));
146
+ return preferCurrentProvider(connections.filter((connection) => hasActiveCapability(connection, capability)));
142
147
  }
143
148
  function hasActiveCapability(connection, capability) {
144
149
  return connection.capabilityGrants.some((grant) => grant.capability === capability && grant.status === "active");
145
150
  }
151
+ function preferCurrentProvider(connections) {
152
+ const usableConnections = connections.filter((connection) => !isStaleLocalhostConnection(connection));
153
+ return usableConnections.find(isCurrentProviderConnection) ?? usableConnections[0];
154
+ }
155
+ function isCurrentProviderConnection(connection) {
156
+ return urlOrigin(connection.issuer) === urlOrigin(DEFAULT_PROVIDER_URL);
157
+ }
158
+ export function isStaleLocalhostConnection(connection) {
159
+ return isLocalhostUrl(connection.issuer) && !isLocalhostUrl(DEFAULT_PROVIDER_URL);
160
+ }
161
+ export function isLocalhostUrl(value) {
162
+ const hostname = urlHostname(value);
163
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
164
+ }
165
+ function urlOrigin(value) {
166
+ try {
167
+ return new URL(value).origin;
168
+ }
169
+ catch {
170
+ return null;
171
+ }
172
+ }
173
+ function urlHostname(value) {
174
+ try {
175
+ return new URL(value).hostname;
176
+ }
177
+ catch {
178
+ return null;
179
+ }
180
+ }
146
181
  function isAgentAuthErrorCode(error, ...codes) {
147
182
  return typeof error === "object"
148
183
  && error !== null
@@ -156,10 +191,17 @@ async function sleep(ms) {
156
191
  export async function disconnectSeedAgents(ui) {
157
192
  const connections = await listSeedAgentConnections();
158
193
  const client = createSeedAgentAuthClient(ui);
194
+ const storage = new SeedAgentAuthStorage();
159
195
  let disconnected = 0;
160
196
  try {
161
197
  for (const connection of connections) {
162
- await client.disconnectAgent(connection.agentId);
198
+ try {
199
+ await client.disconnectAgent(connection.agentId);
200
+ }
201
+ catch (error) {
202
+ ui?.notify?.(`Could not revoke Seed Network Agent ${connection.agentId} at ${connection.issuer}; removing the local saved connection anyway. ${formatError(error)}`, "warning");
203
+ }
204
+ await storage.deleteAgentConnection(connection.agentId);
163
205
  disconnected += 1;
164
206
  }
165
207
  }
@@ -168,6 +210,22 @@ export async function disconnectSeedAgents(ui) {
168
210
  }
169
211
  return disconnected;
170
212
  }
213
+ function formatError(error) {
214
+ if (!(error instanceof Error))
215
+ return String(error);
216
+ const cause = error.cause;
217
+ if (cause && typeof cause === "object") {
218
+ const code = "code" in cause && typeof cause.code === "string" ? cause.code : undefined;
219
+ const message = "message" in cause && typeof cause.message === "string" ? cause.message : undefined;
220
+ if (code && message)
221
+ return `${error.message} (${code}: ${message})`;
222
+ if (code)
223
+ return `${error.message} (${code})`;
224
+ if (message)
225
+ return `${error.message} (${message})`;
226
+ }
227
+ return error.message;
228
+ }
171
229
  function formatApprovalMessage(approval) {
172
230
  const lines = ["Seed Network Agent requires browser approval."];
173
231
  if (approval.user_code)
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@connormartin/seed-network-agent",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
7
- "seed": "./dist/cli.js",
8
- "seed-agent": "./dist/cli.js",
9
- "seed-network-agent": "./dist/cli.js"
7
+ "seed": "dist/cli.js",
8
+ "seed-agent": "dist/cli.js",
9
+ "seed-network-agent": "dist/cli.js"
10
10
  },
11
11
  "files": [
12
12
  "dist",