Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

heymax

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

heymax - npm Package Compare versions

Comparing version
1.1.0
to
1.2.0
+145
dist/copilot/router.js
import { getState, setState } from "../store/db.js";
import { classifyWithLLM } from "./classifier.js";
// ---------------------------------------------------------------------------
// Default configuration
// ---------------------------------------------------------------------------
const DEFAULT_CONFIG = {
enabled: true,
tierModels: {
fast: "gpt-4.1",
standard: "claude-sonnet-4.6",
premium: "claude-opus-4.6",
},
overrides: [
{
name: "design",
keywords: [
"design", "ui", "ux", "css", "layout", "styling", "visual",
"mockup", "wireframe", "frontend design", "tailwind", "responsive",
],
model: "claude-opus-4.6",
},
],
cooldownMessages: 2,
};
// ---------------------------------------------------------------------------
// Module-level state
// ---------------------------------------------------------------------------
let messagesSinceSwitch = 0;
// Short replies that should inherit the previous turn's tier
const FOLLOW_UP_PATTERNS = [
"yes", "no", "do it", "go ahead", "sure", "sounds good", "looks good",
"perfect", "+1", "please", "yep", "yup", "nope", "nah", "ok", "okay",
"got it", "cool", "nice", "great", "alright", "right",
];
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Strip channel prefixes and trim whitespace. */
function sanitize(prompt) {
return prompt
.replace(/^\[via telegram\]\s*/i, "")
.replace(/^\[via tui\]\s*/i, "")
.trim();
}
/** Word-boundary match that avoids partial-word hits (e.g. "ui" ≠ "fruit"). */
function wordMatch(text, keyword) {
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`\\b${escaped}\\b`, "i").test(text);
}
// ---------------------------------------------------------------------------
// Config management
// ---------------------------------------------------------------------------
export function getRouterConfig() {
const stored = getState("router_config");
if (stored) {
try {
return { ...DEFAULT_CONFIG, ...JSON.parse(stored) };
}
catch {
return { ...DEFAULT_CONFIG };
}
}
return { ...DEFAULT_CONFIG };
}
export function updateRouterConfig(partial) {
const current = getRouterConfig();
const merged = {
...current,
...partial,
tierModels: {
...current.tierModels,
...(partial.tierModels ?? {}),
},
overrides: partial.overrides ?? current.overrides,
};
setState("router_config", JSON.stringify(merged));
return merged;
}
// ---------------------------------------------------------------------------
// Classification
// ---------------------------------------------------------------------------
/**
* Classify a message using GPT-4.1. Falls back to "standard" if the LLM
* is unavailable. Background tasks and follow-ups are handled deterministically.
*/
async function classifyMessage(prompt, recentTiers, client) {
const text = sanitize(prompt);
const lower = text.toLowerCase();
// Background tasks → always standard
if (lower.startsWith("[background task completed]"))
return "standard";
// Short follow-ups inherit the previous tier
if (text.length < 20 && recentTiers.length > 0) {
const isFollowUp = FOLLOW_UP_PATTERNS.some((p) => lower === p || lower === p + ".");
if (isFollowUp)
return recentTiers[0];
}
// LLM classification
if (client) {
const tier = await classifyWithLLM(client, text);
if (tier) {
console.log(`[max] Classifier: ${tier}`);
return tier;
}
}
// Fallback — standard is always safe
console.log(`[max] Classifier (fallback): standard`);
return "standard";
}
// ---------------------------------------------------------------------------
// Main entry point
// ---------------------------------------------------------------------------
export async function resolveModel(prompt, currentModel, recentTiers, client) {
const config = getRouterConfig();
// Router disabled → manual mode
if (!config.enabled) {
messagesSinceSwitch = 0;
return { model: currentModel, tier: null, switched: false, routerMode: "manual" };
}
const text = sanitize(prompt);
// 1. Check overrides first — they bypass cooldown
for (const rule of config.overrides) {
if (rule.keywords.some((kw) => wordMatch(text, kw))) {
const switched = rule.model !== currentModel;
if (switched)
messagesSinceSwitch = 0;
return { model: rule.model, tier: null, overrideName: rule.name, switched, routerMode: "auto" };
}
}
// 2. Classify the message
const tier = await classifyMessage(prompt, recentTiers, client);
const targetModel = config.tierModels[tier];
const wouldSwitch = targetModel !== currentModel;
// 3. Cooldown — prevent rapid switching
if (wouldSwitch && messagesSinceSwitch < config.cooldownMessages) {
messagesSinceSwitch++;
return { model: currentModel, tier, switched: false, routerMode: "auto" };
}
if (wouldSwitch)
messagesSinceSwitch = 0;
else
messagesSinceSwitch++;
return { model: targetModel, tier, switched: wouldSwitch, routerMode: "auto" };
}
//# sourceMappingURL=router.js.map
+35
-2
import express from "express";
import { readFileSync, writeFileSync, existsSync } from "fs";
import { randomBytes } from "crypto";
import { sendToOrchestrator, getWorkers, cancelCurrentMessage } from "../copilot/orchestrator.js";
import { sendToOrchestrator, getWorkers, cancelCurrentMessage, getLastRouteResult } from "../copilot/orchestrator.js";
import { sendPhoto } from "../telegram/bot.js";
import { config, persistModel } from "../config.js";
import { getRouterConfig, updateRouterConfig } from "../copilot/router.js";
import { searchMemories } from "../store/db.js";

