add skill

This commit is contained in:
2026-04-30 18:18:56 +09:00
commit b0287d7ef9
4 changed files with 346 additions and 0 deletions

149
scripts/query.js Normal file
View File

@@ -0,0 +1,149 @@
#!/usr/bin/env node
/**
* Busan Airport Limousine Schedule Query Tool
*
* Usage:
* node query.js <stop-name> # to-airport + from-airport for that stop's line
* node query.js <stop-name> --to # to-airport only
* node query.js <stop-name> --from # from-airport only
* node query.js --from-airport --line 1 # all from-airport departures, line 1
* node query.js --from-airport --line 2 # all from-airport departures, line 2
*/
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const DATA = join(__dirname, '../data/schedule.jsonl');
const trips = readFileSync(DATA, 'utf8')
.trim().split('\n')
.map(l => JSON.parse(l));
const args = process.argv.slice(2);
if (args.length === 0) {
console.error('Usage: node query.js <stop-name>');
console.error(' node query.js --from-airport --line 1|2');
process.exit(1);
}
const fromAirportFlag = args.includes('--from-airport');
const lineFlag = args.includes('--line') ? args[args.indexOf('--line') + 1] : null;
const toOnly = args.includes('--to');
const fromOnly = args.includes('--from');
const stopQuery = args.find(a => !a.startsWith('--') && a !== lineFlag) || null;
// ── Helper ──────────────────────────────────────────────────────────────────
function pad(s, n) {
return String(s).padEnd(n);
}
function header(title) {
console.log(`\n${title}`);
console.log('─'.repeat(title.length));
}
// ── From-airport by line (--from-airport --line N) ──────────────────────────
if (fromAirportFlag && lineFlag) {
const line = parseInt(lineFlag);
if (line !== 1 && line !== 2) {
console.error('--line must be 1 or 2'); process.exit(1);
}
// Line 1 = pages 34, Line 2 = pages 56
const pages = line === 1 ? [3, 4] : [5, 6];
const termLabels = { 3: 'Domestic', 4: 'International', 5: 'Domestic', 6: 'International' };
const dest = line === 1 ? 'Haeundae/Gijang' : 'Seomyeon/Bujeon';
header(`[From Airport] Line ${line}: Gimhae Airport → ${dest}`);
console.log(`${'Airport dep'.padEnd(14)} ${'Est. arrival'.padEnd(14)} Terminal`);
console.log(`${'─'.repeat(14)} ${'─'.repeat(14)} ─────────────`);
for (const page of pages) {
const pageTrips = trips.filter(t => t.page === page).sort((a, b) => {
const [ah, am] = a.departure.time.split(':').map(Number);
const [bh, bm] = b.departure.time.split(':').map(Number);
return (ah * 60 + am) - (bh * 60 + bm);
});
for (const t of pageTrips) {
console.log(`${pad(t.departure.time, 14)} ${pad(t.arrival.time, 14)} ${termLabels[page]}`);
}
}
console.log('\n(arrival times estimated — see arrival_note in schedule.jsonl)');
process.exit(0);
}
// ── Stop lookup ──────────────────────────────────────────────────────────────
if (!stopQuery) {
console.error('Provide a stop name or use --from-airport --line 1|2');
process.exit(1);
}
// Find all to-airport trips where the stop appears (partial match)
const matchedTrips = trips.filter(t =>
t.direction === 'to_airport' &&
t.all_stops.some(s => s.stop.includes(stopQuery))
);
if (matchedTrips.length === 0) {
console.error(`No stops found matching "${stopQuery}"`);
console.error('Line 1 stops: 반얀트리 아난티코브 오시리아 장산역 해운대온천사거리 해운대해수욕장 동백섬입구 한화리조트해운대 파크하얏트부산 요트경기장 벡스코 신세계센텀시티 상수도남부사업소');
console.error('Line 2 stops: 부전시장 부전역 서면/롯데호텔백화점 동의대역 주례역');
process.exit(1);
}
// Determine which line this stop belongs to
const line = matchedTrips[0].page === 1 ? 1 : 2;
const dest = line === 1 ? 'Haeundae/Gijang' : 'Seomyeon/Bujeon';
// Find the canonical matched stop name
const matchedStopName = matchedTrips[0].all_stops.find(s => s.stop.includes(stopQuery)).stop;
// ── To-airport section ───────────────────────────────────────────────────────
if (!fromOnly) {
const sorted = [...matchedTrips].sort((a, b) => {
const sa = a.all_stops.find(s => s.stop.includes(stopQuery));
const sb = b.all_stops.find(s => s.stop.includes(stopQuery));
const [ah, am] = sa.time.split(':').map(Number);
const [bh, bm] = sb.time.split(':').map(Number);
return (ah * 60 + am) - (bh * 60 + bm);
});
header(`[To Airport] Line ${line}: ${matchedStopName} → Gimhae Airport`);
console.log(`${'Dep'.padEnd(10)} ${'Airport arr (est)'.padEnd(18)}`);
console.log(`${'─'.repeat(10)} ${'─'.repeat(18)}`);
for (const t of sorted) {
const stopEntry = t.all_stops.find(s => s.stop.includes(stopQuery));
console.log(`${pad(stopEntry.time, 10)} ${t.arrival.time}`);
}
}
// ── From-airport section ─────────────────────────────────────────────────────
if (!toOnly) {
const pages = line === 1 ? [3, 4] : [5, 6];
const termLabels = { 3: 'Dom', 4: 'Int\'l', 5: 'Dom', 6: 'Int\'l' };
header(`[From Airport] Line ${line}: Gimhae Airport → ${dest}`);
console.log(`${'Airport dep'.padEnd(14)} ${'Est. arrival'.padEnd(14)} Terminal`);
console.log(`${'─'.repeat(14)} ${'─'.repeat(14)} ────────`);
const fromTrips = trips
.filter(t => pages.includes(t.page))
.sort((a, b) => {
const [ah, am] = a.departure.time.split(':').map(Number);
const [bh, bm] = b.departure.time.split(':').map(Number);
return (ah * 60 + am) - (bh * 60 + bm);
});
for (const t of fromTrips) {
console.log(`${pad(t.departure.time, 14)} ${pad(t.arrival.time, 14)} ${termLabels[t.page]}`);
}
console.log('\n(arrival times estimated)');
}