Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/contributing/architecture/data-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,9 @@ The schema is defined by migrations in `packages/worker/migrations/`:
- `entity_sources`: durable mapping from user-facing entities to Artifacts repos
and their latest published commit
- `saved_packages`: package metadata/search projection derived from published
`package.json` source
`package.json` source, plus a user-scoped `hidden` flag (0/1) that excludes
the package from default ranked search while leaving list/get/execute paths
intact
- `secret_buckets`: encrypted-secret ownership buckets scoped to `user`,
`package`, or `session`. Package buckets bind directly to `saved_packages.id`;
package runtimes may use their own package secrets, while user secrets require
Expand Down
8 changes: 8 additions & 0 deletions docs/contributing/packages-and-manifests.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,3 +415,11 @@ Search returns packages as the saved-entity unit.
Package detail should expose nested exports, nested jobs, tags, and app
presence. Search should not frame exports or jobs as separate top-level saved
entities.

Saved packages carry a user-scoped **`hidden`** flag in `saved_packages` (set
via **`package_set_hidden`**). Ranked search excludes hidden packages by
default. The public MCP **search** tool and the **meta** domain **search**
capability both accept **`includeHiddenPackages`**. Known-id entity lookup,
**`package_list`**, **`package_get`**, and context-scope package retrievers are
unaffected. Hiding is not deletion, community delisting, or entitlement
exclusion.
16 changes: 16 additions & 0 deletions docs/use/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,9 +374,25 @@ Use:
- `package_save` to create or replace a saved package from a complete UTF-8 text
file set when no local git client is available
- `package_get` and `package_list` to inspect saved packages
- `package_set_hidden` to hide or unhide a package from default search discovery
- `repo_run_commands` to edit, check, and publish repo-backed package source
after it exists using parsed, git-only command forms rather than shell

## Hidden packages

Use **`package_set_hidden`** with a saved **`package_id`** and
**`hidden: true`** to hide a package from ordinary ranked search. Set
**`hidden: false`** to show it again.

Hiding is a discovery preference, not deletion. The package stays saved,
executable, and editable. Hiding is separate from **`package.json#private`**
(community publishing) and from entitlement or access grants.

**`package_list`** and **`package_get`** return a **`hidden`** boolean on each
package summary. Ranked **search** excludes hidden packages unless the caller
passes **`includeHiddenPackages: true`**. Known-id **`entity`** lookups still
resolve hidden packages.

## Author a saved package via direct git push

Saved package source is backed by a Cloudflare Artifacts git repository. You can
Expand Down
7 changes: 7 additions & 0 deletions docs/use/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ The **search** tool finds **built-in capabilities**, **saved packages**,
(`community_search`, `community_get`) or the public `/community` pages. See
[Community packages](./community-packages.md).

