File size: 4,975 Bytes
20f108c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
import { homedir } from "os";
import { resolve } from "path";

export interface PhantomConfig {
  agents: {
    maxInstances: number;
    defaultModel: string;
    defaultProvider: string;
    heartbeatInterval: number;
  };
  ui: {
    borderStyle: string;
    animationSpeed: number;
  };
  providers: {
    openai: { apiKey: string; baseUrl: string };
    anthropic: { apiKey: string };
    ollama: { baseUrl: string; model: string };
  };
}

const CONFIG_DIR = resolve(homedir(), ".config", "phantom");
const CONFIG_PATH = resolve(CONFIG_DIR, "config.json");

export const defaultConfig: PhantomConfig = {
  agents: {
    maxInstances: 8,
    defaultModel: "gpt-4o",
    defaultProvider: "openai",
    heartbeatInterval: 3000,
  },
  ui: {
    borderStyle: "line",
    animationSpeed: 50,
  },
  providers: {
      openai: { apiKey: process.env.OPENAI_API_KEY || "", baseUrl: "https://api.openai.com/v1" },
      anthropic: { apiKey: process.env.ANTHROPIC_API_KEY || "" },
      ollama: { baseUrl: process.env.OLLAMA_HOST || "http://localhost:11434", model: "llama3" },
    },
};

export function loadConfig(): PhantomConfig {
  try {
    if (!existsSync(CONFIG_DIR)) {
      mkdirSync(CONFIG_DIR, { recursive: true });
    }
    if (!existsSync(CONFIG_PATH)) {
      writeFileSync(CONFIG_PATH, JSON.stringify(defaultConfig, null, 2));
      return defaultConfig;
    }
    const raw = readFileSync(CONFIG_PATH, "utf-8");
    const userConfig = JSON.parse(raw);
    
    // Deep merge: user config overrides defaults, preserving nested objects
    function deepMerge(target: any, source: any): any {
      const result = { ...target };
      for (const key of Object.keys(source)) {
        const sourceVal = source[key];
        const targetVal = target[key];
        if (sourceVal && typeof sourceVal === "object" && !Array.isArray(sourceVal) &&
            targetVal && typeof targetVal === "object" && !Array.isArray(targetVal)) {
          result[key] = deepMerge(targetVal, sourceVal);
        } else {
          result[key] = sourceVal;
        }
      }
      return result;
    }
    
    return deepMerge(defaultConfig, userConfig) as PhantomConfig;
  } catch {
    return defaultConfig;
  }
}

export interface EnvInfo {
  tty: boolean;
  interactive: boolean;
  platform: string;
  terminal: string;
  colors: number;
  hasTrueColor: boolean;
  has256: boolean;
  cols: number;
  rows: number;
  screenSize: "tiny" | "small" | "medium" | "large" | "huge";
  inputMode: string;
  isTermux: boolean;
  isTmux: boolean;
  isWSL: boolean;
  isWindows: boolean;
}

export function detectEnv(): EnvInfo {
  const isTTY = !!process.stdin.isTTY;
  const isCI = !!process.env.CI || !!process.env.GITHUB_ACTIONS;
  const isTermux = !!(process.env.TERMUX_VERSION || process.env.PREFIX?.startsWith("/data/data/com.termux"));
  const isWindows = process.platform === "win32";
  const isWSL = !!process.env.WSL_DISTRO_NAME;
  const isTmux = !!process.env.TMUX;
  const term = (process.env.TERM || "unknown").toLowerCase();
  const colorterm = (process.env.COLORTERM || "").toLowerCase();

  let terminal = "unknown";
  if (isTermux) terminal = "termux";
  else if (term.includes("kitty")) terminal = "kitty";
  else if (term.includes("alacritty")) terminal = "alacritty";
  else if (term.includes("gnome")) terminal = "gnome";
  else if (term.includes("konsole")) terminal = "konsole";
  else if (term.includes("tmux") || isTmux) terminal = "tmux";
  else if (term.includes("screen")) terminal = "screen";
  else if (term.includes("xterm")) terminal = "xterm";
  else if (isWindows) terminal = "windows-console";
  else if (process.env.TERM_PROGRAM === "iterm2") terminal = "iterm2";
  else if (process.env.TERM_PROGRAM === "Apple_Terminal") terminal = "apple-terminal";
  else if (process.env.VSCODE_INJECTION) terminal = "vscode";

  let colors = 16;
  if (colorterm === "truecolor" || colorterm === "24bit" || term.includes("truecolor") || term.includes("24bit")) {
    colors = 16777216;
  } else if (term.includes("256") || term.includes("xterm")) {
    colors = 256;
  }

  let cols = 80, rows = 24;
  try {
    if (process.stdout.getWindowSize) {
      [cols, rows] = process.stdout.getWindowSize();
      cols = Math.max(cols, 20);
      rows = Math.max(rows, 10);
    }
  } catch {}

  let screenSize: EnvInfo["screenSize"] = "medium";
  if (cols < 60 || rows < 15) screenSize = "tiny";
  else if (cols < 80 || rows < 24) screenSize = "small";
  else if (cols >= 120 && rows >= 40) screenSize = "large";
  else if (cols >= 160 && rows >= 50) screenSize = "huge";

  return {
    tty: isTTY,
    interactive: isTTY && !isCI,
    platform: isTermux ? "termux" : isWSL ? "wsl" : process.platform,
    terminal,
    colors,
    hasTrueColor: colors >= 16777216,
    has256: colors >= 256,
    cols, rows,
    screenSize,
    inputMode: "keyboard",
    isTermux, isTmux, isWSL, isWindows,
  };
}