large update

This commit is contained in:
ZacharyZhang-NY committed 2026-03-29 14:08:15 -04:00
1 parent 2ed952f19c
commit 65e85135c1
111 files changed
+3936 -2933

No files matched your search

@@ -0,0 +1,61 @@
export function parseCommaArgs(value: string): string[] {
return value
.split(",")
.map((item) => item.trim())
.filter(Boolean);
}
export function parseEnvVars(text: string): Record<string, string> {
const env: Record<string, string> = {};
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq <= 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
env[key] = value;
}
return env;
}
export function parseEnvBindings(bindings: unknown): Record<string, unknown> {
if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {};
const env: Record<string, unknown> = {};
for (const [key, raw] of Object.entries(bindings)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
if (typeof raw === "string") {
env[key] = { type: "plain", value: raw };
continue;
}
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
const rec = raw as Record<string, unknown>;
if (rec.type === "plain" && typeof rec.value === "string") {
env[key] = { type: "plain", value: rec.value };
continue;
}
if (rec.type === "secret_ref" && typeof rec.secretId === "string") {
env[key] = {
type: "secret_ref",
secretId: rec.secretId,
...(typeof rec.version === "number" || rec.version === "latest"
? { version: rec.version }
: {}),
};
}
}
return env;
}
export function parseJsonObject(text: string): Record<string, unknown> | null {
const trimmed = text.trim();
if (!trimmed) return null;
try {
const parsed = JSON.parse(trimmed);
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
return parsed as Record<string, unknown>;
} catch {
return null;
}
}
+47
View File
@@ -0,0 +1,47 @@
export function asRecord(value: unknown): Record<string, unknown> | null {
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}
export function safeJsonParse(text: string): unknown {
try {
return JSON.parse(text);
} catch {
return null;
}
}
export function errorText(value: unknown): string {
if (typeof value === "string") return value;
const rec = asRecord(value);
if (!rec) return "";
const msg =
(typeof rec.message === "string" && rec.message) ||
(typeof rec.error === "string" && rec.error) ||
(typeof rec.code === "string" && rec.code) ||
"";
if (msg) return msg;
try {
return JSON.stringify(rec);
} catch {
return "";
}
}
export function asString(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : fallback;
}
export function asNumber(value: unknown, fallback = 0): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
export function stringifyUnknown(value: unknown): string {
if (typeof value === "string") return value;
if (value === null || value === undefined) return "";
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
+33 -1
View File
@@ -1,4 +1,4 @@
import { spawn, type ChildProcess } from "node:child_process";
import { spawn, execSync, type ChildProcess } from "node:child_process";
import { constants as fsConstants, promises as fs, type Dirent } from "node:fs";
import path from "node:path";
import { StringDecoder } from "node:string_decoder";
@@ -354,6 +354,7 @@ export async function runChildProcess(
cwd: opts.cwd,
env: mergedEnv,
shell: false,
windowsHide: true,
stdio: [opts.stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
}) as ChildProcessWithEvents;
@@ -468,6 +469,37 @@ export async function runChildProcess(
});
}
/**
* Kill a process and all its descendants.
* Windows: `taskkill /T /F` kills the full process tree.
* Unix: SIGKILL the process directly (CLI tools are leaf processes).
*/
function killProcessTree(pid: number): void {
if (process.platform === "win32") {
try {
execSync(`taskkill /T /F /PID ${pid}`, { stdio: "ignore", timeout: 5000 });
} catch { /* process already exited */ }
} else {
try {
process.kill(pid, "SIGKILL");
} catch { /* already dead */ }
}
}
/**
* Force-kill every child process tracked in `runningProcesses` and clear the map.
* Called by the worker during shutdown to ensure no CLI processes survive app quit.
*/
export function shutdownAllRunning(): void {
for (const [runId, entry] of runningProcesses) {
const pid = entry.child.pid;
if (pid != null) {
killProcessTree(pid);
}
runningProcesses.delete(runId);
}
}
/**
* Create symlinks for managed skills in the target directory.
* Per D-13: uses 'junction' type on Windows to avoid Developer Mode requirement.
@@ -0,0 +1,61 @@
import type { AdapterSessionCodec } from "./types.js";
function readNonEmptyString(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
interface SessionCodecOptions {
/** Additional key aliases for sessionId beyond "sessionId" and "session_id" (e.g. "sessionID" for Gemini/OpenCode). */
extraSessionIdKeys?: string[];
}
/**
* Creates a standard session codec shared across adapters.
* All adapters use the same serialize/deserialize/getDisplayId logic,
* differing only in which keys they accept for sessionId.
*/
export function createStandardSessionCodec(options?: SessionCodecOptions): AdapterSessionCodec {
const extraKeys = options?.extraSessionIdKeys ?? [];
function readSessionId(record: Record<string, unknown>): string | null {
return (
readNonEmptyString(record.sessionId) ??
readNonEmptyString(record.session_id) ??
extraKeys.reduce<string | null>((found, key) => found ?? readNonEmptyString(record[key]), null)
);
}
function extractFields(record: Record<string, unknown>) {
const sessionId = readSessionId(record);
if (!sessionId) return null;
const cwd =
readNonEmptyString(record.cwd) ??
readNonEmptyString(record.workdir) ??
readNonEmptyString(record.folder);
const workspaceId = readNonEmptyString(record.workspaceId) ?? readNonEmptyString(record.workspace_id);
const repoUrl = readNonEmptyString(record.repoUrl) ?? readNonEmptyString(record.repo_url);
const repoRef = readNonEmptyString(record.repoRef) ?? readNonEmptyString(record.repo_ref);
return {
sessionId,
...(cwd ? { cwd } : {}),
...(workspaceId ? { workspaceId } : {}),
...(repoUrl ? { repoUrl } : {}),
...(repoRef ? { repoRef } : {}),
};
}
return {
deserialize(raw: unknown) {
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
return extractFields(raw as Record<string, unknown>);
},
serialize(params: Record<string, unknown> | null) {
if (!params) return null;
return extractFields(params);
},
getDisplayId(params: Record<string, unknown> | null) {
if (!params) return null;
return readSessionId(params);
},
};
}
+43
View File
@@ -0,0 +1,43 @@
import path from "node:path";
import type { AdapterEnvironmentCheck, AdapterEnvironmentTestResult } from "./types.js";
export function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] {
if (checks.some((check) => check.level === "error")) return "fail";
if (checks.some((check) => check.level === "warn")) return "warn";
return "pass";
}
export function isNonEmpty(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
export function firstNonEmptyLine(text: string): string {
return (
text
.split(/\r?\n/)
.map((line) => line.trim())
.find(Boolean) ?? ""
);
}
export function commandLooksLike(command: string, expected: string): boolean {
const base = path.basename(command).toLowerCase();
return base === expected || base === `${expected}.cmd` || base === `${expected}.exe`;
}
export function summarizeProbeDetail(stdout: string, stderr: string, parsedError?: string | null): string | null {
const raw = parsedError?.trim() || firstNonEmptyLine(stderr) || firstNonEmptyLine(stdout);
if (!raw) return null;
const clean = raw.replace(/\s+/g, " ").trim();
const max = 240;
return clean.length > max ? `${clean.slice(0, max - 1)}…` : clean;
}
export function normalizeEnv(input: unknown): Record<string, string> {
if (typeof input !== "object" || input === null || Array.isArray(input)) return {};
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
if (typeof value === "string") env[key] = value;
}
return env;
}
-24
View File
@@ -1,7 +1,3 @@
// ---------------------------------------------------------------------------
// Minimal adapter-facing interfaces (no drizzle dependency)
// ---------------------------------------------------------------------------
export interface AdapterAgent {
id: string;
companyId: string;
@@ -20,10 +16,6 @@ export interface AdapterRuntime {
taskKey: string | null;
}
// ---------------------------------------------------------------------------
// Execution types (moved from server/src/adapters/types.ts)
// ---------------------------------------------------------------------------
export interface UsageSummary {
inputTokens: number;
outputTokens: number;
@@ -137,10 +129,6 @@ export interface AdapterEnvironmentTestResult {
testedAt: string;
}
// ---------------------------------------------------------------------------
// Skill sync types -- adapter-level skill state and synchronization
// ---------------------------------------------------------------------------
export type AdapterSkillSyncMode = "unsupported" | "persistent" | "ephemeral";
export type AdapterSkillState =
@@ -246,10 +234,6 @@ export interface ServerAdapterModule {
) => Promise<HireApprovedHookResult>;
}
// ---------------------------------------------------------------------------
// UI types (moved from ui/src/adapters/types.ts)
// ---------------------------------------------------------------------------
export type TranscriptEntry =
| { kind: "assistant"; ts: string; text: string; delta?: boolean }
| { kind: "thinking"; ts: string; text: string; delta?: boolean }
@@ -264,19 +248,11 @@ export type TranscriptEntry =
export type StdoutLineParser = (line: string, ts: string) => TranscriptEntry[];
// ---------------------------------------------------------------------------
// CLI types (moved from cli/src/adapters/types.ts)
// ---------------------------------------------------------------------------
export interface CLIAdapterModule {
type: string;
formatStdoutEvent: (line: string, debug: boolean) => void;
}
// ---------------------------------------------------------------------------
// UI config form values (moved from ui/src/components/AgentConfigForm.tsx)
// ---------------------------------------------------------------------------
/** Skill reference passed from main process to worker for adapter injection. */
export interface ManagedSkillRef {
/** Skill slug used as directory name in the adapter's skill directory */