/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import { CommandError, getSessionSchema, printQuery } from "./query-helpers.js"; import { getOrgAuth } from "../lib/auth.js"; import { formatNavigationPath } from "../lib/formatter.js"; import { schemaExists } from "../lib/introspect.js"; import { renderQuery } from "../lib/query-builder.js"; import { createSession, cloneSession, loadSession, saveSession, deleteSession, listSessions, type QuerySession, } from "../lib/session.js"; import { validateQuery } from "../lib/validator.js"; import { getRootFields } from "../lib/walker.js"; export interface NewSessionOpts { mutation?: boolean; aggregate?: boolean; name?: string; force?: boolean; } export async function queryNew(orgAlias: string, opts: NewSessionOpts): Promise { if (opts.mutation && opts.aggregate) { throw new CommandError("Cannot use --mutation and --aggregate together. Choose one."); } const { instanceUrl } = await getOrgAuth(orgAlias); if (!schemaExists(instanceUrl)) { throw new CommandError( `No cached schema for "${orgAlias}". Run \`graphiti connect ${orgAlias}\` first.`, ); } const operation = opts.mutation ? ("mutation" as const) : opts.aggregate ? ("aggregate" as const) : ("query" as const); if (opts.name) { const existing = listSessions().find((s) => s.name === opts.name); if (existing) { if (opts.force) { deleteSession(existing.id); } else { throw new CommandError( `A session named "${opts.name}" already exists (${existing.id}). ` + `Remove it first with \`graphiti query sessions rm ${opts.name}\`, use --force to replace it, or choose a different name.`, ); } } } const session = createSession(orgAlias, operation, instanceUrl, opts.name); saveSession(session); const schema = getSessionSchema(session); const rootFields = getRootFields(schema, operation); console.log(`Session: ${session.id}${session.name ? ` (${session.name})` : ""}`); if (session.name) { console.log(`Use "${session.id}" or "${session.name}" in subsequent commands.`); } console.log(formatNavigationPath(session.navigationPath)); console.log(" query/"); console.log(" variables/"); console.log(""); console.log(`Schema root (query/): ${rootFields.length} fields`); for (const field of rootFields.slice(0, 10)) { console.log( ` ${field.name}${field.typeKind === "SCALAR" || field.typeKind === "ENUM" ? "" : "/"}`, ); } if (rootFields.length > 10) { console.log(` ... ${rootFields.length - 10} more (use \`cd query\` then \`ls\` to browse)`); } console.log(""); printQuery(session); return session; } export function queryClone( sessionId: string, newName?: string, opts?: { force?: boolean }, ): { id: string; name?: string } { const source = loadSession(sessionId); if (newName) { const existing = listSessions().find((s) => s.name === newName); if (existing) { if (opts?.force) { deleteSession(existing.id); } else { throw new CommandError( `A session named "${newName}" already exists (${existing.id}). ` + `Remove it first, use --force to replace it, or choose a different name.`, ); } } } const cloned = cloneSession(source, newName); saveSession(cloned); console.log( `Cloned session ${source.id}${source.name ? ` (${source.name})` : ""} → ${cloned.id}${cloned.name ? ` (${cloned.name})` : ""}`, ); if (cloned.name) { console.log(`Use "${cloned.id}" or "${cloned.name}" in subsequent commands.`); } console.log(""); printQuery(cloned); return { id: cloned.id, name: cloned.name }; } export function querySessionsList(): void { const sessions = listSessions().sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), ); if (sessions.length === 0) { console.log("No sessions found. Run `graphiti query new ` to create one."); return; } console.log(`${sessions.length} session${sessions.length === 1 ? "" : "s"}:`); console.log(""); const maxIdNameLen = Math.max( ...sessions.map((s) => { const nameDisplay = s.name ? ` (${s.name})` : ""; return (s.id + nameDisplay).length; }), ); for (const s of sessions) { const age = formatAge(new Date(s.createdAt)); const nameDisplay = s.name ? ` (${s.name})` : ""; const idCol = (s.id + nameDisplay).padEnd(maxIdNameLen + 2); console.log(` ${idCol}${s.operation.padEnd(10)}${s.orgAlias.padEnd(20)} ${age}`); } console.log(""); console.log("Resume a session (by ID or name):"); if (sessions.length > 0) { const s = sessions[0]; const identifier = s.name ?? s.id; console.log(` graphiti query ${identifier} ls`); console.log(` graphiti query ${identifier} interactive`); } } export function querySessionsRm(sessionIdOrName: string): void { if (sessionIdOrName === "--all") { const sessions = listSessions(); if (sessions.length === 0) { console.log("No sessions to delete."); return; } for (const s of sessions) { deleteSession(s.id); } console.log(`Deleted ${sessions.length} session${sessions.length === 1 ? "" : "s"}.`); return; } const deleted = deleteSession(sessionIdOrName); if (!deleted) { throw new CommandError(`Session "${sessionIdOrName}" not found.`); } console.log(`Deleted session "${sessionIdOrName}".`); } export function parseDuration(str: string): number { const match = str.match(/^(\d+)\s*(s|m|h|d|w)$/); if (!match) throw new CommandError(`Invalid duration "${str}". Use format: 1d, 12h, 30m, 7w, etc.`); const value = parseInt(match[1], 10); const unit = match[2]; const multipliers: Record = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000, }; return value * multipliers[unit]; } export function querySessionsClean(): void { const sessions = listSessions(); if (sessions.length === 0) { console.log("No sessions found."); return; } let cleaned = 0; for (const sessionMeta of sessions) { const identifier = sessionMeta.name ?? sessionMeta.id; try { const session = loadSession(sessionMeta.id); const schema = getSessionSchema(session); const queryString = renderQuery(session); const errors = validateQuery(schema, queryString); if (errors.length > 0) { deleteSession(sessionMeta.id); console.log( ` Removed ${identifier} (${errors.length} error${errors.length !== 1 ? "s" : ""})`, ); cleaned++; } } catch { deleteSession(sessionMeta.id); console.log(` Removed ${identifier} (load/schema error)`); cleaned++; } } if (cleaned === 0) { console.log("All sessions are valid. Nothing to clean."); } else { console.log(`\nCleaned ${cleaned} invalid session${cleaned !== 1 ? "s" : ""}.`); } } export function querySessionsPrune(olderThan: string): void { const maxAgeMs = parseDuration(olderThan); const sessions = listSessions(); const cutoff = Date.now() - maxAgeMs; let count = 0; for (const s of sessions) { if (new Date(s.createdAt).getTime() < cutoff) { deleteSession(s.id); count++; } } console.log(`Pruned ${count} session${count === 1 ? "" : "s"} older than ${olderThan}.`); } export function formatAge(date: Date): string { const diffMs = Date.now() - date.getTime(); const diffSec = Math.floor(diffMs / 1000); if (diffSec < 60) return `${diffSec}s ago`; const diffMin = Math.floor(diffSec / 60); if (diffMin < 60) return `${diffMin}m ago`; const diffHr = Math.floor(diffMin / 60); if (diffHr < 24) return `${diffHr}h ago`; return `${Math.floor(diffHr / 24)}d ago`; }