@localpulse/cli 0.0.6 → 0.0.7
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/package.json +1 -1
- package/src/index.test.ts +7 -3
- package/src/index.ts +22 -8
- package/src/lib/research-audit.test.ts +3 -12
- package/src/lib/research-audit.ts +5 -13
package/package.json
CHANGED
package/src/index.test.ts
CHANGED
|
@@ -156,7 +156,8 @@ describe("localpulse", () => {
|
|
|
156
156
|
expect(stdout).toContain("Title: Test Night");
|
|
157
157
|
expect(stdout).toContain("Date: 2026-03-14T22:00:00+01:00");
|
|
158
158
|
expect(stdout).toContain("Venue: Shelter (Amsterdam) [ChIJ123]");
|
|
159
|
-
expect(stdout).toContain("Featured: 1
|
|
159
|
+
expect(stdout).toContain("Featured: 1");
|
|
160
|
+
expect(stdout).toContain("DJ Nobu — 1 socials, ");
|
|
160
161
|
expect(stdout).toContain("Organizer: Dekmantel");
|
|
161
162
|
expect(stdout).toContain("URLs: 2 source(s)");
|
|
162
163
|
expect(stdout).toContain("Ticket: https://tickets.example.com");
|
|
@@ -192,7 +193,10 @@ describe("localpulse", () => {
|
|
|
192
193
|
expect(result.parsed.city).toBe("Amsterdam");
|
|
193
194
|
expect(result.parsed.venue_place_id).toBe("ChIJ123");
|
|
194
195
|
expect(result.parsed.featured_count).toBe(2);
|
|
195
|
-
expect(result.parsed.
|
|
196
|
+
expect(result.parsed.featured[0].name).toBe("DJ Nobu");
|
|
197
|
+
expect(result.parsed.featured[0].socials_count).toBe(1);
|
|
198
|
+
expect(result.parsed.featured[1].name).toBe("Nene H");
|
|
199
|
+
expect(result.parsed.featured[1].socials_count).toBe(1);
|
|
196
200
|
expect(result.parsed.organizer).toBe("Dekmantel");
|
|
197
201
|
expect(result.parsed.urls_count).toBe(2);
|
|
198
202
|
expect(result.parsed.ticket_url).toBe("https://tickets.example.com");
|
|
@@ -213,7 +217,7 @@ describe("localpulse", () => {
|
|
|
213
217
|
expect(stdout).toContain("Event name (required)");
|
|
214
218
|
expect(stdout).toContain("ISO 8601 datetime (required)");
|
|
215
219
|
expect(stdout).toContain("Kind of event (required)");
|
|
216
|
-
expect(stdout).toContain("
|
|
220
|
+
expect(stdout).toContain("Scrapeable source pages (required, 1+)");
|
|
217
221
|
expect(stdout).toContain('required unless price is "Free"');
|
|
218
222
|
});
|
|
219
223
|
|
package/src/index.ts
CHANGED
|
@@ -356,8 +356,12 @@ function formatDryRunSummary(
|
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
if (payload.featured?.length) {
|
|
359
|
-
|
|
360
|
-
|
|
359
|
+
lines.push(` Featured: ${payload.featured.length}`);
|
|
360
|
+
for (const p of payload.featured) {
|
|
361
|
+
const socialsCount = p.socials?.length ?? 0;
|
|
362
|
+
const contextWords = p.context?.trim().split(/\s+/).length ?? 0;
|
|
363
|
+
lines.push(` ${p.name} — ${socialsCount} socials, ${contextWords}w context`);
|
|
364
|
+
}
|
|
361
365
|
}
|
|
362
366
|
|
|
363
367
|
if (payload.organizer?.name) {
|
|
@@ -396,7 +400,11 @@ function buildDryRunJsonResult(
|
|
|
396
400
|
city: opts.city ?? null,
|
|
397
401
|
venue_place_id: opts.venuePlaceId ?? null,
|
|
398
402
|
featured_count: payload.featured?.length ?? 0,
|
|
399
|
-
|
|
403
|
+
featured: payload.featured?.map((p) => ({
|
|
404
|
+
name: p.name,
|
|
405
|
+
socials_count: p.socials?.length ?? 0,
|
|
406
|
+
context_words: p.context?.trim().split(/\s+/).length ?? 0,
|
|
407
|
+
})) ?? [],
|
|
400
408
|
organizer: payload.organizer?.name ?? null,
|
|
401
409
|
urls_count: opts.urls?.length ?? 0,
|
|
402
410
|
ticket_url: opts.ticketUrl ?? null,
|
|
@@ -788,8 +796,9 @@ Research payload:
|
|
|
788
796
|
2. Browse their recent posts or Reels for the event announcement.
|
|
789
797
|
3. Check who is tagged in that post — these are verified performer handles.
|
|
790
798
|
4. Also check the caption for @mentions, #hashtags, and links.
|
|
791
|
-
5.
|
|
792
|
-
Spotify, Bandcamp, YouTube)
|
|
799
|
+
5. OPEN each tagged profile in the browser. Extract every link from their bio
|
|
800
|
+
(website, Spotify, Bandcamp, YouTube, SoundCloud). This is how you populate
|
|
801
|
+
featured[].socials[] and featured[].context. Do NOT just find the URL — visit it.
|
|
793
802
|
6. Check the venue's Tagged tab for posts where others tag the venue —
|
|
794
803
|
artists promoting the event often tag the venue and each other.
|
|
795
804
|
|
|
@@ -802,7 +811,9 @@ Research payload:
|
|
|
802
811
|
should appear in featured[] with a type that fits (chef, DJ, host…).
|
|
803
812
|
|
|
804
813
|
Quality checklist (aim to fill as many as possible):
|
|
805
|
-
✓ Featured person socials: Instagram (verified via tags), website,
|
|
814
|
+
✓ Featured person socials: Instagram (verified via tags), website, and:
|
|
815
|
+
- Musicians/DJs: Spotify, Bandcamp, SoundCloud, RA
|
|
816
|
+
- Other types (chefs, hosts, speakers): personal website is usually enough
|
|
806
817
|
✓ Featured person context: pull from Instagram bio — genre, notable work, links
|
|
807
818
|
✓ Organizer socials: Instagram, website, RA promoter page
|
|
808
819
|
✓ Venue google_place_id (search Google Maps → share → extract place ID)
|
|
@@ -841,8 +852,11 @@ Research payload:
|
|
|
841
852
|
"listening session", "pop-up dinner", "food event",
|
|
842
853
|
"tasting", "market"
|
|
843
854
|
.price Ticket price info: "€15-25", "Free", "Sold out"
|
|
844
|
-
.urls[]
|
|
845
|
-
|
|
855
|
+
.urls[] Scrapeable source pages (required, 1+): venue page, artist
|
|
856
|
+
site, event listing. These get scraped for enrichment —
|
|
857
|
+
use publicly accessible, content-rich pages. NOT ticket URLs.
|
|
858
|
+
.ticket_url Ticketing/purchase URL only (required unless price is "Free");
|
|
859
|
+
do NOT duplicate in event.urls
|
|
846
860
|
.context Schedule, door policy, age restrictions, special notes
|
|
847
861
|
|
|
848
862
|
context Anything that doesn't fit above: background on the
|
|
@@ -81,8 +81,8 @@ describe("auditResearchPayload", () => {
|
|
|
81
81
|
const result = auditResearchPayload({
|
|
82
82
|
featured: [{ name: "Sherin Kalam" }],
|
|
83
83
|
});
|
|
84
|
-
const
|
|
85
|
-
expect(
|
|
84
|
+
const context = result.findings.find((f) => f.field === "featured[0].context");
|
|
85
|
+
expect(context!.action).toContain("Sherin Kalam");
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
// --- organizer ---
|
|
@@ -159,16 +159,7 @@ describe("auditResearchPayload", () => {
|
|
|
159
159
|
expect(result.findings.find((f) => f.field === "event.urls")).toBeDefined();
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
-
it("
|
|
163
|
-
const result = auditResearchPayload({
|
|
164
|
-
event: { urls: ["https://example.com"] },
|
|
165
|
-
});
|
|
166
|
-
const f = result.findings.find((f) => f.field === "event.urls");
|
|
167
|
-
expect(f).toBeDefined();
|
|
168
|
-
expect(f!.message).toContain("Only 1");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("does not flag event with 2+ urls", () => {
|
|
162
|
+
it("does not flag event with 1+ urls", () => {
|
|
172
163
|
const result = auditResearchPayload({
|
|
173
164
|
event: { urls: ["https://example.com", "https://ra.co/events/123"] },
|
|
174
165
|
});
|
|
@@ -57,7 +57,7 @@ function auditFeatured(payload: ResearchPayload): AuditFinding[] {
|
|
|
57
57
|
finding(
|
|
58
58
|
`featured[${i}].socials`,
|
|
59
59
|
`${label} has no social profiles.`,
|
|
60
|
-
`
|
|
60
|
+
`Find their Instagram (check venue/organizer posts for @tags). Then OPEN the profile — extract every link from their bio (Spotify, Bandcamp, YouTube, website). Add all verified URLs to featured[${i}].socials[].`,
|
|
61
61
|
),
|
|
62
62
|
);
|
|
63
63
|
}
|
|
@@ -67,7 +67,7 @@ function auditFeatured(payload: ResearchPayload): AuditFinding[] {
|
|
|
67
67
|
finding(
|
|
68
68
|
`featured[${i}].context`,
|
|
69
69
|
`${label} has no context.`,
|
|
70
|
-
`
|
|
70
|
+
`Open ${person.name}'s Instagram profile and website. Pull their bio, genre, notable releases/work, and collaborations for featured[${i}].context. For musicians: include Spotify/Bandcamp links from their bio.`,
|
|
71
71
|
),
|
|
72
72
|
);
|
|
73
73
|
}
|
|
@@ -88,7 +88,7 @@ function auditOrganizer(payload: ResearchPayload): AuditFinding[] {
|
|
|
88
88
|
finding(
|
|
89
89
|
"organizer.socials",
|
|
90
90
|
`Organizer '${name}' has no social profiles.`,
|
|
91
|
-
`Find '${name}' on Instagram
|
|
91
|
+
`Find '${name}' on Instagram. OPEN the profile — extract website, RA page, and other links from their bio. Add all to organizer.socials[].`,
|
|
92
92
|
),
|
|
93
93
|
];
|
|
94
94
|
}
|
|
@@ -159,16 +159,8 @@ function auditEvent(payload: ResearchPayload): AuditFinding[] {
|
|
|
159
159
|
findings.push(
|
|
160
160
|
finding(
|
|
161
161
|
"event.urls",
|
|
162
|
-
"No
|
|
163
|
-
"Add the
|
|
164
|
-
),
|
|
165
|
-
);
|
|
166
|
-
} else if (payload.event.urls.length === 1) {
|
|
167
|
-
findings.push(
|
|
168
|
-
finding(
|
|
169
|
-
"event.urls",
|
|
170
|
-
"Only 1 event URL provided.",
|
|
171
|
-
"Search for this event on RA (ra.co), Facebook Events, or the venue's website and add additional URLs to event.urls[].",
|
|
162
|
+
"No source URLs provided.",
|
|
163
|
+
"Add the event/venue page URL to event.urls[]. These pages get scraped for enrichment — use publicly accessible pages with useful content (venue page, artist site, event listing). Do NOT put ticketing URLs here.",
|
|
172
164
|
),
|
|
173
165
|
);
|
|
174
166
|
}
|