Skip to content
Merged

Dev #16

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
12 changes: 12 additions & 0 deletions .github/workflows/sync-catalog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,15 @@ jobs:
env:
WARP_GITHUB_SYNC_TOKEN: ${{ secrets.WARP_GITHUB_SYNC_TOKEN }}
run: node --import tsx scripts/catalog/sync-to-api.ts --branch "${GITHUB_REF_NAME}" --commit "${GITHUB_SHA}" --repo "${GITHUB_REPOSITORY}"

- name: Dispatch plugin sync
env:
JOAI_PLUGIN_SYNC_TOKEN: ${{ secrets.JOAI_PLUGIN_SYNC_TOKEN }}
if: ${{ env.JOAI_PLUGIN_SYNC_TOKEN != '' }}
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ env.JOAI_PLUGIN_SYNC_TOKEN }}
repository: JoAiHQ/joai--plugins
event-type: warps_catalog_updated
client-payload: >-
{"warps_ref":"${{ github.ref_name }}","warps_sha":"${{ github.sha }}"}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules
dist
.DS_Store
.env
.firecrawl
59 changes: 59 additions & 0 deletions agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,62 @@

- Never use formal "Sie/Ihre" in German translations. Always use informal "du/dein" (lowercase) or rephrase to avoid the pronoun entirely.
- Lowercase "sie/ihre" referring to things (not users) is fine — e.g. "einer Transaktion anhand ihres Hashes" (its hash).

## When Creating or Updating Warps

Every warp must meet these requirements before it can be considered complete:

### 1. Warp JSON — Descriptions

- The `description` field must be 2-3 sentences, optimized for SEO and LLM search discoverability.
- Include relevant keywords naturally — mention the brand/service name, what the action does, and who benefits.
- If a description is only 1 short sentence, it is not ready — expand it.
- All descriptions must have both `en` and `de` translations.
- German translations must sound natural (informal "du" form, not robotic/literal). See the German Localization section.
- Never expose the internal term "warp" in user-facing text. Users know these as "actions".

### 2. meta.ts — SEO Extras (Required)

Every brand that has warps must have a `meta.ts` file in its directory. When adding or updating warps, always update the corresponding `meta.ts`.

The file exports SEO extras per warp, keyed by warp filename (without `.json`) or subdirectory name:

```ts
import type { WarpExtras } from '../types'

export const meta: Record<string, WarpExtras> = {
'warp-name': {
keywords: {
en: ['relevant search term', 'another keyword'],
de: ['relevanter Suchbegriff', 'weiteres Keyword'],
},
useCases: {
en: ['Specific scenario 1', 'Specific scenario 2', 'Specific scenario 3'],
de: ['Spezifisches Szenario 1', 'Spezifisches Szenario 2', 'Spezifisches Szenario 3'],
},
category: 'productivity',
faq: {
en: [
{ question: 'A real question users would ask?', answer: 'Concise 1-2 sentence answer.' },
],
de: [
{ question: 'Eine echte Frage, die Nutzer stellen würden?', answer: 'Knappe Antwort in 1-2 Sätzen.' },
],
},
},
}
```

**Field requirements:**

- `keywords`: relevant search terms users would type. Include brand name, action name, and related terms. Both `en` and `de`.
- `useCases`: 3-4 short, specific scenario strings describing who benefits or what can be done. Not generic platitudes — be concrete. Both `en` and `de`.
- `category`: exactly one of: `'productivity'`, `'communication'`, `'developer'`, `'defi'`, `'staking'`, `'nft'`, `'analytics'`, `'commerce'`, `'social'`, `'security'`, `'infrastructure'`.
- `faq`: 2-3 question/answer pairs per warp. Questions should be things users actually search for. Answers must be concise (1-2 sentences). Both `en` and `de`.

**Content rules:**

- Never reference internal identifiers, technical names, or the word "warp" in any user-facing text.
- German translations must be natural and fluent — not word-for-word translations. Use informal "du" form.
- Keywords should include terms in the user's language, not just English terms translated literally. German crypto/tech terms often stay in English (e.g. "staken", "swappen", "DEX").
- FAQ answers should describe what the user can do, not how the system works internally.
171 changes: 171 additions & 0 deletions scripts/catalog/build-catalog.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
// @vitest-environment node
import { describe, it, expect } from 'vitest'
import path from 'path'
import { fileURLToPath } from 'url'
import { isDraftFile, isPrivateFile, getAliasFromFileName } from './build-catalog.js'
import { buildDistributionCatalog, validateDistributionCatalog } from './distribution.js'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const REPO_ROOT = path.resolve(__dirname, '../../')

