#!/usr/bin/env node
// discover-gamestate.cjs — connect to a keet-daemon over UDS and DISCOVER a room's
// gamestate schema purely by probing (state.list), with ZERO hardcoded game
// knowledge. Demonstrates that an agent can build/confirm context from the data
// itself rather than reimplementing the Godot-side contract.
// KEET_SOCK=<sock> node discover-gamestate.cjs <roomId>
'use strict'
const net = require('net')
const SOCK = process.env.KEET_SOCK || '/tmp/keet-daemon.sock'
const roomId = process.argv[2]
if (!roomId) { console.error('usage: discover-gamestate.cjs <roomId>'); process.exit(1) }
function rpc (method, params) {
return new Promise((resolve, reject) => {
const sock = net.connect(SOCK); let buf = Buffer.alloc(0); const id = (Math.random() * 1e9) | 0
const t = setTimeout(() => { sock.destroy(); reject(new Error('timeout')) }, 20000)
sock.on('data', c => { buf = Buffer.concat([buf, c]); while (buf.length >= 4) { const n = buf.readUInt32LE(0); if (buf.length < 4 + n) break; const m = JSON.parse(buf.slice(4, 4 + n).toString()); buf = buf.slice(4 + n); if (m.id !== id) continue; clearTimeout(t); sock.destroy(); if (m.ok === false || m.error) reject(new Error(m.error)); else resolve(m.data !== undefined ? m.data : m) } })
sock.once('connect', () => { const b = Buffer.from(JSON.stringify({ id, method, params })); const l = Buffer.alloc(4); l.writeUInt32LE(b.length, 0); sock.write(Buffer.concat([l, b])) })
sock.once('error', e => { clearTimeout(t); reject(e) })
})
}
// derive a key "family" by replacing the variable trailing segments with <*>
function family (key) {
// collapse map:<id>: prefix, and trailing id/seq segments
return key
.replace(/^map:[^:]+:/, 'map:<mapId>:')
.replace(/:[0-9a-f]{16,}$/i, ':<hex>')
.replace(/:[^:]*[0-9][^:]*$/, m => /registry/.test(key) ? ':<entityId>' : (/players/.test(key) ? ':<memberId>' : ':<id>'))
}
function shapeOf (v) {
const val = (v && typeof v === 'object' && 'value' in v) ? v.value : v
if (val == null) return 'null'
if (typeof val === 'number') return 'number'
if (typeof val === 'object') return 'json:' + JSON.stringify(Object.keys(val)).slice(0, 60)
const s = String(val)
if (/^\{[\s\S]*"pos"\s*:\s*\[/.test(s)) return 'gdliteral:{pos:[x,y,z],...}'
if (/^\{/.test(s)) return 'gdliteral:dict'
if (/^Vector3\(/.test(s)) return 'gdliteral:Vector3'
if (/^-?\d+\.?\d*$/.test(s)) return 'gdliteral:number'
if (/^".*"$/.test(s)) return 'gdliteral:string'
return 'string:' + s.slice(0, 30)
}
async function main () {
const info = await rpc('state.info', { roomId }).catch(() => ({}))
console.log('room', roomId.slice(0, 16), '| writable', info.writable, '| stateBaseKey', (info.stateBaseKey || '').slice(0, 12))
const r = await rpc('state.list', { roomId, prefix: '' })
const entries = r.entries || r || {}
const keys = Object.keys(entries)
console.log('discovered', keys.length, 'state entries\n')
// group by derived family
const fams = new Map()
for (const k of keys) {
const f = family(k)
if (!fams.has(f)) fams.set(f, { count: 0, sampleKey: k, sampleVal: entries[k] })
fams.get(f).count++
}
console.log('=== DERIVED SCHEMA (from data, no hardcoded game knowledge) ===')
for (const [f, info] of [...fams.entries()].sort((a, b) => b[1].count - a[1].count)) {
const sv = info.sampleVal
const svs = (sv && typeof sv === 'object' && 'value' in sv) ? sv.value : sv
console.log(` ${String(info.count).padStart(4)}x ${f}`)
console.log(` shape: ${shapeOf(sv)}`)
console.log(` e.g. ${info.sampleKey.slice(0, 70)}`)
console.log(` = ${(typeof svs === 'string' ? svs : JSON.stringify(svs)).replace(/\n/g, ' ').slice(0, 90)}`)
}
// special: __members__ (membership projection)
if (entries.__members__ !== undefined) {
const m = entries.__members__
const mm = (m && typeof m === 'object' && 'value' in m) ? m.value : m
const parsed = typeof mm === 'string' ? JSON.parse(mm) : mm
console.log('\n=== __members__ projection ===')
console.log(' members:', Object.keys(parsed || {}).length, '| sample:', JSON.stringify(parsed).slice(0, 100))
}
process.exit(0)
}
main().catch(e => { console.error('PROBE ERROR:', e.message); process.exit(2) })