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

@devlln/helm

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@devlln/helm - npm Package Compare versions

Comparing version
0.1.5
to
0.1.6
+421
bridge/src/codexAutomations.ts
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import path from "node:path";
import type { CodexAutomationSummary, CreateCodexAutomationRequest } from "./types.js";
type TomlValue = string | number | string[];
const DAY_NAMES: Record<string, string> = {
MO: "Monday",
TU: "Tuesday",
WE: "Wednesday",
TH: "Thursday",
FR: "Friday",
SA: "Saturday",
SU: "Sunday",
};
function codexAutomationsDir(): string {
return process.env.CODEX_AUTOMATIONS_DIR?.trim()
|| path.join(homedir(), ".codex", "automations");
}
function parseTomlString(raw: string): string | null {
const trimmed = raw.trim();
if (!trimmed.startsWith("\"") || !trimmed.endsWith("\"")) {
return null;
}
let result = "";
for (let index = 1; index < trimmed.length - 1; index += 1) {
const char = trimmed[index];
if (char !== "\\") {
result += char;
continue;
}
index += 1;
const escaped = trimmed[index];
switch (escaped) {
case "b":
result += "\b";
break;
case "t":
result += "\t";
break;
case "n":
result += "\n";
break;
case "f":
result += "\f";
break;
case "r":
result += "\r";
break;
case "\"":
case "\\":
result += escaped;
break;
default:
result += escaped ?? "";
break;
}
}
return result;
}
function splitTomlArrayItems(raw: string): string[] {
const trimmed = raw.trim();
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
return [];
}
const body = trimmed.slice(1, -1);
const items: string[] = [];
let current = "";
let quoted = false;
let escaping = false;
for (const char of body) {
if (escaping) {
current += `\\${char}`;
escaping = false;
continue;
}
if (char === "\\" && quoted) {
escaping = true;
continue;
}
if (char === "\"") {
quoted = !quoted;
current += char;
continue;
}
if (char === "," && !quoted) {
items.push(current.trim());
current = "";
continue;
}
current += char;
}
if (current.trim()) {
items.push(current.trim());
}
return items;
}
function parseTomlStringArray(raw: string): string[] | null {
const values = splitTomlArrayItems(raw)
.map(parseTomlString)
.filter((value): value is string => value !== null);
return values.length > 0 ? values : [];
}
export function parseCodexAutomationToml(text: string, sourcePath = ""): CodexAutomationSummary | null {
const values = new Map<string, TomlValue>();
for (const rawLine of text.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith("#")) {
continue;
}
const separatorIndex = line.indexOf("=");
if (separatorIndex <= 0) {
continue;
}
const key = line.slice(0, separatorIndex).trim();
const rawValue = line.slice(separatorIndex + 1).trim();
const stringValue = parseTomlString(rawValue);
if (stringValue !== null) {
values.set(key, stringValue);
continue;
}
if (rawValue.startsWith("[")) {
values.set(key, parseTomlStringArray(rawValue) ?? []);
continue;
}
const numberValue = Number(rawValue);
if (Number.isFinite(numberValue)) {
values.set(key, numberValue);
}
}
const id = stringValue(values.get("id"));
const name = stringValue(values.get("name")) ?? id;
if (!id || !name) {
return null;
}
const prompt = stringValue(values.get("prompt")) ?? "";
const cwds = stringArrayValue(values.get("cwds"));
const rrule = stringValue(values.get("rrule"));
const kind = stringValue(values.get("kind")) ?? "manual";
return {
id,
name,
kind,
status: stringValue(values.get("status")) ?? "UNKNOWN",
schedule: rrule,
scheduleSummary: automationScheduleSummary(kind, rrule),
model: stringValue(values.get("model")),
reasoningEffort: stringValue(values.get("reasoning_effort")),
executionEnvironment: stringValue(values.get("execution_environment")),
cwds,
cwd: cwds[0] ?? null,
prompt,
promptPreview: promptPreview(prompt),
createdAt: numberValue(values.get("created_at")),
updatedAt: numberValue(values.get("updated_at")),
sourcePath,
};
}
export async function listCodexAutomations(): Promise<CodexAutomationSummary[]> {
const root = codexAutomationsDir();
let entries: Array<{ isDirectory(): boolean; name: string }>;
try {
entries = (await readdir(root, { withFileTypes: true })).map((entry) => ({
isDirectory: () => entry.isDirectory(),
name: String(entry.name),
}));
} catch {
return [];
}
const automations: CodexAutomationSummary[] = [];
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
const sourcePath = path.join(root, entry.name, "automation.toml");
try {
const automation = parseCodexAutomationToml(await readFile(sourcePath, "utf8"), sourcePath);
if (automation) {
automations.push(automation);
}
} catch {
// A partially-written automation should not hide the rest of the list.
}
}
return automations.sort(automationPrecedes);
}
export async function createCodexAutomation(input: CreateCodexAutomationRequest): Promise<CodexAutomationSummary> {
const name = input.name.trim();
const prompt = input.prompt.trim();
const rrule = input.rrule.trim();
if (!name) {
throw new Error("Automation name is required");
}
if (!prompt) {
throw new Error("Automation prompt is required");
}
if (!rrule) {
throw new Error("Automation schedule is required");
}
const root = codexAutomationsDir();
await mkdir(root, { recursive: true });
const id = await uniqueAutomationId(root, slugify(name));
const automationDir = path.join(root, id);
await mkdir(automationDir, { recursive: false });
const now = Date.now();
const status = normalizedStatus(input.status);
const content = [
"version = 1",
`id = ${tomlString(id)}`,
`kind = "cron"`,
`name = ${tomlString(name)}`,
`prompt = ${tomlString(prompt)}`,
`status = ${tomlString(status)}`,
`rrule = ${tomlString(rrule)}`,
optionalTomlString("model", input.model),
optionalTomlString("reasoning_effort", input.reasoningEffort),
optionalTomlString("execution_environment", input.executionEnvironment),
`cwds = ${tomlStringArray(input.cwd?.trim() ? [input.cwd.trim()] : [])}`,
`created_at = ${now}`,
`updated_at = ${now}`,
"",
].filter((line): line is string => line !== null).join("\n");
const sourcePath = path.join(automationDir, "automation.toml");
await writeFile(sourcePath, content, { encoding: "utf8", flag: "wx" });
const automation = parseCodexAutomationToml(content, sourcePath);
if (!automation) {
throw new Error("Created automation could not be parsed");
}
return automation;
}
function automationPrecedes(lhs: CodexAutomationSummary, rhs: CodexAutomationSummary): number {
const lhsActive = lhs.status.toUpperCase() === "ACTIVE";
const rhsActive = rhs.status.toUpperCase() === "ACTIVE";
if (lhsActive !== rhsActive) {
return lhsActive ? -1 : 1;
}
const lhsUpdatedAt = lhs.updatedAt ?? 0;
const rhsUpdatedAt = rhs.updatedAt ?? 0;
if (lhsUpdatedAt !== rhsUpdatedAt) {
return rhsUpdatedAt - lhsUpdatedAt;
}
return lhs.name.localeCompare(rhs.name, undefined, { sensitivity: "base" });
}
function automationScheduleSummary(kind: string, rrule: string | null): string {
if (!rrule) {
return kind === "cron" ? "Scheduled" : kind;
}
const parsed = Object.fromEntries(
rrule
.replace(/^RRULE:/i, "")
.split(";")
.map((part) => {
const [key, value] = part.split("=");
return [key, value];
})
.filter(([key, value]) => key && value)
);
const frequency = parsed.FREQ;
const interval = Number(parsed.INTERVAL ?? "1");
const time = formattedTime(parsed.BYHOUR, parsed.BYMINUTE);
if (frequency === "HOURLY") {
const base = interval > 1 ? `Every ${interval} hours` : "Hourly";
return parsed.BYMINUTE !== undefined
? `${base} at minute ${String(parsed.BYMINUTE).padStart(2, "0")}`
: base;
}
if (frequency === "DAILY") {
return time ? `Daily at ${time}` : "Daily";
}
if (frequency === "WEEKLY") {
const days = daySummary(parsed.BYDAY);
if (days && time) {
return `Weekly ${days} at ${time}`;
}
if (days) {
return `Weekly ${days}`;
}
return time ? `Weekly at ${time}` : "Weekly";
}
return rrule.replace(/^RRULE:/i, "");
}
function formattedTime(hour: string | undefined, minute: string | undefined): string | null {
if (hour === undefined) {
return null;
}
const hourValue = Number(hour);
const minuteValue = Number(minute ?? "0");
if (!Number.isFinite(hourValue) || !Number.isFinite(minuteValue)) {
return null;
}
const period = hourValue >= 12 ? "PM" : "AM";
const displayHour = hourValue % 12 === 0 ? 12 : hourValue % 12;
return `${displayHour}:${String(minuteValue).padStart(2, "0")} ${period}`;
}
function daySummary(value: string | undefined): string | null {
if (!value) {
return null;
}
const names = value
.split(",")
.map((day) => DAY_NAMES[day])
.filter(Boolean);
if (names.length === 7) {
return "every day";
}
if (names.length === 1) {
return names[0] ?? null;
}
return names.join(", ");
}
function promptPreview(prompt: string): string {
const compact = prompt.replace(/\s+/g, " ").trim();
if (compact.length <= 180) {
return compact;
}
return `${compact.slice(0, 177).trimEnd()}...`;
}
async function uniqueAutomationId(root: string, base: string): Promise<string> {
const cleanBase = base || "automation";
for (let suffix = 0; suffix < 100; suffix += 1) {
const id = suffix === 0 ? cleanBase : `${cleanBase}-${suffix + 1}`;
try {
await readFile(path.join(root, id, "automation.toml"), "utf8");
} catch {
return id;
}
}
return `${cleanBase}-${Date.now()}`;
}
function slugify(value: string): string {
return value
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 64);
}
function normalizedStatus(value: string | null | undefined): string {
const trimmed = value?.trim().toUpperCase();
return trimmed === "PAUSED" ? "PAUSED" : "ACTIVE";
}
function optionalTomlString(key: string, value: string | null | undefined): string | null {
const trimmed = value?.trim();
return trimmed ? `${key} = ${tomlString(trimmed)}` : null;
}
function tomlString(value: string): string {
return `"${value
.replace(/\\/g, "\\\\")
.replace(/"/g, "\\\"")
.replace(/\n/g, "\\n")
.replace(/\r/g, "\\r")
.replace(/\t/g, "\\t")}"`;
}
function tomlStringArray(values: string[]): string {
return `[${values.map(tomlString).join(", ")}]`;
}
function stringValue(value: TomlValue | undefined): string | null {
return typeof value === "string" ? value : null;
}
function stringArrayValue(value: TomlValue | undefined): string[] {
return Array.isArray(value) ? value : [];
}
function numberValue(value: TomlValue | undefined): number | null {
return typeof value === "number" ? value : null;
}
import { existsSync, readFileSync, statSync } from "node:fs";
import { homedir } from "node:os";
import path from "node:path";
type CodexGlobalState = {
"electron-workspace-root-labels"?: Record<string, unknown>;
};
let cachedLabels: Map<string, string> | null = null;
let cachedMtimeMS: number | null = null;
let cachedPath: string | null = null;
function codexGlobalStatePath(): string {
return process.env.CODEX_GLOBAL_STATE_PATH?.trim()
|| path.join(homedir(), ".codex", ".codex-global-state.json");
}
function normalizePath(value: string | null | undefined): string {
return path.resolve(value?.trim() || "/");
}
function readCodexWorkspaceRootLabels(): Map<string, string> {
const filePath = codexGlobalStatePath();
if (!existsSync(filePath)) {
cachedLabels = new Map();
cachedMtimeMS = null;
cachedPath = filePath;
return cachedLabels;
}
try {
const stat = statSync(filePath);
if (cachedLabels && cachedMtimeMS === stat.mtimeMs && cachedPath === filePath) {
return cachedLabels;
}
const parsed = JSON.parse(readFileSync(filePath, "utf8")) as CodexGlobalState;
const rawLabels = parsed["electron-workspace-root-labels"] ?? {};
const labels = new Map<string, string>();
for (const [root, label] of Object.entries(rawLabels)) {
if (typeof label !== "string") {
continue;
}
const normalizedLabel = label.trim();
if (!normalizedLabel) {
continue;
}
labels.set(normalizePath(root), normalizedLabel);
}
cachedLabels = labels;
cachedMtimeMS = stat.mtimeMs;
cachedPath = filePath;
return labels;
} catch {
cachedLabels = new Map();
cachedMtimeMS = null;
cachedPath = filePath;
return cachedLabels;
}
}
export function codexProjectNameForPath(value: string | null | undefined): string | null {
const normalizedPath = normalizePath(value);
return readCodexWorkspaceRootLabels().get(normalizedPath) ?? null;
}
+2
-2
{
"name": "codex-voice-remote-bridge",
"version": "0.1.5",
"version": "0.1.6",
"lockfileVersion": 3,

@@ -9,3 +9,3 @@ "requires": true,

"name": "codex-voice-remote-bridge",
"version": "0.1.5",
"version": "0.1.6",
"dependencies": {

@@ -12,0 +12,0 @@ "dotenv": "^16.6.1",

{
"name": "codex-voice-remote-bridge",
"version": "0.1.5",
"version": "0.1.6",
"private": true,

@@ -5,0 +5,0 @@ "type": "module",

import test from "node:test";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { BridgeServer } from "./bridgeServer.js";
import { BridgeServer, readGitBranchStatus } from "./bridgeServer.js";
import { createCodexAutomation, listCodexAutomations, parseCodexAutomationToml } from "./codexAutomations.js";
import type {

@@ -179,2 +184,31 @@ BackendSummary,

test("git branch status reports local branches for toolbar branch switching", () => {
const repo = mkdtempSync(path.join(tmpdir(), "helm-git-branches-"));
execFileSync("git", ["init", "-b", "main"], { cwd: repo, stdio: "ignore" });
execFileSync("git", ["config", "user.email", "helm@example.com"], { cwd: repo, stdio: "ignore" });
execFileSync("git", ["config", "user.name", "Helm Test"], { cwd: repo, stdio: "ignore" });
writeFileSync(path.join(repo, "README.md"), "helm\n");
execFileSync("git", ["add", "README.md"], { cwd: repo, stdio: "ignore" });
execFileSync("git", ["commit", "-m", "init"], { cwd: repo, stdio: "ignore" });
execFileSync("git", ["branch", "feature/toolbar"], { cwd: repo, stdio: "ignore" });
assert.deepEqual(readGitBranchStatus(repo), {
cwd: repo,
isRepository: true,
currentBranch: "main",
branches: ["feature/toolbar", "main"],
});
});
test("git branch status treats non-repositories as unavailable", () => {
const directory = mkdtempSync(path.join(tmpdir(), "helm-no-git-"));
assert.deepEqual(readGitBranchStatus(directory), {
cwd: directory,
isRepository: false,
currentBranch: null,
branches: [],
});
});
test("running thread summaries synthesize a waiting preview when detail is blank", () => {

@@ -258,2 +292,97 @@ const server = new BridgeServer() as unknown as BridgeServerInternals;

test("Codex automation parser extracts schedule and execution metadata", () => {
const automation = parseCodexAutomationToml(`
version = 1
id = "performance-audit"
kind = "cron"
name = "Performance audit"
prompt = "Audit performance regressions.\\nReport measurements."
status = "ACTIVE"
rrule = "RRULE:FREQ=WEEKLY;BYHOUR=8;BYMINUTE=0;BYDAY=MO"
model = "gpt-5.4"
reasoning_effort = "medium"
execution_environment = "worktree"
cwds = ["/Users/devlin/GitHub/prediction-markets-bot"]
created_at = 1776825539662
updated_at = 1776825539662
`, "/tmp/automation.toml");
assert.equal(automation?.id, "performance-audit");
assert.equal(automation?.name, "Performance audit");
assert.equal(automation?.status, "ACTIVE");
assert.equal(automation?.scheduleSummary, "Weekly Monday at 8:00 AM");
assert.equal(automation?.cwd, "/Users/devlin/GitHub/prediction-markets-bot");
assert.equal(automation?.prompt, "Audit performance regressions.\nReport measurements.");
});
test("Codex automation list reads automation directories and sorts active entries first", async () => {
const root = mkdtempSync(path.join(tmpdir(), "helm-automations-"));
mkdirSync(path.join(root, "paused-task"));
mkdirSync(path.join(root, "active-task"));
writeFileSync(path.join(root, "paused-task", "automation.toml"), `
id = "paused-task"
kind = "cron"
name = "Paused task"
prompt = "Paused"
status = "PAUSED"
updated_at = 200
`);
writeFileSync(path.join(root, "active-task", "automation.toml"), `
id = "active-task"
kind = "cron"
name = "Active task"
prompt = "Active"
status = "ACTIVE"
updated_at = 100
`);
const previous = process.env.CODEX_AUTOMATIONS_DIR;
process.env.CODEX_AUTOMATIONS_DIR = root;
try {
assert.deepEqual(
(await listCodexAutomations()).map((automation) => automation.id),
["active-task", "paused-task"]
);
} finally {
if (previous === undefined) {
delete process.env.CODEX_AUTOMATIONS_DIR;
} else {
process.env.CODEX_AUTOMATIONS_DIR = previous;
}
}
});
test("Codex automation creation writes parseable automation files", async () => {
const root = mkdtempSync(path.join(tmpdir(), "helm-create-automation-"));
const previous = process.env.CODEX_AUTOMATIONS_DIR;
process.env.CODEX_AUTOMATIONS_DIR = root;
try {
const automation = await createCodexAutomation({
name: "Daily mobile audit",
prompt: "Check the mobile app.",
rrule: "RRULE:FREQ=DAILY;BYHOUR=9;BYMINUTE=0",
model: "gpt-5.4",
reasoningEffort: "medium",
executionEnvironment: "local",
cwd: "/Users/devlin/GitHub/helm-dev",
status: "ACTIVE",
});
assert.equal(automation.id, "daily-mobile-audit");
assert.equal(automation.name, "Daily mobile audit");
assert.equal(automation.scheduleSummary, "Daily at 9:00 AM");
assert.equal(automation.cwd, "/Users/devlin/GitHub/helm-dev");
assert.deepEqual(
(await listCodexAutomations()).map((entry) => entry.id),
["daily-mobile-audit"]
);
} finally {
if (previous === undefined) {
delete process.env.CODEX_AUTOMATIONS_DIR;
} else {
process.env.CODEX_AUTOMATIONS_DIR = previous;
}
}
});
test("idle thread preview merge prefers fresh detail over stale fallback text", () => {

@@ -1803,2 +1932,57 @@ const server = new BridgeServer() as unknown as BridgeServerInternals;

test("oversized turns preserve the latest plan item for pinned task lists", () => {
const items: ThreadDetailItem[] = [
threadItem({
id: "user-1",
type: "userMessage",
title: "User message",
rawText: "start",
detail: "start",
}),
threadItem({
id: "plan-1",
type: "plan",
title: "2 out of 5 tasks completed",
rawText: [
"2 out of 5 tasks completed",
"✓ Rank replay blockers",
"✓ Inspect diagnostics",
"◉ Patch passive completion hold gate",
"□ Run focused replay/tests",
"□ Commit and push",
].join("\n"),
detail: [
"2 out of 5 tasks completed",
"✓ Rank replay blockers",
"✓ Inspect diagnostics",
"◉ Patch passive completion hold gate",
"□ Run focused replay/tests",
"□ Commit and push",
].join("\n"),
}),
];
for (let index = 0; index < 30; index += 1) {
items.push(threadItem({
id: `command-${index}`,
type: "commandExecution",
title: "Terminal",
rawText: `command output ${index}`,
detail: `command output ${index}`,
}));
}
const compacted = compactTurn({
id: "turn-1",
status: "running",
error: null,
items,
});
assert.equal(compacted.items[0]?.id, "user-1");
assert.equal(compacted.items.some((item) => item.id === "plan-1"), true);
assert.equal(compacted.items.at(-1)?.id, "command-29");
assert.ok(compacted.items.length <= 24);
});
test("live runtime tail is appended as the newest turn without lowering detail timestamp", () => {

@@ -1805,0 +1989,0 @@ const detail: ThreadDetail = {

import test from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import net from "node:net";
import { tmpdir } from "node:os";
import path from "node:path";
import { CodexAppServerClient, currentPromptDraftFromTerminalTail } from "./codexAppServerClient.js";
import { codexProjectNameForPath } from "./codexProjectNames.js";
import type { JSONValue } from "./types.js";
type TestStartTurnOptions = {
deliveryMode?: "queue" | "steer" | "interrupt";
imageAttachments?: Array<{ path: string; filename?: string; mimeType?: string }>;
fileAttachments?: Array<{ path: string; filename?: string; mimeType?: string }>;
};
type TestQueuedFollowUp = {
id: string;
text: string;
context: {
prompt: string;
addedFiles: JSONValue[];
fileAttachments: JSONValue[];
commentAttachments: JSONValue[];
ideContext: JSONValue | null;
imageAttachments: JSONValue[];
workspaceRoots: string[];
collaborationMode: JSONValue | null;
};
cwd: string | null;
createdAt: number;
};
type CodexClientPrivateHooks = {

@@ -38,2 +66,3 @@ localThreadReadFallback(threadId: string, includeTurns: boolean): Promise<JSONValue | undefined>;

sourceKind?: string | null;
projectName?: string | null;
updatedAt?: number;

@@ -75,4 +104,143 @@ name: string | null;

shouldPreferShellRelayFirst(threadId: string): Promise<boolean>;
setModelAndReasoningViaCodexDesktopIpc(
threadId: string,
model: string,
reasoningEffort: string | null
): Promise<JSONValue | undefined>;
enqueueTurnViaCodexDesktopIpc(
threadId: string,
text: string,
options: TestStartTurnOptions,
thread: { cwd: string; sourceKind?: string | null }
): Promise<JSONValue | undefined>;
startTurnViaCodexDesktopIpc(
threadId: string,
text: string,
options: TestStartTurnOptions,
baseline: unknown
): Promise<JSONValue | undefined>;
codexDesktopQueuedFollowUpsWithAppendedMessage(
currentMessages: TestQueuedFollowUp[],
message: TestQueuedFollowUp
): TestQueuedFollowUp[];
};
function encodeRawServerTextFrame(text: string): Buffer {
const payload = Buffer.from(text, "utf8");
if (payload.length < 126) {
return Buffer.concat([Buffer.from([0x81, payload.length]), payload]);
}
if (payload.length < 65_536) {
const header = Buffer.alloc(4);
header[0] = 0x81;
header[1] = 126;
header.writeUInt16BE(payload.length, 2);
return Buffer.concat([header, payload]);
}
const header = Buffer.alloc(10);
header[0] = 0x81;
header[1] = 127;
header.writeBigUInt64BE(BigInt(payload.length), 2);
return Buffer.concat([header, payload]);
}
function decodeRawClientTextFrame(buffer: Buffer): { text: string; consumed: number } | null {
if (buffer.length < 2) {
return null;
}
const firstByte = buffer[0]!;
const secondByte = buffer[1]!;
const opcode = firstByte & 0x0f;
let length = secondByte & 0x7f;
let offset = 2;
if (length === 126) {
if (buffer.length < offset + 2) {
return null;
}
length = buffer.readUInt16BE(offset);
offset += 2;
} else if (length === 127) {
if (buffer.length < offset + 8) {
return null;
}
length = Number(buffer.readBigUInt64BE(offset));
offset += 8;
}
const masked = (secondByte & 0x80) !== 0;
if (!masked || opcode !== 1) {
throw new Error("expected masked text frame");
}
if (buffer.length < offset + 4 + length) {
return null;
}
const mask = buffer.subarray(offset, offset + 4);
offset += 4;
const payload = Buffer.from(buffer.subarray(offset, offset + length));
for (let index = 0; index < payload.length; index += 1) {
payload[index] = payload[index]! ^ mask[index % 4]!;
}
return { text: payload.toString("utf8"), consumed: offset + length };
}
test("Codex app-server client initializes over raw unix socket transport", async (t) => {
if (process.platform === "win32") {
return;
}
const directory = mkdtempSync(path.join(tmpdir(), "codex-app-server-unix-"));
const socketPath = path.join(directory, "app.sock");
const server = net.createServer((socket) => {
let buffer = Buffer.alloc(0);
socket.on("data", (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
const frame = decodeRawClientTextFrame(buffer);
if (!frame) {
return;
}
buffer = buffer.subarray(frame.consumed);
const request = JSON.parse(frame.text) as {
id: string | number;
method?: string;
};
assert.equal(request.method, "initialize");
socket.write(encodeRawServerTextFrame(JSON.stringify({
id: request.id,
result: {
userAgent: "fake-codex",
codexHome: directory,
platformFamily: "unix",
platformOs: "macos",
},
})), () => {
socket.destroy();
});
});
});
t.after(() => {
server.close();
rmSync(directory, { recursive: true, force: true });
});
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(socketPath, () => {
server.off("error", reject);
resolve();
});
});
const client = new CodexAppServerClient(`unix://${socketPath}`);
await client.connect();
});
test("Codex CLI thread reads use local rollout before app-server", async () => {

@@ -190,2 +358,128 @@ const client = new CodexAppServerClient("ws://127.0.0.1:0");

test("queued Codex desktop turn keeps queue mode when an image is attached", async () => {
const client = new CodexAppServerClient("ws://127.0.0.1:0");
const hooks = client as unknown as CodexClientPrivateHooks;
let queuedText: string | null = null;
let startTurnCalls = 0;
hooks.loadThreadDeliverySummary = async () => ({
sourceKind: "vscode",
status: "running",
});
hooks.readThreadDeliverySnapshot = async () => ({
hasTurnData: true,
turnCount: 3,
matchingUserTextCount: 0,
updatedAt: 123_000,
threadStatus: "running",
activeTurnId: "turn-3",
});
hooks.enqueueTurnViaCodexDesktopIpc = async (_threadId, text) => {
queuedText = text;
return {
ok: true,
mode: "codexDesktopIpcQueuedFollowUpBroadcast",
threadId: "thread-1",
};
};
hooks.startTurnViaCodexDesktopIpc = async () => {
startTurnCalls += 1;
throw new Error("queue with image must not start an immediate desktop turn");
};
const result = await client.startTurn("thread-1", "Use the screenshot", {
deliveryMode: "queue",
imageAttachments: [
{
path: "/tmp/helm-mobile/camera-roll-1.jpg",
filename: "camera-roll-1.jpg",
mimeType: "image/jpeg",
},
],
});
assert.equal(startTurnCalls, 0);
assert.match(queuedText ?? "", /Use the screenshot/);
assert.match(queuedText ?? "", /camera-roll-1\.jpg/);
assert.match(queuedText ?? "", /\/tmp\/helm-mobile\/camera-roll-1\.jpg/);
assert.deepEqual(result, {
ok: true,
mode: "codexDesktopIpcQueuedFollowUpBroadcast",
threadId: "thread-1",
});
});
test("Codex desktop model update uses direct IPC for shared desktop threads", async () => {
const client = new CodexAppServerClient("ws://127.0.0.1:0");
const hooks = client as unknown as CodexClientPrivateHooks;
let ipcCall: { threadId: string; model: string; reasoningEffort: string | null } | null = null;
hooks.loadThreadDeliverySummary = async () => ({
sourceKind: "vscode",
status: "idle",
});
hooks.setModelAndReasoningViaCodexDesktopIpc = async (threadId, model, reasoningEffort) => {
ipcCall = { threadId, model, reasoningEffort };
return { ok: true };
};
const result = await client.setModelAndReasoning("thread-1", " gpt-5.5 ", " high ");
assert.deepEqual(ipcCall, {
threadId: "thread-1",
model: "gpt-5.5",
reasoningEffort: "high",
});
assert.deepEqual(result, { ok: true });
});
test("Codex model update rejects non-shared desktop threads", async () => {
const client = new CodexAppServerClient("ws://127.0.0.1:0");
const hooks = client as unknown as CodexClientPrivateHooks;
hooks.loadThreadDeliverySummary = async () => ({
sourceKind: "cli",
status: "idle",
});
hooks.setModelAndReasoningViaCodexDesktopIpc = async () => {
throw new Error("direct IPC should not run for CLI threads");
};
await assert.rejects(
() => client.setModelAndReasoning("thread-1", "gpt-5.5", "high"),
/shared Codex app session/
);
});
test("Codex desktop queued follow-up append coalesces immediate duplicate messages", () => {
const client = new CodexAppServerClient("ws://127.0.0.1:0");
const hooks = client as unknown as CodexClientPrivateHooks;
const message = (id: string, createdAt: number) => ({
id,
text: "queued from mobile",
context: {
prompt: "queued from mobile",
addedFiles: [],
fileAttachments: [],
commentAttachments: [],
ideContext: null,
imageAttachments: [],
workspaceRoots: ["/Users/devlin/GitHub/helm-dev"],
collaborationMode: null,
},
cwd: "/Users/devlin/GitHub/helm-dev",
createdAt,
});
const first = message("first", 1_000);
const duplicate = message("duplicate", 1_500);
const laterRepeat = message("later", 8_000);
const afterDuplicate = hooks.codexDesktopQueuedFollowUpsWithAppendedMessage([first], duplicate);
const afterLaterRepeat = hooks.codexDesktopQueuedFollowUpsWithAppendedMessage(afterDuplicate, laterRepeat);
assert.deepEqual(afterDuplicate.map((entry) => entry.id), ["first"]);
assert.deepEqual(afterLaterRepeat.map((entry) => entry.id), ["first", "later"]);
});
test("Codex CLI prompt draft extraction ignores styled placeholder text", () => {

@@ -356,2 +650,3 @@ const tail = [

cwd: "/tmp/project",
projectName: "Codex Project",
status: { type: "running" },

@@ -377,2 +672,3 @@ updatedAt: 456_000,

assert.equal(threads[0]?.preview, "Waiting for output...");
assert.equal(threads[0]?.projectName, "Codex Project");
assert.equal(threads[1]?.preview, "No activity yet.");

@@ -382,2 +678,28 @@ assert.equal(threads[0]?.sourceKind, "vscode");

test("Codex project names resolve from desktop workspace-root labels", () => {
const previousPath = process.env.CODEX_GLOBAL_STATE_PATH;
const folder = mkdtempSync(path.join(tmpdir(), "helm-codex-project-"));
const statePath = path.join(folder, ".codex-global-state.json");
process.env.CODEX_GLOBAL_STATE_PATH = statePath;
writeFileSync(
statePath,
JSON.stringify({
"electron-workspace-root-labels": {
"/Users/devlin/Documents/New project": "Wedding",
},
}),
"utf8"
);
try {
assert.equal(codexProjectNameForPath("/Users/devlin/Documents/New project"), "Wedding");
} finally {
if (previousPath === undefined) {
delete process.env.CODEX_GLOBAL_STATE_PATH;
} else {
process.env.CODEX_GLOBAL_STATE_PATH = previousPath;
}
}
});
test("recent title-only unknown app-server sessions stay idle", async () => {

@@ -384,0 +706,0 @@ const client = new CodexAppServerClient("ws://127.0.0.1:0");

@@ -109,2 +109,10 @@ import { AgentBackend, type StartThreadInput } from "./agentBackend.js";

async setModelAndReasoning(
threadId: string,
model: string,
reasoningEffort: string | null = null
): Promise<JSONValue | undefined> {
return await this.transport.setModelAndReasoning(threadId, model, reasoningEffort);
}
async interruptTurn(threadId: string): Promise<JSONValue | undefined> {

@@ -111,0 +119,0 @@ return await this.transport.interruptTurn(threadId);

@@ -18,2 +18,3 @@ import { randomUUID } from "node:crypto";

["thread-follower-interrupt-turn", 1],
["thread-follower-set-model-and-reasoning", 1],
["thread-follower-set-queued-follow-ups-state", 1],

@@ -175,2 +176,14 @@ ["thread-queued-followups-changed", 1],

async setModelAndReasoning(
threadId: string,
model: string,
reasoningEffort: string | null
): Promise<JSONValue | undefined> {
return await this.request("thread-follower-set-model-and-reasoning", {
conversationId: threadId,
model,
reasoningEffort,
});
}
dispose(): void {

@@ -177,0 +190,0 @@ this.rejectPendingResponses(new Error("Codex Desktop IPC client disposed"));

@@ -86,3 +86,10 @@ import test from "node:test";

const turn = turns[0] as { items?: Array<{ type?: string; tool?: string; contentItems?: string }> } | undefined;
const turn = turns[0] as {
items?: Array<{
type?: string;
tool?: string;
contentItems?: string;
imageAttachments?: Array<{ path?: string; mimeType?: string; filename?: string }>;
}>;
} | undefined;
const item = turn?.items?.[0];

@@ -92,4 +99,40 @@ assert.equal(item?.type, "dynamicToolCall");

assert.equal(item?.contentItems, imagePath);
assert.equal(item?.imageAttachments?.[0]?.path, imagePath);
assert.equal(item?.imageAttachments?.[0]?.mimeType, "image/jpeg");
assert.equal(item?.imageAttachments?.[0]?.filename, "dropped-image-1.jpg");
});
test("local rollout fallback emits generated image attachments", () => {
const imagePath = "/Users/devlin/.codex/generated_images/thread-1/ig_result.png";
const turns = parseCodexRolloutTurns(
[
rolloutLine({ type: "task_started", turn_id: "turn-1" }),
rolloutLine({
type: "image_generation_end",
turn_id: "turn-1",
saved_path: imagePath,
revised_prompt: "A Helm app icon.",
}),
].join("\n"),
"thread-1"
);
const turn = turns[0] as {
items?: Array<{
type?: string;
tool?: string;
contentItems?: string;
imageAttachments?: Array<{ path?: string; mimeType?: string; filename?: string; source?: string }>;
}>;
} | undefined;
const item = turn?.items?.[0];
assert.equal(item?.type, "dynamicToolCall");
assert.equal(item?.tool, "Generated Image");
assert.equal(item?.contentItems, imagePath);
assert.equal(item?.imageAttachments?.[0]?.path, imagePath);
assert.equal(item?.imageAttachments?.[0]?.mimeType, "image/png");
assert.equal(item?.imageAttachments?.[0]?.filename, "ig_result.png");
assert.equal(item?.imageAttachments?.[0]?.source, "image_generation");
});
test("local rollout fallback emits called MCP tool calls", () => {

@@ -165,2 +208,39 @@ const turns = parseCodexRolloutTurns(

test("local rollout fallback assigns tailed response plans to turn context", () => {
const turns = parseCodexRolloutTurns(
[
JSON.stringify({
type: "turn_context",
payload: { turn_id: "turn-1" },
}),
responseLine({
type: "function_call",
name: "update_plan",
arguments: JSON.stringify({
plan: [
{ step: "Rank replay blockers", status: "completed" },
{ step: "Patch passive completion hold gate", status: "in_progress" },
{ step: "Verify parity metrics", status: "pending" },
],
}),
}),
rolloutLine({
type: "exec_command_end",
turn_id: "turn-1",
command: "pytest",
status: "completed",
exit_code: 0,
}),
].join("\n"),
"thread-1"
) as Array<{ id: string; items: Array<{ type?: string; text?: string; command?: string }> }>;
assert.equal(turns[0]?.id, "turn-1");
assert.deepEqual(
turns[0]?.items.map((item) => item.type),
["plan", "commandExecution"]
);
assert.match(turns[0]?.items[0]?.text ?? "", /Patch passive completion hold gate/);
});
test("local rollout fallback orders turns by most recent activity", () => {

@@ -167,0 +247,0 @@ const turns = parseCodexRolloutTurns(

@@ -18,3 +18,3 @@ import { execFile } from "node:child_process";

import { findMatchingLaunchByCWD, findMatchingLaunchByThreadID } from "./runtimeLaunchRegistry.js";
import type { JSONValue, ThreadSummary } from "./types.js";
import type { JSONValue, ThreadImageAttachment, ThreadSummary } from "./types.js";

@@ -26,2 +26,4 @@ const execFileAsync = promisify(execFile);

const LOCAL_ROLLOUT_TAIL_READ_BYTES = 12 * 1024 * 1024;
const LOCAL_IMAGE_PATH_RE =
/(?:^|[\s("'`])((?:\/Users\/|\/private\/var\/|\/var\/folders\/|\/tmp\/)[^\s"'`)<]+?\.(?:png|jpe?g|webp|gif|heic|heif))(?:$|[\s"')>`])/giu;

@@ -452,2 +454,11 @@ type CodexThreadRow = {

const payload = objectValue(record?.payload);
if (record?.type === "turn_context") {
const contextTurnId = stringValue(payload?.turn_id);
if (contextTurnId) {
currentTurnId = contextTurnId;
getTurn(currentTurnId, index).status = "running";
}
continue;
}
const payloadType = stringValue(payload?.type);

@@ -627,2 +638,5 @@ if (!record || !payload || !payloadType) {

const imagePath = boundedHeadText(stringValue(payload.path));
const imageAttachments = imagePath
? imageAttachmentsFromPaths([imagePath], `local-view-image-${index}`, "view_image")
: [];
return {

@@ -634,4 +648,26 @@ id: `local-view-image-${index}`,

status: "completed",
...(imageAttachments.length > 0 ? { imageAttachments } : {}),
};
}
case "image_generation_end": {
const imagePath = boundedHeadText(
stringValue(payload.saved_path) ??
stringValue(payload.path) ??
stringValue(payload.output_path)
);
if (!imagePath) {
return null;
}
const imageAttachments = imageAttachmentsFromPaths([imagePath], `local-generated-image-${index}`, "image_generation");
return {
id: `local-generated-image-${index}`,
type: "dynamicToolCall",
tool: "Generated Image",
contentItems: imagePath,
status: "completed",
metadataSummary: boundedHeadText(stringValue(payload.revised_prompt), 280),
...(imageAttachments.length > 0 ? { imageAttachments } : {}),
};
}
case "mcp_tool_call_end": {

@@ -680,2 +716,15 @@ return {

}
case "dynamicToolCall": {
const tool = comparableRolloutText(stringValue(itemObject.tool));
const contentItems = comparableRolloutText(stringValue(itemObject.contentItems));
const imageAttachmentText = Array.isArray(itemObject.imageAttachments)
? itemObject.imageAttachments
.map((entry) => stringValue(objectValue(entry)?.path))
.filter((entry): entry is string => Boolean(entry))
.join(",")
: "";
return tool || contentItems || imageAttachmentText
? `tool:${tool ?? ""}:${contentItems ?? ""}:${imageAttachmentText}`
: null;
}
case "plan": {

@@ -760,2 +809,3 @@ const text = comparableRolloutText(stringValue(itemObject.text));

if (outputText) {
const imageAttachments = imageAttachmentsFromText(outputText, `local-agent-response-${index}`, "message");
return {

@@ -766,2 +816,3 @@ id: `local-agent-response-${index}`,

phase: "response_item",
...(imageAttachments.length > 0 ? { imageAttachments } : {}),
};

@@ -773,2 +824,67 @@ }

function imageAttachmentsFromText(
text: string,
idPrefix: string,
source: string
): ThreadImageAttachment[] {
const paths: string[] = [];
LOCAL_IMAGE_PATH_RE.lastIndex = 0;
for (const match of text.matchAll(LOCAL_IMAGE_PATH_RE)) {
if (match[1]) {
paths.push(match[1]);
}
}
return imageAttachmentsFromPaths(paths, idPrefix, source);
}
function imageAttachmentsFromPaths(
imagePaths: string[],
idPrefix: string,
source: string
): ThreadImageAttachment[] {
const seen = new Set<string>();
const attachments: ThreadImageAttachment[] = [];
for (const rawPath of imagePaths) {
const imagePath = rawPath.trim();
if (!imagePath || seen.has(imagePath)) {
continue;
}
const mimeType = imageMimeTypeForPath(imagePath);
if (!mimeType) {
continue;
}
seen.add(imagePath);
attachments.push({
id: `${idPrefix}-image-${attachments.length + 1}`,
path: imagePath,
mimeType,
filename: path.basename(imagePath),
source,
});
}
return attachments;
}
function imageMimeTypeForPath(imagePath: string): string | null {
switch (path.extname(imagePath).toLowerCase()) {
case ".png":
return "image/png";
case ".jpg":
case ".jpeg":
return "image/jpeg";
case ".webp":
return "image/webp";
case ".gif":
return "image/gif";
case ".heic":
return "image/heic";
case ".heif":
return "image/heif";
default:
return null;
}
}
function responseContentText(

@@ -775,0 +891,0 @@ value: JSONValue | undefined,

@@ -66,2 +66,7 @@ import { CodexAppServerClient } from "./codexAppServerClient.js";

): Promise<JSONValue | undefined>;
setModelAndReasoning(
threadId: string,
model: string,
reasoningEffort?: string | null
): Promise<JSONValue | undefined>;
interruptTurn(threadId: string): Promise<JSONValue | undefined>;

@@ -148,2 +153,10 @@ sendInput(threadId: string, input: string): Promise<JSONValue | undefined>;

async setModelAndReasoning(
threadId: string,
model: string,
reasoningEffort: string | null = null
): Promise<JSONValue | undefined> {
return await this.client.setModelAndReasoning(threadId, model, reasoningEffort);
}
async interruptTurn(threadId: string): Promise<JSONValue | undefined> {

@@ -150,0 +163,0 @@ return await this.client.interruptTurn(threadId);

@@ -24,3 +24,6 @@ import dotenv from "dotenv";

bridgePreferredURL: process.env.BRIDGE_PREFERRED_URL?.trim() || null,
codexAppServerUrl: optionalEnv("CODEX_APP_SERVER_URL", "ws://127.0.0.1:6060"),
codexAppServerUrl: optionalEnv(
"CODEX_APP_SERVER_URL",
`unix://${join(homedir(), ".local", "share", "helm", "codex-app-server.sock")}`
),
bridgePairingFile: optionalEnv(

@@ -27,0 +30,0 @@ "BRIDGE_PAIRING_FILE",

import test from "node:test";
import assert from "node:assert/strict";
import { parseBootedSimulators } from "./simulatorMirror.js";
import {
parseBootedSimulators,
parseSimulatorAccessibilitySnapshot,
} from "./simulatorMirror.js";

@@ -33,1 +36,45 @@ test("parseBootedSimulators returns only booted devices with runtime labels", () => {

});
test("parseSimulatorAccessibilitySnapshot normalizes visible simulator elements", () => {
const snapshot = parseSimulatorAccessibilitySnapshot([
"AXGroup\t\tgroup\t100\t200\t400\t800",
"AXButton\t\tCollapse task list\t120\t250\t80\t40",
"AXStaticText\t\tPlan Gabagool22 parity\t180\t220\t220\t30",
"AXButton\t\tOutside\t20\t20\t10\t10",
].join("\n"));
assert.deepEqual(snapshot.screenFrame, {
x: 100,
y: 200,
width: 400,
height: 800,
});
const button = snapshot.elements.find((element) => element.description === "Collapse task list");
assert.equal(snapshot.elements.length, 2);
assert.deepEqual(button?.normalizedFrame, {
x: 0.05,
y: 0.0625,
width: 0.2,
height: 0.05,
});
});
test("parseSimulatorAccessibilitySnapshot suppresses duplicate large action targets", () => {
const taskLabel = "4 out of 7 tasks completed, " + "task ".repeat(30);
const snapshot = parseSimulatorAccessibilitySnapshot([
"AXGroup\t\tgroup\t100\t200\t400\t800",
`AXButton\t\t${taskLabel}\t100\t200\t400\t200`,
`AXButton\t\t${taskLabel}\t110\t450\t380\t180`,
"AXButton\t\tSend to Codex\t450\t900\t40\t40",
"AXButton\t\tSend to Codex\t450\t950\t40\t40",
].join("\n"));
assert.equal(
snapshot.elements.filter((element) => element.description === taskLabel.trim()).length,
1
);
assert.equal(
snapshot.elements.filter((element) => element.description === "Send to Codex").length,
2
);
});

@@ -16,2 +16,23 @@ import { execFile } from "node:child_process";

export type SimulatorAccessibilityFrame = {
x: number;
y: number;
width: number;
height: number;
};
export type SimulatorAccessibilityElement = {
id: string;
role: string;
name: string | null;
description: string | null;
frame: SimulatorAccessibilityFrame;
normalizedFrame: SimulatorAccessibilityFrame;
};
export type SimulatorAccessibilitySnapshot = {
screenFrame: SimulatorAccessibilityFrame;
elements: SimulatorAccessibilityElement[];
};
export function parseBootedSimulators(output: string): BootedSimulator[] {

@@ -73,1 +94,297 @@ const simulators: BootedSimulator[] = [];

}
export async function listSimulatorAccessibilityElements(): Promise<SimulatorAccessibilitySnapshot> {
const { stdout } = await execFileAsync(
"swift",
["-e", SIMULATOR_ACCESSIBILITY_SWIFT],
{ maxBuffer: 4 * 1024 * 1024 }
);
return parseSimulatorAccessibilitySnapshot(stdout);
}
export function parseSimulatorAccessibilitySnapshot(output: string): SimulatorAccessibilitySnapshot {
const rawElements = output
.split(/\r?\n/)
.map((line) => line.trimEnd())
.filter(Boolean)
.map(parseAccessibilityLine)
.filter((element): element is Omit<SimulatorAccessibilityElement, "id" | "normalizedFrame"> => element !== null);
const screenFrame = simulatorScreenFrame(rawElements);
const seen = new Set<string>();
const seenLargeActionLabels = new Set<string>();
const elements: SimulatorAccessibilityElement[] = [];
for (const element of rawElements) {
if (!isSelectableAccessibilityElement(element)) {
continue;
}
const normalizedFrame = normalizeFrame(element.frame, screenFrame);
if (!normalizedFrame) {
continue;
}
const label = element.description ?? element.name ?? "";
const largeActionKey = largeActionDuplicateKey(element.role, label, normalizedFrame);
if (largeActionKey) {
if (seenLargeActionLabels.has(largeActionKey)) {
continue;
}
seenLargeActionLabels.add(largeActionKey);
}
const identity = [
element.role,
label,
Math.round(normalizedFrame.x * 1000),
Math.round(normalizedFrame.y * 1000),
Math.round(normalizedFrame.width * 1000),
Math.round(normalizedFrame.height * 1000),
].join("|");
if (seen.has(identity)) {
continue;
}
seen.add(identity);
elements.push({
...element,
id: `ax-${elements.length + 1}`,
normalizedFrame,
});
}
return {
screenFrame,
elements: elements
.sort((left, right) => frameArea(right.frame) - frameArea(left.frame))
.slice(0, 180),
};
}
function largeActionDuplicateKey(
role: string,
label: string,
frame: SimulatorAccessibilityFrame
): string | null {
if (role !== "AXButton") {
return null;
}
if (label.length < 80) {
return null;
}
if (frame.width < 0.5 || frame.height < 0.08) {
return null;
}
return `${role}|${label.replace(/\s+/g, " ").trim()}`;
}
function parseAccessibilityLine(line: string): Omit<SimulatorAccessibilityElement, "id" | "normalizedFrame"> | null {
const fields = line.split("\t");
if (fields.length !== 7) {
return null;
}
const [role, name, description, xText, yText, widthText, heightText] = fields;
const x = Number(xText);
const y = Number(yText);
const width = Number(widthText);
const height = Number(heightText);
if (
!role ||
!Number.isFinite(x) ||
!Number.isFinite(y) ||
!Number.isFinite(width) ||
!Number.isFinite(height) ||
width <= 0 ||
height <= 0
) {
return null;
}
return {
role,
name: emptyToNull(name),
description: emptyToNull(description),
frame: { x, y, width, height },
};
}
function simulatorScreenFrame(
elements: Array<Omit<SimulatorAccessibilityElement, "id" | "normalizedFrame">>
): SimulatorAccessibilityFrame {
const candidates = elements
.filter((element) => {
const ratio = element.frame.width / element.frame.height;
return element.role === "AXGroup" &&
element.frame.width >= 250 &&
element.frame.height >= 500 &&
ratio >= 0.35 &&
ratio <= 0.65;
})
.sort((left, right) => frameArea(right.frame) - frameArea(left.frame));
return candidates[0]?.frame ?? { x: 0, y: 0, width: 1, height: 1 };
}
function isSelectableAccessibilityElement(
element: Omit<SimulatorAccessibilityElement, "id" | "normalizedFrame">
): boolean {
if (frameArea(element.frame) < 36) {
return false;
}
if (element.description === "group" || element.description === "toolbar") {
return false;
}
return [
"AXButton",
"AXTextField",
"AXStaticText",
"AXHeading",
"AXImage",
"AXGenericElement",
"AXSlider",
"AXScrollArea",
].includes(element.role);
}
function normalizeFrame(
frame: SimulatorAccessibilityFrame,
screenFrame: SimulatorAccessibilityFrame
): SimulatorAccessibilityFrame | null {
const left = Math.max(frame.x, screenFrame.x);
const top = Math.max(frame.y, screenFrame.y);
const right = Math.min(frame.x + frame.width, screenFrame.x + screenFrame.width);
const bottom = Math.min(frame.y + frame.height, screenFrame.y + screenFrame.height);
const width = right - left;
const height = bottom - top;
if (width <= 0 || height <= 0) {
return null;
}
return {
x: (left - screenFrame.x) / screenFrame.width,
y: (top - screenFrame.y) / screenFrame.height,
width: width / screenFrame.width,
height: height / screenFrame.height,
};
}
function frameArea(frame: SimulatorAccessibilityFrame): number {
return frame.width * frame.height;
}
function emptyToNull(value: string | undefined): string | null {
const trimmed = value?.trim() ?? "";
return trimmed.length > 0 ? trimmed : null;
}
const SIMULATOR_ACCESSIBILITY_SWIFT = String.raw`
import AppKit
import ApplicationServices
struct ElementRecord {
let role: String
let title: String
let description: String
let frame: CGRect
}
func stringAttribute(_ element: AXUIElement, _ attribute: CFString) -> String {
var value: CFTypeRef?
guard AXUIElementCopyAttributeValue(element, attribute, &value) == .success else {
return ""
}
return value as? String ?? ""
}
func elementFrame(_ element: AXUIElement) -> CGRect? {
var positionRef: CFTypeRef?
var sizeRef: CFTypeRef?
guard AXUIElementCopyAttributeValue(element, kAXPositionAttribute as CFString, &positionRef) == .success,
AXUIElementCopyAttributeValue(element, kAXSizeAttribute as CFString, &sizeRef) == .success,
let positionValue = positionRef,
let sizeValue = sizeRef,
CFGetTypeID(positionValue) == AXValueGetTypeID(),
CFGetTypeID(sizeValue) == AXValueGetTypeID()
else {
return nil
}
var position = CGPoint.zero
var size = CGSize.zero
AXValueGetValue((positionValue as! AXValue), .cgPoint, &position)
AXValueGetValue((sizeValue as! AXValue), .cgSize, &size)
guard size.width > 0, size.height > 0 else { return nil }
return CGRect(origin: position, size: size)
}
func childElements(_ element: AXUIElement) -> [AXUIElement] {
for attribute in [kAXVisibleChildrenAttribute as CFString, kAXChildrenAttribute as CFString] {
var value: CFTypeRef?
if AXUIElementCopyAttributeValue(element, attribute, &value) == .success,
let children = value as? [AXUIElement] {
return children
}
}
return []
}
func clean(_ value: String) -> String {
value
.replacingOccurrences(of: "\t", with: " ")
.replacingOccurrences(of: "\n", with: " ")
.replacingOccurrences(of: "\r", with: " ")
}
guard let simulator = NSWorkspace.shared.runningApplications.first(where: {
$0.bundleIdentifier == "com.apple.iphonesimulator" || $0.localizedName == "Simulator"
}) else {
exit(2)
}
let application = AXUIElementCreateApplication(simulator.processIdentifier)
var windowsRef: CFTypeRef?
guard AXUIElementCopyAttributeValue(application, kAXWindowsAttribute as CFString, &windowsRef) == .success,
let windows = windowsRef as? [AXUIElement],
let window = windows.first
else {
exit(3)
}
var records: [ElementRecord] = []
var visitedCount = 0
func walk(_ element: AXUIElement, depth: Int) {
guard visitedCount <= 700, depth <= 10 else { return }
visitedCount += 1
let role = stringAttribute(element, kAXRoleAttribute as CFString)
let title = stringAttribute(element, kAXTitleAttribute as CFString)
let description = stringAttribute(element, kAXDescriptionAttribute as CFString)
if let frame = elementFrame(element) {
records.append(ElementRecord(role: role, title: title, description: description, frame: frame))
}
for child in childElements(element) {
walk(child, depth: depth + 1)
}
}
walk(window, depth: 0)
for record in records {
print([
clean(record.role),
clean(record.title),
clean(record.description),
String(Int(record.frame.minX.rounded())),
String(Int(record.frame.minY.rounded())),
String(Int(record.frame.width.rounded())),
String(Int(record.frame.height.rounded())),
].joined(separator: "\t"))
}
`;

@@ -66,2 +66,3 @@ export type JSONValue =

workspacePath?: string | null;
projectName?: string | null;
status: string;

@@ -135,2 +136,32 @@ updatedAt: number;

export type CodexAutomationSummary = {
id: string;
name: string;
kind: string;
status: string;
schedule: string | null;
scheduleSummary: string;
model: string | null;
reasoningEffort: string | null;
executionEnvironment: string | null;
cwd: string | null;
cwds: string[];
prompt: string;
promptPreview: string;
createdAt: number | null;
updatedAt: number | null;
sourcePath: string;
};
export type CreateCodexAutomationRequest = {
name: string;
prompt: string;
rrule: string;
model?: string | null;
reasoningEffort?: string | null;
executionEnvironment?: string | null;
cwd?: string | null;
status?: string | null;
};
export type ConversationEvent = {

@@ -154,2 +185,10 @@ method: string;

export type ThreadImageAttachment = {
id: string;
path: string;
mimeType: string | null;
filename: string | null;
source: string | null;
};
export type RuntimePhase = "idle" | "running" | "waitingApproval" | "blocked" | "completed" | "unknown";

@@ -207,2 +246,3 @@

exitCode: number | null;
imageAttachments?: ThreadImageAttachment[];
};

@@ -222,2 +262,3 @@

workspacePath?: string | null;
projectName?: string | null;
status: string;

@@ -224,0 +265,0 @@ updatedAt: number;

{
"name": "@devlln/helm",
"version": "0.1.5",
"version": "0.1.6",
"private": false,

@@ -5,0 +5,0 @@ "description": "Helm CLI bridge installer and runtime helpers.",

@@ -0,1 +1,5 @@

<p align="center">
<img src="https://raw.githubusercontent.com/DEVLlN/helm/main/docs/brand/helm-icon-v1/helm-app-icon-v1-1024.png" alt="Helm app icon" width="128">
</p>
# Helm

@@ -2,0 +6,0 @@

@@ -6,3 +6,21 @@ #!/usr/bin/env bash

RUNTIME_DIR="${HELM_PROTOTYPE_RUNTIME_DIR:-$ROOT_DIR/.runtime/prototype}"
BRIDGE_DIR="$ROOT_DIR/bridge"
if [[ -f "$BRIDGE_DIR/.env" ]]; then
set -a
# shellcheck disable=SC1091
source "$BRIDGE_DIR/.env"
set +a
fi
: "${CODEX_APP_SERVER_SOCKET:=$RUNTIME_DIR/codex-app-server.sock}"
LEGACY_CODEX_APP_SERVER_URL="ws://127.0.0.1:6060"
if [[ -z "${CODEX_APP_SERVER_URL:-}" ]] || {
[[ "${CODEX_APP_SERVER_URL:-}" == "$LEGACY_CODEX_APP_SERVER_URL" ]] \
&& [[ "${HELM_FORCE_LEGACY_CODEX_TCP:-0}" != "1" ]]
}; then
CODEX_APP_SERVER_URL="unix://$CODEX_APP_SERVER_SOCKET"
fi
export CODEX_APP_SERVER_URL
stop_pid_file() {

@@ -30,2 +48,7 @@ local label="$1"

APP_SERVER_SOCKET_PATH="$(python3 -c 'from urllib.parse import urlparse; import os; parsed=urlparse(os.environ.get("CODEX_APP_SERVER_URL", "")); print(parsed.path or parsed.netloc if parsed.scheme == "unix" else "")')"
if [[ -n "$APP_SERVER_SOCKET_PATH" && "$APP_SERVER_SOCKET_PATH" == "$RUNTIME_DIR/"* ]]; then
rm -f "$APP_SERVER_SOCKET_PATH"
fi
echo "[prototype] Local helm prototype processes stopped."

@@ -49,2 +49,3 @@ #!/usr/bin/env bash

print(f"Local bridge: {local_bridge_url}")
print(f"Codex app-server: {health.get('codexEndpoint', 'unknown')}")
print(f"Default backend: {health.get('defaultBackendId', 'unknown')}")

@@ -51,0 +52,0 @@ print(f"Pairing token hint: {pairing.get('tokenHint', 'unknown')}")

@@ -48,5 +48,13 @@ #!/usr/bin/env bash

: "${BRIDGE_PORT:=8787}"
: "${CODEX_APP_SERVER_URL:=ws://127.0.0.1:6060}"
: "${CODEX_APP_SERVER_SOCKET:=$RUNTIME_DIR/codex-app-server.sock}"
: "${HELM_RUNTIME_CAPTURE_FILE:=${HOME}/.config/helm/runtime-binary-capture.json}"
LEGACY_CODEX_APP_SERVER_URL="ws://127.0.0.1:6060"
if [[ -z "${CODEX_APP_SERVER_URL:-}" ]] || {
[[ "${CODEX_APP_SERVER_URL:-}" == "$LEGACY_CODEX_APP_SERVER_URL" ]] \
&& [[ "${HELM_FORCE_LEGACY_CODEX_TCP:-0}" != "1" ]]
}; then
CODEX_APP_SERVER_URL="unix://$CODEX_APP_SERVER_SOCKET"
fi
if command -v tailscale >/dev/null 2>&1; then

@@ -69,3 +77,3 @@ TAILSCALE_IP="$(tailscale ip -4 2>/dev/null | head -n 1 || true)"

export BRIDGE_HOST BRIDGE_PORT CODEX_APP_SERVER_URL
export BRIDGE_HOST BRIDGE_PORT CODEX_APP_SERVER_URL CODEX_APP_SERVER_SOCKET

@@ -77,4 +85,16 @@ if [[ "$LAN_MODE" -eq 1 ]]; then

LOCAL_BRIDGE_URL="http://127.0.0.1:${BRIDGE_PORT}"
APP_SERVER_HOST="$(python3 -c 'from urllib.parse import urlparse; import os; print(urlparse(os.environ["CODEX_APP_SERVER_URL"]).hostname or "127.0.0.1")')"
APP_SERVER_PORT="$(python3 -c 'from urllib.parse import urlparse; import os; parsed=urlparse(os.environ["CODEX_APP_SERVER_URL"]); print(parsed.port or 80)')"
APP_SERVER_SCHEME="$(python3 -c 'from urllib.parse import urlparse; import os; print(urlparse(os.environ["CODEX_APP_SERVER_URL"]).scheme)')"
APP_SERVER_SOCKET_PATH=""
APP_SERVER_HOST=""
APP_SERVER_PORT=""
if [[ "$APP_SERVER_SCHEME" == "unix" ]]; then
APP_SERVER_SOCKET_PATH="$(python3 -c 'from urllib.parse import urlparse; import os; parsed=urlparse(os.environ["CODEX_APP_SERVER_URL"]); print(parsed.path or parsed.netloc)')"
if [[ -z "$APP_SERVER_SOCKET_PATH" ]]; then
echo "CODEX_APP_SERVER_URL uses unix:// but does not include a socket path." >&2
exit 1
fi
else
APP_SERVER_HOST="$(python3 -c 'from urllib.parse import urlparse; import os; print(urlparse(os.environ["CODEX_APP_SERVER_URL"]).hostname or "127.0.0.1")')"
APP_SERVER_PORT="$(python3 -c 'from urllib.parse import urlparse; import os; parsed=urlparse(os.environ["CODEX_APP_SERVER_URL"]); print(parsed.port or 80)')"
fi

@@ -101,2 +121,24 @@ port_open() {

unix_socket_open() {
local socket_path="$1"
if [[ ! -S "$socket_path" ]]; then
return 1
fi
python3 - "$socket_path" <<'PY'
import socket
import sys
socket_path = sys.argv[1]
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(0.5)
try:
sock.connect(socket_path)
except OSError:
sys.exit(1)
finally:
sock.close()
PY
}
require_cmd() {

@@ -243,5 +285,19 @@ if ! command -v "$1" >/dev/null 2>&1; then

start_app_server() {
if port_open "$APP_SERVER_HOST" "$APP_SERVER_PORT"; then
echo "[prototype] Codex app-server already listening at $CODEX_APP_SERVER_URL"
return
if [[ "$APP_SERVER_SCHEME" == "unix" ]]; then
if unix_socket_open "$APP_SERVER_SOCKET_PATH"; then
echo "[prototype] Codex app-server already listening at $CODEX_APP_SERVER_URL"
return
fi
if [[ -e "$APP_SERVER_SOCKET_PATH" ]]; then
echo "[prototype] Removing stale Codex app-server socket at $APP_SERVER_SOCKET_PATH"
rm -f "$APP_SERVER_SOCKET_PATH"
fi
mkdir -p "$(dirname "$APP_SERVER_SOCKET_PATH")"
else
if port_open "$APP_SERVER_HOST" "$APP_SERVER_PORT"; then
echo "[prototype] Codex app-server already listening at $CODEX_APP_SERVER_URL"
return
fi
fi

@@ -257,5 +313,81 @@

current_bridge_codex_endpoint() {
curl -sf "$LOCAL_BRIDGE_URL/health" \
| python3 -c 'import json,sys; print(json.load(sys.stdin).get("codexEndpoint", ""))' \
2>/dev/null || true
}
wait_for_bridge_to_stop() {
for _ in $(seq 1 10); do
if ! curl -sf "$LOCAL_BRIDGE_URL/health" >/dev/null 2>&1; then
return 0
fi
sleep 0.2
done
return 1
}
stop_repo_bridge_process_on_port() {
local pids
pids="$(lsof -nP -tiTCP:"${BRIDGE_PORT}" -sTCP:LISTEN 2>/dev/null || true)"
if [[ -z "$pids" ]]; then
return 1
fi
local stopped=1
local pid
for pid in $pids; do
local command
command="$(ps -p "$pid" -o command= 2>/dev/null || true)"
local cwd
cwd="$(lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1)"
if [[ "$command" == *"$BRIDGE_DIR/dist/index.js"* ]] \
|| [[ "$command" == *"dist/index.js"* && "$cwd" == "$BRIDGE_DIR" ]]; then
echo "[prototype] Stopping helm bridge process on port ${BRIDGE_PORT} ($pid)"
kill "$pid" >/dev/null 2>&1 || true
stopped=0
fi
done
return "$stopped"
}
restart_managed_bridge_or_exit() {
local reason="$1"
local stopped=1
if stop_pid_file "helm bridge" "$RUNTIME_DIR/helm-bridge.pid"; then
stopped=0
fi
if wait_for_bridge_to_stop; then
return
fi
if stop_repo_bridge_process_on_port; then
stopped=0
if wait_for_bridge_to_stop; then
return
fi
fi
if [[ "$stopped" -eq 0 ]]; then
echo "[prototype] Existing bridge on port ${BRIDGE_PORT} did not stop; cannot restart for ${reason}." >&2
exit 1
fi
echo "[prototype] helm bridge is already reachable at $LOCAL_BRIDGE_URL but was not started by this prototype launcher." >&2
echo "[prototype] Stop the existing bridge on port ${BRIDGE_PORT}, then rerun scripts/prototype-up.sh so helm can use ${reason}." >&2
exit 1
}
start_bridge() {
if curl -sf "$LOCAL_BRIDGE_URL/health" >/dev/null 2>&1; then
if [[ "$TAILSCALE_ACTIVE" -eq 1 ]] && [[ "$BRIDGE_HOST" == "0.0.0.0" ]]; then
local running_codex_endpoint
running_codex_endpoint="$(current_bridge_codex_endpoint)"
if [[ -n "$running_codex_endpoint" && "$running_codex_endpoint" != "$CODEX_APP_SERVER_URL" ]]; then
echo "[prototype] Restarting helm bridge to use Codex app-server $CODEX_APP_SERVER_URL"
restart_managed_bridge_or_exit "Codex app-server endpoint $CODEX_APP_SERVER_URL"
elif [[ "$TAILSCALE_ACTIVE" -eq 1 ]] && [[ "$BRIDGE_HOST" == "0.0.0.0" ]]; then
local tailscale_bridge_url="http://${TAILSCALE_IP}:${BRIDGE_PORT}"

@@ -267,19 +399,3 @@ if curl -sf --connect-timeout 2 "$tailscale_bridge_url/health" >/dev/null 2>&1; then

if stop_pid_file "helm bridge" "$RUNTIME_DIR/helm-bridge.pid"; then
for _ in $(seq 1 10); do
if ! curl -sf "$LOCAL_BRIDGE_URL/health" >/dev/null 2>&1; then
break
fi
sleep 0.2
done
if curl -sf "$LOCAL_BRIDGE_URL/health" >/dev/null 2>&1; then
echo "[prototype] Existing bridge on port ${BRIDGE_PORT} did not stop; cannot restart for Tailscale binding." >&2
exit 1
fi
else
echo "[prototype] helm bridge is only reachable locally at $LOCAL_BRIDGE_URL." >&2
echo "[prototype] Stop the existing bridge on port ${BRIDGE_PORT}, then rerun scripts/prototype-up.sh so helm can bind for Tailscale." >&2
exit 1
fi
restart_managed_bridge_or_exit "Tailscale binding"
else

@@ -292,4 +408,11 @@ echo "[prototype] helm bridge already available at $LOCAL_BRIDGE_URL"

if curl -sf "$LOCAL_BRIDGE_URL/health" >/dev/null 2>&1; then
echo "[prototype] helm bridge already available at $LOCAL_BRIDGE_URL"
return
local running_codex_endpoint
running_codex_endpoint="$(current_bridge_codex_endpoint)"
if [[ -z "$running_codex_endpoint" || "$running_codex_endpoint" == "$CODEX_APP_SERVER_URL" ]]; then
echo "[prototype] helm bridge already available at $LOCAL_BRIDGE_URL"
return
fi
echo "[prototype] Restarting helm bridge to use Codex app-server $CODEX_APP_SERVER_URL"
restart_managed_bridge_or_exit "Codex app-server endpoint $CODEX_APP_SERVER_URL"
fi

@@ -303,2 +426,15 @@

)
if curl -sf "$LOCAL_BRIDGE_URL/health" >/dev/null 2>&1; then
local running_codex_endpoint
running_codex_endpoint="$(current_bridge_codex_endpoint)"
if [[ -z "$running_codex_endpoint" || "$running_codex_endpoint" == "$CODEX_APP_SERVER_URL" ]]; then
echo "[prototype] helm bridge became available at $LOCAL_BRIDGE_URL"
return
fi
echo "[prototype] Restarting helm bridge to use Codex app-server $CODEX_APP_SERVER_URL"
restart_managed_bridge_or_exit "Codex app-server endpoint $CODEX_APP_SERVER_URL"
fi
launch_detached \

@@ -305,0 +441,0 @@ "$RUNTIME_DIR/helm-bridge.pid" \

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display