diff --git a/package.json b/package.json index 7bb4a2c..eb4a735 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start:server": "node server.js", - "history": "node history.js" + "history": "node history.js", + "setup:geo": "node scripts/setup-geo.js", + "geo:status": "node scripts/geo-status.js" }, "keywords": [], "author": "", diff --git a/publish.js b/publish.js index dcc6252..5bfe8db 100644 --- a/publish.js +++ b/publish.js @@ -1,23 +1,19 @@ -const { spawnSync } = require('child_process'); +const { senso } = require('./scripts/senso-run'); async function publishReceipt({ query, selectedResult, price, txHash, sourceUrl, searchResults }) { - const apiKey = process.env.SENSO_API_KEY; - if (!apiKey) throw new Error('SENSO_API_KEY not set'); + if (!process.env.SENSO_API_KEY) throw new Error('SENSO_API_KEY not set'); const markdown = buildReceiptMarkdown({ query, selectedResult, price, txHash, sourceUrl, searchResults }); const seoTitle = `Shop3 Purchase — ${selectedResult}`; const questionText = `What did Shop3 purchase: ${selectedResult}?`; - // Create a Senso prompt tied to this purchase so we have a geo_question_id to publish against const promptJson = senso( 'prompts create', - ['--data', JSON.stringify({ question_text: questionText, type: 'decision' })], - apiKey + ['--data', JSON.stringify({ question_text: questionText, type: 'decision' })] ); const promptId = promptJson.prompt_id ?? promptJson.id; if (!promptId) throw new Error('Senso prompt create did not return a prompt_id'); - // Publish the receipt as a citeable on cited.md const publishJson = senso( 'engine publish', ['--data', JSON.stringify({ @@ -25,8 +21,7 @@ async function publishReceipt({ query, selectedResult, price, txHash, sourceUrl, raw_markdown: markdown, seo_title: seoTitle, summary: `Shop3 autonomously purchased ${selectedResult} for ${price}. Tx: ${txHash}`, - })], - apiKey + })] ); const url = publishJson.url ?? publishJson.cite_url ?? `https://cited.md/shop3`; @@ -34,20 +29,6 @@ async function publishReceipt({ query, selectedResult, price, txHash, sourceUrl, return url; } -// Run a senso CLI command and return parsed JSON output. -// Uses spawnSync with an explicit argv array — no shell interpolation, no injection surface. -function senso(subcommand, flags, apiKey) { - const argv = [...subcommand.split(' '), ...flags, '--output', 'json', '--quiet']; - const result = spawnSync('senso', argv, { - env: { ...process.env, SENSO_API_KEY: apiKey }, - encoding: 'utf8', - }); - if (result.error) throw result.error; - if (result.status !== 0) throw new Error(`senso exited ${result.status}: ${result.stderr}`); - const clean = result.stdout.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '').trim(); - return JSON.parse(clean); -} - function buildReceiptMarkdown({ query, selectedResult, price, txHash, sourceUrl, searchResults }) { const ts = new Date().toUTCString(); const resultList = (searchResults ?? []) @@ -71,9 +52,8 @@ ${resultList || 'N/A'} ## Payment Proof - **Transaction Hash:** \`${txHash}\` -- **Chain:** Base Sepolia +- **Chain:** ARC-TESTNET - **Token:** USDC -- **Verified:** [View on BaseScan](https://sepolia.basescan.org/tx/${txHash}) --- *Generated autonomously by Shop3. No human intervention after initial prompt.* diff --git a/scripts/geo-status.js b/scripts/geo-status.js new file mode 100644 index 0000000..72c723b --- /dev/null +++ b/scripts/geo-status.js @@ -0,0 +1,87 @@ +require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }); +const { senso } = require('./senso-run'); +const { gauge, increment } = require('../telemetry'); + +async function main() { + console.log('[geo:status] Fetching GEO monitoring results for Shop3...\n'); + + const { prompts = [] } = senso('prompts list'); + + if (prompts.length === 0) { + console.log('[geo:status] No prompts found. Run the Senso onboarding skill to create tracking questions.'); + console.log('[geo:status] Emitting sentinel metrics (last_run_age_seconds = -1).'); + for (const model of ['chatgpt', 'claude', 'perplexity', 'gemini']) { + gauge('geo.last_run_age_seconds', -1, { model }); + } + return; + } + + console.log(`[geo:status] Found ${prompts.length} prompt(s). Fetching run history...\n`); + + const modelStats = {}; + + for (const prompt of prompts) { + let detail; + try { + detail = senso('prompts get', [prompt.id ?? prompt.prompt_id]); + } catch { + continue; + } + + const runs = detail?.prompt?.runs ?? detail?.runs ?? []; + for (const run of runs) { + const results = run.results ?? []; + const runAt = new Date(run.run_at ?? run.created_at ?? 0); + + for (const result of results) { + const model = result.model ?? 'unknown'; + if (!modelStats[model]) { + modelStats[model] = { mentions: 0, citations: 0, prompts: 0, lastRunAt: null }; + } + modelStats[model].prompts += 1; + if (result.brand_mentioned) modelStats[model].mentions += 1; + const citedMdLinks = (result.citations ?? []).filter( + (c) => typeof c === 'string' && c.includes('cited.md') + ); + modelStats[model].citations += citedMdLinks.length; + if (!modelStats[model].lastRunAt || runAt > modelStats[model].lastRunAt) { + modelStats[model].lastRunAt = runAt; + } + } + } + } + + const now = Date.now(); + + if (Object.keys(modelStats).length === 0) { + console.log('[geo:status] No run results yet — GEO runs happen on the configured schedule (Mon/Wed/Fri).'); + console.log('[geo:status] Check https://geo.senso.ai after the next scheduled run.\n'); + for (const model of ['chatgpt', 'claude', 'perplexity', 'gemini']) { + gauge('geo.last_run_age_seconds', -1, { model }); + } + return; + } + + console.log('Model Prompts Mentions Citations Last Run'); + console.log('─'.repeat(65)); + for (const [model, stats] of Object.entries(modelStats)) { + const ageSeconds = stats.lastRunAt ? Math.floor((now - stats.lastRunAt.getTime()) / 1000) : -1; + const ageStr = stats.lastRunAt ? `${Math.floor(ageSeconds / 3600)}h ago` : 'never'; + const mentionRate = stats.prompts > 0 ? `${stats.mentions}/${stats.prompts}` : '—'; + console.log( + `${model.padEnd(12)} ${String(stats.prompts).padEnd(8)} ${mentionRate.padEnd(9)} ${String(stats.citations).padEnd(10)} ${ageStr}` + ); + gauge('geo.mention_score', stats.prompts > 0 ? stats.mentions / stats.prompts : 0, { model }); + gauge('geo.citation_count', stats.citations, { model }); + gauge('geo.last_run_age_seconds', ageSeconds, { model }); + if (stats.mentions > 0) increment('geo.brand_mentioned', { model }); + } + + console.log('\n[geo:status] Metrics emitted to Datadog under shop3.geo.*'); + console.log('[geo:status] Full dashboard: https://geo.senso.ai'); +} + +main().catch((err) => { + console.error('[geo:status] Failed:', err.message); + process.exit(1); +}); diff --git a/scripts/senso-run.js b/scripts/senso-run.js new file mode 100644 index 0000000..1fa957c --- /dev/null +++ b/scripts/senso-run.js @@ -0,0 +1,43 @@ +// Shared helper for running the Senso CLI from Node.js scripts. +// Uses execSync with platform-aware quoting so JSON args survive the shell. +const { execSync } = require('child_process'); + +function quoteArg(s) { + if (process.platform === 'win32') { + return '"' + s.replace(/"/g, '\\"') + '"'; + } + return "'" + s.replace(/'/g, "'\\''") + "'"; +} + +function buildCmd(subcommand, flags, json) { + const apiKey = process.env.SENSO_API_KEY; + if (!apiKey) throw new Error('SENSO_API_KEY not set'); + let cmd = `senso ${subcommand}`; + for (const flag of flags) { + cmd += ' ' + quoteArg(flag); + } + if (json) cmd += ' --output json --quiet'; + return { cmd, apiKey }; +} + +function senso(subcommand, flags = []) { + const { cmd, apiKey } = buildCmd(subcommand, flags, true); + const raw = execSync(cmd, { + env: { ...process.env, SENSO_API_KEY: apiKey }, + encoding: 'utf8', + }); + const clean = raw.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '').trim(); + return JSON.parse(clean); +} + +// For commands that don't return JSON (e.g. run-config set-*) +function sensoVoid(subcommand, flags = []) { + const { cmd, apiKey } = buildCmd(subcommand, flags, false); + execSync(cmd, { + env: { ...process.env, SENSO_API_KEY: apiKey }, + encoding: 'utf8', + stdio: 'pipe', + }); +} + +module.exports = { senso, sensoVoid }; diff --git a/scripts/setup-geo.js b/scripts/setup-geo.js new file mode 100644 index 0000000..4a0542b --- /dev/null +++ b/scripts/setup-geo.js @@ -0,0 +1,41 @@ +require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }); +const { senso, sensoVoid } = require('./senso-run'); + +const MODELS = ['chatgpt', 'claude', 'perplexity', 'gemini']; +const SCHEDULE = [1, 3, 5]; // Mon, Wed, Fri + +async function main() { + console.log('[geo:setup] Configuring GEO monitoring for Shop3...\n'); + + console.log(`[geo:setup] Setting models: ${MODELS.join(', ')}`); + sensoVoid('run-config set-models', ['--data', JSON.stringify({ models: MODELS })]); + + console.log(`[geo:setup] Setting schedule: Mon/Wed/Fri (days ${SCHEDULE.join(',')})`); + sensoVoid('run-config set-schedule', ['--data', JSON.stringify({ schedule: SCHEDULE })]); + + // Verify models + const modelsResult = senso('run-config models'); + const configuredModels = (modelsResult.models ?? []).map((m) => m.name); + const missingModels = MODELS.filter((m) => !configuredModels.includes(m)); + if (missingModels.length > 0) { + throw new Error(`Model config verification failed — missing: ${missingModels.join(', ')}`); + } + console.log(`[geo:setup] ✓ Models verified: ${configuredModels.join(', ')}`); + + // Verify schedule + const scheduleResult = senso('run-config schedule'); + const configuredSchedule = scheduleResult.schedule ?? []; + const missingDays = SCHEDULE.filter((d) => !configuredSchedule.includes(d)); + if (missingDays.length > 0) { + throw new Error(`Schedule verification failed — missing days: ${missingDays.join(', ')}`); + } + console.log(`[geo:setup] ✓ Schedule verified: days ${configuredSchedule.join(',')}`); + + console.log('\n[geo:setup] Done. GEO monitoring will run Mon/Wed/Fri across ChatGPT, Claude, Perplexity, and Gemini.'); + console.log('[geo:setup] First results appear at https://geo.senso.ai within 24-48 hours of the next run.'); +} + +main().catch((err) => { + console.error('[geo:setup] Failed:', err.message); + process.exit(1); +});