@connormartin/seed-network-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,74 @@
1
+ import { createSeedAgentAuthClient, ensureActiveSeedCapability, SEED_SUBMIT_DEAL_CAPABILITY, } from "../../seed-agent-auth.js";
2
+ import { interpretDealText } from "./interpretation.js";
3
+ import { extractPdfText, validateLocalPdf } from "./local-pdf.js";
4
+ import { MAX_SOURCE_CONTENT_CHARS } from "./types.js";
5
+ import { uploadDealDeck } from "./upload-client.js";
6
+ export { extractPdfText, validateLocalPdf } from "./local-pdf.js";
7
+ export { interpretDealText } from "./interpretation.js";
8
+ export async function submitDealSubmission(submission, ui, options = {}) {
9
+ const client = createSeedAgentAuthClient(ui);
10
+ try {
11
+ const connection = await ensureActiveSeedCapability(client, SEED_SUBMIT_DEAL_CAPABILITY, ui, {
12
+ reason: "Create Seed Network deal records from the local Seed Network Agent submit-deal wizard.",
13
+ bindingMessage: "Approve Seed Network Agent deal submission",
14
+ });
15
+ const prepared = await preparePdfDealSubmission(submission, connection, ui, options);
16
+ return await client.executeCapability({
17
+ agentId: connection.agentId,
18
+ capability: SEED_SUBMIT_DEAL_CAPABILITY,
19
+ arguments: prepared,
20
+ });
21
+ }
22
+ finally {
23
+ client.destroy();
24
+ }
25
+ }
26
+ export async function preparePdfDealSubmission(submission, connection, ui, options = {}) {
27
+ const deckPath = answerValue(submission.answers.deck_file_path);
28
+ if (!deckPath)
29
+ throw new Error("Local PDF deck file path is required.");
30
+ ui?.notify?.("Validating local PDF deck…", "info");
31
+ const validated = await validateLocalPdf(deckPath);
32
+ ui?.notify?.("Extracting deck text locally…", "info");
33
+ const extraction = await extractPdfText(validated.buffer);
34
+ ui?.notify?.("Interpreting extracted deal fields locally…", "info");
35
+ const interpreted = await interpretDealText(extraction.text, options.interpretDealText);
36
+ const extractionWarnings = [...validated.warnings, ...extraction.warnings];
37
+ if (extractionWarnings.length > 0) {
38
+ ui?.notify?.(`PDF extraction warnings:\n${extractionWarnings.join("\n")}`, "warning");
39
+ }
40
+ if (interpreted.warnings && interpreted.warnings.length > 0) {
41
+ ui?.notify?.(`Deal interpretation warnings:\n${interpreted.warnings.join("\n")}`, "warning");
42
+ }
43
+ ui?.notify?.("Uploading PDF deck to Seed Network storage…", "info");
44
+ const deck = await uploadDealDeck(validated, connection);
45
+ const sanitizedAnswers = omitAnswer(submission.answers, "deck_file_path");
46
+ const explicitName = answerValue(submission.answers.name);
47
+ return {
48
+ ...submission,
49
+ answers: sanitizedAnswers,
50
+ ...(explicitName ? { name: explicitName } : {}),
51
+ deck,
52
+ extraction: {
53
+ ...extraction,
54
+ warnings: extractionWarnings,
55
+ text: truncate(extraction.text, MAX_SOURCE_CONTENT_CHARS),
56
+ },
57
+ interpreted,
58
+ intake: {
59
+ warmIntro: answerValue(submission.answers.warm_intro),
60
+ alreadyInvestor: answerValue(submission.answers.already_investor),
61
+ },
62
+ };
63
+ }
64
+ function omitAnswer(answers, questionId) {
65
+ const { [questionId]: _omitted, ...rest } = answers;
66
+ return rest;
67
+ }
68
+ function answerValue(answer) {
69
+ const value = answer?.value?.trim();
70
+ return value || undefined;
71
+ }
72
+ function truncate(value, maxLength) {
73
+ return value.length <= maxLength ? value : value.slice(0, maxLength);
74
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "version": "submit-deal.v1",
3
+ "title": "Submit deal",
4
+ "description": "Collect the Seed Network deal intake fields and local PDF deck before creating a deal.",
5
+ "questions": [
6
+ {
7
+ "id": "deck_file_path",
8
+ "label": "Pitch deck PDF",
9
+ "type": "text",
10
+ "required": true,
11
+ "prompt": "Enter the local file path to the pitch deck PDF.",
12
+ "placeholder": "~/Downloads/company-deck.pdf"
13
+ },
14
+ {
15
+ "id": "name",
16
+ "label": "Company",
17
+ "type": "text",
18
+ "required": false,
19
+ "prompt": "What company or deal is this for? Leave blank to infer from the PDF or filename.",
20
+ "placeholder": "Company name"
21
+ },
22
+ {
23
+ "id": "warm_intro",
24
+ "label": "Warm intro",
25
+ "type": "single_select",
26
+ "required": true,
27
+ "prompt": "Can you facilitate a warm intro to the founder?",
28
+ "options": [
29
+ { "value": "yes", "label": "Yes" },
30
+ { "value": "no", "label": "No" },
31
+ { "value": "not_sure", "label": "Not sure" }
32
+ ]
33
+ },
34
+ {
35
+ "id": "already_investor",
36
+ "label": "Investor status",
37
+ "type": "single_select",
38
+ "required": true,
39
+ "prompt": "Are you already an investor in this company?",
40
+ "options": [
41
+ { "value": "yes", "label": "Yes" },
42
+ { "value": "no", "label": "No" },
43
+ { "value": "prefer_not_to_say", "label": "Prefer not to say" }
44
+ ]
45
+ }
46
+ ]
47
+ }
@@ -0,0 +1,100 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { complete } from "@earendil-works/pi-ai";
5
+ import { runOnboardingWizard } from "../../wizard.js";
6
+ import { submitDealSubmission } from "./pipeline.js";
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const submitDealQuestionsPath = path.join(__dirname, "questionnaire.v1.json");
10
+ const DEAL_INTERPRETATION_SYSTEM_PROMPT = [
11
+ "You extract conservative structured data from startup pitch deck text.",
12
+ "Return only strict JSON with keys: companyName, summary, founderNames, website, email, socialLinks, valuationUsd, totalAllocationUsd, terms, confidence, warnings.",
13
+ "Use numeric USD values only when explicitly stated. Put ambiguous financing language in terms. Confidence must be low, medium, or high.",
14
+ ].join(" ");
15
+ export function registerSubmitDealPdf(pi) {
16
+ pi.registerCommand("submit-deal", {
17
+ description: "Submit a Seed Network deal with a local PDF deck and intake questions",
18
+ handler: async (_args, ctx) => {
19
+ if (ctx.hasUI === false) {
20
+ throw new Error("/submit-deal requires interactive Pi UI.");
21
+ }
22
+ await startSubmitDeal(ctx);
23
+ },
24
+ });
25
+ }
26
+ async function startSubmitDeal(ctx) {
27
+ const questionnaire = await loadQuestionnaire(submitDealQuestionsPath);
28
+ const result = await runOnboardingWizard(ctx.ui, questionnaire, {
29
+ startMessage: "Submit deal: starting deal intake wizard.",
30
+ reviewTitle: "Review deal submission",
31
+ reviewDescription: "Review these deal intake answers before creating the Seed Network deal.",
32
+ reviewConfirmPrompt: "Create this deal now?",
33
+ suppressReviewCancelledMessage: true,
34
+ });
35
+ if (result.cancelled || !result.submission) {
36
+ ctx.ui.notify("Deal submission cancelled.", "warning");
37
+ return;
38
+ }
39
+ try {
40
+ const response = await submitDealSubmission(result.submission, ctx.ui, {
41
+ interpretDealText: createPiDealInterpreter(ctx),
42
+ });
43
+ const data = response.data ?? response.result ?? response;
44
+ const deal = isRecord(data) && isRecord(data.deal) ? data.deal : null;
45
+ ctx.ui.notify(formatSubmitDealConfirmation(deal), "info");
46
+ }
47
+ catch (error) {
48
+ ctx.ui.notify(`Seed Network deal submission failed: ${errorMessage(error)}`, "error");
49
+ }
50
+ }
51
+ function createPiDealInterpreter(ctx) {
52
+ if (!ctx.model || !ctx.modelRegistry)
53
+ return undefined;
54
+ return async (text) => {
55
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
56
+ if (!auth.ok || !auth.apiKey) {
57
+ throw new Error(auth.ok ? `No model credentials available for ${ctx.model.provider}` : auth.error);
58
+ }
59
+ const userMessage = {
60
+ role: "user",
61
+ content: [{ type: "text", text: `Extract deal fields from this PDF text:\n\n${text}` }],
62
+ timestamp: Date.now(),
63
+ };
64
+ const response = await complete(ctx.model, { systemPrompt: DEAL_INTERPRETATION_SYSTEM_PROMPT, messages: [userMessage] }, { apiKey: auth.apiKey, headers: auth.headers, signal: ctx.signal });
65
+ if (response.stopReason === "aborted") {
66
+ throw new Error("Pi model interpretation was cancelled.");
67
+ }
68
+ const content = response.content
69
+ .filter((part) => part.type === "text")
70
+ .map((part) => part.text)
71
+ .join("\n")
72
+ .trim();
73
+ return JSON.parse(stripJsonFence(content));
74
+ };
75
+ }
76
+ function stripJsonFence(value) {
77
+ const match = value.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
78
+ return match?.[1]?.trim() ?? value;
79
+ }
80
+ async function loadQuestionnaire(filePath = submitDealQuestionsPath) {
81
+ const raw = await readFile(filePath, "utf8");
82
+ return JSON.parse(raw);
83
+ }
84
+ function isRecord(value) {
85
+ return typeof value === "object" && value !== null && !Array.isArray(value);
86
+ }
87
+ function formatSubmitDealConfirmation(deal) {
88
+ const name = typeof deal?.name === "string" ? deal.name : "Deal";
89
+ const slug = typeof deal?.slug === "string" ? deal.slug : undefined;
90
+ const deckUrl = typeof deal?.deckUrl === "string" ? deal.deckUrl : undefined;
91
+ return [
92
+ `✅ ${name} submitted to Seed Network.`,
93
+ slug ? `View deal: /deals/${slug}` : undefined,
94
+ deckUrl ? `Pitch deck saved: ${deckUrl}` : undefined,
95
+ "The intake answers were saved to the deal notes and application data.",
96
+ ].filter(Boolean).join("\n");
97
+ }
98
+ function errorMessage(error) {
99
+ return error instanceof Error ? error.message : String(error);
100
+ }
@@ -0,0 +1,3 @@
1
+ export const MAX_DEAL_DECK_BYTES = 25 * 1024 * 1024;
2
+ export const MAX_SOURCE_CONTENT_CHARS = 100_000;
3
+ export const MIN_EXTRACTED_TEXT_CHARS = 200;
@@ -0,0 +1,69 @@
1
+ import { createSeedAgentAuthClient, DEFAULT_PROVIDER_URL, SEED_SUBMIT_DEAL_CAPABILITY } from "../../seed-agent-auth.js";
2
+ export async function uploadDealDeck(validated, connection) {
3
+ const client = createSeedAgentAuthClient();
4
+ try {
5
+ const { token } = await client.signJwt({
6
+ agentId: connection.agentId,
7
+ capabilities: [SEED_SUBMIT_DEAL_CAPABILITY],
8
+ });
9
+ const form = new FormData();
10
+ form.set("file", new Blob([validated.buffer], { type: "application/pdf" }), validated.fileName || "deck.pdf");
11
+ form.set("sha256", validated.sha256);
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
+ });
20
+ const data = await response.json().catch(() => null);
21
+ if (!response.ok) {
22
+ const message = isRecord(data) && typeof data.error === "string" ? data.error : `HTTP ${response.status}`;
23
+ throw new Error(`PDF deck upload failed: ${message}`);
24
+ }
25
+ if (!isRecord(data) || data.ok !== true || !isRecord(data.deck)) {
26
+ throw new Error("PDF deck upload returned an invalid response.");
27
+ }
28
+ return parseDeckMetadata(data.deck);
29
+ }
30
+ finally {
31
+ client.destroy();
32
+ }
33
+ }
34
+ function parseDeckMetadata(value) {
35
+ const url = stringProperty(value, "url");
36
+ const fileName = stringProperty(value, "fileName");
37
+ const contentType = stringProperty(value, "contentType");
38
+ const size = numberProperty(value, "size");
39
+ const sha256 = stringProperty(value, "sha256");
40
+ if (contentType !== "application/pdf")
41
+ throw new Error("Uploaded deck response was not a PDF.");
42
+ return { url, fileName, contentType, size, sha256 };
43
+ }
44
+ function agentUploadUrl(connection) {
45
+ const configured = process.env.SEED_NETWORK_AGENT_DECK_UPLOAD_URL;
46
+ if (configured)
47
+ return configured;
48
+ try {
49
+ return new URL("/api/agent/deal-deck-upload", connection.issuer).toString();
50
+ }
51
+ catch {
52
+ return new URL("/api/agent/deal-deck-upload", DEFAULT_PROVIDER_URL).toString();
53
+ }
54
+ }
55
+ function stringProperty(record, field) {
56
+ const value = record[field];
57
+ if (typeof value !== "string" || value.length === 0)
58
+ throw new Error(`Upload response missing ${field}.`);
59
+ return value;
60
+ }
61
+ function numberProperty(record, field) {
62
+ const value = record[field];
63
+ if (typeof value !== "number" || !Number.isFinite(value))
64
+ throw new Error(`Upload response missing ${field}.`);
65
+ return value;
66
+ }
67
+ function isRecord(value) {
68
+ return typeof value === "object" && value !== null && !Array.isArray(value);
69
+ }
@@ -0,0 +1 @@
1
+ export { default } from "./extension/index.js";
package/dist/pi.js ADDED
@@ -0,0 +1,60 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { maybeNotifyUpdate } from "./update-check.js";
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const agentRoot = path.resolve(__dirname, "..");
10
+ export async function runPi(userArgs = []) {
11
+ if (shouldCheckForUpdates(userArgs)) {
12
+ await maybeNotifyUpdate(agentRoot);
13
+ }
14
+ const extensionPath = resolveExtensionPath();
15
+ const systemPromptPath = path.join(agentRoot, "prompts", "SYSTEM.md");
16
+ const systemPrompt = await readFile(systemPromptPath, "utf8");
17
+ const piArgs = [
18
+ "--no-extensions",
19
+ "--no-skills",
20
+ "--no-prompt-templates",
21
+ "--no-themes",
22
+ "--no-context-files",
23
+ "--extension",
24
+ extensionPath,
25
+ "--append-system-prompt",
26
+ systemPrompt,
27
+ ...userArgs,
28
+ ];
29
+ const localPiBin = path.join(agentRoot, "node_modules", ".bin", process.platform === "win32" ? "pi.cmd" : "pi");
30
+ const command = process.env.SEED_NETWORK_AGENT_PI_BIN ?? (existsSync(localPiBin) ? localPiBin : "pi");
31
+ const child = spawn(command, piArgs, {
32
+ cwd: process.cwd(),
33
+ stdio: "inherit",
34
+ env: {
35
+ ...process.env,
36
+ SEED_NETWORK_AGENT_ROOT: agentRoot,
37
+ },
38
+ });
39
+ await new Promise((resolve, reject) => {
40
+ child.on("error", reject);
41
+ child.on("exit", (code, signal) => {
42
+ if (signal) {
43
+ process.kill(process.pid, signal);
44
+ return;
45
+ }
46
+ process.exitCode = code ?? 0;
47
+ resolve();
48
+ });
49
+ });
50
+ }
51
+ function resolveExtensionPath() {
52
+ const compiledExtensionPath = path.join(__dirname, "extension", "index.js");
53
+ if (existsSync(compiledExtensionPath))
54
+ return compiledExtensionPath;
55
+ return path.join(__dirname, "extension", "index.ts");
56
+ }
57
+ function shouldCheckForUpdates(userArgs) {
58
+ const skipFlags = new Set(["--help", "-h", "--version", "-v", "--offline"]);
59
+ return !userArgs.some((arg) => skipFlags.has(arg));
60
+ }
@@ -0,0 +1,211 @@
1
+ import { spawn } from "node:child_process";
2
+ import os from "node:os";
3
+ import { AgentAuthClient } from "@auth/agent";
4
+ import { SeedAgentAuthStorage } from "./agent-auth-storage.js";
5
+ export const SEED_ONBOARDING_CAPABILITY = "submit_onboarding_answers";
6
+ export const SEED_SUBMIT_DEAL_CAPABILITY = "submit_deal";
7
+ export const DEFAULT_PROVIDER_URL = process.env.SEED_NETWORK_AGENT_AUTH_PROVIDER ?? "http://localhost:3000";
8
+ const DEFAULT_SEED_CAPABILITIES = [SEED_ONBOARDING_CAPABILITY, SEED_SUBMIT_DEAL_CAPABILITY];
9
+ export function createSeedAgentAuthClient(ui) {
10
+ return new AgentAuthClient({
11
+ storage: new SeedAgentAuthStorage(),
12
+ allowDirectDiscovery: true,
13
+ hostName: `Seed Network Agent on ${os.hostname()}`,
14
+ approvalTimeoutMs: 5 * 60 * 1000,
15
+ onApprovalRequired: async (approval) => {
16
+ const message = formatApprovalMessage(approval);
17
+ ui?.notify?.(message, "info");
18
+ if (approval.verification_uri_complete) {
19
+ await openUrl(approval.verification_uri_complete);
20
+ }
21
+ },
22
+ onApprovalStatusChange: async (status) => {
23
+ ui?.notify?.(`Seed Network Agent approval status: ${status}`, "info");
24
+ },
25
+ });
26
+ }
27
+ export async function connectSeedAgent(providerUrl, ui) {
28
+ const client = createSeedAgentAuthClient(ui);
29
+ try {
30
+ return await client.connectAgent({
31
+ provider: providerUrl,
32
+ mode: "delegated",
33
+ name: "Seed Network Agent",
34
+ capabilities: DEFAULT_SEED_CAPABILITIES,
35
+ reason: "Sync onboarding answers and submit deal intake records to the connected Seed Network account.",
36
+ preferredMethod: "device_authorization",
37
+ bindingMessage: "Approve Seed Network Agent workspace access",
38
+ });
39
+ }
40
+ finally {
41
+ client.destroy();
42
+ }
43
+ }
44
+ export async function listSeedAgentConnections() {
45
+ return new SeedAgentAuthStorage().listAgentConnections();
46
+ }
47
+ export async function syncOnboardingSubmission(submission, ui) {
48
+ const client = createSeedAgentAuthClient(ui);
49
+ try {
50
+ const connection = await ensureActiveSeedCapability(client, SEED_ONBOARDING_CAPABILITY, ui, {
51
+ reason: "Sync local Seed Network Agent onboarding answers to the connected Seed Network account.",
52
+ bindingMessage: "Approve Seed Network Agent onboarding sync",
53
+ });
54
+ return await client.executeCapability({
55
+ agentId: connection.agentId,
56
+ capability: SEED_ONBOARDING_CAPABILITY,
57
+ arguments: submission,
58
+ });
59
+ }
60
+ finally {
61
+ client.destroy();
62
+ }
63
+ }
64
+ export async function getPreferredConnection() {
65
+ const connections = await listSeedAgentConnections();
66
+ return connections.find((connection) => hasActiveCapability(connection, SEED_ONBOARDING_CAPABILITY))
67
+ ?? connections.find((connection) => hasActiveCapability(connection, SEED_SUBMIT_DEAL_CAPABILITY))
68
+ ?? connections[0];
69
+ }
70
+ export async function ensureActiveSeedCapability(client, capability, ui, approval) {
71
+ const storage = new SeedAgentAuthStorage();
72
+ let connection = await getConnectionWithCapability(capability);
73
+ if (connection)
74
+ return connection;
75
+ connection = await getPreferredConnection();
76
+ if (!connection) {
77
+ ui?.notify?.(`No Seed Network Agent Auth connection found. Connecting to ${DEFAULT_PROVIDER_URL}…`, "info");
78
+ await connectSeedAgent(DEFAULT_PROVIDER_URL, ui);
79
+ connection = await getConnectionWithCapability(capability);
80
+ if (connection)
81
+ return connection;
82
+ }
83
+ if (!connection) {
84
+ throw new Error("No Seed Network Agent Auth connection found. Run /seed connect first.");
85
+ }
86
+ // Refresh server-side grants before requesting. This recovers from a prior
87
+ // run where the user approved in the browser after the local command exited.
88
+ const status = await client.agentStatus(connection.agentId).catch(async (error) => {
89
+ if (isAgentAuthErrorCode(error, "agent_not_found", "agent_revoked", "agent_expired")) {
90
+ ui?.notify?.("Stored Seed Network Agent Auth connection is no longer active. Creating a fresh connection…", "warning");
91
+ await storage.deleteAgentConnection(connection.agentId);
92
+ return null;
93
+ }
94
+ throw error;
95
+ });
96
+ if (!status) {
97
+ await connectSeedAgent(DEFAULT_PROVIDER_URL, ui);
98
+ const refreshed = await getConnectionWithCapability(capability);
99
+ if (refreshed)
100
+ return refreshed;
101
+ throw new Error(`Seed Network capability was not granted after reconnecting: ${capability}`);
102
+ }
103
+ const existingGrant = status.agent_capability_grants.find((grant) => grant.capability === capability);
104
+ if (existingGrant?.status === "active") {
105
+ const refreshed = await storage.getAgentConnection(connection.agentId);
106
+ if (refreshed)
107
+ return refreshed;
108
+ }
109
+ if (existingGrant?.status === "pending") {
110
+ ui?.notify?.(`Seed Network capability ${capability} is already pending without a reusable approval link. Reconnecting to create a fresh approval request…`, "warning");
111
+ await client.disconnectAgent(connection.agentId);
112
+ await connectSeedAgent(DEFAULT_PROVIDER_URL, ui);
113
+ const refreshed = await getConnectionWithCapability(capability);
114
+ if (refreshed)
115
+ return refreshed;
116
+ throw new Error(`Seed Network capability was not granted after reconnecting: ${capability}`);
117
+ }
118
+ ui?.notify?.(`Requesting Seed Network capability: ${capability}…`, "info");
119
+ const result = await client.requestCapability({
120
+ agentId: connection.agentId,
121
+ capabilities: [capability],
122
+ reason: approval.reason,
123
+ preferredMethod: "device_authorization",
124
+ bindingMessage: approval.bindingMessage,
125
+ });
126
+ if (result.denied.includes(capability)) {
127
+ throw new Error(`Seed Network capability denied: ${capability}`);
128
+ }
129
+ if (!result.granted.includes(capability)) {
130
+ ui?.notify?.(`Waiting for Seed Network approval: ${capability}…`, "info");
131
+ return waitForActiveCapability(client, connection.agentId, capability);
132
+ }
133
+ const refreshed = await new SeedAgentAuthStorage().getAgentConnection(connection.agentId);
134
+ return refreshed ?? connection;
135
+ }
136
+ async function waitForActiveCapability(client, agentId, capability, timeoutMs = 5 * 60 * 1000) {
137
+ const deadline = Date.now() + timeoutMs;
138
+ let lastStatus = "unknown";
139
+ while (Date.now() < deadline) {
140
+ const status = await client.agentStatus(agentId);
141
+ lastStatus = status.status;
142
+ const grant = status.agent_capability_grants.find((candidate) => candidate.capability === capability);
143
+ if (grant?.status === "active") {
144
+ const refreshed = await new SeedAgentAuthStorage().getAgentConnection(agentId);
145
+ if (refreshed)
146
+ return refreshed;
147
+ }
148
+ if (grant?.status === "denied" || grant?.status === "revoked") {
149
+ throw new Error(`Seed Network capability ${grant.status}: ${capability}`);
150
+ }
151
+ if (status.status === "rejected" || status.status === "revoked" || status.status === "expired") {
152
+ throw new Error(`Seed Network agent ${status.status} while waiting for capability: ${capability}`);
153
+ }
154
+ await sleep(2000);
155
+ }
156
+ throw new Error(`Timed out waiting for Seed Network capability approval: ${capability} (${lastStatus})`);
157
+ }
158
+ async function getConnectionWithCapability(capability) {
159
+ const connections = await listSeedAgentConnections();
160
+ return connections.find((connection) => hasActiveCapability(connection, capability));
161
+ }
162
+ function hasActiveCapability(connection, capability) {
163
+ return connection.capabilityGrants.some((grant) => grant.capability === capability && grant.status === "active");
164
+ }
165
+ function isAgentAuthErrorCode(error, ...codes) {
166
+ return typeof error === "object"
167
+ && error !== null
168
+ && "code" in error
169
+ && typeof error.code === "string"
170
+ && codes.includes(error.code);
171
+ }
172
+ async function sleep(ms) {
173
+ await new Promise((resolve) => setTimeout(resolve, ms));
174
+ }
175
+ export async function disconnectSeedAgents(ui) {
176
+ const connections = await listSeedAgentConnections();
177
+ const client = createSeedAgentAuthClient(ui);
178
+ let disconnected = 0;
179
+ try {
180
+ for (const connection of connections) {
181
+ await client.disconnectAgent(connection.agentId);
182
+ disconnected += 1;
183
+ }
184
+ }
185
+ finally {
186
+ client.destroy();
187
+ }
188
+ return disconnected;
189
+ }
190
+ function formatApprovalMessage(approval) {
191
+ const lines = ["Seed Network Agent requires browser approval."];
192
+ if (approval.user_code)
193
+ lines.push(`Code: ${approval.user_code}`);
194
+ if (approval.verification_uri_complete)
195
+ lines.push(approval.verification_uri_complete);
196
+ else if (approval.verification_uri)
197
+ lines.push(approval.verification_uri);
198
+ return lines.join("\n");
199
+ }
200
+ async function openUrl(url) {
201
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
202
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
203
+ await new Promise((resolve) => {
204
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
205
+ child.on("error", () => resolve());
206
+ child.on("spawn", () => {
207
+ child.unref();
208
+ resolve();
209
+ });
210
+ });
211
+ }
package/dist/store.js ADDED
@@ -0,0 +1,92 @@
1
+ import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ export class OnboardingStore {
5
+ onboardingDir;
6
+ progressPath;
7
+ submissionsDir;
8
+ constructor(baseDir = path.join(os.homedir(), ".seed-network-agent")) {
9
+ this.onboardingDir = path.join(baseDir, "onboarding");
10
+ this.progressPath = path.join(this.onboardingDir, "progress.json");
11
+ this.submissionsDir = path.join(this.onboardingDir, "submissions");
12
+ }
13
+ async loadProgress() {
14
+ try {
15
+ const raw = await readFile(this.progressPath, "utf8");
16
+ const parsed = JSON.parse(raw);
17
+ if (!parsed.answers || parsed.completedAt)
18
+ return undefined;
19
+ return parsed;
20
+ }
21
+ catch (error) {
22
+ if (isNodeError(error) && error.code === "ENOENT")
23
+ return undefined;
24
+ throw error;
25
+ }
26
+ }
27
+ async recordAnswer(questionnaireVersion, question, answer) {
28
+ const existing = await this.loadProgress();
29
+ const now = new Date().toISOString();
30
+ const progress = {
31
+ questionnaireVersion,
32
+ startedAt: existing?.startedAt ?? now,
33
+ updatedAt: now,
34
+ answers: {
35
+ ...(existing?.answers ?? {}),
36
+ [question.id]: answer,
37
+ },
38
+ };
39
+ await this.writeProgress(progress);
40
+ }
41
+ async saveSubmission(submission) {
42
+ await mkdir(this.submissionsDir, { recursive: true });
43
+ const fileName = `${safeTimestamp(submission.submittedAt)}.json`;
44
+ const submissionPath = path.join(this.submissionsDir, fileName);
45
+ await writeJsonAtomic(submissionPath, submission);
46
+ const progress = await this.loadProgress();
47
+ if (progress) {
48
+ await this.writeProgress({
49
+ ...progress,
50
+ updatedAt: submission.submittedAt,
51
+ completedAt: submission.submittedAt,
52
+ answers: submission.answers,
53
+ });
54
+ }
55
+ return submissionPath;
56
+ }
57
+ async loadLatestSubmission() {
58
+ try {
59
+ const entries = await readdir(this.submissionsDir, { withFileTypes: true });
60
+ const fileNames = entries
61
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
62
+ .map((entry) => entry.name)
63
+ .sort()
64
+ .reverse();
65
+ for (const fileName of fileNames) {
66
+ const raw = await readFile(path.join(this.submissionsDir, fileName), "utf8");
67
+ return JSON.parse(raw);
68
+ }
69
+ return undefined;
70
+ }
71
+ catch (error) {
72
+ if (isNodeError(error) && error.code === "ENOENT")
73
+ return undefined;
74
+ throw error;
75
+ }
76
+ }
77
+ async writeProgress(progress) {
78
+ await mkdir(this.onboardingDir, { recursive: true });
79
+ await writeJsonAtomic(this.progressPath, progress);
80
+ }
81
+ }
82
+ async function writeJsonAtomic(filePath, value) {
83
+ const tmpPath = `${filePath}.${process.pid}.tmp`;
84
+ await writeFile(tmpPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
85
+ await rename(tmpPath, filePath);
86
+ }
87
+ function safeTimestamp(value) {
88
+ return value.replace(/[:.]/g, "-");
89
+ }
90
+ function isNodeError(error) {
91
+ return typeof error === "object" && error !== null && "code" in error;
92
+ }