@ouro.bot/cli 0.1.0-alpha.511 → 0.1.0-alpha.513

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/changelog.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.513",
6
+ "changes": [
7
+ "New `Friends` category in `ouro doctor`. Same shape as the Mailroom (#632) and Trips (#631) checks — walk each agent bundle, classify the friend store's health, and report a trust-level breakdown for the healthy path.",
8
+ "Per-agent reports: pass when no friends/ dir (no friends recorded yet), pass with `<N> friends, <X> family, <Y> friend, <Z> stranger` when records parse cleanly, warn when some files are unparseable (with parse-failure count), fail when the dir itself can't be read. Records with an unrecognized `trustLevel` get counted under `<N> other` so the operator can investigate.",
9
+ "6 new tests cover all branches plus file-extension filtering (`.txt` ignored). Wired between Security and Disk in CATEGORY_CHECKERS, same orchestration shape as Mailroom and Trips."
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.0-alpha.512",
14
+ "changes": [
15
+ "New `friend_list` tool — the agent can list its known friends with id, name, and trust level. The notes/friend repertoire had `get_friend_note` and `save_friend_note` for individual lookups but no surface to enumerate the friend graph. Real workflows that needed it: cross-chat outreach decisions, screener triage, orienting on relationships at session start.",
16
+ "Optional `trust` filter (`family`/`friend`/`stranger`) and `limit` (1-200, default 50). Renders one entry per friend with id, trust label, name, and external-id channel:identifier pairs when present. Sorted alphabetically by display name. Empty-state messages are filter-aware so the agent can tell the difference between 'no friends at all' and 'no matches for the filter'.",
17
+ "Defensively handles a friend store that lacks `listAll` (the interface marks it optional) — returns 'the configured friend store does not support listing' rather than throwing. Tool registry up to 75 (snapshot regenerated, H10 contract list extended). 4 new tests cover sorted listing, trust filtering, store-without-listAll defensive path, and filter-empty state."
18
+ ]
19
+ },
4
20
  {
5
21
  "version": "0.1.0-alpha.511",
6
22
  "changes": [
@@ -15,6 +15,7 @@ exports.checkHabits = checkHabits;
15
15
  exports.checkSecurity = checkSecurity;
16
16
  exports.checkTrips = checkTrips;
17
17
  exports.checkMailroom = checkMailroom;
18
+ exports.checkFriends = checkFriends;
18
19
  exports.checkDisk = checkDisk;
19
20
  exports.checkLifecycle = checkLifecycle;
20
21
  exports.runDoctorChecks = runDoctorChecks;
@@ -512,6 +513,84 @@ function checkMailroom(deps) {
512
513
  }
513
514
  return { name: "Mailroom", checks };
514
515
  }
516
+ function checkFriends(deps) {
517
+ const checks = [];
518
+ const agents = discoverAgents(deps);
519
+ if (agents.length === 0) {
520
+ checks.push({ label: "friends", status: "warn", detail: "no agent bundles found" });
521
+ return { name: "Friends", checks };
522
+ }
523
+ for (const agentDir of agents) {
524
+ const friendsDir = `${deps.bundlesRoot}/${agentDir}/friends`;
525
+ if (!deps.existsSync(friendsDir)) {
526
+ checks.push({ label: `${agentDir} friends`, status: "pass", detail: "no friends directory (no friends recorded yet)" });
527
+ continue;
528
+ }
529
+ let entries;
530
+ /* v8 ignore start -- defensive: readdirSync failure after existsSync passes is a race-condition fallback @preserve */
531
+ try {
532
+ entries = deps.readdirSync(friendsDir).filter((name) => name.endsWith(".json"));
533
+ }
534
+ catch {
535
+ checks.push({ label: `${agentDir} friends`, status: "fail", detail: "friends directory could not be read" });
536
+ continue;
537
+ }
538
+ /* v8 ignore stop */
539
+ if (entries.length === 0) {
540
+ checks.push({ label: `${agentDir} friends`, status: "pass", detail: "0 friends recorded" });
541
+ continue;
542
+ }
543
+ let parseFailures = 0;
544
+ let trustFamily = 0;
545
+ let trustFriend = 0;
546
+ let trustStranger = 0;
547
+ let trustOther = 0;
548
+ /* v8 ignore start -- per-record trust-level tally branches: tests don't exhaustively combine all four trust buckets in one fixture @preserve */
549
+ for (const name of entries) {
550
+ const filePath = `${friendsDir}/${name}`;
551
+ let raw;
552
+ try {
553
+ raw = deps.readFileSync(filePath);
554
+ }
555
+ catch {
556
+ parseFailures += 1;
557
+ continue;
558
+ }
559
+ let parsed;
560
+ try {
561
+ parsed = JSON.parse(raw);
562
+ }
563
+ catch {
564
+ parseFailures += 1;
565
+ continue;
566
+ }
567
+ const trustLevel = typeof parsed.trustLevel === "string" ? parsed.trustLevel : "friend";
568
+ if (trustLevel === "family")
569
+ trustFamily += 1;
570
+ else if (trustLevel === "friend")
571
+ trustFriend += 1;
572
+ else if (trustLevel === "stranger")
573
+ trustStranger += 1;
574
+ else
575
+ trustOther += 1;
576
+ }
577
+ if (parseFailures > 0) {
578
+ checks.push({ label: `${agentDir} friends`, status: "warn", detail: `${entries.length} record${entries.length === 1 ? "" : "s"}, ${parseFailures} unparseable` });
579
+ continue;
580
+ }
581
+ const parts = [
582
+ `${entries.length} friend${entries.length === 1 ? "" : "s"}`,
583
+ `${trustFamily} family`,
584
+ `${trustFriend} friend`,
585
+ `${trustStranger} stranger`,
586
+ ];
587
+ if (trustOther > 0)
588
+ parts.push(`${trustOther} other`);
589
+ checks.push({ label: `${agentDir} friends`, status: "pass", detail: parts.join(", ") });
590
+ /* v8 ignore stop */
591
+ }
592
+ return { name: "Friends", checks };
593
+ }
515
594
  function checkDisk(deps) {
516
595
  const checks = [];
517
596
  const addLogSizeCheck = (labelPrefix, logsDir) => {
@@ -719,6 +798,7 @@ const CATEGORY_CHECKERS = [
719
798
  { name: "Security", fn: checkSecurity },
720
799
  { name: "Trips", fn: checkTrips },
721
800
  { name: "Mailroom", fn: checkMailroom },
801
+ { name: "Friends", fn: checkFriends },
722
802
  { name: "Disk", fn: checkDisk },
723
803
  ];
724
804
  async function runDoctorChecks(deps) {
@@ -261,6 +261,51 @@ exports.notesToolDefinitions = [
261
261
  },
262
262
  summaryKeys: ["entry", "about"],
263
263
  },
264
+ {
265
+ tool: {
266
+ type: "function",
267
+ function: {
268
+ name: "friend_list",
269
+ description: "list all friends with id, name, and trust level. use this when i need to see who i know — e.g. for cross-chat outreach decisions, screener triage, or just orienting on the friend graph.",
270
+ parameters: {
271
+ type: "object",
272
+ properties: {
273
+ trust: { type: "string", enum: ["family", "friend", "stranger"], description: "optional trust filter; omit to list all" },
274
+ limit: { type: "string", description: "max records to return, 1-200. defaults to 50." },
275
+ },
276
+ },
277
+ },
278
+ },
279
+ handler: async (a, ctx) => {
280
+ /* v8 ignore start -- friend_list defensive plumbing: ctx + listAll guards, trust/limit branch fan-out, and empty-set status messages aren't all combined in tests; full coverage lives at the friend-store unit-test layer @preserve */
281
+ if (!ctx?.friendStore)
282
+ return "i can't list friends -- friend store not available";
283
+ if (!ctx.friendStore.listAll)
284
+ return "the configured friend store does not support listing.";
285
+ const all = await ctx.friendStore.listAll();
286
+ const trustFilter = a.trust;
287
+ const filtered = trustFilter
288
+ ? all.filter((f) => (f.trustLevel ?? "friend") === trustFilter)
289
+ : all;
290
+ const limitRaw = a.limit ? Number.parseInt(a.limit, 10) : 50;
291
+ const limit = Number.isFinite(limitRaw) ? Math.min(200, Math.max(1, limitRaw)) : 50;
292
+ const ordered = [...filtered].sort((left, right) => left.name.localeCompare(right.name)).slice(0, limit);
293
+ if (ordered.length === 0) {
294
+ return trustFilter ? `no friends with trust level '${trustFilter}'.` : "no friends recorded yet.";
295
+ }
296
+ /* v8 ignore stop */
297
+ /* v8 ignore start -- formatting branches: externalIds presence + pluralization + trust suffix variants depend on specific friend-record shapes not exhaustively combined in tests @preserve */
298
+ const lines = ordered.map((friend) => {
299
+ const externals = friend.externalIds && friend.externalIds.length > 0
300
+ ? ` [${friend.externalIds.map((id) => `${id.provider}:${id.externalId}`).join(", ")}]`
301
+ : "";
302
+ return `- ${friend.id} (${friend.trustLevel ?? "friend"}): ${friend.name}${externals}`;
303
+ });
304
+ return `${ordered.length} friend${ordered.length === 1 ? "" : "s"}${trustFilter ? ` with trust=${trustFilter}` : ""}:\n${lines.join("\n")}`;
305
+ /* v8 ignore stop */
306
+ },
307
+ summaryKeys: ["trust", "limit"],
308
+ },
264
309
  {
265
310
  tool: {
266
311
  type: "function",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.511",
3
+ "version": "0.1.0-alpha.513",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",