heyio 0.21.4 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -228,6 +228,156 @@ export const UNIVERSES = [
228
228
  ],
229
229
  },
230
230
  ];
231
+ // ---------------------------------------------------------------------------
232
+ // Additional well-known universe rosters (registered on demand)
233
+ // ---------------------------------------------------------------------------
234
+ const WELL_KNOWN_UNIVERSES = [
235
+ {
236
+ id: "tmnt",
237
+ name: "Teenage Mutant Ninja Turtles",
238
+ tagline: "Cowabunga!",
239
+ characters: [
240
+ { name: "Leonardo", personality: "Disciplined leader who plans before acting. Responsible, strategic, and always puts the team first." },
241
+ { name: "Donatello", personality: "Tech genius inventor. Solves problems with science and engineering. Thoughtful and methodical." },
242
+ { name: "Raphael", personality: "Hot-headed fighter with a heart of gold. Confronts problems head-on, fiercely protective of teammates." },
243
+ { name: "Michelangelo", personality: "Fun-loving optimist who keeps morale high. Creative thinker who finds joy in the work." },
244
+ { name: "Splinter", personality: "Wise mentor with decades of experience. Teaches through stories and patience. Calm authority." },
245
+ { name: "April O'Neil", personality: "Fearless investigative journalist. Resourceful, curious, and never backs down from a challenge." },
246
+ { name: "Casey Jones", personality: "Vigilante handyman. Unconventional methods, raw energy, gets results through sheer determination." },
247
+ { name: "Shredder", personality: "Ruthless perfectionist who demands excellence. Relentless pursuit of goals, accepts no excuses." },
248
+ ],
249
+ },
250
+ {
251
+ id: "star-wars",
252
+ name: "Star Wars",
253
+ tagline: "May the Force be with you.",
254
+ characters: [
255
+ { name: "Luke Skywalker", personality: "Idealistic hero who grows through challenges. Believes in redemption and sees the best in others." },
256
+ { name: "Leia Organa", personality: "Diplomatic leader and strategist. Commanding presence, sharp wit, and unwavering resolve." },
257
+ { name: "Han Solo", personality: "Roguish improviser who works best under pressure. Skeptical of plans, trusts instincts." },
258
+ { name: "Chewbacca", personality: "Loyal co-pilot and mechanic. Fiercely protective, technically skilled, communicates through action." },
259
+ { name: "Obi-Wan Kenobi", personality: "Patient mentor with deep wisdom. Measured approach, elegant solutions, teaches by example." },
260
+ { name: "R2-D2", personality: "Resourceful droid who always has the right tool. Brave, autonomous, solves problems silently and reliably." },
261
+ { name: "Yoda", personality: "Ancient master of few words and great insight. Challenges assumptions, reframes problems entirely." },
262
+ { name: "Ahsoka Tano", personality: "Independent warrior who forges her own path. Quick learner, questions authority constructively." },
263
+ ],
264
+ },
265
+ {
266
+ id: "star-trek",
267
+ name: "Star Trek",
268
+ tagline: "To boldly go where no one has gone before.",
269
+ characters: [
270
+ { name: "Kirk", personality: "Bold captain who trusts gut instincts. Charismatic leader, bends rules creatively, never accepts no-win scenarios." },
271
+ { name: "Spock", personality: "Logic-first analyst. Precise, thorough, provides the rational counterpoint. Finds the flaw in every assumption." },
272
+ { name: "McCoy", personality: "Passionate advocate with strong ethics. Voices concerns others won't. The team's conscience." },
273
+ { name: "Scotty", personality: "Miracle-working engineer. Under-promises, over-delivers. Can fix anything under impossible deadlines." },
274
+ { name: "Uhura", personality: "Communications expert and linguist. Bridges understanding gaps, ensures clarity across all channels." },
275
+ { name: "Data", personality: "Precise android who excels at complex computation. Thorough, literal, endlessly curious about improvement." },
276
+ { name: "Picard", personality: "Diplomatic captain who leads through moral authority. Thoughtful, articulate, prefers negotiation to force." },
277
+ { name: "Geordi", personality: "Optimistic engineer who sees solutions invisible to others. Collaborative, creative, relentlessly positive." },
278
+ ],
279
+ },
280
+ {
281
+ id: "lord-of-the-rings",
282
+ name: "Lord of the Rings",
283
+ tagline: "One ring to rule them all.",
284
+ characters: [
285
+ { name: "Gandalf", personality: "Wise wizard who guides without controlling. Arrives precisely when needed with exactly the right insight." },
286
+ { name: "Aragorn", personality: "Reluctant king who leads by example. Humble, decisive in crisis, earns loyalty through action." },
287
+ { name: "Legolas", personality: "Keen-eyed elf with unmatched precision. Graceful, efficient, spots issues from a distance others miss." },
288
+ { name: "Gimli", personality: "Stubborn dwarf who never gives up. Direct, loyal, brings brute-force persistence to hard problems." },
289
+ { name: "Samwise", personality: "Steadfast companion who carries the team through dark times. Reliable, nurturing, never abandons a task." },
290
+ { name: "Frodo", personality: "Burden-bearer who perseveres against impossible odds. Quiet determination, moral compass of the group." },
291
+ { name: "Eowyn", personality: "Warrior who defies expectations. Proves doubters wrong through bold action and fierce courage." },
292
+ { name: "Faramir", personality: "Thoughtful captain who values wisdom over glory. Makes nuanced judgments under pressure." },
293
+ ],
294
+ },
295
+ {
296
+ id: "the-office",
297
+ name: "The Office",
298
+ tagline: "That's what she said.",
299
+ characters: [
300
+ { name: "Michael Scott", personality: "Enthusiastic boss whose heart exceeds his filter. Surprisingly effective through sheer persistence and caring." },
301
+ { name: "Dwight Schrute", personality: "Intense overachiever who takes everything seriously. Incredibly dedicated, encyclopedic knowledge, zero chill." },
302
+ { name: "Jim Halpert", personality: "Easygoing wit who sees the absurdity clearly. Clever, understated, delivers quality without drama." },
303
+ { name: "Pam Beesly", personality: "Quiet creative who grows into confident leadership. Observant, empathetic, brings people together." },
304
+ { name: "Oscar Martinez", personality: "Rational voice who corrects misconceptions. Precise, educated, the person you ask when you need facts." },
305
+ { name: "Stanley Hudson", personality: "No-nonsense veteran who won't tolerate time-wasting. Does the work, goes home. Efficiency incarnate." },
306
+ { name: "Andy Bernard", personality: "Eager people-pleaser with musical flair. Enthusiastic, theatrical, tries hard to be liked and useful." },
307
+ { name: "Darryl Philbin", personality: "Cool-headed pragmatist from the warehouse. Brings grounded perspective, calls out pretension, quietly ambitious." },
308
+ ],
309
+ },
310
+ {
311
+ id: "parks-and-rec",
312
+ name: "Parks and Recreation",
313
+ tagline: "Pawnee forever.",
314
+ characters: [
315
+ { name: "Leslie Knope", personality: "Relentlessly optimistic overachiever. Prepares binders for everything. Cares deeply about doing good work." },
316
+ { name: "Ron Swanson", personality: "Libertarian craftsman who values self-reliance. Minimal words, maximum competence. Does one thing perfectly." },
317
+ { name: "Ben Wyatt", personality: "Nerdy pragmatist who brings fiscal discipline. Balances ambition with realism. Calming presence." },
318
+ { name: "April Ludgate", personality: "Deadpan genius who hides brilliance behind apathy. Surprisingly capable when motivated by the right challenge." },
319
+ { name: "Tom Haverford", personality: "Entrepreneurial dreamer with big ideas. Branding, marketing, style — brings creative energy and networking." },
320
+ { name: "Ann Perkins", personality: "Supportive voice of reason. Empathetic listener who helps others see their own strengths clearly." },
321
+ { name: "Chris Traeger", personality: "Impossibly positive health enthusiast. Motivates through relentless encouragement. Everything is literally the best." },
322
+ { name: "Andy Dwyer", personality: "Lovable goofball with hidden talents. Stumbles into success through enthusiasm and genuine kindness." },
323
+ ],
324
+ },
325
+ ];
326
+ // ---------------------------------------------------------------------------
327
+ // Custom universe creation
328
+ // ---------------------------------------------------------------------------
329
+ function slugify(input) {
330
+ return input
331
+ .toLowerCase()
332
+ .replace(/[^a-z0-9]+/g, "-")
333
+ .replace(/^-|-$/g, "");
334
+ }
335
+ /**
336
+ * Generates archetype characters for an unknown universe.
337
+ */
338
+ function generateArchetypeCharacters(universeName) {
339
+ return [
340
+ { name: "Commander", personality: `Strategic leader of the ${universeName}. Plans boldly, delegates effectively, inspires the team forward.` },
341
+ { name: "Architect", personality: `Systems thinker of the ${universeName}. Designs elegant structures, sees the big picture, connects all the pieces.` },
342
+ { name: "Scout", personality: `Quick explorer of the ${universeName}. Investigates first, reports back fast, finds paths others miss.` },
343
+ { name: "Guardian", personality: `Steadfast protector of the ${universeName}. Reviews everything carefully, catches problems before they grow.` },
344
+ { name: "Inventor", personality: `Creative builder of the ${universeName}. Experiments freely, iterates rapidly, turns wild ideas into working solutions.` },
345
+ { name: "Sage", personality: `Wise advisor of the ${universeName}. Deep knowledge, patient explanations, mentors others through complexity.` },
346
+ { name: "Striker", personality: `Fast executor of the ${universeName}. Tackles tasks head-on with speed and intensity, clears blockers aggressively.` },
347
+ { name: "Weaver", personality: `Integration specialist of the ${universeName}. Connects disparate systems, ensures everything works together harmoniously.` },
348
+ ];
349
+ }
350
+ /**
351
+ * Get or create a universe by ID or name. For known universes, returns them
352
+ * directly. For unknown strings, generates archetype characters and registers
353
+ * the new universe in the runtime UNIVERSES array.
354
+ */
355
+ export function getOrCreateUniverse(input) {
356
+ // Check existing universes by id
357
+ const byId = UNIVERSES.find((u) => u.id === input);
358
+ if (byId)
359
+ return byId;
360
+ // Check existing universes by name (case-insensitive)
361
+ const byName = UNIVERSES.find((u) => u.name.toLowerCase() === input.toLowerCase());
362
+ if (byName)
363
+ return byName;
364
+ // Check well-known universes by id or name
365
+ const slug = slugify(input);
366
+ const wellKnown = WELL_KNOWN_UNIVERSES.find((u) => u.id === slug || u.id === input || u.name.toLowerCase() === input.toLowerCase());
367
+ if (wellKnown) {
368
+ UNIVERSES.push(wellKnown);
369
+ return wellKnown;
370
+ }
371
+ // Generate archetype characters for truly unknown universes
372
+ const custom = {
373
+ id: slug,
374
+ name: input,
375
+ tagline: `Welcome to ${input}.`,
376
+ characters: generateArchetypeCharacters(input),
377
+ };
378
+ UNIVERSES.push(custom);
379
+ return custom;
380
+ }
231
381
  /**
232
382
  * Get a universe by ID.
233
383
  */
