diff --git a/src/functions/backend/db/extensions/grab-tools-overview.ts b/src/functions/backend/db/extensions/grab-tools-overview.ts index ef7722d..e748733 100644 --- a/src/functions/backend/db/extensions/grab-tools-overview.ts +++ b/src/functions/backend/db/extensions/grab-tools-overview.ts @@ -37,6 +37,7 @@ export default async function grabToolsOverview({ user_id }: Params): Promise>({ text: row.config, fallback: {} }), } as CustomToolRecord; diff --git a/src/functions/backend/db/extensions/parse-mcp-server-row.ts b/src/functions/backend/db/extensions/parse-mcp-server-row.ts index e7a5827..35b4694 100644 --- a/src/functions/backend/db/extensions/parse-mcp-server-row.ts +++ b/src/functions/backend/db/extensions/parse-mcp-server-row.ts @@ -29,5 +29,6 @@ export default function parseMcpServerRow({ row }: Params): McpServerRecord { status: (row.status || "untested") as McpServerStatus, last_error: row.last_error || "", last_synced_at: typeof row.last_synced_at == "number" ? row.last_synced_at : null, + updated_at: typeof row.updated_at == "number" ? row.updated_at : null, }; } diff --git a/src/functions/backend/db/extensions/parse-skill-row.ts b/src/functions/backend/db/extensions/parse-skill-row.ts index d0a4bd4..5ed06bb 100644 --- a/src/functions/backend/db/extensions/parse-skill-row.ts +++ b/src/functions/backend/db/extensions/parse-skill-row.ts @@ -17,5 +17,6 @@ export default function parseSkillRow({ row }: Params): SkillRecord { body: row.body || "", all_bots: row.all_bots == 1, enabled: row.enabled != 0, + updated_at: typeof row.updated_at == "number" ? row.updated_at : null, }; } diff --git a/src/functions/backend/db/extensions/record-custom-tool-run.ts b/src/functions/backend/db/extensions/record-custom-tool-run.ts index 757a887..b57a2e5 100644 --- a/src/functions/backend/db/extensions/record-custom-tool-run.ts +++ b/src/functions/backend/db/extensions/record-custom-tool-run.ts @@ -11,13 +11,12 @@ type Params = { /** * Saves how a custom tool's run went (`status`, `last_error`, * `last_checked_at`), for its row in Settings, under Tools. Never throws: - * a run doesn't fail over its bookkeeping. + * a run doesn't fail over its bookkeeping. `updated_at` is left alone: it + * says when the tool's settings last changed. */ export default async function recordCustomToolRun({ custom_tool_id, error }: Params) { - const now = Date.now(); - await BunSQLite.sql({ - sql: `UPDATE custom_tools SET status = ?, last_error = ?, last_checked_at = ?, updated_at = ? WHERE id = ?`, - values: [error ? "error" : "ok", (error || "").slice(0, 4000), now, now, custom_tool_id], + sql: `UPDATE custom_tools SET status = ?, last_error = ?, last_checked_at = ? WHERE id = ?`, + values: [error ? "error" : "ok", (error || "").slice(0, 4000), Date.now(), custom_tool_id], }).catch(() => {}); } diff --git a/src/functions/backend/db/extensions/record-mcp-server-connection.ts b/src/functions/backend/db/extensions/record-mcp-server-connection.ts index 4144ed4..b438279 100644 --- a/src/functions/backend/db/extensions/record-mcp-server-connection.ts +++ b/src/functions/backend/db/extensions/record-mcp-server-connection.ts @@ -11,13 +11,12 @@ type Params = { /** * Saves whether a bot's run could connect to an MCP server, so its row in * Settings, under MCPs, shows a server that stopped answering (or came - * back). Its tools listing is left as it was. Never throws. + * back). Its tools listing and `updated_at` (when its settings last + * changed) are left as they were. Never throws. */ export default async function recordMcpServerConnection({ mcp_server_id, error }: Params) { - const now = Date.now(); - await BunSQLite.sql({ - sql: `UPDATE mcp_servers SET status = ?, last_error = ?, last_synced_at = ?, updated_at = ? WHERE id = ?`, - values: [error ? "error" : "ok", (error || "").slice(0, 4000), now, now, mcp_server_id], + sql: `UPDATE mcp_servers SET status = ?, last_error = ?, last_synced_at = ? WHERE id = ?`, + values: [error ? "error" : "ok", (error || "").slice(0, 4000), Date.now(), mcp_server_id], }).catch(() => {}); } diff --git a/src/functions/backend/db/extensions/set-extension-access.ts b/src/functions/backend/db/extensions/set-extension-access.ts index bd966db..f88fd24 100644 --- a/src/functions/backend/db/extensions/set-extension-access.ts +++ b/src/functions/backend/db/extensions/set-extension-access.ts @@ -21,7 +21,8 @@ type Params = { * super admins and admins. An admin never sees the coordinator, so its * attachment is kept as it is; only a super admin sets "every bot", which * when turned on gives it to every bot (`attachToAllBots`) and every new - * one. Turning it off takes it from no one. + * one. Turning it off takes it from no one. Either way it counts as a + * change to the extension (`updated_at`). */ export default async function setExtensionAccess({ user_id, kind, id, bot_ids, workspace_ids, all_bots }: Params) { const role = await assertCrewManager({ user_id }); @@ -63,5 +64,7 @@ export default async function setExtensionAccess({ user_id, kind, id, bot_ids, w }); if (all_bots && found.payload?.[0]?.all_bots != 1) await attachToAllBots({ kind, id }); + } else { + await BunSQLite.sql({ sql: `UPDATE ${table} SET updated_at = ? WHERE id = ?`, values: [now, id] }); } } diff --git a/src/functions/backend/db/extensions/sync-mcp-server.ts b/src/functions/backend/db/extensions/sync-mcp-server.ts index b70ba53..99a116f 100644 --- a/src/functions/backend/db/extensions/sync-mcp-server.ts +++ b/src/functions/backend/db/extensions/sync-mcp-server.ts @@ -37,8 +37,8 @@ export default async function syncMcpServer({ mcp_server_id, reinstall }: Params const tools = await listMcpTools({ client: await open.client() }); await BunSQLite.sql({ - sql: `UPDATE mcp_servers SET tools_cache = ?, status = 'ok', last_error = '', last_synced_at = ?, updated_at = ? WHERE id = ?`, - values: [JSON.stringify(tools), now, now, server.id], + sql: `UPDATE mcp_servers SET tools_cache = ?, status = 'ok', last_error = '', last_synced_at = ? WHERE id = ?`, + values: [JSON.stringify(tools), now, server.id], }); return { ok: true as const, tools }; @@ -49,8 +49,8 @@ export default async function syncMcpServer({ mcp_server_id, reinstall }: Params }).slice(0, 4000); await BunSQLite.sql({ - sql: `UPDATE mcp_servers SET status = 'error', last_error = ?, last_synced_at = ?, updated_at = ? WHERE id = ?`, - values: [message, now, now, server.id], + sql: `UPDATE mcp_servers SET status = 'error', last_error = ?, last_synced_at = ? WHERE id = ?`, + values: [message, now, server.id], }); return { ok: false as const, error: message }; diff --git a/src/functions/backend/db/extensions/test-mcp-draft.ts b/src/functions/backend/db/extensions/test-mcp-draft.ts index 262e8d2..d3f4a47 100644 --- a/src/functions/backend/db/extensions/test-mcp-draft.ts +++ b/src/functions/backend/db/extensions/test-mcp-draft.ts @@ -39,6 +39,7 @@ export default async function testMcpDraft({ status: "untested", last_error: "", last_synced_at: null, + updated_at: null, }, envs, }); diff --git a/src/harness/tools/bot/extensions/merge-custom-tool-changes.test.ts b/src/harness/tools/bot/extensions/merge-custom-tool-changes.test.ts index bce81f8..f72d275 100644 --- a/src/harness/tools/bot/extensions/merge-custom-tool-changes.test.ts +++ b/src/harness/tools/bot/extensions/merge-custom-tool-changes.test.ts @@ -12,6 +12,7 @@ const weather: CustomToolRecord = { status: "ok", last_error: "", last_checked_at: null, + updated_at: null, kind: "http", config: { method: "GET", diff --git a/src/harness/tools/skills/open-bot-skills.test.ts b/src/harness/tools/skills/open-bot-skills.test.ts index c009a90..17d0b08 100644 --- a/src/harness/tools/skills/open-bot-skills.test.ts +++ b/src/harness/tools/skills/open-bot-skills.test.ts @@ -12,6 +12,7 @@ function skill(overrides: Partial & { slug: string }): SkillRecord body: `${overrides.slug} body`, all_bots: false, enabled: true, + updated_at: null, ...overrides, }; } diff --git a/src/pages/admin/settings/(utils)/updated-ago.ts b/src/pages/admin/settings/(utils)/updated-ago.ts new file mode 100644 index 0000000..548ede0 --- /dev/null +++ b/src/pages/admin/settings/(utils)/updated-ago.ts @@ -0,0 +1,13 @@ +import checkedAgo from "./checked-ago"; + +type Params = { + at: number | null; +}; + +/** + * When an extension's settings last changed, for a row's subtitle: + * "Updated 5m ago". Nothing when it's never been stamped. + */ +export default function updatedAgo({ at }: Params) { + return at ? `Updated ${checkedAgo({ at })}` : undefined; +} diff --git a/src/pages/admin/settings/mcps/(partials)/mcp-server-row.tsx b/src/pages/admin/settings/mcps/(partials)/mcp-server-row.tsx index 5d23bdf..522fac4 100644 --- a/src/pages/admin/settings/mcps/(partials)/mcp-server-row.tsx +++ b/src/pages/admin/settings/mcps/(partials)/mcp-server-row.tsx @@ -11,6 +11,7 @@ import SettingsListRow from "../../(partials)/settings-list-row"; import SettingsRowAction from "../../(partials)/settings-row-action"; import checkedAgo from "../../(utils)/checked-ago"; import extensionAccessLabel from "../../(utils)/extension-access-label"; +import updatedAgo from "../../(utils)/updated-ago"; import McpServerModal from "./mcp-server-modal"; type Props = { @@ -63,6 +64,7 @@ export default function McpServerRow({ server, targets, onChanged, onError }: Pr subtitle={[ McpTransports.find((t) => t.value == server.transport)?.title, extensionAccessLabel({ access: server.access }), + updatedAgo({ at: server.updated_at }), ] .filter(Boolean) .join(" · ")} diff --git a/src/pages/admin/settings/skills/(partials)/skill-row.tsx b/src/pages/admin/settings/skills/(partials)/skill-row.tsx index 654ae0b..d2fc7e2 100644 --- a/src/pages/admin/settings/skills/(partials)/skill-row.tsx +++ b/src/pages/admin/settings/skills/(partials)/skill-row.tsx @@ -8,6 +8,7 @@ import ExtensionAccessModal from "../../(partials)/extension-access-modal"; import SettingsListRow from "../../(partials)/settings-list-row"; import SettingsRowAction from "../../(partials)/settings-row-action"; import extensionAccessLabel from "../../(utils)/extension-access-label"; +import updatedAgo from "../../(utils)/updated-ago"; import SkillModal from "./skill-modal"; type Props = { @@ -51,7 +52,12 @@ export default function SkillRow({ skill, targets, limits, onChanged, onError }: avatar={} title={skill.name} badge={skill.enabled ? undefined : "Off"} - subtitle={[skill.slug, extensionAccessLabel({ access: skill.access }), skill.description] + subtitle={[ + skill.slug, + extensionAccessLabel({ access: skill.access }), + updatedAgo({ at: skill.updated_at }), + skill.description, + ] .filter(Boolean) .join(" · ")} > diff --git a/src/pages/admin/settings/tools/(partials)/custom-tool-row.tsx b/src/pages/admin/settings/tools/(partials)/custom-tool-row.tsx index b6e56bd..64c0107 100644 --- a/src/pages/admin/settings/tools/(partials)/custom-tool-row.tsx +++ b/src/pages/admin/settings/tools/(partials)/custom-tool-row.tsx @@ -11,6 +11,7 @@ import SettingsListRow from "../../(partials)/settings-list-row"; import SettingsRowAction from "../../(partials)/settings-row-action"; import checkedAgo from "../../(utils)/checked-ago"; import extensionAccessLabel from "../../(utils)/extension-access-label"; +import updatedAgo from "../../(utils)/updated-ago"; import CustomToolModal from "./custom-tool-modal"; import CustomToolTryModal from "./custom-tool-try-modal"; @@ -72,6 +73,7 @@ export default function CustomToolRow({ tool, targets, onChanged, onRefreshed, o subtitle={[ CustomToolKinds.find((k) => k.value == tool.kind)?.title, extensionAccessLabel({ access: tool.access }), + updatedAgo({ at: tool.updated_at }), tool.description, ] .filter(Boolean) diff --git a/src/types/index.ts b/src/types/index.ts index febf2b8..8ffdcb4 100755 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -752,6 +752,10 @@ export type McpServerRecord = { status: McpServerStatus; last_error: string; last_synced_at: number | null; + /** + * When its settings last changed (an edit or who gets it), not a run or check. + */ + updated_at: number | null; }; export type CustomToolHttpConfig = { @@ -785,6 +789,10 @@ export type CustomToolRecord = { status: CustomToolStatus; last_error: string; last_checked_at: number | null; + /** + * When its settings last changed (an edit or who gets it), not a run or check. + */ + updated_at: number | null; } & ( | { kind: "http"; config: CustomToolHttpConfig } | { kind: "script"; config: CustomToolScriptConfig } @@ -802,6 +810,10 @@ export type SkillRecord = { body: string; all_bots: boolean; enabled: boolean; + /** + * When its settings last changed (an edit or who gets it), not a run or check. + */ + updated_at: number | null; }; /** @@ -943,6 +955,7 @@ export type PublicCustomTool = { status: CustomToolStatus; last_error: string; last_checked_at: number | null; + updated_at: number | null; access: ExtensionAccess; parameters?: Record; config?: CustomToolHttpConfig | CustomToolScriptConfig | CustomToolPromptConfig; diff --git a/src/utils/describe-skill-proposal.test.ts b/src/utils/describe-skill-proposal.test.ts index 4e9cb3d..4d8f5aa 100644 --- a/src/utils/describe-skill-proposal.test.ts +++ b/src/utils/describe-skill-proposal.test.ts @@ -10,6 +10,7 @@ const current: SkillRecord = { body: "1. Search the client's rankings.\n2. Write the report.\n3. Attach it.", all_bots: false, enabled: true, + updated_at: null, }; describe("describeSkillProposal", () => { diff --git a/src/utils/skill-proposal-changes.test.ts b/src/utils/skill-proposal-changes.test.ts index 6d46c9a..22e0423 100644 --- a/src/utils/skill-proposal-changes.test.ts +++ b/src/utils/skill-proposal-changes.test.ts @@ -10,6 +10,7 @@ const current: SkillRecord = { body: "1. Install Baileys.\n2. Pair.", all_bots: false, enabled: true, + updated_at: null, }; describe("skillProposalChanges", () => {