describe('isPrivateFile', () => {
it('returns true for #-prefixed filenames', () => {
Expand Down Expand Up @@ -79,3 +86,167 @@ describe('sync-to-api: listed filtering', () => {
expect(filtered).toHaveLength(2)
})
})

describe('distribution catalog', () => {
it('builds a public app catalog from listed brand warps', async () => {
const catalog = await buildDistributionCatalog(
REPO_ROOT,
{
schemaVersion: 1,
source: 'github',
repo: 'JoAiHQ/warps',
branch: 'main',
network: 'mainnet',
commitSha: 'test',
generatedAt: '2026-04-01T00:00:00.000Z',
warps: [
{
key: 'multiversx:joai-agent-create',
identifier: '@multiversx:joai-agent-create',
alias: 'joai-agent-create',
chain: 'multiversx',
hash: 'warp-hash-1',
checksum: 'warp-hash-1',
name: 'JoAi: Create Agent',
title: { en: 'Create Agent' },
description: { en: 'Create a JoAi agent.' },
preview: null,
creator: 'github:JoAiHQ/warps',
privileges: [],
listed: true,
primaryAddress: null,
primaryFunc: null,
brand: {
hash: 'brand-hash-1',
slug: 'joai',
active: true,
protocol: 'brand:1.0.0',
name: 'JoAi',
description: { en: 'JoAi brand' },
logo: { default: 'https://example.com/logo.svg' },
urls: { web: 'https://joai.ai' },
colors: { primary: '#98FF98' },
},
warp: {
actions: [{ type: 'collect' }],
},
extras: null,
},
{
key: 'multiversx:joai-private-warp',
identifier: '@multiversx:joai-private-warp',
alias: 'joai-private-warp',
chain: 'multiversx',
hash: 'warp-hash-2',
checksum: 'warp-hash-2',
name: 'JoAi: Private Warp',
title: { en: 'Private Warp' },
description: { en: 'Private warp' },
preview: null,
creator: 'github:JoAiHQ/warps',
privileges: [],
listed: false,
primaryAddress: null,
primaryFunc: null,
brand: {
hash: 'brand-hash-1',
slug: 'joai',
active: true,
protocol: 'brand:1.0.0',
name: 'JoAi',
description: { en: 'JoAi brand' },
logo: { default: 'https://example.com/logo.svg' },
urls: { web: 'https://joai.ai' },
colors: { primary: '#98FF98' },
},
warp: {
actions: [{ type: 'contract' }],
},
extras: null,
},
],
},
new Map(),
)

expect(catalog.apps).toHaveLength(1)
expect(catalog.apps[0]).toMatchObject({
slug: 'joai',
mcpUrl: 'https://cortex.joai.ai/mcp/apps/joai',
providers: {
claude: { enabled: true, status: 'ready' },
codex: { enabled: true, status: 'ready' },
openai: { enabled: true, status: 'runtime_ready' },
},
})
expect(catalog.apps[0].actions).toEqual([
{
alias: 'joai-agent-create',
identifier: '@multiversx:joai-agent-create',
chain: 'multiversx',
name: 'JoAi: Create Agent',
title: { en: 'Create Agent' },
description: { en: 'Create a JoAi agent.' },
actionTypes: ['collect'],
},
])
expect(validateDistributionCatalog(catalog)).toEqual([])
})

it('requires screenshots only for submission-ready OpenAI apps', () => {
const errors = validateDistributionCatalog({
schemaVersion: 1,
source: 'github',
repo: 'JoAiHQ/warps',
branch: 'main',
network: 'mainnet',
commitSha: 'test',
generatedAt: '2026-04-01T00:00:00.000Z',
apps: [
{
slug: 'joai',
name: 'JoAi',
description: { en: 'JoAi' },
logo: { default: 'https://example.com/logo.svg' },
urls: { web: 'https://joai.ai' },
hash: 'brand-hash',
mcpUrl: 'https://cortex.joai.ai/mcp/apps/joai',
install: {
summary: 'summary',
examplePrompts: ['prompt'],
usageNotes: [],
authPrerequisites: [],
},
legal: {
privacyUrl: 'https://legal.vleap.ai/policies/privacy.html',
supportEmail: 'support@joai.ai',
},
review: {
screenshots: [],
reviewerNotes: ['note'],
testPrompts: ['prompt'],
},
ui: {
prefersBorder: true,
csp: {
connectDomains: [],
resourceDomains: [],
frameDomains: [],
baseUriDomains: [],
},
permissions: {},
domain: undefined,
},
providers: {
claude: { provider: 'claude', enabled: true, status: 'ready', notes: [] },
codex: { provider: 'codex', enabled: true, status: 'ready', notes: [] },
openai: { provider: 'openai', enabled: true, status: 'submission_ready', notes: [] },
},
actions: [],
},
],
})

expect(errors).toContain('joai: openai marked submission_ready without screenshots')
})
})
Loading
Loading