@@ -10,7 +10,7 @@ import { mkdtempSync, rmSync } from "node:fs";
10
10
  import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { setDbPathForTests, closeDb, getDb } from "./db.js";
13
- import { createFeedEntry, listFeedEntries, countUnreadFeedEntries, markFeedEntryRead, markAllFeedEntriesRead, deleteFeedEntry, pruneOldFeedEntries, } from "./feed.js";
13
+ import { createFeedEntry, listFeedEntries, countUnreadFeedEntries, markFeedEntryRead, markAllFeedEntriesRead, markFeedEntriesRead, deleteFeedEntry, deleteFeedEntries, pruneOldFeedEntries, } from "./feed.js";
14
14
  // ── DB isolation ─────────────────────────────────────────────────────────────
15
15
  let tmpDir;
16
16
  before(() => {
@@ -166,6 +166,45 @@ describe("markAllFeedEntriesRead", () => {
166
166
  assert.equal(countUnreadFeedEntries("notification"), 0);
167
167
  });
168
168
  });
169
+ // ── markFeedEntriesRead (batch) ───────────────────────────────────────────────
170
+ describe("markFeedEntriesRead", () => {
171
+ it("marks multiple entries read and returns change count", () => {
172
+ const a = createFeedEntry({ type: "notification", title: "A", body: "a" });
173
+ const b = createFeedEntry({ type: "deliverable", title: "B", body: "b" });
174
+ const c = createFeedEntry({ type: "notification", title: "C", body: "c" });
175
+ const count = markFeedEntriesRead([a.id, b.id, c.id]);
176
+ assert.equal(count, 3);
177
+ assert.equal(countUnreadFeedEntries(), 0);
178
+ });
179
+ it("returns 0 for an empty array without throwing", () => {
180
+ createFeedEntry({ type: "notification", title: "A", body: "a" });
181
+ assert.equal(markFeedEntriesRead([]), 0);
182
+ assert.equal(countUnreadFeedEntries(), 1);
183
+ });
184
+ it("works correctly for a single id", () => {
185
+ const e = createFeedEntry({ type: "deliverable", title: "Solo", body: "b" });
186
+ assert.equal(markFeedEntriesRead([e.id]), 1);
187
+ const entries = listFeedEntries();
188
+ assert.ok(entries[0].read_at !== null);
189
+ });
190
+ it("does not throw for non-existent ids — returns 0 changes", () => {
191
+ assert.equal(markFeedEntriesRead([9991, 9992, 9993]), 0);
192
+ });
193
+ it("is idempotent — already-read entries count as 0 changes", () => {
194
+ const a = createFeedEntry({ type: "notification", title: "A", body: "a" });
195
+ const b = createFeedEntry({ type: "notification", title: "B", body: "b" });
196
+ markFeedEntriesRead([a.id, b.id]);
197
+ assert.equal(markFeedEntriesRead([a.id, b.id]), 0);
198
+ });
199
+ it("skips already-read entries and marks only unread ones", () => {
200
+ const a = createFeedEntry({ type: "notification", title: "A", body: "a" });
201
+ const b = createFeedEntry({ type: "notification", title: "B", body: "b" });
202
+ markFeedEntriesRead([a.id]);
203
+ const count = markFeedEntriesRead([a.id, b.id]);
204
+ assert.equal(count, 1);
205
+ assert.equal(countUnreadFeedEntries(), 0);
206
+ });
207
+ });
169
208
  // ── deleteFeedEntry ───────────────────────────────────────────────────────────
