From cfbb339f369a9fd786a3cd146db196bd2bfd5d8d Mon Sep 17 00:00:00 2001 From: Andrew Chan Date: Sat, 22 Aug 2026 00:30:08 -0700 Subject: [PATCH 1/7] db: add a stats_json column to plugin_marketplaces Install counts arrive as a document published beside a marketplace's manifest, and they must survive a restart and an offline start the same way the last-known-good catalog does. Give the marketplace row somewhere to keep one. The column is nullable and, on the upsert input, required rather than defaulted: a refresh that did not re-read the sidecar keeps its counts by passing the value it already had, and making that explicit is what forces every writer to say which it means. All three writers say so here and nothing yet fetches a sidecar, so this commit changes no behavior. The migrate test drops the column before replaying a rewind, matching how every other ALTER TABLE ADD in that suite is handled: the ADD is not re-appliable against a table that already has the column. > AGENT GENERATED --- .../plugin-catalog/plugin-catalog-service.ts | 7 +++++++ .../marketplace-publishers.test.ts | 1 + .../test/services/plugins/plugin-update.test.ts | 1 + .../drizzle/0107_marketplace_install_stats.sql | 1 + packages/db/drizzle/meta/_journal.json | 7 +++++++ packages/db/src/data/plugin-marketplaces.ts | 8 ++++++++ packages/db/src/schema.ts | 7 +++++++ packages/db/test/migrate.test.ts | 17 +++++++++++++++++ 8 files changed, 49 insertions(+) create mode 100644 packages/db/drizzle/0107_marketplace_install_stats.sql diff --git a/apps/server/src/services/plugin-catalog/plugin-catalog-service.ts b/apps/server/src/services/plugin-catalog/plugin-catalog-service.ts index 5e4e594e20..c0da9f16e3 100644 --- a/apps/server/src/services/plugin-catalog/plugin-catalog-service.ts +++ b/apps/server/src/services/plugin-catalog/plugin-catalog-service.ts @@ -276,6 +276,8 @@ export function createPluginCatalogService(deps: { sourceGitRef: null, sourceGitCommit: null, manifestJson: JSON.stringify(BUNDLED_CURATED_MARKETPLACE), + // Counts are keyed by entry id, so they survive a manifest fallback. + statsJson: existing?.statsJson ?? null, etag: null, lastModified: null, lastSuccessfulRefreshAt: null, @@ -597,6 +599,8 @@ export function createPluginCatalogService(deps: { ...marketplaceSourceColumns(source), sourceGitCommit: materialized.commit, manifestJson, + // Carried through untouched; nothing writes counts yet. + statsJson: row.statsJson, etag: materialized.etag, lastModified: materialized.lastModified, lastSuccessfulRefreshAt: attemptedAt, @@ -1064,6 +1068,9 @@ export function createPluginCatalogService(deps: { ...marketplaceSourceColumns(source), sourceGitCommit: materialized.commit, manifestJson: materialized.manifestJson, + // Only the curated marketplace publishes install counts, and it + // can never be added here — its name is reserved above. + statsJson: null, etag: materialized.etag, lastModified: materialized.lastModified, lastSuccessfulRefreshAt: addedAt, diff --git a/apps/server/test/services/plugin-catalog/marketplace-publishers.test.ts b/apps/server/test/services/plugin-catalog/marketplace-publishers.test.ts index 4122ec2a10..576798f562 100644 --- a/apps/server/test/services/plugin-catalog/marketplace-publishers.test.ts +++ b/apps/server/test/services/plugin-catalog/marketplace-publishers.test.ts @@ -25,6 +25,7 @@ function register( sourceGitRef: null, sourceGitCommit: null, manifestJson, + statsJson: null, etag: null, lastModified: null, lastSuccessfulRefreshAt: null, diff --git a/apps/server/test/services/plugins/plugin-update.test.ts b/apps/server/test/services/plugins/plugin-update.test.ts index d627bcf842..454bf78565 100644 --- a/apps/server/test/services/plugins/plugin-update.test.ts +++ b/apps/server/test/services/plugins/plugin-update.test.ts @@ -379,6 +379,7 @@ describe("plugin update service and routes", () => { manifestUrl: workDir, sourceGitRef: null, sourceGitCommit: null, + statsJson: null, manifestJson: JSON.stringify({ schemaVersion: 1, name: "acme-plugins", diff --git a/packages/db/drizzle/0107_marketplace_install_stats.sql b/packages/db/drizzle/0107_marketplace_install_stats.sql new file mode 100644 index 0000000000..3472b711ea --- /dev/null +++ b/packages/db/drizzle/0107_marketplace_install_stats.sql @@ -0,0 +1 @@ +ALTER TABLE `plugin_marketplaces` ADD `stats_json` text; \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index e2b047dd29..4ef118f4d4 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -750,6 +750,13 @@ "when": 1787305850786, "tag": "0106_thread_state_index", "breakpoints": true + }, + { + "idx": 107, + "version": "6", + "when": 1787378771102, + "tag": "0107_marketplace_install_stats", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/data/plugin-marketplaces.ts b/packages/db/src/data/plugin-marketplaces.ts index b289f21908..71b190374c 100644 --- a/packages/db/src/data/plugin-marketplaces.ts +++ b/packages/db/src/data/plugin-marketplaces.ts @@ -12,6 +12,8 @@ export interface PluginMarketplaceRow { sourceGitRef: string | null; sourceGitCommit: string | null; manifestJson: string; + /** Last-known-good `stats.json` document, verbatim; null when there is none. */ + statsJson: string | null; etag: string | null; lastModified: string | null; lastSuccessfulRefreshAt: number | null; @@ -28,6 +30,12 @@ export interface UpsertPluginMarketplaceInput { sourceGitRef: string | null; sourceGitCommit: string | null; manifestJson: string; + /** + * Last-known-good `stats.json`, verbatim. Every caller states it: passing + * the row's current value is how a refresh that did not re-read the sidecar + * keeps the counts it already had. + */ + statsJson: string | null; etag: string | null; lastModified: string | null; lastSuccessfulRefreshAt: number | null; diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index a59e78f51a..6d192ede62 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -340,6 +340,13 @@ export const pluginMarketplaces = sqliteTable("plugin_marketplaces", { /** Commit the last successful "git" refresh read the manifest from. */ sourceGitCommit: text("source_git_commit"), manifestJson: text("manifest_json").notNull(), + /** + * Last-known-good install-count sidecar (`stats.json`) of the curated + * marketplace, verbatim; null when it was never fetched or never parsed. + * It refreshes on its own cadence: the counts move while the manifest sits + * unchanged behind a 304, so it cannot live inside `manifest_json`. + */ + statsJson: text("stats_json"), etag: text("etag"), lastModified: text("last_modified"), lastSuccessfulRefreshAt: integer("last_successful_refresh_at"), diff --git a/packages/db/test/migrate.test.ts b/packages/db/test/migrate.test.ts index 06472b80a8..25491f8543 100644 --- a/packages/db/test/migrate.test.ts +++ b/packages/db/test/migrate.test.ts @@ -712,6 +712,22 @@ function dropEventParentToolCallIdColumn(db: DbConnection): void { } } +/** + * Migration 0107 adds the marketplace install-count sidecar column. A rewind + * that clears journal rows from before it must drop the column, or migrate() + * replays the ADD against a table that already has it. + */ +function dropMarketplaceStatsColumn(db: DbConnection): void { + const columns = db.$client + .prepare<[], TableInfoRow>("PRAGMA table_info(plugin_marketplaces)") + .all(); + if (columns.some((column) => column.name === "stats_json")) { + db.$client + .prepare("ALTER TABLE plugin_marketplaces DROP COLUMN stats_json") + .run(); + } +} + function dropEnvironmentNameColumn(db: DbConnection): void { db.$client.prepare("ALTER TABLE environments DROP COLUMN name").run(); } @@ -5110,6 +5126,7 @@ describe("migrate", () => { }); dropEventParentToolCallIdColumn(db); + dropMarketplaceStatsColumn(db); db.$client .prepare( "DELETE FROM __drizzle_migrations WHERE created_at >= ?", From af384c9822e4f7a0b7a2ce8dbe09fb129debfafd Mon Sep 17 00:00:00 2001 From: Andrew Chan Date: Sat, 22 Aug 2026 00:30:15 -0700 Subject: [PATCH 2/7] db: record the drizzle snapshot for 0107 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated by drizzle-kit alongside the migration in the previous commit, and split out because it is 3,739 lines of machine-written JSON that would otherwise bury the whole change. It has to be committed even though nothing reads it at runtime. drizzle-kit generates each migration by diffing schema.ts against the newest snapshot in meta/. Without this file the next person to run db:generate diffs against 0106, does not see stats_json, and re-emits the same ALTER TABLE ADD inside their own migration — which then fails on every database that already applied 0107. All 109 snapshots are tracked for this reason. Reviewers can skip this commit. It is regenerated, never hand-edited. > AGENT GENERATED --- packages/db/drizzle/meta/0107_snapshot.json | 3740 +++++++++++++++++++ 1 file changed, 3740 insertions(+) create mode 100644 packages/db/drizzle/meta/0107_snapshot.json diff --git a/packages/db/drizzle/meta/0107_snapshot.json b/packages/db/drizzle/meta/0107_snapshot.json new file mode 100644 index 0000000000..32731e0b52 --- /dev/null +++ b/packages/db/drizzle/meta/0107_snapshot.json @@ -0,0 +1,3740 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "fa7fcb36-40f9-401d-a677-43e1dd51a539", + "prevId": "382a9d2b-7e1a-4b0c-97df-7b6321009b60", + "tables": { + "app_settings": { + "name": "app_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "caffeinate": { + "name": "caffeinate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_keyboard_hints": { + "name": "show_keyboard_hints", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "steer_active_thread_on_enter": { + "name": "steer_active_thread_on_enter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_unhandled_provider_events": { + "name": "show_unhandled_provider_events", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "codex_memory_enabled": { + "name": "codex_memory_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "claude_code_memory_enabled": { + "name": "claude_code_memory_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "codex_subagents_disabled": { + "name": "codex_subagents_disabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "claude_code_subagents_disabled": { + "name": "claude_code_subagents_disabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "claude_code_workflows_disabled": { + "name": "claude_code_workflows_disabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "keybinding_overrides": { + "name": "keybinding_overrides", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "onboarding_completed_at": { + "name": "onboarding_completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_settings_values": { + "name": "app_settings_values", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_theme": { + "name": "app_theme", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "theme_id": { + "name": "theme_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "favicon_color": { + "name": "favicon_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apikey": { + "name": "apikey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "referenceId": { + "name": "referenceId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refillInterval": { + "name": "refillInterval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refillAmount": { + "name": "refillAmount", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastRefillAt": { + "name": "lastRefillAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rateLimitEnabled": { + "name": "rateLimitEnabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rateLimitTimeWindow": { + "name": "rateLimitTimeWindow", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rateLimitMax": { + "name": "rateLimitMax", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "requestCount": { + "name": "requestCount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastRequest": { + "name": "lastRequest", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "configId": { + "name": "configId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "apikey_key_unique": { + "name": "apikey_key_unique", + "columns": [ + "key" + ], + "isUnique": true + }, + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + "referenceId" + ], + "isUnique": false + }, + "apikey_config_id_idx": { + "name": "apikey_config_id_idx", + "columns": [ + "configId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "apikey_referenceId_user_id_fk": { + "name": "apikey_referenceId_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "referenceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environments": { + "name": "environments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "managed": { + "name": "managed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_git_repo": { + "name": "is_git_repo", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_worktree": { + "name": "is_worktree", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "merge_base_branch": { + "name": "merge_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "destroy_attempt_id": { + "name": "destroy_attempt_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retire_requested_at": { + "name": "retire_requested_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workspace_provision_type": { + "name": "workspace_provision_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'provisioning'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environments_project_host_path_idx": { + "name": "environments_project_host_path_idx", + "columns": [ + "project_id", + "host_id", + "path" + ], + "isUnique": true + }, + "environments_host_path_lookup_idx": { + "name": "environments_host_path_lookup_idx", + "columns": [ + "host_id", + "path" + ], + "isUnique": false + }, + "environments_project_idx": { + "name": "environments_project_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "environments_status_idx": { + "name": "environments_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": { + "environments_project_id_projects_id_fk": { + "name": "environments_project_id_projects_id_fk", + "tableFrom": "environments", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environments_host_id_hosts_id_fk": { + "name": "environments_host_id_hosts_id_fk", + "tableFrom": "environments", + "tableTo": "hosts", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "events": { + "name": "events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_thread_id": { + "name": "provider_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "item_kind": { + "name": "item_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_tool_call_id": { + "name": "parent_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "generated": { + "as": "(CASE WHEN json_valid(data) THEN json_extract(data, '$.item.tool') END)", + "type": "virtual" + } + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "events_thread_sequence_idx": { + "name": "events_thread_sequence_idx", + "columns": [ + "thread_id", + "sequence" + ], + "isUnique": true + }, + "events_tool_call_parent_lookup_idx": { + "name": "events_tool_call_parent_lookup_idx", + "columns": [ + "thread_id", + "item_id", + "sequence" + ], + "isUnique": false, + "where": "\"events\".\"item_kind\" = 'toolCall'" + }, + "events_todo_tool_call_thread_tool_sequence_idx": { + "name": "events_todo_tool_call_thread_tool_sequence_idx", + "columns": [ + "thread_id", + "tool_name", + "sequence" + ], + "isUnique": false, + "where": "\"events\".\"item_kind\" = 'toolCall' AND \"events\".\"type\" IN ('item/started', 'item/completed')" + }, + "events_parent_tool_call_thread_parent_sequence_idx": { + "name": "events_parent_tool_call_thread_parent_sequence_idx", + "columns": [ + "thread_id", + "parent_tool_call_id", + "sequence" + ], + "isUnique": false, + "where": "\"events\".\"parent_tool_call_id\" IS NOT NULL" + }, + "events_thread_type_item_kind_sequence_idx": { + "name": "events_thread_type_item_kind_sequence_idx", + "columns": [ + "thread_id", + "type", + "item_kind", + "sequence" + ], + "isUnique": false + }, + "events_background_task_thread_type_item_sequence_idx": { + "name": "events_background_task_thread_type_item_sequence_idx", + "columns": [ + "thread_id", + "type", + "item_id", + "sequence" + ], + "isUnique": false, + "where": "\"events\".\"item_kind\" = 'backgroundTask'" + }, + "events_thread_type_sequence_idx": { + "name": "events_thread_type_sequence_idx", + "columns": [ + "thread_id", + "type", + "sequence" + ], + "isUnique": false + }, + "events_thread_turn_type_item_sequence_idx": { + "name": "events_thread_turn_type_item_sequence_idx", + "columns": [ + "thread_id", + "turn_id", + "type", + "item_id", + "sequence" + ], + "isUnique": false + }, + "events_item_lifecycle_thread_item_sequence_idx": { + "name": "events_item_lifecycle_thread_item_sequence_idx", + "columns": [ + "thread_id", + "item_id", + "sequence" + ], + "isUnique": false, + "where": "\"events\".\"type\" IN ('item/started', 'item/completed', 'item/backgroundTask/completed')" + }, + "events_environment_idx": { + "name": "events_environment_idx", + "columns": [ + "environment_id" + ], + "isUnique": false + }, + "events_completed_item_truncation_idx": { + "name": "events_completed_item_truncation_idx", + "columns": [ + "item_kind", + "created_at", + "id" + ], + "isUnique": false, + "where": "\"events\".\"type\" = 'item/completed'" + }, + "events_thread_state_thread_sequence_idx": { + "name": "events_thread_state_thread_sequence_idx", + "columns": [ + "thread_id", + "sequence" + ], + "isUnique": false, + "where": "\"events\".\"type\" IN ('thread/goal/updated', 'thread/goal/cleared', 'thread/extensionState/updated')" + } + }, + "foreignKeys": { + "events_thread_id_threads_id_fk": { + "name": "events_thread_id_threads_id_fk", + "tableFrom": "events", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "events_environment_id_environments_id_fk": { + "name": "events_environment_id_environments_id_fk", + "tableFrom": "events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "events_scope_shape_check": { + "name": "events_scope_shape_check", + "value": "(\n (\"events\".\"scope_kind\" = 'turn' AND \"events\".\"turn_id\" IS NOT NULL)\n OR\n (\"events\".\"scope_kind\" = 'thread' AND \"events\".\"turn_id\" IS NULL)\n )" + } + } + }, + "host_daemon_sessions": { + "name": "host_daemon_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_type": { + "name": "host_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data_dir": { + "name": "data_dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heartbeat_interval_ms": { + "name": "heartbeat_interval_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lease_timeout_ms": { + "name": "lease_timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "closed_at": { + "name": "closed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "host_daemon_sessions_host_status_idx": { + "name": "host_daemon_sessions_host_status_idx", + "columns": [ + "host_id", + "status" + ], + "isUnique": false + }, + "host_daemon_sessions_host_latest_idx": { + "name": "host_daemon_sessions_host_latest_idx", + "columns": [ + "host_id", + "updated_at", + "created_at", + "id" + ], + "isUnique": false + }, + "host_daemon_sessions_closed_prune_idx": { + "name": "host_daemon_sessions_closed_prune_idx", + "columns": [ + "status", + "closed_at", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_daemon_sessions_host_id_hosts_id_fk": { + "name": "host_daemon_sessions_host_id_hosts_id_fk", + "tableFrom": "host_daemon_sessions", + "tableTo": "hosts", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "hosts": { + "name": "hosts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connect_machine_id": { + "name": "connect_machine_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_permission_mode": { + "name": "max_permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'full'" + }, + "destroyed_at": { + "name": "destroyed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_rejected_protocol_version": { + "name": "last_rejected_protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "hosts_last_seen_idx": { + "name": "hosts_last_seen_idx", + "columns": [ + "last_seen_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provenance": { + "name": "provenance", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'direct'" + }, + "catalog_entry_id": { + "name": "catalog_entry_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "catalog_marketplace_name": { + "name": "catalog_marketplace_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'path'" + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_builtin_name": { + "name": "source_builtin_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_npm_package": { + "name": "source_npm_package", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_npm_registry": { + "name": "source_npm_registry", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_npm_requested_spec": { + "name": "source_npm_requested_spec", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_npm_spec_kind": { + "name": "source_npm_spec_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_url": { + "name": "source_git_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_subdirectory": { + "name": "source_git_subdirectory", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_requested_ref": { + "name": "source_git_requested_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_ref_kind": { + "name": "source_git_ref_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_range": { + "name": "source_git_range", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_tag_prefix": { + "name": "source_git_tag_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_resolved_tag": { + "name": "source_git_resolved_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "npm_resolved_version": { + "name": "npm_resolved_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "npm_integrity": { + "name": "npm_integrity", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_resolved_commit": { + "name": "git_resolved_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_update_check_at": { + "name": "last_update_check_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "available_compatible_version": { + "name": "available_compatible_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "newest_incompatible_version": { + "name": "newest_incompatible_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "update_status_detail": { + "name": "update_status_detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_failure_version": { + "name": "last_failure_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_failure_at": { + "name": "last_failure_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_failure_detail": { + "name": "last_failure_detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_artifact_id": { + "name": "active_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "normalization_version": { + "name": "normalization_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "root_dir": { + "name": "root_dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "removed_at": { + "name": "removed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "plugins_active_artifact_id_plugin_artifacts_id_fk": { + "name": "plugins_active_artifact_id_plugin_artifacts_id_fk", + "tableFrom": "plugins", + "tableTo": "plugin_artifacts", + "columnsFrom": [ + "active_artifact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "maintenance_scan_cursors": { + "name": "maintenance_scan_cursors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_kind": { + "name": "item_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_path": { + "name": "output_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_created_at": { + "name": "last_created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_event_id": { + "name": "last_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "maintenance_scan_cursors_path_idx": { + "name": "maintenance_scan_cursors_path_idx", + "columns": [ + "policy", + "version", + "item_kind", + "output_path" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "pending_interactions": { + "name": "pending_interactions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'provider'" + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_thread_id": { + "name": "provider_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_request_id": { + "name": "provider_request_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "renderer_id": { + "name": "renderer_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_reason": { + "name": "status_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "pending_interactions_provider_request_idx": { + "name": "pending_interactions_provider_request_idx", + "columns": [ + "provider_id", + "provider_thread_id", + "provider_request_id" + ], + "isUnique": true + }, + "pending_interactions_thread_created_idx": { + "name": "pending_interactions_thread_created_idx", + "columns": [ + "thread_id", + "created_at" + ], + "isUnique": false + }, + "pending_interactions_thread_status_created_idx": { + "name": "pending_interactions_thread_status_created_idx", + "columns": [ + "thread_id", + "status", + "created_at" + ], + "isUnique": false + }, + "pending_interactions_status_created_idx": { + "name": "pending_interactions_status_created_idx", + "columns": [ + "status", + "created_at" + ], + "isUnique": false + }, + "pending_interactions_plugin_status_created_idx": { + "name": "pending_interactions_plugin_status_created_idx", + "columns": [ + "plugin_id", + "status", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "pending_interactions_thread_id_threads_id_fk": { + "name": "pending_interactions_thread_id_threads_id_fk", + "tableFrom": "pending_interactions", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_artifacts": { + "name": "plugin_artifacts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "npm_resolved_version": { + "name": "npm_resolved_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_resolved_commit": { + "name": "git_resolved_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_checkout_root": { + "name": "git_checkout_root", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integrity": { + "name": "integrity", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "validation_result": { + "name": "validation_result", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validated_at": { + "name": "validated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "plugin_artifacts_plugin_idx": { + "name": "plugin_artifacts_plugin_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_kv": { + "name": "plugin_kv", + "columns": { + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_kv_plugin_id_key_pk": { + "columns": [ + "plugin_id", + "key" + ], + "name": "plugin_kv_plugin_id_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_marketplace_icons": { + "name": "plugin_marketplace_icons", + "columns": { + "marketplace_name": { + "name": "marketplace_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entry_id": { + "name": "entry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_marketplace_icons_marketplace_name_entry_id_pk": { + "columns": [ + "marketplace_name", + "entry_id" + ], + "name": "plugin_marketplace_icons_marketplace_name_entry_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_marketplaces": { + "name": "plugin_marketplaces", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'https'" + }, + "manifest_url": { + "name": "manifest_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_git_ref": { + "name": "source_git_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_commit": { + "name": "source_git_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stats_json": { + "name": "stats_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_modified": { + "name": "last_modified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_successful_refresh_at": { + "name": "last_successful_refresh_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_attempted_refresh_at": { + "name": "last_attempted_refresh_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_schedules": { + "name": "plugin_schedules", + "columns": { + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_status": { + "name": "last_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_schedules_plugin_id_name_pk": { + "columns": [ + "plugin_id", + "name" + ], + "name": "plugin_schedules_plugin_id_name_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_settings_plugin_id_key_pk": { + "columns": [ + "plugin_id", + "key" + ], + "name": "plugin_settings_plugin_id_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_state_snapshots": { + "name": "plugin_state_snapshots", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "from_artifact_id": { + "name": "from_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "to_artifact_id": { + "name": "to_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_path": { + "name": "snapshot_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "database_path": { + "name": "database_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state_path": { + "name": "state_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets_path": { + "name": "secrets_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registration_path": { + "name": "registration_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rollback_candidate_version": { + "name": "rollback_candidate_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rollback_source_fingerprint": { + "name": "rollback_source_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rollback_bb_version": { + "name": "rollback_bb_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rollback_sdk_version": { + "name": "rollback_sdk_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rollback_detail": { + "name": "rollback_detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retained_until": { + "name": "retained_until", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "plugin_state_snapshots_plugin_idx": { + "name": "plugin_state_snapshots_plugin_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + }, + "plugin_state_snapshots_retention_idx": { + "name": "plugin_state_snapshots_retention_idx", + "columns": [ + "retained_until" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_execution_defaults": { + "name": "project_execution_defaults", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_tier": { + "name": "service_tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reasoning_level": { + "name": "reasoning_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_mode": { + "name": "permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_execution_defaults_project_idx": { + "name": "project_execution_defaults_project_idx", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_execution_defaults_project_id_projects_id_fk": { + "name": "project_execution_defaults_project_id_projects_id_fk", + "tableFrom": "project_execution_defaults", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_sources": { + "name": "project_sources", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_sources_project_idx": { + "name": "project_sources_project_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "project_sources_host_idx": { + "name": "project_sources_host_idx", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "project_sources_project_host_idx": { + "name": "project_sources_project_host_idx", + "columns": [ + "project_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_sources_project_id_projects_id_fk": { + "name": "project_sources_project_id_projects_id_fk", + "tableFrom": "project_sources", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_sources_host_id_hosts_id_fk": { + "name": "project_sources_host_id_hosts_id_fk", + "tableFrom": "project_sources", + "tableTo": "hosts", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "project_sources_shape_check": { + "name": "project_sources_shape_check", + "value": "(\n \"project_sources\".\"type\" = 'local_path' AND \"project_sources\".\"host_id\" IS NOT NULL AND \"project_sources\".\"path\" IS NOT NULL\n )" + } + } + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'standard'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "git_remote_url": { + "name": "git_remote_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_key": { + "name": "sort_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'V'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "projects_updated_idx": { + "name": "projects_updated_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "projects_deleted_idx": { + "name": "projects_deleted_idx", + "columns": [ + "deleted_at" + ], + "isUnique": false + }, + "projects_sort_idx": { + "name": "projects_sort_idx", + "columns": [ + "sort_key", + "id" + ], + "isUnique": false + }, + "projects_personal_singleton_idx": { + "name": "projects_personal_singleton_idx", + "columns": [ + "kind" + ], + "isUnique": true, + "where": "\"projects\".\"kind\" = 'personal'" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "prompt_history_entries": { + "name": "prompt_history_entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_sequence": { + "name": "request_sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input": { + "name": "input", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "prompt_history_entries_thread_request_idx": { + "name": "prompt_history_entries_thread_request_idx", + "columns": [ + "thread_id", + "request_sequence" + ], + "isUnique": true + }, + "prompt_history_entries_project_scope_created_idx": { + "name": "prompt_history_entries_project_scope_created_idx", + "columns": [ + "project_id", + "scope", + "created_at", + "request_sequence", + "id" + ], + "isUnique": false + }, + "prompt_history_entries_thread_scope_created_idx": { + "name": "prompt_history_entries_thread_scope_created_idx", + "columns": [ + "thread_id", + "scope", + "created_at", + "request_sequence", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "prompt_history_entries_project_id_projects_id_fk": { + "name": "prompt_history_entries_project_id_projects_id_fk", + "tableFrom": "prompt_history_entries", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "prompt_history_entries_thread_id_threads_id_fk": { + "name": "prompt_history_entries_thread_id_threads_id_fk", + "tableFrom": "prompt_history_entries", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "queued_thread_messages": { + "name": "queued_thread_messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sender_thread_id": { + "name": "sender_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reasoning_level": { + "name": "reasoning_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_mode": { + "name": "permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_tier": { + "name": "service_tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_with_next": { + "name": "group_with_next", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_key": { + "name": "sort_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "queued_thread_messages_thread_created_idx": { + "name": "queued_thread_messages_thread_created_idx", + "columns": [ + "thread_id", + "created_at", + "id" + ], + "isUnique": false + }, + "queued_thread_messages_thread_sort_idx": { + "name": "queued_thread_messages_thread_sort_idx", + "columns": [ + "thread_id", + "sort_key", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "queued_thread_messages_thread_id_threads_id_fk": { + "name": "queued_thread_messages_thread_id_threads_id_fk", + "tableFrom": "queued_thread_messages", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "system_experiments": { + "name": "system_experiments", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "terminal_sessions": { + "name": "terminal_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "daemon_session_id": { + "name": "daemon_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initial_cwd": { + "name": "initial_cwd", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cols": { + "name": "cols", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rows": { + "name": "rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_user_input_at": { + "name": "last_user_input_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "terminal_sessions_thread_status_updated_idx": { + "name": "terminal_sessions_thread_status_updated_idx", + "columns": [ + "thread_id", + "status", + "updated_at" + ], + "isUnique": false + }, + "terminal_sessions_environment_status_idx": { + "name": "terminal_sessions_environment_status_idx", + "columns": [ + "environment_id", + "status" + ], + "isUnique": false + }, + "terminal_sessions_host_status_idx": { + "name": "terminal_sessions_host_status_idx", + "columns": [ + "host_id", + "status" + ], + "isUnique": false + }, + "terminal_sessions_daemon_session_idx": { + "name": "terminal_sessions_daemon_session_idx", + "columns": [ + "daemon_session_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "terminal_sessions_thread_id_threads_id_fk": { + "name": "terminal_sessions_thread_id_threads_id_fk", + "tableFrom": "terminal_sessions", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "terminal_sessions_environment_id_environments_id_fk": { + "name": "terminal_sessions_environment_id_environments_id_fk", + "tableFrom": "terminal_sessions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "terminal_sessions_host_id_hosts_id_fk": { + "name": "terminal_sessions_host_id_hosts_id_fk", + "tableFrom": "terminal_sessions", + "tableTo": "hosts", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "terminal_sessions_daemon_session_id_host_daemon_sessions_id_fk": { + "name": "terminal_sessions_daemon_session_id_host_daemon_sessions_id_fk", + "tableFrom": "terminal_sessions", + "tableTo": "host_daemon_sessions", + "columnsFrom": [ + "daemon_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_dynamic_context_file_states": { + "name": "thread_dynamic_context_file_states", + "columns": { + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_key": { + "name": "file_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_status": { + "name": "content_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "shown_at": { + "name": "shown_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "thread_dynamic_context_file_states_thread_file_idx": { + "name": "thread_dynamic_context_file_states_thread_file_idx", + "columns": [ + "thread_id", + "file_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "thread_dynamic_context_file_states_thread_id_threads_id_fk": { + "name": "thread_dynamic_context_file_states_thread_id_threads_id_fk", + "tableFrom": "thread_dynamic_context_file_states", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_search_segments": { + "name": "thread_search_segments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_seq": { + "name": "source_seq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "thread_search_segments_source_idx": { + "name": "thread_search_segments_source_idx", + "columns": [ + "thread_id", + "source_kind", + "source_key" + ], + "isUnique": true + }, + "thread_search_segments_thread_source_seq_idx": { + "name": "thread_search_segments_thread_source_seq_idx", + "columns": [ + "thread_id", + "source_seq" + ], + "isUnique": false + } + }, + "foreignKeys": { + "thread_search_segments_thread_id_threads_id_fk": { + "name": "thread_search_segments_thread_id_threads_id_fk", + "tableFrom": "thread_search_segments", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_sections": { + "name": "thread_sections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "thread_sections_name_idx": { + "name": "thread_sections_name_idx", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_tabs": { + "name": "thread_tabs", + "columns": { + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tabs_json": { + "name": "tabs_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "thread_tabs_thread_id_threads_id_fk": { + "name": "thread_tabs_thread_id_threads_id_fk", + "tableFrom": "thread_tabs", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "threads": { + "name": "threads", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_override": { + "name": "model_override", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reasoning_level_override": { + "name": "reasoning_level_override", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title_fallback": { + "name": "title_fallback", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'starting'" + }, + "parent_thread_id": { + "name": "parent_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_thread_id": { + "name": "source_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin_plugin_id": { + "name": "origin_plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'visible'" + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_sort_key": { + "name": "pin_sort_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_read_at": { + "name": "last_read_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latest_attention_at": { + "name": "latest_attention_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "threads_project_updated_idx": { + "name": "threads_project_updated_idx", + "columns": [ + "project_id", + "updated_at" + ], + "isUnique": false + }, + "threads_project_archived_deleted_idx": { + "name": "threads_project_archived_deleted_idx", + "columns": [ + "project_id", + "archived_at", + "deleted_at", + "id" + ], + "isUnique": false + }, + "threads_pin_sort_idx": { + "name": "threads_pin_sort_idx", + "columns": [ + "archived_at", + "deleted_at", + "pin_sort_key", + "id" + ], + "isUnique": false, + "where": "\"threads\".\"pinned_at\" IS NOT NULL" + }, + "threads_environment_idx": { + "name": "threads_environment_idx", + "columns": [ + "environment_id" + ], + "isUnique": false + }, + "threads_parent_idx": { + "name": "threads_parent_idx", + "columns": [ + "parent_thread_id" + ], + "isUnique": false + }, + "threads_source_origin_idx": { + "name": "threads_source_origin_idx", + "columns": [ + "source_thread_id", + "origin_kind" + ], + "isUnique": false + }, + "threads_origin_plugin_archived_idx": { + "name": "threads_origin_plugin_archived_idx", + "columns": [ + "origin_plugin_id", + "archived_at" + ], + "isUnique": false + }, + "threads_section_archived_deleted_idx": { + "name": "threads_section_archived_deleted_idx", + "columns": [ + "section_id", + "archived_at", + "deleted_at", + "id" + ], + "isUnique": false + }, + "threads_archived_status_idx": { + "name": "threads_archived_status_idx", + "columns": [ + "archived_at", + "status" + ], + "isUnique": false + }, + "threads_environment_archived_deleted_idx": { + "name": "threads_environment_archived_deleted_idx", + "columns": [ + "environment_id", + "archived_at", + "deleted_at" + ], + "isUnique": false + }, + "threads_active_maintenance_idx": { + "name": "threads_active_maintenance_idx", + "columns": [ + "status" + ], + "isUnique": false, + "where": "\"threads\".\"deleted_at\" IS NULL" + } + }, + "foreignKeys": { + "threads_project_id_projects_id_fk": { + "name": "threads_project_id_projects_id_fk", + "tableFrom": "threads", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "threads_environment_id_environments_id_fk": { + "name": "threads_environment_id_environments_id_fk", + "tableFrom": "threads", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "threads_section_id_thread_sections_id_fk": { + "name": "threads_section_id_thread_sections_id_fk", + "tableFrom": "threads", + "tableTo": "thread_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "threads_parent_thread_id_threads_id_fk": { + "name": "threads_parent_thread_id_threads_id_fk", + "tableFrom": "threads", + "tableTo": "threads", + "columnsFrom": [ + "parent_thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "threads_source_thread_id_threads_id_fk": { + "name": "threads_source_thread_id_threads_id_fk", + "tableFrom": "threads", + "tableTo": "threads", + "columnsFrom": [ + "source_thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file From 67819948579995d1a32d395bb4616bc4803b6d75 Mon Sep 17 00:00:00 2001 From: Andrew Chan Date: Sat, 22 Aug 2026 00:30:24 -0700 Subject: [PATCH 3/7] server: parse and fetch the marketplace install-count sidecar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A self-contained module, unused until the next commit wires it in. Three decisions live here. The fetch is unconditional rather than replaying the manifest's ETag: the counts move while the manifest sits unchanged behind a 304, which is the whole reason they are a separate document instead of a manifest field. A missing sidecar answers null, because a marketplace need not publish counts at all. And the schema is deliberately not strict, unlike the manifest's: that one is a security contract where an unknown field must reject the document, while this is display metadata a later publisher may extend, and losing every count over an unknown field is worse than ignoring the field. A malformed document is still rejected whole — half-parsed counts are worse than none. Ids outside the manifest's own id pattern are dropped rather than stored: they can never match an entry. > AGENT GENERATED --- .../plugin-catalog/marketplace-stats.ts | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 apps/server/src/services/plugin-catalog/marketplace-stats.ts diff --git a/apps/server/src/services/plugin-catalog/marketplace-stats.ts b/apps/server/src/services/plugin-catalog/marketplace-stats.ts new file mode 100644 index 0000000000..d805a19b28 --- /dev/null +++ b/apps/server/src/services/plugin-catalog/marketplace-stats.ts @@ -0,0 +1,128 @@ +import { z } from "zod"; +import { + boundedResponseBytes, + MARKETPLACE_FETCH_TIMEOUT_MS, + type MarketplaceFetch, +} from "./marketplace-http.js"; + +/** Sidecar the curated marketplace publishes beside its manifest. */ +const MARKETPLACE_STATS_FILENAME = "stats.json"; + +/** One id plus one integer per entry; this only bounds a hostile response. */ +const MARKETPLACE_STATS_MAX_BYTES = 512 * 1024; + +/** Same id shape the manifest requires of an entry. */ +const ENTRY_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/u; + +/** + * The install-count sidecar. + * + * Deliberately not a strict schema, unlike the manifest. The manifest is a + * security contract, so an unknown field there rejects the document; this file + * is display metadata that a later publisher may extend, and a store that lost + * its counts because a new field appeared would be worse than one that ignores + * the field. A malformed document is still rejected whole: half-parsed counts + * are worse than none. + */ +const marketplaceStatsSchema = z.object({ + schemaVersion: z.literal(1), + /** When the publisher ran the query, ISO 8601. Recorded, not displayed. */ + generatedAt: z.string(), + plugins: z.record( + z.string(), + z.object({ installs: z.number().int().nonnegative() }), + ), +}); + +export type MarketplaceStats = z.infer; + +export function parseMarketplaceStatsJson( + raw: string, + location: string, +): MarketplaceStats { + let document: unknown; + try { + document = JSON.parse(raw); + } catch (error) { + throw new Error( + `invalid ${location}: not valid JSON (${error instanceof Error ? error.message : String(error)})`, + ); + } + const parsed = marketplaceStatsSchema.safeParse(document); + if (!parsed.success) { + const [issue] = parsed.error.issues; + throw new Error( + `invalid ${location}: ${issue === undefined ? "unexpected shape" : `${issue.path.join(".") || "/"} ${issue.message}`}`, + ); + } + // An id outside the manifest's own id shape can never match an entry, so it + // is dropped here rather than kept as a row nothing will ever read. + return { + ...parsed.data, + plugins: Object.fromEntries( + Object.entries(parsed.data.plugins).filter(([id]) => + ENTRY_ID_PATTERN.test(id), + ), + ), + }; +} + +/** Install counts by entry id, or an empty map when there is no sidecar. */ +export function installCountsFromStatsJson( + statsJson: string | null, + onInvalid?: (message: string) => void, +): ReadonlyMap { + if (statsJson === null) return new Map(); + try { + const stats = parseMarketplaceStatsJson(statsJson, "stored install counts"); + return new Map( + Object.entries(stats.plugins).map(([id, entry]) => [id, entry.installs]), + ); + } catch (error) { + onInvalid?.(error instanceof Error ? error.message : String(error)); + return new Map(); + } +} + +/** Where the sidecar of an https marketplace lives: beside its manifest. */ +export function marketplaceStatsUrl(manifestUrl: string): string { + return new URL(MARKETPLACE_STATS_FILENAME, manifestUrl).toString(); +} + +/** + * Fetch the install-count sidecar of the curated marketplace. + * + * Unconditional on purpose: the counts move while the manifest sits unchanged + * behind a 304, so replaying the manifest's validators here would freeze them. + * A missing file (404) is normal — a marketplace need not publish counts — + * and answers null, as does any failure. The caller keeps the counts it + * already had; a store must not lose its catalog over a cosmetic number. + */ +export async function fetchMarketplaceStats(args: { + manifestUrl: string; + fetch: MarketplaceFetch; +}): Promise { + const url = marketplaceStatsUrl(args.manifestUrl); + const response = await args.fetch(url, { + method: "GET", + headers: new Headers({ accept: "application/json" }), + redirect: "error", + signal: AbortSignal.timeout(MARKETPLACE_FETCH_TIMEOUT_MS), + }); + if (response.status === 404) { + await response.body?.cancel(); + return null; + } + if (!response.ok) { + await response.body?.cancel(); + throw new Error(`request failed with HTTP ${response.status}`); + } + const raw = new TextDecoder().decode( + await boundedResponseBytes( + response, + MARKETPLACE_STATS_MAX_BYTES, + "marketplace install counts", + ), + ); + return parseMarketplaceStatsJson(raw, "marketplace install counts"); +} From 7d54e7d4c206878fd8295bd6ea4429bfbb740049 Mon Sep 17 00:00:00 2001 From: Andrew Chan Date: Sat, 22 Aug 2026 00:30:39 -0700 Subject: [PATCH 4/7] server: read install counts on refresh and report them in search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the sidecar into the catalog and puts the number on the wire. Every refresh of the curated marketplace now fetches the sidecar and stores it in the same transaction as the catalog snapshot, so counts and entries are always published together. A failure warns and keeps the counts already stored: a cosmetic number must never fail a catalog refresh, exactly as a failed manifest read keeps the last-known-good catalog. Only the curated marketplace is asked for a sidecar. BB measures these counts from its own telemetry, so a number beside a third-party listing would be that publisher's claim wearing BB's label — those entries report null and BB never requests the file. Bundled plugins do get counts: telemetry sends a plugin_id for them too, and they are listed under the curated marketplace, so they read from the same document. `installs` joins the catalog search result as nullable with a null default, so a server from before the field degrades to no count rather than to zero — the same shape repositoryUrl already uses. Zero would be a claim; null is the absence of one. Six tests, each failing before this commit: counts on curated and bundled entries, an unnamed entry staying uncounted, the sidecar being re-read while the manifest answers 304, a failed sidecar keeping stored counts while the refresh still records success, a malformed document rejected whole, and a third-party marketplace whose sidecar is never requested. > AGENT GENERATED --- .../src/data/plugins/plugin-model.test.ts | 1 + .../plugin-catalog/plugin-catalog-service.ts | 65 ++++++- .../plugin-catalog-service.test.ts | 164 +++++++++++++++++- packages/server-contract/src/api/plugins.ts | 8 + 4 files changed, 233 insertions(+), 5 deletions(-) diff --git a/apps/mobile/src/data/plugins/plugin-model.test.ts b/apps/mobile/src/data/plugins/plugin-model.test.ts index 0340338102..7d6ed74e79 100644 --- a/apps/mobile/src/data/plugins/plugin-model.test.ts +++ b/apps/mobile/src/data/plugins/plugin-model.test.ts @@ -211,6 +211,7 @@ describe("groupCatalogEntries", () => { category: "Misc", source: "npm:e", repositoryUrl: null, + installs: null, marketplace: "zeta", marketplaceDisplayName: "Zeta", publisherKey: "zeta", diff --git a/apps/server/src/services/plugin-catalog/plugin-catalog-service.ts b/apps/server/src/services/plugin-catalog/plugin-catalog-service.ts index c0da9f16e3..4f3a0e5fd0 100644 --- a/apps/server/src/services/plugin-catalog/plugin-catalog-service.ts +++ b/apps/server/src/services/plugin-catalog/plugin-catalog-service.ts @@ -45,6 +45,10 @@ import { selectGitSemverTag, } from "../plugins/update-resolver.js"; import { fetchMarketplaceIcons } from "./marketplace-icons.js"; +import { + fetchMarketplaceStats, + installCountsFromStatsJson, +} from "./marketplace-stats.js"; import { marketplaceErrorMessage, publicMarketplaceFetch, @@ -402,6 +406,7 @@ export function createPluginCatalogService(deps: { entry: { name: string; pluginId: string; category: string }, manifest: PluginManifest, iconHash: string | null, + installs: number | null, ): PluginCatalogSearchResult { const problem = compatibilityProblem({ bbRange: manifest.bbEngineRange, @@ -436,6 +441,10 @@ export function createPluginCatalogService(deps: { // Bundled plugins are BB's own; attribute them like the seed entries. author: { name: "BB Team", url: "https://getbb.app" }, installed: getInstalledPlugin(deps.db, entry.pluginId) !== undefined, + // Bundled plugins are counted under their own entry id: telemetry sends + // a plugin_id for them too, so the curated sidecar names them alongside + // the entries it lists. + installs, compatible: problem === null, incompatibleReason: problem, }; @@ -487,6 +496,8 @@ export function createPluginCatalogService(deps: { row: PluginMarketplaceRow; catalog: MarketplaceManifest; installedEntryIds: ReadonlySet; + /** Null for every third-party listing, which publishes no counts. */ + installs: number | null; }): PluginCatalogSearchResult { const { entry, row, catalog } = args; const official = row.name === CURATED_MARKETPLACE_NAME; @@ -514,6 +525,7 @@ export function createPluginCatalogService(deps: { installed: args.installedEntryIds.has(catalogEntryKey(row.name, entry.id)) || getInstalledPlugin(deps.db, entry.id) !== undefined, + installs: args.installs, // The listing declares no ranges, so bb cannot judge a marketplace // entry until it has fetched the plugin's own manifest. compatible: true, @@ -547,6 +559,36 @@ export function createPluginCatalogService(deps: { }; } + /** + * The install-count sidecar to store for this refresh. + * + * Only the curated marketplace publishes counts: BB measures them from its + * own telemetry, so a number beside a third-party listing would be that + * publisher's claim wearing BB's label. A fetch failure keeps the counts + * already stored — a cosmetic number must never fail a catalog refresh. + */ + async function refreshedStatsJson( + row: PluginMarketplaceRow, + ): Promise { + if (row.name !== CURATED_MARKETPLACE_NAME || row.sourceKind !== "https") { + return null; + } + try { + const stats = await fetchMarketplaceStats({ + manifestUrl: row.manifestUrl, + fetch: fetchMarketplace, + }); + // A published-then-withdrawn sidecar clears the counts; a 404 that was + // never there to begin with leaves the (already null) column alone. + return stats === null ? null : JSON.stringify(stats); + } catch (error) { + deps.warn?.( + `${row.name} install counts were not refreshed: ${marketplaceErrorMessage(error)}`, + ); + return row.statsJson; + } + } + async function performRefresh( row: PluginMarketplaceRow, attemptedAt: number, @@ -591,6 +633,7 @@ export function createPluginCatalogService(deps: { fetch: fetchMarketplace, ...(deps.warn === undefined ? {} : { warn: deps.warn }), }); + const statsJson = await refreshedStatsJson(row); // The catalog and all icon rows form one snapshot. Network work happens // first, then SQLite publishes the complete snapshot in one commit. deps.db.transaction((tx) => { @@ -599,8 +642,7 @@ export function createPluginCatalogService(deps: { ...marketplaceSourceColumns(source), sourceGitCommit: materialized.commit, manifestJson, - // Carried through untouched; nothing writes counts yet. - statsJson: row.statsJson, + statsJson, etag: materialized.etag, lastModified: materialized.lastModified, lastSuccessfulRefreshAt: attemptedAt, @@ -1115,6 +1157,16 @@ export function createPluginCatalogService(deps: { async search(rawQuery) { const query = rawQuery.trim().toLowerCase(); + // Bundled plugins are listed under the curated marketplace, so they read + // their counts from that marketplace's sidecar too. + const curatedRow = getPluginMarketplace( + deps.db, + CURATED_MARKETPLACE_NAME, + ); + const curatedInstalls = installCountsFromStatsJson( + curatedRow?.statsJson ?? null, + (message) => deps.warn?.(message), + ); const bundledEntries = await Promise.all( officialPlugins.map(async (entry) => { const manifest = await entryManifest(entry); @@ -1124,7 +1176,12 @@ export function createPluginCatalogService(deps: { pluginId: entry.pluginId, tags: [] as string[], marketplaceRank: 0, - result: bundledSearchResult(entry, manifest, icon?.hash ?? null), + result: bundledSearchResult( + entry, + manifest, + icon?.hash ?? null, + curatedInstalls.get(entry.name) ?? null, + ), }; }), ); @@ -1147,6 +1204,7 @@ export function createPluginCatalogService(deps: { const catalogEntries = orderedMarketplaces().flatMap((row, index) => { const catalog = catalogOf(row); if (catalog === null) return []; + const official = row.name === CURATED_MARKETPLACE_NAME; return catalog.plugins.map((entry) => ({ pluginId: entry.id, tags: entry.tags ?? [], @@ -1156,6 +1214,7 @@ export function createPluginCatalogService(deps: { row, catalog, installedEntryIds, + installs: official ? (curatedInstalls.get(entry.id) ?? null) : null, }), })); }); diff --git a/apps/server/test/services/plugin-catalog/plugin-catalog-service.test.ts b/apps/server/test/services/plugin-catalog/plugin-catalog-service.test.ts index b93c34e9d7..886f2e8a0e 100644 --- a/apps/server/test/services/plugin-catalog/plugin-catalog-service.test.ts +++ b/apps/server/test/services/plugin-catalog/plugin-catalog-service.test.ts @@ -24,6 +24,8 @@ import { const MANIFEST_URL = "https://marketplace.test/marketplace/v1/marketplace.json"; const ICON_URL = "https://marketplace.test/marketplace/v1/icons/widgets.svg"; +/** Install-count sidecar beside the manifest; most tests do not publish one. */ +const STATS_URL = "https://marketplace.test/marketplace/v1/stats.json"; const SEED_ENTRY_COUNT = BUNDLED_CURATED_MARKETPLACE.plugins.length; const VALID_SVG = Buffer.from( @@ -313,7 +315,10 @@ describe("plugin catalog service", () => { if (url === MANIFEST_URL) { return jsonResponse( manifest([ - remoteEntry({ id: "raster", icon: { url: "./icons/raster.png" } }), + remoteEntry({ + id: "raster", + icon: { url: "./icons/raster.png" }, + }), remoteEntry({ id: "glyph", icon: { url: "./icons/glyph.svg" } }), ]), ); @@ -605,6 +610,157 @@ describe("plugin catalog service", () => { }); }); + describe("install counts", () => { + function statsResponse( + plugins: Record, + generatedAt = "2026-08-21T00:00:00.000Z", + ): Response { + return jsonResponse({ schemaVersion: 1, generatedAt, plugins }); + } + + /** Serve a manifest, a sidecar, and an icon for anything else. */ + function fetchWith( + stats: () => Response, + entries: unknown[] = [remoteEntry()], + ): MarketplaceFetch { + return async (url) => { + if (url === MANIFEST_URL) return jsonResponse(manifest(entries)); + if (url === STATS_URL) return stats(); + return new Response(VALID_SVG, { + status: 200, + headers: { "content-type": "image/svg+xml" }, + }); + }; + } + + it("reports counts for curated entries and bundled plugins", async () => { + const bundled = listBundledPluginRegistrations()[0]; + if (bundled === undefined) throw new Error("no bundled plugin"); + const catalog = service({ + fetch: fetchWith(() => + statsResponse({ + widgets: { installs: 4_210 }, + [bundled.name]: { installs: 12 }, + }), + ), + }); + + await catalog.refresh(1_000); + const byId = new Map( + (await catalog.search("")).map((entry) => [entry.entryId, entry]), + ); + expect(byId.get("widgets")?.installs).toBe(4_210); + expect(byId.get(bundled.name)?.installs).toBe(12); + }); + + it("leaves an entry the sidecar does not name uncounted", async () => { + const catalog = service({ + fetch: fetchWith(() => statsResponse({ other: { installs: 9 } })), + }); + + await catalog.refresh(1_000); + expect((await catalog.search("widgets"))[0]?.installs).toBeNull(); + }); + + it("re-reads the sidecar when the manifest is unchanged", async () => { + // The whole point of a separate document: counts move behind a 304. + let manifestReads = 0; + let installs = 5; + const catalog = service({ + fetch: async (url) => { + if (url === MANIFEST_URL) { + manifestReads += 1; + return manifestReads === 1 + ? jsonResponse(manifest([remoteEntry()]), { etag: '"v1"' }) + : new Response(null, { status: 304 }); + } + if (url === STATS_URL) { + return statsResponse({ widgets: { installs } }); + } + return new Response(VALID_SVG, { + status: 200, + headers: { "content-type": "image/svg+xml" }, + }); + }, + }); + + await catalog.refresh(1_000); + expect((await catalog.search("widgets"))[0]?.installs).toBe(5); + installs = 40; + await catalog.refresh(2_000); + expect((await catalog.search("widgets"))[0]?.installs).toBe(40); + }); + + it("keeps the stored counts when the sidecar fails, and the refresh still succeeds", async () => { + const warnings: string[] = []; + let sidecarBroken = false; + const catalog = service({ + warn: (message) => warnings.push(message), + fetch: fetchWith(() => + sidecarBroken + ? new Response("nope", { status: 500 }) + : statsResponse({ widgets: { installs: 7 } }), + ), + }); + + await catalog.refresh(1_000); + sidecarBroken = true; + await catalog.refresh(2_000); + expect((await catalog.search("widgets"))[0]?.installs).toBe(7); + expect(getPluginMarketplace(db, "bb-community")).toMatchObject({ + lastSuccessfulRefreshAt: 2_000, + lastError: null, + }); + expect(warnings.join("\n")).toMatch(/install counts were not refreshed/u); + }); + + it("rejects a malformed sidecar whole rather than counting part of it", async () => { + const catalog = service({ + fetch: fetchWith(() => + jsonResponse({ + schemaVersion: 1, + generatedAt: "2026-08-21T00:00:00.000Z", + plugins: { widgets: { installs: -1 } }, + }), + ), + }); + + await catalog.refresh(1_000); + expect((await catalog.search("widgets"))[0]?.installs).toBeNull(); + }); + + it("does not count entries of a third-party marketplace", async () => { + const thirdPartyManifest = + "https://acme.test/marketplace/marketplace.json"; + const statsRequests: string[] = []; + const catalog = service({ + fetch: async (url) => { + if (url === MANIFEST_URL) return jsonResponse(manifest([])); + if (url.endsWith("/stats.json")) { + statsRequests.push(url); + return statsResponse({ widgets: { installs: 999 } }); + } + if (url === thirdPartyManifest) { + return jsonResponse({ + schemaVersion: 1, + name: "acme", + displayName: "Acme", + plugins: [remoteEntry({ icon: "ZoomIn" })], + }); + } + return new Response(null, { status: 404 }); + }, + }); + + await catalog.addMarketplace(thirdPartyManifest); + await catalog.refreshMarketplaces({ attemptedAt: 2_000 }); + expect((await catalog.search("widgets"))[0]?.installs).toBeNull(); + // A publisher's own count wearing BB's label is never fetched at all: + // the only sidecar request in the whole run is the curated one. + expect(statsRequests).toEqual([STATS_URL]); + }); + }); + describe("catalog installs", () => { async function refreshedCatalog(entry: Record) { const catalog = service({ @@ -673,7 +829,10 @@ describe("plugin catalog service", () => { incompatibleReason: null, }); const plan = await catalog.installPlan({ entryId: "widgets" }); - expect(plan).toMatchObject({ compatible: true, incompatibleReason: null }); + expect(plan).toMatchObject({ + compatible: true, + incompatibleReason: null, + }); await expect(catalog.install({ entryId: "widgets" })).rejects.toThrow( "catalog installation stopped by test", ); @@ -807,6 +966,7 @@ describe("plugin catalog service", () => { ]), ); } + if (url === STATS_URL) return new Response(null, { status: 404 }); iconRequests.push(url); return new Response(VALID_SVG, { status: 200, diff --git a/packages/server-contract/src/api/plugins.ts b/packages/server-contract/src/api/plugins.ts index 9f3a69643f..4c05a12698 100644 --- a/packages/server-contract/src/api/plugins.ts +++ b/packages/server-contract/src/api/plugins.ts @@ -419,6 +419,14 @@ export const pluginCatalogSearchResultSchema = z.object({ /** Null for plugins bundled with the app, which list no separate author. */ author: pluginCatalogAuthorSchema.nullable(), installed: z.boolean(), + /** + * How many distinct BB installations reported installing this plugin, from + * the curated marketplace's published `stats.json`. Null when the count is + * unknown: every third-party listing, any entry the sidecar does not name, + * and every server that has not fetched a sidecar yet. Older servers do not + * send it, so those entries show no count. + */ + installs: z.number().int().nonnegative().nullable().default(null), compatible: z.boolean(), incompatibleReason: z.string().nullable(), }); From bf92055e157169cea6067b58abc2635db33c9a33 Mon Sep 17 00:00:00 2001 From: Andrew Chan Date: Sat, 22 Aug 2026 00:30:47 -0700 Subject: [PATCH 5/7] app, mobile, cli: show install counts Renders the number the server now reports. The store card puts a compact "4.2K installs" in its footer beside the publisher and repository link, with the exact number in the title attribute: a card is read at a glance, and the precise figure is there for anyone who wants it. Mobile appends the same compact label to the browse subtitle. `bb plugin search` prints an exact, comma-grouped number, because a terminal column is read to be compared, and the column appears only once some result carries a count so it stays out of the way otherwise. An entry with no count renders nothing at all rather than a zero, on every surface. > AGENT GENERATED --- .../management/BrowsePluginsTab.test.tsx | 64 +++++++++++++++++++ .../plugin/management/BrowsePluginsTab.tsx | 43 +++++++++++-- .../tools/ExtensionsDetailStates.stories.tsx | 1 + .../queries/plugin-catalog-queries.test.ts | 2 + .../hooks/queries/plugin-catalog-queries.ts | 3 + .../views/ToolsView.plugin-detail.test.tsx | 1 + .../command-output/plugin-catalog.test.ts | 20 ++++++ apps/cli/src/commands/plugin.ts | 24 +++++-- .../screens/plugins/PluginBrowseScreen.tsx | 12 ++++ 9 files changed, 161 insertions(+), 9 deletions(-) diff --git a/apps/app/src/components/plugin/management/BrowsePluginsTab.test.tsx b/apps/app/src/components/plugin/management/BrowsePluginsTab.test.tsx index 2e35a78c78..bd983ef842 100644 --- a/apps/app/src/components/plugin/management/BrowsePluginsTab.test.tsx +++ b/apps/app/src/components/plugin/management/BrowsePluginsTab.test.tsx @@ -41,6 +41,7 @@ const MEMORY_ENTRY: PluginCatalogSearchEntry = { official: true, author: null, installed: false, + installs: null, compatible: true, incompatibleReason: null, }; @@ -236,6 +237,69 @@ describe("BrowsePluginsTab", () => { ).toBeNull(); }); + it("shows a compact install count beside the other card footer facts", async () => { + const entries = [ + { ...MEMORY_ENTRY, displayName: "Memory", installs: 4_210 }, + { + ...MEMORY_ENTRY, + entryId: "notes", + pluginId: "notes", + displayName: "Acme Notes", + marketplace: "acme-plugins", + marketplaceDisplayName: "Acme Plugins", + publisherKey: "acme-plugins", + publisherLabel: "Acme Plugins", + official: false, + // Third-party listings publish no counts; the card says nothing + // rather than implying zero. + installs: null, + }, + { + ...MEMORY_ENTRY, + entryId: "solo", + pluginId: "solo", + displayName: "Solo", + installs: 1, + }, + ]; + vi.stubGlobal( + "fetch", + vi.fn(async (url: string) => { + if (url === "/api/v1/plugin-catalog") { + return jsonResponse({ catalog: CATALOG_STATUS }); + } + if (url === "/api/v1/plugin-catalog/search?q=") { + return jsonResponse({ results: entries }); + } + if (url === "/api/v1/plugins") { + return jsonResponse({ enabled: true, plugins: [] }); + } + return jsonResponse({ error: "not found" }, 404); + }), + ); + + const { wrapper } = createQueryClientTestHarness(); + render( + + {}} + onOpenPlugin={() => {}} + onInstallFromSource={() => {}} + /> + , + { wrapper }, + ); + + await screen.findByText("Acme Notes"); + const count = screen.getByText("4.2K installs"); + expect(count.getAttribute("title")).toBe("4,210 installs"); + expect(screen.getByText("1 install")).toBeTruthy(); + // The uncounted card still shows its publisher, so the footer did not + // collapse into a stray separator. + expect(screen.getAllByText("Acme Plugins").length).toBeGreaterThan(0); + expect(screen.queryByText("0 installs")).toBeNull(); + }); + it("keeps a marketplace that copies a publisher label in its own group", async () => { const entries = [ { ...MEMORY_ENTRY, displayName: "Memory" }, diff --git a/apps/app/src/components/plugin/management/BrowsePluginsTab.tsx b/apps/app/src/components/plugin/management/BrowsePluginsTab.tsx index 769b97a890..9a8bb5bcb9 100644 --- a/apps/app/src/components/plugin/management/BrowsePluginsTab.tsx +++ b/apps/app/src/components/plugin/management/BrowsePluginsTab.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { Fragment, useEffect, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useDebounceValue } from "usehooks-ts"; @@ -348,6 +348,19 @@ function groupByPublisher( return groups; } +/** + * Store counts are read at a glance, not audited: "1.2k" carries the scale a + * card needs, and the exact number stays in the title attribute. + */ +const INSTALL_COUNT_FORMATTER = new Intl.NumberFormat(undefined, { + notation: "compact", + maximumFractionDigits: 1, +}); + +export function formatInstallCount(installs: number): string { + return `${INSTALL_COUNT_FORMATTER.format(installs)} ${installs === 1 ? "install" : "installs"}`; +} + function BrowseCard({ entry, installedPluginId, @@ -419,12 +432,32 @@ function BrowseCard({ /> ); + // Only the curated marketplace publishes counts, so this is null for every + // third-party listing and for any entry its sidecar does not name. + const installs = + entry.installs === null ? null : ( + + {formatInstallCount(entry.installs)} + + ); + const footerParts = [ + entry.official ? null : entry.publisherLabel, + installs, + repositoryLink, + ].filter((part) => part !== null); const footerMeta = - entry.official && repositoryLink === null ? undefined : ( + footerParts.length === 0 ? undefined : ( - {entry.official ? null : entry.publisherLabel} - {!entry.official && repositoryLink !== null ? " · " : null} - {repositoryLink} + {footerParts.map((part, index) => ( + // Index keys: the parts are a fixed, ordered set, not a reorderable + // list, so position is their identity. + + {index > 0 ? " · " : null} + {part} + + ))} ); const headerAction = diff --git a/apps/app/src/components/tools/ExtensionsDetailStates.stories.tsx b/apps/app/src/components/tools/ExtensionsDetailStates.stories.tsx index 07e9175994..df6eea5040 100644 --- a/apps/app/src/components/tools/ExtensionsDetailStates.stories.tsx +++ b/apps/app/src/components/tools/ExtensionsDetailStates.stories.tsx @@ -573,6 +573,7 @@ const UNINSTALLED_CATALOG_PLUGIN = { official: true, author: null, installed: false, + installs: null, compatible: true, incompatibleReason: null, } satisfies PluginCatalogSearchEntry; diff --git a/apps/app/src/hooks/queries/plugin-catalog-queries.test.ts b/apps/app/src/hooks/queries/plugin-catalog-queries.test.ts index f1b573b49b..5db2cf6fad 100644 --- a/apps/app/src/hooks/queries/plugin-catalog-queries.test.ts +++ b/apps/app/src/hooks/queries/plugin-catalog-queries.test.ts @@ -164,6 +164,8 @@ describe("plugin catalog queries", () => { official: false, author: { name: "Acme", url: "https://acme.dev" }, installed: false, + // Likewise: a server from before the field reports no count. + installs: null, compatible: false, incompatibleReason: "requires bb >= 0.15", }, diff --git a/apps/app/src/hooks/queries/plugin-catalog-queries.ts b/apps/app/src/hooks/queries/plugin-catalog-queries.ts index 4783120069..ee30842e91 100644 --- a/apps/app/src/hooks/queries/plugin-catalog-queries.ts +++ b/apps/app/src/hooks/queries/plugin-catalog-queries.ts @@ -247,6 +247,8 @@ export interface PluginCatalogSearchEntry { official: boolean; author: PluginCatalogAuthor | null; installed: boolean; + /** Distinct BB installations that reported installing it; null when unknown. */ + installs: number | null; compatible: boolean; incompatibleReason: string | null; } @@ -272,6 +274,7 @@ function toPluginCatalogSearchEntry( official: data.official, author: data.author, installed: data.installed, + installs: data.installs, compatible: data.compatible, incompatibleReason: data.incompatibleReason ?? null, }; diff --git a/apps/app/src/views/ToolsView.plugin-detail.test.tsx b/apps/app/src/views/ToolsView.plugin-detail.test.tsx index dc8da82146..74d820b329 100644 --- a/apps/app/src/views/ToolsView.plugin-detail.test.tsx +++ b/apps/app/src/views/ToolsView.plugin-detail.test.tsx @@ -79,6 +79,7 @@ const GITHUB_CATALOG_ENTRY = { official: true, author: null, installed: false, + installs: null, compatible: true, incompatibleReason: null, } satisfies PluginCatalogSearchEntry; diff --git a/apps/cli/src/__tests__/command-output/plugin-catalog.test.ts b/apps/cli/src/__tests__/command-output/plugin-catalog.test.ts index f5fcb5bf1a..5f15d371e7 100644 --- a/apps/cli/src/__tests__/command-output/plugin-catalog.test.ts +++ b/apps/cli/src/__tests__/command-output/plugin-catalog.test.ts @@ -27,6 +27,7 @@ const searchResult = { official: true, author: null, installed: false, + installs: null, compatible: true, incompatibleReason: null, }; @@ -148,6 +149,25 @@ describe("bb plugin catalog", () => { expect(output).toContain("BB Official"); }); + it("adds an Installs column only once a listing reports counts", async () => { + vi.mocked(fetch).mockResolvedValueOnce(json({ results: [searchResult] })); + await runCommand(["plugin", "search", "lin"], register); + expect(collectLogPayloads(vi.mocked(console.log)).join("\n")).not.toContain( + "Installs", + ); + + vi.mocked(console.log).mockClear(); + vi.mocked(fetch).mockResolvedValueOnce( + json({ results: [{ ...searchResult, installs: 4210 }] }), + ); + await runCommand(["plugin", "search", "lin"], register); + + // Exact, not compact: a terminal column is read to be compared. + const output = collectLogPayloads(vi.mocked(console.log)).join("\n"); + expect(output).toContain("Installs"); + expect(output).toContain("4,210"); + }); + it("outputs raw catalog search results as JSON", async () => { vi.mocked(fetch).mockResolvedValueOnce(json({ results: [searchResult] })); diff --git a/apps/cli/src/commands/plugin.ts b/apps/cli/src/commands/plugin.ts index 08e0ebe535..f6ecd1cdc1 100644 --- a/apps/cli/src/commands/plugin.ts +++ b/apps/cli/src/commands/plugin.ts @@ -912,10 +912,20 @@ export function registerPluginCommands( // Where a listing came from only matters once something other than // BB's own catalog is registered; until then the column is noise. const showMarketplace = results.some((result) => !result.official); + // Only the curated marketplace publishes counts, and only once it has + // been refreshed from a server that serves the sidecar. + const showInstalls = results.some((result) => result.installs !== null); const rows = results.map((result) => [ result.displayName, result.description, ...(showMarketplace ? [result.marketplaceDisplayName] : []), + ...(showInstalls + ? [ + result.installs === null + ? "" + : result.installs.toLocaleString("en-US"), + ] + : []), result.installed ? "✓ installed" : result.compatible @@ -929,9 +939,16 @@ export function registerPluginCommands( "Name", "Description", ...(showMarketplace ? ["Marketplace"] : []), + ...(showInstalls ? ["Installs"] : []), "Status", ], - colWidths: showMarketplace ? [26, 42, 22, 40] : [28, 54, 48], + colWidths: [ + showMarketplace ? 26 : 28, + showMarketplace ? 42 : 54, + ...(showMarketplace ? [22] : []), + ...(showInstalls ? [10] : []), + showMarketplace ? 40 : 48, + ], trimTrailingWhitespace: true, }, rows, @@ -1086,9 +1103,8 @@ export function registerPluginCommands( // moved, not refused; name the install being replaced so // the confirmation is about the move, not a fresh install. const pluginId = derivePluginId(pkg.name); - const { plugins } = await createCliBbSdk( - getUrl(), - ).plugins.list(); + const { plugins } = + await createCliBbSdk(getUrl()).plugins.list(); const installed = plugins.find((p) => p.id === pluginId); if ( installed !== undefined && diff --git a/apps/mobile/src/screens/plugins/PluginBrowseScreen.tsx b/apps/mobile/src/screens/plugins/PluginBrowseScreen.tsx index 03f98de77a..8103d8fef7 100644 --- a/apps/mobile/src/screens/plugins/PluginBrowseScreen.tsx +++ b/apps/mobile/src/screens/plugins/PluginBrowseScreen.tsx @@ -26,9 +26,21 @@ import { AddPluginSheet } from "./AddPluginSheet"; import { SettingsSection } from "./plugin-ui"; import { PluginIcon } from "./ServerSvgIcon"; +/** Store counts are read at a glance: "1.2k installs", not the exact number. */ +const INSTALL_COUNT_FORMATTER = new Intl.NumberFormat(undefined, { + notation: "compact", + maximumFractionDigits: 1, +}); + function entrySubtitle(entry: PluginCatalogSearchResult): string { const parts = [entry.category]; if (!entry.official) parts.push(entry.marketplaceDisplayName); + // Null for every third-party listing, which publishes no counts. + if (entry.installs !== null) { + parts.push( + `${INSTALL_COUNT_FORMATTER.format(entry.installs)} ${entry.installs === 1 ? "install" : "installs"}`, + ); + } if (!entry.compatible) { parts.push(entry.incompatibleReason ?? "Incompatible with this bb"); } From a9d96dd6d785e187f04ad3de343770b85357312f Mon Sep 17 00:00:00 2001 From: Andrew Chan Date: Sat, 22 Aug 2026 00:31:00 -0700 Subject: [PATCH 6/7] docs: document the install-count sidecar The plan doc gets the format and the reasoning: why a sidecar rather than a manifest field, why its parser is lenient where the manifest's is strict, why only the curated marketplace publishes one, and that the count undercounts because telemetry is opt-out and production-only. The guide chapter and the bb-cli skill get the user-facing half, since `bb plugin search` grew a column. Both say plainly that the number is what BB heard about, not a true total. > AGENT GENERATED --- .../skills/builtin-skills/bb-cli/SKILL.md | 5 ++- docs/plugin-marketplace-plan.md | 37 +++++++++++++++++++ .../src/templates/bb-guide-plugins.md | 9 +++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md b/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md index 017f1243a0..71dfbb962c 100644 --- a/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md +++ b/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md @@ -789,7 +789,10 @@ them by mixing ink into canvas), the `--primary` accent, the secondary text tier plugin code, and a failed refresh keeps the last catalog bb validated. - `bb plugin search [--json]` — search the official plugins by id, name, description, category, or tag; status shows installed / compatible / - requires newer bb. + requires newer bb. An **Installs** column appears once the curated + marketplace's `stats.json` sidecar has been read (`installs` in `--json`, + null when unknown): anonymous-telemetry install counts, published only for + bundled plugins and `bb-community` entries, never for third-party ones. - **Third-party marketplaces** (routes under `/api/v1/marketplaces`): - `bb marketplace add ` — add a marketplace from an https manifest URL, `git:[@]` (bb reads `marketplace.json` from the checkout), diff --git a/docs/plugin-marketplace-plan.md b/docs/plugin-marketplace-plan.md index 4c9b2e91f2..b477cbf41c 100644 --- a/docs/plugin-marketplace-plan.md +++ b/docs/plugin-marketplace-plan.md @@ -191,6 +191,43 @@ This applies the conclusions from the Go modules discussion: - Plugin ID collisions across marketplaces resolve through `id@marketplace` install routing. A conflicting installed plugin ID is refused, as today. +## Install counts + +The curated marketplace publishes a second document beside its manifest, +`stats.json`, and BB shows the number on the store card, the mobile browse +row, and in `bb plugin search`: + +```json +{ + "schemaVersion": 1, + "generatedAt": "2026-08-21T06:17:00.000Z", + "plugins": { "thread-hover-cards": { "installs": 4210 } } +} +``` + +- The counts are BB's own measurement, from the `plugin_installed` telemetry + event (`apps/server/src/services/system/telemetry.ts`), which already + carries a `plugin_id` for bundled plugins and `bb-community` entries and + null for everything private. A daily job in the registry repo queries + PostHog and uploads the file to the same R2 prefix as the manifest; a run + that finds no counts fails without uploading rather than zeroing the store. +- A sidecar, not a manifest field. The manifest schema is strict, so an + unknown field there would reject the whole catalog on an older desktop and + need a `schemaVersion` bump; and the counts move daily while the manifest + sits unchanged behind a 304. +- Its parser is deliberately not strict, unlike the manifest's: this is + display metadata a later publisher may extend, and losing every count over + an unknown field is worse than ignoring the field. A malformed document is + still rejected whole — half-parsed counts are worse than none. +- Fetched unconditionally on every refresh, and never allowed to fail one. A + failure keeps the counts already stored, exactly as a failed manifest read + keeps the last-known-good catalog. +- Only the curated marketplace is asked for a sidecar. A number beside a + third-party listing would be that publisher's claim wearing BB's label, so + those entries report `installs: null` and BB does not request the file. +- The count undercounts by construction: telemetry is opt-out and only + production builds report. Present it as installs BB heard about. + ## Provenance Generalize the current `builtin | direct | catalog` enum: keep `catalog` as diff --git a/packages/templates/src/templates/bb-guide-plugins.md b/packages/templates/src/templates/bb-guide-plugins.md index 1c829dc15e..78be699d0d 100644 --- a/packages/templates/src/templates/bb-guide-plugins.md +++ b/packages/templates/src/templates/bb-guide-plugins.md @@ -347,6 +347,15 @@ fetched, validated, and served by the bb server, so the app never requests a marketplace URL. Installing an entry runs the normal install pipeline against its listed git or npm source and records which marketplace listed it. +The BB Community marketplace also publishes install counts beside its +manifest, at https://getbb.app/marketplace/v1/stats.json. bb re-reads that +file on every refresh — the counts move while the manifest sits unchanged — +and shows them in the store and in the Installs column of `bb plugin search`. +The number is how many BB installations reported installing the plugin +through anonymous telemetry, so it undercounts: telemetry is opt-out and only +production builds report. No third-party marketplace has counts; bb measures +them itself rather than repeating a publisher's claim. + Third-party marketplaces Anyone can host a marketplace manifest. Add one with its https manifest URL, From 6c9dcdfbddb326a2d047ffb7b5d3ae62f6484789 Mon Sep 17 00:00:00 2001 From: Andrew Chan Date: Sat, 22 Aug 2026 02:04:48 -0700 Subject: [PATCH 7/7] bump plugin sdk --- packages/domain/src/plugin-sdk-version.ts | 2 +- packages/plugin-sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/domain/src/plugin-sdk-version.ts b/packages/domain/src/plugin-sdk-version.ts index 7cdc4520d6..132f087726 100644 --- a/packages/domain/src/plugin-sdk-version.ts +++ b/packages/domain/src/plugin-sdk-version.ts @@ -16,7 +16,7 @@ // PLUGIN_SDK_MAJOR is 0, so the major-only artifact gate cannot distinguish // 0.x releases and is intentionally vacuous for them until a future 1.0. // Rebuildable artifacts still rebuild on the exact sdkVersion-differs trigger. -export const PLUGIN_SDK_VERSION = "0.4.14"; +export const PLUGIN_SDK_VERSION = "0.4.15"; /** Major of {@link PLUGIN_SDK_VERSION} — the plugin API compatibility number. */ export const PLUGIN_SDK_MAJOR = Number(PLUGIN_SDK_VERSION.split(".", 1)[0]); diff --git a/packages/plugin-sdk/package.json b/packages/plugin-sdk/package.json index e7a8f0b5f6..578b9a0846 100644 --- a/packages/plugin-sdk/package.json +++ b/packages/plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@get-bb/plugin-sdk", - "version": "0.4.14", + "version": "0.4.15", "homepage": "https://github.com/get-bb/bb#readme", "bugs": { "url": "https://github.com/get-bb/bb/issues"