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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand Down
30 changes: 5 additions & 25 deletions publish.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,34 @@
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({
geo_question_id: promptId,
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`;
console.log(`[publish] Receipt live: ${url}`);
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 ?? [])
Expand All @@ -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.*
Expand Down
87 changes: 87 additions & 0 deletions scripts/geo-status.js
Original file line number Diff line number Diff line change
@@ -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);
});
43 changes: 43 additions & 0 deletions scripts/senso-run.js
Original file line number Diff line number Diff line change
@@ -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 };
41 changes: 41 additions & 0 deletions scripts/setup-geo.js
Original file line number Diff line number Diff line change
@@ -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);
});
Loading