@@ -97,3 +98,18 @@ import { listSkills, removeSkill } from "../copilot/skills.js";

if (sseRes) {
sseRes.write(`data: ${JSON.stringify({ type: done ? "message" : "delta", content: text })}\n\n`);
const event = {
type: done ? "message" : "delta",
content: text,
};
if (done) {
const routeResult = getLastRouteResult();
if (routeResult) {
event.route = {
model: routeResult.model,
routerMode: routeResult.routerMode,
tier: routeResult.tier,
...(routeResult.overrideName ? { overrideName: routeResult.overrideName } : {}),
};
}
}
sseRes.write(`data: ${JSON.stringify(event)}\n\n`);
}

@@ -145,2 +161,19 @@ });

});
// Get auto-routing config
app.get("/auto", (_req, res) => {
const routerConfig = getRouterConfig();
const lastRoute = getLastRouteResult();
res.json({
...routerConfig,
currentModel: config.copilotModel,
lastRoute: lastRoute || null,
});
});
// Update auto-routing config
app.post("/auto", (req, res) => {
const body = req.body;
const updated = updateRouterConfig(body);
console.log(`[max] Auto-routing ${updated.enabled ? "enabled" : "disabled"}`);
res.json(updated);
});
// List memories

@@ -147,0 +180,0 @@ app.get("/memory", (_req, res) => {

+10
-6

@@ -53,4 +53,4 @@ import { config as loadEnv } from "dotenv";

};
/** Persist the current model choice to ~/.max/.env */
export function persistModel(model) {
/** Update or append an env var in ~/.max/.env */
function persistEnvVar(key, value) {
ensureMaxHome();

@@ -62,5 +62,5 @@ try {

const updated = lines.map((line) => {
if (line.startsWith("COPILOT_MODEL=")) {
if (line.startsWith(`${key}=`)) {
found = true;
return `COPILOT_MODEL=${model}`;
return `${key}=${value}`;
}

@@ -70,3 +70,3 @@ return line;

if (!found)
updated.push(`COPILOT_MODEL=${model}`);
updated.push(`${key}=${value}`);
writeFileSync(ENV_PATH, updated.join("\n"));

@@ -76,5 +76,9 @@ }

// File doesn't exist — create it
writeFileSync(ENV_PATH, `COPILOT_MODEL=${model}\n`);
writeFileSync(ENV_PATH, `${key}=${value}\n`);
}
}
/** Persist the current model choice to ~/.max/.env */
export function persistModel(model) {
persistEnvVar("COPILOT_MODEL", model);
}
//# sourceMappingURL=config.js.map

@@ -1,140 +0,70 @@

import { config } from "../config.js";
const CLASSIFICATION_TIMEOUT_MS = 10_000;
const MODEL_CACHE_TTL_MS = 5 * 60_000; // refresh available models every 5 min
const CLASSIFICATION_PROMPT = `Classify this request's complexity. Reply with exactly one word: SIMPLE, MEDIUM, or COMPLEX.
// ---------------------------------------------------------------------------
// Persistent GPT-4.1 classifier session
// ---------------------------------------------------------------------------
const CLASSIFIER_MODEL = "gpt-4.1";
const CLASSIFY_TIMEOUT_MS = 8_000;
const SYSTEM_PROMPT = `You are a message complexity classifier for an AI assistant called Max. Your ONLY job is to classify incoming user messages into one of three tiers. Respond with ONLY the tier name — nothing else.
SIMPLE: greetings, factual lookups, yes/no questions, status checks, small talk, one-line answers
MEDIUM: explanations, summaries, code review, moderate analysis, single-file edits
COMPLEX: multi-step coding, architecture, debugging, research, creative writing, multi-file changes
Tiers:
- FAST: Greetings, thanks, acknowledgments, simple yes/no, trivial factual questions ("what time is it?", "hello", "thanks"), casual chat with no technical depth.
- STANDARD: Coding tasks, file operations, tool usage requests, moderate reasoning, questions about technical topics, requests to create/check/manage things, anything involving code or development workflow.
- PREMIUM: Complex architecture decisions, deep analysis, multi-step reasoning, comparing trade-offs, detailed explanations of complex topics, debugging intricate issues, designing systems, strategic planning.
Request: `;
Rules:
- If unsure, respond STANDARD (it's the safe default).
- Respond with exactly one word: FAST, STANDARD, or PREMIUM.`;
let classifierSession;
let classifierSessionModel;
let classifierCreatePromise;
// Cached available model IDs
let cachedModelIds;
let cacheTimestamp = 0;
/** Refresh the available models cache if stale. */
async function getAvailableModels(client) {
if (cachedModelIds && Date.now() - cacheTimestamp < MODEL_CACHE_TTL_MS) {
return cachedModelIds;
}
try {
const models = await client.listModels();
cachedModelIds = new Set(models.map((m) => m.id));
cacheTimestamp = Date.now();
console.log(`[max] Eco: cached ${cachedModelIds.size} available models`);
return cachedModelIds;
}
catch (err) {
console.error(`[max] Eco: failed to list models, using stale cache`);
return cachedModelIds || new Set();
}
}
/** Pick the cheapest available model for classification. */
async function pickClassifierModel(client) {
const available = await getAvailableModels(client);
if (available.size === 0) {
return config.copilotModel; // no model data — use whatever the user has configured
}
try {
// We need billing info which the Set doesn't have — use the cached list call
const models = await client.listModels();
const sorted = [...models]
.filter((m) => m.billing?.multiplier !== undefined)
.sort((a, b) => (a.billing?.multiplier ?? 99) - (b.billing?.multiplier ?? 99));
if (sorted.length > 0)
return sorted[0].id;
return models[0]?.id || config.copilotModel;
}
catch {
return config.copilotModel;
}
}
async function ensureClassifierSession(client) {
const targetModel = await pickClassifierModel(client);
// Recreate if model changed (e.g., new cheaper model became available)
if (classifierSession && classifierSessionModel === targetModel)
let sessionClient;
async function ensureSession(client) {
// Recreate if the client changed (e.g. after a reset)
if (classifierSession && sessionClient === client) {
return classifierSession;
if (classifierCreatePromise)
return classifierCreatePromise;
classifierCreatePromise = (async () => {
console.log(`[max] Creating eco mode classifier session (${targetModel})`);
const session = await client.createSession({
model: targetModel,
streaming: false,
systemMessage: {
content: "You are a request classifier. Reply with exactly one word: SIMPLE, MEDIUM, or COMPLEX. Nothing else.",
},
});
classifierSessionModel = targetModel;
console.log(`[max] Classifier session ready (${targetModel})`);
return session;
})();
try {
classifierSession = await classifierCreatePromise;
return classifierSession;
}
finally {
classifierCreatePromise = undefined;
// Destroy stale session
if (classifierSession) {
classifierSession.destroy().catch(() => { });
classifierSession = undefined;
}
classifierSession = await client.createSession({
model: CLASSIFIER_MODEL,
streaming: false,
systemMessage: { content: SYSTEM_PROMPT },
});
sessionClient = client;
return classifierSession;
}
function parseClassification(response) {
const normalized = response.trim().toUpperCase();
// Match the first standalone classification word
const match = normalized.match(/\b(SIMPLE|MEDIUM|COMPLEX)\b/);
if (match) {
return match[1].toLowerCase();
}
console.log(`[max] Eco classifier: could not parse "${response}", defaulting to MEDIUM`);
return "medium";
}
const TIER_MAP = {
FAST: "fast",
STANDARD: "standard",
PREMIUM: "premium",
};
/**
* Resolve a tier model to an available model ID.
* Returns the tier model if available, otherwise falls back to the current model.
* Classify a message using GPT-4.1.
* Returns the tier, or null if the classifier is unavailable / times out.
*/
function resolveModel(tierModel, available, fallback) {
if (available.has(tierModel))
return tierModel;
console.log(`[max] Eco: model "${tierModel}" not available, falling back to "${fallback}"`);
return fallback;
}
/**
* Classify a user prompt and return the model ID to use.
* Validates the target model against available models.
* Returns the current model on any failure (no disruption).
*/
export async function classifyAndRoute(client, prompt) {
export async function classifyWithLLM(client, message) {
try {
const [session, available] = await Promise.all([
ensureClassifierSession(client),
getAvailableModels(client),
]);
const result = await session.sendAndWait({ prompt: `${CLASSIFICATION_PROMPT}${prompt}` }, CLASSIFICATION_TIMEOUT_MS);
const responseText = result?.data?.content || "";
const tier = parseClassification(responseText);
const tierModel = config.ecoTiers[tier];
const model = resolveModel(tierModel, available, config.copilotModel);
console.log(`[max] Eco classifier: "${tier}" → ${model}${model !== tierModel ? ` (wanted ${tierModel})` : ""}`);
return { model, tier };
const session = await ensureSession(client);
const result = await session.sendAndWait({ prompt: message }, CLASSIFY_TIMEOUT_MS);
const raw = (result?.data?.content || "").trim().toUpperCase();
return TIER_MAP[raw] ?? "standard";
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error(`[max] Eco classifier failed (using current model): ${msg}`);
// Reset classifier session on error so it gets recreated next time
if (/closed|destroy|disposed|invalid|expired|not found/i.test(msg)) {
console.log(`[max] Classifier error (falling back to heuristics): ${err instanceof Error ? err.message : err}`);
// Destroy broken session so it's recreated next time
if (classifierSession) {
classifierSession.destroy().catch(() => { });
classifierSession = undefined;
classifierSessionModel = undefined;
}
return { model: config.copilotModel, tier: "fallback" };
return null;
}
}
/** Tear down the classifier session (e.g., on client reset). */
export function resetClassifier() {
classifierSession = undefined;
classifierSessionModel = undefined;
classifierCreatePromise = undefined;
cachedModelIds = undefined;
cacheTimestamp = 0;
/** Tear down the classifier session (e.g. on shutdown). */
export function stopClassifier() {
if (classifierSession) {
classifierSession.destroy().catch(() => { });
classifierSession = undefined;
sessionClient = undefined;
}
}
//# sourceMappingURL=classifier.js.map

@@ -10,2 +10,3 @@ import { approveAll } from "@github/copilot-sdk";

import { SESSIONS_DIR } from "../paths.js";
import { resolveModel } from "./router.js";
const MAX_RETRIES = 3;

@@ -26,2 +27,9 @@ const RECONNECT_DELAYS_MS = [1_000, 3_000, 10_000];

let healthCheckTimer;
// Router state — tracks model across the session
let currentSessionModel;
let recentTiers = [];
let lastRouteResult;
export function getLastRouteResult() {
return lastRouteResult;
}
// Persistent orchestrator session

@@ -94,2 +102,3 @@ let orchestratorSession;

orchestratorSession = undefined;
currentSessionModel = undefined;
}

@@ -148,2 +157,3 @@ }

console.log(`[max] Resumed orchestrator session successfully`);
currentSessionModel = config.copilotModel;
return session;

@@ -187,2 +197,3 @@ }

}
currentSessionModel = config.copilotModel;
return session;

@@ -248,2 +259,3 @@ }

orchestratorSession = undefined;
currentSessionModel = undefined;
deleteState(ORCHESTRATOR_SESSION_KEY);

@@ -272,2 +284,16 @@ }

try {
// Route the model before executing
const routeResult = await resolveModel(item.prompt, currentSessionModel || config.copilotModel, recentTiers, copilotClient);
if (routeResult.switched) {
console.log(`[max] Auto: switching to ${routeResult.model} (${routeResult.overrideName || routeResult.tier})`);
config.copilotModel = routeResult.model;
orchestratorSession = undefined;
deleteState(ORCHESTRATOR_SESSION_KEY);
}
if (routeResult.tier) {
recentTiers.push(routeResult.tier);
if (recentTiers.length > 5)
recentTiers = recentTiers.slice(-5);
}
lastRouteResult = routeResult;
const result = await executeOnSession(item.prompt, item.callback);

@@ -274,0 +300,0 @@ item.resolve(result);

@@ -86,6 +86,15 @@ export function getOrchestratorSystemMessage(memorySummary, opts) {

### Model Management
- \`list_models\`: List all available Copilot models with their billing tier. Use when the user asks "what models can I use?" or "which model am I using?"
- \`switch_model\`: Switch to a different model. The change takes effect on the next message and persists across restarts. Use when the user says "switch to gpt-4" or "use claude-sonnet".
### Model Management & Auto-Routing
- \`list_models\`: List all available Copilot models with their billing tier.
- \`switch_model\`: Manually switch to a specific model. **This disables auto mode** — auto will stay off until re-enabled. Use when the user explicitly asks to switch to a specific model.
- \`toggle_auto\`: Enable or disable automatic model routing (auto mode).
**Auto Mode**: Max has built-in automatic model routing that selects the best model for each message:
- **Fast tier** (gpt-4.1): Greetings, acknowledgments, simple factual questions
- **Standard tier** (claude-sonnet-4.6): Coding tasks, tool usage, moderate reasoning
- **Premium tier** (claude-opus-4.6): Complex architecture, deep analysis, multi-step reasoning
- **Design override**: UI/UX/design requests always use claude-opus-4.6
Auto mode runs automatically — you don't need to think about it. It saves cost on simple interactions and ensures complex tasks get the best model. If the user asks about auto mode or model selection, explain how it works. If they want to disable it, use \`toggle_auto\`.
### Self-Management

@@ -92,0 +101,0 @@ - \`restart_max\`: Restart the Max daemon. Use when the user asks you to restart, or when needed to apply changes. You'll go offline briefly and come back automatically.

@@ -11,2 +11,3 @@ import { z } from "zod";

import { getCurrentSourceChannel } from "./orchestrator.js";
import { getRouterConfig, updateRouterConfig } from "./router.js";
function isTimeoutError(err) {

@@ -367,2 +368,7 @@ const msg = err instanceof Error ? err.message : String(err);

persistModel(args.model_id);
// Disable router when manually switching — user has explicit preference
if (getRouterConfig().enabled) {
updateRouterConfig({ enabled: false });
return `Switched model from '${previous}' to '${args.model_id}'. Auto-routing disabled (use /auto or toggle_auto to re-enable). Takes effect on next message.`;
}
return `Switched model from '${previous}' to '${args.model_id}'. Takes effect on next message.`;

@@ -376,2 +382,18 @@ }

}),
defineTool("toggle_auto", {
description: "Enable or disable automatic model routing (auto mode). When enabled, Max automatically picks " +
"the best model (fast/standard/premium) for each message to save cost and optimize speed. " +
"Use when the user asks to turn auto-routing on or off.",
parameters: z.object({
enabled: z.boolean().describe("true to enable auto-routing, false to disable"),
}),
handler: async (args) => {
const updated = updateRouterConfig({ enabled: args.enabled });
if (args.enabled) {
const tiers = updated.tierModels;
return `Auto-routing enabled. Tier models:\n• fast: ${tiers.fast}\n• standard: ${tiers.standard}\n• premium: ${tiers.premium}\n\nMax will automatically pick the best model for each message.`;
}
return `Auto-routing disabled. Using fixed model: ${config.copilotModel}`;
},
}),
defineTool("remember", {

@@ -378,0 +400,0 @@ description: "Save something to Max's long-term memory. Use when the user says 'remember that...', " +

import { Bot } from "grammy";
import { config, persistModel } from "../config.js";
import { sendToOrchestrator, cancelCurrentMessage, getWorkers } from "../copilot/orchestrator.js";
import { sendToOrchestrator, cancelCurrentMessage, getWorkers, getLastRouteResult } from "../copilot/orchestrator.js";
import { chunkMessage, toTelegramMarkdown } from "./formatter.js";

@@ -134,5 +134,18 @@ import { searchMemories } from "../store/db.js";

void (async () => {
const formatted = toTelegramMarkdown(text);
// Append model indicator
const routeResult = getLastRouteResult();
let indicatorSuffix = "";
if (routeResult) {
indicatorSuffix = routeResult.routerMode === "auto"
? `\n\n_⚡ auto · ${routeResult.model}_`
: `\n\n_${routeResult.model}_`;
}
const formatted = toTelegramMarkdown(text) + indicatorSuffix;
const chunks = chunkMessage(formatted);
const fallbackChunks = chunkMessage(text);
const fallbackText = routeResult
? text + (routeResult.routerMode === "auto"
? `\n\n⚡ auto · ${routeResult.model}`
: `\n\n${routeResult.model}`)
: text;
const fallbackChunks = chunkMessage(fallbackText);
const sendChunk = async (chunk, fallback, isFirst) => {

@@ -139,0 +152,0 @@ const opts = isFirst

@@ -99,2 +99,62 @@ import * as readline from "readline";

}
/** Strip ANSI escape sequences to measure visible text width. */
function stripAnsi(str) {
return str.replace(/\x1b\[[0-9;]*m/g, "");
}
/** Wrap ANSI-formatted text at word boundaries to fit within maxWidth visible columns. */
function wrapText(text, maxWidth) {
if (maxWidth <= 0 || stripAnsi(text).length <= maxWidth)
return [text];
const RESET = "\x1b[0m";
const lines = [];
let remaining = text;
while (remaining.length > 0) {
if (stripAnsi(remaining).length <= maxWidth) {
lines.push(remaining);
break;
}
let visCount = 0;
let i = 0;
let lastSpaceI = -1;
const ansiStack = [];
let ansiAtSpace = [];
while (i < remaining.length && visCount < maxWidth) {
const match = remaining.slice(i).match(/^\x1b\[[0-9;]*m/);
if (match) {
if (match[0] === RESET)
ansiStack.length = 0;
else
ansiStack.push(match[0]);
i += match[0].length;
}
else {
if (remaining[i] === " ") {
lastSpaceI = i;
ansiAtSpace = [...ansiStack];
}
visCount++;
i++;
}
}
let breakI;
let openAnsi;
if (lastSpaceI > 0) {
breakI = lastSpaceI;
openAnsi = ansiAtSpace;
}
else {
breakI = i;
openAnsi = [...ansiStack];
}
let line = remaining.slice(0, breakI);
remaining = remaining.slice(breakI + (remaining[breakI] === " " ? 1 : 0));
if (openAnsi.length > 0) {
line += RESET;
if (remaining.length > 0)
remaining = openAnsi.join("") + remaining;
}
lines.push(line);
}
return lines;
}
/** Render a complete markdown document to ANSI (used for proactive/background messages). */

@@ -122,5 +182,14 @@ function renderMarkdown(text) {

: ` ${C.dim("SYS")} `;
const availWidth = (process.stdout.columns || 80) - 10;
const lines = text.split("\n");
for (let i = 0; i < lines.length; i++) {
process.stdout.write((i === 0 ? label : LABEL_PAD) + lines[i] + "\n");
const prefix = i === 0 ? label : LABEL_PAD;
const isCodeLine = stripAnsi(lines[i]).startsWith(" \u2502 ");
if (isCodeLine) {
process.stdout.write(prefix + lines[i] + "\n");
}
else {
const wrapped = wrapText(lines[i], availWidth);
process.stdout.write(prefix + wrapped.join("\n" + LABEL_PAD) + "\n");
}
}

@@ -173,3 +242,10 @@ }

const rendered = applyInlineFormatting(renderLine(line, inStreamCodeBlock));
process.stdout.write(prefix + rendered);
if (inStreamCodeBlock) {
process.stdout.write(prefix + rendered);
}
else {
const availWidth = (process.stdout.columns || 80) - 10;
const wrapped = wrapText(rendered, availWidth);
process.stdout.write(prefix + wrapped.join("\n" + LABEL_PAD));
}
}

@@ -343,6 +419,9 @@ process.stdout.write("\n");

}
function showStatus(model, skillCount) {
function showStatus(model, skillCount, routerInfo) {
const parts = [];
if (model)
parts.push(`${C.dim("model:")} ${C.cyan(model)}`);
if (routerInfo?.enabled) {
parts.push(C.cyan("⚡ auto"));
}
if (skillCount !== undefined)

@@ -359,10 +438,13 @@ parts.push(`${C.dim("skills:")} ${C.cyan(String(skillCount))}`);

let skillCount = 0;
let routerInfo;
let done = 0;
const check = () => {
done++;
if (done === 2)
showStatus(model, skillCount);
if (done === 3)
showStatus(model, skillCount, routerInfo);
};
apiGetSilent("/model", (data) => { model = data?.model || "unknown"; check(); });
apiGetSilent("/skills", (data) => { skillCount = Array.isArray(data) ? data.length : 0; check(); });
apiGetSilent("/auto", (data) => { if (data)
routerInfo = { enabled: Boolean(data.enabled) }; check(); });
}

@@ -442,2 +524,9 @@ // ── SSE connection ────────────────────────────────────────

streamedContent = "";
if (event.route && event.route.routerMode === "auto") {
const r = event.route;
const label = r.overrideName
? `⚡ auto · ${r.model} (${r.overrideName})`
: `⚡ auto · ${r.model}`;
process.stdout.write(`\n${LABEL_PAD}${C.dim(label)}`);
}
process.stdout.write("\n\n\n");

@@ -747,2 +836,15 @@ }

}
function cmdAuto() {
apiGetSilent("/auto", (data) => {
if (!data) {
rl.prompt();
return;
}
const newState = !data.enabled;
apiPost("/auto", { enabled: newState }, () => {
const label = newState ? `${C.green("⚡")} auto on` : `auto off`;
console.log(` ${label}\n`);
});
});
}
function cmdHelp() {

@@ -753,2 +855,3 @@ console.log();

console.log(` ${C.coral("/model")} ${C.dim("[name]")} show or switch model`);
console.log(` ${C.coral("/auto")} toggle auto model routing`);
console.log(` ${C.coral("/memory")} show stored memories`);

@@ -847,2 +950,6 @@ console.log(` ${C.coral("/skills")} list installed skills`);

}
if (trimmed === "/auto") {
cmdAuto();
return;
}
if (trimmed === "/memory") {

@@ -849,0 +956,0 @@ cmdMemory();

{
"name": "heymax",
"version": "1.1.0",
"version": "1.2.0",
"description": "Max — a personal AI assistant for developers, built on the GitHub Copilot SDK",

@@ -5,0 +5,0 @@ "bin": {