OXYGENHUB
# 0bfe88762caa spectra-vscode / src / pure.ts
4.2 KiB · last changed Spectra for VS Code 0.1: set up and chat without the command line (#6) 1 day ago source blame history raw
1
// Helpers with no VS Code in them, so they can be tested with node:test.
2
3
/** Server-sent events: feed chunks, get back each event's data. */
4
export class SseParser {
5
  private buf = "";
6
  private data: string[] = [];
7
  push(chunk: string): string[] {
8
    this.buf += chunk;
9
    const out: string[] = [];
10
    let i: number;
11
    while ((i = this.buf.indexOf("\n")) >= 0) {
12
      const line = this.buf.slice(0, i).replace(/\r$/, "");
13
      this.buf = this.buf.slice(i + 1);
14
      if (line === "") {
15
        if (this.data.length) out.push(this.data.join("\n"));
16
        this.data = [];
17
      } else if (line.startsWith("data:")) {
18
        this.data.push(line.slice(5).replace(/^ /, ""));
19
      }
20
    }
21
    return out;
22
  }
23
}
24
25
export const KINDS: { kind: string; label: string; prefix: string }[] = [
26
  { kind: "claude-code", label: "Claude Code", prefix: "claude" },
27
  { kind: "antigravity", label: "Antigravity", prefix: "agy" },
28
  { kind: "kimi-code", label: "Kimi Code", prefix: "kimi" },
29
  { kind: "codex", label: "Codex", prefix: "codex" },
30
  { kind: "other", label: "Another MCP client", prefix: "agent" },
31
];
32
33
/** A name Spectra accepts: lower-case letters, digits and dashes. */
34
export function validName(n: string): boolean {
35
  return /^[a-z0-9][a-z0-9-]{0,63}$/.test(n);
36
}
37
38
/** A project name from a folder name. */
39
export function nameFromFolder(folder: string): string {
40
  const base = folder.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? "";
41
  const n = base.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+/, "").slice(0, 64);
42
  return n || "project";
43
}
44
45
/** The suggested agent name for a kind on a project, not already taken. */
46
export function suggestAgentName(kind: string, project: string, taken: string[]): string {
47
  const prefix = KINDS.find((k) => k.kind === kind)?.prefix ?? "agent";
48
  const base = `${prefix}-${project}`.slice(0, 60);
49
  if (!taken.includes(base)) return base;
50
  for (let i = 2; ; i++) if (!taken.includes(`${base}-${i}`)) return `${base}-${i}`;
51
}
52
53
export const EXPIRIES: { label: string; arg: string | undefined }[] = [
54
  { label: "8 hours", arg: "8h" },
55
  { label: "A day", arg: "1d" },
56
  { label: "A week", arg: "7d" },
57
  { label: "Until I disable it", arg: undefined },
58
];
59
60
/** `~/x` with the home directory. */
61
export function expandHome(p: string, home: string): string {
62
  return p === "~" ? home : p.startsWith("~/") ? home.replace(/\/$/, "") + p.slice(1) : p;
63
}
64
65
/** The arguments of the stdio shim for an agent. */
66
export function shimArgs(agent: string, home: string): string[] {
67
  return ["mcp", "--agent", agent, "--home", home];
68
}
69
70
/** Antigravity's mcp_config.json with Spectra's server set, keeping the rest. */
71
export function mergeMcpConfig(existing: string, command: string, args: string[]): string {
72
  let cfg: Record<string, unknown> = {};
73
  if (existing.trim()) {
74
    const parsed = JSON.parse(existing);
75
    if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) cfg = parsed as Record<string, unknown>;
76
  }
77
  const servers = (cfg.mcpServers && typeof cfg.mcpServers === "object" ? cfg.mcpServers : {}) as Record<string, unknown>;
78
  servers.spectra = { command, args };
79
  cfg.mcpServers = servers;
80
  return JSON.stringify(cfg, null, 2) + "\n";
81
}
82
83
/** The project whose folder holds `folder`, the deepest if several do. */
84
export function projectForFolder(projects: { name: string; path?: string | null }[], folder: string): string | undefined {
85
  const norm = (p: string) => p.replace(/[\\/]+$/, "");
86
  const f = norm(folder);
87
  let best: { name: string; len: number } | undefined;
88
  for (const p of projects) {
89
    if (!p.path) continue;
90
    const q = norm(p.path);
91
    if ((f === q || f.startsWith(q + "/")) && (!best || q.length > best.len)) best = { name: p.name, len: q.length };
92
  }
93
  return best?.name;
94
}
95
96
/** `12:04` today, else `3 Nov 12:04`. */
97
export function when(ms: number, now = Date.now()): string {
98
  const d = new Date(ms);
99
  const t = d.toTimeString().slice(0, 5);
100
  return new Date(now).toDateString() === d.toDateString() ? t : `${d.getDate()} ${d.toLocaleString("en-GB", { month: "short" })} ${t}`;
101
}
102
103
/** Text for HTML. */
104
export function esc(s: string): string {
105
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
106
}