palmier 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import * as readline from "readline";
4
4
  import { spawnCommand, spawnStreamingCommand } from "../spawn-command.js";
5
5
  import { loadConfig } from "../config.js";
6
6
  import { connectNats } from "../nats-client.js";
7
- import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createResultFile, appendResultMessage, finalizeResultFrontmatter } from "../task.js";
7
+ import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createRunDir, appendRunMessage, readRunMessages, getRunDir } from "../task.js";
8
8
  import { getAgent } from "../agents/agent.js";
9
9
  import { getPlatform } from "../platform/index.js";
10
10
  import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
@@ -22,7 +22,7 @@ interface InvocationContext {
22
22
  agent: AgentTool;
23
23
  task: ParsedTask;
24
24
  taskDir: string;
25
- resultFileName: string;
25
+ runId: string;
26
26
  guiEnv: Record<string, string>;
27
27
  nc: NatsConnection | undefined;
28
28
  config: HostConfig;
@@ -51,8 +51,8 @@ async function invokeAgentWithRetry(
51
51
  while (true) {
52
52
  const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, retryPrompt, ctx.transientPermissions);
53
53
  const result = await spawnCommand(command, args, {
54
- cwd: ctx.taskDir,
55
- env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RESULT_FILE: ctx.resultFileName },
54
+ cwd: getRunDir(ctx.taskDir, ctx.runId),
55
+ env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
56
56
  echoStdout: true,
57
57
  resolveOnFailure: true,
58
58
  stdin,
@@ -118,7 +118,7 @@ async function invokeAgentWithRetry(
118
118
  /**
119
119
  * Strip [PALMIER_*] marker lines from agent output.
120
120
  */
121
- function stripPalmierMarkers(output: string): string {
121
+ export function stripPalmierMarkers(output: string): string {
122
122
  return output.split("\n").filter((l) => !l.startsWith("[PALMIER")).join("\n").trim();
123
123
  }
124
124
 
@@ -127,22 +127,24 @@ function stripPalmierMarkers(output: string): string {
127
127
  */
128
128
  async function appendAndNotify(
129
129
  ctx: InvocationContext,
130
- msg: Parameters<typeof appendResultMessage>[2],
130
+ msg: Parameters<typeof appendRunMessage>[2],
131
131
  ): Promise<void> {
132
- appendResultMessage(ctx.taskDir, ctx.resultFileName, msg);
133
- await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated" });
132
+ appendRunMessage(ctx.taskDir, ctx.runId, msg);
133
+ await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
134
134
  }
135
135
 
136
136
  /**
137
- * Find an existing RESULT file with running_state=started (created by the RPC handler).
137
+ * Find the latest run dir that has no status messages yet (just created by the RPC handler).
138
138
  */
139
- function findStartedResultFile(taskDir: string): string | null {
140
- const files = fs.readdirSync(taskDir).filter((f) => f.startsWith("RESULT-") && f.endsWith(".md"));
141
- for (const file of files) {
142
- const content = fs.readFileSync(path.join(taskDir, file), "utf-8");
143
- if (content.includes("running_state: started")) return file;
144
- }
145
- return null;
139
+ function findLatestPendingRunId(taskDir: string): string | null {
140
+ const dirs = fs.readdirSync(taskDir)
141
+ .filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
142
+ .sort();
143
+ if (dirs.length === 0) return null;
144
+ const latest = dirs[dirs.length - 1];
145
+ const messages = readRunMessages(taskDir, latest);
146
+ const hasStatus = messages.some((m) => m.role === "status");
147
+ return hasStatus ? null : latest;
146
148
  }
147
149
 
148
150
  /**
@@ -167,15 +169,11 @@ export async function runCommand(taskId: string): Promise<void> {
167
169
  let nc: NatsConnection | undefined;
168
170
  const taskName = task.frontmatter.name;
169
171
 
170
- // Check for an existing "started" result file (created by the RPC handler)
171
- const existingResult = findStartedResultFile(taskDir);
172
- const startTime = existingResult ? parseInt(existingResult.replace("RESULT-", "").replace(".md", ""), 10) : Date.now();
173
- const resultFileName = existingResult ?? createResultFile(taskDir, taskName, startTime);
174
-
175
- // Snapshot the task file at run time
176
- const taskSnapshotName = `TASK-${startTime}.md`;
177
- if (!fs.existsSync(path.join(taskDir, taskSnapshotName))) {
178
- fs.copyFileSync(path.join(taskDir, "TASK.md"), path.join(taskDir, taskSnapshotName));
172
+ // Use existing run dir if just created by RPC, otherwise create a new one
173
+ const existingRunId = findLatestPendingRunId(taskDir);
174
+ const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now());
175
+ if (!existingRunId) {
176
+ appendHistory(config.projectRoot, { task_id: taskId, run_id: runId });
179
177
  }
180
178
 
181
179
  const cleanup = async () => {
@@ -184,19 +182,12 @@ export async function runCommand(taskId: string): Promise<void> {
184
182
  }
185
183
  };
186
184
 
187
- if (!existingResult) {
188
- appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
189
- }
190
-
191
185
  try {
192
186
  nc = await connectNats(config);
193
187
 
194
- // Mark as started immediately
195
- await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName, resultFileName);
196
-
197
- // Status: started
198
- appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: "started" });
199
- await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated" });
188
+ await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName, runId);
189
+ appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "started" });
190
+ await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
200
191
 
201
192
  // If requires_confirmation, notify clients and wait
202
193
  if (task.frontmatter.requires_confirmation) {
@@ -205,22 +196,21 @@ export async function runCommand(taskId: string): Promise<void> {
205
196
  await publishConfirmResolved(nc, config, taskId, resolvedStatus);
206
197
  if (!confirmed) {
207
198
  console.log("Task aborted by user.");
208
- appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: "aborted" });
209
- finalizeResultFrontmatter(taskDir, resultFileName, { end_time: Date.now(), running_state: "aborted" });
210
- await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, resultFileName);
199
+ appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "aborted" });
200
+ await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, runId);
211
201
  await cleanup();
212
202
  return;
213
203
  }
214
204
  console.log("Task confirmed by user.");
215
- appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: "confirmation" });
216
- await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated" });
205
+ appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "confirmation" });
206
+ await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
217
207
  }
218
208
 
219
209
  // Shared invocation context
220
210
  const guiEnv = getPlatform().getGuiEnv();
221
211
  const agent = getAgent(task.frontmatter.agent);
222
212
  const ctx: InvocationContext = {
223
- agent, task, taskDir, resultFileName, guiEnv, nc, config, taskId,
213
+ agent, task, taskDir, runId, guiEnv, nc, config, taskId,
224
214
  transientPermissions: [],
225
215
  };
226
216
 
@@ -228,9 +218,8 @@ export async function runCommand(taskId: string): Promise<void> {
228
218
  // Command-triggered mode
229
219
  const result = await runCommandTriggeredMode(ctx);
230
220
  const outcome = resolveOutcome(taskDir, result.outcome);
231
- appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: outcome });
232
- finalizeResultFrontmatter(taskDir, resultFileName, { end_time: result.endTime, running_state: outcome });
233
- await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
221
+ appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
222
+ await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
234
223
  console.log(`Task ${taskId} completed (command-triggered).`);
235
224
  } else {
236
225
  // Standard execution — add user prompt as first message
@@ -242,23 +231,21 @@ export async function runCommand(taskId: string): Promise<void> {
242
231
 
243
232
  const result = await invokeAgentWithRetry(ctx, task);
244
233
  const outcome = resolveOutcome(taskDir, result.outcome);
245
- appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: outcome });
246
- finalizeResultFrontmatter(taskDir, resultFileName, { end_time: Date.now(), running_state: outcome });
247
- await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
234
+ appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
235
+ await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
248
236
  console.log(`Task ${taskId} completed.`);
249
237
  }
250
238
  } catch (err) {
251
239
  console.error(`Task ${taskId} failed:`, err);
252
240
  const outcome = resolveOutcome(taskDir, "failed");
253
241
  const errorMsg = err instanceof Error ? err.message : String(err);
254
- appendResultMessage(taskDir, resultFileName, {
242
+ appendRunMessage(taskDir, runId, {
255
243
  role: "assistant",
256
244
  time: Date.now(),
257
245
  content: errorMsg,
258
246
  });
259
- appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: outcome });
260
- finalizeResultFrontmatter(taskDir, resultFileName, { end_time: Date.now(), running_state: outcome });
261
- await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
247
+ appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
248
+ await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
262
249
  process.exitCode = 1;
263
250
  } finally {
264
251
  await cleanup();
@@ -284,8 +271,8 @@ async function runCommandTriggeredMode(
284
271
  console.log(`[command-triggered] Spawning: ${commandStr}`);
285
272
 
286
273
  const child = spawnStreamingCommand(commandStr, {
287
- cwd: ctx.taskDir,
288
- env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id },
274
+ cwd: getRunDir(ctx.taskDir, ctx.runId),
275
+ env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
289
276
  });
290
277
 
291
278
  let linesProcessed = 0;
@@ -297,7 +284,7 @@ async function runCommandTriggeredMode(
297
284
  let commandExited = false;
298
285
  let resolveWhenDone: (() => void) | undefined;
299
286
 
300
- const logPath = path.join(ctx.taskDir, "command-output.log");
287
+ const logPath = path.join(getRunDir(ctx.taskDir, ctx.runId), "command-output.log");
301
288
  function appendLog(line: string, agentOutput: string, outcome: string) {
302
289
  const entry = `[${new Date().toISOString()}] (${outcome}) input: ${line}\n${agentOutput}\n---\n`;
303
290
  fs.appendFileSync(logPath, entry, "utf-8");
@@ -404,7 +391,7 @@ async function publishTaskEvent(
404
391
  taskId: string,
405
392
  eventType: TaskRunningState,
406
393
  taskName?: string,
407
- resultFile?: string,
394
+ runId?: string,
408
395
  ): Promise<void> {
409
396
  writeTaskStatus(taskDir, {
410
397
  running_state: eventType,
@@ -414,7 +401,7 @@ async function publishTaskEvent(
414
401
 
415
402
  const payload: Record<string, unknown> = { event_type: "running-state", running_state: eventType };
416
403
  if (taskName) payload.name = taskName;
417
- if (resultFile) payload.result_file = resultFile;
404
+ if (runId) payload.run_id = runId;
418
405
  await publishHostEvent(nc, config.hostId, taskId, payload);
419
406
  }
420
407
 
@@ -4,7 +4,7 @@ import { loadConfig } from "../config.js";
4
4
  import { connectNats } from "../nats-client.js";
5
5
  import { createRpcHandler } from "../rpc-handler.js";
6
6
  import { startNatsTransport } from "../transports/nats-transport.js";
7
- import { getTaskDir, readTaskStatus, writeTaskStatus, appendHistory, parseTaskFile, appendResultMessage } from "../task.js";
7
+ import { getTaskDir, readTaskStatus, writeTaskStatus, parseTaskFile, appendRunMessage } from "../task.js";
8
8
  import { publishHostEvent } from "../events.js";
9
9
  import { getPlatform } from "../platform/index.js";
10
10
  import { detectAgents } from "../agents/agent.js";
@@ -17,46 +17,11 @@ const POLL_INTERVAL_MS = 30_000;
17
17
  const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
18
18
 
19
19
  /**
20
- * Mark a stuck task as failed: update status.json, write RESULT, append history,
21
- * and broadcast the failure event.
22
- */
23
- async function markTaskFailed(
24
- config: HostConfig,
25
- nc: NatsConnection | undefined,
26
- taskId: string,
27
- reason: string,
28
- ): Promise<void> {
29
- const taskDir = getTaskDir(config.projectRoot, taskId);
30
- const status = readTaskStatus(taskDir);
31
- if (!status || status.running_state !== "started") return;
32
-
33
- console.log(`[monitor] Task ${taskId} ${reason}, marking as failed.`);
34
- const endTime = Date.now();
35
- writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
36
-
37
- let taskName = taskId;
38
- try {
39
- const task = parseTaskFile(taskDir);
40
- taskName = task.frontmatter.name || taskId;
41
- } catch { /* use taskId as fallback */ }
42
-
43
- const resultFileName = `RESULT-${endTime}.md`;
44
- const content = `---\ntask_name: ${taskName}\nrunning_state: failed\nstart_time: ${status.time_stamp}\nend_time: ${endTime}\ntask_file: \n---\n\n`;
45
- fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
46
- appendResultMessage(taskDir, resultFileName, {
47
- role: "assistant",
48
- time: endTime,
49
- content: reason,
50
- });
51
- appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
52
-
53
- const payload: Record<string, unknown> = { event_type: "running-state", running_state: "failed", name: taskName };
54
- await publishHostEvent(nc, config.hostId, taskId, payload);
55
- }
56
-
57
- /**
58
- * Scan all tasks for any stuck in "start" state whose process is no longer alive.
20
+ * Scan all tasks for any stuck in "started" state whose process is no longer alive.
59
21
  * Uses the system scheduler (Task Scheduler / systemd) as the authoritative source.
22
+ *
23
+ * Since run.ts creates the RESULT file and history entry at start, we just need to
24
+ * finalize the existing RESULT file, append a failed status entry, and broadcast.
60
25
  */
61
26
  async function checkStaleTasks(
62
27
  config: HostConfig,
@@ -80,7 +45,35 @@ async function checkStaleTasks(
80
45
  // Ask the system scheduler if the task is still running
81
46
  if (platform.isTaskRunning(taskId)) continue;
82
47
 
83
- await markTaskFailed(config, nc, taskId, "Task process exited unexpectedly");
48
+ console.log(`[monitor] Task ${taskId} process exited unexpectedly, marking as failed.`);
49
+ const endTime = Date.now();
50
+ writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
51
+
52
+ // Find the latest run directory (created by run.ts at start)
53
+ const runId = fs.readdirSync(taskDir)
54
+ .filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
55
+ .sort()
56
+ .pop();
57
+
58
+ if (runId) {
59
+ appendRunMessage(taskDir, runId, {
60
+ role: "status",
61
+ time: endTime,
62
+ content: "",
63
+ type: "failed",
64
+ });
65
+ }
66
+
67
+ let taskName = taskId;
68
+ try {
69
+ taskName = parseTaskFile(taskDir).frontmatter.name || taskId;
70
+ } catch { /* use taskId as fallback */ }
71
+
72
+ await publishHostEvent(nc, config.hostId, taskId, {
73
+ event_type: "running-state",
74
+ running_state: "failed",
75
+ name: taskName,
76
+ });
84
77
  }
85
78
  }
86
79
 
@@ -5,6 +5,8 @@ import { execSync, exec } from "child_process";
5
5
  import { promisify } from "util";
6
6
  import type { PlatformService } from "./platform.js";
7
7
  import type { HostConfig, ParsedTask } from "../types.js";
8
+ import { loadConfig } from "../config.js";
9
+ import { getTaskDir, readTaskStatus } from "../task.js";
8
10
 
9
11
  const execAsync = promisify(exec);
10
12
 
@@ -230,20 +232,28 @@ WantedBy=timers.target
230
232
  }
231
233
 
232
234
  isTaskRunning(taskId: string): boolean {
235
+ // Check systemd first (for scheduled/on-demand runs)
233
236
  const serviceName = getServiceName(taskId);
234
237
  try {
235
- // is-active exits 0 only for "active". For oneshot services (Type=oneshot),
236
- // the state is "activating" while running, which exits non-zero.
237
- // Use show -p ActiveState to reliably get the state without exit code issues.
238
238
  const out = execSync(
239
239
  `systemctl --user show -p ActiveState --value ${serviceName}`,
240
240
  { encoding: "utf-8" },
241
241
  );
242
242
  const state = out.trim();
243
- return state === "active" || state === "activating";
244
- } catch {
245
- return false;
246
- }
243
+ if (state === "active" || state === "activating") return true;
244
+ } catch { /* service may not exist */ }
245
+
246
+ // Fall back to PID check (for follow-up runs spawned directly)
247
+ try {
248
+ const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
249
+ const status = readTaskStatus(taskDir);
250
+ if (status?.pid) {
251
+ process.kill(status.pid, 0); // signal 0 = check if process exists
252
+ return true;
253
+ }
254
+ } catch { /* process not running or config unavailable */ }
255
+
256
+ return false;
247
257
  }
248
258
 
249
259
  getGuiEnv(): Record<string, string> {
@@ -13,14 +13,6 @@ const DAEMON_TASK_NAME = "PalmierDaemon";
13
13
  const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
14
14
  const DAEMON_VBS_FILE = path.join(CONFIG_DIR, "daemon.vbs");
15
15
 
16
- /**
17
- * Build the /tr value for schtasks: a single string with quoted paths
18
- * so Task Scheduler can invoke node with the palmier script + subcommand.
19
- */
20
- function schtasksTr(...subcommand: string[]): string {
21
- const script = process.argv[1] || "palmier";
22
- return `"${process.execPath}" "${script}" ${subcommand.join(" ")}`;
23
- }
24
16
 
25
17
  const DOW_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
26
18
 
@@ -169,7 +161,15 @@ export class WindowsPlatform implements PlatformService {
169
161
  installTaskTimer(config: HostConfig, task: ParsedTask): void {
170
162
  const taskId = task.frontmatter.id;
171
163
  const tn = schtasksTaskName(taskId);
172
- const tr = schtasksTr("run", taskId);
164
+ const script = process.argv[1] || "palmier";
165
+
166
+ // Write a VBS launcher so the task runs without a visible console window
167
+ const vbsPath = path.join(CONFIG_DIR, `task-${taskId}.vbs`);
168
+ const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" run ${taskId}", 0, True`;
169
+ fs.writeFileSync(vbsPath, vbs, "utf-8");
170
+
171
+ const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
172
+ const tr = `"${wscript}" "${vbsPath}"`;
173
173
 
174
174
  // Build trigger XML elements
175
175
  const triggerElements: string[] = [];
@@ -213,6 +213,7 @@ export class WindowsPlatform implements PlatformService {
213
213
  } catch {
214
214
  // Task might not exist — that's fine
215
215
  }
216
+ try { fs.unlinkSync(path.join(CONFIG_DIR, `task-${taskId}.vbs`)); } catch { /* ignore */ }
216
217
  }
217
218
 
218
219
  async startTask(taskId: string): Promise<void> {
@@ -250,16 +251,38 @@ export class WindowsPlatform implements PlatformService {
250
251
  }
251
252
 
252
253
  isTaskRunning(taskId: string): boolean {
254
+ // Check Task Scheduler first (for scheduled/on-demand runs)
253
255
  const tn = schtasksTaskName(taskId);
254
256
  try {
255
257
  const out = execFileSync("schtasks", ["/query", "/tn", tn, "/fo", "CSV", "/nh"], {
256
258
  encoding: "utf-8",
257
259
  windowsHide: true,
258
260
  });
259
- return out.includes('"Running"');
260
- } catch {
261
- return false;
262
- }
261
+ if (out.includes('"Running"')) return true;
262
+ } catch { /* task may not exist in scheduler */ }
263
+
264
+ // Fall back to PID check (for follow-up runs spawned directly, not via schtasks)
265
+ try {
266
+ const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
267
+ const status = readTaskStatus(taskDir);
268
+ if (status?.pid) {
269
+ // tasklist exits 0 if the PID is found
270
+ execFileSync("tasklist", ["/fi", `PID eq ${status.pid}`, "/nh"], {
271
+ encoding: "utf-8",
272
+ windowsHide: true,
273
+ stdio: "pipe",
274
+ });
275
+ // tasklist always exits 0; check if output contains the PID
276
+ const out = execFileSync("tasklist", ["/fi", `PID eq ${status.pid}`, "/fo", "CSV", "/nh"], {
277
+ encoding: "utf-8",
278
+ windowsHide: true,
279
+ stdio: "pipe",
280
+ });
281
+ if (out.includes(`"${status.pid}"`)) return true;
282
+ }
283
+ } catch { /* ignore */ }
284
+
285
+ return false;
263
286
  }
264
287
 
265
288
  getGuiEnv(): Record<string, string> {