**Hidden saved packages** are excluded from ranked **query** results by default.
Pass **`includeHiddenPackages: true`** to include them. Hiding is not deletion:
known-id **`entity`** lookups (for example `my-package:package`),
**`package_list`**, and **`package_get`** still work. Use
**`package_set_hidden`** to hide or unhide a package. See
[Packages](./packages.md#hidden-packages).

## Queries and ranking

Pass a **`query`** string that describes what you want to do. Results are
Expand Down
2 changes: 2 additions & 0 deletions packages/worker/migrations/0058-saved-packages-hidden.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE saved_packages
ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0 CHECK (hidden IN (0, 1));
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const mockModule = vi.hoisted(() => ({
searchText: null,
sourceId: 'source-1',
hasApp: false,
hidden: false,
createdAt: new Date(0).toISOString(),
updatedAt: new Date(0).toISOString(),
},
Expand Down
2 changes: 2 additions & 0 deletions packages/worker/src/app/handlers/account-secrets.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ test('account secrets payload includes all packages and package titles and allow
searchText: null,
sourceId: 'source-1',
hasApp: true,
hidden: false,
createdAt: new Date(0).toISOString(),
updatedAt: new Date(0).toISOString(),
},
Expand All @@ -501,6 +502,7 @@ test('account secrets payload includes all packages and package titles and allow
searchText: null,
sourceId: 'source-2',
hasApp: false,
hidden: false,
createdAt: new Date(0).toISOString(),
updatedAt: new Date(0).toISOString(),
},
Expand Down
1 change: 1 addition & 0 deletions packages/worker/src/app/handlers/package-app.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const mockModule = vi.hoisted(() => ({
searchText: null,
sourceId: 'source-1',
hasApp: true,
hidden: false,
createdAt: new Date(0).toISOString(),
updatedAt: new Date(0).toISOString(),
})),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export async function ensureCommunityFlowSchema(db: D1Database) {
search_text TEXT,
source_id TEXT NOT NULL,
has_app INTEGER NOT NULL DEFAULT 0 CHECK (has_app IN (0, 1)),
hidden INTEGER NOT NULL DEFAULT 0 CHECK (hidden IN (0, 1)),
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP)
)`,
Expand Down
2 changes: 2 additions & 0 deletions packages/worker/src/community/community-service.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ function validSavedPackage() {
searchText: null,
sourceId: 'source-1',
hasApp: false,
hidden: false,
createdAt: '2026-07-01T00:00:00.000Z',
updatedAt: '2026-07-01T00:00:00.000Z',
}
Expand Down Expand Up @@ -555,6 +556,7 @@ test('publishCommunityListing requires MIT license and Intent heading', async ()
searchText: null,
sourceId: 'source-1',
hasApp: false,
hidden: false,
createdAt: '2026-07-01T00:00:00.000Z',
updatedAt: '2026-07-01T00:00:00.000Z',
})
Expand Down
1 change: 1 addition & 0 deletions packages/worker/src/email/inbound.workers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,7 @@ test('inbound email handler dispatches package subscriptions for stored inbound
search_text TEXT,
source_id TEXT NOT NULL,
has_app INTEGER NOT NULL DEFAULT 0,
hidden INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ async function ensurePackageSubscriptionTestSchema(db: D1Database) {
search_text TEXT,
source_id TEXT NOT NULL,
has_app INTEGER NOT NULL DEFAULT 0,
hidden INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { expect, test, vi } from 'vitest'
import { createMcpCallerContext } from '#mcp/context.ts'

const mockModule = vi.hoisted(() => ({
loadSearchRowsAndRegistry: vi.fn(),
resolveSearchMemoryContext: vi.fn(
({ query, memoryContext }: { query: string; memoryContext?: unknown }) =>
memoryContext ?? { query },
),
searchUnified: vi.fn(),
loadDownRemoteConnectorStatuses: vi.fn(async () => []),
serializeRemoteConnectorStatus: vi.fn((status: unknown) => status),
runPackageRetrievers: vi.fn(),
loadRelevantMemoriesForTool: vi.fn(async () => null),
toSlimStructuredMatches: vi.fn(
({ matches }: { matches: unknown }) => matches,
),
}))

vi.mock('#mcp/tools/search.ts', () => ({
loadSearchRowsAndRegistry: (...args: Array<unknown>) =>
mockModule.loadSearchRowsAndRegistry(...args),
resolveSearchMemoryContext: (...args: Array<unknown>) =>
mockModule.resolveSearchMemoryContext(...args),
searchUnified: (...args: Array<unknown>) => mockModule.searchUnified(...args),
loadDownRemoteConnectorStatuses: (...args: Array<unknown>) =>
mockModule.loadDownRemoteConnectorStatuses(...args),
serializeRemoteConnectorStatus: (...args: Array<unknown>) =>
mockModule.serializeRemoteConnectorStatus(...args),
}))

vi.mock('#worker/package-retrievers/service.ts', () => ({
runPackageRetrievers: (...args: Array<unknown>) =>
mockModule.runPackageRetrievers(...args),
}))

vi.mock('#mcp/tools/memory-tool-context.ts', () => ({
loadRelevantMemoriesForTool: (...args: Array<unknown>) =>
mockModule.loadRelevantMemoriesForTool(...args),
}))

vi.mock('#mcp/tools/search-format.ts', () => ({
toSlimStructuredMatches: (...args: Array<unknown>) =>
mockModule.toSlimStructuredMatches(...args),
}))

const { searchCapability } = await import('./search.ts')

function createCtx() {
return {
env: { APP_DB: {} } as Env,
callerContext: createMcpCallerContext({
baseUrl: 'https://example.com',
user: {
userId: 'user-1',
email: 'user@example.com',
displayName: 'User',
},
}),
}
}

function resetMocks() {
mockModule.loadSearchRowsAndRegistry.mockReset()
mockModule.resolveSearchMemoryContext.mockClear()
mockModule.searchUnified.mockReset()
mockModule.loadDownRemoteConnectorStatuses.mockClear()
mockModule.runPackageRetrievers.mockReset()
mockModule.loadRelevantMemoriesForTool.mockClear()
mockModule.toSlimStructuredMatches.mockClear()

mockModule.loadSearchRowsAndRegistry.mockResolvedValue({
registry: { capabilitySpecs: {} },
packageRows: [],
userSecretRows: [],
userValueRows: [],
warnings: [],
})
mockModule.runPackageRetrievers.mockResolvedValue({
results: [],
warnings: [],
})
mockModule.searchUnified.mockResolvedValue({
matches: [],
offline: true,
})
}

test('meta search remaps includeHiddenPackages through to package rows and search-scope retrievers', async () => {
resetMocks()
const ctx = createCtx()

await searchCapability.handler({ query: 'notes' }, ctx)

expect(mockModule.loadSearchRowsAndRegistry).toHaveBeenCalledWith(
expect.objectContaining({
includeHiddenPackages: false,
}),
)
expect(mockModule.runPackageRetrievers).toHaveBeenCalledWith(
expect.objectContaining({
scope: 'search',
includeHiddenPackages: false,
}),
)

resetMocks()
await searchCapability.handler(
{ query: 'notes', includeHiddenPackages: true },
ctx,
)

expect(mockModule.loadSearchRowsAndRegistry).toHaveBeenCalledWith(
expect.objectContaining({
includeHiddenPackages: true,
}),
)
expect(mockModule.runPackageRetrievers).toHaveBeenCalledWith(
expect.objectContaining({
scope: 'search',
includeHiddenPackages: true,
}),
)
})
19 changes: 18 additions & 1 deletion packages/worker/src/mcp/capabilities/meta/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@ function normalizeLimit(limit: number | undefined) {
async function loadSearchRows(input: {
ctx: CapabilityContext
userId: string | null
includeHiddenPackages: boolean
}) {
const { loadSearchRowsAndRegistry } = await import('#mcp/tools/search.ts')
return await loadSearchRowsAndRegistry({
env: input.ctx.env,
callerContext: input.ctx.callerContext,
userId: input.userId,
includeHiddenPackages: input.includeHiddenPackages,
})
}

Expand All @@ -63,6 +65,7 @@ async function runPackageRetrieverSearch(input: {
userId: string | null
query: string
conversationId: string
includeHiddenPackages: boolean
memoryContext?: z.infer<typeof memoryContextInputField>
}) {
if (!input.userId || !input.query) {
Expand All @@ -77,6 +80,7 @@ async function runPackageRetrieverSearch(input: {
userId: input.userId,
scope: 'search',
query: input.query,
includeHiddenPackages: input.includeHiddenPackages,
memoryContext: resolveSearchMemoryContext({
query: input.query,
memoryContext: input.memoryContext,
Expand Down Expand Up @@ -117,6 +121,12 @@ export const searchCapability = defineDomainCapability(
.describe('Max number of ranked results to return. Defaults to 15.'),
conversationId: conversationIdInputField,
memoryContext: memoryContextInputField,
includeHiddenPackages: z
.boolean()
.optional()
.describe(
'Include hidden packages in search results (hidden packages are excluded by default).',
),
}),
outputSchema: searchOutputSchema,
async handler(
Expand All @@ -125,6 +135,7 @@ export const searchCapability = defineDomainCapability(
limit?: number
conversationId?: string
memoryContext?: z.infer<typeof memoryContextInputField>
includeHiddenPackages?: boolean
},
ctx: CapabilityContext,
) {
Expand All @@ -134,13 +145,19 @@ export const searchCapability = defineDomainCapability(
}
const conversationId = resolveConversationId(args.conversationId)
const userId = ctx.callerContext.user?.userId ?? null
const includeHiddenPackages = !!args.includeHiddenPackages
const [searchRows, retrieverRun] = await Promise.all([
loadSearchRows({ ctx, userId }),
loadSearchRows({
ctx,
userId,
includeHiddenPackages,
}),
runPackageRetrieverSearch({
ctx,
userId,
query,
conversationId,
includeHiddenPackages,
memoryContext: args.memoryContext,
}),
])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ test('requires package approval before OpenAPI resolves a user secret', async ()
searchText: null,
sourceId: 'source-1',
hasApp: false,
hidden: false,
createdAt: '2026-07-09T00:00:00.000Z',
updatedAt: '2026-07-09T00:00:00.000Z',
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ test('createStubSavedPackage rejects invalid kody ids and registers valid stubs
description: 'Does the thing.',
source_id: 'source-new',
has_app: 0,
hidden: 0,
}),
)
expect(mockModule.upsertSavedPackageVector).toHaveBeenCalled()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export async function createStubSavedPackage(input: {
search_text: manifest.kody.searchText ?? null,
source_id: ensuredSource.id,
has_app: 0,
hidden: 0,
created_at: now,
updated_at: now,
})
Expand Down
2 changes: 2 additions & 0 deletions packages/worker/src/mcp/capabilities/packages/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { packageInvocationTokenGetCapability } from './package-invocation-token-
import { packageInvocationTokenListCapability } from './package-invocation-token-list.ts'
import { publishExternalPushCapability } from './publish-external-push.ts'
import { savePackageCapability } from './save-package.ts'
import { setPackageHiddenCapability } from './set-package-hidden.ts'

export const packagesDomain = defineDomain({
name: capabilityDomainNames.packages,
Expand All @@ -33,6 +34,7 @@ export const packagesDomain = defineDomain({
getGitRemoteCapability,
listPackagesCapability,
listPackageSubscriptionsCapability,
setPackageHiddenCapability,
packageInvocationTokenListCapability,
packageInvocationTokenGetCapability,
packageDebugListRunsCapability,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ test('getPackageCapability returns export metadata and omits external invocation
searchText: null,
sourceId: 'source-1',
hasApp: true,
hidden: false,
createdAt: '2026-04-25T00:00:00.000Z',
updatedAt: '2026-04-26T00:00:00.000Z',
})
Expand Down Expand Up @@ -168,6 +169,7 @@ test('getPackageCapability returns export metadata and omits external invocation
searchText: null,
sourceId: 'source-1',
hasApp: false,
hidden: false,
createdAt: '2026-04-25T00:00:00.000Z',
updatedAt: '2026-04-26T00:00:00.000Z',
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const getPackageCapability = defineDomainCapability(
description: saved.description,
tags: saved.tags,
has_app: saved.hasApp,
hidden: saved.hidden,
source_id: saved.sourceId,
created_at: saved.createdAt,
updated_at: saved.updatedAt,
Expand Down
Loading
Loading