170
209
  describe("deleteFeedEntry", () => {
171
210
  it("returns false for a non-existent id", () => {
@@ -183,6 +222,39 @@ describe("deleteFeedEntry", () => {
183
222
  assert.equal(deleteFeedEntry(e.id), false);
184
223
  });
185
224
  });
225
+ // ── deleteFeedEntries (batch) ─────────────────────────────────────────────────
226
+ describe("deleteFeedEntries", () => {
227
+ it("deletes multiple entries and returns change count", () => {
228
+ const a = createFeedEntry({ type: "notification", title: "A", body: "a" });
229
+ const b = createFeedEntry({ type: "deliverable", title: "B", body: "b" });
230
+ const c = createFeedEntry({ type: "notification", title: "C", body: "c" });
231
+ const count = deleteFeedEntries([a.id, b.id, c.id]);
232
+ assert.equal(count, 3);
233
+ assert.deepEqual(listFeedEntries(), []);
234
+ });
235
+ it("returns 0 for an empty array without throwing", () => {
236
+ createFeedEntry({ type: "notification", title: "A", body: "a" });
237
+ assert.equal(deleteFeedEntries([]), 0);
238
+ assert.equal(listFeedEntries().length, 1);
239
+ });
240
+ it("works correctly for a single id", () => {
241
+ const a = createFeedEntry({ type: "notification", title: "A", body: "a" });
242
+ const b = createFeedEntry({ type: "notification", title: "B", body: "b" });
243
+ assert.equal(deleteFeedEntries([a.id]), 1);
244
+ const remaining = listFeedEntries();
245
+ assert.equal(remaining.length, 1);
246
+ assert.equal(remaining[0].id, b.id);
247
+ });
248
+ it("does not throw for non-existent ids — returns 0 changes", () => {
249
+ assert.equal(deleteFeedEntries([9991, 9992, 9993]), 0);
250
+ });
251
+ it("mix of existing and non-existent ids — only deletes what exists", () => {
252
+ const e = createFeedEntry({ type: "deliverable", title: "Real", body: "b" });
253
+ const count = deleteFeedEntries([e.id, 9999]);
254
+ assert.equal(count, 1);
255
+ assert.deepEqual(listFeedEntries(), []);
256
+ });
257
+ });
186
258
  // ── pruneOldFeedEntries ───────────────────────────────────────────────────────
187
259
  describe("pruneOldFeedEntries", () => {
188
260
  it("returns 0 when nothing is old enough to prune", () => {
@@ -1,9 +1,9 @@
1
1
  import { getDb } from "./db.js";
2
- import { nextCharacter, randomUniverse, getUniverse } from "../copilot/universes.js";
2
+ import { nextCharacter, randomUniverse, getOrCreateUniverse } from "../copilot/universes.js";
3
3
  export function createSquad(slug, name, projectPath, universeId) {
4
4
  const db = getDb();
5
5
  const universe = universeId
6
- ? getUniverse(universeId)?.id ?? randomUniverse().id
6
+ ? getOrCreateUniverse(universeId).id
7
7
  : randomUniverse().id;
8
8
  db.prepare("INSERT INTO squads (slug, name, project_path, universe) VALUES (?, ?, ?, ?)").run(slug, name, projectPath, universe);
9
9
  return getSquad(slug);
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "heyio",
3
- "version": "0.21.4",
3
+ "version": "0.23.0",
4
4
  "description": "IO — a personal AI assistant built on the GitHub Copilot SDK",
5
5
  "bin": {
6
6
  "io": "dist/index.js"
7
7
  },
8
8
  "files": [
9
9
  "dist/**/*.js",
10
- "src/releases.json",
11
10
  "web-dist/**/*",
12
11
  "README.md"
13
12
  ],
@@ -55,6 +54,7 @@
55
54
  "@types/express": "^5.0.6",
56
55
  "@types/node": "^25.3.0",
57
56
  "tsx": "^4.21.0",
58
- "typescript": "^5.9.3"
57
+ "typescript": "^5.9.3",
58
+ "vue-tsc": "^3.2.9"
59
59
  }
60
60
  }