@clubnet/seedclub 0.2.42 → 0.2.44
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 +24 -13
- package/assets/extensions/seedclub/commands/seedclub.ts +1 -1
- package/assets/extensions/seedclub/tools/opportunity-research.ts +20 -105
- package/assets/extensions/seedclub/ui-copy.ts +2 -2
- package/package.json +1 -1
- package/packages/seedclub-tui/src/app/interactive-mode.mjs +57 -21
package/assets/SYSTEM.md
CHANGED
|
@@ -32,20 +32,30 @@ Bad:
|
|
|
32
32
|
|
|
33
33
|
"This is a good deal."
|
|
34
34
|
"You should invest."
|
|
35
|
-
"The right answer is to
|
|
35
|
+
"The right answer is to source."
|
|
36
36
|
|
|
37
|
-
##
|
|
37
|
+
## Investment First-Read Format
|
|
38
38
|
|
|
39
|
-
When
|
|
39
|
+
When responding to decks, memos, tweets, company links, founder pitches, or investment opportunities, do not narrate your process. Do not open with meta framing such as "Considering a concise response," "I need to," or "I should." Start with the take.
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
2. Why: 1-3 strongest reasons
|
|
43
|
-
3. Main risk or open question
|
|
44
|
-
4. What to pull on next
|
|
41
|
+
Use this order internally, but do not print the structure labels as headings:
|
|
45
42
|
|
|
46
|
-
|
|
43
|
+
1. TLDR
|
|
44
|
+
2. Verdict
|
|
45
|
+
3. Why it is interesting
|
|
46
|
+
4. Where conviction breaks
|
|
47
|
+
5. Team read
|
|
48
|
+
6. Conversational next move
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
In the visible answer, keep at most one label: `TLDR:`. The final move should be phrased as a natural question, not labeled "Conversational next move."
|
|
51
|
+
|
|
52
|
+
Keep the first read sharp and qualitative. Do not default to bulleted Founder / Company / Opportunity sections unless the user asks for extraction. Do not lead with operational deal terms such as raise size, instrument, or use of funds unless they change the investment judgment.
|
|
53
|
+
|
|
54
|
+
Pressure-test claims with named comparables when useful, such as Polymarket, Kalshi, Earnest, Stripe, Shopify, or other relevant category references. End with a conversational pull, not a data-extraction checklist. Do not offer an upsell like "want a sharper investor take?" because the first read should already be sharp.
|
|
55
|
+
|
|
56
|
+
Example shape:
|
|
57
|
+
|
|
58
|
+
"TLDR: worth digging in, but only if the team has a non-obvious distribution wedge. I read it as mixed-positive: the interesting part is not the market size claim; it is whether they can sit between existing venues and a consumer workflow that Polymarket or Kalshi will not own. Conviction breaks if the product is just a nicer front end with no supply, liquidity, or trust advantage. The team read is promising if they have real market-structure instincts, but I would pressure-test that hard. Do they already have venue access or a credible path to it?"
|
|
49
59
|
|
|
50
60
|
Operating model:
|
|
51
61
|
|
|
@@ -79,10 +89,11 @@ Seed Club platform policy:
|
|
|
79
89
|
Research and review policy:
|
|
80
90
|
|
|
81
91
|
- 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
|
|
83
|
-
-
|
|
84
|
-
- After
|
|
85
|
-
-
|
|
92
|
+
- When a member drops a deck, memo, PDF, document, or image, use the Seed Club document reader tool without asking for the program, then review the returned document preview.
|
|
93
|
+
- First answer with the Investment First-Read Format. Do not treat normal deal chat as a submission workflow.
|
|
94
|
+
- After the first read, ask only the highest-leverage conversational next question. Do not ask a long intake checklist.
|
|
95
|
+
- Treat sourcing as a high-intent explicit member action. Do not suggest push/pass/hold, do not ask whether to file a note, and do not create a shared review handoff from ordinary discussion.
|
|
96
|
+
- When the member explicitly asks what to surface, source, or send to the network, answer directly with the few highest-leverage surfaced questions or points. Keep it to 3-5 items unless they ask for more. Do not end by offering to turn it into a note, memo, post, or sharper take.
|
|
86
97
|
- If document preview is partial or failed, say what could not be read and ask targeted questions instead of inventing missing facts.
|
|
87
98
|
|
|
88
99
|
External research policy:
|
|
@@ -201,7 +201,7 @@ export function registerSeedclubCommand(pi: ExtensionAPI, deps: SeedclubDeps) {
|
|
|
201
201
|
handler: async (_args, ctx) => {
|
|
202
202
|
await prefillEditor(
|
|
203
203
|
ctx,
|
|
204
|
-
"Help me diligence this opportunity. Start by asking for the company, deck, memo, source URL, or local file if needed. Use Seed Club records and source-backed external research,
|
|
204
|
+
"Help me diligence this opportunity. Start by asking for the company, deck, memo, source URL, or local file if needed. Use Seed Club records and source-backed external research. Give me a sharp first read that covers TLDR, verdict, why it is interesting, where conviction breaks, team read, and a natural closing question, without printing those as section labels except TLDR. Pressure-test claims with named comparables and avoid generic founder/company/opportunity extraction sections unless I ask for them.",
|
|
205
205
|
);
|
|
206
206
|
},
|
|
207
207
|
});
|
|
@@ -6,8 +6,6 @@ import { ApiError, api, NotConnectedError } from "../api-client.js";
|
|
|
6
6
|
import { makeProgressCallRenderer, makeProgressResultRenderer } from "../tool-utils.js";
|
|
7
7
|
import { getToolCallLabel, getToolSuccessLabel } from "../ui-copy.js";
|
|
8
8
|
|
|
9
|
-
type OpportunityResearchDecision = "pushing" | "passing" | "holding";
|
|
10
|
-
|
|
11
9
|
const DEFAULT_OPPORTUNITY_RESEARCH_PROGRAM_SLUG = "seed-network";
|
|
12
10
|
const MAX_DOCUMENT_PREVIEW_CHARS = 12000;
|
|
13
11
|
const MAX_IMAGE_PREVIEW_BYTES = 8 * 1024 * 1024;
|
|
@@ -39,14 +37,6 @@ function normalizeOptionalNumber(value: unknown): number | null | undefined {
|
|
|
39
37
|
return value;
|
|
40
38
|
}
|
|
41
39
|
|
|
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
40
|
function clipPreviewText(value: string) {
|
|
51
41
|
const normalized = value.replace(/\u0000/g, "").trim();
|
|
52
42
|
if (normalized.length <= MAX_DOCUMENT_PREVIEW_CHARS) {
|
|
@@ -146,17 +136,6 @@ async function buildDocumentPreview(fileName: string, contentType: string, bytes
|
|
|
146
136
|
};
|
|
147
137
|
}
|
|
148
138
|
|
|
149
|
-
function suggestedReviewFields() {
|
|
150
|
-
return [
|
|
151
|
-
"Founder info",
|
|
152
|
-
"Company info",
|
|
153
|
-
"Opportunity 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
139
|
function redactPreviewForJson(preview: DocumentPreview | undefined) {
|
|
161
140
|
if (!preview) return preview;
|
|
162
141
|
const { image, ...rest } = preview;
|
|
@@ -181,70 +160,24 @@ function inferDocumentContentType(fileName: string) {
|
|
|
181
160
|
return "application/octet-stream";
|
|
182
161
|
}
|
|
183
162
|
|
|
184
|
-
function normalizeDecision(value: unknown): OpportunityResearchDecision | 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: OpportunityResearchDecision) {
|
|
190
|
-
return api.post<any>(`/programs/${programSlug}/opportunity-research/drafts/${draftId}/member-position`, {
|
|
191
|
-
ask_unknown: args.askUnknown === true,
|
|
192
|
-
company_name: normalizeOptionalString(args.companyName),
|
|
193
|
-
company_website_url: normalizeOptionalString(args.companyWebsiteUrl),
|
|
194
|
-
contact_info: normalizeOptionalString(args.contactInfo),
|
|
195
|
-
contact_unknown: args.contactUnknown === true,
|
|
196
|
-
decision,
|
|
197
|
-
excitement_reason: normalizeOptionalString(args.excitementReason),
|
|
198
|
-
flagged_topics: normalizeStringArray(args.flaggedTopics),
|
|
199
|
-
founder_names: normalizeStringArray(args.founderNames),
|
|
200
|
-
founder_relationship: normalizeOptionalString(args.founderRelationship),
|
|
201
|
-
founder_unknown: args.founderUnknown === true,
|
|
202
|
-
target_amount: normalizeOptionalNumber(args.targetAmount),
|
|
203
|
-
vouch_text: normalizeOptionalString(args.vouchText),
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
|
|
207
163
|
async function submitOpportunityResearchDocument(args: {
|
|
208
|
-
askUnknown?: boolean | null;
|
|
209
164
|
programSlug?: string | null;
|
|
210
165
|
filePath?: string | null;
|
|
211
166
|
assetKind?: "deck" | "memo" | "supporting_material" | null;
|
|
212
167
|
companyName?: string | null;
|
|
213
168
|
companyWebsiteUrl?: string | null;
|
|
214
169
|
contentType?: string | null;
|
|
215
|
-
contactInfo?: string | null;
|
|
216
|
-
contactUnknown?: boolean | null;
|
|
217
|
-
decision?: OpportunityResearchDecision | null;
|
|
218
|
-
draftId?: string | null;
|
|
219
|
-
excitementReason?: string | null;
|
|
220
170
|
fileName?: string | null;
|
|
221
|
-
flaggedTopics?: string[] | null;
|
|
222
|
-
founderNames?: string[] | null;
|
|
223
|
-
founderRelationship?: string | null;
|
|
224
|
-
founderUnknown?: boolean | null;
|
|
225
171
|
roundType?: string | null;
|
|
226
172
|
sourceUrl?: string | null;
|
|
227
173
|
targetAmount?: number | null;
|
|
228
174
|
valuation?: number | null;
|
|
229
|
-
vouchText?: string | null;
|
|
230
175
|
}, runtime?: any) {
|
|
231
176
|
try {
|
|
232
177
|
const programSlug = normalizeOptionalString(args.programSlug) ?? DEFAULT_OPPORTUNITY_RESEARCH_PROGRAM_SLUG;
|
|
233
|
-
const existingDraftId = normalizeOptionalString(args.draftId);
|
|
234
|
-
const existingDecision = normalizeDecision(args.decision);
|
|
235
|
-
if (existingDraftId) {
|
|
236
|
-
if (!existingDecision) {
|
|
237
|
-
return { draft: { id: existingDraftId }, next_action: "Ask the member whether they want to push this opportunity for review, pass, or hold it for later." };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
runtime?.setWorkingMessage?.("Saving opportunity draft position...");
|
|
241
|
-
const positionResponse = await submitDraftPosition(programSlug, existingDraftId, args, existingDecision);
|
|
242
|
-
return { draft: { id: existingDraftId }, draft_position_result: positionResponse };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
178
|
const filePath = args.filePath;
|
|
246
179
|
if (!filePath) {
|
|
247
|
-
return { error: "filePath is required
|
|
180
|
+
return { error: "filePath is required." };
|
|
248
181
|
}
|
|
249
182
|
|
|
250
183
|
const fileStats = await stat(filePath);
|
|
@@ -255,7 +188,7 @@ async function submitOpportunityResearchDocument(args: {
|
|
|
255
188
|
const fileName = normalizeOptionalString(args.fileName) ?? basename(filePath);
|
|
256
189
|
const contentType = normalizeOptionalString(args.contentType) ?? inferDocumentContentType(fileName);
|
|
257
190
|
const assetKind = normalizeOptionalString(args.assetKind) ?? "deck";
|
|
258
|
-
runtime?.setWorkingMessage?.("
|
|
191
|
+
runtime?.setWorkingMessage?.("Preparing document context...");
|
|
259
192
|
const ingestResponse = await api.post<any>(`/programs/${programSlug}/opportunity-research/document-ingests`, {
|
|
260
193
|
asset_kind: assetKind,
|
|
261
194
|
content_type: contentType,
|
|
@@ -269,10 +202,10 @@ async function submitOpportunityResearchDocument(args: {
|
|
|
269
202
|
const uploadId = typeof ingestResponse?.upload?.id === "string" ? ingestResponse.upload.id : null;
|
|
270
203
|
|
|
271
204
|
if (!uploadUrl || !objectKey || !uploadId) {
|
|
272
|
-
return { error: "The API did not return a valid
|
|
205
|
+
return { error: "The API did not return a valid document processing target.", response: ingestResponse };
|
|
273
206
|
}
|
|
274
207
|
|
|
275
|
-
runtime?.setWorkingMessage?.("
|
|
208
|
+
runtime?.setWorkingMessage?.("Processing document...");
|
|
276
209
|
const bytes = await readFile(filePath);
|
|
277
210
|
const documentPreview = await buildDocumentPreview(fileName, contentType, bytes);
|
|
278
211
|
const uploadHeaders: Record<string, string> = {};
|
|
@@ -292,12 +225,12 @@ async function submitOpportunityResearchDocument(args: {
|
|
|
292
225
|
if (!uploadResponse.ok) {
|
|
293
226
|
const text = await uploadResponse.text().catch(() => "");
|
|
294
227
|
return {
|
|
295
|
-
error: `
|
|
228
|
+
error: `Document processing failed (${uploadResponse.status}).`,
|
|
296
229
|
details: text.slice(0, 500),
|
|
297
230
|
};
|
|
298
231
|
}
|
|
299
232
|
|
|
300
|
-
runtime?.setWorkingMessage?.("
|
|
233
|
+
runtime?.setWorkingMessage?.("Preparing document preview...");
|
|
301
234
|
const completeResponse = await api.post<any>(`/programs/${programSlug}/opportunity-research/document-ingests/${uploadId}/complete`, {
|
|
302
235
|
asset_kind: assetKind,
|
|
303
236
|
company_name: normalizeOptionalString(args.companyName),
|
|
@@ -313,15 +246,9 @@ async function submitOpportunityResearchDocument(args: {
|
|
|
313
246
|
valuation: normalizeOptionalNumber(args.valuation),
|
|
314
247
|
});
|
|
315
248
|
|
|
316
|
-
const decision = normalizeDecision(args.decision);
|
|
317
249
|
return {
|
|
318
250
|
...completeResponse,
|
|
319
|
-
decision_ignored_until_review: Boolean(decision),
|
|
320
251
|
document_preview: documentPreview,
|
|
321
|
-
next_action: decision
|
|
322
|
-
? "The document is saved as a private draft. Review the material, summarize Founder info, Company info, Opportunity info, and Opportunity Summary, then ask missing questions before submitting the member decision."
|
|
323
|
-
: "Review the material, summarize Founder info, Company info, Opportunity info, and Opportunity Summary, then ask whether the member wants to push, pass, or hold after missing context is resolved.",
|
|
324
|
-
suggested_review_fields: suggestedReviewFields(),
|
|
325
252
|
upload: {
|
|
326
253
|
...completeResponse?.upload,
|
|
327
254
|
storage_url: storageUrl ?? completeResponse?.upload?.storage_url ?? null,
|
|
@@ -360,12 +287,15 @@ function makeOpportunityResearchExecute(fn: typeof submitOpportunityResearchDocu
|
|
|
360
287
|
...result,
|
|
361
288
|
document_preview: redactPreviewForJson(result?.document_preview),
|
|
362
289
|
};
|
|
363
|
-
const
|
|
290
|
+
const modelDetails = {
|
|
291
|
+
document_preview: details.document_preview,
|
|
292
|
+
};
|
|
293
|
+
const content: any[] = [{ type: "text", text: JSON.stringify(modelDetails, null, 2) }];
|
|
364
294
|
if (image?.data && image?.mimeType) {
|
|
365
295
|
content.push({ type: "image", data: image.data, mimeType: image.mimeType });
|
|
366
296
|
}
|
|
367
297
|
|
|
368
|
-
return { content, details };
|
|
298
|
+
return { content, details: modelDetails };
|
|
369
299
|
} catch (error) {
|
|
370
300
|
if (error instanceof NotConnectedError) {
|
|
371
301
|
return {
|
|
@@ -388,42 +318,27 @@ function makeOpportunityResearchExecute(fn: typeof submitOpportunityResearchDocu
|
|
|
388
318
|
}
|
|
389
319
|
|
|
390
320
|
export function registerOpportunityResearchTools(pi: ExtensionAPI) {
|
|
391
|
-
|
|
392
|
-
name: "
|
|
393
|
-
label: "
|
|
321
|
+
const readDocumentTool: any = {
|
|
322
|
+
name: "seedclub_read_document",
|
|
323
|
+
label: "Read Document",
|
|
394
324
|
description:
|
|
395
|
-
"
|
|
325
|
+
"Read a local deck, memo, PDF, document, or image for document review context. programSlug defaults to seed-network. For a plain file path or a prompt like 'let's chat about this opportunity', read the document preview and give a sharp first read that covers TLDR, verdict, why it is interesting, where conviction breaks, team read, and a natural closing question. Do not print those as section labels except TLDR. Do not treat this as sourcing or submission. Do not use generic founder/company/opportunity extraction sections unless the user asks for extraction.",
|
|
396
326
|
parameters: Type.Object({
|
|
397
|
-
askUnknown: Type.Optional(Type.Union([Type.Boolean(), Type.Null()], { description: "Set true when the member explicitly says the round ask is unknown." })),
|
|
398
327
|
programSlug: Type.Optional(Type.Union([Type.String({ description: "Program slug for the opportunity research program. Defaults to seed-network." }), Type.Null()])),
|
|
399
|
-
filePath: Type.Optional(Type.Union([Type.String({ description: "Local deck, memo, document, or image path.
|
|
328
|
+
filePath: Type.Optional(Type.Union([Type.String({ description: "Local deck, memo, PDF, document, or image path." }), Type.Null()])),
|
|
400
329
|
assetKind: Type.Optional(Type.Union([Type.Literal("deck"), Type.Literal("memo"), Type.Literal("supporting_material"), Type.Null()])),
|
|
401
330
|
companyName: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
402
331
|
companyWebsiteUrl: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
403
332
|
contentType: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
404
|
-
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." })),
|
|
405
|
-
contactUnknown: Type.Optional(Type.Union([Type.Boolean(), Type.Null()], { description: "Set true when the member explicitly says contact info is unknown." })),
|
|
406
|
-
decision: Type.Optional(Type.Union([Type.Literal("pushing"), Type.Literal("passing"), Type.Literal("holding"), Type.Null()])),
|
|
407
|
-
draftId: Type.Optional(Type.Union([Type.String({ description: "Existing private opportunity draft id to update without re-uploading the file." }), Type.Null()])),
|
|
408
|
-
excitementReason: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Why the member is excited about the company/founder." })),
|
|
409
333
|
fileName: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
410
|
-
flaggedTopics: Type.Optional(Type.Union([Type.Array(Type.String()), Type.Null()])),
|
|
411
|
-
founderNames: Type.Optional(Type.Union([Type.Array(Type.String()), Type.Null()], { description: "Founder names supplied or corrected by the member." })),
|
|
412
|
-
founderRelationship: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "How the member knows the founder." })),
|
|
413
|
-
founderUnknown: Type.Optional(Type.Union([Type.Boolean(), Type.Null()], { description: "Set true when the member explicitly says founder names are unknown." })),
|
|
414
334
|
roundType: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
415
335
|
sourceUrl: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
416
336
|
targetAmount: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
|
|
417
337
|
valuation: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
|
|
418
|
-
vouchText: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
419
338
|
}),
|
|
420
339
|
execute: makeOpportunityResearchExecute(submitOpportunityResearchDocument),
|
|
421
|
-
renderCall: makeProgressCallRenderer(getToolCallLabel("
|
|
422
|
-
renderResult: makeProgressResultRenderer(getToolSuccessLabel("
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const id = details?.draft_position_result?.funding_round?.id ?? details?.draft?.id ?? details?.draft_position_result?.draft?.id ?? null;
|
|
426
|
-
return [company, state, id].filter(Boolean).join(" · ") || undefined;
|
|
427
|
-
}),
|
|
428
|
-
});
|
|
340
|
+
renderCall: makeProgressCallRenderer(getToolCallLabel("seedclub_read_document"), (args) => args?.companyName || args?.filePath || undefined),
|
|
341
|
+
renderResult: makeProgressResultRenderer(getToolSuccessLabel("seedclub_read_document")),
|
|
342
|
+
};
|
|
343
|
+
pi.registerTool(readDocumentTool);
|
|
429
344
|
}
|
|
@@ -11,7 +11,7 @@ export const TOOL_CALL_LABELS: Record<string, string> = {
|
|
|
11
11
|
seedclub_create_crm_task: "Creating CRM task",
|
|
12
12
|
seedclub_save_research: "Saving research",
|
|
13
13
|
seedclub_research_opportunity: "Researching opportunity",
|
|
14
|
-
|
|
14
|
+
seedclub_read_document: "Reading document",
|
|
15
15
|
seedclub_get_research: "Loading research",
|
|
16
16
|
seedclub_list_program_contacts: "Loading program contacts",
|
|
17
17
|
seedclub_list_program_funnels: "Loading program funnels",
|
|
@@ -63,7 +63,7 @@ export const TOOL_SUCCESS_LABELS: Record<string, string> = {
|
|
|
63
63
|
seedclub_create_crm_task: "CRM task created",
|
|
64
64
|
seedclub_save_research: "Research saved",
|
|
65
65
|
seedclub_research_opportunity: "Research queued",
|
|
66
|
-
|
|
66
|
+
seedclub_read_document: "Document contributed to network intelligence",
|
|
67
67
|
seedclub_get_research: "Research loaded",
|
|
68
68
|
seedclub_list_program_contacts: "Program contacts loaded",
|
|
69
69
|
seedclub_list_program_funnels: "Program funnels loaded",
|
package/package.json
CHANGED
|
@@ -99,6 +99,17 @@ function firstTextLine(content) {
|
|
|
99
99
|
return text.split("\n")[0] ?? null;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
function hideThinkingBlock() {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function withoutThinkingContent(message) {
|
|
107
|
+
if (!message || !Array.isArray(message.content)) return message;
|
|
108
|
+
const content = message.content.filter((item) => item?.type !== "thinking");
|
|
109
|
+
if (content.length === message.content.length) return message;
|
|
110
|
+
return { ...message, content };
|
|
111
|
+
}
|
|
112
|
+
|
|
102
113
|
function getToolStatusText(toolName, toolDefinition, args) {
|
|
103
114
|
const callMeta = getToolRenderStatusMeta(toolDefinition, "renderCall");
|
|
104
115
|
if (typeof callMeta?.label === "string" && callMeta.label.trim()) {
|
|
@@ -395,6 +406,7 @@ const SEEDCLUB_AUTH_GATE_STATE_KEY = "__seedclubAuthGateState";
|
|
|
395
406
|
const SEEDCLUB_SHELL_WELCOME_STATE_KEY = "__seedclubShellWelcomeState";
|
|
396
407
|
const UPDATE_PREFS_FILE = join(homedir(), ".seedclub", "agent", ".seedclub-update-prefs.json");
|
|
397
408
|
const UPDATE_COMMAND = "npm install -g @clubnet/seedclub@latest";
|
|
409
|
+
const STARTUP_UPDATE_CHECK_TIMEOUT_MS = 2500;
|
|
398
410
|
const SEEDCLUB_CONFIG_DIR = join(homedir(), ".config", "seedclub");
|
|
399
411
|
const SEEDCLUB_TOKEN_FILE = join(SEEDCLUB_CONFIG_DIR, "token");
|
|
400
412
|
const SEEDCLUB_BASES_FILE = join(SEEDCLUB_CONFIG_DIR, "bases.json");
|
|
@@ -1216,8 +1228,8 @@ export class SeedclubInteractiveModeApp {
|
|
|
1216
1228
|
}),
|
|
1217
1229
|
});
|
|
1218
1230
|
this.editor = this.defaultEditor;
|
|
1219
|
-
|
|
1220
|
-
|
|
1231
|
+
this.pendingTools = new Map();
|
|
1232
|
+
this.streamingAssistant = undefined;
|
|
1221
1233
|
this.toolOutputExpanded = false;
|
|
1222
1234
|
this.activeOverlay = undefined;
|
|
1223
1235
|
this.customHeader = undefined;
|
|
@@ -1291,31 +1303,54 @@ export class SeedclubInteractiveModeApp {
|
|
|
1291
1303
|
this.updateAnnouncementShown = true;
|
|
1292
1304
|
}
|
|
1293
1305
|
|
|
1306
|
+
async getAvailableUpdateInfo(options = {}) {
|
|
1307
|
+
const installed = getInstalledVersionInfo();
|
|
1308
|
+
if (!installed?.seedclubVersion) return null;
|
|
1309
|
+
const latest = await getLatestVersionInfo();
|
|
1310
|
+
if (!latest) return null;
|
|
1311
|
+
if (compareSemver(latest, installed.seedclubVersion) <= 0) return null;
|
|
1312
|
+
const prefs = getUpdatePrefs();
|
|
1313
|
+
if (options.respectSkip !== false && prefs.skipVersion === latest) return null;
|
|
1314
|
+
return {
|
|
1315
|
+
installedVersion: installed.seedclubVersion,
|
|
1316
|
+
latestVersion: latest,
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1294
1320
|
startBackgroundUpdateCheck() {
|
|
1295
1321
|
if (this.updateCheckStarted) return;
|
|
1296
1322
|
this.updateCheckStarted = true;
|
|
1297
1323
|
this.updateAnnouncementShown = false;
|
|
1298
1324
|
this.setAvailableUpdate(null, null);
|
|
1299
1325
|
void (async () => {
|
|
1300
|
-
const
|
|
1301
|
-
if (!
|
|
1302
|
-
|
|
1303
|
-
if (!latest) return;
|
|
1304
|
-
if (compareSemver(latest, installed.seedclubVersion) <= 0) return;
|
|
1305
|
-
const prefs = getUpdatePrefs();
|
|
1306
|
-
if (prefs.skipVersion === latest) return;
|
|
1307
|
-
this.setAvailableUpdate(installed.seedclubVersion, latest);
|
|
1326
|
+
const update = await this.getAvailableUpdateInfo();
|
|
1327
|
+
if (!update) return;
|
|
1328
|
+
this.setAvailableUpdate(update.installedVersion, update.latestVersion);
|
|
1308
1329
|
this.maybeAnnounceAvailableUpdate();
|
|
1309
1330
|
})().catch(() => {});
|
|
1310
1331
|
}
|
|
1311
1332
|
|
|
1312
|
-
async
|
|
1333
|
+
async maybeShowStartupUpdateMenu() {
|
|
1334
|
+
const update = await withTimeout(
|
|
1335
|
+
this.getAvailableUpdateInfo(),
|
|
1336
|
+
STARTUP_UPDATE_CHECK_TIMEOUT_MS,
|
|
1337
|
+
null,
|
|
1338
|
+
);
|
|
1339
|
+
if (!update) return false;
|
|
1340
|
+
this.updateCheckStarted = true;
|
|
1341
|
+
this.setAvailableUpdate(update.installedVersion, update.latestVersion);
|
|
1342
|
+
await this.showUpdateMenu({ availableUpdate: update });
|
|
1343
|
+
this.updateAnnouncementShown = true;
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
async showUpdateMenu(options = {}) {
|
|
1313
1348
|
const installed = getInstalledVersionInfo();
|
|
1314
1349
|
if (!installed?.seedclubVersion) {
|
|
1315
1350
|
this.showExtensionNotify("Unable to determine the installed seedclub version.", "error");
|
|
1316
1351
|
return;
|
|
1317
1352
|
}
|
|
1318
|
-
const latest = await getLatestVersionInfo();
|
|
1353
|
+
const latest = options.availableUpdate?.latestVersion ?? await getLatestVersionInfo();
|
|
1319
1354
|
if (!latest) {
|
|
1320
1355
|
this.showExtensionNotify("Unable to check npm for the latest seedclub version.", "error");
|
|
1321
1356
|
return;
|
|
@@ -1601,7 +1636,7 @@ export class SeedclubInteractiveModeApp {
|
|
|
1601
1636
|
this.chat.addChild(
|
|
1602
1637
|
new AssistantMessageComponent(
|
|
1603
1638
|
assistantMarkdownMessage(text),
|
|
1604
|
-
|
|
1639
|
+
hideThinkingBlock(),
|
|
1605
1640
|
),
|
|
1606
1641
|
);
|
|
1607
1642
|
}
|
|
@@ -1689,8 +1724,8 @@ export class SeedclubInteractiveModeApp {
|
|
|
1689
1724
|
} else if (message.role === "assistant") {
|
|
1690
1725
|
this.chat.addChild(
|
|
1691
1726
|
new AssistantMessageComponent(
|
|
1692
|
-
message,
|
|
1693
|
-
|
|
1727
|
+
withoutThinkingContent(message),
|
|
1728
|
+
hideThinkingBlock(),
|
|
1694
1729
|
),
|
|
1695
1730
|
);
|
|
1696
1731
|
} else if (message.role === "custom" && message.display !== false) {
|
|
@@ -2445,8 +2480,8 @@ export class SeedclubInteractiveModeApp {
|
|
|
2445
2480
|
this.chat.addChild(new UserMessageComponent(messageText(event.message)));
|
|
2446
2481
|
} else if (event.message.role === "assistant") {
|
|
2447
2482
|
this.streamingAssistant = new AssistantMessageComponent(
|
|
2448
|
-
event.message,
|
|
2449
|
-
|
|
2483
|
+
withoutThinkingContent(event.message),
|
|
2484
|
+
hideThinkingBlock(),
|
|
2450
2485
|
);
|
|
2451
2486
|
this.chat.addChild(this.streamingAssistant);
|
|
2452
2487
|
} else if (event.message.role === "custom" && event.message.display !== false) {
|
|
@@ -2456,12 +2491,12 @@ export class SeedclubInteractiveModeApp {
|
|
|
2456
2491
|
if (event.message.role === "assistant") {
|
|
2457
2492
|
if (!this.streamingAssistant) {
|
|
2458
2493
|
this.streamingAssistant = new AssistantMessageComponent(
|
|
2459
|
-
event.message,
|
|
2460
|
-
|
|
2494
|
+
withoutThinkingContent(event.message),
|
|
2495
|
+
hideThinkingBlock(),
|
|
2461
2496
|
);
|
|
2462
2497
|
this.chat.addChild(this.streamingAssistant);
|
|
2463
2498
|
}
|
|
2464
|
-
this.streamingAssistant.updateContent(event.message);
|
|
2499
|
+
this.streamingAssistant.updateContent(withoutThinkingContent(event.message));
|
|
2465
2500
|
}
|
|
2466
2501
|
} else if (event.type === "tool_execution_start") {
|
|
2467
2502
|
const toolDefinition = this.session.getToolDefinition?.(event.toolName);
|
|
@@ -2554,9 +2589,10 @@ export class SeedclubInteractiveModeApp {
|
|
|
2554
2589
|
this.ui.setFocus(this.editor);
|
|
2555
2590
|
await this.bindExtensions();
|
|
2556
2591
|
this.configureAutocomplete();
|
|
2557
|
-
this.bindShellUiLifecycle();
|
|
2558
2592
|
this.unsubscribe = this.session.subscribe((event) => this.handleSessionEvent(event));
|
|
2559
2593
|
this.ui.start();
|
|
2594
|
+
await this.maybeShowStartupUpdateMenu();
|
|
2595
|
+
this.bindShellUiLifecycle();
|
|
2560
2596
|
this.ui.requestRender(true);
|
|
2561
2597
|
}
|
|
2562
2598
|
|