consortium
Version:
Remote control and session sharing CLI for AI coding agents
1,335 lines (1,318 loc) • 72.4 kB
JavaScript
'use strict';
var ink = require('ink');
var React = require('react');
var node_crypto = require('node:crypto');
var path$1 = require('node:path');
var persistence = require('./types-B_i6lpTn.cjs');
var createSessionMetadata = require('./createSessionMetadata-CVgp25Mn.cjs');
var index = require('./index-BMIckAk5.cjs');
var setupOfflineReconnection = require('./setupOfflineReconnection-CY4q78_S.cjs');
var capabilities = require('./capabilities-BsNjrlBG.cjs');
var fs = require('fs');
var path = require('path');
var os = require('os');
var child_process = require('child_process');
var GeminiDisplay = require('./GeminiDisplay-G90hcB70.cjs');
var BasePermissionHandler = require('./BasePermissionHandler-DM5JDRsB.cjs');
var BaseReasoningProcessor = require('./BaseReasoningProcessor-l1KZ6ISd.cjs');
require('axios');
require('chalk');
require('node:fs');
require('node:os');
require('node:events');
require('socket.io-client');
require('zod');
require('tweetnacl');
require('util');
require('fs/promises');
require('crypto');
require('url');
require('node:child_process');
require('node:fs/promises');
require('node:module');
require('node:util');
require('expo-server-sdk');
require('node:readline');
require('node:url');
require('ps-list');
require('cross-spawn');
require('tmp');
require('qrcode-terminal');
require('open');
require('fastify');
require('fastify-type-provider-zod');
require('http');
require('@modelcontextprotocol/sdk/client/index.js');
require('@modelcontextprotocol/sdk/client/streamableHttp.js');
require('readline');
require('@modelcontextprotocol/sdk/server/mcp.js');
require('node:http');
require('@modelcontextprotocol/sdk/server/streamableHttp.js');
require('@agentclientprotocol/sdk');
require('./killSwitch-DwcqKgA9.cjs');
require('libsodium-wrappers');
const GROK_TIMEOUTS = {
/** Grok CLI can be slow on first start (downloading models, etc.) */
init: 12e4,
/** Standard tool call timeout */
toolCall: 12e4,
/** Investigation tools (codebase_investigator) can run for a long time */
investigation: 6e5,
/** Think tools are usually quick */
think: 3e4,
/** Idle detection after last message chunk */
idle: 500
};
const GROK_TOOL_PATTERNS = [
{
name: "change_title",
patterns: ["change_title", "change-title", "consortium__change_title", "mcp__consortium__change_title"],
inputFields: ["title"],
emptyInputDefault: true
// change_title often has empty input (title extracted from context)
},
{
name: "save_memory",
patterns: ["save_memory", "save-memory"],
inputFields: ["memory", "content"]
},
{
name: "think",
patterns: ["think"],
inputFields: ["thought", "thinking"]
}
];
const AVAILABLE_MODELS = [
"grok-2.5-pro",
"grok-2.5-flash",
"grok-2.5-flash-lite"
];
class GrokTransport {
agentName = "grok";
/**
* Grok CLI needs 2 minutes for first start (model download, warm-up)
*/
getInitTimeout() {
return GROK_TIMEOUTS.init;
}
/**
* Filter Grok CLI debug output from stdout.
*
* Grok CLI outputs various debug info (experiments, flags, etc.) to stdout
* that breaks ACP JSON-RPC parsing. We only keep valid JSON lines.
*/
filterStdoutLine(line) {
const trimmed = line.trim();
if (!trimmed) {
return null;
}
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
return null;
}
try {
const parsed = JSON.parse(trimmed);
if (typeof parsed !== "object" || parsed === null) {
return null;
}
return line;
} catch {
return null;
}
}
/**
* Handle Grok CLI stderr output.
*
* Detects:
* - Rate limit errors (429) - logged but not shown (CLI handles retries)
* - Model not found (404) - emit error with available models
* - Other errors during investigation - logged for debugging
*/
handleStderr(text, context) {
const trimmed = text.trim();
if (!trimmed) {
return { message: null, suppress: true };
}
if (trimmed.includes("status 429") || trimmed.includes('code":429') || trimmed.includes("rateLimitExceeded") || trimmed.includes("RESOURCE_EXHAUSTED")) {
return {
message: null,
suppress: false
// Log for debugging but don't show to user
};
}
if (trimmed.includes("status 404") || trimmed.includes('code":404')) {
const errorMessage = {
type: "status",
status: "error",
detail: `Model not found. Available models: ${AVAILABLE_MODELS.join(", ")}`
};
return { message: errorMessage };
}
if (context.hasActiveInvestigation) {
const hasError = trimmed.includes("timeout") || trimmed.includes("Timeout") || trimmed.includes("failed") || trimmed.includes("Failed") || trimmed.includes("error") || trimmed.includes("Error");
if (hasError) {
return { message: null, suppress: false };
}
}
return { message: null };
}
/**
* Grok-specific tool patterns
*/
getToolPatterns() {
return GROK_TOOL_PATTERNS;
}
/**
* Check if tool is an investigation tool (needs longer timeout)
*/
isInvestigationTool(toolCallId, toolKind) {
const lowerId = toolCallId.toLowerCase();
return lowerId.includes("codebase_investigator") || lowerId.includes("investigator") || typeof toolKind === "string" && toolKind.includes("investigator");
}
/**
* Get timeout for a tool call
*/
getToolCallTimeout(toolCallId, toolKind) {
if (this.isInvestigationTool(toolCallId, toolKind)) {
return GROK_TIMEOUTS.investigation;
}
if (toolKind === "think") {
return GROK_TIMEOUTS.think;
}
return GROK_TIMEOUTS.toolCall;
}
/**
* Get idle detection timeout
*/
getIdleTimeout() {
return GROK_TIMEOUTS.idle;
}
/**
* Extract tool name from toolCallId using Grok patterns.
*
* Tool IDs often contain the tool name as a prefix (e.g., "change_title-1765385846663" -> "change_title")
*/
extractToolNameFromId(toolCallId) {
const lowerId = toolCallId.toLowerCase();
for (const toolPattern of GROK_TOOL_PATTERNS) {
for (const pattern of toolPattern.patterns) {
if (lowerId.includes(pattern.toLowerCase())) {
return toolPattern.name;
}
}
}
return null;
}
/**
* Check if input is effectively empty
*/
isEmptyInput(input) {
if (!input) return true;
if (Array.isArray(input)) return input.length === 0;
if (typeof input === "object") return Object.keys(input).length === 0;
return false;
}
/**
* Determine the real tool name from various sources.
*
* When Grok sends "other" or "Unknown tool", tries to determine the real name from:
* 1. toolCallId patterns (most reliable - tool name often embedded in ID)
* 2. Input field signatures (specific fields indicate specific tools)
* 3. Empty input default (some tools like change_title have empty input)
*
* Context-based heuristics were removed as they were fragile and the above
* methods cover all known cases.
*/
determineToolName(toolName, toolCallId, input, _context) {
if (toolName !== "other" && toolName !== "Unknown tool") {
return toolName;
}
const idToolName = this.extractToolNameFromId(toolCallId);
if (idToolName) {
return idToolName;
}
if (input && typeof input === "object" && !Array.isArray(input)) {
const inputKeys = Object.keys(input);
for (const toolPattern of GROK_TOOL_PATTERNS) {
if (toolPattern.inputFields) {
const hasMatchingField = toolPattern.inputFields.some(
(field) => inputKeys.some((key) => key.toLowerCase() === field.toLowerCase())
);
if (hasMatchingField) {
return toolPattern.name;
}
}
}
}
if (this.isEmptyInput(input) && toolName === "other") {
const defaultTool = GROK_TOOL_PATTERNS.find((p) => p.emptyInputDefault);
if (defaultTool) {
return defaultTool.name;
}
}
if (toolName === "other" || toolName === "Unknown tool") {
const inputKeys = input && typeof input === "object" ? Object.keys(input) : [];
persistence.logger.debug(
`[GrokTransport] Unknown tool pattern - toolCallId: "${toolCallId}", toolName: "${toolName}", inputKeys: [${inputKeys.join(", ")}]. Consider adding a new pattern to GROK_TOOL_PATTERNS if this tool appears frequently.`
);
}
return toolName;
}
}
const grokTransport = new GrokTransport();
const GROK_CODE_XAI_API_KEY_ENV = "GROK_CODE_XAI_API_KEY";
const XAI_API_KEY_ENV = "XAI_API_KEY";
const GROK_API_KEY_ENV = "GROK_API_KEY";
const GROK_MODEL_ENV = "GROK_MODEL";
const DEFAULT_GROK_MODEL = "grok-build";
function readGrokLocalConfig() {
let token = null;
let model = null;
let googleCloudProject = null;
let googleCloudProjectEmail = null;
const AUTH_SCOPES = [
"https://auth.x.ai::b1a00492-073a-47ea-816f-4c329264a828",
"https://accounts.x.ai/sign-in"
];
const possiblePaths = [
path.join(os.homedir(), ".grok", "auth.json"),
// canonical, written by `grok login`
path.join(os.homedir(), ".grok", "config.json")
// local model/project overrides
];
for (const configPath of possiblePaths) {
if (fs.existsSync(configPath)) {
try {
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
if (!token) {
for (const scope of AUTH_SCOPES) {
const scoped = config?.[scope];
if (scoped && typeof scoped === "object" && typeof scoped.key === "string" && scoped.key.length > 0) {
token = scoped.key;
persistence.logger.debug(`[Grok] Found token in ${configPath} (scope=${scope})`);
break;
}
}
}
if (!token) {
const foundToken = config.token || config.apiKey || config.access_token || config.GROK_API_KEY;
if (foundToken && typeof foundToken === "string") {
token = foundToken;
persistence.logger.debug(`[Grok] Found token in ${configPath}`);
}
}
if (!model) {
const foundModel = config.model || config.GROK_MODEL;
if (foundModel && typeof foundModel === "string") {
model = foundModel;
persistence.logger.debug(`[Grok] Found model in ${configPath}: ${model}`);
}
}
if (!googleCloudProject) {
const foundProject = config.googleCloudProject || config.google_cloud_project || config.projectId;
if (foundProject && typeof foundProject === "string") {
googleCloudProject = foundProject;
if (config.googleCloudProjectEmail && typeof config.googleCloudProjectEmail === "string") {
googleCloudProjectEmail = config.googleCloudProjectEmail;
}
persistence.logger.debug(`[Grok] Found Google Cloud Project in ${configPath}: ${googleCloudProject}${googleCloudProjectEmail ? ` (for ${googleCloudProjectEmail})` : ""}`);
}
}
} catch (error) {
persistence.logger.debug(`[Grok] Failed to read config from ${configPath}:`, error);
}
}
}
if (!token) {
try {
const gcloudToken = child_process.execSync("gcloud auth application-default print-access-token", {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
timeout: 5e3
}).trim();
if (gcloudToken && gcloudToken.length > 0) {
token = gcloudToken;
persistence.logger.debug("[Grok] Found token via gcloud Application Default Credentials");
}
} catch (error) {
persistence.logger.debug("[Grok] gcloud Application Default Credentials not available");
}
}
if (!googleCloudProject) {
const envProject = process.env.GOOGLE_CLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT_ID;
if (envProject) {
googleCloudProject = envProject;
googleCloudProjectEmail = null;
persistence.logger.debug(`[Grok] Found Google Cloud Project from env: ${googleCloudProject}`);
}
}
return { token, model, googleCloudProject, googleCloudProjectEmail };
}
function determineGrokModel(explicitModel, localConfig) {
if (explicitModel !== void 0) {
if (explicitModel === null) {
return process.env[GROK_MODEL_ENV] || DEFAULT_GROK_MODEL;
} else {
return explicitModel;
}
} else {
const envModel = process.env[GROK_MODEL_ENV];
persistence.logger.debug(`[Grok] Model selection: env[GROK_MODEL_ENV]=${envModel}, localConfig.model=${localConfig.model}, DEFAULT=${DEFAULT_GROK_MODEL}`);
const model = envModel || localConfig.model || DEFAULT_GROK_MODEL;
persistence.logger.debug(`[Grok] Selected model: ${model}`);
return model;
}
}
function saveGrokModelToConfig(model) {
try {
const configDir = path.join(os.homedir(), ".grok");
const configPath = path.join(configDir, "config.json");
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
let config = {};
if (fs.existsSync(configPath)) {
try {
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
} catch (error) {
persistence.logger.debug(`[Grok] Failed to read existing config, creating new one`);
config = {};
}
}
config.model = model;
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
persistence.logger.debug(`[Grok] Saved model "${model}" to ${configPath}`);
} catch (error) {
persistence.logger.debug(`[Grok] Failed to save model to config:`, error);
}
}
function getInitialGrokModel() {
const localConfig = readGrokLocalConfig();
return process.env[GROK_MODEL_ENV] || localConfig.model || DEFAULT_GROK_MODEL;
}
function getGrokModelSource(explicitModel, localConfig) {
if (explicitModel !== void 0 && explicitModel !== null) {
return "explicit";
} else if (process.env[GROK_MODEL_ENV]) {
return "env-var";
} else if (localConfig.model) {
return "local-config";
} else {
return "default";
}
}
function createGrokBackend(options) {
const localConfig = readGrokLocalConfig();
const apiKey = options.cloudToken || localConfig.token || process.env[GROK_CODE_XAI_API_KEY_ENV] || process.env[XAI_API_KEY_ENV] || process.env[GROK_API_KEY_ENV] || options.apiKey;
if (!apiKey) {
persistence.logger.warn(`[Grok] No API key found. Run 'consortium connect grok' or set ${GROK_CODE_XAI_API_KEY_ENV}.`);
}
const grokCommand = "grok";
const model = determineGrokModel(options.model, localConfig);
const acpFlag = process.env.CONSORTIUM_GROK_ACP_FLAG || "agent stdio";
const grokArgs = acpFlag.split(/\s+/).filter(Boolean);
const backendOptions = {
agentName: "grok",
cwd: options.cwd,
command: grokCommand,
args: grokArgs,
env: {
...options.env,
// Populate all three env-var names so the binary picks up the key
// regardless of which one a given release expects.
...apiKey ? {
[GROK_CODE_XAI_API_KEY_ENV]: apiKey,
[XAI_API_KEY_ENV]: apiKey,
[GROK_API_KEY_ENV]: apiKey
} : {},
[GROK_MODEL_ENV]: model,
NODE_ENV: "production",
DEBUG: ""
},
mcpServers: options.mcpServers,
permissionHandler: options.permissionHandler,
transportHandler: grokTransport,
hasChangeTitleInstruction: (prompt) => {
const lower = prompt.toLowerCase();
return lower.includes("change_title") || lower.includes("change title") || lower.includes("set title") || lower.includes("mcp__consortium__change_title");
}
};
const modelSource = getGrokModelSource(options.model, localConfig);
persistence.logger.debug("[Grok] Creating ACP SDK backend with options:", {
cwd: backendOptions.cwd,
command: backendOptions.command,
args: backendOptions.args,
hasApiKey: !!apiKey,
model,
modelSource,
mcpServerCount: options.mcpServers ? Object.keys(options.mcpServers).length : 0
});
return {
backend: new capabilities.AcpBackend(backendOptions),
model,
modelSource
};
}
class GrokPermissionHandler extends BasePermissionHandler.BasePermissionHandler {
currentPermissionMode = "default";
constructor(session) {
super(session);
}
getLogPrefix() {
return "[Grok]";
}
/**
* Update session reference (override for type visibility)
*/
updateSession(newSession) {
super.updateSession(newSession);
}
/**
* Set the current permission mode
* This affects how tool calls are automatically approved/denied
*/
setPermissionMode(mode) {
this.currentPermissionMode = mode;
persistence.logger.debug(`${this.getLogPrefix()} Permission mode set to: ${mode}`);
}
/**
* Check if a tool should be auto-approved based on permission mode
*/
shouldAutoApprove(toolName, toolCallId, input) {
const alwaysAutoApproveNames = ["change_title", "consortium__change_title", "GrokReasoning", "CodexReasoning", "think", "save_memory"];
const alwaysAutoApproveIds = ["change_title", "save_memory"];
if (alwaysAutoApproveNames.some((name) => toolName.toLowerCase().includes(name.toLowerCase()))) {
return true;
}
if (alwaysAutoApproveIds.some((id) => toolCallId.toLowerCase().includes(id.toLowerCase()))) {
return true;
}
switch (this.currentPermissionMode) {
case "yolo":
return true;
case "safe-yolo":
return true;
case "read-only":
const writeTools = ["write", "edit", "create", "delete", "patch", "fs-edit"];
const isWriteTool = writeTools.some((wt) => toolName.toLowerCase().includes(wt));
return !isWriteTool;
case "default":
default:
return false;
}
}
/**
* Handle a tool permission request
* @param toolCallId - The unique ID of the tool call
* @param toolName - The name of the tool being called
* @param input - The input parameters for the tool
* @returns Promise resolving to permission result
*/
async handleToolCall(toolCallId, toolName, input) {
if (this.shouldAutoApprove(toolName, toolCallId, input)) {
persistence.logger.debug(`${this.getLogPrefix()} Auto-approving tool ${toolName} (${toolCallId}) in ${this.currentPermissionMode} mode`);
this.session.updateAgentState((currentState) => ({
...currentState,
completedRequests: {
...currentState.completedRequests,
[toolCallId]: {
tool: toolName,
arguments: input,
createdAt: Date.now(),
completedAt: Date.now(),
status: "approved",
decision: this.currentPermissionMode === "yolo" ? "approved_for_session" : "approved"
}
}
}));
return {
decision: this.currentPermissionMode === "yolo" ? "approved_for_session" : "approved"
};
}
return this.createPendingRequest(toolCallId, toolName, input);
}
}
class GrokReasoningProcessor extends BaseReasoningProcessor.BaseReasoningProcessor {
getToolName() {
return "GrokReasoning";
}
getLogPrefix() {
return "[GrokReasoningProcessor]";
}
/**
* Process a reasoning chunk from agent_thought_chunk.
* Grok sends reasoning as chunks, we accumulate them similar to Codex.
*/
processChunk(chunk) {
this.processInput(chunk);
}
/**
* Complete the reasoning section.
* Called when reasoning is complete (e.g., when status changes to idle).
* Returns true if reasoning was actually completed, false if there was nothing to complete.
*/
complete() {
return this.completeReasoning();
}
}
class GrokDiffProcessor {
previousDiffs = /* @__PURE__ */ new Map();
// Track diffs per file path
onMessage = null;
constructor(onMessage) {
this.onMessage = onMessage || null;
}
/**
* Process an fs-edit event and check if it contains diff information
*/
processFsEdit(path, description, diff) {
persistence.logger.debug(`[GrokDiffProcessor] Processing fs-edit for path: ${path}`);
if (diff) {
this.processDiff(path, diff, description);
} else {
const simpleDiff = `File edited: ${path}${description ? ` - ${description}` : ""}`;
this.processDiff(path, simpleDiff, description);
}
}
/**
* Process a tool result that may contain diff information
*/
processToolResult(toolName, result, callId) {
if (result && typeof result === "object") {
const diff = result.diff || result.unified_diff || result.patch;
const path = result.path || result.file;
if (diff && path) {
persistence.logger.debug(`[GrokDiffProcessor] Found diff in tool result: ${toolName} (${callId})`);
this.processDiff(path, diff, result.description);
} else if (result.changes && typeof result.changes === "object") {
for (const [filePath, change] of Object.entries(result.changes)) {
const changeDiff = change.diff || change.unified_diff || JSON.stringify(change);
this.processDiff(filePath, changeDiff, change.description);
}
}
}
}
/**
* Process a unified diff and check if it has changed from the previous value
*/
processDiff(path, unifiedDiff, description) {
const previousDiff = this.previousDiffs.get(path);
if (previousDiff !== unifiedDiff) {
persistence.logger.debug(`[GrokDiffProcessor] Unified diff changed for ${path}, sending GrokDiff tool call`);
const callId = node_crypto.randomUUID();
const toolCall = {
type: "tool-call",
name: "GrokDiff",
callId,
input: {
unified_diff: unifiedDiff,
path,
description
},
id: node_crypto.randomUUID()
};
this.onMessage?.(toolCall);
const toolResult = {
type: "tool-call-result",
callId,
output: {
status: "completed"
},
id: node_crypto.randomUUID()
};
this.onMessage?.(toolResult);
}
this.previousDiffs.set(path, unifiedDiff);
persistence.logger.debug(`[GrokDiffProcessor] Updated stored diff for ${path}`);
}
/**
* Reset the processor state (called on task_complete or turn_aborted)
*/
reset() {
persistence.logger.debug("[GrokDiffProcessor] Resetting diff state");
this.previousDiffs.clear();
}
/**
* Set the message callback for sending messages directly
*/
setMessageCallback(callback) {
this.onMessage = callback;
}
/**
* Get the current diff value for a specific path
*/
getCurrentDiff(path) {
return this.previousDiffs.get(path) || null;
}
/**
* Get all tracked diffs
*/
getAllDiffs() {
return new Map(this.previousDiffs);
}
}
function hasIncompleteOptions(text) {
const hasOpeningTag = /<options>/i.test(text);
const hasClosingTag = /<\/options>/i.test(text);
return hasOpeningTag && !hasClosingTag;
}
function parseOptionsFromText(text) {
const optionsRegex = /<options>\s*([\s\S]*?)\s*<\/options>/i;
const match = text.match(optionsRegex);
if (!match) {
return { text: text.trim(), options: [] };
}
const optionsBlock = match[1];
const optionRegex = /<option>(.*?)<\/option>/gi;
const options = [];
let optionMatch;
while ((optionMatch = optionRegex.exec(optionsBlock)) !== null) {
const optionText = optionMatch[1].trim();
if (optionText) {
options.push(optionText);
}
}
const textWithoutOptions = text.replace(optionsRegex, "").trim();
return { text: textWithoutOptions, options };
}
function formatOptionsXml(options) {
if (options.length === 0) {
return "";
}
return "\n<options>\n" + options.map((opt) => ` <option>${opt}</option>`).join("\n") + "\n</options>";
}
class ConversationHistory {
messages = [];
maxMessages;
maxCharacters;
currentModel;
constructor(options = {}) {
this.maxMessages = options.maxMessages ?? 20;
this.maxCharacters = options.maxCharacters ?? 5e4;
}
/**
* Set the current model being used
*/
setCurrentModel(model) {
this.currentModel = model;
}
/**
* Check if content is a duplicate of the last message with the same role.
* Deduplication prevents inflating history when the same message is sent multiple times.
*/
isDuplicate(role, content) {
if (this.messages.length === 0) return false;
for (let i = this.messages.length - 1; i >= 0; i--) {
const msg = this.messages[i];
if (msg.role === role) {
const normalizedNew = content.trim().replace(/\s+/g, " ");
const normalizedExisting = msg.content.replace(/\s+/g, " ");
return normalizedNew === normalizedExisting;
}
}
return false;
}
/**
* Add a user message to history
* Skips duplicate messages to prevent history inflation
*/
addUserMessage(content) {
if (!content.trim()) return;
const trimmedContent = content.trim();
if (this.isDuplicate("user", trimmedContent)) {
persistence.logger.debug(`[ConversationHistory] Skipping duplicate user message (${trimmedContent.length} chars)`);
return;
}
this.messages.push({
role: "user",
content: trimmedContent,
timestamp: Date.now()
});
this.trimHistory();
persistence.logger.debug(`[ConversationHistory] Added user message (${trimmedContent.length} chars), total: ${this.messages.length}`);
}
/**
* Add an assistant response to history
* Skips duplicate messages to prevent history inflation
*/
addAssistantMessage(content) {
if (!content.trim()) return;
const trimmedContent = content.trim();
if (this.isDuplicate("assistant", trimmedContent)) {
persistence.logger.debug(`[ConversationHistory] Skipping duplicate assistant message (${trimmedContent.length} chars)`);
return;
}
this.messages.push({
role: "assistant",
content: trimmedContent,
timestamp: Date.now(),
model: this.currentModel
});
this.trimHistory();
persistence.logger.debug(`[ConversationHistory] Added assistant message (${trimmedContent.length} chars), total: ${this.messages.length}`);
}
/**
* Get the number of messages in history
*/
size() {
return this.messages.length;
}
/**
* Check if there's any history to preserve
*/
hasHistory() {
return this.messages.length > 0;
}
/**
* Clear all history
*/
clear() {
this.messages = [];
persistence.logger.debug("[ConversationHistory] History cleared");
}
/**
* Get formatted context for injecting into a new session.
* This is used when the model changes to preserve conversation context.
*
* @returns Formatted string with previous conversation context, or empty string if no history
*/
getContextForNewSession() {
if (this.messages.length === 0) {
return "";
}
const formattedMessages = this.messages.map((msg) => {
const role = msg.role === "user" ? "User" : "Assistant";
const content = msg.content.length > 2e3 ? msg.content.substring(0, 2e3) + "... [truncated]" : msg.content;
return `${role}: ${content}`;
}).join("\n\n");
return `[PREVIOUS CONVERSATION CONTEXT]
The following is our previous conversation. Continue from where we left off:
${formattedMessages}
[END OF PREVIOUS CONTEXT]
`;
}
/**
* Trim history to stay within limits
*/
trimHistory() {
while (this.messages.length > this.maxMessages) {
this.messages.shift();
}
let totalChars = this.messages.reduce((sum, msg) => sum + msg.content.length, 0);
while (totalChars > this.maxCharacters && this.messages.length > 1) {
const removed = this.messages.shift();
if (removed) {
totalChars -= removed.content.length;
}
}
}
/**
* Get a summary of the conversation for logging/debugging
*/
getSummary() {
const totalChars = this.messages.reduce((sum, msg) => sum + msg.content.length, 0);
const userCount = this.messages.filter((m) => m.role === "user").length;
const assistantCount = this.messages.filter((m) => m.role === "assistant").length;
return `${this.messages.length} messages (${userCount} user, ${assistantCount} assistant), ${totalChars} chars`;
}
}
async function step(label, timeoutMs, fn) {
const t0 = Date.now();
let timer;
try {
const result = await Promise.race([
fn(),
new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(
`Grok startup: '${label}' timed out after ${timeoutMs}ms`
)), timeoutMs);
})
]);
const elapsed = Date.now() - t0;
persistence.logger.debug(`[Grok startup] ${label} ok in ${elapsed}ms`);
return result;
} catch (err) {
const elapsed = Date.now() - t0;
persistence.logger.debug(`[Grok startup] ${label} failed in ${elapsed}ms:`, err instanceof Error ? err.message : err);
throw err;
} finally {
if (timer) clearTimeout(timer);
}
}
async function withRetry(label, fn) {
try {
return await fn();
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
const transient = /timed out|timeout|ETIMEDOUT|ECONNRESET|operation has timed out/i.test(msg);
if (!transient) throw err;
persistence.logger.debug(`[Grok startup] ${label} transient failure (${msg}) \u2014 retrying once`);
await new Promise((r) => setTimeout(r, 750));
return await fn();
}
}
async function runGrok(opts) {
const startupT0 = Date.now();
const sessionTag = node_crypto.randomUUID();
persistence.connectionState.setBackend("Grok");
const api = await persistence.ApiClient.create(opts.credentials);
const settings = await persistence.readSettings();
const machineId = settings?.machineId;
if (!machineId) {
console.error(`[START] No machine ID found in settings, which is unexpected since authAndSetupMachineIfNeeded should have created it. Please report this issue on https://github.com/ConsortiumAI/consortium-cli/issues`);
process.exit(1);
}
persistence.logger.debug(`Using machineId: ${machineId}`);
await step("getOrCreateMachine", 3e4, () => api.getOrCreateMachine({
machineId,
metadata: index.initialMachineMetadata
}));
let cloudToken = void 0;
const currentUserEmail = void 0;
try {
const vendorToken = await step("getVendorToken(xai)", 1e4, () => api.getVendorToken("xai"));
if (typeof vendorToken === "string" && vendorToken.length > 0) {
cloudToken = vendorToken;
persistence.logger.debug("[Grok] Using cloud-registered xAI key");
} else if (vendorToken?.oauth?.access_token) {
cloudToken = vendorToken.oauth.access_token;
persistence.logger.debug("[Grok] Using OAuth token from Consortium cloud");
} else if (vendorToken?.token) {
cloudToken = vendorToken.token;
persistence.logger.debug("[Grok] Using cloud-registered xAI key (envelope)");
}
} catch (error) {
persistence.logger.debug("[Grok] Failed to fetch cloud token:", error);
}
const { state, metadata } = createSessionMetadata.createSessionMetadata({
flavor: "grok",
machineId,
startedBy: opts.startedBy
});
const response = await withRetry("getOrCreateSession", () => step("getOrCreateSession", 9e4, () => api.getOrCreateSession({ tag: sessionTag, metadata, state })));
persistence.logger.debug(`[Grok startup] pre-flight complete in ${Date.now() - startupT0}ms`);
let session;
let permissionHandler;
let isProcessingMessage = false;
let pendingSessionSwap = null;
const applyPendingSessionSwap = () => {
if (pendingSessionSwap) {
persistence.logger.debug("[grok] Applying pending session swap");
session = pendingSessionSwap;
if (permissionHandler) {
permissionHandler.updateSession(pendingSessionSwap);
}
pendingSessionSwap = null;
}
};
const { session: initialSession, reconnectionHandle } = setupOfflineReconnection.setupOfflineReconnection({
api,
sessionTag,
metadata,
state,
response,
onSessionSwap: (newSession) => {
if (isProcessingMessage) {
persistence.logger.debug("[grok] Session swap requested during message processing - queueing");
pendingSessionSwap = newSession;
} else {
session = newSession;
if (permissionHandler) {
permissionHandler.updateSession(newSession);
}
}
}
});
session = initialSession;
if (response) {
try {
persistence.logger.debug(`[START] Reporting session ${response.id} to daemon`);
const result = await index.notifyDaemonSessionStarted(response.id, metadata);
if (result.error) {
persistence.logger.debug(`[START] Failed to report to daemon (may not be running):`, result.error);
} else {
persistence.logger.debug(`[START] Reported session ${response.id} to daemon`);
}
} catch (error) {
persistence.logger.debug("[START] Failed to report to daemon (may not be running):", error);
}
}
const attachments = GeminiDisplay.createRunnerAttachments({
credentials: opts.credentials,
sessionId: response?.id ?? sessionTag,
logTag: "grok",
agentId: "opencode",
getCurrentModel: () => currentModel ?? null
});
const messageQueue = new index.MessageQueue2((mode) => index.hashObject({
permissionMode: mode.permissionMode,
model: mode.model
}));
const conversationHistory = new ConversationHistory({ maxMessages: 20, maxCharacters: 5e4 });
let currentPermissionMode = void 0;
let currentModel = void 0;
let hasStoredFirstMessage = false;
session.onUserMessage((message) => {
attachments.observeUserMessage(message);
if (!hasStoredFirstMessage && message.content.text) {
hasStoredFirstMessage = true;
session.updateMetadata((m) => ({
...m,
firstMessage: message.content.text.substring(0, 100)
}));
}
let messagePermissionMode = currentPermissionMode;
if (message.meta?.permissionMode) {
const validModes = ["default", "read-only", "safe-yolo", "yolo"];
if (validModes.includes(message.meta.permissionMode)) {
messagePermissionMode = message.meta.permissionMode;
currentPermissionMode = messagePermissionMode;
updatePermissionMode(messagePermissionMode);
persistence.logger.debug(`[Grok] Permission mode updated from user message to: ${currentPermissionMode}`);
} else {
persistence.logger.debug(`[Grok] Invalid permission mode received: ${message.meta.permissionMode}`);
}
} else {
persistence.logger.debug(`[Grok] User message received with no permission mode override, using current: ${currentPermissionMode ?? "default (effective)"}`);
}
if (currentPermissionMode === void 0) {
currentPermissionMode = "default";
updatePermissionMode("default");
}
let messageModel = currentModel;
if (message.meta?.hasOwnProperty("model")) {
if (message.meta.model === null) {
messageModel = void 0;
currentModel = void 0;
} else if (message.meta.model) {
const previousModel = currentModel;
messageModel = message.meta.model;
currentModel = messageModel;
if (previousModel !== messageModel) {
updateDisplayedModel(messageModel, true);
messageBuffer.addMessage(`Model changed to: ${messageModel}`, "system");
persistence.logger.debug(`[Grok] Model changed from ${previousModel} to ${messageModel}`);
}
}
}
const originalUserMessage = message.content.text;
let fullPrompt = originalUserMessage;
const grokBrevityPrompt = [
"Be concise. Answer the user's question directly without preamble.",
'Do NOT narrate your reasoning, planning, or analysis as prose. Do NOT write sections like "The user query is:", "I need to use tools:", "First,", "Now,", or "Explanation".',
'Do NOT write `<tool_call>...</tool_call>` markup, `<parameter name="...">` blocks, or `INPUT { ... } OUTPUT [...]` blocks in your message text. Tool calls must go through the structured tool API only \u2014 they are surfaced to the user automatically. Never describe a tool call you are about to make or just made.',
'Do NOT iterate on your own output unprompted. If you generate an image, video, file, or any other artifact, produce exactly ONE per user request. Do not regenerate "a better version" unless the user explicitly asks for variations.',
'When you finish, stop. Do not append "Explanation", "Confidence", "Workflow", or similar trailing meta-commentary.'
].join("\n");
if (isFirstMessage) {
const mobilePrompt = message.meta?.appendSystemPrompt;
const combined = mobilePrompt ? `${grokBrevityPrompt}
${mobilePrompt}` : grokBrevityPrompt;
fullPrompt = `${combined}
${originalUserMessage}`;
isFirstMessage = false;
} else {
fullPrompt = `${grokBrevityPrompt}
${originalUserMessage}`;
}
const mode = {
permissionMode: messagePermissionMode || "default",
model: messageModel,
originalUserMessage
// Store original message separately
};
messageQueue.push(fullPrompt, mode);
conversationHistory.addUserMessage(originalUserMessage);
});
let thinking = false;
session.keepAlive(thinking, "remote");
const keepAliveInterval = setInterval(() => {
session.keepAlive(thinking, "remote");
}, 2e3);
let isFirstMessage = true;
const sendReady = () => {
session.sendSessionEvent({ type: "ready" });
try {
api.push().sendToAllDevices(
"It's ready!",
"Grok is waiting for your command",
{ sessionId: session.sessionId }
);
} catch (pushError) {
persistence.logger.debug("[Grok] Failed to send ready push", pushError);
}
};
const emitReadyIfIdle = () => {
if (shouldExit) {
return false;
}
if (thinking) {
return false;
}
if (isResponseInProgress) {
return false;
}
if (messageQueue.size() > 0) {
return false;
}
sendReady();
return true;
};
let abortController = new AbortController();
let shouldExit = false;
let grokBackend = null;
let acpSessionId = null;
let wasSessionCreated = false;
async function handleAbort() {
persistence.logger.debug("[Grok] Abort requested - stopping current task");
session.sendAgentMessage("grok", {
type: "turn_aborted",
id: node_crypto.randomUUID()
});
reasoningProcessor.abort();
diffProcessor.reset();
try {
abortController.abort();
messageQueue.reset();
if (grokBackend && acpSessionId) {
await grokBackend.cancel(acpSessionId);
}
persistence.logger.debug("[Grok] Abort completed - session remains active");
} catch (error) {
persistence.logger.debug("[Grok] Error during abort:", error);
} finally {
abortController = new AbortController();
}
}
const handleKillSession = async () => {
persistence.logger.debug("[Grok] Kill session requested - terminating process");
await handleAbort();
persistence.logger.debug("[Grok] Abort completed, proceeding with termination");
try {
if (session) {
session.updateMetadata((currentMetadata) => ({
...currentMetadata,
lifecycleState: "archived",
lifecycleStateSince: Date.now(),
archivedBy: "cli",
archiveReason: "User terminated"
}));
session.sendSessionDeath();
await session.flush();
await session.close();
}
index.stopCaffeinate();
consortiumServer.stop();
if (grokBackend) {
await grokBackend.dispose();
}
persistence.logger.debug("[Grok] Session termination complete, exiting");
process.exit(0);
} catch (error) {
persistence.logger.debug("[Grok] Error during session termination:", error);
process.exit(1);
}
};
session.rpcHandlerManager.registerHandler("abort", handleAbort);
index.registerKillSessionHandler(session.rpcHandlerManager, handleKillSession);
const messageBuffer = new index.MessageBuffer();
const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
let inkInstance = null;
let displayedModel = getInitialGrokModel();
const localConfig = readGrokLocalConfig();
persistence.logger.debug(`[grok] Initial model setup: env[GROK_MODEL_ENV]=${process.env[GROK_MODEL_ENV] || "not set"}, localConfig=${localConfig.model || "not set"}, displayedModel=${displayedModel}`);
const updateDisplayedModel = (model, saveToConfig = false) => {
if (model === void 0) {
persistence.logger.debug(`[grok] updateDisplayedModel called with undefined, skipping update`);
return;
}
const oldModel = displayedModel;
displayedModel = model;
persistence.logger.debug(`[grok] updateDisplayedModel called: oldModel=${oldModel}, newModel=${model}, saveToConfig=${saveToConfig}`);
if (saveToConfig) {
saveGrokModelToConfig(model);
}
if (hasTTY && oldModel !== model) {
persistence.logger.debug(`[grok] Adding model update message to buffer: [MODEL:${model}]`);
messageBuffer.addMessage(`[MODEL:${model}]`, "system");
} else if (hasTTY) {
persistence.logger.debug(`[grok] Model unchanged, skipping update message`);
}
};
if (hasTTY) {
console.clear();
const DisplayComponent = () => {
const currentModelValue = displayedModel || "grok-2.5-pro";
return React.createElement(GeminiDisplay.GeminiDisplay, {
messageBuffer,
logPath: process.env.DEBUG ? persistence.logger.getLogPath() : void 0,
currentModel: currentModelValue,
onExit: async () => {
persistence.logger.debug("[grok]: Exiting agent via Ctrl-C");
shouldExit = true;
await handleAbort();
}
});
};
inkInstance = ink.render(React.createElement(DisplayComponent), {
exitOnCtrlC: false,
patchConsole: false
});
const initialModelName = displayedModel || "grok-2.5-pro";
persistence.logger.debug(`[grok] Sending initial model to UI: ${initialModelName}`);
messageBuffer.addMessage(`[MODEL:${initialModelName}]`, "system");
}
if (hasTTY) {
process.stdin.resume();
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.setEncoding("utf8");
}
const consortiumServer = await index.startConsortiumServer(session, {
sendArtifact: (msg) => session.sendAgentMessage("grok", msg)
});
const bridgeCommand = path$1.join(persistence.projectPath(), "bin", "consortium-mcp.mjs");
const mcpServers = {
consortium: {
command: bridgeCommand,
args: ["--url", consortiumServer.url]
}
};
permissionHandler = new GrokPermissionHandler(session);
const reasoningProcessor = new GrokReasoningProcessor((message) => {
session.sendAgentMessage("grok", message);
});
const diffProcessor = new GrokDiffProcessor((message) => {
session.sendAgentMessage("grok", message);
});
const updatePermissionMode = (mode) => {
permissionHandler.setPermissionMode(mode);
};
let accumulatedResponse = "";
let isResponseInProgress = false;
let sendPromptInFlight = false;
let hadToolCallInTurn = false;
const toolCallCountsThisTurn = /* @__PURE__ */ new Map();
const GENERATION_TOOL_PATTERNS = [/image[_-]?gen/i, /video[_-]?gen/i, /audio[_-]?gen/i, /imagine/i, /image[_-]?generation/i];
const isGenerationTool = (name) => GENERATION_TOOL_PATTERNS.some((re) => re.test(name));
let pendingChangeTitle = false;
let changeTitleCompleted = false;
let taskStartedSent = false;
function setupGrokMessageHandler(backend) {
backend.onMessage((msg) => {
switch (msg.type) {
case "model-output":
if (msg.textDelta) {
if (!sendPromptInFlight) {
session.sendAgentMessage("grok", {
type: "message",
message: msg.textDelta
});
break;
}
if (!isResponseInProgress) {
messageBuffer.removeLastMessage("system");
messageBuffer.addMessage(msg.textDelta, "assistant");
isResponseInProgress = true;
persistence.logger.debug(`[grok] Started new response, first chunk length: ${msg.textDelta.length}`);
} else {
messageBuffer.updateLastMessage(msg.textDelta, "assistant");
persistence.logger.debug(`[grok] Updated response, chunk length: ${msg.textDelta.length}, total accumulated: ${accumulatedResponse.length + msg.textDelta.length}`);
}
accumulatedResponse += msg.textDelta;
}
break;
case "status":
const statusDetail = msg.detail ? typeof msg.detail === "object" ? JSON.stringify(msg.detail) : String(msg.detail) : "";
persistence.logger.debug(`[grok] Status changed: ${msg.status}${statusDetail ? ` - ${statusDetail}` : ""}`);
if (msg.status === "error") {
persistence.logger.debug(`[grok] \u26A0\uFE0F Error status received: ${statusDetail || "Unknown error"}`);
session.sendAgentMessage("grok", {
type: "turn_aborted",
id: node_crypto.randomUUID()
});
}
if (msg.status === "running") {
thinking = true;
session.keepAlive(thinking, "remote");
if (!taskStartedSent) {
session.sendAgentMessage("grok", {
type: "task_started",
id: node_crypto.randomUUID()
});
taskStartedSent = true;
}
messageBuffer.addMessage("Thinking...", "system");
} else if (msg.status === "idle" || msg.status === "stopped") {
reasoningProcessor.complete();
} else if (msg.status === "error") {
thinking = false;
session.keepAlive(thinking, "remote");
accumulatedResponse = "";
isResponseInProgress = false;
let errorMessage = "Unknown error";
if (msg.detail) {
if (typeof msg.detail === "object") {
const detailObj = msg.detail;
errorMessage = detailObj.message || detailObj.details || JSON.stringify(detailObj);
} else {
errorMessage = String(msg.detail);
}
}
if (errorMessage.includes("Authentication required")) {
errorMessage = `Authentication required.
For Google Workspace accounts, run: consortium grok project set <project-id>
Or use a different Google account: consortium connect grok
Guide: https://goo.gle/grok-cli-auth-docs#workspace-gca`;
}
messageBuffer.addMessage(`Error: ${errorMessage}`, "status");
session.sendAgentMessage("grok", {
type: "message",
message: `Error: ${errorMessage}`
});
}
break;
case "tool-call":
hadToolCallInTurn = true;
{
const prevCount = toolCallCountsThisTurn.get(msg.toolName) ?? 0;
toolCallCountsThisTurn.set(msg.toolName, prevCount + 1);
if (prevCount >= 1 && isGenerationTool(msg.toolName)) {
persistence.logger.debug(`[grok] \u26A0\uFE0F Duplicate generation tool call this turn: ${msg.toolName} (count=${prevCount + 1}). Model is self-iterating; consider tightening system prompt.`);
}
}
const toolArgs = msg.args ? JSON.stringify(msg.args).substring(0, 100) : "";
const isInvestigationTool = msg.toolName === "codebase_investigator" || typeof msg.toolName === "string" && msg.toolName.includes("investigator");
persistence.logger.debug(`[grok] \u{1F527} Tool call received: ${msg.toolName} (${msg.callId})${isInvestigationTool ? " [INVESTIGATION]" : ""}`);
if (isInvestigationTool && msg.args && typeof msg.args === "object" && "objective" in msg.args) {
persistence.logger.debug(`[grok] \u{1F50D} Investigation objective: ${String(msg.args.objective).substring(0, 150)}...`);
}
messageBuffer.addMessage(`Executing: ${msg.toolName}${toolArgs ? ` ${toolArgs}${toolArgs.length >= 100 ? "..." : ""}` : ""}`, "tool");
session.sendAgentMessage("grok", {
type: "tool-call",
name: msg.toolName,
callId: msg.callId,
input: msg.args,
id: node_crypto.randomUUID()
});
break;
case "tool-result":
if (msg.toolName === "change_title" || msg.callId?.includes("change_title") || msg.toolName === "consortium__change_title") {
changeTitleCompleted = true;
persistence.logger.debug("[grok] change_title completed");
}
const isError = msg.result && typeof msg.result === "object" && "error" in msg.result;
const resultText = typeof msg.result === "string" ? msg.result.substring(0, 200) : JSON.stringify(msg.result).substring(0, 200);
const truncatedResult = resultText + (typeof msg.result === "string" && msg.result.length > 200 ? "..." : "");
const resultSize = typeof msg.result === "string" ? msg.result.length : JSON.stringify(msg.result).length;
persistence.logger.debug(`[grok] ${isError ? "\u274C" : "\u2705"} Tool result received: ${msg.toolName} (${msg.callId}) - Size: ${resultSize} bytes${isError ? " [ERROR]" : ""}`);
if (!isError) {
diffProcessor.processToolResult(msg.toolName, msg.result, msg.callId);
}
if (isError) {
const errorMsg = msg.result.error || "Tool call failed";
persistence.logger.debug(`[grok] \u274C Tool call error: ${errorMsg.substring(0, 300)}`);
messageBuffer.addMessage(`Error: ${errorMsg}`, "status");
} else {
if (resultSize > 1e3) {
persistence.logger.debug(`[grok] \u2705 Large tool result (${resultSize} bytes) -