@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,131 @@
1
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ const DEFAULT_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
5
+ const REGISTRY_TIMEOUT_MS = 1500;
6
+ const UPDATE_CACHE_PATH = path.join(os.homedir(), ".seed-network-agent", "update-check.json");
7
+ export async function maybeNotifyUpdate(packageRoot) {
8
+ if (isUpdateCheckDisabled())
9
+ return;
10
+ try {
11
+ const packageInfo = await readPackageInfo(packageRoot);
12
+ if (!packageInfo.name || !packageInfo.version)
13
+ return;
14
+ const cached = await readCache();
15
+ const latestVersion = shouldUseCache(cached, packageInfo.name)
16
+ ? cached.latestVersion
17
+ : await fetchAndCacheLatestVersion(packageInfo.name);
18
+ if (!latestVersion || !isNewerVersion(latestVersion, packageInfo.version))
19
+ return;
20
+ notifyUpdate({
21
+ packageName: packageInfo.name,
22
+ currentVersion: packageInfo.version,
23
+ latestVersion,
24
+ });
25
+ }
26
+ catch {
27
+ // Update checks should never prevent the local agent from starting.
28
+ }
29
+ }
30
+ async function readPackageInfo(packageRoot) {
31
+ const raw = await readFile(path.join(packageRoot, "package.json"), "utf8");
32
+ return JSON.parse(raw);
33
+ }
34
+ function isUpdateCheckDisabled() {
35
+ return isTruthy(process.env.SEED_NETWORK_AGENT_NO_UPDATE_CHECK)
36
+ || isTruthy(process.env.SEED_AGENT_NO_UPDATE_CHECK)
37
+ || isTruthy(process.env.PI_OFFLINE)
38
+ || process.env.NO_UPDATE_NOTIFIER === "1";
39
+ }
40
+ function isTruthy(value) {
41
+ return value === "1" || value === "true" || value === "yes";
42
+ }
43
+ function shouldUseCache(cache, packageName) {
44
+ if (!cache?.checkedAt || cache.packageName !== packageName)
45
+ return false;
46
+ const checkedAt = Date.parse(cache.checkedAt);
47
+ return Number.isFinite(checkedAt) && Date.now() - checkedAt < DEFAULT_CHECK_INTERVAL_MS;
48
+ }
49
+ async function fetchAndCacheLatestVersion(packageName) {
50
+ let latestVersion;
51
+ try {
52
+ const registryPackageName = packageName.startsWith("@")
53
+ ? packageName.replace("/", "%2F")
54
+ : encodeURIComponent(packageName);
55
+ const response = await fetch(`https://registry.npmjs.org/${registryPackageName}/latest`, {
56
+ signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS),
57
+ headers: { accept: "application/json" },
58
+ });
59
+ if (response.ok) {
60
+ const body = await response.json();
61
+ latestVersion = typeof body.version === "string" ? body.version : undefined;
62
+ }
63
+ }
64
+ finally {
65
+ await writeCache({
66
+ checkedAt: new Date().toISOString(),
67
+ packageName,
68
+ latestVersion,
69
+ }).catch(() => undefined);
70
+ }
71
+ return latestVersion;
72
+ }
73
+ async function readCache() {
74
+ try {
75
+ const raw = await readFile(UPDATE_CACHE_PATH, "utf8");
76
+ return JSON.parse(raw);
77
+ }
78
+ catch (error) {
79
+ if (isNodeError(error) && error.code === "ENOENT")
80
+ return undefined;
81
+ throw error;
82
+ }
83
+ }
84
+ async function writeCache(cache) {
85
+ await mkdir(path.dirname(UPDATE_CACHE_PATH), { recursive: true });
86
+ const tmpPath = `${UPDATE_CACHE_PATH}.${process.pid}.tmp`;
87
+ await writeFile(tmpPath, `${JSON.stringify(cache, null, 2)}\n`, "utf8");
88
+ await rename(tmpPath, UPDATE_CACHE_PATH);
89
+ }
90
+ function notifyUpdate(input) {
91
+ const updateCommand = `npm install -g ${input.packageName}@latest`;
92
+ process.stderr.write([
93
+ "",
94
+ `Seed Network Agent update available: ${input.currentVersion} → ${input.latestVersion}`,
95
+ `Update with: ${updateCommand}`,
96
+ `Or run once with: npx ${input.packageName}@latest`,
97
+ "Set SEED_NETWORK_AGENT_NO_UPDATE_CHECK=1 to disable this check.",
98
+ "",
99
+ ].join("\n"));
100
+ }
101
+ function isNewerVersion(candidate, current) {
102
+ const candidateVersion = parseSemver(candidate);
103
+ const currentVersion = parseSemver(current);
104
+ if (!candidateVersion || !currentVersion)
105
+ return candidate !== current;
106
+ for (const key of ["major", "minor", "patch"]) {
107
+ if (candidateVersion[key] > currentVersion[key])
108
+ return true;
109
+ if (candidateVersion[key] < currentVersion[key])
110
+ return false;
111
+ }
112
+ if (!candidateVersion.prerelease && currentVersion.prerelease)
113
+ return true;
114
+ if (candidateVersion.prerelease && !currentVersion.prerelease)
115
+ return false;
116
+ return false;
117
+ }
118
+ function parseSemver(value) {
119
+ const match = value.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);
120
+ if (!match)
121
+ return undefined;
122
+ return {
123
+ major: Number(match[1]),
124
+ minor: Number(match[2]),
125
+ patch: Number(match[3]),
126
+ prerelease: match[4],
127
+ };
128
+ }
129
+ function isNodeError(error) {
130
+ return typeof error === "object" && error !== null && "code" in error;
131
+ }
package/dist/wizard.js ADDED
@@ -0,0 +1,116 @@
1
+ export async function runOnboardingWizard(ui, questionnaire, options = {}) {
2
+ validateQuestionnaire(questionnaire);
3
+ const answers = { ...(options.initialAnswers ?? {}) };
4
+ ui.notify?.(options.startMessage ?? `${questionnaire.title}: starting local onboarding.`, "info");
5
+ for (const question of questionnaire.questions) {
6
+ const answer = await askQuestion(ui, question, answers[question.id]);
7
+ if (!answer) {
8
+ return { cancelled: true, answers };
9
+ }
10
+ answers[question.id] = answer;
11
+ await options.onAnswer?.(question, answer);
12
+ }
13
+ const confirmed = await ui.confirm(options.reviewTitle ?? "Review onboarding answers", formatReview(questionnaire, answers, options));
14
+ if (!confirmed) {
15
+ if (!options.suppressReviewCancelledMessage) {
16
+ ui.notify?.(options.reviewCancelledMessage ?? "Onboarding review cancelled. Progress remains saved locally.", "warning");
17
+ }
18
+ return { cancelled: true, answers };
19
+ }
20
+ return {
21
+ cancelled: false,
22
+ answers,
23
+ submission: {
24
+ questionnaireVersion: questionnaire.version,
25
+ title: questionnaire.title,
26
+ submittedAt: new Date().toISOString(),
27
+ answers,
28
+ },
29
+ };
30
+ }
31
+ async function askQuestion(ui, question, existing) {
32
+ const label = question.label ?? question.id;
33
+ const title = existing
34
+ ? `${label}: ${question.prompt}\nCurrent answer: ${displayAnswer(existing)}`
35
+ : `${label}: ${question.prompt}`;
36
+ if (question.type === "text") {
37
+ return askTextQuestion(ui, question, title);
38
+ }
39
+ return askSingleSelectQuestion(ui, question, title);
40
+ }
41
+ async function askTextQuestion(ui, question, title) {
42
+ while (true) {
43
+ const value = await ui.input(title, question.placeholder);
44
+ if (value === undefined)
45
+ return undefined;
46
+ const trimmed = value.trim();
47
+ if (question.required && trimmed.length === 0) {
48
+ ui.notify?.("This question is required.", "warning");
49
+ continue;
50
+ }
51
+ return buildAnswer(question, trimmed);
52
+ }
53
+ }
54
+ async function askSingleSelectQuestion(ui, question, title) {
55
+ const choices = question.options ?? [];
56
+ const labels = choices.map((option) => option.label);
57
+ while (true) {
58
+ const selected = await ui.select(title, labels);
59
+ if (selected === undefined)
60
+ return undefined;
61
+ const option = choices.find((candidate) => candidate.label === selected);
62
+ if (!option) {
63
+ ui.notify?.("Please choose one of the listed options.", "warning");
64
+ continue;
65
+ }
66
+ return buildAnswer(question, option.value, option.label);
67
+ }
68
+ }
69
+ function buildAnswer(question, value, label) {
70
+ return {
71
+ questionId: question.id,
72
+ questionLabel: question.label ?? question.id,
73
+ questionPrompt: question.prompt,
74
+ type: question.type,
75
+ value,
76
+ label,
77
+ answeredAt: new Date().toISOString(),
78
+ };
79
+ }
80
+ function displayAnswer(answer) {
81
+ return (answer.label ?? answer.value) || "(blank)";
82
+ }
83
+ function formatReview(questionnaire, answers, options = {}) {
84
+ const lines = [
85
+ options.reviewDescription
86
+ ?? questionnaire.description
87
+ ?? "Review your answers before saving the final local submission.",
88
+ "",
89
+ ];
90
+ for (const question of questionnaire.questions) {
91
+ const answer = answers[question.id];
92
+ lines.push(`${question.label ?? question.id}: ${answer ? displayAnswer(answer) : "(unanswered)"}`);
93
+ }
94
+ lines.push("", options.reviewConfirmPrompt ?? "Save this final submission locally?");
95
+ return lines.join("\n");
96
+ }
97
+ function validateQuestionnaire(questionnaire) {
98
+ if (!questionnaire.version)
99
+ throw new Error("Questionnaire is missing version.");
100
+ if (!Array.isArray(questionnaire.questions))
101
+ throw new Error("Questionnaire questions must be an array.");
102
+ const ids = new Set();
103
+ for (const question of questionnaire.questions) {
104
+ if (!question.id)
105
+ throw new Error("Question is missing id.");
106
+ if (ids.has(question.id))
107
+ throw new Error(`Duplicate question id: ${question.id}`);
108
+ ids.add(question.id);
109
+ if (question.type !== "text" && question.type !== "single_select") {
110
+ throw new Error(`Unsupported question type for ${question.id}: ${String(question.type)}`);
111
+ }
112
+ if (question.type === "single_select" && (!question.options || question.options.length === 0)) {
113
+ throw new Error(`single_select question ${question.id} must include options.`);
114
+ }
115
+ }
116
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@connormartin/seed-network-agent",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "bin": {
7
+ "seed": "./dist/cli.js",
8
+ "seed-agent": "./dist/cli.js",
9
+ "seed-network-agent": "./dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "prompts",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "build": "rm -rf dist && tsc -p tsconfig.json && mkdir -p dist/features/onboarding dist/features/submit-deal-pdf && cp src/features/onboarding/questionnaire.v1.json dist/features/onboarding/questionnaire.v1.json && cp src/features/submit-deal-pdf/questionnaire.v1.json dist/features/submit-deal-pdf/questionnaire.v1.json && chmod +x dist/cli.js",
18
+ "dev": "tsx src/cli.ts",
19
+ "start": "node dist/cli.js",
20
+ "prepack": "npm run build"
21
+ },
22
+ "engines": {
23
+ "node": ">=22"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "dependencies": {
29
+ "@auth/agent": "0.4.6",
30
+ "@earendil-works/pi-ai": "^0.74.0",
31
+ "@earendil-works/pi-coding-agent": "^0.74.0",
32
+ "pdf-parse": "^2.4.5"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^25.6.0",
36
+ "tsx": "^4.21.0",
37
+ "typescript": "^6.0.3"
38
+ }
39
+ }
@@ -0,0 +1,12 @@
1
+ You are Seed Network Agent, a thin wrapper around upstream Pi.
2
+
3
+ The seed network is a private network of angel investors that collectively identify, diligence, and invest in early stage startups.
4
+
5
+ Keep the Seed-specific layer obvious:
6
+ - Use clear, direct language.
7
+ - Be explicit when a behavior is part of the Seed onboarding wrapper.
8
+ - Be explicit that onboarding answers stay local unless the user connects Agent Auth and approves syncing.
9
+ - Use `/submit-deal` for the deterministic deal intake flow; it creates a deal only after the user reviews the answers and the Agent Auth capability is granted.
10
+ - After onboarding, behave like normal Pi unless the user asks for Seed-specific workflows.
11
+
12
+ The deterministic onboarding and deal submission sequences are available through extension commands, not enforced by this prompt.