feat: add use cases marketing product with AI-generated content

Transform qualifying scenarios into marketing-ready use cases with:
- AI-generated titles, descriptions, ROI estimates, business value
- Persona/industry/category taxonomy for targeting
- Browseable feed with filtering and ranking
- SEO-optimized case study pages
- Daily cron job for generation and ranking

Database:
- Add Persona, Industry, Category lookup tables
- Add UseCase model with marketing content fields
- Add junction tables for personas/industries/categories
- Add SocialProof model for cached metrics

API:
- GET /api/use-cases - Global directory with filtering
- GET /api/use-cases/[id] - Individual use case details
- GET /api/public/users/[username]/collections/[slug]/use-cases
- POST /api/cron/use-cases - Nightly generation job

Frontend:
- /use-cases - Global feed with persona dropdown
- /use-cases/[slug] - SEO case study page
- /[username]/collections/[slug]/use-cases - Collection feed
- UseCasesFeed component - Sortable table component
- UseCaseCaseStudy component - Full case study layout
This commit is contained in:
Ajax Davis 2026-01-20 16:17:35 +10:00
parent 5fafc90e3e
commit 48c41e8733
245 changed files with 4734 additions and 2075 deletions

View file

@ -1,8 +1,8 @@
import { Args, Command, Flags } from '@oclif/core';
import * as readline from 'node:readline';
import { Args, Command, Flags } from '@oclif/core';
import { getClient } from '../../lib/api-client.js';
import { createOutput } from '../../lib/output.js';
import { getApiKey, getApiUrl } from '../../lib/config.js';
import { createOutput } from '../../lib/output.js';
export default class AgentChat extends Command {
static description = 'Chat with an agent';
@ -202,11 +202,11 @@ export default class AgentChat extends Command {
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({})) as { message?: string };
const errorData = (await response.json().catch(() => ({}))) as { message?: string };
throw new Error(errorData.message || `HTTP ${response.status}`);
}
const data = await response.json() as {
const data = (await response.json()) as {
content: string;
conversationId: string;
toolCalls?: { name: string; result: unknown }[];

View file

@ -1,7 +1,7 @@
import * as readline from 'node:readline';
import { Args, Command, Flags } from '@oclif/core';
import { getClient } from '../../lib/api-client.js';
import { createOutput } from '../../lib/output.js';
import * as readline from 'node:readline';
export default class AgentDelete extends Command {
static description = 'Delete an agent';

View file

@ -1,6 +1,6 @@
import { Command, Flags } from '@oclif/core';
import { hasCredentials, getApiKey, getApiUrl } from '../../lib/config.js';
import { TpmClient } from '../../lib/api-client.js';
import { getApiKey, getApiUrl, hasCredentials } from '../../lib/config.js';
import { createOutput } from '../../lib/output.js';
export default class Status extends Command {
@ -80,7 +80,7 @@ export default class Status extends Command {
authSource,
apiUrl,
user: response.data,
keyPrefix: apiKey.substring(0, 12) + '...',
keyPrefix: `${apiKey.substring(0, 12)}...`,
});
} else {
output.newLine();
@ -92,8 +92,11 @@ export default class Status extends Command {
output.keyValue('Name', response.data.name);
}
output.keyValue('API URL', apiUrl);
output.keyValue('Auth Source', authSource === 'env' ? 'Environment variable' : 'Config file');
output.keyValue('Key Prefix', apiKey.substring(0, 12) + '...');
output.keyValue(
'Auth Source',
authSource === 'env' ? 'Environment variable' : 'Config file'
);
output.keyValue('Key Prefix', `${apiKey.substring(0, 12)}...`);
}
} catch (error) {
spinner.fail('Failed to verify authentication');

View file

@ -1,5 +1,5 @@
import { Args, Command, Flags } from '@oclif/core';
import * as readline from 'node:readline';
import { Args, Command, Flags } from '@oclif/core';
import { getClient } from '../../lib/api-client.js';
import { createOutput } from '../../lib/output.js';

View file

@ -1,5 +1,5 @@
import { Args, Command, Flags } from '@oclif/core';
import * as fs from 'node:fs';
import { Args, Command, Flags } from '@oclif/core';
import { getClient } from '../../lib/api-client.js';
import { createOutput } from '../../lib/output.js';

View file

@ -5,9 +5,7 @@ import { createOutput } from '../../lib/output.js';
export default class CollectionRemove extends Command {
static description = 'Remove a tool from a collection';
static examples = [
'<%= config.bin %> <%= command.id %> my-collection tool-id-1',
];
static examples = ['<%= config.bin %> <%= command.id %> my-collection tool-id-1'];
static flags = {
json: Flags.boolean({

View file

@ -1,8 +1,8 @@
import { Command, Flags } from '@oclif/core';
import * as fs from 'node:fs';
import * as os from 'node:os';
import { getApiKey, getApiUrl, getConfigDir, hasCredentials, getConfig } from '../lib/config.js';
import { Command, Flags } from '@oclif/core';
import { TpmClient } from '../lib/api-client.js';
import { getApiKey, getApiUrl, getConfig, getConfigDir, hasCredentials } from '../lib/config.js';
import { createOutput } from '../lib/output.js';
interface DiagnosticCheck {
@ -59,7 +59,11 @@ export default class Doctor extends Command {
// 3. Check authentication
const hasAuth = hasCredentials() || !!process.env.TPMJS_API_KEY;
const authSource = process.env.TPMJS_API_KEY ? 'environment' : hasCredentials() ? 'config file' : 'none';
const authSource = process.env.TPMJS_API_KEY
? 'environment'
: hasCredentials()
? 'config file'
: 'none';
checks.push({
name: 'Authentication',
status: hasAuth ? 'ok' : 'warning',

View file

@ -1,5 +1,5 @@
import { Args, Command, Flags } from '@oclif/core';
import * as fs from 'node:fs';
import { Args, Command, Flags } from '@oclif/core';
import { createOutput } from '../../lib/output.js';
type ClientType = 'claude' | 'cursor' | 'windsurf' | 'generic';
@ -75,9 +75,10 @@ export default class McpConfig extends Command {
}
// Merge mcpServers
const existingServers = typeof existingConfig.mcpServers === 'object' && existingConfig.mcpServers !== null
? existingConfig.mcpServers as Record<string, unknown>
: {};
const existingServers =
typeof existingConfig.mcpServers === 'object' && existingConfig.mcpServers !== null
? (existingConfig.mcpServers as Record<string, unknown>)
: {};
const mergedConfig = {
...existingConfig,
mcpServers: {

View file

@ -1,6 +1,6 @@
import { Command, Flags } from '@oclif/core';
import * as http from 'node:http';
import * as readline from 'node:readline';
import { Command, Flags } from '@oclif/core';
import { getClient } from '../../lib/api-client.js';
import { createOutput } from '../../lib/output.js';

View file

@ -1,5 +1,5 @@
import { Command, Flags } from '@oclif/core';
import * as readline from 'node:readline';
import { Command, Flags } from '@oclif/core';
import { getClient } from '../lib/api-client.js';
import { createOutput } from '../lib/output.js';

View file

@ -1,6 +1,6 @@
import { Args, Command, Flags } from '@oclif/core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { Args, Command, Flags } from '@oclif/core';
import { getClient } from '../../lib/api-client.js';
import { createOutput } from '../../lib/output.js';
@ -67,9 +67,7 @@ export default class PublishCheck extends Command {
}
// Find exact match
const exactMatch = searchResponse.data?.find(
(tool) => tool.npmPackageName === packageName
);
const exactMatch = searchResponse.data?.find((tool) => tool.npmPackageName === packageName);
if (flags.json) {
output.json({
@ -88,7 +86,9 @@ export default class PublishCheck extends Command {
output.text(`Category: ${exactMatch.category}`);
output.text(`Tier: ${exactMatch.tier}`);
output.text(`Version: ${exactMatch.npmVersion}`);
output.text(`Downloads (last month): ${exactMatch.npmDownloadsLastMonth?.toLocaleString() || 'N/A'}`);
output.text(
`Downloads (last month): ${exactMatch.npmDownloadsLastMonth?.toLocaleString() || 'N/A'}`
);
output.text(`Quality Score: ${exactMatch.qualityScore?.toFixed(2) || 'N/A'}`);
output.text('');
output.text(`View on tpmjs.com:`);

View file

@ -1,6 +1,6 @@
import { Command, Flags } from '@oclif/core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { Command, Flags } from '@oclif/core';
import { createOutput } from '../../lib/output.js';
export default class PublishPreview extends Command {

View file

@ -104,7 +104,7 @@ export default class ScenarioList extends Command {
if (scenarios.length === 0) {
output.info(`No scenarios found for collection "${collection.name}"`);
output.text(
'Generate some with: tpm scenario generate ' + (collection.slug || collection.id)
`Generate some with: tpm scenario generate ${collection.slug || collection.id}`
);
return;
}
@ -112,8 +112,8 @@ export default class ScenarioList extends Command {
output.text(output.bold(`Scenarios for ${collection.name}\n`));
output.table(
scenarios.map((s) => ({
name: s.name || s.prompt.slice(0, 30) + '...',
quality: (s.qualityScore * 100).toFixed(0) + '%',
name: s.name || `${s.prompt.slice(0, 30)}...`,
quality: `${(s.qualityScore * 100).toFixed(0)}%`,
runs: s.totalRuns,
status: s.lastRunStatus || '-',
tags: s.tags.slice(0, 3).join(', ') || '-',
@ -147,9 +147,9 @@ export default class ScenarioList extends Command {
output.table(
response.data.map((s) => ({
name: s.name || s.prompt.slice(0, 30) + '...',
name: s.name || `${s.prompt.slice(0, 30)}...`,
collection: s.collection?.name || '-',
quality: (s.qualityScore * 100).toFixed(0) + '%',
quality: `${(s.qualityScore * 100).toFixed(0)}%`,
runs: s.totalRuns,
status: s.lastRunStatus || '-',
})),

View file

@ -121,7 +121,7 @@ export default class ScenarioRun extends Command {
let errors = 0;
for (const scenario of scenarios) {
const name = scenario.name || scenario.prompt.slice(0, 40) + '...';
const name = scenario.name || `${scenario.prompt.slice(0, 40)}...`;
const runSpinner = output.spinner(`Running: ${name}`);
try {

View file

@ -56,7 +56,7 @@ export default class ScenarioTest extends Command {
}
).data;
scenarioName = scenario.name || scenario.prompt.slice(0, 50) + '...';
scenarioName = scenario.name || `${scenario.prompt.slice(0, 50)}...`;
infoSpinner.stop();
output.text(output.bold(`Scenario: ${scenarioName}`));
@ -131,7 +131,7 @@ export default class ScenarioTest extends Command {
} else {
const truncatedReason =
runData.evaluator.reason.length > 80
? runData.evaluator.reason.slice(0, 80) + '...'
? `${runData.evaluator.reason.slice(0, 80)}...`
: runData.evaluator.reason;
output.text(` Reason: ${truncatedReason}`);
}

View file

@ -69,7 +69,9 @@ export default class ToolExecute extends Command {
const content = fs.readFileSync(flags['input-file'], 'utf-8');
params = JSON.parse(content);
} catch (error) {
output.error(`Failed to read input file: ${error instanceof Error ? error.message : 'Unknown error'}`);
output.error(
`Failed to read input file: ${error instanceof Error ? error.message : 'Unknown error'}`
);
return;
}
}

View file

@ -61,7 +61,7 @@ export default class ToolInfo extends Command {
output.keyValue('Package', tool.package?.npmPackageName || tool.npmPackageName);
output.keyValue('Category', tool.package?.category || tool.category);
output.keyValue('Official', (tool.package?.isOfficial || tool.isOfficial) ? 'Yes' : 'No');
output.keyValue('Official', tool.package?.isOfficial || tool.isOfficial ? 'Yes' : 'No');
output.newLine();
output.subheading('Description');
@ -75,13 +75,20 @@ export default class ToolInfo extends Command {
output.subheading('Metrics');
output.keyValue('Quality Score', tool.qualityScore ? tool.qualityScore.toFixed(2) : 'N/A');
output.keyValue('Downloads/Month', formatDownloads(tool.package?.npmDownloadsLastMonth || tool.npmDownloadsLastMonth));
output.keyValue(
'Downloads/Month',
formatDownloads(tool.package?.npmDownloadsLastMonth || tool.npmDownloadsLastMonth)
);
output.keyValue('Likes', tool.likeCount.toString());
output.newLine();
output.subheading('Links');
output.text(`Web: ${output.link('View on TPMJS', `https://tpmjs.com/tool/${args.package}/${args.tool}`)}`);
output.text(`npm: ${output.link('View on npm', `https://www.npmjs.com/package/${args.package}`)}`);
output.text(
`Web: ${output.link('View on TPMJS', `https://tpmjs.com/tool/${args.package}/${args.tool}`)}`
);
output.text(
`npm: ${output.link('View on npm', `https://www.npmjs.com/package/${args.package}`)}`
);
} catch (error) {
spinner.fail('Failed to fetch tool info');
output.error(

View file

@ -1,7 +1,7 @@
import { Args, Command, Flags } from '@oclif/core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as readline from 'node:readline';
import { Args, Command, Flags } from '@oclif/core';
import { createOutput } from '../../lib/output.js';
const CATEGORIES = [
@ -154,7 +154,8 @@ export default class ToolInit extends Command {
}
}
const author = (await this.prompt(`Author (${process.env.USER}): `)) || process.env.USER || 'unknown';
const author =
(await this.prompt(`Author (${process.env.USER}): `)) || process.env.USER || 'unknown';
return { name: toolName, description, category: category ?? 'utilities', author };
}
@ -219,7 +220,7 @@ export default class ToolInit extends Command {
],
...(template === 'rich'
? {
documentation: 'https://github.com/yourname/' + config.name + '#readme',
documentation: `https://github.com/yourname/${config.name}#readme`,
examples: [
{
title: 'Basic usage',
@ -231,10 +232,7 @@ export default class ToolInit extends Command {
},
};
fs.writeFileSync(
path.join(targetDir, 'package.json'),
JSON.stringify(packageJson, null, 2)
);
fs.writeFileSync(path.join(targetDir, 'package.json'), JSON.stringify(packageJson, null, 2));
// tsconfig.json
const tsconfig = {
@ -254,10 +252,7 @@ export default class ToolInit extends Command {
exclude: ['node_modules', 'dist'],
};
fs.writeFileSync(
path.join(targetDir, 'tsconfig.json'),
JSON.stringify(tsconfig, null, 2)
);
fs.writeFileSync(path.join(targetDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
// tsup.config.ts
const tsupConfig = `import { defineConfig } from 'tsup';

View file

@ -102,11 +102,7 @@ export default class ToolSearch extends Command {
);
if (response.pagination.hasMore) {
output.text(
output.dim(
`Use --offset ${flags.offset + flags.limit} to see more results`
)
);
output.text(output.dim(`Use --offset ${flags.offset + flags.limit} to see more results`));
}
} catch (error) {
spinner.fail('Search failed');

View file

@ -1,6 +1,6 @@
import { Command, Flags } from '@oclif/core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { Command, Flags } from '@oclif/core';
import { getClient } from '../../lib/api-client.js';
import { createOutput } from '../../lib/output.js';
@ -47,7 +47,10 @@ export default class ToolValidate extends Command {
const content = fs.readFileSync(packagePath, 'utf-8');
packageJson = JSON.parse(content);
} catch (error) {
output.error('Failed to parse package.json', error instanceof Error ? error.message : undefined);
output.error(
'Failed to parse package.json',
error instanceof Error ? error.message : undefined
);
return;
}
@ -104,7 +107,10 @@ export default class ToolValidate extends Command {
output.success('Configuration is valid');
output.newLine();
output.keyValue('Tier', response.data.tier || 'minimal');
output.keyValue('Has tpmjs keyword', hasTpmjsKeyword ? 'Yes' : 'No (add for auto-discovery)');
output.keyValue(
'Has tpmjs keyword',
hasTpmjsKeyword ? 'Yes' : 'No (add for auto-discovery)'
);
if (!hasTpmjsKeyword) {
output.newLine();

View file

@ -1,5 +1,5 @@
import { Command, Flags } from '@oclif/core';
import { execSync } from 'node:child_process';
import { Command, Flags } from '@oclif/core';
import { createOutput } from '../lib/output.js';
export default class Update extends Command {

View file

@ -1,6 +1,6 @@
import type { Hook } from '@oclif/core';
const hook: Hook<'init'> = async function () {
const hook: Hook<'init'> = async () => {
// Initialization hook - runs before any command
// Can be used for:
// - Checking for updates

View file

@ -1,36 +1,35 @@
// Library exports for programmatic use
export { TpmClient, getClient, ApiError } from './lib/api-client.js';
export type {
TpmClientOptions,
Agent,
ApiKey,
ApiResponse,
PaginationOptions,
Collection,
CreateAgentInput,
CreateCollectionInput,
PaginatedResponse,
PaginationOptions,
Stats,
Tool,
ToolSearchOptions,
Agent,
CreateAgentInput,
TpmClientOptions,
UpdateAgentInput,
Collection,
CreateCollectionInput,
UpdateCollectionInput,
User,
ApiKey,
Stats,
} from './lib/api-client.js';
export { ApiError, getClient, TpmClient } from './lib/api-client.js';
export type { TpmConfig, TpmCredentials } from './lib/config.js';
export {
getConfig,
setConfig,
getConfigValue,
setConfigValue,
loadCredentials,
saveCredentials,
deleteCredentials,
hasCredentials,
getApiKey,
getApiUrl,
getConfig,
getConfigValue,
hasCredentials,
loadCredentials,
saveCredentials,
setConfig,
setConfigValue,
} from './lib/config.js';
export type { TpmConfig, TpmCredentials } from './lib/config.js';
export { OutputFormatter, createOutput } from './lib/output.js';
export type { OutputOptions } from './lib/output.js';
export { createOutput, OutputFormatter } from './lib/output.js';

View file

@ -250,7 +250,7 @@ export class TpmClient {
};
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`;
headers.Authorization = `Bearer ${this.apiKey}`;
}
const controller = new AbortController();
@ -357,7 +357,7 @@ export class TpmClient {
};
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`;
headers.Authorization = `Bearer ${this.apiKey}`;
}
const response = await fetch(url, {

View file

@ -1,7 +1,7 @@
import Conf from 'conf';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import Conf from 'conf';
export interface TpmConfig {
apiUrl?: string;
@ -57,10 +57,7 @@ export function getConfigValue<K extends keyof TpmConfig>(key: K): TpmConfig[K]
return configStore.get(key);
}
export function setConfigValue<K extends keyof TpmConfig>(
key: K,
value: TpmConfig[K]
): void {
export function setConfigValue<K extends keyof TpmConfig>(key: K, value: TpmConfig[K]): void {
configStore.set(key, value);
}

View file

@ -107,13 +107,13 @@ export class OutputFormatter {
// Key-value pair
keyValue(key: string, value: string | number | boolean | undefined): void {
if (this.options.json) return;
console.log(`${pc.dim(key + ':')} ${value ?? pc.dim('(not set)')}`);
console.log(`${pc.dim(`${key}:`)} ${value ?? pc.dim('(not set)')}`);
}
// List item
listItem(text: string, indent = 0): void {
if (this.options.json) return;
const prefix = ' '.repeat(indent) + '•';
const prefix = `${' '.repeat(indent)}`;
console.log(`${prefix} ${text}`);
}
@ -148,7 +148,7 @@ export class OutputFormatter {
this.json({ code: text, language });
return;
}
console.log(pc.dim('```' + (language ?? '')));
console.log(pc.dim(`\`\`\`${language ?? ''}`));
console.log(text);
console.log(pc.dim('```'));
}

View file

@ -1,11 +1,7 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: [
'src/index.ts',
'src/commands/**/*.ts',
'src/hooks/**/*.ts',
],
entry: ['src/index.ts', 'src/commands/**/*.ts', 'src/hooks/**/*.ts'],
format: ['esm'],
dts: true,
clean: true,