@clubnet/seedclub 0.2.38 → 0.2.39
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/assets/SYSTEM.md +54 -5
- package/assets/extensions/seedclub/commands/seedclub.ts +116 -0
- package/assets/extensions/seedclub/index.ts +5 -2
- package/assets/extensions/seedclub/memory-client.js +8 -0
- package/assets/extensions/seedclub/memory.ts +279 -28
- package/assets/extensions/seedclub/tools/deal-sourcing.ts +427 -0
- package/assets/extensions/seedclub/ui-copy.ts +6 -0
- package/package.json +4 -3
- package/packages/seedclub-tui/src/app/interactive-mode.mjs +57 -8
- package/postinstall.js +6 -1
package/assets/SYSTEM.md
CHANGED
|
@@ -1,11 +1,51 @@
|
|
|
1
|
-
You are a
|
|
1
|
+
You are a Seed Club member agent.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
You help Seed Club members research, understand, remember, and act on early-stage startup deals. You are a sharp investing thought partner first, and a workflow agent second.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Seed Club's goal is to become the core knowledge base for angel investors and the network they invest from. Every deal conversation should make the member smarter, improve their private investing memory, and, when approved, add useful intelligence to the network.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
## Core Posture
|
|
8
|
+
|
|
9
|
+
Members should send you every potentially interesting deck, founder pitch, company, link, or idea. They do not need conviction first. If something seems potentially interesting, help them dig in and consider surfacing it.
|
|
10
|
+
|
|
11
|
+
Default stance:
|
|
12
|
+
|
|
13
|
+
- Be concise in chat.
|
|
14
|
+
- Be opinionated, but provisional.
|
|
15
|
+
- Lead with the core insight.
|
|
16
|
+
- Use specifics and numbers.
|
|
17
|
+
- Flag unknowns clearly.
|
|
18
|
+
- Ask only high-leverage follow-up questions.
|
|
19
|
+
- Help the member form their own judgment.
|
|
20
|
+
- Do not act like your opinion is authoritative.
|
|
21
|
+
- Do not tell the member what to invest in.
|
|
22
|
+
|
|
23
|
+
You are allowed to share takes. Your take exists to start a better conversation.
|
|
24
|
+
|
|
25
|
+
Good:
|
|
26
|
+
|
|
27
|
+
"My first read is positive, but the deal turns on venue dependency."
|
|
28
|
+
"The category signal is real; I'd pressure-test whether this team has distribution."
|
|
29
|
+
"I might be wrong if the founder has a non-obvious GTM wedge."
|
|
30
|
+
|
|
31
|
+
Bad:
|
|
32
|
+
|
|
33
|
+
"This is a good deal."
|
|
34
|
+
"You should invest."
|
|
35
|
+
"The right answer is to push."
|
|
36
|
+
|
|
37
|
+
## Opinion Format
|
|
38
|
+
|
|
39
|
+
When sharing a take, use this structure:
|
|
40
|
+
|
|
41
|
+
1. First read: positive / negative / mixed / unclear
|
|
42
|
+
2. Why: 1-3 strongest reasons
|
|
43
|
+
3. Main risk or open question
|
|
44
|
+
4. What to pull on next
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
|
|
48
|
+
"First read: worth digging in. The category signal is real, the price is reasonable, and they've shipped more than most pre-seed teams. The main risk is whether Polymarket/Kalshi can build or block the consumer layer. I'd pull on venue relationships first."
|
|
9
49
|
|
|
10
50
|
Operating model:
|
|
11
51
|
|
|
@@ -36,6 +76,15 @@ Seed Club platform policy:
|
|
|
36
76
|
- Stay scoped to the user's platform permissions and do not imply access the user does not have.
|
|
37
77
|
- Do not claim Seed Club platform data is unavailable unless a relevant Seed Club tool actually returns an auth, permission, or not-found failure.
|
|
38
78
|
|
|
79
|
+
Research and review policy:
|
|
80
|
+
|
|
81
|
+
- Treat deal, company, founder, pitch deck, memo, and research workflows as `seed-network` by default unless the user explicitly names another program.
|
|
82
|
+
- When a member drops a deck, memo, PDF, document, or image, use the Seed Club research upload tool without asking for the program and let it create a private research draft first.
|
|
83
|
+
- Do not submit a deal for Seed Club review during the initial upload, even if the user sounds excited. First review the returned document preview and summarize Founder info, Company info, Deal info, and Opportunity Summary.
|
|
84
|
+
- After summarizing, ask for missing founder/contact path, ask amount or explicit ask unknown, founder relationship, and excitement/vouch context before pushing.
|
|
85
|
+
- Submit for Seed Club review only after the member explicitly confirms. Passing or holding should stay private and should not create a shared review handoff.
|
|
86
|
+
- If document preview is partial or failed, say what could not be read and ask targeted questions instead of inventing missing facts.
|
|
87
|
+
|
|
39
88
|
External research policy:
|
|
40
89
|
|
|
41
90
|
- If the user asks an open-ended research question that is not explicitly scoped to Seed Club records, do a fast external research pass before answering.
|
|
@@ -57,6 +57,45 @@ async function setSeedEnvironment(mode: "local" | "prod", ctx: any) {
|
|
|
57
57
|
);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
async function getDefaultProgramSlug() {
|
|
61
|
+
const session = await getSessionContext();
|
|
62
|
+
if ("error" in session || !Array.isArray(session.program_access)) return "<program-slug>";
|
|
63
|
+
return session.program_access[0]?.program?.slug || "<program-slug>";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function prefillEditor(ctx: any, text: string) {
|
|
67
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
68
|
+
ctx.ui.setEditorText(text);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function showCalendarMenu(ctx: any, deps: SeedclubDeps) {
|
|
72
|
+
const choice = await ctx.ui.select("Calendar", [
|
|
73
|
+
"Connect personal calendar",
|
|
74
|
+
"Disconnect personal calendar",
|
|
75
|
+
"Check availability",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
switch (choice) {
|
|
79
|
+
case "Connect personal calendar":
|
|
80
|
+
await deps.connectCalendar(ctx);
|
|
81
|
+
break;
|
|
82
|
+
case "Disconnect personal calendar":
|
|
83
|
+
await prefillEditor(
|
|
84
|
+
ctx,
|
|
85
|
+
"Help me disconnect my personal Google Calendar from Seed Club. Check the currently connected calendar account first and only proceed with a real disconnect if a supported Seed Club tool or route exists; otherwise tell me what manual action is needed.",
|
|
86
|
+
);
|
|
87
|
+
break;
|
|
88
|
+
case "Check availability": {
|
|
89
|
+
const defaultProgramSlug = await getDefaultProgramSlug();
|
|
90
|
+
await prefillEditor(
|
|
91
|
+
ctx,
|
|
92
|
+
`Check meeting availability for program ${defaultProgramSlug}. Ask me for the target date first if I have not provided one, then use configured availability slots exactly.`,
|
|
93
|
+
);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
60
99
|
export function registerSeedclubCommand(pi: ExtensionAPI, deps: SeedclubDeps) {
|
|
61
100
|
pi.registerCommand("connect", {
|
|
62
101
|
description: "Connect your Seed Club account",
|
|
@@ -105,6 +144,83 @@ export function registerSeedclubCommand(pi: ExtensionAPI, deps: SeedclubDeps) {
|
|
|
105
144
|
},
|
|
106
145
|
});
|
|
107
146
|
|
|
147
|
+
pi.registerCommand("calendar", {
|
|
148
|
+
description: "Open calendar connect, disconnect, and availability actions",
|
|
149
|
+
handler: async (args, ctx) => {
|
|
150
|
+
const action = args?.trim().toLowerCase();
|
|
151
|
+
if (action === "connect") {
|
|
152
|
+
await deps.connectCalendar(ctx);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (action === "disconnect") {
|
|
156
|
+
await prefillEditor(
|
|
157
|
+
ctx,
|
|
158
|
+
"Help me disconnect my personal Google Calendar from Seed Club. Check the currently connected calendar account first and only proceed with a real disconnect if a supported Seed Club tool or route exists; otherwise tell me what manual action is needed.",
|
|
159
|
+
);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (action === "availability") {
|
|
163
|
+
const defaultProgramSlug = await getDefaultProgramSlug();
|
|
164
|
+
await prefillEditor(
|
|
165
|
+
ctx,
|
|
166
|
+
`Check meeting availability for program ${defaultProgramSlug}. Ask me for the target date first if I have not provided one, then use configured availability slots exactly.`,
|
|
167
|
+
);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await showCalendarMenu(ctx, deps);
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
pi.registerCommand("research", {
|
|
175
|
+
description: "Open a Seed Club research prompt",
|
|
176
|
+
handler: async (_args, ctx) => {
|
|
177
|
+
await prefillEditor(
|
|
178
|
+
ctx,
|
|
179
|
+
"Help me research a Seed Network opportunity. Ask me for the company, deck, memo, source URL, or local file if needed. Save any provided material as private research first, then use Seed Club records and source-backed external research before giving me a concise first read.",
|
|
180
|
+
);
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
pi.registerCommand("source", {
|
|
185
|
+
description: "Open a source-backed research prompt",
|
|
186
|
+
handler: async (_args, ctx) => {
|
|
187
|
+
await prefillEditor(
|
|
188
|
+
ctx,
|
|
189
|
+
"Research this using source-backed evidence. Gather and verify primary or reputable sources, cite source URLs inline, separate Seed Club records from external sources, and flag unknowns clearly.",
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
pi.registerCommand("worldview", {
|
|
195
|
+
description: "Open an investing worldview prompt",
|
|
196
|
+
handler: async (_args, ctx) => {
|
|
197
|
+
await prefillEditor(
|
|
198
|
+
ctx,
|
|
199
|
+
"Summarize my current Seed Club investing worldview from recent memory and Seed Club context. Focus on beliefs, decision heuristics, and places where my view has changed. Flag what evidence supports each point.",
|
|
200
|
+
);
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
pi.registerCommand("theses", {
|
|
205
|
+
description: "Open an investment theses prompt",
|
|
206
|
+
handler: async (_args, ctx) => {
|
|
207
|
+
await prefillEditor(
|
|
208
|
+
ctx,
|
|
209
|
+
"Draft my current investment theses from Seed Club memory and recent deal context. Group by thesis, include supporting signals, counterexamples, and what would change my mind.",
|
|
210
|
+
);
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
pi.registerCommand("concerns", {
|
|
215
|
+
description: "Open an investment concerns prompt",
|
|
216
|
+
handler: async (_args, ctx) => {
|
|
217
|
+
await prefillEditor(
|
|
218
|
+
ctx,
|
|
219
|
+
"List my current recurring investment concerns from Seed Club memory and recent deal context. Group concerns by theme, name the pattern, cite representative context when available, and separate known concerns from guesses.",
|
|
220
|
+
);
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
108
224
|
pi.registerCommand("seedclub", {
|
|
109
225
|
description: "Seed Club",
|
|
110
226
|
handler: async (args, ctx) => {
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from "./recent-entities.js";
|
|
33
33
|
import { getCurrentUser, getSessionContext, registerUtilityTools } from "./tools/utility.js";
|
|
34
34
|
import { registerCrmTools } from "./tools/crm.js";
|
|
35
|
+
import { registerDealSourcingTools } from "./tools/deal-sourcing.js";
|
|
35
36
|
import { registerMeetingTools } from "./tools/meetings.js";
|
|
36
37
|
import { registerMediaTools } from "./tools/media.js";
|
|
37
38
|
import { registerWebTools } from "./tools/web.js";
|
|
@@ -208,6 +209,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
208
209
|
// Tools
|
|
209
210
|
registerUtilityTools(pi);
|
|
210
211
|
registerCrmTools(pi);
|
|
212
|
+
registerDealSourcingTools(pi);
|
|
211
213
|
registerMeetingTools(pi);
|
|
212
214
|
registerMediaTools(pi);
|
|
213
215
|
registerWebTools(pi);
|
|
@@ -433,6 +435,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
433
435
|
|
|
434
436
|
await applyConnectedStatus(ctx, user);
|
|
435
437
|
void refreshPromptContext();
|
|
438
|
+
void memory.enableByInstallDefault(ctx);
|
|
436
439
|
markAuthComplete(getPostAuthInstruction(ctx));
|
|
437
440
|
return user;
|
|
438
441
|
}
|
|
@@ -451,7 +454,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
451
454
|
markAuthInProgress({ message: "Checking Seed Club access..." });
|
|
452
455
|
}
|
|
453
456
|
void ensureSeedclubAuthenticated(ctx).then((authenticated) => {
|
|
454
|
-
if (authenticated) void memory.
|
|
457
|
+
if (authenticated) void memory.enableByInstallDefault(ctx);
|
|
455
458
|
}).catch((error) => {
|
|
456
459
|
const message = error instanceof Error ? error.message : "Unable to verify Seed Club access.";
|
|
457
460
|
markAuthRequired({
|
|
@@ -679,7 +682,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
679
682
|
await storeToken(token, result.email, apiBase, { authBase, name: result.name });
|
|
680
683
|
await applyConnectedStatus(ctx, result);
|
|
681
684
|
await refreshPromptContext();
|
|
682
|
-
void memory.
|
|
685
|
+
void memory.enableByInstallDefault(ctx);
|
|
683
686
|
const nextStep = getPostAuthInstruction(ctx);
|
|
684
687
|
markAuthComplete(nextStep);
|
|
685
688
|
if (options?.notifyOnSuccess) {
|
|
@@ -48,6 +48,14 @@ export async function fetchMemoryContextFromApi(apiClient, payload, timeoutMs) {
|
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
export async function fetchMemoryRecentFromApi(apiClient, payload, timeoutMs) {
|
|
52
|
+
return requestWithTimeout(
|
|
53
|
+
apiClient.post("/agent-memory/recent", payload),
|
|
54
|
+
timeoutMs,
|
|
55
|
+
"Seed Club recent memory",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
export async function writeMemoryEventToApi(apiClient, payload, timeoutMs) {
|
|
52
60
|
return requestWithTimeout(
|
|
53
61
|
apiClient.post("/agent-memory/events", payload),
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
4
7
|
import { NotConnectedError, api } from "./api-client.js";
|
|
5
8
|
import {
|
|
6
|
-
|
|
9
|
+
fetchMemoryRecentFromApi,
|
|
7
10
|
fetchMemoryStatusFromApi,
|
|
8
11
|
requestWithTimeout,
|
|
9
12
|
setMemoryEnabledWithApi,
|
|
@@ -13,7 +16,6 @@ import {
|
|
|
13
16
|
buildMemoryStatusLines,
|
|
14
17
|
detectWorkflowTags,
|
|
15
18
|
extractTextFromMessage,
|
|
16
|
-
formatMemoryPromptBlock,
|
|
17
19
|
getMemoryStatusLabel,
|
|
18
20
|
truncateAssistantText,
|
|
19
21
|
uniqueToolNames,
|
|
@@ -22,8 +24,9 @@ import {
|
|
|
22
24
|
const MEMORY_CONTEXT_TIMEOUT_MS = 20000;
|
|
23
25
|
const MEMORY_STATUS_TIMEOUT_MS = 20000;
|
|
24
26
|
const MEMORY_WRITE_TIMEOUT_MS = 20000;
|
|
25
|
-
const MEMORY_CONTEXT_MAX_TOKENS = 900;
|
|
26
27
|
const MEMORY_SESSION_STRATEGY = String(process.env.SEEDCLUB_MEMORY_SESSION_STRATEGY ?? "repo").toLowerCase();
|
|
28
|
+
const MEMORY_DEFAULT_ENABLED_KEY = "seedclubMemoryDefaultEnabled";
|
|
29
|
+
const MEMORY_DEFAULT_APPLIED_KEY = "seedclubMemoryDefaultApplied";
|
|
27
30
|
|
|
28
31
|
type MemoryStatus = {
|
|
29
32
|
available: boolean;
|
|
@@ -32,19 +35,11 @@ type MemoryStatus = {
|
|
|
32
35
|
workspaceSlug?: string;
|
|
33
36
|
};
|
|
34
37
|
|
|
35
|
-
type MemoryContextResponse = {
|
|
36
|
-
memoryBlock?: string;
|
|
37
|
-
sourceCounts?: {
|
|
38
|
-
user?: number;
|
|
39
|
-
team?: number;
|
|
40
|
-
workflow?: number;
|
|
41
|
-
};
|
|
42
|
-
};
|
|
43
|
-
|
|
44
38
|
type ProgramMemorySlug = "11am" | "seed-network";
|
|
45
39
|
|
|
46
40
|
type MemoryController = {
|
|
47
41
|
refreshStatus: (ctx: any, options?: { notifySetup?: boolean }) => Promise<MemoryStatus | null>;
|
|
42
|
+
enableByInstallDefault: (ctx: any) => Promise<MemoryStatus | null>;
|
|
48
43
|
clear: (ctx: any) => void;
|
|
49
44
|
showMenu: (ctx: any) => Promise<void>;
|
|
50
45
|
};
|
|
@@ -94,6 +89,33 @@ function stableId(scope: string, value: string) {
|
|
|
94
89
|
return `seedclub_${scope}_${hash}`;
|
|
95
90
|
}
|
|
96
91
|
|
|
92
|
+
function getSettingsPath() {
|
|
93
|
+
const agentDir = process.env.SEEDCLUB_CODING_AGENT_DIR || process.env.SEEDCLUB_AGENT_DIR || join(homedir(), ".seedclub", "agent");
|
|
94
|
+
return join(agentDir, "settings.json");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function readMemoryDefaultSettings() {
|
|
98
|
+
try {
|
|
99
|
+
const raw = JSON.parse(await readFile(getSettingsPath(), "utf8"));
|
|
100
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
101
|
+
return { settings: null, enabled: false, applied: true };
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
settings: raw as Record<string, unknown>,
|
|
105
|
+
enabled: raw[MEMORY_DEFAULT_ENABLED_KEY] === true,
|
|
106
|
+
applied: raw[MEMORY_DEFAULT_APPLIED_KEY] === true,
|
|
107
|
+
};
|
|
108
|
+
} catch {
|
|
109
|
+
return { settings: null, enabled: false, applied: true };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function markMemoryDefaultApplied(settings: Record<string, unknown> | null) {
|
|
114
|
+
if (!settings) return;
|
|
115
|
+
settings[MEMORY_DEFAULT_APPLIED_KEY] = true;
|
|
116
|
+
await writeFile(getSettingsPath(), `${JSON.stringify(settings, null, 2)}\n`);
|
|
117
|
+
}
|
|
118
|
+
|
|
97
119
|
function updateMemoryStatus(ctx: any, state: { status: MemoryStatus | null; lastError: string | null }) {
|
|
98
120
|
try {
|
|
99
121
|
ctx?.ui?.setStatus?.("memory", getMemoryStatusLabel({
|
|
@@ -144,7 +166,7 @@ export function registerMemory(pi: ExtensionAPI): MemoryController {
|
|
|
144
166
|
ctx.ui.notify(`Seed Club memory is unavailable.${reason} Use /memory status for details.`, "info");
|
|
145
167
|
}
|
|
146
168
|
if (status.available && status.enabled) {
|
|
147
|
-
void
|
|
169
|
+
void refreshSessionMemorySnapshot(ctx);
|
|
148
170
|
} else {
|
|
149
171
|
state.cachedMemoryBlock = null;
|
|
150
172
|
}
|
|
@@ -185,7 +207,7 @@ export function registerMemory(pi: ExtensionAPI): MemoryController {
|
|
|
185
207
|
);
|
|
186
208
|
state.status = status;
|
|
187
209
|
state.lastError = null;
|
|
188
|
-
if (enabled && status.available) void
|
|
210
|
+
if (enabled && status.available) void refreshSessionMemorySnapshot(ctx);
|
|
189
211
|
if (!enabled) state.cachedMemoryBlock = null;
|
|
190
212
|
updateMemoryStatus(ctx, state);
|
|
191
213
|
ctx.ui.notify(enabled ? "Memory enabled." : "Memory disabled.", "info");
|
|
@@ -196,37 +218,89 @@ export function registerMemory(pi: ExtensionAPI): MemoryController {
|
|
|
196
218
|
}
|
|
197
219
|
}
|
|
198
220
|
|
|
221
|
+
async function enableByInstallDefault(ctx: any) {
|
|
222
|
+
const defaultSettings = await readMemoryDefaultSettings();
|
|
223
|
+
if (!defaultSettings.enabled || defaultSettings.applied) return refreshStatus(ctx, { notifySetup: true });
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
let status = await refreshStatus(ctx, { notifySetup: true });
|
|
227
|
+
if (!status?.available) return status;
|
|
228
|
+
|
|
229
|
+
if (!status.enabled) {
|
|
230
|
+
status = await requestWithTimeout(
|
|
231
|
+
setMemoryEnabledWithApi(api, true) as Promise<MemoryStatus>,
|
|
232
|
+
MEMORY_STATUS_TIMEOUT_MS,
|
|
233
|
+
"Seed Club memory preference",
|
|
234
|
+
);
|
|
235
|
+
state.status = status;
|
|
236
|
+
state.lastError = null;
|
|
237
|
+
updateMemoryStatus(ctx, state);
|
|
238
|
+
if (status.available && status.enabled) void refreshSessionMemorySnapshot(ctx);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (status.available && status.enabled) {
|
|
242
|
+
await markMemoryDefaultApplied(defaultSettings.settings);
|
|
243
|
+
}
|
|
244
|
+
return status;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (error instanceof NotConnectedError) return null;
|
|
247
|
+
state.lastError = errorMessage(error);
|
|
248
|
+
updateMemoryStatus(ctx, state);
|
|
249
|
+
ctx.ui.notify(`Memory default setup failed: ${state.lastError}`, "warning");
|
|
250
|
+
return state.status;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
199
254
|
async function showMenu(ctx: any) {
|
|
200
255
|
const choice = await ctx.ui.select("Memory", [
|
|
201
256
|
"Show status",
|
|
257
|
+
"Show recent memory",
|
|
202
258
|
"Turn memory on",
|
|
203
259
|
"Turn memory off",
|
|
204
260
|
]);
|
|
205
261
|
if (choice === "Show status") await showStatus(ctx);
|
|
262
|
+
if (choice === "Show recent memory") await showRecentMemory(ctx);
|
|
206
263
|
if (choice === "Turn memory on") await setEnabled(ctx, true);
|
|
207
264
|
if (choice === "Turn memory off") await setEnabled(ctx, false);
|
|
208
265
|
}
|
|
209
266
|
|
|
210
|
-
function
|
|
267
|
+
async function showRecentMemory(ctx: any, args = "") {
|
|
268
|
+
try {
|
|
269
|
+
const programSlug = detectProgramSlugFromText(args);
|
|
270
|
+
const recent = await fetchMemoryRecentFromApi(
|
|
271
|
+
api,
|
|
272
|
+
{
|
|
273
|
+
sessionId: getSessionId(ctx),
|
|
274
|
+
cwd: ctx.cwd,
|
|
275
|
+
programSlug,
|
|
276
|
+
limit: 12,
|
|
277
|
+
},
|
|
278
|
+
MEMORY_CONTEXT_TIMEOUT_MS,
|
|
279
|
+
);
|
|
280
|
+
ctx.ui.notify(formatRecentMemoryLines(recent).join("\n"), "info");
|
|
281
|
+
} catch (error) {
|
|
282
|
+
state.lastError = errorMessage(error);
|
|
283
|
+
updateMemoryStatus(ctx, state);
|
|
284
|
+
ctx.ui.notify(`Unable to load recent memory: ${state.lastError}`, "error");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function refreshSessionMemorySnapshot(ctx: any) {
|
|
211
289
|
if (!state.status?.available || !state.status.enabled) {
|
|
212
290
|
state.cachedMemoryBlock = null;
|
|
213
291
|
return Promise.resolve();
|
|
214
292
|
}
|
|
215
|
-
const
|
|
216
|
-
const programSlug = getPrimaryProgramSlug(state.programSlugs) ?? detectProgramSlugFromText(prompt);
|
|
217
|
-
const run = fetchMemoryContextFromApi(
|
|
293
|
+
const run = fetchMemoryRecentFromApi(
|
|
218
294
|
api,
|
|
219
295
|
{
|
|
220
296
|
sessionId: getSessionId(ctx),
|
|
221
297
|
cwd: ctx.cwd,
|
|
222
|
-
|
|
223
|
-
programSlug,
|
|
224
|
-
maxTokens: MEMORY_CONTEXT_MAX_TOKENS,
|
|
298
|
+
limit: 5,
|
|
225
299
|
},
|
|
226
300
|
MEMORY_CONTEXT_TIMEOUT_MS,
|
|
227
301
|
)
|
|
228
|
-
.then((
|
|
229
|
-
state.cachedMemoryBlock =
|
|
302
|
+
.then((recent: any) => {
|
|
303
|
+
state.cachedMemoryBlock = formatSessionMemoryPromptBlock(recent);
|
|
230
304
|
state.lastError = null;
|
|
231
305
|
updateMemoryStatus(ctx, state);
|
|
232
306
|
})
|
|
@@ -243,13 +317,20 @@ export function registerMemory(pi: ExtensionAPI): MemoryController {
|
|
|
243
317
|
}
|
|
244
318
|
|
|
245
319
|
pi.registerCommand("memory", {
|
|
246
|
-
description: "Show
|
|
320
|
+
description: "Show Seed Club memory status, recent memory, or turn memory on/off (`status`, `recent`, `on`, `off`)",
|
|
247
321
|
handler: async (args, ctx) => {
|
|
248
|
-
const
|
|
322
|
+
const trimmedArgs = args.trim();
|
|
323
|
+
const [rawAction = "menu", ...rest] = trimmedArgs.split(/\s+/);
|
|
324
|
+
const action = rawAction.toLowerCase();
|
|
325
|
+
const actionArgs = rest.join(" ");
|
|
249
326
|
if (action === "status") {
|
|
250
327
|
await showStatus(ctx);
|
|
251
328
|
return;
|
|
252
329
|
}
|
|
330
|
+
if (action === "recent" || action === "resume") {
|
|
331
|
+
await showRecentMemory(ctx, actionArgs);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
253
334
|
if (action === "on") {
|
|
254
335
|
await setEnabled(ctx, true);
|
|
255
336
|
return;
|
|
@@ -262,7 +343,7 @@ export function registerMemory(pi: ExtensionAPI): MemoryController {
|
|
|
262
343
|
await showMenu(ctx);
|
|
263
344
|
return;
|
|
264
345
|
}
|
|
265
|
-
ctx.ui.notify("Usage: /memory status | /memory on | /memory off", "error");
|
|
346
|
+
ctx.ui.notify("Usage: /memory status | /memory recent | /memory on | /memory off", "error");
|
|
266
347
|
},
|
|
267
348
|
});
|
|
268
349
|
|
|
@@ -271,13 +352,30 @@ export function registerMemory(pi: ExtensionAPI): MemoryController {
|
|
|
271
352
|
void refreshStatus(ctx);
|
|
272
353
|
});
|
|
273
354
|
|
|
274
|
-
pi.on("before_agent_start", (event) => {
|
|
355
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
275
356
|
state.activePrompt = typeof event.prompt === "string" ? event.prompt : "";
|
|
276
357
|
state.toolNames = [];
|
|
277
358
|
state.programSlugs = uniqueProgramSlugs([detectProgramSlugFromText(state.activePrompt)]);
|
|
278
359
|
state.turnCounter += 1;
|
|
279
360
|
state.lastTurnId = `${Date.now()}-${state.turnCounter}`;
|
|
280
361
|
|
|
362
|
+
if (!state.status && !state.lastError) {
|
|
363
|
+
await refreshStatus(ctx);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (shouldForceRecentMemory(state.activePrompt) && state.status?.available && state.status.enabled) {
|
|
367
|
+
const recentBlock = await fetchRecentMemoryPromptBlock(ctx, state.activePrompt);
|
|
368
|
+
if (recentBlock) {
|
|
369
|
+
return {
|
|
370
|
+
systemPrompt: `${event.systemPrompt}\n\n${recentBlock}`,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (state.status?.available && state.status.enabled && !state.cachedMemoryBlock) {
|
|
376
|
+
await refreshSessionMemorySnapshot(ctx);
|
|
377
|
+
}
|
|
378
|
+
|
|
281
379
|
if (!state.status?.available || !state.status.enabled || !state.cachedMemoryBlock) return;
|
|
282
380
|
return {
|
|
283
381
|
systemPrompt: `${event.systemPrompt}\n\n${state.cachedMemoryBlock}`,
|
|
@@ -314,7 +412,7 @@ export function registerMemory(pi: ExtensionAPI): MemoryController {
|
|
|
314
412
|
},
|
|
315
413
|
MEMORY_WRITE_TIMEOUT_MS,
|
|
316
414
|
).then(() => {
|
|
317
|
-
void
|
|
415
|
+
void refreshSessionMemorySnapshot(ctx);
|
|
318
416
|
}).catch((error: unknown) => {
|
|
319
417
|
state.lastError = errorMessage(error);
|
|
320
418
|
updateMemoryStatus(ctx, state);
|
|
@@ -323,6 +421,7 @@ export function registerMemory(pi: ExtensionAPI): MemoryController {
|
|
|
323
421
|
|
|
324
422
|
return {
|
|
325
423
|
refreshStatus,
|
|
424
|
+
enableByInstallDefault,
|
|
326
425
|
clear,
|
|
327
426
|
showMenu,
|
|
328
427
|
};
|
|
@@ -362,3 +461,155 @@ function extractAssistantTextFromAgentEnd(messages: any[]) {
|
|
|
362
461
|
}
|
|
363
462
|
return "";
|
|
364
463
|
}
|
|
464
|
+
|
|
465
|
+
function shouldForceRecentMemory(prompt: string) {
|
|
466
|
+
return /\b(last|previous|prior|recent|yesterday|earlier)\b.*\b(chat|conversation|talk|discuss|message|session)\b/i.test(prompt) ||
|
|
467
|
+
/\bwhat did (i|we) (last|previously|recently) (chat|talk|discuss)\b/i.test(prompt) ||
|
|
468
|
+
/\bresume\b.*\b(chat|conversation|session)\b/i.test(prompt);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function fetchRecentMemoryPromptBlock(ctx: any, prompt: string) {
|
|
472
|
+
const programSlug = detectProgramSlugFromText(prompt);
|
|
473
|
+
const recent = await fetchMemoryRecentFromApi(
|
|
474
|
+
api,
|
|
475
|
+
{
|
|
476
|
+
sessionId: getSessionId(ctx),
|
|
477
|
+
cwd: ctx.cwd,
|
|
478
|
+
programSlug,
|
|
479
|
+
limit: 12,
|
|
480
|
+
},
|
|
481
|
+
MEMORY_CONTEXT_TIMEOUT_MS,
|
|
482
|
+
);
|
|
483
|
+
return formatRecentMemoryPromptBlock(recent);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function formatRecentMemoryPromptBlock(recent: any) {
|
|
487
|
+
if (!recent?.available || !recent.enabled) return null;
|
|
488
|
+
const lines = [
|
|
489
|
+
"SEED CLUB MEMORY:",
|
|
490
|
+
"The user is asking about prior conversation history. Use the memory below as available context. Do not claim you lack access to prior conversation history when relevant memory is present.",
|
|
491
|
+
`Session: ${recent.displaySessionId || recent.sessionId || "unknown"}`,
|
|
492
|
+
`User peer: ${recent.userPeerId || "unknown"}`,
|
|
493
|
+
];
|
|
494
|
+
|
|
495
|
+
const peerCard = Array.isArray(recent.peerCard) ? recent.peerCard.filter(Boolean) : [];
|
|
496
|
+
if (peerCard.length) {
|
|
497
|
+
lines.push("", "Peer card:");
|
|
498
|
+
for (const item of peerCard.slice(0, 8)) lines.push(`- ${truncateLine(item, 320)}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const summary = recent.contextSummary?.content || recent.summaries?.short?.content || recent.summaries?.long?.content;
|
|
502
|
+
if (summary) {
|
|
503
|
+
lines.push("", "Summary:");
|
|
504
|
+
lines.push(`- ${truncateLine(summary, 1200)}`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const representation = typeof recent.peerRepresentation === "string" ? recent.peerRepresentation.trim() : "";
|
|
508
|
+
if (representation) {
|
|
509
|
+
lines.push("", "Peer representation:");
|
|
510
|
+
lines.push(`- ${truncateLine(representation, 1200)}`);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const messages = Array.isArray(recent.messages) ? recent.messages : [];
|
|
514
|
+
if (messages.length) {
|
|
515
|
+
lines.push("", "Recent messages:");
|
|
516
|
+
for (const message of messages.slice(0, 10)) {
|
|
517
|
+
const role = typeof message.role === "string" && message.role ? message.role : message.peerId || "memory";
|
|
518
|
+
lines.push(`- ${role}: ${truncateLine(message.content, 500)}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return lines.length > 4 ? lines.join("\n") : null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function formatSessionMemoryPromptBlock(recent: any) {
|
|
526
|
+
if (!recent?.available || !recent.enabled) return null;
|
|
527
|
+
const lines = [
|
|
528
|
+
"SEED CLUB MEMORY:",
|
|
529
|
+
"Use this compact memory snapshot for continuity across Seed Club agent sessions. Treat it as background context; do not recite it unless it helps the user.",
|
|
530
|
+
`Session: ${recent.displaySessionId || recent.sessionId || "unknown"}`,
|
|
531
|
+
`User peer: ${recent.userPeerId || "unknown"}`,
|
|
532
|
+
];
|
|
533
|
+
|
|
534
|
+
const peerCard = Array.isArray(recent.peerCard) ? recent.peerCard.filter(Boolean) : [];
|
|
535
|
+
if (peerCard.length) {
|
|
536
|
+
lines.push("", "User profile:");
|
|
537
|
+
for (const item of peerCard.slice(0, 6)) lines.push(`- ${truncateLine(item, 220)}`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const representation = typeof recent.peerRepresentation === "string" ? recent.peerRepresentation.trim() : "";
|
|
541
|
+
if (representation) {
|
|
542
|
+
lines.push("", "User/work style:");
|
|
543
|
+
lines.push(`- ${truncateLine(representation, 500)}`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const summary = recent.contextSummary?.content || recent.summaries?.short?.content || recent.summaries?.long?.content;
|
|
547
|
+
if (summary) {
|
|
548
|
+
lines.push("", "Recent conversation summary:");
|
|
549
|
+
lines.push(`- ${truncateLine(summary, 600)}`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const messages = Array.isArray(recent.messages) ? recent.messages : [];
|
|
553
|
+
if (messages.length) {
|
|
554
|
+
lines.push("", "Recent conversation hints:");
|
|
555
|
+
for (const message of messages.slice(0, 3)) {
|
|
556
|
+
const role = typeof message.role === "string" && message.role ? message.role : message.peerId || "memory";
|
|
557
|
+
lines.push(`- ${role}: ${truncateLine(message.content, 180)}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return lines.length > 4 ? lines.join("\n") : null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function formatRecentMemoryLines(recent: any) {
|
|
565
|
+
if (!recent?.available) {
|
|
566
|
+
return [`memory unavailable: ${recent?.reason || "Honcho is not configured."}`];
|
|
567
|
+
}
|
|
568
|
+
if (!recent.enabled) {
|
|
569
|
+
return ["memory is off"];
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const lines = [
|
|
573
|
+
"recent memory",
|
|
574
|
+
`session: ${recent.displaySessionId || recent.sessionId || "unknown"}`,
|
|
575
|
+
`peer: ${recent.userPeerId || "unknown"}`,
|
|
576
|
+
];
|
|
577
|
+
if (recent.programSlug) lines.push(`program: ${recent.programSlug}`);
|
|
578
|
+
|
|
579
|
+
const peerCard = Array.isArray(recent.peerCard) ? recent.peerCard.filter(Boolean) : [];
|
|
580
|
+
if (peerCard.length) {
|
|
581
|
+
lines.push("", "peer card:");
|
|
582
|
+
for (const item of peerCard.slice(0, 6)) lines.push(`- ${truncateLine(item)}`);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const summary = recent.contextSummary?.content || recent.summaries?.short?.content || recent.summaries?.long?.content;
|
|
586
|
+
if (summary) {
|
|
587
|
+
lines.push("", "summary:");
|
|
588
|
+
lines.push(`- ${truncateLine(summary, 700)}`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const representation = typeof recent.peerRepresentation === "string" ? recent.peerRepresentation.trim() : "";
|
|
592
|
+
if (representation) {
|
|
593
|
+
lines.push("", "peer representation:");
|
|
594
|
+
lines.push(`- ${truncateLine(representation, 700)}`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const messages = Array.isArray(recent.messages) ? recent.messages : [];
|
|
598
|
+
if (messages.length) {
|
|
599
|
+
lines.push("", "recent messages:");
|
|
600
|
+
for (const message of messages.slice(0, 8)) {
|
|
601
|
+
const role = typeof message.role === "string" && message.role ? message.role : message.peerId || "memory";
|
|
602
|
+
lines.push(`- ${role}: ${truncateLine(message.content, 260)}`);
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
lines.push("", "recent messages: none yet");
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return lines;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function truncateLine(value: unknown, maxChars = 280) {
|
|
612
|
+
const text = String(value ?? "").replace(/\s+/g, " ").trim();
|
|
613
|
+
if (text.length <= maxChars) return text;
|
|
614
|
+
return `${text.slice(0, maxChars).trim()}...`;
|
|
615
|
+
}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
4
|
+
import { basename } from "node:path";
|
|
5
|
+
import { ApiError, api, NotConnectedError } from "../api-client.js";
|
|
6
|
+
import { makeProgressCallRenderer, makeProgressResultRenderer } from "../tool-utils.js";
|
|
7
|
+
import { getToolCallLabel, getToolSuccessLabel } from "../ui-copy.js";
|
|
8
|
+
|
|
9
|
+
type DealSourcingDecision = "pushing" | "passing" | "holding";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_DEAL_SOURCING_PROGRAM_SLUG = "seed-network";
|
|
12
|
+
const MAX_DOCUMENT_PREVIEW_CHARS = 12000;
|
|
13
|
+
const MAX_IMAGE_PREVIEW_BYTES = 8 * 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
type DocumentPreview = {
|
|
16
|
+
error?: string;
|
|
17
|
+
extraction_status: "completed" | "failed" | "unsupported";
|
|
18
|
+
file_name: string;
|
|
19
|
+
image?: { data: string; mimeType: string };
|
|
20
|
+
image_attached?: boolean;
|
|
21
|
+
kind: "image" | "pdf" | "text" | "unsupported";
|
|
22
|
+
mime_type: string;
|
|
23
|
+
text?: string | null;
|
|
24
|
+
truncated?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function normalizeOptionalString(value: unknown): string | null | undefined {
|
|
28
|
+
if (value === undefined) return undefined;
|
|
29
|
+
if (value == null) return null;
|
|
30
|
+
if (typeof value !== "string") return undefined;
|
|
31
|
+
const trimmed = value.trim();
|
|
32
|
+
return trimmed ? trimmed : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeOptionalNumber(value: unknown): number | null | undefined {
|
|
36
|
+
if (value === undefined) return undefined;
|
|
37
|
+
if (value == null) return null;
|
|
38
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeStringArray(value: unknown): string[] | undefined {
|
|
43
|
+
if (value === undefined) return undefined;
|
|
44
|
+
if (!Array.isArray(value)) return [];
|
|
45
|
+
return value
|
|
46
|
+
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function clipPreviewText(value: string) {
|
|
51
|
+
const normalized = value.replace(/\u0000/g, "").trim();
|
|
52
|
+
if (normalized.length <= MAX_DOCUMENT_PREVIEW_CHARS) {
|
|
53
|
+
return { text: normalized, truncated: false };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
text: normalized.slice(0, MAX_DOCUMENT_PREVIEW_CHARS),
|
|
58
|
+
truncated: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function extractPdfText(bytes: Buffer) {
|
|
63
|
+
const { PDFParse } = await import("pdf-parse");
|
|
64
|
+
const parser = new PDFParse({ data: bytes });
|
|
65
|
+
try {
|
|
66
|
+
const result = await parser.getText();
|
|
67
|
+
return typeof result?.text === "string" ? result.text : "";
|
|
68
|
+
} finally {
|
|
69
|
+
await parser.destroy().catch(() => undefined);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function buildDocumentPreview(fileName: string, contentType: string, bytes: Buffer): Promise<DocumentPreview> {
|
|
74
|
+
const mimeType = contentType.split(";")[0]?.trim().toLowerCase() || "application/octet-stream";
|
|
75
|
+
|
|
76
|
+
if (mimeType.startsWith("image/")) {
|
|
77
|
+
if (bytes.byteLength > MAX_IMAGE_PREVIEW_BYTES) {
|
|
78
|
+
return {
|
|
79
|
+
error: "Image is too large to attach to the model preview.",
|
|
80
|
+
extraction_status: "failed",
|
|
81
|
+
file_name: fileName,
|
|
82
|
+
image_attached: false,
|
|
83
|
+
kind: "image",
|
|
84
|
+
mime_type: mimeType,
|
|
85
|
+
text: null,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
extraction_status: "completed",
|
|
91
|
+
file_name: fileName,
|
|
92
|
+
image: {
|
|
93
|
+
data: bytes.toString("base64"),
|
|
94
|
+
mimeType,
|
|
95
|
+
},
|
|
96
|
+
image_attached: true,
|
|
97
|
+
kind: "image",
|
|
98
|
+
mime_type: mimeType,
|
|
99
|
+
text: "Image attached for visual review. Summarize visible company, founder, deal, traction, and open-question context from the image.",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (mimeType === "application/pdf") {
|
|
104
|
+
try {
|
|
105
|
+
const { text, truncated } = clipPreviewText(await extractPdfText(bytes));
|
|
106
|
+
return {
|
|
107
|
+
extraction_status: text ? "completed" : "failed",
|
|
108
|
+
error: text ? undefined : "No text could be extracted from the PDF. It may be image-heavy.",
|
|
109
|
+
file_name: fileName,
|
|
110
|
+
kind: "pdf",
|
|
111
|
+
mime_type: mimeType,
|
|
112
|
+
text,
|
|
113
|
+
truncated,
|
|
114
|
+
};
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return {
|
|
117
|
+
error: error instanceof Error ? error.message : String(error),
|
|
118
|
+
extraction_status: "failed",
|
|
119
|
+
file_name: fileName,
|
|
120
|
+
kind: "pdf",
|
|
121
|
+
mime_type: mimeType,
|
|
122
|
+
text: null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (mimeType === "text/plain" || mimeType.startsWith("text/")) {
|
|
128
|
+
const { text, truncated } = clipPreviewText(bytes.toString("utf8"));
|
|
129
|
+
return {
|
|
130
|
+
extraction_status: "completed",
|
|
131
|
+
file_name: fileName,
|
|
132
|
+
kind: "text",
|
|
133
|
+
mime_type: mimeType,
|
|
134
|
+
text,
|
|
135
|
+
truncated,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
error: "Local preview is not supported for this document type yet.",
|
|
141
|
+
extraction_status: "unsupported",
|
|
142
|
+
file_name: fileName,
|
|
143
|
+
kind: "unsupported",
|
|
144
|
+
mime_type: mimeType,
|
|
145
|
+
text: null,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function suggestedReviewFields() {
|
|
150
|
+
return [
|
|
151
|
+
"Founder info",
|
|
152
|
+
"Company info",
|
|
153
|
+
"Deal info, including ask or explicit ask unknown",
|
|
154
|
+
"Opportunity Summary",
|
|
155
|
+
"Founder/contact path or explicit unknown",
|
|
156
|
+
"Member relationship and excitement/vouch before push",
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function redactPreviewForJson(preview: DocumentPreview | undefined) {
|
|
161
|
+
if (!preview) return preview;
|
|
162
|
+
const { image, ...rest } = preview;
|
|
163
|
+
return {
|
|
164
|
+
...rest,
|
|
165
|
+
image_attached: preview.image_attached ?? Boolean(image),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function inferDocumentContentType(fileName: string) {
|
|
170
|
+
const lower = fileName.toLowerCase();
|
|
171
|
+
if (lower.endsWith(".pdf")) return "application/pdf";
|
|
172
|
+
if (lower.endsWith(".png")) return "image/png";
|
|
173
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
174
|
+
if (lower.endsWith(".webp")) return "image/webp";
|
|
175
|
+
if (lower.endsWith(".gif")) return "image/gif";
|
|
176
|
+
if (lower.endsWith(".ppt")) return "application/vnd.ms-powerpoint";
|
|
177
|
+
if (lower.endsWith(".pptx")) return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
178
|
+
if (lower.endsWith(".doc")) return "application/msword";
|
|
179
|
+
if (lower.endsWith(".docx")) return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
180
|
+
if (lower.endsWith(".txt")) return "text/plain";
|
|
181
|
+
return "application/octet-stream";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizeDecision(value: unknown): DealSourcingDecision | null {
|
|
185
|
+
if (value === "pushing" || value === "passing" || value === "holding") return value;
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function submitDraftPosition(programSlug: string, draftId: string, args: any, decision: DealSourcingDecision) {
|
|
190
|
+
return api.post<any>(`/programs/${programSlug}/deal-sourcing/drafts/${draftId}/member-position`, {
|
|
191
|
+
ask_unknown: args.askUnknown === true,
|
|
192
|
+
contact_info: normalizeOptionalString(args.contactInfo),
|
|
193
|
+
contact_unknown: args.contactUnknown === true,
|
|
194
|
+
decision,
|
|
195
|
+
excitement_reason: normalizeOptionalString(args.excitementReason),
|
|
196
|
+
flagged_topics: normalizeStringArray(args.flaggedTopics),
|
|
197
|
+
founder_names: normalizeStringArray(args.founderNames),
|
|
198
|
+
founder_relationship: normalizeOptionalString(args.founderRelationship),
|
|
199
|
+
founder_unknown: args.founderUnknown === true,
|
|
200
|
+
target_amount: normalizeOptionalNumber(args.targetAmount),
|
|
201
|
+
vouch_text: normalizeOptionalString(args.vouchText),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function submitDealSourcingDeck(args: {
|
|
206
|
+
askUnknown?: boolean | null;
|
|
207
|
+
programSlug?: string | null;
|
|
208
|
+
filePath?: string | null;
|
|
209
|
+
assetKind?: "deck" | "memo" | "supporting_material" | null;
|
|
210
|
+
companyName?: string | null;
|
|
211
|
+
companyWebsiteUrl?: string | null;
|
|
212
|
+
contentType?: string | null;
|
|
213
|
+
contactInfo?: string | null;
|
|
214
|
+
contactUnknown?: boolean | null;
|
|
215
|
+
decision?: DealSourcingDecision | null;
|
|
216
|
+
draftId?: string | null;
|
|
217
|
+
excitementReason?: string | null;
|
|
218
|
+
fileName?: string | null;
|
|
219
|
+
flaggedTopics?: string[] | null;
|
|
220
|
+
founderNames?: string[] | null;
|
|
221
|
+
founderRelationship?: string | null;
|
|
222
|
+
founderUnknown?: boolean | null;
|
|
223
|
+
roundType?: string | null;
|
|
224
|
+
sourceUrl?: string | null;
|
|
225
|
+
targetAmount?: number | null;
|
|
226
|
+
valuation?: number | null;
|
|
227
|
+
vouchText?: string | null;
|
|
228
|
+
}, runtime?: any) {
|
|
229
|
+
try {
|
|
230
|
+
const programSlug = normalizeOptionalString(args.programSlug) ?? DEFAULT_DEAL_SOURCING_PROGRAM_SLUG;
|
|
231
|
+
const existingDraftId = normalizeOptionalString(args.draftId);
|
|
232
|
+
const existingDecision = normalizeDecision(args.decision);
|
|
233
|
+
if (existingDraftId) {
|
|
234
|
+
if (!existingDecision) {
|
|
235
|
+
return { draft: { id: existingDraftId }, next_action: "Ask the member whether they want to push/source this draft, pass/reject it, or hold it for later." };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
runtime?.setWorkingMessage?.("Saving deal sourcing draft position...");
|
|
239
|
+
const positionResponse = await submitDraftPosition(programSlug, existingDraftId, args, existingDecision);
|
|
240
|
+
return { draft: { id: existingDraftId }, draft_position_result: positionResponse };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const filePath = args.filePath;
|
|
244
|
+
if (!filePath) {
|
|
245
|
+
return { error: "filePath is required when draftId is not provided." };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const fileStats = await stat(filePath);
|
|
249
|
+
if (!fileStats.isFile()) {
|
|
250
|
+
return { error: "filePath must point to a file." };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const fileName = normalizeOptionalString(args.fileName) ?? basename(filePath);
|
|
254
|
+
const contentType = normalizeOptionalString(args.contentType) ?? inferDocumentContentType(fileName);
|
|
255
|
+
const assetKind = normalizeOptionalString(args.assetKind) ?? "deck";
|
|
256
|
+
runtime?.setWorkingMessage?.("Requesting deal document upload target...");
|
|
257
|
+
const ingestResponse = await api.post<any>(`/programs/${programSlug}/deal-sourcing/document-ingests`, {
|
|
258
|
+
asset_kind: assetKind,
|
|
259
|
+
content_type: contentType,
|
|
260
|
+
file_name: fileName,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const uploadTarget = ingestResponse?.document_upload;
|
|
264
|
+
const uploadUrl = typeof uploadTarget?.upload_url === "string" ? uploadTarget.upload_url : null;
|
|
265
|
+
const objectKey = typeof uploadTarget?.object_key === "string" ? uploadTarget.object_key : null;
|
|
266
|
+
const storageUrl = typeof uploadTarget?.storage_url === "string" ? uploadTarget.storage_url : null;
|
|
267
|
+
const uploadId = typeof ingestResponse?.upload?.id === "string" ? ingestResponse.upload.id : null;
|
|
268
|
+
|
|
269
|
+
if (!uploadUrl || !objectKey || !uploadId) {
|
|
270
|
+
return { error: "The API did not return a valid deal document upload target.", response: ingestResponse };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
runtime?.setWorkingMessage?.("Uploading deal document bytes...");
|
|
274
|
+
const bytes = await readFile(filePath);
|
|
275
|
+
const documentPreview = await buildDocumentPreview(fileName, contentType, bytes);
|
|
276
|
+
const uploadHeaders: Record<string, string> = {};
|
|
277
|
+
if (uploadTarget?.headers && typeof uploadTarget.headers === "object") {
|
|
278
|
+
for (const [key, value] of Object.entries(uploadTarget.headers)) {
|
|
279
|
+
if (typeof value === "string") uploadHeaders[key.toLowerCase()] = value;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
uploadHeaders["content-type"] = uploadHeaders["content-type"] ?? contentType;
|
|
283
|
+
|
|
284
|
+
const uploadResponse = await fetch(uploadUrl, {
|
|
285
|
+
method: "PUT",
|
|
286
|
+
headers: uploadHeaders,
|
|
287
|
+
body: bytes,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (!uploadResponse.ok) {
|
|
291
|
+
const text = await uploadResponse.text().catch(() => "");
|
|
292
|
+
return {
|
|
293
|
+
error: `Spaces upload failed (${uploadResponse.status}).`,
|
|
294
|
+
details: text.slice(0, 500),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
runtime?.setWorkingMessage?.("Creating private research draft...");
|
|
299
|
+
const completeResponse = await api.post<any>(`/programs/${programSlug}/deal-sourcing/document-ingests/${uploadId}/complete`, {
|
|
300
|
+
asset_kind: assetKind,
|
|
301
|
+
company_name: normalizeOptionalString(args.companyName),
|
|
302
|
+
company_website_url: normalizeOptionalString(args.companyWebsiteUrl),
|
|
303
|
+
content_type: contentType,
|
|
304
|
+
file_name: fileName,
|
|
305
|
+
file_size: fileStats.size,
|
|
306
|
+
object_key: objectKey,
|
|
307
|
+
round_type: normalizeOptionalString(args.roundType),
|
|
308
|
+
source_url: normalizeOptionalString(args.sourceUrl),
|
|
309
|
+
storage_url: storageUrl,
|
|
310
|
+
target_amount: normalizeOptionalNumber(args.targetAmount),
|
|
311
|
+
valuation: normalizeOptionalNumber(args.valuation),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const decision = normalizeDecision(args.decision);
|
|
315
|
+
return {
|
|
316
|
+
...completeResponse,
|
|
317
|
+
decision_ignored_until_review: Boolean(decision),
|
|
318
|
+
document_preview: documentPreview,
|
|
319
|
+
next_action: decision
|
|
320
|
+
? "The document is saved as a private draft. Review the material, summarize Founder info, Company info, Deal info, and Opportunity Summary, then ask missing questions before submitting the member decision."
|
|
321
|
+
: "Review the material, summarize Founder info, Company info, Deal info, and Opportunity Summary, then ask whether the member wants to push, pass, or hold after missing context is resolved.",
|
|
322
|
+
suggested_review_fields: suggestedReviewFields(),
|
|
323
|
+
upload: {
|
|
324
|
+
...completeResponse?.upload,
|
|
325
|
+
storage_url: storageUrl ?? completeResponse?.upload?.storage_url ?? null,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
} catch (error) {
|
|
329
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function makeDealSourcingExecute(fn: typeof submitDealSourcingDeck) {
|
|
335
|
+
return async (_toolCallId: string, params: any, _signal: any, _onUpdate: any, ctx: any) => {
|
|
336
|
+
const runtime = {
|
|
337
|
+
ctx,
|
|
338
|
+
setWorkingMessage: (message: string) => {
|
|
339
|
+
try {
|
|
340
|
+
ctx?.ui?.setWorkingMessage?.(message || undefined);
|
|
341
|
+
} catch {
|
|
342
|
+
// Ignore UI status failures so tool execution still succeeds.
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
clearWorkingMessage: () => {
|
|
346
|
+
try {
|
|
347
|
+
ctx?.ui?.setWorkingMessage?.(undefined);
|
|
348
|
+
} catch {
|
|
349
|
+
// Ignore UI status failures so tool execution still succeeds.
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const result = await fn(params, runtime);
|
|
356
|
+
const image = result?.document_preview?.image;
|
|
357
|
+
const details = {
|
|
358
|
+
...result,
|
|
359
|
+
document_preview: redactPreviewForJson(result?.document_preview),
|
|
360
|
+
};
|
|
361
|
+
const content: any[] = [{ type: "text", text: JSON.stringify(details, null, 2) }];
|
|
362
|
+
if (image?.data && image?.mimeType) {
|
|
363
|
+
content.push({ type: "image", data: image.data, mimeType: image.mimeType });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return { content, details };
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (error instanceof NotConnectedError) {
|
|
369
|
+
return {
|
|
370
|
+
content: [{ type: "text" as const, text: "Not connected to Seed Club API. Run /connect or /seedclub to authenticate." }],
|
|
371
|
+
details: { notConnected: true },
|
|
372
|
+
isError: true,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
377
|
+
return {
|
|
378
|
+
content: [{ type: "text" as const, text: `Error: ${message}` }],
|
|
379
|
+
details: { error: message },
|
|
380
|
+
isError: true,
|
|
381
|
+
};
|
|
382
|
+
} finally {
|
|
383
|
+
runtime.clearWorkingMessage();
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function registerDealSourcingTools(pi: ExtensionAPI) {
|
|
389
|
+
pi.registerTool({
|
|
390
|
+
name: "seedclub_submit_deal_sourcing_deck",
|
|
391
|
+
label: "Submit Deal Sourcing Document",
|
|
392
|
+
description:
|
|
393
|
+
"Upload a local deck, memo, document, or image to seedclub-api as a private/team-private deal research draft, or submit pushing/passing/holding for an existing draftId. programSlug defaults to seed-network. Initial uploads always create a private draft first and return document preview context; do not push immediately from an upload. After reviewing and confirming with the member, use draftId plus a decision. If decision is pushing, provide founderNames or founderUnknown, founderRelationship, excitementReason or vouchText, contactInfo or contactUnknown, and targetAmount or askUnknown.",
|
|
394
|
+
parameters: Type.Object({
|
|
395
|
+
askUnknown: Type.Optional(Type.Union([Type.Boolean(), Type.Null()], { description: "Set true when the member explicitly says the round ask is unknown." })),
|
|
396
|
+
programSlug: Type.Optional(Type.Union([Type.String({ description: "Program slug for the investing/deal pipeline program. Defaults to seed-network." }), Type.Null()])),
|
|
397
|
+
filePath: Type.Optional(Type.Union([Type.String({ description: "Local deck, memo, document, or image path. Required unless draftId is provided." }), Type.Null()])),
|
|
398
|
+
assetKind: Type.Optional(Type.Union([Type.Literal("deck"), Type.Literal("memo"), Type.Literal("supporting_material"), Type.Null()])),
|
|
399
|
+
companyName: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
400
|
+
companyWebsiteUrl: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
401
|
+
contentType: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
402
|
+
contactInfo: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Founder/company contact info supplied by the member if not present in the uploaded material." })),
|
|
403
|
+
contactUnknown: Type.Optional(Type.Union([Type.Boolean(), Type.Null()], { description: "Set true when the member explicitly says contact info is unknown." })),
|
|
404
|
+
decision: Type.Optional(Type.Union([Type.Literal("pushing"), Type.Literal("passing"), Type.Literal("holding"), Type.Null()])),
|
|
405
|
+
draftId: Type.Optional(Type.Union([Type.String({ description: "Existing private deal draft id to update without re-uploading the file." }), Type.Null()])),
|
|
406
|
+
excitementReason: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Why the member is excited about the company/founder." })),
|
|
407
|
+
fileName: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
408
|
+
flaggedTopics: Type.Optional(Type.Union([Type.Array(Type.String()), Type.Null()])),
|
|
409
|
+
founderNames: Type.Optional(Type.Union([Type.Array(Type.String()), Type.Null()], { description: "Founder names supplied or corrected by the member." })),
|
|
410
|
+
founderRelationship: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "How the member knows the founder." })),
|
|
411
|
+
founderUnknown: Type.Optional(Type.Union([Type.Boolean(), Type.Null()], { description: "Set true when the member explicitly says founder names are unknown." })),
|
|
412
|
+
roundType: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
413
|
+
sourceUrl: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
414
|
+
targetAmount: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
|
|
415
|
+
valuation: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
|
|
416
|
+
vouchText: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
417
|
+
}),
|
|
418
|
+
execute: makeDealSourcingExecute(submitDealSourcingDeck),
|
|
419
|
+
renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_submit_deal_sourcing_deck"), (args) => args?.companyName || args?.draftId || args?.filePath || undefined),
|
|
420
|
+
renderResult: makeProgressResultRenderer(getToolSuccessLabel("seedclub_submit_deal_sourcing_deck"), (details) => {
|
|
421
|
+
const company = details?.company?.name ?? details?.draft_position_result?.funding_round?.organization_id ?? null;
|
|
422
|
+
const state = details?.draft_position_result?.funding_round?.lifecycle_state ?? details?.draft?.status ?? details?.draft_position_result?.draft?.status ?? null;
|
|
423
|
+
const id = details?.draft_position_result?.funding_round?.id ?? details?.draft?.id ?? details?.draft_position_result?.draft?.id ?? null;
|
|
424
|
+
return [company, state, id].filter(Boolean).join(" · ") || undefined;
|
|
425
|
+
}),
|
|
426
|
+
});
|
|
427
|
+
}
|
|
@@ -9,6 +9,9 @@ export const TOOL_CALL_LABELS: Record<string, string> = {
|
|
|
9
9
|
seedclub_get_crm_record: "Loading CRM record",
|
|
10
10
|
seedclub_create_crm_note: "Saving CRM note",
|
|
11
11
|
seedclub_create_crm_task: "Creating CRM task",
|
|
12
|
+
seedclub_save_research: "Saving research",
|
|
13
|
+
seedclub_research_opportunity: "Researching opportunity",
|
|
14
|
+
seedclub_get_research: "Loading research",
|
|
12
15
|
seedclub_list_program_contacts: "Loading program contacts",
|
|
13
16
|
seedclub_list_program_funnels: "Loading program funnels",
|
|
14
17
|
seedclub_create_funnel_stage: "Creating funnel stage",
|
|
@@ -57,6 +60,9 @@ export const TOOL_SUCCESS_LABELS: Record<string, string> = {
|
|
|
57
60
|
seedclub_get_crm_record: "CRM record loaded",
|
|
58
61
|
seedclub_create_crm_note: "CRM note saved",
|
|
59
62
|
seedclub_create_crm_task: "CRM task created",
|
|
63
|
+
seedclub_save_research: "Research saved",
|
|
64
|
+
seedclub_research_opportunity: "Research queued",
|
|
65
|
+
seedclub_get_research: "Research loaded",
|
|
60
66
|
seedclub_list_program_contacts: "Program contacts loaded",
|
|
61
67
|
seedclub_list_program_funnels: "Program funnels loaded",
|
|
62
68
|
seedclub_create_funnel_stage: "Funnel stage created",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubnet/seedclub",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.39",
|
|
4
4
|
"description": "A branded command-line agent wrapper around pi, with integrated Seed Club commands, tools, and app actions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -23,11 +23,12 @@
|
|
|
23
23
|
"upgrade:pi": "node scripts/upgrade-pi.mjs"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"@mariozechner/pi-coding-agent": "0.65.2",
|
|
26
27
|
"@sinclair/typebox": "^0.34.49",
|
|
27
|
-
"
|
|
28
|
+
"pdf-parse": "^2.4.5"
|
|
28
29
|
},
|
|
29
30
|
"engines": {
|
|
30
|
-
"node": ">=22.
|
|
31
|
+
"node": ">=22.3.0"
|
|
31
32
|
},
|
|
32
33
|
"files": [
|
|
33
34
|
"bin/",
|
|
@@ -961,14 +961,28 @@ function shouldShowCommandInList(name) {
|
|
|
961
961
|
|
|
962
962
|
function getBuiltInSlashCommands() {
|
|
963
963
|
return [
|
|
964
|
-
{ name: "
|
|
964
|
+
{ name: "new", description: "Start a new session" },
|
|
965
965
|
{ name: "compact", description: "Compact the current session context" },
|
|
966
|
-
{ name: "
|
|
966
|
+
{ name: "clear", description: "Clear the visible chat transcript" },
|
|
967
|
+
{ name: "calendar", description: "Open calendar connect and availability actions" },
|
|
968
|
+
{ name: "research", description: "Open a Seed Club research prompt" },
|
|
969
|
+
{ name: "source", description: "Open a source-backed research prompt" },
|
|
970
|
+
{ name: "memory", description: "Show Seed Club memory status, recent memory, or turn memory on/off" },
|
|
971
|
+
{ name: "worldview", description: "Open an investing worldview prompt" },
|
|
972
|
+
{ name: "theses", description: "Open an investment theses prompt" },
|
|
973
|
+
{ name: "concerns", description: "Open an investment concerns prompt" },
|
|
974
|
+
{ name: "model", description: "Select the current model" },
|
|
967
975
|
{ name: "login", description: "Log into a model provider" },
|
|
968
976
|
{ name: "logout", description: "Log out of a model provider" },
|
|
969
|
-
{ name: "
|
|
970
|
-
{ name: "new", description: "Start a new session" },
|
|
977
|
+
{ name: "more", description: "Show everything else" },
|
|
971
978
|
{ name: "quit", description: "Exit seedclub" },
|
|
979
|
+
];
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function getAuxiliaryBuiltInSlashCommands() {
|
|
983
|
+
return [
|
|
984
|
+
{ name: "commands", description: "List the primary command menu" },
|
|
985
|
+
{ name: "extensions", description: "List loaded extensions" },
|
|
972
986
|
{ name: "reload", description: "Reload keybindings, extensions, prompts, and tools" },
|
|
973
987
|
{ name: "update", description: "Show the seedclub package update menu" },
|
|
974
988
|
];
|
|
@@ -1298,14 +1312,17 @@ export class SeedclubInteractiveModeApp {
|
|
|
1298
1312
|
configureAutocomplete() {
|
|
1299
1313
|
const builtInCommands = getBuiltInSlashCommands();
|
|
1300
1314
|
const builtInCommandNames = new Set(builtInCommands.map((command) => command.name));
|
|
1315
|
+
const extraAutocompleteCommands = new Set(["seedclub"]);
|
|
1301
1316
|
const extensionCommands = this.session.extensionRunner?.getRegisteredCommands?.()
|
|
1302
1317
|
.filter((command) => !builtInCommandNames.has(command.invocationName))
|
|
1318
|
+
.filter((command) => extraAutocompleteCommands.has(command.invocationName))
|
|
1303
1319
|
.map((command) => ({
|
|
1304
1320
|
name: command.invocationName,
|
|
1305
1321
|
description: command.description || "",
|
|
1306
1322
|
getArgumentCompletions: command.getArgumentCompletions,
|
|
1307
1323
|
})) ?? [];
|
|
1308
1324
|
const promptCommands = (this.session.promptTemplates ?? [])
|
|
1325
|
+
.filter((template) => extraAutocompleteCommands.has(template.name))
|
|
1309
1326
|
.filter((template) => !builtInCommandNames.has(template.name))
|
|
1310
1327
|
.map((template) => ({
|
|
1311
1328
|
name: template.name,
|
|
@@ -1323,6 +1340,16 @@ export class SeedclubInteractiveModeApp {
|
|
|
1323
1340
|
|
|
1324
1341
|
showCommandsList() {
|
|
1325
1342
|
const builtInCommands = getBuiltInSlashCommands();
|
|
1343
|
+
const lines = builtInCommands
|
|
1344
|
+
.filter((command) => shouldShowCommandInList(command.name))
|
|
1345
|
+
.map((command) => `- \`/${command.name}\`${command.description ? ` ${command.description}` : ""}`)
|
|
1346
|
+
.join("\n");
|
|
1347
|
+
this.appendAssistantMarkdown(`## Commands\n\n${lines}`);
|
|
1348
|
+
this.setStatus("Listed primary commands.", "accent");
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
showMoreCommandsList() {
|
|
1352
|
+
const builtInCommands = [...getBuiltInSlashCommands(), ...getAuxiliaryBuiltInSlashCommands()];
|
|
1326
1353
|
const extensionCommands = this.session.extensionRunner?.getRegisteredCommands?.()
|
|
1327
1354
|
.map((command) => ({
|
|
1328
1355
|
name: command.invocationName,
|
|
@@ -1333,13 +1360,17 @@ export class SeedclubInteractiveModeApp {
|
|
|
1333
1360
|
name: template.name,
|
|
1334
1361
|
description: template.description || "",
|
|
1335
1362
|
}));
|
|
1336
|
-
const
|
|
1363
|
+
const deduped = new Map();
|
|
1364
|
+
for (const command of [...builtInCommands, ...extensionCommands, ...promptCommands]) {
|
|
1365
|
+
if (!deduped.has(command.name)) deduped.set(command.name, command);
|
|
1366
|
+
}
|
|
1367
|
+
const lines = [...deduped.values()]
|
|
1337
1368
|
.filter((command) => shouldShowCommandInList(command.name))
|
|
1338
1369
|
.sort((left, right) => left.name.localeCompare(right.name))
|
|
1339
1370
|
.map((command) => `- \`/${command.name}\`${command.description ? ` ${command.description}` : ""}`)
|
|
1340
1371
|
.join("\n");
|
|
1341
|
-
this.appendAssistantMarkdown(`## Commands\n\n${lines}`);
|
|
1342
|
-
this.setStatus("Listed
|
|
1372
|
+
this.appendAssistantMarkdown(`## More Commands\n\n${lines}`);
|
|
1373
|
+
this.setStatus("Listed all commands.", "accent");
|
|
1343
1374
|
}
|
|
1344
1375
|
|
|
1345
1376
|
showExtensionsList() {
|
|
@@ -1699,7 +1730,7 @@ export class SeedclubInteractiveModeApp {
|
|
|
1699
1730
|
}
|
|
1700
1731
|
|
|
1701
1732
|
showExtensionNotify(message, type = "info") {
|
|
1702
|
-
if (type !== "info") {
|
|
1733
|
+
if (type !== "info" || message.includes("\n")) {
|
|
1703
1734
|
this.appendAssistantMarkdown(`${getNotificationLabel(type)}: ${message}`);
|
|
1704
1735
|
}
|
|
1705
1736
|
this.setStatus(message.split("\n")[0], getNotificationTone(type));
|
|
@@ -1922,6 +1953,10 @@ export class SeedclubInteractiveModeApp {
|
|
|
1922
1953
|
this.showCommandsList();
|
|
1923
1954
|
return true;
|
|
1924
1955
|
}
|
|
1956
|
+
if (text === "/more") {
|
|
1957
|
+
this.showMoreCommandsList();
|
|
1958
|
+
return true;
|
|
1959
|
+
}
|
|
1925
1960
|
if (text === "/extensions") {
|
|
1926
1961
|
this.showExtensionsList();
|
|
1927
1962
|
return true;
|
|
@@ -1947,6 +1982,10 @@ export class SeedclubInteractiveModeApp {
|
|
|
1947
1982
|
await this.handleNewSession();
|
|
1948
1983
|
return true;
|
|
1949
1984
|
}
|
|
1985
|
+
if (text === "/clear") {
|
|
1986
|
+
this.handleClearVisibleChat();
|
|
1987
|
+
return true;
|
|
1988
|
+
}
|
|
1950
1989
|
if (text === "/compact" || text.startsWith("/compact ")) {
|
|
1951
1990
|
const customInstructions = text.startsWith("/compact ") ? text.slice(9).trim() : undefined;
|
|
1952
1991
|
await this.handleCompactCommand(customInstructions);
|
|
@@ -2108,6 +2147,16 @@ export class SeedclubInteractiveModeApp {
|
|
|
2108
2147
|
}
|
|
2109
2148
|
}
|
|
2110
2149
|
|
|
2150
|
+
handleClearVisibleChat() {
|
|
2151
|
+
this.chat.clear();
|
|
2152
|
+
this.pendingTools.clear();
|
|
2153
|
+
this.streamingAssistant = undefined;
|
|
2154
|
+
this.workingMessageOverride = undefined;
|
|
2155
|
+
this.clearTransientStatus();
|
|
2156
|
+
this.setStatus("Cleared visible chat.", "accent");
|
|
2157
|
+
this.ui.requestRender();
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2111
2160
|
async handleNewSession() {
|
|
2112
2161
|
try {
|
|
2113
2162
|
const completed = await this.startNewSession();
|
package/postinstall.js
CHANGED
|
@@ -73,7 +73,12 @@ if (!existsSync(settingsFile)) {
|
|
|
73
73
|
writeFileSync(
|
|
74
74
|
settingsFile,
|
|
75
75
|
JSON.stringify(
|
|
76
|
-
{
|
|
76
|
+
{
|
|
77
|
+
quietStartup: true,
|
|
78
|
+
enableSkillCommands: true,
|
|
79
|
+
seedclubMemoryDefaultEnabled: true,
|
|
80
|
+
seedclubMemoryDefaultApplied: false,
|
|
81
|
+
},
|
|
77
82
|
null,
|
|
78
83
|
2,
|
|
79
84
|
) + "\n",
|